@call-me-sensei/toonlab 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. package/src/water/waterVegetation.js +83 -0
@@ -0,0 +1,300 @@
1
+ import * as THREE from 'three';
2
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
+
4
+ // GLB-export baking for stylized trees/bushes: the runtime canopy is a
5
+ // billboard quad soup driven by a ShaderMaterial (wind, toon ramp, backlit),
6
+ // none of which glTF can carry. prepareTreeForExport() converts a live plant
7
+ // into a plain-material THREE.Group any engine can import:
8
+ //
9
+ // - Foliage cards become STATIC quads oriented by each card's baked
10
+ // shading normal ('normal' mode) or crossed quad pairs ('crossed',
11
+ // default — view-robust from every angle at 2× the triangles).
12
+ // - Every vertex keeps aShadeNormal as its normal, so the imported mesh
13
+ // still lights as one soft volume — the core of the stylized look.
14
+ // - The three-tone palette is baked into vertex colors exactly as the leaf
15
+ // fragment shader computes it under the plant's current sun.
16
+ // - Wind does not survive (it lives in the vertex shader): exports are
17
+ // static meshes, like every tree-tool GLB.
18
+ //
19
+ // The group also carries the plant's recipe in userData.treeRecipe, which
20
+ // GLTFExporter serializes into glTF `extras` — a GLB can always be traced
21
+ // back to (and regenerated from) its recipe.
22
+
23
+ function smoothstep(edge0, edge1, x) {
24
+ const t = THREE.MathUtils.clamp((x - edge0) / (edge1 - edge0), 0, 1);
25
+ return t * t * (3 - 2 * t);
26
+ }
27
+
28
+ // Expand the canopy quad soup (4 verts/card: position = center, aCorner,
29
+ // aShadeNormal, aInfo = [worldSize, phase, tint, heightT]) into static quads.
30
+ // matrix — applied to card centers (canopy-local → export space). Card
31
+ // extents deliberately are NOT scaled: aInfo.x is world size, the
32
+ // same convention the billboard shader uses.
33
+ export function bakeFoliageGeometry(canopyGeometry, {
34
+ mode = 'crossed',
35
+ palette,
36
+ sunDirection = [0.35, 0.72, 0.42],
37
+ sunColor = [1.0, 0.96, 0.84],
38
+ matrix = null,
39
+ } = {}) {
40
+ const positions = canopyGeometry.attributes.position;
41
+ const shadeNormals = canopyGeometry.attributes.aShadeNormal;
42
+ const infos = canopyGeometry.attributes.aInfo;
43
+ const cardCount = positions.count / 4;
44
+ const quadsPerCard = mode === 'crossed' ? 2 : 1;
45
+ const quadCount = cardCount * quadsPerCard;
46
+
47
+ const outPositions = new Float32Array(quadCount * 4 * 3);
48
+ const outNormals = new Float32Array(quadCount * 4 * 3);
49
+ const outUvs = new Float32Array(quadCount * 4 * 2);
50
+ const outColors = new Float32Array(quadCount * 4 * 3);
51
+ const outIndices = new Uint32Array(quadCount * 6);
52
+
53
+ const cornerOffsets = [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]];
54
+ const cornerUvs = [[0, 0], [1, 0], [1, 1], [0, 1]];
55
+
56
+ const worldUp = new THREE.Vector3(0, 1, 0);
57
+ const sun = new THREE.Vector3(...sunDirection).normalize();
58
+ const sunTint = new THREE.Color(...sunColor);
59
+ const center = new THREE.Vector3();
60
+ const normal = new THREE.Vector3();
61
+ const right = new THREE.Vector3();
62
+ const up = new THREE.Vector3();
63
+ const rolledRight = new THREE.Vector3();
64
+ const rolledUp = new THREE.Vector3();
65
+ const quadRight = new THREE.Vector3();
66
+ const vertex = new THREE.Vector3();
67
+ const color = new THREE.Color();
68
+
69
+ let quadIndex = 0;
70
+ for (let card = 0; card < cardCount; card += 1) {
71
+ const base = card * 4;
72
+ center.fromBufferAttribute(positions, base);
73
+ if (matrix) center.applyMatrix4(matrix);
74
+ normal.fromBufferAttribute(shadeNormals, base).normalize();
75
+ const size = infos.getX(base);
76
+ const phase = infos.getY(base);
77
+ const tint = infos.getZ(base);
78
+ const heightT = infos.getW(base);
79
+
80
+ // Stable tangent frame from the shading normal; the fallback covers
81
+ // straight-up/down normals where the up-cross degenerates.
82
+ right.crossVectors(worldUp, normal);
83
+ if (right.lengthSq() < 1e-6) right.set(1, 0, 0);
84
+ right.normalize();
85
+ up.crossVectors(normal, right);
86
+
87
+ // Keep each card's static roll (aInfo.y drives it in the shader too) so
88
+ // the baked crown keeps its shimmered variety instead of gridded quads.
89
+ const roll = phase * Math.PI * 2;
90
+ const rollCos = Math.cos(roll);
91
+ const rollSin = Math.sin(roll);
92
+ rolledRight.copy(right).multiplyScalar(rollCos).addScaledVector(up, rollSin);
93
+ rolledUp.copy(up).multiplyScalar(rollCos).addScaledVector(right, -rollSin);
94
+
95
+ // Vertex color = the leaf fragment shader's palette math under this sun
96
+ // (treeLeaf.frag.glsl): two-band wrap ramp + height-gated crown crest +
97
+ // per-card tint jitter. Sprite luminance multiplies at render via `map`;
98
+ // view-dependent terms (rim, backlit, sky fill) can't bake.
99
+ const wrap = normal.dot(sun) * 0.5 + 0.5;
100
+ const litBand = smoothstep(0.38, 0.56, wrap);
101
+ const crestBand = smoothstep(0.66, 0.78, wrap) *
102
+ smoothstep(0.3, 0.8, heightT * (0.7 + 0.3 * (normal.y * 0.5 + 0.5)));
103
+ color.copy(palette.shadow).lerp(palette.lit, litBand);
104
+ color.lerp(palette.crown, crestBand);
105
+ color.r *= THREE.MathUtils.lerp(1, sunTint.r, litBand * 0.35);
106
+ color.g *= THREE.MathUtils.lerp(1, sunTint.g, litBand * 0.35);
107
+ color.b *= THREE.MathUtils.lerp(1, sunTint.b, litBand * 0.35);
108
+ color.multiplyScalar(0.92 + tint * 0.16);
109
+
110
+ for (let quad = 0; quad < quadsPerCard; quad += 1) {
111
+ if (quad === 0) {
112
+ quadRight.copy(rolledRight);
113
+ } else {
114
+ // Second quad: the card plane rotated 90° around its up axis, so the
115
+ // pair reads solid from every heading.
116
+ quadRight.crossVectors(rolledUp, rolledRight);
117
+ }
118
+ for (let corner = 0; corner < 4; corner += 1) {
119
+ const out = (quadIndex * 4 + corner) * 3;
120
+ vertex.copy(center)
121
+ .addScaledVector(quadRight, cornerOffsets[corner][0] * size)
122
+ .addScaledVector(rolledUp, cornerOffsets[corner][1] * size);
123
+ outPositions[out] = vertex.x;
124
+ outPositions[out + 1] = vertex.y;
125
+ outPositions[out + 2] = vertex.z;
126
+ outNormals[out] = normal.x;
127
+ outNormals[out + 1] = normal.y;
128
+ outNormals[out + 2] = normal.z;
129
+ outColors[out] = color.r;
130
+ outColors[out + 1] = color.g;
131
+ outColors[out + 2] = color.b;
132
+ const uvOut = (quadIndex * 4 + corner) * 2;
133
+ outUvs[uvOut] = cornerUvs[corner][0];
134
+ outUvs[uvOut + 1] = cornerUvs[corner][1];
135
+ }
136
+ const vertexBase = quadIndex * 4;
137
+ outIndices.set([
138
+ vertexBase, vertexBase + 1, vertexBase + 2,
139
+ vertexBase, vertexBase + 2, vertexBase + 3,
140
+ ], quadIndex * 6);
141
+ quadIndex += 1;
142
+ }
143
+ }
144
+
145
+ const geometry = new THREE.BufferGeometry();
146
+ geometry.setIndex(new THREE.BufferAttribute(outIndices, 1));
147
+ geometry.setAttribute('position', new THREE.BufferAttribute(outPositions, 3));
148
+ geometry.setAttribute('normal', new THREE.BufferAttribute(outNormals, 3));
149
+ geometry.setAttribute('uv', new THREE.BufferAttribute(outUvs, 2));
150
+ geometry.setAttribute('color', new THREE.BufferAttribute(outColors, 3));
151
+ geometry.computeBoundingSphere();
152
+ return geometry;
153
+ }
154
+
155
+ // Export copy of the leaf sprite with the fragment shader's luminance remap
156
+ // (color *= 0.78 + sprite.r * 0.36) baked into the pixels: a standard
157
+ // material multiplies the RAW map, which reads ~25% darker than the shader.
158
+ // The 1.14 ceiling clamps — the lost top sliver is invisible in practice.
159
+ export function createBakedLeafTexture(sourceTexture) {
160
+ const image = sourceTexture?.image;
161
+ if (!image) return sourceTexture;
162
+ const canvas = document.createElement('canvas');
163
+ canvas.width = image.width;
164
+ canvas.height = image.height;
165
+ const ctx = canvas.getContext('2d');
166
+ ctx.drawImage(image, 0, 0);
167
+ const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
168
+ const data = pixels.data;
169
+ for (let i = 0; i < data.length; i += 4) {
170
+ data[i] = Math.min(255, 0.78 * 255 + data[i] * 0.36);
171
+ data[i + 1] = Math.min(255, 0.78 * 255 + data[i + 1] * 0.36);
172
+ data[i + 2] = Math.min(255, 0.78 * 255 + data[i + 2] * 0.36);
173
+ }
174
+ ctx.putImageData(pixels, 0, 0);
175
+ const texture = new THREE.CanvasTexture(canvas);
176
+ texture.colorSpace = THREE.NoColorSpace;
177
+ texture.anisotropy = sourceTexture.anisotropy ?? 4;
178
+ // Owned by the export group; disposeExportGroup frees it (shared source
179
+ // textures like the bark map are left alone).
180
+ texture.userData.bakedLeafTexture = true;
181
+ return texture;
182
+ }
183
+
184
+ // Standard-material counterpart of the leaf shader: the (remapped) sprite RGB
185
+ // carries per-leaf luminance, so map × vertex color approximates the shader's
186
+ // palette-times-sprite shading. alphaTest exports as glTF MASK/alphaCutoff.
187
+ export function createBakedFoliageMaterial({ leafMap = null, alphaTest = 0.3 } = {}) {
188
+ return new THREE.MeshStandardMaterial({
189
+ name: 'BakedFoliage',
190
+ map: leafMap ? createBakedLeafTexture(leafMap) : null,
191
+ color: 0xffffff,
192
+ vertexColors: true,
193
+ alphaTest,
194
+ transparent: false,
195
+ side: THREE.DoubleSide,
196
+ roughness: 1,
197
+ metalness: 0,
198
+ });
199
+ }
200
+
201
+ // Live plant (StylizedTree | StylizedBush | StylizedFlower) → export-ready group with plain
202
+ // materials, world scale baked into the vertices (identity transforms), and
203
+ // the recipe attached as userData.treeRecipe (→ glTF extras).
204
+ export function prepareTreeForExport(plant, { foliageMode = 'crossed' } = {}) {
205
+ const group = new THREE.Group();
206
+ group.name = plant.name || 'StylizedPlant';
207
+ const size = plant.scale.x;
208
+
209
+ if (plant.trunkMesh) {
210
+ const trunkGeometry = plant.trunkMesh.geometry.clone()
211
+ .applyMatrix4(new THREE.Matrix4().makeScale(size, size, size));
212
+ const source = plant.trunkMesh.material;
213
+ const trunkMaterial = new THREE.MeshStandardMaterial({
214
+ name: 'BakedBark',
215
+ color: source.color?.clone() ?? new THREE.Color(0xc9ab8a),
216
+ map: source.map ?? null,
217
+ roughness: 1,
218
+ metalness: 0,
219
+ });
220
+ const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
221
+ trunk.name = 'Trunk';
222
+ trunk.castShadow = true;
223
+ trunk.receiveShadow = true;
224
+ group.add(trunk);
225
+ }
226
+
227
+ const canopyMesh = plant.canopyMesh;
228
+ const uniforms = canopyMesh.material.uniforms;
229
+ // Canopy-local point → plant-root space: scale by (size × canopyScale)
230
+ // around the anchor. Card extents stay world-sized (see bakeFoliageGeometry).
231
+ const canopyMatrix = new THREE.Matrix4().compose(
232
+ canopyMesh.position.clone().multiplyScalar(size),
233
+ new THREE.Quaternion(),
234
+ new THREE.Vector3().setScalar(size * canopyMesh.scale.x),
235
+ );
236
+ const foliageGeometry = bakeFoliageGeometry(canopyMesh.geometry, {
237
+ mode: foliageMode,
238
+ palette: {
239
+ lit: uniforms.uLitColor.value,
240
+ shadow: uniforms.uShadowColor.value,
241
+ crown: uniforms.uCrownColor.value,
242
+ },
243
+ sunDirection: uniforms.uSunDirection.value.toArray(),
244
+ sunColor: uniforms.uSunColor.value.toArray(),
245
+ matrix: canopyMatrix,
246
+ });
247
+ const foliage = new THREE.Mesh(foliageGeometry, createBakedFoliageMaterial({
248
+ leafMap: uniforms.uLeafMap.value,
249
+ alphaTest: uniforms.uAlphaCutoff.value,
250
+ }));
251
+ foliage.name = 'Foliage';
252
+ foliage.castShadow = true;
253
+ group.add(foliage);
254
+
255
+ // Flower plants: bake the instanced 3D bloom meshes (vertex-colored
256
+ // petals, canopy-local matrices) into one static mesh so blooms survive
257
+ // the GLB.
258
+ if (plant.headsMesh) {
259
+ const headsMesh = plant.headsMesh;
260
+ const instanceMatrix = new THREE.Matrix4();
261
+ const pieces = [];
262
+ for (let i = 0; i < headsMesh.count; i += 1) {
263
+ headsMesh.getMatrixAt(i, instanceMatrix);
264
+ pieces.push(headsMesh.geometry.clone()
265
+ .applyMatrix4(instanceMatrix)
266
+ .applyMatrix4(canopyMatrix));
267
+ }
268
+ if (pieces.length) {
269
+ const headMaterial = new THREE.MeshStandardMaterial({
270
+ name: 'BakedBlooms',
271
+ vertexColors: true,
272
+ side: THREE.DoubleSide,
273
+ roughness: 1,
274
+ metalness: 0,
275
+ });
276
+ const blooms = new THREE.Mesh(mergeGeometries(pieces), headMaterial);
277
+ pieces.forEach((piece) => piece.dispose());
278
+ blooms.name = 'Blooms';
279
+ blooms.castShadow = true;
280
+ group.add(blooms);
281
+ }
282
+ }
283
+
284
+ if (typeof plant.toJSON === 'function') {
285
+ group.userData.treeRecipe = plant.toJSON();
286
+ }
287
+ return group;
288
+ }
289
+
290
+ // Free everything prepareTreeForExport allocated (baked geometry + materials;
291
+ // shared textures are left alone).
292
+ export function disposeExportGroup(group) {
293
+ group.traverse((node) => {
294
+ if (node.isMesh) {
295
+ node.geometry.dispose();
296
+ if (node.material.map?.userData?.bakedLeafTexture) node.material.map.dispose();
297
+ node.material.dispose();
298
+ }
299
+ });
300
+ }