spriteAPI.cpp 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329
  1. 
  2. #include "spriteAPI.h"
  3. #include "Octree.h"
  4. #include "DirtyRectangles.h"
  5. #include "importer.h"
  6. #include "../../DFPSR/render/ITriangle2D.h"
  7. #include "../../DFPSR/base/endian.h"
  8. #include "../../DFPSR/math/scalar.h"
  9. // Comment out a flag to disable an optimization when debugging
  10. #define DIRTY_RECTANGLE_OPTIMIZATION
  11. namespace dsr {
  12. template <bool HIGH_QUALITY>
  13. static IRect renderModel(const Model& model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, const FVector2D& worldOrigin, const Transform3D& modelToWorldSpace);
  14. template <bool HIGH_QUALITY>
  15. static IRect renderDenseModel(const DenseModel& model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, const FVector2D& worldOrigin, const Transform3D& modelToWorldSpace);
  16. static Transform3D combineWorldToScreenTransform(const FMatrix3x3& worldSpaceToScreenDepth, const FVector2D& worldOrigin) {
  17. return Transform3D(FVector3D(worldOrigin.x, worldOrigin.y, 0.0f), worldSpaceToScreenDepth);
  18. }
  19. static Transform3D combineModelToScreenTransform(const Transform3D& modelToWorldSpace, const FMatrix3x3& worldSpaceToScreenDepth, const FVector2D& worldOrigin) {
  20. return modelToWorldSpace * combineWorldToScreenTransform(worldSpaceToScreenDepth, worldOrigin);
  21. }
  22. struct SpriteConfig {
  23. int centerX, centerY; // The sprite's origin in pixels relative to the upper left corner
  24. int frameRows; // The atlas has one row for each frame
  25. int propertyColumns; // The atlas has one column for each type of information
  26. // The 3D model's bound in tile units
  27. // The height image goes from 0 at minimum Y to 255 at maximum Y
  28. FVector3D minBound, maxBound;
  29. // Shadow shapes
  30. List<FVector3D> points; // 3D points for the triangles to refer to by index
  31. List<int32_t> triangleIndices; // Triangle indices stored in multiples of three integers
  32. // Construction
  33. SpriteConfig(int centerX, int centerY, int frameRows, int propertyColumns, FVector3D minBound, FVector3D maxBound)
  34. : centerX(centerX), centerY(centerY), frameRows(frameRows), propertyColumns(propertyColumns), minBound(minBound), maxBound(maxBound) {}
  35. explicit SpriteConfig(const ReadableString& content) {
  36. config_parse_ini(content, [this](const ReadableString& block, const ReadableString& key, const ReadableString& value) {
  37. if (string_length(block) == 0) {
  38. if (string_caseInsensitiveMatch(key, U"CenterX")) {
  39. this->centerX = string_toInteger(value);
  40. } else if (string_caseInsensitiveMatch(key, U"CenterY")) {
  41. this->centerY = string_toInteger(value);
  42. } else if (string_caseInsensitiveMatch(key, U"FrameRows")) {
  43. this->frameRows = string_toInteger(value);
  44. } else if (string_caseInsensitiveMatch(key, U"PropertyColumns")) {
  45. this->propertyColumns = string_toInteger(value);
  46. } else if (string_caseInsensitiveMatch(key, U"MinBound")) {
  47. this->minBound = parseFVector3D(value);
  48. } else if (string_caseInsensitiveMatch(key, U"MaxBound")) {
  49. this->maxBound = parseFVector3D(value);
  50. } else if (string_caseInsensitiveMatch(key, U"Points")) {
  51. List<String> values = string_split(value, U',');
  52. if (values.length() % 3 != 0) {
  53. throwError("Points contained ", values.length(), " values, which is not evenly divisible by three!");
  54. } else {
  55. this->points.clear();
  56. this->points.reserve(values.length() / 3);
  57. for (int v = 0; v < values.length(); v += 3) {
  58. this->points.push(FVector3D(string_toDouble(values[v]), string_toDouble(values[v+1]), string_toDouble(values[v+2])));
  59. }
  60. }
  61. } else if (string_caseInsensitiveMatch(key, U"TriangleIndices")) {
  62. List<String> values = string_split(value, U',');
  63. if (values.length() % 3 != 0) {
  64. throwError("TriangleIndices contained ", values.length(), " values, which is not evenly divisible by three!");
  65. } else {
  66. this->triangleIndices.clear();
  67. this->triangleIndices.reserve(values.length());
  68. for (int v = 0; v < values.length(); v++) {
  69. this->triangleIndices.push(string_toInteger(values[v]));
  70. }
  71. }
  72. } else {
  73. printText("Unrecognized key \"", key, "\" in sprite configuration file.\n");
  74. }
  75. } else {
  76. printText("Unrecognized block \"", block, "\" in sprite configuration file.\n");
  77. }
  78. });
  79. }
  80. // Add model as a persistent shadow caster in the sprite configuration
  81. void appendShadow(const Model& model) {
  82. points.reserve(this->points.length() + model_getNumberOfPoints(model));
  83. for (int p = 0; p < model_getNumberOfPoints(model); p++) {
  84. this->points.push(model_getPoint(model, p));
  85. }
  86. for (int part = 0; part < model_getNumberOfParts(model); part++) {
  87. for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
  88. int vertexCount = model_getPolygonVertexCount(model, part, poly);
  89. int vertA = 0;
  90. int indexA = model_getVertexPointIndex(model, part, poly, vertA);
  91. for (int vertB = 1; vertB < vertexCount - 1; vertB++) {
  92. int vertC = vertB + 1;
  93. int indexB = model_getVertexPointIndex(model, part, poly, vertB);
  94. int indexC = model_getVertexPointIndex(model, part, poly, vertC);
  95. triangleIndices.push(indexA); triangleIndices.push(indexB); triangleIndices.push(indexC);
  96. }
  97. }
  98. }
  99. }
  100. String toIni() {
  101. // General information
  102. String result = string_combine(
  103. U"; Sprite configuration file\n",
  104. U"CenterX=", this->centerX, "\n",
  105. U"CenterY=", this->centerY, "\n",
  106. U"FrameRows=", this->frameRows, "\n",
  107. U"PropertyColumns=", this->propertyColumns, "\n",
  108. U"MinBound=", this->minBound, "\n",
  109. U"MaxBound=", this->maxBound, "\n"
  110. );
  111. // Low-resolution 3D shape
  112. if (this->points.length() > 0) {
  113. string_append(result, U"Points=");
  114. for (int p = 0; p < this->points.length(); p++) {
  115. if (p > 0) {
  116. string_append(result, U", ");
  117. }
  118. string_append(result, this->points[p]);
  119. }
  120. string_append(result, U"\n");
  121. string_append(result, U"TriangleIndices=");
  122. for (int i = 0; i < this->triangleIndices.length(); i+=3) {
  123. if (i > 0) {
  124. string_append(result, U", ");
  125. }
  126. string_append(result, this->triangleIndices[i], U",", this->triangleIndices[i+1], U",", this->triangleIndices[i+2]);
  127. }
  128. string_append(result, U"\n");
  129. }
  130. return result;
  131. }
  132. };
  133. static ImageF32 scaleHeightImage(const ImageRgbaU8& heightImage, float minHeight, float maxHeight, const ImageRgbaU8& colorImage) {
  134. float scale = (maxHeight - minHeight) / 255.0f;
  135. float offset = minHeight;
  136. int width = image_getWidth(heightImage);
  137. int height = image_getHeight(heightImage);
  138. ImageF32 result = image_create_F32(width, height);
  139. for (int y = 0; y < height; y++) {
  140. for (int x = 0; x < width; x++) {
  141. float value = image_readPixel_clamp(heightImage, x, y).red;
  142. if (image_readPixel_clamp(colorImage, x, y).alpha > 127) {
  143. image_writePixel(result, x, y, (value * scale) + offset);
  144. } else {
  145. image_writePixel(result, x, y, -std::numeric_limits<float>::infinity());
  146. }
  147. }
  148. }
  149. return result;
  150. }
  151. struct SpriteFrame {
  152. IVector2D centerPoint;
  153. ImageRgbaU8 colorImage; // (Red, Green, Blue, _)
  154. ImageRgbaU8 normalImage; // (NormalX, NormalY, NormalZ, _)
  155. ImageF32 heightImage;
  156. SpriteFrame(const IVector2D& centerPoint, const ImageRgbaU8& colorImage, const ImageRgbaU8& normalImage, const ImageF32& heightImage)
  157. : centerPoint(centerPoint), colorImage(colorImage), normalImage(normalImage), heightImage(heightImage) {}
  158. };
  159. struct SpriteType {
  160. public:
  161. IVector3D minBoundMini, maxBoundMini;
  162. List<SpriteFrame> frames;
  163. // TODO: Compress the data using a shadow-only model type of only positions and triangle indices in a single part.
  164. // The shadow model will have its own rendering method excluding the color target.
  165. // Shadow rendering can be a lot simpler by not calculating any vertex weights
  166. // just interpolate the depth using addition, compare to the old value and write the new depth value.
  167. Model shadowModel;
  168. public:
  169. // folderPath should end with a path separator
  170. SpriteType(const String& folderPath, const String& spriteName) {
  171. // Load the image atlas
  172. ImageRgbaU8 loadedAtlas = image_load_RgbaU8(string_combine(folderPath, spriteName, U".png"));
  173. // Load the settings
  174. const SpriteConfig configuration = SpriteConfig(string_load(string_combine(folderPath, spriteName, U".ini")));
  175. this->minBoundMini = IVector3D(
  176. floor(configuration.minBound.x * ortho_miniUnitsPerTile),
  177. floor(configuration.minBound.y * ortho_miniUnitsPerTile),
  178. floor(configuration.minBound.z * ortho_miniUnitsPerTile)
  179. );
  180. this->maxBoundMini = IVector3D(
  181. ceil(configuration.maxBound.x * ortho_miniUnitsPerTile),
  182. ceil(configuration.maxBound.y * ortho_miniUnitsPerTile),
  183. ceil(configuration.maxBound.z * ortho_miniUnitsPerTile)
  184. );
  185. int width = image_getWidth(loadedAtlas) / configuration.propertyColumns;
  186. int height = image_getHeight(loadedAtlas) / configuration.frameRows;
  187. for (int a = 0; a < configuration.frameRows; a++) {
  188. ImageRgbaU8 colorImage = image_getSubImage(loadedAtlas, IRect(0, a * height, width, height));
  189. ImageRgbaU8 heightImage = image_getSubImage(loadedAtlas, IRect(width, a * height, width, height));
  190. ImageRgbaU8 normalImage = image_getSubImage(loadedAtlas, IRect(width * 2, a * height, width, height));
  191. ImageF32 scaledHeightImage = scaleHeightImage(heightImage, configuration.minBound.y, configuration.maxBound.y, colorImage);
  192. this->frames.pushConstruct(IVector2D(configuration.centerX, configuration.centerY), colorImage, normalImage, scaledHeightImage);
  193. }
  194. // Create a model for rendering shadows
  195. if (configuration.points.length() > 0) {
  196. this->shadowModel = model_create();
  197. for (int p = 0; p < configuration.points.length(); p++) {
  198. model_addPoint(this->shadowModel, configuration.points[p]);
  199. }
  200. model_addEmptyPart(this->shadowModel, U"Shadow");
  201. for (int t = 0; t < configuration.triangleIndices.length(); t+=3) {
  202. model_addTriangle(this->shadowModel, 0, configuration.triangleIndices[t], configuration.triangleIndices[t+1], configuration.triangleIndices[t+2]);
  203. }
  204. }
  205. }
  206. public:
  207. // TODO: Force frame count to a power of two or replace modulo with look-up tables in sprite configurations.
  208. int getFrameIndex(Direction direction) {
  209. const int frameFromDir[dir360] = {4, 1, 5, 2, 6, 3, 7, 0};
  210. return frameFromDir[correctDirection(direction)] % this->frames.length();
  211. }
  212. };
  213. struct DenseTriangle {
  214. public:
  215. FVector3D colorA, colorB, colorC, posA, posB, posC, normalA, normalB, normalC;
  216. public:
  217. DenseTriangle() {}
  218. DenseTriangle(
  219. const FVector3D& colorA, const FVector3D& colorB, const FVector3D& colorC,
  220. const FVector3D& posA, const FVector3D& posB, const FVector3D& posC,
  221. const FVector3D& normalA, const FVector3D& normalB, const FVector3D& normalC)
  222. : colorA(colorA), colorB(colorB), colorC(colorC),
  223. posA(posA), posB(posB), posC(posC),
  224. normalA(normalA), normalB(normalB), normalC(normalC) {}
  225. };
  226. // The raw format for dense models using vertex colors instead of textures
  227. // Due to the high number of triangles, indexing positions would cause a lot of cache misses
  228. struct DenseModelImpl {
  229. public:
  230. Array<DenseTriangle> triangles;
  231. FVector3D minBound, maxBound;
  232. public:
  233. // Optimize an existing model
  234. DenseModelImpl(const Model& original);
  235. };
  236. struct ModelType {
  237. public:
  238. DenseModel visibleModel;
  239. Model shadowModel;
  240. public:
  241. // folderPath should end with a path separator
  242. ModelType(const String& folderPath, const String& visibleModelName, const String& shadowModelName) {
  243. this->visibleModel = DenseModel_create(importer_loadModel(folderPath + visibleModelName, true, Transform3D()));
  244. this->shadowModel = importer_loadModel(folderPath + shadowModelName, true, Transform3D());
  245. }
  246. ModelType(const DenseModel& visibleModel, const Model& shadowModel)
  247. : visibleModel(visibleModel), shadowModel(shadowModel) {}
  248. };
  249. // Global list of all sprite types ever loaded
  250. List<SpriteType> spriteTypes;
  251. int spriteWorld_loadSpriteTypeFromFile(const String& folderPath, const String& spriteName) {
  252. spriteTypes.pushConstruct(folderPath, spriteName);
  253. return spriteTypes.length() - 1;
  254. }
  255. int spriteWorld_getSpriteTypeCount() {
  256. return spriteTypes.length();
  257. }
  258. // Global list of all model types ever loaded
  259. List<ModelType> modelTypes;
  260. int spriteWorld_loadModelTypeFromFile(const String& folderPath, const String& visibleModelName, const String& shadowModelName) {
  261. modelTypes.pushConstruct(folderPath, visibleModelName, shadowModelName);
  262. return modelTypes.length() - 1;
  263. }
  264. int spriteWorld_getModelTypeCount() {
  265. return modelTypes.length();
  266. }
  267. static int getSpriteFrameIndex(const SpriteInstance& sprite, OrthoView view) {
  268. return spriteTypes[sprite.typeIndex].getFrameIndex(view.worldDirection + sprite.direction);
  269. }
  270. // Returns a 2D bounding box of affected target pixels
  271. static IRect drawSprite(const SpriteInstance& sprite, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
  272. int frameIndex = getSpriteFrameIndex(sprite, ortho);
  273. const SpriteFrame* frame = &spriteTypes[sprite.typeIndex].frames[frameIndex];
  274. IVector2D screenSpace = ortho.miniTilePositionToScreenPixel(sprite.location, worldCenter) - frame->centerPoint;
  275. float heightOffset = sprite.location.y * ortho_tilesPerMiniUnit;
  276. draw_higher(targetHeight, frame->heightImage, targetColor, frame->colorImage, targetNormal, frame->normalImage, screenSpace.x, screenSpace.y, heightOffset);
  277. return IRect(screenSpace.x, screenSpace.y, image_getWidth(frame->colorImage), image_getHeight(frame->colorImage));
  278. }
  279. static IRect drawModel(const ModelInstance& instance, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
  280. return renderDenseModel<false>(modelTypes[instance.typeIndex].visibleModel, ortho, targetHeight, targetColor, targetNormal, FVector2D(worldCenter.x, worldCenter.y), instance.location);
  281. }
  282. // The camera transform for each direction
  283. FMatrix3x3 ShadowCubeMapSides[6] = {
  284. FMatrix3x3::makeAxisSystem(FVector3D( 1.0f, 0.0f, 0.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  285. FMatrix3x3::makeAxisSystem(FVector3D(-1.0f, 0.0f, 0.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  286. FMatrix3x3::makeAxisSystem(FVector3D( 0.0f, 1.0f, 0.0f), FVector3D(0.0f, 0.0f, 1.0f)),
  287. FMatrix3x3::makeAxisSystem(FVector3D( 0.0f,-1.0f, 0.0f), FVector3D(0.0f, 0.0f, 1.0f)),
  288. FMatrix3x3::makeAxisSystem(FVector3D( 0.0f, 0.0f, 1.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  289. FMatrix3x3::makeAxisSystem(FVector3D( 0.0f, 0.0f,-1.0f), FVector3D(0.0f, 1.0f, 0.0f))
  290. };
  291. // TODO: Move to the ortho API using a safe getter in modulo
  292. FMatrix3x3 spriteDirections[8] = {
  293. FMatrix3x3::makeAxisSystem(FVector3D( 0.0f, 0.0f, 1.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  294. FMatrix3x3::makeAxisSystem(FVector3D( 1.0f, 0.0f, 1.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  295. FMatrix3x3::makeAxisSystem(FVector3D( 1.0f, 0.0f, 0.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  296. FMatrix3x3::makeAxisSystem(FVector3D( 1.0f, 0.0f,-1.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  297. FMatrix3x3::makeAxisSystem(FVector3D( 0.0f, 0.0f,-1.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  298. FMatrix3x3::makeAxisSystem(FVector3D(-1.0f, 0.0f,-1.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  299. FMatrix3x3::makeAxisSystem(FVector3D(-1.0f, 0.0f, 0.0f), FVector3D(0.0f, 1.0f, 0.0f)),
  300. FMatrix3x3::makeAxisSystem(FVector3D(-1.0f, 0.0f, 1.0f), FVector3D(0.0f, 1.0f, 0.0f))
  301. };
  302. struct CubeMapF32 {
  303. int resolution; // The width and height of each shadow depth image or 0 if no shadows are casted
  304. AlignedImageF32 cubeMap; // A vertical sequence of reciprocal depth images for the six sides of the cube
  305. ImageF32 cubeMapViews[6]; // Sub-images sharing their allocations with cubeMap as sub-images
  306. explicit CubeMapF32(int resolution) : resolution(resolution) {
  307. this->cubeMap = image_create_F32(resolution, resolution * 6);
  308. for (int s = 0; s < 6; s++) {
  309. this->cubeMapViews[s] = image_getSubImage(this->cubeMap, IRect(0, s * resolution, resolution, resolution));
  310. }
  311. }
  312. void clear() {
  313. image_fill(this->cubeMap, 0.0f);
  314. }
  315. };
  316. class PointLight {
  317. public:
  318. FVector3D position; // The world-space center in tile units
  319. float radius; // The light radius in tile units
  320. float intensity; // The color's brightness multiplier (using float to allow smooth fading)
  321. ColorRgbI32 color; // The color of the light (using integers to detect when the color is uniform)
  322. bool shadowCasting; // Casting shadows when enabled
  323. public:
  324. PointLight(FVector3D position, float radius, float intensity, ColorRgbI32 color, bool shadowCasting)
  325. : position(position), radius(radius), intensity(intensity), color(color), shadowCasting(shadowCasting) {}
  326. public:
  327. void renderModelShadow(CubeMapF32& shadowTarget, const ModelInstance& modelInstance, const FMatrix3x3& normalToWorld) const {
  328. Model model = modelTypes[modelInstance.typeIndex].shadowModel;
  329. if (model_exists(model)) {
  330. // Place the model relative to the light source's position, to make rendering in light-space easier
  331. Transform3D modelToWorldTransform = modelInstance.location;
  332. modelToWorldTransform.position = modelToWorldTransform.position - this->position;
  333. for (int s = 0; s < 6; s++) {
  334. Camera camera = Camera::createPerspective(Transform3D(FVector3D(), ShadowCubeMapSides[s] * normalToWorld), shadowTarget.resolution, shadowTarget.resolution);
  335. model_renderDepth(model, modelToWorldTransform, shadowTarget.cubeMapViews[s], camera);
  336. }
  337. }
  338. }
  339. void renderSpriteShadow(CubeMapF32& shadowTarget, const SpriteInstance& spriteInstance, const FMatrix3x3& normalToWorld) const {
  340. if (spriteInstance.shadowCasting) {
  341. Model model = spriteTypes[spriteInstance.typeIndex].shadowModel;
  342. if (model_exists(model)) {
  343. // Place the model relative to the light source's position, to make rendering in light-space easier
  344. Transform3D modelToWorldTransform = Transform3D(ortho_miniToFloatingTile(spriteInstance.location) - this->position, spriteDirections[spriteInstance.direction]);
  345. for (int s = 0; s < 6; s++) {
  346. Camera camera = Camera::createPerspective(Transform3D(FVector3D(), ShadowCubeMapSides[s] * normalToWorld), shadowTarget.resolution, shadowTarget.resolution);
  347. model_renderDepth(model, modelToWorldTransform, shadowTarget.cubeMapViews[s], camera);
  348. }
  349. }
  350. }
  351. }
  352. // Render shadows from passive models
  353. void renderPassiveShadows(CubeMapF32& shadowTarget, Octree<ModelInstance>& models, const FMatrix3x3& normalToWorld) const {
  354. IVector3D center = ortho_floatingTileToMini(this->position);
  355. IVector3D minBound = center - ortho_floatingTileToMini(radius);
  356. IVector3D maxBound = center + ortho_floatingTileToMini(radius);
  357. models.map(minBound, maxBound, [this, shadowTarget, normalToWorld](ModelInstance& model, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound) mutable {
  358. this->renderModelShadow(shadowTarget, model, normalToWorld);
  359. });
  360. }
  361. // Render shadows from passive sprites
  362. void renderPassiveShadows(CubeMapF32& shadowTarget, Octree<SpriteInstance>& sprites, const FMatrix3x3& normalToWorld) const {
  363. IVector3D center = ortho_floatingTileToMini(this->position);
  364. IVector3D minBound = center - ortho_floatingTileToMini(radius);
  365. IVector3D maxBound = center + ortho_floatingTileToMini(radius);
  366. sprites.map(minBound, maxBound, [this, shadowTarget, normalToWorld](SpriteInstance& sprite, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound) mutable {
  367. this->renderSpriteShadow(shadowTarget, sprite, normalToWorld);
  368. });
  369. }
  370. public:
  371. void illuminate(const OrthoView& camera, const IVector2D& worldCenter, OrderedImageRgbaU8& lightBuffer, const OrderedImageRgbaU8& normalBuffer, const AlignedImageF32& heightBuffer, const CubeMapF32& shadowSource) const {
  372. if (this->shadowCasting) {
  373. addPointLight(camera, worldCenter, lightBuffer, normalBuffer, heightBuffer, this->position, this->radius, this->intensity, this->color, shadowSource.cubeMap);
  374. } else {
  375. addPointLight(camera, worldCenter, lightBuffer, normalBuffer, heightBuffer, this->position, this->radius, this->intensity, this->color);
  376. }
  377. }
  378. };
  379. class DirectedLight {
  380. public:
  381. FVector3D direction; // The world-space direction
  382. float intensity; // The color's brightness multiplier (using float to allow smooth fading)
  383. ColorRgbI32 color; // The color of the light (using integers to detect when the color is uniform)
  384. public:
  385. DirectedLight(FVector3D direction, float intensity, ColorRgbI32 color)
  386. : direction(direction), intensity(intensity), color(color) {}
  387. public:
  388. void illuminate(const OrthoView& camera, const IVector2D& worldCenter, OrderedImageRgbaU8& lightBuffer, const OrderedImageRgbaU8& normalBuffer, bool overwrite = false) const {
  389. if (overwrite) {
  390. setDirectedLight(camera, lightBuffer, normalBuffer, this->direction, this->intensity, this->color);
  391. } else {
  392. addDirectedLight(camera, lightBuffer, normalBuffer, this->direction, this->intensity, this->color);
  393. }
  394. }
  395. };
  396. // BlockState keeps track of when the background itself needs to update from static objects being created or destroyed
  397. enum class BlockState {
  398. Unused,
  399. Ready,
  400. Dirty
  401. };
  402. class BackgroundBlock {
  403. public:
  404. static const int blockSize = 512;
  405. static const int maxDistance = blockSize * 2;
  406. IRect worldRegion;
  407. int cameraId = 0;
  408. BlockState state = BlockState::Unused;
  409. OrderedImageRgbaU8 diffuseBuffer;
  410. OrderedImageRgbaU8 normalBuffer;
  411. AlignedImageF32 heightBuffer;
  412. private:
  413. IVector3D getBoxCorner(const IVector3D& minBound, const IVector3D& maxBound, int cornerIndex) {
  414. assert(cornerIndex >= 0 && cornerIndex < 8);
  415. return IVector3D(
  416. ((uint32_t)cornerIndex & 1u) ? maxBound.x : minBound.x,
  417. ((uint32_t)cornerIndex & 2u) ? maxBound.y : minBound.y,
  418. ((uint32_t)cornerIndex & 4u) ? maxBound.z : minBound.z
  419. );
  420. }
  421. // Pre-condition: diffuseBuffer must be cleared unless sprites cover the whole block
  422. void draw(Octree<SpriteInstance>& sprites, Octree<ModelInstance>& models, const OrthoView& ortho) {
  423. image_fill(this->normalBuffer, ColorRgbaI32(128));
  424. image_fill(this->heightBuffer, -std::numeric_limits<float>::max());
  425. OcTreeFilter orthoCullingFilter = [ortho,this](const IVector3D& minBound, const IVector3D& maxBound){
  426. IVector2D corners[8];
  427. for (int c = 0; c < 8; c++) {
  428. corners[c] = ortho.miniTileOffsetToScreenPixel(getBoxCorner(minBound, maxBound, c));
  429. }
  430. if (corners[0].x < this->worldRegion.left()
  431. && corners[1].x < this->worldRegion.left()
  432. && corners[2].x < this->worldRegion.left()
  433. && corners[3].x < this->worldRegion.left()
  434. && corners[4].x < this->worldRegion.left()
  435. && corners[5].x < this->worldRegion.left()
  436. && corners[6].x < this->worldRegion.left()
  437. && corners[7].x < this->worldRegion.left()) {
  438. return false;
  439. }
  440. if (corners[0].x > this->worldRegion.right()
  441. && corners[1].x > this->worldRegion.right()
  442. && corners[2].x > this->worldRegion.right()
  443. && corners[3].x > this->worldRegion.right()
  444. && corners[4].x > this->worldRegion.right()
  445. && corners[5].x > this->worldRegion.right()
  446. && corners[6].x > this->worldRegion.right()
  447. && corners[7].x > this->worldRegion.right()) {
  448. return false;
  449. }
  450. if (corners[0].y < this->worldRegion.top()
  451. && corners[1].y < this->worldRegion.top()
  452. && corners[2].y < this->worldRegion.top()
  453. && corners[3].y < this->worldRegion.top()
  454. && corners[4].y < this->worldRegion.top()
  455. && corners[5].y < this->worldRegion.top()
  456. && corners[6].y < this->worldRegion.top()
  457. && corners[7].y < this->worldRegion.top()) {
  458. return false;
  459. }
  460. if (corners[0].y > this->worldRegion.bottom()
  461. && corners[1].y > this->worldRegion.bottom()
  462. && corners[2].y > this->worldRegion.bottom()
  463. && corners[3].y > this->worldRegion.bottom()
  464. && corners[4].y > this->worldRegion.bottom()
  465. && corners[5].y > this->worldRegion.bottom()
  466. && corners[6].y > this->worldRegion.bottom()
  467. && corners[7].y > this->worldRegion.bottom()) {
  468. return false;
  469. }
  470. return true;
  471. };
  472. sprites.map(orthoCullingFilter, [this, ortho](SpriteInstance& sprite, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound){
  473. drawSprite(sprite, ortho, -this->worldRegion.upperLeft(), this->heightBuffer, this->diffuseBuffer, this->normalBuffer);
  474. });
  475. models.map(orthoCullingFilter, [this, ortho](ModelInstance& model, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound){
  476. drawModel(model, ortho, -this->worldRegion.upperLeft(), this->heightBuffer, this->diffuseBuffer, this->normalBuffer);
  477. });
  478. }
  479. public:
  480. BackgroundBlock(Octree<SpriteInstance>& sprites, Octree<ModelInstance>& models, const IRect& worldRegion, const OrthoView& ortho)
  481. : worldRegion(worldRegion), cameraId(ortho.id), state(BlockState::Ready),
  482. diffuseBuffer(image_create_RgbaU8(blockSize, blockSize)),
  483. normalBuffer(image_create_RgbaU8(blockSize, blockSize)),
  484. heightBuffer(image_create_F32(blockSize, blockSize)) {
  485. this->draw(sprites, models, ortho);
  486. }
  487. void update(Octree<SpriteInstance>& sprites, Octree<ModelInstance>& models, const IRect& worldRegion, const OrthoView& ortho) {
  488. this->worldRegion = worldRegion;
  489. this->cameraId = ortho.id;
  490. image_fill(this->diffuseBuffer, ColorRgbaI32(0));
  491. this->draw(sprites, models, ortho);
  492. this->state = BlockState::Ready;
  493. }
  494. void draw(ImageRgbaU8& diffuseTarget, ImageRgbaU8& normalTarget, ImageF32& heightTarget, const IRect& seenRegion) const {
  495. if (this->state != BlockState::Unused) {
  496. int left = this->worldRegion.left() - seenRegion.left();
  497. int top = this->worldRegion.top() - seenRegion.top();
  498. draw_copy(diffuseTarget, this->diffuseBuffer, left, top);
  499. draw_copy(normalTarget, this->normalBuffer, left, top);
  500. draw_copy(heightTarget, this->heightBuffer, left, top);
  501. }
  502. }
  503. void recycle() {
  504. //printText("Recycle block at ", this->worldRegion, "\n");
  505. this->state = BlockState::Unused;
  506. this->worldRegion = IRect();
  507. this->cameraId = -1;
  508. }
  509. };
  510. // TODO: A way to delete passive sprites and models using search criterias for bounding box and leaf content using a boolean lambda
  511. class SpriteWorldImpl {
  512. public:
  513. // World
  514. OrthoSystem ortho;
  515. // Having one passive and one active collection per member type allow packing elements tighter to reduce cache misses.
  516. // It also allow executing rendering sorted by which code has to be fetched into the instruction cache.
  517. // Sprites that rarely change and can be stored in a background image.
  518. Octree<SpriteInstance> passiveSprites;
  519. // Rarely moved models can be rendered using free rotation and uniform scaling to the background image.
  520. Octree<ModelInstance> passiveModels;
  521. // Temporary things are deleted when spriteWorld_clearTemporary is called
  522. List<SpriteInstance> temporarySprites;
  523. List<ModelInstance> temporaryModels;
  524. List<PointLight> temporaryPointLights;
  525. List<DirectedLight> temporaryDirectedLights;
  526. // View
  527. int cameraIndex = 0;
  528. IVector3D cameraLocation;
  529. // Deferred rendering
  530. OrderedImageRgbaU8 diffuseBuffer;
  531. OrderedImageRgbaU8 normalBuffer;
  532. AlignedImageF32 heightBuffer;
  533. OrderedImageRgbaU8 lightBuffer;
  534. // Passive background
  535. // TODO: How can split-screen use multiple cameras without duplicate blocks or deleting the other camera's blocks by distance?
  536. List<BackgroundBlock> backgroundBlocks;
  537. // These dirty rectangles keep track of when the background has to be redrawn to the screen after having drawn a dynamic sprite, moved the camera or changed static geometry
  538. DirtyRectangles dirtyBackground;
  539. private:
  540. // Reused buffers
  541. int shadowResolution;
  542. CubeMapF32 temporaryShadowMap;
  543. public:
  544. SpriteWorldImpl(const OrthoSystem &ortho, int shadowResolution)
  545. : ortho(ortho), passiveSprites(ortho_miniUnitsPerTile * 64), passiveModels(ortho_miniUnitsPerTile * 64), shadowResolution(shadowResolution), temporaryShadowMap(shadowResolution) {}
  546. public:
  547. void updateBlockAt(const IRect& blockRegion, const IRect& seenRegion) {
  548. int unusedBlockIndex = -1;
  549. // Find an existing block
  550. for (int b = 0; b < this->backgroundBlocks.length(); b++) {
  551. BackgroundBlock* currentBlockPtr = &this->backgroundBlocks[b];
  552. if (currentBlockPtr->state != BlockState::Unused) {
  553. // Check direction
  554. if (currentBlockPtr->cameraId == this->ortho.view[this->cameraIndex].id) {
  555. // Check location
  556. if (currentBlockPtr->worldRegion.left() == blockRegion.left() && currentBlockPtr->worldRegion.top() == blockRegion.top()) {
  557. // Update if needed
  558. if (currentBlockPtr->state == BlockState::Dirty) {
  559. currentBlockPtr->update(this->passiveSprites, this->passiveModels, blockRegion, this->ortho.view[this->cameraIndex]);
  560. }
  561. // Use the block
  562. return;
  563. } else {
  564. // See if the block is too far from the camera
  565. if (currentBlockPtr->worldRegion.right() < seenRegion.left() - BackgroundBlock::maxDistance
  566. || currentBlockPtr->worldRegion.left() > seenRegion.right() + BackgroundBlock::maxDistance
  567. || currentBlockPtr->worldRegion.bottom() < seenRegion.top() - BackgroundBlock::maxDistance
  568. || currentBlockPtr->worldRegion.top() > seenRegion.bottom() + BackgroundBlock::maxDistance) {
  569. // Recycle because it's too far away
  570. currentBlockPtr->recycle();
  571. unusedBlockIndex = b;
  572. }
  573. }
  574. } else{
  575. // Recycle directly when another camera angle is used
  576. currentBlockPtr->recycle();
  577. unusedBlockIndex = b;
  578. }
  579. } else {
  580. unusedBlockIndex = b;
  581. }
  582. }
  583. // If none of them matched, we should've passed by any unused block already
  584. if (unusedBlockIndex > -1) {
  585. // We have a block to reuse
  586. this->backgroundBlocks[unusedBlockIndex].update(this->passiveSprites, this->passiveModels, blockRegion, this->ortho.view[this->cameraIndex]);
  587. } else {
  588. // Create a new block
  589. this->backgroundBlocks.pushConstruct(this->passiveSprites, this->passiveModels, blockRegion, this->ortho.view[this->cameraIndex]);
  590. }
  591. }
  592. void invalidateBlockAt(int left, int top) {
  593. // Find an existing block
  594. for (int b = 0; b < this->backgroundBlocks.length(); b++) {
  595. BackgroundBlock* currentBlockPtr = &this->backgroundBlocks[b];
  596. // Assuming that alternative camera angles will be removed when drawing next time
  597. if (currentBlockPtr->state == BlockState::Ready
  598. && currentBlockPtr->worldRegion.left() == left
  599. && currentBlockPtr->worldRegion.top() == top) {
  600. // Make dirty to force an update
  601. currentBlockPtr->state = BlockState::Dirty;
  602. }
  603. }
  604. }
  605. // Make sure that each pixel in seenRegion is occupied by an updated background block
  606. void updateBlocks(const IRect& seenRegion) {
  607. // Round inclusive pixel indices down to containing blocks and iterate over them in strides along x and y
  608. int64_t roundedLeft = roundDown(seenRegion.left(), BackgroundBlock::blockSize);
  609. int64_t roundedTop = roundDown(seenRegion.top(), BackgroundBlock::blockSize);
  610. int64_t roundedRight = roundDown(seenRegion.right() - 1, BackgroundBlock::blockSize);
  611. int64_t roundedBottom = roundDown(seenRegion.bottom() - 1, BackgroundBlock::blockSize);
  612. for (int64_t y = roundedTop; y <= roundedBottom; y += BackgroundBlock::blockSize) {
  613. for (int64_t x = roundedLeft; x <= roundedRight; x += BackgroundBlock::blockSize) {
  614. // Make sure that a block is allocated and pre-drawn at this location
  615. this->updateBlockAt(IRect(x, y, BackgroundBlock::blockSize, BackgroundBlock::blockSize), seenRegion);
  616. }
  617. }
  618. }
  619. void drawDeferred(OrderedImageRgbaU8& diffuseTarget, OrderedImageRgbaU8& normalTarget, AlignedImageF32& heightTarget, const IRect& seenRegion) {
  620. // Check image dimensions
  621. assert(image_getWidth(diffuseTarget) == seenRegion.width() && image_getHeight(diffuseTarget) == seenRegion.height());
  622. assert(image_getWidth(normalTarget) == seenRegion.width() && image_getHeight(normalTarget) == seenRegion.height());
  623. assert(image_getWidth(heightTarget) == seenRegion.width() && image_getHeight(heightTarget) == seenRegion.height());
  624. this->dirtyBackground.setTargetResolution(seenRegion.width(), seenRegion.height());
  625. // Draw passive sprites to blocks
  626. this->updateBlocks(seenRegion);
  627. // Draw background blocks to the target images
  628. for (int b = 0; b < this->backgroundBlocks.length(); b++) {
  629. #ifdef DIRTY_RECTANGLE_OPTIMIZATION
  630. // Optimized version
  631. for (int64_t r = 0; r < this->dirtyBackground.getRectangleCount(); r++) {
  632. IRect screenClip = this->dirtyBackground.getRectangle(r);
  633. IRect worldClip = screenClip + seenRegion.upperLeft();
  634. ImageRgbaU8 clippedDiffuseTarget = image_getSubImage(diffuseTarget, screenClip);
  635. ImageRgbaU8 clippedNormalTarget = image_getSubImage(normalTarget, screenClip);
  636. ImageF32 clippedHeightTarget = image_getSubImage(heightTarget, screenClip);
  637. this->backgroundBlocks[b].draw(clippedDiffuseTarget, clippedNormalTarget, clippedHeightTarget, worldClip);
  638. }
  639. #else
  640. // Reference implementation
  641. this->backgroundBlocks[b].draw(diffuseTarget, normalTarget, heightTarget, seenRegion);
  642. #endif
  643. }
  644. // Reset dirty rectangles so that active sprites may record changes
  645. this->dirtyBackground.noneDirty();
  646. // Draw active sprites to the targets
  647. for (int s = 0; s < this->temporarySprites.length(); s++) {
  648. IRect drawnRegion = drawSprite(this->temporarySprites[s], this->ortho.view[this->cameraIndex], -seenRegion.upperLeft(), heightTarget, diffuseTarget, normalTarget);
  649. this->dirtyBackground.makeRegionDirty(drawnRegion);
  650. }
  651. for (int s = 0; s < this->temporaryModels.length(); s++) {
  652. IRect drawnRegion = drawModel(this->temporaryModels[s], this->ortho.view[this->cameraIndex], -seenRegion.upperLeft(), heightTarget, diffuseTarget, normalTarget);
  653. this->dirtyBackground.makeRegionDirty(drawnRegion);
  654. }
  655. }
  656. public:
  657. // modifiedRegion is given in pixels relative to the world origin for the current camera angle
  658. void updatePassiveRegion(const IRect& modifiedRegion) {
  659. int64_t roundedLeft = roundDown(modifiedRegion.left(), BackgroundBlock::blockSize);
  660. int64_t roundedTop = roundDown(modifiedRegion.top(), BackgroundBlock::blockSize);
  661. int64_t roundedRight = roundDown(modifiedRegion.right() - 1, BackgroundBlock::blockSize);
  662. int64_t roundedBottom = roundDown(modifiedRegion.bottom() - 1, BackgroundBlock::blockSize);
  663. for (int64_t y = roundedTop; y <= roundedBottom; y += BackgroundBlock::blockSize) {
  664. for (int64_t x = roundedLeft; x <= roundedRight; x += BackgroundBlock::blockSize) {
  665. // Make sure that a block is allocated and pre-drawn at this location
  666. this->invalidateBlockAt(x, y);
  667. }
  668. }
  669. // Redrawing the whole background to the screen is very cheap using memcpy, so no need to optimize this rare event
  670. this->dirtyBackground.allDirty();
  671. }
  672. IVector2D findWorldCenter(const AlignedImageRgbaU8& colorTarget) const {
  673. return IVector2D(image_getWidth(colorTarget) / 2, image_getHeight(colorTarget) / 2) - this->ortho.miniTileOffsetToScreenPixel(this->cameraLocation, this->cameraIndex);
  674. }
  675. void draw(AlignedImageRgbaU8& colorTarget) {
  676. double startTime;
  677. IVector2D worldCenter = this->findWorldCenter(colorTarget);
  678. // Resize when the window has resized or the buffers haven't been allocated before
  679. int width = image_getWidth(colorTarget);
  680. int height = image_getHeight(colorTarget);
  681. if (image_getWidth(this->diffuseBuffer) != width || image_getHeight(this->diffuseBuffer) != height) {
  682. this->diffuseBuffer = image_create_RgbaU8(width, height);
  683. this->normalBuffer = image_create_RgbaU8(width, height);
  684. this->lightBuffer = image_create_RgbaU8(width, height);
  685. this->heightBuffer = image_create_F32(width, height);
  686. }
  687. IRect worldRegion = IRect(-worldCenter.x, -worldCenter.y, width, height);
  688. startTime = time_getSeconds();
  689. this->drawDeferred(this->diffuseBuffer, this->normalBuffer, this->heightBuffer, worldRegion);
  690. debugText("Draw deferred: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  691. // Illuminate using directed lights
  692. if (this->temporaryDirectedLights.length() > 0) {
  693. startTime = time_getSeconds();
  694. // Overwriting any light from the previous frame
  695. for (int p = 0; p < this->temporaryDirectedLights.length(); p++) {
  696. this->temporaryDirectedLights[p].illuminate(this->ortho.view[this->cameraIndex], worldCenter, this->lightBuffer, this->normalBuffer, p == 0);
  697. }
  698. debugText("Sun light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  699. } else {
  700. startTime = time_getSeconds();
  701. image_fill(this->lightBuffer, ColorRgbaI32(0)); // Set light to black
  702. debugText("Clear light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  703. }
  704. // Illuminate using point lights
  705. for (int p = 0; p < this->temporaryPointLights.length(); p++) {
  706. PointLight *currentLight = &this->temporaryPointLights[p];
  707. if (currentLight->shadowCasting) {
  708. startTime = time_getSeconds();
  709. this->temporaryShadowMap.clear();
  710. // Shadows from background sprites
  711. currentLight->renderPassiveShadows(this->temporaryShadowMap, this->passiveSprites, ortho.view[this->cameraIndex].normalToWorldSpace);
  712. currentLight->renderPassiveShadows(this->temporaryShadowMap, this->passiveModels, ortho.view[this->cameraIndex].normalToWorldSpace);
  713. // Shadows from temporary sprites
  714. for (int s = 0; s < this->temporarySprites.length(); s++) {
  715. currentLight->renderSpriteShadow(this->temporaryShadowMap, this->temporarySprites[s], ortho.view[this->cameraIndex].normalToWorldSpace);
  716. }
  717. // Shadows from temporary models
  718. for (int s = 0; s < this->temporaryModels.length(); s++) {
  719. currentLight->renderModelShadow(this->temporaryShadowMap, this->temporaryModels[s], ortho.view[this->cameraIndex].normalToWorldSpace);
  720. }
  721. debugText("Cast point-light shadows: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  722. }
  723. startTime = time_getSeconds();
  724. currentLight->illuminate(this->ortho.view[this->cameraIndex], worldCenter, this->lightBuffer, this->normalBuffer, this->heightBuffer, this->temporaryShadowMap);
  725. debugText("Illuminate from point-light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  726. }
  727. // Draw the final image to the target by multiplying diffuse with light
  728. startTime = time_getSeconds();
  729. blendLight(colorTarget, this->diffuseBuffer, this->lightBuffer);
  730. debugText("Blend light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  731. }
  732. };
  733. SpriteWorld spriteWorld_create(OrthoSystem ortho, int shadowResolution) {
  734. return std::make_shared<SpriteWorldImpl>(ortho, shadowResolution);
  735. }
  736. #define MUST_EXIST(OBJECT, METHOD) if (OBJECT.get() == nullptr) { throwError("The " #OBJECT " handle was null in " #METHOD "\n"); }
  737. void spriteWorld_addBackgroundSprite(SpriteWorld& world, const SpriteInstance& sprite) {
  738. MUST_EXIST(world, spriteWorld_addBackgroundSprite);
  739. if (sprite.typeIndex < 0 || sprite.typeIndex >= spriteTypes.length()) { throwError(U"Sprite type index ", sprite.typeIndex, " is out of bound!\n"); }
  740. // Add the passive sprite to the octree
  741. IVector3D origin = sprite.location;
  742. IVector3D minBound = origin + spriteTypes[sprite.typeIndex].minBoundMini;
  743. IVector3D maxBound = origin + spriteTypes[sprite.typeIndex].maxBoundMini;
  744. world->passiveSprites.insert(sprite, origin, minBound, maxBound);
  745. // Find the affected passive region and make it dirty
  746. int frameIndex = getSpriteFrameIndex(sprite, world->ortho.view[world->cameraIndex]);
  747. const SpriteFrame* frame = &spriteTypes[sprite.typeIndex].frames[frameIndex];
  748. IVector2D upperLeft = world->ortho.miniTilePositionToScreenPixel(sprite.location, world->cameraIndex, IVector2D()) - frame->centerPoint;
  749. IRect region = IRect(upperLeft.x, upperLeft.y, image_getWidth(frame->colorImage), image_getHeight(frame->colorImage));
  750. world->updatePassiveRegion(region);
  751. }
  752. void spriteWorld_addTemporarySprite(SpriteWorld& world, const SpriteInstance& sprite) {
  753. MUST_EXIST(world, spriteWorld_addTemporarySprite);
  754. if (sprite.typeIndex < 0 || sprite.typeIndex >= spriteTypes.length()) { throwError(U"Sprite type index ", sprite.typeIndex, " is out of bound!\n"); }
  755. // Add the temporary sprite
  756. world->temporarySprites.push(sprite);
  757. }
  758. static void transformCorners(const FVector3D& minBound, const FVector3D& maxBound, const Transform3D& transform, FVector3D* resultCorners) {
  759. resultCorners[0] = transform.transformPoint(FVector3D(minBound.x, minBound.y, minBound.z));
  760. resultCorners[1] = transform.transformPoint(FVector3D(maxBound.x, minBound.y, minBound.z));
  761. resultCorners[2] = transform.transformPoint(FVector3D(minBound.x, maxBound.y, minBound.z));
  762. resultCorners[3] = transform.transformPoint(FVector3D(maxBound.x, maxBound.y, minBound.z));
  763. resultCorners[4] = transform.transformPoint(FVector3D(minBound.x, minBound.y, maxBound.z));
  764. resultCorners[5] = transform.transformPoint(FVector3D(maxBound.x, minBound.y, maxBound.z));
  765. resultCorners[6] = transform.transformPoint(FVector3D(minBound.x, maxBound.y, maxBound.z));
  766. resultCorners[7] = transform.transformPoint(FVector3D(maxBound.x, maxBound.y, maxBound.z));
  767. }
  768. void spriteWorld_addBackgroundModel(SpriteWorld& world, const ModelInstance& instance) {
  769. MUST_EXIST(world, spriteWorld_addBackgroundModel);
  770. if (instance.typeIndex < 0 || instance.typeIndex >= modelTypes.length()) { throwError(U"Model type index ", instance.typeIndex, " is out of bound!\n"); }
  771. // Get the origin and outer bounds
  772. ModelType *type = &(modelTypes[instance.typeIndex]);
  773. // Create a transform for global pixels
  774. Transform3D worldToGlobalPixels = combineWorldToScreenTransform(world->ortho.view[world->cameraIndex].worldSpaceToScreenDepth, FVector2D());
  775. FVector3D transformedCorners[8];
  776. transformCorners(type->visibleModel->minBound, type->visibleModel->maxBound, instance.location, transformedCorners);
  777. // World-space bound
  778. IVector3D worldModelOrigin = ortho_floatingTileToMini(instance.location.position);
  779. IVector3D worldMinBound = worldModelOrigin;
  780. IVector3D worldMaxBound = worldModelOrigin;
  781. // Screen bound
  782. FVector3D globalPixelOrigin = worldToGlobalPixels.transformPoint(instance.location.position);
  783. IVector2D globalPixelMinBound = IVector2D((int32_t)floor(globalPixelOrigin.x), (int32_t)floor(globalPixelOrigin.y));
  784. IVector2D globalPixelMaxBound = globalPixelMinBound;
  785. for (int c = 0; c < 8; c++) {
  786. FVector3D miniSpaceCorner = transformedCorners[c] * (float)ortho_miniUnitsPerTile;
  787. replaceWithSmaller(worldMinBound.x, (int32_t)floor(miniSpaceCorner.x));
  788. replaceWithSmaller(worldMinBound.y, (int32_t)floor(miniSpaceCorner.y));
  789. replaceWithSmaller(worldMinBound.z, (int32_t)floor(miniSpaceCorner.z));
  790. replaceWithLarger(worldMaxBound.x, (int32_t)ceil(miniSpaceCorner.x));
  791. replaceWithLarger(worldMaxBound.y, (int32_t)ceil(miniSpaceCorner.y));
  792. replaceWithLarger(worldMaxBound.z, (int32_t)ceil(miniSpaceCorner.z));
  793. FVector3D globalPixelSpaceCorner = worldToGlobalPixels.transformPoint(transformedCorners[c]);
  794. replaceWithSmaller(globalPixelMinBound.x, (int32_t)floor(globalPixelSpaceCorner.x));
  795. replaceWithSmaller(globalPixelMinBound.y, (int32_t)floor(globalPixelSpaceCorner.y));
  796. replaceWithLarger(globalPixelMaxBound.x, (int32_t)ceil(globalPixelSpaceCorner.x));
  797. replaceWithLarger(globalPixelMaxBound.y, (int32_t)ceil(globalPixelSpaceCorner.y));
  798. }
  799. // Add the passive sprite to the octree
  800. world->passiveModels.insert(instance, worldModelOrigin, worldMinBound, worldMaxBound);
  801. // Find the affected passive region and make it dirty
  802. world->updatePassiveRegion(IRect(globalPixelMinBound.x, globalPixelMinBound.y, globalPixelMaxBound.x - globalPixelMinBound.x, globalPixelMaxBound.y - globalPixelMinBound.y));
  803. }
  804. void spriteWorld_addTemporaryModel(SpriteWorld& world, const ModelInstance& instance) {
  805. MUST_EXIST(world, spriteWorld_addTemporaryModel);
  806. // Add the temporary model
  807. world->temporaryModels.push(instance);
  808. }
  809. void spriteWorld_createTemporary_pointLight(SpriteWorld& world, const FVector3D position, float radius, float intensity, ColorRgbI32 color, bool shadowCasting) {
  810. MUST_EXIST(world, spriteWorld_createTemporary_pointLight);
  811. world->temporaryPointLights.pushConstruct(position, radius, intensity, color, shadowCasting);
  812. }
  813. void spriteWorld_createTemporary_directedLight(SpriteWorld& world, const FVector3D direction, float intensity, ColorRgbI32 color) {
  814. MUST_EXIST(world, spriteWorld_createTemporary_pointLight);
  815. world->temporaryDirectedLights.pushConstruct(direction, intensity, color);
  816. }
  817. void spriteWorld_clearTemporary(SpriteWorld& world) {
  818. MUST_EXIST(world, spriteWorld_clearTemporary);
  819. world->temporarySprites.clear();
  820. world->temporaryModels.clear();
  821. world->temporaryPointLights.clear();
  822. world->temporaryDirectedLights.clear();
  823. }
  824. void spriteWorld_draw(SpriteWorld& world, AlignedImageRgbaU8& colorTarget) {
  825. MUST_EXIST(world, spriteWorld_draw);
  826. world->draw(colorTarget);
  827. }
  828. IVector3D spriteWorld_findGroundAtPixel(SpriteWorld& world, const AlignedImageRgbaU8& colorBuffer, const IVector2D& pixelLocation) {
  829. MUST_EXIST(world, spriteWorld_findGroundAtPixel);
  830. return world->ortho.pixelToMiniPosition(pixelLocation, world->cameraIndex, world->findWorldCenter(colorBuffer));
  831. }
  832. void spriteWorld_moveCameraInPixels(SpriteWorld& world, const IVector2D& pixelOffset) {
  833. MUST_EXIST(world, spriteWorld_moveCameraInPixels);
  834. if (pixelOffset.x != 0 || pixelOffset.y != 0) {
  835. world->cameraLocation = world->cameraLocation + world->ortho.pixelToMiniOffset(pixelOffset, world->cameraIndex);
  836. world->dirtyBackground.allDirty();
  837. }
  838. }
  839. AlignedImageRgbaU8 spriteWorld_getDiffuseBuffer(SpriteWorld& world) {
  840. MUST_EXIST(world, spriteWorld_getDiffuseBuffer);
  841. return world->diffuseBuffer;
  842. }
  843. OrderedImageRgbaU8 spriteWorld_getNormalBuffer(SpriteWorld& world) {
  844. MUST_EXIST(world, spriteWorld_getNormalBuffer);
  845. return world->normalBuffer;
  846. }
  847. OrderedImageRgbaU8 spriteWorld_getLightBuffer(SpriteWorld& world) {
  848. MUST_EXIST(world, spriteWorld_getLightBuffer);
  849. return world->lightBuffer;
  850. }
  851. AlignedImageF32 spriteWorld_getHeightBuffer(SpriteWorld& world) {
  852. MUST_EXIST(world, spriteWorld_getHeightBuffer);
  853. return world->heightBuffer;
  854. }
  855. int spriteWorld_getCameraDirectionIndex(SpriteWorld& world) {
  856. MUST_EXIST(world, spriteWorld_getCameraDirectionIndex);
  857. return world->cameraIndex;
  858. }
  859. void spriteWorld_setCameraDirectionIndex(SpriteWorld& world, int index) {
  860. MUST_EXIST(world, spriteWorld_setCameraDirectionIndex);
  861. if (index != world->cameraIndex) {
  862. world->cameraIndex = index;
  863. world->dirtyBackground.allDirty();
  864. }
  865. }
  866. static FVector3D FVector4Dto3D(FVector4D v) {
  867. return FVector3D(v.x, v.y, v.z);
  868. }
  869. static FVector2D FVector3Dto2D(FVector3D v) {
  870. return FVector2D(v.x, v.y);
  871. }
  872. // Get the pixel bound from a projected vertex point in floating pixel coordinates
  873. static IRect boundFromVertex(const FVector3D& screenProjection) {
  874. return IRect((int)(screenProjection.x), (int)(screenProjection.y), 1, 1);
  875. }
  876. // Returns true iff the box might be seen using a pessimistic test
  877. static IRect boundingBoxToRectangle(const FVector3D& minBound, const FVector3D& maxBound, const Transform3D& objectToScreenSpace) {
  878. FVector3D points[8];
  879. transformCorners(minBound, maxBound, objectToScreenSpace, points);
  880. IRect result = boundFromVertex(points[0]);
  881. for (int p = 1; p < 8; p++) {
  882. result = IRect::merge(result, boundFromVertex(points[p]));
  883. }
  884. return result;
  885. }
  886. static IRect getBackCulledTriangleBound(const FVector3D& a, const FVector3D& b, const FVector3D& c) {
  887. if (((c.x - a.x) * (b.y - a.y)) + ((c.y - a.y) * (a.x - b.x)) >= 0.0f) {
  888. // Back facing
  889. return IRect();
  890. } else {
  891. // Front facing
  892. int leftBound = (int)std::min(std::min(a.x, b.x), c.x);
  893. int topBound = (int)std::min(std::min(a.y, b.y), c.y);
  894. int rightBound = (int)(std::max(std::max(a.x, b.x), c.x)) + 1;
  895. int bottomBound = (int)(std::max(std::max(a.y, b.y), c.y)) + 1;
  896. return IRect(leftBound, topBound, rightBound - leftBound, bottomBound - topBound);
  897. }
  898. }
  899. static FVector3D normalFromPoints(const FVector3D& A, const FVector3D& B, const FVector3D& C) {
  900. return normalize(crossProduct(B - A, C - A));
  901. }
  902. static FVector3D getAverageNormal(const Model& model, int part, int poly) {
  903. int vertexCount = model_getPolygonVertexCount(model, part, poly);
  904. FVector3D normalSum;
  905. for (int t = 0; t < vertexCount - 2; t++) {
  906. normalSum = normalSum + normalFromPoints(
  907. model_getVertexPosition(model, part, poly, 0),
  908. model_getVertexPosition(model, part, poly, t + 1),
  909. model_getVertexPosition(model, part, poly, t + 2)
  910. );
  911. }
  912. return normalize(normalSum);
  913. }
  914. DenseModel DenseModel_create(const Model& original) {
  915. return std::make_shared<DenseModelImpl>(original);
  916. }
  917. static int getTriangleCount(const Model& original) {
  918. int triangleCount = 0;
  919. for (int part = 0; part < model_getNumberOfParts(original); part++) {
  920. for (int poly = 0; poly < model_getNumberOfPolygons(original, part); poly++) {
  921. int vertexCount = model_getPolygonVertexCount(original, part, poly);
  922. triangleCount += vertexCount - 2;
  923. }
  924. }
  925. return triangleCount;
  926. }
  927. DenseModelImpl::DenseModelImpl(const Model& original)
  928. : triangles(getTriangleCount(original), DenseTriangle()) {
  929. // Get the bounding box
  930. model_getBoundingBox(original, this->minBound, this->maxBound);
  931. // Generate normals
  932. int pointCount = model_getNumberOfPoints(original);
  933. Array<FVector3D> normalPoints(pointCount, FVector3D());
  934. // Calculate smooth normals in object-space, by adding each polygon's normal to each child vertex
  935. for (int part = 0; part < model_getNumberOfParts(original); part++) {
  936. for (int poly = 0; poly < model_getNumberOfPolygons(original, part); poly++) {
  937. FVector3D polygonNormal = getAverageNormal(original, part, poly);
  938. for (int vert = 0; vert < model_getPolygonVertexCount(original, part, poly); vert++) {
  939. int point = model_getVertexPointIndex(original, part, poly, vert);
  940. normalPoints[point] = normalPoints[point] + polygonNormal;
  941. }
  942. }
  943. }
  944. // Normalize the result per vertex, to avoid having unbalanced weights when normalizing per pixel
  945. for (int point = 0; point < pointCount; point++) {
  946. normalPoints[point] = normalize(normalPoints[point]);
  947. }
  948. // Generate a simpler triangle structure
  949. int triangleIndex = 0;
  950. for (int part = 0; part < model_getNumberOfParts(original); part++) {
  951. for (int poly = 0; poly < model_getNumberOfPolygons(original, part); poly++) {
  952. int vertexCount = model_getPolygonVertexCount(original, part, poly);
  953. int vertA = 0;
  954. int indexA = model_getVertexPointIndex(original, part, poly, vertA);
  955. for (int vertB = 1; vertB < vertexCount - 1; vertB++) {
  956. int vertC = vertB + 1;
  957. int indexB = model_getVertexPointIndex(original, part, poly, vertB);
  958. int indexC = model_getVertexPointIndex(original, part, poly, vertC);
  959. triangles[triangleIndex] =
  960. DenseTriangle(
  961. FVector4Dto3D(model_getVertexColor(original, part, poly, vertA)) * 255.0f,
  962. FVector4Dto3D(model_getVertexColor(original, part, poly, vertB)) * 255.0f,
  963. FVector4Dto3D(model_getVertexColor(original, part, poly, vertC)) * 255.0f,
  964. model_getPoint(original, indexA), model_getPoint(original, indexB), model_getPoint(original, indexC),
  965. normalPoints[indexA], normalPoints[indexB], normalPoints[indexC]
  966. );
  967. triangleIndex++;
  968. }
  969. }
  970. }
  971. }
  972. // Pre-conditions:
  973. // * All images must exist and have the same dimensions
  974. // * diffuseTarget and normalTarget must have RGBA pack order
  975. // * All triangles in model must be contained within the image bounds after being projected using view
  976. // Post-condition:
  977. // Returns the dirty pixel bound based on projected positions
  978. // worldOrigin is the perceived world's origin in target pixel coordinates
  979. // modelToWorldSpace is used to place the model freely in the world
  980. template <bool HIGH_QUALITY>
  981. static IRect renderDenseModel(const DenseModel& model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, const FVector2D& worldOrigin, const Transform3D& modelToWorldSpace) {
  982. // Combine position transforms
  983. Transform3D objectToScreenSpace = combineModelToScreenTransform(modelToWorldSpace, view.worldSpaceToScreenDepth, worldOrigin);
  984. // Create a pessimistic 2D bound from the 3D bounding box
  985. IRect pessimisticBound = boundingBoxToRectangle(model->minBound, model->maxBound, objectToScreenSpace);
  986. // Get the target image bound
  987. IRect clipBound = image_getBound(depthBuffer);
  988. // Fast culling test
  989. if (!IRect::overlaps(pessimisticBound, clipBound)) {
  990. // Nothing drawn, no dirty rectangle
  991. return IRect();
  992. }
  993. // Combine normal transforms
  994. FMatrix3x3 modelToNormalSpace = modelToWorldSpace.transform * transpose(view.normalToWorldSpace);
  995. // Get image properties
  996. int diffuseStride = image_getStride(diffuseTarget);
  997. int normalStride = image_getStride(normalTarget);
  998. int heightStride = image_getStride(depthBuffer);
  999. // Call getters in advance to avoid call overhead in the loops
  1000. SafePointer<uint32_t> diffuseData = image_getSafePointer(diffuseTarget);
  1001. SafePointer<uint32_t> normalData = image_getSafePointer(normalTarget);
  1002. SafePointer<float> heightData = image_getSafePointer(depthBuffer);
  1003. // Render triangles
  1004. for (int tri = 0; tri < model->triangles.length(); tri++) {
  1005. DenseTriangle triangle = model->triangles[tri];
  1006. // Transform positions
  1007. FVector3D projectedA = objectToScreenSpace.transformPoint(triangle.posA);
  1008. FVector3D projectedB = objectToScreenSpace.transformPoint(triangle.posB);
  1009. FVector3D projectedC = objectToScreenSpace.transformPoint(triangle.posC);
  1010. IRect triangleBound = IRect::cut(clipBound, getBackCulledTriangleBound(projectedA, projectedB, projectedC));
  1011. if (triangleBound.hasArea()) {
  1012. // Find the first row
  1013. SafePointer<uint32_t> diffuseRow = diffuseData;
  1014. diffuseRow.increaseBytes(diffuseStride * triangleBound.top());
  1015. SafePointer<uint32_t> normalRow = normalData;
  1016. normalRow.increaseBytes(normalStride * triangleBound.top());
  1017. SafePointer<float> heightRow = heightData;
  1018. heightRow.increaseBytes(heightStride * triangleBound.top());
  1019. // Pre-compute matrix inverse for vertex weights
  1020. FVector2D cornerA = FVector3Dto2D(projectedA);
  1021. FVector2D cornerB = FVector3Dto2D(projectedB);
  1022. FVector2D cornerC = FVector3Dto2D(projectedC);
  1023. FMatrix2x2 offsetToWeight = inverse(FMatrix2x2(cornerB - cornerA, cornerC - cornerA));
  1024. // Transform normals
  1025. FVector3D normalA = modelToNormalSpace.transform(triangle.normalA);
  1026. FVector3D normalB = modelToNormalSpace.transform(triangle.normalB);
  1027. FVector3D normalC = modelToNormalSpace.transform(triangle.normalC);
  1028. // Iterate over the triangle's bounding box
  1029. for (int y = triangleBound.top(); y < triangleBound.bottom(); y++) {
  1030. SafePointer<uint32_t> diffusePixel = diffuseRow + triangleBound.left();
  1031. SafePointer<uint32_t> normalPixel = normalRow + triangleBound.left();
  1032. SafePointer<float> heightPixel = heightRow + triangleBound.left();
  1033. for (int x = triangleBound.left(); x < triangleBound.right(); x++) {
  1034. FVector2D weightBC = offsetToWeight.transform(FVector2D(x + 0.5f, y + 0.5f) - cornerA);
  1035. FVector3D weight = FVector3D(1.0f - (weightBC.x + weightBC.y), weightBC.x, weightBC.y);
  1036. // Check if the pixel is inside the triangle
  1037. if (weight.x >= 0.0f && weight.y >= 0.0f && weight.z >= 0.0f ) {
  1038. float height = interpolateUsingAffineWeight(projectedA.z, projectedB.z, projectedC.z, weight);
  1039. if (height > *heightPixel) {
  1040. FVector3D vertexColor = interpolateUsingAffineWeight(triangle.colorA, triangle.colorB, triangle.colorC, weight);
  1041. *heightPixel = height;
  1042. // Write data directly without saturation (Do not use colors outside of the visible range!)
  1043. *diffusePixel = ((uint32_t)vertexColor.x) | ENDIAN_POS_ADDR(((uint32_t)vertexColor.y), 8) | ENDIAN_POS_ADDR(((uint32_t)vertexColor.z), 16) | ENDIAN_POS_ADDR(255, 24);
  1044. if (HIGH_QUALITY) {
  1045. FVector3D normal = (normalize(interpolateUsingAffineWeight(normalA, normalB, normalC, weight)) + 1.0f) * 127.5f;
  1046. *normalPixel = ((uint32_t)normal.x) | ENDIAN_POS_ADDR(((uint32_t)normal.y), 8) | ENDIAN_POS_ADDR(((uint32_t)normal.z), 16) | ENDIAN_POS_ADDR(255, 24);
  1047. } else {
  1048. FVector3D normal = (interpolateUsingAffineWeight(normalA, normalB, normalC, weight) + 1.0f) * 127.5f;
  1049. *normalPixel = ((uint32_t)normal.x) | ENDIAN_POS_ADDR(((uint32_t)normal.y), 8) | ENDIAN_POS_ADDR(((uint32_t)normal.z), 16) | ENDIAN_POS_ADDR(255, 24);
  1050. }
  1051. }
  1052. }
  1053. diffusePixel += 1;
  1054. normalPixel += 1;
  1055. heightPixel += 1;
  1056. }
  1057. diffuseRow.increaseBytes(diffuseStride);
  1058. normalRow.increaseBytes(normalStride);
  1059. heightRow.increaseBytes(heightStride);
  1060. }
  1061. }
  1062. }
  1063. return pessimisticBound;
  1064. }
  1065. void sprite_generateFromModel(ImageRgbaU8& targetAtlas, String& targetConfigText, const Model& visibleModel, const Model& shadowModel, const OrthoSystem& ortho, const String& targetPath, int cameraAngles) {
  1066. // Validate input
  1067. if (cameraAngles < 1) {
  1068. printText(" Need at least one camera angle to generate a sprite!\n");
  1069. return;
  1070. } else if (!model_exists(visibleModel)) {
  1071. printText(" There's nothing to render, because visible model does not exist!\n");
  1072. return;
  1073. } else if (model_getNumberOfParts(visibleModel) == 0) {
  1074. printText(" There's nothing to render in the visible model, because there are no parts in the visible model!\n");
  1075. return;
  1076. } else {
  1077. // Measure the bounding cylinder for determining the uncropped image size
  1078. FVector3D minBound, maxBound;
  1079. model_getBoundingBox(visibleModel, minBound, maxBound);
  1080. // Check if generating a bound failed
  1081. if (minBound.x > maxBound.x) {
  1082. printText(" There's nothing visible in the model, because the 3D bounding box had no points to be created from!\n");
  1083. return;
  1084. }
  1085. printText(" Representing height from ", minBound.y, " to ", maxBound.y, " encoded using 8-bits\n");
  1086. // Calculate initial image size
  1087. float worstCaseDiameter = (std::max(maxBound.x, -minBound.x) + std::max(maxBound.y, -minBound.y) + std::max(maxBound.z, -minBound.z)) * 2;
  1088. int maxRes = roundUp(worstCaseDiameter * ortho.pixelsPerTile, 2) + 4; // Round up to even pixels and add 4 padding pixels
  1089. // Allocate square images from the pessimistic size estimation
  1090. int width = maxRes;
  1091. int height = maxRes;
  1092. ImageF32 depthBuffer = image_create_F32(width, height);
  1093. ImageRgbaU8 colorImage[cameraAngles];
  1094. ImageRgbaU8 heightImage[cameraAngles];
  1095. ImageRgbaU8 normalImage[cameraAngles];
  1096. for (int a = 0; a < cameraAngles; a++) {
  1097. colorImage[a] = image_create_RgbaU8(width, height);
  1098. heightImage[a] = image_create_RgbaU8(width, height);
  1099. normalImage[a] = image_create_RgbaU8(width, height);
  1100. }
  1101. // Generate the optimized model structure with normals
  1102. DenseModel denseModel = DenseModel_create(visibleModel);
  1103. // Render the model to multiple render targets at once
  1104. float heightScale = 255.0f / (maxBound.y - minBound.y);
  1105. for (int a = 0; a < cameraAngles; a++) {
  1106. image_fill(depthBuffer, -1000000000.0f);
  1107. image_fill(colorImage[a], ColorRgbaI32(0, 0, 0, 0));
  1108. FVector2D origin = FVector2D((float)width * 0.5f, (float)height * 0.5f);
  1109. renderDenseModel<true>(denseModel, ortho.view[a], depthBuffer, colorImage[a], normalImage[a], origin, Transform3D());
  1110. // Convert height into an 8 bit channel for saving
  1111. for (int y = 0; y < height; y++) {
  1112. for (int x = 0; x < width; x++) {
  1113. int32_t opacityPixel = image_readPixel_clamp(colorImage[a], x, y).alpha;
  1114. int32_t heightPixel = (image_readPixel_clamp(depthBuffer, x, y) - minBound.y) * heightScale;
  1115. image_writePixel(heightImage[a], x, y, ColorRgbaI32(heightPixel, 0, 0, opacityPixel));
  1116. }
  1117. }
  1118. }
  1119. // Crop all images uniformly for easy atlas packing
  1120. int32_t minX = width;
  1121. int32_t minY = height;
  1122. int32_t maxX = 0;
  1123. int32_t maxY = 0;
  1124. for (int a = 0; a < cameraAngles; a++) {
  1125. for (int y = 0; y < height; y++) {
  1126. for (int x = 0; x < width; x++) {
  1127. if (image_readPixel_border(colorImage[a], x, y).alpha) {
  1128. if (x < minX) minX = x;
  1129. if (x > maxX) maxX = x;
  1130. if (y < minY) minY = y;
  1131. if (y > maxY) maxY = y;
  1132. }
  1133. }
  1134. }
  1135. }
  1136. // Check if cropping failed
  1137. if (minX > maxX) {
  1138. printText(" There's nothing visible in the model, because cropping the final images returned nothing!\n");
  1139. return;
  1140. }
  1141. IRect cropRegion = IRect(minX, minY, (maxX + 1) - minX, (maxY + 1) - minY);
  1142. if (cropRegion.width() < 1 || cropRegion.height() < 1) {
  1143. printText(" Cropping failed to find any drawn pixels!\n");
  1144. return;
  1145. }
  1146. for (int a = 0; a < cameraAngles; a++) {
  1147. colorImage[a] = image_getSubImage(colorImage[a], cropRegion);
  1148. heightImage[a] = image_getSubImage(heightImage[a], cropRegion);
  1149. normalImage[a] = image_getSubImage(normalImage[a], cropRegion);
  1150. }
  1151. int croppedWidth = cropRegion.width();
  1152. int croppedHeight = cropRegion.height();
  1153. int centerX = width / 2 - cropRegion.left();
  1154. int centerY = height / 2 - cropRegion.top();
  1155. printText(" Cropped images of ", croppedWidth, "x", croppedHeight, " pixels with centers at (", centerX, ", ", centerY, ")\n");
  1156. // Pack everything into an image atlas
  1157. targetAtlas = image_create_RgbaU8(croppedWidth * 3, croppedHeight * cameraAngles);
  1158. for (int a = 0; a < cameraAngles; a++) {
  1159. draw_copy(targetAtlas, colorImage[a], 0, a * croppedHeight);
  1160. draw_copy(targetAtlas, heightImage[a], croppedWidth, a * croppedHeight);
  1161. draw_copy(targetAtlas, normalImage[a], croppedWidth * 2, a * croppedHeight);
  1162. }
  1163. SpriteConfig config = SpriteConfig(centerX, centerY, cameraAngles, 3, minBound, maxBound);
  1164. if (model_exists(shadowModel) && model_getNumberOfPoints(shadowModel) > 0) {
  1165. config.appendShadow(shadowModel);
  1166. }
  1167. targetConfigText = config.toIni();
  1168. }
  1169. }
  1170. // Allowing the last decimals to deviate a bit because floating-point operations are rounded differently between computers
  1171. static bool approximateTextMatch(const ReadableString &a, const ReadableString &b, double tolerance = 0.00002) {
  1172. int readerA = 0, readerB = 0;
  1173. while (readerA < string_length(a) && readerB < string_length(b)) {
  1174. DsrChar charA = a[readerA];
  1175. DsrChar charB = b[readerB];
  1176. if (character_isValueCharacter(charA) && character_isValueCharacter(charB)) {
  1177. // Scan forward on both sides while consuming content and comparing the actual value
  1178. int startA = readerA;
  1179. int startB = readerB;
  1180. // Only move forward on valid characters
  1181. if (a[readerA] == U'-') { readerA++; }
  1182. if (b[readerB] == U'-') { readerB++; }
  1183. while (character_isDigit(a[readerA])) { readerA++; }
  1184. while (character_isDigit(b[readerB])) { readerB++; }
  1185. if (a[readerA] == U'.') { readerA++; }
  1186. if (b[readerB] == U'.') { readerB++; }
  1187. while (character_isDigit(a[readerA])) { readerA++; }
  1188. while (character_isDigit(b[readerB])) { readerB++; }
  1189. // Approximate values
  1190. double valueA = string_toDouble(string_exclusiveRange(a, startA, readerA));
  1191. double valueB = string_toDouble(string_exclusiveRange(b, startB, readerB));
  1192. // Check the difference
  1193. double diff = valueB - valueA;
  1194. if (diff > tolerance || diff < -tolerance) {
  1195. // Too big difference, this is probably not a rounding error
  1196. return false;
  1197. }
  1198. } else if (charA != charB) {
  1199. // Difference with a non-value involved
  1200. return false;
  1201. }
  1202. readerA++;
  1203. readerB++;
  1204. }
  1205. if (readerA < string_length(a) - 1 || readerB < string_length(b) - 1) {
  1206. // One text had unmatched remains after the other reached its end
  1207. return false;
  1208. } else {
  1209. return true;
  1210. }
  1211. }
  1212. void sprite_generateFromModel(const Model& visibleModel, const Model& shadowModel, const OrthoSystem& ortho, const String& targetPath, int cameraAngles, bool debug) {
  1213. // Generate an image and a configuration file from the visible model
  1214. ImageRgbaU8 atlasImage; String configText;
  1215. sprite_generateFromModel(atlasImage, configText, visibleModel, shadowModel, ortho, targetPath, cameraAngles);
  1216. // Save the result on success
  1217. if (string_length(configText) > 0) {
  1218. // Save the atlas
  1219. String atlasPath = targetPath + U".png";
  1220. // Try loading any existing image
  1221. ImageRgbaU8 existingAtlasImage = image_load_RgbaU8(atlasPath, false);
  1222. if (image_exists(existingAtlasImage)) {
  1223. int difference = image_maxDifference(atlasImage, existingAtlasImage);
  1224. if (difference <= 2) {
  1225. printText(" No significant changes against ", targetPath, ".\n");
  1226. } else {
  1227. image_save(atlasImage, atlasPath);
  1228. printText(" Updated ", targetPath, " with a deviation of ", difference, ".\n");
  1229. }
  1230. } else {
  1231. // Only save if there was no existing image or it differed significantly from the new result
  1232. // This comparison is made to avoid flooding version history with changes from invisible differences in color rounding
  1233. image_save(atlasImage, atlasPath);
  1234. printText(" Saved atlas to ", targetPath, ".\n");
  1235. }
  1236. // Save the configuration
  1237. String configPath = targetPath + U".ini";
  1238. String oldConfixText = string_load(configPath, false);
  1239. if (approximateTextMatch(configText, oldConfixText)) {
  1240. printText(" No significant changes against ", targetPath, ".\n\n");
  1241. } else {
  1242. string_save(targetPath + U".ini", configText);
  1243. printText(" Saved sprite config to ", targetPath, ".\n\n");
  1244. }
  1245. if (debug) {
  1246. ImageRgbaU8 debugImage; String garbageText;
  1247. // TODO: Show overlap between visible and shadow so that shadow outside of visible is displayed as bright red on a dark model.
  1248. // The number of visible shadow pixels should be reported automatically
  1249. // in an error message at the end of the total execution together with file names.
  1250. sprite_generateFromModel(debugImage, garbageText, shadowModel, Model(), ortho, targetPath + U"Debug", 8);
  1251. image_save(debugImage, targetPath + U"Debug.png");
  1252. }
  1253. }
  1254. }
  1255. }