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.
103 lines
4.1 KiB
Markdown
103 lines
4.1 KiB
Markdown
# 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.
|