mapspinner 1.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/LICENSE +21 -0
- package/README.md +91 -0
- package/package.json +47 -0
- package/src/lib/assets/bark/README.md +8 -0
- package/src/lib/assets/bark/birch_ao_1k.jpg +0 -0
- package/src/lib/assets/bark/birch_color_1k.jpg +0 -0
- package/src/lib/assets/bark/birch_normal_1k.jpg +0 -0
- package/src/lib/assets/bark/birch_roughness_1k.jpg +0 -0
- package/src/lib/assets/bark/oak_ao_1k.jpg +0 -0
- package/src/lib/assets/bark/oak_color_1k.jpg +0 -0
- package/src/lib/assets/bark/oak_normal_1k.jpg +0 -0
- package/src/lib/assets/bark/oak_roughness_1k.jpg +0 -0
- package/src/lib/assets/bark/pine_ao_1k.jpg +0 -0
- package/src/lib/assets/bark/pine_color_1k.jpg +0 -0
- package/src/lib/assets/bark/pine_normal_1k.jpg +0 -0
- package/src/lib/assets/bark/pine_roughness_1k.jpg +0 -0
- package/src/lib/assets/bark/willow_ao_1k.jpg +0 -0
- package/src/lib/assets/bark/willow_color_1k.jpg +0 -0
- package/src/lib/assets/bark/willow_normal_1k.jpg +0 -0
- package/src/lib/assets/bark/willow_roughness_1k.jpg +0 -0
- package/src/lib/assets/leaves/ash_color.png +0 -0
- package/src/lib/assets/leaves/aspen_color.png +0 -0
- package/src/lib/assets/leaves/oak_color.png +0 -0
- package/src/lib/assets/leaves/pine_color.png +0 -0
- package/src/lib/branch.js +28 -0
- package/src/lib/enums.js +23 -0
- package/src/lib/index.js +4 -0
- package/src/lib/options.js +198 -0
- package/src/lib/presets/ash_large.json +96 -0
- package/src/lib/presets/ash_medium.json +96 -0
- package/src/lib/presets/ash_small.json +96 -0
- package/src/lib/presets/aspen_large.json +96 -0
- package/src/lib/presets/aspen_medium.json +96 -0
- package/src/lib/presets/aspen_small.json +96 -0
- package/src/lib/presets/bush_1.json +96 -0
- package/src/lib/presets/bush_2.json +96 -0
- package/src/lib/presets/bush_3.json +96 -0
- package/src/lib/presets/index.js +47 -0
- package/src/lib/presets/oak_large.json +96 -0
- package/src/lib/presets/oak_medium.json +96 -0
- package/src/lib/presets/oak_small.json +96 -0
- package/src/lib/presets/pine_large.json +96 -0
- package/src/lib/presets/pine_medium.json +96 -0
- package/src/lib/presets/pine_small.json +96 -0
- package/src/lib/presets/trellis.json +112 -0
- package/src/lib/rng.js +22 -0
- package/src/lib/textures.js +105 -0
- package/src/lib/tree.js +1062 -0
- package/src/lib/trellis.js +135 -0
package/src/lib/tree.js
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import RNG from './rng.js';
|
|
3
|
+
import { Branch } from './branch.js';
|
|
4
|
+
import { Billboard, TreeType } from './enums.js';
|
|
5
|
+
import TreeOptions from './options.js';
|
|
6
|
+
import { loadPreset } from './presets/index.js';
|
|
7
|
+
import { getBarkTexture, getLeafTexture } from './textures.js';
|
|
8
|
+
import { Trellis } from './trellis.js';
|
|
9
|
+
|
|
10
|
+
// Single shared unit-quad geometry for all leaves across all trees. The
|
|
11
|
+
// per-instance origin/orientation/scale is encoded into instanceMatrix —
|
|
12
|
+
// the geometry itself is just a 4-vertex / 6-index quad.
|
|
13
|
+
let _unitLeafGeo = null;
|
|
14
|
+
function _getUnitLeafGeometry() {
|
|
15
|
+
if (_unitLeafGeo) return _unitLeafGeo;
|
|
16
|
+
const g = new THREE.BufferGeometry();
|
|
17
|
+
// Match the original generateLeaf corner layout (W=L=1):
|
|
18
|
+
// v[0]=(-W/2, L, 0) uv (0,1)
|
|
19
|
+
// v[1]=(-W/2, 0, 0) uv (0,0)
|
|
20
|
+
// v[2]=( W/2, 0, 0) uv (1,0)
|
|
21
|
+
// v[3]=( W/2, L, 0) uv (1,1)
|
|
22
|
+
const positions = new Float32Array([
|
|
23
|
+
-0.5, 1, 0,
|
|
24
|
+
-0.5, 0, 0,
|
|
25
|
+
0.5, 0, 0,
|
|
26
|
+
0.5, 1, 0,
|
|
27
|
+
]);
|
|
28
|
+
const uvs = new Float32Array([0, 1, 0, 0, 1, 0, 1, 1]);
|
|
29
|
+
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
30
|
+
g.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
31
|
+
g.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
|
|
32
|
+
g.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
33
|
+
// Front-facing normal (z+); shader runs with side: DoubleSide so back faces
|
|
34
|
+
// get flipped lighting via the standard pipeline.
|
|
35
|
+
const normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
|
|
36
|
+
g.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
|
|
37
|
+
g.computeBoundingSphere();
|
|
38
|
+
// Boost bounding sphere so frustum culling doesn't drop tilted instances.
|
|
39
|
+
g.boundingSphere.radius = 1.5;
|
|
40
|
+
_unitLeafGeo = g;
|
|
41
|
+
return _unitLeafGeo;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Compose the per-instance leaf transform: rotate by ryRot around local Y,
|
|
45
|
+
// rotate by parent orientation Euler, translate by origin, uniform scale.
|
|
46
|
+
const _tmpEuler = new THREE.Euler();
|
|
47
|
+
const _tmpQ = new THREE.Quaternion();
|
|
48
|
+
const _tmpQy = new THREE.Quaternion();
|
|
49
|
+
const _tmpQfinal = new THREE.Quaternion();
|
|
50
|
+
const _tmpV = new THREE.Vector3();
|
|
51
|
+
const _tmpScale = new THREE.Vector3();
|
|
52
|
+
const _yAxis = new THREE.Vector3(0, 1, 0);
|
|
53
|
+
function _composeLeafMatrix(out, inst) {
|
|
54
|
+
_tmpEuler.set(inst.ex, inst.ey, inst.ez);
|
|
55
|
+
_tmpQ.setFromEuler(_tmpEuler);
|
|
56
|
+
_tmpQy.setFromAxisAngle(_yAxis, inst.ryRot);
|
|
57
|
+
_tmpQfinal.multiplyQuaternions(_tmpQ, _tmpQy);
|
|
58
|
+
_tmpV.set(inst.ox, inst.oy, inst.oz);
|
|
59
|
+
_tmpScale.set(inst.size, inst.size, inst.size);
|
|
60
|
+
out.compose(_tmpV, _tmpQfinal, _tmpScale);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Module-level material caches. Sharing materials across Tree instances cuts
|
|
64
|
+
// allocation cost from O(trees) to O(distinct_visible_configs) and lets the
|
|
65
|
+
// per-frame uTime write target one shader handle per material instead of one
|
|
66
|
+
// per tree. The cache key is the *visible* identity: same key => identical
|
|
67
|
+
// pixel output, so sharing is visually equivalent.
|
|
68
|
+
const _leafMatCache = new Map();
|
|
69
|
+
const _branchMatCache = new Map();
|
|
70
|
+
// Active leaf shader handles (populated on first material compile by Three).
|
|
71
|
+
// Updated en masse via Tree.updateAllShaders(t) — O(distinct_materials).
|
|
72
|
+
const _leafShaders = new Set();
|
|
73
|
+
|
|
74
|
+
function _leafKey(o) {
|
|
75
|
+
return [o.type, o.tint, o.alphaTest, !!o.textured].join('|');
|
|
76
|
+
}
|
|
77
|
+
function _branchKey(o) {
|
|
78
|
+
return [o.type, o.tint, !!o.flatShading, !!o.textured, o.textureScale].join('|');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class Tree extends THREE.Group {
|
|
82
|
+
/** Single per-frame call updates all shared leaf shaders. */
|
|
83
|
+
static updateAllShaders(elapsedTime) {
|
|
84
|
+
for (const sh of _leafShaders) {
|
|
85
|
+
if (sh && sh.uniforms && sh.uniforms.uTime) sh.uniforms.uTime.value = elapsedTime;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
static get sharedLeafMaterialCount() { return _leafMatCache.size; }
|
|
89
|
+
static get sharedLeafShaderCount() { return _leafShaders.size; }
|
|
90
|
+
static get sharedBranchMaterialCount() { return _branchMatCache.size; }
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Consolidate all leaf InstancedMeshes inside `rootGroup` into one big
|
|
94
|
+
* InstancedMesh per leaf material. Cuts per-tree draw call count from
|
|
95
|
+
* O(trees) to O(distinct_leaf_materials). Tree branches stay per-tree
|
|
96
|
+
* (they're unique geometry) but leaves merge across the whole forest.
|
|
97
|
+
* After this call, each tree's leavesMesh is removed from the scene.
|
|
98
|
+
*/
|
|
99
|
+
static consolidateLeaves(rootGroup) {
|
|
100
|
+
// Collect (worldMatrix, leavesMesh, instances) for every tree in the group.
|
|
101
|
+
const buckets = new Map(); // material -> Matrix4[] in rootGroup-local space
|
|
102
|
+
rootGroup.updateMatrixWorld(true);
|
|
103
|
+
const rootInverse = new THREE.Matrix4().copy(rootGroup.matrixWorld).invert();
|
|
104
|
+
const treesToClean = [];
|
|
105
|
+
rootGroup.traverse((o) => {
|
|
106
|
+
if (o instanceof Tree) treesToClean.push(o);
|
|
107
|
+
});
|
|
108
|
+
const tmpM = new THREE.Matrix4();
|
|
109
|
+
for (const tree of treesToClean) {
|
|
110
|
+
const leafMesh = tree.leavesMesh;
|
|
111
|
+
if (!leafMesh || !leafMesh.isInstancedMesh) continue;
|
|
112
|
+
const mat = leafMesh.material;
|
|
113
|
+
// Tree-local instance matrix -> world -> rootGroup-local
|
|
114
|
+
tree.updateMatrixWorld(true);
|
|
115
|
+
const treeWorld = tree.matrixWorld;
|
|
116
|
+
const localToRoot = new THREE.Matrix4().multiplyMatrices(rootInverse, treeWorld);
|
|
117
|
+
const list = buckets.get(mat) || [];
|
|
118
|
+
const inst = leafMesh.instanceMatrix;
|
|
119
|
+
const count = leafMesh.count;
|
|
120
|
+
for (let i = 0; i < count; i++) {
|
|
121
|
+
leafMesh.getMatrixAt(i, tmpM);
|
|
122
|
+
const world = new THREE.Matrix4().multiplyMatrices(localToRoot, tmpM);
|
|
123
|
+
list.push(world);
|
|
124
|
+
}
|
|
125
|
+
buckets.set(mat, list);
|
|
126
|
+
// Remove the per-tree leaves entirely.
|
|
127
|
+
tree.remove(leafMesh);
|
|
128
|
+
tree.leavesMesh = null;
|
|
129
|
+
}
|
|
130
|
+
const geom = _getUnitLeafGeometry();
|
|
131
|
+
const consolidated = [];
|
|
132
|
+
for (const [mat, mats] of buckets.entries()) {
|
|
133
|
+
const merged = new THREE.InstancedMesh(geom, mat, mats.length);
|
|
134
|
+
merged.name = 'leaves-merged';
|
|
135
|
+
merged.castShadow = true;
|
|
136
|
+
merged.receiveShadow = true;
|
|
137
|
+
merged.frustumCulled = false;
|
|
138
|
+
for (let i = 0; i < mats.length; i++) merged.setMatrixAt(i, mats[i]);
|
|
139
|
+
merged.instanceMatrix.needsUpdate = true;
|
|
140
|
+
rootGroup.add(merged);
|
|
141
|
+
consolidated.push(merged);
|
|
142
|
+
}
|
|
143
|
+
return { meshes: consolidated, totalInstances: Array.from(buckets.values()).reduce((s,a)=>s+a.length,0) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Merge per-tree branch meshes (regular non-instanced THREE.Mesh objects)
|
|
148
|
+
* into a small set of merged BufferGeometries grouped by material. Cuts
|
|
149
|
+
* forest branch draw calls from O(trees) to O(distinct_branch_materials).
|
|
150
|
+
* Each tree's branchesMesh is removed from the scene; its geometry data is
|
|
151
|
+
* baked into world space so the merged mesh sits at the rootGroup origin.
|
|
152
|
+
*/
|
|
153
|
+
static consolidateBranches(rootGroup) {
|
|
154
|
+
rootGroup.updateMatrixWorld(true);
|
|
155
|
+
const rootInverse = new THREE.Matrix4().copy(rootGroup.matrixWorld).invert();
|
|
156
|
+
const trees = [];
|
|
157
|
+
rootGroup.traverse((o) => { if (o instanceof Tree) trees.push(o); });
|
|
158
|
+
// material -> { positions:[], normals:[], uvs:[], windFactors:[], indices:[] }
|
|
159
|
+
const buckets = new Map();
|
|
160
|
+
let nextIndexBase = new Map(); // material -> running base
|
|
161
|
+
for (const tree of trees) {
|
|
162
|
+
const bm = tree.branchesMesh;
|
|
163
|
+
if (!bm || !bm.geometry || !bm.geometry.attributes || !bm.geometry.attributes.position) continue;
|
|
164
|
+
const mat = bm.material;
|
|
165
|
+
tree.updateMatrixWorld(true);
|
|
166
|
+
const local = new THREE.Matrix4().multiplyMatrices(rootInverse, tree.matrixWorld);
|
|
167
|
+
const normalLocal = new THREE.Matrix3().getNormalMatrix(local);
|
|
168
|
+
let bucket = buckets.get(mat);
|
|
169
|
+
if (!bucket) { bucket = { pos:[], nrm:[], uvs:[], wind:[], idx:[], base:0 }; buckets.set(mat, bucket); }
|
|
170
|
+
const g = bm.geometry;
|
|
171
|
+
const posAttr = g.attributes.position;
|
|
172
|
+
const nrmAttr = g.attributes.normal;
|
|
173
|
+
const uvAttr = g.attributes.uv;
|
|
174
|
+
const windAttr = g.attributes.windFactor;
|
|
175
|
+
const idxAttr = g.index;
|
|
176
|
+
const v = new THREE.Vector3();
|
|
177
|
+
const n = new THREE.Vector3();
|
|
178
|
+
const base = bucket.base;
|
|
179
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
180
|
+
v.fromBufferAttribute(posAttr, i).applyMatrix4(local);
|
|
181
|
+
bucket.pos.push(v.x, v.y, v.z);
|
|
182
|
+
if (nrmAttr) {
|
|
183
|
+
n.fromBufferAttribute(nrmAttr, i).applyMatrix3(normalLocal).normalize();
|
|
184
|
+
bucket.nrm.push(n.x, n.y, n.z);
|
|
185
|
+
}
|
|
186
|
+
if (uvAttr) bucket.uvs.push(uvAttr.getX(i), uvAttr.getY(i));
|
|
187
|
+
if (windAttr) bucket.wind.push(windAttr.getX(i));
|
|
188
|
+
}
|
|
189
|
+
if (idxAttr) {
|
|
190
|
+
for (let i = 0; i < idxAttr.count; i++) bucket.idx.push(idxAttr.getX(i) + base);
|
|
191
|
+
} else {
|
|
192
|
+
for (let i = 0; i < posAttr.count; i++) bucket.idx.push(i + base);
|
|
193
|
+
}
|
|
194
|
+
bucket.base = base + posAttr.count;
|
|
195
|
+
// Detach per-tree branch mesh
|
|
196
|
+
tree.remove(bm);
|
|
197
|
+
bm.geometry.dispose();
|
|
198
|
+
tree.branchesMesh = null;
|
|
199
|
+
}
|
|
200
|
+
const merged = [];
|
|
201
|
+
for (const [mat, b] of buckets.entries()) {
|
|
202
|
+
const geom = new THREE.BufferGeometry();
|
|
203
|
+
geom.setAttribute('position', new THREE.Float32BufferAttribute(b.pos, 3));
|
|
204
|
+
if (b.nrm.length) geom.setAttribute('normal', new THREE.Float32BufferAttribute(b.nrm, 3));
|
|
205
|
+
if (b.uvs.length) geom.setAttribute('uv', new THREE.Float32BufferAttribute(b.uvs, 2));
|
|
206
|
+
if (b.wind.length) geom.setAttribute('windFactor', new THREE.Float32BufferAttribute(b.wind, 1));
|
|
207
|
+
geom.setIndex(b.idx.length > 65535 ? new THREE.Uint32BufferAttribute(b.idx, 1) : new THREE.Uint16BufferAttribute(b.idx, 1));
|
|
208
|
+
geom.computeBoundingSphere();
|
|
209
|
+
const m = new THREE.Mesh(geom, mat);
|
|
210
|
+
m.name = 'branches-merged';
|
|
211
|
+
m.castShadow = true;
|
|
212
|
+
m.receiveShadow = true;
|
|
213
|
+
rootGroup.add(m);
|
|
214
|
+
merged.push(m);
|
|
215
|
+
}
|
|
216
|
+
return { meshes: merged, materials: buckets.size };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @type {RNG}
|
|
221
|
+
*/
|
|
222
|
+
rng;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @type {TreeOptions}
|
|
226
|
+
*/
|
|
227
|
+
options;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @type {Branch[]}
|
|
231
|
+
*/
|
|
232
|
+
branchQueue = [];
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {TreeOptions} params
|
|
236
|
+
*/
|
|
237
|
+
constructor(options = new TreeOptions()) {
|
|
238
|
+
super();
|
|
239
|
+
this.name = 'Tree';
|
|
240
|
+
this.branchesMesh = new THREE.Mesh();
|
|
241
|
+
// leavesMesh is rebuilt as an InstancedMesh on every generate() —
|
|
242
|
+
// start as a placeholder so .remove()/.add() still work uniformly.
|
|
243
|
+
this.leavesMesh = new THREE.Object3D();
|
|
244
|
+
this.trellisMesh = null;
|
|
245
|
+
this.add(this.branchesMesh);
|
|
246
|
+
this.add(this.leavesMesh);
|
|
247
|
+
this.options = options;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
update(elapsedTime) {
|
|
251
|
+
// Materials are shared across trees; uTime is written once per material
|
|
252
|
+
// via Tree.updateAllShaders. Keep this as a no-op for API compatibility.
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Loads a preset tree from JSON
|
|
257
|
+
* @param {string} preset
|
|
258
|
+
*/
|
|
259
|
+
loadPreset(name) {
|
|
260
|
+
const json = loadPreset(name);
|
|
261
|
+
this.loadFromJson(json);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Loads a tree from JSON
|
|
266
|
+
* @param {TreeOptions} json
|
|
267
|
+
*/
|
|
268
|
+
loadFromJson(json) {
|
|
269
|
+
this.options.copy(json);
|
|
270
|
+
this.generate();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generate a new tree
|
|
275
|
+
*/
|
|
276
|
+
generate() {
|
|
277
|
+
// Clean up old geometry
|
|
278
|
+
this.branches = {
|
|
279
|
+
verts: [],
|
|
280
|
+
normals: [],
|
|
281
|
+
indices: [],
|
|
282
|
+
uvs: [],
|
|
283
|
+
windFactor: []
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Instance descriptors: each entry produces one InstancedMesh slot.
|
|
287
|
+
// Two-sided billboard => emit two entries per leaf with rotation offset.
|
|
288
|
+
this.leaves = { instances: [] };
|
|
289
|
+
|
|
290
|
+
this.rng = new RNG(this.options.seed);
|
|
291
|
+
|
|
292
|
+
// Create the trunk of the tree first
|
|
293
|
+
this.branchQueue.push(
|
|
294
|
+
new Branch(
|
|
295
|
+
new THREE.Vector3(),
|
|
296
|
+
new THREE.Euler(),
|
|
297
|
+
this.options.branch.length[0],
|
|
298
|
+
this.options.branch.radius[0],
|
|
299
|
+
0,
|
|
300
|
+
this.options.branch.sections[0],
|
|
301
|
+
this.options.branch.segments[0],
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
while (this.branchQueue.length > 0) {
|
|
306
|
+
const branch = this.branchQueue.shift();
|
|
307
|
+
this.generateBranch(branch);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.createBranchesGeometry();
|
|
311
|
+
this.createLeavesGeometry();
|
|
312
|
+
this.createTrellis();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Generates a new branch
|
|
317
|
+
* @param {Branch} branch
|
|
318
|
+
* @returns
|
|
319
|
+
*/
|
|
320
|
+
generateBranch(branch) {
|
|
321
|
+
// Used later for geometry index generation
|
|
322
|
+
const indexOffset = this.branches.verts.length / 3;
|
|
323
|
+
|
|
324
|
+
let sectionOrientation = branch.orientation.clone();
|
|
325
|
+
let sectionOrigin = branch.origin.clone();
|
|
326
|
+
let sectionLength =
|
|
327
|
+
branch.length /
|
|
328
|
+
branch.sectionCount /
|
|
329
|
+
(this.options.type === 'Deciduous' ? this.options.branch.levels - 1 : 1);
|
|
330
|
+
|
|
331
|
+
// This information is used for generating child branches after the branch
|
|
332
|
+
// geometry has been constructed
|
|
333
|
+
let sections = [];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i <= branch.sectionCount; i++) {
|
|
336
|
+
let sectionRadius = branch.radius;
|
|
337
|
+
|
|
338
|
+
// If final section of final level, set radius to effecively zero
|
|
339
|
+
if (
|
|
340
|
+
i === branch.sectionCount &&
|
|
341
|
+
branch.level === this.options.branch.levels
|
|
342
|
+
) {
|
|
343
|
+
sectionRadius = 0.001;
|
|
344
|
+
} else if (this.options.type === TreeType.Deciduous) {
|
|
345
|
+
sectionRadius *=
|
|
346
|
+
1 - this.options.branch.taper[branch.level] * (i / branch.sectionCount);
|
|
347
|
+
} else if (this.options.type === TreeType.Evergreen) {
|
|
348
|
+
// Evergreens do not have a terminal branch so they have a taper of 1
|
|
349
|
+
sectionRadius *= 1 - (i / branch.sectionCount);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Create the segments that make up this section.
|
|
353
|
+
let first;
|
|
354
|
+
for (let j = 0; j < branch.segmentCount; j++) {
|
|
355
|
+
let angle = (2.0 * Math.PI * j) / branch.segmentCount;
|
|
356
|
+
|
|
357
|
+
// Create the segment vertex
|
|
358
|
+
const vertex = new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle))
|
|
359
|
+
.multiplyScalar(sectionRadius)
|
|
360
|
+
.applyEuler(sectionOrientation)
|
|
361
|
+
.add(sectionOrigin);
|
|
362
|
+
|
|
363
|
+
const normal = new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle))
|
|
364
|
+
.applyEuler(sectionOrientation)
|
|
365
|
+
.normalize();
|
|
366
|
+
|
|
367
|
+
const uv = new THREE.Vector2(
|
|
368
|
+
j / branch.segmentCount,
|
|
369
|
+
(i % 2 === 0) ? 0 : 1,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
this.branches.verts.push(...Object.values(vertex));
|
|
373
|
+
this.branches.normals.push(...Object.values(normal));
|
|
374
|
+
this.branches.uvs.push(...Object.values(uv));
|
|
375
|
+
|
|
376
|
+
if (j === 0) {
|
|
377
|
+
first = { vertex, normal, uv };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Duplicate the first vertex so there is continuity in the UV mapping
|
|
382
|
+
this.branches.verts.push(...Object.values(first.vertex));
|
|
383
|
+
this.branches.normals.push(...Object.values(first.normal));
|
|
384
|
+
this.branches.uvs.push(1, first.uv.y);
|
|
385
|
+
|
|
386
|
+
// Use this information later on when generating child branches
|
|
387
|
+
sections.push({
|
|
388
|
+
origin: sectionOrigin.clone(),
|
|
389
|
+
orientation: sectionOrientation.clone(),
|
|
390
|
+
radius: sectionRadius,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
sectionOrigin.add(
|
|
394
|
+
new THREE.Vector3(0, sectionLength, 0).applyEuler(sectionOrientation),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Perturb the orientation of the next section randomly. The higher the
|
|
398
|
+
// gnarliness, the larger potential perturbation
|
|
399
|
+
const gnarliness =
|
|
400
|
+
Math.max(1, 1 / Math.sqrt(sectionRadius)) *
|
|
401
|
+
this.options.branch.gnarliness[branch.level];
|
|
402
|
+
|
|
403
|
+
sectionOrientation.x += this.rng.random(gnarliness, -gnarliness);
|
|
404
|
+
sectionOrientation.z += this.rng.random(gnarliness, -gnarliness);
|
|
405
|
+
|
|
406
|
+
// Apply growth force to the branch
|
|
407
|
+
const qSection = new THREE.Quaternion().setFromEuler(sectionOrientation);
|
|
408
|
+
|
|
409
|
+
const qTwist = new THREE.Quaternion().setFromAxisAngle(
|
|
410
|
+
new THREE.Vector3(0, 1, 0),
|
|
411
|
+
this.options.branch.twist[branch.level],
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const qForce = new THREE.Quaternion().setFromUnitVectors(
|
|
415
|
+
new THREE.Vector3(0, 1, 0),
|
|
416
|
+
new THREE.Vector3().copy(this.options.branch.force.direction),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
qSection.multiply(qTwist);
|
|
420
|
+
qSection.rotateTowards(
|
|
421
|
+
qForce,
|
|
422
|
+
this.options.branch.force.strength / sectionRadius,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Apply trellis force if enabled
|
|
426
|
+
if (this.options.trellis.enabled) {
|
|
427
|
+
const trellisResult = this.calculateTrellisForce(sectionOrigin, sectionRadius);
|
|
428
|
+
if (trellisResult) {
|
|
429
|
+
const qTrellis = new THREE.Quaternion().setFromUnitVectors(
|
|
430
|
+
new THREE.Vector3(0, 1, 0),
|
|
431
|
+
trellisResult.direction,
|
|
432
|
+
);
|
|
433
|
+
qSection.rotateTowards(qTrellis, trellisResult.strength);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
sectionOrientation.setFromQuaternion(qSection);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.generateBranchIndices(indexOffset, branch);
|
|
441
|
+
|
|
442
|
+
// Deciduous trees have a terminal branch that grows out of the
|
|
443
|
+
// end of the parent branch
|
|
444
|
+
if (this.options.type === 'deciduous') {
|
|
445
|
+
const lastSection = sections[sections.length - 1];
|
|
446
|
+
|
|
447
|
+
if (branch.level < this.options.branch.levels) {
|
|
448
|
+
this.branchQueue.push(
|
|
449
|
+
new Branch(
|
|
450
|
+
lastSection.origin,
|
|
451
|
+
lastSection.orientation,
|
|
452
|
+
this.options.branch.length[branch.level + 1],
|
|
453
|
+
lastSection.radius,
|
|
454
|
+
branch.level + 1,
|
|
455
|
+
// Section count and segment count must be same as parent branch
|
|
456
|
+
// since the child branch is growing from the end of the parent branch
|
|
457
|
+
branch.sectionCount,
|
|
458
|
+
branch.segmentCount,
|
|
459
|
+
),
|
|
460
|
+
);
|
|
461
|
+
} else {
|
|
462
|
+
this.generateLeaf(lastSection.origin, lastSection.orientation);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// If we are on the last branch level, generate leaves
|
|
467
|
+
if (branch.level === this.options.branch.levels) {
|
|
468
|
+
this.generateLeaves(sections);
|
|
469
|
+
} else if (branch.level < this.options.branch.levels) {
|
|
470
|
+
this.generateChildBranches(
|
|
471
|
+
this.options.branch.children[branch.level],
|
|
472
|
+
branch.level + 1,
|
|
473
|
+
sections);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Generate branches from a parent branch
|
|
479
|
+
* @param {number} count The number of child branches to generate
|
|
480
|
+
* @param {number} level The level of the child branches
|
|
481
|
+
* @param {{
|
|
482
|
+
* origin: THREE.Vector3,
|
|
483
|
+
* orientation: THREE.Euler,
|
|
484
|
+
* radius: number
|
|
485
|
+
* }[]} sections The parent branch's sections
|
|
486
|
+
* @returns
|
|
487
|
+
*/
|
|
488
|
+
generateChildBranches(count, level, sections) {
|
|
489
|
+
const radialOffset = this.rng.random();
|
|
490
|
+
|
|
491
|
+
for (let i = 0; i < count; i++) {
|
|
492
|
+
// Determine how far along the length of the parent branch the child
|
|
493
|
+
// branch should originate from (0 to 1)
|
|
494
|
+
let childBranchStart = this.rng.random(1.0, this.options.branch.start[level]);
|
|
495
|
+
|
|
496
|
+
// Find which sections are on either side of the child branch origin point
|
|
497
|
+
// so we can determine the origin, orientation and radius of the branch
|
|
498
|
+
const sectionIndex = Math.floor(childBranchStart * (sections.length - 1));
|
|
499
|
+
let sectionA, sectionB;
|
|
500
|
+
sectionA = sections[sectionIndex];
|
|
501
|
+
if (sectionIndex === sections.length - 1) {
|
|
502
|
+
sectionB = sectionA;
|
|
503
|
+
} else {
|
|
504
|
+
sectionB = sections[sectionIndex + 1];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Find normalized distance from section A to section B (0 to 1)
|
|
508
|
+
const alpha =
|
|
509
|
+
(childBranchStart - sectionIndex / (sections.length - 1)) /
|
|
510
|
+
(1 / (sections.length - 1));
|
|
511
|
+
|
|
512
|
+
// Linearly interpolate origin from section A to section B
|
|
513
|
+
const childBranchOrigin = new THREE.Vector3().lerpVectors(
|
|
514
|
+
sectionA.origin,
|
|
515
|
+
sectionB.origin,
|
|
516
|
+
alpha,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// Linearly interpolate radius
|
|
520
|
+
const childBranchRadius =
|
|
521
|
+
this.options.branch.radius[level] *
|
|
522
|
+
((1 - alpha) * sectionA.radius + alpha * sectionB.radius);
|
|
523
|
+
|
|
524
|
+
// Linearlly interpolate the orientation
|
|
525
|
+
const qA = new THREE.Quaternion().setFromEuler(sectionA.orientation);
|
|
526
|
+
const qB = new THREE.Quaternion().setFromEuler(sectionB.orientation);
|
|
527
|
+
const parentOrientation = new THREE.Euler().setFromQuaternion(
|
|
528
|
+
qB.slerp(qA, alpha),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
// Calculate the angle offset from the parent branch and the radial angle
|
|
532
|
+
const radialAngle = 2.0 * Math.PI * (radialOffset + i / count);
|
|
533
|
+
const q1 = new THREE.Quaternion().setFromAxisAngle(
|
|
534
|
+
new THREE.Vector3(1, 0, 0),
|
|
535
|
+
this.options.branch.angle[level] / (180 / Math.PI),
|
|
536
|
+
);
|
|
537
|
+
const q2 = new THREE.Quaternion().setFromAxisAngle(
|
|
538
|
+
new THREE.Vector3(0, 1, 0),
|
|
539
|
+
radialAngle,
|
|
540
|
+
);
|
|
541
|
+
const q3 = new THREE.Quaternion().setFromEuler(parentOrientation);
|
|
542
|
+
|
|
543
|
+
const childBranchOrientation = new THREE.Euler().setFromQuaternion(
|
|
544
|
+
q3.multiply(q2.multiply(q1)),
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
let childBranchLength =
|
|
548
|
+
this.options.branch.length[level] *
|
|
549
|
+
(this.options.type === TreeType.Evergreen
|
|
550
|
+
? 1.0 - childBranchStart
|
|
551
|
+
: 1.0);
|
|
552
|
+
|
|
553
|
+
this.branchQueue.push(
|
|
554
|
+
new Branch(
|
|
555
|
+
childBranchOrigin,
|
|
556
|
+
childBranchOrientation,
|
|
557
|
+
childBranchLength,
|
|
558
|
+
childBranchRadius,
|
|
559
|
+
level,
|
|
560
|
+
this.options.branch.sections[level],
|
|
561
|
+
this.options.branch.segments[level],
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Logic for spawning child branches from a parent branch's section
|
|
569
|
+
* @param {{
|
|
570
|
+
* origin: THREE.Vector3,
|
|
571
|
+
* orientation: THREE.Euler,
|
|
572
|
+
* radius: number
|
|
573
|
+
* }[]} sections The parent branch's sections
|
|
574
|
+
* @returns
|
|
575
|
+
*/
|
|
576
|
+
generateLeaves(sections) {
|
|
577
|
+
const radialOffset = this.rng.random();
|
|
578
|
+
|
|
579
|
+
for (let i = 0; i < this.options.leaves.count; i++) {
|
|
580
|
+
// Determine how far along the length of the parent
|
|
581
|
+
// branch the leaf should originate from (0 to 1)
|
|
582
|
+
let leafStart = this.rng.random(1.0, this.options.leaves.start);
|
|
583
|
+
|
|
584
|
+
// Find which sections are on either side of the child branch origin point
|
|
585
|
+
// so we can determine the origin, orientation and radius of the branch
|
|
586
|
+
const sectionIndex = Math.floor(leafStart * (sections.length - 1));
|
|
587
|
+
let sectionA, sectionB;
|
|
588
|
+
sectionA = sections[sectionIndex];
|
|
589
|
+
if (sectionIndex === sections.length - 1) {
|
|
590
|
+
sectionB = sectionA;
|
|
591
|
+
} else {
|
|
592
|
+
sectionB = sections[sectionIndex + 1];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Find normalized distance from section A to section B (0 to 1)
|
|
596
|
+
const alpha =
|
|
597
|
+
(leafStart - sectionIndex / (sections.length - 1)) /
|
|
598
|
+
(1 / (sections.length - 1));
|
|
599
|
+
|
|
600
|
+
// Linearly interpolate origin from section A to section B
|
|
601
|
+
const leafOrigin = new THREE.Vector3().lerpVectors(
|
|
602
|
+
sectionA.origin,
|
|
603
|
+
sectionB.origin,
|
|
604
|
+
alpha,
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// Linearlly interpolate the orientation
|
|
608
|
+
const qA = new THREE.Quaternion().setFromEuler(sectionA.orientation);
|
|
609
|
+
const qB = new THREE.Quaternion().setFromEuler(sectionB.orientation);
|
|
610
|
+
const parentOrientation = new THREE.Euler().setFromQuaternion(
|
|
611
|
+
qB.slerp(qA, alpha),
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// Calculate the angle offset from the parent branch and the radial angle
|
|
615
|
+
const radialAngle = 2.0 * Math.PI * (radialOffset + i / this.options.leaves.count);
|
|
616
|
+
const q1 = new THREE.Quaternion().setFromAxisAngle(
|
|
617
|
+
new THREE.Vector3(1, 0, 0),
|
|
618
|
+
this.options.leaves.angle / (180 / Math.PI),
|
|
619
|
+
);
|
|
620
|
+
const q2 = new THREE.Quaternion().setFromAxisAngle(
|
|
621
|
+
new THREE.Vector3(0, 1, 0),
|
|
622
|
+
radialAngle,
|
|
623
|
+
);
|
|
624
|
+
const q3 = new THREE.Quaternion().setFromEuler(parentOrientation);
|
|
625
|
+
|
|
626
|
+
const leafOrientation = new THREE.Euler().setFromQuaternion(
|
|
627
|
+
q3.multiply(q2.multiply(q1)),
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
this.generateLeaf(leafOrigin, leafOrientation);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Generates a leaves
|
|
636
|
+
* @param {THREE.Vector3} origin The starting point of the branch
|
|
637
|
+
* @param {THREE.Euler} orientation The starting orientation of the branch
|
|
638
|
+
*/
|
|
639
|
+
generateLeaf(origin, orientation) {
|
|
640
|
+
const leafSize =
|
|
641
|
+
this.options.leaves.size *
|
|
642
|
+
(1 +
|
|
643
|
+
this.rng.random(
|
|
644
|
+
this.options.leaves.sizeVariance,
|
|
645
|
+
-this.options.leaves.sizeVariance,
|
|
646
|
+
));
|
|
647
|
+
|
|
648
|
+
// Each instance carries origin, orientation Euler, an extra Y rotation
|
|
649
|
+
// (for the two-sided crossed-quad billboard), and uniform scale.
|
|
650
|
+
this.leaves.instances.push({
|
|
651
|
+
ox: origin.x, oy: origin.y, oz: origin.z,
|
|
652
|
+
ex: orientation.x, ey: orientation.y, ez: orientation.z,
|
|
653
|
+
ryRot: 0,
|
|
654
|
+
size: leafSize,
|
|
655
|
+
});
|
|
656
|
+
if (this.options.leaves.billboard === Billboard.Double) {
|
|
657
|
+
this.leaves.instances.push({
|
|
658
|
+
ox: origin.x, oy: origin.y, oz: origin.z,
|
|
659
|
+
ex: orientation.x, ey: orientation.y, ez: orientation.z,
|
|
660
|
+
ryRot: Math.PI / 2,
|
|
661
|
+
size: leafSize,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Generates the indices for branch geometry
|
|
668
|
+
* @param {Branch} branch
|
|
669
|
+
*/
|
|
670
|
+
generateBranchIndices(indexOffset, branch) {
|
|
671
|
+
// Build geometry each section of the branch (cylinder without end caps)
|
|
672
|
+
let v1, v2, v3, v4;
|
|
673
|
+
const N = branch.segmentCount + 1;
|
|
674
|
+
for (let i = 0; i < branch.sectionCount; i++) {
|
|
675
|
+
// Build the quad for each segment of the section
|
|
676
|
+
for (let j = 0; j < branch.segmentCount; j++) {
|
|
677
|
+
v1 = indexOffset + i * N + j;
|
|
678
|
+
// The last segment wraps around back to the starting segment, so omit j + 1 term
|
|
679
|
+
v2 = indexOffset + i * N + (j + 1);
|
|
680
|
+
v3 = v1 + N;
|
|
681
|
+
v4 = v2 + N;
|
|
682
|
+
this.branches.indices.push(v1, v3, v2, v2, v3, v4);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Generates the geometry for the branches
|
|
689
|
+
*/
|
|
690
|
+
createBranchesGeometry() {
|
|
691
|
+
const g = new THREE.BufferGeometry();
|
|
692
|
+
g.setAttribute(
|
|
693
|
+
'position',
|
|
694
|
+
new THREE.BufferAttribute(new Float32Array(this.branches.verts), 3),
|
|
695
|
+
);
|
|
696
|
+
g.setAttribute(
|
|
697
|
+
'normal',
|
|
698
|
+
new THREE.BufferAttribute(new Float32Array(this.branches.normals), 3),
|
|
699
|
+
);
|
|
700
|
+
g.setAttribute(
|
|
701
|
+
'uv',
|
|
702
|
+
new THREE.BufferAttribute(new Float32Array(this.branches.uvs), 2),
|
|
703
|
+
);
|
|
704
|
+
g.setIndex(
|
|
705
|
+
new THREE.BufferAttribute(new Uint16Array(this.branches.indices), 1),
|
|
706
|
+
);
|
|
707
|
+
g.computeBoundingSphere();
|
|
708
|
+
|
|
709
|
+
const bk = _branchKey(this.options.bark);
|
|
710
|
+
let mat = _branchMatCache.get(bk);
|
|
711
|
+
if (!mat) {
|
|
712
|
+
mat = new THREE.MeshPhongMaterial({
|
|
713
|
+
name: 'branches',
|
|
714
|
+
flatShading: this.options.bark.flatShading,
|
|
715
|
+
color: new THREE.Color(this.options.bark.tint),
|
|
716
|
+
});
|
|
717
|
+
if (this.options.bark.textured) {
|
|
718
|
+
mat.aoMap = getBarkTexture(this.options.bark.type, 'ao', this.options.bark.textureScale);
|
|
719
|
+
mat.map = getBarkTexture(this.options.bark.type, 'color', this.options.bark.textureScale);
|
|
720
|
+
mat.normalMap = getBarkTexture(this.options.bark.type, 'normal', this.options.bark.textureScale);
|
|
721
|
+
mat.roughnessMap = getBarkTexture(this.options.bark.type, 'roughness', this.options.bark.textureScale);
|
|
722
|
+
}
|
|
723
|
+
_branchMatCache.set(bk, mat);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.branchesMesh.geometry.dispose();
|
|
727
|
+
this.branchesMesh.geometry = g;
|
|
728
|
+
// Do NOT dispose cached material — it's shared across trees.
|
|
729
|
+
this.branchesMesh.material = mat;
|
|
730
|
+
this.branchesMesh.castShadow = true;
|
|
731
|
+
this.branchesMesh.receiveShadow = true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Generates the InstancedMesh for the leaves. One unit-quad geometry,
|
|
736
|
+
* per-instance origin/orientation/scale carried in instanceMatrix.
|
|
737
|
+
*/
|
|
738
|
+
createLeavesGeometry() {
|
|
739
|
+
// Single quad in local space — corners match the original generateLeaf
|
|
740
|
+
// layout: bottom-left/right at y=0, top-left/right at y=1, x in [-0.5, 0.5].
|
|
741
|
+
// Per-instance scale lifts these to the leaf's actual world size.
|
|
742
|
+
const g = _getUnitLeafGeometry();
|
|
743
|
+
|
|
744
|
+
const lk = _leafKey(this.options.leaves);
|
|
745
|
+
let mat = _leafMatCache.get(lk);
|
|
746
|
+
if (!mat) {
|
|
747
|
+
mat = this._buildLeafMaterial();
|
|
748
|
+
_leafMatCache.set(lk, mat);
|
|
749
|
+
}
|
|
750
|
+
// Replace the existing leavesMesh with a fresh InstancedMesh sized to
|
|
751
|
+
// current instance count. The old mesh (if any) is removed from this Tree.
|
|
752
|
+
if (this.leavesMesh) {
|
|
753
|
+
this.remove(this.leavesMesh);
|
|
754
|
+
// Don't dispose the unit geometry (shared) and don't dispose the cached
|
|
755
|
+
// material — both are deliberately re-used across all trees.
|
|
756
|
+
}
|
|
757
|
+
const count = this.leaves.instances.length;
|
|
758
|
+
const inst = new THREE.InstancedMesh(g, mat, count);
|
|
759
|
+
inst.name = 'leaves';
|
|
760
|
+
const m = new THREE.Matrix4();
|
|
761
|
+
for (let i = 0; i < count; i++) {
|
|
762
|
+
_composeLeafMatrix(m, this.leaves.instances[i]);
|
|
763
|
+
inst.setMatrixAt(i, m);
|
|
764
|
+
}
|
|
765
|
+
inst.instanceMatrix.needsUpdate = true;
|
|
766
|
+
inst.castShadow = true;
|
|
767
|
+
inst.receiveShadow = true;
|
|
768
|
+
inst.frustumCulled = false; // Wind sway can push verts outside the static bound
|
|
769
|
+
this.leavesMesh = inst;
|
|
770
|
+
this.add(inst);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Build a fresh leaf material with the wind-sway onBeforeCompile hook. */
|
|
774
|
+
_buildLeafMaterial() {
|
|
775
|
+
const mat = new THREE.MeshPhongMaterial({
|
|
776
|
+
name: 'leaves',
|
|
777
|
+
map: getLeafTexture(this.options.leaves.type),
|
|
778
|
+
color: new THREE.Color(this.options.leaves.tint),
|
|
779
|
+
emissive: new THREE.Color(0x0a0e08),
|
|
780
|
+
emissiveIntensity: 0.10,
|
|
781
|
+
side: THREE.DoubleSide,
|
|
782
|
+
alphaTest: this.options.leaves.alphaTest,
|
|
783
|
+
dithering: true
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Add custom shader code for branch swaying
|
|
787
|
+
mat.onBeforeCompile = (shader) => {
|
|
788
|
+
shader.uniforms.uTime = { value: 0 };
|
|
789
|
+
shader.uniforms.uWindStrength = { value: new THREE.Vector3(0.5, 0, 0.5) };
|
|
790
|
+
shader.uniforms.uWindFrequency = { value: 0.5 };
|
|
791
|
+
shader.uniforms.uWindScale = { value: 70 };
|
|
792
|
+
|
|
793
|
+
// Wrap-shading hemisphere term + subsurface transmission so leaf quads
|
|
794
|
+
// catch light from any angle AND glow softly when sun is behind them
|
|
795
|
+
// (light passing through the leaf, like a real translucent leaf in sun).
|
|
796
|
+
const phongPars = THREE.ShaderChunk.lights_phong_pars_fragment.replace(
|
|
797
|
+
'float dotNL = saturate( dot( geometryNormal, directLight.direction ) );',
|
|
798
|
+
`float dotNLraw = dot(geometryNormal, directLight.direction);
|
|
799
|
+
float dotNL = pow(saturate(dotNLraw*0.5+0.5), 1.6);
|
|
800
|
+
// Translucent transmission: when sun is BEHIND the leaf (dotNLraw<0),
|
|
801
|
+
// add diffuse-tinted contribution proportional to how much sun is
|
|
802
|
+
// hitting the back side. Falls off with view angle so it reads as
|
|
803
|
+
// soft glow, not flat fill.
|
|
804
|
+
float transmit = max(-dotNLraw, 0.0);
|
|
805
|
+
transmit = pow(transmit, 1.5); // softer ramp`
|
|
806
|
+
);
|
|
807
|
+
// Inject transmission into the irradiance accumulation. The Phong RE_Direct
|
|
808
|
+
// multiplies BRDF_Lambert(diffuseColor) by lightColor*dotNL — we add a
|
|
809
|
+
// separate transmission term that adds tinted light passing through.
|
|
810
|
+
const phongFragChunk = THREE.ShaderChunk.lights_phong_pars_fragment.includes('RE_Direct_BlinnPhong')
|
|
811
|
+
? phongPars.replace(
|
|
812
|
+
'reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );',
|
|
813
|
+
`reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
|
|
814
|
+
// Transmitted light: leaf-color tinted, attenuated, additive
|
|
815
|
+
reflectedLight.directDiffuse += directLight.color * material.diffuseColor * transmit * 0.42;`
|
|
816
|
+
)
|
|
817
|
+
: phongPars;
|
|
818
|
+
shader.fragmentShader = shader.fragmentShader.replace(
|
|
819
|
+
'#include <lights_phong_pars_fragment>',
|
|
820
|
+
phongFragChunk
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
shader.vertexShader = `
|
|
824
|
+
uniform float uTime;
|
|
825
|
+
uniform vec3 uWindStrength;
|
|
826
|
+
uniform float uWindFrequency;
|
|
827
|
+
uniform float uWindScale;
|
|
828
|
+
` + shader.vertexShader;
|
|
829
|
+
|
|
830
|
+
// Add code for simplex noise
|
|
831
|
+
shader.vertexShader = shader.vertexShader.replace(
|
|
832
|
+
`void main() {`,
|
|
833
|
+
`
|
|
834
|
+
// GLSL Simplex Noise 3D
|
|
835
|
+
// Source: https://github.com/ashima/webgl-noise
|
|
836
|
+
|
|
837
|
+
vec3 mod289(vec3 x) {
|
|
838
|
+
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
vec4 mod289(vec4 x) {
|
|
842
|
+
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
vec4 permute(vec4 x) {
|
|
846
|
+
return mod289(((x*34.0)+1.0)*x);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
vec4 taylorInvSqrt(vec4 r) {
|
|
850
|
+
return 1.79284291400159 - 0.85373472095314 * r;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
vec3 fade(vec3 t) {
|
|
854
|
+
return t*t*t*(t*(t*6.0-15.0)+10.0);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Classic Simplex Noise 3D
|
|
858
|
+
float simplex3(vec3 v) {
|
|
859
|
+
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
|
|
860
|
+
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
861
|
+
|
|
862
|
+
// First corner
|
|
863
|
+
vec3 i = floor(v + dot(v, C.yyy) );
|
|
864
|
+
vec3 x0 = v - i + dot(i, C.xxx);
|
|
865
|
+
|
|
866
|
+
// Other corners
|
|
867
|
+
vec3 g = step(x0.yzx, x0.xyz);
|
|
868
|
+
vec3 l = 1.0 - g;
|
|
869
|
+
vec3 i1 = min( g.xyz, l.zxy );
|
|
870
|
+
vec3 i2 = max( g.xyz, l.zxy );
|
|
871
|
+
|
|
872
|
+
// x0 = x0 - 0. + 0.0 * C
|
|
873
|
+
vec3 x1 = x0 - i1 + C.xxx;
|
|
874
|
+
vec3 x2 = x0 - i2 + C.yyy; // 2.0 * C.x = 1/3 = C.y
|
|
875
|
+
vec3 x3 = x0 - D.yyy; // -1.0 + 3.0 * C.x = -0.5
|
|
876
|
+
|
|
877
|
+
// Permutations
|
|
878
|
+
i = mod289(i);
|
|
879
|
+
vec4 p = permute( permute( permute(
|
|
880
|
+
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
881
|
+
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
882
|
+
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
883
|
+
|
|
884
|
+
// Gradients: 7x7 points over a square, mapped onto an octahedron.
|
|
885
|
+
// The ring size 17*17 = 289 is close to the mapping's singularity.
|
|
886
|
+
float n_ = 0.142857142857; // 1.0/7.0
|
|
887
|
+
vec3 ns = n_ * D.wyz - D.xzx;
|
|
888
|
+
|
|
889
|
+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
|
|
890
|
+
|
|
891
|
+
vec4 x_ = floor(j * ns.z);
|
|
892
|
+
vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
|
|
893
|
+
|
|
894
|
+
vec4 x = x_ *ns.x + ns.yyyy;
|
|
895
|
+
vec4 y = y_ *ns.x + ns.yyyy;
|
|
896
|
+
vec4 h = 1.0 - abs(x) - abs(y);
|
|
897
|
+
|
|
898
|
+
vec4 b0 = vec4( x.xy, y.xy );
|
|
899
|
+
vec4 b1 = vec4( x.zw, y.zw );
|
|
900
|
+
|
|
901
|
+
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
902
|
+
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
903
|
+
vec4 sh = -step(h, vec4(0.0));
|
|
904
|
+
|
|
905
|
+
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
|
|
906
|
+
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
|
|
907
|
+
|
|
908
|
+
vec3 g0 = vec3(a0.xy,h.x);
|
|
909
|
+
vec3 g1 = vec3(a0.zw,h.y);
|
|
910
|
+
vec3 g2 = vec3(a1.xy,h.z);
|
|
911
|
+
vec3 g3 = vec3(a1.zw,h.w);
|
|
912
|
+
|
|
913
|
+
// Normalise gradients
|
|
914
|
+
vec4 norm = taylorInvSqrt(vec4(dot(g0,g0), dot(g1,g1), dot(g2,g2), dot(g3,g3)));
|
|
915
|
+
g0 *= norm.x;
|
|
916
|
+
g1 *= norm.y;
|
|
917
|
+
g2 *= norm.z;
|
|
918
|
+
g3 *= norm.w;
|
|
919
|
+
|
|
920
|
+
// Mix contributions from the four corners
|
|
921
|
+
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
922
|
+
m = m * m;
|
|
923
|
+
return 42.0 * dot( m*m, vec4( dot(g0,x0), dot(g1,x1),
|
|
924
|
+
dot(g2,x2), dot(g3,x3) ) );
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
void main() {`,
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
shader.vertexShader = shader.vertexShader.replace(
|
|
931
|
+
`#include <project_vertex>`,
|
|
932
|
+
`
|
|
933
|
+
vec4 mvPosition = vec4(transformed, 1.0);
|
|
934
|
+
#ifdef USE_INSTANCING
|
|
935
|
+
mvPosition = instanceMatrix * mvPosition;
|
|
936
|
+
#endif
|
|
937
|
+
|
|
938
|
+
// Sample wind noise after instance placement so leaves at different
|
|
939
|
+
// origins get different sway phase — matches the world-space layout.
|
|
940
|
+
float windOffset = 2.0 * 3.14 * simplex3(mvPosition.xyz / uWindScale);
|
|
941
|
+
vec3 windSway = uv.y * uWindStrength * (
|
|
942
|
+
0.5 * sin(uTime * uWindFrequency + windOffset) +
|
|
943
|
+
0.3 * sin(2.0 * uTime * uWindFrequency + 1.3 * windOffset) +
|
|
944
|
+
0.2 * sin(5.0 * uTime * uWindFrequency + 1.5 * windOffset)
|
|
945
|
+
);
|
|
946
|
+
mvPosition.xyz += windSway;
|
|
947
|
+
|
|
948
|
+
mvPosition = modelViewMatrix * mvPosition;
|
|
949
|
+
gl_Position = projectionMatrix * mvPosition;
|
|
950
|
+
`
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
mat.userData.shader = shader;
|
|
954
|
+
_leafShaders.add(shader);
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
return mat;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Create or update the trellis geometry
|
|
962
|
+
*/
|
|
963
|
+
createTrellis() {
|
|
964
|
+
// Remove old trellis if exists
|
|
965
|
+
if (this.trellisMesh) {
|
|
966
|
+
this.remove(this.trellisMesh);
|
|
967
|
+
this.trellisMesh.dispose();
|
|
968
|
+
this.trellisMesh = null;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Create new trellis if enabled and visible
|
|
972
|
+
if (this.options.trellis.enabled && this.options.trellis.visible) {
|
|
973
|
+
this.trellisMesh = new Trellis(this.options.trellis);
|
|
974
|
+
this.trellisMesh.generate();
|
|
975
|
+
this.add(this.trellisMesh);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Find the nearest point on the trellis grid to a given position
|
|
981
|
+
* @param {THREE.Vector3} position
|
|
982
|
+
* @returns {THREE.Vector3}
|
|
983
|
+
*/
|
|
984
|
+
getNearestTrellisPoint(position) {
|
|
985
|
+
const t = this.options.trellis;
|
|
986
|
+
const trellisX = t.position.x;
|
|
987
|
+
const trellisY = t.position.y;
|
|
988
|
+
const trellisZ = t.position.z;
|
|
989
|
+
|
|
990
|
+
// Trellis bounds
|
|
991
|
+
const minX = trellisX - t.width / 2;
|
|
992
|
+
const maxX = trellisX + t.width / 2;
|
|
993
|
+
const minY = trellisY;
|
|
994
|
+
const maxY = trellisY + t.height;
|
|
995
|
+
|
|
996
|
+
// Clamp position to trellis bounds for projection
|
|
997
|
+
const clampedX = Math.max(minX, Math.min(maxX, position.x));
|
|
998
|
+
const clampedY = Math.max(minY, Math.min(maxY, position.y));
|
|
999
|
+
|
|
1000
|
+
// Find nearest horizontal line (Y = constant)
|
|
1001
|
+
const nearestHLineY = Math.round((clampedY - minY) / t.spacing) * t.spacing + minY;
|
|
1002
|
+
const finalHLineY = Math.max(minY, Math.min(maxY, nearestHLineY));
|
|
1003
|
+
|
|
1004
|
+
// Find nearest vertical line (X = constant)
|
|
1005
|
+
const nearestVLineX = Math.round((clampedX - minX) / t.spacing) * t.spacing + minX;
|
|
1006
|
+
const finalVLineX = Math.max(minX, Math.min(maxX, nearestVLineX));
|
|
1007
|
+
|
|
1008
|
+
// Point on nearest horizontal line (X can vary along the line)
|
|
1009
|
+
const pointOnHLine = new THREE.Vector3(clampedX, finalHLineY, trellisZ);
|
|
1010
|
+
|
|
1011
|
+
// Point on nearest vertical line (Y can vary along the line)
|
|
1012
|
+
const pointOnVLine = new THREE.Vector3(finalVLineX, clampedY, trellisZ);
|
|
1013
|
+
|
|
1014
|
+
// Return whichever is closer
|
|
1015
|
+
const distH = position.distanceTo(pointOnHLine);
|
|
1016
|
+
const distV = position.distanceTo(pointOnVLine);
|
|
1017
|
+
|
|
1018
|
+
return distH < distV ? pointOnHLine : pointOnVLine;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Calculate the force vector toward the nearest trellis point
|
|
1023
|
+
* @param {THREE.Vector3} position Current section position
|
|
1024
|
+
* @param {number} radius Current section radius
|
|
1025
|
+
* @returns {{ direction: THREE.Vector3, strength: number } | null}
|
|
1026
|
+
*/
|
|
1027
|
+
calculateTrellisForce(position, radius) {
|
|
1028
|
+
const trellis = this.options.trellis;
|
|
1029
|
+
const nearestPoint = this.getNearestTrellisPoint(position);
|
|
1030
|
+
|
|
1031
|
+
const distance = position.distanceTo(nearestPoint);
|
|
1032
|
+
|
|
1033
|
+
// Only apply force within max distance
|
|
1034
|
+
if (distance > trellis.force.maxDistance) return null;
|
|
1035
|
+
if (distance < 0.001) return null; // Avoid division by zero
|
|
1036
|
+
|
|
1037
|
+
// Calculate direction toward trellis
|
|
1038
|
+
const direction = new THREE.Vector3()
|
|
1039
|
+
.subVectors(nearestPoint, position)
|
|
1040
|
+
.normalize();
|
|
1041
|
+
|
|
1042
|
+
// Calculate strength with distance falloff
|
|
1043
|
+
// Closer = stronger force, scaled by inverse radius (like existing force)
|
|
1044
|
+
const distanceFactor = 1 - Math.pow(
|
|
1045
|
+
distance / trellis.force.maxDistance,
|
|
1046
|
+
trellis.force.falloff,
|
|
1047
|
+
);
|
|
1048
|
+
const strength = trellis.force.strength * distanceFactor / radius;
|
|
1049
|
+
|
|
1050
|
+
return { direction, strength };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
get vertexCount() {
|
|
1054
|
+
// Leaves are instanced: 4 verts per instance.
|
|
1055
|
+
return this.branches.verts.length / 3 + this.leaves.instances.length * 4;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
get triangleCount() {
|
|
1059
|
+
// 2 triangles per leaf instance.
|
|
1060
|
+
return this.branches.indices.length / 3 + this.leaves.instances.length * 2;
|
|
1061
|
+
}
|
|
1062
|
+
}
|