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

orion-ecs / keen-eye / 20903008876

11 Jan 2026 10:34PM UTC coverage: 84.977% (-0.2%) from 85.137%
20903008876

Pull #931

github

web-flow
Merge 5b4256fbb into 293374fb3
Pull Request #931: feat(graphics): Complete shadow mapping with point/spot lights and debug visualization

9626 of 13748 branches covered (70.02%)

Branch coverage included in aggregate %.

0 of 341 new or added lines in 5 files covered. (0.0%)

7 existing lines in 3 files now uncovered.

166766 of 193827 relevant lines covered (86.04%)

0.98 hits per line

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

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

5
namespace KeenEyes.Graphics.Shadows;
6

7
/// <summary>
8
/// Debug system that renders shadow map visualizations as screen overlays.
9
/// </summary>
10
/// <remarks>
11
/// <para>
12
/// This system displays shadow maps as small thumbnails in the corner of the screen,
13
/// useful for debugging shadow rendering issues. It supports:
14
/// </para>
15
/// <list type="bullet">
16
/// <item>Directional light cascade shadow maps (up to 4 cascades)</item>
17
/// <item>Spot light shadow maps</item>
18
/// <item>Point light cubemap shadow maps (6 faces)</item>
19
/// </list>
20
/// <para>
21
/// The system runs in the Render phase after the main RenderSystem and requires
22
/// a ShadowRenderingSystem to be present for accessing shadow data.
23
/// </para>
24
/// </remarks>
25
public sealed class ShadowMapVisualizationSystem : ISystem
26
{
27
    private IWorld? world;
28
    private IGraphicsContext? graphics;
29
    private ShaderHandle depthVisualizationShader;
30
    private ShaderHandle cubemapVisualizationShader;
31
    private MeshHandle quadMesh;
32
    private bool shadersCreated;
33
    private bool disposed;
34

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

38
    /// <summary>
39
    /// Gets or sets the shadow rendering system to visualize.
40
    /// </summary>
NEW
41
    public ShadowRenderingSystem? ShadowSystem { get; set; }
×
42

43
    /// <summary>
44
    /// Gets or sets the visualization mode.
45
    /// </summary>
NEW
46
    public ShadowVisualizationMode Mode { get; set; } = ShadowVisualizationMode.Cascades;
×
47

48
    /// <summary>
49
    /// Gets or sets the size of the visualization thumbnails (0.0 to 1.0, as fraction of screen height).
50
    /// </summary>
NEW
51
    public float ThumbnailSize { get; set; } = 0.2f;
×
52

53
    /// <summary>
54
    /// Gets or sets the padding between thumbnails (in pixels).
55
    /// </summary>
NEW
56
    public float Padding { get; set; } = 10f;
×
57

58
    /// <summary>
59
    /// Gets or sets whether to show cascade color tints for directional shadows.
60
    /// </summary>
NEW
61
    public bool ShowCascadeColors { get; set; } = true;
×
62

63
    /// <summary>
64
    /// Gets or sets the opacity of the visualization overlay (0.0 to 1.0).
65
    /// </summary>
NEW
66
    public float Opacity { get; set; } = 1.0f;
×
67

68
    /// <summary>
69
    /// Gets or sets the index of the specific shadow map to visualize.
70
    /// For cascades: 0-3, for point light faces: 0-5, for spot/point lights: light index.
71
    /// Set to -1 to show all available.
72
    /// </summary>
NEW
73
    public int SelectedIndex { get; set; } = -1;
×
74

75
    /// <summary>
76
    /// Gets or sets the corner where visualizations are displayed.
77
    /// </summary>
NEW
78
    public VisualizationCorner Corner { get; set; } = VisualizationCorner.BottomLeft;
×
79

80
    /// <summary>
81
    /// Gets or sets the viewport width in pixels.
82
    /// </summary>
83
    /// <remarks>
84
    /// This should be updated when the window/viewport resizes.
85
    /// </remarks>
NEW
86
    public int ViewportWidth { get; set; } = 1280;
×
87

88
    /// <summary>
89
    /// Gets or sets the viewport height in pixels.
90
    /// </summary>
91
    /// <remarks>
92
    /// This should be updated when the window/viewport resizes.
93
    /// </remarks>
NEW
94
    public int ViewportHeight { get; set; } = 720;
×
95

96
    /// <summary>
97
    /// Updates the viewport dimensions.
98
    /// </summary>
99
    /// <param name="width">The viewport width in pixels.</param>
100
    /// <param name="height">The viewport height in pixels.</param>
101
    public void SetViewportSize(int width, int height)
102
    {
NEW
103
        ViewportWidth = width;
×
NEW
104
        ViewportHeight = height;
×
NEW
105
    }
×
106

107
    /// <inheritdoc />
108
    public void Initialize(IWorld world)
109
    {
NEW
110
        this.world = world;
×
111

NEW
112
        if (!world.TryGetExtension<IGraphicsContext>(out graphics))
×
113
        {
NEW
114
            throw new InvalidOperationException("ShadowMapVisualizationSystem requires IGraphicsContext extension");
×
115
        }
NEW
116
    }
×
117

118
    /// <inheritdoc />
119
    public void Update(float deltaTime)
120
    {
NEW
121
        if (!Enabled || world is null || graphics is null || !graphics.IsInitialized || ShadowSystem?.ShadowManager is null)
×
122
        {
NEW
123
            return;
×
124
        }
125

126
        // Create shaders and quad mesh on first use
NEW
127
        if (!shadersCreated)
×
128
        {
NEW
129
            CreateResources();
×
NEW
130
            shadersCreated = true;
×
131
        }
132

NEW
133
        var shadowManager = ShadowSystem.ShadowManager;
×
134

NEW
135
        switch (Mode)
×
136
        {
137
            case ShadowVisualizationMode.Cascades:
NEW
138
                RenderCascadeVisualization(shadowManager);
×
NEW
139
                break;
×
140

141
            case ShadowVisualizationMode.SpotLights:
NEW
142
                RenderSpotLightVisualization(shadowManager);
×
NEW
143
                break;
×
144

145
            case ShadowVisualizationMode.PointLights:
NEW
146
                RenderPointLightVisualization(shadowManager);
×
NEW
147
                break;
×
148

149
            case ShadowVisualizationMode.All:
NEW
150
                RenderCascadeVisualization(shadowManager);
×
NEW
151
                RenderSpotLightVisualization(shadowManager);
×
NEW
152
                RenderPointLightVisualization(shadowManager);
×
153
                break;
154
        }
NEW
155
    }
×
156

157
    private void CreateResources()
158
    {
159
        // Fullscreen quad vertex shader
160
        const string quadVertexShader = """
161
            #version 330 core
162

163
            layout (location = 0) in vec3 aPosition;
164
            layout (location = 2) in vec2 aTexCoord;
165

166
            uniform mat4 uTransform;
167

168
            out vec2 vTexCoord;
169

170
            void main()
171
            {
172
                gl_Position = uTransform * vec4(aPosition, 1.0);
173
                vTexCoord = aTexCoord;
174
            }
175
            """;
176

177
        // 2D depth visualization fragment shader
178
        const string depthFragmentShader = """
179
            #version 330 core
180

181
            in vec2 vTexCoord;
182

183
            uniform sampler2D uDepthTexture;
184
            uniform vec3 uTintColor;
185
            uniform float uOpacity;
186

187
            out vec4 FragColor;
188

189
            void main()
190
            {
191
                float depth = texture(uDepthTexture, vTexCoord).r;
192

193
                // Apply tint color
194
                vec3 color = vec3(depth) * uTintColor;
195
                FragColor = vec4(color, uOpacity);
196
            }
197
            """;
198

199
        // Cubemap depth visualization fragment shader
200
        const string cubemapFragmentShader = """
201
            #version 330 core
202

203
            in vec2 vTexCoord;
204

205
            uniform samplerCube uDepthCubemap;
206
            uniform int uFaceIndex;
207
            uniform vec3 uTintColor;
208
            uniform float uOpacity;
209

210
            out vec4 FragColor;
211

212
            vec3 getFaceDirection(int face, vec2 uv)
213
            {
214
                vec2 coord = uv * 2.0 - 1.0;
215

216
                if (face == 0) return vec3( 1.0, -coord.y, -coord.x); // +X
217
                if (face == 1) return vec3(-1.0, -coord.y,  coord.x); // -X
218
                if (face == 2) return vec3( coord.x,  1.0,  coord.y); // +Y
219
                if (face == 3) return vec3( coord.x, -1.0, -coord.y); // -Y
220
                if (face == 4) return vec3( coord.x, -coord.y,  1.0); // +Z
221
                return vec3(-coord.x, -coord.y, -1.0);                // -Z
222
            }
223

224
            void main()
225
            {
226
                vec3 dir = getFaceDirection(uFaceIndex, vTexCoord);
227
                float depth = texture(uDepthCubemap, dir).r;
228

229
                vec3 color = vec3(depth) * uTintColor;
230
                FragColor = vec4(color, uOpacity);
231
            }
232
            """;
233

NEW
234
        depthVisualizationShader = graphics!.CreateShader(quadVertexShader, depthFragmentShader);
×
NEW
235
        cubemapVisualizationShader = graphics.CreateShader(quadVertexShader, cubemapFragmentShader);
×
236

237
        // Create a simple quad mesh (2x2, will be scaled by transform)
NEW
238
        quadMesh = graphics.CreateQuad(2f, 2f);
×
NEW
239
    }
×
240

241
    private void RenderCascadeVisualization(ShadowMapManager shadowManager)
242
    {
NEW
243
        var directionalLights = shadowManager.DirectionalShadowLights.ToList();
×
NEW
244
        if (directionalLights.Count == 0)
×
245
        {
NEW
246
            return;
×
247
        }
248

249
        // Get the first directional light's shadow data
NEW
250
        var shadowData = shadowManager.GetDirectionalShadowData(directionalLights[0]);
×
NEW
251
        if (!shadowData.HasValue)
×
252
        {
NEW
253
            return;
×
254
        }
255

NEW
256
        var data = shadowData.Value;
×
NEW
257
        int cascadeCount = data.Settings.ClampedCascadeCount;
×
258

259
        // Cascade colors: red, green, blue, yellow
NEW
260
        Vector3[] cascadeColors =
×
NEW
261
        [
×
NEW
262
            new Vector3(1.0f, 0.3f, 0.3f),
×
NEW
263
            new Vector3(0.3f, 1.0f, 0.3f),
×
NEW
264
            new Vector3(0.3f, 0.3f, 1.0f),
×
NEW
265
            new Vector3(1.0f, 1.0f, 0.3f)
×
NEW
266
        ];
×
267

NEW
268
        graphics!.BindShader(depthVisualizationShader);
×
NEW
269
        graphics.SetDepthTest(false);
×
NEW
270
        graphics.SetBlending(true);
×
271

NEW
272
        int startCascade = SelectedIndex >= 0 && SelectedIndex < cascadeCount ? SelectedIndex : 0;
×
NEW
273
        int endCascade = SelectedIndex >= 0 && SelectedIndex < cascadeCount ? SelectedIndex + 1 : cascadeCount;
×
274

NEW
275
        for (int i = startCascade; i < endCascade; i++)
×
276
        {
NEW
277
            var renderTarget = data.GetCascadeRenderTarget(i);
×
NEW
278
            if (!renderTarget.IsValid)
×
279
            {
280
                continue;
281
            }
282

NEW
283
            var depthTexture = graphics.GetRenderTargetDepthTexture(renderTarget);
×
NEW
284
            var transform = CalculateThumbnailTransform(i - startCascade, endCascade - startCascade);
×
285

NEW
286
            graphics.BindTexture(depthTexture, 0);
×
NEW
287
            graphics.SetUniform("uDepthTexture", 0);
×
NEW
288
            graphics.SetUniform("uTransform", transform);
×
NEW
289
            graphics.SetUniform("uTintColor", ShowCascadeColors ? cascadeColors[i] : Vector3.One);
×
NEW
290
            graphics.SetUniform("uOpacity", Opacity);
×
291

NEW
292
            graphics.DrawMesh(quadMesh);
×
293
        }
294

NEW
295
        graphics.SetBlending(false);
×
NEW
296
        graphics.SetDepthTest(true);
×
NEW
297
    }
×
298

299
    private void RenderSpotLightVisualization(ShadowMapManager shadowManager)
300
    {
NEW
301
        var spotLights = shadowManager.SpotShadowLights.ToList();
×
NEW
302
        if (spotLights.Count == 0)
×
303
        {
NEW
304
            return;
×
305
        }
306

NEW
307
        graphics!.BindShader(depthVisualizationShader);
×
NEW
308
        graphics.SetDepthTest(false);
×
NEW
309
        graphics.SetBlending(true);
×
310

NEW
311
        int startIndex = SelectedIndex >= 0 && SelectedIndex < spotLights.Count ? SelectedIndex : 0;
×
NEW
312
        int endIndex = SelectedIndex >= 0 && SelectedIndex < spotLights.Count ? SelectedIndex + 1 : spotLights.Count;
×
NEW
313
        int offset = Mode == ShadowVisualizationMode.All ? 4 : 0; // Offset if showing cascades first
×
314

NEW
315
        for (int i = startIndex; i < endIndex; i++)
×
316
        {
NEW
317
            var shadowData = shadowManager.GetSpotShadowData(spotLights[i]);
×
NEW
318
            if (!shadowData.HasValue)
×
319
            {
320
                continue;
321
            }
322

NEW
323
            var data = shadowData.Value;
×
NEW
324
            var depthTexture = graphics.GetRenderTargetDepthTexture(data.RenderTarget);
×
NEW
325
            var transform = CalculateThumbnailTransform(i - startIndex + offset, endIndex - startIndex + offset);
×
326

NEW
327
            graphics.BindTexture(depthTexture, 0);
×
NEW
328
            graphics.SetUniform("uDepthTexture", 0);
×
NEW
329
            graphics.SetUniform("uTransform", transform);
×
NEW
330
            graphics.SetUniform("uTintColor", new Vector3(1.0f, 0.6f, 0.2f)); // Orange tint for spot lights
×
NEW
331
            graphics.SetUniform("uOpacity", Opacity);
×
332

NEW
333
            graphics.DrawMesh(quadMesh);
×
334
        }
335

NEW
336
        graphics.SetBlending(false);
×
NEW
337
        graphics.SetDepthTest(true);
×
NEW
338
    }
×
339

340
    private void RenderPointLightVisualization(ShadowMapManager shadowManager)
341
    {
NEW
342
        var pointLights = shadowManager.PointShadowLights.ToList();
×
NEW
343
        if (pointLights.Count == 0)
×
344
        {
NEW
345
            return;
×
346
        }
347

348
        // For point lights, show the first light's 6 cubemap faces
NEW
349
        var shadowData = shadowManager.GetPointShadowData(pointLights[0]);
×
NEW
350
        if (!shadowData.HasValue)
×
351
        {
NEW
352
            return;
×
353
        }
354

NEW
355
        var data = shadowData.Value;
×
NEW
356
        var cubemapTexture = graphics!.GetCubemapRenderTargetTexture(data.RenderTarget);
×
357

NEW
358
        graphics.BindShader(cubemapVisualizationShader);
×
NEW
359
        graphics.SetDepthTest(false);
×
NEW
360
        graphics.SetBlending(true);
×
361

362
        // Face colors: +X=red, -X=cyan, +Y=green, -Y=magenta, +Z=blue, -Z=yellow
NEW
363
        Vector3[] faceColors =
×
NEW
364
        [
×
NEW
365
            new Vector3(1.0f, 0.3f, 0.3f), // +X
×
NEW
366
            new Vector3(0.3f, 1.0f, 1.0f), // -X
×
NEW
367
            new Vector3(0.3f, 1.0f, 0.3f), // +Y
×
NEW
368
            new Vector3(1.0f, 0.3f, 1.0f), // -Y
×
NEW
369
            new Vector3(0.3f, 0.3f, 1.0f), // +Z
×
NEW
370
            new Vector3(1.0f, 1.0f, 0.3f)  // -Z
×
NEW
371
        ];
×
372

NEW
373
        int startFace = SelectedIndex >= 0 && SelectedIndex < 6 ? SelectedIndex : 0;
×
NEW
374
        int endFace = SelectedIndex >= 0 && SelectedIndex < 6 ? SelectedIndex + 1 : 6;
×
NEW
375
        int offset = Mode == ShadowVisualizationMode.All ? 8 : 0; // Offset if showing cascades + spots first
×
376

NEW
377
        for (int face = startFace; face < endFace; face++)
×
378
        {
NEW
379
            var transform = CalculateThumbnailTransform(face - startFace + offset, endFace - startFace + offset);
×
380

NEW
381
            graphics.BindTexture(cubemapTexture, 0);
×
NEW
382
            graphics.SetUniform("uDepthCubemap", 0);
×
NEW
383
            graphics.SetUniform("uFaceIndex", face);
×
NEW
384
            graphics.SetUniform("uTransform", transform);
×
NEW
385
            graphics.SetUniform("uTintColor", ShowCascadeColors ? faceColors[face] : Vector3.One);
×
NEW
386
            graphics.SetUniform("uOpacity", Opacity);
×
387

NEW
388
            graphics.DrawMesh(quadMesh);
×
389
        }
390

NEW
391
        graphics.SetBlending(false);
×
NEW
392
        graphics.SetDepthTest(true);
×
NEW
393
    }
×
394

395
    private Matrix4x4 CalculateThumbnailTransform(int index, int total)
396
    {
397
        // Use configured viewport size
NEW
398
        int viewportWidth = ViewportWidth;
×
NEW
399
        int viewportHeight = ViewportHeight;
×
400

NEW
401
        if (viewportWidth == 0 || viewportHeight == 0)
×
402
        {
NEW
403
            return Matrix4x4.Identity;
×
404
        }
405

406
        // Calculate thumbnail size in pixels
NEW
407
        float thumbSize = viewportHeight * ThumbnailSize;
×
NEW
408
        float padding = Padding;
×
409

410
        // Calculate position based on corner
411
        float x, y;
NEW
412
        switch (Corner)
×
413
        {
414
            case VisualizationCorner.TopLeft:
NEW
415
                x = padding + index * (thumbSize + padding);
×
NEW
416
                y = viewportHeight - thumbSize - padding;
×
NEW
417
                break;
×
418
            case VisualizationCorner.TopRight:
NEW
419
                x = viewportWidth - thumbSize - padding - index * (thumbSize + padding);
×
NEW
420
                y = viewportHeight - thumbSize - padding;
×
NEW
421
                break;
×
422
            case VisualizationCorner.BottomRight:
NEW
423
                x = viewportWidth - thumbSize - padding - index * (thumbSize + padding);
×
NEW
424
                y = padding;
×
NEW
425
                break;
×
426
            default: // BottomLeft and any future values
NEW
427
                x = padding + index * (thumbSize + padding);
×
NEW
428
                y = padding;
×
429
                break;
430
        }
431

432
        // Convert to NDC (-1 to 1)
NEW
433
        float ndcX = (x / viewportWidth) * 2.0f - 1.0f + (thumbSize / viewportWidth);
×
NEW
434
        float ndcY = (y / viewportHeight) * 2.0f - 1.0f + (thumbSize / viewportHeight);
×
NEW
435
        float scaleX = thumbSize / viewportWidth;
×
NEW
436
        float scaleY = thumbSize / viewportHeight;
×
437

438
        // Create transform matrix (scale then translate)
NEW
439
        return Matrix4x4.CreateScale(scaleX, scaleY, 1.0f) *
×
NEW
440
               Matrix4x4.CreateTranslation(ndcX, ndcY, 0.0f);
×
441
    }
442

443
    /// <inheritdoc />
444
    public void Dispose()
445
    {
NEW
446
        if (disposed)
×
447
        {
NEW
448
            return;
×
449
        }
450

NEW
451
        disposed = true;
×
452

NEW
453
        if (shadersCreated && graphics is not null)
×
454
        {
NEW
455
            graphics.DeleteShader(depthVisualizationShader);
×
NEW
456
            graphics.DeleteShader(cubemapVisualizationShader);
×
NEW
457
            graphics.DeleteMesh(quadMesh);
×
458
        }
NEW
459
    }
×
460
}
461

462
/// <summary>
463
/// Visualization modes for shadow map debugging.
464
/// </summary>
465
public enum ShadowVisualizationMode
466
{
467
    /// <summary>
468
    /// Show directional light cascade shadow maps.
469
    /// </summary>
470
    Cascades,
471

472
    /// <summary>
473
    /// Show spot light shadow maps.
474
    /// </summary>
475
    SpotLights,
476

477
    /// <summary>
478
    /// Show point light cubemap shadow maps.
479
    /// </summary>
480
    PointLights,
481

482
    /// <summary>
483
    /// Show all shadow maps.
484
    /// </summary>
485
    All
486
}
487

488
/// <summary>
489
/// Corner positions for visualization overlay.
490
/// </summary>
491
public enum VisualizationCorner
492
{
493
    /// <summary>
494
    /// Top-left corner of the screen.
495
    /// </summary>
496
    TopLeft,
497

498
    /// <summary>
499
    /// Top-right corner of the screen.
500
    /// </summary>
501
    TopRight,
502

503
    /// <summary>
504
    /// Bottom-left corner of the screen.
505
    /// </summary>
506
    BottomLeft,
507

508
    /// <summary>
509
    /// Bottom-right corner of the screen.
510
    /// </summary>
511
    BottomRight
512
}
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