You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

354 line
13 KiB

  1. /*******************************************************************************************
  2. *
  3. * raylib [shapes] example - top down lights
  4. *
  5. * Example originally created with raylib 4.2, last time updated with raylib 4.2
  6. *
  7. * Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5)
  8. *
  9. * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
  10. * BSD-like license that allows static linking with closed source software
  11. *
  12. * Copyright (c) 2022-2023 Jeffery Myers (@JeffM2501)
  13. *
  14. ********************************************************************************************/
  15. #include "raylib.h"
  16. #include "raymath.h"
  17. #include "rlgl.h"
  18. // Custom Blend Modes
  19. #define RLGL_SRC_ALPHA 0x0302
  20. #define RLGL_MIN 0x8007
  21. #define RLGL_MAX 0x8008
  22. #define MAX_BOXES 20
  23. #define MAX_SHADOWS MAX_BOXES*3 // MAX_BOXES *3. Each box can cast up to two shadow volumes for the edges it is away from, and one for the box itself
  24. #define MAX_LIGHTS 16
  25. // Shadow geometry type
  26. typedef struct ShadowGeometry {
  27. Vector2 vertices[4];
  28. } ShadowGeometry;
  29. // Light info type
  30. typedef struct LightInfo {
  31. bool active; // Is this light slot active?
  32. bool dirty; // Does this light need to be updated?
  33. bool valid; // Is this light in a valid position?
  34. Vector2 position; // Light position
  35. RenderTexture mask; // Alpha mask for the light
  36. float outerRadius; // The distance the light touches
  37. Rectangle bounds; // A cached rectangle of the light bounds to help with culling
  38. ShadowGeometry shadows[MAX_SHADOWS];
  39. int shadowCount;
  40. } LightInfo;
  41. LightInfo lights[MAX_LIGHTS] = { 0 };
  42. // Move a light and mark it as dirty so that we update it's mask next frame
  43. void MoveLight(int slot, float x, float y)
  44. {
  45. lights[slot].dirty = true;
  46. lights[slot].position.x = x;
  47. lights[slot].position.y = y;
  48. // update the cached bounds
  49. lights[slot].bounds.x = x - lights[slot].outerRadius;
  50. lights[slot].bounds.y = y - lights[slot].outerRadius;
  51. }
  52. // Compute a shadow volume for the edge
  53. // It takes the edge and projects it back by the light radius and turns it into a quad
  54. void ComputeShadowVolumeForEdge(int slot, Vector2 sp, Vector2 ep)
  55. {
  56. if (lights[slot].shadowCount >= MAX_SHADOWS) return;
  57. float extension = lights[slot].outerRadius*2;
  58. Vector2 spVector = Vector2Normalize(Vector2Subtract(sp, lights[slot].position));
  59. Vector2 spProjection = Vector2Add(sp, Vector2Scale(spVector, extension));
  60. Vector2 epVector = Vector2Normalize(Vector2Subtract(ep, lights[slot].position));
  61. Vector2 epProjection = Vector2Add(ep, Vector2Scale(epVector, extension));
  62. lights[slot].shadows[lights[slot].shadowCount].vertices[0] = sp;
  63. lights[slot].shadows[lights[slot].shadowCount].vertices[1] = ep;
  64. lights[slot].shadows[lights[slot].shadowCount].vertices[2] = epProjection;
  65. lights[slot].shadows[lights[slot].shadowCount].vertices[3] = spProjection;
  66. lights[slot].shadowCount++;
  67. }
  68. // Draw the light and shadows to the mask for a light
  69. void DrawLightMask(int slot)
  70. {
  71. // Use the light mask
  72. BeginTextureMode(lights[slot].mask);
  73. ClearBackground(WHITE);
  74. // Force the blend mode to only set the alpha of the destination
  75. rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN);
  76. rlSetBlendMode(BLEND_CUSTOM);
  77. // If we are valid, then draw the light radius to the alpha mask
  78. if (lights[slot].valid) DrawCircleGradient((int)lights[slot].position.x, (int)lights[slot].position.y, lights[slot].outerRadius, ColorAlpha(WHITE, 0), WHITE);
  79. rlDrawRenderBatchActive();
  80. // Cut out the shadows from the light radius by forcing the alpha to maximum
  81. rlSetBlendMode(BLEND_ALPHA);
  82. rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MAX);
  83. rlSetBlendMode(BLEND_CUSTOM);
  84. // Draw the shadows to the alpha mask
  85. for (int i = 0; i < lights[slot].shadowCount; i++)
  86. {
  87. DrawTriangleFan(lights[slot].shadows[i].vertices, 4, WHITE);
  88. }
  89. rlDrawRenderBatchActive();
  90. // Go back to normal blend mode
  91. rlSetBlendMode(BLEND_ALPHA);
  92. EndTextureMode();
  93. }
  94. // Setup a light
  95. void SetupLight(int slot, float x, float y, float radius)
  96. {
  97. lights[slot].active = true;
  98. lights[slot].valid = false; // The light must prove it is valid
  99. lights[slot].mask = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
  100. lights[slot].outerRadius = radius;
  101. lights[slot].bounds.width = radius * 2;
  102. lights[slot].bounds.height = radius * 2;
  103. MoveLight(slot, x, y);
  104. // Force the render texture to have something in it
  105. DrawLightMask(slot);
  106. }
  107. // See if a light needs to update it's mask
  108. bool UpdateLight(int slot, Rectangle* boxes, int count)
  109. {
  110. if (!lights[slot].active || !lights[slot].dirty) return false;
  111. lights[slot].dirty = false;
  112. lights[slot].shadowCount = 0;
  113. lights[slot].valid = false;
  114. for (int i = 0; i < count; i++)
  115. {
  116. // Are we in a box? if so we are not valid
  117. if (CheckCollisionPointRec(lights[slot].position, boxes[i])) return false;
  118. // If this box is outside our bounds, we can skip it
  119. if (!CheckCollisionRecs(lights[slot].bounds, boxes[i])) continue;
  120. // Check the edges that are on the same side we are, and cast shadow volumes out from them
  121. // Top
  122. Vector2 sp = (Vector2){ boxes[i].x, boxes[i].y };
  123. Vector2 ep = (Vector2){ boxes[i].x + boxes[i].width, boxes[i].y };
  124. if (lights[slot].position.y > ep.y) ComputeShadowVolumeForEdge(slot, sp, ep);
  125. // Right
  126. sp = ep;
  127. ep.y += boxes[i].height;
  128. if (lights[slot].position.x < ep.x) ComputeShadowVolumeForEdge(slot, sp, ep);
  129. // Bottom
  130. sp = ep;
  131. ep.x -= boxes[i].width;
  132. if (lights[slot].position.y < ep.y) ComputeShadowVolumeForEdge(slot, sp, ep);
  133. // Left
  134. sp = ep;
  135. ep.y -= boxes[i].height;
  136. if (lights[slot].position.x > ep.x) ComputeShadowVolumeForEdge(slot, sp, ep);
  137. // The box itself
  138. lights[slot].shadows[lights[slot].shadowCount].vertices[0] = (Vector2){ boxes[i].x, boxes[i].y };
  139. lights[slot].shadows[lights[slot].shadowCount].vertices[1] = (Vector2){ boxes[i].x, boxes[i].y + boxes[i].height };
  140. lights[slot].shadows[lights[slot].shadowCount].vertices[2] = (Vector2){ boxes[i].x + boxes[i].width, boxes[i].y + boxes[i].height };
  141. lights[slot].shadows[lights[slot].shadowCount].vertices[3] = (Vector2){ boxes[i].x + boxes[i].width, boxes[i].y };
  142. lights[slot].shadowCount++;
  143. }
  144. lights[slot].valid = true;
  145. DrawLightMask(slot);
  146. return true;
  147. }
  148. // Set up some boxes
  149. void SetupBoxes(Rectangle *boxes, int *count)
  150. {
  151. boxes[0] = (Rectangle){ 150,80, 40, 40 };
  152. boxes[1] = (Rectangle){ 1200, 700, 40, 40 };
  153. boxes[2] = (Rectangle){ 200, 600, 40, 40 };
  154. boxes[3] = (Rectangle){ 1000, 50, 40, 40 };
  155. boxes[4] = (Rectangle){ 500, 350, 40, 40 };
  156. for (int i = 5; i < MAX_BOXES; i++)
  157. {
  158. boxes[i] = (Rectangle){(float)GetRandomValue(0,GetScreenWidth()), (float)GetRandomValue(0,GetScreenHeight()), (float)GetRandomValue(10,100), (float)GetRandomValue(10,100) };
  159. }
  160. *count = MAX_BOXES;
  161. }
  162. //------------------------------------------------------------------------------------
  163. // Program main entry point
  164. //------------------------------------------------------------------------------------
  165. int main(void)
  166. {
  167. // Initialization
  168. //--------------------------------------------------------------------------------------
  169. const int screenWidth = 800;
  170. const int screenHeight = 450;
  171. InitWindow(screenWidth, screenHeight, "raylib [shapes] example - top down lights");
  172. // Initialize our 'world' of boxes
  173. int boxCount = 0;
  174. Rectangle boxes[MAX_BOXES] = { 0 };
  175. SetupBoxes(boxes, &boxCount);
  176. // Create a checkerboard ground texture
  177. Image img = GenImageChecked(64, 64, 32, 32, DARKBROWN, DARKGRAY);
  178. Texture2D backgroundTexture = LoadTextureFromImage(img);
  179. UnloadImage(img);
  180. // Create a global light mask to hold all the blended lights
  181. RenderTexture lightMask = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
  182. // Setup initial light
  183. SetupLight(0, 600, 400, 300);
  184. int nextLight = 1;
  185. bool showLines = false;
  186. SetTargetFPS(60); // Set our game to run at 60 frames-per-second
  187. //--------------------------------------------------------------------------------------
  188. // Main game loop
  189. while (!WindowShouldClose()) // Detect window close button or ESC key
  190. {
  191. // Update
  192. //----------------------------------------------------------------------------------
  193. // Drag light 0
  194. if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) MoveLight(0, GetMousePosition().x, GetMousePosition().y);
  195. // Make a new light
  196. if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && (nextLight < MAX_LIGHTS))
  197. {
  198. SetupLight(nextLight, GetMousePosition().x, GetMousePosition().y, 200);
  199. nextLight++;
  200. }
  201. // Toggle debug info
  202. if (IsKeyPressed(KEY_F1)) showLines = !showLines;
  203. // Update the lights and keep track if any were dirty so we know if we need to update the master light mask
  204. bool dirtyLights = false;
  205. for (int i = 0; i < MAX_LIGHTS; i++)
  206. {
  207. if (UpdateLight(i, boxes, boxCount)) dirtyLights = true;
  208. }
  209. // Update the light mask
  210. if (dirtyLights)
  211. {
  212. // Build up the light mask
  213. BeginTextureMode(lightMask);
  214. ClearBackground(BLACK);
  215. // Force the blend mode to only set the alpha of the destination
  216. rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN);
  217. rlSetBlendMode(BLEND_CUSTOM);
  218. // Merge in all the light masks
  219. for (int i = 0; i < MAX_LIGHTS; i++)
  220. {
  221. if (lights[i].active) DrawTextureRec(lights[i].mask.texture, (Rectangle){ 0, 0, (float)GetScreenWidth(), -(float)GetScreenHeight() }, Vector2Zero(), WHITE);
  222. }
  223. rlDrawRenderBatchActive();
  224. // Go back to normal blend
  225. rlSetBlendMode(BLEND_ALPHA);
  226. EndTextureMode();
  227. }
  228. //----------------------------------------------------------------------------------
  229. // Draw
  230. //----------------------------------------------------------------------------------
  231. BeginDrawing();
  232. ClearBackground(BLACK);
  233. // Draw the tile background
  234. DrawTextureRec(backgroundTexture, (Rectangle){ 0, 0, (float)GetScreenWidth(), (float)GetScreenHeight() }, Vector2Zero(), WHITE);
  235. // Overlay the shadows from all the lights
  236. DrawTextureRec(lightMask.texture, (Rectangle){ 0, 0, (float)GetScreenWidth(), -(float)GetScreenHeight() }, Vector2Zero(), ColorAlpha(WHITE, showLines? 0.75f : 1.0f));
  237. // Draw the lights
  238. for (int i = 0; i < MAX_LIGHTS; i++)
  239. {
  240. if (lights[i].active) DrawCircle((int)lights[i].position.x, (int)lights[i].position.y, 10, (i == 0)? YELLOW : WHITE);
  241. }
  242. if (showLines)
  243. {
  244. for (int s = 0; s < lights[0].shadowCount; s++)
  245. {
  246. DrawTriangleFan(lights[0].shadows[s].vertices, 4, DARKPURPLE);
  247. }
  248. for (int b = 0; b < boxCount; b++)
  249. {
  250. if (CheckCollisionRecs(boxes[b],lights[0].bounds)) DrawRectangleRec(boxes[b], PURPLE);
  251. DrawRectangleLines((int)boxes[b].x, (int)boxes[b].y, (int)boxes[b].width, (int)boxes[b].height, DARKBLUE);
  252. }
  253. DrawText("(F1) Hide Shadow Volumes", 10, 50, 10, GREEN);
  254. }
  255. else
  256. {
  257. DrawText("(F1) Show Shadow Volumes", 10, 50, 10, GREEN);
  258. }
  259. DrawFPS(screenWidth - 80, 10);
  260. DrawText("Drag to move light #1", 10, 10, 10, DARKGREEN);
  261. DrawText("Right click to add new light", 10, 30, 10, DARKGREEN);
  262. EndDrawing();
  263. //----------------------------------------------------------------------------------
  264. }
  265. // De-Initialization
  266. //--------------------------------------------------------------------------------------
  267. UnloadTexture(backgroundTexture);
  268. UnloadRenderTexture(lightMask);
  269. for (int i = 0; i < MAX_LIGHTS; i++)
  270. {
  271. if (lights[i].active) UnloadRenderTexture(lights[i].mask);
  272. }
  273. CloseWindow(); // Close window and OpenGL context
  274. //--------------------------------------------------------------------------------------
  275. return 0;
  276. }