mirror of
https://github.com/raylib-zig/raylib-zig.git
synced 2025-12-06 06:13:08 +00:00
example: add top_down_lights
This commit is contained in:
parent
813f0323af
commit
125ead95a3
@ -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",
|
||||
|
||||
341
examples/shapes/top_down_lights.zig
Normal file
341
examples/shapes/top_down_lights.zig
Normal 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
|
||||
//--------------------------------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user