tool.cpp 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. /*
  2. TODO:
  3. * Make alternative models for animated characters and damaged buildings.
  4. * Make the custom rendering system able to render directly into a game with triangle culling and clipping.
  5. */
  6. #include <assert.h>
  7. #include <limits>
  8. #include <functional>
  9. #include "../../DFPSR/includeFramework.h"
  10. #include "sprite/spriteAPI.h"
  11. using namespace dsr;
  12. static constexpr float colorScale = 1.0f / 255.0f;
  13. static FVector4D pixelToVertexColor(const ColorRgbaI32& color) {
  14. return FVector4D(color.red * colorScale, color.green * colorScale, color.blue * colorScale, 1.0f);
  15. }
  16. static int createTriangle(Model& model, int part, int indexA, int indexB, int indexC, FVector4D colorA, FVector4D colorB, FVector4D colorC, bool flip = false) {
  17. if (flip) {
  18. int poly = model_addTriangle(model, part, indexB, indexA, indexC);
  19. model_setVertexColor(model, part, poly, 0, colorB);
  20. model_setVertexColor(model, part, poly, 1, colorA);
  21. model_setVertexColor(model, part, poly, 2, colorC);
  22. return poly;
  23. } else {
  24. int poly = model_addTriangle(model, part, indexA, indexB, indexC);
  25. model_setVertexColor(model, part, poly, 0, colorA);
  26. model_setVertexColor(model, part, poly, 1, colorB);
  27. model_setVertexColor(model, part, poly, 2, colorC);
  28. return poly;
  29. }
  30. }
  31. using TransformFunction = std::function<FVector3D(int pixelX, int pixelY, int displacement)>;
  32. // Returns the start point index for another side to weld against
  33. int createGridSide(Model& model, int part, const ImageU8& heightMap, const ImageRgbaU8& colorMap,
  34. const TransformFunction& transform, bool clipZero, bool mergeSides, bool flipDepth = false, bool flipFaces = false, int otherStartPointIndex = -1) {
  35. int startPointIndex = model_getNumberOfPoints(model);
  36. int mapWidth = image_getWidth(heightMap);
  37. int mapHeight = image_getHeight(heightMap);
  38. int flipScale = flipDepth ? -1 : 1;
  39. int columns = mergeSides ? mapWidth + 1 : mapWidth;
  40. // Create a part for the polygons
  41. for (int z = 0; z < mapHeight; z++) {
  42. for (int x = 0; x < columns; x++) {
  43. // Sample the height map and convert to world space
  44. int cx = x % mapWidth;
  45. int heightC = image_readPixel_border(heightMap, cx, z);
  46. // Add the point to the model
  47. if (x < mapWidth) {
  48. // Create a position from the 3D index
  49. model_addPoint(model, transform(x, z, heightC * flipScale));
  50. }
  51. if (x > 0 && z > 0) {
  52. // Create vertex data
  53. // A-B
  54. // |
  55. // D-C
  56. int px = x - 1;
  57. int cz = z;
  58. int pz = z - 1;
  59. // Sample previous heights
  60. int heightA = image_readPixel_border(heightMap, px, pz);
  61. int heightB = image_readPixel_border(heightMap, cx, pz);
  62. int heightD = image_readPixel_border(heightMap, px, cz);
  63. // Tell where to weld with another side's points
  64. bool weldA = otherStartPointIndex > -1 && heightA == 0;
  65. bool weldB = otherStartPointIndex > -1 && heightB == 0;
  66. bool weldC = otherStartPointIndex > -1 && heightC == 0;
  67. bool weldD = otherStartPointIndex > -1 && heightD == 0;
  68. // Get indices to points
  69. int indexA = (weldA ? otherStartPointIndex : startPointIndex) + px + pz * mapWidth;
  70. int indexB = (weldB ? otherStartPointIndex : startPointIndex) + cx + pz * mapWidth;
  71. int indexC = (weldC ? otherStartPointIndex : startPointIndex) + cx + cz * mapWidth;
  72. int indexD = (weldD ? otherStartPointIndex : startPointIndex) + px + cz * mapWidth;
  73. // Sample colors
  74. FVector4D colorA = pixelToVertexColor(image_readPixel_tile(colorMap, px, pz));
  75. FVector4D colorB = pixelToVertexColor(image_readPixel_tile(colorMap, cx, pz));
  76. FVector4D colorC = pixelToVertexColor(image_readPixel_tile(colorMap, cx, cz));
  77. FVector4D colorD = pixelToVertexColor(image_readPixel_tile(colorMap, px, cz));
  78. // Decide how to split triangles and which ones to display
  79. bool acSplit = false;
  80. bool skipFirst = false;
  81. bool skipSecond = false;
  82. if (heightA == 0 && heightC == 0) {
  83. // ABCD fan of ABC and ACD
  84. acSplit = true;
  85. if (heightB == 0) { skipFirst = true; }
  86. if (heightD == 0) { skipSecond = true; }
  87. } else if (heightB == 0 && heightD == 0) {
  88. // BCDA fan of ACD and BDA
  89. acSplit = false;
  90. if (heightC == 0) { skipFirst = true; }
  91. if (heightA == 0) { skipSecond = true; }
  92. } else {
  93. int cA = image_readPixel_tile(heightMap, cx - 2, cz - 2);
  94. int cB = image_readPixel_tile(heightMap, cx + 1, cz - 2);
  95. int cC = image_readPixel_tile(heightMap, cx + 1, cz + 1);
  96. int cD = image_readPixel_tile(heightMap, cx - 2, cz + 1);
  97. int diffAC = abs((cA + cC) - (heightA + heightC));
  98. int diffBD = abs((cB + cD) - (heightB + heightD));
  99. acSplit = diffBD > diffAC;
  100. }
  101. if (!clipZero) {
  102. skipFirst = false;
  103. skipSecond = false;
  104. }
  105. // Create a polygon
  106. if (!(skipFirst && skipSecond)) {
  107. if (acSplit) {
  108. if (!skipFirst) {
  109. createTriangle(model, part,
  110. indexA, indexB, indexC,
  111. colorA, colorB, colorC, flipFaces
  112. );
  113. }
  114. if (!skipSecond) {
  115. createTriangle(model, part,
  116. indexA, indexC, indexD,
  117. colorA, colorC, colorD, flipFaces
  118. );
  119. }
  120. } else {
  121. if (!skipFirst) {
  122. createTriangle(model, part,
  123. indexB, indexC, indexD,
  124. colorB, colorC, colorD, flipFaces
  125. );
  126. }
  127. if (!skipSecond) {
  128. createTriangle(model, part,
  129. indexB, indexD, indexA,
  130. colorB, colorD, colorA, flipFaces
  131. );
  132. }
  133. }
  134. }
  135. }
  136. }
  137. }
  138. return startPointIndex;
  139. }
  140. // clipZero:
  141. // Removing triangles from pixels with displacement zero.
  142. // Used for carving out non-square shapes using black height as the background.
  143. // mergeSides:
  144. // Connect vertices from the left side of the image with the right side using additional polygons.
  145. // Used for cylinder shapes to remove the seam where the sides meet.
  146. // mirror:
  147. // Create another instance of the height field with surfaces and displacement turned in the other direction.
  148. // weldNormals:
  149. // Merges normals between mirrored sides to let normals at displacement zero merge with the other side.
  150. // mirror must be active for this to have an effect, because there's no mirrored side to weld against otherwise.
  151. // clipZero must be active to hide polygons without a normal. (What is the average direction of two opposing planes?)
  152. void createGrid(Model& model, int part, const ImageU8& heightMap, const ImageRgbaU8& colorMap,
  153. const TransformFunction& transform, bool clipZero, bool mergeSides, bool mirror, bool weldNormals) {
  154. if (weldNormals && !mirror) {
  155. printText("\n Warning! Cannot weld normals without a mirrored side. The \"weldNormals\" will be ignored because \"mirror\" was not active.\n\n");
  156. weldNormals = false;
  157. }
  158. if (weldNormals && !clipZero) {
  159. printText("\n Warning! Cannot weld normals without clipping zero displacement. The \"weldNormals\" will be ignored because \"clipZero\" was not active.\n\n");
  160. weldNormals = false;
  161. }
  162. // Generate primary side
  163. int otherStartPointIndex = createGridSide(model, part, heightMap, colorMap, transform, clipZero, mergeSides);
  164. // Generate additional mirrored side
  165. if (mirror) {
  166. createGridSide(model, part, heightMap, colorMap, transform, clipZero, mergeSides, true, true, weldNormals ? otherStartPointIndex : -1);
  167. }
  168. }
  169. // The part of ParserState that resets when creating a new part but is kept after generating geometry
  170. struct PartSettings {
  171. Transform3D location;
  172. float displacement = 1.0f, patchWidth = 1.0f, patchHeight = 1.0f, radius = 0.0f;
  173. int clipZero = 0; // 1 will cut away displacements from height zero, 0 will try to display all polygons
  174. int mirror = 0; // 1 will let height fields generate polygons on both sides to create solid shapes
  175. PartSettings() {}
  176. };
  177. struct ParserState {
  178. String sourcePath;
  179. int angles = 4;
  180. Model model, shadow;
  181. int part = -1; // Current part index for model (No index used for shadows)
  182. PartSettings partSettings;
  183. explicit ParserState(const String& sourcePath) : sourcePath(sourcePath), model(model_create()), shadow(model_create()) {
  184. model_addEmptyPart(this->shadow, U"shadow");
  185. }
  186. };
  187. static void parse_scope(ParserState& state, const ReadableString& key) {
  188. // End the previous scope
  189. state.partSettings = PartSettings();
  190. state.part = -1;
  191. if (string_caseInsensitiveMatch(key, U"PART")) {
  192. // Enter a new part's scope
  193. printText(" New part begins\n");
  194. state.part = model_addEmptyPart(state.model, U"part");
  195. } else {
  196. printText(" Unrecognized scope ", key, " within <>.\n");
  197. }
  198. }
  199. #define MATCH_ASSIGN_GLOBAL(NAME,ACCESS,PARSER,DESCRIPTION) \
  200. if (string_caseInsensitiveMatch(key, NAME)) { \
  201. ACCESS = PARSER(value); \
  202. printText(" ", #DESCRIPTION, " = ", ACCESS, "\n"); \
  203. }
  204. #define MATCH_ASSIGN(BLOCK,NAME,ACCESS,PARSER,DESCRIPTION) \
  205. if (string_caseInsensitiveMatch(key, NAME)) { \
  206. if (state.BLOCK == -1) { \
  207. printText(" Cannot assign ", DESCRIPTION, " without a ", #BLOCK, ".\n"); \
  208. } else { \
  209. ACCESS = PARSER(value); \
  210. printText(" ", #DESCRIPTION, " = ", ACCESS, "\n"); \
  211. } \
  212. }
  213. static void parse_assignment(ParserState& state, const ReadableString& key, const ReadableString& value) {
  214. MATCH_ASSIGN_GLOBAL(U"Angles", state.angles, string_toInteger, "camera angle count")
  215. else MATCH_ASSIGN(part, U"Origin", state.partSettings.location.position, parseFVector3D, "origin")
  216. else MATCH_ASSIGN(part, U"XAxis", state.partSettings.location.transform.xAxis, parseFVector3D, "X-Axis")
  217. else MATCH_ASSIGN(part, U"YAxis", state.partSettings.location.transform.yAxis, parseFVector3D, "Y-Axis")
  218. else MATCH_ASSIGN(part, U"ZAxis", state.partSettings.location.transform.zAxis, parseFVector3D, "Z-Axis")
  219. else MATCH_ASSIGN(part, U"Displacement", state.partSettings.displacement, string_toDouble, "displacement")
  220. else MATCH_ASSIGN(part, U"ClipZero", state.partSettings.clipZero, string_toInteger, "zero clipping")
  221. else MATCH_ASSIGN(part, U"Mirror", state.partSettings.mirror, string_toInteger, "mirror flag")
  222. else MATCH_ASSIGN(part, U"PatchWidth", state.partSettings.patchWidth, string_toDouble, "patch width")
  223. else MATCH_ASSIGN(part, U"PatchHeight", state.partSettings.patchHeight, string_toDouble, "patch height")
  224. else MATCH_ASSIGN(part, U"Radius", state.partSettings.radius, string_toDouble, "radius")
  225. else {
  226. printText(" Tried to assign ", value, " to unrecognized key ", key, ".\n");
  227. }
  228. }
  229. enum class Shape {
  230. None, Plane, Box, Cylinder, LeftHandedModel, RightHandedModel
  231. };
  232. static Shape ShapeFromName(const ReadableString& name) {
  233. if (string_caseInsensitiveMatch(name, U"PLANE")) {
  234. return Shape::Plane;
  235. } else if (string_caseInsensitiveMatch(name, U"BOX")) {
  236. return Shape::Box;
  237. } else if (string_caseInsensitiveMatch(name, U"CYLINDER")) {
  238. return Shape::Cylinder;
  239. } else if (string_caseInsensitiveMatch(name, U"LEFTHANDEDMODEL")) {
  240. return Shape::LeftHandedModel;
  241. } else if (string_caseInsensitiveMatch(name, U"RIGHTHANDEDMODEL")) {
  242. return Shape::RightHandedModel;
  243. } else {
  244. throwError("Unhandled shape \"", name, "\"!\n");
  245. return Shape::None;
  246. }
  247. }
  248. static String nameOfShape(Shape shape) {
  249. if (shape == Shape::None) {
  250. return U"None";
  251. } else if (shape == Shape::Plane) {
  252. return U"Plane";
  253. } else if (shape == Shape::Box) {
  254. return U"Box";
  255. } else if (shape == Shape::Cylinder) {
  256. return U"Cylinder";
  257. } else if (shape == Shape::LeftHandedModel) {
  258. return U"LeftHandedModel";
  259. } else if (shape == Shape::RightHandedModel) {
  260. return U"RightHandedModel";
  261. } else {
  262. return U"?";
  263. }
  264. }
  265. // TODO: Arguments for repeating the input images so that pillars can reuse textures for multiple sides when only one camera angle will be saved
  266. static void generateField(ParserState& state, Shape shape, const ImageU8& heightMap, const ImageRgbaU8& colorMap, bool shadow) {
  267. Transform3D system = state.partSettings.location;
  268. bool clipZero = state.partSettings.clipZero;
  269. float offsetPerUnit = state.partSettings.displacement / 255.0f;
  270. bool mirror = state.partSettings.mirror != 0;
  271. bool mergeSides = shape == Shape::Cylinder;
  272. bool weldNormals = mirror && clipZero;
  273. // Create a transform function based on the shape
  274. TransformFunction transform;
  275. if (shape == Shape::Plane) {
  276. // PatchWidth along local X
  277. // PatchHeight along local Z
  278. // Displacement along local Y
  279. float widthScale = state.partSettings.patchWidth / (image_getWidth(heightMap) - 1);
  280. float heightScale = state.partSettings.patchHeight / -(image_getHeight(heightMap) - 1);
  281. FVector3D localScaling = FVector3D(widthScale, offsetPerUnit, heightScale);
  282. FVector3D localOrigin = FVector3D(state.partSettings.patchWidth * -0.5f, 0.0f, state.partSettings.patchHeight * 0.5f);
  283. transform = [system, localOrigin, localScaling](int pixelX, int pixelY, int displacement){
  284. return system.transformPoint(localOrigin + (FVector3D(pixelX, displacement, pixelY) * localScaling));
  285. };
  286. } else if (shape == Shape::Cylinder) {
  287. // Radius + Displacement along local X, Z
  288. // PatchHeight along local Y
  289. float radius = state.partSettings.radius;
  290. float angleScale = 6.283185307f / image_getWidth(heightMap);
  291. float angleOffset = angleScale * 0.5f; // Start and end half a pixel from the seam
  292. float heightScale = state.partSettings.patchHeight / -(image_getHeight(heightMap) - 1);
  293. float heightOffset = state.partSettings.patchHeight * 0.5f;
  294. int lastRow = image_getHeight(heightMap) - 1;
  295. bool fillHoles = !mirror && !clipZero; // Automatically fill the holes to close the shape when not mirroring nor clipping the sides
  296. transform = [system, angleOffset, angleScale, heightOffset, heightScale, radius, offsetPerUnit, fillHoles, lastRow](int pixelX, int pixelY, int displacement){
  297. float angle = ((float)pixelX * angleScale) + angleOffset;
  298. float offset = ((float)displacement * offsetPerUnit) + radius;
  299. float height = ((float)pixelY * heightScale) + heightOffset;
  300. if (fillHoles && (pixelY == 0 || pixelY == lastRow)) {
  301. offset = 0.0f;
  302. }
  303. return system.transformPoint(FVector3D(-sin(angle) * offset, height, cos(angle) * offset));
  304. };
  305. } else {
  306. printText("Field generation is not implemented for ", nameOfShape(shape), "!\n");
  307. return;
  308. }
  309. if (shadow) {
  310. createGrid(state.shadow, 0, heightMap, colorMap, transform, clipZero, mergeSides, mirror, weldNormals);
  311. } else {
  312. createGrid(state.model, state.part, heightMap, colorMap, transform, clipZero, mergeSides, mirror, weldNormals);
  313. }
  314. }
  315. struct PlyProperty {
  316. String name;
  317. bool list;
  318. int scale = 1; // 1 for normalized input, 255 for uchar
  319. // Single property
  320. PlyProperty(String name, ReadableString typeName) : name(name), list(false) {
  321. if (string_caseInsensitiveMatch(typeName, U"UCHAR")) {
  322. this->scale = 255;
  323. } else {
  324. this->scale = 1;
  325. }
  326. }
  327. // List of properties
  328. PlyProperty(String name, ReadableString typeName, ReadableString lengthTypeName) : name(name), list(true) {
  329. if (string_caseInsensitiveMatch(typeName, U"UCHAR")) {
  330. this->scale = 255;
  331. } else {
  332. this->scale = 1;
  333. }
  334. if (string_caseInsensitiveMatch(lengthTypeName, U"FLOAT")) {
  335. printText("loadPlyModel: Using floating-point numbers to describe the length of a list is nonsense!\n");
  336. }
  337. }
  338. };
  339. struct PlyElement {
  340. String name; // Name of the collection
  341. int count; // Size of the collection
  342. List<PlyProperty> properties; // Properties on each line (list properties consume additional tokens)
  343. PlyElement(const String &name, int count) : name(name), count(count) {}
  344. };
  345. enum class PlyDataInput {
  346. Ignore, Vertex, Face
  347. };
  348. static PlyDataInput PlyDataInputFromName(const ReadableString& name) {
  349. if (string_caseInsensitiveMatch(name, U"VERTEX")) {
  350. return PlyDataInput::Vertex;
  351. } else if (string_caseInsensitiveMatch(name, U"FACE")) {
  352. return PlyDataInput::Face;
  353. } else {
  354. return PlyDataInput::Ignore;
  355. }
  356. }
  357. struct PlyVertex {
  358. FVector3D position = FVector3D(0.0f, 0.0f, 0.0f);
  359. FVector4D color = FVector4D(1.0f, 1.0f, 1.0f, 1.0f);
  360. };
  361. // When exporting PLY to this tool:
  362. // +X is right
  363. // +Y is up
  364. // +Z is forward
  365. // This coordinate system is left handed, which makes more sense when working with depth buffers.
  366. // If exporting from a right-handed editor, setting Y as up and Z as forward might flip the X axis to the left side.
  367. // In that case, flip the X axis when calling this function.
  368. static void loadPlyModel(ParserState& state, const ReadableString& content, bool shadow, bool flipX) {
  369. //printText("loadPlyModel:\n", content, "\n");
  370. // Find the target model
  371. Model targetModel = shadow ? state.shadow : state.model;
  372. int startPointIndex = model_getNumberOfPoints(targetModel);
  373. int targetPart = shadow ? 0 : state.part;
  374. // Split lines
  375. List<ReadableString> lines = string_split(content, U'\n');
  376. List<PlyElement> elements;
  377. bool readingContent = false; // True after passing end_header
  378. int elementIndex = -1; // current member of elements
  379. int memberIndex = 0; // current data line within the content of the current element
  380. PlyDataInput inputMode = PlyDataInput::Ignore;
  381. // Temporary geometry
  382. List<PlyVertex> vertices;
  383. if (lines.length() < 2) {
  384. printText("loadPlyModel: Failed to identify line-breaks in the PLY file!\n");
  385. return;
  386. } else if (!string_caseInsensitiveMatch(string_removeOuterWhiteSpace(lines[0]), U"PLY")) {
  387. printText("loadPlyModel: Failed to identify the file as PLY!\n");
  388. return;
  389. } else if (!string_caseInsensitiveMatch(string_removeOuterWhiteSpace(lines[1]), U"FORMAT ASCII 1.0")) {
  390. printText("loadPlyModel: Only supporting the ascii 1.0 format!\n");
  391. return;
  392. }
  393. for (int l = 0; l < lines.length(); l++) {
  394. // Tokenize the current line
  395. ReadableString currentLine = string_removeOuterWhiteSpace(lines[l]);
  396. List<ReadableString> tokens;
  397. string_split_inPlace(tokens, currentLine, U' ');
  398. if (tokens.length() > 0 && !string_caseInsensitiveMatch(tokens[0], U"COMMENT")) {
  399. if (readingContent) {
  400. // Parse geometry
  401. if (inputMode == PlyDataInput::Vertex || inputMode == PlyDataInput::Face) {
  402. // Create new vertex with default properties
  403. if (inputMode == PlyDataInput::Vertex) {
  404. vertices.push(PlyVertex());
  405. }
  406. PlyElement *currentElement = &(elements[elementIndex]);
  407. int tokenIndex = 0;
  408. for (int propertyIndex = 0; propertyIndex < currentElement->properties.length(); propertyIndex++) {
  409. if (tokenIndex >= tokens.length()) {
  410. printText("loadPlyModel: Undeclared properties given to ", currentElement->name, " in the data!\n");
  411. break;
  412. }
  413. PlyProperty *currentProperty = &(currentElement->properties[propertyIndex]);
  414. if (currentProperty->list) {
  415. int listLength = string_toInteger(tokens[tokenIndex]);
  416. tokenIndex++;
  417. // Detect polygons
  418. if (inputMode == PlyDataInput::Face && string_caseInsensitiveMatch(currentProperty->name, U"VERTEX_INDICES")) {
  419. if (vertices.length() == 0) {
  420. printText("loadPlyModel: This ply importer does not support feeding polygons before vertices! Using vertices before defining them would require an additional intermediate representation.\n");
  421. }
  422. bool flipSides = flipX;
  423. if (listLength == 4) {
  424. // Use a quad to save memory
  425. int indexA = string_toInteger(tokens[tokenIndex]);
  426. int indexB = string_toInteger(tokens[tokenIndex + 1]);
  427. int indexC = string_toInteger(tokens[tokenIndex + 2]);
  428. int indexD = string_toInteger(tokens[tokenIndex + 3]);
  429. FVector4D colorA = vertices[indexA].color;
  430. FVector4D colorB = vertices[indexB].color;
  431. FVector4D colorC = vertices[indexC].color;
  432. FVector4D colorD = vertices[indexD].color;
  433. if (flipSides) {
  434. int polygon = model_addQuad(targetModel, targetPart,
  435. startPointIndex + indexD,
  436. startPointIndex + indexC,
  437. startPointIndex + indexB,
  438. startPointIndex + indexA
  439. );
  440. model_setVertexColor(targetModel, targetPart, polygon, 0, colorD);
  441. model_setVertexColor(targetModel, targetPart, polygon, 1, colorC);
  442. model_setVertexColor(targetModel, targetPart, polygon, 2, colorB);
  443. model_setVertexColor(targetModel, targetPart, polygon, 3, colorA);
  444. } else {
  445. int polygon = model_addQuad(targetModel, targetPart,
  446. startPointIndex + indexA,
  447. startPointIndex + indexB,
  448. startPointIndex + indexC,
  449. startPointIndex + indexD
  450. );
  451. model_setVertexColor(targetModel, targetPart, polygon, 0, colorA);
  452. model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
  453. model_setVertexColor(targetModel, targetPart, polygon, 2, colorC);
  454. model_setVertexColor(targetModel, targetPart, polygon, 3, colorD);
  455. }
  456. } else {
  457. // Polygon generating a triangle fan
  458. int indexA = string_toInteger(tokens[tokenIndex]);
  459. int indexB = string_toInteger(tokens[tokenIndex + 1]);
  460. FVector4D colorA = vertices[indexA].color;
  461. FVector4D colorB = vertices[indexB].color;
  462. for (int i = 2; i < listLength; i++) {
  463. int indexC = string_toInteger(tokens[tokenIndex + i]);
  464. FVector4D colorC = vertices[indexC].color;
  465. // Create a triangle
  466. if (flipSides) {
  467. int polygon = model_addTriangle(targetModel, targetPart,
  468. startPointIndex + indexC,
  469. startPointIndex + indexB,
  470. startPointIndex + indexA
  471. );
  472. model_setVertexColor(targetModel, targetPart, polygon, 0, colorC);
  473. model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
  474. model_setVertexColor(targetModel, targetPart, polygon, 2, colorA);
  475. } else {
  476. int polygon = model_addTriangle(targetModel, targetPart,
  477. startPointIndex + indexA,
  478. startPointIndex + indexB,
  479. startPointIndex + indexC
  480. );
  481. model_setVertexColor(targetModel, targetPart, polygon, 0, colorA);
  482. model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
  483. model_setVertexColor(targetModel, targetPart, polygon, 2, colorC);
  484. }
  485. // Iterate the triangle fan
  486. indexB = indexC;
  487. colorB = colorC;
  488. }
  489. }
  490. }
  491. tokenIndex += listLength;
  492. } else {
  493. // Detect vertex data
  494. if (inputMode == PlyDataInput::Vertex) {
  495. float value = string_toDouble(tokens[tokenIndex]) / (double)currentProperty->scale;
  496. // Swap X, Y and Z to convert from PLY coordinates
  497. if (string_caseInsensitiveMatch(currentProperty->name, U"X")) {
  498. if (flipX) {
  499. value = -value; // Right-handed to left-handed conversion
  500. }
  501. vertices[vertices.length() - 1].position.x = value;
  502. } else if (string_caseInsensitiveMatch(currentProperty->name, U"Y")) {
  503. vertices[vertices.length() - 1].position.y = value;
  504. } else if (string_caseInsensitiveMatch(currentProperty->name, U"Z")) {
  505. vertices[vertices.length() - 1].position.z = value;
  506. } else if (string_caseInsensitiveMatch(currentProperty->name, U"RED")) {
  507. vertices[vertices.length() - 1].color.x = value;
  508. } else if (string_caseInsensitiveMatch(currentProperty->name, U"GREEN")) {
  509. vertices[vertices.length() - 1].color.y = value;
  510. } else if (string_caseInsensitiveMatch(currentProperty->name, U"BLUE")) {
  511. vertices[vertices.length() - 1].color.z = value;
  512. } else if (string_caseInsensitiveMatch(currentProperty->name, U"ALPHA")) {
  513. vertices[vertices.length() - 1].color.w = value;
  514. }
  515. }
  516. }
  517. // Count one for a list size or single property
  518. tokenIndex++;
  519. }
  520. // Complete the vertex
  521. if (inputMode == PlyDataInput::Vertex) {
  522. FVector3D localPosition = vertices[vertices.length() - 1].position;
  523. model_addPoint(targetModel, state.partSettings.location.transformPoint(localPosition));
  524. }
  525. }
  526. memberIndex++;
  527. if (memberIndex >= elements[elementIndex].count) {
  528. // Done with the element
  529. elementIndex++;
  530. memberIndex = 0;
  531. if (elementIndex >= elements.length()) {
  532. // Done with the file
  533. if (l < lines.length() - 1) {
  534. // Remaining lines will be ignored with a warning
  535. printText("loadPlyModel: Ignored ", (lines.length() - 1) - l, " undeclared lines at file end!\n");
  536. }
  537. return;
  538. } else {
  539. // Identify the next element by name
  540. inputMode = PlyDataInputFromName(elements[elementIndex].name);
  541. }
  542. }
  543. } else {
  544. if (tokens.length() == 1) {
  545. if (string_caseInsensitiveMatch(tokens[0], U"END_HEADER")) {
  546. readingContent = true;
  547. elementIndex = 0;
  548. memberIndex = 0;
  549. if (elements.length() < 2) {
  550. printText("loadPlyModel: Need at least two elements to defined faces and vertices in the model!\n");
  551. return;
  552. }
  553. // Identify the first element by name
  554. inputMode = PlyDataInputFromName(elements[elementIndex].name);
  555. }
  556. } else if (tokens.length() >= 3) {
  557. if (string_caseInsensitiveMatch(tokens[0], U"ELEMENT")) {
  558. elements.push(PlyElement(tokens[1], string_toInteger(tokens[2])));
  559. elementIndex = elements.length() - 1;
  560. } else if (string_caseInsensitiveMatch(tokens[0], U"PROPERTY")) {
  561. if (elementIndex < 0) {
  562. printText("loadPlyModel: Cannot declare a property without an element!\n");
  563. } else if (readingContent) {
  564. printText("loadPlyModel: Cannot declare a property outside of the header!\n");
  565. } else {
  566. if (tokens.length() == 3) {
  567. // Single property
  568. elements[elementIndex].properties.push(PlyProperty(tokens[2], tokens[1]));
  569. } else if (tokens.length() == 5 && string_caseInsensitiveMatch(tokens[1], U"LIST")) {
  570. // Integer followed by that number of properties as a list
  571. elements[elementIndex].properties.push(PlyProperty(tokens[4], tokens[3], tokens[2]));
  572. } else {
  573. printText("loadPlyModel: Unable to parse property!\n");
  574. return;
  575. }
  576. }
  577. }
  578. }
  579. }
  580. }
  581. }
  582. }
  583. static void loadModel(ParserState& state, const ReadableString& filename, bool shadow, bool flipX) {
  584. int lastDotIndex = string_findLast(filename, U'.');
  585. if (lastDotIndex == -1) {
  586. printText("The model's filename ", filename, " does not have an extension!\n");
  587. } else {
  588. ReadableString extension = string_after(filename, lastDotIndex);
  589. if (string_caseInsensitiveMatch(extension, U"PLY")) {
  590. // Store the whole model file in a string for fast reading
  591. String content = string_load(state.sourcePath + filename);
  592. // Parse the file from the string
  593. loadPlyModel(state, content, shadow, flipX);
  594. } else {
  595. printText("The extension ", extension, " in ", filename, " is not yet supported! You can implement an importer and call it from the loadModel function in tool.cpp.\n");
  596. }
  597. }
  598. }
  599. static void generateBasicShape(ParserState& state, Shape shape, const ReadableString& arg1, const ReadableString& arg2, const ReadableString& arg3, bool shadow) {
  600. Transform3D system = state.partSettings.location;
  601. Model model = shadow ? state.shadow : state.model;
  602. int part = shadow ? 0 : state.part;
  603. // All shapes are centered around the axis system's origin from -0.5 to +0.5 of any given size
  604. if (shape == Shape::Box) {
  605. // Parse arguments
  606. float width = string_toDouble(arg1);
  607. float height = string_toDouble(arg2);
  608. float depth = string_toDouble(arg3);
  609. // Create a bound
  610. FVector3D upper = FVector3D(width, height, depth) * 0.5f;
  611. FVector3D lower = -upper;
  612. // Positions
  613. int first = model_getNumberOfPoints(model);
  614. model_addPoint(model, system.transformPoint(FVector3D(lower.x, lower.y, lower.z))); // first + 0: Left-down-near
  615. model_addPoint(model, system.transformPoint(FVector3D(lower.x, lower.y, upper.z))); // first + 1: Left-down-far
  616. model_addPoint(model, system.transformPoint(FVector3D(lower.x, upper.y, lower.z))); // first + 2: Left-up-near
  617. model_addPoint(model, system.transformPoint(FVector3D(lower.x, upper.y, upper.z))); // first + 3: Left-up-far
  618. model_addPoint(model, system.transformPoint(FVector3D(upper.x, lower.y, lower.z))); // first + 4: Right-down-near
  619. model_addPoint(model, system.transformPoint(FVector3D(upper.x, lower.y, upper.z))); // first + 5: Right-down-far
  620. model_addPoint(model, system.transformPoint(FVector3D(upper.x, upper.y, lower.z))); // first + 6: Right-up-near
  621. model_addPoint(model, system.transformPoint(FVector3D(upper.x, upper.y, upper.z))); // first + 7: Right-up-far
  622. // Polygons
  623. model_addQuad(model, part, first + 3, first + 2, first + 0, first + 1); // Left quad
  624. model_addQuad(model, part, first + 6, first + 7, first + 5, first + 4); // Right quad
  625. model_addQuad(model, part, first + 2, first + 6, first + 4, first + 0); // Front quad
  626. model_addQuad(model, part, first + 7, first + 3, first + 1, first + 5); // Back quad
  627. model_addQuad(model, part, first + 3, first + 7, first + 6, first + 2); // Top quad
  628. model_addQuad(model, part, first + 0, first + 4, first + 5, first + 1); // Bottom quad
  629. } else if (shape == Shape::Cylinder) {
  630. // Parse arguments
  631. float radius = string_toDouble(arg1);
  632. float height = string_toDouble(arg2);
  633. int sideCount = string_toDouble(arg3);
  634. // Create a bound
  635. float topHeight = height * 0.5f;
  636. float bottomHeight = height * -0.5f;
  637. // Positions
  638. float angleScale = 6.283185307 / (float)sideCount;
  639. int centerTop = model_addPoint(model, system.transformPoint(FVector3D(0.0f, topHeight, 0.0f)));
  640. int firstTopSide = model_getNumberOfPoints(model);
  641. for (int p = 0; p < sideCount; p++) {
  642. float radians = p * angleScale;
  643. model_addPoint(model, system.transformPoint(FVector3D(sin(radians) * radius, topHeight, cos(radians) * radius)));
  644. }
  645. int centerBottom = model_addPoint(model, system.transformPoint(FVector3D(0.0f, bottomHeight, 0.0f)));
  646. int firstBottomSide = model_getNumberOfPoints(model);
  647. for (int p = 0; p < sideCount; p++) {
  648. float radians = p * angleScale;
  649. model_addPoint(model, system.transformPoint(FVector3D(sin(radians) * radius, bottomHeight, cos(radians) * radius)));
  650. }
  651. for (int p = 0; p < sideCount; p++) {
  652. int q = (p + 1) % sideCount;
  653. // Top fan
  654. model_addTriangle(model, part, centerTop, firstTopSide + p, firstTopSide + q);
  655. // Bottom fan
  656. model_addTriangle(model, part, centerBottom, firstBottomSide + q, firstBottomSide + p);
  657. // Side
  658. model_addQuad(model, part, firstTopSide + q, firstTopSide + p, firstBottomSide + p, firstBottomSide + q);
  659. }
  660. } else {
  661. printText("Basic shape generation is not implemented for ", nameOfShape(shape), "!\n");
  662. return;
  663. }
  664. }
  665. // Used when displaying shadow models for debugging
  666. static ImageRgbaU8 createDebugTexture() {
  667. ImageRgbaU8 result = image_create_RgbaU8(2, 2);
  668. image_writePixel(result, 0, 0, ColorRgbaI32(255, 0, 0, 255));
  669. image_writePixel(result, 1, 0, ColorRgbaI32(0, 255, 0, 255));
  670. image_writePixel(result, 0, 1, ColorRgbaI32(0, 0, 255, 255));
  671. image_writePixel(result, 1, 1, ColorRgbaI32(255, 255, 0, 255));
  672. return result;
  673. }
  674. ImageRgbaU8 debugTexture = createDebugTexture();
  675. static void parse_shape(ParserState& state, List<ReadableString>& args, bool shadow) {
  676. if (state.part == -1) {
  677. printText(" Cannot generate a ", args[0], " without a part.\n");
  678. }
  679. Shape shape = ShapeFromName(args[0]);
  680. if (shape == Shape::LeftHandedModel || shape == Shape::RightHandedModel) {
  681. if (args.length() > 2) {
  682. printText(" Too many arguments when trying to load a model. Just give one file name without spaces.\n");
  683. } else if (args.length() < 2) {
  684. printText(" Loading a model requires a filename.\n");
  685. } else {
  686. bool flipX = (shape == Shape::RightHandedModel);
  687. loadModel(state, args[1], shadow, flipX);
  688. }
  689. } else if (args.length() == 2) {
  690. // Shape, HeightMap
  691. ImageU8 heightMap = image_get_red(image_load_RgbaU8(state.sourcePath + args[1]));
  692. generateField(state, shape, heightMap, debugTexture, shadow);
  693. } else if (args.length() == 3) {
  694. // Shape, HeightMap, ColorMap
  695. ImageU8 heightMap = image_get_red(image_load_RgbaU8(state.sourcePath + args[1]));
  696. ImageRgbaU8 colorMap = image_load_RgbaU8(state.sourcePath + args[2]);
  697. generateField(state, shape, heightMap, colorMap, shadow);
  698. } else if (args.length() == 4) {
  699. // Shape, Width, Height, Depth
  700. generateBasicShape(state, shape, args[1], args[2], args[3], shadow);
  701. } else {
  702. printText(" The ", args[0], " shape needs at least a height map to know the number of vertices to generate. A color map can also be given.\n");
  703. }
  704. }
  705. static void parse_dsm(ParserState& state, const ReadableString& content) {
  706. List<ReadableString> lines = string_split(content, U'\n');
  707. List<ReadableString> args; // Reusing the buffer for in-place splitting of arguments on each line
  708. for (int l = 0; l < lines.length(); l++) {
  709. // Get the current line
  710. ReadableString line = lines[l];
  711. // Skip comments
  712. int commentIndex = string_findFirst(line, U';');
  713. if (commentIndex > -1) {
  714. line = string_removeOuterWhiteSpace(string_before(line, commentIndex));
  715. }
  716. if (string_length(line) > 0) {
  717. // Find assignments
  718. int assignmentIndex = string_findFirst(line, U'=');
  719. int colonIndex = string_findFirst(line, U':');
  720. int blockStartIndex = string_findFirst(line, U'<');
  721. int blockEndIndex = string_findFirst(line, U'>');
  722. if (assignmentIndex > -1) {
  723. ReadableString key = string_removeOuterWhiteSpace(string_before(line, assignmentIndex));
  724. ReadableString value = string_removeOuterWhiteSpace(string_after(line, assignmentIndex));
  725. parse_assignment(state, key, value);
  726. } else if (colonIndex > -1) {
  727. ReadableString command = string_removeOuterWhiteSpace(string_before(line, colonIndex));
  728. ReadableString argContent = string_after(line, colonIndex);
  729. string_split_inPlace(args, argContent, U',');
  730. for (int a = 0; a < args.length(); a++) {
  731. args[a] = string_removeOuterWhiteSpace(args[a]);
  732. }
  733. if (string_caseInsensitiveMatch(command, U"Visible")) {
  734. parse_shape(state, args, false);
  735. } else if (string_caseInsensitiveMatch(command, U"Shadow")) {
  736. parse_shape(state, args, true);
  737. } else {
  738. printText(" Unrecognized command ", command, ".\n");
  739. }
  740. } else if (blockStartIndex > -1 && blockEndIndex > -1) {
  741. String block = string_removeOuterWhiteSpace(string_inclusiveRange(line, blockStartIndex + 1, blockEndIndex - 1));
  742. parse_scope(state, block);
  743. } else {
  744. printText("Unrecognized content \"", line, "\" on line ", l + 1, ".\n");
  745. }
  746. }
  747. }
  748. }
  749. void processScript(const String& sourcePath, const String& targetPath, OrthoSystem ortho, const String& scriptName) {
  750. // Initialize a parser state containing an empty model
  751. ParserState state = ParserState(sourcePath);
  752. // Parse the script to fill the state with a model and additional render settings
  753. String scriptPath = string_combine(state.sourcePath, scriptName, U".dsm");
  754. printText("Generating ", scriptPath, "\n");
  755. parse_dsm(state, string_load(scriptPath));
  756. // Render the model
  757. sprite_generateFromModel(state.model, state.shadow, ortho, targetPath + scriptName, state.angles, false);
  758. }
  759. // The first argument is the source folder in which the model scripts are stored.
  760. // The second argument is the target folder in which the results are saved.
  761. // The third argument is the ortho configuration file path.
  762. // The following arguments are plain names of the scripts to process without any path nor extension.
  763. void tool_main(int argn, char **argv) {
  764. if (argn < 5) {
  765. printText("Nothing to process. Terminating sprite generation tool.\n");
  766. } else {
  767. String sourcePath = string_combine(argv[1], file_separator());
  768. String targetPath = string_combine(argv[2], file_separator());
  769. OrthoSystem ortho = OrthoSystem(string_load(String(argv[3])));
  770. for (int a = 4; a < argn; a++) {
  771. processScript(sourcePath, targetPath, ortho, String(argv[a]));
  772. }
  773. }
  774. }