@call-me-sensei/toonlab 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +231 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +752 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -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 }` shared across terrain, water, grass, and trees.
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, grass,
86
- * flowerField, trees, treeGroup, masks, worldPreset, classification }`
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 = { strength: 0.35 },
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
- const direction = Array.isArray(skySunDirection)
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(waterOptions.width) || width,
283
- depth: Number(waterOptions.depth) || depth,
469
+ width: Number(configuredWidth) || width,
470
+ depth: Number(configuredDepth) || depth,
284
471
  ...(heightAt ? { bedHeight: heightAt } : {}),
285
- ...cleanObject(waterOptions.settings),
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 under water.
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
- const vegetationMask = masks ? combineMasks(masks.slope, masks.water) : null;
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
- const collision = createWorldCollision({ heightAt });
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 (cloudShadows !== false) applyCloudShadow(cleanObject(cloudShadows));
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
- for (const object of [sky, waterSurface, forest, grassField, flowerField, sunRig?.group ?? sunRig]) {
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);