Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
DirectSceneLayerDarknessOverlay.cs
1using System.Drawing;
6using SkiaSharp;
7
9
32{
33 private readonly List<RevealSource> _revealSources = [];
34 private readonly List<TrackedLightReveal> _trackedLightReveals = [];
35 private readonly List<TrackedLightLayerReveal> _trackedLightLayers = [];
36
37 private readonly SKPaint _darknessPaint = new()
38 {
39 IsAntialias = true,
40 BlendMode = SKBlendMode.SrcOver,
41 Style = SKPaintStyle.Fill
42 };
43
44 private Color _darknessColor = Color.Black;
45 private byte _darknessOpacity = 190;
46 private float _innerClearRadiusRatio = 0.20f;
47 private float _midpointRadiusRatio = 0.62f;
48 private float _midpointStrength = 0.45f;
49
58 SceneLayer sceneLayer,
59 Rectangle worldBounds,
60 string? nickname = null)
61 : base(renderSurfaceHost,
63 sceneLayer,
64 view: null,
65 screenBounds: null,
66 worldBounds: worldBounds,
67 nickname: nickname)
68 {
69 ZOrder = 20_000;
70 RebuildDarknessPaint();
71 }
72
76 public IReadOnlyList<RevealSource> RevealSources => _revealSources;
77
81 public Rectangle DarknessWorldBounds
82 {
83 get => WorldBounds;
84 set => WorldBounds = value;
85 }
86
90 public Color DarknessColor
91 {
92 get => _darknessColor;
93 set
94 {
95 if (_darknessColor == value)
96 return;
97
98 _darknessColor = value;
99 RebuildDarknessPaint();
100 ForceRefresh();
101 }
102 }
103
107 public byte DarknessOpacity
108 {
109 get => _darknessOpacity;
110 set
111 {
112 if (_darknessOpacity == value)
113 return;
114
115 _darknessOpacity = value;
116 RebuildDarknessPaint();
117 ForceRefresh();
118 }
119 }
120
125 {
126 get => _innerClearRadiusRatio;
127 set
128 {
129 float clamped = Math.Clamp(value, 0f, 1f);
130 if (Math.Abs(_innerClearRadiusRatio - clamped) < 0.0001f)
131 return;
132
133 _innerClearRadiusRatio = clamped;
134 if (_midpointRadiusRatio < _innerClearRadiusRatio)
135 _midpointRadiusRatio = _innerClearRadiusRatio;
136
137 ForceRefresh();
138 }
139 }
140
145 {
146 get => _midpointRadiusRatio;
147 set
148 {
149 float clamped = Math.Clamp(value, 0f, 1f);
150 clamped = Math.Max(clamped, _innerClearRadiusRatio);
151
152 if (Math.Abs(_midpointRadiusRatio - clamped) < 0.0001f)
153 return;
154
155 _midpointRadiusRatio = clamped;
156 ForceRefresh();
157 }
158 }
159
163 public float MidpointStrength
164 {
165 get => _midpointStrength;
166 set
167 {
168 float clamped = Math.Clamp(value, 0f, 1f);
169 if (Math.Abs(_midpointStrength - clamped) < 0.0001f)
170 return;
171
172 _midpointStrength = clamped;
173 ForceRefresh();
174 }
175 }
176
184 public RevealSource AddRevealSource(PointF centerWorldPx, float radiusWorldPx, string? nickname = null)
185 {
186 var source = new RevealSource(this, centerWorldPx, radiusWorldPx, nickname);
187 _revealSources.Add(source);
188 ForceRefresh();
189 return source;
190 }
191
208 float radiusScale = 1f,
209 float intensityScale = 1f,
210 bool trackIntensity = true,
211 string? nickname = null)
212 {
213 if (light is null)
214 throw new ArgumentNullException(nameof(light));
215
216 EnsureSameSceneLayer(light);
217
218 if (_trackedLightReveals.Any(t => ReferenceEquals(t.Light, light)))
219 return _trackedLightReveals.First(t => ReferenceEquals(t.Light, light)).Source;
220
221 var source = new RevealSource(this, light.CenterWorldPx, light.RadiusWorldPx * Math.Max(0f, radiusScale), nickname ?? light.Nickname);
222 _revealSources.Add(source);
223
224 var tracked = new TrackedLightReveal(light, source, Math.Max(0f, radiusScale), Math.Max(0f, intensityScale), trackIntensity);
225 _trackedLightReveals.Add(tracked);
226
227 light.Changed += OnTrackedLightChanged;
228 light.Disposing += OnTrackedLightDisposing;
229
230 SyncTrackedLight(tracked);
231 ForceRefresh();
232 return source;
233 }
234
246 public void TrackLightLayer(DirectLightLayer lightLayer,
247 float radiusScale = 1f,
248 float intensityScale = 1f,
249 bool trackIntensity = true)
250 {
251 if (lightLayer is null)
252 throw new ArgumentNullException(nameof(lightLayer));
253
254 if (!ReferenceEquals(lightLayer.SceneLayer, SceneLayer))
255 throw new ArgumentException("SceneLayer-bound darkness can only track a DirectLightLayer attached to the same SceneLayer.", nameof(lightLayer));
256
257 if (_trackedLightLayers.Any(t => ReferenceEquals(t.LightLayer, lightLayer)))
258 return;
259
260 var trackedLayer = new TrackedLightLayerReveal(
261 lightLayer,
262 Math.Max(0f, radiusScale),
263 Math.Max(0f, intensityScale),
264 trackIntensity);
265
266 _trackedLightLayers.Add(trackedLayer);
267
268 foreach (var light in lightLayer.Lights)
269 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
270
271 lightLayer.LightAdded += OnTrackedLayerLightAdded;
272 lightLayer.LightRemoving += OnTrackedLayerLightRemoving;
273 }
274
281 {
282 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Light, light));
283 if (tracked is null)
284 return false;
285
286 UntrackLight(tracked, removeRevealSource: true);
287 return true;
288 }
289
296 {
297 if (source is null)
298 return false;
299
300 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Source, source));
301 if (tracked is not null)
302 UntrackLight(tracked, removeRevealSource: false);
303
304 bool removed = _revealSources.Remove(source);
305 if (removed)
306 ForceRefresh();
307
308 return removed;
309 }
310
314 public void ClearRevealSources()
315 {
316 if (_revealSources.Count == 0 && _trackedLightReveals.Count == 0 && _trackedLightLayers.Count == 0)
317 return;
318
319 foreach (var trackedLayer in _trackedLightLayers.ToArray())
320 {
321 trackedLayer.LightLayer.LightAdded -= OnTrackedLayerLightAdded;
322 trackedLayer.LightLayer.LightRemoving -= OnTrackedLayerLightRemoving;
323 }
324
325 _trackedLightLayers.Clear();
326
327 foreach (var tracked in _trackedLightReveals.ToArray())
328 UntrackLight(tracked, removeRevealSource: false);
329
330 _revealSources.Clear();
331 ForceRefresh();
332 }
333
340 {
341 DarknessWorldBounds = worldBounds;
342 return this;
343 }
344
351 {
352 DarknessColor = color;
353 return this;
354 }
355
362 {
363 DarknessOpacity = opacity;
364 return this;
365 }
366
373 {
374 InnerClearRadiusRatio = ratio;
375 return this;
376 }
377
384 {
385 MidpointRadiusRatio = ratio;
386 return this;
387 }
388
395 {
396 MidpointStrength = strength;
397 return this;
398 }
399
401 protected override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
402 {
403 if (destRectScreen.Width <= 0f || destRectScreen.Height <= 0f || WorldBounds.Width <= 0 || WorldBounds.Height <= 0)
404 return;
405
406 var canvas = backbuffer.Canvas;
407 var destRect = destRectScreen.ToSKRect();
408
409 // Work in an isolated layer so DstOut reveal holes affect only this overlay,
410 // not the already-rendered scene content beneath it.
411 canvas.SaveLayer(destRect, null);
412
413 canvas.DrawRect(destRect, _darknessPaint);
414
415 for (int i = 0; i < _revealSources.Count; i++)
416 {
417 var source = _revealSources[i];
418 if (!source.Enabled || source.RadiusWorldPx <= 0.01f || source.Intensity <= 0.001f)
419 continue;
420
421 var centerScreen = WorldPointToDrawRect(source.CenterWorldPx, destRectScreen);
422 float radiusScreen = WorldRadiusToDrawRadius(source.RadiusWorldPx, destRectScreen);
423
424 if (radiusScreen <= 0.01f)
425 continue;
426
427 using var revealShader = BuildRevealShader(centerScreen, radiusScreen, source.Intensity);
428 using var revealPaint = new SKPaint
429 {
430 IsAntialias = true,
431 BlendMode = SKBlendMode.DstOut,
432 Shader = revealShader,
433 Style = SKPaintStyle.Fill
434 };
435
436 canvas.DrawRect(destRect, revealPaint);
437 }
438
439 canvas.Restore();
440 }
441
442 internal void RefreshFromSourceChange()
443 {
444 ForceRefresh();
445 }
446
447 private void OnTrackedLightChanged(DirectRadialLight light)
448 {
449 foreach (var tracked in _trackedLightReveals.Where(t => ReferenceEquals(t.Light, light)).ToArray())
450 SyncTrackedLight(tracked);
451 }
452
453 private void OnTrackedLightDisposing(object? sender, IDirectDrawable drawing)
454 {
455 if (drawing is DirectRadialLight light)
456 UntrackLight(light);
457 }
458
459 private void OnTrackedLayerLightAdded(DirectRadialLight light)
460 {
461 if (!ReferenceEquals(light.SceneLayer, SceneLayer))
462 return;
463
464 foreach (var trackedLayer in _trackedLightLayers.Where(t => t.LightLayer.Lights.Contains(light)).ToArray())
465 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
466 }
467
468 private void OnTrackedLayerLightRemoving(DirectRadialLight light)
469 {
470 UntrackLight(light);
471 }
472
473 private void SyncTrackedLight(TrackedLightReveal tracked)
474 {
475 var light = tracked.Light;
476 float intensity = tracked.TrackIntensity
477 ? light.EffectiveIntensity * tracked.IntensityScale
478 : tracked.IntensityScale;
479
480 tracked.Source.SyncFromTrackedLight(
481 light.CenterWorldPx,
482 light.RadiusWorldPx * tracked.RadiusScale,
483 intensity);
484 }
485
486 private void UntrackLight(TrackedLightReveal tracked, bool removeRevealSource)
487 {
488 tracked.Light.Changed -= OnTrackedLightChanged;
489 tracked.Light.Disposing -= OnTrackedLightDisposing;
490 _trackedLightReveals.Remove(tracked);
491
492 if (removeRevealSource)
493 _revealSources.Remove(tracked.Source);
494
495 ForceRefresh();
496 }
497
498 private void EnsureSameSceneLayer(DirectRadialLight light)
499 {
500 if (!ReferenceEquals(light.SceneLayer, SceneLayer))
501 throw new ArgumentException("SceneLayer-bound darkness can only track lights attached to the same SceneLayer.", nameof(light));
502 }
503
504 private PointF WorldPointToDrawRect(PointF worldPx, RectangleF destRectScreen)
505 {
506 float scaleX = destRectScreen.Width / WorldBounds.Width;
507 float scaleY = destRectScreen.Height / WorldBounds.Height;
508
509 return new PointF(
510 destRectScreen.Left + (worldPx.X - WorldBounds.Left) * scaleX,
511 destRectScreen.Top + (worldPx.Y - WorldBounds.Top) * scaleY);
512 }
513
514 private float WorldRadiusToDrawRadius(float radiusWorldPx, RectangleF destRectScreen)
515 {
516 float scaleX = destRectScreen.Width / WorldBounds.Width;
517 float scaleY = destRectScreen.Height / WorldBounds.Height;
518
519 return Math.Max(Math.Abs(radiusWorldPx * scaleX), Math.Abs(radiusWorldPx * scaleY));
520 }
521
522 private void RebuildDarknessPaint()
523 {
524 _darknessPaint.Color = Color.FromArgb(_darknessOpacity, _darknessColor).ToSKColor();
525 }
526
527 private SKShader BuildRevealShader(PointF centerScreenPx, float radiusScreenPx, float intensity)
528 {
529 byte centerAlpha = (byte)Math.Clamp((int)Math.Round(255f * Math.Clamp(intensity, 0f, 1f)), 0, 255);
530 byte midAlpha = (byte)Math.Clamp((int)Math.Round(255f * Math.Clamp(intensity, 0f, 1f) * _midpointStrength), 0, 255);
531
532 return SKShader.CreateRadialGradient(
533 new SKPoint(centerScreenPx.X, centerScreenPx.Y),
534 radiusScreenPx,
535 colors:
536 [
537 new SKColor(255, 255, 255, centerAlpha),
538 new SKColor(255, 255, 255, centerAlpha),
539 new SKColor(255, 255, 255, midAlpha),
540 new SKColor(255, 255, 255, 0)
541 ],
542 colorPos:
543 [
544 0f,
545 _innerClearRadiusRatio,
546 _midpointRadiusRatio,
547 1f
548 ],
549 mode: SKShaderTileMode.Clamp);
550 }
551
553 protected override void Dispose(bool disposing)
554 {
555 if (disposing)
556 {
558 _darknessPaint.Dispose();
559 }
560
561 base.Dispose(disposing);
562 }
563
564 private sealed class TrackedLightReveal(
565 DirectRadialLight light,
566 RevealSource source,
567 float radiusScale,
568 float intensityScale,
569 bool trackIntensity)
570 {
574 public DirectRadialLight Light { get; } = light;
578 public RevealSource Source { get; } = source;
582 public float RadiusScale { get; } = radiusScale;
586 public float IntensityScale { get; } = intensityScale;
590 public bool TrackIntensity { get; } = trackIntensity;
591 }
592
593 private sealed class TrackedLightLayerReveal(
594 DirectLightLayer lightLayer,
595 float radiusScale,
596 float intensityScale,
597 bool trackIntensity)
598 {
602 public DirectLightLayer LightLayer { get; } = lightLayer;
606 public float RadiusScale { get; } = radiusScale;
610 public float IntensityScale { get; } = intensityScale;
614 public bool TrackIntensity { get; } = trackIntensity;
615 }
616
620 public sealed class RevealSource
621 {
622 private readonly DirectSceneLayerDarknessOverlay _owner;
623 private PointF _centerWorldPx;
624 private float _radiusWorldPx;
625 private float _intensity = 1f;
626 private bool _enabled = true;
627
628 internal RevealSource(DirectSceneLayerDarknessOverlay owner, PointF centerWorldPx, float radiusWorldPx, string? nickname)
629 {
630 _owner = owner;
631 _centerWorldPx = centerWorldPx;
632 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
633 Nickname = nickname;
634 }
635
639 public string? Nickname { get; }
640
644 public bool Enabled
645 {
646 get => _enabled;
647 set
648 {
649 if (_enabled == value)
650 return;
651
652 _enabled = value;
653 _owner.RefreshFromSourceChange();
654 }
655 }
656
660 public PointF CenterWorldPx
661 {
662 get => _centerWorldPx;
663 set
664 {
665 if (_centerWorldPx == value)
666 return;
667
668 _centerWorldPx = value;
669 _owner.RefreshFromSourceChange();
670 }
671 }
672
676 public float RadiusWorldPx
677 {
678 get => _radiusWorldPx;
679 set
680 {
681 float clamped = Math.Max(0f, value);
682 if (Math.Abs(_radiusWorldPx - clamped) < 0.0001f)
683 return;
684
685 _radiusWorldPx = clamped;
686 _owner.RefreshFromSourceChange();
687 }
688 }
689
693 public float Intensity
694 {
695 get => _intensity;
696 set
697 {
698 float clamped = Math.Clamp(value, 0f, 1f);
699 if (Math.Abs(_intensity - clamped) < 0.0001f)
700 return;
701
702 _intensity = clamped;
703 _owner.RefreshFromSourceChange();
704 }
705 }
706
711 public void MoveTo(PointF centerWorldPx)
712 {
713 CenterWorldPx = centerWorldPx;
714 }
715
720 public void SetRadius(float radiusWorldPx)
721 {
722 RadiusWorldPx = radiusWorldPx;
723 }
724
725 internal void SyncFromTrackedLight(PointF centerWorldPx, float radiusWorldPx, float intensity)
726 {
727 bool changed =
728 _centerWorldPx != centerWorldPx ||
729 Math.Abs(_radiusWorldPx - radiusWorldPx) >= 0.0001f ||
730 Math.Abs(_intensity - intensity) >= 0.0001f;
731
732 if (!changed)
733 return;
734
735 _centerWorldPx = centerWorldPx;
736 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
737 _intensity = Math.Clamp(intensity, 0f, 1f);
738 _owner.RefreshFromSourceChange();
739 }
740 }
741}
int ZOrder
Gets or sets the Z-order (depth) used for sorting this direct drawing relative to other drawables.
string? Nickname
Gets the optional human-readable name for this direct drawing.
SceneLayer? SceneLayer
Gets the scene layer to which this direct drawing is attached, or null if the drawing is in view mode...
Rectangle WorldBounds
Gets or sets the world-space bounds of this direct drawing in pixels.
DirectDrawingBase(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, SceneLayer? sceneLayer, View? view, Rectangle? screenBounds, Rectangle? worldBounds, string? nickname=null)
Initializes a new instance of the DirectDrawingBase class.
Convenience owner for a group of scene-layer radial lights.
SceneLayer SceneLayer
Gets the scene layer where lights are drawn.
Draws a bounded, radial light effect as a scene-layer direct drawing.
float RadiusWorldPx
Gets the light radius in world pixels.
PointF CenterWorldPx
Gets the light center in world pixels.
Represents a single world-space reveal source used by a DirectSceneLayerDarknessOverlay.
PointF CenterWorldPx
Gets or sets the reveal center in world-space pixels.
bool Enabled
Gets or sets whether this reveal source is currently active.
string? Nickname
Optional human-readable name for debugging and lookup.
void MoveTo(PointF centerWorldPx)
Moves the reveal source to a new world-space position.
void SetRadius(float radiusWorldPx)
Sets the reveal radius in world-space pixels.
float RadiusWorldPx
Gets or sets the reveal radius in world-space pixels.
DirectSceneLayerDarknessOverlay(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle worldBounds, string? nickname=null)
Initializes a new scene-layer darkness/fog overlay.
RevealSource TrackLight(DirectRadialLight light, float radiusScale=1f, float intensityScale=1f, bool trackIntensity=true, string? nickname=null)
Adds a reveal source that automatically tracks a DirectRadialLight on this same SceneLayer.
DirectSceneLayerDarknessOverlay SetMidpointStrength(float strength)
Fluent helper to set the midpoint reveal strength.
bool RemoveRevealSource(RevealSource source)
Removes a reveal source from this overlay.
byte DarknessOpacity
Gets or sets the opacity of the darkness overlay.
void ClearRevealSources()
Removes all reveal sources and stops all automatic tracking.
Rectangle DarknessWorldBounds
Gets or sets the world-space region affected by the darkness/fog.
float MidpointStrength
Gets or sets the removal strength at the midpoint stop.
float InnerClearRadiusRatio
Gets or sets the radius ratio that remains fully revealed before falloff begins.
IReadOnlyList< RevealSource > RevealSources
Gets a live, ordered list of reveal sources used by this overlay.
void TrackLightLayer(DirectLightLayer lightLayer, float radiusScale=1f, float intensityScale=1f, bool trackIntensity=true)
Tracks every current and future light in a DirectLightLayer attached to this same SceneLayer.
override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
DirectSceneLayerDarknessOverlay SetDarknessWorldBounds(Rectangle worldBounds)
Fluent helper to set the darkness/fog world bounds.
float MidpointRadiusRatio
Gets or sets the radius ratio of the midpoint alpha stop used for reveal falloff.
DirectSceneLayerDarknessOverlay SetDarknessColor(Color color)
Fluent helper to set the darkness tint.
DirectSceneLayerDarknessOverlay SetDarknessOpacity(byte opacity)
Fluent helper to set the darkness opacity.
Color DarknessColor
Gets or sets the tint color of the overlay.
override void Dispose(bool disposing)
Releases resources used by this DirectDrawingBase instance.When disposing is true,...
DirectSceneLayerDarknessOverlay SetInnerClearRadiusRatio(float ratio)
Fluent helper to set the fully-revealed radius ratio.
bool UntrackLight(DirectRadialLight light)
Stops tracking a specific light and removes its reveal source.
RevealSource AddRevealSource(PointF centerWorldPx, float radiusWorldPx, string? nickname=null)
Adds a new manually controlled reveal source in world-space.
DirectSceneLayerDarknessOverlay SetMidpointRadiusRatio(float ratio)
Fluent helper to set the midpoint radius ratio.
Represents a base class for managing a graphical backbuffer, and is the in-memory surface where rende...
SKCanvas Canvas
Gets the SkiaSharp canvas used for drawing operations on this backbuffer.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
Represents a single layer within a Scene, containing a grid of tiles and supporting various coordinat...
Definition SceneLayer.cs:40
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.