前言:本节介绍边缘光(Rim Light)的原理与实现
基础Rim Light
1.核心原理
Rim Light 利用视线方向(V)与表面法线(N)的夹角来计算边缘亮度,当视线方向与表面法线接近垂直时(即物体的轮廓边缘位置),点积结果趋近于0,通过:
1 - saturate(dot(N, V))
计算得到的边缘光强度会达到最大值,从而在物体边缘生成高亮效果。
这种效果模拟了现实中光线从物体背面或侧面穿透、勾勒轮廓的视觉表现,常用于卡通风格、次表面散射材质的渲染。
2.代码实现
Shader "MyCustom/BasicRimLight" { Properties { // 边缘光颜色 _RimLightColor ("_RimLightColor", Color) = (1, 1, 1, 1) // _RimLightPower ("_RimLightPower", Range(0, 10)) = 1 _RimLightIntensity ("_RimLightIntensity", Range(0, 10)) = 1 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" float4 _RimLightColor; float _RimLightPower; float _RimLightIntensity; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldView : TEXCOORD1; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); // 法线方向 o.worldNormal = UnityObjectToWorldNormal(v.normal); float4 worldPos = mul(unity_ObjectToWorld, v.vertex); // 视线方向 o.worldView = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz); return o; } // 边缘光公式 float3 funcFresnel(float3 worldNormal, float3 worldView) { float nv = max(0, dot(worldNormal, worldView)); float3 fresnel = pow(1 - nv, _RimLightPower) * _RimLightIntensity * _RimLightColor.rgb; return fresnel; } fixed4 frag (v2f i) : SV_Target { float3 fresnel = funcFresnel(i.worldNormal, i.worldView); return float4(fresnel, 1); } ENDCG } } FallBack "Diffuse" }Fake Rim Light
1.核心原理
Fake Rim Light(假边缘光) 是一种更轻量、更具风格化的实现方案。它不依赖复杂的法线与视线夹角计算,而是通过贴图来模拟轮廓高亮。Fake Rim Light 的核心在于“不求物理正确,但求视觉有效”。
2.代码实现
Shader "MyCustom/FakeRimLight" { Properties { // 边缘光贴图 _FakeRimLightTex ("_FakeRimLightTex", 2D) = "white" {} _RimLightColor ("_RimLightColor", Color) = (1, 1, 1, 1) _RimLightPower ("_RimLightPower", Range(0, 10)) = 1 _RimLightIntensity ("_RimLightIntensity", Range(0, 10)) = 1 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" float4 _RimLightColor; float _RimLightPower; float _RimLightIntensity; sampler2D _FakeRimLightTex; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldView : TEXCOORD2; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.worldNormal = UnityObjectToWorldNormal(v.normal); float4 worldPos = mul(unity_ObjectToWorld, v.vertex); o.worldView = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz); return o; } float3 funcFresnel(float3 worldNormal, float3 worldView) { float nv = max(0, dot(worldNormal, worldView)); float3 fresnel = pow(1 - nv, _RimLightPower) * _RimLightIntensity * _RimLightColor.rgb; return fresnel; } fixed4 frag (v2f i) : SV_Target { float3 fresnel = funcFresnel(i.worldNormal, i.worldView); // 没有使用标准的 UV,而是利用世界空间法线的 X 分量来采样纹理 // 无论模型如何旋转,纹理都会根据法线朝向“投影”到模型表面。例如,朝左的面会采样纹理左侧的颜色,朝右的面采样右侧。 float u = i.worldNormal.x * 0.5 + 0.5; float3 fakeRimLight = tex2D(_FakeRimLightTex, float2(u, i.uv.y)).rgb; float3 finalColor = fakeRimLight * fresnel; return float4(finalColor, 1); } ENDCG } } FallBack "Diffuse" }基于FresnelSchlicks的可变换范围的Rim Light
代码实现:
Shader "MyCustom/SchlickFresnel" { Properties { _RimLightColor0 ("_RimLightColor0", Color) = (1, 0, 0, 1) _RimLightColor1 ("_RimLightColor1", Color) = (0, 0, 1, 1) _RimLightPower ("_RimLightPower", Range(0, 10)) = 1 _RimLightIntensity ("_RimLightIntensity", Range(0, 10)) = 1 _SchlickFresnelBias ("_SchlickFresnelBias", Range(0, 2)) = 0.6 _SchlickFresnelEta ("_SchlickFresnelEta", Range(0, 10)) = 0.4 _AngleMin ("_AngleMin", Range(0, 360)) = 0 _AngleMax ("_AngleMax", Range(0, 360)) = 45 _Inverse ("_Inverse", Range(-1, 1)) = 1 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" #include "MyCustomHeader.cginc" #define PI 3.14159265359 float4 _RimLightColor0; float4 _RimLightColor1; float _RimLightPower; float _RimLightIntensity; float _SchlickFresnelBias; float _SchlickFresnelEta; float _AngleMin; float _AngleMax; float _Inverse; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 viewNormal : TEXCOORD1; float3 worldNormal : TEXCOORD2; float3 worldView : TEXCOORD3; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.viewNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal)); o.worldNormal = UnityObjectToWorldNormal(v.normal); float4 worldPos = mul(unity_ObjectToWorld, v.vertex); o.worldView = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz); return o; } // 边缘光公式 float3 funcFresnel(float3 worldNormal, float3 worldView) { float nv = max(0, dot(worldNormal, worldView)); float3 fresnel = pow(1 - nv, _RimLightPower) * _RimLightIntensity * _RimLightColor0.rgb; return fresnel; } // 将value在input范围中对应到output中,输出v float funcSetRange(float2 input, float2 output, float value) { float v; Unity_Remap_float(value, input, output, v); return v; } // 非线性调节菲涅尔曲线形状,改变高光衰减速度 float funcBias(float bias, float t) { return pow(t, -log2(bias)); } // 法线转极坐标,将 (x,y)转换为 (θ,r)用于角度判断 float2 polarCoordinates(float2 uv) { float2 output; Unity_PolarCoordinates_float(uv, 0, 1, 1, output); return output; } // 平滑的角度区间遮罩,在 _AngleMin 和 _AngleMax 之间生成 0~1 的平滑过渡值 float customLerp(float a, float b, float t) { return (step(a, t) - step(b, t)) * (smoothstep(a, a + (b - a) / 2, t) * (1 - smoothstep(a + (b - a) / 2, b, t))); } // 实现SchlickFresnel float3 funcSchlickFresnel(float bias, float eta, float intensity, float nv) { // 插值输出,填的是一些经验参数 float a = funcSetRange(float2(0, 1), float2(0, 0.5), bias); float f0 = funcSetRange(float2(0, 1), float2(0, 0.2), eta); //经验参数公式: float fresnelFactor = _FresnelRatio + (1 - _FresnelRatio) * pow(1 - dot(viewDir, i.worldNormal), 5); float fresnel = f0 + (1 - f0) * pow(1 - nv, 5); // 应用非线性调节参数 float SchlickFresnel = funcBias(a, fresnel) * intensity; return SchlickFresnel; } float3 schlickFresnel(float3 worldNormal, float3 worldView) { float nv = max(0, dot(worldNormal, worldView)); float3 fresnel = funcSchlickFresnel(_SchlickFresnelBias, _SchlickFresnelEta, _RimLightIntensity, nv); return fresnel; } float3 schlickFresnelSegment(float3 viewNormal, float3 worldNormal, float3 worldView, float inverse) { // 计算fresnel的显示区域 float2 uv = float2(viewNormal.x * inverse, viewNormal.y); float2 p = polarCoordinates(uv); // 角度插值输出 float angleMin = funcSetRange(float2(0, 360), float2(0, 1), _AngleMin); float angleMax = funcSetRange(float2(0, 360), float2(0, 1), _AngleMax); float t = clamp(p.y, 0, 1); float v = customLerp(angleMin, angleMax, t); // 计算fresnel float3 fresnel = schlickFresnel(worldNormal, worldView); // 计算区域fresnel float3 fresnelSegment = fresnel * v * pow(p.x, 2); return fresnelSegment; } // 根据附加两种边缘颜色 float3 schlickFresnelSegmentColor(float3 viewNormal, float3 worldNormal, float3 worldView) { float3 color0 = schlickFresnelSegment(viewNormal, worldNormal, worldView, _Inverse) * _RimLightColor0.rgb; float3 color1 = schlickFresnelSegment(viewNormal, worldNormal, worldView, -_Inverse) * _RimLightColor1.rgb; return color0 + color1; } fixed4 frag (v2f i) : SV_Target { float3 fresnel = schlickFresnelSegmentColor(i.viewNormal, i.worldNormal, i.worldView); return float4(fresnel, 1); } ENDCG } } FallBack "Diffuse" }