mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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/navara/lines/lineMesh.test.mjs",
|
||||||
"src/globe/pickDelivery.test.mjs",
|
"src/globe/pickDelivery.test.mjs",
|
||||||
"src/globe/navara/picking.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 * as geo from '../../geo/index.js';
|
||||||
import { DefaultPlugin } from '@navaramap/three-default-plugin';
|
import { DefaultPlugin } from '@navaramap/three-default-plugin';
|
||||||
import { createNavaraEngine } from './createNavaraEngine.js';
|
import { createNavaraEngine } from './createNavaraEngine.js';
|
||||||
import { forEachLuminanceUniform, navaraHooks } from './internals.js';
|
import {
|
||||||
|
forEachLuminanceUniform,
|
||||||
|
keepCascadedLightIds,
|
||||||
|
navaraHooks,
|
||||||
|
} from './internals.js';
|
||||||
import { buildNavaraLook } from './look.js';
|
import { buildNavaraLook } from './look.js';
|
||||||
import { createNavaraSharpen } from './sharpen.js';
|
import { createNavaraSharpen } from './sharpen.js';
|
||||||
import { createNavaraMovers } from './movers/index.js';
|
import { createNavaraMovers } from './movers/index.js';
|
||||||
@@ -112,6 +116,8 @@ export async function createNavaraGlobe({
|
|||||||
() => engine.clock.frameTime(),
|
() => engine.clock.frameTime(),
|
||||||
{ forEachLuminanceUniform },
|
{ forEachLuminanceUniform },
|
||||||
);
|
);
|
||||||
|
// The look's shadowed sun leaked a light per frame (internals.js).
|
||||||
|
keepCascadedLightIds(view);
|
||||||
engine = createNavaraEngine(view, {
|
engine = createNavaraEngine(view, {
|
||||||
hooks: navaraHooks(view),
|
hooks: navaraHooks(view),
|
||||||
container,
|
container,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// found it and the public API that would replace it upstream.
|
// found it and the public API that would replace it upstream.
|
||||||
|
|
||||||
import { ModelMesh } from '@navaramap/three';
|
import { ModelMesh } from '@navaramap/three';
|
||||||
|
import { patchCascadedLightCopy } from './cascadedLightCopy.js';
|
||||||
|
|
||||||
export const NAVARA_INTERNALS_VERSION = '0.2.1';
|
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. */
|
/** The hooks createNavaraEngine takes. */
|
||||||
export const navaraHooks = (view) => ({
|
export const navaraHooks = (view) => ({
|
||||||
retainTileGeometry,
|
retainTileGeometry,
|
||||||
|
|||||||
Reference in new issue
Block a user