@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,691 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createTreeLeafNodeMaterial } from '../shaders-tsl/tree-leaf.js';
|
|
4
|
+
|
|
5
|
+
// Modern anime-style tree canopies: instead of a lumpy low-poly blob, the crown is
|
|
6
|
+
// built from hundreds of camera-facing leaf-cluster cards sampled over a set
|
|
7
|
+
// of overlapping sphere "blobs". Each card's shading normal is baked to point
|
|
8
|
+
// outward from the canopy core, so the toon ramp lights the whole crown as a
|
|
9
|
+
// single soft volume — the core trick behind modern anime style trees.
|
|
10
|
+
//
|
|
11
|
+
// const geometry = createTreeFoliageGeometry({ seed: 3 });
|
|
12
|
+
// const { material, depthMaterial } = createTreeFoliageMaterials({ color: 0x4da258 });
|
|
13
|
+
// const canopy = new THREE.Mesh(geometry, material);
|
|
14
|
+
// canopy.customDepthMaterial = depthMaterial; // leafy cast shadows
|
|
15
|
+
// canopy.castShadow = true;
|
|
16
|
+
// material.uniforms.uTime.value += delta; // each frame
|
|
17
|
+
|
|
18
|
+
// Deterministic RNG so tree variants are stable across reloads.
|
|
19
|
+
function mulberry32(seed) {
|
|
20
|
+
let state = Math.floor(seed * 1e6) >>> 0;
|
|
21
|
+
return () => {
|
|
22
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
23
|
+
let t = state;
|
|
24
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
25
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
26
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback blob layout: one big core with satellite lobes. Prefer
|
|
31
|
+
// createCanopyBlobs() for real trees — identical blobs make every crown the
|
|
32
|
+
// same near-sphere.
|
|
33
|
+
export const TREE_FOLIAGE_BLOBS = [
|
|
34
|
+
{ offset: [0, 0, 0], radius: 1.0 },
|
|
35
|
+
{ offset: [-0.58, -0.3, 0.12], radius: 0.62 },
|
|
36
|
+
{ offset: [0.54, -0.34, -0.16], radius: 0.58 },
|
|
37
|
+
{ offset: [0.14, 0.48, 0.06], radius: 0.56 },
|
|
38
|
+
{ offset: [-0.2, -0.12, 0.55], radius: 0.5 },
|
|
39
|
+
{ offset: [0.08, -0.2, -0.56], radius: 0.48 },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Seeded irregular crown layout, anime-style: wider than tall, asymmetric
|
|
43
|
+
// lobes ringing the core at varying heights, and sometimes an outrigger tuft
|
|
44
|
+
// on a long limb past the main mass. Feed the result to both
|
|
45
|
+
// createTreeSkeleton and createTreeFoliageGeometry so limbs and leaves agree.
|
|
46
|
+
// spread — horizontal reach of the lobes (1 ≈ core radius)
|
|
47
|
+
// width — X-axis reach multiplier; depth — Z-axis reach multiplier.
|
|
48
|
+
// width 1.6 / depth 0.7 gives a wide, shallow hedge-like crown.
|
|
49
|
+
// flatten — vertical squash; low values give the wide, layered look
|
|
50
|
+
export function createCanopyBlobs({
|
|
51
|
+
lobeCount = 6,
|
|
52
|
+
spread = 1.25,
|
|
53
|
+
width = 1,
|
|
54
|
+
depth = 1,
|
|
55
|
+
flatten = 0.5,
|
|
56
|
+
coreRadius = 0.9,
|
|
57
|
+
lobeRadiusRange = [0.45, 0.78],
|
|
58
|
+
outriggerChance = 0.65,
|
|
59
|
+
seed = 1,
|
|
60
|
+
} = {}) {
|
|
61
|
+
const rng = mulberry32(seed + 5.13);
|
|
62
|
+
const blobs = [{ offset: [0, 0.05, 0], radius: coreRadius }];
|
|
63
|
+
for (let i = 0; i < lobeCount; i += 1) {
|
|
64
|
+
const angle = (i / lobeCount) * Math.PI * 2 + (rng() - 0.5) * 1.4;
|
|
65
|
+
const distance = spread * (0.5 + rng() * 0.55);
|
|
66
|
+
blobs.push({
|
|
67
|
+
offset: [
|
|
68
|
+
Math.cos(angle) * distance * width,
|
|
69
|
+
(rng() * 2 - 0.65) * flatten,
|
|
70
|
+
Math.sin(angle) * distance * depth,
|
|
71
|
+
],
|
|
72
|
+
radius: THREE.MathUtils.lerp(lobeRadiusRange[0], lobeRadiusRange[1], rng()),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (rng() < outriggerChance) {
|
|
76
|
+
const angle = rng() * Math.PI * 2;
|
|
77
|
+
blobs.push({
|
|
78
|
+
offset: [
|
|
79
|
+
Math.cos(angle) * spread * 1.7 * width,
|
|
80
|
+
(rng() - 0.6) * flatten,
|
|
81
|
+
Math.sin(angle) * spread * 1.7 * depth,
|
|
82
|
+
],
|
|
83
|
+
radius: lobeRadiusRange[0] * (0.8 + rng() * 0.4),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return blobs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Alpha-cutout leaf-cluster sprite drawn at runtime (no texture assets).
|
|
90
|
+
// The card must read as FOLIAGE: a dense mass of many distinct small leaves.
|
|
91
|
+
// Heavy overlap keeps the middle solid (so crowns never look like confetti)
|
|
92
|
+
// while individual leaf silhouettes stay visible on the rim and as strong
|
|
93
|
+
// per-leaf luminance variation inside (so it never looks like a smooth
|
|
94
|
+
// cloud-blob either). Leaves are drawn inside-out: dark random-facing
|
|
95
|
+
// interior leaves first, bright outward-pointing rim leaves on top.
|
|
96
|
+
// RGB holds per-leaf luminance, alpha holds the cutout silhouette.
|
|
97
|
+
/**
|
|
98
|
+
* Traces one leaf silhouette into the 2D context, centered at the origin,
|
|
99
|
+
* `length` tall along Y and `width` wide. Shared by the crown sprite, the
|
|
100
|
+
* designer's shape previews, and the falling-leaf particles, so a shape
|
|
101
|
+
* choice looks identical everywhere.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} shape 'teardrop'|'round'|'maple'|'gingko'|'needle'|'custom'
|
|
104
|
+
* @param {Array<[number, number]>|null} outline Normalized custom outline
|
|
105
|
+
* (points in -0.5..0.5 leaf-local space, y = tip direction).
|
|
106
|
+
*/
|
|
107
|
+
export function traceLeafShapePath(ctx, shape, length, width, outline = null) {
|
|
108
|
+
ctx.beginPath();
|
|
109
|
+
if (shape === 'custom' && outline?.length >= 3) {
|
|
110
|
+
ctx.moveTo(outline[0][0] * width, outline[0][1] * length);
|
|
111
|
+
for (let i = 1; i < outline.length; i += 1) {
|
|
112
|
+
ctx.lineTo(outline[i][0] * width, outline[i][1] * length);
|
|
113
|
+
}
|
|
114
|
+
} else if (shape === 'round') {
|
|
115
|
+
ctx.ellipse(0, 0, width * 0.52, length * 0.5, 0, 0, Math.PI * 2);
|
|
116
|
+
} else if (shape === 'needle') {
|
|
117
|
+
ctx.moveTo(0, -length * 0.5);
|
|
118
|
+
ctx.quadraticCurveTo(width * 0.16, 0, 0, length * 0.5);
|
|
119
|
+
ctx.quadraticCurveTo(-width * 0.16, 0, 0, -length * 0.5);
|
|
120
|
+
} else if (shape === 'maple') {
|
|
121
|
+
// Five-lobed star with pinched sinuses.
|
|
122
|
+
const lobes = 5;
|
|
123
|
+
for (let i = 0; i <= lobes * 2; i += 1) {
|
|
124
|
+
const angle = -Math.PI / 2 + (i * Math.PI) / lobes;
|
|
125
|
+
const radial = i % 2 === 0 ? 0.5 : 0.22;
|
|
126
|
+
const x = Math.cos(angle) * width * radial;
|
|
127
|
+
const y = Math.sin(angle) * length * radial;
|
|
128
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
129
|
+
else ctx.lineTo(x, y);
|
|
130
|
+
}
|
|
131
|
+
} else if (shape === 'gingko') {
|
|
132
|
+
// Fan: narrow stem base opening to a wide notched top edge.
|
|
133
|
+
ctx.moveTo(0, length * 0.5);
|
|
134
|
+
ctx.quadraticCurveTo(width * 0.55, length * 0.25, width * 0.5, -length * 0.3);
|
|
135
|
+
ctx.quadraticCurveTo(width * 0.2, -length * 0.52, 0, -length * 0.34);
|
|
136
|
+
ctx.quadraticCurveTo(-width * 0.2, -length * 0.52, -width * 0.5, -length * 0.3);
|
|
137
|
+
ctx.quadraticCurveTo(-width * 0.55, length * 0.25, 0, length * 0.5);
|
|
138
|
+
} else {
|
|
139
|
+
// Teardrop (default): round base, pointed tip.
|
|
140
|
+
ctx.moveTo(0, -length * 0.5);
|
|
141
|
+
ctx.quadraticCurveTo(width * 0.62, -length * 0.1, 0, length * 0.5);
|
|
142
|
+
ctx.quadraticCurveTo(-width * 0.62, -length * 0.1, 0, -length * 0.5);
|
|
143
|
+
}
|
|
144
|
+
ctx.closePath();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const LEAF_SHAPE_PRESETS = Object.freeze(['teardrop', 'round', 'maple', 'gingko', 'needle']);
|
|
148
|
+
|
|
149
|
+
export function createLeafSpriteTexture({
|
|
150
|
+
size = 512,
|
|
151
|
+
leafCount = 170,
|
|
152
|
+
seed = 7,
|
|
153
|
+
shape = 'teardrop',
|
|
154
|
+
customOutline = null,
|
|
155
|
+
} = {}) {
|
|
156
|
+
const rng = mulberry32(seed);
|
|
157
|
+
const canvas = document.createElement('canvas');
|
|
158
|
+
canvas.width = size;
|
|
159
|
+
canvas.height = size;
|
|
160
|
+
const ctx = canvas.getContext('2d');
|
|
161
|
+
ctx.clearRect(0, 0, size, size);
|
|
162
|
+
|
|
163
|
+
const center = size / 2;
|
|
164
|
+
const drawLeaf = (x, y, length, width, angle, luminance) => {
|
|
165
|
+
const value = Math.round(THREE.MathUtils.clamp(luminance, 0, 1) * 255);
|
|
166
|
+
ctx.fillStyle = `rgb(${value},${value},${value})`;
|
|
167
|
+
ctx.save();
|
|
168
|
+
ctx.translate(x, y);
|
|
169
|
+
ctx.rotate(angle);
|
|
170
|
+
traceLeafShapePath(ctx, shape, length, width, customOutline);
|
|
171
|
+
ctx.fill();
|
|
172
|
+
ctx.restore();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const leaves = [];
|
|
176
|
+
for (let i = 0; i < leafCount; i += 1) {
|
|
177
|
+
const angle = rng() * Math.PI * 2;
|
|
178
|
+
// Clumpy reachable radius so the outline isn't a circle.
|
|
179
|
+
const reach = 0.38 + 0.05 * Math.sin(angle * 3 + seed) + 0.04 * Math.sin(angle * 7 + 1.3);
|
|
180
|
+
const radius = Math.pow(rng(), 0.52) * size * reach;
|
|
181
|
+
const edgeT = Math.min(radius / (size * 0.4), 1);
|
|
182
|
+
leaves.push({ angle, radius, edgeT, pick: rng(), spin: rng(), shade: rng() });
|
|
183
|
+
}
|
|
184
|
+
// Inside-out: rim leaves draw last, on top, with crisp silhouettes.
|
|
185
|
+
leaves.sort((a, b) => a.radius - b.radius);
|
|
186
|
+
leaves.forEach((leaf) => {
|
|
187
|
+
const x = center + Math.cos(leaf.angle) * leaf.radius;
|
|
188
|
+
const y = center + Math.sin(leaf.angle) * leaf.radius;
|
|
189
|
+
const length = size * (0.095 + leaf.pick * 0.06);
|
|
190
|
+
const width = length * (0.55 + leaf.spin * 0.3);
|
|
191
|
+
// Interior leaves: darker, randomly oriented. Rim leaves: brighter,
|
|
192
|
+
// pointing outward like real growth.
|
|
193
|
+
const scatter = (1 - leaf.edgeT) * 2.6 + 0.5;
|
|
194
|
+
const orientation = leaf.angle + Math.PI / 2 + (leaf.spin - 0.5) * scatter;
|
|
195
|
+
const luminance = THREE.MathUtils.lerp(
|
|
196
|
+
0.5 + leaf.shade * 0.22,
|
|
197
|
+
0.76 + leaf.shade * 0.24,
|
|
198
|
+
leaf.edgeT,
|
|
199
|
+
);
|
|
200
|
+
drawLeaf(x, y, length, width, orientation, luminance);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
204
|
+
texture.colorSpace = THREE.NoColorSpace;
|
|
205
|
+
texture.anisotropy = 4;
|
|
206
|
+
return texture;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Merged quad soup: 4 verts per card, position = card center, corners spread
|
|
210
|
+
// in the vertex shader (billboarding). Cards sit on the blob surfaces so the
|
|
211
|
+
// canopy silhouette stays clumpy rather than spherical.
|
|
212
|
+
// Few LARGE heavily-overlapping cards — not many small ones — is what makes
|
|
213
|
+
// crowns read as one fluffy mass (the classic three.js "fluffy trees"
|
|
214
|
+
// recipe). Small-card swarms read as floating confetti.
|
|
215
|
+
export function createTreeFoliageGeometry({
|
|
216
|
+
blobs = TREE_FOLIAGE_BLOBS,
|
|
217
|
+
cardCount = 170,
|
|
218
|
+
cardSizeRange = [1.0, 1.6],
|
|
219
|
+
// Overall canopy size multiplier (1 ≈ 1.6 m crown radius). For large trees
|
|
220
|
+
// prefer raising cardCount alongside size so clusters stay leaf-scaled
|
|
221
|
+
// instead of ballooning with the crown.
|
|
222
|
+
size = 1,
|
|
223
|
+
// Final world scale the tree renders at (object transform × size). Leaves
|
|
224
|
+
// must stay leaf-sized no matter how big the tree gets, so coverage comes
|
|
225
|
+
// from MORE cards, never bigger ones: card count scales with
|
|
226
|
+
// coverageScale^2 (crown surface area), card size stays put.
|
|
227
|
+
coverageScale = 1,
|
|
228
|
+
// 0..1 leaf coverage. 1 = a solid crown; lower values thin the cards
|
|
229
|
+
// overall, open see-through gap pockets in the silhouette, and clear the
|
|
230
|
+
// underside so branches and sky show through from the side or below.
|
|
231
|
+
leafDensity = 1,
|
|
232
|
+
// Branch attachment points ({ position: Vector3, direction: Vector3 } in
|
|
233
|
+
// canopy-local space, e.g. from createTreeSkeleton). When given, leaf
|
|
234
|
+
// clusters grow as tufts around these points — no floating leaves — and
|
|
235
|
+
// gap culling removes whole tufts, exposing the wood beneath. Without
|
|
236
|
+
// attachments, cards sample the blob shells directly.
|
|
237
|
+
attachments = null,
|
|
238
|
+
cardsPerCluster = 5,
|
|
239
|
+
clusterRadius = 0.48,
|
|
240
|
+
// Per-attachment overrides keyed by attachment index:
|
|
241
|
+
// { [index]: { cardsPerCluster?, clusterRadius?, densityScale? } }
|
|
242
|
+
// cardsPerCluster/clusterRadius replace the globals for that tuft;
|
|
243
|
+
// densityScale multiplies its card count (0 = bare branch — the mandatory
|
|
244
|
+
// minimum is waived when an explicit override asks for it).
|
|
245
|
+
attachmentOverrides = null,
|
|
246
|
+
// false = no blob-shell fill between the attachment tufts: the crown is
|
|
247
|
+
// ONLY bushes at the branch ends and the wood in between stays on show
|
|
248
|
+
// (the Sumeru bare-limb look). Only meaningful with attachments.
|
|
249
|
+
shellFill = true,
|
|
250
|
+
// When set, the blob-shell fill ADDS this many cards (scaled by density
|
|
251
|
+
// and coverage) on top of whatever the attachments produced, instead of
|
|
252
|
+
// only topping up to cardCount. Scribbled foliage areas need this: a
|
|
253
|
+
// branch-tuft-heavy tree already exceeds the top-up budget, so painted
|
|
254
|
+
// blobs would otherwise get zero cards.
|
|
255
|
+
shellBudget = null,
|
|
256
|
+
seed = 1,
|
|
257
|
+
} = {}) {
|
|
258
|
+
const rng = mulberry32(seed + 11.7);
|
|
259
|
+
const density = THREE.MathUtils.clamp(leafDensity, 0.05, 2);
|
|
260
|
+
// Gap/culling math below is written for 0..1 coverage; density above 1
|
|
261
|
+
// means "extra lush" and must never produce negative gap counts.
|
|
262
|
+
const openness = Math.max(0, 1 - density);
|
|
263
|
+
const coverage = Math.min(Math.max(coverageScale, 0.4) ** 2, 9);
|
|
264
|
+
|
|
265
|
+
// Gap pockets: a few random directions on the crown where cards are very
|
|
266
|
+
// likely to be culled, so patches of sky read through the canopy.
|
|
267
|
+
const gapCount = Math.max(0, Math.round(1 + openness * 5 + rng() * 2));
|
|
268
|
+
const gaps = Array.from({ length: gapCount }, () => {
|
|
269
|
+
const theta = rng() * Math.PI * 2;
|
|
270
|
+
const cosPhi = rng() * 2 - 1;
|
|
271
|
+
const sinPhi = Math.sqrt(Math.max(0, 1 - cosPhi * cosPhi));
|
|
272
|
+
return {
|
|
273
|
+
direction: new THREE.Vector3(Math.cos(theta) * sinPhi, cosPhi, Math.sin(theta) * sinPhi),
|
|
274
|
+
// Cosine threshold: wider pockets as density drops.
|
|
275
|
+
cosRadius: Math.cos(0.35 + rng() * 0.3 + openness * 0.45),
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Weight blob selection by surface area so lobes get even coverage.
|
|
280
|
+
const weights = blobs.map((blob) => blob.radius * blob.radius);
|
|
281
|
+
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
|
282
|
+
|
|
283
|
+
// Shading normals radiate from a core slightly below the canopy center,
|
|
284
|
+
// which biases normals upward — tops read lit, undersides read shaded.
|
|
285
|
+
const core = new THREE.Vector3(0, -0.35, 0);
|
|
286
|
+
|
|
287
|
+
const cardCenter = new THREE.Vector3();
|
|
288
|
+
const shadeNormal = new THREE.Vector3();
|
|
289
|
+
let minY = Infinity;
|
|
290
|
+
let maxY = -Infinity;
|
|
291
|
+
|
|
292
|
+
const cardData = [];
|
|
293
|
+
const targetCards = Math.round(cardCount * (0.4 + 0.6 * density) * coverage);
|
|
294
|
+
|
|
295
|
+
// Gap pockets + underside thinning; culls by the card's direction from the
|
|
296
|
+
// canopy core, so lower density opens believable holes instead of a
|
|
297
|
+
// uniform thinning.
|
|
298
|
+
const isCulled = (direction) => {
|
|
299
|
+
for (const gap of gaps) {
|
|
300
|
+
if (direction.dot(gap.direction) > gap.cosRadius && rng() < 0.55 + openness * 0.4) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return direction.y < -0.25 && rng() < openness * 0.85;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// attachment = index of the tuft's branch end, -1 for shell-fill cards —
|
|
308
|
+
// baked into the aAttachment vertex attribute so pointer picks can map a
|
|
309
|
+
// leaf card back to its branch.
|
|
310
|
+
const pushCard = (cardRng = rng, attachment = -1) => {
|
|
311
|
+
shadeNormal.copy(cardCenter).sub(core).normalize();
|
|
312
|
+
cardCenter.multiplyScalar(size);
|
|
313
|
+
minY = Math.min(minY, cardCenter.y);
|
|
314
|
+
maxY = Math.max(maxY, cardCenter.y);
|
|
315
|
+
cardData.push({
|
|
316
|
+
attachment,
|
|
317
|
+
center: cardCenter.clone(),
|
|
318
|
+
normal: shadeNormal.clone(),
|
|
319
|
+
size: THREE.MathUtils.lerp(cardSizeRange[0], cardSizeRange[1], cardRng()) * size,
|
|
320
|
+
phase: cardRng(),
|
|
321
|
+
tint: cardRng(),
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Card on a blob shell — the base fill that keeps the crown a solid mass.
|
|
326
|
+
const sampleShellCard = () => {
|
|
327
|
+
let pick = rng() * totalWeight;
|
|
328
|
+
let blobIndex = 0;
|
|
329
|
+
while (pick > weights[blobIndex] && blobIndex < blobs.length - 1) {
|
|
330
|
+
pick -= weights[blobIndex];
|
|
331
|
+
blobIndex += 1;
|
|
332
|
+
}
|
|
333
|
+
const blob = blobs[blobIndex];
|
|
334
|
+
const theta = rng() * Math.PI * 2;
|
|
335
|
+
const cosPhi = rng() * 2 - 1;
|
|
336
|
+
const sinPhi = Math.sqrt(Math.max(0, 1 - cosPhi * cosPhi));
|
|
337
|
+
const shell = blob.radius * (0.72 + rng() * 0.3);
|
|
338
|
+
cardCenter.set(
|
|
339
|
+
Math.cos(theta) * sinPhi * shell + blob.offset[0],
|
|
340
|
+
cosPhi * shell + blob.offset[1],
|
|
341
|
+
Math.sin(theta) * sinPhi * shell + blob.offset[2],
|
|
342
|
+
);
|
|
343
|
+
shadeNormal.copy(cardCenter).sub(core).normalize();
|
|
344
|
+
if (isCulled(shadeNormal)) return;
|
|
345
|
+
pushCard();
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
if (attachments?.length) {
|
|
349
|
+
// MANDATORY pass first: every attachment gets a full puff of cards
|
|
350
|
+
// engulfing the branch end — dense terminal masses are what make the
|
|
351
|
+
// anime look, and a skipped puff means a naked branch. Never culled,
|
|
352
|
+
// never budget-capped. Each tuft draws from its OWN seeded rng so a
|
|
353
|
+
// per-branch override reshapes only that tuft, never its neighbors.
|
|
354
|
+
attachments.forEach((attachment, attachmentIndex) => {
|
|
355
|
+
const tuftRng = mulberry32(seed * 3.7 + 17.9 + attachmentIndex * 101.3);
|
|
356
|
+
const override = attachmentOverrides?.[attachmentIndex] ?? null;
|
|
357
|
+
const tuftCardsPerCluster = override?.cardsPerCluster ?? cardsPerCluster;
|
|
358
|
+
const tuftRadius = override?.clusterRadius ?? clusterRadius;
|
|
359
|
+
const densityScale = override?.densityScale ?? 1;
|
|
360
|
+
const rawCount = Math.round(
|
|
361
|
+
tuftCardsPerCluster * (0.8 + tuftRng() * 0.5) * coverage * densityScale *
|
|
362
|
+
Math.max(1, density));
|
|
363
|
+
// Only an explicit override may bare a branch.
|
|
364
|
+
const tuftCards = override ? Math.max(rawCount, 0) : Math.max(rawCount, 3);
|
|
365
|
+
for (let i = 0; i < tuftCards; i += 1) {
|
|
366
|
+
// Point in a sphere around the branch end, pushed slightly past it
|
|
367
|
+
// along the growth direction so the wood tip is swallowed by leaves.
|
|
368
|
+
const radius = tuftRadius * (0.35 + 0.65 * Math.cbrt(tuftRng()));
|
|
369
|
+
const theta = tuftRng() * Math.PI * 2;
|
|
370
|
+
const cosPhi = tuftRng() * 2 - 1;
|
|
371
|
+
const sinPhi = Math.sqrt(Math.max(0, 1 - cosPhi * cosPhi));
|
|
372
|
+
cardCenter.set(
|
|
373
|
+
Math.cos(theta) * sinPhi,
|
|
374
|
+
cosPhi,
|
|
375
|
+
Math.sin(theta) * sinPhi,
|
|
376
|
+
).multiplyScalar(radius).add(attachment.position);
|
|
377
|
+
if (attachment.direction) {
|
|
378
|
+
cardCenter.addScaledVector(
|
|
379
|
+
attachment.direction, (0.1 + tuftRng() * 0.35) * tuftRadius);
|
|
380
|
+
}
|
|
381
|
+
pushCard(tuftRng, attachmentIndex);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Then shell fill (gap-cullable) tops the crown up to a solid mass —
|
|
385
|
+
// unless the caller wants bare wood between the tufts.
|
|
386
|
+
if (shellFill) {
|
|
387
|
+
const shellTarget = shellBudget != null
|
|
388
|
+
? cardData.length + Math.round(shellBudget * (0.4 + 0.6 * density) * coverage)
|
|
389
|
+
: targetCards;
|
|
390
|
+
let sampleBudget = cardCount * 4 * coverage;
|
|
391
|
+
while (cardData.length < shellTarget && sampleBudget > 0) {
|
|
392
|
+
sampleBudget -= 1;
|
|
393
|
+
sampleShellCard();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
let sampleBudget = cardCount * 4 * coverage;
|
|
398
|
+
while (cardData.length < targetCards && sampleBudget > 0) {
|
|
399
|
+
sampleBudget -= 1;
|
|
400
|
+
sampleShellCard();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const centers = new Float32Array(cardData.length * 4 * 3);
|
|
405
|
+
const corners = new Float32Array(cardData.length * 4 * 2);
|
|
406
|
+
const uvs = new Float32Array(cardData.length * 4 * 2);
|
|
407
|
+
const shadeNormals = new Float32Array(cardData.length * 4 * 3);
|
|
408
|
+
const infos = new Float32Array(cardData.length * 4 * 4);
|
|
409
|
+
const attachmentIds = new Float32Array(cardData.length * 4);
|
|
410
|
+
const indices = new Uint32Array(cardData.length * 6);
|
|
411
|
+
|
|
412
|
+
const cornerOffsets = [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]];
|
|
413
|
+
const cornerUvs = [[0, 0], [1, 0], [1, 1], [0, 1]];
|
|
414
|
+
cardData.forEach((card, cardIndex) => {
|
|
415
|
+
const heightT = (card.center.y - minY) / Math.max(maxY - minY, 1e-4);
|
|
416
|
+
for (let corner = 0; corner < 4; corner += 1) {
|
|
417
|
+
const vertex = cardIndex * 4 + corner;
|
|
418
|
+
centers[vertex * 3] = card.center.x;
|
|
419
|
+
centers[vertex * 3 + 1] = card.center.y;
|
|
420
|
+
centers[vertex * 3 + 2] = card.center.z;
|
|
421
|
+
corners[vertex * 2] = cornerOffsets[corner][0];
|
|
422
|
+
corners[vertex * 2 + 1] = cornerOffsets[corner][1];
|
|
423
|
+
uvs[vertex * 2] = cornerUvs[corner][0];
|
|
424
|
+
uvs[vertex * 2 + 1] = cornerUvs[corner][1];
|
|
425
|
+
shadeNormals[vertex * 3] = card.normal.x;
|
|
426
|
+
shadeNormals[vertex * 3 + 1] = card.normal.y;
|
|
427
|
+
shadeNormals[vertex * 3 + 2] = card.normal.z;
|
|
428
|
+
infos[vertex * 4] = card.size;
|
|
429
|
+
infos[vertex * 4 + 1] = card.phase;
|
|
430
|
+
infos[vertex * 4 + 2] = card.tint;
|
|
431
|
+
infos[vertex * 4 + 3] = heightT;
|
|
432
|
+
attachmentIds[vertex] = card.attachment;
|
|
433
|
+
}
|
|
434
|
+
const base = cardIndex * 4;
|
|
435
|
+
indices.set([base, base + 1, base + 2, base, base + 2, base + 3], cardIndex * 6);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const geometry = new THREE.BufferGeometry();
|
|
439
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
440
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(centers, 3));
|
|
441
|
+
geometry.setAttribute('aCorner', new THREE.BufferAttribute(corners, 2));
|
|
442
|
+
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
|
|
443
|
+
geometry.setAttribute('aShadeNormal', new THREE.BufferAttribute(shadeNormals, 3));
|
|
444
|
+
geometry.setAttribute('aInfo', new THREE.BufferAttribute(infos, 4));
|
|
445
|
+
// Branch-end index per card (-1 = shell fill), for pointer picking.
|
|
446
|
+
geometry.setAttribute('aAttachment', new THREE.BufferAttribute(attachmentIds, 1));
|
|
447
|
+
// Billboards expand past their centers; pad the bounds so culling and the
|
|
448
|
+
// shadow camera keep the swaying crown fully inside.
|
|
449
|
+
geometry.computeBoundingSphere();
|
|
450
|
+
geometry.boundingSphere.radius += cardSizeRange[1] * size;
|
|
451
|
+
return geometry;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function setSrgb(color, value) {
|
|
455
|
+
if (Array.isArray(value)) {
|
|
456
|
+
color.setRGB(value[0], value[1], value[2], THREE.SRGBColorSpace);
|
|
457
|
+
} else {
|
|
458
|
+
color.set(value);
|
|
459
|
+
}
|
|
460
|
+
return color;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Flexible canopy color spec → concrete color. Accepts:
|
|
464
|
+
// 0x4da258 / '#4da258' / THREE.Color — that color
|
|
465
|
+
// [0.3, 0.63, 0.34] — sRGB triplet (all values ≤ 1)
|
|
466
|
+
// [0x4da258, 0xe8c33c, '#d97b29'] — list: seeded random pick
|
|
467
|
+
// { colors: [...] } — same as a list
|
|
468
|
+
// { from: 0x4da258, to: 0xe8c33c } — seeded random blend of the two
|
|
469
|
+
// { hue: [0.08, 0.16], saturation: [0.6, 0.85], lightness: [0.45, 0.6] }
|
|
470
|
+
// — seeded random HSL in ranges
|
|
471
|
+
// The same spec + seed always resolves to the same color, so forests are
|
|
472
|
+
// varied but stable across reloads.
|
|
473
|
+
export function resolveCanopyColor(spec, seed = 1) {
|
|
474
|
+
const rng = mulberry32(seed * 3.71 + 9.23);
|
|
475
|
+
const range = (value, fallback) => {
|
|
476
|
+
if (Array.isArray(value)) {
|
|
477
|
+
return THREE.MathUtils.lerp(value[0], value[1] ?? value[0], rng());
|
|
478
|
+
}
|
|
479
|
+
return value ?? fallback;
|
|
480
|
+
};
|
|
481
|
+
if (Array.isArray(spec)) {
|
|
482
|
+
const isTriplet = spec.length === 3 &&
|
|
483
|
+
spec.every((v) => typeof v === 'number' && v >= 0 && v <= 1);
|
|
484
|
+
if (isTriplet) return setSrgb(new THREE.Color(), spec);
|
|
485
|
+
return resolveCanopyColor(spec[Math.floor(rng() * spec.length)], seed + 1);
|
|
486
|
+
}
|
|
487
|
+
if (spec && typeof spec === 'object' && !spec.isColor) {
|
|
488
|
+
if (spec.colors) {
|
|
489
|
+
return resolveCanopyColor(spec.colors[Math.floor(rng() * spec.colors.length)], seed + 1);
|
|
490
|
+
}
|
|
491
|
+
if (spec.from !== undefined && spec.to !== undefined) {
|
|
492
|
+
const from = setSrgb(new THREE.Color(), spec.from);
|
|
493
|
+
const to = setSrgb(new THREE.Color(), spec.to);
|
|
494
|
+
return from.lerp(to, rng());
|
|
495
|
+
}
|
|
496
|
+
return new THREE.Color().setHSL(
|
|
497
|
+
range(spec.hue, 0.33),
|
|
498
|
+
range(spec.saturation, 0.55),
|
|
499
|
+
range(spec.lightness, 0.45),
|
|
500
|
+
THREE.SRGBColorSpace,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return setSrgb(new THREE.Color(), spec);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Derive the anime three-tone palette from one canopy hue: cool dark
|
|
507
|
+
// shade, the base as the lit tone, and a warm bright crest for sun-struck
|
|
508
|
+
// crown tops (green trees crest toward yellow-green, yellow trees toward
|
|
509
|
+
// gold). Any tone can be pinned explicitly via `overrides` ({ lit, shadow,
|
|
510
|
+
// crown }) for full art direction.
|
|
511
|
+
export function deriveCanopyPalette(color, overrides = {}) {
|
|
512
|
+
const lit = overrides.lit
|
|
513
|
+
? setSrgb(new THREE.Color(), overrides.lit)
|
|
514
|
+
: setSrgb(new THREE.Color(), color);
|
|
515
|
+
const shadow = overrides.shadow
|
|
516
|
+
? setSrgb(new THREE.Color(), overrides.shadow)
|
|
517
|
+
: lit.clone().offsetHSL(0.045, 0.04, -0.16);
|
|
518
|
+
const crown = overrides.crown
|
|
519
|
+
? setSrgb(new THREE.Color(), overrides.crown)
|
|
520
|
+
: lit.clone().offsetHSL(-0.07, 0.06, 0.14);
|
|
521
|
+
return { lit, shadow, crown };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let sharedLeafSprite = null;
|
|
525
|
+
function defaultLeafSprite() {
|
|
526
|
+
if (!sharedLeafSprite) sharedLeafSprite = createLeafSpriteTexture();
|
|
527
|
+
return sharedLeafSprite;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Shared uniform setters behind the setSun/setWind/... methods that every
|
|
531
|
+
// canopy-bearing class exposes (StylizedTree, StylizedTreeFoliage,
|
|
532
|
+
// StylizedBush): one implementation, thin delegates.
|
|
533
|
+
export function setCanopySun(uniforms, { direction, color, sky } = {}) {
|
|
534
|
+
if (direction) {
|
|
535
|
+
uniforms.uSunDirection.value.set(direction[0], direction[1], direction[2]).normalize();
|
|
536
|
+
}
|
|
537
|
+
if (color) setSrgb(uniforms.uSunColor.value, color);
|
|
538
|
+
if (sky) setSrgb(uniforms.uSkyColor.value, sky);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function setCanopyWind(uniforms, { direction, speed, strength } = {}) {
|
|
542
|
+
if (direction) uniforms.uWindDirection.value.set(direction[0], direction[1]);
|
|
543
|
+
if (Number.isFinite(speed)) uniforms.uWindSpeed.value = speed;
|
|
544
|
+
if (Number.isFinite(strength)) uniforms.uWindStrength.value = strength;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function setCanopySceneShadow(uniforms, { strength } = {}) {
|
|
548
|
+
if (Number.isFinite(strength)) uniforms.uSceneShadowStrength.value = strength;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function setCanopyCloudShadow(uniforms, { strength, coverage, scale, velocity } = {}) {
|
|
552
|
+
if (Number.isFinite(strength)) uniforms.uCloudShadowStrength.value = strength;
|
|
553
|
+
if (Number.isFinite(coverage)) uniforms.uCloudShadowCoverage.value = coverage;
|
|
554
|
+
if (Number.isFinite(scale)) uniforms.uCloudShadowScale.value = scale;
|
|
555
|
+
if (velocity) {
|
|
556
|
+
uniforms.uCloudShadowVelocity.value.set(
|
|
557
|
+
velocity[0] ?? velocity.x ?? 0, velocity[1] ?? velocity.y ?? 0);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function tickCanopyTime(uniforms, delta) {
|
|
562
|
+
uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Color + shadow-depth material pair. Pass `sharedUniforms` (e.g. one uTime /
|
|
566
|
+
// sun block reused across every tree material) to drive all canopies from a
|
|
567
|
+
// single per-frame update.
|
|
568
|
+
export function createTreeFoliageMaterials({
|
|
569
|
+
// Any resolveCanopyColor spec: single color, list, {from,to} blend, or
|
|
570
|
+
// HSL ranges — resolved deterministically with `seed`.
|
|
571
|
+
color = 0x4da258,
|
|
572
|
+
// Pin any of { lit, shadow, crown } explicitly; the rest derive from color.
|
|
573
|
+
palette: paletteOverrides = {},
|
|
574
|
+
seed = 1,
|
|
575
|
+
leafMap = null,
|
|
576
|
+
// Low enough that mipmap-averaged alpha doesn't erode distant crowns.
|
|
577
|
+
alphaCutoff = 0.3,
|
|
578
|
+
windDirection = [1, 0.3],
|
|
579
|
+
windSpeed = 1.0,
|
|
580
|
+
windStrength = 0.05,
|
|
581
|
+
sunDirection = [0.35, 0.72, 0.42],
|
|
582
|
+
sunColor = [1.0, 0.96, 0.84],
|
|
583
|
+
skyColor = [0.62, 0.78, 0.95],
|
|
584
|
+
// How strongly renderer shadow maps shift the crown toward its shadow tone.
|
|
585
|
+
sceneShadowStrength = 0.55,
|
|
586
|
+
// Translucent glow on leaves between the camera and the sun.
|
|
587
|
+
backlitStrength = 0.35,
|
|
588
|
+
// Drifting procedural cloud shadows across the crown. strength 0 disables;
|
|
589
|
+
// velocity is noise-space drift per second (worldDrift = velocity / scale).
|
|
590
|
+
cloudShadowStrength = 0,
|
|
591
|
+
cloudShadowCoverage = 0.45,
|
|
592
|
+
cloudShadowScale = 0.012,
|
|
593
|
+
cloudShadowVelocity = [0.02, 0.006],
|
|
594
|
+
sharedUniforms = {},
|
|
595
|
+
} = {}) {
|
|
596
|
+
const palette = deriveCanopyPalette(resolveCanopyColor(color, seed), paletteOverrides);
|
|
597
|
+
const map = leafMap ?? defaultLeafSprite();
|
|
598
|
+
|
|
599
|
+
const material = createTreeLeafNodeMaterial({
|
|
600
|
+
alphaCutoff,
|
|
601
|
+
backlitStrength,
|
|
602
|
+
cloudShadowCoverage,
|
|
603
|
+
cloudShadowScale,
|
|
604
|
+
cloudShadowStrength,
|
|
605
|
+
cloudShadowVelocity,
|
|
606
|
+
leafMap: map,
|
|
607
|
+
sceneShadowStrength,
|
|
608
|
+
windDirection,
|
|
609
|
+
windSpeed,
|
|
610
|
+
windStrength,
|
|
611
|
+
});
|
|
612
|
+
material.uniforms.uSunDirection.value.set(...sunDirection);
|
|
613
|
+
setSrgb(material.uniforms.uSunColor.value, sunColor);
|
|
614
|
+
setSrgb(material.uniforms.uSkyColor.value, skyColor);
|
|
615
|
+
material.uniforms.uLitColor.value.copy(palette.lit);
|
|
616
|
+
material.uniforms.uShadowColor.value.copy(palette.shadow);
|
|
617
|
+
material.uniforms.uCrownColor.value.copy(palette.crown);
|
|
618
|
+
|
|
619
|
+
// Shared classic uniform blocks (frozen playground callers) drive the
|
|
620
|
+
// node uniforms through write-through wrappers: object values share the
|
|
621
|
+
// instance, numeric values get accessor-backed sync.
|
|
622
|
+
for (const [name, entry] of Object.entries(sharedUniforms)) {
|
|
623
|
+
const node = material.uniforms[name];
|
|
624
|
+
if (!node || !entry || typeof entry !== 'object') continue;
|
|
625
|
+
if (entry.value !== null && typeof entry.value === 'object') {
|
|
626
|
+
node.value = entry.value;
|
|
627
|
+
} else {
|
|
628
|
+
node.value = entry.value;
|
|
629
|
+
Object.defineProperty(entry, 'value', {
|
|
630
|
+
configurable: true,
|
|
631
|
+
get: () => node.value,
|
|
632
|
+
set: (next) => { node.value = next; },
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// The mesh-level customDepthMaterial is the shared contract for both
|
|
638
|
+
// three's native shadow pass and the Water Lab's node-backend sun pass.
|
|
639
|
+
const depthMaterial = material.userData.createDepthColorVariant();
|
|
640
|
+
return { material, depthMaterial, palette };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Convenience mesh wrapper for non-React scenes.
|
|
644
|
+
export class StylizedTreeFoliage extends THREE.Mesh {
|
|
645
|
+
constructor({ geometry: geometryOptions = {}, ...materialOptions } = {}) {
|
|
646
|
+
const geometry = createTreeFoliageGeometry(geometryOptions);
|
|
647
|
+
const { material, depthMaterial } = createTreeFoliageMaterials(materialOptions);
|
|
648
|
+
super(geometry, material);
|
|
649
|
+
this.name = 'StylizedTreeFoliage';
|
|
650
|
+
this.customDepthMaterial = depthMaterial;
|
|
651
|
+
this.castShadow = true;
|
|
652
|
+
this.receiveShadow = true;
|
|
653
|
+
this.frustumCulled = false;
|
|
654
|
+
// Keep the loaded-scene material adapter from replacing the leaf shader.
|
|
655
|
+
this.userData.environmentShaderExclude = true;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
setSun(options) {
|
|
659
|
+
setCanopySun(this.material.uniforms, options);
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
setWind(options) {
|
|
664
|
+
setCanopyWind(this.material.uniforms, options);
|
|
665
|
+
return this;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// How strongly scene shadows (neighboring trees, the character) shift the
|
|
669
|
+
// crown toward its shadow palette. 0 disables.
|
|
670
|
+
setSceneShadow(options) {
|
|
671
|
+
setCanopySceneShadow(this.material.uniforms, options);
|
|
672
|
+
return this;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Drifting procedural cloud shadows across the crown. strength 0 disables.
|
|
676
|
+
setCloudShadow(options) {
|
|
677
|
+
setCanopyCloudShadow(this.material.uniforms, options);
|
|
678
|
+
return this;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
update(delta) {
|
|
682
|
+
tickCanopyTime(this.material.uniforms, delta);
|
|
683
|
+
return this;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
dispose() {
|
|
687
|
+
this.geometry.dispose();
|
|
688
|
+
this.material.dispose();
|
|
689
|
+
this.customDepthMaterial?.dispose();
|
|
690
|
+
}
|
|
691
|
+
}
|