playcanvas
Version:
PlayCanvas WebGL game engine
348 lines (345 loc) • 14.3 kB
JavaScript
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, UNIFORMTYPE_MAT4, UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT } from '../../platform/graphics/constants.js';
import { drawQuadWithShader } from '../graphics/quad-render-utils.js';
import { shadowTypeInfo, EVENT_PRECULL, EVENT_POSTCULL, LIGHTTYPE_OMNI, LIGHTTYPE_DIRECTIONAL, SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME, BLUR_GAUSSIAN, SHADER_SHADOW } 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 { BindGroupFormat, BindUniformBufferFormat } from '../../platform/graphics/bind-group-format.js';
import { BlendState } from '../../platform/graphics/blend-state.js';
const tempSet = 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.0 * 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.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 {
constructor(renderer, lightTextureAtlas){
this.shadowPassCache = [];
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);
}
static createShadowCamera(shadowType, type, face) {
const shadowCam = LightCamera.create('ShadowCamera', 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 : 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: lightType,
shadowType: 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.setupCullMode(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 indirectSlot = meshInstance.indirectData?.get(camera);
device.draw(mesh.primitive[style], mesh.indexBuffer[style], instancingData?.count, indirectSlot);
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;
}
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, insideRenderPass = true) {
const device = this.device;
const shadowMapStartTime = now();
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);
}
if (insideRenderPass) {
renderer.setupViewport(shadowCam, rt);
if (clear) {
renderer.clear(shadowCam);
}
} else {
renderer.clearView(shadowCam, rt, true, false);
}
this.setupRenderState(device, light);
this.submitCasters(lightRenderData.visibleCasters, light, shadowCam);
renderer._shadowMapTime += now() - shadowMapStartTime;
}
render(light, camera, insideRenderPass = true) {
if (this.needsShadowRendering(light)) {
const faceCount = light.numShadowFaces;
for(let face = 0; face < faceCount; face++){
this.prepareFace(light, camera, face);
this.renderFace(light, camera, face, true, insideRenderPass);
}
this.renderVsm(light, camera);
}
}
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 = 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 };