计算机图形学
课程作业2:网格编辑
一、概述
简要介绍作业实现的主要内容。
本次计算机图形学作业包括两个模块,分别是贝塞尔曲线与曲面实现和基于半边数据结构的三角网格。
模块一:贝塞尔曲线与曲面
- 算法1:使用1D de Casteljau实现贝塞尔曲线
- 通过实现递归算法对给定的控制点进行线性插值,生成贝塞尔曲线上的中间点和最终点。
- de Casteljau算法是一种迭代线性插值方法,通过不断对控制点进行插值,逐步收敛得到曲线上的任意一点。
- 算法2:基于1D de Casteljau实现贝塞尔曲面
- 将贝塞尔曲线的算法扩展至二维,生成贝塞尔曲面。
- 通过逐行对控制点插值,再对每行的结果进行插值,最终生成曲面上对应的点。
模块二:基于半边数据结构的三角网格上采样
- 算法3:加权面积法求顶点法线
- 通过对顶点相邻三角形的法向量进行加权平均来计算顶点的法向量,其中权重由三角形面积决定。
- 这种方法常用于实现Phong着色,使得曲面更光滑、明暗过渡更自然(计算机图形学作业2)。
- 算法4:边翻转(Edge Flip)
- 实现局部网格重构操作,翻转相邻两个三角形的共享边以改变网格拓扑结构。
- 边翻转操作的实现需要确保所有半边、顶点、边和面在翻转后指向正确的网格元素。
- 算法5:边分割(Edge Split)
- 对网格中的边进行分割,在边的中点插入一个新的顶点,并对原始的三角形进行分割,形成新的三角形。
- 这种操作会引入新的顶点、边和面,增加网格的复杂度(计算机图形学作业2)。
- 算法6:基于Loop Subdivision的网格上采样
- 通过Loop细分算法将每个三角形细分为更小的三角形,以提高网格的分辨率。
- 包括两个步骤:(1)细分每个三角形为四个较小的三角形;(2)根据加权平均来更新顶点的位置,使网格更平滑。
- Loop细分过程中还需执行边的翻转和顶点位置的更新,确保得到的细分网格在几何上是对称和合理的。
开发与验证
- 使用
meshedit
程序载入不同类型的文件(如贝塞尔曲线.bzc
文件、贝塞尔曲面.bez
文件、三角网格.dae
文件)以验证每个算法的正确性。 - 通过键盘交互(如按键“F”来翻转边、“S”来分割边)来观察网格在执行操作后的变化,确保实现的每个函数工作正常。
二、贝塞尔曲线和曲面
1. 基于 1D de Casteljau 绘制贝塞尔曲线
简要解释 de Casteljau 算法以及您如何实现它以绘制贝塞尔曲线。
de Casteljau 算法是一种用于评估贝塞尔曲线的递归几何算法。它通过对控制点之间逐步进行线性插值来计算曲线上的点。该算法由法国工程师 Paul de Casteljau 在 1959 年提出,常用于计算任意阶的贝塞尔曲线,因为它比直接使用贝塞尔公式数值稳定。
算法步骤:
-
输入:给定一组控制点和参数
t
(t
通常在[0, 1]
之间)。 -
线性插值:对于每一对相邻控制点,按照插值公式进行计算:
这将生成一组新的点,称为中间点。
-
递归:对新生成的中间点集重复步骤 2,直到只剩下一个点为止。
-
输出:当只剩下一个点时,这个点就是曲线在参数
t
下的位置。
优点:
- 几何性:de Casteljau 算法完全基于几何构造,而不涉及复杂的多项式运算,数值稳定性高。
- 任意阶数:可以处理任意阶数的贝塞尔曲线,而不仅仅是二次或三次。
通过递归地缩减点集,de Casteljau 算法可以高效地计算曲线上任意参数值 t
对应的点。
实现代码:
std::vector<Vector2D> BezierCurve::evaluateStep(std::vector<Vector2D> const &points)
{
// TODO Part 1.
int numpoints = points.size();
std::vector<Vector2D>LerpPoints;
if (numpoints == 1)return points;//只有一个控制点
for (int i = 0; i < numpoints-1; i++) {
LerpPoints.push_back((1.0 - t) * points[i] + t * points[i + 1]);
}
return LerpPoints;
}
效果
2. 基于 1D de Casteljau 绘制贝塞尔曲面
简要说明 de Casteljau 算法如何扩展到贝塞尔曲面,以及如何实现它以绘制贝塞尔曲面。
在贝塞尔曲面(Bezier Patch)的评估中,选择先在u
方向上插值,然后在v
方向上插值,源于曲面本质上是二维的结构,这需要在两个方向上进行插值。具体来说,贝塞尔曲面是由二维网格状排列的控制点构成的。为了得到贝塞尔曲面上某个点的最终位置,我们需要分别在两个方向(u
和v
)对控制点进行插值计算。
原因和步骤分析:
-
贝塞尔曲面定义:
贝塞尔曲面是贝塞尔曲线的二维扩展,它由一组二维控制点网格构成。网格中的每行和每列可以视为一条贝塞尔曲线。u
方向表示在控制点网格中的行方向(横向)。v
方向表示在控制点网格中的列方向(纵向)。
-
先在
u
方向插值:
对于贝塞尔曲面上某个点,首先,我们需要固定v
,对网格中的每一行控制点进行贝塞尔插值,得到一组中间的插值点。这相当于对控制点网格中的每一行执行一次一维贝塞尔曲线插值。插值参数u
决定了如何在每一行的控制点之间插值。- 这一过程生成了一组沿
v
方向排列的点(每行一个点)。
- 这一过程生成了一组沿
-
再在
v
方向插值:
在u
方向完成插值后,我们会得到一组新的点。这些点代表着v
方向上的点集。然后,在这些中间点上使用贝塞尔插值参数v
进行第二次插值,得到最终的贝塞尔曲面上对应点的坐标。
为什么这样做?
-
简化计算过程:这种做法将一个二维插值问题分解成两个一维插值问题,简化了复杂度。每个方向的插值操作都相对简单且独立,适合使用递归算法(如de Casteljau算法)进行处理。
-
几何解释:贝塞尔曲面可以看作是沿两个方向的贝塞尔曲线的“混合”。先在
u
方向进行插值,相当于在横向上找到曲面中的一系列点;然后在这些点之间再进行v
方向的插值,得出最终结果。
这个过程将复杂的二维曲面评估问题分解为两个较为简单的贝塞尔曲线评估问题,符合递归的思路,使得算法在处理高维问题时更加简洁和高效。
根据给定参数评估一维和二维的贝塞尔曲线。代码分为三部分:第一部分是一步线性插值,第二部分是一维贝塞尔曲线的完全评估,第三部分是二维贝塞尔曲面的评估。
1.BezierPatch::evaluateStep
std::vector<Vector3D> BezierPatch::evaluateStep(std::vector<Vector3D> const &points, double t) const
{
int numpoints = points.size();
std::vector<Vector3D> LerpPoints;//创建一个空的`Vector3D`点的向量,用于存储线性插值后的点。
if (numpoints == 1) return points;//如果输入的点只有一个,直接返回(不需要插值)
for (int i = 0; i < numpoints - 1; i++) {
LerpPoints.push_back((1.0 - t) * points[i] + t * points[i + 1]);
//对所有相邻点进行插值运算 `(1.0 - t) * points[i] + t * points[i + 1]`,其中`(1.0 - t)`和`t`是线性插值系数。
}
return LerpPoints;
}
作用:
这是一步线性插值函数(Lerp, Linear Interpolation),用于对一组控制点之间进行线性插值。对于给定的一组三维点 points
和一个插值参数 t
,它计算每对相邻点之间的插值,并返回插值后的点集。
2.BezierPatch::evaluate1D
Vector3D BezierPatch::evaluate1D(std::vector<Vector3D> const &points, double t) const
{
if (points.size() == 1)//如果只剩下一个点,说明递归已经到底,直接返回这个点作为最终插值结果。
{
return points[0];
}
return evaluate1D(evaluateStep(points, t), t);
//当前点集调用`evaluateStep`进行一步线性插值,然后递归调用`evaluate1D`,继续插值,直到只剩下一个点。
}
作用:
这个函数是完整的一维贝塞尔曲线评估函数,它使用de Casteljau算法递归地评估一组点在给定参数t
下的最终插值值。它依赖于evaluateStep
函数来逐步减少点的数量,直到只剩下一个点为止。
这个函数通过递归的方式不断缩减点集,最后得到一维贝塞尔曲线在t
位置的插值点。
3. BezierPatch::evaluate
Vector3D BezierPatch::evaluate(double u, double v) const
{
std::vector<Vector3D> uPoints;
int n = controlPoints.size();
for (int i = 0; i < n; i++) {
uPoints.push_back(evaluate1D(controlPoints[i], u));
//对每一行控制点,调用`evaluate1D`,用参数`u`对它们进行插值,结果存入`uPoints`
}
return evaluate1D(uPoints, v);//对`uPoints`进行一次一维贝塞尔插值,得到最终结果。
}
作用:
这个函数用于评估二维的贝塞尔曲面(Bezier Patch),即在两个参数u
和v
的控制下,对控制点网格进行插值,得到曲面上对应的三维点。这个函数的核心思想是先在u
方向上对每一行控制点进行插值,然后在v
方向上再对插值结果进行插值,从而得到最终的三维贝塞尔曲面上对应的点。
效果
三、三角网格和半边数据结构
3. 基于面积加权的顶点法线计算
简要说明您如何实现面积加权的顶点法线。
[显示 dae/teapot.dae(不是 .bez)的屏幕截图,比较有和没有顶点法线的茶壶着色。使用 Q 切换默认平面着色和 Phong 着色。]
将面积和面的法向量相乘,是为了计算顶点法向量时,较大面积的面对顶点法向量的贡献更大,反映出更准确的几何特征。
法向量的主要用途是光照计算和曲面平滑。对于光照模型(如 Phong 光照模型),法向量用于计算光线与表面的夹角,进而决定表面如何反射光线。如果法向量不准确,会导致光照效果不自然,表面看起来不平滑。
具体算法:
/* 遍历一个顶点的相邻三角形,计算每个三角形的法向量。
将这些法向量按照三角形的面积加权求和,得到一个加权平均的法向量。
对这个加权平均的法向量进行归一化,以确保它的长度为1,从而得到该顶点的近似单位法向量。*/
Vector3D weighted_normal(0, 0, 0); // 存储加权向量的累加结果
HalfedgeCIter h = halfedge(); // 获取当前顶点的出发半边
do {
if (!h->face()->isBoundary()) {
// 获得三角形面,v0 v1 v2 是相邻的三个顶点
Vector3D v0 = position;
Vector3D v1 = h->next()->vertex()->position;
Vector3D v2 = h->next()->next()->vertex()->position;
// 计算三角形面积
double face_area = (cross(v1 - v0, v2 - v0).norm()) / 2.0;//三角形的面积是两边向量叉积的一半
// 面积加权累加法向量
weighted_normal += face_area * h->face()->normal();
}
// 遍历到下一个相邻三角形
h = h->twin()->next();
} while (h != halfedge()); // 直到回到起始的半边
// 返回归一化后的法向量
return weighted_normal.unit();
没有顶点法线的茶壶着色
算法实现以后
4. 边翻转(Edge Flip)
简要说明您如何实现边翻转操作。
[显示茶壶在边缘翻转之前和之后的屏幕截图。]
分析
网格中的所有元素(即半边、顶点、边和⾯)如下图
根据半边数据结构很容易将列表写出,该数据结构如下图
列出更新前的列表
//内部的半边
HalfedgeIter h0 = e0->halfedge();
HalfedgeIter h1 = h0->next();
HalfedgeIter h2 = h1->next();
HalfedgeIter h3 = h0->twin();
HalfedgeIter h4 = h3->next();
HalfedgeIter h5 = h4->next();
//外部的半边
HalfedgeIter h6 = h1->twin();
HalfedgeIter h7 = h2->twin();
HalfedgeIter h8 = h4->twin();
HalfedgeIter h9 = h5->twin();
//顶点
VertexIter v0 = h0->vertex();//v0是h0的起点
VertexIter v1 = h3->vertex();//v1是h0的终点
VertexIter v2 = h2->vertex();//v2是h2的终点,作为翻转边的一个顶点
VertexIter v3 = h5->vertex();//v3是h5的终点,翻转边的另一个顶点
//面
FaceIter f0 = h0->face();
FaceIter f1 = h3->face();
//边
EdgeIter e1 = h1->edge();
EdgeIter e2 = h2->edge();
EdgeIter e3 = h4->edge();
EdgeIter e4 = h5->edge();
更新后的列表
//更新半边
h0->setNeighbors(h1, h3, v2, e0, f0);
h1->setNeighbors(h2, h9, v3, e4, f0);
h2->setNeighbors(h0, h6, v1, e1, f0);
h3->setNeighbors(h4, h0, v3, e0, f1);
h4->setNeighbors(h5, h7, v2, e2, f1);
h5->setNeighbors(h3, h8, v0, e3, f1);
h6->setNeighbors(h6->next(), h2, v2, e1, h6->face());
h7->setNeighbors(h7->next(), h4, v0, e2, h7->face());
h8->setNeighbors(h8->next(), h5, v3, e3, h8->face());
h9->setNeighbors(h9->next(), h1, v1, e4, h9->face());
//更新顶点
v0->halfedge() = h5;
v1->halfedge() = h2;
v2->halfedge() = h4;
v3->halfedge() = h1;
//更新面
f0->halfedge() = h0;
f1->halfedge() = h3;
//更新边
e0->halfedge() = h0;
e1->halfedge() = h2;
e2->halfedge() = h4;
e3->halfedge() = h5;
e4->halfedge() = h1;
代码
EdgeIter HalfedgeMesh::flipEdge(EdgeIter e0)//e0是要翻转的边
{
if (!e0->isBoundary()) {
//更新前后两部分的代码
}
return e0;
}
判断每个网格否位于边界上,可使用isBoundary() 函数,如果元素位于边界环上,则 该函数返回 e0。
结果
茶壶翻转前见3.3
部分边翻转后
5. 边分割(Edge Split)
简要说明您如何实现边分割操作。
分析
新顶点的位置为输入边的中点。
分割后边、点、面、半边的关系如下图
代码
if (!e0->isBoundary()) {
//内部的半边
HalfedgeIter h0 = e0->halfedge();
HalfedgeIter h1 = h0->next();
HalfedgeIter h2 = h1->next();
HalfedgeIter h3 = h0->twin();
HalfedgeIter h4 = h3->next();
HalfedgeIter h5 = h4->next();
//外部的半边
HalfedgeIter h6 = h1->twin();
HalfedgeIter h7 = h2->twin();
HalfedgeIter h8 = h4->twin();
HalfedgeIter h9 = h5->twin();
//顶点
VertexIter v0 = h0->vertex();//v0是h0的起点
VertexIter v1 = h3->vertex();//v1是h0的终点
VertexIter v2 = h2->vertex();//v2是h2的终点,作为翻转边的一个顶点
VertexIter v3 = h5->vertex();//v3是h5的终点,翻转边的另一个顶点
//面
FaceIter f0 = h0->face();
FaceIter f1 = h3->face();
//边
EdgeIter e1 = h1->edge();
EdgeIter e2 = h2->edge();
EdgeIter e3 = h4->edge();
EdgeIter e4 = h5->edge();
//新建顶点
VertexIter v4 = newVertex();
//新建半边
HalfedgeIter h10 = newHalfedge();
HalfedgeIter h11 = newHalfedge();
HalfedgeIter h12 = newHalfedge();
HalfedgeIter h13 = newHalfedge();
HalfedgeIter h14 = newHalfedge();
HalfedgeIter h15 = newHalfedge();
//新建面
FaceIter f2 = newFace();
FaceIter f3 = newFace();
//新建边
EdgeIter e5 = newEdge();
EdgeIter e6 = newEdge();
EdgeIter e7 = newEdge();
//更新半边
h0->setNeighbors(h1, h3, v0, e0, f0);
h1->setNeighbors(h2, h15, v4, e7, f0);
h2->setNeighbors(h0, h7, v2, e2, f0);
h3->setNeighbors(h4, h0, v4, e0, f1);
h4->setNeighbors(h5, h8, v0, e3, f1);
h5->setNeighbors(h3, h10, v3, e5, f1);
h6->setNeighbors(h6->next(), h14, v2, e1, h6->face());
h7->setNeighbors(h7->next(), h2, v0, e2, h7->face());
h8->setNeighbors(h8->next(), h4, v3, e3, h8->face());
h9->setNeighbors(h9->next(), h11, v1, e4, h9->face());
h10->setNeighbors(h11, h5, v4, e5, f2);
h11->setNeighbors(h12, h9, v3, e4, f2);
h12->setNeighbors(h10, h13, v1, e6, f2);
h13->setNeighbors(h14, h12, v4, e6, f3);
h14->setNeighbors(h15, h6, v1, e1, f3);
h15->setNeighbors(h13, h1, v2, e7, f3);
//更新中点位置
v4->position = (v0->position + v1->position) / 2.0;
v4->isNew = 1;
//更新顶点
v0->halfedge() = h0;
v1->halfedge() = h12;
v2->halfedge() = h15;
v3->halfedge() = h5;
v4->halfedge() = h3;
//更新面
f0->halfedge() = h0;
f1->halfedge() = h3;
f2->halfedge() = h10;
f3->halfedge() = h13;
//更新边
e0->halfedge() = h0;
e1->halfedge() = h14;
e2->halfedge() = h2;
e3->halfedge() = h4;
e4->halfedge() = h11;
e5->halfedge() = h5;
e6->halfedge() = h13;
e7->halfedge() = h1;
//设置新边标志
e0->isNew = 0;
e6->isNew = 0;
e5->isNew = 1;
e7->isNew = 1;
//返回新建的顶点
return v4;
}
return VertexIter();
结果
[显示网格在边分割之前和之后的屏幕截图。]
[显示网格在边分割和边翻转组合之前和之后的屏幕截图。]
翻转:
分割:
[如果实现了对边界边的支持,请显示正确处理边界边上分割操作的实现屏幕截图。]
边界的点线面关系:
边界分割代码如下
else {
//处理边界的分割
//获取边的信息
HalfedgeIter h0 = e0->halfedge();
HalfedgeIter h1 = h0->next();
HalfedgeIter h2 = h1->next();
HalfedgeIter h3 = h0->twin();
HalfedgeIter h4 = h2->twin();
HalfedgeIter h5 = h1->twin();
//顶点
VertexIter v0 = h0->vertex();
VertexIter v1 = h1->vertex();
VertexIter v2 = h2->vertex();
//面
FaceIter f0 = h0->face();
//边
EdgeIter e1 = h1->edge();
EdgeIter e2 = h2->edge();
//新建顶点
VertexIter v3 = newVertex();
//新建半边
HalfedgeIter h6 = newHalfedge();
HalfedgeIter h7 = newHalfedge();
HalfedgeIter h8 = newHalfedge();
HalfedgeIter h9 = newHalfedge();
//新建面
FaceIter f1 = newFace();
//新建边
EdgeIter e3 = newEdge();
EdgeIter e4 = newEdge();
//更新半边
h0->setNeighbors(h1, h3, v0, e0, f0);
h1->setNeighbors(h2, h6, v3, e4, f0);
h2->setNeighbors(h0, h4, v2, e2, f0);
h3->setNeighbors(h3->next(), h0, v3, e0, h3->face());
h4->setNeighbors(h4->next(), h2, v0, e2, h4->face());
h5->setNeighbors(h5->next(), h8, v2, e1, h5->face());
h6->setNeighbors(h7, h1, v2, e4, f1);
h7->setNeighbors(h8, h9, v3, e3, f1);
h8->setNeighbors(h6, h5, v1, e1, f1);
h9->setNeighbors(h9->next(), h7, v1, e3, h9->face());
//更新中点位置
v3->position = (v0->position + v1->position) / 2.0;
v3->isNew = 1;
//更新点
v0->halfedge() = h0;
v1->halfedge() = h9;
v2->halfedge() = h6;
v3->halfedge() = h1;
//更新面
f0->halfedge() = h0;
f1->halfedge() = h7;
//更新边
e0->halfedge() = h0;
e1->halfedge() = h8;
e2->halfedge() = h2;
e3->halfedge() = h7;
e4->halfedge() = h1;
//设置新边标志
e0->isNew = 0;
e3->isNew = 1;
e4->isNew = 1;
//返回新建的顶点
return v3;
}
导入一个右边图形,边界分割前后的图形如下
6. 利用Loop Subdivision进行网格上采样
简要说明您如何实现Loop细分的。
分析
Loop细分的目标是通过插入新顶点和调整现有顶点的位置,将原本较粗糙的多边形网格逐渐变得更加平滑。它是一种基于顶点的细分算法,通常应用于三角形网格(triangle mesh)。通过逐步细分原始的粗糙模型,Loop细分算法可以创建出细腻的、光滑的表面。
首先,Loop细分会在网格的每条边上插入一个新顶点。新顶点的位置根据原始顶点的位置和权重系数来计算。 其次,Loop细分会对原始的顶点进行位置调整。调整后的顶点位置不仅取决于该顶点原本的位置,还会参考周围邻居顶点的位置。这样做的目的是让网格变得更加光滑。
- 对于原始顶点:它们会根据周围邻点的平均值被移动到新的位置,以确保网格的平滑性。
- 对于新生成的顶点:它们的位置是通过它们相邻的顶点按照加权平均计算出来的。
对于位于⼀对三⻆形(A,C,B)和(A,B,D)交界边 AB 上的新顶点,其更 新⽅法如下
3/8 * (A + B) + 1/8 * (C + D)
原始顶点的位置更新为
(1 - n * u) * original_position + u * original_neighbor_position_sum
其中 n 是顶点度数,即与顶点关联的边数, u 是如图所示的顶 点, original_position 是该顶点的原始位置, ⽽ original_neighbor_position_sum 是原始顶点的相邻顶点的位置的总和。如果n=3,u=3/16,其余情况下u=3/(8*n)。
将网格进行4-1细分后,将连接一个旧顶点和一个新顶点的边进行翻转,优化网格的拓扑结构,确保网格的形状保持良好,避免不必要的变形或扭曲。如下图,
代码
for (EdgeIter e = mesh.edgesBegin(); e != mesh.edgesEnd(); e++) {
//标记原有的边为旧边
e->isNew = 0;
//获取边的信息
//内部的半边
HalfedgeIter h0 = e->halfedge();
HalfedgeIter h1 = h0->next();
HalfedgeIter h2 = h1->next();
HalfedgeIter h3 = h0->twin();
HalfedgeIter h4 = h3->next();
HalfedgeIter h5 = h4->next();
//顶点
VertexIter B = h0->vertex();
VertexIter C = h3->vertex();
VertexIter A = h2->vertex();
VertexIter D = h5->vertex();
//新顶点的位置
//预先计算并将新顶点的位置存储在旧边的 newPosition 字段中
e->newPosition = 3.0 / 8.0 * (B->position + C->position) + 1.0 / 8.0 * (A->position + D->position);
}
//计算新的顶点位置
for (VertexIter v = mesh.verticesBegin(); v != mesh.verticesEnd(); v++) {
//标记原有的顶点为旧顶点
v->isNew = 0;
//将旧顶点的新位置设置为其周围旧顶点的加权组合
HalfedgeIter h = v->halfedge();
Vector3D original_neighbor_position_sum(0, 0, 0);
do {
original_neighbor_position_sum += h->twin()->vertex()->position;
h = h->twin()->next();
} while (h != v->halfedge());
float n = (float)v->degree();
float u = (n == 3.0) ? (3.0 / 16.0) : (3.0 / (8.0 * n));
v->newPosition = (1.0 - n * u) * v->position + u * original_neighbor_position_sum;
}
//分割所有旧边
for (EdgeIter e = mesh.edgesBegin(); e != mesh.edgesEnd(); e++) {
VertexIter B = e->halfedge()->vertex();
VertexIter C = e->halfedge()->twin()->vertex();
//当它是旧边时分割
if (!(B->isNew || C->isNew)) {
VertexIter E = mesh.splitEdge(e);
E->newPosition = e->newPosition;
}
}
//翻转连接一个旧顶点和一个新顶点的新边,优化网格拓扑结构
for (EdgeIter e = mesh.edgesBegin(); e != mesh.edgesEnd(); e++) {
VertexIter v0 = e->halfedge()->vertex();
VertexIter v1 = e->halfedge()->twin()->vertex();
if (e->isNew && (v0->isNew + v1->isNew == 1)) {
mesh.flipEdge(e);
}
}
//更新所有顶点的位置
for (VertexIter v = mesh.verticesBegin(); v != mesh.verticesEnd(); v++) {
v->position = v->newPosition;
v->isNew = 0;
}
}
结果
[截取一些屏幕截图,观察网格在细分后的行为:尖角和边界会发生什么?可以通过预先分割一些边缘来减少这种影响吗?]
[加载 dae/cube.dae,对立方体执行几次细分迭代。请注意,在重复细分后,立方体变得略微不对称。您能否通过边缘翻转和分割对立方体进行预处理,以便立方体对称细分?记录这些效果并解释它们发生的原因。还要解释您的预处理如何帮助缓解这些影响。]
解决不对称问题
对于立方体,我先手动使每个面分割成四个三角形(即连接面对角线),然后再进行loop细分,就得到了正确的对称的正方体
原因
-
每个正方形面连接两条对角线,将其分割为四个三角形。
-
这种方法确保每个面内部的三角形连接方式一致,消除了初始三角化过程中可能存在的不一致性,
-
减少细分过程中的误差积累:
通过预先规则化网格拓扑结构,减少了细分过程中因不规则性引入的误差,从而保持了立方体的整体对称性。
预处理前
预处理后
四、实践与思考
简要介绍编程调试经历,观察算法实验效果的优点和存在的问题,若有,则如何进行改进的思路和效果。
编程调试经历与实验观察
在完成本次计算机图形学作业过程中,我经历了多次的编程调试,特别是在实现贝塞尔曲线与曲面以及基于半边数据结构的三角网格上采样时,面临了一些挑战,同时也取得了许多宝贵的经验。下面简要介绍调试过程、实验效果的优点和存在的问题,以及一些改进的思路和效果。
1. 编程调试经历
-
贝塞尔曲线与曲面实现
-
挑战:递归实现的正确性:
- 在实现1D de Casteljau算法时,递归函数需要对控制点进行多次插值。调试的主要问题在于递归终止条件的判断,以及如何正确处理浮点数精度带来的误差。
- 调试策略:通过增加详细的日志输出来验证每一步的插值计算是否正确,并通过绘制中间点观察曲线的变化,以确保最终生成的曲线是光滑且符合期望的。
-
贝塞尔曲面:
- 在处理二维贝塞尔曲面时,如何正确组织和处理控制点是一个较大的挑战。最初遇到的问题是控制点顺序的错误导致曲面形状异常。
- 调试策略:通过将曲面拆解为逐行插值和逐列插值,分步调试,从而发现控制点的错误顺序并修正。
-
-
基于半边数据结构的网格操作
-
挑战:边翻转与分割的实现:
- 在实现边翻转和边分割时,确保所有相关的半边、顶点、边和面正确更新是相当复杂的。一开始常见的问题是数据指针未正确更新,导致网格出现断裂或无效引用。
- 调试策略:通过逐步输出半边、顶点等对象的状态,检查它们的指向是否正确,并使用可视化工具实时查看网格变化,帮助识别和解决错误。
-
Loop细分:
- 在Loop细分过程中,面临的一个常见问题是如何正确地平衡新顶点和旧顶点的位置,使得细分后网格既平滑又符合原始形状。调试中发现部分顶点位置不符合预期,导致网格扭曲。
- 调试策略:通过手动计算少量顶点的位置来验证细分计算是否正确,并引入均值法来平衡顶点新位置的计算,从而提高细分结果的平滑度和对称性。
-
2. 算法实验效果的优点和存在的问题
-
贝塞尔曲线与曲面
- 优点:贝塞尔曲线和曲面的实现效果良好,曲线生成的光滑度高,特别是使用de Casteljau算法能够逐步收敛到精确的曲线上,曲面插值的结果平滑且自然。
- 存在的问题:在处理高阶贝塞尔曲线和复杂贝塞尔曲面时,计算量和递归深度增加较快,导致程序运行缓慢。
- 改进思路:可以使用更高效的算法(如Bernstein多项式)或者将递归改为迭代实现,以减少递归深度。并通过缓存中间结果来减少重复计算,提高计算效率。
-
基于半边数据结构的网格操作
- 优点:边翻转和边分割的操作对于网格的优化和上采样效果显著,特别是在处理复杂网格时,能够有效提高网格的平滑度和整体结构的对称性。
- 存在的问题:在进行边分割和Loop细分时,细分后的新顶点的位置计算有时会产生轻微的误差,特别是在不规则网格中,容易导致网格出现细微的不对称或边缘拉长的情况。
- 改进思路:为了解决这些细微的不对称,可以在每次细分后对网格进行局部的边翻转操作来平衡网格结构。此外,可以通过对邻近顶点进行加权平均来平滑新顶点的位置,以减少位置误差。
3. 改进效果
-
效率与性能:
- 在贝塞尔曲线和曲面实现中,将递归改为迭代,并引入缓存后,运行时间大幅减少,特别是在处理高阶贝塞尔曲线时,性能得到了显著提升。
-
网格的对称性和光滑度:
- 在Loop细分中,通过增加边翻转的次数来保持网格的对称性,实验效果表明,细分后的网格整体光滑度更高,边缘拉伸问题也得到了缓解。
- 通过改进新顶点位置的加权计算,细分后的网格更符合原始网格的几何形状,且局部细节更加自然,提升了网格的视觉效果。
提高细分结果的平滑度和对称性。
2. 算法实验效果的优点和存在的问题
-
贝塞尔曲线与曲面
- 优点:贝塞尔曲线和曲面的实现效果良好,曲线生成的光滑度高,特别是使用de Casteljau算法能够逐步收敛到精确的曲线上,曲面插值的结果平滑且自然。
- 存在的问题:在处理高阶贝塞尔曲线和复杂贝塞尔曲面时,计算量和递归深度增加较快,导致程序运行缓慢。
- 改进思路:可以使用更高效的算法(如Bernstein多项式)或者将递归改为迭代实现,以减少递归深度。并通过缓存中间结果来减少重复计算,提高计算效率。
-
基于半边数据结构的网格操作
- 优点:边翻转和边分割的操作对于网格的优化和上采样效果显著,特别是在处理复杂网格时,能够有效提高网格的平滑度和整体结构的对称性。
- 存在的问题:在进行边分割和Loop细分时,细分后的新顶点的位置计算有时会产生轻微的误差,特别是在不规则网格中,容易导致网格出现细微的不对称或边缘拉长的情况。
- 改进思路:为了解决这些细微的不对称,可以在每次细分后对网格进行局部的边翻转操作来平衡网格结构。此外,可以通过对邻近顶点进行加权平均来平滑新顶点的位置,以减少位置误差。
3. 改进效果
-
效率与性能:
- 在贝塞尔曲线和曲面实现中,将递归改为迭代,并引入缓存后,运行时间大幅减少,特别是在处理高阶贝塞尔曲线时,性能得到了显著提升。
-
网格的对称性和光滑度:
- 在Loop细分中,通过增加边翻转的次数来保持网格的对称性,实验效果表明,细分后的网格整体光滑度更高,边缘拉伸问题也得到了缓解。
- 通过改进新顶点位置的加权计算,细分后的网格更符合原始网格的几何形状,且局部细节更加自然,提升了网格的视觉效果。
总的来说,通过本次作业的编程调试,我对计算机图形学中几何建模的核心算法有了更深的理解。通过不断调整和优化代码,我实现了贝塞尔曲线、贝塞尔曲面以及基于半边数据结构的网格操作,并通过多次实验有效提升了细分网格的对称性和光滑度,同时提高了程序的运行效率。