54 private readonly List<RevealSource> _revealSources = [];
55 private readonly List<TrackedLightReveal> _trackedLightReveals = [];
56 private readonly List<TrackedLightLayerReveal> _trackedLightLayers = [];
58 private readonly SKPaint _darknessPaint =
new()
61 BlendMode = SKBlendMode.SrcOver,
62 Style = SKPaintStyle.Fill
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;
84 string? nickname =
null)
85 : base(renderSurfaceHost,
89 screenBounds: view.
Viewport.TargetRectPx,
93 ProjectionLayer = projectionLayer ??
throw new ArgumentNullException(nameof(projectionLayer));
95 RebuildDarknessPaint();
113 get => _darknessColor;
116 if (_darknessColor == value)
119 _darknessColor = value;
120 RebuildDarknessPaint();
130 get => _darknessOpacity;
133 if (_darknessOpacity == value)
136 _darknessOpacity = value;
137 RebuildDarknessPaint();
151 get => _innerClearRadiusRatio;
154 float clamped = Math.Clamp(value, 0f, 1f);
155 if (Math.Abs(_innerClearRadiusRatio - clamped) < 0.0001f)
158 _innerClearRadiusRatio = clamped;
159 if (_midpointRadiusRatio < _innerClearRadiusRatio)
160 _midpointRadiusRatio = _innerClearRadiusRatio;
171 get => _midpointRadiusRatio;
174 float clamped = Math.Clamp(value, 0f, 1f);
175 clamped = Math.Max(clamped, _innerClearRadiusRatio);
177 if (Math.Abs(_midpointRadiusRatio - clamped) < 0.0001f)
180 _midpointRadiusRatio = clamped;
193 get => _midpointStrength;
196 float clamped = Math.Clamp(value, 0f, 1f);
197 if (Math.Abs(_midpointStrength - clamped) < 0.0001f)
200 _midpointStrength = clamped;
214 var source =
new RevealSource(
this, centerWorldPx, radiusWorldPx, nickname);
215 _revealSources.Add(source);
240 float radiusScale = 1f,
241 float intensityScale = 1f,
242 bool trackIntensity =
true,
243 string? nickname =
null)
246 throw new ArgumentNullException(nameof(light));
248 if (_trackedLightReveals.Any(t => ReferenceEquals(t.Light, light)))
249 return _trackedLightReveals.First(t => ReferenceEquals(t.Light, light)).Source;
252 _revealSources.Add(source);
254 var tracked =
new TrackedLightReveal(light, source, Math.Max(0f, radiusScale), Math.Max(0f, intensityScale), trackIntensity);
255 _trackedLightReveals.Add(tracked);
257 light.Changed += OnTrackedLightChanged;
258 light.Disposing += OnTrackedLightDisposing;
260 SyncTrackedLight(tracked);
283 float radiusScale = 1f,
284 float intensityScale = 1f,
285 bool trackIntensity =
true)
287 if (lightLayer is
null)
288 throw new ArgumentNullException(nameof(lightLayer));
290 if (_trackedLightLayers.Any(t => ReferenceEquals(t.LightLayer, lightLayer)))
293 var trackedLayer =
new TrackedLightLayerReveal(
295 Math.Max(0f, radiusScale),
296 Math.Max(0f, intensityScale),
299 _trackedLightLayers.Add(trackedLayer);
301 foreach (var light
in lightLayer.Lights)
302 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
304 lightLayer.LightAdded += OnTrackedLayerLightAdded;
305 lightLayer.LightRemoving += OnTrackedLayerLightRemoving;
315 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Light, light));
333 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Source, source));
334 if (tracked is not
null)
337 bool removed = _revealSources.Remove(source);
349 if (_revealSources.Count == 0 && _trackedLightReveals.Count == 0 && _trackedLightLayers.Count == 0)
352 foreach (var trackedLayer
in _trackedLightLayers.ToArray())
354 trackedLayer.LightLayer.LightAdded -= OnTrackedLayerLightAdded;
355 trackedLayer.LightLayer.LightRemoving -= OnTrackedLayerLightRemoving;
358 _trackedLightLayers.Clear();
360 foreach (var tracked
in _trackedLightReveals.ToArray())
363 _revealSources.Clear();
426 var targetRect =
View!.Viewport.TargetRectPx;
436 var canvas = backbuffer.
Canvas;
437 var destRect = destRectScreen.ToSKRect();
441 canvas.SaveLayer(destRect,
null);
443 canvas.DrawRect(destRect, _darknessPaint);
445 for (
int i = 0; i < _revealSources.Count; i++)
447 var source = _revealSources[i];
448 if (!source.Enabled || source.RadiusWorldPx <= 0.01f || source.Intensity <= 0.001f)
453 var radiusProbeWorld =
new PointF(source.CenterWorldPx.X + source.RadiusWorldPx, source.CenterWorldPx.Y);
455 float screenRadius = Math.Abs(radiusProbeScreen.X - centerScreen.X);
457 if (screenRadius <= 0.01f)
460 using var revealShader = BuildRevealShader(centerScreen, screenRadius, source.Intensity);
461 using var revealPaint =
new SKPaint
464 BlendMode = SKBlendMode.DstOut,
465 Shader = revealShader,
466 Style = SKPaintStyle.Fill
469 canvas.DrawRect(destRect, revealPaint);
475 internal void RefreshFromSourceChange()
480 private void OnTrackedLightChanged(DirectRadialLight light)
482 foreach (var tracked
in _trackedLightReveals.Where(t => ReferenceEquals(t.Light, light)).ToArray())
483 SyncTrackedLight(tracked);
486 private void OnTrackedLightDisposing(
object? sender, IDirectDrawable drawing)
488 if (drawing is DirectRadialLight light)
492 private void OnTrackedLayerLightAdded(DirectRadialLight light)
494 foreach (var trackedLayer
in _trackedLightLayers.Where(t => t.LightLayer.Lights.Contains(light)).ToArray())
495 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
498 private void OnTrackedLayerLightRemoving(DirectRadialLight light)
503 private void SyncTrackedLight(TrackedLightReveal tracked)
505 var light = tracked.Light;
506 float intensity = tracked.TrackIntensity
507 ? light.EffectiveIntensity * tracked.IntensityScale
508 : tracked.IntensityScale;
510 tracked.Source.SyncFromTrackedLight(
512 light.RadiusWorldPx * tracked.RadiusScale,
516 private void UntrackLight(TrackedLightReveal tracked,
bool removeRevealSource)
518 tracked.Light.Changed -= OnTrackedLightChanged;
519 tracked.Light.Disposing -= OnTrackedLightDisposing;
520 _trackedLightReveals.Remove(tracked);
522 if (removeRevealSource)
523 _revealSources.Remove(tracked.Source);
528 private void RebuildDarknessPaint()
530 _darknessPaint.Color = Color.FromArgb(_darknessOpacity, _darknessColor).ToSKColor();
533 private SKShader BuildRevealShader(PointF centerScreenPx,
float radiusScreenPx,
float intensity)
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);
538 return SKShader.CreateRadialGradient(
539 new SKPoint(centerScreenPx.X, centerScreenPx.Y),
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)
551 _innerClearRadiusRatio,
552 _midpointRadiusRatio,
555 mode: SKShaderTileMode.Clamp);
559 protected override void Dispose(
bool disposing)
564 _darknessPaint.Dispose();
567 base.Dispose(disposing);
570 private sealed class TrackedLightReveal(
574 float intensityScale,
584 public RevealSource Source {
get; } = source;
588 public float RadiusScale {
get; } = radiusScale;
592 public float IntensityScale {
get; } = intensityScale;
596 public bool TrackIntensity {
get; } = trackIntensity;
599 private sealed class TrackedLightLayerReveal(
600 DirectLightLayer lightLayer,
602 float intensityScale,
608 public DirectLightLayer LightLayer {
get; } = lightLayer;
612 public float RadiusScale {
get; } = radiusScale;
616 public float IntensityScale {
get; } = intensityScale;
620 public bool TrackIntensity {
get; } = trackIntensity;
626 public sealed class RevealSource
629 private PointF _centerWorldPx;
630 private float _radiusWorldPx;
631 private float _intensity = 1f;
632 private bool _enabled =
true;
634 internal RevealSource(
DirectDarknessOverlay owner, PointF centerWorldPx,
float radiusWorldPx,
string? nickname)
637 _centerWorldPx = centerWorldPx;
638 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
655 if (_enabled == value)
659 _owner.RefreshFromSourceChange();
668 get => _centerWorldPx;
671 if (_centerWorldPx == value)
674 _centerWorldPx = value;
675 _owner.RefreshFromSourceChange();
684 get => _radiusWorldPx;
687 float clamped = Math.Max(0f, value);
688 if (Math.Abs(_radiusWorldPx - clamped) < 0.0001f)
691 _radiusWorldPx = clamped;
692 _owner.RefreshFromSourceChange();
707 float clamped = Math.Clamp(value, 0f, 1f);
708 if (Math.Abs(_intensity - clamped) < 0.0001f)
711 _intensity = clamped;
712 _owner.RefreshFromSourceChange();
734 internal void SyncFromTrackedLight(PointF centerWorldPx,
float radiusWorldPx,
float intensity)
737 _centerWorldPx != centerWorldPx ||
738 Math.Abs(_radiusWorldPx - radiusWorldPx) >= 0.0001f ||
739 Math.Abs(_intensity - intensity) >= 0.0001f;
744 _centerWorldPx = centerWorldPx;
745 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
746 _intensity = Math.Clamp(intensity, 0f, 1f);
747 _owner.RefreshFromSourceChange();
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...
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.