claygl/src/prePass/ShadowMap.ts

843 lines
27 KiB
TypeScript

import * as glenum from '../core/glenum';
import Vector3 from '../math/Vector3';
import BoundingBox from '../math/BoundingBox';
import Frustum from '../math/Frustum';
import Matrix4 from '../math/Matrix4';
import Renderer, { RenderPassConfig } from '../Renderer';
import Shader, { ShaderPrecision } from '../Shader';
import Material from '../Material';
import FrameBuffer from '../FrameBuffer';
import Texture from '../Texture';
import Texture2D from '../Texture2D';
import TextureCube, { CubeTarget, cubeTargets } from '../TextureCube';
import PerspectiveCamera from '../camera/Perspective';
import OrthoCamera from '../camera/Orthographic';
import TexturePool from '../compositor/TexturePool';
import * as mat4 from '../glmatrix/mat4';
import shadowmapEssl from '../shader/source/shadowmap.glsl.js';
import type Renderable from '../Renderable';
import { Notifier } from '../core';
import Scene from '../Scene';
import Camera from '../Camera';
import CompositorFullscreenQuadPass from '../compositor/Pass';
import Light from '../Light';
import DirectionalLight from '../light/Directional';
import SpotLight from '../light/Spot';
import PointLight from '../light/Point';
Shader.import(shadowmapEssl);
function getDepthMaterialUniform(renderable: Renderable, depthMaterial: Material, symbol: string) {
if (symbol === 'alphaMap') {
return renderable.material.get('diffuseMap');
} else if (symbol === 'alphaCutoff') {
if (
renderable.material.isDefined('fragment', 'ALPHA_TEST') &&
renderable.material.get('diffuseMap')
) {
const alphaCutoff = renderable.material.get('alphaCutoff');
return alphaCutoff || 0;
}
return 0;
} else if (symbol === 'uvRepeat') {
return renderable.material.get('uvRepeat');
} else if (symbol === 'uvOffset') {
return renderable.material.get('uvOffset');
} else {
return depthMaterial.get(symbol);
}
}
function isDepthMaterialChanged(renderable: Renderable, prevRenderable: Renderable) {
const matA = renderable.material;
const matB = prevRenderable.material;
return (
matA.get('diffuseMap') !== matB.get('diffuseMap') ||
(matA.get('alphaCutoff') || 0) !== (matB.get('alphaCutoff') || 0)
);
}
const lightViewMatrix = new Matrix4();
const sceneViewBoundingBox = new BoundingBox();
const lightViewBBox = new BoundingBox();
const splitFrustum = new Frustum();
const splitProjMatrix = new Matrix4();
const cropBBox = new BoundingBox();
const cropMatrix = new Matrix4();
const lightViewProjMatrix = new Matrix4();
const lightProjMatrix = new Matrix4();
interface ShadowMapPassOpts {
/**
* If apply PCF soft shadow
* @type {number}
*/
softShadow: boolean;
shadowBlur: number;
}
/**
* Pass rendering shadow map.
*
* @constructor clay.prePass.ShadowMap
* @extends clay.core.Base
* @example
* const shadowMapPass = new clay.prePass.ShadowMap();
* ...
* animation.on('frame', function (frameTime) {
* shadowMapPass.render(renderer, scene, camera);
* renderer.render(scene, camera);
* });
*/
class ShadowMapPass extends Notifier {
softShadow: boolean = true;
/**
* Soft shadow blur size
* @type {number}
*/
shadowBlur = 1.0;
lightFrustumBias: number | 'auto' = 'auto';
kernelPCF = new Float32Array([1, 0, 1, 1, -1, 1, 0, 1, -1, 0, -1, -1, 1, -1, 0, -1]);
precision: ShaderPrecision = 'highp';
private _frameBuffer = new FrameBuffer();
private _textures: Record<string, Texture> = {};
private _shadowMapNumber: Record<string, number> = {
POINT_LIGHT: 0,
DIRECTIONAL_LIGHT: 0,
SPOT_LIGHT: 0
};
private _depthMaterials: Record<string, Material> = {};
private _distanceMaterials: Record<string, Material> = {};
private _receivers: Renderable[] = [];
private _lightsCastShadow: Light[] = [];
private _lightCameras: {
point?: Record<CubeTarget, PerspectiveCamera>;
directional?: OrthoCamera;
spot?: PerspectiveCamera;
} = {};
private _lightMaterials: Record<number, Material> = {};
private _texturePool = new TexturePool();
private _debugPass?: CompositorFullscreenQuadPass;
constructor(opts?: Partial<ShadowMapPassOpts>) {
super();
Object.assign(this, opts);
}
/**
* Render scene to shadow textures
*/
render(renderer: Renderer, scene: Scene, sceneCamera?: Camera, notUpdateScene?: boolean) {
if (!sceneCamera) {
sceneCamera = scene.getMainCamera();
}
this.trigger('beforerender', this, renderer, scene, sceneCamera);
this._renderShadowPass(renderer, scene, sceneCamera, notUpdateScene);
this.trigger('afterrender', this, renderer, scene, sceneCamera);
}
/**
* Debug rendering of shadow textures
*/
renderDebug(renderer: Renderer, size: number) {
let debugPass = this._debugPass;
if (!debugPass) {
debugPass = this._debugPass = new CompositorFullscreenQuadPass(
Shader.source('clay.sm.debug_depth')
);
}
renderer.saveClear();
const viewport = renderer.viewport;
let x = 0;
const y = 0;
const width = size || viewport.width / 4;
const height = width;
for (const name in this._textures) {
const texture = this._textures[name];
renderer.setViewport(x, y, (width * texture.width) / texture.height, height);
debugPass.setUniform('depthMap', texture);
debugPass.renderQuad(renderer);
x += (width * texture.width) / texture.height;
}
renderer.setViewport(viewport);
renderer.restoreClear();
}
_updateReceivers(renderer: Renderer, mesh: Renderable) {
if (mesh.receiveShadow) {
this._receivers.push(mesh);
mesh.material.set('shadowEnabled', 1);
mesh.material.set('pcfKernel', this.kernelPCF);
} else {
mesh.material.set('shadowEnabled', 0);
}
const kernelPCF = this.kernelPCF;
if (kernelPCF && kernelPCF.length) {
mesh.material.define('fragment', 'PCF_KERNEL_SIZE', kernelPCF.length / 2);
} else {
mesh.material.undefine('fragment', 'PCF_KERNEL_SIZE');
}
}
_update(renderer: Renderer, scene: Scene) {
const self = this;
scene.traverse(function (renderable) {
if (renderable.isRenderable()) {
self._updateReceivers(renderer, renderable);
}
});
for (let i = 0; i < scene.lights.length; i++) {
const light = scene.lights[i];
if (light.castShadow && !light.invisible) {
this._lightsCastShadow.push(light);
}
}
}
_renderShadowPass(
renderer: Renderer,
scene: Scene,
sceneCamera: Camera,
notUpdateScene?: boolean
) {
// reset
for (const name in this._shadowMapNumber) {
this._shadowMapNumber[name] = 0;
}
this._lightsCastShadow.length = 0;
this._receivers.length = 0;
const _gl = renderer.gl;
if (!notUpdateScene) {
scene.update();
}
if (sceneCamera) {
sceneCamera.update();
}
scene.updateLights();
this._update(renderer, scene);
// Needs to update the receivers again if shadows come from 1 to 0.
if (!this._lightsCastShadow.length) {
return;
}
_gl.enable(_gl.DEPTH_TEST);
_gl.depthMask(true);
_gl.disable(_gl.BLEND);
// Clear with high-z, so the part not rendered will not been shadowed
// TODO
// TODO restore
_gl.clearColor(1.0, 1.0, 1.0, 1.0);
// Shadow uniforms
const spotLightShadowMaps: Texture2D[] = [];
const spotLightMatrices: mat4.Mat4Array[] = [];
const directionalLightShadowMaps: Texture2D[] = [];
const directionalLightMatrices: mat4.Mat4Array[] = [];
const shadowCascadeClips: number[] = [];
const pointLightShadowMaps: TextureCube[] = [];
let dirLightHasCascade;
// Create textures for shadow map
for (let i = 0; i < this._lightsCastShadow.length; i++) {
const light = this._lightsCastShadow[i] as DirectionalLight | SpotLight | PointLight;
if (light.type === 'DIRECTIONAL_LIGHT') {
if (dirLightHasCascade) {
console.warn('Only one direectional light supported with shadow cascade');
continue;
}
if (light.shadowCascade > 4) {
console.warn('Support at most 4 cascade');
continue;
}
if (light.shadowCascade > 1) {
dirLightHasCascade = light;
}
this.renderDirectionalLightShadow(
renderer,
scene,
sceneCamera as PerspectiveCamera | OrthoCamera,
light,
shadowCascadeClips,
directionalLightMatrices,
directionalLightShadowMaps
);
} else if (light.type === 'SPOT_LIGHT') {
this.renderSpotLightShadow(renderer, scene, light, spotLightMatrices, spotLightShadowMaps);
} else if (light.type === 'POINT_LIGHT') {
this.renderPointLightShadow(renderer, scene, light, pointLightShadowMaps);
}
this._shadowMapNumber[light.type]++;
}
for (const lightType in this._shadowMapNumber) {
const number = this._shadowMapNumber[lightType];
const key = lightType + '_SHADOWMAP_COUNT';
for (let i = 0; i < this._receivers.length; i++) {
const mesh = this._receivers[i];
const material = mesh.material;
if (material.fragmentDefines[key] !== number) {
if (number > 0) {
material.define('fragment', key, number);
} else if (material.isDefined('fragment', key)) {
material.undefine('fragment', key);
}
}
}
}
for (let i = 0; i < this._receivers.length; i++) {
const mesh = this._receivers[i];
const material = mesh.material;
if (dirLightHasCascade) {
material.define('fragment', 'SHADOW_CASCADE', dirLightHasCascade.shadowCascade);
} else {
material.undefine('fragment', 'SHADOW_CASCADE');
}
}
const shadowUniforms = scene.shadowUniforms;
function getSize(texture: Texture) {
return texture.height;
}
if (directionalLightShadowMaps.length > 0) {
const directionalLightShadowMapSizes = directionalLightShadowMaps.map(getSize);
shadowUniforms.directionalLightShadowMaps = {
value: directionalLightShadowMaps,
type: 'tv'
};
shadowUniforms.directionalLightMatrices = { value: directionalLightMatrices, type: 'm4v' };
shadowUniforms.directionalLightShadowMapSizes = {
value: directionalLightShadowMapSizes,
type: '1fv'
};
if (dirLightHasCascade) {
const shadowCascadeClipsNear = shadowCascadeClips.slice();
const shadowCascadeClipsFar = shadowCascadeClips.slice();
shadowCascadeClipsNear.pop();
shadowCascadeClipsFar.shift();
// Iterate from far to near
shadowCascadeClipsNear.reverse();
shadowCascadeClipsFar.reverse();
// directionalLightShadowMaps.reverse();
directionalLightMatrices.reverse();
shadowUniforms.shadowCascadeClipsNear = { value: shadowCascadeClipsNear, type: '1fv' };
shadowUniforms.shadowCascadeClipsFar = { value: shadowCascadeClipsFar, type: '1fv' };
}
}
if (spotLightShadowMaps.length > 0) {
const spotLightShadowMapSizes = spotLightShadowMaps.map(getSize);
const shadowUniforms = scene.shadowUniforms;
shadowUniforms.spotLightShadowMaps = { value: spotLightShadowMaps, type: 'tv' };
shadowUniforms.spotLightMatrices = { value: spotLightMatrices, type: 'm4v' };
shadowUniforms.spotLightShadowMapSizes = { value: spotLightShadowMapSizes, type: '1fv' };
}
if (pointLightShadowMaps.length > 0) {
shadowUniforms.pointLightShadowMaps = { value: pointLightShadowMaps, type: 'tv' };
}
}
renderDirectionalLightShadow(
renderer: Renderer,
scene: Scene,
sceneCamera: PerspectiveCamera | OrthoCamera,
light: DirectionalLight,
shadowCascadeClips: number[],
directionalLightMatrices: mat4.Mat4Array[],
directionalLightShadowMaps: Texture2D[]
) {
const defaultShadowMaterial = this._getDepthMaterial(light);
const passConfig: RenderPassConfig = {
getMaterial(renderable) {
return (renderable as Renderable).shadowDepthMaterial || defaultShadowMaterial;
},
isMaterialChanged: isDepthMaterialChanged,
getUniform: getDepthMaterialUniform,
ifRender(renderable) {
return (renderable as Renderable).castShadow;
},
sortCompare: Renderer.opaqueSortCompare
};
// First frame
if (!scene.viewBoundingBoxLastFrame.isFinite()) {
const boundingBox = scene.getBoundingBox();
scene.viewBoundingBoxLastFrame.copy(boundingBox).applyTransform(sceneCamera.viewMatrix);
}
// Considering moving speed since the bounding box is from last frame
// TODO: add a bias
const clippedFar = Math.min(-scene.viewBoundingBoxLastFrame.min.z, sceneCamera.far);
const clippedNear = Math.max(-scene.viewBoundingBoxLastFrame.max.z, sceneCamera.near);
const lightCamera = this._getDirectionalLightCamera(light, scene, sceneCamera);
const lvpMat4Arr = lightViewProjMatrix.array;
lightProjMatrix.copy(lightCamera.projectionMatrix);
mat4.invert(lightViewMatrix.array, lightCamera.worldTransform.array);
mat4.multiply(lightViewMatrix.array, lightViewMatrix.array, sceneCamera.worldTransform.array);
mat4.multiply(lvpMat4Arr, lightProjMatrix.array, lightViewMatrix.array);
const clipPlanes = [];
const isPerspective = sceneCamera instanceof PerspectiveCamera;
const scaleZ = (sceneCamera.near + sceneCamera.far) / (sceneCamera.near - sceneCamera.far);
const offsetZ = (2 * sceneCamera.near * sceneCamera.far) / (sceneCamera.near - sceneCamera.far);
for (let i = 0; i <= light.shadowCascade; i++) {
const clog = clippedNear * Math.pow(clippedFar / clippedNear, i / light.shadowCascade);
const cuni = clippedNear + ((clippedFar - clippedNear) * i) / light.shadowCascade;
const c = clog * light.cascadeSplitLogFactor + cuni * (1 - light.cascadeSplitLogFactor);
clipPlanes.push(c);
shadowCascadeClips.push(-(-c * scaleZ + offsetZ) / -c);
}
const texture = this._getTexture(light, light.shadowCascade);
directionalLightShadowMaps.push(texture);
const viewport = renderer.viewport;
const _gl = renderer.gl;
this._frameBuffer.attach(texture);
this._frameBuffer.bind(renderer);
_gl.clear(_gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT);
for (let i = 0; i < light.shadowCascade; i++) {
// Get the splitted frustum
const nearPlane = clipPlanes[i];
const farPlane = clipPlanes[i + 1];
if (isPerspective) {
mat4.perspective(
splitProjMatrix.array,
(sceneCamera.fov / 180) * Math.PI,
sceneCamera.aspect,
nearPlane,
farPlane
);
} else {
mat4.ortho(
splitProjMatrix.array,
sceneCamera.left,
sceneCamera.right,
sceneCamera.bottom,
sceneCamera.top,
nearPlane,
farPlane
);
}
splitFrustum.setFromProjection(splitProjMatrix);
splitFrustum.getTransformedBoundingBox(cropBBox, lightViewMatrix);
cropBBox.applyProjection(lightProjMatrix);
const _min = cropBBox.min.array;
const _max = cropBBox.max.array;
_min[0] = Math.max(_min[0], -1);
_min[1] = Math.max(_min[1], -1);
_max[0] = Math.min(_max[0], 1);
_max[1] = Math.min(_max[1], 1);
cropMatrix.ortho(_min[0], _max[0], _min[1], _max[1], 1, -1);
lightCamera.projectionMatrix.multiplyLeft(cropMatrix);
const shadowSize = light.shadowResolution || 512;
// Reversed, left to right => far to near
renderer.setViewport(
(light.shadowCascade - i - 1) * shadowSize,
0,
shadowSize,
shadowSize,
1
);
const renderList = scene.updateRenderList(lightCamera);
renderer.renderPass(renderList.opaque, lightCamera, passConfig);
const matrix = new Matrix4();
matrix.copy(lightCamera.viewMatrix).multiplyLeft(lightCamera.projectionMatrix);
directionalLightMatrices.push(matrix.array);
lightCamera.projectionMatrix.copy(lightProjMatrix);
}
this._frameBuffer.unbind(renderer);
renderer.setViewport(viewport);
}
renderSpotLightShadow(
renderer: Renderer,
scene: Scene,
light: SpotLight,
spotLightMatrices: mat4.Mat4Array[],
spotLightShadowMaps: Texture2D[]
) {
const texture = this._getTexture(light);
const lightCamera = this._getSpotLightCamera(light);
const _gl = renderer.gl;
this._frameBuffer.attach(texture);
this._frameBuffer.bind(renderer);
_gl.clear(_gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT);
const defaultShadowMaterial = this._getDepthMaterial(light);
const passConfig: RenderPassConfig = {
getMaterial(renderable) {
return (renderable as Renderable).shadowDepthMaterial || defaultShadowMaterial;
},
isMaterialChanged: isDepthMaterialChanged,
getUniform: getDepthMaterialUniform,
ifRender(renderable) {
return (renderable as Renderable).castShadow;
},
sortCompare: Renderer.opaqueSortCompare
};
const renderList = scene.updateRenderList(lightCamera);
renderer.renderPass(renderList.opaque, lightCamera, passConfig);
this._frameBuffer.unbind(renderer);
const matrix = new Matrix4();
matrix.copy(lightCamera.worldTransform).invert().multiplyLeft(lightCamera.projectionMatrix);
spotLightShadowMaps.push(texture);
spotLightMatrices.push(matrix.array);
}
renderPointLightShadow(
renderer: Renderer,
scene: Scene,
light: PointLight,
pointLightShadowMaps: TextureCube[]
) {
const texture = this._getTexture(light);
const _gl = renderer.gl;
pointLightShadowMaps.push(texture);
const defaultShadowMaterial = this._getDepthMaterial(light);
const passConfig: RenderPassConfig = {
getMaterial(renderable) {
return (renderable as Renderable).shadowDepthMaterial || defaultShadowMaterial;
},
getUniform: getDepthMaterialUniform,
sortCompare: Renderer.opaqueSortCompare
};
const renderListEachSide: Record<CubeTarget, Renderable[]> = {
px: [],
py: [],
pz: [],
nx: [],
ny: [],
nz: []
};
const bbox = new BoundingBox();
const lightWorldPosition = light.getWorldPosition().array;
const lightBBox = new BoundingBox();
const range = light.range;
lightBBox.min.setArray(lightWorldPosition);
lightBBox.max.setArray(lightWorldPosition);
const extent = new Vector3(range, range, range);
lightBBox.max.add(extent);
lightBBox.min.sub(extent);
const targetsNeedRender = {} as Record<CubeTarget, boolean>;
scene.traverse(function (renderable) {
if (renderable.isRenderable() && renderable.castShadow) {
const geometry = renderable.geometry;
if (!geometry.boundingBox) {
for (let i = 0; i < cubeTargets.length; i++) {
renderListEachSide[cubeTargets[i]].push(renderable);
}
return;
}
bbox.transformFrom(geometry.boundingBox, renderable.worldTransform);
if (!bbox.intersectBoundingBox(lightBBox)) {
return;
}
bbox.updateVertices();
for (let i = 0; i < cubeTargets.length; i++) {
targetsNeedRender[cubeTargets[i]] = false;
}
for (let i = 0; i < 8; i++) {
const vtx = bbox.vertices![i];
const x = vtx[0] - lightWorldPosition[0];
const y = vtx[1] - lightWorldPosition[1];
const z = vtx[2] - lightWorldPosition[2];
const absx = Math.abs(x);
const absy = Math.abs(y);
const absz = Math.abs(z);
if (absx > absy) {
if (absx > absz) {
targetsNeedRender[x > 0 ? 'px' : 'nx'] = true;
} else {
targetsNeedRender[z > 0 ? 'pz' : 'nz'] = true;
}
} else {
if (absy > absz) {
targetsNeedRender[y > 0 ? 'py' : 'ny'] = true;
} else {
targetsNeedRender[z > 0 ? 'pz' : 'nz'] = true;
}
}
}
for (let i = 0; i < cubeTargets.length; i++) {
if (targetsNeedRender[cubeTargets[i]]) {
renderListEachSide[cubeTargets[i]].push(renderable);
}
}
}
});
for (let i = 0; i < 6; i++) {
const target = cubeTargets[i];
const camera = this._getPointLightCamera(light, target);
this._frameBuffer.attach(texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i);
this._frameBuffer.bind(renderer);
_gl.clear(_gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT);
renderer.renderPass(renderListEachSide[target], camera, passConfig);
}
this._frameBuffer.unbind(renderer);
}
_getDepthMaterial(light: Light) {
let shadowMaterial = this._lightMaterials[light.__uid__];
const isPointLight = light.type === 'POINT_LIGHT';
if (!shadowMaterial) {
const shaderPrefix = isPointLight ? 'clay.sm.distance.' : 'clay.sm.depth.';
shadowMaterial = new Material({
precision: this.precision,
shader: new Shader(
Shader.source(shaderPrefix + 'vertex'),
Shader.source(shaderPrefix + 'fragment')
)
});
this._lightMaterials[light.__uid__] = shadowMaterial;
}
if ((light as DirectionalLight).shadowSlopeScale != null) {
shadowMaterial.setUniform('slopeScale', (light as DirectionalLight).shadowSlopeScale);
}
if ((light as DirectionalLight).shadowBias != null) {
shadowMaterial.setUniform('bias', (light as DirectionalLight).shadowBias);
}
if (isPointLight) {
shadowMaterial.set('lightPosition', light.getWorldPosition().array);
shadowMaterial.set('range', (light as PointLight).range);
}
return shadowMaterial;
}
_getTexture(light: PointLight, cascade?: number): TextureCube;
_getTexture(light: DirectionalLight | SpotLight, cascade?: number): Texture2D;
_getTexture(light: DirectionalLight | PointLight | SpotLight, cascade?: number) {
const key = light.__uid__;
let texture = this._textures[key];
const resolution = light.shadowResolution || 512;
cascade = cascade || 1;
if (!texture) {
if (light.type === 'POINT_LIGHT') {
texture = new TextureCube();
} else {
texture = new Texture2D();
}
// At most 4 cascade
// TODO share with height ?
texture.width = resolution * cascade;
texture.height = resolution;
texture.minFilter = glenum.NEAREST;
texture.magFilter = glenum.NEAREST;
texture.useMipmap = false;
this._textures[key] = texture;
}
return texture;
}
_getPointLightCamera(light: PointLight, target: CubeTarget) {
if (!this._lightCameras.point) {
this._lightCameras.point = {
px: new PerspectiveCamera(),
nx: new PerspectiveCamera(),
py: new PerspectiveCamera(),
ny: new PerspectiveCamera(),
pz: new PerspectiveCamera(),
nz: new PerspectiveCamera()
};
}
const camera = this._lightCameras.point[target];
camera.far = light.range;
camera.fov = 90;
camera.position.set(0, 0, 0);
switch (target) {
case 'px':
camera.lookAt(Vector3.POSITIVE_X, Vector3.NEGATIVE_Y);
break;
case 'nx':
camera.lookAt(Vector3.NEGATIVE_X, Vector3.NEGATIVE_Y);
break;
case 'py':
camera.lookAt(Vector3.POSITIVE_Y, Vector3.POSITIVE_Z);
break;
case 'ny':
camera.lookAt(Vector3.NEGATIVE_Y, Vector3.NEGATIVE_Z);
break;
case 'pz':
camera.lookAt(Vector3.POSITIVE_Z, Vector3.NEGATIVE_Y);
break;
case 'nz':
camera.lookAt(Vector3.NEGATIVE_Z, Vector3.NEGATIVE_Y);
break;
}
light.getWorldPosition(camera.position);
camera.update();
return camera;
}
// Camera of directional light will be adjusted
// to contain the view frustum and scene bounding box as tightly as possible
_getDirectionalLightCamera(
light: DirectionalLight,
scene: Scene,
sceneCamera: PerspectiveCamera | OrthoCamera
) {
if (!this._lightCameras.directional) {
this._lightCameras.directional = new OrthoCamera();
}
const camera = this._lightCameras.directional;
sceneViewBoundingBox.copy(scene.viewBoundingBoxLastFrame);
sceneViewBoundingBox.intersection(sceneCamera.frustum.boundingBox);
// Move to the center of frustum(in world space)
camera.position
.copy(sceneViewBoundingBox.min)
.add(sceneViewBoundingBox.max)
.scale(0.5)
.transformMat4(sceneCamera.worldTransform);
camera.rotation.copy(light.rotation);
camera.scale.copy(light.scale);
camera.updateWorldTransform();
// Transform to light view space
Matrix4.invert(lightViewMatrix, camera.worldTransform);
Matrix4.multiply(lightViewMatrix, lightViewMatrix, sceneCamera.worldTransform);
lightViewBBox.copy(sceneViewBoundingBox).applyTransform(lightViewMatrix);
const min = lightViewBBox.min.array;
const max = lightViewBBox.max.array;
// Move camera to adjust the near to 0
camera.position
.set((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, max[2])
.transformMat4(camera.worldTransform);
camera.near = 0;
camera.far = -min[2] + max[2];
// Make sure receivers not in the frustum will stil receive the shadow.
if (isNaN(+this.lightFrustumBias)) {
camera.far *= 4;
} else {
camera.far += +this.lightFrustumBias;
}
camera.left = min[0];
camera.right = max[0];
camera.top = max[1];
camera.bottom = min[1];
camera.update();
return camera;
}
_getSpotLightCamera(light: SpotLight) {
if (!this._lightCameras.spot) {
this._lightCameras.spot = new PerspectiveCamera();
}
const camera = this._lightCameras.spot;
// Update properties
camera.fov = light.penumbraAngle * 2;
camera.far = light.range;
camera.worldTransform.copy(light.worldTransform);
camera.updateProjectionMatrix();
mat4.invert(camera.viewMatrix.array, camera.worldTransform.array);
return camera;
}
/**
* @param {clay.Renderer|WebGLRenderingContext} [renderer]
* @memberOf clay.prePass.ShadowMap.prototype
*/
// PENDING Renderer or WebGLRenderingContext
dispose(renderer: Renderer) {
if (this._frameBuffer) {
this._frameBuffer.dispose(renderer);
}
for (const name in this._textures) {
this._textures[name].dispose(renderer);
}
this._texturePool.clear(renderer);
this._depthMaterials = {};
this._distanceMaterials = {};
this._textures = {};
this._lightCameras = {};
this._shadowMapNumber = {
POINT_LIGHT: 0,
DIRECTIONAL_LIGHT: 0,
SPOT_LIGHT: 0
};
for (let i = 0; i < this._receivers.length; i++) {
const mesh = this._receivers[i];
// Mesh may be disposed
if (mesh.material) {
const material = mesh.material;
material.undefine('fragment', 'POINT_LIGHT_SHADOW_COUNT');
material.undefine('fragment', 'DIRECTIONAL_LIGHT_SHADOW_COUNT');
material.undefine('fragment', 'AMBIENT_LIGHT_SHADOW_COUNT');
material.set('shadowEnabled', 0);
}
}
this._receivers = [];
this._lightsCastShadow = [];
}
}
export default ShadowMapPass;