mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
perf(navara): resolve MSAA at geometry stages
Keep four-sample geometry and depth while using single-sample postprocessing buffers. Preserve forward and overlay coverage, skip empty transfers, and add matched pixel and regression checks.
This commit is contained in:
1 parent
8279af2e6d
commit
c76872dfaa
6 files changed
+306
-1
No files matched your search
@@ -1270,5 +1270,7 @@
|
|||||||
"src/layers/recentImagery/render/tiles.test.mjs",
|
"src/layers/recentImagery/render/tiles.test.mjs",
|
||||||
"src/layers/recentImagery/render/navara.test.mjs",
|
"src/layers/recentImagery/render/navara.test.mjs",
|
||||||
"src/ui/imageryBoxNavara.test.mjs",
|
"src/ui/imageryBoxNavara.test.mjs",
|
||||||
"scripts/qa-w2-imagery.mjs"
|
"scripts/qa-w2-imagery.mjs",
|
||||||
|
"src/globe/navara/postSamplingPass.test.mjs",
|
||||||
|
"scripts/qa-post-sampling.mjs"
|
||||||
]
|
]
|
||||||
@@ -2669,6 +2669,8 @@
|
|||||||
"src/globe/navara/movers/pack.js",
|
"src/globe/navara/movers/pack.js",
|
||||||
"src/globe/navara/movers/frameCamera.js",
|
"src/globe/navara/movers/frameCamera.js",
|
||||||
"src/globe/navara/overlayPass.js",
|
"src/globe/navara/overlayPass.js",
|
||||||
|
"src/globe/navara/postSampling.js",
|
||||||
|
"src/globe/navara/postSamplingPass.js",
|
||||||
"src/globe/navara/movers/modelMover.js",
|
"src/globe/navara/movers/modelMover.js",
|
||||||
"src/globe/navara/movers/modelTint.js",
|
"src/globe/navara/movers/modelTint.js",
|
||||||
"src/globe/navara/movers/index.js",
|
"src/globe/navara/movers/index.js",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { createNavaraSharpen } from './sharpen.js';
|
|||||||
import { createNavaraMovers } from './movers/index.js';
|
import { createNavaraMovers } from './movers/index.js';
|
||||||
import { createNavaraLines } from './lines/index.js';
|
import { createNavaraLines } from './lines/index.js';
|
||||||
import { createNavaraOverlay } from './overlay.js';
|
import { createNavaraOverlay } from './overlay.js';
|
||||||
|
import { createPostSampling } from './postSampling.js';
|
||||||
import {
|
import {
|
||||||
NAVARA_CONTEXT_ATTRIBUTES,
|
NAVARA_CONTEXT_ATTRIBUTES,
|
||||||
ACES_FILMIC,
|
ACES_FILMIC,
|
||||||
@@ -108,6 +109,7 @@ export async function createNavaraGlobe({
|
|||||||
let engine;
|
let engine;
|
||||||
let overlay = null;
|
let overlay = null;
|
||||||
let removeLook = null;
|
let removeLook = null;
|
||||||
|
let removePostSampling = null;
|
||||||
try {
|
try {
|
||||||
view = new ThreeView(navaraViewOptions({ container, canvas, pixelRatio }));
|
view = new ThreeView(navaraViewOptions({ container, canvas, pixelRatio }));
|
||||||
const plugin = new DefaultPlugin();
|
const plugin = new DefaultPlugin();
|
||||||
@@ -127,6 +129,7 @@ export async function createNavaraGlobe({
|
|||||||
);
|
);
|
||||||
// The look's shadowed sun leaked a light per frame (internals.js).
|
// The look's shadowed sun leaked a light per frame (internals.js).
|
||||||
keepCascadedLightIds(view);
|
keepCascadedLightIds(view);
|
||||||
|
removePostSampling = createPostSampling(view);
|
||||||
// Screen overlays and model display colour after tone mapping (overlay.js).
|
// Screen overlays and model display colour after tone mapping (overlay.js).
|
||||||
overlay = createNavaraOverlay(view);
|
overlay = createNavaraOverlay(view);
|
||||||
engine = createNavaraEngine(view, {
|
engine = createNavaraEngine(view, {
|
||||||
@@ -182,6 +185,7 @@ export async function createNavaraGlobe({
|
|||||||
removeLook = () => removers.forEach((remove) => remove());
|
removeLook = () => removers.forEach((remove) => remove());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
removeLook?.();
|
removeLook?.();
|
||||||
|
removePostSampling?.();
|
||||||
engine?.destroy();
|
engine?.destroy();
|
||||||
overlay?.destroy();
|
overlay?.destroy();
|
||||||
view?.dispose();
|
view?.dispose();
|
||||||
@@ -201,6 +205,7 @@ export async function createNavaraGlobe({
|
|||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
removeLook();
|
removeLook();
|
||||||
|
removePostSampling();
|
||||||
engine.destroy();
|
engine.destroy();
|
||||||
overlay.destroy();
|
overlay.destroy();
|
||||||
view.dispose();
|
view.dispose();
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { EffectDesc, Pass as NavaraPass } from '@navaramap/three';
|
||||||
|
import { GeometrySamplingPass } from './postSamplingPass.js';
|
||||||
|
|
||||||
|
const KEY = 'gevPostSampling';
|
||||||
|
class PostSamplingDesc extends EffectDesc {
|
||||||
|
static key = KEY;
|
||||||
|
static insertBefore = ['mrt'];
|
||||||
|
|
||||||
|
createPass() {
|
||||||
|
return new NavaraPass(new GeometrySamplingPass(this.ctx), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPostSampling(view) {
|
||||||
|
view.registerEffect(KEY, PostSamplingDesc);
|
||||||
|
const handle = view.addEffect({ [KEY]: {}, visible: true });
|
||||||
|
return () => handle.delete();
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { Pass } from 'postprocessing';
|
||||||
|
import { AlwaysDepth, Mesh, PlaneGeometry, ShaderMaterial } from 'three';
|
||||||
|
|
||||||
|
function hasGeometry(scene) {
|
||||||
|
let found = false;
|
||||||
|
scene.traverseVisible((object) => {
|
||||||
|
if (object.isMesh || object.isLine || object.isPoints || object.isSprite)
|
||||||
|
found = true;
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navara 0.2.1 multisamples every composer target, including full-screen
|
||||||
|
// effects. Resolve geometry once; post effects need one sample per pixel.
|
||||||
|
// Keep forward meshes and the final overlay multisampled too, not just MRT.
|
||||||
|
export class GeometrySamplingPass extends Pass {
|
||||||
|
constructor(ctx) {
|
||||||
|
super('GevGeometrySampling');
|
||||||
|
this.needsSwap = false;
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.samples = ctx.getInputBuffer().samples;
|
||||||
|
this.buffers = new Map();
|
||||||
|
this.wrapped = new Map();
|
||||||
|
this.target = null;
|
||||||
|
this.copy = new ShaderMaterial({
|
||||||
|
vertexShader:
|
||||||
|
'varying vec2 vUv; void main(){vUv=uv;gl_Position=vec4(position.xy,0.,1.);}',
|
||||||
|
fragmentShader:
|
||||||
|
'uniform sampler2D image; uniform sampler2D depth; varying vec2 vUv; void main(){gl_FragColor=texture2D(image,vUv);gl_FragDepth=texture2D(depth,vUv).r;}',
|
||||||
|
uniforms: { image: { value: null }, depth: { value: null } },
|
||||||
|
depthTest: true,
|
||||||
|
depthFunc: AlwaysDepth,
|
||||||
|
depthWrite: true,
|
||||||
|
toneMapped: false,
|
||||||
|
});
|
||||||
|
this.background = new Mesh(new PlaneGeometry(2, 2), this.copy);
|
||||||
|
this.background.frustumCulled = false;
|
||||||
|
this.background.renderOrder = -Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(renderer, input, output) {
|
||||||
|
if (!this.samples) return;
|
||||||
|
for (const buffer of [input, output]) {
|
||||||
|
if (!this.buffers.has(buffer)) this.buffers.set(buffer, buffer.samples);
|
||||||
|
this.setSamples(buffer, 0);
|
||||||
|
}
|
||||||
|
// G-buffer attachments can be replaced when an effect is added/removed.
|
||||||
|
this.setSamples(this.ctx.getRenderTarget(), this.samples);
|
||||||
|
for (const key of ['mrt', 'transparent', 'gevOverlay']) {
|
||||||
|
const pass = this.ctx.getPass(key);
|
||||||
|
if (!pass || this.wrapped.has(pass)) continue;
|
||||||
|
const original = pass.render;
|
||||||
|
const owner = this;
|
||||||
|
pass.render = function (renderer, input, output, ...rest) {
|
||||||
|
if (key === 'gevOverlay' && !this.overlay.hasDrawables()) return;
|
||||||
|
if (key === 'transparent' && !hasGeometry(owner.ctx.scenes.transparent))
|
||||||
|
return;
|
||||||
|
if (key === 'mrt' && !hasGeometry(owner.ctx.scenes.opaque))
|
||||||
|
return original.call(this, renderer, input, output, ...rest);
|
||||||
|
const target = (owner.target ??= input.clone());
|
||||||
|
owner.setSamples(target, owner.samples);
|
||||||
|
target.setSize(input.width, input.height);
|
||||||
|
const autoClear = renderer.autoClear;
|
||||||
|
renderer.autoClear = false;
|
||||||
|
try {
|
||||||
|
if (key !== 'mrt') {
|
||||||
|
owner.copy.uniforms.image.value = input.texture;
|
||||||
|
owner.copy.uniforms.depth.value = input.depthTexture;
|
||||||
|
// Copy and geometry share one render() and therefore one MSAA
|
||||||
|
// resolve. The opaque background sorts before transparent glyphs.
|
||||||
|
this.scene.add(owner.background);
|
||||||
|
}
|
||||||
|
original.call(this, renderer, target, output, ...rest);
|
||||||
|
renderer.initRenderTarget(input);
|
||||||
|
renderer.copyTextureToTexture(target.texture, input.texture);
|
||||||
|
if (key === 'mrt')
|
||||||
|
renderer.copyTextureToTexture(
|
||||||
|
target.depthTexture,
|
||||||
|
input.depthTexture,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
owner.background.removeFromParent();
|
||||||
|
renderer.setRenderTarget(input);
|
||||||
|
renderer.autoClear = autoClear;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.wrapped.set(pass, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSamples(target, samples) {
|
||||||
|
if (target.samples === samples) return;
|
||||||
|
target.dispose();
|
||||||
|
target.samples = samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
for (const [pass, render] of this.wrapped) pass.render = render;
|
||||||
|
for (const [buffer, samples] of this.buffers)
|
||||||
|
this.setSamples(buffer, samples);
|
||||||
|
this.wrapped.clear();
|
||||||
|
this.buffers.clear();
|
||||||
|
this.background.geometry.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { DepthTexture, Group, Mesh, Scene, WebGLRenderTarget } from 'three';
|
||||||
|
import { GeometrySamplingPass } from './postSamplingPass.js';
|
||||||
|
|
||||||
|
function fixture() {
|
||||||
|
const target = () => {
|
||||||
|
const buffer = new WebGLRenderTarget(32, 24, { samples: 4 });
|
||||||
|
buffer.depthTexture = new DepthTexture(32, 24);
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
const input = target(),
|
||||||
|
output = target();
|
||||||
|
let mrt = target();
|
||||||
|
const calls = [],
|
||||||
|
copies = [],
|
||||||
|
draws = [];
|
||||||
|
const scenes = { opaque: new Scene(), transparent: new Scene() };
|
||||||
|
const passes = Object.fromEntries(
|
||||||
|
['mrt', 'transparent', 'gevOverlay'].map((key) => [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
scene: new Scene(),
|
||||||
|
overlay: { hasDrawables: () => true },
|
||||||
|
render(renderer, input, output, ...rest) {
|
||||||
|
calls.push({
|
||||||
|
key,
|
||||||
|
receiver: this,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
rest,
|
||||||
|
background: this.scene.children[0],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const originals = Object.fromEntries(
|
||||||
|
Object.entries(passes).map(([key, pass]) => [key, pass.render]),
|
||||||
|
);
|
||||||
|
const renderer = {
|
||||||
|
autoClear: true,
|
||||||
|
setRenderTarget(target) {
|
||||||
|
this.target = target;
|
||||||
|
},
|
||||||
|
render(scene) {
|
||||||
|
draws.push(scene);
|
||||||
|
},
|
||||||
|
initRenderTarget() {},
|
||||||
|
copyTextureToTexture(from, to) {
|
||||||
|
copies.push([from, to]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const ctx = {
|
||||||
|
scenes,
|
||||||
|
getInputBuffer: () => input,
|
||||||
|
getRenderTarget: () => mrt,
|
||||||
|
getPass: (key) => passes[key],
|
||||||
|
};
|
||||||
|
const policy = new GeometrySamplingPass(ctx);
|
||||||
|
return {
|
||||||
|
policy,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
scenes,
|
||||||
|
passes,
|
||||||
|
originals,
|
||||||
|
renderer,
|
||||||
|
calls,
|
||||||
|
copies,
|
||||||
|
draws,
|
||||||
|
replaceMrt() {
|
||||||
|
mrt = target();
|
||||||
|
mrt.samples = 0;
|
||||||
|
return mrt;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('post targets use one sample; replacement MRT and actual forward geometry retain four', () => {
|
||||||
|
const f = fixture();
|
||||||
|
try {
|
||||||
|
f.scenes.opaque.add(new Mesh());
|
||||||
|
f.policy.render(f.renderer, f.input, f.output);
|
||||||
|
assert.equal(f.input.samples, 0);
|
||||||
|
assert.equal(f.output.samples, 0);
|
||||||
|
const mrt = f.replaceMrt();
|
||||||
|
f.policy.render(f.renderer, f.input, f.output);
|
||||||
|
assert.equal(mrt.samples, 4);
|
||||||
|
f.passes.mrt.render(f.renderer, f.input, f.output, 0.5, false);
|
||||||
|
const call = f.calls[0];
|
||||||
|
assert.equal(call.receiver, f.passes.mrt);
|
||||||
|
assert.equal(call.input.samples, 4);
|
||||||
|
assert.notEqual(call.input, f.input);
|
||||||
|
assert.deepEqual(call.rest, [0.5, false]);
|
||||||
|
assert.deepEqual(f.copies, [
|
||||||
|
[call.input.texture, f.input.texture],
|
||||||
|
[call.input.depthTexture, f.input.depthTexture],
|
||||||
|
]);
|
||||||
|
assert.equal(f.renderer.autoClear, true);
|
||||||
|
} finally {
|
||||||
|
f.policy.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty forward scenes avoid copies, including hidden geometry', () => {
|
||||||
|
const f = fixture();
|
||||||
|
try {
|
||||||
|
const hidden = new Group();
|
||||||
|
hidden.visible = false;
|
||||||
|
hidden.add(new Mesh());
|
||||||
|
f.scenes.opaque.add(hidden);
|
||||||
|
f.passes.gevOverlay.overlay.hasDrawables = () => false;
|
||||||
|
f.policy.render(f.renderer, f.input, f.output);
|
||||||
|
for (const pass of Object.values(f.passes))
|
||||||
|
pass.render(f.renderer, f.input, f.output);
|
||||||
|
assert.deepEqual(
|
||||||
|
f.calls.map((call) => call.key),
|
||||||
|
['mrt'],
|
||||||
|
);
|
||||||
|
assert.equal(f.calls[0].input, f.input);
|
||||||
|
assert.equal(f.copies.length, 0);
|
||||||
|
assert.equal(f.policy.target, null);
|
||||||
|
hidden.visible = true;
|
||||||
|
f.passes.mrt.render(f.renderer, f.input, f.output);
|
||||||
|
assert.equal(f.calls[1].input.samples, 4);
|
||||||
|
} finally {
|
||||||
|
f.policy.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transparent and overlay draws copy the current ping-pong input and keep MSAA after resize', () => {
|
||||||
|
const f = fixture();
|
||||||
|
try {
|
||||||
|
f.scenes.transparent.add(new Mesh());
|
||||||
|
f.policy.render(f.renderer, f.input, f.output);
|
||||||
|
f.output.setSize(64, 48);
|
||||||
|
for (const key of ['transparent', 'gevOverlay']) {
|
||||||
|
f.passes[key].render(f.renderer, f.output, f.input);
|
||||||
|
assert.equal(f.policy.copy.uniforms.image.value, f.output.texture);
|
||||||
|
assert.equal(f.policy.copy.uniforms.depth.value, f.output.depthTexture);
|
||||||
|
assert.equal(f.calls.at(-1).input.samples, 4);
|
||||||
|
assert.equal(f.calls.at(-1).input.width, 64);
|
||||||
|
assert.equal(f.calls.at(-1).input.height, 48);
|
||||||
|
assert.deepEqual(f.copies.at(-1), [
|
||||||
|
f.policy.target.texture,
|
||||||
|
f.output.texture,
|
||||||
|
]);
|
||||||
|
assert.equal(f.calls.at(-1).background, f.policy.background);
|
||||||
|
assert.equal(f.passes[key].scene.children.length, 0);
|
||||||
|
}
|
||||||
|
assert.equal(f.draws.length, 0);
|
||||||
|
assert.equal(f.renderer.autoClear, true);
|
||||||
|
} finally {
|
||||||
|
f.policy.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disposing restores public passes and buffer sampling without per-frame reallocations', () => {
|
||||||
|
const f = fixture();
|
||||||
|
let disposals = 0;
|
||||||
|
f.input.addEventListener('dispose', () => disposals++);
|
||||||
|
f.policy.render(f.renderer, f.input, f.output);
|
||||||
|
f.policy.render(f.renderer, f.input, f.output);
|
||||||
|
assert.equal(disposals, 1);
|
||||||
|
f.policy.dispose();
|
||||||
|
assert.equal(disposals, 2);
|
||||||
|
assert.equal(f.input.samples, 4);
|
||||||
|
assert.equal(f.output.samples, 4);
|
||||||
|
for (const key of Object.keys(f.passes))
|
||||||
|
assert.equal(f.passes[key].render, f.originals[key]);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user