feat(render): DEM 地形+影像贴图(spec ④) + dd_slice 交互切片

- 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 低分辨率→起伏细微。
This commit is contained in:
gaozheng 2026-06-08 11:25:45 +08:00
parent 8466fe3a5a
commit 7007619bf2
12 changed files with 354 additions and 19 deletions

View File

@ -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 <cmd>"`(**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 <exe>`(找不到 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. 渲染验证手段(务必用)

View File

@ -89,7 +89,13 @@
- 剖面顶部电极▼标记grid.lat/lon 或 x 轴电极位);可选数值标签。
- 提交 `feat(view): 数据详情电极标记 + 工具条对齐原型`
### (增量 5可选右下属性面板规范化 + 整体 dock 透视持久化
### 增量 5DEM 地形 + 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)
---

View File

@ -4,7 +4,8 @@
// - 中央「二维地图 / 三维视图」:两个互斥视图(内容不同,不是同一物体换相机)。
// 二维地图 = 对每个勾选数据集 buildSurveyLinelat/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 <QVTKOpenGLStereoWidget.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImagePlaneWidget.h>
#include <vtkLookupTable.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
@ -180,6 +186,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 三维图层显隐(由「视图详情」浮层控制)+ 项目 CRS→WGS84(体素配准)。
auto showCurtain = std::make_shared<bool>(true); // 帘面,默认显示
auto showVoxel = std::make_shared<bool>(false); // 体素,默认关
auto showTerrain = std::make_shared<bool>(false); // 地形(DEM+影像),默认关
auto showSlice = std::make_shared<bool>(false); // dd_slice 交互切片,默认关
// 持久的切片 widget(挂 interactor跨重建保活rebuildCentral 据条件创建/拆除)。
auto slicePlane = std::make_shared<vtkSmartPointer<vtkImagePlaneWidget>>();
std::shared_ptr<geopro::core::CrsTransform> crs; // PROJ 失败→空→体素层无效(不崩)
try {
crs = std::make_shared<geopro::core::CrsTransform>(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<double> 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<vtkImagePlaneWidget>::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();

View File

@ -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<ScatterField> LocalSampleRepository::loadVoxelScatters() {
std::vector<ScatterField> out;
out.push_back(parseScatter(readFile(kVoxelScatterFile1)));

View File

@ -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<geopro::core::ScatterField> loadVoxelScatters();

View File

@ -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})

View File

@ -0,0 +1,189 @@
#include "actors/TerrainActor.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include <vector>
#include <gdal.h>
#include <gdal_priv.h>
#include <vtkFloatArray.h>
#include <vtkImageData.h>
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper.h>
#include <vtkStructuredGrid.h>
#include <vtkStructuredGridGeometryFilter.h>
#include <vtkTexture.h>
#include <vtkUnsignedCharArray.h>
#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<vtkTexture> readTexture(const std::string& path) {
auto* ds = static_cast<GDALDataset*>(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<unsigned char> buf(static_cast<size_t>(w) * h * nb);
// 交错读 RGBpixelSpace=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<GSpacing>(w) * nb, 1);
GDALClose(ds);
if (err != CE_None) return nullptr;
vtkNew<vtkImageData> img;
img->SetDimensions(w, h, 1);
vtkNew<vtkUnsignedCharArray> sc;
sc->SetNumberOfComponents(3);
sc->SetNumberOfTuples(static_cast<vtkIdType>(w) * h);
for (int r = 0; r < h; ++r)
for (int c = 0; c < w; ++c) {
const size_t s = (static_cast<size_t>(r) * w + c) * nb;
const vtkIdType d = static_cast<vtkIdType>(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<vtkTexture>::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<vtkActor> buildTerrain(const std::string& demPath, const std::string& imagePath,
const geopro::core::GeoLocalFrame& frame, double zScale)
{
GDALAllRegister();
auto* dem = static_cast<GDALDataset*>(GDALOpen(demPath.c_str(), GA_ReadOnly));
if (!dem) return vtkSmartPointer<vtkActor>::New();
const Raster dg = readGeo(dem);
if (!dg.ok()) { GDALClose(dem); return vtkSmartPointer<vtkActor>::New(); }
// DEM 高程像素(float)。
std::vector<float> elev(static_cast<size_t>(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<float>::infinity();
float vmax = -std::numeric_limits<float>::infinity();
for (float v : elev)
if (!(hasNoData && v == static_cast<float>(noData))) {
vmin = std::min(vmin, v);
vmax = std::max(vmax, v);
}
if (!(vmin <= vmax)) { vmin = vmax = 0.0F; }
// 坐标变换DEM CRS → 43264326 → 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<GDALDataset*>(GDALOpen(imagePath.c_str(), GA_ReadOnly));
if (img) { ig = readGeo(img); GDALClose(img); }
const bool hasImage = ig.ok();
// 结构化网格:点=世界局部米(E,N,elev*zScale),标量=高程;纹理坐标(若有影像)。
vtkNew<vtkStructuredGrid> sgrid;
sgrid->SetDimensions(dg.w, dg.h, 1);
vtkNew<vtkPoints> points;
points->SetNumberOfPoints(static_cast<vtkIdType>(dg.w) * dg.h);
vtkNew<vtkFloatArray> sc;
sc->SetName("elev");
sc->SetNumberOfTuples(static_cast<vtkIdType>(dg.w) * dg.h);
vtkNew<vtkFloatArray> tc;
tc->SetName("tc");
tc->SetNumberOfComponents(2);
if (hasImage) tc->SetNumberOfTuples(static_cast<vtkIdType>(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<vtkIdType>(j) * dg.w + i;
float z = elev[static_cast<size_t>(j) * dg.w + i];
if (hasNoData && z == static_cast<float>(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<vtkStructuredGridGeometryFilter> geom;
geom->SetInputData(sgrid);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(geom->GetOutputPort());
auto actor = vtkSmartPointer<vtkActor>::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

View File

@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <vtkActor.h>
#include <vtkSmartPointer.h>
#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<vtkActor> buildTerrain(const std::string& demPath,
const std::string& imagePath,
const geopro::core::GeoLocalFrame& frame,
double zScale = 1.0);
} // namespace geopro::render

View File

@ -72,6 +72,8 @@ target_sources(geopro_tests PRIVATE render/test_scatter.cpp)
target_sources(geopro_tests PRIVATE render/test_anomaly.cpp)
# ElectrodebuildElectrodes(剖面顶边朝下三角 ) //
target_sources(geopro_tests PRIVATE render/test_electrode.cpp)
# TerrainbuildTerrain(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})

View File

@ -0,0 +1,32 @@
#include <gtest/gtest.h>
#include <string>
#include <vtkActor.h>
#include <vtkMapper.h>
#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);
}

View File

@ -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<vtkRenderer> 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;
}

View File

@ -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",