UnityShader渲染纹理(Render Texture)应用(镜子、玻璃 ...
现在GPU允许我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理(Render Target Texture),指GPU允许我们把场景同事渲染到国歌渲染目标纹理中,而不再需要为每个选居然目标纹理单独渲染完整的场景。Unity为渲染目标纹理定义了专门的纹理类型——渲染纹理(Render Texture)。使用渲染纹理通常的两种方式:
[*]在Project目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置为渲染纹理,这样该摄像机的渲染结果就会实时更新到渲染纹理中,而不会显示在屏幕上。
[*]在屏幕后处理时使用GrabPass命令或者OnRenderImage函数来获取当前屏幕图像,Unity会把这个图像放到一张和屏幕分量率等同的渲染纹理中,然后我们可以在自定义的Pas中把他们当成普通的纹理来处理,从而实现各种屏幕特效。
镜子效果
在场景中创建一个相机(MirrorCamera),在Project窗口创建一个 Render Texture ,并将 Render Texture 赋值给 MirrorCamera 的Target Texture 属性。这样,MirrorCamera 中的渲染结果都会实时更新到这张Render Texture中。然后再在场景中创建一个四边形(Quad),调整Quad的大小将它作为镜子。接着调整MirrorCamera的位置,裁剪平面视角的参数,使MirrorCamera渲染出的图像是我们我们希望的镜子图形。需要注意的是,我们创建的Render Texture的尺寸比例最好与镜子(Quad)的尺寸比例相同,这样方便我们来调整相机位置。再调整时,我试了很多方法,发现将 MirrorCamera 的近平面设置的跟镜子(Quad)的大小相同且位置重合的时候效果时最好的。如下图所示:
MirrorCamera
Render Texture
相机和镜子的位置摆放
接下来就到了我们为镜子写Shader 的时候了,其实很简单,只需要把渲染纹理左右翻转展示在镜子上即可。
Shader "Unlit/Mirror"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.x = 1 - o.uv.x;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
接下来把我们创建的 Render Texture 赋值给该Shader材质球的_MainTex即可。渲染结果如下。
玻璃效果
玻璃效果的实现则需要我们在开头提到的那个GrabPass命令,当我们在Shder中定义了一个GrabPass之后,Unity会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的Pass中访问它进行更复杂的处理。需要注意的是,在使用GrabPass时,我们需要额外小心物体的渲染队列设置,因为GrabPass通常用于渲染透明物体,尽管代码立不包含混合指令,但我们仍然徐娅把物体的渲染你队列设置为透明队列("Queue"="Transparent"),这样才可以保证当渲染该物体时,所有的不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像。具体代码实现如下:
Shader "Unlit/GlassRefraction"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {} //玻璃的材质纹理
_BumpMap ("BumpMap", 2D) = "bump" {} //玻璃的法线纹理
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {} //模拟反射的环境纹理
_Distortion ("Distortion", Range(0, 100)) = 10//控制折射程度
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0 //控制折射和反射的混合度 为0时只包含反射效果,为1时只包含折射效果
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
GrabPass {"_RefractionTex"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
fixed4 t2w0 : TEXCOORD1;
fixed4 t2w1 : TEXCOORD2;
fixed4 t2w2 : TEXCOORD3;
float4 scrPos : TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
samplerCUBE _Cubemap;
float _Distortion;
float _RefractAmount;
sampler2D _RefractionTex;
//得到 _RefractionTex 纹理的纹素大小(如:一个256 * 512 的纹理,它的纹素大小为(1/256,1/512)),用于对屏幕图像采样坐标偏移的计算
float4 _RefractionTex_TexelSize;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//BumpMap 和 MainTex 使用同一套uv
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//获取屏幕图像的采样坐标
o.scrPos = ComputeGrabScreenPos(o.pos);
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex);
fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
fixed3 worldTangent = normalize(UnityObjectToWorldDir(v.tangent));
fixed3 worldBiTangent = cross(worldNormal,worldTangent) * v.tangent.w;
//切线空间到世界空间的转换矩阵 和 世界坐标 写入插值器
o.t2w0 = fixed4(worldTangent.x, worldBiTangent.x, worldNormal.x, worldPos.x);
o.t2w1 = fixed4(worldTangent.y, worldBiTangent.y, worldNormal.y, worldPos.y);
o.t2w2 = fixed4(worldTangent.y, worldBiTangent.y, worldNormal.y, worldPos.y);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldPos = fixed3(i.t2w0.w, i.t2w1.w, i.t2w2.w);
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//切线空间到世界空间的转换矩阵
fixed3x3 t2w = fixed3x3(i.t2w0.xyz, i.t2w1.xyz, i.t2w2.xyz);
//法线贴图采样
fixed4 bumpTex = tex2D(_BumpMap, i.uv);
//解包法线贴图获取切线空间法线方向
fixed3 tangentSpaceNormal = UnpackNormal(bumpTex);
//切线空间法线转到世界空间
fixed3 worldNormal = mul(t2w, tangentSpaceNormal);
//计算屏幕采样坐标偏移
float2 offset = tangentSpaceNormal.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.scrPos.xy = offset + i.scrPos.xy;
//使用透视除法对屏幕图像采样,即折射颜色
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
//根据视角方向计算反射的入射光方向
fixed3 reflDir = reflect(-worldViewDir, worldNormal);
//主纹理采样
fixed4 mainTex = tex2D(_MainTex, i.uv);
//立方体纹理采样
fixed4 cubeTex = texCUBE(_Cubemap,reflDir);
//计算反射光颜色
fixed3 reflCol = cubeTex.rgb * mainTex.rgb;
//计算最终混合颜色
//fixed4 col = fixed4( reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount , 1);
fixed4 col = fixed4( lerp(reflCol, refrCol, _RefractAmount) , 1);
return col;
}
ENDCG
}
}
}
可以看到,在片元着色器中,我们使用了切线空间下的法线方向,_Distortion属性和_RefractionTex_TexelSize来对屏幕图像的采样坐标进行偏移,模拟折射效果。_Distortion越大偏移量越大,玻璃背后的物体看起来形变成都越大。使用切线空间下的法线方向时因为它可以反应顶点局部空间下的偏移方向(即xy分量)。
随后使用了透视触发得到屏幕图像采样坐标,这是因为我们在顶点着色器中使用ComputeGrabScreenPos得到的屏幕坐标不是真正的屏幕坐标,而是少了一个除裁剪坐标的w分量的步骤。这是因为投影空间不是线性的,而插值往往是线性的。我们如果在顶点着色器中直接除裁剪坐标的w分量。顶点着色器到片元着色器的会有一个线性插值的过程,这样得到的结果就会不正确,所以我们需要在片元着色器中完成屏幕坐标的计算。
最后渲染结果如下:
左图为 _RefractAmount = 0.5的渲染结果,右图为 _RefractAmount = 1的渲染结果
书中对GrabPass和渲染纹理+额外摄像机的方案进行了优缺点的对比。如下图:
此外,书中还提到了 ccc,感兴趣的可以去猫一眼。
此篇文章是在学习《Unity Shader入门精要》一书的笔记,加入了一些个人理解,如有错误之处,还请各位不吝赐教。
页:
[1]