Watch
2
0
Fork
You've already forked raylib-cs
0
raylib-cs/Examples/Shapes/TopDownLights.cs
tiger tiger tiger 8c22e68c2a
WASM examples (+ backports of new official examples) (#344)
* chg: New build system that uses the officially distributed binaries, bumped version to 8.0.0, simplified git workflow, removed deprecated OpenGL 1.1 functionality.

* chg: Modernize CI workflow, enable SourceLink

- Bump workflow actions to latest majors (Node 24); drop deprecated softprops/action-gh-release@v1
- Trigger push builds on main instead of master
- Create local nuget feed dir before pack (fixes NU1301)
- Enable Microsoft.SourceLink.GitHub for debugging symbols (ref PR #340)

* fix: centralized version data in Directory.build.props, and fixed various interop details that had incorrect function signatures

* chore: updated readme

* fix: version the native extract marker and chain download via DependsOnTargets

The .extracted marker now includes the raylib package name, so bumping
TargetRaylibTag re-extracts the new archive instead of silently keeping
(and packing/copying) the previous version's files.

_PrepareNativeLibrary and _StageWasmNative now depend directly on
_DownloadAndExtractInternal instead of CallTarget-ing it; dependency
targets run in the same project instance, so the resolved properties
(RaylibPackageName etc.) propagate naturally.

* fix: let the binding build for browser-wasm on both net8.0 and net10.0

The net8-era wasm workload (Microsoft.NET.Runtime.WebAssembly.Sdk 8.0.x,
auto-imported for RID browser-wasm) treats every browser-wasm project as
a wasm app: it forces OutputType=Exe after project evaluation (CS5001
for a classlib) and hooks its app-bundle build after Build, which errors
because a library has no assemblies to bundle. Opt Raylib-cs out via
DisableAutoWasmBuildApp (props time, before the workload defaults its
trigger) and pin OutputType back to Library in Directory.Build.targets
(evaluated after the workload props, so the assignment wins). net10's
wasm SDK needs neither workaround.

* chore: readme updated

* chg: simplifying build logic - a simple line in the documentation should save us the code here

* fix: Wrong signature of FrameBufferComplete

* chore: readme update

* feat: samples default to local project reference, and can optionally use the nuget package

* feat: backporting existing raylib-cs examples and new official raylib examples to WASM, adopting raylib's original code style

* chore: readme, gitignore, and targets backport.

* fix: Examples.csproj runs the download task when building locally

* feat: backporting existing raylib-cs examples and new official raylib examples to WASM, adopting raylib's original code style

* chore: readme, gitignore, and targets backport.

* chore: clean up linter warnings

* feat: html harness focuses the example and allows quick navigation with J/K instead.

* chore: readme mentions the property to use nuget vs. the local project reference

* feat: replaced the J/K navigation with good old HTML buttons

* chore: run dotnet format scoped default (was previously scoped to just 'style')
2026-07-30 18:34:34 +01:00

458 lines
16 KiB
C#

/*******************************************************************************************
*
* raylib [shapes] example - top down lights
*
* Example complexity rating: [★★★★] 4/4
*
* Example originally created with raylib 4.2, last time updated with raylib 4.2
*
* Example contributed by Jeffery Myers (@JeffM2501) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2022-2025 Jeffery Myers (@JeffM2501)
*
********************************************************************************************/
using static Raylib_cs.Raymath;
using static Raylib_cs.Rlgl;
namespace Examples.Shapes;
public partial class TopDownLights : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
// Custom Blend Modes
private const int RLGL_SRC_ALPHA = 0x0302;
private const int RLGL_MIN = 0x8007;
private const int RLGL_MAX = 0x8008;
private const int MAX_BOXES = 20;
private const int 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
private const int MAX_LIGHTS = 16;
public string Name => "Shapes / Top Down Lights";
public string Title => "raylib [shapes] example - top down lights";
//----------------------------------------------------------------------------------
// Types and Structures Definition
//----------------------------------------------------------------------------------
// Shadow geometry type
private class ShadowGeometry
{
public Vector2[] vertices = new Vector2[4];
}
// Light info type
private class LightInfo
{
public bool active; // Is this light slot active?
public bool dirty; // Does this light need to be updated?
public bool valid; // Is this light in a valid position?
public Vector2 position; // Light position
public RenderTexture2D mask; // Alpha mask for the light
public float outerRadius; // The distance the light touches
public Rectangle bounds; // A cached rectangle of the light bounds to help with culling
public ShadowGeometry[] shadows = new ShadowGeometry[MAX_SHADOWS];
public int shadowCount;
public LightInfo()
{
for (int i = 0; i < MAX_SHADOWS; i++)
{
shadows[i] = new ShadowGeometry();
}
}
}
//------------------------------------------------------------------------------------
// Global Variables Definition
//------------------------------------------------------------------------------------
private LightInfo[] lights;
private int boxCount;
private Rectangle[] boxes;
private Texture2D backgroundTexture;
private RenderTexture2D lightMask;
private int nextLight;
private bool showLines;
public void Init()
{
lights = new LightInfo[MAX_LIGHTS];
for (int i = 0; i < MAX_LIGHTS; i++)
{
lights[i] = new LightInfo();
}
// Initialize our 'world' of boxes
boxCount = 0;
boxes = new Rectangle[MAX_BOXES];
SetupBoxes();
// Create a checkerboard ground texture
Image img = GenImageChecked(64, 64, 32, 32, Color.DarkBrown, Color.DarkGray);
backgroundTexture = LoadTextureFromImage(img);
UnloadImage(img);
// Create a global light mask to hold all the blended lights
lightMask = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
// Setup initial light
SetupLight(0, 600, 400, 300);
nextLight = 1;
showLines = false;
}
public void Update()
{
// Update
//----------------------------------------------------------------------------------
// Drag light 0
if (IsMouseButtonDown(MouseButton.Left))
{
MoveLight(0, GetMousePosition().X, GetMousePosition().Y);
}
// Make a new light
if (IsMouseButtonPressed(MouseButton.Right) && (nextLight < MAX_LIGHTS))
{
SetupLight(nextLight, GetMousePosition().X, GetMousePosition().Y, 200);
nextLight++;
}
// Toggle debug info
if (IsKeyPressed(KeyboardKey.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
bool dirtyLights = false;
for (int i = 0; i < MAX_LIGHTS; i++)
{
if (UpdateLight(i, boxes, boxCount))
{
dirtyLights = true;
}
}
// Update the light mask
if (dirtyLights)
{
// Build up the light mask
BeginTextureMode(lightMask);
ClearBackground(Color.Black);
// Force the blend mode to only set the alpha of the destination
SetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN);
SetBlendMode(BlendMode.Custom);
// Merge in all the light masks
for (int i = 0; i < MAX_LIGHTS; i++)
{
if (lights[i].active)
{
DrawTextureRec(lights[i].mask.Texture, new Rectangle(0, 0, (float)GetScreenWidth(), -(float)GetScreenHeight()), Vector2Zero(), Color.White);
}
}
DrawRenderBatchActive();
// Go back to normal blend
SetBlendMode(BlendMode.Alpha);
EndTextureMode();
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.Black);
// Draw the tile background
DrawTextureRec(backgroundTexture, new Rectangle(0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()), Vector2Zero(), Color.White);
// Overlay the shadows from all the lights
DrawTextureRec(lightMask.Texture, new Rectangle(0, 0, (float)GetScreenWidth(), -(float)GetScreenHeight()), Vector2Zero(), ColorAlpha(Color.White, showLines ? 0.75f : 1.0f));
// Draw the lights
for (int i = 0; i < MAX_LIGHTS; i++)
{
if (lights[i].active)
{
DrawCircle((int)lights[i].position.X, (int)lights[i].position.Y, 10, (i == 0) ? Color.Yellow : Color.White);
}
}
if (showLines)
{
for (int s = 0; s < lights[0].shadowCount; s++)
{
DrawTriangleFan(lights[0].shadows[s].vertices, 4, Color.DarkPurple);
}
for (int b = 0; b < boxCount; b++)
{
if (CheckCollisionRecs(boxes[b], lights[0].bounds))
{
DrawRectangleRec(boxes[b], Color.Purple);
}
DrawRectangleLines((int)boxes[b].X, (int)boxes[b].Y, (int)boxes[b].Width, (int)boxes[b].Height, Color.DarkBlue);
}
DrawText("(F1) Hide Shadow Volumes", 10, 50, 10, Color.Green);
}
else
{
DrawText("(F1) Show Shadow Volumes", 10, 50, 10, Color.Green);
}
DrawFPS(screenWidth - 80, 10);
DrawText("Drag to move light #1", 10, 10, 10, Color.DarkGreen);
DrawText("Right click to add new light", 10, 30, 10, Color.DarkGreen);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
UnloadTexture(backgroundTexture);
UnloadRenderTexture(lightMask);
for (int i = 0; i < MAX_LIGHTS; i++)
{
if (lights[i].active)
{
UnloadRenderTexture(lights[i].mask);
}
}
}
//------------------------------------------------------------------------------------
// Module Functions Definition
//------------------------------------------------------------------------------------
// Move a light and mark it as dirty so that we update it's mask next frame
private void MoveLight(int slot, float x, float y)
{
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
private void ComputeShadowVolumeForEdge(int slot, Vector2 sp, Vector2 ep)
{
if (lights[slot].shadowCount >= MAX_SHADOWS)
{
return;
}
float extension = lights[slot].outerRadius * 2;
Vector2 spVector = Vector2Normalize(Vector2Subtract(sp, lights[slot].position));
Vector2 spProjection = Vector2Add(sp, Vector2Scale(spVector, extension));
Vector2 epVector = Vector2Normalize(Vector2Subtract(ep, lights[slot].position));
Vector2 epProjection = Vector2Add(ep, 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++;
}
// Setup a light
private void SetupLight(int slot, float x, float y, float radius)
{
lights[slot].active = true;
lights[slot].valid = false; // The light must prove it is valid
lights[slot].mask = LoadRenderTexture(GetScreenWidth(), 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
private bool UpdateLight(int slot, Rectangle[] boxes, int count)
{
if (!lights[slot].active || !lights[slot].dirty)
{
return false;
}
lights[slot].dirty = false;
lights[slot].shadowCount = 0;
lights[slot].valid = false;
for (int i = 0; i < count; i++)
{
// Are we in a box? if so we are not valid
if (CheckCollisionPointRec(lights[slot].position, boxes[i]))
{
return false;
}
// If this box is outside our bounds, we can skip it
if (!CheckCollisionRecs(lights[slot].bounds, boxes[i]))
{
continue;
}
// Check the edges that are on the same side we are, and cast shadow volumes out from them
// Top
Vector2 sp = new Vector2(boxes[i].X, boxes[i].Y);
Vector2 ep = new Vector2(boxes[i].X + boxes[i].Width, boxes[i].Y);
if (lights[slot].position.Y > ep.Y)
{
ComputeShadowVolumeForEdge(slot, sp, ep);
}
// Right
sp = ep;
ep.Y += boxes[i].Height;
if (lights[slot].position.X < ep.X)
{
ComputeShadowVolumeForEdge(slot, sp, ep);
}
// Bottom
sp = ep;
ep.X -= boxes[i].Width;
if (lights[slot].position.Y < ep.Y)
{
ComputeShadowVolumeForEdge(slot, sp, ep);
}
// Left
sp = ep;
ep.Y -= boxes[i].Height;
if (lights[slot].position.X > ep.X)
{
ComputeShadowVolumeForEdge(slot, sp, ep);
}
// The box itself
lights[slot].shadows[lights[slot].shadowCount].vertices[0] = new Vector2(boxes[i].X, boxes[i].Y);
lights[slot].shadows[lights[slot].shadowCount].vertices[1] = new Vector2(boxes[i].X, boxes[i].Y + boxes[i].Height);
lights[slot].shadows[lights[slot].shadowCount].vertices[2] = new Vector2(boxes[i].X + boxes[i].Width, boxes[i].Y + boxes[i].Height);
lights[slot].shadows[lights[slot].shadowCount].vertices[3] = new Vector2(boxes[i].X + boxes[i].Width, boxes[i].Y);
lights[slot].shadowCount++;
}
lights[slot].valid = true;
DrawLightMask(slot);
return true;
}
// Draw the light and shadows to the mask for a light
private void DrawLightMask(int slot)
{
// Use the light mask
BeginTextureMode(lights[slot].mask);
ClearBackground(Color.White);
// Force the blend mode to only set the alpha of the destination
SetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN);
SetBlendMode(BlendMode.Custom);
// If we are valid, then draw the light radius to the alpha mask
if (lights[slot].valid)
{
DrawCircleGradient(lights[slot].position, lights[slot].outerRadius, ColorAlpha(Color.White, 0), Color.White);
}
DrawRenderBatchActive();
// Cut out the shadows from the light radius by forcing the alpha to maximum
SetBlendMode(BlendMode.Alpha);
SetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MAX);
SetBlendMode(BlendMode.Custom);
// Draw the shadows to the alpha mask
for (int i = 0; i < lights[slot].shadowCount; i++)
{
DrawTriangleFan(lights[slot].shadows[i].vertices, 4, Color.White);
}
DrawRenderBatchActive();
// Go back to normal blend mode
SetBlendMode(BlendMode.Alpha);
EndTextureMode();
}
// Set up some boxes
private void SetupBoxes()
{
boxes[0] = new Rectangle(150, 80, 40, 40);
boxes[1] = new Rectangle(1200, 700, 40, 40);
boxes[2] = new Rectangle(200, 600, 40, 40);
boxes[3] = new Rectangle(1000, 50, 40, 40);
boxes[4] = new Rectangle(500, 350, 40, 40);
for (int i = 5; i < MAX_BOXES; i++)
{
boxes[i] = new Rectangle((float)GetRandomValue(0, GetScreenWidth()), (float)GetRandomValue(0, GetScreenHeight()), (float)GetRandomValue(10, 100), (float)GetRandomValue(10, 100));
}
boxCount = MAX_BOXES;
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [shapes] example - top down lights");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new TopDownLights();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}