diff --git a/e2e/case/camera-ssao.ts b/e2e/case/camera-ssao.ts index 15a80d23c7..d0bc20dbb7 100644 --- a/e2e/case/camera-ssao.ts +++ b/e2e/case/camera-ssao.ts @@ -14,14 +14,15 @@ import { PBRMaterial, PrimitiveMesh, SkyBoxMaterial, - SSAOQuality, + AmbientOcclusionQuality, Vector3, - WebGLEngine + WebGLEngine, + WebGLMode } from "@galacean/engine"; import { initScreenshot, updateForE2E } from "./.mockForE2E"; Logger.enable(); -WebGLEngine.create({ canvas: "canvas" }).then((engine) => { +WebGLEngine.create({ canvas: "canvas",graphicDeviceOptions: { webGLMode: WebGLMode.WebGL1 } }).then((engine) => { engine.canvas.resizeByClientSize(2); const scene = engine.sceneManager.activeScene; const rootEntity = scene.createRootEntity(); @@ -32,13 +33,13 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { cameraEntity.transform.position = new Vector3(0.8, 1, 3.5); const camera = cameraEntity.addComponent(Camera); - scene.ssao.enabled = true; - scene.ssao.radius = 0.4; - scene.ssao.intensity = 3; - scene.ssao.power = 1.0; - scene.ssao.bias = 0.0005; - scene.ssao.bilateralThreshold = 0.01; - scene.ssao.quality = SSAOQuality.High; + scene.ambientOcclusion.enabled = true; + scene.ambientOcclusion.radius = 0.4; + scene.ambientOcclusion.intensity = 3; + scene.ambientOcclusion.power = 1.0; + scene.ambientOcclusion.bias = 0.0005; + scene.ambientOcclusion.bilateralThreshold = 0.01; + scene.ambientOcclusion.quality = AmbientOcclusionQuality.High; const lightNode = rootEntity.createChild("light_node"); lightNode.addComponent(DirectLight).color = new Color(1, 1, 1); @@ -82,6 +83,7 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { ambientLight.specularIntensity = 1; }) .then(() => { + // engine.run(); updateForE2E(engine); initScreenshot(engine, camera); }); diff --git a/e2e/config.ts b/e2e/config.ts index 02a0cb6ace..b03a4eedd4 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -263,7 +263,7 @@ export const E2E_CONFIG = { category: "Camera", caseFileName: "camera-ssao", threshold: 0, - diffPercentage: 0.08 + diffPercentage: 0.12 } }, Physics: { diff --git a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts index 07a4720a36..c2140b4055 100644 --- a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts +++ b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts @@ -6,6 +6,7 @@ import { BackgroundTextureFillMode } from "../enums/BackgroundTextureFillMode"; import { CameraClearFlags } from "../enums/CameraClearFlags"; import { DepthTextureMode } from "../enums/DepthTextureMode"; import { ReplacementFailureStrategy } from "../enums/ReplacementFailureStrategy"; +import { ScalableAmbientObscurancePass } from "../lighting/ambientOcclusion/ScalableAmbientObscurancePass"; import { FinalPass } from "../postProcess"; import { Shader } from "../shader/Shader"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; @@ -28,8 +29,6 @@ import { DepthOnlyPass } from "./DepthOnlyPass"; import { OpaqueTexturePass } from "./OpaqueTexturePass"; import { PipelineUtils } from "./PipelineUtils"; import { ContextRendererUpdateFlag, RenderContext } from "./RenderContext"; -import { SSAOPass } from "../lighting/screenSpaceLighting/ScreenSpaceAmbientOcclusionPass"; -import { ScreenSpaceAmbientOcclusion } from "../lighting/screenSpaceLighting"; import { RenderElement } from "./RenderElement"; import { SubRenderElement } from "./SubRenderElement"; import { PipelineStage } from "./enums/PipelineStage"; @@ -46,7 +45,7 @@ export class BasicRenderPipeline { private _internalColorTarget: RenderTarget = null; private _cascadedShadowCasterPass: CascadedShadowCasterPass; private _depthOnlyPass: DepthOnlyPass; - private _ssaoPass: SSAOPass; + private _saoPass: ScalableAmbientObscurancePass; private _opaqueTexturePass: OpaqueTexturePass; private _finalPass: FinalPass; private _copyBackgroundTexture: Texture2D; @@ -63,7 +62,7 @@ export class BasicRenderPipeline { this._cullingResults = new CullingResults(); this._cascadedShadowCasterPass = new CascadedShadowCasterPass(camera); this._depthOnlyPass = new DepthOnlyPass(engine); - this._ssaoPass = new SSAOPass(engine); + this._saoPass = new ScalableAmbientObscurancePass(engine); this._opaqueTexturePass = new OpaqueTexturePass(engine); this._finalPass = new FinalPass(engine); } @@ -93,7 +92,15 @@ export class BasicRenderPipeline { const cullingResults = this._cullingResults; const sunlight = scene._lightManager._sunlight; const depthOnlyPass = this._depthOnlyPass; - const depthPassEnabled = camera.depthTextureMode === DepthTextureMode.PrePass && depthOnlyPass._supportDepthTexture; + const ambientOcclusionEnabled = scene.ambientOcclusion._isValid(); + const supportDepthTexture = depthOnlyPass.supportDepthTexture; + + // Ambient occlusion enable will force enable depth prepass + if (ambientOcclusionEnabled) { + camera.depthTextureMode = DepthTextureMode.PrePass; + } + + const depthPassEnabled = camera.depthTextureMode === DepthTextureMode.PrePass && supportDepthTexture; const finalClearFlags = camera.clearFlags & ~(ignoreClear ?? CameraClearFlags.None); const msaaSamples = renderTarget ? renderTarget.antiAliasing : camera.msaaSamples; @@ -196,6 +203,16 @@ export class BasicRenderPipeline { } } + // Scalable ambient obscurance pass + // Before opaque pass so materials can sample ambient occlusion in BRDF + if (ambientOcclusionEnabled && supportDepthTexture) { + const saoPass = this._saoPass; + saoPass.onConfig(camera, this._depthOnlyPass.renderTarget); + saoPass.onRender(context); + } else { + this._saoPass.release(); + } + this._drawRenderPass(context, camera, finalClearFlags, cubeFace, mipLevel); } @@ -265,16 +282,6 @@ export class BasicRenderPipeline { context.setRenderTarget(colorTarget, colorViewport, mipLevel, cubeFace); } - // Screen space ambient occlusion pass - // Before opaque pass so materials can sample ambient occlusion in BRDF - if (scene.ssao.enabled) { - camera.depthTextureMode = DepthTextureMode.PrePass; - const ssaoPass = this._ssaoPass; - ssaoPass.onConfig(camera, colorTarget); - ssaoPass.onRender(context); - context.setRenderTarget(colorTarget, colorViewport, mipLevel, cubeFace); - } - const maskManager = scene._maskManager; if (finalClearFlags & CameraClearFlags.Stencil) { maskManager.hasStencilWritten = false; diff --git a/packages/core/src/RenderPipeline/DepthOnlyPass.ts b/packages/core/src/RenderPipeline/DepthOnlyPass.ts index 7318ee9fa0..8050ac9996 100644 --- a/packages/core/src/RenderPipeline/DepthOnlyPass.ts +++ b/packages/core/src/RenderPipeline/DepthOnlyPass.ts @@ -15,13 +15,12 @@ import { PipelineStage } from "./enums/PipelineStage"; * Depth only pass. */ export class DepthOnlyPass extends PipelinePass { - readonly _supportDepthTexture: boolean; - - private _renderTarget: RenderTarget; + readonly supportDepthTexture: boolean; + renderTarget: RenderTarget; constructor(engine: Engine) { super(engine); - this._supportDepthTexture = engine._hardwareRenderer.canIUse(GLCapabilityType.depthTexture); + this.supportDepthTexture = engine._hardwareRenderer.canIUse(GLCapabilityType.depthTexture); } onConfig(camera: Camera): void { @@ -30,7 +29,7 @@ export class DepthOnlyPass extends PipelinePass { const renderTarget = PipelineUtils.recreateRenderTargetIfNeeded( engine, - this._renderTarget, + this.renderTarget, width, height, null, @@ -43,12 +42,12 @@ export class DepthOnlyPass extends PipelinePass { TextureFilterMode.Point ); - this._renderTarget = renderTarget; + this.renderTarget = renderTarget; } override onRender(context: RenderContext, cullingResults: CullingResults): void { const engine = this.engine; - const renderTarget = this._renderTarget; + const renderTarget = this.renderTarget; const camera = context.camera; const rhi = engine._hardwareRenderer; context.setRenderTarget(renderTarget, PipelineUtils.defaultViewport, 0); @@ -58,6 +57,6 @@ export class DepthOnlyPass extends PipelinePass { cullingResults.opaqueQueue.render(context, PipelineStage.DepthOnly); cullingResults.alphaTestQueue.render(context, PipelineStage.DepthOnly); - camera.shaderData.setTexture(Camera._cameraDepthTextureProperty, this._renderTarget.depthTexture); + camera.shaderData.setTexture(Camera._cameraDepthTextureProperty, this.renderTarget.depthTexture); } } diff --git a/packages/core/src/RenderPipeline/PipelineUtils.ts b/packages/core/src/RenderPipeline/PipelineUtils.ts index e089c43290..50cc808eb3 100644 --- a/packages/core/src/RenderPipeline/PipelineUtils.ts +++ b/packages/core/src/RenderPipeline/PipelineUtils.ts @@ -86,7 +86,7 @@ export class PipelineUtils { ): RenderTarget { const currentColorTexture = currentRenderTarget?.getColorTexture(0); const colorTexture = - colorFormat != undefined + colorFormat != null ? PipelineUtils.recreateTextureIfNeeded( engine, currentColorTexture, diff --git a/packages/core/src/Scene.ts b/packages/core/src/Scene.ts index ed83d18839..2d2ef70c8a 100644 --- a/packages/core/src/Scene.ts +++ b/packages/core/src/Scene.ts @@ -8,7 +8,7 @@ import { SceneManager } from "./SceneManager"; import { EngineObject, Logger } from "./base"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; import { FogMode } from "./enums/FogMode"; -import { DirectLight, ScreenSpaceAmbientOcclusion } from "./lighting"; +import { AmbientOcclusion, DirectLight } from "./lighting"; import { AmbientLight } from "./lighting/AmbientLight"; import { LightManager } from "./lighting/LightManager"; import { PhysicsScene } from "./physics/PhysicsScene"; @@ -53,6 +53,14 @@ export class Scene extends EngineObject { /** Post process manager. */ readonly postProcessManager = new PostProcessManager(this); + /** + * Ambient Occlusion settings. + * @remarks + * Darkens areas where objects are close together to simulate natural light blocking, + * such as corners, crevices, and contact points between surfaces. + */ + readonly ambientOcclusion = new AmbientOcclusion(this); + /* @internal */ _lightManager: LightManager = new LightManager(); /* @internal */ @@ -79,7 +87,6 @@ export class Scene extends EngineObject { private _isActive: boolean = true; private _sun: DirectLight | null; private _enableTransparentShadow = false; - private _ssao = new ScreenSpaceAmbientOcclusion(); /** * Whether the scene is active. @@ -266,22 +273,6 @@ export class Scene extends EngineObject { } } - /** - * Screen Space Ambient Occlusion (SSAO) settings. - * @remarks - * Darkens areas where objects are close together to simulate natural light blocking, - * such as corners, crevices, and contact points between surfaces. - */ - get ssao(): ScreenSpaceAmbientOcclusion { - return this._ssao; - } - - set ssao(value: ScreenSpaceAmbientOcclusion) { - if (this._ssao !== value) { - this._ssao = value; - } - } - /** * Create scene. * @param engine - Engine diff --git a/packages/core/src/lighting/ambientOcclusion/AmbientOcclusion.ts b/packages/core/src/lighting/ambientOcclusion/AmbientOcclusion.ts new file mode 100644 index 0000000000..b2bf20e4a2 --- /dev/null +++ b/packages/core/src/lighting/ambientOcclusion/AmbientOcclusion.ts @@ -0,0 +1,129 @@ +import { Scene } from "../../Scene"; +import { ShaderMacro } from "../../shader/ShaderMacro"; +import { AmbientOcclusionQuality } from "../enums/AmbientOcclusionQuality"; + +/** + * Ambient Occlusion settings. + * @remarks + * Adds realistic shadows to corners, crevices, and areas where objects meet. + */ +export class AmbientOcclusion { + private static _enableMacro = ShaderMacro.getByName("SCENE_ENABLE_AMBIENT_OCCLUSION"); + + /** + * Controls the quality of the ambient occlusion effect. + * @remarks + * If set to `AmbientOcclusionQuality.Low`, the effect will use fewer samples for faster performance. + * If set to `AmbientOcclusionQuality.Medium`, the effect will balance quality and performance. + * If set to `AmbientOcclusionQuality.High`, the effect will use more samples for better quality, but the performance will be worse. + */ + quality = AmbientOcclusionQuality.Low; + + /** + * Controls the bias to prevent self-occlusion artifacts. + * Valid range: [0.0, 0.1] + * @defaultValue 0.01 + */ + bias = 0.01; + + private _scene: Scene; + private _enabled = false; + private _power = 1.0; + private _bilateralThreshold = 0.05; + private _radius = 0.5; + private _intensity = 1.0; + + /** + * Control whether ambient occlusion is enabled or not. + */ + get enabled(): boolean { + return this._enabled; + } + + set enabled(value: boolean) { + if (this._enabled !== value) { + this._enabled = value; + this._toggleAmbientOcclusionMacro(); + } + } + + /** + * Controls the radius of the ambient occlusion effect. + * Higher values create larger occlusion areas. + * Valid range: [0.0, 10.0] + * @defaultValue 0.5 + */ + get radius(): number { + return this._radius; + } + + set radius(value: number) { + if (this._radius !== value) { + this._radius = Math.max(0.0, Math.min(10.0, value)); + } + } + + /** + * Controls the strength of the ambient occlusion effect. + * Valid range: [0.0, Infinity) + * @defaultValue 1.0 + */ + get intensity(): number { + return this._intensity; + } + + set intensity(value: number) { + if (this._intensity !== value) { + this._intensity = Math.max(0.0, value); + this._toggleAmbientOcclusionMacro(); + } + } + + /** + * Control the contrast of the ambient occlusion effect. + * The larger the value, the grayer the effect. + * Valid range: [0.1, 5.0] + * @defaultValue 1.0 + */ + get power(): number { + return this._power; + } + + set power(value: number) { + this._power = Math.max(0.1, Math.min(5.0, value)); + } + + /** + * Control the threshold for blurred edges. + * Smaller value that retains the edge will result in sharper edges, + * while a larger value will make the edges softer. + * Valid range: (0.000001, 1.0] + * @defaultValue 0.05 + */ + get bilateralThreshold(): number { + return this._bilateralThreshold; + } + + set bilateralThreshold(value: number) { + this._bilateralThreshold = Math.max(1e-6, Math.min(1.0, value)); + } + + constructor(scene: Scene) { + this._scene = scene; + } + + /** + * @internal + */ + _isValid(): boolean { + return this._enabled && this.intensity > 0; + } + + private _toggleAmbientOcclusionMacro(): void { + if (this._isValid()) { + this._scene.shaderData.enableMacro(AmbientOcclusion._enableMacro); + } else { + this._scene.shaderData.disableMacro(AmbientOcclusion._enableMacro); + } + } +} diff --git a/packages/core/src/lighting/ambientOcclusion/ScalableAmbientObscurancePass.ts b/packages/core/src/lighting/ambientOcclusion/ScalableAmbientObscurancePass.ts new file mode 100644 index 0000000000..64009e52a8 --- /dev/null +++ b/packages/core/src/lighting/ambientOcclusion/ScalableAmbientObscurancePass.ts @@ -0,0 +1,214 @@ +import { Vector2, Vector4 } from "@galacean/engine-math"; +import { Camera } from "../../Camera"; +import { Engine } from "../../Engine"; +import { Material } from "../../material"; +import { Blitter } from "../../RenderPipeline/Blitter"; +import { PipelinePass } from "../../RenderPipeline/PipelinePass"; +import { PipelineUtils } from "../../RenderPipeline/PipelineUtils"; +import { RenderContext } from "../../RenderPipeline/RenderContext"; +import { Shader, ShaderData, ShaderPass, ShaderProperty } from "../../shader"; +import blitVs from "../../shaderlib/extra/Blit.vs.glsl"; +import { SystemInfo } from "../../SystemInfo"; +import { RenderTarget, Texture2D, TextureFilterMode, TextureFormat, TextureWrapMode } from "../../texture"; +import { AmbientOcclusionQuality } from "../enums/AmbientOcclusionQuality"; +import bilateralBlurFS from "./shaders/Blur/BilateralBlur.glsl"; +import scalableAmbientOcclusionFS from "./shaders/ScalableAmbientOcclusion.glsl"; + +/** + * @internal + * Scalable Ambient Obscurance render pass. + */ +export class ScalableAmbientObscurancePass extends PipelinePass { + static readonly SHADER_NAME = "ScalableAmbientOcclusion"; + + private static _invRadiusSquaredProp = ShaderProperty.getByName("material_invRadiusSquared"); + private static _intensityProp = ShaderProperty.getByName("material_intensity"); + private static _projectionScaleRadiusProp = ShaderProperty.getByName("material_projectionScaleRadius"); + private static _biasProp = ShaderProperty.getByName("material_bias"); + private static _minHorizonAngleSineSquaredProp = ShaderProperty.getByName("material_minHorizonAngleSineSquared"); + private static _peak2Prop = ShaderProperty.getByName("material_peak2"); + private static _powerProp = ShaderProperty.getByName("material_power"); + private static _invPositionProp = ShaderProperty.getByName("material_invProjScaleXY"); + + // Shader properties for bilateral blur + private static _farPlaneOverEdgeDistanceProp = ShaderProperty.getByName("material_farPlaneOverEdgeDistance"); + private static _kernelProp = ShaderProperty.getByName("material_kernel"); + + private readonly _material: Material; + + private _saoRenderTarget?: RenderTarget; + private _depthRenderTarget: RenderTarget; + private _blurRenderTarget: RenderTarget; + + private _sampleCount = 7; + private _position = new Vector2(); + private _offsetX = new Vector4(); + private _offsetY = new Vector4(); + private _quality: AmbientOcclusionQuality; + + constructor(engine: Engine) { + super(engine); + + const material = new Material(engine, Shader.find(ScalableAmbientObscurancePass.SHADER_NAME)); + material._addReferCount(1); + this._material = material; + } + + onConfig(camera: Camera, depthRenderTarget: RenderTarget): void { + const { engine } = this; + const { width, height } = camera.pixelViewport; + + this._depthRenderTarget = depthRenderTarget; + + const format = SystemInfo.supportsTextureFormat(engine, TextureFormat.R8) ? TextureFormat.R8 : TextureFormat.R8G8B8; + + this._saoRenderTarget = PipelineUtils.recreateRenderTargetIfNeeded( + engine, + this._saoRenderTarget, + width, + height, + format, + null, + false, + false, + false, + 1, + TextureWrapMode.Clamp, + TextureFilterMode.Bilinear + ); + this._blurRenderTarget = PipelineUtils.recreateRenderTargetIfNeeded( + engine, + this._blurRenderTarget, + width, + height, + format, + null, + false, + false, + false, + 1, + TextureWrapMode.Clamp, + TextureFilterMode.Bilinear + ); + } + + override onRender(context: RenderContext): void { + const { engine } = this; + const { camera } = context; + const { viewport, scene } = camera; + const { ambientOcclusion } = scene; + const { shaderData } = this._material; + const { projectionMatrix } = context; + + // For a typical projection matrix in column-major order: + // projection[0][0] is at index 0 (X scaling) + // projection[1][1] is at index 5 (Y scaling) + // The inverse values we need are: + const invProjection0 = 1.0 / projectionMatrix.elements[0]; + const invProjection1 = 1.0 / projectionMatrix.elements[5]; + + const position = this._position.set(invProjection0 * 2.0, invProjection1 * 2.0); + shaderData.setVector2(ScalableAmbientObscurancePass._invPositionProp, position); + + const { quality } = ambientOcclusion; + this._updateBlurKernel(shaderData, quality); + shaderData.enableMacro("SSAO_QUALITY", quality.toString()); + + const { radius, bias } = ambientOcclusion; + const peak = 0.1 * radius; + const peak2 = peak * peak; + const intensity = (2 * Math.PI * peak * ambientOcclusion.intensity) / this._sampleCount; + const power = ambientOcclusion.power * 2.0; + const projectionScaleRadius = radius * projectionMatrix.elements[5]; + const invRadiusSquared = 1.0 / (radius * radius); + const farPlaneOverEdgeDistance = -camera.farClipPlane / ambientOcclusion.bilateralThreshold; + + shaderData.setFloat(ScalableAmbientObscurancePass._invRadiusSquaredProp, invRadiusSquared); + shaderData.setFloat(ScalableAmbientObscurancePass._intensityProp, intensity); + shaderData.setFloat(ScalableAmbientObscurancePass._powerProp, power); + shaderData.setFloat(ScalableAmbientObscurancePass._projectionScaleRadiusProp, projectionScaleRadius); + shaderData.setFloat(ScalableAmbientObscurancePass._biasProp, bias); + shaderData.setFloat(ScalableAmbientObscurancePass._peak2Prop, peak2); + + shaderData.setFloat(ScalableAmbientObscurancePass._farPlaneOverEdgeDistanceProp, farPlaneOverEdgeDistance); + + const { _saoRenderTarget: saoTarget, _material: material } = this; + + // Draw ambient occlusion texture + const sourceTexture = this._depthRenderTarget.depthTexture; + Blitter.blitTexture(engine, sourceTexture, saoTarget, 0, viewport, material, 0); + + // Horizontal blur, saoRenderTarget -> blurRenderTarget + const aoTexture = saoTarget.getColorTexture(); + const offsetX = this._offsetX.set(1, 1, 1 / aoTexture.width, 0); + Blitter.blitTexture(engine, aoTexture, this._blurRenderTarget, 0, viewport, material, 1, offsetX); + + // Vertical blur, blurRenderTarget -> saoRenderTarget + const horizontalBlur = this._blurRenderTarget.getColorTexture(); + const offsetY = this._offsetY.set(1, 1, 0, 1 / aoTexture.height); + Blitter.blitTexture(engine, horizontalBlur, saoTarget, 0, viewport, material, 1, offsetY); + + // Set the SAO texture + camera.shaderData.setTexture(Camera._cameraSSAOTextureProperty, aoTexture); + } + + release(): void { + if (this._saoRenderTarget) { + this._saoRenderTarget.getColorTexture(0)?.destroy(true); + this._saoRenderTarget.destroy(true); + this._saoRenderTarget = null; + } + if (this._blurRenderTarget) { + this._blurRenderTarget.getColorTexture(0)?.destroy(true); + this._blurRenderTarget.destroy(true); + this._blurRenderTarget = null; + } + this._depthRenderTarget = null; + const material = this._material; + material._addReferCount(-1); + material.destroy(); + } + + private _updateBlurKernel(blurShaderData: ShaderData, quality: AmbientOcclusionQuality): void { + if (quality === this._quality) { + return; + } + + let sampleCount: number; + let standardDeviation: number; + + switch (quality) { + case AmbientOcclusionQuality.Low: + sampleCount = 7; + standardDeviation = 8.0; + break; + case AmbientOcclusionQuality.Medium: + sampleCount = 11; + standardDeviation = 8.0; + break; + case AmbientOcclusionQuality.High: + sampleCount = 16; + standardDeviation = 6.0; + break; + } + this._sampleCount = sampleCount; + + const kernelArraySize = 16; + const gaussianKernel = new Float32Array(kernelArraySize); + const variance = 2.0 * standardDeviation * standardDeviation; + for (let i = 0; i < sampleCount; i++) { + gaussianKernel[i] = Math.exp(-(i * i) / variance); + } + for (let i = sampleCount; i < kernelArraySize; i++) { + gaussianKernel[i] = 0.0; + } + + blurShaderData.setFloatArray(ScalableAmbientObscurancePass._kernelProp, gaussianKernel); + this._quality = quality; + } +} + +Shader.create(ScalableAmbientObscurancePass.SHADER_NAME, [ + new ShaderPass("ScalableAmbientOcclusion", blitVs, scalableAmbientOcclusionFS), + new ShaderPass("BilateralBlur", blitVs, bilateralBlurFS) +]); diff --git a/packages/core/src/lighting/ambientOcclusion/index.ts b/packages/core/src/lighting/ambientOcclusion/index.ts new file mode 100644 index 0000000000..cfb01397c2 --- /dev/null +++ b/packages/core/src/lighting/ambientOcclusion/index.ts @@ -0,0 +1,2 @@ +export { AmbientOcclusionQuality } from "../enums/AmbientOcclusionQuality"; +export { AmbientOcclusion } from "./AmbientOcclusion"; diff --git a/packages/core/src/lighting/screenSpaceLighting/shaders/Blur/BilateralBlur.glsl b/packages/core/src/lighting/ambientOcclusion/shaders/Blur/BilateralBlur.glsl similarity index 96% rename from packages/core/src/lighting/screenSpaceLighting/shaders/Blur/BilateralBlur.glsl rename to packages/core/src/lighting/ambientOcclusion/shaders/Blur/BilateralBlur.glsl index 125e549f56..0e78aa1903 100644 --- a/packages/core/src/lighting/screenSpaceLighting/shaders/Blur/BilateralBlur.glsl +++ b/packages/core/src/lighting/ambientOcclusion/shaders/Blur/BilateralBlur.glsl @@ -7,7 +7,7 @@ uniform vec4 renderer_SourceScaleOffset; uniform sampler2D camera_DepthTexture; -#ifdef SCENE_ENABLE_SSAO +#ifdef SCENE_ENABLE_AMBIENT_OCCLUSION uniform float material_farPlaneOverEdgeDistance; #if SSAO_QUALITY == 0 #define SAMPLE_COUNT 7 @@ -38,7 +38,7 @@ uniform sampler2D camera_DepthTexture; void main(){ mediump vec4 color = texture2D(renderer_BlitTexture, v_uv); - #ifdef SCENE_ENABLE_SSAO + #ifdef SCENE_ENABLE_AMBIENT_OCCLUSION float depth = texture2D(camera_DepthTexture, v_uv).r; // Weight of the center pixel from the Gaussian kernel (typically 1.0) float totalWeight = material_kernel[0]; diff --git a/packages/core/src/lighting/ambientOcclusion/shaders/ScalableAmbientOcclusion.glsl b/packages/core/src/lighting/ambientOcclusion/shaders/ScalableAmbientOcclusion.glsl new file mode 100644 index 0000000000..508d66a01d --- /dev/null +++ b/packages/core/src/lighting/ambientOcclusion/shaders/ScalableAmbientOcclusion.glsl @@ -0,0 +1,202 @@ +#include + +varying vec2 v_uv; +uniform vec4 renderer_texelSize; // x: 1/width, y: 1/height, z: width, w: height +uniform sampler2D renderer_BlitTexture; // Camera_DepthTexture + +#define PI 3.14159265359 +#if SSAO_QUALITY == 0 + #define SAMPLE_COUNT 7.0 + #define SPIRAL_TURNS 3.0 + // inc = (1.0f / (SAMPLE_COUNT - 0.5f)) * SPIRAL_TURNS * 2.0 * PI + // angleIncCosSin = vec2(cos(inc), sin(inc)) + const vec2 angleIncCosSin = vec2(-0.971148, 0.238227); +#elif SSAO_QUALITY == 1 + #define SAMPLE_COUNT 11.0 + #define SPIRAL_TURNS 6.0 + const vec2 angleIncCosSin = vec2(-0.896127, -0.443780); +#elif SSAO_QUALITY == 2 + #define SAMPLE_COUNT 16.0 + #define SPIRAL_TURNS 7.0 + const vec2 angleIncCosSin = vec2(-0.966846, 0.255311); +#endif + +uniform float material_invRadiusSquared; // Inverse of the squared radius +uniform float material_minHorizonAngleSineSquared; // Minimum horizon angle sine squared +uniform float material_intensity; // Intensity of the ambient occlusion +uniform float material_projectionScaleRadius; +uniform float material_bias; // Bias to avoid self-occlusion +uniform float material_peak2; // Peak value to avoid singularities +uniform float material_power; // Exponent to convert occlusion to visibility +uniform vec2 material_invProjScaleXY; //invProjection[0][0] * 2, invProjection[1][1] * 2 + +uniform vec4 camera_ProjectionParams; + +// maps orthographic depth buffer value (linear, [0, 1]) to view-space eye depth +float LinearDepthToViewDepth(float depth){ + return camera_ProjectionParams.y + (camera_ProjectionParams.z - camera_ProjectionParams.y) * depth; +} + +vec3 computeViewSpacePosition(vec2 uv, float linearDepth, vec2 invProjScaleXY) { + #ifdef CAMERA_ORTHOGRAPHIC + return vec3((vec2(0.5) - uv) * invProjScaleXY , linearDepth); + #else + return vec3((vec2(0.5) - uv) * invProjScaleXY * linearDepth, linearDepth); + #endif +} + +float SampleAndGetLinearViewDepth(float depth) { + #ifdef CAMERA_ORTHOGRAPHIC + return LinearDepthToViewDepth(depth); + #else + return remapDepthBufferLinear01(depth); + #endif +} + +vec3 computeViewSpaceNormal(vec2 uv, sampler2D depthTexture, float depth, vec2 invProjScaleXY, vec3 viewPos, vec2 sourceSize) { + vec3 normal = vec3(0.0); +#if SSAO_QUALITY == 0 || SSAO_QUALITY == 1 + vec2 uvdx = uv + vec2(sourceSize.x, 0.0); + vec2 uvdy = uv + vec2(0.0, sourceSize.y); + + float depthX = texture2D(depthTexture, uvdx).r; + float depthY = texture2D(depthTexture, uvdy).r; + + vec3 px = computeViewSpacePosition(uvdx, SampleAndGetLinearViewDepth(depthX), invProjScaleXY); + vec3 py = computeViewSpacePosition(uvdy, SampleAndGetLinearViewDepth(depthY), invProjScaleXY); + + vec3 dpdx = px - viewPos; + vec3 dpdy = py - viewPos; + + normal = normalize(cross(dpdx, dpdy)); + +#elif SSAO_QUALITY == 2 + vec2 dx = vec2(sourceSize.x, 0.0); + vec2 dy = vec2(0.0, sourceSize.y); + + vec4 H; + H.x = texture2D(depthTexture, uv - dx).r; // left + H.y = texture2D(depthTexture, uv + dx).r; // right + H.z = texture2D(depthTexture, uv - dx * 2.0).r; // left2 + H.w = texture2D(depthTexture, uv + dx * 2.0).r; // right2 + + // Calculate horizontal edge weights + vec2 horizontalEdgeWeights = abs((2.0 * H.xy - H.zw) - depth); + + vec3 pos_l = computeViewSpacePosition(uv - dx, SampleAndGetLinearViewDepth(H.x), invProjScaleXY); + vec3 pos_r = computeViewSpacePosition(uv + dx, SampleAndGetLinearViewDepth(H.y), invProjScaleXY); + vec3 dpdx = (horizontalEdgeWeights.x < horizontalEdgeWeights.y) ? (viewPos - pos_l) : (pos_r - viewPos); + + // Sample depths for vertical edge detection + vec4 V; + V.x = texture2D(depthTexture, uv - dy).r; // down + V.y = texture2D(depthTexture, uv + dy).r; // up + V.z = texture2D(depthTexture, uv - dy * 2.0).r; // down2 + V.w = texture2D(depthTexture, uv + dy * 2.0).r; // up2 + + // Calculate vertical edge weights + vec2 verticalEdgeWeights = abs((2.0 * V.xy - V.zw) - depth); + vec3 pos_d = computeViewSpacePosition(uv - dy, SampleAndGetLinearViewDepth(V.x), invProjScaleXY); + vec3 pos_u = computeViewSpacePosition(uv + dy, SampleAndGetLinearViewDepth(V.y), invProjScaleXY); + vec3 dpdy = (verticalEdgeWeights.x < verticalEdgeWeights.y) ? (viewPos - pos_d) : (pos_u - viewPos); + normal = normalize(cross(dpdx, dpdy)); + #endif + return normal; + +} + +vec3 tapLocation(float i, const float noise) { + float offset = ((2.0 * PI) * 2.4) * noise; + float angle = ((i / SAMPLE_COUNT) * SPIRAL_TURNS) * (2.0 * PI) + offset; + float radius = (i + noise + 0.5) / SAMPLE_COUNT; + return vec3(cos(angle), sin(angle), radius * radius); +} + +vec2 startPosition(const float noise) { + float angle = ((2.0 * PI) * 2.4) * noise; + return vec2(cos(angle), sin(angle)); +} + +mat2 tapAngleStep() { + vec2 t = angleIncCosSin; + return mat2(t.x, t.y, -t.y, t.x); +} + +vec3 tapLocationFast(float i, vec2 p, const float noise) { + float radius = (i + noise + 0.5) / SAMPLE_COUNT; + return vec3(p, radius * radius); +} + +void computeAmbientOcclusionSAO(inout float occlusion, float i, float ssDiskRadius, vec2 uv, vec3 originPosition, vec3 normal, + vec2 tapPosition, float noise, vec2 texSize) { + + vec3 tap = tapLocationFast(i, tapPosition, noise); + + float ssRadius = max(1.0, tap.z * ssDiskRadius); // at least 1 pixel screen-space radius + + vec2 uvSamplePos = uv + vec2(ssRadius * tap.xy) * texSize; + + float occlusionDepth = texture2D(renderer_BlitTexture, uvSamplePos).r; + float linearOcclusionDepth = SampleAndGetLinearViewDepth(occlusionDepth); + // “p” is the position after spiral sampling + vec3 p = computeViewSpacePosition(uvSamplePos, linearOcclusionDepth, material_invProjScaleXY); + + // now we have the sample, compute AO + vec3 v = p - originPosition; // sample vector + float vv = dot(v, v); // squared distance + float vn = dot(v, normal); // distance * cos(v, normal) + + // discard samples that are outside of the radius, preventing distant geometry to + // cast shadows -- there are many functions that work and choosing one is an artistic + // decision. + float weight = pow(max(0.0, 1.0 - vv * material_invRadiusSquared), 2.0); + + // discard samples that are too close to the horizon to reduce shadows cast by geometry + // not sufficently tessellated. The goal is to discard samples that form an angle 'beta' + // smaller than 'epsilon' with the horizon. We already have dot(v,n) which is equal to the + // sin(beta) * |v|. So the test simplifies to vn^2 < vv * sin(epsilon)^2. + weight *= step(vv * material_minHorizonAngleSineSquared, vn * vn); + + //Calculate the contribution of a single sampling point to Ambient Occlusion + float sampleOcclusion = max(0.0, vn + (originPosition.z * material_bias)) / (vv + material_peak2); + occlusion += weight * sampleOcclusion; +} + +void scalableAmbientObscurance(out float obscurance, vec2 fragCoord, vec2 uv, vec3 origin, vec3 normal, vec2 texSize) { + float noise = interleavedGradientNoise(fragCoord); + vec2 tapPosition = startPosition(noise); + mat2 angleStep = tapAngleStep(); + + // Choose the screen-space sample radius + // proportional to the projected area of the sphere + float ssDiskRadius = -(material_projectionScaleRadius / origin.z); + + // accumulate the occlusion amount of all sampling points + obscurance = 0.0; + for (float i = 0.0; i < SAMPLE_COUNT; i += 1.0) { + computeAmbientOcclusionSAO(obscurance, i, ssDiskRadius, uv, origin, normal, tapPosition, noise, texSize); + tapPosition = angleStep * tapPosition; + } + obscurance = sqrt(obscurance * material_intensity); +} + + +void main(){ + float aoVisibility = 0.0; + float depth = texture2D(renderer_BlitTexture, v_uv).r; + float linearDepth = SampleAndGetLinearViewDepth(depth); + + // Reconstruct view space position from depth + vec3 viewPos = computeViewSpacePosition(v_uv, linearDepth, material_invProjScaleXY); + + // Compute normal + vec3 normal = computeViewSpaceNormal(v_uv, renderer_BlitTexture, depth, material_invProjScaleXY, viewPos, renderer_texelSize.xy); + + float occlusion = 0.0; + scalableAmbientObscurance(occlusion, gl_FragCoord.xy, v_uv, viewPos, normal, renderer_texelSize.xy); + + // occlusion to visibility + aoVisibility = pow(clamp(1.0 - occlusion, 0.0, 1.0), material_power); + gl_FragColor = vec4(aoVisibility, aoVisibility, aoVisibility, 1.0); +} + diff --git a/packages/core/src/lighting/enums/AmbientOcclusionQuality.ts b/packages/core/src/lighting/enums/AmbientOcclusionQuality.ts new file mode 100644 index 0000000000..ae2a6718da --- /dev/null +++ b/packages/core/src/lighting/enums/AmbientOcclusionQuality.ts @@ -0,0 +1,11 @@ +/** + * Ambient occlusion quality levels that control the balance between visual quality and performance. + */ +export enum AmbientOcclusionQuality { + /** Low quality - fewer samples, better performance. */ + Low, + /** Medium quality - balanced samples and performance. */ + Medium, + /** High quality - more samples, slower performance. */ + High +} diff --git a/packages/core/src/lighting/index.ts b/packages/core/src/lighting/index.ts index 96a02114b5..01fbcf2961 100644 --- a/packages/core/src/lighting/index.ts +++ b/packages/core/src/lighting/index.ts @@ -4,4 +4,4 @@ export { DiffuseMode } from "./enums/DiffuseMode"; export { Light } from "./Light"; export { PointLight } from "./PointLight"; export { SpotLight } from "./SpotLight"; -export { ScreenSpaceAmbientOcclusion, SSAOQuality } from "./screenSpaceLighting"; +export { AmbientOcclusion, AmbientOcclusionQuality } from "./ambientOcclusion"; diff --git a/packages/core/src/lighting/screenSpaceLighting/ScreenSpaceAmbientOcclusionEffect.ts b/packages/core/src/lighting/screenSpaceLighting/ScreenSpaceAmbientOcclusionEffect.ts deleted file mode 100644 index 55cc01ab80..0000000000 --- a/packages/core/src/lighting/screenSpaceLighting/ScreenSpaceAmbientOcclusionEffect.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Shader, ShaderMacro, ShaderPass, ShaderProperty } from "../../shader"; -import blitVs from "../../shaderlib/extra/Blit.vs.glsl"; -import scalableAmbientOcclusionFS from "./shaders/ScalableAmbientOcclusion.glsl"; -import bilateralBlurFS from "./shaders/Blur/BilateralBlur.glsl"; - -/** - * Screen Space Ambient Occlusion quality levels. - */ -export enum SSAOQuality { - /** Low quality - fewer samples, faster performance */ - Low = 0, - /** Medium quality - balanced samples and performance */ - Medium = 1, - /** High quality - more samples, better quality */ - High = 2 -} - -/** - * Screen Space Ambient Occlusion effect configuration. - */ -export class ScreenSpaceAmbientOcclusion { - static readonly SHADER_NAME = "ScalableAmbientOcclusion"; - - // Shader properties for ambient occlusion calculation - /** @internal */ - static _invRadiusSquaredProp = ShaderProperty.getByName("material_invRadiusSquared"); - /** @internal */ - static _intensityProp = ShaderProperty.getByName("material_intensity"); - /** @internal */ - static _projectionScaleRadiusProp = ShaderProperty.getByName("material_projectionScaleRadius"); - /** @internal */ - static _biasProp = ShaderProperty.getByName("material_bias"); - /** @internal */ - static _minHorizonAngleSineSquaredProp = ShaderProperty.getByName("material_minHorizonAngleSineSquared"); - /** @internal */ - static _peak2Prop = ShaderProperty.getByName("material_peak2"); - /** @internal */ - static _powerProp = ShaderProperty.getByName("material_power"); - /** @internal */ - static _invPositionProp = ShaderProperty.getByName("material_invProjScaleXY"); - - // Shader properties for bilateral blur - /** @internal */ - static _farPlaneOverEdgeDistanceProp = ShaderProperty.getByName("material_farPlaneOverEdgeDistance"); - /** @internal */ - static _kernelProp = ShaderProperty.getByName("material_kernel"); - - // Shader macros - /** @internal */ - static _enableMacro = ShaderMacro.getByName("SCENE_ENABLE_SSAO"); - - private _enabled: boolean = false; - private _quality: SSAOQuality = SSAOQuality.Low; - private _radius: number = 0.5; - private _intensity: number = 1.0; - private _bias: number = 0.01; - private _power: number = 1.0; - private _bilateralThreshold: number = 0.05; - - /** - * Control whether screen space ambient occlusion is enabled or not. - */ - get enabled(): boolean { - return this._enabled; - } - - set enabled(value: boolean) { - if (value === this._enabled) { - return; - } - this._enabled = value; - } - - /** - * Controls the quality of the Screen Space Ambient Occlusion. - * @remarks - * If set to `SSAOQuality.Low`, the effect will use fewer samples for faster performance. - * If set to `SSAOQuality.Medium`, the effect will balance quality and performance. - * If set to `SSAOQuality.High`, the effect will use more samples for better quality,but the performance will be even worse. - */ - get quality(): SSAOQuality { - return this._quality; - } - - set quality(value: SSAOQuality) { - if (this._quality !== value) { - this._quality = value; - } - } - - /** - * Controls the radius of the Screen Space Ambient Occlusion radius. - * Higher values create larger occlusion areas. - * @default 0.5 - * @range [0.0, 10.0] - */ - get radius(): number { - return this._radius; - } - - set radius(value: number) { - if (this._radius !== value) { - this._radius = Math.max(0.0, Math.min(10.0, value)); - } - } - - /** - * Controls the strength of the Screen Space Ambient Occlusion effect. - * @default 1.0 - * @range [0.0, ∞) - */ - get intensity(): number { - return this._intensity; - } - - set intensity(value: number) { - if (this._intensity !== value) { - this._intensity = Math.max(0.0, value); - } - } - - /** - * Controls the bias to prevent self-occlusion artifacts. - * @default 0.01 - * @range [0.0, 0.1] - */ - get bias(): number { - return this._bias; - } - - set bias(value: number) { - if (this._bias !== value) { - this._bias = value; - } - } - - /** - * Control the contrast of the Screen Space Ambient Occlusion, - * The larger the value, the grayer the effect. - * @default 1.0 - * @range [0.1, 5.0] - */ - get power(): number { - return this._power; - } - - set power(value: number) { - this._power = Math.max(0.1, Math.min(5.0, value)); - } - - /** - * Control the threshold for blurred edges. - * @remarks - * Smaller value that retains the edge will result in sharper edges, - * while a larger value will make the edges softer. - * @default 0.05 - * @range (0.000001, 1.0] - */ - get bilateralThreshold(): number { - return this._bilateralThreshold; - } - - set bilateralThreshold(value: number) { - this._bilateralThreshold = Math.max(1e-6, Math.min(1.0, value)); - } - - /** - * @internal - */ - isValid(): boolean { - return this.enabled && this.intensity > 0; - } -} - -Shader.create(ScreenSpaceAmbientOcclusion.SHADER_NAME, [ - new ShaderPass("ScalableAmbientOcclusion", blitVs, scalableAmbientOcclusionFS), - new ShaderPass("BilateralBlur", blitVs, bilateralBlurFS) -]); diff --git a/packages/core/src/lighting/screenSpaceLighting/ScreenSpaceAmbientOcclusionPass.ts b/packages/core/src/lighting/screenSpaceLighting/ScreenSpaceAmbientOcclusionPass.ts deleted file mode 100644 index c578367499..0000000000 --- a/packages/core/src/lighting/screenSpaceLighting/ScreenSpaceAmbientOcclusionPass.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { Camera } from "../../Camera"; -import { Engine } from "../../Engine"; -import { Material } from "../../material"; -import { Shader, ShaderData } from "../../shader"; -import { RenderTarget, Texture2D, TextureFilterMode, TextureFormat, TextureWrapMode } from "../../texture"; -import { Blitter } from "../../RenderPipeline/Blitter"; -import { PipelinePass } from "../../RenderPipeline/PipelinePass"; -import { PipelineUtils } from "../../RenderPipeline/PipelineUtils"; -import { RenderContext } from "../../RenderPipeline/RenderContext"; -import { ScreenSpaceAmbientOcclusion, SSAOQuality } from "./ScreenSpaceAmbientOcclusionEffect"; -import { Vector2, Vector4 } from "@galacean/engine-math"; -import { SystemInfo } from "../../SystemInfo"; - -/** - * @internal - * Screen Space Ambient Occlusion render pass. - */ -export class SSAOPass extends PipelinePass { - private readonly _ssaoMaterial: Material; - private readonly _bilateralBlurMaterial: Material; - - private _ssaoRenderTarget?: RenderTarget; - private _inputRenderTarget: RenderTarget; - private _blurRenderTarget: RenderTarget; - - private _sampleCount: number = 7; - private _position = new Vector2(); - private _offsetX = new Vector4(); - private _offsetY = new Vector4(); - - private _quality: SSAOQuality = SSAOQuality.Low; - private _kernel: Float32Array = null; - - constructor(engine: Engine) { - super(engine); - - // Create SSAO material - const ssaoMaterial = new Material(engine, Shader.find(ScreenSpaceAmbientOcclusion.SHADER_NAME)); - ssaoMaterial._addReferCount(1); - this._ssaoMaterial = ssaoMaterial; - - //Bilateral Blur material - const bilateralBlurMaterial = new Material(engine, Shader.find(ScreenSpaceAmbientOcclusion.SHADER_NAME)); - bilateralBlurMaterial._addReferCount(1); - this._bilateralBlurMaterial = bilateralBlurMaterial; - - // ShaderData initialization - const ssaoShaderData = this._ssaoMaterial.shaderData; - - const radius = 0.5; - const defaultPower = 1.0; - const peak = 0.1 * radius; - const intensity = (2 * Math.PI * peak) / this._sampleCount; - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._invRadiusSquaredProp, 1.0 / (radius * radius)); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._intensityProp, intensity); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._powerProp, defaultPower * 2.0); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._peak2Prop, peak * peak); - } - - private _setQuality(blurShaderData: ShaderData, quality: SSAOQuality): void { - if (quality === this._quality && this._kernel !== null) { - return; - } - - let sampleCount: number; - let standardDeviation: number; - - switch (quality) { - case SSAOQuality.Low: - sampleCount = 7; - standardDeviation = 8.0; - break; - case SSAOQuality.Medium: - sampleCount = 11; - standardDeviation = 8.0; - break; - case SSAOQuality.High: - default: - sampleCount = 16; - standardDeviation = 6.0; - break; - } - this._sampleCount = sampleCount; - - const kernelArraySize = 16; - const gaussianKernel = new Float32Array(kernelArraySize); - for (let i = 0; i < sampleCount; i++) { - const w = Math.exp(-(i * i) / (2.0 * standardDeviation * standardDeviation)); - gaussianKernel[i] = w; - } - for (let i = sampleCount; i < kernelArraySize; i++) gaussianKernel[i] = 0.0; - this._quality = quality; - this._kernel = gaussianKernel; - blurShaderData.setFloatArray(ScreenSpaceAmbientOcclusion._kernelProp, gaussianKernel); - } - - onConfig(camera: Camera, inputRenderTarget: RenderTarget): void { - const { pixelViewport } = camera; - const engine = this.engine; - this._inputRenderTarget = inputRenderTarget; - - const textureFormat = SystemInfo.supportsTextureFormat(engine, TextureFormat.R8) - ? TextureFormat.R8 - : TextureFormat.R8G8B8; - - this._ssaoRenderTarget = PipelineUtils.recreateRenderTargetIfNeeded( - this.engine, - this._ssaoRenderTarget, - pixelViewport.width, - pixelViewport.height, - textureFormat, - null, - false, - false, - false, - 1, - TextureWrapMode.Clamp, - TextureFilterMode.Bilinear - ); - this._blurRenderTarget = PipelineUtils.recreateRenderTargetIfNeeded( - engine, - this._blurRenderTarget, - pixelViewport.width, - pixelViewport.height, - textureFormat, - null, - false, - false, - false, - 1, - TextureWrapMode.Clamp, - TextureFilterMode.Bilinear - ); - } - - override onRender(context: RenderContext): void { - const { engine } = this; - const { camera } = context; - const { viewport } = camera; - const scene = camera.scene; - const ssaoEffect = scene.ssao; - const ssaoShaderData = this._ssaoMaterial.shaderData; - const blurShaderData = this._bilateralBlurMaterial.shaderData; - const projectionMatrix = context.projectionMatrix; - - // For a typical projection matrix in column-major order: - // projection[0][0] is at index 0 (X scaling) - // projection[1][1] is at index 5 (Y scaling) - // The inverse values we need are: - // invProjection[0][0] = 1 / projection[0][0] - // invProjection[1][1] = 1 / projection[1][1] - const invProjection0 = 1.0 / projectionMatrix.elements[0]; // 1 / projection[0][0] - const invProjection1 = 1.0 / projectionMatrix.elements[5]; // 1 / projection[1][1] - - const position = this._position.set(invProjection0 * 2.0, invProjection1 * 2.0); - ssaoShaderData.setVector2(ScreenSpaceAmbientOcclusion._invPositionProp, position); - - if (ssaoEffect?.isValid()) { - this._setQuality(blurShaderData, ssaoEffect.quality); - const qualityValue = ssaoEffect.quality.toString(); - scene.shaderData.enableMacro("SCENE_ENABLE_SSAO"); - ssaoShaderData.enableMacro("SSAO_QUALITY", qualityValue); - blurShaderData.enableMacro("SSAO_QUALITY", qualityValue); - - const radius = ssaoEffect.radius; - const peak = 0.1 * radius; - const intensity = (2 * Math.PI * peak * ssaoEffect.intensity) / this._sampleCount; - const bias = ssaoEffect.bias; - const power = ssaoEffect.power * 2.0; - const projectionScaleRadius = radius * projectionMatrix.elements[5]; - const peak2 = peak * peak; - const invRadiusSquared = 1.0 / (radius * radius); - const farPlaneOverEdgeDistance = -camera.farClipPlane / ssaoEffect.bilateralThreshold; - - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._invRadiusSquaredProp, invRadiusSquared); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._intensityProp, intensity); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._powerProp, power); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._projectionScaleRadiusProp, projectionScaleRadius); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._biasProp, bias); - ssaoShaderData.setFloat(ScreenSpaceAmbientOcclusion._peak2Prop, peak2); - ssaoShaderData.enableMacro(ScreenSpaceAmbientOcclusion._enableMacro); - - blurShaderData.enableMacro(ScreenSpaceAmbientOcclusion._enableMacro); - blurShaderData.setFloat(ScreenSpaceAmbientOcclusion._farPlaneOverEdgeDistanceProp, farPlaneOverEdgeDistance); - } else { - scene.shaderData.disableMacro("SCENE_ENABLE_SSAO"); - ssaoShaderData.disableMacro(ScreenSpaceAmbientOcclusion._enableMacro); - blurShaderData.disableMacro(ScreenSpaceAmbientOcclusion._enableMacro); - return; - } - - const blurTarget = this._blurRenderTarget; - const ssaoTarget = this._ssaoRenderTarget; - - // draw ambient occlusion texture - const sourceTexture = this._inputRenderTarget.getColorTexture(); - Blitter.blitTexture(engine, sourceTexture, ssaoTarget, 0, viewport, this._ssaoMaterial, 0); - - // Separable bilateral blur pass - const aoTexture = this._ssaoRenderTarget.getColorTexture(); - // Horizontal blur: ssaoRenderTarget -> blurRenderTarget - const offsetX = this._offsetX.set(1, 1, 1 / aoTexture.width, 0); - const offsetY = this._offsetY.set(1, 1, 0, 1 / aoTexture.height); - Blitter.blitTexture(engine, aoTexture, blurTarget, 0, viewport, this._bilateralBlurMaterial, 1, offsetX); - // Vertical blur: blurRenderTarget -> ssaoRenderTarget - const horizontalBlur = this._blurRenderTarget.getColorTexture(); - Blitter.blitTexture(engine, horizontalBlur, ssaoTarget, 0, viewport, this._bilateralBlurMaterial, 1, offsetY); - - // Set the SSAO texture - camera.shaderData.setTexture(Camera._cameraSSAOTextureProperty, aoTexture); - } - - release(): void { - if (this._ssaoRenderTarget) { - this._ssaoRenderTarget.getColorTexture(0)?.destroy(true); - this._ssaoRenderTarget.destroy(true); - this._ssaoRenderTarget = null; - } - if (this._blurRenderTarget) { - this._blurRenderTarget.getColorTexture(0)?.destroy(true); - this._blurRenderTarget.destroy(true); - this._blurRenderTarget = null; - } - this._kernel = null; - this._inputRenderTarget = null; - this._ssaoMaterial._addReferCount(-1); - this._ssaoMaterial.destroy(); - this._bilateralBlurMaterial._addReferCount(-1); - this._bilateralBlurMaterial.destroy(); - } -} diff --git a/packages/core/src/lighting/screenSpaceLighting/index.ts b/packages/core/src/lighting/screenSpaceLighting/index.ts deleted file mode 100644 index e68d3a9d13..0000000000 --- a/packages/core/src/lighting/screenSpaceLighting/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ScreenSpaceAmbientOcclusion, SSAOQuality } from "./ScreenSpaceAmbientOcclusionEffect"; diff --git a/packages/core/src/lighting/screenSpaceLighting/shaders/ScalableAmbientOcclusion.glsl b/packages/core/src/lighting/screenSpaceLighting/shaders/ScalableAmbientOcclusion.glsl deleted file mode 100644 index 67df388612..0000000000 --- a/packages/core/src/lighting/screenSpaceLighting/shaders/ScalableAmbientOcclusion.glsl +++ /dev/null @@ -1,206 +0,0 @@ -#include - -varying vec2 v_uv; -uniform vec4 renderer_texelSize; // x: 1/width, y: 1/height, z: width, w: height -uniform sampler2D camera_DepthTexture; - -#ifdef SCENE_ENABLE_SSAO - #define PI 3.14159265359 - #if SSAO_QUALITY == 0 - #define SAMPLE_COUNT 7.0 - #define SPIRAL_TURNS 3.0 - // inc = (1.0f / (SAMPLE_COUNT - 0.5f)) * SPIRAL_TURNS * 2.0 * PI - // angleIncCosSin = vec2(cos(inc), sin(inc)) - const vec2 angleIncCosSin = vec2(-0.971148, 0.238227); - #elif SSAO_QUALITY == 1 - #define SAMPLE_COUNT 11.0 - #define SPIRAL_TURNS 6.0 - const vec2 angleIncCosSin = vec2(-0.896127, -0.443780); - #elif SSAO_QUALITY == 2 - #define SAMPLE_COUNT 16.0 - #define SPIRAL_TURNS 7.0 - const vec2 angleIncCosSin = vec2(-0.966846, 0.255311); - #endif - - uniform float material_invRadiusSquared; // Inverse of the squared radius - uniform float material_minHorizonAngleSineSquared; // Minimum horizon angle sine squared - uniform float material_intensity; // Intensity of the ambient occlusion - uniform float material_projectionScaleRadius; - uniform float material_bias; // Bias to avoid self-occlusion - uniform float material_peak2; // Peak value to avoid singularities - uniform float material_power; // Exponent to convert occlusion to visibility - uniform vec2 material_invProjScaleXY; //invProjection[0][0] * 2, invProjection[1][1] * 2 - - uniform vec4 camera_ProjectionParams; - - // maps orthographic depth buffer value (linear, [0, 1]) to view-space eye depth - float LinearDepthToViewDepth(float depth){ - return camera_ProjectionParams.y + (camera_ProjectionParams.z - camera_ProjectionParams.y) * depth; - } - - vec3 computeViewSpacePosition(vec2 uv, float linearDepth, vec2 invProjScaleXY) { - #ifdef CAMERA_ORTHOGRAPHIC - return vec3((vec2(0.5) - uv) * invProjScaleXY , linearDepth); - #else - return vec3((vec2(0.5) - uv) * invProjScaleXY * linearDepth, linearDepth); - #endif - } - - float SampleAndGetLinearViewDepth(float depth) { - #ifdef CAMERA_ORTHOGRAPHIC - return LinearDepthToViewDepth(depth); - #else - return remapDepthBufferLinear01(depth); - #endif - } - - vec3 computeViewSpaceNormal(vec2 uv, sampler2D depthTexture, float depth, vec2 invProjScaleXY, vec3 viewPos, vec2 sourceSize) { - vec3 normal = vec3(0.0); - #if SSAO_QUALITY == 0 || SSAO_QUALITY == 1 - vec2 uvdx = uv + vec2(sourceSize.x, 0.0); - vec2 uvdy = uv + vec2(0.0, sourceSize.y); - - float depthX = texture2D(depthTexture, uvdx).r; - float depthY = texture2D(depthTexture, uvdy).r; - - vec3 px = computeViewSpacePosition(uvdx, SampleAndGetLinearViewDepth(depthX), invProjScaleXY); - vec3 py = computeViewSpacePosition(uvdy, SampleAndGetLinearViewDepth(depthY), invProjScaleXY); - - vec3 dpdx = px - viewPos; - vec3 dpdy = py - viewPos; - - normal = normalize(cross(dpdx, dpdy)); - - #elif SSAO_QUALITY == 2 - vec2 dx = vec2(sourceSize.x, 0.0); - vec2 dy = vec2(0.0, sourceSize.y); - - vec4 H; - H.x = texture2D(depthTexture, uv - dx).r; // left - H.y = texture2D(depthTexture, uv + dx).r; // right - H.z = texture2D(depthTexture, uv - dx * 2.0).r; // left2 - H.w = texture2D(depthTexture, uv + dx * 2.0).r; // right2 - - // Calculate horizontal edge weights - vec2 horizontalEdgeWeights = abs((2.0 * H.xy - H.zw) - depth); - - vec3 pos_l = computeViewSpacePosition(uv - dx, SampleAndGetLinearViewDepth(H.x), invProjScaleXY); - vec3 pos_r = computeViewSpacePosition(uv + dx, SampleAndGetLinearViewDepth(H.y), invProjScaleXY); - vec3 dpdx = (horizontalEdgeWeights.x < horizontalEdgeWeights.y) ? (viewPos - pos_l) : (pos_r - viewPos); - - // Sample depths for vertical edge detection - vec4 V; - V.x = texture2D(depthTexture, uv - dy).r; // down - V.y = texture2D(depthTexture, uv + dy).r; // up - V.z = texture2D(depthTexture, uv - dy * 2.0).r; // down2 - V.w = texture2D(depthTexture, uv + dy * 2.0).r; // up2 - - // Calculate vertical edge weights - vec2 verticalEdgeWeights = abs((2.0 * V.xy - V.zw) - depth); - vec3 pos_d = computeViewSpacePosition(uv - dy, SampleAndGetLinearViewDepth(V.x), invProjScaleXY); - vec3 pos_u = computeViewSpacePosition(uv + dy, SampleAndGetLinearViewDepth(V.y), invProjScaleXY); - vec3 dpdy = (verticalEdgeWeights.x < verticalEdgeWeights.y) ? (viewPos - pos_d) : (pos_u - viewPos); - normal = normalize(cross(dpdx, dpdy)); - #endif - return normal; - - } - - vec3 tapLocation(float i, const float noise) { - float offset = ((2.0 * PI) * 2.4) * noise; - float angle = ((i / SAMPLE_COUNT) * SPIRAL_TURNS) * (2.0 * PI) + offset; - float radius = (i + noise + 0.5) / SAMPLE_COUNT; - return vec3(cos(angle), sin(angle), radius * radius); - } - - vec2 startPosition(const float noise) { - float angle = ((2.0 * PI) * 2.4) * noise; - return vec2(cos(angle), sin(angle)); - } - - mat2 tapAngleStep() { - vec2 t = angleIncCosSin; - return mat2(t.x, t.y, -t.y, t.x); - } - - vec3 tapLocationFast(float i, vec2 p, const float noise) { - float radius = (i + noise + 0.5) / SAMPLE_COUNT; - return vec3(p, radius * radius); - } - - void computeAmbientOcclusionSAO(inout float occlusion, float i, float ssDiskRadius, vec2 uv, vec3 originPosition, vec3 normal, - vec2 tapPosition, float noise, vec2 texSize) { - - vec3 tap = tapLocationFast(i, tapPosition, noise); - - float ssRadius = max(1.0, tap.z * ssDiskRadius); // at least 1 pixel screen-space radius - - vec2 uvSamplePos = uv + vec2(ssRadius * tap.xy) * texSize; - - float occlusionDepth = texture2D(camera_DepthTexture, uvSamplePos).r; - float linearOcclusionDepth = SampleAndGetLinearViewDepth(occlusionDepth); - // “p” is the position after spiral sampling - vec3 p = computeViewSpacePosition(uvSamplePos, linearOcclusionDepth, material_invProjScaleXY); - - // now we have the sample, compute AO - vec3 v = p - originPosition; // sample vector - float vv = dot(v, v); // squared distance - float vn = dot(v, normal); // distance * cos(v, normal) - - // discard samples that are outside of the radius, preventing distant geometry to - // cast shadows -- there are many functions that work and choosing one is an artistic - // decision. - float weight = pow(max(0.0, 1.0 - vv * material_invRadiusSquared), 2.0); - - // discard samples that are too close to the horizon to reduce shadows cast by geometry - // not sufficently tessellated. The goal is to discard samples that form an angle 'beta' - // smaller than 'epsilon' with the horizon. We already have dot(v,n) which is equal to the - // sin(beta) * |v|. So the test simplifies to vn^2 < vv * sin(epsilon)^2. - weight *= step(vv * material_minHorizonAngleSineSquared, vn * vn); - - //Calculate the contribution of a single sampling point to Ambient Occlusion - float sampleOcclusion = max(0.0, vn + (originPosition.z * material_bias)) / (vv + material_peak2); - occlusion += weight * sampleOcclusion; - } - - void scalableAmbientObscurance(out float obscurance, vec2 fragCoord, vec2 uv, vec3 origin, vec3 normal, vec2 texSize) { - float noise = interleavedGradientNoise(fragCoord); - vec2 tapPosition = startPosition(noise); - mat2 angleStep = tapAngleStep(); - - // Choose the screen-space sample radius - // proportional to the projected area of the sphere - float ssDiskRadius = -(material_projectionScaleRadius / origin.z); - - // accumulate the occlusion amount of all sampling points - obscurance = 0.0; - for (float i = 0.0; i < SAMPLE_COUNT; i += 1.0) { - computeAmbientOcclusionSAO(obscurance, i, ssDiskRadius, uv, origin, normal, tapPosition, noise, texSize); - tapPosition = angleStep * tapPosition; - } - obscurance = sqrt(obscurance * material_intensity); - } -#endif - - -void main(){ - float aoVisibility = 0.0; - #ifdef SCENE_ENABLE_SSAO - float depth = texture2D(camera_DepthTexture, v_uv).r; - float linearDepth = SampleAndGetLinearViewDepth(depth); - - // Reconstruct view space position from depth - vec3 viewPos = computeViewSpacePosition(v_uv, linearDepth, material_invProjScaleXY); - - // Compute normal - vec3 normal = computeViewSpaceNormal(v_uv, camera_DepthTexture, depth, material_invProjScaleXY, viewPos, renderer_texelSize.xy); - - float occlusion = 0.0; - scalableAmbientObscurance(occlusion, gl_FragCoord.xy, v_uv, viewPos, normal, renderer_texelSize.xy); - - // occlusion to visibility - aoVisibility = pow(clamp(1.0 - occlusion, 0.0, 1.0), material_power); - #endif - gl_FragColor = vec4(aoVisibility, aoVisibility, aoVisibility, 1.0); -} - diff --git a/packages/core/src/shaderlib/common.glsl b/packages/core/src/shaderlib/common.glsl index 1fc89a57ef..26edb214ca 100644 --- a/packages/core/src/shaderlib/common.glsl +++ b/packages/core/src/shaderlib/common.glsl @@ -82,12 +82,13 @@ float remapDepthBufferLinear01(float z){ return 1.0/ (camera_DepthBufferParams.x * z + camera_DepthBufferParams.y); } -//From Next Generation Post Processing in Call of Duty: Advanced Warfare [Jimenez 2014] +// From Next Generation Post Processing in Call of Duty: Advanced Warfare [Jimenez 2014] // http://advances.realtimerendering.com/s2014/index.html -float interleavedGradientNoise(vec2 pixCoord) +// sampleCoord must not be normalized (e.g. window coordinates) +float interleavedGradientNoise(vec2 sampleCoord) { const vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189); - return fract(magic.z * fract(dot(pixCoord, magic.xy))); + return fract(magic.z * fract(dot(sampleCoord, magic.xy))); } #ifdef GRAPHICS_API_WEBGL2 diff --git a/packages/core/src/shaderlib/pbr/pbr_helper.glsl b/packages/core/src/shaderlib/pbr/pbr_helper.glsl index a640810c9e..12ed7ae946 100644 --- a/packages/core/src/shaderlib/pbr/pbr_helper.glsl +++ b/packages/core/src/shaderlib/pbr/pbr_helper.glsl @@ -173,18 +173,15 @@ void initMaterial(out Material material, inout Geometry geometry){ diffuseAO = ((texture2D(material_OcclusionTexture, aoUV)).r - 1.0) * material_OcclusionIntensity + 1.0; #endif - #if defined(MATERIAL_HAS_OCCLUSION_TEXTURE) && defined(SCENE_USE_SPECULAR_ENV) - specularAO = saturate( pow( geometry.dotNV + diffuseAO, exp2( - 16.0 * material.roughness - 1.0 ) ) - 1.0 + diffuseAO ); - #endif - - #ifdef SCENE_ENABLE_SSAO + #ifdef SCENE_ENABLE_AMBIENT_OCCLUSION vec4 samplingPositionNDC = camera_ProjMat * camera_ViewMat * vec4( geometry.position, 1.0 ); vec2 ssaoUV = (samplingPositionNDC.xy / samplingPositionNDC.w) * 0.5 + 0.5; float ssao = texture2D(camera_SSAOTexture, ssaoUV).r; diffuseAO *= ssao; - #ifdef SCENE_USE_SPECULAR_ENV - specularAO = saturate(pow( geometry.dotNV + diffuseAO, exp2(-16.0 * material.roughness - 1.0)) - 1.0 + diffuseAO); - #endif + #endif + + #if (defined(MATERIAL_HAS_OCCLUSION_TEXTURE) || defined(SCENE_ENABLE_AMBIENT_OCCLUSION))&& defined(SCENE_USE_SPECULAR_ENV) + specularAO = saturate( pow( geometry.dotNV + diffuseAO, exp2( - 16.0 * material.roughness - 1.0 ) ) - 1.0 + diffuseAO ); #endif material.diffuseAO = diffuseAO;