@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
1,102 lines (1,101 loc) • 78.4 kB
JavaScript
import { Matrix, Vector3, Vector2 } from "../../Maths/math.vector.js";
import { Color4 } from "../../Maths/math.color.js";
import { VertexBuffer } from "../../Buffers/buffer.js";
import { Light } from "../../Lights/light.js";
import { Texture } from "../../Materials/Textures/texture.js";
import { RenderTargetTexture } from "../../Materials/Textures/renderTargetTexture.js";
import { PostProcess } from "../../PostProcesses/postProcess.js";
import { BlurPostProcess } from "../../PostProcesses/blurPostProcess.js";
import { Observable } from "../../Misc/observable.js";
import { _WarnImport } from "../../Misc/devTools.js";
import { EffectFallbacks } from "../../Materials/effectFallbacks.js";
import { RenderingManager } from "../../Rendering/renderingManager.js";
import { DrawWrapper } from "../../Materials/drawWrapper.js";
import { addClipPlaneUniforms, bindClipPlane, prepareStringDefinesForClipPlanes } from "../../Materials/clipPlaneMaterialHelper.js";
import { BindMorphTargetParameters, BindSceneUniformBuffer, PrepareDefinesAndAttributesForMorphTargets, PushAttributesForInstances, } from "../../Materials/materialHelper.functions.js";
/**
* Default implementation IShadowGenerator.
* This is the main object responsible of generating shadows in the framework.
* Documentation: https://doc.babylonjs.com/features/featuresDeepDive/lights/shadows
* @see [WebGL](https://playground.babylonjs.com/#IFYDRS#0)
* @see [WebGPU](https://playground.babylonjs.com/#IFYDRS#835)
*/
export class ShadowGenerator {
/**
* Gets the bias: offset applied on the depth preventing acnea (in light direction).
*/
get bias() {
return this._bias;
}
/**
* Sets the bias: offset applied on the depth preventing acnea (in light direction).
*/
set bias(bias) {
this._bias = bias;
}
/**
* Gets the normalBias: offset applied on the depth preventing acnea (along side the normal direction and proportional to the light/normal angle).
*/
get normalBias() {
return this._normalBias;
}
/**
* Sets the normalBias: offset applied on the depth preventing acnea (along side the normal direction and proportional to the light/normal angle).
*/
set normalBias(normalBias) {
this._normalBias = normalBias;
}
/**
* Gets the blur box offset: offset applied during the blur pass.
* Only useful if useKernelBlur = false
*/
get blurBoxOffset() {
return this._blurBoxOffset;
}
/**
* Sets the blur box offset: offset applied during the blur pass.
* Only useful if useKernelBlur = false
*/
set blurBoxOffset(value) {
if (this._blurBoxOffset === value) {
return;
}
this._blurBoxOffset = value;
this._disposeBlurPostProcesses();
}
/**
* Gets the blur scale: scale of the blurred texture compared to the main shadow map.
* 2 means half of the size.
*/
get blurScale() {
return this._blurScale;
}
/**
* Sets the blur scale: scale of the blurred texture compared to the main shadow map.
* 2 means half of the size.
*/
set blurScale(value) {
if (this._blurScale === value) {
return;
}
this._blurScale = value;
this._disposeBlurPostProcesses();
}
/**
* Gets the blur kernel: kernel size of the blur pass.
* Only useful if useKernelBlur = true
*/
get blurKernel() {
return this._blurKernel;
}
/**
* Sets the blur kernel: kernel size of the blur pass.
* Only useful if useKernelBlur = true
*/
set blurKernel(value) {
if (this._blurKernel === value) {
return;
}
this._blurKernel = value;
this._disposeBlurPostProcesses();
}
/**
* Gets whether the blur pass is a kernel blur (if true) or box blur.
* Only useful in filtered mode (useBlurExponentialShadowMap...)
*/
get useKernelBlur() {
return this._useKernelBlur;
}
/**
* Sets whether the blur pass is a kernel blur (if true) or box blur.
* Only useful in filtered mode (useBlurExponentialShadowMap...)
*/
set useKernelBlur(value) {
if (this._useKernelBlur === value) {
return;
}
this._useKernelBlur = value;
this._disposeBlurPostProcesses();
}
/**
* Gets the depth scale used in ESM mode.
*/
get depthScale() {
return this._depthScale !== undefined ? this._depthScale : this._light.getDepthScale();
}
/**
* Sets the depth scale used in ESM mode.
* This can override the scale stored on the light.
*/
set depthScale(value) {
this._depthScale = value;
}
_validateFilter(filter) {
return filter;
}
/**
* Gets the current mode of the shadow generator (normal, PCF, ESM...).
* The returned value is a number equal to one of the available mode defined in ShadowMap.FILTER_x like _FILTER_NONE
*/
get filter() {
return this._filter;
}
/**
* Sets the current mode of the shadow generator (normal, PCF, ESM...).
* The returned value is a number equal to one of the available mode defined in ShadowMap.FILTER_x like _FILTER_NONE
*/
set filter(value) {
value = this._validateFilter(value);
// Blurring the cubemap is going to be too expensive. Reverting to unblurred version
if (this._light.needCube()) {
if (value === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP) {
this.useExponentialShadowMap = true;
return;
}
else if (value === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) {
this.useCloseExponentialShadowMap = true;
return;
}
// PCF on cubemap would also be expensive
else if (value === ShadowGenerator.FILTER_PCF || value === ShadowGenerator.FILTER_PCSS) {
this.usePoissonSampling = true;
return;
}
}
// Weblg1 fallback for PCF.
if (value === ShadowGenerator.FILTER_PCF || value === ShadowGenerator.FILTER_PCSS) {
if (!this._scene.getEngine()._features.supportShadowSamplers) {
this.usePoissonSampling = true;
return;
}
}
if (this._filter === value) {
return;
}
this._filter = value;
this._disposeBlurPostProcesses();
this._applyFilterValues();
this._light._markMeshesAsLightDirty();
}
/**
* Gets if the current filter is set to Poisson Sampling.
*/
get usePoissonSampling() {
return this.filter === ShadowGenerator.FILTER_POISSONSAMPLING;
}
/**
* Sets the current filter to Poisson Sampling.
*/
set usePoissonSampling(value) {
const filter = this._validateFilter(ShadowGenerator.FILTER_POISSONSAMPLING);
if (!value && this.filter !== ShadowGenerator.FILTER_POISSONSAMPLING) {
return;
}
this.filter = value ? filter : ShadowGenerator.FILTER_NONE;
}
/**
* Gets if the current filter is set to ESM.
*/
get useExponentialShadowMap() {
return this.filter === ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP;
}
/**
* Sets the current filter is to ESM.
*/
set useExponentialShadowMap(value) {
const filter = this._validateFilter(ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP);
if (!value && this.filter !== ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP) {
return;
}
this.filter = value ? filter : ShadowGenerator.FILTER_NONE;
}
/**
* Gets if the current filter is set to filtered ESM.
*/
get useBlurExponentialShadowMap() {
return this.filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP;
}
/**
* Gets if the current filter is set to filtered ESM.
*/
set useBlurExponentialShadowMap(value) {
const filter = this._validateFilter(ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP);
if (!value && this.filter !== ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP) {
return;
}
this.filter = value ? filter : ShadowGenerator.FILTER_NONE;
}
/**
* Gets if the current filter is set to "close ESM" (using the inverse of the
* exponential to prevent steep falloff artifacts).
*/
get useCloseExponentialShadowMap() {
return this.filter === ShadowGenerator.FILTER_CLOSEEXPONENTIALSHADOWMAP;
}
/**
* Sets the current filter to "close ESM" (using the inverse of the
* exponential to prevent steep falloff artifacts).
*/
set useCloseExponentialShadowMap(value) {
const filter = this._validateFilter(ShadowGenerator.FILTER_CLOSEEXPONENTIALSHADOWMAP);
if (!value && this.filter !== ShadowGenerator.FILTER_CLOSEEXPONENTIALSHADOWMAP) {
return;
}
this.filter = value ? filter : ShadowGenerator.FILTER_NONE;
}
/**
* Gets if the current filter is set to filtered "close ESM" (using the inverse of the
* exponential to prevent steep falloff artifacts).
*/
get useBlurCloseExponentialShadowMap() {
return this.filter === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP;
}
/**
* Sets the current filter to filtered "close ESM" (using the inverse of the
* exponential to prevent steep falloff artifacts).
*/
set useBlurCloseExponentialShadowMap(value) {
const filter = this._validateFilter(ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP);
if (!value && this.filter !== ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) {
return;
}
this.filter = value ? filter : ShadowGenerator.FILTER_NONE;
}
/**
* Gets if the current filter is set to "PCF" (percentage closer filtering).
*/
get usePercentageCloserFiltering() {
return this.filter === ShadowGenerator.FILTER_PCF;
}
/**
* Sets the current filter to "PCF" (percentage closer filtering).
*/
set usePercentageCloserFiltering(value) {
const filter = this._validateFilter(ShadowGenerator.FILTER_PCF);
if (!value && this.filter !== ShadowGenerator.FILTER_PCF) {
return;
}
this.filter = value ? filter : ShadowGenerator.FILTER_NONE;
}
/**
* Gets the PCF or PCSS Quality.
* Only valid if usePercentageCloserFiltering or usePercentageCloserFiltering is true.
*/
get filteringQuality() {
return this._filteringQuality;
}
/**
* Sets the PCF or PCSS Quality.
* Only valid if usePercentageCloserFiltering or usePercentageCloserFiltering is true.
*/
set filteringQuality(filteringQuality) {
if (this._filteringQuality === filteringQuality) {
return;
}
this._filteringQuality = filteringQuality;
this._disposeBlurPostProcesses();
this._applyFilterValues();
this._light._markMeshesAsLightDirty();
}
/**
* Gets if the current filter is set to "PCSS" (contact hardening).
*/
get useContactHardeningShadow() {
return this.filter === ShadowGenerator.FILTER_PCSS;
}
/**
* Sets the current filter to "PCSS" (contact hardening).
*/
set useContactHardeningShadow(value) {
const filter = this._validateFilter(ShadowGenerator.FILTER_PCSS);
if (!value && this.filter !== ShadowGenerator.FILTER_PCSS) {
return;
}
this.filter = value ? filter : ShadowGenerator.FILTER_NONE;
}
/**
* Gets the Light Size (in shadow map uv unit) used in PCSS to determine the blocker search area and the penumbra size.
* Using a ratio helps keeping shape stability independently of the map size.
*
* It does not account for the light projection as it was having too much
* instability during the light setup or during light position changes.
*
* Only valid if useContactHardeningShadow is true.
*/
get contactHardeningLightSizeUVRatio() {
return this._contactHardeningLightSizeUVRatio;
}
/**
* Sets the Light Size (in shadow map uv unit) used in PCSS to determine the blocker search area and the penumbra size.
* Using a ratio helps keeping shape stability independently of the map size.
*
* It does not account for the light projection as it was having too much
* instability during the light setup or during light position changes.
*
* Only valid if useContactHardeningShadow is true.
*/
set contactHardeningLightSizeUVRatio(contactHardeningLightSizeUVRatio) {
this._contactHardeningLightSizeUVRatio = contactHardeningLightSizeUVRatio;
}
/** Gets or sets the actual darkness of a shadow */
get darkness() {
return this._darkness;
}
set darkness(value) {
this.setDarkness(value);
}
/**
* Returns the darkness value (float). This can only decrease the actual darkness of a shadow.
* 0 means strongest and 1 would means no shadow.
* @returns the darkness.
*/
getDarkness() {
return this._darkness;
}
/**
* Sets the darkness value (float). This can only decrease the actual darkness of a shadow.
* @param darkness The darkness value 0 means strongest and 1 would means no shadow.
* @returns the shadow generator allowing fluent coding.
*/
setDarkness(darkness) {
if (darkness >= 1.0) {
this._darkness = 1.0;
}
else if (darkness <= 0.0) {
this._darkness = 0.0;
}
else {
this._darkness = darkness;
}
return this;
}
/** Gets or sets the ability to have transparent shadow */
get transparencyShadow() {
return this._transparencyShadow;
}
set transparencyShadow(value) {
this.setTransparencyShadow(value);
}
/**
* Sets the ability to have transparent shadow (boolean).
* @param transparent True if transparent else False
* @returns the shadow generator allowing fluent coding
*/
setTransparencyShadow(transparent) {
this._transparencyShadow = transparent;
return this;
}
/**
* Gets the main RTT containing the shadow map (usually storing depth from the light point of view).
* @returns The render target texture if present otherwise, null
*/
getShadowMap() {
return this._shadowMap;
}
/**
* Gets the RTT used during rendering (can be a blurred version of the shadow map or the shadow map itself).
* @returns The render target texture if the shadow map is present otherwise, null
*/
getShadowMapForRendering() {
if (this._shadowMap2) {
return this._shadowMap2;
}
return this._shadowMap;
}
/**
* Gets the class name of that object
* @returns "ShadowGenerator"
*/
getClassName() {
return ShadowGenerator.CLASSNAME;
}
/**
* Helper function to add a mesh and its descendants to the list of shadow casters.
* @param mesh Mesh to add
* @param includeDescendants boolean indicating if the descendants should be added. Default to true
* @returns the Shadow Generator itself
*/
addShadowCaster(mesh, includeDescendants = true) {
if (!this._shadowMap) {
return this;
}
if (!this._shadowMap.renderList) {
this._shadowMap.renderList = [];
}
if (this._shadowMap.renderList.indexOf(mesh) === -1) {
this._shadowMap.renderList.push(mesh);
}
if (includeDescendants) {
for (const childMesh of mesh.getChildMeshes()) {
if (this._shadowMap.renderList.indexOf(childMesh) === -1) {
this._shadowMap.renderList.push(childMesh);
}
}
}
return this;
}
/**
* Helper function to remove a mesh and its descendants from the list of shadow casters
* @param mesh Mesh to remove
* @param includeDescendants boolean indicating if the descendants should be removed. Default to true
* @returns the Shadow Generator itself
*/
removeShadowCaster(mesh, includeDescendants = true) {
if (!this._shadowMap || !this._shadowMap.renderList) {
return this;
}
const index = this._shadowMap.renderList.indexOf(mesh);
if (index !== -1) {
this._shadowMap.renderList.splice(index, 1);
}
if (includeDescendants) {
for (const child of mesh.getChildren()) {
this.removeShadowCaster(child);
}
}
return this;
}
/**
* Returns the associated light object.
* @returns the light generating the shadow
*/
getLight() {
return this._light;
}
/**
* Gets the shader language used in this generator.
*/
get shaderLanguage() {
return this._shaderLanguage;
}
_getCamera() {
return this._camera ?? this._scene.activeCamera;
}
/**
* Gets or sets the size of the texture what stores the shadows
*/
get mapSize() {
return this._mapSize;
}
set mapSize(size) {
this._mapSize = size;
this._light._markMeshesAsLightDirty();
this.recreateShadowMap();
}
/**
* Creates a ShadowGenerator object.
* A ShadowGenerator is the required tool to use the shadows.
* Each light casting shadows needs to use its own ShadowGenerator.
* Documentation : https://doc.babylonjs.com/features/featuresDeepDive/lights/shadows
* @param mapSize The size of the texture what stores the shadows. Example : 1024.
* @param light The light object generating the shadows.
* @param usefullFloatFirst By default the generator will try to use half float textures but if you need precision (for self shadowing for instance), you can use this option to enforce full float texture.
* @param camera Camera associated with this shadow generator (default: null). If null, takes the scene active camera at the time we need to access it
* @param useRedTextureType Forces the generator to use a Red instead of a RGBA type for the shadow map texture format (default: false)
* @param forceGLSL defines a boolean indicating if the shader must be compiled in GLSL even if we are using WebGPU
*/
constructor(mapSize, light, usefullFloatFirst, camera, useRedTextureType, forceGLSL = false) {
/**
* Observable triggered before the shadow is rendered. Can be used to update internal effect state
*/
this.onBeforeShadowMapRenderObservable = new Observable();
/**
* Observable triggered after the shadow is rendered. Can be used to restore internal effect state
*/
this.onAfterShadowMapRenderObservable = new Observable();
/**
* Observable triggered before a mesh is rendered in the shadow map.
* Can be used to update internal effect state (that you can get from the onBeforeShadowMapRenderObservable)
*/
this.onBeforeShadowMapRenderMeshObservable = new Observable();
/**
* Observable triggered after a mesh is rendered in the shadow map.
* Can be used to update internal effect state (that you can get from the onAfterShadowMapRenderObservable)
*/
this.onAfterShadowMapRenderMeshObservable = new Observable();
/**
* Specifies if the `ShadowGenerator` should be serialized, `true` to skip serialization.
* Note a `ShadowGenerator` will not be serialized if its light has `doNotSerialize=true`
*/
this.doNotSerialize = false;
this._bias = 0.00005;
this._normalBias = 0;
this._blurBoxOffset = 1;
this._blurScale = 2;
this._blurKernel = 1;
this._useKernelBlur = false;
this._filter = ShadowGenerator.FILTER_NONE;
this._filteringQuality = ShadowGenerator.QUALITY_HIGH;
this._contactHardeningLightSizeUVRatio = 0.1;
this._darkness = 0;
this._transparencyShadow = false;
/**
* Enables or disables shadows with varying strength based on the transparency
* When it is enabled, the strength of the shadow is taken equal to mesh.visibility
* If you enabled an alpha texture on your material, the alpha value red from the texture is also combined to compute the strength:
* mesh.visibility * alphaTexture.a
* The texture used is the diffuse by default, but it can be set to the opacity by setting useOpacityTextureForTransparentShadow
* Note that by definition transparencyShadow must be set to true for enableSoftTransparentShadow to work!
*/
this.enableSoftTransparentShadow = false;
/**
* If this is true, use the opacity texture's alpha channel for transparent shadows instead of the diffuse one
*/
this.useOpacityTextureForTransparentShadow = false;
/**
* Controls the extent to which the shadows fade out at the edge of the frustum
*/
this.frustumEdgeFalloff = 0;
/** Shader language used by the generator */
this._shaderLanguage = 0 /* ShaderLanguage.GLSL */;
/**
* If true the shadow map is generated by rendering the back face of the mesh instead of the front face.
* This can help with self-shadowing as the geometry making up the back of objects is slightly offset.
* It might on the other hand introduce peter panning.
*/
this.forceBackFacesOnly = false;
this._lightDirection = Vector3.Zero();
this._viewMatrix = Matrix.Zero();
this._projectionMatrix = Matrix.Zero();
this._transformMatrix = Matrix.Zero();
this._cachedPosition = new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
this._cachedDirection = new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
this._currentFaceIndex = 0;
this._currentFaceIndexCache = 0;
this._defaultTextureMatrix = Matrix.Identity();
this._shadersLoaded = false;
this._mapSize = mapSize;
this._light = light;
this._scene = light.getScene();
this._camera = camera ?? null;
this._useRedTextureType = !!useRedTextureType;
this._initShaderSourceAsync(forceGLSL);
let shadowGenerators = light._shadowGenerators;
if (!shadowGenerators) {
shadowGenerators = light._shadowGenerators = new Map();
}
shadowGenerators.set(this._camera, this);
this.id = light.id;
this._useUBO = this._scene.getEngine().supportsUniformBuffers;
if (this._useUBO) {
this._sceneUBOs = [];
this._sceneUBOs.push(this._scene.createSceneUniformBuffer(`Scene for Shadow Generator (light "${this._light.name}")`));
}
ShadowGenerator._SceneComponentInitialization(this._scene);
// Texture type fallback from float to int if not supported.
const caps = this._scene.getEngine().getCaps();
if (!usefullFloatFirst) {
if (caps.textureHalfFloatRender && caps.textureHalfFloatLinearFiltering) {
this._textureType = 2;
}
else if (caps.textureFloatRender && caps.textureFloatLinearFiltering) {
this._textureType = 1;
}
else {
this._textureType = 0;
}
}
else {
if (caps.textureFloatRender && caps.textureFloatLinearFiltering) {
this._textureType = 1;
}
else if (caps.textureHalfFloatRender && caps.textureHalfFloatLinearFiltering) {
this._textureType = 2;
}
else {
this._textureType = 0;
}
}
this._initializeGenerator();
this._applyFilterValues();
}
_initializeGenerator() {
this._light._markMeshesAsLightDirty();
this._initializeShadowMap();
}
_createTargetRenderTexture() {
const engine = this._scene.getEngine();
if (engine._features.supportDepthStencilTexture) {
this._shadowMap = new RenderTargetTexture(this._light.name + "_shadowMap", this._mapSize, this._scene, false, true, this._textureType, this._light.needCube(), undefined, false, false, undefined, this._useRedTextureType ? 6 : 5);
this._shadowMap.createDepthStencilTexture(engine.useReverseDepthBuffer ? 516 : 513, true, undefined, undefined, undefined, `DepthStencilForShadowGenerator-${this._light.name}`);
}
else {
this._shadowMap = new RenderTargetTexture(this._light.name + "_shadowMap", this._mapSize, this._scene, false, true, this._textureType, this._light.needCube());
}
this._shadowMap.noPrePassRenderer = true;
}
_initializeShadowMap() {
this._createTargetRenderTexture();
if (this._shadowMap === null) {
return;
}
this._shadowMap.wrapU = Texture.CLAMP_ADDRESSMODE;
this._shadowMap.wrapV = Texture.CLAMP_ADDRESSMODE;
this._shadowMap.anisotropicFilteringLevel = 1;
this._shadowMap.updateSamplingMode(Texture.BILINEAR_SAMPLINGMODE);
this._shadowMap.renderParticles = false;
this._shadowMap.ignoreCameraViewport = true;
if (this._storedUniqueId) {
this._shadowMap.uniqueId = this._storedUniqueId;
}
// Custom render function.
this._shadowMap.customRenderFunction = (opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes) => this._renderForShadowMap(opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes);
// When preWarm is false, forces the mesh is ready function to true as we are double checking it
// in the custom render function. Also it prevents side effects and useless
// shader variations in DEPTHPREPASS mode.
this._shadowMap.customIsReadyFunction = (mesh, _refreshRate, preWarm) => {
if (!preWarm || !mesh.subMeshes) {
return true;
}
let isReady = true;
for (const subMesh of mesh.subMeshes) {
const renderingMesh = subMesh.getRenderingMesh();
const scene = this._scene;
const engine = scene.getEngine();
const material = subMesh.getMaterial();
if (!material || subMesh.verticesCount === 0 || (this.customAllowRendering && !this.customAllowRendering(subMesh))) {
continue;
}
const batch = renderingMesh._getInstancesRenderList(subMesh._id, !!subMesh.getReplacementMesh());
if (batch.mustReturn) {
continue;
}
const hardwareInstancedRendering = engine.getCaps().instancedArrays &&
((batch.visibleInstances[subMesh._id] !== null && batch.visibleInstances[subMesh._id] !== undefined) || renderingMesh.hasThinInstances);
const isTransparent = material.needAlphaBlendingForMesh(renderingMesh);
isReady = this.isReady(subMesh, hardwareInstancedRendering, isTransparent) && isReady;
}
return isReady;
};
const engine = this._scene.getEngine();
this._shadowMap.onBeforeBindObservable.add(() => {
this._currentSceneUBO = this._scene.getSceneUniformBuffer();
engine._debugPushGroup?.(`shadow map generation for pass id ${engine.currentRenderPassId}`, 1);
});
// Record Face Index before render.
this._shadowMap.onBeforeRenderObservable.add((faceIndex) => {
if (this._sceneUBOs) {
this._scene.setSceneUniformBuffer(this._sceneUBOs[0]);
}
this._currentFaceIndex = faceIndex;
if (this._filter === ShadowGenerator.FILTER_PCF) {
engine.setColorWrite(false);
}
this.getTransformMatrix(); // generate the view/projection matrix
this._scene.setTransformMatrix(this._viewMatrix, this._projectionMatrix);
if (this._useUBO) {
this._scene.getSceneUniformBuffer().unbindEffect();
this._scene.finalizeSceneUbo();
}
});
// Blur if required after render.
this._shadowMap.onAfterUnbindObservable.add(() => {
if (this._sceneUBOs) {
this._scene.setSceneUniformBuffer(this._currentSceneUBO);
}
this._scene.updateTransformMatrix(); // restore the view/projection matrices of the active camera
if (this._filter === ShadowGenerator.FILTER_PCF) {
engine.setColorWrite(true);
}
if (!this.useBlurExponentialShadowMap && !this.useBlurCloseExponentialShadowMap) {
engine._debugPopGroup?.(1);
return;
}
const shadowMap = this.getShadowMapForRendering();
if (shadowMap) {
this._scene.postProcessManager.directRender(this._blurPostProcesses, shadowMap.renderTarget, true);
engine.unBindFramebuffer(shadowMap.renderTarget, true);
}
engine._debugPopGroup?.(1);
});
// Clear according to the chosen filter.
const clearZero = new Color4(0, 0, 0, 0);
const clearOne = new Color4(1.0, 1.0, 1.0, 1.0);
this._shadowMap.onClearObservable.add((engine) => {
if (this._filter === ShadowGenerator.FILTER_PCF) {
engine.clear(clearOne, false, true, false);
}
else if (this.useExponentialShadowMap || this.useBlurExponentialShadowMap) {
engine.clear(clearZero, true, true, false);
}
else {
engine.clear(clearOne, true, true, false);
}
});
// Recreate on resize.
this._shadowMap.onResizeObservable.add((rtt) => {
this._storedUniqueId = this._shadowMap.uniqueId;
this._mapSize = rtt.getRenderSize();
this._light._markMeshesAsLightDirty();
this.recreateShadowMap();
});
// Ensures rendering groupids do not erase the depth buffer
// or we would lose the shadows information.
for (let i = RenderingManager.MIN_RENDERINGGROUPS; i < RenderingManager.MAX_RENDERINGGROUPS; i++) {
this._shadowMap.setRenderingAutoClearDepthStencil(i, false);
}
}
async _initShaderSourceAsync(forceGLSL = false) {
const engine = this._scene.getEngine();
if (engine.isWebGPU && !forceGLSL && !ShadowGenerator.ForceGLSL) {
this._shaderLanguage = 1 /* ShaderLanguage.WGSL */;
await Promise.all([
import("../../ShadersWGSL/shadowMap.fragment.js"),
import("../../ShadersWGSL/shadowMap.vertex.js"),
import("../../ShadersWGSL/depthBoxBlur.fragment.js"),
import("../../ShadersWGSL/ShadersInclude/shadowMapFragmentSoftTransparentShadow.js"),
]);
}
else {
await Promise.all([
import("../../Shaders/shadowMap.fragment.js"),
import("../../Shaders/shadowMap.vertex.js"),
import("../../Shaders/depthBoxBlur.fragment.js"),
import("../../Shaders/ShadersInclude/shadowMapFragmentSoftTransparentShadow.js"),
]);
}
this._shadersLoaded = true;
}
_initializeBlurRTTAndPostProcesses() {
const engine = this._scene.getEngine();
const targetSize = this._mapSize / this.blurScale;
if (!this.useKernelBlur || this.blurScale !== 1.0) {
this._shadowMap2 = new RenderTargetTexture(this._light.name + "_shadowMap2", targetSize, this._scene, false, true, this._textureType, undefined, undefined, false);
this._shadowMap2.wrapU = Texture.CLAMP_ADDRESSMODE;
this._shadowMap2.wrapV = Texture.CLAMP_ADDRESSMODE;
this._shadowMap2.updateSamplingMode(Texture.BILINEAR_SAMPLINGMODE);
}
if (this.useKernelBlur) {
this._kernelBlurXPostprocess = new BlurPostProcess(this._light.name + "KernelBlurX", new Vector2(1, 0), this.blurKernel, 1.0, null, Texture.BILINEAR_SAMPLINGMODE, engine, false, this._textureType);
this._kernelBlurXPostprocess.width = targetSize;
this._kernelBlurXPostprocess.height = targetSize;
this._kernelBlurXPostprocess.externalTextureSamplerBinding = true;
this._kernelBlurXPostprocess.onApplyObservable.add((effect) => {
effect.setTexture("textureSampler", this._shadowMap);
});
this._kernelBlurYPostprocess = new BlurPostProcess(this._light.name + "KernelBlurY", new Vector2(0, 1), this.blurKernel, 1.0, null, Texture.BILINEAR_SAMPLINGMODE, engine, false, this._textureType);
this._kernelBlurXPostprocess.autoClear = false;
this._kernelBlurYPostprocess.autoClear = false;
if (this._textureType === 0) {
this._kernelBlurXPostprocess.packedFloat = true;
this._kernelBlurYPostprocess.packedFloat = true;
}
this._blurPostProcesses = [this._kernelBlurXPostprocess, this._kernelBlurYPostprocess];
}
else {
this._boxBlurPostprocess = new PostProcess(this._light.name + "DepthBoxBlur", "depthBoxBlur", ["screenSize", "boxOffset"], [], 1.0, null, Texture.BILINEAR_SAMPLINGMODE, engine, false, "#define OFFSET " + this._blurBoxOffset, this._textureType, undefined, undefined, undefined, undefined, this._shaderLanguage);
this._boxBlurPostprocess.externalTextureSamplerBinding = true;
this._boxBlurPostprocess.onApplyObservable.add((effect) => {
effect.setFloat2("screenSize", targetSize, targetSize);
effect.setTexture("textureSampler", this._shadowMap);
});
this._boxBlurPostprocess.autoClear = false;
this._blurPostProcesses = [this._boxBlurPostprocess];
}
}
_renderForShadowMap(opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes) {
let index;
if (depthOnlySubMeshes.length) {
for (index = 0; index < depthOnlySubMeshes.length; index++) {
this._renderSubMeshForShadowMap(depthOnlySubMeshes.data[index]);
}
}
for (index = 0; index < opaqueSubMeshes.length; index++) {
this._renderSubMeshForShadowMap(opaqueSubMeshes.data[index]);
}
for (index = 0; index < alphaTestSubMeshes.length; index++) {
this._renderSubMeshForShadowMap(alphaTestSubMeshes.data[index]);
}
if (this._transparencyShadow) {
for (index = 0; index < transparentSubMeshes.length; index++) {
this._renderSubMeshForShadowMap(transparentSubMeshes.data[index], true);
}
}
else {
for (index = 0; index < transparentSubMeshes.length; index++) {
transparentSubMeshes.data[index].getEffectiveMesh()._internalAbstractMeshDataInfo._isActiveIntermediate = false;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_bindCustomEffectForRenderSubMeshForShadowMap(subMesh, effect, mesh) {
effect.setMatrix("viewProjection", this.getTransformMatrix());
}
_renderSubMeshForShadowMap(subMesh, isTransparent = false) {
const renderingMesh = subMesh.getRenderingMesh();
const effectiveMesh = subMesh.getEffectiveMesh();
const scene = this._scene;
const engine = scene.getEngine();
const material = subMesh.getMaterial();
effectiveMesh._internalAbstractMeshDataInfo._isActiveIntermediate = false;
if (!material || subMesh.verticesCount === 0 || subMesh._renderId === scene.getRenderId()) {
return;
}
// Culling
// Note:
// In rhs mode, we assume that meshes will be rendered in right-handed space (i.e. with an RHS camera), so the default value of material.sideOrientation is updated accordingly (see material constructor).
// However, when generating a shadow map, we render from the point of view of the light, whose view/projection matrices are always in lhs mode.
// We therefore need to "undo" the sideOrientation inversion that was previously performed when constructing the material.
const useRHS = scene.useRightHandedSystem;
const detNeg = effectiveMesh._getWorldMatrixDeterminant() < 0;
let sideOrientation = material._getEffectiveOrientation(renderingMesh);
if ((detNeg && !useRHS) || (!detNeg && useRHS)) {
sideOrientation =
sideOrientation === 0 ? 1 : 0;
}
const reverseSideOrientation = sideOrientation === 0;
engine.setState(material.backFaceCulling, undefined, undefined, reverseSideOrientation, material.cullBackFaces);
// Managing instances
const batch = renderingMesh._getInstancesRenderList(subMesh._id, !!subMesh.getReplacementMesh());
if (batch.mustReturn) {
return;
}
const hardwareInstancedRendering = engine.getCaps().instancedArrays &&
((batch.visibleInstances[subMesh._id] !== null && batch.visibleInstances[subMesh._id] !== undefined) || renderingMesh.hasThinInstances);
if (this.customAllowRendering && !this.customAllowRendering(subMesh)) {
return;
}
if (this.isReady(subMesh, hardwareInstancedRendering, isTransparent)) {
subMesh._renderId = scene.getRenderId();
const shadowDepthWrapper = material.shadowDepthWrapper;
const drawWrapper = shadowDepthWrapper?.getEffect(subMesh, this, engine.currentRenderPassId) ?? subMesh._getDrawWrapper();
const effect = DrawWrapper.GetEffect(drawWrapper);
engine.enableEffect(drawWrapper);
if (!hardwareInstancedRendering) {
renderingMesh._bind(subMesh, effect, material.fillMode);
}
this.getTransformMatrix(); // make sure _cachedDirection et _cachedPosition are up to date
effect.setFloat3("biasAndScaleSM", this.bias, this.normalBias, this.depthScale);
if (this.getLight().getTypeID() === Light.LIGHTTYPEID_DIRECTIONALLIGHT) {
effect.setVector3("lightDataSM", this._cachedDirection);
}
else {
effect.setVector3("lightDataSM", this._cachedPosition);
}
const camera = this._getCamera();
effect.setFloat2("depthValuesSM", this.getLight().getDepthMinZ(camera), this.getLight().getDepthMinZ(camera) + this.getLight().getDepthMaxZ(camera));
if (isTransparent && this.enableSoftTransparentShadow) {
effect.setFloat2("softTransparentShadowSM", effectiveMesh.visibility * material.alpha, this._opacityTexture?.getAlphaFromRGB ? 1 : 0);
}
if (shadowDepthWrapper) {
subMesh._setMainDrawWrapperOverride(drawWrapper);
if (shadowDepthWrapper.standalone) {
shadowDepthWrapper.baseMaterial.bindForSubMesh(effectiveMesh.getWorldMatrix(), renderingMesh, subMesh);
}
else {
material.bindForSubMesh(effectiveMesh.getWorldMatrix(), renderingMesh, subMesh);
}
subMesh._setMainDrawWrapperOverride(null);
}
else {
// Alpha test
if (this._opacityTexture) {
effect.setTexture("diffuseSampler", this._opacityTexture);
effect.setMatrix("diffuseMatrix", this._opacityTexture.getTextureMatrix() || this._defaultTextureMatrix);
}
// Bones
if (renderingMesh.useBones && renderingMesh.computeBonesUsingShaders && renderingMesh.skeleton) {
const skeleton = renderingMesh.skeleton;
if (skeleton.isUsingTextureForMatrices) {
const boneTexture = skeleton.getTransformMatrixTexture(renderingMesh);
if (!boneTexture) {
return;
}
effect.setTexture("boneSampler", boneTexture);
effect.setFloat("boneTextureWidth", 4.0 * (skeleton.bones.length + 1));
}
else {
effect.setMatrices("mBones", skeleton.getTransformMatrices(renderingMesh));
}
}
// Morph targets
BindMorphTargetParameters(renderingMesh, effect);
if (renderingMesh.morphTargetManager && renderingMesh.morphTargetManager.isUsingTextureForTargets) {
renderingMesh.morphTargetManager._bind(effect);
}
// Baked vertex animations
const bvaManager = subMesh.getMesh().bakedVertexAnimationManager;
if (bvaManager && bvaManager.isEnabled) {
bvaManager.bind(effect, hardwareInstancedRendering);
}
// Clip planes
bindClipPlane(effect, material, scene);
}
if (!this._useUBO && !shadowDepthWrapper) {
this._bindCustomEffectForRenderSubMeshForShadowMap(subMesh, effect, effectiveMesh);
}
BindSceneUniformBuffer(effect, this._scene.getSceneUniformBuffer());
this._scene.getSceneUniformBuffer().bindUniformBuffer();
const world = effectiveMesh.getWorldMatrix();
// In the non hardware instanced mode, the Mesh ubo update is done by the callback passed to renderingMesh._processRendering (see below)
if (hardwareInstancedRendering) {
effectiveMesh.getMeshUniformBuffer().bindToEffect(effect, "Mesh");
effectiveMesh.transferToEffect(world);
}
if (this.forceBackFacesOnly) {
engine.setState(true, 0, false, true, material.cullBackFaces);
}
// Observables
this.onBeforeShadowMapRenderMeshObservable.notifyObservers(renderingMesh);
this.onBeforeShadowMapRenderObservable.notifyObservers(effect);
// Draw
renderingMesh._processRendering(effectiveMesh, subMesh, effect, material.fillMode, batch, hardwareInstancedRendering, (isInstance, worldOverride) => {
if (effectiveMesh !== renderingMesh && !isInstance) {
renderingMesh.getMeshUniformBuffer().bindToEffect(effect, "Mesh");
renderingMesh.transferToEffect(worldOverride);
}
else {
effectiveMesh.getMeshUniformBuffer().bindToEffect(effect, "Mesh");
effectiveMesh.transferToEffect(isInstance ? worldOverride : world);
}
});
if (this.forceBackFacesOnly) {
engine.setState(true, 0, false, false, material.cullBackFaces);
}
// Observables
this.onAfterShadowMapRenderObservable.notifyObservers(effect);
this.onAfterShadowMapRenderMeshObservable.notifyObservers(renderingMesh);
}
else {
// Need to reset refresh rate of the shadowMap
if (this._shadowMap) {
this._shadowMap.resetRefreshCounter();
}
}
}
_applyFilterValues() {
if (!this._shadowMap) {
return;
}
if (this.filter === ShadowGenerator.FILTER_NONE || this.filter === ShadowGenerator.FILTER_PCSS) {
this._shadowMap.updateSamplingMode(Texture.NEAREST_SAMPLINGMODE);
}
else {
this._shadowMap.updateSamplingMode(Texture.BILINEAR_SAMPLINGMODE);
}
}
/**
* Forces all the attached effect to compile to enable rendering only once ready vs. lazily compiling effects.
* @param onCompiled Callback triggered at the and of the effects compilation
* @param options Sets of optional options forcing the compilation with different modes
*/
forceCompilation(onCompiled, options) {
const localOptions = {
useInstances: false,
...options,
};
const shadowMap = this.getShadowMap();
if (!shadowMap) {
if (onCompiled) {
onCompiled(this);
}
return;
}
const renderList = shadowMap.renderList;
if (!renderList) {
if (onCompiled) {
onCompiled(this);
}
return;
}
const subMeshes = [];
for (const mesh of renderList) {
subMeshes.push(...mesh.subMeshes);
}
if (subMeshes.length === 0) {
if (onCompiled) {
onCompiled(this);
}
return;
}
let currentIndex = 0;
const checkReady = () => {
if (!this._scene || !this._scene.getEngine()) {
return;
}
while (this.isReady(subMeshes[currentIndex], localOptions.useInstances, subMeshes[currentIndex].getMaterial()?.needAlphaBlendingForMesh(subMeshes[currentIndex].getMesh()) ?? false)) {
currentIndex++;
if (currentIndex >= subMeshes.length) {
if (onCompiled) {
onCompiled(this);
}
return;
}
}
setTimeout(checkReady, 16);
};
checkReady();
}
/**
* Forces all the attached effect to compile to enable rendering only once ready vs. lazily compiling effects.
* @param options Sets of optional options forcing the compilation with different modes
* @returns A promise that resolves when the compilation completes
*/
forceCompilationAsync(options) {
return new Promise((resolve) => {
this.forceCompilation(() => {
resolve();
}, options);
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_isReadyCustomDefines(defines, subMesh, useInstances) { }
_prepareShadowDefines(subMesh, useInstances, defines, isTransparent) {
defines.push("#define SM_LIGHTTYPE_" + this._light.getClassName().toUpperCase());
defines.push("#define SM_FLOAT " + (this._textureType !== 0 ? "1" : "0"));
defines.push("#define SM_ESM " + (this.useExponentialShadowMap || this.useBlurExponentialShadowMap ? "1" : "0"));
defines.push("#define SM_DEPTHTEXTURE " + (this.usePercentageCloserFiltering || this.useContactHardeningShadow ? "1" : "0"));
const mesh = subMesh.getMesh();
// Normal bias.
defines.push("#define SM_NORMALBIAS " + (this.normalBias && mesh.isVerticesDataPresent(VertexBuffer.NormalKind) ? "1" : "0"));
defines.push("#define SM_DIRECTIONINLIGHTDATA " + (this.getLight().getTypeID() === Light.LIGHTTYPEID_DIRECTIONALLIGHT ? "1" : "0"));
// Point light
defines.push("#define SM_USEDISTANCE " + (this._light.needCube() ? "1" : "0"));
// Soft transparent shadows
defines.push("#define SM_SOFTTRANSPARENTSHADOW " + (this.enableSoftTransparentShadow && isTransparent ? "1" : "0"));
this._isReadyCustomDefines(defines, subMesh, useInstances);
return defines;
}
/**
* Determine whether the shadow generator is ready or not (mainly all effects and related post processes needs to be ready).
* @param subMesh The submesh we want to render in the shadow map
* @param useInstances Defines whether will draw in the map using instances
* @param isTransparent Indicates that isReady is called for a transparent subMesh
* @returns true if ready otherwise, false
*/
isReady(subMesh, useInstances, isTransparent) {
if (!this._shadersLoaded) {
return false;
}
const material = subMesh.getMaterial(), shadowDepthWrapper = material?.shadowDepthWrapper;
this._opacityTexture = null;
if (!material) {
return false;
}
const defines = [];
this._prepareShadowDefines(subMesh, useInstances, defines, isTransparent);
if (shadowDepthWrapper) {
if (!shadowDepthWrapper.isReadyForSubMesh(subMesh, defines, this, useInstances, this._scene.getEngine().currentRenderPassId)) {
return false;
}
}
else {
const subMeshEffect = subMesh._getDrawWrapper(undefined, true);
let effect = subMeshEffect.effect;
let cachedDefines = subMeshEffect.defines;
const attribs = [VertexBuffer.PositionKind];
const mesh = subMesh.getMesh();
let useNormal = false;
let uv1 = false;
let uv2 = false;
const color = false;
// Normal bias.
if (this.normalBias && mesh.isVerticesDataPresent(VertexBuffer.NormalKind)) {
attribs.push(VertexBuffer.NormalKind);
defines.push("#define NORMAL");
useNormal = true;
if (mesh.nonUniformScaling) {
defines.push("#define NONUNIFORMSCALING");
}
}
//