spriteAPI.cpp 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. #include "spriteAPI.h"
  2. #include "Octree.h"
  3. #include "../../../DFPSR/render/ITriangle2D.h"
  4. namespace dsr {
  5. struct SpriteConfig {
  6. int centerX, centerY; // The sprite's origin in pixels relative to the upper left corner
  7. int frameRows; // The atlas has one row for each frame
  8. int propertyColumns; // The atlas has one column for each type of information
  9. // The 3D model's bound in tile units
  10. // The height image goes from 0 at minimum Y to 255 at maximum Y
  11. FVector3D minBound, maxBound;
  12. // Shadow shapes
  13. List<FVector3D> points; // 3D points for the triangles to refer to by index
  14. List<int32_t> triangleIndices; // Triangle indices stored in multiples of three integers
  15. // Construction
  16. SpriteConfig(int centerX, int centerY, int frameRows, int propertyColumns, FVector3D minBound, FVector3D maxBound)
  17. : centerX(centerX), centerY(centerY), frameRows(frameRows), propertyColumns(propertyColumns), minBound(minBound), maxBound(maxBound) {}
  18. explicit SpriteConfig(const ReadableString& content) {
  19. config_parse_ini(content, [this](const ReadableString& block, const ReadableString& key, const ReadableString& value) {
  20. if (block.length() == 0) {
  21. if (string_caseInsensitiveMatch(key, U"CenterX")) {
  22. this->centerX = string_parseInteger(value);
  23. } else if (string_caseInsensitiveMatch(key, U"CenterY")) {
  24. this->centerY = string_parseInteger(value);
  25. } else if (string_caseInsensitiveMatch(key, U"FrameRows")) {
  26. this->frameRows = string_parseInteger(value);
  27. } else if (string_caseInsensitiveMatch(key, U"PropertyColumns")) {
  28. this->propertyColumns = string_parseInteger(value);
  29. } else if (string_caseInsensitiveMatch(key, U"MinBound")) {
  30. this->minBound = parseFVector3D(value);
  31. } else if (string_caseInsensitiveMatch(key, U"MaxBound")) {
  32. this->maxBound = parseFVector3D(value);
  33. } else if (string_caseInsensitiveMatch(key, U"Points")) {
  34. List<ReadableString> values = value.split(U',');
  35. if (values.length() % 3 != 0) {
  36. throwError("Points contained ", values.length(), " values, which is not evenly divisible by three!");
  37. } else {
  38. this->points.clear();
  39. this->points.reserve(values.length() / 3);
  40. for (int v = 0; v < values.length(); v += 3) {
  41. this->points.push(FVector3D(string_parseDouble(values[v]), string_parseDouble(values[v+1]), string_parseDouble(values[v+2])));
  42. }
  43. }
  44. } else if (string_caseInsensitiveMatch(key, U"TriangleIndices")) {
  45. List<ReadableString> values = value.split(U',');
  46. if (values.length() % 3 != 0) {
  47. throwError("TriangleIndices contained ", values.length(), " values, which is not evenly divisible by three!");
  48. } else {
  49. this->triangleIndices.clear();
  50. this->triangleIndices.reserve(values.length());
  51. for (int v = 0; v < values.length(); v++) {
  52. this->triangleIndices.push(string_parseInteger(values[v]));
  53. }
  54. }
  55. } else {
  56. printText("Unrecognized key \"", key, "\" in sprite configuration file.\n");
  57. }
  58. } else {
  59. printText("Unrecognized block \"", block, "\" in sprite configuration file.\n");
  60. }
  61. });
  62. }
  63. // Add model as a persistent shadow caster in the sprite configuration
  64. void appendShadow(const Model& model) {
  65. points.reserve(this->points.length() + model_getNumberOfPoints(model));
  66. for (int p = 0; p < model_getNumberOfPoints(model); p++) {
  67. this->points.push(model_getPoint(model, p));
  68. }
  69. for (int part = 0; part < model_getNumberOfParts(model); part++) {
  70. for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
  71. int vertexCount = model_getPolygonVertexCount(model, part, poly);
  72. int vertA = 0;
  73. int indexA = model_getVertexPointIndex(model, part, poly, vertA);
  74. for (int vertB = 1; vertB < vertexCount - 1; vertB++) {
  75. int vertC = vertB + 1;
  76. int indexB = model_getVertexPointIndex(model, part, poly, vertB);
  77. int indexC = model_getVertexPointIndex(model, part, poly, vertC);
  78. triangleIndices.push(indexA); triangleIndices.push(indexB); triangleIndices.push(indexC);
  79. }
  80. }
  81. }
  82. }
  83. String toIni() {
  84. // General information
  85. String result = string_combine(
  86. U"; Sprite configuration file\n",
  87. U"CenterX=", this->centerX, "\n",
  88. U"CenterY=", this->centerY, "\n",
  89. U"FrameRows=", this->frameRows, "\n",
  90. U"PropertyColumns=", this->propertyColumns, "\n",
  91. U"MinBound=", this->minBound, "\n",
  92. U"MaxBound=", this->maxBound, "\n"
  93. );
  94. // Low-resolution 3D shape
  95. if (this->points.length() > 0) {
  96. string_append(result, U"Points=");
  97. for (int p = 0; p < this->points.length(); p++) {
  98. if (p > 0) {
  99. string_append(result, U", ");
  100. }
  101. string_append(result, this->points[p]);
  102. }
  103. string_append(result, U"\n");
  104. string_append(result, U"TriangleIndices=");
  105. for (int i = 0; i < this->triangleIndices.length(); i+=3) {
  106. if (i > 0) {
  107. string_append(result, U", ");
  108. }
  109. string_append(result, this->triangleIndices[i], U",", this->triangleIndices[i+1], U",", this->triangleIndices[i+2]);
  110. }
  111. string_append(result, U"\n");
  112. }
  113. return result;
  114. }
  115. };
  116. static ImageF32 scaleHeightImage(const ImageRgbaU8& heightImage, float minHeight, float maxHeight, const ImageRgbaU8& colorImage) {
  117. float scale = (maxHeight - minHeight) / 255.0f;
  118. float offset = minHeight;
  119. int width = image_getWidth(heightImage);
  120. int height = image_getHeight(heightImage);
  121. ImageF32 result = image_create_F32(width, height);
  122. for (int y = 0; y < height; y++) {
  123. for (int x = 0; x < width; x++) {
  124. float value = image_readPixel_clamp(heightImage, x, y).red;
  125. if (image_readPixel_clamp(colorImage, x, y).alpha > 127) {
  126. image_writePixel(result, x, y, (value * scale) + offset);
  127. } else {
  128. image_writePixel(result, x, y, -std::numeric_limits<float>::infinity());
  129. }
  130. }
  131. }
  132. return result;
  133. }
  134. struct SpriteFrame {
  135. IVector2D centerPoint;
  136. ImageRgbaU8 colorImage; // (Red, Green, Blue, _)
  137. ImageRgbaU8 normalImage; // (NormalX, NormalY, NormalZ, _)
  138. ImageF32 heightImage;
  139. SpriteFrame(const IVector2D& centerPoint, const ImageRgbaU8& colorImage, const ImageRgbaU8& normalImage, const ImageF32& heightImage)
  140. : centerPoint(centerPoint), colorImage(colorImage), normalImage(normalImage), heightImage(heightImage) {}
  141. };
  142. struct SpriteType {
  143. public:
  144. IVector3D minBoundMini, maxBoundMini;
  145. List<SpriteFrame> frames;
  146. // TODO: Compress the data using a shadow-only model type of only positions and triangle indices in a single part.
  147. // The shadow model will have its own rendering method excluding the color target.
  148. // Shadow rendering can be a lot simpler by not calculating any vertex weights
  149. // just interpolate the depth using addition, compare to the old value and write the new depth value.
  150. Model shadowModel;
  151. public:
  152. // folderPath should end with a path separator
  153. SpriteType(const String& folderPath, const String& spriteName) {
  154. // Load the image atlas
  155. ImageRgbaU8 loadedAtlas = image_load_RgbaU8(string_combine(folderPath, spriteName, U".png"));
  156. // Load the settings
  157. const SpriteConfig configuration = SpriteConfig(string_load(string_combine(folderPath, spriteName, U".ini")));
  158. this->minBoundMini = IVector3D(
  159. floor(configuration.minBound.x * ortho_miniUnitsPerTile),
  160. floor(configuration.minBound.y * ortho_miniUnitsPerTile),
  161. floor(configuration.minBound.z * ortho_miniUnitsPerTile)
  162. );
  163. this->maxBoundMini = IVector3D(
  164. ceil(configuration.maxBound.x * ortho_miniUnitsPerTile),
  165. ceil(configuration.maxBound.y * ortho_miniUnitsPerTile),
  166. ceil(configuration.maxBound.z * ortho_miniUnitsPerTile)
  167. );
  168. int width = image_getWidth(loadedAtlas) / configuration.propertyColumns;
  169. int height = image_getHeight(loadedAtlas) / configuration.frameRows;
  170. for (int a = 0; a < configuration.frameRows; a++) {
  171. ImageRgbaU8 colorImage = image_getSubImage(loadedAtlas, IRect(0, a * height, width, height));
  172. ImageRgbaU8 heightImage = image_getSubImage(loadedAtlas, IRect(width, a * height, width, height));
  173. ImageRgbaU8 normalImage = image_getSubImage(loadedAtlas, IRect(width * 2, a * height, width, height));
  174. ImageF32 scaledHeightImage = scaleHeightImage(heightImage, configuration.minBound.y, configuration.maxBound.y, colorImage);
  175. this->frames.pushConstruct(IVector2D(configuration.centerX, configuration.centerY), colorImage, normalImage, scaledHeightImage);
  176. }
  177. // Create a model for rendering shadows
  178. if (configuration.points.length() > 0) {
  179. this->shadowModel = model_create();
  180. for (int p = 0; p < configuration.points.length(); p++) {
  181. model_addPoint(this->shadowModel, configuration.points[p]);
  182. }
  183. model_addEmptyPart(this->shadowModel, U"Shadow");
  184. for (int t = 0; t < configuration.triangleIndices.length(); t+=3) {
  185. model_addTriangle(this->shadowModel, 0, configuration.triangleIndices[t], configuration.triangleIndices[t+1], configuration.triangleIndices[t+2]);
  186. }
  187. }
  188. }
  189. public:
  190. // TODO: Force frame count to a power of two or replace modulo with look-up tables in sprite configurations.
  191. int getFrameIndex(Direction direction) {
  192. const int frameFromDir[dir360] = {4, 1, 5, 2, 6, 3, 7, 0};
  193. return frameFromDir[correctDirection(direction)] % this->frames.length();
  194. }
  195. };
  196. // Global list of all sprite types ever loaded
  197. List<SpriteType> types;
  198. static int getSpriteFrameIndex(const Sprite& sprite, OrthoView view) {
  199. return types[sprite.typeIndex].getFrameIndex(view.worldDirection + sprite.direction);
  200. }
  201. void drawSprite(const Sprite& sprite, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
  202. int frameIndex = getSpriteFrameIndex(sprite, ortho);
  203. const SpriteFrame* frame = &types[sprite.typeIndex].frames[frameIndex];
  204. IVector2D screenSpace = ortho.miniTilePositionToScreenPixel(sprite.location, worldCenter) - frame->centerPoint;
  205. float heightOffset = sprite.location.y * ortho_tilesPerMiniUnit;
  206. if (image_exists(targetColor)) {
  207. if (image_exists(targetNormal)) {
  208. draw_higher(targetHeight, frame->heightImage, targetColor, frame->colorImage, targetNormal, frame->normalImage, screenSpace.x, screenSpace.y, heightOffset);
  209. } else {
  210. draw_higher(targetHeight, frame->heightImage, targetColor, frame->colorImage, screenSpace.x, screenSpace.y, heightOffset);
  211. }
  212. } else {
  213. if (image_exists(targetNormal)) {
  214. draw_higher(targetHeight, frame->heightImage, targetNormal, frame->normalImage, screenSpace.x, screenSpace.y, heightOffset);
  215. } else {
  216. draw_higher(targetHeight, frame->heightImage, screenSpace.x, screenSpace.y, heightOffset);
  217. }
  218. }
  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 renderSpriteShadow(CubeMapF32& shadowTarget, const Sprite& sprite, const FMatrix3x3& normalToWorld) const {
  266. if (sprite.shadowCasting) {
  267. Model model = types[sprite.typeIndex].shadowModel;
  268. if (model_exists(model)) {
  269. // Place the model relative to the light source's position, to make rendering in light-space easier
  270. Transform3D modelToWorldTransform = Transform3D(ortho_miniToFloatingTile(sprite.location) - this->position, spriteDirections[sprite.direction]);
  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. }
  278. void renderSpriteShadows(CubeMapF32& shadowTarget, Octree<Sprite>& sprites, const FMatrix3x3& normalToWorld) const {
  279. IVector3D center = ortho_floatingTileToMini(this->position);
  280. IVector3D minBound = center - ortho_floatingTileToMini(radius);
  281. IVector3D maxBound = center + ortho_floatingTileToMini(radius);
  282. sprites.map(minBound, maxBound, [this, shadowTarget, normalToWorld](Sprite& sprite, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound) mutable {
  283. this->renderSpriteShadow(shadowTarget, sprite, normalToWorld);
  284. });
  285. }
  286. public:
  287. void illuminate(const OrthoView& camera, const IVector2D& worldCenter, OrderedImageRgbaU8& lightBuffer, const OrderedImageRgbaU8& normalBuffer, const AlignedImageF32& heightBuffer, const CubeMapF32& shadowSource) const {
  288. if (this->shadowCasting) {
  289. addPointLight(camera, worldCenter, lightBuffer, normalBuffer, heightBuffer, this->position, this->radius, this->intensity, this->color, shadowSource.cubeMap);
  290. } else {
  291. addPointLight(camera, worldCenter, lightBuffer, normalBuffer, heightBuffer, this->position, this->radius, this->intensity, this->color);
  292. }
  293. }
  294. };
  295. class DirectedLight {
  296. public:
  297. FVector3D direction; // The world-space direction
  298. float intensity; // The color's brightness multiplier (using float to allow smooth fading)
  299. ColorRgbI32 color; // The color of the light (using integers to detect when the color is uniform)
  300. public:
  301. DirectedLight(FVector3D direction, float intensity, ColorRgbI32 color)
  302. : direction(direction), intensity(intensity), color(color) {}
  303. public:
  304. void illuminate(const OrthoView& camera, const IVector2D& worldCenter, OrderedImageRgbaU8& lightBuffer, const OrderedImageRgbaU8& normalBuffer, bool overwrite = false) const {
  305. if (overwrite) {
  306. setDirectedLight(camera, lightBuffer, normalBuffer, this->direction, this->intensity, this->color);
  307. } else {
  308. addDirectedLight(camera, lightBuffer, normalBuffer, this->direction, this->intensity, this->color);
  309. }
  310. }
  311. };
  312. enum class BlockState {
  313. Unused,
  314. Ready,
  315. Dirty
  316. };
  317. class BackgroundBlock {
  318. public:
  319. static const int blockSize = 512;
  320. static const int maxDistance = blockSize * 2;
  321. IRect worldRegion;
  322. int cameraId = 0;
  323. BlockState state = BlockState::Unused;
  324. OrderedImageRgbaU8 diffuseBuffer;
  325. OrderedImageRgbaU8 normalBuffer;
  326. AlignedImageF32 heightBuffer;
  327. private:
  328. IVector3D getBoxCorner(const IVector3D& minBound, const IVector3D& maxBound, int cornerIndex) {
  329. assert(cornerIndex >= 0 && cornerIndex < 8);
  330. return IVector3D(
  331. ((uint32_t)cornerIndex & 1u) ? maxBound.x : minBound.x,
  332. ((uint32_t)cornerIndex & 2u) ? maxBound.y : minBound.y,
  333. ((uint32_t)cornerIndex & 4u) ? maxBound.z : minBound.z
  334. );
  335. }
  336. // Pre-condition: diffuseBuffer must be cleared unless sprites cover the whole block
  337. void draw(Octree<Sprite>& sprites, const OrthoView& ortho) {
  338. image_fill(this->normalBuffer, ColorRgbaI32(128));
  339. image_fill(this->heightBuffer, -std::numeric_limits<float>::max());
  340. sprites.map(
  341. [ortho,this](const IVector3D& minBound, const IVector3D& maxBound){
  342. IVector2D corners[8];
  343. for (int c = 0; c < 8; c++) {
  344. corners[c] = ortho.miniTileOffsetToScreenPixel(getBoxCorner(minBound, maxBound, c));
  345. }
  346. if (corners[0].x < this->worldRegion.left()
  347. && corners[1].x < this->worldRegion.left()
  348. && corners[2].x < this->worldRegion.left()
  349. && corners[3].x < this->worldRegion.left()
  350. && corners[4].x < this->worldRegion.left()
  351. && corners[5].x < this->worldRegion.left()
  352. && corners[6].x < this->worldRegion.left()
  353. && corners[7].x < this->worldRegion.left()) {
  354. return false;
  355. }
  356. if (corners[0].x > this->worldRegion.right()
  357. && corners[1].x > this->worldRegion.right()
  358. && corners[2].x > this->worldRegion.right()
  359. && corners[3].x > this->worldRegion.right()
  360. && corners[4].x > this->worldRegion.right()
  361. && corners[5].x > this->worldRegion.right()
  362. && corners[6].x > this->worldRegion.right()
  363. && corners[7].x > this->worldRegion.right()) {
  364. return false;
  365. }
  366. if (corners[0].y < this->worldRegion.top()
  367. && corners[1].y < this->worldRegion.top()
  368. && corners[2].y < this->worldRegion.top()
  369. && corners[3].y < this->worldRegion.top()
  370. && corners[4].y < this->worldRegion.top()
  371. && corners[5].y < this->worldRegion.top()
  372. && corners[6].y < this->worldRegion.top()
  373. && corners[7].y < this->worldRegion.top()) {
  374. return false;
  375. }
  376. if (corners[0].y > this->worldRegion.bottom()
  377. && corners[1].y > this->worldRegion.bottom()
  378. && corners[2].y > this->worldRegion.bottom()
  379. && corners[3].y > this->worldRegion.bottom()
  380. && corners[4].y > this->worldRegion.bottom()
  381. && corners[5].y > this->worldRegion.bottom()
  382. && corners[6].y > this->worldRegion.bottom()
  383. && corners[7].y > this->worldRegion.bottom()) {
  384. return false;
  385. }
  386. return true;
  387. },
  388. [this, ortho](Sprite& sprite, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound){
  389. drawSprite(sprite, ortho, -this->worldRegion.upperLeft(), this->heightBuffer, this->diffuseBuffer, this->normalBuffer);
  390. });
  391. }
  392. public:
  393. BackgroundBlock(Octree<Sprite>& sprites, const IRect& worldRegion, const OrthoView& ortho)
  394. : worldRegion(worldRegion), cameraId(ortho.id), state(BlockState::Ready),
  395. diffuseBuffer(image_create_RgbaU8(blockSize, blockSize)),
  396. normalBuffer(image_create_RgbaU8(blockSize, blockSize)),
  397. heightBuffer(image_create_F32(blockSize, blockSize)) {
  398. this->draw(sprites, ortho);
  399. }
  400. void update(Octree<Sprite>& sprites, const IRect& worldRegion, const OrthoView& ortho) {
  401. this->worldRegion = worldRegion;
  402. this->cameraId = ortho.id;
  403. image_fill(this->diffuseBuffer, ColorRgbaI32(0));
  404. this->draw(sprites, ortho);
  405. this->state = BlockState::Ready;
  406. }
  407. void draw(OrderedImageRgbaU8& diffuseTarget, OrderedImageRgbaU8& normalTarget, AlignedImageF32& heightTarget, const IRect& seenRegion) const {
  408. if (this->state != BlockState::Unused) {
  409. int left = this->worldRegion.left() - seenRegion.left();
  410. int top = this->worldRegion.top() - seenRegion.top();
  411. draw_copy(diffuseTarget, this->diffuseBuffer, left, top);
  412. draw_copy(normalTarget, this->normalBuffer, left, top);
  413. draw_copy(heightTarget, this->heightBuffer, left, top);
  414. }
  415. }
  416. void recycle() {
  417. //printText("Recycle block at ", this->worldRegion, "\n");
  418. this->state = BlockState::Unused;
  419. this->worldRegion = IRect();
  420. this->cameraId = -1;
  421. }
  422. };
  423. // TODO: A way to delete passive sprites using search criterias for bounding box and leaf content using a boolean lambda
  424. class SpriteWorldImpl {
  425. public:
  426. // World
  427. OrthoSystem ortho;
  428. // Sprites that rarely change and can be stored in a background image. (no animations allowed)
  429. // TODO: Don't store the position twice, by keeping it separate from the Sprite struct.
  430. Octree<Sprite> passiveSprites;
  431. // Temporary things are deleted when spriteWorld_clearTemporary is called
  432. List<Sprite> temporarySprites;
  433. List<PointLight> temporaryPointLights;
  434. List<DirectedLight> temporaryDirectedLights;
  435. // View
  436. int cameraIndex = 0;
  437. IVector3D cameraLocation;
  438. // Deferred rendering
  439. OrderedImageRgbaU8 diffuseBuffer;
  440. OrderedImageRgbaU8 normalBuffer;
  441. AlignedImageF32 heightBuffer;
  442. OrderedImageRgbaU8 lightBuffer;
  443. // Passive background
  444. // TODO: How can split-screen use multiple cameras without duplicate blocks or deleting the other camera's blocks by distance?
  445. List<BackgroundBlock> backgroundBlocks;
  446. private:
  447. // Reused buffers
  448. int shadowResolution;
  449. CubeMapF32 temporaryShadowMap;
  450. public:
  451. SpriteWorldImpl(const OrthoSystem &ortho, int shadowResolution)
  452. : ortho(ortho), passiveSprites(ortho_miniUnitsPerTile * 64), shadowResolution(shadowResolution), temporaryShadowMap(shadowResolution) {}
  453. public:
  454. void updateBlockAt(const IRect& blockRegion, const IRect& seenRegion) {
  455. int unusedBlockIndex = -1;
  456. // Find an existing block
  457. for (int b = 0; b < this->backgroundBlocks.length(); b++) {
  458. BackgroundBlock* currentBlockPtr = &this->backgroundBlocks[b];
  459. if (currentBlockPtr->state != BlockState::Unused) {
  460. // Check direction
  461. if (currentBlockPtr->cameraId == this->ortho.view[this->cameraIndex].id) {
  462. // Check location
  463. if (currentBlockPtr->worldRegion.left() == blockRegion.left() && currentBlockPtr->worldRegion.top() == blockRegion.top()) {
  464. // Update if needed
  465. if (currentBlockPtr->state == BlockState::Dirty) {
  466. currentBlockPtr->update(this->passiveSprites, blockRegion, this->ortho.view[this->cameraIndex]);
  467. }
  468. // Use the block
  469. return;
  470. } else {
  471. // See if the block is too far from the camera
  472. if (currentBlockPtr->worldRegion.right() < seenRegion.left() - BackgroundBlock::maxDistance
  473. || currentBlockPtr->worldRegion.left() > seenRegion.right() + BackgroundBlock::maxDistance
  474. || currentBlockPtr->worldRegion.bottom() < seenRegion.top() - BackgroundBlock::maxDistance
  475. || currentBlockPtr->worldRegion.top() > seenRegion.bottom() + BackgroundBlock::maxDistance) {
  476. // Recycle because it's too far away
  477. currentBlockPtr->recycle();
  478. unusedBlockIndex = b;
  479. }
  480. }
  481. } else{
  482. // Recycle directly when another camera angle is used
  483. currentBlockPtr->recycle();
  484. unusedBlockIndex = b;
  485. }
  486. } else {
  487. unusedBlockIndex = b;
  488. }
  489. }
  490. // If none of them matched, we should've passed by any unused block already
  491. if (unusedBlockIndex > -1) {
  492. // We have a block to reuse
  493. this->backgroundBlocks[unusedBlockIndex].update(this->passiveSprites, blockRegion, this->ortho.view[this->cameraIndex]);
  494. } else {
  495. // Create a new block
  496. this->backgroundBlocks.pushConstruct(this->passiveSprites, blockRegion, this->ortho.view[this->cameraIndex]);
  497. }
  498. }
  499. void invalidateBlockAt(int left, int top) {
  500. // Find an existing block
  501. for (int b = 0; b < this->backgroundBlocks.length(); b++) {
  502. BackgroundBlock* currentBlockPtr = &this->backgroundBlocks[b];
  503. // Assuming that alternative camera angles will be removed when drawing next time
  504. if (currentBlockPtr->state == BlockState::Ready
  505. && currentBlockPtr->worldRegion.left() == left
  506. && currentBlockPtr->worldRegion.top() == top) {
  507. // Make dirty to force an update
  508. currentBlockPtr->state = BlockState::Dirty;
  509. }
  510. }
  511. }
  512. // Make sure that each pixel in seenRegion is occupied by an updated background block
  513. void updateBlocks(const IRect& seenRegion) {
  514. // Round inclusive pixel indices down to containing blocks and iterate over them in strides along x and y
  515. int64_t roundedLeft = roundDown(seenRegion.left(), BackgroundBlock::blockSize);
  516. int64_t roundedTop = roundDown(seenRegion.top(), BackgroundBlock::blockSize);
  517. int64_t roundedRight = roundDown(seenRegion.right() - 1, BackgroundBlock::blockSize);
  518. int64_t roundedBottom = roundDown(seenRegion.bottom() - 1, BackgroundBlock::blockSize);
  519. for (int64_t y = roundedTop; y <= roundedBottom; y += BackgroundBlock::blockSize) {
  520. for (int64_t x = roundedLeft; x <= roundedRight; x += BackgroundBlock::blockSize) {
  521. // Make sure that a block is allocated and pre-drawn at this location
  522. this->updateBlockAt(IRect(x, y, BackgroundBlock::blockSize, BackgroundBlock::blockSize), seenRegion);
  523. }
  524. }
  525. }
  526. void drawDeferred(OrderedImageRgbaU8& diffuseTarget, OrderedImageRgbaU8& normalTarget, AlignedImageF32& heightTarget, const IRect& seenRegion) {
  527. // Check image dimensions
  528. assert(image_getWidth(diffuseTarget) == seenRegion.width() && image_getHeight(diffuseTarget) == seenRegion.height());
  529. assert(image_getWidth(normalTarget) == seenRegion.width() && image_getHeight(normalTarget) == seenRegion.height());
  530. assert(image_getWidth(heightTarget) == seenRegion.width() && image_getHeight(heightTarget) == seenRegion.height());
  531. // Draw passive sprites to blocks
  532. this->updateBlocks(seenRegion);
  533. // Draw blocks to the targets
  534. for (int b = 0; b < this->backgroundBlocks.length(); b++) {
  535. this->backgroundBlocks[b].draw(diffuseTarget, normalTarget, heightTarget, seenRegion);
  536. }
  537. // Draw active sprites to the targets
  538. for (int s = 0; s < this->temporarySprites.length(); s++) {
  539. drawSprite(this->temporarySprites[s], this->ortho.view[this->cameraIndex], -seenRegion.upperLeft(), heightTarget, diffuseTarget, normalTarget);
  540. }
  541. }
  542. public:
  543. void updatePassiveRegion(const IRect& modifiedRegion) {
  544. int64_t roundedLeft = roundDown(modifiedRegion.left(), BackgroundBlock::blockSize);
  545. int64_t roundedTop = roundDown(modifiedRegion.top(), BackgroundBlock::blockSize);
  546. int64_t roundedRight = roundDown(modifiedRegion.right() - 1, BackgroundBlock::blockSize);
  547. int64_t roundedBottom = roundDown(modifiedRegion.bottom() - 1, BackgroundBlock::blockSize);
  548. for (int64_t y = roundedTop; y <= roundedBottom; y += BackgroundBlock::blockSize) {
  549. for (int64_t x = roundedLeft; x <= roundedRight; x += BackgroundBlock::blockSize) {
  550. // Make sure that a block is allocated and pre-drawn at this location
  551. this->invalidateBlockAt(x, y);
  552. }
  553. }
  554. }
  555. IVector2D findWorldCenter(const AlignedImageRgbaU8& colorTarget) const {
  556. return IVector2D(image_getWidth(colorTarget) / 2, image_getHeight(colorTarget) / 2) - this->ortho.miniTileOffsetToScreenPixel(this->cameraLocation, this->cameraIndex);
  557. }
  558. void draw(AlignedImageRgbaU8& colorTarget) {
  559. double startTime;
  560. IVector2D worldCenter = this->findWorldCenter(colorTarget);
  561. // Resize when the window has resized or the buffers haven't been allocated before
  562. int width = image_getWidth(colorTarget);
  563. int height = image_getHeight(colorTarget);
  564. if (image_getWidth(this->diffuseBuffer) != width || image_getHeight(this->diffuseBuffer) != height) {
  565. this->diffuseBuffer = image_create_RgbaU8(width, height);
  566. this->normalBuffer = image_create_RgbaU8(width, height);
  567. this->lightBuffer = image_create_RgbaU8(width, height);
  568. this->heightBuffer = image_create_F32(width, height);
  569. }
  570. IRect worldRegion = IRect(-worldCenter.x, -worldCenter.y, width, height);
  571. startTime = time_getSeconds();
  572. this->drawDeferred(this->diffuseBuffer, this->normalBuffer, this->heightBuffer, worldRegion);
  573. debugText("Draw deferred: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  574. // Illuminate using directed lights
  575. if (this->temporaryDirectedLights.length() > 0) {
  576. startTime = time_getSeconds();
  577. // Overwriting any light from the previous frame
  578. for (int p = 0; p < this->temporaryDirectedLights.length(); p++) {
  579. this->temporaryDirectedLights[p].illuminate(this->ortho.view[this->cameraIndex], worldCenter, this->lightBuffer, this->normalBuffer, p == 0);
  580. }
  581. debugText("Sun light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  582. } else {
  583. startTime = time_getSeconds();
  584. image_fill(this->lightBuffer, ColorRgbaI32(0)); // Set light to black
  585. debugText("Clear light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  586. }
  587. // Illuminate using point lights
  588. for (int p = 0; p < this->temporaryPointLights.length(); p++) {
  589. PointLight *currentLight = &this->temporaryPointLights[p];
  590. if (currentLight->shadowCasting) {
  591. startTime = time_getSeconds();
  592. this->temporaryShadowMap.clear();
  593. currentLight->renderSpriteShadows(this->temporaryShadowMap, this->passiveSprites, ortho.view[this->cameraIndex].normalToWorldSpace);
  594. for (int s = 0; s < this->temporarySprites.length(); s++) {
  595. currentLight->renderSpriteShadow(this->temporaryShadowMap, this->temporarySprites[s], ortho.view[this->cameraIndex].normalToWorldSpace);
  596. }
  597. debugText("Cast point-light shadows: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  598. }
  599. startTime = time_getSeconds();
  600. currentLight->illuminate(this->ortho.view[this->cameraIndex], worldCenter, this->lightBuffer, this->normalBuffer, this->heightBuffer, this->temporaryShadowMap);
  601. debugText("Illuminate from point-light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  602. }
  603. // Draw the final image to the target by multiplying diffuse with light
  604. startTime = time_getSeconds();
  605. blendLight(colorTarget, this->diffuseBuffer, this->lightBuffer);
  606. debugText("Blend light: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
  607. }
  608. };
  609. int sprite_loadTypeFromFile(const String& folderPath, const String& spriteName) {
  610. types.pushConstruct(folderPath, spriteName);
  611. return types.length() - 1;
  612. }
  613. int sprite_getTypeCount() {
  614. return types.length();
  615. }
  616. SpriteWorld spriteWorld_create(OrthoSystem ortho, int shadowResolution) {
  617. return std::make_shared<SpriteWorldImpl>(ortho, shadowResolution);
  618. }
  619. #define MUST_EXIST(OBJECT, METHOD) if (OBJECT.get() == nullptr) { throwError("The " #OBJECT " handle was null in " #METHOD "\n"); }
  620. void spriteWorld_addBackgroundSprite(SpriteWorld& world, const Sprite& sprite) {
  621. MUST_EXIST(world, spriteWorld_addBackgroundSprite);
  622. // TODO: Validate type index before looking up the bounding box, for easy debugging
  623. // TODO: Replace sprite.location with a separate position argument, possibly constructing in place using the API
  624. // Add the passive sprite to the octree
  625. IVector3D origin = sprite.location;
  626. IVector3D minBound = origin + types[sprite.typeIndex].minBoundMini;
  627. IVector3D maxBound = origin + types[sprite.typeIndex].maxBoundMini;
  628. world->passiveSprites.insert(sprite, origin, minBound, maxBound);
  629. // Find the affected passive region and make it dirty
  630. int frameIndex = getSpriteFrameIndex(sprite, world->ortho.view[world->cameraIndex]);
  631. const SpriteFrame* frame = &types[sprite.typeIndex].frames[frameIndex];
  632. IVector2D upperLeft = world->ortho.miniTilePositionToScreenPixel(sprite.location, world->cameraIndex, IVector2D()) - frame->centerPoint;
  633. IRect region = IRect(upperLeft.x, upperLeft.y, image_getWidth(frame->colorImage), image_getHeight(frame->colorImage));
  634. world->updatePassiveRegion(region);
  635. }
  636. void spriteWorld_addTemporarySprite(SpriteWorld& world, const Sprite& sprite) {
  637. MUST_EXIST(world, spriteWorld_addTemporarySprite);
  638. // Add the temporary sprite
  639. world->temporarySprites.push(sprite);
  640. }
  641. void spriteWorld_createTemporary_pointLight(SpriteWorld& world, const FVector3D position, float radius, float intensity, ColorRgbI32 color, bool shadowCasting) {
  642. MUST_EXIST(world, spriteWorld_createTemporary_pointLight);
  643. world->temporaryPointLights.pushConstruct(position, radius, intensity, color, shadowCasting);
  644. }
  645. void spriteWorld_createTemporary_directedLight(SpriteWorld& world, const FVector3D direction, float intensity, ColorRgbI32 color) {
  646. MUST_EXIST(world, spriteWorld_createTemporary_pointLight);
  647. world->temporaryDirectedLights.pushConstruct(direction, intensity, color);
  648. }
  649. void spriteWorld_clearTemporary(SpriteWorld& world) {
  650. MUST_EXIST(world, spriteWorld_clearTemporary);
  651. world->temporarySprites.clear();
  652. world->temporaryPointLights.clear();
  653. world->temporaryDirectedLights.clear();
  654. }
  655. void spriteWorld_draw(SpriteWorld& world, AlignedImageRgbaU8& colorTarget) {
  656. MUST_EXIST(world, spriteWorld_draw);
  657. world->draw(colorTarget);
  658. }
  659. IVector3D spriteWorld_findGroundAtPixel(SpriteWorld& world, const AlignedImageRgbaU8& colorBuffer, const IVector2D& pixelLocation) {
  660. MUST_EXIST(world, spriteWorld_findGroundAtPixel);
  661. return world->ortho.pixelToMiniPosition(pixelLocation, world->cameraIndex, world->findWorldCenter(colorBuffer));
  662. }
  663. void spriteWorld_moveCameraInPixels(SpriteWorld& world, const IVector2D& pixelOffset) {
  664. MUST_EXIST(world, spriteWorld_moveCameraInPixels);
  665. world->cameraLocation = world->cameraLocation + world->ortho.pixelToMiniOffset(pixelOffset, world->cameraIndex);
  666. }
  667. AlignedImageRgbaU8 spriteWorld_getDiffuseBuffer(SpriteWorld& world) {
  668. MUST_EXIST(world, spriteWorld_getDiffuseBuffer);
  669. return world->diffuseBuffer;
  670. }
  671. OrderedImageRgbaU8 spriteWorld_getNormalBuffer(SpriteWorld& world) {
  672. MUST_EXIST(world, spriteWorld_getNormalBuffer);
  673. return world->normalBuffer;
  674. }
  675. OrderedImageRgbaU8 spriteWorld_getLightBuffer(SpriteWorld& world) {
  676. MUST_EXIST(world, spriteWorld_getLightBuffer);
  677. return world->lightBuffer;
  678. }
  679. AlignedImageF32 spriteWorld_getHeightBuffer(SpriteWorld& world) {
  680. MUST_EXIST(world, spriteWorld_getHeightBuffer);
  681. return world->heightBuffer;
  682. }
  683. int spriteWorld_getCameraDirectionIndex(SpriteWorld& world) {
  684. MUST_EXIST(world, spriteWorld_getCameraDirectionIndex);
  685. return world->cameraIndex;
  686. }
  687. void spriteWorld_setCameraDirectionIndex(SpriteWorld& world, int index) {
  688. MUST_EXIST(world, spriteWorld_setCameraDirectionIndex);
  689. world->cameraIndex = index;
  690. }
  691. static FVector3D normalFromPoints(const FVector3D& A, const FVector3D& B, const FVector3D& C) {
  692. return normalize(crossProduct(B - A, C - A));
  693. }
  694. static FVector3D getAverageNormal(const Model& model, int part, int poly) {
  695. int vertexCount = model_getPolygonVertexCount(model, part, poly);
  696. FVector3D normalSum;
  697. for (int t = 0; t < vertexCount - 2; t++) {
  698. normalSum = normalSum + normalFromPoints(
  699. model_getVertexPosition(model, part, poly, 0),
  700. model_getVertexPosition(model, part, poly, t + 1),
  701. model_getVertexPosition(model, part, poly, t + 2)
  702. );
  703. }
  704. return normalize(normalSum);
  705. }
  706. // Pre-conditions:
  707. // * All images must exist and have the same dimensions
  708. // * All triangles in model must be contained within the image bounds after being projected using view
  709. // TODO: Render directly with a location to a 16-bit depth buffer for background 3D models and brush preview
  710. static void sprite_render(Model model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget) {
  711. int pointCount = model_getNumberOfPoints(model);
  712. IRect clipBound = image_getBound(depthBuffer);
  713. FVector2D projectionOffset = FVector2D((float)clipBound.width() * 0.5f, (float)clipBound.height() * 0.5f);
  714. // TODO: Allow having length 0 for Arrays and Fields by preventing all access to elements in special cases
  715. Array<FVector3D> projectedPoints(pointCount, FVector3D()); // pixel X, pixel Y, mini-tile height
  716. Array<FVector3D> normalPoints(pointCount, FVector3D()); // normal X, Y, Z
  717. // TODO: Store an array of normals for each point, sum normal vectors for each included polygon and normalize the result
  718. // Interpolate and normalize again for each pixel
  719. for (int point = 0; point < pointCount; point++) {
  720. FVector3D projected = view.worldSpaceToScreenDepth.transform(model_getPoint(model, point));
  721. projectedPoints[point] = FVector3D(projected.x + projectionOffset.x, projected.y + projectionOffset.y, projected.z);
  722. }
  723. // Calculate rounded normals in light-space.
  724. // TODO: Pre-generate normals in world space before transforming into light space.
  725. FMatrix3x3 normalToWorldSpace = view.normalToWorldSpace;
  726. for (int part = 0; part < model_getNumberOfParts(model); part++) {
  727. for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
  728. // Transform the normal into a coordinate system aligned with the camera.
  729. // Otherwise the rotation cannot be used for individual rotation to have a corner for each wall.
  730. FVector3D worldNormal = getAverageNormal(model, part, poly);
  731. FVector3D localNormal = normalToWorldSpace.transformTransposed(worldNormal);
  732. for (int vert = 0; vert < model_getPolygonVertexCount(model, part, poly); vert++) {
  733. int point = model_getVertexPointIndex(model, part, poly, vert);
  734. normalPoints[point] = normalPoints[point] + localNormal;
  735. }
  736. }
  737. }
  738. for (int point = 0; point < pointCount; point++) {
  739. normalPoints[point] = normalize(normalPoints[point]);
  740. }
  741. // Render polygons as triangle fans
  742. for (int part = 0; part < model_getNumberOfParts(model); part++) {
  743. for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
  744. int vertexCount = model_getPolygonVertexCount(model, part, poly);
  745. int vertA = 0;
  746. FVector4D vertexColorA = model_getVertexColor(model, part, poly, vertA) * 255.0f;
  747. int indexA = model_getVertexPointIndex(model, part, poly, vertA);
  748. FVector3D normalA = normalPoints[indexA];
  749. FVector3D pointA = projectedPoints[indexA];
  750. LVector2D subPixelA = LVector2D(safeRoundInt64(pointA.x * constants::unitsPerPixel), safeRoundInt64(pointA.y * constants::unitsPerPixel));
  751. for (int vertB = 1; vertB < vertexCount - 1; vertB++) {
  752. int vertC = vertB + 1;
  753. int indexB = model_getVertexPointIndex(model, part, poly, vertB);
  754. int indexC = model_getVertexPointIndex(model, part, poly, vertC);
  755. FVector4D vertexColorB = model_getVertexColor(model, part, poly, vertB) * 255.0f;
  756. FVector4D vertexColorC = model_getVertexColor(model, part, poly, vertC) * 255.0f;
  757. FVector3D normalB = normalPoints[indexB];
  758. FVector3D normalC = normalPoints[indexC];
  759. FVector3D pointB = projectedPoints[indexB];
  760. FVector3D pointC = projectedPoints[indexC];
  761. LVector2D subPixelB = LVector2D(safeRoundInt64(pointB.x * constants::unitsPerPixel), safeRoundInt64(pointB.y * constants::unitsPerPixel));
  762. LVector2D subPixelC = LVector2D(safeRoundInt64(pointC.x * constants::unitsPerPixel), safeRoundInt64(pointC.y * constants::unitsPerPixel));
  763. IRect triangleBound = IRect::cut(clipBound, getTriangleBound(subPixelA, subPixelB, subPixelC));
  764. int rowCount = triangleBound.height();
  765. if (rowCount > 0) {
  766. // TODO: Fix the excess pixel bugs
  767. RowInterval rows[rowCount];
  768. rasterizeTriangle(subPixelA, subPixelB, subPixelC, rows, triangleBound);
  769. for (int y = triangleBound.top(); y < triangleBound.bottom(); y++) {
  770. int rowIndex = y - triangleBound.top();
  771. int left = rows[rowIndex].left;
  772. int right = rows[rowIndex].right;
  773. for (int x = left; x < right; x++) {
  774. FVector3D weight = getAffineWeight(FVector2D(pointA.x, pointA.y), FVector2D(pointB.x, pointB.y), FVector2D(pointC.x, pointC.y), FVector2D(x + 0.5f, y + 0.5f));
  775. float height = interpolateUsingAffineWeight(pointA.z, pointB.z, pointC.z, weight);
  776. if (height > image_readPixel_clamp(depthBuffer, x, y)) {
  777. FVector4D vertexColor = interpolateUsingAffineWeight(vertexColorA, vertexColorB, vertexColorC, weight);
  778. FVector3D normal = (normalize(interpolateUsingAffineWeight(normalA, normalB, normalC, weight)) + 1.0f) * 127.5f;
  779. image_writePixel(depthBuffer, x, y, height);
  780. image_writePixel(diffuseTarget, x, y, ColorRgbaI32(vertexColor.x, vertexColor.y, vertexColor.z, 255));
  781. image_writePixel(normalTarget, x, y, ColorRgbaI32(normal.x, normal.y, normal.z, 255));
  782. }
  783. }
  784. }
  785. }
  786. }
  787. }
  788. }
  789. }
  790. void sprite_generateFromModel(ImageRgbaU8& targetAtlas, String& targetConfigText, const Model& visibleModel, const Model& shadowModel, const OrthoSystem& ortho, const String& targetPath, int cameraAngles) {
  791. // Validate input
  792. if (cameraAngles < 1) {
  793. printText(" Need at least one camera angle to generate a sprite!\n");
  794. return;
  795. } else if (!model_exists(visibleModel)) {
  796. printText(" There's nothing to render, because visible model does not exist!\n");
  797. return;
  798. } else if (model_getNumberOfParts(visibleModel) == 0) {
  799. printText(" There's nothing to render in the visible model, because there are no parts in the visible model!\n");
  800. return;
  801. } else {
  802. // Measure the bounding cylinder for determining the uncropped image size
  803. FVector3D minBound = FVector3D(std::numeric_limits<float>::max());
  804. FVector3D maxBound = FVector3D(-std::numeric_limits<float>::max());
  805. for (int p = 0; p < model_getNumberOfPoints(visibleModel); p++) {
  806. FVector3D point = model_getPoint(visibleModel, p);
  807. if (point.x < minBound.x) { minBound.x = point.x; }
  808. if (point.y < minBound.y) { minBound.y = point.y; }
  809. if (point.z < minBound.z) { minBound.z = point.z; }
  810. if (point.x > maxBound.x) { maxBound.x = point.x; }
  811. if (point.y > maxBound.y) { maxBound.y = point.y; }
  812. if (point.z > maxBound.z) { maxBound.z = point.z; }
  813. }
  814. // Check if generating a bound failed
  815. if (minBound.x > maxBound.x) {
  816. printText(" There's nothing visible in the model, because the 3D bounding box had no points to be created from!\n");
  817. return;
  818. }
  819. printText(" Representing height from ", minBound.y, " to ", maxBound.y, " encoded using 8-bits\n");
  820. // Calculate initial image size
  821. float worstCaseDiameter = (std::max(maxBound.x, -minBound.x) + std::max(maxBound.y, -minBound.y) + std::max(maxBound.z, -minBound.z)) * 2;
  822. int maxRes = roundUp(worstCaseDiameter * ortho.pixelsPerTile, 2) + 4; // Round up to even pixels and add 4 padding pixels
  823. // Allocate square images from the pessimistic size estimation
  824. int width = maxRes;
  825. int height = maxRes;
  826. ImageF32 depthBuffer = image_create_F32(width, height);
  827. ImageRgbaU8 colorImage[cameraAngles];
  828. ImageRgbaU8 heightImage[cameraAngles];
  829. ImageRgbaU8 normalImage[cameraAngles];
  830. for (int a = 0; a < cameraAngles; a++) {
  831. colorImage[a] = image_create_RgbaU8(width, height);
  832. heightImage[a] = image_create_RgbaU8(width, height);
  833. normalImage[a] = image_create_RgbaU8(width, height);
  834. }
  835. // Render the model to multiple render targets at once
  836. float heightScale = 255.0f / (maxBound.y - minBound.y);
  837. for (int a = 0; a < cameraAngles; a++) {
  838. image_fill(depthBuffer, -1000000000.0f);
  839. image_fill(colorImage[a], ColorRgbaI32(0, 0, 0, 0));
  840. sprite_render(visibleModel, ortho.view[a], depthBuffer, colorImage[a], normalImage[a]);
  841. // Convert height into an 8 bit channel for saving
  842. for (int y = 0; y < height; y++) {
  843. for (int x = 0; x < width; x++) {
  844. int32_t opacityPixel = image_readPixel_clamp(colorImage[a], x, y).alpha;
  845. int32_t heightPixel = (image_readPixel_clamp(depthBuffer, x, y) - minBound.y) * heightScale;
  846. image_writePixel(heightImage[a], x, y, ColorRgbaI32(heightPixel, 0, 0, opacityPixel));
  847. }
  848. }
  849. }
  850. // Crop all images uniformly for easy atlas packing
  851. int32_t minX = width;
  852. int32_t minY = height;
  853. int32_t maxX = 0;
  854. int32_t maxY = 0;
  855. for (int a = 0; a < cameraAngles; a++) {
  856. for (int y = 0; y < height; y++) {
  857. for (int x = 0; x < width; x++) {
  858. if (image_readPixel_border(colorImage[a], x, y).alpha) {
  859. if (x < minX) minX = x;
  860. if (x > maxX) maxX = x;
  861. if (y < minY) minY = y;
  862. if (y > maxY) maxY = y;
  863. }
  864. }
  865. }
  866. }
  867. // Check if cropping failed
  868. if (minX > maxX) {
  869. printText(" There's nothing visible in the model, because cropping the final images returned nothing!\n");
  870. return;
  871. }
  872. IRect cropRegion = IRect(minX, minY, (maxX + 1) - minX, (maxY + 1) - minY);
  873. if (cropRegion.width() < 1 || cropRegion.height() < 1) {
  874. printText(" Cropping failed to find any drawn pixels!\n");
  875. return;
  876. }
  877. for (int a = 0; a < cameraAngles; a++) {
  878. colorImage[a] = image_getSubImage(colorImage[a], cropRegion);
  879. heightImage[a] = image_getSubImage(heightImage[a], cropRegion);
  880. normalImage[a] = image_getSubImage(normalImage[a], cropRegion);
  881. }
  882. int croppedWidth = cropRegion.width();
  883. int croppedHeight = cropRegion.height();
  884. int centerX = width / 2 - cropRegion.left();
  885. int centerY = height / 2 - cropRegion.top();
  886. printText(" Cropped images of ", croppedWidth, "x", croppedHeight, " pixels with centers at (", centerX, ", ", centerY, ")\n");
  887. // Pack everything into an image atlas
  888. targetAtlas = image_create_RgbaU8(croppedWidth * 3, croppedHeight * cameraAngles);
  889. for (int a = 0; a < cameraAngles; a++) {
  890. draw_copy(targetAtlas, colorImage[a], 0, a * croppedHeight);
  891. draw_copy(targetAtlas, heightImage[a], croppedWidth, a * croppedHeight);
  892. draw_copy(targetAtlas, normalImage[a], croppedWidth * 2, a * croppedHeight);
  893. }
  894. SpriteConfig config = SpriteConfig(centerX, centerY, cameraAngles, 3, minBound, maxBound);
  895. if (model_exists(shadowModel) && model_getNumberOfPoints(shadowModel) > 0) {
  896. config.appendShadow(shadowModel);
  897. }
  898. targetConfigText = config.toIni();
  899. }
  900. }
  901. void sprite_generateFromModel(const Model& visibleModel, const Model& shadowModel, const OrthoSystem& ortho, const String& targetPath, int cameraAngles, bool debug) {
  902. // Generate an image and a configuration file from the visible model
  903. ImageRgbaU8 atlasImage; String configText;
  904. sprite_generateFromModel(atlasImage, configText, visibleModel, shadowModel, ortho, targetPath, cameraAngles);
  905. // Save the result on success
  906. if (configText.length() > 0) {
  907. // Save the atlas
  908. String atlasPath = targetPath + U".png";
  909. // Try loading any existing image
  910. ImageRgbaU8 existingAtlasImage = image_load_RgbaU8(atlasPath, false);
  911. if (image_exists(existingAtlasImage)) {
  912. int difference = image_maxDifference(atlasImage, existingAtlasImage);
  913. if (difference <= 2) {
  914. printText(" No significant changes against ", targetPath, ".\n");
  915. } else {
  916. image_save(atlasImage, atlasPath);
  917. printText(" Updated ", targetPath, " with a deviation of ", difference, ".\n");
  918. }
  919. } else {
  920. // Only save if there was no existing image or it differed significantly from the new result
  921. // This comparison is made to avoid flooding version history with changes from invisible differences in color rounding
  922. image_save(atlasImage, atlasPath);
  923. printText(" Saved atlas to ", targetPath, ".\n");
  924. }
  925. // Save the configuration
  926. String configPath = targetPath + U".ini";
  927. String oldConfixText = string_load(configPath, false);
  928. if (string_match(configText, oldConfixText)) {
  929. printText(" No significant changes against ", targetPath, ".\n\n");
  930. } else {
  931. string_save(targetPath + U".ini", configText);
  932. printText(" Saved sprite config to ", targetPath, ".\n\n");
  933. }
  934. if (debug) {
  935. ImageRgbaU8 debugImage; String garbageText;
  936. // TODO: Show overlap between visible and shadow so that shadow outside of visible is displayed as bright red on a dark model.
  937. // The number of visible shadow pixels should be reported automatically
  938. // in an error message at the end of the total execution together with file names.
  939. sprite_generateFromModel(debugImage, garbageText, shadowModel, Model(), ortho, targetPath + U"Debug", 8);
  940. image_save(debugImage, targetPath + U"Debug.png");
  941. }
  942. }
  943. }
  944. }