简介:这份资源面向计算几何、GIS与图形学方向的开发者,提供一套基于VC 6.0实现的快速TIN三角网生成算法工程,重点解决离散点集构网效率与Delaunay三角剖分实现问题,适合具备一定C++基础、需要研究三角网构建原理或将其嵌入自有项目的中高级读者参考。压缩包共88个文件,约769KB,以cpp与h源码为主体,涵盖顶点、边、三角形、节点及三角网等核心类,并附有obj编译产物、exe可执行程序、res资源文件与dsp、dsw工程配置,可直接用VC 6.0打开编译调试。资源已有1320人学习下载,具备一定参考热度。工程目录按文档、视图、对话框与几何算法类分层组织,读者可从中获取完整的Delaunay构网实现思路、类结构设计与可运行示例,便于对照理解三角剖分的建网流程与数据结构组织方式,并在此基础上做二次开发或性能验证。
1. 从一份 VC6 工程说起:TIN 三角网生成到底能跑多快
前阵子帮一个做地形建模的朋友看代码,他扔过来一个 VC6.0 的工程包,说这是他在某实验室资料盘里翻出来的老物件,标题写着“最快的 TIN 三角网生成算法”。我第一反应是:VC6 时代的代码,能跑吗?结果把工程拉起来编译,喂进去几千个散点,三角网几乎是瞬间铺满屏幕,比我手头那套基于通用三角剖分的实现快出一大截。这件事挺反直觉——很多人默认老代码就是慢、就是过时,但 TIN(不规则三角网)这类几何算法,性能瓶颈往往不在语言和编译器,而在剖分策略和数据结构本身。这份资源适合两类人:一是做地形、点云、测绘相关二次开发,需要把散点快速连成三角网的开发者;二是想研究经典三角剖分实现、拿它当算法课参考的工程党。下面我按“它是什么、怎么跑、核心怎么实现、怎么改”的顺序拆一遍。
2. 工程结构与技术选型:VC6 工程里到底装了什么
2.1 目录与文件构成
拿到包先别急着双击 .dsw,先看目录。这类 VC6 几何算法工程通常长这样,我按常见布局给你梳理一遍,实际文件名可能略有出入,但角色是对应的:
TIN_Project/ ├── TIN.dsw # VC6 工作区文件,双击入口 ├── TIN.dsp # 工程文件,记录源文件和编译选项 ├── main.cpp # 控制台入口,读点、调算法、输出结果 ├── tin.h / tin.cpp # 三角网核心:剖分、拓扑维护 ├── point.h # 点结构、比较器、几何谓词 ├── io.h / io.cpp # 点集读写,常见是文本格式 ├── data/ │ └── sample.pts # 示例散点,每行 x y [z] └── Debug/ Release/ # 编译产物目录核心就三个文件:point.h定义点和几何判断,tin.cpp是剖分主体,main.cpp负责串流程。VC6 工程的好处是依赖极少,基本只用标准 C++ 和少量 Windows 控制台 API,不需要第三方库,这也是它能在老机器上直接跑起来的原因。坏处也明显:MFC 痕迹、#include <iostream.h>这种老写法、char*满天飞,用现代编译器直接编大概率报一堆错。
2.2 为什么是 VC6,以及怎么在现代环境跑
VC6 是那个年代 Windows 下最顺手的 C++ 工具链,工程里常见的写法包括iostream.h、void main()、for(int i=0;...)变量作用域问题。想在新机器上跑,有两条路:
第一条,装个 VC6 虚拟机或兼容环境,原样编译,最省心,适合只想看效果的人。第二条,迁到现代工具链,我一般这么干:
# 用 g++ 先探一遍编译错误,把老写法逐个替换 g++ -std=c++11 main.cpp tin.cpp io.cpp -o tin_demo常见要改的点:#include <iostream.h>改成#include <iostream>加using namespace std;;void main()改int main()并return 0;;for循环里重复定义变量改成循环外声明或直接升 C++11。改完能过编译,算法逻辑一行都不用动,因为核心是纯计算,不依赖 MFC 界面。
提示:如果工程里带了 MFC 对话框做可视化,迁移成本会高不少。先确认
main.cpp是不是控制台入口,是的话优先走控制台路线,把算法和界面解耦。
2.3 输入输出约定
这类工程的数据入口通常是纯文本,一行一个点,空格或制表符分隔,x y或x y z。读点在io.cpp里,常见实现是fscanf或ifstream逐行解析。输出一般是三角面索引列表,每行三个点号,或者直接写.tri文件给别的工具渲染。搞清楚这个约定,你才能把自己的数据喂进去——这是二次开发第一步,别小看。
3. 核心算法拆解:散点是怎么被连成三角网的
3.1 整体剖分思路
TIN 生成的经典路线有几种:逐点插入的 Delaunay 三角剖分、分治递归、扫描线。标题强调“最快”,从工程结构看,这类实现多半走的是逐点插入 + 局部优化的路子,因为它在实现复杂度和实际速度之间平衡得最好,配合好的点定位结构,几千到几万个点都能压到毫秒级。核心循环长这样:
// tin.cpp 核心流程(示意,按常见实现整理) void TIN::build(const std::vector<Point>& pts) { initSuperTriangle(pts); // 建一个包住所有点的大三角形 for (size_t i = 0; i < pts.size(); ++i) { Triangle* t = locate(pts[i]); // 定位该点落在哪个三角形内 splitTriangle(t, pts[i]); // 一分为三 legalize(pts[i]); // 翻边,恢复 Delaunay 性质 } removeSuperTriangle(); // 删掉辅助三角形 }逻辑说明:initSuperTriangle造一个足够大的辅助三角形,保证所有散点都在它内部,这样插入时不用处理边界特判;locate找目标三角形,是性能关键;splitTriangle把三角形拆成三个;legalize做空外接圆检测,不满足就翻边。参数上,pts是输入点集,顺序会影响插入效率,常见做法是先按 x 排序再插,减少定位时的回溯。
3.2 点定位与翻边优化
locate如果每次从头遍历所有三角形,复杂度直接爆炸。合格实现一般用方向行走:从上一个插入点所在的三角形出发,沿着目标点方向逐个跨边走过去,平均几步就到位。翻边部分用栈维护待检查的边对:
// legalize:用栈做局部 Delaunay 修复 void TIN::legalize(Point* p) { std::stack<Edge*> stk; pushEdgesAround(p, stk); // 把新点周围的边压栈 while (!stk.empty()) { Edge* e = stk.top(); stk.pop(); if (!inCircle(e->tri1, e->tri2)) continue; // 空圆检测通过 flip(e); // 翻边 pushEdgesAround(e->a, stk); // 新边继续检查 pushEdgesAround(e->b, stk); } }inCircle是几何谓词,判断第四点是否落在三角形外接圆内,浮点误差在这里最容易翻车,后面避坑章细说。flip交换两个相邻三角形的公共边,保持拓扑一致。这套“插入—分裂—翻边”的组合,就是它快的根本原因:每步只动局部,不全局重算。
3.3 数据结构与内存布局
老工程常见用指针串三角形,Triangle里存三个Point*和三个邻接Triangle*。这种写法直观,但缓存不友好。想再快,可以把三角形存进连续数组,用整型索引代替指针,翻边时只改索引。这是二次开发里性价比最高的优化之一,改动量不大,速度能再提一截。
4. 跑通与验证:从示例点到自己的数据
4.1 编译运行的最小步骤
按第 2 章改完老写法后,编译命令和运行方式如下:
g++ -O2 -std=c++11 main.cpp tin.cpp io.cpp -o tin_demo ./tin_demo data/sample.pts out.tri-O2一定要加,几何算法对循环优化很敏感。main.cpp里通常硬编码了输入输出路径,跑之前先看一眼,或者改成命令行参数。跑完检查out.tri行数,正常应接近2n - 2 - h(n 为点数,h 为凸包点数),差太多说明剖分有问题。
4.2 用可视化验证结果
光看索引文件没感觉,我一般写个几十行的 Python 脚本把三角面画出来:
import matplotlib.pyplot as plt pts, tris = [], [] with open("data/sample.pts") as f: for line in f: x, y = map(float, line.split()[:2]); pts.append((x, y)) with open("out.tri") as f: for line in f: a, b, c = map(int, line.split()[:3]); tris.append((a, b, c)) for a, b, c in tris: xs = [pts[a][0], pts[b][0], pts[c][0], pts[a][0]] ys = [pts[a][1], pts[b][1], pts[c][1], pts[a][1]] plt.plot(xs, ys, 'b-', lw=0.5) plt.scatter(*zip(*pts), s=2, c='r') plt.show()这段脚本只依赖 matplotlib,把点和三角边叠在一起看,有没有空洞、有没有交叉边一目了然。验证通过再换自己的数据,别一上来就喂几万个点,先小样本确认流程对。
4.3 性能怎么量
计时用clock()或std::chrono,在build前后各打一次。测的时候注意:点集顺序、是否含重复点、坐标量级都会影响结果。我习惯准备三组数据——随机均匀点、聚簇点、近似共线点,分别看耗时和正确性,后者最容易暴露几何谓词的精度问题。
5. 避坑与排查:几何算法最容易翻车的几个地方
5.1 现象:剖分结果出现交叉边或空洞
原因:浮点精度导致inCircle判断错误,翻边翻错方向,拓扑被破坏。解决:把几何谓词改成自适应精度版本,或者对坐标做归一化,把量级压到 1 附近再算。血泪经验是,坐标动辄几十万的地形数据,不归一化几乎必翻车。
5.2 现象:程序在某个点插入时卡死或越界
原因:点恰好落在已有边上或顶点上,locate的行走逻辑没处理退化情况,死循环。解决:插入前做去重和共线检测,locate里加步数上限保护,超限就回退到暴力查找。
5.3 现象:现代编译器报一堆错,改完还链接失败
原因:VC6 的iostream.h、隐式int、for作用域等老写法。解决:按第 2 章逐项替换,链接失败多半是漏了某个.cpp没加进编译命令,对照.dsp里的源文件列表补全。
5.4 现象:结果对但特别慢
原因:locate退化成全局遍历,或者翻边栈里重复压边。解决:确认行走定位生效,翻边时对边做标记避免重复入栈;再不行就上第 3.3 节的数组化改造。
6. 二次开发:把它改成你能用的模块
拿到这种老工程,直接改不如先包一层。我的习惯是把tin.cpp的build抽成一个纯函数接口,输入点数组、输出三角形索引数组,跟文件 IO 彻底解耦,这样能塞进任何宿主程序:
// 对外接口:不依赖全局状态,方便嵌入 bool buildTIN(const std::vector<double>& xy, // 扁平坐标 x0,y0,x1,y1... std::vector<int>& outTris); // 每三个一组索引改造时优先做三件事:一是把指针拓扑换成索引数组,缓存友好且好序列化;二是给几何谓词加容差参数,不同数据量级调不同阈值;三是加一个凸包预处理,把外围点先固定,减少辅助三角形的干扰。验证方法很直接——同一组数据,改造前后三角面数量和面积总和应一致,差异超过千分之一就说明拓扑变了。
还有一个具体技巧:如果你的场景是地形,点带高程 z,别在三维里直接剖分,先投影到 xy 平面生成 TIN,再把 z 插值回去,速度快一个量级且结果稳定。我早年图省事直接在三维做,翻边判断写错一个符号,调了两天才发现是投影没做对。从那以后我每次接几何算法工程,都强制先用小样本加可视化跑一遍再上真实数据。希望帮到你。
本文还有配套的精品资源,点击获取