UNPKG

playcanvas

Version:

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

349 lines (348 loc) 13.3 kB
import { now } from "../../core/time.js"; import { Color } from "../../core/math/color.js"; import { Mat4 } from "../../core/math/mat4.js"; import { Vec3 } from "../../core/math/vec3.js"; import { Vec4 } from "../../core/math/vec4.js"; import { SEMANTIC_POSITION, SHADERSTAGE_FRAGMENT, SHADERSTAGE_VERTEX, UNIFORMTYPE_MAT4, UNIFORM_BUFFER_DEFAULT_SLOT_NAME } from "../../platform/graphics/constants.js"; import { drawQuadWithShader } from "../graphics/quad-render-utils.js"; import { BLUR_GAUSSIAN, EVENT_POSTCULL, EVENT_PRECULL, LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, SHADER_SHADOW, SHADOWCAMERA_NAME, SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME, shadowTypeInfo } from "../constants.js"; import { ShaderPass } from "../shader-pass.js"; import { ShaderUtils } from "../shader-lib/shader-utils.js"; import { LightCamera } from "./light-camera.js"; import { UniformBufferFormat, UniformFormat } from "../../platform/graphics/uniform-buffer-format.js"; import { BindUniformBufferFormat, BindGroupFormat } from "../../platform/graphics/bind-group-format.js"; import { BlendState } from "../../platform/graphics/blend-state.js"; const tempSet = /* @__PURE__ */ new Set(); const shadowCamView = new Mat4(); const shadowCamViewProj = new Mat4(); const pixelOffset = new Float32Array(2); const blurScissorRect = new Vec4(1, 1, 0, 0); const viewportMatrix = new Mat4(); function gauss(x, sigma) { return Math.exp(-(x * x) / (2 * sigma * sigma)); } function gaussWeights(kernelSize) { const sigma = (kernelSize - 1) / (2 * 3); const halfWidth = (kernelSize - 1) * 0.5; const values = new Array(kernelSize); let sum = 0; for (let i = 0; i < kernelSize; ++i) { values[i] = gauss(i - halfWidth, sigma); sum += values[i]; } for (let i = 0; i < kernelSize; ++i) { values[i] /= sum; } return values; } class ShadowRenderer { shadowPassCache = []; constructor(renderer, lightTextureAtlas) { this.device = renderer.device; this.renderer = renderer; this.lightTextureAtlas = lightTextureAtlas; const scope = this.device.scope; this.sourceId = scope.resolve("source"); this.pixelOffsetId = scope.resolve("pixelOffset"); this.weightId = scope.resolve("weight[0]"); this.blurVsmShader = [{}, {}]; this.blurVsmWeights = {}; this.shadowMapLightRadiusId = scope.resolve("light_radius"); this.viewUniformFormat = null; this.viewBindGroupFormat = null; this.blendStateWrite = new BlendState(); this.blendStateNoWrite = new BlendState(); this.blendStateNoWrite.setColorWrite(false, false, false, false); } // creates shadow camera for a light and sets up its constant properties static createShadowCamera(device, shadowType, type, face) { const shadowCam = LightCamera.create(device, SHADOWCAMERA_NAME, type, face); const shadowInfo = shadowTypeInfo.get(shadowType); const isVsm = shadowInfo?.vsm ?? false; const isPcf = shadowInfo?.pcf ?? false; if (isVsm) { shadowCam.clearColor = new Color(0, 0, 0, 0); } else { shadowCam.clearColor = new Color(1, 1, 1, 1); } shadowCam.clearDepthBuffer = true; shadowCam.clearStencilBuffer = false; shadowCam.clearColorBuffer = !isPcf; return shadowCam; } _cullShadowCastersInternal(meshInstances, visible, camera) { const numInstances = meshInstances.length; for (let i = 0; i < numInstances; i++) { const meshInstance = meshInstances[i]; if (meshInstance.castShadow) { if (!meshInstance.cull || meshInstance._isVisible(camera)) { meshInstance.visibleThisFrame = true; visible.push(meshInstance); } } } } cullShadowCasters(comp, light, visible, camera, casters) { this.renderer.scene?.fire(EVENT_PRECULL, camera); visible.length = 0; if (casters) { this._cullShadowCastersInternal(casters, visible, camera); } else { const layers = comp.layerList; const len = layers.length; for (let i = 0; i < len; i++) { const layer = layers[i]; if (layer._lightsSet.has(light)) { if (!tempSet.has(layer)) { tempSet.add(layer); this._cullShadowCastersInternal(layer.shadowCasters, visible, camera); } } } tempSet.clear(); } visible.sort(this.sortCompareShader); this.renderer.scene?.fire(EVENT_POSTCULL, camera); } sortCompareShader(drawCallA, drawCallB) { const keyA = drawCallA._sortKeyShadow; const keyB = drawCallB._sortKeyShadow; if (keyA === keyB) { return drawCallB.mesh.id - drawCallA.mesh.id; } return keyB - keyA; } setupRenderState(device, light) { const isClustered = this.renderer.scene.clusteredLightingEnabled; const useShadowSampler = isClustered ? light._isPcf : ( // both spot and omni light are using shadow sampler when clustered light._isPcf && light._type !== LIGHTTYPE_OMNI ); device.setBlendState(useShadowSampler ? this.blendStateNoWrite : this.blendStateWrite); device.setDepthState(light.shadowDepthState); device.setStencilState(null, null); } dispatchUniforms(light, shadowCam, lightRenderData, face) { const shadowCamNode = shadowCam._node; if (light._type !== LIGHTTYPE_DIRECTIONAL) { this.renderer.dispatchViewPos(shadowCamNode.getPosition()); this.shadowMapLightRadiusId.setValue(light.attenuationEnd); } shadowCamView.setTRS(shadowCamNode.getPosition(), shadowCamNode.getRotation(), Vec3.ONE).invert(); shadowCamViewProj.mul2(shadowCam.projectionMatrix, shadowCamView); const rectViewport = lightRenderData.shadowViewport; shadowCam.rect = rectViewport; shadowCam.scissorRect = lightRenderData.shadowScissor; viewportMatrix.setViewport(rectViewport.x, rectViewport.y, rectViewport.z, rectViewport.w); lightRenderData.shadowMatrix.mul2(viewportMatrix, shadowCamViewProj); if (light._type === LIGHTTYPE_DIRECTIONAL) { light._shadowMatrixPalette.set(lightRenderData.shadowMatrix.data, face * 16); } } getShadowPass(light) { const lightType = light._type; const shadowType = light._shadowType; let shadowPassInfo = this.shadowPassCache[lightType]?.[shadowType]; if (!shadowPassInfo) { const shadowPassName = `ShadowPass_${lightType}_${shadowType}`; shadowPassInfo = ShaderPass.get(this.device).allocate(shadowPassName, { isShadow: true, lightType, shadowType }); if (!this.shadowPassCache[lightType]) { this.shadowPassCache[lightType] = []; } this.shadowPassCache[lightType][shadowType] = shadowPassInfo; } return shadowPassInfo.index; } submitCasters(visibleCasters, light, camera) { const device = this.device; const renderer = this.renderer; const scene = renderer.scene; const passFlags = 1 << SHADER_SHADOW; const shadowPass = this.getShadowPass(light); const cameraShaderParams = camera.shaderParams; const flipFactor = camera.renderTarget.flipY ? -1 : 1; const count = visibleCasters.length; for (let i = 0; i < count; i++) { const meshInstance = visibleCasters[i]; const mesh = meshInstance.mesh; const instancingData = meshInstance.instancingData; if (instancingData && instancingData.count <= 0) { continue; } meshInstance.ensureMaterial(device); const material = meshInstance.material; renderer.setBaseConstants(device, material); renderer.setSkinning(device, meshInstance); if (material.dirty) { material.updateUniforms(device, scene); material.dirty = false; } renderer.setupCullModeAndFrontFace(true, flipFactor, meshInstance); material.setParameters(device); meshInstance.setParameters(device, passFlags); const shaderInstance = meshInstance.getShaderInstance(shadowPass, 0, scene, cameraShaderParams, this.viewUniformFormat, this.viewBindGroupFormat); const shadowShader = shaderInstance.shader; if (shadowShader.failed) continue; meshInstance._sortKeyShadow = shadowShader.id; device.setShader(shadowShader); renderer.setVertexBuffers(device, mesh); renderer.setMorphing(device, meshInstance.morphInstance); if (instancingData) { device.setVertexBuffer(instancingData.vertexBuffer); } renderer.setMeshInstanceMatrices(meshInstance); renderer.setupMeshUniformBuffers(shaderInstance); const style = meshInstance.renderStyle; const indirectData = meshInstance.getDrawCommands(camera); device.draw(mesh.primitive[style], mesh.indexBuffer[style], instancingData?.count, indirectData); renderer._shadowDrawCalls++; if (instancingData) { renderer._instancedDrawCalls++; } } } needsShadowRendering(light) { const needs = light.enabled && light.castShadows && light.shadowUpdateMode !== SHADOWUPDATE_NONE && light.visibleThisFrame; if (light.shadowUpdateMode === SHADOWUPDATE_THISFRAME) { light.shadowUpdateMode = SHADOWUPDATE_NONE; } if (needs) { this.renderer._shadowMapUpdates += light.numShadowFaces; } return needs; } getLightRenderData(light, camera, face) { return light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, face); } setupRenderPass(renderPass, shadowCamera, clearRenderTarget) { const rt = shadowCamera.renderTarget; renderPass.init(rt); renderPass.depthStencilOps.clearDepthValue = 1; renderPass.depthStencilOps.clearDepth = clearRenderTarget; if (rt.depthBuffer) { renderPass.depthStencilOps.storeDepth = true; } else { renderPass.colorOps.clearValue.copy(shadowCamera.clearColor); renderPass.colorOps.clear = clearRenderTarget; renderPass.depthStencilOps.storeDepth = false; } renderPass.requiresCubemaps = false; } // prepares render target / render target settings to allow render pass to be set up prepareFace(light, camera, face) { const type = light._type; const lightRenderData = this.getLightRenderData(light, camera, face); const shadowCam = lightRenderData.shadowCamera; const renderTargetIndex = type === LIGHTTYPE_DIRECTIONAL ? 0 : face; shadowCam.renderTarget = light._shadowMap.renderTargets[renderTargetIndex]; return shadowCam; } renderFace(light, camera, face, clear) { const device = this.device; const lightRenderData = this.getLightRenderData(light, camera, face); const shadowCam = lightRenderData.shadowCamera; this.dispatchUniforms(light, shadowCam, lightRenderData, face); const rt = shadowCam.renderTarget; const renderer = this.renderer; renderer.setCameraUniforms(shadowCam, rt); if (device.supportsUniformBuffers) { renderer.setupViewUniformBuffers(lightRenderData.viewBindGroups, this.viewUniformFormat, this.viewBindGroupFormat, null); } renderer.setupViewport(shadowCam, rt); if (clear) { renderer.clear(shadowCam); } this.setupRenderState(device, light); this.submitCasters(lightRenderData.visibleCasters, light, shadowCam); } renderVsm(light, camera) { if (light._isVsm && light._vsmBlurSize > 1) { const isClustered = this.renderer.scene.clusteredLightingEnabled; if (!isClustered || light._type === LIGHTTYPE_DIRECTIONAL) { this.applyVsmBlur(light, camera); } } } getVsmBlurShader(blurMode, filterSize) { const cache = this.blurVsmShader; let blurShader = cache[blurMode][filterSize]; if (!blurShader) { this.blurVsmWeights[filterSize] = gaussWeights(filterSize); const defines = /* @__PURE__ */ new Map(); defines.set("{SAMPLES}", filterSize); if (blurMode === 1) defines.set("GAUSS", ""); blurShader = ShaderUtils.createShader(this.device, { uniqueName: `blurVsm${blurMode}${filterSize}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: "fullscreenQuadVS", fragmentChunk: "blurVSMPS", fragmentDefines: defines }); cache[blurMode][filterSize] = blurShader; } return blurShader; } applyVsmBlur(light, camera) { const device = this.device; device.setBlendState(BlendState.NOBLEND); const lightRenderData = light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, 0); const shadowCam = lightRenderData.shadowCamera; const origShadowMap = shadowCam.renderTarget; const tempShadowMap = this.renderer.shadowMapCache.get(device, light); const tempRt = tempShadowMap.renderTargets[0]; const blurMode = light.vsmBlurMode; const filterSize = light._vsmBlurSize; const blurShader = this.getVsmBlurShader(blurMode, filterSize); blurScissorRect.z = light._shadowResolution - 2; blurScissorRect.w = blurScissorRect.z; this.sourceId.setValue(origShadowMap.colorBuffer); pixelOffset[0] = 1 / light._shadowResolution; pixelOffset[1] = 0; this.pixelOffsetId.setValue(pixelOffset); if (blurMode === BLUR_GAUSSIAN) this.weightId.setValue(this.blurVsmWeights[filterSize]); drawQuadWithShader(device, tempRt, blurShader, null, blurScissorRect); this.sourceId.setValue(tempRt.colorBuffer); pixelOffset[1] = pixelOffset[0]; pixelOffset[0] = 0; this.pixelOffsetId.setValue(pixelOffset); drawQuadWithShader(device, origShadowMap, blurShader, null, blurScissorRect); this.renderer.shadowMapCache.add(light, tempShadowMap); } initViewBindGroupFormat() { if (this.device.supportsUniformBuffers && !this.viewUniformFormat) { this.viewUniformFormat = new UniformBufferFormat(this.device, [ new UniformFormat("matrix_viewProjection", UNIFORMTYPE_MAT4) ]); this.viewBindGroupFormat = new BindGroupFormat(this.device, [ new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT) ]); } } frameUpdate() { this.initViewBindGroupFormat(); } } export { ShadowRenderer };