playcanvas
Version:
PlayCanvas WebGL game engine
209 lines (206 loc) • 10.8 kB
JavaScript
import { Debug } from '../../core/debug.js';
import { math } from '../../core/math/math.js';
import { Vec3 } from '../../core/math/vec3.js';
import { Mat4 } from '../../core/math/mat4.js';
import { BoundingBox } from '../../core/shape/bounding-box.js';
import { SHADOWUPDATE_NONE, LIGHTTYPE_DIRECTIONAL } from '../constants.js';
import { ShadowMap } from './shadow-map.js';
import { RenderPassShadowDirectional } from './render-pass-shadow-directional.js';
/**
* @import { Camera } from '../camera.js'
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
* @import { Light } from '../light.js'
* @import { Renderer } from './renderer.js'
* @import { ShadowRenderer } from './shadow-renderer.js'
* @import { MeshInstance } from '../mesh-instance.js';
*/ const visibleSceneAabb = new BoundingBox();
const center = new Vec3();
const shadowCamView = new Mat4();
const aabbPoints = [
new Vec3(),
new Vec3(),
new Vec3(),
new Vec3(),
new Vec3(),
new Vec3(),
new Vec3(),
new Vec3()
];
// evaluate depth range the aabb takes in the space of the camera
const _depthRange = {
min: 0,
max: 0
};
function getDepthRange(cameraViewMatrix, aabbMin, aabbMax) {
aabbPoints[0].x = aabbPoints[1].x = aabbPoints[2].x = aabbPoints[3].x = aabbMin.x;
aabbPoints[1].y = aabbPoints[3].y = aabbPoints[7].y = aabbPoints[5].y = aabbMin.y;
aabbPoints[2].z = aabbPoints[3].z = aabbPoints[6].z = aabbPoints[7].z = aabbMin.z;
aabbPoints[4].x = aabbPoints[5].x = aabbPoints[6].x = aabbPoints[7].x = aabbMax.x;
aabbPoints[0].y = aabbPoints[2].y = aabbPoints[4].y = aabbPoints[6].y = aabbMax.y;
aabbPoints[0].z = aabbPoints[1].z = aabbPoints[4].z = aabbPoints[5].z = aabbMax.z;
let minz = 9999999999;
let maxz = -9999999999;
for(let i = 0; i < 8; ++i){
cameraViewMatrix.transformPoint(aabbPoints[i], aabbPoints[i]);
const z = aabbPoints[i].z;
if (z < minz) minz = z;
if (z > maxz) maxz = z;
}
_depthRange.min = minz;
_depthRange.max = maxz;
return _depthRange;
}
class ShadowRendererDirectional {
constructor(renderer, shadowRenderer){
this.renderer = renderer;
this.shadowRenderer = shadowRenderer;
this.device = renderer.device;
}
// cull directional shadow map
cull(light, comp, camera, casters = null) {
// force light visibility if function was manually called
light.visibleThisFrame = true;
if (!light._shadowMap) {
light._shadowMap = ShadowMap.create(this.device, light);
}
// generate splits for the cascades
const nearDist = camera._nearClip;
this.generateSplitDistances(light, nearDist, Math.min(camera._farClip, light.shadowDistance));
const shadowUpdateOverrides = light.shadowUpdateOverrides;
for(let cascade = 0; cascade < light.numCascades; cascade++){
// if manually controlling cascade rendering and the cascade does not render this frame
if (shadowUpdateOverrides?.[cascade] === SHADOWUPDATE_NONE) {
break;
}
const lightRenderData = light.getRenderData(camera, cascade);
const shadowCam = lightRenderData.shadowCamera;
// assign render target
// Note: this is done during rendering for all shadow maps, but do it here for the case shadow rendering for the directional light
// is disabled - we need shadow map to be assigned for rendering to work even in this case. This needs further refactoring - as when
// shadow rendering is set to SHADOWUPDATE_NONE, we should not even execute shadow map culling
shadowCam.renderTarget = light._shadowMap.renderTargets[0];
// viewport
lightRenderData.shadowViewport.copy(light.cascades[cascade]);
lightRenderData.shadowScissor.copy(light.cascades[cascade]);
const shadowCamNode = shadowCam._node;
const lightNode = light._node;
shadowCamNode.setPosition(lightNode.getPosition());
// Camera looks down the negative Z, and directional light points down the negative Y
shadowCamNode.setRotation(lightNode.getRotation());
shadowCamNode.rotateLocal(-90, 0, 0);
// get camera's frustum corners for the cascade, convert them to world space and find their center
const frustumNearDist = cascade === 0 ? nearDist : light._shadowCascadeDistances[cascade - 1];
const frustumFarDist = light._shadowCascadeDistances[cascade];
const frustumPoints = camera.getFrustumCorners(frustumNearDist, frustumFarDist);
center.set(0, 0, 0);
const cameraWorldMat = camera.node.getWorldTransform();
for(let i = 0; i < 8; i++){
cameraWorldMat.transformPoint(frustumPoints[i], frustumPoints[i]);
center.add(frustumPoints[i]);
}
center.mulScalar(1 / 8);
// radius of the world space bounding sphere for the frustum slice
let radius = 0;
for(let i = 0; i < 8; i++){
const dist = frustumPoints[i].sub(center).length();
if (dist > radius) {
radius = dist;
}
}
// axis of light coordinate system
const right = shadowCamNode.right;
const up = shadowCamNode.up;
const lightDir = shadowCamNode.forward;
// transform the sphere's center into the center of the shadow map, pixel aligned.
// this makes the shadow map stable and avoids shimmering on the edges when the camera moves
const sizeRatio = 0.25 * light._shadowResolution / radius;
const x = Math.ceil(center.dot(up) * sizeRatio) / sizeRatio;
const y = Math.ceil(center.dot(right) * sizeRatio) / sizeRatio;
const scaledUp = up.mulScalar(x);
const scaledRight = right.mulScalar(y);
const dot = center.dot(lightDir);
const scaledDir = lightDir.mulScalar(dot);
center.add2(scaledUp, scaledRight).add(scaledDir);
// look at the center from far away to include all casters during culling
shadowCamNode.setPosition(center);
shadowCamNode.translateLocal(0, 0, 1000000);
shadowCam.nearClip = 0.01;
shadowCam.farClip = 2000000;
shadowCam.orthoHeight = radius;
// cull shadow casters
this.renderer.updateCameraFrustum(shadowCam);
this.shadowRenderer.cullShadowCasters(comp, light, lightRenderData.visibleCasters, shadowCam, casters);
const cascadeFlag = 1 << cascade;
const visibleCasters = lightRenderData.visibleCasters;
const origNumVisibleCasters = visibleCasters.length;
let numVisibleCasters = 0;
// exclude all mesh instances that are hidden for this cascade.
// find out AABB of visible shadow casters
for(let i = 0; i < origNumVisibleCasters; i++){
const meshInstance = visibleCasters[i];
if (meshInstance.shadowCascadeMask & cascadeFlag) {
visibleCasters[numVisibleCasters++] = meshInstance;
if (numVisibleCasters === 1) {
visibleSceneAabb.copy(meshInstance.aabb);
} else {
visibleSceneAabb.add(meshInstance.aabb);
}
}
}
// remove empty tail
if (origNumVisibleCasters !== numVisibleCasters) {
visibleCasters.length = numVisibleCasters;
}
// calculate depth range of the caster's AABB from the point of view of the shadow camera
shadowCamView.copy(shadowCamNode.getWorldTransform()).invert();
const depthRange = getDepthRange(shadowCamView, visibleSceneAabb.getMin(), visibleSceneAabb.getMax());
// adjust shadow camera's near and far plane to the depth range of casters to maximize precision
// of values stored in the shadow map. Make it slightly larger to avoid clipping on near / far plane.
shadowCamNode.translateLocal(0, 0, depthRange.max + 0.1);
shadowCam.farClip = depthRange.max - depthRange.min + 0.2;
lightRenderData.projectionCompensation = radius;
}
}
// function to generate frustum split distances
generateSplitDistances(light, nearDist, farDist) {
light._shadowCascadeDistances.fill(farDist);
for(let i = 1; i < light.numCascades; i++){
// lerp between linear and logarithmic distance, called practical split distance
const fraction = i / light.numCascades;
const linearDist = nearDist + (farDist - nearDist) * fraction;
const logDist = nearDist * (farDist / nearDist) ** fraction;
const dist = math.lerp(linearDist, logDist, light.cascadeDistribution);
light._shadowCascadeDistances[i - 1] = dist;
}
}
/**
* Create a render pass for directional light shadow rendering for a specified camera.
*
* @param {Light} light - The directional light.
* @param {Camera} camera - The camera.
* @returns {RenderPassShadowDirectional|null} - The render pass if the shadow rendering is
* required, or null otherwise.
*/ getLightRenderPass(light, camera) {
Debug.assert(light && light._type === LIGHTTYPE_DIRECTIONAL);
let renderPass = null;
if (this.shadowRenderer.needsShadowRendering(light)) {
// shadow cascades have more faces rendered within a singe render pass
const faceCount = light.numShadowFaces;
const shadowUpdateOverrides = light.shadowUpdateOverrides;
// prepare render targets / cameras for rendering
let allCascadesRendering = true;
let shadowCamera;
for(let face = 0; face < faceCount; face++){
if (shadowUpdateOverrides?.[face] === SHADOWUPDATE_NONE) {
allCascadesRendering = false;
}
shadowCamera = this.shadowRenderer.prepareFace(light, camera, face);
}
renderPass = new RenderPassShadowDirectional(this.device, this.shadowRenderer, light, camera, allCascadesRendering);
// setup render pass using any of the cameras, they all have the same pass related properties
this.shadowRenderer.setupRenderPass(renderPass, shadowCamera, allCascadesRendering);
}
return renderPass;
}
}
export { ShadowRendererDirectional };