spriteAPI.cpp 75 KB

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