GAMES101课程学习笔记—Lec 13~14(1):Ray Tracing(1) Whitted风格光线追踪

1 引入——Shadow Mapping 阴影贴图

之前提到光栅化的着色,我们知道这是一种局部的现象
着色的过程中,我们只会考虑着色点自己,光源,以及摄像机
我们不考虑其他物体,甚至不考虑物资自身的其他部分对着色点的影响
而事实上是会有遮挡的关系的,是会有阴影的
之前我们解决不了阴影问题,现在来试着在光栅化的范围里面解决一下

  • 一种解决方法即为Shadow Mapping
    在这里插入图片描述
  • 本质上Shadow Mapping这个方法是一种图像空间的做法,也就是在生成阴影这一步,我们不需要知道场景的几何信息(会在之后的实现步骤中讲述)
    同时这个方法也会产生走样问题,并且经典的Shadow Mapping只能处理点光源(接下来也会以点光源为例进行讲解)
    这个方法的关键思想在于——如果有点不在阴影里,那么这个点可以被摄像机和光源都看到
    从这个思想我们看出阴影应该会有很明显的边界,也就是硬阴影
    在这里插入图片描述

1.1 阴影贴图原理

  • 首先我们从光源看向场景,做一遍光栅化,我们就会得到光源能看到什么,得到一幅图
    在这里插入图片描述
  • 我们不进行着色,把这个图的深度记下来
    在这里插入图片描述
  • 我们从摄像机出发,再次看向这个场景

在这里插入图片描述

  • 我们把现在看到的点,投影回光源刚才看到的投影平面上
    然后把 现在看到的点 和 投影回去光源得到的位置 这两个进行深度比较
    如果深度一致,那就可以被看到
    在这里插入图片描述
  • 如果深度不一致,那就看不到
    在这里插入图片描述

下面举例

  • 我们做Shadow Mapping的一个结果
    在这里插入图片描述
  • 从光源视角看到的图,以及深度
    在这里插入图片描述
    在这里插入图片描述
  • 从相机看过去,做完测试的结果
    看着很脏,为什么呢?这也是这个方法存在的问题之一
    浮点数的相等比较存在精度问题
    人们处理精度的方法很多,但是都不能本质解决问题
    还有一个问题是,一开始我们从光源看向场景,我们要把它存到一个图里面
    这个图本身存在分辨率,它与渲染时的分辨率的搭配不好的话,会存在走样
    更大的深度图的分辨率,开销也会变大
    在这里插入图片描述
  • 即使存在问题,依然是目前的主流方法
    在这里插入图片描述

1.2 存在的问题

  • 总结一下提到的问题
    硬阴影(点光源)
    阴影图的分辨率
    浮点数的比较
    所以从光栅化的思想去做全局的现象,是比较困难的事情
    在这里插入图片描述
  • 关于软硬阴影的问题
    其实就是本影与半影的关系
    如果有软阴影,一定是因为光源有一定的大小
    在这里插入图片描述

2 Why Ray Tracing?

  • 光栅化不好做全局的效果
    比如软阴影,光泽反射,间接光照
    有一些巧妙的方法可以处理,但是不能保证正确性
    在这里插入图片描述
  • 光栅化很快速,但是质量不高
    在这里插入图片描述
  • 光线追踪是很准确的,但是会比较慢
    光栅化很容易做到实时,光线追踪经常做离线的应用
    在这里插入图片描述

3 基本光线跟踪算法

3.1 光线定义

  • 我们首先需要对光线定义
    光沿直线传播,不发生碰撞,是从光源到人眼的
    对于第三个性质,我们在根据光路可逆性,应用时会采取从人眼到光源的方法
    在这里插入图片描述

3.2 Ray Casting 光线投射

光线追踪既然是追踪,我们会从终点开始,也就是从眼睛/相机开始
我们首先需要做的是光线投射

  • 我们假设往虚拟的世界中看,眼前放了一个成像平面,成像平面被我们画成不同的像素格子
    对于每一个像素,我们可以从相机连一条线,穿过这个像素,这样就可以打出一根光线,可以打到场景中
    如果光线和场景的某一物体相交,那么交点和光源连线,看光源是否可见这个点(这个点在不在阴影里),如果可见,那么就形成一条有效的光路
    那么就可以计算这条光路上的能量,进行着色
    在这里插入图片描述
  • 在下面的例子中,我们永远考虑眼睛是一个针孔摄像机,即眼睛是一个点,一个位置,不考虑实际相机的处理,以及镜头什么的(这部分会在路径追踪说)
    对于场景中的物体,我们假设光打到它之后会发生完美的折射与反射
    下图从眼睛开始,穿过成像平面的一个像素,投射一条光线(eye ray)
    这个光线会打到场景的某一个位置上,我们取最近的交点
    (这一步其实就解决了深度测试的问题)
    在这里插入图片描述
  • 当我们发现了一个点之后,我们要考虑这个点会不会被照亮
    我们从这点到光源连一条线(shadow ray)
    如果可以连上就表示能被照亮(下图黑线箭头为法线)
    有了法线,入射方向,出射方向,我们就可以做着色,写入像素的值,这时候可以用各种各样的着色模型 比如之前的Blinn Phong
    在这里插入图片描述

光线投射做了这么一件事,每一个像素投出去一个光线,和场景相交求的话求最近交点,最近交点和光源连线,判定是否可见,然后算着色,写回像素的值

3.3 递归(Whitted风格)光线追踪

之前就是用光线投射的方法,我们还是只考虑光线弹射一次,但其实光线可以弹射很多次,这也就是接下来要介绍的这个方法能做的
在这里插入图片描述

  • 我们还是从光线投射开始
    在这里插入图片描述
  • 在这个点上,我们先考虑这个球是一个玻璃球
    光线打到这个球上肯定发生两个事情,一个是要被反射掉,一个是被折射进去
    在这里插入图片描述
    在这里插入图片描述
  • 在算着色的过程中也发生了一点变化
    之前是光线投射到这个点之后,看这个点能不能被照亮,然后再计算它的着色
    在光线弹射次数多了以后,我们在每一个弹射点都会去计算着色的值(能量损失什么也要算),然后把它们都加回这个像素的值里面去
    在这里插入图片描述

4 光线与物体相交

4.1 光线与隐式表面相交

我们要判断光线投射出去之后要打到什么,也就是要求交点
那么求交点之前我们先把数学上的光线定义出来

  • 光线定义也就是一条射线,有一个起点,有一个方向,有这两个量就可以定义一条光线
    光线上的任何一个点都可以用 t 为自变量的函数表示
    在这里插入图片描述
  • 为了说明光线与曲面的交点
    我们从光线与球求交的情况开始切入,交点即这个点又在球上又在光线上
    方程于是可以建立起来了
    在这里插入图片描述
    解这个方程可以有很多方法,我们根据解的情况可以得到位置关系
    在这里插入图片描述
  • 我们拓展到一般性的隐式表面,方法都是一样的
    在这里插入图片描述

4.2 光线与显式表面求交

  • 对于显式表面的渲染,光线如何与三角形求交是一个很重要的话题
    在几何上,通过这个办法也可以判断一个点在不在物体内(点如果在封闭形状内,向外打一条光线,得到的交点数量一定是奇数)
    话题回到光线与三角形求交
    在下图的小奶牛中,判断光线是否与它相交
    最最简单的做法就是把它的三角形面挨个判断一遍(每个三角形面都会有0个或者1个交点)
    很直观但是很慢(之后会介绍加速方法)
    在这里插入图片描述
  • 怎么样做三角形和光线的交点呢?
    三角形肯定在一个平面内,所以问题可以被分成两部分
    光线是否和平面有交点
    这个交点在不在三角形内部
    在这里插入图片描述
  • 平面的定义采用点法式的定义方式
    -即用一个平面上的点与平面上的法线,利用点乘为0的方式建立平面方程
    在这里插入图片描述
  • 我们把光线方程带入平面方程,解出来光线与平面的交点
    再之后可以判断在不在三角形内部
    在这里插入图片描述
  • 但是人们想两步合成一步,也就是下面的方法
    左边是光线上的点,右边是用重心坐标表示的三角形内的点
    解法如下图
    解出来之后要判断是否合理,首先 t 得是正的,并且b1 b2 b3都是非负的
    在这里插入图片描述

5 轴对齐包围盒(AABB)的求交

  • 我们与每一个三角形求交,可以找到最近的交点
    但是 计算次数 = 像素数×三角形数×弹射数 这样太慢了
    所以我们要对这个过程进行改进加速,方法之一是包围盒
    在这里插入图片描述
  • 包围盒的思想是,将一个复杂的物体,用简单的形状围起来,保证物体一定在这个简单的形状之内
    如果光线连包围盒都碰不到,那肯定碰不到包围盒里的物体
    在这里插入图片描述
  • 对于三维的形状,我们最常用的是长方体——不同的三对对面形成的形状
    这也是AABB——轴对齐包围盒
    所谓轴对齐,也就是长方体的任何一个轴都是沿着坐标轴的
    在这里插入图片描述
  • 接下来我们考虑光线与包围盒的求交
    我们先从二维的角度(由不同的两对对面形成)考虑,三维可以同理得到
    对于给定的一个光线
    我们可以分别求出它与竖直和水平面的交点(此时t可能会有正负)
    我们取min里的max,max里的min,
    于是得到了进入和出去包围盒的 t 的值
    在这里插入图片描述
  • 从三维的情况来说的话
    只有光线都进入了三组对面,才能说光线进入了这个盒子
    只要光线离开任意一对对面,就离开了这个盒子
    我们对三组对面都计算一次 t
    取min中的max为最后进入时间,max中的min为最早出去的时间
    进入小于出去时,也就代表光线在它们之间是在盒子里的
    在这里插入图片描述
  • 光线不是直线,是射线
    所以有交点的情况下,我们需要对 t 进行检查
    盒子在光线背后——出去时间是负数
    光线起点在盒子内部——出去时间是负数且进入为正数
    进行总结,在AABB的情况下
    iff(if and only if)进出t小于出去t且进入t为正数
    此时才有交点
    在这里插入图片描述
  • 为什么我们要用轴对齐包围盒?
    因为轴对齐的情况下,我们可以在求t的时候,只用某一轴的信息,不用整个坐标,比点乘计算会更容易一点
    在这里插入图片描述

6 使用轴对齐包围盒加速光线追踪

如果场景非常复杂,我们会先找到包围盒,然后再根据包围盒的情况进行对物体的求交
找到包围盒的过程是空间划分的过程

6.1 均匀空间划分

  • 首先我们找到一个场景的包围盒
    在这里插入图片描述
  • 然后对这个包围盒进行划分,分成一堆格子
    在这里插入图片描述
  • 接着判定与物体相交的格子
    在这里插入图片描述
  • 做完以上预处理之后,我们得到了可能含有物体的格子
    在接下来做光线追踪的过程中,我们只要判断光线有交到的是不是有物体的格子,不是的话直接跳过,是的话再对盒子里的物体求交
    (此处假设光线与物体求交很慢,与盒子求交很快)
    下图一条光线沿着右上打过去,我们怎么知道它会碰到哪个盒子呢
    最简单的想法是,如果是右上,那么下一个盒子一定在右边或者上边(这也是之前光栅化没提到的话题,如何光栅化一条线)
    在这里插入图片描述
  • 所谓加速就是多做光线与盒子求交,少做光线与物体求交,那么加速效果怎么样呢
    如果划分成1×1的格子,那么没有加速效果
    在这里插入图片描述
    如果划分太密集,效率也不高
    在这里插入图片描述
    根据经验,人们大概得出划分成场景中物体数目的27倍的格子数比较好
    在这里插入图片描述
  • 格子的划分方法在大量均匀分布的物体上比较有效
    在这里插入图片描述
    然而在复杂空旷的场景中表现不好,会出现类似体育场中找茶壶的现象
    在这里插入图片描述

6.2 空间划分

在格子法中,空间划分的都是大小相同的格子,然而有些空旷的地方不需要这样,太浪费了,我们想在没物体的地方用大盒子,有物体的地方用密集的盒子
这也就引出了空间划分的方法
在这里插入图片描述

  • 左一是八叉树划分,先把三维空间切成八份(二维的话如图是四份),对于每一个子节点,再切一遍,以此类推
    如何停止呢,我们可以定个类似这样的规则,比如二维的情况,切成四块时三块都没物体,就不在往下切 之类的,其思想也就是划分到一定程度还没物体就不切了
    但是人们不喜欢八叉树,因为比如在二维这种划分方法是四叉树,三维是八叉树,那么维度更高,就是2的n次方叉树,这样并不好,维度更高会越来越复杂
  • 为了解决这个问题,能让空间得到划分并且和维度无关,人们发明了KD树
    KD树和八叉树的划分方法几乎完全相同,只不过它每次沿着某一个轴看开,并且只砍一刀,永远这么做
    空间被划分成类似二叉树的结果,每次节点的划分底下都只有两个子节点
    划分时为了均匀起见,xyz依次砍
  • 还有很多方法,比如BSP树,是一种对空间二分的划分方法,它每次选一个方向砍开,它和KD树的区别是它不是横平竖直地砍,而且它会有越高维越不好计算的问题(砍开二维用线,砍开三维用面,维度越高越复杂)

6.2.1 KD树

以下是一个KD树划分的例子
我们先明确,这些空间划分方法是在光线追踪之前,先把空间加速结构划分好,然后再进行光线追踪
下图每一块都需要砍开,只不过示意图只沿着右边在做

  • 一开始竖直划分成1 B
    之后B划分成2 C
    以此类推
    在这里插入图片描述
  • 当前节点沿着哪个轴划分
    划分在哪里
    中间节点一定有子节点
    最终的物体存在叶子节点上
    在这里插入图片描述

下面我们看看这个结构将如何加速光线追踪

  • 考虑我们划分成这种情况
    在这里插入图片描述
  • 首先考虑最大的包围盒A,发现有交点,那么对于左右子节点可能都有交集
    在这里插入图片描述
  • 我们发现和左边蓝色的有交点
    我们假设它是叶子节点,就先这样,继续看右边,发现也有交点
    在这里插入图片描述
    在这里插入图片描述
  • 以此类推判断2 然后判断C (图略)
    然后判断3,发现3是叶子节点,在3中与物体求交,找到交点
    在这里插入图片描述

KD树会产生一些问题

  • 给出一个节点的包围盒,我们要知道它和哪些三角形有交集,这是很难的,不太好写
  • 一个物体和很多包围盒都有交集的话,它可能会存在很多个叶子节点中

6.3 物体划分&BVH

空间划分存在一些问题,于是人们发现了另一种做法,我们不从空间划分,我们从物体开始划分,这种划分形成的加速结构也就是所谓的BVH

  • BVH是怎么运作的呢,最开始也一样有一个盒子包围场景作为根节点
    在这里插入图片描述
  • 然后我们把物体分为两部分(怎么求之后再说),然后重新求他们这两部分的包围盒
    在这里插入图片描述
  • 以蓝色节点为例,我们继续划分再重新求包围盒,划分到比较少的三角形,比如5个三角形左右就可以停止
    在这里插入图片描述
  • BVH的特性是,一个物体可以指存在在一个节点里,并且不用算三角形和包围盒求交了
    但是BVH的划分并没有划分开,包围盒可能会相交,不过没什么问题,我们做到尽可能重叠少就好,这也是当今一个研究的方向
    下图进行了一个总结
    在这里插入图片描述
  • 关于怎么划分一个节点,有很多方法
    比如 选一个最长的轴划分 或者 取中间的物体(第n/2个三角形)(可以采用快速划分算法)
    主要是为了保证树的平衡
    在这里插入图片描述
  • 关于BVH的存储结构也做一个总结,只有子节点存物体
    在这里插入图片描述
  • 如何加速光线追踪和KD树其实差不多,到子节点测物体
    在这里插入图片描述

一个是对空间的划分,一个是对物体的划分,以下是总结
在这里插入图片描述

到这里我们算是讲完了whitted风格的光线追踪。

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我会尽力回答你的问题。首先需要明确的是,编写CAN通信数据收发的程序需要一些基础知识,比如CAN协议的使用、STM32f103的寄存器配置等。如果你对这些不熟悉,建议先学习一下相关知识再开始编程。 下面是一个简单的参考代码,可以实现基于TIN1定时器的100msCAN通信数据收发: ```c #include "stm32f10x.h" #include "stm32f10x_can.h" // 定义CAN通信的ID和数据 #define CAN_TX_ID 0x01 #define CAN_RX_ID 0x02 #define CAN_DATA "Hello, CAN!" // 定义CAN初始化函数 void CAN_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; // 配置CAN引脚 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置CAN参数 CAN_InitStructure.CAN_TTCM = DISABLE; CAN_InitStructure.CAN_ABOM = DISABLE; CAN_InitStructure.CAN_AWUM = DISABLE; CAN_InitStructure.CAN_NART = ENABLE; CAN_InitStructure.CAN_RFLM = DISABLE; CAN_InitStructure.CAN_TXFP = ENABLE; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq; CAN_InitStructure.CAN_Prescaler = 6; CAN_Init(CAN1, &CAN_InitStructure); // 配置CAN过滤器 CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = (CAN_TX_ID << 5); CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (CAN_RX_ID << 5); CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); // 启动CAN CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); CAN_ITConfig(CAN1, CAN_IT_TME, ENABLE); CAN_ITConfig(CAN1, CAN_IT_EWG, ENABLE); CAN_ITConfig(CAN1, CAN_IT_EPV, ENABLE); CAN_ITConfig(CAN1, CAN_IT_BOF, ENABLE); CAN_ITConfig(CAN1, CAN_IT_LEC, ENABLE); CAN_ITConfig(CAN1, CAN_IT_ERR, ENABLE); } // 定义CAN发送函数 void CAN_Send_Message(void) { CanTxMsg TxMessage; TxMessage.StdId = CAN_TX_ID; TxMessage.IDE = CAN_ID_STD; TxMessage.RTR = CAN_RTR_DATA; TxMessage.DLC = strlen(CAN_DATA); strcpy((char *)TxMessage.Data, CAN_DATA); if (CAN_Transmit(CAN1, &TxMessage) == CAN_NO_MB) { // 发送缓存已满 } } // 定义CAN接收函数 void CAN_Receive_Message(void) { CanRxMsg RxMessage; if (CAN_MessagePending(CAN1, CAN_FIFO0) != 0) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); if (RxMessage.StdId == CAN_RX_ID && RxMessage.IDE == CAN_ID_STD && RxMessage.DLC == strlen(CAN_DATA)) { if (strcmp((char *)RxMessage.Data, CAN_DATA) == 0) { // 收到正确的数据 } } } } // 定义TIN1定时器初始化函数 void TIM1_Configuration(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; NVIC_InitTypeDef NVIC_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_TimeBaseInitStruct.TIM_Prescaler = 7200 - 1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStruct.TIM_Period = 1000 - 1; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct); NVIC_InitStruct.NVIC_IRQChannel = TIM1_UP_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); TIM_ClearITPendingBit(TIM1, TIM_IT_Update); TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); TIM_Cmd(TIM1, ENABLE); } // 定义TIN1定时器中断处理函数 void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { CAN_Send_Message(); CAN_Receive_Message(); TIM_ClearITPendingBit(TIM1, TIM_IT_Update); } } // 主函数 int main(void) { CAN_Configuration(); TIM1_Configuration(); while (1) { // 主循环 } } ``` 以上代码仅供参考,实际使用时需要根据具体情况修改。希望对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值