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.
880 lines (879 loc) • 35.6 kB
JavaScript
import { generateUUID, throwError, throwWarning } from "../../utils/utils.mjs";
import { Buffer } from "../buffers/Buffer.mjs";
import { MediaTexture } from "../textures/MediaTexture.mjs";
import { RenderPass } from "../renderPasses/RenderPass.mjs";
import { DOMElement } from "../DOM/DOMElement.mjs";
import { Scene } from "../scenes/Scene.mjs";
import { TasksQueueManager } from "../../utils/TasksQueueManager.mjs";
import { GPUDeviceManager } from "./GPUDeviceManager.mjs";
//#region src/core/renderers/GPURenderer.ts
/**
* Base renderer class, that could technically be used to render compute passes and draw fullscreen quads, even tho it is strongly advised to use at least the {@link core/renderers/GPUCameraRenderer.GPUCameraRenderer | GPUCameraRenderer} class instead.
* A renderer is responsible for:
* - Setting a {@link GPUCanvasContext | context}.
* - Handling the {@link HTMLCanvasElement | canvas} onto everything is drawn.
* - Creating a {@link RenderPass} that will handle our render and depth textures and the render pass descriptor.
* - Keeping track of every specific class objects created relative to computing and rendering.
* - Creating a {@link Scene} class that will take care of the rendering process of all previously mentioned objects.
*/
var GPURenderer = class {
/**
* GPURenderer constructor
* @param parameters - {@link GPURendererParams | parameters} used to create this {@link GPURenderer}.
*/
constructor({ deviceManager, label, container, pixelRatio = 1, autoResize = true, context = {}, renderPass }) {
this._onBeforeRenderCallback = (commandEncoder) => {};
this._onAfterRenderCallback = (commandEncoder) => {};
this._onResizeCallback = () => {};
this._onAfterResizeCallback = () => {};
this.type = "GPURenderer";
this.uuid = generateUUID();
if (!deviceManager || !(deviceManager instanceof GPUDeviceManager)) throwError(label ? `${label} (${this.type}): no device manager or wrong device manager provided: ${typeof deviceManager} (${deviceManager?.constructor.name})` : `${this.type}: no device manager or wrong device manager provided: ${typeof deviceManager} (${deviceManager?.constructor.name})`);
if (!label) label = `${this.constructor.name}${deviceManager.renderers.length}`;
this.deviceManager = deviceManager;
this.deviceManager.addRenderer(this);
this.shouldRender = true;
this.shouldRenderScene = true;
const contextOptions = {
alphaMode: "premultiplied",
format: this.deviceManager.gpu?.getPreferredCanvasFormat() || "bgra8unorm",
...context
};
renderPass = {
useDepth: true,
sampleCount: 4,
...renderPass
};
this.options = {
deviceManager,
label,
container,
pixelRatio,
autoResize,
context: contextOptions,
renderPass
};
this.pixelRatio = pixelRatio ?? window.devicePixelRatio ?? 1;
const isOffscreenCanvas = container instanceof OffscreenCanvas;
const isContainerCanvas = isOffscreenCanvas || container instanceof HTMLCanvasElement;
this.canvas = isContainerCanvas ? container : document.createElement("canvas");
const { width, height } = this.canvas;
this.rectBBox = {
width,
height,
top: 0,
left: 0
};
this.viewport = null;
this.scissorRect = null;
this.setScene();
this.setTasksQueues();
this.setRendererObjects();
this.setMainRenderPasses();
if (!isOffscreenCanvas) {
this.domElement = new DOMElement({
element: container,
priority: 5,
onSizeChanged: () => {
if (this.options.autoResize) this.resize();
}
});
this.resize();
if (!isContainerCanvas) this.domElement.element.appendChild(this.canvas);
}
if (this.deviceManager.device) this.setContext();
}
/**
* Set the renderer {@link RectBBox} and canvas sizes.
* @param rectBBox - The optional new {@link canvas} {@link RectBBox} to set.
*/
setSize(rectBBox = null) {
rectBBox = {
width: Math.max(1, this.boundingRect.width),
height: Math.max(1, this.boundingRect.height),
top: this.boundingRect.top,
left: this.boundingRect.left,
...rectBBox
};
this.rectBBox = rectBBox;
const renderingSize = {
width: this.rectBBox.width,
height: this.rectBBox.height
};
renderingSize.width *= this.pixelRatio;
renderingSize.height *= this.pixelRatio;
this.clampToMaxDimension(renderingSize);
this.canvas.width = Math.floor(renderingSize.width);
this.canvas.height = Math.floor(renderingSize.height);
if (this.canvas.style) {
this.canvas.style.width = this.rectBBox.width + "px";
this.canvas.style.height = this.rectBBox.height + "px";
}
}
/**
* Set the renderer, {@link renderPass} and {@link postProcessingPass} {@link viewport} values. Beware that if you use a {@link viewport}, you should resize it yourself so it does not overflow the `canvas` in the `onResize` callback to avoid issues.
* @param viewport - {@link RenderPassViewport} settings to use. Can be set to `null` to cancel the {@link viewport}.
*/
setViewport(viewport = null) {
if (!viewport) {
this.viewport = null;
this.renderPass?.setViewport(null);
this.postProcessingPass?.setViewport(null);
} else {
viewport = {
width: this.canvas.width,
height: this.canvas.height,
top: 0,
left: 0,
minDepth: 0,
maxDepth: 1,
...viewport
};
let { width, height, top, left, minDepth, maxDepth } = viewport;
width = Math.min(width, this.canvas.width);
height = Math.min(height, this.canvas.height);
top = Math.max(0, top);
left = Math.max(0, left);
this.viewport = {
width,
height,
top,
left,
minDepth,
maxDepth
};
this.renderPass?.setViewport(this.viewport);
this.postProcessingPass?.setViewport(this.viewport);
}
}
/**
* Set the renderer, {@link renderPass} and {@link postProcessingPass} {@link GPURenderer#scissorRect | scissorRect} values. Beware that if you use a {@link GPURenderer#scissorRect | scissorRect}, you should resize it yourself so it does not overflow the `canvas` in the `onResize` callback to avoid issues.
* @param scissorRect - {@link RectBBox} settings to use. Can be set to `null` to cancel the {@link GPURenderer#scissorRect | scissorRect}.
*/
setScissorRect(scissorRect = null) {
if (!scissorRect) {
this.scissorRect = null;
this.renderPass?.setScissorRect(null);
this.postProcessingPass?.setScissorRect(null);
} else {
scissorRect = {
width: this.canvas.width,
height: this.canvas.height,
top: 0,
left: 0,
...scissorRect
};
let { width, height, top, left } = scissorRect;
width = Math.min(width, this.canvas.width);
height = Math.min(height, this.canvas.height);
top = Math.max(0, top);
left = Math.max(0, left);
this.scissorRect = {
width,
height,
top,
left
};
this.renderPass?.setScissorRect(this.scissorRect);
this.postProcessingPass?.setScissorRect(this.scissorRect);
}
}
/**
* Set the renderer {@link GPURenderer.pixelRatio | pixel ratio} and {@link resize} it.
* @param pixelRatio - New pixel ratio to use.
*/
setPixelRatio(pixelRatio = 1) {
this.pixelRatio = pixelRatio;
this.resize(this.rectBBox);
}
/**
* Resize our {@link GPURenderer}.
* @param rectBBox - The optional new {@link canvas} {@link RectBBox} to set.
*/
resize(rectBBox = null) {
this.setSize(rectBBox);
this._onResizeCallback && this._onResizeCallback();
this.resizeObjects();
this._onAfterResizeCallback && this._onAfterResizeCallback();
}
/**
* Resize all tracked objects ({@link Texture | textures}, {@link RenderPass | render passes}, {@link RenderTarget | render targets}, {@link ComputePass | compute passes} and meshes).
*/
resizeObjects() {
this.renderBundles.forEach((renderBundle) => renderBundle.resize());
this.textures.forEach((texture) => {
texture.resize();
});
this.renderPass?.resize();
this.postProcessingPass?.resize();
this.renderTargets.forEach((renderTarget) => renderTarget.resize());
this.computePasses.forEach((computePass) => computePass.resize());
this.pingPongPlanes.forEach((pingPongPlane) => pingPongPlane.resize(this.boundingRect));
this.shaderPasses.forEach((shaderPass) => shaderPass.resize(this.boundingRect));
this.resizeMeshes();
}
/**
* Resize the {@link meshes}.
*/
resizeMeshes() {
this.meshes.forEach((mesh) => {
mesh.resize(this.boundingRect);
});
}
/**
* Get our {@link domElement | DOM Element} {@link DOMElement#boundingRect | bounding rectangle}. If there's no {@link domElement | DOM Element} (like when using an offscreen canvas for example), the {@link rectBBox} values are used.
*/
get boundingRect() {
if (!!this.domElement && !!this.domElement.boundingRect) return this.domElement.boundingRect;
else if (!!this.domElement) {
const boundingRect = this.domElement.element?.getBoundingClientRect();
return {
top: boundingRect.top,
right: boundingRect.right,
bottom: boundingRect.bottom,
left: boundingRect.left,
width: boundingRect.width,
height: boundingRect.height,
x: boundingRect.x,
y: boundingRect.y
};
} else return {
top: this.rectBBox.top,
right: this.rectBBox.left + this.rectBBox.width,
bottom: this.rectBBox.top + this.rectBBox.height,
left: this.rectBBox.left,
width: this.rectBBox.width,
height: this.rectBBox.height,
x: this.rectBBox.left,
y: this.rectBBox.top
};
}
/**
* Clamp to max WebGPU texture dimensions.
* @param dimension - Width and height dimensions to clamp.
*/
clampToMaxDimension(dimension) {
if (this.device) {
dimension.width = Math.min(this.device.limits.maxTextureDimension2D, dimension.width);
dimension.height = Math.min(this.device.limits.maxTextureDimension2D, dimension.height);
}
}
/**
* Get our {@link GPUDeviceManager#device | device}.
* @readonly
*/
get device() {
return this.deviceManager.device;
}
/**
* Get whether our {@link GPUDeviceManager} is ready (i.e. its {@link GPUDeviceManager#adapter | adapter} and {@link GPUDeviceManager#device | device} are set) its {@link context} is set and its size is set.
* @readonly
*/
get ready() {
return this.deviceManager.ready && !!this.context && !!this.boundingRect.width && !!this.boundingRect.height;
}
/**
* Get our {@link core/renderers/GPUDeviceManager.GPUDeviceManagerBaseParams#production | GPUDeviceManager production flag}.
* @readonly
*/
get production() {
return this.deviceManager.options.production;
}
/**
* Get all the created {@link GPUDeviceManager#samplers | samplers}.
* @readonly
*/
get samplers() {
return this.deviceManager.samplers;
}
/**
* Get all the created {@link GPUDeviceManager#buffers | GPU buffers}.
* @readonly
*/
get buffers() {
return this.deviceManager.buffers;
}
/**
* Get all the created {@link GPUDeviceManager#indirectBuffers | indirect buffers}.
* @readonly
*/
get indirectBuffers() {
return this.deviceManager.indirectBuffers;
}
/**
* Get the {@link GPUDeviceManager#pipelineManager | pipeline manager}.
* @readonly
*/
get pipelineManager() {
return this.deviceManager.pipelineManager;
}
/**
* Get all the rendered objects (i.e. compute passes, meshes, ping pong planes and shader passes) created by the {@link GPUDeviceManager}.
* @readonly
*/
get deviceRenderedObjects() {
return this.deviceManager.deviceRenderedObjects;
}
/**
* Configure our {@link context} with the given options.
*/
configureContext() {
this.context.configure({
device: this.device,
...this.options.context,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST
});
}
/**
* Set our {@link context} if possible and initialize the {@link renderPass} and {@link postProcessingPass}.
*/
setContext() {
this.context = this.canvas.getContext("webgpu");
if (this.device) {
try {
this.configureContext();
} catch (e) {
const preferredFormat = this.deviceManager.gpu.getPreferredCanvasFormat();
if (this.options.context.format !== preferredFormat) {
this.options.context.format = preferredFormat;
if (this.renderPass && this.renderPass.options.colorAttachments?.length) this.renderPass.options.colorAttachments[0].targetFormat = preferredFormat;
this.configureContext();
} else {
this.context = null;
console.error(e);
}
}
this.textures.forEach((texture) => {
if (!texture.texture) texture.createTexture();
});
this.renderPasses.forEach((renderPass) => renderPass.init());
}
}
/**
* Called when the {@link GPUDeviceManager#device | device} is lost.
* Force all our scene objects to lose context.
*/
loseContext() {
this.renderBundles.forEach((bundle) => bundle.loseContext());
this.renderedObjects.forEach((sceneObject) => sceneObject.loseContext());
}
/**
* Called when the {@link GPUDeviceManager#device | device} should be restored.
* Configure the context again, resize the {@link RenderTarget | render targets} and {@link Texture | textures}, restore our {@link renderedObjects | rendered objects} context.
*/
restoreContext() {
this.configureContext();
this.textures.forEach((texture) => {
texture.createTexture();
});
this.renderPass?.resize();
this.postProcessingPass?.resize();
this.renderTargets.forEach((renderTarget) => renderTarget.resize());
this.renderedObjects.forEach((sceneObject) => sceneObject.restoreContext());
this.environmentMaps.forEach((environmentMap) => {
environmentMap.computeBRDFLUTTexture();
environmentMap.computeFromHDR();
});
}
/**
* Set our {@link renderPass | main render pass} that will be used to render the result of our draw commands back to the screen and our {@link postProcessingPass | postprocessing pass} that will be used for any additional postprocessing render passes.
*/
setMainRenderPasses() {
this.renderPass = new RenderPass(this, {
label: this.options.label + " render pass",
...this.options.renderPass
});
this.scene.setMainRenderPassEntry();
this.postProcessingPass = new RenderPass(this, {
label: this.options.label + " post processing render pass",
useDepth: false,
sampleCount: 1
});
}
/**
* Set our {@link scene}.
*/
setScene() {
this.scene = new Scene({ renderer: this });
}
/**
* Create a {@link !GPUBuffer}.
* @param buffer - {@link Buffer} to use for buffer creation.
* @returns - newly created {@link !GPUBuffer}.
*/
createBuffer(buffer) {
const GPUBuffer = this.deviceManager.device?.createBuffer(buffer.options);
this.deviceManager.addBuffer(buffer);
return GPUBuffer;
}
/**
* Remove a {@link Buffer} from our {@link GPUDeviceManager#buffers | buffers Map}.
* @param buffer - {@link Buffer} to remove.
*/
removeBuffer(buffer) {
this.deviceManager.removeBuffer(buffer);
}
/**
* Write to a {@link GPUBuffer}.
* @param buffer - {@link GPUBuffer} to write to.
* @param bufferOffset - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUQueue/writeBuffer#bufferoffset | Buffer offset}.
* @param data - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUQueue/writeBuffer#data | Data} to write.
*/
queueWriteBuffer(buffer, bufferOffset, data) {
this.deviceManager.device?.queue.writeBuffer(buffer, bufferOffset, data);
}
/**
* Copy a source {@link Buffer#GPUBuffer | Buffer GPUBuffer} into a destination {@link Buffer#GPUBuffer | Buffer GPUBuffer}.
* @param parameters - Parameters used to realize the copy.
* @param parameters.srcBuffer - Source {@link Buffer}.
* @param [parameters.dstBuffer] - Destination {@link Buffer}. Will create a new one if none provided.
* @param [parameters.commandEncoder] - {@link GPUCommandEncoder} to use for the copy. Will create a new one and submit the command buffer if none provided.
* @returns - Destination {@link Buffer} after copy.
*/
copyBufferToBuffer({ srcBuffer, dstBuffer, commandEncoder }) {
if (!srcBuffer || !srcBuffer.GPUBuffer) {
throwWarning(`${this.options.label} (${this.type}): cannot copy to buffer because the source buffer has not been provided`);
return null;
}
if (!dstBuffer) dstBuffer = new Buffer();
if (!dstBuffer.GPUBuffer) dstBuffer.createBuffer(this, {
label: `GPURenderer (${this.options.label}): destination copy buffer from: ${srcBuffer.options.label}`,
size: srcBuffer.GPUBuffer.size,
usage: ["copyDst", "mapRead"]
});
if (srcBuffer.GPUBuffer.mapState !== "unmapped") {
throwWarning(`${this.options.label} (${this.type}): Cannot copy from ${srcBuffer.GPUBuffer} because it is currently mapped`);
return;
}
if (dstBuffer.GPUBuffer.mapState !== "unmapped") {
throwWarning(`${this.options.label} (${this.type}): Cannot copy from ${dstBuffer.GPUBuffer} because it is currently mapped`);
return;
}
const hasCommandEncoder = !!commandEncoder;
if (!hasCommandEncoder) {
commandEncoder = this.deviceManager.device?.createCommandEncoder({ label: `${this.type} (${this.options.label}): Copy buffer command encoder` });
!this.production && commandEncoder.pushDebugGroup(`${this.type} (${this.options.label}): Copy buffer command encoder`);
}
commandEncoder.copyBufferToBuffer(srcBuffer.GPUBuffer, 0, dstBuffer.GPUBuffer, 0, dstBuffer.GPUBuffer.size);
if (!hasCommandEncoder) {
!this.production && commandEncoder.popDebugGroup();
const commandBuffer = commandEncoder.finish();
this.deviceManager.device?.queue.submit([commandBuffer]);
}
return dstBuffer;
}
/**
* Get all created {@link AllowedBindGroups | bind group} tracked by our {@link GPUDeviceManager}.
* @readonly
*/
get bindGroups() {
return this.deviceManager.bindGroups;
}
/**
* Add a {@link AllowedBindGroups | bind group} to our {@link GPUDeviceManager#bindGroups | bind groups array}.
* @param bindGroup - {@link AllowedBindGroups | Bind group} to add.
*/
addBindGroup(bindGroup) {
this.deviceManager.addBindGroup(bindGroup);
}
/**
* Remove a {@link AllowedBindGroups | bind group} from our {@link GPUDeviceManager#bindGroups | bind groups array}.
* @param bindGroup - {@link AllowedBindGroups | Bind group} to remove.
*/
removeBindGroup(bindGroup) {
this.deviceManager.removeBindGroup(bindGroup);
}
/**
* Create a {@link GPUBindGroupLayout}.
* @param bindGroupLayoutDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createBindGroupLayout#descriptor | GPUBindGroupLayoutDescriptor}.
* @returns - Newly created {@link GPUBindGroupLayout}.
*/
createBindGroupLayout(bindGroupLayoutDescriptor) {
return this.deviceManager.device?.createBindGroupLayout(bindGroupLayoutDescriptor);
}
/**
* Create a {@link GPUBindGroup}.
* @param bindGroupDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createBindGroup#descriptor | GPUBindGroupDescriptor}.
* @returns - Newly created {@link GPUBindGroup}.
*/
createBindGroup(bindGroupDescriptor) {
return this.deviceManager.device?.createBindGroup(bindGroupDescriptor);
}
/**
* Create a {@link GPUShaderModule}.
* @param shaderModuleDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createShaderModule#descriptor | GPUShaderModuleDescriptor}
* @returns - Newly created {@link GPUShaderModule}.
*/
createShaderModule(shaderModuleDescriptor) {
return this.device?.createShaderModule(shaderModuleDescriptor);
}
/**
* Create a {@link GPUPipelineLayout}.
* @param pipelineLayoutDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createPipelineLayout#descriptor | GPUPipelineLayoutDescriptor}.
* @returns - Newly created {@link GPUPipelineLayout}.
*/
createPipelineLayout(pipelineLayoutDescriptor) {
return this.device?.createPipelineLayout(pipelineLayoutDescriptor);
}
/**
* Create a {@link GPURenderPipeline}.
* @param pipelineDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createRenderPipeline#descriptor | GPURenderPipelineDescriptor}.
* @returns - Newly created {@link GPURenderPipeline}.
*/
createRenderPipeline(pipelineDescriptor) {
return this.device?.createRenderPipeline(pipelineDescriptor);
}
/**
* Asynchronously create a {@link GPURenderPipeline}.
* @param pipelineDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createRenderPipeline#descriptor | GPURenderPipelineDescriptor}.
* @returns - Newly created {@link GPURenderPipeline}.
*/
async createRenderPipelineAsync(pipelineDescriptor) {
return await this.device?.createRenderPipelineAsync(pipelineDescriptor);
}
/**
* Create a {@link GPUComputePipeline}.
* @param pipelineDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createComputePipeline#descriptor | GPUComputePipelineDescriptor}.
* @returns - Newly created {@link GPUComputePipeline}.
*/
createComputePipeline(pipelineDescriptor) {
return this.device?.createComputePipeline(pipelineDescriptor);
}
/**
* Asynchronously create a {@link GPUComputePipeline}.
* @param pipelineDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createComputePipeline#descriptor | GPUComputePipelineDescriptor}.
* @returns - Newly created {@link GPUComputePipeline}.
*/
async createComputePipelineAsync(pipelineDescriptor) {
return await this.device?.createComputePipelineAsync(pipelineDescriptor);
}
/**
* Add a {@link Texture} to our {@link textures} array.
* @param texture - {@link Texture} to add.
*/
addTexture(texture) {
this.textures.push(texture);
}
/**
* Remove a {@link Texture} from our {@link textures} array.
* @param texture - {@link Texture} to remove.
*/
removeTexture(texture) {
this.textures = this.textures.filter((t) => t.uuid !== texture.uuid);
}
/**
* Create a {@link GPUTexture}.
* @param textureDescriptor - {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createTexture#descriptor | GPUTextureDescriptor}.
* @returns - Newly created {@link GPUTexture}.
*/
createTexture(textureDescriptor) {
return this.deviceManager.device?.createTexture(textureDescriptor);
}
/**
* Upload a {@link MediaTexture#texture | texture} or {@link DOMTexture#texture | texture} to the GPU.
* @param texture - {@link MediaTexture} or {@link DOMTexture} containing the {@link GPUTexture} to upload.
* @param sourceIndex - Index of the source to upload (for cube maps). Default to `0`.
*/
uploadTexture(texture, sourceIndex = 0) {
this.deviceManager.uploadTexture(texture, sourceIndex);
}
/**
* Generate mips on the GPU using our {@link GPUDeviceManager}.
* @param texture - {@link Texture}, {@link MediaTexture} or {@link DOMTexture} for which to generate the mips.
* @param commandEncoder - optional {@link GPUCommandEncoder} to use if we're already in the middle of a command encoding process.
*/
generateMips(texture, commandEncoder = null) {
this.deviceManager.generateMips(texture, commandEncoder);
}
/**
* Import a {@link GPUExternalTexture}.
* @param source - {@link HTMLVideoElement} or {@link VideoFrame} source.
* @param label - Optional label of the texture.
* @returns - {@link GPUExternalTexture}.
*/
importExternalTexture(source, label = "") {
return this.deviceManager.device?.importExternalTexture({
label,
source
});
}
/**
* Copy a {@link GPUTexture} to a {@link Texture} using a {@link GPUCommandEncoder}. Automatically generate mips after copy if the {@link Texture} needs it.
* @param gpuTexture - {@link GPUTexture} source to copy from.
* @param texture - {@link Texture} destination to copy onto.
* @param commandEncoder - {@link GPUCommandEncoder} to use for copy operation.
*/
copyGPUTextureToTexture(gpuTexture, texture, commandEncoder) {
if (gpuTexture.width !== texture.texture.width || gpuTexture.height !== texture.texture.height || gpuTexture.depthOrArrayLayers !== texture.texture.depthOrArrayLayers) return;
commandEncoder.copyTextureToTexture({ texture: gpuTexture }, { texture: texture.texture }, [
gpuTexture.width,
gpuTexture.height,
gpuTexture.depthOrArrayLayers
]);
if (texture.options.generateMips) this.generateMips(texture, commandEncoder);
}
/**
* Copy a {@link Texture} to a {@link Texture} using a {@link GPUCommandEncoder}. Automatically generate mips after copy if the destination {@link Texture} needs it.
* @param texture1 - {@link Texture} source to copy from.
* @param texture2 - {@link Texture} destination to copy onto.
* @param commandEncoder - {@link GPUCommandEncoder} to use for copy operation.
*/
copyTextureToTexture(texture1, texture2, commandEncoder) {
this.copyGPUTextureToTexture(texture1.texture, texture2, commandEncoder);
}
/**
* Copy a {@link Texture} to a {@link GPUTexture} using a {@link GPUCommandEncoder}.
* @param texture - {@link Texture} source to copy from.
* @param gpuTexture - {@link GPUTexture} destination to copy onto.
* @param commandEncoder - {@link GPUCommandEncoder} to use for copy operation.
*/
copyTextureToGPUTexture(texture, gpuTexture, commandEncoder) {
if (gpuTexture.width !== texture.texture.width || gpuTexture.height !== texture.texture.height || gpuTexture.depthOrArrayLayers !== texture.texture.depthOrArrayLayers) return;
commandEncoder.copyTextureToTexture({ texture: texture.texture }, { texture: gpuTexture }, [
gpuTexture.width,
gpuTexture.height,
gpuTexture.depthOrArrayLayers
]);
}
/**
* Check if a {@link Sampler} has already been created with the same {@link Sampler#options | parameters}.
* Use it if found, else create a new one and add it to the {@link GPUDeviceManager#samplers | samplers array}.
* @param sampler - {@link Sampler} to create.
* @returns - {@link GPUSampler} from cache or newly created {@link GPUSampler}.
*/
createSampler(sampler) {
const existingSampler = this.samplers.find((existingSampler) => {
return JSON.stringify(existingSampler.options) === JSON.stringify(sampler.options) && existingSampler.sampler;
});
if (existingSampler) return existingSampler.sampler;
else {
const { type, ...samplerOptions } = sampler.options;
const gpuSampler = this.deviceManager.device?.createSampler({
label: sampler.label,
...samplerOptions
});
this.deviceManager.addSampler(sampler);
return gpuSampler;
}
}
/**
* Remove a {@link Sampler} from our {@link GPUDeviceManager#samplers | samplers array}.
* @param sampler - {@link Sampler} to remove.
*/
removeSampler(sampler) {
this.deviceManager.removeSampler(sampler);
}
/**
* Set different tasks queue managers to execute callbacks at different phases of our render call:
* - {@link onBeforeCommandEncoderCreation}: callbacks executed before the creation of the command encoder.
* - {@link onBeforeRenderScene}: callbacks executed after the creation of the command encoder and before rendering the {@link Scene}.
* - {@link onAfterRenderScene}: callbacks executed after the creation of the command encoder and after rendering the {@link Scene}.
* - {@link onAfterCommandEncoderSubmission}: callbacks executed after the submission of the command encoder.
*/
setTasksQueues() {
this.onBeforeCommandEncoderCreation = new TasksQueueManager();
this.onBeforeRenderScene = new TasksQueueManager();
this.onAfterRenderScene = new TasksQueueManager();
this.onAfterCommandEncoderSubmission = new TasksQueueManager();
}
/**
* Set all objects arrays and {@link Map} that we'll keep track of.
*/
setRendererObjects() {
this.computePasses = [];
this.pingPongPlanes = [];
this.shaderPasses = [];
this.renderPasses = /* @__PURE__ */ new Map();
this.renderTargets = [];
this.meshes = [];
this.textures = [];
this.environmentMaps = /* @__PURE__ */ new Map();
this.renderBundles = /* @__PURE__ */ new Map();
this.animations = /* @__PURE__ */ new Map();
}
/**
* Get all this {@link GPURenderer} rendered objects (i.e. compute passes, meshes, ping pong planes and shader passes).
* @readonly
*/
get renderedObjects() {
return [
...this.computePasses,
...this.meshes,
...this.shaderPasses,
...this.pingPongPlanes
];
}
/**
* Get all objects ({@link RenderedMesh | rendered meshes} or {@link ComputePass | compute passes}) using a given {@link AllowedBindGroups | bind group}.
* Useful (but slow) to know if a resource is used by multiple objects and if it is safe to destroy it or not.
* @param bindGroup - {@link AllowedBindGroups | Bind group} to check.
*/
getObjectsByBindGroup(bindGroup) {
return this.deviceRenderedObjects.filter((object) => {
return [
...object.material.bindGroups,
...object.material.inputsBindGroups,
...object.material.clonedBindGroups
].some((bG) => bG.uuid === bindGroup.uuid);
});
}
/**
* Get all objects ({@link RenderedMesh | rendered meshes} or {@link ComputePass | compute passes}) using a given {@link DOMTexture}, {@link MediaTexture} or {@link Texture}.
* Useful to know if a resource is used by multiple objects and if it is safe to destroy it or not.
* @param texture - {@link DOMTexture}, {@link MediaTexture} or {@link Texture} to check.
*/
getObjectsByTexture(texture) {
return this.deviceRenderedObjects.filter((object) => {
return object.material.textures.some((t) => t.uuid === texture.uuid);
});
}
/**
* Assign a callback function to _onBeforeRenderCallback.
* @param callback - callback to run just before the {@link render} method will be executed.
* @returns - Our renderer.
*/
onBeforeRender(callback) {
if (callback) this._onBeforeRenderCallback = callback;
return this;
}
/**
* Assign a callback function to _onAfterRenderCallback.
* @param callback - callback to run just after the {@link render} method has been executed.
* @returns - Our renderer.
*/
onAfterRender(callback) {
if (callback) this._onAfterRenderCallback = callback;
return this;
}
/**
* Callback to run after the {@link GPURenderer} has been resized but before the {@link resizeObjects} method has been executed (before the {@link Texture | textures}, {@link RenderPass | render passes}, {@link RenderTarget | render targets}, {@link ComputePass | compute passes} and meshes are resized).
* @param callback - callback to execute.
* @returns - Our renderer.
*/
onResize(callback) {
if (callback) this._onResizeCallback = callback;
return this;
}
/**
* Callback to run after the {@link GPURenderer} has been resized and after the {@link resizeObjects} method has been executed (after the {@link Texture | textures}, {@link RenderPass | render passes}, {@link RenderTarget | render targets}, {@link ComputePass | compute passes} and meshes have been resized).
* @param callback - callback to execute.
* @returns - Our renderer.
*/
onAfterResize(callback) {
if (callback) this._onAfterResizeCallback = callback;
return this;
}
/**
* Render a single {@link ComputePass}.
* @param commandEncoder - current {@link GPUCommandEncoder} to use.
* @param computePass - {@link ComputePass} to run.
* @param copyBuffer - Whether to copy all writable binding buffers that need it.
*/
renderSingleComputePass(commandEncoder, computePass, copyBuffer = true) {
const pass = commandEncoder.beginComputePass();
computePass.render(pass);
pass.end();
if (copyBuffer) computePass.copyBufferToResult(commandEncoder);
}
/**
* Render a single {@link RenderedMesh | Mesh}.
* @param commandEncoder - current {@link GPUCommandEncoder}.
* @param mesh - {@link RenderedMesh | Mesh} to render.
*/
renderSingleMesh(commandEncoder, mesh) {
const pass = commandEncoder.beginRenderPass(this.renderPass.descriptor);
mesh.render(pass);
pass.end();
}
/**
* Render an array of objects (either {@link RenderedMesh | Meshes} or {@link ComputePass}) once. This method won't call any of the renderer render hooks like {@link onBeforeRender}, {@link onAfterRender}.
* @param objects - Array of {@link RenderedMesh | Meshes} or {@link ComputePass} to render.
*/
renderOnce(objects) {
const commandEncoder = this.device?.createCommandEncoder({ label: "Render once command encoder" });
!this.production && commandEncoder.pushDebugGroup("Render once command encoder");
this.pipelineManager.resetCurrentPipeline();
objects.forEach((object) => {
if (object.type === "ComputePass") this.renderSingleComputePass(commandEncoder, object);
else this.renderSingleMesh(commandEncoder, object);
});
!this.production && commandEncoder.popDebugGroup();
const commandBuffer = commandEncoder.finish();
this.device?.queue.submit([commandBuffer]);
this.pipelineManager.resetCurrentPipeline();
}
/**
* Force to clear a {@link GPURenderer} content to its {@link RenderPass#options.clearValue | clear value} by rendering and empty pass.
* @param commandEncoder - {@link GPUCommandEncoder} to use if any.
* @param renderPass - {@link RenderPass} to clear. Default to {@link GPURenderer#renderPass | renderPass}.
*/
forceClear(commandEncoder, renderPass = this.renderPass) {
const hasCommandEncoder = !!commandEncoder;
if (!hasCommandEncoder) {
commandEncoder = this.device?.createCommandEncoder({ label: `${this.type} (${this.options.label}): Force clear command encoder` });
!this.production && commandEncoder.pushDebugGroup(`${this.type} (${this.options.label}): Force clear command encoder`);
}
renderPass.updateView();
renderPass.setDepthReadOnly(false);
renderPass.setLoadOp("clear");
renderPass.setDepthLoadOp("clear");
commandEncoder.beginRenderPass(renderPass.descriptor).end();
if (!hasCommandEncoder) {
!this.production && commandEncoder.popDebugGroup();
const commandBuffer = commandEncoder.finish();
this.device?.queue.submit([commandBuffer]);
}
}
/**
* Called by the {@link GPUDeviceManager#render | GPUDeviceManager render method} before the {@link GPUCommandEncoder} has been created. Used to update the {@link Scene} matrix stack.
*/
onBeforeCommandEncoder() {
if (!this.ready) return;
if (this.shouldRenderScene) this.scene?.onBeforeRender();
this.onBeforeCommandEncoderCreation.execute();
}
/**
* Called by the {@link GPUDeviceManager#render | GPUDeviceManager render method} after the {@link GPUCommandEncoder} has been created.
*/
onAfterCommandEncoder() {
if (!this.ready) return;
this.textures.forEach((texture) => {
if (texture instanceof MediaTexture) texture.closeVideoFrame();
});
this.onAfterCommandEncoderSubmission.execute();
}
/**
* Called at each draw call to render our scene and its content.
* @param commandEncoder - current {@link GPUCommandEncoder}.
*/
render(commandEncoder) {
if (!this.ready || !this.shouldRender) return;
this._onBeforeRenderCallback && this._onBeforeRenderCallback(commandEncoder);
this.onBeforeRenderScene.execute(commandEncoder);
if (this.shouldRenderScene) {
this.textures.forEach((texture) => {
if (texture instanceof MediaTexture) texture.update();
});
this.scene?.render(commandEncoder);
}
this._onAfterRenderCallback && this._onAfterRenderCallback(commandEncoder);
this.onAfterRenderScene.execute(commandEncoder);
}
/**
* Destroy our {@link GPURenderer} and everything that needs to be destroyed as well.
*/
destroy() {
this.deviceManager.renderers = this.deviceManager.renderers.filter((renderer) => renderer.uuid !== this.uuid);
this.domElement?.destroy();
this.renderBundles.forEach((bundle) => bundle.destroy());
this.animations = /* @__PURE__ */ new Map();
this.renderPass?.destroy();
this.postProcessingPass?.destroy();
this.renderTargets.forEach((renderTarget) => renderTarget.destroy());
this.renderedObjects.forEach((sceneObject) => sceneObject.remove());
this.textures.forEach((texture) => texture.destroy());
this.context?.unconfigure();
}
};
//#endregion
export { GPURenderer };