101复习(5`几何、光线追踪)
几何
- 隐式:
- 数学表达式
- bool运算:并集,差值,交集
- 距离函数SDF:两个物体求得距离函数后混合,可以得到有趣的几何体
- 分型
- 缺点:很难知道哪些点满足表达式
- 优点:容易判断一个点是否满足表达式,将点带入表达式,如果结果为负在物体内,0在物体表面,为正在物体外
- 显示
- 图元(点云,线(直线/曲线),三角形)
- 参数映射方法,由2维空间转换到3维空间的函数
- 优点:很容易知道包含哪些点
- 缺点:很难判断一个点是否在表面上
- 曲线
- 贝塞尔曲线
- 曲线特点:
- 第0层有m个控制点,则称为m - 1次贝塞尔曲线,比如有4个控制点,为3次贝塞尔曲线
- 曲线一定经过第0层的起点、终点 控制点,但不一定经过第0层的中间控制点
- 曲线一定在第0层所有控制点形成的凸包内(包裹所有图形的最小范围)
- 德卡斯特里奥算法——循环:
- 思想:已一定步长取t(0——1)的值,每次根据t逐层插值生成新的点,当到达最后一层,将此生成点作为贝塞尔曲线点之一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
pair<double, double> linearInterpolation( const pair<double, double>& p1, const pair<double, double>& p2, double t) { return {(1 - t) * p1.first + t * p2.first, (1 - t) * p1.second + t * p2.second}; } vector<pair<double, double>> calculateBezierPoints( const vector<pair<double, double>>& controlPoints, double stepSize) { vector<pair<double, double>> points; for (double t = 0; t <= 1; t += stepSize) { vector<pair<double, double>> lastPoints = controlPoints; while (lastPoints.size() > 1) { vector<pair<double, double>> newPoints; for (size_t i = 0; i < lastPoints.size() - 1; ++i) { pair<double, double> newPoint = linearInterpolation(lastPoints[i], lastPoints[i + 1], t); newPoints.push_back(newPoint); } lastPoints = newPoints; } points.push_back(lastPoints[0]); } return points; }
展开
- 具体实现:
- 输入第0层所有控制点controlPoints,和Δt
- points用于存储所有贝塞尔曲线点
- lastPoints用于保存上一层的生成点,每次循环会首先初始为第0层的控制点controlPoints
- newPoints用于存储当前层新生成点
- while控制点==1时,将唯一的生成点,加入到points中
- 伯恩斯坦多项式——算法:
- 分段考虑
- 曲线特点:
- 三次参数样条曲线
- 当我们当已知某些点而不知道具体方程时候,有两种做法去采集数据:拟合或者插值,拟合不要求经过已知点,插值保证已知点都会经过
- 贝塞尔曲线是一种拟合思想,对于第0层的哪些中间点,不会保证经过它们
- 三次参数样条曲线是插值的思想,可以保证经过所有已知点(控制点)
- 思想:
- 假设有n+1个控制点,用xi(xi,yi)表示
- 每两个点组成一个区间[xi,xi+1],总共有n个区间
- 每个区间都是一个三次方程,称为三次样条函数Si(x),组成总的三次参数样条曲线S(x)
- 三次样条函数满足以下条件:
- 插值过点:S(xi) = yi
- 曲线光滑:S(x),S
(x),S`(x),连续 - 形式:yi = ai + bix + cix^2 + dix^3
- 构建方程:
- 每个区间有4个未知数abcd,总共n个区间,也就是4n个未知数,需要4n个方程求解
- 首先对n-1个内部点,有方程:Si(xi+1) = yi+1, Si+1(xi+1) = yi+1(Si(xi+1)表示[xi, xi+1]区间上的索引为xi+1的控制点,Si+1(xi+1)表示[xi+1, xi+2]区间上的索引为xi+1的控制点),这样建立了2(n-1)== 2n - 2个方程
- 对于两个端点分别满足:S0(x0) = y0, Sn-1(xn) = yn,现在总共2n个方程
- 对n-1个内部点,有方程:Si`(xi+1) = Si+1`(xi+1), Si``(xi+1) = Si+1``(xi+1)(因为它们是同一个点,导数应该相等),这样又新增了2n - 2个点,现在总共有4n-2个方程了
- 剩余的两个方程通过边界条件得到:
- 自然边界:S” (x0) = 0 = S” (xn),两个端点的二阶导数都==0
- 固定边界:S0’ (x0) = A, Sn-1` (xn) = B,两个端点的一阶导数分别==A和B
- 非节点边界:S0``` (x0) = S1``` (x1), Sn-2```(xn-1) = Sn-1``` (xn),第一个点的三阶导数等于第二个点的三阶导数,最后第一个点的三阶导数等于倒数第二个点的三阶导数
- 解未知数
- Cardinal曲线
- 对比:
- 贝塞尔曲线不过点
- 三次参数样条曲线平滑度最好
- 三次参数样条曲线性能消耗较高
- 贝塞尔曲线,可以支持局部修改不影响整体(分段),也可以普通计算影响整体
- 三次参数样条修改一个点会影响整体(mi决定ci的值,ci的值决定第i段曲线形状,mi的值依赖于mi+1,mi+2,而mi+1的值又依赖mi+2,mi+3……)
- Cardinal曲线修改一个点只会影响相邻的部分(因为只依赖相邻点),不会影响整体
- 因此简单易用不需要过点选择贝塞尔曲线
- 想要更平滑的曲线选择三次参数样条曲线
- 想要局部控制选择Cardinal曲线
- 贝塞尔曲线
- 曲面
- 贝塞尔曲面
- 比如给4*4 16个点,先从u方向,生成4条贝塞尔曲线,根据时间t,沿着v方向,查找4个曲线上的控制点,生成数条贝塞尔曲线
- 贝塞尔曲面
- 网格细分
- 细分:将由三角形面表示的网格,增加三角形数量
- loop细分
- catmull clark细分
- 细分:将由三角形面表示的网格,增加三角形数量
- 网格简化:
- 将由三角形面表示的网格,减少三角形数量
- 边坍缩:边的两个顶点合并到新点,边不再存在
- 坍缩后放在什么位置:边坍缩形成的新点,应放在最优为止(二次误差度量:和周围面距离的平方和最小)
- 坍缩哪个边:遍历网格体所有边,计算最小二次误差度量,从误差最小的开始坍缩
- 数据结构:找到当前误差最小的边,并再坍缩一个边后,周围边受到影响要重新计算,用优先队列
- 正规化:使得三角形面过渡更加平滑
光线追踪
- 光栅化:实现全局阴影,glossy,全局光照……较困难
- 光追: 渲染管线默认支持光栅化渲染,可以利用opengl计算着色器自行实现光追,vulkan光追扩展,实现
- 特点:性能低,效果好
- 设定:
- 光线
- 沿直线传播(实则光是波动的)
- 不相互碰撞
- 光线可逆性(光从光源出发到物体到人眼,从人眼出发到物体到光源)
- 相机
- 针孔相机 + 成像平面
- 光线
- 步骤
- 准备场景数据
- 光线投射:
- 对成像平面的每个像素方向,都发射一条光线
- 光线弹射(反射,折射)
- 找到场景中最近交点:
- 光线数学表示:射线r(t):O + td,起点 + 某时间 * 方向(单位) == t时的光线终点位置,向量相加
- 交点:即在光线上,又在物体表面
- 光和隐式物体求交:
- 光和球体求交:
- 球体数学表示:(p - c)^2 - R^2 = 0, 球面点到球心的距离的平方 == 半径的平方
- 交点满足:(O + td - c)^2 - R^2 = 0,
- 求解未知数t,求解一元二次方程,根据求根公式求解
- 检查解的合理性
- 其中实数(b^2 >= 4ac)有意义,虚数没有意义
- t <= 0没有意义
- 没有解,说明相离,光线和球体没有交点
- 有一个解,相切,有一个交点
- 有2个解,相交,有两个交点,应该选择最近的,也就是t值最小的点
- 光和球体求交:
- 光和显示物体求交
- 光和场景求交
- for(光线){for(图元){求交,维护最小t值}}
- 加速求交
- 光和物体加速求交
- 包围盒:把复杂物体用简单的几何体包围起来
- 轴对齐包围盒AABB:任意一边都对齐于某个坐标轴
- 光和2D包围盒求交
- 光和3D包围盒求交
- 长方体:3个对面的中心区域,通常用左下和右上两个点表示
- 沿每个轴求交:……
- 光线进入离开长方体的时刻:光线进入了3个对面,则进入盒体,光线离开任意一个对面,则离开盒体,max(tenter), min(texit)
- 判断光和盒体是否有交点
- 如果texit < 0,则光线在面的背后,则不可能有交点
- 如果tenter < 0,texit >= 0, 则光线起点在盒体内,则有交点
- 也就是当tenter < texit && texit >= 0 则有交点
- 光和场景加速求交
- 加速求交按空间划分数据结构:四叉树/OCT叉树/kd树/BSP树
- 数据结构介绍:详见其他章节
- 算法思想:
- 划分空间:在光线追踪前
- 求交点:
- dfs:
- 返回条件:如果和叶节点相交,将其内所有物体,加入到res结果数组,如果光线和盒体不相交返回
- 状态转移:否则和子盒体递归求交
- 遍历结果数组:如果物体相交,并t值 < 当前最小t,则更新,返回t
- dfs:
- 叶盒体大小多少合理:
- 如果太稀疏:仍要遍历很多的物体求交
- 如果太密:需要和很多很多叶盒子求交
- 也就是要控制一个度才能达到最优效果,C~=27 * 物体数量n
- 更适用于什么样的物体布局
- 物体均匀分布
- 算法缺陷:一个网格体在多个叶包围盒内,不能简单的根据物体中心位置决定它在哪个包围盒中,因为这样会忽略某个更近的物体,但如果划分到所有相交的包围盒中,又很难计算,所以一般不用空间数据结构的方式
- 加速求交按物体划分数据结构:BVH
- 划分空间:按照物体划分
- 返回条件:每个包围盒内包含较少物体(比如<5)
- 状态转移:找到当前长度最长的维度,找到中位数物体(可以排序 / 算法(215. 数组中的第K个最大元素, 用三路快排)), 划分为两部分,分解为两个包围盒,对两个子节点递归划分空间
- 求交:和上面一样
- 注意空间划分:和八叉树不同,不是均匀划分,和kd树不同,不是把物体相切,两个包围盒就像总包围盒一样包裹住各自的物体,这样保证任何物体都在关联的包围盒的内部,就算包围盒间有重叠,也不影响求交
- 划分空间:按照物体划分
- 加速求交按空间划分数据结构:四叉树/OCT叉树/kd树/BSP树
- 光和物体加速求交
- 找到场景中最近交点:
- 像素着色
- 辐射度量学
- 在CG中通常使用辐射度量描述光照,因为它给出了一系列度量方法和单位
- Radiant energy:辐射能量Q,单位焦耳J,辐射出的总能量, 随时间增加
- Radiant power:辐射通量Φ,单位瓦特W,单位时间辐射出的能量
- 弧度:弧长/半径,圆周长2Π r / 半径r = 2Π总弧度
- solid angle立体角: Ω, 单位sr
- 立体角 = 球面面积A / r^2
- 总立体角:球体总面积4Π r^2 / r^2 = 4Π
- 球体大圆周长:2Πr
- 单位面积(球体坐标系)dA = (r dθ)(r sinθ dΦ) = r^2 sinθ dθ dΦ
- 单位立体角 dω = dA/r^2 = sinθ dθ dΦ
- Radiant Intensity:辐射强度I = dΦ / dΩ,在单位时间内,光源会向四面八方辐射能量,往立体角辐射出的能量
- Radiant Irradiance: 辐射照度E = dΦ/dA, 在单位时间内,物体会从半球方向接收能量,dA接收的辐射能量
- A应垂直于辐射方向的表面,因为当不垂直时接收的能量会减少
- Radiant Radiance:
- 辐射率:L(p,ω) = d^2Φ(p,ω) / dω dAcosθ
- p点被着色点,ω表示辐射方向,dω单位立体角、dA单位面积,分子是二次导数
- cosθ:法线方向和辐射方向的夹角,夹角为90度,说明A于辐射方向垂直,cos为1,不影响A的大小,越平行说明A接收能量的有效范围减少,使得A缩小
- 可以理解为:
- d²y/da db = d(dy/da)/db
- L = dI / dAcosθ: 从dω辐射的能量,到dA接收的能量
- L = dE / dωcosθ: 从dA辐射到dω的能量,
- 渲染方程
- BRDF双向反射分布函数:
- bsdf = brdf + btdf,因此brdf这里仅考虑反射不考虑折射
- fr(wi->wr入射->出射): Lr / Li, 描述了比值关系: 反射到某dw的能量Lr / dA从dw接收的能量Li
- BRDF表示材质,材质是物体和光作用的方式的抽象
- 漫反射fr:
- 假设任何方向进来的光能量一致,因此Li和fi是常量,可以提取到积分外,对cos项积分结果为Π,由于不考虑吸收,根据能量守恒Li == Lo,则fr = 1/Π, 引入光吸收即颜色值p,fr = p/Π
- 镜面反射fr:
- 反射方程Lr:
- 渲染方程Lo:
- BRDF双向反射分布函数:
- 全局光照_光栅化:
- 直接光照——渲染方程:
- 间接光照——IBL:
- 预处理准备:
- IBL:是光栅化实现环境光的一种方式,基于图像的光照,将图像视为光源
- 环境贴图:
- 获取/生成方式:
- 使用资源:较少耗费性能,没有考虑周围场景的影响
- 预处理生成: 从原点向6个方向渲染生成环境贴图,考虑周围场景,但这是不正确的,对于不同的p点,受到物体遮挡关系是不同的,都使用原点位置生成的环境贴图,将获得错误结果
- 预处理生成: 从每个物体向6个方向渲染生成环境贴图,性能消耗极大,不支持动态场景
- 动态生成:每帧从每个物体向6个方向渲染生成环境贴图,性能消耗极大,支持动态场景
- HDR环境贴图资源:
- HDR -> LDR
- HDR:由于PBR基于物理的,光照强度直接使用物理等效值,如果不使用HDR,很多光源被限制到1,无法正确区分相对亮度
- 默认缓冲是LDR的,在渲染天空盒这一步,我们在shader中,手动色调映射和gamma校正,使得HDR可以正确转换到LDR纹理中
- 存储格式
- RGB 格式:有RGB 3个通道,每个通道8位(可以表示0-255区间,通常映射到0——1)
- 存储HDR需要RGB 3个通道,每个通道32位
- RGBE 格式: 是一种高效存储的方式,RGBA 4个通道,每个通道8位,第4个alpha通道存放指数,以便可以用32位存储空间表示HDR数据,不过在使用的时候,需要解析转换为RGB
- 环境贴图格式:
- 从ERP到cubemap:
- 加载hdr文件:依旧通过stb_image库加载,它会将RGBE格式隐式转换为RGB格式
- 渲染cubemap::创建正方体,放到世界原点位置,对立方体渲染6次,每次对应一个面,根据3D方向(由于位于原点直接使用顶点位置即可),数学转换为2D uv位置,从ERP纹理获取的颜色值作为片元颜色,输出到cubemap的一个面中(也是HDR格式)
- 渲染天空盒:详见其他章节,它由于受到摄像机旋转的影响,需要每帧渲染
- HDR -> LDR
- 获取/生成方式:
- 辐射度图:
- 对环境贴图每个纹素卷积,生成以此方向的半球采样辐射率积分结果,这样预处理,在之后渲染方程使用L项时,直接通过3D方向对辐射度图采样即可(辐照度图不需要每帧渲染,因为它和物体的相对方向不变,假设天空和物体不运动情况)
- 渲染辐射度图:
- 创建正方体,渲染到世界原点位置,对立方体渲染6次,每次对应一个面
- 对半球均匀离散采样,弧度取值区间:Φ从0——2Π,θ从0——1/2Π,这样是半球区间,每次弧度增加Delta,
- 根据当前Φθ弧度值,转换为方向向量,
- 根据TBN变换,转换到当前片元的切线空间(法线方向用顶点位置)
- 结果输出到cubemap的一个面中
- 预滤波环境贴图
- 类似辐射度图,也使用cubemap存储,但考虑了物体粗糙度,物体粗糙度越高,采样越分散,使用mipmap存储,利用glGenerateMipmap函数生成,为了平滑过渡,需要为贴图启用三线性过滤
- N==R==V:镜面反射不同于漫反射,随着观察角度v不同,看到的结果也不同,如果预计算处理,我们无法为所有可能的 V 都预计算一张贴图(BRDF/辐照度),因此只能把v假设为固定值n,也就是假设v和r都为n方向
- 低差异序列:xi.x为index/N,范围0——1,均匀递增 xi.y取值范围0——1,生成伪随机样本,比起纯随机样本分布更加均匀
- GGX重要性采样
- 生成球坐标
- a = roughness * roughness,则a范围0——1
- 方位角 (phi),2.0 * PI * Xi.x; 球体大圆周长,均匀分割
- 天顶角 (theta)的余弦值正弦值
- 宽度:cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); 当a取值为0,结果为1,当a取值为1,结果为0——1之间,粗糙度越高,范围越大
- 高度:sinTheta = sqrt(1.0 - cosTheta*cosTheta); 当a取值为0,cosTheta为1,sinTheta为0,当a取值为1,cosTheta为0——1,sinTheta为0——1
- 当a取0绝对光滑时,cosTheta为1,sinTheta为0,转换为笛卡尔坐标永远是(0, 0, 1)指向正y
- 当a取1绝对粗糙时,cosTheta为0——1,sinTheta为0——1,转换为笛卡尔坐标,均匀覆盖整个半球
- 粗糙度越低越集中到y,粗糙度越高越分散
- 由于cosTheta为0——1,sinTheta为0——1,不会超过半球范围
- 转为笛卡尔坐标即向量
- 转换到TBN切线空间(法线方向用顶点位置)
- up仅用来构建TBN矩阵的,并不是最后的N向量,TBN才是最后的正交基,形成以N即采样方向半球
- N.z趋近于0,说明越接近y(由于把视线假定为n方向,围绕v生成镜面波瓣,也就等于围绕n生成),使用z轴构建,否则使用y轴构建
- 取反射向量:ImportanceSampleGGX获得围绕v形成镜面波瓣的反射向量,采样的应该是入射光,这样得知v看到的颜色
- 把所有采样结果平均取均值
- mipmap生成:
- 要在刚才基础上,外部嵌套循环次数
- glRenderbufferStorage每次缩小mipmap的分辨率和缩小视口大小,这并不会使得渲染区域缩小,渲染区域由P决定,而视口影响图像分辨率(一个图像的采样频率)
- 传入的粗糙度值a增大(相当于使用更大范围过滤器过滤,将产生更模糊的图像)
- 访问可以指定mipmap等级采样(可以是小数)
- 优化:
- 贴图接缝:
- 亮点:
- 成因:
- 在生成预滤波环境贴图时,不再直接采样源环境贴图,而是glGenerateMipmap() 生成它的一系列mipmap,根据a动态计算level去从对应的mipmap采样辐射度
- 当粗糙度越大,GGX的结果越小,pdf值越小,样本立体角增大,样本立体角/源立体角增大,level增大,从更高等级的mipmap采样
- 使用高级别mipmap,使得原来小而明亮的点,转为大而亮度降低的点,这样消除了原来明亮的噪点
- BRDF积分图:
- 漫反射
- 直接光照值 + 间接光照值——漫反射(F项是常量,L项直接采样)
- 镜面
- 直接光照值 + 间接光照值——漫反射 + 间接光照值——镜面反射
- 预处理准备:
- 全局光照_光线追踪:
- 全局光照:直接光照(直接到达物体,没有经过弹射) + 间接光照/环境光(1次及以上光线弹射)
- 渲染方程设定:
- 忽略渲染方程自发光项
- 渲染方程的积分项用蒙特卡洛——采样法求解,蒙特卡洛包含3项(f(x),n,p),f(x)对应LFcos这3项——被积函数
- 如何采样:采样数量n项自定义的(例如100),最简单方法为均匀采样,则p项 = 1/n
- 直接光照——渲染方程:
- 定义Lo,发射n条光线,如果光线打击到光源,Lo+=计算结果,如果没有打到光源,Lo+=0
- 间接光照——路径追踪:
- 定义Lo,发射n条光线,如果光线打击到光源,Lo+=计算结果,如果光线打到了物体,Lo+=递归计算结果(相当于作为光源,即Li项)
- 返回条件:如果和场景没有交点 / 超过最大弹射次数
- 优化算法复杂度:
- 比如每个物体发射100条光线,每根光线都打到物体,这些被打到的物体又要发射100条光线,这样变为了10000条光线路径……
- 这样会发生指数级爆炸,为n^bounces次,n为采样次数,bounces为弹射次数
- 当n==1时,1^bounces = 1,始终为1条光线路径,这样就防止了指数级爆炸
- 减少噪声:
- 每个像素随机发射n条光线,SPP每个像素采样次数,将这些光线结果取平均颜色值
- 俄罗斯轮盘赌注:
- 减少噪声:
- 总结:
- 辐射度量学
本文由作者按照 CC BY 4.0 进行授权





























