Support shadow fade (#1960)

* feat: support shadow fade
This commit is contained in:
zhuxudong
2024-01-17 13:34:21 +08:00
committed by GitHub
parent 8541659337
commit 9d2e8ff369
10 changed files with 254 additions and 153 deletions

56
e2e/case/shadow-basic.ts Normal file
View File

@@ -0,0 +1,56 @@
/**
* @title Shadow basic
* @category Shadow
*/
import {
Animator,
Camera,
DirectLight,
GLTFResource,
MeshRenderer,
PBRMaterial,
PrimitiveMesh,
ShadowResolution,
ShadowType,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { initScreenshot, updateForE2E } from "./.mockForE2E";
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
scene.shadowResolution = ShadowResolution.Medium;
scene.shadowDistance = 5;
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.setPosition(0, 2, 3);
cameraEntity.transform.lookAt(new Vector3(0));
const camera = cameraEntity.addComponent(Camera);
const lightEntity = rootEntity.createChild("light_node");
const light = lightEntity.addComponent(DirectLight);
lightEntity.transform.setPosition(-10, 10, 10);
lightEntity.transform.lookAt(new Vector3(0, 0, 0));
light.shadowType = ShadowType.SoftHigh;
const planeEntity = rootEntity.createChild("plane_node");
const renderer = planeEntity.addComponent(MeshRenderer);
renderer.mesh = PrimitiveMesh.createPlane(engine, 10, 10);
const planeMaterial = new PBRMaterial(engine);
renderer.setMaterial(planeMaterial);
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((asset) => {
const { defaultSceneRoot } = asset;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
animator.play(asset.animations[0].name);
updateForE2E(engine, 500);
initScreenshot(engine, camera);
});
});

View File

@@ -89,5 +89,12 @@ export const E2E_CONFIG = {
caseFileName: "material-unlit",
threshold: 0.2
}
},
Shadow: {
basic: {
category: "Shadow",
caseFileName: "shadow-basic",
threshold: 0.2
}
}
};

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cbb0915691b1eef33dad7a7dc3961d13af46a560645560a61c7903aba39b6a7
size 113721

View File

@@ -68,5 +68,6 @@
"eslint --fix",
"git add"
]
}
}
},
"repository": "git@github.com:galacean/runtime.git"
}

View File

@@ -42,6 +42,11 @@ export class Scene extends EngineObject {
shadowFourCascadeSplits: Vector3 = new Vector3(1.0 / 15, 3.0 / 15.0, 7.0 / 15.0);
/** Max Shadow distance. */
shadowDistance: number = 50;
/**
* Last shadow fade distance in percentage, range [0,1].
* @remarks Value 0 is used for no shadow fade.
*/
shadowFadeBorder: number = 0.1;
/* @internal */
_lightManager: LightManager = new LightManager();

View File

@@ -13,7 +13,6 @@
shadowAttenuation = 1.0;
#ifdef SCENE_IS_CALCULATE_SHADOWS
shadowAttenuation *= sampleShadowMap();
// int sunIndex = int(scene_ShadowInfo.z);
#endif
DirectLight directionalLight;

View File

@@ -79,7 +79,6 @@ void addTotalDirectRadiance(Geometry geometry, Material material, inout Reflecte
shadowAttenuation = 1.0;
#ifdef SCENE_IS_CALCULATE_SHADOWS
shadowAttenuation *= sampleShadowMap();
// int sunIndex = int(scene_ShadowInfo.z);
#endif
DirectLight directionalLight;

View File

@@ -9,8 +9,8 @@
#include <ShadowCoord>
#endif
// intensity, resolution, sunIndex
uniform vec3 scene_ShadowInfo;
// intensity, null, fadeScale, fadeBias
uniform vec4 scene_ShadowInfo;
uniform vec4 scene_ShadowMapSize;
#ifdef GRAPHICS_API_WEBGL2
@@ -73,6 +73,13 @@
}
#endif
float getShadowFade(vec3 positionWS){
vec3 camToPixel = positionWS - camera_Position;
float distanceCamToPixel2 = dot(camToPixel, camToPixel);
return saturate( distanceCamToPixel2 * scene_ShadowInfo.z + scene_ShadowInfo.w );
}
float sampleShadowMap() {
#if SCENE_SHADOW_CASCADED_COUNT == 1
vec3 shadowCoord = v_shadowCoord;
@@ -93,7 +100,9 @@
#if SCENE_SHADOW_TYPE == 3
attenuation = sampleShadowMapFiltered9(scene_ShadowMap, shadowCoord, scene_ShadowMapSize);
#endif
attenuation = mix(1.0, attenuation, scene_ShadowInfo.x);
float shadowFade = getShadowFade(v_pos);
attenuation = mix(1.0, mix(attenuation, 1.0, shadowFade), scene_ShadowInfo.x);
}
return attenuation;
}

View File

@@ -1,10 +1,11 @@
import { Color, MathUtil, Matrix, Vector2, Vector3, Vector4 } from "@galacean/engine-math";
import { Camera } from "../Camera";
import { Layer } from "../Layer";
import { PipelineStage } from "../RenderPipeline/index";
import { PipelinePass } from "../RenderPipeline/PipelinePass";
import { PipelineUtils } from "../RenderPipeline/PipelineUtils";
import { RenderContext } from "../RenderPipeline/RenderContext";
import { RenderQueue } from "../RenderPipeline/RenderQueue";
import { PipelineStage } from "../RenderPipeline/index";
import { GLCapabilityType } from "../base/Constant";
import { CameraClearFlags } from "../enums/CameraClearFlags";
import { DirectLight } from "../lighting";
@@ -17,7 +18,6 @@ import { TextureWrapMode } from "../texture/enums/TextureWrapMode";
import { ShadowSliceData } from "./ShadowSliceData";
import { ShadowUtils } from "./ShadowUtils";
import { ShadowCascadesMode } from "./enum/ShadowCascadesMode";
import { PipelinePass } from "../RenderPipeline/PipelinePass";
/**
* Cascade shadow caster pass.
@@ -52,13 +52,12 @@ export class CascadedShadowCasterPass extends PipelinePass {
private _shadowSliceData: ShadowSliceData = new ShadowSliceData();
private _lightUp: Vector3 = new Vector3();
private _lightSide: Vector3 = new Vector3();
private _existShadowMap: boolean = false;
private _splitBoundSpheres = new Float32Array(CascadedShadowCasterPass._maxCascades * 4);
/** The end is project precision problem in shader. */
private _shadowMatrices = new Float32Array((CascadedShadowCasterPass._maxCascades + 1) * 16);
// strength, null, lightIndex
private _shadowInfos = new Vector3();
// intensity, null, fadeScale, fadeBias
private _shadowInfos = new Vector4();
private _depthTexture: Texture2D;
private _renderTarget: RenderTarget;
private _viewportOffsets: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()];
@@ -75,16 +74,13 @@ export class CascadedShadowCasterPass extends PipelinePass {
* @internal
*/
override onRender(context: RenderContext): void {
const light = this._camera.scene._lightManager._sunlight;
this._updateShadowSettings();
this._existShadowMap = false;
this._renderDirectShadowMap(context);
if (this._existShadowMap) {
this._updateReceiversShaderData();
}
this._renderDirectShadowMap(context, light);
this._updateReceiversShaderData(light);
}
private _renderDirectShadowMap(context: RenderContext): void {
private _renderDirectShadowMap(context: RenderContext, light: DirectLight): void {
const {
_engine: engine,
_camera: camera,
@@ -108,151 +104,147 @@ export class CascadedShadowCasterPass extends PipelinePass {
const lightSide = this._lightSide;
const lightForward = shadowSliceData.virtualCamera.forward;
const light = scene._lightManager._sunlight;
if (light) {
const shadowFar = Math.min(camera.scene.shadowDistance, camera.farClipPlane);
this._getCascadesSplitDistance(shadowFar);
// Prepare render target
const { z: width, w: height } = this._shadowMapSize;
const format = this._shadowMapFormat;
let renderTarget: RenderTarget;
let shadowTexture: Texture2D;
if (this._supportDepthTexture) {
renderTarget = PipelineUtils.recreateRenderTargetIfNeeded(
engine,
this._renderTarget,
width,
height,
null,
format,
false
);
shadowTexture = <Texture2D>renderTarget.depthTexture;
} else {
renderTarget = PipelineUtils.recreateRenderTargetIfNeeded(
engine,
this._renderTarget,
width,
height,
format,
null,
false
);
shadowTexture = <Texture2D>renderTarget.getColorTexture(0);
}
// Prepare render target
const { z: width, w: height } = this._shadowMapSize;
const format = this._shadowMapFormat;
let renderTarget: RenderTarget;
let shadowTexture: Texture2D;
if (this._supportDepthTexture) {
renderTarget = PipelineUtils.recreateRenderTargetIfNeeded(
engine,
this._renderTarget,
width,
height,
null,
format,
false
);
shadowTexture = <Texture2D>renderTarget.depthTexture;
} else {
renderTarget = PipelineUtils.recreateRenderTargetIfNeeded(
engine,
this._renderTarget,
width,
height,
format,
null,
false
);
shadowTexture = <Texture2D>renderTarget.getColorTexture(0);
}
shadowTexture.wrapModeU = shadowTexture.wrapModeV = TextureWrapMode.Clamp;
if (engine._hardwareRenderer._isWebGL2) {
shadowTexture.depthCompareFunction = TextureDepthCompareFunction.Less;
}
shadowTexture.wrapModeU = shadowTexture.wrapModeV = TextureWrapMode.Clamp;
if (engine._hardwareRenderer._isWebGL2) {
shadowTexture.depthCompareFunction = TextureDepthCompareFunction.Less;
}
this._renderTarget = renderTarget;
this._depthTexture = shadowTexture;
this._renderTarget = renderTarget;
this._depthTexture = shadowTexture;
// @todo: shouldn't set viewport and scissor in activeRenderTarget
rhi.activeRenderTarget(renderTarget, CascadedShadowCasterPass._viewport, 0);
if (this._supportDepthTexture) {
rhi.clearRenderTarget(engine, CameraClearFlags.Depth, null);
} else {
rhi.clearRenderTarget(engine, CameraClearFlags.All, CascadedShadowCasterPass._clearColor);
}
// @todo: shouldn't set viewport and scissor in activeRenderTarget
rhi.activeRenderTarget(renderTarget, CascadedShadowCasterPass._viewport, 0);
if (this._supportDepthTexture) {
rhi.clearRenderTarget(engine, CameraClearFlags.Depth, null);
} else {
rhi.clearRenderTarget(engine, CameraClearFlags.All, CascadedShadowCasterPass._clearColor);
}
this._shadowInfos.x = light.shadowStrength;
this._shadowInfos.z = 0; // @todo: sun light index always 0
// prepare light and camera direction
Matrix.rotationQuaternion(light.entity.transform.worldRotationQuaternion, lightWorld);
lightSide.set(lightWorldE[0], lightWorldE[1], lightWorldE[2]);
lightUp.set(lightWorldE[4], lightWorldE[5], lightWorldE[6]);
lightForward.set(-lightWorldE[8], -lightWorldE[9], -lightWorldE[10]);
const cameraForward = CascadedShadowCasterPass._tempVector;
cameraForward.copyFrom(camera.entity.transform.worldForward);
// prepare light and camera direction
Matrix.rotationQuaternion(light.entity.transform.worldRotationQuaternion, lightWorld);
lightSide.set(lightWorldE[0], lightWorldE[1], lightWorldE[2]);
lightUp.set(lightWorldE[4], lightWorldE[5], lightWorldE[6]);
lightForward.set(-lightWorldE[8], -lightWorldE[9], -lightWorldE[10]);
const cameraForward = CascadedShadowCasterPass._tempVector;
cameraForward.copyFrom(camera.entity.transform.worldForward);
const shadowTileResolution = this._shadowTileResolution;
const shadowTileResolution = this._shadowTileResolution;
for (let j = 0; j < shadowCascades; j++) {
ShadowUtils.getBoundSphereByFrustum(
splitDistance[j],
splitDistance[j + 1],
camera,
cameraForward,
shadowSliceData
);
ShadowUtils.getDirectionLightShadowCullPlanes(
camera._frustum,
splitDistance[j],
camera.nearClipPlane,
lightForward,
shadowSliceData
);
for (let j = 0; j < shadowCascades; j++) {
ShadowUtils.getBoundSphereByFrustum(
splitDistance[j],
splitDistance[j + 1],
camera,
cameraForward,
shadowSliceData
);
ShadowUtils.getDirectionLightShadowCullPlanes(
camera._frustum,
splitDistance[j],
camera.nearClipPlane,
lightForward,
shadowSliceData
);
ShadowUtils.getDirectionalLightMatrices(
lightUp,
lightSide,
lightForward,
j,
light.shadowNearPlane,
ShadowUtils.getDirectionalLightMatrices(
lightUp,
lightSide,
lightForward,
j,
light.shadowNearPlane,
shadowTileResolution,
shadowSliceData,
shadowMatrices
);
if (shadowCascades > 1) {
ShadowUtils.applySliceTransform(
shadowTileResolution,
shadowSliceData,
width,
height,
j,
this._viewportOffsets[j],
shadowMatrices
);
if (shadowCascades > 1) {
ShadowUtils.applySliceTransform(
shadowTileResolution,
width,
height,
j,
this._viewportOffsets[j],
shadowMatrices
);
}
this._updateSingleShadowCasterShaderData(<DirectLight>light, shadowSliceData, context);
// upload pre-cascade infos.
const center = boundSphere.center;
const radius = boundSphere.radius;
const offset = j * 4;
splitBoundSpheres[offset] = center.x;
splitBoundSpheres[offset + 1] = center.y;
splitBoundSpheres[offset + 2] = center.z;
splitBoundSpheres[offset + 3] = radius * radius;
opaqueQueue.clear();
alphaTestQueue.clear();
transparentQueue.clear();
const renderers = componentsManager._renderers;
const elements = renderers._elements;
for (let k = renderers.length - 1; k >= 0; --k) {
ShadowUtils.shadowCullFrustum(context, light, elements[k], shadowSliceData);
}
if (opaqueQueue.elements.length || alphaTestQueue.elements.length) {
opaqueQueue.sort(RenderQueue._compareFromNearToFar);
alphaTestQueue.sort(RenderQueue._compareFromNearToFar);
const { x, y } = viewports[j];
rhi.setGlobalDepthBias(1.0, 1.0);
rhi.viewport(x, y, shadowTileResolution, shadowTileResolution);
// for no cascade is for the edge,for cascade is for the beyond maxCascade pixel can use (0,0,0) trick sample the shadowMap
rhi.scissor(x + 1, y + 1, shadowTileResolution - 2, shadowTileResolution - 2);
engine._renderCount++;
opaqueQueue.render(camera, Layer.Everything, PipelineStage.ShadowCaster);
alphaTestQueue.render(camera, Layer.Everything, PipelineStage.ShadowCaster);
rhi.setGlobalDepthBias(0, 0);
}
}
this._existShadowMap = true;
this._updateSingleShadowCasterShaderData(light, shadowSliceData, context);
// upload pre-cascade infos.
const center = boundSphere.center;
const radius = boundSphere.radius;
const offset = j * 4;
splitBoundSpheres[offset] = center.x;
splitBoundSpheres[offset + 1] = center.y;
splitBoundSpheres[offset + 2] = center.z;
splitBoundSpheres[offset + 3] = radius * radius;
opaqueQueue.clear();
alphaTestQueue.clear();
transparentQueue.clear();
const renderers = componentsManager._renderers;
const elements = renderers._elements;
for (let k = renderers.length - 1; k >= 0; --k) {
ShadowUtils.shadowCullFrustum(context, light, elements[k], shadowSliceData);
}
if (opaqueQueue.elements.length || alphaTestQueue.elements.length) {
opaqueQueue.sort(RenderQueue._compareFromNearToFar);
alphaTestQueue.sort(RenderQueue._compareFromNearToFar);
const { x, y } = viewports[j];
rhi.setGlobalDepthBias(1.0, 1.0);
rhi.viewport(x, y, shadowTileResolution, shadowTileResolution);
// for no cascade is for the edge,for cascade is for the beyond maxCascade pixel can use (0,0,0) trick sample the shadowMap
rhi.scissor(x + 1, y + 1, shadowTileResolution - 2, shadowTileResolution - 2);
engine._renderCount++;
opaqueQueue.render(camera, Layer.Everything, PipelineStage.ShadowCaster);
alphaTestQueue.render(camera, Layer.Everything, PipelineStage.ShadowCaster);
rhi.setGlobalDepthBias(0, 0);
}
}
}
private _updateReceiversShaderData(): void {
const scene = this._camera.scene;
private _updateReceiversShaderData(light: DirectLight): void {
const camera = this._camera;
const scene = camera.scene;
const splitBoundSpheres = this._splitBoundSpheres;
const shadowMatrices = this._shadowMatrices;
const shadowCascades = scene.shadowCascades;
const shadowFar = Math.min(scene.shadowDistance, camera.farClipPlane);
ShadowUtils.getScaleAndBiasForLinearDistanceFade(Math.pow(shadowFar, 2), scene.shadowFadeBorder, this._shadowInfos);
this._shadowInfos.x = light.shadowStrength;
// set zero matrix to project the index out of max cascade
if (shadowCascades > 1) {
for (let i = shadowCascades * 4, n = splitBoundSpheres.length; i < n; i++) {
@@ -267,7 +259,7 @@ export class CascadedShadowCasterPass extends PipelinePass {
const shaderData = scene.shaderData;
shaderData.setFloatArray(CascadedShadowCasterPass._shadowMatricesProperty, this._shadowMatrices);
shaderData.setVector3(CascadedShadowCasterPass._shadowInfosProperty, this._shadowInfos);
shaderData.setVector4(CascadedShadowCasterPass._shadowInfosProperty, this._shadowInfos);
shaderData.setTexture(CascadedShadowCasterPass._shadowMapsProperty, this._depthTexture);
shaderData.setFloatArray(CascadedShadowCasterPass._shadowSplitSpheresProperty, this._splitBoundSpheres);
shaderData.setVector4(CascadedShadowCasterPass._shadowMapSize, this._shadowMapSize);
@@ -316,10 +308,14 @@ export class CascadedShadowCasterPass extends PipelinePass {
}
private _updateShadowSettings(): void {
const scene = this._camera.scene;
const camera = this._camera;
const scene = camera.scene;
const shadowFormat = ShadowUtils.shadowDepthFormat(scene.shadowResolution, this._supportDepthTexture);
const shadowResolution = ShadowUtils.shadowResolution(scene.shadowResolution);
const shadowCascades = scene.shadowCascades;
const shadowFar = Math.min(scene.shadowDistance, camera.farClipPlane);
this._getCascadesSplitDistance(shadowFar);
if (
shadowFormat !== this._shadowMapFormat ||

View File

@@ -7,17 +7,18 @@ import {
Matrix,
Plane,
Vector2,
Vector3
Vector3,
Vector4
} from "@galacean/engine-math";
import { Camera } from "../Camera";
import { DirectLight, Light } from "../lighting";
import { Renderer } from "../Renderer";
import { RenderContext } from "../RenderPipeline/RenderContext";
import { TextureFormat } from "../texture";
import { Renderer } from "../Renderer";
import { Utils } from "../Utils";
import { DirectLight, Light } from "../lighting";
import { TextureFormat } from "../texture";
import { ShadowSliceData } from "./ShadowSliceData";
import { ShadowResolution } from "./enum/ShadowResolution";
import { ShadowType } from "./enum/ShadowType";
import { ShadowSliceData } from "./ShadowSliceData";
/**
* @internal
@@ -439,4 +440,29 @@ export class ShadowUtils {
const offset = cascadeIndex * 16;
Utils._floatMatrixMultiply(sliceMatrix, outShadowMatrices, offset, outShadowMatrices, offset);
}
/**
* Extract scale and bias from a fade distance to achieve a linear fading of the fade distance.
*/
static getScaleAndBiasForLinearDistanceFade(fadeDistance: number, border: number, outInfo: Vector4): void {
// (P^2-N^2)/(F^2-N^2)
// To avoid division from zero
// This values ensure that fade within cascade will be 0 and outside 1
if (border < 0.0001) {
const multiplier = 1000; // To avoid blending if difference is in fractions
outInfo.z = multiplier;
outInfo.w = -fadeDistance * multiplier;
return;
}
border = 1 - border;
border *= border;
// Fade with distance calculation is just a linear fade from 90% of fade distance to fade distance. 90% arbitrarily chosen but should work well enough.
const distanceFadeNear = border * fadeDistance;
const fadeRange = fadeDistance - distanceFadeNear;
outInfo.z = 1.0 / fadeRange;
outInfo.w = -distanceFadeNear / fadeRange;
}
}