pixel shader dreams

Spent the afternoon rewriting the pixel shader pipeline. The old approach was brute-forcing per-pixel lighting calculations when we could batch them through a deferred rendering pass instead.

vertex fragment out deferred pass: batch lighting in screen-space

The new pipeline is roughly 3x faster in our test scene. Still need to handle transparency correctly — transparent objects need a forward pass layered on top.

// deferred lighting accumulation
for (light in scene.lights) {
    accumBuffer += calcLight(
        gBuffer.normal,
        gBuffer.albedo,
        light.position
    );
}

Tomorrow: integrate the shadow map atlas. The current per-light shadow approach won't scale past 8 dynamic lights.

tilemap overhaul

Completely rebuilt the tilemap system from scratch. The old autotile approach used a massive lookup table — 256 entries for each tile type. The new system uses a bitmask approach with Wang tiles instead.

bitmask: 0b1110 wang tile: NE variants: 3

Wang tiles cut our tile asset count by 60%. The bitmask lookup is O(1) and the visual quality is actually better — natural-looking transitions between terrain types without visible repetition.

fn resolve_tile(neighbors: u8) -> TileVariant {
    let wang_id = neighbors & 0b1111;
    let variant = hash(wang_id, pos) % VARIANT_COUNT;
    WANG_TABLE[wang_id][variant]
}

procedural clouds

Finally cracked the procedural cloud system. Layered Perlin noise with domain warping gives us clouds that feel organic and shift naturally with wind direction.

layer 1: base layer 2: detail layer 3: warp

Three noise layers: base shape (low frequency), detail edges (high frequency), and domain warp (shifts UV coordinates for organic movement). The warp layer is the secret sauce — without it, clouds look like blobs.

let warp = noise(uv * 0.3 + time * 0.02);
let warped_uv = uv + warp * 0.15;
let cloud = fbm(warped_uv, 4, 0.5);

Performance is good — runs at 60fps on integrated GPUs. The trick is computing the noise in a low-res buffer and upscaling with bilateral filtering.

dialogue system v2

Rewrote the dialogue system from a flat array to a proper graph structure. Each node can branch, loop, or gate on game state. The editor now shows connections visually.

start A B merge gate: hasItem("key") ? A : B

The graph approach means NPCs can remember past conversations. State gates check inventory, quest progress, even time-of-day. Feels much more alive than the linear scripts we had before.

struct DialogueNode {
    text: String,
    choices: Vec<Choice>,
    gate: Option<Condition>,
    on_enter: Vec<Effect>,
}

character controller

Rebuilt the character controller with coyote time and input buffering. The old controller felt stiff — missed jumps at ledge edges, unresponsive dash cancels. Now it feels buttery.

Key changes: 6-frame coyote window (100ms at 60fps), 4-frame input buffer for jump, and variable jump height via hold duration. Also added a subtle squash/stretch on land and launch.

const COYOTE_FRAMES: u32 = 6;
const BUFFER_FRAMES: u32 = 4;

if input.jump_pressed {
    jump_buffer = BUFFER_FRAMES;
}
if grounded || coyote_timer > 0 {
    if jump_buffer > 0 {
        velocity.y = JUMP_FORCE;
        apply_squash(0.8, 1.2);
    }
}

The squash/stretch is subtle but makes a huge difference in game feel. 20% compression on land, 20% stretch on jump. Spring back to normal over 6 frames.

sound design notes

Spent the day recording and processing foley for the forest biome. Kitchen pans + crinkled paper = surprisingly good "magical sparkle" effect when pitch-shifted and layered.

Sound palette for the forest area:

  • Ambient: layered wind recordings at different frequencies
  • Footsteps: gravel samples with randomized pitch (0.9 - 1.1x)
  • UI: soft bell tones, pentatonic scale
  • Combat: reversed cymbal crashes for "charge up" effect

The randomized pitch on footsteps prevents the "machine gun" repetition problem. Even 10% variation makes them sound completely natural. Also added velocity-based volume — walking is quieter than running.

fn play_footstep(velocity: f32) {
    let sample = footsteps.random();
    let pitch = rng.range(0.9, 1.1);
    let volume = lerp(0.3, 1.0, velocity / MAX_SPEED);
    audio.play(sample, pitch, volume);
}