@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.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +231 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +752 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
package/docs/lighting.md
ADDED
|
@@ -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.
|