diff --git a/src/scene/post-processing.ts b/src/scene/post-processing.ts index 403a836..9f5d740 100644 --- a/src/scene/post-processing.ts +++ b/src/scene/post-processing.ts @@ -43,6 +43,7 @@ interface BloomPassLike { strength: number; radius: number; threshold: number; + materialHighPassFilter?: { dispose(): void }; renderTargetBright?: RenderTargetLike; renderTargetsHorizontal?: readonly RenderTargetLike[]; renderTargetsVertical?: readonly RenderTargetLike[]; @@ -63,9 +64,27 @@ export interface PostProcessingFactories { const productionFactories: PostProcessingFactories = { createComposer: (renderer) => new EffectComposer(renderer), createRenderPass: (scene, camera) => new RenderPass(scene, camera), - createBloomPass: () => - new UnrealBloomPass(new THREE.Vector2(1, 1), 0.3, 0.38, 1.05), - createOutputPass: () => new OutputPass(), + createBloomPass: () => { + const bloom = new UnrealBloomPass(new THREE.Vector2(1, 1), 0.3, 0.38, 1.05); + // These fullscreen passes overwrite every color pixel. No pass samples + // their depth, and scene depth lives in the composer's separate targets. + for (const target of [ + bloom.renderTargetBright, + ...bloom.renderTargetsHorizontal, + ...bloom.renderTargetsVertical, + ]) { + target.depthBuffer = false; + } + return bloom; + }, + createOutputPass: () => { + const output = new OutputPass(); + // Output is always the last pass and renders directly to the canvas. + // Neither RenderPass nor bloom swaps buffers, so swapping here would only + // alternate the scene between two identical full-resolution GPU targets. + output.needsSwap = false; + return output; + }, }; export function createPostProcessor( @@ -114,6 +133,8 @@ export function createPostProcessor( if (disposed) return; disposed = true; renderPass.dispose(); + // Three r185 omits this material from UnrealBloomPass.dispose(). + bloomPass.materialHighPassFilter?.dispose(); bloomPass.dispose(); outputPass.dispose(); composer.dispose(); diff --git a/src/scene/resources.ts b/src/scene/resources.ts index 1f1ff5c..f00321e 100644 --- a/src/scene/resources.ts +++ b/src/scene/resources.ts @@ -146,7 +146,15 @@ export class SceneResourceBag { this.listeners.clear(); for (const observer of this.observers) observer.disconnect(); this.observers.clear(); - for (const resource of this.resources.keys()) resource.dispose(); + // Texture, target and material disposal needs the renderer's allocation + // records. Disposing the renderer first clears those records before the + // resources can release their WebGL handles during a quality change. + for (const [resource, kind] of this.resources) { + if (kind !== "renderer") resource.dispose(); + } + for (const [resource, kind] of this.resources) { + if (kind === "renderer") resource.dispose(); + } this.resources.clear(); } diff --git a/src/scene/world/sign-stage.ts b/src/scene/world/sign-stage.ts index 684ca28..061862d 100644 --- a/src/scene/world/sign-stage.ts +++ b/src/scene/world/sign-stage.ts @@ -14,6 +14,14 @@ export function createSignStage(resources: SceneResourceBag) { (element): element is HTMLElement => element instanceof HTMLElement, ) : []; + // Read the complete stage before writing masks. Interleaving each panel's + // reads and writes forces style recalculation again for the next panel. + const measurements = panels.map((panel) => ({ + panel, + heading: panel.querySelector(':scope > h2'), + bounds: null as DOMRect | null, + headingOffset: '', + })); let dirty = true; let cutoff = 0; let stageBottom = 176; @@ -91,14 +99,16 @@ export function createSignStage(resources: SceneResourceBag) { readingTop = topEdge + feather; // Only the panel crossing the edge needs a composited mask. Masking // the complete document would allocate a very tall intermediate layer. - for (const panel of panels) { - const bounds = panel.getBoundingClientRect(); - const heading = panel.querySelector(':scope > h2'); + for (const measurement of measurements) { + const bounds = measurement.panel.getBoundingClientRect(); + measurement.bounds = bounds; + if (measurement.heading) + measurement.headingOffset = `${(measurement.heading.getBoundingClientRect().top - bounds.top).toFixed(2)}px`; + } + for (const { panel, bounds, heading, headingOffset } of measurements) { + if (!bounds) continue; if (heading) - panel.style.setProperty( - '--chapter-heading-offset', - `${(heading.getBoundingClientRect().top - bounds.top).toFixed(2)}px`, - ); + panel.style.setProperty('--chapter-heading-offset', headingOffset); const top = topEdge * reveal; const bottom = short && width < 768 ? height - 164 : Infinity; if (