145 lines
4.3 KiB
TypeScript
145 lines
4.3 KiB
TypeScript
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 type {
|
|
GpuRenderTargetAllocation,
|
|
GpuRenderTargetReporter,
|
|
SceneResourceBag,
|
|
} from "@/scene/resources";
|
|
|
|
export interface PostProcessor extends GpuRenderTargetReporter {
|
|
resize(width: number, height: number, pixelRatio: number): void;
|
|
render(scene: unknown, camera: unknown): void;
|
|
dispose(): void;
|
|
}
|
|
|
|
interface RenderTargetLike {
|
|
width: number;
|
|
height: number;
|
|
samples?: number;
|
|
depthBuffer?: boolean;
|
|
}
|
|
|
|
interface ComposerLike {
|
|
renderTarget1?: RenderTargetLike;
|
|
renderTarget2?: RenderTargetLike;
|
|
addPass(pass: unknown): void;
|
|
setPixelRatio(pixelRatio: number): void;
|
|
setSize(width: number, height: number): void;
|
|
render(): void;
|
|
dispose(): void;
|
|
}
|
|
|
|
interface RenderPassLike {
|
|
scene: unknown;
|
|
camera: unknown;
|
|
dispose(): void;
|
|
}
|
|
|
|
interface BloomPassLike {
|
|
renderTargetBright?: RenderTargetLike;
|
|
renderTargetsHorizontal?: readonly RenderTargetLike[];
|
|
renderTargetsVertical?: readonly RenderTargetLike[];
|
|
dispose(): void;
|
|
}
|
|
|
|
export interface PostProcessingFactories {
|
|
createComposer(renderer: unknown): ComposerLike;
|
|
createRenderPass(scene: unknown, camera: unknown): RenderPassLike;
|
|
createBloomPass(): BloomPassLike;
|
|
}
|
|
|
|
const productionFactories: PostProcessingFactories = {
|
|
createComposer: (renderer) => new EffectComposer(renderer),
|
|
createRenderPass: (scene, camera) => new RenderPass(scene, camera),
|
|
createBloomPass: () =>
|
|
new UnrealBloomPass(new THREE.Vector2(1, 1), 0.46, 0.32, 0.84),
|
|
};
|
|
|
|
export function createPostProcessor(
|
|
resources: SceneResourceBag,
|
|
renderer: unknown,
|
|
factories: PostProcessingFactories = productionFactories,
|
|
): PostProcessor {
|
|
const composer = factories.createComposer(renderer);
|
|
const renderPass = factories.createRenderPass(
|
|
new THREE.Scene(),
|
|
new THREE.Camera(),
|
|
);
|
|
const bloomPass = factories.createBloomPass();
|
|
composer.addPass(renderPass);
|
|
composer.addPass(bloomPass);
|
|
let disposed = false;
|
|
|
|
const processor: PostProcessor = {
|
|
gpuRenderTargets() {
|
|
return collectRenderTargetAllocations(composer, bloomPass);
|
|
},
|
|
resize(width, height, pixelRatio) {
|
|
if (disposed) return;
|
|
composer.setPixelRatio(clamp(pixelRatio, 0.5, 2));
|
|
composer.setSize(
|
|
Math.max(1, Math.floor(finiteOr(width, 1))),
|
|
Math.max(1, Math.floor(finiteOr(height, 1))),
|
|
);
|
|
},
|
|
render(scene, camera) {
|
|
if (disposed) return;
|
|
renderPass.scene = scene;
|
|
renderPass.camera = camera;
|
|
composer.render();
|
|
},
|
|
dispose() {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
renderPass.dispose();
|
|
bloomPass.dispose();
|
|
composer.dispose();
|
|
},
|
|
};
|
|
return resources.own("pass", processor);
|
|
}
|
|
|
|
function collectRenderTargetAllocations(
|
|
composer: ComposerLike,
|
|
bloomPass: BloomPassLike,
|
|
): readonly GpuRenderTargetAllocation[] {
|
|
const candidates: ReadonlyArray<
|
|
readonly [string, RenderTargetLike | undefined]
|
|
> = [
|
|
["composer-target-1", composer.renderTarget1],
|
|
["composer-target-2", composer.renderTarget2],
|
|
["bloom-bright", bloomPass.renderTargetBright],
|
|
...(bloomPass.renderTargetsHorizontal ?? []).map(
|
|
(target, index) => [`bloom-horizontal-${index}`, target] as const,
|
|
),
|
|
...(bloomPass.renderTargetsVertical ?? []).map(
|
|
(target, index) => [`bloom-vertical-${index}`, target] as const,
|
|
),
|
|
];
|
|
const seen = new Set<RenderTargetLike>();
|
|
return candidates.flatMap(([id, target]) => {
|
|
if (target === undefined || seen.has(target)) return [];
|
|
seen.add(target);
|
|
return [
|
|
{
|
|
id,
|
|
width: Math.max(1, Math.floor(finiteOr(target.width, 1))),
|
|
height: Math.max(1, Math.floor(finiteOr(target.height, 1))),
|
|
bytesPerPixel: 4,
|
|
attachments: target.depthBuffer === false ? 1 : 2,
|
|
samples: Math.max(1, Math.floor(finiteOr(target.samples ?? 1, 1))),
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
function finiteOr(value: number, fallback: number): number {
|
|
return Number.isFinite(value) ? value : fallback;
|
|
}
|
|
|
|
function clamp(value: number, minimum: number, maximum: number): number {
|
|
return THREE.MathUtils.clamp(finiteOr(value, minimum), minimum, maximum);
|
|
}
|