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
DirectDarknessOverlay.cs
1using System.Drawing;
7using SkiaSharp;
8
10
53{
54 private readonly List<RevealSource> _revealSources = [];
55 private readonly List<TrackedLightReveal> _trackedLightReveals = [];
56 private readonly List<TrackedLightLayerReveal> _trackedLightLayers = [];
57
58 private readonly SKPaint _darknessPaint = new()
59 {
60 IsAntialias = true,
61 BlendMode = SKBlendMode.SrcOver,
62 Style = SKPaintStyle.Fill
63 };
64
65 private Color _darknessColor = Color.Black;
66 private byte _darknessOpacity = 190;
67 private float _innerClearRadiusRatio = 0.20f;
68 private float _midpointRadiusRatio = 0.62f;
69 private float _midpointStrength = 0.45f;
70
82 View view,
83 SceneLayer projectionLayer,
84 string? nickname = null)
85 : base(renderSurfaceHost,
87 sceneLayer: null,
88 view: view,
89 screenBounds: view.Viewport.TargetRectPx,
90 worldBounds: null,
91 nickname: nickname)
92 {
93 ProjectionLayer = projectionLayer ?? throw new ArgumentNullException(nameof(projectionLayer));
94 ZOrder = 20_000;
95 RebuildDarknessPaint();
96 }
97
102
106 public IReadOnlyList<RevealSource> RevealSources => _revealSources;
107
111 public Color DarknessColor
112 {
113 get => _darknessColor;
114 set
115 {
116 if (_darknessColor == value)
117 return;
118
119 _darknessColor = value;
120 RebuildDarknessPaint();
121 ForceRefresh();
122 }
123 }
124
128 public byte DarknessOpacity
129 {
130 get => _darknessOpacity;
131 set
132 {
133 if (_darknessOpacity == value)
134 return;
135
136 _darknessOpacity = value;
137 RebuildDarknessPaint();
138 ForceRefresh();
139 }
140 }
141
150 {
151 get => _innerClearRadiusRatio;
152 set
153 {
154 float clamped = Math.Clamp(value, 0f, 1f);
155 if (Math.Abs(_innerClearRadiusRatio - clamped) < 0.0001f)
156 return;
157
158 _innerClearRadiusRatio = clamped;
159 if (_midpointRadiusRatio < _innerClearRadiusRatio)
160 _midpointRadiusRatio = _innerClearRadiusRatio;
161
162 ForceRefresh();
163 }
164 }
165
170 {
171 get => _midpointRadiusRatio;
172 set
173 {
174 float clamped = Math.Clamp(value, 0f, 1f);
175 clamped = Math.Max(clamped, _innerClearRadiusRatio);
176
177 if (Math.Abs(_midpointRadiusRatio - clamped) < 0.0001f)
178 return;
179
180 _midpointRadiusRatio = clamped;
181 ForceRefresh();
182 }
183 }
184
191 public float MidpointStrength
192 {
193 get => _midpointStrength;
194 set
195 {
196 float clamped = Math.Clamp(value, 0f, 1f);
197 if (Math.Abs(_midpointStrength - clamped) < 0.0001f)
198 return;
199
200 _midpointStrength = clamped;
201 ForceRefresh();
202 }
203 }
204
212 public RevealSource AddRevealSource(PointF centerWorldPx, float radiusWorldPx, string? nickname = null)
213 {
214 var source = new RevealSource(this, centerWorldPx, radiusWorldPx, nickname);
215 _revealSources.Add(source);
216 ForceRefresh();
217 return source;
218 }
219
240 float radiusScale = 1f,
241 float intensityScale = 1f,
242 bool trackIntensity = true,
243 string? nickname = null)
244 {
245 if (light is null)
246 throw new ArgumentNullException(nameof(light));
247
248 if (_trackedLightReveals.Any(t => ReferenceEquals(t.Light, light)))
249 return _trackedLightReveals.First(t => ReferenceEquals(t.Light, light)).Source;
250
251 var source = new RevealSource(this, light.CenterWorldPx, light.RadiusWorldPx * Math.Max(0f, radiusScale), nickname ?? light.Nickname);
252 _revealSources.Add(source);
253
254 var tracked = new TrackedLightReveal(light, source, Math.Max(0f, radiusScale), Math.Max(0f, intensityScale), trackIntensity);
255 _trackedLightReveals.Add(tracked);
256
257 light.Changed += OnTrackedLightChanged;
258 light.Disposing += OnTrackedLightDisposing;
259
260 SyncTrackedLight(tracked);
261 ForceRefresh();
262 return source;
263 }
264
282 public void TrackLightLayer(DirectLightLayer lightLayer,
283 float radiusScale = 1f,
284 float intensityScale = 1f,
285 bool trackIntensity = true)
286 {
287 if (lightLayer is null)
288 throw new ArgumentNullException(nameof(lightLayer));
289
290 if (_trackedLightLayers.Any(t => ReferenceEquals(t.LightLayer, lightLayer)))
291 return;
292
293 var trackedLayer = new TrackedLightLayerReveal(
294 lightLayer,
295 Math.Max(0f, radiusScale),
296 Math.Max(0f, intensityScale),
297 trackIntensity);
298
299 _trackedLightLayers.Add(trackedLayer);
300
301 foreach (var light in lightLayer.Lights)
302 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
303
304 lightLayer.LightAdded += OnTrackedLayerLightAdded;
305 lightLayer.LightRemoving += OnTrackedLayerLightRemoving;
306 }
307
314 {
315 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Light, light));
316 if (tracked is null)
317 return false;
318
319 UntrackLight(tracked, removeRevealSource: true);
320 return true;
321 }
322
329 {
330 if (source is null)
331 return false;
332
333 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Source, source));
334 if (tracked is not null)
335 UntrackLight(tracked, removeRevealSource: false);
336
337 bool removed = _revealSources.Remove(source);
338 if (removed)
339 ForceRefresh();
340
341 return removed;
342 }
343
347 public void ClearRevealSources()
348 {
349 if (_revealSources.Count == 0 && _trackedLightReveals.Count == 0 && _trackedLightLayers.Count == 0)
350 return;
351
352 foreach (var trackedLayer in _trackedLightLayers.ToArray())
353 {
354 trackedLayer.LightLayer.LightAdded -= OnTrackedLayerLightAdded;
355 trackedLayer.LightLayer.LightRemoving -= OnTrackedLayerLightRemoving;
356 }
357
358 _trackedLightLayers.Clear();
359
360 foreach (var tracked in _trackedLightReveals.ToArray())
361 UntrackLight(tracked, removeRevealSource: false);
362
363 _revealSources.Clear();
364 ForceRefresh();
365 }
366
373 {
374 DarknessColor = color;
375 return this;
376 }
377
384 {
385 DarknessOpacity = opacity;
386 return this;
387 }
388
395 {
396 InnerClearRadiusRatio = ratio;
397 return this;
398 }
399
406 {
407 MidpointRadiusRatio = ratio;
408 return this;
409 }
410
417 {
418 MidpointStrength = strength;
419 return this;
420 }
421
423 public override void Update(long tick)
424 {
425 // Keep the overlay pinned to the current viewport bounds.
426 var targetRect = View!.Viewport.TargetRectPx;
427 if (ScreenBounds != targetRect)
428 ScreenBounds = targetRect;
429
430 base.Update(tick);
431 }
432
434 protected override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
435 {
436 var canvas = backbuffer.Canvas;
437 var destRect = destRectScreen.ToSKRect();
438
439 // Work in an isolated layer so "holes" reveal the already-rendered scene beneath,
440 // rather than clearing the backbuffer itself.
441 canvas.SaveLayer(destRect, null);
442
443 canvas.DrawRect(destRect, _darknessPaint);
444
445 for (int i = 0; i < _revealSources.Count; i++)
446 {
447 var source = _revealSources[i];
448 if (!source.Enabled || source.RadiusWorldPx <= 0.01f || source.Intensity <= 0.001f)
449 continue;
450
451 var centerScreen = View!.WorldPxToScreenPx(ProjectionLayer, source.CenterWorldPx);
452
453 var radiusProbeWorld = new PointF(source.CenterWorldPx.X + source.RadiusWorldPx, source.CenterWorldPx.Y);
454 var radiusProbeScreen = View.WorldPxToScreenPx(ProjectionLayer, radiusProbeWorld);
455 float screenRadius = Math.Abs(radiusProbeScreen.X - centerScreen.X);
456
457 if (screenRadius <= 0.01f)
458 continue;
459
460 using var revealShader = BuildRevealShader(centerScreen, screenRadius, source.Intensity);
461 using var revealPaint = new SKPaint
462 {
463 IsAntialias = true,
464 BlendMode = SKBlendMode.DstOut,
465 Shader = revealShader,
466 Style = SKPaintStyle.Fill
467 };
468
469 canvas.DrawRect(destRect, revealPaint);
470 }
471
472 canvas.Restore();
473 }
474
475 internal void RefreshFromSourceChange()
476 {
477 ForceRefresh();
478 }
479
480 private void OnTrackedLightChanged(DirectRadialLight light)
481 {
482 foreach (var tracked in _trackedLightReveals.Where(t => ReferenceEquals(t.Light, light)).ToArray())
483 SyncTrackedLight(tracked);
484 }
485
486 private void OnTrackedLightDisposing(object? sender, IDirectDrawable drawing)
487 {
488 if (drawing is DirectRadialLight light)
489 UntrackLight(light);
490 }
491
492 private void OnTrackedLayerLightAdded(DirectRadialLight light)
493 {
494 foreach (var trackedLayer in _trackedLightLayers.Where(t => t.LightLayer.Lights.Contains(light)).ToArray())
495 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
496 }
497
498 private void OnTrackedLayerLightRemoving(DirectRadialLight light)
499 {
500 UntrackLight(light);
501 }
502
503 private void SyncTrackedLight(TrackedLightReveal tracked)
504 {
505 var light = tracked.Light;
506 float intensity = tracked.TrackIntensity
507 ? light.EffectiveIntensity * tracked.IntensityScale
508 : tracked.IntensityScale;
509
510 tracked.Source.SyncFromTrackedLight(
511 light.CenterWorldPx,
512 light.RadiusWorldPx * tracked.RadiusScale,
513 intensity);
514 }
515
516 private void UntrackLight(TrackedLightReveal tracked, bool removeRevealSource)
517 {
518 tracked.Light.Changed -= OnTrackedLightChanged;
519 tracked.Light.Disposing -= OnTrackedLightDisposing;
520 _trackedLightReveals.Remove(tracked);
521
522 if (removeRevealSource)
523 _revealSources.Remove(tracked.Source);
524
525 ForceRefresh();
526 }
527
528 private void RebuildDarknessPaint()
529 {
530 _darknessPaint.Color = Color.FromArgb(_darknessOpacity, _darknessColor).ToSKColor();
531 }
532
533 private SKShader BuildRevealShader(PointF centerScreenPx, float radiusScreenPx, float intensity)
534 {
535 byte centerAlpha = (byte)Math.Clamp((int)Math.Round(255f * Math.Clamp(intensity, 0f, 1f)), 0, 255);
536 byte midAlpha = (byte)Math.Clamp((int)Math.Round(255f * Math.Clamp(intensity, 0f, 1f) * _midpointStrength), 0, 255);
537
538 return SKShader.CreateRadialGradient(
539 new SKPoint(centerScreenPx.X, centerScreenPx.Y),
540 radiusScreenPx,
541 colors:
542 [
543 new SKColor(255, 255, 255, centerAlpha),
544 new SKColor(255, 255, 255, centerAlpha),
545 new SKColor(255, 255, 255, midAlpha),
546 new SKColor(255, 255, 255, 0)
547 ],
548 colorPos:
549 [
550 0f,
551 _innerClearRadiusRatio,
552 _midpointRadiusRatio,
553 1f
554 ],
555 mode: SKShaderTileMode.Clamp);
556 }
557
559 protected override void Dispose(bool disposing)
560 {
561 if (disposing)
562 {
564 _darknessPaint.Dispose();
565 }
566
567 base.Dispose(disposing);
568 }
569
570 private sealed class TrackedLightReveal(
571 DirectRadialLight light,
572 RevealSource source,
573 float radiusScale,
574 float intensityScale,
575 bool trackIntensity)
576 {
580 public DirectRadialLight Light { get; } = light;
584 public RevealSource Source { get; } = source;
588 public float RadiusScale { get; } = radiusScale;
592 public float IntensityScale { get; } = intensityScale;
596 public bool TrackIntensity { get; } = trackIntensity;
597 }
598
599 private sealed class TrackedLightLayerReveal(
600 DirectLightLayer lightLayer,
601 float radiusScale,
602 float intensityScale,
603 bool trackIntensity)
604 {
608 public DirectLightLayer LightLayer { get; } = lightLayer;
612 public float RadiusScale { get; } = radiusScale;
616 public float IntensityScale { get; } = intensityScale;
620 public bool TrackIntensity { get; } = trackIntensity;
621 }
622
626 public sealed class RevealSource
627 {
628 private readonly DirectDarknessOverlay _owner;
629 private PointF _centerWorldPx;
630 private float _radiusWorldPx;
631 private float _intensity = 1f;
632 private bool _enabled = true;
633
634 internal RevealSource(DirectDarknessOverlay owner, PointF centerWorldPx, float radiusWorldPx, string? nickname)
635 {
636 _owner = owner;
637 _centerWorldPx = centerWorldPx;
638 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
639 Nickname = nickname;
640 }
641
645 public string? Nickname { get; }
646
650 public bool Enabled
651 {
652 get => _enabled;
653 set
654 {
655 if (_enabled == value)
656 return;
657
658 _enabled = value;
659 _owner.RefreshFromSourceChange();
660 }
661 }
662
666 public PointF CenterWorldPx
667 {
668 get => _centerWorldPx;
669 set
670 {
671 if (_centerWorldPx == value)
672 return;
673
674 _centerWorldPx = value;
675 _owner.RefreshFromSourceChange();
676 }
677 }
678
682 public float RadiusWorldPx
683 {
684 get => _radiusWorldPx;
685 set
686 {
687 float clamped = Math.Max(0f, value);
688 if (Math.Abs(_radiusWorldPx - clamped) < 0.0001f)
689 return;
690
691 _radiusWorldPx = clamped;
692 _owner.RefreshFromSourceChange();
693 }
694 }
695
702 public float Intensity
703 {
704 get => _intensity;
705 set
706 {
707 float clamped = Math.Clamp(value, 0f, 1f);
708 if (Math.Abs(_intensity - clamped) < 0.0001f)
709 return;
710
711 _intensity = clamped;
712 _owner.RefreshFromSourceChange();
713 }
714 }
715
720 public void MoveTo(PointF centerWorldPx)
721 {
722 CenterWorldPx = centerWorldPx;
723 }
724
729 public void SetRadius(float radiusWorldPx)
730 {
731 RadiusWorldPx = radiusWorldPx;
732 }
733
734 internal void SyncFromTrackedLight(PointF centerWorldPx, float radiusWorldPx, float intensity)
735 {
736 bool changed =
737 _centerWorldPx != centerWorldPx ||
738 Math.Abs(_radiusWorldPx - radiusWorldPx) >= 0.0001f ||
739 Math.Abs(_intensity - intensity) >= 0.0001f;
740
741 if (!changed)
742 return;
743
744 _centerWorldPx = centerWorldPx;
745 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
746 _intensity = Math.Clamp(intensity, 0f, 1f);
747 _owner.RefreshFromSourceChange();
748 }
749 }
750}
Represents a single world-space reveal source used by a DirectDarknessOverlay.
void MoveTo(PointF centerWorldPx)
Moves the reveal source to a new world-space position.
bool Enabled
Gets or sets whether this reveal source is currently active.
void SetRadius(float radiusWorldPx)
Sets the reveal radius in world-space pixels.
float RadiusWorldPx
Gets or sets the reveal radius in world-space pixels.
float Intensity
Gets or sets the reveal intensity from 0..1.
string? Nickname
Optional human-readable name for debugging and lookup.
PointF CenterWorldPx
Gets or sets the reveal center in world-space pixels.
byte DarknessOpacity
Gets or sets the opacity of the darkness overlay.
void ClearRevealSources()
Removes all reveal sources and stops all automatic tracking.
RevealSource AddRevealSource(PointF centerWorldPx, float radiusWorldPx, string? nickname=null)
Adds a new manually controlled reveal source in world-space.
DirectDarknessOverlay(RenderSurfaceHostBase renderSurfaceHost, View view, SceneLayer projectionLayer, string? nickname=null)
Initializes a new darkness/fog overlay that covers a specific view.
float MidpointRadiusRatio
Gets or sets the radius ratio of the midpoint alpha stop used for reveal falloff.
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.
DirectDarknessOverlay SetDarknessColor(Color color)
Fluent helper to set the darkness tint.
bool UntrackLight(DirectRadialLight light)
Stops tracking a specific light and removes its reveal source.
void TrackLightLayer(DirectLightLayer lightLayer, float radiusScale=1f, float intensityScale=1f, bool trackIntensity=true)
Tracks every current and future light in a DirectLightLayer.
DirectDarknessOverlay SetInnerClearRadiusRatio(float ratio)
Fluent helper to set the fully-revealed radius ratio.
SceneLayer ProjectionLayer
Gets the scene layer used to project reveal source world positions into screen coordinates.
bool RemoveRevealSource(RevealSource source)
Removes a reveal source from this 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.
Color DarknessColor
Gets or sets the tint color of the overlay.
DirectDarknessOverlay SetDarknessOpacity(byte opacity)
Fluent helper to set the darkness opacity.
override void Update(long tick)
Performs per-frame update logic for this direct drawing, including fade and reveal animations....
IReadOnlyList< RevealSource > RevealSources
Gets a live, ordered list of reveal sources used by this overlay.
override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
DirectDarknessOverlay SetMidpointStrength(float strength)
Fluent helper to set the midpoint reveal strength.
override void Dispose(bool disposing)
Releases resources used by this DirectDrawingBase instance.When disposing is true,...
DirectDarknessOverlay SetMidpointRadiusRatio(float ratio)
Fluent helper to set the midpoint radius ratio.
int ZOrder
Gets or sets the Z-order (depth) used for sorting this direct drawing relative to other drawables.
View? View
Gets the view to which this direct drawing is attached, or null if the drawing is in scene-layer mode...
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...
DirectDrawingBase(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, SceneLayer? sceneLayer, View? view, Rectangle? screenBounds, Rectangle? worldBounds, string? nickname=null)
Initializes a new instance of the DirectDrawingBase class.
Rectangle ScreenBounds
Gets or sets the screen-space bounds of this direct drawing in pixels.
Convenience owner for a group of scene-layer radial lights.
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 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 ...
A rectangular window on the render target with its own zoom and placement. Viewport never moves the w...
Definition Viewport.cs:10
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.