@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.
Files changed (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -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 +739 -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
@@ -1,6 +1,20 @@
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 { createGrassNodeMaterial } from '../shaders-tsl/grass.js';
10
+ import { applyVegetationShader } from './vegetationShaders.js';
11
+
12
+ export {
13
+ GRASS_COLOR_PALETTES,
14
+ applyGrassColorPalette,
15
+ matchGrassColorPalette,
16
+ resolveGrassColorPalette,
17
+ } from './grassPalettes.js';
4
18
 
5
19
  const pushScratch = new THREE.Vector3();
6
20
 
@@ -61,6 +75,7 @@ export const DEFAULT_GRASS_SETTINGS = Object.freeze({
61
75
  cloudShadowStrength: 0,
62
76
  cloudShadowVelocity: Object.freeze([0.02, 0.006]),
63
77
  gustFrequency: 0.35,
78
+ gustResponse: 1,
64
79
  gustSpeed: 1.6,
65
80
  pushRadius: 0.9,
66
81
  shadowStrength: 0.9,
@@ -70,10 +85,17 @@ export const DEFAULT_GRASS_SETTINGS = Object.freeze({
70
85
  sunDirection: Object.freeze([0.35, 0.72, 0.42]),
71
86
  tipColor: Object.freeze([0.74, 0.9, 0.42]),
72
87
  windDirection: Object.freeze([1, 0.3]),
88
+ windResponse: 1,
73
89
  windSpeed: 1.0,
74
90
  windStrength: 0.16,
75
91
  });
76
92
 
93
+ /** Document `type` discriminator for portable grass presets. */
94
+ export const GRASS_PRESET_DOCUMENT_TYPE = 'toonlab/grass-preset';
95
+
96
+ /** Current portable grass preset schema version. */
97
+ export const GRASS_PRESET_SCHEMA_VERSION = 2;
98
+
77
99
  // Named grass presets: 'default' is the baseline; 'call_me_sensei' is the
78
100
  // studio-managed signature look, curated and updated over releases.
79
101
  // Community presets register alongside them via registerGrassPreset().
@@ -89,6 +111,7 @@ const grassPresetRegistry = new Map([
89
111
  settings: Object.freeze({}),
90
112
  })],
91
113
  ]);
114
+ const BUILT_IN_GRASS_PRESET_IDS = new Set(['default', 'call_me_sensei']);
92
115
 
93
116
  /**
94
117
  * Registers a named grass preset so it resolves in `createGrassSettings({
@@ -96,19 +119,17 @@ const grassPresetRegistry = new Map([
96
119
  * settings? }` or flat settings.
97
120
  */
98
121
  export function registerGrassPreset(name, preset = {}, { overwrite = false } = {}) {
99
- const id = String(name ?? '').trim();
100
- if (!id) throw new Error('Grass preset name is required.');
101
- if (!overwrite && grassPresetRegistry.has(id)) {
102
- throw new Error(`Grass preset "${id}" already exists.`);
122
+ const document = createGrassPresetDocument(name, preset);
123
+ if (!overwrite && grassPresetRegistry.has(document.id)) {
124
+ throw new Error(`Grass preset "${document.id}" already exists.`);
103
125
  }
104
- const { label, description, settings, ...flat } = cleanObject(preset);
105
126
  const entry = Object.freeze({
106
- description: typeof description === 'string' ? description : '',
107
- label: typeof label === 'string' && label ? label : id,
108
- settings: Object.freeze({ ...cleanObject(settings ?? flat) }),
127
+ description: document.description,
128
+ label: document.label,
129
+ settings: Object.freeze({ ...document.settings }),
109
130
  });
110
- grassPresetRegistry.set(id, entry);
111
- return { description: entry.description, id, label: entry.label };
131
+ grassPresetRegistry.set(document.id, entry);
132
+ return { description: entry.description, id: document.id, label: entry.label };
112
133
  }
113
134
 
114
135
  /** Lists registered grass presets as `{ id, label, description }` (for HUDs). */
@@ -120,6 +141,13 @@ export function getGrassPresetOptions() {
120
141
  }));
121
142
  }
122
143
 
144
+ /** Removes a registered community/local preset. Built-in preset ids are protected. */
145
+ export function unregisterGrassPreset(name) {
146
+ const id = normalizeGrassPresetId(name);
147
+ if (!id || BUILT_IN_GRASS_PRESET_IDS.has(id)) return false;
148
+ return grassPresetRegistry.delete(id);
149
+ }
150
+
123
151
  /**
124
152
  * Validates, clamps, and merges partial grass options over
125
153
  * {@link DEFAULT_GRASS_SETTINGS}. Unknown keys are ignored; malformed values
@@ -132,7 +160,7 @@ export function getGrassPresetOptions() {
132
160
  * @returns {Object} A complete, plain grass settings object.
133
161
  */
134
162
  export function createGrassSettings(options = {}) {
135
- const source = cleanObject(options);
163
+ const source = typeof options === 'string' ? { preset: options } : cleanObject(options);
136
164
  const presetSettings = grassPresetRegistry.get(source.preset)?.settings;
137
165
  const base = presetSettings ? { ...DEFAULT_GRASS_SETTINGS, ...presetSettings } : DEFAULT_GRASS_SETTINGS;
138
166
  return {
@@ -145,6 +173,7 @@ export function createGrassSettings(options = {}) {
145
173
  cloudShadowStrength: finiteNumber(source.cloudShadowStrength, base.cloudShadowStrength, { min: 0, max: 1 }),
146
174
  cloudShadowVelocity: vectorArray(source.cloudShadowVelocity, base.cloudShadowVelocity, 2),
147
175
  gustFrequency: finiteNumber(source.gustFrequency, base.gustFrequency, { min: 0 }),
176
+ gustResponse: finiteNumber(source.gustResponse, base.gustResponse, { min: 0 }),
148
177
  gustSpeed: finiteNumber(source.gustSpeed, base.gustSpeed, { min: 0 }),
149
178
  pushRadius: finiteNumber(source.pushRadius, base.pushRadius, { min: 0 }),
150
179
  shadowStrength: finiteNumber(source.shadowStrength, base.shadowStrength, { min: 0, max: 1 }),
@@ -154,6 +183,7 @@ export function createGrassSettings(options = {}) {
154
183
  sunDirection: vectorArray(source.sunDirection, base.sunDirection, 3),
155
184
  tipColor: colorArray(source.tipColor, base.tipColor),
156
185
  windDirection: vectorArray(source.windDirection, base.windDirection, 2),
186
+ windResponse: finiteNumber(source.windResponse, base.windResponse, { min: 0 }),
157
187
  windSpeed: finiteNumber(source.windSpeed, base.windSpeed),
158
188
  windStrength: finiteNumber(source.windStrength, base.windStrength, { min: 0 }),
159
189
  };
@@ -171,24 +201,48 @@ export const GRASS_SETTING_GROUPS = Object.freeze([
171
201
  label: 'Blades',
172
202
  }),
173
203
  Object.freeze({
174
- description: 'Per-blade wind sway and the traveling gust bands that ripple across the field.',
204
+ description: 'Asset-level flexibility: how this grass responds when a scene supplies wind and gusts.',
175
205
  id: 'wind',
176
- label: 'Wind',
206
+ label: 'Motion',
207
+ }),
208
+ Object.freeze({
209
+ description: "The blades' coordinated base, tip, and material shadow colors — the grass's identity, whatever the scene lighting does. Magical blue grass welcome.",
210
+ id: 'palette',
211
+ label: 'Palette',
177
212
  }),
178
213
  Object.freeze({
179
- description: 'Blade palette and sun/sky response, including the backlit glow on blades between the camera and the sun.',
214
+ description: 'How the blades RESPOND to scene light — e.g. the backlit glow on blades between the camera and the sun.',
180
215
  id: 'lighting',
181
216
  label: 'Lighting',
182
217
  }),
183
218
  Object.freeze({
184
- description: 'Scene-shadow darkening and the drifting procedural cloud shadows over the field.',
219
+ description: 'Grass-material shadow strength and palette tint. The renderer and cloud-shadow fields themselves come from the scene.',
185
220
  id: 'shadows',
186
221
  label: 'Shadows',
187
222
  }),
188
223
  Object.freeze({
189
- description: 'Character push-away response around the push target.',
224
+ description: 'Current sun direction/color and sky color supplied by the scene at runtime.',
225
+ id: 'sceneLight',
226
+ label: 'Scene Light',
227
+ scene: true,
228
+ }),
229
+ Object.freeze({
230
+ description: 'Current world wind and gust field supplied by weather or another scene system.',
231
+ id: 'sceneWind',
232
+ label: 'Scene Wind',
233
+ scene: true,
234
+ }),
235
+ Object.freeze({
236
+ description: 'Current drifting cloud-shadow field shared across terrain, water, and vegetation.',
237
+ id: 'sceneCloudShadow',
238
+ label: 'Cloud Field',
239
+ scene: true,
240
+ }),
241
+ Object.freeze({
242
+ description: 'Current push target and influence radius supplied per scene or grass instance.',
190
243
  id: 'interaction',
191
244
  label: 'Interaction',
245
+ scene: true,
192
246
  }),
193
247
  ]);
194
248
 
@@ -206,37 +260,51 @@ const GRASS_FIELD_DEFINITIONS = Object.freeze({
206
260
  },
207
261
  },
208
262
  wind: {
263
+ windResponse: {
264
+ description: 'Asset flexibility multiplier applied to the current scene wind strength. 1 preserves the authored baseline; 0 keeps blades still.',
265
+ label: 'Wind Response',
266
+ range: { max: 8, min: 0, step: 0.01 },
267
+ type: 'number',
268
+ },
269
+ gustResponse: {
270
+ description: 'How strongly this grass follows gust bands relative to its regular wind sway.',
271
+ label: 'Gust Response',
272
+ range: { max: 4, min: 0, step: 0.01 },
273
+ type: 'number',
274
+ },
275
+ },
276
+ sceneWind: {
209
277
  windDirection: {
210
- description: 'Horizontal (XZ) heading the wind blows toward. Magnitude does not matter; use wind strength for amplitude.',
278
+ description: 'Current horizontal (XZ) heading the world wind blows toward.',
211
279
  label: 'Wind Direction',
212
280
  type: 'vector2',
213
281
  },
214
282
  windSpeed: {
215
- description: 'How fast the per-blade sway oscillates.',
283
+ description: 'Current temporal speed of the world wind.',
216
284
  label: 'Wind Speed',
217
285
  range: { max: 4, min: 0, step: 0.01 },
218
286
  type: 'number',
219
287
  },
220
288
  windStrength: {
221
- description: 'How far blade tips bend with the wind.',
289
+ description: 'Current world wind amplitude before the asset response multiplier.',
222
290
  label: 'Wind Strength',
223
291
  range: { max: 1, min: 0, step: 0.005 },
224
292
  type: 'number',
225
293
  },
226
294
  gustFrequency: {
227
- description: 'Spatial frequency of the traveling gust bands; higher packs gust waves closer together.',
295
+ description: 'Current spatial frequency of the world gust bands.',
228
296
  label: 'Gust Frequency',
229
297
  range: { max: 2, min: 0, step: 0.01 },
230
298
  type: 'number',
231
299
  },
232
300
  gustSpeed: {
233
- description: 'How fast gust bands travel across the field.',
301
+ description: 'Current travel speed of the world gust bands.',
234
302
  label: 'Gust Speed',
235
303
  range: { max: 6, min: 0, step: 0.01 },
236
304
  type: 'number',
237
305
  },
238
306
  },
239
- lighting: {
307
+ palette: {
240
308
  baseColor: {
241
309
  description: 'Blade color at the root.',
242
310
  label: 'Base Color',
@@ -247,6 +315,16 @@ const GRASS_FIELD_DEFINITIONS = Object.freeze({
247
315
  label: 'Tip Color',
248
316
  type: 'color',
249
317
  },
318
+ },
319
+ lighting: {
320
+ backlitStrength: {
321
+ description: 'Translucent backlight boost when the camera looks toward the sun through the blades.',
322
+ label: 'Backlit Strength',
323
+ range: { max: 2, min: 0, step: 0.01 },
324
+ type: 'number',
325
+ },
326
+ },
327
+ sceneLight: {
250
328
  sunDirection: {
251
329
  description: 'World-space direction toward the sun (normalized on apply). Match your main directional light.',
252
330
  label: 'Sun Direction',
@@ -262,12 +340,6 @@ const GRASS_FIELD_DEFINITIONS = Object.freeze({
262
340
  label: 'Sky Color',
263
341
  type: 'color',
264
342
  },
265
- backlitStrength: {
266
- description: 'Translucent backlight boost when the camera looks toward the sun through the blades.',
267
- label: 'Backlit Strength',
268
- range: { max: 2, min: 0, step: 0.01 },
269
- type: 'number',
270
- },
271
343
  },
272
344
  shadows: {
273
345
  shadowStrength: {
@@ -277,37 +349,39 @@ const GRASS_FIELD_DEFINITIONS = Object.freeze({
277
349
  type: 'number',
278
350
  },
279
351
  shadowTint: {
280
- description: 'Color a fully shadowed blade is multiplied by (cool and dark so grass matches the terrain shadow response).',
352
+ description: 'Grass material color approached in full scene or cloud shadow. Palette presets set it with base/tip colors; the IP-wide vegetation shadow treatment still layers over it.',
281
353
  label: 'Shadow Tint',
282
354
  type: 'color',
283
355
  },
356
+ },
357
+ sceneCloudShadow: {
284
358
  cloudShadowStrength: {
285
- description: 'How strongly drifting procedural cloud shadows darken the field. 0 disables the effect.',
359
+ description: 'Current strength of the shared procedural cloud-shadow field. 0 disables it.',
286
360
  label: 'Cloud Shadow Strength',
287
361
  range: { max: 1, min: 0, step: 0.01 },
288
362
  type: 'number',
289
363
  },
290
364
  cloudShadowCoverage: {
291
- description: 'Fraction of the field covered by cloud shadow at any moment.',
365
+ description: 'Current fraction of the world covered by cloud shadow.',
292
366
  label: 'Cloud Shadow Coverage',
293
367
  range: { max: 1, min: 0, step: 0.01 },
294
368
  type: 'number',
295
369
  },
296
370
  cloudShadowScale: {
297
- description: 'World-to-noise scale of the cloud shadow pattern; smaller values give larger cloud shapes.',
371
+ description: 'Current world-to-noise scale of the shared cloud pattern.',
298
372
  label: 'Cloud Shadow Scale',
299
373
  range: { max: 0.1, min: 0.001, step: 0.001 },
300
374
  type: 'number',
301
375
  },
302
376
  cloudShadowVelocity: {
303
- description: 'Cloud shadow drift in noise-space units per second (world drift = velocity / scale).',
377
+ description: 'Current cloud-shadow drift in noise-space units per second.',
304
378
  label: 'Cloud Shadow Velocity',
305
379
  type: 'vector2',
306
380
  },
307
381
  },
308
382
  interaction: {
309
383
  pushRadius: {
310
- description: 'Radius in meters around the push target within which blades bend away.',
384
+ description: 'Current radius in meters around the scene push target.',
311
385
  label: 'Push Radius',
312
386
  range: { max: 3, min: 0, step: 0.01 },
313
387
  type: 'number',
@@ -327,7 +401,7 @@ function createGrassFieldMetadata(group, key, field) {
327
401
  optionLabels: field.optionLabels ?? null,
328
402
  options: field.options ?? null,
329
403
  range: field.range ?? null,
330
- serializable: field.serializable ?? true,
404
+ serializable: field.serializable ?? !group.scene,
331
405
  type: field.type,
332
406
  });
333
407
  }
@@ -352,6 +426,135 @@ export const GRASS_SETTING_FIELD_SCHEMA = Object.freeze(
352
426
  ),
353
427
  );
354
428
 
429
+ const GRASS_FIELDS_BY_KEY = Object.freeze(Object.fromEntries(
430
+ Object.values(GRASS_SETTING_FIELD_SCHEMA)
431
+ .flatMap((fields) => Object.entries(fields)),
432
+ ));
433
+
434
+ function normalizeGrassPresetId(value) {
435
+ return String(value ?? '').trim();
436
+ }
437
+
438
+ function collectTopLevelGrassSettings(source) {
439
+ const input = cleanObject(source);
440
+ return Object.fromEntries(
441
+ Object.keys(GRASS_FIELDS_BY_KEY)
442
+ .filter((key) => input[key] !== undefined)
443
+ .map((key) => [key, input[key]]),
444
+ );
445
+ }
446
+
447
+ function collectGrassPresetWarnings(settings = {}) {
448
+ const warnings = [];
449
+ for (const key of Object.keys(cleanObject(settings))) {
450
+ const field = GRASS_FIELDS_BY_KEY[key];
451
+ if (!field) warnings.push(`Unknown grass setting "${key}" was ignored.`);
452
+ else if (!field.serializable) {
453
+ warnings.push(`Grass setting "${key}" is scene-owned and was not stored in the preset.`);
454
+ }
455
+ }
456
+ return warnings;
457
+ }
458
+
459
+ /**
460
+ * Normalizes a grass preset into complete JSON-safe product settings. Scene
461
+ * inputs supplied by the active sun/sky rig are deliberately excluded.
462
+ */
463
+ export function sanitizeGrassPresetSettings(settings = {}) {
464
+ const knownSettings = collectTopLevelGrassSettings(settings);
465
+ const normalized = createGrassSettings(knownSettings);
466
+ return Object.fromEntries(
467
+ Object.entries(GRASS_FIELDS_BY_KEY)
468
+ .filter(([, field]) => field.serializable)
469
+ .map(([key]) => [key, normalized[key]]),
470
+ );
471
+ }
472
+
473
+ function migrateGrassPresetDocument(input) {
474
+ const source = cleanObject(input);
475
+ const numericVersion = Number(source.version ?? source.schemaVersion ?? 0);
476
+ const version = Number.isFinite(numericVersion) ? Math.round(numericVersion) : 0;
477
+ if (version > GRASS_PRESET_SCHEMA_VERSION) return { ...source, version };
478
+ const nestedSettings = cleanObject(source.settings);
479
+ const settings = Object.keys(nestedSettings).length > 0
480
+ ? { ...nestedSettings }
481
+ : collectTopLevelGrassSettings(source);
482
+
483
+ // Schema v1 treated windStrength as a portable grass value. Preserve that
484
+ // authored look by converting it to the v2 species response relative to the
485
+ // historical world-wind default. Keep the original key so validation also
486
+ // explains that the live scene field is no longer stored. A v2 document is
487
+ // never reinterpreted this way.
488
+ if (version <= 1 && settings.windResponse === undefined
489
+ && Number.isFinite(Number(settings.windStrength))) {
490
+ settings.windResponse = Math.max(Number(settings.windStrength), 0)
491
+ / DEFAULT_GRASS_SETTINGS.windStrength;
492
+ }
493
+ return {
494
+ description: source.description ?? '',
495
+ id: source.id ?? source.name ?? source.preset ?? '',
496
+ label: source.label ?? source.title ?? source.name ?? source.id ?? '',
497
+ settings,
498
+ type: source.type ?? GRASS_PRESET_DOCUMENT_TYPE,
499
+ version: GRASS_PRESET_SCHEMA_VERSION,
500
+ };
501
+ }
502
+
503
+ /** Validates and normalizes a portable grass preset document. Never throws. */
504
+ export function validateGrassPresetDocument(input) {
505
+ if (cleanObject(input).type !== undefined && input.type !== GRASS_PRESET_DOCUMENT_TYPE) {
506
+ return {
507
+ errors: [`Grass preset type must be "${GRASS_PRESET_DOCUMENT_TYPE}".`],
508
+ ok: false,
509
+ value: null,
510
+ warnings: [],
511
+ };
512
+ }
513
+ return validateSettingsPresetDocument(input, {
514
+ collectWarnings: collectGrassPresetWarnings,
515
+ documentType: GRASS_PRESET_DOCUMENT_TYPE,
516
+ migrateDocument: migrateGrassPresetDocument,
517
+ normalizeId: normalizeGrassPresetId,
518
+ sanitizeSettings: sanitizeGrassPresetSettings,
519
+ schemaVersion: GRASS_PRESET_SCHEMA_VERSION,
520
+ });
521
+ }
522
+
523
+ /** Parses JSON text or an object into a validated grass preset document. */
524
+ export function parseGrassPresetDocument(input) {
525
+ return parsePresetDocument(input, validateGrassPresetDocument, {
526
+ invalidJsonLabel: 'grass preset',
527
+ });
528
+ }
529
+
530
+ /** Creates a canonical, versioned grass preset document. */
531
+ export function createGrassPresetDocument(id, definition = {}) {
532
+ return createSettingsPresetDocument(id, definition, {
533
+ collectSettings: (source) => source.settings ?? collectTopLevelGrassSettings(source),
534
+ documentType: GRASS_PRESET_DOCUMENT_TYPE,
535
+ schemaVersion: GRASS_PRESET_SCHEMA_VERSION,
536
+ validateDocument: validateGrassPresetDocument,
537
+ });
538
+ }
539
+
540
+ /** Serializes a grass preset id/definition or document-like object as JSON. */
541
+ export function serializeGrassPreset(idOrDocument, definition = {}, { pretty = true } = {}) {
542
+ return serializePresetDocument(idOrDocument, definition, {
543
+ argumentCount: arguments.length,
544
+ createDocument: createGrassPresetDocument,
545
+ pretty,
546
+ });
547
+ }
548
+
549
+ /** Registers a portable grass document, overwriting an existing id by default. */
550
+ export function registerSerializedGrassPreset(input, options = {}) {
551
+ const result = parseGrassPresetDocument(input);
552
+ if (!result.ok) throw new Error(result.errors.join(' '));
553
+ return registerGrassPreset(result.value.id, result.value, {
554
+ overwrite: options.overwrite ?? true,
555
+ });
556
+ }
557
+
355
558
  // Dense instanced grass: procedural tapered blades with wind sway and a
356
559
  // push-away radius around a character. One draw call for the whole field;
357
560
  // emitting is a one-time attribute fill, animation is entirely in the vertex
@@ -370,7 +573,7 @@ export const GRASS_SETTING_FIELD_SCHEMA = Object.freeze(
370
573
  // existing callers keep working unchanged.
371
574
  export class StylizedGrassField extends THREE.Mesh {
372
575
  constructor(options = {}) {
373
- const { placements = [] } = cleanObject(options);
576
+ const { placements = [], vegetationShader = null } = cleanObject(options);
374
577
  const settings = createGrassSettings(options);
375
578
  const { bladeHeightRange, bladeWidthRange } = settings;
376
579
 
@@ -401,7 +604,7 @@ export class StylizedGrassField extends THREE.Mesh {
401
604
  geometry.instanceCount = placements.length;
402
605
  geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e5);
403
606
 
404
- const material = createGrassNodeMaterial(settings);
607
+ const material = createGrassNodeMaterial(settings, vegetationShader);
405
608
  setSrgbColor(material.uniforms.uBaseColor.value, settings.baseColor);
406
609
  setSrgbColor(material.uniforms.uTipColor.value, settings.tipColor);
407
610
  setSrgbColor(material.uniforms.uSunColor.value, settings.sunColor);
@@ -418,7 +621,8 @@ export class StylizedGrassField extends THREE.Mesh {
418
621
 
419
622
  /**
420
623
  * Runtime re-tune: merges `options` into the current settings and pushes
421
- * every material-driven value (wind, palette, sun, shadows, push radius)
624
+ * every material-driven value (asset response, current scene fields,
625
+ * palette, sun, shadows, and push radius)
422
626
  * into the uniforms. `bladeHeightRange` / `bladeWidthRange` are baked into
423
627
  * the instance attributes at construction and are construction-only; new
424
628
  * values are stored but do not reshape existing blades.
@@ -441,7 +645,9 @@ export class StylizedGrassField extends THREE.Mesh {
441
645
  uniforms.uWindSpeed.value = settings.windSpeed;
442
646
  uniforms.uWindStrength.value = settings.windStrength;
443
647
  uniforms.uGustFrequency.value = settings.gustFrequency;
648
+ uniforms.uGustResponse.value = settings.gustResponse;
444
649
  uniforms.uGustSpeed.value = settings.gustSpeed;
650
+ uniforms.uWindResponse.value = settings.windResponse;
445
651
  uniforms.uPushRadius.value = settings.pushRadius;
446
652
  uniforms.uBacklitStrength.value = settings.backlitStrength;
447
653
  uniforms.uCloudShadowStrength.value = settings.cloudShadowStrength;
@@ -459,22 +665,26 @@ export class StylizedGrassField extends THREE.Mesh {
459
665
  }
460
666
 
461
667
  setWind({ direction, speed, strength, gustFrequency, gustSpeed } = {}) {
462
- this.applySettings({
463
- gustFrequency,
464
- gustSpeed,
465
- windDirection: direction,
466
- windSpeed: speed,
467
- windStrength: strength,
468
- });
668
+ const uniforms = this.material.uniforms;
669
+ if (direction !== undefined) {
670
+ const next = vectorArray(direction, this.settings.windDirection, 2);
671
+ uniforms.uWindDirection.value.set(next[0], next[1]);
672
+ }
673
+ if (speed !== undefined) uniforms.uWindSpeed.value = finiteNumber(speed, uniforms.uWindSpeed.value);
674
+ if (strength !== undefined) uniforms.uWindStrength.value = finiteNumber(strength, uniforms.uWindStrength.value, { min: 0 });
675
+ if (gustFrequency !== undefined) uniforms.uGustFrequency.value = finiteNumber(gustFrequency, uniforms.uGustFrequency.value, { min: 0 });
676
+ if (gustSpeed !== undefined) uniforms.uGustSpeed.value = finiteNumber(gustSpeed, uniforms.uGustSpeed.value, { min: 0 });
469
677
  return this;
470
678
  }
471
679
 
472
680
  setSun({ direction, color, sky } = {}) {
473
- this.applySettings({
474
- skyColor: sky,
475
- sunColor: color,
476
- sunDirection: direction,
477
- });
681
+ const uniforms = this.material.uniforms;
682
+ if (direction !== undefined) {
683
+ const next = vectorArray(direction, this.settings.sunDirection, 3);
684
+ uniforms.uSunDirection.value.set(...next).normalize();
685
+ }
686
+ if (color !== undefined) setSrgbColor(uniforms.uSunColor.value, colorArray(color, this.settings.sunColor));
687
+ if (sky !== undefined) setSrgbColor(uniforms.uSkyColor.value, colorArray(sky, this.settings.skyColor));
478
688
  return this;
479
689
  }
480
690
 
@@ -491,15 +701,33 @@ export class StylizedGrassField extends THREE.Mesh {
491
701
  // Drifting procedural cloud shadows over the field. strength 0 disables.
492
702
  // velocity is uv-space drift per second (worldDrift = velocity / scale).
493
703
  setCloudShadow({ strength, coverage, scale, velocity } = {}) {
494
- this.applySettings({
495
- cloudShadowCoverage: coverage,
496
- cloudShadowScale: scale,
497
- cloudShadowStrength: strength,
498
- cloudShadowVelocity: velocity,
499
- });
704
+ const uniforms = this.material.uniforms;
705
+ if (strength !== undefined) uniforms.uCloudShadowStrength.value = finiteNumber(strength, uniforms.uCloudShadowStrength.value, { min: 0, max: 1 });
706
+ if (coverage !== undefined) uniforms.uCloudShadowCoverage.value = finiteNumber(coverage, uniforms.uCloudShadowCoverage.value, { min: 0, max: 1 });
707
+ if (scale !== undefined) uniforms.uCloudShadowScale.value = finiteNumber(scale, uniforms.uCloudShadowScale.value, { min: 0.0001 });
708
+ if (velocity !== undefined) {
709
+ const next = vectorArray(velocity, this.settings.cloudShadowVelocity, 2);
710
+ uniforms.uCloudShadowVelocity.value.set(next[0], next[1]);
711
+ }
500
712
  return this;
501
713
  }
502
714
 
715
+ /** Current world surface state. Responses remain owned by the shader/material profile. */
716
+ setSurfaceWeather({ wetness, snowCover } = {}) {
717
+ const uniforms = this.material.uniforms;
718
+ if (uniforms.uWetness && wetness !== undefined) {
719
+ uniforms.uWetness.value = finiteNumber(wetness, uniforms.uWetness.value, { min: 0, max: 1 });
720
+ }
721
+ if (uniforms.uSnowCover && snowCover !== undefined) {
722
+ uniforms.uSnowCover.value = finiteNumber(snowCover, uniforms.uSnowCover.value, { min: 0, max: 1 });
723
+ }
724
+ return this;
725
+ }
726
+
727
+ setVegetationShader(profile) {
728
+ return applyVegetationShader(this, profile);
729
+ }
730
+
503
731
  // Collapse blades between start and end meters from the camera so distant,
504
732
  // fog-swallowed grass stops costing fill rate. Pass nothing to disable.
505
733
  setDistanceFade({ start = 1e6, end } = {}) {
@@ -515,6 +743,13 @@ export class StylizedGrassField extends THREE.Mesh {
515
743
  return this;
516
744
  }
517
745
 
746
+ /** Sets the current scene/instance interaction radius without editing the asset preset. */
747
+ setPushRadius(radius) {
748
+ const uniforms = this.material.uniforms;
749
+ uniforms.uPushRadius.value = finiteNumber(radius, uniforms.uPushRadius.value, { min: 0 });
750
+ return this;
751
+ }
752
+
518
753
  update(delta) {
519
754
  const uniforms = this.material.uniforms;
520
755
  uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
@@ -15,6 +15,11 @@ import {
15
15
  setCanopyWind,
16
16
  tickCanopyTime,
17
17
  } from './stylizedTreeFoliage.js';
18
+ import {
19
+ createWoodySurfaceNodeMaterial,
20
+ setWoodySurfaceSun,
21
+ } from '../shaders-tsl/woody-surface.js';
22
+ import { applyVegetationShader } from './vegetationShaders.js';
18
23
 
19
24
  // Modern anime-style stylized trees, fully parameterized for drop-in use:
20
25
  //
@@ -1980,7 +1985,7 @@ function sameSettingValue(a, b) {
1980
1985
  // skeleton — createTreeSkeleton options (limbsPerBlob, limbRadius, ...)
1981
1986
  // canopy — createTreeFoliageGeometry options (blobs, cardCount, ...)
1982
1987
  // foliage — createTreeFoliageMaterials options (cutoff, wind, sun, ...)
1983
- // trunkMaterial— any THREE material; default is a warm MeshToonMaterial
1988
+ // trunkMaterial— any THREE material; default is a stylized woody material
1984
1989
  export class StylizedTree extends THREE.Group {
1985
1990
  constructor(options = {}) {
1986
1991
  super();
@@ -2009,6 +2014,7 @@ export class StylizedTree extends THREE.Group {
2009
2014
  // collar (a complete tree meets the ground).
2010
2015
  const {
2011
2016
  trunkMaterial = null,
2017
+ vegetationShader = null,
2012
2018
  branchSpines = [],
2013
2019
  extraBlobs = [],
2014
2020
  extraAttachments = [],
@@ -2373,12 +2379,18 @@ export class StylizedTree extends THREE.Group {
2373
2379
  ? { leafMap: leafSpriteForShape(leafShape) }
2374
2380
  : {}),
2375
2381
  ...settings.foliage,
2382
+ vegetationShader,
2376
2383
  });
2377
2384
 
2378
2385
  this.trunkMesh = new THREE.Mesh(
2379
2386
  trunkGeometry,
2380
- trunkMaterial ?? new THREE.MeshToonMaterial({ color: 0xc9ab8a }),
2387
+ trunkMaterial ?? createWoodySurfaceNodeMaterial({
2388
+ color: 0xc9ab8a,
2389
+ height: settings.trunk.height,
2390
+ vegetationShader,
2391
+ }),
2381
2392
  );
2393
+ this.trunkMesh.userData.toonlabVegetationRole = 'woodySurface';
2382
2394
  this.trunkMesh.castShadow = true;
2383
2395
  this.trunkMesh.receiveShadow = trunkReceiveShadow;
2384
2396
 
@@ -2449,6 +2461,11 @@ export class StylizedTree extends THREE.Group {
2449
2461
  uniforms.uCloudShadowScale.value = foliage.cloudShadowScale;
2450
2462
  uniforms.uCloudShadowVelocity.value.set(
2451
2463
  foliage.cloudShadowVelocity[0], foliage.cloudShadowVelocity[1]);
2464
+ setWoodySurfaceSun(this.trunkMesh.material, {
2465
+ color: foliage.sunColor,
2466
+ direction: foliage.sunDirection,
2467
+ sky: foliage.skyColor,
2468
+ });
2452
2469
 
2453
2470
  // Re-derive the three-tone palette from the (possibly updated) canopy
2454
2471
  // color/palette pins. Constructor-only foliage.color/palette overrides
@@ -2470,6 +2487,7 @@ export class StylizedTree extends THREE.Group {
2470
2487
 
2471
2488
  setSun(options) {
2472
2489
  setCanopySun(this.canopyMesh.material.uniforms, options ?? {});
2490
+ setWoodySurfaceSun(this.trunkMesh.material, options ?? {});
2473
2491
  return this;
2474
2492
  }
2475
2493
 
@@ -2490,6 +2508,24 @@ export class StylizedTree extends THREE.Group {
2490
2508
  return this;
2491
2509
  }
2492
2510
 
2511
+ /** Current world surface state; material response coefficients stay in the vegetation shader profile. */
2512
+ setSurfaceWeather({ wetness, snowCover } = {}) {
2513
+ for (const material of [this.canopyMesh?.material, this.trunkMesh?.material]) {
2514
+ const uniforms = material?.uniforms;
2515
+ if (uniforms?.uWetness && wetness !== undefined) {
2516
+ uniforms.uWetness.value = THREE.MathUtils.clamp(Number(wetness) || 0, 0, 1);
2517
+ }
2518
+ if (uniforms?.uSnowCover && snowCover !== undefined) {
2519
+ uniforms.uSnowCover.value = THREE.MathUtils.clamp(Number(snowCover) || 0, 0, 1);
2520
+ }
2521
+ }
2522
+ return this;
2523
+ }
2524
+
2525
+ setVegetationShader(profile) {
2526
+ return applyVegetationShader(this, profile);
2527
+ }
2528
+
2493
2529
  update(delta) {
2494
2530
  tickCanopyTime(this.canopyMesh.material.uniforms, delta);
2495
2531
  return this;