# Mini-Spline **Repository Path**: null_552_0547/mini-spline ## Basic Information - **Project Name**: Mini-Spline - **Description**: Mini-Spline 通过 Qt 下的 OpenGL 编程实现样条曲线曲面生成、数据拟合、点云参数化、正交投影等功能。其中整理了一个小型的数值计算库,一个半边结构的网格数据类型,并实现网格模型的基本处理方法。 - **Primary Language**: C++ - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-06-01 - **Last Updated**: 2025-06-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Mini-Spline #### 介绍 Mini-Spline 通过 Qt 下的 OpenGL 编程实现样条曲线曲面生成、数据拟合、点云参数化、正交投影等功能。其中整理了一个小型的数值计算库,一个半边结构的网格数据类型,并实现网格模型的基本处理方法。 #### 开发进度 3.9 * 整理线性代数库,修改了断言的方式为标准 assert; * 使用 catch2 开源库进行数值测试 * 完成向量操作测试 * 完成矩阵基本操作测试 * 对基本矩阵求解器进行测试 3.10 * 完成隐式 QR 方法求解器的调试; * 搭建窗口程序; * 建立抽象类 MiniObject 以及 MiniCloud, MiniCurve 子类; * 完成一般着色器配置,可以绘制 MiniCloud; 3.11 * 优化片段着色器,可以修改颜色 UserColor; * 顶点着色器添加视图、模型、投影矩阵; * 之后合并了模型视图矩阵,去除了单独的模型矩阵; * 完成相机类的设计 MiniCamera; * 完成鼠标滚动调整距离,鼠标点击拖拽调整视角; * 完成基本绘制框架:坐标轴 drawAxis,物体绘制流程 drawObject; * 将半边网格整合为 MiniMesh 类型; * 重新调整了 point3f, vector3f, rgb_space 结构,整合为 Vector3f 统一表示三维点/向量/RGB 空间类型; * 整合 Quaternion 四元数类型; * 去除了包围盒和 QEM 简化算法; * 完成兔子模型的读取显示; * **注意到 SpLinearSolver 的 Jacobi 迭代没有用到权 omega;暂时搁置稀疏矩阵类型和求解器;** * 预计使用 Catch2 检测 Vector3f, Quaternion 类型的算法,还未考虑如何测试 MiniMesh 半边网格类型; 3.12 * 移除增量凸包和平面凸包算法; * 移除 MiniMesh 中的颜色列表; * 修改了网格材质结构; * 完成 Phong 光照着色器:包括漫反射、镜面反射、环境光、距离衰减效果; 3.13 * 将 Test 测试部分整合进来,调整 makefile 文件,使得测试程序可以直接调用 Qt 程序相关的头文件和源文件; * 测试 Vector3f 类型及相关算法; 3.14 * 测试 Quaternion 类型及相关算法; * 调整了光照系数,将其与渲染颜色合并; * 将光照和材质结构单独封装; * 调整继承关系: * MiniObject -> MiniCurve -> MiniPolyline * MiniObject -> MiniCurve -> MiniBSpline * 规划文件数据格式 3.15 * 暂时确定项目类图 MinSpline * 完善这些类的成员信息 * 修改成员继承关系 MiniSpline * 完成 MiniPolyline MiniLine 类; * 整合 B 样条曲线类型及其算法,并用 Catch2 进行测试: * 寻找 u 参数区间 * 计算 u 参数下的非零基函数 * 计算 u 参数对应的曲线值 3.16 * 整合 B 样条曲线类型及其算法,并用 Catch2 进行测试: * 计算 u 参数对应的导数值 * 计算导数曲线控制点 * 整合 B 样条曲面及其算法: * 计算 u,v 参数对应的点 3.17 * 修改继承关系:简化图元的继承; * 重新整理图形绘制,将相关方法封装在 MiniHandler 中,通过 handler 获得图元的绘制信息; * 整合 B 样条曲面及其算法,并用 Catch2 进行测试: * 计算 u,v 参数对应的偏导曲线的控制点 * 计算 u,v 参数对应的偏导 3.18 * 整合 B 样条曲线类型及其算法,并用 Catch2 进行测试: * 计算 u,v 参数对应的非零基函数及其导数 * 整合 B 样条曲面及其算法,并用 Catch2 进行测试: * 验证计算 u,v 参数对应的偏导及其控制点 * 实现 Handler 类的继承关系,分别保管不同类型的图元; * 实现对点云的拾取操作; 3.19 * 实现对样条曲线、样条曲面的基本拾取操作(基于点的拾取,因此对于物体中的“空隙”不能拾取); * 实现样条曲面的光照绘制; * 实现样条曲线控制多边形的绘制; * 实现样条曲面控制点的绘制; * 增加对点云的顶点修改交互方法; * 取消了对 B 样条曲线曲面的直接拾取,改为可以拾取控制顶点; * 增加 B 样条曲线和曲面的交互方法(修改控制顶点),按照三个给定方向计算鼠标移动的投影,沿着投影最大的方向移动; * 增加线框模式和填充模式的切换; 3.21 * 将 Polyline 类型重新分离出来,并创建对应的处理类; * 增加 B 样条曲面显示等参线; 3.22 * 优化修改控制顶点的方法,对 x,y,z 坐标轴和鼠标移动都进行投影到投影平面上,然后计算鼠标移动在投影后的 x,y,z 轴上的移动; * 增加显示/隐藏控制点的功能; * 测试样条曲线最小二乘拟合点云的方法(给定参数曲线表达式,手动输入数据点进行拟合); * 将曲线拟合方法合并到 MiniCloud 中; * 测试样条曲面最小二乘拟合点云的方法; * 将曲面拟合方法合并到 MiniCloud 中; * 现在的类继承关系 MiniSpline * **增加对 MiniObject 类的全部 assert 断言(MiniBSplineSurface 没有设置断言)**; 3.24 * 实现 PCA 方法计算离散点集的 3D 包围盒算法; * 实现 B 样条曲线的节点插入算法; * 实现 B 样条曲面的节点插入算法(基于 B 样条曲线的节点插入,效率较低); * **预期使用 Catch2 测试这些算法**; 3.25 * 实现最近点的牛顿迭代计算点到 B 样条曲线投影的算法; * 实现最近点的牛顿迭代计算点到 B 样条曲面投影的算法; * 实现点到 B 样条曲线的一阶正交投影算法; * 实现点到 B 样条曲面的二阶正交投影算法(从下向上投影还有问题,可能和曲率圆的方向有关系,之后测试); 3.26 * 实现一个曲面拟合曲线的组合算法; * 整理曲线、曲面拟合以及最近点投影、正交投影算法的测试; * 发现一个 bug,点云拖动会消失(修复,由于测试代码中 cloud 指针被删除); * 修改曲面的最近点和正交投影算法的输入:增加了选择初始迭代点的参数; * 测试空间曲线的曲面拟合方法; 4.13 * 修改图形界面: * 增加显示图元信息的表格控件 * 增加输入命令名的输入框 * 增加执行命令的按钮 * 增加填写参数、显示算法结果(误差)的表格控件 * 增加对 B 样条曲线曲面的节点插入算法的测试程序 * 增加 B 样条曲线曲线节点优化算法以及测试程序 4.14 * 实现最小二乘渐进迭代逼近算法 LSPIA(曲线、曲面) * 实现增量迭代计算(不断插入新的节点) * 产生的问题 * 不使用增量迭代,计算效果不佳,因为曲线围成的空洞里的控制点没有办法移动 * 新的节点插入在曲线附近,导致插入节点在这里非常密集,产生曲面的质量差 * 好处是只要限制可移动的控制点,就可以避免奇异情况 4.20 * 并入多项式类型 Polynomial 和数值积分方法 Integrator * 提取 FindSpan 等基函数的函数到 MiniObject 中定义 * MiniBSpline 重命名为 MiniBSplineCurve * 修改了 MiniBSplineCurve,MiniBSplineSurface 中关于 FindSpan 等基函数的部分 * 对应调整了 Test 程序中的测试代码 4.21 * 增加能量项约束 $$ E_v=E_{LS}+E_{bending}+E_{streching} $$ * 使用均匀样条拟合,节点插入时也均匀插入,迭代两三次 * 增加能量项,系数和为 1 * 增加参数值变化量作为终止准则 * 针对参数化算法优化 Cholesky 分解过程 * 测试 CPU 时间 4.22 * 修改并测试稀疏矩阵类型,加速最小二乘系数的计算速度; * 修改基本数据类型 * MiniBSpline -> MiniBSplineCurve * MiniPolyline -> MiniDiscreteCurve * 删除 MiniLine 类型 * 增加 MiniParaCurve, MiniParaSurface 类型 4.23 * 增加测试稀疏矩阵的 Cholesky 分解算法,并验证其复杂度过高,最后删除; * 实现 MiniParaCurve, MiniParaSurface 类型的基本成员函数; * 实现加权中传播的 LSPIA 方法:使用高斯加权,即用一个矩阵作为核心平均加权,虽然能算,但是效果不佳; * 考虑将控制点向外移动,避免折叠的方法 4.24 * 增加 RungeKutta 法和 ODE 求解器并进行测试 * 经典 RK 方法 * ESDI RK 方法 * Gauss-Legendre RK 方法 * 增加了产生首一平移 Gauss-Legendre 多项式的函数 * 为只投影曲线的参数化方法增加曲线正交投影算法; 4.25 * 修改图元显示颜色的交互模式: * 取消了曲面的选取操作 * 增加了 basisColor 变量,控制不同图元的显示颜色 * 增加了参量曲线、曲面的求导和求偏导操作 * 曲面求偏导设置精度为 1e-6,因为 1e-8 精度下函数差为零(不稳定现象) * 增加了 MiniParaCurve, MiniParaSurface 的显示控制句柄 * 测试曲线投影下的参数化方法:效果不佳; * 增加步进修正的正交投影算法; 4.26 * 完成步进修正的正交投影方法; * 修正有向包围盒投影方向的问题; * 增加基于 QR 分解求解拟合线性系统的方法; * 修改生成均匀样条的高效构造函数; * 增加使用环面正交投影的点投影算法; * 修正了法曲率圆的正交点投影算法; 4.27 * 增加迭代生成初始 DBS 的算法; * 网格单点距离最小化投影算法 * 网格等参线点弦长最小化 + 距离最小化加权投影算法 * 两个算法效果相近 * 后者的系数矩阵是三对角阵 * **考虑增加三对角阵的上下二对角分解** * **考虑增加追赶法求解方程组** * 统一能量项拟合与 LSPIA 函数的参数输入; * 发现一个问题:如果控制网格在 uv 方向相同,产生的包围盒的坐标会沿着对角线,导致一般 LSPIA 与最小二乘拟合都不能计算; * 修改了冒泡排序的 bug:同时调整了 Vector, MiniCloud 中的 sort 方法;(之后又进行了调整) * **暂时搁置包围盒的问题** 5.2 * 移除 QR 分解求解拟合线性系统的方法; ```cpp MiniBSplineSurface *MiniCloud::EnergyFittingSurfaceQR(MiniArray &u, MiniArray &v, int Nu, int Nv, int p, int q) const { // N 个控制点 int nu = Nu - 1; int nv = Nv - 1; int m = m_points.size() - 1; // 构造样条 MiniBSplineSurface *spline = new MiniBSplineSurface(nu + 1, nv + 1, p, q); MiniArray2D &cpts = spline->GetPoints(); // 标准化到 [0,1] 上,由于这时 u,v 参数不是分别递增的,所以需要寻找最大最小值 Real umin = u[0]; Real umax = u[0]; for (int i = 0; i < m + 1; i++) { if (umin > u[i]) umin = u[i]; if (umax < u[i]) umax = u[i]; } Real du = umax - umin; for (int i = 0; i < m + 1; i++) u[i] = (u[i] - umin) / du; Real vmin = v[0]; Real vmax = v[0]; for (int i = 0; i < m + 1; i++) { if (vmin > v[i]) vmin = v[i]; if (vmax < v[i]) vmax = v[i]; } Real dv = vmax - vmin; for (int i = 0; i < m + 1; i++) v[i] = (v[i] - vmin) / dv; // 计算系数矩阵的 CPU 时间 clock_t start = clock(); // 填充最小二乘系数矩阵 Matrix A(m + 1 + (nu + 1) * (nv + 1), (nu + 1) * (nv + 1)); for (int k = 0; k < m + 1; k++) { int uSpan = FindSpan(u[k], spline->GetUDegree(), spline->GetUKnots()); int vSpan = FindSpan(v[k], spline->GetVDegree(), spline->GetVKnots()); MiniArray Bu = BasisFuncs(uSpan, u[k], spline->GetUDegree(), spline->GetUKnots()); MiniArray Bv = BasisFuncs(vSpan, v[k], spline->GetVDegree(), spline->GetVKnots()); // 每次填充 (p + 1) * (q + 1) 规模的元素 int uIndex = (uSpan - p) * (nv + 1); for (int i = 0; i < p + 1; i++) { int vIndex = vSpan - q; for (int j = 0; j < q + 1; j++) A(k, uIndex + vIndex + j) = Bu[i] * Bv[j]; uIndex += nv + 1; } } // 构造含有能量项约束的系数矩阵,先计算出 Gauss 积分权和节点 GaussIntegrator I(3); Vector nodes = I.nodes(); Vector weights = I.weights(); // 分别沿着 u,v 方向计算积分,然后保存 Matrix Bu0(nu + 1, nu + 1); Matrix Bu1(nu + 1, nu + 1); Matrix Bu2(nu + 1, nu + 1); Matrix Bv0(nv + 1, nv + 1); Matrix Bv1(nv + 1, nv + 1); Matrix Bv2(nv + 1, nv + 1); MiniArray &uknots = spline->GetUKnots(); for (int i = p; i < nu + 1; i++) { Real mid = (uknots[i + 1] + uknots[i]) / 2; Real ratio = (uknots[i + 1] - uknots[i]) / 2; // 三个积分节点分别计算值相加 for (int j = 0; j < 3; j++) { MiniArray2D Bu = DerivBasisFuncs(2, i, mid + nodes[j] * ratio, p, uknots); for (int ui = 0; ui < p + 1; ui++) { for (int uj = 0; uj < p + 1; uj++) { Bu0(i - p + ui, i - p + uj) += Bu[0][ui] * Bu[0][uj] * weights[j]; Bu1(i - p + ui, i - p + uj) += Bu[1][ui] * Bu[1][uj] * weights[j]; Bu2(i - p + ui, i - p + uj) += Bu[2][ui] * Bu[2][uj] * weights[j]; } } } } MiniArray &vknots = spline->GetVKnots(); for (int i = q; i < nv + 1; i++) { Real mid = (vknots[i + 1] + vknots[i]) / 2; Real ratio = (vknots[i + 1] - vknots[i]) / 2; // 三个积分节点分别计算值相加 for (int j = 0; j < 3; j++) { MiniArray2D Bv = DerivBasisFuncs(2, i, mid + nodes[j] * ratio, q, vknots); for (int vi = 0; vi < q + 1; vi++) { for (int vj = 0; vj < q + 1; vj++) { Bv0(i - q + vi, i - q + vj) += Bv[0][vi] * Bv[0][vj] * weights[j]; Bv1(i - q + vi, i - q + vj) += Bv[1][vi] * Bv[1][vj] * weights[j]; Bv2(i - q + vi, i - q + vj) += Bv[2][vi] * Bv[2][vj] * weights[j]; } } } } double time = double(clock() - start) / CLOCKS_PER_SEC; // qDebug() << "Prepare Matrix: " << time; // 计算获得最终矩阵的 CPU 时间 start = clock(); // 配置权重 int Nuv = (nu + 1) * (nv + 1); Real alpha = 1e-6; Real beta = 1e-6; Real gamma = 1 - alpha - beta; for (int i = 0; i < Nuv; i++) { for (int j = 0; j < Nuv; j++) { // 获得 B_{ui}'(u) * B_{vi}(v) * B_{uj}'(u) * B_{vj}(v) 对应的索引 int ui = i / (nv + 1); int uj = j / (nv + 1); int vi = i % (nv + 1); int vj = j % (nv + 1); // 累计在 B 矩阵上 A(m + 1 + i, j) += Bu1(ui, uj) * Bv0(vi, vj) * alpha / gamma; // BuBu^T A(m + 1 + i, j) += Bu0(ui, uj) * Bv1(vi, vj) * alpha / gamma; // BvBv^T A(m + 1 + i, j) += Bu2(ui, uj) * Bv0(vi, vj) * beta / gamma; // BuuBuu^T A(m + 1 + i, j) += Bu0(ui, uj) * Bv2(vi, vj) * beta / gamma; // BvvBvv^T A(m + 1 + i, j) += 2 * Bu1(ui, uj) * Bv1(vi, vj) * beta / gamma; // 2 * BuvBuv^T } } time = double(clock() - start) / CLOCKS_PER_SEC; // qDebug() << "Final Matrix: " << time; // 计算求解整个系统的 CPU 时间 start = clock(); // 求解最小二乘+能量约束系统 Vector bx(m + 1 + Nuv), by(m + 1 + Nuv), bz(m + 1 + Nuv); for (int j = 0; j < m + 1; j++) { bx[j] = m_points[j].x(); by[j] = m_points[j].y(); bz[j] = m_points[j].z(); } // 作 QR 分解 int row = A.row(); int col = A.col(); Vector d(col); Matrix B = A.QR(d); // Hb = b - beta * v * v^T * b = b - (beta * v^T * b) * v for (int i = 0; i < col; i++) { // Householder 变换作用于 bx Real sum = bx[i]; for (int j = i + 1; j < row; j++) sum += B(j, i) * bx[j]; sum *= d[i]; bx[i] -= sum; for (int j = i + 1; j < row; j++) bx[j] -= B(j, i) * sum; // Householder 变换作用于 by sum = by[i]; for (int j = i + 1; j < row; j++) sum += B(j, i) * by[j]; sum *= d[i]; by[i] -= sum; for (int j = i + 1; j < row; j++) by[j] -= B(j, i) * sum; // Householder 变换作用于 bz sum = bz[i]; for (int j = i + 1; j < row; j++) sum += B(j, i) * bz[j]; sum *= d[i]; bz[i] -= sum; for (int j = i + 1; j < row; j++) bz[j] -= B(j, i) * sum; } // 获得一个更小规模的矩阵 Vector b1(col), b2(col), b3(col); for (int i = 0; i < col; i++) { b1[i] = bx[i]; b2[i] = by[i]; b3[i] = bz[i]; } // 求解上半部分的线性方程组 LinearSolver solver; Vector X = solver.solveUpper(B, b1); Vector Y = solver.solveUpper(B, b2); Vector Z = solver.solveUpper(B, b3); for (int i = 0; i < nu + 1; i++) { for (int j = 0; j < nv + 1; j++) { int index = i * (nv + 1) + j; cpts[i][j] = {X[index], Y[index], Z[index]}; } } time = double(clock() - start) / CLOCKS_PER_SEC; // qDebug() << "Solving Time: " << time; return spline; } ``` * 将有向包围盒和归一化算法提取出来单独作为外部函数; 5.3 * 增加累积弦长最小化求解拟合线性系统的方法; * 增加平滑化函数约束求解拟合线性系统的算法; * 移除 WLSPIA 方法 ```cpp MiniBSplineSurface *MiniCloud::WLSPIASurface(MiniArray &u, MiniArray &v, int Nu, int Nv, int p, int q, Real tol) const { // N 个控制点,m + 1 个数据点和对应参数 int nu = Nu - 1; int nv = Nv - 1; int m = m_points.size() - 1; // 构造样条 MiniBSplineSurface *spline = new MiniBSplineSurface(nu + 1, nv + 1, p, q); MiniArray2D &P = spline->GetPoints(); const MiniArray &Q = GetPoints(); // 几何迭代法 Real e = tol * 10; int times = 0; while (e > tol && times < ITERATION) { MiniArray2D basisSum(nu + 1, MiniArray(nv + 1)); // 记录每个控制点对应的那组基函数和 MiniArray2D deltaSum(nu + 1, MiniArray(nv + 1)); // 记录每个控制点对应的那组偏差向量加权和 // 对每个数据点计算增量 for (int i = 0; i < m + 1; i++) { Vector3f delta = Q[i] - spline->SurfacePoint(u[i], v[i]); // 计算此参数对应的非零基函数值 int ju = FindSpan(u[i], spline->GetUDegree(), spline->GetUKnots()); int jv = FindSpan(v[i], spline->GetVDegree(), spline->GetVKnots()); MiniArray basisFuncU = BasisFuncs(ju, u[i], spline->GetUDegree(), spline->GetUKnots()); MiniArray basisFuncV = BasisFuncs(jv, v[i], spline->GetVDegree(), spline->GetVKnots()); // 累计参数 for (int ku = 0; ku < int(basisFuncU.size()); ku++) { for (int kv = 0; kv < int(basisFuncV.size()); kv++) { basisSum[ju - p + ku][jv - q + kv] += basisFuncU[ku] * basisFuncV[kv]; deltaSum[ju - p + ku][jv - q + kv] = deltaSum[ju - p + ku][jv - q + kv] + delta * basisFuncU[ku] * basisFuncV[kv]; } } } // 得到控制点增量的平均值并加上增量,注意由于存在空洞,不是所有控制点都可以移动 for (int i = 0; i < nu + 1; i++) { for (int j = 0; j < nv + 1; j++) { if (fabs(basisSum[i][j]) > FLT_MIN) deltaSum[i][j] = deltaSum[i][j] / basisSum[i][j]; } } // 加权矩阵规模是控制点网格的一半以上 int wu = (nu + 1) / 4 + 1; int wv = (nv + 1) / 4 + 1; int cu = wu / 2; int cv = wv / 2; Matrix weight(wu, wv); for (int i = 0; i < wu; i++) { int di = cu - std::abs(i - cu); for (int j = 0; j < wv; j++) { int dj = cv - std::abs(j - cv); // weight(i, j) = pow(2, std::min(di, dj)); weight(i, j) = pow(1.5, di + dj); } } // cout << weight << endl; for (int i = 0; i < nu + 1; i++) { for (int j = 0; j < nv + 1; j++) { Real s = 0; Vector3f delta = {0, 0, 0}; for (int ki = 0; ki < wu; ki++) { for (int kj = 0; kj < wv; kj++) { int wi = i + ki - wu / 2; int wj = j + kj - wv / 2; if (wi >= 0 && wi < nu + 1 && wj >= 0 && wj < nv + 1) { delta = delta + deltaSum[wi][wj] * weight(ki, kj); s += weight(ki, kj); } } } // 取平均值 P[i][j] = P[i][j] + delta / s; } } if (times < 10) { // 加权光滑化 for (int i = 0; i < wu; i++) { int di = cu - std::abs(i - cu); for (int j = 0; j < wv; j++) { int dj = cv - std::abs(j - cv); weight(i, j) = pow(2, std::min(di, dj)); // weight(i, j) = pow(1.2, di + dj); } } MiniArray2D Q(nu + 1, MiniArray(nv + 1)); for (int i = 0; i < nu + 1; i++) { for (int j = 0; j < nv + 1; j++) { Real s = 0; Vector3f delta = {0, 0, 0}; for (int ki = 0; ki < wu; ki++) { for (int kj = 0; kj < wv; kj++) { int wi = i + ki - wu / 2; int wj = j + kj - wv / 2; if (wi >= 0 && wi < nu + 1 && wj >= 0 && wj < nv + 1) { delta = delta + P[wi][wj] * weight(ki, kj); s += weight(ki, kj); } } } // 取平均值 Q[i][j] = delta / s; } } for (int i = 0; i < nu + 1; i++) for (int j = 0; j < nv + 1; j++) P[i][j] = Q[i][j]; } // 计算区间误差 e = 0; for (int i = 0; i < m + 1; i++) { Real err = length(spline->SurfacePoint(u[i], v[i]) - m_points[i]); e += err * err / (m + 1); } times++; } return spline; } ``` * **考虑优化 SSDE 初始化 DBS 的算法:最小化横向与纵向累计弦长函数之和,修改论文中的矩阵;**移除 SSDE 生成 DBS 的方法 ```cpp MiniBSplineSurface *MiniCloud::SSDEInitialDBS(int Nu, int Nv, int p, int q, Real tol, Real ru, Real rv) const { // 计算一个包围盒 MiniArray box = OrientBoundingBox(m_points); // N 个控制点,m + 1 个数据点和对应参数 int nu = Nu - 1; int nv = Nv - 1; int m = m_points.size() - 1; // 构造样条 MiniBSplineSurface *spline = new MiniBSplineSurface(nu + 1, nv + 1, p, q); MiniArray2D &P = spline->GetPoints(); const MiniArray &Q = GetPoints(); // 调整为右手坐标系,两个方向的先后顺序会决定光照时的法向 if (dot(cross(box[0], box[2]), box[4]) < 0) { Vector3f tx = box[0]; box[0] = box[2]; box[2] = tx; // 注意正反方向的都要交换 tx = box[1]; box[1] = box[3]; box[3] = tx; } // 稍微放大包围盒,不要放大原点位置 box[0] = box[0] * ru; box[1] = box[1] * ru; box[2] = box[2] * rv; box[3] = box[3] * rv; // 根据包围盒设置初始控制点 MiniArray oxy(3); oxy[0] = box.back() + box[1] + box[3] + box[5]; oxy[1] = normalize(box[0]); oxy[2] = normalize(box[2]); Real su = (length(box[0]) + length(box[1])) / nu; Real sv = (length(box[2]) + length(box[3])) / nv; for (int i = 0; i < nu + 1; i++) for (int j = 0; j < nv + 1; j++) P[i][j] = oxy[0] + oxy[1] * su * i + oxy[2] * sv * j; // 设置平均权重 MiniArray w(m + 1); for (int i = 0; i < m + 1; i++) w[i] = 0.5; // 加细分割曲面网格 nu *= 2, nv *= 2; LinearSolver solver; int times = 0; Real e = 0, e1 = tol * 10; Real kappa = 1e-3; while (fabs(e - e1) > tol && times < ITERATION) { // 记录新的误差 e = e1; // 构造相交网格及其参数 MiniArray u((nu + 1) * (nv + 1)); MiniArray v((nu + 1) * (nv + 1)); MiniArray2D R(nu + 1, MiniArray(nv + 1)); MiniArray2D N(nu + 1, MiniArray(nv + 1)); for (int i = 1; i < nu; i++) { for (int j = 1; j < nv; j++) { u[i * (nv + 1) + j] = 1.0 / nu * i; v[i * (nv + 1) + j] = 1.0 / nv * j; // 内部点计算法向 MiniArray2D S = spline->SurfaceDerivPoint(1, 1.0 / nu * i, 1.0 / nv * j); R[i][j] = S[0][0]; N[i][j] = normalize(cross(S[1][0], S[0][1])); } } // 外插计算边界点处的法向 // 左右边界外插 for (int i = 1; i < nu; i++) { Real t = 1.0 / nu * i; u[i * (nv + 1)] = t; v[i * (nv + 1)] = 0; R[i][0] = spline->SurfacePoint(t, 0); N[i][0] = normalize(N[i][1] * 2 - N[i][2]); u[i * (nv + 1) + nv] = t; v[i * (nv + 1) + nv] = 1; R[i][nv] = spline->SurfacePoint(t, 1); N[i][nv] = normalize(N[i][nv-1] * 2 - N[i][nv-2]); } // 上下边界外插 for (int i = 1; i < nv; i++) { Real t = 1.0 / nv * i; u[i] = 0; v[i] = t; R[0][i] = spline->SurfacePoint(0, t); N[0][i] = normalize(N[1][i] * 2 - N[2][i]); u[nu * (nv + 1) + i] = 1; v[nu * (nv + 1) + i] = t; R[nu][i] = spline->SurfacePoint(1, t); N[nu][i] = normalize(N[nu-1][i] * 2 - N[nu-2][i]); } // 四个角点外插 u[0] = 0, v[0] = 0; u[nv] = 0, v[nv] = 1; u[nu * (nv + 1)] = 1, v[nu * (nv + 1)] = 0; u[nu * (nv + 1) + nv] = 1, v[nu * (nv + 1) + nv] = 1; R[0][0] = spline->SurfacePoint(0, 0); R[0][nv] = spline->SurfacePoint(0, 1); R[nu][0] = spline->SurfacePoint(1, 0); R[nu][nv] = spline->SurfacePoint(1, 1); N[0][0] = normalize(N[0][1] * 2 - N[0][2]); N[0][nv] = normalize(N[0][nv-1] * 2 - N[0][nv-2]); N[nu][0] = normalize(N[nu-1][0] * 2 - N[nu-2][0]); N[nu][nv] = normalize(N[nu-1][nv] * 2 - N[nu-2][nv]); // 遍历每行控制点构造线性系统求解 e1 = 0; for (int i = 0; i < nu + 1; i++) { // 先计算出两个目标向量 Vector b(nv + 1); for (int j = 0; j < nv + 1; j++) { Vector c(5); // 对每个点云中的点计算误差 for (int mu = 0; mu < m + 1; mu++) { Real a = 1 / pow(length(R[i][j] - Q[mu]), 4); c[0] += a; c[1] += a * Q[mu].x(); c[2] += a * Q[mu].y(); c[3] += a * Q[mu].z(); c[4] += a * (Q[mu].x() * Q[mu].x() + Q[mu].y() * Q[mu].y() + Q[mu].z() * Q[mu].z()); } // 计算出极小值参数 Real lambda = (c[1] * N[i][j].x() + c[2] * N[i][j].y() + c[3] * N[i][j].z()) / c[0]; b[j] = lambda - dot(R[i][j], N[i][j]); } // 开始加权 b[0] *= 1 - kappa; b[nv] *= 1 - kappa; for (int j = 1; j < nv; j++) b[j] = b[j] * (1 - kappa) + dot(R[i][j - 1] - R[i][j] * 2 + R[i][j + 1], N[i][j]) * kappa; // 然后计算系数矩阵 Matrix A(nv + 1, nv + 1); A(0, 0) = kappa; A(nv, nv) = kappa; for (int j = 1; j < nv; j++) { A(j, j - 1) = -dot(N[j][j - 1], N[j][j]) * kappa; A(j, j) = 2 * kappa; A(j, j + 1) = -dot(N[j][j], N[j][j + 1]) * kappa; } A(1, 0) = A(nv - 1, nv) = 0; for (int j = 0; j < nv + 1; j++) A(j, j) += 1 - kappa; // 求解方程得到参数 t,然后修改这一行 Vector t = solver.solve(A, b, true); for (int j = 0; j < nv + 1; j++) { R[i][j] = R[i][j] + N[i][j] * t[j]; e1 += length(spline->SurfacePoint(1.0 / nu * i, 1.0 / nv * j) - R[i][j]); } } e1 /= (nu + 1) * (nv + 1); MiniCloud cloud(MiniArray(0)); MiniArray &final = cloud.GetPoints(); for (int i = 0; i < nu + 1; i++) for (int j = 0; j < nv + 1; j++) final.push_back(R[i][j]); // 计算完成后即可移除原先曲面 delete spline; // 重新拟合曲面 spline = cloud.ChordFittingSurface(u, v, Nu, Nv, p, q, w, 0, 0); times++; // 逐渐减小光滑化操作 kappa *= 1; } return spline; } ``` * 增加使用稀疏矩阵求解的 Jacobi, GS, SOR 迭代方法; * 引入 Eigen3 库到 MiniObject.h 头文件中; * 用 Eigen3 中的算法替换掉 FittingSurface 的两个方法中稀疏矩阵和 Cholesky 分解求解方程的算法; 5.4 * 修改了 B 样条曲面和参量曲面中的计算光照法向时 Su, Sv 的叉乘顺序; * 向有向包围盒算法中加入了调整坐标系的方法,使得包围盒的最短正向轴总是沿着 y 轴正向,其余坐标轴与其构成右手坐标系; * 封装了毕业论文中的边界曲线参数化算法; * 引入 OCC 的数据结构:熟悉裁剪曲面、基本的样条数据结构等; * 通过 OCC 架构实现场景中图元属性的保存和加载:为 MiniObject 构成的场景设置文件数据,能够同时将多条曲线、网格保存在一个文件中,提供读取文件的方法; * **由于加入了 Eigen 库,Test 程序无法编译,考虑使用 makefile 协调**; * **考虑将 Basic 部分的测试和 MiniObject 部分的测试分离**; * **实现对所有算法的测试程序**; * 整理当前数据结构 * 统一 MiniDiscreteCurve, MiniParaCurve, MiniBSplineCurve 的继承关系:都继承自 MiniCurve * 统一 MiniParaSurface, MiniBSplineSurface 的继承关系:都继承自 MiniSurface * 移除网格中的材质信息; * 添加菜单功能:打开和保存文件; * 增加创建 B 样条曲线曲面的菜单功能(设置中心位置、控制点数和节点数(自动均匀节点)以及是否是周期样条,曲线设置长度,曲面设置长和宽,控制点自动均匀排列,可双击曲线、曲面设置节点向量); * 封装各种不同的算法 * 完善菜单功能,并合并之前的各种算法; * 封装空间封闭曲线参数化函数; * 增加图元信息显示 * 增加坐标轴显示(虚线坐标,可以显示或隐藏) * 增加观察者坐标显示 * 增加纹理映射(斑马纹) * 优化图像数据显示形式(修改当前的数据点光照格式) * 增加框选功能 * 增加嵌入 RungeKutta 方法(允许自适应步长)* * 增加对稀疏矩阵迭代求解算法的测试;