@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
@@ -0,0 +1,360 @@
1
+ // Shared vegetation style nodes. These uniforms describe the IP's rendering
2
+ // treatment; current sun, wind, weather amounts, albedo, and interaction stay
3
+ // in their own namespaces so applying a style cannot overwrite world state.
4
+
5
+ import * as THREE from 'three';
6
+ import {
7
+ abs,
8
+ cameraPosition,
9
+ clamp,
10
+ dot,
11
+ faceDirection,
12
+ mix,
13
+ normalize,
14
+ pow,
15
+ smoothstep,
16
+ uniform,
17
+ vec3,
18
+ } from 'three/tsl';
19
+
20
+ const FALLBACK = Object.freeze({
21
+ lighting: Object.freeze({
22
+ rimPower: 3,
23
+ rimStrength: 0.12,
24
+ shadowTint: Object.freeze([0.36, 0.4, 0.58]),
25
+ shadowTintStrength: 1,
26
+ skyFillStrength: 0.08,
27
+ sunTintStrength: 0.25,
28
+ }),
29
+ thinSurface: Object.freeze({
30
+ diffuseWrap: 0.5,
31
+ normalUpBias: 0,
32
+ transmissionPower: 3.5,
33
+ transmissionShadowFloor: 0.35,
34
+ transmissionStrength: 0.35,
35
+ twoSidedLighting: 1,
36
+ }),
37
+ weatherResponse: Object.freeze({
38
+ snowEdgeSoftness: 0.2,
39
+ snowShadowStrength: 0.65,
40
+ snowTint: Object.freeze([0.92, 0.96, 1]),
41
+ wetDarkening: 0.15,
42
+ wetDesaturation: 0.05,
43
+ wetHighlightStrength: 0.2,
44
+ }),
45
+ grass: Object.freeze({
46
+ backlitStrength: 0.4,
47
+ bandSoftness: 0.1,
48
+ bandThreshold: 0.49,
49
+ bendExponent: 2,
50
+ cloudShadowResponse: 0.35,
51
+ colorVariationStrength: 0.2,
52
+ gustSheenStrength: 0.22,
53
+ gustSheenThreshold: 0.78,
54
+ interactionResponse: 1,
55
+ rootOcclusionHeight: 0.62,
56
+ rootOcclusionStrength: 0.36,
57
+ sceneShadowResponse: 0.7,
58
+ shadowFloor: 0.35,
59
+ tipGradientEnd: 0.95,
60
+ tipGradientStart: 0.1,
61
+ }),
62
+ foliage: Object.freeze({
63
+ backlitStrength: 0.35,
64
+ bandSoftness: 0.18,
65
+ bandThreshold: 0.47,
66
+ cardVariationStrength: 0.16,
67
+ cloudShadowResponse: 0,
68
+ crestSoftness: 0.12,
69
+ crestThreshold: 0.72,
70
+ crownOcclusionStrength: 0.2,
71
+ sceneShadowResponse: 0.55,
72
+ spriteLuminanceStrength: 0.36,
73
+ transmissionPowerMultiplier: 1,
74
+ }),
75
+ flower: Object.freeze({
76
+ backlitStrength: 0.35,
77
+ bandSoftness: 0.1,
78
+ bandThreshold: 0.5,
79
+ centerLightResponse: 0.8,
80
+ centerShadowResponse: 1,
81
+ cupDarkeningStrength: 0.1,
82
+ petalTransmissionMultiplier: 1,
83
+ sceneShadowResponse: 0.85,
84
+ unlitPetalLift: 0.35,
85
+ }),
86
+ bark: Object.freeze({
87
+ bandCount: 3,
88
+ bandSoftness: 0,
89
+ rimStrength: 0,
90
+ shadowFloor: 0.35,
91
+ skyFillStrength: 0.04,
92
+ specularStrength: 0,
93
+ sunTintStrength: 0.15,
94
+ verticalShadeStrength: 0,
95
+ }),
96
+ stem: Object.freeze({
97
+ bandCount: 3,
98
+ bandSoftness: 0.08,
99
+ shadowFloor: 0.42,
100
+ skyFillStrength: 0.06,
101
+ transmissionStrength: 0.08,
102
+ rimStrength: 0.02,
103
+ }),
104
+ });
105
+
106
+ function sourceSettings(profile) {
107
+ return profile?.settings && typeof profile.settings === 'object' ? profile.settings : (profile ?? {});
108
+ }
109
+
110
+ function value(settings, group, key) {
111
+ return settings?.[group]?.[key] ?? FALLBACK[group][key];
112
+ }
113
+
114
+ function color(settings, group, key) {
115
+ const rgb = value(settings, group, key);
116
+ return new THREE.Color().setRGB(rgb[0], rgb[1], rgb[2], THREE.SRGBColorSpace);
117
+ }
118
+
119
+ /** Uniform contract shared by every vegetation material role. */
120
+ export function createVegetationStyleUniforms(profile = {}, role = null) {
121
+ const settings = sourceSettings(profile);
122
+ const roles = new Set((Array.isArray(role) ? role : [role]).filter(Boolean));
123
+ const hasRole = (...candidates) => candidates.some((candidate) => roles.has(candidate));
124
+ const u = {
125
+ uSnowCover: uniform(0),
126
+ uSnowRetention: uniform(1),
127
+ uStyleLightingRimPower: uniform(value(settings, 'lighting', 'rimPower')),
128
+ uStyleLightingRimStrength: uniform(value(settings, 'lighting', 'rimStrength')),
129
+ uStyleLightingShadowTint: uniform(color(settings, 'lighting', 'shadowTint')),
130
+ uStyleLightingShadowTintStrength: uniform(value(settings, 'lighting', 'shadowTintStrength')),
131
+ uStyleLightingSkyFillStrength: uniform(value(settings, 'lighting', 'skyFillStrength')),
132
+ uStyleLightingSunTintStrength: uniform(value(settings, 'lighting', 'sunTintStrength')),
133
+ uStyleWeatherResponseSnowEdgeSoftness: uniform(value(settings, 'weatherResponse', 'snowEdgeSoftness')),
134
+ uStyleWeatherResponseSnowShadowStrength: uniform(value(settings, 'weatherResponse', 'snowShadowStrength')),
135
+ uStyleWeatherResponseSnowTint: uniform(color(settings, 'weatherResponse', 'snowTint')),
136
+ uStyleWeatherResponseWetDarkening: uniform(value(settings, 'weatherResponse', 'wetDarkening')),
137
+ uStyleWeatherResponseWetDesaturation: uniform(value(settings, 'weatherResponse', 'wetDesaturation')),
138
+ uStyleWeatherResponseWetHighlightStrength: uniform(value(settings, 'weatherResponse', 'wetHighlightStrength')),
139
+ uWetness: uniform(0),
140
+ uWetnessResponse: uniform(1),
141
+ };
142
+
143
+ if (hasRole('grassBlade', 'foliageCard', 'flowerPetal')) Object.assign(u, {
144
+ uStyleThinSurfaceDiffuseWrap: uniform(value(settings, 'thinSurface', 'diffuseWrap')),
145
+ uStyleThinSurfaceNormalUpBias: uniform(value(settings, 'thinSurface', 'normalUpBias')),
146
+ uStyleThinSurfaceTransmissionPower: uniform(value(settings, 'thinSurface', 'transmissionPower')),
147
+ uStyleThinSurfaceTransmissionShadowFloor: uniform(value(settings, 'thinSurface', 'transmissionShadowFloor')),
148
+ uStyleThinSurfaceTransmissionStrength: uniform(value(settings, 'thinSurface', 'transmissionStrength')),
149
+ uStyleThinSurfaceTwoSidedLighting: uniform(value(settings, 'thinSurface', 'twoSidedLighting')),
150
+ });
151
+ if (hasRole('grassBlade')) Object.assign(u, {
152
+ uStyleGrassBacklitStrength: uniform(value(settings, 'grass', 'backlitStrength')),
153
+ uStyleGrassBandSoftness: uniform(value(settings, 'grass', 'bandSoftness')),
154
+ uStyleGrassBandThreshold: uniform(value(settings, 'grass', 'bandThreshold')),
155
+ uStyleGrassBendExponent: uniform(value(settings, 'grass', 'bendExponent')),
156
+ uStyleGrassCloudShadowResponse: uniform(value(settings, 'grass', 'cloudShadowResponse')),
157
+ uStyleGrassColorVariationStrength: uniform(value(settings, 'grass', 'colorVariationStrength')),
158
+ uStyleGrassGustSheenStrength: uniform(value(settings, 'grass', 'gustSheenStrength')),
159
+ uStyleGrassGustSheenThreshold: uniform(value(settings, 'grass', 'gustSheenThreshold')),
160
+ uStyleGrassInteractionResponse: uniform(value(settings, 'grass', 'interactionResponse')),
161
+ uStyleGrassRootOcclusionHeight: uniform(value(settings, 'grass', 'rootOcclusionHeight')),
162
+ uStyleGrassRootOcclusionStrength: uniform(value(settings, 'grass', 'rootOcclusionStrength')),
163
+ uStyleGrassSceneShadowResponse: uniform(value(settings, 'grass', 'sceneShadowResponse')),
164
+ uStyleGrassShadowFloor: uniform(value(settings, 'grass', 'shadowFloor')),
165
+ uStyleGrassTipGradientEnd: uniform(value(settings, 'grass', 'tipGradientEnd')),
166
+ uStyleGrassTipGradientStart: uniform(value(settings, 'grass', 'tipGradientStart')),
167
+ });
168
+ if (hasRole('foliageCard')) Object.assign(u, {
169
+ uStyleFoliageBacklitStrength: uniform(value(settings, 'foliage', 'backlitStrength')),
170
+ uStyleFoliageBandSoftness: uniform(value(settings, 'foliage', 'bandSoftness')),
171
+ uStyleFoliageBandThreshold: uniform(value(settings, 'foliage', 'bandThreshold')),
172
+ uStyleFoliageCardVariationStrength: uniform(value(settings, 'foliage', 'cardVariationStrength')),
173
+ uStyleFoliageCloudShadowResponse: uniform(value(settings, 'foliage', 'cloudShadowResponse')),
174
+ uStyleFoliageCrestSoftness: uniform(value(settings, 'foliage', 'crestSoftness')),
175
+ uStyleFoliageCrestThreshold: uniform(value(settings, 'foliage', 'crestThreshold')),
176
+ uStyleFoliageCrownOcclusionStrength: uniform(value(settings, 'foliage', 'crownOcclusionStrength')),
177
+ uStyleFoliageSceneShadowResponse: uniform(value(settings, 'foliage', 'sceneShadowResponse')),
178
+ uStyleFoliageSpriteLuminanceStrength: uniform(value(settings, 'foliage', 'spriteLuminanceStrength')),
179
+ uStyleFoliageTransmissionPowerMultiplier: uniform(value(settings, 'foliage', 'transmissionPowerMultiplier')),
180
+ });
181
+ if (hasRole('flowerPetal', 'flowerCenter')) Object.assign(u, {
182
+ uStyleFlowerBandSoftness: uniform(value(settings, 'flower', 'bandSoftness')),
183
+ uStyleFlowerBandThreshold: uniform(value(settings, 'flower', 'bandThreshold')),
184
+ uStyleFlowerSceneShadowResponse: uniform(value(settings, 'flower', 'sceneShadowResponse')),
185
+ });
186
+ if (hasRole('flowerPetal')) Object.assign(u, {
187
+ uStyleFlowerBacklitStrength: uniform(value(settings, 'flower', 'backlitStrength')),
188
+ uStyleFlowerCupDarkeningStrength: uniform(value(settings, 'flower', 'cupDarkeningStrength')),
189
+ uStyleFlowerPetalTransmissionMultiplier: uniform(value(settings, 'flower', 'petalTransmissionMultiplier')),
190
+ uStyleFlowerUnlitPetalLift: uniform(value(settings, 'flower', 'unlitPetalLift')),
191
+ });
192
+ if (hasRole('flowerCenter')) Object.assign(u, {
193
+ uStyleFlowerCenterLightResponse: uniform(value(settings, 'flower', 'centerLightResponse')),
194
+ uStyleFlowerCenterShadowResponse: uniform(value(settings, 'flower', 'centerShadowResponse')),
195
+ });
196
+ if (hasRole('herbaceousStem')) Object.assign(u, {
197
+ uStyleStemBandCount: uniform(value(settings, 'stem', 'bandCount')),
198
+ uStyleStemBandSoftness: uniform(value(settings, 'stem', 'bandSoftness')),
199
+ uStyleStemShadowFloor: uniform(value(settings, 'stem', 'shadowFloor')),
200
+ uStyleStemRimStrength: uniform(value(settings, 'stem', 'rimStrength')),
201
+ uStyleStemSkyFillStrength: uniform(value(settings, 'stem', 'skyFillStrength')),
202
+ uStyleStemTransmissionStrength: uniform(value(settings, 'stem', 'transmissionStrength')),
203
+ });
204
+ if (hasRole('woodySurface')) Object.assign(u, {
205
+ uStyleBarkBandCount: uniform(value(settings, 'bark', 'bandCount')),
206
+ uStyleBarkBandSoftness: uniform(value(settings, 'bark', 'bandSoftness')),
207
+ uStyleBarkRimStrength: uniform(value(settings, 'bark', 'rimStrength')),
208
+ uStyleBarkShadowFloor: uniform(value(settings, 'bark', 'shadowFloor')),
209
+ uStyleBarkSkyFillStrength: uniform(value(settings, 'bark', 'skyFillStrength')),
210
+ uStyleBarkSpecularStrength: uniform(value(settings, 'bark', 'specularStrength')),
211
+ uStyleBarkSunTintStrength: uniform(value(settings, 'bark', 'sunTintStrength')),
212
+ uStyleBarkVerticalShadeStrength: uniform(value(settings, 'bark', 'verticalShadeStrength')),
213
+ });
214
+ return u;
215
+ }
216
+
217
+ export function vegetationBand(wrap, threshold, softness) {
218
+ const half = softness.mul(0.5);
219
+ return smoothstep(threshold.sub(half), threshold.add(half), wrap);
220
+ }
221
+
222
+ export function vegetationVisibility(
223
+ sceneShadow,
224
+ cloudShadow,
225
+ sceneShadowResponse,
226
+ cloudShadowResponse = 1,
227
+ ) {
228
+ const scene = mix(1.0, sceneShadow, sceneShadowResponse);
229
+ const cloud = mix(1.0, cloudShadow, cloudShadowResponse);
230
+ return scene.mul(cloud);
231
+ }
232
+
233
+ /** Common thin-surface lighting. Family shaders can layer role bands on top. */
234
+ export function shadeVegetationSurface({
235
+ baseColor,
236
+ bandOverride = null,
237
+ bandSoftness,
238
+ bandThreshold,
239
+ cloudShadow,
240
+ u,
241
+ diffuseWrap = u.uStyleThinSurfaceDiffuseWrap,
242
+ normal,
243
+ normalUpBias = u.uStyleThinSurfaceNormalUpBias,
244
+ materialShadowColor = null,
245
+ sceneShadow,
246
+ sceneShadowResponse,
247
+ cloudShadowResponse = 1,
248
+ shadowFloor,
249
+ skyColor,
250
+ sunColor,
251
+ sunDirection,
252
+ rimStrength = u.uStyleLightingRimStrength,
253
+ transmissionMultiplier = 1,
254
+ transmissionPower = u.uStyleThinSurfaceTransmissionPower,
255
+ transmissionShadowFloor = u.uStyleThinSurfaceTransmissionShadowFloor,
256
+ transmissionStrength = u.uStyleThinSurfaceTransmissionStrength,
257
+ twoSidedLighting = u.uStyleThinSurfaceTwoSidedLighting,
258
+ skyFillStrength = u.uStyleLightingSkyFillStrength,
259
+ worldPosition,
260
+ }) {
261
+ const doubleSidedNormal = normal.mul(faceDirection);
262
+ const faceAwareNormal = mix(normal, doubleSidedNormal, twoSidedLighting);
263
+ const styledNormal = normalize(mix(
264
+ faceAwareNormal,
265
+ vec3(0, 1, 0),
266
+ normalUpBias,
267
+ ));
268
+ const sun = normalize(sunDirection);
269
+ const wrap = dot(styledNormal, sun)
270
+ .mul(diffuseWrap.oneMinus())
271
+ .add(diffuseWrap);
272
+ const visibility = vegetationVisibility(
273
+ sceneShadow,
274
+ cloudShadow,
275
+ sceneShadowResponse,
276
+ cloudShadowResponse,
277
+ ).toVar();
278
+ const band = (bandOverride ?? vegetationBand(wrap, bandThreshold, bandSoftness))
279
+ .mul(visibility).toVar();
280
+ const colorNode = baseColor.mul(mix(shadowFloor, 1.0, band)).toVar();
281
+
282
+ // An asset palette may carry its own shadow tone (grass and tree foliage
283
+ // are the common cases). It establishes the material identity in shadow;
284
+ // the IP-wide treatment tint below still layers over every material.
285
+ const materialShadowAmount = visibility.oneMinus();
286
+ if (materialShadowColor) {
287
+ colorNode.assign(mix(
288
+ colorNode,
289
+ materialShadowColor,
290
+ clamp(materialShadowAmount, 0, 1),
291
+ ));
292
+ }
293
+ const shadowAmount = materialShadowAmount.mul(u.uStyleLightingShadowTintStrength);
294
+ colorNode.mulAssign(mix(
295
+ vec3(1),
296
+ u.uStyleLightingShadowTint,
297
+ clamp(shadowAmount, 0, 1),
298
+ ));
299
+ colorNode.mulAssign(mix(vec3(1), sunColor, band.mul(u.uStyleLightingSunTintStrength)));
300
+ colorNode.addAssign(skyColor.mul(band.oneMinus()).mul(skyFillStrength));
301
+
302
+ const viewDirection = normalize(cameraPosition.sub(worldPosition));
303
+ const rim = pow(
304
+ clamp(abs(dot(styledNormal, viewDirection)).oneMinus(), 0, 1),
305
+ u.uStyleLightingRimPower,
306
+ );
307
+ colorNode.addAssign(skyColor.mul(rim).mul(rimStrength));
308
+ const transmission = pow(
309
+ clamp(dot(viewDirection, sun.negate()), 0, 1),
310
+ transmissionPower,
311
+ ).mul(transmissionStrength).mul(transmissionMultiplier);
312
+ colorNode.addAssign(
313
+ sunColor.mul(transmission).mul(mix(
314
+ transmissionShadowFloor,
315
+ 1,
316
+ visibility,
317
+ )),
318
+ );
319
+
320
+ const wet = clamp(u.uWetness.mul(u.uWetnessResponse), 0, 1).toVar();
321
+ colorNode.mulAssign(wet.mul(u.uStyleWeatherResponseWetDarkening).oneMinus());
322
+ const luminance = dot(colorNode, vec3(0.299, 0.587, 0.114));
323
+ colorNode.assign(mix(
324
+ colorNode,
325
+ vec3(luminance),
326
+ wet.mul(u.uStyleWeatherResponseWetDesaturation),
327
+ ));
328
+ colorNode.addAssign(
329
+ sunColor.mul(wet).mul(u.uStyleWeatherResponseWetHighlightStrength)
330
+ .mul(rim.mul(0.5).add(0.08)),
331
+ );
332
+
333
+ const snowSoftness = u.uStyleWeatherResponseSnowEdgeSoftness.max(0.001);
334
+ const snowEdge = snowSoftness.mul(0.5);
335
+ const snowFacing = smoothstep(
336
+ snowSoftness.mul(0.25).sub(snowEdge),
337
+ snowSoftness.mul(0.25).add(snowEdge),
338
+ styledNormal.y,
339
+ );
340
+ const snow = clamp(u.uSnowCover.mul(u.uSnowRetention).mul(snowFacing), 0, 1);
341
+ const snowTint = u.uStyleWeatherResponseSnowTint.mul(mix(
342
+ u.uStyleWeatherResponseSnowShadowStrength,
343
+ 1,
344
+ visibility,
345
+ ));
346
+ colorNode.assign(mix(colorNode, snowTint, snow));
347
+
348
+ return { band, color: colorNode, normal: styledNormal, visibility, viewDirection };
349
+ }
350
+
351
+ export function tagVegetationRole(material, role, variant) {
352
+ const roles = Object.freeze([...new Set((Array.isArray(role) ? role : [role]).filter(Boolean))]);
353
+ material.userData.toonlabVegetation = Object.freeze({
354
+ role: roles[0] ?? '',
355
+ roles,
356
+ variant,
357
+ version: 1,
358
+ });
359
+ return material;
360
+ }
@@ -0,0 +1,31 @@
1
+ // World-anchored persistent shoreline state shared by water foam and wet
2
+ // terrain materials. RGBA = moisture, surface film, active foam, residue.
3
+
4
+ import {
5
+ clamp,
6
+ smoothstep,
7
+ vec2,
8
+ } from 'three/tsl';
9
+
10
+ export function createWaterShoreStateChunk({ u }) {
11
+ const shoreStateUv = (worldXZ) => worldXZ
12
+ .sub(u.uShoreStateRegion.xy)
13
+ .div(u.uShoreStateRegion.zw.mul(2.0))
14
+ .add(0.5);
15
+
16
+ const shoreStateSample = (worldXZ) => {
17
+ const fieldUv = shoreStateUv(worldXZ).toVar();
18
+ // Fade over two percent of the atlas instead of revealing a rectangular
19
+ // edge when a world uses a finite shore-state tile.
20
+ const border = smoothstep(vec2(0.0), vec2(0.02), fieldUv)
21
+ .mul(smoothstep(vec2(0.98), vec2(1.0), fieldUv).oneMinus());
22
+ const mask = border.x.mul(border.y).mul(u.uUseShoreState).toVar();
23
+ return u.uShoreStateMap
24
+ .sample(clamp(fieldUv, vec2(0.0), vec2(1.0)))
25
+ .level(0)
26
+ .mul(mask)
27
+ .toVar();
28
+ };
29
+
30
+ return { shoreStateSample, shoreStateUv };
31
+ }
@@ -13,6 +13,8 @@
13
13
  // The GLSL `i < 2 ? 1.0 : chopWeight` slot filter is a JS-side ternary per
14
14
  // unrolled iteration — slots 0/1 (the dominant swell / set-beat pair) always
15
15
  // pass at full strength, the shorter cross chop fades toward the surf zone.
16
+ // That amplitude filter is independent of nearshore phase applicability:
17
+ // slot 1 only shares q when WaterSurface marks it as an authored beat partner.
16
18
 
17
19
  import {
18
20
  clamp,
@@ -20,6 +22,8 @@ import {
20
22
  cos,
21
23
  dot,
22
24
  float,
25
+ fract,
26
+ mix,
23
27
  normalize,
24
28
  sin,
25
29
  vec3,
@@ -28,9 +32,42 @@ import {
28
32
  export function createWaterWavesChunk({ wavesA, wavesB, waveCount }) {
29
33
  const count = Math.max(1, Math.trunc(waveCount));
30
34
 
35
+ // The primary swell and an eligible set-beat partner can replace their
36
+ // deep-water plane coordinate with a CPU-baked mild-slope phase field.
37
+ // `waveVector` is grad(q), in
38
+ // multiples of the offshore wave number; it therefore supplies both the
39
+ // refracted propagation direction and the shortened shallow wavelength.
40
+ // This is a JS-side branch while the node graph is built, so the six short
41
+ // cross-wave slots keep their original graph and cost.
42
+ const spatialPhaseFrame = (slot, direction, restXZ, nearshore) => {
43
+ const baseCoordinate = dot(direction, restXZ);
44
+ if (!nearshore || slot >= 2) {
45
+ return {
46
+ coordinate: baseCoordinate,
47
+ gradient: direction,
48
+ orbitDirection: direction,
49
+ };
50
+ }
51
+ // slotMask: 0 = disabled, 1 = primary only, 2 = same-direction beat pair.
52
+ // Encoding both applicability bits in the existing attribute channel
53
+ // avoids another varying/uniform and keeps cross-running slot 1 on its
54
+ // authored plane phase when Wave Set Strength is zero.
55
+ const slotMask = nearshore.slotMask ?? float(2.0);
56
+ const slotWeight = slot === 0
57
+ ? clamp(slotMask, 0.0, 1.0)
58
+ : clamp(slotMask.sub(1.0), 0.0, 1.0);
59
+ const blend = nearshore.blend.mul(slotWeight).toVar();
60
+ const gradient = mix(direction, nearshore.waveVector, blend).toVar();
61
+ return {
62
+ coordinate: mix(baseCoordinate, nearshore.phaseCoordinate, blend),
63
+ gradient,
64
+ orbitDirection: normalize(gradient),
65
+ };
66
+ };
67
+
31
68
  // vec3 displacement of the rest position; chopWeight ∈ [0,1] filters the
32
69
  // short cross-running components (1 = full open-water spectrum).
33
- const gerstnerDisplacementFiltered = (restXZ, time, chopWeight) => {
70
+ const gerstnerDisplacementFiltered = (restXZ, time, chopWeight, nearshore = null) => {
34
71
  const displacement = vec3(0.0).toVar();
35
72
  for (let i = 0; i < count; i += 1) {
36
73
  const a = wavesA.element(i).toVar();
@@ -41,20 +78,48 @@ export function createWaterWavesChunk({ wavesA, wavesB, waveCount }) {
41
78
  const amplitude = (i < 2 ? b.x : b.x.mul(chopWeight)).toVar();
42
79
  const phase = b.y;
43
80
  const steepness = b.z;
44
- const theta = waveNumber.mul(dot(direction, restXZ)).sub(omega.mul(time)).add(phase).toVar();
81
+ const phaseFrame = spatialPhaseFrame(i, direction, restXZ, nearshore);
82
+ const theta = waveNumber.mul(phaseFrame.coordinate)
83
+ .sub(omega.mul(time)).add(phase).toVar();
45
84
  const sinTheta = sin(theta);
46
85
  const cosTheta = cos(theta);
47
86
  displacement.y.addAssign(amplitude.mul(sinTheta));
48
- displacement.xz.addAssign(direction.mul(steepness.mul(amplitude).mul(cosTheta)));
87
+ displacement.xz.addAssign(
88
+ phaseFrame.orbitDirection.mul(steepness.mul(amplitude).mul(cosTheta)),
89
+ );
49
90
  }
50
91
  return displacement;
51
92
  };
52
93
 
53
94
  const gerstnerDisplacement = (restXZ, time) => gerstnerDisplacementFiltered(restXZ, time, float(1.0));
54
95
 
96
+ // Vertical signal of the two long swell slots that survive into the surf
97
+ // zone. Swash evaluates this at the projected rest shoreline rather than at
98
+ // every beach vertex, so one arriving swell produces one connected edge.
99
+ const gerstnerSwellHeight = (restXZ, time, nearshore = null) => {
100
+ const height = float(0.0).toVar();
101
+ for (let i = 0; i < Math.min(2, count); i += 1) {
102
+ const a = wavesA.element(i).toVar();
103
+ const b = wavesB.element(i).toVar();
104
+ const phaseFrame = spatialPhaseFrame(i, a.xy, restXZ, nearshore);
105
+ const theta = a.w.mul(phaseFrame.coordinate).sub(a.z.mul(time)).add(b.y).toVar();
106
+ height.addAssign(b.x.mul(sin(theta)));
107
+ }
108
+ return height;
109
+ };
110
+
111
+ // 0..1 cycle of the dominant crest at the rest shoreline. Cycle zero is
112
+ // the instant that crest reaches the shoreline, so the breaker can hand
113
+ // directly into uprush instead of looking like an unrelated animation.
114
+ const primarySwellCycle = (time) => {
115
+ const a = wavesA.element(0).toVar();
116
+ const b = wavesB.element(0).toVar();
117
+ return fract(a.z.mul(time).sub(b.y).add(Math.PI * 0.5).div(Math.PI * 2));
118
+ };
119
+
55
120
  // Analytic surface frame at the rest position plus the 0..1 crest factor
56
121
  // used for whitecap foam (GLSL `out float crest` → returned alongside).
57
- const gerstnerNormalFiltered = (restXZ, time, chopWeight) => {
122
+ const gerstnerNormalFiltered = (restXZ, time, chopWeight, nearshore = null) => {
58
123
  const tangent = vec3(1.0, 0.0, 0.0).toVar();
59
124
  const bitangent = vec3(0.0, 0.0, 1.0).toVar();
60
125
  const crestSum = float(0.0).toVar();
@@ -69,16 +134,28 @@ export function createWaterWavesChunk({ wavesA, wavesB, waveCount }) {
69
134
  const phase = b.y;
70
135
  const steepness = b.z;
71
136
  const crestWeight = b.w;
72
- const theta = waveNumber.mul(dot(direction, restXZ)).sub(omega.mul(time)).add(phase).toVar();
137
+ const phaseFrame = spatialPhaseFrame(i, direction, restXZ, nearshore);
138
+ const theta = waveNumber.mul(phaseFrame.coordinate)
139
+ .sub(omega.mul(time)).add(phase).toVar();
73
140
  const sinTheta = sin(theta).toVar();
74
141
  const cosTheta = cos(theta).toVar();
75
142
  const waveKA = waveNumber.mul(amplitude).toVar();
76
- tangent.x.subAssign(steepness.mul(direction.x).mul(direction.x).mul(waveKA).mul(sinTheta));
77
- tangent.y.addAssign(direction.x.mul(waveKA).mul(cosTheta));
78
- tangent.z.subAssign(steepness.mul(direction.x).mul(direction.y).mul(waveKA).mul(sinTheta));
79
- bitangent.x.subAssign(steepness.mul(direction.x).mul(direction.y).mul(waveKA).mul(sinTheta));
80
- bitangent.y.addAssign(direction.y.mul(waveKA).mul(cosTheta));
81
- bitangent.z.subAssign(steepness.mul(direction.y).mul(direction.y).mul(waveKA).mul(sinTheta));
143
+ const phaseGradient = phaseFrame.gradient;
144
+ const orbitDirection = phaseFrame.orbitDirection;
145
+ tangent.x.subAssign(
146
+ steepness.mul(orbitDirection.x).mul(phaseGradient.x).mul(waveKA).mul(sinTheta),
147
+ );
148
+ tangent.y.addAssign(phaseGradient.x.mul(waveKA).mul(cosTheta));
149
+ tangent.z.subAssign(
150
+ steepness.mul(orbitDirection.y).mul(phaseGradient.x).mul(waveKA).mul(sinTheta),
151
+ );
152
+ bitangent.x.subAssign(
153
+ steepness.mul(orbitDirection.x).mul(phaseGradient.y).mul(waveKA).mul(sinTheta),
154
+ );
155
+ bitangent.y.addAssign(phaseGradient.y.mul(waveKA).mul(cosTheta));
156
+ bitangent.z.subAssign(
157
+ steepness.mul(orbitDirection.y).mul(phaseGradient.y).mul(waveKA).mul(sinTheta),
158
+ );
82
159
  const crestPeak = sinTheta.mul(0.5).add(0.5).toVar();
83
160
  crestSum.addAssign(crestWeight.mul(amplitude).mul(crestPeak).mul(crestPeak));
84
161
  crestTotal.addAssign(crestWeight.mul(amplitude));
@@ -96,5 +173,7 @@ export function createWaterWavesChunk({ wavesA, wavesB, waveCount }) {
96
173
  gerstnerDisplacementFiltered,
97
174
  gerstnerNormal,
98
175
  gerstnerNormalFiltered,
176
+ gerstnerSwellHeight,
177
+ primarySwellCycle,
99
178
  };
100
179
  }
@@ -80,6 +80,7 @@ import { applySaturation, envLuma, windowPaneMask } from './chunks/environment-c
80
80
  import { environmentDebugColor } from './chunks/environment-debug.js';
81
81
  import { createEnvironmentLightingChunk } from './chunks/environment-lighting.js';
82
82
  import { sampleEnvironmentSunShadow } from './chunks/environment-sun-shadow.js';
83
+ import { projectedWaterCaustics } from './chunks/projected-water-caustics.js';
83
84
  import { stylizedCloudShadow } from './chunks/stylized-cloud-shadow.js';
84
85
  import { toonSceneLights } from './chunks/character-scene-lights.js';
85
86
 
@@ -162,6 +163,11 @@ export function createEnvironmentNodeMaterial({
162
163
  lightingInfluence: uniform(hasSun ? 1.0 : 0.36),
163
164
  saturation: uniform(1.12),
164
165
  shadowLift: uniform(hasSun ? 0.0 : 0.66),
166
+ // How much of the CAST sun shadow applies to direct light. 1 = classic
167
+ // full-depth shadows. Worlds whose look runs near-zero ambient (the
168
+ // vivid outdoor presets) crush building-scale shadow masses to black at
169
+ // 1.0 — ~0.7 keeps them as painted mid-tones instead.
170
+ sunShadowStrength: uniform(1.0),
165
171
  skyTintStrength: uniform(0.24),
166
172
  emissiveStrength: uniform(hasSun ? 0.0 : isEmissive ? 0.55 : 0.0),
167
173
  sunBoost: uniform(hasSun ? 0.08 : 0.0),
@@ -526,7 +532,7 @@ export function createEnvironmentNodeMaterial({
526
532
  const sampledSunShadow = sampleEnvironmentSunShadow(vWorldPosition).toVar();
527
533
  const sunlightVisibility = float(1.0).toVar();
528
534
  If(u.enableShadowMask.greaterThan(0.5), () => {
529
- sunlightVisibility.assign(sampledSunShadow);
535
+ sunlightVisibility.assign(mix(1.0, sampledSunShadow, u.sunShadowStrength));
530
536
  });
531
537
  // Drifting procedural cloud shadows over outdoor terrain; strength
532
538
  // defaults to 0 so indoor scenes are untouched.
@@ -688,6 +694,14 @@ export function createEnvironmentNodeMaterial({
688
694
  });
689
695
  }
690
696
 
697
+ // When the camera is below an active WaterSurface, the seabed is drawn
698
+ // directly instead of being seen through the water material. Receive the
699
+ // same moving sunlight here; the shared updater keeps this inert above
700
+ // water, where the surface shader already composites its own caustics.
701
+ color.addAssign(
702
+ projectedWaterCaustics(vWorldPosition, normalWorld).mul(sunlightVisibility),
703
+ );
704
+
691
705
  color.assign(applySaturation(color.mul(u.exposure), u.saturation));
692
706
 
693
707
  // World-height fog: dense near the environment floor, fading with height.