UNPKG

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.

496 lines (493 loc) 19.1 kB
import { isRenderer } from '../renderers/utils.mjs'; import { generateUUID, throwWarning } from '../../utils/utils.mjs'; import { BufferBinding } from '../bindings/BufferBinding.mjs'; import { IndirectBuffer } from '../../extras/buffers/IndirectBuffer.mjs'; import { MediaTexture } from '../textures/MediaTexture.mjs'; var __typeError = (msg) => { throw TypeError(msg); }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var _useProjection, _ready, _RenderBundle_instances, setBinding_fn, patchBindingOffset_fn, onSizeChanged_fn, setDescriptor_fn, encodeRenderCommands_fn, cleanUp_fn; let bundleIndex = 0; class RenderBundle { /** * RenderBundle constructor * @param renderer - {@link Renderer} or {@link GPUCurtains} class object used to create this {@link RenderBundle}. * @param parameters - {@link RenderBundleParams | parameters} use to create this {@link RenderBundle}. */ constructor(renderer, { label, renderPass = null, renderOrder = 0, transparent = null, visible = true, size = 0, useBuffer = false, useIndirectDraw = false } = {}) { __privateAdd(this, _RenderBundle_instances); /** @ignore */ // whether this render bundle should be added to the 'projected' or 'unProjected' Scene stacks. __privateAdd(this, _useProjection); /** @ignore */ __privateAdd(this, _ready); this.type = "RenderBundle"; renderer = isRenderer(renderer, this.type); this.uuid = generateUUID(); Object.defineProperty(this, "index", { value: bundleIndex++ }); this.renderOrder = renderOrder; this.transparent = transparent; this.visible = visible; label = label ?? this.type + this.index; this.options = { label, renderPass, useBuffer, size, useIndirectDraw }; this.meshes = /* @__PURE__ */ new Map(); this.encoder = null; this.bundle = null; __privateSet(this, _ready, false); this.binding = null; this.indirectBuffer = null; this.setRenderer(renderer); if (this.options.useIndirectDraw) { this.indirectBuffer = new IndirectBuffer(this.renderer); } if (this.options.useBuffer) { __privateSet(this, _useProjection, true); if (this.options.size !== 0) { __privateMethod(this, _RenderBundle_instances, setBinding_fn).call(this); } else { this.options.useBuffer = false; if (!this.renderer.production) { throwWarning( `${this.options.label} (${this.type}): Cannot use a single transformation buffer if the size parameter has not been set upon creation.` ); } } } } /** * Set the {@link RenderBundle} {@link RenderBundle.renderer | renderer} and eventually remove/add to the {@link core/scenes/Scene.Scene | Scene}. * @param renderer - new {@link Renderer} to use. */ setRenderer(renderer) { if (this.renderer) { this.removeFromScene(); this.renderer.renderBundles.delete(this.uuid); } this.renderer = renderer; this.renderer.renderBundles.set(this.uuid, this); if (this.meshes.size >= 1) { this.addToScene(); } } /** * Add our {@link RenderBundle} to the {@link core/scenes/Scene.Scene | Scene}. * Once we have at least one mesh in our {@link meshes} Map, we can add the {@link RenderBundle} to the {@link core/scenes/Scene.Scene | Scene} at the right place. */ addToScene() { const firstEntry = this.meshes.entries().next(); if (firstEntry && firstEntry.value && firstEntry.value.length && firstEntry.value[1]) { const mesh = firstEntry.value[1]; const isTransparent = !!mesh.transparent; if (this.transparent === null) { this.transparent = isTransparent; } if (mesh.type !== "ShaderPass" && mesh.type !== "PingPongPlane") { const { useProjection } = mesh.material.options.rendering; if (this.useProjection === null) { this.useProjection = useProjection; } const projectionStack = this.renderer.scene.getMeshProjectionStack(mesh); this.renderer.scene.addRenderBundle(this, projectionStack); } else { this.size = 1; mesh.renderOrder = this.renderOrder; this.useProjection = false; } } } /** * Remove our {@link RenderBundle} from the {@link core/scenes/Scene.Scene | Scene}. */ removeFromScene() { this.renderer.scene.removeRenderBundle(this); } /** * Get whether our {@link RenderBundle} handles {@link core/renderers/GPURenderer.ProjectedMesh | projected meshes} or not (useful to know in which {@link core/scenes/Scene.Scene | Scene} stack it has been added. * @readonly * @returns - Whether our {@link RenderBundle} handles {@link core/renderers/GPURenderer.ProjectedMesh | projected meshes} or not. */ get useProjection() { return __privateGet(this, _useProjection); } /** * Set whether our {@link RenderBundle} handles {@link core/renderers/GPURenderer.ProjectedMesh | projected meshes} or not. * @param value - New projection value. */ set useProjection(value) { __privateSet(this, _useProjection, value); } /** * Set the new {@link RenderBundle} size. Should be used before adding or removing {@link meshes} to the {@link RenderBundle} if the {@link bundle} has already been created (especially if it's using a {@link binding}). * @param value - New size to set. */ set size(value) { if (value !== this.options.size) { if (this.ready && !this.renderer.production) { throwWarning( `${this.options.label} (${this.type}): The content of a render bundle is meant to be static. You should not change its size after it has been created.` ); } this.ready = false; __privateMethod(this, _RenderBundle_instances, onSizeChanged_fn).call(this, value); this.options.size = value; } } /** * Get whether our {@link RenderBundle} is ready. * @readonly * @returns - Whether our {@link RenderBundle} is ready. */ get ready() { return this.renderer.ready && __privateGet(this, _ready); } /** * Set whether our {@link RenderBundle} is ready and encode it if needed. * @param value - New ready state. */ set ready(value) { if (value && !this.ready) { const init = () => { this.size = this.meshes.size; if (this.options.useIndirectDraw) { this.meshes.forEach((mesh) => { this.indirectBuffer.addGeometry(mesh.geometry); }); this.indirectBuffer.create(); } __privateMethod(this, _RenderBundle_instances, encodeRenderCommands_fn).call(this); __privateSet(this, _ready, value); }; if (this.renderer.device) { init(); } else { const taskId = this.renderer.onBeforeCommandEncoderCreation.add( () => { if (this.renderer.device) { this.renderer.onBeforeCommandEncoderCreation.remove(taskId); init(); } }, { once: false } ); } } else if (!value && this.ready) { this.bundle = null; __privateSet(this, _ready, value); } } /** * Called by the {@link core/scenes/Scene.Scene | Scene} to eventually add a {@link RenderedMesh | mesh} to this {@link RenderBundle}. Can set the {@link RenderBundleOptions#renderPass | render pass} if needed. If the {@link RenderBundleOptions#renderPass | render pass} is already set and the mesh output {@link RenderPass} does not match, it won't be added. * @param mesh - {@link RenderedMesh | Mesh} to eventually add. * @param outputPass - The mesh output {@link RenderPass}. */ addMesh(mesh, outputPass) { if (!this.options.renderPass) { this.options.renderPass = outputPass; } else if (outputPass.uuid !== this.options.renderPass.uuid) { if (!this.renderer.production) { throwWarning( `${this.options.label} (${this.type}): Cannot add Mesh ${mesh.options.label} to this render bundle because the output render passes do not match.` ); } mesh.renderBundle = null; return; } if (mesh.options.stencil) { if (!this.renderer.production) { throwWarning( `${this.options.label} (${this.type}): Cannot add Mesh ${mesh.options.label} to this render bundle because stencil operations are not supported by render bundles.` ); } mesh.renderBundle = null; return; } if (this.ready && !this.renderer.production) { throwWarning( `${this.options.label} (${this.type}): The content of a render bundle is meant to be static. You should not add meshes to it after it has been created (mesh added: ${mesh.options.label}).` ); } this.ready = false; this.meshes.set(mesh.uuid, mesh); if (this.meshes.size === 1) { this.addToScene(); } } /** * Remove any {@link RenderedMesh | rendered mesh} from this {@link RenderBundle}. * @param mesh - {@link RenderedMesh | Mesh} to remove. */ removeSceneObject(mesh) { if (this.ready && !this.renderer.production) { throwWarning( `${this.options.label} (${this.type}): The content of a render bundle is meant to be static. You should not remove meshes from it after it has been created (mesh removed: ${mesh.options.label}).` ); } this.ready = false; this.meshes.delete(mesh.uuid); mesh.setRenderBundle(null, false); if (this.options.useIndirectDraw) { mesh.geometry.indirectDraw = null; } } /** * Remove a {@link SceneStackedMesh | scene stacked mesh} from this {@link RenderBundle}. * @param mesh - {@link SceneStackedMesh | Scene stacked mesh} to remove. * @param keepMesh - Whether to preserve the mesh in order to render it normally again. Default to `true`. */ removeMesh(mesh, keepMesh = true) { this.removeSceneObject(mesh); if (keepMesh && mesh.type !== "ShaderPass" && mesh.type !== "PingPongPlane") { this.renderer.scene.addMesh(mesh); } if (this.meshes.size === 0) { this.renderer.scene.removeRenderBundle(this); } } /** * Update the {@link binding} buffer if needed. */ updateBinding() { if (this.binding && this.binding.shouldUpdate && this.binding.buffer.GPUBuffer) { this.renderer.queueWriteBuffer(this.binding.buffer.GPUBuffer, 0, this.binding.arrayBuffer); this.binding.shouldUpdate = false; } } /** * If one of the {@link meshes} is using a {@link core/textures/Texture.Texture | Texture} dependent of the {@link renderer}, invalidate the {@link RenderBundle} in order to resize the {@link core/textures/Texture.Texture | Texture}. */ resize() { for (const [_uuid, mesh] of this.meshes) { const hasRenderTexture = mesh.textures.find((texture) => !texture.options.fixedSize); if (hasRenderTexture) { this.ready = false; break; } } } /** * Render the {@link RenderBundle}. * * If it is ready, execute each {@link core/meshes/Mesh.Mesh.onBeforeRenderPass | mesh onBeforeRenderPass method}, {@link updateBinding | update the binding} if needed, execute the {@link bundle} and finally execute each {@link core/meshes/Mesh.Mesh.onAfterRenderPass | mesh onAfterRenderPass method}. * * If not, just render its {@link meshes} as usual and check whether they are all ready and if we can therefore encode our {@link RenderBundle}. * @param pass - {@link GPURenderPassEncoder} to use. */ render(pass) { if (!this.renderer.ready) return; if (this.ready && this.bundle && this.visible) { this.meshes.forEach((mesh) => { mesh.onBeforeRenderPass(); }); this.updateBinding(); this.renderer.pipelineManager.resetCurrentPipeline(); if (!this.renderer.production) { pass.pushDebugGroup(`${this.options.label}: execute bundle`); } pass.executeBundles([this.bundle]); if (!this.renderer.production) { pass.popDebugGroup(); } this.renderer.pipelineManager.resetCurrentPipeline(); this.meshes.forEach((mesh) => { mesh.onAfterRenderPass(); }); } if (!this.ready) { let isReady = true; for (const [_key, mesh] of this.meshes) { mesh.render(pass); if (!mesh.ready) { isReady = false; } for (const texture of mesh.textures) { if (texture instanceof MediaTexture && !texture.sourcesUploaded) { isReady = false; } } } this.updateBinding(); this.ready = isReady; } } /** * Called when the {@link core/renderers/GPURenderer.GPURenderer#device | WebGPU device} has been lost. * Just set the {@link ready} flag to `false` to eventually invalidate the {@link bundle}. */ loseContext() { this.ready = false; } /** * Empty the {@link RenderBundle}. Can eventually re-add the {@link SceneStackedMesh | scene stacked meshes} to the {@link core/scenes/Scene.Scene | Scene} in order to render them normally again. * @param keepMeshes - Whether to preserve the {@link meshes} in order to render them normally again. Default to `true`. */ empty(keepMeshes = true) { this.ready = false; this.meshes.forEach((mesh) => { this.removeMesh(mesh, keepMeshes); }); this.size = 0; } /** * Remove the {@link RenderBundle}, i.e. destroy it while preserving the {@link SceneStackedMesh | scene stacked meshes} by re-adding them to the {@link core/scenes/Scene.Scene | Scene}. */ remove() { this.empty(true); __privateMethod(this, _RenderBundle_instances, cleanUp_fn).call(this); } /** * Remove the {@link RenderBundle} from our {@link core/scenes/Scene.Scene | Scene}, {@link RenderedMesh#remove | remove the meshes}, eventually destroy the {@link binding} and remove the {@link RenderBundle} from the {@link Renderer}. */ destroy() { this.ready = false; this.meshes.forEach((mesh) => { mesh.remove(); }); this.size = 0; __privateMethod(this, _RenderBundle_instances, cleanUp_fn).call(this); } } _useProjection = new WeakMap(); _ready = new WeakMap(); _RenderBundle_instances = new WeakSet(); /** * Set the {@link binding} and patches its array and buffer size if needed. * @private */ setBinding_fn = function() { this.binding = new BufferBinding({ label: this.options.label + " matrices", name: "matrices", visibility: ["vertex", "fragment"], struct: { model: { type: "array<mat4x4f>", value: new Float32Array(16 * this.options.size) }, modelView: { type: "array<mat4x4f>", value: new Float32Array(16 * this.options.size) }, normal: { type: "array<mat3x3f>", value: new Float32Array(12 * this.options.size) } } }); __privateMethod(this, _RenderBundle_instances, patchBindingOffset_fn).call(this, this.options.size); }; /** * Path the {@link binding} array and buffer size with the minimum {@link core/renderers/GPURenderer.GPURenderer#device | device} buffer offset alignment. * @param size - new {@link binding} size to use. * @private */ patchBindingOffset_fn = function(size) { const minOffset = this.renderer.device?.limits.minUniformBufferOffsetAlignment || 256; if (this.binding.arrayBufferSize < size * minOffset) { this.binding.arrayBufferSize = size * minOffset; this.binding.arrayBuffer = new ArrayBuffer(this.binding.arrayBufferSize); this.binding.arrayView = new DataView(this.binding.arrayBuffer, 0, this.binding.arrayBufferSize); this.binding.buffer.size = this.binding.arrayBuffer.byteLength; } }; /** * Called each time the {@link RenderBundle} size has actually changed. * @param newSize - new {@link RenderBundle} size to set. * @private */ onSizeChanged_fn = function(newSize) { if (newSize > this.options.size && this.binding) { __privateMethod(this, _RenderBundle_instances, patchBindingOffset_fn).call(this, newSize); let offset = 0; this.meshes.forEach((mesh) => { mesh.patchRenderBundleBinding(offset); offset++; }); if (this.binding.buffer.GPUBuffer) { this.binding.buffer.GPUBuffer.destroy(); this.binding.buffer.createBuffer(this.renderer, { label: this.binding.options.label, usage: [ ...["copySrc", "copyDst", this.binding.bindingType], ...this.binding.options.usage ] }); this.binding.shouldUpdate = true; } } }; /** * Set the {@link descriptor} based on the {@link RenderBundleOptions#renderPass | render pass}. * @private */ setDescriptor_fn = function() { this.descriptor = { ...this.options.renderPass.options.colorAttachments && { colorFormats: this.options.renderPass.options.colorAttachments.map( (colorAttachment) => colorAttachment.targetFormat ) }, ...this.options.renderPass.options.useDepth && { depthStencilFormat: this.options.renderPass.options.depthFormat }, sampleCount: this.options.renderPass.options.sampleCount }; }; /** * Create the {@link descriptor}, {@link encoder} and {@link bundle} used by this {@link RenderBundle}. * @private */ encodeRenderCommands_fn = function() { __privateMethod(this, _RenderBundle_instances, setDescriptor_fn).call(this); this.renderer.pipelineManager.resetCurrentPipeline(); this.encoder = this.renderer.device.createRenderBundleEncoder({ ...this.descriptor, label: this.options.label + " (encoder)" }); if (!this.renderer.production) { this.encoder.pushDebugGroup(`${this.options.label}: create encoder`); } this.meshes.forEach((mesh) => { mesh.material.render(this.encoder); mesh.geometry.render(this.encoder); }); if (!this.renderer.production) { this.encoder.popDebugGroup(); } this.bundle = this.encoder.finish({ label: this.options.label + " (bundle)" }); this.renderer.pipelineManager.resetCurrentPipeline(); }; /** * Destroy the {@link binding} buffer if needed and remove the {@link RenderBundle} from the {@link Renderer}. * @private */ cleanUp_fn = function() { if (this.binding) { this.renderer.removeBuffer(this.binding.buffer); this.binding.buffer.destroy(); } if (this.indirectBuffer) { this.indirectBuffer.destroy(); } this.renderer.renderBundles.delete(this.uuid); }; export { RenderBundle };