agents/openai.yaml
interface:
display_name: "Three.js AAA Graphics Builder"
short_description: "Upgrade Three.js graphics quality"
default_prompt: "Use $threejs-aaa-graphics-builder to improve this game's graphics within its art direction and verify the result at gameplay scale."
references/authoring-recipes.md
# Authoring Recipes — Models, World, Render
Concrete recipes for building premium browser-game art in Three.js: what to model, how to lay the graphics code out, and how to set up the renderer behind it.
## Contents
- Modeling principles
- Minimum premium asset pass
- Hero vehicle / hero character / obstacle families / rewards / world prop kit
- Procedural geometry techniques
- Graphics architecture and factory contract
- Renderer, camera, lighting, shadows
- Fog, background, post-processing
## Modeling principles
- Silhouette first. A model should be recognizable as a dark shape before materials or glow.
- Combine primitive bases with authored geometry: extrusions, bevels, curves, tubes, lathes, custom buffers, decals, trim, instanced micro-detail.
- Asymmetry and functional parts: hinges, fins, vents, handles, rails, brackets, sensors, cables, panels, bolts.
- Detail goes where the camera looks — player-facing surfaces, not hidden undersides.
- State variants come from material swaps, animated child parts, emissive strips, and VFX sockets.
- Collision proxy stays separate from the detailed visual group.
- Shared geometries/materials and instancing for repeated bolts, panels, lights, windows, spikes, rocks, rail segments.
- Name important child meshes: `cockpitGlass`, `leftEngine`, `hazardTeeth`, `pickupCore`, `collisionProxy`.
## Minimum premium asset pass
A game asking for premium/AAA/showcase quality needs a design-derived asset pass:
- Its focal subject at actual camera scale, with the state cues the core loop needs. This may be a player model, a table and cue, or a set of puzzle pieces.
- Distinct forms and telegraphs for gameplay roles that players must distinguish. A wave game may need several enemies; pool does not need enemies at all.
- Authored interactables and feedback for the actions that exist, without inventing pickups or reward systems to satisfy a quota.
- A reusable world kit sized to the level plan, with enough variation to avoid accidental repetition at the playable camera distance.
- A coherent material kit appropriate to the art direction: trim, decals, roughness variation, or emissive masks where they serve the design.
- Collision proxies where needed and renderer diagnostics for the integrated scene.
Prove one representative playable scene before expanding the kit. The recipes below are options for their genres, not a required shopping list for every game.
## Hero vehicle
Runners, racers, hovercraft, spaceships, drones, arcade vehicles.
- Core hull: `ExtrudeGeometry` or a custom tapered `BufferGeometry`.
- Nose: wedge, intake, sensor strip, bumper, or blade.
- Cockpit: glass dome from sphere/lathe segments, beveled capsule, or faceted canopy.
- Engines: cylinders/cones/tubes with nozzle rings, inner emissive discs, heat fins, trail sockets.
- Wings/fins: extruded triangular or curved plates with bevel and trim lines.
- Undercarriage: skids, landing pads, rail clamps, suspension arms, thruster pods.
- Decals: panel lines, numeric marks, faction glyph, hazard ticks, bolts.
- State cues: boost flares, shield shell, damage scorch, pickup glow, overheat red.
- Collision proxy: one capsule/box/sphere group matching the gameplay footprint.
A box with two cylinders and a glow is a placeholder, not a hero.
## Hero character
Arena fighters, brawlers, platformers, stylized third-person.
- Body mass: torso, pelvis, head/helmet, limbs from tapered capsules and cylinders at custom scales.
- Rig illusion: separate shoulders, elbows, knees, wrists, ankles, belt, backpack, armor plates.
- Identity: visor, mask, hair or helmet crest, color-blocked silhouette, weapon or tool.
- Animation-ready pivots: group limbs under named joints even when animation is procedural.
- Material zones: skin, fabric, armor, metal, glass, emissive accents.
- State cues: hit flash material, shield ring, attack trail socket, stamina/charge glow.
- Collision proxy: capsule or cylinder independent of mesh detail.
Stacked spheres with no costume, joints, or silhouette is a placeholder.
## Obstacle and enemy families
Distinct gameplay reads, each with a unique silhouette, a danger material cue, a telegraph visible from distance, an animation or state change, a collision proxy, and low-cost repeated detail:
- Low barrier: ground-hugging slab, spikes, rails, caution panels, animated warning light.
- Gate/arch: overhead frame, side posts, pulsing pass/avoid lane, moving shutters.
- Moving hazard: rotating arm, sweeper beam, drone, crusher, sliding block, orbiting mines.
- Trap/zone: laser grid, electric puddle, collapsing tile, gravity well, proximity mine.
- Enemy: body core, sensor/head, weapon, shield, locomotion or hover base, attack telegraph.
Recolored cubes and cones are one variant, not a family.
## Rewards and interactables
Readable and desirable while the player is moving.
- Token: outer ring, inner core, value icon, shimmer cards, collect burst socket.
- Shard: faceted crystal, metal bracket, orbiting chips, emissive seam.
- Capsule: glass shell, suspended item, end caps, rotating label strip.
- Power-up: icon silhouette matched to its effect; color and shape differ from score pickups.
- Objective item: larger scale, unique motion, UI echo, stronger lighting and VFX.
For moving collectibles, useful states are idle (rotation, pulse, bob), attract (when attraction is a real mechanic), and collect (vanish, burst, score trail, HUD update). Other interactables use their own transitions, such as aim/contact/settle for a ball or hover/place/upgrade for a tower.
## World prop kit
Modular, instanceable, recombinable:
- Track/road: lane plates, seams, arrows, side rails, guard segments, repair panels.
- Arena: boundary rings, floor tiles, spawn pads, cover blocks, goal markers.
- City/sci-fi: window strips, antennas, rooftop units, bridge trusses, pylons, billboards.
- Nature: rocks from custom faceted buffers, cliffs, roots, crystals, grass cards.
- Industrial: pipes, vents, cables, tanks, crates, gantries, lights, warning signs.
- Space/air: debris panels, satellites, buoys, asteroid chunks, parallax dust.
Layer it: near props create speed and scale, mid props define the playable corridor, far props create depth without stealing draw calls. Build the world as play / near / mid / far / motion layers, and keep every layer clear of threats and the next decision.
## Procedural geometry techniques
| Class | Use for |
| --- | --- |
| `ExtrudeGeometry` | panels, fins, wings, badges, glyphs, signs |
| `LatheGeometry` | capsules, domes, engines, pipes, turret bases |
| `TubeGeometry` | cables, rails, trails, conduits, curved weapons |
| custom `BufferGeometry` | tapered hulls, rocks, shards, wedges, low-poly terrain |
| `ShapeGeometry` | decals, flat icons, trim strips, hazard markers |
| `InstancedMesh` | windows, bolts, lane markers, debris, grass, lights |
| `LOD` | hero/background variants, dense prop reductions |
When real bevel geometry is too expensive, fake it: duplicate thin trim meshes, edge strips, or slightly offset darker panels.
Use roughness/metalness contrast rather than hue contrast alone; emissive for authored signals rather than whole objects; glass and clearcoat sparingly on hero details; a darker contact material under important objects; decals to imply scale and function; UI icon shapes reused as world decals for cohesion.
## Graphics architecture
Keep these concepts separate even when a small project puts several in one file: materials, authored geometry, repeated props, effects, render settings, diagnostics.
```text
src/assets/MaterialLibrary.ts src/assets/ProceduralTextures.ts
src/assets/DecalShapes.ts src/assets/ImportedAssetRegistry.ts
src/assets/modelFactories/{Hero,Obstacle,Reward}Factory.ts
src/assets/modelFactories/WorldPropKit.ts
src/systems/LightingRig.ts src/systems/RenderPipeline.ts
src/systems/VfxSystem.ts src/systems/QualityDiagnostics.ts
```
Factories return a group plus metadata:
```ts
type ModelFactoryResult = {
root: THREE.Group;
collision?: THREE.Object3D;
lod?: THREE.LOD;
bounds?: THREE.Box3;
diagnostics?: { meshes: number; materials: number; geometries: number; triangles?: number };
};
```
Imported GLB/FBX models get a loader wrapper returning the same shape plus animation clips. Generation API calls never appear in browser runtime code.
Procedural texture and decal kit — canvas textures, shape geometry, or thin offset meshes for panel lines and hatches, trim sheets and edge bands, window strips and city light grids, hazard stripes and arrows and lane glyphs, scratches and wear and scorch. Set filtering, mipmaps, repeat/wrap, color space, and anisotropy deliberately; avoid unique full-size textures for tiny repeated marks.
## Renderer and camera
- `renderer.outputColorSpace = THREE.SRGBColorSpace`.
- Tone mapping chosen deliberately: `ACESFilmicToneMapping` suits cinematic stylized scenes; simpler tone mapping can read better for bright arcade games.
- Tune exposure against active gameplay, not a static title view.
- Cap DPR: start at `Math.min(devicePixelRatio, 1.5)` on mobile, `2` on desktop, then profile.
- Resize updates canvas, renderer, camera, composer, and UI CSS variables together.
- Camera keeps the next decision visible — player, immediate threat or reward, and route — with foreground speed elements, playable midground, and background scale cues. Check mobile framing separately; narrow layouts usually need different offsets.
## Lighting and shadows
A small readable stack: key (defines form), fill (keeps gameplay objects legible), rim/back (separates player and hazards from background), practical/emissive (beacons, engines, pickups, arena markers), and contact shadows or blobs (grounding).
Real shadows go to hero, major hazards, and large world anchors. Use smaller shadow maps and fewer casters when profiling shows cost; cheap contact discs or transparent planes for pickups and hovering objects; tune bias against acne and peter-panning. Prefer baked-looking emissive cues, light cards, and small unlit decals over many unmeasured dynamic lights.
## Fog, background, post
Fog reveals depth and mood; it does not stand in for an empty world. Layer background silhouettes at varied scales and heights, add parallax for motion-heavy games, and keep hazards and rewards readable against fog values.
Post is a finishing pass: bloom on authored emissive elements only, subtle vignette, low-opacity grain, chromatic aberration only for brief event-driven impacts, and geometry trails in preference to motion blur. Compare screenshots with post on and off, and profile the cost — concrete chain settings are in `shader-cookbook.md`.
When performance drops, cut post and shadow cost first, then cull/LOD/instance, then reduce asset density where it is least visible.
references/shader-cookbook.md
# Shader And Material Cookbook
Concrete material, shader, and post-processing recipes. Use with `technical-art.md` (budgets, when shader work is justified) and `authoring-recipes.md` (render pipeline, lighting, fog, post).
Targets three.js `^0.184`; imports use the `three/addons/*` alias (maps to `examples/jsm`). Every entry lists **When**, **Cost** (draw calls / fill rate / compile), and **Read** (the rule: effects clarify gameplay, never hide missing geometry).
## Prerequisite: Renderer And Environment Map
Metals and glossy dielectrics read as flat gray without an environment map to reflect. Set this up once before PBR materials.
```ts
import * as THREE from 'three';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping; // simpler tone mapping for bright arcade reads
renderer.toneMappingExposure = 1.0;
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
// 5-line env map: neutral studio IBL, no HDR file needed.
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture; // r184: RoomEnvironment() takes no args
scene.environmentIntensity = 1.0; // r184 global multiplier over per-material envMapIntensity
pmrem.dispose();
```
Needed by any scene with metal, ceramic, glass, or clearcoat. Cost is one PMREM bake at startup, near-zero per frame; it lights support surfaces so hero emissive/trim still wins attention.
## PBR Material Recipes
Copy the config; tune `color` to the palette. `envMapIntensity` values assume the env map above. Use `MeshStandardMaterial` unless a `MeshPhysicalMaterial`-only feature (clearcoat, transmission, sheen) is visible during play.
```ts
// Painted metal (car body, ship hull panel) — dielectric paint over metal read via clearcoat.
new THREE.MeshPhysicalMaterial({ color: 0x1f6feb, metalness: 0.0, roughness: 0.5,
clearcoat: 0.9, clearcoatRoughness: 0.15, envMapIntensity: 1.0 });
// Bare brushed metal (raw steel, gun frame) — needs the env map to look metallic.
new THREE.MeshStandardMaterial({ color: 0xaeb4bd, metalness: 1.0, roughness: 0.4, envMapIntensity: 1.1 });
// Rubber / tire — near-black, no reflection, kills the env map.
new THREE.MeshStandardMaterial({ color: 0x0a0a0b, metalness: 0.0, roughness: 0.92, envMapIntensity: 0.35 });
// Matte plastic (housings, crates) — dielectric, mid roughness, muted reflection.
new THREE.MeshStandardMaterial({ color: 0xd23b3b, metalness: 0.0, roughness: 0.62, envMapIntensity: 0.6 });
// Glossy ceramic / clean hull — sharp reflection, add clearcoat for wet-look premium.
new THREE.MeshPhysicalMaterial({ color: 0xf5f5f5, metalness: 0.0, roughness: 0.12,
clearcoat: 1.0, clearcoatRoughness: 0.05, envMapIntensity: 1.0 });
// Emissive signal (beacon, pickup core) — dark base so only the glow reads; >1 intensity feeds bloom.
new THREE.MeshStandardMaterial({ color: 0x101010, emissive: 0x18e0ff, emissiveIntensity: 2.5,
metalness: 0.0, roughness: 0.4 });
// Cloth / fabric — high roughness + sheen for the soft edge highlight.
new THREE.MeshPhysicalMaterial({ color: 0x3a4a6b, metalness: 0.0, roughness: 0.9,
sheen: 1.0, sheenRoughness: 0.5, sheenColor: new THREE.Color(0x8899bb), envMapIntensity: 0.5 });
```
**Read:** separate roles by roughness/metalness contrast (matte vs glossy, metal vs plastic), not hue alone.
### Glass: real vs fake
```ts
// REAL refractive glass — MeshPhysicalMaterial transmission.
new THREE.MeshPhysicalMaterial({ metalness: 0.0, roughness: 0.05, transmission: 1.0,
thickness: 0.5, ior: 1.5, envMapIntensity: 1.0 });
```
- **When:** one or two hero surfaces (cockpit canopy, potion vial) at close range.
- **Cost:** high. Each transmissive material triggers an extra scene render into a transmission buffer every frame; fill-rate heavy and multiplies with resolution. Never use on repeated/instanced props.
- **Read:** refraction must not smear the hazard behind it into unreadability.
```ts
// CHEAP fake glass — no transmission buffer. Use for repeated windows, visors, shields.
new THREE.MeshPhysicalMaterial({ color: 0x88ccff, metalness: 0.0, roughness: 0.1,
transparent: true, opacity: 0.25, clearcoat: 1.0, envMapIntensity: 1.5, depthWrite: false });
```
- **Cost:** one transparent draw call, no extra render target. Add the fresnel rim below for a readable edge.
## onBeforeCompile Patterns
Inject GLSL into stock materials to keep PBR lighting for free. Rules that make this safe with shared materials:
- **Cache key:** any material whose `onBeforeCompile` injects code MUST set `customProgramCacheKey` returning a string unique to that injection. Without it three can hand back a cached program compiled from a different (un-injected) material of the same type, silently dropping your code.
- **Sharing:** `onBeforeCompile` runs once per compiled program. Reuse one material instance across meshes and its uniforms update once for all. For per-object variation, use separate material instances (same cache key → program is still reused) or drive it from `instanceMatrix` / `instanceColor`.
- **Animating uniforms:** `onBeforeCompile` fires once, so stash the shader (`material.userData.shader = shader`) and write the uniform each frame: `if (m.userData.shader) m.userData.shader.uniforms.uTime.value = t;`. The snippets below use this pattern.
### (a) Fresnel rim glow
`vNormal` and `vViewPosition` (both view space) exist in the Standard/Physical fragment shader; `saturate` is defined in `<common>`.
```ts
material.onBeforeCompile = (shader) => {
shader.uniforms.uRimColor = { value: new THREE.Color(0x33ccff) };
shader.uniforms.uRimPower = { value: 3.0 };
shader.uniforms.uRimStrength = { value: 1.5 };
shader.fragmentShader =
'uniform vec3 uRimColor;\nuniform float uRimPower;\nuniform float uRimStrength;\n' +
shader.fragmentShader.replace(
'#include <emissivemap_fragment>',
`#include <emissivemap_fragment>
float fres = pow(1.0 - saturate(dot(normalize(vNormal), normalize(vViewPosition))), uRimPower);
totalEmissiveRadiance += uRimColor * fres * uRimStrength;`
);
};
material.customProgramCacheKey = () => 'fresnel-rim';
```
- **When:** shields, cloak/invuln states, silhouette separation from a busy background.
- **Cost:** a few ALU ops, no extra passes.
- **Read:** the rim marks a state change; keep base color readable when the rim is off.
### (b) Scrolling emissive panels
Inject a private UV varying so it works without a map assigned.
```ts
material.onBeforeCompile = (shader) => {
shader.uniforms.uTime = { value: 0 };
shader.uniforms.uPanelColor = { value: new THREE.Color(0x18e0ff) };
material.userData.shader = shader;
shader.vertexShader = 'varying vec2 vCookUv;\n' + shader.vertexShader.replace(
'#include <begin_vertex>', '#include <begin_vertex>\n vCookUv = uv;');
shader.fragmentShader =
'uniform float uTime;\nuniform vec3 uPanelColor;\nvarying vec2 vCookUv;\n' +
shader.fragmentShader.replace(
'#include <emissivemap_fragment>',
`#include <emissivemap_fragment>
float scroll = fract(vCookUv.y * 6.0 - uTime * 0.5);
float band = smoothstep(0.46, 0.5, scroll) * smoothstep(0.54, 0.5, scroll);
totalEmissiveRadiance += uPanelColor * band * 2.0;`
);
};
material.customProgramCacheKey = () => 'scroll-emissive';
```
- **When:** energy conduits, reactor walls, loading/charge bars, boost lanes.
- **Cost:** one `fract`/`smoothstep`, no textures.
- **Read:** scroll direction/speed should encode state (charging up, draining down).
### (c) Wind sway (foliage / flags)
`transformed` is object space and displaced before `<project_vertex>` applies `instanceMatrix`, so read the instance translation column for per-instance phase. Assumes model origin at the base, up = +Y.
```ts
material.onBeforeCompile = (shader) => {
shader.uniforms.uTime = { value: 0 };
material.userData.shader = shader;
shader.vertexShader = 'uniform float uTime;\n' + shader.vertexShader.replace(
'#include <begin_vertex>',
`#include <begin_vertex>
#ifdef USE_INSTANCING
float phase = instanceMatrix[3].x + instanceMatrix[3].z; // instance world offset
#else
float phase = 0.0;
#endif
float h = max(position.y, 0.0); // base stays planted, tips move most
transformed.x += sin(uTime * 1.5 + phase) * 0.08 * h;
transformed.z += cos(uTime * 1.1 + phase) * 0.05 * h;`
);
};
material.customProgramCacheKey = () => 'wind-sway';
```
- **When:** grass cards, banners, antennae, kelp — background life, not gameplay geometry.
- **Cost:** two trig ops per vertex; free on an `InstancedMesh`.
- **Read:** sway is ambient motion; never move collidable/interactable geometry with it.
### (d) Dissolve / spawn
Threshold-discard with a glowing edge. Inject a local-position varying and a hash; drive `uProgress` 0→1 to despawn, 1→0 to spawn.
```ts
material.onBeforeCompile = (shader) => {
shader.uniforms.uProgress = { value: 0 };
shader.uniforms.uEdgeColor = { value: new THREE.Color(0xff6a00) };
material.userData.shader = shader;
shader.vertexShader = 'varying vec3 vDisPos;\n' + shader.vertexShader.replace(
'#include <begin_vertex>', '#include <begin_vertex>\n vDisPos = position;');
shader.fragmentShader =
`uniform float uProgress;\nuniform vec3 uEdgeColor;\nvarying vec3 vDisPos;
float hash13(vec3 p){ p = fract(p * 0.1031); p += dot(p, p.yzx + 33.33); return fract((p.x + p.y) * p.z); }\n` +
shader.fragmentShader.replace(
'#include <dithering_fragment>',
`float n = hash13(floor(vDisPos * 12.0));
if (n < uProgress) discard;
float edge = smoothstep(uProgress, uProgress + 0.08, n);
gl_FragColor.rgb += uEdgeColor * (1.0 - edge) * 3.0; // post-tonemap add feeds bloom
#include <dithering_fragment>`
);
};
material.customProgramCacheKey = () => 'dissolve';
```
- **When:** enemy death, teleport-in, pickup spawn, object streaming.
- **Cost:** one hash + `discard` (discard disables early-Z; keep it to spawning objects, not the whole scene).
- **Read:** the edge color/direction telegraphs the event — spawn vs destroy must look different.
## Gradient Sky Dome
Cheaper than a cubemap for stylized scenes: a `BackSide` sphere with a top/horizon lerp plus a sun disc and halo.
```ts
const skyUniforms = {
uTop: { value: new THREE.Color(0x3a6fb0) },
uHorizon: { value: new THREE.Color(0xcfe4f5) },
uSunColor: { value: new THREE.Color(0xfff2cc) },
uSunDir: { value: new THREE.Vector3(0.4, 0.28, 0.6).normalize() },
};
const sky = new THREE.Mesh(
new THREE.SphereGeometry(500, 32, 16),
new THREE.ShaderMaterial({
side: THREE.BackSide, depthWrite: false, uniforms: skyUniforms,
vertexShader: `varying vec3 vDir;
void main(){ vDir = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: `varying vec3 vDir;
uniform vec3 uTop, uHorizon, uSunColor, uSunDir;
void main(){
float h = clamp(vDir.y * 0.5 + 0.5, 0.0, 1.0);
vec3 col = mix(uHorizon, uTop, pow(h, 0.6));
float d = clamp(dot(normalize(vDir), normalize(uSunDir)), 0.0, 1.0);
col += uSunColor * (pow(d, 800.0) + pow(d, 8.0) * 0.25); // disc + halo
gl_FragColor = vec4(col, 1.0);
}`,
})
);
sky.frustumCulled = false;
scene.add(sky);
```
- **When:** any stylized outdoor scene without a photographic backdrop.
- **Cost:** one draw call, no cubemap textures, no mips.
- **Read:** a raw `ShaderMaterial` bypasses tone mapping and sRGB conversion — author colors in display space; if the scene runs ACES, nudge them brighter. Keep horizon value distinct from hazards silhouetted against it.
## Post-Processing Chain
Finishing pass only. Move tone mapping/sRGB to `OutputPass`; keep `renderer.toneMapping` set so it reads it.
```ts
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// UnrealBloomPass(resolution, strength, radius, threshold)
const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight),
0.45, // strength: 0.35-0.6
0.3, // radius: 0.2-0.4
0.85); // threshold: only pixels brighter than this bloom
composer.addPass(bloom);
const VignetteShader = {
uniforms: { tDiffuse: { value: null }, uStrength: { value: 0.85 }, uSize: { value: 0.72 } },
vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: `uniform sampler2D tDiffuse; uniform float uStrength, uSize; varying vec2 vUv;
void main(){
vec4 c = texture2D(tDiffuse, vUv);
float d = distance(vUv, vec2(0.5));
c.rgb *= mix(1.0, smoothstep(uSize, uSize - 0.45, d), uStrength);
gl_FragColor = c;
}`,
};
composer.addPass(new ShaderPass(VignetteShader));
composer.addPass(new OutputPass()); // ALWAYS last: tone mapping + sRGB
// loop: composer.render() instead of renderer.render()
// resize: composer.setSize(w, h); composer.setPixelRatio(Math.min(devicePixelRatio, 2));
```
- **Bloom rule:** bloom sells authored emissive (threshold 0.85 keeps mid-bright materials out). It must never be the main source of detail — if a shape only reads because it glows, the geometry is missing.
- **Vignette cost:** one full-screen ShaderPass; keep `uStrength` subtle, never darken the play path.
- **Mobile:** the composer allocates full-resolution HDR targets, so cost scales with DPR². Cap DPR before adding passes. On low-end, skip the composer (call `renderer.render`) or run bloom-only via `composer.setPixelRatio(Math.min(devicePixelRatio, 1.25))`. Compare screenshots with post on/off and profile.
## Cheap Tricks
- **Vertex-color AO** — bake occlusion into the mesh: `geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))`, material `{ vertexColors: true }`, darken cavities/creases. **When:** static props/terrain. **Cost:** one attribute, zero draw calls. **Read:** ground shapes with contact darkness; don't tint gameplay-critical surfaces.
- **Polygon-offset decals** — coplanar decal mesh with `{ polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1, transparent: true }` to kill z-fighting. **When:** panel lines, numbers, faction glyphs. **Cost:** +1 draw call each — instance repeats. **Read:** decals imply function/scale, not random surface noise.
- **Fake contact shadow** — flat `PlaneGeometry` with a radial-gradient `CanvasTexture`, `{ transparent: true, depthWrite: false }`, under the object; scale/fade alpha with height. **When:** hovering or moving props that don't warrant a shadow map. **Cost:** one draw call, no shadow pass. **Read:** anchors floating objects so position reads.
- **Emissive LOD signals** — swap `emissiveIntensity` or material by distance so far pickups/beacons still read, add geometry detail only up close. **When:** dense repeated signals. **Cost:** a material/uniform swap. **Read:** keep the signal color constant across LOD so identity survives the transition.
- **Matcap props** — `new THREE.MeshMatcapMaterial({ matcap })` bakes lighting into one texture; no lights or env needed. **When:** background/stylized props. **Cost:** the cheapest lit-looking material, one texture. **Read:** matcap ignores scene lights, so never use it where a dynamic light or state glow must show on the surface.
references/technical-art.md
# Technical Art For Three.js Games
Readable authored detail that survives active gameplay, mobile viewports, and WebGL budgets — not maximum detail.
## Render budget starting points
Starting contracts, not universal limits. Measure on the target game; document every deliberate overrun as a tradeoff. The canvas inspector (`npm run inspect:canvas`) compares live diagnostics against these numbers and reports over-budget rows.
| Metric (worst active-play view) | Desktop | Mobile |
| --- | --- | --- |
| Draw calls (`info.render.calls`) | <= 300 | <= 150 |
| Triangles (`info.render.triangles`) | <= 750k | <= 300k |
| Geometries (`info.memory.geometries`) | <= 300 | <= 200 |
| Textures (`info.memory.textures`) | <= 60 | <= 40 |
| Texture memory (est.) | <= 256 MB | <= 128 MB |
| Shadow-casting lights | <= 2 | 1 |
| Shadow map size | <= 2048 | <= 1024 |
| DPR cap | 2 | 1.5-2 |
| Post passes (beyond render+output) | <= 2 | 0-1 |
Where to spend: draw calls go to instanced or material-merged repeats; triangles go to silhouettes near the camera, with LOD or impostors behind; unique material count grows faster than geometry count, so share roles aggressively; real shadows go to hero objects and grounding anchors, with blob/contact meshes for small repeated props (cheap contact-shadow recipe in `shader-cookbook.md`).
Report actual diagnostics after a graphics pass: calls, triangles, geometries, textures, materials, post passes, shadow settings, DPR cap, and the bottleneck.
## Material kit
Named shared roles, reused across every mesh that plays the same part — not one-off colors:
`bodyPrimary` (dominant shell) · `bodySecondary` (panel contrast) · `trim` (rails, bevels, edge highlights) · `hazard` (danger, damage, warning stripes) · `reward` (collectibles) · `shieldBoost` (shield/boost/status) · `glass` (cockpit, lens, visor) · `emissiveSignal` (authored glow strips, status lights) · `groundContact` (dark matte, shadow receivers) · `decalDark` / `decalLight` (panel lines, scratches, numbers).
UI signal colors and world signal colors come from the same set.
`MeshStandardMaterial` for most surfaces; `MeshPhysicalMaterial` selectively for cockpit glass, clearcoat panels, iridescent shields, hero details.
Shader and `onBeforeCompile` work earns its place through state readability (shield ripple, heat, cloak, damage pulse), surface identity (water, forcefield, hologram, energy core), cheap procedural variation replacing textures, or separating player/threat/reward from background. Use the proven values and GLSL patterns in `shader-cookbook.md` rather than improvising.
## VFX
Event-driven, tied to gameplay state, pooled, with geometries and materials reused:
- Pickup: ring contraction, shard burst, score trail, brief HUD echo.
- Hit/fail: impact ring, debris, damage flash, hit pause, camera impulse.
- Boost/speed: engine trail, lane streaks, FOV ease, side streaks, audio pitch.
- Near miss/combo: side spark, line snap, badge pulse, streak counter.
- Shield: refractive shell, rim pulse, absorbed-impact ripple, material swap.
- Spawn/despawn: anticipation pulse, telegraph, dissolve or scale snap.
Each effect should point at the player, a threat, a reward, or an impact, and clear the collision volume, the HUD, and the next decision. Heavy shake and strobe need a reduced-motion fallback.
Threats read differently from rewards by shape and motion, not only hue; interactables separate from background by silhouette, value, and material. Anything conveyed by color alone needs a shape, icon, or motion backup.
## Instancing, LOD, culling
Instance many copies sharing geometry and material with varying transforms: windows, bolts, lane markers, city lights, debris, stars, crowd cards, track panels, repeated pickups, background modules.
- Set `instanceMatrix.needsUpdate` / `instanceColor.needsUpdate` once after a batch of changes, not per instance.
- Recompute bounds for instanced groups when transforms move materially.
- Different materials or constantly changing transforms erase the win — instancing is not free.
- Collision stays separate from instanced visual detail.
LOD earns its place when an object spans large distance ranges, when the silhouette only matters near camera, or when an imported model is heavier than a background role needs. Add hysteresis or distance gaps so transitions do not pop, and check them under gameplay camera motion rather than static orbit.
## Imported and generated asset cleanup
For every imported GLB/FBX hero asset: confirm scale, pivot, forward/up orientation, bounds, and active-play silhouette; build a collision proxy independent of the visual mesh; inspect file size, triangles, mesh/material/texture counts, and animation clips; simplify excessive materials and textures; add an LOD or simplified variant when reused many times; check PBR readability under the game's own lighting rather than a model viewer.
API keys and temporary download URLs stay out of client code and out of checked-in files.
## Surface detail
Reusable systems beat one-off geometry: canvas-generated trim sheets for panel lines, markings, arrows and numbers; thin offset decal meshes for hazard marks, faction symbols, lane glyphs, scuffs; shared small noise/wear textures instead of unique full-size images; procedural UV-independent detail for repeated hard-surface props.
Surface detail reinforces scale, function, faction, route, or state.
references/visual-scorecard.md
# Visual Scorecard
Score active-play screenshots — not title screens, not isolated showroom models. Desktop and mobile when mobile is in scope.
## Calibration anchors
Packaged in `threejs-aaa-graphics-builder/assets/scorecard-anchors/`. View them before scoring World, Hero, Materials, or Lighting:
- `scene-1.jpg` — **1**: primitive player and pickups, flat sparse arena, utility HUD.
- `scene-2.jpg` — **2**: authored track kit, imported hero asset, designed genre HUD, intentional lighting.
- `scene-3.jpg` — **2.5–3**: dense layered world in active play, readable hero silhouette, event VFX, cohesive HUD.
If a surface reads closer to `scene-1` than `scene-3`, it is a 1–2 no matter how much code went into it.
## Categories
Scale: **0** placeholder / no evidence · **1** basic styled · **2** premium stylized · **3** showcase.
| Category | 1 | 2 | 3 |
| --- | --- | --- | --- |
| Art direction | theme is mostly colors and fog | theme drives forms, materials, UI, world, feedback | distinct identity in every surface |
| Hero/player | basic object with glow or simple attachments | authored silhouette, decals/trim, state cues, collision proxy | memorable layered model with expressive feedback |
| Obstacles/enemies | gameplay roles are hard to distinguish | readable role-specific forms, telegraphs, and material cues | expressive challenge geometry or varied family with anticipation |
| Rewards/interactables | important interactions have generic or absent feedback | authored forms, readable interaction states, UI feedback | purpose and value stay clear during motion |
| World/environment | themed but sparse repeated blocks | layered prop kit, foreground/midground/background, scale cues | dense authored world that aids readability |
| Materials/textures | basic roughness/metalness or emissive color | shared material roles, procedural decals, trim, panel lines, wear | rich cohesive material language, measured resource use |
| Lighting/render | fog and bloom used as the style | intentional tone mapping, exposure, key/fill/rim, contact, depth | cinematic but readable, disciplined post |
| VFX/motion | generic particles and trails | event-driven VFX: boost, pickup, hit, fail, combo, shield, spawn | high-impact effects that clarify gameplay and stay cheap |
| UI/HUD | generic stat-card dashboard | genre-specific states, meters/icons, responsive text fit | cohesive interface, strong hierarchy, polished transitions |
| Performance evidence | informal "seems fine" | renderer counts, build/browser QA, target-viewport shots, budget notes | baseline/post metrics, bottleneck notes, asset strategy, tradeoffs |
Keep all ten categories, but name their genre equivalents before scoring. In pool, Hero can mean the table/cue/ball presentation, Obstacles the rails/pockets and shot constraints, and Interactables the balls/aim/contact feedback. In a puzzle game these may be the board, constraints, and manipulable pieces. Do not invent enemies, loot, neon trim, or extra props to increase a score. Repeated identical forms can be correct for the rules; score their authorship and readability, not an arbitrary variant count. Deliberately minimal art can score well when its composition, material decisions, and feedback are demonstrably finished.
## Thresholds
- **Premium**: every category ≥ 2, average ≥ 2.3, renderer diagnostics reported after graphics changes.
- **Showcase**: no category below 2, at least six at 3, average ≥ 2.7, before/after performance evidence.
## Automatic failures
Any one of these means the work is not premium yet, whatever the individual scores say:
- Active screenshot is dominated by unrefined placeholders or empty space that the design does not justify, rather than authored composition and readable gameplay.
- Hero asset is an unrefined primitive placeholder plus glow. Different gameplay roles are indistinguishable without a deliberate design reason.
- HUD is mostly rectangular stat/debug cards.
- Fog, darkness, bloom, or particles are standing in for missing authored geometry.
- UI overlaps the play path, clips text, or breaks safe areas on a target viewport.
- The game is not playable through real input, or no active-play screenshot exists.
- No renderer diagnostics after major graphics work.
## Measured evidence
Run the canvas inspector (`npm run inspect:canvas`, or `threejs-qa-release/scripts/inspect-threejs-canvas.mjs`) on the target viewports and cite its `metrics` and `renderBudget` blocks. These are advisory signals; a low value needs an explanation, not a higher score. Do not add noise, clutter, or particles just to raise pixel metrics; an intentional clean composition may legitimately measure low.
- `colorEntropyBits` below ~3.0, or `dominantColorShare` above ~0.6 — sparse flat scene. Evidence against World or Materials above 2.
- `edgeDensity` below ~0.04 — primitive-dominant or empty framing. Evidence against World and Hero above 2.
- `luminance.contrast` below ~60 — fog/darkness compression. Evidence against Lighting above 2.
- `renderBudget` rows over the tier budget need a documented tradeoff (see `technical-art.md`).
Score the **complete declared** capture set for the change, including desktop and mobile when both are targets. Use acknowledged `--state` captures for relevant mid-run states (late waves, fail, stress); a requested label without a working state hook is not evidence. Animated work also needs unpaused motion evidence of transitions and contact timing. A still image cannot establish animation quality.
## Reporting
Give each category a before/after number with one line of evidence, then the average and any automatic failures still standing. For a new game with no baseline, use `not captured` for before; never invent a score. If a category is below threshold, name the next pass that fixes it. A narrow fix does not require re-scoring unchanged categories or re-establishing the entire game's premium status.
SKILL.md
---
name: threejs-aaa-graphics-builder
description: "Upgrade Three.js games from prototype visuals to premium browser graphics: art-direction critique, procedural model building, material and texture libraries, world prop kits, shaders, VFX, lighting and render pipeline, LOD and instancing, render budgets, and a 10-category visual scorecard. Use when screenshots still look basic or the user asks for premium, AAA, high-fidelity, showcase, or less-basic graphics."
---
# Three.js AAA Graphics Builder
Own the production graphics pass: turn basic screenshots into authored, high-density, performance-aware visuals.
## References
| File | Read it when |
| --- | --- |
| `references/visual-scorecard.md` | scoring visuals or making any premium/AAA/showcase claim |
| `references/authoring-recipes.md` | building hero, obstacle, reward, world-kit, or prop models; changing lighting, tone mapping, shadows, fog, post, or graphics architecture |
| `references/technical-art.md` | render budgets, material kits, VFX systems, instancing/LOD, imported asset cleanup, anything that could affect browser performance |
| `references/shader-cookbook.md` | custom shaders, `onBeforeCompile`, skies, or post-processing; use recipes as tested starting points and verify them against the project's Three.js version |
For a broad "still looks basic" or premium pass, read all four before implementing. A narrow graphics edit loads only its relevant references and checks; the requested style and scope override recipe defaults.
## Core rule
Glow does not make primitives look AAA. Build authored forms first, then materials, then lighting, then effects — in that order.
## Workflow
1. Capture or inspect active-play screenshots on the target viewports when a playable scene exists.
2. For an existing game, score the affected views and pick the weakest surfaces. For a new game, establish art direction, camera scale, material roles, and the hero target first; do not invent a before screenshot.
3. Add the graphics architecture the game is missing: material library, procedural textures and decals, model factories, world prop kit, VFX system, render pipeline, diagnostics.
4. Choose a source per high-value surface: procedural Three.js, a `threejs-image-generator` reference or texture, a `threejs-3d-generator` model, or an image-to-3D hybrid chain. Run the credential probe when external generation is in scope.
Inspect the concept/model before dependent generation or rigging. Finish one representative playable scene with actual assets and feedback before expanding the content kit.
5. Upgrade every weak visible surface, not only the hero: hazards, rewards, ground and track, foreground props, background layers, telegraphs, material variation, state VFX.
6. Add lighting, tone mapping, and render polish once authored forms exist.
7. Add event-driven VFX tied to gameplay state.
8. Re-score against the calibration anchors, citing the inspector's measured metrics. Keep going until every premium category is at least 2, or name the exact blocker.
## Asset sourcing
When external generation is in scope, run `threejs-game-director/scripts/probe_asset_credentials.sh` before assuming anything about keys. No probe or paid submission is needed for explicitly procedural art.
With keys set, generated assets belong on the hero surfaces — player, character, creature, boss, vehicle, ship, building, weapon, signature prop, hero environment piece — and on high-value 2D: skies, backgrounds, texture and trim references, decals, faction marks, icons, GUI and title art, image-to-3D inputs. Respect explicit procedural-only art or external-generation restrictions. Procedural Three.js handles repeated props, kits, collision proxies, VFX geometry, and instanced volume.
Use the director's `references/asset-recovery.md`: recover transient failures and accepted tasks before fallback. Missing keys, exhausted credits, or exhausted bounded recovery permit a local replacement with the remaining quality gap reported. A single timeout is not evidence that generation is unavailable.
For animated assets inspect motion as well as silhouettes: locomotion, blend transitions, foot contacts, hit timing, and secondary motion in real gameplay. A focused independent critique may identify defects after a substantial pass; the lead remains responsible for the final score and integration.
## Report
Score before and after with one line of evidence per category, the surfaces you upgraded, files changed, screenshots, renderer diagnostics against the budget table, generated asset paths and task IDs, and what is still weak. Include imported-asset diagnostics (scale, bounds, collision proxy, clips) when generated 3D was used.