Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved shadow cascades rendering, allowing per cascade update #4921

Merged
merged 6 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 81 additions & 11 deletions examples/src/examples/graphics/shadow-cascades.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import * as pc from '../../../../';

import { BindingTwoWay } from '@playcanvas/pcui';
import { LabelGroup, Panel, SelectInput, SliderInput } from '@playcanvas/pcui/react';
import { BooleanInput, LabelGroup, Panel, SelectInput, SliderInput } from '@playcanvas/pcui/react';
import { Observer } from '@playcanvas/observer';

class ShadowCascadesExample {
Expand All @@ -25,6 +25,9 @@ class ShadowCascadesExample {
<LabelGroup text='Count'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'settings.light.numCascades' }} min={1} max={4} precision={0}/>
</LabelGroup>
<LabelGroup text='Every Frame'>
<BooleanInput type='toggle' binding={new BindingTwoWay()} link={{ observer: data, path: 'settings.light.everyFrame' }} value={data.get('settings.light.everyFrame')}/>
</LabelGroup>
<LabelGroup text='Resolution'>
<SliderInput binding={new BindingTwoWay()} link={{ observer: data, path: 'settings.light.shadowResolution' }} min={128} max={2048} precision={0}/>
</LabelGroup>
Expand Down Expand Up @@ -61,7 +64,8 @@ class ShadowCascadesExample {
shadowResolution: 2048, // shadow map resolution storing 4 cascades
cascadeDistribution: 0.5, // distribution of cascade distances to prefer sharpness closer to the camera
shadowType: pc.SHADOW_PCF3, // shadow filter type
vsmBlurSize: 11 // shader filter blur size for VSM shadows
vsmBlurSize: 11, // shader filter blur size for VSM shadows
everyFrame: true // true if all cascades update every frame
}
});

Expand All @@ -84,6 +88,34 @@ class ShadowCascadesExample {
terrain.setLocalScale(30, 30, 30);
app.root.addChild(terrain);

// get the clouds so that we can animate them
const srcClouds : Array<pc.Entity> = terrain.find((node: pc.GraphNode) => {

const isCloud = node.name.includes('Icosphere');

if (isCloud) {
// no shadow receiving for clouds
(node as pc.Entity).render.receiveShadows = false;
}

return isCloud;
});

// clone some additional clouds
const clouds : Array<pc.Entity> = [];
srcClouds.forEach((cloud) => {
clouds.push(cloud);

for (let i = 0; i < 3; i++) {
const clone = cloud.clone() as pc.Entity;
cloud.parent.addChild(clone);
clouds.push(clone);
}
});

// shuffle the array to give clouds random order
clouds.sort(() => Math.random() - 0.5);

// find a tree in the middle to use as a focus point
const tree = terrain.findOne("name", "Arbol 2.002");

Expand All @@ -95,7 +127,7 @@ class ShadowCascadesExample {
});

// and position it in the world
camera.setLocalPosition(300, 60, 25);
camera.setLocalPosition(300, 160, 25);

// add orbit camera script with a mouse and a touch support
camera.addComponent("script");
Expand Down Expand Up @@ -129,21 +161,59 @@ class ShadowCascadesExample {
app.root.addChild(dirLight);
dirLight.setLocalEulerAngles(45, 350, 20);

// update mode of cascades
let updateEveryFrame = true;

// handle HUD changes - update properties on the light
data.on('*:set', (path: string, value: any) => {
const pathArray = path.split('.');
// @ts-ignore
dirLight.light[pathArray[2]] = value;

if (pathArray[2] === 'everyFrame') {
updateEveryFrame = value;
} else {
// @ts-ignore
dirLight.light[pathArray[2]] = value;
}
});

// on the first frame, when camera is updated, move it further away from the focus tree
let firstFrame = true;
app.on("update", function () {
if (firstFrame) {
firstFrame = false;
const cloudSpeed = 0.2;
let frameNumber = 0;
let time = 0;
app.on("update", function (dt: number) {

time += dt;

// on the first frame, when camera is updated, move it further away from the focus tree
if (frameNumber === 0) {
// @ts-ignore engine-tsd
camera.script.orbitCamera.distance = 320;
camera.script.orbitCamera.distance = 470;
}

if (updateEveryFrame) {

// no per cascade rendering control
dirLight.light.faceUpdateModes = null;

} else {

// set up shadow update modes, nearest cascade updates each frame, then next one every 5 and so on
dirLight.light.faceUpdateModes = [
pc.SHADOWUPDATE_THISFRAME,
(frameNumber % 5) === 0 ? pc.SHADOWUPDATE_THISFRAME : pc.SHADOWUPDATE_NONE,
(frameNumber % 10) === 0 ? pc.SHADOWUPDATE_THISFRAME : pc.SHADOWUPDATE_NONE,
(frameNumber % 15) === 0 ? pc.SHADOWUPDATE_THISFRAME : pc.SHADOWUPDATE_NONE
];
}

// move the clouds around
clouds.forEach((cloud, index: number) => {
const redialOffset = (index / clouds.length) * (6.24 / cloudSpeed);
const radius = 8 + 2 * Math.sin(redialOffset);
const cloudTime = time + redialOffset;
cloud.setLocalPosition(radius * Math.sin(cloudTime * cloudSpeed), 4, radius * Math.cos(cloudTime * cloudSpeed));
});

frameNumber++;
});
});
}
Expand Down
17 changes: 13 additions & 4 deletions src/framework/components/light/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,6 @@ class LightComponent extends Component {
this.onEnable();
}

updateShadow() {
this.light.updateShadow();
}

onCookieAssetSet() {
let forceLoad = false;

Expand Down Expand Up @@ -321,6 +317,19 @@ class LightComponent extends Component {
// remove cookie asset events
this.cookieAsset = null;
}

/**
* Returns an array of SHADOWUPDATE_ setting per shadow cascade, or undefined if not used.
*
* @type {number[]}
*/
set faceUpdateModes(values) {
mvaligursky marked this conversation as resolved.
Show resolved Hide resolved
this.light.faceUpdateModes = values;
}

get faceUpdateModes() {
return this.light.faceUpdateModes;
}
}

function _defineProperty(name, defaultValue, setFunc, skipEqualsCheck) {
Expand Down
25 changes: 18 additions & 7 deletions src/scene/light.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Vec2 } from '../core/math/vec2.js';
import { Vec3 } from '../core/math/vec3.js';
import { Vec4 } from '../core/math/vec4.js';

import { DEVICETYPE_WEBGPU } from '../platform/graphics/constants.js';

import {
BLUR_GAUSSIAN,
LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, LIGHTTYPE_SPOT,
Expand Down Expand Up @@ -171,6 +173,7 @@ class Light {
this.shadowIntensity = 1.0;
this._normalOffsetBias = 0.0;
this.shadowUpdateMode = SHADOWUPDATE_REALTIME;
this.faceUpdateModes = null;
this._isVsm = false;
this._isPcf = true;

Expand Down Expand Up @@ -250,6 +253,7 @@ class Light {

const stype = this._shadowType;
this._shadowType = null;
this.faceUpdateModes = null;
this.shadowType = stype; // refresh shadow type; switching from direct/spot to omni and back may change it
}

Expand Down Expand Up @@ -294,7 +298,8 @@ class Light {
if (this._type === LIGHTTYPE_OMNI)
value = SHADOW_PCF3; // VSM or HW PCF for omni lights is not supported yet

if (value === SHADOW_PCF5 && !device.webgl2) {
const supportsPCF5 = device.webgl2 || device.deviceType === DEVICETYPE_WEBGPU;
if (value === SHADOW_PCF5 && !supportsPCF5) {
value = SHADOW_PCF3; // fallback from HW PCF to old PCF
}

Expand Down Expand Up @@ -574,6 +579,14 @@ class Light {
if (this.shadowUpdateMode === SHADOWUPDATE_NONE) {
this.shadowUpdateMode = SHADOWUPDATE_THISFRAME;
}

if (this.faceUpdateModes) {
for (let i = 0; i < this.faceUpdateModes.length; i++) {
if (this.faceUpdateModes[i] === SHADOWUPDATE_NONE) {
this.faceUpdateModes[i] = SHADOWUPDATE_THISFRAME;
}
}
}
}

// returns LightRenderData with matching camera and face
Expand Down Expand Up @@ -620,6 +633,10 @@ class Light {
clone.shadowUpdateMode = this.shadowUpdateMode;
clone.mask = this.mask;

if (this.faceUpdateModes) {
clone.faceUpdateModes = this.faceUpdateModes.slice();
}

// Spot properties
clone.innerConeAngle = this._innerConeAngle;
clone.outerConeAngle = this._outerConeAngle;
Expand Down Expand Up @@ -801,12 +818,6 @@ class Light {
this._updateFinalColor();
}

updateShadow() {
if (this.shadowUpdateMode !== SHADOWUPDATE_REALTIME) {
this.shadowUpdateMode = SHADOWUPDATE_THISFRAME;
}
}

layersDirty() {
if (this._scene?.layers) {
this._scene.layers._dirtyLights = true;
Expand Down
41 changes: 38 additions & 3 deletions src/scene/renderer/shadow-renderer-directional.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Debug, DebugHelper } 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
LIGHTTYPE_DIRECTIONAL, SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME
} from '../constants.js';
import { RenderPass } from '../../platform/graphics/render-pass.js';

Expand Down Expand Up @@ -63,8 +64,14 @@ class ShadowRendererDirectional extends ShadowRenderer {
const nearDist = camera._nearClip;
this.generateSplitDistances(light, nearDist, light.shadowDistance);

const faceUpdateModes = light.faceUpdateModes;
for (let cascade = 0; cascade < light.numCascades; cascade++) {

// if manually controlling cascade rendering and the cascade does not render this frame
if (faceUpdateModes?.[cascade] === SHADOWUPDATE_NONE) {
break;
}

const lightRenderData = light.getRenderData(camera, cascade);
const shadowCam = lightRenderData.shadowCamera;

Expand Down Expand Up @@ -160,22 +167,50 @@ class ShadowRendererDirectional extends ShadowRenderer {
}
}

// function to generate frustum split distances
generateSplitDistances(light, nearDist, farDist) {

light._shadowCascadeDistances.fill(farDist);
for (let i = 1; i < light.numCascades; i++) {

// lerp between linear and logarithmic distance, called practical split distance
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;
}
}

addLightRenderPasses(frameGraph, light, camera) {

// shadow cascades have more faces rendered within a singe render pass
const faceCount = light.numShadowFaces;
const faceUpdateModes = light.faceUpdateModes;

// prepare render targets / cameras for rendering
let allCascadesRendering = true;
let shadowCamera;
for (let face = 0; face < faceCount; face++) {

if (faceUpdateModes?.[face] === SHADOWUPDATE_NONE)
allCascadesRendering = false;

shadowCamera = this.prepareFace(light, camera, face);
}

const renderPass = new RenderPass(this.device, () => {

// inside the render pass, render all faces
for (let face = 0; face < faceCount; face++) {
this.renderFace(light, camera, face, false);

if (faceUpdateModes?.[face] !== SHADOWUPDATE_NONE) {
this.renderFace(light, camera, face, !allCascadesRendering);
}

if (faceUpdateModes?.[face] === SHADOWUPDATE_THISFRAME) {
faceUpdateModes[face] = SHADOWUPDATE_NONE;
}
}

}, () => {
Expand All @@ -186,7 +221,7 @@ class ShadowRendererDirectional extends ShadowRenderer {
});

// setup render pass using any of the cameras, they all have the same pass related properties
this.setupRenderPass(renderPass, shadowCamera);
this.setupRenderPass(renderPass, shadowCamera, allCascadesRendering);
DebugHelper.setName(renderPass, `DirShadow-${light._node.name}`);

frameGraph.addRenderPass(renderPass);
Expand Down
39 changes: 13 additions & 26 deletions src/scene/renderer/shadow-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ 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 { math } from '../../core/math/math.js';
import { Vec3 } from '../../core/math/vec3.js';
import { Vec4 } from '../../core/math/vec4.js';

Expand Down Expand Up @@ -160,21 +159,6 @@ class ShadowRenderer {
visible.sort(this.renderer.sortCompareDepth);
}

// function to generate frustum split distances
generateSplitDistances(light, nearDist, farDist) {

light._shadowCascadeDistances.fill(farDist);
for (let i = 1; i < light.numCascades; i++) {

// lerp between linear and logarithmic distance, called practical split distance
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;
}
}

setupRenderState(device, light) {

const isClustered = this.renderer.scene.clusteredLightingEnabled;
Expand Down Expand Up @@ -340,20 +324,23 @@ class ShadowRenderer {
return light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, face);
}

setupRenderPass(renderPass, shadowCamera) {
setupRenderPass(renderPass, shadowCamera, clearRenderTarget) {

const rt = shadowCamera.renderTarget;
renderPass.init(rt);

// color
const clearColor = shadowCamera.clearColorBuffer;
renderPass.colorOps.clear = clearColor;
if (clearColor)
renderPass.colorOps.clearValue.copy(shadowCamera.clearColor);

// depth
renderPass.depthStencilOps.storeDepth = !clearColor;
renderPass.setClearDepth(1.0);
// only clear the render pass target if all faces (cascades) are getting rendered
if (clearRenderTarget) {
// color
const clearColor = shadowCamera.clearColorBuffer;
renderPass.colorOps.clear = clearColor;
if (clearColor)
renderPass.colorOps.clearValue.copy(shadowCamera.clearColor);

// depth
renderPass.depthStencilOps.storeDepth = !clearColor;
renderPass.setClearDepth(1.0);
}

// not sampling dynamically generated cubemaps
renderPass.requiresCubemaps = false;
Expand Down