UNPKG

playcanvas

Version:

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

342 lines (341 loc) 14.1 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 { now } from "../../core/time.js"; import { BoundingSphere } from "../../core/shape/bounding-sphere.js"; import { LIGHTTYPE_DIRECTIONAL, SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME, EVENT_PRECULL, EVENT_POSTCULL, EVENT_CULL_END } from "../constants.js"; const tempSphere = new BoundingSphere(); const _tempSet = /* @__PURE__ */ new Set(); class Culler { /** * @param {Renderer} renderer - The renderer that owns this culler. */ constructor(renderer) { /** * A set of visible mesh instances which need further processing before being rendered, e.g. * skinning or morphing. Extracted during culling. * * @type {Set<MeshInstance>} */ __publicField(this, "processingMeshInstances", /* @__PURE__ */ new Set()); /** * The distinct cameras with mesh-instance cull requests registered for the current frame, in * registration order. Populated by {@link Culler#requestMeshInstanceCull} and drained by * {@link Culler#executeMeshInstanceCull}. Reused across frames to avoid per-frame allocation. * * @type {Camera[]} * @private */ __publicField(this, "_cullCameras", []); /** * A list of unique directional shadow casting lights for each enabled camera. This is generated * each frame during light culling. * * @type {Map<Camera, Array<Light>>} */ __publicField(this, "cameraDirShadowLights", /* @__PURE__ */ new Map()); /** * A mapping of a directional light to a camera, for which the shadow is currently valid. This * is cleared each frame, and updated each time a directional light shadow is rendered for a * camera, and allows us to manually schedule shadow passes when a new camera needs a shadow. * * @type {Map<Light, Camera>} */ __publicField(this, "dirLightShadows", /* @__PURE__ */ new Map()); this.renderer = renderer; } /** * @param {Camera} camera - The camera used for culling. * @param {MeshInstance[]} drawCalls - Draw calls to cull. * @param {CulledInstances} culledInstances - Stores culled instances. */ cullMeshInstances(camera, drawCalls, culledInstances) { const cullTime = now(); const opaque = culledInstances.opaque; opaque.length = 0; const transparent = culledInstances.transparent; transparent.length = 0; const doCull = camera.frustumCulling; const count = drawCalls.length; for (let i = 0; i < count; i++) { const drawCall = drawCalls[i]; if (drawCall.visible) { const visible = !doCull || !drawCall.cull || drawCall._isVisible(camera); if (visible) { drawCall.visibleThisFrame = true; const bucket = drawCall.transparent ? transparent : opaque; bucket.push(drawCall); if (drawCall.skinInstance || drawCall.morphInstance || drawCall.gsplatInstance) { this.processingMeshInstances.add(drawCall); if (drawCall.gsplatInstance) { drawCall.gsplatInstance.cameras.push(camera); } } } } } this.renderer._cullTime += now() - cullTime; this.renderer._numDrawCallsCulled += doCull ? count : 0; } /** * Culls a set of lights against a camera's frustum, marking the visible ones (and updating their * max screen size and physical-units flag). Directional lights are marked visible at the start * of the frame and are skipped here, so only local (omni / spot) lights are frustum tested. In * non-clustered lighting, a shadow-casting light with no shadow map allocated yet is also marked * visible so its shadow map gets allocated. * * @param {Camera} camera - The camera whose frustum the lights are culled against. * @param {Light[]} lights - The lights to cull (typically a layer's lights). */ cullLights(camera, lights) { const { scene } = this.renderer; const clusteredLightingEnabled = scene.clusteredLightingEnabled; const physicalUnits = scene.physicalUnits; for (let i = 0; i < lights.length; i++) { const light = lights[i]; if (light.enabled) { if (light._type !== LIGHTTYPE_DIRECTIONAL) { light.getBoundingSphere(tempSphere); if (camera.frustum.containsSphere(tempSphere)) { light.visibleThisFrame = true; light.usePhysicalUnits = physicalUnits; const screenSize = camera.getScreenSize(tempSphere); light.maxScreenSize = Math.max(light.maxScreenSize, screenSize); } else { if (!clusteredLightingEnabled) { if (light.castShadows && !light.shadowMap) { light.visibleThisFrame = true; } } } } else { light.usePhysicalUnits = scene.physicalUnits; } } } } /** * Shadow map culling for directional and visible local lights visible meshInstances are * collected into light._renderData, and are marked as visible for directional lights also * shadow camera matrix is set up. * * @param {LayerComposition} comp - The layer composition. */ cullShadowmaps(comp) { const { renderer } = this; const localLights = renderer.localLights; for (let i = 0; i < localLights.length; i++) { const light = localLights[i]; if (light._type !== LIGHTTYPE_DIRECTIONAL) { if (light.visibleThisFrame && light.castShadows && light.shadowUpdateMode !== SHADOWUPDATE_NONE) { renderer._shadowRendererLocal.cull(light, comp); } } } this.cameraDirShadowLights.forEach((lightList, camera) => { for (let i = 0; i < lightList.length; i++) { renderer._shadowRendererDirectional.cull(lightList[i], comp, camera); } }); } /** * After the frame graph is built and shadow casters are culled, account for shadow-map updates * and consume one-shot ({@link SHADOWUPDATE_THISFRAME}) requests for lights whose shadow * actually rendered this frame, reverting them to {@link SHADOWUPDATE_NONE}. A light that did * not render this frame (for example an off-screen local light) keeps its request, so the * shadow updates the next frame the light is rendered. Must run after both the frame graph build * and shadow-caster culling, so that `needsShadowRendering` (a pure predicate) reports the same * result to both before the update mode is changed here. */ consumeOneShotShadows() { const { renderer } = this; const clustered = renderer.scene.clusteredLightingEnabled; const shadowRenderer = renderer.shadowRenderer; const localLights = renderer.localLights; for (let i = 0; i < localLights.length; i++) { const light = localLights[i]; if (shadowRenderer.needsShadowRendering(light) && (!clustered || light.atlasViewportAllocated)) { renderer._shadowMapUpdates += light.numShadowFaces; if (light.shadowUpdateMode === SHADOWUPDATE_THISFRAME) { light.shadowUpdateMode = SHADOWUPDATE_NONE; } } } this.cameraDirShadowLights.forEach((lightList) => { for (let i = 0; i < lightList.length; i++) { const light = lightList[i]; if (shadowRenderer.needsShadowRendering(light)) { renderer._shadowMapUpdates += light.numShadowFaces; if (light.shadowUpdateMode === SHADOWUPDATE_THISFRAME) { light.shadowUpdateMode = SHADOWUPDATE_NONE; } } } }); } /** * Collects the set of shadow-casting directional lights for each camera into * {@link Culler#cameraDirShadowLights}, and ensures each such light has a shadow map allocated. * This is independent of mesh culling and camera frusta (it uses only the composition's cameras * and the layers' directional lights), so it can run before the frame graph is built. The * actual shadow-caster culling is done separately. * * @param {LayerComposition} comp - The layer composition. */ collectDirectionalShadowLights(comp) { const { renderer } = this; this.cameraDirShadowLights.clear(); const cameras = comp.cameras; for (let i = 0; i < cameras.length; i++) { const cameraComponent = cameras[i]; if (cameraComponent.enabled) { const camera = cameraComponent.camera; let lightList; const cameraLayers = camera.layers; for (let l = 0; l < cameraLayers.length; l++) { const cameraLayer = comp.getLayerById(cameraLayers[l]); if (cameraLayer) { const layerDirLights = cameraLayer.splitLights[LIGHTTYPE_DIRECTIONAL]; for (let j = 0; j < layerDirLights.length; j++) { const light = layerDirLights[j]; if (light.castShadows && !_tempSet.has(light)) { _tempSet.add(light); lightList = lightList ?? []; lightList.push(light); renderer._shadowRendererDirectional.prepareShadowMap(light); } } } } if (lightList) { this.cameraDirShadowLights.set(camera, lightList); } _tempSet.clear(); } } } /** * Per-camera light visibility culling, light-atlas allocation and directional-shadow-light * collection. This is independent of mesh culling and the frame graph, so it runs before the * frame graph is built. The mesh and shadow-caster culling that depends on it is done later in * {@link Culler#cullComposition}. * * @param {LayerComposition} comp - The layer composition. */ updateLightVisibility(comp) { const { renderer } = this; const { scene } = renderer; const lights = renderer.lights; for (let i = 0; i < lights.length; i++) { lights[i].beginFrame(); } const numCameras = comp.cameras.length; for (let i = 0; i < numCameras; i++) { const camera = comp.cameras[i]; camera.camera.updateFrustum(); const layerIds = camera.layers; for (let j = 0; j < layerIds.length; j++) { const layer = comp.getLayerById(layerIds[j]); if (layer && layer.enabled) { this.cullLights(camera.camera, layer._lights); } } } const isClustered = scene.clusteredLightingEnabled; if (isClustered) { renderer.updateLightTextureAtlas(); } const localLights = renderer.localLights; for (let i = 0; i < localLights.length; i++) { const light = localLights[i]; if (light._type !== LIGHTTYPE_DIRECTIONAL) { if (isClustered) { if (light.atlasSlotUpdated && light.shadowUpdateMode === SHADOWUPDATE_NONE) { light.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } } else { if (light.castShadows && light.visibleThisFrame && !light._shadowMap) { renderer._shadowRendererLocal.prepareShadowMap(light); if (light.shadowUpdateMode === SHADOWUPDATE_NONE) { light.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } } } } } this.collectDirectionalShadowLights(comp); } /** * Registers a request to cull a layer's mesh instances for a camera in the current frame. The * culling itself is performed later, in a single batch, by * {@link Culler#executeMeshInstanceCull}. Requests are de-duplicated per (camera, layer), so * requesting the same pair more than once (e.g. for its opaque and transparent sub-layers) is * harmless. * * This lets the frame graph drive culling - each pass requests the (camera, layer) pairs it * will actually render - instead of culling every camera/layer combination in the composition. * * @param {Camera} camera - The camera to cull for. * @param {Layer} layer - The layer whose mesh instances should be culled. */ requestMeshInstanceCull(camera, layer) { if (camera.addCullLayer(layer)) { this._cullCameras.push(camera); } } /** * Performs all mesh-instance culling requested via {@link Culler#requestMeshInstanceCull} this * frame, then clears the request list. For each requested camera the precull event is fired * (before the frustum is refreshed, so a listener may still adjust the camera), the camera * frustum is updated, each requested layer is culled, and the postcull event is fired. * * The events are passed the owning camera component for a framework camera, or null for an * internal camera (shadow / reflection / picker), matching the documented precull/postcull * contract. */ executeMeshInstanceCull() { const { renderer } = this; const { scene } = renderer; const cameras = this._cullCameras; for (let i = 0; i < cameras.length; i++) { const camera = cameras[i]; const cameraComponent = camera.node?.camera ?? null; scene?.fire(EVENT_PRECULL, cameraComponent); camera.updateFrustum(); for (const layer of camera.cullLayers) { this.cullMeshInstances(camera, layer.meshInstances, layer.getCulledInstances(camera)); } scene?.fire(EVENT_POSTCULL, cameraComponent); camera.clearCullLayers(); } cameras.length = 0; } /** * Visibility culling of meshInstances and shadow casters. Light visibility, the light atlas and * the directional-shadow-light collection are done earlier in * {@link Culler#updateLightVisibility}. * * @param {LayerComposition} comp - The layer composition. */ cullComposition(comp) { const cullTime = now(); const { renderer } = this; const { scene } = renderer; this.processingMeshInstances.clear(); renderer._camerasRendered += comp.cameras.length; this.executeMeshInstanceCull(); this.cullShadowmaps(comp); scene?.fire(EVENT_CULL_END); renderer._cullTime += now() - cullTime; } } export { Culler };