feat/vtk-3d-view #7
|
|
@ -0,0 +1,80 @@
|
|||
# Task 12c 报告:LOD-fps 探针(全量交互渲染最后一根链子)
|
||||
|
||||
## 状态
|
||||
|
||||
**完成 / PASS** —— 四件事(a/b/c/d)全做,双闸通过(无纹理维度错误 + 三段均回读非空像素),
|
||||
真实实测,未编造。LOD-based C 路线在本机判据下钉死可行。
|
||||
|
||||
## 实测数字(本机 RTX 3060 Laptop GPU,离屏,frames=120,多次重跑稳定)
|
||||
|
||||
| 项 | 维度 | 结果 | 交互级判据 |
|
||||
|---|---|---|---|
|
||||
| (a) 粗层概览 fps | level2 整卷 11119×8×41 (~3.6M 体素) | **~752 fps**(多跑 590~759) | ✔ 远超 ≥30 |
|
||||
| (b) 全分辨率局部 fps | level0 局部 256×29×162 (~120 万体素,4 brick 列) | **~380 fps**(多跑 374~422) | ✔ 远超 ≥30 |
|
||||
| (c) LOD 切换过渡 | 切换帧 60/120,从远观(level2)dolly 拉近到近观局部(level0) | 平均 **1.09ms/帧**,切换帧 **~5.5ms**(尖峰 ~6×邻帧),最大 ~6.95ms | 无可感知卡顿 ✔ |
|
||||
|
||||
- **粗层概览 fps**:~752 fps(达交互级 ✔)
|
||||
- **全分辨率局部 fps**:~380 fps(达交互级 ✔)
|
||||
- **LOD 切换过渡帧耗时 / 是否卡顿**:切换帧 ~5.5ms(仍 <1 个 60Hz 帧 16.7ms)→ **无可感知卡顿**
|
||||
- **截图路径**:`docs/superpowers/plans/poc-lod-shots/`
|
||||
- `lod-overview.png`(level2 整线概览,全 2200m 线呈细带)
|
||||
- `lod-fullres-local.png`(level0 局部,全分辨率板面有细节)
|
||||
- `lod-transition-mid.png`(切换后推近的过渡中间帧)
|
||||
- **是否都达交互级**:**是**。(a)/(b) 均 >>30fps;(c) 切换无可感知卡顿。
|
||||
|
||||
## 设计与诚实测法
|
||||
|
||||
- 在真实金字塔 store(`gpr_poc build ... --levels 3`,level0=44476×29×162,
|
||||
level1=22238×15×81,level2=11119×8×41,level3=5560×4×21)上跑,非合成。
|
||||
- (a)/(b):把对应 level 的所有 brick 重组成单张 VTK_SHORT vtkImageData
|
||||
(逻辑同 `WholeVolumeSource`,按 level 维度 + spacing×2^level / 局部段 X 偏移),
|
||||
喂 `buildVoxelI16FromImage`(SmartVolumeMapper,GPU 路径),旋相机 120 帧测 fps。
|
||||
level2/局部段单轴均 <16384 → 单 3D 纹理可成,无纹理墙。
|
||||
- (c):同一窗口,相机从远观(level2 整卷)dolly 拉近;第 60 帧跨越 LOD 切换那一下
|
||||
把体从 level2 概览换成 level0 局部 + 焦点移到局部段中心,**逐帧记帧耗时**,
|
||||
标切换帧尖峰。这是审核人加的验收点①(测切换动态,非两端静态)。
|
||||
- (d):`vtkWindowToImageFilter`+`vtkPNGWriter` 存 3 张 PNG,供人眼判
|
||||
“概览糊→拉近清晰”(审核人验收点②)。
|
||||
- **双闸(同 9c,绝不把空纹理假帧率当性能)**:
|
||||
① `CapturingOutputWindow` 捕获 3D 纹理维度错误(实测=否);
|
||||
② 真实回读前缓冲像素,统计非背景像素(概览 1889 / 局部 167612 / 过渡 21924,
|
||||
三段均非空)。两闸全过,fps 可信。
|
||||
|
||||
## 卡顿判据说明(避免误报)
|
||||
|
||||
切换帧含一次性建 actor / 换 mapper 输入,~5.5ms,是邻帧(~0.9ms)的 ~6×;但绝对值
|
||||
仍 < 1 个 60Hz 帧(16.7ms),人眼不可感。故采用**绝对耗时判据**:切换帧 >33ms(2 帧)
|
||||
才记“可感知卡顿”,16.7~33ms 记“轻微抖动”,亚毫秒基线下尖峰倍数虽大但绝对值低不算
|
||||
卡顿。本机切换帧 ~5.5ms → 无可感知卡顿。
|
||||
|
||||
## 判据结论
|
||||
|
||||
粗层概览 + 全分辨率局部**都达交互级**(≥30fps,远超)且切换**无不可接受卡顿**
|
||||
→ 命中 brief 第一条判据:**LOD-based C 路线钉死可行**。
|
||||
|
||||
对照 12b:整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限;本探针证实
|
||||
“渲更少体素 = LOD” 这根杠杆有效——粗层 ~752fps、全分辨率局部 ~380fps,两端都远
|
||||
在交互级,且 LOD 切换瞬态 ~5.5ms 无卡顿。
|
||||
|
||||
## 最低配未验声明(审核人验收点③)
|
||||
|
||||
本探针**仅在本机(RTX 3060 Laptop GPU,NVIDIA 555.97,OpenGL 4.5)跑得上限数字**。
|
||||
**最低配机器未验证**,需用户在目标机跑 `gpr_poc renderLOD <store>` 或提供型号后再评估。
|
||||
本机数字是上限,最低配可能更低。
|
||||
|
||||
## 进程峰值内存
|
||||
|
||||
~99 MB(探针逐 level 重组单张 image,未常驻整卷;level0 局部仅取 4 brick 列)。
|
||||
|
||||
## Concerns
|
||||
|
||||
1. **截图视觉偏暗/偏细**:体绘制 `kMaxOpacity=0.15`(复用探针传函)+ 整线物理纵横比
|
||||
极扁(2200m × ~1.5m × 8m),故概览图中整线呈一条细带、过渡中间帧呈小斜板。
|
||||
这是物理真实呈现(整线本就是长薄带),非渲染缺陷;但作为“人眼判可接受度”素材
|
||||
偏素净。若需更醒目的生产视觉,需后续调传函不透明度/着色与取景,超出探针范畴(YAGNI)。
|
||||
2. **(c) 为单次脚本化切换**:测的是“从 level2 直切 level0 局部”一次硬切的瞬态;
|
||||
生产里多级连续 LOD/视野自适应的换页节奏、预取与 morphing/淡入是探针过了之后的
|
||||
工程(brief 明确不在本探针范围)。
|
||||
3. **(b) 局部仅取 4 brick 列(256 体素宽)**:证“全分辨率局部块快”;若生产需更宽的
|
||||
全分辨率窗口(仍需 <16384 或分区/分块),fps 会随体素数下降,需届时按窗口大小复测。
|
||||
4. **最低配仍是最大未知**(见上声明)。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -25,3 +25,25 @@
|
|||
|
||||
## 判据结论
|
||||
单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(10.9517 fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。
|
||||
|
||||
|
||||
|
||||
# POC-C LOD-fps 探针结果(Task 12c)
|
||||
|
||||
金字塔 store: tmp\store_lod_001(level0=44476x29x162,总 4 层)
|
||||
|
||||
| 项 | 维度 | 结果 |
|
||||
|---|---|---|
|
||||
| (a) 粗层概览 fps | level2 11119x8x41 | 752.061589 fps (交互级) |
|
||||
| (b) 全分辨率局部 fps | level0 局部 256x29x162 | 374.625725 fps (交互级) |
|
||||
| (c) LOD 切换过渡 | 切换帧 60/120 | 平均 1.09062ms,切换帧 5.4629ms(尖峰 6.04704×),无可感知卡顿 |
|
||||
|
||||
- 卡顿判据:切换帧绝对耗时 >33ms(2 个 60Hz 帧)才记可感知卡顿;16.7~33ms 记轻微抖动;亚毫秒基线下尖峰倍数大但绝对值低不算卡顿。
|
||||
- 双闸:纹理维度错误=否;三段均渲出非空像素=是(概览 1889 / 局部 167612 / 过渡 21924)。
|
||||
- 截图(人眼判“概览糊→拉近清晰”):docs/superpowers/plans/poc-lod-shots/lod-overview.png、lod-fullres-local.png、lod-transition-mid.png
|
||||
- 进程峰值内存: 99.2266 MB
|
||||
|
||||
## 判据结论
|
||||
粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ LOD-based C 路线钉死可行。
|
||||
|
||||
**最低配未验声明**:本探针仅在本机(RTX 3060)跑得上限数字,最低配机器未验证,需用户在目标机跑或提供型号。
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
find_package(VTK REQUIRED COMPONENTS
|
||||
CommonCore CommonDataModel
|
||||
RenderingCore RenderingOpenGL2 RenderingVolume RenderingVolumeOpenGL2
|
||||
ImagingCore InteractionStyle GUISupportQt)
|
||||
ImagingCore InteractionStyle GUISupportQt IOImage)
|
||||
|
||||
add_executable(gpr_poc main.cpp)
|
||||
|
||||
|
|
|
|||
|
|
@ -55,14 +55,18 @@
|
|||
#include <vtkVolumeProperty.h>
|
||||
#include <vtkObjectFactory.h>
|
||||
#include <vtkOutputWindow.h>
|
||||
#include <vtkPNGWriter.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkShortArray.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
#include <vtkSmartVolumeMapper.h>
|
||||
#include <vtkUnsignedCharArray.h>
|
||||
#include <vtkVolume.h>
|
||||
#include <vtkWindowToImageFilter.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using geopro::tools::Probe;
|
||||
|
|
@ -1317,6 +1321,446 @@ int cmdRenderCPartitioned(int argc, char** argv) {
|
|||
return volFpsValid ? 0 : 1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LOD-fps 探针(POC-C 最后一根链子,Task 12c)
|
||||
// ============================================================================
|
||||
//
|
||||
// 12b 已证整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限,fps 杠杆只有 LOD
|
||||
// (渲更少体素)。本探针在【真实金字塔 store】上验四件事,全离屏、双闸防假帧率:
|
||||
// (a) 粗层概览 fps:level2 整卷(单轴 <16384 → 单 SmartVolumeMapper)。
|
||||
// (b) 全分辨率局部 fps:level0 一段 brick 列(沿线局部)。
|
||||
// (c) LOD 切换动态过渡:相机从远观(level2)逐步拉近到近观局部(level0),跨越
|
||||
// LOD 切换那一下逐帧记帧耗时,标切换帧尖峰/stall。
|
||||
// (d) 截图:lod-overview.png / lod-fullres-local.png / lod-transition-mid.png。
|
||||
//
|
||||
// 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||||
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||||
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
|
||||
|
||||
// 把金字塔某 level 重组成整卷 VTK_SHORT vtkImageData(逻辑同 WholeVolumeSource,
|
||||
// 但按 level 维度 + spacing×2^level,使物理范围与 level0 一致)。
|
||||
vtkSmartPointer<vtkImageData> buildLevelImage(
|
||||
const geopro::data::ChunkedVolumeStore& store, int level,
|
||||
const geopro::data::StoreMeta& m) {
|
||||
int nx = 0, ny = 0, nz = 0;
|
||||
store.dims(level, nx, ny, nz);
|
||||
const int brick = m.brick;
|
||||
const double sc = static_cast<double>(1 << level); // 2^level
|
||||
|
||||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||||
img->SetDimensions(nx, ny, nz);
|
||||
img->SetOrigin(m.origin[0], m.origin[1], m.origin[2]);
|
||||
img->SetSpacing(m.spacing[0] * sc, m.spacing[1] * sc, m.spacing[2] * sc);
|
||||
|
||||
vtkNew<vtkShortArray> arr;
|
||||
arr->SetName("v");
|
||||
arr->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
|
||||
|
||||
for (int bz = 0; bz < store.bricksZ(level); ++bz) {
|
||||
for (int by = 0; by < store.bricksY(level); ++by) {
|
||||
for (int bx = 0; bx < store.bricksX(level); ++bx) {
|
||||
const std::vector<std::int16_t> raw = store.readBrick(level, bx, by, bz);
|
||||
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
|
||||
const int bw = (nx - i0 < brick) ? (nx - i0) : brick;
|
||||
const int bh = (ny - j0 < brick) ? (ny - j0) : brick;
|
||||
const int bd = (nz - k0 < brick) ? (nz - k0) : brick;
|
||||
std::size_t w = 0;
|
||||
for (int kk = 0; kk < bd; ++kk) {
|
||||
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
|
||||
for (int jj = 0; jj < bh; ++jj) {
|
||||
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
|
||||
vtkIdType id = (gk * ny + gj) * nx + i0;
|
||||
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
img->GetPointData()->SetScalars(arr);
|
||||
return img;
|
||||
}
|
||||
|
||||
// 取 level0 一段 brick 列 [bx0, bx0+bxCount) × 全 Y × 全 Z 重组成局部整卷
|
||||
// VTK_SHORT image(X 维 = bxCount*brick ≤ 几百,远 <16384,单 3D 纹理)。
|
||||
// Origin 沿 X 偏移到该段起点,spacing 用 level0 原值。
|
||||
vtkSmartPointer<vtkImageData> buildLocalLevel0Image(
|
||||
const geopro::data::ChunkedVolumeStore& store,
|
||||
const geopro::data::StoreMeta& m, int bx0, int bxCount) {
|
||||
const int brick = m.brick;
|
||||
const int nx0 = m.nx, ny0 = m.ny, nz0 = m.nz;
|
||||
const int totBx = store.bricksX(0);
|
||||
bx0 = std::max(0, std::min(bx0, totBx - 1));
|
||||
bxCount = std::max(1, std::min(bxCount, totBx - bx0));
|
||||
|
||||
const int i0Global = bx0 * brick;
|
||||
const int localNx = std::min(bxCount * brick, nx0 - i0Global);
|
||||
|
||||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||||
img->SetDimensions(localNx, ny0, nz0);
|
||||
img->SetOrigin(m.origin[0] + i0Global * m.spacing[0], m.origin[1],
|
||||
m.origin[2]);
|
||||
img->SetSpacing(m.spacing[0], m.spacing[1], m.spacing[2]);
|
||||
|
||||
vtkNew<vtkShortArray> arr;
|
||||
arr->SetName("v");
|
||||
arr->SetNumberOfTuples(static_cast<vtkIdType>(localNx) * ny0 * nz0);
|
||||
|
||||
for (int bz = 0; bz < store.bricksZ(0); ++bz) {
|
||||
for (int by = 0; by < store.bricksY(0); ++by) {
|
||||
for (int bx = bx0; bx < bx0 + bxCount; ++bx) {
|
||||
const std::vector<std::int16_t> raw = store.readBrick(0, bx, by, bz);
|
||||
const int gi0 = bx * brick, j0 = by * brick, k0 = bz * brick;
|
||||
const int li0 = gi0 - i0Global; // 局部 X 起点
|
||||
const int bw = (nx0 - gi0 < brick) ? (nx0 - gi0) : brick;
|
||||
const int bh = (ny0 - j0 < brick) ? (ny0 - j0) : brick;
|
||||
const int bd = (nz0 - k0 < brick) ? (nz0 - k0) : brick;
|
||||
std::size_t w = 0;
|
||||
for (int kk = 0; kk < bd; ++kk) {
|
||||
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
|
||||
for (int jj = 0; jj < bh; ++jj) {
|
||||
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
|
||||
vtkIdType id = (gk * ny0 + gj) * localNx + li0;
|
||||
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
img->GetPointData()->SetScalars(arr);
|
||||
return img;
|
||||
}
|
||||
|
||||
// 统计当前窗口前缓冲非背景像素(>10 任一通道)。
|
||||
vtkIdType countNonBlackPixels(vtkRenderWindow* rw, int w, int h) {
|
||||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, px);
|
||||
vtkIdType nb = 0;
|
||||
const vtkIdType np = px->GetNumberOfTuples();
|
||||
for (vtkIdType i = 0; i < np; ++i) {
|
||||
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
|
||||
px->GetComponent(i, 2) > 10) {
|
||||
++nb;
|
||||
}
|
||||
}
|
||||
return nb;
|
||||
}
|
||||
|
||||
// 离屏窗口截图 → PNG。
|
||||
void savePng(vtkRenderWindow* rw, const std::string& path) {
|
||||
rw->Render();
|
||||
vtkNew<vtkWindowToImageFilter> w2i;
|
||||
w2i->SetInput(rw);
|
||||
w2i->SetInputBufferTypeToRGB();
|
||||
w2i->ReadFrontBufferOff();
|
||||
w2i->Update();
|
||||
vtkNew<vtkPNGWriter> writer;
|
||||
writer->SetFileName(path.c_str());
|
||||
writer->SetInputConnection(w2i->GetOutputPort());
|
||||
writer->Write();
|
||||
}
|
||||
|
||||
int cmdRenderLOD(int argc, char** argv) {
|
||||
const Args a = parseArgs(argc, argv, 2);
|
||||
if (a.positional.empty()) {
|
||||
std::cerr << "用法: gpr_poc renderLOD <storeDir> [--frames 120]\n";
|
||||
return 2;
|
||||
}
|
||||
const std::string dir = a.positional[0];
|
||||
const int frames = std::stoi(a.get("frames", "120"));
|
||||
std::cout << "[renderLOD] storeDir=" << dir << " frames=" << frames << "\n";
|
||||
|
||||
// 闸门复检:不可渲染机不产假 fps。
|
||||
std::cout << "[renderLOD] 离屏闸门复检...\n";
|
||||
if (cmdOffscreenSmoke() != 0) {
|
||||
std::cout << "[renderLOD] 闸门失败,中止,不产出 fps。\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
geopro::data::ChunkedVolumeStore store(dir);
|
||||
const geopro::data::StoreMeta& m = store.meta();
|
||||
const int totLevels = store.levels();
|
||||
std::cout << "[renderLOD] level0=" << m.nx << "x" << m.ny << "x" << m.nz
|
||||
<< " 总层数=" << totLevels << "\n";
|
||||
if (totLevels < 3) {
|
||||
std::cout << "[renderLOD] 警告: 金字塔层数 <3(需 build --levels 3)。\n";
|
||||
}
|
||||
|
||||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||||
|
||||
const fs::path shotDir =
|
||||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||||
fs::create_directories(shotDir);
|
||||
|
||||
const int winW = 1024, winH = 768;
|
||||
|
||||
// 共用一个捕获式 OutputWindow,贯穿三段渲染。
|
||||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||||
vtkOutputWindow::SetInstance(capWin);
|
||||
|
||||
// ---- (a) 粗层概览 fps:level2 整卷 ----
|
||||
const int ovLevel = std::min(2, totLevels - 1);
|
||||
std::cout << "[renderLOD] (a) 建 level" << ovLevel << " 整卷 image...\n";
|
||||
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
|
||||
int ovNx, ovNy, ovNz;
|
||||
store.dims(ovLevel, ovNx, ovNy, ovNz);
|
||||
|
||||
auto rwOv = makeOffscreenWindow(winW, winH);
|
||||
vtkNew<vtkRenderer> renOv;
|
||||
renOv->SetBackground(0.0, 0.0, 0.0);
|
||||
rwOv->AddRenderer(renOv);
|
||||
vtkSmartPointer<vtkVolume> ovVol =
|
||||
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
|
||||
vmax);
|
||||
renOv->AddVolume(ovVol);
|
||||
// 先测 fps(benchVolumeFps 内部会 ResetCamera + 旋满一圈)。
|
||||
const double ovFps = benchVolumeFps(rwOv.Get(), renOv, frames);
|
||||
// 截图前重设一个利于人眼的取景:整线物理纵横比极扁(~2200m×1.5m×8m),俯视角
|
||||
// 看宽面才能呈现整条带(而非边缘线)。
|
||||
renOv->ResetCamera();
|
||||
renOv->GetActiveCamera()->Elevation(55.0);
|
||||
renOv->GetActiveCamera()->Azimuth(20.0);
|
||||
renOv->ResetCameraClippingRange();
|
||||
rwOv->Render();
|
||||
const vtkIdType ovNonBlack = countNonBlackPixels(rwOv.Get(), winW, winH);
|
||||
savePng(rwOv.Get(), (shotDir / "lod-overview.png").string());
|
||||
std::cout << "[renderLOD] (a) 概览 fps=" << ovFps << " 非空像素=" << ovNonBlack
|
||||
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
|
||||
<< ")\n";
|
||||
|
||||
// ---- (b) 全分辨率局部 fps:level0 一段 brick 列 ----
|
||||
const int totBx = store.bricksX(0);
|
||||
const int localBx = std::min(4, totBx); // 4 brick 列 ≈ 256 体素宽
|
||||
const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 取沿线中段
|
||||
std::cout << "[renderLOD] (b) 建 level0 局部 image (brick列 [" << bx0 << ","
|
||||
<< (bx0 + localBx) << ") / " << totBx << ")...\n";
|
||||
vtkSmartPointer<vtkImageData> locImg =
|
||||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||||
int locDims[3];
|
||||
locImg->GetDimensions(locDims);
|
||||
|
||||
auto rwLoc = makeOffscreenWindow(winW, winH);
|
||||
vtkNew<vtkRenderer> renLoc;
|
||||
renLoc->SetBackground(0.0, 0.0, 0.0);
|
||||
rwLoc->AddRenderer(renLoc);
|
||||
vtkSmartPointer<vtkVolume> locVol =
|
||||
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
|
||||
vmax);
|
||||
renLoc->AddVolume(locVol);
|
||||
const double locFps = benchVolumeFps(rwLoc.Get(), renLoc, frames);
|
||||
// 截图取景:局部块(256×29×162)斜俯视,呈现全分辨率细节供与概览对比。
|
||||
renLoc->ResetCamera();
|
||||
renLoc->GetActiveCamera()->Elevation(35.0);
|
||||
renLoc->GetActiveCamera()->Azimuth(25.0);
|
||||
renLoc->ResetCameraClippingRange();
|
||||
rwLoc->Render();
|
||||
const vtkIdType locNonBlack = countNonBlackPixels(rwLoc.Get(), winW, winH);
|
||||
savePng(rwLoc.Get(), (shotDir / "lod-fullres-local.png").string());
|
||||
std::cout << "[renderLOD] (b) 局部 fps=" << locFps << " 非空像素="
|
||||
<< locNonBlack << " (level0 局部 " << locDims[0] << "x" << locDims[1]
|
||||
<< "x" << locDims[2] << ")\n";
|
||||
|
||||
// ---- (c) LOD 切换动态过渡 ----
|
||||
// 同一窗口:相机从远观(看整卷,用 level2 概览体)逐步 dolly 拉近,到一半处
|
||||
// 跨越 LOD 切换——把体从 level2 整卷换成 level0 局部体(重设 mapper 输入/相机
|
||||
// 目标),逐帧记帧耗时,标切换帧尖峰。
|
||||
std::cout << "[renderLOD] (c) LOD 切换动态过渡(" << frames << " 帧 dolly)...\n";
|
||||
auto rwTr = makeOffscreenWindow(winW, winH);
|
||||
vtkNew<vtkRenderer> renTr;
|
||||
renTr->SetBackground(0.0, 0.0, 0.0);
|
||||
rwTr->AddRenderer(renTr);
|
||||
|
||||
// 远观体 = level2 概览(新建一份,避免与 (a) 共享 actor 状态)。
|
||||
vtkSmartPointer<vtkVolume> farVol =
|
||||
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
|
||||
vmax);
|
||||
// 近观体 = level0 局部(复用 (b) 的 image)。
|
||||
vtkSmartPointer<vtkVolume> nearVol =
|
||||
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
|
||||
vmax);
|
||||
|
||||
renTr->AddVolume(farVol);
|
||||
renTr->ResetCamera(); // 框住整卷(level2 与 level0 物理范围一致)
|
||||
vtkCamera* camTr = renTr->GetActiveCamera();
|
||||
camTr->Elevation(20.0);
|
||||
renTr->ResetCameraClippingRange();
|
||||
rwTr->Render(); // 预热远观
|
||||
|
||||
// dolly 目标:从当前(远)拉近到局部段中心。
|
||||
double locCenter[3];
|
||||
locImg->GetCenter(locCenter);
|
||||
const int switchFrame = frames / 2;
|
||||
const double dollyPerFrame =
|
||||
std::pow(6.0, 1.0 / std::max(1, switchFrame)); // 切换前累计 dolly≈6×
|
||||
|
||||
std::vector<double> frameMs(frames, 0.0);
|
||||
bool switched = false;
|
||||
double switchStallMs = 0.0;
|
||||
|
||||
for (int f = 0; f < frames; ++f) {
|
||||
Stopwatch swF;
|
||||
if (f == switchFrame && !switched) {
|
||||
// —— LOD 切换那一下 ——:换体 + 把相机焦点移到局部段中心。
|
||||
renTr->RemoveVolume(farVol);
|
||||
renTr->AddVolume(nearVol);
|
||||
camTr->SetFocalPoint(locCenter[0], locCenter[1], locCenter[2]);
|
||||
renTr->ResetCameraClippingRange();
|
||||
switched = true;
|
||||
}
|
||||
// 渐进拉近(切换前 dolly 进;切换后继续推近 + 轻微环绕,逐步框满局部块)。
|
||||
camTr->Dolly(switched ? 1.04 : dollyPerFrame);
|
||||
if (switched) camTr->Azimuth(0.5);
|
||||
renTr->ResetCameraClippingRange();
|
||||
rwTr->Render();
|
||||
frameMs[f] = swF.elapsedMs();
|
||||
if (f == switchFrame) switchStallMs = frameMs[f];
|
||||
// 切换后推近一小段再截“过渡中间帧”,使局部块已明显呈现(而非切换瞬间仍很远)。
|
||||
if (f == switchFrame + (frames - switchFrame) / 3) {
|
||||
savePng(rwTr.Get(), (shotDir / "lod-transition-mid.png").string());
|
||||
}
|
||||
}
|
||||
|
||||
// 过渡帧耗时统计:平均、最大、切换帧、切换帧相对邻帧的尖峰倍数。
|
||||
double sum = 0, mx = 0;
|
||||
for (double v : frameMs) {
|
||||
sum += v;
|
||||
mx = std::max(mx, v);
|
||||
}
|
||||
const double avgMs = frames > 0 ? sum / frames : 0.0;
|
||||
const double preMs =
|
||||
switchFrame > 0 ? frameMs[switchFrame - 1] : avgMs;
|
||||
const double spikeRatio = preMs > 0 ? switchStallMs / preMs : 0.0;
|
||||
// 可感知卡顿判据(绝对耗时为准,尖峰倍数仅作次级信号):当两端帧耗时是亚毫秒
|
||||
// 时,一次性换体的 ~9ms 抖动倍数虽大但仍 <1 个 60Hz 帧(16.7ms),人眼不可感。
|
||||
// 故:切换帧 >1 个 60Hz 帧(16.7ms)才记“轻微”,>2 帧(33ms)记“可感知卡顿”。
|
||||
constexpr double kFrame60Ms = 1000.0 / 60.0; // 16.7ms
|
||||
const bool perceptibleStall = switchStallMs > 2.0 * kFrame60Ms; // >33ms
|
||||
const bool minorHitch =
|
||||
!perceptibleStall && switchStallMs > kFrame60Ms; // 16.7~33ms 轻微
|
||||
const vtkIdType trNonBlack = countNonBlackPixels(rwTr.Get(), winW, winH);
|
||||
|
||||
const bool textureErr = capWin->textureError();
|
||||
vtkOutputWindow::SetInstance(nullptr);
|
||||
|
||||
// 双闸:无纹理错 + 三段均渲出非空像素。
|
||||
const bool renderedNonEmpty =
|
||||
(ovNonBlack > 0) && (locNonBlack > 0) && (trNonBlack > 0);
|
||||
const bool valid = !textureErr && renderedNonEmpty;
|
||||
|
||||
const double ovFpsV = valid ? ovFps : -1.0;
|
||||
const double locFpsV = valid ? locFps : -1.0;
|
||||
const bool ovInteractive = valid && ovFps >= 15.0;
|
||||
const bool locInteractive = valid && locFps >= 15.0;
|
||||
const double peak = Probe::peakMemMB();
|
||||
|
||||
const char* stallTxt =
|
||||
perceptibleStall ? "可感知卡顿" : (minorHitch ? "轻微抖动(<2帧)" : "无");
|
||||
std::cout << "[renderLOD] (c) 过渡帧耗时 avg=" << avgMs << "ms max=" << mx
|
||||
<< "ms 切换帧=" << switchStallMs << "ms (邻帧 " << preMs << "ms, 尖峰 "
|
||||
<< spikeRatio << "×) 卡顿=" << stallTxt << "\n";
|
||||
|
||||
std::cout << "\n=== renderLOD LOD-fps 探针指标 ===\n";
|
||||
std::cout << "离屏闸门 : OK\n";
|
||||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||||
std::cout << "三段均渲出非空 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||||
<< " (概览=" << ovNonBlack << " 局部=" << locNonBlack
|
||||
<< " 过渡=" << trNonBlack << ")\n";
|
||||
std::cout << "(a) 粗层概览 fps : "
|
||||
<< (valid ? std::to_string(ovFpsV) : std::string("INVALID"))
|
||||
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
|
||||
<< ") 交互级=" << (ovInteractive ? "是 ✔" : "否 ✘") << "\n";
|
||||
std::cout << "(b) 全分辨率局部fps: "
|
||||
<< (valid ? std::to_string(locFpsV) : std::string("INVALID"))
|
||||
<< " (level0 局部 " << locDims[0] << "x" << locDims[1] << "x"
|
||||
<< locDims[2] << ") 交互级=" << (locInteractive ? "是 ✔" : "否 ✘")
|
||||
<< "\n";
|
||||
std::cout << "(c) 过渡平均/最大 : " << avgMs << " / " << mx << " ms\n";
|
||||
std::cout << " 切换帧耗时 : " << switchStallMs << " ms (邻帧 " << preMs
|
||||
<< " ms, 尖峰 " << spikeRatio << "×)\n";
|
||||
std::cout << " 可感知卡顿 : " << stallTxt
|
||||
<< (perceptibleStall ? " ✘" : " ✔") << " (判据:切换帧 >33ms 才记卡顿"
|
||||
"; 1 帧 60Hz=16.7ms)\n";
|
||||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||||
std::cout << "截图 : " << shotDir.string()
|
||||
<< " (lod-overview / lod-fullres-local / lod-transition-mid)\n";
|
||||
|
||||
writeMetricLine(
|
||||
"renderLOD,dir=" + dir + ",totLevels=" + std::to_string(totLevels) +
|
||||
",ovLevel=" + std::to_string(ovLevel) +
|
||||
",ovDims=" + std::to_string(ovNx) + "x" + std::to_string(ovNy) + "x" +
|
||||
std::to_string(ovNz) +
|
||||
",ovFps=" + (valid ? std::to_string(ovFpsV) : "INVALID") +
|
||||
",ovNonBlack=" + std::to_string(ovNonBlack) +
|
||||
",locDims=" + std::to_string(locDims[0]) + "x" +
|
||||
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
|
||||
",locFps=" + (valid ? std::to_string(locFpsV) : "INVALID") +
|
||||
",locNonBlack=" + std::to_string(locNonBlack) +
|
||||
",trAvgMs=" + std::to_string(avgMs) + ",trMaxMs=" + std::to_string(mx) +
|
||||
",switchMs=" + std::to_string(switchStallMs) +
|
||||
",switchSpike=" + std::to_string(spikeRatio) +
|
||||
",stall=" + std::to_string(perceptibleStall ? 1 : 0) +
|
||||
",trNonBlack=" + std::to_string(trNonBlack) +
|
||||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||||
",valid=" + std::to_string(valid ? 1 : 0) +
|
||||
",peakMB=" + std::to_string(peak));
|
||||
|
||||
// 写 poc-results-C.md 的 LOD 段(追加,不覆盖 renderC-partitioned 段)。
|
||||
{
|
||||
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 LOD-fps 探针结果(Task 12c)\n\n";
|
||||
rf << "金字塔 store: " << dir << "(level0=" << m.nx << "x" << m.ny << "x"
|
||||
<< m.nz << ",总 " << totLevels << " 层)\n\n";
|
||||
rf << "| 项 | 维度 | 结果 |\n|---|---|---|\n";
|
||||
rf << "| (a) 粗层概览 fps | level" << ovLevel << " " << ovNx << "x" << ovNy
|
||||
<< "x" << ovNz << " | " << (valid ? std::to_string(ovFpsV) : "INVALID")
|
||||
<< " fps " << (ovInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
|
||||
rf << "| (b) 全分辨率局部 fps | level0 局部 " << locDims[0] << "x"
|
||||
<< locDims[1] << "x" << locDims[2] << " | "
|
||||
<< (valid ? std::to_string(locFpsV) : "INVALID") << " fps "
|
||||
<< (locInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
|
||||
rf << "| (c) LOD 切换过渡 | 切换帧 " << switchFrame << "/" << frames
|
||||
<< " | 平均 " << avgMs << "ms,切换帧 " << switchStallMs << "ms(尖峰 "
|
||||
<< spikeRatio << "×),"
|
||||
<< (perceptibleStall ? "可感知卡顿"
|
||||
: (minorHitch ? "轻微抖动" : "无可感知卡顿"))
|
||||
<< " |\n\n";
|
||||
rf << "- 卡顿判据:切换帧绝对耗时 >33ms(2 个 60Hz 帧)才记可感知卡顿;"
|
||||
"16.7~33ms 记轻微抖动;亚毫秒基线下尖峰倍数大但绝对值低不算卡顿。\n";
|
||||
rf << "- 双闸:纹理维度错误=" << (textureErr ? "是" : "否")
|
||||
<< ";三段均渲出非空像素=" << (renderedNonEmpty ? "是" : "否")
|
||||
<< "(概览 " << ovNonBlack << " / 局部 " << locNonBlack << " / 过渡 "
|
||||
<< trNonBlack << ")。\n";
|
||||
rf << "- 截图(人眼判“概览糊→拉近清晰”):docs/superpowers/plans/poc-lod-shots/"
|
||||
"lod-overview.png、lod-fullres-local.png、lod-transition-mid.png\n";
|
||||
rf << "- 进程峰值内存: " << peak << " MB\n\n";
|
||||
rf << "## 判据结论\n";
|
||||
if (valid && ovInteractive && locInteractive && !perceptibleStall) {
|
||||
rf << "粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ "
|
||||
"LOD-based C 路线钉死可行。\n";
|
||||
} else if (valid && ovInteractive && !locInteractive) {
|
||||
rf << "粗层快但全分辨率局部仍慢 → VTK 体绘制有真实天花板,记录,"
|
||||
"评估 OpenVDS/自建 GL。\n";
|
||||
} else if (valid && perceptibleStall) {
|
||||
rf << "两端 fps 可接受但切换卡顿明显(切换帧 " << switchStallMs
|
||||
<< "ms)→ 为后续 morphing/淡入提供依据。\n";
|
||||
} else if (!valid) {
|
||||
rf << "双闸未过(纹理错或空渲染)→ 数字不可信,如实标 INVALID。\n";
|
||||
} else {
|
||||
rf << "部分达标,详见上表。\n";
|
||||
}
|
||||
rf << "\n**最低配未验声明**:本探针仅在本机(RTX 3060)跑得上限数字,"
|
||||
"最低配机器未验证,需用户在目标机跑或提供型号。\n";
|
||||
}
|
||||
std::cout << "[renderLOD] 报告追加写入 " << repo.string() << "\n";
|
||||
}
|
||||
|
||||
return valid ? 0 : 1;
|
||||
}
|
||||
|
||||
void usage() {
|
||||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||||
|
|
@ -1326,7 +1770,8 @@ void usage() {
|
|||
" gpr_poc offscreen-smoke\n"
|
||||
" gpr_poc renderB <storeDir> [--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";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
@ -1346,6 +1791,7 @@ int main(int argc, char** argv) {
|
|||
if (cmd == "renderC") return cmdRenderC(argc, argv);
|
||||
if (cmd == "renderC-partitioned")
|
||||
return cmdRenderCPartitioned(argc, argv);
|
||||
if (cmd == "renderLOD") return cmdRenderLOD(argc, argv);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "错误: " << e.what() << "\n";
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue