@call-me-sensei/toonlab 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +231 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +752 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,386 @@
1
+ // Game-wide lighting identity as one portable document.
2
+ //
3
+ // A lighting style answers "what does light in this game look like at any
4
+ // hour" — the day cycle (sun color/intensity/exposure/sky/fog per hour), the
5
+ // sun's path, the ambient policy, the shadow policy, and the global fixture
6
+ // response — so scenes reference a style plus a time instead of scattering
7
+ // light configuration. Fixture placements (street lamps, lanterns) live in
8
+ // lightingFixtures.js; createLightingSystem applies both to a scene/world.
9
+ //
10
+ // The keyframes generalize the previously hard-coded day cycle in
11
+ // src/environment/environmentTimeOfDay.js into registerable data.
12
+
13
+ import {
14
+ createSettingsPresetDocument,
15
+ parsePresetDocument,
16
+ serializePresetDocument,
17
+ validateSettingsPresetDocument,
18
+ } from '../core/presetDocuments.js';
19
+ import { colorTemperatureToRgb, resolveLightColor } from './colorIntensity.js';
20
+ import { createShadowPolicy } from './lightingDocuments.js';
21
+ import { clamp, cloneJson, finite, isPlainObject, slug } from './utils.js';
22
+
23
+ export const LIGHTING_STYLE_DOCUMENT_TYPE = 'toonlab/lighting-style';
24
+ export const LIGHTING_STYLE_SCHEMA_VERSION = 1;
25
+
26
+ /** Every lighting-style field applies as a hot update; nothing forces a rebuild. */
27
+ export const LIGHTING_STYLE_APPLY_METADATA = Object.freeze({ '*': 'hot' });
28
+
29
+ function styleColor(value, fallback = [1, 1, 1]) {
30
+ if (value === undefined || value === null) return [...fallback];
31
+ if (isPlainObject(value) && value.kelvin !== undefined) {
32
+ return colorTemperatureToRgb(value.kelvin);
33
+ }
34
+ return resolveLightColor(value);
35
+ }
36
+
37
+ function normalizeKeyframeSky(source, base) {
38
+ const input = isPlainObject(source) ? source : {};
39
+ return {
40
+ horizon: styleColor(input.horizon, base.horizon),
41
+ stars: clamp(finite(input.stars, base.stars), 0, 1),
42
+ zenith: styleColor(input.zenith, base.zenith),
43
+ };
44
+ }
45
+
46
+ function normalizeKeyframe(source = {}, previous = null) {
47
+ const base = previous ?? {
48
+ accentScale: 1,
49
+ ambientScale: 1,
50
+ exposureScale: 1,
51
+ fixtureScale: 0,
52
+ fogColor: [0.78, 0.85, 0.95],
53
+ sky: { horizon: [0.78, 0.92, 1.0], stars: 0, zenith: [0.28, 0.56, 0.92] },
54
+ skyGroundTint: [1.05, 0.97, 0.9],
55
+ skyTopTint: [0.86, 0.96, 1.08],
56
+ sunColor: [1, 0.95, 0.85],
57
+ sunIntensity: 1,
58
+ };
59
+ return {
60
+ sky: normalizeKeyframeSky(source.sky, base.sky),
61
+ accentScale: clamp(finite(source.accentScale, base.accentScale), 0, 4),
62
+ ambientScale: clamp(finite(source.ambientScale, base.ambientScale), 0, 8),
63
+ exposureScale: clamp(finite(source.exposureScale, base.exposureScale), 0.05, 8),
64
+ fixtureScale: clamp(finite(source.fixtureScale ?? source.lampScale, base.fixtureScale), 0, 8),
65
+ fogColor: styleColor(source.fogColor, base.fogColor),
66
+ hour: clamp(finite(source.hour, 12), 0, 24),
67
+ // Sky tints intentionally allow >1 values (the environment materials use
68
+ // them as multipliers), so they bypass the 0-1 color clamp.
69
+ skyGroundTint: Array.isArray(source.skyGroundTint)
70
+ ? source.skyGroundTint.slice(0, 3).map((channel) => Math.max(finite(channel, 1), 0))
71
+ : [...base.skyGroundTint],
72
+ skyTopTint: Array.isArray(source.skyTopTint)
73
+ ? source.skyTopTint.slice(0, 3).map((channel) => Math.max(finite(channel, 1), 0))
74
+ : [...base.skyTopTint],
75
+ sunColor: styleColor(source.sunColor, base.sunColor),
76
+ sunIntensity: clamp(finite(source.sunIntensity, base.sunIntensity), 0, 16),
77
+ };
78
+ }
79
+
80
+ function normalizeDayCycle(source) {
81
+ const list = Array.isArray(source) && source.length > 0 ? source : null;
82
+ const frames = [];
83
+ let previous = null;
84
+ for (const entry of list ?? [{ hour: 12 }]) {
85
+ previous = normalizeKeyframe(isPlainObject(entry) ? entry : {}, previous);
86
+ frames.push(previous);
87
+ }
88
+ frames.sort((a, b) => a.hour - b.hour);
89
+ // Seamless midnight: mirror the first frame at hour 24 unless authored.
90
+ if (frames.at(-1).hour < 24) frames.push({ ...cloneJson(frames[0]), hour: 24 });
91
+ if (frames[0].hour > 0) frames.unshift({ ...cloneJson(frames.at(-1)), hour: 0 });
92
+ return frames;
93
+ }
94
+
95
+ function normalizeSunPath(source = {}) {
96
+ return {
97
+ azimuthArc: clamp(finite(source.azimuthArc, Math.PI * 1.6), 0, Math.PI * 2),
98
+ azimuthOffset: finite(source.azimuthOffset, 0),
99
+ heightBase: clamp(finite(source.heightBase, 0.4), 0, 2),
100
+ heightScale: clamp(finite(source.heightScale, 0.6), 0, 2),
101
+ minElevation: clamp(finite(source.minElevation, 0.06), 0, 1),
102
+ orbitRadius: clamp(finite(source.orbitRadius, 0.85), 0.05, 2),
103
+ };
104
+ }
105
+
106
+ /** Normalizes a complete lighting style from a partial source. */
107
+ export function createLightingStyleSettings(source = {}) {
108
+ const input = isPlainObject(source) ? source : {};
109
+ const ambient = isPlainObject(input.ambientLight) ? input.ambientLight : {};
110
+ const exposure = isPlainObject(input.exposure) ? input.exposure : {};
111
+ const fixtures = isPlainObject(input.fixtures) ? input.fixtures : {};
112
+ const toon = isPlainObject(input.toonResponse) ? input.toonResponse : {};
113
+ return {
114
+ ambientLight: {
115
+ color: styleColor(ambient.color, [0.72, 0.78, 0.92]),
116
+ enabled: ambient.enabled === undefined ? true : Boolean(ambient.enabled),
117
+ intensity: clamp(finite(ambient.intensity, 0.45), 0, 8),
118
+ },
119
+ dayCycle: normalizeDayCycle(input.dayCycle ?? input.keyframes),
120
+ exposure: {
121
+ base: clamp(finite(exposure.base, 1), 0.05, 8),
122
+ enabled: exposure.enabled === undefined ? true : Boolean(exposure.enabled),
123
+ },
124
+ fixtures: {
125
+ intensityScale: clamp(finite(fixtures.intensityScale, 1), 0, 8),
126
+ },
127
+ quality: typeof input.quality === 'string' ? input.quality : 'balanced',
128
+ shadowPolicy: createShadowPolicy(input.shadowPolicy),
129
+ sunPath: normalizeSunPath(input.sunPath),
130
+ toonResponse: {
131
+ bandSoftness: clamp(finite(toon.bandSoftness, 0.08), 0, 1),
132
+ rimInfluence: clamp(finite(toon.rimInfluence, 0), 0, 1),
133
+ shadowTint: toon.shadowTint === undefined || toon.shadowTint === null
134
+ ? null
135
+ : styleColor(toon.shadowTint, [0.35, 0.38, 0.52]),
136
+ },
137
+ };
138
+ }
139
+
140
+ export const sanitizeLightingStyleSettings = createLightingStyleSettings;
141
+
142
+ function lerp(a, b, t) {
143
+ return a + (b - a) * t;
144
+ }
145
+
146
+ function lerpColor(a, b, t) {
147
+ return [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)];
148
+ }
149
+
150
+ /**
151
+ * Samples the style's day cycle at an hour (0-24, wrapping). Returns plain
152
+ * arrays/numbers so callers decide what becomes a THREE object.
153
+ */
154
+ export function sampleLightingStyle(settings, hour = 12) {
155
+ const style = isPlainObject(settings) && Array.isArray(settings.dayCycle)
156
+ ? settings
157
+ : createLightingStyleSettings(settings);
158
+ const normalizedHour = ((finite(hour, 12) % 24) + 24) % 24;
159
+ const frames = style.dayCycle;
160
+ let previous = frames[0];
161
+ let next = frames.at(-1);
162
+ for (let index = 0; index < frames.length - 1; index += 1) {
163
+ if (normalizedHour >= frames[index].hour && normalizedHour <= frames[index + 1].hour) {
164
+ previous = frames[index];
165
+ next = frames[index + 1];
166
+ break;
167
+ }
168
+ }
169
+ const span = Math.max(next.hour - previous.hour, 0.001);
170
+ const t = clamp((normalizedHour - previous.hour) / span, 0, 1);
171
+
172
+ const path = style.sunPath;
173
+ const dayT = normalizedHour / 24;
174
+ const azimuth = path.azimuthOffset + (dayT - 0.5) * path.azimuthArc;
175
+ const elevation = Math.max(Math.sin(((normalizedHour - 6) / 12) * Math.PI), path.minElevation);
176
+
177
+ return {
178
+ accentScale: lerp(previous.accentScale, next.accentScale, t),
179
+ ambientScale: lerp(previous.ambientScale, next.ambientScale, t),
180
+ exposure: style.exposure.base * lerp(previous.exposureScale, next.exposureScale, t),
181
+ fixtureScale: lerp(previous.fixtureScale, next.fixtureScale, t),
182
+ fogColor: lerpColor(previous.fogColor, next.fogColor, t),
183
+ hour: normalizedHour,
184
+ skyGroundTint: lerpColor(previous.skyGroundTint, next.skyGroundTint, t),
185
+ skyHorizonColor: lerpColor(previous.sky.horizon, next.sky.horizon, t),
186
+ skyTopTint: lerpColor(previous.skyTopTint, next.skyTopTint, t),
187
+ skyZenithColor: lerpColor(previous.sky.zenith, next.sky.zenith, t),
188
+ starsStrength: lerp(previous.sky.stars, next.sky.stars, t),
189
+ sunColor: lerpColor(previous.sunColor, next.sunColor, t),
190
+ sunElevation: elevation,
191
+ sunIntensity: lerp(previous.sunIntensity, next.sunIntensity, t),
192
+ sunSourceRatios: {
193
+ x: Math.sin(azimuth) * path.orbitRadius,
194
+ y: path.heightBase + elevation * path.heightScale,
195
+ z: -Math.cos(azimuth) * path.orbitRadius,
196
+ },
197
+ };
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Preset registry — starting points, never a boundary. register/overwrite is
202
+ // the supported path for app-defined styles.
203
+
204
+ const STYLE_PRESETS = new Map();
205
+
206
+ /** Registers a lighting style preset. Returns the normalized id. */
207
+ export function registerLightingStylePreset(id, definition = {}, { overwrite = false } = {}) {
208
+ const key = slug(id, 'style');
209
+ if (!overwrite && STYLE_PRESETS.has(key)) {
210
+ throw new Error(`Lighting style preset "${key}" already exists. Pass { overwrite: true } to replace it.`);
211
+ }
212
+ const source = isPlainObject(definition) ? definition : {};
213
+ STYLE_PRESETS.set(key, {
214
+ description: String(source.description ?? ''),
215
+ label: String(source.label ?? source.name ?? key),
216
+ settings: createLightingStyleSettings(source.settings ?? source),
217
+ });
218
+ return key;
219
+ }
220
+
221
+ export function getLightingStylePresetOptions() {
222
+ return [...STYLE_PRESETS.entries()].map(([id, entry]) => ({
223
+ description: entry.description, id, label: entry.label,
224
+ }));
225
+ }
226
+
227
+ /** Resolves an id, document, or settings object into normalized settings. */
228
+ export function resolveLightingStylePreset(value = 'storybook') {
229
+ if (typeof value === 'string') {
230
+ const entry = STYLE_PRESETS.get(value) ?? STYLE_PRESETS.get(slug(value, 'style'));
231
+ if (!entry) {
232
+ throw new Error(`Unknown lighting style preset "${value}". Registered: ${[...STYLE_PRESETS.keys()].join(', ')}.`);
233
+ }
234
+ return cloneJson(entry.settings);
235
+ }
236
+ if (isPlainObject(value) && value.type === LIGHTING_STYLE_DOCUMENT_TYPE) {
237
+ return createLightingStyleSettings(value.settings);
238
+ }
239
+ return createLightingStyleSettings(value);
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Versioned preset documents (shared core envelope).
244
+
245
+ function migrateStyleDocument(input) {
246
+ return {
247
+ ...input,
248
+ id: input.id ?? input.name,
249
+ label: input.label ?? input.title ?? input.name,
250
+ settings: input.settings ?? input.style ?? {},
251
+ type: LIGHTING_STYLE_DOCUMENT_TYPE,
252
+ version: Number(input.version ?? input.schemaVersion ?? 1),
253
+ };
254
+ }
255
+
256
+ export function validateLightingStylePresetDocument(input) {
257
+ return validateSettingsPresetDocument(input, {
258
+ documentType: LIGHTING_STYLE_DOCUMENT_TYPE,
259
+ migrateDocument: migrateStyleDocument,
260
+ normalizeId: (value) => slug(value, ''),
261
+ sanitizeSettings: createLightingStyleSettings,
262
+ schemaVersion: LIGHTING_STYLE_SCHEMA_VERSION,
263
+ });
264
+ }
265
+
266
+ export function createLightingStylePresetDocument(id, definition = {}) {
267
+ return createSettingsPresetDocument(id, definition, {
268
+ collectSettings: (source) => createLightingStyleSettings(source.settings ?? source),
269
+ documentType: LIGHTING_STYLE_DOCUMENT_TYPE,
270
+ schemaVersion: LIGHTING_STYLE_SCHEMA_VERSION,
271
+ validateDocument: validateLightingStylePresetDocument,
272
+ });
273
+ }
274
+
275
+ export function parseLightingStylePresetDocument(input) {
276
+ return parsePresetDocument(input, validateLightingStylePresetDocument, {
277
+ invalidJsonLabel: 'lighting style preset',
278
+ });
279
+ }
280
+
281
+ export function serializeLightingStylePresetDocument(idOrDocument, definition = {}, { pretty = true } = {}) {
282
+ return serializePresetDocument(idOrDocument, definition, {
283
+ createDocument: createLightingStylePresetDocument,
284
+ pretty,
285
+ });
286
+ }
287
+
288
+ /** Registers a parsed/validated style document in the runtime registry. */
289
+ export function registerLightingStylePresetDocument(document, { overwrite = true } = {}) {
290
+ const result = validateLightingStylePresetDocument(document);
291
+ if (!result.ok) throw new Error(result.errors.join(' '));
292
+ return registerLightingStylePreset(result.value.id, {
293
+ description: result.value.description,
294
+ label: result.value.label,
295
+ settings: result.value.settings,
296
+ }, { overwrite });
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Built-in styles. `storybook` reproduces the classic anime-interior day the
301
+ // environment rigs shipped with, so existing worlds keep their look.
302
+
303
+ registerLightingStylePreset('storybook', {
304
+ description: 'Classic anime day: cool lamp-lit night, warm low morning, neutral noon, amber evening.',
305
+ label: 'Storybook Day',
306
+ settings: {
307
+ ambientLight: { color: [0.74, 0.79, 0.94], intensity: 0.45 },
308
+ dayCycle: [
309
+ { accentScale: 0.2, ambientScale: 0.55, exposureScale: 0.92, fixtureScale: 1.35, fogColor: [0.32, 0.38, 0.55], hour: 0, sky: { horizon: [0.22, 0.28, 0.5], stars: 0.85, zenith: [0.06, 0.09, 0.24] }, skyGroundTint: [0.82, 0.86, 1.0], skyTopTint: [0.62, 0.72, 1.02], sunColor: 0x9bbcff, sunIntensity: 0.1 },
310
+ { accentScale: 0.85, ambientScale: 0.8, exposureScale: 1, fixtureScale: 0.7, fogColor: [0.85, 0.7, 0.55], hour: 6, sky: { horizon: [0.98, 0.78, 0.6], stars: 0, zenith: [0.42, 0.52, 0.86] }, skyGroundTint: [1.1, 0.94, 0.82], skyTopTint: [0.95, 0.9, 1.0], sunColor: 0xffb56a, sunIntensity: 0.45 },
311
+ { accentScale: 1, ambientScale: 1, exposureScale: 1.04, fixtureScale: 0, fogColor: [0.78, 0.85, 0.95], hour: 12, sky: { horizon: [0.78, 0.92, 1.0], stars: 0, zenith: [0.28, 0.56, 0.92] }, skyGroundTint: [1.05, 0.97, 0.9], skyTopTint: [0.86, 0.96, 1.08], sunColor: 0xfff1d8, sunIntensity: 1.0 },
312
+ { accentScale: 0.9, ambientScale: 0.78, exposureScale: 1, fixtureScale: 0.8, fogColor: [0.82, 0.6, 0.45], hour: 18, sky: { horizon: [1.0, 0.64, 0.44], stars: 0.05, zenith: [0.34, 0.36, 0.7] }, skyGroundTint: [1.14, 0.9, 0.74], skyTopTint: [0.92, 0.84, 0.96], sunColor: 0xff884b, sunIntensity: 0.5 },
313
+ { accentScale: 0.3, ambientScale: 0.58, exposureScale: 0.94, fixtureScale: 1.3, fogColor: [0.34, 0.4, 0.58], hour: 21, sky: { horizon: [0.24, 0.3, 0.52], stars: 0.75, zenith: [0.07, 0.1, 0.26] }, skyGroundTint: [0.84, 0.87, 1.0], skyTopTint: [0.64, 0.74, 1.02], sunColor: 0x9bbcff, sunIntensity: 0.14 },
314
+ ],
315
+ },
316
+ });
317
+
318
+ registerLightingStylePreset('call-me-sensei', {
319
+ description: 'Vivid, vibrant anime-open-world light: saturated daylight, luminous never-crushed shadows, punchy golden hour, and rich blue nights where fixtures carry the frame.',
320
+ label: 'Call Me Sensei',
321
+ settings: {
322
+ // High cool-tinted ambient keeps shadow regions luminous and colorful
323
+ // instead of gray — the core of the vivid anime-open-world look.
324
+ ambientLight: { color: [0.62, 0.72, 1.0], intensity: 0.58 },
325
+ exposure: { base: 1.08 },
326
+ fixtures: { intensityScale: 1.3 },
327
+ toonResponse: { bandSoftness: 0.06, rimInfluence: 0.35, shadowTint: [0.42, 0.5, 0.85] },
328
+ dayCycle: [
329
+ { accentScale: 0.3, ambientScale: 0.62, exposureScale: 0.94, fixtureScale: 1.55, fogColor: [0.16, 0.2, 0.46], hour: 0, sky: { horizon: [0.12, 0.2, 0.5], stars: 1, zenith: [0.02, 0.05, 0.2] }, skyGroundTint: [0.72, 0.78, 1.05], skyTopTint: [0.42, 0.55, 1.05], sunColor: { kelvin: 11500 }, sunIntensity: 0.16 },
330
+ { accentScale: 1.2, ambientScale: 0.85, exposureScale: 1.04, fixtureScale: 0.7, fogColor: [1.0, 0.68, 0.5], hour: 6, sky: { horizon: [1.0, 0.72, 0.52], stars: 0, zenith: [0.38, 0.5, 0.95] }, skyGroundTint: [1.2, 0.95, 0.78], skyTopTint: [0.95, 0.85, 1.05], sunColor: { kelvin: 2900 }, sunIntensity: 0.65 },
331
+ { accentScale: 1.15, ambientScale: 1.1, exposureScale: 1.1, fixtureScale: 0, fogColor: [0.72, 0.86, 1.0], hour: 12, sky: { horizon: [0.72, 0.9, 1.05], stars: 0, zenith: [0.18, 0.5, 1.0] }, skyGroundTint: [1.12, 1.02, 0.9], skyTopTint: [0.78, 0.98, 1.2], sunColor: { kelvin: 5400 }, sunIntensity: 1.25 },
332
+ { accentScale: 1.35, ambientScale: 0.85, exposureScale: 1.05, fixtureScale: 0.85, fogColor: [1.0, 0.52, 0.36], hour: 18, sky: { horizon: [1.1, 0.5, 0.32], stars: 0.08, zenith: [0.3, 0.3, 0.78] }, skyGroundTint: [1.25, 0.85, 0.66], skyTopTint: [0.95, 0.72, 1.0], sunColor: { kelvin: 2100 }, sunIntensity: 0.72 },
333
+ { accentScale: 0.4, ambientScale: 0.64, exposureScale: 0.96, fixtureScale: 1.5, fogColor: [0.18, 0.22, 0.5], hour: 21, sky: { horizon: [0.15, 0.22, 0.52], stars: 0.95, zenith: [0.03, 0.07, 0.24] }, skyGroundTint: [0.74, 0.8, 1.05], skyTopTint: [0.45, 0.58, 1.05], sunColor: { kelvin: 11000 }, sunIntensity: 0.18 },
334
+ ],
335
+ },
336
+ });
337
+
338
+ registerLightingStylePreset('golden-summer', {
339
+ description: 'Warm high-key daylight with a long amber golden hour and gentle nights.',
340
+ label: 'Golden Summer',
341
+ settings: {
342
+ ambientLight: { color: [0.9, 0.82, 0.7], intensity: 0.5 },
343
+ exposure: { base: 1.06 },
344
+ dayCycle: [
345
+ { accentScale: 0.25, ambientScale: 0.6, exposureScale: 0.9, fixtureScale: 1.2, fogColor: [0.36, 0.4, 0.56], hour: 0, sky: { horizon: [0.26, 0.3, 0.5], stars: 0.8, zenith: [0.07, 0.1, 0.26] }, sunColor: { kelvin: 12000 }, sunIntensity: 0.12 },
346
+ { accentScale: 1, ambientScale: 0.85, exposureScale: 1, fixtureScale: 0.5, fogColor: [0.95, 0.78, 0.58], hour: 6.5, sky: { horizon: [1.05, 0.8, 0.55], stars: 0, zenith: [0.42, 0.54, 0.9] }, sunColor: { kelvin: 3200 }, sunIntensity: 0.55 },
347
+ { accentScale: 1, ambientScale: 1.05, exposureScale: 1.06, fixtureScale: 0, fogColor: [0.85, 0.88, 0.92], hour: 12.5, sky: { horizon: [0.85, 0.94, 1.0], stars: 0, zenith: [0.3, 0.58, 0.94] }, skyGroundTint: [1.1, 1.0, 0.88], skyTopTint: [0.9, 0.98, 1.1], sunColor: { kelvin: 5600 }, sunIntensity: 1.15 },
348
+ { accentScale: 1.15, ambientScale: 0.82, exposureScale: 1.02, fixtureScale: 0.6, fogColor: [0.96, 0.66, 0.42], hour: 18.5, sky: { horizon: [1.1, 0.62, 0.36], stars: 0.05, zenith: [0.38, 0.36, 0.68] }, skyGroundTint: [1.2, 0.92, 0.7], skyTopTint: [0.98, 0.84, 0.9], sunColor: { kelvin: 2400 }, sunIntensity: 0.6 },
349
+ { accentScale: 0.35, ambientScale: 0.62, exposureScale: 0.92, fixtureScale: 1.15, fogColor: [0.38, 0.42, 0.6], hour: 21.5, sky: { horizon: [0.28, 0.32, 0.54], stars: 0.7, zenith: [0.08, 0.11, 0.28] }, sunColor: { kelvin: 11000 }, sunIntensity: 0.16 },
350
+ ],
351
+ },
352
+ });
353
+
354
+ registerLightingStylePreset('overcast-pastel', {
355
+ description: 'Soft shadowless pastel light: flat ambient, muted sun, cool fog.',
356
+ label: 'Overcast Pastel',
357
+ settings: {
358
+ ambientLight: { color: [0.82, 0.85, 0.9], intensity: 0.62 },
359
+ exposure: { base: 0.98 },
360
+ shadowPolicy: { maxShadowedLights: 1 },
361
+ dayCycle: [
362
+ { accentScale: 0.1, ambientScale: 0.7, exposureScale: 0.95, fixtureScale: 1.1, fogColor: [0.4, 0.44, 0.55], hour: 0, sky: { horizon: [0.3, 0.33, 0.45], stars: 0.25, zenith: [0.12, 0.14, 0.24] }, sunColor: { kelvin: 9000 }, sunIntensity: 0.12 },
363
+ { accentScale: 0.3, ambientScale: 1.15, exposureScale: 1, fixtureScale: 0.2, fogColor: [0.78, 0.82, 0.88], hour: 8, sky: { horizon: [0.82, 0.85, 0.9], stars: 0, zenith: [0.55, 0.62, 0.74] }, sunColor: { kelvin: 6800 }, sunIntensity: 0.45 },
364
+ { accentScale: 0.35, ambientScale: 1.25, exposureScale: 1, fixtureScale: 0, fogColor: [0.8, 0.84, 0.9], hour: 13, sky: { horizon: [0.85, 0.88, 0.93], stars: 0, zenith: [0.58, 0.66, 0.78] }, skyGroundTint: [0.98, 1.0, 1.02], skyTopTint: [0.88, 0.92, 1.0], sunColor: { kelvin: 6500 }, sunIntensity: 0.55 },
365
+ { accentScale: 0.25, ambientScale: 1.05, exposureScale: 0.98, fixtureScale: 0.35, fogColor: [0.66, 0.66, 0.74], hour: 18, sky: { horizon: [0.7, 0.68, 0.74], stars: 0, zenith: [0.42, 0.44, 0.56] }, sunColor: { kelvin: 7200 }, sunIntensity: 0.35 },
366
+ { accentScale: 0.12, ambientScale: 0.72, exposureScale: 0.95, fixtureScale: 1.05, fogColor: [0.4, 0.44, 0.56], hour: 21, sky: { horizon: [0.31, 0.34, 0.46], stars: 0.2, zenith: [0.13, 0.15, 0.26] }, sunColor: { kelvin: 9000 }, sunIntensity: 0.14 },
367
+ ],
368
+ },
369
+ });
370
+
371
+ registerLightingStylePreset('neon-night', {
372
+ description: 'Perpetual dusk-to-night cycle where fixtures carry the scene over a cool blue base.',
373
+ label: 'Neon Night',
374
+ settings: {
375
+ ambientLight: { color: [0.45, 0.5, 0.8], intensity: 0.4 },
376
+ exposure: { base: 0.96 },
377
+ fixtures: { intensityScale: 1.35 },
378
+ dayCycle: [
379
+ { accentScale: 0.15, ambientScale: 0.5, exposureScale: 0.9, fixtureScale: 1.5, fogColor: [0.24, 0.2, 0.42], hour: 0, sky: { horizon: [0.2, 0.14, 0.4], stars: 0.9, zenith: [0.05, 0.04, 0.16] }, skyGroundTint: [0.7, 0.7, 0.95], skyTopTint: [0.5, 0.55, 0.95], sunColor: { kelvin: 15000 }, sunIntensity: 0.08 },
380
+ { accentScale: 0.4, ambientScale: 0.68, exposureScale: 0.96, fixtureScale: 1, fogColor: [0.45, 0.36, 0.55], hour: 8, sky: { horizon: [0.5, 0.4, 0.6], stars: 0.1, zenith: [0.2, 0.2, 0.42] }, sunColor: { kelvin: 8000 }, sunIntensity: 0.32 },
381
+ { accentScale: 0.5, ambientScale: 0.8, exposureScale: 1, fixtureScale: 0.7, fogColor: [0.55, 0.48, 0.62], hour: 13, sky: { horizon: [0.6, 0.54, 0.68], stars: 0, zenith: [0.3, 0.32, 0.55] }, sunColor: { kelvin: 6800 }, sunIntensity: 0.5 },
382
+ { accentScale: 0.6, ambientScale: 0.66, exposureScale: 0.96, fixtureScale: 1.25, fogColor: [0.5, 0.3, 0.55], hour: 18, sky: { horizon: [0.62, 0.3, 0.55], stars: 0.25, zenith: [0.18, 0.12, 0.4] }, sunColor: { kelvin: 3000 }, sunIntensity: 0.3 },
383
+ { accentScale: 0.2, ambientScale: 0.52, exposureScale: 0.9, fixtureScale: 1.5, fogColor: [0.26, 0.22, 0.44], hour: 21, sky: { horizon: [0.22, 0.16, 0.42], stars: 0.85, zenith: [0.06, 0.05, 0.18] }, sunColor: { kelvin: 14000 }, sunIntensity: 0.1 },
384
+ ],
385
+ },
386
+ });