mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(globe): Navara's shadowed sun leaked a light per frame
Navara 0.2.1's transparent pass refreshes its copy of every scene light each frame with copy(light, true). For the sun's cascaded shadow lights (three-csm CascadedDirectionalLights) that copy clones every cascade, so each frame brought new DirectionalLights with new ids and three's WebGLLights uniforms cache (keyed by id) grew by about 1.2 KB a frame in the looks whose sun casts shadows (minimal, photoreal). src/globe/navara/cascadedLightCopy.js patches the copy to update the cascades it has in place (pose, colour, intensity, target, shadow settings and camera) when the counts match, and keeps the rebuild otherwise; internals.js finds the class among the view's scene lights (private view._scenes.light) and applies it when the globe is built. Tests on three-csm's own class: the original copy changes the light ids, the patched one keeps the lights and takes the values. Post-GC JS heap (CDP HeapProfiler.collectGarbage every 10 s, the app on ?engine=navara rendering continuously at 60 fps): minimal 4.33 → 0.20 MB/min over 70 s and 0.06 MB/min over 180 s; photoreal 4.34 → 0.19 MB/min; cinematic (no shadow) 0.34. Added to the upstream asks.
This commit is contained in:
1 parent
c327a25744
commit
8812117013
5 files changed
+185
-2
No files matched your search
@@ -1080,5 +1080,6 @@
|
||||
"src/globe/navara/lines/lineMesh.test.mjs",
|
||||
"src/globe/pickDelivery.test.mjs",
|
||||
"src/globe/navara/picking.test.mjs",
|
||||
"src/data/selectionDispatcher.test.mjs"
|
||||
"src/data/selectionDispatcher.test.mjs",
|
||||
"src/globe/navara/cascadedLightCopy.test.mjs"
|
||||
]
|
||||
@@ -0,0 +1,85 @@
|
||||
// Navara 0.2.1 leak fix (phase 3 review): Navara's transparent pass keeps a
|
||||
// copy of each scene light and refreshes it every frame with
|
||||
// `copy.copy(light, true)`. For the sun's cascaded shadow lights
|
||||
// (@navaramap/three-csm CascadedDirectionalLights) that copy clones every
|
||||
// cascade, so each frame brings new DirectionalLight objects with new ids,
|
||||
// and three's WebGLLights uniforms cache (keyed by light id) grows by about
|
||||
// 1.2 KB a frame in looks with a shadowed sun. The patched copy updates the
|
||||
// cascades it already has in place (pose, colour, intensity, target, shadow
|
||||
// settings and camera) when the cascade counts match, and keeps the original
|
||||
// rebuild otherwise. internals.js applies it to the class Navara uses.
|
||||
// Upstream ask: copy cascaded lights in place (or sync without copy()).
|
||||
|
||||
const PATCHED = Symbol('gev.cascadedLightCopy');
|
||||
|
||||
function copyLightInPlace(target, source) {
|
||||
target.position.copy(source.position);
|
||||
target.quaternion.copy(source.quaternion);
|
||||
target.scale.copy(source.scale);
|
||||
target.matrix.copy(source.matrix);
|
||||
target.matrixWorld.copy(source.matrixWorld);
|
||||
target.matrixAutoUpdate = source.matrixAutoUpdate;
|
||||
target.visible = source.visible;
|
||||
target.castShadow = source.castShadow;
|
||||
target.layers.mask = source.layers.mask;
|
||||
target.color.copy(source.color);
|
||||
target.intensity = source.intensity;
|
||||
const to = target.target;
|
||||
const from = source.target;
|
||||
to.position.copy(from.position);
|
||||
to.quaternion.copy(from.quaternion);
|
||||
to.matrix.copy(from.matrix);
|
||||
to.matrixWorld.copy(from.matrixWorld);
|
||||
const shadow = target.shadow;
|
||||
const original = source.shadow;
|
||||
shadow.intensity = original.intensity;
|
||||
shadow.bias = original.bias;
|
||||
shadow.normalBias = original.normalBias;
|
||||
shadow.radius = original.radius;
|
||||
shadow.blurSamples = original.blurSamples;
|
||||
shadow.mapSize.copy(original.mapSize);
|
||||
shadow.camera.copy(original.camera);
|
||||
shadow.camera.updateProjectionMatrix?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches CascadedDirectionalLights.prototype.copy (idempotent).
|
||||
* @returns {() => void} restore the original copy
|
||||
*/
|
||||
export function patchCascadedLightCopy(proto) {
|
||||
if (proto.copy[PATCHED]) return proto.copy[PATCHED];
|
||||
const original = proto.copy;
|
||||
if (typeof original !== 'function')
|
||||
throw new Error('CascadedDirectionalLights.copy not found');
|
||||
function copyInPlace(source, recursive) {
|
||||
const mine = this.cascadedLights;
|
||||
const theirs = source?.cascadedLights;
|
||||
if (
|
||||
!Array.isArray(mine) ||
|
||||
!Array.isArray(theirs) ||
|
||||
mine.length === 0 ||
|
||||
mine.length !== theirs.length
|
||||
)
|
||||
return original.call(this, source, recursive);
|
||||
// Object3D's own fields, without children (as the original's
|
||||
// super.copy(source, false)).
|
||||
this.name = source.name;
|
||||
this.position.copy(source.position);
|
||||
this.quaternion.copy(source.quaternion);
|
||||
this.scale.copy(source.scale);
|
||||
this.matrix.copy(source.matrix);
|
||||
this.matrixWorld.copy(source.matrixWorld);
|
||||
this.visible = source.visible;
|
||||
this.direction?.copy?.(source.direction);
|
||||
for (let index = 0; index < mine.length; index++)
|
||||
copyLightInPlace(mine[index], theirs[index]);
|
||||
return this;
|
||||
}
|
||||
copyInPlace.gevInPlace = true;
|
||||
const restore = () => {
|
||||
if (proto.copy === copyInPlace) proto.copy = original;
|
||||
};
|
||||
copyInPlace[PATCHED] = restore;
|
||||
proto.copy = copyInPlace;
|
||||
return restore;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import csm from '@navaramap/three-csm';
|
||||
import { patchCascadedLightCopy } from './cascadedLightCopy.js';
|
||||
|
||||
const { CascadedDirectionalLights } = csm;
|
||||
|
||||
function sun(count = 3) {
|
||||
const light = new CascadedDirectionalLights(0xffffff, 2);
|
||||
light.setCount(count);
|
||||
light.cascadedLights.forEach((cascade, index) => {
|
||||
cascade.position.set(index, 2, 3);
|
||||
cascade.target.position.set(index, 0, 0);
|
||||
cascade.updateMatrixWorld(true);
|
||||
cascade.target.updateMatrixWorld(true);
|
||||
cascade.shadow.bias = -0.001 * (index + 1);
|
||||
});
|
||||
return light;
|
||||
}
|
||||
|
||||
test("Navara 0.2.1's copy clones new lights every call (the leak)", () => {
|
||||
const source = sun();
|
||||
const copy = new CascadedDirectionalLights();
|
||||
copy.copy(source, true);
|
||||
const ids = copy.cascadedLights.map((light) => light.id);
|
||||
copy.copy(source, true);
|
||||
assert.notDeepEqual(
|
||||
copy.cascadedLights.map((light) => light.id),
|
||||
ids,
|
||||
);
|
||||
});
|
||||
|
||||
test('patched, a copy keeps its lights and takes the values', () => {
|
||||
const restore = patchCascadedLightCopy(CascadedDirectionalLights.prototype);
|
||||
try {
|
||||
const source = sun();
|
||||
const copy = new CascadedDirectionalLights();
|
||||
copy.copy(source, true); // first copy: builds the lights
|
||||
const lights = [...copy.cascadedLights];
|
||||
const targets = lights.map((light) => light.target);
|
||||
source.cascadedLights[1].intensity = 5;
|
||||
source.cascadedLights[1].color.set(0xff0000);
|
||||
source.cascadedLights[2].position.set(9, 9, 9);
|
||||
source.cascadedLights[2].target.position.set(7, 0, 0);
|
||||
source.cascadedLights[2].updateMatrixWorld(true);
|
||||
source.cascadedLights[2].target.updateMatrixWorld(true);
|
||||
source.cascadedLights[0].shadow.camera.left = -77;
|
||||
copy.copy(source, true);
|
||||
assert.deepEqual(copy.cascadedLights, lights, 'the same light objects');
|
||||
assert.deepEqual(
|
||||
copy.cascadedLights.map((light) => light.target),
|
||||
targets,
|
||||
);
|
||||
assert.equal(copy.cascadedLights[1].intensity, 5);
|
||||
assert.equal(copy.cascadedLights[1].color.getHex(), 0xff0000);
|
||||
assert.deepEqual(copy.cascadedLights[2].position.toArray(), [9, 9, 9]);
|
||||
assert.equal(copy.cascadedLights[2].target.matrixWorld.elements[12], 7);
|
||||
assert.equal(copy.cascadedLights[0].shadow.camera.left, -77);
|
||||
assert.equal(copy.cascadedLights[2].shadow.bias, -0.003);
|
||||
// A different cascade count rebuilds, as before.
|
||||
source.setCount(4);
|
||||
copy.copy(source, true);
|
||||
assert.equal(copy.cascadedLights.length, 4);
|
||||
assert.equal(
|
||||
patchCascadedLightCopy(CascadedDirectionalLights.prototype),
|
||||
restore,
|
||||
'idempotent',
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
assert.equal(CascadedDirectionalLights.prototype.copy.gevInPlace, undefined);
|
||||
});
|
||||
@@ -6,7 +6,11 @@ import ThreeView from '@navaramap/three';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { DefaultPlugin } from '@navaramap/three-default-plugin';
|
||||
import { createNavaraEngine } from './createNavaraEngine.js';
|
||||
import { forEachLuminanceUniform, navaraHooks } from './internals.js';
|
||||
import {
|
||||
forEachLuminanceUniform,
|
||||
keepCascadedLightIds,
|
||||
navaraHooks,
|
||||
} from './internals.js';
|
||||
import { buildNavaraLook } from './look.js';
|
||||
import { createNavaraSharpen } from './sharpen.js';
|
||||
import { createNavaraMovers } from './movers/index.js';
|
||||
@@ -112,6 +116,8 @@ export async function createNavaraGlobe({
|
||||
() => engine.clock.frameTime(),
|
||||
{ forEachLuminanceUniform },
|
||||
);
|
||||
// The look's shadowed sun leaked a light per frame (internals.js).
|
||||
keepCascadedLightIds(view);
|
||||
engine = createNavaraEngine(view, {
|
||||
hooks: navaraHooks(view),
|
||||
container,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// found it and the public API that would replace it upstream.
|
||||
|
||||
import { ModelMesh } from '@navaramap/three';
|
||||
import { patchCascadedLightCopy } from './cascadedLightCopy.js';
|
||||
|
||||
export const NAVARA_INTERNALS_VERSION = '0.2.1';
|
||||
|
||||
@@ -130,6 +131,23 @@ export function forEachLuminanceUniform(handles, visit) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3 review (leak): the sun's cascaded shadow lights are re-cloned by
|
||||
* the transparent pass every frame (cascadedLightCopy.js). Finds the
|
||||
* CascadedDirectionalLights class among the view's scene lights (private:
|
||||
* view._scenes.light) and patches its copy to update in place. Returns the
|
||||
* restore function, or null when the look has no cascaded light (the
|
||||
* cinematic sun casts no shadow). Upstream ask: copy cascaded lights in
|
||||
* place.
|
||||
*/
|
||||
export function keepCascadedLightIds(view) {
|
||||
const lights = view._scenes?.light?.children ?? [];
|
||||
const cascaded = lights.find((light) => Array.isArray(light?.cascadedLights));
|
||||
return cascaded
|
||||
? patchCascadedLightCopy(Object.getPrototypeOf(cascaded))
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The hooks createNavaraEngine takes. */
|
||||
export const navaraHooks = (view) => ({
|
||||
retainTileGeometry,
|
||||
|
||||
Reference in new issue
Block a user