参考书籍:《Unity Shader 入门精要》
Unity 制作小地图(Minimap)系统 两种方法
官网文档:Graphics Command Buffers
GPU允许把三维场景渲染到一个中间缓冲中,即渲染目标纹理(Render Target Texture, RTT),而不是传统的帧缓冲或后备缓冲。延迟渲染使用多重渲染目标(Multiple Render Target, MRT),即同时渲染到多个渲染目标纹理。
Unity中使用渲染纹理通常有两种方式:
Properties {
_MainTex ("Main Tex", 2D) = "white" {} // 传入自己创建的RenderTexture
}
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv.x = 1 - o.uv.x; // 反转x分量
return o;
}
fixed4 frag(v2f i) : SV_Target {
return tex2D(_MainTex, i.uv); // 直接输出反转后的纹理
}
反射从1到0,折射从0到1。玻璃后面的图像被“扭曲”。
通过GrabPass获取屏幕图像,通常用于渲染透明物体。及对物体后面的图像做更复杂的处理,模拟折射效果等。虽然代码不包含混合指令,但往往需要把物体的渲染队列设置成透明队列(“Queue” = “Transparent”)。
实现原理:结合法线纹理修改法线信息,然后使用上一篇的Cubemap来模拟玻璃的反射,再用GrabPass获取玻璃后面的屏幕图像,最后用切线空间下的法线对屏幕做偏移。
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中:
// Transparent:在所有不透明物体渲染后再渲染。Opaque:确保在着色器替换时,该物体在需要时正确渲染。
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
// 抓取屏幕图像的Pass,存入到纹理变量_RefractionTex中。
GrabPass { "_RefractionTex" }
Pass {
...
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
samplerCUBE _Cubemap;
float _Distortion;
fixed _RefractAmount;
sampler2D _RefractionTex; // 对应GrabPass指定的纹理名
float4 _RefractionTex_TexelSize; // 纹理大小。如256x512的纹理,值为(1/256, 1/512)
...
struct v2f {
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0; // 屏幕图像的采样坐标
...
};
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos); // 计算抓取的屏幕图像采样坐标,在UnityCG.cginc中定义。
... // 法线纹理采样,切线空间转换到世界空间等
return o;
}
fixed4 frag (v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.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;
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb; // 透射除法 计算得到屏幕坐标
// 法线再转到世界空间,原理见前面第8篇
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
// 法线再转到世界空间
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);
}
}
_GrabTexture
来访问图像。如果多个物体都这样抓取屏幕,Unity都会单独抓取屏幕,消耗较大。但每个物体屏幕图像不同。- | 渲染纹理 | GrabPass |
---|---|---|
实现难度 | 复杂,要创建纹理,额外相机,设置相机渲染目标,最后传入Shader或者Raw Image。 | 简单,只需几行代码即可抓取屏幕。 |
效率 | 高,尤其是在移动设备。 | 低,在移动设备上,GrabPass虽然不会重新渲染场景,但要从CPU读取后备缓冲的数据,破坏了CPU和GPU的并行性,比较耗时。 |
自由度 | 高,虽然要重新渲染,但可以控制哪些进行渲染,分辨率大小等等。如最上面链接的游戏小地图制作。 | 低,获取的图形分辨率和屏幕一致。 |
除了上面两种方法,还可以使用命令缓冲(Command Buffers)实现抓取屏幕效果。见最上面链接。