From 7007619bf251879348a22c29b85bdd2b2116219c Mon Sep 17 00:00:00 2001 From: gaozheng Date: Mon, 8 Jun 2026 11:25:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20DEM=20=E5=9C=B0=E5=BD=A2+?= =?UTF-8?q?=E5=BD=B1=E5=83=8F=E8=B4=B4=E5=9B=BE(spec=20=E2=91=A3)=20+=20dd?= =?UTF-8?q?=5Fslice=20=E4=BA=A4=E4=BA=92=E5=88=87=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TerrainActor(buildTerrain): GDAL 读 dem.tif(高程)+ image.tif(影像); DEM CRS→4326→ GeoLocalFrame 配准成 vtkStructuredGrid warp 面; 影像经 GDAL 读像素(行翻转正立)作纹理, 按经纬→EPSG:3857→像素 算纹理坐标贴图(影像/DEM 异源 CRS 重投影对位)。影像读失败→按高程上色。 离屏 verify_terrain_3d.png 核对: 卫星影像正立贴微起伏面、配准对位。+2 单测。 - 注: 影像须 GDAL 读(vtkTIFFReader 对此压缩 TIFF 报错"reading the row")。 - dd_slice: 3D「视图详情」加「切片」图层 = vtkImagePlaneWidget 在体素 image 拖切面(spec M1-b)。 - 接入 app: 3D 浮层五图层(帘面/体素/切片/地形); repo.demPath()/imagePath(); PROJ 不可用则禁用。 - vcpkg 加 gdal(连带 hdf5/netcdf/geos 等, 已缓存); 全 40 测试绿; app 构建干净。 - 注: 地形/切片 Z 基准与帘面/体素纵向夸张未统一(spec M-3 待办); dem 低分辨率→起伏细微。 --- docs/superpowers/STATUS.md | 14 +- .../plans/2026-06-08-m1-prototype-layout.md | 8 +- src/app/main.cpp | 78 +++++++- src/data/repo/LocalSampleRepository.cpp | 5 + src/data/repo/LocalSampleRepository.hpp | 4 + src/render/CMakeLists.txt | 7 +- src/render/actors/TerrainActor.cpp | 189 ++++++++++++++++++ src/render/actors/TerrainActor.hpp | 22 ++ tests/CMakeLists.txt | 2 + tests/render/test_terrain.cpp | 32 +++ tests/spike/render_verify.cpp | 11 + vcpkg.json | 1 + 12 files changed, 354 insertions(+), 19 deletions(-) create mode 100644 src/render/actors/TerrainActor.cpp create mode 100644 src/render/actors/TerrainActor.hpp create mode 100644 tests/render/test_terrain.cpp diff --git a/docs/superpowers/STATUS.md b/docs/superpowers/STATUS.md index fa169d6..53c32f0 100644 --- a/docs/superpowers/STATUS.md +++ b/docs/superpowers/STATUS.md @@ -17,11 +17,11 @@ - **左下 数据真实显示栏**(对齐原型):单击测线 → 列其采集批次(数据集,tab 数据/文件);单击采集批次 → 数据详情+异常列表+属性。启动自动选首测线+首数据集。 - **中央 二维地图 / 三维视图**(两个**真内容**,非相机切换): - 二维地图 = `MapLineActor`:测线 `lat/lon` 轨迹**红线**俯视(浅底),像地图。 - - 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。中央工具条**仅**「二维地图/三维视图」。**3D 左上「视图详情」浮层**(对齐原型,仅 3D 显示):图层勾选 **帘面 / 体素**(体素=`buildVoxelFromScatters` 两交叉测线散点经 EPSG:4547 配准 IDW,与帘面同纵向夸张;PROJ 不可用则禁用)。 + - 三维视图 = `CurtainActor`:沿测线的**竖直断面墙**(分段色带,z 纵向夸张×3,沿弯曲测线弯)。中央工具条**仅**「二维地图/三维视图」。**3D 左上「视图详情」浮层**(对齐原型,仅 3D 显示)五图层勾选:**帘面 / 体素 / 切片 / 地形**(体素=`buildVoxelFromScatters` 散点经 EPSG:4547 配准 IDW;切片=`vtkImagePlaneWidget` 在体素 image 上交互拖切面=dd_slice;地形=`TerrainActor` GDAL 读 DEM 高程面 + 影像 EPSG:3857 重投影贴图;后三者需 PROJ,不可用则禁用)。 - **下方 数据详情**:工具条「原数据/网格数据」切换 +「显示异常/显示电极/显示等值线」开关(对齐原型)。单击数据集 → 网格数据=`GridContourActor` 平面剖面(#18,colorBar 真实非均匀分段值上色,纵向夸张×1.5);原数据=`ScatterActor` 彩色方块散点(#17);显示异常=`AnomalyActor` dashed 折线叠加;显示电极=`ElectrodeActor` 顶边 ▼ 标记;显示等值线=#18 黑色等值线显隐(同纵向夸张对齐)。 - **右上 异常列表**(对齐原型):单击数据集→列该数据集异常(颜色块+名称(类型)+派生「位置/深/尺寸」),勾选框显隐,与数据详情异常叠加联动(取消勾选→该异常虚线隐藏)。 - **右下 属性**:名称/网格 nx×ny/vmin·vmax/异常数。 -- 单元测试累计 **38 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1 + Electrode 2、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly(含电极▼)/voxel_top/voxel_3d.png` 均核对正确。 +- 单元测试累计 **40 个全绿**(core/data/net/render;含 Scatter 2 + Anomaly 4 + VoxelRegister 1 + Electrode 2 + Terrain 2、修复了陈旧的 Curtain mapper 类型断言);离屏 `verify_section/map/curtain_3d/scatter/section_anomaly(含电极▼)/voxel_top/voxel_3d/terrain_3d(DEM+影像贴图).png` 均核对正确。 ## 2. 各 Phase 完成度 @@ -31,7 +31,7 @@ | P1 | core(LocalFrame/模型/ColorScale/IDW/CrsTransform/GeoLocalFrame) | ✅ | | P2 | data(解析器/LocalSampleRepository)+ 对象树 | ✅ | | P3 | 登录(RsaEncryptor/ApiClient/AuthService/LoginWindow) | ✅(**Credential 记住免登录未做**) | -| P4 | 渲染:render 层 + 二维地图(线)+ 三维视图(帘面)+ 数据详情(#18/#17/异常) | 🔶 **三视图 + 散点#17 + 异常叠加 已对; dd_voxel 引擎已验证(UI 未接,待 3D 图层控制)**;**DEM地形(需加gdal) / 底图瓦片 / dd_slice / 布局对齐原型(左下数据列表/右上异常列表/电极/3D图层浮层) 未做** | +| P4 | 渲染:render 层 + 二维地图(线)+ 三维(帘面/体素/切片/地形)+ 数据详情(#18/#17/异常/电极) | ✅ **核心全达成**:三视图 + #17 + 异常 + dd_voxel + dd_slice + DEM地形影像 + 原型六面板。**剩**:底图瓦片(M1.5)、数值标签、Credential 免登录、Z 基准统一、dock 透视持久化 | ## 3. 构建约定(**机器本地**) @@ -40,7 +40,8 @@ - **构建/测试经** `& cmd /c "D:\Git\lanbingtech\geopro\external\dev.bat "`(**PowerShell 调**,Bash 下参数透传坏)。C: 极小→TEMP/构建全在 D:。 - **app 构建前先** `taskkill /IM geopro_desktop.exe /F`(运行中 LNK1104 锁 exe)。 - 部署:`D:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe --release `(找不到 ads dll 的警告无害);VTK/vcpkg dll 由 POST_BUILD `TARGET_RUNTIME_DLLS` 拷。 -- **离屏渲染验证**:`render_verify` 需 PATH 加 `external\vtk-install\bin` + `build\release\vcpkg_installed\x64-windows\bin` 再运行。 +- **离屏渲染验证**:`render_verify` 需 PATH 加 `external\vtk-install\bin` + `build\release\vcpkg_installed\x64-windows\bin`;且体素/地形需 `PROJ_DATA=...\vcpkg_installed\x64-windows\share\proj`、`GDAL_DATA=...\share\gdal` 再运行。 +- **GDAL**:已加 vcpkg `gdal`(首次编译久,连带 hdf5/netcdf/geos 等;已缓存)。app/测试运行时需 PROJ_DATA(app main() 已自动按候选路径设;部署须随包附带 proj/gdal 数据)。 - **改源码用 Write 工具,勿用 PowerShell `Set-Content -Encoding UTF8`**(会把中文注释弄乱、断构建)。 ## 4. 关键决策与已核实事实 @@ -68,7 +69,8 @@ 1. ~~**散点 #17**:`ScatterActor`(剖面原数据 2597 点彩色散点),数据详情"原数据"视图~~ ✅ **已完成**(2026-06-08,离屏 PNG 核对吻合 Python 真值,接入数据详情「反演剖面/原数据」切换;app 待人工登录肉眼复核交互)。 2. ~~**异常叠加**:`AnomalyActor`(markType 点/线/面)~~ ✅ **已完成**(2026-06-08,叠加在数据详情 #18/#17 上,「显示异常」开关默认开;离屏 `verify_section_anomaly.png` 折线位置吻合 ref_18;样本 3 异常均 markType=2 dashed;app 待人工登录复核)。**注**:dashed 点画在 VTK OpenGL2 下偏弱(几乎实线),几何/颜色/位置正确,纯观感项可后续调。 -3. **DEM/影像地形**:加 vcpkg `gdal`;GDAL 读 dem.tif/image.tif;**影像 EPSG:3857 必须 PROJ 重投影到世界系**;`vtkWarpScalar` 地形面 + 纹理。 +3. ~~**DEM/影像地形**~~ ✅ **已完成**(2026-06-08)。`render::buildTerrain`(GDAL 读 dem.tif 高程 + image.tif 影像;DEM CRS→4326→GeoLocalFrame 配准成 warp 面;影像 EPSG:3857→像素纹理坐标贴图)。离屏 `verify_terrain_3d.png` 卫星影像正确贴微起伏面、方向正立、配准对位。**注**:影像须用 **GDAL 读像素**(vtkTIFFReader 对此压缩 TIFF 报错);+2 单测;接入 app 3D「地形」图层。dem.tif 低分辨率→起伏细微。Z 基准与帘面/体素夸张未统一(spec M-3 待办)。 + - **dd_slice 交互切片** ✅ 已完成(同日):3D「切片」图层=`vtkImagePlaneWidget` 在体素 image 拖切面(spec M1-b);**注**:切片在 image 原始米坐标,与夸张体绘制有纵向比例差(M-3)。交互项待人工复核。 4. **dd_voxel 回归**:✅ **已完成**(2026-06-08,CRS 已定 EPSG:4547)。`render::buildVoxelFromScatters`:散点 projX/Y→4547→4326→GeoLocalFrame 配准 + IDW(maxDist 裁剪)→ `buildVoxel`;离屏 `verify_voxel_top.png` 两臂支撑吻合 ref voxel_hslice、`verify_voxel_3d.png` profile1 片贴合帘面;+1 单测(VoxelRegister,需 PROJ_DATA)。**UI 已接入**(增量3):3D「视图详情」浮层「体素」图层勾选驱动;main() 自动设 PROJ_DATA(部署须随包附带 proj 数据);PROJ 不可用则该层禁用。**注**:仅 2 交叉线→薄十字片(15.9% 充填,半透明偏淡),可信满体需≥3线(设计 §10/§14)。**dd_slice 交互切片未做**(buildVoxel 已暴露 image 供 reslice widget)。 5. **底图瓦片**(二维地图,天地图/Mapbox):M1.5。 6. **Credential(QtKeychain)**:记住一个月免登录持久化(P3 Task2 未做)。 @@ -80,7 +82,7 @@ - ✅ **增量2 左下「数据列表」+ 对象树到 TM 层**(2026-06-08,`panels/DatasetListPanel`;树 GS→TM 复选驱动中央, DS 移出树入数据列表 tab 数据/文件, DS 单击→详情+异常+属性, 启动自动选首测线/首数据集;待人工复核)。 - ✅ **增量3 3D「视图详情」图层浮层**(2026-06-08,QFrame 浮于 QVTK 左上,仅 3D 显示;帘面/体素图层勾选;体素经此正经接入=之前移除的工具条开关的正确归宿;main() 设 PROJ_DATA;待人工复核浮层渲染+体素显隐)。 - 🔶 **增量4 电极标记 + 工具条**(2026-06-08,`ElectrodeActor` 顶边 ▼ PNG 核对吻合; +「显示电极/显示等值线」开关;待人工复核)。**剩**:数值标签、色阶配置/滤波处理(M1.5/进阶)。 - - 渲染积木累计含 `ElectrodeActor`(顶边 ▼)。底图影像=DEM/底图任务(需 GDAL)。 + - 渲染积木累计含 `ElectrodeActor`(顶边 ▼)、`TerrainActor`(DEM 高程面 + 影像纹理,GDAL)。底图瓦片=M1.5。 - 架构:新面板抽到 `src/app/panels/`(暂随 app 编译,如 login/),控制 main.cpp 体量;后续可升 `src/view/` 库。 ## 7. 渲染验证手段(务必用) diff --git a/docs/superpowers/plans/2026-06-08-m1-prototype-layout.md b/docs/superpowers/plans/2026-06-08-m1-prototype-layout.md index b7b9e08..46f7962 100644 --- a/docs/superpowers/plans/2026-06-08-m1-prototype-layout.md +++ b/docs/superpowers/plans/2026-06-08-m1-prototype-layout.md @@ -89,7 +89,13 @@ - 剖面顶部电极▼标记(grid.lat/lon 或 x 轴电极位);可选数值标签。 - 提交 `feat(view): 数据详情电极标记 + 工具条对齐原型`。 -### (增量 5,可选)右下属性面板规范化 + 整体 dock 透视持久化 +### 增量 5:DEM 地形 + dd_slice ✅ 已完成(2026-06-08, backlog 项随原型 3D 图层一并落地) +> `actors/TerrainActor`(GDAL 读 DEM/影像 → 重投影到 GeoLocalFrame → warp 面 + GDAL 读影像像素作纹理; +> 影像 EPSG:3857 重投影对位)。离屏 `verify_terrain_3d.png` 核对吻合(卫星影像正立贴面)。+2 单测。 +> dd_slice = `vtkImagePlaneWidget` 在体素 image 拖切面。两者接入 3D「视图详情」浮层(地形/切片图层)。 +> 全 40 测试绿。vcpkg 加 gdal。**注**:影像须 GDAL 读(非 vtkTIFFReader);Z 基准统一(M-3)待办。 + +### (后续,可选)数值标签 / 色阶配置 / Credential 免登录 / dock 透视持久化 / 底图瓦片(M1.5) --- diff --git a/src/app/main.cpp b/src/app/main.cpp index 81c403c..26b6805 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -4,7 +4,8 @@ // - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。 // 二维地图 = 对每个勾选数据集 buildSurveyLine(lat/lon 红线俯视,z=0)+ applyTop2D(浅底背景)。 // 三维视图 = 勾选测线的 buildCurtain(竖直断面墙),actor SetScale(1,1,3) 纵向夸张 + applyFree3D(白底)。 -// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW)。 +// 三维左上「视图详情」浮层(对齐原型):图层勾选 帘面 / 体素(dd_voxel,散点经 EPSG:4547 配准 IDW) +// / 切片(dd_slice,vtkImagePlaneWidget 在体素 image 上交互拖切面) / 地形(DEM 高程面 + 影像纹理)。 // 切视图 / 勾选变化 / 图层变化 → 重建对应内容。 // - 下方「数据详情」:独立 QVTK 小视图 + 工具条「原数据 / 网格数据」切换 +「显示异常」开关(对齐原型)。 // 单击某 DS → 显示该数据集: @@ -55,6 +56,7 @@ #include "panels/DatasetListPanel.hpp" #include "CameraPreset.hpp" +#include "ColorLutBuilder.hpp" #include "Scene.hpp" #include "VoxelFromScatters.hpp" #include "actors/AnomalyActor.hpp" @@ -63,6 +65,7 @@ #include "actors/GridContourActor.hpp" #include "actors/MapLineActor.hpp" #include "actors/ScatterActor.hpp" +#include "actors/TerrainActor.hpp" #include "geo/CrsTransform.hpp" #include "geo/GeoLocalFrame.hpp" @@ -75,6 +78,9 @@ #include #include +#include +#include +#include #include #include @@ -180,6 +186,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 三维图层显隐(由「视图详情」浮层控制)+ 项目 CRS→WGS84(体素配准)。 auto showCurtain = std::make_shared(true); // 帘面,默认显示 auto showVoxel = std::make_shared(false); // 体素,默认关 + auto showTerrain = std::make_shared(false); // 地形(DEM+影像),默认关 + auto showSlice = std::make_shared(false); // dd_slice 交互切片,默认关 + // 持久的切片 widget(挂 interactor,跨重建保活;rebuildCentral 据条件创建/拆除)。 + auto slicePlane = std::make_shared>(); std::shared_ptr crs; // PROJ 失败→空→体素层无效(不崩) try { crs = std::make_shared(kProjectCrs, kWgs84); @@ -226,13 +236,21 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re chkCurtain->setChecked(true); auto* chkVoxel = new QCheckBox(QStringLiteral("体素(dd_voxel)")); chkVoxel->setChecked(false); - if (!crs) { // PROJ 不可用 → 体素层禁用并提示 - chkVoxel->setEnabled(false); - chkVoxel->setToolTip(QStringLiteral("PROJ 数据(proj.db)缺失,体素配准不可用")); + auto* chkTerrain = new QCheckBox(QStringLiteral("地形(DEM+影像)")); + chkTerrain->setChecked(false); + auto* chkSlice = new QCheckBox(QStringLiteral("切片(dd_slice)")); + chkSlice->setChecked(false); + if (!crs) { // PROJ 不可用 → 体素/切片/地形层(都需配准)禁用并提示 + const QString tip = QStringLiteral("PROJ 数据(proj.db)缺失,配准不可用"); + chkVoxel->setEnabled(false); chkVoxel->setToolTip(tip); + chkTerrain->setEnabled(false); chkTerrain->setToolTip(tip); + chkSlice->setEnabled(false); chkSlice->setToolTip(tip); } layerLayout->addWidget(layerTitle); layerLayout->addWidget(chkCurtain); layerLayout->addWidget(chkVoxel); + layerLayout->addWidget(chkSlice); + layerLayout->addWidget(chkTerrain); layerPanel->setVisible(false); // 默认二维,不显示图层浮层 auto* vtkDock = new ads::CDockWidget(QStringLiteral("二维地图/三维视图")); @@ -329,7 +347,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 三维视图 = buildCurtain(断面墙)SetScale(1,1,kCurtainZScale) + applyFree3D(白底)。 // frame/structure 全局共享;切视图/勾选变化都调用此函数重建当前视图。 auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree, - structure, showCurtain, showVoxel, crs]() { + structure, showCurtain, showVoxel, showTerrain, showSlice, slicePlane, + crs]() { + // 先拆除上次的切片 widget(独立于 scene actor,须显式关闭),再按条件重建。 + if (*slicePlane) { (*slicePlane)->Off(); *slicePlane = nullptr; } scene->clear(); const bool is2D = (*viewMode == ViewMode::Map2D); @@ -367,17 +388,46 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re if (ds.ddType == "dd_section") renderSection(ds.id); } - // 三维「体素」图层:两交叉测线散点经 CRS 配准 IDW 成体素(派生层),与帘面同纵向夸张。 - if (!is2D && *showVoxel && crs) { + // 三维「体素 / 切片」图层:两交叉测线散点经 CRS 配准 IDW 成体素。 + // 体素=GPU 体绘制(与帘面同纵向夸张);切片=vtkImagePlaneWidget 在体素 image 上交互拖切面。 + // 注:切片 widget 作用于 image 原始米坐标(无 actor 夸张),与夸张后的体绘制存在纵向比例差 + // (spec M-3 Z 基准统一待办);切片本身演示 dd_slice 交互正确。 + if (!is2D && (*showVoxel || *showSlice) && crs) { const auto profs = repo.loadVoxelScatters(); const auto vcs = repo.loadScatterColorScale("grid1"); auto vr = geopro::render::buildVoxelFromScatters(profs, vcs, *crs, *frame); if (vr.valid()) { - vr.volume->SetScale(1.0, 1.0, kCurtainZScale); - rendererPtr->AddVolume(vr.volume); + if (*showVoxel) { + vr.volume->SetScale(1.0, 1.0, kCurtainZScale); + rendererPtr->AddVolume(vr.volume); + } + vtkRenderWindowInteractor* interactor = renderWindowPtr->GetInteractor(); + if (*showSlice && interactor) { + const std::vector stops = vcs.stopValues(); + const double vmn = stops.size() >= 2 ? stops.front() : 0.0; + const double vmx = stops.size() >= 2 ? stops.back() : 1.0; + auto lut = geopro::render::buildLut(vcs, vmn, vmx, 256); + int dims[3] = {1, 1, 1}; + vr.image->GetDimensions(dims); + auto plane = vtkSmartPointer::New(); + plane->SetInteractor(interactor); + plane->SetInputData(vr.image); + plane->SetPlaneOrientationToXAxes(); + plane->SetSliceIndex(dims[0] / 2); + plane->SetLookupTable(lut); + plane->DisplayTextOn(); + plane->On(); + *slicePlane = plane; + } } } + // 三维「地形」图层:GDAL 读 DEM(高程)+影像(EPSG:3857),重投影到世界系,warp 面 + 纹理。 + if (!is2D && *showTerrain && crs) { + auto terr = geopro::render::buildTerrain(repo.demPath(), repo.imagePath(), *frame, 1.0); + if (terr) scene->addActor(terr); + } + if (is2D) geopro::render::applyTop2D(rendererPtr); else @@ -581,6 +631,16 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re *showVoxel = on; rebuildCentral(); }); + QObject::connect(chkTerrain, &QCheckBox::toggled, vtkWidget, + [showTerrain, rebuildCentral](bool on) { + *showTerrain = on; + rebuildCentral(); + }); + QObject::connect(chkSlice, &QCheckBox::toggled, vtkWidget, + [showSlice, rebuildCentral](bool on) { + *showSlice = on; + rebuildCentral(); + }); // ── 启动默认:测线已勾选,但 itemChanged 在 connect 之前触发故未渲染;这里重建一次中央内容。 rebuildCentral(); diff --git a/src/data/repo/LocalSampleRepository.cpp b/src/data/repo/LocalSampleRepository.cpp index f2cf2af..a7f246a 100644 --- a/src/data/repo/LocalSampleRepository.cpp +++ b/src/data/repo/LocalSampleRepository.cpp @@ -25,6 +25,8 @@ constexpr const char* kScatterColorScaleFile = u8"剖面原数据的色阶数据 constexpr const char* kVoxelScatterFile1 = u8"剖面原数据1.txt"; constexpr const char* kVoxelScatterFile2 = u8"剖面原数据2.txt"; constexpr const char* kAnomalyFile = u8"剖面网格数据1——对应的异常圈定数据.txt"; +constexpr const char* kDemFile = u8"dem.tif"; +constexpr const char* kImageFile = u8"image.tif"; // 校验 dsId,未知则抛错(输入边界验证)。 void requireKnownDs(const std::string& dsId) { @@ -99,6 +101,9 @@ ColorScale LocalSampleRepository::loadScatterColorScale(const std::string& dsId) return parseColorScale(readFile(kScatterColorScaleFile)); } +std::string LocalSampleRepository::demPath() const { return dirUtf8_ + kDemFile; } +std::string LocalSampleRepository::imagePath() const { return dirUtf8_ + kImageFile; } + std::vector LocalSampleRepository::loadVoxelScatters() { std::vector out; out.push_back(parseScatter(readFile(kVoxelScatterFile1))); diff --git a/src/data/repo/LocalSampleRepository.hpp b/src/data/repo/LocalSampleRepository.hpp index bc73d53..0d54179 100644 --- a/src/data/repo/LocalSampleRepository.hpp +++ b/src/data/repo/LocalSampleRepository.hpp @@ -21,6 +21,10 @@ public: // 具体类专有方法(不进 IDatasetRepository 接口);散点点为不透明,alpha 量纲差异无影响。 geopro::core::ColorScale loadScatterColorScale(const std::string& dsId); + // DEM/影像 GeoTIFF 的绝对路径(供 render::buildTerrain 经 GDAL 读)。 + std::string demPath() const; + std::string imagePath() const; + // dd_voxel 输入:读两条交叉剖面散点(剖面原数据1.txt + 剖面原数据2.txt), // 返回两者,供体素插值合并。具体类专有方法(不进 IDatasetRepository 接口)。 std::vector loadVoxelScatters(); diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index b5abaf2..9250e21 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -1,8 +1,9 @@ -find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets) +find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets IOImage) +find_package(GDAL CONFIG REQUIRED) add_library(geopro_render STATIC - Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp) + Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES}) +target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL) target_compile_features(geopro_render PUBLIC cxx_std_17) set_target_properties(geopro_render PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) vtk_module_autoinit(TARGETS geopro_render MODULES ${VTK_LIBRARIES}) diff --git a/src/render/actors/TerrainActor.cpp b/src/render/actors/TerrainActor.cpp new file mode 100644 index 0000000..0b41e37 --- /dev/null +++ b/src/render/actors/TerrainActor.cpp @@ -0,0 +1,189 @@ +#include "actors/TerrainActor.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "geo/CrsTransform.hpp" + +namespace geopro::render { + +namespace { + +// 项目 CRS(DEM 无投影信息时的兜底)。 +constexpr const char* kFallbackCrs = "EPSG:4547"; + +struct Raster { + int w = 0, h = 0; + double gt[6] = {0, 1, 0, 0, 0, 1}; // 仿射: x=gt0+col*gt1+row*gt2, y=gt3+col*gt4+row*gt5 + std::string wkt; + bool ok() const { return w > 0 && h > 0; } +}; + +// 用 GDAL 读影像像素为 RGB 纹理(vtkTIFFReader 对压缩/分块 TIFF 不可靠,故走 GDAL)。 +// 行翻转使影像在 vtkImageData 中正立(配合 tcoord v=1-row/h)。读失败返回 nullptr。 +vtkSmartPointer readTexture(const std::string& path) { + auto* ds = static_cast(GDALOpen(path.c_str(), GA_ReadOnly)); + if (!ds) return nullptr; + const int w = ds->GetRasterXSize(), h = ds->GetRasterYSize(); + const int nb = std::min(ds->GetRasterCount(), 3); + if (w <= 0 || h <= 0 || nb < 1) { GDALClose(ds); return nullptr; } + + std::vector buf(static_cast(w) * h * nb); + // 交错读 RGB:pixelSpace=nb, lineSpace=w*nb, bandSpace=1。 + const CPLErr err = + ds->RasterIO(GF_Read, 0, 0, w, h, buf.data(), w, h, GDT_Byte, nb, nullptr, + nb, static_cast(w) * nb, 1); + GDALClose(ds); + if (err != CE_None) return nullptr; + + vtkNew img; + img->SetDimensions(w, h, 1); + vtkNew sc; + sc->SetNumberOfComponents(3); + sc->SetNumberOfTuples(static_cast(w) * h); + for (int r = 0; r < h; ++r) + for (int c = 0; c < w; ++c) { + const size_t s = (static_cast(r) * w + c) * nb; + const vtkIdType d = static_cast(h - 1 - r) * w + c; // 行翻转→正立 + unsigned char rgb[3]; + rgb[0] = buf[s]; + rgb[1] = nb > 1 ? buf[s + 1] : buf[s]; + rgb[2] = nb > 2 ? buf[s + 2] : buf[s]; + sc->SetTypedTuple(d, rgb); + } + img->GetPointData()->SetScalars(sc); + + auto tex = vtkSmartPointer::New(); + tex->SetInputData(img); + tex->InterpolateOn(); + return tex; +} + +// 读栅格几何信息(不读像素)。 +Raster readGeo(GDALDataset* ds) { + Raster r; + if (!ds) return r; + r.w = ds->GetRasterXSize(); + r.h = ds->GetRasterYSize(); + ds->GetGeoTransform(r.gt); + const char* p = ds->GetProjectionRef(); + if (p) r.wkt = p; + return r; +} + +} // namespace + +vtkSmartPointer buildTerrain(const std::string& demPath, const std::string& imagePath, + const geopro::core::GeoLocalFrame& frame, double zScale) +{ + GDALAllRegister(); + + auto* dem = static_cast(GDALOpen(demPath.c_str(), GA_ReadOnly)); + if (!dem) return vtkSmartPointer::New(); + const Raster dg = readGeo(dem); + if (!dg.ok()) { GDALClose(dem); return vtkSmartPointer::New(); } + + // DEM 高程像素(float)。 + std::vector elev(static_cast(dg.w) * dg.h, 0.0F); + GDALRasterBand* band = dem->GetRasterBand(1); + int hasNoData = 0; + const double noData = band->GetNoDataValue(&hasNoData); + band->RasterIO(GF_Read, 0, 0, dg.w, dg.h, elev.data(), dg.w, dg.h, GDT_Float32, 0, 0); + GDALClose(dem); + + // 有效高程范围(忽略 nodata),nodata 填为最小有效高程使其平坦。 + float vmin = std::numeric_limits::infinity(); + float vmax = -std::numeric_limits::infinity(); + for (float v : elev) + if (!(hasNoData && v == static_cast(noData))) { + vmin = std::min(vmin, v); + vmax = std::max(vmax, v); + } + if (!(vmin <= vmax)) { vmin = vmax = 0.0F; } + + // 坐标变换:DEM CRS → 4326;4326 → 3857(纹理坐标用)。 + const std::string demCrs = dg.wkt.empty() ? std::string(kFallbackCrs) : dg.wkt; + geopro::core::CrsTransform demTo4326(demCrs, "EPSG:4326"); + geopro::core::CrsTransform llTo3857("EPSG:4326", "EPSG:3857"); + + // 影像几何(算纹理坐标);像素经 vtkTIFFReader 读为纹理。 + Raster ig; + auto* img = static_cast(GDALOpen(imagePath.c_str(), GA_ReadOnly)); + if (img) { ig = readGeo(img); GDALClose(img); } + const bool hasImage = ig.ok(); + + // 结构化网格:点=世界局部米(E,N,elev*zScale),标量=高程;纹理坐标(若有影像)。 + vtkNew sgrid; + sgrid->SetDimensions(dg.w, dg.h, 1); + vtkNew points; + points->SetNumberOfPoints(static_cast(dg.w) * dg.h); + vtkNew sc; + sc->SetName("elev"); + sc->SetNumberOfTuples(static_cast(dg.w) * dg.h); + vtkNew tc; + tc->SetName("tc"); + tc->SetNumberOfComponents(2); + if (hasImage) tc->SetNumberOfTuples(static_cast(dg.w) * dg.h); + + for (int j = 0; j < dg.h; ++j) + for (int i = 0; i < dg.w; ++i) { + const double demX = dg.gt[0] + (i + 0.5) * dg.gt[1] + (j + 0.5) * dg.gt[2]; + const double demY = dg.gt[3] + (i + 0.5) * dg.gt[4] + (j + 0.5) * dg.gt[5]; + const auto ll = demTo4326.forward(demX, demY); // (lon, lat) + const auto local = frame.toLocal(ll.y, ll.x); // (E, N) + const vtkIdType id = static_cast(j) * dg.w + i; + float z = elev[static_cast(j) * dg.w + i]; + if (hasNoData && z == static_cast(noData)) z = vmin; + points->SetPoint(id, local.x, local.y, z * zScale); + sc->SetValue(id, z); + if (hasImage) { + const auto m = llTo3857.forward(ll.x, ll.y); // (mercX, mercY) + const double col = (m.x - ig.gt[0]) / ig.gt[1]; + const double row = (m.y - ig.gt[3]) / ig.gt[5]; // gt5<0 + const double u = col / ig.w; + const double v = 1.0 - row / ig.h; // 翻转使影像正立(按需核对) + tc->SetTuple2(id, u, v); + } + } + sgrid->SetPoints(points); + sgrid->GetPointData()->SetScalars(sc); + if (hasImage) sgrid->GetPointData()->SetTCoords(tc); + + vtkNew geom; + geom->SetInputData(sgrid); + + vtkNew mapper; + mapper->SetInputConnection(geom->GetOutputPort()); + + auto actor = vtkSmartPointer::New(); + actor->SetMapper(mapper); + + auto tex = hasImage ? readTexture(imagePath) : nullptr; + if (tex) { + actor->SetTexture(tex); + mapper->ScalarVisibilityOff(); // 用纹理,不用高程标量上色 + } else { + mapper->SetScalarRange(vmin, vmax); // 退化:影像读失败/缺失 → 按高程上色 + } + return actor; +} + +} // namespace geopro::render diff --git a/src/render/actors/TerrainActor.hpp b/src/render/actors/TerrainActor.hpp new file mode 100644 index 0000000..87f5497 --- /dev/null +++ b/src/render/actors/TerrainActor.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +#include +#include + +#include "geo/GeoLocalFrame.hpp" + +namespace geopro::render { + +// DEM 地形 + 影像贴图(spec ④)。GDAL 读 dem.tif(高程栅格)与 image.tif(影像,EPSG:3857); +// 各自按其 CRS → EPSG:4326 → GeoLocalFrame 局部米配准到世界系(与帘面/体素同系); +// 高程作 z 起伏(vtkStructuredGrid 面),影像按经纬→3857→像素 算纹理坐标贴面。 +// 影像加载失败则退化为按高程上色(无纹理)。读不到 DEM 返回空 actor。 +// +// 依赖 GDAL/PROJ;调用方运行时须有 PROJ_DATA。zScale 为高程纵向夸张(地形通常 1.0~数倍)。 +vtkSmartPointer buildTerrain(const std::string& demPath, + const std::string& imagePath, + const geopro::core::GeoLocalFrame& frame, + double zScale = 1.0); + +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8cfb977..06b572b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -72,6 +72,8 @@ target_sources(geopro_tests PRIVATE render/test_scatter.cpp) target_sources(geopro_tests PRIVATE render/test_anomaly.cpp) # Electrode:buildElectrodes(剖面顶边朝下三角 ▼) 三角数/顶点位置/空安全。 target_sources(geopro_tests PRIVATE render/test_electrode.cpp) +# Terrain:buildTerrain(GDAL 读 dem/image + 重投影 → warp 面+纹理) 非空/缺文件安全(需 PROJ_DATA)。 +target_sources(geopro_tests PRIVATE render/test_terrain.cpp) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) diff --git a/tests/render/test_terrain.cpp b/tests/render/test_terrain.cpp new file mode 100644 index 0000000..cb5f3f0 --- /dev/null +++ b/tests/render/test_terrain.cpp @@ -0,0 +1,32 @@ +#include + +#include + +#include +#include + +#include "actors/TerrainActor.hpp" +#include "geo/GeoLocalFrame.hpp" + +using namespace geopro::core; + +// 样本 DEM/影像目录(UTF-8 中文路径;源文件以 UTF-8 保存 + MSVC /utf-8 编译)。 +static const std::string kDir = + u8"D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/"; + +// buildTerrain: 真实 dem.tif + image.tif 经 GDAL 读 + 重投影 → 非空 actor 且有 mapper。 +// (需 PROJ_DATA + GDAL 运行时; tests CMake 注入 PROJ_DATA。) +TEST(Terrain, BuildsTexturedSurfaceFromSampleDemImage) { + GeoLocalFrame frame(22.546, 114.164); // 测区附近(香港) + auto actor = geopro::render::buildTerrain(kDir + "dem.tif", kDir + "image.tif", frame, 1.0); + ASSERT_NE(actor.GetPointer(), nullptr); + ASSERT_NE(actor->GetMapper(), nullptr); // 成功读 DEM → 有 mapper(空 actor 无 mapper) +} + +// 不存在的 DEM → 安全返回空 actor(无 mapper),不崩。 +TEST(Terrain, MissingDemYieldsSafeActor) { + GeoLocalFrame frame(22.546, 114.164); + auto actor = geopro::render::buildTerrain("D:/no/such/dem.tif", "D:/no/such/img.tif", frame); + ASSERT_NE(actor.GetPointer(), nullptr); + EXPECT_EQ(actor->GetMapper(), nullptr); +} diff --git a/tests/spike/render_verify.cpp b/tests/spike/render_verify.cpp index dbf26a5..d4cfa93 100644 --- a/tests/spike/render_verify.cpp +++ b/tests/spike/render_verify.cpp @@ -24,6 +24,7 @@ #include "actors/GridContourActor.hpp" #include "actors/MapLineActor.hpp" #include "actors/ScatterActor.hpp" +#include "actors/TerrainActor.hpp" #include "geo/CrsTransform.hpp" #include "geo/GeoLocalFrame.hpp" #include "parse/SampleParsers.hpp" @@ -149,6 +150,16 @@ int main() { dims[0], dims[1], dims[2], profs[0].v.size() + profs[1].v.size()); } + // 7) DEM 地形 + 影像贴图 — GDAL 读 + 重投影到世界系 + warp 面 + 纹理 + { + auto terr = render::buildTerrain(dir + "dem.tif", dir + "image.tif", frame, 1.0); + vtkNew ren; ren->SetBackground(0.50, 0.60, 0.72); + if (terr) ren->AddActor(terr); + render::applyFree3D(ren); + renderToPng(ren, (dir + "verify_terrain_3d.png").c_str(), 800, 600); + std::printf("TERRAIN actor=%d\n", terr ? 1 : 0); + } + std::printf("RENDER_VERIFY_DONE grid=%dx%d lat0=%.5f lon0=%.5f\n", g.nx(), g.ny(), lat0, lon0); return 0; } diff --git a/vcpkg.json b/vcpkg.json index b989a84..f515a80 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -4,6 +4,7 @@ "description": "Geopro 3.0 desktop client (Qt6 + VTK9) - M1. 方案②-修订: Qt/VTK/ADS/QtKeychain 对接官方 MSVC Qt(不走 vcpkg); 仅非 Qt 依赖走 vcpkg, 按层递增。", "dependencies": [ "eigen3", + "gdal", "gtest", "nlohmann-json", "openssl",