Codex CLI commited on
Commit
09a3a37
·
1 Parent(s): fe3647a

feat(world): switch tree materials to simpler Lambert shading for performance optimization

Browse files
Files changed (4) hide show
  1. src/combat.js +52 -6
  2. src/lighting.js +3 -2
  3. src/main.js +11 -3
  4. 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
- if (hits.length > 0) {
76
- firstHit = hits[0];
77
- end.copy(firstHit.point);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- sun.shadow.mapSize.width = 1024;
42
- sun.shadow.mapSize.height = 1024;
 
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
- // Slightly cheaper shadow filter
37
- G.renderer.shadowMap.type = THREE.PCFShadowMap;
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) el.textContent = String(fps);
 
 
 
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
- const TRUNK_MAT = new THREE.MeshStandardMaterial({
 
10
  color: 0x6b4f32,
11
- roughness: 0.9,
12
- metalness: 0.0
13
  });
14
 
15
  // Foliage material with simple vertex sway, inspired by reference project
16
- const FOLIAGE_MAT = new THREE.MeshStandardMaterial({
17
  color: 0x2f6b3d,
18
- roughness: 0.8,
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.MeshStandardMaterial({
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.MeshStandardMaterial({
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.MeshStandardMaterial({
141
  color: 0x2b6a37,
142
- roughness: 0.95,
143
- metalness: 0.0,
144
  flatShading: false
145
  });
146
 
147
- const ROCK_MAT = new THREE.MeshStandardMaterial({
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
- // Keep blockers in sync if trees already exist
761
- if (G.treeTrunks && Array.isArray(G.treeTrunks) && G.treeTrunks.length) {
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
- // Create a layered pine-like tree with shared materials and wind-swaying foliage
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
- // Trunk (reuses base geometry, scaled)
810
- const trunk = new THREE.Mesh(GEO_TRUNK, TRUNK_MAT);
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
- tree.position.set(x, y, z);
836
- G.scene.add(tree);
837
- G.treeMeshes.push(tree);
838
- G.treeTrunks.push(trunk);
839
 
840
- // Add collider roughly matching trunk base radius
 
 
 
 
 
 
 
841
  const trunkBaseRadius = 1.2 * s;
842
  G.treeColliders.push({ x, z, radius: trunkBaseRadius });
843
-
844
  placed++;
845
  }
846
- // Update blockers list once trees are generated
 
 
 
 
 
 
 
 
 
847
  if (G.ground) {
848
- G.blockers = [G.ground, ...G.treeTrunks];
849
  } else {
850
- G.blockers = [...G.treeTrunks];
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