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.
574 lines (573 loc) • 22 kB
JavaScript
import { generateUUID, throwWarning } from "../../../utils/utils.mjs";
import { isRenderer } from "../../renderers/utils.mjs";
import { Texture } from "../../textures/Texture.mjs";
import { MediaTexture } from "../../textures/MediaTexture.mjs";
import { getDefaultVertexShaderCode } from "../../shaders/full/vertex/get-default-vertex-shader-code.mjs";
import { getDefaultFragmentCode } from "../../shaders/full/fragment/get-default-fragment-code.mjs";
import { RenderMaterial } from "../../materials/RenderMaterial.mjs";
//#region src/core/meshes/mixins/MeshBaseMixin.ts
let meshIndex = 0;
/** @const - Default Mesh parameters to merge with user defined parameters. */
const defaultMeshBaseParams = {
autoRender: true,
useProjection: false,
useAsyncPipeline: true,
cullMode: "back",
depth: true,
depthWriteEnabled: true,
depthCompare: "less",
depthFormat: "depth24plus",
transparent: false,
visible: true,
renderOrder: 0,
texturesOptions: {},
renderBundle: null
};
/**
* Used to mix the basic Mesh properties and methods defined in {@link MeshBaseClass} (basically, set a {@link Geometry} and a {@link RenderMaterial} and render them, add helpers to create {@link MediaTexture} and {@link Texture}) with a given Base of type {@link core/objects3D/Object3D.Object3D | Object3D}, {@link core/objects3D/ProjectedObject3D.ProjectedObject3D | ProjectedObject3D}, {@link curtains/objects3D/DOMObject3D.DOMObject3D | DOMObject3D} or an empty class.
* @param Base - the class to mix onto
* @returns - the mixed classes, creating a basic Mesh.
*/
function MeshBaseMixin(Base) {
/**
* MeshBase defines our base properties and methods
*/
return class MeshBase extends Base {
/** Flag indicating whether to draw this {@link MeshBase} or not */
#visible;
/** Whether we should add this {@link MeshBase} to our {@link core/scenes/Scene.Scene | Scene} to let it handle the rendering process automatically */
#autoRender = true;
/**
* MeshBase constructor
*
* @typedef MeshBaseArrayParams
* @type {array}
* @property {(Renderer|GPUCurtains)} 0 - our {@link Renderer} class object
* @property {(string|HTMLElement|null)} 1 - a DOM HTML Element that can be bound to a Mesh
* @property {MeshBaseParams} 2 - {@link MeshBaseParams | Mesh base parameters}
*
* @param {MeshBaseArrayParams} params - our MeshBaseMixin parameters
*/
constructor(...params) {
super(params[0], params[1], {
...defaultMeshBaseParams,
...params[2]
});
this._onReadyCallback = () => {};
this._onBeforeRenderCallback = () => {};
this._onRenderCallback = () => {};
this._onAfterRenderCallback = () => {};
this._onAfterResizeCallback = () => {};
let renderer = params[0];
const parameters = {
...defaultMeshBaseParams,
...params[2]
};
this.type = "MeshBase";
this.uuid = generateUUID();
Object.defineProperty(this, "index", { value: meshIndex++ });
renderer = isRenderer(renderer, parameters.label ? parameters.label + " " + this.type : this.type);
this.renderer = renderer;
const { label, shaders, geometry, visible, renderOrder, outputTarget, additionalOutputTargets, useCustomScenePassEntry, renderBundle, texturesOptions, autoRender, ...meshParameters } = parameters;
this.outputTarget = outputTarget ?? null;
this.renderBundle = renderBundle ?? null;
this.additionalOutputTargets = additionalOutputTargets || [];
meshParameters.sampleCount = !!meshParameters.sampleCount ? meshParameters.sampleCount : this.outputTarget ? this.outputTarget.renderPass.options.sampleCount : this.renderer && this.renderer.renderPass ? this.renderer.renderPass.options.sampleCount : 1;
this.options = {
...this.options ?? {},
label: label ?? "Mesh " + this.renderer.meshes.length,
...shaders !== void 0 ? { shaders } : {},
...outputTarget !== void 0 && { outputTarget },
...renderBundle !== void 0 && { renderBundle },
texturesOptions,
...autoRender !== void 0 && { autoRender },
useCustomScenePassEntry,
...meshParameters
};
if (autoRender !== void 0) this.#autoRender = autoRender;
this.visible = visible;
this.renderOrder = renderOrder;
this.ready = false;
this.userData = {};
if (geometry) this.useGeometry(geometry);
this.setMaterial({
...this.cleanupRenderMaterialParameters({ ...this.options }),
...geometry && {
verticesOrder: geometry.verticesOrder,
topology: geometry.topology
}
});
this.addToScene(true);
}
/**
* Get private #autoRender value
* @readonly
*/
get autoRender() {
return this.#autoRender;
}
/**
* Get/set whether a Mesh is ready or not
* @readonly
*/
get ready() {
return this._ready;
}
set ready(value) {
if (value && !this._ready) this._onReadyCallback && this._onReadyCallback();
this._ready = value;
}
/**
* Add a Mesh to the {@link core/scenes/Scene.Scene | Scene} and optionally to the renderer. Can patch the {@link RenderMaterial} render options to match the {@link RenderPass} used to draw this Mesh.
* @param addToRenderer - whether to add this Mesh to the {@link Renderer#meshes | Renderer meshes array}
*/
addToScene(addToRenderer = false) {
if (addToRenderer) this.renderer.meshes.push(this);
this.setRenderingOptionsForRenderPass(this.outputTarget ? this.outputTarget.renderPass : this.renderer.renderPass);
if (this.#autoRender) {
this.renderer.scene.addMesh(this);
if (this.additionalOutputTargets.length) this.additionalOutputTargets.forEach((renderTarget) => {
this.renderer.scene.addMeshToRenderTargetStack(this, renderTarget);
});
}
}
/**
* Remove a Mesh from the {@link core/scenes/Scene.Scene | Scene} and optionally from the renderer as well.
* @param removeFromRenderer - whether to remove this Mesh from the {@link Renderer#meshes | Renderer meshes array}
*/
removeFromScene(removeFromRenderer = false) {
if (this.#autoRender) this.renderer.scene.removeMesh(this);
if (removeFromRenderer) this.renderer.meshes = this.renderer.meshes.filter((m) => m.uuid !== this.uuid);
}
/**
* Set a new {@link Renderer} for this Mesh
* @param renderer - new {@link Renderer} to set
*/
setRenderer(renderer) {
renderer = renderer && renderer.renderer || renderer;
if (!renderer || !(renderer.type === "GPURenderer" || renderer.type === "GPUCameraRenderer" || renderer.type === "GPUCurtainsRenderer")) {
throwWarning(`${this.options.label}: Cannot set ${renderer} as a renderer because it is not of a valid Renderer type.`);
return;
}
this.material?.setRenderer(renderer);
const oldRenderer = this.renderer;
this.removeFromScene(true);
this.renderer = renderer;
this.addToScene(true);
if (!oldRenderer.meshes.length) oldRenderer.onBeforeRenderScene.add((commandEncoder) => {
oldRenderer.forceClear(commandEncoder);
}, { once: true });
}
/**
* Assign or remove a {@link RenderTarget} to this Mesh.
* Since this manipulates the {@link core/scenes/Scene.Scene | Scene} stacks, it can be used to remove a {@link RenderTarget} as well.
* @param outputTarget - the {@link RenderTarget} to assign or null if we want to remove the current {@link RenderTarget}.
*/
setOutputTarget(outputTarget) {
if (outputTarget && outputTarget.type !== "RenderTarget") {
throwWarning(`${this.options.label ?? this.type}: outputTarget is not a RenderTarget: ${outputTarget.type}`);
return;
}
this.removeFromScene();
this.outputTarget = outputTarget;
this.addToScene();
}
/**
* Assign or remove a {@link RenderBundle} to this Mesh.
* @param renderBundle - the {@link RenderBundle} to assign or null if we want to remove the current {@link RenderBundle}.
* @param updateScene - Whether to remove and then re-add the Mesh from the {@link core/scenes/Scene.Scene | Scene} or not.
*/
setRenderBundle(renderBundle, updateScene = true) {
if (updateScene) {
this.removeFromScene();
this.renderBundle = renderBundle;
this.addToScene();
} else this.renderBundle = renderBundle;
}
/**
* 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 draw the Mesh
*/
loseContext() {
this.ready = false;
this.geometry.loseContext();
this.material.loseContext();
}
/**
* Called when the {@link core/renderers/GPUDeviceManager.GPUDeviceManager#device | device} has been restored
*/
restoreContext() {
this.geometry.restoreContext(this.renderer);
this.material.restoreContext();
}
/**
* Set default shaders if one or both of them are missing
*/
setShaders() {
const { shaders } = this.options;
if (!shaders) this.options.shaders = {
vertex: {
code: getDefaultVertexShaderCode,
entryPoint: "main"
},
fragment: {
code: getDefaultFragmentCode,
entryPoint: "main"
}
};
else {
if (!shaders.vertex || !shaders.vertex.code) shaders.vertex = {
code: getDefaultVertexShaderCode,
entryPoint: "main"
};
if (shaders.fragment === void 0 || shaders.fragment && !shaders.fragment.code) shaders.fragment = {
code: getDefaultFragmentCode,
entryPoint: "main"
};
}
}
/**
* Set or update the Mesh {@link Geometry}
* @param geometry - new {@link Geometry} to use
*/
useGeometry(geometry) {
if (this.geometry) {
if (geometry.shouldCompute) geometry.computeGeometry();
if (this.geometry.layoutCacheKey !== geometry.layoutCacheKey) {
throwWarning(`${this.options.label} (${this.type}): the current and new geometries do not have the same vertexBuffers layout, causing a probable pipeline recompilation. This should be avoided.\n\nCurrent geometry layout:\n\n${this.geometry.wgslStructFragment}\n\n--------\n\nNew geometry layout:\n\n${geometry.wgslStructFragment}`);
this.material.setAttributesFromGeometry(geometry);
this.material.setPipelineEntry();
}
this.geometry.consumers.delete(this.uuid);
if (this.options.renderBundle) this.options.renderBundle.ready = false;
}
this.geometry = geometry;
this.geometry.consumers.add(this.uuid);
this.computeGeometry();
if (this.material) {
const renderingOptions = {
...this.material.options.rendering,
verticesOrder: geometry.verticesOrder,
topology: geometry.topology
};
this.material.setRenderingOptions(renderingOptions);
}
}
/**
* Compute the Mesh geometry if needed
*/
computeGeometry() {
if (this.geometry.shouldCompute) this.geometry.computeGeometry();
}
/**
* Set our Mesh geometry: create buffers and add attributes to material
*/
setGeometry() {
if (this.geometry) {
if (!this.geometry.ready) this.geometry.createBuffers({
renderer: this.renderer,
label: this.options.label + " geometry"
});
this.setMaterialGeometryAttributes();
}
}
/**
* Set or update the {@link RenderMaterial} {@link types/Materials.RenderMaterialRenderingOptions | rendering options} to match the {@link RenderPass#descriptor | RenderPass descriptor} used to draw this Mesh.
* @param renderPass - {@link RenderPass | RenderPass} used to draw this Mesh, default to the {@link core/renderers/GPURenderer.GPURenderer#renderPass | renderer renderPass}.
*/
setRenderingOptionsForRenderPass(renderPass) {
const renderingOptions = {
transparent: this.transparent,
sampleCount: renderPass.options.sampleCount,
...renderPass.options.colorAttachments.length && { targets: renderPass.options.colorAttachments.map((colorAttachment, index) => {
return {
format: colorAttachment.targetFormat,
...this.options.targets?.length && this.options.targets[index] && this.options.targets[index].blend && { blend: this.options.targets[index].blend }
};
}) },
depth: renderPass.options.useDepth,
...renderPass.options.useDepth && { depthFormat: renderPass.options.depthFormat }
};
this.material?.setRenderingOptions({
...this.material.options.rendering,
...renderingOptions
});
}
/**
* Hook used to clean up parameters before sending them to the {@link RenderMaterial}.
* @param parameters - parameters to clean before sending them to the {@link RenderMaterial}
* @returns - cleaned parameters
*/
cleanupRenderMaterialParameters(parameters) {
delete parameters.additionalOutputTargets;
delete parameters.autoRender;
delete parameters.outputTarget;
delete parameters.renderBundle;
delete parameters.texturesOptions;
delete parameters.useCustomScenePassEntry;
return parameters;
}
/**
* Set or update the Mesh {@link RenderMaterial}
* @param material - new {@link RenderMaterial} to use
*/
useMaterial(material) {
let currentCacheKey = null;
if (this.material) {
if (this.geometry) currentCacheKey = this.material.cacheKey;
if (this.options.renderBundle) this.options.renderBundle.ready = false;
}
this.material = material;
if (this.geometry) this.material.setAttributesFromGeometry(this.geometry);
this.transparent = this.material.options.rendering.transparent;
if (currentCacheKey && currentCacheKey !== this.material.cacheKey) if (this.material.ready) this.material.setPipelineEntry();
else this.material.compileMaterial();
}
/**
* Patch the shaders if needed, then set the Mesh material
* @param meshParameters - {@link RenderMaterialParams | RenderMaterial parameters}
*/
setMaterial(meshParameters) {
this.setShaders();
meshParameters.shaders = this.options.shaders;
meshParameters.label = meshParameters.label + " material";
this.useMaterial(new RenderMaterial(this.renderer, meshParameters));
}
/**
* Set Mesh material attributes
*/
setMaterialGeometryAttributes() {
if (this.material && !this.material.attributes) this.material.setAttributesFromGeometry(this.geometry);
}
/**
* Get the transparent property value.
*/
get transparent() {
return this._transparent;
}
/**
* Set the transparent property value. Update the {@link RenderMaterial} rendering options and {@link core/scenes/Scene.Scene | Scene} stack if needed.
* @param value
*/
set transparent(value) {
const switchTransparency = this.transparent !== void 0 && value !== this.transparent;
if (switchTransparency) this.removeFromScene();
this._transparent = value;
if (switchTransparency) this.addToScene();
}
/**
* Get the visible property value.
*/
get visible() {
if (super.visible !== void 0) return super.visible;
else return this.#visible;
}
/**
* Set the visible property value.
* @param value - New visibility value.
*/
set visible(value) {
if (super.visible !== void 0) super.visible = value;
else this.#visible = value;
if (this.renderBundle) this.renderBundle.ready = false;
}
/**
* Get our {@link RenderMaterial#textures | RenderMaterial 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 texturesOptions = {
...options,
...this.options.texturesOptions
};
if (this.renderBundle) texturesOptions.useExternalTextures = false;
const mediaTexture = new MediaTexture(this.renderer, texturesOptions);
this.addTexture(mediaTexture);
return mediaTexture;
}
/**
* 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} or {@link MediaTexture} to add.
*/
addTexture(texture) {
if (this.renderBundle) this.renderBundle.ready = false;
this.material.addTexture(texture);
}
/**
* Get the current {@link RenderMaterial} uniforms
* @readonly
*/
get uniforms() {
return this.material?.uniforms;
}
/**
* Get the current {@link RenderMaterial} storages
* @readonly
*/
get storages() {
return this.material?.storages;
}
/**
* Resize the Mesh.
* @param boundingRect - optional new {@link DOMElementBoundingRect} to use.
*/
resize(boundingRect) {
if (super.resize) super.resize(boundingRect);
this.resizeTextures();
this._onAfterResizeCallback && this._onAfterResizeCallback();
}
/**
* Resize the {@link textures}.
*/
resizeTextures() {
this.textures?.forEach((texture) => {
if (texture.options.fromTexture) texture.copy(texture.options.fromTexture);
});
}
/**
* Callback to execute when a Mesh is ready - i.e. its {@link material} and {@link geometry} are ready.
* @param callback - Callback to run when {@link MeshBase} is ready.
* @returns - Our Mesh.
*/
onReady(callback) {
if (callback) this._onReadyCallback = callback;
return this;
}
/**
* Callback to execute before updating the {@link core/scenes/Scene.Scene | Scene} matrix stack. This means it is called early and allows to update transformations values before actually setting the Mesh matrices (if any). This also means it won't be called if the Mesh has not been added to the {@link core/scenes/Scene.Scene | Scene}. The callback won't be called if the {@link Renderer} is not ready or the Mesh itself is neither {@link ready} nor {@link visible}.
* @param callback - Callback to run just before updating the {@link core/scenes/Scene.Scene | Scene} matrix stack.
* @returns - Our Mesh
*/
onBeforeRender(callback) {
if (callback) this._onBeforeRenderCallback = callback;
return this;
}
/**
* Callback to execute right before actually rendering the Mesh. Useful to update uniforms for example. The callback won't be called if the {@link Renderer} is not ready or the Mesh itself is neither {@link ready} nor {@link visible}.
* @param callback - Callback to run just before rendering the {@link MeshBase}.
* @returns - Our Mesh.
*/
onRender(callback) {
if (callback) this._onRenderCallback = callback;
return this;
}
/**
* Callback to execute just after a Mesh has been rendered. The callback won't be called if the {@link Renderer} is not ready or the Mesh itself is neither {@link ready} nor {@link visible}.
* @param callback - Callback to run just after {@link MeshBase} has been rendered.
* @returns - Our Mesh.
*/
onAfterRender(callback) {
if (callback) this._onAfterRenderCallback = callback;
return this;
}
/**
* Callback to execute just after a Mesh has been resized.
* @param callback - Callback to run just after {@link MeshBase} has been resized.
* @returns - Our Mesh.
*/
onAfterResize(callback) {
if (callback) this._onAfterResizeCallback = callback;
return this;
}
/**
* Execute {@link onBeforeRender} callback if needed. Called by the {@link core/scenes/Scene.Scene | Scene} before updating the matrix stack.
*/
onBeforeRenderScene() {
if (!this.renderer.ready || !this.ready || !this.visible) return;
this._onBeforeRenderCallback && this._onBeforeRenderCallback();
}
/**
* Called before rendering the Mesh.
* Set the geometry if needed (create buffers and add attributes to the {@link RenderMaterial}).
* Then executes {@link RenderMaterial#onBeforeRender}: create its bind groups and pipeline if needed and eventually update its bindings.
*/
onBeforeRenderPass() {
if (!this.renderer.ready) return;
this.setGeometry();
if (this.visible && this.ready) this._onRenderCallback && this._onRenderCallback();
this.material.onBeforeRender();
this.ready = this.material && this.material.ready && this.geometry && this.geometry.ready;
}
/**
* Render our {@link MeshBase} if the {@link RenderMaterial} is ready.
* @param pass - Current render pass encoder.
*/
onRenderPass(pass) {
if (!this.ready) return;
this.renderPass(pass);
}
/**
* Render the {@link material} and {@link geometry}.
* @param pass - Current render pass encoder.
*/
renderPass(pass) {
this.material.render(pass);
this.geometry.render(pass);
}
/**
* Called after having rendered the Mesh.
*/
onAfterRenderPass() {
this._onAfterRenderCallback && this._onAfterRenderCallback();
}
/**
* Render our Mesh:
* - Execute {@link onBeforeRenderPass}.
* - Stop here if {@link Renderer} is not ready or Mesh is not {@link visible}.
* - Execute super render call if it exists.
* - {@link onRenderPass | render} our {@link material} and {@link geometry}.
* - Execute {@link onAfterRenderPass}.
* @param pass - Current render pass encoder.
*/
render(pass) {
this.onBeforeRenderPass();
if (!this.renderer.ready || !this.visible) return;
!this.renderer.production && pass.pushDebugGroup(this.options.label);
this.onRenderPass(pass);
!this.renderer.production && pass.popDebugGroup();
this.onAfterRenderPass();
}
/**
* Remove the Mesh from the {@link core/scenes/Scene.Scene | Scene} and destroy it.
*/
remove() {
this.removeFromScene(true);
this.destroy();
if (!this.renderer.meshes.length) this.renderer.onBeforeRenderScene.add((commandEncoder) => {
this.renderer.forceClear(commandEncoder);
}, { once: true });
}
/**
* Destroy the Mesh.
*/
destroy() {
if (super.destroy) super.destroy();
this.material?.destroy();
this.geometry.consumers.delete(this.uuid);
if (!this.geometry.consumers.size) this.geometry?.destroy(this.renderer);
}
};
}
//#endregion
export { MeshBaseMixin };