unity 深度纹理和法线纹理

原理

在实现某些屏幕后处理的时候,仅仅只有图像的rgb信息是很难实现的,同简单的图像处理不同的是,我们能在shader中获得图像每个像素的深度信息和法线信息。
想要理解深度纹理的原理就必须要对渲染流水线有个比较深入的了解,深度纹理和法线纹理通过unity底层使用的一个单独的pass来把整个场景再次渲染一遍,保存在_CameraDepthTexture里面,其中RG通道存储法线信息,BA通道存储深度信息。
由于经过了非线性的投影变换,我们的深度信息也不是线性的,如果需要得到线性的深度信息,我们需要对其进行手动转换,这过程就涉及到渲染流水线中的投影变换。
在这里插入图片描述观察一下我们的投影矩阵,很容易得到裁剪空间的z坐标与w坐标
在这里插入图片描述
深度纹理存储的是映射到(0,1)范围的NDC,因此有
在这里插入图片描述
上面那些公式很容易能推导出来观察空间的深度的表达式
在这里插入图片描述

但是unity摄像机正向对应的z为负值,为了得到正数的表示,我们把上面的结果取反,就能得到最后的结果了。
在这里插入图片描述

因为这个z是真实的距离,因此其范围是近裁剪面到远裁剪面,也就是[near,far],如果想要把它映射到0~1之间,只需要除以一个far就行了,
在这里插入图片描述
上面的结果就是映射到01空间的线性深度值。unity的函数LinearEyeDepth和Linear01Depth分别就是计算真实深度和映射到01的深度的函数。

2.实现

下面三张图片,第一张图片是原图,第二张图片是其深度纹理,第三张图片是法线纹理。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述其输出过程是非常简单的,但是得到这两张纹理的方法确实后面的一些高级的屏幕特效所需要的。
深度纹理的获取方法:
1.摄像机设置好正确的模式
Camera.main.depthTextureMode = DepthTextureMode.DepthNormals;
2.声明正确命名的变量
sampler2D _CameraDepthTexture;
3.解码
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
float liearDepth = Linear01Depth(depth);
法线纹理的获取方法
1.摄像机设置好正确的模式
Camera.main.depthTextureMode = DepthTextureMode.DepthNormals;
2.声明正确命名的变量
sampler2D _CameraDepthNormalsTexture
3.解码
fixed3 normal = DecodeViewNormalStereo(tex2D(_CameraDepthNormalsTexture, i.uv));
获取上面的图片只需要简单的输出他们即可

3.系统函数解析

inline float Linear01Depth( float z )
{
    return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

上面的两个函数看起来好像和我们上面描述的公式不大一样,其中重点可能就是_ZBufferParams,我们再来看一看_ZBufferParams的源码

// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
// or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1)
// x = -1+far/near
// y = 1
// z = x/far
// w = 1/far
float4 _ZBufferParams;

把_ZBufferParams的各个分量代入到上面的公式里面,得到的结果正是我们上面推导出来的~

inline float3 DecodeViewNormalStereo( float4 enc4 )
{
    float kScale = 1.7777;
    float3 nn = enc4.xyz*float3(2*kScale,2*kScale,0) + float3(-kScale,-kScale,1);
    float g = 2.0 / dot(nn.xyz,nn.xyz);
    float3 n;
    n.xy = g*nn.xy;
    n.z = g-1;
    return n;
}

乍一看这个函数有点超出我的理解范围了…
但是查阅了一些资料,对它还是有了那么一点理解。上面的函数是解码函数,不妨我们从编码函数开始理解

inline float2 EncodeViewNormalStereo( float3 n )
{
    float kScale = 1.7777;
    float2 enc;
    enc = n.xy / (n.z+1);
    enc /= kScale;
    enc = enc*0.5+0.5;
    return enc;
}

函数的作用就是把3维的法线映射到2维且0~1的坐标下,这里使用的方法叫做球极投影,
在这里插入图片描述P就是我们的法线坐标,N是(0,0,1),然后把法线和N连线,在XOY平面上的投影就是最后的坐标了,根据相似三角形的关系,我们很快就能得到
x’ = x/(1+z) ,y’ = y/(1+z),如果P和N在同一侧,当然p’可能就不在圆内,不过一想,这样的点我们是看不到的,在背面剔除的时候已经没了,为了使得编码得到的纹理效果尽可能好,投影值应该除以一个缩放值之后再编码到纹理中,unity内置的是1.777,这个算是一个经验值吧,也就是16:9,然后再要把法线从(-1,1)映射到(0,1)之间,至此,编码操作的代码已经很容易理解了。
理解了编码操作之后,解码操作也只是其逆操作,首先nn把其重新映射到(-1,1)范围,然后再恢复x,y的值,最后我还是没弄懂为什么z=2/(xx+yy+z*z)-1,魔法一样,不想了,头发要紧。

发布了31 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43813453/article/details/100993467
今日推荐