• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 20898720638

11 Jan 2026 05:01PM UTC coverage: 85.223%. First build
20898720638

Pull #927

github

web-flow
Merge c5df84569 into e92f9bff8
Pull Request #927: feat(graphics): Add render targets and shadow mapping with CSM

9588 of 13629 branches covered (70.35%)

Branch coverage included in aggregate %.

33 of 648 new or added lines in 14 files covered. (5.09%)

165974 of 192374 relevant lines covered (86.28%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/KeenEyes.Graphics/Shadows/ShadowRenderingSystem.cs
1
using System.Numerics;
2
using KeenEyes.Common;
3
using KeenEyes.Graphics.Abstractions;
4

5
namespace KeenEyes.Graphics.Shadows;
6

7
/// <summary>
8
/// System that renders shadow maps for shadow-casting lights.
9
/// </summary>
10
/// <remarks>
11
/// <para>
12
/// This system runs before the main <see cref="RenderSystem"/> to render depth-only
13
/// passes for each shadow-casting light. The resulting shadow maps are then used
14
/// by the render system for shadow calculations.
15
/// </para>
16
/// <para>
17
/// For directional lights, this system implements Cascaded Shadow Maps (CSM) with
18
/// configurable cascade counts. For point lights, it uses cubemap shadow maps.
19
/// </para>
20
/// </remarks>
21
public sealed class ShadowRenderingSystem : ISystem
22
{
23
    private IWorld? world;
24
    private IGraphicsContext? graphics;
25
    private ShadowMapManager? shadowManager;
26
    private ShaderHandle depthShader;
27
    private bool shadersCreated;
28
    private bool disposed;
29

30
    // Cached entity data for shadow casters
NEW
31
    private readonly List<(Entity Entity, Transform3D Transform, Renderable Renderable)> shadowCasters = [];
×
32

33
    /// <inheritdoc />
NEW
34
    public bool Enabled { get; set; } = true;
×
35

36
    /// <summary>
37
    /// Gets or sets the shadow settings used for all shadow maps.
38
    /// </summary>
NEW
39
    public ShadowSettings Settings { get; set; } = ShadowSettings.Default;
×
40

41
    /// <summary>
42
    /// Gets the shadow map manager for accessing shadow data.
43
    /// </summary>
NEW
44
    public ShadowMapManager? ShadowManager => shadowManager;
×
45

46
    /// <inheritdoc />
47
    public void Initialize(IWorld world)
48
    {
NEW
49
        this.world = world;
×
50

NEW
51
        if (!world.TryGetExtension<IGraphicsContext>(out graphics))
×
52
        {
NEW
53
            throw new InvalidOperationException("ShadowRenderingSystem requires IGraphicsContext extension");
×
54
        }
55

NEW
56
        shadowManager = new ShadowMapManager(graphics!)
×
NEW
57
        {
×
NEW
58
            Settings = Settings
×
NEW
59
        };
×
NEW
60
    }
×
61

62
    /// <inheritdoc />
63
    public void Update(float deltaTime)
64
    {
NEW
65
        if (world is null || graphics is null || !graphics.IsInitialized || shadowManager is null)
×
66
        {
NEW
67
            return;
×
68
        }
69

70
        // Create shaders on first use (graphics must be initialized)
NEW
71
        if (!shadersCreated)
×
72
        {
NEW
73
            CreateShaders();
×
NEW
74
            shadersCreated = true;
×
75
        }
76

77
        // Find active camera for cascade calculations
NEW
78
        Camera camera = default;
×
NEW
79
        Transform3D cameraTransform = default;
×
NEW
80
        bool foundCamera = false;
×
81

NEW
82
        foreach (var entity in world.Query<Camera, Transform3D, MainCameraTag>())
×
83
        {
NEW
84
            camera = world.Get<Camera>(entity);
×
NEW
85
            cameraTransform = world.Get<Transform3D>(entity);
×
NEW
86
            foundCamera = true;
×
NEW
87
            break;
×
88
        }
89

NEW
90
        if (!foundCamera)
×
91
        {
NEW
92
            foreach (var entity in world.Query<Camera, Transform3D>())
×
93
            {
NEW
94
                camera = world.Get<Camera>(entity);
×
NEW
95
                cameraTransform = world.Get<Transform3D>(entity);
×
NEW
96
                foundCamera = true;
×
NEW
97
                break;
×
98
            }
99
        }
100

NEW
101
        if (!foundCamera)
×
102
        {
NEW
103
            return; // No camera, no shadows needed
×
104
        }
105

106
        // Calculate camera matrices
NEW
107
        Matrix4x4 viewMatrix = camera.ViewMatrix(cameraTransform);
×
NEW
108
        Matrix4x4 projectionMatrix = camera.ProjectionMatrix();
×
109

110
        // Collect shadow casters
NEW
111
        CollectShadowCasters();
×
112

113
        // Process each shadow-casting light
NEW
114
        foreach (var lightEntity in world.Query<Light, Transform3D>())
×
115
        {
NEW
116
            ref readonly var light = ref world.Get<Light>(lightEntity);
×
NEW
117
            if (!light.CastShadows)
×
118
            {
119
                continue;
120
            }
121

NEW
122
            ref readonly var lightTransform = ref world.Get<Transform3D>(lightEntity);
×
123

NEW
124
            switch (light.Type)
×
125
            {
126
                case LightType.Directional:
NEW
127
                    RenderDirectionalShadow(
×
NEW
128
                        lightEntity.Id,
×
NEW
129
                        lightTransform.Forward(),
×
NEW
130
                        viewMatrix,
×
NEW
131
                        projectionMatrix,
×
NEW
132
                        camera.NearPlane);
×
133
                    break;
134

135
                case LightType.Point:
136
                    // Point light shadows would render to cubemap
137
                    // TODO: Implement point light shadows
138
                    break;
139

140
                case LightType.Spot:
141
                    // Spot light shadows render to 2D texture
142
                    // TODO: Implement spot light shadows
143
                    break;
144
            }
145
        }
NEW
146
    }
×
147

148
    private void CreateShaders()
149
    {
150
        // Create depth-only shader for shadow passes
151
        // Using a simple shader that just transforms vertices
152
        const string depthVertexSource = """
153
            #version 330 core
154

155
            layout (location = 0) in vec3 aPosition;
156

157
            uniform mat4 uModel;
158
            uniform mat4 uLightSpaceMatrix;
159

160
            void main()
161
            {
162
                gl_Position = uLightSpaceMatrix * uModel * vec4(aPosition, 1.0);
163
            }
164
            """;
165

166
        const string depthFragmentSource = """
167
            #version 330 core
168

169
            void main()
170
            {
171
                // Depth is automatically written
172
            }
173
            """;
174

NEW
175
        depthShader = graphics!.CreateShader(depthVertexSource, depthFragmentSource);
×
NEW
176
    }
×
177

178
    private void CollectShadowCasters()
179
    {
NEW
180
        shadowCasters.Clear();
×
181

NEW
182
        foreach (var entity in world!.Query<Transform3D, Renderable>())
×
183
        {
NEW
184
            ref readonly var renderable = ref world.Get<Renderable>(entity);
×
185

186
            // Skip entities that don't cast shadows
NEW
187
            if (!renderable.CastShadows || renderable.MeshId <= 0)
×
188
            {
189
                continue;
190
            }
191

NEW
192
            ref readonly var transform = ref world.Get<Transform3D>(entity);
×
NEW
193
            shadowCasters.Add((entity, transform, renderable));
×
194
        }
NEW
195
    }
×
196

197
    private void RenderDirectionalShadow(
198
        int lightEntityId,
199
        Vector3 lightDirection,
200
        Matrix4x4 cameraView,
201
        Matrix4x4 cameraProjection,
202
        float cameraNear)
203
    {
204
        // Ensure shadow map exists
NEW
205
        shadowManager!.CreateDirectionalShadowMap(lightEntityId, Settings);
×
206

207
        // Update light-space matrices
NEW
208
        shadowManager.UpdateDirectionalLightMatrices(
×
NEW
209
            lightEntityId,
×
NEW
210
            lightDirection,
×
NEW
211
            cameraView,
×
NEW
212
            cameraProjection,
×
NEW
213
            cameraNear);
×
214

NEW
215
        var shadowData = shadowManager.GetDirectionalShadowData(lightEntityId);
×
NEW
216
        if (!shadowData.HasValue)
×
217
        {
NEW
218
            return;
×
219
        }
220

NEW
221
        var data = shadowData.Value;
×
NEW
222
        int cascadeCount = data.Settings.ClampedCascadeCount;
×
NEW
223
        int resolution = data.Settings.ResolutionPixels;
×
224

225
        // Bind depth shader
NEW
226
        graphics!.BindShader(depthShader);
×
227

228
        // Save current render state
NEW
229
        graphics.SetDepthTest(true);
×
NEW
230
        graphics.SetCulling(true, CullFaceMode.Front); // Render back faces to reduce peter-panning
×
231

232
        // Render each cascade
NEW
233
        for (int cascade = 0; cascade < cascadeCount; cascade++)
×
234
        {
NEW
235
            var renderTarget = data.GetCascadeRenderTarget(cascade);
×
NEW
236
            var lightSpaceMatrix = data.GetLightSpaceMatrix(cascade);
×
237

238
            // Bind render target
NEW
239
            graphics.BindRenderTarget(renderTarget);
×
NEW
240
            graphics.SetViewport(0, 0, resolution, resolution);
×
NEW
241
            graphics.Clear(ClearMask.DepthBuffer);
×
242

243
            // Set light-space matrix uniform
NEW
244
            graphics.SetUniform("uLightSpaceMatrix", lightSpaceMatrix);
×
245

246
            // Render all shadow casters
NEW
247
            foreach (var (_, transform, renderable) in shadowCasters)
×
248
            {
NEW
249
                Matrix4x4 modelMatrix = transform.Matrix();
×
NEW
250
                graphics.SetUniform("uModel", modelMatrix);
×
251

NEW
252
                var meshHandle = new MeshHandle(renderable.MeshId);
×
NEW
253
                graphics.DrawMesh(meshHandle);
×
254
            }
255
        }
256

257
        // Unbind render target and restore state
NEW
258
        graphics.UnbindRenderTarget();
×
NEW
259
        graphics.SetCulling(true, CullFaceMode.Back); // Restore normal culling
×
NEW
260
    }
×
261

262
    /// <inheritdoc />
263
    public void Dispose()
264
    {
NEW
265
        if (disposed)
×
266
        {
NEW
267
            return;
×
268
        }
269

NEW
270
        disposed = true;
×
271

NEW
272
        if (shadersCreated && graphics is not null)
×
273
        {
NEW
274
            graphics.DeleteShader(depthShader);
×
275
        }
276

NEW
277
        shadowManager?.Dispose();
×
NEW
278
        shadowCasters.Clear();
×
NEW
279
    }
×
280
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc