《入门精要》中模拟玻璃是用了Unity里的一个特殊的Pass来实现的,这个Pass就是GrabPass,比起上一篇博客实现镜子的方法,这个方法我认为相对复杂,因此在实现之前需要对GrabPass及实现原理做一个更加详细的介绍。
1 效果及代码
1.1 效果
1.2 Shader完整代码
Shader "Unity Shaders Book/Chapter 10/GlassRefraction"
{
//Properties
Properties {
_MainTex ("Main Tex", 2D) = "white" {} //玻璃材质纹理
_Cubemap ("EM", Cube) = "_Skybox" {}
_BumpMap ("Bump Map", 2D) = "bump" {} //玻璃法线纹理
//control the distortion of refraction
_Distortion ("Distortion", range(0, 100)) = 10
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
}
SubShader {
//Queue must be transparent, opaque objects will be drawn before
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
//define a pass to grab the screen behind the object,
//see the result by using "_RefractionTex"
GrabPass {"_RefractionTex"}
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
//Properties
sampler2D _MainTex;
float4 _MainTex_ST;
samplerCUBE _Cubemap;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _Distortion;
fixed _RefractAmount;
//remember to add:
sampler2D _RefractionTex;
//get the texel size:
float4 _RefractionTex_TexelSize;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
float4 srcPos : TEXCOORD4;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//抓取屏幕图像的采样坐标
o.srcPos = ComputeGrabScreenPos(o.pos);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = UnityObjectToWorldNormal(v.normal).xyz;
float3 worldTangent = UnityObjectToWorldNormal(v.tangent).xyz;
float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
//计算切线空间 -> 世界空间的矩阵,只需要3x3
//按列摆放
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i) :SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
//计算光照需要参数:
fixed3 worldlightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 worldviewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//对纹理采样+解码,得到法线方向
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
//由于这里需要模拟的是玻璃的折射效果,因此不能是这种常规的法线纹理的偏移:
//bump.xy *= _BumpScale;
//bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
//开始实现折射效果
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.srcPos.xy = offset + i.srcPos.xy;
//采样得到“折射”颜色:
fixed3 refractColor = tex2D(_RefractionTex, i.srcPos.xy/i.srcPos.w).rgb;
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
//开始环境映射:
fixed3 reflectDir = reflect(-worldviewDir, bump);
fixed4 texColor = tex2D(_MainTex, i.uv.xy);
fixed3 reflectColor = texCUBE(_Cubemap, reflectDir).rgb * texColor.rgb;
fixed3 finalColor = reflectColor * (1 - _RefractAmount) + refractColor * _RefractAmount;
return fixed4(finalColor, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
2 一些重点过程
2.1 制作场景
场景物体拜访和贴图完全参考《入门精要》:
以及当前场景的Cubemap的创建:由于相同项目下之前已经在Assets -> Edit加了一个可以获取场景中某个GameObject角度的Cubemap,直接按照相似的方法创建了创建过程可以参考【Unity Shader】Unity中如何创建Cubemap?
2.2 关于渲染队列设置
乍一看,SubShader的标签设置好像是前后矛盾的, 渲染队列Queue是Transparent透明的,而当前shader的渲染类型RenderType确是不透明:
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
GrabPass {"_RefractionTex"}
设置Queue的作用
我们上效果,如果不加上"Queue"="Transparent",效果如下:
当前的Shader是挂在外面的Cube上的,从上图效果和1.1的效果对比可以看出,Queue设置为Transparent是为了保证当前屏幕空间画面里的比Cube深度大、但是是不透明的物体(默认的Queue就是不透明)也能渲染并呈现出来,达到“透过玻璃观察”的效果。
设置RenderType的作用
还跟上面一样,假设把"RenderType"="Opaque"去掉,会发现效果跟1.1的没有任何变化。这是因为!RenderType其实是提前给当前的Shader归类了,为了方便之后使用着色器替换(Shader Relacement)的时候,当前Shader能被正确的使用。至于什么是着色器替换,后面会涉及到,这里就先不解释了,挖个坑以后填。
2.3 2种GrabPass的使用方法
参考ShaderLab:GrabPass - Unity 手册,从官方文档中可知GrabPass是ShaderLab语法中的一员,是包含在SubShader内的一种特殊的通道类型,它直接定义了一个额外的抓取屏幕图像的Pass,把即将绘制对象时的屏幕内容抓取到某个纹理中,这个纹理可以在后面的Pass中被使用去做一些效果。它的使用方式通常有两种:
GrabPass {}
即直接在Pass语义块前添加GrabPass {},{}里啥也不写,那么后续抓取屏幕图像的Pass会使用_GrabTexture来访问屏幕图像。这种方法看似方便,省去了定义一个新texture的麻烦。但当场景中多个物体都需要这种形式来抓取屏幕时(我理解的是有多个物体需要做出类似“玻璃”的效果),Unity都会为每个物体单独执行一次这个Pass的抓取操作,每个物体都会生成属于自己的_GrabTexture,这样的效果虽好,但代价是很大的。
GrabPass {"TextureName"}
就像上述代码中的:
GrabPass {"_RefractionTex"}
给我们Pass抓取屏幕图像定义一个专属的、名为"_RefractionTex"的纹理,后续的Pass中如果需要使用,就可以通过这个名称来访问抓取屏幕图像的纹理啦!比如上述代码中的:
fixed3 refractColor = tex2D(_RefractionTex, i.srcPos.xy/i.srcPos.w).rgb;
就是直接使用了定义的纹理名称来访问这个纹理。这样使用方法的好处是,同一个屏幕下多个需要GrabPass的物体都使用同一次渲染出的纹理,也就是仅进行一次GrabPass的抓取图像操作,这样就可以大大节省消耗!而且大部分情况下,都使用一张抓取的图像已经能满足效果需求了。
二者的对比
这里我们还是用到了Unity提供的Frame Debug,同时为了更好的对比效果,我在场景中多添加了一个想实现透明效果的Cube,这里仅看透明物体的渲染步骤。
使用GrabPass {"_RefractionTex"}时,可以发现步骤中两个Cube是共用同一张Texture的,仅Grab了一次:
而当使用GrabPass {}时,Grab了两次:
两次的RenderTexuter分别是:
二者的消耗对比(左GrabPass {"_RefractionTex"};右GrabPass {}),可以发现右边消耗明显比左边大:
2.4 获取纹素大小:_TexelSize
这里是为了提一提Shader中定义的:
//get the texel size:
float4 _RefractionTex_TexelSize;
以后的使用中如果想要获取某张纹理的纹素大小,就可以在纹理名称后加上_TexelSize啦!这个有点类似_MainTex_ST,都是Unity Shader的内置属性~
2.5 ComputerGrabScreenPos函数
这是一个Unity Shader的内置函数,ComputerGrabScreenPos()括号中输入裁剪空间下的顶点位置坐标,可以得到当前被抓取的屏幕图像的屏幕坐标。关于屏幕坐标获得好像有另一个函数?——ComputerScreenPos,那么问题来了:为什么不用ComputerScreenPos?关于这个问题,可以先保留着,我将在接下来的博客中仔细说明(又给自己挖了一个坑。。。)这里仅需要知道ComputerGrabScreenPos()的作用就行!
2.6 如何实现折射效果?
关于折射,我们好像真的学过并使用过一个折射相关的Unity内置函数——Refract(i, n, ri),但这里实现折射并不是真的要实现折射光的效果(太消耗啦!),而是采用GrabPass+给屏幕坐标一个偏移的方式实现玻璃的折射效果。
GrabPass在前面已经介绍过了,这里过一遍如何给屏幕坐标偏移,主要体现在如下代码:
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
//由于这里需要模拟的是玻璃的折射效果,因此不能是这种常规的法线纹理的偏移:
//bump.xy *= _BumpScale;
//bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
//开始实现折射效果
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.srcPos.xy = offset + i.srcPos.xy;
//采样得到“折射”颜色:
fixed3 refractColor = tex2D(_RefractionTex, i.srcPos.xy/i.srcPos.w).rgb;
对bump偏移
如果不记得如何在世界空间使用法线纹理了,可以先去看看【Unity Shader】纹理实践5.0:世界空间下使用法线纹理,如果仅实现法线纹理但并不考虑玻璃效果,直接给bump一个传统的变化就行,但这里需要加上玻璃的折射效果,因此还需要结合定义的_Distortion变量和_RefractionTex_TexelSize变量对bump“做手脚”。
- _Distorion——控制折射的扭曲程度,其实它的道理跟传统应用中的“_BumpScale”是一样的
- _RefractionTex_TexelSize——偏移量的大小,当然是根据纹理坐标而偏移
获取折射颜色
tex2D(_RefractionTex, i.srcPos.xy/i.srcPos.w)这里用了一个透视除法!这里涉及到了如何在Unity中获取片元在屏幕上的像素位置,这一点之前在学习基础理论时忽略了,后期会再补上,这里就不赘述(好家伙,又挖了一个坑。。。)。
最终呈现的颜色
fixed3 finalColor = reflectColor * (1 - _RefractAmount) + refractColor * _RefractAmount;
这里用了一个_RefractAmount巧妙地控制了折射和反射的占比(跟之前的环境映射中实现折射效果一样的操作),其实就是一个自行给定的菲涅尔项。