spriteAPI.cpp 55 KB

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