图形学第二次作业

计算机图形学

课程作业2:网格编辑

一、概述

简要介绍作业实现的主要内容。

本次计算机图形学作业包括两个模块,分别是贝塞尔曲线与曲面实现基于半边数据结构的三角网格

模块一:贝塞尔曲线与曲面
  1. 算法1:使用1D de Casteljau实现贝塞尔曲线
    • 通过实现递归算法对给定的控制点进行线性插值,生成贝塞尔曲线上的中间点和最终点。
    • de Casteljau算法是一种迭代线性插值方法,通过不断对控制点进行插值,逐步收敛得到曲线上的任意一点。
  2. 算法2:基于1D de Casteljau实现贝塞尔曲面
    • 将贝塞尔曲线的算法扩展至二维,生成贝塞尔曲面。
    • 通过逐行对控制点插值,再对每行的结果进行插值,最终生成曲面上对应的点。
模块二:基于半边数据结构的三角网格上采样
  1. 算法3:加权面积法求顶点法线
    • 通过对顶点相邻三角形的法向量进行加权平均来计算顶点的法向量,其中权重由三角形面积决定。
    • 这种方法常用于实现Phong着色,使得曲面更光滑、明暗过渡更自然(计算机图形学作业2)。
  2. 算法4:边翻转(Edge Flip)
    • 实现局部网格重构操作,翻转相邻两个三角形的共享边以改变网格拓扑结构。
    • 边翻转操作的实现需要确保所有半边、顶点、边和面在翻转后指向正确的网格元素。
  3. 算法5:边分割(Edge Split)
    • 对网格中的边进行分割,在边的中点插入一个新的顶点,并对原始的三角形进行分割,形成新的三角形。
    • 这种操作会引入新的顶点、边和面,增加网格的复杂度(计算机图形学作业2)。
  4. 算法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 年提出,常用于计算任意阶的贝塞尔曲线,因为它比直接使用贝塞尔公式数值稳定。

算法步骤:
  1. 输入:给定一组控制点和参数 tt 通常在 [0, 1] 之间)。

  2. 线性插值:对于每一对相邻控制点,按照插值公式进行计算:

    在这里插入图片描述

    这将生成一组新的点,称为中间点。

  3. 递归:对新生成的中间点集重复步骤 2,直到只剩下一个点为止。

  4. 输出:当只剩下一个点时,这个点就是曲线在参数 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方向上插值,源于曲面本质上是二维的结构,这需要在两个方向上进行插值。具体来说,贝塞尔曲面是由二维网格状排列的控制点构成的。为了得到贝塞尔曲面上某个点的最终位置,我们需要分别在两个方向(uv)对控制点进行插值计算。

原因和步骤分析:
  1. 贝塞尔曲面定义
    贝塞尔曲面是贝塞尔曲线的二维扩展,它由一组二维控制点网格构成。网格中的每行和每列可以视为一条贝塞尔曲线。

    • u 方向表示在控制点网格中的行方向(横向)。
    • v 方向表示在控制点网格中的列方向(纵向)。
  2. 先在 u 方向插值
    对于贝塞尔曲面上某个点,首先,我们需要固定 v,对网格中的每一行控制点进行贝塞尔插值,得到一组中间的插值点。这相当于对控制点网格中的每一行执行一次一维贝塞尔曲线插值。插值参数 u 决定了如何在每一行的控制点之间插值。

    • 这一过程生成了一组沿 v 方向排列的点(每行一个点)。
  3. 再在 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),即在两个参数uv的控制下,对控制点网格进行插值,得到曲面上对应的三维点。这个函数的核心思想是先在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细分中,通过增加边翻转的次数来保持网格的对称性,实验效果表明,细分后的网格整体光滑度更高,边缘拉伸问题也得到了缓解。
    • 通过改进新顶点位置的加权计算,细分后的网格更符合原始网格的几何形状,且局部细节更加自然,提升了网格的视觉效果。

​ 总的来说,通过本次作业的编程调试,我对计算机图形学中几何建模的核心算法有了更深的理解。通过不断调整和优化代码,我实现了贝塞尔曲线、贝塞尔曲面以及基于半边数据结构的网格操作,并通过多次实验有效提升了细分网格的对称性和光滑度,同时提高了程序的运行效率。

计算机图形学作业题 1. 计算机中由图形的形状参数(方程或分析表达式的系数,线段的端点坐标等)加属性参数 (颜色、线型等)来表示图形称图形的参数表示;枚举出图形中所有的点称图形的点阵 表示,简称为图像(数字图像) 2. 什么是计算机图形学?计算机图形学有哪些研究内容? 3. 计算机图形学有哪些应用领域? 4. 计算机图形学有哪些相关学科分支?它们的相互关系是怎样的? 5. 图形系统的软件系统由哪些软件组成?举例说明。 6. 了解计算机图形系统的硬件。 7. 什么是显示器的分辨率、纵横比、刷新率? 8. 什么是像素、分辨率、颜色数?分辨率、颜色数与显存的关系? 分辨率M(N、颜色个数K与显存大小V的关系: 例:分辨率为1024像素/行(768行/帧,每像素24位(bit)颜色(224种颜色)的显示 器,所需的显存为:1024(768(24位(bit)=1024(768(24/8=2359296字节(byte)。 或:每像素16777216种颜色(24位真彩色),1024(768的分辨率,所需显存为:102 4(768(log216777216位显存=2359296字节显存。 9. 什么是图元的生成?分别列举两种直线和圆扫描转换算法。 10. OpenGL由核心库GL(Graphics Library)和实用函数库GLU(Graphics Library Utilities)两个库组成。 11. 区域填充算法要求区域是连通的,因为只有在连通区域中,才可能将种子点的颜色扩 展到区域内的其它点。 区域可分为 向连通区域和 向连通区域。区域填充算法有 填充算法和 填充算法。 12. 字符生成有哪两种方式? 点阵式(bitmap fonts点阵字——raster光栅方法):采用逐位映射的方式得到字符的点阵和编码——字模位 点阵。 笔画式(outline fonts笔画字——stroke方法):将字符笔画分解为线段,以线段端点坐标为字符字模的编 码。 13. 图形信息包含图形的 和 。 14. 什么是图形变换?图形变换只改变图形的 不改变图形的 。图形变换包括 和 ( )。 15. 熟练掌握二维图形的齐次坐标表示、平移、比例、旋转、对称变换以及复合变换的方 法和原则。 16. 图形的几何变换包括 、 、 、 、 ;图形连续作一次以上的几何变换称 变换。 17. 试写出图示多边形绕点A(xo,yo)旋转的变换矩阵。要求写出求解过程及结果。 18. 试写出针对固定参考点、任意方向的比例变换矩阵。 19. 试写出对任意直线y=mx+b的对称变换矩阵。 20. 什么是窗口?什么是视区?什么是观察变换? 21. 简述二维观察变换的流程。 22. 试述窗口到视区的变换步骤,并推出变换矩阵。 23. 已知w1=10,w2=20,w3=40,w4=80, v1=80,v2=110,v3=10,v4=130, 窗口中一点P(15,60),求视区中的映射点P'? 24. 在观察变换前必须确定图形的哪部分在窗口内,那些部分在窗口外,这个选择处理过 程称为 。 25. 使用Open GL的变换函数,若程序中先后调用的几个变换函数所定义的矩阵及顺序为L, M, N,其作用顺序为: 。 26. 试列举你所知的直线和多边形裁剪算法。 27. 简述Cohen-Sutherland(代码)线段裁剪算法。 28. 窗口和多边形如下图,应用Sutherland- Hodgman算法(逐边裁剪算法),对多边形进行裁剪。请以左、上、右、下的顺序列出 窗口各边裁剪多边形后所得的多边形顶点表。 29. 任何满足欧拉公式的形体称为 形体。 30. 超二次曲面通过将额外的参数插入 曲面方程而形成。 31. 在曲线、曲面的表示上,参数方程有何优点? 32. 要变换参数曲线曲面可以直接变换它的 ,而对于非参数形式则必须变换 。 33. 欧几里得曲线是 物体,沿三维曲线路径的位置可用 参数描述。 34. 规格化参变量 t [0, 1] 使得曲线曲面的 容易确定。 35. 什么是插值?什么是逼近?什么是拟合? 36. 给定一组有序的数据点 Pi ,i =0, 1, …, n,称为控制点,构造一条曲线顺序通过每个控制点,称为对这组控制点进行 ,所构造的曲线称为 。 37. 构造一条曲线使之在某种意义下最接近给定的数据点,而不要求通过其中任何一个点 ,称为对这些数据点进行 ,所构造的曲线曲线。 38. 拟合(Fitting)是 和 的统称。 39. 对于一组有序的型值点,确定一种参数分割,称之对这组型值点 。确定某个单参数矢函数,即确定参数曲线方程,称为曲线的 。 40. 参数域中所有节点构成的序列称为 矢量。 41. 什么是参数化?什么是参数区间的规格化? 42. 什么是参数连续性? 二条曲线P
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值