我们都知道描边效果在游戏中很常见,比如选中某个角色时需要凸显该模型,就会采用描边效果,今天我们就来实现一下该效果。
描边的效果实现方式有很多种,就以目前我知道的就有三种方式。
一:模型扩张
效果图:
大致思路:需要两个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
}
}
}
还有第三种方法是基于图像实现的,比上面的两种方式稍微复杂一点,这里暂时就不详说了,后面再单独出一篇文章来详解。