Problem: a photoreal or night scene cannot be lit. Verified on main: LightingConfig in packages/core/src/game/playableGame.ts (search interface LightingConfig) has ambient, directional, hemisphere only; packages/shell/src/render/EnvironmentLighting.tsx PMREMs a two-color gradient sphere, no RGBELoader; look is "cinematic" | "flat" in packages/core/src/render/lookPreset.ts:15 with cinematic as the silent default at :53; the grade defaults in packages/shell/src/postfx/gradeShader.ts:6-12 are a fixed film look; quality is three DPR numbers in packages/core/src/settings/settingsModel.ts:110-114; packages/shell/src/render/modelLoad.ts registers only MeshoptDecoder; packages/shell/src/materialOverride.ts:42 returns early for anything but MeshStandardMaterial; ModelMaterialMaps has color, normal, roughness, ao only.
Done: shadow flags on cloned models (#1695).
Tasks, one PR each, in order. Every task with a rendered change needs before/after bun run shoot the-robots --mode play shots in the PR.
-
Loaders. In modelLoad.ts add configureModelLoaders({ dracoDecoderPath?, ktx2TranscoderPath? }) that sets DRACOLoader and KTX2Loader (from three/examples/jsm/loaders/) on sharedGltfLoader; default paths to the jsdelivr CDN copies for the pinned three version, and call KTX2Loader.detectSupport(renderer) from Shell3dPresentation.tsx once the renderer exists. Add .ktx2 to MATERIAL_MAP_FILES in packages/assets/src/materials.ts. Test: a unit test that configureModelLoaders is idempotent and stores the paths. CHANGELOG Added.
-
Material maps and physical materials. Widen ModelMaterialMaps with metalness, emissive, height; in materialOverride.ts handle MeshPhysicalMaterial (it extends Standard, so replace the instanceof MeshStandardMaterial early return with an isMeshStandardMaterial check that also passes physical) and apply the new maps. Test in materialOverride.test.ts. Shots: a catalog model with a metalness map.
-
Environment source. New packages/core/src/render/environment.ts: export type EnvironmentSource = { kind: "gradient"; sky: string; ground: string; sun?: string; intensity?: number } | { kind: "hdri"; url: string; rotation?: number; intensity?: number } | { kind: "cube"; urls: [string, string, string, string, string, string]; intensity?: number } with JSDoc. Add environment?: EnvironmentSource to the game presentation config next to lighting in playableGame.ts, and to the editor document environment block in packages/core/src/editor/types.ts (optional field, decode/encode in document.ts). EnvironmentLighting.tsx loads hdri with RGBELoader (.hdr) or EXRLoader (.exr) into a PMREM, cube with CubeTextureLoader; keep the gradient path as the default. Add an inspector field in packages/editor/src/LightingPanel.tsx. Shots: the same scene under a free CC0 HDRI from Poly Haven (record it in CREDITS.md).
-
Punctual lights. Add to LightingConfig: point?: readonly { position: [number, number, number]; color?: string; intensity?: number; range?: number; decay?: number; castShadow?: boolean }[], spot?: readonly { position; target; color?; intensity?; range?; angle?; penumbra?; castShadow? }[], maxDynamicLights?: number (default 8; extra lights beyond the budget are skipped with one dev warning). Render them in packages/shell/src/render/SceneLighting.tsx. Add an editor kind light via registerSceneKind in packages/core/src/scene/builtinSceneKinds.ts with params type/color/intensity/range/castShadow, and a runtime consumer that turns light markers into LightingConfig.point/spot entries. Shots: a night scene with two point lights.
-
Looks and LUT. In lookPreset.ts add "neutral" (no grade, ACES, no bloom, no AO), "photoreal" (ACES, exposure 1, SMAA, AO 0.8, no grain, no vignette), "toon" (no tonemapping, no AO, no bloom, saturation 1.0). Change the default when look is unset from cinematic to neutral and write a CHANGELOG Migrate bullet telling games to set look: "cinematic" to keep the old output. Add lut?: { url: string; size: 16 | 32 | 64; intensity?: number } to GradeConfig in packages/core/src/render/postProcessing.ts; implement in gradeShader.ts as a 3D texture sampled after the existing grade (accept a .cube file parsed on load, or a strip PNG). Shots: one scene under each look.
-
Graphics profile. New packages/core/src/settings/graphicsProfile.ts: export interface GraphicsProfile { renderScale: number; shadowMapSize: 512 | 1024 | 2048 | 4096; cascades: 1 | 2 | 3 | 4; drawDistance: number; particleCap: number; postStages: { ao: boolean; bloom: boolean; dof: boolean; smaa: boolean } } plus DEFAULT_GRAPHICS_PROFILES: Record<"low" | "medium" | "high", GraphicsProfile> and resolveGraphicsProfile(quality, overrides?). Games override per tier through defineGame({ graphics: Partial<Record<tier, Partial<GraphicsProfile>>> }). Wire: Shell3dPresentation.tsx dpr from renderScale, SceneLighting.tsx map size and cascades, CullingProvider.tsx draw distance, PostProcessing.tsx stage toggles (degrade AO and DoF instead of dropping them at non-high). Tests for resolveGraphicsProfile. Shots: low vs high.
Acceptance: tasks 1 to 6 merged. Closes #1680 on task 6. Overlaps with #1558 (material registry, post graph) are fine; this issue only adds data and defaults.
Rules: wait for bun run agent:bootstrap --check; branch claude/<slug> off origin/main; claim comment first; one task per PR with Refs #1680; bun run gen after export changes (JSDoc on every new export); CHANGELOG bullet; check-types and test on core and shell plus check-stateful-ratchet, check-doc-symbols, check-orphan-ratchet; merge origin/main before pushing; shots via bun run games:clone, bun run shoot daemon start, bun run shoot <game> --mode play --size half --out shots/x.png, before shot from stashed origin/main shell code, bun run pr-shots --dir <branch> shots/*.png; squash auto-merge; fix CI on the same branch.
Problem: a photoreal or night scene cannot be lit. Verified on main:
LightingConfiginpackages/core/src/game/playableGame.ts(searchinterface LightingConfig) has ambient, directional, hemisphere only;packages/shell/src/render/EnvironmentLighting.tsxPMREMs a two-color gradient sphere, noRGBELoader;lookis"cinematic" | "flat"inpackages/core/src/render/lookPreset.ts:15with cinematic as the silent default at:53; the grade defaults inpackages/shell/src/postfx/gradeShader.ts:6-12are a fixed film look;qualityis three DPR numbers inpackages/core/src/settings/settingsModel.ts:110-114;packages/shell/src/render/modelLoad.tsregisters onlyMeshoptDecoder;packages/shell/src/materialOverride.ts:42returns early for anything butMeshStandardMaterial;ModelMaterialMapshas color, normal, roughness, ao only.Done: shadow flags on cloned models (#1695).
Tasks, one PR each, in order. Every task with a rendered change needs before/after
bun run shoot the-robots --mode playshots in the PR.Loaders. In
modelLoad.tsaddconfigureModelLoaders({ dracoDecoderPath?, ktx2TranscoderPath? })that setsDRACOLoaderandKTX2Loader(fromthree/examples/jsm/loaders/) onsharedGltfLoader; default paths to the jsdelivr CDN copies for the pinned three version, and callKTX2Loader.detectSupport(renderer)fromShell3dPresentation.tsxonce the renderer exists. Add.ktx2toMATERIAL_MAP_FILESinpackages/assets/src/materials.ts. Test: a unit test thatconfigureModelLoadersis idempotent and stores the paths. CHANGELOG Added.Material maps and physical materials. Widen
ModelMaterialMapswithmetalness,emissive,height; inmaterialOverride.tshandleMeshPhysicalMaterial(it extends Standard, so replace theinstanceof MeshStandardMaterialearly return with anisMeshStandardMaterialcheck that also passes physical) and apply the new maps. Test inmaterialOverride.test.ts. Shots: a catalog model with a metalness map.Environment source. New
packages/core/src/render/environment.ts:export type EnvironmentSource = { kind: "gradient"; sky: string; ground: string; sun?: string; intensity?: number } | { kind: "hdri"; url: string; rotation?: number; intensity?: number } | { kind: "cube"; urls: [string, string, string, string, string, string]; intensity?: number }with JSDoc. Addenvironment?: EnvironmentSourceto the game presentation config next tolightinginplayableGame.ts, and to the editor document environment block inpackages/core/src/editor/types.ts(optional field, decode/encode indocument.ts).EnvironmentLighting.tsxloads hdri withRGBELoader(.hdr) orEXRLoader(.exr) into a PMREM, cube withCubeTextureLoader; keep the gradient path as the default. Add an inspector field inpackages/editor/src/LightingPanel.tsx. Shots: the same scene under a free CC0 HDRI from Poly Haven (record it inCREDITS.md).Punctual lights. Add to
LightingConfig:point?: readonly { position: [number, number, number]; color?: string; intensity?: number; range?: number; decay?: number; castShadow?: boolean }[],spot?: readonly { position; target; color?; intensity?; range?; angle?; penumbra?; castShadow? }[],maxDynamicLights?: number(default 8; extra lights beyond the budget are skipped with one dev warning). Render them inpackages/shell/src/render/SceneLighting.tsx. Add an editor kindlightviaregisterSceneKindinpackages/core/src/scene/builtinSceneKinds.tswith params type/color/intensity/range/castShadow, and a runtime consumer that turnslightmarkers intoLightingConfig.point/spotentries. Shots: a night scene with two point lights.Looks and LUT. In
lookPreset.tsadd"neutral"(no grade, ACES, no bloom, no AO),"photoreal"(ACES, exposure 1, SMAA, AO 0.8, no grain, no vignette),"toon"(no tonemapping, no AO, no bloom, saturation 1.0). Change the default whenlookis unset fromcinematictoneutraland write a CHANGELOG Migrate bullet telling games to setlook: "cinematic"to keep the old output. Addlut?: { url: string; size: 16 | 32 | 64; intensity?: number }toGradeConfiginpackages/core/src/render/postProcessing.ts; implement ingradeShader.tsas a 3D texture sampled after the existing grade (accept a.cubefile parsed on load, or a strip PNG). Shots: one scene under each look.Graphics profile. New
packages/core/src/settings/graphicsProfile.ts:export interface GraphicsProfile { renderScale: number; shadowMapSize: 512 | 1024 | 2048 | 4096; cascades: 1 | 2 | 3 | 4; drawDistance: number; particleCap: number; postStages: { ao: boolean; bloom: boolean; dof: boolean; smaa: boolean } }plusDEFAULT_GRAPHICS_PROFILES: Record<"low" | "medium" | "high", GraphicsProfile>andresolveGraphicsProfile(quality, overrides?). Games override per tier throughdefineGame({ graphics: Partial<Record<tier, Partial<GraphicsProfile>>> }). Wire:Shell3dPresentation.tsxdpr fromrenderScale,SceneLighting.tsxmap size and cascades,CullingProvider.tsxdraw distance,PostProcessing.tsxstage toggles (degrade AO and DoF instead of dropping them at non-high). Tests forresolveGraphicsProfile. Shots: low vs high.Acceptance: tasks 1 to 6 merged.
Closes #1680on task 6. Overlaps with #1558 (material registry, post graph) are fine; this issue only adds data and defaults.Rules: wait for
bun run agent:bootstrap --check; branchclaude/<slug>offorigin/main; claim comment first; one task per PR withRefs #1680;bun run genafter export changes (JSDoc on every new export); CHANGELOG bullet;check-typesandteston core and shell pluscheck-stateful-ratchet,check-doc-symbols,check-orphan-ratchet; mergeorigin/mainbefore pushing; shots viabun run games:clone,bun run shoot daemon start,bun run shoot <game> --mode play --size half --out shots/x.png, before shot from stashedorigin/mainshell code,bun run pr-shots --dir <branch> shots/*.png; squash auto-merge; fix CI on the same branch.