这两天研究了一些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.0, 4.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_MVP, v.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(_Normal, i.normalScrollUv.xy));
nrml += UnpackNormal(tex2D(_Normal, i.normalScrollUv.zw));
nrml.xy *= 0.025;
fixed4 rtRefl = tex2D (_ReflectionTex, (i.screen.xy / i.screen.w) + nrml.xy);
rtRefl += tex2D (_FakeReflect, i.fakeRefl + nrml.xy * 2.0);
fixed4 tex = tex2D (_MainTex, i.uv.xy + nrml.xy * 0.05);
#ifdef LIGHTMAP_ON
fixed3 lm = ( DecodeLightmap (UNITY_SAMPLE_TEX2D(unity_Lightmap, i.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坐标后传递给片段着色器。
如果有什么地方讲的不对也希望大家能指出,共同进步