diff --git a/docs/superpowers/specs/2026-06-26-gpr-3d-render-perf-ANALYSIS-for-review.md b/docs/superpowers/specs/2026-06-26-gpr-3d-render-perf-ANALYSIS-for-review.md
new file mode 100644
index 0000000..9e2bed9
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-26-gpr-3d-render-perf-ANALYSIS-for-review.md
@@ -0,0 +1,210 @@
+# GPR 多通道三维体渲染性能问题 — 分析文档(供外部专家评审)
+
+> 自包含技术文档。读者无需了解本代码库内部,只需具备 GPU 体绘制 / VTK 基础。
+> 目的:把"探地雷达(GPR)多通道阵列数据渲成可交互三维体"遇到的性能问题、已试方案、实测数据、
+> 待定关键点完整呈现,供外部专家判断方向。
+
+---
+
+## 1. 背景与系统
+
+- 桌面端 C++ 应用(Qt6 + **VTK 9.6.2**),渲染探地雷达(GPR)采集的地下三维数据,要求**可交互**(旋转/缩放)。
+- 渲染用 VTK 的 `vtkGPUVolumeRayCastMapper`(OpenGL GPU 光线投射体绘制)。
+- 当前测试机 GPU:32 个着色器纹理单元(典型独显/中端)。
+
+## 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 fps(GPU 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 无自动 ESS;OSPRay 本环境未编、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 条 dense(coarse 32):底图 level 0、Y=56、分 3 包,**渲染正确**;交互 ~1.7fps(**未加 D 方案的旧构建**)。
+
+## 7. 关键点——已实测,结论修正(原假设两条都被推翻)
+
+### 7.1 "重叠几层"——实测:**平均 ~8.7 层、最大 15 层(不是 ~2–3,也不是 20)**
+纯几何测(各线世界 AABB 投到 X-Y 俯视 footprint、细网格统计每格覆盖层数,`--overlapStat`):
+- footprint:横向 X≈37m、沿路 Y≈2.2km;
+- **有体覆盖处平均重叠 8.74 层,最大 15 层**;穿 12–14 个体的格子占 ~42%。
+- **结论修正**:原"~2–3 层"假设**错**(开发团队和外部专家都猜偏了);**重叠是真实的大瓶颈**。
+- **且这 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 方案(手动步长)**确实直接、强力提速**;但**保质量的步长(≈ Nyquist,0.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/IndeX;Intel→OSPRay-GPU;A 卡→基本无成熟方案)。
+跨厂商唯一通用的是 **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 之上降级为"一个被摊薄的常数因子"(passcost:20 层小体叠加仍 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
`:ESS 空块潜力(§6.1)
+ - `gpr_poc view-all --overlapStat`:实测重叠层数(§7.1)
+ - `gpr_poc view-all --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 已解决的通用问题)。
diff --git a/docs/superpowers/specs/2026-06-26-gpr-multibackend-ess-rendering.md b/docs/superpowers/specs/2026-06-26-gpr-multibackend-ess-rendering.md
new file mode 100644
index 0000000..fb7a6d0
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-26-gpr-multibackend-ess-rendering.md
@@ -0,0 +1,132 @@
+# ⚠️ 本 spec 已被实测推翻,勿照此实现
+
+> **结论(见 `2026-06-26-gpr-3d-render-perf-ANALYSIS-for-review.md` §10):ESS/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) 加速架构 — Spec(2026-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 加速结构**跳过"在传函里全透明"的块**,
+对稀疏数据常 5–50× 提速、**零质量损失**。但 **VTK 库存 GPU mapper 不做自动 ESS**(仅有受限的
+`UseDepthPass` 等高线跳过)。→ 真 ESS 必须**换专业体渲染后端**(其底层自带 ESS + 正确合成多重叠体)。
+
+## 3. 关键事实:跨厂商 GPU 加速不存在单一方案
+GPU 体光追渲染器全是**厂商锁定**:
+
+| 后端 | 硬件 | ESS | 跨厂商 | 角色 |
+|---|---|---|---|---|
+| OpenGL(vtkGPUVolumeRayCastMapper,现状)| 任意 GPU(N/A/Intel) | ❌ | ✅ | **终极兜底** |
+| OSPRay(CPU,Embree/ISPC)| 任意 x86 CPU(免显卡)| ✅ | ✅ | **通用基线** |
+| OSPRay-GPU(SYCL/oneAPI)| 仅 Intel Arc/数据中心卡 | ✅ | ❌ | Intel 独显 |
+| ANARI + VisRTX(OptiX)| 仅 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-CPU(CPU+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/OpenVKL;ANARI-SDK/VisRTX)。用户已确认工程量无所谓。
+- 渲染层抽象一个 `IVolumeRenderBackend`,运行时按 §4 选具体 mapper:
+ `vtkGPUVolumeRayCastMapper`(OpenGL) / `vtkOSPRayPass`+volume / `vtkAnariPass`+volume。
+- **数据不变**:逐线密体(含通道插值,spec 前一份)原样喂各后端;多体合成由后端负责(无 K=7 分包)。
+- 硬件探测:GL_VENDOR / 平台 API(DXGI 枚举适配器)判 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-GPU(Intel)较新、不如 CPU 路成熟;ANARI/VisRTX 需 NVIDIA 驱动 + VisRTX 库。
+- 各后端传函/外观与 OpenGL 有差异,需重新调一致。
+- 本环境能否编出带光追/ANARI 的 VTK(vcpkg/手动依赖)待 POC-1 验证。
+- CPU+ESS 在低核机上的实际帧率待实测。
+
+## 9. 验收
+1. 客户机无论有无显卡/何种显卡,自动选到可跑的后端并出图(OSPRay-CPU 永远兜底)。
+2. 20 条密体总览:逐线分开、不造假、**ESS 后端下不卡**(目标交互 ≥ 可用帧率,质量不降)。
+3. 手动选项只列兼容项;集显默认 CPU。
+4. 数据层(通道插值密体)零改动复用。
diff --git a/src/io/gpr/Gpr3dvVolumeBridge.cpp b/src/io/gpr/Gpr3dvVolumeBridge.cpp
index 589fd31..e3b7bcf 100644
--- a/src/io/gpr/Gpr3dvVolumeBridge.cpp
+++ b/src/io/gpr/Gpr3dvVolumeBridge.cpp
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include
@@ -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 latOff;
+ const auto& chx = processed.header.chXOffsets;
+ if (chx.size() == channels)
+ for (int c = 0; c < channels; ++c)
+ latOff.push_back(static_cast(chx[c]));
+ std::vector rows;
+ bool interpolated = false;
+ if (static_cast(latOff.size()) == channels && targetDy > 0.0) {
+ rows = planChannelInterpolation(latOff, targetDy);
+ interpolated = (static_cast(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(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(chData.size());
+ const bool hasA = t < static_cast(chA.size());
+ const bool hasB = t < static_cast(chB.size());
for (int s = 0; s < samples; ++s) {
- short v = 0;
- if (hasTrace && s < static_cast(chData[t].size())) v = chData[t][s];
- // X=输出道 to、Y=通道 c、Z=样本 s。
- built.vol.at(to, c, s) = quant.toQ(static_cast(v));
+ const double va =
+ (hasA && s < static_cast(chA[t].size())) ? chA[t][s] : 0.0;
+ const double vb =
+ (hasB && s < static_cast(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;
diff --git a/src/io/gpr/Gpr3dvVolumeBridge.hpp b/src/io/gpr/Gpr3dvVolumeBridge.hpp
index b404559..0dd84ef 100644
--- a/src/io/gpr/Gpr3dvVolumeBridge.hpp
+++ b/src/io/gpr/Gpr3dvVolumeBridge.hpp
@@ -40,11 +40,15 @@ struct BridgeMetrics {
// metricsOut 非空时回填维度/量化/spacing/耗时(供 CLI 报告,不编造)。
// coarse(下采样因子,≥1):沿测线(道/X 轴)每 coarse 道取 1,spacing.x ×coarse 保形;
// 通道/样本(横向/深度)保留全分辨率。coarse≤1 即全分辨率。磁盘紧张时省空间用。
+// targetDy(米,>0 启用):线内【通道间插值】目标横向间距。读各通道真实横向偏移
+// (header.chXOffsets) 规则网格化 Y 到 targetDy:ny=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
diff --git a/src/io/gpr/GprGeometry.cpp b/src/io/gpr/GprGeometry.cpp
index b45b023..8cb6fb9 100644
--- a/src/io/gpr/GprGeometry.cpp
+++ b/src/io/gpr/GprGeometry.cpp
@@ -1,5 +1,8 @@
#include "io/gpr/GprGeometry.hpp"
+#include
+#include
+#include
#include
#include
#include
@@ -22,6 +25,46 @@ std::vector parseChannelXOffsets(const std::string& ordText) {
return offsets;
}
+std::vector planChannelInterpolation(
+ const std::vector& offsets, double targetDy) {
+ const int n = static_cast(offsets.size());
+ std::vector 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 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(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(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(s) * h.timeWindowNs /
diff --git a/src/io/gpr/GprGeometry.hpp b/src/io/gpr/GprGeometry.hpp
index cabcecc..9338833 100644
--- a/src/io/gpr/GprGeometry.hpp
+++ b/src/io/gpr/GprGeometry.hpp
@@ -11,6 +11,22 @@ namespace geopro::io::gpr {
// 解析 .ord 文本,返回末列==1 的有效通道的横向偏移(第 2 列),按文件顺序。
std::vector 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 planChannelInterpolation(
+ const std::vector& 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);
diff --git a/tests/io/gpr/test_gpr3dv_volume_bridge.cpp b/tests/io/gpr/test_gpr3dv_volume_bridge.cpp
index c1bb45c..594c49b 100644
--- a/tests/io/gpr/test_gpr3dv_volume_bridge.cpp
+++ b/tests/io/gpr/test_gpr3dv_volume_bridge.cpp
@@ -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;
diff --git a/tests/io/gpr/test_gpr_geometry.cpp b/tests/io/gpr/test_gpr_geometry.cpp
index 51f9287..8d5decc 100644
--- a/tests/io/gpr/test_gpr_geometry.cpp
+++ b/tests/io/gpr/test_gpr_geometry.cpp
@@ -1,6 +1,8 @@
#include "io/gpr/GprGeometry.hpp"
#include "io/gpr/IprHeader.hpp"
#include
+#include
+#include
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.372m;targetDy=0.025 → ny=round(1.372/0.025)+1=56。
+ std::vector 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 sorted = {-0.4, -0.2, 0.0, 0.2, 0.4};
+ std::vector 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& 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(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); // 跨度
#include
#include
+#include
#include
+#include
+#include
#include
#include
#include
@@ -71,12 +76,18 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
#include
#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -842,7 +853,8 @@ struct LineBuildResult {
// 异常(加载失败/立方体空/短桩线维度退化)由调用方捕获,不在此中断批量。
LineBuildResult buildOneLine(const std::string& lineDir,
const std::string& linePrefix,
- const std::string& out, int levels, int coarse) {
+ const std::string& out, int levels, int coarse,
+ double targetDy) {
LineBuildResult r;
r.prefix = linePrefix;
@@ -854,7 +866,7 @@ LineBuildResult buildOneLine(const std::string& lineDir,
Stopwatch swBridge;
geopro::io::gpr::BridgeMetrics bm;
geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeFromGpr3dv(
- lineDir, linePrefix, &bm, coarse);
+ lineDir, linePrefix, &bm, coarse, targetDy);
const double bridgeMs = swBridge.elapsedMs();
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(),
@@ -941,6 +953,161 @@ LineBuildResult buildOneLine(const std::string& lineDir,
return r;
}
+// ess-stat:实测一条线密体的"空体素跳过(ESS)潜力"——按块(默认 16³)算 min/max,统计有多少块
+// 整段值落在"近零背景带"(在 V 形不透明传函里≈透明)内 → 这些块 ESS 会整块跳过。空块占比≈ESS
+// 提速潜力。零依赖、跑在真实数据上,是上不上 OSPRay/ESS 的关键决策数。
+int cmdEssStat(int argc, char** argv) {
+ const Args a = parseArgs(argc, argv, 2);
+ if (a.positional.size() < 2) {
+ std::cerr << "用法: gpr_poc ess-stat [--coarse 8] "
+ "[--targetDy 0.025] [--block 16]\n";
+ return 2;
+ }
+ const std::string lineDir = a.positional[0];
+ const std::string prefix = a.positional[1];
+ const int coarse = std::stoi(a.get("coarse", "8"));
+ const double targetDy = std::stod(a.get("targetDy", "0.025"));
+ const int B = std::max(1, std::stoi(a.get("block", "16")));
+
+ geopro::io::gpr::BridgeMetrics bm;
+ geopro::core::BuiltI16 built =
+ geopro::io::gpr::buildLineVolumeFromGpr3dv(lineDir, prefix, &bm, coarse,
+ targetDy);
+ const auto& vol = built.vol;
+ const int nx = vol.nx(), ny = vol.ny(), nz = vol.nz();
+ const double center = built.quant.offset;
+ const double half =
+ std::max(1e-9, 0.5 * (built.vmaxPhys - built.vminPhys));
+ std::cout << "[ess-stat] " << prefix << " 体维度=" << nx << "x" << ny << "x"
+ << nz << " 值域=[" << built.vminPhys << "," << built.vmaxPhys
+ << "] 块=" << B << "³\n";
+
+ // 几个"透明带半宽"阈值(相对半值域):背景带越宽,可跳块越多(但太宽会跳掉弱反射)。
+ const double taus[] = {0.05, 0.10, 0.15, 0.20, 0.30};
+ constexpr int NT = 5;
+ long total = 0, skip[NT] = {0, 0, 0, 0, 0};
+ // 同时统计"近零体素占比"(单体素 < 0.1 半值域)作直觉参考。
+ long voxNear = 0, voxTotal = 0;
+
+ for (int z0 = 0; z0 < nz; z0 += B)
+ for (int y0 = 0; y0 < ny; y0 += B)
+ for (int x0 = 0; x0 < nx; x0 += B) {
+ short mn = std::numeric_limits::max();
+ short mx = std::numeric_limits::min();
+ const int x1 = std::min(x0 + B, nx), y1 = std::min(y0 + B, ny),
+ z1 = std::min(z0 + B, nz);
+ for (int z = z0; z < z1; ++z)
+ for (int y = y0; y < y1; ++y)
+ for (int x = x0; x < x1; ++x) {
+ const short v = vol.at(x, y, z);
+ if (v < mn) mn = v;
+ if (v > mx) mx = v;
+ const double p = built.quant.toPhys(v);
+ ++voxTotal;
+ if (std::abs(p - center) < 0.10 * half) ++voxNear;
+ }
+ ++total;
+ const double pmn = built.quant.toPhys(mn);
+ const double pmx = built.quant.toPhys(mx);
+ const double dev = std::max(std::abs(pmn - center), std::abs(pmx - center));
+ for (int t = 0; t < NT; ++t)
+ if (dev < taus[t] * half) ++skip[t];
+ }
+
+ std::cout << "[ess-stat] 近零体素占比(<0.1 半值域) = "
+ << (voxTotal ? 100.0 * voxNear / voxTotal : 0.0) << "%\n";
+ std::cout << "[ess-stat] 可跳块占比(块内全段落在透明带) / 对应 ESS 理论提速:\n";
+ for (int t = 0; t < NT; ++t) {
+ const double frac = total ? static_cast(skip[t]) / total : 0.0;
+ const double speedup = frac < 0.999 ? 1.0 / (1.0 - frac) : 999.0;
+ std::cout << " 透明带半宽 " << static_cast(taus[t] * 100)
+ << "% 半值域 → 可跳块 " << 100.0 * frac << "% → 理论上限 ~"
+ << speedup << "×\n";
+ }
+ std::cout << "[ess-stat] 注:理论上限=1/(1-可跳块占比),实际 OSPRay 还有 SIMD/光线相干等增益,"
+ "但非空块(反射层)仍要采。可跳块>80% 即 ESS 大幅值得上。\n";
+ return 0;
+}
+
+vtkSmartPointer makeOffscreenWindow(int w, int h); // 前置声明(定义在后)
+
+// passcost:确诊 P11/P12"各线独立 mapper + LOD 仍 0.5fps"卡在哪。
+// N 个独立 vtkSmartVolumeMapper(GPU),每个渲一个 size³ 体,测离屏稳态 fps vs N。
+// --overlap 0:N 个体【铺开】不重叠(光线各穿 ~1 个)→ 隔离"每遍固定开销"(嫌疑2);
+// --overlap 1:N 个体【叠在一起】重叠(光线穿 N 个)→ 隔离"重叠采样"(嫌疑3)。
+// 铺开随 N 线性掉=固定开销(嫌疑2,最坏);铺开不掉、叠加掉=重叠(嫌疑3);都不掉=选区没调小(嫌疑1,最好)。
+int cmdPassCost(int argc, char** argv) {
+ const Args a = parseArgs(argc, argv, 0);
+ const int S = std::stoi(a.get("size", "64")); // 每体边长(模拟 LOD 削小后的小区)
+ const bool overlap = std::stoi(a.get("overlap", "0")) != 0;
+ const int W = 1400, H = 900;
+
+ // 合成一个 S³ 体(含非平凡图案,避免被早终止/空跳优化掉)。
+ auto img = vtkSmartPointer::New();
+ img->SetDimensions(S, S, S);
+ img->SetSpacing(1.0, 1.0, 1.0);
+ vtkNew arr;
+ arr->SetName("v");
+ arr->SetNumberOfTuples(static_cast(S) * S * S);
+ for (int z = 0; z < S; ++z)
+ for (int y = 0; y < S; ++y)
+ for (int x = 0; x < S; ++x) {
+ const double v = 200.0 * std::sin(0.3 * x) * std::cos(0.3 * z) + 300.0;
+ arr->SetValue((static_cast(z) * S + y) * S + x,
+ static_cast(v));
+ }
+ img->GetPointData()->SetScalars(arr);
+
+ auto prop = vtkSmartPointer::New();
+ vtkNew col;
+ col->AddRGBPoint(0, 0.1, 0.1, 0.2);
+ col->AddRGBPoint(600, 0.95, 0.95, 0.9);
+ vtkNew op;
+ op->AddPoint(0, 0.0);
+ op->AddPoint(300, 0.05);
+ op->AddPoint(600, 0.4);
+ prop->SetColor(col);
+ prop->SetScalarOpacity(op);
+ prop->SetInterpolationTypeToLinear();
+
+ std::cout << "=== passcost:N 个独立 GPU mapper × " << S << "³ 体 ("
+ << (overlap ? "叠在一起/重叠" : "铺开/不重叠") << ") ===\n";
+ std::cout << "N fps(离屏稳态)\n";
+ for (int N : {1, 3, 5, 10, 20}) {
+ auto rw = makeOffscreenWindow(W, H);
+ vtkNew ren;
+ ren->SetBackground(0.05, 0.05, 0.08);
+ rw->AddRenderer(ren);
+ std::vector> ms;
+ std::vector> vs;
+ for (int n = 0; n < N; ++n) {
+ auto m = vtkSmartPointer::New();
+ m->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
+ m->SetInputData(img);
+ auto v = vtkSmartPointer::New();
+ v->SetMapper(m);
+ v->SetProperty(prop);
+ // 铺开:沿 X 排开 S 间距(不重叠);重叠:全堆在原点。
+ if (!overlap) v->SetPosition(static_cast(n) * S * 1.05, 0, 0);
+ ren->AddVolume(v);
+ ms.push_back(m);
+ vs.push_back(v);
+ }
+ ren->ResetCamera();
+ rw->Render(); // 预热(编译着色器/上传纹理)
+ rw->Render();
+ Stopwatch sw;
+ const int F = 30;
+ for (int f = 0; f < F; ++f) rw->Render();
+ const double ms_per = sw.elapsedMs();
+ const double fps = ms_per > 0 ? F * 1000.0 / ms_per : 0.0;
+ std::cout << N << " " << fps << "\n";
+ }
+ std::cout << "判读:铺开随 N 线性掉=每遍固定开销(嫌疑2,最坏,逼架构取舍);"
+ "铺开不掉/叠加掉=重叠(嫌疑3);都不怎么掉=0.5fps 是选区没调小(嫌疑1,最好,可修)。\n";
+ return 0;
+}
+
int cmdBuildLine(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.size() < 2) {
@@ -954,12 +1121,13 @@ int cmdBuildLine(int argc, char** argv) {
const std::string linePrefix = a.positional[1];
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
+ const double targetDy = std::stod(a.get("targetDy", "0.025"));
const std::string out =
a.get("out", (fs::temp_directory_path() / "gpr_store_line").string());
try {
const LineBuildResult r =
- buildOneLine(lineDir, linePrefix, out, levels, coarse);
+ buildOneLine(lineDir, linePrefix, out, levels, coarse, targetDy);
if (!r.ok) {
std::cerr << "[build-line] 跳过: " << r.reason << "\n";
return 1;
@@ -988,6 +1156,7 @@ int cmdBuildAll(int argc, char** argv) {
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
const double minFreeGB = std::stod(a.get("minFreeGB", "3"));
+ const double targetDy = std::stod(a.get("targetDy", "0.025"));
// 1) 发现所有测线前缀:扫 *__A.iprh,取 "<...>_" 部分(去 _A)。
std::set prefixSet;
@@ -1033,7 +1202,7 @@ int cmdBuildAll(int argc, char** argv) {
LineBuildResult r;
r.prefix = prefix;
try {
- r = buildOneLine(lineDir, prefix, out, levels, coarse);
+ r = buildOneLine(lineDir, prefix, out, levels, coarse, targetDy);
} catch (const std::exception& e) {
r.ok = false;
r.reason = std::string("异常: ") + e.what();
@@ -1523,17 +1692,26 @@ class CapturingOutputWindow : public vtkOutputWindow {
s.find("MAX_3D_TEXTURE_SIZE") != std::string::npos) {
textureError_ = true;
}
+ // 纹理【单元数】超限(multi-volume 一个包挂太多体)——与 3D 纹理【尺寸】超限区分,
+ // 供 view-all 自动减小每包体数(K)退避,直到本机硬件不再报错。
+ if (s.find("number of textures") != std::string::npos ||
+ s.find("Hardware does not support the number") != std::string::npos) {
+ textureCountError_ = true;
+ }
}
// 仍透传到 stderr,便于人工查看。
if (txt) std::cerr << txt;
}
bool textureError() const { return textureError_; }
+ bool textureCountError() const { return textureCountError_; }
+ void resetTextureCountError() { textureCountError_ = false; }
const std::string& captured() const { return captured_; }
private:
std::string captured_;
bool textureError_ = false;
+ bool textureCountError_ = false;
};
vtkStandardNewMacro(CapturingOutputWindow);
@@ -1795,12 +1973,18 @@ geopro::core::ColorScale makeGrayEnhancedColorScale(double vmin, double vmax) {
return cs;
}
+// 背景压制强度(--bgSuppress,0..1):0=原观感(近零背景压低但可见);越大→近零背景越
+// 透明 + 中心透明死区越宽,只留中/强反射层 → "压背景、突出反射"(业界标准传函做法)。
+// 注意:压太狠会连带抹掉弱异常(与 ESS 阈值同取舍);本系统弱异常靠切片抓,三维体压背景风险低。
+double gBgSuppress = 0.0;
+
// 「实体感」不透明度包络(Task 12d gallery):与 structural 双端斜坡不同,这里让
// 中高值段普遍可见——背景(近零)仍压低但不归零,中高段从 floorOpacity 平滑升到
// maxOpacity,使体读起来像半透明实心块、内部层次(而非只剩两端薄壳)可见。
// floorOpacity:近零背景的最低不透明度(0.05~0.12,压住但不消失)
// maxOpacity :强反射端的不透明度峰值(0.85 时近实心)
// midOpacity :中值段(半幅处)的不透明度(0.3~0.5,决定「半透明实心」观感)
+// gBgSuppress :见上,压低 floor + 加宽中心透明死区。
vtkSmartPointer makeSolidVolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys, double floorOpacity, double midOpacity,
@@ -1821,6 +2005,11 @@ vtkSmartPointer makeSolidVolumeProperty(
// 不透明度:V 形(中段=零附近背景=floor,正负两端=max),但全程 ≥floor 且中值
// 段≈mid → 整体半透明实心、内部层次可见,而非两端薄壳。
+ // --bgSuppress F:F>0 时把近零背景 floor 压到 floor*(1-F),并在中心开 ±(F*0.45*half) 的
+ // 全透明死区 → 压背景、突出中/强反射(F=0 即原观感)。
+ const double bg = std::clamp(gBgSuppress, 0.0, 1.0);
+ const double cFloor = floorOpacity * (1.0 - bg); // 背景压低(F→1 趋 0)
+ const double dead = bg * 0.45; // 中心透明死区半宽(占 half 的比例)
vtkNew opacity;
opacity->AddPoint(
static_cast(geopro::core::ScalarVolumeI16::kBlank), 0.0);
@@ -1828,7 +2017,13 @@ vtkSmartPointer makeSolidVolumeProperty(
const double half = 0.5 * (qmaxD - qminD);
opacity->AddPoint(qminD, maxOpacity); // 强负反射:近实心
opacity->AddPoint(qmid - 0.55 * half, midOpacity); // 中负段:半透明实心
- opacity->AddPoint(qmid, floorOpacity); // 近零背景:压低但可见
+ if (dead > 0.0) {
+ opacity->AddPoint(qmid - dead * half, 0.0); // 死区左沿:透明
+ opacity->AddPoint(qmid, 0.0); // 中心背景:透明
+ opacity->AddPoint(qmid + dead * half, 0.0); // 死区右沿:透明
+ } else {
+ opacity->AddPoint(qmid, cFloor); // 近零背景:压低但可见
+ }
opacity->AddPoint(qmid + 0.55 * half, midOpacity); // 中正段:半透明实心
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:近实心
@@ -3967,6 +4162,244 @@ int cmdViewGallery(const std::string& dir, int frames,
return rc;
}
+// buildLineProperty 定义在 view-all 段(本函数之后)→ 前置声明,供 slice 渲染体用。
+vtkSmartPointer buildLineProperty(
+ const geopro::data::StoreMeta& m, vtkImageData* basis, double sharedVmin,
+ double sharedVmax);
+
+// ============================================================================
+// slice:复用桌面端 SliceTool(同一份 geopro_render 代码,非重写)在 GPR 体上切片
+// ============================================================================
+// 加载一条线的体图 → 开真窗口 → 挂 SliceTool(4 种 axis:updown=深度切片C-scan/
+// frontback=纵向剖面radargram/leftright=横向剖面/oblique=任意斜切)。切面可拖动/旋转/滚轮推进。
+int cmdSlice(int argc, char** argv) {
+ using geopro::render::interact::SliceAxis;
+ using geopro::render::interact::SliceTool;
+ const Args a = parseArgs(argc, argv, 2); // 跳过 exe[0] + 命令名[1]
+ if (a.positional.empty()) {
+ std::cerr << "用法: gpr_poc slice [--axis updown|frontback|leftright|oblique] "
+ "[--exagg 8] [--bgSuppress 0.5]\n"
+ " 渲染半透明三维体 + 切面(在体内切,移动看不同剖面,同桌面端)。\n"
+ " updown=深度切片C-scan(水平面), frontback=纵向剖面radargram(沿线竖直面),\n"
+ " leftright=横向剖面(垂直线竖直面), oblique=任意角度斜切。\n"
+ " bgSuppress 越大体越透(更易看清体内切面)。\n";
+ return 2;
+ }
+ const std::string storeDir = a.positional[0];
+ const std::string axisStr = a.get("axis", "frontback");
+ const double exagg = std::stod(a.get("exagg", "8"));
+ const std::string shot = a.get("shot", ""); // 非空=离屏出图验证(不开真窗口)
+
+ geopro::render::ViewAdaptiveVolumeSource source(storeDir, /*exagg=*/1.0);
+ source.setAspect(1400.0 / 900.0);
+ source.setViewportHeight(900);
+ vtkImageData* base = source.baseImage();
+ if (base == nullptr) {
+ std::cerr << "[slice] 底图为空,无法切片(store 有效?)\n";
+ return 1;
+ }
+ const geopro::data::StoreMeta& meta = source.meta();
+
+ // 色阶与体绘制同口径(2/98 分位 + pickColor),保证切片配色与三维体一致。
+ const GalleryVariant& v = kViewDefaultVariant;
+ double vmin = meta.vminPhys, vmax = meta.vmaxPhys;
+ const ScalarPercentiles pc =
+ sampleScalarPercentiles(base, meta.quant, 0.02, 0.98);
+ if (pc.samples > 0) {
+ vmin = pc.lo;
+ vmax = pc.hi;
+ }
+ const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
+
+ // 把 exagg 烤进 Z spacing(显示用,与桌面端"VE 烤入 origin/spacing"同口径)。
+ // ShallowCopy 不动 source 的底图;SliceTool 持非拥有指针,img 在本函数作用域内保活。
+ auto img = vtkSmartPointer::New();
+ img->ShallowCopy(base);
+ double sp[3];
+ img->GetSpacing(sp);
+ img->SetSpacing(sp[0], sp[1], sp[2] * exagg);
+
+ SliceAxis axis = SliceAxis::UpDown;
+ if (axisStr == "frontback") axis = SliceAxis::FrontBack;
+ else if (axisStr == "leftright") axis = SliceAxis::LeftRight;
+ else if (axisStr == "oblique") axis = SliceAxis::Oblique;
+
+ vtkNew ren;
+ ren->SetBackground(0.06, 0.06, 0.09);
+ vtkNew rw;
+ rw->AddRenderer(ren);
+ rw->SetSize(1400, 900);
+ rw->SetWindowName("gpr_poc slice —— 三维体 + 切面(复用桌面端 SliceTool)");
+ if (!shot.empty()) rw->SetOffScreenRendering(1); // 验证模式:离屏
+
+ // 【关键】渲染三维体本身——桌面端切片是"在渲染出的三维体上切",切面在体内切、移动看不同剖面。
+ // 体设半透明(bgSuppress 默认压背景)→ 能透过体看见体内的切面。
+ gBgSuppress = std::clamp(std::stod(a.get("bgSuppress", "0.5")), 0.0, 1.0);
+ vtkSmartPointer volProp = buildLineProperty(meta, base, vmin, vmax);
+ vtkNew volMapper;
+ volMapper->SetInputData(img);
+ vtkNew vol;
+ vol->SetMapper(volMapper);
+ vol->SetProperty(volProp);
+ ren->AddVolume(vol);
+
+ // 体包围盒轮廓(上下文:看切面在体里的位置)。
+ vtkNew outline;
+ outline->SetInputData(img);
+ vtkNew omap;
+ omap->SetInputConnection(outline->GetOutputPort());
+ vtkNew oact;
+ oact->SetMapper(omap);
+ oact->GetProperty()->SetColor(0.4, 0.4, 0.5);
+ ren->AddActor(oact);
+
+ vtkNew iren;
+ iren->SetRenderWindow(rw);
+ vtkNew style;
+ iren->SetInteractorStyle(style);
+ iren->Initialize(); // SliceTool 构造即 On(),须先有活 interactor
+
+ // 复用桌面端 SliceTool(geopro_render 同一份代码)。
+ SliceTool slice(img, iren, axis, cs, vmin, vmax);
+
+ int d[3];
+ img->GetDimensions(d);
+ std::cout << "[slice] axis=" << axisStr << " 体维度=" << d[0] << "x" << d[1]
+ << "x" << d[2] << " 值域=[" << vmin << "," << vmax << "]\n";
+
+ // 相机正对切面(沿法向看,否则极扁的体侧视成一条线、看不到切片纹理)。
+ double bnd[6];
+ img->GetBounds(bnd);
+ const double cx = 0.5 * (bnd[0] + bnd[1]), cy = 0.5 * (bnd[2] + bnd[3]),
+ cz = 0.5 * (bnd[4] + bnd[5]);
+ vtkCamera* cam = ren->GetActiveCamera();
+ cam->SetFocalPoint(cx, cy, cz);
+ if (axis == SliceAxis::UpDown) { // X-Y 面,法向 Z → 俯视
+ cam->SetPosition(cx, cy, cz + 1.0);
+ cam->SetViewUp(0, 1, 0);
+ } else if (axis == SliceAxis::LeftRight) { // Y-Z 面,法向 X → 侧视
+ cam->SetPosition(cx + 1.0, cy, cz);
+ cam->SetViewUp(0, 0, 1);
+ } else { // FrontBack/Oblique:X-Z 面,法向 Y → 正视(radargram)
+ cam->SetPosition(cx, cy - 1.0, cz);
+ cam->SetViewUp(0, 0, 1);
+ }
+ ren->ResetCamera(); // 沿该朝向拟合距离框住体
+ if (!shot.empty()) {
+ rw->Render();
+ savePng(rw, shot);
+ std::cout << "[slice] 离屏出图: " << shot << "(axis=" << axisStr << ")\n";
+ return 0;
+ }
+ std::cout << "[slice] 打开真窗口(半透明三维体 + 切面)。左键旋转 / 滚轮缩放 / "
+ "拖切面移动看不同剖面 / q 退出。\n";
+ rw->Render();
+ iren->Start();
+ // 干净拆除:先 Off 切面 widget,再 Finalize GL 上下文,避免关窗时 widget 在死上下文上
+ // 重建 shader 报错(depth-blit / texture 告警)。
+ slice.close();
+ rw->Finalize();
+ std::cout << "[slice] 窗口关闭,退出。\n";
+ return 0;
+}
+
+// ============================================================================
+// C-scan 深度切片(--slice):在 20 条合成体上加"水平深度切面"——每条线贡献它在该深度的
+// 水平幅值图,按各自世界变换摆进场景 → 拼成"整条路某深度的俯视图"(GPR 找管线/空洞的主力视图)。
+// 深度方向(Z)在"绕竖直 Z 旋转 + 平移"的摆放下不变,故一个世界深度面 = 各线同一 z 切片,
+// 直接用 vtkImageActor(显示 z=k 切片) + SetUserTransform(线的世界变换) 摆位,无需斜切 reslice。
+// 键盘 Up/Down 改深度 → 扫过不同深度。逐线真实数据、不合并、空隙透明。
+// ============================================================================
+// 全局世界切面:定义【一个】世界轴对齐的薄板(slab),把【每条线】用各自 worldInv 作 ResliceTransform
+// reslice 到这【同一个世界面】上采样(覆盖外=透明),再 blend 合成一张 → 真正一整片切面。
+// 整片沿扫描轴移动 = 所有线被同一个面同步扫过(无论朝向)。
+// depth(updown):沿 world Z 扫、面=X-Y;cross(leftright):沿 world Y 扫、面=X-Z(横穿所有线);
+// radargram(frontback):沿 world X 扫、面=Y-Z。
+struct GlobalSlice {
+ // 竖直剖面(横切/顺路):全局面 reslice 各线 → 同一世界面。
+ std::vector> reslices;
+ // 深度(水平):逐线整张水平切面(共面、全覆盖、原生分辨率)。lineActors 非空即此模式。
+ std::vector> lineActors;
+ std::vector> winv; // 各线 world→local(算深度 z 索引)
+ std::vector> lorg, lspc;
+ std::vector> ldim;
+ int sweepAxis = 1; // world 扫描轴 0=X 1=Y 2=Z
+ int inA = 0, inB = 2; // 面内两个 world 轴
+ double sweepWorld = 0, sweepMin = 0, sweepMax = 1;
+ double footLo[3] = {0, 0, 0}, footHi[3] = {0, 0, 0}; // footprint AABB(取景用)
+ vtkSmartPointer planeSrc; // 可见的切面矩形(淡),让"面"看得见
+ vtkRenderWindow* rw = nullptr;
+ std::vector volumes;
+ double volUnit = 20.0;
+ bool volOn = true;
+
+ void applySlice() {
+ if (!lineActors.empty()) { // 深度逐线模式:每线设 z=k 那一片(共面、全覆盖)
+ for (std::size_t i = 0; i < lineActors.size(); ++i) {
+ double W[3] = {0.5 * (footLo[0] + footHi[0]), 0.5 * (footLo[1] + footHi[1]),
+ 0.5 * (footLo[2] + footHi[2])};
+ W[sweepAxis] = sweepWorld;
+ double L[3];
+ winv[i]->TransformPoint(W, L);
+ const int k = std::clamp(
+ static_cast(std::lround((L[2] - lorg[i][2]) / lspc[i][2])), 0,
+ ldim[i][2] - 1);
+ lineActors[i]->SetDisplayExtent(0, ldim[i][0] - 1, 0, ldim[i][1] - 1, k, k);
+ }
+ } else {
+ for (auto& rs : reslices) {
+ double o[3];
+ rs->GetOutputOrigin(o);
+ o[sweepAxis] = sweepWorld; // 整片世界面沿扫描轴移动(所有线同步)
+ rs->SetOutputOrigin(o);
+ }
+ }
+ if (planeSrc) { // 同步移动可见的切面矩形
+ double o[3] = {footLo[0], footLo[1], footLo[2]};
+ o[sweepAxis] = sweepWorld;
+ double p1[3] = {o[0], o[1], o[2]}, p2[3] = {o[0], o[1], o[2]};
+ p1[inA] = footHi[inA];
+ p2[inB] = footHi[inB];
+ planeSrc->SetOrigin(o);
+ planeSrc->SetPoint1(p1);
+ planeSrc->SetPoint2(p2);
+ }
+ if (rw) rw->Render();
+ }
+ void applyVolume() {
+ for (auto* v : volumes) {
+ if (v == nullptr) continue;
+ v->SetVisibility(volOn ? 1 : 0);
+ if (v->GetProperty())
+ v->GetProperty()->SetScalarOpacityUnitDistance(volUnit);
+ }
+ if (rw) rw->Render();
+ }
+};
+
+void cscanOnKey(vtkObject* caller, unsigned long, void* cd, void*) {
+ auto* st = static_cast(cd);
+ auto* iren = static_cast(caller);
+ const std::string key = iren->GetKeySym() ? iren->GetKeySym() : "";
+ const double step = (st->sweepMax - st->sweepMin) * 0.02;
+ if (key == "Up") {
+ st->sweepWorld = std::min(st->sweepMax, st->sweepWorld + step);
+ st->applySlice();
+ } else if (key == "Down") {
+ st->sweepWorld = std::max(st->sweepMin, st->sweepWorld - step);
+ st->applySlice();
+ } else if (key == "v" || key == "V") {
+ st->volOn = !st->volOn;
+ st->applyVolume();
+ } else if (key == "bracketright") {
+ st->volUnit *= 1.5;
+ st->applyVolume();
+ } else if (key == "bracketleft") {
+ st->volUnit = std::max(0.5, st->volUnit / 1.5);
+ st->applyVolume();
+ }
+}
+
// ============================================================================
// view-all:全部独立体按精确 CGCS2000 坐标/朝向摆进同一 3D 场景一起渲(测区全貌)(P7→P9)
// ============================================================================
@@ -4003,15 +4436,25 @@ struct PlacedSource {
vtkSmartPointer world; // T:Scale(1,1,exagg)→RotateZ→Translate
vtkSmartPointer worldInv; // T⁻¹(相机逆变换到局部帧)
vtkSmartPointer prop; // 逐线 2/98 分位标定的传函
- // P12:每线只渲【一个】体(不再底图层 + 高清层叠双渲,20 线=40 体爆 1fps)。
- // 该体输入由引擎按相机选的 LOD 决定:起步喂 baseImage()(粗 whole,小且不空),
- // 引擎备好更合适的 currentImages() 后整图换上 → 任何时刻每线 ≤1 体 → 最多 20 体。
- vtkSmartPointer volume; // 唯一体(套 T)
- vtkSmartPointer mapper;
+ // 单遍合成(方案 A):每条线仍是【独立插值】的体,但 20 条不再各自一个 mapper(=20 遍
+ // ray-cast,物理 20× 卡死),而是全部作为【同一个 vtkGPUVolumeRayCastMapper 的不同
+ // 端口】注册进一个 vtkMultiVolume → 单遍 ray-cast 一次性合成(重叠也只穿一遍)。
+ // ps.volume 只承载该端口的世界变换 T + 逐线传函(不再持自己的 mapper);该端口的
+ // vtkImageData 由引擎按相机选的 LOD 决定,经 multiMapper->SetInputDataObject(port,img) 换上。
+ vtkSmartPointer volume; // 该线的体(套 T + 逐线 prop)
vtkSmartPointer currentImg; // 持当前单图引用(mapper 仅持裸指针)
- double worldBounds[6] = {0, 0, 0, 0, 0, 0}; // 该线(含 T+底图盒)的世界 AABB(视锥裁剪用)
- bool culled = false; // 本帧是否被视锥裁掉(两层皆隐 → 真跳过)
+ // LOD 中心架构(确诊后):每条线【独立】自己的 mapper(passcost 证明 N 遍开销温和),
+ // 配视野 LOD 引擎换区 + 停手才重建。弃 multi-volume 单遍(它为单遍关掉了 LOD→渲整卷大贴图→卡)。
+ vtkSmartPointer mapper; // 每线独立 GPU mapper(弃 multi-volume 单遍)
+ vtkGPUVolumeRayCastMapper* multiMapper = nullptr; // 旧 multi-volume 残留(保编译,不再用)
+ vtkMultiVolume* multiVol = nullptr; // 旧残留
+ int port = 0; // 旧残留
+
+ vtkSmartPointer fullYImg; // §3:全 Y 密度底图(通道 LOD 抽平面的源)
+ int yStride = 1; // §3:当前通道维 LOD stride(1=全密度)
+
+ double worldBounds[6] = {0, 0, 0, 0, 0, 0}; // 该线(含 T+底图盒)的世界 AABB(取景用)
};
// 由 起点+航向+Z 夸张 → 世界刚体变换 T。
@@ -4032,10 +4475,15 @@ vtkSmartPointer makeLineTransform(double startX, double startY,
// 逐线传函:从该线常驻底图(整卷代表)实测 2/98 分位标定色阶/不透明度端点 + 梯度门,
// 与单条 view 的传函标定同口径(底图非空恒可标定;退化回退全量化域)。
vtkSmartPointer buildLineProperty(
- const geopro::data::StoreMeta& m, vtkImageData* basis) {
+ const geopro::data::StoreMeta& m, vtkImageData* basis,
+ double sharedVmin = 0.0, double sharedVmax = 0.0) {
const GalleryVariant& v = kViewDefaultVariant; // P4 默认醒目版(var4)
double vmin = m.vminPhys, vmax = m.vmaxPhys;
- if (basis != nullptr) {
+ if (sharedVmin < sharedVmax) {
+ // #6:传入【统一物理幅值范围】→ 同一幅值在所有线上映射到同一颜色(跨线可比)。
+ vmin = sharedVmin;
+ vmax = sharedVmax;
+ } else if (basis != nullptr) {
const ScalarPercentiles pc =
sampleScalarPercentiles(basis, m.quant, 0.02, 0.98);
if (pc.samples > 0) {
@@ -4051,12 +4499,144 @@ vtkSmartPointer buildLineProperty(
: nullptr);
}
+// 根因验证用:把一张体沿 Y(通道维)抽半 → 同范围、同 X/Z/origin,仅 Y 平面数减半、
+// Y spacing 翻倍。这正是"通道 LOD 换密度"的等价改动(同范围、不同 Y 密度),用来确定
+// 就地换端口贴图在 multi-volume 里能否算对。
+vtkSmartPointer downsampleY(vtkImageData* in) {
+ int d[3];
+ in->GetDimensions(d);
+ double sp[3], org[3];
+ in->GetSpacing(sp);
+ in->GetOrigin(org);
+ const int ny2 = std::max(1, (d[1] + 1) / 2);
+ auto out = vtkSmartPointer::New();
+ out->SetDimensions(d[0], ny2, d[2]);
+ out->SetOrigin(org);
+ out->SetSpacing(sp[0], sp[1] * 2.0, sp[2]); // Y 间距翻倍(平面数减半)
+ vtkNew arr;
+ arr->SetName("v");
+ arr->SetNumberOfTuples(static_cast(d[0]) * ny2 * d[2]);
+ auto* inArr = vtkShortArray::SafeDownCast(in->GetPointData()->GetScalars());
+ for (int k = 0; k < d[2]; ++k)
+ for (int j2 = 0; j2 < ny2; ++j2) {
+ const int j = std::min(j2 * 2, d[1] - 1);
+ for (int i = 0; i < d[0]; ++i) {
+ const vtkIdType idIn =
+ (static_cast(k) * d[1] + j) * d[0] + i;
+ const vtkIdType idOut =
+ (static_cast(k) * ny2 + j2) * d[0] + i;
+ arr->SetValue(idOut, inArr ? inArr->GetValue(idIn) : 0);
+ }
+ }
+ out->GetPointData()->SetScalars(arr);
+ return out;
+}
+
+// §3 通道维 LOD:沿 Y(通道维)按 stride 抽平面 → 同范围、保包围盒(ny'×spY×stride≈ny×spY),
+// stride=1 即原图。stride∈{1,2,4,8} → 显示 全/半/1-4/1-8 通道密度。bbox 不变 → multi-volume
+// 就地换不破坏(已 --swapTest 验证)。stride==1 时直接返回原图(零拷贝)。
+vtkSmartPointer subsampleYStride(vtkImageData* in, int stride) {
+ if (in == nullptr || stride <= 1) return in;
+ int d[3];
+ in->GetDimensions(d);
+ double sp[3], org[3];
+ in->GetSpacing(sp);
+ in->GetOrigin(org);
+ const int ny2 = std::max(1, (d[1] + stride - 1) / stride);
+ auto out = vtkSmartPointer::New();
+ out->SetDimensions(d[0], ny2, d[2]);
+ out->SetOrigin(org);
+ out->SetSpacing(sp[0], sp[1] * stride, sp[2]); // Y 间距 ×stride → 跨度不变
+ vtkNew arr;
+ arr->SetName("v");
+ arr->SetNumberOfTuples(static_cast(d[0]) * ny2 * d[2]);
+ auto* inArr = vtkShortArray::SafeDownCast(in->GetPointData()->GetScalars());
+ for (int k = 0; k < d[2]; ++k)
+ for (int j2 = 0; j2 < ny2; ++j2) {
+ const int j = std::min(j2 * stride, d[1] - 1);
+ for (int i = 0; i < d[0]; ++i) {
+ const vtkIdType idIn =
+ (static_cast(k) * d[1] + j) * d[0] + i;
+ const vtkIdType idOut =
+ (static_cast(k) * ny2 + j2) * d[0] + i;
+ arr->SetValue(idOut, inArr ? inArr->GetValue(idIn) : 0);
+ }
+ }
+ out->GetPointData()->SetScalars(arr);
+ return out;
+}
+
+// 根因验证用:把一张体沿 X 裁掉后半 → 范围/包围盒改变(模拟引擎换"子区域"),用来确认
+// "改包围盒的换图"才是破坏 multi-volume 的真凶。
+vtkSmartPointer cropXhalf(vtkImageData* in) {
+ int d[3];
+ in->GetDimensions(d);
+ double sp[3], org[3];
+ in->GetSpacing(sp);
+ in->GetOrigin(org);
+ const int nx2 = std::max(1, d[0] / 2);
+ auto out = vtkSmartPointer::New();
+ out->SetDimensions(nx2, d[1], d[2]);
+ out->SetOrigin(org); // 保 origin,但 X 范围缩到一半 → 包围盒变
+ out->SetSpacing(sp);
+ vtkNew arr;
+ arr->SetName("v");
+ arr->SetNumberOfTuples(static_cast(nx2) * d[1] * d[2]);
+ auto* inArr = vtkShortArray::SafeDownCast(in->GetPointData()->GetScalars());
+ for (int k = 0; k < d[2]; ++k)
+ for (int j = 0; j < d[1]; ++j)
+ for (int i = 0; i < nx2; ++i) {
+ const vtkIdType idIn = (static_cast(k) * d[1] + j) * d[0] + i;
+ const vtkIdType idOut = (static_cast(k) * d[1] + j) * nx2 + i;
+ arr->SetValue(idOut, inArr ? inArr->GetValue(idIn) : 0);
+ }
+ out->GetPointData()->SetScalars(arr);
+ return out;
+}
+
+// #6 色标图例:按【统一物理幅值范围】+ 默认配色建一个屏幕右侧色阶条(含刻度),让用户
+// 知道颜色代表多大的幅值。配色与体属性同源(pickColor),故图例颜色与体内颜色一致。
+vtkSmartPointer buildScalarBar(double vmin, double vmax) {
+ const GalleryVariant& v = kViewDefaultVariant;
+ const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
+ auto lut = vtkSmartPointer::New();
+ constexpr int N = 64;
+ for (int t = 0; t < N; ++t) {
+ const double phys = vmin + (vmax - vmin) * t / (N - 1);
+ const auto c = cs.colorAt(phys);
+ lut->AddRGBPoint(phys, c.r / 255.0, c.g / 255.0, c.b / 255.0);
+ }
+ auto bar = vtkSmartPointer::New();
+ bar->SetLookupTable(lut);
+ bar->SetTitle("Amplitude"); // ASCII(默认字体无 CJK,避免乱码)
+ bar->SetNumberOfLabels(5);
+ bar->SetMaximumWidthInPixels(80);
+ bar->GetTitleTextProperty()->SetColor(1, 1, 1);
+ bar->GetLabelTextProperty()->SetColor(1, 1, 1);
+ bar->SetPosition(0.91, 0.28);
+ bar->SetWidth(0.08);
+ bar->SetHeight(0.46);
+ return bar;
+}
+
// 把世界相机参数逆变换到某线局部帧(T⁻¹):pos/focal 是点(含平移逆),up 是方向
// (仅旋转逆,TransformVector 不含平移)。再调引擎 updateView 选层选区(视锥外→引擎
// 内部 selectLod 判 empty → 不提交,保留上一就绪/无图)。
+// 诊断开关:置 1 时跳过引擎选层/换图,全程只渲各线 baseImage(粗整卷)。
+// 用于隔离"首帧连续→引擎换图后断开/消失"——若 baseOnly 始终连续,则根因是引擎换图。
+bool gViewAllBaseOnly = false;
+
+// 根因验证开关:就地换端口贴图后施加何种"失效/重算"策略,看能否让 multi-volume 算对。
+// 0=不做(基线,已知会断开) 1=multiVol.Modified() 2=重 SetVolume+Modified
+// 3=2 + mapper.Modified() 4=3 + 强制 multiVol.GetBounds() 重算
+int gViewAllSwapFix = 0;
+
+// §3 通道维 LOD:按相机距离选 Y stride(远疏近密),保包围盒就地换 Y 平面子集。--chanLod 0 关。
+bool gChanLod = true;
+
void viewAllSubmitOneLine(PlacedSource& ps, vtkCamera* worldCam,
double aspect, int viewportH) {
- if (ps.culled || worldCam == nullptr) return;
+ if (gViewAllBaseOnly || worldCam == nullptr) return;
double wp[3], wf[3], wu[3];
worldCam->GetPosition(wp);
worldCam->GetFocalPoint(wf);
@@ -4083,112 +4663,149 @@ void viewAllSubmitOneLine(PlacedSource& ps, vtkCamera* worldCam,
1.0});
}
-// 非阻塞拉取该线后台已就绪的引擎单图,换上唯一体的 mapper(无新结果→沿用上一帧)。
-// 返回 1=换上新图。P12:单体单层,引擎选 LOD(远→粗 whole,近→细局部)。
+// 非阻塞拉取该线后台已就绪的引擎单图(视野 LOD 选区),换上该线【自己的 mapper】
+// (无新结果→沿用上一帧)。返回 1=换上新图。引擎选 LOD(远→粗 whole,近→细局部小区)。
+// 各线独立 mapper → 换"改包围盒的子区域"安全(无 multi-volume 可破坏)。
int viewAllPickOneLine(PlacedSource& ps) {
- if (ps.culled) return 0;
+ if (gViewAllBaseOnly) return 0; // 诊断:不换图,保持 baseImage
auto imgs = ps.source->currentImages(); // 内部 takeLatest(非阻塞)
if (imgs.empty() || imgs[0] == nullptr) return 0;
if (imgs[0] == ps.currentImg) return 0;
ps.currentImg = imgs[0];
- ps.mapper->SetInputData(ps.currentImg);
- ps.mapper->Update();
- return 1;
-}
-
-// 视锥裁剪(#2):把该线世界 AABB 与相机 6 个视锥面比对,整盒在任一面外侧 → 裁掉
-// (base+hires 两层皆隐 → 该线本帧完全不渲,省下解压/重组/ray-march)。盒 8 角全在
-// 某面负半空间才裁(保守,绝不误裁部分可见的线)。
-bool aabbOutsideFrustum(const double b[6], const double planes[24]) {
- for (int p = 0; p < 6; ++p) {
- const double a = planes[p * 4 + 0], bb = planes[p * 4 + 1],
- cc = planes[p * 4 + 2], dd = planes[p * 4 + 3];
- bool allOut = true;
- for (int cx = 0; cx < 2 && allOut; ++cx)
- for (int cy = 0; cy < 2 && allOut; ++cy)
- for (int cz = 0; cz < 2 && allOut; ++cz) {
- const double x = b[cx], y = b[2 + cy], z = b[4 + cz];
- if (a * x + bb * y + cc * z + dd >= 0.0) allOut = false; // 该角在面内
- }
- if (allOut) return true; // 全 8 角在该面外 → 整盒在视锥外
+ if (ps.mapper) {
+ ps.mapper->SetInputData(ps.currentImg);
+ ps.mapper->Update();
}
- return false;
+ return 1;
}
// view-all 每帧驱动共享状态(挂 interactor 回调)。
struct ViewAllState {
std::vector* lines = nullptr;
+ std::vector>* mappers = nullptr;
vtkRenderer* ren = nullptr;
vtkCamera* cam = nullptr;
vtkRenderWindow* rw = nullptr;
vtkTextActor* fpsText = nullptr;
double aspect = 1400.0 / 900.0;
int viewportH = 900;
+ double dragImgSample = 4.0; // 交互态屏幕降采样倍率(--maxImgSample)
+ double sampleDist = 0.3; // 静止态沿光线步长(世界米,--sampleDist;越大越快越糙)
+ double dragSampleMul = 3.0; // 交互态沿光线步长再放大倍数(--dragSampleMul)
+ bool lowQ = false; // 当前是否处于低质(交互)态
+ int idleTicks = 0; // 连续无交互的定时器 tick 数(>=阈值 → 恢复全质量)
bool inCb = false;
};
-// 重算各线视锥可见性(裁屏外线)+ 对可见线提交引擎目标(非阻塞)。culled 线两层皆隐。
-void viewAllRefreshFrustum(ViewAllState* st) {
- double planes[24];
- st->cam->GetFrustumPlanes(st->aspect, planes);
- for (PlacedSource& ps : *st->lines) {
- const bool outside = aabbOutsideFrustum(ps.worldBounds, planes);
- ps.culled = outside;
- // P12:每线唯一体。视锥外→隐(真跳过 ray-cast);可见→显并提交引擎目标(局部帧)。
- ps.volume->SetVisibility(outside ? 0 : 1);
- if (!outside) viewAllSubmitOneLine(ps, st->cam, st->aspect, st->viewportH);
+// 交互态(拖动/滚轮):屏幕降采样(1/dragImgSample) + 沿光线步长加粗(sampleDist×dragSampleMul)。
+// 关键:AutoAdjust 只降屏幕、不降沿光线步长;20 个体每步采 20 次,沿光线步长才是大头,
+// 故交互时必须手动把沿光线步长也加粗(否则拖动仍卡)。
+void viewAllSetWheelCoarse(ViewAllState* st) {
+ if (!st->mappers) return;
+ for (auto& mm : *st->mappers) {
+ mm->SetImageSampleDistance(st->dragImgSample);
+ mm->SetSampleDistance(static_cast(st->sampleDist * st->dragSampleMul));
}
+ st->lowQ = true;
}
-// 交互进行中:只做视锥裁剪(便宜,仅切可见性),绝不提交引擎目标/重建纹理。
-// 拖动中频繁提交=20 条体反复重建+上传 GPU→GPU 100% thrash→卡死。拖动只渲已有
-// 纹理(mapper 降采样保流畅);重建只在松手(EndInteraction)触发一次。
+// 静止态:全屏幕分辨率 + 沿光线步长回到 sampleDist(清晰)。AutoAdjust 全程关(手动控)。
+void viewAllRestoreAdaptive(ViewAllState* st) {
+ if (!st->mappers) return;
+ for (auto& mm : *st->mappers) {
+ mm->SetImageSampleDistance(1.0);
+ mm->SetSampleDistance(static_cast(st->sampleDist));
+ }
+ st->lowQ = false;
+}
+
+// §3 通道维 LOD:按相机距离/场景对角线选 Y stride(远→疏省、近→全密度)。
+int viewAllChanStride(vtkRenderer* ren) {
+ double b[6];
+ ren->ComputeVisiblePropBounds(b);
+ const double diag = std::sqrt((b[1] - b[0]) * (b[1] - b[0]) +
+ (b[3] - b[2]) * (b[3] - b[2]) +
+ (b[5] - b[4]) * (b[5] - b[4]));
+ const double dist = ren->GetActiveCamera()->GetDistance();
+ const double r = diag > 1e-9 ? dist / diag : 1.0;
+ if (r > 1.4) return 8; // 远 → 半通道密度(7 平面/56)
+ if (r > 0.8) return 4; // 中 → 原始通道(14)
+ if (r > 0.45) return 2; // 近 → 原始+1(28)
+ return 1; // 最近 → 全插值(56)
+}
+
+// 按当前相机选 stride,对各线就地换 Y 平面子集(保包围盒 → multi-volume 不破坏)。
+// 返回换图线数(>0 需重渲)。
+int viewAllApplyChanLod(ViewAllState* st) {
+ if (!gChanLod) return 0;
+ const int stride = viewAllChanStride(st->ren);
+ int changed = 0;
+ for (PlacedSource& ps : *st->lines) {
+ if (ps.fullYImg == nullptr || ps.yStride == stride) continue;
+ ps.yStride = stride;
+ ps.currentImg = subsampleYStride(ps.fullYImg, stride);
+ if (ps.multiMapper)
+ ps.multiMapper->SetInputDataObject(ps.port, ps.currentImg);
+ ++changed; // 保包围盒换图,无需失效重算
+ }
+ return changed;
+}
+
+// 对所有线提交引擎目标(非阻塞,按当前相机各自选 LOD)。
+// 方案 A 起:20 条共享【单个】vtkGPUVolumeRayCastMapper 单遍合成,重叠也只穿一遍 →
+// 不再需要视锥裁剪削 20× 开销(且各线 AABB 都覆盖全路、互相重叠,裁剪本就几乎无效)。
+void viewAllSubmitTargets(ViewAllState* st) {
+ for (PlacedSource& ps : *st->lines)
+ viewAllSubmitOneLine(ps, st->cam, st->aspect, st->viewportH);
+}
+
+// 拖动进行中(InteractionEvent):质量交给 AutoAdjust 自适应(够快就清晰,不够才降)。
+// 拖动中:进入交互态低质(屏幕 + 沿光线步长都加粗)+ 重置裁剪范围(防旋转裁掉)。绝不重建纹理。
void viewAllOnInteracting(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast(clientData);
- double planes[24];
- st->cam->GetFrustumPlanes(st->aspect, planes);
- for (PlacedSource& ps : *st->lines) {
- const bool outside = aabbOutsideFrustum(ps.worldBounds, planes);
- ps.culled = outside;
- ps.volume->SetVisibility(outside ? 0 : 1);
- }
+ if (!st->lowQ) viewAllSetWheelCoarse(st); // 进入交互态低质(拖动跟手)
+ st->idleTicks = 0;
+ if (st->ren) st->ren->ResetCameraClippingRange();
}
-// 定时器:非阻塞拉取各可见线后台已就绪的新高清纹理换上 → 有新图才重渲。
+// 滚轮缩放(MouseWheelForward/BackwardEvent):立刻固定低分辨率(AutoAdjust 单帧来不及
+// 自适应,故手动降)。高优先级先于 style 的 Dolly+Render → 滚轮那一帧即低质快渲。
+void viewAllOnWheel(vtkObject*, unsigned long, void* clientData, void*) {
+ auto* st = static_cast(clientData);
+ viewAllSetWheelCoarse(st);
+ st->idleTicks = 0;
+ if (st->ren) st->ren->ResetCameraClippingRange();
+}
+
+// 定时器:滚轮停手约 idle 后恢复自适应 + 全质量重渲。
void viewAllOnTimer(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast(clientData);
if (st->inCb) return;
- int changed = 0;
- for (PlacedSource& ps : *st->lines) changed += viewAllPickOneLine(ps);
- if (changed > 0) {
+ if (st->lowQ && ++st->idleTicks >= 3) { // ~3×定时器周期无交互 → 恢复
+ viewAllRestoreAdaptive(st);
+ viewAllApplyChanLod(st); // §3:滚轮缩放停手后按新距离重选通道密度
st->ren->ResetCameraClippingRange();
st->rw->Render();
}
}
-// 交互结束:重算视锥 + 提交 + 拉取 + 刷新 fps(仅松手触发一次)。
+// 拖动结束(EndInteractionEvent):恢复自适应(静止 AutoAdjust→全质量)+ 刷新 fps。
void viewAllOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast(clientData);
if (st->inCb) return;
st->inCb = true;
- viewAllRefreshFrustum(st);
- int culledN = 0, visN = 0;
- for (PlacedSource& ps : *st->lines) {
- viewAllPickOneLine(ps);
- if (ps.culled) ++culledN; else ++visN;
- }
+ viewAllRestoreAdaptive(st);
+ viewAllApplyChanLod(st); // §3:拖动结束后按当前距离重选通道密度
+ st->idleTicks = 0;
st->ren->ResetCameraClippingRange();
- constexpr int kFpsProbeFrames = 3;
Stopwatch swR;
- for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render();
- const double fps = swR.elapsedMs() > 0
- ? 1000.0 * kFpsProbeFrames / swR.elapsedMs()
- : 0.0;
+ st->rw->Render(); // 一帧全质量;其耗时即静止 fps
+ const double fps = swR.elapsedMs() > 0 ? 1000.0 / swR.elapsedMs() : 0.0;
char buf[256];
std::snprintf(buf, sizeof(buf),
- "fps: %.1f | visible lines: %d | culled: %d", fps, visN,
- culledN);
+ "fps(静止): %.1f | lines: %d | 单遍合成(multi-volume)", fps,
+ static_cast(st->lines->size()));
if (st->fpsText) st->fpsText->SetInput(buf);
st->rw->Render();
st->inCb = false;
@@ -4213,6 +4830,30 @@ int cmdViewAll(int argc, char** argv) {
const double spread = std::stod(a.get("spread", "0"));
const bool preview = a.kv.count("preview") > 0;
const std::string shotDir = a.get("shotDir", storesDir);
+ // --maxPerPass N:每个 vtkMultiVolume 单遍合成最多挂几条体(受 GPU 每着色器纹理单元
+ // 上限约束,每体约吃 4 个单元)。0=自动按 GPU 纹理单元数推断(推荐)。超过的线分成
+ // 多个 multi-volume 包,每包一遍 → 总遍数=ceil(N/K)(远少于"每线一遍"的 N 遍)。
+ const int maxPerPassArg = std::stoi(a.get("maxPerPass", "0"));
+ // LOD 中心架构(passcost 确诊后):各线独立 mapper → 引擎换 LOD 子区【安全】(无 multi-volume
+ // 可破坏)。故视野 LOD【默认开】,单帧渲染量随视野走、与 20 条总量解耦。--baseOnly 仅诊断用
+ // (锁定整卷底图、关引擎换图)。
+ gViewAllBaseOnly = a.kv.count("baseOnly") != 0;
+ gViewAllSwapFix = std::stoi(a.get("swapFix", "0")); // 旧 multi-volume 换图诊断(已无意义,保留不碍)
+ // 引擎金字塔已逐级降 Y,无需单独抽 Y 平面 → 通道维 LOD【默认关】(--chanLod 1 可重开)。
+ gChanLod = std::stoi(a.get("chanLod", "0")) != 0;
+ // --bgSuppress F(0..1):压背景、突出反射层(传函压低近零背景 + 中心透明死区)。
+ // 0=原观感(背景可见);0.3~0.6 让总览干净;过大(>0.7)会连带抹掉弱反射(弱异常靠切片抓)。
+ gBgSuppress = std::clamp(std::stod(a.get("bgSuppress", "0")), 0.0, 1.0);
+ // --maxImgSample F:拖动态屏幕采样距离上限(AutoAdjust 拖动时最多降到 1/F 分辨率,
+ // 越大越快越糊;松手自动恢复全质量)。默认 4。拉近卡顿可调大(如 8)。
+ // LOD 已扛住性能(概览/拉近都够快)→ 拖动【默认不降质,保持清晰】。降质纯 opt-in 兜底:
+ // --maxImgSample F(拖动屏幕降到 1/F,默认 1=不降)、--dragSampleMul M(拖动步长×M,默认 1=不变)。
+ // 仅当低端机拖动仍卡时才调大它们。
+ const double maxImgSample = std::stod(a.get("maxImgSample", "1"));
+ // --sampleDist D:静止态沿光线步长(世界米)。通道插值后 Y 很密,自动步长会算得过细→巨卡,
+ // 故固定一个合理值。越大越快越糙;卡时调大(0.5/1.0),太糊/丢层调小(0.15)。
+ const double sampleDist = std::stod(a.get("sampleDist", "0.3"));
+ const double dragSampleMul = std::stod(a.get("dragSampleMul", "1"));
std::cout << "[view-all] storesDir=" << storesDir << " gpsDir=" << gpsDir
<< " exagg=" << exagg << " level=" << level << " spread=" << spread
@@ -4370,16 +5011,18 @@ int cmdViewAll(int argc, char** argv) {
// 起步喂粗底图(小且不空),引擎备好更合适 LOD 单图后整图换上。
ps.prop = buildLineProperty(ps.meta, ps.source->baseImage());
- ps.mapper = vtkSmartPointer::New();
- ps.mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
- // #1 拖动降采样:交互式采样距离自适应(拖动→大步长降采样跟手,松手→全质量)。
- ps.mapper->SetAutoAdjustSampleDistances(1);
- ps.mapper->SetInteractiveAdjustSampleDistances(1);
+ // LOD 中心:每条线建【自己的】GPU mapper,起步喂 baseImage,引擎选区后换上。
+ // 手动控质(与旧 multi-volume 同口径):AutoAdjust 关、静止步长 sampleDist。
+ ps.mapper = vtkSmartPointer::New();
+ ps.mapper->SetAutoAdjustSampleDistances(0);
+ ps.mapper->SetImageSampleDistance(1.0);
+ ps.mapper->SetSampleDistance(static_cast(sampleDist));
ps.volume = vtkSmartPointer::New();
if (ps.source->baseImage() != nullptr) {
- ps.mapper->SetInputData(ps.source->baseImage());
- ps.mapper->Update();
ps.currentImg = ps.source->baseImage(); // 起步即有图(不空白)
+ ps.fullYImg = ps.source->baseImage();
+ ps.mapper->SetInputData(ps.currentImg);
+ ps.mapper->Update();
}
ps.volume->SetMapper(ps.mapper);
ps.volume->SetProperty(ps.prop);
@@ -4421,7 +5064,73 @@ int cmdViewAll(int argc, char** argv) {
std::cout << "[view-all] 加载并定位线数=" << lines.size()
<< "(每线一个 ViewAdaptiveVolumeSource 引擎)\n";
- // 4) 同一 renderer 加全部线的底图层 + 高清层。
+ // 诊断(--overlapStat):实测一条俯视光线真实穿几个体——把各线世界 AABB 投到 X-Y 平面,
+ // 在公共 footprint 上撒细网格,统计每格被几条线的 AABB 覆盖(=该处俯视光线穿的体数)。
+ // 纯几何、不渲染。验证"20× 重叠"是真是假(§7.1)。
+ if (a.kv.count("overlapStat") > 0) {
+ double mnx = 1e30, mny = 1e30, mxx = -1e30, mxy = -1e30;
+ for (const auto& ps : lines) {
+ mnx = std::min(mnx, ps.worldBounds[0]); mxx = std::max(mxx, ps.worldBounds[1]);
+ mny = std::min(mny, ps.worldBounds[2]); mxy = std::max(mxy, ps.worldBounds[3]);
+ }
+ const int G = 400; // 网格分辨率
+ const double sx = (mxx - mnx) / G, sy = (mxy - mny) / G;
+ long covered = 0, sumCov = 0, maxCov = 0;
+ std::vector hist(static_cast(lines.size()) + 1, 0);
+ for (int j = 0; j < G; ++j)
+ for (int i = 0; i < G; ++i) {
+ const double cx = mnx + (i + 0.5) * sx, cy = mny + (j + 0.5) * sy;
+ int cov = 0;
+ for (const auto& ps : lines)
+ if (cx >= ps.worldBounds[0] && cx <= ps.worldBounds[1] &&
+ cy >= ps.worldBounds[2] && cy <= ps.worldBounds[3])
+ ++cov;
+ if (cov > 0) { ++covered; sumCov += cov; maxCov = std::max(maxCov, cov); }
+ ++hist[cov];
+ }
+ std::cout << "\n=== overlapStat(俯视光线穿体数,纯几何)===\n";
+ std::cout << "footprint X=[" << mnx << "," << mxx << "] Y=[" << mny << "," << mxy
+ << "] (米)\n";
+ std::cout << "有体覆盖格的【平均重叠层数】= "
+ << (covered ? double(sumCov) / covered : 0.0)
+ << " 最大重叠 = " << maxCov << " / 共 " << lines.size() << " 条\n";
+ std::cout << "重叠层数分布(覆盖格中占比):\n";
+ for (std::size_t k = 1; k < hist.size(); ++k)
+ if (hist[k] > 0)
+ std::cout << " 穿 " << k << " 个体: " << (covered ? 100.0 * hist[k] / covered : 0.0)
+ << "%\n";
+ std::cout << "结论:平均/最大若 ~2–3 而非 ~20,则'20×重叠'是误判,瓶颈在别处(密 Y 细采样)。\n";
+ return 0;
+ }
+
+ // #6 统一传函:汇总各线底图 2/98 分位 → 一套【公共物理幅值范围】,重建各线属性使
+ // 同一幅值跨线映射同色(可比)。各线仍用自己的 quant(量化差异由各自 quant 吸收)。
+ double sumLo = 0.0, sumHi = 0.0;
+ int nRange = 0;
+ for (PlacedSource& ps : lines) {
+ vtkImageData* b = ps.source->baseImage();
+ if (b == nullptr) continue;
+ const ScalarPercentiles pc =
+ sampleScalarPercentiles(b, ps.meta.quant, 0.02, 0.98);
+ if (pc.samples > 0) {
+ sumLo += pc.lo;
+ sumHi += pc.hi;
+ ++nRange;
+ }
+ }
+ double shVmin = 0.0, shVmax = 0.0;
+ if (nRange > 0 && sumLo / nRange < sumHi / nRange) {
+ shVmin = sumLo / nRange;
+ shVmax = sumHi / nRange;
+ for (PlacedSource& ps : lines) {
+ ps.prop = buildLineProperty(ps.meta, ps.source->baseImage(), shVmin, shVmax);
+ ps.volume->SetProperty(ps.prop);
+ }
+ std::cout << "[view-all] #6 统一幅值范围 [" << shVmin << ", " << shVmax
+ << "](跨线一致 + 色标图例)\n";
+ }
+
+ // 4) 单个共享 vtkGPUVolumeRayCastMapper + vtkMultiVolume:全部线作为端口单遍合成。
auto rw = preview ? makeOffscreenWindow(winW, winH)
: vtkSmartPointer::New();
if (!preview) rw->SetSize(winW, winH);
@@ -4430,42 +5139,253 @@ int cmdViewAll(int argc, char** argv) {
kViewDefaultVariant.bg[2]);
rw->AddRenderer(ren);
+ // #6 色标图例(右侧色阶条 + 幅值刻度)。仅在拿到公共幅值范围时加。
+ if (shVmin < shVmax) ren->AddViewProp(buildScalarBar(shVmin, shVmax));
+
auto capWin = vtkSmartPointer::New();
vtkOutputWindow::SetInstance(capWin);
- for (PlacedSource& ps : lines) {
- ren->AddVolume(ps.volume); // P12:每线唯一体(引擎 LOD 单层)
+ // 方案 A 核心:把多条【各自独立插值】的体作为同一个 vtkGPUVolumeRayCastMapper 的不同
+ // 端口注册进一个 vtkMultiVolume → 单遍 ray-cast 一次性合成(含重叠),消除"N 体=N 遍
+ // ray-cast"的物理 N× 开销;每条线仍各保留自己的世界变换 T 与逐线传函(满足"分开插值、
+ // 合并渲染")。
+ //
+ // 硬约束:单个 multi-volume 同时挂的体数受 GPU 每着色器纹理单元上限制约(每体约吃 4 个
+ // 单元:体标量 + 颜色 TF + 不透明 TF + 梯度)。超限会报"Hardware does not support the
+ // number of textures"并悄悄丢体。故按 K 条/包分组:每包一个 multi-volume 单遍合成,
+ // 总遍数=ceil(N/K)(远少于每线一遍的 N 遍)。实测本机 K=7(32 单元 GPU)。
+ //
+ // 先空渲一帧建好 GL 上下文,再查 GPU 纹理单元数推断【起始】K(每包体数)。
+ rw->Render();
+ int K = maxPerPassArg;
+ const bool autoK = (maxPerPassArg <= 0);
+ if (autoK) {
+ int units = 0;
+ if (auto* oglrw = vtkOpenGLRenderWindow::SafeDownCast(rw)) {
+ if (auto* tum = oglrw->GetTextureUnitManager())
+ units = tum->GetNumberOfTextureUnits();
+ }
+ K = units > 8 ? (units - 4) / 4 : 1; // 起始【估计】:留 4 单元、每体 ~4 单元
+ if (K < 1) K = 1;
+ std::cout << "[view-all] GPU 纹理单元=" << units << " → 起始每包体数 K=" << K
+ << "(仅估计;下面按真实硬件告警自动退避。--maxPerPass 可锁定)\n";
+ } else {
+ std::cout << "[view-all] 每包体数 K=" << K << "(--maxPerPass 锁定,不自动退避)\n";
}
+
+ // LOD 中心装配:各线独立 GPU mapper(已在上面建好、喂了 baseImage),直接把每条线的 volume
+ // 加进同一 renderer。无 multi-volume → 无纹理单元上限、无分包、无退避(passcost 证明 N 遍开销
+ // 温和:20 条独立 mapper 铺开 78fps)。每帧实际渲染量由视野 LOD(引擎选区)压住、与 20 条总量解耦。
+ (void)K;
+ (void)autoK;
+ std::vector> mappers;
+ mappers.reserve(lines.size());
+ for (PlacedSource& ps : lines) {
+ if (ps.mapper) mappers.push_back(ps.mapper);
+ ren->AddVolume(ps.volume);
+ }
+ ren->ResetCamera();
+ rw->Render();
std::cout << "[view-all] 已加入场景线数=" << lines.size()
- << "(每线唯一体=" << lines.size()
- << " 体,引擎 LOD 单层,各 ≤16384 单纹理,绝不撞 GL 纹理墙)\n";
+ << "(各线独立 GPU mapper + 视野 LOD,弃 multi-volume 单遍;单帧渲染量随视野走)\n";
+
+ // --slice:全局世界切面——定义一个世界轴对齐薄板,每条线用 worldInv 作 ResliceTransform
+ // reslice 到这【同一个面】上采样(覆盖外透明),blend 合成一张 → 真正一整片切面,整体扫过去。
+ GlobalSlice gs;
+ const bool sliceOn = a.kv.count("slice") > 0;
+ vtkSmartPointer sliceActor;
+ if (sliceOn) {
+ gs.rw = rw.Get();
+ const std::string sax = a.get("slice", "");
+ int inPlane[2];
+ if (sax == "frontback") { // 顺路 radargram:沿 world X 扫,面=Y-Z
+ gs.sweepAxis = 0; inPlane[0] = 1; inPlane[1] = 2;
+ } else if (sax == "leftright") { // 横切断面:沿 world Y(沿路) 扫,面=X-Z(横穿所有线)
+ gs.sweepAxis = 1; inPlane[0] = 0; inPlane[1] = 2;
+ } else { // updown 深度 C-scan:沿 world Z 扫,面=X-Y
+ gs.sweepAxis = 2; inPlane[0] = 0; inPlane[1] = 1;
+ }
+ gs.inA = inPlane[0];
+ gs.inB = inPlane[1];
+ // footprint:全线世界 AABB 并集。
+ double lo[3] = {1e30, 1e30, 1e30}, hi[3] = {-1e30, -1e30, -1e30};
+ for (const PlacedSource& ps : lines)
+ for (int d = 0; d < 3; ++d) {
+ lo[d] = std::min(lo[d], ps.worldBounds[d * 2]);
+ hi[d] = std::max(hi[d], ps.worldBounds[d * 2 + 1]);
+ }
+ gs.sweepMin = lo[gs.sweepAxis];
+ gs.sweepMax = hi[gs.sweepAxis];
+ const double atFrac = std::clamp(std::stod(a.get("sliceAt", "0.5")), 0.0, 1.0);
+ gs.sweepWorld = lo[gs.sweepAxis] + atFrac * (hi[gs.sweepAxis] - lo[gs.sweepAxis]);
+ for (int d = 0; d < 3; ++d) { gs.footLo[d] = lo[d]; gs.footHi[d] = hi[d]; }
+ const double SENT = -32768.0; // 覆盖外 sentinel(int16 数据 ±7417 之外)
+ const geopro::core::ColorScale sliceCs =
+ pickColor(kViewDefaultVariant.color, shVmin, shVmax);
+ vtkSmartPointer lut = makeLut(sliceCs, shVmin, shVmax);
+
+ if (gs.sweepAxis == 2) {
+ // ── 深度 C-scan:逐线整张水平切面(深度共面→拼成完整 C-scan,全覆盖、原生分辨率)──
+ for (PlacedSource& ps : lines) {
+ vtkImageData* b = ps.source->baseImage();
+ if (b == nullptr) continue;
+ int dd[3];
+ double oo[3], ss[3];
+ b->GetDimensions(dd);
+ b->GetOrigin(oo);
+ b->GetSpacing(ss);
+ auto col = vtkSmartPointer::New();
+ col->SetLookupTable(lut);
+ col->SetInputData(b);
+ col->SetOutputFormatToRGBA();
+ col->Update();
+ auto act = vtkSmartPointer::New();
+ act->GetMapper()->SetInputConnection(col->GetOutputPort());
+ act->SetUserTransform(ps.world); // 整张水平片摆进世界(共面)
+ ren->AddActor(act);
+ gs.lineActors.push_back(act);
+ gs.winv.push_back(ps.worldInv);
+ gs.lorg.push_back({oo[0], oo[1], oo[2]});
+ gs.lspc.push_back({ss[0], ss[1], ss[2]});
+ gs.ldim.push_back({dd[0], dd[1], dd[2]});
+ if (ps.volume) gs.volumes.push_back(ps.volume.Get());
+ }
+ } else {
+ // ── 横切/顺路(竖直面):全局面 reslice 各线到同一世界面 + blend(共面、可见整片)──
+ // 输出薄板:面内铺满 footprint(目标 0.1m、每轴上限 2000),扫描轴 1 片@sweepWorld。
+ const double targetSp = 0.1;
+ double osp[3], oorg[3];
+ int oext[6];
+ for (int d = 0; d < 3; ++d) {
+ if (d == gs.sweepAxis) {
+ osp[d] = 1.0; oorg[d] = gs.sweepWorld; oext[d * 2] = 0; oext[d * 2 + 1] = 0;
+ } else {
+ const double span = std::max(1e-3, hi[d] - lo[d]);
+ const int Nd = std::clamp(static_cast(span / targetSp), 64, 2000);
+ osp[d] = span / Nd;
+ oorg[d] = lo[d];
+ oext[d * 2] = 0; oext[d * 2 + 1] = Nd - 1;
+ }
+ }
+ auto blend = vtkSmartPointer::New();
+ blend->SetBlendModeToNormal();
+ for (PlacedSource& ps : lines) {
+ vtkImageData* b = ps.source->baseImage();
+ if (b == nullptr) continue;
+ auto rs = vtkSmartPointer::New();
+ rs->SetInputData(b);
+ rs->SetResliceTransform(ps.worldInv);
+ rs->SetInterpolationModeToNearestNeighbor();
+ rs->SetBackgroundLevel(SENT);
+ rs->SetOutputDimensionality(3);
+ rs->SetOutputSpacing(osp);
+ rs->SetOutputOrigin(oorg);
+ rs->SetOutputExtent(oext);
+ auto rgb = vtkSmartPointer::New();
+ rgb->SetLookupTable(lut);
+ rgb->SetInputConnection(rs->GetOutputPort());
+ rgb->SetOutputFormatToRGB();
+ auto th = vtkSmartPointer::New();
+ th->SetInputConnection(rs->GetOutputPort());
+ th->ThresholdBetween(SENT - 0.5, SENT + 0.5);
+ th->SetInValue(0);
+ th->SetOutValue(255);
+ th->SetOutputScalarTypeToUnsignedChar();
+ auto app = vtkSmartPointer::New();
+ app->AddInputConnection(rgb->GetOutputPort());
+ app->AddInputConnection(th->GetOutputPort());
+ blend->AddInputConnection(app->GetOutputPort());
+ gs.reslices.push_back(rs);
+ if (ps.volume) gs.volumes.push_back(ps.volume.Get());
+ }
+ sliceActor = vtkSmartPointer::New();
+ sliceActor->GetMapper()->SetInputConnection(blend->GetOutputPort());
+ blend->Update();
+ sliceActor->SetDisplayExtent(oext[0], oext[1], oext[2], oext[3], oext[4], oext[5]);
+ ren->AddActor(sliceActor);
+ }
+ gs.applySlice();
+ gs.applyVolume();
+ const char* axisName = gs.sweepAxis == 2
+ ? "深度 C-scan(逐线整片,全覆盖原生分辨率,沿深度扫)"
+ : gs.sweepAxis == 1
+ ? "横切断面(全局面,沿路扫,整片横穿所有线)"
+ : "顺路 radargram(全局面,沿横向扫)";
+ const std::size_t nslice =
+ gs.lineActors.empty() ? gs.reslices.size() : gs.lineActors.size();
+ std::cout << "[view-all] --slice:切片已加(" << nslice << " 线," << axisName
+ << ")。↑↓整片扫过 / [ ]调体透明度 / v 体显隐。\n";
+ }
ViewAllState st;
st.lines = &lines;
+ st.mappers = &mappers;
st.ren = ren.Get();
st.rw = rw.Get();
st.aspect = aspect;
st.viewportH = winH;
+ st.dragImgSample = maxImgSample;
+ st.sampleDist = sampleDist;
+ st.dragSampleMul = dragSampleMul;
// 首帧:ResetCamera 框全测区 → 概览(各线选粗 LOD 底图)。提交引擎目标 + 阻塞拉首图。
ren->ResetCamera();
st.cam = ren->GetActiveCamera();
- viewAllRefreshFrustum(&st);
+ viewAllSubmitTargets(&st);
// 概览阻塞拉一次(保证首帧高清就绪,离屏/真窗口都从有图起步)。
for (PlacedSource& ps : lines) {
- if (ps.culled) continue;
for (int tries = 0; tries < 200; ++tries) {
if (viewAllPickOneLine(ps)) break;
if (ps.currentImg != nullptr) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
}
+ viewAllApplyChanLod(&st); // §3:首帧按概览距离选通道密度(远→疏)
rw->Render();
if (capWin->textureError()) {
std::cerr << "[view-all] 警告: 仍检测到 3D 纹理维度错误(不应发生,引擎契约 "
"≤16384)。\n";
}
+ // 根因验证(--swapTest):确定性复现"通道 LOD 换密度"。before=全 Y 渲一帧存图;
+ // 然后把每条线端口就地换成 Y 抽半的图(同范围、不同 Y 密度)+ 施加 --swapFix 策略;
+ // after 再渲一帧存图。对比 before/after:after 若仍连续/在位 → 就地换贴图能算对。
+ if (a.kv.count("swapTest") > 0) {
+ const std::string sd = a.get("shotDir", "tmp/swaptest");
+ fs::create_directories(sd);
+ ren->ResetCamera();
+ ren->GetActiveCamera()->Elevation(30);
+ ren->GetActiveCamera()->Azimuth(25);
+ ren->ResetCameraClippingRange();
+ rw->Render();
+ savePng(rw.Get(), (fs::path(sd) / "before-fullY.png").string());
+ for (PlacedSource& ps : lines) {
+ if (ps.currentImg == nullptr) continue;
+ int dIn[3];
+ ps.currentImg->GetDimensions(dIn);
+ // --swapMode x = 改包围盒(X 裁半,模拟引擎子区域);否则 y = 保包围盒(改 Y 密度,通道 LOD)。
+ auto dY = (a.get("swapMode", "y") == "x") ? cropXhalf(ps.currentImg)
+ : downsampleY(ps.currentImg);
+ int dOut[3];
+ dY->GetDimensions(dOut);
+ ps.currentImg = dY;
+ ps.multiMapper->SetInputDataObject(ps.port, dY);
+ if (gViewAllSwapFix >= 2) ps.multiVol->SetVolume(ps.volume, ps.port);
+ ps.multiVol->Modified();
+ if (gViewAllSwapFix >= 3) ps.multiMapper->Modified();
+ if (gViewAllSwapFix >= 4) ps.multiVol->GetBounds();
+ std::cout << "[swapTest] " << ps.name << " Y " << dIn[1] << "→" << dOut[1]
+ << "\n";
+ }
+ ren->ResetCameraClippingRange();
+ rw->Render();
+ savePng(rw.Get(), (fs::path(sd) / "after-halfY.png").string());
+ std::cout << "[swapTest] swapFix=" << gViewAllSwapFix
+ << " 出图: " << (fs::path(sd) / "before-fullY.png").string()
+ << " / after-halfY.png(after 连续在位=能算对)\n";
+ return 0;
+ }
+
if (preview) {
fs::create_directories(shotDir);
@@ -4482,12 +5402,19 @@ int cmdViewAll(int argc, char** argv) {
cam->SetViewUp(0, 1, 0);
ren->ResetCameraClippingRange();
st.cam = cam;
- viewAllRefreshFrustum(&st);
+ viewAllSubmitTargets(&st);
for (PlacedSource& ps : lines)
- for (int t = 0; t < 100 && !ps.culled && ps.currentImg == nullptr; ++t) {
+ for (int t = 0; t < 100 && ps.currentImg == nullptr; ++t) {
if (viewAllPickOneLine(ps)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
+ viewAllApplyChanLod(&st);
+ if (!lines.empty()) {
+ int d[3] = {0, 0, 0};
+ if (lines[0].currentImg) lines[0].currentImg->GetDimensions(d);
+ std::cout << "[chanLod] 俯视(远) stride=" << lines[0].yStride
+ << " 线0 Y=" << d[1] << "\n";
+ }
rw->Render();
savePng(rw.Get(), (fs::path(shotDir) / "view-all-top.png").string());
std::cout << "[view-all] 俯视图存: "
@@ -4504,13 +5431,13 @@ int cmdViewAll(int argc, char** argv) {
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
st.cam = cam;
- viewAllRefreshFrustum(&st);
+ viewAllSubmitTargets(&st);
for (PlacedSource& ps : lines) {
- for (int t = 0; t < 100 && !ps.culled && ps.currentImg == nullptr; ++t) {
+ for (int t = 0; t < 100 && ps.currentImg == nullptr; ++t) {
if (viewAllPickOneLine(ps)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
- if (ps.culled) ++ovCulled; else ++ovVisible;
+ ++ovVisible;
}
rw->Render();
savePng(rw.Get(),
@@ -4560,27 +5487,61 @@ int cmdViewAll(int argc, char** argv) {
ren->ResetCameraClippingRange();
st.cam = cam;
(void)segLen;
- viewAllRefreshFrustum(&st);
+ viewAllSubmitTargets(&st);
for (PlacedSource& ps : lines) {
- for (int t = 0; t < 120 && !ps.culled && ps.currentImg == nullptr; ++t) {
+ for (int t = 0; t < 120 && ps.currentImg == nullptr; ++t) {
if (viewAllPickOneLine(ps)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
- if (ps.culled) ++nearCulled; else ++nearVisible;
+ ++nearVisible;
+ }
+ viewAllApplyChanLod(&st);
+ if (!lines.empty()) {
+ int d[3] = {0, 0, 0};
+ if (lines[0].currentImg) lines[0].currentImg->GetDimensions(d);
+ std::cout << "[chanLod] 拉近(近) stride=" << lines[0].yStride
+ << " 线0 Y=" << d[1] << "\n";
}
rw->Render();
savePng(rw.Get(), (fs::path(shotDir) / "view-all-near.png").string());
std::cout << "[view-all] 拉近图存: "
<< (fs::path(shotDir) / "view-all-near.png").string() << "\n";
+ // --slice 验证图:相机沿扫描轴正对切面(看整片切面 face-on)。
+ if (sliceOn) {
+ const int sa = gs.sweepAxis;
+ const int a = (sa == 0) ? 1 : 0; // 面内轴 1
+ const int b = (sa == 2) ? 1 : 2; // 面内轴 2
+ double ctr[3];
+ for (int d = 0; d < 3; ++d) ctr[d] = 0.5 * (gs.footLo[d] + gs.footHi[d]);
+ ctr[sa] = gs.sweepWorld;
+ const double extA = std::max(1.0, gs.footHi[a] - gs.footLo[a]);
+ const double extB = std::max(1.0, gs.footHi[b] - gs.footLo[b]);
+ const double dist = 0.6 * std::max(extA, extB) / tanH * 1.4;
+ double pos[3] = {ctr[0], ctr[1], ctr[2]};
+ pos[sa] += dist; // 沿扫描轴退后,正对切面
+ cam->SetFocalPoint(ctr[0], ctr[1], ctr[2]);
+ cam->SetPosition(pos[0], pos[1], pos[2]);
+ // 俯视(深度,沿Z看)用 Y 朝上;竖直剖面用 Z 朝上(避免 up 与视向平行退化)。
+ if (sa == 2) cam->SetViewUp(0, 1, 0);
+ else cam->SetViewUp(0, 0, 1);
+ ren->ResetCameraClippingRange();
+ rw->SetDesiredUpdateRate(0.5);
+ gs.volOn = false; // 验证:隐去体,只看切面本身
+ gs.applyVolume();
+ rw->Render();
+ savePng(rw.Get(), (fs::path(shotDir) / "view-all-slice.png").string());
+ std::cout << "[view-all] 切面正视图存: "
+ << (fs::path(shotDir) / "view-all-slice.png").string() << "\n";
+ }
+
rw->SetDesiredUpdateRate(15.0); // 拖动态:降采样
rw->Render();
Stopwatch sw;
const int frames = 60;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(f % 2 == 0 ? 0.4 : -0.4); // 小幅摆动(不转回全测区)
- viewAllRefreshFrustum(&st); // 拖动中持续重算视锥裁剪
- rw->Render();
+ rw->Render(); // 单遍合成,无需逐帧重提交(拖动只渲已有纹理)
}
fpsNear = sw.elapsedMs() > 0 ? frames * 1000.0 / sw.elapsedMs() : 0.0;
rw->SetDesiredUpdateRate(0.5);
@@ -4627,7 +5588,7 @@ int cmdViewAll(int argc, char** argv) {
// 真窗口:可旋转/缩放(每线引擎 LOD + 视锥裁剪 + 拖动降采样)。
vtkOutputWindow::SetInstance(nullptr);
- rw->SetWindowName("gpr_poc view-all —— 20 条独立体引擎LOD/视锥裁剪/拖动降采样");
+ rw->SetWindowName("gpr_poc view-all —— 多体单遍合成(multi-volume)/逐线LOD/拖动降采样");
// 屏幕左上角 fps + 可见/裁剪线数文本。
vtkNew fpsText;
@@ -4660,7 +5621,27 @@ int cmdViewAll(int argc, char** argv) {
cbTimer->SetClientData(&st);
iren->AddObserver(vtkCommand::TimerEvent, cbTimer);
- std::cout << "[view-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出。\n";
+ // 滚轮缩放:高优先级(1.0)先于 style 的 Dolly+Render → 该帧即按低质渲染。
+ vtkNew cbWheelF;
+ cbWheelF->SetCallback(viewAllOnWheel);
+ cbWheelF->SetClientData(&st);
+ iren->AddObserver(vtkCommand::MouseWheelForwardEvent, cbWheelF, 1.0);
+ vtkNew cbWheelB;
+ cbWheelB->SetCallback(viewAllOnWheel);
+ cbWheelB->SetClientData(&st);
+ iren->AddObserver(vtkCommand::MouseWheelBackwardEvent, cbWheelB, 1.0);
+
+ // --slice:键盘 Up/Down 改 C-scan 深度(高优先级,先于 style 处理)。
+ vtkNew cbKey;
+ if (sliceOn) {
+ cbKey->SetCallback(cscanOnKey);
+ cbKey->SetClientData(&gs);
+ iren->AddObserver(vtkCommand::KeyPressEvent, cbKey, 1.0);
+ }
+
+ std::cout << "[view-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出"
+ << (sliceOn ? " / ↑↓改切片深度 / [ ]调体透明度 / v 体显隐" : "")
+ << "。\n";
iren->Initialize();
iren->CreateRepeatingTimer(33); // ~30Hz 非阻塞拉取后台就绪纹理
rw->Render();
@@ -5825,6 +6806,9 @@ int main(int argc, char** argv) {
if (cmd == "build-geo") return cmdBuildGeo(argc, argv);
if (cmd == "build-line") return cmdBuildLine(argc, argv);
if (cmd == "build-all") return cmdBuildAll(argc, argv);
+ if (cmd == "ess-stat") return cmdEssStat(argc, argv);
+ if (cmd == "passcost") return cmdPassCost(argc, argv);
+ if (cmd == "slice") return cmdSlice(argc, argv);
if (cmd == "build-survey-line") return cmdBuildSurveyLine(argc, argv);
if (cmd == "build-survey-all") return cmdBuildSurveyAll(argc, argv);
if (cmd == "load") return cmdLoad(argc, argv);