@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.
- package/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- package/src/water/waterVegetation.js +83 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Common-flower species catalog + canvas head-sprite builder. Shared by the
|
|
2
|
+
// tree lab's flower patch (canopy blossoms / ground stems) and the
|
|
3
|
+
// StylizedFlower plant, so every flower feature agrees on what a "daisy" or
|
|
4
|
+
// "poppy" looks like. Heads are procedural canvas sprites built from the
|
|
5
|
+
// same traceLeafShapePath silhouettes as leaves — no texture assets.
|
|
6
|
+
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
9
|
+
import { traceLeafShapePath } from './stylizedTreeFoliage.js';
|
|
10
|
+
|
|
11
|
+
// Each species is a full head look: petal layers (outer → inner; `scale`
|
|
12
|
+
// shrinks a layer, `shade` darkens the petal color, `offset` rotates it
|
|
13
|
+
// between the previous layer's petals) plus an optional center disc.
|
|
14
|
+
// `color`/`height` are the defaults the UI applies when the species is
|
|
15
|
+
// picked — the user can still recolor or resize afterwards. `headDiameter`
|
|
16
|
+
// is the real-world bloom diameter in meters, so plants keep believable
|
|
17
|
+
// proportions at any plant size (headScale multiplies it). `faceUp` is the
|
|
18
|
+
// up-bias mixed into each bloom's twig direction — a WORLD-FIXED facing
|
|
19
|
+
// (blooms never track the camera): near 0 faces outward like a mature
|
|
20
|
+
// sunflower, large opens to the sky like a tulip cup.
|
|
21
|
+
// The first three ids (tulip/daisy/blossom) predate the catalog and must
|
|
22
|
+
// keep their ids so saved recipes still resolve.
|
|
23
|
+
export const FLOWER_SPECIES = Object.freeze([
|
|
24
|
+
{
|
|
25
|
+
color: [0.93, 0.4, 0.45],
|
|
26
|
+
height: 0.4,
|
|
27
|
+
icon: '🌷',
|
|
28
|
+
headDiameter: 0.09,
|
|
29
|
+
curl: 0.5,
|
|
30
|
+
faceUp: 2.5,
|
|
31
|
+
id: 'tulip',
|
|
32
|
+
label: 'Tulip',
|
|
33
|
+
layers: [{ count: 3, lift: 66, shape: 'round', width: 0.62 }, { count: 3, lift: 58, offset: 0.5, shade: 0.9, shape: 'round', width: 0.62 }],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
center: { color: '#f0c437', radius: 13 },
|
|
37
|
+
color: [1, 0.98, 0.92],
|
|
38
|
+
height: 0.3,
|
|
39
|
+
icon: '🌼',
|
|
40
|
+
headDiameter: 0.06,
|
|
41
|
+
curl: 0.15,
|
|
42
|
+
faceUp: 1.2,
|
|
43
|
+
id: 'daisy',
|
|
44
|
+
label: 'Daisy',
|
|
45
|
+
layers: [{ count: 10, lift: 12, shape: 'teardrop', width: 0.3 }],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
center: { color: '#f0c437', radius: 13 },
|
|
49
|
+
color: [0.97, 0.78, 0.86],
|
|
50
|
+
height: 0.3,
|
|
51
|
+
icon: '🌸',
|
|
52
|
+
headDiameter: 0.05,
|
|
53
|
+
curl: 0.2,
|
|
54
|
+
faceUp: 0.5,
|
|
55
|
+
id: 'blossom',
|
|
56
|
+
label: 'Cherry Blossom',
|
|
57
|
+
layers: [{ count: 5, lift: 24, shape: 'round', width: 0.5 }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
center: { color: '#5c3a1e', radius: 26 },
|
|
61
|
+
color: [0.99, 0.78, 0.18],
|
|
62
|
+
height: 0.85,
|
|
63
|
+
icon: '🌻',
|
|
64
|
+
headDiameter: 0.28,
|
|
65
|
+
curl: 0.1,
|
|
66
|
+
faceUp: 0.25,
|
|
67
|
+
id: 'sunflower',
|
|
68
|
+
label: 'Sunflower',
|
|
69
|
+
layers: [
|
|
70
|
+
{ count: 16, lift: 10, shape: 'teardrop', width: 0.24 },
|
|
71
|
+
{ count: 16, lift: 16, offset: 0.5, scale: 0.82, shade: 0.8, shape: 'teardrop', width: 0.24 },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
center: { color: '#2f2a33', radius: 11 },
|
|
76
|
+
color: [0.86, 0.19, 0.14],
|
|
77
|
+
height: 0.45,
|
|
78
|
+
icon: '🌺',
|
|
79
|
+
headDiameter: 0.1,
|
|
80
|
+
curl: 0.3,
|
|
81
|
+
faceUp: 1.0,
|
|
82
|
+
id: 'poppy',
|
|
83
|
+
label: 'Poppy',
|
|
84
|
+
layers: [{ count: 4, lift: 40, shape: 'round', width: 1.0 }],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
color: [0.82, 0.16, 0.28],
|
|
88
|
+
height: 0.5,
|
|
89
|
+
icon: '🌹',
|
|
90
|
+
headDiameter: 0.09,
|
|
91
|
+
curl: 0.45,
|
|
92
|
+
faceUp: 0.55,
|
|
93
|
+
id: 'rose',
|
|
94
|
+
label: 'Rose',
|
|
95
|
+
layers: [
|
|
96
|
+
{ count: 7, lift: 30, shape: 'round', width: 0.62 },
|
|
97
|
+
{ count: 5, lift: 48, offset: 0.5, scale: 0.8, shade: 0.8, shape: 'round', width: 0.62 },
|
|
98
|
+
{ count: 3, lift: 68, offset: 0.25, scale: 0.55, shade: 0.62, shape: 'round', width: 0.62 },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
center: { color: '#3b3f7a', radius: 9 },
|
|
103
|
+
color: [0.35, 0.47, 0.9],
|
|
104
|
+
height: 0.4,
|
|
105
|
+
icon: '💠',
|
|
106
|
+
headDiameter: 0.05,
|
|
107
|
+
curl: 0.2,
|
|
108
|
+
faceUp: 0.9,
|
|
109
|
+
id: 'cornflower',
|
|
110
|
+
label: 'Cornflower',
|
|
111
|
+
layers: [{ count: 8, lift: 26, shape: 'maple', width: 0.42 }],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
center: { color: '#e8a616', radius: 11 },
|
|
115
|
+
color: [0.99, 0.85, 0.2],
|
|
116
|
+
height: 0.25,
|
|
117
|
+
icon: '✿',
|
|
118
|
+
headDiameter: 0.045,
|
|
119
|
+
curl: 0.3,
|
|
120
|
+
faceUp: 1.2,
|
|
121
|
+
id: 'buttercup',
|
|
122
|
+
label: 'Buttercup',
|
|
123
|
+
layers: [{ count: 5, lift: 36, shape: 'round', width: 0.55 }],
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
function channelHex(channel) {
|
|
128
|
+
return Math.round(Math.min(Math.max(channel, 0), 1) * 255).toString(16).padStart(2, '0');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function toRgb(color) {
|
|
132
|
+
if (Array.isArray(color)) return color;
|
|
133
|
+
const int = parseInt(String(color).replace('#', ''), 16);
|
|
134
|
+
return [((int >> 16) & 255) / 255, ((int >> 8) & 255) / 255, (int & 255) / 255];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function rgbHex(rgb, shade = 1) {
|
|
138
|
+
return `#${rgb.map((channel) => channelHex(channel * shade)).join('')}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function paintGeometry(geometry, [r, g, b]) {
|
|
142
|
+
const count = geometry.getAttribute('position').count;
|
|
143
|
+
const colors = new Float32Array(count * 3);
|
|
144
|
+
for (let i = 0; i < count; i += 1) {
|
|
145
|
+
colors[i * 3] = r;
|
|
146
|
+
colors[i * 3 + 1] = g;
|
|
147
|
+
colors[i * 3 + 2] = b;
|
|
148
|
+
}
|
|
149
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
150
|
+
return geometry;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Builds a real 3D head mesh for a species (EZ-Tree ships small flower
|
|
155
|
+
* MODELS; ours are procedural): curved petal planes arranged in rings —
|
|
156
|
+
* `lift` degrees above flat per layer (a daisy ring lies almost flat, a
|
|
157
|
+
* tulip's petals stand up into a cup), species `curl` bowing each petal
|
|
158
|
+
* toward the axis — plus a center dome. Unit size (head diameter ≈ 1,
|
|
159
|
+
* axis = +Z), vertex-colored, ready for a vertexColors toon material.
|
|
160
|
+
*
|
|
161
|
+
* @param {Object} [options] `species` (FLOWER_SPECIES id) and optional
|
|
162
|
+
* `color` petal override (sRGB triplet or hex).
|
|
163
|
+
* @returns {THREE.BufferGeometry} merged head geometry.
|
|
164
|
+
*/
|
|
165
|
+
export function createFlowerHeadGeometry({ species, color } = {}) {
|
|
166
|
+
const spec = FLOWER_SPECIES.find((entry) => entry.id === species)
|
|
167
|
+
?? FLOWER_SPECIES[0];
|
|
168
|
+
const rgb = toRgb(color ?? spec.color);
|
|
169
|
+
const curl = spec.curl ?? 0.2;
|
|
170
|
+
// Sprite center radii are px of a 128px canvas; 64px = unit head radius 0.5.
|
|
171
|
+
const centerRadius = spec.center ? (spec.center.radius / 64) * 0.5 : 0;
|
|
172
|
+
const petalBase = centerRadius * 0.75 + 0.02;
|
|
173
|
+
const pieces = [];
|
|
174
|
+
|
|
175
|
+
for (const layer of spec.layers) {
|
|
176
|
+
const lift = THREE.MathUtils.degToRad(layer.lift ?? 20);
|
|
177
|
+
const layerScale = layer.scale ?? 1;
|
|
178
|
+
const length = Math.min(
|
|
179
|
+
(0.5 * layerScale - petalBase) / Math.max(Math.cos(lift), 0.35),
|
|
180
|
+
0.8 * layerScale,
|
|
181
|
+
);
|
|
182
|
+
const width = length * (layer.width ?? 0.5);
|
|
183
|
+
const shade = layer.shade ?? 1;
|
|
184
|
+
const petalColor = [rgb[0] * shade, rgb[1] * shade, rgb[2] * shade];
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < layer.count; i += 1) {
|
|
187
|
+
const petal = new THREE.PlaneGeometry(width, length, 1, 5);
|
|
188
|
+
petal.translate(0, length / 2, 0); // base at the origin
|
|
189
|
+
const position = petal.getAttribute('position');
|
|
190
|
+
for (let v = 0; v < position.count; v += 1) {
|
|
191
|
+
const t = position.getY(v) / length;
|
|
192
|
+
// Rounded petal silhouette + curl bowing the tip toward the axis.
|
|
193
|
+
position.setX(v, position.getX(v) * Math.sin(Math.PI * (0.12 + 0.88 * t)) ** 0.7);
|
|
194
|
+
position.setZ(v, curl * length * t * t);
|
|
195
|
+
}
|
|
196
|
+
petal.computeVertexNormals();
|
|
197
|
+
const azimuth = ((i + (layer.offset ?? 0)) / layer.count) * Math.PI * 2;
|
|
198
|
+
petal.applyMatrix4(new THREE.Matrix4()
|
|
199
|
+
.makeRotationZ(azimuth)
|
|
200
|
+
.multiply(new THREE.Matrix4().makeTranslation(0, petalBase, 0))
|
|
201
|
+
.multiply(new THREE.Matrix4().makeRotationX(lift)));
|
|
202
|
+
pieces.push(paintGeometry(petal, petalColor));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (spec.center) {
|
|
207
|
+
const dome = new THREE.SphereGeometry(centerRadius, 10, 5, 0, Math.PI * 2, 0, Math.PI / 2);
|
|
208
|
+
dome.rotateX(Math.PI / 2);
|
|
209
|
+
dome.scale(1, 1, 0.45);
|
|
210
|
+
pieces.push(paintGeometry(dome, toRgb(spec.center.color)));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const merged = mergeGeometries(pieces);
|
|
214
|
+
pieces.forEach((piece) => piece.dispose());
|
|
215
|
+
return merged;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Builds a 128×128 head sprite for a species (or a hand-drawn petal
|
|
220
|
+
* outline). `petal.preset` is a FLOWER_SPECIES id or 'custom' (with
|
|
221
|
+
* `petal.outline`); `color` overrides the species petal color (sRGB triplet
|
|
222
|
+
* or hex — falls back to the species default when null).
|
|
223
|
+
*/
|
|
224
|
+
export function createFlowerHeadTexture({ color, petal }) {
|
|
225
|
+
const spec = FLOWER_SPECIES.find((entry) => entry.id === petal?.preset)
|
|
226
|
+
?? FLOWER_SPECIES[0];
|
|
227
|
+
const outline = petal?.preset === 'custom' ? petal.outline : null;
|
|
228
|
+
const rgb = toRgb(color ?? spec.color);
|
|
229
|
+
const layers = outline
|
|
230
|
+
? [{ count: 5, shape: 'custom', width: 0.7 }]
|
|
231
|
+
: spec.layers;
|
|
232
|
+
|
|
233
|
+
const canvas = document.createElement('canvas');
|
|
234
|
+
canvas.width = 128;
|
|
235
|
+
canvas.height = 128;
|
|
236
|
+
const ctx = canvas.getContext('2d');
|
|
237
|
+
ctx.translate(64, 64);
|
|
238
|
+
layers.forEach((layer) => {
|
|
239
|
+
const scale = layer.scale ?? 1;
|
|
240
|
+
ctx.fillStyle = rgbHex(rgb, layer.shade ?? 1);
|
|
241
|
+
for (let i = 0; i < layer.count; i += 1) {
|
|
242
|
+
ctx.save();
|
|
243
|
+
ctx.rotate(((i + (layer.offset ?? 0)) / layer.count) * Math.PI * 2);
|
|
244
|
+
ctx.translate(0, -30 * scale);
|
|
245
|
+
traceLeafShapePath(ctx, layer.shape, 58 * scale, 58 * scale * layer.width, outline);
|
|
246
|
+
ctx.fill();
|
|
247
|
+
ctx.restore();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
const center = outline ? { color: '#f0c437', radius: 13 } : spec.center;
|
|
251
|
+
if (center) {
|
|
252
|
+
ctx.fillStyle = center.color;
|
|
253
|
+
ctx.beginPath();
|
|
254
|
+
ctx.arc(0, 0, center.radius, 0, Math.PI * 2);
|
|
255
|
+
ctx.fill();
|
|
256
|
+
}
|
|
257
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
258
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
259
|
+
return texture;
|
|
260
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Stylized vegetation barrel. Import from '@call-me-sensei/toonlab/vegetation'.
|
|
2
|
+
export * from './stylizedGrass.js';
|
|
3
|
+
export * from './stylizedFlowers.js';
|
|
4
|
+
export * from './flowerSpecies.js';
|
|
5
|
+
export * from './stylizedTree.js';
|
|
6
|
+
export * from './stylizedTreeFoliage.js';
|
|
7
|
+
export * from './stylizedBush.js';
|
|
8
|
+
export * from './stylizedFlower.js';
|
|
9
|
+
export * from './treeRecipe.js';
|
|
10
|
+
export * from './treeExport.js';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createCanopyBlobs,
|
|
5
|
+
createTreeFoliageGeometry,
|
|
6
|
+
createTreeFoliageMaterials,
|
|
7
|
+
setCanopyCloudShadow,
|
|
8
|
+
setCanopySceneShadow,
|
|
9
|
+
setCanopySun,
|
|
10
|
+
setCanopyWind,
|
|
11
|
+
tickCanopyTime,
|
|
12
|
+
} from './stylizedTreeFoliage.js';
|
|
13
|
+
import {
|
|
14
|
+
TREE_RECIPE_SCHEMA,
|
|
15
|
+
TREE_RECIPE_VERSION,
|
|
16
|
+
serializableTreeOptions,
|
|
17
|
+
} from './stylizedTree.js';
|
|
18
|
+
|
|
19
|
+
// Trunkless plant: the tree's leaf-card canopy resting on the ground —
|
|
20
|
+
// bushes, shrubs, hedges. Same deterministic seeding and the same runtime
|
|
21
|
+
// API as StylizedTree (setSun/setWind/update/...), so scenes and the tree
|
|
22
|
+
// designer treat plants polymorphically:
|
|
23
|
+
//
|
|
24
|
+
// const bush = new StylizedBush({ size: 0.8, seed: 4, canopyColor: 0x4da258 });
|
|
25
|
+
// scene.add(bush);
|
|
26
|
+
// bush.update(delta);
|
|
27
|
+
//
|
|
28
|
+
// width / depth — X / Z reach multipliers (hedges: width up, depth down)
|
|
29
|
+
// flatten — vertical squash; low values give low, spreading shrubs
|
|
30
|
+
// canopyLayout — extra createCanopyBlobs overrides (lobeCount, spread, ...)
|
|
31
|
+
// canopy — createTreeFoliageGeometry options (cardCount, ...)
|
|
32
|
+
// foliage — createTreeFoliageMaterials options (cutoff, wind, sun, ...)
|
|
33
|
+
export class StylizedBush extends THREE.Group {
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
super();
|
|
36
|
+
const {
|
|
37
|
+
size = 1,
|
|
38
|
+
seed = 1,
|
|
39
|
+
canopyColor = 0x4da258,
|
|
40
|
+
canopyPalette = {},
|
|
41
|
+
width = 1.3,
|
|
42
|
+
depth = 1.1,
|
|
43
|
+
flatten = 0.35,
|
|
44
|
+
leafDensity = 1,
|
|
45
|
+
canopy = {},
|
|
46
|
+
canopyLayout = {},
|
|
47
|
+
foliage = {},
|
|
48
|
+
} = options;
|
|
49
|
+
this.name = 'StylizedBush';
|
|
50
|
+
// Kept for toJSON(); not cloned (see StylizedTree.config).
|
|
51
|
+
this.config = options;
|
|
52
|
+
|
|
53
|
+
// Squat layout: smaller core and tighter lobes than a tree crown, so a
|
|
54
|
+
// size-1 bush reads hip-high rather than like a floating tree top.
|
|
55
|
+
const coreRadius = canopyLayout.coreRadius ?? 0.7;
|
|
56
|
+
const blobs = canopy.blobs ?? createCanopyBlobs({
|
|
57
|
+
seed,
|
|
58
|
+
width,
|
|
59
|
+
depth,
|
|
60
|
+
flatten,
|
|
61
|
+
spread: 1.0,
|
|
62
|
+
coreRadius,
|
|
63
|
+
lobeRadiusRange: [0.35, 0.6],
|
|
64
|
+
...canopyLayout,
|
|
65
|
+
});
|
|
66
|
+
const geometry = createTreeFoliageGeometry({
|
|
67
|
+
seed: seed * 7.31 + 1.7,
|
|
68
|
+
leafDensity,
|
|
69
|
+
attachments: null,
|
|
70
|
+
coverageScale: size,
|
|
71
|
+
cardSizeRange: [0.7, 1.1],
|
|
72
|
+
...canopy,
|
|
73
|
+
blobs,
|
|
74
|
+
});
|
|
75
|
+
const materials = createTreeFoliageMaterials({
|
|
76
|
+
color: canopyColor,
|
|
77
|
+
palette: canopyPalette,
|
|
78
|
+
seed,
|
|
79
|
+
...foliage,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.canopyMesh = new THREE.Mesh(geometry, materials.material);
|
|
83
|
+
this.canopyMesh.customDepthMaterial = materials.depthMaterial;
|
|
84
|
+
this.canopyMesh.castShadow = true;
|
|
85
|
+
this.canopyMesh.receiveShadow = true;
|
|
86
|
+
this.canopyMesh.frustumCulled = false;
|
|
87
|
+
// Keep the loaded-scene material adapter from replacing the leaf shader.
|
|
88
|
+
this.canopyMesh.userData.environmentShaderExclude = true;
|
|
89
|
+
// Rest the mass on the ground instead of centering it on the origin.
|
|
90
|
+
this.canopyMesh.position.y = coreRadius * 0.85;
|
|
91
|
+
|
|
92
|
+
this.add(this.canopyMesh);
|
|
93
|
+
this.scale.setScalar(size);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setSun(options) {
|
|
97
|
+
setCanopySun(this.canopyMesh.material.uniforms, options ?? {});
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setWind(options) {
|
|
102
|
+
setCanopyWind(this.canopyMesh.material.uniforms, options);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setSceneShadow(options) {
|
|
107
|
+
setCanopySceneShadow(this.canopyMesh.material.uniforms, options);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setCloudShadow(options) {
|
|
112
|
+
setCanopyCloudShadow(this.canopyMesh.material.uniforms, options);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
update(delta) {
|
|
117
|
+
tickCanopyTime(this.canopyMesh.material.uniforms, delta);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Recipe document that rebuilds this exact bush (see StylizedTree.toJSON).
|
|
122
|
+
toJSON() {
|
|
123
|
+
return {
|
|
124
|
+
schema: TREE_RECIPE_SCHEMA,
|
|
125
|
+
version: TREE_RECIPE_VERSION,
|
|
126
|
+
type: 'bush',
|
|
127
|
+
options: serializableTreeOptions(this.config),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
dispose() {
|
|
132
|
+
this.canopyMesh.geometry.dispose();
|
|
133
|
+
this.canopyMesh.material.dispose();
|
|
134
|
+
this.canopyMesh.customDepthMaterial?.dispose();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
StylizedTree,
|
|
5
|
+
TREE_RECIPE_SCHEMA,
|
|
6
|
+
TREE_RECIPE_VERSION,
|
|
7
|
+
serializableTreeOptions,
|
|
8
|
+
} from './stylizedTree.js';
|
|
9
|
+
import { FLOWER_SPECIES, createFlowerHeadGeometry } from './flowerSpecies.js';
|
|
10
|
+
import {
|
|
11
|
+
createFlowerBloomNodeMaterial,
|
|
12
|
+
createFlowerStemNodeMaterial,
|
|
13
|
+
} from '../shaders-tsl/flower.js';
|
|
14
|
+
|
|
15
|
+
// Horizontal heading of the lab's default sun ([0.35, 0.75, 0.5]): blooms
|
|
16
|
+
// bake their facing toward it at build time (heliotropism), fixed in world
|
|
17
|
+
// space.
|
|
18
|
+
const SUN_HEADING = new THREE.Vector3(0.45, 0, 0.65).normalize();
|
|
19
|
+
const UP = new THREE.Vector3(0, 1, 0);
|
|
20
|
+
|
|
21
|
+
// Deterministic head-orientation rng (mulberry32-alike, tiny) — same recipe,
|
|
22
|
+
// same blooms.
|
|
23
|
+
function rng(seed) {
|
|
24
|
+
let state = seed >>> 0;
|
|
25
|
+
return () => {
|
|
26
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
27
|
+
let t = Math.imul(state ^ (state >>> 15), 1 | state);
|
|
28
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
29
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A flowering plant as a first-class plant type: the full StylizedTree
|
|
34
|
+
// machinery — its own stem (trunk), branch skeleton, and leaf-card tufts —
|
|
35
|
+
// re-proportioned to read as a flower, with bloom heads from the
|
|
36
|
+
// FLOWER_SPECIES catalog at every branch tip. Same polymorphic runtime API
|
|
37
|
+
// as StylizedTree/StylizedBush (setSun/setWind/update/toJSON/dispose), so
|
|
38
|
+
// scenes and the tree designer treat all plant types alike:
|
|
39
|
+
//
|
|
40
|
+
// const flower = new StylizedFlower({ species: 'sunflower', seed: 4 });
|
|
41
|
+
// scene.add(flower);
|
|
42
|
+
// flower.update(delta);
|
|
43
|
+
//
|
|
44
|
+
// species — FLOWER_SPECIES id ('daisy', 'poppy', 'rose', ...)
|
|
45
|
+
// headColor — petal color override (sRGB triplet); null = species default
|
|
46
|
+
// headScale — bloom size relative to the leaf tufts
|
|
47
|
+
// stemColor — toon stem/branch color
|
|
48
|
+
// ...rest — any StylizedTree option (trunk, skeleton, canopy, ...)
|
|
49
|
+
export class StylizedFlower extends StylizedTree {
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
const source = options && typeof options === 'object' ? options : {};
|
|
52
|
+
const {
|
|
53
|
+
species = 'daisy',
|
|
54
|
+
headColor = null,
|
|
55
|
+
headScale = 1,
|
|
56
|
+
stemColor = 0x4d8a3f,
|
|
57
|
+
...treeOptions
|
|
58
|
+
} = source;
|
|
59
|
+
|
|
60
|
+
// Flowering-plant proportions layered over the incoming tree options:
|
|
61
|
+
// whatever trunk/skeleton settings arrive, the stem stays thin and green
|
|
62
|
+
// and the leaf tufts stay small, so switching a tree recipe's type to
|
|
63
|
+
// 'flower' always reads as a flower rather than a green tree.
|
|
64
|
+
const trunk = { ...treeOptions.trunk };
|
|
65
|
+
// Stem height is capped in WORLD meters (branches and blooms extend
|
|
66
|
+
// past the stem, so a 1.2m stem tops out around head height): tree
|
|
67
|
+
// recipes switched to 'flower' stay human-scale instead of inheriting
|
|
68
|
+
// a tree-sized trunk.
|
|
69
|
+
const plantSize = treeOptions.size ?? 1;
|
|
70
|
+
trunk.height = Math.min(trunk.height ?? 1.2, 1.2 / Math.max(plantSize, 0.1));
|
|
71
|
+
trunk.radiusBottom = Math.min(trunk.radiusBottom ?? 0.045, 0.07);
|
|
72
|
+
super({
|
|
73
|
+
canopyColor: 0x4d8a3f,
|
|
74
|
+
leafDensity: 0.7,
|
|
75
|
+
...treeOptions,
|
|
76
|
+
trunk,
|
|
77
|
+
canopyScale: Math.min(treeOptions.canopyScale ?? 0.45, 0.7),
|
|
78
|
+
canopy: {
|
|
79
|
+
cardSizeRange: [0.32, 0.5],
|
|
80
|
+
cardsPerCluster: 4,
|
|
81
|
+
clusterRadius: 0.2,
|
|
82
|
+
...treeOptions.canopy,
|
|
83
|
+
},
|
|
84
|
+
skeleton: {
|
|
85
|
+
radialSegments: 6,
|
|
86
|
+
tipRadius: 0.012,
|
|
87
|
+
minLimbRadius: 0.01,
|
|
88
|
+
...treeOptions.skeleton,
|
|
89
|
+
},
|
|
90
|
+
trunkMaterial: createFlowerStemNodeMaterial({
|
|
91
|
+
color: stemColor,
|
|
92
|
+
height: trunk.height,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
this.name = 'StylizedFlower';
|
|
96
|
+
// super() stored the re-proportioned options; toJSON must round-trip the
|
|
97
|
+
// ORIGINAL flower options so the recipe rebuilds this exact plant.
|
|
98
|
+
this.config = source;
|
|
99
|
+
|
|
100
|
+
// Blooms grow from branch tips. foliageAttachments are canopyMesh-local
|
|
101
|
+
// and index-aligned with the leaf tufts, so blooms sit where the twigs
|
|
102
|
+
// end and inherit the canopy anchor/scale by parenting. Heads are
|
|
103
|
+
// camera-facing billboard instances (iOrigin/iInfo), so a bloom never
|
|
104
|
+
// renders as an edge-on streak.
|
|
105
|
+
const spec = FLOWER_SPECIES.find((entry) => entry.id === species)
|
|
106
|
+
?? FLOWER_SPECIES.find((entry) => entry.id === 'daisy');
|
|
107
|
+
const attachments = this.foliageAttachments;
|
|
108
|
+
const size = this.settings.tree.size;
|
|
109
|
+
const canopyScale = this.canopyMesh.scale.x || 1;
|
|
110
|
+
// Real-world bloom size: species headDiameter is in METERS, converted to
|
|
111
|
+
// canopy-local units, so a daisy stays palm-sized and a sunflower
|
|
112
|
+
// plate-sized no matter how the plant is scaled or the crown laid out.
|
|
113
|
+
const headSize = (spec.headDiameter ?? 0.08) * headScale
|
|
114
|
+
/ Math.max(size * canopyScale, 1e-3);
|
|
115
|
+
|
|
116
|
+
const random = rng((this.settings.tree.seed * 131 + 17) >>> 0);
|
|
117
|
+
const direction = new THREE.Vector3();
|
|
118
|
+
const zAxis = new THREE.Vector3(0, 0, 1);
|
|
119
|
+
const placed = [];
|
|
120
|
+
attachments.forEach((attachment) => {
|
|
121
|
+
const jitter = 0.8 + random() * 0.4;
|
|
122
|
+
const phase = random();
|
|
123
|
+
// Clear of the leaf tuft along the twig so leaf cards don't slice
|
|
124
|
+
// through the petals: half a head plus a tuft-sized clearance
|
|
125
|
+
// (clusterRadius + card reach, in canopy-local units).
|
|
126
|
+
direction.copy(attachment.direction ?? zAxis).normalize();
|
|
127
|
+
const center = attachment.position.clone()
|
|
128
|
+
.addScaledVector(direction, headSize * jitter * 0.5 + 0.3);
|
|
129
|
+
const radius = (headSize * jitter) / 2;
|
|
130
|
+
// Bloom-vs-bloom collision: skip heads that would overlap an already
|
|
131
|
+
// placed one, so crowded twig clusters read as distinct flowers.
|
|
132
|
+
const collides = placed.some((other) =>
|
|
133
|
+
other.center.distanceTo(center) < (other.radius + radius) * 0.85);
|
|
134
|
+
if (collides) return;
|
|
135
|
+
// World-fixed facing (never follows the camera): heliotropism — every
|
|
136
|
+
// bloom leans toward the scene's sun heading (the lab's default sun
|
|
137
|
+
// azimuth), lifted by the species' faceUp bias and varied a little by
|
|
138
|
+
// its own twig. Sunflowers all face the sun near-horizontally; tulip
|
|
139
|
+
// cups open skyward.
|
|
140
|
+
const tilt = SUN_HEADING.clone()
|
|
141
|
+
.addScaledVector(direction, 0.35)
|
|
142
|
+
.add(UP.clone().multiplyScalar(spec.faceUp ?? 0.8))
|
|
143
|
+
.normalize();
|
|
144
|
+
placed.push({ center, phase, radius, size: headSize * jitter, tilt });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Leaf/bloom collision: collapse any leaf card whose billboard extent
|
|
148
|
+
// would slice through a bloom. Cards expand aInfo.x around their center
|
|
149
|
+
// in the vertex shader, so zeroing aInfo.x on all four corners removes
|
|
150
|
+
// the card without touching the index buffer.
|
|
151
|
+
if (placed.length) {
|
|
152
|
+
const cardCenters = this.canopyMesh.geometry.getAttribute('position');
|
|
153
|
+
const cardInfo = this.canopyMesh.geometry.getAttribute('aInfo');
|
|
154
|
+
const cardCenter = new THREE.Vector3();
|
|
155
|
+
for (let vertex = 0; vertex < cardCenters.count; vertex += 4) {
|
|
156
|
+
cardCenter.fromBufferAttribute(cardCenters, vertex);
|
|
157
|
+
const reach = cardInfo.getX(vertex) * 0.45;
|
|
158
|
+
const hit = placed.some((head) =>
|
|
159
|
+
cardCenter.distanceTo(head.center) < head.radius + reach);
|
|
160
|
+
if (hit) {
|
|
161
|
+
for (let corner = 0; corner < 4; corner += 1) cardInfo.setX(vertex + corner, 0);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
cardInfo.needsUpdate = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Real 3D bloom meshes (EZ-Tree-style flower models, built procedurally):
|
|
168
|
+
// curved petals + center dome, vertex-colored, world-anchored — each
|
|
169
|
+
// bloom faces up-and-out along its twig like a real flower head instead
|
|
170
|
+
// of tracking the camera.
|
|
171
|
+
const headMaterial = createFlowerBloomNodeMaterial({
|
|
172
|
+
windSpeed: 1.1,
|
|
173
|
+
windStrength: headSize * 0.04,
|
|
174
|
+
});
|
|
175
|
+
const heads = new THREE.InstancedMesh(
|
|
176
|
+
createFlowerHeadGeometry({ color: headColor, species: spec.id }),
|
|
177
|
+
headMaterial, placed.length);
|
|
178
|
+
heads.name = 'StylizedFlowerHeads';
|
|
179
|
+
heads.frustumCulled = false;
|
|
180
|
+
heads.userData.environmentShaderExclude = true;
|
|
181
|
+
|
|
182
|
+
const matrix = new THREE.Matrix4();
|
|
183
|
+
const facing = new THREE.Quaternion();
|
|
184
|
+
const roll = new THREE.Quaternion();
|
|
185
|
+
const scale = new THREE.Vector3();
|
|
186
|
+
placed.forEach((head, index) => {
|
|
187
|
+
direction.copy(head.tilt);
|
|
188
|
+
facing.setFromUnitVectors(zAxis, direction);
|
|
189
|
+
roll.setFromAxisAngle(direction, head.phase * Math.PI * 2);
|
|
190
|
+
scale.setScalar(head.size);
|
|
191
|
+
matrix.compose(head.center, roll.multiply(facing), scale);
|
|
192
|
+
heads.setMatrixAt(index, matrix);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.headsMesh = heads;
|
|
196
|
+
this.canopyMesh.add(heads);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
toJSON() {
|
|
200
|
+
return {
|
|
201
|
+
schema: TREE_RECIPE_SCHEMA,
|
|
202
|
+
version: TREE_RECIPE_VERSION,
|
|
203
|
+
type: 'flower',
|
|
204
|
+
options: serializableTreeOptions(this.config),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
dispose() {
|
|
209
|
+
this.headsMesh.geometry.dispose();
|
|
210
|
+
this.headsMesh.material.dispose();
|
|
211
|
+
super.dispose();
|
|
212
|
+
}
|
|
213
|
+
}
|