Angry Bots水面反射效果剖析

这两天研究了一些unity官方的例子Angry Bots,发现其中的水面反射挺有趣,就算是作为读书笔记写下来吧。


图中显示了地面的水相当于一块模糊的镜子一样。

就先来说说其中的原理吧,生活中你站在一块镜子面前,看到的东西都是面向镜子的那一面,就相当于镜子中有一个你(虚像)正朝着外面看一样,高中物理可以解释。为此,我们必须准备一台辅助的camera来充当镜子中的你(虚像),于是得到一个相反的世界的影像,最后我们还需要把这个虚拟的影响映射到一块镜子上,也就是水面。


第一步,创建一个camera,项目中取名为Main CameraReflectionMain Camera,可以看一下照相机的属性



注意到Target Texture这个属性,这是动态生成的纹理,绘制了该照相机在场景中所看到的(相当于镜子里看到的),为了保证Main CameraReflectionMain Camera确实是主照相机的景象,需要以镜子作为平面,而两照相机以平面(镜面)对称存在,而工程中,把这部分计算放在了ReflectionFx.cs文件中的RenderReflectionFor函数里面

    先来看看两个照相机所看到的不同

下面的照相机正事从镜子里面看到的影像,现在的工作就是把下面的那张纹理中需要的部分映射到水面上。官方的做法是写了一个shader,我们来看看这个着色器是如何工作的。

随便选中一个具有反射效果的物体,我们可以他们的材质上都有一个共同的着色器RealtimeReflectionInWaterFlow,

目前我们只需要关心_ReflectionTex这个参数,传入了上一张图中所示的下图的纹理,即Main CameraReflectionMain Camera的影像。打开RealtimeReflectionInWaterFlow这个文件,其实该文件有用的部分只有第一个SubShader:

Shader "AngryBots/RealtimeReflectionInWaterFlow" {
    
Properties {
    _MainTex ("Base"2D) = "white" {}
    _Normal("Normal"2D) = "bump" {}
    _ReflectionTex("_ReflectionTex"2D) = "black" {}
    _FakeReflect("Fake reflection"2D) = "black" {}
    _DirectionUv("Wet scroll direction (2 samples)"Vector) = (1.0,1.0, -0.2,-0.2)
    _TexAtlasTiling("Tex atlas tiling"Vector) = (8.0,8.04.0,4.0)    
}

CGINCLUDE        

struct v2f_full
{
    half4 pos : SV_POSITION;
    half2 uv : TEXCOORD0;
    half4 normalScrollUv : TEXCOORD1;    
    half4 screen : TEXCOORD2;
    half2 fakeRefl : TEXCOORD3;
    #ifdef LIGHTMAP_ON
        half2 uvLM : TEXCOORD4;
    #endif    
};
    
#include "AngryInclude.cginc"        

half4 _DirectionUv;
half4 _TexAtlasTiling;

sampler2D _MainTex;
sampler2D _Normal;        
sampler2D _ReflectionTex;
sampler2D _FakeReflect;
            
ENDCG 

SubShader {
    Tags { "RenderType"="Opaque" }

    LOD 300 

    Pass {
        CGPROGRAM
        
        float4 _MainTex_ST;
        // float4 unity_LightmapST;    
        // sampler2D unity_Lightmap;
        
        v2f_full vert (appdata_full v
        {
            v2f_full o;
            o.pos = mul (UNITY_MATRIX_MVPv.vertex);
            o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex);
            
            #ifdef LIGHTMAP_ON
                o.uvLM = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
            #endif
            
            o.normalScrollUv.xyzw = v.texcoord.xyxy * _TexAtlasTiling + _Time.xxxx * _DirectionUv;
                        
            o.fakeRefl = EthansFakeReflection(v.vertex);
            o.screen = ComputeScreenPos(o.pos);
                
            return o
        }
                
        fixed4 frag (v2f_full i) : COLOR0 
        {
            half3 nrml = UnpackNormal(tex2D(_Normali.normalScrollUv.xy));
            nrml += UnpackNormal(tex2D(_Normali.normalScrollUv.zw));
            
            nrml.xy *= 0.025;
                                        
            fixed4 rtRefl = tex2D (_ReflectionTex, (i.screen.xy / i.screen.w) + nrml.xy);
            rtRefl += tex2D (_FakeReflecti.fakeRefl + nrml.xy * 2.0);
                        
            fixed4 tex = tex2D (_MainTexi.uv.xy + nrml.xy * 0.05);
        
            #ifdef LIGHTMAP_ON
                fixed3 lm = ( DecodeLightmap (UNITY_SAMPLE_TEX2D(unity_Lightmapi.uvLM)));
                tex.rgb *= lm;
            #endif    
            
            tex  = tex + tex.a * rtRefl;
            
            return tex;    
        }    
        
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile LIGHTMAP_OFF LIGHTMAP_ON
        #pragma fragmentoption ARB_precision_hint_fastest 
    
        ENDCG
    }

}

先看片段着色器中最后的像素输出的 tex  = tex + tex.a * rtRefl;tex.a是一个(0, 1)之间的小数,官方这么做,是为了弱化镜像纹理的影响,显得水面更逼真,如果是需要做完完全全的一面镜子,大可将tex.a舍弃,使得最后效果为叠加的,让镜像更清晰。再看看顶点着色器中o.screen = ComputeScreenPos(o.pos);

前文已经说过,我们必须把纹理中属于镜子外面的影像舍弃,这个方法就是用了场景中的物体的顶点作为限定,映射到屏幕座标,最终得出了Main CameraReflectionMain Camera照相机中看到的哪一部分才是镜子的影像,生成了uv座标后传递给片段着色器。

如果有什么地方讲的不对也希望大家能指出,共同进步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章