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
JavaScript
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 };