spriteAPI.cpp 59 KB

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