Unity Shader 屏幕空间反射 (SSR) 原理解析

深入理解 URP 中 SSR 的实现原理、工作流程与性能优化策略,附带完整案例分析与代码实现

什么是屏幕空间反射 (SSR)

屏幕空间反射(Screen Space Reflection,简称 SSR)是一种实时反射技术,它利用当前渲染帧的深度缓冲区和颜色缓冲区来计算反射效果。与传统的基于屏幕外渲染(Render Target)的反射方法相比,SSR 可以更高效地生成接触反射、湿润表面反射等效果。

核心优势

SSR 的最大优势在于它能捕捉到屏幕内的所有物体产生的反射,无需额外的渲染 pass 或渲染目标,尤其适合表现潮湿地面、金属表面、玻璃等材质的即时反射。

SSR 工作原理详解

2.1 核心技术:光线步进 (Ray Marching)

SSR 的核心算法是光线步进(Ray Marching)。对于屏幕上的每个像素,我们沿着反射方向逐步向前探测,每次步进后查询深度缓冲区判断是否与场景几何体相交。

1

计算反射向量

根据表面法线和入射方向,使用公式 R = I - 2(N·I)N 计算反射向量 R

2

执行光线步进

从反射表面位置开始,沿着反射方向以固定步长向前推进

3

深度测试

在每个步进点,将光线深度与深度缓冲区中的值进行比较

4

命中检测

当光线深度小于等于场景深度时,说明发生了命中

5

精确求交

使用二分搜索或线性搜索精确找到交点位置

6

采样反射颜色

从颜色缓冲区中采样命中点对应的颜色值

2.2 深度缓冲区的重要性

深度缓冲区存储了从摄像机到场景中每个像素对应点的距离信息。SSR 依赖深度缓冲区来判断光线是否与场景相交------当光线到达某点的深度值小于深度缓冲区中存储的深度时,表明光线命中了更近的物体。

步进步长的权衡

步长过大:可能错过细小的几何体特征,导致漏检

步长过小:计算量大幅增加,影响性能。建议在反射近距离使用小步长,远距离使用大步长

URP 中 SSR 的实现

3.1 URP vs HDRP

Unity 的渲染管线对 SSR 的支持程度不同。了解这些差异有助于选择合适的实现方案:

URP
  • 需要自行实现或使用第三方方案
  • 提供 Volume Framework 扩展点
  • 适合移动端和轻度反射效果
  • 需要 Shader 编程能力
HDRP
  • 内置 Screen Space Reflection
  • 完整的 PBR 材质支持
  • 高质量但计算量大
  • 适合 PC 和主机平台

3.2 URP SSR 配置参数

如果你使用的是支持 SSR 的 URP 版本,以下是 Volume 组件中的关键参数:

enabledbool启用或禁用 SSR 效果

qualityenum渲染质量等级:Low / Medium / High / Ultra

maxDistancefloat最大反射距离,超出此范围的物体不产生反射

iterationCountint光线步进的最大迭代次数,影响精度和性能

stepSizefloat每次步进的距离,影响覆盖范围和精度

thicknessfloat光线检测的厚度阈值,用于处理薄物体

roughSurfacebool是否为粗糙表面启用降噪模糊

代码实现:自定义 SSR Shader

以下是一个完整的 URP SSR 实现方案,包含 Shader 代码和配置脚本。

4.1 SSR Compute Shader

cs 复制代码
// SSR 光线步进计算着色器

#pragma kernel SSRMain

 

// 常量定义

static const int MAX_STEPS = 64;

static const float MIN_STEP = 0.05;

static const float MAX_STEP = 2.0;

static const float THICKNESS = 0.1;

 

// 纹理和采样器

Texture2D _CameraDepthTexture;

Texture2D _CameraNormalsTexture;

Texture2D _CameraOpaqueTexture;

SamplerState linearClampSampler;

 

// 矩阵和参数

float4x4 _ViewMatrix;

float4x4 _InverseViewMatrix;

float4x4 _InverseProjectionMatrix;

float _ScreenParams;

float _RayDistance;

 

// 输出结构

RWTexture2D<float4> _SSRResult;

RWTexture2D<float> _SSRHitMask;

 

// 辅助函数:重建视图空间位置

float3 ReconstructViewPos(float2 uv, float depth)

{

    float4 clipPos = float4(uv * 2.0 - 1.0, depth, 1.0);

    float4 viewPos = mul(_InverseProjectionMatrix, clipPos);

    return viewPos.xyz / viewPos.w;

}

 

// 辅助函数:获取光线步长

float GetStepSize(float currentDepth)

{

    // 根据当前深度动态调整步长

    float t = saturate(currentDepth / _RayDistance);

    return lerp(MIN_STEP, MAX_STEP, t);

}

 

// 核心 Shader 入口

[numthreads(8, 8, 1)]

void SSRMain(uint3 id : SV_DispatchThreadID)

{

    // 坐标归一化

    float2 uv = id.xy / float2(_ScreenParams, _ScreenParams.y);

    float depth = _CameraDepthTexture.Load(int3(id.xy, 0)).r;

    

    // 跳过远平面和无反射区域

    if (depth >= 0.9999) {

        _SSRResult[id.xy] = float4(0, 0, 0, 0);

        return;

    }

    

    // 重建视图空间位置和法线

    float3 viewPos = ReconstructViewPos(uv, depth);

    float3 normal = _CameraNormalsTexture.Load(int3(id.xy, 0)).rgb;

    normal = mul(float3(normal.xy, normal.z * 2.0 - 1.0), (float3x3)_ViewMatrix);

    

    // 计算反射方向

    float3 viewDir = normalize(viewPos);

    float3 reflectDir = reflect(viewDir, normal);

    

    // 光线步进主循环

    float4 result = float4(0, 0, 0, 0);

    float rayLength = 0.0;

    bool hit = false;

    

    for (int i = 0; i < MAX_STEPS; i++)

    {

        // 计算当前采样点

        float3 samplePos = viewPos + reflectDir * rayLength;

        float stepSize = GetStepSize(rayLength);

        

        // 投影到屏幕空间

        float4 clipPos = mul(float4(samplePos, 1.0), _InverseViewMatrix);

        clipPos /= clipPos.w;

        float2 sampleUV = clipPos.xy * 0.5 + 0.5;

        

        // 边界检查

        if (sampleUV.x < 0 || sampleUV.x > 1 ||

            sampleUV.y < 0 || sampleUV.y > 1)

        {

            break;

        }

        

        // 深度对比测试

        float sceneDepth = _CameraDepthTexture.Sample(linearClampSampler, sampleUV).r;

        float rayDepth = clipPos.z * 0.5 + 0.5;

        

        // 检测命中

        if (rayDepth < sceneDepth + THICKNESS)

        {

            // 命中!采样反射颜色

            result = _CameraOpaqueTexture.Sample(linearClampSampler, sampleUV);

            result.a = 1.0 - saturate(rayLength / _RayDistance);

            hit = true;

            break;

        }

        

        // 继续步进

        rayLength += stepSize;

        

        // 超出最大距离

        if (rayLength > _RayDistance)

            break;

    }

    

    // 写入结果

    _SSRResult[id.xy] = result;

    _SSRHitMask[id.xy] = hit ? 1.0 : 0.0;

}

4.2 SSR 后处理 Shader

cs 复制代码
// 后处理 SSR 混合 Shader

Shader "URP/SSREffect"

{

    Properties

    {

        _MainTex ("Source Texture", 2D) = "black" {}

        _SSRTex ("SSR Texture", 2D) = "black" {}

        _ReflectionIntensity ("Reflection Intensity", Range(0, 1)) = 1.0

        _Roughness ("Roughness Bias", Range(0, 1)) = 0.1

    }

    

    SubShader

    {

        Tags { "RenderPipeline" = "UniversalPipeline" }

        

        Pass

        {

            HLSLPROGRAM

            #pragma vertex FullscreenVert

            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            

            // 纹理声明

            TEXTURE2D(_MainTex);

            TEXTURE2D(_SSRTex);

            SAMPLER(linearRepeatSampler);

            

            // 常量缓冲区

            CBUFFER_START(UnityPerMaterial)

                float _ReflectionIntensity;

                float _Roughness;

            CBUFFER_END

            

            // 全屏顶点着色器

            float4 FullscreenVert(uint vertexID : SV_VertexID) : SV_POSITION

            {

                float2 positions[4] = {

                    { -1, -1 }, { 1, -1 }, { -1, 1 }, { 1, 1 }

                };

                return float4(positions[vertexID], 0, 1);

            }

            

            // 片元着色器:混合原始颜色和反射

            float4 frag(float4 position : SV_POSITION,

                       float2 uv : TEXCOORD0) : SV_Target

            {

                // 采样原始颜色

                float4 originalColor = SAMPLE_TEXTURE2D(_MainTex, linearRepeatSampler, uv);

                

                // 采样 SSR 结果

                float4 ssrColor = SAMPLE_TEXTURE2D(_SSRTex, linearRepeatSampler, uv);

                

                // 粗糙度模糊处理

                if (_Roughness > 0.0)

                {

                    float2 blurDir = float2(_Roughness * 0.02, 0);

                    float4 blurAccum = float4(0, 0, 0, 0);

                    

                    // 简化的 5-tap 模糊

                    for (int i = -2; i <= 2; i++)

                    {

                        blurAccum += SAMPLE_TEXTURE2D(_SSRTex, linearRepeatSampler,

                                            uv + blurDir * i);

                    }

                    ssrColor = blurAccum / 5.0;

                }

                

                // 线性混合原始颜色和反射

                float blendFactor = ssrColor.a * _ReflectionIntensity;

                float3 finalColor = lerp(originalColor.rgb,

                                              ssrColor.rgb,

                                              blendFactor);

                

                return float4(finalColor, originalColor.a);

            }

            ENDHLSL

        }

    }

}

4.3 C# 配置脚本

cs 复制代码
using UnityEngine;

using UnityEngine.Rendering;

using UnityEngine.Rendering.Universal;

 

/// <summary>

/// SSR 后处理效果配置组件

/// </summary>

public class SSREffect : ScriptableRendererFeature

{

    [System.Serializable]

    public class SSRSettings

    {

        [Header("Render Settings")]

        public RenderPassEvent renderEvent = RenderPassEvent.AfterRenderingOpaques;

        

        [Header("SSR Parameters")]

        public int maxSteps = 64;

        public float maxDistance = 50f;

        public float stepSize = 0.5f;

        public float thickness = 0.1f;

        

        [Header("Quality")]

        public float roughness = 0.1f;

        public float intensity = 1.0f;

    }

    

    public SSRSettings settings = new SSRSettings();

    private SSRPass ssrPass;

    

    /// <summary>

    /// 创建 Pass 实例

    /// </summary>

    public override void Create()

    {

        ssrPass = new SSRPass(settings, renderPassEvent);

    }

    

    /// <summary>

    /// 添加 Pass 到渲染队列

    /// </summary>

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)

    {

        ssrPass.Setup(renderer);

        renderer.EnqueuePass(ssrPass);

    }

}

 

/// <summary>

/// SSR 渲染 Pass 实现

/// </summary>

class SSRPass : ScriptableRenderPass

{

    private SSREffect.SSRSettings settings;

    private ScriptableRenderer renderer;

    private Material ssrMaterial;

    private ComputeShader ssrCompute;

    private int ssrKernel;

    private RenderTargetHandle ssrTarget;

    

    public SSRPass(SSREffect.SSRSettings settings, RenderPassEvent evt)

    {

        this.settings = settings;

        renderPassEvent = evt;

        

        // 加载 Compute Shader

        ssrCompute = Resources.Load<ComputeShader>("SSRCompute");

        ssrKernel = ssrCompute.FindKernel("SSRMain");

        

        // 创建材质

        Shader ssrShader = Shader.Find("URP/SSREffect");

        if (ssrShader != null)

        {

            ssrMaterial = new Material(ssrShader);

            ssrMaterial.hideFlags = HideFlags.HideAndDontSave;

        }

    }

    

    public void Setup(ScriptableRenderer renderer)

    {

        this.renderer = renderer;

    }

    

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)

    {

        // 检查是否启用

        if (settings == null || ssrMaterial == null)

            return;

        

        CommandBuffer cmd = CommandBuffer.Pool.Get("SSR Effect");

        

        Camera camera = renderingData.cameraData.camera;

        var descriptor = camera.texelSize;

        descriptor.width = (int)(descriptor.width * settings.resolution);

        descriptor.height = (int)(descriptor.height * settings.resolution);

        

        // 1. 执行 Compute Shader

        cmd.SetComputeTextureParam(ssrCompute, ssrKernel, "_SSRResult", ssrTarget.id);

        cmd.SetComputeMatrixParam(ssrCompute, "_InverseViewMatrix", camera.worldToCameraMatrix);

        cmd.SetComputeFloatParam(ssrCompute, "_RayDistance", settings.maxDistance);

        

        // 分发线程组

        int threadGroupsX = (int)Mathf.Ceil(descriptor.width / 8f);

        int threadGroupsY = (Int64)Mathf.Ceil(descriptor.height / 8f);

        cmd.DispatchCompute(ssrCompute, ssrKernel, threadGroupsX, threadGroupsY, 1);

        

        // 2. 后处理混合

        ssrMaterial.SetTexture("_SSRTex", ssrTarget.id);

        ssrMaterial.SetFloat("_ReflectionIntensity", settings.intensity);

        ssrMaterial.SetFloat("_Roughness", settings.roughness);

        

        cmd.Blit(null, RenderTargetHandle.GetTemporary(descriptor), ssrMaterial);

        context.ExecuteCommandBuffer(cmd);

    }

    

    public override void OnCameraCleanup(CommandBuffer cmd)

    {

        // 释放临时渲染目标

        cmd.ReleaseTemporary(RenderTargetHandle.GetTemporary(ssrTarget));

    }

}

案例分析:潮湿地面反射

让我们通过一个实际案例来理解 SSR 的应用场景和配置方法。本案例展示如何实现雨后街道的潮湿地面反射效果。

5.1 材质配置

cs 复制代码
// 潮湿地面材质 - 基于物理的反射计算

 

// 根据粗糙度计算 Fresnel 效应

float3 CalculateFresnel(float noV, float roughness)

{

    // Schlick 近似

    float f0 = 0.04; // 非金属的基础反射率

    float f90 = saturate(f0 + (1.0 - f0) * pow(1.0 - noV, 5.0));

    // 粗糙度影响掠射角的反射强度

    return float3(f90);

}

 

// 主渲染函数

float4 WetFloorFragment(SurfaceData surface, float3 ssrReflection)

{

    float noV = dot(surface.normal, surface.viewDir);

    float3 fresnel = CalculateFresnel(noV, surface.roughness);

    

    // 潮湿地面特征:低粗糙度,强 Fresnel

    float wetness = saturate(1.0 - surface.roughness * 3.0);

    float reflectionStrength = wetness * length(fresnel);

    

    // 混合 SSR 和基础反射

    float3 finalReflection = lerp(surface.envReflection,

                                        ssrReflection,

                                        reflectionStrength);

    

    return float4(finalReflection, 1.0);

}

5.2 性能优化配置

优化项 推荐值 说明
分辨率比例 0.5 - 0.75 SSR 渲染分辨率降低,减少计算量
最大步数 32 - 64 移动端建议 32,PC 可用 64
最大距离 20 - 50m 超出距离的反射效果渐隐
步长范围 0.1 - 1.0 近处用小步长保证精度

最佳实践

对于潮湿地面这类大面积反射,建议配合粗糙度遮罩使用------只在积水区域启用 SSR,其余区域使用传统的环境反射或平面反射,这样可以在保证视觉效果的同时显著提升性能。

常见问题与解决方案

Q1: 为什么 SSR 在屏幕边缘产生不自然的接缝?

这是因为光线步进在屏幕边界处超出范围导致采样失败。解决方案是在边界处进行镜像处理,或者设置较小的最大步长使光线不会轻易越界。

Q2: 透明物体无法产生正确的反射怎么办?

SSR 默认基于不透明几何体的深度。如果需要透明物体的反射,需要在额外的 Pass 中单独处理,或使用传统的渲染目标反射方法作为补充。

Q3: 移动端如何优化 SSR 性能?

降分辨率0.25SSR 目标分辨率设为屏幕的 25%

减少步数16-24降低光线步进的最大迭代次数

限制范围10-15m减小最大反射距离

选择性启用遮罩只对特定材质启用 SSR

Q4: 如何处理 SSR 与后处理特效的兼容性?

SSR Pass 应在所有后处理效果之前执行,确保反射颜色不会被后续的色调映射或色彩校正影响。如果需要正确的色彩分级,需要在 SSR 之后保存反射颜色,或者在 SSR 计算时使用线性空间颜色。

总结

屏幕空间反射是现代实时渲染中不可或缺的特效之一。通过理解其光线步进原理、深度测试机制以及与渲染管线的集成方式,我们可以构建出既高质量又高效的反射效果。

关键要点

原理: SSR 基于当前帧的深度和颜色缓冲区,通过光线步进检测反射命中点
优势: 无需额外渲染目标,可捕捉屏幕内所有物体的反射
限制: 无法反射屏幕外物体,薄物体可能漏检
**优化:**动态步长、降分辨率、选择性启用是移动端优化的关键

相关推荐
qq_654366982 小时前
如何安全清理数据库中未引用的图片文件
jvm·数据库·python
心前阳光2 小时前
Unity之利用特性给ScriptableObject分组
unity·游戏引擎
2401_882273722 小时前
HTML怎么创建成就隐藏后恢复_HTML“重新公开”操作入口【详解】
jvm·数据库·python
mxwin2 小时前
Unity Shader 屏幕空间法线重建 从深度缓冲反推世界法线——原理、踩坑与 URP Shader 实战
unity·游戏引擎·shader
weixin_458580122 小时前
如何自定义修改 Traccar Web 界面模板
jvm·数据库·python
空中海2 小时前
第五篇:Unity工程化能力
elasticsearch·unity·游戏引擎
m0_515098422 小时前
如何修改AWR保留时间_将默认8天保留期延长至30天的设置
jvm·数据库·python
qq_654366982 小时前
如何在 macOS 上为 PHP 8.0 正确集成 XML-RPC 支持
jvm·数据库·python
2301_773553622 小时前
Bootstrap 4.5 实现多级下拉菜单并行展开(不自动关闭其他已开菜单)
jvm·数据库·python