前言本节介绍边缘光Rim Light的原理与实现基础Rim Light1.核心原理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 { RenderTypeOpaque } 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 Light1.核心原理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 { RenderTypeOpaque } 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 { RenderTypeOpaque } 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 }