feat(gpr): 三维体 LOD 多线渲染 + 全局切片(深度/横切/顺路) + 诊断

渲染架构改 LOD 中心:各线独立 mapper + 视野自适应 LOD,弃 multi-volume 单遍。
实测确诊多线卡顿真因是"没用 LOD、渲整卷大贴图"(passcost 排除固定开销;
overlapStat 实测重叠 ~9× 非 20×;ESS 实测仅 ~2× 不解决重叠),非渲染器问题。

切片(view-all --slice [updown|leftright|frontback]):
- 深度 C-scan:逐线整张水平片(深度共面→拼成完整 C-scan,全覆盖、原生分辨率)
- 横切/顺路:全局世界面 reslice 各线到同一面 + blend(竖直面几何上每线只切细断面)
- ↑↓ 整片扫过 / [ ] 体透明度 / v 体显隐 / --sliceAt 跳位

其他:通道插值(2.5cm,从.ord读)接入 gpr_poc;--bgSuppress 压背景突出反射;
slice 命令复用桌面端 SliceTool 切单线。

诊断命令:ess-stat(空块潜力)/--overlapStat(重叠层数)/passcost(N遍vs重叠隔离)。
分析文档:性能确诊(否定 ESS/OSPRay,LOD 为通用解)。
This commit is contained in:
gaozheng 2026-06-26 23:25:51 +08:00
parent 5bf3a8e5dd
commit cf1c06cde8
9 changed files with 1632 additions and 118 deletions

View File

@ -0,0 +1,210 @@
# GPR 多通道三维体渲染性能问题 — 分析文档(供外部专家评审)
> 自包含技术文档。读者无需了解本代码库内部,只需具备 GPU 体绘制 / VTK 基础。
> 目的:把"探地雷达(GPR)多通道阵列数据渲成可交互三维体"遇到的性能问题、已试方案、实测数据、
> 待定关键点完整呈现,供外部专家判断方向。
---
## 1. 背景与系统
- 桌面端 C++ 应用Qt6 + **VTK 9.6.2**),渲染探地雷达(GPR)采集的地下三维数据,要求**可交互**(旋转/缩放)。
- 渲染用 VTK 的 `vtkGPUVolumeRayCastMapper`OpenGL GPU 光线投射体绘制)。
- 当前测试机 GPU32 个着色器纹理单元(典型独显/中端)。
## 2. 数据特征(关键,决定一切)
多通道阵列 GPR一次采集一条"测线(line)"
- **道(trace)**:一个位置一根天线的垂直回波,深度方向 **821 采样**
- **通道(channel)**:阵列横向并排的多对天线,本数据 **14 通道****相邻通道横向间距 ≈ 10.5cm**(来自 `.ord` 文件真实偏移 -0.686…+0.686m,跨度 1.37m)。
- **沿测线道间距 ≈ 4.9cm**(比横向通道间距细 ~2 倍)。
- 一条测线:沿路 **~45305 道**,覆盖 **~2.2km**(一条南北向道路)。
- **共 20 条测线 = 同一条路来回扫 20 趟**(车载,每趟阵列覆盖约 1.4m 宽,多趟铺横向)。
**单条线 = 一个三维体**X=沿测线(~45305)、Y=通道(14)、Z=深度(821)。
**关键业务约束(来自现场专家)**
- 通道太稀10.5cm)→ 需**线内通道间插值**加密(相邻真实天线之间插,物理成立);
- **绝不做"测线之间"的插值**(车与车之间是真实物理空隙,插出来"信号全是假的",工程上不可接受);
- 多条测线"分开各自插值,渲染可以合到一起"。
GPR 数据的统计特征(实测,见 §6**~91% 体素近零(反射层之间是空的),但反射层横贯整个深度分布**(不是集中一坨)。
## 3. 已建成并验证可用的功能(不是问题所在)
1. **线内通道插值**:读 `.ord` 真实横向偏移,规则化到 2.5cm 网格、相邻通道线性插值(不跨线)。
实测 Y 由 14 加密到 **56**。有单元测试。
2. **多体单遍合成**20 条独立体(各自插值)作为一个 `vtkGPUVolumeRayCastMapper` 的多个端口注册进
`vtkMultiVolume`**单遍 ray-cast 合成**(而非每条体一遍)。已验证。
3. **纹理单元上限自动退避**:单个 multi-volume 同时挂的体数受 GPU 每着色器纹理单元上限制约
(每体约吃 4 个单元 → 32 单元机上**一个包最多 7 体**,第 8 体报错并丢体)。已实现"渲一帧→报错则
每包减 1 重建重渲"的自动退避(强制 K=12 → 自动退避到 7无丢体
4. **运行时换贴图边界**(确定性测试结论):给某端口**就地换贴图**——若**保持包围盒不变**(同范围、
只改 Y 密度)则 multi-volume 算得对;若**改包围盒**任意子区域、origin/范围/spacing 变)则破坏
其缓存 `TexToBBox` → 体断开/消失。
5. 通道维 LOD、统一传函、色标图例等。
## 4. 核心问题:性能
- **20 条密体Y=56总览交互极卡**:静止 ~1.7 fps旋转/缩放掉到 < 1 fpsGPU 100%)。
- 渲染**视觉正确**(雷达剖面纹理清晰、横向连续、合成无误),纯属性能。
- 现有提速手段都是**"交互时降质"**方向(降屏幕分辨率、加粗采样步长)——**损可见质量**,治标。
用户明确要求:"有没有不损可见质量的根本性提速(业界最佳实践)?"
## 5. 已分析/已试的所有方案(含理由与状态)
| # | 方案 | 理由 | 状态 / 结论 |
|---|---|---|---|
| A | multi-volume 单遍合成 | 把"N 体=N 遍 ray-cast"降到分包遍数 | ✅ 已实现。但单遍内每步仍要在重叠体里逐个采样 |
| B | 纹理单元自动退避分包 | 绕开 GPU 纹理单元上限、不丢体 | ✅ 已实现K=7/包20 条=3 包)。代价:**跨包重叠合成不正确(接缝)** |
| C | 交互降屏幕采样(ImageSampleDistance) | VTK AutoAdjust 标准手段 | ✅ 已做。**损质量**;且 AutoAdjust 只降屏幕、不降沿光线步长 |
| D | 交互手动加粗沿光线步长(SampleDistance) | 通道插值后 Y 密→自动步长极细→巨卡;这才是大头 | ✅ 已实现(`--sampleDist`/`--dragSampleMul`)。**损质量;且用户尚未实测**(见 §7 待定) |
| E | 通道维 LOD远疏近密换 Y 平面子集)| 保包围盒换贴图(#4 验证安全) | ✅ 已实现。但**只减纹理内存、不减每步重叠体采样次数 → 对此瓶颈几乎无效** |
| F | **装箱单体(binning)**:各线先逐线插值,再把**真实道**摆进一个总览网格体(空隙透明、不跨线插值)| 一个体一遍、每步采 1 次 → ~20×真实数据无假信号 | ⚠️ 技术可行、合规,但**用户否决**:装箱合并后**总览里分不出各线**,而用户要"一起渲染时仍能逐线区分/查看"→ 合并即失去意义 |
| G | **空体素跳过 ESS换 OSPRay/ANARI 后端)**:跳过透明背景块 | 业界对稀疏体的头号提速、不损质量 | ❌ **实测对本数据收益有限**(见 §6保质量阈值下仅 ~2×且**ESS 跳的是空区、不解决"重叠"**。VTK 库存 mapper 无自动 ESSOSPRay 本环境未编、vcpkg 无包 |
| H | 减少同屏体数(只渲选中 ≤7 条)| 真实工作流本就是选几条1 包 1 遍 | ✅ 免费、永远有效(使用方式,非技术) |
## 6. 关键实测数据
### 6.1 ESS空体素跳过潜力——零依赖实测决定要不要上 OSPRay
对一条真实测线密体5702×56×789按块算 min/max统计"整块落在近零透明带"的占比(=ESS 可跳块):
| 透明带半宽(相对半值域) | 8³ 块可跳占比 | 理论提速上限 1/(1占比) | 说明 |
|---|---|---|---|
| 5% | 8% | 1.1× | 极保守 |
| **10%(保质量,不丢弱反射)** | **52%** | **~2.1×** | — |
| 20% | 80% | ~4.9× | 开始把弱反射当背景丢 |
| 30%(激进)| 90% | ~10× | 明显损质量 |
- 体素层面 **91% 近零**但块层面ESS 实际粒度)保质量阈值下**只能跳 ~52% → 理论 ~2×**。
- 原因:**反射层横贯整个深度分布**,多数块里总混着信号、跳不掉。要 10× 须用激进阈值(损质量)。
- **更关键**ESS 跳"空区"**不减少"重叠"**——在有信号的块里仍要逐个采样所有重叠体。
### 6.2 其它实测
- multi-volume 纹理单元上限:本机 7 体/包32 单元),第 8 体报 "Hardware does not support the number of textures"。
- 体维度示例coarse 4 → ~11000×56×793/线coarse 32 → ~1400×56×780/线。
- 全 20 条 densecoarse 32底图 level 0、Y=56、分 3 包,**渲染正确**;交互 ~1.7fps**未加 D 方案的旧构建**)。
## 7. 关键点——已实测,结论修正(原假设两条都被推翻)
### 7.1 "重叠几层"——实测:**平均 ~8.7 层、最大 15 层(不是 ~23也不是 20**
纯几何测(各线世界 AABB 投到 X-Y 俯视 footprint、细网格统计每格覆盖层数`--overlapStat`
- footprint横向 X≈37m、沿路 Y≈2.2km
- **有体覆盖处平均重叠 8.74 层,最大 15 层**;穿 1214 个体的格子占 ~42%。
- **结论修正**:原"~23 层"假设**错**(开发团队和外部专家都猜偏了);**重叠是真实的大瓶颈**。
- **且这 9 层是冗余**20 趟是同一条路反复扫,同一地下点被测了 ~9 次 → 这 9 个重叠体在该处**都非空**
(同一地下结构)→ **ESS 在重叠区一个都跳不掉**(再次印证 ESS 不解决重叠)。
### 7.2 "采样 vs 重叠谁是大头"——实测:**采样瓶颈fps 线性正比于步长**
20 条密体、静止近景、离屏(步长越大越快越糙):
| sampleDist | fps | 相对 |
|---|---|---|
| 0.2(≈自动细)| 1.3 | 1× |
| 0.5 | 3.2 | 2.5× |
| 1.0 | 5.9 | 4.5× |
| 2.0 | 11.3 | 8.7× |
| 4.0 | 20.9 | 16× |
- **步长翻倍→fps 翻倍 → GPU 是采样瓶颈**。总开销 ≈ 光线数 × (光线长/步长) × ~9 个重叠体。
- D 方案(手动步长)**确实直接、强力提速**;但**保质量的步长(≈ Nyquist0.5×体素)下仍只 ~2 fps**
——因为 **9× 冗余重叠**把它乘了回去。要到交互级(10+fps) 得把步长粗到 ~2.0(欠采样、损 Z 薄层)。
### 7.3 合并诊断(两测合起来)
**慢 = 采样密度 × ~9 倍冗余重叠,两者都真实。**
- D 方案(粗化采样):提速强,但保质量步长下被 9× 重叠压回 ~2fps要交互须损质量。
- **唯一"保质量又快"的,是去掉那 9× 冗余重叠**(同路重扫的同一地下点):合并/装箱(取真实道、不跨线
插值)→ 一个体一遍 → ~9× 提速、且 Nyquist 步长下也能交互、**零质量损失**(冗余测量本就该合并降噪)。
- **但这与用户"保持 20 条可区分"直接冲突**——而这 9 层在物理上是**冗余测量**(同一地下结构扫了 9 遍),
保持它们"可区分"的工程价值存疑。
### 7.4 CPU OSPRay vs GPU仍未测
ESS 对本数据 ~2× 且不解决 9× 重叠OSPRay 主要 CPU、对手是 GPU 数千核。**很可能换 OSPRay 比现状还慢**
且为 ~2× 重编整个 VTK 投入产出极差。不建议在去掉冗余重叠之前考虑。
### 7.5 "多线为何卡"的根因确诊passcost决定架构——结论**不是固定开销,是没用 LOD**
> 背景:最初 P11/P12 是"各线独立 mapper + 视野 LOD",实测仍 0.5fps。需确诊卡在三个嫌疑哪个:
> ①LOD 选区没削小 ②N 遍固定开销 ③重叠没摊掉。`passcost` 命令N 个独立 GPU mapper 各渲一个 64³ 小体
> (模拟 LOD 削过的小区),分"铺开/不重叠"与"叠在一起/重叠",测离屏稳态 fps vs N。
| N | 铺开(不重叠) fps | 叠加(重叠) fps |
|---|---|---|
| 1 | 177 | 204 |
| 5 | 162 | 43 |
| 10 | 144 | 22 |
| **20** | **78** | **11** |
**判读(决定性):**
- **嫌疑 ②N 遍固定开销)排除**20 个独立 mapper 铺开仍 **78fps**177→78远非线性
**各线独立 mapper 架构上完全可行,固定开销温和。"multi-volume 单遍 ⊥ 视野 LOD"这个不可兼得不致命**——
放弃单遍、回独立 mapper 并不慢。
- **嫌疑 ③(重叠)真实但小体下可控**:叠加随 N ~1/N每条光线乘 N**20 层 64³ 叠加仍 11fps**(可用),
再叠屏幕降采样更快。
- **嫌疑 ①(选区没削小)= 真凶**passcost 小体 20 层叠加=11fps而真实 view-all 只 1.7fps——差距全在
**贴图大小**:当前渲的是**整卷底图**~11000×56×200 ≈ 上亿体素/条),**根本没用视野 LOD 把它削成小区**。
这是**最好结局:可修,不动地基,只需真正用上 LOD**。
**对架构的直接含义**:本会话引入的 **multi-volume 单遍是错误取舍**——为"单遍"关掉了 LOD、改固定整卷贴图
导致大贴图 × 9 层重叠 = 1.7fps。而 passcost 证明独立 mapper 够快,**根本不必为单遍牺牲 LOD**。
## 8. 部署约束(硬件不确定,跨厂商)
客户机配置未知(可能无独显,或 N卡/A卡/Intel。**没有任何单一渲染器能在 N 卡和 A 卡上都做"GPU+ESS"**——
GPU 体光追渲染器全厂商锁定N 卡→NVIDIA VisRTX/OptiX/IndeXIntel→OSPRay-GPUA 卡→基本无成熟方案)。
跨厂商唯一通用的是 **OSPRay-CPU免显卡、任意 x86****OpenGL任意 GPU、但无 ESS=现状)**
若上多后端,需"OSPRay-CPU 保底 + 探测到 N卡/Intel独显时升对应 GPU 后端 + OpenGL 终极兜底"。
## 9. 关键问题——大多已被实测回答
1. ~~有没有不损质量的根本性提速法~~ **有且是通用解LOD视野自适应多分辨率**——让 GPU 单帧实际
ray-cast 的体素量**与数据总量解耦、只与"屏幕能看清的量"挂钩**Task 12c 单体已验证 752/380fps
§7.5 passcost 证明它**对多线也成立**(独立 mapper 开销温和20 条铺开 78fps
2. ~~"卡"的主因~~ **已确诊§7.5):不是 N 遍固定开销(排除),是【当前根本没用 LOD、渲整卷大贴图】嫌疑①。**
本会话引入的 multi-volume 单遍为"单遍"关掉了 LOD → 大贴图 × 9 层重叠 → 1.7fps。
3. **9 层重叠的正确定位**(外部专家纠偏 + passcost 印证):它只是**这批数据(同路重扫 20 趟)的特例倍数**
**不是渲染本质问题**。本质是"单帧采样量 > GPU 吞吐",通用解是 LOD扛任意大数据无重叠但更大也能扛
9 层重叠在 LOD 之上降级为"一个被摊薄的常数因子"passcost20 层小体叠加仍 11fps
**不要让渲染架构围绕这个特例设计。**
4. ESS/OSPRay/多后端:**继续埋掉**——ESS 对本数据 ~2× 且不解决重叠、CPU 对手是 GPU且**它解决的是 LOD
已经解决的通用问题**,投入产出差。
## 10. 最终结论passcost 确诊后,架构清晰)
- **渲染架构 = LOD 中心(视野自适应、单帧量与总量解耦)。** 这是扛"任意大数据"的通用根本解,
Task 12c 单体已验证、§7.5 passcost 多线也成立。
- **本会话的 multi-volume 单遍是错误取舍**:为"单遍合成"牺牲了 LOD、改固定整卷大贴图正是当前 1.7fps 的
直接原因。passcost 证明独立 mapper 开销温和20 条 78fps**根本不必为单遍弃 LOD**
- **正解 = 各线独立 mapper + 视野 LOD逐线用 Task 12c 引擎)+ 停手才重建**(不每帧重建,避免 P11/P12
那种"20 条每帧重建上传"的 thrash——那才是 0.5fps 的另一半原因,与稳态 ray-cast 无关,已被 P13 思路解决)。
让每条线只渲视野内小区 → 即使 9 层叠加也可用。
- **9 层重叠 = LOD 之上的可选应用层优化**(对同路重扫冗余可"合并/降噪",顺带省 9×**不进渲染地基**。
用户要逐条区分就不合并(靠 LOD 摊薄),要纯总览就合并。
- **采样步长D 方案)= LOD 框架内的质量旋钮**,非独立根本解。
- **ESS/OSPRay/多后端:不做**(不解决 LOD 已解决的通用问题,对本数据收益差)。
**下一步(确诊已完成,可开工)**:把多线总览从"multi-volume 单遍固定整卷"改回"各线独立 mapper +
视野 LOD + 停手重建",让单帧渲染量随视野走、与 20 条总量解耦;实测多线总览是否达交互级。
这是顺着通用 LOD 框架、被 passcost 数据支撑的明确方向——不再围着 9 层重叠这个特例转。
---
### 附:相关已落地代码 / 诊断工具(如专家要复现)
- 通道插值:`src/io/gpr/GprGeometry.cpp::planChannelInterpolation` + `Gpr3dvVolumeBridge.cpp`
- 多体合成/退避/质量控制/通道 LOD`tools/gpr_poc/main.cpp::cmdViewAll`
- **诊断命令**`tools/gpr_poc/main.cpp`,可直接跑复现 §6/§7 的数):
- `gpr_poc ess-stat <dir> <line>`ESS 空块潜力§6.1
- `gpr_poc view-all <dir> <gps> --overlapStat`实测重叠层数§7.1
- `gpr_poc view-all <dir> <gps> --sampleDist D`步长↔fps§7.2
- `gpr_poc passcost --size 64 --overlap 0|1`N 遍开销 vs 重叠 隔离测§7.5
- 数据/插值口径 spec`docs/superpowers/specs/2026-06-25-gpr-line-channel-interpolation-and-multivolume.md`
- 多后端 ESS 架构 spec**结论:不做,见本文 §10**`docs/superpowers/specs/2026-06-26-gpr-multibackend-ess-rendering.md`
---
## 摘要(一页结论,供决策)
1. **现象**20 条通道插值密体总览,~1.7fps、交互更卡。视觉正确,纯性能。
2. **确诊**passcost 隔离测):**不是 N 遍固定开销**20 独立 mapper 铺开 78fps排除
是**当前根本没用视野 LOD、在渲整卷大贴图**× 9 层重叠)。本会话的 multi-volume 单遍为"单遍"
牺牲了 LOD是直接原因。
3. **通用根本解 = LOD**单帧渲染量与数据总量解耦扛任意大数据Task 12c 单体 752fps、passcost 多线
也成立。9 层重叠只是**本批数据的特例倍数**,是 LOD 之上一个可摊薄/可选合并的因子,**不是架构核心**。
4. **正解**:各线独立 mapper + 视野 LOD + 停手才重建(弃 multi-volume 单遍)。
5. **明确否定**ESS/OSPRay/多后端(对本数据 ~2×、不解决重叠、解决的是 LOD 已解决的通用问题)。

View File

@ -0,0 +1,132 @@
# ⚠️ 本 spec 已被实测推翻,勿照此实现
> **结论(见 `2026-06-26-gpr-3d-render-perf-ANALYSIS-for-review.md` §10ESS/OSPRay/多后端【不做】。**
> 实测ESS 对本数据 ~2× 且不解决重叠passcost 确诊"多线卡"的真因是【没用视野 LOD、渲整卷大贴图】
> 不是固定开销。**正解 = LOD 中心(各线独立 mapper + 视野 LOD + 停手重建),见下方实现计划。**
> 本文余下"多后端/ESS"内容仅作历史记录。
---
## 实现计划LOD 中心多线总览 — 已确诊、可执行)
**目标**:把 `cmdViewAll` 从"multi-volume 单遍 + 固定整卷大贴图"改为"各线独立 mapper + 视野 LOD +
停手才重建",使单帧渲染量随视野走、与 20 条总量解耦。
**改动步骤(`tools/gpr_poc/main.cpp::cmdViewAll`**
1. `PlacedSource` 加回**每线自己的 `vtkSmartVolumeMapper`**GPU 模式);删 multi-volume 用法
multiMapper/multiVol/port 不再需要,但可暂留不碍)。
2. **装配**:删 `buildBundles` + 退避(无 multi-volume 即无纹理单元上限);改为逐线
`mapper->SetInputData(baseImage)``volume->SetMapper(mapper)``ren->AddVolume(volume)`
各线 mapper 收进 `mappers` 向量(供质量控制)。
3. **开 LOD**`gViewAllBaseOnly = false`(启用引擎选区换图);引擎换的是"改包围盒的子区域"
各线独立 mapper 下**安全**(无 multi-volume 可破坏)。
4. **关 channel LOD**`gChanLod = false`——引擎金字塔已逐级降 Y无需单独抽 Y 平面。
5. `viewAllPickOneLine``ps.mapper->SetInputData(ps.currentImg); ps.mapper->Update();`(非 multiMapper 端口)。
6. **停手才重建(已有,确认接线)**:拖动中(`viewAllOnInteracting`)只降质+重置裁剪、**不提交引擎目标**
松手(`viewAllOnInteract`)/定时器 idle 才 `viewAllSubmitTargets`(提交 LOD 目标)+ `viewAllPickOneLine`
(拉就绪区域换上)。避免 P11/P12"每帧 20 条重建上传"thrash。
7. **质量旋钮保留**`--sampleDist`/`--maxImgSample`/`--dragSampleMul` 作 LOD 框架内的交互降质兜底。
8. **总览级别**:引擎 `selectLod` 按屏幕像素选层——拉远时全路映射到少量像素 → 自动选粗层(小贴图)→
20 条小贴图即可用;拉近 → 小区域细层。**确认 selectLod 多线下确实选到粗层**(若没有,调
selectLod 的屏幕像素阈值——这是 §7.5 嫌疑①的修复点)。
**验收**:离屏看各线底图 level 随相机距离变(远→粗/小、近→细/小区);真窗口测 20 条总览交互级 fps、
拖动跟手、松手清晰、过档位无明显卡顿(外部专家提示重点盯"停手重建过渡手感")。
---
# GPR 三维体渲染:多后端 + 空体素跳过(ESS) 加速架构 — Spec2026-06-26已废
> 解决"20 个重叠密体在 VTK 库存 OpenGL mapper 上又卡又只能降质"的根本性方案。
> 关联:`docs/superpowers/specs/2026-06-25-gpr-line-channel-interpolation-and-multivolume.md`(数据/插值口径)。
---
## 0. 一句话目标
用**空体素跳过(ESS)** 这一不损可见质量的业界技术,把"逐线分开的多个密体合并渲染"做到**不卡**
并以**多渲染后端 + 自动适配**覆盖客户侧未知/多厂商硬件(含无显卡)。
---
## 1. 问题与根因
- 现状渲染 = VTK `vtkGPUVolumeRayCastMapper`OpenGL。**任意 GPU 可跑(最通用),但无 ESS**。
- 20 个重叠半透明密体:每条光线每步在 20 体各采一次、光线又长 → 几十亿次纹理查找/帧 → 1.7fps、交互更卡。
- 通道插值后 Y 加密到 2.5cm,使自动沿光线步长更细 → 更卡。
- **已尝试的"交互降质"(屏幕降采样 + 沿光线步长加粗)是治标**,损可见质量。
## 2. 根本方案ESS不损质量
GPR 体 ~90% 是近零背景反射层之间空。ESS 用 min/max 加速结构**跳过"在传函里全透明"的块**
对稀疏数据常 550× 提速、**零质量损失**。但 **VTK 库存 GPU mapper 不做自动 ESS**(仅有受限的
`UseDepthPass` 等高线跳过)。→ 真 ESS 必须**换专业体渲染后端**(其底层自带 ESS + 正确合成多重叠体)。
## 3. 关键事实:跨厂商 GPU 加速不存在单一方案
GPU 体光追渲染器全是**厂商锁定**
| 后端 | 硬件 | ESS | 跨厂商 | 角色 |
|---|---|---|---|---|
| OpenGLvtkGPUVolumeRayCastMapper现状| 任意 GPU(N/A/Intel) | ❌ | ✅ | **终极兜底** |
| OSPRayCPUEmbree/ISPC| 任意 x86 CPU免显卡| ✅ | ✅ | **通用基线** |
| OSPRay-GPUSYCL/oneAPI| 仅 Intel Arc/数据中心卡 | ✅ | ❌ | Intel 独显 |
| ANARI + VisRTXOptiX| 仅 NVIDIA | ✅ | ❌ | N 卡 |
| AMD GPU 体渲染+ESS | — | — | ❌ 无成熟方案 | — |
**结论**:没有"同时 N/A 卡的 GPU-ESS"。**面向未知客户机OSPRay-CPU 是最稳通用选择**免显卡、ESS、质量不降
## 4. 渲染后端架构(多后端 + 自动适配 + 手动覆盖 + 兼容灰掉)
### 4.1 用户可见选项(按硬件/结果命名,不暴露库名)
| 用户看到 | 背后实现 | 适配硬件 |
|---|---|---|
| **自动(推荐)** | 探测后选下面之一 | — |
| GPU 加速N卡| ANARI + VisRTX | NVIDIA 独显 |
| GPU 加速Intel| OSPRay-GPU | Intel Arc 独显 |
| CPU通用免显卡| OSPRay-CPU | 任意 CPU |
| 通用 GPU兼容| OpenGL现状 mapper| 任意 GPU兜底|
### 4.2 自动探测逻辑
```
if NVIDIA 独显: → VisRTX(GPU)
elif Intel Arc 独显: → OSPRay-GPU
elif AMD(独显/核显) 或 Intel 核显 或 无显卡 或 探测失败:
→ OSPRay-CPU默认 // 核显一律走 CPU弱+共享内存+多不被GPU后端支持
// 强力 A 卡可手动选"通用 OpenGL"用其 GPU(无 ESS),但不作默认
```
- **手动覆盖**:用户可自选,但**只列出与当前硬件兼容的项**(不兼容灰掉,如 A 卡上禁 VisRTX
- **集显建议**:一律 OSPRay-CPUCPU+ESS 比让弱核显硬渲更稳更快)。
### 4.3 部署策略(一句话)
**OSPRay-CPU 保底通用;探测到 N卡/Intel Arc 时升对应 GPU 后端A 卡/核显吃 CPU 基线OpenGL 终极兜底。**
## 5. 上 ESS 后端后,废弃哪些
- ❌ **装箱单体(binning)**:当初为绕 OpenGL 无 ESS 才提(代价=丢"逐线分开"。ESS 后端让**逐线分开
的多体也快** → 不需要装箱。**逐线分开 + 不造假 + 不卡,三者兼得。**
- ❌ **交互降质权宜**(屏幕/沿光线步长加粗ESS 后端有自带的自适应/渐进式细化,基本不用;保留作任何后端的兜底。
- ❌ 自写 ESS shader / 预积分 / UseDepthPass后端自带无需自行实现。
- ✅ **保留**"选几条 ds(≤7)" 是使用方式(非技术),永远有效。
## 6. 实现要点(工程)
- VTK 需**重编**带:`RenderingRayTracing`(OSPRay)、`RenderingAnari`(ANARI/VisRTX) 模块 + 依赖
OSPRay/Embree/ISPC/OpenVKLANARI-SDK/VisRTX。用户已确认工程量无所谓。
- 渲染层抽象一个 `IVolumeRenderBackend`,运行时按 §4 选具体 mapper
`vtkGPUVolumeRayCastMapper`(OpenGL) / `vtkOSPRayPass`+volume / `vtkAnariPass`+volume。
- **数据不变**逐线密体含通道插值spec 前一份)原样喂各后端;多体合成由后端负责(无 K=7 分包)。
- 硬件探测GL_VENDOR / 平台 APIDXGI 枚举适配器)判 N/A/Intel + 独显/核显。
## 7. POC 计划(先验"CPU+ESS 够不够快"——最通用、风险最低)
1. **POC-1先做**:最小程序,用 **OSPRay-CPU** 渲一个 GPR 密体tmp/lines_all_dense 里一条/几条),
**实测普通 CPU 上对多体/密体的 fps + ESS 提速比**,对照现状 OpenGL。先确认 OSPRay 在本环境
可编可跑、CPU+ESS 实际够快——这是整套方案值不值得上的关键闸门。
2. **POC-2**:若有 N 卡ANARI+VisRTX 渲同一体,对照 GPU 提速。
3. POC 通过 → 才动手重编 VTK + 接后端抽象层。
## 8. 风险 / 待定
- OSPRay-GPUIntel较新、不如 CPU 路成熟ANARI/VisRTX 需 NVIDIA 驱动 + VisRTX 库。
- 各后端传函/外观与 OpenGL 有差异,需重新调一致。
- 本环境能否编出带光追/ANARI 的 VTKvcpkg/手动依赖)待 POC-1 验证。
- CPU+ESS 在低核机上的实际帧率待实测。
## 9. 验收
1. 客户机无论有无显卡/何种显卡自动选到可跑的后端并出图OSPRay-CPU 永远兜底)。
2. 20 条密体总览:逐线分开、不造假、**ESS 后端下不卡**(目标交互 ≥ 可用帧率,质量不降)。
3. 手动选项只列兼容项;集显默认 CPU。
4. 数据层(通道插值密体)零改动复用。

View File

@ -4,6 +4,7 @@
#include <cmath>
#include <limits>
#include <stdexcept>
#include <vector>
#include <QString>
@ -12,6 +13,7 @@
#include "RadarProcessor.h"
#include "core/model/ScalarVolumeI16.hpp"
#include "io/gpr/GprGeometry.hpp" // planChannelInterpolation
namespace geopro::io::gpr {
@ -68,7 +70,7 @@ double nowMs(std::chrono::steady_clock::time_point t0) {
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
const std::string& linePrefix,
BridgeMetrics* metricsOut,
int coarse) {
int coarse, double targetDy) {
const int stride = coarse > 1 ? coarse : 1; // 沿测线下采样步长(≥1)
const QString dir = QString::fromLocal8Bit(lineDir.c_str());
const QString base = QString::fromLocal8Bit(linePrefix.c_str());
@ -107,9 +109,25 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
}
// 下采样后输出道数(向上取整保留末道附近)nxOut = ceil(traces/stride)。
const int nxOut = (traces + stride - 1) / stride;
const int nx = nxOut; // X=道(沿测线,已按 stride 下采样)
const int ny = channels; // Y=通道(横向)
const int nz = samples; // Z=样本(深度)
const int nx = nxOut; // X=道(沿测线,已按 stride 下采样)
const int nz = samples; // Z=样本(深度)
// §1 线内通道插值:读各通道真实横向偏移(header.chXOffsets) → 规则网格化 Y 到 targetDy。
// 绝不跨线;间距/通道数从数据来,不假设。退路(无偏移/未启用)= 逐通道 identity。
std::vector<double> latOff;
const auto& chx = processed.header.chXOffsets;
if (chx.size() == channels)
for (int c = 0; c < channels; ++c)
latOff.push_back(static_cast<double>(chx[c]));
std::vector<geopro::io::gpr::ChannelInterpRow> rows;
bool interpolated = false;
if (static_cast<int>(latOff.size()) == channels && targetDy > 0.0) {
rows = planChannelInterpolation(latOff, targetDy);
interpolated = (static_cast<int>(rows.size()) != channels);
}
if (rows.empty())
for (int c = 0; c < channels; ++c) rows.push_back({c, c, 0.0});
const int ny = static_cast<int>(rows.size()); // Y=通道(横向,可能已插值加密)
// 3) 扫处理后值域 → Quant(offset=中点,防溢出)。
const auto tFill = std::chrono::steady_clock::now();
@ -138,20 +156,26 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0;
quant.offset = 0.5 * (vmin + vmax); // 中点 → 防溢出
// 4) 逐 (ch,trace,sample) 填体。GPR 立方体为稠密体(每体素有值),无空洞 → 不置 kBlank。
// 4) 逐 (输出行 j, trace, sample) 填体。每个输出行 = 两侧最近真实通道线性插值
// (a==b 时即原通道)。GPR 立方体稠密(每体素有值),无空洞 → 不置 kBlank。
// 沿测线按 stride 下采样:输出道 to → 源道 t = to*stride。
geopro::core::BuiltI16 built;
built.vol = geopro::core::ScalarVolumeI16(nx, ny, nz);
for (int c = 0; c < channels; ++c) {
const auto& chData = processed.volumeData[c];
for (int j = 0; j < ny; ++j) {
const auto& chA = processed.volumeData[rows[j].a];
const auto& chB = processed.volumeData[rows[j].b];
const double wb = rows[j].wb, wa = 1.0 - wb;
for (int to = 0; to < nxOut; ++to) {
const int t = to * stride;
const bool hasTrace = t < static_cast<int>(chData.size());
const bool hasA = t < static_cast<int>(chA.size());
const bool hasB = t < static_cast<int>(chB.size());
for (int s = 0; s < samples; ++s) {
short v = 0;
if (hasTrace && s < static_cast<int>(chData[t].size())) v = chData[t][s];
// X=输出道 to、Y=通道 c、Z=样本 s。
built.vol.at(to, c, s) = quant.toQ(static_cast<double>(v));
const double va =
(hasA && s < static_cast<int>(chA[t].size())) ? chA[t][s] : 0.0;
const double vb =
(hasB && s < static_cast<int>(chB[t].size())) ? chB[t][s] : 0.0;
// X=输出道 to、Y=输出行 j、Z=样本 s。
built.vol.at(to, j, s) = quant.toQ(wa * va + wb * vb);
}
}
}
@ -162,7 +186,8 @@ geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
// 下采样后相邻输出道在世界中跨 stride 个原始道距 → dx ×stride 保持真实尺度。
const double dxBase = h.distanceInc > 1e-9 ? h.distanceInc : 1.0;
const double dx = dxBase * stride;
const double dy = channelSpacingY(h, channels);
// 插值后 Y 已规则化到 targetDy 网格;否则用原通道横距。
const double dy = interpolated ? targetDy : channelSpacingY(h, channels);
const double dz = depthSpacingZ(h);
built.quant = quant;

View File

@ -40,11 +40,15 @@ struct BridgeMetrics {
// metricsOut 非空时回填维度/量化/spacing/耗时(供 CLI 报告,不编造)。
// coarse(下采样因子≥1):沿测线(道/X 轴)每 coarse 道取 1spacing.x ×coarse 保形;
// 通道/样本(横向/深度)保留全分辨率。coarse≤1 即全分辨率。磁盘紧张时省空间用。
// targetDy(米,>0 启用):线内【通道间插值】目标横向间距。读各通道真实横向偏移
// (header.chXOffsets) 规则网格化 Y 到 targetDyny=round(跨度/targetDy)+1逐行线性
// 插值(不跨线、不假设道间距)。<=0 或无偏移 → 不插值Y=原通道数。默认 0.025(2.5cm)。
// 失败(加载失败/立方体为空) → 抛 std::runtime_error。
geopro::core::BuiltI16 buildLineVolumeFromGpr3dv(const std::string& lineDir,
const std::string& linePrefix,
BridgeMetrics* metricsOut,
int coarse = 1);
int coarse = 1,
double targetDy = 0.025);
} // namespace geopro::io::gpr

View File

@ -1,5 +1,8 @@
#include "io/gpr/GprGeometry.hpp"
#include <algorithm>
#include <cmath>
#include <numeric>
#include <sstream>
#include <string>
#include <vector>
@ -22,6 +25,46 @@ std::vector<double> parseChannelXOffsets(const std::string& ordText) {
return offsets;
}
std::vector<ChannelInterpRow> planChannelInterpolation(
const std::vector<double>& offsets, double targetDy) {
const int n = static_cast<int>(offsets.size());
std::vector<ChannelInterpRow> rows;
// 退化:通道<2 或 targetDy 非法 → 逐通道 identity。
if (n < 2 || targetDy <= 0.0) {
for (int i = 0; i < n; ++i) rows.push_back({i, i, 0.0});
return rows;
}
// 按偏移排序的通道索引(端点 / 区间定位用;通道本身可能非有序)。
std::vector<int> ord(n);
std::iota(ord.begin(), ord.end(), 0);
std::sort(ord.begin(), ord.end(),
[&](int x, int y) { return offsets[x] < offsets[y]; });
const double mn = offsets[ord.front()];
const double mx = offsets[ord.back()];
const double span = mx - mn;
// 跨度已比 targetDy 还密 → 不加密,逐通道 identity保原通道序
if (span <= targetDy * 0.5) {
for (int i = 0; i < n; ++i) rows.push_back({i, i, 0.0});
return rows;
}
const int ny = static_cast<int>(std::lround(span / targetDy)) + 1;
int k = 0; // ord 内区间左指针offsets[ord[k]] <= p
for (int j = 0; j < ny; ++j) {
const double p = mn + static_cast<double>(j) * targetDy;
while (k + 1 < n && offsets[ord[k + 1]] < p) ++k;
if (k + 1 >= n) { // p 在最右通道之外 → 取最右通道
rows.push_back({ord[n - 1], ord[n - 1], 0.0});
continue;
}
const int a = ord[k], b = ord[k + 1];
const double oa = offsets[a], ob = offsets[b];
double wb = (ob > oa) ? (p - oa) / (ob - oa) : 0.0;
wb = std::clamp(wb, 0.0, 1.0);
rows.push_back({a, b, wb});
}
return rows;
}
double depthOfSample(int s, const IprHeader& h) {
if (h.samples <= 1) return 0.0; // 防除零
const double timeNs = static_cast<double>(s) * h.timeWindowNs /

View File

@ -11,6 +11,22 @@ namespace geopro::io::gpr {
// 解析 .ord 文本,返回末列==1 的有效通道的横向偏移(第 2 列),按文件顺序。
std::vector<double> parseChannelXOffsets(const std::string& ordText);
// 通道间插值方案:一个输出网格行 = (1-wb)*通道[a] + wb*通道[b](线性)。
// a==b 时即原样取该通道(无插值)。
struct ChannelInterpRow {
int a = 0;
int b = 0;
double wb = 0.0;
};
// 按真实横向偏移 offsets(米,逐通道) + 目标横向间距 targetDy(米) 规则网格化通道维(Y)
// 返回每个输出网格行的线性插值方案。网格在 [min(off), max(off)] 上以 targetDy 等距取
// ny = round(span/targetDy)+1 行;每行找两侧最近真实通道线性插值(端点外用端点)。
// 退化(通道<2 / targetDy<=0 / 跨度已比 targetDy 还密) → 逐通道 identity每通道一行
// 纯函数,便于单测:不依赖任何文件/模型。
std::vector<ChannelInterpRow> planChannelInterpolation(
const std::vector<double>& offsets, double targetDy);
// 采样序号 s → 深度(米)。depth = soilVelocity[m/s] * (s * timeWindowNs/(samples-1) * 1e-9) / 2。
// samples<=1 时返回 0 防除零。
double depthOfSample(int s, const IprHeader& h);

View File

@ -22,7 +22,8 @@ namespace {
void writeSyntheticChannel(const fs::path& iprhPath, int samples, int traces,
std::int16_t base, double chYOffset,
double distanceInterval, double timeWindowNs,
double soilVelocity, int channels) {
double soilVelocity, int channels,
double chXOffset = 1e30) { // 1e30=不写 CH_X_OFFSET不触发通道插值
std::ofstream h(iprhPath);
h << "SAMPLES: " << samples << "\n";
h << "LAST TRACE: " << (traces - 1) << "\n";
@ -31,6 +32,7 @@ void writeSyntheticChannel(const fs::path& iprhPath, int samples, int traces,
h << "SOIL VELOCITY: " << soilVelocity << "\n";
h << "DISTANCE INTERVAL: " << distanceInterval << "\n";
h << "CH_Y_OFFSET: " << chYOffset << "\n";
if (chXOffset < 1e29) h << "CH_X_OFFSET: " << chXOffset << "\n"; // 横向偏移(插值用)
h.close();
fs::path iprbPath = iprhPath;
@ -119,6 +121,39 @@ TEST_F(Gpr3dvBridgeTest, MapsAxesQuantAndSpacing) {
geopro::core::ScalarVolumeI16::kBlank);
}
TEST_F(Gpr3dvBridgeTest, ChannelInterpDensifiesYThroughBridge) {
// §1 端到端3 通道带真实横向偏移 CH_X_OFFSET=-0.5/0/+0.5(跨度 1.0)。
// targetDy=0.25 → ny=round(1.0/0.25)+1=5从 3 通道插值加密到 5 平面dy=0.25。
const int samples = 64, traces = 40, channels = 3;
writeSyntheticChannel(dir_ / "syn_001_A01.iprh", samples, traces, 100, -1.5,
0.05, 100.0, 0.1, channels, /*chXOffset=*/-0.5);
writeSyntheticChannel(dir_ / "syn_001_A02.iprh", samples, traces, 200, -1.5,
0.05, 100.0, 0.1, channels, /*chXOffset=*/0.0);
writeSyntheticChannel(dir_ / "syn_001_A03.iprh", samples, traces, 300, -1.5,
0.05, 100.0, 0.1, channels, /*chXOffset=*/0.5);
geopro::io::gpr::BridgeMetrics bm;
geopro::core::BuiltI16 built;
ASSERT_NO_THROW({
built = geopro::io::gpr::buildLineVolumeFromGpr3dv(
dir_.string(), "syn_001", &bm, /*coarse=*/1, /*targetDy=*/0.25);
});
EXPECT_EQ(built.vol.ny(), 5); // 3 通道 → 5 网格平面
EXPECT_EQ(built.vol.ny(), bm.ny);
EXPECT_NEAR(built.spacing[1], 0.25, 1e-9); // dy = targetDy
// 稠密(无 kBlank插值值落在原通道值域内线性混合不外溢
EXPECT_NE(built.vol.at(0, 2, 0), geopro::core::ScalarVolumeI16::kBlank);
// 关闭插值targetDy=0→ ny 回到原通道数 3。
geopro::core::BuiltI16 raw;
ASSERT_NO_THROW({
raw = geopro::io::gpr::buildLineVolumeFromGpr3dv(
dir_.string(), "syn_001", nullptr, /*coarse=*/1, /*targetDy=*/0.0);
});
EXPECT_EQ(raw.vol.ny(), channels);
}
TEST_F(Gpr3dvBridgeTest, ThrowsOnMissingLine) {
// 目录无任何 _A.iprh → loadImpulseMultiChannel 失败 → 抛异常。
geopro::io::gpr::BridgeMetrics bm;

View File

@ -1,6 +1,8 @@
#include "io/gpr/GprGeometry.hpp"
#include "io/gpr/IprHeader.hpp"
#include <gtest/gtest.h>
#include <algorithm>
#include <vector>
using namespace geopro::io::gpr;
TEST(GprGeometry, ParsesActiveChannelOffsets) {
@ -11,6 +13,69 @@ TEST(GprGeometry, ParsesActiveChannelOffsets) {
EXPECT_NEAR(xs[1], -0.581, 1e-6);
}
TEST(GprGeometry, ChannelInterpDensifiesToTargetGrid) {
// 14 通道 ~均匀 0.105m,跨度 1.372mtargetDy=0.025 → ny=round(1.372/0.025)+1=56。
std::vector<double> off;
for (int i = 0; i < 14; ++i) off.push_back(-0.686 + i * 0.1055385); // -0.686..+0.686
auto rows = planChannelInterpolation(off, 0.025);
EXPECT_EQ(rows.size(), 56u);
// 首行=最左通道(无插值),末行=最右通道。
EXPECT_EQ(rows.front().a, 0);
EXPECT_NEAR(rows.front().wb, 0.0, 1e-9);
EXPECT_EQ(rows.back().a, 13);
// 中间存在真插值行wb 落在 (0,1))。
bool sawInterp = false;
for (const auto& r : rows)
if (r.a != r.b && r.wb > 0.05 && r.wb < 0.95) sawInterp = true;
EXPECT_TRUE(sawInterp);
}
TEST(GprGeometry, ChannelInterpDynamicCountBySpacing) {
// 不同道间距 → 不同插值条数(动态,不写死)。两通道 0.10m 间距:
// targetDy=0.025 → ny=round(0.10/0.025)+1=5端点 + 中间 3 条)。
auto r1 = planChannelInterpolation({0.0, 0.10}, 0.025);
EXPECT_EQ(r1.size(), 5u);
// targetDy=0.05 → ny=round(0.10/0.05)+1=3端点 + 中间 1 条)。
auto r2 = planChannelInterpolation({0.0, 0.10}, 0.05);
EXPECT_EQ(r2.size(), 3u);
}
TEST(GprGeometry, ChannelInterpHandlesUnsortedOffsets) {
// 通道在文件里可能非按偏移有序(任意排布)。偏移乱序但物理跨度相同 → 网格行数、
// 端点、单调性应不受顺序影响(内部按偏移排序定位)。
std::vector<double> sorted = {-0.4, -0.2, 0.0, 0.2, 0.4};
std::vector<double> shuffled = {0.0, 0.4, -0.4, 0.2, -0.2}; // 同偏移集合,乱序
auto rs = planChannelInterpolation(sorted, 0.1);
auto ru = planChannelInterpolation(shuffled, 0.1);
ASSERT_EQ(rs.size(), ru.size()); // ny 仅取决于跨度,与顺序无关
EXPECT_EQ(rs.size(), 9u); // round(0.8/0.1)+1
// 真正的不变量:每行的【有效插值位置】=(1-wb)*off[a]+wb*off[b] 应等于网格位置
// p=min+j*targetDy与通道在文件里的顺序无关。末行恰落在 max 时会以 [次末,末] 夹
// 且 wb=1值=末通道),故不能直接断言 a==末通道,要看有效位置。
auto effPos = [](const std::vector<double>& off, const ChannelInterpRow& r) {
return (1.0 - r.wb) * off[r.a] + r.wb * off[r.b];
};
for (std::size_t j = 0; j < ru.size(); ++j) {
const double p = -0.4 + static_cast<double>(j) * 0.1;
EXPECT_NEAR(effPos(sorted, rs[j]), p, 1e-9); // 有序
EXPECT_NEAR(effPos(shuffled, ru[j]), p, 1e-9); // 乱序:同一结果
// a/b 偏移夹住网格位置。
const double oa = shuffled[ru[j].a], ob = shuffled[ru[j].b];
EXPECT_LE(std::min(oa, ob) - 1e-9, p);
EXPECT_GE(std::max(oa, ob) + 1e-9, p);
}
}
TEST(GprGeometry, ChannelInterpDegenerateIdentity) {
// 单通道 / 已比 targetDy 密 / targetDy<=0 → 逐通道 identity。
EXPECT_EQ(planChannelInterpolation({0.5}, 0.025).size(), 1u);
auto dense = planChannelInterpolation({0.0, 0.01}, 0.025); // 跨度<targetDy/2
ASSERT_EQ(dense.size(), 2u);
EXPECT_EQ(dense[0].a, 0);
EXPECT_EQ(dense[1].a, 1);
EXPECT_EQ(planChannelInterpolation({0.0, 0.10}, -1.0).size(), 2u);
}
TEST(GprGeometry, DepthOfLastSampleMatchesPhysics) {
IprHeader h{};
h.samples = 821;

File diff suppressed because it is too large Load Diff