spriteAPI.cpp 56 KB

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