← Writing · EndWise

Lighting

Hard-edged light shafts through windows and doorways. Furniture carves them as you drag — occluders are the same AABBs the level design solver maintains, including tip. Doorways use the same shaft treatment as windows.

FIG. 02

Occlusion from gameplay AABBs

Furniture carves the beam. Solver AABBs stream into the shader as occluders; mid-drag, the carved tunnel updates and the floor behind the piece lights up.
FIG. 02

Carved tunnels & the tip

Height is the other axis of the carve. The couch is two stacked AABBs (seat + backrest) that shadow each other; tip rotates the mattress a true 90° and lands it one lane past the edge.
FIG. 03

The carve, from eye level

What the player sees. One-point perspective of the same AABB carve: the shadow tunnel is a volume, and the beam still reads in front of the couch.

Froxel fog

Godot’s volumetric fog is a froxel grid: camera-aligned volume texture, sun shadow maps per cell, temporally smoothed. Soft edges only — a hard silhouette would sit between froxels and get smeared by the temporal filter.

Shafts are a separate system. Froxels stay at low resolution for ambient gloom.

Beams

Each beam is a parallelepiped — aperture rectangle sheared along the sun direction. A non-orthonormal shear basis maps world space to a unit cube in beam-local space; inside-test is three axis comparisons. No shadow map, no raymarch on the CPU path — ALU only; runs on Compatibility / web too.

# CPU: aperture right × up × sun-scaled depth → unit cube under xf
var shear := Basis(wall_tangent * w, Vector3.UP * h, sun_dir * depth)
var xf := Transform3D(shear, center + sun_dir * (depth * 0.5))
var inv := xf.affine_inverse()  # columns + origin uploaded as uniforms
// GPU: world → beam-local, then three comparisons against the unit cube
vec3 local = beam_cx[b] * p.x + beam_cy[b] * p.y + beam_cz[b] * p.z + beam_org[b];
if (all(lessThan(abs(local), vec3(0.5)))) {
    // fragment is inside this beam
}
FIG. 01

The shear basis

World-space window beam parallelepiped transforming into a unit cube in beam-local space for a slab test
Slanted volume, axis-aligned test. Non-orthonormal basis (u, v, w) turns “is this fragment lit?” into three comparisons — pure ALU, no shadow map.

Solver AABBs upload as occluders (slight shrink so pieces don’t self-shadow). Shadow tunnels update mid-drag. Godot has no max-blend; overlapping additive beams double-brighten, so one union-AABB cover mesh shades each fragment once — max across ≤10 beams / ≤32 occluders in the shader.

Current path: prism mesh per beam, raymarch inside, slab-test furniture AABBs per sample.

Bugs that shaped the path

Vanishing beam. Standard back-face culling: step inside a beam, look at the window — shafts disappear. Inside a convex hull every visible face is a back-face. Fix: cull front faces, shade back faces, march the segment between.

FIG. 03

The vanishing beam

Top-down comparison of back-face culling rejecting all visible hull faces from inside the beam versus front-face culling keeping far faces for raymarching
Inside a convex hull, every visible face is a back-face. Cull front faces instead, then march camera → hull exit. Sequel: depth_draw_never does not disable depth tests.

Missing chunks. Tipped mattress → wedges of beam behind it gone. depth_draw_never only suppresses depth writes, not tests — hull fragments behind furniture die in the z-buffer before the shader runs. Fix: depth_test_disabled + manual clamp against scene depth in the shader.

Flooded doorway. Haze volume at 2× room depth flooded the door opening. Fix: fit the fog volume to the room.

Sun + shadows

Window mullion shapes on furniture come from the ordinary sun + shadow map. Soft penumbras against hard shafts look wrong, so the sun uses zero angular distance and lower shadow filter quality.