tool.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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. #include "sprite/importer.h"
  12. using namespace dsr;
  13. static constexpr float colorScale = 1.0f / 255.0f;
  14. static FVector4D pixelToVertexColor(const ColorRgbaI32& color) {
  15. return FVector4D(color.red * colorScale, color.green * colorScale, color.blue * colorScale, 1.0f);
  16. }
  17. static int createTriangle(Model& model, int part, int indexA, int indexB, int indexC, FVector4D colorA, FVector4D colorB, FVector4D colorC, bool flip = false) {
  18. if (flip) {
  19. int poly = model_addTriangle(model, part, indexB, indexA, indexC);
  20. model_setVertexColor(model, part, poly, 0, colorB);
  21. model_setVertexColor(model, part, poly, 1, colorA);
  22. model_setVertexColor(model, part, poly, 2, colorC);
  23. return poly;
  24. } else {
  25. int poly = model_addTriangle(model, part, indexA, indexB, indexC);
  26. model_setVertexColor(model, part, poly, 0, colorA);
  27. model_setVertexColor(model, part, poly, 1, colorB);
  28. model_setVertexColor(model, part, poly, 2, colorC);
  29. return poly;
  30. }
  31. }
  32. using TransformFunction = std::function<FVector3D(int pixelX, int pixelY, int displacement)>;
  33. // Returns the start point index for another side to weld against
  34. int createGridSide(Model& model, int part, const ImageU8& heightMap, const ImageRgbaU8& colorMap,
  35. const TransformFunction& transform, bool clipZero, bool mergeSides, bool flipDepth = false, bool flipFaces = false, int otherStartPointIndex = -1) {
  36. int startPointIndex = model_getNumberOfPoints(model);
  37. int mapWidth = image_getWidth(heightMap);
  38. int mapHeight = image_getHeight(heightMap);
  39. int flipScale = flipDepth ? -1 : 1;
  40. int columns = mergeSides ? mapWidth + 1 : mapWidth;
  41. // Create a part for the polygons
  42. for (int z = 0; z < mapHeight; z++) {
  43. for (int x = 0; x < columns; x++) {
  44. // Sample the height map and convert to world space
  45. int cx = x % mapWidth;
  46. int heightC = image_readPixel_border(heightMap, cx, z);
  47. // Add the point to the model
  48. if (x < mapWidth) {
  49. // Create a position from the 3D index
  50. model_addPoint(model, transform(x, z, heightC * flipScale));
  51. }
  52. if (x > 0 && z > 0) {
  53. // Create vertex data
  54. // A-B
  55. // |
  56. // D-C
  57. int px = x - 1;
  58. int cz = z;
  59. int pz = z - 1;
  60. // Sample previous heights
  61. int heightA = image_readPixel_border(heightMap, px, pz);
  62. int heightB = image_readPixel_border(heightMap, cx, pz);
  63. int heightD = image_readPixel_border(heightMap, px, cz);
  64. // Tell where to weld with another side's points
  65. bool weldA = otherStartPointIndex > -1 && heightA == 0;
  66. bool weldB = otherStartPointIndex > -1 && heightB == 0;
  67. bool weldC = otherStartPointIndex > -1 && heightC == 0;
  68. bool weldD = otherStartPointIndex > -1 && heightD == 0;
  69. // Get indices to points
  70. int indexA = (weldA ? otherStartPointIndex : startPointIndex) + px + pz * mapWidth;
  71. int indexB = (weldB ? otherStartPointIndex : startPointIndex) + cx + pz * mapWidth;
  72. int indexC = (weldC ? otherStartPointIndex : startPointIndex) + cx + cz * mapWidth;
  73. int indexD = (weldD ? otherStartPointIndex : startPointIndex) + px + cz * mapWidth;
  74. // Sample colors
  75. FVector4D colorA = pixelToVertexColor(image_readPixel_tile(colorMap, px, pz));
  76. FVector4D colorB = pixelToVertexColor(image_readPixel_tile(colorMap, cx, pz));
  77. FVector4D colorC = pixelToVertexColor(image_readPixel_tile(colorMap, cx, cz));
  78. FVector4D colorD = pixelToVertexColor(image_readPixel_tile(colorMap, px, cz));
  79. // Decide how to split triangles and which ones to display
  80. bool acSplit = false;
  81. bool skipFirst = false;
  82. bool skipSecond = false;
  83. if (heightA == 0 && heightC == 0) {
  84. // ABCD fan of ABC and ACD
  85. acSplit = true;
  86. if (heightB == 0) { skipFirst = true; }
  87. if (heightD == 0) { skipSecond = true; }
  88. } else if (heightB == 0 && heightD == 0) {
  89. // BCDA fan of ACD and BDA
  90. acSplit = false;
  91. if (heightC == 0) { skipFirst = true; }
  92. if (heightA == 0) { skipSecond = true; }
  93. } else {
  94. int cA = image_readPixel_tile(heightMap, cx - 2, cz - 2);
  95. int cB = image_readPixel_tile(heightMap, cx + 1, cz - 2);
  96. int cC = image_readPixel_tile(heightMap, cx + 1, cz + 1);
  97. int cD = image_readPixel_tile(heightMap, cx - 2, cz + 1);
  98. int diffAC = abs((cA + cC) - (heightA + heightC));
  99. int diffBD = abs((cB + cD) - (heightB + heightD));
  100. acSplit = diffBD > diffAC;
  101. }
  102. if (!clipZero) {
  103. skipFirst = false;
  104. skipSecond = false;
  105. }
  106. // Create a polygon
  107. if (!(skipFirst && skipSecond)) {
  108. if (acSplit) {
  109. if (!skipFirst) {
  110. createTriangle(model, part,
  111. indexA, indexB, indexC,
  112. colorA, colorB, colorC, flipFaces
  113. );
  114. }
  115. if (!skipSecond) {
  116. createTriangle(model, part,
  117. indexA, indexC, indexD,
  118. colorA, colorC, colorD, flipFaces
  119. );
  120. }
  121. } else {
  122. if (!skipFirst) {
  123. createTriangle(model, part,
  124. indexB, indexC, indexD,
  125. colorB, colorC, colorD, flipFaces
  126. );
  127. }
  128. if (!skipSecond) {
  129. createTriangle(model, part,
  130. indexB, indexD, indexA,
  131. colorB, colorD, colorA, flipFaces
  132. );
  133. }
  134. }
  135. }
  136. }
  137. }
  138. }
  139. return startPointIndex;
  140. }
  141. // clipZero:
  142. // Removing triangles from pixels with displacement zero.
  143. // Used for carving out non-square shapes using black height as the background.
  144. // mergeSides:
  145. // Connect vertices from the left side of the image with the right side using additional polygons.
  146. // Used for cylinder shapes to remove the seam where the sides meet.
  147. // mirror:
  148. // Create another instance of the height field with surfaces and displacement turned in the other direction.
  149. // weldNormals:
  150. // Merges normals between mirrored sides to let normals at displacement zero merge with the other side.
  151. // mirror must be active for this to have an effect, because there's no mirrored side to weld against otherwise.
  152. // clipZero must be active to hide polygons without a normal. (What is the average direction of two opposing planes?)
  153. void createGrid(Model& model, int part, const ImageU8& heightMap, const ImageRgbaU8& colorMap,
  154. const TransformFunction& transform, bool clipZero, bool mergeSides, bool mirror, bool weldNormals) {
  155. if (weldNormals && !mirror) {
  156. printText("\n Warning! Cannot weld normals without a mirrored side. The \"weldNormals\" will be ignored because \"mirror\" was not active.\n\n");
  157. weldNormals = false;
  158. }
  159. if (weldNormals && !clipZero) {
  160. printText("\n Warning! Cannot weld normals without clipping zero displacement. The \"weldNormals\" will be ignored because \"clipZero\" was not active.\n\n");
  161. weldNormals = false;
  162. }
  163. // Generate primary side
  164. int otherStartPointIndex = createGridSide(model, part, heightMap, colorMap, transform, clipZero, mergeSides);
  165. // Generate additional mirrored side
  166. if (mirror) {
  167. createGridSide(model, part, heightMap, colorMap, transform, clipZero, mergeSides, true, true, weldNormals ? otherStartPointIndex : -1);
  168. }
  169. }
  170. // The part of ParserState that resets when creating a new part but is kept after generating geometry
  171. struct PartSettings {
  172. Transform3D location;
  173. float displacement = 1.0f, patchWidth = 1.0f, patchHeight = 1.0f, radius = 0.0f;
  174. int clipZero = 0; // 1 will cut away displacements from height zero, 0 will try to display all polygons
  175. int mirror = 0; // 1 will let height fields generate polygons on both sides to create solid shapes
  176. PartSettings() {}
  177. };
  178. struct ParserState {
  179. String sourcePath;
  180. int angles = 4;
  181. Model model, shadow;
  182. int part = -1; // Current part index for model (No index used for shadows)
  183. PartSettings partSettings;
  184. explicit ParserState(const String& sourcePath) : sourcePath(sourcePath), model(model_create()), shadow(model_create()) {
  185. model_addEmptyPart(this->shadow, U"shadow");
  186. }
  187. };
  188. static void parse_scope(ParserState& state, const ReadableString& key) {
  189. // End the previous scope
  190. state.partSettings = PartSettings();
  191. state.part = -1;
  192. if (string_caseInsensitiveMatch(key, U"PART")) {
  193. // Enter a new part's scope
  194. printText(" New part begins\n");
  195. state.part = model_addEmptyPart(state.model, U"part");
  196. } else {
  197. printText(" Unrecognized scope ", key, " within <>.\n");
  198. }
  199. }
  200. #define MATCH_ASSIGN_GLOBAL(NAME,ACCESS,PARSER,DESCRIPTION) \
  201. if (string_caseInsensitiveMatch(key, NAME)) { \
  202. ACCESS = PARSER(value); \
  203. printText(" ", #DESCRIPTION, " = ", ACCESS, "\n"); \
  204. }
  205. #define MATCH_ASSIGN(BLOCK,NAME,ACCESS,PARSER,DESCRIPTION) \
  206. if (string_caseInsensitiveMatch(key, NAME)) { \
  207. if (state.BLOCK == -1) { \
  208. printText(" Cannot assign ", DESCRIPTION, " without a ", #BLOCK, ".\n"); \
  209. } else { \
  210. ACCESS = PARSER(value); \
  211. printText(" ", #DESCRIPTION, " = ", ACCESS, "\n"); \
  212. } \
  213. }
  214. static void parse_assignment(ParserState& state, const ReadableString& key, const ReadableString& value) {
  215. MATCH_ASSIGN_GLOBAL(U"Angles", state.angles, string_toInteger, "camera angle count")
  216. else MATCH_ASSIGN(part, U"Origin", state.partSettings.location.position, parseFVector3D, "origin")
  217. else MATCH_ASSIGN(part, U"XAxis", state.partSettings.location.transform.xAxis, parseFVector3D, "X-Axis")
  218. else MATCH_ASSIGN(part, U"YAxis", state.partSettings.location.transform.yAxis, parseFVector3D, "Y-Axis")
  219. else MATCH_ASSIGN(part, U"ZAxis", state.partSettings.location.transform.zAxis, parseFVector3D, "Z-Axis")
  220. else MATCH_ASSIGN(part, U"Displacement", state.partSettings.displacement, string_toDouble, "displacement")
  221. else MATCH_ASSIGN(part, U"ClipZero", state.partSettings.clipZero, string_toInteger, "zero clipping")
  222. else MATCH_ASSIGN(part, U"Mirror", state.partSettings.mirror, string_toInteger, "mirror flag")
  223. else MATCH_ASSIGN(part, U"PatchWidth", state.partSettings.patchWidth, string_toDouble, "patch width")
  224. else MATCH_ASSIGN(part, U"PatchHeight", state.partSettings.patchHeight, string_toDouble, "patch height")
  225. else MATCH_ASSIGN(part, U"Radius", state.partSettings.radius, string_toDouble, "radius")
  226. else {
  227. printText(" Tried to assign ", value, " to unrecognized key ", key, ".\n");
  228. }
  229. }
  230. enum class Shape {
  231. None, Plane, Box, Cylinder, LeftHandedModel, RightHandedModel
  232. };
  233. static Shape ShapeFromName(const ReadableString& name) {
  234. if (string_caseInsensitiveMatch(name, U"PLANE")) {
  235. return Shape::Plane;
  236. } else if (string_caseInsensitiveMatch(name, U"BOX")) {
  237. return Shape::Box;
  238. } else if (string_caseInsensitiveMatch(name, U"CYLINDER")) {
  239. return Shape::Cylinder;
  240. } else if (string_caseInsensitiveMatch(name, U"LEFTHANDEDMODEL")) {
  241. return Shape::LeftHandedModel;
  242. } else if (string_caseInsensitiveMatch(name, U"RIGHTHANDEDMODEL")) {
  243. return Shape::RightHandedModel;
  244. } else {
  245. throwError("Unhandled shape \"", name, "\"!\n");
  246. return Shape::None;
  247. }
  248. }
  249. static String nameOfShape(Shape shape) {
  250. if (shape == Shape::None) {
  251. return U"None";
  252. } else if (shape == Shape::Plane) {
  253. return U"Plane";
  254. } else if (shape == Shape::Box) {
  255. return U"Box";
  256. } else if (shape == Shape::Cylinder) {
  257. return U"Cylinder";
  258. } else if (shape == Shape::LeftHandedModel) {
  259. return U"LeftHandedModel";
  260. } else if (shape == Shape::RightHandedModel) {
  261. return U"RightHandedModel";
  262. } else {
  263. return U"?";
  264. }
  265. }
  266. // TODO: Arguments for repeating the input images so that pillars can reuse textures for multiple sides when only one camera angle will be saved
  267. static void generateField(ParserState& state, Shape shape, const ImageU8& heightMap, const ImageRgbaU8& colorMap, bool shadow) {
  268. Transform3D system = state.partSettings.location;
  269. bool clipZero = state.partSettings.clipZero;
  270. float offsetPerUnit = state.partSettings.displacement / 255.0f;
  271. bool mirror = state.partSettings.mirror != 0;
  272. bool mergeSides = shape == Shape::Cylinder;
  273. bool weldNormals = mirror && clipZero;
  274. // Create a transform function based on the shape
  275. TransformFunction transform;
  276. if (shape == Shape::Plane) {
  277. // PatchWidth along local X
  278. // PatchHeight along local Z
  279. // Displacement along local Y
  280. float widthScale = state.partSettings.patchWidth / (image_getWidth(heightMap) - 1);
  281. float heightScale = state.partSettings.patchHeight / -(image_getHeight(heightMap) - 1);
  282. FVector3D localScaling = FVector3D(widthScale, offsetPerUnit, heightScale);
  283. FVector3D localOrigin = FVector3D(state.partSettings.patchWidth * -0.5f, 0.0f, state.partSettings.patchHeight * 0.5f);
  284. transform = [system, localOrigin, localScaling](int pixelX, int pixelY, int displacement){
  285. return system.transformPoint(localOrigin + (FVector3D(pixelX, displacement, pixelY) * localScaling));
  286. };
  287. } else if (shape == Shape::Cylinder) {
  288. // Radius + Displacement along local X, Z
  289. // PatchHeight along local Y
  290. float radius = state.partSettings.radius;
  291. float angleScale = 6.283185307f / image_getWidth(heightMap);
  292. float angleOffset = angleScale * 0.5f; // Start and end half a pixel from the seam
  293. float heightScale = state.partSettings.patchHeight / -(image_getHeight(heightMap) - 1);
  294. float heightOffset = state.partSettings.patchHeight * 0.5f;
  295. int lastRow = image_getHeight(heightMap) - 1;
  296. bool fillHoles = !mirror && !clipZero; // Automatically fill the holes to close the shape when not mirroring nor clipping the sides
  297. transform = [system, angleOffset, angleScale, heightOffset, heightScale, radius, offsetPerUnit, fillHoles, lastRow](int pixelX, int pixelY, int displacement){
  298. float angle = ((float)pixelX * angleScale) + angleOffset;
  299. float offset = ((float)displacement * offsetPerUnit) + radius;
  300. float height = ((float)pixelY * heightScale) + heightOffset;
  301. if (fillHoles && (pixelY == 0 || pixelY == lastRow)) {
  302. offset = 0.0f;
  303. }
  304. return system.transformPoint(FVector3D(-sin(angle) * offset, height, cos(angle) * offset));
  305. };
  306. } else {
  307. printText("Field generation is not implemented for ", nameOfShape(shape), "!\n");
  308. return;
  309. }
  310. if (shadow) {
  311. createGrid(state.shadow, 0, heightMap, colorMap, transform, clipZero, mergeSides, mirror, weldNormals);
  312. } else {
  313. createGrid(state.model, state.part, heightMap, colorMap, transform, clipZero, mergeSides, mirror, weldNormals);
  314. }
  315. }
  316. static void generateBasicShape(ParserState& state, Shape shape, const ReadableString& arg1, const ReadableString& arg2, const ReadableString& arg3, bool shadow) {
  317. Transform3D system = state.partSettings.location;
  318. Model model = shadow ? state.shadow : state.model;
  319. int part = shadow ? 0 : state.part;
  320. // All shapes are centered around the axis system's origin from -0.5 to +0.5 of any given size
  321. if (shape == Shape::Box) {
  322. // Parse arguments
  323. float width = string_toDouble(arg1);
  324. float height = string_toDouble(arg2);
  325. float depth = string_toDouble(arg3);
  326. // Create a bound
  327. FVector3D upper = FVector3D(width, height, depth) * 0.5f;
  328. FVector3D lower = -upper;
  329. // Positions
  330. int first = model_getNumberOfPoints(model);
  331. model_addPoint(model, system.transformPoint(FVector3D(lower.x, lower.y, lower.z))); // first + 0: Left-down-near
  332. model_addPoint(model, system.transformPoint(FVector3D(lower.x, lower.y, upper.z))); // first + 1: Left-down-far
  333. model_addPoint(model, system.transformPoint(FVector3D(lower.x, upper.y, lower.z))); // first + 2: Left-up-near
  334. model_addPoint(model, system.transformPoint(FVector3D(lower.x, upper.y, upper.z))); // first + 3: Left-up-far
  335. model_addPoint(model, system.transformPoint(FVector3D(upper.x, lower.y, lower.z))); // first + 4: Right-down-near
  336. model_addPoint(model, system.transformPoint(FVector3D(upper.x, lower.y, upper.z))); // first + 5: Right-down-far
  337. model_addPoint(model, system.transformPoint(FVector3D(upper.x, upper.y, lower.z))); // first + 6: Right-up-near
  338. model_addPoint(model, system.transformPoint(FVector3D(upper.x, upper.y, upper.z))); // first + 7: Right-up-far
  339. // Polygons
  340. model_addQuad(model, part, first + 3, first + 2, first + 0, first + 1); // Left quad
  341. model_addQuad(model, part, first + 6, first + 7, first + 5, first + 4); // Right quad
  342. model_addQuad(model, part, first + 2, first + 6, first + 4, first + 0); // Front quad
  343. model_addQuad(model, part, first + 7, first + 3, first + 1, first + 5); // Back quad
  344. model_addQuad(model, part, first + 3, first + 7, first + 6, first + 2); // Top quad
  345. model_addQuad(model, part, first + 0, first + 4, first + 5, first + 1); // Bottom quad
  346. } else if (shape == Shape::Cylinder) {
  347. // Parse arguments
  348. float radius = string_toDouble(arg1);
  349. float height = string_toDouble(arg2);
  350. int sideCount = string_toDouble(arg3);
  351. // Create a bound
  352. float topHeight = height * 0.5f;
  353. float bottomHeight = height * -0.5f;
  354. // Positions
  355. float angleScale = 6.283185307 / (float)sideCount;
  356. int centerTop = model_addPoint(model, system.transformPoint(FVector3D(0.0f, topHeight, 0.0f)));
  357. int firstTopSide = model_getNumberOfPoints(model);
  358. for (int p = 0; p < sideCount; p++) {
  359. float radians = p * angleScale;
  360. model_addPoint(model, system.transformPoint(FVector3D(sin(radians) * radius, topHeight, cos(radians) * radius)));
  361. }
  362. int centerBottom = model_addPoint(model, system.transformPoint(FVector3D(0.0f, bottomHeight, 0.0f)));
  363. int firstBottomSide = model_getNumberOfPoints(model);
  364. for (int p = 0; p < sideCount; p++) {
  365. float radians = p * angleScale;
  366. model_addPoint(model, system.transformPoint(FVector3D(sin(radians) * radius, bottomHeight, cos(radians) * radius)));
  367. }
  368. for (int p = 0; p < sideCount; p++) {
  369. int q = (p + 1) % sideCount;
  370. // Top fan
  371. model_addTriangle(model, part, centerTop, firstTopSide + p, firstTopSide + q);
  372. // Bottom fan
  373. model_addTriangle(model, part, centerBottom, firstBottomSide + q, firstBottomSide + p);
  374. // Side
  375. model_addQuad(model, part, firstTopSide + q, firstTopSide + p, firstBottomSide + p, firstBottomSide + q);
  376. }
  377. } else {
  378. printText("Basic shape generation is not implemented for ", nameOfShape(shape), "!\n");
  379. return;
  380. }
  381. }
  382. // Used when displaying shadow models for debugging
  383. static ImageRgbaU8 createDebugTexture() {
  384. ImageRgbaU8 result = image_create_RgbaU8(2, 2);
  385. image_writePixel(result, 0, 0, ColorRgbaI32(255, 0, 0, 255));
  386. image_writePixel(result, 1, 0, ColorRgbaI32(0, 255, 0, 255));
  387. image_writePixel(result, 0, 1, ColorRgbaI32(0, 0, 255, 255));
  388. image_writePixel(result, 1, 1, ColorRgbaI32(255, 255, 0, 255));
  389. return result;
  390. }
  391. ImageRgbaU8 debugTexture = createDebugTexture();
  392. static void parse_shape(ParserState& state, List<String>& args, bool shadow) {
  393. if (state.part == -1) {
  394. printText(" Cannot generate a ", args[0], " without a part.\n");
  395. }
  396. Shape shape = ShapeFromName(args[0]);
  397. if (shape == Shape::LeftHandedModel || shape == Shape::RightHandedModel) {
  398. if (args.length() > 2) {
  399. printText(" Too many arguments when trying to load a model. Just give one file name without spaces.\n");
  400. } else if (args.length() < 2) {
  401. printText(" Loading a model requires a filename.\n");
  402. } else {
  403. bool flipX = (shape == Shape::RightHandedModel);
  404. Model targetModel = shadow ? state.shadow : state.model;
  405. int targetPart = shadow ? 0 : state.part;
  406. importer_loadModel(targetModel, targetPart, string_combine(state.sourcePath, args[1]), flipX, state.partSettings.location);
  407. }
  408. } else if (args.length() == 2) {
  409. // Shape, HeightMap
  410. ImageU8 heightMap = image_get_red(image_load_RgbaU8(state.sourcePath + args[1]));
  411. generateField(state, shape, heightMap, debugTexture, shadow);
  412. } else if (args.length() == 3) {
  413. // Shape, HeightMap, ColorMap
  414. ImageU8 heightMap = image_get_red(image_load_RgbaU8(state.sourcePath + args[1]));
  415. ImageRgbaU8 colorMap = image_load_RgbaU8(state.sourcePath + args[2]);
  416. generateField(state, shape, heightMap, colorMap, shadow);
  417. } else if (args.length() == 4) {
  418. // Shape, Width, Height, Depth
  419. generateBasicShape(state, shape, args[1], args[2], args[3], shadow);
  420. } else {
  421. 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");
  422. }
  423. }
  424. static void parse_dsm(ParserState& state, const ReadableString& content) {
  425. List<String> lines = string_split(content, U'\n');
  426. for (int l = 0; l < lines.length(); l++) {
  427. // Get the current line
  428. ReadableString line = lines[l];
  429. // Skip comments
  430. int commentIndex = string_findFirst(line, U';');
  431. if (commentIndex > -1) {
  432. line = string_removeOuterWhiteSpace(string_before(line, commentIndex));
  433. }
  434. if (string_length(line) > 0) {
  435. // Find assignments
  436. int assignmentIndex = string_findFirst(line, U'=');
  437. int colonIndex = string_findFirst(line, U':');
  438. int blockStartIndex = string_findFirst(line, U'<');
  439. int blockEndIndex = string_findFirst(line, U'>');
  440. if (assignmentIndex > -1) {
  441. ReadableString key = string_removeOuterWhiteSpace(string_before(line, assignmentIndex));
  442. ReadableString value = string_removeOuterWhiteSpace(string_after(line, assignmentIndex));
  443. parse_assignment(state, key, value);
  444. } else if (colonIndex > -1) {
  445. ReadableString command = string_removeOuterWhiteSpace(string_before(line, colonIndex));
  446. ReadableString argContent = string_after(line, colonIndex);
  447. List<String> args = string_split(argContent, U',');
  448. for (int a = 0; a < args.length(); a++) {
  449. args[a] = string_removeOuterWhiteSpace(args[a]);
  450. }
  451. if (string_caseInsensitiveMatch(command, U"Visible")) {
  452. parse_shape(state, args, false);
  453. } else if (string_caseInsensitiveMatch(command, U"Shadow")) {
  454. parse_shape(state, args, true);
  455. } else {
  456. printText(" Unrecognized command ", command, ".\n");
  457. }
  458. } else if (blockStartIndex > -1 && blockEndIndex > -1) {
  459. String block = string_removeOuterWhiteSpace(string_inclusiveRange(line, blockStartIndex + 1, blockEndIndex - 1));
  460. parse_scope(state, block);
  461. } else {
  462. printText("Unrecognized content \"", line, "\" on line ", l + 1, ".\n");
  463. }
  464. }
  465. }
  466. }
  467. void processScript(const String& sourcePath, const String& targetPath, OrthoSystem ortho, const String& scriptName) {
  468. // Initialize a parser state containing an empty model
  469. ParserState state = ParserState(sourcePath);
  470. // Parse the script to fill the state with a model and additional render settings
  471. String scriptPath = string_combine(state.sourcePath, scriptName, U".dsm");
  472. printText("Generating ", scriptPath, "\n");
  473. parse_dsm(state, string_load(scriptPath));
  474. // Render the model
  475. sprite_generateFromModel(state.model, state.shadow, ortho, targetPath + scriptName, state.angles, false);
  476. }
  477. // The first argument is the source folder in which the model scripts are stored.
  478. // The second argument is the target folder in which the results are saved.
  479. // The third argument is the ortho configuration file path.
  480. // The following arguments are plain names of the scripts to process without any path nor extension.
  481. void tool_main(int argn, char **argv) {
  482. if (argn < 5) {
  483. printText("Nothing to process. Terminating sprite generation tool.\n");
  484. } else {
  485. String sourcePath = string_combine(argv[1], file_separator());
  486. String targetPath = string_combine(argv[2], file_separator());
  487. OrthoSystem ortho = OrthoSystem(string_load(String(argv[3])));
  488. for (int a = 4; a < argn; a++) {
  489. processScript(sourcePath, targetPath, ortho, String(argv[a]));
  490. }
  491. }
  492. }