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
DirectRadialLight.cs
1using System.Drawing;
6using SkiaSharp;
7
9
32{
33 private PointF _centerWorldPx;
34 private float _radiusWorldPx;
35 private Color _lightColor;
36 private float _intensity = 1f;
37 private float _hotspotRadiusRatio = 0.06f;
38 private float _midpointRadiusRatio = 0.55f;
39 private float _midpointIntensityRatio = 0.35f;
40 private bool _isAntialias = true;
41 private SKBlendMode _blendMode = SKBlendMode.Screen;
42
43 private bool _flickerEnabled;
44 private float _flickerAmount = 0.08f;
45 private int _flickerRefreshHz = 12;
46 private float _flickerPhaseSec;
47 private float _flickerMultiplier = 1f;
48 private long _lastFlickerRefreshTick;
49
59 public event Action<DirectRadialLight>? Changed;
60
71 Color lightColor,
72 RenderSurfaceHostBase renderSurfaceHost,
73 SceneLayer sceneLayer,
74 PointF centerWorldPx,
75 float radiusWorldPx,
76 string? nickname = null)
77 : base(
78 renderSurfaceHost,
80 sceneLayer,
81 view: null,
82 screenBounds: null,
83 worldBounds: BoundsFromCenter(centerWorldPx, radiusWorldPx),
84 nickname)
85 {
86 _lightColor = lightColor;
87 _centerWorldPx = centerWorldPx;
88 _radiusWorldPx = Math.Max(1f, radiusWorldPx);
89 }
90
94 public PointF CenterWorldPx
95 {
96 get => _centerWorldPx;
97 set
98 {
99 if (ApproximatelyEqual(_centerWorldPx, value))
100 return;
101
102 _centerWorldPx = value;
103 UpdateWorldBounds();
104 }
105 }
106
113 public float RadiusWorldPx
114 {
115 get => _radiusWorldPx;
116 set
117 {
118 var clamped = Math.Max(1f, value);
119 if (Math.Abs(_radiusWorldPx - clamped) < 0.001f)
120 return;
121
122 _radiusWorldPx = clamped;
123 UpdateWorldBounds();
124 }
125 }
126
130 public Color LightColor
131 {
132 get => _lightColor;
133 set
134 {
135 if (_lightColor.ToArgb() == value.ToArgb())
136 return;
137
138 _lightColor = value;
139 ForceRefresh();
140 OnChanged();
141 }
142 }
143
147 public float Intensity
148 {
149 get => _intensity;
150 set
151 {
152 var clamped = Clamp01(value);
153 if (Math.Abs(_intensity - clamped) < 0.001f)
154 return;
155
156 _intensity = clamped;
157 ForceRefresh();
158 OnChanged();
159 }
160 }
161
165 public float EffectiveIntensity => Clamp01(_intensity * _flickerMultiplier);
166
170 public SKBlendMode BlendMode
171 {
172 get => _blendMode;
173 set
174 {
175 if (_blendMode == value)
176 return;
177
178 _blendMode = value;
179 ForceRefresh();
180 }
181 }
182
187 {
188 get => _hotspotRadiusRatio;
189 set
190 {
191 var clamped = Clamp01(value);
192 if (Math.Abs(_hotspotRadiusRatio - clamped) < 0.001f)
193 return;
194
195 _hotspotRadiusRatio = clamped;
196 ForceRefresh();
197 }
198 }
199
204 {
205 get => _midpointRadiusRatio;
206 set
207 {
208 var clamped = Clamp01(value);
209 if (Math.Abs(_midpointRadiusRatio - clamped) < 0.001f)
210 return;
211
212 _midpointRadiusRatio = clamped;
213 ForceRefresh();
214 }
215 }
216
221 {
222 get => _midpointIntensityRatio;
223 set
224 {
225 var clamped = Clamp01(value);
226 if (Math.Abs(_midpointIntensityRatio - clamped) < 0.001f)
227 return;
228
229 _midpointIntensityRatio = clamped;
230 ForceRefresh();
231 }
232 }
233
237 public bool IsAntialias
238 {
239 get => _isAntialias;
240 set
241 {
242 if (_isAntialias == value)
243 return;
244
245 _isAntialias = value;
246 ForceRefresh();
247 }
248 }
249
258 public bool FlickerEnabled
259 {
260 get => _flickerEnabled;
261 set
262 {
263 if (_flickerEnabled == value)
264 return;
265
266 _flickerEnabled = value;
267 _flickerMultiplier = 1f;
268 _lastFlickerRefreshTick = 0;
269 ForceRefresh();
270 OnChanged();
271 }
272 }
273
277 public float FlickerAmount
278 {
279 get => _flickerAmount;
280 set
281 {
282 var clamped = Math.Clamp(value, 0f, 1f);
283 if (Math.Abs(_flickerAmount - clamped) < 0.001f)
284 return;
285
286 _flickerAmount = clamped;
287 ForceRefresh();
288 OnChanged();
289 }
290 }
291
296 {
297 get => _flickerRefreshHz;
298 set => _flickerRefreshHz = Math.Max(1, value);
299 }
300
306 public DirectRadialLight MoveTo(PointF centerWorldPx)
307 {
308 CenterWorldPx = centerWorldPx;
309 return this;
310 }
311
317 public DirectRadialLight SetRadius(float radiusWorldPx)
318 {
319 RadiusWorldPx = radiusWorldPx;
320 return this;
321 }
322
328 public DirectRadialLight SetIntensity(float intensity)
329 {
330 Intensity = intensity;
331 return this;
332 }
333
335 public override void Update(long tick)
336 {
337 UpdateFlicker(tick);
338 base.Update(tick);
339 }
340
342 protected override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
343 {
344 if (destRectScreen.Width <= 0f || destRectScreen.Height <= 0f)
345 return;
346
347 var canvas = backbuffer.Canvas;
348 var worldBounds = WorldBounds;
349
350 if (worldBounds.Width <= 0 || worldBounds.Height <= 0)
351 return;
352
353 var rect = new SKRect(destRectScreen.Left, destRectScreen.Top, destRectScreen.Right, destRectScreen.Bottom);
354 float scaleX = destRectScreen.Width / worldBounds.Width;
355 float scaleY = destRectScreen.Height / worldBounds.Height;
356
357 // Use the actual world-space center/radius rather than the integer-rounded
358 // WorldBounds midpoint. The bounds are intentionally pixel-aligned for dirty
359 // rectangles, but the shader geometry should preserve sub-pixel light movement
360 // so a moving torch does not pop by ~0.5px as its bounds floor/ceil change.
361 var center = new SKPoint(
362 destRectScreen.Left + (_centerWorldPx.X - worldBounds.Left) * scaleX,
363 destRectScreen.Top + (_centerWorldPx.Y - worldBounds.Top) * scaleY);
364
365 var radius = Math.Max(Math.Abs(_radiusWorldPx * scaleX), Math.Abs(_radiusWorldPx * scaleY));
366
367 if (radius <= 0f)
368 return;
369
370 var hotspot = Math.Clamp(_hotspotRadiusRatio, 0f, 0.95f);
371 var midpoint = Math.Clamp(_midpointRadiusRatio, hotspot + 0.001f, 0.999f);
372 var effectiveIntensity = EffectiveIntensity;
373
374 var inner = ToSkColor(_lightColor, effectiveIntensity);
375 var middle = ToSkColor(_lightColor, effectiveIntensity * _midpointIntensityRatio);
376 var outer = ToSkColor(_lightColor, 0f);
377
378 using var shader = SKShader.CreateRadialGradient(
379 center,
380 radius,
381 new[] { inner, inner, middle, outer },
382 new[] { 0f, hotspot, midpoint, 1f },
383 SKShaderTileMode.Clamp);
384
385 using var paint = new SKPaint
386 {
387 Shader = shader,
388 Style = SKPaintStyle.Fill,
389 BlendMode = _blendMode,
390 IsAntialias = _isAntialias
391 };
392
393 canvas.DrawRect(rect, paint);
394 }
395
396 private void UpdateFlicker(long tick)
397 {
398 if (!_flickerEnabled)
399 return;
400
401 if (_lastFlickerRefreshTick == 0)
402 {
403 _lastFlickerRefreshTick = tick;
404 return;
405 }
406
407 if (tick <= _lastFlickerRefreshTick)
408 return;
409
410 var dt = HighResTimer.GetDuration(_lastFlickerRefreshTick, tick);
411 var refreshIntervalSec = 1f / _flickerRefreshHz;
412
413 if (dt < refreshIntervalSec)
414 return;
415
416 _flickerPhaseSec += dt;
417
418 var primary = 0.5f + 0.5f * MathF.Sin(_flickerPhaseSec * MathF.Tau * _flickerRefreshHz);
419 var secondary = 0.5f + 0.5f * MathF.Sin(_flickerPhaseSec * MathF.Tau * (_flickerRefreshHz * 0.37f + 0.73f));
420 var mixed = primary * 0.65f + secondary * 0.35f;
421
422 _flickerMultiplier = 1f - _flickerAmount + mixed * _flickerAmount * 2f;
423 _lastFlickerRefreshTick = tick;
424
425 ForceRefresh();
426 OnChanged();
427 }
428
429 private void UpdateWorldBounds()
430 {
431 WorldBounds = BoundsFromCenter(_centerWorldPx, _radiusWorldPx);
432 OnChanged();
433 }
434
435 private void OnChanged()
436 {
437 Changed?.Invoke(this);
438 }
439
440 private static Rectangle BoundsFromCenter(PointF centerWorldPx, float radiusWorldPx)
441 {
442 var radius = Math.Max(1f, radiusWorldPx);
443 var left = (int)MathF.Floor(centerWorldPx.X - radius);
444 var top = (int)MathF.Floor(centerWorldPx.Y - radius);
445 var right = (int)MathF.Ceiling(centerWorldPx.X + radius);
446 var bottom = (int)MathF.Ceiling(centerWorldPx.Y + radius);
447
448 return Rectangle.FromLTRB(left, top, right, bottom);
449 }
450
451 private static SKColor ToSkColor(Color color, float intensity)
452 {
453 var alpha = (byte)Math.Clamp(color.A * Clamp01(intensity), 0f, 255f);
454 return new SKColor(color.R, color.G, color.B, alpha);
455 }
456
457 private static float Clamp01(float value) => Math.Clamp(value, 0f, 1f);
458
459 private static bool ApproximatelyEqual(PointF left, PointF right)
460 {
461 return Math.Abs(left.X - right.X) < 0.001f &&
462 Math.Abs(left.Y - right.Y) < 0.001f;
463 }
464}
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.
DirectRadialLight SetRadius(float radiusWorldPx)
Sets the light radius and returns this instance for chaining.
float FlickerAmount
Gets or sets how much flicker may alter the base intensity. Recommended range: 0 to 0....
int FlickerRefreshHz
Gets or sets how often flicker is recomputed and dirtied.
float MidpointIntensityRatio
Gets or sets the middle stop intensity multiplier from 0 to 1.
Action< DirectRadialLight >? Changed
Occurs when this light changes in a way tracked darkness overlays care about.
float EffectiveIntensity
Gets the current intensity after flicker has been applied.
bool IsAntialias
Gets or sets whether Skia antialiasing is enabled for the light draw.
float RadiusWorldPx
Gets the light radius in world pixels.
DirectRadialLight SetIntensity(float intensity)
Sets the light intensity and returns this instance for chaining.
override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
Color LightColor
Gets or sets the color and base alpha of the light.
DirectRadialLight(Color lightColor, RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, PointF centerWorldPx, float radiusWorldPx, string? nickname=null)
Initializes a new scene-layer radial light.
SKBlendMode BlendMode
Gets or sets the blend mode used when compositing the light over scene content.
float MidpointRadiusRatio
Gets or sets the radius ratio for the middle gradient stop.
float HotspotRadiusRatio
Gets or sets the radius ratio for the hottest inner portion of the gradient.
bool FlickerEnabled
Gets or sets whether the light should periodically vary its intensity.
override void Update(long tick)
Performs per-frame update logic for this direct drawing, including fade and reveal animations....
float Intensity
Gets or sets the overall intensity multiplier from 0 to 1.
PointF CenterWorldPx
Gets the light center in world pixels.
DirectRadialLight MoveTo(PointF centerWorldPx)
Moves the light center and returns this instance for chaining.
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 ...
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.