渲染目标纹理(RTT):把整个三维场景渲染到一个中间缓冲中,而不是传统的帧缓冲
多重渲染目标(MRT):把场景同时渲染到多个渲染目标纹理中,不需要为每个渲染目标纹理单独渲染完整的场景,比如延迟渲染
在Unity中定义为渲染纹理(Render Texture),有两种实现方式:一种是在Project目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置为该纹理;另一种是在屏幕后处理时使用GrabPass命令或OnRenderImage函数来获得当前屏幕图像,放在一张等大的纹理中。
新建一个小房间的场景,里面放置一些球形和立方体,并且放一个四边形作为镜子。
放置一个新的摄像机,并把它的渲染目标设置为新建的渲染纹理Mirror Texture。编辑应用于镜子材质的shader,只需要处理一个纹理属性,并且在顶点着色器中翻转纹理x坐标,并在片元着色器中输出:
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
// Mirror needs to filp x
o.uv.x = 1 - o.uv.x;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return tex2D(_MainTex, i.uv);
}
把前面的渲染纹理应用于这个shader,效果如下:
这部分使用GrabPass的方法。
声明玻璃的纹理、法线纹理,_Cubemap模拟的是反射的环境,_Distortion控制折射的扭曲程度,_RefractAmount控制反射程度。
Properties {
_MainTex ("Main Tex", 2D) = "white" {
}
_BumpMap ("Normal Map", 2D) = "bump" {
}
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {
}
_Distortion ("Distortion", Range(0, 100)) = 10
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
}
定义相应渲染队列,用GrabPass获取图像。
SubShader {
Tags {
"Queue"="Transparent" "RenderType"="Opaque" }
GrabPass {
"_RefractionTex" }
变量中除了之前的属性,还有_RefractionTex(对应上面GrabPass的抓取结果)和它的纹素大小。
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
samplerCUBE _Cubemap;
float _Distortion;
fixed _RefractAmount;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
顶点着色器中,使用ComputeGrabScreenPos()
函数获得屏幕图像采样坐标。因为需要在片元着色器中把法线方向从切线空间变换到世界空间,以便对Cubemap进行采样,这个变换矩阵存放在TtoW
的xyz分量里,世界坐标存放在w轴。
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = 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;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
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;
}
从TtoW
得到世界坐标,提取法线bump
,利用_Distortion
得到坐标偏移量,计算模拟的折射颜色refrCol
。接着变换切线空间获取法线、反射方向,然后对Cubemap采样,和主纹理相乘得到反射颜色reflCol
。
最后使用_RefractAmount
对折射和反射颜色进行混合。
fixed4 frag (v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// Get the normal in tangent space
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
// Compute the offset in tangent space
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
// Convert the normal to world space
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 reflDir = reflect(-worldViewDir, bump);
fixed4 texColor = tex2D(_MainTex, i.uv.xy);
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;
return fixed4(finalColor, 1);
}
设置纹理、法线和环境Cubemap:
可以得到下图类似玻璃的效果:
从效率上讲,使用渲染纹理比较好,尤其在移动设备上。我们可以调整摄像机的渲染层等信息,来控制纹理的大小;而GrabPass获取的是屏幕分辨率大小的图像,在高分辨率的设备上会有严重的带宽影响。
程序纹理是由计算机生成的,使用这种纹理的好处是可以方便控制纹理属性,得到丰富的动画和视觉效果。