diff --git a/e2e/case/shadow-basic.ts b/e2e/case/shadow-basic.ts new file mode 100644 index 0000000000..d801ba02ae --- /dev/null +++ b/e2e/case/shadow-basic.ts @@ -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("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); + }); +}); diff --git a/e2e/config.ts b/e2e/config.ts index 7cda28feca..0fe2fb5aab 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -89,5 +89,12 @@ export const E2E_CONFIG = { caseFileName: "material-unlit", threshold: 0.2 } + }, + Shadow: { + basic: { + category: "Shadow", + caseFileName: "shadow-basic", + threshold: 0.2 + } } }; diff --git a/e2e/fixtures/originImage/Shadow_shadow-basic.jpg b/e2e/fixtures/originImage/Shadow_shadow-basic.jpg new file mode 100644 index 0000000000..7f1f712c09 --- /dev/null +++ b/e2e/fixtures/originImage/Shadow_shadow-basic.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9cbb0915691b1eef33dad7a7dc3961d13af46a560645560a61c7903aba39b6a7 +size 113721 diff --git a/package.json b/package.json index fa0857d17c..cb59a3af61 100644 --- a/package.json +++ b/package.json @@ -68,5 +68,6 @@ "eslint --fix", "git add" ] - } -} + }, + "repository": "git@github.com:galacean/runtime.git" +} \ No newline at end of file diff --git a/packages/core/src/Scene.ts b/packages/core/src/Scene.ts index 1a4644c1d3..6474a43f18 100644 --- a/packages/core/src/Scene.ts +++ b/packages/core/src/Scene.ts @@ -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(); diff --git a/packages/core/src/shaderlib/mobile_blinnphong_frag.glsl b/packages/core/src/shaderlib/mobile_blinnphong_frag.glsl index f143caa54d..41876b7022 100644 --- a/packages/core/src/shaderlib/mobile_blinnphong_frag.glsl +++ b/packages/core/src/shaderlib/mobile_blinnphong_frag.glsl @@ -13,7 +13,6 @@ shadowAttenuation = 1.0; #ifdef SCENE_IS_CALCULATE_SHADOWS shadowAttenuation *= sampleShadowMap(); - // int sunIndex = int(scene_ShadowInfo.z); #endif DirectLight directionalLight; diff --git a/packages/core/src/shaderlib/pbr/direct_irradiance_frag_define.glsl b/packages/core/src/shaderlib/pbr/direct_irradiance_frag_define.glsl index 43b918bf8c..dfe3213443 100644 --- a/packages/core/src/shaderlib/pbr/direct_irradiance_frag_define.glsl +++ b/packages/core/src/shaderlib/pbr/direct_irradiance_frag_define.glsl @@ -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; diff --git a/packages/core/src/shaderlib/shadow/ShadowFragmentDeclaration.glsl b/packages/core/src/shaderlib/shadow/ShadowFragmentDeclaration.glsl index 29322c2772..1fa6e6e22a 100644 --- a/packages/core/src/shaderlib/shadow/ShadowFragmentDeclaration.glsl +++ b/packages/core/src/shaderlib/shadow/ShadowFragmentDeclaration.glsl @@ -9,8 +9,8 @@ #include #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; } diff --git a/packages/core/src/shadow/CascadedShadowCasterPass.ts b/packages/core/src/shadow/CascadedShadowCasterPass.ts index 4e1d2d5eb5..52e0bd0197 100644 --- a/packages/core/src/shadow/CascadedShadowCasterPass.ts +++ b/packages/core/src/shadow/CascadedShadowCasterPass.ts @@ -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 = renderTarget.depthTexture; - } else { - renderTarget = PipelineUtils.recreateRenderTargetIfNeeded( - engine, - this._renderTarget, + // 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 = renderTarget.depthTexture; + } else { + renderTarget = PipelineUtils.recreateRenderTargetIfNeeded( + engine, + this._renderTarget, + width, + height, + format, + null, + false + ); + shadowTexture = renderTarget.getColorTexture(0); + } + + shadowTexture.wrapModeU = shadowTexture.wrapModeV = TextureWrapMode.Clamp; + if (engine._hardwareRenderer._isWebGL2) { + shadowTexture.depthCompareFunction = TextureDepthCompareFunction.Less; + } + + 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); + } + + // 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; + + 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, + shadowTileResolution, + shadowSliceData, + shadowMatrices + ); + if (shadowCascades > 1) { + ShadowUtils.applySliceTransform( + shadowTileResolution, width, height, - format, - null, - false + j, + this._viewportOffsets[j], + shadowMatrices ); - shadowTexture = renderTarget.getColorTexture(0); } - - shadowTexture.wrapModeU = shadowTexture.wrapModeV = TextureWrapMode.Clamp; - if (engine._hardwareRenderer._isWebGL2) { - shadowTexture.depthCompareFunction = TextureDepthCompareFunction.Less; + 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); } - this._renderTarget = renderTarget; - this._depthTexture = shadowTexture; + if (opaqueQueue.elements.length || alphaTestQueue.elements.length) { + opaqueQueue.sort(RenderQueue._compareFromNearToFar); + alphaTestQueue.sort(RenderQueue._compareFromNearToFar); - // @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 + const { x, y } = viewports[j]; - // 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); + rhi.setGlobalDepthBias(1.0, 1.0); - 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 - ); + 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++; - ShadowUtils.getDirectionalLightMatrices( - lightUp, - lightSide, - lightForward, - j, - light.shadowNearPlane, - shadowTileResolution, - shadowSliceData, - shadowMatrices - ); - if (shadowCascades > 1) { - ShadowUtils.applySliceTransform( - shadowTileResolution, - width, - height, - j, - this._viewportOffsets[j], - shadowMatrices - ); - } - 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); - } + opaqueQueue.render(camera, Layer.Everything, PipelineStage.ShadowCaster); + alphaTestQueue.render(camera, Layer.Everything, PipelineStage.ShadowCaster); + rhi.setGlobalDepthBias(0, 0); } - this._existShadowMap = true; } } - 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 || diff --git a/packages/core/src/shadow/ShadowUtils.ts b/packages/core/src/shadow/ShadowUtils.ts index e2cbd87def..5c966b2091 100644 --- a/packages/core/src/shadow/ShadowUtils.ts +++ b/packages/core/src/shadow/ShadowUtils.ts @@ -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; + } }