@call-me-sensei/toonlab 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +231 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +752 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
package/src/stylizedWorld.js
CHANGED
|
@@ -13,6 +13,10 @@ import { StylizedSky } from './sky/stylizedSky.js';
|
|
|
13
13
|
import { StylizedGrassField } from './vegetation/stylizedGrass.js';
|
|
14
14
|
import { StylizedFlowerField } from './vegetation/stylizedFlowers.js';
|
|
15
15
|
import { StylizedForest } from './vegetation/stylizedForest.js';
|
|
16
|
+
import {
|
|
17
|
+
applyVegetationShader,
|
|
18
|
+
createVegetationShaderSettings,
|
|
19
|
+
} from './vegetation/vegetationShaders.js';
|
|
16
20
|
import {
|
|
17
21
|
combineMasks,
|
|
18
22
|
createSlopeMask,
|
|
@@ -22,6 +26,13 @@ import {
|
|
|
22
26
|
} from './vegetation/scatter.js';
|
|
23
27
|
import { resolveWorldPreset } from './worldPresets.js';
|
|
24
28
|
import { createWorldCollision } from './worldCollision.js';
|
|
29
|
+
import { createStylizedPaths } from './pathgen/stylizedPaths.js';
|
|
30
|
+
import { connectPointsOfInterest } from './pathgen/pathRouter.js';
|
|
31
|
+
import { createStylizedVillage } from './villagegen/stylizedVillage.js';
|
|
32
|
+
import { pickPoiSites } from './villagegen/villageSites.js';
|
|
33
|
+
import { createFauna } from './fauna/index.js';
|
|
34
|
+
import { createAmbientFx } from './ambientfx/index.js';
|
|
35
|
+
import { createWeatherSystem } from './weather/index.js';
|
|
25
36
|
|
|
26
37
|
// The composed golden path: one call assembles a complete stylized outdoor
|
|
27
38
|
// world — environment-shaded terrain, sun rig, sky dome, interactive water,
|
|
@@ -54,6 +65,15 @@ function cleanObject(value) {
|
|
|
54
65
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
function sunColorArray(value, fallback) {
|
|
69
|
+
if (value?.isColor) return [value.r, value.g, value.b];
|
|
70
|
+
if (Array.isArray(value) && value.length >= 3) {
|
|
71
|
+
const next = value.slice(0, 3).map(Number);
|
|
72
|
+
if (next.every(Number.isFinite)) return next;
|
|
73
|
+
}
|
|
74
|
+
return fallback.slice();
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
/**
|
|
58
78
|
* Assembles a stylized outdoor world from a world preset.
|
|
59
79
|
*
|
|
@@ -70,20 +90,33 @@ function cleanObject(value) {
|
|
|
70
90
|
* @param {string} [options.preset] World preset name (default 'outdoorGameplay').
|
|
71
91
|
* @param {Object|false} [options.water] `{ level, width, depth, settings }`,
|
|
72
92
|
* or `false` to skip water.
|
|
93
|
+
* @param {Object|false} [options.paths] A network from `createStylizedPaths`
|
|
94
|
+
* OR options for one (`{ seed, auto, routes, settings }`). Wired fully:
|
|
95
|
+
* ribbons join the environment shading + fog, the on-path mask parts
|
|
96
|
+
* grass/flowers/trees, bridge rails register collision, and
|
|
97
|
+
* `collision.groundHeight` follows the flattened `paths.heightAt`.
|
|
73
98
|
* @param {Object|false} [options.trees] `{ scatter, settings, center }`
|
|
74
99
|
* overrides over the world preset, or `false` to skip.
|
|
75
100
|
* @param {Object|false} [options.grass] Same shape as trees, or `false`.
|
|
76
101
|
* @param {Object|false} [options.flowers] `{ scatter, settings }`; off unless given.
|
|
102
|
+
* @param {Object|string} [options.vegetationShader] One IP-wide vegetation
|
|
103
|
+
* shader profile/settings object. Albedo, construction, and live world
|
|
104
|
+
* values remain owned by trees/grass/flowers and the host scene.
|
|
77
105
|
* @param {Object3D} [options.followTarget] Character root: water ripple
|
|
78
106
|
* window + interactor splashes/wakes, grass push-away.
|
|
79
107
|
* @param {Object|false} [options.cloudShadows] `{ strength, coverage, scale,
|
|
80
|
-
* velocity }`
|
|
108
|
+
* velocity }` legacy override merged into the weather cloud field.
|
|
109
|
+
* @param {Object|false} [options.weather] `{ preset, settings, seed }` builds
|
|
110
|
+
* the shared weather coordinator. `false` keeps the legacy static cloud
|
|
111
|
+
* shadow path. Defaults to the world preset's Call Me Sensei weather.
|
|
81
112
|
* @param {boolean} [options.sun] Build the environment sun rig (default true).
|
|
82
113
|
* @param {boolean} [options.applyCamera] Apply preset camera planes (default true).
|
|
83
114
|
* @param {Object} [options.environment] Extra `applyEnvironmentShader`
|
|
84
115
|
* options merged last (features/parameters/roleOverrides/...).
|
|
85
|
-
* @returns {Promise<Object>} `{ update, dispose, sky, water, sunRig,
|
|
86
|
-
*
|
|
116
|
+
* @returns {Promise<Object>} `{ update, dispose, sky, water, sunRig, sunState,
|
|
117
|
+
* setSun, grass, flowerField, forest, masks, worldPreset, classification }`.
|
|
118
|
+
* `setSun({ direction, color, sky })` is the transient scene-light adapter
|
|
119
|
+
* used by Lighting and Weather; it does not rewrite portable asset presets.
|
|
87
120
|
*/
|
|
88
121
|
export async function createStylizedWorld({
|
|
89
122
|
renderer,
|
|
@@ -92,14 +125,20 @@ export async function createStylizedWorld({
|
|
|
92
125
|
terrain,
|
|
93
126
|
preset = 'outdoorGameplay',
|
|
94
127
|
water = {},
|
|
128
|
+
paths = false,
|
|
129
|
+
pois = false,
|
|
130
|
+
fauna = false,
|
|
131
|
+
ambientfx = false,
|
|
132
|
+
weather = {},
|
|
95
133
|
sky: skyOptions = {},
|
|
96
134
|
fog = {},
|
|
97
135
|
shadows = {},
|
|
98
136
|
trees = {},
|
|
99
137
|
grass = {},
|
|
100
138
|
flowers = false,
|
|
139
|
+
vegetationShader = {},
|
|
101
140
|
followTarget = null,
|
|
102
|
-
cloudShadows =
|
|
141
|
+
cloudShadows = null,
|
|
103
142
|
sun = true,
|
|
104
143
|
applyCamera = true,
|
|
105
144
|
environment = {},
|
|
@@ -111,6 +150,7 @@ export async function createStylizedWorld({
|
|
|
111
150
|
|
|
112
151
|
const worldPreset = resolveWorldPreset(preset);
|
|
113
152
|
if (!worldPreset) throw new Error(`Unknown world preset "${preset}".`);
|
|
153
|
+
let vegetationShaderSettings = createVegetationShaderSettings(vegetationShader);
|
|
114
154
|
|
|
115
155
|
// terrain.size accepts a number (square), { width, depth }, or { x, z }
|
|
116
156
|
// (the shape createStylizedTerrain returns as meshExtent) — silently
|
|
@@ -128,6 +168,104 @@ export async function createStylizedWorld({
|
|
|
128
168
|
camera.updateProjectionMatrix();
|
|
129
169
|
}
|
|
130
170
|
|
|
171
|
+
// POIs before paths, paths before the environment conversion. Villages
|
|
172
|
+
// contribute their street centerlines as explicit routes, and their
|
|
173
|
+
// entries become router nodes — roads between settlements come free.
|
|
174
|
+
const earlyWaterLevel = water !== false ? Number(cleanObject(water).level) || 0 : 0;
|
|
175
|
+
let poiList = [];
|
|
176
|
+
const poiRoutes = [];
|
|
177
|
+
if (pois && heightAt) {
|
|
178
|
+
const poiOptions = cleanObject(pois);
|
|
179
|
+
const poiSeed = Math.round(Number(poiOptions.seed) || 1);
|
|
180
|
+
const requests = [];
|
|
181
|
+
for (const [key, archetype] of [
|
|
182
|
+
['villages', 'village'],
|
|
183
|
+
['shrines', 'shrine'],
|
|
184
|
+
['ruins', 'ruin'],
|
|
185
|
+
['campsites', 'campsite'],
|
|
186
|
+
['pierHamlets', 'pierHamlet'],
|
|
187
|
+
]) {
|
|
188
|
+
const count = Math.max(Math.round(Number(poiOptions[key]) || 0), 0);
|
|
189
|
+
if (count > 0) requests.push({ archetype, count });
|
|
190
|
+
}
|
|
191
|
+
// Hosts with decorative rims beyond the playable area pass pois.size to
|
|
192
|
+
// keep settlements reachable.
|
|
193
|
+
const poiSize = poiOptions.size ?? { x: width, z: depth };
|
|
194
|
+
const sites = pickPoiSites({
|
|
195
|
+
heightAt,
|
|
196
|
+
requests,
|
|
197
|
+
seed: poiSeed,
|
|
198
|
+
size: poiSize,
|
|
199
|
+
waterLevel: earlyWaterLevel,
|
|
200
|
+
});
|
|
201
|
+
poiList = sites.map((site) => {
|
|
202
|
+
const village = createStylizedVillage({
|
|
203
|
+
archetype: site.archetype,
|
|
204
|
+
center: { x: site.x, z: site.z },
|
|
205
|
+
heightAt,
|
|
206
|
+
parent: terrainRoot,
|
|
207
|
+
radius: site.radius,
|
|
208
|
+
seed: site.seed,
|
|
209
|
+
waterLevel: earlyWaterLevel,
|
|
210
|
+
});
|
|
211
|
+
poiRoutes.push(...village.streetRoutes);
|
|
212
|
+
return village;
|
|
213
|
+
});
|
|
214
|
+
// Connect POI entries into a road network (MST + occasional loop).
|
|
215
|
+
const entryNodes = poiList.map((poi) => ({
|
|
216
|
+
x: (poi.entries[0].x + poi.entries[1].x) / 2,
|
|
217
|
+
z: (poi.entries[0].z + poi.entries[1].z) / 2,
|
|
218
|
+
}));
|
|
219
|
+
if (entryNodes.length >= 2) {
|
|
220
|
+
const edges = connectPointsOfInterest(entryNodes, { loopChance: 0.3, seed: poiSeed });
|
|
221
|
+
for (const [fromIndex, toIndex] of edges) {
|
|
222
|
+
const fromPoi = poiList[fromIndex];
|
|
223
|
+
const toPoi = poiList[toIndex];
|
|
224
|
+
// route from the nearest entry of one place to the nearest of the other
|
|
225
|
+
let bestPair = null;
|
|
226
|
+
for (const from of fromPoi.entries) {
|
|
227
|
+
for (const to of toPoi.entries) {
|
|
228
|
+
const distance = (from.x - to.x) ** 2 + (from.z - to.z) ** 2;
|
|
229
|
+
if (!bestPair || distance < bestPair.distance) bestPair = { distance, from, to };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
poiRoutes.push({
|
|
233
|
+
from: [bestPair.from.x, bestPair.from.z],
|
|
234
|
+
style: 'dirt',
|
|
235
|
+
to: [bestPair.to.x, bestPair.to.z],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Path network before the environment conversion, so ribbons and bridges
|
|
242
|
+
// parented under the terrain root get shaded (and join the height fog)
|
|
243
|
+
// exactly like the terrain itself. Accepts a prebuilt network or builds
|
|
244
|
+
// one here from the terrain contract.
|
|
245
|
+
let pathsSystem = null;
|
|
246
|
+
let ownsPaths = false;
|
|
247
|
+
if (paths || poiRoutes.length > 0) {
|
|
248
|
+
if (paths && typeof paths.maskAt === 'function' && paths.root) {
|
|
249
|
+
pathsSystem = paths;
|
|
250
|
+
} else if (heightAt) {
|
|
251
|
+
const pathOptions = cleanObject(paths);
|
|
252
|
+
const userRoutes = Array.isArray(pathOptions.routes) ? pathOptions.routes : [];
|
|
253
|
+
const hasExplicit = userRoutes.length > 0 || poiRoutes.length > 0;
|
|
254
|
+
pathsSystem = createStylizedPaths({
|
|
255
|
+
auto: pathOptions.auto
|
|
256
|
+
?? (hasExplicit || !paths ? null : { count: pathOptions.count ?? 4, styles: pathOptions.styles ?? ['dirt'] }),
|
|
257
|
+
heightAt,
|
|
258
|
+
routes: [...poiRoutes, ...userRoutes],
|
|
259
|
+
seed: pathOptions.seed ?? 1,
|
|
260
|
+
settings: pathOptions.settings ?? {},
|
|
261
|
+
size: { x: width, z: depth },
|
|
262
|
+
waterLevel: earlyWaterLevel,
|
|
263
|
+
});
|
|
264
|
+
ownsPaths = true;
|
|
265
|
+
}
|
|
266
|
+
if (pathsSystem?.root && !pathsSystem.root.parent) terrainRoot.add(pathsSystem.root);
|
|
267
|
+
}
|
|
268
|
+
|
|
131
269
|
// Environment shading over the terrain (and anything parented under it).
|
|
132
270
|
const environmentBox = new THREE.Box3().setFromObject(terrainRoot);
|
|
133
271
|
const environmentPreset = resolveEnvironmentPreset(worldPreset.environment?.preset ?? 'default') ?? {};
|
|
@@ -192,6 +330,15 @@ export async function createStylizedWorld({
|
|
|
192
330
|
} : {}),
|
|
193
331
|
})
|
|
194
332
|
: null;
|
|
333
|
+
const sunDirectionState = Array.isArray(skySunDirection)
|
|
334
|
+
? new THREE.Vector3(...skySunDirection)
|
|
335
|
+
: (sunRig?.light
|
|
336
|
+
? sunRig.light.position.clone().sub(sunRig.light.target.position)
|
|
337
|
+
: new THREE.Vector3(0.35, 0.8, 0.45));
|
|
338
|
+
if (sunDirectionState.lengthSq() < 1e-8) sunDirectionState.set(0.35, 0.8, 0.45);
|
|
339
|
+
sunDirectionState.normalize();
|
|
340
|
+
const sunColorState = [1, 0.96, 0.84];
|
|
341
|
+
const sunSkyColorState = [0.62, 0.78, 0.95];
|
|
195
342
|
|
|
196
343
|
// Shadows are ON by default — a character that casts nothing on the grass
|
|
197
344
|
// is the fastest way for a stylized world to read fake. The renderer's
|
|
@@ -233,14 +380,7 @@ export async function createStylizedWorld({
|
|
|
233
380
|
// elevation by the terrain box's aspect ratio — a 2 km × 200 m box
|
|
234
381
|
// squashes a 2 PM sun nearly horizontal, and shadows land tens of
|
|
235
382
|
// meters away from their casters.)
|
|
236
|
-
|
|
237
|
-
? new THREE.Vector3(
|
|
238
|
-
skySunDirection[0],
|
|
239
|
-
Math.max(skySunDirection[1], 0.15),
|
|
240
|
-
skySunDirection[2],
|
|
241
|
-
)
|
|
242
|
-
: light.position.clone().sub(environmentBox.getCenter(new THREE.Vector3()));
|
|
243
|
-
sunShadowOffset = direction.normalize().multiplyScalar(shadowArea * 3 + 120);
|
|
383
|
+
sunShadowOffset = sunDirectionState.clone().multiplyScalar(shadowArea * 3 + 120);
|
|
244
384
|
scene.add(light.target);
|
|
245
385
|
}
|
|
246
386
|
}
|
|
@@ -264,6 +404,46 @@ export async function createStylizedWorld({
|
|
|
264
404
|
};
|
|
265
405
|
followSunShadow();
|
|
266
406
|
|
|
407
|
+
// One scene-input adapter keeps the actual DirectionalLight, shadow follow,
|
|
408
|
+
// and every custom vegetation shader on the same sun/sky values. These are
|
|
409
|
+
// transient scene values: authored Grass/Flower/Tree settings stay intact.
|
|
410
|
+
const setSun = ({ direction, color, sky: skyColor } = {}) => {
|
|
411
|
+
if (direction !== undefined) {
|
|
412
|
+
const channels = direction?.isVector3
|
|
413
|
+
? [direction.x, direction.y, direction.z]
|
|
414
|
+
: direction;
|
|
415
|
+
if (Array.isArray(channels) && channels.length >= 3) {
|
|
416
|
+
const next = channels.slice(0, 3).map(Number);
|
|
417
|
+
if (next.every(Number.isFinite)) sunDirectionState.fromArray(next);
|
|
418
|
+
}
|
|
419
|
+
if (sunDirectionState.lengthSq() < 1e-8) sunDirectionState.set(0.35, 0.8, 0.45);
|
|
420
|
+
sunDirectionState.normalize();
|
|
421
|
+
}
|
|
422
|
+
if (color !== undefined) sunColorState.splice(0, 3, ...sunColorArray(color, sunColorState));
|
|
423
|
+
if (skyColor !== undefined) sunSkyColorState.splice(0, 3, ...sunColorArray(skyColor, sunSkyColorState));
|
|
424
|
+
|
|
425
|
+
// A direction is not an environment-relative point. The rig owns a true
|
|
426
|
+
// direction adapter so non-square worlds and shadows:false remain aligned.
|
|
427
|
+
sunRig?.setDirection?.(sunDirectionState);
|
|
428
|
+
sunRig?.setState?.({ color: new THREE.Color().setRGB(...sunColorState) });
|
|
429
|
+
if (sunShadowOffset) {
|
|
430
|
+
sunShadowOffset.copy(sunDirectionState).multiplyScalar(shadowArea * 3 + 120);
|
|
431
|
+
followSunShadow();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const current = {
|
|
435
|
+
color: sunColorState.slice(),
|
|
436
|
+
direction: sunDirectionState.toArray(),
|
|
437
|
+
sky: sunSkyColorState.slice(),
|
|
438
|
+
};
|
|
439
|
+
grassField?.setSun?.(current);
|
|
440
|
+
flowerField?.setSun?.(current);
|
|
441
|
+
forest?.setSun?.(current);
|
|
442
|
+
ambientFx?.setSun?.({ direction: current.direction });
|
|
443
|
+
return current;
|
|
444
|
+
};
|
|
445
|
+
const setSunDirection = (value) => setSun({ direction: value }).direction;
|
|
446
|
+
|
|
267
447
|
// Sky dome — the water's reflection fallback, so it comes before water.
|
|
268
448
|
const sky = new StylizedSky({
|
|
269
449
|
preset: worldPreset.sky?.preset,
|
|
@@ -277,12 +457,23 @@ export async function createStylizedWorld({
|
|
|
277
457
|
let waterSurface = null;
|
|
278
458
|
if (water !== false) {
|
|
279
459
|
const waterOptions = cleanObject(water);
|
|
460
|
+
const {
|
|
461
|
+
depth: configuredDepth,
|
|
462
|
+
level: _waterLevel,
|
|
463
|
+
settings: configuredWaterSettings,
|
|
464
|
+
width: configuredWidth,
|
|
465
|
+
...waterRuntimeOptions
|
|
466
|
+
} = waterOptions;
|
|
280
467
|
waterSurface = new WaterSurface({
|
|
281
468
|
preset: worldPreset.water?.preset,
|
|
282
|
-
width: Number(
|
|
283
|
-
depth: Number(
|
|
469
|
+
width: Number(configuredWidth) || width,
|
|
470
|
+
depth: Number(configuredDepth) || depth,
|
|
284
471
|
...(heightAt ? { bedHeight: heightAt } : {}),
|
|
285
|
-
|
|
472
|
+
// Runtime stages (passes/simulation/splashes/currentField/etc.) are
|
|
473
|
+
// valid composed-world options too; previously only material settings
|
|
474
|
+
// made it through this wrapper.
|
|
475
|
+
...waterRuntimeOptions,
|
|
476
|
+
...cleanObject(configuredWaterSettings),
|
|
286
477
|
});
|
|
287
478
|
waterSurface.position.y = waterLevel;
|
|
288
479
|
// Match the environment's height fog so far-shore water hazes exactly
|
|
@@ -299,14 +490,50 @@ export async function createStylizedWorld({
|
|
|
299
490
|
}
|
|
300
491
|
}
|
|
301
492
|
|
|
302
|
-
// Placement masks: no vegetation on cliffs or
|
|
493
|
+
// Placement masks: no vegetation on cliffs, under water, or on paths.
|
|
303
494
|
const masks = heightAt
|
|
304
495
|
? {
|
|
305
496
|
slope: createSlopeMask({ heightAt, maxSlope: 0.55 }),
|
|
306
497
|
water: createWaterMask({ heightAt, margin: 0.4, waterLevel }),
|
|
307
498
|
}
|
|
308
499
|
: null;
|
|
309
|
-
|
|
500
|
+
if (masks && pathsSystem) {
|
|
501
|
+
// Grass/flowers/trees part around the network instead of growing
|
|
502
|
+
// through the road surface. The low threshold clears a shoulder wide
|
|
503
|
+
// enough that meter-tall blades can't lean over the ribbon.
|
|
504
|
+
masks.paths = (x, z) => pathsSystem.maskAt(x, z) <= 0.12;
|
|
505
|
+
}
|
|
506
|
+
if (masks && poiList.length > 0) {
|
|
507
|
+
// No trees through roofs: a coarse spatial hash over every POI blocker
|
|
508
|
+
// circle (buildings, wells, fences) keeps scatter out of settlements.
|
|
509
|
+
const cell = 8;
|
|
510
|
+
const buckets = new Map();
|
|
511
|
+
for (const poi of poiList) {
|
|
512
|
+
for (const circle of poi.blockers) {
|
|
513
|
+
const pad = circle.radius + 1.2;
|
|
514
|
+
for (let ix = Math.floor((circle.x - pad) / cell); ix <= Math.floor((circle.x + pad) / cell); ix += 1) {
|
|
515
|
+
for (let iz = Math.floor((circle.z - pad) / cell); iz <= Math.floor((circle.z + pad) / cell); iz += 1) {
|
|
516
|
+
const key = `${ix},${iz}`;
|
|
517
|
+
const bucket = buckets.get(key);
|
|
518
|
+
if (bucket) bucket.push(circle);
|
|
519
|
+
else buckets.set(key, [circle]);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
masks.pois = (x, z) => {
|
|
525
|
+
const bucket = buckets.get(`${Math.floor(x / cell)},${Math.floor(z / cell)}`);
|
|
526
|
+
if (!bucket) return true;
|
|
527
|
+
for (const circle of bucket) {
|
|
528
|
+
const pad = circle.radius + 1.2;
|
|
529
|
+
if ((circle.x - x) ** 2 + (circle.z - z) ** 2 < pad * pad) return false;
|
|
530
|
+
}
|
|
531
|
+
return true;
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const vegetationMask = masks
|
|
535
|
+
? combineMasks(masks.slope, masks.water, masks.paths, masks.pois)
|
|
536
|
+
: (pathsSystem ? (x, z) => pathsSystem.maskAt(x, z) <= 0.12 : null);
|
|
310
537
|
const defaultCenter = followTarget?.position ?? { x: 0, z: 0 };
|
|
311
538
|
// Per-system custom masks (e.g. a createNoisePatchMask forest-cluster
|
|
312
539
|
// mask) AND with the built-in slope/water masks.
|
|
@@ -338,6 +565,7 @@ export async function createStylizedWorld({
|
|
|
338
565
|
// merged-geometry impostors (~12k verts/tree) that get redrawn by the
|
|
339
566
|
// main, water, and shadow passes — the difference between 20 and 60 fps.
|
|
340
567
|
renderer,
|
|
568
|
+
vegetationShader: vegetationShaderSettings,
|
|
341
569
|
settings: {
|
|
342
570
|
...cleanObject(worldPreset.trees?.settings),
|
|
343
571
|
...cleanObject(treeOptions.settings),
|
|
@@ -362,6 +590,7 @@ export async function createStylizedWorld({
|
|
|
362
590
|
// the window edge. Far grass simply doesn't exist — same as the anime
|
|
363
591
|
// games this emulates.
|
|
364
592
|
let grassField = null;
|
|
593
|
+
let weatherSystem = null;
|
|
365
594
|
const grassState = { center: null, options: cleanObject(grass), radius: 0 };
|
|
366
595
|
const buildGrassAt = (center) => {
|
|
367
596
|
const grassOptions = grassState.options;
|
|
@@ -381,6 +610,7 @@ export async function createStylizedWorld({
|
|
|
381
610
|
preset: worldPreset.grass?.preset,
|
|
382
611
|
...cleanObject(worldPreset.grass?.settings),
|
|
383
612
|
...cleanObject(grassOptions.settings),
|
|
613
|
+
vegetationShader: vegetationShaderSettings,
|
|
384
614
|
placements,
|
|
385
615
|
});
|
|
386
616
|
field.setDistanceFade({ end: grassState.radius * 0.98, start: grassState.radius * 0.62 });
|
|
@@ -407,6 +637,7 @@ export async function createStylizedWorld({
|
|
|
407
637
|
if (previous && grassField !== previous) {
|
|
408
638
|
scene.remove(previous);
|
|
409
639
|
previous.dispose();
|
|
640
|
+
weatherSystem?.refresh();
|
|
410
641
|
}
|
|
411
642
|
};
|
|
412
643
|
|
|
@@ -425,18 +656,88 @@ export async function createStylizedWorld({
|
|
|
425
656
|
if (placements.length > 0) {
|
|
426
657
|
flowerField = new StylizedFlowerField({
|
|
427
658
|
...cleanObject(flowerOptions.settings),
|
|
659
|
+
vegetationShader: vegetationShaderSettings,
|
|
428
660
|
placements,
|
|
429
661
|
});
|
|
430
662
|
scene.add(flowerField);
|
|
431
663
|
}
|
|
432
664
|
}
|
|
433
665
|
|
|
666
|
+
// The living layer: instanced GPU-animated ambient creatures, staggered
|
|
667
|
+
// steering, budgets as populations. Off unless asked for.
|
|
668
|
+
let faunaSystem = null;
|
|
669
|
+
if (fauna && heightAt) {
|
|
670
|
+
const faunaOptions = cleanObject(fauna);
|
|
671
|
+
faunaSystem = createFauna({
|
|
672
|
+
bounds: { x: width / 2 - 10, z: depth / 2 - 10 },
|
|
673
|
+
followTarget,
|
|
674
|
+
heightAt,
|
|
675
|
+
masks: { flowers: faunaOptions.flowerMask ?? vegetationMask ?? null },
|
|
676
|
+
preset: faunaOptions.preset ?? null,
|
|
677
|
+
seed: faunaOptions.seed ?? 1,
|
|
678
|
+
settings: faunaOptions.settings ?? {},
|
|
679
|
+
species: faunaOptions.species ?? {},
|
|
680
|
+
waterLevel,
|
|
681
|
+
});
|
|
682
|
+
// Same height-fog layer terrain/water/forest get — birds that skip it
|
|
683
|
+
// stay sharp saturated specks on hazed mountains.
|
|
684
|
+
faunaSystem.setDistanceFog({
|
|
685
|
+
color: heightFogParams.heightFogColor ?? [0.66, 0.8, 0.94],
|
|
686
|
+
density: Number(heightFogParams.heightFogDensity) || 0.0016,
|
|
687
|
+
falloff: Number(heightFogParams.heightFogFalloff) || 400,
|
|
688
|
+
floorY: environmentBox.min.y,
|
|
689
|
+
});
|
|
690
|
+
scene.add(faunaSystem.root);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Ambient atmosphere: petals/leaves/fireflies/pollen/mist over one GPU
|
|
694
|
+
// particle backbone, in a follow window like the grass. One wind for the
|
|
695
|
+
// whole world.
|
|
696
|
+
let ambientFx = null;
|
|
697
|
+
if (ambientfx) {
|
|
698
|
+
const fxOptions = cleanObject(ambientfx);
|
|
699
|
+
ambientFx = createAmbientFx({
|
|
700
|
+
followTarget,
|
|
701
|
+
heightAt,
|
|
702
|
+
seed: Number(fxOptions.seed) || 1,
|
|
703
|
+
waterLevel: waterSurface ? waterLevel : null,
|
|
704
|
+
...fxOptions,
|
|
705
|
+
});
|
|
706
|
+
const grassWind = grassField?.settings ?? {};
|
|
707
|
+
ambientFx.setWind({
|
|
708
|
+
windDirection: grassWind.windDirection,
|
|
709
|
+
windSpeed: grassWind.windSpeed,
|
|
710
|
+
windStrength: grassWind.windStrength,
|
|
711
|
+
});
|
|
712
|
+
ambientFx.setDistanceFog({
|
|
713
|
+
color: heightFogParams.heightFogColor ?? [0.66, 0.8, 0.94],
|
|
714
|
+
density: Number(heightFogParams.heightFogDensity) || 0.0016,
|
|
715
|
+
falloff: Number(heightFogParams.heightFogFalloff) || 400,
|
|
716
|
+
floorY: environmentBox.min.y,
|
|
717
|
+
});
|
|
718
|
+
if (Array.isArray(skySunDirection)) ambientFx.setSun({ direction: skySunDirection });
|
|
719
|
+
scene.add(ambientFx.root);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Align all scene consumers with the authored Sky before Weather or an
|
|
723
|
+
// optional LightingSystem adds transient layers.
|
|
724
|
+
setSun({
|
|
725
|
+
color: sky.renderedSettings?.sunColor ?? sky.settings?.sunColor,
|
|
726
|
+
direction: sunDirectionState,
|
|
727
|
+
sky: sky.renderedSettings?.horizonColor ?? sky.settings?.horizonColor,
|
|
728
|
+
});
|
|
729
|
+
|
|
434
730
|
// Walkability: circle blockers on a spatial hash, tree trunks
|
|
435
731
|
// pre-registered, so characters can't walk through the world ToonLab just
|
|
436
732
|
// built. Hosts add their own blockers (rocks, props) via
|
|
437
733
|
// `world.collision.addCircles(...)` and call
|
|
438
734
|
// `world.collision.resolve(character.position, radius)` per frame.
|
|
439
|
-
|
|
735
|
+
// Ground truth follows the flattened path profile where one exists, so
|
|
736
|
+
// characters walk the road surface (and bridge decks) instead of the raw
|
|
737
|
+
// terrain underneath.
|
|
738
|
+
const collision = createWorldCollision({ heightAt: pathsSystem?.heightAt ?? heightAt });
|
|
739
|
+
if (pathsSystem?.blockers?.length) collision.addCircles(pathsSystem.blockers);
|
|
740
|
+
for (const poi of poiList) collision.addCircles(poi.blockers);
|
|
440
741
|
if (forest) {
|
|
441
742
|
const trunkScale = [
|
|
442
743
|
cleanObject(cleanObject(trees).settings).size,
|
|
@@ -452,9 +753,65 @@ export async function createStylizedWorld({
|
|
|
452
753
|
setEnvironmentCloudShadow(field);
|
|
453
754
|
waterSurface?.setCloudShadow(field);
|
|
454
755
|
grassField?.setCloudShadow(field);
|
|
756
|
+
flowerField?.setCloudShadow(field);
|
|
455
757
|
forest?.setCloudShadow(field);
|
|
758
|
+
faunaSystem?.setCloudShadow?.(field);
|
|
456
759
|
};
|
|
457
|
-
if (
|
|
760
|
+
if (weather === false) {
|
|
761
|
+
if (cloudShadows !== false) applyCloudShadow(
|
|
762
|
+
cloudShadows === null ? { strength: 0.35 } : cleanObject(cloudShadows),
|
|
763
|
+
);
|
|
764
|
+
} else {
|
|
765
|
+
const weatherOptions = cleanObject(weather);
|
|
766
|
+
const presetWeatherSettings = cleanObject(worldPreset.weather?.settings);
|
|
767
|
+
const optionWeatherSettings = cleanObject(weatherOptions.settings);
|
|
768
|
+
const weatherSettings = {
|
|
769
|
+
...presetWeatherSettings,
|
|
770
|
+
...optionWeatherSettings,
|
|
771
|
+
atmosphere: {
|
|
772
|
+
...cleanObject(presetWeatherSettings.atmosphere),
|
|
773
|
+
...cleanObject(optionWeatherSettings.atmosphere),
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
const legacyCloud = cloudShadows === false
|
|
777
|
+
? { strength: 0 }
|
|
778
|
+
: (cloudShadows === null ? {} : cleanObject(cloudShadows));
|
|
779
|
+
weatherSystem = createWeatherSystem({
|
|
780
|
+
ambientFx,
|
|
781
|
+
camera,
|
|
782
|
+
environmentRoot: terrainRoot,
|
|
783
|
+
fauna: faunaSystem,
|
|
784
|
+
flowers: flowerField,
|
|
785
|
+
followTarget,
|
|
786
|
+
forest: () => forest,
|
|
787
|
+
getSun: () => ({
|
|
788
|
+
color: sunColorState.slice(),
|
|
789
|
+
direction: sunDirectionState.toArray(),
|
|
790
|
+
sky: sunSkyColorState.slice(),
|
|
791
|
+
}),
|
|
792
|
+
grass: () => grassField,
|
|
793
|
+
groundHeightAt: heightAt,
|
|
794
|
+
precipitationFloorY: environmentBox.min.y,
|
|
795
|
+
preset: weatherOptions.preset ?? worldPreset.weather?.preset ?? 'call_me_sensei',
|
|
796
|
+
renderer,
|
|
797
|
+
scene,
|
|
798
|
+
seed: Number(weatherOptions.seed) || 1,
|
|
799
|
+
setCloudShadow: applyCloudShadow,
|
|
800
|
+
setSun,
|
|
801
|
+
settings: {
|
|
802
|
+
...weatherSettings,
|
|
803
|
+
atmosphere: {
|
|
804
|
+
...cleanObject(weatherSettings.atmosphere),
|
|
805
|
+
...(Number.isFinite(legacyCloud.strength) ? { cloudShadowStrength: legacyCloud.strength } : {}),
|
|
806
|
+
...(Number.isFinite(legacyCloud.coverage) ? { cloudShadowCoverage: legacyCloud.coverage } : {}),
|
|
807
|
+
...(Number.isFinite(legacyCloud.scale) ? { cloudShadowScale: legacyCloud.scale } : {}),
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
sky,
|
|
811
|
+
sunRig,
|
|
812
|
+
water: waterSurface,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
458
815
|
|
|
459
816
|
let disposed = false;
|
|
460
817
|
return {
|
|
@@ -465,18 +822,60 @@ export async function createStylizedWorld({
|
|
|
465
822
|
disposed = true;
|
|
466
823
|
sunShadowPass?.dispose();
|
|
467
824
|
forest?.dispose();
|
|
468
|
-
|
|
825
|
+
if (ownsPaths) pathsSystem?.dispose();
|
|
826
|
+
for (const poi of poiList) poi.dispose();
|
|
827
|
+
faunaSystem?.dispose();
|
|
828
|
+
ambientFx?.dispose();
|
|
829
|
+
weatherSystem?.dispose();
|
|
830
|
+
for (const object of [sky, waterSurface, forest, grassField, flowerField,
|
|
831
|
+
faunaSystem?.root, ambientFx?.root, sunRig?.group ?? sunRig]) {
|
|
469
832
|
if (object?.parent) object.parent.remove(object);
|
|
470
833
|
}
|
|
471
834
|
},
|
|
472
835
|
flowerField,
|
|
473
836
|
fog: sceneFog,
|
|
474
837
|
forest,
|
|
838
|
+
environmentRoot: terrainRoot,
|
|
475
839
|
get grass() { return grassField; },
|
|
840
|
+
ambientFx,
|
|
841
|
+
fauna: faunaSystem,
|
|
476
842
|
masks,
|
|
843
|
+
paths: pathsSystem,
|
|
844
|
+
pois: poiList,
|
|
477
845
|
setCloudShadow: applyCloudShadow,
|
|
846
|
+
setSun,
|
|
847
|
+
setSunDirection,
|
|
848
|
+
setVegetationShader(profile) {
|
|
849
|
+
vegetationShaderSettings = createVegetationShaderSettings(profile);
|
|
850
|
+
const reports = [
|
|
851
|
+
applyVegetationShader(grassField, vegetationShaderSettings),
|
|
852
|
+
applyVegetationShader(flowerField, vegetationShaderSettings),
|
|
853
|
+
forest?.setVegetationShader?.(vegetationShaderSettings)
|
|
854
|
+
?? applyVegetationShader(forest, vegetationShaderSettings),
|
|
855
|
+
];
|
|
856
|
+
return {
|
|
857
|
+
applied: reports.reduce((sum, report) => sum + (report?.applied ?? 0), 0),
|
|
858
|
+
reports,
|
|
859
|
+
requiresForestImpostorRebake: Boolean(reports[2]?.requiresImpostorRebake),
|
|
860
|
+
settings: vegetationShaderSettings,
|
|
861
|
+
unsupported: reports.flatMap((report) => report?.unsupported ?? []),
|
|
862
|
+
writes: reports.reduce((sum, report) => sum + (report?.writes ?? 0), 0),
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
setWeather(presetOrSettings, options) {
|
|
866
|
+
return weatherSystem?.transitionTo(presetOrSettings, options) ?? null;
|
|
867
|
+
},
|
|
478
868
|
sky,
|
|
869
|
+
get sunState() {
|
|
870
|
+
return {
|
|
871
|
+
color: sunColorState.slice(),
|
|
872
|
+
direction: sunDirectionState.toArray(),
|
|
873
|
+
sky: sunSkyColorState.slice(),
|
|
874
|
+
};
|
|
875
|
+
},
|
|
876
|
+
get sunDirection() { return sunDirectionState.toArray(); },
|
|
479
877
|
sunRig,
|
|
878
|
+
weather: weatherSystem,
|
|
480
879
|
/** Call once per frame before rendering. */
|
|
481
880
|
update(delta = 0.016) {
|
|
482
881
|
if (disposed) return;
|
|
@@ -488,6 +887,10 @@ export async function createStylizedWorld({
|
|
|
488
887
|
shadowFrame += 1;
|
|
489
888
|
if (sunShadowPass && shadowFrame % shadowInterval === 0) sunShadowPass.update();
|
|
490
889
|
refreshGrassWindow();
|
|
890
|
+
weatherSystem?.update(delta);
|
|
891
|
+
for (const poi of poiList) poi.update(delta, camera);
|
|
892
|
+
faunaSystem?.update(delta);
|
|
893
|
+
ambientFx?.update(delta, camera);
|
|
491
894
|
waterSurface?.update(renderer, scene, camera, delta);
|
|
492
895
|
sky.update(delta, camera);
|
|
493
896
|
grassField?.update(delta);
|