@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
package/src/sky/stylizedSky.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
createSettingsPresetDocument,
|
|
5
|
+
parsePresetDocument,
|
|
6
|
+
serializePresetDocument,
|
|
7
|
+
validateSettingsPresetDocument,
|
|
8
|
+
} from '../core/presetDocuments.js';
|
|
3
9
|
import { createSkyNodeMaterial } from '../shaders-tsl/sky.js';
|
|
10
|
+
import { SKY_SCENE_OVERRIDE_PRIORITIES } from './sceneOverrideLayers.js';
|
|
11
|
+
import { resolveSkyQuality } from './skyQuality.js';
|
|
12
|
+
|
|
13
|
+
export { SKY_SCENE_OVERRIDE_PRIORITIES } from './sceneOverrideLayers.js';
|
|
14
|
+
export { SKY_QUALITY_OPTIONS, SKY_QUALITY_TIERS, resolveSkyQuality } from './skyQuality.js';
|
|
4
15
|
|
|
5
16
|
// Procedural stylized sky dome: vertical gradient, sun disc, painterly
|
|
6
17
|
// two-tone clouds, and stars — no texture assets. Designed as the companion
|
|
@@ -25,31 +36,58 @@ export const DEFAULT_SKY_SETTINGS = Object.freeze({
|
|
|
25
36
|
sunDirection: [0.35, 0.8, 0.45],
|
|
26
37
|
sunColor: [1.0, 0.95, 0.82],
|
|
27
38
|
sunSize: 0.026,
|
|
39
|
+
sunDiscSoftness: 0.5,
|
|
28
40
|
sunGlowStrength: 1.0,
|
|
41
|
+
sunDiscIntensity: 2.4,
|
|
42
|
+
sunGlowSpread: 5.0,
|
|
43
|
+
sunGlowCoreSharpness: 60.0,
|
|
44
|
+
sunGlowBroadStrength: 0.16,
|
|
45
|
+
sunGlowCoreStrength: 0.5,
|
|
46
|
+
sunCloudOcclusionStrength: 1.0,
|
|
29
47
|
horizonScattering: 0.5,
|
|
48
|
+
zenithExponent: 0.48,
|
|
49
|
+
groundExponent: 0.55,
|
|
50
|
+
horizonBandSize: 0.42,
|
|
51
|
+
horizonSunPower: 5.0,
|
|
30
52
|
cloudCoverage: 0.42,
|
|
31
53
|
cloudScale: 1.6,
|
|
32
54
|
cloudSpeed: 1.0,
|
|
55
|
+
cloudDirection: Object.freeze([0.9615239476, 0.2747211279]),
|
|
56
|
+
cloudSeed: 0,
|
|
57
|
+
cloudProjection: 0.22,
|
|
58
|
+
cloudSoftness: 0.1,
|
|
59
|
+
cloudEdgeOpacity: 0.65,
|
|
60
|
+
cloudOpacity: 1.0,
|
|
61
|
+
cloudShadeStrength: 0.85,
|
|
62
|
+
cloudShadeThreshold: 0.02,
|
|
63
|
+
cloudShadeSoftness: 0.06,
|
|
64
|
+
cloudLightOffset: 0.4,
|
|
65
|
+
cloudSilverLiningStrength: 0.3,
|
|
66
|
+
cloudSunPower: 10.0,
|
|
67
|
+
cloudHorizonFade: 0.16,
|
|
33
68
|
cloudColor: [1.0, 1.0, 1.0],
|
|
34
69
|
cloudShadeColor: [0.68, 0.78, 0.92],
|
|
35
70
|
starsStrength: 0.0,
|
|
71
|
+
starsColor: Object.freeze([1.0, 0.98, 0.92]),
|
|
72
|
+
starsSeed: 0,
|
|
73
|
+
starsScale: 14.0,
|
|
74
|
+
starsDensity: 0.28,
|
|
75
|
+
starsSize: 0.06,
|
|
76
|
+
starsTwinkleStrength: 0.8,
|
|
77
|
+
starsTwinkleSpeed: 1.0,
|
|
78
|
+
starsHorizonFade: 0.24,
|
|
36
79
|
});
|
|
37
80
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
description: 'Studio-managed signature sky, curated by Call Me Sensei and updated over releases. Currently the tuned library defaults.',
|
|
49
|
-
label: 'Call Me Sensei',
|
|
50
|
-
settings: Object.freeze({}),
|
|
51
|
-
})],
|
|
52
|
-
]);
|
|
81
|
+
/** Document `type` discriminator for portable sky-look presets. */
|
|
82
|
+
export const SKY_PRESET_DOCUMENT_TYPE = 'toonlab/sky-preset';
|
|
83
|
+
|
|
84
|
+
/** Current portable sky preset schema version. */
|
|
85
|
+
export const SKY_PRESET_SCHEMA_VERSION = 1;
|
|
86
|
+
|
|
87
|
+
// Named sky looks. Built-ins and imported preset documents use the same
|
|
88
|
+
// registry so a preset behaves identically whether it shipped with Toonlab or
|
|
89
|
+
// was authored in a lab and registered at runtime.
|
|
90
|
+
const skyPresetRegistry = new Map();
|
|
53
91
|
|
|
54
92
|
/**
|
|
55
93
|
* Registers a named sky preset so it resolves in `createSkySettings({
|
|
@@ -57,20 +95,17 @@ const skyPresetRegistry = new Map([
|
|
|
57
95
|
* settings? }` or flat settings.
|
|
58
96
|
*/
|
|
59
97
|
export function registerSkyPreset(name, preset = {}, { overwrite = false } = {}) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
|
|
63
|
-
throw new Error(`Sky preset "${id}" already exists.`);
|
|
98
|
+
const document = createSkyPresetDocument(name, preset);
|
|
99
|
+
if (!overwrite && skyPresetRegistry.has(document.id)) {
|
|
100
|
+
throw new Error(`Sky preset "${document.id}" already exists.`);
|
|
64
101
|
}
|
|
65
|
-
const source = preset && typeof preset === 'object' ? preset : {};
|
|
66
|
-
const { label, description, settings, ...flat } = source;
|
|
67
102
|
const entry = Object.freeze({
|
|
68
|
-
description:
|
|
69
|
-
label:
|
|
70
|
-
settings: Object.freeze({ ...
|
|
103
|
+
description: document.description,
|
|
104
|
+
label: document.label,
|
|
105
|
+
settings: Object.freeze({ ...document.settings }),
|
|
71
106
|
});
|
|
72
|
-
skyPresetRegistry.set(id, entry);
|
|
73
|
-
return { description: entry.description, id, label: entry.label };
|
|
107
|
+
skyPresetRegistry.set(document.id, entry);
|
|
108
|
+
return { description: entry.description, id: document.id, label: entry.label };
|
|
74
109
|
}
|
|
75
110
|
|
|
76
111
|
/** Lists registered sky presets as `{ id, label, description }` (for HUDs). */
|
|
@@ -82,9 +117,9 @@ export function getSkyPresetOptions() {
|
|
|
82
117
|
}));
|
|
83
118
|
}
|
|
84
119
|
|
|
85
|
-
function finiteNumber(value, fallback) {
|
|
120
|
+
function finiteNumber(value, fallback, { max = Infinity, min = -Infinity } = {}) {
|
|
86
121
|
const number = Number(value);
|
|
87
|
-
return Number.isFinite(number) ? number : fallback;
|
|
122
|
+
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
|
|
88
123
|
}
|
|
89
124
|
|
|
90
125
|
function colorArray(value, fallback) {
|
|
@@ -113,6 +148,29 @@ function vector3Array(value, fallback) {
|
|
|
113
148
|
return fallback.slice();
|
|
114
149
|
}
|
|
115
150
|
|
|
151
|
+
function vector2Array(value, fallback) {
|
|
152
|
+
if (value?.isVector2) return [value.x, value.y];
|
|
153
|
+
if (Array.isArray(value) && value.length >= 2) {
|
|
154
|
+
const next = value.slice(0, 2).map(Number);
|
|
155
|
+
return next.every(Number.isFinite) ? next : fallback.slice();
|
|
156
|
+
}
|
|
157
|
+
return fallback.slice();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function direction2Array(value, fallback) {
|
|
161
|
+
const supplied = value?.isVector2 || (
|
|
162
|
+
Array.isArray(value)
|
|
163
|
+
&& value.length >= 2
|
|
164
|
+
&& value.slice(0, 2).every((channel) => Number.isFinite(Number(channel)))
|
|
165
|
+
);
|
|
166
|
+
if (!supplied) return fallback.slice();
|
|
167
|
+
const next = vector2Array(value, fallback);
|
|
168
|
+
const length = Math.hypot(next[0], next[1]);
|
|
169
|
+
if (length < 1e-6) return fallback.slice();
|
|
170
|
+
return [next[0] / length, next[1] / length]
|
|
171
|
+
.map((channel) => Number(channel.toFixed(10)));
|
|
172
|
+
}
|
|
173
|
+
|
|
116
174
|
/**
|
|
117
175
|
* Validates and merges partial sky options over {@link DEFAULT_SKY_SETTINGS}.
|
|
118
176
|
* Unknown keys are ignored; malformed values fall back to their defaults.
|
|
@@ -123,7 +181,9 @@ function vector3Array(value, fallback) {
|
|
|
123
181
|
* @returns {Object} A complete, plain sky settings object.
|
|
124
182
|
*/
|
|
125
183
|
export function createSkySettings(options = {}) {
|
|
126
|
-
const source =
|
|
184
|
+
const source = typeof options === 'string'
|
|
185
|
+
? { preset: options }
|
|
186
|
+
: (options && typeof options === 'object' ? options : {});
|
|
127
187
|
const presetSettings = skyPresetRegistry.get(source.preset)?.settings;
|
|
128
188
|
const base = presetSettings ? { ...DEFAULT_SKY_SETTINGS, ...presetSettings } : DEFAULT_SKY_SETTINGS;
|
|
129
189
|
return {
|
|
@@ -133,15 +193,47 @@ export function createSkySettings(options = {}) {
|
|
|
133
193
|
groundColor: colorArray(source.groundColor, base.groundColor),
|
|
134
194
|
sunDirection: vector3Array(source.sunDirection, base.sunDirection),
|
|
135
195
|
sunColor: colorArray(source.sunColor, base.sunColor),
|
|
136
|
-
sunSize: finiteNumber(source.sunSize, base.sunSize),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
196
|
+
sunSize: finiteNumber(source.sunSize, base.sunSize, { max: 0.2, min: 0 }),
|
|
197
|
+
sunDiscSoftness: finiteNumber(source.sunDiscSoftness, base.sunDiscSoftness, { max: 1, min: 0.01 }),
|
|
198
|
+
sunGlowStrength: finiteNumber(source.sunGlowStrength, base.sunGlowStrength, { max: 4, min: 0 }),
|
|
199
|
+
sunDiscIntensity: finiteNumber(source.sunDiscIntensity, base.sunDiscIntensity, { max: 8, min: 0 }),
|
|
200
|
+
sunGlowSpread: finiteNumber(source.sunGlowSpread, base.sunGlowSpread, { max: 20, min: 1 }),
|
|
201
|
+
sunGlowCoreSharpness: finiteNumber(source.sunGlowCoreSharpness, base.sunGlowCoreSharpness, { max: 200, min: 5 }),
|
|
202
|
+
sunGlowBroadStrength: finiteNumber(source.sunGlowBroadStrength, base.sunGlowBroadStrength, { max: 2, min: 0 }),
|
|
203
|
+
sunGlowCoreStrength: finiteNumber(source.sunGlowCoreStrength, base.sunGlowCoreStrength, { max: 2, min: 0 }),
|
|
204
|
+
sunCloudOcclusionStrength: finiteNumber(source.sunCloudOcclusionStrength, base.sunCloudOcclusionStrength, { max: 1, min: 0 }),
|
|
205
|
+
horizonScattering: finiteNumber(source.horizonScattering, base.horizonScattering, { max: 1, min: 0 }),
|
|
206
|
+
zenithExponent: finiteNumber(source.zenithExponent, base.zenithExponent, { max: 4, min: 0.1 }),
|
|
207
|
+
groundExponent: finiteNumber(source.groundExponent, base.groundExponent, { max: 4, min: 0.1 }),
|
|
208
|
+
horizonBandSize: finiteNumber(source.horizonBandSize, base.horizonBandSize, { max: 1, min: 0.02 }),
|
|
209
|
+
horizonSunPower: finiteNumber(source.horizonSunPower, base.horizonSunPower, { max: 20, min: 0.5 }),
|
|
210
|
+
cloudCoverage: finiteNumber(source.cloudCoverage, base.cloudCoverage, { max: 1, min: 0 }),
|
|
211
|
+
cloudScale: finiteNumber(source.cloudScale, base.cloudScale, { max: 6, min: 0.1 }),
|
|
212
|
+
cloudSpeed: finiteNumber(source.cloudSpeed, base.cloudSpeed, { max: 4, min: 0 }),
|
|
213
|
+
cloudDirection: direction2Array(source.cloudDirection, base.cloudDirection),
|
|
214
|
+
cloudSeed: Math.round(finiteNumber(source.cloudSeed, base.cloudSeed, { max: 1000, min: 0 })),
|
|
215
|
+
cloudProjection: finiteNumber(source.cloudProjection, base.cloudProjection, { max: 0.8, min: 0.05 }),
|
|
216
|
+
cloudSoftness: finiteNumber(source.cloudSoftness, base.cloudSoftness, { max: 0.3, min: 0.01 }),
|
|
217
|
+
cloudEdgeOpacity: finiteNumber(source.cloudEdgeOpacity, base.cloudEdgeOpacity, { max: 1, min: 0 }),
|
|
218
|
+
cloudOpacity: finiteNumber(source.cloudOpacity, base.cloudOpacity, { max: 1, min: 0 }),
|
|
219
|
+
cloudShadeStrength: finiteNumber(source.cloudShadeStrength, base.cloudShadeStrength, { max: 1, min: 0 }),
|
|
220
|
+
cloudShadeThreshold: finiteNumber(source.cloudShadeThreshold, base.cloudShadeThreshold, { max: 0.3, min: -0.3 }),
|
|
221
|
+
cloudShadeSoftness: finiteNumber(source.cloudShadeSoftness, base.cloudShadeSoftness, { max: 0.3, min: 0.001 }),
|
|
222
|
+
cloudLightOffset: finiteNumber(source.cloudLightOffset, base.cloudLightOffset, { max: 2, min: 0 }),
|
|
223
|
+
cloudSilverLiningStrength: finiteNumber(source.cloudSilverLiningStrength, base.cloudSilverLiningStrength, { max: 2, min: 0 }),
|
|
224
|
+
cloudSunPower: finiteNumber(source.cloudSunPower, base.cloudSunPower, { max: 40, min: 1 }),
|
|
225
|
+
cloudHorizonFade: finiteNumber(source.cloudHorizonFade, base.cloudHorizonFade, { max: 0.8, min: 0.02 }),
|
|
142
226
|
cloudColor: colorArray(source.cloudColor, base.cloudColor),
|
|
143
227
|
cloudShadeColor: colorArray(source.cloudShadeColor, base.cloudShadeColor),
|
|
144
|
-
starsStrength: finiteNumber(source.starsStrength, base.starsStrength),
|
|
228
|
+
starsStrength: finiteNumber(source.starsStrength, base.starsStrength, { max: 2, min: 0 }),
|
|
229
|
+
starsColor: colorArray(source.starsColor, base.starsColor),
|
|
230
|
+
starsSeed: Math.round(finiteNumber(source.starsSeed, base.starsSeed, { max: 1000, min: 0 })),
|
|
231
|
+
starsScale: finiteNumber(source.starsScale, base.starsScale, { max: 64, min: 2 }),
|
|
232
|
+
starsDensity: finiteNumber(source.starsDensity, base.starsDensity, { max: 1, min: 0 }),
|
|
233
|
+
starsSize: finiteNumber(source.starsSize, base.starsSize, { max: 0.2, min: 0.005 }),
|
|
234
|
+
starsTwinkleStrength: finiteNumber(source.starsTwinkleStrength, base.starsTwinkleStrength, { max: 1, min: 0 }),
|
|
235
|
+
starsTwinkleSpeed: finiteNumber(source.starsTwinkleSpeed, base.starsTwinkleSpeed, { max: 4, min: 0 }),
|
|
236
|
+
starsHorizonFade: finiteNumber(source.starsHorizonFade, base.starsHorizonFade, { max: 1, min: 0.04 }),
|
|
145
237
|
};
|
|
146
238
|
}
|
|
147
239
|
|
|
@@ -184,6 +276,7 @@ const SKY_FIELD_DEFINITIONS = Object.freeze({
|
|
|
184
276
|
description: 'Sphere radius of the sky dome in meters. Construction-only: baked into the dome geometry; applySettings stores but does not rebuild it.',
|
|
185
277
|
label: 'Radius',
|
|
186
278
|
range: { max: 1000, min: 10, step: 1 },
|
|
279
|
+
serializable: false,
|
|
187
280
|
type: 'number',
|
|
188
281
|
},
|
|
189
282
|
},
|
|
@@ -203,9 +296,33 @@ const SKY_FIELD_DEFINITIONS = Object.freeze({
|
|
|
203
296
|
label: 'Ground Color',
|
|
204
297
|
type: 'color',
|
|
205
298
|
},
|
|
299
|
+
zenithExponent: {
|
|
300
|
+
description: 'Shape of the horizon-to-zenith gradient. Lower values bring the zenith color farther toward the horizon.',
|
|
301
|
+
label: 'Zenith Gradient Shape',
|
|
302
|
+
range: { max: 4, min: 0.1, step: 0.01 },
|
|
303
|
+
type: 'number',
|
|
304
|
+
},
|
|
305
|
+
groundExponent: {
|
|
306
|
+
description: 'Shape of the mirrored below-horizon fade into the ground color.',
|
|
307
|
+
label: 'Ground Gradient Shape',
|
|
308
|
+
range: { max: 4, min: 0.1, step: 0.01 },
|
|
309
|
+
type: 'number',
|
|
310
|
+
},
|
|
311
|
+
horizonBandSize: {
|
|
312
|
+
description: 'Vertical size of the sun-side atmospheric scattering band around the horizon.',
|
|
313
|
+
label: 'Horizon Band Size',
|
|
314
|
+
range: { max: 1, min: 0.02, step: 0.01 },
|
|
315
|
+
type: 'number',
|
|
316
|
+
},
|
|
317
|
+
horizonSunPower: {
|
|
318
|
+
description: 'How tightly horizon scattering concentrates toward the sun direction.',
|
|
319
|
+
label: 'Horizon Sun Focus',
|
|
320
|
+
range: { max: 20, min: 0.5, step: 0.1 },
|
|
321
|
+
type: 'number',
|
|
322
|
+
},
|
|
206
323
|
horizonScattering: {
|
|
207
|
-
description: '
|
|
208
|
-
label: 'Horizon
|
|
324
|
+
description: 'Strength of the bright sun-side atmospheric wedge at the horizon.',
|
|
325
|
+
label: 'Horizon Scatter Strength',
|
|
209
326
|
range: { max: 1, min: 0, step: 0.01 },
|
|
210
327
|
type: 'number',
|
|
211
328
|
},
|
|
@@ -227,12 +344,54 @@ const SKY_FIELD_DEFINITIONS = Object.freeze({
|
|
|
227
344
|
range: { max: 0.2, min: 0, step: 0.001 },
|
|
228
345
|
type: 'number',
|
|
229
346
|
},
|
|
347
|
+
sunDiscSoftness: {
|
|
348
|
+
description: 'Fraction of the disc radius used for its anti-aliased painterly edge.',
|
|
349
|
+
label: 'Disc Edge Softness',
|
|
350
|
+
range: { max: 1, min: 0.01, step: 0.01 },
|
|
351
|
+
type: 'number',
|
|
352
|
+
},
|
|
353
|
+
sunDiscIntensity: {
|
|
354
|
+
description: 'Brightness multiplier of the solid sun disc before the renderer tone map.',
|
|
355
|
+
label: 'Disc Intensity',
|
|
356
|
+
range: { max: 8, min: 0, step: 0.05 },
|
|
357
|
+
type: 'number',
|
|
358
|
+
},
|
|
230
359
|
sunGlowStrength: {
|
|
231
|
-
description: '
|
|
232
|
-
label: '
|
|
360
|
+
description: 'Master intensity of the broad and core sun glow terms.',
|
|
361
|
+
label: 'Glow Strength',
|
|
233
362
|
range: { max: 4, min: 0, step: 0.01 },
|
|
234
363
|
type: 'number',
|
|
235
364
|
},
|
|
365
|
+
sunGlowSpread: {
|
|
366
|
+
description: 'Falloff power of the broad halo. Lower values spread the glow across more sky.',
|
|
367
|
+
label: 'Glow Spread',
|
|
368
|
+
range: { max: 20, min: 1, step: 0.1 },
|
|
369
|
+
type: 'number',
|
|
370
|
+
},
|
|
371
|
+
sunGlowCoreSharpness: {
|
|
372
|
+
description: 'Falloff power of the tight inner halo. Higher values make a smaller, sharper core.',
|
|
373
|
+
label: 'Core Sharpness',
|
|
374
|
+
range: { max: 200, min: 5, step: 1 },
|
|
375
|
+
type: 'number',
|
|
376
|
+
},
|
|
377
|
+
sunGlowBroadStrength: {
|
|
378
|
+
description: 'Contribution of the broad halo inside the master glow strength.',
|
|
379
|
+
label: 'Broad Halo',
|
|
380
|
+
range: { max: 2, min: 0, step: 0.01 },
|
|
381
|
+
type: 'number',
|
|
382
|
+
},
|
|
383
|
+
sunGlowCoreStrength: {
|
|
384
|
+
description: 'Contribution of the tight inner halo inside the master glow strength.',
|
|
385
|
+
label: 'Core Halo',
|
|
386
|
+
range: { max: 2, min: 0, step: 0.01 },
|
|
387
|
+
type: 'number',
|
|
388
|
+
},
|
|
389
|
+
sunCloudOcclusionStrength: {
|
|
390
|
+
description: 'How strongly dense cloud coverage hides the sun disc. 0 keeps the disc visible through cloud.',
|
|
391
|
+
label: 'Cloud Occlusion',
|
|
392
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
393
|
+
type: 'number',
|
|
394
|
+
},
|
|
236
395
|
},
|
|
237
396
|
clouds: {
|
|
238
397
|
cloudCoverage: {
|
|
@@ -247,12 +406,48 @@ const SKY_FIELD_DEFINITIONS = Object.freeze({
|
|
|
247
406
|
range: { max: 6, min: 0.1, step: 0.01 },
|
|
248
407
|
type: 'number',
|
|
249
408
|
},
|
|
409
|
+
cloudSoftness: {
|
|
410
|
+
description: 'Width of the painterly cloud silhouette transition.',
|
|
411
|
+
label: 'Edge Softness',
|
|
412
|
+
range: { max: 0.3, min: 0.01, step: 0.005 },
|
|
413
|
+
type: 'number',
|
|
414
|
+
},
|
|
415
|
+
cloudProjection: {
|
|
416
|
+
description: 'Perspective offset of the virtual cloud plane; higher values flatten clouds toward the horizon.',
|
|
417
|
+
label: 'Layer Projection',
|
|
418
|
+
range: { max: 0.8, min: 0.05, step: 0.01 },
|
|
419
|
+
type: 'number',
|
|
420
|
+
},
|
|
421
|
+
cloudOpacity: {
|
|
422
|
+
description: 'Overall blend opacity of the procedural cloud layer.',
|
|
423
|
+
label: 'Cloud Opacity',
|
|
424
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
425
|
+
type: 'number',
|
|
426
|
+
},
|
|
427
|
+
cloudEdgeOpacity: {
|
|
428
|
+
description: 'Opacity of the soft outer silhouette relative to the solid cloud core.',
|
|
429
|
+
label: 'Edge Opacity',
|
|
430
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
431
|
+
type: 'number',
|
|
432
|
+
},
|
|
250
433
|
cloudSpeed: {
|
|
251
|
-
description: 'How fast
|
|
434
|
+
description: 'How fast the authored cloud layer drifts across the dome.',
|
|
252
435
|
label: 'Cloud Speed',
|
|
253
436
|
range: { max: 4, min: 0, step: 0.01 },
|
|
254
437
|
type: 'number',
|
|
255
438
|
},
|
|
439
|
+
cloudDirection: {
|
|
440
|
+
description: 'Normalized horizontal drift direction of the authored cloud layer; speed is controlled separately.',
|
|
441
|
+
label: 'Cloud Direction',
|
|
442
|
+
type: 'vector2',
|
|
443
|
+
},
|
|
444
|
+
cloudSeed: {
|
|
445
|
+
description: 'Offsets the procedural cloud field to produce a different deterministic composition.',
|
|
446
|
+
integer: true,
|
|
447
|
+
label: 'Cloud Pattern Seed',
|
|
448
|
+
range: { max: 1000, min: 0, step: 1 },
|
|
449
|
+
type: 'number',
|
|
450
|
+
},
|
|
256
451
|
cloudColor: {
|
|
257
452
|
description: 'Lit tone of the two-tone painterly clouds.',
|
|
258
453
|
label: 'Cloud Color',
|
|
@@ -263,6 +458,48 @@ const SKY_FIELD_DEFINITIONS = Object.freeze({
|
|
|
263
458
|
label: 'Cloud Shade Color',
|
|
264
459
|
type: 'color',
|
|
265
460
|
},
|
|
461
|
+
cloudShadeStrength: {
|
|
462
|
+
description: 'Strength of the two-tone shaded underside.',
|
|
463
|
+
label: 'Shade Strength',
|
|
464
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
465
|
+
type: 'number',
|
|
466
|
+
},
|
|
467
|
+
cloudShadeThreshold: {
|
|
468
|
+
description: 'Noise-difference threshold that separates the lit and shaded cloud tones.',
|
|
469
|
+
label: 'Shade Threshold',
|
|
470
|
+
range: { max: 0.3, min: -0.3, step: 0.005 },
|
|
471
|
+
type: 'number',
|
|
472
|
+
},
|
|
473
|
+
cloudShadeSoftness: {
|
|
474
|
+
description: 'Softness of the transition between the two cloud tones.',
|
|
475
|
+
label: 'Shade Softness',
|
|
476
|
+
range: { max: 0.3, min: 0.001, step: 0.005 },
|
|
477
|
+
type: 'number',
|
|
478
|
+
},
|
|
479
|
+
cloudLightOffset: {
|
|
480
|
+
description: 'Distance of the secondary noise sample toward the sun; controls the depth and directionality of cloud shading.',
|
|
481
|
+
label: 'Light Sample Offset',
|
|
482
|
+
range: { max: 2, min: 0, step: 0.01 },
|
|
483
|
+
type: 'number',
|
|
484
|
+
},
|
|
485
|
+
cloudSilverLiningStrength: {
|
|
486
|
+
description: 'Warm sun-colored lining added to cloud edges facing the sun.',
|
|
487
|
+
label: 'Silver Lining',
|
|
488
|
+
range: { max: 2, min: 0, step: 0.01 },
|
|
489
|
+
type: 'number',
|
|
490
|
+
},
|
|
491
|
+
cloudSunPower: {
|
|
492
|
+
description: 'Angular focus of the sun-colored cloud lining.',
|
|
493
|
+
label: 'Lining Focus',
|
|
494
|
+
range: { max: 40, min: 1, step: 0.5 },
|
|
495
|
+
type: 'number',
|
|
496
|
+
},
|
|
497
|
+
cloudHorizonFade: {
|
|
498
|
+
description: 'Altitude at which the cloud layer reaches full opacity above the horizon.',
|
|
499
|
+
label: 'Horizon Fade',
|
|
500
|
+
range: { max: 0.8, min: 0.02, step: 0.01 },
|
|
501
|
+
type: 'number',
|
|
502
|
+
},
|
|
266
503
|
},
|
|
267
504
|
stars: {
|
|
268
505
|
starsStrength: {
|
|
@@ -271,6 +508,54 @@ const SKY_FIELD_DEFINITIONS = Object.freeze({
|
|
|
271
508
|
range: { max: 2, min: 0, step: 0.01 },
|
|
272
509
|
type: 'number',
|
|
273
510
|
},
|
|
511
|
+
starsColor: {
|
|
512
|
+
description: 'Tint of the procedural star glints.',
|
|
513
|
+
label: 'Stars Color',
|
|
514
|
+
type: 'color',
|
|
515
|
+
},
|
|
516
|
+
starsSeed: {
|
|
517
|
+
description: 'Offsets the deterministic star pattern without changing density or size.',
|
|
518
|
+
integer: true,
|
|
519
|
+
label: 'Stars Pattern Seed',
|
|
520
|
+
range: { max: 1000, min: 0, step: 1 },
|
|
521
|
+
type: 'number',
|
|
522
|
+
},
|
|
523
|
+
starsDensity: {
|
|
524
|
+
description: 'Fraction of candidate cells allowed to contain a visible star.',
|
|
525
|
+
label: 'Stars Density',
|
|
526
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
527
|
+
type: 'number',
|
|
528
|
+
},
|
|
529
|
+
starsScale: {
|
|
530
|
+
description: 'Density scale of the projected star grid; higher values produce more, smaller cells.',
|
|
531
|
+
label: 'Stars Scale',
|
|
532
|
+
range: { max: 64, min: 2, step: 0.5 },
|
|
533
|
+
type: 'number',
|
|
534
|
+
},
|
|
535
|
+
starsSize: {
|
|
536
|
+
description: 'Size of each procedural star glint inside its cell.',
|
|
537
|
+
label: 'Stars Size',
|
|
538
|
+
range: { max: 0.2, min: 0.005, step: 0.005 },
|
|
539
|
+
type: 'number',
|
|
540
|
+
},
|
|
541
|
+
starsTwinkleStrength: {
|
|
542
|
+
description: 'Depth of per-star brightness animation. 0 disables twinkle without hiding stars.',
|
|
543
|
+
label: 'Twinkle Strength',
|
|
544
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
545
|
+
type: 'number',
|
|
546
|
+
},
|
|
547
|
+
starsTwinkleSpeed: {
|
|
548
|
+
description: 'Speed multiplier of the seeded per-star twinkle animation.',
|
|
549
|
+
label: 'Twinkle Speed',
|
|
550
|
+
range: { max: 4, min: 0, step: 0.01 },
|
|
551
|
+
type: 'number',
|
|
552
|
+
},
|
|
553
|
+
starsHorizonFade: {
|
|
554
|
+
description: 'Altitude at which the star field reaches full brightness above the horizon.',
|
|
555
|
+
label: 'Horizon Fade',
|
|
556
|
+
range: { max: 1, min: 0.04, step: 0.01 },
|
|
557
|
+
type: 'number',
|
|
558
|
+
},
|
|
274
559
|
},
|
|
275
560
|
});
|
|
276
561
|
|
|
@@ -281,6 +566,7 @@ function createSkyFieldMetadata(group, key, field) {
|
|
|
281
566
|
description: field.description,
|
|
282
567
|
group: group.id,
|
|
283
568
|
id: `${group.id}.${key}`,
|
|
569
|
+
integer: field.integer ?? false,
|
|
284
570
|
key,
|
|
285
571
|
label: field.label,
|
|
286
572
|
optionLabels: field.optionLabels ?? null,
|
|
@@ -310,6 +596,239 @@ export const SKY_SETTING_FIELD_SCHEMA = Object.freeze(
|
|
|
310
596
|
),
|
|
311
597
|
);
|
|
312
598
|
|
|
599
|
+
const SKY_FIELDS_BY_KEY = Object.freeze(Object.fromEntries(
|
|
600
|
+
Object.values(SKY_SETTING_FIELD_SCHEMA)
|
|
601
|
+
.flatMap((fields) => Object.entries(fields)),
|
|
602
|
+
));
|
|
603
|
+
|
|
604
|
+
function cleanSkyObject(value) {
|
|
605
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function normalizeSkyPresetId(value) {
|
|
609
|
+
return String(value ?? '').trim();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function collectTopLevelSkySettings(source) {
|
|
613
|
+
const input = cleanSkyObject(source);
|
|
614
|
+
return Object.fromEntries(
|
|
615
|
+
Object.keys(SKY_FIELDS_BY_KEY)
|
|
616
|
+
.filter((key) => input[key] !== undefined)
|
|
617
|
+
.map((key) => [key, input[key]]),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function collectSkyPresetWarnings(settings = {}) {
|
|
622
|
+
const warnings = [];
|
|
623
|
+
for (const key of Object.keys(cleanSkyObject(settings))) {
|
|
624
|
+
const field = SKY_FIELDS_BY_KEY[key];
|
|
625
|
+
if (!field) warnings.push(`Unknown sky setting "${key}" was ignored.`);
|
|
626
|
+
else if (!field.serializable) {
|
|
627
|
+
warnings.push(`Sky setting "${key}" is construction-only and was not stored in the preset.`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return warnings;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Normalizes a sky look into a complete, JSON-safe settings object. Runtime
|
|
635
|
+
* construction state such as the dome radius is intentionally excluded.
|
|
636
|
+
*/
|
|
637
|
+
export function sanitizeSkyPresetSettings(settings = {}) {
|
|
638
|
+
const knownSettings = collectTopLevelSkySettings(settings);
|
|
639
|
+
const normalized = createSkySettings(knownSettings);
|
|
640
|
+
return Object.fromEntries(
|
|
641
|
+
Object.entries(SKY_FIELDS_BY_KEY)
|
|
642
|
+
.filter(([, field]) => field.serializable)
|
|
643
|
+
.map(([key]) => [key, normalized[key]]),
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function migrateSkyPresetDocument(input) {
|
|
648
|
+
const source = cleanSkyObject(input);
|
|
649
|
+
const numericVersion = Number(source.version ?? source.schemaVersion ?? 0);
|
|
650
|
+
const version = Number.isFinite(numericVersion) ? Math.round(numericVersion) : 0;
|
|
651
|
+
if (version > SKY_PRESET_SCHEMA_VERSION) return { ...source, version };
|
|
652
|
+
const nestedSettings = cleanSkyObject(source.settings);
|
|
653
|
+
return {
|
|
654
|
+
description: source.description ?? '',
|
|
655
|
+
id: source.id ?? source.name ?? source.preset ?? '',
|
|
656
|
+
label: source.label ?? source.title ?? source.name ?? source.id ?? '',
|
|
657
|
+
settings: Object.keys(nestedSettings).length > 0
|
|
658
|
+
? nestedSettings
|
|
659
|
+
: collectTopLevelSkySettings(source),
|
|
660
|
+
type: source.type ?? SKY_PRESET_DOCUMENT_TYPE,
|
|
661
|
+
version: SKY_PRESET_SCHEMA_VERSION,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Validates and normalizes a portable sky preset document. Never throws. */
|
|
666
|
+
export function validateSkyPresetDocument(input) {
|
|
667
|
+
if (cleanSkyObject(input).type !== undefined && input.type !== SKY_PRESET_DOCUMENT_TYPE) {
|
|
668
|
+
return {
|
|
669
|
+
errors: [`Sky preset type must be "${SKY_PRESET_DOCUMENT_TYPE}".`],
|
|
670
|
+
ok: false,
|
|
671
|
+
value: null,
|
|
672
|
+
warnings: [],
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
return validateSettingsPresetDocument(input, {
|
|
676
|
+
collectWarnings: collectSkyPresetWarnings,
|
|
677
|
+
documentType: SKY_PRESET_DOCUMENT_TYPE,
|
|
678
|
+
migrateDocument: migrateSkyPresetDocument,
|
|
679
|
+
normalizeId: normalizeSkyPresetId,
|
|
680
|
+
sanitizeSettings: sanitizeSkyPresetSettings,
|
|
681
|
+
schemaVersion: SKY_PRESET_SCHEMA_VERSION,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** Parses JSON text or an object into a validated sky preset document. */
|
|
686
|
+
export function parseSkyPresetDocument(input) {
|
|
687
|
+
return parsePresetDocument(input, validateSkyPresetDocument, {
|
|
688
|
+
invalidJsonLabel: 'sky preset',
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** Creates a canonical, versioned sky preset document. */
|
|
693
|
+
export function createSkyPresetDocument(id, definition = {}) {
|
|
694
|
+
return createSettingsPresetDocument(id, definition, {
|
|
695
|
+
collectSettings: (source) => source.settings ?? collectTopLevelSkySettings(source),
|
|
696
|
+
documentType: SKY_PRESET_DOCUMENT_TYPE,
|
|
697
|
+
schemaVersion: SKY_PRESET_SCHEMA_VERSION,
|
|
698
|
+
validateDocument: validateSkyPresetDocument,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** Serializes a sky preset id/definition or document-like object as JSON. */
|
|
703
|
+
export function serializeSkyPreset(idOrDocument, definition = {}, { pretty = true } = {}) {
|
|
704
|
+
return serializePresetDocument(idOrDocument, definition, {
|
|
705
|
+
argumentCount: arguments.length,
|
|
706
|
+
createDocument: createSkyPresetDocument,
|
|
707
|
+
pretty,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Registers a portable preset document, overwriting an existing id by default. */
|
|
712
|
+
export function registerSerializedSkyPreset(input, options = {}) {
|
|
713
|
+
const result = parseSkyPresetDocument(input);
|
|
714
|
+
if (!result.ok) throw new Error(result.errors.join(' '));
|
|
715
|
+
return registerSkyPreset(result.value.id, result.value, {
|
|
716
|
+
overwrite: options.overwrite ?? true,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Keep the historical ids stable, then provide distinct looks that are useful
|
|
721
|
+
// as authoring starting points rather than aliases for the same daytime sky.
|
|
722
|
+
registerSkyPreset('default', {
|
|
723
|
+
description: 'Baseline stylized daytime sky.',
|
|
724
|
+
label: 'Default',
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
registerSkyPreset('call_me_sensei', {
|
|
728
|
+
description: 'Studio-managed signature sky, curated by Call Me Sensei and updated over releases.',
|
|
729
|
+
label: 'Call Me Sensei',
|
|
730
|
+
settings: {
|
|
731
|
+
cloudCoverage: 0.36,
|
|
732
|
+
cloudScale: 1.45,
|
|
733
|
+
cloudSeed: 7,
|
|
734
|
+
cloudSoftness: 0.12,
|
|
735
|
+
horizonScattering: 0.56,
|
|
736
|
+
sunGlowStrength: 1.1,
|
|
737
|
+
zenithColor: [0.24, 0.52, 0.92],
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
registerSkyPreset('clear_day', {
|
|
742
|
+
description: 'Crisp blue daylight with sparse, slow-moving clouds.',
|
|
743
|
+
label: 'Clear Day',
|
|
744
|
+
settings: {
|
|
745
|
+
cloudCoverage: 0.12,
|
|
746
|
+
cloudEdgeOpacity: 0.48,
|
|
747
|
+
cloudScale: 1.9,
|
|
748
|
+
cloudSeed: 12,
|
|
749
|
+
cloudSoftness: 0.075,
|
|
750
|
+
cloudSpeed: 0.65,
|
|
751
|
+
horizonColor: [0.76, 0.93, 1.0],
|
|
752
|
+
horizonScattering: 0.42,
|
|
753
|
+
sunGlowStrength: 1.15,
|
|
754
|
+
zenithColor: [0.2, 0.52, 0.95],
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
registerSkyPreset('golden_hour', {
|
|
759
|
+
description: 'Low warm sun, peach horizon, and softly lit evening clouds.',
|
|
760
|
+
label: 'Golden Hour',
|
|
761
|
+
settings: {
|
|
762
|
+
cloudColor: [1.0, 0.78, 0.6],
|
|
763
|
+
cloudCoverage: 0.34,
|
|
764
|
+
cloudProjection: 0.28,
|
|
765
|
+
cloudSeed: 47,
|
|
766
|
+
cloudShadeColor: [0.58, 0.38, 0.52],
|
|
767
|
+
cloudSilverLiningStrength: 0.62,
|
|
768
|
+
groundColor: [0.3, 0.22, 0.28],
|
|
769
|
+
horizonColor: [1.0, 0.55, 0.28],
|
|
770
|
+
horizonScattering: 0.76,
|
|
771
|
+
starsStrength: 0.04,
|
|
772
|
+
sunColor: [1.0, 0.62, 0.3],
|
|
773
|
+
sunDirection: [0.76, 0.18, 0.36],
|
|
774
|
+
sunGlowStrength: 1.85,
|
|
775
|
+
sunGlowSpread: 3.8,
|
|
776
|
+
sunDiscIntensity: 3.0,
|
|
777
|
+
sunSize: 0.036,
|
|
778
|
+
zenithColor: [0.22, 0.36, 0.68],
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
registerSkyPreset('overcast', {
|
|
783
|
+
description: 'Dense cool cloud cover with broad, low-contrast daylight.',
|
|
784
|
+
label: 'Overcast',
|
|
785
|
+
settings: {
|
|
786
|
+
cloudColor: [0.78, 0.83, 0.9],
|
|
787
|
+
cloudCoverage: 0.88,
|
|
788
|
+
cloudEdgeOpacity: 0.82,
|
|
789
|
+
cloudScale: 1.2,
|
|
790
|
+
cloudSeed: 88,
|
|
791
|
+
cloudShadeColor: [0.42, 0.5, 0.62],
|
|
792
|
+
cloudShadeSoftness: 0.1,
|
|
793
|
+
cloudSoftness: 0.17,
|
|
794
|
+
cloudSpeed: 0.72,
|
|
795
|
+
groundColor: [0.3, 0.35, 0.42],
|
|
796
|
+
horizonColor: [0.62, 0.7, 0.78],
|
|
797
|
+
horizonScattering: 0.72,
|
|
798
|
+
sunColor: [0.82, 0.88, 1.0],
|
|
799
|
+
sunGlowStrength: 0.16,
|
|
800
|
+
sunSize: 0.018,
|
|
801
|
+
zenithColor: [0.38, 0.49, 0.62],
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
registerSkyPreset('moonlit', {
|
|
806
|
+
description: 'Deep blue night with a cool moon glow, quiet clouds, and bright stars.',
|
|
807
|
+
label: 'Moonlit Night',
|
|
808
|
+
settings: {
|
|
809
|
+
cloudColor: [0.2, 0.27, 0.42],
|
|
810
|
+
cloudCoverage: 0.28,
|
|
811
|
+
cloudSeed: 31,
|
|
812
|
+
cloudShadeColor: [0.06, 0.08, 0.17],
|
|
813
|
+
cloudSpeed: 0.35,
|
|
814
|
+
groundColor: [0.015, 0.02, 0.05],
|
|
815
|
+
horizonColor: [0.09, 0.14, 0.25],
|
|
816
|
+
horizonScattering: 0.25,
|
|
817
|
+
starsStrength: 1.1,
|
|
818
|
+
starsColor: [0.72, 0.82, 1.0],
|
|
819
|
+
starsDensity: 0.42,
|
|
820
|
+
starsScale: 18,
|
|
821
|
+
starsSeed: 173,
|
|
822
|
+
starsSize: 0.045,
|
|
823
|
+
starsTwinkleStrength: 0.9,
|
|
824
|
+
sunColor: [0.58, 0.7, 1.0],
|
|
825
|
+
sunDirection: [-0.45, 0.6, 0.3],
|
|
826
|
+
sunGlowStrength: 0.38,
|
|
827
|
+
sunSize: 0.018,
|
|
828
|
+
zenithColor: [0.015, 0.035, 0.11],
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
|
|
313
832
|
function setSrgbColorUniform(uniform, rgb) {
|
|
314
833
|
uniform.value.setRGB(rgb[0], rgb[1], rgb[2], THREE.SRGBColorSpace);
|
|
315
834
|
}
|
|
@@ -319,27 +838,66 @@ export function applySkySettingsToMaterial(material, options = {}) {
|
|
|
319
838
|
// natively, the TSL factory via same-name UniformNodes (`.value` on both).
|
|
320
839
|
const uniforms = material?.uniforms;
|
|
321
840
|
if (!uniforms) return material;
|
|
322
|
-
const
|
|
841
|
+
const source = cleanSkyObject(options);
|
|
842
|
+
const settings = createSkySettings({
|
|
843
|
+
...(source.preset === undefined ? cleanSkyObject(material.userData?.skySettings) : {}),
|
|
844
|
+
...source,
|
|
845
|
+
});
|
|
323
846
|
setSrgbColorUniform(uniforms.uZenithColor, settings.zenithColor);
|
|
324
847
|
setSrgbColorUniform(uniforms.uHorizonColor, settings.horizonColor);
|
|
325
848
|
setSrgbColorUniform(uniforms.uGroundColor, settings.groundColor);
|
|
849
|
+
uniforms.uZenithExponent.value = settings.zenithExponent;
|
|
850
|
+
uniforms.uGroundExponent.value = settings.groundExponent;
|
|
851
|
+
uniforms.uHorizonBandSize.value = settings.horizonBandSize;
|
|
852
|
+
uniforms.uHorizonSunPower.value = settings.horizonSunPower;
|
|
326
853
|
uniforms.uSunDirection.value.set(...settings.sunDirection).normalize();
|
|
327
854
|
setSrgbColorUniform(uniforms.uSunColor, settings.sunColor);
|
|
328
855
|
uniforms.uSunSize.value = settings.sunSize;
|
|
856
|
+
uniforms.uSunDiscSoftness.value = settings.sunDiscSoftness;
|
|
857
|
+
uniforms.uSunDiscIntensity.value = settings.sunDiscIntensity;
|
|
329
858
|
uniforms.uSunGlowStrength.value = settings.sunGlowStrength;
|
|
859
|
+
uniforms.uSunGlowSpread.value = settings.sunGlowSpread;
|
|
860
|
+
uniforms.uSunGlowCoreSharpness.value = settings.sunGlowCoreSharpness;
|
|
861
|
+
uniforms.uSunGlowBroadStrength.value = settings.sunGlowBroadStrength;
|
|
862
|
+
uniforms.uSunGlowCoreStrength.value = settings.sunGlowCoreStrength;
|
|
863
|
+
uniforms.uSunCloudOcclusionStrength.value = settings.sunCloudOcclusionStrength;
|
|
330
864
|
uniforms.uHorizonScattering.value = settings.horizonScattering;
|
|
331
865
|
uniforms.uCloudCoverage.value = settings.cloudCoverage;
|
|
332
866
|
uniforms.uCloudScale.value = settings.cloudScale;
|
|
333
867
|
uniforms.uCloudSpeed.value = settings.cloudSpeed;
|
|
868
|
+
uniforms.uCloudDirection.value.set(...settings.cloudDirection);
|
|
869
|
+
uniforms.uCloudSeed.value = settings.cloudSeed;
|
|
870
|
+
uniforms.uCloudProjection.value = settings.cloudProjection;
|
|
871
|
+
uniforms.uCloudSoftness.value = settings.cloudSoftness;
|
|
872
|
+
uniforms.uCloudEdgeOpacity.value = settings.cloudEdgeOpacity;
|
|
873
|
+
uniforms.uCloudOpacity.value = settings.cloudOpacity;
|
|
874
|
+
uniforms.uCloudShadeStrength.value = settings.cloudShadeStrength;
|
|
875
|
+
uniforms.uCloudShadeThreshold.value = settings.cloudShadeThreshold;
|
|
876
|
+
uniforms.uCloudShadeSoftness.value = settings.cloudShadeSoftness;
|
|
877
|
+
uniforms.uCloudLightOffset.value = settings.cloudLightOffset;
|
|
878
|
+
uniforms.uCloudSilverLiningStrength.value = settings.cloudSilverLiningStrength;
|
|
879
|
+
uniforms.uCloudSunPower.value = settings.cloudSunPower;
|
|
880
|
+
uniforms.uCloudHorizonFade.value = settings.cloudHorizonFade;
|
|
334
881
|
setSrgbColorUniform(uniforms.uCloudColor, settings.cloudColor);
|
|
335
882
|
setSrgbColorUniform(uniforms.uCloudShadeColor, settings.cloudShadeColor);
|
|
336
883
|
uniforms.uStarsStrength.value = settings.starsStrength;
|
|
884
|
+
setSrgbColorUniform(uniforms.uStarsColor, settings.starsColor);
|
|
885
|
+
uniforms.uStarsSeed.value = settings.starsSeed;
|
|
886
|
+
uniforms.uStarsScale.value = settings.starsScale;
|
|
887
|
+
uniforms.uStarsDensity.value = settings.starsDensity;
|
|
888
|
+
uniforms.uStarsSize.value = settings.starsSize;
|
|
889
|
+
uniforms.uStarsTwinkleStrength.value = settings.starsTwinkleStrength;
|
|
890
|
+
uniforms.uStarsTwinkleSpeed.value = settings.starsTwinkleSpeed;
|
|
891
|
+
uniforms.uStarsHorizonFade.value = settings.starsHorizonFade;
|
|
337
892
|
material.userData.skySettings = settings;
|
|
338
893
|
return material;
|
|
339
894
|
}
|
|
340
895
|
|
|
341
896
|
export function createSkyMaterial(options = {}) {
|
|
342
|
-
return applySkySettingsToMaterial(
|
|
897
|
+
return applySkySettingsToMaterial(
|
|
898
|
+
createSkyNodeMaterial({ quality: options?.quality }),
|
|
899
|
+
options,
|
|
900
|
+
);
|
|
343
901
|
}
|
|
344
902
|
|
|
345
903
|
export class StylizedSky extends THREE.Mesh {
|
|
@@ -350,16 +908,75 @@ export class StylizedSky extends THREE.Mesh {
|
|
|
350
908
|
*/
|
|
351
909
|
constructor(options = {}) {
|
|
352
910
|
const settings = createSkySettings(options);
|
|
353
|
-
|
|
911
|
+
const quality = resolveSkyQuality(options?.quality);
|
|
912
|
+
super(
|
|
913
|
+
new THREE.SphereGeometry(settings.radius, 48, 24),
|
|
914
|
+
createSkyMaterial({ ...settings, quality }),
|
|
915
|
+
);
|
|
354
916
|
this.name = 'StylizedSky';
|
|
355
917
|
this.frustumCulled = false;
|
|
356
918
|
this.renderOrder = -100;
|
|
919
|
+
this._authoredSettings = settings;
|
|
920
|
+
this._quality = quality;
|
|
921
|
+
this._retiredMaterials = [];
|
|
922
|
+
this._sceneOverrideLayers = new Map();
|
|
923
|
+
this._sceneOverrideSequence = 0;
|
|
924
|
+
this._sceneOverrides = {};
|
|
357
925
|
}
|
|
358
926
|
|
|
359
927
|
get settings() {
|
|
928
|
+
return this._authoredSettings;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Current compile-time deployment tier; not part of the authored preset. */
|
|
932
|
+
get quality() {
|
|
933
|
+
return this._quality;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** The settings currently uploaded after transient scene overrides. */
|
|
937
|
+
get renderedSettings() {
|
|
360
938
|
return this.material.userData.skySettings;
|
|
361
939
|
}
|
|
362
940
|
|
|
941
|
+
/** Current transient scene overrides, kept separate from authored settings. */
|
|
942
|
+
get sceneOverrides() {
|
|
943
|
+
return structuredClone(this._sceneOverrides);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** Ordered runtime layer metadata, without exposing mutable resolvers. */
|
|
947
|
+
get sceneOverrideLayers() {
|
|
948
|
+
return [...this._sceneOverrideLayers.values()]
|
|
949
|
+
.sort((a, b) => a.priority - b.priority || a.order - b.order)
|
|
950
|
+
.map((layer) => ({ id: layer.id, priority: layer.priority }));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
_composeSceneSettings() {
|
|
954
|
+
let composed = createSkySettings(this._authoredSettings);
|
|
955
|
+
const layers = [...this._sceneOverrideLayers.values()]
|
|
956
|
+
.sort((a, b) => a.priority - b.priority || a.order - b.order);
|
|
957
|
+
for (const layer of layers) {
|
|
958
|
+
const source = layer.resolve
|
|
959
|
+
? layer.resolve(structuredClone(composed))
|
|
960
|
+
: layer.settings;
|
|
961
|
+
const next = collectTopLevelSkySettings(source);
|
|
962
|
+
delete next.radius;
|
|
963
|
+
composed = createSkySettings({ ...composed, ...next });
|
|
964
|
+
}
|
|
965
|
+
return composed;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
_applyComposedSceneSettings() {
|
|
969
|
+
const composed = this._composeSceneSettings();
|
|
970
|
+
this._sceneOverrides = Object.fromEntries(
|
|
971
|
+
Object.keys(SKY_FIELDS_BY_KEY)
|
|
972
|
+
.filter((key) => key !== 'radius')
|
|
973
|
+
.filter((key) => JSON.stringify(composed[key]) !== JSON.stringify(this._authoredSettings[key]))
|
|
974
|
+
.map((key) => [key, composed[key]]),
|
|
975
|
+
);
|
|
976
|
+
applySkySettingsToMaterial(this.material, composed);
|
|
977
|
+
return this.renderedSettings;
|
|
978
|
+
}
|
|
979
|
+
|
|
363
980
|
/**
|
|
364
981
|
* Runtime re-tune: merges `options` into the current settings and pushes
|
|
365
982
|
* every value into the material uniforms. `radius` is construction-only
|
|
@@ -371,10 +988,99 @@ export class StylizedSky extends THREE.Mesh {
|
|
|
371
988
|
* @returns {Object} The updated settings object.
|
|
372
989
|
*/
|
|
373
990
|
applySettings(options = {}) {
|
|
374
|
-
|
|
991
|
+
if (cleanSkyObject(options).preset !== undefined) {
|
|
992
|
+
const { preset, ...overrides } = options;
|
|
993
|
+
return this.setPreset(preset, overrides);
|
|
994
|
+
}
|
|
995
|
+
this._authoredSettings = createSkySettings({ ...this._authoredSettings, ...options });
|
|
996
|
+
this._applyComposedSceneSettings();
|
|
375
997
|
return this.settings;
|
|
376
998
|
}
|
|
377
999
|
|
|
1000
|
+
/** Replaces the authored look from a registered preset, then recomposes runtime layers. */
|
|
1001
|
+
setPreset(name, overrides = {}) {
|
|
1002
|
+
this._authoredSettings = createSkySettings({ preset: name, ...cleanSkyObject(overrides) });
|
|
1003
|
+
this._applyComposedSceneSettings();
|
|
1004
|
+
return this.settings;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Adds or replaces one transient world-state layer. A resolver receives the
|
|
1009
|
+
* result of all lower-priority layers, which lets Weather tint the current
|
|
1010
|
+
* Lighting time-of-day instead of competing with it.
|
|
1011
|
+
*/
|
|
1012
|
+
setSceneOverrideLayer(id, optionsOrResolver = {}, {
|
|
1013
|
+
priority = SKY_SCENE_OVERRIDE_PRIORITIES.scene,
|
|
1014
|
+
replace = true,
|
|
1015
|
+
} = {}) {
|
|
1016
|
+
if ((typeof id !== 'string' || id.length === 0) && typeof id !== 'symbol') {
|
|
1017
|
+
throw new TypeError('A sky scene override layer needs a non-empty string or Symbol id.');
|
|
1018
|
+
}
|
|
1019
|
+
const existing = this._sceneOverrideLayers.get(id);
|
|
1020
|
+
const resolve = typeof optionsOrResolver === 'function' ? optionsOrResolver : null;
|
|
1021
|
+
let settings = null;
|
|
1022
|
+
if (!resolve) {
|
|
1023
|
+
const next = collectTopLevelSkySettings(optionsOrResolver);
|
|
1024
|
+
delete next.radius;
|
|
1025
|
+
settings = !replace && existing?.settings
|
|
1026
|
+
? { ...existing.settings, ...next }
|
|
1027
|
+
: next;
|
|
1028
|
+
}
|
|
1029
|
+
this._sceneOverrideLayers.set(id, {
|
|
1030
|
+
id,
|
|
1031
|
+
order: existing?.order ?? this._sceneOverrideSequence++,
|
|
1032
|
+
priority: Number.isFinite(Number(priority)) ? Number(priority) : SKY_SCENE_OVERRIDE_PRIORITIES.scene,
|
|
1033
|
+
resolve,
|
|
1034
|
+
settings,
|
|
1035
|
+
});
|
|
1036
|
+
return this._applyComposedSceneSettings();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/** Removes one runtime owner without disturbing any other active layer. */
|
|
1040
|
+
clearSceneOverrideLayer(id) {
|
|
1041
|
+
if (!this._sceneOverrideLayers.delete(id)) return this.renderedSettings;
|
|
1042
|
+
return this._applyComposedSceneSettings();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Applies transient Lighting/Weather/world-state inputs without modifying
|
|
1047
|
+
* the authored sky preset returned by {@link settings}.
|
|
1048
|
+
*/
|
|
1049
|
+
setSceneOverrides(options = {}, { replace = false } = {}) {
|
|
1050
|
+
return this.setSceneOverrideLayer('scene', options, {
|
|
1051
|
+
priority: SKY_SCENE_OVERRIDE_PRIORITIES.scene,
|
|
1052
|
+
replace,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Clears only the compatibility/manual `scene` layer. */
|
|
1057
|
+
clearSceneOverrides() {
|
|
1058
|
+
return this.clearSceneOverrideLayer('scene');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** Explicit full teardown for hosts that own every runtime Sky layer. */
|
|
1062
|
+
clearAllSceneOverrideLayers() {
|
|
1063
|
+
this._sceneOverrideLayers.clear();
|
|
1064
|
+
return this._applyComposedSceneSettings();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/** Rebuilds only the material graph for a new deployment-quality tier. */
|
|
1068
|
+
setQuality(value) {
|
|
1069
|
+
const quality = resolveSkyQuality(value);
|
|
1070
|
+
if (quality.cloudOctaves === this._quality.cloudOctaves) return this;
|
|
1071
|
+
const previous = this.material;
|
|
1072
|
+
const elapsed = previous.uniforms?.uTime?.value ?? 0;
|
|
1073
|
+
this._quality = quality;
|
|
1074
|
+
this.material = createSkyMaterial({ ...this.renderedSettings, quality });
|
|
1075
|
+
this.material.uniforms.uTime.value = elapsed;
|
|
1076
|
+
// WebGPU submission is asynchronous. Disposing the old graph here can
|
|
1077
|
+
// destroy binding buffers still referenced by the command buffer already
|
|
1078
|
+
// in flight. Quality changes are rare deployment/lab actions, so retain
|
|
1079
|
+
// replaced graphs and release them with the Sky object at safe teardown.
|
|
1080
|
+
this._retiredMaterials.push(previous);
|
|
1081
|
+
return this;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
378
1084
|
// Advances cloud/star animation and keeps the dome centered on the camera.
|
|
379
1085
|
update(delta, camera) {
|
|
380
1086
|
this.material.uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
|
|
@@ -385,5 +1091,7 @@ export class StylizedSky extends THREE.Mesh {
|
|
|
385
1091
|
dispose() {
|
|
386
1092
|
this.geometry.dispose();
|
|
387
1093
|
this.material.dispose();
|
|
1094
|
+
for (const material of this._retiredMaterials) material.dispose();
|
|
1095
|
+
this._retiredMaterials.length = 0;
|
|
388
1096
|
}
|
|
389
1097
|
}
|