@jgengine/shell 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/Shell3dPresentation.js +29 -11
  3. package/dist/devtools/FrameTelemetry.d.ts +12 -0
  4. package/dist/devtools/FrameTelemetry.js +68 -0
  5. package/dist/devtools/agentBridge.js +2 -0
  6. package/dist/devtools/devtoolsReports.d.ts +2 -0
  7. package/dist/devtools/devtoolsReports.js +3 -0
  8. package/dist/devtools/frameSampler.d.ts +127 -0
  9. package/dist/devtools/frameSampler.js +345 -0
  10. package/dist/environment/AuthoredDaylight.d.ts +28 -0
  11. package/dist/environment/AuthoredDaylight.js +42 -0
  12. package/dist/environment/Daylight.d.ts +55 -7
  13. package/dist/environment/Daylight.js +126 -29
  14. package/dist/environment/daylightCycle.d.ts +26 -4
  15. package/dist/environment/daylightCycle.js +36 -5
  16. package/dist/postfx/PostProcessing.d.ts +7 -1
  17. package/dist/postfx/PostProcessing.js +28 -2
  18. package/dist/postfx/autoExposure.d.ts +49 -0
  19. package/dist/postfx/autoExposure.js +189 -0
  20. package/dist/postfx/gradeShader.js +43 -4
  21. package/dist/presentationRecovery3d.js +2 -1
  22. package/dist/render/CascadedShadows.d.ts +3 -1
  23. package/dist/render/CascadedShadows.js +9 -1
  24. package/dist/render/EnvironmentLighting.d.ts +10 -1
  25. package/dist/render/EnvironmentLighting.js +11 -3
  26. package/dist/render/IrradianceVolumeLighting.d.ts +18 -0
  27. package/dist/render/IrradianceVolumeLighting.js +35 -0
  28. package/dist/render/SceneLighting.d.ts +14 -1
  29. package/dist/render/SceneLighting.js +29 -8
  30. package/dist/render/authoredGrade.d.ts +11 -0
  31. package/dist/render/authoredGrade.js +26 -0
  32. package/dist/render/presentationLook.d.ts +15 -0
  33. package/dist/render/presentationLook.js +18 -0
  34. package/dist/render/stableTextureSet.d.ts +9 -0
  35. package/dist/render/stableTextureSet.js +24 -0
  36. package/dist/render/triplanarMaterial.d.ts +52 -0
  37. package/dist/render/triplanarMaterial.js +169 -0
  38. package/dist/settings/appliedSettings.js +16 -13
  39. package/package.json +4 -4
  40. package/skills/jgengine/api.md +59 -6
  41. package/skills/jgengine/capabilities.md +4 -0
  42. package/skills/jgengine-ui/api.md +82 -22
  43. package/skills/jgengine-ui/capabilities.md +10 -0
  44. package/skills/jgengine-ui/reference-primitives.md +4 -2
  45. package/skills/jgengine-world/SKILL.md +3 -1
  46. package/skills/jgengine-world/api.md +107 -3
  47. package/skills/jgengine-world/capabilities.md +22 -2
  48. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
@@ -33,10 +33,30 @@
33
33
  - `createI18n` (function): function createI18n(options: I18nOptions): I18n — Create a translator over a message catalog: active-locale lookup with a fallback-locale chain, `{param}` interpolation, and `Intl.PluralRules`-based pluralization. Observable (`subscribe`) so React re-renders on `setLocale`; the catalog is caller-owned static data, the locale is the only state.
34
34
  - `interpolate` (function): function interpolate(template: string, params?: TParams): string — Replace `{name}` placeholders in `template` with `params[name]`. Unknown placeholders are left intact. Pure and locale-independent.
35
35
 
36
+ ## @jgengine/core/render/colorPipeline
37
+
38
+ - `AutoExposureConfig` (interface): interface AutoExposureConfig — Bounds and adaptation policy for clamped auto-exposure.
39
+ - `DEFAULT_AUTO_EXPOSURE` (const): const DEFAULT_AUTO_EXPOSURE: Readonly<Required<AutoExposureConfig>> — Default clamped auto-exposure. The range is deliberately asymmetric: a bright street is already readable after AgX's shoulder, while a lamp-lit interior is the case that crushes, so the meter may lift 1.3 stops but only darken 0.7 — an artist's exposure survives.
40
+ - `METERING_HIGHLIGHT_CEILING` (const): const METERING_HIGHLIGHT_CEILING: number — Pixels count toward metering up to this multiple of {@link WHITE_POINT}. A window or a sun disc sits far above it and would otherwise pull the whole frame down to a silhouette.
41
+ - `MID_GREY` (const): const MID_GREY: 0.18 — Linear scene luminance of an 18% grey card — the auto-exposure metering target.
42
+ - `WHITE_POINT` (const): const WHITE_POINT: 1 — Calibration of the output pipeline, in linear scene luminance (1.0 = a white-looking surface at exposure 1).
43
+ - `adaptExposure` (function): function adaptExposure(current: number, target: number, deltaSeconds: number, adaptationSeconds = DEFAULT_AUTO_EXPOSURE.adaptationSeconds): number — Frame-rate independent blend of `current` toward `target`, exponential in time so a hitch does not overshoot and a steady view settles. `deltaSeconds` may be 0..∞; a zero-length step keeps `current`.
44
+ - `exposureForKey` (function): function exposureForKey(keyLuminance: number, config: AutoExposureConfig = {}): number — Exposure that places a frame whose key luminance is `keyLuminance` on {@link MID_GREY}, bounded by the authored range. A dark interior asks for more than `max` and gets `max`; a blown-out exterior asks for less than `min` and gets `min`, so the frame never runs away at either end.
45
+ - `keyLuminanceFromSamples` (function): function keyLuminanceFromSamples(samples: ArrayLike<number>): number — Key luminance of a metered frame: the median of its samples, each clamped to {@link METERING_HIGHLIGHT_CEILING}. A median rather than a mean because a bright sky, a window or a lamp is a large minority of the frame: averaging those in drags an interior down to a silhouette, where the median still reports the room.
46
+
36
47
  ## @jgengine/core/render/environment
37
48
 
38
49
  - `EnvironmentSource` (type): type EnvironmentSource = | { kind: "gradient"; sky: string; ground: string; sun?: string; intensity?: number } /** Equirectangular HDR or EXR environment map loaded from `url`. */ | { kind: "hdri"; url: string; rotation?: number; intensity?: number } /** Six faces of a cube environment map, in three… — A declarative source for image-based scene lighting.
39
50
 
51
+ ## @jgengine/core/render/lightProbes
52
+
53
+ - `IrradianceBakeInput` (interface): interface IrradianceBakeInput — Everything a bake needs for one volume: where the grid sits, how much sky reaches it, and what fills the dark.
54
+ - `IrradianceBakeSettings` (interface): interface IrradianceBakeSettings — Authored bake inputs stored beside the samples so a volume can be re-baked without the editor.
55
+ - `IrradianceVolume` (interface): interface IrradianceVolume — One irradiance volume authored on a scene document.
56
+ - `PROBE_STRIDE` (const): const PROBE_STRIDE: number — Floats stored per probe: {@link SH_COEFFICIENTS} × RGB.
57
+ - `SH_COEFFICIENTS` (const): const SH_COEFFICIENTS: 9 — Baked irradiance volume: a grid of second-order (9-coefficient, RGB) spherical-harmonic probes over a box region, sampled at runtime for ambient lighting. It is the datum an interior has that a hemisphere light cannot: a room's fill is its own, not the sky's, and it varies across the space.
58
+ - `bakeIrradianceVolume` (function): function bakeIrradianceVolume(input: IrradianceBakeInput): IrradianceVolume — Bakes an irradiance volume from authored inputs: sky radiance where the region is open, an authored ceiling/floor fill where it is enclosed, plus inverse-square lamp radiance. Each probe projects its directional radiance into L2 SH, so ambient varies across the grid and keeps a lit side instead of flattening to one number.
59
+
40
60
  ## @jgengine/core/render/lightingBalance
41
61
 
42
62
  - `DEFAULT_IBL_AMBIENT_SCALE` (const): const DEFAULT_IBL_AMBIENT_SCALE: 0.5 — Ambient multiplier applied under image-based light when `iblAmbientScale` is unset.
@@ -59,6 +79,16 @@
59
79
  - `mergePostProcessing` (function): function mergePostProcessing(preset: PostProcessingConfig, authored: PostProcessingConfig | undefined): PostProcessingConfig — Deep-merge a game's partial post chain over a preset's. Every field the game set wins, including `false` to drop a stage; fields it left unset come from the preset, and stage objects merge field by field (tuples such as `grade.lift` replace whole).
60
80
  - `resolveGameLook` (function): function resolveGameLook(input: GameLookInput): ResolvedGameLook — Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`; `"flat"` passes the explicit knobs through untouched. Anything the game authored wins — the preset only fills unset knobs, and it never adds a sky when the world already owns one (so the sky's tuned sun/hemisphere serve as the lighting rig). A styled preset's post stages are deep-merged under a partial game `postProcessing` ({@link mergePostProcessing}); under `"neutral"` a game's own chain is used as authored.
61
81
 
82
+ ## @jgengine/core/render/physicalSky
83
+
84
+ - `MOONLIGHT_COLOR` (const): const MOONLIGHT_COLOR: "#a9c3e8" — Moonlight's colour: scattered sunlight, blue-shifted because the eye's scotopic response peaks there.
85
+ - `MOONLIGHT_PEAK` (const): const MOONLIGHT_PEAK: 0.12 — Peak moonlight as a fraction of the authored sunlight, at the moon's highest point. Real moonlight is ~2.5e-6 of daylight; every game exaggerates it to stay readable, and this is the exaggeration, not a measurement. Games override it through `SkyEnvironmentConfig.moon`.
86
+ - `NIGHT_FOG_COLOR` (const): const NIGHT_FOG_COLOR: "#0a0e16" — Fog color once the sun is gone: the night sky's own haze, lifted enough to keep silhouettes readable.
87
+ - `elevationOf` (function): function elevationOf(direction: readonly [number, number, number]): number — Elevation of a world-space direction in degrees; a zero vector reads as overhead.
88
+ - `moonIllumination` (function): function moonIllumination(moonElevationDeg: number, peak: number = MOONLIGHT_PEAK): number — Moonlight at `sunElevationDeg`'s opposite point: full at the top of the moon's arc, zero while the moon is below the horizon, scaled by `peak` (a fraction of the authored sun intensity).
89
+ - `starOpacity` (function): function starOpacity(sunElevationDeg: number): number — How visible the stars are: none in daylight, half at the first hint of a set sun, full once it is 6° under.
90
+ - `sunlightColor` (function): function sunlightColor(elevationDeg: number): string — The sun's colour at `elevationDeg`, normalised so its brightest channel is full: white overhead, orange at the horizon, moon-blue once it has set. Intensity stays the authored cycle's job.
91
+
62
92
  ## @jgengine/core/render/postProcessing
63
93
 
64
94
  - `AoConfig` (interface): interface AoConfig — Ground-truth ambient occlusion stage — darkens contact creases and cavities for depth.
@@ -66,12 +96,20 @@
66
96
  - `DEFAULT_AO` (const): const DEFAULT_AO: Readonly<AoConfig> — Contact shading used when a post chain leaves `ao` unset: soft creases, no dark halos.
67
97
  - `DofConfig` (interface): interface DofConfig — Depth-of-field (bokeh) stage — throws the fore/background out of focus around a focus distance.
68
98
  - `GradeConfig` (interface): interface GradeConfig — Final colour-grade stage: lift/gain/gamma, saturation, vignette, film grain — applied in display space after tone mapping.
99
+ - `GradeLut` (type): type GradeLut = { url: string; size?: number } | { cube: LutCube } — A grade's LUT: a strip PNG (one `size`-wide tile per blue slice) or a baked cube.
100
+ - `LutCube` (interface): interface LutCube — A baked display-space LUT cube: `size³` RGB entries with red varying fastest, three bytes per entry in 0..255. Kept as plain data so a scene document can author one and a test can assert on it.
69
101
  - `OutlineConfig` (interface): interface OutlineConfig — Ink outlines traced from depth and surface-normal edges — silhouettes plus interior creases.
70
102
  - `PostAaMode` (type): type PostAaMode = "smaa" | "msaa" | false — Edge antialiasing for the post chain. SMAA runs in linear-sRGB before the output tone-map and cleans alpha-tested foliage/particles that MSAA samples alone leave crawling. `"msaa"` keeps only the render-target multisample resolve; `false` disables AA stages.
71
103
  - `PostProcessingConfig` (interface): interface PostProcessingConfig — Declarative post-processing chain (RenderPass → AO → Bloom → SMAA → tone-map output → Grade). Present on a game means the shell mounts an `EffectComposer` and owns the render; absent means the renderer draws directly (unchanged). Each stage is a config object, `false` to skip, or omitted for its default. Pure data — no three.js types leak into core.
72
104
  - `STUDIO_STAGE_POST` (const): const STUDIO_STAGE_POST: PostProcessingConfig — A cinematic "product shot" post preset — the full chain on (contact-AO, soft bloom, a warm film grade with vignette + a touch of grain + chromatic aberration). Meant for a `StudioStage` where a single parametric asset is framed on a backdrop, so every studio reads shipped, not intern-tier. DoF is left off by default (it needs a per-scene focus distance); set `dof` to enable it.
73
105
  - `StylizeConfig` (interface): interface StylizeConfig — Art-style stage that turns the same models and lighting into a different look without touching materials: ink outlines (comic / Borderlands), cel bands (cartoon), pixelation (retro). Runs after tone mapping, before the grade.
74
- - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage.
106
+ - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage. `"agx"` is the pipeline default: it keeps hue in saturated highlights and rolls a sun-lit surface off gradually, where `"aces"` skews bright colours toward white.
107
+
108
+ ## @jgengine/core/render/skyOwnedLighting
109
+
110
+ - `SkyOwnedLighting` (interface): interface SkyOwnedLighting — Which half of a game's authored `lighting` a time-of-day sky takes over.
111
+ - `skyNoonKeyframes` (function): function skyNoonKeyframes<T extends SkyEnvironmentConfig>(sky: T, lighting: LightingConfig | undefined, fogColor?: string): T — The authored `lighting` as the day cycle's **noon keyframes**, filling only the sky fields the game left unset. A game's midday sun, fill and fog therefore become the peak the cycle scales and cross-fades from, so an authored world gets a sunset and a night without re-authoring the same numbers in the sky. Authored sky fields always win over the lighting config.
112
+ - `splitSkyOwnedLighting` (function): function splitSkyOwnedLighting(lighting: LightingConfig | undefined): SkyOwnedLighting — Splits authored lighting into the sun/ambient terms a time-of-day sky drives and the rest. Returns an empty `sky` when the game authored no lighting, so callers can keep engine defaults.
75
113
 
76
114
  ## @jgengine/core/render/sprite2d
77
115
 
@@ -256,7 +294,7 @@
256
294
  - `TEXT_SCALE_MAX` (const): const TEXT_SCALE_MAX: 2 — Maximum UI text scale.
257
295
  - `TEXT_SCALE_MIN` (const): const TEXT_SCALE_MIN: 0.75 — Minimum UI text scale.
258
296
  - `TParams` (type): type TParams = Readonly<Record<string, string | number>> — Interpolation values for a message template.
259
- - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage.
297
+ - `ToneMappingMode` (type): type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none" — Renderer tone-mapping curve applied by the post chain's output stage. `"agx"` is the pipeline default: it keeps hue in saturated highlights and rolls a sun-lit surface off gradually, where `"aces"` skews bright colours toward white.
260
298
  - `TooltipContent` (interface): interface TooltipContent — Structured tooltip content — a renderer lays these fields out; the model owns what is shown.
261
299
  - `UI_SCALE_MAX` (const): const UI_SCALE_MAX: 1.5 — ⚠ undocumented
262
300
  - `UI_SCALE_MIN` (const): const UI_SCALE_MIN: 0.5 — ⚠ undocumented
@@ -2208,6 +2246,16 @@
2208
2246
 
2209
2247
  - `DebugSummary` (interface): interface DebugSummary — Compact `debug_summary` payload: the numbers an agent needs to judge a run, about 1 KB.
2210
2248
 
2249
+ ## @jgengine/shell/devtools/frameSampler
2250
+
2251
+ - `FrameReportInputs` (interface): interface FrameReportInputs — Everything the report needs, gathered by whichever producer is asking.
2252
+ - `FrameTelemetry` (interface): interface FrameTelemetry — The installed sampler: ask it for a report, reset its window, or tear it down.
2253
+ - `FrameTelemetrySources` (interface): interface FrameTelemetrySources — What the shell hands the sampler: live getters, called only when a report is built.
2254
+ - `GlCounterTotals` (interface): interface GlCounterTotals — GPU-object creation counts, the fields the budget gate compares.
2255
+ - `REPORT_PUSH_MS` (const): const REPORT_PUSH_MS: 1000 — Cadence of the push to the dev server, in milliseconds.
2256
+ - `TELEMETRY_FLAG` (const): const TELEMETRY_FLAG: "telemetry" — In-page frame telemetry: the game reports on itself instead of needing a second process to attach a profiler. Frame timings come from the devtools frame ring, renderer counts from the live renderer, GPU-object creation from wrapped GL calls, and per-system numbers from `devtools.probes` — all read on demand, so nothing here runs on the hot path.
2257
+ - `TELEMETRY_WARMUP_MS` (const): const TELEMETRY_WARMUP_MS: 5000 — How long after the sampler starts the GL counters get their post-warmup baseline.
2258
+
2211
2259
  ## @jgengine/shell/devtools/panelAtoms
2212
2260
 
2213
2261
  - `SectionLabel` (function): function SectionLabel({ children }: { children: string }): React.JSX.Element — ⚠ undocumented
@@ -2244,34 +2292,38 @@
2244
2292
 
2245
2293
  - `DayNightSky` (function): function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }: DayNightSkyProps): React.JSX.Element — Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt` geometry. This is the turnkey presentation seam — a game mounts this one component in its scene and gets a moving, color-graded day-night sky from the serializable model, with no hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are free-form and never interpreted here.
2246
2294
  - `DayNightSkyProps` (interface): interface DayNightSkyProps — Props for {@link DayNightSky}: the cycle to bind plus optional dome shape and light-strength knobs.
2247
- - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig } — ⚠ undocumented
2248
- - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; ambient?: {skyColor?: string; groundColor?: string; intensi… — ⚠ undocumented
2249
- - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; ambientIntensity: number; skyTop: string; skyBottom: string; background: string } — ⚠ undocumented
2295
+ - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig; sunColor?: string; fogColor?: string; moonIntensity?: number; moonColor?: string } — ⚠ undocumented
2296
+ - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; moon?: {position?: readonly [number, number, number]; inten… — ⚠ undocumented
2297
+ - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; sunColor: string; ambientIntensity: number; skyTop: string; skyBottom: string; background: string; fogColor: string; moonPosition: [number, number, number]; moonIntensity: numbe… — ⚠ undocumented
2250
2298
  - `EnvironmentScene` (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element — ⚠ undocumented
2251
2299
  - `EnvironmentSceneProps` (interface): interface EnvironmentSceneProps { feature: EnvironmentWorldFeature } — ⚠ undocumented
2252
2300
  - `SKY_PRESET_DAY_FRACTION` (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number> — ⚠ undocumented
2253
- - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2254
- - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; hazeStrength?: number; cloudiness?: number; sunGlow… — ⚠ undocumented
2301
+ - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2302
+ - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; moonDirection?: readonly [number, number, number]; … — ⚠ undocumented
2255
2303
  - `SkyLightOwnership` (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
2256
- - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2304
+ - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2257
2305
  - `VolumetricCloudsProps` (interface): interface VolumetricCloudsProps — Props for {@link VolumetricClouds} — fully-resolved cloud rules plus the shared sun direction.
2258
2306
 
2259
2307
  ## @jgengine/shell/environment
2260
2308
 
2261
2309
  - `DayNightSky` (function): function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }: DayNightSkyProps): React.JSX.Element — Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt` geometry. This is the turnkey presentation seam — a game mounts this one component in its scene and gets a moving, color-graded day-night sky from the serializable model, with no hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are free-form and never interpreted here.
2262
2310
  - `DayNightSkyProps` (interface): interface DayNightSkyProps — Props for {@link DayNightSky}: the cycle to bind plus optional dome shape and light-strength knobs.
2263
- - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig } — ⚠ undocumented
2264
- - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; ambient?: {skyColor?: string; groundColor?: string; intensi… — ⚠ undocumented
2265
- - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; ambientIntensity: number; skyTop: string; skyBottom: string; background: string } — ⚠ undocumented
2311
+ - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig; sunColor?: string; fogColor?: string; moonIntensity?: number; moonColor?: string } — ⚠ undocumented
2312
+ - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; moon?: {position?: readonly [number, number, number]; inten… — ⚠ undocumented
2313
+ - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; sunColor: string; ambientIntensity: number; skyTop: string; skyBottom: string; background: string; fogColor: string; moonPosition: [number, number, number]; moonIntensity: numbe… — ⚠ undocumented
2266
2314
  - `EnvironmentScene` (function): function EnvironmentScene({ feature }: EnvironmentSceneProps): React.JSX.Element — ⚠ undocumented
2267
2315
  - `EnvironmentSceneProps` (interface): interface EnvironmentSceneProps { feature: EnvironmentWorldFeature } — ⚠ undocumented
2268
2316
  - `SKY_PRESET_DAY_FRACTION` (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number> — ⚠ undocumented
2269
- - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2270
- - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; hazeStrength?: number; cloudiness?: number; sunGlow… — ⚠ undocumented
2317
+ - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2318
+ - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; moonDirection?: readonly [number, number, number]; … — ⚠ undocumented
2271
2319
  - `SkyLightOwnership` (type): type SkyLightOwnership = "authored" | "sky-default" — Policy for composing sky backdrops with `PlayableGame.lighting`: - authored lighting present → sky renders dome + fog only; lights stay game-owned - no authored lighting → sky may emit its default sun/hemisphere with the dome Time-of-day never rewrites configured lights; it only drives sky colors/fog (and sky-owned lights when the game did not author lighting).
2272
- - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2320
+ - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2273
2321
  - `VolumetricCloudsProps` (interface): interface VolumetricCloudsProps — Props for {@link VolumetricClouds} — fully-resolved cloud rules plus the shared sun direction.
2274
2322
 
2323
+ ## @jgengine/shell/environment/AuthoredDaylight
2324
+
2325
+ - `AuthoredDaylightProps` (interface): interface AuthoredDaylightProps — Props for {@link AuthoredDaylight}: the authored sky whose values are the noon keyframe, the world clock, and the holder configured lights read.
2326
+
2275
2327
  ## @jgengine/shell/environment/DayNightSky
2276
2328
 
2277
2329
  - `DayNightSky` (function): function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }: DayNightSkyProps): React.JSX.Element — Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt` geometry. This is the turnkey presentation seam — a game mounts this one component in its scene and gets a moving, color-graded day-night sky from the serializable model, with no hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are free-form and never interpreted here.
@@ -2279,10 +2331,10 @@
2279
2331
 
2280
2332
  ## @jgengine/shell/environment/Daylight
2281
2333
 
2282
- - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; ambient?: {skyColor?: string; groundColor?: string; intensi… — ⚠ undocumented
2283
- - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2284
- - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; hazeStrength?: number; cloudiness?: number; sunGlow… — ⚠ undocumented
2285
- - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[] } — ⚠ undocumented
2334
+ - `DaylightProps` (interface): interface DaylightProps { sky?: SkyDomeProps | false; fog?: {color?: string; near?: number; far?: number} | false; sun?: {position?: readonly [number, number, number]; intensity?: number; color?: string}; moon?: {position?: readonly [number, number, number]; inten… — ⚠ undocumented
2335
+ - `SkyDaylightProps` (interface): interface SkyDaylightProps { sky: SkyEnvironmentDescriptor; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2336
+ - `SkyDomeProps` (interface): interface SkyDomeProps { topColor?: string; horizonColor?: string; radius?: number; offset?: number; exponent?: number; sunDirection?: readonly [number, number, number]; sunColor?: string; sunIntensity?: number; moonDirection?: readonly [number, number, number]; … — ⚠ undocumented
2337
+ - `TimeOfDayDaylightProps` (interface): interface TimeOfDayDaylightProps { sky: SkyEnvironmentDescriptor; clock?: {calendar(): {dayFraction: number}}; lights?: boolean; bands?: readonly BiomeBand[]; fogRange?: {near?: number; far?: number} } — ⚠ undocumented
2286
2338
 
2287
2339
  ## @jgengine/shell/environment/EnvironmentScene
2288
2340
 
@@ -2310,9 +2362,10 @@
2310
2362
  - `DEFAULT_DAY_SKY_BOTTOM` (const): const DEFAULT_DAY_SKY_BOTTOM: "#e3f4ff" — ⚠ undocumented
2311
2363
  - `DEFAULT_DAY_SKY_TOP` (const): const DEFAULT_DAY_SKY_TOP: "#3fa4f2" — ⚠ undocumented
2312
2364
  - `DEFAULT_DAY_SUN_INTENSITY` (const): const DEFAULT_DAY_SUN_INTENSITY: 1 — ⚠ undocumented
2365
+ - `DEFAULT_SUN_COLOR` (const): const DEFAULT_SUN_COLOR: "#fff1c9" — Warm-white noon sun used when the game authored no sun tint (the shell's `SUN_COLOR`).
2313
2366
  - `DEFAULT_SUN_ELEVATION_DEG` (const): const DEFAULT_SUN_ELEVATION_DEG: number — The engine arc's noon height: `atan(1 / SUN_DEPTH_RATIO)`, ~68 degrees.
2314
- - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig } — ⚠ undocumented
2315
- - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; ambientIntensity: number; skyTop: string; skyBottom: string; background: string } — ⚠ undocumented
2367
+ - `DaylightCycleConfig` (interface): interface DaylightCycleConfig { horizonColor?: string; zenithColor?: string; sunIntensity?: number; ambientIntensity?: number; sun?: SkySunConfig; sunColor?: string; fogColor?: string; moonIntensity?: number; moonColor?: string } — ⚠ undocumented
2368
+ - `DaylightState` (interface): interface DaylightState { sunPosition: [number, number, number]; sunIntensity: number; sunColor: string; ambientIntensity: number; skyTop: string; skyBottom: string; background: string; fogColor: string; moonPosition: [number, number, number]; moonIntensity: numbe… — ⚠ undocumented
2316
2369
  - `SKY_PRESET_DAY_FRACTION` (const): const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number> — ⚠ undocumented
2317
2370
 
2318
2371
  ## @jgengine/shell/environment/groundPadMath
@@ -2425,7 +2478,7 @@
2425
2478
 
2426
2479
  ## @jgengine/shell/postfx/PostProcessing
2427
2480
 
2428
- - `PostProcessing` (function): function PostProcessing({ config, quality = "high", stages }: { config: PostProcessingConfig; quality?: GraphicsQuality; stages?: { ao: boolean; bloom: boolean; dof: boolean; smaa: boolean } }): null — Mounts an `EffectComposer` inside the shell Canvas and takes over rendering (priority-1 `useFrame`, which disables R3F auto-render) to run the configured post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
2481
+ - `PostProcessing` (function): function PostProcessing({ config, quality = "high", stages }: { config: PostProcessingConfig; quality?: GraphicsQuality; stages?: { ao: boolean; bloom: boolean; dof: boolean; smaa: boolean } }): null — Mounts an `EffectComposer` inside the shell Canvas and takes over rendering (priority-1 `useFrame`, which disables R3F auto-render) to run the configured post chain: RenderPass → GTAO → UnrealBloom → SMAA → exposure meter → OutputPass → Stylize → Grade. Rendered only when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
2429
2482
 
2430
2483
  ## @jgengine/shell/postfx/ScreenEffectsOverlay
2431
2484
 
@@ -2476,7 +2529,7 @@
2476
2529
  ## @jgengine/shell/render/SceneLighting
2477
2530
 
2478
2531
  - `BackdropFog` (function): function BackdropFog({ fog }: { fog: BackdropConfig["fog"] }): React.JSX.Element | null — ⚠ undocumented
2479
- - `ConfiguredLighting` (function): function ConfiguredLighting({ lighting, profile }: { lighting: LightingConfig; profile?: GraphicsProfile }): React.JSX.Element — ⚠ undocumented
2532
+ - `ConfiguredLighting` (function): function ConfiguredLighting({ lighting, profile, modulation, }: { lighting: LightingConfig; profile?: GraphicsProfile; /** Day-cycle scaling for the first directional (the sun) and the ambient fill. */ modulation?: DaylightModulation; }): React.JSX.Element — ⚠ undocumented
2480
2533
  - `POINT_LIGHT_BUDGET` (const): const POINT_LIGHT_BUDGET: 8 — Default cap for combined dynamic point and spot lights.
2481
2534
  - `syncShadowProjection` (function): function syncShadowProjection(light: DirectionalLight): void — Refresh a directional light's shadow projection after its frustum bounds change. R3F pierced `shadow-camera-*` props write the bounds but never call `updateProjectionMatrix()`, so depth renders with the stale default ~10×10 box and `shadowCameraSize` is silently inert.
2482
2535
 
@@ -2604,6 +2657,13 @@
2604
2657
 
2605
2658
  - `drawProceduralTileLayer` (function): function drawProceduralTileLayer(canvas: CanvasFixture): TilemapWorldConfig — Draw the deterministic tile atlas used by textured tile-layer captures and tests.
2606
2659
 
2660
+ ## @jgengine/shell/render/triplanarMaterial
2661
+
2662
+ - `TriplanarAssetMaterial` (function): function TriplanarAssetMaterial({ asset, ...options }: { asset: MaterialAsset } & { tileMetres?: number; sharpness?: number; normalScale?: number }): React.JSX.Element — Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal.
2663
+ - `TriplanarProjectionOptions` (interface): interface TriplanarProjectionOptions — Inputs for {@link applyTriplanarProjection}.
2664
+ - `applyTriplanarProjection` (function): function applyTriplanarProjection(material: THREE.MeshStandardMaterial, options: TriplanarProjectionOptions): THREE.MeshStandardMaterial — Projects a standard material's maps from world position instead of mesh UVs, so world geometry without a `uv` attribute still renders a textured PBR surface. Instanced meshes project in their instanced world space. Skinned meshes keep the bind-pose normal for the blend weights, which is close enough for the static world surfaces this path exists for.
2665
+ - `useTriplanarAssetMaterial` (function): function useTriplanarAssetMaterial(asset: MaterialAsset, options: { tileMetres?: number; sharpness?: number; normalScale?: number } = {}): THREE.MeshStandardMaterial — Render a serializable material asset — one built by `libraryMaterialAsset`, or authored in the editor — with world-space triplanar projection, so world geometry needs no UVs. Tile size defaults to the asset's own `physicalSize`, which the shared library sets per surface.
2666
+
2607
2667
  ## @jgengine/shell/render/useDisposable
2608
2668
 
2609
2669
  - `Disposable` (interface): interface Disposable — Anything exposing dispose() — three.js geometries, materials, textures, render targets.
@@ -177,6 +177,8 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
177
177
  ## day-night-sky — drop-in R3F presenter that drives the engine's existing sky dome and lights from a serializable day-night cycle for a moving, color-graded day-night look
178
178
 
179
179
  - `DayNightSky` (function) · `import { DayNightSky } from "@jgengine/shell/environment"`
180
+ - `skyNoonKeyframes` (function) · `import { skyNoonKeyframes } from "@jgengine/core/render/skyOwnedLighting"`
181
+ - `splitSkyOwnedLighting` (function) · `import { splitSkyOwnedLighting } from "@jgengine/core/render/skyOwnedLighting"`
180
182
 
181
183
  ## debounced-commit — bind a slider/number/color control to an expensive commit path (scene-document patch, settings persist) — instant local mirror, one trailing commit per pause, flush on release/blur/unmount; no raw onChange→patch wiring
182
184
 
@@ -401,6 +403,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
401
403
 
402
404
  - `KillFeed` (function) · `import { KillFeed } from "@jgengine/react"`
403
405
 
406
+ ## light-probes — bake an irradiance volume from authored sky, fill and lamp inputs
407
+
408
+ - `bakeIrradianceVolume` (function) · `import { bakeIrradianceVolume } from "@jgengine/core/render/lightProbes"`
409
+
404
410
  ## live-markers — a self-ticking MarkerSet kept in sync from the live scene each HUD tick — pass a rebuild(markers, ctx) that clears+repopulates, no hand-rolled setInterval
405
411
 
406
412
  - `useLiveMarkers` (function) · `import { useLiveMarkers } from "@jgengine/react"`
@@ -440,6 +446,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
440
446
 
441
447
  - `configureMaterialTexture` (function) · `import { configureMaterialTexture } from "@jgengine/shell/render/materialAsset"`
442
448
 
449
+ ## material-triplanar — render a material asset on world geometry without UVs
450
+
451
+ - `useTriplanarAssetMaterial` (function) · `import { useTriplanarAssetMaterial } from "@jgengine/shell/render/triplanarMaterial"`
452
+
443
453
  ## menu-router — screen stack for a game front-end — push/back/replace/reset with optional Escape-to-back
444
454
 
445
455
  - `useMenuRouter` (function) · `import { useMenuRouter } from "@jgengine/react/menuRouter"`
@@ -84,9 +84,11 @@ Do not create a primitive whose only value is wrapping a `div` with border radiu
84
84
 
85
85
  The post chain itself is never hand-wired render passes. Set `defineGame({ postProcessing })` (`PostProcessingConfig` from `@jgengine/core/render/postProcessing`) and the shell mounts an `EffectComposer` and owns the render: RenderPass → AO → Bloom → tone-map output → Stylize → Grade. Each stage is a config object, `false` to skip, or omitted for its tuned default:
86
86
 
87
- - `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `aces`) + `exposure`.
87
+ - `toneMapping: "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none"` (default `agx`, which keeps hue in saturated highlights; `toon` picks `linear` on purpose) + `exposure` (default 1).
88
+ - `autoExposure: { min, max, adaptationSeconds } | false` — clamped auto-exposure. Every four frames the shell meters the pre-tone-map frame to a 16×16 log-luminance grid and drives `exposure` toward 18% grey (`MID_GREY`), bounded by `min`/`max` (defaults 0.5×..2.5×, 0.6 s adaptation). `exposure` stays the baseline it corrects around; `false` pins it. One white point calibrates the whole pipeline: `WHITE_POINT = 1` (`@jgengine/core/render/colorPipeline`) is the linear luminance AgX prints as white, and pixels up to 8× it count toward metering so a window cannot set the key.
88
89
  - `bloom: { strength, radius, threshold }` — HDR glow around bright pixels (defaults 0.32 / 0.55 / 0.85).
89
- - `grade: { lift, gain, gamma, saturation, vignette, grain }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain.
90
+ - `grade: { lift, gain, gamma, saturation, vignette, grain, lut }` — display-space colour grade: cool-shadow/warm-highlight split, vignette, animated film grain. `lut` is either a strip PNG (`{ url, size }`) or a baked cube (`{ cube: { size, data } }`), trilinearly sampled after the numeric grade.
91
+ - **Authored per game.** A scene document's `environment.grading` (Lighting workspace panel) bakes a small display LUT from shadow/midtone/highlight swatches plus saturation and contrast, and carries the clamped exposure range. The shell reads it from `editorLayers` and folds it over the game's static post config, so the authored LUT and range win where they exist and an ungraded game mounts exactly the chain it did before. Author it in `editor.scene.json`; do not hardcode a grade in the game config.
90
92
  - `ao: { radius, intensity, distanceFalloff, blend }` — ground-truth ambient occlusion. Omitted runs `DEFAULT_AO` on tiers whose `postStages.ao` is on (medium and up; low skips it); `ao: false` turns it off everywhere. It adds a normal/depth prepass (one extra draw per opaque mesh) and is fill-heavy: 8 samples on medium, 12 on high, 16 on ultra.
91
93
  - `stylize: { outline, bands, pixelSize }` — art style without touching materials. `outline: { color, thickness, opacity, depthThreshold, normalThreshold, fadeDistance }` traces ink lines from depth and normal edges (one extra normal/depth scene render, shadow maps reused); `bands` quantizes brightness into cel bands keeping hue; `pixelSize` snaps the frame to retro blocks. Mix freely: outline alone is a clean comic ink, `bands: 3` plus outline is cartoon, `pixelSize` plus `bands` is retro. Mark custom overlay effects with `POSTFX_OVERLAY_USERDATA` so they are not outlined; transparent, alpha-cut, points, lines and sprites are skipped automatically.
92
94
 
@@ -59,7 +59,9 @@ NPC decisions are a `decisionGraph` behavior: the graph and its blackboard are d
59
59
 
60
60
  For a bounded local detour over movement collision geometry, use `planSolidRoute` (`nav/solidRoute`) with an indexed `solidObstaclesNear` or `sourceObstaclesNear` query. Handle `no-path` and `budget` separately; retain caller movement, goals, terrain and retry policy. See [reference.md](reference.md#local-collision-routes) for limits.
61
61
 
62
- Route on a polygon nav mesh through one `createNavMeshQuery(mesh)` per mesh; price terrain and gate doors with `areaCosts` via `retune`, and keep `maxNodes` bounded so a long request returns a `partial` route instead of stalling a frame.
62
+ Route on an editor-baked polygon nav mesh through `createNavRuntime` (`nav/navRuntime`): read the bake off the scene document with `navMeshFromBake(document.bakes)`, price terrain and gate doors with `areaCosts`, author doors, ladders and jumps as `AuthoredNavLink` world points, and call `applyMesh(nextBake)` after an edit — polygons that survive keep their identity through the returned `remap`, so only routes crossing changed polygons need re-planning (`createNavCrowd`'s `revalidate`). A bare `createNavMeshQuery` per mesh is still the lower-level seam; keep `maxNodes` bounded so a long request returns a `partial` route instead of stalling a frame.
63
+
64
+ Move many agents across that mesh with `createNavCrowd` (`nav/navCrowd`): it plans corridors, follows the funnel-smoothed waypoints, keeps them apart with `createRvoCrowd` (`ai/rvo`) reciprocal velocity obstacles, bounds path searches to a per-tick time and count budget, and serializes through `snapshot`/`restore`. Give a crowd converging on one spot a `spread` radius so agents fan out (`ai/crowd`'s stable per-id offset) instead of stacking. Avoidance keeps agents inside the mesh's outer bounds; interior walls remain the game's collision layer.
63
65
 
64
66
  ### Fallback-seam diagnostics
65
67