《入门精要》中模拟玻璃是用了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 制作场景

场景物体拜访和贴图完全参考《入门精要》:

Unity3D 实现门窗破碎的效果 unity玻璃_unity

以及当前场景的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",效果如下:

Unity3D 实现门窗破碎的效果 unity玻璃_游戏_02

 当前的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了一次:

Unity3D 实现门窗破碎的效果 unity玻璃_游戏_03

Unity3D 实现门窗破碎的效果 unity玻璃_unity_04

而当使用GrabPass {}时,Grab了两次:

Unity3D 实现门窗破碎的效果 unity玻璃_unity_05

两次的RenderTexuter分别是:

Unity3D 实现门窗破碎的效果 unity玻璃_游戏_06

Unity3D 实现门窗破碎的效果 unity玻璃_unity_07

二者的消耗对比(左GrabPass {"_RefractionTex"};右GrabPass {}),可以发现右边消耗明显比左边大:

Unity3D 实现门窗破碎的效果 unity玻璃_unity_08

Unity3D 实现门窗破碎的效果 unity玻璃_Unity3D 实现门窗破碎的效果_09

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巧妙地控制了折射和反射的占比(跟之前的环境映射中实现折射效果一样的操作),其实就是一个自行给定的菲涅尔项。