Lazy loaded image
Unity Shader
从零实现 Unity 边缘外发光:Rim Light + Bloom 原理与完整实现
字数 1858阅读时长 5 分钟
2026-1-30
2026-4-16
type
status
date
slug
summary
tags
category
icon
password

一、目标是什么

做游戏时经常看到一种效果:角色或物体的边缘有一圈发光,像是被光晕包裹(比如网易永劫无间的霸体人物边缘发光效果)。这种效果在技术上叫 Rim Light(边缘光),也常被称为菲涅尔效果(Fresnel Effect)。
这篇文章记录从零写出这个效果的完整过程,包括原理推导、代码实现、以及用 Post-Processing 让效果更好看的步骤。

二、原理:为什么边缘会发光

自然界里有一个现象:当你看向一面玻璃的正面时,玻璃很透明;但从很斜的角度看时,玻璃会变得很反光。这就是菲涅尔现象。
Rim Light 借用了同样的思路:视线方向和物体表面越垂直(正对着你),亮度越低;越平行(边缘处),亮度越高。
计算方法:
rim = 1 - dot(法线方向, 视线方向)
  • dot 是点积,结果是两个向量的夹角余弦
  • 正对摄像机的面:法线和视线方向几乎平行,点积接近 1,1 - 1 = 0,不发光
  • 边缘的面:法线和视线方向接近垂直,点积接近 0,1 - 0 = 1,完全发光
再用 pow 控制衰减速度:pow(rim, _RimPower) ,RimPower 越大,发光区域越窄越锐利。

三、第一步:搭建 Shader 基础结构

在 Unity 的 Built-in 渲染管线里,Shader 分三层:
  • Properties:暴露给 Inspector 的可调参数
  • SubShader / Pass:实际的渲染逻辑
  • CGPROGRAM:顶点着色器和片段着色器的代码区
先写好框架,把需要的参数暴露出来:
Shader "Custom/BuiltInRimLight" { Properties { _MainTex ("主纹理", 2D) = "white" {} _RimColor ("边缘光颜色", Color) = (0.5, 0.5, 1, 1) _RimPower ("边缘强度", Range(0, 8)) = 2.0 } ... }
_RimPower 范围设 0~8:太小边缘光会漫到整个模型,太大只有极细一条线。

四、第二步:顶点着色器——把数据传到片段着色器

顶点着色器的任务是把每个顶点从模型空间变换到正确的坐标系,并把需要的数据打包传给片段着色器(v2f 结构体)。
边缘光需要两个关键数据:世界法线视线方向,必须在同一个坐标系(世界空间)下计算。
v2f vert(appdata v) { v2f o; // 顶点位置:模型空间 → 裁剪空间(GPU 最终用这个绘制到屏幕) o.pos = UnityObjectToClipPos(v.vertex); // 法线:模型空间 → 世界空间(注意不能直接用矩阵乘,要用专用函数) o.worldNormal = UnityObjectToWorldNormal(v.normal); // 视线方向:从顶点指向摄像机,Unity 内置函数自动计算 o.viewDir = WorldSpaceViewDir(v.vertex); // UV 传递(处理 Tiling/Offset) o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; }
为什么法线不能直接乘变换矩阵?因为模型做了非等比缩放时,用位置变换矩阵会让法线方向出错。UnityObjectToWorldNormal 内部用了转置逆矩阵,专门解决这个问题。

五、第三步:片段着色器——计算 Rim 值

fixed4 frag(v2f i) : SV_Target { fixed4 baseColor = tex2D(_MainTex, i.uv); float rim = pow(1 - saturate(dot(normalize(i.worldNormal), normalize(i.viewDir))), _RimPower); return baseColor + rim * _RimColor; }
saturate 把点积结果夹在 [0, 1] 之间,防止出现负数导致颜色异常。normalize 确保向量是单位长度,否则点积不代表角度关系。
这一步已经能看到边缘光效果了,但边缘比较生硬。

六、第四步:加入柔化层,模拟外发光扩散

真实的发光效果不只是一条锐利的线,还有向外扩散的柔光晕。用两层叠加来模拟:
float rimSharp = pow(rim, _RimPower); // 锐利内层 float rimSoft = pow(rim, _RimPower * 0.25); // 柔化外层,指数更小 → 覆盖范围更宽 return baseColor + rimSharp * _RimColor + rimSoft * _RimColor * 0.2;
_RimPower * 0.25 让指数更小,pow 曲线更平缓,发光范围更宽。* 0.2 让外层强度只有内层的五分之一,形成自然的扩散晕染感。
两层叠加后,效果变成:核心处有一条亮线,外侧有柔和光晕,视觉上像是真的在发光。

七、第五步:加 Post-Processing Bloom,让光晕更真实

Rim Light 本质是在模型材质上加颜色,但颜色再亮也只是"亮色",不会真的像光一样往外扩散。要做到"往外溢出"的感觉,需要后处理。
配置步骤:
  1. Package Manager 安装 Post Processing 包
  1. 在场景的 Main Camera 上加 Post-process Layer 组件,Layer 选 PostProcessing
  1. 新建一个空物体,Layer 设为 PostProcessing,加 Post-process Volume 组件,勾选 Is Global
  1. 在 Volume 里新建 Profile,添加 Bloom 效果
Bloom 参数配置:
参数
说明
Intensity
6
发光强度,值越大扩散越明显
Threshold
默认
超过此亮度的像素才触发 Bloom
Bloom 的原理:把画面中超过阈值的高亮像素做模糊处理,再叠回原画面,产生光溢出的感觉。配合 Rim Light 的高亮边缘,效果会非常自然。

八、总结:学到了什么

核心原理: 法线和视线的点积是边缘光的数学基础,本质是判断"这个面有多斜对着摄像机"。这个思路可以扩展到几乎所有方向相关的视觉效果。
两个坐标系的重要性: 法线、视线、光线的计算必须在同一个坐标系下进行,不然方向对不上。世界空间是最常用的统一坐标系。
分层叠加的思路: 一个效果不够自然时,可以用多层叠加——主层精确,辅助层柔化——而不是去改主层的公式。这个思路在很多 Shader 里都适用。
材质 + 后处理的配合: Shader 负责"像素该是什么颜色",Post-Processing 负责"整个画面该怎么处理"。两者配合才能做出最完整的视觉效果。

九、发散:还能延伸到哪里

方向1 - 让发光颜色动起来_Time 控制颜色随时间变化,或者接入脚本动态修改 _RimColor,做出能量护盾充能、受伤闪光等效果。
方向2 - 基于深度的外描边 Rim Light 是"从材质内部算边缘",还有另一种思路是"用两个 Pass,第二个 Pass 把模型放大一点,只画背面",可以做出更均匀的卡通描边效果。
方向3 - Bloom 阈值联动 配合 HDR 颜色,让 Rim 颜色的亮度超过 1(比如 (2, 2, 4, 1)),Bloom 的 Threshold 卡在 1 附近,只让边缘光触发 Bloom,其他区域不受影响,效果更精准。
方向4 - URP 迁移 Built-in 管线的 CGPROGRAM 在 URP 里要改成 HLSLPROGRAMUnityCG.cginc 换成 Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl,思路完全一样,只是 API 名称不同。
上一篇
UnityShader-迷雾效果
下一篇
unity/git-compare

评论
Loading...
点击和我聊天吧~