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

103 lines
4.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```rust
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:
```rust
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.