实现边缘光与基于深度的屏幕空间等宽边缘光

实现边缘光与基于深度的屏幕空间等宽边缘光

屏幕空间等距边缘光1

 

在角色渲染、卡通渲染(Toon Shading)以及影视特效中,边缘光(Rim Light) 是一种常用的增强模型轮廓的视觉手段。传统的边缘光基于法线与视线方向的夹角计算,但在某些场景下会导致边缘光“溢出”到模型内部,不够自然。
本文将介绍两种方案的结合:

  1. 基于法线与视线方向的边缘光(经典 Rim Light);

  2. 基于深度图的屏幕空间等宽边缘光(Depth-based Rim Light)。

最终效果是:边缘光能严格控制在物体外轮廓区域,避免出现在模型内部,同时保持宽度一致。

一、传统边缘光(Rim Light)

实现原理

  • 核心思路:利用法线与视线方向的夹角来判断是否在边缘。

    • dot(N, V) 越小,说明该像素越接近模型轮廓。

  • 通过 smoothstep 控制过渡曲线,得到柔和的边缘光效果。

  • 做了受光面与背光面的区别处理
    half m_ndl = saturate(dot(inputData.normalWS, light.direction));
    
    half RTD_RL_MAIN = lerp(0.0, 1.0,
        smoothstep( 1.71, lerp(RTD_RL_S_Sli_Dark, RTD_RL_S_Sli_Light, m_ndl),
        pow(abs(1.0 - max(0, dot(inputData.normalWS, inputData.viewDirectionWS))),
            lerp((1.0 - _RimLightUnfillDark), (1.0 - _RimLightUnfillLight), m_ndl))));
    
    half RTD_RL_IL_OO = lerp(0.0, RTD_RL_MAIN, _RimLigInt);
    
    half3 NdLRimColor = lerp(
        _RimLightColorDark.rgb * _RimLightColorPowerDark,
        _RimLightColorLight.rgb * _RimLightColorPowerLight,
        step(0.15, m_ndl));

    特点

    ✅ 优点:

    • 计算简单,仅依赖法线与视线向量。

    • 易于通过参数调节柔和度与宽度。

    ❌ 缺点:

    • 在模型内部也可能出现亮边(例如衣服褶皱、凹陷处)。

    • 边缘宽度不恒定,会随深度与投影畸变而变化。

二、屏幕空间基于深度的等宽边缘光

实现原理

  1. 屏幕空间 UV:从 positionCS 转换得到屏幕 UV。

  2. 视图空间法线偏移:利用法线方向在屏幕空间产生偏移点。

  3. 深度采样:分别采样中心像素与偏移像素的深度值。

  4. 深度差异检测:如果深度差异较大,说明这是模型边缘。

  5. Rim Mask:通过 smoothstep 生成柔和过渡的边缘光 Mask。

// 1. 获取屏幕UV
float2 screenUV = inputData.positionCS.xy / _ScreenParams.xy;

// 2. 世界法线 → 视图空间 → 偏移方向
float3 worldNormal = normalize(normalWS);
float3 viewNormal = normalize(mul((float3x3) UNITY_MATRIX_V, worldNormal));
float2 offsetUV = screenUV + viewNormal.xy * _RimOffsets * 0.01 / inputData.positionCS.w;

// 3. 采样中心 & 偏移的深度
float centerRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV);
float offsetRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, offsetUV);

// 4. 线性化深度
float centerLinearDepth = LinearEyeDepth(centerRawDepth, _ZBufferParams);
float offsetLinearDepth = LinearEyeDepth(offsetRawDepth, _ZBufferParams);

// 5. 深度差异 → 边缘 Mask
float depthDiff = abs(offsetLinearDepth - centerLinearDepth);
float rimMask = smoothstep(0.01, 0.1, depthDiff);

特点

✅ 优点:

  • 只会在模型外轮廓处出现边缘光。

  • 边缘宽度与深度无关,屏幕空间上保持一致。

❌ 缺点:

  • 多了一次深度采样,会增加显存带宽开销。

  • 偏移采样方向依赖法线,在某些角度会出现不稳定。


三、两种方法的结合

在实际项目中,可以将 法线 Rim Light深度 Rim Light 结合:

  • 法线 Rim Light 提供基础的轮廓感与过渡;

  • 深度 Rim Light 确保边缘光只出现在物体轮廓,不渗入模型内部。

最终实现:

half RTD_RL_IL_OO = ...;      // 法线 Rim 强度
float rimMask = ...;          // 深度 Rim Mask
half finalRim = lerp(0, RTD_RL_IL_OO, rimMask);

lightingData.additionalLightsColor = lerp(
    lightingData.additionalLightsColor,
    lightingData.additionalLightsColor + NdLRimColor,
    finalRim);

这样,我们既保留了传统边缘光的柔和渐变,又利用深度检测消除了模型内部的溢出问题。


四、应用场景

  • 卡通渲染:让角色的外轮廓有明显的高光边缘。

  • 科幻材质:能量护盾、光效溢出效果。

  • 电影渲染:强调人物边缘与背景的分离。


五、总结

  • 传统 Rim Light:快速,依赖法线与视线,适合大多数情况。

  • 深度 Rim Light:严格控制边缘,仅在物体轮廓出现,保证一致性。

  • 二者结合:兼顾表现力与准确性,是更通用的方案。

完整代码:

#if defined(_RIMLIGHT)
    half RTD_RL_S_Sli_Light = lerp(1.70,0.29,_RimLightSoftnessLight);
    half RTD_RL_S_Sli_Dark = lerp(1.70,0.29,_RimLightSoftnessDark);
    half m_ndl =saturate(dot(inputData.normalWS, light.direction));
    half RTD_RL_MAIN = lerp(0.0, 1.0,
        smoothstep( 1.71, lerp(RTD_RL_S_Sli_Dark, RTD_RL_S_Sli_Light, m_ndl),
        pow(abs( 1.0-max(0,dot(inputData.normalWS, inputData.viewDirectionWS))), lerp((1.0 - _RimLightUnfillDark), (1.0 - _RimLightUnfillLight), m_ndl)) ));
    half RTD_RL_IL_OO = lerp( 0.0, RTD_RL_MAIN, _RimLigInt);
    half3 NdLRimColor = lerp(_RimLightColorDark.rgb * _RimLightColorPowerDark, _RimLightColorLight.rgb * _RimLightColorPowerLight, step(0.15,m_ndl));
 //==========================================================================================
 // 屏幕空间深度等宽边缘光
 // 屏幕空间UV
     #if defined(_RIMDEPTH)
        // 1. 获取标准化屏幕UV(NDC → UV)
        float2 screenUV = inputData.positionCS.xy / _ScreenParams.xy;
        // 2. 使用世界法线,转换到视图空间作为偏移方向
        float3 worldNormal = normalize(normalWS);
        float3 viewNormal = normalize(mul((float3x3) UNITY_MATRIX_V, worldNormal)); // 转为视图空间
        float2 offsetUV = screenUV + viewNormal.xy * _RimOffsets * 0.01 / inputData.positionCS.w; // 视角矫正偏移

        // 3. 采样中心和偏移位置的深度
        float centerRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV);
        float offsetRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, offsetUV);

        // 4. 线性化深度
        float centerLinearDepth = LinearEyeDepth(centerRawDepth, _ZBufferParams);
        float offsetLinearDepth = LinearEyeDepth(offsetRawDepth, _ZBufferParams);

        // 5. 边缘检测,使用 smoothstep 柔和控制
        float depthDiff = abs(offsetLinearDepth - centerLinearDepth);
        float rimMask = smoothstep(0.01, 0.1, depthDiff); // 范围可调

        // 6. 最终 rim 强度
        half finalRim = lerp(0, RTD_RL_IL_OO, rimMask); // RTD_RL_IL_OO 为最大边缘光强度
        lightingData.additionalLightsColor = lerp(lightingData.additionalLightsColor, lightingData.additionalLightsColor + NdLRimColor, finalRim);
    #endif
lightingData.additionalLightsColor = lerp(lightingData.additionalLightsColor, lightingData.additionalLightsColor + NdLRimColor, RTD_RL_IL_OO);
#endif
© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容