feat(scene): add film grade and lens finish pass
This commit is contained in:
@@ -2,7 +2,7 @@ import * as THREE from "three";
|
||||
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
||||
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
||||
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
|
||||
import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass.js";
|
||||
import { LensFinishPass, type LensState } from "@/scene/cinematic-grade";
|
||||
import type {
|
||||
GpuRenderTargetAllocation,
|
||||
GpuRenderTargetReporter,
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
export interface PostProcessor extends GpuRenderTargetReporter {
|
||||
resize(width: number, height: number, pixelRatio: number): void;
|
||||
setDynamics(acceleration: number): void;
|
||||
setLens(state: LensState): void;
|
||||
render(scene: unknown, camera: unknown): void;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -46,11 +47,13 @@ interface BloomPassLike {
|
||||
materialHighPassFilter?: { dispose(): void };
|
||||
renderTargetBright?: RenderTargetLike;
|
||||
renderTargetsHorizontal?: readonly RenderTargetLike[];
|
||||
renderTargetsVertical?: readonly RenderTargetLike[];
|
||||
renderTargetsVertical?: readonly (RenderTargetLike & { texture?: unknown })[];
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
interface OutputPassLike {
|
||||
setStreakSource?(texture: unknown, strength?: number): void;
|
||||
setLens?(state: LensState): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
@@ -61,11 +64,20 @@ export interface PostProcessingFactories {
|
||||
createOutputPass(): OutputPassLike;
|
||||
}
|
||||
|
||||
// Lights read as light: a lower threshold lets lamps, trails and the sun's limb
|
||||
// bloom, while the asphalt and the panels' surroundings stay below it.
|
||||
const BLOOM_REST = { strength: 0.58, radius: 0.58, threshold: 0.85 } as const;
|
||||
|
||||
const productionFactories: PostProcessingFactories = {
|
||||
createComposer: (renderer) => new EffectComposer(renderer),
|
||||
createRenderPass: (scene, camera) => new RenderPass(scene, camera),
|
||||
createBloomPass: () => {
|
||||
const bloom = new UnrealBloomPass(new THREE.Vector2(1, 1), 0.3, 0.38, 1.05);
|
||||
const bloom = new UnrealBloomPass(
|
||||
new THREE.Vector2(1, 1),
|
||||
BLOOM_REST.strength,
|
||||
BLOOM_REST.radius,
|
||||
BLOOM_REST.threshold,
|
||||
);
|
||||
// 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 [
|
||||
@@ -77,14 +89,10 @@ const productionFactories: PostProcessingFactories = {
|
||||
}
|
||||
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;
|
||||
},
|
||||
// The lens pass replaces OutputPass one for one: last pass, straight to the
|
||||
// canvas, no swap. Neither RenderPass nor bloom swaps buffers, so swapping
|
||||
// here would only alternate two identical full-resolution GPU targets.
|
||||
createOutputPass: () => new LensFinishPass(),
|
||||
};
|
||||
|
||||
export function createPostProcessor(
|
||||
@@ -102,6 +110,9 @@ export function createPostProcessor(
|
||||
composer.addPass(renderPass);
|
||||
composer.addPass(bloomPass);
|
||||
composer.addPass(outputPass);
|
||||
// A quarter-resolution bloom mip is already blurred: sampling it sideways
|
||||
// gives anamorphic streaks without a target of their own.
|
||||
outputPass.setStreakSource?.(bloomPass.renderTargetsVertical?.[1]?.texture ?? null, 0.22);
|
||||
let disposed = false;
|
||||
|
||||
const processor: PostProcessor = {
|
||||
@@ -119,9 +130,13 @@ export function createPostProcessor(
|
||||
setDynamics(acceleration) {
|
||||
if (disposed) return;
|
||||
const energy = clamp(acceleration, 0, 1);
|
||||
bloomPass.strength = 0.3 + energy * 0.12;
|
||||
bloomPass.radius = 0.38 + energy * 0.06;
|
||||
bloomPass.threshold = 1.05 - energy * 0.03;
|
||||
bloomPass.strength = BLOOM_REST.strength + energy * 0.16;
|
||||
bloomPass.radius = BLOOM_REST.radius + energy * 0.06;
|
||||
bloomPass.threshold = BLOOM_REST.threshold - energy * 0.05;
|
||||
},
|
||||
setLens(state) {
|
||||
if (disposed) return;
|
||||
outputPass.setLens?.(state);
|
||||
},
|
||||
render(scene, camera) {
|
||||
if (disposed) return;
|
||||
|
||||
Reference in New Issue
Block a user