auto-git:
[add] src/rendering/quantized-environment-blend-cache.ts
This commit is contained in:
505
src/rendering/quantized-environment-blend-cache.ts
Normal file
505
src/rendering/quantized-environment-blend-cache.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
import {
|
||||
EquirectangularReflectionMapping,
|
||||
HalfFloatType,
|
||||
LinearFilter,
|
||||
LinearSRGBColorSpace,
|
||||
Mesh,
|
||||
NoToneMapping,
|
||||
OrthographicCamera,
|
||||
PlaneGeometry,
|
||||
PMREMGenerator,
|
||||
Scene,
|
||||
ShaderMaterial,
|
||||
SRGBColorSpace,
|
||||
Texture,
|
||||
WebGLRenderTarget,
|
||||
WebGLRenderer
|
||||
} from "three";
|
||||
|
||||
import type { WorldEnvironmentBlendTextureResolver } from "./world-background-renderer";
|
||||
|
||||
const DEFAULT_QUANTIZED_BLEND_BUCKET_COUNT = 8;
|
||||
const DEFAULT_BLEND_RENDER_TARGET_WIDTH = 512;
|
||||
const DEFAULT_BLEND_RENDER_TARGET_HEIGHT = 256;
|
||||
|
||||
const BLEND_VERTEX_SHADER = `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position.xy, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const BLEND_FRAGMENT_SHADER = `
|
||||
uniform sampler2D uBaseTexture;
|
||||
uniform sampler2D uOverlayTexture;
|
||||
uniform float uBlendAmount;
|
||||
uniform float uBaseTextureIsSrgb;
|
||||
uniform float uOverlayTextureIsSrgb;
|
||||
varying vec2 vUv;
|
||||
|
||||
vec3 srgbToLinear(vec3 color) {
|
||||
bvec3 cutoff = lessThanEqual(color, vec3(0.04045));
|
||||
vec3 lower = color / 12.92;
|
||||
vec3 higher = pow((color + 0.055) / 1.055, vec3(2.4));
|
||||
return mix(higher, lower, vec3(cutoff));
|
||||
}
|
||||
|
||||
vec3 decodeTextureColor(sampler2D map, vec2 uv, float isSrgb) {
|
||||
vec3 color = texture2D(map, uv).rgb;
|
||||
return mix(color, srgbToLinear(color), isSrgb);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 baseColor = decodeTextureColor(uBaseTexture, vUv, uBaseTextureIsSrgb);
|
||||
vec3 overlayColor = decodeTextureColor(
|
||||
uOverlayTexture,
|
||||
vUv,
|
||||
uOverlayTextureIsSrgb
|
||||
);
|
||||
|
||||
gl_FragColor = vec4(
|
||||
mix(baseColor, overlayColor, clamp(uBlendAmount, 0.0, 1.0)),
|
||||
1.0
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function defaultBuildScheduler(callback: () => void) {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.requestAnimationFrame === "function"
|
||||
) {
|
||||
window.requestAnimationFrame(() => {
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(callback, 0);
|
||||
}
|
||||
|
||||
function getTextureCacheKey(texture: Texture): string {
|
||||
return texture.uuid;
|
||||
}
|
||||
|
||||
function getOrderedPairCacheKey(
|
||||
baseTexture: Texture,
|
||||
overlayTexture: Texture
|
||||
): string {
|
||||
return `${getTextureCacheKey(baseTexture)}->${getTextureCacheKey(overlayTexture)}`;
|
||||
}
|
||||
|
||||
function isSrgbTexture(texture: Texture): boolean {
|
||||
return texture.colorSpace === SRGBColorSpace;
|
||||
}
|
||||
|
||||
export interface CachedEnvironmentBlendTexture {
|
||||
texture: Texture;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
export interface QuantizedEnvironmentBlendCacheOptions {
|
||||
bucketCount?: number;
|
||||
buildBlendTexture: (
|
||||
baseTexture: Texture,
|
||||
overlayTexture: Texture,
|
||||
blendAmount: number
|
||||
) => CachedEnvironmentBlendTexture;
|
||||
disposeBuildResources?: () => void;
|
||||
onTextureReady?: () => void;
|
||||
scheduleBuild?: (callback: () => void) => void;
|
||||
}
|
||||
|
||||
interface QueuedEnvironmentBlendBuild {
|
||||
baseTexture: Texture;
|
||||
overlayTexture: Texture;
|
||||
bucketIndex: number;
|
||||
blendAmount: number;
|
||||
key: string;
|
||||
pairKey: string;
|
||||
}
|
||||
|
||||
export function quantizeEnvironmentBlendBucket(
|
||||
blendAmount: number,
|
||||
bucketCount: number = DEFAULT_QUANTIZED_BLEND_BUCKET_COUNT
|
||||
): number {
|
||||
const safeBucketCount = Math.max(1, Math.floor(bucketCount));
|
||||
return Math.round(clamp(blendAmount, 0, 1) * safeBucketCount);
|
||||
}
|
||||
|
||||
export function getQuantizedEnvironmentBlendAmount(
|
||||
bucketIndex: number,
|
||||
bucketCount: number = DEFAULT_QUANTIZED_BLEND_BUCKET_COUNT
|
||||
): number {
|
||||
const safeBucketCount = Math.max(1, Math.floor(bucketCount));
|
||||
return clamp(bucketIndex / safeBucketCount, 0, 1);
|
||||
}
|
||||
|
||||
export function createQuantizedEnvironmentBlendCacheKey(
|
||||
baseTexture: Texture,
|
||||
overlayTexture: Texture,
|
||||
bucketIndex: number
|
||||
): string {
|
||||
return `${getOrderedPairCacheKey(baseTexture, overlayTexture)}@${bucketIndex}`;
|
||||
}
|
||||
|
||||
export class QuantizedEnvironmentBlendCache
|
||||
implements WorldEnvironmentBlendTextureResolver
|
||||
{
|
||||
private readonly bucketCount: number;
|
||||
private readonly buildBlendTexture: QuantizedEnvironmentBlendCacheOptions["buildBlendTexture"];
|
||||
private readonly disposeBuildResources: (() => void) | undefined;
|
||||
private readonly onTextureReady: (() => void) | undefined;
|
||||
private readonly scheduleBuildCallback: (callback: () => void) => void;
|
||||
private readonly entries = new Map<string, CachedEnvironmentBlendTexture>();
|
||||
private readonly pairEntries = new Map<
|
||||
string,
|
||||
Map<number, CachedEnvironmentBlendTexture>
|
||||
>();
|
||||
private readonly pendingKeys = new Set<string>();
|
||||
private readonly failedKeys = new Set<string>();
|
||||
private pendingBuilds: QueuedEnvironmentBlendBuild[] = [];
|
||||
private buildScheduled = false;
|
||||
private disposed = false;
|
||||
|
||||
constructor(options: QuantizedEnvironmentBlendCacheOptions) {
|
||||
this.bucketCount = Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
options.bucketCount ?? DEFAULT_QUANTIZED_BLEND_BUCKET_COUNT
|
||||
)
|
||||
);
|
||||
this.buildBlendTexture = options.buildBlendTexture;
|
||||
this.disposeBuildResources = options.disposeBuildResources;
|
||||
this.onTextureReady = options.onTextureReady;
|
||||
this.scheduleBuildCallback = options.scheduleBuild ?? defaultBuildScheduler;
|
||||
}
|
||||
|
||||
resolveBlendTexture(
|
||||
baseTexture: Texture,
|
||||
overlayTexture: Texture,
|
||||
blendAmount: number
|
||||
): Texture | null {
|
||||
const bucketIndex = quantizeEnvironmentBlendBucket(
|
||||
blendAmount,
|
||||
this.bucketCount
|
||||
);
|
||||
|
||||
if (bucketIndex <= 0) {
|
||||
return baseTexture;
|
||||
}
|
||||
|
||||
if (bucketIndex >= this.bucketCount) {
|
||||
return overlayTexture;
|
||||
}
|
||||
|
||||
const key = createQuantizedEnvironmentBlendCacheKey(
|
||||
baseTexture,
|
||||
overlayTexture,
|
||||
bucketIndex
|
||||
);
|
||||
const cachedEntry = this.entries.get(key);
|
||||
|
||||
if (cachedEntry !== undefined) {
|
||||
return cachedEntry.texture;
|
||||
}
|
||||
|
||||
if (!this.failedKeys.has(key)) {
|
||||
this.queueBuild({
|
||||
baseTexture,
|
||||
overlayTexture,
|
||||
bucketIndex,
|
||||
blendAmount: getQuantizedEnvironmentBlendAmount(
|
||||
bucketIndex,
|
||||
this.bucketCount
|
||||
),
|
||||
key,
|
||||
pairKey: getOrderedPairCacheKey(baseTexture, overlayTexture)
|
||||
});
|
||||
}
|
||||
|
||||
return this.findNearestCachedTexture(
|
||||
getOrderedPairCacheKey(baseTexture, overlayTexture),
|
||||
bucketIndex
|
||||
);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.pendingBuilds = [];
|
||||
this.pendingKeys.clear();
|
||||
this.failedKeys.clear();
|
||||
|
||||
for (const entry of this.entries.values()) {
|
||||
entry.dispose();
|
||||
}
|
||||
|
||||
this.entries.clear();
|
||||
this.pairEntries.clear();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposed = true;
|
||||
this.clear();
|
||||
this.disposeBuildResources?.();
|
||||
}
|
||||
|
||||
private queueBuild(request: QueuedEnvironmentBlendBuild) {
|
||||
if (
|
||||
this.disposed ||
|
||||
this.entries.has(request.key) ||
|
||||
this.pendingKeys.has(request.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingKeys.add(request.key);
|
||||
this.pendingBuilds.push(request);
|
||||
|
||||
if (this.buildScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildScheduled = true;
|
||||
this.scheduleBuildCallback(() => {
|
||||
this.buildScheduled = false;
|
||||
this.processNextBuild();
|
||||
});
|
||||
}
|
||||
|
||||
private processNextBuild() {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextBuild = this.pendingBuilds.shift();
|
||||
|
||||
if (nextBuild === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingKeys.delete(nextBuild.key);
|
||||
|
||||
if (this.entries.has(nextBuild.key) || this.failedKeys.has(nextBuild.key)) {
|
||||
this.schedulePendingBuilds();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const builtEntry = this.buildBlendTexture(
|
||||
nextBuild.baseTexture,
|
||||
nextBuild.overlayTexture,
|
||||
nextBuild.blendAmount
|
||||
);
|
||||
this.entries.set(nextBuild.key, builtEntry);
|
||||
let pairEntries = this.pairEntries.get(nextBuild.pairKey);
|
||||
|
||||
if (pairEntries === undefined) {
|
||||
pairEntries = new Map<number, CachedEnvironmentBlendTexture>();
|
||||
this.pairEntries.set(nextBuild.pairKey, pairEntries);
|
||||
}
|
||||
|
||||
pairEntries.set(nextBuild.bucketIndex, builtEntry);
|
||||
this.onTextureReady?.();
|
||||
} catch (error) {
|
||||
this.failedKeys.add(nextBuild.key);
|
||||
console.warn(
|
||||
"Failed to build quantized environment blend texture.",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
this.schedulePendingBuilds();
|
||||
}
|
||||
|
||||
private schedulePendingBuilds() {
|
||||
if (this.disposed || this.buildScheduled || this.pendingBuilds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildScheduled = true;
|
||||
this.scheduleBuildCallback(() => {
|
||||
this.buildScheduled = false;
|
||||
this.processNextBuild();
|
||||
});
|
||||
}
|
||||
|
||||
private findNearestCachedTexture(
|
||||
pairKey: string,
|
||||
bucketIndex: number
|
||||
): Texture | null {
|
||||
const pairEntries = this.pairEntries.get(pairKey);
|
||||
|
||||
if (pairEntries === undefined || pairEntries.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nearestTexture: Texture | null = null;
|
||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const [availableBucketIndex, entry] of pairEntries.entries()) {
|
||||
const distance = Math.abs(availableBucketIndex - bucketIndex);
|
||||
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
nearestTexture = entry.texture;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestTexture;
|
||||
}
|
||||
}
|
||||
|
||||
class RendererEnvironmentBlendTextureBuilder {
|
||||
private readonly pmremGenerator: PMREMGenerator;
|
||||
private readonly blendScene = new Scene();
|
||||
private readonly blendCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
private readonly blendMaterial = new ShaderMaterial({
|
||||
uniforms: {
|
||||
uBaseTexture: {
|
||||
value: null
|
||||
},
|
||||
uOverlayTexture: {
|
||||
value: null
|
||||
},
|
||||
uBlendAmount: {
|
||||
value: 0
|
||||
},
|
||||
uBaseTextureIsSrgb: {
|
||||
value: 1
|
||||
},
|
||||
uOverlayTextureIsSrgb: {
|
||||
value: 1
|
||||
}
|
||||
},
|
||||
vertexShader: BLEND_VERTEX_SHADER,
|
||||
fragmentShader: BLEND_FRAGMENT_SHADER,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
fog: false
|
||||
});
|
||||
private readonly blendMesh = new Mesh(
|
||||
new PlaneGeometry(2, 2),
|
||||
this.blendMaterial
|
||||
);
|
||||
private readonly scratchTarget: WebGLRenderTarget;
|
||||
|
||||
constructor(
|
||||
private readonly renderer: WebGLRenderer,
|
||||
options: {
|
||||
targetWidth?: number;
|
||||
targetHeight?: number;
|
||||
} = {}
|
||||
) {
|
||||
this.pmremGenerator = new PMREMGenerator(renderer);
|
||||
this.pmremGenerator.compileEquirectangularShader();
|
||||
this.blendMesh.frustumCulled = false;
|
||||
this.blendScene.add(this.blendMesh);
|
||||
this.scratchTarget = new WebGLRenderTarget(
|
||||
options.targetWidth ?? DEFAULT_BLEND_RENDER_TARGET_WIDTH,
|
||||
options.targetHeight ?? DEFAULT_BLEND_RENDER_TARGET_HEIGHT,
|
||||
{
|
||||
depthBuffer: false,
|
||||
stencilBuffer: false,
|
||||
magFilter: LinearFilter,
|
||||
minFilter: LinearFilter,
|
||||
type: HalfFloatType
|
||||
}
|
||||
);
|
||||
this.scratchTarget.texture.colorSpace = LinearSRGBColorSpace;
|
||||
this.scratchTarget.texture.mapping = EquirectangularReflectionMapping;
|
||||
this.scratchTarget.texture.generateMipmaps = false;
|
||||
}
|
||||
|
||||
build(
|
||||
baseTexture: Texture,
|
||||
overlayTexture: Texture,
|
||||
blendAmount: number
|
||||
): CachedEnvironmentBlendTexture {
|
||||
const previousRenderTarget = this.renderer.getRenderTarget();
|
||||
const previousAutoClear = this.renderer.autoClear;
|
||||
const previousXrEnabled = this.renderer.xr.enabled;
|
||||
const previousToneMapping = this.renderer.toneMapping;
|
||||
const previousOutputColorSpace = this.renderer.outputColorSpace;
|
||||
|
||||
this.renderer.xr.enabled = false;
|
||||
this.renderer.autoClear = true;
|
||||
this.renderer.toneMapping = NoToneMapping;
|
||||
this.renderer.outputColorSpace = LinearSRGBColorSpace;
|
||||
this.blendMaterial.uniforms.uBaseTexture.value = baseTexture;
|
||||
this.blendMaterial.uniforms.uOverlayTexture.value = overlayTexture;
|
||||
this.blendMaterial.uniforms.uBlendAmount.value = blendAmount;
|
||||
this.blendMaterial.uniforms.uBaseTextureIsSrgb.value = isSrgbTexture(
|
||||
baseTexture
|
||||
)
|
||||
? 1
|
||||
: 0;
|
||||
this.blendMaterial.uniforms.uOverlayTextureIsSrgb.value = isSrgbTexture(
|
||||
overlayTexture
|
||||
)
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
this.renderer.setRenderTarget(this.scratchTarget);
|
||||
this.renderer.clear();
|
||||
this.renderer.render(this.blendScene, this.blendCamera);
|
||||
|
||||
const pmremTarget = this.pmremGenerator.fromEquirectangular(
|
||||
this.scratchTarget.texture
|
||||
);
|
||||
|
||||
this.renderer.setRenderTarget(previousRenderTarget);
|
||||
this.renderer.autoClear = previousAutoClear;
|
||||
this.renderer.xr.enabled = previousXrEnabled;
|
||||
this.renderer.toneMapping = previousToneMapping;
|
||||
this.renderer.outputColorSpace = previousOutputColorSpace;
|
||||
|
||||
return {
|
||||
texture: pmremTarget.texture,
|
||||
dispose: () => {
|
||||
pmremTarget.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.scratchTarget.dispose();
|
||||
this.blendMesh.geometry.dispose();
|
||||
this.blendMaterial.dispose();
|
||||
this.pmremGenerator.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function createRendererQuantizedEnvironmentBlendCache(
|
||||
renderer: WebGLRenderer,
|
||||
options: Omit<
|
||||
QuantizedEnvironmentBlendCacheOptions,
|
||||
"buildBlendTexture" | "disposeBuildResources"
|
||||
> & {
|
||||
targetWidth?: number;
|
||||
targetHeight?: number;
|
||||
} = {}
|
||||
): QuantizedEnvironmentBlendCache {
|
||||
const builder = new RendererEnvironmentBlendTextureBuilder(renderer, {
|
||||
targetWidth: options.targetWidth,
|
||||
targetHeight: options.targetHeight
|
||||
});
|
||||
|
||||
return new QuantizedEnvironmentBlendCache({
|
||||
...options,
|
||||
buildBlendTexture: (baseTexture, overlayTexture, blendAmount) =>
|
||||
builder.build(baseTexture, overlayTexture, blendAmount),
|
||||
disposeBuildResources: () => {
|
||||
builder.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user