example: add top_down_lights

This commit is contained in:
Timothy Fiss 2025-07-23 17:49:30 -06:00 committed by Nikolas
parent 813f0323af
commit 125ead95a3
2 changed files with 346 additions and 0 deletions

View File

@ -269,6 +269,11 @@ pub fn build(b: *std.Build) !void {
.path = "examples/shapes/splines_drawing.zig", .path = "examples/shapes/splines_drawing.zig",
.desc = "Renders a spline", .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", .name = "sprite_anim",
.path = "examples/textures/sprite_anim.zig", .path = "examples/textures/sprite_anim.zig",

View File

@ -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
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
}