关于我这几天去哪了,在写一个水面的shader,再加上最近公司事情多,就耽搁了。在网上分析了好几种水面的shader,最后发现大体上就两种,下面就来分析这两种shader。
这类型的shader包括两部分:
1、需要被倒影的物体本身,我命名为MirrorShader
2、被投射的水面shader,我命名为waterShader
Shader "ShaderPath/MirrorShader"//shader的选择路径
{
Properties//该Shader可控的属性
{
_PlaneNormal("地面旋转角度",Vector) = (0,0,0,0) //地面法线
_PlanePosition("地面位置",Vector) = (0,0,0,0) //地面位置
_GradientTex("GradientTex", 2D) = "white" {}//渐变贴图
_DiffuseColor("DiffuseColor",Color) = (1,1,1,1)//漫反射的主色调
_SpecularColor("SpecularColor",Color) = (1,1,1,1)//高光反射的主色调
_Gloss("Gloss",Range(1,100)) = 2 //光泽度(反光度) 控制高光区域的大小
_MirrorRange("MirrorRange", Range(0, 1)) = 1 // 镜面范围(最大范围,超出该范围就不反射)
_MirrorAlpha("MirrorAlpha", Range(0, 1)) = 1 // 镜面图像不透明度
_MirrorFadeAlpha("_MirrorFadeAlpha", Range(0,1)) = 0.5 // 镜面范围值边缘位置的不透明度,如果调整为0,意思越接近该最大范围的透明就越接近该值:0
}
//先用CGINCLUDE ENDCG包裹起来片元顶点着色器,后面看比较直观
CGINCLUDE
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
//镜像片元结构体
struct v2f_m {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 normal : TEXCOORD1;
float4 wPos : TEXCOORD2;
float3 lightDir : TEXCOORD3;
float3 viewDir : TEXCOORD4;
};
//主体片元结构体
struct v2f
{
float3 lightDir : TEXCOORD0;
float3 viewDir : TEXCOORD1;
float3 normal : TEXCOORD2;
float2 uv : TEXCOORD3;//用于存储纹理信息
float4 pos : SV_POSITION;//每个片元结构体必须有的
};
float4 _PlaneNormal;
float4 _PlanePosition;
sampler2D _GradientTex;
float4 _GradientTex_ST;//图片的(平铺和偏移系数)如果要使图片的Tilling和Offset生效就必须定义
fixed4 _DiffuseColor;
fixed4 _SpecularColor;
float _Gloss;
float _MirrorRange;
float _MirrorAlpha;
float _MirrorFadeAlpha;
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//把顶点从模型空间转换到剪裁空间
o.uv = TRANSFORM_TEX(v.uv, _GradientTex);//使图片对应的_ST生效,这里就是_GradientTex_ST
//要区分UnityWorldSpaceLightDir()传入一个float3的世界坐标 和 WorldSpaceLightDir()传入一个float4的模型的顶点坐标
//UnityWorldSpaceViewDir()传入一个float3的世界坐标 WorldSpaceViewDir()传入一个float4的模型的顶点坐标
o.lightDir = WorldSpaceLightDir(v.vertex);//获取世界坐标下的光线方向
o.viewDir = WorldSpaceViewDir(v.vertex);//获取世界坐标下的观察方向
o.normal = normalize(UnityObjectToWorldNormal(v.normal));//将模型的法线转到世界坐标
return o;
}
fixed4 frag(v2f i) : SV_Target//返回一个RGBA到模型上
{
fixed3 lightDir = normalize(i.lightDir);//归一化
fixed3 viewDir = normalize(i.viewDir);//归一化
//半罗伯特反射
fixed halfLambert = (1 + dot(lightDir,i.normal)) / 2;
fixed3 gradient = tex2D(_GradientTex,fixed2(halfLambert + i.uv.x,0)).rgb * _DiffuseColor;//采样_GradientTex里的颜色信息
fixed3 diffuse = _LightColor0 * gradient;
//Blinn-Phong模型高光
fixed3 halfView = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _SpecularColor * pow(saturate(dot(i.normal,halfView)),_Gloss);
return fixed4(diffuse + specular,1);
}
//镜像定点着色器
v2f_m Mirrorvert(appdata v)
{
v2f_m o;
o.wPos = mul(unity_ObjectToWorld, v.vertex);
float3 nn = -_PlaneNormal.xyz; // 地面法线反向,镜像的物体要用
float3 dp = o.wPos.xyz - _PlanePosition.xyz; // 平面点与世界空间的点的向量(即:从平面的点指向世界空间点的方向)
half nd = dot(_PlaneNormal.xyz, dp); // 计算出点与平面的垂直距离
o.wPos.xyz += nn * (nd * 2); // 将垂直距离反向2倍的距离,就是镜像的位置
o.pos = mul(unity_MatrixVP, o.wPos); //计算模型当前视图的投影点,更多表查看下面的“UnityShader部分常用内置矩阵表”
o.normal.xyz = UnityObjectToWorldNormal(v.normal);
fixed t = nd / _MirrorRange; // 将位置与镜面最大范围比利作为fade alpha的插值系数
fixed a = lerp(_MirrorAlpha, _MirrorAlpha * _MirrorFadeAlpha, t);
o.normal.w = a; // 透明度我们存于o.normal.w
o.wPos.w = nd; // 距离存于o.wPos.w
o.uv = v.uv;
o.lightDir = WorldSpaceLightDir(v.vertex);//获取世界坐标下的光线方向
o.viewDir = WorldSpaceViewDir(v.vertex);//获取世界坐标下的观察方向
return o;
}
fixed4 Mirrorfrag(v2f_m i) : SV_Target//返回一个RGBA到模型上
{
if (i.wPos.w > _MirrorRange) discard; // 超过镜像范围也丢弃
if (i.normal.w <= 0) discard; // 透明度为0丢弃
float3 dir = i.wPos.xyz - _PlaneNormal.xyz; // 平面与插值点的指向
half d = dot(dir, _PlaneNormal.xyz); // 与反向镜面的距离
if (d > 0) discard; // 如果超过了平面,那就丢弃
fixed3 lightDir = normalize(i.lightDir);//归一化
fixed3 viewDir = normalize(i.viewDir);//归一化
//半罗伯特反射
fixed halfLambert = (1 + dot(lightDir,i.normal)) / 2;
//fixed2(halfLambert+i.uv.x,0) x的值必须如上,具体解释看下面,y的值任意,因为图片的颜色值不随y的变化而变化
fixed3 gradient = tex2D(_GradientTex,fixed2(halfLambert + i.uv.x,0)).rgb * _DiffuseColor;//采样_GradientTex里的颜色信息
fixed3 diffuse = _LightColor0 * gradient;
fixed3 halfView = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _SpecularColor * pow(saturate(dot(i.normal,halfView)),_Gloss);
return fixed4(diffuse + specular , i.normal.w);
}
ENDCG
SubShader//子着色器
{
Pass{
Tags {"LightMode" = "ForwardBase"} //定义该Pass在Unity光照流水线中的角色
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
Pass
{
Tags { "Queue" = "Geometry" "RenderType" = "Opaque" }
Cull Front //镜像里正反颠倒,此时应该剪裁掉正面
ZTest Always //这里必须保持深度测试通过,否则会被水面挡住
Blend SrcAlpha OneMinusSrcAlpha //渲染的叠加方式
Stencil {
Ref 1
Comp Equal
}
Tags {"LightMode" = "ForwardBase"}
//与ENDCG相照应,将CG代码包裹
CGPROGRAM
//顶点函数定义
#pragma vertex Mirrorvert
//片元函数定义
#pragma fragment Mirrorfrag
ENDCG
}
}
}
// jave.lin 2019.08.15
Shader "ShaderPath/WaterShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "Queue"="Geometry" "RenderType"="Opaque" }
ZWrite On
Stencil {
Ref 1
Comp Always
Pass Replace
}
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv);
return col * _Color;
}
ENDCG
}
}
}
效果如下如
UnityShader部分常用内置矩阵表
核心代码详解
1、下面这段代码是镜像呈现的核心代码
float3 nn = -normal; // 法线反向
float3 dp = o.wPos.xyz - _PlanePosition.xyz; // 平面点与世界空间的点的向量(即:从平面的点指向世界空间点的方向)
half nd = dot(normal, dp); // 计算出点与平面的垂直距离
o.wPos.xyz += nn * (nd * 2); // 将垂直距离反向2倍的距离,就是镜像的位置
如下图所示,
平面过点O的法线为normal,
P为模型上的点,P’为镜像点,
dp为向量OP,
|nd| = normal·dp = |normal|·|dp| cosθ = |dp|cosθ
PS:normal为单位向量,θ为normal、dp的夹角
P’ = -normal * (2 * nd )
2、关于模板测试
在MirrorShader和WaterShader里面都有关于模板测试的东西
MirrorShader中
Stencil
{
Ref 1
Comp Equal
}
WaterShader中
Stencil
{
Ref 1
Comp Always
Pass Replace
}
上面两个模板合起来解释就是:
1、在物体上渲染出来的投影设置模板参照值(Ref)为1,并且只要模板参考值相同的就通过模板测试;
2、水面的模板参照值为1,模板测试一律通过,并且只要通过了模板测试和深度测试的像素就直接显示出来
3、总结就是物体倒影和水面的模板参照值相同,并且物体倒影的深度测试也通过,所以物体的倒影能在水面显示
关于模板测试更多的知识查看该链接