Unity Shader学习记录(十)

Unity Shader学习记录(十)

  前文提到的屏幕后处理特效只是一类在渲染完成后的帧画面基础上做二次处理的特效,虽然在大部分情况下它们是可用而且足够高效的,但更多的情况下我们不仅需要当前的帧画面,还需要场景的深度和法线信息。
  一个典型的例子是边缘检测,前文中使用Sobel算子进行卷积运算来检测边缘其实并不精确,因为颜色的变化有时候并不说明真正的物体边缘,而且光照效果也会影响到边缘检测。此时如果在深度和法线信息的基础上进行边缘检测运算,其精确度会提高很多,因为深度和法线信息里没有颜色,也没有光照。
  Unity为这两种信息分别提供了对应的纹理参数,也就是将深度信息渲染到一张深度纹理中,法线信息同理;想要在Shader中使用这些纹理信息就必须事先声明。

如何获取深度和法线纹理

  在真正获取这两种纹理之前,首先看看它们的基本原理是什么。
  所谓深度纹理,顾名思义就是一张渲染纹理,它的样子和平常用于模型渲染的贴图没什么两样,但它的内容并不是一系列的颜色像素,它的每一个像素值都是一个高精度的深度值,直接对应当前渲染帧里的这一点上的深度。
  那么问题来了,这个深度值怎么得到的?这个流程其实就是引擎渲染画面的过程中的一部分,包括顶点变换以及归一化;简单来说,一个模型要被渲染到屏幕上,首先要对它的顶点做变换,也就是模型空间变换到齐次裁剪空间,Shader的顶点计算函数中常见的一行代码

o.pos = UnityObjectToClipPos(v.vertex);

就是用来干这个的。
  在这个变换的最后一步会有一个投影矩阵用于计算映射到齐次裁剪空间后的坐标值,对于透视型摄像机而言这个投影矩阵会是非线性的,而正交型摄像机的投影矩阵是线性的,这也就直接关系到最后的深度纹理是何种情况。
  当变换完成之后,深度纹理就能很轻松地得到了,只要保留齐次裁剪空间下每个顶点坐标的Z分量即可,由于此时的Z分量落在[-1,1]范围内,因此用一个公式对其进行再次映射。

d=0.5z+0.5 d = 0.5 z + 0.5

  最后得到的所有 d d 值就组成了深度纹理。
  由此可见,要得到深度纹理必须对场景做一次坐标变换,那么要得到它实际上就可以有两种方法,第一种是最简单粗暴的,单独用一个Pass来进行运算并得到深度纹理;另一种方法则是直接使用深度缓存,因为Untiy在渲染过程中会主动保存深度缓存用于后续的阴影和透明渲染。
  与此同时,需要注意的是,直接使用深度缓存作为深度纹理是有前提条件的,通常来说在延迟渲染路径中使用深度缓存是毫无问题的,但其它的路径,尤其是前向渲染路径下就不一定了。
  因此当无法获取到深度缓存时,还是要使用单独的Pass来得到深度纹理,在这个Pass中Unity会自动选择那些渲染类型标签(即SubShader内的RenderType)为Opaque的物体,判断它们的渲染队列是否小于等于2500,如果满足条件则将物体渲染到深度和法线纹理中。
  所以,正确设置渲染类型标签是使用深度和法线纹理的基础。
  在这之后,要获取深度和法线纹理就很简单了,只要通过脚本给摄像机设置一项属性即可

camera.depthTextureMode = DepthTextureMode.Depth;

  设置好这个属性后,就可以在Shader中通过声明_CameraDepthTexture变量来访问它。
  同理,还可以设置该属性为其它值来得到不同的结果,甚至使用组合参数。

camera.depthTextureMode = DepthTextureMode.DepthNormals; // 获取深度法线纹理
camera.depthTextureMode = DepthTextureMode.Depth | DepthTextureMode.DepthNormals; // 同时产生深度和深度法线纹理

  接着,在Shader中就可以对深度和法线纹理进行采样来获得信息了。绝大多数情况下,这个采样过程只需要简单地使用tex2D函数即可,但有些其他平台会有不同的需求,因此Unity提供了统一方案的宏SAMPLE_DEPTH_TEXTURE,使用方法如下

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

  但是,注意一点,当摄像机是透视类型时,这样采样得到的深度值是非线性的,如果直接参与计算可能无法得到想要的结果,因此在使用前要将采样结果变换到线性空间下,而这个过程只需要倒推顶点变换过程即可。
  倒推结果是一个用深度值 d d 来表达 zview z v i e w 的公式。

zview=1NFNFd+1N z v i e w = 1 N − F N F d + 1 N

  公式中的 N N F F 分别指摄像机视锥体的深度范围上界和下界, d d 毫无疑问就是深度值,而结果是视角空间下的深度值,因为视角空间是线性的,因此此时得到的深度值就是线性的。
  当然了,如果想得到视锥体范围不是[N,F]而是[0,1]范围的深度值,那就将上文的公式整体除以F即可
z01=1NFNd+FN z 01 = 1 N − F N d + F N

  实际使用中并不需要真的进行这些繁杂的运算,Unity提供了两个辅助函数给开发者使用,一个是LinearEyeDepth,看名字就知道这是计算视角空间下的深度值;另一个是Linear01Depth,得到一个[0,1]范围的线性深度值。
  此外如果设置了摄像机的depthTextureMode为DepthNormals,此时深度和法线信息会被记录到同一张纹理上,那么采样结果就需要进行解码,Untiy也提供了对应的解码函数DecodeDepthNormal,在UnityCG.cginc中定义。

float4 packedTex = tex2D(_CameraDepthNormalsTexture, i.uv);
float depth;
float3 normal;
DecodeDepthNormal(packedTex, out depth, out normal);

  一般而言深度和法线纹理是不可见的,因为它们总是在游戏运行过程中生成,如果想要看到它们方便调试,可以打开Unity提供的帧调试器,定位到生成深度和法线纹理的事件即可看到。当然了,此时看到的深度值都是非线性的,如果要查看线性的深度值也可以自行在片元处理函数里将其输出为颜色,这样一来屏幕上就能看见线性空间下的深度和法线纹理了。

另一种运动模糊

  在前面的文章里提到过运动模糊这种特效,通过简单的屏幕后处理可以达到一个近似的效果,但那种混合多张屏幕图像的方法既不高效,效果也差强人意。
  另一种提到的技术是速度缓冲,也就是把所有物体的速度缓存到一张纹理中,这张纹理的每个像素储存着画面上每个像素点的运动速度,然后利用这张纹理去计算模糊的大小和方向。
  如何生成速度缓冲是一个重点,比较直白的方法是让场景中每个物体自己将自己的速度渲染到缓冲纹理里,但这样一来必须要修改每个物体的Shdaer,为其添加计算速度的代码并输出到指定纹理中。
  另外一种方法是通过比较前后两帧的裁剪空间坐标差来得到具体点的速度,这种方法就不需要修改所有物体的Shader,而且可以在一次屏幕后处理中完成,缺点很明显,就是在片元计算中使用了矩阵运算,影响效率。
  接着还是老样子,先挂摄像机脚本

public class MotionBlurWithDepthTexture : PostEffectBase {
    public Shader motionBlurShader;
    private Material motionBlurMaterial;
    public Material material {
        get {
            motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
            
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值