diff --git a/build.zig b/build.zig index 3327b80..57e22d8 100644 --- a/build.zig +++ b/build.zig @@ -269,6 +269,11 @@ pub fn build(b: *std.Build) !void { .path = "examples/shapes/splines_drawing.zig", .desc = "Renders a spline", }, + .{ + .name = "top_down_lights", + .path = "examples/shapes/top_down_lights.zig", + .desc = "Renders a sceen with shadows and a top down persepective", + }, .{ .name = "sprite_anim", .path = "examples/textures/sprite_anim.zig", diff --git a/examples/shapes/top_down_lights.zig b/examples/shapes/top_down_lights.zig new file mode 100644 index 0000000..2990309 --- /dev/null +++ b/examples/shapes/top_down_lights.zig @@ -0,0 +1,341 @@ +const rl = @import("raylib"); +const std = @import("std"); + +// Custom Blend Modes +const RLGL_SRC_ALPHA = 0x0302; +const RLGL_MIN = 0x8007; +const RLGL_MAX = 0x8008; + +const MAX_BOXES = 20; +/// 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 +const MAX_SHADOWS = MAX_BOXES * 3; +const MAX_LIGHTS = 16; + +// Shadow geometry type +const ShadowGeometry = struct { + vertices: [4]rl.Vector2, +}; + +// Light info type +const LightInfo = struct { + /// Is this light slot active? + active: bool, + /// Does this light need to be updated? + dirty: bool, + /// Is this light in a valid position? + valid: bool, + + /// Light position + position: rl.Vector2, + /// Alpha mask for the light + mask: rl.RenderTexture, + /// The distance the light touches + outerRadius: f32, + /// A cached rectangle of the light bounds to help with culling + bounds: rl.Rectangle, + + shadows: [MAX_SHADOWS]ShadowGeometry, + shadowCount: usize, +}; + +var lights: [MAX_LIGHTS]LightInfo = undefined; + +// Move a light and mark it as dirty so that we update it's mask next frame +fn MoveLight(slot: usize, x: f32, y: f32) void { + lights[slot].dirty = true; + lights[slot].position.x = x; + lights[slot].position.y = y; + + // update the cached bounds + lights[slot].bounds.x = x - lights[slot].outerRadius; + lights[slot].bounds.y = y - lights[slot].outerRadius; +} + +// Compute a shadow volume for the edge +// It takes the edge and projects it back by the light radius and turns it into a quad +fn computeShadowVolumeForEdge(slot: usize, sp: rl.Vector2, ep: rl.Vector2) void { + if (lights[slot].shadowCount >= MAX_SHADOWS) return; + + const extension = lights[slot].outerRadius * 2; + + const spVector = rl.math.vector2Normalize(rl.math.vector2Subtract(sp, lights[slot].position)); + const spProjection = rl.math.vector2Add(sp, rl.math.vector2Scale(spVector, extension)); + + const epVector = rl.math.vector2Normalize(rl.math.vector2Subtract(ep, lights[slot].position)); + const epProjection = rl.math.vector2Add(ep, rl.math.vector2Scale(epVector, extension)); + + lights[slot].shadows[lights[slot].shadowCount].vertices[0] = sp; + lights[slot].shadows[lights[slot].shadowCount].vertices[1] = ep; + lights[slot].shadows[lights[slot].shadowCount].vertices[2] = epProjection; + lights[slot].shadows[lights[slot].shadowCount].vertices[3] = spProjection; + + lights[slot].shadowCount += 1; +} + +// Draw the light and shadows to the mask for a light +fn drawLightMask(slot: usize) void { + // Use the light mask + rl.beginTextureMode(lights[slot].mask); + defer rl.endTextureMode(); + + rl.clearBackground(.white); + + // Force the blend mode to only set the alpha of the destination + rl.gl.rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN); + rl.gl.rlSetBlendMode(@intFromEnum(rl.gl.rlBlendMode.rl_blend_custom)); + // defer going back to normal blend mode + defer rl.gl.rlSetBlendMode(@intFromEnum(rl.gl.rlBlendMode.rl_blend_alpha)); + + // If we are valid, then draw the light radius to the alpha mask + if (lights[slot].valid) rl.drawCircleGradient(@intFromFloat(lights[slot].position.x), @intFromFloat(lights[slot].position.y), lights[slot].outerRadius, rl.colorAlpha(.white, 0), .white); + + rl.gl.rlDrawRenderBatchActive(); + + // Cut out the shadows from the light radius by forcing the alpha to maximum + rl.gl.rlSetBlendMode(@intFromEnum(rl.gl.rlBlendMode.rl_blend_alpha)); + rl.gl.rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MAX); + rl.gl.rlSetBlendMode(@intFromEnum(rl.gl.rlBlendMode.rl_blend_custom)); + + // Draw the shadows to the alpha mask + for (0..lights[slot].shadowCount) |i| { + rl.drawTriangleFan(&lights[slot].shadows[i].vertices, .white); + } + + rl.gl.rlDrawRenderBatchActive(); +} + +// Setup a light +fn setupLight(slot: usize, x: f32, y: f32, radius: f32) !void { + lights[slot].active = true; + lights[slot].valid = false; // The light must prove it is valid + lights[slot].mask = try rl.loadRenderTexture(rl.getScreenWidth(), rl.getScreenHeight()); + lights[slot].outerRadius = radius; + + lights[slot].bounds.width = radius * 2; + lights[slot].bounds.height = radius * 2; + + MoveLight(slot, x, y); + + // Force the render texture to have something in it + drawLightMask(slot); +} + +// See if a light needs to update it's mask +// fn UpdateLight(slot: usize, Rectangle* boxes, int count) bool +fn updateLight(slot: usize, boxes: []rl.Rectangle) bool { + if (!lights[slot].active or !lights[slot].dirty) return false; + + lights[slot].dirty = false; + lights[slot].shadowCount = 0; + lights[slot].valid = false; + + for (boxes) |box| { + // Are we in a box? if so we are not valid + if (rl.checkCollisionPointRec(lights[slot].position, box)) return false; + + // If this box is outside our bounds, we can skip it + if (!rl.checkCollisionRecs(lights[slot].bounds, box)) continue; + + // Check the edges that are on the same side we are, and cast shadow volumes out from them + + // Top + var sp = rl.Vector2{ .x = box.x, .y = box.y }; + var ep = rl.Vector2{ .x = box.x + box.width, .y = box.y }; + + if (lights[slot].position.y > ep.y) computeShadowVolumeForEdge(slot, sp, ep); + + // Right + sp = ep; + ep.y += box.height; + if (lights[slot].position.x < ep.x) computeShadowVolumeForEdge(slot, sp, ep); + + // Bottom + sp = ep; + ep.x -= box.width; + if (lights[slot].position.y < ep.y) computeShadowVolumeForEdge(slot, sp, ep); + + // Left + sp = ep; + ep.y -= box.height; + if (lights[slot].position.x > ep.x) computeShadowVolumeForEdge(slot, sp, ep); + + // The box itself + lights[slot].shadows[lights[slot].shadowCount].vertices[0] = rl.Vector2{ .x = box.x, .y = box.y }; + lights[slot].shadows[lights[slot].shadowCount].vertices[1] = rl.Vector2{ .x = box.x, .y = box.y + box.height }; + lights[slot].shadows[lights[slot].shadowCount].vertices[2] = rl.Vector2{ .x = box.x + box.width, .y = box.y + box.height }; + lights[slot].shadows[lights[slot].shadowCount].vertices[3] = rl.Vector2{ .x = box.x + box.width, .y = box.y }; + lights[slot].shadowCount += 1; + } + + lights[slot].valid = true; + + drawLightMask(slot); + + return true; +} + +// Set up some boxes +fn setupBoxes(boxes: []rl.Rectangle) void { + boxes[0] = rl.Rectangle{ .x = 150, .y = 80, .width = 40, .height = 40 }; + boxes[1] = rl.Rectangle{ .x = 1200, .y = 700, .width = 40, .height = 40 }; + boxes[2] = rl.Rectangle{ .x = 200, .y = 600, .width = 40, .height = 40 }; + boxes[3] = rl.Rectangle{ .x = 1000, .y = 50, .width = 40, .height = 40 }; + boxes[4] = rl.Rectangle{ .x = 500, .y = 350, .width = 40, .height = 40 }; + + for (5..boxes.len) |i| { + boxes[i] = rl.Rectangle{ + .x = @floatFromInt(rl.getRandomValue(0, rl.getScreenWidth())), + .y = @floatFromInt(rl.getRandomValue(0, rl.getScreenHeight())), + .width = @floatFromInt(rl.getRandomValue(10, 100)), + .height = @floatFromInt(rl.getRandomValue(10, 100)), + }; + } +} + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +pub fn main() anyerror!void { + // Initialization + //-------------------------------------------------------------------------------------- + const screenWidth = 800; + const screenHeight = 450; + + rl.initWindow(screenWidth, screenHeight, "raylib [shapes] example - top down lights"); + defer rl.closeWindow(); // Close window and OpenGL context + + // Initialize our 'world' of boxes + var boxes: [MAX_BOXES]rl.Rectangle = undefined; + setupBoxes(&boxes); + + // Create a checkerboard ground texture + const img = rl.genImageChecked(64, 64, 32, 32, .dark_brown, .dark_gray); + defer rl.unloadImage(img); + const backgroundTexture = try rl.loadTextureFromImage(img); + defer rl.unloadTexture(backgroundTexture); + + // Create a global light mask to hold all the blended lights + const lightMask = try rl.loadRenderTexture(rl.getScreenWidth(), rl.getScreenHeight()); + defer rl.unloadRenderTexture(lightMask); + + // Setup initial light + try setupLight(0, 600, 400, 300); + defer { + //deinitialize light + for (&lights) |*light| { + if (light.active) rl.unloadRenderTexture(light.mask); + } + } + var nextLight: usize = 1; + + var showLines = false; + + rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!rl.windowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + // Drag light 0 + if (rl.isMouseButtonDown(.left)) MoveLight(0, rl.getMousePosition().x, rl.getMousePosition().y); + + // Make a new light + if (rl.isMouseButtonPressed(.right) and (nextLight < MAX_LIGHTS)) { + try setupLight(nextLight, rl.getMousePosition().x, rl.getMousePosition().y, 200); + nextLight += 1; + } + + // Toggle debug info + if (rl.isKeyPressed(.f1)) showLines = !showLines; + + // Update the lights and keep track if any were dirty so we know if we need to update the master light mask + var dirtyLights = false; + for (0..MAX_LIGHTS) |i| { + if (updateLight(i, &boxes)) dirtyLights = true; + } + + // Update the light mask + if (dirtyLights) { + // Build up the light mask + rl.beginTextureMode(lightMask); + defer rl.endTextureMode(); + + rl.clearBackground(.black); + + // Force the blend mode to only set the alpha of the destination + rl.gl.rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN); + rl.gl.rlSetBlendMode(@intFromEnum(rl.gl.rlBlendMode.rl_blend_custom)); + defer rl.gl.rlSetBlendMode(@intFromEnum(rl.gl.rlBlendMode.rl_blend_alpha)); + + // Merge in all the light masks + for (lights) |light| { + if (light.active) rl.drawTextureRec(light.mask.texture, rl.Rectangle{ + .x = 0, + .y = 0, + .width = @floatFromInt(rl.getScreenWidth()), + .height = @floatFromInt(-rl.getScreenHeight()), + }, rl.Vector2.zero(), .white); + } + + rl.gl.rlDrawRenderBatchActive(); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + rl.beginDrawing(); + defer rl.endDrawing(); + + rl.clearBackground(.black); + + // Draw the tile background + rl.drawTextureRec(backgroundTexture, rl.Rectangle{ + .x = 0, + .y = 0, + .width = @floatFromInt(rl.getScreenWidth()), + .height = @floatFromInt(rl.getScreenHeight()), + }, rl.Vector2.zero(), .white); + + // Overlay the shadows from all the lights + rl.drawTextureRec(lightMask.texture, rl.Rectangle{ + .x = 0, + .y = 0, + .width = @floatFromInt(rl.getScreenWidth()), + .height = @floatFromInt(-rl.getScreenHeight()), + }, rl.Vector2.zero(), rl.colorAlpha(.white, if (showLines) 0.75 else 1.0)); + + // Draw the lights + for (0..MAX_LIGHTS) |i| { + if (lights[i].active) rl.drawCircle(@intFromFloat(lights[i].position.x), @intFromFloat(lights[i].position.y), 10, if (i == 0) .yellow else .white); + } + + if (showLines) { + for (0..lights[0].shadowCount) |s| { + rl.drawTriangleFan(&lights[0].shadows[s].vertices, .dark_purple); + } + + for (boxes) |box| { + if (rl.checkCollisionRecs(box, lights[0].bounds)) rl.drawRectangleRec(box, .purple); + + rl.drawRectangleLines(@intFromFloat(box.x), @intFromFloat(box.y), @intFromFloat(box.width), @intFromFloat(box.height), .dark_blue); + } + + rl.drawText("(F1) Hide Shadow Volumes", 10, 50, 10, .green); + } else { + rl.drawText("(F1) Show Shadow Volumes", 10, 50, 10, .green); + } + + rl.drawFPS(screenWidth - 80, 10); + rl.drawText("Drag to move light #1", 10, 10, 10, .dark_green); + rl.drawText("Right click to add new light", 10, 30, 10, .dark_green); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + + //-------------------------------------------------------------------------------------- + +}