Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions BOLT'S JOURNAL - PERFORMANCE LEARNINGS.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
- Direct matrix array manipulation (`instanceMatrix.array`) bypasses expensive Object3D composition and matrix allocations, significantly improving rendering batcher update loops.
## 2024-04-09 - TSL and GC Performance Rules\n**Learning:** In Three.js, TSL math nodes are generally faster and preferred over updating uniforms via JS every frame for performance optimization. For Three.js InstancedMesh objects, colors must be updated via `.setColorAt()`. Modifying the material directly will incorrectly affect all instances. In Candy World, collision detection handled in JavaScript becomes a severe bottleneck at >500 entities. AssemblyScript/WASM handles 2000+ entities efficiently.\n**Action:** Use TSL math nodes instead of JS uniforms whenever possible. Always use `.setColorAt()` for InstancedMesh colors. Use WASM for heavy collision detection.

## 2024-05-XX - Zero-Allocation Matrix Batching
**Learning:** Calling `Object3D.updateMatrix()` and `mesh.setMatrixAt()` inside update loops or batch generation code causes significant CPU overhead and garbage collection (GC) spikes because they instantiate intermediate objects and allocate arrays under the hood.
**Action:** For all `InstancedMesh` batchers, construct `Matrix4` locally using zero-allocation scratch variables (`_scratchMatrix.compose(pos, quat, scale)`) and copy the result directly to the underlying buffer memory using `_scratchMatrix.toArray(mesh.instanceMatrix.array, index * 16)`. Always follow up with `mesh.instanceMatrix.needsUpdate = true`.
3 changes: 2 additions & 1 deletion src/compute/gpu-foliage-animator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,8 @@ export async function updateInstancedMeshFromAnimator(
}

dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
dummy.matrix.toArray(mesh.instanceMatrix.array, (i) * 16);
}

mesh.instanceMatrix.needsUpdate = true;
Expand Down
3 changes: 2 additions & 1 deletion src/compute/gpu-particle-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,8 @@ export class GPUParticleSystem {
this.dummy.scale.set(scale, scale, scale);
this.dummy.updateMatrix();

this.particleMesh.setMatrixAt(i, this.dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
this.dummy.matrix.toArray(this.particleMesh.instanceMatrix.array, (i) * 16);
}

this.particleMesh.instanceMatrix.needsUpdate = true;
Expand Down
3 changes: 2 additions & 1 deletion src/foliage/cave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ export function createCaveEntrance(options: CaveOptions = {}): THREE.Group {
_scratchObj.scale.set(s * 0.5, s, s * 0.5);
_scratchObj.updateMatrix();

formationsMesh.setMatrixAt(i, _scratchObj.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchObj.matrix.toArray(formationsMesh.instanceMatrix.array, (i) * 16);
}
group.add(formationsMesh);

Expand Down
3 changes: 2 additions & 1 deletion src/foliage/cloud-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@ export class CloudBatcher {
for (let i = 0; i < count; i++) {
// Global = CloudWorld * PuffLocal
_scratchMat.multiplyMatrices(worldMat, puffs[i]);
this.mesh.setMatrixAt(start + i, _scratchMat);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMat.toArray(this.mesh.instanceMatrix.array, (start + i) * 16);
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/foliage/dandelion-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ export class DandelionBatcher {
_scratchScale.setScalar(scale);
_scratchMat.scale(_scratchScale);

this.mesh!.setMatrixAt(i, _scratchMat);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMat.toArray(this.mesh!.instanceMatrix.array, (i) * 16);
this.mesh!.instanceMatrix.needsUpdate = true;
this.mesh!.count = this.count;

Expand All @@ -273,7 +274,8 @@ export class DandelionBatcher {
this.dummy.scale.set(0, 0, 0);
this.dummy.updateMatrix();

this.mesh.setMatrixAt(batchIndex, this.dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
this.dummy.matrix.toArray(this.mesh.instanceMatrix.array, (batchIndex) * 16);
this.mesh.instanceMatrix.needsUpdate = true;

console.log(`[DandelionBatcher] Harvested dandelion #${batchIndex}`);
Expand Down
3 changes: 2 additions & 1 deletion src/foliage/flower-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ export class FlowerBatcher {

if (index >= max) return;

mesh.setMatrixAt(index, matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
matrix.toArray(mesh.instanceMatrix.array, (index) * 16);
if (color && mesh.instanceColor) {
mesh.setColorAt(index, color);
}
Expand Down
9 changes: 6 additions & 3 deletions src/foliage/glowing-flower-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ export class GlowingFlowerBatcher {
_scratchScale.set(0.05, stemHeight, 0.05);
_scratchMat.makeScale(_scratchScale.x, _scratchScale.y, _scratchScale.z);
_scratchMat.premultiply(baseMatrix);
this.stemMesh!.setMatrixAt(i, _scratchMat);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMat.toArray(this.stemMesh!.instanceMatrix.array, (i) * 16);

// Head Transform (At top of stem)
// ⚡ OPTIMIZATION: Re-use scratch variable to avoid GC spikes
Expand All @@ -221,7 +222,8 @@ export class GlowingFlowerBatcher {
_scratchMat2.scale(_scratchScale);
_scratchMat2.premultiply(baseMatrix);
const headWorld = _scratchMat2;
this.headMesh!.setMatrixAt(i, headWorld);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
headWorld.toArray(this.headMesh!.instanceMatrix.array, (i) * 16);

// Wash Transform (At top of stem)
// ⚡ OPTIMIZATION: Re-use scratch variable to avoid GC spikes
Expand All @@ -231,7 +233,8 @@ export class GlowingFlowerBatcher {
_scratchMat3.scale(_scratchScale);
_scratchMat3.premultiply(baseMatrix);
const washWorld = _scratchMat3;
this.washMesh!.setMatrixAt(i, washWorld);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
washWorld.toArray(this.washMesh!.instanceMatrix.array, (i) * 16);

// Color
if (typeof color === 'number') _scratchColor.setHex(color);
Expand Down
3 changes: 2 additions & 1 deletion src/foliage/grass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ export function addGrassInstance(x: number, y: number, z: number) {
dummy.scale.set(s, s, s);

dummy.updateMatrix();
mesh.setMatrixAt(index, dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
dummy.matrix.toArray(mesh.instanceMatrix.array, (index) * 16);
mesh.count++;
mesh.instanceMatrix.needsUpdate = true;
}
Expand Down
6 changes: 4 additions & 2 deletions src/foliage/lantern-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,12 @@ export class LanternBatcher {
dummy.updateMatrix();

// Stem
this.stemMesh!.setMatrixAt(i, dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
dummy.matrix.toArray(this.stemMesh!.instanceMatrix.array, (i) * 16);

// Top
this.topMesh!.setMatrixAt(i, dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
dummy.matrix.toArray(this.topMesh!.instanceMatrix.array, (i) * 16);

// Params
const height = options.height || 2.5;
Expand Down
3 changes: 2 additions & 1 deletion src/foliage/lod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,8 @@ export class FoliageLODManager {
mesh.count = count;

for (let i = 0; i < count; i++) {
mesh.setMatrixAt(i, matrices[i]);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
matrices[i].toArray(mesh.instanceMatrix.array, (i) * 16);
mesh.setColorAt(i, colors[i]);
}

Expand Down
6 changes: 4 additions & 2 deletions src/foliage/mushroom-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,8 @@ export class MushroomBatcher {

// 1. Set Matrix
dummy.updateMatrix();
this.mesh!.setMatrixAt(i, dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
dummy.matrix.toArray(this.mesh!.instanceMatrix.array, (i) * 16);

// PALETTE: Set Color
// Default to Red (0xFF6B6B) if no note color provided
Expand Down Expand Up @@ -692,7 +693,8 @@ export class MushroomBatcher {
// A. Copy Attributes from Last to Removed
// Matrix
this.mesh!.getMatrixAt(lastIndex, _scratchMatrix);
this.mesh!.setMatrixAt(indexToRemove, _scratchMatrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (indexToRemove) * 16);

// Color
this.mesh!.getColorAt(lastIndex, _scratchColor);
Expand Down
18 changes: 12 additions & 6 deletions src/foliage/simple-flower-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,8 @@ export class SimpleFlowerBatcher {
_scratchScale.set(0.05, stemHeight, 0.05);
_scratchMat.makeScale(_scratchScale.x, _scratchScale.y, _scratchScale.z);
_scratchMat.premultiply(baseMatrix); // Apply World Transform
this.stemMesh!.setMatrixAt(i, _scratchMat);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMat.toArray(this.stemMesh!.instanceMatrix.array, (i) * 16);

// Head Transform (At top of stem)
// Translation(0, stemHeight, 0) relative to Base.
Expand All @@ -306,7 +307,8 @@ export class SimpleFlowerBatcher {
const headWorld = _scratchMat2; // No clone, avoid GC spike

// Petals
this.petalMesh!.setMatrixAt(i, headWorld);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
headWorld.toArray(this.petalMesh!.instanceMatrix.array, (i) * 16);

// Color
if (typeof color === 'number') _scratchColor.setHex(color);
Expand All @@ -317,20 +319,24 @@ export class SimpleFlowerBatcher {
// Center: Scale(0.1)
_scratchMat.makeScale(0.1, 0.1, 0.1);
_scratchMat.premultiply(headWorld);
this.centerMesh!.setMatrixAt(i, _scratchMat);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMat.toArray(this.centerMesh!.instanceMatrix.array, (i) * 16);

// Stamens: No extra scale needed (baked in geometry), just head transform
this.stamenMesh!.setMatrixAt(i, headWorld);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
headWorld.toArray(this.stamenMesh!.instanceMatrix.array, (i) * 16);

// Beam: Random chance
if (Math.random() > 0.5) {
_scratchMat.makeScale(0.1, 1.0, 0.1);
_scratchMat.premultiply(headWorld);
this.beamMesh!.setMatrixAt(i, _scratchMat);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMat.toArray(this.beamMesh!.instanceMatrix.array, (i) * 16);
} else {
_scratchMat.makeScale(0, 0, 0);
_scratchMat.premultiply(headWorld);
this.beamMesh!.setMatrixAt(i, _scratchMat);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMat.toArray(this.beamMesh!.instanceMatrix.array, (i) * 16);
}
this.beamMesh!.setColorAt(i, _scratchColor);

Expand Down
12 changes: 8 additions & 4 deletions src/foliage/waterfall-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ export class WaterfallBatcher {
_scratchPos.y -= height * 0.5;

_scratchMatrix.compose(_scratchPos, _scratchQuat, _scratchScale);
this.mesh!.setMatrixAt(index, _scratchMatrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (index) * 16);
this.mesh!.instanceMatrix.needsUpdate = true;

// 2. Setup Splashes (8 per waterfall)
Expand Down Expand Up @@ -276,7 +277,8 @@ export class WaterfallBatcher {

// Initialize matrix to identity (needed for rendering, even if positionNode overrides)
_scratchMatrix.identity();
this.splashMesh!.setMatrixAt(si, _scratchMatrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMatrix.toArray(this.splashMesh!.instanceMatrix.array, (si) * 16);
}

this.splashOrigin!.needsUpdate = true;
Expand All @@ -298,7 +300,8 @@ export class WaterfallBatcher {
if (indexToRemove !== lastIndex) {
// Swap Column
this.mesh!.getMatrixAt(lastIndex, _scratchMatrix);
this.mesh!.setMatrixAt(indexToRemove, _scratchMatrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (indexToRemove) * 16);

// Swap Splashes (Block of 8)
const srcStart = lastIndex * SPLASHES_PER_WATERFALL;
Expand Down Expand Up @@ -364,7 +367,8 @@ export class WaterfallBatcher {
_scratchScale.z = _scratchScale.x * thicknessScale;

_scratchMatrix.compose(_scratchPos, _scratchQuat, _scratchScale);
this.mesh!.setMatrixAt(index, _scratchMatrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (index) * 16);
this.mesh!.instanceMatrix.needsUpdate = true;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/gameplay/jitter-mines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ class JitterMineSystem {
_scratchDummy.scale.setScalar(1);
_scratchDummy.rotation.set(0,0,0);
_scratchDummy.updateMatrix();
this.mesh.setMatrixAt(i, _scratchDummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
_scratchDummy.matrix.toArray(this.mesh.instanceMatrix.array, (i) * 16);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/gameplay/rainbow-blaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ class ProjectilePool {
// WebGPU TSL multiplies custom positionNode output by instanceMatrix
const identityMatrix = new THREE.Matrix4();
for (let i = 0; i < MAX_PROJECTILES; i++) {
this.mesh.setMatrixAt(i, identityMatrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
identityMatrix.toArray(this.mesh.instanceMatrix.array, (i) * 16);
}
this.mesh.instanceMatrix.needsUpdate = true;

Expand Down
3 changes: 2 additions & 1 deletion src/systems/glitch-grenade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ class GlitchGrenadeSystem {
// ⚡ OPTIMIZATION: Write a pure identity matrix into the instanceMatrix buffer
const identityMatrix = new THREE.Matrix4();
for (let i = 0; i < MAX_GRENADES; i++) {
this.mesh.setMatrixAt(i, identityMatrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
identityMatrix.toArray(this.mesh.instanceMatrix.array, (i) * 16);
}
this.mesh.instanceMatrix.needsUpdate = true;

Expand Down
3 changes: 2 additions & 1 deletion src/systems/physics/physics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,8 @@ function checkHarmonyOrbs() {
harmonyOrbSystem.dummy.position.set(0, -9999, 0);
harmonyOrbSystem.dummy.scale.setScalar(0);
harmonyOrbSystem.dummy.updateMatrix();
harmonyOrbSystem.mesh.setMatrixAt(i, harmonyOrbSystem.dummy.matrix);
// ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt
harmonyOrbSystem.dummy.matrix.toArray(harmonyOrbSystem.mesh.instanceMatrix.array, (i) * 16);
harmonyOrbSystem.mesh.instanceMatrix.needsUpdate = true;

// Visuals & Logic
Expand Down
Loading