playcanvas
Version:
PlayCanvas WebGL game engine
429 lines (426 loc) • 20.3 kB
JavaScript
import { Debug } from '../../core/debug.js';
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 { UNIFORMTYPE_MAT4, UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT } from '../../platform/graphics/constants.js';
import { DebugGraphics } from '../../platform/graphics/debug-graphics.js';
import { drawQuadWithShader } from '../graphics/quad-render-utils.js';
import { shadowTypeInfo, LIGHTTYPE_OMNI, LIGHTTYPE_DIRECTIONAL, SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME, BLUR_GAUSSIAN, SHADER_SHADOW } from '../constants.js';
import { ShaderPass } from '../shader-pass.js';
import { shaderChunks } from '../shader-lib/chunks/chunks.js';
import { createShaderFromCode } from '../shader-lib/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';
/**
* @import { Camera } from '../camera.js'
* @import { LayerComposition } from '../composition/layer-composition.js'
* @import { LightTextureAtlas } from '../lighting/light-texture-atlas.js'
* @import { Light } from '../light.js'
* @import { MeshInstance } from '../mesh-instance.js'
* @import { Renderer } from './renderer.js'
* @import { ShaderPassInfo } from '../shader-pass.js'
*/ var tempSet = new Set();
var shadowCamView = new Mat4();
var shadowCamViewProj = new Mat4();
var pixelOffset = new Float32Array(2);
var blurScissorRect = new Vec4(1, 1, 0, 0);
var viewportMatrix = new Mat4();
function gauss(x, sigma) {
return Math.exp(-(x * x) / (2.0 * sigma * sigma));
}
function gaussWeights(kernelSize) {
var sigma = (kernelSize - 1) / (2 * 3);
var halfWidth = (kernelSize - 1) * 0.5;
var values = new Array(kernelSize);
var sum = 0.0;
for(var i = 0; i < kernelSize; ++i){
values[i] = gauss(i - halfWidth, sigma);
sum += values[i];
}
for(var i1 = 0; i1 < kernelSize; ++i1){
values[i1] /= sum;
}
return values;
}
class ShadowRenderer {
// creates shadow camera for a light and sets up its constant properties
static createShadowCamera(shadowType, type, face) {
var shadowCam = LightCamera.create('ShadowCamera', type, face);
var shadowInfo = shadowTypeInfo.get(shadowType);
Debug.assert(shadowInfo);
var _shadowInfo_vsm;
var isVsm = (_shadowInfo_vsm = shadowInfo == null ? void 0 : shadowInfo.vsm) != null ? _shadowInfo_vsm : false;
var _shadowInfo_pcf;
var isPcf = (_shadowInfo_pcf = shadowInfo == null ? void 0 : shadowInfo.pcf) != null ? _shadowInfo_pcf : false;
// don't clear the color buffer if rendering a depth map
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;
// clear color buffer only when using it
shadowCam.clearColorBuffer = !isPcf;
return shadowCam;
}
_cullShadowCastersInternal(meshInstances, visible, camera) {
var numInstances = meshInstances.length;
for(var i = 0; i < numInstances; i++){
var meshInstance = meshInstances[i];
if (meshInstance.castShadow) {
if (!meshInstance.cull || meshInstance._isVisible(camera)) {
meshInstance.visibleThisFrame = true;
visible.push(meshInstance);
}
}
}
}
/**
* Culls the list of shadow casters used by the light by the camera, storing visible mesh
* instances in the specified array.
*
* @param {LayerComposition} comp - The layer composition used as a source of shadow casters,
* if those are not provided directly.
* @param {Light} light - The light.
* @param {MeshInstance[]} visible - The array to store visible mesh instances in.
* @param {Camera} camera - The camera.
* @param {MeshInstance[]} [casters] - Optional array of mesh instances to use as casters.
*/ cullShadowCasters(comp, light, visible, camera, casters) {
visible.length = 0;
// if the casters are supplied, use them
if (casters) {
this._cullShadowCastersInternal(casters, visible, camera);
} else {
// for each layer
var layers = comp.layerList;
var len = layers.length;
for(var i = 0; i < len; i++){
var layer = layers[i];
if (layer._lightsSet.has(light)) {
// layer can be in the list two times (opaque, transp), add casters only one time
if (!tempSet.has(layer)) {
tempSet.add(layer);
this._cullShadowCastersInternal(layer.shadowCasters, visible, camera);
}
}
}
tempSet.clear();
}
// this sorts the shadow casters by the shader id
visible.sort(this.sortCompareShader);
}
sortCompareShader(drawCallA, drawCallB) {
var keyA = drawCallA._sortKeyShadow;
var keyB = drawCallB._sortKeyShadow;
if (keyA === keyB) {
return drawCallB.mesh.id - drawCallA.mesh.id;
}
return keyB - keyA;
}
setupRenderState(device, light) {
// Set standard shadowmap states
var isClustered = this.renderer.scene.clusteredLightingEnabled;
var useShadowSampler = isClustered ? light._isPcf : light._isPcf && light._type !== LIGHTTYPE_OMNI; // for non-clustered, point light is using depth encoded in color buffer (should change to shadow sampler)
device.setBlendState(useShadowSampler ? this.blendStateNoWrite : this.blendStateWrite);
device.setDepthState(light.shadowDepthState);
device.setStencilState(null, null);
}
dispatchUniforms(light, shadowCam, lightRenderData, face) {
var shadowCamNode = shadowCam._node;
// position / range
if (light._type !== LIGHTTYPE_DIRECTIONAL) {
this.renderer.dispatchViewPos(shadowCamNode.getPosition());
this.shadowMapLightRadiusId.setValue(light.attenuationEnd);
}
// view-projection shadow matrix
shadowCamView.setTRS(shadowCamNode.getPosition(), shadowCamNode.getRotation(), Vec3.ONE).invert();
shadowCamViewProj.mul2(shadowCam.projectionMatrix, shadowCamView);
// viewport handling
var 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) {
// copy matrix to shadow cascade palette
light._shadowMatrixPalette.set(lightRenderData.shadowMatrix.data, face * 16);
}
}
/**
* @param {Light} light - The light.
* @returns {number} Index of shadow pass info.
*/ getShadowPass(light) {
var _this_shadowPassCache_lightType;
// get shader pass from cache for this light type and shadow type
var lightType = light._type;
var shadowType = light._shadowType;
var shadowPassInfo = (_this_shadowPassCache_lightType = this.shadowPassCache[lightType]) == null ? void 0 : _this_shadowPassCache_lightType[shadowType];
if (!shadowPassInfo) {
// new shader pass if not in cache
var shadowPassName = "ShadowPass_" + lightType + "_" + shadowType;
shadowPassInfo = ShaderPass.get(this.device).allocate(shadowPassName, {
isShadow: true,
lightType: lightType,
shadowType: shadowType
});
// add it to the cache
if (!this.shadowPassCache[lightType]) {
this.shadowPassCache[lightType] = [];
}
this.shadowPassCache[lightType][shadowType] = shadowPassInfo;
}
return shadowPassInfo.index;
}
/**
* @param {MeshInstance[]} visibleCasters - Visible mesh instances.
* @param {Light} light - The light.
* @param {Camera} camera - The camera.
*/ submitCasters(visibleCasters, light, camera) {
var device = this.device;
var renderer = this.renderer;
var scene = renderer.scene;
var passFlags = 1 << SHADER_SHADOW;
var shadowPass = this.getShadowPass(light);
var cameraShaderParams = camera.shaderParams;
// reverse face culling when shadow map has flipY set to true which cases reversed winding order
var flipFactor = camera.renderTarget.flipY ? -1 : 1;
// Render
var count = visibleCasters.length;
for(var i = 0; i < count; i++){
var meshInstance = visibleCasters[i];
var mesh = meshInstance.mesh;
meshInstance.ensureMaterial(device);
var material = meshInstance.material;
DebugGraphics.pushGpuMarker(device, "Node: " + meshInstance.node.name + ", Material: " + material.name);
// set basic material states/parameters
renderer.setBaseConstants(device, material);
renderer.setSkinning(device, meshInstance);
if (material.dirty) {
material.updateUniforms(device, scene);
material.dirty = false;
}
renderer.setupCullMode(true, flipFactor, meshInstance);
// Uniforms I (shadow): material
material.setParameters(device);
// Uniforms II (shadow): meshInstance overrides
meshInstance.setParameters(device, passFlags);
var shaderInstance = meshInstance.getShaderInstance(shadowPass, 0, scene, cameraShaderParams, this.viewUniformFormat, this.viewBindGroupFormat);
var shadowShader = shaderInstance.shader;
Debug.assert(shadowShader, "no shader for pass " + shadowPass, material);
// sort shadow casters by shader
meshInstance._sortKeyShadow = shadowShader.id;
device.setShader(shadowShader);
// set buffers
renderer.setVertexBuffers(device, mesh);
renderer.setMorphing(device, meshInstance.morphInstance);
this.renderer.setupMeshUniformBuffers(shaderInstance, meshInstance);
var style = meshInstance.renderStyle;
device.setIndexBuffer(mesh.indexBuffer[style]);
// draw
renderer.drawInstance(device, meshInstance, mesh, style);
renderer._shadowDrawCalls++;
DebugGraphics.popGpuMarker(device);
}
}
needsShadowRendering(light) {
var 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) {
// directional shadows are per camera, so get appropriate render data
return light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, face);
}
setupRenderPass(renderPass, shadowCamera, clearRenderTarget) {
var rt = shadowCamera.renderTarget;
renderPass.init(rt);
renderPass.depthStencilOps.clearDepthValue = 1;
renderPass.depthStencilOps.clearDepth = clearRenderTarget;
// if rendering to depth buffer
if (rt.depthBuffer) {
renderPass.depthStencilOps.storeDepth = true;
} else {
renderPass.colorOps.clearValue.copy(shadowCamera.clearColor);
renderPass.colorOps.clear = clearRenderTarget;
renderPass.depthStencilOps.storeDepth = false;
}
// not sampling dynamically generated cubemaps
renderPass.requiresCubemaps = false;
}
// prepares render target / render target settings to allow render pass to be set up
prepareFace(light, camera, face) {
var type = light._type;
var lightRenderData = this.getLightRenderData(light, camera, face);
var shadowCam = lightRenderData.shadowCamera;
// assign render target for the face
var renderTargetIndex = type === LIGHTTYPE_DIRECTIONAL ? 0 : face;
shadowCam.renderTarget = light._shadowMap.renderTargets[renderTargetIndex];
return shadowCam;
}
renderFace(light, camera, face, clear, insideRenderPass) {
if (insideRenderPass === void 0) insideRenderPass = true;
var device = this.device;
var shadowMapStartTime = now();
DebugGraphics.pushGpuMarker(device, "SHADOW " + light._node.name + " FACE " + face);
var lightRenderData = this.getLightRenderData(light, camera, face);
var shadowCam = lightRenderData.shadowCamera;
this.dispatchUniforms(light, shadowCam, lightRenderData, face);
var rt = shadowCam.renderTarget;
var renderer = this.renderer;
renderer.setCameraUniforms(shadowCam, rt);
if (device.supportsUniformBuffers) {
renderer.setupViewUniformBuffers(lightRenderData.viewBindGroups, this.viewUniformFormat, this.viewBindGroupFormat, 1);
}
if (insideRenderPass) {
renderer.setupViewport(shadowCam, rt);
// clear here is used to clear a viewport inside render target.
if (clear) {
renderer.clear(shadowCam);
}
} else {
// this is only used by lightmapper, till it's converted to render passes
renderer.clearView(shadowCam, rt, true, false);
}
this.setupRenderState(device, light);
// render mesh instances
this.submitCasters(lightRenderData.visibleCasters, light, shadowCam);
DebugGraphics.popGpuMarker(device);
renderer._shadowMapTime += now() - shadowMapStartTime;
}
render(light, camera, insideRenderPass) {
if (insideRenderPass === void 0) insideRenderPass = true;
if (this.needsShadowRendering(light)) {
var faceCount = light.numShadowFaces;
// render faces
for(var face = 0; face < faceCount; face++){
this.prepareFace(light, camera, face);
this.renderFace(light, camera, face, true, insideRenderPass);
}
// apply vsm
this.renderVsm(light, camera);
}
}
renderVsm(light, camera) {
// VSM blur if light supports vsm (directional and spot in general)
if (light._isVsm && light._vsmBlurSize > 1) {
// in clustered mode, only directional light can be vms
var isClustered = this.renderer.scene.clusteredLightingEnabled;
if (!isClustered || light._type === LIGHTTYPE_DIRECTIONAL) {
this.applyVsmBlur(light, camera);
}
}
}
getVsmBlurShader(blurMode, filterSize) {
var cache = this.blurVsmShader;
var blurShader = cache[blurMode][filterSize];
if (!blurShader) {
this.blurVsmWeights[filterSize] = gaussWeights(filterSize);
var blurVS = shaderChunks.fullscreenQuadVS;
var blurFS = "#define SAMPLES " + filterSize + "\n";
blurFS += this.blurVsmShaderCode[blurMode];
var blurShaderName = "blurVsm" + blurMode + filterSize;
blurShader = createShaderFromCode(this.device, blurVS, blurFS, blurShaderName);
cache[blurMode][filterSize] = blurShader;
}
return blurShader;
}
applyVsmBlur(light, camera) {
var device = this.device;
DebugGraphics.pushGpuMarker(device, "VSM " + light._node.name);
// render state
device.setBlendState(BlendState.NOBLEND);
var lightRenderData = light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, 0);
var shadowCam = lightRenderData.shadowCamera;
var origShadowMap = shadowCam.renderTarget;
// temporary render target for blurring
// TODO: this is probably not optimal and shadow map could have depth buffer on in addition to color buffer,
// and for blurring only one buffer is needed.
var tempShadowMap = this.renderer.shadowMapCache.get(device, light);
var tempRt = tempShadowMap.renderTargets[0];
var blurMode = light.vsmBlurMode;
var filterSize = light._vsmBlurSize;
var blurShader = this.getVsmBlurShader(blurMode, filterSize);
blurScissorRect.z = light._shadowResolution - 2;
blurScissorRect.w = blurScissorRect.z;
// Blur horizontal
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);
// Blur vertical
this.sourceId.setValue(tempRt.colorBuffer);
pixelOffset[1] = pixelOffset[0];
pixelOffset[0] = 0;
this.pixelOffsetId.setValue(pixelOffset);
drawQuadWithShader(device, origShadowMap, blurShader, null, blurScissorRect);
// return the temporary shadow map back to the cache
this.renderer.shadowMapCache.add(light, tempShadowMap);
DebugGraphics.popGpuMarker(device);
}
initViewBindGroupFormat() {
if (this.device.supportsUniformBuffers && !this.viewUniformFormat) {
// format of the view uniform buffer
this.viewUniformFormat = new UniformBufferFormat(this.device, [
new UniformFormat('matrix_viewProjection', UNIFORMTYPE_MAT4)
]);
// format of the view bind group - contains single uniform buffer, and no textures
this.viewBindGroupFormat = new BindGroupFormat(this.device, [
new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT)
]);
}
}
frameUpdate() {
this.initViewBindGroupFormat();
}
/**
* @param {Renderer} renderer - The renderer.
* @param {LightTextureAtlas} lightTextureAtlas - The shadow map atlas.
*/ constructor(renderer, lightTextureAtlas){
/**
* A cache of shadow passes. First index is looked up by light type, second by shadow type.
*
* @type {ShaderPassInfo[][]}
* @private
*/ this.shadowPassCache = [];
this.device = renderer.device;
/** @type {Renderer} */ this.renderer = renderer;
/** @type {LightTextureAtlas} */ this.lightTextureAtlas = lightTextureAtlas;
var scope = this.device.scope;
// VSM
this.sourceId = scope.resolve('source');
this.pixelOffsetId = scope.resolve('pixelOffset');
this.weightId = scope.resolve('weight[0]');
this.blurVsmShaderCode = [
shaderChunks.blurVSMPS,
"#define GAUSS\n" + shaderChunks.blurVSMPS
];
// cache for vsm blur shaders
this.blurVsmShader = [
{},
{}
];
this.blurVsmWeights = {};
// uniforms
this.shadowMapLightRadiusId = scope.resolve('light_radius');
// view bind group format with its uniform buffer format
this.viewUniformFormat = null;
this.viewBindGroupFormat = null;
// blend states
this.blendStateWrite = new BlendState();
this.blendStateNoWrite = new BlendState();
this.blendStateNoWrite.setColorWrite(false, false, false, false);
}
}
export { ShadowRenderer };