一、前言
现代GPU允许我们把整个三维场景目标渲染到一个中间缓冲中,即渲染目标纹理(RTT)。
多重渲染目标(MRT),这种技术指的是GPU允许我们把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景,延迟渲染就是其中的一个应用。
Untiy,定义了渲染纹理(Render Texture),用来代替渲染目标纹理。
Unity中使用渲染纹理有两种方式:
1.一种方法是创建一个渲染纹理,然后把某个摄像机的渲染目标设置成该纹理,这样摄像机的渲染结果就会实时更新到渲染纹理中,而不是显示在屏幕上,使用这种方法还可以选择渲染纹理的分辨率、滤波模式等纹理属性。
2.在屏幕后处理时使用GrabPass命令或OnRenderImage函数获取当前屏幕图像,Unity会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中。
之后我们就可以使用自定义的PASS中把他们当成普通纹理进行处理,从而实现屏幕特效。
二、镜子效果
Shader "Unity Shaders Book/Chapter 10/Mirror" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {
} //渲染纹理 镜子
}
SubShader {
Tags {
"RenderType"="Opaque" "Queue"="Geometry"}
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
struct a2v {
float4 vertex : POSITION;
float3 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
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; //翻转x轴
return o;
}
fixed4 frag(v2f i) : SV_Target {
return tex2D(_MainTex, i.uv); //进行采样
}
ENDCG
}
}
FallBack Off
}
三、玻璃效果
Unity中可以使用GrabPass来获取屏幕图像,这个Pass会将屏幕图像绘制在一张纹理中,然后在后续的Pass中可以访问它。
一般诸如玻璃等透明材质的模拟,都用这个Pass来获得纹理。
他与普通的混合不同,这个可以让我们对物体后面的图像进行更复杂的处理,例如折射效果等等。
下面代码除了材质主纹理和法线纹理外,
使用了之前的环境纹理来营造反射效果,
使用这个GrabPass来获取纹理用于折射效果。
–代码:写了注释,就是有一行书中和实例不同,不知道区别在哪。。
Shader "Unity Shaders Book/Chapter 10/Glass Refraction" {
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 //为0时,只包含反射效果,为1时,只包含折射效果
}
SubShader {
// We must be transparent, so other objects are drawn before this one.
Tags {
"Queue"="Transparent" "RenderType"="Opaque" } //渲染队列透明队列,RenderType的设置是为了使用着色器替换。
GrabPass {
"_RefractionTex" } //定义GrabPass来抓取屏幕图像,内部字符串是我们抓取图像被定义在哪个纹理中。
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
samplerCUBE _Cubemap;
float _Distortion;
fixed _RefractAmount;
sampler2D _RefractionTex; //GrabPass抓取的纹理
float4 _RefractionTex_TexelSize; //获取纹理纹素大小
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord: TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0;
float4 uv : TEXCOORD1;
float4 TtoW0 : TEXCOORD2; //存储切线空间到世界空间的变换矩阵,w向量用于存储worldPos,节省空间
float4 TtoW1 : TEXCOORD3;
float4 TtoW2 : TEXCOORD4;
};
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); //裁剪空间
o.scrPos = ComputeGrabScreenPos(o.pos); //这个函数得到对应被抓取的屏幕图像采样坐标,
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); //对材质纹理和法线纹理进行偏移计算(得出采样坐标),存到一个float4中
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; //使用叉乘×w向量,得到副切线
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); //进行列排列,获取变换矩阵
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); //为了节省空间将世界空间下的顶点坐标存储在w分量。
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); //提取w分量获取世界空间下的顶点坐标
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos)); //得到该片元的视角方向
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); //法线纹理用来修改模型的法线信息
//对法线纹理进行采样,得到的是切线空间下的法线方向(也就是说这张纹理存储的就是切线空间的信息)
//这里我们使用切线空间下的法线方向是因为这样可以反应顶点局部空间下的法线方向。
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy; //使用这个法线方向与扭曲程度再与抓取的纹素大小相乘进行模拟偏移效果
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy; //书上没×z,不知道这里为什么。。书上之前齐次除法那里确实是z轴时不需要的。。
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb; //这里是进行透视除法,得到真正的屏幕坐标,再对纹理采样
//上面这里就是先 对屏幕图像的采样坐标进行偏移,用来模拟折射效果,
//然后更新屏幕采样坐标,
//然后再使用这个屏幕采样坐标对 抓取的屏幕图像进行采样,得到最终的模拟的折射效果颜色
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; //最终颜色使用_RefractAmount混合,
return fixed4(finalColor, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
GrabPass中的纹理命名使用有两个使用方法:
1.GrabPass { }
:在之后的Pass中我们可以直接使用_GrabTexture访问屏幕纹理。但是这样每次都会进行抓取,比较浪费性能。
2.GrabPass { "_RefractionTex" }
:这时纹理就是存储在_RefractionTex中,之后在很多Pass中都可以使用,包括多少场景中的多少物体都可以使用,节省性能。
四、渲染纹理和GrabPass区别
这两种,
GrabPass会更方便,因为渲染纹理我们需要进行摄像机,渲染纹理对应,然后再赋值给Shader,
渲染纹理效率会更好,尤其是在移动设备上,渲染纹理我们可以自定义纹理大小,尽管这种方法会再渲染一遍部分场景,但我们可以调整渲染层减少二次渲染的场景大小。或者使用其他方法控制摄像机是否开启。
而GrabPass获取到的图像分辨率是和屏幕一样的,这意味着在高分辨率设备上,会有严重的带宽影响,
在移动设备上,GrabPass不会重新渲染场景,它往往是需要CPU直接读取后备缓冲中的数据,破坏了CPU、GPU之间的并行性,甚至在一些移动设备上不支持。
Unity5以后提供了命令缓冲来允许扩展渲染流水线,也可以实现抓取屏幕效果。
它可以在不透明物体渲染后把当前图像复制到一个临时的渲染目标纹理中,再进行一些特殊操作,实现诸如模糊等的特殊效果。