gpu-curtains
Version:
gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.
559 lines (558 loc) • 19.5 kB
JavaScript
import { generateUUID, throwWarning } from "../../utils/utils.mjs";
import { isRenderer } from "../../core/renderers/utils.mjs";
import { Vec2 } from "../../math/Vec2.mjs";
import { Texture } from "../../core/textures/Texture.mjs";
import { Mat3 } from "../../math/Mat3.mjs";
import { Sampler } from "../../core/samplers/Sampler.mjs";
import { ComputePass } from "../../core/computePasses/ComputePass.mjs";
import { HDRLoader } from "../loaders/HDRLoader.mjs";
import { computeBRDFLUT } from "../../core/shaders/full/compute/compute-BRDF-LUT.mjs";
import { computeCubemapFromHDR } from "../../core/shaders/full/compute/compute-cubemap-from-HDR.mjs";
import { computeDiffuseFromCubemap } from "../../core/shaders/full/compute/compute-diffuse-from-cubemap.mjs";
import { PMREMGeneration } from "../../core/shaders/chunks/utils/PMREM-generation.mjs";
//#region src/extras/environmentMap/EnvironmentMap.ts
/**
* Utility to create environment maps specular, diffuse and LUT textures using an HDR file.
*
* Create a LUT texture on init using a {@link ComputePass}. Can load an HDR file and then create the cubemap and diffuse textures using two separate {@link ComputePass} and the PMREM texture using custom mips.
*
* Especially useful for IBL shading with {@link extras/meshes/LitMesh.LitMesh | LitMesh}.
*
* @example
* ```javascript
* // assuming 'renderer' is a valid renderer or curtains instance
* const environmentMap = new EnvironmentMap(renderer)
* await environmentMap.loadAndComputeFromHDR('path/to/environment-map.hdr')
* ```
*/
var EnvironmentMap = class {
/** Parsed {@link HDRImageData} from the {@link HDRLoader} if any. */
#hdrData;
/** BRDF GGX LUT storage {@link Texture} used in the compute shader. */
#lutStorageTexture;
/**
* {@link EnvironmentMap} constructor.
* @param renderer - {@link Renderer} or {@link GPUCurtains} class object used to create this {@link EnvironmentMap}.
* @param params - {@link EnvironmentMapParams | parameters} use to create this {@link EnvironmentMap}. Defines the various textures options.
*/
constructor(renderer, params = {}) {
this._onRotationAxisChangedCallback = () => {};
this.uuid = generateUUID();
this.setRenderer(renderer);
const lutTextureDefaultParams = {
size: 256,
computeSampleCount: 512,
label: "Environment LUT texture",
name: "lutTexture",
format: "rgba16float"
};
const diffuseTextureDefaultParams = {
size: 128,
computeSampleCount: 2048,
label: "Environment diffuse texture",
name: "envDiffuseTexture",
format: "rgba16float"
};
const specularTextureDefaultParams = {
label: "Environment specular texture",
name: "envSpecularTexture",
format: "rgba16float",
numSamples: 512
};
params = {
useLutTexture: true,
diffuseIntensity: 1,
specularIntensity: 1,
rotation: Math.PI / 2,
...params
};
if (params.lutTextureParams) params.lutTextureParams = {
...lutTextureDefaultParams,
...params.lutTextureParams
};
else params.lutTextureParams = lutTextureDefaultParams;
if (params.diffuseTextureParams) params.diffuseTextureParams = {
...diffuseTextureDefaultParams,
...params.diffuseTextureParams
};
else params.diffuseTextureParams = diffuseTextureDefaultParams;
if (params.specularTextureParams) params.specularTextureParams = {
...specularTextureDefaultParams,
...params.specularTextureParams
};
else params.specularTextureParams = specularTextureDefaultParams;
this.options = params;
this.sampler = new Sampler(this.renderer, {
label: "Clamp sampler",
name: "clampSampler",
magFilter: "linear",
minFilter: "linear",
mipmapFilter: "linear",
addressModeU: "clamp-to-edge",
addressModeV: "clamp-to-edge",
addressModeW: "clamp-to-edge"
});
this.rotationMatrix = new Mat3().rotateByAngleY(-Math.PI / 2);
this.hdrLoader = new HDRLoader();
if (this.options.useLutTexture) {
this.createLUTTextures();
this.computeBRDFLUTTexture();
}
this.createSpecularDiffuseTextures();
}
/**
* Set or reset this {@link EnvironmentMap} {@link EnvironmentMap.renderer | renderer}.
* @param renderer - New {@link Renderer} or {@link GPUCurtains} instance to use.
*/
setRenderer(renderer) {
if (this.renderer) this.renderer.environmentMaps.delete(this.uuid);
renderer = isRenderer(renderer, "EnvironmentMap");
this.renderer = renderer;
this.renderer.environmentMaps.set(this.uuid, this);
}
/**
* Get the current {@link EnvironmentMapOptions.rotation | rotation}, in radians.
*/
get rotation() {
return this.options.rotation;
}
/**
* Set the current {@link EnvironmentMapOptions.rotation | rotation}, in radians.
* @param value - New {@link EnvironmentMapOptions.rotation | rotation} to use, in radians.
*/
set rotation(value) {
if (value !== this.options.rotation) {
this.options.rotation = value;
this.rotationMatrix.rotateByAngleY(-value);
this._onRotationAxisChangedCallback && this._onRotationAxisChangedCallback();
}
}
/**
* Callback to call whenever the {@link EnvironmentMapOptions.rotation | rotation} changed.
* @param callback - Called whenever the {@link EnvironmentMapOptions.rotation | rotation} changed.
*/
onRotationAxisChanged(callback) {
if (callback) this._onRotationAxisChangedCallback = callback;
return this;
}
/**
* Create our {@link lutTexture} eagerly.
*/
createLUTTextures() {
const { size, computeSampleCount, ...lutTextureParams } = this.options.lutTextureParams;
this.#lutStorageTexture = new Texture(this.renderer, {
label: "LUT storage texture",
name: "lutStorageTexture",
format: lutTextureParams.format,
visibility: ["compute", "fragment"],
usage: [
"copySrc",
"storageBinding",
"textureBinding"
],
type: "storage",
fixedSize: {
width: size,
height: size
},
autoDestroy: false
});
this.lutTexture = new Texture(this.renderer, {
...lutTextureParams,
visibility: ["fragment"],
fixedSize: {
width: size,
height: size
},
autoDestroy: false,
fromTexture: this.#lutStorageTexture
});
}
/**
* Create our {@link specularTexture} and {@link diffuseTexture} eagerly. They could be resized later when calling the {@link computeFromHDR} method.
*/
createSpecularDiffuseTextures() {
const textureDefaultOptions = {
viewDimension: "cube",
autoDestroy: false
};
this.cubemapTexture = new Texture(this.renderer, {
label: "Environment cube map texture",
name: "cubemapTexture",
format: this.options.specularTextureParams.format,
generateMips: true,
visibility: ["fragment", "compute"],
fixedSize: {
width: 256,
height: 256
},
...textureDefaultOptions
});
this.specularTexture = new Texture(this.renderer, {
...this.options.specularTextureParams,
generateMips: false,
useMips: true,
visibility: ["fragment", "compute"],
fixedSize: {
width: 256,
height: 256
},
...textureDefaultOptions
});
const { size, computeSampleCount, ...diffuseTextureParams } = this.options.diffuseTextureParams;
this.diffuseTexture = new Texture(this.renderer, {
...diffuseTextureParams,
visibility: ["fragment"],
fixedSize: {
width: size,
height: size
},
...textureDefaultOptions
});
}
/**
* Run a {@link ComputePass} once by creating a {@link GPUCommandEncoder} and execute the pass.
* @param parameters - Parameters used to run the compute pass.
* @param parameters.computePass - {@link ComputePass} to run.
* @param parameters.label - Optional label for the {@link GPUCommandEncoder}.
* @param parameters.onAfterCompute - Optional callback to run just after the pass has been executed. Useful for eventual texture copies.
* @private
*/
#runComputePass({ computePass, label = "", onAfterCompute = (commandEncoder) => {} }) {
const commandEncoder = this.renderer.device?.createCommandEncoder({ label });
!this.renderer.production && commandEncoder.pushDebugGroup(label);
this.renderer.renderSingleComputePass(commandEncoder, computePass, false);
onAfterCompute(commandEncoder);
!this.renderer.production && commandEncoder.popDebugGroup();
const commandBuffer = commandEncoder.finish();
this.renderer.device?.queue.submit([commandBuffer]);
this.renderer.pipelineManager.resetCurrentPipeline();
}
/**
* Create the {@link lutTexture | BRDF GGX and sheen LUT texture} using the provided {@link LUTTextureParams | LUT texture options} and a {@link ComputePass} that runs once.
*/
async computeBRDFLUTTexture() {
let cachedLUT = null;
for (const renderer of this.renderer.deviceManager.renderers) {
for (const [uuid, envMap] of renderer.environmentMaps) if (uuid !== this.uuid && envMap.lutTexture && envMap.lutTexture.size.width === this.lutTexture.size.width) {
cachedLUT = envMap.lutTexture;
break;
}
if (cachedLUT) break;
}
if (cachedLUT) {
this.lutTexture.copy(cachedLUT);
return;
}
const { computeSampleCount } = this.options.lutTextureParams;
let computeLUTPass = new ComputePass(this.renderer, {
label: "Compute LUT texture",
autoRender: false,
dispatchSize: [
Math.ceil(this.#lutStorageTexture.size.width / 8),
Math.ceil(this.#lutStorageTexture.size.height / 8),
1
],
shaders: { compute: { code: computeBRDFLUT } },
uniforms: { params: { struct: { sampleCount: {
type: "u32",
value: computeSampleCount
} } } },
textures: [this.#lutStorageTexture]
});
await computeLUTPass.material.compileMaterial();
this.#runComputePass({
computePass: computeLUTPass,
label: "Compute LUT texture command encoder"
});
this.lutTexture.textureBinding.resource = this.lutTexture.texture;
computeLUTPass.remove();
computeLUTPass = null;
}
/**
* Create the {@link cubemapTexture | cube map texture} from a loaded {@link HDRImageData} using a {@link ComputePass} that runs once.
* @param parsedHdr - parsed {@link HDRImageData} loaded by the {@link hdrLoader}.
*/
async computeSpecularCubemapFromHDRData(parsedHdr) {
let cubeStorageTexture = new Texture(this.renderer, {
label: "Cubemap storage",
name: "storageCubemap",
format: this.cubemapTexture.options.format,
visibility: ["compute"],
usage: [
"copySrc",
"storageBinding",
"textureBinding"
],
type: "storage",
fixedSize: {
width: this.cubemapTexture.size.width,
height: this.cubemapTexture.size.height,
depth: 6
},
viewDimension: "2d-array"
});
let computeCubeMapPass = new ComputePass(this.renderer, {
label: "Compute cubemap from equirectangular",
autoRender: false,
dispatchSize: [
Math.ceil(this.cubemapTexture.size.width / 8),
Math.ceil(this.cubemapTexture.size.height / 8),
6
],
shaders: { compute: { code: computeCubemapFromHDR } },
storages: { params: {
visibility: ["compute"],
struct: {
hdrImageData: {
type: "array<vec4f>",
value: parsedHdr.data
},
imageSize: {
type: "vec2f",
value: new Vec2(parsedHdr.width, parsedHdr.height)
},
faceSize: {
type: "u32",
value: this.cubemapTexture.size.width
}
}
} },
textures: [cubeStorageTexture]
});
await computeCubeMapPass.material.compileMaterial();
let mipBuffers = [];
this.#runComputePass({
computePass: computeCubeMapPass,
label: "Compute specular cube map command encoder",
onAfterCompute: (commandEncoder) => {
this.renderer.copyGPUTextureToTexture(cubeStorageTexture.texture, this.cubemapTexture, commandEncoder);
this.generateSpecularPMREMTexture(commandEncoder, mipBuffers);
}
});
computeCubeMapPass.remove();
cubeStorageTexture.destroy();
mipBuffers.forEach((buffer) => buffer.destroy());
cubeStorageTexture = null;
computeCubeMapPass = null;
mipBuffers = [];
}
/**
* Generates the {@link specularTexture} Prefiltered, Mipmapped Radiance Environment Map (PMREM).
* We manually generate the {@link specularTexture} prefiltered mips from our original {@link cubemapTexture}.
*
* @param commandEncoder - {@link GPUCommandEncoder} to use for mips generation.
* @param mipBuffers - Array of {@link GPUBuffer} that will be created for each mips. Will be destroyed later.
*/
generateSpecularPMREMTexture(commandEncoder, mipBuffers) {
if (!this.cubemapTexture.texture) {
if (!this.renderer.production) throwWarning("EnvironmentMap: Could not generate the PMREM mips because the cubemap texture is not set:" + this.cubemapTexture);
return;
}
const shaderModule = this.renderer.device.createShaderModule({
label: "PMREM generation",
code: PMREMGeneration
});
const pipeline = this.renderer.device.createRenderPipeline({
label: "Mip level generator pipeline",
layout: "auto",
vertex: { module: shaderModule },
fragment: {
module: shaderModule,
targets: [{ format: this.specularTexture.texture.format }]
}
});
let width = this.specularTexture.texture.width;
let height = this.specularTexture.texture.height;
const mipCount = this.specularTexture.texture.mipLevelCount;
const nbFaces = this.specularTexture.texture.depthOrArrayLayers;
let baseMipLevel = 0;
const generateMips = (baseMipLevel = 0) => {
for (let layer = 0; layer < nbFaces; layer++) {
const faceMipArray = new Uint32Array([
layer,
baseMipLevel + 1,
mipCount,
this.options.specularTextureParams.numSamples,
this.specularTexture.texture.width,
0,
0,
0
]);
const paramsBuffer = this.renderer.device.createBuffer({
size: faceMipArray.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Uint32Array(paramsBuffer.getMappedRange()).set(faceMipArray);
paramsBuffer.unmap();
mipBuffers.push(paramsBuffer);
const bindGroup = this.renderer.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: this.sampler.sampler
},
{
binding: 1,
resource: this.cubemapTexture.texture.createView({
dimension: "cube",
arrayLayerCount: 6
})
},
{
binding: 2,
resource: { buffer: paramsBuffer }
}
]
});
const renderPassDescriptor = {
label: "PMREM generation render pass",
colorAttachments: [{
view: this.specularTexture.texture.createView({
dimension: "2d",
baseMipLevel: baseMipLevel + 1,
mipLevelCount: 1,
baseArrayLayer: layer,
arrayLayerCount: 1
}),
loadOp: "clear",
storeOp: "store"
}]
};
const pass = commandEncoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(6);
pass.end();
}
};
generateMips(-1);
while (width > 1 || height > 1) {
width = Math.max(1, width / 2 | 0);
height = Math.max(1, height / 2 | 0);
generateMips(baseMipLevel);
baseMipLevel++;
}
this.specularTexture.textureBinding.resource = this.specularTexture.texture;
}
/**
* Compute the {@link diffuseTexture | diffuse cube map texture} from the {@link cubemapTexture | cube map texture } using the provided {@link DiffuseTextureParams | diffuse texture options} and a {@link ComputePass} that runs once.
*/
async computeDiffuseFromCubemap() {
if (!this.cubemapTexture.texture) {
if (!this.renderer.production) throwWarning("EnvironmentMap: Could not generate the diffuse texture because the cube map texture is not set:" + this.cubemapTexture);
return;
}
let diffuseStorageTexture = new Texture(this.renderer, {
label: "Diffuse storage cubemap",
name: "diffuseEnvMap",
format: this.diffuseTexture.options.format,
visibility: ["compute"],
usage: ["copySrc", "storageBinding"],
type: "storage",
fixedSize: {
width: this.diffuseTexture.size.width,
height: this.diffuseTexture.size.height,
depth: 6
},
viewDimension: "2d-array"
});
let computeDiffusePass = new ComputePass(this.renderer, {
label: "Compute diffuse map from specular map",
autoRender: false,
dispatchSize: [
Math.ceil(this.diffuseTexture.size.width / 8),
Math.ceil(this.diffuseTexture.size.height / 8),
6
],
shaders: { compute: { code: computeDiffuseFromCubemap(this.cubemapTexture) } },
uniforms: { params: { struct: {
faceSize: {
type: "u32",
value: this.diffuseTexture.size.width
},
maxMipLevel: {
type: "u32",
value: this.cubemapTexture.texture.mipLevelCount
},
sampleCount: {
type: "u32",
value: this.options.diffuseTextureParams.computeSampleCount
}
} } },
samplers: [this.sampler],
textures: [this.cubemapTexture, diffuseStorageTexture]
});
await computeDiffusePass.material.compileMaterial();
this.#runComputePass({
computePass: computeDiffusePass,
label: "Compute diffuse cube map from specular cube map command encoder",
onAfterCompute: (commandEncoder) => {
this.renderer.copyGPUTextureToTexture(diffuseStorageTexture.texture, this.diffuseTexture, commandEncoder);
this.diffuseTexture.textureBinding.resource = this.diffuseTexture.texture;
}
});
computeDiffusePass.remove();
diffuseStorageTexture.destroy();
diffuseStorageTexture = null;
computeDiffusePass = null;
}
/**
* Load an HDR environment map and then generate the {@link specularTexture} and {@link diffuseTexture} using two separate {@link ComputePass}.
* @param url - The url of the .hdr file to load.
*/
async loadAndComputeFromHDR(url) {
this.#hdrData = await this.hdrLoader.loadFromUrl(url);
const { width, height } = this.#hdrData ? this.#hdrData : {
width: 1024,
height: 512
};
const faceSize = Math.max(width / 4, height / 2);
if (this.cubemapTexture.size.width !== faceSize || this.cubemapTexture.size.height !== faceSize) {
this.cubemapTexture.options.fixedSize.width = faceSize;
this.cubemapTexture.options.fixedSize.height = faceSize;
this.cubemapTexture.size.width = faceSize;
this.cubemapTexture.size.height = faceSize;
this.cubemapTexture.createTexture();
}
if (this.specularTexture.size.width !== faceSize || this.specularTexture.size.height !== faceSize) {
this.specularTexture.options.fixedSize.width = faceSize;
this.specularTexture.options.fixedSize.height = faceSize;
this.specularTexture.size.width = faceSize;
this.specularTexture.size.height = faceSize;
this.specularTexture.createTexture();
}
const { size } = this.options.diffuseTextureParams;
const diffuseSize = Math.min(size, faceSize);
if (this.diffuseTexture.size.width !== diffuseSize || this.diffuseTexture.size.height !== diffuseSize) {
this.diffuseTexture.options.fixedSize.width = diffuseSize;
this.diffuseTexture.options.fixedSize.height = diffuseSize;
this.diffuseTexture.size.width = diffuseSize;
this.diffuseTexture.size.height = diffuseSize;
this.diffuseTexture.createTexture();
}
this.computeFromHDR();
}
/**
* Generate the {@link specularTexture} and {@link diffuseTexture} using two separate {@link ComputePass}.
*/
computeFromHDR() {
if (this.#hdrData) this.computeSpecularCubemapFromHDRData(this.#hdrData).then(() => {
this.computeDiffuseFromCubemap();
});
}
/**
* Destroy the {@link EnvironmentMap} and its associated textures.
*/
destroy() {
this.cubemapTexture?.destroy();
this.diffuseTexture?.destroy();
this.specularTexture?.destroy();
this.lutTexture?.destroy();
this.#lutStorageTexture.destroy();
}
};
//#endregion
export { EnvironmentMap };