【OpenGL小作坊】C# + OpenTK + OpenGL实现.tif点云转换成.obj模型
2026-08-21 09:57:32
0浏览
收藏
[先生们/女士们先看结果]
1.高程图片.tif记录了模型的高度信息。
2.通过转换将.tif高程图片构建成.obj模型
一.读取高程图像(.tif)信息
这里我们会用到一个库——GDAL,它在C#中可用于获取.tif像素信息。对其感兴趣的小伙伴可深入研究一下,这里我们仅探讨如何读取.tif的图片信息。
Gdal.AllRegister(); //初始化GDAL
//获取高程图像.tif的图像信息dem
using Dataset ds = Gdal.Open(".tif图片路径", Access.GA_ReadOnly);
width = ds.RasterXSize;
height = ds.RasterYSize;
Band band = ds.GetRasterBand(1);
band.GetNoDataValue(out double noData, out int hasNoData);
float[] buffer = new float[width * height];
band.ReadRaster(0, 0, width, height, buffer, width, height, 0, 0);
float[,] dem = new float[height, width];
isNoData = new bool[height, width];
for (int y = 0; y for (int x = 0; x float v = buffer[y * width + x];
bool nd = (hasNoData == 1 && Math.Abs(v - (float)noData) 0.001f) || v == 0f;
dem[y, x] = nd ? 0 : v;
isNoData[y, x] = nd;
}
}
二.根据高程图像(.tif)构建模型网格Mesh.
上一步骤中,我们获取到了高程图像(.tif)的图像信息。下面我们就利用这些高度信息来构建模型。
先前对3D模型有所了解的同学想必清楚,构建一个3D模型,其实就是集齐模型的顶点、法线、面、纹理坐标(Uv),然后将高程图像的高度信息,作为要构建模型的顶点信息。
internal class ObjMesh //自定义的模型类
{
public List Vertices { get; } = new();
public Listint, int, int)> Faces { get; } = new();
}
int rows = dem.GetLength(0);
int cols = dem.GetLength(1);
// 计算中心点偏移量
float centerX = (cols - 1) * 0.5f * xyScale;
float centerZ = -(rows - 1) * 0.5f * xyScale; // 注意:Z轴为负方向
ObjMesh mesh = new ObjMesh();
int[,] topIndexMap = new int[rows, cols];
int[,] bottomIndexMap = new int[rows, cols];
// ---------- 顶部顶点 ----------
for (int y = 0; y for (int x = 0; x if (outerNoData[y, x])
{
topIndexMap[y, x] = 0;
continue;
}
topIndexMap[y, x] = mesh.Vertices.Count + 1;
// 将坐标原点移动到中心点
float xPos = x * xyScale - centerX;
float zPos = -y * xyScale - centerZ; // 注意:Z轴为负方向
mesh.Vertices.Add(new Vector3(
xPos,
dem[y, x] * zScale,
zPos
));
}
}
// ---------- 顶部三角面 ----------
for (int y = 0; y for (int x = 0; x int v00 = topIndexMap[y, x];
int v10 = topIndexMap[y, x + step];
int v01 = topIndexMap[y + step, x];
int v11 = topIndexMap[y + step, x + step];
if (v00 == 0 || v10 == 0 || v01 == 0 || v11 == 0)
continue;
mesh.Faces.Add((v00, v01, v10));
mesh.Faces.Add((v10, v01, v11));
}
}
// ---------- 创建底部顶点映射 ----------
for (int y = 0; y for (int x = 0; x // 只有顶部有效的点才有底部顶点
if (topIndexMap[y, x] != 0)
{
Vector3 topV = mesh.Vertices[topIndexMap[y, x] - 1];
bottomIndexMap[y, x] = mesh.Vertices.Count + 1;
mesh.Vertices.Add(new Vector3(
topV.X,
bottomY,
topV.Z
));
}
}
}
// ---------- 创建侧边墙 ----------
// 垂直边(X方向)
for (int y = 0; y for (int x = 0; x int idx00 = topIndexMap[y, x];
int idx10 = topIndexMap[y, x + step];
// 如果两个顶点都存在,且至少有一个是边缘点
if (idx00 != 0 && idx10 != 0)
{
bool isEdge = IsEdgePoint(topIndexMap, x, y, rows, cols, step) ||
IsEdgePoint(topIndexMap, x + step, y, rows, cols, step);
if (isEdge)
{
int b00 = bottomIndexMap[y, x];
int b10 = bottomIndexMap[y, x + step];
mesh.Faces.Add((idx00, b00, b10));
mesh.Faces.Add((idx00, b10, idx10));
}
}
// 如果一个存在一个不存在,创建三角形连接
else if (idx00 != 0 && idx10 == 0)
{
int b00 = bottomIndexMap[y, x];
Vector3 topV = mesh.Vertices[idx00 - 1];
int b10 = mesh.Vertices.Count + 1;
// 计算新的底部顶点坐标(使用中心偏移)
float xPos = (x + step) * xyScale - centerX;
float zPos = -y * xyScale - centerZ;
mesh.Vertices.Add(new Vector3(
xPos,
bottomY,
zPos
));
mesh.Faces.Add((idx00, b00, b10));
// 更新映射
bottomIndexMap[y, x + step] = b10;
}
else if (idx00 == 0 && idx10 != 0)
{
int b10 = bottomIndexMap[y, x + step];
Vector3 topV = mesh.Vertices[idx10 - 1];
int b00 = mesh.Vertices.Count + 1;
// 计算新的底部顶点坐标(使用中心偏移)
float xPos = x * xyScale - centerX;
float zPos = -y * xyScale - centerZ;
mesh.Vertices.Add(new Vector3(
xPos,
bottomY,
zPos
));
mesh.Faces.Add((idx10, b10, b00));
// 更新映射
bottomIndexMap[y, x] = b00;
}
}
}
// 垂直边(Y方向)
for (int y = 0; y for (int x = 0; x int idx00 = topIndexMap[y, x];
int idx01 = topIndexMap[y + step, x];
if (idx00 != 0 && idx01 != 0)
{
bool isEdge = IsEdgePoint(topIndexMap, x, y, rows, cols, step) ||
IsEdgePoint(topIndexMap, x, y + step, rows, cols, step);
if (isEdge)
{
int b00 = bottomIndexMap[y, x];
int b01 = bottomIndexMap[y + step, x];
mesh.Faces.Add((idx00, b00, b01));
mesh.Faces.Add((idx00, b01, idx01));
}
}
else if (idx00 != 0 && idx01 == 0)
{
int b00 = bottomIndexMap[y, x];
Vector3 topV = mesh.Vertices[idx00 - 1];
int b01 = mesh.Vertices.Count + 1;
// 计算新的底部顶点坐标(使用中心偏移)
float xPos = x * xyScale - centerX;
float zPos = -(y + step) * xyScale - centerZ;
mesh.Vertices.Add(new Vector3(
xPos,
bottomY,
zPos
));
mesh.Faces.Add((idx00, b00, b01));
bottomIndexMap[y + step, x] = b01;
}
else if (idx00 == 0 && idx01 != 0)
{
int b01 = bottomIndexMap[y + step, x];
Vector3 topV = mesh.Vertices[idx01 - 1];
int b00 = mesh.Vertices.Count + 1;
// 计算新的底部顶点坐标(使用中心偏移)
float xPos = x * xyScale - centerX;
float zPos = -y * xyScale - centerZ;
mesh.Vertices.Add(new Vector3(
xPos,
bottomY,
zPos
));
mesh.Faces.Add((idx01, b01, b00));
bottomIndexMap[y, x] = b00;
}
}
}
// ---------- 创建底面(只覆盖有顶部顶点的地方) ----------
// 只在顶部面存在的区域创建底面三角形
for (int y = 0; y for (int x = 0; x int t00 = topIndexMap[y, x];
int t10 = topIndexMap[y, x + step];
int t01 = topIndexMap[y + step, x];
int t11 = topIndexMap[y + step, x + step];
// 如果这个网格的顶部存在(四个角都有顶部顶点),则创建对应的底面
if (t00 != 0 && t10 != 0 && t01 != 0 && t11 != 0)
{
int b00 = bottomIndexMap[y, x];
int b10 = bottomIndexMap[y, x + step];
int b01 = bottomIndexMap[y + step, x];
int b11 = bottomIndexMap[y + step, x + step];
// 底面三角面(注意法线方向向下,顶点顺序要逆时针)
mesh.Faces.Add((b00, b10, b01));
mesh.Faces.Add((b10, b11, b01));
}
}
}
// ---------- 处理孤立的底面区域 ----------
// 对于那些顶部只有一个顶点的地方,创建单个三角形的底面
for (int y = 0; y for (int x = 0; x // 如果当前点有顶部顶点,但周围四个网格中没有一个完整的顶部面
if (topIndexMap[y, x] != 0)
{
bool hasAdjacentTopFace = false;
// 检查左上网格
if (y - step >= 0 && x - step >= 0)
{
if (topIndexMap[y - step, x - step] != 0 &&
topIndexMap[y - step, x] != 0 &&
topIndexMap[y, x - step] != 0)
{
hasAdjacentTopFace = true;
}
}
// 检查右上网格
if (y - step >= 0 && x + step if (topIndexMap[y - step, x] != 0 &&
topIndexMap[y - step, x + step] != 0 &&
topIndexMap[y, x + step] != 0)
{
hasAdjacentTopFace = true;
}
}
// 检查左下网格
if (y + step = 0)
{
if (topIndexMap[y, x - step] != 0 &&
topIndexMap[y + step, x - step] != 0 &&
topIndexMap[y + step, x] != 0)
{
hasAdjacentTopFace = true;
}
}
// 检查右下网格
if (y + step if (topIndexMap[y, x + step] != 0 &&
topIndexMap[y + step, x] != 0 &&
topIndexMap[y + step, x + step] != 0)
{
hasAdjacentTopFace = true;
}
}
// 如果没有相邻的顶部面,为这个孤立的点创建一个小的三角底面
if (!hasAdjacentTopFace)
{
int b00 = bottomIndexMap[y, x];
// 创建两个相邻的虚拟底部点(使用中心偏移)
Vector3 bottomCenter = mesh.Vertices[b00 - 1];
int b01 = mesh.Vertices.Count + 1;
mesh.Vertices.Add(new Vector3(
bottomCenter.X - 0.5f * xyScale,
bottomY,
bottomCenter.Z
));
int b10 = mesh.Vertices.Count + 1;
mesh.Vertices.Add(new Vector3(
bottomCenter.X,
bottomY,
bottomCenter.Z + 0.5f * xyScale
));
mesh.Faces.Add((b00, b10, b01));
}
}
}
}
三.将模型写入.obj文件
上一步中,我们已经获得了模型的所有顶点信息,面信息。下面只要将mesh写入.obj即可。
private static void WriteObj(string path, ObjMesh mesh)
{
using StreamWriter sw = new StreamWriter(path);
sw.WriteLine("# DEM Terrain with Walls + Bottom");
foreach (var v in mesh.Vertices)
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
foreach (var f in mesh.Faces)
sw.WriteLine($"f {f.Item1} {f.Item2} {f.Item3}");
}
Python 3.14 finally 控制流警告怎么处理:异常保留与迁移检查
- 上一篇
- Python 3.14 finally 控制流警告怎么处理:异常保留与迁移检查
- 下一篇
- C# 正则表达式(4):分支与回溯引用
查看更多
最新文章
-
- 文章 · 软件教程 | 3小时前 | 接口鉴权 · postman · API测试 · OAuth 2.0 · 客户端凭据 · Postman scope OAuth 2.0 Client Credentials Access Token
- Postman OAuth 2.0 客户端凭据怎么配置:Token、Scope 与 Use Token 核对
- 176浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 最佳实践
- C#.NET ref struct 深度解析:语义、限制与最佳实践
- 279浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- C#.NET struct 全解析:什么时候该用值类型?
- 464浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- 深入理解 C#.NET record:不可变对象与值语义的现代实践
- 154浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- 一篇搞定 dotnet ef:EF Core 常用命令与实战指南
- 420浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 最佳实践
- C#.NET 索引器完全解析:语法、场景与最佳实践
- 479浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- C# 正则表达式(4):分支与回溯引用
- 340浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- C# 正则表达式(5):前瞻/后顾(Lookaround)——零宽断言做“条件校验”和“精确提取”
- 250浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- GCC命令行提示permission denied怎么办
- 129浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 第三方
- Clang配置第三方库链接教程
- 310浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | 其他
- Clang在macOS下编译报错怎么排查
- 391浏览 收藏
-
- 文章 · 软件教程 | 5小时前 | C语言
- Clang编译C语言时怎么开启调试信息
- 131浏览 收藏
查看更多
课程推荐
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
查看更多
AI推荐
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 5051次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4577次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4532次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4787次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4743次使用
查看更多
相关文章
-
- VS Code 怎么给 Go 项目配置测试任务:tasks.json 运行与结果验收
- 2026-07-09 501浏览
-
- Windows 11 如何开启 HEIF 图片支持
- 2026-05-31 501浏览
-
- TikTok用户画像与付费订阅变现方法
- 2026-05-27 501浏览
-
- 学信网学历翻译件申请方法
- 2026-05-27 501浏览
-
- Windows 11 24H2 更新失败0x80070005解决方法
- 2026-05-26 501浏览

