Repository navigation
Expand file tree
/
Copy pathtree.tsx
More file actions
221 lines (203 loc) · 7.17 KB
/
Copy pathtree.tsx
File metadata and controls
221 lines (203 loc) · 7.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import { useLayoutEffect, useMemo, useRef } from "react";
import { useGLTF, useTexture } from "@react-three/drei";
import { MeshStandardNodeMaterial } from "three/webgpu";
import { attribute, positionLocal } from "three/tsl";
import {
DoubleSide,
InstancedBufferAttribute,
NoColorSpace,
Object3D,
type InstancedMesh,
type Mesh,
type Texture,
} from "three";
import { TEXTURE_PATHS } from "../config/scene-config";
import { extractFirstMeshGeometry } from "../utils/mesh-geometry";
import { windSwayOffset } from "../materials/wind";
import { cameraFacingNormal } from "../materials/normals";
import { useUniform } from "../utils/use-uniform";
type Props = {
// World position of the tree's base, plus optional yaw/scale so scattered
// copies don't look identical.
position: [number, number, number];
rotationY?: number;
scale?: number;
windStrength: number;
windSpeed: number;
windAngle: number;
gustScale: number;
turbulence: number;
flutter: number;
treeSway: number;
noiseMap: Texture;
};
// One canopy bush placed on top of the trunk. `pos` is relative to the tree
// group; `yaw`/`scale` vary each copy so the three together read as one full,
// varied canopy rather than a single blob.
type BushInstance = {
pos: [number, number, number];
yaw: number;
scale: number;
};
// Uniform scale-up for the trunk GLB (its mesh is ~0.16 units tall, base at
// y~=0), bringing the top up so the bushes can sit on it.
const TRUNK_SCALE = 12;
const BUSH_INSTANCES: ReadonlyArray<BushInstance> = [
{ pos: [-0.47, 7.59, 0.48], yaw: 0.0, scale: 0.85 },
{ pos: [-3.87, 6.79, -4.47], yaw: 1.3, scale: 0.76 },
{ pos: [-2.08, 10.5, 0.18], yaw: 2.5, scale: 0.9 },
];
// A trunk model with its baked material, topped by three instanced canopy
// bushes. The bushes share the grass wind node: each instance gets a per-bush
// world origin (so the gust sweeping the field also moves the tree, in sync) and
// a yaw basis (so the bend stays coherent in world space across the rotated
// instances) — the same attribute trick the grass uses.
export function Tree({
position,
rotationY = 0,
scale = 1,
windStrength,
windSpeed,
windAngle,
gustScale,
turbulence,
flutter,
treeSway,
noiseMap,
}: Props) {
const [posX, , posZ] = position;
const { scene: leavesScene } = useGLTF(TEXTURE_PATHS.treeLeaves);
const { scene: trunkScene } = useGLTF(TEXTURE_PATHS.treeTrunk);
const alphaMap = useTexture(TEXTURE_PATHS.treeLeavesAlpha);
const windStrengthU = useUniform(windStrength);
const windSpeedU = useUniform(windSpeed);
const windAngleU = useUniform(windAngle);
const gustScaleU = useUniform(gustScale);
const turbulenceU = useUniform(turbulence);
const flutterU = useUniform(flutter);
const treeSwayU = useUniform(treeSway);
// A private copy of the trunk graph (so its node transform + material render as
// authored) with shadows enabled on its meshes.
const trunk = useMemo(() => {
const clone = trunkScene.clone(true);
clone.traverse((o) => {
const mesh = o as Mesh;
if (mesh.isMesh) {
mesh.castShadow = true;
mesh.receiveShadow = true;
}
});
return clone;
}, [trunkScene]);
// The canopy geometry plus per-instance attributes the wind node needs:
// - aOrigin: each bush's world XZ, so the travelling gust is sampled at the
// bush's actual location (positionWorld collapses to the same value for every
// instance in the vertex stage, which would move them in lockstep).
// - aFacing: cos/sin of each bush's yaw, to counter-rotate the world bend into
// the instance's local frame.
// The geometry is cloned per tree because these attributes are baked from this
// tree's world position — sharing the cached GLB geometry would make every
// tree read the last one's gust origin.
const { geometry, baseY, height } = useMemo(() => {
const bounds = extractFirstMeshGeometry(leavesScene);
if (!bounds.geometry) return bounds;
const geom = bounds.geometry.clone();
const count = BUSH_INSTANCES.length;
const origin = new Float32Array(count * 2);
const facing = new Float32Array(count * 2);
BUSH_INSTANCES.forEach((b, i) => {
origin[i * 2 + 0] = posX + b.pos[0];
origin[i * 2 + 1] = posZ + b.pos[2];
facing[i * 2 + 0] = Math.cos(b.yaw);
facing[i * 2 + 1] = Math.sin(b.yaw);
});
geom.setAttribute("aOrigin", new InstancedBufferAttribute(origin, 2));
geom.setAttribute("aFacing", new InstancedBufferAttribute(facing, 2));
return { ...bounds, geometry: geom };
}, [leavesScene, posX, posZ]);
const material = useMemo(() => {
// The alpha map is a non-color mask; keep it linear so its values aren't
// gamma-shifted before the alphaTest comparison.
alphaMap.colorSpace = NoColorSpace;
alphaMap.flipY = false;
alphaMap.needsUpdate = true;
const m = new MeshStandardNodeMaterial({
color: "#4a6b27",
alphaMap,
alphaTest: 0.1,
side: DoubleSide,
roughness: 0.8,
metalness: 0,
});
m.positionNode = positionLocal.add(
windSwayOffset({
baseY,
height,
// Per-instance world origin / yaw basis baked above. instanceIndex now
// varies across the three bushes, so each also desyncs via the wind
// node's per-element hash.
origin: attribute("aOrigin", "vec2"),
facing: attribute("aFacing", "vec2"),
windStrength: windStrengthU,
windSpeed: windSpeedU,
windAngle: windAngleU,
gustScale: gustScaleU,
turbulence: turbulenceU,
flutter: flutterU,
noiseMap,
bendExponent: 1.2,
// Desync leaf clumps within a bush from object-space noise, lift the calm
// floor so the whole canopy drifts, and give it a larger bend than a
// 1-unit blade.
clusterScale: 0.5,
canopyLean: 0.18,
amplitude: treeSwayU,
})
);
// Each leaf card is a flat double-sided quad; orient its shading normal to
// face the camera so both faces shade alike instead of one reading dark.
m.normalNode = cameraFacingNormal().view;
return m;
}, [
alphaMap,
baseY,
height,
windStrengthU,
windSpeedU,
windAngleU,
gustScaleU,
turbulenceU,
flutterU,
treeSwayU,
noiseMap,
]);
const bushesRef = useRef<InstancedMesh>(null!);
useLayoutEffect(() => {
const mesh = bushesRef.current;
if (!mesh || !geometry) return;
const dummy = new Object3D();
BUSH_INSTANCES.forEach((b, i) => {
dummy.position.set(b.pos[0], b.pos[1], b.pos[2]);
dummy.rotation.set(0, b.yaw, 0);
dummy.scale.setScalar(b.scale);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
});
mesh.instanceMatrix.needsUpdate = true;
}, [geometry]);
if (!geometry) return null;
return (
<group position={position} rotation={[0, rotationY, 0]} scale={scale}>
<primitive object={trunk} scale={TRUNK_SCALE} />
<instancedMesh
ref={bushesRef}
args={[geometry, material, BUSH_INSTANCES.length]}
castShadow
// receiveShadow
frustumCulled={false}
/>
</group>
);
}
useGLTF.preload(TEXTURE_PATHS.treeLeaves);
useGLTF.preload(TEXTURE_PATHS.treeTrunk);