spriteAPI.cpp 69 KB

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