Spaces:
Running
Running
Codex CLI
commited on
Commit
·
09a3a37
1
Parent(s):
fe3647a
feat(world): switch tree materials to simpler Lambert shading for performance optimization
Browse files- src/combat.js +52 -6
- src/lighting.js +3 -2
- src/main.js +11 -3
- src/world.js +55 -73
src/combat.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CFG } from './config.js';
|
|
| 3 |
import { G } from './globals.js';
|
| 4 |
import { updateHUD } from './hud.js';
|
| 5 |
import { spawnTracer, spawnImpact, spawnMuzzleFlash } from './fx.js';
|
|
|
|
| 6 |
import { spawnShellCasing } from './casings.js';
|
| 7 |
import { popHelmet } from './helmets.js';
|
| 8 |
import { beginReload } from './weapon.js';
|
|
@@ -72,10 +73,55 @@ export function performShooting(delta) {
|
|
| 72 |
let end = TMPv2.clone().multiplyScalar(CFG.gun.range).add(G.camera.position);
|
| 73 |
let firstHit = null;
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
// Find enemy and hit zone by traversing up the hierarchy
|
| 80 |
function findEnemyAndZone(obj) {
|
| 81 |
let cur = obj;
|
|
@@ -105,14 +151,11 @@ export function performShooting(delta) {
|
|
| 105 |
popHelmet(enemy, shotDir, firstHit.point);
|
| 106 |
}
|
| 107 |
|
| 108 |
-
// Debug hit indicator removed to avoid per-shot allocations
|
| 109 |
-
|
| 110 |
if (enemy.hp <= 0) {
|
| 111 |
enemy.alive = false;
|
| 112 |
enemy.deathTimer = 0;
|
| 113 |
G.waves.aliveCount--;
|
| 114 |
G.player.score += isHead ? 15 : 10;
|
| 115 |
-
// Drop 1-5 green health orbs
|
| 116 |
const cnt = 1 + Math.floor(G.random() * 5);
|
| 117 |
spawnHealthOrbs(enemy.pos, cnt);
|
| 118 |
}
|
|
@@ -122,6 +165,9 @@ export function performShooting(delta) {
|
|
| 122 |
: UP;
|
| 123 |
spawnImpact(firstHit.point, n);
|
| 124 |
}
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
spawnTracer(TMPv1, end);
|
|
|
|
| 3 |
import { G } from './globals.js';
|
| 4 |
import { updateHUD } from './hud.js';
|
| 5 |
import { spawnTracer, spawnImpact, spawnMuzzleFlash } from './fx.js';
|
| 6 |
+
import { getTreesInAABB, getTerrainHeight } from './world.js';
|
| 7 |
import { spawnShellCasing } from './casings.js';
|
| 8 |
import { popHelmet } from './helmets.js';
|
| 9 |
import { beginReload } from './weapon.js';
|
|
|
|
| 73 |
let end = TMPv2.clone().multiplyScalar(CFG.gun.range).add(G.camera.position);
|
| 74 |
let firstHit = null;
|
| 75 |
|
| 76 |
+
// Choose nearest of raycast hit (enemy/ground) and tree trunk collision
|
| 77 |
+
const origin = G.camera.position;
|
| 78 |
+
const rayFirst = hits.length > 0 ? hits[0] : null;
|
| 79 |
+
const rayDist = rayFirst ? origin.distanceTo(rayFirst.point) : Infinity;
|
| 80 |
+
|
| 81 |
+
// Candidate tree hit along the segment (origin -> max range)
|
| 82 |
+
let treeHitU = Infinity;
|
| 83 |
+
{
|
| 84 |
+
const minX = Math.min(origin.x, end.x) - 2.0;
|
| 85 |
+
const maxX = Math.max(origin.x, end.x) + 2.0;
|
| 86 |
+
const minZ = Math.min(origin.z, end.z) - 2.0;
|
| 87 |
+
const maxZ = Math.max(origin.z, end.z) + 2.0;
|
| 88 |
+
const cands = getTreesInAABB(minX, minZ, maxX, maxZ);
|
| 89 |
+
const ox = origin.x, oz = origin.z;
|
| 90 |
+
const ex = end.x, ez = end.z;
|
| 91 |
+
const vx = ex - ox, vz = ez - oz;
|
| 92 |
+
const vv = vx * vx + vz * vz || 1;
|
| 93 |
+
for (let i = 0; i < cands.length; i++) {
|
| 94 |
+
const t = cands[i];
|
| 95 |
+
const wx = t.x - ox, wz = t.z - oz;
|
| 96 |
+
let u = (wx * vx + wz * vz) / vv;
|
| 97 |
+
if (u < 0) u = 0; else if (u > 1) u = 1;
|
| 98 |
+
const px = ox + u * vx, pz = oz + u * vz;
|
| 99 |
+
const dx = t.x - px, dz = t.z - pz;
|
| 100 |
+
const rr = (t.radius + 0.2) * (t.radius + 0.2);
|
| 101 |
+
if (dx * dx + dz * dz <= rr) {
|
| 102 |
+
const yAt = origin.y + (end.y - origin.y) * u;
|
| 103 |
+
if (yAt < 8 && u < treeHitU) treeHitU = u;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
|
| 108 |
+
if (treeHitU !== Infinity && (treeHitU * origin.distanceTo(end)) < rayDist) {
|
| 109 |
+
// Trunk is the nearest hit
|
| 110 |
+
const hitPos = new THREE.Vector3(
|
| 111 |
+
origin.x + (end.x - origin.x) * treeHitU,
|
| 112 |
+
origin.y + (end.y - origin.y) * treeHitU,
|
| 113 |
+
origin.z + (end.z - origin.z) * treeHitU
|
| 114 |
+
);
|
| 115 |
+
const gy = getTerrainHeight(hitPos.x, hitPos.z) + 0.02;
|
| 116 |
+
if (hitPos.y < gy) hitPos.y = gy;
|
| 117 |
+
end.copy(hitPos);
|
| 118 |
+
firstHit = { point: hitPos, object: null, face: null };
|
| 119 |
+
} else if (rayFirst) {
|
| 120 |
+
firstHit = rayFirst;
|
| 121 |
+
end.copy(rayFirst.point);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
if (firstHit && firstHit.object) {
|
| 125 |
// Find enemy and hit zone by traversing up the hierarchy
|
| 126 |
function findEnemyAndZone(obj) {
|
| 127 |
let cur = obj;
|
|
|
|
| 151 |
popHelmet(enemy, shotDir, firstHit.point);
|
| 152 |
}
|
| 153 |
|
|
|
|
|
|
|
| 154 |
if (enemy.hp <= 0) {
|
| 155 |
enemy.alive = false;
|
| 156 |
enemy.deathTimer = 0;
|
| 157 |
G.waves.aliveCount--;
|
| 158 |
G.player.score += isHead ? 15 : 10;
|
|
|
|
| 159 |
const cnt = 1 + Math.floor(G.random() * 5);
|
| 160 |
spawnHealthOrbs(enemy.pos, cnt);
|
| 161 |
}
|
|
|
|
| 165 |
: UP;
|
| 166 |
spawnImpact(firstHit.point, n);
|
| 167 |
}
|
| 168 |
+
} else if (firstHit && !firstHit.object) {
|
| 169 |
+
// Blocked by a tree trunk approximation
|
| 170 |
+
spawnImpact(firstHit.point, UP);
|
| 171 |
}
|
| 172 |
|
| 173 |
spawnTracer(TMPv1, end);
|
src/lighting.js
CHANGED
|
@@ -38,8 +38,9 @@ export function setupLights() {
|
|
| 38 |
sun.shadow.camera.bottom = -60;
|
| 39 |
sun.shadow.camera.near = 0.1;
|
| 40 |
sun.shadow.camera.far = 240;
|
| 41 |
-
|
| 42 |
-
sun.shadow.mapSize.
|
|
|
|
| 43 |
sun.target = new THREE.Object3D();
|
| 44 |
G.scene.add(sun);
|
| 45 |
G.scene.add(sun.target);
|
|
|
|
| 38 |
sun.shadow.camera.bottom = -60;
|
| 39 |
sun.shadow.camera.near = 0.1;
|
| 40 |
sun.shadow.camera.far = 240;
|
| 41 |
+
// Smaller shadow map for performance
|
| 42 |
+
sun.shadow.mapSize.width = 512;
|
| 43 |
+
sun.shadow.mapSize.height = 512;
|
| 44 |
sun.target = new THREE.Object3D();
|
| 45 |
G.scene.add(sun);
|
| 46 |
G.scene.add(sun.target);
|
src/main.js
CHANGED
|
@@ -33,8 +33,8 @@ function init() {
|
|
| 33 |
// Lower pixel ratio cap for significant fill-rate savings
|
| 34 |
G.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.0));
|
| 35 |
G.renderer.shadowMap.enabled = true;
|
| 36 |
-
//
|
| 37 |
-
G.renderer.shadowMap.type = THREE.
|
| 38 |
document.body.appendChild(G.renderer.domElement);
|
| 39 |
|
| 40 |
// Scene
|
|
@@ -103,6 +103,11 @@ function init() {
|
|
| 103 |
beginReload,
|
| 104 |
updateWeaponAnchor
|
| 105 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
function startGame() {
|
|
@@ -242,7 +247,10 @@ function animate() {
|
|
| 242 |
if (G._fpsAccum >= G._fpsNext) {
|
| 243 |
const fps = Math.round(G._fpsFrames / G._fpsAccum);
|
| 244 |
const el = document.getElementById('fps');
|
| 245 |
-
if (el)
|
|
|
|
|
|
|
|
|
|
| 246 |
G._fpsAccum = 0; G._fpsFrames = 0;
|
| 247 |
}
|
| 248 |
|
|
|
|
| 33 |
// Lower pixel ratio cap for significant fill-rate savings
|
| 34 |
G.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.0));
|
| 35 |
G.renderer.shadowMap.enabled = true;
|
| 36 |
+
// Use the cheapest shadow filter for CPU savings
|
| 37 |
+
G.renderer.shadowMap.type = THREE.BasicShadowMap;
|
| 38 |
document.body.appendChild(G.renderer.domElement);
|
| 39 |
|
| 40 |
// Scene
|
|
|
|
| 103 |
beginReload,
|
| 104 |
updateWeaponAnchor
|
| 105 |
});
|
| 106 |
+
|
| 107 |
+
// Pre-warm shaders to avoid first-frame hitches
|
| 108 |
+
if (G.renderer && G.scene && G.camera) {
|
| 109 |
+
try { G.renderer.compile(G.scene, G.camera); } catch (_) {}
|
| 110 |
+
}
|
| 111 |
}
|
| 112 |
|
| 113 |
function startGame() {
|
|
|
|
| 247 |
if (G._fpsAccum >= G._fpsNext) {
|
| 248 |
const fps = Math.round(G._fpsFrames / G._fpsAccum);
|
| 249 |
const el = document.getElementById('fps');
|
| 250 |
+
if (el) {
|
| 251 |
+
const info = G.renderer?.info?.render || { calls: 0, triangles: 0 };
|
| 252 |
+
el.textContent = `${fps} | calls:${info.calls} tris:${info.triangles}`;
|
| 253 |
+
}
|
| 254 |
G._fpsAccum = 0; G._fpsFrames = 0;
|
| 255 |
}
|
| 256 |
|
src/world.js
CHANGED
|
@@ -6,17 +6,16 @@ import { G } from './globals.js';
|
|
| 6 |
// We keep these module-scoped so all trees can share them efficiently.
|
| 7 |
const FOLIAGE_WIND = { uTime: { value: 0 }, uStrength: { value: 0.35 } };
|
| 8 |
|
| 9 |
-
|
|
|
|
| 10 |
color: 0x6b4f32,
|
| 11 |
-
|
| 12 |
-
metalness: 0.0
|
| 13 |
});
|
| 14 |
|
| 15 |
// Foliage material with simple vertex sway, inspired by reference project
|
| 16 |
-
const FOLIAGE_MAT = new THREE.
|
| 17 |
color: 0x2f6b3d,
|
| 18 |
-
|
| 19 |
-
metalness: 0.0
|
| 20 |
});
|
| 21 |
FOLIAGE_MAT.onBeforeCompile = (shader) => {
|
| 22 |
shader.uniforms.uTime = FOLIAGE_WIND.uTime;
|
|
@@ -74,10 +73,8 @@ export function tickForest(timeSec) {
|
|
| 74 |
|
| 75 |
// --- Ground cover (grass, flowers, bushes, rocks) ---
|
| 76 |
// Shared materials
|
| 77 |
-
const GRASS_MAT = new THREE.
|
| 78 |
color: 0xffffff,
|
| 79 |
-
roughness: 0.95,
|
| 80 |
-
metalness: 0.0,
|
| 81 |
vertexColors: true,
|
| 82 |
side: THREE.DoubleSide
|
| 83 |
});
|
|
@@ -106,10 +103,8 @@ GRASS_MAT.onBeforeCompile = (shader) => {
|
|
| 106 |
};
|
| 107 |
GRASS_MAT.needsUpdate = true;
|
| 108 |
|
| 109 |
-
const FLOWER_MAT = new THREE.
|
| 110 |
color: 0xffffff,
|
| 111 |
-
roughness: 0.9,
|
| 112 |
-
metalness: 0.0,
|
| 113 |
vertexColors: true,
|
| 114 |
side: THREE.DoubleSide
|
| 115 |
});
|
|
@@ -137,17 +132,13 @@ FLOWER_MAT.onBeforeCompile = (shader) => {
|
|
| 137 |
};
|
| 138 |
FLOWER_MAT.needsUpdate = true;
|
| 139 |
|
| 140 |
-
const BUSH_MAT = new THREE.
|
| 141 |
color: 0x2b6a37,
|
| 142 |
-
roughness: 0.95,
|
| 143 |
-
metalness: 0.0,
|
| 144 |
flatShading: false
|
| 145 |
});
|
| 146 |
|
| 147 |
-
const ROCK_MAT = new THREE.
|
| 148 |
color: 0x7b7066,
|
| 149 |
-
roughness: 1.0,
|
| 150 |
-
metalness: 0.0,
|
| 151 |
flatShading: true
|
| 152 |
});
|
| 153 |
|
|
@@ -757,14 +748,8 @@ export function setupGround() {
|
|
| 757 |
ground.receiveShadow = true;
|
| 758 |
G.scene.add(ground);
|
| 759 |
G.ground = ground;
|
| 760 |
-
//
|
| 761 |
-
|
| 762 |
-
G.blockers = [ground, ...G.treeTrunks];
|
| 763 |
-
} else if (G.treeMeshes && Array.isArray(G.treeMeshes) && G.treeMeshes.length) {
|
| 764 |
-
G.blockers = [ground, ...G.treeMeshes];
|
| 765 |
-
} else {
|
| 766 |
-
G.blockers = [ground];
|
| 767 |
-
}
|
| 768 |
}
|
| 769 |
|
| 770 |
export function generateForest() {
|
|
@@ -774,14 +759,28 @@ export function generateForest() {
|
|
| 774 |
const maxAttempts = CFG.treeCount * 3;
|
| 775 |
let attempts = 0;
|
| 776 |
|
|
|
|
| 777 |
G.treeColliders.length = 0;
|
| 778 |
-
G.treeMeshes.length = 0;
|
| 779 |
if (!G.treeTrunks) G.treeTrunks = [];
|
| 780 |
-
G.treeTrunks.length = 0;
|
|
|
|
|
|
|
| 781 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
while (placed < CFG.treeCount && attempts < maxAttempts) {
|
| 783 |
attempts++;
|
| 784 |
-
|
| 785 |
const x = (G.random() - 0.5) * CFG.forestSize;
|
| 786 |
const z = (G.random() - 0.5) * CFG.forestSize;
|
| 787 |
|
|
@@ -793,63 +792,46 @@ export function generateForest() {
|
|
| 793 |
for (const collider of G.treeColliders) {
|
| 794 |
const dx = x - collider.x;
|
| 795 |
const dz = z - collider.z;
|
| 796 |
-
if (dx * dx + dz * dz < Math.pow(collider.radius * 2, 2)) {
|
| 797 |
-
tooClose = true;
|
| 798 |
-
break;
|
| 799 |
-
}
|
| 800 |
}
|
| 801 |
if (tooClose) continue;
|
| 802 |
|
| 803 |
-
//
|
| 804 |
-
const tree = new THREE.Group();
|
| 805 |
-
|
| 806 |
-
// Random uniform scale for size variety
|
| 807 |
const s = 0.75 + G.random() * 0.8; // ~0.75..1.55
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
trunk.scale.set(s, s, s);
|
| 812 |
-
trunk.castShadow = true;
|
| 813 |
-
trunk.receiveShadow = true;
|
| 814 |
-
tree.add(trunk);
|
| 815 |
-
|
| 816 |
-
// Foliage: three cones + a small spherical crown
|
| 817 |
-
const foliage1 = new THREE.Mesh(GEO_CONE1, FOLIAGE_MAT);
|
| 818 |
-
const foliage2 = new THREE.Mesh(GEO_CONE2, FOLIAGE_MAT);
|
| 819 |
-
const foliage3 = new THREE.Mesh(GEO_CONE3, FOLIAGE_MAT);
|
| 820 |
-
const crown = new THREE.Mesh(GEO_SPH, FOLIAGE_MAT);
|
| 821 |
-
const fScale = s * (0.9 + G.random() * 0.15); // slight variance per tree
|
| 822 |
-
foliage1.scale.set(fScale, fScale, fScale);
|
| 823 |
-
foliage2.scale.set(fScale, fScale, fScale);
|
| 824 |
-
foliage3.scale.set(fScale, fScale, fScale);
|
| 825 |
-
crown.scale.set(fScale, fScale, fScale);
|
| 826 |
-
// Keep only trunk casting shadows; foliage receiving only to reduce shadow pass cost
|
| 827 |
-
foliage1.castShadow = foliage2.castShadow = foliage3.castShadow = crown.castShadow = false;
|
| 828 |
-
foliage1.receiveShadow = foliage2.receiveShadow = foliage3.receiveShadow = crown.receiveShadow = true;
|
| 829 |
-
tree.add(foliage1, foliage2, foliage3, crown);
|
| 830 |
-
|
| 831 |
-
// Random rotation for natural look
|
| 832 |
-
tree.rotation.y = G.random() * Math.PI * 2;
|
| 833 |
-
|
| 834 |
const y = getTerrainHeight(x, z);
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
G.treeMeshes.push(tree);
|
| 838 |
-
G.treeTrunks.push(trunk);
|
| 839 |
|
| 840 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 841 |
const trunkBaseRadius = 1.2 * s;
|
| 842 |
G.treeColliders.push({ x, z, radius: trunkBaseRadius });
|
| 843 |
-
|
| 844 |
placed++;
|
| 845 |
}
|
| 846 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
if (G.ground) {
|
| 848 |
-
G.blockers = [G.ground
|
| 849 |
} else {
|
| 850 |
-
G.blockers = [
|
| 851 |
}
|
| 852 |
-
// Build spatial index for tree colliders to accelerate queries
|
| 853 |
buildTreeGrid();
|
| 854 |
}
|
| 855 |
|
|
|
|
| 6 |
// We keep these module-scoped so all trees can share them efficiently.
|
| 7 |
const FOLIAGE_WIND = { uTime: { value: 0 }, uStrength: { value: 0.35 } };
|
| 8 |
|
| 9 |
+
// Use simpler Lambert shading for mass content to reduce uniforms
|
| 10 |
+
const TRUNK_MAT = new THREE.MeshLambertMaterial({
|
| 11 |
color: 0x6b4f32,
|
| 12 |
+
emissive: 0x000000
|
|
|
|
| 13 |
});
|
| 14 |
|
| 15 |
// Foliage material with simple vertex sway, inspired by reference project
|
| 16 |
+
const FOLIAGE_MAT = new THREE.MeshLambertMaterial({
|
| 17 |
color: 0x2f6b3d,
|
| 18 |
+
emissive: 0x000000
|
|
|
|
| 19 |
});
|
| 20 |
FOLIAGE_MAT.onBeforeCompile = (shader) => {
|
| 21 |
shader.uniforms.uTime = FOLIAGE_WIND.uTime;
|
|
|
|
| 73 |
|
| 74 |
// --- Ground cover (grass, flowers, bushes, rocks) ---
|
| 75 |
// Shared materials
|
| 76 |
+
const GRASS_MAT = new THREE.MeshLambertMaterial({
|
| 77 |
color: 0xffffff,
|
|
|
|
|
|
|
| 78 |
vertexColors: true,
|
| 79 |
side: THREE.DoubleSide
|
| 80 |
});
|
|
|
|
| 103 |
};
|
| 104 |
GRASS_MAT.needsUpdate = true;
|
| 105 |
|
| 106 |
+
const FLOWER_MAT = new THREE.MeshLambertMaterial({
|
| 107 |
color: 0xffffff,
|
|
|
|
|
|
|
| 108 |
vertexColors: true,
|
| 109 |
side: THREE.DoubleSide
|
| 110 |
});
|
|
|
|
| 132 |
};
|
| 133 |
FLOWER_MAT.needsUpdate = true;
|
| 134 |
|
| 135 |
+
const BUSH_MAT = new THREE.MeshLambertMaterial({
|
| 136 |
color: 0x2b6a37,
|
|
|
|
|
|
|
| 137 |
flatShading: false
|
| 138 |
});
|
| 139 |
|
| 140 |
+
const ROCK_MAT = new THREE.MeshLambertMaterial({
|
| 141 |
color: 0x7b7066,
|
|
|
|
|
|
|
| 142 |
flatShading: true
|
| 143 |
});
|
| 144 |
|
|
|
|
| 748 |
ground.receiveShadow = true;
|
| 749 |
G.scene.add(ground);
|
| 750 |
G.ground = ground;
|
| 751 |
+
// With instanced trees we approximate trunk blocking via spatial grid; keep raycast blockers minimal
|
| 752 |
+
G.blockers = [ground];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
}
|
| 754 |
|
| 755 |
export function generateForest() {
|
|
|
|
| 759 |
const maxAttempts = CFG.treeCount * 3;
|
| 760 |
let attempts = 0;
|
| 761 |
|
| 762 |
+
// Reset data
|
| 763 |
G.treeColliders.length = 0;
|
|
|
|
| 764 |
if (!G.treeTrunks) G.treeTrunks = [];
|
| 765 |
+
G.treeTrunks.length = 0; // retained for compatibility; no longer populated with individual meshes
|
| 766 |
+
if (!G.treeMeshes) G.treeMeshes = [];
|
| 767 |
+
G.treeMeshes.length = 0;
|
| 768 |
|
| 769 |
+
// Prepare instanced batches (upper bound capacity = CFG.treeCount)
|
| 770 |
+
const trunkIM = new THREE.InstancedMesh(GEO_TRUNK, TRUNK_MAT, CFG.treeCount);
|
| 771 |
+
trunkIM.castShadow = true; trunkIM.receiveShadow = true;
|
| 772 |
+
const cone1IM = new THREE.InstancedMesh(GEO_CONE1, FOLIAGE_MAT, CFG.treeCount);
|
| 773 |
+
const cone2IM = new THREE.InstancedMesh(GEO_CONE2, FOLIAGE_MAT, CFG.treeCount);
|
| 774 |
+
const cone3IM = new THREE.InstancedMesh(GEO_CONE3, FOLIAGE_MAT, CFG.treeCount);
|
| 775 |
+
const crownIM = new THREE.InstancedMesh(GEO_SPH, FOLIAGE_MAT, CFG.treeCount);
|
| 776 |
+
cone1IM.castShadow = cone2IM.castShadow = cone3IM.castShadow = crownIM.castShadow = false;
|
| 777 |
+
cone1IM.receiveShadow = cone2IM.receiveShadow = cone3IM.receiveShadow = crownIM.receiveShadow = true;
|
| 778 |
+
|
| 779 |
+
const m = new THREE.Matrix4();
|
| 780 |
+
const quat = new THREE.Quaternion();
|
| 781 |
+
const scl = new THREE.Vector3();
|
| 782 |
while (placed < CFG.treeCount && attempts < maxAttempts) {
|
| 783 |
attempts++;
|
|
|
|
| 784 |
const x = (G.random() - 0.5) * CFG.forestSize;
|
| 785 |
const z = (G.random() - 0.5) * CFG.forestSize;
|
| 786 |
|
|
|
|
| 792 |
for (const collider of G.treeColliders) {
|
| 793 |
const dx = x - collider.x;
|
| 794 |
const dz = z - collider.z;
|
| 795 |
+
if (dx * dx + dz * dz < Math.pow(collider.radius * 2, 2)) { tooClose = true; break; }
|
|
|
|
|
|
|
|
|
|
| 796 |
}
|
| 797 |
if (tooClose) continue;
|
| 798 |
|
| 799 |
+
// Random uniform scale and rotation per tree
|
|
|
|
|
|
|
|
|
|
| 800 |
const s = 0.75 + G.random() * 0.8; // ~0.75..1.55
|
| 801 |
+
const fScale = s * (0.9 + G.random() * 0.15);
|
| 802 |
+
const yaw = G.random() * Math.PI * 2;
|
| 803 |
+
quat.setFromEuler(new THREE.Euler(0, yaw, 0));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
const y = getTerrainHeight(x, z);
|
| 805 |
+
m.compose(new THREE.Vector3(x, y, z), quat, new THREE.Vector3(s, s, s));
|
| 806 |
+
trunkIM.setMatrixAt(placed, m);
|
|
|
|
|
|
|
| 807 |
|
| 808 |
+
// Foliage uses same transform but with foliage scale factor
|
| 809 |
+
m.compose(new THREE.Vector3(x, y, z), quat, new THREE.Vector3(fScale, fScale, fScale));
|
| 810 |
+
cone1IM.setMatrixAt(placed, m);
|
| 811 |
+
cone2IM.setMatrixAt(placed, m);
|
| 812 |
+
cone3IM.setMatrixAt(placed, m);
|
| 813 |
+
crownIM.setMatrixAt(placed, m);
|
| 814 |
+
|
| 815 |
+
// Collider roughly matching trunk base radius
|
| 816 |
const trunkBaseRadius = 1.2 * s;
|
| 817 |
G.treeColliders.push({ x, z, radius: trunkBaseRadius });
|
|
|
|
| 818 |
placed++;
|
| 819 |
}
|
| 820 |
+
trunkIM.count = placed; cone1IM.count = placed; cone2IM.count = placed; cone3IM.count = placed; crownIM.count = placed;
|
| 821 |
+
trunkIM.instanceMatrix.needsUpdate = true;
|
| 822 |
+
cone1IM.instanceMatrix.needsUpdate = cone2IM.instanceMatrix.needsUpdate = true;
|
| 823 |
+
cone3IM.instanceMatrix.needsUpdate = crownIM.instanceMatrix.needsUpdate = true;
|
| 824 |
+
|
| 825 |
+
if (placed > 0) {
|
| 826 |
+
G.scene.add(trunkIM, cone1IM, cone2IM, cone3IM, crownIM);
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
// With instancing, keep blockers to ground; tree collisions handled via grid tests
|
| 830 |
if (G.ground) {
|
| 831 |
+
G.blockers = [G.ground];
|
| 832 |
} else {
|
| 833 |
+
G.blockers = [];
|
| 834 |
}
|
|
|
|
| 835 |
buildTreeGrid();
|
| 836 |
}
|
| 837 |
|