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

orion-ecs / keen-eye / 20923977474

12 Jan 2026 03:00PM UTC coverage: 65.151% (-20.0%) from 85.137%
20923977474

push

github

tyevco
feat(kesl): Update KeslCompiler API for vertex/fragment shaders (#583)

Phase 6: Integration & Polish

- Update CompileAndGenerate to handle vertex and fragment shaders
- Add GenerateShader overloads for VertexDeclaration and FragmentDeclaration
- Use appropriate file extensions (.vert.glsl, .frag.glsl) for
  vertex and fragment shaders

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

7288 of 10967 branches covered (66.45%)

Branch coverage included in aggregate %.

10 of 43 new or added lines in 1 file covered. (23.26%)

801 existing lines in 12 files now uncovered.

46001 of 70826 relevant lines covered (64.95%)

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
31
    private readonly List<(Entity Entity, Transform3D Transform, Renderable Renderable)> shadowCasters = [];
×
32

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

135
                case LightType.Point:
UNCOV
136
                    RenderPointLightShadow(
×
UNCOV
137
                        lightEntity.Id,
×
UNCOV
138
                        lightTransform.Position,
×
UNCOV
139
                        light.Range);
×
UNCOV
140
                    break;
×
141

142
                case LightType.Spot:
UNCOV
143
                    RenderSpotLightShadow(
×
UNCOV
144
                        lightEntity.Id,
×
UNCOV
145
                        lightTransform.Position,
×
146
                        lightTransform.Forward(),
×
UNCOV
147
                        light.OuterConeAngle,
×
UNCOV
148
                        light.Range);
×
149
                    break;
150
            }
151
        }
UNCOV
152
    }
×
153

154
    private void CreateShaders()
155
    {
156
        // Create depth-only shader for shadow passes
157
        // Using a simple shader that just transforms vertices
158
        const string depthVertexSource = """
159
            #version 330 core
160

161
            layout (location = 0) in vec3 aPosition;
162

163
            uniform mat4 uModel;
164
            uniform mat4 uLightSpaceMatrix;
165

166
            void main()
167
            {
168
                gl_Position = uLightSpaceMatrix * uModel * vec4(aPosition, 1.0);
169
            }
170
            """;
171

172
        const string depthFragmentSource = """
173
            #version 330 core
174

175
            void main()
176
            {
177
                // Depth is automatically written
178
            }
179
            """;
180

UNCOV
181
        depthShader = graphics!.CreateShader(depthVertexSource, depthFragmentSource);
×
182
    }
×
183

184
    private void CollectShadowCasters()
185
    {
UNCOV
186
        shadowCasters.Clear();
×
187

UNCOV
188
        foreach (var entity in world!.Query<Transform3D, Renderable>())
×
189
        {
UNCOV
190
            ref readonly var renderable = ref world.Get<Renderable>(entity);
×
191

192
            // Skip entities that don't cast shadows
193
            if (!renderable.CastShadows || renderable.MeshId <= 0)
×
194
            {
195
                continue;
196
            }
197

UNCOV
198
            ref readonly var transform = ref world.Get<Transform3D>(entity);
×
UNCOV
199
            shadowCasters.Add((entity, transform, renderable));
×
200
        }
UNCOV
201
    }
×
202

203
    private void RenderDirectionalShadow(
204
        int lightEntityId,
205
        Vector3 lightDirection,
206
        Matrix4x4 cameraView,
207
        Matrix4x4 cameraProjection,
208
        float cameraNear)
209
    {
210
        // Ensure shadow map exists
211
        shadowManager!.CreateDirectionalShadowMap(lightEntityId, Settings);
×
212

213
        // Update light-space matrices
UNCOV
214
        shadowManager.UpdateDirectionalLightMatrices(
×
215
            lightEntityId,
×
216
            lightDirection,
×
UNCOV
217
            cameraView,
×
218
            cameraProjection,
×
UNCOV
219
            cameraNear);
×
220

221
        var shadowData = shadowManager.GetDirectionalShadowData(lightEntityId);
×
222
        if (!shadowData.HasValue)
×
223
        {
UNCOV
224
            return;
×
225
        }
226

UNCOV
227
        var data = shadowData.Value;
×
UNCOV
228
        int cascadeCount = data.Settings.ClampedCascadeCount;
×
229
        int resolution = data.Settings.ResolutionPixels;
×
230

231
        // Bind depth shader
UNCOV
232
        graphics!.BindShader(depthShader);
×
233

234
        // Save current render state
235
        graphics.SetDepthTest(true);
×
236
        graphics.SetCulling(true, CullFaceMode.Front); // Render back faces to reduce peter-panning
×
237

238
        // Render each cascade
239
        for (int cascade = 0; cascade < cascadeCount; cascade++)
×
240
        {
241
            var renderTarget = data.GetCascadeRenderTarget(cascade);
×
UNCOV
242
            var lightSpaceMatrix = data.GetLightSpaceMatrix(cascade);
×
243

244
            // Bind render target
UNCOV
245
            graphics.BindRenderTarget(renderTarget);
×
UNCOV
246
            graphics.SetViewport(0, 0, resolution, resolution);
×
247
            graphics.Clear(ClearMask.DepthBuffer);
×
248

249
            // Set light-space matrix uniform
250
            graphics.SetUniform("uLightSpaceMatrix", lightSpaceMatrix);
×
251

252
            // Render all shadow casters
253
            foreach (var (_, transform, renderable) in shadowCasters)
×
254
            {
UNCOV
255
                Matrix4x4 modelMatrix = transform.Matrix();
×
UNCOV
256
                graphics.SetUniform("uModel", modelMatrix);
×
257

258
                var meshHandle = new MeshHandle(renderable.MeshId);
×
259
                graphics.DrawMesh(meshHandle);
×
260
            }
261
        }
262

263
        // Unbind render target and restore state
UNCOV
264
        graphics.UnbindRenderTarget();
×
265
        graphics.SetCulling(true, CullFaceMode.Back); // Restore normal culling
×
UNCOV
266
    }
×
267

268
    private void RenderSpotLightShadow(
269
        int lightEntityId,
270
        Vector3 lightPosition,
271
        Vector3 lightDirection,
272
        float outerConeAngle,
273
        float range)
274
    {
275
        // Ensure shadow map exists
UNCOV
276
        shadowManager!.CreateSpotShadowMap(lightEntityId);
×
277

278
        // Update light-space matrix
279
        shadowManager.UpdateSpotLightMatrix(
×
UNCOV
280
            lightEntityId,
×
UNCOV
281
            lightPosition,
×
UNCOV
282
            lightDirection,
×
UNCOV
283
            outerConeAngle,
×
UNCOV
284
            range);
×
285

UNCOV
286
        var shadowData = shadowManager.GetSpotShadowData(lightEntityId);
×
UNCOV
287
        if (!shadowData.HasValue)
×
288
        {
UNCOV
289
            return;
×
290
        }
291

UNCOV
292
        var data = shadowData.Value;
×
UNCOV
293
        int resolution = Settings.ResolutionPixels;
×
294

295
        // Bind depth shader (reuse the same shader as directional)
UNCOV
296
        graphics!.BindShader(depthShader);
×
297

298
        // Configure render state
UNCOV
299
        graphics.SetDepthTest(true);
×
UNCOV
300
        graphics.SetCulling(true, CullFaceMode.Front); // Render back faces to reduce peter-panning
×
301

302
        // Bind render target
UNCOV
303
        graphics.BindRenderTarget(data.RenderTarget);
×
UNCOV
304
        graphics.SetViewport(0, 0, resolution, resolution);
×
UNCOV
305
        graphics.Clear(ClearMask.DepthBuffer);
×
306

307
        // Set light-space matrix uniform
UNCOV
308
        graphics.SetUniform("uLightSpaceMatrix", data.LightSpaceMatrix);
×
309

310
        // Render all shadow casters
UNCOV
311
        foreach (var (_, transform, renderable) in shadowCasters)
×
312
        {
UNCOV
313
            Matrix4x4 modelMatrix = transform.Matrix();
×
UNCOV
314
            graphics.SetUniform("uModel", modelMatrix);
×
315

UNCOV
316
            var meshHandle = new MeshHandle(renderable.MeshId);
×
UNCOV
317
            graphics.DrawMesh(meshHandle);
×
318
        }
319

320
        // Unbind render target and restore state
UNCOV
321
        graphics.UnbindRenderTarget();
×
UNCOV
322
        graphics.SetCulling(true, CullFaceMode.Back);
×
UNCOV
323
    }
×
324

325
    private void RenderPointLightShadow(
326
        int lightEntityId,
327
        Vector3 lightPosition,
328
        float range)
329
    {
330
        // Ensure shadow map exists
UNCOV
331
        shadowManager!.CreatePointShadowMap(lightEntityId);
×
332

333
        // Update light data
UNCOV
334
        shadowManager.UpdatePointLightData(lightEntityId, lightPosition, range);
×
335

UNCOV
336
        var shadowData = shadowManager.GetPointShadowData(lightEntityId);
×
UNCOV
337
        if (!shadowData.HasValue)
×
338
        {
UNCOV
339
            return;
×
340
        }
341

UNCOV
342
        var data = shadowData.Value;
×
UNCOV
343
        int resolution = data.RenderTarget.Size;
×
344

345
        // Bind depth shader
UNCOV
346
        graphics!.BindShader(depthShader);
×
347

348
        // Configure render state
UNCOV
349
        graphics.SetDepthTest(true);
×
UNCOV
350
        graphics.SetCulling(true, CullFaceMode.Front); // Render back faces to reduce peter-panning
×
351

352
        // Render each cubemap face
UNCOV
353
        CubemapFace[] faces =
×
UNCOV
354
        [
×
UNCOV
355
            CubemapFace.PositiveX,
×
UNCOV
356
            CubemapFace.NegativeX,
×
UNCOV
357
            CubemapFace.PositiveY,
×
UNCOV
358
            CubemapFace.NegativeY,
×
UNCOV
359
            CubemapFace.PositiveZ,
×
UNCOV
360
            CubemapFace.NegativeZ
×
UNCOV
361
        ];
×
362

UNCOV
363
        for (int faceIndex = 0; faceIndex < 6; faceIndex++)
×
364
        {
365
            // Bind the cubemap face as render target
UNCOV
366
            graphics.BindCubemapRenderTarget(data.RenderTarget, faces[faceIndex]);
×
UNCOV
367
            graphics.SetViewport(0, 0, resolution, resolution);
×
UNCOV
368
            graphics.Clear(ClearMask.DepthBuffer);
×
369

370
            // Get the light-space matrix for this face
UNCOV
371
            var lightSpaceMatrix = CascadeUtils.GetPointLightShadowMatrix(lightPosition, range, faceIndex);
×
UNCOV
372
            graphics.SetUniform("uLightSpaceMatrix", lightSpaceMatrix);
×
373

374
            // Render all shadow casters
UNCOV
375
            foreach (var (_, transform, renderable) in shadowCasters)
×
376
            {
UNCOV
377
                Matrix4x4 modelMatrix = transform.Matrix();
×
UNCOV
378
                graphics.SetUniform("uModel", modelMatrix);
×
379

UNCOV
380
                var meshHandle = new MeshHandle(renderable.MeshId);
×
UNCOV
381
                graphics.DrawMesh(meshHandle);
×
382
            }
383
        }
384

385
        // Unbind render target and restore state
UNCOV
386
        graphics.UnbindRenderTarget();
×
UNCOV
387
        graphics.SetCulling(true, CullFaceMode.Back);
×
UNCOV
388
    }
×
389

390
    /// <inheritdoc />
391
    public void Dispose()
392
    {
UNCOV
393
        if (disposed)
×
394
        {
UNCOV
395
            return;
×
396
        }
397

UNCOV
398
        disposed = true;
×
399

UNCOV
400
        if (shadersCreated && graphics is not null)
×
401
        {
UNCOV
402
            graphics.DeleteShader(depthShader);
×
403
        }
404

UNCOV
405
        shadowManager?.Dispose();
×
UNCOV
406
        shadowCasters.Clear();
×
UNCOV
407
    }
×
408
}
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