@call-me-sensei/toonlab 0.2.0 → 0.3.1

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 (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +231 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +752 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,825 @@
1
+ # Lighting
2
+
3
+ `@call-me-sensei/toonlab/lighting` is ToonLab's portable lighting-authoring
4
+ layer. It gives games one vocabulary for lights, reusable styles and fixtures,
5
+ quality budgets, runtime selection, diagnostics, and engine export. The module
6
+ coordinates Three.js lights and preserves metadata for ToonLab's stylized
7
+ material response; it does not replace an engine renderer,
8
+ global-illumination solver, or ray tracer.
9
+
10
+ ## The three artifacts
11
+
12
+ Lighting identity is authored once and referenced everywhere, instead of
13
+ configuring lights per scene:
14
+
15
+ | Artifact | Document | Cardinality | What it captures |
16
+ |---|---|---|---|
17
+ | **Lighting style** | `toonlab/lighting-style` | one per game (a few variants) | The full day as one curve: sun color/intensity per hour, sun path, ambient policy, sky/fog palette, exposure philosophy, shadow policy, fixture response |
18
+ | **Light fixture** | `toonlab/light-fixture` | a small vocabulary | One *kind* of light ("street lamp", "lantern"): base descriptor + seeded variation domains + flicker + day/night schedule |
19
+ | **Scene overlay** | runtime object | tiny, per scene/zone | Fixture placements plus small adjustments (exposure nudge, ambient warmth) blended in and out |
20
+
21
+ The mental model: *the sun at noon looks a certain way, at 18:00 a certain
22
+ way, a street lamp looks a certain way — scenes reference those, they never
23
+ restate them.* Same recipe + seed resolves identically in the Lighting Lab,
24
+ through MCP, and in a shipped game.
25
+
26
+ ## Quick start: the lighting system
27
+
28
+ ```js
29
+ import { createLightingSystem } from '@call-me-sensei/toonlab/lighting';
30
+
31
+ const lighting = createLightingSystem({
32
+ camera,
33
+ renderer,
34
+ scene,
35
+ style: 'call-me-sensei', // or 'storybook', a saved document, or settings
36
+ });
37
+ lighting.attach({ fog: scene.fog, driveSunPosition: true });
38
+
39
+ lighting.setTimeOfDay(18.5); // the whole look follows the style's day cycle
40
+ lighting.place('street-lamp', [4, 0, 2]); // seeded variation per placement
41
+ lighting.place('cms-lantern', [1.2, 0, 0.5]); // vivid Call Me Sensei fixture
42
+
43
+ // per frame
44
+ lighting.update(delta, camera); // flicker, overlay blends, light budgets
45
+ ```
46
+
47
+ Built-in styles: `call-me-sensei` (vivid Genshin/ZZZ-direction daylight,
48
+ luminous shadows, fixture-lit blue nights), `storybook`, `golden-summer`,
49
+ `overcast-pastel`, `neon-night`. Built-in fixtures include `street-lamp`,
50
+ `paper-lantern`, `window-glow`, `neon-sign`, `campfire`, `shrine-candle`, and
51
+ the Call Me Sensei set (`cms-street-lamp`, `cms-lantern`, `cms-city-neon`).
52
+ Both registries are open: `registerLightingStylePreset` /
53
+ `registerLightFixture` (or their document variants) add your own without
54
+ touching built-ins.
55
+
56
+ ### Working with worlds
57
+
58
+ `lighting.attachWorld(world)` binds a `createStylizedWorld` result. The style
59
+ drives its world-owned sun-direction adapter (so the visible disc, directional
60
+ light, cast shadows, Grass/Flower/Tree shader inputs, and Water highlight
61
+ agree), sun color/intensity/accents, environment-material sky/fog tints, scene
62
+ fog, exposure, and private
63
+ priority-100 Sky and Water layers.
64
+
65
+ If `world.weather` exists, `attachWorld` also calls
66
+ `weather.setLightingSystem(lighting)`. Lighting remains the sole writer for
67
+ sun, ambient, and fog color; Weather supplies normalized modulation and owns
68
+ higher-priority Sky/Water layers. `detach()` restores the prior world sun
69
+ direction and removes only Lighting's layers, leaving standalone Weather
70
+ active. The world bridge is `getSun`/`setSun({ direction, color, sky })` (with
71
+ `getSunDirection`/`setSunDirection` retained for direction-only compatibility).
72
+ In custom integrations, the equivalent Weather bridge is
73
+ `lighting.setWeatherModulation({ sunIntensityScale, sunColorTint,
74
+ ambientScale, fogColorOverride, ... })`.
75
+
76
+ Sun translation remains world-owned because the shadow-follow window moves the
77
+ light and target around the player. Standalone scenes without a world adapter
78
+ may pass `driveSunPosition: true` instead.
79
+
80
+ ### Generating styles and fixtures
81
+
82
+ Both artifact types have generator recipes on the shared grammar
83
+ (`core/generation.js`): domains, locks, seeds, deterministic resolution.
84
+
85
+ ```js
86
+ import {
87
+ createLightingStyleGeneratorRecipe,
88
+ resolveLightingStyleGeneratorRecipe,
89
+ } from '@call-me-sensei/toonlab/lighting';
90
+
91
+ const recipe = createLightingStyleGeneratorRecipe('my-style', {
92
+ family: 'call-me-sensei', // anime-day | call-me-sensei | golden | noir-neon | pastel-overcast
93
+ locks: ['sun.dayKelvin'], // subtree locks respected across reseeds
94
+ seed: 4207,
95
+ });
96
+ const settings = resolveLightingStyleGeneratorRecipe(recipe); // deterministic
97
+ ```
98
+
99
+ Fixture generation (`createLightFixtureGeneratorRecipe`, families
100
+ `warm-practical` / `cms-practical` / `flame` / `neon`) samples the fixture
101
+ *and* the spread of its per-placement variation, so one seed yields a fixture
102
+ that itself yields endless placement variety.
103
+
104
+ ### Lifecycle and safety
105
+
106
+ - `setTimeOfDay` / `advanceTime` / `setStyle` are hot updates; placements
107
+ survive a style swap.
108
+ - `place()` returns a handle: `{ id, light, set(overrides), remove() }`.
109
+ Same-type edits patch the realized light in place — no rig rebuild.
110
+ - Area-light fixtures (`window-glow`, neon tubes) wait for the LTC lookup
111
+ textures to load (`area-ltc-pending` in diagnostics) instead of crashing the
112
+ node backends; `ensureAreaLightSupport()` preloads them.
113
+ - `stats()` / `toJSON()` / `reset()` / `dispose()` follow the standard runtime
114
+ contract; `dispose()` restores fog, exposure, the complete world/physical sun
115
+ state, and every environment tint it captured, unbinds Weather, and clears
116
+ only its own Sky/Water layers. Dispose order is safe: if Weather is removed
117
+ first, Lighting restores Weather's pre-condition sun baseline when it later
118
+ detaches.
119
+
120
+ ### Known integration limits (v1)
121
+
122
+ - On the node backends, ToonLab's toon/terrain materials read at most the
123
+ first directional light, 8 point lights, 4 spot lights, and 2 hemisphere
124
+ lights through the shared toon light mirror; area lights affect standard
125
+ materials only. Fixture budgets should respect those caps in toon worlds.
126
+ - Inside `createStylizedWorld`, the sun's shadow pass and position remain
127
+ world-owned; the style contributes color/intensity/accents.
128
+
129
+ ---
130
+
131
+ The sections below cover the lower-level engine the system is built on —
132
+ light descriptors, rigs, quality budgets, runtime selection, and engine
133
+ export. Reach for them when you need one-off lights or a custom realization
134
+ layer; most games should stay on styles + fixtures above.
135
+
136
+ The engine-export boundary is especially clear for Unreal Engine:
137
+
138
+ > **MegaLights is an engine-native Unreal Engine 5.8 direct-lighting feature.**
139
+ > ToonLab does not reimplement MegaLights in JavaScript or WebGPU. ToonLab
140
+ > records lighting intent and exports a versioned manifest that an Unreal
141
+ > adapter can realize with MegaLights. Lumen remains a separate Unreal-native
142
+ > choice for global illumination and reflections.
143
+
144
+ This separation keeps a lighting recipe useful in a browser, a Three.js game,
145
+ an editor tool, and an Unreal import pipeline without pretending those targets
146
+ have identical renderers.
147
+
148
+ ## Low-level quick start
149
+
150
+ ```js
151
+ import {
152
+ createLightDescriptor,
153
+ createLightingManager,
154
+ createLightingRecipe,
155
+ resolveLightingQualityPreset,
156
+ } from '@call-me-sensei/toonlab/lighting';
157
+
158
+ const recipe = createLightingRecipe({
159
+ id: 'harbor-night',
160
+ name: 'Harbor Night',
161
+ lights: [
162
+ createLightDescriptor('directional', {
163
+ id: 'moon',
164
+ name: 'Moon',
165
+ position: [-18, 32, -24],
166
+ target: [0, 0, 0],
167
+ color: '#b9ceff',
168
+ intensity: { value: 0.8, unit: 'lux' },
169
+ tags: ['exterior', 'key'],
170
+ castShadow: true,
171
+ shadow: { enabled: true, mapSize: 2048, priority: 100 },
172
+ }),
173
+ createLightDescriptor('point', {
174
+ id: 'pier-lantern',
175
+ name: 'Pier Lantern',
176
+ position: [8, 2.4, -3],
177
+ color: { temperatureKelvin: 2200 },
178
+ intensity: { value: 420, unit: 'lumens' },
179
+ distance: 12,
180
+ maxDistance: 24,
181
+ tags: ['exterior', 'practical'],
182
+ castShadow: true,
183
+ shadow: { enabled: true, mapSize: 512, priority: 40 },
184
+ }),
185
+ ],
186
+ shadowPolicy: {
187
+ mode: 'budgeted',
188
+ allowedTypes: ['directional', 'point', 'spot'],
189
+ maxShadowedLights: 4,
190
+ maxShadowMapPixels: 4 * 2048 * 2048,
191
+ updateMode: 'auto',
192
+ },
193
+ });
194
+
195
+ const lighting = createLightingManager({
196
+ renderer,
197
+ scene,
198
+ camera,
199
+ recipe,
200
+ quality: resolveLightingQualityPreset('high'),
201
+ textureResolver: async ({ uri }) => assetLoader.loadAsync(uri),
202
+ });
203
+
204
+ lighting.addToScene(scene); // optional when `scene` was supplied above
205
+
206
+ // Before rendering each frame. `focus` makes local-light distance selection
207
+ // follow the player rather than a cinematic camera cut.
208
+ lighting.update({ camera, focus: player.position });
209
+
210
+ // Runtime edits retain the stable id.
211
+ lighting.updateLight('pier-lantern', {
212
+ intensity: { value: 560, unit: 'lumens' },
213
+ });
214
+ lighting.setLightEnabled('pier-lantern', powerIsOn);
215
+
216
+ // On teardown:
217
+ lighting.dispose();
218
+ ```
219
+
220
+ `createLightingRig(options)` is an alias for `createLightingManager(options)`
221
+ for codebases that call every scene-light collection a rig. The manager owns
222
+ only the objects it creates. The host continues to own the renderer, scene,
223
+ camera, environment materials, and render loop.
224
+
225
+ ## Public API at a glance
226
+
227
+ The lighting subpath centralizes the portable contract:
228
+
229
+ | Concern | Public API |
230
+ |---|---|
231
+ | Runtime | `createLightingManager`, `createLightingRig`, `realizeLightingRecipe` |
232
+ | Descriptors | `LIGHT_TYPES`, `createLightDescriptor`, the eight type-specific creators, and the cookie/IES/linking/shadow/artistic helpers |
233
+ | Color and intensity | `createLightColor`, `createLightIntensity`, `colorTemperatureToRgb`, unit conversion helpers |
234
+ | Recipe documents | `createLightingRecipe`, `validateLightingRecipe`, `assertLightingRecipe`, `serializeLightingRecipe`, `deserializeLightingRecipe` |
235
+ | Look documents | `createLightingLook`, `validateLightingLook`, `assertLightingLook`, `serializeLightingLook`, `deserializeLightingLook` |
236
+ | Presets | Frozen `LIGHTING_*_PRESETS` registries, `getLightingPresetOptions`, `resolveLightingPreset`, and the four type-specific resolvers |
237
+ | Quality profiles | `createLightingQualityProfile`, `resolveLightingQualityPreset` |
238
+ | Capability planning | `createLightingCapabilityReport`, `getLightingTypeCapability`, `snapshotLightingCapabilities` |
239
+ | Unreal handoff | `exportLightingRecipeToUnreal58`, `serializeUnrealLightingManifest` |
240
+
241
+ Validation functions return `{ ok, valid, errors, warnings }` and do not
242
+ coerce their input. `assert...` returns the supplied valid document or throws.
243
+ `deserialize...` accepts JSON text or an already-parsed object, validates it,
244
+ returns a fresh normalized document, and never adds anything to a scene.
245
+
246
+ ## Light descriptors
247
+
248
+ A descriptor is JSON-safe authoring intent, not a `THREE.Light`. Stable `id`
249
+ values are mandatory inside a recipe because selection, patching, animation,
250
+ diagnostics, and engine import all refer to them.
251
+
252
+ ```js
253
+ const sign = createLightDescriptor('rectArea', {
254
+ id: 'ramen-sign',
255
+ name: 'Ramen sign bounce',
256
+ position: [2.5, 3.1, -1.2],
257
+ target: [0, 1.8, -1.2],
258
+ width: 1.8,
259
+ height: 0.5,
260
+ color: [1, 0.12, 0.05],
261
+ intensity: { value: 900, unit: 'lumens' },
262
+ linking: {
263
+ includeTags: ['street', 'characters'],
264
+ excludeTags: ['sky'],
265
+ },
266
+ tags: ['night', 'practical', 'neon'],
267
+ artistic: { role: 'practical', bandSoftness: 0.16 },
268
+ userData: { fixtureId: 'shop-17/sign' },
269
+ });
270
+ ```
271
+
272
+ Every descriptor can carry these common fields:
273
+
274
+ | Field | Meaning |
275
+ |---|---|
276
+ | `id`, `name` | Stable machine id and optional display name. |
277
+ | `type` | One of `LIGHT_TYPES`. |
278
+ | `enabled` | Authored state; budget culling is reported separately. |
279
+ | `position`, `target` | World-space source and target points in meters. Directional, spot, and area lights derive orientation from these points. |
280
+ | `color` | RGB/hex input or `{ temperatureKelvin, tint }`; normalized documents store `{ rgb, temperatureKelvin, tint }`. |
281
+ | `intensity` | A scalar or `{ value, unit, artisticMultiplier, referenceDistance }`. Units are `unitless`, `lux`, `candela`, `lumens`, and `nits`. |
282
+ | `distance`, `maxDistance`, `decay` | Three.js attenuation distance, runtime selection distance, and attenuation exponent. |
283
+ | `width`, `height`, `angle`, `penumbra` | Area and spot shape parameters used when the selected backend can represent them. |
284
+ | `priority` | Runtime selection priority before distance tie-breaking. |
285
+ | `castShadow`, `shadow` | Request plus `{ enabled, priority, mapSize, bias, normalBias, radius, near, far, extent }`. |
286
+ | `cookie` | `{ uri, key, channel, intensity }` projected texture reference; core realization supports spot lights. |
287
+ | `ies` | `{ uri, key, intensity }` photometric-profile reference for point and spot lights. |
288
+ | `linking` | `{ includeTags, excludeTags }` receiver intent. |
289
+ | `layers` | Three.js layer indices applied directly to the realized light. |
290
+ | `artistic` | Toon metadata: role, band softness, shadow tint, rim influence, and diffuse/specular multipliers. |
291
+ | `tags`, `userData` | Selection, grouping, provenance, and host-specific JSON data. |
292
+
293
+ Colors in serialized documents are normalized to an object containing RGB or
294
+ temperature intent plus tint. Resource fields contain references, never live
295
+ textures or engine objects. A
296
+ `textureResolver` supplied to the manager decides whether and how a URI is
297
+ loaded, so importing untrusted JSON does not implicitly fetch network assets.
298
+
299
+ ### Light types
300
+
301
+ `LIGHT_TYPES` includes:
302
+
303
+ | Type | Intended use |
304
+ |---|---|
305
+ | `ambient` | Uniform, inexpensive scene fill. |
306
+ | `directional` | Sun, moon, or another effectively infinite source. |
307
+ | `point` | Bulb, flame, orb, or omnidirectional practical. |
308
+ | `spot` | Flashlight, stage spot, downlight, or projector. |
309
+ | `hemisphere` | Cheap sky/ground ambient split. |
310
+ | `rectArea` | Window, panel, sign, or rectangular softbox. |
311
+ | `discArea` | Round softbox, portal, or broad circular emitter. |
312
+ | `tubeArea` | Fluorescent tube, neon stroke, or long strip source. |
313
+
314
+ Disc and tube descriptors are first-class portable intent even where Three.js
315
+ does not expose matching native light classes. The core Three.js realization
316
+ uses a rectangular-area approximation and records that fallback in
317
+ diagnostics. An engine adapter may realize the original disc or tube intent
318
+ more accurately.
319
+
320
+ IES profiles and light linking follow the same rule: the core preserves and
321
+ validates the authoring intent. A host adapter consumes them when supported;
322
+ otherwise the manager uses an unprofiled light or broad receiver set and
323
+ emits a structured fallback rather than silently discarding the field.
324
+
325
+ ## Recipes and the four preset levels
326
+
327
+ The hierarchy deliberately separates reusable art from platform budgets:
328
+
329
+ ```text
330
+ luminaire -> rig -> look
331
+ + quality
332
+ |
333
+ v
334
+ resolved recipe
335
+ ```
336
+
337
+ | Level | Contains | Examples |
338
+ |---|---|---|
339
+ | **Luminaire** | One fixture descriptor plus default resource references and metadata. | paper lantern, torch, neon tube, streetlight, window panel |
340
+ | **Rig** | Multiple luminaires/lights with transforms and named artistic roles. | three-point character rig, outdoor sun, warm interior, night market |
341
+ | **Look** | A recipe or rig selection plus environment, time-of-day, material-response, exposure, and post intent. | overcast noon, warm interior evening, moonlit harbor |
342
+ | **Quality** | Portable active-light, distance, feature, and shadow budgets only. | `mobile`, `balanced`, `high`, `cinematic` |
343
+
344
+ Do not put performance policy into a luminaire or rig. The same night-market
345
+ rig should resolve under a mobile budget and a high-end budget without
346
+ forking the artistic preset.
347
+
348
+ `getLightingPresetOptions(kind)` returns lab-ready
349
+ `{ id, label, description, kind }` records. `kind` is `luminaire`, `rig`,
350
+ `look`, or `quality`; omit it to list every registry. The four resolvers return
351
+ fresh normalized values so callers can safely apply overrides.
352
+
353
+ The built-in registries are frozen. V1 intentionally avoids a process-global
354
+ custom-preset registry: treat a custom luminaire as a descriptor, a custom rig
355
+ as a recipe, a custom quality preset as a quality-profile object, and a custom
356
+ look as a look document. Store those values in a project catalog or the
357
+ Lighting Lab's local library and serialize recipes/looks for reuse. The
358
+ `createLightingLookPreset`/`validateLightingLookPreset`/serialization aliases
359
+ make that saved-preset intent explicit without inventing a second look schema.
360
+
361
+ ### Recipe document
362
+
363
+ The canonical portable recipe shape is:
364
+
365
+ ```js
366
+ {
367
+ type: 'toonlab/lighting-recipe',
368
+ schemaVersion: 1,
369
+ id: 'shrine-evening',
370
+ name: 'Shrine Evening',
371
+ lights: [ /* normalized descriptors */ ],
372
+ shadowPolicy: { /* global budget and defaults */ },
373
+ metadata: {
374
+ author: 'Example Studio',
375
+ sourcePreset: 'rig/shrine-courtyard',
376
+ },
377
+ }
378
+ ```
379
+
380
+ A recipe is the resolved, deterministic scene-light snapshot. Preset ids may
381
+ be retained in `metadata` for provenance, but a shipped recipe should contain
382
+ all resolved descriptors required to reproduce the setup. This avoids a
383
+ registry update changing a released game unexpectedly.
384
+
385
+ ### Look document
386
+
387
+ A look packages lighting with the adjacent systems needed to reproduce the
388
+ shot:
389
+
390
+ ```js
391
+ const look = createLightingLook({
392
+ id: 'shrine-rain-1800',
393
+ name: 'Shrine Rain at 18:00',
394
+ recipe,
395
+ quality: 'high',
396
+ environment: {
397
+ preset: 'exteriorDay',
398
+ timeOfDay: 18,
399
+ weatherPreset: 'rain',
400
+ fog: { density: 0.0015 },
401
+ },
402
+ post: {
403
+ preset: 'call_me_sensei',
404
+ exposure: 1.05,
405
+ },
406
+ metadata: { shot: 'courtyard-wide' },
407
+ });
408
+ ```
409
+
410
+ The lighting manager realizes `look.recipe`. Environment, weather, water,
411
+ sky, and post-processing remain owned by their existing ToonLab modules. A
412
+ world or lab coordinator reads the other look sections and applies them to
413
+ those systems; unknown integration data is retained for engine adapters.
414
+
415
+ ## Serialization and preset reuse
416
+
417
+ Recipes and looks use versioned JSON documents:
418
+
419
+ ```js
420
+ import {
421
+ deserializeLightingRecipe,
422
+ serializeLightingRecipe,
423
+ validateLightingRecipe,
424
+ } from '@call-me-sensei/toonlab/lighting';
425
+
426
+ const json = serializeLightingRecipe(recipe, { pretty: true });
427
+ localStorage.setItem('my-game/lighting/shrine', json);
428
+
429
+ try {
430
+ const imported = deserializeLightingRecipe(
431
+ localStorage.getItem('my-game/lighting/shrine'),
432
+ );
433
+ lighting.setRecipe(imported);
434
+ } catch (error) {
435
+ console.error(error.message);
436
+ }
437
+
438
+ // For a non-throwing check of an already-parsed object:
439
+ const validation = validateLightingRecipe(JSON.parse(json));
440
+ console.warn(...validation.warnings);
441
+ ```
442
+
443
+ The serializer emits version-stamped JSON and the schemas do not embed
444
+ textures, `Object3D`s, callbacks, or renderer state. Validation:
445
+
446
+ - checks document tags, schema versions, descriptor structure, and ids;
447
+ - rejects duplicate light ids and invalid descriptor shapes;
448
+ - warns about cookies, IES, physical-unit, or shadow combinations that the
449
+ portable descriptor contract cannot realize directly;
450
+ - preserves JSON-safe `metadata` for round trips;
451
+ - rejects documents newer than the supported schema instead of guessing;
452
+ - leaves normalization to `createLightingRecipe`/`deserializeLightingRecipe`.
453
+
454
+ Store source-controlled presets as ordinary JSON beside game content. For
455
+ runtime user presets, store the serialized recipe/look and treat resource URIs
456
+ as untrusted input. Applications decide which URI schemes and asset roots a
457
+ `textureResolver` permits.
458
+
459
+ ## Runtime manager
460
+
461
+ `createLightingManager(options)` turns a normalized recipe into Three.js scene
462
+ objects and continuously enforces the active quality budget.
463
+
464
+ ```js
465
+ const lighting = createLightingManager({
466
+ scene,
467
+ camera,
468
+ recipe,
469
+ quality: 'high',
470
+ capabilities: createLightingCapabilityReport({ renderer }),
471
+ textureResolver,
472
+ });
473
+ ```
474
+
475
+ Manager surface:
476
+
477
+ | Member | Contract |
478
+ |---|---|
479
+ | `group` | Root `THREE.Group` containing manager-owned lights/helpers. |
480
+ | `recipe`, `quality` | Current normalized snapshots. Treat them as read-only. |
481
+ | `setRecipe(recipeOrPresetId)` | Normalize the recipe, rebuild manager-owned lights, and re-plan budgets. |
482
+ | `setQuality(idOrQuality)` | Change only runtime policy; authored light descriptors remain unchanged. |
483
+ | `updateLight(id, patch)` | Normalize and replace one descriptor while retaining its stable id. |
484
+ | `setLightEnabled(id, enabled)` | Toggle authored state without losing settings. |
485
+ | `getLight(id)` | Return the realized Three.js light, or `null` for an unknown id. |
486
+ | `addToScene(scene)`, `removeFromScene()` | Explicit scene attachment for hosts that did not pass `scene`. |
487
+ | `applyLook(lookOrPresetId)` | Apply its recipe/quality and return `{ environment, post }` for the host coordinator. |
488
+ | `update({ camera, focus })` | Re-evaluate distance/priority culling, shadow allocation, and diagnostics. |
489
+ | `requestShadowUpdate(id?)` | Mark one or all existing shadow maps for refresh. |
490
+ | `subscribe(listener)` | Observe `selection`, `recipe`, `quality`, `light`, and `cookie` diagnostics events. |
491
+ | `getDiagnostics()` | Return a serializable backend, budget, selection, shadow, and fallback report. |
492
+ | `dispose()` | Detach and release manager-owned objects. Resolved cookie textures remain host-owned unless `disposeCookieTextures: true` was requested. |
493
+
494
+ Selection is deterministic for one recipe order and focus point. Global lights
495
+ receive a fixed preference; local lights are ranked by descriptor `priority`
496
+ then distance to `focus`. The manager applies each type cap and then the total
497
+ cap, using recipe order as the final tie-breaker. The current v1 selector does
498
+ not estimate projected screen influence or add hysteresis, so set meaningful
499
+ priorities and avoid placing many equal-priority lights exactly at a budget
500
+ boundary.
501
+
502
+ ## Capabilities and deterministic fallbacks
503
+
504
+ `createLightingCapabilityReport({ renderer })` describes
505
+ what the current target can realize. Passing the report into a manager makes
506
+ planning deterministic and testable; omitting it lets the manager inspect the
507
+ renderer.
508
+
509
+ The report exposes `backend`, `supportedLightTypes`, renderer limits, and
510
+ feature-specific values for area-light realization, cookies, IES, linking,
511
+ shadows, many-light rendering, MegaLights, and Lumen. The manager keeps a
512
+ recipe valid when a feature degrades and exposes the chosen approximation in
513
+ diagnostics.
514
+
515
+ ### Supported/fallback matrix
516
+
517
+ This table describes the portable ToonLab/Three.js baseline. An engine adapter
518
+ can report stronger support without changing the recipe.
519
+
520
+ | Feature | WebGPU/TSL | WebGL2 fallback | Unreal 5.8 export intent | Portable fallback |
521
+ |---|---|---|---|---|
522
+ | Directional light | Native; the custom ToonLab shadow bridge consumes one primary directional shadow | Native; same primary-shadow constraint on custom materials | `DirectionalLight` intent | Author one primary shadowed directional when using the custom bridge |
523
+ | Point light | Native, subject to material/quality light limits | Native, subject to tighter quality limits | `PointLight` intent; may opt into engine MegaLights | Priority/distance-cull over budget |
524
+ | Spot light | Native, subject to material/quality light limits | Native, subject to tighter quality limits | `SpotLight` intent; may opt into engine MegaLights | Priority/distance-cull over budget |
525
+ | Hemisphere light | Native ambient approximation | Native ambient approximation | Sky/ambient intent for adapter mapping | Fold into ambient/sky contribution |
526
+ | Ambient light | Native uniform fill | Native uniform fill | Approximate `SkyLight` intent | Keep as non-directional fill |
527
+ | Rect area light | Native `THREE.RectAreaLight` object; material support remains renderer-dependent | Same material caveat | `RectLight` intent | Cull when `allowAreaLights` is false |
528
+ | Disc/tube area light | `THREE.RectAreaLight` approximation | Same approximation | Preserve original source-shape intent on a `RectLight` mapping | Approximate and report shape loss |
529
+ | Local-light shadows | Three scene objects can request them; custom ToonLab TSL material consumption is backend/material dependent | Backend/material dependent and budget-limited | Preserve cast-shadow request | Disable lowest-priority shadows first; retain direct light |
530
+ | Cascaded sun shadows | Not provided; `directionalCascades` is recipe intent only | Not provided | Preserve cascade-count intent in source policy | One directional shadow camera per realized light |
531
+ | Shadow atlas | Budget/selection policy only; no general portable atlas renderer | Budget/selection policy only | Adapter may map to engine virtual shadow resources | Independent engine shadow maps within texel budget |
532
+ | Cookies/gobos | Spot `light.map` when `textureResolver` returns a `THREE.Texture` | Same, subject to quality | Preserve light-function texture reference | Untextured light plus cookie status diagnostic |
533
+ | IES profiles | Validated metadata in core | Validated metadata in core | Preserve IES asset reference and multiplier | Unprofiled point/spot plus diagnostic |
534
+ | Light linking | Three layers applied directly; tag linking is metadata/host hook | Same | Preserve channels and include/exclude intent | Broad receiver set plus warning |
535
+ | Many-light selection | CPU priority/distance selection and quality caps | Same with a measured profile | Export all authored lights and MegaLights intent | Cull deterministically before upload |
536
+ | Clustered/Forward+ lighting | Not promised by the baseline contract | Not promised | Engine-owned | Fixed/budgeted active set |
537
+ | Stochastic ray-traced direct lighting | Not implemented | Not available | MegaLights preference only | Raster direct lights and shadow maps |
538
+ | Dynamic GI/reflections | Separate lightmap, ambient-probe, planar-reflection, sky, and post hooks | Same separate hooks | Lumen preference is exported separately | No GI or reflection solve in the lighting manager |
539
+
540
+ ToonLab's current custom character/environment shaders have bounded local
541
+ light arrays. A quality profile must never assume that creating more Three.js
542
+ lights means every custom material consumes them. Manager diagnostics describe
543
+ realized Three.js objects and quality selection, not per-material shader-array
544
+ consumption; combine them with the selected material/backend limits.
545
+
546
+ ## Quality and shadow budgets
547
+
548
+ A quality preset is an explicit budget. Built-ins are `mobile`, `balanced`,
549
+ `high`, and `cinematic`; a custom profile can constrain:
550
+
551
+ ```js
552
+ const handheld = {
553
+ id: 'handheld',
554
+ label: 'Handheld',
555
+ maxLights: 10,
556
+ maxLightsByType: {
557
+ ambient: 1,
558
+ hemisphere: 1,
559
+ directional: 1,
560
+ point: 6,
561
+ spot: 2,
562
+ rectArea: 0,
563
+ discArea: 0,
564
+ tubeArea: 0,
565
+ },
566
+ maxDistance: 55,
567
+ maxShadowedLights: 1,
568
+ maxShadowMapPixels: 2048 * 2048,
569
+ shadowMapSizeScale: 0.5,
570
+ allowAreaLights: false,
571
+ allowCookies: false,
572
+ };
573
+
574
+ lighting.setQuality(handheld);
575
+ ```
576
+
577
+ Recipe `shadowPolicy` and quality shadow fields both constrain resources; the
578
+ stricter count and pixel limit wins. Allocation order is:
579
+
580
+ 1. Cull disabled lights, quality-disabled area lights, and local lights beyond
581
+ the effective `maxDistance`.
582
+ 2. Apply `maxLightsByType`, then the quality `maxLights` total.
583
+ 3. Keep active lights whose `castShadow` and `shadow.enabled` are both true and
584
+ whose type is in `shadowPolicy.allowedTypes`.
585
+ 4. Rank shadow candidates by light priority plus shadow priority, then distance
586
+ and recipe order.
587
+ 5. Allocate until either `maxShadowedLights` or `maxShadowMapPixels` is spent.
588
+ A point-light shadow counts all six cube faces. Denied shadows keep their
589
+ direct light.
590
+
591
+ `shadowPolicy.updateMode` is `everyFrame`, `auto`, or `manual`. Manual hosts
592
+ call `requestShadowUpdate(id?)` when a light or caster changes. Each light's
593
+ effective map allocation appears as `shadowPixels` in diagnostics.
594
+
595
+ ## Diagnostics and debug views
596
+
597
+ `getDiagnostics()` is intentionally serializable so labs, automated tests,
598
+ telemetry, and bug reports can all inspect the same facts:
599
+
600
+ ```js
601
+ const report = lighting.getDiagnostics();
602
+
603
+ console.table(report.entries);
604
+ console.log('active', report.selectedIds);
605
+ console.log('shadowed', report.shadowedIds);
606
+ console.warn(...report.warnings);
607
+ ```
608
+
609
+ The report includes:
610
+
611
+ - backend, recipe id, quality id, and total/active/shadowed counts;
612
+ - `countsByType`, `selectedIds`, and `shadowedIds`;
613
+ - one `entries` row per descriptor with active/shadowed state, cull reason,
614
+ distance, score, allocated shadow pixels, cookie/IES status, and fallback;
615
+ - capability, approximation, missing-resource, budget, IES, and linking
616
+ warnings.
617
+
618
+ The Lighting Lab visualizes `final`, `unlit`, `influence`, `complexity`, and
619
+ `shadows`. Production games can build lighter
620
+ overlays from the same report without importing lab code.
621
+
622
+ ## Lighting Lab
623
+
624
+ Run the dedicated editor at `/lighting-lab/`. It is the authoring and
625
+ diagnostics surface for this module. Character Shader Lab remains focused on
626
+ how character materials respond to light, while Sky Lab authors the visible
627
+ sky baseline rather than the scene's actual light and shadow policy. See
628
+ [Lab responsibilities](lab-architecture.md).
629
+
630
+ The Lighting Lab provides:
631
+
632
+ - a light outliner with add, duplicate, delete, solo, mute, and stable id
633
+ display/preservation;
634
+ - transform gizmos plus numeric position/target, shape, photometry, linking,
635
+ resource, and shadow controls;
636
+ - Character Studio, Material Spheres, Interior Room, Outdoor Vista, and
637
+ Many-Lights Stress stages;
638
+ - time-of-day preview that moves a tagged sun and adjusts stage sky/exposure;
639
+ - backend and quality selection with explicit capability/fallback reporting;
640
+ - live total/active/culled/shadowed counts and budget warnings;
641
+ - final, unlit, influence, complexity, and shadow-camera debug modes;
642
+ - built-in luminaire/rig/look/quality presets plus locally saved recipes;
643
+ - JSON import, copy, download, local save, and deterministic re-open;
644
+ - Unreal 5.8 manifest copy/download with MegaLights and Lumen intent shown
645
+ separately.
646
+
647
+ The Many-Lights Stress stage is a workload and fallback test, not evidence of
648
+ a hidden MegaLights renderer. It should make culling, quality limits, shadow
649
+ allocation, and backend differences obvious.
650
+
651
+ ## Unreal Engine 5.8 export contract
652
+
653
+ `exportLightingRecipeToUnreal58(recipe, options)` produces a JSON-safe
654
+ interchange manifest. It does **not** write `.uasset` files, launch Unreal,
655
+ change project settings, compile shaders, or enable plugins.
656
+
657
+ ```js
658
+ import {
659
+ exportLightingRecipeToUnreal58,
660
+ serializeUnrealLightingManifest,
661
+ } from '@call-me-sensei/toonlab/lighting';
662
+
663
+ const manifest = exportLightingRecipeToUnreal58(look.recipe, {
664
+ megaLights: 'prefer', // 'disabled' | 'prefer' | 'require'
665
+ lumen: 'prefer', // covers Unreal GI and reflection intent
666
+ worldScale: 100, // Three meters -> Unreal centimeters
667
+ });
668
+
669
+ const json = serializeUnrealLightingManifest(manifest, { pretty: true });
670
+ ```
671
+
672
+ The manifest is an adapter contract with:
673
+
674
+ ```js
675
+ {
676
+ type: 'toonlab/unreal-lighting-manifest',
677
+ schemaVersion: 1,
678
+ engine: { name: 'Unreal Engine', targetVersion: '5.8' },
679
+ coordinateSystem: {
680
+ sourceUnits: 'meters',
681
+ worldScale: 100,
682
+ mapping: 'UnrealXYZcm = [-ThreeZ, ThreeX, ThreeY] * worldScale',
683
+ },
684
+ rendererIntent: {
685
+ megaLights: {
686
+ implementation: 'unreal-native',
687
+ intent: 'prefer',
688
+ scope: 'eligible local direct lights',
689
+ },
690
+ lumen: {
691
+ intent: 'prefer',
692
+ scope: 'global illumination and reflections',
693
+ },
694
+ },
695
+ source: {
696
+ recipeId: 'shrine-evening',
697
+ recipeSchemaVersion: 1,
698
+ shadowPolicy: { /* copied portable policy */ },
699
+ },
700
+ lights: [ /* class, transform, photometry, shape, shadow, resources */ ],
701
+ warnings: [],
702
+ }
703
+ ```
704
+
705
+ Expected semantic mappings are:
706
+
707
+ | ToonLab intent | Unreal adapter target |
708
+ |---|---|
709
+ | `directional` | Directional Light actor/component |
710
+ | `point` | Point Light actor/component |
711
+ | `spot` | Spot Light actor/component |
712
+ | `rectArea` | Rect Light actor/component |
713
+ | `discArea`, `tubeArea` | Best available source-shape mapping or an explicit approximation warning |
714
+ | `hemisphere` | Sky/ambient environment intent, not a forced local-light actor |
715
+ | `cookie` | Project light-function/material reference |
716
+ | `ies` | Project IES texture/profile reference |
717
+ | `linking` | Lighting Channels, tags, or project-defined receiver mapping |
718
+ | `shadow` | Cast-shadow, map-size, bias, and priority intent |
719
+
720
+ The Unreal-side importer owns exact class/property names, coordinate
721
+ conversion, asset lookup, project settings, platform checks, and actor
722
+ creation. It must return unresolved resource keys and unsupported mappings to
723
+ the user instead of guessing.
724
+
725
+ MegaLights applies to eligible local direct lighting and shadows. Lumen applies
726
+ to indirect lighting and reflections. Requesting both in a manifest is valid
727
+ because they fill different roles. An intent of `prefer` lets an importer
728
+ select a safe project fallback; `require` asks it to fail when the project,
729
+ renderer, platform, material path, or light type cannot honor the request.
730
+
731
+ ## Migrating environment presets
732
+
733
+ Existing `toonlab/environment-preset` documents remain supported. Migration
734
+ is additive: keep their shader features/parameters and move only scene-light
735
+ and orchestration intent into a lighting look.
736
+
737
+ | Environment preset field | Lighting destination |
738
+ |---|---|
739
+ | `features`, `parameters` | `look.environment.environmentShader`; continue applying through `applyEnvironmentShader` |
740
+ | `rig.sun` | A directional descriptor, or keep the existing `createEnvironmentSunRig` during gradual migration |
741
+ | `rig.lampIntensity` | Multiplier on migrated practical/luminaire descriptors |
742
+ | `rig.spotShadows` | Local-light `castShadow` plus `shadow.enabled` |
743
+ | `rig.timeOfDayHour` | `look.environment.timeOfDayHour` |
744
+ | `rig.probe` | Ambient-probe integration request |
745
+ | `rig.planarReflection` | Reflection integration request, not a light descriptor |
746
+ | `rig.dustMotes` | Environment/VFX integration request, not a light descriptor |
747
+ | `rig.bakeVertexAo` | Environment baking request, not runtime direct lighting |
748
+
749
+ Example migration without destroying the original preset:
750
+
751
+ ```js
752
+ import {
753
+ resolveEnvironmentPreset,
754
+ } from '@call-me-sensei/toonlab/environment';
755
+ import {
756
+ createLightDescriptor,
757
+ createLightingLook,
758
+ createLightingRecipe,
759
+ } from '@call-me-sensei/toonlab/lighting';
760
+
761
+ const legacy = resolveEnvironmentPreset('interiorEvening');
762
+
763
+ const migrated = createLightingLook({
764
+ id: 'interior-evening-v1',
765
+ name: 'Interior Evening',
766
+ recipe: createLightingRecipe({
767
+ id: 'interior-evening-lights',
768
+ lights: legacy.rig.sun ? [
769
+ createLightDescriptor('directional', {
770
+ id: 'sun',
771
+ position: [24, 32, 42],
772
+ target: [0, 0, 0],
773
+ intensity: { value: 1, unit: 'lux' },
774
+ castShadow: true,
775
+ shadow: { enabled: true, mapSize: 2048, priority: 100 },
776
+ }),
777
+ ] : [],
778
+ metadata: { migratedFrom: 'environment:interiorEvening' },
779
+ }),
780
+ environment: {
781
+ environmentShader: {
782
+ features: legacy.features,
783
+ parameters: legacy.parameters,
784
+ },
785
+ timeOfDayHour: legacy.rig.timeOfDayHour,
786
+ ambientProbe: Boolean(legacy.rig.probe),
787
+ planarReflection: Boolean(legacy.rig.planarReflection),
788
+ dustMotes: Boolean(legacy.rig.dustMotes),
789
+ bakeVertexAo: legacy.rig.bakeVertexAo,
790
+ },
791
+ });
792
+ ```
793
+
794
+ An old environment preset does not contain lamp positions, fixture identities,
795
+ physical units, or complete shadow budgets. A migration tool must therefore
796
+ report those as authoring tasks; it must not invent a room layout. Existing
797
+ `createEnvironmentSunRig`/`createEnvironmentLampRig` integrations can remain
798
+ in place while a project gradually replaces their scalar rig hints with
799
+ descriptors.
800
+
801
+ ## Integration assumptions
802
+
803
+ - One world unit is one meter; Three.js uses a right-handed, Y-up world.
804
+ - Directional, spot, and area orientation is authored with world-space
805
+ `position` and `target`; adapters derive and convert their own direction.
806
+ - The host owns `renderer`, `scene`, `camera`, render order, and tone mapping.
807
+ Call `lighting.update(...)` before the scene or post pipeline renders.
808
+ - Physical units are meaningful only when the host keeps a coherent exposure,
809
+ color-management, distance, and renderer-lighting setup. Artistic scalar
810
+ intensity remains available for stylized workflows.
811
+ - The portable manager creates ordinary Three.js lights and applies documented
812
+ approximations. It does not patch engine internals or promise that every
813
+ material consumes every Three.js light.
814
+ - ToonLab environment, character, water, vegetation, sky, weather, and post
815
+ modules remain independent consumers. A look coordinator binds their shared
816
+ sun direction, color, exposure, fog, reflections, and weather state.
817
+ - Asset references are resolved by the host. Core validation and Unreal export
818
+ perform no network access.
819
+ - Unreal export is intentionally semantic and one-way. Reimport/merge policy,
820
+ actor ownership, transactions, source control, and project mutation belong
821
+ to an Unreal Editor plugin or project tool.
822
+
823
+ These assumptions are part of the portability contract. When a target cannot
824
+ honor one, it should produce a capability or export diagnostic rather than a
825
+ visually different silent default.