@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,395 @@
1
+ // Canonical settings and field metadata for the parametric prop generators.
2
+ // Same texture-free discipline as debrisgen: geometry + baked vertex color
3
+ // + ToonLab's environment shader produce the final read. Every prop is
4
+ // seeded, palette-driven, and rebuildable from a recipe document forever.
5
+
6
+ export const PROP_TYPES = Object.freeze({
7
+ fence: Object.freeze({
8
+ label: 'Fence',
9
+ description: 'Post-and-rail runs that follow terrain. Linear: built along a spline, not instanced.',
10
+ icon: '🚧',
11
+ linear: true,
12
+ variants: Object.freeze([
13
+ Object.freeze({ id: 'ranch', label: 'Ranch rails' }),
14
+ Object.freeze({ id: 'log', label: 'Log rail' }),
15
+ Object.freeze({ id: 'rope', label: 'Rope & posts' }),
16
+ Object.freeze({ id: 'picket', label: 'Picket' }),
17
+ ]),
18
+ }),
19
+ lantern: Object.freeze({
20
+ label: 'Lantern',
21
+ description: 'Stone tōrō pedestals, hanging chōchin, and wooden post lamps.',
22
+ icon: '🏮',
23
+ variants: Object.freeze([
24
+ Object.freeze({ id: 'stoneToro', label: 'Stone tōrō' }),
25
+ Object.freeze({ id: 'chochin', label: 'Hanging chōchin' }),
26
+ Object.freeze({ id: 'woodPost', label: 'Wood post lamp' }),
27
+ ]),
28
+ }),
29
+ signpost: Object.freeze({
30
+ label: 'Signpost',
31
+ description: 'Direction boards on a post — junction dressing.',
32
+ icon: '🪧',
33
+ variants: Object.freeze([
34
+ Object.freeze({ id: 'single', label: 'Single board' }),
35
+ Object.freeze({ id: 'double', label: 'Two boards' }),
36
+ Object.freeze({ id: 'crossroads', label: 'Crossroads' }),
37
+ ]),
38
+ }),
39
+ stoneStairs: Object.freeze({
40
+ label: 'Stone stairs',
41
+ description: 'Free-standing stone step runs for gardens and shrine approaches.',
42
+ icon: '🪜',
43
+ variants: Object.freeze([
44
+ Object.freeze({ id: 'straight', label: 'Straight' }),
45
+ Object.freeze({ id: 'worn', label: 'Worn & uneven' }),
46
+ ]),
47
+ }),
48
+ milestone: Object.freeze({
49
+ label: 'Milestone',
50
+ description: 'Way markers: road stones, small obelisks, mossy jizō markers.',
51
+ icon: '🗿',
52
+ variants: Object.freeze([
53
+ Object.freeze({ id: 'roadStone', label: 'Road stone' }),
54
+ Object.freeze({ id: 'obelisk', label: 'Obelisk' }),
55
+ Object.freeze({ id: 'jizo', label: 'Jizō marker' }),
56
+ ]),
57
+ }),
58
+ well: Object.freeze({
59
+ label: 'Well',
60
+ description: 'Village stone well, optionally roofed with a bucket winch.',
61
+ icon: '🕳️',
62
+ variants: Object.freeze([
63
+ Object.freeze({ id: 'roofed', label: 'Roofed' }),
64
+ Object.freeze({ id: 'open', label: 'Open ring' }),
65
+ ]),
66
+ }),
67
+ crateStack: Object.freeze({
68
+ label: 'Crates & barrels',
69
+ description: 'Market and dock clutter: crate stacks, barrel groups, sack piles.',
70
+ icon: '📦',
71
+ variants: Object.freeze([
72
+ Object.freeze({ id: 'crates', label: 'Crates' }),
73
+ Object.freeze({ id: 'barrels', label: 'Barrels' }),
74
+ Object.freeze({ id: 'mixed', label: 'Mixed' }),
75
+ ]),
76
+ }),
77
+ firewood: Object.freeze({
78
+ label: 'Firewood pile',
79
+ description: 'Stacked split logs against winter.',
80
+ icon: '🪵',
81
+ variants: Object.freeze([
82
+ Object.freeze({ id: 'stacked', label: 'Neat stack' }),
83
+ Object.freeze({ id: 'loose', label: 'Loose pile' }),
84
+ ]),
85
+ }),
86
+ torii: Object.freeze({
87
+ label: 'Torii gate',
88
+ description: 'Shrine gates: curved myōjin or straight shinmei.',
89
+ icon: '⛩️',
90
+ variants: Object.freeze([
91
+ Object.freeze({ id: 'myojin', label: 'Myōjin (curved)' }),
92
+ Object.freeze({ id: 'shinmei', label: 'Shinmei (straight)' }),
93
+ ]),
94
+ }),
95
+ pier: Object.freeze({
96
+ label: 'Pier',
97
+ description: 'Plank dock on posts, reaching from the shore over water.',
98
+ icon: '🛶',
99
+ variants: Object.freeze([
100
+ Object.freeze({ id: 'straight', label: 'Straight' }),
101
+ Object.freeze({ id: 'tShape', label: 'T-head' }),
102
+ ]),
103
+ }),
104
+ stoneWall: Object.freeze({
105
+ label: 'Stone wall',
106
+ description: 'Dry-stacked field walls. Linear: built along a spline.',
107
+ icon: '🧱',
108
+ linear: true,
109
+ variants: Object.freeze([
110
+ Object.freeze({ id: 'dry', label: 'Dry stack' }),
111
+ Object.freeze({ id: 'dressed', label: 'Dressed courses' }),
112
+ ]),
113
+ }),
114
+ bench: Object.freeze({
115
+ label: 'Bench',
116
+ description: 'Plank or split-log seating.',
117
+ icon: '🪑',
118
+ variants: Object.freeze([
119
+ Object.freeze({ id: 'plank', label: 'Plank bench' }),
120
+ Object.freeze({ id: 'log', label: 'Split log' }),
121
+ ]),
122
+ }),
123
+ });
124
+
125
+ const field = (key, label, min, max, step, caption) => Object.freeze({
126
+ caption, key, label, max, min, step,
127
+ });
128
+
129
+ // Per-type shape fields (the sliders each generator understands).
130
+ export const PROP_TYPE_FIELDS = Object.freeze({
131
+ fence: Object.freeze([
132
+ field('postSpacing', 'Post spacing', 1.2, 4, 0.1, 'Meters between posts along the run.'),
133
+ field('postHeight', 'Post height', 0.6, 1.6, 0.05, 'Height of each post above ground.'),
134
+ field('thickness', 'Thickness', 0.05, 0.18, 0.005, 'Post and rail cross-section.'),
135
+ field('railCount', 'Rails', 1, 3, 1, 'Horizontal rails between posts.'),
136
+ field('sag', 'Sag', 0, 1, 0.05, 'Rope droop / rail bow between posts.'),
137
+ field('lean', 'Lean', 0, 1, 0.05, 'Random post lean — weathered fences lean.'),
138
+ field('gapChance', 'Gaps', 0, 0.5, 0.05, 'Chance a rail span is missing (broken-down look).'),
139
+ ]),
140
+ lantern: Object.freeze([
141
+ field('height', 'Height', 0.7, 2.4, 0.05, 'Total lantern height.'),
142
+ field('lampSize', 'Lamp size', 0.6, 1.6, 0.05, 'Firebox / paper body scale.'),
143
+ field('roofOverhang', 'Roof overhang', 0.8, 1.8, 0.05, 'Cap overhang beyond the body.'),
144
+ field('glow', 'Glow', 0, 1, 0.05, 'Warm emissive strength of the lamp body.'),
145
+ field('wear', 'Wear', 0, 1, 0.05, 'Chips and moss on stone, patches on paper.'),
146
+ ]),
147
+ signpost: Object.freeze([
148
+ field('height', 'Height', 1.4, 3, 0.05, 'Post height.'),
149
+ field('boardLength', 'Board length', 0.5, 1.4, 0.05, 'Length of each direction board.'),
150
+ field('tilt', 'Tilt', 0, 1, 0.05, 'How crooked the post and boards sit.'),
151
+ field('wear', 'Wear', 0, 1, 0.05, 'Cracks and fading.'),
152
+ ]),
153
+ stoneStairs: Object.freeze([
154
+ field('width', 'Width', 0.8, 3, 0.05, 'Step width.'),
155
+ field('stepCount', 'Steps', 3, 14, 1, 'Number of steps in the run.'),
156
+ field('stepHeight', 'Step height', 0.12, 0.28, 0.01, 'Riser height per step.'),
157
+ field('wear', 'Wear', 0, 1, 0.05, 'Rounding, tilt, and chipping of old steps.'),
158
+ ]),
159
+ milestone: Object.freeze([
160
+ field('height', 'Height', 0.4, 1.6, 0.05, 'Marker height.'),
161
+ field('girth', 'Girth', 0.5, 1.6, 0.05, 'Marker thickness relative to height.'),
162
+ field('moss', 'Moss', 0, 1, 0.05, 'Green weathering from the base up.'),
163
+ field('inscription', 'Inscription', 0, 1, 0.05, 'Carved band detail strength.'),
164
+ ]),
165
+ well: Object.freeze([
166
+ field('radius', 'Ring radius', 0.5, 1.3, 0.05, 'Inner stone ring radius.'),
167
+ field('wallHeight', 'Wall height', 0.5, 1.2, 0.05, 'Stone ring height.'),
168
+ field('roofHeight', 'Roof height', 1.6, 3, 0.05, 'Peak height of the roof frame (roofed variant).'),
169
+ field('bucket', 'Bucket', 0, 1, 1, 'Hang a bucket from the winch beam.'),
170
+ ]),
171
+ crateStack: Object.freeze([
172
+ field('count', 'Pieces', 1, 9, 1, 'Crates / barrels in the group.'),
173
+ field('size', 'Size', 0.4, 1, 0.02, 'Base size of each piece in meters.'),
174
+ field('stackiness', 'Stacking', 0, 1, 0.05, 'Chance pieces stack instead of spreading.'),
175
+ field('jitter', 'Jitter', 0, 1, 0.05, 'Rotation and offset chaos.'),
176
+ ]),
177
+ firewood: Object.freeze([
178
+ field('length', 'Length', 0.8, 2.6, 0.05, 'Stack length.'),
179
+ field('height', 'Height', 0.4, 1.2, 0.05, 'Stack height.'),
180
+ field('logRadius', 'Log radius', 0.05, 0.14, 0.005, 'Split log radius.'),
181
+ ]),
182
+ torii: Object.freeze([
183
+ field('height', 'Height', 2, 7, 0.1, 'Gate height to the top lintel.'),
184
+ field('width', 'Width', 0.8, 1.6, 0.05, 'Span relative to height.'),
185
+ field('pillarRadius', 'Pillar radius', 0.06, 0.16, 0.005, 'Pillar thickness relative to height.'),
186
+ field('curvature', 'Curvature', 0, 1, 0.05, 'Upsweep of the top lintel (myōjin).'),
187
+ ]),
188
+ pier: Object.freeze([
189
+ field('length', 'Length', 3, 14, 0.5, 'Reach from the shore anchor point.'),
190
+ field('width', 'Width', 1, 2.6, 0.1, 'Deck width.'),
191
+ field('deckHeight', 'Deck height', 0.4, 1.4, 0.05, 'Deck above the waterline.'),
192
+ field('rails', 'Rails', 0, 2, 1, 'Railed sides (0, 1, or both).'),
193
+ ]),
194
+ stoneWall: Object.freeze([
195
+ field('height', 'Height', 0.4, 1.4, 0.05, 'Wall height.'),
196
+ field('thickness', 'Thickness', 0.25, 0.7, 0.05, 'Wall thickness.'),
197
+ field('stoneSize', 'Stone size', 0.5, 1.5, 0.05, 'Relative size of individual stones.'),
198
+ field('topCourse', 'Top course', 0, 1, 1, 'Finish with upright coping stones.'),
199
+ ]),
200
+ bench: Object.freeze([
201
+ field('length', 'Length', 0.9, 2.4, 0.05, 'Seat length.'),
202
+ field('height', 'Height', 0.35, 0.55, 0.01, 'Seat height.'),
203
+ field('backrest', 'Backrest', 0, 1, 1, 'Add a backrest.'),
204
+ ]),
205
+ });
206
+
207
+ export const PROP_LOOK_FIELDS = Object.freeze([
208
+ field('variation', 'Color variation', 0, 0.5, 0.01, 'Per-piece and per-vertex color drift.'),
209
+ field('edgeLight', 'Edge light', 0, 1, 0.05, 'Lightens upward faces for the hand-painted read.'),
210
+ field('roughness', 'Roughness', 0, 1, 0.05, 'Surface character retained in exported materials.'),
211
+ ]);
212
+
213
+ export const DEFAULT_PROP_SETTINGS = Object.freeze({
214
+ asset: Object.freeze({
215
+ scale: 1,
216
+ seed: 2107,
217
+ type: 'lantern',
218
+ variant: 'stoneToro',
219
+ }),
220
+ shape: Object.freeze({
221
+ backrest: 0,
222
+ boardLength: 0.85,
223
+ bucket: 1,
224
+ count: 4,
225
+ curvature: 0.55,
226
+ deckHeight: 0.7,
227
+ gapChance: 0,
228
+ girth: 1,
229
+ glow: 0.55,
230
+ height: 1.6,
231
+ inscription: 0.5,
232
+ jitter: 0.5,
233
+ lampSize: 1,
234
+ lean: 0.25,
235
+ length: 1.6,
236
+ logRadius: 0.08,
237
+ moss: 0.35,
238
+ pillarRadius: 0.1,
239
+ postHeight: 1.05,
240
+ postSpacing: 2.2,
241
+ radius: 0.8,
242
+ railCount: 2,
243
+ rails: 1,
244
+ roofHeight: 2.3,
245
+ roofOverhang: 1.25,
246
+ sag: 0.35,
247
+ stackiness: 0.55,
248
+ size: 0.62,
249
+ stepCount: 6,
250
+ stepHeight: 0.18,
251
+ stoneSize: 1,
252
+ thickness: 0.09,
253
+ tilt: 0.2,
254
+ topCourse: 1,
255
+ wallHeight: 0.85,
256
+ wear: 0.35,
257
+ width: 1.15,
258
+ }),
259
+ surface: Object.freeze({
260
+ accentColor: Object.freeze([0.85, 0.42, 0.16]),
261
+ edgeLight: 0.3,
262
+ primaryColor: Object.freeze([0.4, 0.41, 0.39]),
263
+ roughness: 0.9,
264
+ secondaryColor: Object.freeze([0.5, 0.36, 0.22]),
265
+ variation: 0.14,
266
+ }),
267
+ });
268
+
269
+ // Per-type defaults layered over DEFAULT_PROP_SETTINGS (same mechanism as
270
+ // DEBRIS_TYPE_DEFAULTS): sensible starting shapes and palettes per type.
271
+ export const PROP_TYPE_DEFAULTS = Object.freeze({
272
+ fence: Object.freeze({
273
+ asset: Object.freeze({ variant: 'ranch' }),
274
+ shape: Object.freeze({ gapChance: 0, lean: 0.3, postHeight: 1.05, postSpacing: 2.2, railCount: 2, sag: 0.3, thickness: 0.08 }),
275
+ surface: Object.freeze({ accentColor: [0.55, 0.4, 0.24], primaryColor: [0.38, 0.27, 0.16], secondaryColor: [0.48, 0.35, 0.21] }),
276
+ }),
277
+ lantern: Object.freeze({
278
+ asset: Object.freeze({ variant: 'stoneToro' }),
279
+ shape: Object.freeze({ glow: 0.55, height: 1.6, lampSize: 1, roofOverhang: 1.25, wear: 0.4 }),
280
+ surface: Object.freeze({ accentColor: [1, 0.72, 0.32], primaryColor: [0.42, 0.43, 0.41], secondaryColor: [0.52, 0.52, 0.48] }),
281
+ }),
282
+ signpost: Object.freeze({
283
+ asset: Object.freeze({ variant: 'double' }),
284
+ shape: Object.freeze({ boardLength: 0.85, height: 2.1, tilt: 0.25, wear: 0.35 }),
285
+ surface: Object.freeze({ accentColor: [0.82, 0.74, 0.55], primaryColor: [0.36, 0.26, 0.16], secondaryColor: [0.5, 0.38, 0.24] }),
286
+ }),
287
+ stoneStairs: Object.freeze({
288
+ asset: Object.freeze({ variant: 'straight' }),
289
+ shape: Object.freeze({ stepCount: 6, stepHeight: 0.18, wear: 0.45, width: 1.4 }),
290
+ surface: Object.freeze({ accentColor: [0.45, 0.52, 0.38], primaryColor: [0.42, 0.43, 0.41], secondaryColor: [0.55, 0.54, 0.5] }),
291
+ }),
292
+ milestone: Object.freeze({
293
+ asset: Object.freeze({ variant: 'roadStone' }),
294
+ shape: Object.freeze({ girth: 1, height: 0.8, inscription: 0.5, moss: 0.4 }),
295
+ surface: Object.freeze({ accentColor: [0.4, 0.5, 0.32], primaryColor: [0.44, 0.45, 0.43], secondaryColor: [0.56, 0.55, 0.5] }),
296
+ }),
297
+ well: Object.freeze({
298
+ asset: Object.freeze({ variant: 'roofed' }),
299
+ shape: Object.freeze({ bucket: 1, radius: 0.8, roofHeight: 2.3, wallHeight: 0.85 }),
300
+ surface: Object.freeze({ accentColor: [0.5, 0.36, 0.22], primaryColor: [0.45, 0.46, 0.44], secondaryColor: [0.4, 0.29, 0.18] }),
301
+ }),
302
+ crateStack: Object.freeze({
303
+ asset: Object.freeze({ variant: 'mixed' }),
304
+ shape: Object.freeze({ count: 4, jitter: 0.5, size: 0.62, stackiness: 0.55 }),
305
+ surface: Object.freeze({ accentColor: [0.32, 0.33, 0.36], primaryColor: [0.5, 0.37, 0.22], secondaryColor: [0.6, 0.47, 0.28] }),
306
+ }),
307
+ firewood: Object.freeze({
308
+ asset: Object.freeze({ variant: 'stacked' }),
309
+ shape: Object.freeze({ height: 0.7, length: 1.6, logRadius: 0.08 }),
310
+ surface: Object.freeze({ accentColor: [0.72, 0.62, 0.46], primaryColor: [0.35, 0.24, 0.14], secondaryColor: [0.52, 0.4, 0.26] }),
311
+ }),
312
+ torii: Object.freeze({
313
+ asset: Object.freeze({ variant: 'myojin' }),
314
+ shape: Object.freeze({ curvature: 0.55, height: 4.2, pillarRadius: 0.1, width: 1.15 }),
315
+ surface: Object.freeze({ accentColor: [0.16, 0.16, 0.18], primaryColor: [0.78, 0.2, 0.12], secondaryColor: [0.62, 0.16, 0.1] }),
316
+ }),
317
+ pier: Object.freeze({
318
+ asset: Object.freeze({ variant: 'straight' }),
319
+ shape: Object.freeze({ deckHeight: 0.7, length: 8, rails: 1, width: 1.8 }),
320
+ surface: Object.freeze({ accentColor: [0.36, 0.3, 0.24], primaryColor: [0.42, 0.3, 0.18], secondaryColor: [0.52, 0.4, 0.26] }),
321
+ }),
322
+ stoneWall: Object.freeze({
323
+ asset: Object.freeze({ variant: 'dry' }),
324
+ shape: Object.freeze({ height: 0.8, stoneSize: 1, thickness: 0.45, topCourse: 1 }),
325
+ surface: Object.freeze({ accentColor: [0.42, 0.5, 0.34], primaryColor: [0.46, 0.46, 0.44], secondaryColor: [0.56, 0.55, 0.51] }),
326
+ }),
327
+ bench: Object.freeze({
328
+ asset: Object.freeze({ variant: 'plank' }),
329
+ shape: Object.freeze({ backrest: 0, height: 0.45, length: 1.5 }),
330
+ surface: Object.freeze({ accentColor: [0.55, 0.42, 0.26], primaryColor: [0.4, 0.29, 0.18], secondaryColor: [0.5, 0.38, 0.24] }),
331
+ }),
332
+ });
333
+
334
+ function cloneValue(value) {
335
+ if (Array.isArray(value)) return [...value];
336
+ if (value && typeof value === 'object') return Object.fromEntries(
337
+ Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]),
338
+ );
339
+ return value;
340
+ }
341
+
342
+ export function clonePropSettings(settings = DEFAULT_PROP_SETTINGS) {
343
+ return cloneValue(settings);
344
+ }
345
+
346
+ export function createPropSettings(overrides = {}) {
347
+ const requestedType = overrides.asset?.type;
348
+ const type = PROP_TYPES[requestedType] ? requestedType : (requestedType ? 'lantern' : null);
349
+ const typeDefaults = type ? PROP_TYPE_DEFAULTS[type] : null;
350
+ const result = clonePropSettings(DEFAULT_PROP_SETTINGS);
351
+ if (typeDefaults) {
352
+ Object.assign(result.asset, cloneValue(typeDefaults.asset ?? {}));
353
+ Object.assign(result.shape, cloneValue(typeDefaults.shape ?? {}));
354
+ Object.assign(result.surface, cloneValue(typeDefaults.surface ?? {}));
355
+ result.asset.type = type;
356
+ }
357
+ Object.assign(result.asset, cloneValue(overrides.asset ?? {}));
358
+ Object.assign(result.shape, cloneValue(overrides.shape ?? {}));
359
+ Object.assign(result.surface, cloneValue(overrides.surface ?? {}));
360
+ result.asset.type = PROP_TYPES[result.asset.type] ? result.asset.type : 'lantern';
361
+ const variants = PROP_TYPES[result.asset.type].variants;
362
+ if (!variants.some((entry) => entry.id === result.asset.variant)) {
363
+ result.asset.variant = variants[0].id;
364
+ }
365
+ result.asset.seed = Math.max(0, Math.round(Number(result.asset.seed) || 0)) >>> 0;
366
+ result.asset.scale = Math.min(4, Math.max(0.25, Number(result.asset.scale) || 1));
367
+ return result;
368
+ }
369
+
370
+ export const PROP_RECIPE_KIND = 'toonlab.propRecipe';
371
+ export const PROP_RECIPE_VERSION = 1;
372
+
373
+ export function createPropRecipeDocument(settings, { name = 'Untitled prop' } = {}) {
374
+ return {
375
+ kind: PROP_RECIPE_KIND,
376
+ name,
377
+ settings: createPropSettings(settings),
378
+ version: PROP_RECIPE_VERSION,
379
+ };
380
+ }
381
+
382
+ export function validatePropRecipeDocument(document) {
383
+ const errors = [];
384
+ if (!document || typeof document !== 'object') errors.push('Recipe must be an object.');
385
+ else {
386
+ if (document.kind !== PROP_RECIPE_KIND) errors.push('Unknown recipe kind.');
387
+ if (document.version !== PROP_RECIPE_VERSION) errors.push('Unsupported recipe version.');
388
+ if (!document.settings?.asset) errors.push('Recipe settings are missing.');
389
+ const requestedType = document.settings?.asset?.type;
390
+ if (requestedType && !PROP_TYPES[requestedType]) {
391
+ errors.push(`Unknown prop type “${requestedType}”.`);
392
+ }
393
+ }
394
+ return { errors, ok: errors.length === 0 };
395
+ }
@@ -0,0 +1,242 @@
1
+ // Shared stylized caustic receiver for ToonLab environment/shore materials.
2
+ //
3
+ // The water surface already composites caustics into its above-water view.
4
+ // This receiver is enabled only while the camera is submerged, when the
5
+ // seabed is rendered directly. It follows the useful architecture from
6
+ // Three.js Water Pro's documented ocean-floor effect: a seamless precomputed
7
+ // Voronoi field, two independently moving samples, and a minimum combine.
8
+ // The texture is generated deterministically at runtime, so the library keeps
9
+ // its no-external-water-assets contract and avoids running a Voronoi search in
10
+ // every environment fragment.
11
+
12
+ import * as THREE from 'three';
13
+ import {
14
+ abs,
15
+ clamp,
16
+ exp,
17
+ float,
18
+ max,
19
+ min,
20
+ mix,
21
+ sin,
22
+ smoothstep,
23
+ texture,
24
+ uniform,
25
+ vec2,
26
+ vec3,
27
+ } from 'three/tsl';
28
+
29
+ const TEXTURE_SIZE = 96;
30
+ const SITE_GRID = 7;
31
+
32
+ function hash01(x, y, salt) {
33
+ const value = Math.sin(x * 127.1 + y * 311.7 + salt * 74.7) * 43758.5453123;
34
+ return value - Math.floor(value);
35
+ }
36
+
37
+ function smooth01(value) {
38
+ const t = Math.min(1, Math.max(0, value));
39
+ return t * t * (3 - 2 * t);
40
+ }
41
+
42
+ function createCausticCellTexture() {
43
+ const sites = [];
44
+ for (let y = 0; y < SITE_GRID; y += 1) {
45
+ for (let x = 0; x < SITE_GRID; x += 1) {
46
+ sites.push({
47
+ x: (x + 0.18 + hash01(x, y, 1) * 0.64) / SITE_GRID,
48
+ y: (y + 0.18 + hash01(x, y, 2) * 0.64) / SITE_GRID,
49
+ });
50
+ }
51
+ }
52
+
53
+ const data = new Uint8Array(TEXTURE_SIZE * TEXTURE_SIZE * 4);
54
+ for (let py = 0; py < TEXTURE_SIZE; py += 1) {
55
+ const v = (py + 0.5) / TEXTURE_SIZE;
56
+ for (let px = 0; px < TEXTURE_SIZE; px += 1) {
57
+ const u = (px + 0.5) / TEXTURE_SIZE;
58
+ let nearest = Infinity;
59
+ let second = Infinity;
60
+ for (const site of sites) {
61
+ let dx = Math.abs(u - site.x);
62
+ let dy = Math.abs(v - site.y);
63
+ dx = Math.min(dx, 1 - dx);
64
+ dy = Math.min(dy, 1 - dy);
65
+ const distanceSquared = dx * dx + dy * dy;
66
+ if (distanceSquared < nearest) {
67
+ second = nearest;
68
+ nearest = distanceSquared;
69
+ } else if (distanceSquared < second) {
70
+ second = distanceSquared;
71
+ }
72
+ }
73
+ // Store bright cell interiors and dark borders. The shader's
74
+ // 1 - min(layerA, layerB) turns either layer's borders into light.
75
+ const edgeDistance = Math.sqrt(second) - Math.sqrt(nearest);
76
+ const cellInterior = smooth01((edgeDistance - 0.0015) / 0.014);
77
+ const value = Math.round(cellInterior * 255);
78
+ const index = (py * TEXTURE_SIZE + px) * 4;
79
+ data[index] = value;
80
+ data[index + 1] = value;
81
+ data[index + 2] = value;
82
+ data[index + 3] = 255;
83
+ }
84
+ }
85
+
86
+ const result = new THREE.DataTexture(
87
+ data,
88
+ TEXTURE_SIZE,
89
+ TEXTURE_SIZE,
90
+ THREE.RGBAFormat,
91
+ );
92
+ result.name = 'ToonLab.ProjectedWaterCaustics';
93
+ result.colorSpace = THREE.NoColorSpace;
94
+ result.wrapS = THREE.RepeatWrapping;
95
+ result.wrapT = THREE.RepeatWrapping;
96
+ result.minFilter = THREE.LinearFilter;
97
+ result.magFilter = THREE.LinearFilter;
98
+ result.generateMipmaps = false;
99
+ result.needsUpdate = true;
100
+ return result;
101
+ }
102
+
103
+ const causticTexture = createCausticCellTexture();
104
+
105
+ export const projectedWaterCausticUniforms = {
106
+ map: texture(causticTexture),
107
+ enabled: uniform(0.0),
108
+ time: uniform(0.0),
109
+ waterLevel: uniform(0.0),
110
+ // centerX, centerZ, halfWidth, halfDepth
111
+ region: uniform(new THREE.Vector4(0, 0, 0.5, 0.5)),
112
+ color: uniform(new THREE.Color(0.75, 0.98, 1.0)),
113
+ intensity: uniform(0.35),
114
+ scale: uniform(0.8),
115
+ speed: uniform(0.6),
116
+ flowDirection: uniform(new THREE.Vector2(0.72, -0.18)),
117
+ waveDistortion: uniform(0.06),
118
+ depthAttenuation: uniform(0.32),
119
+ };
120
+
121
+ function setSrgbColor(target, value) {
122
+ if (value?.isColor) {
123
+ target.copy(value);
124
+ } else if (Array.isArray(value)) {
125
+ target.setRGB(
126
+ value[0] ?? 1,
127
+ value[1] ?? 1,
128
+ value[2] ?? 1,
129
+ THREE.SRGBColorSpace,
130
+ );
131
+ }
132
+ }
133
+
134
+ /** Refresh the shared receiver from the active WaterSurface once per frame. */
135
+ export function updateProjectedWaterCaustics({
136
+ enabled,
137
+ time,
138
+ waterLevel,
139
+ centerX,
140
+ centerZ,
141
+ halfWidth,
142
+ halfDepth,
143
+ color,
144
+ intensity,
145
+ scale,
146
+ speed,
147
+ flowDirection,
148
+ waveDistortion,
149
+ depthAttenuation,
150
+ } = {}) {
151
+ const u = projectedWaterCausticUniforms;
152
+ u.enabled.value = enabled ? 1 : 0;
153
+ if (Number.isFinite(time)) u.time.value = time;
154
+ if (Number.isFinite(waterLevel)) u.waterLevel.value = waterLevel;
155
+ if ([centerX, centerZ, halfWidth, halfDepth].every(Number.isFinite)) {
156
+ u.region.value.set(
157
+ centerX,
158
+ centerZ,
159
+ Math.max(halfWidth, 0.001),
160
+ Math.max(halfDepth, 0.001),
161
+ );
162
+ }
163
+ setSrgbColor(u.color.value, color);
164
+ if (Number.isFinite(intensity)) u.intensity.value = THREE.MathUtils.clamp(intensity, 0, 4);
165
+ if (Number.isFinite(scale)) u.scale.value = THREE.MathUtils.clamp(scale, 0.02, 12);
166
+ if (Number.isFinite(speed)) u.speed.value = THREE.MathUtils.clamp(speed, 0, 8);
167
+ if (Array.isArray(flowDirection)) {
168
+ u.flowDirection.value.set(flowDirection[0] ?? 1, flowDirection[1] ?? 0);
169
+ } else if (flowDirection?.isVector2) {
170
+ u.flowDirection.value.copy(flowDirection);
171
+ }
172
+ if (Number.isFinite(waveDistortion)) {
173
+ u.waveDistortion.value = THREE.MathUtils.clamp(waveDistortion, 0, 1);
174
+ }
175
+ if (Number.isFinite(depthAttenuation)) {
176
+ u.depthAttenuation.value = THREE.MathUtils.clamp(depthAttenuation, 0.001, 8);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Returns additive underwater caustic light for a world position/normal.
182
+ * This deliberately stays branchless. A previous TSL `If`-wrapped version
183
+ * compiled as a zeroed material branch on some WebGPU/WebGL node backends,
184
+ * darkening the receiver whenever caustics were disabled above water.
185
+ */
186
+ export function projectedWaterCaustics(worldPosition, worldNormal) {
187
+ const u = projectedWaterCausticUniforms;
188
+ const depth = max(u.waterLevel.sub(worldPosition.y), 0.0);
189
+ const local = abs(worldPosition.xz.sub(u.region.xy));
190
+ const feather = max(min(u.region.z, u.region.w).mul(0.025), 0.04);
191
+ const inside = smoothstep(
192
+ u.region.z.sub(feather),
193
+ u.region.z,
194
+ local.x,
195
+ ).oneMinus().mul(smoothstep(
196
+ u.region.w.sub(feather),
197
+ u.region.w,
198
+ local.y,
199
+ ).oneMinus());
200
+ const submerged = smoothstep(0.012, 0.08, depth);
201
+ const depthFade = exp(depth.mul(u.depthAttenuation).negate());
202
+ const receivingFace = mix(
203
+ 0.22,
204
+ 1.0,
205
+ smoothstep(0.04, 0.72, clamp(worldNormal.y, 0.0, 1.0)),
206
+ );
207
+
208
+ const phase = u.time.mul(u.speed);
209
+ const waveWarp = vec2(
210
+ sin(worldPosition.z.mul(0.38).add(phase.mul(0.7))),
211
+ sin(worldPosition.x.mul(0.31).sub(phase.mul(0.57))),
212
+ ).mul(u.waveDistortion);
213
+ const flow = u.flowDirection.mul(phase).mul(0.07);
214
+ // The generated tile contains SITE_GRID cells per axis. Normalize that
215
+ // baked frequency so the public causticsScale keeps its existing meaning
216
+ // (roughly cells per metre) instead of becoming seven times too dense and
217
+ // averaging into a flat cyan wash at gameplay camera distances.
218
+ const anchor = worldPosition.xz.mul(u.scale.div(float(SITE_GRID)));
219
+ const uvA = anchor.add(flow).add(waveWarp);
220
+ const rotated = vec2(
221
+ anchor.x.mul(0.752).sub(anchor.y.mul(0.659)),
222
+ anchor.x.mul(0.659).add(anchor.y.mul(0.752)),
223
+ );
224
+ const uvB = rotated.mul(1.31)
225
+ .sub(flow.mul(0.82))
226
+ .sub(waveWarp.yx.mul(0.73));
227
+ const cellA = u.map.sample(uvA).level(0).r;
228
+ const cellB = u.map.sample(uvB).level(0).r;
229
+ const web = min(cellA, cellB).oneMinus();
230
+ const strands = smoothstep(0.2, 0.9, web).pow(1.7);
231
+ const pulse = sin(phase.mul(0.9).add(worldPosition.x.mul(0.11)))
232
+ .mul(0.5).add(0.5);
233
+ return u.color
234
+ .mul(u.intensity)
235
+ .mul(strands)
236
+ .mul(mix(0.76, 1.0, pulse))
237
+ .mul(submerged)
238
+ .mul(depthFade)
239
+ .mul(receivingFace)
240
+ .mul(inside)
241
+ .mul(clamp(u.enabled, 0.0, 1.0));
242
+ }