3074 lines
133 KiB
C++
3074 lines
133 KiB
C++
// gpr_poc —— POC-B headless 度量 CLI。
|
||
//
|
||
// 串起整条地基:发现 14 通道 .iprb + .ord → assembleGprSurvey → buildGprVolume
|
||
// → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load),
|
||
// 在真实/合成数据上输出可测的真实指标(耗时/维度/体积/压缩比/加载/峰值内存)。
|
||
//
|
||
// 子命令:
|
||
// gpr_poc build <dir> [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out <storeDir>] [--levels 2]
|
||
// gpr_poc load <storeDir>
|
||
// gpr_poc selftest
|
||
// gpr_poc offscreen-smoke —— 离屏 GL 闸门冒烟
|
||
// gpr_poc renderB <storeDir> [--frames 120] —— 离屏体绘制/切片 fps 基准
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <cstdlib>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <iostream>
|
||
#include <map>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "Probe.hpp"
|
||
|
||
#include "core/algo/GprVolumeBuilder.hpp"
|
||
#include "core/algo/IInterpolator.hpp"
|
||
#include "core/model/GprSurvey.hpp"
|
||
#include "core/model/ColorScale.hpp"
|
||
#include "core/model/ScalarVolumeI16.hpp"
|
||
#include "data/store/ChunkedVolumeStore.hpp"
|
||
#include "io/gpr/GprSurveyAssembler.hpp"
|
||
#include "render/actors/VoxelActor.hpp"
|
||
#include "render/source/OutOfCoreSource.hpp"
|
||
#include "render/source/WholeVolumeSource.hpp"
|
||
|
||
// ---- VTK 离屏渲染 ----
|
||
#include <vtkActor.h>
|
||
#include <vtkCamera.h>
|
||
#include <vtkCubeSource.h>
|
||
#include <vtkGPUVolumeRayCastMapper.h>
|
||
#include <vtkOpenGLGPUVolumeRayCastMapper.h>
|
||
#include <vtkImageActor.h>
|
||
#include <vtkImageData.h>
|
||
#include <vtkImageMapToColors.h>
|
||
#include <vtkImageMapper3D.h>
|
||
#include <vtkImageReslice.h>
|
||
#include <vtkLookupTable.h>
|
||
#include <vtkColorTransferFunction.h>
|
||
#include <vtkMultiBlockDataSet.h>
|
||
#include <vtkMultiBlockVolumeMapper.h>
|
||
#include <vtkNew.h>
|
||
#include <vtkPiecewiseFunction.h>
|
||
#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 <vtkRenderWindowInteractor.h>
|
||
#include <vtkInteractorStyleTrackballCamera.h>
|
||
#include <vtkCallbackCommand.h>
|
||
#include <vtkTextActor.h>
|
||
#include <vtkTextProperty.h>
|
||
#include <vtkRenderer.h>
|
||
#include <vtkShortArray.h>
|
||
#include <vtkSmartPointer.h>
|
||
#include <vtkSmartVolumeMapper.h>
|
||
#include <vtkUnsignedCharArray.h>
|
||
#include <vtkVolume.h>
|
||
#include <vtkWindowToImageFilter.h>
|
||
|
||
#ifdef _WIN32
|
||
#include <windows.h> // SetConsoleOutputCP(修中文控制台 GBK 乱码)
|
||
#endif
|
||
|
||
namespace fs = std::filesystem;
|
||
using geopro::tools::Probe;
|
||
using geopro::tools::Stopwatch;
|
||
|
||
namespace {
|
||
|
||
constexpr int kChannels = 14;
|
||
|
||
// ---- 命令行参数解析(极简 --key value)----
|
||
struct Args {
|
||
std::map<std::string, std::string> kv;
|
||
std::vector<std::string> positional;
|
||
|
||
std::string get(const std::string& key, const std::string& def) const {
|
||
auto it = kv.find(key);
|
||
return it != kv.end() ? it->second : def;
|
||
}
|
||
};
|
||
|
||
Args parseArgs(int argc, char** argv, int start) {
|
||
Args a;
|
||
for (int i = start; i < argc; ++i) {
|
||
std::string tok = argv[i];
|
||
if (tok.rfind("--", 0) == 0) {
|
||
// 下一个 token 也是 --xxx(或本 token 是末尾)→ 本 token 是无值布尔旗标,
|
||
// 存空串(避免把后面的旗标误当成本旗标的值,如 --preview --near)。
|
||
const bool hasValue =
|
||
(i + 1 < argc) && std::string(argv[i + 1]).rfind("--", 0) != 0;
|
||
a.kv[tok.substr(2)] = hasValue ? argv[i + 1] : "";
|
||
if (hasValue) ++i;
|
||
} else {
|
||
a.positional.push_back(tok);
|
||
}
|
||
}
|
||
return a;
|
||
}
|
||
|
||
// 把一行指标追加写入 last-metrics.txt(与可执行同目录的工具源目录无关,
|
||
// 写到当前工作目录便于汇总;CSV 风格一行)。
|
||
void writeMetricLine(const std::string& line) {
|
||
std::ofstream f("last-metrics.txt", std::ios::app);
|
||
if (f) f << line << "\n";
|
||
}
|
||
|
||
// 发现某线 14 通道 .iprb(按通道号 A01..A14 排序)+ 该线 .ord。
|
||
struct LineFiles {
|
||
std::vector<std::string> iprb; // 已按通道号排序
|
||
std::string ord;
|
||
};
|
||
|
||
LineFiles discoverLine(const std::string& dir, const std::string& line) {
|
||
LineFiles lf;
|
||
std::map<int, std::string> byChannel; // 通道号 → 路径(自动按号排序)
|
||
std::string ordPath;
|
||
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
const std::string ext = e.path().extension().string();
|
||
|
||
// .ord:优先匹配本线(含 "_<line>" 且以 .ord 结尾),否则记下工区任一 .ord 作兜底。
|
||
if (ext == ".ord") {
|
||
if (name.find("_" + line + ".") != std::string::npos) {
|
||
ordPath = e.path().string();
|
||
} else if (ordPath.empty()) {
|
||
ordPath = e.path().string();
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// .iprb:匹配 "*_<line>_A<NN>.iprb"。
|
||
if (ext != ".iprb") continue;
|
||
const std::string tag = "_" + line + "_A";
|
||
const std::size_t pos = name.find(tag);
|
||
if (pos == std::string::npos) continue;
|
||
const std::size_t numStart = pos + tag.size();
|
||
std::size_t numEnd = numStart;
|
||
while (numEnd < name.size() &&
|
||
std::isdigit(static_cast<unsigned char>(name[numEnd]))) {
|
||
++numEnd;
|
||
}
|
||
if (numEnd == numStart) continue;
|
||
const int ch = std::stoi(name.substr(numStart, numEnd - numStart));
|
||
byChannel[ch] = e.path().string();
|
||
}
|
||
|
||
for (const auto& [ch, path] : byChannel) lf.iprb.push_back(path);
|
||
lf.ord = ordPath;
|
||
return lf;
|
||
}
|
||
|
||
// 由 survey 推 GridSpec:X 沿测线,Y 跨通道,Z 深度。
|
||
geopro::core::GridSpec specFromSurvey(const geopro::core::GprSurvey& s,
|
||
double cellXY, double cellZ) {
|
||
geopro::core::GridSpec spec{};
|
||
|
||
const double rangeX =
|
||
(s.ntraces > 1) ? (s.ntraces - 1) * s.dx : 0.0;
|
||
const double y0 = s.channelY.empty() ? 0.0 : s.channelY.front();
|
||
const double y1 = s.channelY.empty() ? 0.0 : s.channelY.back();
|
||
const double rangeY = y1 - y0;
|
||
const double rangeZ =
|
||
(s.samples > 1) ? (s.samples - 1) * s.dz : 0.0;
|
||
|
||
auto cells = [](double range, double cell) {
|
||
if (cell <= 0.0) return 1;
|
||
return static_cast<int>(std::ceil(range / cell)) + 1;
|
||
};
|
||
|
||
spec.ox = s.x0;
|
||
spec.oy = y0;
|
||
spec.oz = s.z0;
|
||
spec.dx = cellXY;
|
||
spec.dy = cellXY;
|
||
spec.dz = cellZ;
|
||
spec.nx = cells(rangeX, cellXY);
|
||
spec.ny = cells(rangeY, cellXY);
|
||
spec.nz = cells(rangeZ, cellZ);
|
||
spec.power = 2.0;
|
||
spec.maxDist = cellXY * 2.0;
|
||
return spec;
|
||
}
|
||
|
||
// 落盘 data.bin 体积(所有 data*.bin 之和,含金字塔各级)。
|
||
std::int64_t storeDataBytes(const std::string& dir) {
|
||
std::int64_t total = 0;
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
if (name.rfind("data", 0) == 0 &&
|
||
e.path().extension().string() == ".bin") {
|
||
total += static_cast<std::int64_t>(e.file_size());
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
int cmdBuild(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const std::string line = a.get("line", "001");
|
||
const double cellXY = std::stod(a.get("cellXY", "0.2"));
|
||
const double cellZ = std::stod(a.get("cellZ", "0.05"));
|
||
const int levels = std::stoi(a.get("levels", "2"));
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / ("gpr_store_" + line)).string());
|
||
|
||
std::cout << "[build] dir=" << dir << " line=" << line
|
||
<< " cellXY=" << cellXY << " cellZ=" << cellZ
|
||
<< " levels=" << levels << " out=" << out << "\n";
|
||
|
||
const LineFiles lf = discoverLine(dir, line);
|
||
std::cout << "[build] 发现通道数=" << lf.iprb.size()
|
||
<< " ord=" << (lf.ord.empty() ? "(无)" : lf.ord) << "\n";
|
||
if (lf.iprb.size() != static_cast<std::size_t>(kChannels)) {
|
||
std::cerr << "[build] 警告: 通道数 != " << kChannels
|
||
<< "(仍按发现数继续)\n";
|
||
}
|
||
if (lf.iprb.empty() || lf.ord.empty()) {
|
||
std::cerr << "[build] 错误: 未发现 .iprb 或 .ord\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 装配
|
||
Stopwatch swAsm;
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(lf.iprb, lf.ord);
|
||
const double asmMs = swAsm.elapsedMs();
|
||
std::cout << "[build] 装配完成 ntraces=" << survey.ntraces
|
||
<< " samples=" << survey.samples
|
||
<< " channels=" << survey.channelY.size()
|
||
<< " dx=" << survey.dx << " dz=" << survey.dz << "\n";
|
||
|
||
// 2) 建体
|
||
const geopro::core::GridSpec spec = specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[build] GridSpec nx=" << spec.nx << " ny=" << spec.ny
|
||
<< " nz=" << spec.nz << " dx=" << spec.dx << " dy=" << spec.dy
|
||
<< " dz=" << spec.dz << " maxDist=" << spec.maxDist << "\n";
|
||
|
||
Stopwatch swBuild;
|
||
geopro::core::BuiltI16 built = geopro::core::buildGprVolume(survey, spec);
|
||
const double buildMs = swBuild.elapsedMs();
|
||
|
||
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(), nz = built.vol.nz();
|
||
const std::int64_t voxels = nx * ny * nz;
|
||
const std::int64_t rawBytes = voxels * 2; // int16
|
||
|
||
// 3) 落盘 + 金字塔
|
||
fs::create_directories(out);
|
||
Stopwatch swWrite;
|
||
geopro::data::ChunkedVolumeStore::write(out, built);
|
||
const double writeMs = swWrite.elapsedMs();
|
||
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramid(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build 指标 ===\n";
|
||
std::cout << "装配耗时(ms) : " << asmMs << "\n";
|
||
std::cout << "建体耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "体维度 : " << nx << " x " << ny << " x " << nz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build,line=" + line + ",cellXY=" + std::to_string(cellXY) +
|
||
",cellZ=" + std::to_string(cellZ) + ",nx=" + std::to_string(nx) +
|
||
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) + ",asmMs=" + std::to_string(asmMs) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",writeMs=" + std::to_string(writeMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
int cmdLoad(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc load <storeDir>\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
std::cout << "[load] storeDir=" << dir << "\n";
|
||
|
||
Stopwatch sw;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = sw.elapsedMs();
|
||
|
||
const auto& m = src.meta();
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== load 指标 ===\n";
|
||
std::cout << "加载耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "整卷维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine("load,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) +
|
||
",nz=" + std::to_string(m.nz) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ---- selftest:合成极小数据走完整 build→load 管线 ----
|
||
|
||
// 写一个极小通道的 .iprb + .iprh(samples 采样、traces 道,值 = base + t + s)。
|
||
void writeSyntheticChannel(const fs::path& iprbPath, int samples, int traces,
|
||
std::int16_t base) {
|
||
const fs::path iprhPath =
|
||
fs::path(iprbPath).replace_extension(".iprh");
|
||
std::ofstream h(iprhPath);
|
||
h << "SAMPLES: " << samples << "\n";
|
||
h << "LAST TRACE: " << (traces - 1) << "\n";
|
||
h << "CHANNELS: 2\n";
|
||
h << "TIMEWINDOW: 100.0\n";
|
||
h << "SOIL VELOCITY: 100.0\n"; // m/µs → ×1e6 → 1e8 m/s
|
||
h << "DISTANCE INTERVAL: 0.05\n";
|
||
h.close();
|
||
|
||
std::ofstream b(iprbPath, std::ios::binary);
|
||
// 布局 [trace*samples + s],s 最快。
|
||
for (int t = 0; t < traces; ++t) {
|
||
for (int s = 0; s < samples; ++s) {
|
||
const std::int16_t v =
|
||
static_cast<std::int16_t>(base + t + s);
|
||
b.write(reinterpret_cast<const char*>(&v), sizeof(v));
|
||
}
|
||
}
|
||
}
|
||
|
||
int cmdSelftest() {
|
||
std::cout << "[selftest] 构造极小合成 survey(2 通道)...\n";
|
||
const fs::path tmp =
|
||
fs::temp_directory_path() / "gpr_poc_selftest";
|
||
std::error_code ec;
|
||
fs::remove_all(tmp, ec);
|
||
fs::create_directories(tmp);
|
||
|
||
const int samples = 8;
|
||
const int traces = 12;
|
||
|
||
// 2 通道 .iprb/.iprh + .ord(末列==1 标记有效通道,第 2 列为横偏 Y)。
|
||
writeSyntheticChannel(tmp / "syn_001_A01.iprb", samples, traces,
|
||
/*base=*/100);
|
||
writeSyntheticChannel(tmp / "syn_001_A02.iprb", samples, traces,
|
||
/*base=*/200);
|
||
{
|
||
std::ofstream ord(tmp / "syn_001.ord");
|
||
ord << "0 0.000000 -1.5 1\n";
|
||
ord << "1 1.000000 -1.5 1\n";
|
||
}
|
||
|
||
const std::vector<std::string> iprb = {
|
||
(tmp / "syn_001_A01.iprb").string(),
|
||
(tmp / "syn_001_A02.iprb").string()};
|
||
const std::string ord = (tmp / "syn_001.ord").string();
|
||
|
||
bool ok = true;
|
||
auto check = [&](bool cond, const std::string& msg) {
|
||
if (!cond) {
|
||
std::cerr << "[selftest] FAIL: " << msg << "\n";
|
||
ok = false;
|
||
}
|
||
};
|
||
|
||
try {
|
||
// 装配
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(iprb, ord);
|
||
check(survey.ntraces == traces, "ntraces");
|
||
check(survey.samples == samples, "samples");
|
||
check(survey.channelY.size() == 2, "channels");
|
||
// channelY 升序:A01 偏移 0.0 在前,A02 偏移 1.0 在后。
|
||
check(survey.channelY.front() < survey.channelY.back(), "channelY 升序");
|
||
|
||
// 建体:cellXY 取通道间距 1.0 → ny=2;cellZ 较细确保 nz>1。
|
||
const double cellXY = 1.0;
|
||
const double cellZ = std::max(survey.dz, 1e-12);
|
||
const geopro::core::GridSpec spec =
|
||
specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[selftest] GridSpec " << spec.nx << "x" << spec.ny << "x"
|
||
<< spec.nz << " dz=" << spec.dz << "\n";
|
||
check(spec.ny == 2, "ny==2");
|
||
|
||
geopro::core::BuiltI16 built =
|
||
geopro::core::buildGprVolume(survey, spec);
|
||
check(built.vol.nx() == spec.nx, "built nx");
|
||
check(built.vol.ny() == spec.ny, "built ny");
|
||
check(built.vol.nz() == spec.nz, "built nz");
|
||
|
||
// 落盘 + 金字塔
|
||
const std::string store = (tmp / "store").string();
|
||
fs::create_directories(store);
|
||
geopro::data::ChunkedVolumeStore::write(store, built, /*brick=*/4);
|
||
{
|
||
geopro::data::ChunkedVolumeStore s(store);
|
||
s.buildPyramid(1);
|
||
check(s.levels() == 2, "金字塔层数==2");
|
||
}
|
||
|
||
// 加载整卷,校验维度一致
|
||
geopro::render::WholeVolumeSource src(store);
|
||
check(src.meta().nx == spec.nx, "load nx");
|
||
check(src.meta().ny == spec.ny, "load ny");
|
||
check(src.meta().nz == spec.nz, "load nz");
|
||
|
||
// 某体素值合理性:x0/y0 角点应有非 blank 量化值(落格命中首道首通道)。
|
||
const std::int16_t q = built.vol.at(0, 0, 0);
|
||
check(q != geopro::core::ScalarVolumeI16::kBlank, "(0,0,0) 非 blank");
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "[selftest] 异常: " << e.what() << "\n";
|
||
ok = false;
|
||
}
|
||
|
||
fs::remove_all(tmp, ec);
|
||
std::cout << "[selftest] " << (ok ? "PASS" : "FAIL") << "\n";
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 离屏 GPU 渲染基准(POC-B)
|
||
// ============================================================================
|
||
|
||
// 捕获 VTK 错误输出的 OutputWindow:用于侦测体绘制时 vtkVolumeTexture 报的
|
||
// "Invalid texture dimensions" / "MAX_3D_TEXTURE_SIZE" —— 一旦出现,说明整卷
|
||
// 单张 3D 纹理上传失败,体绘制 fps 无意义,必须如实标 INVALID(绝不当真上报)。
|
||
class CapturingOutputWindow : public vtkOutputWindow {
|
||
public:
|
||
static CapturingOutputWindow* New();
|
||
vtkTypeMacro(CapturingOutputWindow, vtkOutputWindow);
|
||
|
||
void DisplayText(const char* txt) override {
|
||
if (txt) {
|
||
const std::string s(txt);
|
||
captured_ += s;
|
||
if (s.find("texture dimensions") != std::string::npos ||
|
||
s.find("MAX_3D_TEXTURE_SIZE") != std::string::npos) {
|
||
textureError_ = true;
|
||
}
|
||
}
|
||
// 仍透传到 stderr,便于人工查看。
|
||
if (txt) std::cerr << txt;
|
||
}
|
||
|
||
bool textureError() const { return textureError_; }
|
||
const std::string& captured() const { return captured_; }
|
||
|
||
private:
|
||
std::string captured_;
|
||
bool textureError_ = false;
|
||
};
|
||
vtkStandardNewMacro(CapturingOutputWindow);
|
||
|
||
// 创建一个离屏 vtkRenderWindow(VTK9.6:SetShowWindow(false)+OffScreenRenderingOn)。
|
||
vtkSmartPointer<vtkRenderWindow> makeOffscreenWindow(int w, int h) {
|
||
auto rw = vtkSmartPointer<vtkRenderWindow>::New();
|
||
rw->SetOffScreenRendering(1);
|
||
rw->SetShowWindow(false);
|
||
rw->SetSize(w, h);
|
||
return rw;
|
||
}
|
||
|
||
// 闸门:最小离屏渲染冒烟。返回 0=OK,非 0=离屏 GL 起不来(BLOCKED_OFFSCREEN)。
|
||
// 流程:离屏窗口 → 加一个 cube actor → Render() → 读回像素,确认非全黑/读得到。
|
||
int cmdOffscreenSmoke() {
|
||
std::cout << "[offscreen-smoke] 创建离屏 vtkRenderWindow...\n";
|
||
try {
|
||
auto rw = makeOffscreenWindow(256, 256);
|
||
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.1, 0.1, 0.2);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkNew<vtkCubeSource> cube;
|
||
cube->SetXLength(1.0);
|
||
cube->SetYLength(1.0);
|
||
cube->SetZLength(1.0);
|
||
|
||
vtkNew<vtkPolyDataMapper> mapper;
|
||
mapper->SetInputConnection(cube->GetOutputPort());
|
||
|
||
vtkNew<vtkActor> actor;
|
||
actor->SetMapper(mapper);
|
||
actor->GetProperty()->SetColor(1.0, 0.6, 0.2);
|
||
ren->AddActor(actor);
|
||
ren->ResetCamera();
|
||
|
||
// Render():若 GL 上下文创建失败,VTK 会输出错误(多数返回,少数抛)。
|
||
rw->Render();
|
||
|
||
// 读回像素验证:取整窗 RGB,确认能读到且非全 0。
|
||
const int* sz = rw->GetSize();
|
||
const int w = sz[0], h = sz[1];
|
||
if (w <= 0 || h <= 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 窗口尺寸为 0(上下文未建立)\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
// GetRGBACharPixelData(x0,y0,x1,y1,front,arr):front=1 读前缓冲。
|
||
const int ok =
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, pixels);
|
||
if (ok == 0 || pixels->GetNumberOfTuples() == 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 读不到像素\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
// 统计非背景像素(cube 应渲出橙色,存在像素 R 通道明显高于背景)。
|
||
vtkIdType nonBlack = 0;
|
||
const vtkIdType n = pixels->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < n; ++i) {
|
||
const double r = pixels->GetComponent(i, 0);
|
||
const double g = pixels->GetComponent(i, 1);
|
||
const double b = pixels->GetComponent(i, 2);
|
||
if (r > 80 || g > 80 || b > 80) ++nonBlack;
|
||
}
|
||
|
||
const char* caps = rw->ReportCapabilities();
|
||
std::cout << "[offscreen-smoke] 读回像素 " << n << " 个,非背景像素 "
|
||
<< nonBlack << "\n";
|
||
std::cout << "[offscreen-smoke] GL 能力:\n"
|
||
<< (caps ? caps : "(无)") << "\n";
|
||
|
||
if (nonBlack == 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 渲染结果全为背景(actor 未画出)\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
std::cout << "[offscreen-smoke] OK:离屏 GL 可用,可继续真实基准。\n";
|
||
std::cout << "STATUS=OK\n";
|
||
return 0;
|
||
} catch (const std::exception& e) {
|
||
std::cout << "[offscreen-smoke] FAIL: 异常 " << e.what() << "\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// 体绘制 fps:每帧绕 azimuth 旋相机再 Render(),避免被驱动优化成空渲染。
|
||
double benchVolumeFps(vtkRenderWindow* rw, vtkRenderer* ren, int frames) {
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
rw->Render(); // 预热一帧(首帧含上传显存/编译 shader,不计时)
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames); // 每帧转一点,扫满一圈
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
|
||
}
|
||
|
||
// 切片扫描 fps:沿 K 轴(深度)逐偏移 reslice 取轴向切面 + 纹理渲染,每帧推进偏移。
|
||
double benchSliceFps(vtkRenderWindow* rw, vtkRenderer* ren,
|
||
vtkImageData* full, vtkLookupTable* lut, int frames) {
|
||
// reslice:固定轴向(XY 平面),沿 Z 改变 ResliceAxesOrigin 扫过整卷。
|
||
vtkNew<vtkImageReslice> reslice;
|
||
reslice->SetInputData(full);
|
||
reslice->SetOutputDimensionality(2);
|
||
reslice->SetInterpolationModeToLinear();
|
||
|
||
vtkNew<vtkImageMapToColors> colorize;
|
||
colorize->SetLookupTable(lut);
|
||
colorize->SetInputConnection(reslice->GetOutputPort());
|
||
|
||
vtkNew<vtkImageActor> imgActor;
|
||
imgActor->GetMapper()->SetInputConnection(colorize->GetOutputPort());
|
||
ren->AddViewProp(imgActor);
|
||
ren->ResetCamera();
|
||
|
||
double bounds[6];
|
||
full->GetBounds(bounds);
|
||
const double zMin = bounds[4], zMax = bounds[5];
|
||
const double ox = 0.5 * (bounds[0] + bounds[1]);
|
||
const double oy = 0.5 * (bounds[2] + bounds[3]);
|
||
|
||
rw->Render(); // 预热
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
const double t = static_cast<double>(f) / std::max(1, frames - 1);
|
||
const double z = zMin + (zMax - zMin) * t;
|
||
reslice->SetResliceAxesOrigin(ox, oy, z);
|
||
reslice->Modified();
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
|
||
}
|
||
|
||
// 由 ColorScale 物理区间建 256 级 VTK LUT(切片纹理着色用,与体绘制色阶同源)。
|
||
vtkSmartPointer<vtkLookupTable> makeLut(const geopro::core::ColorScale& cs,
|
||
double vmin, double vmax) {
|
||
auto lut = vtkSmartPointer<vtkLookupTable>::New();
|
||
const int n = 256;
|
||
lut->SetNumberOfTableValues(n);
|
||
lut->SetRange(vmin, vmax);
|
||
for (int i = 0; i < n; ++i) {
|
||
const double v = vmin + (vmax - vmin) * i / (n - 1);
|
||
const auto c = cs.colorAt(v);
|
||
lut->SetTableValue(i, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0);
|
||
}
|
||
lut->Build();
|
||
return lut;
|
||
}
|
||
|
||
// 简单蓝-白-红色阶(与 test_color_scale 同款最简构造)。
|
||
geopro::core::ColorScale makeColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double mid = 0.5 * (vmin + vmax);
|
||
cs.addStop(vmin, geopro::core::Rgba{0, 0, 255, 255});
|
||
cs.addStop(mid, geopro::core::Rgba{255, 255, 255, 255});
|
||
cs.addStop(vmax, geopro::core::Rgba{255, 0, 0, 255});
|
||
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;
|
||
}
|
||
|
||
// 地震高对比色阶(seismic 红-白-蓝):两端饱和亮色(强正=亮红、强负=亮蓝),
|
||
// 零附近白。比 structural 更亮、对比更狠,正负反射一眼分开。
|
||
geopro::core::ColorScale makeSeismicColorScale(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{30, 60, 255, 255}); // 亮蓝(强负)
|
||
cs.addStop(at(0.30), geopro::core::Rgba{120, 180, 255, 255}); // 浅蓝
|
||
cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 白(零)
|
||
cs.addStop(at(0.70), geopro::core::Rgba{255, 170, 120, 255}); // 浅橙
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 40, 30, 255}); // 亮红(强正)
|
||
return cs;
|
||
}
|
||
|
||
// jet 类高饱和色阶(蓝-青-绿-黄-红):全程高亮高饱和,最大化色彩动态范围,
|
||
// 弱信号也能映到鲜明色相,适合「一眼铺满层次」的取向。
|
||
geopro::core::ColorScale makeJetColorScale(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, 200, 255}); // 蓝
|
||
cs.addStop(at(0.25), geopro::core::Rgba{0, 200, 255, 255}); // 青
|
||
cs.addStop(at(0.50), geopro::core::Rgba{0, 230, 60, 255}); // 绿
|
||
cs.addStop(at(0.75), geopro::core::Rgba{255, 230, 0, 255}); // 黄
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 30, 0, 255}); // 红
|
||
return cs;
|
||
}
|
||
|
||
// 「实体感」不透明度包络(Task 12d gallery):与 structural 双端斜坡不同,这里让
|
||
// 中高值段普遍可见——背景(近零)仍压低但不归零,中高段从 floorOpacity 平滑升到
|
||
// maxOpacity,使体读起来像半透明实心块、内部层次(而非只剩两端薄壳)可见。
|
||
// floorOpacity:近零背景的最低不透明度(0.05~0.12,压住但不消失)
|
||
// maxOpacity :强反射端的不透明度峰值(0.85 时近实心)
|
||
// midOpacity :中值段(半幅处)的不透明度(0.3~0.5,决定「半透明实心」观感)
|
||
vtkSmartPointer<vtkVolumeProperty> makeSolidVolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys, double floorOpacity, double midOpacity,
|
||
double maxOpacity) {
|
||
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);
|
||
}
|
||
|
||
// 不透明度:V 形(中段=零附近背景=floor,正负两端=max),但全程 ≥floor 且中值
|
||
// 段≈mid → 整体半透明实心、内部层次可见,而非两端薄壳。
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
const double qmid = 0.5 * (qminD + qmaxD);
|
||
const double half = 0.5 * (qmaxD - qminD);
|
||
opacity->AddPoint(qminD, maxOpacity); // 强负反射:近实心
|
||
opacity->AddPoint(qmid - 0.55 * half, midOpacity); // 中负段:半透明实心
|
||
opacity->AddPoint(qmid, floorOpacity); // 近零背景:压低但可见
|
||
opacity->AddPoint(qmid + 0.55 * half, midOpacity); // 中正段:半透明实心
|
||
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:近实心
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 参数化量化域传函:与 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) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderB <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderB] storeDir=" << dir << " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:renderB 前先确认离屏可用(避免在不可渲染机上跑出假数据)。
|
||
std::cout << "[renderB] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderB] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 加载整卷(VTK_SHORT)。
|
||
Stopwatch swLoad;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
std::cout << "[renderB] 整卷 " << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 体素=" << voxels << " 字节=" << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
|
||
<< "ms\n";
|
||
|
||
auto images = src.currentImages();
|
||
if (images.empty() || !images.front()) {
|
||
std::cerr << "[renderB] 错误: currentImages 为空\n";
|
||
return 1;
|
||
}
|
||
vtkImageData* shortImg = images.front().Get();
|
||
|
||
// 色阶用 meta 的物理区间。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
|
||
// 2) 体绘制(离屏)。
|
||
auto rw = makeOffscreenWindow(1024, 768);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkSmartPointer<vtkVolume> volume =
|
||
geopro::render::buildVoxelI16FromImage(shortImg, m.quant, cs, vmin, vmax);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装上捕获式 OutputWindow:拦截体绘制时的 3D 纹理维度错误。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
std::cout << "[renderB] 体绘制基准(" << frames << " 帧旋转相机)...\n";
|
||
const double volFpsRaw = benchVolumeFps(rw, ren, frames);
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr); // 还原默认输出窗口
|
||
|
||
// 进显存判据:SmartVolumeMapper 实际用的渲染模式(2=GPURenderMode)。
|
||
int renderMode = -1;
|
||
bool lowResResample = false;
|
||
if (auto* svm =
|
||
vtkSmartVolumeMapper::SafeDownCast(volume->GetMapper())) {
|
||
renderMode = svm->GetLastUsedRenderMode();
|
||
// 大体可能触发降质重采样(GPU 显存不足时 SmartVolumeMapper 走低分辨率)。
|
||
lowResResample = (svm->GetInteractiveAdjustSampleDistances() == 0 &&
|
||
renderMode != vtkSmartVolumeMapper::GPURenderMode);
|
||
}
|
||
const bool onGpu = (renderMode == vtkSmartVolumeMapper::GPURenderMode);
|
||
|
||
// 任一维度超过 GL_MAX_3D_TEXTURE_SIZE(本机实测 16384)→ 整卷无法成单张 3D 纹理。
|
||
constexpr int kMax3DTexObserved = 16384;
|
||
const bool dimOversize =
|
||
(m.nx > kMax3DTexObserved || m.ny > kMax3DTexObserved ||
|
||
m.nz > kMax3DTexObserved);
|
||
// 体绘制 fps 是否可信:上传成功(无纹理错误且未超限)才算真实整卷体绘制帧率。
|
||
const bool volFpsValid = !textureErr && !dimOversize;
|
||
const double volFps = volFpsValid ? volFpsRaw : -1.0;
|
||
|
||
std::cout << "[renderB] 体绘制 raw_fps=" << volFpsRaw
|
||
<< " 渲染模式=" << renderMode << (onGpu ? "(GPU)" : "(非GPU)")
|
||
<< " 纹理维度错误=" << (textureErr ? "是" : "否")
|
||
<< " 超 16384=" << (dimOversize ? "是" : "否") << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << "[renderB] 警告: 整卷未能成单张 3D 纹理(X=" << m.nx
|
||
<< " > " << kMax3DTexObserved
|
||
<< "),体绘制 fps 无意义 → 标 INVALID。\n";
|
||
}
|
||
|
||
// 3) 切片扫描(离屏,沿 Z 扫整卷)。
|
||
vtkNew<vtkRenderer> ren2;
|
||
ren2->SetBackground(0.0, 0.0, 0.0);
|
||
auto rw2 = makeOffscreenWindow(1024, 768);
|
||
rw2->AddRenderer(ren2);
|
||
vtkSmartPointer<vtkLookupTable> lut = makeLut(cs, vmin, vmax);
|
||
|
||
std::cout << "[renderB] 切片扫描基准(" << frames << " 帧沿 Z 推进)...\n";
|
||
const double sliceFps =
|
||
benchSliceFps(rw2, ren2, src.sliceSource(), lut, frames);
|
||
std::cout << "[renderB] 切片 fps=" << sliceFps << "\n";
|
||
|
||
const double peak = Probe::peakMemMB();
|
||
const std::string vram = "N/A"; // VTK 安装未带 GLEW 头,无法直查 NVX 显存
|
||
|
||
// 4) 汇总打印。
|
||
const std::string volFpsStr =
|
||
volFpsValid ? std::to_string(volFps) : "INVALID(整卷超 3D 纹理上限)";
|
||
|
||
std::cout << "\n=== renderB GPU 指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "体绘制 fps : " << volFpsStr << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << " (raw_fps=" << volFpsRaw
|
||
<< " 为空纹理渲染,X=" << m.nx << " > 16384,不可信)\n";
|
||
}
|
||
std::cout << "切片扫描 fps : " << sliceFps << " (2D 纹理,无 3D 上限约束)\n";
|
||
std::cout << "渲染模式 : " << renderMode
|
||
<< (onGpu ? " (GPU 路径)" : " (非 GPU)") << "\n";
|
||
std::cout << "整卷进显存 : "
|
||
<< (volFpsValid && onGpu ? "是(单张 3D 纹理)"
|
||
: "否(超 GL_MAX_3D_TEXTURE_SIZE 16384)")
|
||
<< "\n";
|
||
std::cout << "降质重采样 : " << (lowResResample ? "是" : "否") << "\n";
|
||
std::cout << "GPU 显存 : " << vram << "\n";
|
||
std::cout << "进程峰值内存(MB): " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderB,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",volFps=" + volFpsStr +
|
||
",volFpsRaw=" + std::to_string(volFpsRaw) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",sliceFps=" + std::to_string(sliceFps) +
|
||
",renderMode=" + std::to_string(renderMode) +
|
||
",onGpu=" + std::to_string(onGpu ? 1 : 0) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 核外分块体绘制基准(POC-C,命门探针)
|
||
// ============================================================================
|
||
|
||
// 量化域传函(与 VoxelActor::buildVoxelI16FromImage 同逻辑):颜色对每量化级 qv 用
|
||
// q.toPhys(qv) 反查 ColorScale;不透明度 kBlank→0、[qmin,qmax] 线性到 kMaxOpacity。
|
||
// MultiBlock 全块共用同一 vtkVolumeProperty(挂在单个 vtkVolume 上)。
|
||
vtkSmartPointer<vtkVolumeProperty> makeI16VolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys) {
|
||
constexpr int kTransferSamples = 64;
|
||
constexpr double kMaxOpacity = 0.15;
|
||
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);
|
||
}
|
||
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
opacity->AddPoint(qminD, 0.0);
|
||
opacity->AddPoint(qmaxD, kMaxOpacity);
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 由当前工作集图像组装 vtkMultiBlockDataSet(每块一个 vtkImageData)。
|
||
vtkSmartPointer<vtkMultiBlockDataSet> makeMultiBlock(
|
||
const std::vector<vtkSmartPointer<vtkImageData>>& imgs) {
|
||
auto mb = vtkSmartPointer<vtkMultiBlockDataSet>::New();
|
||
mb->SetNumberOfBlocks(static_cast<unsigned int>(imgs.size()));
|
||
for (unsigned int i = 0; i < imgs.size(); ++i) {
|
||
mb->SetBlock(i, imgs[i].Get());
|
||
}
|
||
return mb;
|
||
}
|
||
|
||
int cmdRenderC(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const std::size_t budget =
|
||
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderC] storeDir=" << dir << " budget=" << budget
|
||
<< " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderC] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderC] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 核外源(读 meta + 建 pager,不载整卷)。
|
||
Stopwatch swLoad;
|
||
geopro::render::OutOfCoreSource src(dir, budget);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
|
||
const int winW = 1024, winH = 768;
|
||
src.setAspect(static_cast<double>(winW) / winH);
|
||
|
||
std::cout << "[renderC] 体 " << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 体素=" << voxels << " (整卷 X=" << m.nx
|
||
<< " > 16384 → renderB INVALID),源构造 " << loadMs << "ms\n";
|
||
|
||
// 色阶用 meta 物理区间。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
|
||
|
||
// 2) 离屏 + MultiBlock 体绘制。
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkNew<vtkMultiBlockVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装捕获式 OutputWindow:拦截每块上传时的 3D 纹理维度错误(应无,因块 ≤64³)。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 相机:先以全体定向(看整卷),首帧 update 选出工作集后再 ResetCamera 到
|
||
// 实际驻留块的 mapper 包围盒(budget<视野总块时工作集只覆盖体的一部分,框住它
|
||
// 才能确证核外体绘制真渲出;这是 budget 受限下的诚实测法,报告说明)。
|
||
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
|
||
m.origin[1], m.origin[1] + m.ny * m.spacing[1],
|
||
m.origin[2], m.origin[2] + m.nz * m.spacing[2]);
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
|
||
auto refreshBlocks = [&]() {
|
||
src.update(cam);
|
||
auto imgs = src.currentImages();
|
||
auto mb = makeMultiBlock(imgs);
|
||
mapper->SetInputDataObject(mb);
|
||
mapper->Update();
|
||
return imgs.size();
|
||
};
|
||
|
||
const std::size_t warmBlocks = refreshBlocks();
|
||
// 用工作集(mapper)实际包围盒重置相机,框住驻留块。
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
if (b[0] <= b[1]) {
|
||
ren->ResetCamera(b);
|
||
} else {
|
||
ren->ResetCamera();
|
||
}
|
||
}
|
||
rw->Render(); // 预热(上传显存 + 编译 shader,不计时)
|
||
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
std::cout << "[renderC] 工作集包围盒 x[" << b[0] << "," << b[1] << "] y["
|
||
<< b[2] << "," << b[3] << "] z[" << b[4] << "," << b[5] << "]\n";
|
||
}
|
||
|
||
std::cout << "[renderC] 预热:level=" << src.lastLevel()
|
||
<< " 视野块=" << src.lastVisibleCount() << "/"
|
||
<< src.lastLevelBrickTotal()
|
||
<< " 驻留=" << src.residentCount() << " 渲染块=" << warmBlocks
|
||
<< "\n";
|
||
|
||
std::size_t maxResident = src.residentCount();
|
||
std::size_t sumBlocks = 0;
|
||
|
||
// 3a) 静态工作集体绘制 fps:工作集固定(不每帧换块),只旋相机 + Render。
|
||
// 隔离"纯 GPU MultiBlock 体绘制"成本(剔除分块换页/解压/重建 mapper 开销),
|
||
// 直接对照 renderB 整卷 fps,回答未知 #6(真实体绘制 fps)。
|
||
std::cout << "[renderC] 静态工作集体绘制基准(" << frames << " 帧旋相机)...\n";
|
||
Stopwatch swStatic;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render(); // 工作集不变,仅旋转
|
||
}
|
||
const double staticMs = swStatic.elapsedMs();
|
||
const double staticFps = staticMs > 0 ? frames * 1000.0 / staticMs : 0.0;
|
||
std::cout << "[renderC] 静态工作集 fps=" << staticFps << "\n";
|
||
|
||
// 3b) 动态换页体绘制 fps:每帧 update(cam)(重选 LOD/视野块,含 qUncompress 解压
|
||
// 换入的块 + 重建 MultiBlock)+ Render。回答未知 #4(热路径解压是否拖垮 fps)
|
||
// 与 #5(内存恒定)。同时累计 update 耗时占比。
|
||
std::cout << "[renderC] 动态换页体绘制基准(" << frames << " 帧旋相机)...\n";
|
||
double updateMsTotal = 0.0;
|
||
Stopwatch swDyn;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
Stopwatch swU;
|
||
const std::size_t blocks = refreshBlocks(); // update + 重建 MultiBlock
|
||
updateMsTotal += swU.elapsedMs();
|
||
sumBlocks += blocks;
|
||
maxResident = std::max(maxResident, src.residentCount());
|
||
rw->Render();
|
||
}
|
||
const double dynMs = swDyn.elapsedMs();
|
||
const double dynFps = dynMs > 0 ? frames * 1000.0 / dynMs : 0.0;
|
||
const double rawFps = dynFps; // 主报告口径:含换页的真实交互 fps
|
||
std::cout << "[renderC] 动态换页 fps=" << dynFps
|
||
<< " (其中 update/换页/重建 平均 " << (updateMsTotal / frames)
|
||
<< " ms/帧)\n";
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 4) 正确性判据:渲出非空像素(非全背景)。
|
||
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, pixels);
|
||
vtkIdType nonBlack = 0;
|
||
const vtkIdType npx = pixels->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < npx; ++i) {
|
||
if (pixels->GetComponent(i, 0) > 10 || pixels->GetComponent(i, 1) > 10 ||
|
||
pixels->GetComponent(i, 2) > 10) {
|
||
++nonBlack;
|
||
}
|
||
}
|
||
const bool renderedNonEmpty = (nonBlack > 0);
|
||
|
||
// 渲染模式(MultiBlock 内部每块一个 SmartVolumeMapper;此处取一块代表性查询)。
|
||
// MultiBlock 不直接暴露 LastUsedRenderMode,故以纹理无错 + 非空像素为体绘制真出证据。
|
||
const bool volFpsValid = !textureErr && renderedNonEmpty;
|
||
|
||
const double peak = Probe::peakMemMB();
|
||
const double avgBlocks =
|
||
frames > 0 ? static_cast<double>(sumBlocks) / frames : 0.0;
|
||
|
||
std::cout << "\n=== renderC 核外体绘制指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< " (整卷 X 超 16384,renderB=INVALID)\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "budget(块) : " << budget << "\n";
|
||
std::cout << "峰值驻留(块) : " << maxResident
|
||
<< (maxResident <= budget ? " (≤budget,内存恒定 OK)"
|
||
: " (!! 超 budget)")
|
||
<< "\n";
|
||
std::cout << "末帧 level : " << src.lastLevel() << "\n";
|
||
std::cout << "末帧视野块/总块 : " << src.lastVisibleCount() << " / "
|
||
<< src.lastLevelBrickTotal() << "\n";
|
||
std::cout << "平均渲染块/帧 : " << avgBlocks << "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (非背景像素=" << nonBlack << ")\n";
|
||
std::cout << "静态工作集 fps : "
|
||
<< (volFpsValid ? std::to_string(staticFps)
|
||
: std::string("INVALID(纹理错或空渲染)"))
|
||
<< " (纯 GPU MultiBlock 体绘制)\n";
|
||
std::cout << "动态换页 fps : "
|
||
<< (volFpsValid ? std::to_string(dynFps)
|
||
: std::string("INVALID(纹理错或空渲染)"))
|
||
<< " (含每帧 update/解压/重建 mapper)\n";
|
||
std::cout << " 换页均耗时/帧 : " << (updateMsTotal / frames) << " ms\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "对照 renderB : 整卷 INVALID(超 3D 纹理上限);renderC "
|
||
<< (volFpsValid ? "真渲出 ✔" : "未渲出 ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderC,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",budget=" + std::to_string(budget) +
|
||
",maxResident=" + std::to_string(maxResident) +
|
||
",lastLevel=" + std::to_string(src.lastLevel()) +
|
||
",lastVisible=" + std::to_string(src.lastVisibleCount()) +
|
||
",lastLevelTotal=" + std::to_string(src.lastLevelBrickTotal()) +
|
||
",avgBlocks=" + std::to_string(avgBlocks) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",nonBlack=" + std::to_string(nonBlack) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",staticFps=" + (volFpsValid ? std::to_string(staticFps) : "INVALID") +
|
||
",dynFps=" + (volFpsValid ? std::to_string(dynFps) : "INVALID") +
|
||
",updateMsPerFrame=" + std::to_string(updateMsTotal / frames) +
|
||
",rawFps=" + std::to_string(rawFps) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return volFpsValid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 单 mapper SetPartitions 整卷体绘制基准(POC-C-partitioned,去风险探针)
|
||
// ============================================================================
|
||
//
|
||
// 验"对的架构":整卷喂【单个】vtkGPUVolumeRayCastMapper(其 OpenGL 实现 =
|
||
// vtkOpenGLGPUVolumeRayCastMapper),用 SetPartitions(ceil(nx/16384),...) 让同一
|
||
// mapper 内部把体沿轴分区上传(每区 ≤16384 绕过 GL_MAX_3D_TEXTURE_SIZE),一次
|
||
// ray cast。对照 9c 整卷单 SmartVolumeMapper(INVALID,纹理墙) 与 12 MultiBlock
|
||
// (每块一 mapper,9.5 静态/1.45 换页)。
|
||
//
|
||
// 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
|
||
int cmdRenderCPartitioned(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr
|
||
<< "用法: gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderC-partitioned] storeDir=" << dir << " frames=" << frames
|
||
<< "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderC-partitioned] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderC-partitioned] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) WholeVolumeSource 重组整卷 VTK_SHORT image(常驻内存,约 400MB)。
|
||
Stopwatch swLoad;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
std::cout << "[renderC-partitioned] 整卷 " << m.nx << "x" << m.ny << "x"
|
||
<< m.nz << " 体素=" << voxels << " 字节=" << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
|
||
<< "ms\n";
|
||
|
||
auto images = src.currentImages();
|
||
if (images.empty() || !images.front()) {
|
||
std::cerr << "[renderC-partitioned] 错误: currentImages 为空\n";
|
||
return 1;
|
||
}
|
||
vtkImageData* shortImg = images.front().Get();
|
||
|
||
// 2) 分区数:任一轴 > 16384 → ceil(dim/16384) 个分区,其余轴 1。
|
||
constexpr int kMax3DTex = 16384;
|
||
auto partCount = [](int dim) {
|
||
return static_cast<unsigned short>((dim + kMax3DTex - 1) / kMax3DTex);
|
||
};
|
||
const unsigned short px = partCount(m.nx);
|
||
const unsigned short py = partCount(m.ny);
|
||
const unsigned short pz = partCount(m.nz);
|
||
std::cout << "[renderC-partitioned] SetPartitions(" << px << "," << py << ","
|
||
<< pz << ") 每区上限 ≤" << kMax3DTex << " (沿线 " << m.nx << "/"
|
||
<< px << "=" << (m.nx + px - 1) / px << ")\n";
|
||
|
||
// 3) 量化域传函(复用现有 makeI16VolumeProperty:qmin/qmax + kBlank 透明)。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
|
||
|
||
// 4) 离屏 + 单个 GPU ray cast mapper + SetPartitions。
|
||
const int winW = 1024, winH = 768;
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
// vtkGPUVolumeRayCastMapper 抽象基类无 SetPartitions(在 OpenGL 实现上);
|
||
// 直接建 OpenGL 具体类(工厂默认产物同此),喂【整卷单 image】不预切块。
|
||
vtkNew<vtkOpenGLGPUVolumeRayCastMapper> mapper;
|
||
mapper->SetInputData(shortImg);
|
||
mapper->SetPartitions(px, py, pz);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装捕获式 OutputWindow:拦截分区上传时的 3D 纹理维度错误。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 相机:用 mapper 实际包围盒定向(整卷,非工作集);体极扁长(44476:29:162),
|
||
// ResetCamera 全体后再倾斜抬高视角,让薄维度可见(否则边缘视角近乎不可见)。
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
if (b[0] <= b[1]) {
|
||
ren->ResetCamera(b);
|
||
} else {
|
||
ren->ResetCamera();
|
||
}
|
||
}
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(30.0); // 抬高,避免纯边缘视角看不到薄板
|
||
cam->Azimuth(30.0);
|
||
ren->ResetCameraClippingRange();
|
||
|
||
// 每帧旋相机 + Render 测 fps;同时多帧采样非背景像素取最大值
|
||
// (区分"真渲不出"与"末帧恰好边缘视角空"——后者只是采样时机)。
|
||
auto countNonBlack = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 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;
|
||
};
|
||
|
||
std::cout << "[renderC-partitioned] 单 mapper 整卷体绘制基准(" << frames
|
||
<< " 帧旋相机)...\n";
|
||
rw->Render(); // 预热(分区上传 + 编译 shader,不计时)
|
||
vtkIdType maxNonBlack = countNonBlack();
|
||
const int sampleEvery = std::max(1, frames / 8);
|
||
Stopwatch swBench;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
if (f % sampleEvery == 0) {
|
||
maxNonBlack = std::max(maxNonBlack, countNonBlack());
|
||
}
|
||
}
|
||
const double benchMs = swBench.elapsedMs();
|
||
const double volFpsRaw =
|
||
benchMs > 0.0 ? frames * 1000.0 / benchMs : 0.0;
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 5) 正确性判据:整个旋转扫描中的最大非背景像素(非空才算真渲出)。
|
||
const vtkIdType nonBlack = maxNonBlack;
|
||
const bool renderedNonEmpty = (nonBlack > 0);
|
||
|
||
// 双闸:无纹理错 + 非空像素 → fps 可信。
|
||
const bool volFpsValid = !textureErr && renderedNonEmpty;
|
||
const double volFps = volFpsValid ? volFpsRaw : -1.0;
|
||
const double peak = Probe::peakMemMB();
|
||
const bool interactive = volFpsValid && volFps >= 15.0;
|
||
|
||
const std::string volFpsStr =
|
||
volFpsValid ? std::to_string(volFps)
|
||
: std::string("INVALID(纹理错或空渲染)");
|
||
|
||
std::cout << "\n=== renderC-partitioned 单 mapper SetPartitions 指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "分区数(px,py,pz) : " << px << "," << py << "," << pz << "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (非背景像素=" << nonBlack << ")\n";
|
||
std::cout << "体绘制 fps : " << volFpsStr << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << " (raw_fps=" << volFpsRaw << " 不可信)\n";
|
||
}
|
||
std::cout << "达交互级(≥15fps) : "
|
||
<< (interactive ? "是 ✔" : "否 ✘") << "\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "对照 renderB : 整卷单 SmartVolumeMapper=INVALID(纹理墙);"
|
||
"renderC MultiBlock=9.5 静态/1.45 换页;本探针="
|
||
<< (volFpsValid ? volFpsStr + "fps" : "INVALID") << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderC-partitioned,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",px=" + std::to_string(px) + ",py=" + std::to_string(py) +
|
||
",pz=" + std::to_string(pz) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",nonBlack=" + std::to_string(nonBlack) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",volFps=" + volFpsStr + ",volFpsRaw=" + std::to_string(volFpsRaw) +
|
||
",interactive=" + std::to_string(interactive ? 1 : 0) +
|
||
",loadMs=" + std::to_string(loadMs) + ",peakMB=" + std::to_string(peak));
|
||
|
||
// 写报告文件(覆盖式,含对照表)。
|
||
{
|
||
const fs::path repo =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||
fs::create_directories(repo.parent_path());
|
||
std::ofstream rf(repo.string());
|
||
if (rf) {
|
||
rf << "# POC-C 单 mapper SetPartitions 整卷体绘制探针结果\n\n";
|
||
rf << "## 体\n";
|
||
rf << "- 维度: " << m.nx << " x " << m.ny << " x " << m.nz << " (体素 "
|
||
<< voxels << ")\n";
|
||
rf << "- 整卷字节: " << wholeBytes << " B ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB, VTK_SHORT)\n";
|
||
rf << "- store: " << dir << "\n\n";
|
||
rf << "## 单 mapper SetPartitions\n";
|
||
rf << "- mapper: vtkOpenGLGPUVolumeRayCastMapper (整卷单 image,不预切块)\n";
|
||
rf << "- 分区数: SetPartitions(" << px << ", " << py << ", " << pz
|
||
<< ") 每区上限 ≤" << kMax3DTex << "\n";
|
||
rf << "- 纹理维度错误: " << (textureErr ? "是" : "否") << "\n";
|
||
rf << "- 渲出非空像素: " << (renderedNonEmpty ? "是" : "否") << " (非背景像素 "
|
||
<< nonBlack << ")\n";
|
||
rf << "- 体绘制 fps: " << volFpsStr << "\n";
|
||
rf << "- 达交互级(≥15fps): " << (interactive ? "是" : "否") << "\n";
|
||
rf << "- 进程峰值内存: " << peak << " MB\n";
|
||
rf << "- 源构造耗时: " << loadMs << " ms\n\n";
|
||
rf << "## 对照表\n\n";
|
||
rf << "| 路径 | 是否渲出 | fps |\n";
|
||
rf << "|---|---|---|\n";
|
||
rf << "| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙) | — |\n";
|
||
rf << "| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态/1.45 换页 |\n";
|
||
rf << "| renderC-partitioned 单 mapper SetPartitions | "
|
||
<< (volFpsValid ? "渲出" : "未渲出") << " | "
|
||
<< (volFpsValid ? volFpsStr : std::string("INVALID")) << " |\n\n";
|
||
rf << "## 判据结论\n";
|
||
if (volFpsValid && interactive) {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【真渲出且达交互级】(" << volFps
|
||
<< " fps ≥15)。C production 路线钉死可行。\n";
|
||
} else if (volFpsValid) {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(" << volFps
|
||
<< " fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。\n";
|
||
} else {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【未真渲出】(纹理错="
|
||
<< (textureErr ? "是" : "否") << ",非空像素="
|
||
<< (renderedNonEmpty ? "是" : "否")
|
||
<< ")。SetPartitions 未能绕过纹理墙,如实记录。\n";
|
||
}
|
||
std::cout << "[renderC-partitioned] 报告写入 " << repo.string() << "\n";
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
// 画面平均亮度(0~255):取前缓冲 RGB 求 luma 均值。Task 12d gallery 报告用,
|
||
// 量化「整体偏暗 vs 变亮」——背景占多数,故这是含背景的全屏均亮(横向对比有效)。
|
||
double meanBrightness(vtkRenderWindow* rw, int w, int h) {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, px);
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
if (np == 0) return 0.0;
|
||
double sum = 0.0;
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
const double r = px->GetComponent(i, 0);
|
||
const double g = px->GetComponent(i, 1);
|
||
const double b = px->GetComponent(i, 2);
|
||
sum += 0.299 * r + 0.587 * g + 0.114 * b;
|
||
}
|
||
return sum / static_cast<double>(np);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ① 视觉调优:出一帧能看结构的图 + 调优前后 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;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 视觉调参画廊(Task 12d gallery):view --preview --variant N
|
||
// ============================================================================
|
||
//
|
||
// 同一局部段(沿线中段 kViewDefaultLocalBricks 列全分辨率) + 同一相机框法
|
||
// (ResetCamera→Elevation/Azimuth→Zoom),只换「不透明度包络 / 配色 / 取景角度 /
|
||
// 背景」四组视觉参数,各存一张 PNG 供控制方挑选。fps 对视觉调参近乎中性,每组实测验证。
|
||
//
|
||
// 注:交互窗口(无 flag 的 view)默认即采用 var4(kViewDefaultVariant)——配色/不透明度
|
||
// 包络/取景/exagg/背景全部走同一份 var4 参数,故「交互默认画面 == view-var4」。
|
||
enum class OpacityProfile {
|
||
kSolid, // V 形实体感:中高值段普遍可见,半透明实心块
|
||
kStructural, // 现有双端斜坡:仅正负两端不透明(对照基线)
|
||
};
|
||
enum class ColorChoice { kStructural, kSeismic, kJet };
|
||
|
||
struct GalleryVariant {
|
||
const char* name; // 文件名后缀:view-<name>.png
|
||
OpacityProfile profile;
|
||
ColorChoice color;
|
||
double floorOpacity; // 近零背景不透明度(kSolid 用)
|
||
double midOpacity; // 中值段不透明度(kSolid 用)
|
||
double maxOpacity; // 两端峰值不透明度
|
||
double exagg; // 垂向夸张
|
||
double elevation; // ResetCamera 后 Elevation
|
||
double azimuth; // 再 Azimuth
|
||
double zoom; // 再 Zoom 填满画面
|
||
double bg[3]; // 背景 RGB
|
||
const char* desc; // 报告用中文说明
|
||
};
|
||
|
||
// 4 组视觉参数。值经离屏实跑挑出(详见报告)。
|
||
const GalleryVariant kGalleryVariants[] = {
|
||
// var1:高不透明度实体感——seismic 亮配色 + V 形包络(中段 0.40/两端 0.85),
|
||
// floor 压到 0.04:近零层间隙近透明,亮层面浮出 → 内部层状结构可读。
|
||
{"var1", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.04, 0.40, 0.85, 8.0, 22.0, 28.0, 1.9, {0.05, 0.05, 0.09},
|
||
"高不透明度实体感:V形包络(floor0.04/mid0.40/max0.85)+seismic 亮配色,"
|
||
"半透明实心、内部层次可见"},
|
||
// var2:高对比配色——jet 全程高饱和 + 中等不透明度 V 形包络。
|
||
{"var2", OpacityProfile::kSolid, ColorChoice::kJet,
|
||
0.04, 0.32, 0.70, 8.0, 22.0, 28.0, 1.9, {0.06, 0.06, 0.10},
|
||
"高对比配色:jet 蓝青绿黄红全程高饱和 + 中等 V 形包络(mid0.32/max0.70)"},
|
||
// var3:居中正对纵截面——低 Elevation/Azimuth 摆平、正对 X-Z 长侧面(层状反射沿
|
||
// X 延展最清晰)、Zoom2.0 填满 ~70%;floor 压更低让层间隙透明、层面立体。
|
||
{"var3", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.03, 0.38, 0.82, 9.0, 10.0, 12.0, 2.0, {0.05, 0.05, 0.09},
|
||
"居中正对纵截面:低 El10/Az12 摆平正对 X-Z 长侧面、Zoom2.0 填满视野,"
|
||
"floor0.03 凸显层面,exagg9 放大薄轴"},
|
||
// var4:最像真实 GPR 三维图——seismic + 略提背景亮 + 微立体角 + 实体包络。
|
||
// 综合最佳,选作交互窗口默认(kViewDefaultVariant)。
|
||
{"var4", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.035, 0.38, 0.84, 8.0, 18.0, 22.0, 2.0, {0.07, 0.08, 0.11},
|
||
"综合最佳:seismic + 实体包络(floor0.035/mid0.38/max0.84) + 微立体取景"
|
||
"(El18/Az22/Zoom2.0) + 略亮冷灰背景"},
|
||
};
|
||
|
||
// 交互窗口(无 flag 的 view)的默认视觉变体 = var4(kGalleryVariants 末项)。
|
||
// 交互默认与 view-var4 走同一份参数 → 二者画面一致(DRY,不复制粘贴漂移)。
|
||
const GalleryVariant& kViewDefaultVariant =
|
||
kGalleryVariants[sizeof(kGalleryVariants) / sizeof(kGalleryVariants[0]) - 1];
|
||
|
||
geopro::core::ColorScale pickColor(ColorChoice c, double vmin, double vmax) {
|
||
switch (c) {
|
||
case ColorChoice::kSeismic: return makeSeismicColorScale(vmin, vmax);
|
||
case ColorChoice::kJet: return makeJetColorScale(vmin, vmax);
|
||
case ColorChoice::kStructural:
|
||
default: return makeStructuralColorScale(vmin, vmax);
|
||
}
|
||
}
|
||
|
||
// 按变体的不透明度包络建体属性(gallery / 交互默认共用,DRY)。kSolid 走 V 形实体
|
||
// 包络(floor/mid/max),kStructural 走双端斜坡(仅 maxOpacity)。
|
||
vtkSmartPointer<vtkVolumeProperty> makeVariantProperty(
|
||
const GalleryVariant& v, const geopro::core::Quant& q,
|
||
const geopro::core::ColorScale& cs, double vmin, double vmax,
|
||
double maxOpacity) {
|
||
if (v.profile == OpacityProfile::kSolid) {
|
||
return makeSolidVolumeProperty(q, cs, vmin, vmax, v.floorOpacity,
|
||
v.midOpacity, maxOpacity);
|
||
}
|
||
return makeTunedVolumeProperty(q, cs, vmin, vmax, maxOpacity);
|
||
}
|
||
|
||
// ============================================================================
|
||
// ③ view:真窗口可交互(给用户肉眼测 + 最低配机跑)(Task 12d)
|
||
// ============================================================================
|
||
//
|
||
// 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂
|
||
// OutOfCoreSource:相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保
|
||
// 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。
|
||
// 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。
|
||
//
|
||
// 离屏 smoke:--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。
|
||
|
||
// 整卷单张 3D 纹理的轴上限(同 renderLOD/renderB 实测 GL_MAX_3D_TEXTURE_SIZE)。
|
||
constexpr int kViewMax3DTex = 16384;
|
||
|
||
// 单纹理统一渲染(Task 12d-singletex):
|
||
//
|
||
// 交互 view 不再用 vtkMultiBlockVolumeMapper + budget 分块(缺块、个位数 fps)。
|
||
// 任何相机位置都只渲【一张 vtkImageData + 单个 vtkSmartVolumeMapper】,与 --preview
|
||
// 走完全同一条产单图 + 同一 mapper 的路径,保证一致、高 fps(与预览 184fps 同档)。
|
||
//
|
||
// LOD 选层规则(拉近变细、拉远变粗):
|
||
// - 远观/中景(相机选中粗层)→ 升到最细的「整卷各轴 ≤16384」层(本数据 L2:11119、
|
||
// L3:5560),整卷重组成一张纹理,任何缩放都显示完整体,绝不缺块。
|
||
// - 拉近(相机选中 level0/1,X 超 16384 无法整卷成单纹理)→ 取当前视野在 level0 的
|
||
// X 子区域(沿线裁一段,使子体各轴 ≤16384)重组一张纹理。
|
||
// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。
|
||
|
||
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。
|
||
struct ViewState {
|
||
geopro::data::ChunkedVolumeStore* store = nullptr;
|
||
vtkSmartVolumeMapper* mapper = nullptr; // 单纹理:单 SmartVolumeMapper
|
||
vtkRenderer* ren = nullptr;
|
||
vtkCamera* cam = nullptr;
|
||
vtkTextActor* fpsText = nullptr;
|
||
vtkRenderWindow* rw = nullptr;
|
||
double exagg = 8.0;
|
||
int lastLevel = -1;
|
||
// 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。
|
||
int cachedWholeLevel = -1;
|
||
vtkSmartPointer<vtkImageData> cachedWholeImg;
|
||
// 局部子区域 image 缓存(按 level0 brick 段缓存,仅在段变化时重组)。
|
||
int cachedLocalBx0 = -1;
|
||
int cachedLocalCount = -1;
|
||
vtkSmartPointer<vtkImageData> cachedLocalImg;
|
||
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
|
||
bool inCb = false;
|
||
};
|
||
|
||
// 某 level 整卷各轴是否都 ≤16384(可成单张 3D 纹理 → 整卷单 mapper 渲染)。
|
||
bool levelFitsSingleTexture(const geopro::data::ChunkedVolumeStore& store,
|
||
int level) {
|
||
int nx = 0, ny = 0, nz = 0;
|
||
store.dims(level, nx, ny, nz);
|
||
return nx <= kViewMax3DTex && ny <= kViewMax3DTex && nz <= kViewMax3DTex;
|
||
}
|
||
|
||
// 给定相机选中的 level,返回真正用于整卷渲染的 level:从 picked 起向粗逐层找,
|
||
// 取第一个整卷各轴 ≤16384 的层(如 level0/1 长线 X 超 16384,则升到 level2)。
|
||
// 找不到(极端情况)返回 -1,调用方退回局部子区域路径。
|
||
int wholeVolumeLevelFor(const geopro::data::ChunkedVolumeStore& store,
|
||
int picked) {
|
||
const int maxLevel = store.levels() - 1;
|
||
for (int lv = std::max(0, picked); lv <= maxLevel; ++lv) {
|
||
if (levelFitsSingleTexture(store, lv)) {
|
||
return lv;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
// 由相机到体中心距离粗分档选 LOD level(移植 OutOfCoreSource::pickLevel,使交互
|
||
// view 不再依赖 OutOfCoreSource)。0=最细。cam==nullptr 或单层 → 0。
|
||
int viewPickLevel(const geopro::data::ChunkedVolumeStore& store, vtkCamera* cam) {
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int maxLevel = store.levels() - 1;
|
||
if (cam == nullptr || maxLevel <= 0) return 0;
|
||
const double dx = m.nx * m.spacing[0];
|
||
const double dy = m.ny * m.spacing[1];
|
||
const double dz = m.nz * m.spacing[2];
|
||
const double diag = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||
if (diag <= 0.0) return 0;
|
||
double pos[3];
|
||
cam->GetPosition(pos);
|
||
const double cx = m.origin[0] + 0.5 * dx;
|
||
const double cy = m.origin[1] + 0.5 * dy;
|
||
const double cz = m.origin[2] + 0.5 * dz;
|
||
const double ddx = pos[0] - cx, ddy = pos[1] - cy, ddz = pos[2] - cz;
|
||
const double dist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
|
||
const double ratio = dist / diag;
|
||
int level = 0;
|
||
if (ratio >= 1.0) level = 1;
|
||
if (ratio >= 2.0) level = 2;
|
||
if (ratio >= 4.0) level = 3;
|
||
return std::min(level, maxLevel);
|
||
}
|
||
|
||
// 拉近时,level0 整卷 X(44476)>16384 无法成单纹理 → 只取相机视野覆盖的 X 段。
|
||
// 用相机视锥在 X 轴上的世界投影范围交体范围,换算成 level0 的 brick 列区间,并夹到
|
||
// 「段宽 ≤16384 体素」(=256 brick 列,远大于任何视野所需)。返回 [bx0, count)。
|
||
void viewLocalBrickRange(const geopro::data::ChunkedVolumeStore& store,
|
||
vtkCamera* cam, int& bx0, int& count) {
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int brick = m.brick;
|
||
const int totBx = store.bricksX(0);
|
||
const int maxBrickCols = kViewMax3DTex / brick; // 段宽上限(体素 ≤16384)
|
||
|
||
// 默认:以体中段为中心取 kViewDefaultLocalBricks 列(无相机或退化时)。
|
||
int centerBx = totBx / 2;
|
||
int halfCols = std::max(2, maxBrickCols / 4); // 视野估不出时给个稳妥宽度
|
||
|
||
if (cam != nullptr) {
|
||
// 相机焦点 X 投影到 level0 brick 列;视野宽度由相机到焦点距离粗估。
|
||
double fp[3], pos[3];
|
||
cam->GetFocalPoint(fp);
|
||
cam->GetPosition(pos);
|
||
const double x0 = m.origin[0];
|
||
const double xspan = (m.nx > 1) ? (m.nx - 1) * m.spacing[0] : 1.0;
|
||
const double fx = (fp[0] - x0) / (xspan > 0 ? xspan : 1.0); // 0..1
|
||
centerBx = std::clamp(static_cast<int>(fx * totBx), 0, totBx - 1);
|
||
// 视野半宽(世界)≈ 视距 × tan(半视角),再换成 brick 列;夹到合理区间。
|
||
const double ddx = pos[0] - fp[0], ddy = pos[1] - fp[1],
|
||
ddz = pos[2] - fp[2];
|
||
const double viewDist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
|
||
const double halfAngle = 0.5 * cam->GetViewAngle() * 3.14159265 / 180.0;
|
||
const double halfWorld = viewDist * std::tan(halfAngle);
|
||
const double colWorld = brick * m.spacing[0];
|
||
halfCols = std::clamp(static_cast<int>(halfWorld / colWorld) + 1, 2,
|
||
maxBrickCols / 2);
|
||
}
|
||
|
||
bx0 = std::clamp(centerBx - halfCols, 0, std::max(0, totBx - 1));
|
||
count = std::min(2 * halfCols, totBx - bx0);
|
||
count = std::max(1, std::min(count, maxBrickCols));
|
||
}
|
||
|
||
// 单纹理刷新:按相机选 LOD,产【一张 image】喂单 SmartVolumeMapper。返回喂入的块数
|
||
// (恒为 1,单纹理)。同步刷新 st->lastLevel(fps 文本用)。
|
||
std::size_t viewRefreshSingle(ViewState* st) {
|
||
const int picked = viewPickLevel(*st->store, st->cam);
|
||
|
||
// 概览/中远:升到最细的「整卷 ≤16384」层,整卷一张纹理(缓存,仅 level 变才重组)。
|
||
const int wlv = wholeVolumeLevelFor(*st->store, picked);
|
||
if (wlv >= 0 && picked >= 1) {
|
||
if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) {
|
||
st->cachedWholeImg = buildLevelImage(*st->store, wlv, st->store->meta());
|
||
st->cachedWholeLevel = wlv;
|
||
}
|
||
st->mapper->SetInputData(st->cachedWholeImg);
|
||
st->mapper->Update();
|
||
st->lastLevel = wlv;
|
||
return 1;
|
||
}
|
||
|
||
// 拉近(picked==0,要全分辨率):取视野覆盖的 level0 X 子段,一张纹理。
|
||
int bx0 = 0, cnt = 1;
|
||
viewLocalBrickRange(*st->store, st->cam, bx0, cnt);
|
||
if (st->cachedLocalBx0 != bx0 || st->cachedLocalCount != cnt ||
|
||
st->cachedLocalImg == nullptr) {
|
||
st->cachedLocalImg =
|
||
buildLocalLevel0Image(*st->store, st->store->meta(), bx0, cnt);
|
||
st->cachedLocalBx0 = bx0;
|
||
st->cachedLocalCount = cnt;
|
||
}
|
||
st->mapper->SetInputData(st->cachedLocalImg);
|
||
st->mapper->Update();
|
||
st->lastLevel = 0;
|
||
return 1;
|
||
}
|
||
|
||
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
|
||
//
|
||
// fps 修复(Task 12d-fix3):之前用 frameTimer(上次回调到本次的墙钟)算 fps,把
|
||
// 用户思考/不动的空闲时间也算进去,显示的是「空闲间隔」(如 0.2fps),不可信。改为
|
||
// 松手时连渲 kFpsProbeFrames 帧、累计「实际 Render 耗时」取均值,得到真实渲染帧率。
|
||
void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewState*>(clientData);
|
||
// 防重入:本回调内部会 st->rw->Render(),若该 Render 再触发观察者进本回调
|
||
// 将无限递归。已在回调中则直接返回(双保险)。
|
||
if (st->inCb) return;
|
||
st->inCb = true;
|
||
// EndInteraction 时重选 LOD + 重组单图(仅松手触发一次,避免拖动中卡)。
|
||
const std::size_t blocks = viewRefreshSingle(st);
|
||
st->ren->ResetCameraClippingRange();
|
||
const int lvl = st->lastLevel;
|
||
|
||
// 真实渲染帧率:连渲若干帧,只累计 Render() 本身耗时(不含空闲)。首帧含切换后
|
||
// 的纹理上传/shader 编译,故跑 kFpsProbeFrames 帧取均值更可信。
|
||
constexpr int kFpsProbeFrames = 3;
|
||
Stopwatch swR;
|
||
for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render();
|
||
const double renderMs = swR.elapsedMs() / kFpsProbeFrames;
|
||
const double fps = renderMs > 0 ? 1000.0 / renderMs : 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(); // 末帧带上更新后的 fps 文本
|
||
st->inCb = false;
|
||
}
|
||
|
||
// 默认取景宽度:沿测线取约 256 道(=4 brick 列×64)的一段作首帧局部段。整线横截面
|
||
// 相对长度 1:34,框整卷只会看到一条隐形细带;框这个局部段,层状结构才充满视野
|
||
// (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面
|
||
// 越填不满画面;256 道是 ① cmdTune 出 lod-tuned-local.png(有清晰层状结构)的取景,
|
||
// 沿用之以保证首帧同等可读。
|
||
constexpr int kViewDefaultLocalBricks = 4;
|
||
|
||
// 建立 view 的「默认取景」:把 level0 一段局部体(沿线中段)整卷单块喂 mapper,再
|
||
// ResetCamera 到该局部段(actor 已 SetScale(1,exagg,exagg)),置相机为能看出层状
|
||
// 结构的角度。真窗口 / --smoke / --preview 三条路径共用此函数 → 渲的是同一画面。
|
||
// 取景角度(Elevation/Azimuth/Zoom)取自 kViewDefaultVariant(var4),与 view-var4 一致。
|
||
//
|
||
// 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0(默认即全分辨率局部段)。
|
||
std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
|
||
geopro::data::ChunkedVolumeStore& store = *st->store;
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 沿线中段
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
|
||
// 单纹理:一张 image 直接喂单 SmartVolumeMapper(与 --preview 同路径)。
|
||
st->mapper->SetInputData(locImg);
|
||
st->mapper->Update();
|
||
st->cachedLocalImg = locImg; // 持有引用并作缓存键,避免被释放/重组
|
||
st->cachedLocalBx0 = bx0;
|
||
st->cachedLocalCount = localBx;
|
||
st->lastLevel = 0;
|
||
|
||
// 框住局部段:用无参 ResetCamera(按 actor 的【已 SetScale(1,exagg,exagg)】缩放
|
||
// 后包围盒框,把 exagg 后的 Y/Z 一并纳入;mapper->GetBounds() 是未缩放的,不可用),
|
||
// 相机角度沿用能看出结构的 Elevation/Azimuth,再 Zoom 拉近填满画面。
|
||
ren->ResetCamera();
|
||
st->cam = ren->GetActiveCamera();
|
||
st->cam->Elevation(kViewDefaultVariant.elevation); // var4 取景:El18
|
||
st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景:Az22
|
||
st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景:Zoom2.0 填满画面
|
||
ren->ResetCameraClippingRange();
|
||
return 1; // 单纹理:恒一张 image
|
||
}
|
||
|
||
// 渲一组画廊变体并存 PNG,报告 结构像素 / 平均亮度 / fps。返回 0=OK。
|
||
int runGalleryVariant(const std::string& dir, const GalleryVariant& v,
|
||
int frames) {
|
||
const int winW = 1280, winH = 800;
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
|
||
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeVariantProperty(v, m.quant, cs, vmin, vmax, v.maxOpacity);
|
||
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]);
|
||
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, v.exagg, v.exagg);
|
||
ren->AddVolume(volume);
|
||
|
||
// 局部段(沿线中段,同 viewSetupDefaultFrame 的取段法)。
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
std::vector<vtkSmartPointer<vtkImageData>> one{locImg};
|
||
auto mb = makeMultiBlock(one);
|
||
mapper->SetInputDataObject(mb);
|
||
mapper->Update();
|
||
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(v.elevation);
|
||
cam->Azimuth(v.azimuth);
|
||
cam->Zoom(v.zoom);
|
||
ren->ResetCameraClippingRange();
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
rw->Render();
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const std::string pngPath =
|
||
(shotDir / (std::string("view-") + v.name + ".png")).string();
|
||
savePng(rw.Get(), pngPath);
|
||
|
||
// 结构像素:任一通道 >50(排除暗背景),度量「确有体结构」。
|
||
auto countStructPixels = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType structPx = countStructPixels();
|
||
const double bright = meanBrightness(rw.Get(), winW, winH);
|
||
|
||
rw->Render(); // 预热再测 fps
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
|
||
const bool texErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool ok = !texErr && structPx > 0;
|
||
|
||
std::cout << "\n--- gallery " << v.name << " ---\n";
|
||
std::cout << "参数 : " << v.desc << "\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "结构像素(>50) : " << structPx << " / " << (winW * winH)
|
||
<< " (" << (100.0 * structPx / (winW * winH)) << "%)\n";
|
||
std::cout << "平均亮度(0-255) : " << bright << "\n";
|
||
std::cout << "真实 fps : " << (ok ? std::to_string(fps) : "INVALID")
|
||
<< " (" << frames << " 帧旋相机)\n";
|
||
std::cout << "结果 : " << (ok ? "OK" : "FAIL") << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-gallery," + std::string(v.name) + ",dir=" + dir +
|
||
",profile=" + (v.profile == OpacityProfile::kSolid ? "solid" : "struct") +
|
||
",floor=" + std::to_string(v.floorOpacity) +
|
||
",mid=" + std::to_string(v.midOpacity) +
|
||
",max=" + std::to_string(v.maxOpacity) +
|
||
",exagg=" + std::to_string(v.exagg) +
|
||
",el=" + std::to_string(v.elevation) +
|
||
",az=" + std::to_string(v.azimuth) + ",zoom=" + std::to_string(v.zoom) +
|
||
",structPx=" + std::to_string(structPx) +
|
||
",bright=" + std::to_string(bright) +
|
||
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
|
||
",png=" + pngPath);
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
// view --gallery:依次渲全部 4 组变体。
|
||
int cmdViewGallery(const std::string& dir, int frames) {
|
||
std::cout << "[view --gallery] storeDir=" << dir << " frames=" << frames
|
||
<< "\n[view --gallery] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[view --gallery] 闸门失败,中止。\n";
|
||
return 1;
|
||
}
|
||
int rc = 0;
|
||
for (const auto& v : kGalleryVariants) {
|
||
if (runGalleryVariant(dir, v, frames) != 0) rc = 1;
|
||
}
|
||
std::cout << "\n[view --gallery] 完成,4 张图存于 "
|
||
"docs/superpowers/plans/poc-lod-shots/view-var{1..4}.png\n";
|
||
return rc;
|
||
}
|
||
|
||
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] [--preview] [--variant N] "
|
||
"[--gallery] [--frames 90]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
// 交互/preview/smoke 默认视觉参数 = kViewDefaultVariant(var4):配色/不透明度包络/
|
||
// exagg/背景全部走 var4,故默认画面 == view-var4(DRY,与画廊同源)。命令行 --exagg /
|
||
// --opacity 若用户显式传则覆盖 var4 对应值,否则用 var4 的 exagg / maxOpacity。
|
||
const GalleryVariant& dv = kViewDefaultVariant;
|
||
const double exagg =
|
||
a.kv.count("exagg") ? std::stod(a.get("exagg", "8")) : dv.exagg;
|
||
const double opacity = a.kv.count("opacity")
|
||
? std::stod(a.get("opacity", "0.5"))
|
||
: dv.maxOpacity;
|
||
const std::size_t budget =
|
||
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
|
||
const int frames = std::stoi(a.get("frames", "90"));
|
||
auto hasFlag = [&](const char* name) {
|
||
return a.kv.count(name) > 0 ||
|
||
std::find(a.positional.begin(), a.positional.end(),
|
||
std::string("--") + name) != a.positional.end();
|
||
};
|
||
const bool smoke = hasFlag("smoke");
|
||
const bool preview = hasFlag("preview");
|
||
// 拉近预览(Task 12d-singletex):--preview --variant near(或 --near)走与真窗口
|
||
// 完全相同的单纹理拉近路径(viewRefreshSingle 选 level0 局部子区域),供控制方 Read
|
||
// 验证「拉近后」单图非空、完整、fps 高。
|
||
const bool nearPreview =
|
||
hasFlag("near") || (a.kv.count("variant") && a.get("variant", "") == "near");
|
||
|
||
// 画廊模式(Task 12d):渲 4 组视觉调参图供挑选。优先于其余路径。
|
||
if (hasFlag("gallery")) {
|
||
return cmdViewGallery(dir, frames);
|
||
}
|
||
// 单变体:view --preview --variant N(N=1..4),只渲第 N 组(near 不走此路)。
|
||
if (preview && a.kv.count("variant") && !nearPreview) {
|
||
const int vi = std::stoi(a.get("variant", "1"));
|
||
const int n = static_cast<int>(sizeof(kGalleryVariants) /
|
||
sizeof(kGalleryVariants[0]));
|
||
if (vi < 1 || vi > n) {
|
||
std::cerr << "[view] --variant 需在 1.." << n << " 之间\n";
|
||
return 2;
|
||
}
|
||
std::cout << "[view] storeDir=" << dir << " 单变体 variant=" << vi << "\n";
|
||
if (cmdOffscreenSmoke() != 0) return 1;
|
||
return runGalleryVariant(dir, kGalleryVariants[vi - 1], frames);
|
||
}
|
||
|
||
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
|
||
<< " opacity=" << opacity << " budget=" << budget
|
||
<< (preview ? " [PREVIEW 离屏存图+测fps]"
|
||
: (smoke ? " [SMOKE 离屏]" : " [真窗口交互]"))
|
||
<< "\n";
|
||
|
||
// preview/smoke 走离屏。
|
||
const bool offscreen = smoke || preview;
|
||
const int winW = 1280, winH = 800;
|
||
|
||
// 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产
|
||
// 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。
|
||
(void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const auto& m = store.meta();
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
// 配色/不透明度包络取自 var4:seismic + V 形实体包络(floor/mid + opacity 作峰值)。
|
||
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeVariantProperty(dv, m.quant, cs, vmin, vmax, opacity);
|
||
|
||
// 渲染窗口:preview/smoke 走离屏,否则真窗口。
|
||
vtkSmartPointer<vtkRenderWindow> rw;
|
||
if (offscreen) {
|
||
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(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景
|
||
rw->AddRenderer(ren);
|
||
|
||
// 单纹理:单 vtkSmartVolumeMapper(GPU 光线投射,整张 3D 纹理),与 --preview /
|
||
// gallery 同一 mapper 类型,保证交互画面 == 预览画面、fps 同档。
|
||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
mapper->SetAutoAdjustSampleDistances(0);
|
||
mapper->SetInteractiveAdjustSampleDistances(0);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, exagg, exagg); // 垂向夸张(默认 var4 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.store = &store;
|
||
st.mapper = mapper.Get();
|
||
st.ren = ren.Get();
|
||
st.fpsText = fpsText.Get();
|
||
st.rw = rw.Get();
|
||
st.exagg = exagg;
|
||
|
||
// 相机初始定向(修复 1):默认框「局部段」而非整卷。整线横截面 1:34,框整卷
|
||
// 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口
|
||
// 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。
|
||
std::size_t warm = viewSetupDefaultFrame(&st, ren);
|
||
rw->Render();
|
||
|
||
// 拉近预览:在默认取景基础上拉近相机,再走 viewRefreshSingle(与真窗口缩放后
|
||
// 完全相同的单纹理路径,选 level0 局部子区域),验证「拉近后」单图非空、完整。
|
||
if (nearPreview) {
|
||
st.cam->Dolly(2.5); // 拉近
|
||
ren->ResetCameraClippingRange();
|
||
warm = viewRefreshSingle(&st);
|
||
rw->Render();
|
||
}
|
||
|
||
std::cout << "[view] 预热(" << (nearPreview ? "拉近局部段" : "默认局部段")
|
||
<< "): level=" << st.lastLevel << " 渲染块=" << warm << "\n";
|
||
|
||
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH);
|
||
const bool textureErr = capWin->textureError();
|
||
const bool renderedOk = !textureErr && nonBlack > 0;
|
||
|
||
if (preview) {
|
||
// 修复 2:用与真窗口完全相同的默认相机/source/exagg/传函(viewSetupDefaultFrame
|
||
// 已建好),离屏渲一帧存图 → 控制方先 Read 确认开窗默认画面非空、有结构。
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const std::string pngPath =
|
||
(shotDir / (nearPreview ? "view-near.png" : "view-default.png"))
|
||
.string();
|
||
savePng(rw.Get(), pngPath);
|
||
// 结构像素计数:背景为深蓝灰(R/G≈10,B≈20),countNonBlackPixels(>10) 会把整屏
|
||
// 背景都算「非空」,对验证「画面有结构」无意义。改为只数明显亮于背景的像素
|
||
// (任一通道 >50),作为「确有渲出的体结构」的诚实判据。
|
||
auto countStructPixels = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType defStruct = countStructPixels();
|
||
|
||
// 旋相机 N 帧测真实 fps(非首帧:首帧含纹理上传/shader 编译已在预热完成)。
|
||
rw->Render(); // 再预热一帧,确保管线热
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
st.cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
|
||
|
||
const bool texErr2 = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool ok = !texErr2 && defStruct > 0;
|
||
|
||
std::cout << "\n=== view --preview 离屏默认视角验证 ===\n";
|
||
std::cout << "默认局部段维度 : " << kViewDefaultLocalBricks
|
||
<< " brick 列(沿线中段) level0\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "结构像素(>50) : " << defStruct << " / " << (winW * winH)
|
||
<< " (" << (100.0 * defStruct / (winW * winH))
|
||
<< "%, 已排除深蓝灰背景)\n";
|
||
std::cout << "纹理维度错误 : " << (texErr2 ? "是(!!)" : "否") << "\n";
|
||
std::cout << "真实渲染 fps : " << (ok ? std::to_string(fps) : "INVALID")
|
||
<< " (" << frames << " 帧旋相机, 非首帧)\n";
|
||
std::cout << "preview 结果 : "
|
||
<< (ok ? "OK ✔ 默认视角有结构" : "FAIL ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-preview,dir=" + dir + ",exagg=" + std::to_string(exagg) +
|
||
",opacity=" + std::to_string(opacity) +
|
||
",localBricks=" + std::to_string(kViewDefaultLocalBricks) +
|
||
",structPixels=" + std::to_string(defStruct) +
|
||
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
|
||
",textureErr=" + std::to_string(texErr2 ? 1 : 0) +
|
||
",ok=" + std::to_string(ok ? 1 : 0) +
|
||
",png=" + pngPath);
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
if (smoke) {
|
||
// 离屏 smoke:模拟一次缩放 → 验 LOD 切换 + 不崩(单纹理路径)。
|
||
const int lvlNear = st.lastLevel;
|
||
st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD(整卷粗层单纹理)
|
||
ren->ResetCameraClippingRange();
|
||
const std::size_t blocksFar = viewRefreshSingle(&st);
|
||
const int lvlFar = st.lastLevel;
|
||
rw->Render();
|
||
st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LOD(level0 局部子区域)
|
||
ren->ResetCameraClippingRange();
|
||
viewRefreshSingle(&st);
|
||
const int lvlNear2 = st.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 + 刷 fps(仅松手触发一次,不自激)。
|
||
// 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent
|
||
// 形成无限递归重渲(窗口卡死、fps≈0)。fps 文本在松手时刷新即可。
|
||
iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
|
||
|
||
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
|
||
iren->Initialize();
|
||
rw->Render();
|
||
iren->Start();
|
||
|
||
std::cout << "[view] 窗口关闭,退出。\n";
|
||
return 0;
|
||
}
|
||
|
||
void usage() {
|
||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n"
|
||
" gpr_poc load <storeDir>\n"
|
||
" gpr_poc selftest\n"
|
||
" 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 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] "
|
||
"[--smoke] [--preview] [--near] [--variant N] [--gallery] "
|
||
"[--frames 90]\n";
|
||
}
|
||
|
||
} // namespace
|
||
|
||
int main(int argc, char** argv) {
|
||
#ifdef _WIN32
|
||
// Windows 控制台默认 GBK,会把 UTF-8 中文输出显示为乱码。设为 UTF-8 码页修复。
|
||
SetConsoleOutputCP(CP_UTF8);
|
||
#endif
|
||
if (argc < 2) {
|
||
usage();
|
||
return 2;
|
||
}
|
||
const std::string cmd = argv[1];
|
||
try {
|
||
if (cmd == "build") return cmdBuild(argc, argv);
|
||
if (cmd == "load") return cmdLoad(argc, argv);
|
||
if (cmd == "selftest") return cmdSelftest();
|
||
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();
|
||
if (cmd == "renderB") return cmdRenderB(argc, argv);
|
||
if (cmd == "renderC") return cmdRenderC(argc, argv);
|
||
if (cmd == "renderC-partitioned")
|
||
return cmdRenderCPartitioned(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) {
|
||
std::cerr << "错误: " << e.what() << "\n";
|
||
return 1;
|
||
}
|
||
usage();
|
||
return 2;
|
||
}
|