文章首发于lengyueling.cn 欢迎来访交流!
PDF版本已经附着lengyueling.cn文末,需要自取。
导论
图形学应用场景
-
-
PBR:之狼
-
卡通渲染:无主之地
-
-
黑客帝国
-
疯狂动物城,冰雪奇缘
-
概念图
-
虚拟现实
-
模拟仿真
-
矢量图
技术挑战图形学
光栅化(rasterization):
在屏幕上显示三维空间中的几何图形
实时:30fps 否则就成了离线
曲线和网格(Curves and Meshes) :各种细分方法
光纤追踪(ray tracing):动画电影应用广泛,但速度慢
目前,游戏使用实时光跟踪
仿真和模拟(animation/simutatoin)
OpenGL、dx是图形学API不是图形学
计算机图形学与计算机视觉
-
图形学不是计算机视觉。
-
计算机视觉:猜测、预测、分析和处理
-
计算机视觉是理解这个世界,计算机图形学是创造这个世界
线性代数复习
图形默认使用列向量,课程默认为右手系
点乘应用于图形学
AB=|A||B|cosθ
-
点乘主要用于两个单位向量的夹角,
-
观察两个向量是同向、垂直还是反向,观察两个向量的接近。如果两个向量的点乘以接近1,则非常接近,如果接近0,则非常远
-
一个向量可以分解成两个(多个)向量和
叉乘应用于图形学
-
AxB=-BxA
-
判断左右(内外)
-
若AxB正则点A在点BA左侧,如果点P在点A、B、C内,则ABXAP,BCxBP,CAxCP结果是外(内)ABC内
-
如果任何结果不同,P在ABC外
-
-
定义坐标系
-
要求:单位向量,相互垂直(点乘为0,叉乘结果为另一轴)
-
任何向量都可以分解成多个投影
-
矩阵知识点
-
(MxN)(NxP)=(MxP)
-
矩阵交换律,只有结合律
-
转置:(AB)T=BT AT
-
单位矩阵I(对角阵I):可计算矩阵A-1(A逆)可用于返回转换前的结果
-
向量的点乘、叉乘都可以转换为矩阵相乘
-
点乘:A·B=ATB
-
叉乘:AxB=A*B
概率论复习(用于路径跟踪)
X:随机变量
pi一定是非负,一切pi相加等于1
EX:数学期望
X~P(X):概率密度函数,概率的连续分布
变换
2D 变换
缩放
-
X'=SX
-
Y=SY
X轴翻转
-
X'=-X
-
Y'=Y
切变
旋转
齐次坐标
加入齐次坐标,以解决平移必须加法的问题。
-
矩阵没有交换律:当需要平移和线性转换时,需要先线性转换再平移
-
仿射变换=线性变换 平移
3D 变换
齐次坐标
3D点:(x,y,z,1)T
3D向量:(x,y,z,0)T
旋转向量和欧拉角
RXYZ(α,β,γ)=RX(α)Ry(β)Rz(γ)
使用右手螺旋定则
在三维坐标系中:
XxY=Z XxZ=-Y YxZ=X
因此RY(α)中为X或Y的转置
同时,去掉分别代表XYZ操作行列可以转换为最基本的旋转
罗德里格旋转公式
这是一种可以表示任意旋转后的向量
四元数解决了两个旋转角度之间的差异,本课没有具体说明
视图变换
视图变换是什么?
想想如何拍照:
-
找个好地方安排拍照的人(模型变换)
-
茄子!(投影变换)
定义相机
-
position(相机在哪里)e
-
look-at/gaze direction(朝哪个方向)g
-
up direction(向上:控制歪斜)t
同时,相机默认向上方向 Y。永远朝着-Z从方向上看,总是在原点。
将任何点相机移到默认点
-
先平移
-
然后旋转(利用逆矩阵的性质,从原点到任何点转移)
(g x t = e)
投影变换
:更像工程制图,没有近大远小的性质 :近大远小更类似于现实中的形式
正交投影
一种简单的理解方式
-
把z轴去掉
-
相机位于原点,看向-Z轴,向上为Y轴
-
将生成的矩形平移并缩放到[-1,1]2
标准方式
一般来说,任何立方体都应该成为标准(canonical)立方体
先做平移,再做缩放
透视投影
如何做透视投影?
-
将视锥压扁成立方体
-
做正交变换
压扁操作
其中:
n为近平面
f为远平面
利用相似三角形求出y轴的位置
同理可以的到
Z轴如何处理?
当我们完成X、Y轴处理后,发现Z轴仍不知道,同时乘z后的到右下的矩阵
同时可以通过矩阵叉乘的到未知矩阵的一部分:
再通过下图的方法得出原本的数值为
以此得到:
同时z轴在远平面上的数值并不会改变,因此的到:
通过解上面两个方程的到:
因此Z轴一行的矩阵为:
光栅化
如何定义视椎体宽高比和垂直可视角度?
fov:垂直可视角度
t:在y轴的高度
n:近平面上的z轴上的点
r:中心点到右边的距离
如何将投射完成的标准立方体显示到屏幕上?
什么是屏幕?
-
二维数组
-
每个元素是一个像素
-
一种经典的光栅成像设备
光栅化:在屏幕上绘画
像素:
-
在屏幕上最小单位的小方块
-
由红绿蓝三原色混合而成
屏幕空间
-
像素都是以(x,y)的形式表示,其中x、y都是整数
-
像素的范围从(0,0)到(宽度-1,长度-1)
-
像素的中心在(x+0.5,y+0.5)
-
屏幕覆盖范围为(0, 0) 到 (宽, 高)
视口变换
-
Z轴被忽略
-
将原本【-1,1】²的正方体变换为【0,宽】x【0,高】(视口变换)
-
视口变换矩阵
光栅显示设备
三角形-基本形状单元
-
最基础的多边形
-
任何多边形都可以
-
-
独特的性质
-
除非折成两个三角形,否则永远是一个面
-
三角形的内外很明确
-
可以利用重心插值进行三角形顶点插值
-
采样
在某个点对函数求值就是采样,我们通过采样将函数离散化。
Inside函数
inside(tri,x,y)
1:point(x,y)在三角形内
0:其余情况
遍历所有点,判断所有点是否在像素内
for(int x = 0; x < xmax; ++x)
{
for(int y = 0; y < ymax; ++y)
{
image[x][y] = inside(tri, x + 0.5, y + 0.5);
}
}
通过叉乘计算出(见叉乘在图形学的应用)点是否在三角形内
如果碰巧点在三角形的边界,本课程中不做处理,也可以特殊处理
包围盒优化
利用包围盒(Bounding Box)对一定不会包含三角形的像素进行优化
对于窄长的三角形并不友好
采样的瑕疵
-
锯齿
通过inside函数渲染出来的三角形有明显锯齿
抗锯齿是图形学中重要的难题
不能先采样再做模糊
之所以会出现锯齿(走样)是因为出现了(后面有讲)
-
摩尔纹
-
车轮效应
信号时间变化太快以至于采样跟不上变化的速度
时域与频域
频域是描述信号在频率方面特性时用到的一种坐标系
时域是描述数学函数或物理信号对时间的关系的一种坐标系。
傅里叶变换会将时域转化为频域
将函数表示为正弦余弦的加权和
随着展开式越来越多,越来越接近我们想要表达的函数
如果采样的频率不够,还原过来的函数就会越来越不精准
频域和时域可以通过傅里叶变换和逆变换互相转换
傅里叶频域图
滤波
删除特定的频率被称之为滤波
高通滤波:只显示高频信息(只显示边界-锐化,将低频信息盖住
低通滤波:只显示低频滤波(画面变模糊,将高频信息盖住
卷积
滤波=卷积(=平均)
简化的定义:结果为相邻数的平均值
定理:时域的卷积等于频域的乘积
频谱混叠
由于采样稀疏,因此出现频谱混叠从而出现锯齿(走样)如果屏幕中像素非常多,密集的采样就不容易出现走样。
因此使用分辨率高的显示器,频谱的搬移间隔大,不容易出现频谱混叠。
同时,将信息进行低通采样再进行采样即可反走样。
反走样(抗锯齿)
解决方法:
-
通过将每个像素进行模糊卷积f(x,y)
-
卷积=滤波=平均
-
-
然后再对灭个像素的中心取样
在光栅化一个三角形时,像素颜色的平均值f(x,y)= 三角形的覆盖像素的面积
超采样抗锯齿(MSAA)
MSAA: Antialiasing By Supersampling
这是一种对反走样的近似
将每个像素的内部多增加采样点再进行模糊卷积
MSAA X4
缺点:增加了很多的计算量
其他的抗锯齿方法
-
FXAA (Fast Approximate AA)
-
快速近似抗锯齿
-
图像的快速处理
-
-
TAA (Temporal AA)
-
对上一帧进行处理
-
超分辨率
从低分辨率处理成高分辨率
与反走样类似,也是解决了样本不足的问题
目前可使用DLSS(深度学习的方法)进行超分辨率处理
画家算法
灵感来源于画优化
先画(渲染)距离远的,再画(渲染)距离近的。
问题:难以确定谁在前谁在后
深度缓冲(Z-Buffer)
-
深度图-储存每个像素对应的最浅的深度
-
结果图-储存最终的结果
特别定义:Z越小(越黑)越近,越大越远(与通常的右手系不同,仅仅为了便于理解)
算法:
//默认深度为无限远
for(each triangle T)
{
for(each sample(x,y,z) in T)//遍历任意一个三角形中的任意一个像素
{
if(z<zbuffer[x,y])//如果此时的深度小于之前记录好的深度
{
framebuffer[x,y] = rgb;//三角形着色
zbuffer[x,y] = z;//更新小的深度
}
else
{
//...
}
}
}
着色
定义:对不同的物体应用不同的材质
Blinn-Phong反射模型
裴祥风(Bùi Tường Phong)先生改进的反射模型。
-
高光(Specular highlights)
-
漫反射(Diffuse reflection)
-
间接/环境光照(Ambient lighting)
漫反射
兰伯特余弦定律(Lambert)
兰伯特余弦定律:cosθ=l·n
n:法线方向
l:光照方向
不同角度的物体反射的光不同
光照衰减
I1=I0/r²
I:光线强度
朗伯着色器
Kd:颜色扩散系数
Ld:漫反射反射光
漫反射只和物体本身与光线有关,与观察方向v无关
引入max(0,n·l)是因为若点乘小于0说明是从下面射过来的,没有意义。
若两个向量的点乘接近1则离得很近,若接近0则离得很远。
高光
Ls:高光反射光
Ks:镜面反射系数(通常认为是白色的)
引入半程向量h,如果镜面反射方向与观察点接近,则半程向量h与法线方向n接近
Blinn-Phong反射模型是对Phong反射模型得改进,引入半程向量h比使用镜面反射方向r计算量更小
由于cosα的容忍度太大,导致高光太大,所以引入p次幂,一般使用100-200次幂
环境光照
环境光与入射方向、法线方向、观察方向无关,是一个常数
总结
着色频率
着色频率不同,着色效果也不同
-
以顶点为单位着色(Gouraud shading)
-
以三角形平面为单位着色(Flat shading)
-
以像素为单位着色(Phong shading)
如果面足够多,逐顶点未必比逐像素效果好。
从一个球来获取法线方向是容易的,
复杂的模型通过将相邻的四个面进行对面积的加权平均的到四个面的平均法向量。
逐像素着色通过顶点法线的重心插值来实现
实时渲染管线
简化的流程
-
输入空间中一系列的点
-
顶点处理
-
三角形处理
-
光栅化
-
着色
-
片段(像素)处理
-
帧缓冲区处理
-
-
输出
目前渲染管线都是在GPU中被编程完成了,只有顶点处理和片段处理可以编程
Shader编程
利用GLSL对顶点着色器和片段着色器进行编程。
片段着色器对每个片段都执行一次。
以下是一个GLSL片段着色器的程序代码:
uniform sampler2D myTexture; // 获取纹理 uniform是全局变量
uniform vec3 lightDir; //获取光照方向
varying vec2 uv;//获取uv坐标
varying vec3 norm; //获取法线坐标
void diffuseShader()
{
vec3 kd;//获取kd系数
kd = texture2d(myTexture, uv);
kd *= clamp(dot(–lightDir, norm), 0.0, 1.0); //Phong模型漫反射
gl_FragColor = vec4(kd, 1.0); //输出该像素的颜色
}
图形管线实现工具
-
集成显卡
-
独立显卡
GPU:多核心进行多现成并行计算
纹理映射
纹理映射就是定义任意点的基本属性。
每个三维的模型上的任意点都能对应在uv坐标上的某个点上。
纹理可以被重复使用
重心坐标
插值
为什么要插值?
为了获取平滑的过渡
插值的内容有哪些?
纹理坐标、颜色、法向量
如何做插值?
定义与性质
重心坐标是定义在三角形上的,在三角形ABC所形成的平面内任意一个点(x,y)都可以表示为三个顶点ABC坐标的线性组合。
仅需要满足α+β+γ=1的条件。若αβγ均非负数,则(x,y)点一定在三角形内。
A点的重心坐标为(1,0,0)
B点为(0,1,0)
C点为(0,0,1)
同时,重心坐标的αβγ可以使用它占三角形的总面积来表示,
例如AA的面积为(x,y)点与BC点的连线形成的小三角形,
因此也说明了αβγ相加为什么需要等于1。
重心可以表示为(α,β,γ)=(1/3,1/3,1/3)
插值的应用
因此可以通过获得ABC点的值来获取到三角形内任意坐标的位置、颜色、法线、深度等信息。
应用材质
将原本在各个顶点上的值,通过重心坐标插值到uv以及纹理坐标上的频幕上的每个采样点上。
纹理定义的值就是漫反射系数Kd
问题1:纹理太小了怎么办?-双线性插值
双线性插值(中)和双三线性插值(右)
双线性插值(Bilinear)
-
找到(s,t),s、t都在0-1之间
-
进行线性插值
-
先进行上下两点的水平插值,例如u0 =(u00+u10)s
-
再进行竖直的插值(u0+u1)t
-
-
得到红点对应的值,例如rgb值
问题2:纹理太大怎么办?-Mipmap
会造成进处出现锯齿,远处出现摩尔纹。
Mipmap只能做近似的正方形的范围查询。
-
level0是原始图像,每提高一个level,分辨率小一倍,利用相邻的四个像素的rgb做平均操作。
-
做mipmap比会增加原本图像1/3的额外存储量。
-
在屏幕空间中取当前像素相邻的像素带你并查询其对应的uv坐标。
-
计算出当前像素点与其他像素点距离其他像素点的最大值L。
-
根据最大值L通过上面公式计算得到该点所处的层数D。
通过mipmap操作得到哪些区域的像素要使用第几层进行平均操作。
过渡不平滑-三线性插值
如果算出来的D值是一个小数,这会造成图片Mipmap做错不平滑的问题。
如何解决——三线性插值
-
对该D值分别进行向下和向上取整。如D=1.2Z则取1和2
-
对两个D值分别进行双线性插值(见纹理太小)。
-
对两个插值的结果再做一次线性插值,如0.8xD1+0.2xD2
过度模糊-各向异性过滤
如果只使用Mipmap则远处会出现过度模糊的问题(完全糊成一块)。
如何解决——各向异性过滤
纹理中未必都是正方形的像素,因此运用mipmap查询会造成查询范围过大。
对矩形进行x轴或者y轴的压缩来进行各向异性过滤。
各项异性过滤可以解决矩形的纹理,但无法解决斜向的纹理。
解决斜向的纹理-EWA过滤。通过多次圆形的采样来解决过度模糊的问题。
纹理映射的应用
纹理=内存+范围查询(例如mipmap)
纹理是GPU上的一块内存,我们可以对内存做范围查询。
环境映射(Environment Map)
球面环境映射(Spherical Environment Map):
将环境光反射在球上就可以获得该场景的环境光。
球面映射(Spherical Map):
将球面环境映射可以展开为一张图,但是上下会被扭曲(类似世界地图)
立方体映射(CubeMap):
为了解决球面扭曲的问题,使用立方体来进行环境光照的纹理映射。
凹凸/法线贴图(Bump/Normal Map)
-
其最大的意义是为了表现相对高度来展示凹凸效果减少面数。
-
凹凸、法线贴图仅表示凹凸效果不会改变几何形体。
计算法线贴图(一维)
假设下图中的蓝点为p点()
-
p点原来的法线朝上,即n(p) = (0, 1)
-
下图蓝色曲线为使用法线贴图后的效果。
-
通过dp = (c[h(p+1) - h(p)])/1求出两点的高度差。其中c为常数表示凹凸贴图的影响程度,h为高度p点、p+1点对应高度。
-
因此切线可表示为(1,dp)。
-
切线与法线为垂直的关系因此n(p) = (-dp, 1)
计算法线贴图(二维)
二维的情况下有u、v两个方向的变换。
实际情况下法线方向不一定朝上,这里的例子是基于一个局部坐标系确定的。
-
n(p) = (0, 0,1)
-
dp/du = c1 * [h(u+1) - h(u)]
-
dp/dv = c2 * [h(v+1) - h(v)]
-
n = (-dp/du, -dp/dv, 1)
位移贴图(Displacement mapping)
-
位移贴图会真实改变模型,会展现出凸起部分的投影。
-
模型需要足够细致,采样需要足够高。
DirectX使用曲面细分来提高计算效率。
三维纹理
-
利用三维空间中的噪声函数进行纹理映射(例如Perlin Noise)
-
预先进行环境光遮蔽计算模型阴影
-
体渲染通过三维纹理记录信息,然后进行渲染
几何
几何表示方法的分类
-
隐式
-
可以通过一个函数来表示的几何体。
-
例如圆可以表示为f(x,y,z)=0
-
优点:可以很容易判断某个点是否在几何体上
-
缺点:难以通过函数判断出几何体的真实形状
-
-
显式
-
通过参数映射表示的几何体(uv坐标转换为xyz坐标)
-
参数映射:通过某个带有u,v的函数分别表示出x,yz的坐标
-
-
直接给出几何体
-
缺点:难以表示出某个点是否在几何体上。
-
优点:容易看出来几何体的真实形状
-
对于复杂的几何体十分不友好。
隐式几何
CSG(Constructive Solid Geometry)
复杂的几何体通过简单几何体进行集合运算(交并补)得到,该操作被称之为CSG。
距离函数
下图A与B相进行融合操作后得到blend(A,B),左边三分之一完全被挡住,中间被挡住一半,最后边完全没被挡住。
距离函数:任何一个点到达的最短距离。
通过AB距离函数相加得到融合后的SDF图,可以转化为blend(A,B)这张图。
距离函数可以将两个靠近的集合体进行融合。
距离函数通过水平集(LevelSet)得到F(X)=0(边界)
分型
分型这种递归问题在渲染中会引发严重的走样
显式几何
点云
-
用密集的点放在空间中
-
是某个坐标系下的数据集
-
每个点包含了坐标、颜色等一系列信息
-
只要采样足够密集,理论上可以表示任意集合体
-
如果采样不够密集,将会无法分辨模型的形状
-
应用:激光扫描
多边形网格
简介
-
讲面拆解为多边形(大多是三角形和四边形),存储顶点和多边形信息
-
在图形学中应用的最为广泛
如何储存多边形信息?
使用OBJ格式讲几何体的点、法线、纹理坐标分别表示,然后再表示,面与面的连接关系。
下图定义了一个立方体,有八个顶点(V),六个面(Vn)多个纹理坐标(vt)表示,然后使用f表示他们之间的关系((f V/Vt/Vn)
贝塞尔曲线
只要求一定要经过起止点,起止点之间的若干个控制点用于控制曲线弯曲的方向,最终形成一条经过起止点的光滑曲线被成为贝塞尔曲线。
德卡斯特里奥算法
通过德卡斯特里奥算法法来绘制贝塞尔曲线。
-
引入参数t(范围 为0-1)
-
取b0到b1,b1到b2上t位置的点b0‘,b1’
-
将b0‘,b1’连接
-
取b0‘到b1’t位置上的带你b0‘’
-
将所有的0-1所有的b0‘’点都遍历一份相连即可得到贝塞尔曲线
-
若有n个控制点则将上面步骤进行递归操作直到找到最终位移b0n
总结得到公式:
可以求得一个以t为自变量的函数,由这些点形成的集合构成贝塞尔曲线
其中:
-
b为n个贝塞尔控制点
-
Bn为伯恩斯坦多项式
其中:
(排列组合Cni)
例子:b0、b1、b2、b3为3d空间中的点,通过函数求得n个离散的点
-
对贝塞尔曲线做仿射变换只需要对控制点、起止点做仿射变换再重新绘制一遍即可。
-
对投影变换没有这样的性质
凸包
贝塞尔曲线拥有凸包的性质。
连接贝塞尔曲线最外围的控制点,将其相互连接形成一个封闭空间,画出来的贝塞尔曲线一定在凸包范围内。
若贝塞尔曲线是一个直线则凸包也是一个直线。
逐段贝塞尔曲线
当控制点太多会影响控制点的效果。
每四个控制点定义一条贝塞尔曲线,然后再将他们连接起来。
类似PhotoShop的钢笔工具
若想要逐段贝塞尔曲线平滑过渡则需要将相邻控制点共线(如3,5)否则会出现该曲线后边段的不平滑现象。
将两个逐段贝塞尔曲线中的两部分连接,连接点被称之为C0连续
若相邻两个控制点距离连接点相同且共线则该连接点被称之为C1连续(再连接处一阶连续可导)
贝塞尔曲面
与二维的贝塞尔曲线思想类似,但是需要定义两个参数u和v(取代之前的参数t)
先对f(u)进行遍历得到曲线,再嵌套遍历得到曲面f(u,v),类似于两层for循环进行遍历
网格操作
网格操作的分类
-
网格细分
-
让网格的面数更多
-
Loop细分
-
Catmull-Clark细分
-
-
网格简化
-
让网格面数更少
-
边坍缩
-
通过“二次误差度量”得到坍缩后最优的点
-
-
-
网格正规化
-
让网格中的三角形趋近于正三角形
-
Loop细分
-
将每个三角形变为4个三角形
-
根据权重指定新的顶点位置
-
新的顶点和老的顶点以不同的规则来改变自己的位置
-
对于新的顶点:
V'=3/8 * (A + B) + 1/8 * (C + D)
-
V'为新的顶点变换后的位置
-
A、B分别为于两个面被共享边的的老顶点
-
C、D为非共享边的两个顶点。
对于旧的顶点:
V'=(1 - n*u) * V + original_position * neighbor_position_sum
-
V'为旧的顶点变换后的位置
-
n为顶点的度(链接的顶点数)
-
如果n=3则u=3/16 其他情况u=3/(8n)
-
neighbor_position_sum为邻居点的平均位置
-
original_position为旧顶点原本的位置
问题:只能对完全为三角形的几何体进行细分
Catmull-Clark细分
相对于Loop细分的优势:可以用于任意不同的面的细分。
奇异点:顶点的度!=4
-
将面的中点和面上线的中点连起来
-
在第一次细分之后,非四边形面数量会加到原本奇异点的数量上
-
-
面上的中点(f)、边上的中点(e)、老的顶点(v)变化情况如下图
边坍缩算法
通过不断迭代进行边坍缩操作达到简化模型的目的。
边坍缩面临的问题:
-
坍缩哪些面?
-
如果优先坍缩不重要的面,那如何界定不重要?
-
坍缩后的顶点位置如何描述
二次误差度量
如果将减面时候的点直接平均将会得到左边的图,显然不理想。
通过二次误差度量得到右边的点得到理想效果。
边坍缩算法的步骤
-
对每一条边打一个分数,分数就是他坍缩后的二次误差度量
-
对分数最低(误差最小)的边做边坍缩
-
用到了Dijkstra最短路径算法
-
-
重新执行第一步知道完成整个模型的边坍缩
阴影映射(Shadows mapping)
基本流程
-
从光源看向场景并做深度测试
-
从视锥体位置看向场景,从场景中看到的点投影回光源得到该点在深度图中的位置
-
有些点可以被视锥体看到也可以被光源看到(视锥体投影到的深度图与光源中的深度图相同)
-
有些点可以被视锥体看到但被光源看到(视锥体投影到的深度图与光源中的深度图)
-
-
不能被光源看到的位置但可以被视锥体看到的位置就是阴影的位置
出现的问题
-
只能做硬阴影不能做软阴影(目前以及有技术可以做到)
-
由于shadow map的分辨率问题,阴影可能出现锯齿
-
浮点精度问题,可能在是否能同时被视锥体与光源看到的界定上出现问题引发误差
软阴影
软阴影的边缘比较模糊,没有硬阴影锐利的边缘
下图以日食为例子介绍了软阴影在真实物理中形成的原因:
光线追踪
为什么要使用光线追踪?
光栅化难以将以下的效果做好:
-
软阴影
-
毛玻璃材质的反射
-
间接光照
光栅化虽然较快,但是质量较低。
光线追踪的几个假设
-
光沿直线传播(虽然这是错的)
-
光线与光线之间不会发生碰撞(虽然这是错的)
-
光线从是从光源不断传播直到视锥体的(光线的可逆性)
光线投射
光线投射的假设:
-
出射点是一个点
-
光源点光源
-
场景物体中的反射为镜面反射
光线投射的步骤:
-
从初射点穿过成像平面打出一根光线到场景中
-
找到与场景的最近交点
-
将交点和光源连接, 判断物体是否在阴影中
-
计算着色情况写回像素中
Whitted-Style光线追踪
渲染速度
对于下面这张图:
-
1979年需要74分钟
-
2016年需要6秒
-
2012年只需要1/30秒
递归光线追踪
-
光线不仅仅只会反射,还会折射、然后再与其他物体进行反射
-
光线每次反射折射都会有能量损耗,不然经过无限次的累加只会变成白色
-
递归过程需要设置一个最大次数
-
两次反射和折射情况如下图可视
确定光线与场景的交点
光线方程
对于每一束光线都满足以下方程:
其中o为开始点,d为方向(单位向量),t为时间
r(t)=o+td 0<=t<∞
求交点:
对于任何隐式的集合体,将r(t)以p点带入隐式的方程中算出f(o+td)=0即可算出t求出交点。
显式几何
-
几何体由若干个面组成
-
在几何体的面上判断是否与他的面相交
-
几何上的平面由一个法线和一个点p‘表示
步骤:
-
可使用(p-p')·N=0表示一个平面
-
将光线方程以p带入光线方程中
-
求出t算出交点
-
判断交点在三角形的内还是外
-
若在内则是三角形的交点
Möller Trumbore算法
中文名: 射线三角相交算法
可以更快的求三角形与射线的交点
-
已知 光线满足r(t)=o+td
-
P0、P1、P2为三角形三个顶点
-
可以得到以下等式
-
通过以下E1、E2等的参数定义可以解出等式得到交点
光线追踪加速
如果三角形面特别多,以上面的算法进行计算将会特别慢
轴对⻬包围盒(AABB)
特殊的包围盒定义
三对面的交集形成的立方体,三对面分别于xyz轴平行
使用包围盒包围与光一定轴对齐,减少了计算量
原始方案中:
需要3次减法,6次乘法和1次除法。
AABB中:
每一个轴只需要一次除法和一次减法,一共只需要3次减法和三次除法
这么定义包围盒的好处:
只有三对面都有光线相交(进和出)时间才能证明光线已经进入包围盒
这也定义可以先判断光线是否经过该包围盒,如果不经过就不再进行更多的操作,节省了计算时间。
判定光线与包围盒的交点
对于二维的包围盒
-
先判断与x这对面与光线相交(进和出)的时间tmin、tmax
-
再判断y与光线的相交时间
-
取交集(求出tmin的最大值,tmax的最小值)
三维与二维类似:
-
增加了一对平面,判断三对面的相交时间tmin、tmax
-
求出tmin的最大值,tmax的最小值
-
若tmax-tmin大于0则说明光线与包围盒相交
几种负值的情况的处理:
-
tmax<0则说明包围盒在光线背面(无交点)
-
tmin>=0且tmin<0则说明光源再包围盒中(有交点)
总结:AABB有交点当且仅当tmin<tmax且tmax>=0
空间划分
AABB的均匀划分
AABB均匀划分的步骤:
-
找到包围盒(最外层的正方体)
-
建立网格(黑色网格)
-
标记与包围盒相交的网格(灰色标记)
-
从光线发射方向逐个遍历网格
-
将每个遍历到的网格测试与其的交点
建立网格的目的:
-
通过网格可以判断有tmin、tmax
-
若有则对网格内的物体进行交点判断
网格的密度认为数量大约为27x物体数量
但是这种网格定义方式对物体分布不均匀的场景中不友好(即使这样的网格也很常用)。
因此引入空间划分来切割网格。
空间划分的分类
八叉树:
-
八叉树是在每个子树下面画十字,划分为四块(在二维下是分为四块,三维是八块)
-
由于是均匀划分,会出现将同一个物体划分为两块的问题。
KD树:
-
每次划分只划分为两块(类似二叉树)
-
在二维中第一次为水平的划分,第二次为竖直的,然后循环划分
-
在三维中类似,以xyz轴顺序进行划分
-
这样可以保证划分比较均匀
-
我们在AABB中主要使用KD树(曾经)
BSP树:
-
划分和kd树类似
-
由于划分不是横平竖直的不能用于AABB的划分
KD树
建立KD树
通过一次对x,y(二维)进行递归划分,得到下面的KD树
遍历KD树
-
假设有一条光线射出
-
逐个对每个子树进行判断是否有与包围盒相交
-
若相交则对他的子树继续遍历知道将找到所有与光线相交的包围盒
-
将其包围盒下的物体查找交点
KD树的问题:
-
难以判断物体与包围盒边界的相交问题
-
一个物体容易同时穿过多个包围盒被重复计算影响性能
因此KD树的应用场景越来越小。
目前广泛使用的技术名叫层次包围盒(BVH)
层次包围盒(BVH)
BVH的特点:
-
按照三角形进行划分,因此一个物体只可能出现在一个包围盒内
-
由于按照三角形进行划分,因此不会出现那一判断包围盒边界的问题
-
BVH的思想更像分组,只需将三角形进行分组并对三角形较多的组进行递归分组
-
每次的划分都选择XYZ中最长的轴进行划分,这样保证划分的大小比较平均
-
总是选择中间的三角形进行划分,这样划分出来树更加接近平衡二叉树
-
划分到一个比较小的数量后停止划分(比如5个三角形)
BVH的伪代码:
Intersect(Ray ray, BVH node)
{
if (ray misses node.bbox)
return;//如果与节点光线都不相交就返回
if (node is a leaf node)//如果相交且这是一个叶子节点
{
test intersection with all objs;//将节点内三角形都做判断
return closest intersection;//返回最近的那个
}
hit1 = Intersect(ray, node.child1);//如果不是叶子节点则递归找到最近的那个
hit2 = Intersect(ray, node.child2);
return the closer of hit1, hit2;
}
辐射度量学
由于Whitted-Style形成的光线追踪体系其实并不够真实,引入辐射度量学
同时定义了以下几个属性来描述光照:
-
Radiant Energy(辐射能量)
-
Radiant Flux(辐射通量)功率
-
Radiant Intensity(辐射强度)光源发出的某一方向上的亮度
-
Radiant Irradiance(辐射光照度)某一平面所接受到的光线亮度
-
Radiant Radiance(辐射亮度)一条传播光线所具有的亮度
Radiant Energy
光线照射下辐射出来的能量,单位为焦耳,用Q表示。
Radiant flux
-
单位时间内的能量消耗,类似于功率,单位为瓦/流明*
-
单位时间内通过一个感光平面光子的数量
Radiant Intensity
角
弧度制:θ=l/r
l:弧长
r:半径
一个圆形有2π个弧度
立体角
Ω=A/r2
A:球面上的面积
立体角是弧度制在三维上的延申
一个球有4π个立体弧度
微分立体角
-
首先确定空间中的方向θ和
-
其中rdθ是微分面积元的高,rsinθdϕ 是微分面积元的宽
-
两者相乘即可计算出球的投影面积A
同时可以验证立体角在球上的积分为4π
在辐射度量学中用Ω来表示方向,用θϕ定义方向
定义
-
每微分立体角(某个方向)上的功率(光强),单位为W/sr又被称作为cd(坎德拉)
-
(光源在某个方向上的亮度)
应用:点光源
-
定义一个点光源所有方向的亮度都相同
-
flux为I在所有方向上的积分
-
得出I=ϕ/4π
Radiant Irradiance
单位面积上(某个点)接收到的光照功率,单位为W/m2 或者lm/m2 又称Lux(勒克斯)
在此可以解释布林冯模型中光线亮度需要乘cosθ
同时也能解释模型中光线强度的衰减
Radiant Radiance
定义
单位微分立体角,单位面积的功率,单位为nit(尼特),1nit=1 cd/m²
Irradiance与Radiance的区别
-
Irradiance是对于一个点所接受到的所有的光线
-
Radiance是对于一个点面向某个方向接受到的光线
注意:H²为半球
双向反射分布函数(BRDF)
-
BRDF定义了某个点接受到能量后反射能量与入射光的比例
-
表示指定方向的反射光和入射光的比例关系
-
的不同决定BRDF方程的不同
反射方程
借助BRDF可以定义出反射方程,即在某个反向接受到的所有的反射光线。
如上图,反射到的光线是由入射光乘以BRDF这一反射比例得到的。
渲染方程
定义
-
渲染方程由两部分组成,自发光Le和反射光
-
其中反射光可以由光源直射也可以由其他的物体反射
-
在反射方程的基础上加入自发光,定义了渲染方程
-
渲染方程定义了所有的光线传播规律
-
n·ωi与cosθ一致
-
Ω为半球
单个光源的反射方程
点光源只有一个方向有入射光,所以不用积分
多个光源的反射方程
面光源的反射方程
面光源是点光源的集合因此对面光源所在立体角进行积分可以得出面光源的反射方程
面光源的渲染方程
-
将其他物体反射过来的光当成光源,得到渲染方程
-
只需要计算反射光源其余的自发光、BRDF(材质)、cosθ均已知
方程简化
-
目前只有出射点反射出去的能量和入射点反射过来的能量未知
-
可以将渲染方程最终简化为下面的形式方便理解
最终可以将渲染方程:
-
在一个场景内能量守恒
-
L为所有的物体辐射出的所有能量
-
E为光源分辐射出来的能量
-
KL为光源辐射出来的能量被反射出来的能量
解出L并通过二项式定理得到下面的式子。
-
L=E为场景内自身辐射的能量
-
L=KE为场景内辐射的能量+一次反射的能量
-
L=KE+K²E为场景内辐射的能量+一次反射的能量+二次反射的能量
-
以此类推......
蒙特卡洛路径追踪
蒙特卡洛积分
对函数的积分域多次采样求均值作为积分的近似值
例子:
将积分均匀采样,每个采样的概率都是1/b-a,得出以下式子。
其中将b-a移到后面可以得到最开始蒙特卡洛积分的式子。
Witted-Style的问题
路径追踪解决了在Witted-Style中不正确的部分
-
无法完成Glossy的材质,在Glossy材质中光线打到材质上不完全沿着Specular的方向走
-
漫反射后仍然会多次反射,需要引入全局光照
蒙特卡洛积分应用到渲染方程
将某个渲染点p的ωi方向进行多次采样找到反射光源的角度得到蒙特卡洛积分
通过这样的方法可以算出任意着色点的渲染方程
直接光照的算法
shade(p, wo)
Randomly choose N directions wi~pdf
Lo = 0.0
For each wi
Trace a ray r(p, wi)
If ray r hit the light
Lo += (1 / N) * L_i * f_r * cosine / pdf(wi)
Return Lo
全局光照的算法
shade(p, wo)
Randomly choose N directions wi~pdf
Lo = 0.0
For each wi
Trace a ray r(p, wi)
If ray r hit the light
Lo += (1 / N) * L_i * f_r * cosine / pdf(wi)
Else If ray r hit an object at q
Lo += (1 / N) * shade(q, -wi) * f_r * cosine / pdf(wi)
Return Lo
如果采样中反射过来的不是光源而是物体则将物体也当做光源,将物体反射过来的能量来进行计算。
上述算法的产生的问题
反弹次数的上升产生射线数量爆炸
一根光线打到物体后会反射很多个光线到同一个物体,以此类推产生指数爆炸、
因此如果蒙特卡洛积分采样次数为1则不会出现指数爆炸的现象。
路径追踪算法:
shade(p, wo)
Randomly choose ONE direction wi~pdf(w)
Trace a ray r(p, wi)
If ray r hit the light
Return L_i * f_r * cosine / pdf(wi)
Else If ray r hit an object at q
Return shade(q, -wi) * f_r * cosine / pdf(wi
只采样一次的光线追踪被称之为路径追踪,上面采样N次的被称之为分布式光线追踪(Distributed Ray Tracing)
生成多个路径进行路径追踪:
对于每个像素发射N条光线(采样)做以下的算法
将每个射出去的采样接收到能量的点做蒙特卡洛积分得到平均值
ray_generation(camPos, pixel)
Uniformly choose N sample positions within the pixel
pixel_radiance = 0.0
For each sample in the pixel
Shoot a ray r(camPos, cam_to_sample)
If ray r hit the scene at p
pixel_radiance += 1 / N * shade(p, sample_to_cam)
Return pixel_radiance
由于路径追踪递归产生的死循环
-
首先增加一个结束的概率P_RR
-
每次调用shade函数的时候做一个随机数判断
-
若大于随机数则return0反之继续执行shade函数并返回L0/P_RR
-
这样算下来数学期望不会变,E(L0)=P∗(L0/P)+(1−P)∗0=Lo
-
且路径追踪总会停下来
shade(p, wo)
Manually specify a probability P_RR
Randomly select ksi in a uniform dist. in [0, 1]
If (ksi > P_RR) return 0.0;
Randomly choose ONE direction wi~pdf(w)
Trace a ray r(p, wi)
If ray r hit the light
Return L_i * f_r * cosine / pdf(wi) / P_RR
Else If ray r hit an object at q
Return shade(q, -wi) * f_r * cosine / pdf(wi) / P_RR
提高效率
每个像素中的采样点越多,形成的效果越好,比如下图中最右边的例子需要50000个采样才能找到光线,大多数的采样被浪费了
因此为了提高效率我们可以通过找到光源与方向的关系,改写渲染方程,将渲染方程写成对光源的积分
光源与方向的关系式:
改写的渲染方程:
我们最终渲染出来的光纤传播分解为两个部分
-
光源的直接光照
-
光源对其他物体的弹射(需要用到上面的随机算法)
如果光源中有物体挡住则不能渲染光源的直接光照,通过一个if来解决
shade(p, wo)
# 光源的直接光照
L_dir = 0.0
Uniformly sample the light at x’ (pdf_light = 1 / A)
Shoot a ray from p to x’
If the ray is not blocked in the middle
L_dir = L_i * f_r * cos θ * cos θ’ / |x’ - p|^2 / pdf_light
# 光源对其他物体的弹射
L_indir = 0.0
Test Russian Roulette with probability P_RR
Uniformly sample the hemisphere toward wi (pdf_hemi = 1 / 2pi)
Trace a ray r(p, wi)
If ray r hit a non-emitting object at q
L_indir = shade(q, -wi) * f_r * cos θ / pdf_hemi / P_RR
Return L_dir + L_indir
其他的知识
-
点光源不容易处理,因此建议写成一个很小的面光
-
路径追踪可以做到几乎100%的真实
-
光线追踪有很多类型
-
(单向和双向)路径跟踪
-
光子映射
-
Metropolis light transport(MLT)
-
VCM / UPBP
-
-
课程中光线追踪未涉及的部分
-
函数采样理论
-
选择什么样的PDF(重要性采样)
-
随机数的生成
-
结合不同的采用结果(如光源和着色点)
-
像素发出多个路径,是否平均其着色效果即可(pixel reconstruction filter)
-
像素的radiance和color的区别(伽马矫正)
-
材质与外观(BRDF)
材质由BRDF决定,或者说材质就是BRDF。
漫反射材质
-
对于一个漫反射材质的物体,我们认为物体本身没有自发光(因此没有Le),且不会吸收光线,照射过来的光线会均匀的反射到四周。
-
且假设入射光是均匀的
-
因此Lo=Li,反射的能量等于收到的能量,因此fr(BRDF)=1/π
-
我们可以引入反射率(albedo)ρ,取值位于0~1让物体能够接受能量获得不同颜色的BRDF
-
下面Lo得出了漫反射材质的渲染方程
镜面反射
-
在正视图中(左边的图),镜面反射的入射角和出射角可以通过平行四边形法则借助法线方向得出左边的关系
-
在俯视图,又称方位角(右边的图)可以得出右边的关系
-
通过下面的公式可以算出出射角的方向
折射定律
斯涅耳定律
-
引入折射率,右边是常用介质的折射率
-
比如从空气射入水则ηi为1,ηt为1.333
-
通过上面的公式可以计算出出射角的角度
全反射
-
当出射角无意义即根号小于0((ηi/ηt)>1)时候,则物体不会发生折射,这种现象被称为全反射
-
例如人在水底往上看,由于发生全反射现象,只能看到一小块锥形的区域,这也被称为斯内尔窗
菲涅尔项
-
当入射角与物体相互垂直的时候,反射会更加明显
-
菲涅尔项对绝缘体会更加明显(下1图),对导体来说并不明显(下2图)-因为导体的折射率是负数。
菲涅尔项公式:
-
上面的公式是准确的,但是计算十分麻烦,需要考虑极化
-
下面的公式名为 Schlick’s approximation,是一个近似解,图形学中广泛使用。
微表面材质
-
物体表面即使是粗糙的,但是从远处看,我们可以将其看做是一个平的表面
-
例如在空间站中看地球,也可以看到高光反射的地方
-
从远处看是材质,从近处看是几何
-
如果一个表面法线分布集中我们认为他是glossy材质,如果分布分散则认为是漫反射材质
第一个F是菲涅尔项,决定多少能量被反射(垂直时候被反射的能量多,反之少)
第二个G是几何项,计算微表面上的相互遮挡(在grazing angle平着照射微表面的时候容易出现互相遮挡)
第三个D是法线分布,当微表面的法线方向与给定的出入射角的半程向量一定才证明这个微表面的入射方向出射方向正确
各向同性/各向异性材质
-
材质的性质进行累加
-
可逆性,对换入射角和出射角可以得到同样的BRDF
-
能量守恒,出射的能量不可能比入射的能量少
-
各向同性和各向异性
BRDF的测量
实际测量出来的BRDF和公式推算出来的BRDF经常会有很大差距
-
可以通过仪器来枚举出测试样例的所有测试点进行BRDF的测量
-
当然这样测量十分的麻烦,我们可以利用BRDF的性质来简化测量
-
比如各向同性的材质只需要测量半圈(由于只有三维且利用可逆性可以只测量半球