makepad/skills/makepad-aaa-graphics-builder/references/shader-cookbook.md
Arena Agent fe21c07d84 skills: add Makepad game skills pack ported from threejs-game-skills
Nine agent skills for building Makepad/Rust games, ported from
majidmanzarpour/threejs-game-skills. Same director-routed workflow and
premium bar; runtime rewritten for this fork's game crates.

- makepad-game-director        entrypoint, routing, continuity, asset probe
- makepad-gameplay-systems     loop, movers vs rigid bodies, input, camera, netplay
- makepad-aaa-graphics-builder lighting, shaders, budget, visual scorecard
- makepad-game-ui-designer     HUD, menus, touch and XR UI
- makepad-debug-profiler       defect bisection and profiling
- makepad-qa-release           verification ladder, evidence, packaging
- makepad-3d-generator         CC0 model search, casts, procedural geometry
- makepad-image-generator      texgen textures, palettes, sky, icons
- makepad-audio-generator      sample bank, mixer, material impacts, 3D audio

Written against the real APIs in libs/game/*, libs/sim and apps/arcade:
the game.* verb table, GameRenderer adaptive quality, the packed 6-float
GameMeshVertex layout, script_mod! splash styling, makepad-test driving,
and the BUDGETS.md numbers. No paid generation API is required - the CC0
library plus seeded makepad-game-gen replaces them.

Includes install.sh (Codex/Claude), validate-skills.sh and a repo-aware
probe_assets.sh; both scripts verified against this checkout.
2026-09-05 21:18:58 +00:00

4.1 KiB
Raw Permalink Blame History

Shader cookbook

The override seam

Shaders live in Rust (libs/game/render/src/shaders.rs, and the DrawGame* structs). Their look is overridden in the splash DSL by inheritance:

script_mod! {
    use mod.prelude.widgets_internal.*
    use mod.widgets.*

    mod.widgets.MyViewBase = #(MyView::register_widget(vm))
    mod.widgets.MyView = set_type_default() do mod.widgets.MyViewBase {
        width: Fill
        height: Fill
        draw_cube     += { light_dir: vec3(0.35, 0.8, 0.45) }
        draw_alpha    += { light_dir: vec3(0.35, 0.8, 0.45) }
        draw_terrain  += { light_dir: vec3(0.35, 0.8, 0.45) }
        draw_skinned  += { light_dir: vec3(0.35, 0.8, 0.45) }
        draw_models   += { light_dir: vec3(0.35, 0.8, 0.45) }
    }
}

The engine owns structure — where a vertex is, where a spark is, how long it lives. The override owns appearance. A generated or untrusted game can restyle freely without being able to break the simulation. Do not defeat this by moving gameplay constants into the DSL.

Set light_dir on every draw type at once. A skinned character lit differently from its terrain is the most common unexplained-wrongness bug here.

Vertex packing

Vertex attributes in this engine are f32-only. There is no u8/u16/i16 attribute type. Compression means bit-packing into f32 lanes and unpacking in the shader.

Builtins available on every backend (Metal / GLSL / HLSL / WGSL):

unpack2f16(x) -> vec2     // two half floats from one f32 lane
unpack4u8(x)  -> vec4     // four unorm bytes from one f32 lane

geom.GameMeshVertex (draw/geometry_gen.rs) is the shared packed layout:

field packing floats
position 3 × f32, exact 3
normal octahedral, 2 × f16 in one lane 1
uv 2 × f16 in one lane 1
colour 4 × unorm8 in one lane 1
total 6 / 24 B

geom.VectorVertexPacked is the house precedent. Use the shared layout; do not invent a fatter vertex for convenience.

Octahedral normals: map the unit sphere to a square, store 2 components, reconstruct the third in-shader. Error is negligible for shading and it halves the cost against three f32s.

Uniform vs instance

The rule that produced the measured 27% instance saving:

If a value is identical for every instance in a batch, it is a uniform, not an instance field.

sun_color, sun_sky, sun_ground, fog_color are uniforms. fog_density stayed per-instance only because shadows switch it off individually. Twelve floats of duplication per cube × every cube × every frame is what moving them saved.

Check your width from the compiled shader, not by counting:

stats.instance_floats   // RenderStats

Common overrides

Palette tint per instance — prefer ModelInstance::with_tint(vec4) over a shader variant. One shader, many looks.

Rim light — cheap hero separation, the fastest route from silhouette 2 to 3:

rim = pow(1.0 - abs(dot(normal, view_dir)), 3.0);
color += rim * rim_color * rim_strength;

Hemispheric ambient — the difference between "rendered" and "lit":

ambient = mix(sun_ground, sun_sky, normal.y * 0.5 + 0.5);

Fog — must match the sky at the horizon, or the world reads as pasted on a backdrop:

f = 1.0 - exp(-dist * fog_density);
color = mix(color, fog_color, f);

Firework look override (the house precedent in arcade_view.rs): real fireworks are symmetric. Pick one uniform radial angle per spark, then add swirl and fizzle so sparks look like burning matter rather than points on a sphere. The engine shader keeps the ballistic arc; the override supplies the wobble.

Cost guidance

Per-pixel work multiplies by resolution; per-vertex by mesh density; per-instance by draw count. On Quest, bandwidth binds before ALU, so:

  • Move maths from fragment to vertex where the result is smooth across a triangle.
  • Prefer one more ALU op to one more texture fetch.
  • Branching is cheap when the whole warp agrees, expensive when it splits — branch on a uniform, not on a per-pixel value.
  • pow is not free; x*x*x is.

Verify against apps/arcade/BUDGETS.md with renderer.frame_p90_ms() before and after.