羽化光渲染技术怎么用 从游戏到影视后期边缘渐变光效教程 常见应用场景与实用技巧全解析

羽化光,或者说边缘渐变光效,最近在独立游戏圈和短视频特效里火得一塌糊涂。你有没有注意过,有些角色的轮廓边缘会有一层柔和的光晕,像被晨雾轻轻笼罩着?那种”仙气”或者”科技感”,很多都是靠羽化光渲染出来的。今天咱们就掰开揉碎了聊聊这个技术。

羽化光到底是什么

羽化光本质上是边缘光(Rim Light)的软化处理。简单说,就是物体的轮廓边缘会有一圈渐变过渡的光,而不是生硬的明暗分界线。”羽化”这个词借自Photoshop的羽化选区——边缘模糊、柔和,过渡自然。

在实际渲染管线里,它通常通过以下几个手段实现:

  • 背面剔除(Back-face culling)的变体:只渲染法线朝外的面,让边缘发光
  • 法线与世界/屏幕空间的夹角检测:夹角越大,边缘越亮
  • 菲涅耳(Fresnel)效应模拟:视角越接近平行表面,反射越强

下面我用Unity的Shader Graph和HLSL代码给你拆解一下。

游戏引擎里的羽化光实现

方案一:Shader Graph可视化实现(Unity URP/HDRP)

这是最简单的方式,不需要写代码。打开Unity的Shader Graph,按以下步骤操作:

第一步:创建基础材质的Shader Graph

输入节点 → 几何处理 → 数学运算 → 颜色混合 → 输出

第二步:关键节点配置

打开Unity编辑器,新建一个PBR Material或Unlit Material的Shader Graph,然后在Graph面板里按这个逻辑连线:

节点 作用 参数设置
Object World Normals 获取世界空间法线 输出端口Normal
View World Position 获取世界空间相机位置 输出端口Position
Vector 3(相机上方向) 定义光的方向参考 (0,1,0) 或自定义
Dot Product 计算法线与视角的夹角 输入A=法线,输入B=视角方向
Pow 锐化边缘效果 Exponent=2~8
Lerp 混合基础颜色和边缘光 T值来自上面的结果
Alpha One Minus 反相得到边缘遮罩 让中心暗、边缘亮

第三步:核心公式节点连线

在Shader Graph里,关键的数学逻辑是这样的——你需要一个节点组合来算出”边缘度”:

EdgeAmount = 1.0 - dot(NormalizedViewDirection, WorldNormal)

然后在Shader Graph里对应的是:

  1. 把 View World Position 减去 Object World Position,得到从物体指向相机的向量
  2. 对这个向量做 Normalize
  3. 和 Object World Normals 做 Dot Product
  4. 用一个 1 - Dot 的节点,或者把结果取反(Multiply by -1, Add 1)

这样得到的值,中心法线朝向相机时值接近0,边缘法线垂直于视线时值接近1,正好是边缘光需要的遮罩。

第四步:加上羽化过渡

光有边缘遮罩还不够,边缘会太硬。你需要用 Soft Parameter 或者 Smoothstep 节点来做平滑:

Smoothstep(EdgeThreshold - Softness, EdgeThreshold + Softness, EdgeAmount)

在Shader Graph里,找到 Smoothstep 节点,三个输入分别是:

  • Min:边缘阈值下限(比如0.3)
  • Max:边缘阈值上限(比如0.7)
  • In:上面的EdgeAmount

这样边缘光就有了柔和的渐变过渡,不会有一条刺眼的分界线。

方案二:HLSL手写Shader(更灵活,适合定制)

如果你需要更多控制,比如下面的效果:

  • 边缘光的颜色随角度变化
  • 双面渲染
  • 结合屏幕空间后处理

那我推荐直接写HLSL代码。下面是URP里一个完整可用的边缘光Shader示例:

Shader "Custom/RimLight"
{
    Properties
    {
        [Header(Base Color)]
        _BaseColor ("Base Color", Color) = (1,1,1,1)
        
        [Header(Rim Light)]
        _RimColor ("Rim Color", Color) = (0.5, 0.8, 1, 1)
        _RimThreshold ("Rim Threshold", Range(0, 1)) = 0.3
        _RimSoftness ("Rim Softness", Range(0, 1)) = 0.5
        _RimIntensity ("Rim Intensity", Range(0, 5)) = 1.5
        
        [Header(Direction)]
        _RimDirection ("Rim Direction (World Space)", Vector) = (0, 1, 0, 0)
        _UseViewDirection ("Use View Direction", Int) = 1
    }
    
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry" }
        
        LOD 100
        
        Pass
        {
            Tags { "LightMode" = "UniversalForward" }
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };
            
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
                float4 _RimColor;
                float _RimThreshold;
                float _RimSoftness;
                float _RimIntensity;
                float3 _RimDirection;
                int _UseViewDirection;
            CBUFFER_END
            
            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.worldNormal = TransformObjectToWorldNormal(v.normal);
                o.worldPos = TransformObjectToWorld(v.vertex);
                o.viewDir = _UseViewDirection 
                    ? normalize(_WorldSpaceCameraPos - o.worldPos)
                    : normalize(_RimDirection);
                return o;
            }
            
            half4 frag(v2f i) : SV_Target
            {
                // 计算法线与视线方向的点积
                float NdotV = dot(normalize(i.worldNormal), normalize(i.viewDir));
                
                // 取反,让边缘值变大
                float rimRaw = 1.0 - abs(NdotV);
                
                // Smoothstep 做羽化过渡
                float rim = smoothstep(
                    _RimThreshold - _RimSoftness,
                    _RimThreshold + _RimSoftness,
                    rimRaw
                );
                
                // 混合基础颜色和边缘光
                half3 finalColor = lerp(_BaseColor.rgb, _RimColor.rgb, rim * _RimIntensity);
                
                return half4(finalColor, 1.0);
            }
            ENDHLSL
        }
    }
}

代码里的关键参数,我解释一下:

_RimThreshold(边缘阈值):控制边缘光出现的范围。值越大,边缘光范围越小,越集中;值越小,边缘光越宽。

_RimSoftness(柔化参数):这个就是”羽化”的核心。它控制边缘光从出现到消失的过渡区域宽度。值越大,边缘越柔和,不会有明显的分界线。

_RimIntensity(边缘光强度):边缘光的亮度倍数。可以超过1.0做出自发光效果。

_UseViewDirection(方向模式):0表示使用固定的世界空间方向(比如从上方打来的边缘光),1表示使用相机视角方向(经典的菲涅耳边缘光效果)。

影视后期里的羽化光效果

游戏用的是实时渲染,影视后期走的是合成路径。常见的软件是After Effects、Nuke、达芬奇Resolve。

After Effects里的实现方法

在AE里做羽化光效,核心思路是从边缘生成遮罩,再用渐变填充光色。

方法一:CC Lightleak + 边缘提取

素材层 → 复制一层 → 上层做边缘检测 → 反相 → 高斯模糊羽化 → 叠加模式

具体操作步骤:

第一步:准备素材 把需要加边缘光的画面导入AE,创建一个新的预合成。

第二步:提取边缘 在复制出来的上层应用以下效果(按顺序):

  1. Effects → Generate → CC Lightleak

    • 把Blend Mode设为Screen
    • 调整Lightleak Color让边缘光呈现你想要的色调
    • 这个方法适合做出彩色渐变边缘光
  2. 更精准的方式:用Luma Matte提取边缘

    • 上层应用 Effects → Adjustment → Levels
    • 把Input Black调到128以上,只保留亮部边缘
    • 再应用 Gaussian Blur 做羽化(Radius约10-30像素,看画面分辨率)

第三步:创建边缘光层

新建 Solid 层(颜色选边缘光的色调)
→ 应用 Effect → Blur & Sharpen → Fast Box Blur
→ 用 Layer → Matte → Luma Invert 反相遮罩
→ 合成模式设为 Add 或 Screen
→ 调整Opacity控制强度

方法二:使用Saber插件(专业推荐)

如果你经常做边缘光效果,Saber(由VideoCopilot出品)是行业标准工具。它的Edge Glow功能可以直接在素材上生成羽化边缘光,支持:

  • 边缘颜色渐变
  • 边缘粗细可调
  • 羽化程度可控
  • 支持发光追踪

操作步骤:

选中素材层 → 应用 Effect → Saber → 
选择 preset 里的 "Edge Glow" 类预设 → 
调整 Glow Amount 和 Feather

Nuke里的实现方式

Nuke是专业影视合成的工作流,用节点来实现:

Read("source") 
→ Channel(rgba, r) 
→ Level(黑场抬升,提取亮部边缘)
→ Blur(羽化)
→ Reformat/EdgeGlow
→ Add(叠加模式)
→ Write("output")

核心节点序列:

# Nuke脚本示例(伪代码表示逻辑)
read = nuke.nodes.Read(file="input.mov")
edge = nuke.nodes.Levels(input=read['output'], black=0.6, white=1.0)  # 提取边缘
blur = nuke.nodes.Blur(input=edge, blurx=20, blury=20)  # 羽化
color = nuke.nodes.RGB(input=blur, r=0.8, g=0.6, b=0.9)  # 边缘光颜色
result = nuke.nodes.Add(a=read['output'], b=color, operation='Add')
nuke.nodes.Write(input=result, file="output.exr")

关键点:Nuke里用的是EXR格式做中间文件,保留完整的动态范围,边缘光可以叠加在原始画面上而不损失细节。

常见应用场景

场景一:角色轮廓光(游戏/动画)

这是羽化光最经典的应用。角色在背光、逆光场景下,轮廓边缘会有一圈柔和的光,让角色从背景中分离出来。

游戏实现要点:

  • 用背面渲染(Backface Rendering)的方式:单独渲染一层背面法线,叠加到主渲染上
  • 或者用Screen Space的方法:先渲染 depth buffer,再在屏幕空间计算边缘

影视动画要点:

  • 通常是3D软件里(Maya/Houdini)的XPU/Hair特效,或者是合成阶段加的光效
  • 关键是要配合灯光方向,让边缘光出现在光源对立面

场景二:UI元素的发光边框

游戏中技能图标、按钮选中状态,经常有一圈渐变发光的边框。这个用羽化光做非常合适。

Unity UGUI实现思路:

// 简单的边缘光Shader,用于UI
Shader "Custom/UIRimLight"
{
    Properties
    {
        [Texture2D] _MainTex ("Base Texture", 2D) = "white" {}
        [Color] _BaseColor ("Base Color", Color) = (1,1,1,1)
        [Color] _RimColor ("Rim Color", Color) = (0.5, 0.8, 1, 1)
        [Range(0,1)] _RimThreshold ("Threshold", Float) = 0.3
        [Range(0,1)] _RimSoftness ("Softness", Float) = 0.5
        [Range(0,5)] _RimIntensity ("Intensity", Float) = 1.5
    }
    
    SubShader
    {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off
        
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
            };
            
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
            };
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _BaseColor;
            float4 _RimColor;
            float _RimThreshold;
            float _RimSoftness;
            float _RimIntensity;
            
            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color;
                return o;
            }
            
            half4 frag(v2f i) : SV_Target
            {
                // 从UV坐标计算归一化中心距离(用于边缘检测)
                float2 center = i.uv - 0.5;
                // 对于矩形UI,用min/max计算到边缘的距离
                float rimDist = min(min(center.x, 1-center.x), min(center.y, 1-center.y));
                // 反转:边缘处rimDist小,中心处大
                float edgeMask = 1.0 - smoothstep(0.0, 0.15, rimDist);
                
                // 羽化边缘光
                float rim = smoothstep(
                    _RimThreshold - _RimSoftness,
                    _RimThreshold + _RimSoftness,
                    edgeMask
                );
                
                half4 texColor = tex2D(_MainTex, i.uv);
                half3 finalColor = lerp(texColor.rgb * _BaseColor.rgb, _RimColor.rgb, rim * _RimIntensity);
                half alpha = texColor.a * i.color.a;
                
                return half4(finalColor, alpha);
            }
            ENDHLSL
        }
    }
}

这个Shader的思路和之前不同——它是基于UV空间到边缘的距离来计算边缘光,而不是法线方向。适合UI这类不需要3D法线的场景。

场景三:魔法/技能特效的边缘辉光

游戏中的火焰、闪电、魔法阵,边缘通常有一圈柔和的辉光。这种效果在游戏里常用后处理(Post-processing)实现。

Unity URP Post-process实现:

using UnityEngine;
using UnityEngine.Rendering.Universal;

[ExecuteAlways]
public class RimLightPostProcess : MonoBehaviour
{
    [Range(0, 1)] public float threshold = 0.8f;
    [Range(0, 1)] public float softness = 0.2f;
    public Color rimColor = Color.cyan;
    [Range(0, 5)] public float intensity = 1.0f;
    
    public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
    
    private Material material;
    
    public Material materialSource
    {
        get
        {
            if (material == null)
            {
                material = new Material(Shader.Find("Custom/RimLightPostProcess"));
                material.hideFlags = HideFlags.HideAndDontSave;
            }
            return material;
        }
    }
    
    public class Pass : ScriptableRenderPass
    {
        RenderingParameters m_settings;
        FilterSettings m_filterSettings;
        int m_ProfilerTag;
        Material m_material;
        float m_threshold;
        float m_softness;
        Color m_rimColor;
        float m_intensity;
        
        public Pass(
            RenderingParameters settings,
            FilterSettings filterSettings,
            int profilerTag,
            Material material,
            float threshold,
            float softness,
            Color rimColor,
            float intensity
        )
        {
            m_settings = settings;
            m_filterSettings = filterSettings;
            m_ProfilerTag = profilerTag;
            m_material = material;
            m_threshold = threshold;
            m_softness = softness;
            m_rimColor = rimColor;
            m_intensity = intensity;
            
            renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
        }
        
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (!renderingData.cameraData.isSceneViewCamera && !m_settings.hideParticles)
            {
                CommandBuffer cmd = CommandBufferPool.Get("RimLight");
                RenderingUtils.ConfigureCommandBuffer(cmd);
                
                // 绘制全屏四边形,应用边缘光后处理
                cmd.DrawMesh(
                    renderingData.cameraData.renderer.cullingResult.visibleMeshes[0].mesh,
                    Matrix4x4.identity,
                    m_material,
                    0, 0, null
                );
                
                context.ExecuteCommandBuffer(cmd);
                CommandBufferPool.Release(cmd);
            }
        }
    }
}

对应的Shader(后处理版本):

Shader "Custom/RimLightPostProcess"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Threshold ("Threshold", Range(0,1)) = 0.8
        _Softness ("Softness", Range(0,1)) = 0.2
        _RimColor ("Rim Color", Color) = (0,1,1,1)
        _Intensity ("Intensity", Range(0,5)) = 1.0
    }
    
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        
        Pass
        {
            Tags { "RenderPipeline" = "UniversalRenderPipeline" }
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            
            float _Threshold;
            float _Softness;
            float4 _RimColor;
            float _Intensity;
            
            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
            };
            
            struct Varyings
            {
                float4 positionCS   : SV_POSITION;
                float2 uv           : TEXCOORD0;
            };
            
            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = input.uv;
                return output;
            }
            
            half4 frag(Varyings input) : SV_Target
            {
                // 采样深度缓冲
                float depth = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, input.uv).r;
                
                // 将深度转换为线性值(URP标准)
                float linearDepth = LinearEyeDepth(depth, _ZBufferParams);
                
                // 计算屏幕空间梯度,检测边缘
                float2 pixelSize = float2(1.0 / _ScreenParams.x, 1.0 / _ScreenParams.y);
                
                float dX = abs(linearDepth - SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, input.uv + float2(pixelSize.x, 0)).r);
                float dY = abs(linearDepth - SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, input.uv + float2(0, pixelSize.y)).r);
                
                // 边缘强度
                float edgeStrength = max(dX, dY);
                
                // 羽化边缘光
                float rim = smoothstep(
                    _Threshold - _Softness,
                    _Threshold + _Softness,
                    edgeStrength
                );
                
                // 获取原画面颜色
                half4 sceneColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
                
                // 混合边缘光
                half3 finalColor = sceneColor.rgb + _RimColor.rgb * rim * _Intensity;
                
                return half4(finalColor, sceneColor.a);
            }
            ENDHLSL
        }
    }
}

这个后处理方案的原理是:通过深度缓冲的梯度变化来检测边缘——深度突然变化的地方,就是物体的轮廓边缘。

场景四:透明物体的边缘辉光

透明物体(玻璃、水、魔法屏障)的边缘光效果最为明显。因为透明物体本身透光,边缘处光线散射,自然形成一圈光晕。

实现技巧:

  • 双面渲染(Two-Sided Rendering)
  • 开启Alpha To Coverage
  • 边缘光强度随透明度增加
// 透明物体边缘光Shader关键片段
// 开启双面渲染
Tags { "RenderType"="Transparent" "Queue"="Transparent" "ForceNoShadowCasting"="True" }
Cull Off  // 关闭剔除,双面渲染

// 在Fragment中
// 计算双面法线
float3 worldNormal = normalize(i.worldNormal);
float NdotV = dot(worldNormal, viewDir);

// 双面检测:法线指向相机内用-abs,指向外用+
float rimRaw = 1.0 - abs(NdotV);
float rim = smoothstep(threshold - softness, threshold + softness, rimRaw);

// 透明物体边缘光通常要强一些
float alpha = max(texColor.a, rim * intensity);

实用技巧和避坑指南

技巧一:边缘光颜色要配合场景色调

不要随便选一个亮蓝色或亮绿色就往场景里加。边缘光的颜色应该:

  • 暖色场景(日落、篝火)→ 边缘光偏暖橙、琥珀色
  • 冷色场景(水下、夜空)→ 边缘光偏蓝紫、青绿
  • 科技感场景 → 边缘光偏青白、电光蓝

一个简单的方法:从场景的主光源颜色里提取一点作为边缘光颜色,这样视觉上更协调。

技巧二:羽化程度的控制

边缘光的”羽化”不是越软越好。有几个判断标准:

  • 近景特写:羽化可以大一些,柔和的边缘光显得更精致
  • 远景/小角色:羽化要小,边缘光太软会看不清轮廓
  • 快速运动场景:羽化适中,太软会在运动模糊时显得糊成一片

经验值参考:

  • 手机游戏中的边缘光,Softness通常在0.1~0.3之间
  • 主机/PC游戏,可以用0.2~0.5
  • 影视级效果,可以达到0.5以上

技巧三:性能优化

边缘光如果做在实时渲染里,性能消耗有几个来源:

优化方式 效果
使用Screen Space方案 避免逐面计算法线,但需要深度缓冲
背面剔除变体 只渲染背面,开销小,但需要额外Pass
预烘焙边缘光贴图 离线烘焙,运行时零开销,适合静态场景
LOD控制 远处物体降低边缘光精度或关闭

最省性能的方案:用Unity的SRP Batcher + Instanced Stereoscopic Rendering,配合一个简单的高程采样边缘光。

技巧四:影视后期里的常见问题

做影视边缘光时,最容易出问题的地方:

  1. 边缘光太硬,有明显的切割线

    • 解决:加大Blur半径,或者用Gaussian而不是Box Blur
  2. 边缘光颜色不统一

    • 解决:用Color Correction节点统一边缘光的色温,再叠加
  3. 暗部边缘光被吃掉了

    • 解决:先提升暗部亮度(Levels/ Curves),再做边缘提取
  4. 透明物体边缘光溢出

    • 解决:用Matte Control限制边缘光的范围,不要让它渗透到物体内部

技巧五:和辉光(Bloom)的配合

羽化光和辉光是两个不同的效果,但经常一起用:

  • 羽化光:精确控制边缘位置和形状
  • 辉光:让边缘光”溢出”到周围,产生光晕感

正确的使用顺序是:先加羽化边缘光,再加Bloom。如果反过来,Bloom会让边缘光模糊掉,失去轮廓感。

在Unity HDRP里,可以这样配置:

1. 材质Shader里加边缘光(Rim Light)
2. Post-process Volume里加Bloom
3. Bloom的Threshold设高一些,只让边缘光区域触发辉光

总结

羽化光渲染技术听起来高大上,但核心就三件事:检测边缘、柔化过渡、叠加颜色。不管是游戏实时渲染还是影视后期合成,思路都是一致的。

游戏里你可以选择Shader Graph可视化、HLSL手写、或者后处理方案,各有优劣。影视后期里After Effects和Nuke都能做,关键是要理解边缘提取的逻辑。

最重要的是:不要滥用。边缘光加一点能让画面更精致,加多了就会显得廉价和嘈杂。一个好的边缘光效果应该是”你能感觉到它存在,但不会注意到它”。

如果你在实战中遇到问题,最常见的坑就是边缘光太硬或者颜色不协调——这两个问题,一个调Softness参数,一个调颜色匹配,基本就能解决。祝你在羽化光的路上越走越顺!