sandbox.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. 
  2. /*
  3. An application for previewing tiles and sprites together for potential games.
  4. If you design game assets separatelly, they will often look much worse when you put them together.
  5. Unmatching scale, shadows, colors, themes, et cetera...
  6. That's why it's important to preview your assets together as early as possible while still designing them.
  7. */
  8. /*
  9. BUGS:
  10. * The mouse move is repeated automatically when changing pixel scale, but the same doesn't work for when the window itself moved.
  11. How can a new mouse-move event be triggered from the current location when toggling full-screen so that the window itself moves?
  12. * Tiles placed at different heights do not have synchronized rounding between each other.
  13. Try to round the Y offset separatelly from the XZ location's screen coordinate.
  14. 3D BUGS:
  15. DRAWN:
  16. * There's an ugly seam from not connecting the other side of cylinder fields.
  17. Probably haven't created any extra triangle strip on that region.
  18. SHADOWS:
  19. * The bounding box of shadows differs from the visible pixel's bound in the config file.
  20. Expand the bound using the shadow model's points to include everything safely.
  21. * When eroding the dimensions of shadow shapes, there's gaps when placing tiles next to each other
  22. Can erosion and bias be applied in each shadow map while sampling or as a separate pass?
  23. Is this much bias even needed when using bilinear interpolation in depth divided space directly from the texture?
  24. * There's no way to close the gaps on height fields without using black pixels to create zero offset at the ends.
  25. This creates open holes when not using zero clipping.
  26. An optional triangle patch can be added along the open sides. (all for planes and excluding sides for cylinders)
  27. VISUALS:
  28. * Make a directed light source that casts light and shadows from a fixed direction but can fade like a point light.
  29. Useful for street-lights and sky-lights that want to avoid normalizing and projecting light directions per pixel.
  30. Can be used both with and without casting shadows.
  31. Can use intensity maps to project patterns within the square.
  32. A rough 2D convex hull from the image can be generated for a tighter light frustum.
  33. Otherwise, one can just apply a round mask and use a cone.
  34. * Projective background decals.
  35. Used like passive lights but drawing to the diffuse layer and ignoring dynamic sprites.
  36. Will only be drawn when updating passive blocks or adding to existing background blocks.
  37. A 3D transform defines where the decal is placed like a cube in world space.
  38. The near and far clipping can use a fading threshold to allow placing explosion decals without creating hard seams.
  39. New sprites added after a decal should not be affected by an old sprite.
  40. How can this be solved without resorting to dangerous polymorphism.
  41. Allow defining decals locally for each level by loading their images from a temporary image pool of level specific content.
  42. This can be used to write instructions specific to a certain mission and give a unique look to an otherwise generic level.
  43. Billboards and signs can also be possible to reuse with custom images and text.
  44. * Static 3D models that are rendered when the background updates.
  45. These have normal resolution and can be freely rotated, scaled and colored.
  46. They draw shadows just like the pre-rendered sprites.
  47. * See if there's a shadow smoothing method worth using on the CPU.
  48. The blend filter is already quite heavy with the saturation, so it might as well do something more useful than a single multiplication as the main feature.
  49. The difficult thing is to preserve details from normal mapping and tiny details while making shadow edges look smooth.
  50. * Allow having many high-quality light sources by introducing fully passive lights.
  51. Useful for indirect light from the sky and general ambient light.
  52. The background stores RGBA light buffers to make passive lights super cheap.
  53. This light will mostly store soft light, so shadows from dynamic sprites will
  54. draw blob shadows as decals on the background before drawing themselves.
  55. This will give an illusion of dynamic ambient occlusion,
  56. especially if surface normals affect the intensity using custom shadow decals.
  57. Dynamic sprites overwrites with their own interpretation of the passive light.
  58. Dynamic lights add to the light buffer without caring about what's background and what's dynamic.
  59. A quad-tree stencil will remember which areas have foreground drawn on top of the background.
  60. This stencil is later used for a pass of dynamic light from passive light sources using stored primary cubemaps.
  61. The background will divide the light using multiple cube-maps for the same illumination by adding offset varitations in the light sampling function.
  62. * Make a reusable system for distance adaptive light sources.
  63. The same illumination filter should take multiple cubemaps rendered from slightly different locations.
  64. These can be interleaved into a unified packed look-up if the distortion
  65. of looking it up from the same offset is compensated for somehow.
  66. The first cubemap will be persistent and used later for dynamic light.
  67. The later cubemaps will be temporary when generating the background's softer light.
  68. USABILITY:
  69. * Tool for selecting and removing passive sprites.
  70. Use both unique handles for simplicity and the raw look-up for handling multiple sprites at once:
  71. Given an optional integer argument (defaulted to zero) to background sprite construction.
  72. This allow making custom filtering of sprites by category or giving a unique index to a sprite.
  73. A lookup can later return references to the sprite instances together with the key and allow custom filtering.
  74. A deletion lookup can take a function returning true when the background sprite should be deleted.
  75. The full 3D location and custom key will be returned for filtering.
  76. If the game wants to filter by direction or anything else, then encode that into the key.
  77. OPTIMIZE:
  78. * Make a tile based light culling.
  79. The background has pre-stored minimum and maximum depth for tiles of 32² pixel blocks.
  80. The screen has 64² pixel min-max blocks reading from 4-9 background blocks.
  81. Drawing active sprites will write using its own 32² max blocks to the screens depth bound.
  82. Minimum is kept because drawing can only increase and rarely covers whole areas.
  83. Each 64² block on the screen then generates a tilted cube hull of the region's visible pixels.
  84. This tells which light frustums are seen and which parts of their cube maps have to be rendered.
  85. After rendering the seen shadow-map viewports, blocks including the same set of light sources are merged horizontally.
  86. A vertical split of blocks is used for multi-threading.
  87. Example light count for square light regions (real regions will be shaped by 3D light frustums intersecting visible pixel bounds)
  88. 0--01----10-0
  89. 1--12-21-10-0
  90. 1--12-21-10-0
  91. 1-----10----0
  92. * Decrease peak time using a vertical brick pattern using a half row offset on odd background block columns.
  93. This is optimized for wide aspect ratios, which is more common than standing formats.
  94. Cutting the peak repainting area into half without increasing the minimum buffered region.
  95. Scheduling updates of nearby blocks can take one at a time when there's nothing that must be updated instantly.
  96. * Create a debug feature in spriteAPI for displaying the octree using lines.
  97. One color for the owned space and another for the sprite bounding boxes.
  98. Pressing a certain button in Sandbox should toggle the debug drawing to allow asserting that the tree is well balanced for the level's size.
  99. LATER:
  100. * Make a ground layer using height and blend maps for outdoor scenes.
  101. Each tile region will decide if ground should be drawn there.
  102. Disabling the ground on a tile will look at the main tile replacing the ground for walking heights.
  103. Grass and small stones will use a separate system, because background sprites do not adapt to the ground height.
  104. These can be generated from deterministic random values compared against blend maps to save space.
  105. Additional natural sprites can be added one by one at specific locations.
  106. * When loading the frames from an atlas, crop the images further and apply separate offsets per frame.
  107. This will significantly improve rendering speed for 8 direction sprites.
  108. */
  109. #include "../../DFPSR/includeFramework.h"
  110. #include "../SpriteEngine/spriteAPI.h"
  111. #include "../SpriteEngine/importer.h"
  112. #include <assert.h>
  113. #include <limits>
  114. using namespace dsr;
  115. static const String mediaPath = string_combine(U"media", file_separator());
  116. static const String imagePath = string_combine(mediaPath, U"images", file_separator());
  117. static const String modelPath = string_combine(mediaPath, U"models", file_separator());
  118. // Variables
  119. static bool running = true;
  120. static bool updateImage = true;
  121. static IVector2D mousePos;
  122. static bool panorate = false;
  123. static bool tileAlign = false;
  124. static int debugView = 0;
  125. static int mouseLights = 1;
  126. static int random(const int minimum, const int maximum) {
  127. if (maximum > minimum) {
  128. return (std::rand() % (maximum + 1 - minimum)) + minimum;
  129. } else {
  130. return minimum;
  131. }
  132. }
  133. // Variables
  134. static int brushHeight = 0; // In mini-tile units
  135. static SpriteInstance spriteBrush(0, dir0, IVector3D(), true);
  136. static bool placingModel = false; // True when left mouse button is pressed and the direction is being assigned
  137. static ModelInstance modelBrush(0, Transform3D());
  138. static const int brushStep = ortho_miniUnitsPerTile / 32;
  139. static int pressing_left = 0, pressing_right = 0, pressing_up = 0, pressing_down = 0, pressing_delete = 0;
  140. static IVector2D cameraMovement;
  141. static const int cameraSpeed = 1;
  142. // World
  143. static SpriteWorld world;
  144. bool ambientLight = true;
  145. bool castShadows = true;
  146. // GUI
  147. static Window window;
  148. Component mainPanel, toolPanel, spritePanel, spriteList, modelPanel, modelList;
  149. static int overlayMode = 2;
  150. static const int OverlayMode_None = 0;
  151. static const int OverlayMode_Profiling = 1;
  152. static const int OverlayMode_Tools = 2;
  153. static const int OverlayModeCount = 3;
  154. static int tool = 0;
  155. static const int Tool_PlaceSprite = 0;
  156. static const int Tool_PlaceModel = 1;
  157. static const int ToolCount = 2;
  158. void updateOverlay() {
  159. component_setProperty_integer(toolPanel, U"Visible", overlayMode == OverlayMode_Tools);
  160. component_setProperty_integer(spritePanel, U"Visible", tool == Tool_PlaceSprite);
  161. component_setProperty_integer(modelPanel, U"Visible", tool == Tool_PlaceModel);
  162. }
  163. void loadSprite(const ReadableString& name) {
  164. spriteWorld_loadSpriteTypeFromFile(imagePath, name);
  165. component_call(spriteList, U"PushElement", name);
  166. component_setProperty_integer(spriteList, U"SelectedIndex", 0);
  167. }
  168. void loadModel(const ReadableString& name, const ReadableString& visibleName, const ReadableString& shadowName) {
  169. spriteWorld_loadModelTypeFromFile(modelPath, visibleName, shadowName);
  170. component_call(modelList, U"PushElement", name);
  171. component_setProperty_integer(modelList, U"SelectedIndex", 0);
  172. }
  173. void sandbox_main() {
  174. // Create the world
  175. world = spriteWorld_create(OrthoSystem(string_load(string_combine(mediaPath, U"Ortho.ini"))), 256);
  176. // Create a window
  177. String title = U"David Piuva's Software Renderer - Graphics sandbox";
  178. window = window_create(title, 1600, 900);
  179. //window = window_create_fullscreen(title);
  180. // Load an interface to the window
  181. window_loadInterfaceFromFile(window, mediaPath + U"interface.lof");
  182. // Tell the application to terminate when the window is closed
  183. window_setCloseEvent(window, []() {
  184. running = false;
  185. });
  186. // Get direct window events
  187. window_setMouseEvent(window, [](const MouseEvent& event) {
  188. if (event.mouseEventType == MouseEventType::MouseMove) {
  189. if (panorate) {
  190. // Move the camera in exact pixels
  191. spriteWorld_moveCameraInPixels(world, mousePos - event.position);
  192. }
  193. mousePos = event.position;
  194. }
  195. });
  196. window_setKeyboardEvent(window, [](const KeyboardEvent& event) {
  197. DsrKey key = event.dsrKey;
  198. if (event.keyboardEventType == KeyboardEventType::KeyDown) {
  199. if (key == DsrKey_V) {
  200. debugView = 0;
  201. } else if (key == DsrKey_B) {
  202. debugView = 1;
  203. } else if (key == DsrKey_N) {
  204. debugView = 2;
  205. } else if (key == DsrKey_M) {
  206. debugView = 3;
  207. } else if (key == DsrKey_L) {
  208. debugView = 4;
  209. } else if (key >= DsrKey_1 && key <= DsrKey_9) {
  210. window_setPixelScale(window, key - DsrKey_0);
  211. } else if (key == DsrKey_R) {
  212. ambientLight = !ambientLight;
  213. } else if (key == DsrKey_T) {
  214. tileAlign = !tileAlign;
  215. } else if (key == DsrKey_Y) {
  216. castShadows = !castShadows;
  217. } else if (key == DsrKey_F) {
  218. overlayMode = (overlayMode + 1) % OverlayModeCount;
  219. updateOverlay();
  220. } else if (key == DsrKey_K) {
  221. mouseLights = (mouseLights + 1) % 5;
  222. } else if (key == DsrKey_C) {
  223. // Rotate the world clockwise using four camera angles
  224. spriteWorld_setCameraDirectionIndex(world, (spriteWorld_getCameraDirectionIndex(world) + 1) % 4);
  225. } else if (key == DsrKey_Z) {
  226. // Rotate the world counter-clockwise using four camera angles
  227. spriteWorld_setCameraDirectionIndex(world, (spriteWorld_getCameraDirectionIndex(world) + 3) % 4);
  228. } else if (key == DsrKey_F11) {
  229. // Toggle full-screen
  230. window_setFullScreen(window, !window_isFullScreen(window));
  231. } else if (key == DsrKey_Escape) {
  232. // Terminate safely after the next frame
  233. running = false;
  234. } else if (key == DsrKey_A) {
  235. pressing_left = 1;
  236. } else if (key == DsrKey_D) {
  237. pressing_right = 1;
  238. } else if (key == DsrKey_W) {
  239. pressing_up = 1;
  240. } else if (key == DsrKey_S) {
  241. pressing_down = 1;
  242. } else if (key == DsrKey_Delete) {
  243. pressing_delete = 1;
  244. } else if (key == DsrKey_LeftArrow) {
  245. spriteBrush.direction = correctDirection(spriteBrush.direction + dir270);
  246. } else if (key == DsrKey_RightArrow) {
  247. spriteBrush.direction = correctDirection(spriteBrush.direction + dir90);
  248. }
  249. } else if (event.keyboardEventType == KeyboardEventType::KeyUp) {
  250. if (key == DsrKey_A) {
  251. pressing_left = 0;
  252. } else if (key == DsrKey_D) {
  253. pressing_right = 0;
  254. } else if (key == DsrKey_W) {
  255. pressing_up = 0;
  256. } else if (key == DsrKey_S) {
  257. pressing_down = 0;
  258. } else if (key == DsrKey_Delete) {
  259. pressing_delete = 0;
  260. }
  261. }
  262. cameraMovement.x = pressing_right - pressing_left;
  263. cameraMovement.y = pressing_down - pressing_up;
  264. });
  265. // Get component handles and assign actions
  266. mainPanel = window_getRoot(window);
  267. component_setMouseDownEvent(mainPanel, [](const MouseEvent& event) {
  268. if (event.key == MouseKeyEnum::Left) {
  269. if (overlayMode == OverlayMode_Tools) {
  270. // Place a passive visual instance using the brush
  271. if (tool == Tool_PlaceSprite) {
  272. spriteWorld_addBackgroundSprite(world, spriteBrush);
  273. } else if (tool == Tool_PlaceModel) {
  274. placingModel = true;
  275. }
  276. }
  277. } else if (event.key == MouseKeyEnum::Right) {
  278. panorate = true;
  279. }
  280. });
  281. component_setMouseUpEvent(mainPanel, [](const MouseEvent& event) {
  282. if (event.key == MouseKeyEnum::Left) {
  283. if (overlayMode == OverlayMode_Tools) {
  284. // Place a passive visual instance using the brush
  285. if (tool == Tool_PlaceModel && placingModel) {
  286. spriteWorld_addBackgroundModel(world, modelBrush);
  287. placingModel = false;
  288. }
  289. }
  290. } else if (event.key == MouseKeyEnum::Right) {
  291. panorate = false;
  292. }
  293. });
  294. component_setMouseScrollEvent(mainPanel, [](const MouseEvent& event) {
  295. if (event.key == MouseKeyEnum::ScrollUp) {
  296. brushHeight += brushStep;
  297. } else if (event.key == MouseKeyEnum::ScrollDown) {
  298. brushHeight -= brushStep;
  299. }
  300. });
  301. toolPanel = window_findComponentByName(window, U"toolPanel");
  302. spritePanel = window_findComponentByName(window, U"spritePanel");
  303. modelPanel = window_findComponentByName(window, U"modelPanel");
  304. component_setPressedEvent(window_findComponentByName(window, U"spriteButton"), []() {
  305. tool = Tool_PlaceSprite;
  306. placingModel = false;
  307. updateOverlay();
  308. });
  309. component_setPressedEvent(window_findComponentByName(window, U"modelButton"), []() {
  310. tool = Tool_PlaceModel;
  311. placingModel = false;
  312. updateOverlay();
  313. });
  314. spriteList = window_findComponentByName(window, U"spriteList");
  315. component_setSelectEvent(spriteList, [](int64_t index) {
  316. spriteBrush.typeIndex = index;
  317. });
  318. modelList = window_findComponentByName(window, U"modelList");
  319. component_setSelectEvent(modelList, [](int64_t index) {
  320. modelBrush.typeIndex = index;
  321. });
  322. component_setPressedEvent(window_findComponentByName(window, U"leftButton"), []() {
  323. spriteBrush.direction = correctDirection(spriteBrush.direction + dir270);
  324. });
  325. component_setPressedEvent(window_findComponentByName(window, U"rightButton"), []() {
  326. spriteBrush.direction = correctDirection(spriteBrush.direction + dir90);
  327. });
  328. updateOverlay();
  329. // Create sprite types while listing their presence in the tool menu
  330. loadSprite(U"Floor");
  331. loadSprite(U"WoodenFloor");
  332. loadSprite(U"WoodenFence");
  333. loadSprite(U"WoodenBarrel");
  334. loadSprite(U"Pillar");
  335. loadSprite(U"Character_Mage");
  336. // Load models
  337. loadModel(U"Barrel", U"Barrel_LowDetail.ply", U"Barrel_Shadow.ply");
  338. loadModel(U"Mage", U"Character_Mage.ply", U"Character_Mage_Shadow.ply");
  339. // Create passive sprites
  340. for (int z = -300; z < 300; z++) {
  341. for (int x = -300; x < 300; x++) {
  342. // The bottom floor does not have to throw shadows
  343. spriteWorld_addBackgroundSprite(world, SpriteInstance(random(0, 1), random(0, 3) * dir90, IVector3D(x * ortho_miniUnitsPerTile, 0, z * ortho_miniUnitsPerTile), false));
  344. }
  345. }
  346. for (int z = -300; z < 300; z++) {
  347. for (int x = -300; x < 300; x++) {
  348. if (random(1, 4) == 1) {
  349. // Obstacles should cast shadows when possible
  350. spriteWorld_addBackgroundSprite(world, SpriteInstance(random(2, 4), random(0, 3) * dir90, IVector3D(x * ortho_miniUnitsPerTile, 0, z * ortho_miniUnitsPerTile), true));
  351. } else if (random(1, 20) == 1) {
  352. // Characters are just static geometry for testing
  353. spriteWorld_addBackgroundSprite(world, SpriteInstance(5, random(0, 7) * dir45, IVector3D(x * ortho_miniUnitsPerTile, 0, z * ortho_miniUnitsPerTile), true));
  354. }
  355. }
  356. }
  357. // Animation timing
  358. double frameStartTime = time_getSeconds();
  359. double secondsPerFrame = 0.0;
  360. double stepRemainder = 0.0;
  361. // Profiling
  362. double profileStartTime = time_getSeconds();
  363. int64_t profileFrameCount = 0; // Frames per second
  364. float profileFrameRate = 0.0f;
  365. double maxFrameTime = 0.0, lastMaxFrameTime = 0.0; // Peak per second
  366. while(running) {
  367. // Always render the image when profiling or moving the camera
  368. updateImage = overlayMode != OverlayMode_Tools || cameraMovement.x != 0 || cameraMovement.y != 0;
  369. // Execute actions
  370. if (window_executeEvents(window)) {
  371. // If editing, only update the image when the user did something
  372. updateImage = true;
  373. }
  374. if (updateImage) {
  375. // Request buffers after executing the events, to get newly allocated buffers after resize events
  376. AlignedImageRgbaU8 colorBuffer = window_getCanvas(window);
  377. // Calculate a number of whole millisecond ticks per frame
  378. // By performing game logic in multiples of msTicks, integer operations
  379. // can be scaled without comming to a full stop in high frame rates
  380. stepRemainder += secondsPerFrame * 1000.0;
  381. int msTicks = (int)stepRemainder;
  382. stepRemainder -= (double)msTicks;
  383. // Move the camera
  384. int cameraSteps = cameraSpeed * msTicks;
  385. // TODO: Find a way to move the camera using exact pixel offsets so that the camera's 3D location is only generating the 2D offset when rotating.
  386. // Can the sprite brush be guaranteed to come back to the mouse location after adding and subtracting the same 2D camera offset?
  387. // A new integer coordinate system along the ground might move half a pixel vertically and a full pixel sideways in the diagonal view.
  388. // Otherwise the approximation defeats the whole purpose of using whole integers in msTicks.
  389. spriteWorld_moveCameraInPixels(world, cameraMovement * cameraSteps);
  390. // Remove temporary visuals
  391. spriteWorld_clearTemporary(world);
  392. // Place the brush
  393. IVector3D mouseMiniPos = spriteWorld_findGroundAtPixel(world, colorBuffer, mousePos);
  394. FVector3D worldBrushPos = FVector3D(
  395. mouseMiniPos.x * ortho_tilesPerMiniUnit,
  396. brushHeight * ortho_tilesPerMiniUnit,
  397. mouseMiniPos.z * ortho_tilesPerMiniUnit
  398. );
  399. if (placingModel) {
  400. // Drag with the left mouse button around the selected location to select the angle
  401. // Scroll to another height to direct it towards another height
  402. modelBrush.location.transform = FMatrix3x3::makeAxisSystem(modelBrush.location.position - worldBrushPos, FVector3D(0.0f, 1.0f, 0.0f)); // TODO: An integer based rotation system for the brush
  403. } else {
  404. modelBrush.location = Transform3D(
  405. worldBrushPos,
  406. FMatrix3x3::makeAxisSystem(FVector3D(1.0f, 0.0f, 0.0f), FVector3D(0.0f, 1.0f, 0.0f)) // TODO: An integer based rotation system for the brush
  407. );
  408. }
  409. spriteBrush.location = IVector3D(mouseMiniPos.x, brushHeight, mouseMiniPos.z);
  410. if (tileAlign) {
  411. spriteBrush.location = ortho_roundToTile(spriteBrush.location);
  412. }
  413. // Repeated tools
  414. if (pressing_delete) {
  415. IVector3D searchMinBound = IVector3D(mouseMiniPos.x - ortho_miniUnitsPerTile / 2,-1000000, mouseMiniPos.z - ortho_miniUnitsPerTile / 2);
  416. IVector3D searchMaxBound = IVector3D(mouseMiniPos.x + ortho_miniUnitsPerTile / 2, 1000000, mouseMiniPos.z + ortho_miniUnitsPerTile / 2);
  417. spriteWorld_removeBackgroundSprites(world, searchMinBound, searchMaxBound);
  418. spriteWorld_removeBackgroundModels(world, searchMinBound, searchMaxBound);
  419. }
  420. // Illuminate the world using soft light from the sky
  421. if (ambientLight) {
  422. spriteWorld_createTemporary_directedLight(world, FVector3D(1.0f, -1.0f, 0.0f), 0.1f, ColorRgbI32(255, 255, 255));
  423. }
  424. // Create a temporary point light over the brush
  425. // Temporary light sources are easier to use for dynamic light because they don't need any handle
  426. if (mouseLights == 1) {
  427. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(0.0f, 0.5f, 0.0f), 4.0f, 4.0f, ColorRgbI32(128, 255, 128), castShadows);
  428. } else if (mouseLights == 2) {
  429. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(-2.0f, 0.5f, 1.0f), 4.0f, 2.0f, ColorRgbI32(255, 128, 128), castShadows);
  430. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(2.0f, 0.52f, -1.0f), 4.0f, 2.0f, ColorRgbI32(128, 255, 128), castShadows);
  431. } else if (mouseLights == 3) {
  432. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(-2.0f, 0.5f, 1.0f), 4.0f, 1.333f, ColorRgbI32(255, 128, 128), castShadows);
  433. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(1.0f, 0.51f, 2.0f), 4.0f, 1.333f, ColorRgbI32(128, 255, 128), castShadows);
  434. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(2.0f, 0.52f, -1.0f), 4.0f, 1.333f, ColorRgbI32(128, 128, 255), castShadows);
  435. } else if (mouseLights == 4) {
  436. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(-2.0f, 0.5f, 1.0f), 4.0f, 1.0f, ColorRgbI32(255, 128, 128), castShadows);
  437. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(1.0f, 0.51f, 2.0f), 4.0f, 1.0f, ColorRgbI32(128, 255, 128), castShadows);
  438. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(2.0f, 0.52f, -1.0f), 4.0f, 1.0f, ColorRgbI32(128, 128, 255), castShadows);
  439. spriteWorld_createTemporary_pointLight(world, ortho_miniToFloatingTile(spriteBrush.location) + FVector3D(-1.0f, 0.53f, -2.0f), 4.0f, 1.0f, ColorRgbI32(255, 255, 128), castShadows);
  440. }
  441. // Show the sprite brush
  442. if (overlayMode == OverlayMode_Tools) {
  443. if (tool == Tool_PlaceSprite && spriteWorld_getSpriteTypeCount() > 0) {
  444. spriteWorld_addTemporarySprite(world, spriteBrush);
  445. } else if (tool == Tool_PlaceModel && spriteWorld_getModelTypeCount() > 0) {
  446. spriteWorld_addTemporaryModel(world, modelBrush);
  447. }
  448. }
  449. // Draw the world
  450. spriteWorld_draw(world, colorBuffer);
  451. // Debug views (Slow but failsafe)
  452. if (debugView == 1) {
  453. draw_copy(colorBuffer, spriteWorld_getDiffuseBuffer(world));
  454. } else if (debugView == 2) {
  455. draw_copy(colorBuffer, spriteWorld_getNormalBuffer(world));
  456. } else if (debugView == 3) {
  457. AlignedImageF32 heightBuffer = spriteWorld_getHeightBuffer(world);
  458. for (int y = 0; y < image_getHeight(colorBuffer); y++) {
  459. for (int x = 0; x < image_getWidth(colorBuffer); x++) {
  460. float height = image_readPixel_clamp(heightBuffer, x, y) * 255.0f;
  461. if (height < 0.0f) { height = 0.0f; }
  462. if (height > 255.0f) { height = 255.0f; }
  463. image_writePixel(colorBuffer, x, y, ColorRgbaI32(height, 0, 0, 255));
  464. }
  465. }
  466. } else if (debugView == 4) {
  467. draw_copy(colorBuffer, spriteWorld_getLightBuffer(world));
  468. }
  469. // Overlays
  470. window_drawComponents(window);
  471. // Profiling mode
  472. if (overlayMode == OverlayMode_Profiling) {
  473. IVector2D writer = IVector2D(10, 10);
  474. font_printLine(colorBuffer, font_getDefault(), string_combine(U"FPS: ", profileFrameRate), writer, ColorRgbaI32(255, 255, 255, 255)); writer.y += 20;
  475. font_printLine(colorBuffer, font_getDefault(), string_combine(U"avg ms: ", 1000.0f / profileFrameRate), writer, ColorRgbaI32(255, 255, 255, 255)); writer.y += 20;
  476. font_printLine(colorBuffer, font_getDefault(), string_combine(U"max ms: ", 1000.0f * lastMaxFrameTime), writer, ColorRgbaI32(255, 255, 255, 255)); writer.y += 20;
  477. }
  478. window_showCanvas(window);
  479. } else {
  480. // If updateImage is false then just delay a bit while waiting for input
  481. time_sleepSeconds(0.01);
  482. }
  483. double newTime = time_getSeconds();
  484. secondsPerFrame = newTime - frameStartTime;
  485. frameStartTime = newTime;
  486. // Profiling
  487. if (secondsPerFrame > maxFrameTime) { maxFrameTime = secondsPerFrame; }
  488. profileFrameCount++;
  489. if (newTime > profileStartTime + 1.0) {
  490. double duration = newTime - profileStartTime;
  491. profileFrameRate = (double)profileFrameCount / duration;
  492. profileStartTime = newTime;
  493. profileFrameCount = 0;
  494. lastMaxFrameTime = maxFrameTime;
  495. maxFrameTime = 0.0;
  496. }
  497. }
  498. }