Shader学习笔记(八)

Shader 高级篇(四)

使用深度和法线纹理

屏幕后处理效果都只是在屏幕颜色图像上进行各种操作来实现的。然而,很多时候不仅需要当前屏幕的颜色信息,还希望得到深度和法线信息。

获取深度和法线纹理

1. 背后的原理

深度纹理实际就是一张渲染纹理,只不过它里面存储的像素值不是颜色值,而是一个高精度的深度值。这些深度值来自于顶点变换后得到的归一化设备坐标 (Normalized Device Coordinates,NDC) ,这里的深度值主要通过在顶点着色器中乘以MVP变换矩阵得到的,在变换的最后一步,需要使用一个透视投影矩阵来变换顶点,透视投影矩阵是非线性的,如下图:
在这里插入图片描述
而正交投影是线性的,如下图:
在这里插入图片描述
在得到NDC后,深度纹理中的像素值就可很方便地计算得到了,这些深度值就对应了NDC中顶点坐标的z分量的值。由于NDCz分量的范围在[-1,1],为了让这些值能够存储在一张图像中,需要使用下面的公式进行映射:

d=0.5 * z +0.5

其中,d对应了深度纹理中的像素值,z对应了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) 来得到深度纹理。如Shader中不包含这样一个Pass,那么该物体就不会出现在深度纹理中i。深度纹理的精度取绝于使用的深度缓存的精度。如果选择生成一张深度+法线纹理,Unity会创建一张和屏幕分辨率相同、精度为32位(每个通道为8位)的纹理,其中观察空间下的法线信息会被编码进纹理的 RG通道而深度信息会被编码进 BA 通道,法线信息的获取在延迟渲染 中是可以非常容易就得到的,Unity 只需要合并深度和法线缓存即可。而在前向渲染中,默认情况下是不会创建法线缓存的,因此Unity底层使用了一个单独的 Pass 把整个场景再次渲染一遍来完成, 这个Pass被包含在Unity 内置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormalTexture.shader 文件中找到这个用于渲染深度和法线信息的Pass

2. 如何获取

Shader中直接访问特定的纹理属性即可。这个与 Unity 沟通的过程是通过在脚本中设置摄像头的 depthTextureMode 来完成的。

camera.depthTextureMode = DepthTextureMode.Depth;

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

camera.depthTextureMode=
DepthTexrureMode.DepthNormals;

可组合这些模式,让一个摄像机同时产生一张深度和深度+法线纹理:

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

Unity 提供了一个统一的宏SAMPLE_DEPTH_TEXTURE,用来处理这些由于平台差异造成的问题。而只需要在Shader中使用SAMPLE_DEPTH_TEXTURE宏对深度纹理进行采样,如:

float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);

其中,i.uv 是一个float2 类型的变量,对应了当前像素的纹理坐标,类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJSAMPLE_DEPTH_TEXTURE_LODSAMPLE_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) 得到的屏幕坐标。这些宏的定义,可在Unity内置的 HLSL.Support.cginc 文件中找到。
当通过纹理采样得到深度值后,这些深度值往往都是非线性的,这种非线性来自于透视投影使用的裁剪矩阵。然而,在计算过程中通常需要线性的深度值,也就是需要把投影后的深度值变换到线性空间下,如视角空间下的深度值。下面以透视投影为例,推导如何由深度纹理中的深度信息计算得到视角空间下的深度值。
当使用透视投影的裁剪矩阵Pclip 对视角空间下的一个顶点进行变换后,裁剪空间下顶点的 z 和 w 分量为:

在这里插入图片描述
深度纹理中的深度值是通过下面的公式由NDC计算而得的:
在这里插入图片描述
幸运的是,Unity 提供了两个辅助函数来进行上述的计算过程——LinearEyeDepthLinear01DepthLinearEyeDepth负责把深度纹理的采样结果转换到视角空间下的深度值,也就是上面得到的取反后的结果。
而Linear01Depth 则会返回一个范围在[0,1]的线性深度值,就是上面得到的Z01,这两个函数内部使用了内置的 _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]的线性深度值,而得到的法线则是视角空间下的法线方向。同样,可通过调用 DecodeFloatRGDecodeViewNormalStereo 来解码深度+ 法线纹理中的深度和法线信息。
可自行在片元着色器中输出转换或解码后的深度和法线值,使用类似下面的代码来输出线性深度值:

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);

2. 再谈运动模糊

一种生成速度映射图的方法。该方法利用深度纹理在片元着色器中为每个像素计算其在世界坐标的位置,这是通过使用当前的视角* 投影矩阵的逆矩阵对NDC下的顶点坐标进行变换得到的,当得到世界空间中的顶点坐标后,使用前一帧的视角*投影矩阵对其进行变换,得到该位置在前一帧中的NDC坐标。然后,计算前一帧和当前帧的位置差,生成该像素的速度。这种方法的优点是可在一个屏幕后处理中完成整个效果的模拟,但缺点是需要在片元着色器中进行两次矩阵乘法的操作,对性能有所影响。

(1)1. 声明脚本MotionBlurWithDepthTextureTest.cs把给脚本拖到摄像机上。
在这里插入图片描述
2. 实现 OnRenderImage 函数:
在这里插入图片描述
(2)Shader部分
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最终效果

请添加图片描述

全局雾效

雾效(Fog) 是游戏里经常使用的一种效果。Unity 内置的雾效可以产生基于距离的线性或指数雾效。需要在Shader 中添加#pragma multi_compile_fog 指令,同时还需要使用相关的内置宏,例如UNITY_FOG_COORDS、UNITY_TRANSFER_FOG 和 UNITY_APPLY_FOG等。这种方法的缺点在于,不仅需要为场景中所有物体添加相关的渲染代码,而且能够实现的效果也非常有限。
本节将学习一种基于屏幕后处理的全局雾效的实现。使用这种方法,不需要更改场景内渲染的物体所使用的Shader代码,而仅仅依靠一次屏幕后处理的步骤即可。这种方法的自由性很高,方便地模拟各种雾效,均匀的雾效、基于距离的线性/指数雾效、基于高度的雾效等。
本节将会学习一个快速从深度纹理中重建世界坐标的方法。这种方法首先对图像空间下的视锥体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后,把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。得到世界坐标后,就可以轻松地使用各个公式来模拟全局雾效了。

重建世界坐标

首先来了解如何从深度纹理中重建世界坐标。坐标系中的一个顶点坐标可通过它相对于另一个顶点坐标的偏移量来求得。重建像素的世界坐标也是基于这样的思想,只需要知道摄像机在世界空间下的位置,以及世界空间下该像素相对于摄像机的偏移量,把它们相加就可得到该像素的世界坐标。
float4 worldPos=_WorldSpaceCameraPos + linearDepth * interpolatedRay;
其中,linearDepth * interpolatedRay 可以计算得到该像素相对于摄像机的偏移量,linearDepth 是由深度纹理得到的线性深度值,interpolatedRay 是由顶点着色器输出并插值后得到的射线,它不仅包含了该像素到摄像机的方向,也包含了距离信息。interpolateRay 来源于对近裁剪平面的4个角的某个特定向量的插值,这4个向量包含了它们到摄像机的方向和距离信息,可利用摄像机的近裁剪平面距离、FOV、横纵比计算而得。显示了计算时使用的一些辅助向量。先计算两个向量——toTop 和 toRight,它们是起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量。它们的计算公式如下:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

雾的计算

在简单的雾效实现中,需要计算一个雾效系数f,作为混合原始颜色和雾的颜色的混合系数:

float3 afterFog = f * fogColor + (1-f) * origColor;

这个雾效系数 f 有很多计算方法。在Unity内置的雾效实现中,支持三种雾的计算方式——线性(Linear)、指数(Exponential)以及指数的平方(Exponential Squared)。当给定距离z后,f 的计算公式分别如下:
在这里插入图片描述

实现

(1)

  1. 声明脚本FogWithDepthTextureTest.cs把给脚本拖到摄像机上。
    在这里插入图片描述

  2. OnRenderImage的实现
    在这里插入图片描述

(2)Shader部分

  1. 设置参数
    在这里插入图片描述
  2. 顶点着色器
    在这里插入图片描述
  3. 片元着色器
    在这里插入图片描述

最终效果

请添加图片描述

再谈边缘检测

上期介绍如何使用 Sobel 算子对屏幕图像进行边缘检测,实现描边的效果。但是,直接利用颜色信息进行边缘检测的方法会产生很多不希望得到的边缘线。
本节将学习如何在深度和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息,通过这样的方式检测出来的边缘更加可靠。
将使用 Roberts 算子进行边缘检测。卷积核如下:
在这里插入图片描述
Roberts 算子的本质就是计算左上角和右下角的差值,乘以右上角和左下角的差值,作为评估边缘的依据。在下面的实现中,也会按这样的方式,取对角方向的深度或法线值,比较它们之间的差值,若超过某个阈值,就认为它们之间存在一条边。

(1)

  1. 声明脚本FogWithDepthTextureTest.cs把给脚本拖到摄像机上。
    在这里插入图片描述
  2. 实现OnRenderImage函数,把各个参数传递给材质:
    在这里插入图片描述
    (2)Shader部分
  3. 参数声明
    在这里插入图片描述
  4. 顶点着色器:
    在这里插入图片描述
  5. 片元着色器:
    在这里插入图片描述
    4.最后
    在这里插入图片描述

最终效果

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42050609/article/details/124969602