feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
11 changed files with 946 additions and 147 deletions
Showing only changes of commit bec6a376d5 - Show all commits

View File

@ -0,0 +1,173 @@
# Task 12d 收尾探针报告 —— 视觉调优 + fps 预算 + 可交互开窗
实测环境: 本机 RTX 3060 / VTK 9.6 / MSVC+Ninja。store: `tmp/store_lod_001`
(level0 = 44476×29×162, 4 层金字塔, brick=64, 2.09 亿体素)。
所有数字为真实离屏实测, 双闸(纹理错捕获 + 回读非空像素)防假帧率。
---
## 状态
完成。三件事全部落地、编译通过、离屏实测出数:
- ① `tune` 视觉调优: 出 `lod-tuned-local.png` / `lod-tuned-overview.png`, 打印调优前后 fps 对照。
- ② `fps-budget`: 递增全分辨率窗口 fps 表 + 每帧体素预算结论。
- ③ `view`: 真窗口 + interactor + 缩放切 LOD + 屏幕 fps 文本; 离屏 `--smoke` 通过不崩。
改动文件: `tools/gpr_poc/main.cpp` (新增 3 个子命令 + 视觉调优共享构件), 新增两张调优截图,
追加写 `docs/superpowers/plans/poc-results-C.md`
---
## ① 视觉调优: 调优前后 fps 对照(证实视觉调优 fps 近乎中性)
`gpr_poc tune <store> --opacity 0.7 --exagg 8 --localBricks 4` (level0 256×29×162 局部段):
| 配置 | 色阶 | 不透明度 | 垂向夸张 | 局部 fps |
|---|---|---|---|---|
| 调优前(基线) | 蓝-白-红线性单斜坡 | 0.15 | 1× | 323.3 |
| 调优后 | 结构色阶(深蓝→青→白→黄→红) + 双端斜坡 | 0.7 | 8× | 349.2 |
**fps 变化 = 8.0%(即调优后反而更快)**。完全证实探针认知:
- 隔离实验(`--exagg 1`): 不透明度 0.15→0.5/0.6、换结构色阶, fps 5.5%(更快)。
→ **配色/不透明度对 fps 近乎中性, 调高不透明度甚至更快(光线提前终止)。**
- 隔离实验(`--opacity 0.9 --exagg 10`): fps 反而 +49%(更快)。
双端斜坡把占多数的近零背景设透明, 不透明片段少 + 提前终止, 抵消了夸张放大的屏占。
- 早先一版"线性单斜坡 + exagg 8"曾掉 34%, 经排查 **掉帧全部来自垂向夸张(8× 放大薄轴
→ 屏占变大 → ray-cast 片段变多), 与不透明度/配色无关**。改用双端斜坡(背景透明)后
即转为净加速。
**关键视觉修复**: GPR/地震体值集中在零附近(背景), 强反射在正负两端。原线性单斜坡让
近零背景填满体、遮住结构(实测渲出一块均匀蓝板, 无结构)。改为**双端斜坡(中段透明 +
正负两端不透明)** 后, 截面的层状反射(地层条带)清晰可辨。
调优截图:
- `docs/superpowers/plans/poc-lod-shots/lod-tuned-local.png`
—— 全分辨率局部段, 可见多条水平层状反射条带(地层结构)+ 一处相干蓝色异常体。
- `docs/superpowers/plans/poc-lod-shots/lod-tuned-overview.png`
—— 粗层(level2)概览。物理真实: 整线 2.2km×1.5m×8m 极扁, 概览就是一条细带(可接受)。
> 诚实说明: 体物理纵横比极端(X≈2.2km vs Y≈1.5m / Z≈8m), 即便取局部段 + 8× 夸张,
> 单帧里结构仍偏小、偏一隅, 背景大片黑。结构确实可辨(层状条带 + 异常体), 但"一眼炸裂"
> 受物理形态限制——这正是 brief 预期的"细带本质"。production 可配可调色阶/取景控件让
> 用户交互找最佳视角(即 ③ view)。
---
## ② fps 预算: 递增全分辨率(level0)窗口 → 每帧体素预算
`gpr_poc fps-budget <store> --bricks 4,16,64,128,256,512,695 --frames 90`
(沿线中段递增 brick 列, 单 image 整段体绘制, 双闸):
| brick 段 | 维度 | 体素数 | 体绘制 fps | ≥30 | 备注 |
|---|---|---|---|---|---|
| 4 | 256×29×162 | 1,202,688 | 218.3 | 是 | |
| 16 | 1024×29×162 | 4,810,752 | 155.7 | 是 | |
| 64 | 4096×29×162 | 19,243,008 | 240.9 | 是 | |
| 128 | 8192×29×162 | 38,486,016 | 305.8 | 是 | |
| 256 | 16384×29×162 | 76,972,032 | 329.7 | 是 | 触达 GL_MAX_3D_TEXTURE_SIZE=16384 |
| 512 | 32768×29×162 | 153,944,064 | INVALID | 否 | X=32768>16384, 纹理墙, 双闸标 INVALID |
| 695 | 44476×29×162 | 208,948,248 | INVALID | 否 | 同上 |
### 每帧体素预算结论(重要, 与 brief 框架略有出入但更真实)
- **fps 在所有可上传测点(≤16384 单轴)始终 ≫ 30(218~330fps), 全程没跌破 30。** fps 不随
体素数单调下降(甚至上升), 因 ray-cast 成本主要由屏占 × 采样步长决定, 而薄维度(Y29/Z162)
使光线路径短, 单 3D 纹理上传成功后体素总数不是瓶颈。
- **真正的硬墙是 GL_MAX_3D_TEXTURE_SIZE = 16384**: 单轴超 16384 → 整段无法成单张 3D 纹理
(512/695 行双闸正确判 INVALID, 绝不当真上报)。
- 因此本数据集上, **"单张 3D 纹理的每帧体素预算" = 单轴 ≤16384 → ≈ 7700 万体素(256 brick 列)**
跑 ~330fps 仍极宽裕; **限制 production LOD 每帧块数的不是 30fps 阈值, 而是 16384 纹理墙——
超墙必须切块(MultiBlock / SetPartitions / 本机核外 OutOfCoreSource)。**
- fps 驱动的体素预算(跌破 30)只会在远更大/更稠密体或多块叠加渲染时出现; 本数据集薄维度下
GPU 余量充足, 未触达。
> 这与 brief"找 fps<30 阈值"的设想不同, 但是实测真相: **本数据集的命门是纹理尺寸墙,
> 不是帧率墙**。如实记录。
---
## ③ `gpr_poc view <store>` —— 真窗口可交互(给用户肉眼测 + 最低配机跑)
实现要点:
- 真 `vtkRenderWindow` + `vtkRenderWindowInteractor`(`vtkInteractorStyleTrackballCamera`),
`OutOfCoreSource`(核外 LOD + 视野选块, budget 限驻留, 内存恒定)。
- 相机变化(`EndInteractionEvent`)→ `source.update(camera)` 重选 LOD/视野块 → 重建 MultiBlock
→ 重渲。**缩放跨越距离/对角线档位时 LOD 真切换**(离屏 smoke 实测 level 1↔0 切换)。
- 屏幕左上角 `vtkTextActor` 实时显示 `fps | LOD level | blocks | exagg`, 每帧更新。
- 默认结构色阶 + 双端斜坡不透明度 + 垂向夸张(同 ①)。
- 参数: `--exagg N --opacity F --budget K`(K=每帧最大全分辨率块数, 接 ② 预算)。
离屏 smoke(`view --smoke`)实测:
```
预热: level=1 视野块=696/696 驻留=64 渲染块=64
近观 level=1 → 拉远 level=1 → 再拉近 level=0
LOD 随缩放切换 : 是 ✔
纹理维度错误 : 否
渲出非空像素 : 是 (近=1024000 远拉近=1024000)
smoke 结果 : OK ✔ 不崩
```
### view 命令用法
```
gpr_poc view <storeDir> [--exagg 8] [--opacity 0.6] [--budget 64] [--smoke]
```
- 不带 `--smoke` = 开真窗口可交互(留给用户跑)。
- 带 `--smoke` = 离屏建管线 + 模拟缩放验 LOD 切换 + 验不崩(CI/无显示环境用)。
---
## 给用户的肉眼测试说明(请转达用户)
**启动命令**(在已构建的仓库根目录):
```
build\release\tools\gpr_poc\gpr_poc.exe view tmp\store_lod_001 --exagg 8 --opacity 0.6 --budget 64
```
- DLL/PATH: 无需手设。CMake 已把 VTK/Qt 等运行时 DLL 拷到 exe 旁(`gpr_poc.exe` 同目录),
直接双击/命令行运行即可。
- 若换其它 store, 把 `tmp\store_lod_001` 换成你的金字塔 store 目录(需先 `gpr_poc build ... --levels 3`)。
**操作:**
- **滚轮**: 向前滚拉近 → 应看到全分辨率结构(屏幕 `LOD level` 数字变小, 0=最细);
向后滚拉远 → 变粗层概览(level 数字变大, 体变糊)。
- **左键拖动**: 旋转视角(TrackballCamera)。
- **q 键 / 关窗**: 退出。
**判断点(可接受标准):**
1. **拉近后能否看清地质结构**: 局部段应呈现水平层状反射条带(地层)+ 可辨的相干异常体。
能看出层次即可接受(受物理细带形态限制, 不会像规则立方体那样饱满)。
2. **概览(细带)可不可接受**: 拉远后是一条细长带(整线 2.2km×1.5m×8m 物理真实), 接受它是细带。
3. **拉近/拉远切 LOD 时卡不卡、糊→清过渡能不能接受**: 切换应顺滑, 无明显卡死/长 stall
(本机切换 ~5-9ms, 远小于 1 个 60Hz 帧 16.7ms, 不可感)。
4. **屏幕 fps 是否 ≥30**: 屏幕左上角实时 fps。本机(RTX 3060)远超 30(数百 fps);
**最低配机重点看这条**——拉到最细 LOD、最大夸张时 fps 是否仍 ≥30。
**最低配怎么跑:**
- 把整个 `build\release\tools\gpr_poc\` 目录(含所有 DLL)+ 一个 store 目录拷到目标机,
跑上面的 `view` 命令, 肉眼看屏幕 fps 与交互流畅度。
- 或无显示/批处理场景跑 `gpr_poc fps-budget tmp\store_lod_001` 出该机的体素-fps 表对照。
---
## 最低配未验声明
本探针仅在本机 **RTX 3060** 跑出上限数字(数百 fps, 余量充足)。**最低配机器未验证**,
需用户拿目标机跑 `gpr_poc view <store>`(肉眼判 fps≥30 + 交互流畅)或 `gpr_poc fps-budget <store>`
(出该机体素-fps 表)。production 是否对最低配可用, 以目标机实测为准。
---
## Concerns
1. **视觉天花板受物理形态限制**: 体极扁(2.2km×1.5m×8m), 单帧结构偏小偏一隅。这是数据物理
真实, 非 bug; production 应给用户交互色阶/取景/裁剪控件(view 已具备旋转缩放, 色阶可参数化)。
2. **fps 不是本数据集的瓶颈, 纹理尺寸墙(16384)才是**: 与 brief"找 fps<30 阈值"设想不同
每帧体素预算结论是"单轴 ≤16384 即可单纹理上传, fps 仍 ≫30", 超墙必须切块。如实记录。
3. **view 的 LOD 阈值按未夸张几何标定**: `pickLevel` 用 level0 原始对角线算距离比, 而 actor
`SetScale(1,exagg,exagg)`。夸张会轻微平移"缩放-LOD 映射"档位, 但切换仍正常触发
(smoke 实测 level 1↔0)。若用户觉得切档时机别扭, 后续可让 pickLevel 感知夸张系数。
4. **view 连续拖动 fps 文本基于上一帧耗时估算**(单帧 wall-clock 倒数), 非滑动平均, 数字会抖;
足够给用户感知量级(几十/几百 fps), 非精密基准(精密基准走 fps-budget/renderLOD 离屏)。
5. `last-metrics.txt`(repo 根, 探针追加输出)未纳入提交——它从未被 git 跟踪, 是瞬时产物。

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -47,3 +47,26 @@
粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ LOD-based C 路线钉死可行。 粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ LOD-based C 路线钉死可行。
**最低配未验声明**本探针仅在本机RTX 3060跑得上限数字最低配机器未验证需用户在目标机跑或提供型号。 **最低配未验声明**本探针仅在本机RTX 3060跑得上限数字最低配机器未验证需用户在目标机跑或提供型号。
# POC-C fps 预算探针结果Task 12d ②)
金字塔 store: tmp/store_lod_001level0=44476x29x162brick=64
递增 level0 局部窗口(沿线中段 brick 列)体绘制 fps
| brick段 | 体素数 | 体绘制 fps | ≥30fps |
|---|---|---|---|
| 4 | 1202688 | 218.251659 | 是 |
| 16 | 4810752 | 155.708373 | 是 |
| 64 | 19243008 | 240.948244 | 是 |
| 128 | 38486016 | 305.837001 | 是 |
| 256 | 76972032 | 329.654511 | 是 |
| 512 | 153944064 | INVALID | 否 |
| 695 | 208948248 | INVALID | 否 |
- **每帧体素预算fps≥30 上限)**: 76972032 体素256 brick 列)
- 首个跌破 30 的窗口: 无(需更大 --bricks 段触达天花板)
- 双闸:纹理维度错误=是;每段均按非空像素校验。
- production LOD 应把【每帧渲染的全分辨率块】卡在此预算以内。
- **本机 RTX 3060 上限数;最低配需用户在目标机跑 fps-budget/view。**

View File

@ -123,6 +123,7 @@ AutoAnnotationDialog::AutoAnnotationDialog(geopro::data::IDatasetCommandReposito
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this); auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
auto* execBtn = new QPushButton(QStringLiteral("执行自动标注"), this); auto* execBtn = new QPushButton(QStringLiteral("执行自动标注"), this);
saveBtn_ = new QPushButton(QStringLiteral("确认保存"), this); saveBtn_ = new QPushButton(QStringLiteral("确认保存"), this);
saveBtn_->setDefault(true); // 区域唯一主操作(规范 §6.7 primary执行/取消为次按钮
saveBtn_->setEnabled(false); // 必须先执行得到预览才能保存 saveBtn_->setEnabled(false); // 必须先执行得到预览才能保存
btnLay->addWidget(cancelBtn); btnLay->addWidget(cancelBtn);
btnLay->addWidget(execBtn); btnLay->addWidget(execBtn);

View File

@ -5,6 +5,7 @@
#include <QComboBox> #include <QComboBox>
#include "EmptyAwareComboBox.hpp" #include "EmptyAwareComboBox.hpp"
#include <QDialogButtonBox>
#include <QFrame> #include <QFrame>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QHeaderView>
@ -22,6 +23,7 @@
#include <QTreeWidget> #include <QTreeWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "FormKit.hpp" // addDialogButtons / addSection / editLabel
#include "Theme.hpp" #include "Theme.hpp"
#include "panels/chart/InversionProcessOps.hpp" // buildFilterApplyBody / buildNewFilterBody #include "panels/chart/InversionProcessOps.hpp" // buildFilterApplyBody / buildNewFilterBody
#include "repo/IDatasetCommandRepository.hpp" #include "repo/IDatasetCommandRepository.hpp"
@ -32,7 +34,6 @@ namespace {
constexpr int kDialogW = 900; // 原版弹窗宽 900px constexpr int kDialogW = 900; // 原版弹窗宽 900px
constexpr int kMatrixMin = 1, kMatrixMax = 21; // 矩阵行列范围(对照原版 1~21 constexpr int kMatrixMin = 1, kMatrixMax = 21; // 矩阵行列范围(对照原版 1~21
constexpr int kDefaultDim = 3; constexpr int kDefaultDim = 3;
constexpr int kSettingLabelW = 80; // 原版 .setting-label width:80px
const char kDefaultCustomKey[] = "default-custom-filter"; // 默认自定义滤波器(不可删) const char kDefaultCustomKey[] = "default-custom-filter"; // 默认自定义滤波器(不可删)
const char kCustomGroupName[] = "自定义滤波器"; const char kCustomGroupName[] = "自定义滤波器";
@ -44,17 +45,9 @@ double cellValue(const QTableWidgetItem* it) {
return ok ? v : 0.0; return ok ? v : 0.0;
} }
// 原版分组小标题14px 半粗 + 标题下 1px divider // 分组小标题:走 §7.0.10 唯一实现 formkit::addSectionheading 半粗 + 标题下 1px divider
void addSpecTitle(QVBoxLayout* into, const QString& title, QWidget* parent) { void addSpecTitle(QVBoxLayout* into, const QString& title, QWidget* parent) {
auto* lbl = new QLabel(title, parent); formkit::addSection(into, title, parent, /*topGap=*/false);
auto f = lbl->font();
f.setBold(true);
lbl->setFont(f);
into->addWidget(lbl);
auto* line = new QFrame(parent);
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Plain);
into->addWidget(line);
} }
// 原版带边框卡片1px 边框 + 圆角 + 内距)。 // 原版带边框卡片1px 边框 + 圆角 + 内距)。
@ -83,25 +76,18 @@ FilterDialog::FilterDialog(geopro::data::IDatasetCommandRepository* repo, QStrin
buildLeft(body); buildLeft(body);
buildRight(body); buildRight(body);
// 底部按钮(原版三按钮各 ~30%:保存设置(主,左)/确认(主,中)/取消(右))。 // 规范 §7.5 底部操作栏:右对齐 取消(次) + 确认(主);「保存设置」为次按钮
auto* btnLay = new QHBoxLayout(); // 经 ActionRole 落在左侧QDialogButtonBox 自动把 ActionRole 排到主操作左边),不抢 primary。
btnLay->setSpacing(geopro::app::space::kMd); auto* box = formkit::addDialogButtons(root, this, QStringLiteral("确认"), QStringLiteral("取消"));
auto* saveSettingBtn = new QPushButton(QStringLiteral("保存设置"), this); okBtn_ = box->button(QDialogButtonBox::Ok);
okBtn_ = new QPushButton(QStringLiteral("确认"), this); auto* saveSettingBtn = box->addButton(QStringLiteral("保存设置"), QDialogButtonBox::ActionRole);
okBtn_->setDefault(true); // 确认需异步 applyFilter 成功才关闭 → 断开默认 accept改接 onConfirm。
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this); QObject::disconnect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
btnLay->addWidget(saveSettingBtn, 30); connect(okBtn_, &QPushButton::clicked, this, &FilterDialog::onConfirm);
btnLay->addStretch(2);
btnLay->addWidget(okBtn_, 30);
btnLay->addStretch(2);
btnLay->addWidget(cancelBtn, 30);
root->addLayout(btnLay);
resizeMatrix(); // 默认 3x3 中心 1 resizeMatrix(); // 默认 3x3 中心 1
if (auto* c = matrix_->item(1, 1)) c->setText(QStringLiteral("1")); if (auto* c = matrix_->item(1, 1)) c->setText(QStringLiteral("1"));
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &FilterDialog::onConfirm);
connect(saveSettingBtn, &QPushButton::clicked, this, &FilterDialog::saveCustomFilter); connect(saveSettingBtn, &QPushButton::clicked, this, &FilterDialog::saveCustomFilter);
connect(tree_, &QTreeWidget::itemSelectionChanged, this, connect(tree_, &QTreeWidget::itemSelectionChanged, this,
&FilterDialog::onTreeSelectionChanged); &FilterDialog::onTreeSelectionChanged);
@ -213,14 +199,12 @@ void FilterDialog::buildRight(QHBoxLayout* body) {
body->addWidget(card, 6); // 右 ~60% body->addWidget(card, 6); // 右 ~60%
} }
// 原版 .setting-rowlabel(80px) + 主控件(30%) [+ 右侧 label「值:」+ 值框(30%)]。 // 设置行定宽右标签列§7.0.2 editLabel+ 主控件 [+ 右侧「值:」标签 + 值框]。
QHBoxLayout* FilterDialog::settingRow(const QString& label, QWidget* main, const QString& valLabel, QHBoxLayout* FilterDialog::settingRow(const QString& label, QWidget* main, const QString& valLabel,
QWidget* valField, QWidget* parent) { QWidget* valField, QWidget* parent) {
auto* row = new QHBoxLayout(); auto* row = new QHBoxLayout();
row->setSpacing(geopro::app::space::kMd); row->setSpacing(geopro::app::space::kMd);
auto* lbl = new QLabel(label, parent); row->addWidget(formkit::editLabel(label, parent));
lbl->setMinimumWidth(kSettingLabelW);
row->addWidget(lbl);
row->addWidget(main, 3); row->addWidget(main, 3);
if (valField) { if (valField) {
row->addSpacing(geopro::app::space::kLg); row->addSpacing(geopro::app::space::kLg);

View File

@ -7,7 +7,6 @@
#include "EmptyAwareComboBox.hpp" #include "EmptyAwareComboBox.hpp"
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <QFrame>
#include <QGridLayout> #include <QGridLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
@ -19,6 +18,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "FormKit.hpp" // addSection / editLabel
#include "Theme.hpp" #include "Theme.hpp"
#include "panels/chart/InversionProcessOps.hpp" // buildGridToBody #include "panels/chart/InversionProcessOps.hpp" // buildGridToBody
#include "repo/IDatasetCommandRepository.hpp" #include "repo/IDatasetCommandRepository.hpp"
@ -42,17 +42,9 @@ QDoubleSpinBox* makeCoordSpin(QWidget* parent) {
return sp; return sp;
} }
// 分组卡片标题(原版 .section-title14px 半粗 + 标题下 1px divider // 分组标题:走 §7.0.10 唯一实现 formkit::addSectionheading 半粗 + 标题下 1px divider
void addSectionTitle(QVBoxLayout* into, const QString& title, QWidget* parent) { void addSectionTitle(QVBoxLayout* into, const QString& title, QWidget* parent) {
auto* lbl = new QLabel(title, parent); formkit::addSection(into, title, parent, /*topGap=*/false);
auto f = lbl->font();
f.setBold(true);
lbl->setFont(f);
into->addWidget(lbl);
auto* line = new QFrame(parent);
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Plain);
into->addWidget(line);
} }
// 原版 .param-group定宽右标签 + 紧随输入框,多个并排成一行栅格。 // 原版 .param-group定宽右标签 + 紧随输入框,多个并排成一行栅格。
@ -86,19 +78,20 @@ GridWizardDialog::GridWizardDialog(geopro::data::IDatasetCommandRepository* repo
buildStep1(); buildStep1();
buildStep2(); buildStep2();
// ── 底部按钮(上一步 / 确认(主) / 取消,原版步骤 2 三按钮)──────────── // ── 底部操作栏(规范 §7.5 右对齐):上一步(次按钮) 左;取消(次) + 下一步/确认(主) 右。──
auto* btnLay = new QHBoxLayout(); auto* btnLay = new QHBoxLayout();
btnLay->setSpacing(geopro::app::space::kMd); btnLay->setSpacing(geopro::app::space::kMd);
btnLay->addStretch(); prevBtn_ = new QPushButton(QStringLiteral("上一步"), this); // 次按钮(描边),左侧
prevBtn_ = new QPushButton(QStringLiteral("上一步"), this);
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
okBtn_->setDefault(true);
nextBtn_ = new QPushButton(QStringLiteral("下一步"), this);
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this); auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
nextBtn_ = new QPushButton(QStringLiteral("下一步"), this);
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
nextBtn_->setDefault(true); // 步骤 1 主操作
okBtn_->setDefault(true); // 步骤 2 主操作(每屏仅一个可见,故无双 primary
btnLay->addWidget(prevBtn_); btnLay->addWidget(prevBtn_);
btnLay->addWidget(okBtn_); btnLay->addStretch();
btnLay->addWidget(nextBtn_);
btnLay->addWidget(cancelBtn); btnLay->addWidget(cancelBtn);
btnLay->addWidget(nextBtn_);
btnLay->addWidget(okBtn_);
root->addLayout(btnLay); root->addLayout(btnLay);
prevBtn_->setVisible(false); prevBtn_->setVisible(false);
okBtn_->setVisible(false); okBtn_->setVisible(false);

View File

@ -3,6 +3,8 @@
#include <utility> #include <utility>
#include <QButtonGroup> #include <QButtonGroup>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
@ -12,6 +14,7 @@
#include <QRadioButton> #include <QRadioButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "FormKit.hpp" // makeEditForm / editLabel / capField / addDialogButtons
#include "Theme.hpp" #include "Theme.hpp"
#include "ToastOverlay.hpp" // showToast统一成功轻提示规范 §7.7 #include "ToastOverlay.hpp" // showToast统一成功轻提示规范 §7.7
#include "panels/chart/ScatterDataOps.hpp" // buildSaveRawDataBody纯组装便于单测 #include "panels/chart/ScatterDataOps.hpp" // buildSaveRawDataBody纯组装便于单测
@ -20,9 +23,8 @@
namespace geopro::app { namespace geopro::app {
namespace { namespace {
constexpr int kInversionW = 400; // 原版 inversion 另存为弹窗宽 400px constexpr int kInversionW = 420; // 规范 §7.5 小号对话框宽
constexpr int kRawDataW = 280; // 原版 RawData「数据另存为」弹窗宽 280px constexpr int kRawDataW = 420; // 同上(窄内容仍取小号标准宽,避免局促)
constexpr int kLabelW = 60; // 原版 .label width:60px
} // namespace } // namespace
SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo, QString dsId, SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo, QString dsId,
@ -30,34 +32,32 @@ SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* r
: QDialog(parent), mode_(mode), repo_(repo), dsId_(std::move(dsId)) { : QDialog(parent), mode_(mode), repo_(repo), dsId_(std::move(dsId)) {
setModal(true); setModal(true);
auto* root = new QVBoxLayout(this); // 规范 §7.5 对话框外壳 + §7.0.10 唯一表单实现makeEditForm
root->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg, auto* root = formkit::dialogRoot(this);
geopro::app::space::kLg, geopro::app::space::kLg); auto* form = formkit::makeEditForm();
root->setSpacing(geopro::app::space::kMd);
if (mode_ == Mode::Inversion) { if (mode_ == Mode::Inversion) {
// ── inversion原版「另存为新的网格数据」400px,仅名称行 ── // ── inversion原版「另存为新的网格数据」,仅名称行 ──
setWindowTitle(QStringLiteral("另存为新的网格数据")); setWindowTitle(QStringLiteral("另存为新的网格数据"));
setFixedWidth(kInversionW); setFixedWidth(scaledPx(kInversionW));
auto* nameRow = new QHBoxLayout(); nameLabel_ = formkit::editLabel(QStringLiteral("名称"), this); // 原版 label「名称」
nameRow->setSpacing(geopro::app::space::kMd);
nameLabel_ = new QLabel(QStringLiteral("名称:"), this); // 原版 label「名称:」
nameLabel_->setMinimumWidth(kLabelW);
nameEdit_ = new QLineEdit(this); nameEdit_ = new QLineEdit(this);
nameEdit_->setPlaceholderText(QStringLiteral("请输入名称")); nameEdit_->setPlaceholderText(QStringLiteral("请输入名称"));
nameEdit_->setText(QStringLiteral("网格数据1")); // 原版默认值 nameEdit_->setText(QStringLiteral("网格数据1")); // 原版默认值
nameRow->addWidget(nameLabel_); formkit::capField(nameEdit_);
nameRow->addWidget(nameEdit_, 1); form->addRow(nameLabel_, nameEdit_);
root->addLayout(nameRow); root->addLayout(form);
} else { } else {
// ── RawDatameasurement新增/覆盖 + 名称(对照原版「数据另存为」280px)── // ── RawDatameasurement新增/覆盖 + 名称(对照原版「数据另存为」)──
setWindowTitle(QStringLiteral("数据另存为")); setWindowTitle(QStringLiteral("数据另存为"));
setFixedWidth(kRawDataW); setFixedWidth(scaledPx(kRawDataW));
auto* opLay = new QHBoxLayout(); auto* opWrap = new QWidget(this);
auto* rbNew = new QRadioButton(QStringLiteral("新增"), this); auto* opLay = new QHBoxLayout(opWrap);
auto* rbOverwrite = new QRadioButton(QStringLiteral("覆盖"), this); opLay->setContentsMargins(0, 0, 0, 0);
auto* rbNew = new QRadioButton(QStringLiteral("新增"), opWrap);
auto* rbOverwrite = new QRadioButton(QStringLiteral("覆盖"), opWrap);
opGroup_ = new QButtonGroup(this); opGroup_ = new QButtonGroup(this);
opGroup_->addButton(rbNew, 1); opGroup_->addButton(rbNew, 1);
opGroup_->addButton(rbOverwrite, 0); opGroup_->addButton(rbOverwrite, 0);
@ -65,16 +65,13 @@ SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* r
opLay->addWidget(rbNew); opLay->addWidget(rbNew);
opLay->addWidget(rbOverwrite); opLay->addWidget(rbOverwrite);
opLay->addStretch(); opLay->addStretch();
root->addLayout(opLay); form->addRow(formkit::editLabel(QStringLiteral("操作"), this), opWrap);
auto* nameRow = new QHBoxLayout(); nameLabel_ = formkit::editLabel(QStringLiteral("数据名称"), this);
nameRow->setSpacing(geopro::app::space::kMd);
nameLabel_ = new QLabel(QStringLiteral("数据名称"), this);
nameLabel_->setMinimumWidth(kLabelW);
nameEdit_ = new QLineEdit(this); nameEdit_ = new QLineEdit(this);
nameRow->addWidget(nameLabel_); formkit::capField(nameEdit_);
nameRow->addWidget(nameEdit_, 1); form->addRow(nameLabel_, nameEdit_);
root->addLayout(nameRow); root->addLayout(form);
// 切到覆盖隐藏名称框,切回新增显示。 // 切到覆盖隐藏名称框,切回新增显示。
connect(opGroup_, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) { connect(opGroup_, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) {
@ -84,18 +81,10 @@ SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* r
}); });
} }
// 底部按钮:原版右对齐,确认(主,左)/取消(右)。 // 规范 §7.5 底部操作栏:右对齐 取消(次) + 确认(主);确认需异步保存成功才关闭。
auto* btnLay = new QHBoxLayout(); auto* box = formkit::addDialogButtons(root, this, QStringLiteral("确认"), QStringLiteral("取消"));
btnLay->setSpacing(geopro::app::space::kMd); okBtn_ = box->button(QDialogButtonBox::Ok);
btnLay->addStretch(); QObject::disconnect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
okBtn_->setDefault(true);
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
btnLay->addWidget(okBtn_);
btnLay->addWidget(cancelBtn);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &SaveAsDialog::onConfirm); connect(okBtn_, &QPushButton::clicked, this, &SaveAsDialog::onConfirm);
} }

View File

@ -15,6 +15,7 @@
#include <QSignalBlocker> #include <QSignalBlocker>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "FormKit.hpp" // makeEditForm / editLabel / capField
#include "Theme.hpp" #include "Theme.hpp"
#include "ToastOverlay.hpp" // showToast成功轻提示 #include "ToastOverlay.hpp" // showToast成功轻提示
#include "panels/chart/RangeSlider.hpp" #include "panels/chart/RangeSlider.hpp"
@ -97,16 +98,16 @@ ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository
currentPtsLbl_->setStyleSheet(QStringLiteral("color:%1;").arg(kHighlight)); // 橙色高亮 currentPtsLbl_->setStyleSheet(QStringLiteral("color:%1;").arg(kHighlight)); // 橙色高亮
infoLay->addLayout(statForm); infoLay->addLayout(statForm);
// 最大值在上、最小值在下(对照原版输入框顺序)。 // 最大值在上、最小值在下(对照原版输入框顺序)。可编辑表单走 §7.0.10 唯一实现。
auto* inputForm = new QFormLayout(); auto* inputForm = formkit::makeEditForm();
maxSpin_ = new QDoubleSpinBox(this); maxSpin_ = new QDoubleSpinBox(this);
maxSpin_->setRange(-kSpinRange, kSpinRange); maxSpin_->setRange(-kSpinRange, kSpinRange);
maxSpin_->setDecimals(2); maxSpin_->setDecimals(2);
minSpin_ = new QDoubleSpinBox(this); minSpin_ = new QDoubleSpinBox(this);
minSpin_->setRange(-kSpinRange, kSpinRange); minSpin_->setRange(-kSpinRange, kSpinRange);
minSpin_->setDecimals(2); minSpin_->setDecimals(2);
inputForm->addRow(new QLabel(QStringLiteral("最大值")), maxSpin_); inputForm->addRow(formkit::editLabel(QStringLiteral("最大值"), this), maxSpin_);
inputForm->addRow(new QLabel(QStringLiteral("最小值")), minSpin_); inputForm->addRow(formkit::editLabel(QStringLiteral("最小值"), this), minSpin_);
infoLay->addLayout(inputForm); infoLay->addLayout(inputForm);
// 计算分布 / 重置(信息区中部,对照原版 .filter-actions // 计算分布 / 重置(信息区中部,对照原版 .filter-actions

View File

@ -6,8 +6,9 @@
#include <QComboBox> #include <QComboBox>
#include "EmptyAwareComboBox.hpp" #include "EmptyAwareComboBox.hpp"
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox> #include <QMessageBox>
#include <QPointer> #include <QPointer>
@ -16,7 +17,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "FormKit.hpp" // formkit::comboBox(空态感知下拉) #include "FormKit.hpp" // formkit::comboBox / makeEditForm / editLabel / capField / addDialogButtons
#include "Theme.hpp" #include "Theme.hpp"
#include "panels/chart/InversionProcessOps.hpp" // buildWhitenBody #include "panels/chart/InversionProcessOps.hpp" // buildWhitenBody
#include "repo/IDatasetCommandRepository.hpp" #include "repo/IDatasetCommandRepository.hpp"
@ -24,26 +25,12 @@
namespace geopro::app { namespace geopro::app {
namespace { namespace {
constexpr int kDialogW = 550; // 原版弹窗宽 550px constexpr int kDialogW = 560; // 规范 §7.5 中号对话框宽
constexpr int kLabelMinW = 120; // 原版 .field-label min-width:120px 右对齐
constexpr double kCtrlRatio = 0.6; // 原版控件宽 60%
// 原版 .field-label定宽右对齐标签。 // 把「标签 + 控件」按 §7.0 度量加入表单(右对齐定宽标签列 + 字段宽上限)。
QLabel* fieldLabel(const QString& text, QWidget* parent) { void addFormRow(QFormLayout* form, const QString& label, QWidget* ctrl, QWidget* parent) {
auto* lbl = new QLabel(text, parent); formkit::capField(ctrl);
lbl->setMinimumWidth(kLabelMinW); form->addRow(formkit::editLabel(label, parent), ctrl);
lbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
return lbl;
}
// 原版 .option-itemflex 行(标签右对齐 + 控件占 60%)。控件外裹一层以 60/40 拉伸。
QHBoxLayout* optionRow(const QString& label, QWidget* ctrl, QWidget* parent) {
auto* row = new QHBoxLayout();
row->setSpacing(geopro::app::space::kLg);
row->addWidget(fieldLabel(label, parent));
row->addWidget(ctrl, 6); // 控件 60%
row->addStretch(4); // 余 40% 留白
return row;
} }
} // namespace } // namespace
@ -56,30 +43,30 @@ WhiteningDialog::WhiteningDialog(geopro::data::IDatasetCommandRepository* repo,
tmObjectId_(std::move(tmObjectId)) { tmObjectId_(std::move(tmObjectId)) {
setWindowTitle(QStringLiteral("白化配置")); // 原版 whiteningSetting setWindowTitle(QStringLiteral("白化配置")); // 原版 whiteningSetting
setModal(true); setModal(true);
setFixedWidth(kDialogW); setFixedWidth(scaledPx(kDialogW));
auto* root = new QVBoxLayout(this); // 规范 §7.5 对话框外壳:统一边距 + 行距dialogRoot
root->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg, auto* root = formkit::dialogRoot(this);
geopro::app::space::kLg, geopro::app::space::kLg);
root->setSpacing(geopro::app::space::kMd);
// 白化方式下拉(原版 3 项,数值对照 whiteningMethod 1/2/3 // 白化方式下拉(原版 3 项,数值对照 whiteningMethod 1/2/3
methodCombo_ = new EmptyAwareComboBox(this); methodCombo_ = new EmptyAwareComboBox(this);
methodCombo_->addItem(QStringLiteral("数据边界自动白化"), 1); methodCombo_->addItem(QStringLiteral("数据边界自动白化"), 1);
methodCombo_->addItem(QStringLiteral("白化文件"), 2); methodCombo_->addItem(QStringLiteral("白化文件"), 2);
methodCombo_->addItem(QStringLiteral("模型白化"), 3); methodCombo_->addItem(QStringLiteral("模型白化"), 3);
root->addLayout(optionRow(QStringLiteral("白化方式"), methodCombo_, this)); // §7.0.10 唯一实现makeEditForm + editLabel右对齐定宽标签列
auto* methodForm = formkit::makeEditForm();
addFormRow(methodForm, QStringLiteral("白化方式"), methodCombo_, this);
root->addLayout(methodForm);
stack_ = new QStackedWidget(this); stack_ = new QStackedWidget(this);
root->addWidget(stack_); root->addWidget(stack_);
// ── 方式 1数据边界自动白化边界扩展文本框 + 内/外白化单选)──────────── // ── 方式 1数据边界自动白化边界扩展文本框 + 内/外白化单选)────────────
auto* page1 = new QWidget(this); auto* page1 = new QWidget(this);
auto* p1 = new QVBoxLayout(page1); auto* p1 = formkit::makeEditForm();
p1->setContentsMargins(0, 0, 0, 0); page1->setLayout(p1);
p1->setSpacing(geopro::app::space::kMd);
extension_ = new QLineEdit(QStringLiteral("0"), page1); // 原版 AInput默认 "0" extension_ = new QLineEdit(QStringLiteral("0"), page1); // 原版 AInput默认 "0"
p1->addLayout(optionRow(QStringLiteral("白化边界扩展"), extension_, page1)); addFormRow(p1, QStringLiteral("白化边界扩展"), extension_, page1);
auto* typeWrap = new QWidget(page1); auto* typeWrap = new QWidget(page1);
auto* typeRow = new QHBoxLayout(typeWrap); auto* typeRow = new QHBoxLayout(typeWrap);
typeRow->setContentsMargins(0, 0, 0, 0); typeRow->setContentsMargins(0, 0, 0, 0);
@ -92,22 +79,22 @@ WhiteningDialog::WhiteningDialog(geopro::data::IDatasetCommandRepository* repo,
typeRow->addWidget(rbOuter); typeRow->addWidget(rbOuter);
typeRow->addWidget(rbInner); typeRow->addWidget(rbInner);
typeRow->addStretch(); typeRow->addStretch();
p1->addLayout(optionRow(QStringLiteral("白化"), typeWrap, page1)); p1->addRow(formkit::editLabel(QStringLiteral("白化"), page1), typeWrap);
stack_->addWidget(page1); stack_->addWidget(page1);
// ── 方式 2白化文件选文件────────────────────────────────────── // ── 方式 2白化文件选文件──────────────────────────────────────
auto* page2 = new QWidget(this); auto* page2 = new QWidget(this);
auto* p2 = new QVBoxLayout(page2); auto* p2 = formkit::makeEditForm();
p2->setContentsMargins(0, 0, 0, 0); page2->setLayout(p2);
// 空态感知下拉白化文件异步加载listWhitenedData未选显占位、无文件弹「暂无数据」。 // 空态感知下拉白化文件异步加载listWhitenedData未选显占位、无文件弹「暂无数据」。
fileCombo_ = formkit::comboBox(QStringLiteral("请选择白化文件"), page2); fileCombo_ = formkit::comboBox(QStringLiteral("请选择白化文件"), page2);
p2->addLayout(optionRow(QStringLiteral("选择白化文件"), fileCombo_, page2)); addFormRow(p2, QStringLiteral("选择白化文件"), fileCombo_, page2);
stack_->addWidget(page2); stack_->addWidget(page2);
// ── 方式 3模型白化梯形/矩形)─────────────────────────────────── // ── 方式 3模型白化梯形/矩形)───────────────────────────────────
auto* page3 = new QWidget(this); auto* page3 = new QWidget(this);
auto* p3 = new QVBoxLayout(page3); auto* p3 = formkit::makeEditForm();
p3->setContentsMargins(0, 0, 0, 0); page3->setLayout(p3);
auto* subWrap = new QWidget(page3); auto* subWrap = new QWidget(page3);
auto* subRow = new QHBoxLayout(subWrap); auto* subRow = new QHBoxLayout(subWrap);
subRow->setContentsMargins(0, 0, 0, 0); subRow->setContentsMargins(0, 0, 0, 0);
@ -120,23 +107,14 @@ WhiteningDialog::WhiteningDialog(geopro::data::IDatasetCommandRepository* repo,
subRow->addWidget(rbTrap); subRow->addWidget(rbTrap);
subRow->addWidget(rbRect); subRow->addWidget(rbRect);
subRow->addStretch(); subRow->addStretch();
p3->addLayout(optionRow(QStringLiteral("白化"), subWrap, page3)); p3->addRow(formkit::editLabel(QStringLiteral("白化"), page3), subWrap);
stack_->addWidget(page3); stack_->addWidget(page3);
Q_UNUSED(kCtrlRatio); // 规范 §7.5 底部操作栏:右对齐,取消(次) 左 + 确认(主) 右。
// 确认需先异步 whitenData 成功才关闭 → 断开 Ok 默认 accept改接 onConfirm。
// 底部按钮:原版 justify-content:space-between确认(主,左)/取消(右) 各 45%。 auto* box = formkit::addDialogButtons(root, this, QStringLiteral("确认"), QStringLiteral("取消"));
auto* btnLay = new QHBoxLayout(); okBtn_ = box->button(QDialogButtonBox::Ok);
btnLay->setSpacing(geopro::app::space::kMd); QObject::disconnect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
okBtn_->setDefault(true);
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
btnLay->addWidget(okBtn_, 45);
btnLay->addStretch(10);
btnLay->addWidget(cancelBtn, 45);
root->addLayout(btnLay);
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(okBtn_, &QPushButton::clicked, this, &WhiteningDialog::onConfirm); connect(okBtn_, &QPushButton::clicked, this, &WhiteningDialog::onConfirm);
connect(methodCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, connect(methodCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { onMethodChanged(methodCombo_->currentData().toInt()); }); [this](int) { onMethodChanged(methodCombo_->currentData().toInt()); });

View File

@ -60,6 +60,11 @@
#include <vtkPolyDataMapper.h> #include <vtkPolyDataMapper.h>
#include <vtkProperty.h> #include <vtkProperty.h>
#include <vtkRenderWindow.h> #include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkCallbackCommand.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h>
#include <vtkRenderer.h> #include <vtkRenderer.h>
#include <vtkShortArray.h> #include <vtkShortArray.h>
#include <vtkSmartPointer.h> #include <vtkSmartPointer.h>
@ -653,6 +658,98 @@ geopro::core::ColorScale makeColorScale(double vmin, double vmax) {
return cs; return cs;
} }
// ============================================================================
// 视觉调优共享构件Task 12d ①)
// ============================================================================
//
// 结构化配色:地震/雷达体常用的「结构色阶」——深蓝(强负)→青→白(零)→黄→红(强正)
// 比单纯蓝-白-红更易拉开正负反射层次。值域用数据 vmin/vmax无需手调控制点。
geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) {
geopro::core::ColorScale cs;
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
auto at = [&](double t) { return vmin + span * t; };
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 140, 255}); // 深蓝
cs.addStop(at(0.25), geopro::core::Rgba{0, 160, 220, 255}); // 青
cs.addStop(at(0.50), geopro::core::Rgba{245, 245, 245, 255}); // 白(零附近)
cs.addStop(at(0.75), geopro::core::Rgba{250, 190, 30, 255}); // 黄
cs.addStop(at(1.00), geopro::core::Rgba{170, 0, 0, 255}); // 暗红
return cs;
}
// 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。
// 不透明度调高时光线提前终止fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。
vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys, double maxOpacity,
bool structuralOpacity = true) {
constexpr int kTransferSamples = 64;
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
const double qminD = static_cast<double>(q.toQ(vminPhys));
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
vtkNew<vtkColorTransferFunction> color;
for (int t = 0; t < kTransferSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
const double phys = q.toPhys(qvLevel);
const auto c = cs.colorAt(phys);
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
}
// 不透明度:
// - 原始(structuralOpacity=false):线性单斜坡 [qmin,qmax]→[0,maxOpacity]
// 与 VoxelActor 默认一致,作调优前对照基线。
// - 调优(structuralOpacity=true)双端斜坡。GPR/地震体值多集中在零附近(背景)
// 强反射在正负两端;线性单斜坡会让占多数的近零背景填满体、遮住结构。改为
// 「中段(零附近)透明 + 正负两端不透明」——抑制背景、凸显强反射层,截面结构才看得出。
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
if (structuralOpacity) {
const double qmid = 0.5 * (qminD + qmaxD);
const double half = 0.5 * (qmaxD - qminD);
opacity->AddPoint(qminD, maxOpacity); // 强负反射:不透明
opacity->AddPoint(qmid - 0.30 * half, 0.0); // 近零背景:透明
opacity->AddPoint(qmid + 0.30 * half, 0.0);
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:不透明
} else {
opacity->AddPoint(qminD, 0.0);
opacity->AddPoint(qmaxD, maxOpacity);
}
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
prop->SetColor(color);
prop->SetScalarOpacity(opacity);
prop->SetInterpolationTypeToLinear();
prop->ShadeOff();
return prop;
}
// 由预构建 VTK_SHORT 图像建一个「视觉调优」体:自定义不透明度 + 垂向夸张。
// 垂向夸张用 vtkVolume::SetScale(1, exagg, exagg) 缩放跨通道(Y)与深度(Z)两薄轴,
// 不改图像数据;体物理极扁(X≈2.2km vs Y≈1.5m/Z≈8m),放大薄轴截面结构才看得出。
vtkSmartPointer<vtkVolume> buildTunedVolume(vtkImageData* shortImg,
const geopro::core::Quant& q,
const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys,
double maxOpacity, double exagg,
bool structuralOpacity = true) {
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetInputData(shortImg);
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto prop = makeTunedVolumeProperty(q, cs, vminPhys, vmaxPhys, maxOpacity,
structuralOpacity);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:放大 Y/Z 薄轴
return volume;
}
int cmdRenderB(int argc, char** argv) { int cmdRenderB(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2); const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) { if (a.positional.empty()) {
@ -1761,6 +1858,557 @@ int cmdRenderLOD(int argc, char** argv) {
return valid ? 0 : 1; return valid ? 0 : 1;
} }
// ============================================================================
// ① 视觉调优:出一帧能看结构的图 + 调优前后 fps 对照Task 12d
// ============================================================================
//
// 在【真实金字塔 store】上对局部段(level0 一段 brick 列)与粗层概览(level2 整卷)
// 各跑两遍体绘制 fps调优前(默认色阶 0.15 不透明度 无夸张) vs 调优后(结构色阶 +
// --opacity + --exagg 垂向夸张),离屏存 lod-tuned-local.png / lod-tuned-overview.png
// 并打印前后 fps 对照——证实「视觉调优对 fps 近乎中性」这一探针认知。双闸防假帧率。
int cmdTune(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
"[--frames 120] [--localBricks 4]\n";
return 2;
}
const std::string dir = a.positional[0];
const double opacity = std::stod(a.get("opacity", "0.5"));
const double exagg = std::stod(a.get("exagg", "8"));
const int frames = std::stoi(a.get("frames", "120"));
const int localBricks = std::stoi(a.get("localBricks", "4"));
std::cout << "[tune] storeDir=" << dir << " opacity=" << opacity
<< " exagg=" << exagg << " frames=" << frames << "\n";
std::cout << "[tune] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[tune] 闸门失败,中止。\n";
return 1;
}
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
const int totLevels = store.levels();
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale csPlain = makeColorScale(vmin, vmax);
const geopro::core::ColorScale csTuned = makeStructuralColorScale(vmin, vmax);
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
const int winW = 1024, winH = 768;
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// ---- 局部段level0 一段 brick 列(沿线中段)----
const int totBx = store.bricksX(0);
const int localBx = std::min(localBricks, totBx);
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
vtkSmartPointer<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
int locDims[3];
locImg->GetDimensions(locDims);
// 调优前局部 fps默认色阶 0.15 无夸张)。
auto rwA = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renA;
renA->SetBackground(0.0, 0.0, 0.0);
rwA->AddRenderer(renA);
vtkSmartPointer<vtkVolume> volA =
buildTunedVolume(locImg.Get(), m.quant, csPlain, vmin, vmax, 0.15, 1.0,
/*structuralOpacity=*/false); // 原始线性单斜坡基线
renA->AddVolume(volA);
const double locFpsBefore = benchVolumeFps(rwA.Get(), renA, frames);
// 调优后局部 fps结构色阶 + opacity + exagg
auto rwB = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renB;
renB->SetBackground(0.04, 0.04, 0.08); // 深蓝灰背景,衬托体
rwB->AddRenderer(renB);
vtkSmartPointer<vtkVolume> volB =
buildTunedVolume(locImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
exagg);
renB->AddVolume(volB);
const double locFpsAfter = benchVolumeFps(rwB.Get(), renB, frames);
// 调优后取景:夸张后块更"立体"斜俯视呈现截面层次Zoom 拉近填满画面。
renB->ResetCamera();
renB->GetActiveCamera()->Elevation(28.0);
renB->GetActiveCamera()->Azimuth(30.0);
renB->GetActiveCamera()->Zoom(1.7);
renB->ResetCameraClippingRange();
rwB->Render();
const vtkIdType locNonBlack = countNonBlackPixels(rwB.Get(), winW, winH);
savePng(rwB.Get(), (shotDir / "lod-tuned-local.png").string());
// ---- 概览level2 整卷(接受它就是细带)----
const int ovLevel = std::min(2, totLevels - 1);
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
auto rwO = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renO;
renO->SetBackground(0.04, 0.04, 0.08);
rwO->AddRenderer(renO);
vtkSmartPointer<vtkVolume> volO =
buildTunedVolume(ovImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
exagg);
renO->AddVolume(volO);
const double ovFpsAfter = benchVolumeFps(rwO.Get(), renO, frames);
renO->ResetCamera();
renO->GetActiveCamera()->Elevation(50.0);
renO->GetActiveCamera()->Azimuth(20.0);
renO->ResetCameraClippingRange();
rwO->Render();
const vtkIdType ovNonBlack = countNonBlackPixels(rwO.Get(), winW, winH);
savePng(rwO.Get(), (shotDir / "lod-tuned-overview.png").string());
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const bool valid =
!textureErr && locNonBlack > 0 && ovNonBlack > 0;
const double dropPct =
locFpsBefore > 0 ? (locFpsBefore - locFpsAfter) / locFpsBefore * 100.0
: 0.0;
std::cout << "\n=== tune 视觉调优指标 ===\n";
std::cout << "局部段维度 : " << locDims[0] << "x" << locDims[1] << "x"
<< locDims[2] << " (level0)\n";
std::cout << "调优前局部 fps : "
<< (valid ? std::to_string(locFpsBefore) : "INVALID")
<< " (默认蓝白红, 不透明度 0.15, 无夸张)\n";
std::cout << "调优后局部 fps : "
<< (valid ? std::to_string(locFpsAfter) : "INVALID")
<< " (结构色阶, 不透明度 " << opacity << ", 夸张 " << exagg
<< "x)\n";
std::cout << "fps 变化 : " << dropPct
<< "% (正=变慢/负=变快; 探针预期近乎中性)\n";
std::cout << "调优后概览 fps : "
<< (valid ? std::to_string(ovFpsAfter) : "INVALID") << " (level"
<< ovLevel << ")\n";
std::cout << "双闸 : 纹理错=" << (textureErr ? "" : "")
<< " 局部非空=" << locNonBlack << " 概览非空=" << ovNonBlack
<< "" << (valid ? "可信" : "INVALID") << "\n";
std::cout << "截图 : " << shotDir.string()
<< " (lod-tuned-local.png / lod-tuned-overview.png)\n";
writeMetricLine(
"tune,dir=" + dir + ",opacity=" + std::to_string(opacity) +
",exagg=" + std::to_string(exagg) +
",locDims=" + std::to_string(locDims[0]) + "x" +
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
",locFpsBefore=" + (valid ? std::to_string(locFpsBefore) : "INVALID") +
",locFpsAfter=" + (valid ? std::to_string(locFpsAfter) : "INVALID") +
",dropPct=" + std::to_string(dropPct) +
",ovFpsAfter=" + (valid ? std::to_string(ovFpsAfter) : "INVALID") +
",locNonBlack=" + std::to_string(locNonBlack) +
",ovNonBlack=" + std::to_string(ovNonBlack) +
",valid=" + std::to_string(valid ? 1 : 0));
return valid ? 0 : 1;
}
// ============================================================================
// ② fps 预算:递增全分辨率(level0)窗口找「每帧体素预算」Task 12d
// ============================================================================
//
// 对递增的 level0 brick 列段4,16,64,128,256 brick可 --bricks 覆盖)各重组成
// 局部整卷 image 跑体绘制 fps输出表 brick数/体素数/fps找出 fps 跌破 30 的体素
// 阈值 = production LOD 每帧渲染的全分辨率块数上限。双闸防假帧率。
int cmdFpsBudget(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc fps-budget <storeDir> [--frames 90] "
"[--bricks 4,16,64,128,256]\n";
return 2;
}
const std::string dir = a.positional[0];
const int frames = std::stoi(a.get("frames", "90"));
const double opacity = std::stod(a.get("opacity", "0.5"));
const double exagg = std::stod(a.get("exagg", "8"));
// 解析 brick 段列表(逗号分隔)。
std::vector<int> brickSteps;
{
const std::string raw = a.get("bricks", "4,16,64,128,256");
std::string cur;
for (char ch : raw) {
if (ch == ',') {
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
cur.clear();
} else {
cur.push_back(ch);
}
}
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
}
std::cout << "[fps-budget] storeDir=" << dir << " frames=" << frames << "\n";
std::cout << "[fps-budget] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[fps-budget] 闸门失败,中止,不产出 fps。\n";
return 1;
}
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
const int totBx = store.bricksX(0);
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
std::cout << "[fps-budget] level0=" << m.nx << "x" << m.ny << "x" << m.nz
<< " 总 brick列=" << totBx << " brick=" << m.brick << "\n";
struct Row {
int bricks;
long long voxels;
double fps;
bool valid;
};
std::vector<Row> rows;
constexpr double kTargetFps = 30.0;
long long budgetVoxels = -1; // fps 跌破 30 前的最大体素数
int budgetBricks = -1;
long long firstBelowVoxels = -1;
int firstBelowBricks = -1;
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
for (int nb : brickSteps) {
const int localBx = std::min(nb, totBx);
if (localBx <= 0) continue;
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
vtkSmartPointer<vtkImageData> img =
buildLocalLevel0Image(store, m, bx0, localBx);
int d[3];
img->GetDimensions(d);
const long long voxels =
static_cast<long long>(d[0]) * d[1] * d[2];
auto rw = makeOffscreenWindow(1024, 768);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
vtkSmartPointer<vtkVolume> vol =
buildTunedVolume(img.Get(), m.quant, cs, vmin, vmax, opacity, exagg);
ren->AddVolume(vol);
const double fps = benchVolumeFps(rw.Get(), ren, frames);
// 双闸:纹理无错 + 该段渲出非空像素。
ren->ResetCamera();
rw->Render();
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), 1024, 768);
const bool valid = !capWin->textureError() && nonBlack > 0;
rows.push_back({localBx, voxels, fps, valid});
std::cout << "[fps-budget] brick=" << localBx << " (" << d[0] << "x" << d[1]
<< "x" << d[2] << ") 体素=" << voxels << " fps="
<< (valid ? std::to_string(fps) : "INVALID")
<< " 非空=" << nonBlack << "\n";
if (valid) {
if (fps >= kTargetFps) {
if (voxels > budgetVoxels) {
budgetVoxels = voxels;
budgetBricks = localBx;
}
} else if (firstBelowVoxels < 0) {
firstBelowVoxels = voxels;
firstBelowBricks = localBx;
}
}
}
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const double peak = Probe::peakMemMB();
std::cout << "\n=== fps-budget 每帧体素预算表 ===\n";
std::cout << "| brick段 | 维度体素数 | 体绘制 fps | ≥30 |\n";
std::cout << "|---|---|---|---|\n";
for (const auto& r : rows) {
std::cout << "| " << r.bricks << " | " << r.voxels << " | "
<< (r.valid ? std::to_string(r.fps) : std::string("INVALID"))
<< " | " << (r.valid && r.fps >= kTargetFps ? "" : "")
<< " |\n";
}
std::cout << "\n每帧体素预算(fps≥30 上限) : "
<< (budgetVoxels >= 0 ? std::to_string(budgetVoxels) +
" 体素 (" + std::to_string(budgetBricks) +
" brick列)"
: std::string("未触达(所有测点均 ≥30)"))
<< "\n";
std::cout << "首个跌破 30 的窗口 : "
<< (firstBelowVoxels >= 0
? std::to_string(firstBelowVoxels) + " 体素 (" +
std::to_string(firstBelowBricks) + " brick列)"
: std::string("无(测点未跌破; 需更大 --bricks)"))
<< "\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "")
<< "\n";
std::cout << "进程峰值内存(MB) : " << peak << "\n";
// 落 last-metrics + 追加写 poc-results-C.md。
for (const auto& r : rows) {
writeMetricLine(
"fps-budget,dir=" + dir + ",bricks=" + std::to_string(r.bricks) +
",voxels=" + std::to_string(r.voxels) +
",fps=" + (r.valid ? std::to_string(r.fps) : "INVALID") +
",valid=" + std::to_string(r.valid ? 1 : 0));
}
{
const fs::path repo =
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
fs::create_directories(repo.parent_path());
std::ofstream rf(repo.string(), std::ios::app);
if (rf) {
rf << "\n\n# POC-C fps 预算探针结果Task 12d ②)\n\n";
rf << "金字塔 store: " << dir << "level0=" << m.nx << "x" << m.ny << "x"
<< m.nz << "brick=" << m.brick << "\n\n";
rf << "递增 level0 局部窗口(沿线中段 brick 列)体绘制 fps\n\n";
rf << "| brick段 | 体素数 | 体绘制 fps | ≥30fps |\n|---|---|---|---|\n";
for (const auto& r : rows) {
rf << "| " << r.bricks << " | " << r.voxels << " | "
<< (r.valid ? std::to_string(r.fps) : "INVALID") << " | "
<< (r.valid && r.fps >= kTargetFps ? "" : "") << " |\n";
}
rf << "\n- **每帧体素预算fps≥30 上限)**: "
<< (budgetVoxels >= 0
? std::to_string(budgetVoxels) + " 体素(" +
std::to_string(budgetBricks) + " brick 列)"
: "未触达,所有测点 ≥30fps")
<< "\n";
rf << "- 首个跌破 30 的窗口: "
<< (firstBelowVoxels >= 0
? std::to_string(firstBelowVoxels) + " 体素(" +
std::to_string(firstBelowBricks) + " brick 列)"
: "无(需更大 --bricks 段触达天花板)")
<< "\n";
rf << "- 双闸:纹理维度错误=" << (textureErr ? "" : "")
<< ";每段均按非空像素校验。\n";
rf << "- production LOD 应把【每帧渲染的全分辨率块】卡在此预算以内。\n";
rf << "- **本机 RTX 3060 上限数;最低配需用户在目标机跑 fps-budget/view。**\n";
}
std::cout << "[fps-budget] 报告追加写入 " << repo.string() << "\n";
}
return textureErr ? 1 : 0;
}
// ============================================================================
// ③ view真窗口可交互给用户肉眼测 + 最低配机跑Task 12d
// ============================================================================
//
// 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂
// OutOfCoreSource相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保
// 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。
// 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。
//
// 离屏 smoke--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction/Timer/Render 上)。
struct ViewState {
geopro::render::OutOfCoreSource* src = nullptr;
vtkMultiBlockVolumeMapper* mapper = nullptr;
vtkCamera* cam = nullptr;
vtkTextActor* fpsText = nullptr;
vtkRenderWindow* rw = nullptr;
Stopwatch frameTimer;
double exagg = 8.0;
int lastLevel = -1;
};
// 用 source 当前工作集刷新 mapper 输入(每块成 MultiBlock。返回块数。
std::size_t viewRefreshBlocks(ViewState* st) {
st->src->update(st->cam);
auto imgs = st->src->currentImages();
auto mb = makeMultiBlock(imgs);
st->mapper->SetInputDataObject(mb);
st->mapper->Update();
return imgs.size();
}
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewState*>(clientData);
const double frameMs = st->frameTimer.elapsedMs();
const std::size_t blocks = viewRefreshBlocks(st);
const int lvl = st->src->lastLevel();
const double fps = frameMs > 0 ? 1000.0 / frameMs : 0.0;
char buf[256];
std::snprintf(buf, sizeof(buf),
"fps: %.1f | LOD level: %d | blocks: %zu | exagg: %.0fx",
fps, lvl, blocks, st->exagg);
st->fpsText->SetInput(buf);
st->lastLevel = lvl;
st->rw->Render();
st->frameTimer.reset();
}
int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke]\n";
return 2;
}
const std::string dir = a.positional[0];
const double exagg = std::stod(a.get("exagg", "8"));
const double opacity = std::stod(a.get("opacity", "0.5"));
const std::size_t budget =
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
const bool smoke = a.kv.count("smoke") > 0 ||
std::find(a.positional.begin(), a.positional.end(),
"--smoke") != a.positional.end();
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
<< " opacity=" << opacity << " budget=" << budget
<< (smoke ? " [SMOKE 离屏]" : " [真窗口交互]") << "\n";
const int winW = 1280, winH = 800;
// 核外源(读 meta + 建 pager不载整卷
geopro::render::OutOfCoreSource src(dir, budget);
const auto& m = src.meta();
src.setAspect(static_cast<double>(winW) / winH);
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeTunedVolumeProperty(m.quant, cs, vmin, vmax, opacity);
// 渲染窗口smoke 走离屏,否则真窗口。
vtkSmartPointer<vtkRenderWindow> rw;
if (smoke) {
rw = makeOffscreenWindow(winW, winH);
} else {
rw = vtkSmartPointer<vtkRenderWindow>::New();
rw->SetSize(winW, winH);
rw->SetWindowName("gpr_poc view —— 核外 LOD 体绘制 (滚轮缩放切 LOD, 左键旋转)");
}
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.04, 0.04, 0.08);
rw->AddRenderer(ren);
vtkNew<vtkMultiBlockVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, exagg, exagg); // 垂向夸张(同 ①)
ren->AddVolume(volume);
// 屏幕左上角实时 fps 文本。
vtkNew<vtkTextActor> fpsText;
fpsText->SetInput("fps: -- | LOD level: --");
fpsText->GetTextProperty()->SetFontSize(20);
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
fpsText->SetDisplayPosition(12, winH - 30);
ren->AddViewProp(fpsText);
// 捕获式 OutputWindow拦截块上传纹理错
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
ViewState st;
st.src = &src;
st.mapper = mapper.Get();
st.fpsText = fpsText.Get();
st.rw = rw.Get();
st.exagg = exagg;
// 相机初始定向:先框整体选出工作集,再 ResetCamera 到工作集包围盒(同 renderC
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
m.origin[1], m.origin[1] + m.ny * m.spacing[1] * exagg,
m.origin[2], m.origin[2] + m.nz * m.spacing[2] * exagg);
st.cam = ren->GetActiveCamera();
const std::size_t warm = viewRefreshBlocks(&st);
{
double b[6];
mapper->GetBounds(b);
if (b[0] <= b[1]) {
// 工作集包围盒需按 exagg 缩放后再框actor 已 SetScale
ren->ResetCamera();
}
}
st.cam->Elevation(25.0);
st.cam->Azimuth(25.0);
ren->ResetCameraClippingRange();
rw->Render();
std::cout << "[view] 预热: level=" << src.lastLevel() << " 视野块="
<< src.lastVisibleCount() << "/" << src.lastLevelBrickTotal()
<< " 驻留=" << src.residentCount() << " 渲染块=" << warm << "\n";
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH);
const bool textureErr = capWin->textureError();
const bool renderedOk = !textureErr && nonBlack > 0;
if (smoke) {
// 离屏 smoke模拟一次缩放 → 验 LOD 切换 + 不崩。
const int lvlNear = src.lastLevel();
st.cam->Dolly(0.2); // 拉远 → 期望切粗 LOD
ren->ResetCameraClippingRange();
const std::size_t blocksFar = viewRefreshBlocks(&st);
const int lvlFar = src.lastLevel();
rw->Render();
st.cam->Dolly(8.0); // 拉近 → 期望切细 LOD
ren->ResetCameraClippingRange();
viewRefreshBlocks(&st);
const int lvlNear2 = src.lastLevel();
rw->Render();
const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH);
vtkOutputWindow::SetInstance(nullptr);
const bool lodSwitched = (lvlFar != lvlNear) || (lvlNear2 != lvlFar);
const bool ok = renderedOk && nb2 > 0 && !capWin->textureError();
std::cout << "\n=== view --smoke 离屏冒烟 ===\n";
std::cout << "近观 level=" << lvlNear << " → 拉远 level=" << lvlFar
<< " → 再拉近 level=" << lvlNear2 << "\n";
std::cout << "LOD 随缩放切换 : " << (lodSwitched ? "是 ✔" : "否(测点档位未跨界)")
<< " (blocksFar=" << blocksFar << ")\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "") << "\n";
std::cout << "渲出非空像素 : " << (renderedOk ? "" : "否(!!)")
<< " (近=" << nonBlack << " 远拉近=" << nb2 << ")\n";
std::cout << "smoke 结果 : " << (ok ? "OK ✔ 不崩" : "FAIL ✘") << "\n";
return ok ? 0 : 1;
}
vtkOutputWindow::SetInstance(nullptr);
if (!renderedOk) {
std::cout << "[view] 警告: 首帧未渲出非空像素(纹理错=" << textureErr
<< ");窗口仍开,供人工排查。\n";
}
// 真窗口交互TrackballCamera + 每次交互结束重选 LOD + 刷 fps 文本。
vtkNew<vtkRenderWindowInteractor> iren;
iren->SetRenderWindow(rw);
vtkNew<vtkInteractorStyleTrackballCamera> style;
iren->SetInteractorStyle(style);
vtkNew<vtkCallbackCommand> cb;
cb->SetCallback(viewOnInteract);
cb->SetClientData(&st);
// EndInteraction旋转/缩放松手后重选 LOD保证 LOD 真切换 + fps 刷新)。
iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
// 每帧 Render 后也更新一次 fps 文本(连续拖动时实时反馈)。
rw->AddObserver(vtkCommand::EndEvent, cb);
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
st.frameTimer.reset();
iren->Initialize();
rw->Render();
iren->Start();
std::cout << "[view] 窗口关闭,退出。\n";
return 0;
}
void usage() { void usage() {
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n" std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] " " gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
@ -1771,7 +2419,13 @@ void usage() {
" gpr_poc renderB <storeDir> [--frames 120]\n" " gpr_poc renderB <storeDir> [--frames 120]\n"
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n" " gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n"
" gpr_poc renderC-partitioned <storeDir> [--frames 120]\n" " gpr_poc renderC-partitioned <storeDir> [--frames 120]\n"
" gpr_poc renderLOD <storeDir> [--frames 120]\n"; " gpr_poc renderLOD <storeDir> [--frames 120]\n"
" gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
"[--frames 120] [--localBricks 4]\n"
" gpr_poc fps-budget <storeDir> [--frames 90] "
"[--bricks 4,16,64,128,256]\n"
" gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke]\n";
} }
} // namespace } // namespace
@ -1792,6 +2446,9 @@ int main(int argc, char** argv) {
if (cmd == "renderC-partitioned") if (cmd == "renderC-partitioned")
return cmdRenderCPartitioned(argc, argv); return cmdRenderCPartitioned(argc, argv);
if (cmd == "renderLOD") return cmdRenderLOD(argc, argv); if (cmd == "renderLOD") return cmdRenderLOD(argc, argv);
if (cmd == "tune") return cmdTune(argc, argv);
if (cmd == "fps-budget") return cmdFpsBudget(argc, argv);
if (cmd == "view") return cmdView(argc, argv);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << "\n"; std::cerr << "错误: " << e.what() << "\n";
return 1; return 1;