【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 ) * Scale

WorldPos = WorldSpaceCameraPos + ScaledRay * LinearEyeDepth

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

欧盟EDPS发布首份生成式人工智能与数据安全指南解读

6月3日,欧洲数据保护监督机构(EDPS)在其官网上发布了题为《生成式人工智能与EUDPR》的指南(注:EUDPR指的是《欧盟2018/1725号条例》),这是首份适用于欧盟机构的人工智能与数据安全指南。 01 指南…

STM32 SPI驱动读取LSM6DSRTR

提示:通过SPI驱动读取传感器数据 文章目录 前言一、LSM6DSRTR二、配置步骤1.配置SPI2.引入 LSM驱动库3.结果 总结 前言 制作一个倾角传感器,通过SPI读取LSM6DSRTR的加速度数据转换为角度,不用IIC的原因是考虑IIC通讯的协议过于繁琐&#xff…

c# iText使用

引入包 用nuget安装itext和itext.bouncy-castle-adapter包: 创建pdf string path "a.pdf"; PdfWriter writer new PdfWriter(path); PdfDocument pdfDoc new PdfDocument(writer); var docnew Document(pdfDoc); Paragraph p new Paragraph(&quo…

Java装饰器模式,装饰器模式通常通过创建一个接口和一个或多个实现了该接口的类来开始,然后创建装饰器类,这些类也实现了相同的接口

1、定义一个接口Component public interface Component { void operation(); }2、创建一个实现了Component接口的简单类SimpleComponent public class SimpleComponent implements Component { Override public void operation() { System.out.println("SimpleCom…

正大国际期货:什么是主力合约?

一个期货品种,在同一时间段,会上市多个月份的合约, 由于主力合约交易量大,流动性高,一般建议新手交易主力合约。 主力合约通常指交易集中,流动性好的合约 ,即在一段时间内交易量和持仓量最大的…

java框架树结构实现(带层级、编码、排序)

1、需求 实现一个影像资料库的功能,用树结构对资料进行分类 2、数据结构 通过id、pid表示父子关系 通过code表示层级关系 通过layer表示层级 通过sort进行排序 3、实体类 package org.jeecg.modules.image.entity;import com.baomidou.mybatisplus.annotation…

交叉编译freetype

目录 一、前言 二、交叉编译 freetype 1.交叉编译安装工具链 zlib 2.交叉编译安装工具链 libpng 3.交叉编译安装工具链 freetype 4.编译测试发现错误并解决 5.上机测试 一、前言 交叉编译常见错误解决方法可看:交叉编译中常见错误解决方法_交叉编译后fail t…

DevExpress Installed

一、What’s Installed 统一安装程序将DevExpress控件和库注册到Visual Studio中,并安装DevExpress实用工具、演示应用程序和IDE插件。 Visual Studio工具箱中的DevExpress控件 Visual Studio中的DevExpress菜单 Demo Applications 演示应用程序 Launch the Demo…

基于细节增强卷积和内容引导注意的单图像去雾

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读:DEA-Net:基于细节增强卷积和内容引导注意的单图像去雾1、研究背景2、方法提出3、相关知识3.1、DEConv3.3、多重卷积的…

Springboot+druid+多数据源

背景:早期项目是springboot2.x druid 的单数据源工程,其中使用了dblink的方式进行跨数据库访问。现在客户的机房搬迁,记账的下游数据库说是搬到不同区域,dblink的方式需要长期占用资源,需要修改成直连方式。 按照AI的…

AttenFace一个基于人脸识别的实时考勤验证系统算法研究

0 、引言 论文提出了一个使用面部识别、允许实时监控考勤的考勤系统, 可以检查由于欺骗和遗漏造成的欺诈。 论文地址:https://arxiv.org/abs/2211.07582v1 1. 概述 在大学和其他机构的课堂上,通常会进行考勤。然而,这种方式往往…

工业互联网基本概念及关键技术(295页PPT)

资料介绍: 工业互联网的核心是通过工业互联网平台把设备、生产线、工厂、供应商、产品和客户紧密地连接融合起来。这种连接能够形成跨设备、跨系统、跨厂区、跨地区的互联互通,从而提高效率,推动整个制造服务体系智能化。同时,工…

2024最新华为OD机试-C/D卷 - 在线OJ使用说明

文章目录 🪐在线 OJ 入口🎧申请OD使用权限🍓在线 OJ 的使用说明OJ主界面专题系列语言支持评测结果 🪐在线 OJ 入口 🔗 2024最新华为OD机试 - 在线OJ入 🎧申请OD使用权限 本专栏配套 OJ 的为了配合考友更高…

git: 批量删除分支

环境: window11git version 2.42.0git-bash.exe window环境下: 1. 批量删除本地 git branch |grep xxx |xargs git branch -D比如: 想批量删除本地含有 release 关键字的分支: 2. 批量删除远程 git branch -r | grep xxxx | …

Qt for Android 申请摄像头权限

步骤 1. 添加用户权限 AndroidManifest.xml 中新增&#xff08;不添加后面申请选项时不弹窗&#xff09; 或者再Qt Creator中直接添加 2. Qt代码申请权限 Qt自己封装好了一些常用的权限申请&#xff0c; 详情Qt Assistant文档搜索 QPermission查看 #include <QPermi…

kafka-消费者组(SpringBoot整合Kafka)

文章目录 1、消费者组1.1、使用 efak 创建 主题 my_topic1 并建立6个分区并给每个分区建立3个副本1.2、创建生产者发送消息1.3、application.yml配置1.4、创建消费者监听器1.5、创建SpringBoot启动类1.6、屏蔽 kafka debug 日志 logback.xml1.7、引入spring-kafka依赖1.8、消费…

如何理解与学习数学分析——第二部分——数学分析中的基本概念——第7章——连续性

第2 部分&#xff1a;数学分析中的基本概念 (Concepts in Analysis) 7. 连续性(Continuity) 本章首先讨论连续性的直观概念&#xff0c;并介绍与早期数学中常见的函数不同的函数。解释了连续性的定义&#xff0c;并演示了如何使用它来证明函数在一点上连续&#xff0c;以及证…

K210视觉识别模块学习笔记5:(嘉楠)训练使用模型_识别人脸

今日开始学习K210视觉识别模块:(嘉楠)训练与使用模型_识别人脸 亚博智能的K210视觉识别模块...... 固件库版本: canmv_yahboom_v2.1.1.bin 之前的训练网址部署模型时需要我们自己更换固件&#xff0c;而且还不能用亚博的图像操作库函数了&#xff0c;这十分不友好&#xff0…

【Python系列】Python 方法变量参数详解

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

C++第二十四弹---从零开始模拟STL中的list(上)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、基本结构 2、基本函数实现 2.1、默认构造函数 2.2、尾插数据 3、迭代器的封装 3.1、迭代器的基本结构 3.2、迭代器重载函数的实现 4、迭…