-
-
Notifications
You must be signed in to change notification settings - Fork 2
Direct Lighting and Darkness
This page explains Gondwana's lightweight direct-drawing lighting helpers:
DirectRadialLightDirectLightLayerDirectDarknessOverlayDirectSceneLayerDarknessOverlay
These classes provide a modular way to add torch glows, player vision, view-specific darkness, and SceneLayer-bound fog/darkness without replacing Gondwana's renderer or introducing a full lighting engine.
- What this feature is
- What this feature is not
- Mental model
- When to use each class
- View darkness vs SceneLayer darkness
- Coordinate spaces
- Minimal setup checklist
- Recipe 1: draw one standalone torch glow
- Recipe 2: manage several torch lights
- Recipe 3: darken the view with player vision
- Recipe 4: torch glow plus matching darkness reveal
- Recipe 5: track an entire light layer
- Recipe 6: player-held torch
- Recipe 7: manual reveal with no visible glow
- Recipe 8: temporary spell or pickup glow
- Recipe 9: SceneLayer-bound fog or room darkness
- Recipe 10: SceneLayer darkness tracking SceneLayer lights
- Recipe 11: moving fog cloud or poison haze
- Tuning guide
- Dirty rectangles and performance
- Layering and Z-order
- Cleanup and scene changes
- Troubleshooting
- API quick reference
- Summary
Direct lighting and darkness are DirectDrawing-based visual effects.
They are designed for common 2D/2.5D game visuals such as:
- torch glows
- lamp glows
- player vision circles
- dungeon darkness
- fog-of-war style darkness overlays
- actual fog, smoke, haze, or poison clouds
- magic sight
- temporary spell auras
- small localized visual lighting effects
The system is intentionally small. It is meant to be something a game can opt into when needed.
The helpers are useful when you want visuals like:
The player is carrying a torch.
The dungeon is dark.
The torch creates a soft visible circle.
The torch glow flickers gently.
They are also useful for less literal effects:
A magic item glows.
A stealth enemy has a vision radius.
A cutscene spotlights one part of the map.
A hazard gives off a pulsing aura.
A swamp layer has drifting mist over part of the world.
A room contains magical darkness that every camera should see.
This is not a full lighting engine.
It does not provide:
- shadows
- wall occlusion
- ray casting
- normal maps
- physically based lighting
- light bouncing
- tile-aware visibility blocking
- automatic line-of-sight logic
The helpers draw visual effects. Game logic still decides where lights are, which objects emit light, and whether a reveal source should exist.
For example, this system can draw a torch glow around a player. It does not automatically know that a stone wall should block the torch.
If your game needs wall-aware visibility, you can build that separately and use the result to decide which reveal sources should exist, where they should be, or how large they should be.
Think of the system as three related pieces:
DirectRadialLight
draws a warm, bounded glow on a SceneLayer
DirectDarknessOverlay
draws darkness over a View
then cuts soft reveal holes through it
DirectSceneLayerDarknessOverlay
draws darkness/fog over a bounded world region on one SceneLayer
then cuts soft reveal holes through that layer-local overlay
Used together, a light and a View darkness overlay create the classic player-vision or torch-in-a-dark-room effect:
scene tiles and sprites
+ view darkness overlay
- reveal hole around torch/player
+ warm radial torch glow
Used together, a light and a SceneLayer darkness overlay create a world-local fog/darkness effect:
scene layer content
+ bounded layer fog/darkness region
- reveal hole around layer-local torch
+ warm radial torch glow on that same layer
The darkness reveal and the visible light glow are separate on purpose.
That gives you several useful combinations:
| Combination | Result | Typical use |
|---|---|---|
| Light only | Warm glow over normal scene | lamps, spell glows, engine exhaust |
| View darkness only | Whole view darkened | cutscene fade, night vision baseline |
| View darkness + manual reveal | Player vision / stealth vision / fog-of-war reveal | per-player visibility |
| View darkness + tracked light | Torch glow and View reveal hole move together | dungeon exploration |
| SceneLayer darkness only | Bounded fog/darkness exists in the world | swamp mist, room darkness |
| SceneLayer darkness + tracked light | Layer-local fog/darkness reveals around layer-local lights | smoke/fog affected by torches on that layer |
Use this when the effect itself should be visible:
- torch glow
- lantern glow
- spell aura
- explosion flash
- glowing crystal
- engine exhaust glow
- warm window light
You can use DirectRadialLight directly. You do not need DirectLightLayer.
DirectLightLayer is a convenience owner/factory.
It is not a SceneLayer.
Use it when you want:
- a list of lights
- quick torch defaults
- easier cleanup
-
TrackLightLayer(...)support from the darkness overlay
For one or two lights, direct DirectRadialLight construction is fine.
For many lights, DirectLightLayer keeps code cleaner.
Use this when you want:
- a dark dungeon
- fog-like screen darkening
- player vision circles
- visibility holes around torches
- stealth / sight radius effects
DirectDarknessOverlay is view-space. It covers the target View.
Reveal sources are defined in world-space and projected through a scene layer.
Use this when the effect is something that physically exists on a specific SceneLayer:
- swamp mist
- smoke clouds
- poison gas
- dust haze
- magical darkness in one room
- localized fog over terrain
- low cloud layer over a graveyard
DirectSceneLayerDarknessOverlay is SceneLayer-space. It has WorldBounds, participates in that layer's draw order, and is visible to any View that sees that part of the layer.
It can also track DirectRadialLight instances, but only lights on the same SceneLayer. That is intentional. This helper does not implement cross-layer light spill.
The main design choice is whether the darkness/fog is about what a viewer can see or what exists in the world.
Use DirectDarknessOverlay when the question is:
What does this View/player/camera get to see?
Good fits:
- player vision
- fog-of-war reveal
- stealth visibility
- split-screen player-specific darkness
- minimap reveal rules
- per-camera night vision
Use DirectSceneLayerDarknessOverlay when the question is:
What fog/darkness exists at this place on this SceneLayer?
Good fits:
- actual fog
- smoke
- poison clouds
- room-local magical darkness
- layer-specific haze
- environmental mist that all players should see
The quick test is:
If two Views look at the same world area, should both Views see the same effect?
If yes, use SceneLayer-bound darkness.
If no, use View-bound darkness.
Examples:
| Effect | Better fit | Why |
|---|---|---|
| Player torch visibility | DirectDarknessOverlay |
Each player/View may have different visibility |
| Fog-of-war | DirectDarknessOverlay |
It represents player knowledge or exploration |
| Split-screen PvP darkness | DirectDarknessOverlay |
Each player can have separate reveal rules |
| Swamp mist | DirectSceneLayerDarknessOverlay |
It physically exists on the map |
| Poison gas cloud | DirectSceneLayerDarknessOverlay |
It has a world position and should be visible to any camera |
| Dark room region | DirectSceneLayerDarknessOverlay |
The room itself is dark, independent of the viewer |
The lighting helpers intentionally use Gondwana's existing coordinate model.
A light created at:
new PointF(520, 320)is centered at world pixel (520, 320) on its SceneLayer.
If the camera moves, the light remains attached to that world location.
If the view zooms, the light scales with the world.
The darkness overlay is attached to a View, so it covers the view's viewport.
var darkness = new DirectDarknessOverlay(
renderSurfaceHost,
mainView,
dungeonLayer,
"dungeon-darkness");The mainView argument says:
Cover this view with darkness.
The dungeonLayer argument says:
Use this scene layer when converting reveal source world positions into screen positions.
For most games, use your main gameplay layer as the projection layer.
SceneLayer-bound darkness is attached to a SceneLayer and has WorldBounds:
var layerFog = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(0, 0, 1024, 768),
"dungeon-fog");The rectangle is in world pixels on dungeonLayer.
If the camera moves, the fog/darkness remains attached to that world region.
If two Views can see that same world region, both Views see the same fog/darkness.
Reveal sources for DirectSceneLayerDarknessOverlay are also world-space:
layerFog.AddRevealSource(
centerWorldPx: new PointF(320, 240),
radiusWorldPx: 120f);Unlike DirectDarknessOverlay, there is no View argument and no projectionLayer argument. The overlay already belongs to one SceneLayer.
To add a basic torch-in-darkness effect, you need these existing game objects:
renderSurfaceHost
mainView
main gameplay SceneLayer, e.g. dungeonLayer
Then add:
using System.Drawing;
using Gondwana.Drawing.Direct;Then create:
- a light
- a darkness overlay
- a tracking link between them
var torch = new DirectRadialLight(
Color.FromArgb(180, 255, 190, 80),
renderSurfaceHost,
dungeonLayer,
new PointF(520, 320),
120f,
"torch-01");
var darkness = new DirectDarknessOverlay(
renderSurfaceHost,
mainView,
dungeonLayer,
"dungeon-darkness")
.SetDarknessOpacity(190);
darkness.TrackLight(torch);That is the smallest complete version of the paired effect.
For actual fog or world-local darkness, the checklist is slightly different:
renderSurfaceHost
SceneLayer where the fog/darkness lives
world-space Rectangle for the affected region
optional lights or reveal sources on that same SceneLayer
Then create a SceneLayer-bound overlay:
var layerFog = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(0, 0, 1024, 768),
"dungeon-layer-fog");Use this when you only want a visible glow and do not want to darken the screen.
using System.Drawing;
using Gondwana.Drawing.Direct;
var torch = new DirectRadialLight(
lightColor: Color.FromArgb(180, 255, 190, 80),
renderSurfaceHost: renderSurfaceHost,
sceneLayer: dungeonLayer,
centerWorldPx: new PointF(520, 320),
radiusWorldPx: 120f,
nickname: "torch-01");
torch.Intensity = 0.85f;
torch.FlickerEnabled = true;
torch.FlickerAmount = 0.08f;
torch.FlickerRefreshHz = 12;
torch.ZOrder = 10_000;What you should see:
- a warm amber glow centered at world pixel
(520, 320) - a bright center
- a soft fade toward the edge
- subtle flicker if
FlickerEnabledis true
This does not darken the rest of the scene. It only draws the glow.
On a bright scene, this may look subtle. On a dark scene, it should be obvious.
Use DirectLightLayer when you want a simple owner for multiple lights.
using System.Drawing;
using Gondwana.Drawing.Direct;
var lights = new DirectLightLayer(renderSurfaceHost, dungeonLayer);
var torchA = lights.AddTorchLight(
centerWorldPx: new PointF(256, 256),
radiusWorldPx: 120f,
nickname: "torch-a");
var torchB = lights.AddTorchLight(
centerWorldPx: new PointF(768, 256),
radiusWorldPx: 120f,
nickname: "torch-b");
var torchC = lights.AddTorchLight(
centerWorldPx: new PointF(512, 640),
radiusWorldPx: 160f,
nickname: "torch-c");Then configure the lights:
torchA.FlickerEnabled = true;
torchB.FlickerEnabled = true;
torchC.FlickerEnabled = true;
torchA.FlickerRefreshHz = 10;
torchB.FlickerRefreshHz = 12;
torchC.FlickerRefreshHz = 9;What you should see:
- three separate torch glows
- each has its own radius
- each can flicker independently
DirectLightLayer does not draw anything by itself. It owns the DirectRadialLight instances.
Use this when you want darkness and a player-visible area, but no warm torch glow.
using System.Drawing;
using Gondwana.Drawing.Direct;
var darkness = new DirectDarknessOverlay(
renderSurfaceHost,
mainView,
dungeonLayer,
"dungeon-darkness")
.SetDarknessColor(Color.Black)
.SetDarknessOpacity(190)
.SetInnerClearRadiusRatio(0.25f)
.SetMidpointRadiusRatio(0.65f)
.SetMidpointStrength(0.45f);
var playerVision = darkness.AddRevealSource(
centerWorldPx: playerWorldCenterPx,
radiusWorldPx: 180f,
nickname: "player-vision");During gameplay, move the reveal source as the player moves:
playerVision.MoveTo(playerWorldCenterPx);What you should see:
- the whole view is darkened
- the player has a soft circular visible area
- the visible area follows the player
- there is no warm glow, only a visibility hole
This is useful for player vision, stealth vision, fog reveal, or line-of-sight prototypes.
This is the common dungeon torch setup.
using System.Drawing;
using Gondwana.Drawing.Direct;
var torch = new DirectRadialLight(
Color.FromArgb(180, 255, 190, 80),
renderSurfaceHost,
dungeonLayer,
new PointF(520, 320),
120f,
"torch-01");
torch.FlickerEnabled = true;
torch.FlickerAmount = 0.08f;
torch.FlickerRefreshHz = 12;
torch.Intensity = 0.85f;
torch.ZOrder = 10_000;
var darkness = new DirectDarknessOverlay(
renderSurfaceHost,
mainView,
dungeonLayer,
"dungeon-darkness")
.SetDarknessColor(Color.Black)
.SetDarknessOpacity(190)
.SetInnerClearRadiusRatio(0.20f)
.SetMidpointRadiusRatio(0.62f)
.SetMidpointStrength(0.45f);
darkness.TrackLight(torch);TrackLight(torch) creates a reveal source that follows the light's:
- center
- radius
- intensity, unless intensity tracking is disabled
Now this:
torch.MoveTo(new PointF(600, 360));moves both:
the visible torch glow
the darkness reveal hole
TrackLight(...) is idempotent. Calling it again for the same light returns the existing reveal source and keeps the original tracking options. To change radiusScale, intensityScale, or trackIntensity, untrack and then track again.
What you should see:
- the whole viewport becomes dark
- the area around the torch is visible
- the reveal has a soft falloff
- the torch glow appears inside/around the revealed area
- moving the torch moves both the glow and the visibility hole
Use TrackLightLayer(...) when you want the darkness overlay to automatically reveal around every light in a DirectLightLayer.
using System.Drawing;
using Gondwana.Drawing.Direct;
var lights = new DirectLightLayer(renderSurfaceHost, dungeonLayer);
var darkness = new DirectDarknessOverlay(
renderSurfaceHost,
mainView,
dungeonLayer,
"dungeon-darkness")
.SetDarknessOpacity(200);
darkness.TrackLightLayer(lights);
var torchA = lights.AddTorchLight(new PointF(300, 240), 100f, nickname: "torch-a");
var torchB = lights.AddTorchLight(new PointF(700, 420), 140f, nickname: "torch-b");Because TrackLightLayer is active, both torches automatically get reveal sources.
Lights added later are also tracked:
var torchC = lights.AddTorchLight(new PointF(900, 500), 120f, nickname: "torch-c");Removing a light removes its tracked reveal source:
lights.Remove(torchB);What you should see:
- the view is darkened
- each torch creates a soft reveal hole
- each torch also draws its warm glow
- adding/removing lights updates the darkness overlay tracking
This is the cleanest setup for rooms with multiple torches or lamps.
A player-held torch is usually just a light that moves to the player's world center each update.
Setup:
using System.Drawing;
using Gondwana.Drawing.Direct;
var playerTorch = new DirectRadialLight(
Color.FromArgb(190, 255, 190, 80),
renderSurfaceHost,
dungeonLayer,
playerWorldCenterPx,
150f,
"player-torch");
playerTorch.FlickerEnabled = true;
playerTorch.FlickerAmount = 0.08f;
playerTorch.FlickerRefreshHz = 12;
playerTorch.Intensity = 0.90f;
playerTorch.ZOrder = 10_000;
var darkness = new DirectDarknessOverlay(renderSurfaceHost, mainView, dungeonLayer)
.SetDarknessOpacity(200);
darkness.TrackLight(
playerTorch,
radiusScale: 1.15f,
intensityScale: 1.0f);During gameplay:
playerTorch.MoveTo(playerWorldCenterPx);What you should see:
- the player carries a warm flickering light
- the visible area follows the player
- the reveal is slightly larger than the glow because
radiusScaleis1.15f
This pattern is useful for top-down dungeon games, cave scenes, night levels, and stealth prototypes.
Manual reveal sources are useful when visibility should exist without a visible light effect.
Examples:
- player vision
- enemy vision
- stealth detection radius
- magic detection
- scripted cutscene spotlight
- minimap reveal logic
var darkness = new DirectDarknessOverlay(renderSurfaceHost, mainView, dungeonLayer)
.SetDarknessOpacity(185);
var vision = darkness.AddRevealSource(
centerWorldPx: playerWorldCenterPx,
radiusWorldPx: 180f,
nickname: "player-vision");During gameplay:
vision.MoveTo(playerWorldCenterPx);Change the radius when a power-up is active:
vision.SetRadius(260f);Make the reveal weaker:
vision.Intensity = 0.50f;What you should see:
- the scene is dark
- the player has a visibility circle
- no warm glow is drawn
- lower intensity leaves some darkness inside the reveal
A temporary glow does not need a darkness overlay.
var pickupGlow = new DirectRadialLight(
Color.FromArgb(150, 120, 180, 255),
renderSurfaceHost,
dungeonLayer,
pickupWorldCenterPx,
80f,
"mana-pickup-glow");
pickupGlow.Intensity = 0.70f;
pickupGlow.FlickerEnabled = true;
pickupGlow.FlickerAmount = 0.05f;
pickupGlow.FlickerRefreshHz = 8;
pickupGlow.ZOrder = 9_000;When the pickup is collected:
pickupGlow.Dispose();What you should see:
- a small colored aura around the pickup
- optional subtle shimmer/flicker
- no darkness behavior unless you also use
DirectDarknessOverlay
Use DirectSceneLayerDarknessOverlay when the darkness or fog is a world effect that belongs to one SceneLayer.
This example creates a dark/fogged rectangular area on dungeonLayer:
using System.Drawing;
using Gondwana.Drawing.Direct;
var roomDarkness = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(256, 128, 512, 384),
"north-room-darkness")
.SetDarknessColor(Color.Black)
.SetDarknessOpacity(180)
.SetInnerClearRadiusRatio(0.20f)
.SetMidpointRadiusRatio(0.65f)
.SetMidpointStrength(0.45f);What you should see:
- only the world rectangle
(256, 128, 512, 384)is darkened - the effect moves with
dungeonLayeras the camera moves - any View looking at that part of the layer sees the same darkness
- content outside the rectangle is not affected
This is a good fit for an actual dark room or a fog bank that lives on the map.
You can add a manual reveal source to it:
var doorwayReveal = roomDarkness.AddRevealSource(
centerWorldPx: new PointF(320, 240),
radiusWorldPx: 96f,
nickname: "doorway-light");
doorwayReveal.Intensity = 0.75f;Manual reveal sources only cut through the SceneLayer darkness. They do not create warm glow by themselves.
SceneLayer-bound darkness can track DirectRadialLight instances on the same SceneLayer.
using System.Drawing;
using Gondwana.Drawing.Direct;
var lights = new DirectLightLayer(renderSurfaceHost, dungeonLayer);
var torch = lights.AddTorchLight(
centerWorldPx: new PointF(320, 240),
radiusWorldPx: 140f,
nickname: "room-torch");
torch.FlickerEnabled = true;
torch.FlickerAmount = 0.08f;
torch.FlickerRefreshHz = 12;
var roomDarkness = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(256, 128, 512, 384),
"north-room-darkness")
.SetDarknessOpacity(190);
roomDarkness.TrackLight(torch, radiusScale: 1.15f);Now the torch and the SceneLayer darkness reveal stay synchronized.
If the torch moves:
torch.MoveTo(new PointF(420, 260));the reveal hole moves with it.
What you should see:
dungeon layer content
+ bounded room darkness/fog
- soft reveal around room torch
+ warm radial torch glow
If the fog/darkness should reveal around every light in a DirectLightLayer, track the owner:
var lights = new DirectLightLayer(renderSurfaceHost, dungeonLayer);
var roomDarkness = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(0, 0, 1024, 768),
"layer-fog")
.SetDarknessOpacity(170);
roomDarkness.TrackLightLayer(lights, radiusScale: 1.10f);
lights.AddTorchLight(new PointF(256, 256), 120f, nickname: "torch-a");
lights.AddTorchLight(new PointF(640, 360), 160f, nickname: "torch-b");Lights added later are tracked automatically.
Lights removed from the DirectLightLayer remove their tracked reveal source.
SceneLayer-bound darkness only tracks lights from the same SceneLayer:
var backgroundFog = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
backgroundLayer,
new Rectangle(0, 0, 1024, 768));
// Throws: playerTorch belongs to dungeonLayer, not backgroundLayer.
backgroundFog.TrackLight(playerTorch);That is intentional. This helper does not implement cross-layer light spill.
Because DirectSceneLayerDarknessOverlay is a normal scene-layer direct drawing, you can move its world bounds.
var poisonCloud = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(400, 300, 240, 180),
"poison-cloud")
.SetDarknessColor(Color.FromArgb(80, 160, 90))
.SetDarknessOpacity(150)
.SetMidpointStrength(0.35f);Move the whole cloud by changing its world bounds:
var bounds = poisonCloud.DarknessWorldBounds;
bounds.Offset(1, 0);
poisonCloud.DarknessWorldBounds = bounds;Add a weak reveal source if something pushes the haze away:
var windGap = poisonCloud.AddRevealSource(
centerWorldPx: new PointF(480, 360),
radiusWorldPx: 80f,
nickname: "wind-gap");
windGap.Intensity = 0.35f;What you should see:
- a bounded colored haze/fog region on the SceneLayer
- it moves with the world and camera
- its dirty region follows the old and new world bounds
- any View looking at the cloud sees the same cloud
Controls how dark the overlay is.
darkness.SetDarknessOpacity(190);Suggested values:
| Value | Result |
|---|---|
60 |
very light haze |
80 |
light haze |
120 |
dim scene |
160 |
moody darkness |
190 |
dungeon darkness |
220 |
very dark |
240 |
nearly black |
Start around 180 to 200 for a dungeon.
Controls how far the player or light can see.
playerVision.SetRadius(180f);
torch.SetRadius(120f);Suggested values:
| Radius | Result |
|---|---|
60f |
small candle / item glow |
100f |
small torch |
150f |
player-held torch |
220f |
strong lantern / magic sight |
300f+ |
large beacon / scripted reveal |
Controls how much of the reveal circle is fully clear before falloff begins.
darkness.SetInnerClearRadiusRatio(0.20f);Suggested values:
| Value | Result |
|---|---|
0.00f |
fade starts immediately |
0.15f |
small clear center |
0.25f |
comfortable player vision |
0.40f |
large clear center |
0.65f |
mostly clear with soft edge |
Controls where the middle falloff stop occurs.
darkness.SetMidpointRadiusRatio(0.62f);Higher values make the reveal stay stronger farther from the center.
Suggested values:
| Value | Result |
|---|---|
0.40f |
aggressive falloff |
0.60f |
balanced falloff |
0.75f |
wide, soft falloff |
0.90f |
reveal stays strong almost to edge |
Controls how strong the reveal is at the midpoint.
darkness.SetMidpointStrength(0.45f);Higher values create a softer, wider reveal. Lower values make the edge fade more aggressively.
Suggested values:
| Value | Result |
|---|---|
0.20f |
sharp/dim falloff |
0.45f |
balanced torch falloff |
0.70f |
soft broad reveal |
1.00f |
very strong until edge |
Controls how strong the visible glow is.
torch.Intensity = 0.85f;Suggested values:
| Value | Result |
|---|---|
0.25f |
faint glow |
0.50f |
mild glow |
0.85f |
strong torch |
1.00f |
maximum glow |
Flicker should usually be subtle.
torch.FlickerEnabled = true;
torch.FlickerAmount = 0.08f;
torch.FlickerRefreshHz = 12;Suggested values:
| Property | Suggested range |
|---|---|
FlickerAmount |
0.04f to 0.12f
|
FlickerRefreshHz |
8 to 15
|
Avoid very high flicker values unless the effect is intentionally unstable, magical, or hazardous.
When tracking a light, you can scale the reveal independently:
darkness.TrackLight(
torch,
radiusScale: 1.25f,
intensityScale: 1.0f);This is useful because visible glow radius and gameplay visibility radius are not always the same thing.
Common choices:
| Setup | Meaning |
|---|---|
radiusScale: 1.0f |
reveal matches glow radius |
radiusScale: 1.15f |
reveal slightly larger than glow |
radiusScale: 0.80f |
glow spills beyond clear vision |
intensityScale: 0.75f |
reveal remains partially dark |
If a flickering light makes the darkness reveal pulse too much, disable intensity tracking:
darkness.TrackLight(
torch,
radiusScale: 1.0f,
intensityScale: 1.0f,
trackIntensity: false);The reveal will still follow position and radius, but it will not pulse with the torch's flicker.
DirectSceneLayerDarknessOverlay is bounded by DarknessWorldBounds / WorldBounds.
var layerFog = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(256, 128, 512, 384));A larger rectangle covers more of the world and costs more to redraw.
A smaller rectangle is better for localized room darkness, smoke clouds, and fog banks.
Suggested starting sizes:
| Use case | Bounds guidance |
|---|---|
| One room | room world rectangle |
| Smoke cloud | tight rectangle around smoke |
| Poison gas | hazard area plus soft edge padding |
| Whole layer haze | full visible gameplay area or full map region |
| Background mist | broad but low-opacity region |
If the effect is really meant to cover the whole screen regardless of camera position, prefer DirectDarknessOverlay.
DirectRadialLight is bounded in world space.
That means its dirty area is the light's world bounds:
center +/- radius
This works well with Gondwana's dirty-rectangle renderer because the light only affects a finite region.
When a light moves, the old and new bounds need to be redrawn. The helper handles that through its direct-drawing bounds.
DirectDarknessOverlay is different. It is view-sized.
When the View darkness overlay or one of its reveal sources changes, it generally refreshes the full target viewport. This is intentional. The overlay covers the whole view, so the simplest correct behavior is to redraw the whole view overlay.
DirectSceneLayerDarknessOverlay is in between those two. It is not full-view, but it can still be large.
Its dirty area is its WorldBounds:
DirectSceneLayerDarknessOverlay.WorldBounds
When a manual reveal source changes, or when a tracked light moves, the overlay refreshes its bounded world region. That is much better than refreshing the whole View when the fog/darkness is local to a room or cloud, but it is still more expensive than a small DirectRadialLight if the bounds are large.
Performance guidance:
- A few torch lights are fine.
- Flicker at
8to15Hz usually looks good. - Avoid making every light flicker at full frame rate.
- A full-view darkness overlay is more expensive than a small bounded light.
- A large SceneLayer darkness overlay is cheaper than full-view darkness only if its bounds are meaningfully smaller than the View or affected map area.
- Use one View darkness overlay per View unless you have a specific reason to do otherwise.
- Use SceneLayer darkness for bounded world fog, smoke, or room darkness.
- GPU-backed rendering is the natural long-term home for large animated overlays.
- Bitmap rendering can still handle this, but full-view and large-world effects should be used thoughtfully.
Practical starting point for player-view darkness:
1 darkness overlay per View
1 player-held torch
0 to 10 static torches
flicker at 8-12 Hz
moderate radii
Practical starting point for SceneLayer fog/darkness:
1 bounded SceneLayer overlay per fog/dark region
bounds no larger than necessary
manual reveal sources for static clear spots
tracked lights only when the fog should open around lights
These helpers are drawn through the DirectDrawing system, so ZOrder matters.
For View darkness, suggested ordering is:
tiles and sprites
lower-Z direct drawings
DirectRadialLight glow
DirectDarknessOverlay
HUD/widgets/debug overlays
Typical values:
torch.ZOrder = 10_000;
darkness.ZOrder = 20_000;For SceneLayer darkness, the ordering is local to the same SceneLayer:
tiles and sprites on this SceneLayer
DirectRadialLight glow on this SceneLayer
DirectSceneLayerDarknessOverlay on this SceneLayer
later SceneLayers / View overlays / HUD
Typical values:
torch.ZOrder = 10_000;
layerFog.ZOrder = 20_000;If the darkness appears above everything and hides the glow, either:
- the glow needs to be drawn after the darkness, or
- the reveal hole needs to expose the glow beneath, depending on the look you want.
For the common torch-in-darkness look, the practical intent is:
darkness overlay creates visibility
light glow provides warm color inside/around that visibility
If the result looks wrong, first check ZOrder.
Also check whether you used the right darkness type:
DirectDarknessOverlay
View-level; good for player visibility
DirectSceneLayerDarknessOverlay
SceneLayer-level; good for actual fog/darkness in the world
Direct drawings should be disposed when they are no longer needed.
For a standalone light:
torch.Dispose();For a temporary pickup glow:
pickupGlow.Dispose();For a View darkness overlay:
darkness.Dispose();For a SceneLayer darkness overlay:
layerFog.Dispose();For a light owner, use whatever cleanup helper is provided by DirectLightLayer, or dispose/remove the lights it owns.
Good times to clean up:
- leaving a level
- changing scenes
- removing a torch object
- collecting a glowing pickup
- ending a spell effect
- closing a cutscene overlay
- removing a fog bank, smoke cloud, or poison haze
If a light remains visible after it should be gone, make sure the corresponding DirectRadialLight was disposed or removed from its owner.
If a reveal hole remains visible after a light is gone, make sure the darkness overlay is no longer tracking that light, or remove the manual reveal source.
If SceneLayer fog remains visible after leaving a room or area, make sure the corresponding DirectSceneLayerDarknessOverlay was disposed or had its DarknessWorldBounds changed.
DirectRadialLight only draws the glow.
Add DirectDarknessOverlay if you want the rest of the scene darkened.
var darkness = new DirectDarknessOverlay(renderSurfaceHost, mainView, dungeonLayer)
.SetDarknessOpacity(190);
darkness.TrackLight(torch);Add at least one reveal source:
darkness.AddRevealSource(playerWorldCenterPx, 180f);or track a light:
darkness.TrackLight(torch);Also check that the reveal source is using the correct world position and that the overlay's projection layer is the layer where that world position makes sense.
Make sure you are moving the tracked DirectRadialLight, not just a separate game object.
torch.MoveTo(playerWorldCenterPx);If you are using manual reveal sources, move the reveal source directly.
playerVision.MoveTo(playerWorldCenterPx);Check the projectionLayer used by DirectDarknessOverlay.
var darkness = new DirectDarknessOverlay(
renderSurfaceHost,
mainView,
dungeonLayer,
"dungeon-darkness");The dungeonLayer should usually be the same layer that the light's world coordinates are based on.
Try increasing the light intensity:
torch.Intensity = 1.0f;Try increasing darkness opacity:
darkness.SetDarknessOpacity(210);Try increasing the reveal radius:
torch.SetRadius(160f);Try lowering darkness opacity:
darkness.SetDarknessOpacity(150);Try increasing midpoint strength:
darkness.SetMidpointStrength(0.70f);Try increasing midpoint radius:
darkness.SetMidpointRadiusRatio(0.75f);Reduce the amount:
torch.FlickerAmount = 0.04f;Lower the refresh rate:
torch.FlickerRefreshHz = 8;Or disable reveal intensity tracking:
darkness.TrackLight(
torch,
radiusScale: 1.0f,
intensityScale: 1.0f,
trackIntensity: false);You probably used DirectDarknessOverlay.
That overlay is View-specific and is usually correct for player vision or fog-of-war.
For actual fog that exists in the world and should be the same for every View, use DirectSceneLayerDarknessOverlay instead:
var layerFog = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(0, 0, 1024, 768));DirectSceneLayerDarknessOverlay only tracks lights on the same SceneLayer.
This is intentional.
// Works only when torch.SceneLayer == layerFog.SceneLayer.
layerFog.TrackLight(torch);If you need cross-layer spill later, that should be a larger lighting-system feature, not an accidental behavior in this helper.
Shrink its world bounds:
layerFog.DarknessWorldBounds = new Rectangle(256, 128, 512, 384);For room darkness, use the room's world rectangle.
For a smoke cloud, use a rectangle tightly around the cloud plus a little padding for soft edges.
Try:
- reducing darkness usage to one overlay per view
- lowering
FlickerRefreshHz - disabling reveal intensity tracking for flickering lights
- reducing the number of animated lights
- using smaller radii
- using fewer full-view overlays
- testing with GPU-backed rendering
Create directly:
var light = new DirectRadialLight(
Color.FromArgb(180, 255, 190, 80),
renderSurfaceHost,
dungeonLayer,
new PointF(520, 320),
120f,
"torch-01");Common members:
light.CenterWorldPx
light.RadiusWorldPx
light.LightColor
light.Intensity
light.EffectiveIntensity
light.BlendMode
light.FlickerEnabled
light.FlickerAmount
light.FlickerRefreshHz
light.MoveTo(new PointF(600, 360));
light.SetRadius(160f);
light.Dispose();Create an owner:
var lights = new DirectLightLayer(renderSurfaceHost, dungeonLayer);Add torch-style lights:
var torch = lights.AddTorchLight(
new PointF(520, 320),
120f,
nickname: "torch-01");Remove a light:
lights.Remove(torch);Create an overlay:
var darkness = new DirectDarknessOverlay(
renderSurfaceHost,
mainView,
dungeonLayer,
"dungeon-darkness");Tune it:
darkness
.SetDarknessColor(Color.Black)
.SetDarknessOpacity(190)
.SetInnerClearRadiusRatio(0.20f)
.SetMidpointRadiusRatio(0.62f)
.SetMidpointStrength(0.45f);Add manual reveal:
var vision = darkness.AddRevealSource(playerWorldCenterPx, 180f, "player-vision");
vision.MoveTo(playerWorldCenterPx);
vision.SetRadius(220f);
vision.Intensity = 0.75f;Track one light:
darkness.TrackLight(torch);Track one light with tuning:
darkness.TrackLight(
torch,
radiusScale: 1.15f,
intensityScale: 1.0f,
trackIntensity: false);Track a light owner:
darkness.TrackLightLayer(lights);Create a bounded SceneLayer darkness/fog overlay:
var layerFog = new DirectSceneLayerDarknessOverlay(
renderSurfaceHost,
dungeonLayer,
new Rectangle(0, 0, 1024, 768),
"layer-fog");Tune it:
layerFog
.SetDarknessColor(Color.Black)
.SetDarknessOpacity(180)
.SetInnerClearRadiusRatio(0.20f)
.SetMidpointRadiusRatio(0.65f)
.SetMidpointStrength(0.45f);Move or resize the affected world region:
layerFog.DarknessWorldBounds = new Rectangle(256, 128, 512, 384);Add manual reveal:
var reveal = layerFog.AddRevealSource(
new PointF(320, 240),
120f,
"room-reveal");
reveal.Intensity = 0.75f;Track one same-layer light:
layerFog.TrackLight(torch, radiusScale: 1.15f);Track a same-layer light owner:
layerFog.TrackLightLayer(lights, radiusScale: 1.10f);Dispose when done:
layerFog.Dispose();Use DirectRadialLight when you want a visible glow.
Use DirectLightLayer when you want to manage several glow lights conveniently.
Use DirectDarknessOverlay when you want a View darkened with soft visibility holes. This is the right fit for player vision, fog-of-war, split-screen visibility, and per-camera darkness.
Use DirectSceneLayerDarknessOverlay when you want bounded fog/darkness that physically belongs to one SceneLayer. This is the right fit for actual fog, smoke, poison haze, room darkness, and other world-local atmosphere.
Use TrackLight or TrackLightLayer when you want the glow and reveal hole to stay synchronized automatically.
The system is deliberately DirectDrawing-based, modular, and renderer-friendly. It gives Gondwana a practical first lighting/darkness workflow without turning the engine into a full lighting engine before it needs to be one. Cross-layer light spill, shadows, wall occlusion, and light bouncing remain intentionally out of scope.
- Home
- Make Your First Game in 30 Minutes
- Engine Architecture Overview
- Gondwana Engine Lifecycle
- Gondwana CLI Cheatsheet
- Assets Files
- Tilesheets
- Scenes and SceneLayers
- Sprites
- Views, Cameras, and Viewports
- DirectDrawing
- Game State Files
- Logging
- Movement and Controllers
- Input Handling
- Collision Detection
- Timers and Engine Timing
- Using the Effects System
- Engine Configuration