feat: shadow mapping via rlgl depth FBO (official raylib approach)

- Depth-only FBO via Rlgl.LoadFramebuffer + Rlgl.LoadTextureDepth (2048x2048)
- Shadow pass: BeginMode3D with orthographic light camera, grab
  ModelView/Projection matrices via Rlgl.GetMatrixModelview/Projection
- Main pass: Rlgl.ActiveTextureSlot(1) + Rlgl.EnableTexture(depth) +
  SetShaderValue(shadowMapLoc, 1) to bind shadow map on texture unit 1
- Main shader: uniform mat4 lightViewProj + uniform sampler2D shadowMap
  with PCF 3x3 soft shadows, bias 0.005
- Shadow factor applied to first directional light only
- Shadow shader: empty fragment (depth written automatically)
- 66/66 tests, all shaders compile, 145 FPS
This commit is contained in:
emil28092005
2026-06-17 18:03:45 +03:00
parent de262732bd
commit 2e73a0b52c
+144 -10
View File
@@ -20,7 +20,12 @@ namespace Engine.Graphics.RaylibBackend;
/// </summary>
public sealed class RaylibRenderer : IRenderer
{
private const int ShadowMapSize = 2048;
private readonly Shader _shader;
private readonly Shader _shadowShader;
private readonly uint _shadowFbo;
private readonly uint _shadowDepthTex;
private readonly Dictionary<Entity, Raylib_cs.Model> _modelCache = new();
private readonly Dictionary<string, Texture2D> _textureCache = new();
private readonly int _materialColorLoc;
@@ -36,7 +41,8 @@ public sealed class RaylibRenderer : IRenderer
private readonly int _lightPosLoc;
private readonly int _lightTypeLoc;
private readonly int _lightRangeLoc;
// Shadow receiver shader uniforms
private readonly int _lightViewProjLoc;
private readonly int _shadowMapLoc;
private readonly float[] _lightDirs = new float[12];
private readonly float[] _lightPositions = new float[12];
private readonly float[] _lightRanges = new float[4];
@@ -57,6 +63,13 @@ public sealed class RaylibRenderer : IRenderer
public RaylibRenderer()
{
_shader = LoadShader();
_shadowShader = LoadShadowShader();
// Create depth-only FBO for shadow mapping (per official raylib example)
_shadowFbo = Rlgl.LoadFramebuffer();
_shadowDepthTex = Rlgl.LoadTextureDepth(ShadowMapSize, ShadowMapSize, false);
Rlgl.FramebufferAttach(_shadowFbo, _shadowDepthTex, FramebufferAttachType.Depth, FramebufferAttachTextureType.Texture2D, 0);
Rlgl.FramebufferComplete(_shadowFbo);
// Main shader uniform locations
_materialColorLoc = Raylib.GetShaderLocation(_shader, "materialColor");
@@ -72,7 +85,8 @@ public sealed class RaylibRenderer : IRenderer
_lightPosLoc = Raylib.GetShaderLocation(_shader, "lightPositions");
_lightTypeLoc = Raylib.GetShaderLocation(_shader, "lightTypes");
_lightRangeLoc = Raylib.GetShaderLocation(_shader, "lightRanges");
_lightViewProjLoc = Raylib.GetShaderLocation(_shader, "lightViewProj");
_shadowMapLoc = Raylib.GetShaderLocation(_shader, "shadowMap");
}
public void RequestScreenshot(string outputPath)
@@ -87,7 +101,68 @@ public sealed class RaylibRenderer : IRenderer
public void RenderWorld(World world)
{
var camera = GetCamera(world);
CollectLights(world);
var hasDirectionalLight = _lightCount > 0 && _lightTypes[0] == (int)LightType.Directional;
Matrix4x4 lightViewProj = Matrix4x4.Identity;
// === PASS 1: Shadow map (render depth from light's POV) ===
if (hasDirectionalLight)
{
var lightDir = new Vector3(_lightDirs[0], _lightDirs[1], _lightDirs[2]);
var sceneCenter = new Vector3(0, 0.5f, 0);
var lightPos = sceneCenter - lightDir * 30f;
var up = MathF.Abs(Vector3.Dot(lightDir, Vector3.UnitY)) > 0.99f
? Vector3.UnitZ : Vector3.UnitY;
var shadowCamera = new Camera3D
{
Position = lightPos,
Target = sceneCenter,
Up = up,
FovY = 0,
Projection = CameraProjection.Orthographic
};
// Use BeginTextureMode with our custom depth FBO
Rlgl.EnableFramebuffer(_shadowFbo);
Rlgl.Viewport(0, 0, ShadowMapSize, ShadowMapSize);
Rlgl.ClearColor(255, 255, 255, 255);
Rlgl.ClearScreenBuffers();
Raylib.BeginMode3D(shadowCamera);
// Grab light view/proj matrices AFTER BeginMode3D (like official example)
lightViewProj = Rlgl.GetMatrixModelview() * Rlgl.GetMatrixProjection();
// Draw all shadow casters with the simple shadow shader
world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
{
if (e.Name() == "Grid" || e.Name() == "Floor")
return;
var model = GetOrUploadModel(e, mesh);
var modelMatrix = transform.GetMatrix();
if (Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position))
{
var (axis, angle) = QuaternionToAxisAngle(rotation);
unsafe
{
var origShader = model.Materials[0].Shader;
model.Materials[0].Shader = _shadowShader;
Raylib.DrawModelEx(model, position, axis, angle * 180.0f / MathF.PI, scale, Color.White);
model.Materials[0].Shader = origShader;
}
}
});
Raylib.EndMode3D();
Rlgl.DisableFramebuffer();
Rlgl.Viewport(0, 0, Raylib.GetScreenWidth(), Raylib.GetScreenHeight());
}
// === PASS 2: Main render with shadow sampling ===
Raylib.BeginDrawing();
Raylib.ClearBackground(new Color(25, 30, 40, 255));
Raylib.BeginMode3D(ToRaylib(camera));
@@ -96,6 +171,19 @@ public sealed class RaylibRenderer : IRenderer
SetFrameLights();
Raylib.SetShaderValue(_shader, _viewPosLoc, new float[] { camera.Position.X, camera.Position.Y, camera.Position.Z }, ShaderUniformDataType.Vec3);
// Set shadow uniforms on the main shader
if (hasDirectionalLight && _lightViewProjLoc >= 0)
{
Raylib.SetShaderValueMatrix(_shader, _lightViewProjLoc, lightViewProj);
// Bind shadow map on texture unit 1, tell shader sampler to use it
if (_shadowMapLoc >= 0)
{
Rlgl.ActiveTextureSlot(1);
Rlgl.EnableTexture(_shadowDepthTex);
Raylib.SetShaderValue(_shader, _shadowMapLoc, 1, ShaderUniformDataType.Int);
}
}
Rlgl.DisableBackfaceCulling();
world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
@@ -115,6 +203,9 @@ public sealed class RaylibRenderer : IRenderer
}
});
// Reset texture unit 0 after shadow binding
Rlgl.ActiveTextureSlot(0);
Rlgl.EnableBackfaceCulling();
Raylib.DrawGrid(20, 1.0f);
@@ -410,6 +501,22 @@ public sealed class RaylibRenderer : IRenderer
private static Shader LoadShadowShader()
{
const string VertexSource = @"#version 330 core
in vec3 vertexPosition;
uniform mat4 mvp;
void main()
{
gl_Position = mvp * vec4(vertexPosition, 1.0);
}";
const string FragmentSource = @"#version 330 core
void main() {}";
return Raylib.LoadShaderFromMemory(VertexSource, FragmentSource);
}
private static Shader LoadShader()
{
const string VertexSource = @"#version 330 core
@@ -453,6 +560,8 @@ uniform float lightIntensities[4];
uniform vec3 lightColors[4];
uniform int lightTypes[4];
uniform float lightRanges[4];
uniform mat4 lightViewProj;
uniform sampler2D shadowMap;
vec3 ACESFilm(vec3 x)
{
@@ -460,7 +569,6 @@ vec3 ACESFilm(vec3 x)
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}
// Smooth point light attenuation (Unity-like)
float Attenuation(float dist, float range)
{
float r = max(range, 0.001);
@@ -471,15 +579,32 @@ float Attenuation(float dist, float range)
return clamp(1.0 / (1.0 + 25.0 * x4), 0.0, 1.0) * smoothstep(1.0, 0.0, x);
}
float CalculateShadow(vec3 worldPos)
{
vec4 lp = lightViewProj * vec4(worldPos, 1.0);
vec3 ndc = lp.xyz / lp.w;
vec3 uvw = ndc * 0.5 + 0.5;
if (uvw.x < 0.0 || uvw.x > 1.0 || uvw.y < 0.0 || uvw.y > 1.0 || uvw.z > 1.0)
return 1.0;
float bias = 0.005;
vec2 ts = vec2(1.0 / 2048.0);
float s = 0.0;
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
float d = texture(shadowMap, uvw.xy + vec2(x, y) * ts).r;
s += (uvw.z - bias > d) ? 0.3 : 1.0;
}
}
return s / 9.0;
}
void main()
{
vec3 normal = normalize(vNormal);
// Convert sRGB vertex color + material color to linear before lighting
vec3 albedo = pow(vColor.rgb * materialColor.rgb, vec3(2.2));
if (useTexture != 0)
{
vec2 uv = vTexCoord * 4.0;
// Texture is already in sRGB — convert to linear
vec3 texColor = pow(texture(texture0, uv).rgb, vec3(2.2));
albedo *= texColor;
}
@@ -493,7 +618,11 @@ void main()
float hemisphere = 0.5 + 0.5 * normal.y;
vec3 result = albedo * mix(groundColor, skyColor, hemisphere) * 0.4;
// F0 as vec3: dielectric 0.04, metals use albedo
// Shadow factor for first directional light
float shadow = 1.0;
if (lightCount > 0 && lightTypes[0] == 0)
shadow = CalculateShadow(vWorldPos);
vec3 F0 = mix(vec3(0.04), albedo, metal);
float shininess = mix(8.0, 256.0, 1.0 - rough);
@@ -502,18 +631,20 @@ void main()
vec3 L;
float atten = 1.0;
if (lightTypes[i] == 1) // Point light
if (lightTypes[i] == 1)
{
vec3 toLight = lightPositions[i] - vWorldPos;
float dist = length(toLight);
L = toLight / max(dist, 0.001);
atten = Attenuation(dist, lightRanges[i]);
}
else // Directional light
else
{
L = normalize(-lightDirs[i]);
}
float lightShadow = (i == 0 && lightTypes[0] == 0) ? shadow : 1.0;
vec3 H = normalize(L + viewDir);
float NdotL = max(dot(normal, L), 0.0);
float NdotH = max(dot(normal, H), 0.0);
@@ -522,8 +653,8 @@ void main()
float spec = pow(NdotH, shininess);
vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - HdotV, 5.0);
vec3 specularColor = mix(fresnel, albedo * fresnel, metal);
vec3 diffuse = albedo * lightColors[i] * diff * lightIntensities[i] * atten * 1.5;
vec3 specular = specularColor * spec * lightIntensities[i] * atten;
vec3 diffuse = albedo * lightColors[i] * diff * lightIntensities[i] * atten * 1.5 * lightShadow;
vec3 specular = specularColor * spec * lightIntensities[i] * atten * lightShadow;
diffuse *= (1.0 - fresnel * (1.0 - metal * 0.5));
result += diffuse + specular;
}
@@ -549,6 +680,9 @@ void main()
Raylib.UnloadTexture(texture);
_textureCache.Clear();
Raylib.UnloadShader(_shadowShader);
Rlgl.UnloadTexture(_shadowDepthTex);
Rlgl.UnloadFramebuffer(_shadowFbo);
Raylib.UnloadShader(_shader);
}