Unity_Shader高级篇_13_Unity Shader入门精要

第13章 使用深度和法线纹理
在12章中,屏幕后处理效果都只是在屏幕颜色图像上进行各种操作来实现的。然而,很多时候我们不仅需要当前屏幕的颜色信息,还希望得到深度和法线信息。例如,在进行边缘检测时,直接利用颜色信息会使检测到的边缘信息受物体纹理和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息,通过这样的方式检测出的边缘更加可靠。
本章中,我们将学习如何在Unity中获取深度纹理和法线纹理来实现特定的屏幕后处理效果。在13.1节中,我们首先会学习如何在Unity中获取这两种纹理。在13.2节中,我们会利用深度纹理来计算摄影机的移动速度,实现摄影机的运动模糊效果。在13.3节中,我们会学习如何利用深度纹理来重建屏幕像素在世界空间中的位置,从而模拟屏幕雾效。13.4节会再次学习边缘检测的另一种实现,即利用深度和法线纹理进行边缘检测。

13.1 获取深度和法线纹理
获取原理:深度纹理实际就是一张渲染纹理,只不过它里面存储的像素值不是颜色值,而是一个高精度的深度值。由于被存储在一张纹理中,深度纹理里的深度值范围是[0,1],而且通常是非线性分布的。那么,这些深度值是从哪里得到的呢?要回答这个问题,我们需要回顾在第四章学过的顶点变换的过程。总体来说,这些深度值来自于顶点变换后得到的归一化的设备坐标(Normalized Device Coordinates,NDC)。
图13.1显示了4.6.7小节中给出的Unity中透视投影对顶点的变换过程。左图显示了投影变换前,即观察空间下视锥体的结构及相应的顶点位置,中间的图显示了应用透视裁剪矩阵后的变换结果,即顶点着色器阶段输出的顶点变换结果,最右侧的图则是底层硬件进行了透视出发后得到的归一化的设备坐标。需要注意的是,这里的投影过程是建立在Uinty对坐标系的假定上的,也就是说,我们针对的是观察空间为右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换到NDC后z分量范围将在[-1,1]之间的情况。而在类似DirectX这样的图形接口中,变换后z分量范围将在[0,1]之间。如果需要在其他图形接口下实现本章的类似效果,需要对一些计算参数做出相应变化。变换时使用的矩阵运算(4.6.7)
Unity_Shader高级篇_13_Unity Shader入门精要_第1张图片
图13.2显示了在使用正交摄像机时投影变换的过程。同样,变换后会得到一个范围为[-1,1]的立方体。正交投影使用的变换矩阵是线性的。
Unity_Shader高级篇_13_Unity Shader入门精要_第2张图片
在得到NDC后,深度纹理中的像素值就可以很方便地计算得到了,这些深度值就对应了NDC中顶点坐标的z分量的值。由于NDC中z分量的范围在[-1,1],为了让这些值能够存储在一张图像中,我们需要使用下面的公式对其进行映射:

d = 0.5·z(ndc) + 0.5

其中d对应了深度纹理中的像素值,z(ndc)对应了NDC坐标中的z分量的值。
在Unity中,深度纹理可以直接来自于真正的深度缓存,也可以是由一个单独的Pass渲染而得,这取决于使用的渲染路径和硬件。通常来将,当使用延迟渲染路径(包括遗留的渲染路径)时,深度纹理理所当然可以访问到,因为延迟渲染会把这些信息渲染到G-buffer中。而当无法直接获取深度缓存时,深度和法线纹理是通过一个单独的Pass渲染而得的。具体实现是,Unity会使用着色器替代(Shader Replacement)技术选择那些渲染类型(即SubShader的RenderType标签)为Opaque的物体,判断它们使用的渲染队列是否小于等于2500(内置的Background、Geometry和AlphaTest渲染队列均在此范围内),如果满足条件,就把它渲染到深度和法线纹理中。因此,要想让物体能够出现在深度和法线纹理中,就必须在Shader中设置正确的RenderType标签。
在Unity中,我们选择让一个摄影机生成一张深度纹理或是一张深度+法线纹理。选择前者,Unity会直接获取深度缓存或是按之前讲到的着色器替代技术,选取需要的不透明物体,并使用它投射阴影时使用的Pass(即LightMode被设置为ShadowCaster的Pass(9.4))来得到深度纹理。如果Shader中不包含这样一个Pass,那么这个物体就不会出现在深度纹理中(当然,它也不能向其他物体投射阴影)。深度纹理的精度通常是24位或16位,这取决于使用的深度缓存的精度。如果选择后者,Unity会创建一张和屏幕分辨率相同、精度为32位(每个通道为8位)的纹理,其中观察空间下的法线信息会被编码进纹理的R和G通道,而深度信息会被编码进B和A通道。法线信息的获取再延迟渲染中是可以非常容易就得到的,Unity只需要合并深度和法线缓存即可。而在前向渲染中,默认情况下是不会创建法线缓存的,因此Unity底层使用了一个单独的Pass把整个场景再次渲染一遍来完成。这个Pass被包含在Unity内置的一个Unity Shader中,我们可以在内置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormalTexture.shader文件中找到这个用于渲染深度和法线信息的Pass。
如何获取

camera.depthTextureMode = DepthTextureMode.Depth;

一旦设置好了上面的摄像机模式后,我们就可以在Shader中通过声明_CameraDepthTexture变量来访问它。
同理,如果想要获取深度+法线纹理,我们只需要在代码中这样设置:

camera.depthTextureMode = DepthTextureMode.DepthNormals;

然后在Shader中通过声明_CameraDepthNormalsTexture变量来访问它。
我们还可以组合这些模式,让一个摄像机同时产生一张深度和深度+法线纹理:

camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;

在Unity5中,我们还可以在摄像机的Camera组件上看到当前摄像机是否渲染深度或深度+法线纹理。当在Shader中访问到深度深度纹理_CameraDepthTexture后,我们就可以使用当前像素的纹理坐标对它进行采样。绝大多下,我们直接使用tex2D函数采样即可,但在某些平台(例如PS3和PSP2)上,我们需要一些特殊处理;Unity为我们提供了一个统一 的宏(SAMPLE_DEPTH+FEXTURE).用来处理这些由于平台差异造成的的 。而我们只需要在Shader在中使用SAMPLR_DEPTH_ETXTURE宏对深度纹理进行采样,例如:

// 在这个像素点获得深度缓冲值。
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);

其中,i.uv是一个float2类型的变量,对应了当前像素的纹理坐标。类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJ(预计样本)和SAMPLE_DEPTH_TEXTURE_LOD(具有LOD级别的样本)。SAMPLE_DEPTH_TEXTURE_PROJ宏同样接受两个参数——深度纹理和一个float3或float4类型的纹理坐标,它的内部使用了tex2Dproj这样的函数进行投影纹理采样,纹理坐标的前两个分量首先会除以最后一个分量,再进行纹理采样。如果提供了第四个分量,还会进行一次比较,通常用于阴影的实现中。SAMPLE_DEPTH_TEXTURE_PROJ的第二个参数通常是由顶点着色器输出插值而得到屏幕坐标:

float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,UNITY_PROJ_COORD(i.scrPos));

其中,i.scrPos是在着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标。上述这些宏的定义,在内置的HLSLSupport.cginc文件中找到。
当通过纹理采样得到深度值后,这些深度值往往是非线性的,这种非线性来自于透视投影使用的剪裁矩阵。然而,在我们的计算过程中通常是需要线性的深度值,也就是说,我们需要把投影后的深度值变换到线性空间下,例如视角空间下的深度值。实际上,我们是倒推顶点变换的过程即可。
Unity提供了两个辅助函数(LinearEyeDepth和Linear01Depth)。LinearEyeDepth负责把深度纹理的采样结果转换到视角空间下的深度值。而Linear01Depth则会返回一个范围在[0,1]的线性深度值。这两个函数内部使用了内置的_ZBufferParams变量来得到远近裁剪平面的距离。
如果我们需要获取深度+法线纹理,可以直接使用tex2D函数对_CameraDepthNormalsTexture进行采样,得到里面存储的深度和法线信息。Unity提供了辅助函数来为我们对这个采样结果进行解码,从而得到深度值和法线方向。这个函数是DecodeDepthNormal,它在UnityCG.cginc里被定义:

inline void DecodeDepthNormal (float4 enc, out float depth, out float3 normal)
{
     depth = DecodeFloatRG(enc.zw);
     normal = DecodeViewNormalStereo(enc);
}

DecodeDepthNormal 的第一个参数是对深度+法线纹理的采样结果,这个采样结果是Unity对深度和法线信息编码后的结果,它的xy分量存储的是视角空间下的法线信息,而深度信息被编码进了zw分量。通过调用DecodeDepthNormal 函数对采样结果解码后,我们就可以得到解码后的深度值和法线。这个深度值是范围在[0,1]的线性深度值(这与单独的深度纹理中存储的深度值不同),而得到的法线则是视角空间下的法线方向。同样,我们也可以通过调用DecodeFloatRG和DecodeViewNormalStereo来解码深度+法线纹理中的深度和法线信息。

查看深度和法线纹理
利用帧调试器(Frame Debugger)。
下图中左边列表看到的是深度纹理,右侧为深度+法线纹理。如果当前摄像机需要生成深度和法线纹理,帧调试器的面板中就会出现相应的渲染事件。只要单击对应的事件就可以查看得到的深度和法线纹理。
Unity_Shader高级篇_13_Unity Shader入门精要_第3张图片
使用帧调试器查看到的深度纹理是非线性空间的深度值,而深度+法线纹理都是由Unity编码后的结果。有时,显示出线性空间下的深度信息或解码后的法线方向会更加有用。此时,我们可以自行在片元着色器中输出转换或解码后的深度和法线值,如图13.4所示,输出代码非常简单,我们可以使用类似下面的代码来输出线性深度值:

float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)
float linearDepth = Linear01Depth(depth);
return fixed4(linearDepth,linearDepth ,linearDepth ,1.0);

或是输出法线方向:

fixed3 normal = DecodeViewNormalStereo(tex2D(_CameraDepthNormalsTexture, i.uv).xy);
return fixed4(normal*0.5+0.5,1.0);

Unity_Shader高级篇_13_Unity Shader入门精要_第4张图片
在查看深度纹理时,读者得到的画面有可能几乎是全黑或全白的。这时候我们可以把摄像机的远裁剪平面的距离(Unity默认为1000)调小,使视锥体的范围刚好覆盖场景的所在区域。这是用为,由于投影变换时需要覆盖从近裁剪平面到远裁剪平面的所有深度区域,当远裁剪平面的距离过大时,会导致离摄像机较近的距离被映射到非常小的深度值,如果场景是一个封闭的区域(如图13.4所示),那么这就会导致画面看起来几乎是全黑的。相反,如果场景是一个开放区域,且物体离摄像机的距离较远,就会导致画面几乎是全白的。

13.2 再谈运动模糊
在12.6中,我们了解了如何通过混合多张屏幕图像来模拟运动模糊的效果。但是,另一种应用更广泛的技术则是使用速度映射图。速度映射图中存储了每个像素的速度,然后使用这个速度来决定模糊的方向和大小。速度缓冲的生成有多种方法,一种方法是把场景中所有物体的速度渲染到一张纹理中。但这种方法的缺点在于需要修改场景中所有物体的Shader代码,使其添加计算速度的代码并输出到一个渲染纹理中。
《GPU Gems3》在第二十七章(http://http.developer.nvidia.com/GPUGems3/gpugems3_ch27.html)中介绍了一种生成速度映射图的方法。优点是可以在一个屏幕后处理步骤中完成整个效果的模拟,但缺点是需要在片元着色器中进行两次矩阵乘法的操作,对性能有所影响。

(1)新建场景(Scene_13_2)。
(2)将Translating.cs脚本拖拽给摄像机,让其场景中不断运动。
(3)新建脚本(MotionBlurWithDepthTexture.cs)。拖拽到摄影机中。
(4)新建Unity Shader(Chapter13-MotionBlurWithDepthTexture)。

using UnityEngine;
using System.Collections;

public class MotionBlurWithDepthTexture : PostEffectsBase {

    //声明该效果需要的Shader,并据此创建相应的材质:
    public Shader motionBlurShader;
    private Material motionBlurMaterial = null;

    public Material material {  
        get {
            motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
            return motionBlurMaterial;
        }  
    }
    //由于需要得到摄像机的视角和投影矩阵,我们需要定义一个Camera类型的变量,以获取该脚本所在的摄像机组件:
    private Camera myCamera;
    public Camera camera {
        get {
            if (myCamera == null) {
                myCamera = GetComponent();
            }
            return myCamera;
        }
    }
    //定义运动模糊时模糊图像使用的大小。
    [Range(0.0f, 1.0f)]
    public float blurSize = 0.5f;
    //定义一个变量来保存上一帧摄像机的视角*投影矩阵:
    private Matrix4x4 previousViewProjectionMatrix;
    //由于本例需要获取摄像机的深度纹理,我们在脚本的OnEnable函数中设置摄像机的状态:
    void OnEnable() {
        camera.depthTextureMode |= DepthTextureMode.Depth;

        previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
    }
    //
    void OnRenderImage (RenderTexture src, RenderTexture dest) {
        if (material != null) {
            material.SetFloat("_BlurSize", blurSize);

            material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
            Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
            Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
            material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
            previousViewProjectionMatrix = currentViewProjectionMatrix;

            Graphics.Blit (src, dest, material);
        } else {
            Graphics.Blit(src, dest);
        }
    }
    //我们首先需要计算和传递运动模糊使用的各个属性。本例需要使用两个变换矩阵——
    //前一帧的视角*投影矩阵以及当前的视角*投影矩阵的逆矩阵。因此,我们通过调用camer.worldToCameraMatrix
    //和camera.projectionMatrix 来分别得到当前摄像机的视角矩阵和投影矩阵。对它们相乘取逆,得到
    //当前帧的视角*投影矩阵的逆矩阵,并传递给材质。然后,我们把取逆前的结果存储在previousViewProjectionMatrix
    //变量中,以便在下一帧时传给材质的_PreviousViewProjectionMatrix属性。
}
Shader "Unity Shaders Book/Chapter 13/Motion Blur With Depth Texture" {
    Properties {
        //对应了输入的渲染纹理,
        _MainTex ("Base (RGB)", 2D) = "white" {}
        //模糊图像时使用的参数u。
        _BlurSize ("Blur Size", Float) = 1.0
        //虽然在脚本里设置了材质的_PreviousViewProjectionMatrix和_CurrentViewProjectionInverseMatrix属性,
        //但并没有在Properties快中声明它们。这是因为Unity没有提供矩阵类型的属性,
        //但我们仍然可以在CG代码块中定义这些矩阵,并从脚本中设置它们。
    }
    SubShader {
        CGINCLUDE

        #include "UnityCG.cginc"

        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        sampler2D _CameraDepthTexture;
        float4x4 _CurrentViewProjectionInverseMatrix;
        float4x4 _PreviousViewProjectionMatrix;
        half _BlurSize;
        //除了定义在Properties声明的_MainTex和_BlurSize属性,我们还声明了其他三个变量。
        //_CameraDepthTexture是unity传递给我们的深度纹理,而_CurrentViewProjectionInverseMatrix和
        //_PreviousViewProjectionMatrix是由脚本传递而来的矩阵。除此之外,我们还声明了
        //_MainTex_TexelSize变量,它对应了主纹理的纹素大小,我们需要使用该变量来对深度
        //纹理的采样坐标进行平台差异化处理(5.6.1)

        struct v2f {
            //空间信息
            float4 pos : SV_POSITION;
            //空间转屏幕信息
            half2 uv : TEXCOORD0;
            //深度信息
            half2 uv_depth : TEXCOORD1;
        };

        //顶点着色器基本一致。
        v2f vert(appdata_img v) {
            v2f o;
            o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

            o.uv = v.texcoord;
            //增加了专门用于对深度纹理采样的纹理坐标变量
            o.uv_depth = v.texcoord;

            //对深度纹理的采样坐标进行了平台差异化处理。以便在类似DirectX的平台上,在开启了抗锯齿的情况下仍然可以的到正确的坐标。
            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                o.uv_depth.y = 1 - o.uv_depth.y;
            #endif

            return o;
        }
        //片元着色器是算法的重点所在
        fixed4 frag(v2f i) : SV_Target {
            // 在这个像素点获得深度缓冲值。
            float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
            // H是在1到1范围内的这个像素的视口位置。
            float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
            // 由视图-投影反向转换。
            float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
            // 除以w得到世界的位置。
            float4 worldPos = D / D.w;

            // 当前窗口的位置
            float4 currentPos = H;
            // 使用世界位置,并由之前的视图-投影矩阵进行变换。
            float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
            // 转换成非齐次点-1,1除以w。
            previousPos /= previousPos.w;

            // 使用这个框架的位置和最后一个帧来计算像素的速度。
            float2 velocity = (currentPos.xy - previousPos.xy)/2.0f;
            //当得到该像素的速度后,我们就可以使用该速度值对它的领域像素进行采样,相加后取平均值
            //得到一个模糊的效果。并使用_BlurSize来控制采样距离。
            float2 uv = i.uv;
            float4 c = tex2D(_MainTex, uv);
            uv += velocity * _BlurSize;
            for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
                float4 currentColor = tex2D(_MainTex, uv);
                c += currentColor;
            }
            c /= 3;

            return fixed4(c.rgb, 1.0);
        }

        ENDCG

        //然后,我们定义了运动模糊所需的Pass:
        Pass {      
            ZTest Always Cull Off ZWrite Off

            CGPROGRAM  

            #pragma vertex vert  
            #pragma fragment frag  

            ENDCG  
        }
    } 
    FallBack Off
}

本节实现的运动模糊使用与场景静止、摄像机快速运动的情况,这是因为我们在计算时只考虑了摄像机的运动。因此,如果读者把本节中的代码应用到一个物体快速运动而摄像机静止的场景,会发现不会产生任何运动模糊。如果我们想要对快移动的物体产生运动模糊的效果,就需要生成更加精确的速度映射图。我们可以在Unity自带的ImageEffect包中找到更多的运动模糊的实现方法。

13.3 全局雾效
雾效(Fog)是游戏里经常使用的一种效果。Unity内置的雾效可以产生基于距离的线性或指数雾效。然而,要想在自己编写的顶点/片元着色器中实现这些雾效,我们需要在Shadr中添加#pragma multi_compile_fog指令,同时还需要使用相关的内置宏,例如UNITY_FOG_COORDS、UNITY_TRANSFER_FOG和UNITY_APPLY_FOG等。这种方法的缺点在于,我们不仅需要为场景中所有物体添加相关的渲染代码,而且能够实现的效果也非常有限。当我们需要对雾效进行一些个性化操作时,例如使用基于高度的雾效等,仅仅使用Unity内置的雾效就变得不再可行。
这里我们将实现一种基于屏幕后处理的全局雾效的实现。使用这种方法,我们不需要更改场景内渲染的物体所使用的Shader代码,而仅仅依靠一次屏幕后处理的步骤即可。这种方法的自由性很高,我们可以方便地模拟各种雾效,例如均匀雾效、基于距离的线性/指数雾效、基于高度的雾效等。
基于屏幕后处理的全局雾效的关键是,根据深度纹理来重建每个像素在世界空间下的位置。尽管我们在模拟运动模糊时已经实现了这个要求,即构建出当前像素的NDC坐标,再通过当前射像机的视角*投影矩阵的逆矩阵来得到世界空间下的像素坐标,但是,这样的实现需要在片元着色器中重建世界坐标的方法。这种方法首先对图像空间下的视锥体射线(从摄影机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后,我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。当我们得到世界坐标后,就可以轻松地使用各个公式来模拟全局雾效了。
13.3.1 重建世界坐标
如何从深度纹理中重建世界坐标。
已知,坐标系中的一个顶点坐标可以通过它相对于另一个顶点坐标的偏移量来求得。重建像素的世界坐标也是基于这样的思想。我们只需要知道摄像机在世界空间下的位置,以及世界空间下该像素相对于摄像机的偏移量,把他们相加就可以得到该像素的世界坐标。代码表示如下:

float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;

其中,_WorldSpaceCameraPos是摄像机在世界空间下的位置,这可以由Unity的内置变量直接访问得到。而linearDepth* interpolatedRay则可以计算得到该像素相对于摄像机的偏移量,linearDepth是由深度纹理得到的线性深度值,interpolateRay是由顶点着色器输出并插值后得到的射线,它不仅包含了该像素到摄像机的方向,也包含了距离信息。linearDepth的获取我们已经在13.1.2中解释过了。
interpolatedRay来源于对近裁剪平面的四个角的某个特定向量的插值,这四个向量包含了他们到摄像机的方向和距离信息,我们可以利用摄像机的近裁剪平面距离、FOV、横纵比计算而得。图13.6显示了计算时使用的一些辅助向量。为了方便计算,我们可以先计算两个向量——toTop和toRight,它们是起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量。计算公式如下:

halfHeight = Near × tan(FOV/2)
toTop = camera.up × halfHeight
toRight = camera.right × halfHeight.aspect

Unity_Shader高级篇_13_Unity Shader入门精要_第5张图片
其中,Near是近裁剪平面的距离,FOV是竖直方向的视角范围,camera.up、camera.right分别对应了摄像机的正上方和正右方。
当得到这两个辅助向量后,我们就可以计算4个角相对于摄像机的方向了。我们以左上角为例(见图13.6中的TL点),它的计算公式如下:

TL = camera.forward.Nera + toTop - toRight
//依靠基本的矢量运算验证上面的结果。同理,其他三个角的计算也是类似的:
TP = camera.forward.Nera + toTop + toRight
BL = camera.forward.Nera - toTop - toRight
BR = camera.forward.Nera + toTop + toRight

注意,上面求得的四个向量不仅包含了方向信息,他们的模对应了四个点到摄像机的空间距离。由于我们得到的线性深度值并非是点到摄像机的欧式距离,而是在z方向上的距离,因此,我们不能直接使用深度值和四个角的单位方向的乘积来计算它们到摄像机的偏移量,如图13.7所示。想要把深度值和四个角的单位方向的乘积来计算它们到摄像机的偏移量,如图13.7所示,想要把深度值转换到摄像机的欧式距离也很简单,我们以TL点为例,根据相似三角形原理,TL所在的射线上,像素的深度值和它到摄像机的实际距离的比等于近裁剪平面的距离和TL向量的模的比,即:

depth/dist = Near/|TL|

由此可得,我们需要的TL距离摄像机的欧式距离dist:

dist = |TL|/Near × depth

由于四个点相互对称,因此其他三个向量的模和TL相等,即我们可以使用同一个因子和单位向量相乘,得到他们对应的向量值:

scale = |TL|/|Near|

Ray(TL) = TL/|TL| × scale
Ray(TR) = TR/|TR| × scale
Ray(BL) = BL/|BL| × scale
Ray(BR) = BR/|BR| × scale

Unity_Shader高级篇_13_Unity Shader入门精要_第6张图片
屏幕后处理的原理是使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片。这个四边形面片的四个顶点就对应了近裁剪平面的四个角。因此,我们把上面的计算结果传递给顶点着色器,顶点着色器根据当前的位置选择它所对应的向量,然后再将其输出,经插值后传递个片元着色器得到interpolatedRay,这样我们就可以用一开始提到的公式重建该像素在世界空间下的位置了。

雾的计算
Unity_Shader高级篇_13_Unity Shader入门精要_第7张图片

实现
(1)新建场景(Scene_13_3)。
(2)搭建测试场景,同时将translatinig.cs脚本拖拽给摄像机。
(3)新建脚本(FogWithDepthTexture.cs),并拖拽到摄像机上。
(4)新建Unity Shader (Chapter13-FogWithDepthTexture)

using UnityEngine;
using System.Collections;

public class FogWithDepthTexture : PostEffectsBase {
    //声明该效果需要的Shader,并据此创建相应的材质
    public Shader fogShader;
    private Material fogMaterial = null;

    public Material material {  
        get {
            fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
            return fogMaterial;
        }  
    }
    //在本节中,我们需要获取摄像机的相关参数,如近裁剪平面的距离、FOV等,同时还需要获取摄像机在世界下的前方、上方和右方等方向,因此我们用两个变量存储摄像机的
    //Cameera组件和Transform组件:
    private Camera myCamera;
    public  Camera camera {
        get {
            if (myCamera == null) {
                myCamera = GetComponent();
            }
            return myCamera;
        }
    }

    private Transform myCameraTransform;
    public Transform cameraTransform {
        get {
            if (myCameraTransform == null) {
                myCameraTransform = camera.transform;
            }

            return myCameraTransform;
        }
    }

    //定义模拟雾效时使用的各个参数:浓度,颜色,起始高度,终止高度。
    [Range(0.0f, 3.0f)]
    public float fogDensity = 1.0f;

    public Color fogColor = Color.white;

    public float fogStart = 0.0f;
    public float fogEnd = 2.0f;
    //因为需要获取摄像机的深度纹理,我们在脚本的OnEnable函数中设置摄像机的相应状态:
    void OnEnable() {
        camera.depthTextureMode |= DepthTextureMode.Depth;
    }

    void OnRenderImage (RenderTexture src, RenderTexture dest) {
        if (material != null) {
            Matrix4x4 frustumCorners = Matrix4x4.identity;

            float fov = camera.fieldOfView;
            float near = camera.nearClipPlane;
            float aspect = camera.aspect;

            float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
            Vector3 toRight = cameraTransform.right * halfHeight * aspect;
            Vector3 toTop = cameraTransform.up * halfHeight;

            Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
            float scale = topLeft.magnitude / near;

            topLeft.Normalize();
            topLeft *= scale;

            Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
            topRight.Normalize();
            topRight *= scale;

            Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
            bottomLeft.Normalize();
            bottomLeft *= scale;

            Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
            bottomRight.Normalize();
            bottomRight *= scale;

            frustumCorners.SetRow(0, bottomLeft);
            frustumCorners.SetRow(1, bottomRight);
            frustumCorners.SetRow(2, topRight);
            frustumCorners.SetRow(3, topLeft);

            material.SetMatrix("_FrustumCornersRay", frustumCorners);

            material.SetFloat("_FogDensity", fogDensity);
            material.SetColor("_FogColor", fogColor);
            material.SetFloat("_FogStart", fogStart);
            material.SetFloat("_FogEnd", fogEnd);

            Graphics.Blit (src, dest, material);
        } else {
            Graphics.Blit(src, dest);
        }
    }   
}
//首先计算了近裁剪平面的四个角对应的向量,并把它们存储在一个矩阵类型的变量(frustumCorners)中。计算过程我们已经在13.3.1中解释过了,代码只是套用了之前的公式而已。我们按一定顺序把这四个方向存储到了frustumCorners不同的行中,这个顺序是非常重要的,因为这决定了我们在顶点着色器中使用哪一行作为该点的待插值向量。随后,我们把结果和其他参数传递给材质,并调用Graphics.Blit (src, dest, material)把渲染结果显示在屏幕上。
Shader "Unity Shaders Book/Chapter 13/Fog With Depth Texture" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        //浓度参数
        _FogDensity ("Fog Density", Float) = 1.0
        //颜色
        _FogColor ("Fog Color", Color) = (1, 1, 1, 1)
        //起始高度
        _FogStart ("Fog Start", Float) = 0.0
        //终止高度
        _FogEnd ("Fog End", Float) = 1.0
    }
    SubShader {
        CGINCLUDE

        #include "UnityCG.cginc"

        float4x4 _FrustumCornersRay;

        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        //深度纹理
        sampler2D _CameraDepthTexture;
        half _FogDensity;
        fixed4 _FogColor;
        float _FogStart;
        float _FogEnd;

        struct v2f {
            float4 pos : SV_POSITION;
            half2 uv : TEXCOORD0;
            half2 uv_depth : TEXCOORD1;
            float4 interpolatedRay : TEXCOORD2;
        };

        v2f vert(appdata_img v) {
            v2f o;
            o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

            o.uv = v.texcoord;
            o.uv_depth = v.texcoord;

            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                o.uv_depth.y = 1 - o.uv_depth.y;
            #endif

            //分象限
            int index = 0;
            if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
                index = 0;
            } else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
                index = 1;
            } else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
                index = 2;
            } else {
                index = 3;
            }

            //
            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                index = 3 - index;
            #endif

            o.interpolatedRay = _FrustumCornersRay[index];

            return o;
        }

        //在v2f结构体中,我们除了定义顶点位置、屏幕图像和深度纹理的坐标外,还定义了interpolatedRay
        //变量存储插值后的像素想向量。在顶点着色器中,我们对深度纹理的采样做坐标进行了平台化差异处理。
        //更重要的是,我们要决定该点对应了四个角中的那个角。我们采用的方法是判断他的
        //纹理坐标。我们知道,在Unity中,纹理坐标的(0,0)点对应了左下角,而(1,1,)点对应了
        //右上角。我们据此来判断该顶点对应的索引,这个对应关系和我们脚本中对frustumCorners和赋值
        //顺序是一致的。实际上,不同平台的纹理坐标不一定是满足上面的条件的,例如DirectX和Metal这样
        //的平台,左上角对应了(0,0)点,但大多数情况下Unity会把这些平台下的屏幕图像进行翻转,
        //因此我们仍然可以利用这个条件。但如果在类似DirectX的平台上开启了抗锯齿,Unity
        //就不会进行这个翻转。为了此时仍然可以得到相应相应顶点位置的索引值,我们对索引值也进行了
        //平台差异化处理(5.6.1),以便在必要时也对索引值进行翻转。最后,我们使用索引值来获取
        //_FrustumCornersRay中对应的行为为该顶点的interpolatedRay值。

        //产生线性(linear)雾效:
        fixed4 frag(v2f i) : SV_Target {
            float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
            float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;

            //线性雾效的计算,基于高度的雾效。
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
            fogDensity = saturate(fogDensity * _FogDensity);

            fixed4 finalColor = tex2D(_MainTex, i.uv);
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);

            return finalColor;
        }

        //首先我们需要重建该像素在世界空间中的位置。为此,我们首先使用SAMPLE_DEPTH_TEXTURE
        //对深度纹理进行采样,再使用LinearEyeDepth得到视角空间下的线性深度值。之后,与interpolatedRay
        //相乘后再和世界空间下的摄像机位置相加,即可得到世界空间下的位置。

        //得到世界坐标后,模拟雾效就变得非常容易。在本例中,我们选择实现基于高度的雾效模拟。,
        //我们根据材质属性_FogEnd和_FogStart计算当前的像素高度worldPos.y对应的雾效系数
        //fogDensity,再和参数_FogDensity相乘后,利用saturate函数截取到【0,1】范围内,
        //作为最后的雾效系数。然后,我们使用该系数将雾的颜色和原始颜色进行混合后返回。
        ENDCG

        //定义了雾效渲染所需的Pass
        Pass {
            ZTest Always Cull Off ZWrite Off

            CGPROGRAM  

            #pragma vertex vert  
            #pragma fragment frag  

            ENDCG  
        }
    } 
    FallBack Off
}

上述过程实现的前提是摄影机的投影类型是透视投影。如果需要正交投影的情况下重建世界坐标,需要使用不用的公式,但是这个过程不会比透视投影的情况更加复杂。(http://www.derschmale.com/2014/03/19/reconstructing-positions-from-the-depth-buffer-pt-2-perspective-and-orthographic-general-case/)

你可能感兴趣的:(Shader,观后感)