Unity Shader学习笔记(16)渲染纹理(Render Texture)

发表于2017-12-29
评论0 4.3k浏览
渲染纹理(Render Texture)
GPU允许把三维场景渲染到一个中间缓冲中,即渲染目标纹理(Render Target Texture, RTT),而不是传统的帧缓冲或后备缓冲。延迟渲染使用多重渲染目标(Multiple Render Target, MRT),即同时渲染到多个渲染目标纹理。 

Unity中使用渲染纹理通常有两种方式: 
1. 在Project目录创建渲染纹理,摄像机中设置渲染目标为该纹理。可以模拟镜子效果、投影(如制作游戏的小地图)。 
2. 屏幕后处理时使用GrabPass命令或OnRenderImage函数来获取当前屏幕图像,实现各种屏幕特效。

镜子效果(Mirror)
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);       // 直接输出反转后的纹理
}

玻璃效果(Glass)
反射从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);
    }
}

GrabPass支持的形式
直接使用GrabPass{ },在后面Pass中直接使用_GrabTexture来访问图像。如果多个物体都这样抓取屏幕,Unity都会单独抓取屏幕,消耗较大。但每个物体屏幕图像不同。
使用GrabPass {“TextureName”},如上例子。但Unity只会在每一帧时,为第一个使用TextureName纹理的物体抓一次屏幕,同时意味着所有物体都用这一张屏幕图像。

渲染纹理 和 GrabPass的比较
-渲染纹理GrabPass
实现难度复杂,要创建纹理,额外相机,设置相机渲染目标,最后传入Shader或者Raw Image。简单,只需几行代码即可抓取屏幕。
效率高,尤其是在移动设备。低,在移动设备上,GrabPass虽然不会重新渲染场景,但要从CPU读取后备缓冲的数据,破坏了CPU和GPU的并行性,比较耗时。
自由度高,虽然要重新渲染,但可以控制哪些进行渲染,分辨率

除了上面两种方法,还可以使用命令缓冲(Command Buffers)实现抓取屏幕效果

Unity Shader学习笔记系列教程:

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论