fix: close final portfolio quality gaps
This commit is contained in:
@@ -1,16 +1,29 @@
|
||||
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 { SceneResourceBag } from '@/scene/resources';
|
||||
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 {
|
||||
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;
|
||||
@@ -25,6 +38,9 @@ interface RenderPassLike {
|
||||
}
|
||||
|
||||
interface BloomPassLike {
|
||||
renderTargetBright?: RenderTargetLike;
|
||||
renderTargetsHorizontal?: readonly RenderTargetLike[];
|
||||
renderTargetsVertical?: readonly RenderTargetLike[];
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
@@ -37,12 +53,8 @@ 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.46,
|
||||
0.32,
|
||||
0.84,
|
||||
),
|
||||
createBloomPass: () =>
|
||||
new UnrealBloomPass(new THREE.Vector2(1, 1), 0.46, 0.32, 0.84),
|
||||
};
|
||||
|
||||
export function createPostProcessor(
|
||||
@@ -51,13 +63,19 @@ export function createPostProcessor(
|
||||
factories: PostProcessingFactories = productionFactories,
|
||||
): PostProcessor {
|
||||
const composer = factories.createComposer(renderer);
|
||||
const renderPass = factories.createRenderPass(new THREE.Scene(), new THREE.Camera());
|
||||
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));
|
||||
@@ -80,7 +98,41 @@ export function createPostProcessor(
|
||||
composer.dispose();
|
||||
},
|
||||
};
|
||||
return resources.own('pass', processor);
|
||||
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 {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export type SceneResourceKind =
|
||||
| 'geometry'
|
||||
| 'material'
|
||||
| 'texture'
|
||||
| 'target'
|
||||
| 'pass'
|
||||
| 'renderer'
|
||||
| 'other';
|
||||
| "geometry"
|
||||
| "material"
|
||||
| "texture"
|
||||
| "target"
|
||||
| "pass"
|
||||
| "renderer"
|
||||
| "other";
|
||||
|
||||
export interface DisposableResource {
|
||||
dispose(): void;
|
||||
@@ -42,15 +42,25 @@ export interface GpuRenderTargetAllocation {
|
||||
samples: number;
|
||||
}
|
||||
|
||||
export interface GpuRenderTargetReporter {
|
||||
gpuRenderTargets(): readonly GpuRenderTargetAllocation[];
|
||||
}
|
||||
|
||||
export interface SceneGpuBudgetSnapshot {
|
||||
tier: 'low' | 'high';
|
||||
tier: "low" | "high";
|
||||
textures: readonly GpuTextureAllocation[];
|
||||
geometryBytes: number;
|
||||
renderTargets: readonly GpuRenderTargetAllocation[];
|
||||
}
|
||||
|
||||
const resourceKinds: readonly SceneResourceKind[] = [
|
||||
'geometry', 'material', 'texture', 'target', 'pass', 'renderer', 'other',
|
||||
"geometry",
|
||||
"material",
|
||||
"texture",
|
||||
"target",
|
||||
"pass",
|
||||
"renderer",
|
||||
"other",
|
||||
];
|
||||
|
||||
export class SceneResourceBag {
|
||||
@@ -141,7 +151,9 @@ export class SceneResourceBag {
|
||||
}
|
||||
|
||||
snapshot(): SceneResourceSnapshot {
|
||||
const owned = Object.fromEntries(resourceKinds.map((kind) => [kind, 0])) as Record<SceneResourceKind, number>;
|
||||
const owned = Object.fromEntries(
|
||||
resourceKinds.map((kind) => [kind, 0]),
|
||||
) as Record<SceneResourceKind, number>;
|
||||
for (const kind of this.resources.values()) owned[kind] += 1;
|
||||
return {
|
||||
disposed: this.disposed,
|
||||
@@ -154,23 +166,38 @@ export class SceneResourceBag {
|
||||
}
|
||||
|
||||
gpuBudgetSnapshot(input: {
|
||||
tier: 'low' | 'high';
|
||||
defaultFramebuffer: Omit<GpuRenderTargetAllocation, 'id'>;
|
||||
tier: "low" | "high";
|
||||
defaultFramebuffer: Omit<GpuRenderTargetAllocation, "id">;
|
||||
}): SceneGpuBudgetSnapshot {
|
||||
let geometryBytes = 0;
|
||||
const seenBuffers = new Set<ArrayBufferLike>();
|
||||
const textures: GpuTextureAllocation[] = [];
|
||||
const renderTargets: GpuRenderTargetAllocation[] = [{ id: 'default-framebuffer', ...input.defaultFramebuffer }];
|
||||
const renderTargets: GpuRenderTargetAllocation[] = [
|
||||
{ id: "default-framebuffer", ...input.defaultFramebuffer },
|
||||
];
|
||||
let textureIndex = 0;
|
||||
let targetIndex = 0;
|
||||
for (const [resource, kind] of this.resources) {
|
||||
if (kind === 'geometry') {
|
||||
const reporter = resource as DisposableResource &
|
||||
Partial<GpuRenderTargetReporter>;
|
||||
if (typeof reporter.gpuRenderTargets === "function") {
|
||||
for (const allocation of reporter.gpuRenderTargets()) {
|
||||
validateRenderTargetAllocation(allocation);
|
||||
renderTargets.push({ ...allocation });
|
||||
}
|
||||
}
|
||||
if (kind === "geometry") {
|
||||
const geometry = resource as DisposableResource & {
|
||||
attributes?: Record<string, { array?: ArrayBufferView; data?: { array?: ArrayBufferView } }>;
|
||||
attributes?: Record<
|
||||
string,
|
||||
{ array?: ArrayBufferView; data?: { array?: ArrayBufferView } }
|
||||
>;
|
||||
index?: { array?: ArrayBufferView } | null;
|
||||
};
|
||||
const arrays = [
|
||||
...Object.values(geometry.attributes ?? {}).map((attribute) => attribute.array ?? attribute.data?.array),
|
||||
...Object.values(geometry.attributes ?? {}).map(
|
||||
(attribute) => attribute.array ?? attribute.data?.array,
|
||||
),
|
||||
geometry.index?.array,
|
||||
].filter((array): array is ArrayBufferView => array !== undefined);
|
||||
for (const array of arrays) {
|
||||
@@ -179,28 +206,58 @@ export class SceneResourceBag {
|
||||
geometryBytes += array.byteLength;
|
||||
}
|
||||
}
|
||||
if (kind === 'texture') {
|
||||
if (kind === "texture") {
|
||||
const texture = resource as DisposableResource & {
|
||||
image?: { width?: number; height?: number; data?: ArrayBufferView };
|
||||
generateMipmaps?: boolean;
|
||||
};
|
||||
const width = Number(texture.image?.width);
|
||||
const height = Number(texture.image?.height);
|
||||
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
|
||||
throw new Error('Owned production texture is missing measurable dimensions.');
|
||||
if (
|
||||
!Number.isInteger(width) ||
|
||||
width <= 0 ||
|
||||
!Number.isInteger(height) ||
|
||||
height <= 0
|
||||
) {
|
||||
throw new Error(
|
||||
"Owned production texture is missing measurable dimensions.",
|
||||
);
|
||||
}
|
||||
const dataBytes = texture.image?.data?.byteLength;
|
||||
const bytesPerPixel = dataBytes === undefined ? 4 : dataBytes / (width * height);
|
||||
if (!Number.isInteger(bytesPerPixel) || bytesPerPixel <= 0) throw new Error('Owned production texture has an unsupported pixel layout.');
|
||||
textures.push({ id: `owned-texture-${textureIndex}`, width, height, bytesPerPixel, layers: 1, mipmapped: texture.generateMipmaps !== false });
|
||||
const bytesPerPixel =
|
||||
dataBytes === undefined ? 4 : dataBytes / (width * height);
|
||||
if (!Number.isInteger(bytesPerPixel) || bytesPerPixel <= 0)
|
||||
throw new Error(
|
||||
"Owned production texture has an unsupported pixel layout.",
|
||||
);
|
||||
textures.push({
|
||||
id: `owned-texture-${textureIndex}`,
|
||||
width,
|
||||
height,
|
||||
bytesPerPixel,
|
||||
layers: 1,
|
||||
mipmapped: texture.generateMipmaps !== false,
|
||||
});
|
||||
textureIndex += 1;
|
||||
}
|
||||
if (kind === 'target') {
|
||||
const target = resource as DisposableResource & { width?: number; height?: number; samples?: number; depthBuffer?: boolean };
|
||||
if (kind === "target") {
|
||||
const target = resource as DisposableResource & {
|
||||
width?: number;
|
||||
height?: number;
|
||||
samples?: number;
|
||||
depthBuffer?: boolean;
|
||||
};
|
||||
const width = Number(target.width);
|
||||
const height = Number(target.height);
|
||||
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
|
||||
throw new Error('Owned production render target is missing measurable dimensions.');
|
||||
if (
|
||||
!Number.isInteger(width) ||
|
||||
width <= 0 ||
|
||||
!Number.isInteger(height) ||
|
||||
height <= 0
|
||||
) {
|
||||
throw new Error(
|
||||
"Owned production render target is missing measurable dimensions.",
|
||||
);
|
||||
}
|
||||
renderTargets.push({
|
||||
id: `owned-render-target-${targetIndex}`,
|
||||
@@ -216,3 +273,25 @@ export class SceneResourceBag {
|
||||
return { tier: input.tier, textures, geometryBytes, renderTargets };
|
||||
}
|
||||
}
|
||||
|
||||
function validateRenderTargetAllocation(
|
||||
allocation: GpuRenderTargetAllocation,
|
||||
): void {
|
||||
if (
|
||||
allocation.id.length === 0 ||
|
||||
!Number.isInteger(allocation.width) ||
|
||||
allocation.width <= 0 ||
|
||||
!Number.isInteger(allocation.height) ||
|
||||
allocation.height <= 0 ||
|
||||
!Number.isInteger(allocation.bytesPerPixel) ||
|
||||
allocation.bytesPerPixel <= 0 ||
|
||||
!Number.isInteger(allocation.attachments) ||
|
||||
allocation.attachments <= 0 ||
|
||||
!Number.isInteger(allocation.samples) ||
|
||||
allocation.samples <= 0
|
||||
) {
|
||||
throw new Error(
|
||||
"Owned post-processing render target has an invalid allocation description.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user