33 private readonly List<RevealSource> _revealSources = [];
34 private readonly List<TrackedLightReveal> _trackedLightReveals = [];
35 private readonly List<TrackedLightLayerReveal> _trackedLightLayers = [];
37 private readonly SKPaint _darknessPaint =
new()
40 BlendMode = SKBlendMode.SrcOver,
41 Style = SKPaintStyle.Fill
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;
59 Rectangle worldBounds,
60 string? nickname =
null)
61 : base(renderSurfaceHost,
66 worldBounds: worldBounds,
70 RebuildDarknessPaint();
92 get => _darknessColor;
95 if (_darknessColor == value)
98 _darknessColor = value;
99 RebuildDarknessPaint();
109 get => _darknessOpacity;
112 if (_darknessOpacity == value)
115 _darknessOpacity = value;
116 RebuildDarknessPaint();
126 get => _innerClearRadiusRatio;
129 float clamped = Math.Clamp(value, 0f, 1f);
130 if (Math.Abs(_innerClearRadiusRatio - clamped) < 0.0001f)
133 _innerClearRadiusRatio = clamped;
134 if (_midpointRadiusRatio < _innerClearRadiusRatio)
135 _midpointRadiusRatio = _innerClearRadiusRatio;
146 get => _midpointRadiusRatio;
149 float clamped = Math.Clamp(value, 0f, 1f);
150 clamped = Math.Max(clamped, _innerClearRadiusRatio);
152 if (Math.Abs(_midpointRadiusRatio - clamped) < 0.0001f)
155 _midpointRadiusRatio = clamped;
165 get => _midpointStrength;
168 float clamped = Math.Clamp(value, 0f, 1f);
169 if (Math.Abs(_midpointStrength - clamped) < 0.0001f)
172 _midpointStrength = clamped;
186 var source =
new RevealSource(
this, centerWorldPx, radiusWorldPx, nickname);
187 _revealSources.Add(source);
208 float radiusScale = 1f,
209 float intensityScale = 1f,
210 bool trackIntensity =
true,
211 string? nickname =
null)
214 throw new ArgumentNullException(nameof(light));
216 EnsureSameSceneLayer(light);
218 if (_trackedLightReveals.Any(t => ReferenceEquals(t.Light, light)))
219 return _trackedLightReveals.First(t => ReferenceEquals(t.Light, light)).Source;
222 _revealSources.Add(source);
224 var tracked =
new TrackedLightReveal(light, source, Math.Max(0f, radiusScale), Math.Max(0f, intensityScale), trackIntensity);
225 _trackedLightReveals.Add(tracked);
227 light.Changed += OnTrackedLightChanged;
228 light.Disposing += OnTrackedLightDisposing;
230 SyncTrackedLight(tracked);
247 float radiusScale = 1f,
248 float intensityScale = 1f,
249 bool trackIntensity =
true)
251 if (lightLayer is
null)
252 throw new ArgumentNullException(nameof(lightLayer));
255 throw new ArgumentException(
"SceneLayer-bound darkness can only track a DirectLightLayer attached to the same SceneLayer.", nameof(lightLayer));
257 if (_trackedLightLayers.Any(t => ReferenceEquals(t.LightLayer, lightLayer)))
260 var trackedLayer =
new TrackedLightLayerReveal(
262 Math.Max(0f, radiusScale),
263 Math.Max(0f, intensityScale),
266 _trackedLightLayers.Add(trackedLayer);
268 foreach (var light
in lightLayer.Lights)
269 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
271 lightLayer.LightAdded += OnTrackedLayerLightAdded;
272 lightLayer.LightRemoving += OnTrackedLayerLightRemoving;
282 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Light, light));
300 var tracked = _trackedLightReveals.FirstOrDefault(t => ReferenceEquals(t.Source, source));
301 if (tracked is not
null)
304 bool removed = _revealSources.Remove(source);
316 if (_revealSources.Count == 0 && _trackedLightReveals.Count == 0 && _trackedLightLayers.Count == 0)
319 foreach (var trackedLayer
in _trackedLightLayers.ToArray())
321 trackedLayer.LightLayer.LightAdded -= OnTrackedLayerLightAdded;
322 trackedLayer.LightLayer.LightRemoving -= OnTrackedLayerLightRemoving;
325 _trackedLightLayers.Clear();
327 foreach (var tracked
in _trackedLightReveals.ToArray())
330 _revealSources.Clear();
403 if (destRectScreen.Width <= 0f || destRectScreen.Height <= 0f ||
WorldBounds.Width <= 0 ||
WorldBounds.Height <= 0)
406 var canvas = backbuffer.
Canvas;
407 var destRect = destRectScreen.ToSKRect();
411 canvas.SaveLayer(destRect,
null);
413 canvas.DrawRect(destRect, _darknessPaint);
415 for (
int i = 0; i < _revealSources.Count; i++)
417 var source = _revealSources[i];
418 if (!source.Enabled || source.RadiusWorldPx <= 0.01f || source.Intensity <= 0.001f)
421 var centerScreen = WorldPointToDrawRect(source.CenterWorldPx, destRectScreen);
422 float radiusScreen = WorldRadiusToDrawRadius(source.RadiusWorldPx, destRectScreen);
424 if (radiusScreen <= 0.01f)
427 using var revealShader = BuildRevealShader(centerScreen, radiusScreen, source.Intensity);
428 using var revealPaint =
new SKPaint
431 BlendMode = SKBlendMode.DstOut,
432 Shader = revealShader,
433 Style = SKPaintStyle.Fill
436 canvas.DrawRect(destRect, revealPaint);
442 internal void RefreshFromSourceChange()
447 private void OnTrackedLightChanged(DirectRadialLight light)
449 foreach (var tracked
in _trackedLightReveals.Where(t => ReferenceEquals(t.Light, light)).ToArray())
450 SyncTrackedLight(tracked);
453 private void OnTrackedLightDisposing(
object? sender, IDirectDrawable drawing)
455 if (drawing is DirectRadialLight light)
459 private void OnTrackedLayerLightAdded(DirectRadialLight light)
461 if (!ReferenceEquals(light.SceneLayer,
SceneLayer))
464 foreach (var trackedLayer
in _trackedLightLayers.Where(t => t.LightLayer.Lights.Contains(light)).ToArray())
465 TrackLight(light, trackedLayer.RadiusScale, trackedLayer.IntensityScale, trackedLayer.TrackIntensity);
468 private void OnTrackedLayerLightRemoving(DirectRadialLight light)
473 private void SyncTrackedLight(TrackedLightReveal tracked)
475 var light = tracked.Light;
476 float intensity = tracked.TrackIntensity
477 ? light.EffectiveIntensity * tracked.IntensityScale
478 : tracked.IntensityScale;
480 tracked.Source.SyncFromTrackedLight(
482 light.RadiusWorldPx * tracked.RadiusScale,
486 private void UntrackLight(TrackedLightReveal tracked,
bool removeRevealSource)
488 tracked.Light.Changed -= OnTrackedLightChanged;
489 tracked.Light.Disposing -= OnTrackedLightDisposing;
490 _trackedLightReveals.Remove(tracked);
492 if (removeRevealSource)
493 _revealSources.Remove(tracked.Source);
498 private void EnsureSameSceneLayer(DirectRadialLight light)
500 if (!ReferenceEquals(light.SceneLayer,
SceneLayer))
501 throw new ArgumentException(
"SceneLayer-bound darkness can only track lights attached to the same SceneLayer.", nameof(light));
504 private PointF WorldPointToDrawRect(PointF worldPx, RectangleF destRectScreen)
506 float scaleX = destRectScreen.Width /
WorldBounds.Width;
507 float scaleY = destRectScreen.Height /
WorldBounds.Height;
510 destRectScreen.Left + (worldPx.X -
WorldBounds.Left) * scaleX,
511 destRectScreen.Top + (worldPx.Y -
WorldBounds.Top) * scaleY);
514 private float WorldRadiusToDrawRadius(
float radiusWorldPx, RectangleF destRectScreen)
516 float scaleX = destRectScreen.Width /
WorldBounds.Width;
517 float scaleY = destRectScreen.Height /
WorldBounds.Height;
519 return Math.Max(Math.Abs(radiusWorldPx * scaleX), Math.Abs(radiusWorldPx * scaleY));
522 private void RebuildDarknessPaint()
524 _darknessPaint.Color = Color.FromArgb(_darknessOpacity, _darknessColor).ToSKColor();
527 private SKShader BuildRevealShader(PointF centerScreenPx,
float radiusScreenPx,
float intensity)
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);
532 return SKShader.CreateRadialGradient(
533 new SKPoint(centerScreenPx.X, centerScreenPx.Y),
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)
545 _innerClearRadiusRatio,
546 _midpointRadiusRatio,
549 mode: SKShaderTileMode.Clamp);
553 protected override void Dispose(
bool disposing)
558 _darknessPaint.Dispose();
561 base.Dispose(disposing);
564 private sealed class TrackedLightReveal(
568 float intensityScale,
578 public RevealSource Source {
get; } = source;
582 public float RadiusScale {
get; } = radiusScale;
586 public float IntensityScale {
get; } = intensityScale;
590 public bool TrackIntensity {
get; } = trackIntensity;
593 private sealed class TrackedLightLayerReveal(
594 DirectLightLayer lightLayer,
596 float intensityScale,
602 public DirectLightLayer LightLayer {
get; } = lightLayer;
606 public float RadiusScale {
get; } = radiusScale;
610 public float IntensityScale {
get; } = intensityScale;
614 public bool TrackIntensity {
get; } = trackIntensity;
620 public sealed class RevealSource
623 private PointF _centerWorldPx;
624 private float _radiusWorldPx;
625 private float _intensity = 1f;
626 private bool _enabled =
true;
631 _centerWorldPx = centerWorldPx;
632 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
649 if (_enabled == value)
653 _owner.RefreshFromSourceChange();
662 get => _centerWorldPx;
665 if (_centerWorldPx == value)
668 _centerWorldPx = value;
669 _owner.RefreshFromSourceChange();
678 get => _radiusWorldPx;
681 float clamped = Math.Max(0f, value);
682 if (Math.Abs(_radiusWorldPx - clamped) < 0.0001f)
685 _radiusWorldPx = clamped;
686 _owner.RefreshFromSourceChange();
698 float clamped = Math.Clamp(value, 0f, 1f);
699 if (Math.Abs(_intensity - clamped) < 0.0001f)
702 _intensity = clamped;
703 _owner.RefreshFromSourceChange();
725 internal void SyncFromTrackedLight(PointF centerWorldPx,
float radiusWorldPx,
float intensity)
728 _centerWorldPx != centerWorldPx ||
729 Math.Abs(_radiusWorldPx - radiusWorldPx) >= 0.0001f ||
730 Math.Abs(_intensity - intensity) >= 0.0001f;
735 _centerWorldPx = centerWorldPx;
736 _radiusWorldPx = Math.Max(0f, radiusWorldPx);
737 _intensity = Math.Clamp(intensity, 0f, 1f);
738 _owner.RefreshFromSourceChange();