UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

225 lines (224 loc) 9.71 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); 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 { LIGHTTYPE_DIRECTIONAL, SHADOWUPDATE_NONE } from "../constants.js"; import { ShadowMap } from "./shadow-map.js"; import { RenderPassShadowDirectional } from "./render-pass-shadow-directional.js"; const _unionSceneAabb = new BoundingBox(); const _cascadeAabbs = [new BoundingBox(), new BoundingBox(), new BoundingBox(), new BoundingBox()]; const _cascadeAabbValid = [false, false, false, false]; const _cascadeRadii = [0, 0, 0, 0]; 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() ]; 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) { /** @type {Renderer} */ __publicField(this, "renderer"); /** @type {ShadowRenderer} */ __publicField(this, "shadowRenderer"); /** @type {GraphicsDevice} */ __publicField(this, "device"); this.renderer = renderer; this.shadowRenderer = shadowRenderer; this.device = renderer.device; } // Minimal prerequisite for directional shadow-pass creation: ensure the shadow map exists // and the light is marked visible. This is caster- and camera-independent, so it can run // early in the frame (before mesh culling), unlike cull() which sets up the per-cascade // shadow cameras and culls casters. prepareShadowMap(light) { light.visibleThisFrame = true; if (!light._shadowMap) { light._shadowMap = ShadowMap.create(this.device, light); } } // cull directional shadow map. prepareShadowMap(light) must have been called first. cull(light, comp, camera, casters = null) { Debug.assert(light._shadowMap, "ShadowRendererDirectional.cull requires prepareShadowMap() to have been called for the light first."); const nearDist = camera._nearClip; this.generateSplitDistances(light, nearDist, Math.min(camera._farClip, light.shadowDistance)); const shadowUpdateOverrides = light.shadowUpdateOverrides; let numActiveCascades = 0; for (let cascade = 0; cascade < light.numCascades; cascade++) { if (shadowUpdateOverrides?.[cascade] === SHADOWUPDATE_NONE) { break; } const lightRenderData = light.getRenderData(camera, cascade); const shadowCam = lightRenderData.shadowCamera; shadowCam.renderTarget = light._shadowMap.renderTargets[0]; lightRenderData.shadowViewport.copy(light.cascades[cascade]); lightRenderData.shadowScissor.copy(light.cascades[cascade]); const shadowCamNode = shadowCam._node; const lightNode = light._node; shadowCamNode.setPosition(lightNode.getPosition()); shadowCamNode.setRotation(lightNode.getRotation()); shadowCamNode.rotateLocal(-90, 0, 0); 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); let radius = 0; for (let i = 0; i < 8; i++) { const dist = frustumPoints[i].sub(center).length(); if (dist > radius) { radius = dist; } } const right = shadowCamNode.right; const up = shadowCamNode.up; const lightDir = shadowCamNode.forward; 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); shadowCamNode.setPosition(center); shadowCamNode.translateLocal(0, 0, 1e6); shadowCam.nearClip = 0.01; shadowCam.farClip = 2e6; shadowCam.orthoHeight = radius; shadowCam.updateFrustum(); this.shadowRenderer.cullShadowCasters(comp, light, lightRenderData.visibleCasters, shadowCam, casters); const cascadeFlag = 1 << cascade; const visibleCasters = lightRenderData.visibleCasters; const origNumVisibleCasters = visibleCasters.length; let numVisibleCasters = 0; const cascadeAabb = _cascadeAabbs[cascade]; for (let i = 0; i < origNumVisibleCasters; i++) { const meshInstance = visibleCasters[i]; if (meshInstance.shadowCascadeMask & cascadeFlag) { visibleCasters[numVisibleCasters++] = meshInstance; if (numVisibleCasters === 1) { cascadeAabb.copy(meshInstance.aabb); } else { cascadeAabb.add(meshInstance.aabb); } } } if (origNumVisibleCasters !== numVisibleCasters) { visibleCasters.length = numVisibleCasters; } _cascadeAabbValid[cascade] = numVisibleCasters > 0; _cascadeRadii[cascade] = radius; numActiveCascades++; } let useUnion = false; if (light._isPcss) { for (let cascade = 0; cascade < numActiveCascades; cascade++) { if (!_cascadeAabbValid[cascade]) continue; if (!useUnion) { _unionSceneAabb.copy(_cascadeAabbs[cascade]); useUnion = true; } else { _unionSceneAabb.add(_cascadeAabbs[cascade]); } } } for (let cascade = 0; cascade < numActiveCascades; cascade++) { let aabbSource; if (useUnion) { aabbSource = _unionSceneAabb; } else if (_cascadeAabbValid[cascade]) { aabbSource = _cascadeAabbs[cascade]; } else { continue; } const lightRenderData = light.getRenderData(camera, cascade); const shadowCam = lightRenderData.shadowCamera; const shadowCamNode = shadowCam._node; shadowCamView.copy(shadowCamNode.getWorldTransform()).invert(); const depthRange = getDepthRange(shadowCamView, aabbSource.getMin(), aabbSource.getMax()); shadowCamNode.translateLocal(0, 0, depthRange.max + 0.1); shadowCam.farClip = depthRange.max - depthRange.min + 0.2; lightRenderData.projectionCompensation = _cascadeRadii[cascade]; } } // function to generate frustum split distances generateSplitDistances(light, nearDist, farDist) { light._shadowCascadeDistances.fill(farDist); for (let i = 1; i < light.numCascades; i++) { 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)) { const faceCount = light.numShadowFaces; const shadowUpdateOverrides = light.shadowUpdateOverrides; 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); this.shadowRenderer.setupRenderPass(renderPass, shadowCamera, allCascadesRendering); } return renderPass; } } export { ShadowRendererDirectional };