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

news2024/11/27 10:28:56

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 + ToRight + ToTop

同理:

O_LU = Camera.Forward * Near - ToRight + ToTop
O_LD = Camera.Forward * Near - ToRight - ToTop
O_RD = Camera.Forward * Near + ToRight - 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 ) * Scale

WorldPos = WorldSpaceCameraPos + ScaledRay * LinearEyeDepth

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

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

相关文章

实现Redis和数据库数据同步问题(JAVA代码实现)

这里我用到了Redis当中的发布订阅模式实现(JAVA代码实现) 先看图示 下面为代码实现 首先将RedisMessageListenerContainer交给Spring管理. Configuration public class redisConfig {AutowiredRedisConnectionFactory redisConnectionFactory;AutowiredQualifier("car…

《精通ChatGPT:从入门到大师的Prompt指南》附录A:常用Prompt示例

附录A:常用Prompt示例 在《精通ChatGPT:从入门到大师的Prompt指南》的附录A中,我们将展示一系列常用的Prompt示例,帮助读者更好地理解和应用Prompt技术。每个示例将包含Prompt的描述、使用场景、预期结果以及实际输出。希望这些示…

Springboot+Vue的网上购物商城系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 角色对应功能 用户商家 功能截图

二、Nginx原来是这样?(系列篇02)

二、Nginx原来是这样?(系列篇02) 大家好,我是秋意零。 今天分享Nginx系列篇的第二节。Nginx目录结构、运行原理、基本配置。 更多请关注,Nginx系列篇主页:https://mp.weixin.qq.com/mp/appmsgalbum?__b…

MFA 轰炸:苹果用户的攻击目标

一些 Apple (苹果) 用户报告了利用密码重置功能进行的网络钓鱼攻击。 你注意到 iPhone 上的系统提示你输入密码。你点击“不允许”。然后这种情况一次又一次地发生。 在某个时候,你可能会感到恼火或开始恐慌,然后点击“允许”。 然后,你接…

[羊城杯 2023]CSGO

主函数初始化时,有反调试检测 打个断点在前面,然后nop掉 go语言的主函数是main_main 直接来到main_main,发现能可能是base64变表 在前面打个断点,F9 此处为base64变表,来到v25的地址处 得到变表LMNOPQRSTUVWXYZab…

【云原生】Kubernetes----轻量级的现代HTTP反向代理和负载均衡器之Traefik

目录 引言 一、Traefik基本概念 (一)什么是Ingress (二)什么是Traefik (三)Traefik和Nginx的区别 1.设计目标 2.配置语言 3.容器支持 4.功能特性 二、安装部署Traefik (一&#xff0…

Redis实战篇02

1.分布式锁Redisson 简单介绍: 使用setnx可能会出现的极端问题: Redisson的简介: 简单的使用: 业务代码的改造: private void handleVoucherOrder(VoucherOrder voucherOrder) {Long userId voucherOrder.getUserI…

NGINX之location和rewrite

一.NGINX常用的正则表达式 二.Location location作用:对访问的路径做访问控制或者代理转发 1.location 常用的匹配规则: 进行普通字符精确匹配,也就是完全匹配^~ / 表示普通字符匹配。使用前缀匹配。如果匹配成功,则不再匹配其它 …

QGraphicsView实现简易地图20『鹰眼视图-全图显示』

前文链接:QGraphicsView实现简易地图19『迁徙图』 鹰眼视图-全图显示 能够显示所有已加载的瓦片地图,支持当前视口的范围显示器。鼠标在鹰眼视图上移动时,支持是否干预主视图地图加载两种模式,即移动时是否让主视图加载空白处的瓦…

无头+单向+非循环链表的实现

这里写目录标题 1. 链表1.1 链表的概念及结构1.2 链表的分类 2. 接口实现3. 链表的实现3.1 打印链表3.2 头插3.3 尾插3.4 头删3.5 尾删3.6 单链表查找3.7 在pos之前插入3.8 在pos之后插入3.9 删除pos位置的值3.10 删除pos位置之后的值3.11 链表的释放3.12 动态申请一个节点 4. …

基于SVPWM矢量控制的无速度传感器电机控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 基于SVPWM矢量控制的无速度传感器电机控制系统simulink建模与仿真,包括电机,SVPWM模块,矢量控制器模块等。 2.系统仿真结果 3.核心程序与模…

【Java 百“练”成钢】Java 基础:带参数的方法

Java 基础:带参数的方法 01.求和02.字符串输出03.寻找最大值04.寻找最小值05.字符串拼接06.求平均值07.数组排序08.累乘09.存在的字符串10.长整型求和11.寻找字符串索引12.字符串拼接(StringBuilder) 01.求和 public class SumCalculator {s…

各类电机数学模型相关公式总结 —— 集成芯片驱动

0、背景技术概述 永磁直流电机(PMDC)、永磁同步电机(PMSM)、无刷直流电机(BLDC)以及混合式两相步进电机在小功率应用场景中多采用集成芯片驱动(如二合一、三合一驱动芯片)的原因主要…

python 多任务之多线程

多线程 线程是程序执行的最小单位,实际上进程只负责分配资源,而利用这些资源执行程序的是线程,也就是说进程是线程的容器,一个进程中最少有一个线程来负责执行程序,它可以与同属一个进程的其它线程共享进程所拥有的全…

前端使用轮播图的方法有哪些

前端使用轮播图的方法可以使用swiper:Swiper中文网-轮播图幻灯片js插件,H5页面前端开发 这是swiper官网,在官网里面可以找到很多轮播图的实际案例: 我们挑选可用的案例或者修改的案例,打开后打开源码,就可以获取到当前的源码了,加以调试就可以获得我们需要的结果, 例如: 上图…

openai 前员工释放出关于AGI的前世今生和未来发展趋势的详细报告

目录 1.引言2.AGI的临近3.投资与工业动员4.国家安全与AI竞赛5.技术挑战与机遇6.项目与政策7.结语8.原文PDF链接PS.扩展阅读ps1.六自由度机器人相关文章资源ps2.四轴机器相关文章资源ps3.移动小车相关文章资源 1.引言 2024年,我们站在了一个全新的科技前沿。在这篇文…

LabVIEW电机槽楔松动声测系统

LabVIEW电机槽楔松动声测系统 开发了一种利用LabVIEW软件和硬件平台,为大型电机设计的槽楔松动声测系统。该系统通过声波检测技术,实现了对电机槽楔是否松动的快速准确判断,极大地提高了检测效率和安全性。 项目背景 大型电机在运行过程中…

python-微分方程计算

首先导入数据 import numpy as np from scipy.integrate import odeint from scipy.optimize import minimize import matplotlib.pyplot as pltdata np.array([[30, 4],[47.2, 6.1],[70.2, 9.8],[77.4, 35.2],[36.3, 59.4],[20.6, 41.7],[18.1, 19],[21.4, 13],[22, 8.3],[2…

字符串形成树形

字符串形成树形 有的时候我们形成树形不是以ID的关系进行匹配的而是以字符串进行形成 数据 CREATE TABLE `contract_main_org_info` (`id` bigint(20) NOT NULL COMMENT 组织单位id,`parent_id` int(11) NULL DEFAULT NULL COMMENT 父组织单位id,`org_name` varchar(255) CHA…