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.
405 lines (404 loc) • 20.2 kB
JavaScript
import { generateUUID } from "../../utils/utils.mjs";
import { isRenderer } from "../renderers/utils.mjs";
import { Texture } from "../textures/Texture.mjs";
//#region src/core/renderPasses/RenderPass.ts
/**
* Used by {@link core/renderPasses/RenderTarget.RenderTarget | RenderTarget} and the {@link Renderer} to render to one or multiple {@link RenderPass#viewTextures | view textures} (and optionally a {@link RenderPass#depthTexture | depth texture}), using a specific {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#descriptor | GPURenderPassDescriptor}.
*/
var RenderPass = class {
/** Whether the {@link RenderPass} should handle stencil. Default to `false`, eventually set to `true` based on the {@link depthTexture} format. */
#useStencil;
/**
* RenderPass constructor
* @param renderer - {@link Renderer} object or {@link GPUCurtains} class object used to create this {@link RenderPass}
* @param parameters - {@link RenderPassParams | parameters} used to create this {@link RenderPass}.
*/
constructor(renderer, { label = "Render Pass", sampleCount = 4, qualityRatio = 1, fixedSize = null, useColorAttachments = true, renderToSwapChain = true, colorAttachments = [], useDepth = true, depthTexture = null, forceDepthLoadOp = null, forceDepthStoreOp = null, depthClearValue = 1, depthFormat = "depth24plus", forceDepthReadOnly = null, stencilClearValue = 0, forceStencilLoadOp = null, forceStencilStoreOp = null, forceStencilReadOnly = null } = {}) {
this.type = "RenderPass";
renderer = isRenderer(renderer, label + " " + this.type);
this.renderer = renderer;
this.uuid = generateUUID();
this.viewport = null;
this.scissorRect = null;
this.#useStencil = false;
if (useColorAttachments) {
const defaultColorAttachment = {
loadOp: "clear",
storeOp: "store",
clearValue: [
0,
0,
0,
0
],
targetFormat: this.renderer.options.context.format
};
if (!colorAttachments.length) colorAttachments = [defaultColorAttachment];
else colorAttachments = colorAttachments.map((colorAttachment) => {
return {
...defaultColorAttachment,
...colorAttachment
};
});
}
this.options = {
label,
sampleCount,
qualityRatio,
fixedSize,
useColorAttachments,
renderToSwapChain,
colorAttachments,
useDepth,
...depthTexture !== void 0 && { depthTexture },
depthLoadOp: forceDepthLoadOp ?? "clear",
forceDepthLoadOp,
depthStoreOp: forceDepthStoreOp ?? "store",
forceDepthStoreOp,
depthClearValue,
depthFormat,
depthReadOnly: forceDepthReadOnly === null ? false : forceDepthReadOnly,
forceDepthReadOnly,
stencilClearValue,
stencilLoadOp: forceStencilLoadOp ?? "clear",
forceStencilLoadOp,
stencilStoreOp: forceStencilStoreOp ?? "store",
forceStencilStoreOp,
stencilReadOnly: forceStencilReadOnly === null ? false : forceStencilReadOnly,
forceStencilReadOnly
};
this.renderer.renderPasses.set(this.uuid, this);
if (this.renderer.device) this.init();
}
/**
* Initialize the {@link RenderPass} textures and descriptor.
*/
init() {
if (this.options.useDepth) this.createDepthTexture();
this.viewTextures = [];
this.resolveTargets = [];
if (this.options.useColorAttachments && (!this.options.renderToSwapChain || this.options.sampleCount > 1)) {
this.createViewTextures();
this.createResolveTargets();
}
this.setRenderPassDescriptor();
}
/**
* Reset this {@link RenderPass} {@link RenderPass.renderer | renderer}.
* @param renderer - New {@link Renderer} or {@link GPUCurtains} instance to use.
*/
setRenderer(renderer) {
if (this.renderer) this.renderer.renderPasses.delete(this.uuid);
renderer = isRenderer(renderer, this.options.label + " " + this.type);
this.renderer = renderer;
if (this.options.useDepth && !this.options.depthTexture) this.depthTexture.setRenderer(this.renderer);
this.viewTextures.forEach((texture) => {
texture.setRenderer(this.renderer);
});
this.resolveTargets.forEach((texture) => {
if (texture) texture.setRenderer(this.renderer);
});
this.renderer.renderPasses.set(this.uuid, this);
}
/**
* Create and set our {@link depthTexture | depth texture}.
*/
createDepthTexture() {
if (this.options.depthTexture) {
this.depthTexture = this.options.depthTexture;
this.options.depthFormat = this.options.depthTexture.options.format;
} else this.depthTexture = new Texture(this.renderer, {
label: this.options.label + " depth texture",
name: "depthTexture",
format: this.options.depthFormat,
sampleCount: this.options.sampleCount,
qualityRatio: this.options.qualityRatio,
...this.options.fixedSize && { fixedSize: this.options.fixedSize },
type: "depth",
usage: ["renderAttachment", "textureBinding"]
});
if (this.depthTexture.options.format.includes("stencil")) this.#useStencil = true;
}
/**
* Create and set our {@link viewTextures | view textures}.
*/
createViewTextures() {
this.options.colorAttachments.forEach((colorAttachment, index) => {
this.viewTextures.push(new Texture(this.renderer, {
label: `${this.options.label} colorAttachment[${index}] view texture`,
name: `colorAttachment${index}ViewTexture`,
format: colorAttachment.targetFormat,
sampleCount: this.options.sampleCount,
qualityRatio: this.options.qualityRatio,
...this.options.fixedSize && { fixedSize: this.options.fixedSize },
type: "texture",
usage: [
"copySrc",
"copyDst",
"renderAttachment",
"textureBinding"
]
}));
});
}
/**
* Create and set our {@link resolveTargets | resolve targets} in case the {@link viewTextures} are multisampled.
*
* Note that if this {@link RenderPass} should {@link RenderPassParams#renderToSwapChain | render to the swap chain}, the first resolve target will be set to `null` as the current swap chain texture will be used anyway in the render loop (see {@link updateView}).
*/
createResolveTargets() {
if (this.options.sampleCount > 1) this.options.colorAttachments.forEach((colorAttachment, index) => {
this.resolveTargets.push(this.options.renderToSwapChain && index === 0 ? null : new Texture(this.renderer, {
label: `${this.options.label} resolve target[${index}] texture`,
name: `resolveTarget${index}Texture`,
format: colorAttachment.targetFormat,
sampleCount: 1,
qualityRatio: this.options.qualityRatio,
type: "texture"
}));
});
}
/**
* Get the textures outputted by this {@link RenderPass}, which means the {@link viewTextures} if not multisampled, or their {@link resolveTargets} else (beware that the first resolve target might be `null` if this {@link RenderPass} should {@link RenderPassParams#renderToSwapChain | render to the swap chain}).
* @readonly
*/
get outputTextures() {
return this.options.sampleCount > 1 ? this.resolveTargets : this.viewTextures;
}
/**
* Set our render pass {@link descriptor}.
*/
setRenderPassDescriptor(depthTextureView = null) {
this.descriptor = {
label: this.options.label + " descriptor",
colorAttachments: this.options.colorAttachments.map((colorAttachment, index) => {
return {
view: this.viewTextures[index]?.texture.createView({ label: this.viewTextures[index]?.texture.label + " view" }),
...this.resolveTargets.length && { resolveTarget: this.resolveTargets[index]?.texture.createView({ label: this.resolveTargets[index]?.texture.label + " view" }) },
clearValue: colorAttachment.clearValue,
loadOp: colorAttachment.loadOp,
storeOp: colorAttachment.storeOp,
...colorAttachment.depthSlice !== void 0 && { depthSlice: colorAttachment.depthSlice }
};
}),
...this.options.useDepth && { depthStencilAttachment: {
view: depthTextureView || this.depthTexture.texture.createView({ label: this.depthTexture.texture.label + " view" }),
...this.depthStencilAttachmentSettings
} }
};
}
/**
* Get the {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#depthstencil_attachment_object_structure | descriptor depthStencilAttachment} settings, except for the {@link depthTexture} view.
* @readonly
*/
get depthStencilAttachmentSettings() {
const depthReadOnly = !!this.options.depthReadOnly;
const stencilReadOnly = !!this.options.stencilReadOnly;
return {
depthClearValue: this.options.depthClearValue,
...!depthReadOnly && {
depthLoadOp: this.options.depthLoadOp,
depthStoreOp: this.options.depthStoreOp
},
depthReadOnly,
...this.#useStencil && {
...!stencilReadOnly && {
stencilLoadOp: this.options.stencilLoadOp,
stencilStoreOp: this.options.stencilStoreOp
},
stencilReadOnly
}
};
}
/**
* Update the {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#depthstencil_attachment_object_structure | descriptor depthStencilAttachment} settings, except for the {@link depthTexture} view.
* @private
*/
#updateDepthAttachmentSettings() {
if (this.options.useDepth && this.descriptor.depthStencilAttachment) this.descriptor.depthStencilAttachment = {
view: this.descriptor.depthStencilAttachment.view,
...this.depthStencilAttachmentSettings
};
}
/**
* Set the {@link viewport} to use if any.
* @param viewport - {@link RenderPassViewport} settings to use. Can be set to `null` to cancel the {@link viewport}.
*/
setViewport(viewport = null) {
this.viewport = viewport;
}
/**
* Set the {@link scissorRect} to use if any.
* @param scissorRect - {@link RectBBox} size to use for scissors. Can be set to `null` to cancel the {@link scissorRect}.
*/
setScissorRect(scissorRect = null) {
this.scissorRect = scissorRect;
}
/**
* Begin the {@link GPURenderPassEncoder} and eventually set the {@link viewport} and {@link scissorRect}.
* @param commandEncoder - {@link GPUCommandEncoder} to use.
* @param descriptor - Custom {@link https://gpuweb.github.io/types/interfaces/GPURenderPassDescriptor.html | GPURenderPassDescriptor} to use if any. Default to {@link RenderPass#descriptor | descriptor}.
* @returns - The created {@link GPURenderPassEncoder}.
*/
beginRenderPass(commandEncoder, descriptor = this.descriptor) {
const pass = commandEncoder.beginRenderPass(descriptor);
if (this.viewport) pass.setViewport(this.viewport.left, this.viewport.top, this.viewport.width, this.viewport.height, this.viewport.minDepth, this.viewport.maxDepth);
if (this.scissorRect) pass.setScissorRect(this.scissorRect.left, this.scissorRect.top, this.scissorRect.width, this.scissorRect.height);
return pass;
}
/**
* Update our {@link RenderPass} textures quality ratio.
* @param qualityRatio - New quality ratio to use.
*/
setQualityRatio(qualityRatio = 1) {
if (this.options.qualityRatio === qualityRatio) return;
this.options.qualityRatio = qualityRatio;
this.viewTextures.forEach((viewTexture) => {
viewTexture.setQualityRatio(this.options.qualityRatio);
});
this.resolveTargets.forEach((resolveTarget) => {
resolveTarget?.setQualityRatio(this.options.qualityRatio);
});
if (!this.options.depthTexture && this.options.useDepth) this.depthTexture.setQualityRatio(this.options.qualityRatio);
this.resize();
}
/**
* Resize our {@link RenderPass}: reset its {@link Texture}.
*/
resize() {
if (!this.renderer.device) return;
if (this.options.useDepth) this.descriptor.depthStencilAttachment.view = this.depthTexture.texture.createView({ label: this.depthTexture.options.label + " view" });
this.viewTextures.forEach((viewTexture, index) => {
this.descriptor.colorAttachments[index].view = viewTexture.texture.createView({ label: viewTexture.options.label + " view" });
});
this.resolveTargets.forEach((resolveTarget, index) => {
if (resolveTarget) this.descriptor.colorAttachments[index].resolveTarget = resolveTarget.texture.createView({ label: resolveTarget.options.label + " view" });
});
}
/**
* Set the {@link descriptor} {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#loadop | load operation}.
* @param loadOp - New {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#loadop | load operation} to use.
* @param colorAttachmentIndex - Index of the color attachment for which to use this load operation.
*/
setLoadOp(loadOp = "clear", colorAttachmentIndex = 0) {
if (this.options.useColorAttachments) {
if (this.options.colorAttachments[colorAttachmentIndex]) this.options.colorAttachments[colorAttachmentIndex].loadOp = loadOp;
if (this.descriptor) {
if (this.descriptor.colorAttachments && this.descriptor.colorAttachments[colorAttachmentIndex]) this.descriptor.colorAttachments[colorAttachmentIndex].loadOp = loadOp;
}
}
}
/**
* Set the {@link descriptor} {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#depthloadop | depth load operation} if {@link RenderPassOptions#forceDepthLoadOp | forceDepthLoadOp options} has not been defined.
* @param depthLoadOp - New {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#depthloadop | depth load operation} to use.
* @param force - Force the update of the `depthLoadOp` setting regardless of {@link RenderPassOptions#forceDepthLoadOp | forceDepthLoadOp option} value.
*/
setDepthLoadOp(depthLoadOp = "clear", force = false) {
if (this.options.forceDepthLoadOp && !force) return;
this.options.depthLoadOp = depthLoadOp;
this.#updateDepthAttachmentSettings();
}
/**
* Set the {@link descriptor} {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#depthstoreop | depth store operation} if {@link RenderPassOptions#forceDepthStoreOp | forceDepthStoreOp option} has not been defined.
* @param depthStoreOp - New {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#depthstoreop | depth store operation} to use.
* @param force - Force the update of the `depthStoreOp` setting regardless of {@link RenderPassOptions#forceDepthStoreOp | forceDepthStoreOp option} value.
*/
setDepthStoreOp(depthStoreOp = "store", force = false) {
if (this.options.forceDepthStoreOp && !force) return;
this.options.depthStoreOp = depthStoreOp;
this.#updateDepthAttachmentSettings();
}
/**
* Set the new {@link RenderPassOptions.depthReadOnly | depthReadOnly} setting if {@link RenderPassOptions#forceDepthReadOnly | forceDepthReadOnly options} has not been defined.
* @param value - Whether the depth buffer should be read-only or not.
* @param force - Force the update of the `depthReadOnly` setting regardless of {@link RenderPassOptions#forceDepthReadOnly | forceDepthReadOnly option} value.
*/
setDepthReadOnly(value, force = false) {
if (this.options.forceDepthReadOnly !== null && !force) return;
this.options.depthReadOnly = value;
this.#updateDepthAttachmentSettings();
}
/**
* Set the {@link descriptor} {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#stencilloadop | stencil load operation} if {@link RenderPassOptions#forceStencilLoadOp | forceStencilLoadOp options} has not been defined.
* @param stencilLoadOp - New {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#stencilloadop | stencil load operation} to use.
* @param force - Force the update of the `stencilLoadOp` setting regardless of {@link RenderPassOptions#forceStencilLoadOp | forceStencilLoadOp option} value.
*/
setStencilLoadOp(stencilLoadOp = "clear", force = false) {
if (this.options.forceStencilLoadOp && !force) return;
this.options.stencilLoadOp = stencilLoadOp;
this.#updateDepthAttachmentSettings();
}
/**
* Set the {@link descriptor} {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#stencilstoreop | stencil store operation} if {@link RenderPassOptions#forceStencilStoreOp | forceStencilStoreOp options} has not been defined.
* @param stencilStoreOp - New {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#stencilstoreop | stencil store operation} to use.
* @param force - Force the update of the `stencilStoreOp` setting regardless of {@link RenderPassOptions#forceStencilStoreOp | forceStencilStoreOp option} value.
*/
setStencilStoreOp(stencilStoreOp = "store", force = false) {
if (this.options.forceStencilStoreOp && !force) return;
this.options.stencilStoreOp = stencilStoreOp;
this.#updateDepthAttachmentSettings();
}
/**
* Set the new {@link RenderPassOptions.stencilReadOnly | stencilReadOnly} setting if {@link RenderPassOptions#forceStencilReadOnly | forceStencilReadOnly options} has not been defined.
* @param value - Whether the stencil buffer should be read-only or not.
* @param force - Force the update of the `stencilReadOnly` setting regardless of {@link RenderPassOptions#forceStencilReadOnly | forceStencilReadOnly option} value.
*/
setStencilReadOnly(value, force = false) {
if (this.options.forceStencilReadOnly !== null && !force) return;
this.options.stencilReadOnly = value;
this.#updateDepthAttachmentSettings();
}
/**
* Set our {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#clearvalue | clear colors value}.<br>
* Beware that if the {@link renderer} is using {@link core/renderers/GPURenderer.GPURendererContextOptions#alphaMode | premultiplied alpha mode}, your `R`, `G` and `B` channels should be premultiplied by your alpha channel.
* @param clearValue - new {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#clearvalue | clear colors value} to use.
* @param colorAttachmentIndex - index of the color attachment for which to use this clear value.
*/
setClearValue(clearValue = [
0,
0,
0,
0
], colorAttachmentIndex = 0) {
if (this.options.useColorAttachments) {
if (this.renderer.options.context.alphaMode === "premultiplied") {
const alpha = clearValue[3];
clearValue[0] = Math.min(clearValue[0], alpha);
clearValue[1] = Math.min(clearValue[1], alpha);
clearValue[2] = Math.min(clearValue[2], alpha);
}
if (this.options.colorAttachments[colorAttachmentIndex]) this.options.colorAttachments[colorAttachmentIndex].clearValue = clearValue;
if (this.descriptor) {
if (this.descriptor.colorAttachments && this.descriptor.colorAttachments[colorAttachmentIndex]) this.descriptor.colorAttachments[colorAttachmentIndex].clearValue = clearValue;
}
}
}
/**
* Set the current {@link descriptor} texture {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUCommandEncoder/beginRenderPass#view | view} and {@link GPUCommandEncoder.beginRenderPass().resolveTarget | resolveTarget} (depending on whether we're using multisampling).
* @param renderTexture - {@link GPUTexture} to use, or the {@link core/renderers/GPURenderer.GPURenderer#context | context} {@link GPUTexture | current texture} if null.
* @returns - the {@link GPUTexture | texture} to render to.
*/
updateView(renderTexture = null) {
if (!this.options.colorAttachments.length || !this.options.renderToSwapChain) return renderTexture;
if (!renderTexture) {
renderTexture = this.renderer.context.getCurrentTexture();
renderTexture.label = `${this.renderer.type} context current texture`;
}
if (this.options.sampleCount > 1) {
this.descriptor.colorAttachments[0].view = this.viewTextures[0].texture.createView({ label: this.viewTextures[0].options.label + " view" });
this.descriptor.colorAttachments[0].resolveTarget = renderTexture.createView({ label: renderTexture.label + " resolve target view" });
} else this.descriptor.colorAttachments[0].view = renderTexture.createView({ label: renderTexture.label + " view" });
return renderTexture;
}
/**
* Destroy our {@link RenderPass}.
*/
destroy() {
this.viewTextures.forEach((viewTexture) => viewTexture.destroy());
this.resolveTargets.forEach((resolveTarget) => resolveTarget?.destroy());
if (!this.options.depthTexture && this.depthTexture) this.depthTexture.destroy();
this.renderer.renderPasses.delete(this.uuid);
}
};
//#endregion
export { RenderPass };