TOC
Open TOC
Info
https://sites.cs.ucsb.edu/~lingqi/teaching/games101.html
https://www.bilibili.com/video/av90798049
计算机图形学概述
课程概述
Gold standard in Video Games (Real-time Applications)
- Curves and Meshes
- Ray Tracing
Gold standard in Animations / Movies (Offline Applications)
计算机图形学和计算机视觉
No clear boundaries.
向量与线性代数
内积
Forward / backward (dot product positive / negative)
外积
Left / right (cross product outward / inward)
右手系
标准正交基
矩阵
变换(二维与三维)
Homogeneous coordinates
注意到平移不是线性变换
于是引入齐次坐标形式,添加第三个分量
对于 point 而言
对于 vector 而言
基本的运算如下
保持了相当的几何意义
注意 point 加上 point 定义为两个 point 的中点
于是对于仿射变换,可以统一为线性变换的形式
注意是先做线性变换,再做平移
于是统一了平移和线性变换
对于 3D Transformations,可以类似的推广
PA0
eigen
https://eigen.tuxfamily.org/index.php?title=Main_Page
cmake
使用现代 cmake
target_link_libraries 似乎不必要
构建脚本 run.sh
note
简单的翻译一下即可
输出为
简单的验证一下
变换(模型、视图、投影)
对于三维变换,有类似的齐次坐标形式
缩放和平移都十分 easy,下面考虑旋转
由于旋转的自由度变大了,先考虑简单的情形,即绕坐标轴旋转
注意到
x×y=zx×z=−yy×z=x
所以绕 y 轴旋转的矩阵有所不同
有了这三个矩阵,就可以描述三维空间中任意的旋转,这便是欧拉角
详见
另外还有一个公式叫罗德里格旋转公式
详见
想象在拍照片
- Find a good place and arrange people. (model transformation)
- Find a good angle to put the camera. (view transformation)
- Cheese! (projection transformation)
主要介绍后两种变换
主要是摆放相机的位置
首先需要描述相机的位置
注意到物体和相机的相对位置
我们简化为,相机位于原点,头顶 y 轴,朝向 -z 轴,即
e={0,0,0}g^={0,0,−1}t^={0,1,0}
下面考虑如何将相机变换到这个位置,这样物体只要使用相同的变换,就能保证相对位置不变
先做平移,再做旋转
第二步的关键在于,旋转变换矩阵是正交矩阵,即
MT=M−1
所以可以考虑逆变换,即将坐标轴旋转到相应的位置
分为正交投影和透视投影
Orthographic Projection
We want to map a cuboid [l, r] x [b, t] x [f, n]
to the canonical cube
left, right, bottom, top, far, near
由于相机朝向 -z 轴,所以 far 的 z 坐标更小
单位立方体就是
变换矩阵不难得到
Perspective Projection
使用化归的思想,先将台体变换为长方体,再使用正交投影
这里有一些假设
- near 平面不变
- far 平面的 z 坐标不变
- far 平面的中心点不变
根据相似三角形关系,可以得到 x 分量和 y 分量的变换关系
y′=znyx′=znx
于是有
M⋅xyz1=nynx?z
注意对齐次坐标中的 point 各分量数乘某个非零值并不改变 point
可以不严谨的得到
M=n0?00n?000?100?0
下面根据假设,取 near 平面上的某点,在变换后不变,即
M⋅xyn1=nynxn2n
取 far 平面的中心点,在变换后不变,即
M⋅00f1=00f2f
综合诸式,可以不严谨的得到
M=n0000n0000n+f100−nf0
再左乘正交投影的矩阵即可
**思考题:**对于 near 平面和 far 平面的中间平面,考虑其中心点
M⋅002n+f1=00n+fn2+f21
注意到
2n+f≤n+fn2+f2
所以实际上会变远
光栅化(三角形的离散化)
接着上节课的内容
当得到了一个单位立方体后,我们需要将其绘制在屏幕上
为简化考虑,我们忽略 z 分量,并做如下的变换
[−1,1]2→[0,width]×[0,height]
这里的 width 和 height 是屏幕的参数
更具体的,屏幕实际上是像素的二维数组
而光栅化表示在屏幕上绘制像素
目前我们使用 RGB 表示像素
下面定义 screen space
注意区分像素和像素的下标
如此,Viewport transform matrix 易得如下
M=2width00002height0000102width2height01
Rasterization
Different raster displays
略
Rasterizing a triangle
绘制的基本单元为三角形,理由如下
当我们得到三角形的三个顶点的坐标后,需要以像素的形式绘制出来
最基本的想法是——采样
使用一个二重循环即可
注意像素的中心需要加上 0.5
而 inside 函数用来判断该像素是否在三角形内部
显然,我们需要使用外积
当下面三个式子同向时
AB×APBC×BPCA×CP
P 点就在三角形内部
一些其他议题:
PA1
opencv
run
可以直接 ./run.sh
,按 ESC 退出
也可以指定参数
note
两个函数的实现是简单的
提高项需要使用罗德里格旋转公式,就不做了
先分析一下三个变换矩阵
实际上就是绕 z 轴旋转
通过命令行指定 angle
也可以通过按 A 键与 D 键动态的变换 angle
实际上就是移动相机的位置
使用 eye_pos 描述
给出了 field-of-view (fov) 和 aspect ratio
有如下的关系
tan2fov=∣n∣taspect ratio=tr
进而得到 left, right, bottom, top, far, near
需注意这里假定了对称性 l = -r, b = -t
然后先透视再正交投影即可
pic
修正透视投影矩阵后
光栅化(深度测试与抗锯齿)
这一讲主要分析和处理 Aliasing Artifacts 问题
当采样频率低于实际信号的频率时,就会出现 Aliasing Artifacts
而一种解决方案就是进行 Pre-Filter 后,再进行 Sample
注意顺序
下面解释
- Why undersampling introduces aliasing?
- Why pre-filtering then sampling can do antialiasing?
Why undersampling introduces aliasing?
从 spatial domain 和 frequency domain 两个角度来理解
祭出傅里叶变换
从 spatial domain 来理解
从 frequency domain 来理解
- Sampling = Repeating Frequency Contents
- Aliasing = Mixed Frequency Contents
这里似乎有关系,采样频率越低,在 spatial domain 间隔越大,在 frequency domain 间隔越小
具体见课件,反正不太懂
Why pre-filtering then sampling can do antialiasing?
在无法提升采样频率的前提下,使用低通滤波器过滤高频信息,可以避免 Mixed Frequency Contents
而过滤,在 spatial domain 中就是卷积,就是取平均值,在 frequency domain 中就是相乘
Antialiasing in practice
以此为基础,有如下解决 Aliasing Artifacts 的方案
其他技术
- FXAA (Fast Approximate AA)
- TAA (Temporal AA)
- Super resolution / super sampling
- DLSS (Deep Learning Super Sampling)
着色(光照与基本着色模型)
Z-buffering
光栅化最后的内容
主要解决 visibility / occlusion 的问题
思想如下
- Store current min. z-value for each sample (pixel)
- Needs an additional buffer for depth values
- frame buffer stores color values
- depth buffer (z-buffer) stores depth
算法伪代码如下
两个注意点
- 为了方便处理,我们将 z 进行了反转,保证都是正数,并且越大表示离视点越远
- 若不使用抗锯齿,就是以像素为单位,否则可能以采样点为单位
一个简单的例子
若每个三角形占据常数个像素,算法复杂度与三角形个数成线性关系
Shading
我们给着色一个定义
- In Merriam-Webster Dictionary
The darkening or coloring of an illustration or diagram with parallel lines or a block of color.
The process of applying a material to an object.
下面考虑一个简单的着色模型
Blinn-Phong Reflectance Model
从上到下依次为
需要给着色点进行形式化描述
也就是考虑观察方向,光线方向和表面的法向
均为单位向量
需要注意着色具有局部性(近似为平面才有法向),因而不会因为光线的遮挡而产生阴影
shading ≠ shadow
下面给出漫反射的一个近似公式
注意里面的 I 为光能,r 代表光源和着色点的距离
通过球壳表面光能固定推导出平方反比的关系
而 k 是材质与漫反射相关的系数
注意对于漫反射而言,无论观察方向,着色都是一致的
PA2
color
三角形一
三角形二
run
可以这样指定参数
note
先创建 bounding box
然后遍历每个像素,注意像素中心的屏幕空间坐标需要加上 0.5
使用 insideTriangle 判断是否在三角形内部
这里将参数改成了 float
使用三维向量进行外积,z 分量为 0
最后只要看外积出的向量的 z 分量即可
(a1,a2,a3)×(b1,b2,b3)=(a2b3−a3b2,a3b1−a1b3,a1b2−a2b1)
然后计算出插值深度值,并与深度缓冲区中的相应值进行比较
这里奇怪的是需要对插值深度值取负,否则深度关系就反了
感觉哪里实现错了
最后更新 depth_buf 和 frame_buf 即可
使用 get_index 函数将二维坐标转换为一维坐标
后来发现是透视投影矩阵有点问题
由于传入 get_projection_matrix
函数的 zNear 和 zFar 是正数
所以实际上要将透视投影矩阵的 n 和 f 取负
M=n0000n0000n+f100−nf0
简单的做法就是这样
M=n0000n0000n+f−100−nf0
这样的话就不需要对插值深度值取负了
pic
修正透视投影矩阵后
大小有点怪怪的
antialiasing
框架不变,只是在填充 frame_buf 时取平均
确实优化了一丢丢
着色(着色频率、图形管线、纹理映射)
Blinn-Phong reflectance model
接着上节课的内容
下面给出高光的一个近似公式
这里进行了一次转化,对于高光而言,可以近似认为是镜面反射,设反射光为 I’
那么有角度关系
α=arccos(n,h)=21arccos(I′,v)
也就是转化为半程向量和法向量之间的夹角
而常数 p 可以用来控制 reflection lobe
对于环境光而言,在固定光能和系数的情形下,可以认为是常数
于是 Blinn-Phong Reflectance Model 总结如下
Shading Frequencies
一图胜千言
Flat 是每个三角形视为一个着色点
Gouraud 是每个三角形的顶点视为一个着色点,顶点的法向量通过对周围三角形的法向量取均值得到
Phong 是每个像素视为一个着色点,像素的法向量通过插值得到
Graphics (Real-time Rendering) Pipeline
这里相当于总结了目前所学的内容
引入了顶点处理的步骤
从而着色可以选择在 Vertex Processing 中进行,着色频率为 Gouraud
也可以选择在 Fragment Processing 中进行,着色频率为 Phong 或 Flat
Shader Programs
着色器就是在某些阶段处理着色的程序
Example GLSL fragment shader program
描述了对单个顶点或像素进行的操作
另外推荐一个网站 https://www.shadertoy.com/
GPU
实现图形管线的硬件便是 GPU
多核心,高度并行
Texture Mapping
独眼巨人警告
相当于从屏幕空间到纹理空间的映射
定义了 Blinn-Phong reflectance model 公式的中的 k 值
我们以漫反射的 k 值为例
得到纹理空间的坐标后,还可能需要插值处理
感觉图中箭头指向的位置不太对
着色(插值、高级纹理映射)
Barycentric Coordinates
之前在许多地方都遇到了插值
通过三角形的重心坐标实现插值,公式如下
面积关系可以直观的理解
△ABP:△BCP:△CAP=γ:α:β
我们以插值颜色为例
需要注意在投影变换下插值坐标可能发生变化
所以三维的属性在投影之前就应该进行插值
Texture antialiasing
What if the texture is too small?
纹理的分辨率太低
我们定义 pixel on a texture 为 texel
下面介绍双线性插值
红色点是映射之后的坐标,而黑色点则是 texel
找最近的四个点,在两个方向线性插值即可
可以认为是正方形的重心坐标
当然还有效果更好的插值方法,如双三次插值 Bicubic Interpolation
What if the texture is too large?
通常是映射后所占据的纹理空间过大
当然超采样是一种解决方案
我们考虑换一个思路,之前都是试图求纹理空间某个点对应的值
现在我们尝试求纹理空间某个范围对内应的统计量,如平均值或极值
这实际上就是二维的 range queries
有一个近似的解决方案为 Mipmap
Allowing (fast, approx., square) range queries
先对纹理空间进行预处理,所占用的额外空间不超过原占用空间的三分之一
然后通过某种方式得到查询范围的边长 L,并计算层级 D
D=log2(L)
接着在层级中进行双线性插值
最后在层级间进行线性插值,因为 D 可能不是整数
示意图如下,这也被称为三线性插值
然而 Mipmap 对非正方形的处理误差会较大
于是便有了各向异性过滤 Anisotropic Filtering,来处理矩阵的情形
所占用的额外空间不超过原占用空间的三倍
然而各向异性过滤对于不规则的空间仍不易处理,可以参考 EWA filtering
PA3
run
似乎不支持实时旋转
interface
需要使用的 eigen 方法
cwiseProduct
对应元素相乘
norm
计算 L2 范数
normalized
单位化
note
首先需要修改一下 models 的路径
修改函数 rasterize_triangle
依次实现颜色、法向量、纹理颜色、纹理坐标的插值
最终的颜色通过调用着色器函数 fragment_shader
得到
另外 insideTriangle
的参数类型有细微变化,Vector3f -> Vector4f
使用框架代码,将整型改为浮点型
修改函数 get_projection_matrix
之前的实现没有考虑到 ortho_trans
可以通过注释 clang-format
调整自动格式化的范围
修改函数 phong_fragment_shader
,通过 Blinn-Phong reflectance model 计算 Fragment Color
直接看有纹理的版本
修改函数 bump_fragment_shader
,实现 Bump mapping
这是后面的内容,属实有点超纲
注意 h(u,v) = texture_color(u,v).norm
其中 u,v
是 tex_coords
w,h
是 texture
的宽度与高度
displacement_fragment_shader
相当于多了 Blinn-Phong reflectance model 的部分
pic
感觉视角和 Bump mapping 实现的有点问题
pa3-normal
pa3-phong
pa3-texture
pa3-bump
pa3-displacement
todo
几何(基本表示方法)
Applications of textures
着色最后的内容
一些纹理的应用
In modern GPUs, texture = memory + range query (filtering)
General method to bring data to fragment calculations
Environment Map
环境光贴图可以让模型反射出周围环境的样子
Bump Mapping / Normal mapping
使用纹理空间表示 height 或 normal
以二维的 Bump Mapping 为例,扰动法线
Displacement mapping
已经不太懂了
https://zhuanlan.zhihu.com/p/340786255
Ambient occlusion texture map
预先计算环境光遮蔽,并反映在纹理上
shadow
3D Textures
Introduction to geometry
Implicit Representations
Inside/Outside Tests Easy
解析式
f(x,y,z)=x2+y2+z2−1
- Constructive Solid Geometry
使用布尔运算组合
记录到物体的最短距离
例如直接线性插值,中间部分就是灰色
使用有向距离函数变换后再 blend
中间部分的距离就是零,变换回来正好左边黑右边白
类似等高线
分形结构
Explicit Representations
Sampling Is Easy
- All points are given directly or via parameter mapping
参数方程
f:R2→R3;(u,v)→(x,y,z)
如
f(u,v)=(cosu⋅sinv,sinu⋅sinv,cosv)
几何(曲线与曲面)
Explicit Representations
顾名思义,一堆密集的点形成几何图形
存储顶点和多边形
- The Wavefront Object File
(.obj)
Format
PA3 的 models 文件夹里有这种类型的文件
https://en.wikipedia.org/wiki/Wavefront_.obj_file
Curves
主要讲贝塞尔曲线 Bézier Curves
de Casteljau Algorithm
直接从代数的角度计算
以三次贝塞尔曲线为例,有四个控制点,一个参数
所以贝塞尔曲线是 Explicit Representations
控制点之间做线性插值,得到新的控制点
然后递归进行,最终得到对应参数的点
以二次贝塞尔曲线为例
观察形式类似于二项展开
不难推出,对于 n 次贝塞尔曲线,有
Properties of Bézier Curves
- 起点和终点与两端的控制点重合
- 可以计算出曲线两端的切线方向和大小
以三次贝塞尔曲线为例
求导即可
Piecewise Bézier Curves
分段贝塞尔曲线
通常使用三次贝塞尔曲线
https://math.hws.edu/eck/cs424/notes2013/canvas/bezier.html
下面讨论连续性
- 不中断可以保证 C0 连续
- 而切线方向和大小相同可以保证 C1 连续
Other types of splines
Bézier Curves 的超集
Surfaces
Bezier Surfaces
多引入一个参数
计算思路大致相同,只不过多一个维度
Mesh Operations
处理曲面的多边形
- 细分 - 第三幅图
- 简化 - 第四幅图
- 规范化 - 第五幅图
更多内容在下一节课
PA4
note
相当水
另外还实现了通用的 naive_bezier
使用了 C++17 中的 beta
函数计算二项展开系数
需要修改 mouse_handler
和 main
中的控制点数量以实现任意阶的贝塞尔曲线
还有光栅化的时候坐标似乎不是整数
为什么 x 和 y 颠倒了
pic
五阶贝塞尔曲线
同时使用 bezier
和 naive_bezier
绘制
todo
几何(网格处理)、阴影图
Mesh Subdivision
Loop Subdivision
Loop 是人名
只对三角形面有效
分两步
- Split each triangle into four
- Assign new vertex positions according to weights
- New / old vertices updated differently
对于新生成的顶点,根据下图分配权重
对于原本的顶点
需要得到该顶点的度 n,然后根据 n 设置权重 u
u={1638n3n=3otherwise
这样可以保证当 n 较小时,顶点自身的权重较大,而当 n 较大时,顶点自身的权重较小
Catmull-Clark Subdivision
可用于任意多边形面
同样分两步走,首先看如何 split
第一步,在每个面的中心添加一个点,该点与这个面各边的中点相连
第一步过后,奇异点的数量会增加某个值,该值等于原图中非四边形面的数量
之后重复上述操作,可以证明奇异点的数量不会改变
下面考虑权重问题,直接给出公式
Mesh Simplification
Suppose we simplify a mesh using edge collapsing
相当于将一条边的两个端点变为一个点
为了计算变换的误差,需要引入二次误差度量
也就是该点与原相邻面距离的平方和
对于曲面中的每一条边,都可以计算出对应二次误差度量的最小值
以从小到大的顺序进行 edge collapsing 即可
注意每进行一步 edge collapsing,可能需要重新计算某些边对应二次误差度量的最小值
本质上是贪心算法,不保证能找到最优解,不过实际中效果还不错
Shadow Mapping
光线追踪的导论
之前提过,光栅化的着色不考虑 shadow
现在考虑点光源产生的硬阴影
关键思想是 the points NOT in shadow must be seen both by the light and by the camera
所以着色要分为两轮
- 从点光源的方向看场景,进行深度测试,得到 Shadow Mapping
- 从相机的方向看场景,对于每一个点,反向应用 Shadow Mapping 得到点光源的方向的深度值,并进行比较
可见的情形
不可见的情形
这种方法得到的 shadow 可能会出现走样,取决于 Shadow Mapping 的采样率
下面区分一下硬阴影和软阴影
软阴影中阴影的渐变效果,本质上是因为光源有一定的大小,而非点光源
光线追踪(基本原理)
Basic Ray-Tracing Algorithm
对光线的假设
- 光线沿直线传播
- 光线之间没有干扰
- 光路可逆
只要考虑到光线的反射和折射,并使用 Blinn-Phong reflectance model 叠加着色,就是最基本的光线追踪算法
Recursive (Whitted-Style) Ray Tracing
下面考虑一些技术细节
Ray-Surface Intersection
需要求出光线和几何表面的交点
首先需要为光线建模,实际上就是一条射线
Implicit Surfaces
以 Algebraic Surfaces 为例
只需要联立下面两式
r(t)=o+tdf(p)=0
求出正实数解 t 即可
Explicit Surfaces
以 Polygon Mesh 为例
首先考虑一个简单的算法,对光线和几何表面的每个三角形求交点
光线和三角形求交,实际上可以分解为
- 光线和平面求交
- 交点是否在三角形内(外积)
所以只要考虑光线和平面求交,使用点法式表示平面方程
联立即可解出 t
一个优化是直接考虑光线和三角形求交
即 Möller Trumbore Algorithm
使用重心坐标表示交点 P^
向量的维度为 3
有三个未知量 t,b1,b2
也就是一个线性方程组
Accelerating Ray-Surface Intersection
当场景中三角形数量较多时,或者像素点数量较多时
对光线和几何表面的每个三角形求交点,实际上至少需要计算这么多次
我们使用 Bounding Volumes 加速
Bounding Volumes
当光线和 Bounding Volumes 都不相交时,肯定不会与 Surface 相交
为了计算方便,我们使用 Axis-Aligned Bounding Box (AABB)
核心算法如下,我们视 Bounding Volumes 为三对平面相交的部分
求出光线进入三对平面的 max 和光线离开三对平面的 min
即可得出光线在 Bounding Volumes 内部的时间段
满足如下两个条件
tenter<texittexit≥0
即可判定光线和 Bounding Volumes 相交
tenter<0 也无所谓,代表光线源点在 Bounding Volumes 内部
而关于计算 t,由于平面和坐标轴对齐,所以只考虑一个分量即可
PA5
竟然没有使用任何第三方库……
Render
原理如下
https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-generating-camera-rays/generating-camera-rays
可能是因为相机朝向 -z 轴,所以计算 y 轴分量时需要取负
另外别忘了加上 0.5
本质上是映射
[0,width]×[0,height]→[−1,1]2
rayTriangleIntersect
v0,v1,v2 是三角形的三个顶点
orig 是光线的起点
dir 是光线单位化的方向向量
tnear,u,v 是需要使用 Möller Trumbore Algorithm 来更新的参数
直接抄公式即可
注意限制条件
pic
使用 imagemagick 转换 ppm 为 png
光线追踪(加速结构)
上节课讲了如何计算光线和 Bounding Volumes 求交
这节课介绍如何利用 Bounding Volumes 加速光线和几何表面求交
以二维为例
首先需要将场景进行划分,进行预处理
考虑均匀划分
然后让光线与 grid 求交,若相交,且内部有物体,再去和物体求交
光线进行 grid traversal 时,不必考虑所有的 grids
以图中的方向为例,只需要考虑右面和上面的 grid 即可
类似画一条直线,参见 PA1 rasterizer.cpp
中的 draw_line
函数
Bresenham’s line drawing algorithm
对于 grid 的分辨率,若太大,起不到加速效果,若太小,光线与 grid 求交的开销太大
有经验规则
然而,均匀划分对于物体不均匀分布的场景而言效果并不好
Spatial Partitions
考虑场景的不均匀划分
以 KD-Tree 为例,region 构成一个二叉树
需要注意,叶子节点需要根据 region 计算哪些物体在其内部,这并不容易
并且物体可能同时在不同的 region 中
Object Partitions & Bounding Volume Hierarchy (BVH)
考虑物体的不均匀划分
注意 Bounding Volumes 之间可能有重叠
每一次划分后,都需要重新计算子结构的 Bounding Volumes
而对于物体的划分,可以根据某个方向取中位数
总结一下求交的算法
综合了
- 光线和 Surface 求交
- 光线和 Bounding Volume 求交
- 加速结构
区分一下上述两种不均匀划分
至此,Recursive (Whitted-Style) Ray Tracing 基本上就介绍完了
Basic Radiometry
进入辐射度量学这个新的话题
我们需要对光的空间特性进行更精确的度量
首先引入几个概念
Radiant Energy
能量 - 焦耳
Q[J=Joule]
Radiant Flux (Power)
功率/辐射通量 - 瓦特/流明
Φ≡dtdQ[W=Watt][lm=lumen]
Radiant Intensity
辐射强度 - 坎德拉
I(ω)≡dωdΦ(ω)[srW][srlm=cd=candela]
其中 ω 为立体角
加上微分符号就是微分立体角,即球面空间中一个方向上的单位立体角
使用球坐标系易知
dω=r2dA=sinθdθdϕ
于是 Radiant Flux 和 Radiant Intensity 有关系
Φ=∫S2Idω=4πI
PA6
使用 Bounding Volumes 加速光线追踪
migration
首先需要将 PA5 的代码迁移过来
PA6 抽象出了 Scene 类和 Ray 类
castRay
的调用方式有所改变
然后需要修改 Triangle.hpp
中的 getIntersection
Triangle 类和 MeshTriangle 类 override 掉 getIntersection
方法
但是 MeshTriangle 类的实现和 Triangle 类的实现并不一致
MeshTriangle 类需要调用 BVHAccel::Intersect
可能是因为 Triangle 类代表了 Implicit Surfaces,而 MeshTriangle 类代表了 Explicit Surfaces
还有一些奇怪的 intersect 方法,似乎并没有被使用
Bounds3::IntersectP
然后实现光线和 Bounding Volumes 求交
由于平面是和坐标轴是平行的,所以 Bounds3 类中存储了三对平面的某个轴的坐标
例如平行于 yz 平面的平面只需要存储 x 坐标
另外需要保证 tmin≤tmax
并没有使用 dirIsNeg
参数
BVHAccel::getIntersection
最后实现利用加速结构的求交的算法
注意到每个叶子节点,也就是每个 Bounding Volume 中只有一个物体
可以通过 BVHAccel::recursiveBuild
观察建立 BVH 的细节
总结一下调用轨迹
Scene::castRay
Scene::intersect
BVHAccel::Intersect
BVHAccel::getIntersection
Bounds3::IntersectP
pic
todo
光线追踪(辐射度量学、渲染方程与全局光照)
Basic Radiometry
接着上节课的内容
Irradiance
辐射照度 - 勒克斯
E(p)≡dAdΦ(p)[m2W][m2lm=lux]
注意这里的面积是投影面积
Radiance
辐射亮度
比 Irradiance 相比多了一个方向
比 Intensity 相比多了一个面积
power per unit solid angle per projected unit area
L(p,ω)≡dωdAcosθd2Φ(p,ω)
请关注如下的关系
- Irradiance: power per projected unit area
- Intensity: power per solid angle
- Radiance: Irradiance per solid angle
- Radiance: Intensity per projected unit area
于是有
L(p,ω)=dωcosθdE(p)=dAcosθdI(ω)
Bidirectional Reflectance Distribution Function (BRDF)
本质上是一个比例
IrradianceRadiance
The Reflection Equation
对半球面所有可能的方向积分即可
The Rendering Equation
加上着色点本身的辐射,即 Emission 项
Lo(p,ωo)=Le(p,ωo)+∫Ω+Li(p,ωi)fr(p,ωi,ωo)(n⋅ωi)dωi
Ω+ 代表半球面
we assume that all directions are pointing outwards
需要理解 Reflection 项积分符号的含义
当只有一个点光源时
Li(p,ωi)fr(p,ωi,ωo)(n⋅ωi)
当有多个点光源时,求和即可
∑Li(p,ωi)fr(p,ωi,ωo)(n⋅ωi)
当光源有一定的大小,即非点光源时,积分即可
∫ΩLi(p,ωi)fr(p,ωi,ωo)(n⋅ωi)ωi
为了处理多次反射的情形,我们视反射光也为光源,将等式简化为
I(u)=e(u)+I(v)∫K(u,v)dv
考虑全局性,继续简化为
L,E are vectors, K is the light transport matrix
可以解得
注意之前所学的光栅化着色的 Shadow Mapping 只能计算前两项
除了第一项,其余部分即为 global illumination 全局光照
光线追踪(蒙特卡洛积分与路径追踪)
Monte Carlo Integration
数值方法求近似积分
∫abf(x)dx≈N1i=1∑Np(Xi)f(Xi)Xi∼p(x)
X 是连续型随机变量
以均匀分布为例,p(x)=b−a1,则
∫abf(x)dx≈Nb−ai=1∑Nf(Xi)
Path Tracing
Whitted-Style Ray Tracing 有如下假设
- Always perform specular reflections / refractions
- Stop bouncing at diffuse surfaces
然而这些假设并不合理
为此我们需要借助上节课的 The Rendering Equation 进行光线追踪
考虑计算全局光照,对于给定的着色点和观察方向
利用蒙特卡洛积分,有如下的算法
注意这里的 wo,wi
第一个分支计算直接光照,即第二项
第二个分支使用递归计算所有的间接光照,即第二项之后的项
注意对 wi 取反方向
但需要注意这里递归会爆炸,有关系 rays = N ^ bounces
所以我们取 N = 1
但随机性引入了噪声,所以我们需要在 ray_generation
时取 N 个方向
SPP (samples per pixel)
然而上述着色算法仍存在问题,即递归可能不会终止
为此引入 Russian Roulette,即俄罗斯轮盘赌
- 设定某个概率 P
- P 概率下,计算着色并将最终结果除以 P
- 1−P 概率下,直接返回结果 0
这可以保证最终结果的期望相同
shade
算法如下
另外注意到,当光源较小时,shade
追踪的光线可能很难 hit 光源
为此考虑在光源平面,而非在着色点平面,进行采样
需要对 f 进行变换
sample on x, integrate on x
于是将着色分为两个部分
- light source (direct, no need to have RR)
- other reflectors (indirect, RR)
shade
算法最终如下
需要考虑到光源和着色点之间有 block 的情形
Further…
- Uniformly sampling the hemisphere
How? And in general, how to sample any function? (sampling)
- Monte Carlo integration allows arbitrary pdfs
What’s the best choice? (importance sampling)
- Do random numbers matter?
Yes! (low discrepancy sequences)
- I can sample the hemisphere and the light
Can I combine them? Yes! (multiple imp. sampling)
- The radiance of a pixel is the average of radiance on all paths passing through it
Why? (pixel reconstruction filter)
- Is the radiance of a pixel the color of a pixel?
No. (gamma correction, curves, color space)
PA7
algorithm
伪代码如下
实现如下
note
修改 Bounds3::IntersectP
中的相交条件
多一个等号
示意图如下
注意光线的方向,对 p 点而言,始终是 outwards
区分 eval
和 pdf
中的参数 wi
和 wo
关于检测光源和着色点之间是否有 block,需要射出额外的光线 ray_p_to_x,并判断交点和 x 点是否一致
关于间接光照,似乎并不需要分支判断
参数 depth 代表递归的次数,不过没有用到
thread
参考 https://github.com/Quanwei1992/GAMES101/blob/master/07/Renderer.cpp
使用多线程加速渲染,按 height 将屏幕划分给多个 worker
并且修改 CMakeLists.txt
速度提升了约 2~3 倍
pic
128 spp
渲染了一个小时
todo
TODO
图形学对其他学科的要求挺高的……
而且调试完全没有头绪……
于是就这样吧……