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.
374 lines (373 loc) • 13.8 kB
JavaScript
import { generateUUID } from "../../utils/utils.mjs";
import { isRenderer } from "../renderers/utils.mjs";
import { Texture } from "../textures/Texture.mjs";
import { MediaTexture } from "../textures/MediaTexture.mjs";
import { ComputeMaterial } from "../materials/ComputeMaterial.mjs";
//#region src/core/computePasses/ComputePass.ts
let computePassIndex = 0;
/**
* Used to create a {@link ComputePass}, i.e. run computations on the GPU.<br>
* A {@link ComputePass} is basically a wrapper around a {@link ComputeMaterial} that handles most of the process.
*
* The default render behaviour of a {@link ComputePass} is to set its {@link core/bindGroups/BindGroup.BindGroup | bind groups} and then dispatch the workgroups based on the provided {@link ComputeMaterial#dispatchSize | dispatchSize}.<br>
* However, most of the time you'd want a slightly more complex behaviour. The {@link ComputePass#useCustomRender | `useCustomRender` hook} lets you define a totally custom behaviour, but you'll have to set all the {@link core/bindGroups/BindGroup.BindGroup | bind groups} and dispatch the workgroups by yourself.
*
* @example
* ```javascript
* // set our main GPUCurtains instance
* const gpuCurtains = new GPUCurtains({
* container: '#canvas' // selector of our WebGPU canvas container
* })
*
* // set the GPU device
* // note this is asynchronous
* await gpuCurtains.setDevice()
*
* // let's assume we are going to compute the positions of 100.000 particles
* const nbParticles = 100_000
*
* const computePass = new ComputePass(gpuCurtains, {
* label: 'My compute pass',
* shaders: {
* compute: {
* code: computeShaderCode, // assume it is a valid WGSL compute shader
* },
* },
* dispatchSize: Math.ceil(nbParticles / 64),
* storages: {
* particles: {
* access: 'read_write',
* struct: {
* position: {
* type: 'array<vec4f>',
* value: new Float32Array(nbParticles * 4),
* },
* },
* },
* },
* })
* ```
*/
var ComputePass = class {
/**
* Whether this {@link ComputePass} should be added to our {@link core/scenes/Scene.Scene | Scene} to let it handle the rendering process automatically.
* @private
*/
#autoRender = true;
/** Flag indicating whether this {@link ComputePass} should run or not, much like the {@link core/meshes/Mesh.Mesh#visible | Mesh visible} flag. */
#active = true;
/**
* ComputePass constructor
* @param renderer - {@link Renderer} class object or {@link GPUCurtains} class object used to create this {@link ComputePass}.
* @param parameters - {@link ComputePassParams | parameters} used to create our {@link ComputePass}.
*/
constructor(renderer, parameters = {}) {
this._onReadyCallback = () => {};
this._onBeforeRenderCallback = () => {};
this._onRenderCallback = () => {};
this._onAfterRenderCallback = () => {};
this._onAfterResizeCallback = () => {};
const type = "ComputePass";
renderer = isRenderer(renderer, parameters.label ? `${parameters.label} ${type}` : type);
parameters.label = parameters.label ?? "ComputePass " + renderer.computePasses?.length;
this.renderer = renderer;
this.type = type;
this.uuid = generateUUID();
Object.defineProperty(this, "index", { value: computePassIndex++ });
const { label, shaders, renderOrder, uniforms, storages, bindings, bindGroups, samplers, textures, autoRender, active, useAsyncPipeline, texturesOptions, dispatchSize } = parameters;
this.options = {
label,
shaders,
...autoRender !== void 0 && { autoRender },
...active !== void 0 && { active },
...renderOrder !== void 0 && { renderOrder },
...dispatchSize !== void 0 && { dispatchSize },
useAsyncPipeline: useAsyncPipeline === void 0 ? true : useAsyncPipeline,
texturesOptions
};
this.renderOrder = renderOrder ?? 0;
if (autoRender !== void 0) this.#autoRender = autoRender;
this.#active = active === void 0 ? true : active;
this.userData = {};
this.ready = false;
this.setMaterial({
label: this.options.label,
shaders: this.options.shaders,
uniforms,
storages,
bindings,
bindGroups,
samplers,
textures,
useAsyncPipeline,
dispatchSize
});
this.addToScene(true);
}
/**
* Get or set whether the compute pass is ready to render (the material has been successfully compiled).
* @readonly
*/
get ready() {
return this._ready;
}
set ready(value) {
if (value) this._onReadyCallback && this._onReadyCallback();
this._ready = value;
}
/**
* Add our {@link ComputePass} to the scene and optionally to the renderer.
* @param addToRenderer - Whether to add this {@link ComputePass} to the {@link Renderer#computePasses | Renderer computePasses array}.
*/
addToScene(addToRenderer = false) {
if (addToRenderer) this.renderer.computePasses.push(this);
if (this.#autoRender) this.renderer.scene.addComputePass(this);
}
/**
* Remove our {@link ComputePass} from the scene and optionally from the renderer as well.
* @param removeFromRenderer - whether to remove this {@link ComputePass} from the {@link Renderer#computePasses | Renderer computePasses array}.
*/
removeFromScene(removeFromRenderer = false) {
if (this.#autoRender) this.renderer.scene.removeComputePass(this);
if (removeFromRenderer) this.renderer.computePasses = this.renderer.computePasses.filter((computePass) => computePass.uuid !== this.uuid);
}
/**
* Set a new {@link Renderer} for this {@link ComputePass}.
* @param renderer - new {@link Renderer} to set.
*/
setRenderer(renderer) {
renderer = isRenderer(renderer, this.options.label + " ComputePass");
this.material?.setRenderer(renderer);
this.removeFromScene(true);
this.renderer = renderer;
this.addToScene(true);
}
/**
* Create the compute pass material.
* @param computeParameters - {@link ComputeMaterial} parameters.
*/
setMaterial(computeParameters) {
this.useMaterial(new ComputeMaterial(this.renderer, computeParameters));
}
/**
* Set or update the {@link ComputePass} {@link ComputeMaterial}.
* @param material - New {@link ComputeMaterial} to use.
*/
useMaterial(material) {
this.material = material;
}
/**
* Called when the {@link core/renderers/GPUDeviceManager.GPUDeviceManager#device | device} has been lost to prepare everything for restoration.
* Basically set all the {@link GPUBuffer} to null so they will be reset next time we try to render.
*/
loseContext() {
this.material.loseContext();
}
/**
* Called when the {@link core/renderers/GPUDeviceManager.GPUDeviceManager#device | device} has been restored.
*/
restoreContext() {
this.material.restoreContext();
}
/**
* Get the active property value.
*/
get active() {
return this.#active;
}
/**
* Set the active property value.
* @param value - New active value.
*/
set active(value) {
this.#active = value;
}
/**
* Get our {@link ComputeMaterial#textures | ComputeMaterial textures array}
* @readonly
*/
get textures() {
return this.material?.textures || [];
}
/**
* Create a new {@link MediaTexture}.
* @param options - {@link MediaTextureParams | MediaTexture parameters}.
* @returns - Newly created {@link MediaTexture}.
*/
createMediaTexture(options) {
if (!options.name) options.name = "texture" + this.textures.length;
if (!options.label) options.label = this.options.label + " " + options.name;
const texture = new MediaTexture(this.renderer, {
...options,
...this.options.texturesOptions
});
this.addTexture(texture);
return texture;
}
/**
* Create a new {@link Texture}.
* @param options - {@link TextureParams | Texture parameters}.
* @returns - newly created {@link Texture}.
*/
createTexture(options) {
if (!options.name) options.name = "texture" + this.textures.length;
const texture = new Texture(this.renderer, options);
this.addTexture(texture);
return texture;
}
/**
* Add a {@link Texture} or {@link MediaTexture}.
* @param texture - {@link Texture} to add.
*/
addTexture(texture) {
this.material.addTexture(texture);
}
/**
* Get our {@link ComputeMaterial#uniforms | ComputeMaterial uniforms}.
* @readonly
*/
get uniforms() {
return this.material?.uniforms;
}
/**
* Get our {@link ComputeMaterial#storages | ComputeMaterial storages}.
* @readonly
*/
get storages() {
return this.material?.storages;
}
/**
* Called from the renderer, useful to trigger an after resize callback.
*/
resize() {
this._onAfterResizeCallback && this._onAfterResizeCallback();
}
/** EVENTS **/
/**
* Callback to run when the {@link ComputePass} is ready.
* @param callback - Callback to run when {@link ComputePass} is ready.
* @returns - Our {@link ComputePass}.
*/
onReady(callback) {
if (callback) this._onReadyCallback = callback;
return this;
}
/**
* Callback to run before the {@link ComputePass} is rendered.
* @param callback - Callback to run just before {@link ComputePass} will be rendered. The callback won't be called if the {@link Renderer} is not ready or the {@link ComputePass} itself is neither {@link ready} nor {@link active}.
* @returns - Our {@link ComputePass}.
*/
onBeforeRender(callback) {
if (callback) this._onBeforeRenderCallback = callback;
return this;
}
/**
* Callback to run when the {@link ComputePass} is rendered.
* @param callback - Callback to run when {@link ComputePass} is rendered. The callback won't be called if the {@link Renderer} is not ready or the {@link ComputePass} itself is neither {@link ready} nor {@link active}.
* @returns - Our {@link ComputePass}.
*/
onRender(callback) {
if (callback) this._onRenderCallback = callback;
return this;
}
/**
* Callback to run after the {@link ComputePass} has been rendered.
* @param callback - Callback to run just after {@link ComputePass} has been rendered. The callback won't be called if the {@link Renderer} is not ready or the {@link ComputePass} itself is neither {@link ready} nor {@link active}.
* @returns - Our {@link ComputePass}.
*/
onAfterRender(callback) {
if (callback) this._onAfterRenderCallback = callback;
return this;
}
/**
* Callback used to run a custom render function instead of the default one. This won't be called if the {@link Renderer} is not ready or the {@link ComputePass} itself is neither {@link ready} nor {@link active}.
* @param callback - Your custom render function where you will have to set all the {@link core/bindGroups/BindGroup.BindGroup | bind groups} and dispatch the workgroups by yourself.
* @returns - Our {@link ComputePass}.
*/
useCustomRender(callback) {
this.material.useCustomRender(callback);
return this;
}
/**
* Callback to run after the {@link core/renderers/GPURenderer.GPURenderer | renderer} has been resized.
* @param callback - Callback to run just after {@link core/renderers/GPURenderer.GPURenderer | renderer} has been resized.
* @returns - Our {@link ComputePass}.
*/
onAfterResize(callback) {
if (callback) this._onAfterResizeCallback = callback;
return this;
}
/**
* Called before rendering the {@link ComputePass}.
* Checks if the material is ready and eventually update its bindings.
*/
onBeforeRenderPass() {
if (!this.renderer.ready) return;
if (this.active) this._onBeforeRenderCallback && this._onBeforeRenderCallback();
this.material.onBeforeRender();
if (this.material && this.material.ready && !this.ready) this.ready = true;
}
/**
* Render our {@link ComputeMaterial}.
* @param pass - Current compute pass encoder.
*/
onRenderPass(pass) {
if (!this.material.ready) return;
this._onRenderCallback && this._onRenderCallback();
this.material.render(pass);
}
/**
* Called after having rendered the {@link ComputePass}.
*/
onAfterRenderPass() {
this._onAfterRenderCallback && this._onAfterRenderCallback();
}
/**
* Render our compute pass.
* Basically just check if our {@link core/renderers/GPURenderer.GPURenderer | renderer} is ready, and then render our {@link ComputeMaterial}.
* @param pass - Current compute pass encoder.
*/
render(pass) {
this.onBeforeRenderPass();
if (!this.renderer.ready || !this.active) return;
!this.renderer.production && pass.pushDebugGroup(this.options.label);
this.onRenderPass(pass);
!this.renderer.production && pass.popDebugGroup();
this.onAfterRenderPass();
}
/**
* Copy the result of our read/write GPUBuffer into our result binding array.
* @param commandEncoder - Current GPU command encoder.
*/
copyBufferToResult(commandEncoder) {
this.material?.copyBufferToResult(commandEncoder);
}
/**
* Get the {@link core/bindings/WritableBufferBinding.WritableBufferBinding#resultBuffer | result GPU buffer} content by {@link core/bindings/WritableBufferBinding.WritableBufferBinding | binding} and {@link core/bindings/bufferElements/BufferElement.BufferElement | buffer element} names.
* @param parameters - Parameters used to get the result
* @param parameters.bindingName - {@link core/bindings/WritableBufferBinding.WritableBufferBinding#name | binding name} from which to get the result.
* @param parameters.bufferElementName - Optional {@link core/bindings/bufferElements/BufferElement.BufferElement | buffer element} (i.e. struct member) name if the result needs to be restrained to only one element.
* @returns - The mapped content of the {@link GPUBuffer} as a {@link Float32Array}.
*/
async getComputeResult({ bindingName, bufferElementName }) {
return await this.material?.getComputeResult({
bindingName,
bufferElementName
});
}
/**
* Remove the {@link ComputePass} from the {@link core/scenes/Scene.Scene | Scene} and destroy it.
*/
remove() {
this.removeFromScene(true);
this.destroy();
}
/**
* Destroy the {@link ComputePass}.
*/
destroy() {
this.material?.destroy();
}
};
//#endregion
export { ComputePass };