@call-me-sensei/toonlab 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -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 +739 -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
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
// Developer-facing lighting runtime: one style + a fixture vocabulary +
|
|
2
|
+
// tiny scene overlays, instead of per-scene light configuration.
|
|
3
|
+
//
|
|
4
|
+
// const lighting = createLightingSystem({ scene, renderer, style: 'storybook' });
|
|
5
|
+
// lighting.attachWorld(world); // binds sun rig, fog (once per world)
|
|
6
|
+
// lighting.setTimeOfDay(18.5); // whole look follows the style's day cycle
|
|
7
|
+
// lighting.place('street-lamp', [4, 0, 2]); // semantic fixture, seeded variation
|
|
8
|
+
// lighting.applyOverlay(tavernOverlay); // small per-scene adjustments
|
|
9
|
+
// lighting.update(dt, camera); // flicker, blends, light budgets
|
|
10
|
+
//
|
|
11
|
+
// Ownership contract (one writer per property): while attached, the system
|
|
12
|
+
// owns sun direction/color/intensity + accents, coordinated Sky/Water/
|
|
13
|
+
// vegetation scene inputs, environment tints, fog color, tone-mapping
|
|
14
|
+
// exposure, its own ambient light, and every placed fixture. A composed world
|
|
15
|
+
// keeps owning the physical shadow-follow translation while Lighting supplies
|
|
16
|
+
// the true direction through world.setSun(). Weather modulates through
|
|
17
|
+
// setWeatherModulation() rather than writing the same targets.
|
|
18
|
+
|
|
19
|
+
import * as THREE from 'three';
|
|
20
|
+
|
|
21
|
+
import { deriveSeed } from '../core/generation.js';
|
|
22
|
+
import { createLightingManager } from './lightingRuntime.js';
|
|
23
|
+
import { resolveFixturePlacement, resolveLightFixture } from './lightingFixtures.js';
|
|
24
|
+
import { resolveLightingStylePreset, sampleLightingStyle } from './lightingStyle.js';
|
|
25
|
+
import { clamp, cloneJson, finite, isPlainObject, slug } from './utils.js';
|
|
26
|
+
import { SKY_SCENE_OVERRIDE_PRIORITIES } from '../sky/sceneOverrideLayers.js';
|
|
27
|
+
import { WATER_SCENE_OVERRIDE_PRIORITIES } from '../water/sceneOverrideLayers.js';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_MODULATION = Object.freeze({
|
|
30
|
+
ambientScale: 1,
|
|
31
|
+
exposureScale: 1,
|
|
32
|
+
fixtureScale: 1,
|
|
33
|
+
fogColorOverride: null,
|
|
34
|
+
fogColorTint: null,
|
|
35
|
+
sunColorTint: null,
|
|
36
|
+
sunIntensityScale: 1,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function normalizeModulation(source = {}) {
|
|
40
|
+
const input = isPlainObject(source) ? source : {};
|
|
41
|
+
return {
|
|
42
|
+
ambientScale: Math.max(finite(input.ambientScale, 1), 0),
|
|
43
|
+
exposureScale: Math.max(finite(input.exposureScale, 1), 0),
|
|
44
|
+
fixtureScale: Math.max(finite(input.fixtureScale, 1), 0),
|
|
45
|
+
fogColorOverride: Array.isArray(input.fogColorOverride)
|
|
46
|
+
? input.fogColorOverride.slice(0, 3).map((v) => clamp(finite(v, 0), 0, 1))
|
|
47
|
+
: null,
|
|
48
|
+
fogColorTint: Array.isArray(input.fogColorTint) ? input.fogColorTint.slice(0, 3).map((v) => Math.max(finite(v, 1), 0)) : null,
|
|
49
|
+
sunColorTint: Array.isArray(input.sunColorTint) ? input.sunColorTint.slice(0, 3).map((v) => Math.max(finite(v, 1), 0)) : null,
|
|
50
|
+
sunIntensityScale: Math.max(finite(input.sunIntensityScale, 1), 0),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeOverlay(source = {}) {
|
|
55
|
+
const input = isPlainObject(source) ? source : {};
|
|
56
|
+
const adjustments = isPlainObject(input.adjustments) ? input.adjustments : input;
|
|
57
|
+
return {
|
|
58
|
+
adjustments: {
|
|
59
|
+
ambientScale: Math.max(finite(adjustments.ambientScale, 1), 0),
|
|
60
|
+
exposureScale: Math.max(finite(adjustments.exposureScale, 1), 0),
|
|
61
|
+
fixtureScale: Math.max(finite(adjustments.fixtureScale, 1), 0),
|
|
62
|
+
fogColor: Array.isArray(adjustments.fogColor) ? adjustments.fogColor.slice(0, 3).map((v) => clamp(finite(v, 0), 0, 1)) : null,
|
|
63
|
+
sunIntensityScale: Math.max(finite(adjustments.sunIntensityScale, 1), 0),
|
|
64
|
+
},
|
|
65
|
+
fixtures: Array.isArray(input.fixtures)
|
|
66
|
+
? input.fixtures.filter(isPlainObject).map((entry) => ({
|
|
67
|
+
fixture: entry.fixture ?? entry.id,
|
|
68
|
+
overrides: isPlainObject(entry.overrides) ? entry.overrides : null,
|
|
69
|
+
position: Array.isArray(entry.position) ? entry.position.slice(0, 3) : null,
|
|
70
|
+
seed: entry.seed,
|
|
71
|
+
target: Array.isArray(entry.target) ? entry.target.slice(0, 3) : null,
|
|
72
|
+
}))
|
|
73
|
+
: [],
|
|
74
|
+
id: slug(input.id ?? input.name, 'overlay'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scheduleScale(schedule, frame) {
|
|
79
|
+
if (schedule.mode === 'always') return 1;
|
|
80
|
+
if (schedule.mode === 'day') {
|
|
81
|
+
// fixtureScale is high at night; day fixtures invert it.
|
|
82
|
+
return Math.max(clamp(1 - frame.fixtureScale, 0, 1), schedule.minimum);
|
|
83
|
+
}
|
|
84
|
+
return Math.max(frame.fixtureScale, schedule.minimum);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ENVIRONMENT_LIGHTING_UNIFORM_KEYS = Object.freeze([
|
|
88
|
+
'heightFogColor',
|
|
89
|
+
'skyGroundTint',
|
|
90
|
+
'skyTopTint',
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
function captureEnvironmentLighting(root) {
|
|
94
|
+
if (!root?.traverse) return [];
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const snapshots = [];
|
|
97
|
+
root.traverse((object) => {
|
|
98
|
+
if (!object.isMesh || !object.material) return;
|
|
99
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
100
|
+
for (const material of materials) {
|
|
101
|
+
if (seen.has(material) || !material?.userData?.environmentMaterial || !material.uniforms) continue;
|
|
102
|
+
seen.add(material);
|
|
103
|
+
const values = {};
|
|
104
|
+
for (const key of ENVIRONMENT_LIGHTING_UNIFORM_KEYS) {
|
|
105
|
+
const value = material.uniforms[key]?.value;
|
|
106
|
+
if (value?.clone) values[key] = value.clone();
|
|
107
|
+
}
|
|
108
|
+
snapshots.push({ material, values });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return snapshots;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function restoreEnvironmentLighting(snapshots = []) {
|
|
115
|
+
for (const { material, values } of snapshots) {
|
|
116
|
+
for (const [key, value] of Object.entries(values)) {
|
|
117
|
+
material?.uniforms?.[key]?.value?.copy?.(value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function captureSunRigState(sunRig) {
|
|
123
|
+
const light = sunRig?.light;
|
|
124
|
+
if (!light) return null;
|
|
125
|
+
const opacity = (mesh) => mesh?.material?.uniforms?.opacity?.value ?? null;
|
|
126
|
+
return {
|
|
127
|
+
beamOpacity: opacity(sunRig.beam),
|
|
128
|
+
color: light.color.toArray(),
|
|
129
|
+
diskOpacity: opacity(sunRig.disk),
|
|
130
|
+
intensity: light.intensity,
|
|
131
|
+
shaftOpacity: opacity(sunRig.shaft),
|
|
132
|
+
spillOpacity: opacity(sunRig.spill),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function restoreSunRigState(sunRig, state) {
|
|
137
|
+
if (!sunRig?.light || !state) return;
|
|
138
|
+
const color = Array.isArray(state.color)
|
|
139
|
+
? new THREE.Color().fromArray(state.color)
|
|
140
|
+
: state.color;
|
|
141
|
+
if (sunRig.setState) {
|
|
142
|
+
sunRig.setState({
|
|
143
|
+
beamOpacity: state.beamOpacity ?? undefined,
|
|
144
|
+
color,
|
|
145
|
+
diskOpacity: state.diskOpacity ?? undefined,
|
|
146
|
+
intensity: state.intensity,
|
|
147
|
+
shaftOpacity: state.shaftOpacity ?? undefined,
|
|
148
|
+
spillOpacity: state.spillOpacity ?? undefined,
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
if (color) sunRig.light.color.copy(color);
|
|
152
|
+
if (state.intensity !== null && state.intensity !== undefined) sunRig.light.intensity = state.intensity;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createLightingSystem({
|
|
157
|
+
camera = null,
|
|
158
|
+
capabilities = null,
|
|
159
|
+
quality = null,
|
|
160
|
+
renderer = null,
|
|
161
|
+
scene = null,
|
|
162
|
+
seed = 1,
|
|
163
|
+
style = 'storybook',
|
|
164
|
+
textureResolver = null,
|
|
165
|
+
timeOfDay = 12,
|
|
166
|
+
} = {}) {
|
|
167
|
+
let styleSettings = resolveLightingStylePreset(style);
|
|
168
|
+
let hour = clamp(finite(timeOfDay, 12), 0, 24);
|
|
169
|
+
let frame = sampleLightingStyle(styleSettings, hour);
|
|
170
|
+
let modulation = { ...DEFAULT_MODULATION };
|
|
171
|
+
let clock = 0;
|
|
172
|
+
let placementCounter = 0;
|
|
173
|
+
let disposed = false;
|
|
174
|
+
|
|
175
|
+
const manager = createLightingManager({
|
|
176
|
+
camera,
|
|
177
|
+
capabilities,
|
|
178
|
+
quality: quality ?? styleSettings.quality,
|
|
179
|
+
recipe: { id: 'lighting-system', lights: [], name: 'Lighting System', shadowPolicy: styleSettings.shadowPolicy },
|
|
180
|
+
renderer,
|
|
181
|
+
scene,
|
|
182
|
+
textureResolver,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// The system owns exactly one ambient light so scene ambience has a single
|
|
186
|
+
// writer; it lives in the manager group so scene attachment follows along.
|
|
187
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0);
|
|
188
|
+
ambientLight.name = 'Lighting System Ambient';
|
|
189
|
+
manager.group.add(ambientLight);
|
|
190
|
+
|
|
191
|
+
const placements = new Map();
|
|
192
|
+
const overlays = new Map();
|
|
193
|
+
const skyOverrideLayer = Symbol('ToonLab LightingSystem sky layer');
|
|
194
|
+
const waterOverrideLayer = Symbol('ToonLab LightingSystem water layer');
|
|
195
|
+
|
|
196
|
+
// Attach targets + the state to restore on detach/dispose.
|
|
197
|
+
const attachment = {
|
|
198
|
+
driveSunPosition: false,
|
|
199
|
+
environmentRoot: null,
|
|
200
|
+
fog: null,
|
|
201
|
+
getSun: null,
|
|
202
|
+
getSunDirection: null,
|
|
203
|
+
lampRig: null,
|
|
204
|
+
ownSun: null,
|
|
205
|
+
restore: null,
|
|
206
|
+
sky: null,
|
|
207
|
+
sunDistance: 60,
|
|
208
|
+
setSunDirection: null,
|
|
209
|
+
setSun: null,
|
|
210
|
+
sunRig: null,
|
|
211
|
+
weather: null,
|
|
212
|
+
weatherLighting: null,
|
|
213
|
+
water: null,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const SKY_DRIVEN_KEYS = ['horizonColor', 'starsStrength', 'sunColor', 'sunDirection', 'zenithColor'];
|
|
217
|
+
const WATER_DRIVEN_KEYS = ['skyHorizonColor', 'skyZenithColor', 'sunColor', 'sunDirection'];
|
|
218
|
+
|
|
219
|
+
function captureRestoreState() {
|
|
220
|
+
const sunLight = attachment.sunRig?.light ?? null;
|
|
221
|
+
const skySettings = attachment.sky?.setSceneOverrideLayer
|
|
222
|
+
? null
|
|
223
|
+
: attachment.sky?.settings ?? null;
|
|
224
|
+
const waterSettings = attachment.water?.setSceneOverrideLayer
|
|
225
|
+
? null
|
|
226
|
+
: attachment.water?.settings ?? null;
|
|
227
|
+
attachment.restore = {
|
|
228
|
+
environmentLighting: captureEnvironmentLighting(attachment.environmentRoot),
|
|
229
|
+
exposure: renderer ? renderer.toneMappingExposure : null,
|
|
230
|
+
fogColor: attachment.fog?.color?.clone?.() ?? null,
|
|
231
|
+
sky: skySettings
|
|
232
|
+
? Object.fromEntries(SKY_DRIVEN_KEYS.map((key) => [key, cloneJson(skySettings[key])]))
|
|
233
|
+
: null,
|
|
234
|
+
sunColor: sunLight?.color?.clone?.() ?? null,
|
|
235
|
+
sunState: attachment.getSun ? cloneJson(attachment.getSun()) : null,
|
|
236
|
+
sunDirection: attachment.getSunDirection
|
|
237
|
+
? cloneJson(attachment.getSunDirection())
|
|
238
|
+
: null,
|
|
239
|
+
sunIntensity: sunLight?.intensity ?? null,
|
|
240
|
+
sunPosition: sunLight?.position?.clone?.() ?? null,
|
|
241
|
+
sunRigState: captureSunRigState(attachment.sunRig),
|
|
242
|
+
water: waterSettings
|
|
243
|
+
? Object.fromEntries(WATER_DRIVEN_KEYS.map((key) => [key, cloneJson(waterSettings[key])]))
|
|
244
|
+
: null,
|
|
245
|
+
weatherSunBaseline: attachment.weather?.sunBaseline ?? null,
|
|
246
|
+
weatherSunRigBaseline: attachment.weather?.sunRigBaseline ?? null,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function restoreAttachedState() {
|
|
251
|
+
const restore = attachment.restore;
|
|
252
|
+
const weatherWasDisposed = Boolean(attachment.weather?.disposed);
|
|
253
|
+
const sunStateToRestore = weatherWasDisposed && restore?.weatherSunBaseline
|
|
254
|
+
? restore.weatherSunBaseline
|
|
255
|
+
: restore?.sunState;
|
|
256
|
+
const sunRigStateToRestore = weatherWasDisposed && restore?.weatherSunRigBaseline
|
|
257
|
+
? restore.weatherSunRigBaseline
|
|
258
|
+
: restore?.sunRigState;
|
|
259
|
+
if (restore) {
|
|
260
|
+
if (renderer && restore.exposure !== null) renderer.toneMappingExposure = restore.exposure;
|
|
261
|
+
if (attachment.fog && restore.fogColor) attachment.fog.color.copy(restore.fogColor);
|
|
262
|
+
const sunLight = attachment.sunRig?.light ?? null;
|
|
263
|
+
if (sunLight) {
|
|
264
|
+
restoreSunRigState(attachment.sunRig, sunRigStateToRestore);
|
|
265
|
+
if (restore.sunPosition && attachment.driveSunPosition) sunLight.position.copy(restore.sunPosition);
|
|
266
|
+
}
|
|
267
|
+
if (sunStateToRestore && attachment.setSun) attachment.setSun(sunStateToRestore);
|
|
268
|
+
}
|
|
269
|
+
if (attachment.weather?.setLightingSystem
|
|
270
|
+
&& attachment.weather.lightingSystem === api) {
|
|
271
|
+
attachment.weather.setLightingSystem(attachment.weatherLighting);
|
|
272
|
+
}
|
|
273
|
+
// Resetting Weather modulation refreshes the Lighting frame once, so the
|
|
274
|
+
// world-owned direction must be restored after that refresh.
|
|
275
|
+
const directionToRestore = sunStateToRestore?.direction ?? restore?.sunDirection;
|
|
276
|
+
if (directionToRestore && attachment.setSunDirection) {
|
|
277
|
+
attachment.setSunDirection(directionToRestore);
|
|
278
|
+
}
|
|
279
|
+
// Unbinding Weather resets Lighting modulation, which refreshes its Sky
|
|
280
|
+
// layer once. Remove our layer afterwards so only Weather remains.
|
|
281
|
+
if (attachment.sky?.clearSceneOverrideLayer) {
|
|
282
|
+
attachment.sky.clearSceneOverrideLayer(skyOverrideLayer);
|
|
283
|
+
} else if (attachment.sky && restore?.sky) {
|
|
284
|
+
attachment.sky.applySettings(restore.sky);
|
|
285
|
+
}
|
|
286
|
+
if (attachment.water?.clearSceneOverrideLayer) {
|
|
287
|
+
attachment.water.clearSceneOverrideLayer(waterOverrideLayer);
|
|
288
|
+
} else if (attachment.water?.clearSceneOverrides) {
|
|
289
|
+
attachment.water.clearSceneOverrides();
|
|
290
|
+
} else if (attachment.water && restore?.water) {
|
|
291
|
+
attachment.water.applySettings?.(restore.water);
|
|
292
|
+
}
|
|
293
|
+
restoreEnvironmentLighting(restore?.environmentLighting);
|
|
294
|
+
attachment.restore = null;
|
|
295
|
+
attachment.weather = null;
|
|
296
|
+
attachment.weatherLighting = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function overlayFactor(key) {
|
|
300
|
+
let factor = 1;
|
|
301
|
+
for (const overlay of overlays.values()) {
|
|
302
|
+
const value = overlay.adjustments[key];
|
|
303
|
+
factor *= 1 + (value - 1) * overlay.weight;
|
|
304
|
+
}
|
|
305
|
+
return factor;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function overlayFogColor() {
|
|
309
|
+
let color = null;
|
|
310
|
+
let weight = 0;
|
|
311
|
+
for (const overlay of overlays.values()) {
|
|
312
|
+
if (overlay.adjustments.fogColor && overlay.weight > weight) {
|
|
313
|
+
color = overlay.adjustments.fogColor;
|
|
314
|
+
weight = overlay.weight;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return color ? { color, weight } : null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const workColor = new THREE.Color();
|
|
321
|
+
|
|
322
|
+
function applyFrame() {
|
|
323
|
+
const sunScale = modulation.sunIntensityScale * overlayFactor('sunIntensityScale');
|
|
324
|
+
const sunColor = [...frame.sunColor];
|
|
325
|
+
if (modulation.sunColorTint) {
|
|
326
|
+
for (let i = 0; i < 3; i += 1) sunColor[i] = clamp(sunColor[i] * modulation.sunColorTint[i], 0, 4);
|
|
327
|
+
}
|
|
328
|
+
const sunDirection = [frame.sunSourceRatios.x, frame.sunSourceRatios.y, frame.sunSourceRatios.z];
|
|
329
|
+
|
|
330
|
+
if (attachment.sunRig?.setState) {
|
|
331
|
+
attachment.sunRig.setState({
|
|
332
|
+
beamOpacity: 0.28 * frame.accentScale * sunScale,
|
|
333
|
+
color: workColor.setRGB(sunColor[0], sunColor[1], sunColor[2]),
|
|
334
|
+
diskOpacity: 0.62 * frame.accentScale * sunScale,
|
|
335
|
+
intensity: frame.sunIntensity * sunScale,
|
|
336
|
+
shaftOpacity: 0.1 * frame.accentScale * sunScale,
|
|
337
|
+
spillOpacity: 0.3 * frame.accentScale * sunScale,
|
|
338
|
+
...(attachment.driveSunPosition ? { sourceRatios: frame.sunSourceRatios } : {}),
|
|
339
|
+
});
|
|
340
|
+
} else if (attachment.ownSun) {
|
|
341
|
+
attachment.ownSun.color.setRGB(sunColor[0], sunColor[1], sunColor[2]);
|
|
342
|
+
attachment.ownSun.intensity = frame.sunIntensity * sunScale;
|
|
343
|
+
attachment.ownSun.position.set(
|
|
344
|
+
frame.sunSourceRatios.x,
|
|
345
|
+
frame.sunSourceRatios.y,
|
|
346
|
+
frame.sunSourceRatios.z,
|
|
347
|
+
).multiplyScalar(attachment.sunDistance);
|
|
348
|
+
attachment.ownSun.target.position.set(0, 0, 0);
|
|
349
|
+
attachment.ownSun.target.updateMatrixWorld();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const style = styleSettings;
|
|
353
|
+
ambientLight.visible = style.ambientLight.enabled;
|
|
354
|
+
ambientLight.color.setRGB(...style.ambientLight.color);
|
|
355
|
+
ambientLight.intensity = style.ambientLight.intensity
|
|
356
|
+
* frame.ambientScale
|
|
357
|
+
* modulation.ambientScale
|
|
358
|
+
* overlayFactor('ambientScale');
|
|
359
|
+
|
|
360
|
+
const effectiveFogColor = [...frame.fogColor];
|
|
361
|
+
if (modulation.fogColorTint) {
|
|
362
|
+
for (let i = 0; i < 3; i += 1) effectiveFogColor[i] = clamp(effectiveFogColor[i] * modulation.fogColorTint[i], 0, 1);
|
|
363
|
+
}
|
|
364
|
+
if (modulation.fogColorOverride) {
|
|
365
|
+
for (let i = 0; i < 3; i += 1) effectiveFogColor[i] = modulation.fogColorOverride[i];
|
|
366
|
+
}
|
|
367
|
+
const overlayFog = overlayFogColor();
|
|
368
|
+
if (overlayFog) {
|
|
369
|
+
for (let i = 0; i < 3; i += 1) {
|
|
370
|
+
effectiveFogColor[i] += (overlayFog.color[i] - effectiveFogColor[i]) * overlayFog.weight;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (attachment.fog?.color) {
|
|
374
|
+
attachment.fog.color.setRGB(...effectiveFogColor);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (renderer && style.exposure.enabled) {
|
|
378
|
+
renderer.toneMappingExposure = frame.exposure
|
|
379
|
+
* modulation.exposureScale
|
|
380
|
+
* overlayFactor('exposureScale');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
attachment.lampRig?.setIntensity?.(frame.fixtureScale);
|
|
384
|
+
|
|
385
|
+
// The house sky dome follows the style's day cycle: zenith/horizon
|
|
386
|
+
// colors, star strength, and the sun disk color/direction all move with
|
|
387
|
+
// the hour, so nights are actually dark and starry.
|
|
388
|
+
if (attachment.sky) {
|
|
389
|
+
const skyFrame = {
|
|
390
|
+
horizonColor: frame.skyHorizonColor,
|
|
391
|
+
starsStrength: frame.starsStrength,
|
|
392
|
+
sunColor: sunColor,
|
|
393
|
+
sunDirection,
|
|
394
|
+
zenithColor: frame.skyZenithColor,
|
|
395
|
+
};
|
|
396
|
+
if (attachment.sky.setSceneOverrideLayer) {
|
|
397
|
+
attachment.sky.setSceneOverrideLayer(skyOverrideLayer, skyFrame, {
|
|
398
|
+
priority: SKY_SCENE_OVERRIDE_PRIORITIES.lighting,
|
|
399
|
+
});
|
|
400
|
+
} else if (attachment.sky.setSceneOverrides) {
|
|
401
|
+
attachment.sky.setSceneOverrides(skyFrame);
|
|
402
|
+
} else if (attachment.sky.applySettings) {
|
|
403
|
+
attachment.sky.applySettings(skyFrame);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const renderedSky = attachment.sky?.renderedSettings;
|
|
408
|
+
const skyFillColor = renderedSky?.horizonColor ?? frame.skyHorizonColor;
|
|
409
|
+
if (attachment.setSun) {
|
|
410
|
+
attachment.setSun({ color: sunColor, direction: sunDirection, sky: skyFillColor });
|
|
411
|
+
} else {
|
|
412
|
+
attachment.setSunDirection?.(sunDirection);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (attachment.water) {
|
|
416
|
+
const waterFrame = {
|
|
417
|
+
skyHorizonColor: renderedSky?.horizonColor ?? frame.skyHorizonColor,
|
|
418
|
+
skyZenithColor: renderedSky?.zenithColor ?? frame.skyZenithColor,
|
|
419
|
+
sunColor,
|
|
420
|
+
sunDirection,
|
|
421
|
+
};
|
|
422
|
+
if (attachment.water.setSceneOverrideLayer) {
|
|
423
|
+
attachment.water.setSceneOverrideLayer(waterOverrideLayer, waterFrame, {
|
|
424
|
+
priority: WATER_SCENE_OVERRIDE_PRIORITIES.lighting,
|
|
425
|
+
});
|
|
426
|
+
} else if (attachment.water.setSceneOverrides) {
|
|
427
|
+
attachment.water.setSceneOverrides(waterFrame);
|
|
428
|
+
} else if (attachment.water.applySettings) {
|
|
429
|
+
attachment.water.applySettings(waterFrame);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (attachment.environmentRoot?.traverse) {
|
|
434
|
+
attachment.environmentRoot.traverse((object) => {
|
|
435
|
+
if (!object.isMesh || !object.material) return;
|
|
436
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
437
|
+
for (const material of materials) {
|
|
438
|
+
if (!material?.userData?.environmentMaterial || !material.uniforms) continue;
|
|
439
|
+
material.uniforms.skyGroundTint?.value?.setRGB?.(...frame.skyGroundTint);
|
|
440
|
+
material.uniforms.skyTopTint?.value?.setRGB?.(...frame.skyTopTint);
|
|
441
|
+
material.uniforms.heightFogColor?.value?.setRGB?.(...effectiveFogColor);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function applyPlacementIntensities() {
|
|
448
|
+
if (placements.size === 0) return;
|
|
449
|
+
const styleScale = styleSettings.fixtures.intensityScale
|
|
450
|
+
* modulation.fixtureScale
|
|
451
|
+
* overlayFactor('fixtureScale');
|
|
452
|
+
for (const placement of placements.values()) {
|
|
453
|
+
const light = manager.getLight(placement.lightId);
|
|
454
|
+
if (!light) continue;
|
|
455
|
+
let intensity = placement.baseIntensity * styleScale * scheduleScale(placement.schedule, frame);
|
|
456
|
+
if (placement.flicker.amount > 0) {
|
|
457
|
+
const t = clock * placement.flicker.speed * Math.PI * 2 + placement.flickerPhase;
|
|
458
|
+
// Two detuned sines make organic, deterministic flicker without a
|
|
459
|
+
// noise table; amplitude stays within ±amount.
|
|
460
|
+
const wave = Math.sin(t) * 0.6 + Math.sin(t * 1.7 + placement.flickerPhase) * 0.4;
|
|
461
|
+
intensity *= 1 + placement.flicker.amount * wave;
|
|
462
|
+
}
|
|
463
|
+
light.intensity = Math.max(intensity, 0);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function refreshFrame() {
|
|
468
|
+
frame = sampleLightingStyle(styleSettings, hour);
|
|
469
|
+
applyFrame();
|
|
470
|
+
applyPlacementIntensities();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const api = {
|
|
474
|
+
/** Binds scene handles. Call with a stylized world or explicit targets. */
|
|
475
|
+
attach({
|
|
476
|
+
driveSunPosition = false,
|
|
477
|
+
environmentRoot = null,
|
|
478
|
+
fog = null,
|
|
479
|
+
getSun = null,
|
|
480
|
+
getSunDirection = null,
|
|
481
|
+
lampRig = null,
|
|
482
|
+
sky = null,
|
|
483
|
+
sunDistance = 60,
|
|
484
|
+
setSunDirection = null,
|
|
485
|
+
setSun = null,
|
|
486
|
+
sunRig = null,
|
|
487
|
+
weather = null,
|
|
488
|
+
water = null,
|
|
489
|
+
} = {}) {
|
|
490
|
+
if (disposed) throw new Error('Cannot attach a disposed lighting system.');
|
|
491
|
+
restoreAttachedState();
|
|
492
|
+
attachment.driveSunPosition = Boolean(driveSunPosition);
|
|
493
|
+
attachment.environmentRoot = environmentRoot;
|
|
494
|
+
attachment.fog = fog ?? null;
|
|
495
|
+
attachment.getSun = typeof getSun === 'function' ? getSun : null;
|
|
496
|
+
attachment.getSunDirection = typeof getSunDirection === 'function' ? getSunDirection : null;
|
|
497
|
+
attachment.lampRig = lampRig ?? null;
|
|
498
|
+
attachment.sky = sky?.setSceneOverrideLayer || sky?.setSceneOverrides || sky?.applySettings
|
|
499
|
+
? sky
|
|
500
|
+
: null;
|
|
501
|
+
attachment.sunDistance = Math.max(finite(sunDistance, 60), 1);
|
|
502
|
+
attachment.setSunDirection = typeof setSunDirection === 'function' ? setSunDirection : null;
|
|
503
|
+
attachment.setSun = typeof setSun === 'function' ? setSun : null;
|
|
504
|
+
attachment.sunRig = sunRig?.light ? sunRig : null;
|
|
505
|
+
attachment.weather = weather?.setLightingSystem ? weather : null;
|
|
506
|
+
attachment.weatherLighting = attachment.weather?.lightingSystem ?? null;
|
|
507
|
+
attachment.water = water?.setSceneOverrideLayer || water?.setSceneOverrides || water?.applySettings
|
|
508
|
+
? water
|
|
509
|
+
: null;
|
|
510
|
+
if (!attachment.sunRig && scene && !attachment.ownSun) {
|
|
511
|
+
const sun = new THREE.DirectionalLight(0xffffff, 1);
|
|
512
|
+
sun.name = 'Lighting System Sun';
|
|
513
|
+
sun.castShadow = true;
|
|
514
|
+
manager.group.add(sun);
|
|
515
|
+
manager.group.add(sun.target);
|
|
516
|
+
attachment.ownSun = sun;
|
|
517
|
+
}
|
|
518
|
+
captureRestoreState();
|
|
519
|
+
refreshFrame();
|
|
520
|
+
attachment.weather?.setLightingSystem(api);
|
|
521
|
+
return api;
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
/** Convenience: binds the handles a createStylizedWorld result exposes. */
|
|
525
|
+
attachWorld(world, options = {}) {
|
|
526
|
+
return api.attach({
|
|
527
|
+
environmentRoot: world?.environmentRoot ?? null,
|
|
528
|
+
fog: world?.fog ?? null,
|
|
529
|
+
getSun: world ? () => world.sunState : null,
|
|
530
|
+
getSunDirection: world ? () => world.sunDirection : null,
|
|
531
|
+
sky: world?.sky ?? null,
|
|
532
|
+
sunRig: world?.sunRig ?? null,
|
|
533
|
+
setSun: world?.setSun ?? null,
|
|
534
|
+
setSunDirection: world?.setSunDirection ?? null,
|
|
535
|
+
weather: world?.weather ?? null,
|
|
536
|
+
water: world?.water ?? null,
|
|
537
|
+
// Worlds translate the physical light every frame for shadow-follow;
|
|
538
|
+
// the style drives its true direction, color, and intensity. Sky uses
|
|
539
|
+
// a lower-priority Lighting layer so Weather composes above it without
|
|
540
|
+
// either system mutating the authored Sky preset.
|
|
541
|
+
driveSunPosition: false,
|
|
542
|
+
...options,
|
|
543
|
+
});
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
detach() {
|
|
547
|
+
restoreAttachedState();
|
|
548
|
+
attachment.driveSunPosition = false;
|
|
549
|
+
attachment.environmentRoot = null;
|
|
550
|
+
attachment.fog = null;
|
|
551
|
+
attachment.getSun = null;
|
|
552
|
+
attachment.getSunDirection = null;
|
|
553
|
+
attachment.lampRig = null;
|
|
554
|
+
attachment.sky = null;
|
|
555
|
+
attachment.sunRig = null;
|
|
556
|
+
attachment.setSunDirection = null;
|
|
557
|
+
attachment.setSun = null;
|
|
558
|
+
attachment.weather = null;
|
|
559
|
+
attachment.weatherLighting = null;
|
|
560
|
+
attachment.water = null;
|
|
561
|
+
return api;
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
/** Swaps the whole lighting identity. Placements survive. */
|
|
565
|
+
setStyle(nextStyle) {
|
|
566
|
+
if (disposed) throw new Error('Cannot update a disposed lighting system.');
|
|
567
|
+
styleSettings = resolveLightingStylePreset(nextStyle);
|
|
568
|
+
if (!quality) manager.setQuality(styleSettings.quality);
|
|
569
|
+
refreshFrame();
|
|
570
|
+
return api;
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
setQuality(nextQuality) {
|
|
574
|
+
quality = nextQuality;
|
|
575
|
+
manager.setQuality(nextQuality);
|
|
576
|
+
return api;
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
/** Sets the hour (0-24). The entire look follows the style's day cycle. */
|
|
580
|
+
setTimeOfDay(nextHour) {
|
|
581
|
+
hour = ((finite(nextHour, 12) % 24) + 24) % 24;
|
|
582
|
+
refreshFrame();
|
|
583
|
+
return frame;
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
/** Advances the day cycle; use from update loops for live cycles. */
|
|
587
|
+
advanceTime(hoursDelta) {
|
|
588
|
+
return api.setTimeOfDay(hour + finite(hoursDelta, 0));
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Weather integration point: one multiplicative layer instead of a second
|
|
593
|
+
* writer on the same lights/fog/exposure.
|
|
594
|
+
*/
|
|
595
|
+
setWeatherModulation(nextModulation) {
|
|
596
|
+
modulation = normalizeModulation(nextModulation);
|
|
597
|
+
refreshFrame();
|
|
598
|
+
return api;
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
/** Places a fixture. Returns a handle: { id, descriptor, light, remove, set }. */
|
|
602
|
+
place(fixture, position = null, { id = null, overrides = null, seed: placementSeed = null, target = null } = {}) {
|
|
603
|
+
if (disposed) throw new Error('Cannot update a disposed lighting system.');
|
|
604
|
+
const fixtureId = typeof fixture === 'string' ? fixture : slug(fixture?.id ?? 'fixture', 'fixture');
|
|
605
|
+
const fixtureSettings = resolveLightFixture(fixture);
|
|
606
|
+
placementCounter += 1;
|
|
607
|
+
const resolvedSeed = placementSeed ?? deriveSeed(seed, `${fixtureId}:${placementCounter}`);
|
|
608
|
+
const placement = resolveFixturePlacement(fixtureSettings, {
|
|
609
|
+
id: id ?? `${fixtureId}-${placementCounter}`,
|
|
610
|
+
overrides,
|
|
611
|
+
position,
|
|
612
|
+
seed: resolvedSeed,
|
|
613
|
+
target,
|
|
614
|
+
});
|
|
615
|
+
const lightId = manager.addLight(placement.descriptor);
|
|
616
|
+
const light = manager.getLight(lightId);
|
|
617
|
+
const record = {
|
|
618
|
+
baseIntensity: light?.intensity ?? 1,
|
|
619
|
+
fixtureId,
|
|
620
|
+
flicker: placement.flicker,
|
|
621
|
+
flickerPhase: placement.flickerPhase,
|
|
622
|
+
input: { fixture: fixtureId, id, overrides: cloneJson(overrides), position, seed: resolvedSeed, target },
|
|
623
|
+
lightId,
|
|
624
|
+
overlayId: null,
|
|
625
|
+
schedule: placement.schedule,
|
|
626
|
+
};
|
|
627
|
+
placements.set(lightId, record);
|
|
628
|
+
applyPlacementIntensities();
|
|
629
|
+
return {
|
|
630
|
+
descriptor: placement.descriptor,
|
|
631
|
+
id: lightId,
|
|
632
|
+
light,
|
|
633
|
+
remove: () => api.removePlacement(lightId),
|
|
634
|
+
set: (nextOverrides) => {
|
|
635
|
+
const next = manager.updateLight(lightId, nextOverrides);
|
|
636
|
+
if (next) record.baseIntensity = manager.getLight(lightId)?.intensity ?? record.baseIntensity;
|
|
637
|
+
applyPlacementIntensities();
|
|
638
|
+
return next;
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
removePlacement(idOrHandle) {
|
|
644
|
+
const lightId = typeof idOrHandle === 'string' ? idOrHandle : idOrHandle?.id;
|
|
645
|
+
if (!placements.has(lightId)) return false;
|
|
646
|
+
placements.delete(lightId);
|
|
647
|
+
return manager.removeLight(lightId);
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
/** Applies a scene overlay; adjustments blend in over blendSeconds. */
|
|
651
|
+
applyOverlay(overlay, { blendSeconds = 0.4 } = {}) {
|
|
652
|
+
if (disposed) throw new Error('Cannot update a disposed lighting system.');
|
|
653
|
+
const normalized = normalizeOverlay(overlay);
|
|
654
|
+
api.removeOverlay(normalized.id, { blendSeconds: 0 });
|
|
655
|
+
const placementIds = [];
|
|
656
|
+
for (const entry of normalized.fixtures) {
|
|
657
|
+
if (!entry.fixture) continue;
|
|
658
|
+
const handle = api.place(entry.fixture, entry.position, {
|
|
659
|
+
overrides: entry.overrides,
|
|
660
|
+
seed: entry.seed,
|
|
661
|
+
target: entry.target,
|
|
662
|
+
});
|
|
663
|
+
placements.get(handle.id).overlayId = normalized.id;
|
|
664
|
+
placementIds.push(handle.id);
|
|
665
|
+
}
|
|
666
|
+
overlays.set(normalized.id, {
|
|
667
|
+
adjustments: normalized.adjustments,
|
|
668
|
+
blendSeconds: Math.max(finite(blendSeconds, 0.4), 0),
|
|
669
|
+
id: normalized.id,
|
|
670
|
+
placementIds,
|
|
671
|
+
target: 1,
|
|
672
|
+
weight: blendSeconds > 0 ? 0 : 1,
|
|
673
|
+
});
|
|
674
|
+
refreshFrame();
|
|
675
|
+
return normalized.id;
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
removeOverlay(id, { blendSeconds = 0.4 } = {}) {
|
|
679
|
+
const overlay = overlays.get(id);
|
|
680
|
+
if (!overlay) return false;
|
|
681
|
+
overlay.target = 0;
|
|
682
|
+
overlay.blendSeconds = Math.max(finite(blendSeconds, 0.4), 0);
|
|
683
|
+
if (overlay.blendSeconds === 0) {
|
|
684
|
+
for (const placementId of overlay.placementIds) api.removePlacement(placementId);
|
|
685
|
+
overlays.delete(id);
|
|
686
|
+
refreshFrame();
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
},
|
|
690
|
+
|
|
691
|
+
/** Per-frame: overlay blends, fixture flicker, light budget selection. */
|
|
692
|
+
update(delta = 0.016, nextCamera = null) {
|
|
693
|
+
if (disposed) return;
|
|
694
|
+
const dt = clamp(finite(delta, 0.016), 0, 0.1);
|
|
695
|
+
clock += dt;
|
|
696
|
+
let overlaysChanged = false;
|
|
697
|
+
for (const overlay of [...overlays.values()]) {
|
|
698
|
+
if (overlay.weight !== overlay.target) {
|
|
699
|
+
const step = overlay.blendSeconds > 0 ? dt / overlay.blendSeconds : 1;
|
|
700
|
+
overlay.weight = overlay.target > overlay.weight
|
|
701
|
+
? Math.min(overlay.weight + step, overlay.target)
|
|
702
|
+
: Math.max(overlay.weight - step, overlay.target);
|
|
703
|
+
overlaysChanged = true;
|
|
704
|
+
if (overlay.weight === 0 && overlay.target === 0) {
|
|
705
|
+
for (const placementId of overlay.placementIds) api.removePlacement(placementId);
|
|
706
|
+
overlays.delete(overlay.id);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (overlaysChanged) applyFrame();
|
|
711
|
+
applyPlacementIntensities();
|
|
712
|
+
manager.update(nextCamera ? { camera: nextCamera } : {});
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
stats() {
|
|
716
|
+
const diagnostics = manager.getDiagnostics();
|
|
717
|
+
const byCategory = {};
|
|
718
|
+
for (const placement of placements.values()) {
|
|
719
|
+
byCategory[placement.fixtureId] = (byCategory[placement.fixtureId] ?? 0) + 1;
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
activeLightCount: diagnostics.activeLightCount,
|
|
723
|
+
backend: diagnostics.backend,
|
|
724
|
+
hour,
|
|
725
|
+
overlayCount: overlays.size,
|
|
726
|
+
placementCount: placements.size,
|
|
727
|
+
placementsByFixture: byCategory,
|
|
728
|
+
shadowedLightCount: diagnostics.shadowedLightCount,
|
|
729
|
+
totalLightCount: diagnostics.totalLightCount,
|
|
730
|
+
warnings: diagnostics.warnings,
|
|
731
|
+
};
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
toJSON() {
|
|
735
|
+
return {
|
|
736
|
+
placements: [...placements.values()]
|
|
737
|
+
.filter((placement) => placement.overlayId === null)
|
|
738
|
+
.map((placement) => cloneJson(placement.input)),
|
|
739
|
+
style: cloneJson(styleSettings),
|
|
740
|
+
timeOfDay: hour,
|
|
741
|
+
};
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
/** Removes placements/overlays/modulation and re-applies the style. */
|
|
745
|
+
reset() {
|
|
746
|
+
for (const id of [...placements.keys()]) api.removePlacement(id);
|
|
747
|
+
overlays.clear();
|
|
748
|
+
modulation = { ...DEFAULT_MODULATION };
|
|
749
|
+
placementCounter = 0;
|
|
750
|
+
refreshFrame();
|
|
751
|
+
return api;
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
dispose() {
|
|
755
|
+
if (disposed) return;
|
|
756
|
+
api.reset();
|
|
757
|
+
restoreAttachedState();
|
|
758
|
+
attachment.ownSun?.shadow?.map?.dispose?.();
|
|
759
|
+
manager.dispose();
|
|
760
|
+
disposed = true;
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
get frame() { return frame; },
|
|
764
|
+
get manager() { return manager; },
|
|
765
|
+
get style() { return styleSettings; },
|
|
766
|
+
get timeOfDay() { return hour; },
|
|
767
|
+
getDiagnostics: () => manager.getDiagnostics(),
|
|
768
|
+
/** Toon-response metadata for material adapters (band softness, tint). */
|
|
769
|
+
getToonResponse: () => cloneJson(styleSettings.toonResponse),
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
refreshFrame();
|
|
773
|
+
return api;
|
|
774
|
+
}
|