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.
672 lines (669 loc) • 23.9 kB
JavaScript
import { isCameraRenderer } from '../renderers/utils.mjs';
import { Vec2 } from '../../math/Vec2.mjs';
import { Mat4 } from '../../math/Mat4.mjs';
import { RenderTarget } from '../renderPasses/RenderTarget.mjs';
import { Sampler } from '../samplers/Sampler.mjs';
import { Mesh } from '../meshes/Mesh.mjs';
import { Vec3 } from '../../math/Vec3.mjs';
import { RenderBundle } from '../renderPasses/RenderBundle.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 _intensity, _bias, _normalBias, _pcfSamples, _isActive, _autoRender, _receivingMeshes, _Shadow_instances, setParameters_fn;
const shadowStruct = {
isActive: {
type: "i32",
value: 0
},
pcfSamples: {
type: "i32",
value: 0
},
bias: {
type: "f32",
value: 0
},
normalBias: {
type: "f32",
value: 0
},
intensity: {
type: "f32",
value: 0
}
};
class Shadow {
/**
* Shadow constructor
* @param renderer - {@link CameraRenderer} or {@link GPUCurtains} used to create this {@link Shadow}.
* @param parameters - {@link ShadowBaseParams} used to create this {@link Shadow}.
*/
constructor(renderer, {
light,
intensity = 1,
bias = 0,
normalBias = 0,
pcfSamples = 1,
depthTextureSize = new Vec2(512),
depthTextureFormat = "depth24plus",
autoRender = true,
useRenderBundle = true
} = {}) {
__privateAdd(this, _Shadow_instances);
/** @ignore */
__privateAdd(this, _intensity);
/** @ignore */
__privateAdd(this, _bias);
/** @ignore */
__privateAdd(this, _normalBias);
/** @ignore */
__privateAdd(this, _pcfSamples);
/** @ignore */
__privateAdd(this, _isActive);
/** @ignore */
__privateAdd(this, _autoRender);
/** Map of all the shadow receiving {@link Mesh}. */
__privateAdd(this, _receivingMeshes);
this.setRenderer(renderer);
this.light = light;
this.index = this.light.index;
this.options = {
light,
intensity,
bias,
normalBias,
pcfSamples,
depthTextureSize,
depthTextureFormat,
useRenderBundle
};
this.sampleCount = 1;
this.castingMeshes = /* @__PURE__ */ new Map();
__privateSet(this, _receivingMeshes, /* @__PURE__ */ new Map());
this.depthMeshes = /* @__PURE__ */ new Map();
this.renderBundle = null;
__privateMethod(this, _Shadow_instances, setParameters_fn).call(this, {
intensity,
bias,
normalBias,
pcfSamples,
depthTextureSize,
depthTextureFormat,
autoRender,
useRenderBundle
});
this.isActive = false;
}
/**
* Set or reset this shadow {@link CameraRenderer}.
* @param renderer - New {@link CameraRenderer} or {@link GPUCurtains} instance to use.
*/
setRenderer(renderer) {
const oldRenderer = this.renderer;
renderer = isCameraRenderer(renderer, this.constructor.name);
this.renderer = renderer;
this.setRendererBinding();
if (this.depthPassTarget) {
this.depthPassTarget.setRenderer(this.renderer);
}
if (this.renderBundle) {
this.renderBundle.setRenderer(this.renderer);
}
this.castingMeshes = /* @__PURE__ */ new Map();
this.renderer.meshes.forEach((mesh) => {
if ("castShadows" in mesh.options && mesh.options.castShadows) {
this.castingMeshes.set(mesh.uuid, mesh);
}
});
this.depthMeshes?.forEach((depthMesh) => {
depthMesh.setRenderer(this.renderer);
});
if (oldRenderer) {
this.reset();
if (__privateGet(this, _autoRender)) {
this.setDepthPass();
}
}
}
/** @ignore */
setRendererBinding() {
this.rendererBinding = null;
}
/**
* Set the parameters and start casting shadows by setting the {@link isActive} setter to `true`.<br>
* Called internally by the associated {@link core/lights/Light.Light | Light} if any shadow parameters are specified when creating it. Can also be called directly.
* @param parameters - Parameters to use for this {@link Shadow}.
*/
cast({
intensity,
bias,
normalBias,
pcfSamples,
depthTextureSize,
depthTextureFormat,
autoRender,
useRenderBundle
} = {}) {
__privateMethod(this, _Shadow_instances, setParameters_fn).call(this, {
intensity,
bias,
normalBias,
pcfSamples,
depthTextureSize,
depthTextureFormat,
autoRender,
useRenderBundle
});
this.isActive = true;
}
/**
* Resend all properties to the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}. Called when the maximum number of corresponding {@link core/lights/Light.Light | lights} has been overflowed or when the {@link renderer} has changed.
*/
reset() {
this.onPropertyChanged("isActive", this.isActive ? 1 : 0);
if (this.isActive) {
this.onPropertyChanged("intensity", this.intensity);
this.onPropertyChanged("bias", this.bias);
this.onPropertyChanged("normalBias", this.normalBias);
this.onPropertyChanged("pcfSamples", this.pcfSamples);
}
}
/**
* Update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding} input value and tell the {@link CameraRenderer#cameraLightsBindGroup | renderer camera, lights and shadows} bind group to update.
* @param propertyKey - name of the property to update.
* @param value - new value of the property.
*/
onPropertyChanged(propertyKey, value) {
if (this.rendererBinding && this.rendererBinding.childrenBindings.length > this.index) {
if (value instanceof Mat4) {
for (let i = 0; i < value.elements.length; i++) {
this.rendererBinding.childrenBindings[this.index].inputs[propertyKey].value[i] = value.elements[i];
}
this.rendererBinding.childrenBindings[this.index].inputs[propertyKey].shouldUpdate = true;
} else if (value instanceof Vec3) {
this.rendererBinding.childrenBindings[this.index].inputs[propertyKey].shouldUpdate = true;
this.rendererBinding.childrenBindings[this.index].inputs[propertyKey].value.copy(value);
} else {
this.rendererBinding.childrenBindings[this.index].inputs[propertyKey].value = value;
}
this.renderer.shouldUpdateCameraLightsBindGroup();
}
}
/**
* Get whether this {@link Shadow} is actually casting shadows.
* @returns - Whether this {@link Shadow} is actually casting shadows.
*/
get isActive() {
return __privateGet(this, _isActive);
}
/**
* Start or stop casting shadows.
* @param value - New active state.
*/
set isActive(value) {
if (!value && this.isActive) {
this.destroy();
} else if (value && !this.isActive) {
if (this.renderer.ready) {
this.init();
} else {
const taskId = this.renderer.onBeforeCommandEncoderCreation.add(
() => {
if (this.renderer.ready) {
this.renderer.onBeforeCommandEncoderCreation.remove(taskId);
this.init();
}
},
{ once: false }
);
}
}
__privateSet(this, _isActive, value);
}
/**
* Get this {@link Shadow} intensity.
* @returns - The {@link Shadow} intensity.
*/
get intensity() {
return __privateGet(this, _intensity);
}
/**
* Set this {@link Shadow} intensity and update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}.
* @param value - The new {@link Shadow} intensity.
*/
set intensity(value) {
__privateSet(this, _intensity, value);
this.onPropertyChanged("intensity", this.intensity);
if (!value) {
this.clearDepthTexture();
}
}
/**
* Get this {@link Shadow} bias.
* @returns - The {@link Shadow} bias.
*/
get bias() {
return __privateGet(this, _bias);
}
/**
* Set this {@link Shadow} bias and update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}.
* @param value - The new {@link Shadow} bias.
*/
set bias(value) {
__privateSet(this, _bias, value);
this.onPropertyChanged("bias", this.bias);
}
/**
* Get this {@link Shadow} normal bias.
* @returns - The {@link Shadow} normal bias.
*/
get normalBias() {
return __privateGet(this, _normalBias);
}
/**
* Set this {@link Shadow} normal bias and update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}.
* @param value - The new {@link Shadow} normal bias.
*/
set normalBias(value) {
__privateSet(this, _normalBias, value);
this.onPropertyChanged("normalBias", this.normalBias);
}
/**
* Get this {@link Shadow} PCF samples count.
* @returns - The {@link Shadow} PCF samples count.
*/
get pcfSamples() {
return __privateGet(this, _pcfSamples);
}
/**
* Set this {@link Shadow} PCF samples count and update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}.
* @param value - The new {@link Shadow} PCF samples count.
*/
set pcfSamples(value) {
__privateSet(this, _pcfSamples, Math.max(1, Math.ceil(value)));
this.onPropertyChanged("pcfSamples", this.pcfSamples);
}
/**
* Set the {@link depthComparisonSampler}, {@link depthTexture}, {@link depthPassTarget} and start rendering to the shadow map.
*/
init() {
if (!this.depthComparisonSampler) {
const samplerExists = this.renderer.samplers.find((sampler) => sampler.name === "depthComparisonSampler");
this.depthComparisonSampler = samplerExists || new Sampler(this.renderer, {
label: "Depth comparison sampler",
name: "depthComparisonSampler",
// we do not want to repeat the shadows
addressModeU: "clamp-to-edge",
addressModeV: "clamp-to-edge",
compare: "less",
minFilter: "linear",
magFilter: "linear",
type: "comparison"
});
}
this.setDepthTexture();
this.depthTextureSize.onChange(() => this.onDepthTextureSizeChanged());
if (!this.depthPassTarget) {
this.createDepthPassTarget();
}
if (this.options.useRenderBundle && !this.renderBundle) {
this.renderBundle = new RenderBundle(this.renderer, {
label: `Depth render bundle for ${this.light.type}Shadow ${this.index}`,
renderPass: this.depthPassTarget.renderPass,
transparent: false,
useBuffer: true,
size: 1
});
}
if (__privateGet(this, _autoRender)) {
this.setDepthPass();
this.onPropertyChanged("isActive", 1);
}
}
/**
* Reset the {@link depthTexture} when the {@link depthTextureSize} changes.
*/
onDepthTextureSizeChanged() {
this.setDepthTexture();
}
/**
* Set or resize the {@link depthTexture} and eventually resize the {@link depthPassTarget} as well.
*/
setDepthTexture() {
if (this.depthTexture && (this.depthTexture.size.width !== this.depthTextureSize.x || this.depthTexture.size.height !== this.depthTextureSize.y)) {
this.depthTexture.options.fixedSize.width = this.depthTextureSize.x;
this.depthTexture.options.fixedSize.height = this.depthTextureSize.y;
this.depthTexture.size.width = this.depthTextureSize.x;
this.depthTexture.size.height = this.depthTextureSize.y;
this.depthTexture.createTexture();
if (this.depthPassTarget) {
this.depthPassTarget.resize();
}
} else if (!this.depthTexture) {
this.createDepthTexture();
}
}
/**
* Create the {@link depthTexture}.
*/
createDepthTexture() {
}
/** Destroy the {@link depthTexture}. */
destroyDepthTexture() {
this.depthTexture?.destroy();
this.depthTexture = null;
this.depthTextureSize.onChange(() => {
});
}
/**
* Clear the content of the depth texture. Called whenever the {@link castingMeshes} {@link Map} is empty after having removed a mesh, or if all {@link castingMeshes} `visible` properties are `false`.
*/
clearDepthTexture() {
if (!this.depthTexture || !this.depthTexture.texture) return;
const commandEncoder = this.renderer.device.createCommandEncoder();
!this.renderer.production && commandEncoder.pushDebugGroup(`Clear ${this.depthTexture.texture.label} command encoder`);
const renderPassDescriptor = {
colorAttachments: [],
depthStencilAttachment: {
view: this.depthTexture.texture.createView({
label: "Clear " + this.depthTexture.texture.label + " view"
}),
depthLoadOp: "clear",
// Clear the depth attachment
depthClearValue: 1,
// Clear to the maximum depth (farthest possible depth)
depthStoreOp: "store"
// Store the cleared depth
}
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
!this.renderer.production && commandEncoder.popDebugGroup();
this.renderer.device.queue.submit([commandEncoder.finish()]);
}
/**
* Create the {@link depthPassTarget}.
*/
createDepthPassTarget() {
this.depthPassTarget = new RenderTarget(this.renderer, {
label: `Depth pass render target for ${this.light.type}Shadow ${this.index}`,
useColorAttachments: false,
depthTexture: this.depthTexture,
sampleCount: this.sampleCount,
autoRender: __privateGet(this, _autoRender)
});
}
/**
* Set our {@link depthPassTarget} corresponding {@link CameraRenderer#scene | scene} render pass entry custom render pass.
*/
setDepthPass() {
const renderPassEntry = this.renderer.scene.getRenderTargetPassEntry(this.depthPassTarget);
renderPassEntry.useCustomRenderPass = (commandEncoder) => {
if (this.renderer.ready) {
this.render(commandEncoder);
}
};
}
/**
* Render the depth pass. Called by the {@link CameraRenderer#scene | scene} when rendering the {@link depthPassTarget} render pass entry, or by the {@link renderOnce} method.<br />
* - Render all the depth meshes.
* @param commandEncoder - {@link GPUCommandEncoder} to use.
*/
render(commandEncoder) {
if (!this.castingMeshes.size || !this.light.intensity || !this.intensity) return;
let shouldRender = false;
for (const [_uuid, mesh] of this.castingMeshes) {
if (mesh.visible) {
shouldRender = true;
break;
}
}
if (!shouldRender) {
this.clearDepthTexture();
return;
}
this.renderDepthPass(commandEncoder);
this.renderer.pipelineManager.resetCurrentPipeline();
}
/**
* Render the shadow map only once. Useful with static scenes if autoRender has been set to `false` to only take one snapshot of the shadow map.
*/
async renderOnce() {
if (!__privateGet(this, _autoRender)) {
this.onPropertyChanged("isActive", 1);
await Promise.all(
[...this.depthMeshes.values()].map(async (depthMesh) => {
depthMesh.setGeometry();
await depthMesh.material.compileMaterial();
})
);
this.renderer.onBeforeRenderScene.add(
(commandEncoder) => {
this.render(commandEncoder);
},
{
once: true
}
);
}
}
/**
* Render all the {@link castingMeshes} into the {@link depthPassTarget}.
* @param commandEncoder - {@link GPUCommandEncoder} to use.
*/
renderDepthPass(commandEncoder) {
this.renderer.pipelineManager.resetCurrentPipeline();
const depthPass = this.depthPassTarget.renderPass.beginRenderPass(commandEncoder);
if (!this.renderer.production)
depthPass.pushDebugGroup(`${this.light.type}Shadow (index: ${this.index}): depth pass`);
if (this.renderBundle) {
this.renderBundle.render(depthPass);
} else {
for (const [uuid, depthMesh] of this.depthMeshes) {
if (!this.castingMeshes.get(uuid)?.visible) {
continue;
}
depthMesh.render(depthPass);
}
}
if (!this.renderer.production) depthPass.popDebugGroup();
depthPass.end();
}
/**
* Get the default depth pass vertex shader for this {@link Shadow}.
* parameters - {@link VertexShaderInputBaseParams} used to compute the output `worldPosition` and `normal` vectors.
* @returns - Depth pass vertex shader.
*/
getDefaultShadowDepthVs({ bindings = [], geometry }) {
return {
/** Returned code. */
code: ` fn main( position: vec4f) -> vec4f { return position; }`
};
}
/**
* Get the default depth pass fragment shader for this {@link Shadow}.
* @returns - A {@link ShaderOptions} if a depth pass fragment shader is needed, `false` otherwise.
*/
getDefaultShadowDepthFs() {
return false;
}
/**
* Patch the given {@link Mesh | mesh} material parameters to create the depth mesh.
* @param mesh - original {@link Mesh | mesh} to use.
* @param parameters - Optional additional parameters to use for the depth mesh.
* @returns - Patched parameters.
*/
patchShadowCastingMeshParams(mesh, parameters = {}) {
parameters = { ...mesh.material.options.rendering, ...parameters };
parameters.targets = [];
const bindings = [];
mesh.material.inputsBindings.forEach((binding) => {
if (binding.name.includes("skin") || binding.name.includes("morphTarget")) {
bindings.push(binding);
}
});
const instancesBinding = mesh.material.getBufferBindingByName("instances");
if (instancesBinding) {
bindings.push(instancesBinding);
}
if (parameters.bindings) {
parameters.bindings = [...bindings, ...parameters.bindings];
} else {
parameters.bindings = [...bindings];
}
if (!parameters.shaders) {
parameters.shaders = {
vertex: this.getDefaultShadowDepthVs({ bindings, geometry: mesh.geometry }),
fragment: this.getDefaultShadowDepthFs()
};
}
return parameters;
}
/**
* Add a {@link Mesh} to the shadow map. Internally called by the {@link Mesh} if its `castShadows` parameters has been set to `true`, but can also be called externally to selectively cast shadows or to add specific parameters (such as custom depth pass shaders).
* - {@link patchShadowCastingMeshParams | Patch} the parameters.
* - Create a new depth {@link Mesh} with the patched parameters.
* - Add the {@link Mesh} to the {@link castingMeshes} Map.
* @param mesh - {@link Mesh} to add to the shadow map.
* @param parameters - Optional {@link RenderMaterialParams | parameters} to use for the depth mesh.
*/
addShadowCastingMesh(mesh, parameters = {}) {
if (this.castingMeshes.get(mesh.uuid)) return;
mesh.options.castShadows = true;
parameters = this.patchShadowCastingMeshParams(mesh, parameters);
if (this.depthMeshes.get(mesh.uuid)) {
this.depthMeshes.get(mesh.uuid).remove();
this.depthMeshes.delete(mesh.uuid);
}
if (this.renderBundle) {
this.renderBundle.size = this.depthMeshes.size + 1;
}
const depthMesh = new Mesh(this.renderer, {
label: `${this.light.type}Shadow (index: ${this.index}) ${mesh.options.label} depth mesh`,
...parameters,
geometry: mesh.geometry,
// explicitly set empty output targets
// we just want to write to the depth texture
targets: [],
outputTarget: this.depthPassTarget,
frustumCulling: false,
// draw shadow even if original mesh is hidden
autoRender: __privateGet(this, _autoRender),
...this.renderBundle && { renderBundle: this.renderBundle }
});
if (!__privateGet(this, _autoRender) && this.renderBundle) {
this.renderBundle.meshes.set(depthMesh.uuid, depthMesh);
}
depthMesh.parent = mesh;
this.depthMeshes.set(mesh.uuid, depthMesh);
this.castingMeshes.set(mesh.uuid, mesh);
}
/**
* Add a shadow receiving {@link Mesh} to the #receivingMeshes {@link Map}.
* @param mesh - Shadow receiving {@link Mesh} to add.
*/
addShadowReceivingMesh(mesh) {
__privateGet(this, _receivingMeshes).set(mesh.uuid, mesh);
}
/**
* Remove a shadow receiving {@link Mesh} from the #receivingMeshes {@link Map}.
* @param mesh - Shadow receiving {@link Mesh} to remove.
*/
removeShadowReceivingMesh(mesh) {
__privateGet(this, _receivingMeshes).delete(mesh.uuid);
if (__privateGet(this, _receivingMeshes).size === 0 && !this.isActive) {
this.destroyDepthTexture();
}
}
/**
* Remove a {@link Mesh} from the shadow map and destroy its depth mesh.
* @param mesh - {@link Mesh} to remove.
*/
removeMesh(mesh) {
const depthMesh = this.depthMeshes.get(mesh.uuid);
if (depthMesh) {
depthMesh.remove();
this.depthMeshes.delete(mesh.uuid);
}
this.castingMeshes.delete(mesh.uuid);
if (this.castingMeshes.size === 0) {
this.clearDepthTexture();
}
}
/**
* If one of the {@link castingMeshes} had its geometry change, update the corresponding depth mesh geometry as well.
* @param mesh - Original {@link Mesh} which geometry just changed.
* @param geometry - New {@link Mesh} {@link Geometry} to use.
*/
updateMeshGeometry(mesh, geometry) {
const depthMesh = this.depthMeshes.get(mesh.uuid);
if (depthMesh) {
depthMesh.useGeometry(geometry);
}
}
/**
* Destroy the {@link Shadow}.
*/
destroy() {
this.onPropertyChanged("isActive", 0);
__privateSet(this, _isActive, false);
if (this.renderBundle) {
this.renderBundle.destroy();
}
this.castingMeshes.forEach((mesh) => this.removeMesh(mesh));
this.castingMeshes = /* @__PURE__ */ new Map();
this.depthMeshes = /* @__PURE__ */ new Map();
this.depthPassTarget?.destroy();
if (__privateGet(this, _receivingMeshes).size === 0) {
this.destroyDepthTexture();
}
}
}
_intensity = new WeakMap();
_bias = new WeakMap();
_normalBias = new WeakMap();
_pcfSamples = new WeakMap();
_isActive = new WeakMap();
_autoRender = new WeakMap();
_receivingMeshes = new WeakMap();
_Shadow_instances = new WeakSet();
// TODO unused for now, should we really consider this case?
// updateIndex(index: number) {
// const shouldUpdateIndex = index !== this.index
//
// this.index = index
//
// if (shouldUpdateIndex) {
// throwWarning(`This ${this.light.type}Shadow index has changed, the shaders need to be recreated`)
// }
// }
/**
* Set the {@link Shadow} parameters.
* @param parameters - Parameters to use for this {@link Shadow}.
* @private
*/
setParameters_fn = function({
intensity = 1,
bias = 0,
normalBias = 0,
pcfSamples = 1,
depthTextureSize = new Vec2(512),
depthTextureFormat = "depth24plus",
autoRender = true,
useRenderBundle = true
} = {}) {
this.intensity = intensity;
this.bias = bias;
this.normalBias = normalBias;
this.pcfSamples = pcfSamples;
this.depthTextureSize = depthTextureSize;
this.depthTextureFormat = depthTextureFormat;
__privateSet(this, _autoRender, autoRender);
this.options.useRenderBundle = useRenderBundle;
};
export { Shadow, shadowStruct };