【Unity Shader入门精要 第13章】使用深度和法线纹理(一)

1. 原理

深度纹理的本质是一张RenderTexture,只不过其中记录的不是颜色值,而是一个深度值

这些深度值来自于顶点在空间变换后得到的归一化设备坐标(NDC)的Z值

由于NDC坐标的分量取值范围在[-1, 1]之间,要使颜色值能够覆盖所有范围,需要对其进行映射:d = (ZNDC + 1) / 2

  • 当 d 为0时,距离摄像机最近,此时位于近剪裁面上
  • 当 d 为1时,距离摄像机最远,此时位于远剪裁面上

2. 数据来源

在延迟渲染中,由于第一个 Pass 会将深度/法线等信息都渲染到 G-Buffer 中,因此对于延迟渲染来讲,要生成深度纹理,可以直接从G缓冲区中读取数据

在前向渲染中,没有生成 G-Buffer 数据的过程,此时 Unity 会使用着色器替换技术,选择所有 Pass 设置了标签 “RenderType” = “Opaque” 的物体,然后检查"Queue"标签,如果该标签设置的渲染队列所对应的值小于2500,该物体就会参与深度纹理的计算,并使用一个单独的 Pass 渲染深度纹理。

也就是说,无论前向渲染还是延迟渲染,在生成深度纹理时,都需要先计算深度信息,此时Unity会查找参与深度计算的物体身上是否有“LightMode” = “ShadowCaster” 的 Pass,如果有,则使用该 Pass 进行计算,否则不计算。

如果设置的是生成深度 + 法线纹理,还会使用另外一个特定的Pass生成法线信息。

如果生成的是深度纹理,根据所用的深度缓存的精度,深度纹理的精度通常是24或16位,如果生成的是深度 + 法线纹理,Unity会创建一张和屏幕相同分辨率的32位纹理,其中,观察空间的法线写入RG通道,深度写入BA通道。

3. 获取纹理

3.1 获取深度纹理

在脚本中设置摄像机的深度纹理类型:_camera.depthTextureMode = DepthTextureMode.Depth
在Shader中声明变量:_CameraDepthTexture

3.2 获取深度+法线纹理

在脚本中设置摄像机的深度纹理类型:_camera.depthTextureMode = DepthTextureMode.DepthNormals
在Shader中声明变量:_CameraDepthNormalsTexture

4. 采样纹理

4.1 采样深度纹理

可以通过tex2D对深度纹理直接进行采样,Unity也提供了一系列采样深度纹理的方法,通过使用这些方法,可以兼容各个平台的差异

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

我们上面说过,深度纹理中存储的是NDC坐标映射到[0, 1]范围内的值,我们这里可以把它大体等同于NDC坐标来分析。NDC坐标是怎么来的呢?是观察空间内的坐标先经过投影变换,然后除以w得到的。投影变换的矩阵 ( Mfrustum ) 如下:
( X X X 0 0 0 0 Y Y Y 0 0 0 0 − ( F a r + N e a r ) / ( F a r − N e a r ) − 2 ( F a r ∗ N e a r / ( F a r − N e a r ) ) 0 0 − 1 0 ) \left( \begin{matrix} XXX & 0 & 0 & 0\\ 0 & YYY & 0 & 0\\ 0 & 0 & -(Far + Near)/(Far - Near) & -2(Far * Near/(Far - Near))\\ 0 &0&-1&0 \end{matrix} \right) XXX0000YYY0000(Far+Near)/(FarNear)1002(FarNear/(FarNear))0
假设观察空间内有一点Pview = (Xview, Yview, Zview),我们用 Mfrustum * Pview 即可得到该点在齐次裁剪空间下的对应坐标Pclip = (Xclip, YClip, Zclip, WClip) = ( _, _, -(Far + Near)/(Far - Near) * Zview - 2(Far * Near/(Far - Near)), -Zview)

然后对该坐标进行齐次除法得到NDC坐标,这里我们只看Z分量: ZNDC = (Far + Near)/(Far - Near) + 2(Far * Near/(Far - Near)) * (1 / Zview

因为Far和Near都是常数,为了使式子看起来更清晰,我们用A、B代替其中常数的部分,于是得到: ZNDC = A + B / Zview

而上面通过 SMAPLE_DEPTH_TEXTURE 方法采样得到的深度值 d 就是 ZNDC 映射到 [0, 1] 区间得到的值:d = 0.5 * (A + B / Zview) + 0.5 = (0.5A + 0.5) + 0.5B / Zview

我们这里不需要关心常数的值,依然用AB代替,因此 d 也可表达成 d = A + B / Zview

可见,深度纹理(包括深度缓冲区)中记录的深度值 d 与点在观察空间中的实际深度 Zview 并不成线性关系。这就导致在实现一些效果时,直接对d插值会得到错误的结果。

比如有两个点A、B,它们在观察空间中真实的深度为ZA、ZB,转换成深度纹理中的深度值为 dA、dB,同时在AB的中间有一点C,其在观察空间的真实深度为 ZC = (ZB + ZA)/ 2,通过上面的分析我们已经知道,d 与 Zview 并不成线性关系,也就是说 C 点在深度纹理中记录的深度值 dC ≠ (dB + dA)/ 2。因此,当需要求C点的真实深度时(比如根据法线重构世界坐标),不能直接对dA、dB进行线性插值。我们需要先将 d 转换到一个线性空间中,然后在这个线性空间中再进行插值。Unity为此提供了两个方法:

  • LinearEyeDepth:将 d 转换到观察空间的线性值,由于观察空间的Z向范围是从近剪裁面到远剪裁面,因此该方法得到的值也在[Near, Far]的范围内
  • Linear01Depth:将 d 值转换到观察空间的线性值,但是结果除以了Far,因此最终值被限定到了[0, 1]的范围内

除此以外,Unity还提供了其他类似的宏方法,如SAMPLE_DEPTH_TEXTURE_PROJ 和 SAMPLE_DEPTH_TEXTURE_LOD。

4.2 采样深度+法线纹理

对于深度+法线纹理,通常直接使用 tex2D 方法对 _CameraDepthNormalsTexture 进行采样,采样得到的颜色值包括了深度和法线两部分信息,Unity提供了函数帮我们对其进行解码:

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

其中:

  • enc 为对深度 + 法线纹理的采样结果
  • depth 用于接收解码得到的深度,这个深度值为[0, 1]之间的线性值,相当于直接解码出一个 Linear01Depth 的值,因此不需要再手动处理
  • normal 用于接收解码得到的法线,该法线同样是观察空间下的法线

5. 基于深度纹理重建世界坐标的两种方式

5.1 NDC坐标逆向变换

回想【Unity Shader入门精要 第4章】数学基础(二)中提到的Unity的五个空间,对于世界空间中的一个点,经过 VP 变换后转换到齐次剪裁空间,然后通过齐次除法得到NDC坐标,最后通过屏幕映射映射到屏幕上。

第一种重建世界坐标的思路就是将上述过程逆向进行。

首先需要通过屏幕像素构建出NDC坐标。

  • 在Unity中,NDC坐标的范围在[-1, 1],我们在片元着色器中采样使用的uv坐标的范围在[0, 1],其实就是NDC坐标的XY分量经过(NDC + 1)/ 2 得到的,因此:XYNDC = 2 * XYUV - 1
  • 对深度纹理进行采样得到深度值d,上面说过,d = (ZNDC + 1) / 2,因此:ZNDC = 2*d - 1
  • NDC坐标的W分量固定为1:WNDC = 1
  • 最终得到:PNDC = ( 2 * XUV - 1, 2 * YUV - 1, 2*d - 1, 1 )

构建出NDC坐标后,就可以推导出重建世界坐标的公式,整个推导过程是建立在如下四条已知条件上的:

  • Pclip = Matrixvp * Pworld
  • XYZNDC = XYZclip / Wclip
  • WNDC = 1
  • Wworld = 1

推导过程:

  • XYZNDC = XYZclip / Wclip
    XYZclip = Wclip * XYZNDC
    Pclip = ( XYZclip, Wclip ) = ( Wclip * XYZNDC, Wclip )

  • 由 Pclip = Matrixvp * Pworld 可得:
    Matrixvp -1 * Pclip = Pworld
    Matrixvp -1 * ( Wclip * XYZNDC, Wclip ) = Pworld
    Wclip * Matrixvp -1 * ( XYZNDC, 1 ) = Pworld

  • 由于 WNDC = 1,因此:
    Wclip * Matrixvp -1 * ( XYZNDC, 1 ) = Pworld
    Wclip * Matrixvp -1 * ( XYZNDC, WNDC ) = Pworld
    Wclip * Matrixvp -1 * PNDC = Pworld

  • 我们只看W分量:
    Wclip * ( Matrixvp -1 * PNDC ).W = Wworld = 1 ➡
    Wclip = 1 / ( Matrixvp -1 * PNDC ).W

  • 将Wclip代入上面标黄的式子得到:
    Matrixvp -1 * PNDC / ( Matrixvp -1 * PNDC ).W = Pworld

最终得到: Pworld = Matrixvp -1 * PNDC / ( Matrixvp -1 * PNDC ).W

5.2 射线插值

射线插值重建像素世界坐标的原理基于下图:

在这里插入图片描述
对于屏幕上的一点P’,假设其对应的3D空间中的真实点的位置为P,则P点的位置可以通过摄像机的位置O加上向量OP来求得:

P = O + OP

O可以直接通过 _WorldSpaceCameraPos 变量获得,那么如何获得OP向量呢?

可以看到,上图中的黄色虚线部分是两个相似三角形,根据相似三角形的性质可知:

OP = Ray * LinearEyeDepth / Near

其中 LinearEyeDepth 可以通过深度纹理获得,Near为摄像机近剪裁面距离,也可以通过摄像机获得,于是问题只剩下求Ray向量。

首先我们想一下,屏幕后处理中处理的是什么?

屏幕后处理所处理的对象,是当前摄像机渲染的 RenderTexture,其实就是一个由四个顶点、两个三角面构成的四边形网格,如下图所示:

在这里插入图片描述
在屏幕后处理引用的 Shader 中,顶点着色器要处理的只有上图中 LeftUp、LeftDown、RightDown、RightUp 四个顶点。

那 P’ 又是什么?
在这里插入图片描述
P’ 是在片元着色器中处理的一个片元,它对应的是某个三角面覆盖的一个像素,如上图所示。我们在顶点着色器中并没有(也没有办法)对 P’ 直接设置数据,但是在片元着色器中依然可以获得 P’ 的uv坐标、法线等信息。之所以 P’ 有这些信息,是因为我们为每个顶点设置了这些信息,并且将这些信息放到了 v2f 结构的各种插值寄存器中(v2f 中定义的各种字段)。在后续三角形遍历阶段,引擎发现 P’ 被 LeftUp、RightDown 和 RgihtUp 三个顶点围成的三角面覆盖到了,然后就会将三个顶点插值寄存器中的各种数据进行插值,计算出 P’ 点对应每个字段的值。

所以摄像机到 P’ 的射线可以通过摄像机到LeftUp、RightDown 和 RgihtUp三个顶点的射线插值获得(下方三角面同理),于是问题又变成求摄像机到四个顶点的射线。

摄像机到四个顶点的射线很好求,就是向量的加减乘除:

在这里插入图片描述

上图蓝色四边形代表摄像机的近剪裁面,ToRight 和 ToTop分别表示近剪裁面中心到最右边和最上边的向量,则从摄像机到近剪裁面右上角的向量:

O_RU = Camera.Forward * Near + RoRight + ToTop

同理:

O_LU = Camera.Forward * Near - RoRight + ToTop
O_LD = Camera.Forward * Near - RoRight - ToTop
O_RD = Camera.Forward * Near + RoRight - ToTop

在这里插入图片描述
注意,与上面一张图不同,这张图里紫线表示的是距离而不是向量,根据图中所示,定义:

HalfHeight = | ToTop | = Near * Tangent(Fov / 2)

则:

ToTop = Camera.Up * HalfHeight 
ToRight = Camera.Right * HalfHeight  * aspect

将 ToTop 和 ToRight 代入即可求出O_RU,同理还可求出 O_LU、O_LD、O_RD

然后我们再看一下最初要求的射线Ray:

OP = Ray * LinearEyeDepth / Near

这一部分是需要在片元着色器中逐像素计算的,为了节省性能,可以把式子中 Ray/Near 的部分合并成一个 ScaledRay,也就是说我们提供给顶点着色器的就是一个经过了( /Near) 处理的射线。

最终,整理一下涉及到的代码

HalfHeight = Near * Tangent(Fov / 2)
ToTop = Camera.Up * HalfHeight 
ToRight = Camera.Right * HalfHeight  * aspect
Scale = 1 / Near
Scaled_O_LD = ( Camera.Forward * Near - ToRight - ToTop ) * Scale
Scaled_O_RD = ( Camera.Forward * Near + ToRight - ToTop ) * Scale
Scaled_O_RU = ( Camera.Forward * Near + ToRight + ToTop ) * Scale
Scaled_O_LU = ( Camera.Forward * Near - ToRight + ToTop ) * ScaleWorldPos = WorldSpaceCameraPos + ScaledRay * LinearEyeDepth

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/23195.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于pytorch的车牌识别

🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 一、导入数据 from torchvision.transforms import transforms from torch.utils.data import DataLoader from torchvision import datase…

在Jenkins 中使用 NVM 管理 Node.js 部署项目的自动化脚本

在Jenkins 中使用 NVM 管理 Node.js 部署项目的自动化脚本 人生旅途,总有人不断地走来,有人不断地离去。当新名字变成老名字,当老的名字渐渐模糊,又是一个故事的结束和另一个故事的开始。 在现代软件开发中,持续集成/持…

容器化实践:DevOps环境下的容器交付流程

DevOps的兴起是为了应对市场和消费者对技术应用的不断增长的需求。它的目标是构建一个更快的开发环境,同时保持软件的高质量标准。DevOps还致力于在敏捷开发周期中提升软件的整体品质。这一目标的实现依赖于多种技术、平台和工具的综合运用。 结合容器化技术与DevO…

深入理解mysql中的各种超时属性

1. 前言 connectTimeout: 连接超时 loginTimeout: 登录超时 socketTimeout: Socket网络超时,即读超时 queryTimeout: sql执行超时 transactionTimeout:spring事务超时 innodb_lock_wait_timeout:innodb锁等待超时 wait_timeout:非交互式连接关闭前的等待时间 inter…

uniapp小程序多线程 Worker 实战【2024】

需求 最近遇到个小程序异步解码的需求,采用了WebAssembly,涉及大量的计算。由于小程序的双线程模型只有一个线程处理数据,因此智能寻求其它的解决方案。查看小程序的文档,发现小程序还提供一个异步线程的Worker方案,可…

联想Y410P跑大模型

安装vs 2017 查看GPU版本 查看支持哪个版本的cuda windows cuda更新教程_cuda 12.0-CSDN博客 下载并安装cuda tookit 10.1 CUDA Toolkit 10.1 Update 2 Archive | NVIDIA Developer 找到下载的文件,安装 参考安装链接 Win10 Vs2017 CUDA10.1安装(避坑…

InnoDB存储引擎非常重要的一个机制--MVCC(多版本并发控制)

Mysql是如何实现隔离性的?(锁MVCC) 隔离性是指一个事务内部的操作以及操作的数据对正在进行的其他事务是隔离的,并发执行的各个事务之间不能相互干扰。隔离性可以防止多个事务并发执行时,可能存在交叉执行导致数据的不…

安全U盘和普通U盘有什么区别?

安全U盘(也称为加密U盘或安全闪存驱动器)与普通U盘肯定是有一些区别的,从字面意思上来看,就能看出,安全U盘是能够保护文件数据安全性的,普通U盘没这一些功能的,可随意拷贝文件,不防盗…

面试4:c++(数位物联)

1.const 关健字的作用 定义常量,防止变量被意外修改,增强程序的可读性和维护性。 可以用于指针,声明指向常量的指针或常量指针。 2.static关健字的作用 (1)在函数内,用于修饰局部变量,使其生命周期延长到整个程序运行期…

一文了解UVLED线光源的应用

在机器视觉系统中,光源作为不可或缺的一部分,能够提高目标成像效果,增强检测效果。光源的选择至关重要,选到不合适的会影响成像及检测效果。针对不同的检测对象,不同的形状光源应运而生。我们来看看最UVLED线光源。 下面以CCS的光…

zoomeye api报错 request invalid, validate usage and try again

项目场景: 调用zoomeye的api接口进行数据拿取 问题描述 之前接口一直通着今天突然报错,以下为源代码 pip install zoomeye from zoomeye.sdk import ZoomEye zm ZoomEye(api_key"34A8B452-D874-C63E0-8471-F3D4f89766f") zm.dork_search(a…

图片像素缩放,支持个性化自定义与精准比例调整,让图像处理更轻松便捷!

图片已经成为我们生活中不可或缺的一部分。无论是社交媒体的分享,还是工作文档的编辑,图片都扮演着至关重要的角色。然而,你是否曾经遇到过这样的问题:一张高清大图在上传时却受限于平台的大小要求,或者一张小图需要放…

tkinter+火山引擎+python实现语音识别聊天机器人

想要做一款能通过语音识别来聊天的智能机器人,首先需要能通过麦克风录制语音进行识别转换成文字,将文字发送给机器人得到聊天结果,并能将返回的文字转换成语音进行合成,之后再通过本地播放语音实现语音交互。 架构: 实现步骤 一、本地录音 本地录音可以通过pyAudio库实…

2024-06-05-记一次cnvd渗透

前言:挖src挖郁闷了,闲来无事选择挖一个cnvd来练练手,本次的漏洞都没啥难度,企查查资产过了5000万 说一下cnvd证书的下放标准 对于中危及中危以上通用型漏洞(CVSS2.0基准评分超过4.0分),以及涉…

红酒:红酒保存中的光照与避免阳光直射

在红酒保存中,光照是一个常常被忽视的因素。光照对红酒的影响是不可小觑的,因为阳光中的紫外线会加速红酒的氧化,导致其口感和品质的下降。因此,在保存云仓酒庄雷盛红酒时,应特别注意避免阳光直射。 阳光直射对红酒的影…

企业代码签名证书1300元

随着手机和电脑等设备的普及,越来越多的开发者进入软件行业,为了软件的安全性、完整性和可信度,开发者往往会使用由正规CA认证机构颁发的代码签名证书对软件代码进行数字签名,来标识软件的来源和软件开发者的真实身份。今天就随SS…

博物馆文物库房管理软件

博物馆作为文化遗产的守护者和传承者,承载着人类智慧与文明的结晶。在博物馆的背后,一个庞大而严密的管理系统支撑着文物的保护与展示。而其中,文物库房管理软件的使用,无疑是一项重要的管理工具。 文物库房管理软件的功能具有多样…

【CentOS 7】挑战探索:在CentOS 7上实现Python 3.9的完美部署指南

【CentOS 7】挑战探索:在CentOS 7上实现Python 3.9的完美部署指南 大家好 我是寸铁👊 总结了一篇【CentOS 7】挑战探索:在CentOS 7上实现Python 3.9的完美部署指南详细步骤✨ 喜欢的小伙伴可以点点关注 💝 前言 此篇教程只适用于p…

【Mybatis】源码分析-高级应用

1、Mybatis配置文件深入理解 1.2、动态SQL语句 Mybatis 的映射⽂件中,前⾯我们的 SQL 都是⽐较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化的,此时在前⾯的学习中我们的 SQL 就不能满⾜要求了。 1.2.1、条件判断 我们根…

技巧:合并ZIP分卷压缩包

如果ZIP压缩文件文件体积过大,大家可能会选择“分卷压缩”来压缩ZIP文件,那么,如何合并zip分卷压缩包呢?今天我们分享两个ZIP分卷压缩包合并的方法给大家。 方法一: 我们可以将分卷压缩包,通过解压的方式…