目录
往期作业汇总帖
https://games-cn.org/forums/topic/allhw/
Lecture 10: Geometry 1 (Introduction)
纹理的作用
将数据导入到 Fragment Shader 中的通用方法
(1)存储环境光照
(2)存储微表面几何信息
(3)预处理的纹理
(4)3D体积渲染
(5)…
环境贴图
现在假设我向着一个方向看,会看到环境的某个部分
当我移动我的位置之后,即使我眼睛看的方向不变,我也应该会看到环境的不同部分
现在认为环境距离我无限远,所以我不管怎么移动我的位置,只要我看的方向不变,看到的环境内容就不变,这个与位置无关只与方向有关的内容就被存为环境纹理
也就是环境纹理上面记录了 <方向,颜色> 的信息
因此环境纹理可以存储在一个球面上
球面环境贴图:南北极畸变比较严重
立方体环境贴图:在OpenGL中有对应的接口,本质上是6个2D纹理的拼接。
凹凸贴图
纹理除了描述颜色,也可以描述任意位置上的任意属性
凹凸(Bump)贴图:定义了片元着色器中着色点沿着着色点的旧的法线移动的相对高度,并且要求计算着色点移动之后,该着色点的新的法线方向。将着色点的新位置和新法线用于片元着色器
怎么计算凹凸贴图的新法线
假设任何一个点,他原本是一个平面
那么平面的法向是 ( 0 , 1 ) (0,1) (0,1)
加上凹凸贴图之后,那么得到的表面的高度就完全是由凹凸贴图决定
在凹凸贴图所定义的表面上,通过求这个表面的切线得到该处的法向
例如在一维情况下,横坐标是 u u u,曲线是 f ( u ) f(u) f(u),那么 d p = f ( u + d u ) − f ( u ) \mathrm dp = f(u + \mathrm du) - f(u) dp=f(u+du)−f(u),设 d u = 1 \mathrm du = 1 du=1,此时的切线方向为 ( 1 , d p ) = ( 1 , f ( u + 1 ) − f ( u ) ) (1, \mathrm dp) = (1, f(u+1)-f(u)) (1,dp)=(1,f(u+1)−f(u))
假设原本是一个平面的好处就是,这个 f ( u ) f(u) f(u) 目前完全是凹凸贴图的高度,这样方便计算
那么法线方向就是 ( − d p , 1 ) (-\mathrm dp, 1) (−dp,1)
之后再归一化
同理,对于二维空间,假设原来是一个平面
那么平面的法向是 ( 0 , 0 , 1 ) (0,0,1) (0,0,1)
嗯……但是也不能这么说吧
感觉这里我没理解到位
然后就是,对于某点,假设 d u = 1 \mathrm du = 1 du=1,那么在 u u u 方向上的切线方向是 ( 1 , 0 , d p / d u ) (1, 0, \mathrm dp/\mathrm du) (1,0,dp/du);假设 d v = 1 \mathrm dv = 1 dv=1,那么在 v v v 方向上的切线方向是 ( 0 , 1 , d p / d v ) (0, 1, \mathrm dp/\mathrm dv) (0,1,dp/dv)
那么已知某一点在两个基向量方向上的切线,这两个切线所构成的平面的法向量就是这一点在曲面上的法向量
n = ( 1 , 0 , d p / d u ) × ( 0 , 1 , d p / d v ) = ∣ i j k 1 0 d p / d u 0 1 d p / d v ∣ = ( − d p / d u , − d p / d v , 1 ) n = (1, 0, \mathrm dp/\mathrm du) \times (0, 1, \mathrm dp/\mathrm dv) \\ = \left |\begin{array}{cccc} i & j & k \\ 1 & 0 & \mathrm dp/\mathrm du \\ 0 & 1 & \mathrm dp/\mathrm dv \end{array}\right| \\ = (-\mathrm dp/\mathrm du, -\mathrm dp/\mathrm dv, 1) n=(1,0,dp/du)×(0,1,dp/dv)= i10j01kdp/dudp/dv =(−dp/du,−dp/dv,1)
之后再归一化
位移贴图
位移(displacement)贴图
位移贴图改变了三角形顶点的位置
这样在物体边缘可以看到顶点位置变化,也可以在自身投影出位移后的顶点的阴影
这里也会有采样的问题,也就是模型需要足够细致,才能让三角形顶点对位移贴图的采样率大于位移贴图的颜色的变化率
但是如果位移贴图很细致的话,就要求原模型也很细致了
但是我们不希望模型可以无限细致,这样就对我的生产有负担
我就希望在使用位移贴图的时候,有一个程序来判断我是否需要让模型上的三角形更小,来适应这个位移贴图
如果能检测到需要,我就可以把一个三角形拆成若干个小三角形,然后使用拆出来的小三角形的顶点对位移贴图采样
DirectX Dynamic Tessellation
搜了一下,用处还很大……?
那为什么不直接在原模型中增加模型面数呢?
原因有如下三点:
- 动态LOD(levels of detail,细节层次),我们可以基于摄像机的远近和其它因素决定要产生的细节丰富度。如果我们离物体比较远,就没有必要产生那么多模型面数。
- 使物理和动画模拟更加高效。低面数模型来计算物理和动画,可以使得计算效率更加高,接着再曲面细分,达到更逼真的显示效果。
- 节约内存。我们可以节约硬盘容量、CPU内存、GPU显存,在计算的时候再凭空新增细节顶点。
————————————————
版权声明:本文为CSDN博主「梦幻DUO」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_24229853/article/details/77162389
三维纹理
定义了空间中任意一个点的值
噪声纹理
定义噪声函数,例如柏林噪声
AO 贴图
想要实时地算一些细节的阴影,也可以用动态的环境光遮蔽 Ambient Occlusion
但是如果只考虑单个物体的内部的细节之间的遮挡关系,就可以预先算好,然后生成一张贴图
AO 贴图描述了某一个物体的表面上的任何一点所接受到的环境光被该物体本身的周围几何体所遮蔽的百分比, 使得环境光渲染的结果变得正确
https://zhuanlan.zhihu.com/p/400633131
几何表示方法
隐式表达
f(x, y, z) = 0
-
代数表面:由公式求解而来
-
构造表面:CSG ConstructIversonSolid Geometry
由基本图元 交并差 求解而来
-
SDF(Signed Distance Field):距离场定义的几何边界
描述点到表面的最近距离
空间中任何一个点到你想要表述的这个几何形体上面的任意一个点之间的最小距离
这个最小距离可以是正的也可以是负的
在物体内部就是负的,在物体外部就是正的
函数等于 0 的时候表示物体的表面
如果某个物体的距离函数难以表示成解析式,那么也可以用纹理的方法来定义 SDF 的值,这个纹理就叫做水平集 Level Set Methods
感觉也可以跟势场比较
隐式的优点:
-
方便使用函数描述
-
方便判断点在物体内部还是外部
-
方便判断点与物体表面是否相交
-
方便严格定义
-
方便表述拓扑
-
难以描述实际模型
显式表达
z = f(x, y) 或者是通过参数映射的方式给出 (u,v) -> (x,y,z)
对于显示表达,判断一个点在图形内部还是外部变难了
分型
分型会引起强烈的走样
因为分型的变化频率会趋向很高
Lecture 11: Geometry 2 (Curves and Surfaces)
计算机图形的显式表示
三角形网格
-
方便遍历
-
结构复杂 存储了顶点和面
Bezier曲面
细分曲面
NURBS曲面
点云
-
结构简单,就是一堆点
-
方便描述各种几何形体
-
只有在数据集很大的时候才有效
-
经常转换成三角形网络
-
很难处理那些采样不足的区域
贝塞尔曲线
二次贝塞尔曲线(quadratic)
给定 P0 P1 P2
引入一个参数 t
这个参数 t 就是一个显式的表示
找 P0P1 上将该线段划分为 t 和 1-t 比例的点 P01
找 P1P2 上将该线段划分为 t 和 1-t 比例的点 P11
找 P01P11 上将该线段划分为 t 和 1-t 比例的点 P(t) 这个 P(t) 就是二次贝塞尔曲线上的点
对于 P0 P1 P2 枚举所有可能的 t 得到的所有的 P(t)
P(t) 点构成的曲线就叫二次贝塞尔曲线
由此可见,为什么说这是显式表示……因为是有一个参数 t,t 从 0 到 1 变化,绘制出了曲线
有的时候有人说这是递归过程
但是可能没有详细说到底是哪里递归了
实际上递归说的是,从 n 阶贝塞尔曲线的计算一直降,降到 1 阶贝塞尔曲线的计算,这个阶数的不断下降就是递归
比如这里,原本是三个控制点两条线段,2 阶贝塞尔曲线,取比例点连线之后得到两个控制点一条线段,1 阶贝塞尔曲线
三次贝塞尔曲线(Cubic)
与二次贝塞尔曲线相比,多了一个控制点
也就是从 3 阶递归到 2 阶,1 阶
贝塞尔曲线的解析式
伯恩斯坦多项式
描述一个二项分布
其实就是 曲线上的点的坐标(t) = sum{控制点的坐标 * 二项分布系数(t)}
那么其实这个坐标是不限制维度的,那么其实我们就可以拓展到三维
贝塞尔曲线的性质
-
插值点的函数 b ( t ) b(t) b(t) 的区间端点是第一个控制点 b 0 b_0 b0 和最后一个控制点 b 3 b_3 b3
-
插值点的函数 b ( t ) b(t) b(t) 的区间端点的导数
这个自己写一下 b ( t ) b(t) b(t) 导数式就知道了
-
仿射不变性
对每个控制点做仿射变换,得到的新的贝塞尔曲线与原来的贝塞尔曲线是一样的
那么就只需要记录控制点就好了
只对仿射变换有这个性质,但是对其他的就没有,例如投影变换
-
凸包性质
贝塞尔曲线在控制点构成的凸包之内
分段 Piecewise 贝塞尔曲线
对一个复杂的曲线,如果用很多的控制点来描述的话,当我动了一个其中一个控制点,会对整条曲线都产生影响
为了控制点对曲线的影响具有局部性,将贝塞尔曲线分段,要求分段的贝塞尔函数在连接处连续
0 1 2 3 四个控制点视为两个控制杆
两个切线的方向共线,大小相等,才认为曲线连续
这里要求导数连续,也就是 C 1 C^1 C1 连续
其他
多个低阶的贝塞尔曲线怎么合并成一个高阶的贝塞尔曲线
一个高阶的贝塞尔曲线怎么拆成多个低阶的贝塞尔曲线
一个具有很多控制点的贝塞尔曲线怎么简化控制点而保持曲线与原曲线近似
样条
B 样条
基函数样条
更复杂的叫 NURBS 非均匀有理 B 样条
贝塞尔曲面
Y 方向上排列多条 XZ 面上的贝塞尔曲线,然后在某一个 X 值上,取到各个贝塞尔曲线上的点,作为这个 YZ 面上的贝塞尔曲线的控制点
移动 X 值,得到的不同 YZ 面上的贝塞尔曲线合起来就是一个贝塞尔曲面
其他
多个贝塞尔曲面怎么拼接
网格
网格细分
网格简化
网格正规化 regularzation
也就是不出现尖而长的三角形
Lecture 12: Geometry 3
三角网格操作
网格细化:让网格变多,增加细节,效率降低,用于模型离相机较近的场景
网格简化:让网格变少,形状更粗糙,提高绘制效率,用于模型离相机较远的场景。
网格规则化:让网格大小差不多
Loop Subdivision
发明者的姓是 Loop 而不是循环的意思
已知细分就是在边上加入一个新的顶点,然后调整这个新旧顶点的位置和连接方式
对于新顶点的处理
对于一般情况,新的顶点所在的边被两个三角面共享
认为新顶点的位置是这个顶点周围的旧顶点的加权平均
新顶点的位置 = 3/8*(A+B)+1/8*(C+D)
对于旧顶点的处理
对于一般情况,旧的顶点被可以被六个三角面共享
小于六个获得大于六个也是可以的……但是他就是拿这个作为例子
旧顶点的位置 = (1 - n * u) * original_position + u * neighbor_position_sum
u 是一个分数,表示权重
如果连了很多三角形,这个旧顶点本身的位置就不太重要,如果连的三角形少,这个旧顶点本身的位置就重要
各个旧顶点的位置更新是并行计算的,与更新先后顺序无关
这里还没有将新顶点之间的连接
Catmull-Clark 表面细分
在四边形网格中,如果出现了三角形,那么一般情况如下
为了在表面细分的时候得到四边形面,取这个三角面的中心,与三条边的中心相连
相连之后得到的就是四边面了
奇异点指的是度不为 4 的点
可见,连线之前,三角形的重合的那两个点,以及连线之后,三角形的中心,都是奇异点
现在是两个度为 5 的奇异点,两个度为 3 的奇异点
这是因为我在一个非四边形面里面去一个中点与各边中点相连,所以必定是奇异点
也就是说,新的奇异点的数量与原来的非四边形面的数量有关系
原来的每一个非四边形面引入了一个奇异点之后,这个非四边形面就会消失
那也就说明,以后的细分中,奇异点的数量就不会增加了
也就是说,只在第一次细分的时候才增加奇异点的数量
更新方法
面中的新顶点 f = v 1 + v 2 + v 3 + v 4 4 f = \dfrac{v_1 + v_2 + v_3 + v_4}{4} f=4v1+v2+v3+v4
边上的新顶点 e = v 1 + v 2 + f 1 + f 2 4 e = \dfrac{v_1 + v_2 + f_1 + f_2}{4} e=4v1+v2+f1+f2
旧顶点的新位置
v
=
f
1
+
f
2
+
f
3
+
f
4
+
2
(
m
1
+
m
2
+
m
3
+
m
4
)
+
4
p
16
v = \dfrac{f_1 + f_2 + f_3 + f_4 + 2(m_1 + m_2 + m_3 + m_4) + 4p}{16}
v=16f1+f2+f3+f4+2(m1+m2+m3+m4)+4p
其中 v 是旧顶点的新位置,p 是旧顶点的旧位置
Loop 细分只能用于三角形面
Catmull-Clark 细分可以用于四边形和三角形面
边坍缩
哪些边可以坍缩哪些边不行
使用二次误差度量 Quadric Error Metric
对于每一条边,都计算坍缩了这条边之后的二次度量误差,然后选择误差最小的那个,然后再选择第二小的……
问题在于,坍缩了一条边之后,与这些边相连的这些边也会受到影响,所以这些受到影响的边的二次度量误差也会变
-
对所有的边计算坍缩之后的二次度量误差
-
取所有的边之中,二次度量误差最小的那条边,移出
-
更新这个移出的边相关联的边的二次度量误差
-
回到 2
这里因为希望在 o(1) 取到最小值,所以使用优先队列
但是又希望可以更新堆顶相关联的元素,所以使用删除 x 插入 x’ 来完成对 x 的更新,所以要使用带删除的优先队列
这里每一次进行 2,是在每一步下的局部最小值
那么其实这个算法是贪心算法
Shadow Mapping 阴影
传统的一个物体一个应用于自身的纹理,会有一个问题是,他与场景中的其他物体无关,而且可能走样
阴影的实现思路是,不在阴影中的物体必须同时能被光源和相机看到
硬阴影:非 0 即 1
软阴影
Pass 1: Render from Light
从光源绘制,得到记录了从光源看场景中每个点的深度图Picture1。
在OpenGL中需要用到帧缓冲对象FBO,将结果绘制到一个纹理中。
Pass 2: Render from Eye
从相机绘制,
(1)计算每个像素点到光源的距离d2,
(2)如果d2大于深度图Picture1记录的对应位置的深度d1,则说明该像素点处于阴影中,不会接收到直接光照,
(3)如果d2不大于深度图Picture1记录的对应位置的深度d1,则说明该像素点能接收到直接光照,不在阴影中。
变回去:只需要将顶点左乘光源的mvp矩阵。就可以获得在光源坐标系下的坐标,这时候把z和shadowMap中的深度进行对比即可
这是从相机看到的场景中的,对比过深度之后的结果
这里看出对比深度优点“脏”
一方面是因为浮点数比较比较困难
另一方面是 shadowMap 的分辨率。如果 shadowMap 的分辨率低,场景的分辨率高,就会产生走样
shadowMap 只能产生硬阴影,产生不了软阴影
软阴影的出现是因为阴影中的某一点,可以看到光源的一部分,看不到光源的另外一个部分
这就要求光源一定是要具有体积的
作业 4
naive_bezier
是用来作为 opencv 参考的
有了参考就很简单了
我还写了一个原地的算法hhh
总之感觉这可能是最简单的作业了
#include <chrono>
#include <iostream>
#include <opencv2/opencv.hpp>
std::vector<cv::Point2f> control_points;
void mouse_handler(int event, int x, int y, int flags, void *userdata)
{
/*if (event == cv::EVENT_LBUTTONDOWN && control_points.size() < 4)
{
std::cout << "Left button of the mouse is clicked - position (" << x << ", "
<< y << ")" << '\n';
control_points.emplace_back(x, y);
}*/
if (event == cv::EVENT_LBUTTONDOWN)
{
std::cout << "Left button of the mouse is clicked - position (" << x << ", "
<< y << ")" << '\n';
control_points.emplace_back(x, y);
}
}
void naive_bezier(const std::vector<cv::Point2f> &points, cv::Mat &window)
{
auto &p_0 = points[0];
auto &p_1 = points[1];
auto &p_2 = points[2];
auto &p_3 = points[3];
for (double t = 0.0; t <= 1.0; t += 0.001)
{
auto point = std::pow(1 - t, 3) * p_0 + 3 * t * std::pow(1 - t, 2) * p_1 +
3 * std::pow(t, 2) * (1 - t) * p_2 + std::pow(t, 3) * p_3;
window.at<cv::Vec3b>(point.y, point.x)[2] = 255;
}
}
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{
// TODO: Implement de Casteljau's algorithm
// copy
std::vector<cv::Point2f> tmp_points(control_points);
// in-place algorithm
for (int iter_size = tmp_points.size() - 1; iter_size > 0; --iter_size) {
for (int i = 0; i < iter_size; ++i) {
tmp_points[i] = t * tmp_points[i] + (1 - t) * tmp_points[i + 1];
}
}
return tmp_points[0];
}
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{
// TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's
// recursive Bezier algorithm.
if (control_points.size() == 0) return;
for (double t = 0.0; t <= 1.0; t += 0.001)
{
auto point = recursive_bezier(control_points, t);
window.at<cv::Vec3b>(point.y, point.x)[2] = 255;
}
}
int main()
{
cv::Mat window = cv::Mat(700, 700, CV_8UC3, cv::Scalar(0));
cv::cvtColor(window, window, cv::COLOR_BGR2RGB);
cv::namedWindow("Bezier Curve", cv::WINDOW_AUTOSIZE);
cv::setMouseCallback("Bezier Curve", mouse_handler, nullptr);
int key = -1;
while (key != 27)
{
// clear window
window.setTo(cv::Scalar(0, 0, 0));
for (auto &point : control_points)
{
cv::circle(window, point, 3, {255, 255, 255}, 3);
}
//if (control_points.size() == 4)
//{
// //naive_bezier(control_points, window);
// bezier(control_points, window);
// cv::imshow("Bezier Curve", window);
// cv::imwrite("my_bezier_curve.png", window);
// key = cv::waitKey(0);
// return 0;
//}
bezier(control_points, window);
cv::imshow("Bezier Curve", window);
key = cv::waitKey(20);
}
return 0;
}
这里我将功能改为了点击一次就是创造一个构造点,最终可以用多个构造点画贝塞尔曲线