羽化光渲染技术怎么用 从游戏到影视后期边缘渐变光效教程 常见应用场景与实用技巧全解析
羽化光,或者说边缘渐变光效,最近在独立游戏圈和短视频特效里火得一塌糊涂。你有没有注意过,有些角色的轮廓边缘会有一层柔和的光晕,像被晨雾轻轻笼罩着?那种”仙气”或者”科技感”,很多都是靠羽化光渲染出来的。今天咱们就掰开揉碎了聊聊这个技术。
羽化光到底是什么
羽化光本质上是边缘光(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里对应的是:
- 把 View World Position 减去 Object World Position,得到从物体指向相机的向量
- 对这个向量做 Normalize
- 和 Object World Normals 做 Dot Product
- 用一个 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,创建一个新的预合成。
第二步:提取边缘 在复制出来的上层应用以下效果(按顺序):
Effects → Generate → CC Lightleak
- 把Blend Mode设为Screen
- 调整Lightleak Color让边缘光呈现你想要的色调
- 这个方法适合做出彩色渐变边缘光
更精准的方式:用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,配合一个简单的高程采样边缘光。
技巧四:影视后期里的常见问题
做影视边缘光时,最容易出问题的地方:
边缘光太硬,有明显的切割线
- 解决:加大Blur半径,或者用Gaussian而不是Box Blur
边缘光颜色不统一
- 解决:用Color Correction节点统一边缘光的色温,再叠加
暗部边缘光被吃掉了
- 解决:先提升暗部亮度(Levels/ Curves),再做边缘提取
透明物体边缘光溢出
- 解决:用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参数,一个调颜色匹配,基本就能解决。祝你在羽化光的路上越走越顺!
