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.

439 lines (438 loc) 17.5 kB
import { isCameraRenderer } from "../../renderers/utils.mjs"; import { BufferBinding } from "../../bindings/BufferBinding.mjs"; import { Texture } from "../../textures/Texture.mjs"; import { DOMFrustum } from "../../DOM/DOMFrustum.mjs"; import { getDefaultProjectedVertexShaderCode } from "../../shaders/full/vertex/get-default-projected-vertex-shader-code.mjs"; import { MeshBaseMixin } from "./MeshBaseMixin.mjs"; import { getDefaultNormalFragmentCode } from "../../shaders/full/fragment/get-default-normal-fragment-code.mjs"; import { getPCFDirectionalShadowContribution } from "../../shaders/chunks/fragment/head/get-PCF-directional-shadow-contribution.mjs"; import { getPCFDirectionalShadows } from "../../shaders/chunks/fragment/head/get-PCF-directional-shadows.mjs"; import { getPCFPointShadowContribution } from "../../shaders/chunks/fragment/head/get-PCF-point-shadow-contribution.mjs"; import { getPCFPointShadows } from "../../shaders/chunks/fragment/head/get-PCF-point-shadows.mjs"; import { getPCFSpotShadows } from "../../shaders/chunks/fragment/head/get-PCF-spot-shadows.mjs"; import { getPCFSpotShadowContribution } from "../../shaders/chunks/fragment/head/get-PCF-spot-shadow-contribution.mjs"; import { getPCFBaseShadowContribution } from "../../shaders/chunks/fragment/head/get-PCF-base-shadow-contribution.mjs"; //#region src/core/meshes/mixins/ProjectedMeshBaseMixin.ts /** @const - Default ProjectedMesh parameters to merge with user defined parameters */ const defaultProjectedMeshParams = { frustumCulling: "OBB", DOMFrustumMargins: { top: 0, right: 0, bottom: 0, left: 0 }, receiveShadows: false, castShadows: false, transmissive: false }; /** * Used to add the properties and methods defined in {@link ProjectedMeshBaseClass} to the {@link MeshBaseClass} and mix it with a given Base of type {@link ProjectedObject3D} or {@link curtains/objects3D/DOMObject3D.DOMObject3D | DOMObject3D}. * @param Base - the class to mix onto, should be of {@link ProjectedObject3D} or {@link curtains/objects3D/DOMObject3D.DOMObject3D | DOMObject3D} type * @returns - the mixed classes, creating a Projected Mesh. */ function ProjectedMeshBaseMixin(Base) { /** * ProjectedMeshBase defines our base properties and methods */ return class ProjectedMeshBase extends MeshBaseMixin(Base) { /** * ProjectedMeshBase constructor * * @typedef MeshBaseArrayParams * @type {array} * @property {(CameraRenderer|GPUCurtains)} 0 - our renderer class object * @property {(string|HTMLElement|null)} 1 - the DOM HTML Element that can be bound to a Mesh * @property {ProjectedMeshParameters} 2 - Projected Mesh parameters * * @param {MeshBaseArrayParams} params - our MeshBaseMixin parameters */ constructor(...params) { super(params[0], params[1], { ...defaultProjectedMeshParams, ...params[2], useProjection: true }); this._onReEnterViewCallback = () => {}; this._onLeaveViewCallback = () => {}; let renderer = params[0]; const parameters = { ...defaultProjectedMeshParams, ...params[2], useProjection: true }; this.type = "MeshTransformed"; renderer = isCameraRenderer(renderer, parameters.label ? parameters.label + " " + this.type : this.type); this.renderer = renderer; const { frustumCulling, DOMFrustumMargins, receiveShadows, castShadows, transmissive } = parameters; this.options = { ...this.options ?? {}, frustumCulling, DOMFrustumMargins, receiveShadows, castShadows, transmissive }; if (this.options.castShadows) this.renderer.shadowCastingLights.forEach((light) => { if (light.shadow.isActive) light.shadow.addShadowCastingMesh(this); }); this.setDOMFrustum(); } /** * Set or reset this Mesh {@link renderer}. * @param renderer - New {@link CameraRenderer} or {@link GPUCurtains} instance to use. */ setRenderer(renderer) { if (this.renderer && this.options.castShadows) this.renderer.shadowCastingLights.forEach((light) => { if (light.shadow.isActive) light.shadow.removeMesh(this); }); if (this.options.transmissive) { renderer = isCameraRenderer(renderer, this.options.label + " " + renderer.type); renderer.createTransmissionTarget(); let transmissiveTexture = this.material.textures.find((texture) => texture.options.name === "transmissionBackgroundTexture"); if (transmissiveTexture) transmissiveTexture.copy(renderer.transmissionTarget.texture); } super.setRenderer(renderer); this.camera = this.renderer.camera; if (this.options.castShadows) this.renderer.shadowCastingLights.forEach((light) => { if (light.shadow.isActive) light.shadow.addShadowCastingMesh(this); }); } /** * 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 (this.renderBundle && renderBundle && this.renderBundle.uuid === renderBundle.uuid) return; const hasRenderBundle = !!this.renderBundle; const bindGroup = this.material.getBindGroupByBindingName("matrices"); const matrices = this.material.getBufferBindingByName("matrices"); if (this.renderBundle && !renderBundle && matrices.parent) { matrices.parent = null; matrices.shouldResetBindGroup = true; bindGroup.createBindingBuffer(matrices); } super.setRenderBundle(renderBundle, updateScene); if (this.renderBundle && this.renderBundle.binding) { if (hasRenderBundle) bindGroup.destroyBufferBinding(matrices); matrices.options.offset = this.renderBundle.meshes.size - 1; matrices.parent = this.renderBundle.binding; matrices.shouldResetBindGroup = true; } } /** * Reset the {@link BufferBinding | matrices buffer binding} parent and offset and tell its bind group to update. * @param offset - New offset to use in the parent {@link RenderBundle#binding | RenderBundle binding}. */ patchRenderBundleBinding(offset = 0) { const matrices = this.material.getBufferBindingByName("matrices"); matrices.options.offset = offset; matrices.parent = this.renderBundle.binding; matrices.shouldResetBindGroup = true; } /** * Set default shaders if one or both of them are missing. * Can also patch the fragment shader if the mesh should receive shadows. */ setShaders() { const { shaders } = this.options; if (!shaders) this.options.shaders = { vertex: { code: getDefaultProjectedVertexShaderCode, entryPoint: "main" }, fragment: { code: getDefaultNormalFragmentCode, entryPoint: "main" } }; else { if (!shaders.vertex || !shaders.vertex.code) shaders.vertex = { code: getDefaultProjectedVertexShaderCode, entryPoint: "main" }; if (shaders.fragment === void 0 || shaders.fragment && !shaders.fragment.code) shaders.fragment = { code: getDefaultNormalFragmentCode, entryPoint: "main" }; } if (this.options.receiveShadows) { this.renderer.shadowCastingLights.forEach((light) => { light.shadow.addShadowReceivingMesh(this); }); if (this.renderer.shadowCastingLights.find((light) => light.shadow.isActive) && shaders.fragment) shaders.fragment.code = getPCFBaseShadowContribution + getPCFDirectionalShadows(this.renderer) + getPCFDirectionalShadowContribution + getPCFPointShadows(this.renderer) + getPCFPointShadowContribution + getPCFSpotShadows(this.renderer) + getPCFSpotShadowContribution + shaders.fragment.code; } return shaders; } /** * Set or update the Projected Mesh {@link Geometry} * @param geometry - new {@link Geometry} to use */ useGeometry(geometry) { super.useGeometry(geometry); if (this.renderer && this.options.castShadows) this.renderer.shadowCastingLights.forEach((light) => { if (light.shadow.isActive) light.shadow.updateMeshGeometry(this, geometry); }); if (this.domFrustum) this.domFrustum.boundingBox = this.geometry.boundingBox; this.shouldUpdateMatrixStack(); } /** * Set the Mesh frustum culling */ setDOMFrustum() { this.domFrustum = new DOMFrustum({ boundingBox: this.geometry?.boundingBox, modelViewProjectionMatrix: this.modelViewProjectionMatrix, containerBoundingRect: this.renderer.boundingRect, DOMFrustumMargins: this.options.DOMFrustumMargins, onReEnterView: () => { this._onReEnterViewCallback && this._onReEnterViewCallback(); }, onLeaveView: () => { this._onLeaveViewCallback && this._onLeaveViewCallback(); } }); this.DOMFrustumMargins = this.domFrustum.DOMFrustumMargins; this.frustumCulling = this.options.frustumCulling; } /** * Get whether the Mesh is currently in the {@link camera} frustum. * @readonly */ get isInFrustum() { return this.domFrustum.isIntersecting; } /** * Hook used to clean up parameters before sending them to the material. * @param parameters - parameters to clean before sending them to the {@link core/materials/RenderMaterial.RenderMaterial | RenderMaterial} * @returns - cleaned parameters */ cleanupRenderMaterialParameters(parameters) { delete parameters.castShadows; delete parameters.DOMFrustumMargins; delete parameters.frustumCulling; delete parameters.receiveShadows; delete parameters.transmissive; if (this.options.receiveShadows) { const depthTextures = []; let depthSamplers = []; this.renderer.shadowCastingLights.forEach((light) => { if (light.shadow.isActive) { depthTextures.push(light.shadow.depthTexture); depthSamplers.push(light.shadow.depthComparisonSampler); } }); depthSamplers = depthSamplers.filter((sampler, i, array) => array.findIndex((s) => s.uuid === sampler.uuid) === i); if (parameters.textures) parameters.textures = [...parameters.textures, ...depthTextures]; else parameters.textures = depthTextures; if (parameters.samplers) parameters.samplers = [...parameters.samplers, ...depthSamplers]; else parameters.samplers = depthSamplers; } if (this.options.transmissive) { this.renderer.createTransmissionTarget(); const transmissionTexture = new Texture(this.renderer, { label: this.options.label + " transmission texture", name: "transmissionBackgroundTexture", fromTexture: this.renderer.transmissionTarget.texture }); if (parameters.textures) parameters.textures = [...parameters.textures, transmissionTexture]; else parameters.textures = [transmissionTexture]; if (parameters.samplers) parameters.samplers = [...parameters.samplers, this.renderer.transmissionTarget.sampler]; else parameters.samplers = [this.renderer.transmissionTarget.sampler]; } return super.cleanupRenderMaterialParameters(parameters); } /** * Set a Mesh matrices uniforms inputs then call {@link MeshBaseClass} super method * @param meshParameters - {@link RenderMaterialParams | RenderMaterial parameters} */ setMaterial(meshParameters) { const matricesUniforms = { label: "Matrices", name: "matrices", visibility: ["vertex", "fragment"], minOffset: this.renderer.device ? this.renderer.device.limits.minUniformBufferOffsetAlignment : 256, struct: { model: { type: "mat4x4f", value: this.worldMatrix }, modelView: { type: "mat4x4f", value: this.modelViewMatrix }, normal: { type: "mat3x3f", value: this.normalMatrix } } }; if (this.options.renderBundle && this.options.renderBundle.binding) { matricesUniforms.parent = this.options.renderBundle.binding; matricesUniforms.offset = this.options.renderBundle.meshes.size; } const meshTransformationBinding = new BufferBinding(matricesUniforms); if (!meshParameters.bindings) meshParameters.bindings = []; meshParameters.bindings.unshift(meshTransformationBinding); super.setMaterial(meshParameters); } /** * Update this Mesh camera {@link BindGroup}. Useful if the Mesh needs to be rendered with a different {@link core/renderers/GPUCameraRenderer.GPUCameraRenderer#camera | camera} than the {@link CameraRenderer} one. * @param cameraBindGroup - New camera {@link BindGroup} to use. Should be a clon from the {@link CameraRenderer} one. */ setCameraBindGroup(cameraBindGroup) { if (this.material && this.material.useCameraBindGroup && this.material.bindGroups.length) this.material.bindGroups[0] = cameraBindGroup; } /** * Get the visible property value. */ get visible() { return super.visible; } /** * Set the visible property value. * @param value - New visibility value. */ set visible(value) { this.shouldUpdateMatrixStack(); super.visible = value; } /** * Get the parent visibilty property value. */ get parentVisibility() { return super.parentVisibility; } /** * Set the parent visibilty property value. Should not be used directly. * @param value - New parent visibility value. */ set parentVisibility(value) { super.parentVisibility = value; if (this.renderBundle) this.renderBundle.ready = false; } /** * Resize our {@link ProjectedMeshBaseClass} * @param boundingRect - the new bounding rectangle */ resize(boundingRect) { if (this.domFrustum) this.domFrustum.setContainerBoundingRect(this.renderer.boundingRect); super.resize(boundingRect); } /** * Get our {@link DOMFrustum} projected bounding rectangle * @readonly */ get projectedBoundingRect() { return this.domFrustum?.projectedBoundingRect; } /** * Assign a callback function to _onReEnterViewCallback * @param callback - callback to run when {@link ProjectedMeshBaseClass} is reentering the view frustum * @returns - our Mesh */ onReEnterView(callback) { if (callback) this._onReEnterViewCallback = callback; return this; } /** * Assign a callback function to _onLeaveViewCallback * @param callback - callback to run when {@link ProjectedMeshBaseClass} is leaving the view frustum * @returns - our Mesh */ onLeaveView(callback) { if (callback) this._onLeaveViewCallback = callback; return this; } /** * Get the geometry bounding sphere in clip space. * @readonly */ get clipSpaceBoundingSphere() { const { center, radius, min, max } = this.geometry.boundingBox; const translation = this.worldMatrix.getTranslation(); const maxWorldRadius = radius * this.worldMatrix.getMaxScaleOnAxis(); const cMin = center.clone().add(translation); cMin.z += min.z; const cMax = center.clone().add(translation); cMax.z += max.z; const sMin = cMin.clone(); sMin.y += maxWorldRadius; const sMax = cMax.clone(); sMax.y += maxWorldRadius; cMin.applyMat4(this.camera.viewProjectionMatrix); cMax.applyMat4(this.camera.viewProjectionMatrix); sMin.applyMat4(this.camera.viewProjectionMatrix); sMax.applyMat4(this.camera.viewProjectionMatrix); const rMin = cMin.distance(sMin); const rMax = cMax.distance(sMax); const rectMin = { xMin: cMin.x - rMin, xMax: cMin.x + rMin, yMin: cMin.y - rMin, yMax: cMin.y + rMin }; const rectMax = { xMin: cMax.x - rMax, xMax: cMax.x + rMax, yMin: cMax.y - rMax, yMax: cMax.y + rMax }; const rect = { xMin: Math.min(rectMin.xMin, rectMax.xMin), yMin: Math.min(rectMin.yMin, rectMax.yMin), xMax: Math.max(rectMin.xMax, rectMax.xMax), yMax: Math.max(rectMin.yMax, rectMax.yMax) }; const sphereCenter = cMax.add(cMin).multiplyScalar(.5).clone(); sphereCenter.x = (rect.xMax + rect.xMin) / 2; sphereCenter.y = (rect.yMax + rect.yMin) / 2; return { center: sphereCenter, radius: Math.max(rect.xMax - rect.xMin, rect.yMax - rect.yMin) * .5 }; } /** * Check if the Mesh lies inside the {@link camera} view frustum or not using the test defined by {@link frustumCulling}. */ checkFrustumCulling() { if (this.matricesNeedUpdate) { if (this.domFrustum && this.frustumCulling) { if (this.frustumCulling === "sphere") this.domFrustum.setDocumentCoordsFromClipSpaceSphere(this.clipSpaceBoundingSphere); else this.domFrustum.setDocumentCoordsFromClipSpaceOBB(); this.domFrustum.intersectsContainer(); } } } /** * Tell our matrices bindings to update if needed and call {@link MeshBaseClass#onBeforeRenderPass | Mesh base onBeforeRenderPass} super. */ onBeforeRenderPass() { if (this.material && this.matricesNeedUpdate) this.material.shouldUpdateInputsBindings("matrices"); super.onBeforeRenderPass(); } /** * Render our Mesh if the {@link RenderMaterial} is ready and if it is not frustum culled. * @param pass - current render pass */ onRenderPass(pass) { if (!this.ready) return; if (this.domFrustum && this.domFrustum.isIntersecting || !this.frustumCulling) this.renderPass(pass); } /** * Destroy the Mesh, and handle shadow casting or receiving meshes. */ destroy() { if (this.options.castShadows) this.renderer.shadowCastingLights.forEach((light) => { if (light.shadow.isActive) light.shadow.removeMesh(this); }); if (this.options.receiveShadows) this.renderer.shadowCastingLights.forEach((light) => { light.shadow.removeShadowReceivingMesh(this); }); super.destroy(); } }; } //#endregion export { ProjectedMeshBaseMixin };