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.

510 lines (509 loc) 29.8 kB
import { throwWarning } from "../../utils/utils.mjs"; import { Vec3 } from "../../math/Vec3.mjs"; import { Object3D } from "../objects3D/Object3D.mjs"; import { isRenderer } from "../renderers/utils.mjs"; import { Mesh } from "../meshes/Mesh.mjs"; //#region src/core/scenes/Scene.ts const camPosA = new Vec3(); const camPosB = new Vec3(); const posA = new Vec3(); const posB = new Vec3(); /** * Used to by the {@link Renderer} to render everything that needs to be rendered (compute passes, meshes and/or render bundles) in the right order with the right pass descriptors and target textures, perform textures copy at the right time, etc. * * ## Render order * * - Run all the {@link ComputePass} first, sorted by their {@link ComputePass#renderOrder | renderOrder} * - Then render all {@link renderPassEntries} `pingPong` entries Meshes or {@link RenderBundle}, sorted by their {@link PingPongPlane#renderOrder | renderOrder}. * - Then all Meshes that need to be rendered into specific {@link renderPassEntries} outputTarget entries: * - First, the opaque unprojected Meshes (i.e. opaque {@link core/meshes/FullscreenPlane.FullscreenPlane | FullscreenPlane} or {@link RenderBundle}, if any), sorted by their {@link core/meshes/FullscreenPlane.FullscreenPlane#renderOrder | renderOrder}. * - Then, the transparent unprojected Meshes (i.e. transparent {@link core/meshes/FullscreenPlane.FullscreenPlane | FullscreenPlane} or {@link RenderBundle}, if any), sorted by their {@link core/meshes/FullscreenPlane.FullscreenPlane#renderOrder | renderOrder}. * - Then render all {@link renderPassEntries} `prePass` entries {@link ShaderPass}, sorted by their {@link ShaderPass#renderOrder | renderOrder}. This would be mainly used for "blit" passes rendered before the content that needs to be actually rendered on the main screen buffer. * - Then, the opaque projected Meshes (i.e. opaque {@link core/meshes/Mesh.Mesh | Mesh}, {@link curtains/meshes/DOMMesh.DOMMesh | DOMMesh}, {@link curtains/meshes/Plane.Plane | Plane}) or {@link RenderBundle}, sorted by their {@link core/meshes/Mesh.Mesh#renderOrder | renderOrder}. * - Finally, the transparent projected Meshes (i.e. transparent {@link core/meshes/Mesh.Mesh | Mesh}, {@link curtains/meshes/DOMMesh.DOMMesh | DOMMesh}, {@link curtains/meshes/Plane.Plane | Plane} or {@link RenderBundle}), sorted by their Z position and then their {@link core/meshes/Mesh.Mesh#renderOrder | renderOrder}. * - Next, all the Meshes that need to be rendered directly to the {@link renderPassEntries} screen main buffer (the {@link Renderer} current texture), in the same order than above. The `screen` {@link renderPassEntries} array can have multiple entries, allowing you to render first a given set of meshes, the perform any operation (like getting the outputted content) before rendering another set of meshes. * - Finally, all the post processing {@link ShaderPass} in the `postProPass` {@link renderPassEntries} array. */ var Scene = class extends Object3D { /** * Keep track of whether the next render pass should set its `loadOp` to `'load'` when rendering. * @private */ #shouldLoadColors; /** * Keep track of whether the next render pass should set its `depthLoadOp` to `'load'` when rendering. * @private */ #shouldLoadDepth; /** * Scene constructor * @param renderer - {@link Renderer} object or {@link GPUCurtains} class object used to create this {@link Scene}. */ constructor({ renderer }) { super(); renderer = isRenderer(renderer, "Scene"); this.renderer = renderer; this.#shouldLoadColors = false; this.#shouldLoadDepth = false; this.computePassEntries = []; this.renderPassEntries = { pingPong: [], renderTarget: [], prePass: [], screen: [], postRenderTarget: [], postProPass: [] }; } /** * Create a new {@link RenderPassEntry} in the {@link renderPassEntries} `screen` array. * @param label - Optional label to use for this {@link RenderPassEntry}. * @param order - Optional order into which insert this {@link renderPassEntries} `screen` array. A positive number means at the end of the array, a negative number means at the beginning. Default to `1`. * @returns - The new {@link RenderPassEntry}. */ createScreenPassEntry(label = "", order = 1) { const screenPassEntry = { label, renderPass: this.renderer.renderPass, renderTexture: null, onBeforeRenderPass: null, onAfterRenderPass: null, useCustomRenderPass: null, element: null, stack: { unProjected: { opaque: [], transparent: [] }, projected: { opaque: [], transparent: [] } } }; if (order >= 0) this.renderPassEntries.screen.push(screenPassEntry); else this.renderPassEntries.screen.unshift(screenPassEntry); return screenPassEntry; } /** * Set the main {@link Renderer} render pass entry. */ setMainRenderPassEntry() { this.createScreenPassEntry("Main scene screen render pass"); } /** * Get the number of meshes a {@link RenderPassEntry | render pass entry} should draw. * @param renderPassEntry - The {@link RenderPassEntry | render pass entry} to test. */ getRenderPassEntryLength(renderPassEntry) { if (!renderPassEntry) return 0; else return renderPassEntry.element ? renderPassEntry.element.visible ? 1 : 0 : renderPassEntry.stack.unProjected.opaque.length + renderPassEntry.stack.unProjected.transparent.length + renderPassEntry.stack.projected.opaque.length + renderPassEntry.stack.projected.transparent.length; } /** * Add a {@link ComputePass} to our scene {@link computePassEntries} array. * @param computePass - {@link ComputePass} to add. */ addComputePass(computePass) { this.computePassEntries.push(computePass); this.computePassEntries.sort((a, b) => { if (a.renderOrder !== b.renderOrder) return a.renderOrder - b.renderOrder; else return a.index - b.index; }); } /** * Remove a {@link ComputePass} from our scene {@link computePassEntries} array. * @param computePass - {@link ComputePass} to remove. */ removeComputePass(computePass) { this.computePassEntries = this.computePassEntries.filter((cP) => cP.uuid !== computePass.uuid); } /** * Add a {@link RenderTarget} to our scene {@link renderPassEntries} outputTarget array. * Every Meshes later added to this {@link RenderTarget} will be rendered to the {@link RenderTarget#renderTexture | RenderTarget Texture} using the {@link RenderTarget#renderPass.descriptor | RenderTarget RenderPass descriptor}. * @param renderTarget - {@link RenderTarget} to add. */ addRenderTarget(renderTarget) { const targetPassEntries = renderTarget.options.isPostTarget ? this.renderPassEntries.postRenderTarget : this.renderPassEntries.renderTarget; if (!targetPassEntries.find((entry) => entry.renderPass.uuid === renderTarget.renderPass.uuid)) targetPassEntries.push({ label: renderTarget.options.label ? `${renderTarget.options.label} pass entry` : `RenderTarget ${renderTarget.uuid} pass entry`, renderPass: renderTarget.renderPass, renderTexture: renderTarget.renderTexture, onBeforeRenderPass: null, onAfterRenderPass: null, useCustomRenderPass: null, element: null, stack: { unProjected: { opaque: [], transparent: [] }, projected: { opaque: [], transparent: [] } } }); } /** * Remove a {@link RenderTarget} from our scene {@link renderPassEntries} outputTarget array. * @param renderTarget - {@link RenderTarget} to add. */ removeRenderTarget(renderTarget) { let targetPassEntries = renderTarget.options.isPostTarget ? this.renderPassEntries.postRenderTarget : this.renderPassEntries.renderTarget; targetPassEntries = targetPassEntries.filter((entry) => entry.renderPass.uuid !== renderTarget.renderPass.uuid); } /** * Get the {@link RenderPassEntry} in the {@link renderPassEntries} `renderTarget` array (or `screen` array if no {@link RenderTarget} is passed) corresponding to the given {@link RenderTarget}. * @param renderTarget - {@link RenderTarget} to use to retrieve the {@link RenderPassEntry} if any. * @returns - {@link RenderPassEntry} found. */ getRenderTargetPassEntry(renderTarget = null) { if (renderTarget) return (renderTarget.options.isPostTarget ? this.renderPassEntries.postRenderTarget : this.renderPassEntries.renderTarget).find((passEntry) => passEntry.renderPass.uuid === renderTarget.renderPass.uuid); else return this.renderPassEntries.screen.find((passEntry) => passEntry.renderPass.uuid === this.renderer.renderPass.uuid); } /** * Get the correct {@link renderPassEntries | render pass entry} (either {@link renderPassEntries} outputTarget or {@link renderPassEntries} screen) {@link Stack} onto which this Mesh should be added, depending on whether it's projected or not. * @param mesh - Mesh to check. * @returns - The corresponding render pass entry {@link Stack}. */ getMeshProjectionStack(mesh) { const isTransparent = mesh.transparent; const hasRendererTransmissionPassEntry = "transmissionTarget" in this.renderer && this.renderer.transmissionTarget && this.renderer.transmissionTarget.passEntry; const isTransmissive = "transmissive" in mesh.options && mesh.options.transmissive; const { stack } = mesh.options.useCustomScenePassEntry ? mesh.options.useCustomScenePassEntry : isTransmissive || hasRendererTransmissionPassEntry && isTransparent ? this.renderer.transmissionTarget.passEntry : this.getRenderTargetPassEntry(mesh.outputTarget); return mesh.material.options.rendering.useProjection ? stack.projected : stack.unProjected; } /** * Order a {@link SceneStackedObject} array by using the {@link core/meshes/Mesh.Mesh.renderOrder | renderOrder} or {@link core/meshes/Mesh.Mesh.index | index} properties. * @param stack - {@link SceneStackedObject} to sort, filled with {@link RenderedMesh} or {@link RenderBundle}. */ orderStack(stack) { stack.sort((a, b) => { return a.renderOrder - b.renderOrder || a.index - b.index; }); } /** * Test whether a {@link SceneStackedObject} is a {@link RenderBundle} or not. * @param object - Object to test. * @returns - Whether the object is a {@link RenderBundle} or not. */ isStackObjectRenderBundle(object) { return object.type === "RenderBundle"; } /** * Add a {@link SceneStackedMesh} to the given {@link RenderTarget} corresponding {@link RenderPassEntry}. * @param mesh - {@link SceneStackedMesh} to add. * @param renderTarget - {@link RenderTarget} to get the {@link RenderPassEntry} from. If not set, will add to the first {@link renderPassEntries} `screen` array entry. */ addMeshToRenderTargetStack(mesh, renderTarget = null) { const { stack } = this.getRenderTargetPassEntry(renderTarget); const isTransparent = !!mesh.transparent; const projectionStack = mesh.material.options.rendering.useProjection ? stack.projected : stack.unProjected; const similarMeshes = isTransparent ? projectionStack.transparent : projectionStack.opaque; similarMeshes.push(mesh); this.orderStack(similarMeshes); } /** * Add a Mesh to the correct {@link renderPassEntries | render pass entry} {@link Stack} array. * Meshes are then ordered by their {@link core/meshes/mixins/MeshBaseMixin.MeshBaseClass#index | indexes (order of creation]}, {@link core/pipelines/RenderPipelineEntry.RenderPipelineEntry#index | pipeline entry indexes} and then {@link core/meshes/mixins/MeshBaseMixin.MeshBaseClass#renderOrder | renderOrder}. * @param mesh - Mesh to add. */ addMesh(mesh) { if (mesh.renderBundle) mesh.renderBundle.addMesh(mesh, mesh.outputTarget ? mesh.outputTarget.renderPass : this.renderer.renderPass); const { useProjection } = mesh.material.options.rendering; if (!mesh.renderBundle) { const projectionStack = this.getMeshProjectionStack(mesh); const similarMeshes = !!mesh.transparent ? projectionStack.transparent : projectionStack.opaque; similarMeshes.push(mesh); this.orderStack(similarMeshes); } if ("parent" in mesh && !mesh.parent && useProjection) mesh.parent = this; } /** * Remove a Mesh from our {@link Scene}. * @param mesh - Mesh to remove. */ removeMesh(mesh) { if (mesh.renderBundle) mesh.renderBundle.removeMesh(mesh, false); else { const projectionType = mesh.material.options.rendering.useProjection ? "projected" : "unProjected"; const transparencyType = !!mesh.transparent ? "transparent" : "opaque"; for (const renderPassEntries of Object.values(this.renderPassEntries)) renderPassEntries.forEach((renderPassEntry) => { if (renderPassEntry.stack) renderPassEntry.stack[projectionType][transparencyType] = renderPassEntry.stack[projectionType][transparencyType].filter((m) => m.uuid !== mesh.uuid); }); } if ("transmissive" in mesh.options && mesh.options.transmissive) { const transmissivePassEntry = this.renderer.transmissionTarget.passEntry; if ((transmissivePassEntry ? this.getRenderPassEntryLength(transmissivePassEntry) : 0) === 0) this.renderer.destroyTransmissionTarget(); } if ("parent" in mesh && mesh.parent && mesh.parent.object3DIndex === this.object3DIndex) mesh.parent = null; } /** * Add a {@link RenderBundle} to the correct {@link renderPassEntries | render pass entry} {@link Stack} array. * @param renderBundle - {@link RenderBundle} to add. * @param projectionStack - {@link ProjectionStack} onto which to add the {@link RenderBundle}. */ addRenderBundle(renderBundle, projectionStack) { const similarObjects = !!renderBundle.transparent ? projectionStack.transparent : projectionStack.opaque; similarObjects.push(renderBundle); this.orderStack(similarObjects); } /** * Remove a {@link RenderBundle} from our {@link Scene}. * @param renderBundle - {@link RenderBundle} to remove. */ removeRenderBundle(renderBundle) { const projectionType = !!renderBundle.useProjection ? "projected" : "unProjected"; const transparencyType = !!renderBundle.transparent ? "transparent" : "opaque"; const renderPassEntry = [...this.renderPassEntries.renderTarget, ...this.renderPassEntries.postRenderTarget].find((passEntry) => passEntry.renderPass.uuid === renderBundle.options.renderPass?.uuid); if (renderPassEntry) { const { stack } = renderPassEntry; const projectionStack = stack[projectionType]; projectionStack[transparencyType] = projectionStack[transparencyType].filter((bundle) => bundle.uuid !== renderBundle.uuid); } else this.renderPassEntries.screen.forEach((renderPassEntry) => { if (renderPassEntry.stack) renderPassEntry.stack[projectionType][transparencyType] = renderPassEntry.stack[projectionType][transparencyType].filter((m) => m.uuid !== renderBundle.uuid); }); } /** * Add a {@link ShaderPass} to our scene {@link renderPassEntries} `prePass` or `postProPass` array. * Before rendering the {@link ShaderPass}, we will copy the correct input texture into its {@link ShaderPass#renderTexture | renderTexture}. * This also handles the {@link renderPassEntries} `postProPass` array entries order: We will first draw selective passes and then finally global post processing passes. * @see {@link https://codesandbox.io/p/sandbox/webgpu-render-to-2-textures-without-texture-copy-c4sx4s?file=%2Fsrc%2Findex.js%3A10%2C4 | minimal code example} * @param shaderPass - {@link ShaderPass} to add. */ addShaderPass(shaderPass) { const onBeforeRenderPass = shaderPass.inputTarget || shaderPass.outputTarget ? null : (commandEncoder, swapChainTexture) => { if (shaderPass.renderTexture && swapChainTexture) this.renderer.copyGPUTextureToTexture(swapChainTexture, shaderPass.renderTexture, commandEncoder); this.renderer.postProcessingPass.setLoadOp("clear"); }; const onAfterRenderPass = shaderPass.options.copyOutputToRenderTexture ? (commandEncoder, swapChainTexture) => { if (shaderPass.renderTexture && swapChainTexture) this.renderer.copyGPUTextureToTexture(swapChainTexture, shaderPass.renderTexture, commandEncoder); } : null; const outputPass = shaderPass.outputTarget ? shaderPass.outputTarget.renderPass : !shaderPass.options.isPrePass ? this.renderer.postProcessingPass : this.renderer.renderPass; const shaderPassEntry = { label: shaderPass.options.isPrePass ? shaderPass.options.label + " scene pre pass" : shaderPass.options.label + " scene post processing pass", renderPass: outputPass, renderTexture: shaderPass.outputTarget ? shaderPass.outputTarget.renderTexture : null, onBeforeRenderPass, onAfterRenderPass, useCustomRenderPass: null, element: shaderPass, stack: null }; if (shaderPass.renderBundle) { const { renderBundle } = shaderPass; if (renderBundle.meshes.size >= 1) { throwWarning(`${renderBundle.options.label} (${renderBundle.type}): Cannot add more than 1 ShaderPass to a render bundle. This ShaderPass will not be added: ${shaderPass.options.label}`); shaderPass.renderBundle = null; } else renderBundle.addMesh(shaderPass, outputPass); } if (!shaderPass.options.isPrePass) { this.renderPassEntries.postProPass.push(shaderPassEntry); this.renderPassEntries.postProPass.sort((a, b) => { const isPostProA = a.element && !a.element.outputTarget; const renderOrderA = a.element ? a.element.renderOrder : 0; const indexA = a.element ? a.element.index : 0; const isPostProB = b.element && !b.element.outputTarget; const renderOrderB = b.element ? b.element.renderOrder : 0; const indexB = b.element ? b.element.index : 0; if (isPostProA && !isPostProB) return 1; else if (!isPostProA && isPostProB) return -1; else if (renderOrderA !== renderOrderB) return renderOrderA - renderOrderB; else return indexA - indexB; }); } else { this.renderPassEntries.prePass.push(shaderPassEntry); this.renderPassEntries.prePass.sort((a, b) => a.element.renderOrder - b.element.renderOrder || a.element.index - b.element.index); } } /** * Remove a {@link ShaderPass} from our scene {@link renderPassEntries} `prePass` or `postProPass` array. * @param shaderPass - {@link ShaderPass} to remove. */ removeShaderPass(shaderPass) { if (shaderPass.renderBundle) shaderPass.renderBundle.empty(); if (!shaderPass.options.isPrePass) this.renderPassEntries.postProPass = this.renderPassEntries.postProPass.filter((entry) => !entry.element || entry.element.uuid !== shaderPass.uuid); else this.renderPassEntries.prePass = this.renderPassEntries.prePass.filter((entry) => !entry.element || entry.element.uuid !== shaderPass.uuid); } /** * Add a {@link PingPongPlane} to our scene {@link renderPassEntries} pingPong array. * After rendering the {@link PingPongPlane}, we will copy the context current texture into its {@link PingPongPlane#renderTexture | renderTexture} so we'll be able to use it as an input for the next pass. * @see {@link https://codesandbox.io/p/sandbox/webgpu-render-ping-pong-to-texture-use-in-quad-gwjx9p | minimal code example}. * @param pingPongPlane - {@link PingPongPlane} to add. */ addPingPongPlane(pingPongPlane) { this.renderPassEntries.pingPong.push({ label: pingPongPlane.options.label + " scene pass", renderPass: pingPongPlane.outputTarget.renderPass, renderTexture: pingPongPlane.outputTarget.renderTexture, onBeforeRenderPass: null, onAfterRenderPass: (commandEncoder, swapChainTexture) => { this.renderer.copyGPUTextureToTexture(swapChainTexture, pingPongPlane.renderTexture, commandEncoder); }, useCustomRenderPass: null, element: pingPongPlane, stack: null }); if (pingPongPlane.renderBundle) { const { renderBundle } = pingPongPlane; if (renderBundle.meshes.size >= 1) { throwWarning(`${renderBundle.options.label} (${renderBundle.type}): Cannot add more than 1 PingPongPlane to a render bundle. This PingPongPlane will not be added: ${pingPongPlane.options.label}`); pingPongPlane.renderBundle = null; } else renderBundle.addMesh(pingPongPlane, pingPongPlane.outputTarget.renderPass); } this.renderPassEntries.pingPong.sort((a, b) => a.element.renderOrder - b.element.renderOrder); } /** * Remove a {@link PingPongPlane} from our scene {@link renderPassEntries} pingPong array. * @param pingPongPlane - {@link PingPongPlane} to remove. */ removePingPongPlane(pingPongPlane) { if (pingPongPlane.renderBundle) pingPongPlane.renderBundle.empty(); this.renderPassEntries.pingPong = this.renderPassEntries.pingPong.filter((entry) => entry.element.uuid !== pingPongPlane.uuid); } /** * Get any rendered object or {@link RenderTarget} {@link RenderPassEntry}. Useful to override a {@link RenderPassEntry#onBeforeRenderPass | RenderPassEntry onBeforeRenderPass} or {@link RenderPassEntry#onAfterRenderPass | RenderPassEntry onAfterRenderPass} default behavior. * @param object - The object from which we want to get the parentMesh {@link RenderPassEntry} * @returns - The {@link RenderPassEntry} if found. */ getObjectRenderPassEntry(object) { if (object.type === "RenderTarget") return (object.options.isPostTarget ? this.renderPassEntries.postRenderTarget : this.renderPassEntries.renderTarget).find((entry) => entry.renderPass.uuid === object.renderPass.uuid); else if (object.type === "PingPongPlane") return this.renderPassEntries.pingPong.find((entry) => entry.element.uuid === object.uuid); else if (object.type === "ShaderPass") if (object.options.isPrePass) return this.renderPassEntries.prePass.find((entry) => entry.element?.uuid === object.uuid); else return this.renderPassEntries.postProPass.find((entry) => entry.element?.uuid === object.uuid); else { const entryType = object.outputTarget ? "renderTarget" : "screen"; if (object.renderBundle) return this.renderPassEntries[entryType].find((entry) => { return [ ...entry.stack.unProjected.opaque, ...entry.stack.unProjected.transparent, ...entry.stack.projected.opaque, ...entry.stack.projected.transparent ].filter((object) => object.type === "RenderBundle").some((bundle) => { return bundle.meshes.get(object.uuid); }); }); else return this.renderPassEntries[entryType].find((entry) => { return [ ...entry.stack.unProjected.opaque, ...entry.stack.unProjected.transparent, ...entry.stack.projected.opaque, ...entry.stack.projected.transparent ].some((mesh) => mesh.uuid === object.uuid); }); } } /** * Sort transparent projected meshes by their render order or distance to the camera (farther meshes should be drawn first). * @param meshes - Transparent projected meshes array to sort. */ sortTransparentMeshes(meshes) { meshes.sort((meshA, meshB) => { if (meshA.renderOrder !== meshB.renderOrder) return meshA.renderOrder - meshB.renderOrder; if (this.isStackObjectRenderBundle(meshA) || this.isStackObjectRenderBundle(meshB)) return meshA.renderOrder - meshB.renderOrder; meshA.geometry ? posA.copy(meshA.geometry.boundingBox.center).applyMat4(meshA.worldMatrix) : meshA.worldMatrix.getTranslation(posA); meshB.geometry ? posB.copy(meshB.geometry.boundingBox.center).applyMat4(meshB.worldMatrix) : meshB.worldMatrix.getTranslation(posB); const radiusA = meshA.geometry ? meshA.geometry.boundingBox.radius * meshA.worldMatrix.getMaxScaleOnAxis() : 0; const radiusB = meshB.geometry ? meshB.geometry.boundingBox.radius * meshB.worldMatrix.getMaxScaleOnAxis() : 0; return meshB.camera.worldMatrix.getTranslation(camPosB).distance(posB) - radiusB - (meshA.camera.worldMatrix.getTranslation(camPosA).distance(posA) - radiusA); }); } /** * Sort transparent and transmissive meshes and eventually swap transparent meshes stack so they are drawn before or after transmissive meshes based on their distance to camera. */ /** * Here we render a {@link RenderPassEntry}: * - Set its {@link RenderPass#descriptor | renderPass descriptor} view or resolveTarget and get it at as swap chain texture. * - Execute {@link RenderPassEntry#onBeforeRenderPass | onBeforeRenderPass} callback if specified. * - Begin the {@link GPURenderPassEncoder | GPU render pass encoder} using our {@link RenderPass#descriptor | renderPass descriptor}. * - Render the single element if specified or the render pass entry {@link Stack}: draw unprojected opaque / transparent meshes first, then set the {@link core/renderers/GPUCameraRenderer.GPUCameraRenderer#cameraLightsBindGroup | camera and lights bind group} and draw projected opaque / transparent meshes. * - End the {@link GPURenderPassEncoder | GPU render pass encoder}. * - Execute {@link RenderPassEntry#onAfterRenderPass | onAfterRenderPass} callback if specified. * - Reset {@link core/pipelines/PipelineManager.PipelineManager#currentPipelineIndex | pipeline manager current pipeline}. * @param commandEncoder - Current {@link GPUCommandEncoder}. * @param renderPassEntry - {@link RenderPassEntry} to render. */ renderSinglePassEntry(commandEncoder, renderPassEntry) { const swapChainTexture = renderPassEntry.renderPass.updateView(renderPassEntry.renderTexture?.texture); renderPassEntry.onBeforeRenderPass && renderPassEntry.onBeforeRenderPass(commandEncoder, swapChainTexture); if (renderPassEntry.useCustomRenderPass) renderPassEntry.useCustomRenderPass(commandEncoder); else { const pass = renderPassEntry.renderPass.beginRenderPass(commandEncoder); if (!this.renderer.production) pass.pushDebugGroup(renderPassEntry.element ? `${renderPassEntry.element.options.label} render pass using ${renderPassEntry.renderPass.options.label} descriptor` : `Render stack pass using ${renderPassEntry.renderPass.options.label}${renderPassEntry.renderTexture ? " onto " + renderPassEntry.renderTexture.options.label : ""}`); if (renderPassEntry.element) if (renderPassEntry.element.renderBundle) renderPassEntry.element.renderBundle.render(pass); else renderPassEntry.element.render(pass); else if (renderPassEntry.stack) { for (const mesh of renderPassEntry.stack.unProjected.opaque) mesh.render(pass); for (const mesh of renderPassEntry.stack.unProjected.transparent) mesh.render(pass); if (renderPassEntry.stack.projected.opaque.length || renderPassEntry.stack.projected.transparent.length) { for (const mesh of renderPassEntry.stack.projected.opaque) mesh.render(pass); this.sortTransparentMeshes(renderPassEntry.stack.projected.transparent.filter((o) => o instanceof Mesh)); for (const mesh of renderPassEntry.stack.projected.transparent) mesh.render(pass); } } if (!this.renderer.production) pass.popDebugGroup(); pass.end(); } renderPassEntry.onAfterRenderPass && renderPassEntry.onAfterRenderPass(commandEncoder, swapChainTexture); this.renderer.pipelineManager.resetCurrentPipeline(); if (renderPassEntry.renderPass.options.useDepth && renderPassEntry.renderPass.options.renderToSwapChain && !renderPassEntry.renderPass.options.depthReadOnly && renderPassEntry.renderPass.options.depthStoreOp === "store" && renderPassEntry.renderPass.depthTexture.uuid === this.renderer.renderPass.depthTexture?.uuid) this.#shouldLoadDepth = true; } /** * Before actually rendering the scene, update matrix stack and frustum culling checks. Batching these calls greatly improve performance. Called by the {@link renderer} before rendering. */ onBeforeRender() { if ("lights" in this.renderer) this.renderer.lights.forEach((light) => { light.onBeforeRenderScene(); }); this.renderer.meshes.forEach((mesh) => { mesh.onBeforeRenderScene(); }); this.renderer.animations.forEach((targetsAnimation) => targetsAnimation.update()); this.updateMatrixStack(); this.renderer.animations.forEach((targetsAnimation) => targetsAnimation.onAfterUpdate()); for (const mesh of this.renderer.meshes) if ("checkFrustumCulling" in mesh && mesh.visible) mesh.checkFrustumCulling(); } /** * Render our {@link Scene}. * - Execute {@link onBeforeRender} first. * - Then render {@link computePassEntries}. * - And finally render our {@link renderPassEntries}. * @param commandEncoder - Current {@link GPUCommandEncoder}. */ render(commandEncoder) { for (const computePass of this.computePassEntries) { const pass = commandEncoder.beginComputePass(); if (!this.renderer.production) pass.pushDebugGroup(`${computePass.options.label}: begin compute pass`); computePass.render(pass); if (!this.renderer.production) pass.popDebugGroup(); pass.end(); computePass.copyBufferToResult(commandEncoder); this.renderer.pipelineManager.resetCurrentPipeline(); } this.#shouldLoadColors = false; this.#shouldLoadDepth = false; for (const renderPassEntryType in this.renderPassEntries) { if (renderPassEntryType === "postProPass") { this.renderer.renderPass.setDepthReadOnly(false); this.renderer.renderPass.setDepthLoadOp("clear"); this.#shouldLoadColors = true; } this.renderPassEntries[renderPassEntryType].forEach((renderPassEntry) => { if (!this.getRenderPassEntryLength(renderPassEntry)) return; if (renderPassEntryType === "prePass" || renderPassEntryType === "postProPass") renderPassEntry.renderPass.setDepthReadOnly(true); else renderPassEntry.renderPass.setDepthReadOnly(false); if (renderPassEntryType === "postRenderTarget") { this.#shouldLoadColors = false; this.#shouldLoadDepth = false; } if (renderPassEntryType === "postProPass" && renderPassEntry.renderPass.uuid !== this.renderer.postProcessingPass.uuid) this.#shouldLoadColors = false; renderPassEntry.renderPass.setLoadOp(this.#shouldLoadColors ? "load" : "clear"); renderPassEntry.renderPass.setDepthLoadOp(this.#shouldLoadDepth && !renderPassEntry.renderPass.options.depthReadOnly ? "load" : "clear"); this.renderSinglePassEntry(commandEncoder, renderPassEntry); if (renderPassEntryType !== "renderTarget" && renderPassEntryType !== "postRenderTarget") this.#shouldLoadColors = true; }); } } }; //#endregion export { Scene };