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.
4.1 KiB
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.
powis not free;x*x*xis.
Verify against apps/arcade/BUDGETS.md with renderer.frame_p90_ms() before and after.