我們都知道描邊效果在遊戲中很常見,比如選中某個角色時需要凸顯該模型,就會採用描邊效果,今天我們就來實現一下該效果。
描邊的效果實現方式有很多種,就以目前我知道的就有三種方式。
一:模型擴張
效果圖:
大致思路:需要兩個pass,一個pass渲染背面並且沿着法線方向擴張,用來作爲輪廓,一個pass渲染正面,正常渲染。
核心:主要在第一個pass的頂點着色器中對頂點的偏移,偏移方向爲法線方向。
話不多說直接上代碼,具體可看代碼,有詳細註釋。
//--------------------------- 【描邊】 - 法線擴張---------------------
//create by 長生但酒狂
Shader "lcl/shader3D/outLine3D_swell"
{
//---------------------------【屬性】---------------------------
Properties
{
// 主紋理
_MainTex ("Texture", 2D) = "white" {}
// 主顏色
_Color("Color",Color)=(1,1,1,1)
// 描邊強度
_power("power",Range(0,0.2)) = 0.05
// 描邊顏色
_lineColor("lineColor",Color)=(1,1,1,1)
}
// ------------------------【CG代碼】---------------------------
CGINCLUDE
#include "UnityCG.cginc"
#include "Lighting.cginc"
//頂點着色器輸入結構體
struct appdata
{
float4 vertex : POSITION;//頂點座標
float2 uv : TEXCOORD0;//紋理座標
float3 normal:NORMAL;//法線
};
//頂點着色器輸出結構體
struct v2f
{
float4 vertex : SV_POSITION;//像素座標
float2 uv : TEXCOORD0;//紋理座標
float3 worldNormalDir:COLOR0;//世界空間裏的法線方向
float3 worldPos:COLOR1;//世界空間裏的座標
};
// ------------------------【變量聲明】---------------------------
//紋理
sampler2D _MainTex;
//內置的變量,紋理中的單像素尺寸
float4 _MainTex_TexelSize;
//主顏色
float4 _Color;
//描邊強度
float _power;
//描邊顏色
float4 _lineColor;
// ------------------------【背面-頂點着色器】---------------------------
v2f vert_back (appdata v)
{
v2f o;
//法線方向
v.normal = normalize(v.normal);
//頂點沿着法線方向擴張
v.vertex.xyz += v.normal * _power;
//由模型空間座標系轉換到裁剪空間
o.vertex = UnityObjectToClipPos(v.vertex);
//輸出結果
return o;
}
// ------------------------【背面-片元着色器】---------------------------
fixed4 frag_back (v2f i) : SV_Target
{
//直接輸出顏色
return _lineColor;
}
// ------------------------【正面-頂點着色器】---------------------------
v2f vert_front (appdata v)
{
//正常渲染
v2f o;
o.uv = v.uv;
//法線從模型空間座標系轉換到世界座標系
o.worldNormalDir = mul(v.normal,(float3x3) unity_WorldToObject);
//頂點從模型空間座標系轉換到世界座標系
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
//歸一化
v.normal = normalize(v.normal);
//由模型空間座標系轉換到裁剪空間
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
// ------------------------【正面-片元着色器】---------------------------
fixed4 frag_front (v2f i) : SV_Target
{
//正常渲染
//紋理顏色值
fixed4 col = tex2D(_MainTex, i.uv);
//環境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Color.xyz;
//視角方向
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
//法線方向
float3 normaleDir = normalize(i.worldNormalDir);
//光照方向歸一化
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//半蘭伯特模型
fixed3 lambert = 0.5 * dot(normaleDir, worldLightDir) + 0.5;
//漫反射
fixed3 diffuse = lambert * _Color.xyz * _LightColor0.xyz + ambient;
//最終結果
fixed3 result = diffuse * col.xyz;
return float4(result,1);
}
ENDCG
// ------------------------【子着色器】---------------------------
SubShader
{
//透明度混合模式
Blend SrcAlpha OneMinusSrcAlpha
//渲染隊列
Tags{ "Queue" = "Transparent"}
// ------------------------【背面通道】---------------------------
Pass
{
//剔除正面
Cull Front
//防止背面模型穿透正面模型
//關閉深度寫入,爲了讓正面的pass完全覆蓋背面,同時要把渲染隊列改成Transparent,此時物體渲染順序是從後到前的
ZWrite Off
CGPROGRAM
#pragma vertex vert_back
#pragma fragment frag_back
ENDCG
}
// ------------------------【正面通道】---------------------------
Pass
{
//剔除背面
Cull Back
CGPROGRAM
#pragma vertex vert_front
#pragma fragment frag_front
ENDCG
}
}
}
二:基於法線與視角方向的夾角
大致思路:計算法線與視角方向的夾角,夾角越大,越接近邊緣。
效果圖如下:
shader代碼:
//--------------------------- 【描邊】 - 基於法線與視角夾角---------------------
//create by 長生但酒狂
Shader "lcl/shader3D/outline3D"
{
//---------------------------【屬性】---------------------------
Properties
{
// 主紋理
_MainTex ("Texture", 2D) = "white" {}
// 主顏色
_Color("Color",Color)=(1,1,1,1)
// 描邊強度
_power("lineWidth",Range(0,10)) = 1
// 描邊顏色
_lineColor("lineColor",Color)=(1,1,1,1)
}
// ------------------------【子着色器】---------------------------
SubShader
{
//渲染隊列
Tags{
"Queue" = "Transparent"
}
Blend SrcAlpha OneMinusSrcAlpha
// 通道
Pass
{
// ------------------------【CG代碼】---------------------------
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
//頂點着色器輸入結構體
struct appdata
{
float4 vertex : POSITION;//頂點座標
float2 uv : TEXCOORD0;//紋理座標
float3 normal:NORMAL;//法線
};
//頂點着色器輸出結構體
struct v2f
{
float4 vertex : SV_POSITION;//像素座標
float2 uv : TEXCOORD0;//紋理座標
float3 worldNormalDir:COLOR0;//世界空間裏的法線方向
float3 worldPos:COLOR1;//世界空間裏的座標
};
// ------------------------【頂點着色器】---------------------------
v2f vert (appdata v)
{
v2f o;
o.uv = v.uv;
o.worldNormalDir = mul(v.normal,(float3x3) unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
// ------------------------【變量聲明】---------------------------
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float4 _Color;
float _power;
float4 _lineColor;
// ------------------------【片元着色器】---------------------------
fixed4 frag (v2f i) : SV_Target
{
//紋理顏色
fixed4 col = tex2D(_MainTex, i.uv);
//環境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Color.xyz;
//視角方向
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
//法線方向
float3 normaleDir = normalize(i.worldNormalDir);
//光照方向歸一化
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//半蘭伯特模型
fixed3 lambert = 0.5 * dot(normaleDir, worldLightDir) + 0.5;
//漫反射
fixed3 diffuse = lambert * _Color.xyz * _LightColor0.xyz + ambient;
fixed3 result = diffuse * col.xyz;
//計算視角方向與法線的夾角(夾角越大,value值越小,越接近邊緣)
float value = dot(viewDir,normaleDir);
//
value = 1 - saturate(value);
//通過_power調節描邊強度
value = pow(value,_power);
//源顏色值和描邊顏色做插值
result =lerp(result,_lineColor,value) ;
return float4(result,1);
}
ENDCG
}
}
}
還有第三種方法是基於圖像實現的,比上面的兩種方式稍微複雜一點,這裏暫時就不詳說了,後面再單獨出一篇文章來詳解。