geopro/tools/gpr_poc/main.cpp

5856 lines
265 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <thread>
#include <vector>
#include "Probe.hpp"
#include "core/algo/GeoVolumeBuilder.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/Gpr3dvSurveyVolumeBridge.hpp"
#include "io/gpr/Gpr3dvVolumeBridge.hpp"
#include "io/gpr/GprSurveyAssembler.hpp"
#include "io/gpr/GpsTrack.hpp"
#include "io/gpr/IprHeader.hpp"
#include "render/actors/VoxelActor.hpp"
#include "render/source/OutOfCoreSource.hpp"
#include "render/source/ViewAdaptiveVolumeSource.hpp"
#include "render/source/WholeVolumeSource.hpp"
// P9: WGS84 → CGCS2000 高斯-克吕格精确平面坐标(vendored 3DGPRViewer零 Qt)。
#include "CoordinateTransform.h"
// ---- 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 <vtkTransform.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 推 GridSpecX 沿测线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;
}
// ============================================================================
// build-stream多线合并流式建大体Track B 总验收)
// ============================================================================
//
// 把工区目录下全部测线(各 14 通道 .iprb + 该线 .ord流式建成【一个连续合并大体】
// 1) 扫目录发现所有线号;
// 2) 定合并网格:沿 X 顺序排列(线 i 接在线 i-1 之后,按 brick 对齐对齐到 64 格边界),
// Y/Z 取各线最大值——退路近似report 标注),证明大体流式建得出来且内存有界;
// 3) 全局量化:单遍扫所有线所有 slab 定全局 vmin/vmax一次只持一个 64 道 slab
// 4) 单个 StreamingVolumeWriter 跨线逐 slab 逐 brick 写(各线落在合并网格对应 X 区域),
// 全程不持整卷/不持整线 survey
// 5) buildPyramidStreaming → finalize。
//
// 复用 buildGprVolumeStreaming 的 slab/采样核机制sampleGprPoint + StreamingVolumeWriter
// 仅在 X 方向把多线拼到同一 store。
constexpr int kStreamBrick = 64; // 与 StreamingVolumeWriter/Store 内部 brick 一致
int ceilDivInt(int n, int b) { return (n + b - 1) / b; }
int extentOf(int n, int b, int brick) {
const int got = n - b * brick;
return got < brick ? got : brick;
}
// 发现工区内全部线号(三位零填充,如 "001"):扫 .ord取 "*_<NNN>.ord" 的 NNN。
std::vector<std::string> discoverLines(const std::string& dir) {
std::vector<std::string> lines;
for (const auto& e : fs::directory_iterator(dir)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".ord") continue;
const std::string stem = e.path().stem().string(); // 明星路_001
const std::size_t us = stem.find_last_of('_');
if (us == std::string::npos) continue;
const std::string num = stem.substr(us + 1);
bool allDigit = !num.empty();
for (char c : num)
if (!std::isdigit(static_cast<unsigned char>(c))) allDigit = false;
if (allDigit) lines.push_back(num);
}
std::sort(lines.begin(), lines.end());
return lines;
}
// 一条线的几何 + 道距 + 全线总道数不持整线1 道 slab 取标尺header 取总道数)。
struct LineGeom {
int samples = 0;
std::int64_t totalTraces = 0;
double dx = 1.0, dz = 1.0;
std::vector<double> channelY; // 升序
int nx = 0, ny = 0, nz = 0; // 该线在合并网格下的体素维度X 未对齐 brick
};
// 全线总道数 = min 通道(fileBytes/(samples*2)),与 assembleGprSurvey 对齐口径一致。
std::int64_t totalTracesOf(const std::vector<std::string>& iprb, int samples) {
std::int64_t minTr = std::numeric_limits<std::int64_t>::max();
const std::int64_t per = static_cast<std::int64_t>(samples) * 2;
for (const auto& p : iprb) {
const std::int64_t bytes = static_cast<std::int64_t>(fs::file_size(p));
if (per <= 0) throw std::runtime_error("samples<=0");
minTr = std::min(minTr, bytes / per);
}
return minTr;
}
int cmdBuildStream(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc build-stream <dir> [--cellXY 0.05] "
"[--cellZ 0.05] [--out <storeDir>] [--levels 3] "
"[--sliceXBricks 8] [--maxLines N]\n";
return 2;
}
const std::string dir = a.positional[0];
const double cellXY = std::stod(a.get("cellXY", "0.05"));
const double cellZ = std::stod(a.get("cellZ", "0.05"));
const int levels = std::stoi(a.get("levels", "3"));
int sliceXBricks = std::stoi(a.get("sliceXBricks", "8"));
if (sliceXBricks <= 0) sliceXBricks = 1;
const int maxLines = std::stoi(a.get("maxLines", "0")); // 0=全部
const std::string out =
a.get("out", (fs::temp_directory_path() / "gpr_store_merged").string());
std::cout << "[build-stream] dir=" << dir << " cellXY=" << cellXY
<< " cellZ=" << cellZ << " levels=" << levels
<< " sliceXBricks=" << sliceXBricks << " out=" << out << "\n";
// 1) 发现线号。
std::vector<std::string> lineNos = discoverLines(dir);
if (maxLines > 0 && static_cast<int>(lineNos.size()) > maxLines)
lineNos.resize(maxLines);
std::cout << "[build-stream] 发现测线数=" << lineNos.size() << "\n";
if (lineNos.empty()) {
std::cerr << "[build-stream] 错误: 未发现任何 .ord 测线\n";
return 1;
}
Stopwatch swTotal;
// 2) 各线文件 + 几何1 道 slab 取标尺,不持整线)。
std::vector<LineFiles> files;
std::vector<LineGeom> geom;
files.reserve(lineNos.size());
geom.reserve(lineNos.size());
for (const std::string& ln : lineNos) {
LineFiles lf = discoverLine(dir, ln);
if (lf.iprb.empty() || lf.ord.empty()) {
std::cerr << "[build-stream] 警告: 线 " << ln << " 缺 iprb/ord跳过\n";
continue;
}
LineGeom g;
// 1 道 slab取 dx/dz/samples/channelY内存只随 1 道)。
const geopro::core::GprSurvey s0 =
geopro::io::gpr::assembleGprSurveySlab(lf.iprb, lf.ord, 0, 1);
g.samples = s0.samples;
g.dx = s0.dx;
g.dz = s0.dz;
g.channelY = s0.channelY;
g.totalTraces = totalTracesOf(lf.iprb, g.samples);
// 该线在合并网格下维度X/Z 落格Y 跨通道):与 specFromSurvey 同式。
const double rangeX = (g.totalTraces > 1) ? (g.totalTraces - 1) * g.dx : 0.0;
const double rangeY =
g.channelY.empty() ? 0.0 : (g.channelY.back() - g.channelY.front());
const double rangeZ = (g.samples > 1) ? (g.samples - 1) * g.dz : 0.0;
auto cells = [](double range, double cell) {
if (cell <= 0.0) return 1;
return static_cast<int>(std::ceil(range / cell)) + 1;
};
g.nx = cells(rangeX, cellXY);
g.ny = cells(rangeY, cellXY);
g.nz = cells(rangeZ, cellZ);
std::cout << "[build-stream] 线 " << ln << " 通道=" << lf.iprb.size()
<< " 道数=" << g.totalTraces << " samples=" << g.samples
<< " nx=" << g.nx << " ny=" << g.ny << " nz=" << g.nz << "\n";
files.push_back(std::move(lf));
geom.push_back(std::move(g));
}
if (files.empty()) {
std::cerr << "[build-stream] 错误: 无可用测线\n";
return 1;
}
// 3) 合并网格(沿 X 顺序排列;各线 X 起点对齐到 brick 边界)。
// 每线占 [xBrickOffset, xBrickOffset + ceil(nx/brick)) 的 brick 列。
std::vector<int> xBrickOffset(files.size());
int mergedBx = 0, mergedNy = 0, mergedNz = 0;
for (std::size_t i = 0; i < files.size(); ++i) {
xBrickOffset[i] = mergedBx;
mergedBx += ceilDivInt(geom[i].nx, kStreamBrick);
mergedNy = std::max(mergedNy, geom[i].ny);
mergedNz = std::max(mergedNz, geom[i].nz);
}
const int mergedNx = mergedBx * kStreamBrick; // 末线 brick 对齐后整宽
const int bY = ceilDivInt(mergedNy, kStreamBrick);
const int bZ = ceilDivInt(mergedNz, kStreamBrick);
std::cout << "[build-stream] 合并网格 nx=" << mergedNx << " ny=" << mergedNy
<< " nz=" << mergedNz << " (bX=" << mergedBx << " bY=" << bY
<< " bZ=" << bZ << ")\n";
// 每线网格 specorigin 沿 X 平移到该线 brick 起点的世界 X
auto specForLine = [&](std::size_t i) {
geopro::core::GridSpec spec{};
spec.ox = xBrickOffset[i] * kStreamBrick * cellXY; // 该线在合并体的世界 X 起点
spec.oy = geom[i].channelY.empty() ? 0.0 : geom[i].channelY.front();
spec.oz = 0.0;
spec.dx = cellXY;
spec.dy = cellXY;
spec.dz = cellZ;
spec.nx = geom[i].nx;
spec.ny = geom[i].ny;
spec.nz = geom[i].nz;
spec.power = 2.0;
spec.maxDist = cellXY * 2.0;
return spec;
};
// 4) 全局量化:单遍扫所有线所有 slab一次只持一个 64 道 slab
std::cout << "[build-stream] 扫全局量化区间...\n";
Stopwatch swScan;
double vmin = std::numeric_limits<double>::infinity();
double vmax = -std::numeric_limits<double>::infinity();
constexpr std::int64_t kScanChunk = 64;
for (std::size_t i = 0; i < files.size(); ++i) {
const std::int64_t total = geom[i].totalTraces;
for (std::int64_t t0 = 0; t0 < total; t0 += kScanChunk) {
const std::int64_t t1 = std::min(total, t0 + kScanChunk);
const auto slab = geopro::io::gpr::assembleGprSurveySlab(
files[i].iprb, files[i].ord, t0, t1);
for (double v : slab.values) {
if (std::isnan(v)) continue;
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
}
}
}
if (!(vmin <= vmax)) {
vmin = 0.0;
vmax = 0.0;
}
const double scanMs = swScan.elapsedMs();
std::cout << "[build-stream] 全局值域 [" << vmin << ", " << vmax << "] 扫描 "
<< scanMs << "ms\n";
geopro::core::Quant quant;
quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0;
quant.offset = 0.5 * (vmin + vmax);
// 5) 合并 StoreMeta + 单个 StreamingVolumeWriter。
geopro::data::StoreMeta meta;
meta.nx = mergedNx;
meta.ny = mergedNy;
meta.nz = mergedNz;
meta.brick = kStreamBrick;
meta.origin = {0.0, 0.0, 0.0};
meta.spacing = {cellXY, cellXY, cellZ};
meta.quant = quant;
meta.vminPhys = vmin;
meta.vmaxPhys = vmax;
fs::create_directories(out);
geopro::data::StreamingVolumeWriter writer(out, meta);
// 6) 跨线逐 slab 逐 brick 写。每线在自己的 brick 列区间内沿 X 分 slab
// 合并网格 brick (mergedBx, by, bz)
// - 落在某线列区间内且该线有覆盖 → sampleGprPoint线局部索引
// - 否则 → blank线间填充 + 该线 ny/nz 之外的合并余量)。
std::cout << "[build-stream] 流式写合并大体...\n";
Stopwatch swBuild;
for (std::size_t i = 0; i < files.size(); ++i) {
const geopro::core::GridSpec spec = specForLine(i);
const int lineBx = ceilDivInt(geom[i].nx, kStreamBrick);
const int lineBy = ceilDivInt(geom[i].ny, kStreamBrick);
const int lineBz = ceilDivInt(geom[i].nz, kStreamBrick);
const std::int64_t total = geom[i].totalTraces;
const double surveyDx = geom[i].dx > 0.0 ? geom[i].dx : 1.0;
// 沿 X 分 slabbrick 对齐),每 slab 含 sliceXBricks 个 X brick。
for (int bcol = 0; bcol < lineBx; bcol += sliceXBricks) {
const int bxEnd = std::min(lineBx, bcol + sliceXBricks);
const int gx0 = bcol * kStreamBrick;
const int gx1 = std::min(spec.nx, bxEnd * kStreamBrick);
// 该 slab 网格 X 列 → 全局道范围(夹到 [0,total)),可能全越界。
std::int64_t t0 = std::numeric_limits<std::int64_t>::max();
std::int64_t t1 = std::numeric_limits<std::int64_t>::min();
for (int gi = gx0; gi < gx1; ++gi) {
const double worldX = gi * cellXY; // 线局部世界 Xspec.ox 已含偏移,但落格用线内 x0=0
const std::int64_t g = std::llround(worldX / surveyDx);
if (g < 0 || g >= total) continue;
t0 = std::min(t0, g);
t1 = std::max(t1, g);
}
const bool hasTraces = (t0 <= t1);
geopro::core::GprSurvey slab;
// 线局部 specx0=0 落格(与 assembleGprSurveySlab 的 x0=t0*dx 对齐靠 worldX
geopro::core::GridSpec localSpec = spec;
localSpec.ox = 0.0; // 采样核用线局部坐标
if (hasTraces) {
slab = geopro::io::gpr::assembleGprSurveySlab(files[i].iprb,
files[i].ord, t0, t1 + 1);
}
// 写该 slab 覆盖的合并 brickX 列 [bcol,bxEnd) → 合并列 +xBrickOffset[i]
// Y/Z 全程(含该线 ny/nz 之外的合并余量 → blank
for (int bz = 0; bz < bZ; ++bz) {
for (int by = 0; by < bY; ++by) {
for (int lbx = bcol; lbx < bxEnd; ++lbx) {
const int mbx = xBrickOffset[i] + lbx; // 合并 brick X 索引
const int bw = extentOf(mergedNx, mbx, kStreamBrick);
const int bh = extentOf(mergedNy, by, kStreamBrick);
const int bd = extentOf(mergedNz, bz, kStreamBrick);
std::vector<std::int16_t> voxels(
static_cast<std::size_t>(bw) * bh * bd);
// 该 brick 是否落在线自身覆盖范围内(线 brick 网格内)。
const bool inLine =
(lbx < lineBx && by < lineBy && bz < lineBz && hasTraces);
if (!inLine) {
std::fill(voxels.begin(), voxels.end(),
geopro::core::ScalarVolumeI16::kBlank);
} else {
const int i0 = lbx * kStreamBrick, j0 = by * kStreamBrick,
k0 = bz * kStreamBrick;
std::size_t wi = 0;
for (int kk = 0; kk < bd; ++kk) {
for (int jj = 0; jj < bh; ++jj) {
for (int ii = 0; ii < bw; ++ii) {
const int gi = i0 + ii, gj = j0 + jj, gk = k0 + kk;
// 线网格之外的余格(合并 brick 比线 brick 大)→ blank。
if (gi >= spec.nx || gj >= spec.ny || gk >= spec.nz) {
voxels[wi++] = geopro::core::ScalarVolumeI16::kBlank;
} else {
voxels[wi++] = geopro::core::sampleGprPoint(
slab, localSpec, gi, gj, gk, quant);
}
}
}
}
}
writer.writeBrick(mbx, by, bz, voxels);
}
}
}
}
std::cout << "[build-stream] 线 " << lineNos[i] << " 写入完成 ("
<< (i + 1) << "/" << files.size()
<< ") 峰值内存(MB)=" << Probe::peakMemMB() << "\n";
}
writer.finalize();
const double buildMs = swBuild.elapsedMs();
// 7) 流式金字塔。
std::cout << "[build-stream] 流式金字塔 levels=" << levels << "...\n";
Stopwatch swPyr;
{
geopro::data::ChunkedVolumeStore store(out);
store.buildPyramidStreaming(levels);
}
const double pyrMs = swPyr.elapsedMs();
const std::int64_t voxels =
static_cast<std::int64_t>(mergedNx) * mergedNy * mergedNz;
const std::int64_t rawBytes = voxels * 2;
const std::int64_t dataBytes = storeDataBytes(out);
const double ratio =
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
const double totalMs = swTotal.elapsedMs();
const double peak = Probe::peakMemMB();
std::cout << "\n=== build-stream 指标(多线合并大体)===\n";
std::cout << "合并方式 : 沿 X 顺序排列退路近似brick 对齐)\n";
std::cout << "测线数 : " << files.size() << "\n";
std::cout << "合并体维度 : " << mergedNx << " x " << mergedNy << " x "
<< mergedNz << "\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 << "扫量化耗时(ms) : " << scanMs << "\n";
std::cout << "建体耗时(ms) : " << buildMs << "\n";
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
std::cout << "总耗时(ms) : " << totalMs << "\n";
std::cout << "峰值内存(MB) : " << peak << "\n";
writeMetricLine(
"build-stream,lines=" + std::to_string(files.size()) +
",cellXY=" + std::to_string(cellXY) + ",cellZ=" + std::to_string(cellZ) +
",nx=" + std::to_string(mergedNx) + ",ny=" + std::to_string(mergedNy) +
",nz=" + std::to_string(mergedNz) + ",voxels=" + std::to_string(voxels) +
",rawB=" + std::to_string(rawBytes) +
",dataB=" + std::to_string(dataBytes) +
",ratio=" + std::to_string(ratio) + ",scanMs=" + std::to_string(scanMs) +
",buildMs=" + std::to_string(buildMs) +
",pyrMs=" + std::to_string(pyrMs) +
",totalMs=" + std::to_string(totalMs) +
",peakMB=" + std::to_string(peak));
return 0;
}
// ============================================================================
// build-geo按真实 RTK 几何把多线插值进统一路向网格Task G1
// ============================================================================
//
// 消除 build-stream 顺序拼接的退化扁带:各线 .gps RTK 轨迹 → 经纬 → 局部米 →
// PCA 路向旋转 → 道按里程均匀分布定位、14 通道横偏垂直航向摆放 → 全部插进统一
// 路向网格(≈4472×43×81)重叠取均值 → 量化写 ChunkedVolumeStore + 金字塔。
int cmdBuildGeo(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] "
"[--out <storeDir>] [--levels 4] [--maxLines N] "
"[--curvilinear]\n";
return 2;
}
const std::string dir = a.positional[0];
const double cellXY = std::stod(a.get("cellXY", "0.5"));
const double cellZ = std::stod(a.get("cellZ", "0.1"));
const int levels = std::stoi(a.get("levels", "4"));
const int maxLines = std::stoi(a.get("maxLines", "0"));
const bool curvilinear = a.kv.count("curvilinear") > 0; // 曲线版G2
const std::string out =
a.get("out", (fs::temp_directory_path() / "gpr_store_geo").string());
std::cout << "[build-geo] dir=" << dir << " cellXY=" << cellXY
<< " cellZ=" << cellZ << " levels=" << levels
<< " mode=" << (curvilinear ? "曲线(中心线)" : "PCA")
<< " out=" << out << "\n";
// 发现线号 → 各线 iprb/ord复用 discoverLine+ .gps按线号匹配
std::vector<std::string> lineNos = discoverLines(dir);
if (maxLines > 0 && static_cast<int>(lineNos.size()) > maxLines)
lineNos.resize(maxLines);
std::cout << "[build-geo] 发现测线数=" << lineNos.size() << "\n";
if (lineNos.empty()) {
std::cerr << "[build-geo] 错误: 未发现任何 .ord 测线\n";
return 1;
}
std::vector<geopro::core::GeoLineInput> lines;
for (const std::string& ln : lineNos) {
const LineFiles lf = discoverLine(dir, ln);
if (lf.iprb.empty() || lf.ord.empty()) {
std::cerr << "[build-geo] 警告: 线 " << ln << " 缺 iprb/ord跳过\n";
continue;
}
// .gps匹配 "*_<ln>.gps"。
std::string gps;
for (const auto& e : fs::directory_iterator(dir)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".gps") continue;
const std::string stem = e.path().stem().string();
const std::size_t us = stem.find_last_of('_');
if (us != std::string::npos && stem.substr(us + 1) == ln) {
gps = e.path().string();
break;
}
}
if (gps.empty()) {
std::cerr << "[build-geo] 警告: 线 " << ln << " 缺 .gps跳过\n";
continue;
}
geopro::core::GeoLineInput in;
in.iprb = lf.iprb;
in.ord = lf.ord;
in.gps = gps;
lines.push_back(std::move(in));
}
if (lines.empty()) {
std::cerr << "[build-geo] 错误: 无可用测线\n";
return 1;
}
std::cout << "[build-geo] 可用测线数=" << lines.size() << "\n";
fs::create_directories(out);
Stopwatch sw;
const geopro::core::GeoGridSpec spec{cellXY, cellZ};
const geopro::core::GeoBuildResult r =
geopro::core::buildGeoVolume(lines, spec, out, levels, curvilinear);
const double buildMs = sw.elapsedMs();
const double fillRate =
r.total > 0 ? static_cast<double>(r.filled) / r.total : 0.0;
const double rotDeg = r.rotRad * 180.0 / 3.14159265358979323846;
const std::int64_t dataBytes = storeDataBytes(out);
const double peak = Probe::peakMemMB();
std::cout << "\n=== build-geo 指标(真实 RTK 几何统一路向体)===\n";
std::cout << "网格模式 : " << (curvilinear ? "曲线(中心线展开)" : "PCA 旋转")
<< "\n";
std::cout << "测线数 : " << lines.size() << "\n";
std::cout << "体维度 : " << r.nx << " x " << r.ny << " x " << r.nz
<< "\n";
std::cout << "总 cell : " << r.total << "\n";
std::cout << "非空 cell : " << r.filled << "\n";
std::cout << "填充率 : " << (fillRate * 100.0) << " %\n";
std::cout << "路向旋转角 : " << rotDeg << " ° (" << r.rotRad << " rad)\n";
std::cout << "网格原点(m) : (" << r.oxM << ", " << r.oyM << ")\n";
std::cout << "data.bin(B) : " << dataBytes << " ("
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
std::cout << "建体总耗时(ms) : " << buildMs << "\n";
std::cout << "峰值内存(MB) : " << peak << "\n";
writeMetricLine(
"build-geo,lines=" + std::to_string(lines.size()) +
",cellXY=" + std::to_string(cellXY) + ",cellZ=" + std::to_string(cellZ) +
",nx=" + std::to_string(r.nx) + ",ny=" + std::to_string(r.ny) +
",nz=" + std::to_string(r.nz) + ",total=" + std::to_string(r.total) +
",filled=" + std::to_string(r.filled) +
",fillRate=" + std::to_string(fillRate) +
",rotDeg=" + std::to_string(rotDeg) +
",buildMs=" + std::to_string(buildMs) +
",peakMB=" + std::to_string(peak));
return 0;
}
// ============================================================================
// build-line走 gpr3dv(P1) 处理链→处理后立方体→geopro 量化体→分块+金字塔(P2)
// ============================================================================
//
// 与 build/build-stream/build-geo 不同:本命令不走 geopro 自家解析+采样核建体,
// 而是【复用 vendored gpr3dv 的读+处理链】(算法零改动)产出处理后立方体
// volumeData[通道][道][样本],再桥接(Gpr3dvVolumeBridge)成 geopro int16 量化体
// (轴 X=道/Y=通道/Z=样本),落 ChunkedVolumeStore + buildPyramid。原样渲(14 格 Y
// 薄体),不做横向插值加密。
// 磁盘剩余空间(GB):查 path 所在卷可用字节。失败(路径不存在等)→ -1(视为未知)。
double freeSpaceGB(const std::string& path) {
std::error_code ec;
// 用已存在的父目录查(out 目录可能还没建)。
fs::path p = fs::absolute(path, ec);
while (!p.empty() && !fs::exists(p, ec)) p = p.parent_path();
if (p.empty()) p = fs::current_path(ec);
const fs::space_info si = fs::space(p, ec);
if (ec) return -1.0;
return static_cast<double>(si.available) / (1024.0 * 1024.0 * 1024.0);
}
// 单线建体结果(供 build-all 汇总,不编造)。ok=false 时 reason 给清晰原因。
struct LineBuildResult {
std::string prefix;
bool ok = false;
std::string reason; // 失败/跳过原因
std::int64_t nx = 0, ny = 0, nz = 0;
std::int64_t dataBytes = 0;
};
// 单线建体核心gpr3dv 处理链 → 桥接量化体(可 coarse 下采样) → 落盘 + 金字塔。
// 异常(加载失败/立方体空/短桩线维度退化)由调用方捕获,不在此中断批量。
LineBuildResult buildOneLine(const std::string& lineDir,
const std::string& linePrefix,
const std::string& out, int levels, int coarse) {
LineBuildResult r;
r.prefix = linePrefix;
std::cout << "[build-line] lineDir=" << lineDir << " linePrefix=" << linePrefix
<< " levels=" << levels << " coarse=" << coarse << " out=" << out
<< "\n";
// 1) gpr3dv 处理链 → 处理后立方体 → 桥接量化体(coarse 沿测线下采样)。
Stopwatch swBridge;
geopro::io::gpr::BridgeMetrics bm;
geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeFromGpr3dv(
lineDir, linePrefix, &bm, coarse);
const double bridgeMs = swBridge.elapsedMs();
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(),
nz = built.vol.nz();
r.nx = nx;
r.ny = ny;
r.nz = nz;
// 短桩线/退化体守护:维度过小无法建有意义的金字塔/体绘制 → 报因跳过(不落盘)。
if (nx < 2 || ny < 1 || nz < 2) {
r.ok = false;
r.reason = "体维度退化(道×通道×样本=" + std::to_string(nx) + "x" +
std::to_string(ny) + "x" + std::to_string(nz) +
"),无法建可看体,跳过";
std::cerr << "[build-line] " << linePrefix << " " << r.reason << "\n";
return r;
}
const std::int64_t voxels = nx * ny * nz;
const std::int64_t rawBytes = voxels * 2; // int16
std::cout << "[build-line] 处理前后平均绝对幅值: " << bm.meanAbsBefore << ""
<< bm.meanAbsAfter << " (处理"
<< (std::abs(bm.meanAbsAfter - bm.meanAbsBefore) > 1e-9 ? "已生效"
: "未变化")
<< ")\n";
std::cout << "[build-line] 体维度(道×通道×样本) = " << nx << " x " << ny
<< " x " << nz << "\n";
std::cout << "[build-line] 世界 spacing dx=" << bm.dx << " dy=" << bm.dy
<< " dz=" << bm.dz << " (m)\n";
// 2) 落盘 + 金字塔(道很长 → 需 levels≥2 使最粗层≤16384)。
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);
r.dataBytes = dataBytes;
const double ratio =
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
const double peak = Probe::peakMemMB();
std::cout << "\n=== build-line 指标(gpr3dv 处理后真三维体)===\n";
std::cout << "桥接耗时(ms) : " << bridgeMs << " (含 读 " << bm.loadMs
<< " + 处理 " << bm.pipelineMs << " + 量化填体 " << bm.fillMs
<< ")\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 << "处理后值域 : [" << bm.vminPhys << ", " << bm.vmaxPhys
<< "] 量化 scale=" << built.quant.scale
<< " offset=" << built.quant.offset << "\n";
std::cout << "世界 spacing : dx=" << bm.dx << " dy=" << bm.dy
<< " dz=" << bm.dz << " (m)\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,prefix=" + linePrefix + ",coarse=" + std::to_string(coarse) +
",nx=" + std::to_string(nx) + ",ny=" + std::to_string(ny) +
",nz=" + std::to_string(nz) + ",voxels=" + std::to_string(voxels) +
",vmin=" + std::to_string(bm.vminPhys) +
",vmax=" + std::to_string(bm.vmaxPhys) +
",dx=" + std::to_string(bm.dx) + ",dy=" + std::to_string(bm.dy) +
",dz=" + std::to_string(bm.dz) + ",rawB=" + std::to_string(rawBytes) +
",dataB=" + std::to_string(dataBytes) +
",ratio=" + std::to_string(ratio) +
",bridgeMs=" + std::to_string(bridgeMs) +
",writeMs=" + std::to_string(writeMs) +
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
r.ok = true;
return r;
}
int cmdBuildLine(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.size() < 2) {
std::cerr << "用法: gpr_poc build-line <lineDir> <linePrefix> "
"--out <storeDir> [--levels 3] [--coarse F]\n"
"例: gpr_poc build-line \"D:/Downloads/明星路\" 明星路_001 "
"--out tmp/line001_proc --levels 3 --coarse 4\n";
return 2;
}
const std::string lineDir = a.positional[0];
const std::string linePrefix = a.positional[1];
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
const std::string out =
a.get("out", (fs::temp_directory_path() / "gpr_store_line").string());
try {
const LineBuildResult r =
buildOneLine(lineDir, linePrefix, out, levels, coarse);
if (!r.ok) {
std::cerr << "[build-line] 跳过: " << r.reason << "\n";
return 1;
}
return 0;
} catch (const std::exception& e) {
std::cerr << "[build-line] 失败(" << linePrefix << "): " << e.what() << "\n";
return 1;
}
}
// build-all发现目录下所有测线(_Axx 分组),逐条 build-line 到 baseDir/<lineName>/。
// 磁盘守护:每条建前查可用空间,低于阈值(默认 3GB)即停并报已建哪些。
// 短桩线/异常单条捕获并跳过(报因),不中断其余。
int cmdBuildAll(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty() || !a.kv.count("outBase")) {
std::cerr << "用法: gpr_poc build-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--minFreeGB 3]\n"
"例: gpr_poc build-all \"D:/Downloads/明星路\" "
"--outBase tmp/lines_all --levels 3 --coarse 4\n";
return 2;
}
const std::string lineDir = a.positional[0];
const std::string outBase = a.get("outBase", "");
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
const double minFreeGB = std::stod(a.get("minFreeGB", "3"));
// 1) 发现所有测线前缀:扫 *_<line>_A<NN>.iprh取 "<...>_<line>" 部分(去 _A<NN>)。
std::set<std::string> prefixSet;
std::error_code ec;
for (const auto& e : fs::directory_iterator(lineDir, ec)) {
if (!e.is_regular_file()) continue;
const std::string name = e.path().filename().string();
if (e.path().extension().string() != ".iprh") continue;
// 找 "_A<NN>" 通道后缀,截断得测线前缀。
const std::size_t pos = name.rfind("_A");
if (pos == std::string::npos) continue;
std::size_t d = pos + 2;
while (d < name.size() && std::isdigit(static_cast<unsigned char>(name[d])))
++d;
if (d == pos + 2) continue; // _A 后无数字 → 非通道文件
prefixSet.insert(name.substr(0, pos));
}
if (prefixSet.empty()) {
std::cerr << "[build-all] 未在 " << lineDir << " 发现任何测线(*_A<NN>.iprh)\n";
return 1;
}
std::vector<std::string> prefixes(prefixSet.begin(), prefixSet.end());
std::cout << "[build-all] 发现 " << prefixes.size() << " 条测线outBase="
<< outBase << " levels=" << levels << " coarse=" << coarse
<< " minFreeGB=" << minFreeGB << "\n";
fs::create_directories(outBase, ec);
// 2) 逐条建:磁盘守护 → buildOneLine(单条 try/catch)。
std::vector<LineBuildResult> results;
bool stoppedByDisk = false;
for (const std::string& prefix : prefixes) {
const double freeGB = freeSpaceGB(outBase);
std::cout << "\n[build-all] --- " << prefix << " --- 剩余磁盘 "
<< freeGB << " GB\n";
if (freeGB >= 0.0 && freeGB < minFreeGB) {
std::cerr << "[build-all] 磁盘守护触发: 剩余 " << freeGB << " GB < "
<< minFreeGB << " GB停止未建 " << prefix << " 及其后。\n";
stoppedByDisk = true;
break;
}
const std::string out = (fs::path(outBase) / prefix).string();
LineBuildResult r;
r.prefix = prefix;
try {
r = buildOneLine(lineDir, prefix, out, levels, coarse);
} catch (const std::exception& e) {
r.ok = false;
r.reason = std::string("异常: ") + e.what();
std::cerr << "[build-all] " << prefix << " 失败: " << e.what()
<< "(跳过,继续)\n";
}
results.push_back(r);
}
// 3) 汇总。
std::cout << "\n=== build-all 汇总 ===\n";
int okCount = 0;
std::int64_t totalBytes = 0;
for (const auto& r : results) {
if (r.ok) {
++okCount;
totalBytes += r.dataBytes;
std::cout << " [OK] " << r.prefix << " 维度=" << r.nx << "x" << r.ny
<< "x" << r.nz << " data="
<< r.dataBytes / (1024.0 * 1024.0) << " MB\n";
} else {
std::cout << " [跳过] " << r.prefix << " 原因=" << r.reason << "\n";
}
}
std::cout << "成功 " << okCount << "/" << prefixes.size()
<< " 条,合计 data=" << totalBytes / (1024.0 * 1024.0 * 1024.0)
<< " GB剩余磁盘 " << freeSpaceGB(outBase) << " GB\n";
if (stoppedByDisk)
std::cout << "注意: 因磁盘守护提前停止,部分测线未建。\n";
return 0;
}
// ============================================================================
// build-survey-line / build-survey-all测绘级精确坐标逐线世界对齐体(Task P8)
// ============================================================================
//
// 与 build-line(线局部坐标 X=道/Y=通道/Z=样本origin≈0)不同:本路径走 P8 测绘级桥接
// (Gpr3dvSurveyVolumeBridge)——用 vendored 3DGPRViewer 的精确坐标/轨迹/网格代码
// (CoordinateTransform/TrajectoryCalculator/CScanGridder算法零改动),让单线严格按
// CGCS2000 大地坐标、逐通道逐道跟 GPS 轨迹建成【世界轴对齐体】(跟路的弯)。体 meta.origin
// 为真实 CGCS2000 世界米,多线共享同一参考系 → view-survey-all 直接按 origin 摆放即精确就位。
// 按线前缀(如 "明星路_001")在 dir 下找匹配的 .gps(尾段 _NNN 相同)。空串=未找到。
std::string findGpsForPrefix(const std::string& dir, const std::string& prefix) {
const std::size_t us = prefix.find_last_of('_');
if (us == std::string::npos) return "";
const std::string num = prefix.substr(us + 1);
std::error_code ec;
for (const auto& e : fs::directory_iterator(dir, ec)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".gps") continue;
const std::string stem = e.path().stem().string();
const std::size_t s2 = stem.find_last_of('_');
if (s2 != std::string::npos && stem.substr(s2 + 1) == num)
return e.path().string();
}
return "";
}
// 单线测绘级建体核心P8 桥接 → 世界对齐量化体 → 落盘 + 金字塔。
// 异常(加载失败/GPS 无效/体空/退化)由调用方捕获,不在此中断批量。
LineBuildResult buildOneSurveyLine(const std::string& lineDir,
const std::string& linePrefix,
const std::string& gpsPath,
const std::string& out, int levels,
int coarse, double cellSizeM,
double searchRadiusM) {
LineBuildResult r;
r.prefix = linePrefix;
std::cout << "[build-survey-line] lineDir=" << lineDir
<< " linePrefix=" << linePrefix << " gps=" << gpsPath
<< " levels=" << levels << " coarse=" << coarse
<< " cellSize=" << cellSizeM << " out=" << out << "\n";
Stopwatch swBridge;
geopro::io::gpr::SurveyBridgeMetrics bm;
geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeSurvey(
lineDir, linePrefix, gpsPath, &bm, coarse, cellSizeM, searchRadiusM);
const double bridgeMs = swBridge.elapsedMs();
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(),
nz = built.vol.nz();
r.nx = nx;
r.ny = ny;
r.nz = nz;
if (nx < 2 || ny < 2 || nz < 2) {
r.ok = false;
r.reason = "世界体维度退化(东×北×深=" + std::to_string(nx) + "x" +
std::to_string(ny) + "x" + std::to_string(nz) +
"),无法建可看体,跳过";
std::cerr << "[build-survey-line] " << linePrefix << " " << r.reason << "\n";
return r;
}
const std::int64_t voxels = nx * ny * nz;
const std::int64_t rawBytes = voxels * 2;
const double fillRate =
bm.totalCells > 0
? static_cast<double>(bm.filledCells) / bm.totalCells
: 0.0;
std::cout << "[build-survey-line] 处理前后平均绝对幅值: " << bm.meanAbsBefore
<< "" << bm.meanAbsAfter << "\n";
std::cout << "[build-survey-line] RTK点=" << bm.rtkPoints
<< " 通道=" << bm.channels << " CGCS带号=" << bm.cgcsZone
<< " 中央经线=" << bm.centralMeridianDeg << "°\n";
std::cout << "[build-survey-line] 世界体维度(东×北×深) = " << nx << " x " << ny
<< " x " << nz << " 非空体素=" << bm.filledCells << " ("
<< (fillRate * 100.0) << "%)\n";
std::cout << "[build-survey-line] CGCS2000 世界 origin=(" << std::fixed
<< bm.originX << ", " << bm.originY << ") spacing cell=" << bm.cellSizeM
<< " dz=" << bm.dz << " (m)\n";
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);
r.dataBytes = dataBytes;
const double ratio =
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
const double peak = Probe::peakMemMB();
std::cout << "\n=== build-survey-line 指标(测绘级 CGCS2000 世界对齐体)===\n";
std::cout << "桥接耗时(ms) : " << bridgeMs << " (读 " << bm.loadMs
<< " + 处理 " << bm.pipelineMs << " + GPS轨迹 " << bm.trajMs
<< " + 网格量化 " << bm.gridMs << ")\n";
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
std::cout << "世界体维度 : " << nx << " x " << ny << " x " << nz << "\n";
std::cout << "体素数 : " << voxels << " 非空 " << bm.filledCells
<< " (" << (fillRate * 100.0) << "%)\n";
std::cout << "处理后值域 : [" << bm.vminPhys << ", " << bm.vmaxPhys
<< "] 量化 scale=" << built.quant.scale
<< " offset=" << built.quant.offset << "\n";
std::cout << "CGCS2000 origin: (" << std::fixed << bm.originX << ", "
<< bm.originY << ") (东, 北) 米\n";
std::cout << "世界 spacing : cell=" << bm.cellSizeM << " dz=" << bm.dz
<< " (m)\n";
std::cout << "data.bin(B) : " << dataBytes << " ("
<< dataBytes / (1024.0 * 1024.0) << " MB) 压缩比 " << ratio
<< " x\n";
std::cout << "峰值内存(MB) : " << peak << "\n";
writeMetricLine(
"build-survey-line,prefix=" + linePrefix +
",coarse=" + std::to_string(coarse) + ",nx=" + std::to_string(nx) +
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
",voxels=" + std::to_string(voxels) +
",filled=" + std::to_string(bm.filledCells) +
",zone=" + std::to_string(bm.cgcsZone) +
",originX=" + std::to_string(bm.originX) +
",originY=" + std::to_string(bm.originY) +
",cell=" + std::to_string(bm.cellSizeM) + ",dz=" + std::to_string(bm.dz) +
",vmin=" + std::to_string(bm.vminPhys) +
",vmax=" + std::to_string(bm.vmaxPhys) +
",dataB=" + std::to_string(dataBytes) +
",bridgeMs=" + std::to_string(bridgeMs) +
",peakMB=" + std::to_string(peak));
r.ok = true;
return r;
}
int cmdBuildSurveyLine(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.size() < 2) {
std::cerr << "用法: gpr_poc build-survey-line <lineDir> <linePrefix> "
"--out <storeDir> [--levels 3] [--coarse F] [--cell 0.05] "
"[--radius 0.5] [--gps <path>]\n"
"例: gpr_poc build-survey-line \"D:/Downloads/明星路\" "
"明星路_001 --out tmp/survey001 --levels 3 --coarse 4\n";
return 2;
}
const std::string lineDir = a.positional[0];
const std::string linePrefix = a.positional[1];
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
const double cellSizeM = std::stod(a.get("cell", "0"));
const double radiusM = std::stod(a.get("radius", "0"));
std::string gps = a.get("gps", "");
if (gps.empty()) gps = findGpsForPrefix(lineDir, linePrefix);
const std::string out = a.get(
"out", (fs::temp_directory_path() / "gpr_store_survey_line").string());
if (gps.empty()) {
std::cerr << "[build-survey-line] 失败: 未找到 " << linePrefix
<< " 的 .gps(可用 --gps 指定)\n";
return 1;
}
try {
const LineBuildResult r = buildOneSurveyLine(
lineDir, linePrefix, gps, out, levels, coarse, cellSizeM, radiusM);
if (!r.ok) {
std::cerr << "[build-survey-line] 跳过: " << r.reason << "\n";
return 1;
}
return 0;
} catch (const std::exception& e) {
std::cerr << "[build-survey-line] 失败(" << linePrefix << "): " << e.what()
<< "\n";
return 1;
}
}
// build-survey-all发现所有测线 → 逐条走 P8 测绘级路径建世界对齐体到 baseDir/<lineName>/。
// 各线 .gps 自动按尾段 _NNN 匹配;缺 .gps 的线跳过报因。磁盘守护同 build-all。
int cmdBuildSurveyAll(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty() || !a.kv.count("outBase")) {
std::cerr << "用法: gpr_poc build-survey-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--cell 0.05] [--radius 0.5] "
"[--minFreeGB 3]\n"
"例: gpr_poc build-survey-all \"D:/Downloads/明星路\" "
"--outBase tmp/survey_all --levels 3 --coarse 4\n";
return 2;
}
const std::string lineDir = a.positional[0];
const std::string outBase = a.get("outBase", "");
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
const double cellSizeM = std::stod(a.get("cell", "0"));
const double radiusM = std::stod(a.get("radius", "0"));
const double minFreeGB = std::stod(a.get("minFreeGB", "3"));
// 发现所有测线前缀(扫 *_A<NN>.iprh截 _A<NN> 得前缀),与 build-all 同口径。
std::set<std::string> prefixSet;
std::error_code ec;
for (const auto& e : fs::directory_iterator(lineDir, ec)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".iprh") continue;
const std::string name = e.path().filename().string();
const std::size_t pos = name.rfind("_A");
if (pos == std::string::npos) continue;
std::size_t d = pos + 2;
while (d < name.size() && std::isdigit(static_cast<unsigned char>(name[d])))
++d;
if (d == pos + 2) continue;
prefixSet.insert(name.substr(0, pos));
}
if (prefixSet.empty()) {
std::cerr << "[build-survey-all] 未在 " << lineDir
<< " 发现任何测线(*_A<NN>.iprh)\n";
return 1;
}
std::vector<std::string> prefixes(prefixSet.begin(), prefixSet.end());
std::cout << "[build-survey-all] 发现 " << prefixes.size()
<< " 条测线outBase=" << outBase << " levels=" << levels
<< " coarse=" << coarse << " minFreeGB=" << minFreeGB << "\n";
fs::create_directories(outBase, ec);
std::vector<LineBuildResult> results;
bool stoppedByDisk = false;
for (const std::string& prefix : prefixes) {
const double freeGB = freeSpaceGB(outBase);
std::cout << "\n[build-survey-all] --- " << prefix << " --- 剩余磁盘 "
<< freeGB << " GB\n";
if (freeGB >= 0.0 && freeGB < minFreeGB) {
std::cerr << "[build-survey-all] 磁盘守护触发: 剩余 " << freeGB << " GB < "
<< minFreeGB << " GB停止未建 " << prefix << " 及其后。\n";
stoppedByDisk = true;
break;
}
const std::string out = (fs::path(outBase) / prefix).string();
LineBuildResult r;
r.prefix = prefix;
const std::string gps = findGpsForPrefix(lineDir, prefix);
if (gps.empty()) {
r.ok = false;
r.reason = "缺 .gps跳过";
std::cerr << "[build-survey-all] " << prefix << " 缺 .gps跳过\n";
results.push_back(r);
continue;
}
try {
r = buildOneSurveyLine(lineDir, prefix, gps, out, levels, coarse,
cellSizeM, radiusM);
} catch (const std::exception& e) {
r.ok = false;
r.reason = std::string("异常: ") + e.what();
std::cerr << "[build-survey-all] " << prefix << " 失败: " << e.what()
<< "(跳过,继续)\n";
}
results.push_back(r);
}
std::cout << "\n=== build-survey-all 汇总(测绘级 CGCS2000 世界对齐体) ===\n";
int okCount = 0;
std::int64_t totalBytes = 0;
for (const auto& r : results) {
if (r.ok) {
++okCount;
totalBytes += r.dataBytes;
std::cout << " [OK] " << r.prefix << " 维度(东×北×深)=" << r.nx << "x"
<< r.ny << "x" << r.nz << " data="
<< r.dataBytes / (1024.0 * 1024.0) << " MB\n";
} else {
std::cout << " [跳过] " << r.prefix << " 原因=" << r.reason << "\n";
}
}
std::cout << "成功 " << okCount << "/" << prefixes.size()
<< " 条,合计 data=" << totalBytes / (1024.0 * 1024.0 * 1024.0)
<< " GB剩余磁盘 " << freeSpaceGB(outBase) << " GB\n";
if (stoppedByDisk)
std::cout << "注意: 因磁盘守护提前停止,部分测线未建。\n";
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 + .iprhsamples 采样、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] 构造极小合成 survey2 通道)...\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=2cellZ 较细确保 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);
// 创建一个离屏 vtkRenderWindowVTK9.6SetShowWindow(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 更亮、对比更狠,正负反射一眼分开。
// 前置声明(实现在 polish 段):梯度幅值分位统计,供 C4 画廊的梯度不透明度标定。
struct GradStats {
double median = 0, p75 = 0, p90 = 0, p99 = 0, mx = 0;
std::size_t samples = 0;
};
GradStats sampleGradientMagnitude(vtkImageData* img);
// 标量分位标定P3 修可见性核心):扫该体实际体素值分布,取 2%/98% 分位作色阶/
// 不透明度的物理端点,裁掉离群。处理后体值多集中在 ±窄带、少量离群到 ±9000若按
// 全量化域(meta.vminPhys/vmaxPhys=±9249)映射 → 窄带信号落近透明区 → 整体近黑。
// 返回物理单位的 {lo, hi}(已按 quant.toPhys 反算)。前置声明,实现在 polish 段。
struct ScalarPercentiles {
double lo = 0.0, hi = 0.0; // 物理单位2% / 98% 分位)
std::size_t samples = 0;
};
ScalarPercentiles sampleScalarPercentiles(vtkImageData* img,
const geopro::core::Quant& q,
double pLo, double pHi);
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;
}
// 调亮版 seismic与 makeSeismicColorScale 同红-白-蓝走向,但把蓝端提亮、整体抬白,
// 弱信号也落在更亮的色域(强负不再是深蓝、零附近纯白、强正亮红),整体更醒目不发暗。
geopro::core::ColorScale makeBrightSeismicColorScale(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{70, 130, 255, 255}); // 亮蓝(强负)
cs.addStop(at(0.30), geopro::core::Rgba{170, 210, 255, 255}); // 浅亮蓝
cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 纯白(零)
cs.addStop(at(0.70), geopro::core::Rgba{255, 200, 150, 255}); // 亮浅橙
cs.addStop(at(1.00), geopro::core::Rgba{255, 80, 60, 255}); // 亮红(强正)
return cs;
}
// 增强灰度:黑→白单调,但中段抬亮、两端拉满,弱反射也落在中亮灰,层界面/竖纹对比醒目。
// GPR 内部水平层叠/基底反射用灰度往往最干净直读(不被多色相干扰)。
geopro::core::ColorScale makeGrayEnhancedColorScale(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{20, 25, 40, 255}); // 强负:近黑带冷调
cs.addStop(at(0.30), geopro::core::Rgba{120, 125, 135, 255}); // 中负:中灰(抬亮)
cs.addStop(at(0.50), geopro::core::Rgba{180, 182, 188, 255}); // 零附近:亮灰
cs.addStop(at(0.70), geopro::core::Rgba{225, 222, 210, 255}); // 中正:暖亮灰
cs.addStop(at(1.00), geopro::core::Rgba{255, 252, 240, 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) 粗层概览 fpslevel2 整卷(单轴 <16384 → 单 SmartVolumeMapper
// (b) 全分辨率局部 fpslevel0 一段 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 imageX 维 = 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) 粗层概览 fpslevel2 整卷 ----
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);
// 先测 fpsbenchVolumeFps 内部会 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) 全分辨率局部 fpslevel0 一段 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 galleryview --preview --variant N
// ============================================================================
//
// 同一局部段(沿线中段 kViewDefaultLocalBricks 列全分辨率) + 同一相机框法
// (ResetCamera→Elevation/Azimuth→Zoom),只换「不透明度包络 / 配色 / 取景角度 /
// 背景」四组视觉参数,各存一张 PNG 供控制方挑选。fps 对视觉调参近乎中性,每组实测验证。
//
// 注:交互窗口(无 flag 的 view)默认即采用 var4kViewDefaultVariant——配色/不透明度
// 包络/取景/exagg/背景全部走同一份 var4 参数,故「交互默认画面 == view-var4」。
enum class OpacityProfile {
kSolid, // V 形实体感:中高值段普遍可见,半透明实心块
kStructural, // 现有双端斜坡:仅正负两端不透明(对照基线)
};
enum class ColorChoice {
kStructural,
kSeismic,
kJet,
kBrightSeismic, // 调亮版 seismic
kGrayEnhanced, // 增强灰度
};
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; // 报告用中文说明
// ---- C4 视觉调优(梯度不透明度 + 光照),默认关 → 不改既有变体行为 ----
bool useGradientOpacity = false; // SetGradientOpacity均匀层透明、界面/异常显形
bool useShade = false; // SetShade层界面带立体明暗
// ---- P4 调亮/调清晰 ----
// 梯度门松弛度 0~10=严格(均匀层全透、偏暗) 1=宽松(均匀层保留底不透明、更亮更满)。
// 宽松时降低梯度阈值并抬高「低梯度区」的不透明度地板,让横向层叠/基底反射等弱结构保留。
double gradGateRelax = 0.0;
double ambient = 0.30; // 光照环境项别太低否则体面偏暗useShade 时生效。
};
// P4 调亮/调清晰4 组对照(暗版基线 / 提亮 / 高对比 / 灰度增强)。
// 同一局部段、同一斜穿取景El45/Az30。P3 默认seismic+严格梯度门+低 ambient整体
// 偏暗、均匀层被门全透成空。本组在「消雾」与「够亮够满」间放宽门控、抬 ambient、换更亮
// 配色,让横向层叠/竖纹/基底反射醒目。所有端点按该体 2/98 分位自适应runGalleryVariant
// 内标定),非写死单一数据。末项 = kViewDefaultVariant → 交互窗口默认取最清晰醒目组。
//
// 字段floorOpacity/midOpacity/maxOpacityV 形标量包络、gradGateRelax梯度门松弛
// 0~1、ambient光照环境项。提亮组抬高 floor/mid 让均匀层保留底不透明、抬 ambient
// 防体面发暗、放宽梯度门保留弱结构。
const GalleryVariant kGalleryVariants[] = {
// var1暗版基线= P3 默认——seismic + 严格梯度门(relax0) + 低 ambient0.3 + 暗背景。
// 均匀层几乎全透、整体偏暗偏空,仅作对照。
{"var1", OpacityProfile::kSolid, ColorChoice::kSeismic,
0.04, 0.30, 0.60, 8.0, 45.0, 30.0, 1.5, {0.07, 0.08, 0.11},
"暗版基线(P3默认)seismic+严格梯度门+低ambient+暗背景,均匀层近全透、偏暗偏空",
/*useGradientOpacity=*/true, /*useShade=*/true,
/*gradGateRelax=*/0.0, /*ambient=*/0.30},
// var2提亮——调亮版 seismic + 放宽梯度门(relax0.6) + 抬 floor/mid/max + 高 ambient
// + 略亮背景。均匀层保留底不透明、横向层叠透出、整体明显更亮更实。
{"var2", OpacityProfile::kSolid, ColorChoice::kBrightSeismic,
0.10, 0.45, 0.75, 8.0, 45.0, 30.0, 1.5, {0.12, 0.13, 0.17},
"提亮调亮版seismic+放宽梯度门(relax0.6)+抬不透明度(floor0.10/mid0.45/max0.75)"
"+高ambient0.5,均匀层保留、层叠透出、明显更亮",
/*useGradientOpacity=*/true, /*useShade=*/true,
/*gradGateRelax=*/0.6, /*ambient=*/0.50},
// var3高对比——jet 高饱和配色 + 放宽梯度门(relax0.5) + 抬不透明度 + ambient0.45 +
// 暗背景衬高饱和。弱信号也映到鲜明色相,层界面/异常对比最狠。
{"var3", OpacityProfile::kSolid, ColorChoice::kJet,
0.08, 0.42, 0.78, 8.0, 45.0, 30.0, 1.5, {0.04, 0.04, 0.07},
"高对比jet高饱和+放宽梯度门(relax0.5)+抬不透明度+ambient0.45+暗背景衬色,"
"弱信号映鲜明色相、层界面对比最狠",
/*useGradientOpacity=*/true, /*useShade=*/true,
/*gradGateRelax=*/0.5, /*ambient=*/0.45},
// var4灰度增强默认——增强灰度 + 放宽梯度门(relax0.7) + 抬不透明度 + 高 ambient
// + 中性背景。GPR 内部水平层叠/竖纹/基底反射用增强灰度最干净直读,选作交互默认。
{"var4", OpacityProfile::kSolid, ColorChoice::kGrayEnhanced,
0.12, 0.48, 0.80, 8.0, 45.0, 30.0, 1.5, {0.14, 0.15, 0.18},
"灰度增强(默认/综合最佳):增强灰度+放宽梯度门(relax0.7)+抬不透明度"
"(floor0.12/mid0.48/max0.80)+高ambient0.5+中性背景,层叠/竖纹/基底反射干净醒目"
" → 交互窗口默认",
/*useGradientOpacity=*/true, /*useShade=*/true,
/*gradGateRelax=*/0.7, /*ambient=*/0.50},
};
// 交互窗口(无 flag 的 view)的默认视觉变体 = var4kGalleryVariants 末项)。
// 交互默认与 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::kBrightSeismic:
return makeBrightSeismicColorScale(vmin, vmax);
case ColorChoice::kGrayEnhanced:
return makeGrayEnhancedColorScale(vmin, vmax);
case ColorChoice::kStructural:
default: return makeStructuralColorScale(vmin, vmax);
}
}
// 按变体的不透明度包络建体属性gallery / 交互默认共用DRY。kSolid 走 V 形实体
// 包络(floor/mid/max)kStructural 走双端斜坡(仅 maxOpacity)。
//
// C4 视觉调优:若变体开了 useGradientOpacity 且传入了实测梯度统计 gs再叠加
// 1) 梯度不透明度(SetGradientOpacity):低梯度(均匀层)→透明、高梯度(界面/异常)→显形,
// 让射线穿透均匀雾、内部界面浮出(阈值按 gs 的 median/p90/p99 标定,不靠猜);
// 2) 光照(useShade → SetShade + Ambient/Diffuse/Specular):层界面带立体明暗。
vtkSmartPointer<vtkVolumeProperty> makeVariantProperty(
const GalleryVariant& v, const geopro::core::Quant& q,
const geopro::core::ColorScale& cs, double vmin, double vmax,
double maxOpacity, const GradStats* gs = nullptr) {
vtkSmartPointer<vtkVolumeProperty> prop;
if (v.profile == OpacityProfile::kSolid) {
prop = makeSolidVolumeProperty(q, cs, vmin, vmax, v.floorOpacity,
v.midOpacity, maxOpacity);
} else {
prop = makeTunedVolumeProperty(q, cs, vmin, vmax, maxOpacity);
}
// 梯度不透明度:均匀层(低梯度)半透/透明,层界面/异常边缘(高梯度)显形。阈值按实测
// 分布,并按 gradGateRelax 松弛:
// - relax=0严格暗版median→0、p90→0.5、p99→0.9,均匀层全透 → 偏暗偏空。
// - relax>0调亮低梯度地板抬到 floorG均匀层保留底不透明、更亮更满阈值
// 整体左移(更早升起),让横向层叠/基底反射等弱结构保留可见。
// 无 gs未测则跳过。
if (v.useGradientOpacity && gs != nullptr && gs->samples > 0) {
const double relax = std::clamp(v.gradGateRelax, 0.0, 1.0);
const double floorG = 0.30 * relax; // 低梯度区不透明度地板(均匀层保留度)
const double midG = 0.5 + 0.25 * relax;
const double hiG = 0.9;
// 阈值左移:松弛越大,门越早从低梯度升起(结构保留越多)。
const double tLo = std::max(1.0, gs->median * (1.0 - 0.7 * relax));
const double tMid = std::max(2.0, gs->p90 * (1.0 - 0.6 * relax));
const double tHi = std::max(3.0, gs->p99 * (1.0 - 0.4 * relax));
vtkNew<vtkPiecewiseFunction> grad;
grad->AddPoint(0.0, floorG);
grad->AddPoint(tLo, floorG);
grad->AddPoint(tMid, midG);
grad->AddPoint(tHi, hiG);
prop->SetGradientOpacity(grad);
}
// 光照ShadeOn + Ambient/Diffuse保留立体明暗Specular 压到 0.05(近乎关)避免
// 旋转时视角相关的高光在体表游走形成「移动白斑」。Ambient 由变体控(别太低否则偏暗)。
if (v.useShade) {
prop->ShadeOn();
prop->SetAmbient(v.ambient);
prop->SetDiffuse(0.7);
prop->SetSpecular(0.05);
prop->SetSpecularPower(10.0);
}
return prop;
}
// ============================================================================
// ③ 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/1X 超 16384 无法整卷成单纹理)→ 取当前视野在 level0 的
// X 子区域(沿线裁一段,使子体各轴 ≤16384重组一张纹理。
// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。
//
// 渲染源 = ViewAdaptiveVolumeSource(C2):每次交互结束 source->update(cam) 用 C1
// selectLod 选层选区 → 从分块存储重组【当前视野区域单图】→ 喂单 SmartVolumeMapper。
// 退掉旧 POC 简化路径viewPickLevel/wholeVolumeLevelFor/viewLocalBrickRange/缓存
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
struct ViewState {
geopro::render::ViewAdaptiveVolumeSource* source = nullptr;
vtkSmartVolumeMapper* mapper = nullptr; // 高清层:单 SmartVolumeMapper叠在底图上
vtkSmartVolumeMapper* baseMapper = nullptr; // 常驻底图层 mapper用于按高清块挖空 cropping
vtkRenderer* ren = nullptr;
vtkCamera* cam = nullptr;
vtkTextActor* fpsText = nullptr;
vtkRenderWindow* rw = nullptr;
double exagg = 8.0;
int lastLevel = -1;
std::string dir; // store 目录:首帧直读 level0 局部段(类 gallery),绕开 LOD 选粗层
// 预建的首帧高清段(level0 沿线中段)cmdView 已为分位标定建好,直接复用喂 mapper,
// 避免在 viewSetupDefaultFrame 内重复读盘。空则该函数再按 dir 直读。
vtkSmartPointer<vtkImageData> seedSegImg;
// 持有当前高清单图引用避免被释放mapper 仅持裸指针)。
vtkSmartPointer<vtkImageData> currentImg;
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
bool inCb = false;
};
// C3-8 底图按高清块挖空:用 vtkVolumeMapper 的 Cropping 在【底图 mapper】上裁掉高清块
// 覆盖的那一块,使任意时刻 = 底图(高清区外) + 高清(高清区内),无空间重叠 → 不再双渲发白。
//
// 裁剪平面用高清单图的【模型坐标包围盒】(GetBounds)。底图与高清两 actor 用同一份
// SetScale(1,1,exagg),且高清单图自带绝对世界 originbuildLocalLevel0Image 沿 X 偏移),
// 与底图同坐标系 → 高清块的模型盒平面直接作底图 cropping 平面即对齐(两层 scale 一致)。
//
// CroppingRegionFlags6 个平面把空间分成 3×3×3=27 区,中心区(盒内)= bit 0x0002000
// (VTK_CROP_SUBVOLUME)。要「渲盒外、挖掉盒内」→ 全 27 区减中心区 = 0x7ffffff & ~0x0002000。
void viewSyncBaseCropping(ViewState* st) {
if (st->baseMapper == nullptr) return;
if (st->currentImg == nullptr) { // 高清未就绪:底图不裁剪,全渲(绝不空白)
st->baseMapper->SetCropping(0);
return;
}
double b[6];
st->currentImg->GetBounds(b); // 模型坐标盒(含绝对 X origin与底图同系
st->baseMapper->SetCroppingRegionPlanes(b[0], b[1], b[2], b[3], b[4], b[5]);
st->baseMapper->SetCroppingRegionFlags(0x7ffffff & ~VTK_CROP_SUBVOLUME);
st->baseMapper->SetCropping(1);
}
// C3-2 非阻塞拉取:把最新已就绪单图喂 mapper若有新结果。不阻塞主线程——
// 后台 builder 没新结果就沿用上一帧(拖动跟手的关键)。返回 1=喂了新图0=无变化。
std::size_t viewPickLatest(ViewState* st) {
auto imgs = st->source->currentImages(); // 内部 takeLatest非阻塞
if (imgs.empty() || imgs[0] == nullptr) return 0; // 无新结果:保留上一帧
if (imgs[0] == st->currentImg) return 0; // 同一张:无需重喂
st->currentImg = imgs[0];
st->lastLevel = st->source->lastLevel();
st->mapper->SetInputData(st->currentImg);
st->mapper->Update();
viewSyncBaseCropping(st); // 高清块换位 → 同步更新底图挖空盒,保持无缝无重叠
return 1;
}
// 单纹理刷新C3-2 异步source->update(cam) 只【提交目标】(非阻塞),随后非阻塞
// 拉一次最新就绪。拖动中主线程不被重组卡住——新纹理由后台备好、下一帧/定时器换上。
// 返回当前喂入的块数1=有就绪单图0=尚无就绪/视锥外)。
std::size_t viewRefreshSingle(ViewState* st) {
st->source->update(st->cam); // 提交目标,立即返回
viewPickLatest(st);
return st->currentImg != nullptr ? 1 : 0;
}
// 阻塞式刷新:提交目标后【轮询到就绪】再返回(带超时)。仅用于 preview/smoke/默认
// 取景这类「需要保证拿到一张图」的离屏/初始化场景——交互路径绝不用此(会卡主线程)。
std::size_t viewRefreshBlocking(ViewState* st, int maxTries = 3000,
int sleepMs = 2) {
st->source->update(st->cam); // 提交目标
for (int i = 0; i < maxTries; ++i) {
if (viewPickLatest(st)) return 1;
if (st->currentImg != nullptr) return 1; // 已有上一就绪且无新结果
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
}
return st->currentImg != nullptr ? 1 : 0;
}
// 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;
}
// C3-2 拖动跟手核心:交互进行中(旋转/缩放每次相机变化)只【提交目标】(非阻塞),
// 绝不在主线程重组——主线程立刻继续响应输入,画面用上一张已就绪纹理(跟手)。
void viewOnInteracting(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewState*>(clientData);
st->source->update(st->cam); // 提交最新视野目标立即返回supersede 旧目标)
}
// C3-2 定时器:周期性非阻塞拉取后台已就绪的新纹理换上 → 拖动中/松手后新 LOD 备好
// 即自然显示,主线程从不被重组卡住。无新结果则什么也不做(不重渲、不抖)。
void viewOnTimer(vtkObject* caller, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewState*>(clientData);
if (st->inCb) return; // 与 fps 探针回调互斥,避免重入
if (viewPickLatest(st)) {
st->ren->ResetCameraClippingRange();
st->rw->Render(); // 仅在确有新纹理时重渲
}
(void)caller;
}
// 默认取景宽度:沿测线取约 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::render::ViewAdaptiveVolumeSource& source = *st->source;
const geopro::data::StoreMeta& m = source.meta();
// 首帧默认取景:先把相机放到沿线中段一个局部窗口(~kViewDefaultLocalBricks 列)
// 正前方近观 → 触发 C1 选 level0 + 视野子区间C2 重组该视野单图。先用「局部段
// 包围盒」ResetCamera 把相机框到该段,再 source->update(cam) 让 C1/C2 选区重组。
const int brick = m.brick;
const int totBricksX = (m.nx + brick - 1) / brick;
const int localBx = std::min(kViewDefaultLocalBricks, totBricksX);
const int bx0 = std::max(0, totBricksX / 2 - localBx / 2); // 沿线中段
// 该局部段世界 X 范围level0brick 列 [bx0, bx0+localBx))。把首帧相机框到这一段
// (而非整卷)是 P3 修复 #2 的关键:相机近观局部段 → C1 selectLod 选 level0 局部子区,
// C2 重组该段单图,framing 该段 → 14×796 截面 + 沿线一段充满视野(类厚 B-scan)。
// 框整卷则 selectLod 选最粗层(整条 45305 细带)、看着空白。
const double segX0 = m.origin[0] + bx0 * brick * m.spacing[0];
const double segX1 =
m.origin[0] + std::min(m.nx, (bx0 + localBx) * brick) * m.spacing[0];
// 段exagg 后)世界尺寸与中心 + 包围球半径。X 取该段宽(非整卷),Y/Z 全幅(薄轴)。
const double wx = std::max(1.0, segX1 - segX0);
const double wy = std::max(1.0, m.ny * m.spacing[1] * st->exagg);
const double wz = std::max(1.0, m.nz * m.spacing[2] * st->exagg);
const double cx = 0.5 * (segX0 + segX1);
const double cy = m.origin[1] + 0.5 * wy;
const double cz = m.origin[2] + 0.5 * wz;
const double radius = 0.5 * std::sqrt(wx * wx + wy * wy + wz * wz);
// 相机从 +Y 看段中心(看进【X-Z 宽面=B-scan 墙】),距离 = 半径/tan(半视角)×余量。
// 段几何 = X≈12.6m 宽 × Y≈1.5m 薄(跨通道) × Z 深(exagg 后高)。exagg 只夸张深度(Z),
// Y 仍真实极薄 → 若从 +X(沿线)看只见薄前缘、近空。改从 +Y 俯看宽 X-Z 面: GPR 水平
// 分层沿 X 铺开、随 Z 叠层,这一面才读得出内部结构。整条概览靠用户滚轮拉远(细带几何必然)。
st->cam = ren->GetActiveCamera();
const double fovY = st->cam->GetViewAngle();
const double halfAngle = 0.5 * fovY * 3.14159265358979 / 180.0;
const double tanH = std::max(1e-3, std::tan(halfAngle));
const double dist = radius / tanH * 1.4; // 1.4:留余量含 aspect/边缘
st->cam->SetFocalPoint(cx, cy, cz);
st->cam->SetPosition(cx, cy + dist, cz); // +Y 视点 → 正对 X-Z 宽面
st->cam->SetViewUp(0, 0, 1); // Z 朝上(深度向下)
ren->ResetCameraClippingRange();
// 首帧高清段直读(P3 修复 #2 核心):异步 LOD 源在「框一段」的视距下仍会选最粗层
// (整条 45305 细带 → 看着近黑),不可取。改为【直接从 store 读 level0 沿线中段子体】
// (与 gallery 的 buildLocalLevel0Image 同一直读路径,非 LOD 算法),喂高清 mapper —
// 保证首帧就是「全分辨率一段」的清晰块体。后续交互仍由异步源接管(用户拉远/拖动按
// 视距正常选层)。退化(读不到段)再回退异步阻塞刷新。
std::size_t blocks = 0;
{
vtkSmartPointer<vtkImageData> locImg = st->seedSegImg; // cmdView 预建,优先复用
if (locImg == nullptr && !st->dir.empty()) { // 退化:按 dir 直读
geopro::data::ChunkedVolumeStore store(st->dir);
locImg = buildLocalLevel0Image(store, m, bx0, localBx);
}
if (locImg != nullptr) {
st->currentImg = locImg;
st->lastLevel = 0;
st->mapper->SetInputData(locImg);
st->mapper->Update();
viewSyncBaseCropping(st); // 底图按该段挖空,无缝叠加
blocks = 1;
}
}
if (blocks == 0) blocks = viewRefreshBlocking(st); // 退化:回退异步源
// 框住【局部段】:无参 ResetCamera 会按场景全部 actor(含常驻整卷底图,45305 长)的
// 包围盒框 → 整条细带、截面填不满 → 看着空白。改为只框高清段(currentImg=level0 沿
// 线中段子体)的包围盒,使 14×796 截面+沿线一段充满视野(类厚 B-scan);整条概览靠用户
// 滚轮拉远(细带是 1:34 几何必然,非 bug)。Z 轴按 actor 的 SetScale(1,1,exagg) 同步夸张。
if (st->currentImg != nullptr) {
double b[6];
st->currentImg->GetBounds(b); // 高清段模型坐标盒(含绝对 X origin)
b[4] *= st->exagg; // Z 下界随深度夸张
b[5] *= st->exagg; // Z 上界随深度夸张
ren->ResetCamera(b); // 只框该段 → 段充满画面
} else {
ren->ResetCamera(); // 退化(无高清段):回退全场景框
}
// 取景角度:默认相机已置 +Y 正对 X-Z 宽面。var4 的 El45/Az30 是为 gallery 的 +X /
// Y 也夸张几何调的,套到这里(+Y 视、Y 真实极薄)会让宽面强烈斜退、大片黑。改用为本
// +Y B-scan 几何调的小角度(El/Az 各 ~15-18°)留 3D 立体感而宽面仍基本正对,Zoom 拉足
// 让 X-Z 面填满。仅默认/交互/preview 取景,不动 var4 gallery 参数。
constexpr double kDefaultFrameElevation = 18.0; // 轻俯,见顶面薄边显层叠
constexpr double kDefaultFrameAzimuth = 15.0; // 轻偏,宽面仍基本正对
constexpr double kDefaultFrameZoom = 1.1; // ResetCamera 已贴合该段,只略收边距
st->cam = ren->GetActiveCamera();
st->cam->Elevation(kDefaultFrameElevation);
st->cam->Azimuth(kDefaultFrameAzimuth);
st->cam->Zoom(kDefaultFrameZoom);
ren->ResetCameraClippingRange();
return blocks;
}
// 渲一组画廊变体并存 PNG报告 结构像素 / 平均亮度 / fps。返回 0=OK。
// shotDirOverride 非空 → PNG 存到该目录P4 让 gallery 出图落在 store 同目录,便于对照)。
int runGalleryVariant(const std::string& dir, const GalleryVariant& v,
int frames, const std::string& shotDirOverride = "") {
const int winW = 1280, winH = 800;
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]);
rw->AddRenderer(ren);
// 局部段(沿线中段,同 viewSetupDefaultFrame 的取段法)。先建体 → 实测梯度分布
// 用于 C4 梯度不透明度标定(仅开了 useGradientOpacity 的变体需要)。
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);
// P3 传函分位标定:色阶/不透明度端点按该局部段【实际值 2%/98% 分位】裁离群。
double vmin = m.vminPhys, vmax = m.vmaxPhys;
{
const ScalarPercentiles pc =
sampleScalarPercentiles(locImg.Get(), m.quant, 0.02, 0.98);
if (pc.samples > 0) {
vmin = pc.lo;
vmax = pc.hi;
std::cout << "[gallery " << v.name << "] 传函分位标定(样本 " << pc.samples
<< "): 2%=" << vmin << " 98%=" << vmax << " (全域 ["
<< m.vminPhys << ", " << m.vmaxPhys << "])\n";
}
}
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
GradStats gs;
if (v.useGradientOpacity) {
gs = sampleGradientMagnitude(locImg.Get());
std::cout << "[gallery " << v.name << "] 梯度幅值分布(量化域,样本 " << gs.samples
<< "): median=" << gs.median << " p90=" << gs.p90
<< " p99=" << gs.p99 << " max=" << gs.mx << "\n";
}
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
v, m.quant, cs, vmin, vmax, v.maxOpacity,
v.useGradientOpacity ? &gs : nullptr);
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);
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 =
shotDirOverride.empty()
? fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots"
: fs::path(shotDirOverride);
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 组变体。shotDir 空 → 默认 docs/.../poc-lod-shots
// 否则存到 shotDirP4默认传 store 目录4 张图落在 tmp/line001_proc
int cmdViewGallery(const std::string& dir, int frames,
const std::string& shotDir = "") {
std::cout << "[view --gallery] storeDir=" << dir << " frames=" << frames
<< " shotDir=" << (shotDir.empty() ? "(默认)" : shotDir)
<< "\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, shotDir) != 0) rc = 1;
}
const std::string outDesc =
shotDir.empty() ? "docs/superpowers/plans/poc-lod-shots" : shotDir;
std::cout << "\n[view --gallery] 完成4 张图存于 " << outDesc
<< "/view-var{1..4}.png\n";
return rc;
}
// ============================================================================
// view-all全部独立体按精确 CGCS2000 坐标/朝向摆进同一 3D 场景一起渲测区全貌P7→P9
// ============================================================================
//
// 对应客户端「选多个 ds 一起生成三维」:每条线是独立密实 coarse 体(线局部坐标 X=沿测线、
// Y=通道横向、Z=深度origin≈0本命令把它们按各自 .gps 真实位置/航向摆进同一世界框。
//
// P9 升级:起点投影从 lonLatToLocalM简化等距投影换成 CoordinateTransform::wgs84ToCgcs2000
// CGCS2000 高斯-克吕格 3°带与 P8 测绘级桥接同口径),公共带号取首条线首点经度推断,
// 全线共用同一带号 → 同一参考系;公共世界原点 = 全体 CGCS2000 最小东/北。
// - 每条线刚体变换:平移到该线 .gps 起点 CGCS2000 局部米 + 绕竖直 Z 轴转该线航向角
// 起→止主方向CGCS2000 系),使体局部 X沿测线对齐真实航向、Y横向垂直、Z 竖直;
// - 深度 Z 用 exagg 夸张(只 Z
// - 每条体加载为一张整卷 vtkImageData密实体小按 --level 选层整体上纹理,不走 LOD
// 套上该线 vtkTransform全加进同一 renderer 一起渲。
// 传函/配色用 P4 默认醒目版var4增强灰度 + 实体包络 + 梯度门 + 光照),逐体按 2/98 分位标定。
//
// --preview 离屏出俯视(top) + 斜视(oblique)两张图展示 20 条并排成测区;否则开真窗口可转可缩。
// 一条线在世界中的摆放(刚体变换 + 该线的视野自适应引擎源)。
//
// P11每条线不再整卷加载固定层而是各持一个 ViewAdaptiveVolumeSourceLOD+视锥
// 裁剪+异步重组引擎,与单条 view 完全同款)。引擎 exagg=1.0(不烘几何),垂向夸张/
// 航向/平移全由世界变换 T 承担(与单条 view 把 exagg 放 actor SetScale 同理)。每帧
// 把世界相机逆变换到该线局部帧喂引擎 → selectLod 选层选区(视锥外→引擎不提交)。
struct PlacedSource {
std::string name; // 明星路_NNN
std::unique_ptr<geopro::render::ViewAdaptiveVolumeSource> source;
geopro::data::StoreMeta meta; // 量化/几何
double startX = 0, startY = 0; // 起点局部米(相对公共原点)
double headingDeg = 0; // 航向角(度,相对 +X 东向)
double spreadX = 0, spreadY = 0; // 可选横向铺开偏移
vtkSmartPointer<vtkTransform> world; // TScale(1,1,exagg)→RotateZ→Translate
vtkSmartPointer<vtkTransform> worldInv; // T⁻¹相机逆变换到局部帧
vtkSmartPointer<vtkVolumeProperty> prop; // 逐线 2/98 分位标定的传函(底图+高清共用)
vtkSmartPointer<vtkVolume> baseVolume; // 常驻粗底图(永在场,套 T
vtkSmartPointer<vtkSmartVolumeMapper> baseMapper;
vtkSmartPointer<vtkVolume> hiresVolume; // 高清叠加(就绪后局部覆盖,套 T
vtkSmartPointer<vtkSmartVolumeMapper> hiresMapper;
vtkSmartPointer<vtkImageData> currentImg; // 持当前高清单图引用mapper 仅持裸指针)
double worldBounds[6] = {0, 0, 0, 0, 0, 0}; // 该线(含 T+底图盒)的世界 AABB视锥裁剪用
bool culled = false; // 本帧是否被视锥裁掉(两层皆隐 → 真跳过)
};
// 由 起点+航向+Z 夸张 → 世界刚体变换 T。
// 合成顺序VTK PostMultiply先加先施于点Scale(1,1,exagg)→RotateZ→Translate
// 即点先按 exagg 拉伸 Z局部深度再绕竖直轴转真实航向再平移到世界起点。
vtkSmartPointer<vtkTransform> makeLineTransform(double startX, double startY,
double headingDeg,
double spreadX, double spreadY,
double exagg) {
auto xf = vtkSmartPointer<vtkTransform>::New();
xf->PostMultiply();
xf->Scale(1.0, 1.0, exagg);
xf->RotateZ(headingDeg);
xf->Translate(startX + spreadX, startY + spreadY, 0.0);
return xf;
}
// 逐线传函:从该线常驻底图(整卷代表)实测 2/98 分位标定色阶/不透明度端点 + 梯度门,
// 与单条 view 的传函标定同口径(底图非空恒可标定;退化回退全量化域)。
vtkSmartPointer<vtkVolumeProperty> buildLineProperty(
const geopro::data::StoreMeta& m, vtkImageData* basis) {
const GalleryVariant& v = kViewDefaultVariant; // P4 默认醒目版var4
double vmin = m.vminPhys, vmax = m.vmaxPhys;
if (basis != nullptr) {
const ScalarPercentiles pc =
sampleScalarPercentiles(basis, m.quant, 0.02, 0.98);
if (pc.samples > 0) {
vmin = pc.lo;
vmax = pc.hi;
}
}
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
GradStats gs;
if (v.useGradientOpacity && basis != nullptr) gs = sampleGradientMagnitude(basis);
return makeVariantProperty(v, m.quant, cs, vmin, vmax, v.maxOpacity,
(v.useGradientOpacity && gs.samples > 0) ? &gs
: nullptr);
}
// 把世界相机参数逆变换到某线局部帧T⁻¹pos/focal 是点含平移逆up 是方向
// 仅旋转逆TransformVector 不含平移)。再调引擎 updateView 选层选区(视锥外→引擎
// 内部 selectLod 判 empty → 不提交,保留上一就绪/无图)。
void viewAllSubmitOneLine(PlacedSource& ps, vtkCamera* worldCam,
double aspect, int viewportH) {
if (ps.culled || worldCam == nullptr) return;
double wp[3], wf[3], wu[3];
worldCam->GetPosition(wp);
worldCam->GetFocalPoint(wf);
worldCam->GetViewUp(wu);
geopro::render::CameraView c{};
ps.worldInv->TransformPoint(wp, c.pos);
ps.worldInv->TransformPoint(wf, c.focal);
ps.worldInv->TransformVector(wu, c.up);
c.fovYDeg = worldCam->GetViewAngle();
c.aspect = aspect;
c.viewportH = viewportH;
// 引擎 exagg=1.0:局部帧几何无夸张(夸张在 T 里),故喂引擎 volumeView 默认即可。
ps.source->setAspect(aspect);
ps.source->setViewportHeight(viewportH);
ps.source->updateView(c, geopro::render::VolumeView{
ps.meta.nx, ps.meta.ny, ps.meta.nz,
ps.meta.brick, ps.source->levelCount(),
{ps.meta.origin[0], ps.meta.origin[1],
ps.meta.origin[2]},
{ps.meta.spacing[0], ps.meta.spacing[1],
ps.meta.spacing[2]},
1.0});
}
// 非阻塞拉取该线后台已就绪的高清单图,喂高清 mapper无新结果→沿用上一帧
// 返回 1=换上新图。
int viewAllPickOneLine(PlacedSource& ps) {
if (ps.culled) return 0;
auto imgs = ps.source->currentImages(); // 内部 takeLatest非阻塞
if (imgs.empty() || imgs[0] == nullptr) return 0;
if (imgs[0] == ps.currentImg) return 0;
ps.currentImg = imgs[0];
ps.hiresMapper->SetInputData(ps.currentImg);
ps.hiresMapper->Update();
ps.hiresVolume->SetVisibility(1);
return 1;
}
// 视锥裁剪(#2把该线世界 AABB 与相机 6 个视锥面比对,整盒在任一面外侧 → 裁掉
// base+hires 两层皆隐 → 该线本帧完全不渲,省下解压/重组/ray-march。盒 8 角全在
// 某面负半空间才裁(保守,绝不误裁部分可见的线)。
bool aabbOutsideFrustum(const double b[6], const double planes[24]) {
for (int p = 0; p < 6; ++p) {
const double a = planes[p * 4 + 0], bb = planes[p * 4 + 1],
cc = planes[p * 4 + 2], dd = planes[p * 4 + 3];
bool allOut = true;
for (int cx = 0; cx < 2 && allOut; ++cx)
for (int cy = 0; cy < 2 && allOut; ++cy)
for (int cz = 0; cz < 2 && allOut; ++cz) {
const double x = b[cx], y = b[2 + cy], z = b[4 + cz];
if (a * x + bb * y + cc * z + dd >= 0.0) allOut = false; // 该角在面内
}
if (allOut) return true; // 全 8 角在该面外 → 整盒在视锥外
}
return false;
}
// view-all 每帧驱动共享状态(挂 interactor 回调)。
struct ViewAllState {
std::vector<PlacedSource>* lines = nullptr;
vtkRenderer* ren = nullptr;
vtkCamera* cam = nullptr;
vtkRenderWindow* rw = nullptr;
vtkTextActor* fpsText = nullptr;
double aspect = 1400.0 / 900.0;
int viewportH = 900;
bool inCb = false;
};
// 重算各线视锥可见性(裁屏外线)+ 对可见线提交引擎目标非阻塞。culled 线两层皆隐。
void viewAllRefreshFrustum(ViewAllState* st) {
double planes[24];
st->cam->GetFrustumPlanes(st->aspect, planes);
for (PlacedSource& ps : *st->lines) {
const bool outside = aabbOutsideFrustum(ps.worldBounds, planes);
ps.culled = outside;
ps.baseVolume->SetVisibility(outside ? 0 : 1);
if (outside) {
ps.hiresVolume->SetVisibility(0);
} else {
// 可见:提交引擎目标(局部帧),高清可见性由 pick 决定(有就绪图才显)。
viewAllSubmitOneLine(ps, st->cam, st->aspect, st->viewportH);
ps.hiresVolume->SetVisibility(ps.currentImg != nullptr ? 1 : 0);
}
}
}
// 交互进行中:只重算视锥可见性 + 提交目标(非阻塞),主线程立即继续响应输入。
void viewAllOnInteracting(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewAllState*>(clientData);
viewAllRefreshFrustum(st);
}
// 定时器:非阻塞拉取各可见线后台已就绪的新高清纹理换上 → 有新图才重渲。
void viewAllOnTimer(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewAllState*>(clientData);
if (st->inCb) return;
int changed = 0;
for (PlacedSource& ps : *st->lines) changed += viewAllPickOneLine(ps);
if (changed > 0) {
st->ren->ResetCameraClippingRange();
st->rw->Render();
}
}
// 交互结束:重算视锥 + 提交 + 拉取 + 刷新 fps仅松手触发一次
void viewAllOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewAllState*>(clientData);
if (st->inCb) return;
st->inCb = true;
viewAllRefreshFrustum(st);
int culledN = 0, visN = 0;
for (PlacedSource& ps : *st->lines) {
viewAllPickOneLine(ps);
if (ps.culled) ++culledN; else ++visN;
}
st->ren->ResetCameraClippingRange();
constexpr int kFpsProbeFrames = 3;
Stopwatch swR;
for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render();
const double fps = swR.elapsedMs() > 0
? 1000.0 * kFpsProbeFrames / swR.elapsedMs()
: 0.0;
char buf[256];
std::snprintf(buf, sizeof(buf),
"fps: %.1f | visible lines: %d | culled: %d", fps, visN,
culledN);
if (st->fpsText) st->fpsText->SetInput(buf);
st->rw->Render();
st->inCb = false;
}
int cmdViewAll(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.size() < 2) {
std::cerr << "用法: gpr_poc view-all <storesDir> <gpsDir> [--preview] "
"[--exagg 8] [--level 1] [--spread M] [--shotDir <dir>]\n"
"例: gpr_poc view-all tmp/lines_all D:/Downloads/明星路 "
"--preview --exagg 8\n";
return 2;
}
const std::string storesDir = a.positional[0];
const std::string gpsDir = a.positional[1];
const double exagg = std::stod(a.get("exagg", "8"));
// --level每条体取金字塔哪一层整渲。默认 1L1 ~5664×7×398/体20 体内存/纹理可控)。
const int level = std::stoi(a.get("level", "1"));
// --spread M每条线沿垂直自身航向方向额外横向偏移 index*M 米,把「同一条路重复多趟、
// 真实位置高度重叠」的体在视觉上铺开成可分辨的并排测区M=0=纯真实位置,默认 0
const double spread = std::stod(a.get("spread", "0"));
const bool preview = a.kv.count("preview") > 0;
const std::string shotDir = a.get("shotDir", storesDir);
std::cout << "[view-all] storesDir=" << storesDir << " gpsDir=" << gpsDir
<< " exagg=" << exagg << " level=" << level << " spread=" << spread
<< (preview ? " [PREVIEW 离屏俯视+斜视出图]" : " [真窗口可交互]")
<< "\n";
// 离屏闸门不可渲机不产假结果preview/真窗口都需 GL
std::cout << "[view-all] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[view-all] 闸门失败,中止。\n";
return 1;
}
// 1) 发现 storesDir 下所有 明星路_NNN 体目录(含 meta.json按名排序。
std::vector<std::string> storeNames;
for (const auto& e : fs::directory_iterator(storesDir)) {
if (!e.is_directory()) continue;
if (!fs::exists(e.path() / "meta.json")) continue;
storeNames.push_back(e.path().filename().string());
}
std::sort(storeNames.begin(), storeNames.end());
std::cout << "[view-all] 发现体目录数=" << storeNames.size() << "\n";
if (storeNames.empty()) {
std::cerr << "[view-all] 错误: storesDir 下未发现任何含 meta.json 的体目录\n";
return 1;
}
// 2) 各线 .gps按目录名末段 _NNN 匹配),先全解析定公共世界原点(最小经纬)。
struct LineGps {
std::string name;
std::string num; // NNN
std::string gpsPath;
geopro::io::gpr::GpsTrack track;
};
std::vector<LineGps> gpsList;
// P9CGCS2000 公共参考。带号由首条可用线首点经度推断(3°带),全线共用同一带号 →
// 同一高斯-克吕格平面参考系;公共原点取全体投影后最小东/北(数值减到较小,避免大坐标精度损失)。
constexpr double kDeg2Rad = 3.14159265358979323846 / 180.0;
int cgcsZone = 0;
bool zoneSet = false;
double minEast = std::numeric_limits<double>::infinity();
double minNorth = std::numeric_limits<double>::infinity();
for (const std::string& nm : storeNames) {
const std::size_t us = nm.find_last_of('_');
if (us == std::string::npos) {
std::cerr << "[view-all] 跳过 " << nm << ":名无 _NNN 后缀,无法配 .gps\n";
continue;
}
const std::string num = nm.substr(us + 1);
// .gps匹配 "*_<num>.gps"。
std::string gpsPath;
for (const auto& e : fs::directory_iterator(gpsDir)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".gps") continue;
const std::string stem = e.path().stem().string();
const std::size_t s2 = stem.find_last_of('_');
if (s2 != std::string::npos && stem.substr(s2 + 1) == num) {
gpsPath = e.path().string();
break;
}
}
if (gpsPath.empty()) {
std::cerr << "[view-all] 跳过 " << nm << ":缺 .gpsgpsDir 无 *_" << num
<< ".gps\n";
continue;
}
geopro::io::gpr::GpsTrack tr = geopro::io::gpr::parseGps(gpsPath);
if (tr.pts.size() < 2) {
std::cerr << "[view-all] 跳过 " << nm << ".gps 轨迹点 <2无法定位/航向)\n";
continue;
}
// 带号:首条可用线首点经度推断(与 P8 Gpr3dvSurveyVolumeBridge 同口径),全线共用。
if (!zoneSet) {
cgcsZone = static_cast<int>(std::lround(tr.pts.front().lon / 3.0));
zoneSet = true;
}
for (const auto& p : tr.pts) {
double cx = 0.0, cy = 0.0; // cx=北(northing), cy=东(easting含带号)
CoordinateTransform::wgs84ToCgcs2000(p.lat * kDeg2Rad, p.lon * kDeg2Rad,
cgcsZone, cx, cy);
minEast = std::min(minEast, cy);
minNorth = std::min(minNorth, cx);
}
gpsList.push_back({nm, num, gpsPath, std::move(tr)});
}
if (gpsList.empty()) {
std::cerr << "[view-all] 错误: 无任何线同时具备体与可用 .gps\n";
return 1;
}
std::cout << "[view-all] CGCS2000 带号=" << cgcsZone
<< " 公共世界原点(最小东/北)=(" << std::fixed << minEast << ", "
<< minNorth << ")" << std::defaultfloat << " (共 " << gpsList.size()
<< " 线参与)\n";
if (spread <= 0.0) {
std::cout << "[view-all] 提示: --spread=0 用纯真实 GPS 位置;本工区为同一条路重复多趟、"
"横向仅约数十米,真实位置下多趟高度重叠会叠成一条带。"
"如需把各趟铺开成可分辨的并排测区,加 --spread 60。\n";
}
// 3) 逐线:算起点局部米 + 航向 + 建 ViewAdaptiveVolumeSource 引擎 → PlacedSource。
// P11不再整卷固定层加载撞 GL 16384 纹理墙),改为每线一个视野自适应引擎,
// 引擎恒产 ≤16384 单纹理LOD+视野选区exagg/航向/平移由世界变换 T 承担。
const int winW = 1400, winH = 900;
const double aspect = static_cast<double>(winW) / winH;
std::vector<PlacedSource> lines;
int lineIdx = 0;
for (const LineGps& lg : gpsList) {
// 轨迹 → CGCS2000 局部米投影到公共带号后减公共原点。XY 约定 x=东、y=北。
std::vector<geopro::io::gpr::XY> trackM;
trackM.reserve(lg.track.pts.size());
for (const auto& p : lg.track.pts) {
double cx = 0.0, cy = 0.0; // cx=北, cy=东
CoordinateTransform::wgs84ToCgcs2000(p.lat * kDeg2Rad, p.lon * kDeg2Rad,
cgcsZone, cx, cy);
trackM.push_back(geopro::io::gpr::XY{cy - minEast, cx - minNorth});
}
const geopro::io::gpr::XY& start = trackM.front();
const geopro::io::gpr::XY& end = trackM.back();
const double hx = end.x - start.x, hy = end.y - start.y;
const double headingDeg = std::atan2(hy, hx) * 180.0 / 3.14159265358979323846;
const double hlen = std::hypot(hx, hy);
double spreadX = 0, spreadY = 0;
if (spread > 0.0 && hlen > 0.0) {
const double off = lineIdx * spread;
spreadX = (-hy / hlen) * off;
spreadY = (hx / hlen) * off;
}
++lineIdx;
const std::string storePath = (fs::path(storesDir) / lg.name).string();
PlacedSource ps;
ps.name = lg.name;
// 引擎 exagg=1.0:垂向夸张放进世界变换 T与单条 view 把 exagg 放 actor 同理)。
ps.source = std::make_unique<geopro::render::ViewAdaptiveVolumeSource>(
storePath, /*exagg=*/1.0);
ps.source->setAspect(aspect);
ps.source->setViewportHeight(winH);
ps.meta = ps.source->meta();
ps.startX = start.x;
ps.startY = start.y;
ps.headingDeg = headingDeg;
ps.spreadX = spreadX;
ps.spreadY = spreadY;
// 世界变换 T + 逆变换(相机逆变换到局部帧)。
ps.world = makeLineTransform(start.x, start.y, headingDeg, spreadX, spreadY,
exagg);
ps.worldInv = vtkSmartPointer<vtkTransform>::New();
ps.worldInv->DeepCopy(ps.world);
ps.worldInv->Inverse();
// 逐线传函(从常驻底图标定)+ 底图层 + 高清层,两层皆套世界变换 T。
ps.prop = buildLineProperty(ps.meta, ps.source->baseImage());
ps.baseMapper = vtkSmartPointer<vtkSmartVolumeMapper>::New();
ps.baseMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
ps.baseMapper->SetAutoAdjustSampleDistances(1);
ps.baseMapper->SetInteractiveAdjustSampleDistances(1);
ps.baseVolume = vtkSmartPointer<vtkVolume>::New();
if (ps.source->baseImage() != nullptr) {
ps.baseMapper->SetInputData(ps.source->baseImage());
ps.baseMapper->Update();
}
ps.baseVolume->SetMapper(ps.baseMapper);
ps.baseVolume->SetProperty(ps.prop);
ps.baseVolume->SetUserTransform(ps.world);
ps.hiresMapper = vtkSmartPointer<vtkSmartVolumeMapper>::New();
ps.hiresMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
// #1 拖动降采样:交互式采样距离自适应(拖动→大步长降采样跟手,松手→全质量)。
ps.hiresMapper->SetAutoAdjustSampleDistances(1);
ps.hiresMapper->SetInteractiveAdjustSampleDistances(1);
ps.hiresVolume = vtkSmartPointer<vtkVolume>::New();
ps.hiresVolume->SetMapper(ps.hiresMapper);
ps.hiresVolume->SetProperty(ps.prop);
ps.hiresVolume->SetUserTransform(ps.world);
ps.hiresVolume->SetVisibility(0); // 无就绪高清前不显(底图兜底)
// 该线世界 AABB底图模型盒经 T 变换的 8 角包络)→ 视锥裁剪用。
if (ps.source->baseImage() != nullptr) {
double mb[6];
ps.source->baseImage()->GetBounds(mb);
double lo[3] = {std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::infinity()};
double hi[3] = {-std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity()};
for (int cx = 0; cx < 2; ++cx)
for (int cy = 0; cy < 2; ++cy)
for (int cz = 0; cz < 2; ++cz) {
double in[3] = {mb[cx], mb[2 + cy], mb[4 + cz]}, out[3];
ps.world->TransformPoint(in, out);
for (int d = 0; d < 3; ++d) {
lo[d] = std::min(lo[d], out[d]);
hi[d] = std::max(hi[d], out[d]);
}
}
ps.worldBounds[0] = lo[0]; ps.worldBounds[1] = hi[0];
ps.worldBounds[2] = lo[1]; ps.worldBounds[3] = hi[1];
ps.worldBounds[4] = lo[2]; ps.worldBounds[5] = hi[2];
}
int bd[3] = {0, 0, 0};
if (ps.source->baseImage()) ps.source->baseImage()->GetDimensions(bd);
std::cout << "[view-all] " << lg.name << " 引擎底图 level="
<< ps.source->baseLevel() << " 底图维度=" << bd[0] << "x" << bd[1]
<< "x" << bd[2] << " 起点局部米=(" << start.x << ", " << start.y
<< ") 航向=" << headingDeg << "°\n";
lines.push_back(std::move(ps));
}
std::cout << "[view-all] 加载并定位线数=" << lines.size()
<< "(每线一个 ViewAdaptiveVolumeSource 引擎)\n";
// 4) 同一 renderer 加全部线的底图层 + 高清层。
auto rw = preview ? makeOffscreenWindow(winW, winH)
: vtkSmartPointer<vtkRenderWindow>::New();
if (!preview) rw->SetSize(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(kViewDefaultVariant.bg[0], kViewDefaultVariant.bg[1],
kViewDefaultVariant.bg[2]);
rw->AddRenderer(ren);
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
for (PlacedSource& ps : lines) {
ren->AddVolume(ps.baseVolume); // 先加底图 → 底层常渲
ren->AddVolume(ps.hiresVolume); // 后加高清 → 叠在底图上
}
std::cout << "[view-all] 已加入场景线数=" << lines.size()
<< "(底图常驻 + 高清叠加,各 ≤16384 单纹理,绝不撞 GL 纹理墙)\n";
ViewAllState st;
st.lines = &lines;
st.ren = ren.Get();
st.rw = rw.Get();
st.aspect = aspect;
st.viewportH = winH;
// 首帧ResetCamera 框全测区 → 概览(各线选粗 LOD 底图)。提交引擎目标 + 阻塞拉首图。
ren->ResetCamera();
st.cam = ren->GetActiveCamera();
viewAllRefreshFrustum(&st);
// 概览阻塞拉一次(保证首帧高清就绪,离屏/真窗口都从有图起步)。
for (PlacedSource& ps : lines) {
if (ps.culled) continue;
for (int tries = 0; tries < 200; ++tries) {
if (viewAllPickOneLine(ps)) break;
if (ps.currentImg != nullptr) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
}
rw->Render();
if (capWin->textureError()) {
std::cerr << "[view-all] 警告: 仍检测到 3D 纹理维度错误(不应发生,引擎契约 "
"≤16384\n";
}
if (preview) {
fs::create_directories(shotDir);
// (A) 概览俯视(top):相机沿 -Z 俯看 XY 平面(看 20 条在测区平面铺开)。
{
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
double fp[3];
cam->GetFocalPoint(fp);
const double* b = ren->ComputeVisiblePropBounds();
const double span = std::max({b[1] - b[0], b[3] - b[2], b[5] - b[4]});
cam->SetPosition(fp[0], fp[1], fp[2] + span * 2.0);
cam->SetFocalPoint(fp[0], fp[1], fp[2]);
cam->SetViewUp(0, 1, 0);
ren->ResetCameraClippingRange();
st.cam = cam;
viewAllRefreshFrustum(&st);
for (PlacedSource& ps : lines)
for (int t = 0; t < 100 && !ps.culled && ps.currentImg == nullptr; ++t) {
if (viewAllPickOneLine(ps)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
rw->Render();
savePng(rw.Get(), (fs::path(shotDir) / "view-all-top.png").string());
std::cout << "[view-all] 俯视图存: "
<< (fs::path(shotDir) / "view-all-top.png").string() << "\n";
}
// (B) 概览斜视(oblique)var4 取景 + 概览 fps全部可见、各选粗 LOD
int ovVisible = 0, ovCulled = 0;
double fpsOverview = 0.0;
{
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(35.0);
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
st.cam = cam;
viewAllRefreshFrustum(&st);
for (PlacedSource& ps : lines) {
for (int t = 0; t < 100 && !ps.culled && ps.currentImg == nullptr; ++t) {
if (viewAllPickOneLine(ps)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
if (ps.culled) ++ovCulled; else ++ovVisible;
}
rw->Render();
savePng(rw.Get(),
(fs::path(shotDir) / "view-all-oblique.png").string());
std::cout << "[view-all] 斜视图存: "
<< (fs::path(shotDir) / "view-all-oblique.png").string() << "\n";
rw->SetDesiredUpdateRate(15.0); // 拖动态:降采样
rw->Render();
Stopwatch sw;
const int frames = 60;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
rw->Render();
}
fpsOverview = sw.elapsedMs() > 0 ? frames * 1000.0 / sw.elapsedMs() : 0.0;
}
// (C) 拉近一段:把焦点对准测区中段一条线的世界中心、近距正对该段 → 大部分线出
// 视锥被裁掉,当前线只渲可见段的合适 LOD。报拉近 fps与概览对比
// fps 探针用小幅 azimuth 摆动±6°模拟拉近态轻微转动观察而非整周 orbit
// ——整周 orbit 会把全测区转回视野使裁剪失效,不代表真实"拉近看一段"的交互。
int nearVisible = 0, nearCulled = 0;
double fpsNear = 0.0;
{
// 选中段一条线(取参与线的中位)作拉近目标,焦点置其世界 AABB 中心。
const PlacedSource& tgt = lines[lines.size() / 2];
const double cx = 0.5 * (tgt.worldBounds[0] + tgt.worldBounds[1]);
const double cy = 0.5 * (tgt.worldBounds[2] + tgt.worldBounds[3]);
const double cz = 0.5 * (tgt.worldBounds[4] + tgt.worldBounds[5]);
// 该段(含 exagg 后)世界跨度 → 近距视距(贴该段,使其充满视野、邻线出视锥)。
const double segLen = std::max({tgt.worldBounds[1] - tgt.worldBounds[0],
tgt.worldBounds[3] - tgt.worldBounds[2],
tgt.worldBounds[5] - tgt.worldBounds[4]});
vtkCamera* cam = ren->GetActiveCamera();
const double fovY = cam->GetViewAngle();
const double tanH =
std::max(1e-3, std::tan(0.5 * fovY * 3.14159265358979 / 180.0));
// 近距:只贴该段横截面尺度(取较短轴 ~该段 Y/Z 跨度的 1/4使邻行线出视锥。
const double shortSpan = std::max(
1.0, std::min(tgt.worldBounds[3] - tgt.worldBounds[2],
tgt.worldBounds[5] - tgt.worldBounds[4]));
const double dist = (0.25 * shortSpan) / tanH * 1.4;
cam->SetFocalPoint(cx, cy, cz);
cam->SetPosition(cx, cy + dist, cz + 0.3 * dist); // 斜上方近观该段
cam->SetViewUp(0, 0, 1);
ren->ResetCameraClippingRange();
st.cam = cam;
(void)segLen;
viewAllRefreshFrustum(&st);
for (PlacedSource& ps : lines) {
for (int t = 0; t < 120 && !ps.culled && ps.currentImg == nullptr; ++t) {
if (viewAllPickOneLine(ps)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
if (ps.culled) ++nearCulled; else ++nearVisible;
}
rw->Render();
savePng(rw.Get(), (fs::path(shotDir) / "view-all-near.png").string());
std::cout << "[view-all] 拉近图存: "
<< (fs::path(shotDir) / "view-all-near.png").string() << "\n";
rw->SetDesiredUpdateRate(15.0); // 拖动态:降采样
rw->Render();
Stopwatch sw;
const int frames = 60;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(f % 2 == 0 ? 0.4 : -0.4); // 小幅摆动(不转回全测区)
viewAllRefreshFrustum(&st); // 拖动中持续重算视锥裁剪
rw->Render();
}
fpsNear = sw.elapsedMs() > 0 ? frames * 1000.0 / sw.elapsedMs() : 0.0;
rw->SetDesiredUpdateRate(0.5);
}
const vtkIdType nb = countNonBlackPixels(rw.Get(), winW, winH);
const bool texErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
std::cout << "\n=== view-all --preview 测区全貌(多体共场景,引擎 LOD+视锥裁剪)===\n";
std::cout << "参与线数 : " << lines.size() << "\n";
std::cout << "exagg(Z) : " << exagg << "\n";
std::cout << "spread(横向铺开) : " << spread << " m (0=纯真实位置)\n";
std::cout << "纹理维度错误 : " << (texErr ? "是(!!)" : "否(引擎契约 ≤16384")
<< "\n";
std::cout << "概览可见/裁剪线 : " << ovVisible << " / " << ovCulled << "\n";
std::cout << "概览 fps(60帧旋) : " << fpsOverview << "\n";
std::cout << "拉近可见/裁剪线 : " << nearVisible << " / " << nearCulled
<< " (视锥裁剪生效:裁掉屏外线)\n";
std::cout << "拉近 fps(60帧旋) : " << fpsNear << "\n";
const double speedup = fpsOverview > 0 ? fpsNear / fpsOverview : 0.0;
std::cout << "拉近/概览 fps 比 : " << speedup << "x\n";
std::cout << "末帧非黑像素 : " << nb << " / " << (winW * winH) << "\n";
std::cout << "俯视图 : "
<< (fs::path(shotDir) / "view-all-top.png").string() << "\n";
std::cout << "斜视图 : "
<< (fs::path(shotDir) / "view-all-oblique.png").string() << "\n";
std::cout << "拉近图 : "
<< (fs::path(shotDir) / "view-all-near.png").string() << "\n";
writeMetricLine(
"view-all,lines=" + std::to_string(lines.size()) +
",exagg=" + std::to_string(exagg) + ",spread=" + std::to_string(spread) +
",ovVisible=" + std::to_string(ovVisible) +
",ovCulled=" + std::to_string(ovCulled) +
",nearVisible=" + std::to_string(nearVisible) +
",nearCulled=" + std::to_string(nearCulled) +
",fpsOverview=" + std::to_string(fpsOverview) +
",fpsNear=" + std::to_string(fpsNear) +
",nonBlack=" + std::to_string(nb) +
",texErr=" + std::to_string(texErr ? 1 : 0));
return (nb > 0 && !texErr) ? 0 : 1;
}
// 真窗口:可旋转/缩放(每线引擎 LOD + 视锥裁剪 + 拖动降采样)。
vtkOutputWindow::SetInstance(nullptr);
rw->SetWindowName("gpr_poc view-all —— 20 条独立体引擎LOD/视锥裁剪/拖动降采样");
// 屏幕左上角 fps + 可见/裁剪线数文本。
vtkNew<vtkTextActor> fpsText;
fpsText->SetInput("fps: -- | visible lines: -- | culled: --");
fpsText->GetTextProperty()->SetFontSize(20);
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
fpsText->SetDisplayPosition(12, winH - 30);
ren->AddViewProp(fpsText);
st.fpsText = fpsText.Get();
vtkNew<vtkRenderWindowInteractor> iren;
iren->SetRenderWindow(rw);
vtkNew<vtkInteractorStyleTrackballCamera> style;
iren->SetInteractorStyle(style);
iren->SetDesiredUpdateRate(15.0); // 拖动态mapper 降采样
iren->SetStillUpdateRate(0.5); // 静止态:全质量
vtkNew<vtkCallbackCommand> cbInteract;
cbInteract->SetCallback(viewAllOnInteracting);
cbInteract->SetClientData(&st);
iren->AddObserver(vtkCommand::InteractionEvent, cbInteract);
vtkNew<vtkCallbackCommand> cbEnd;
cbEnd->SetCallback(viewAllOnInteract);
cbEnd->SetClientData(&st);
iren->AddObserver(vtkCommand::EndInteractionEvent, cbEnd);
vtkNew<vtkCallbackCommand> cbTimer;
cbTimer->SetCallback(viewAllOnTimer);
cbTimer->SetClientData(&st);
iren->AddObserver(vtkCommand::TimerEvent, cbTimer);
std::cout << "[view-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出。\n";
iren->Initialize();
iren->CreateRepeatingTimer(33); // ~30Hz 非阻塞拉取后台就绪纹理
rw->Render();
iren->Start();
std::cout << "[view-all] 窗口关闭,退出。\n";
return 0;
}
// ============================================================================
// view-survey-all测绘级世界对齐体直接按 CGCS2000 origin 摆放渲染(Task P8)
// ============================================================================
//
// 与 view-all(线局部体 + .gps 起点+航向刚体近似摆放)不同:本命令的体已是测绘级世界对齐
// 体(build-survey-all 产出meta.origin=CGCS2000 世界米,轴 X=东/Y=北/Z=深)。故【无需 .gps、
// 无需航向旋转】——直接按各体 meta.origin 平移摆进同一世界框即精确就位(跟路的弯)。
// 取公共参考原点(全体 origin 最小东/北)平移到近零局部框,保 VTK 数值稳定且相对位置严格不变。
// 由测绘级世界对齐体 + 公共参考原点 + Z 夸张 → vtkVolume(平移到世界位,无旋转)。
vtkSmartPointer<vtkVolume> makeSurveyPlacedVolume(
vtkImageData* img, const geopro::data::StoreMeta& m, double refX,
double refY, double exagg, double& vminOut, double& vmaxOut) {
const GalleryVariant& v = kViewDefaultVariant;
double vmin = m.vminPhys, vmax = m.vmaxPhys;
const ScalarPercentiles pc =
sampleScalarPercentiles(img, m.quant, 0.02, 0.98);
if (pc.samples > 0) {
vmin = pc.lo;
vmax = pc.hi;
}
vminOut = vmin;
vmaxOut = vmax;
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
GradStats gs;
if (v.useGradientOpacity) gs = sampleGradientMagnitude(img);
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
v, m.quant, cs, vmin, vmax, v.maxOpacity,
v.useGradientOpacity ? &gs : nullptr);
// img 的 origin 是 meta.origin(CGCS 世界米,东向 ~4e7 量级)。直接把 image origin 减去公共
// 参考原点落到近零局部框——避免 VTK 内部 float32 在 ~4e7 绝对坐标上丢精度(±1m);相对位置严格不变。
double iorg[3];
img->GetOrigin(iorg);
img->SetOrigin(iorg[0] - refX, iorg[1] - refY, iorg[2]);
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetInputData(img);
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
// 体已落近零世界框;仅 Z 夸张(局部深度)。
auto xf = vtkSmartPointer<vtkTransform>::New();
xf->PostMultiply();
xf->Scale(1.0, 1.0, exagg);
volume->SetUserTransform(xf);
return volume;
}
int cmdViewSurveyAll(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view-survey-all <storesDir> [--preview] "
"[--exagg 8] [--level 1] [--shotDir <dir>]\n"
"例: gpr_poc view-survey-all tmp/survey_all --preview "
"--exagg 8\n";
return 2;
}
const std::string storesDir = a.positional[0];
const double exagg = std::stod(a.get("exagg", "8"));
const int level = std::stoi(a.get("level", "1"));
const bool preview = a.kv.count("preview") > 0;
const std::string shotDir = a.get("shotDir", storesDir);
std::cout << "[view-survey-all] storesDir=" << storesDir << " exagg=" << exagg
<< " level=" << level
<< (preview ? " [PREVIEW 离屏俯视+斜视出图]" : " [真窗口可交互]")
<< "\n";
std::cout << "[view-survey-all] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[view-survey-all] 闸门失败,中止。\n";
return 1;
}
// 1) 发现体目录(含 meta.json),按名排序。
std::vector<std::string> storeNames;
for (const auto& e : fs::directory_iterator(storesDir)) {
if (!e.is_directory()) continue;
if (!fs::exists(e.path() / "meta.json")) continue;
storeNames.push_back(e.path().filename().string());
}
std::sort(storeNames.begin(), storeNames.end());
std::cout << "[view-survey-all] 发现体目录数=" << storeNames.size() << "\n";
if (storeNames.empty()) {
std::cerr << "[view-survey-all] 错误: 未发现任何含 meta.json 的体目录\n";
return 1;
}
// 2) 逐体读 meta先定公共参考原点(全体 origin 最小东/北)。
struct SurveyPlaced {
std::string name;
vtkSmartPointer<vtkImageData> img;
geopro::data::StoreMeta meta;
};
std::vector<SurveyPlaced> placed;
double refX = std::numeric_limits<double>::infinity();
double refY = std::numeric_limits<double>::infinity();
for (const std::string& nm : storeNames) {
const std::string storePath = (fs::path(storesDir) / nm).string();
geopro::data::ChunkedVolumeStore store(storePath);
const geopro::data::StoreMeta m = store.meta();
refX = std::min(refX, m.origin[0]);
refY = std::min(refY, m.origin[1]);
const int lv = std::max(0, std::min(level, store.levels() - 1));
vtkSmartPointer<vtkImageData> img = buildLevelImage(store, lv, m);
std::cout << "[view-survey-all] " << nm << " level=" << lv << " 维度="
<< img->GetDimensions()[0] << "x" << img->GetDimensions()[1] << "x"
<< img->GetDimensions()[2] << " CGCS origin=(" << std::fixed
<< m.origin[0] << ", " << m.origin[1] << ")\n";
placed.push_back({nm, img, m});
}
if (placed.empty()) {
std::cerr << "[view-survey-all] 错误: 无可用体\n";
return 1;
}
std::cout << "[view-survey-all] 公共参考原点(最小东/北)=(" << std::fixed << refX
<< ", " << refY << ") (共 " << placed.size() << " 体)\n";
// 3) 同一 renderer 加全部世界对齐体(按 origin-ref 摆放,无旋转)。
const int winW = 1400, winH = 900;
auto rw = preview ? makeOffscreenWindow(winW, winH)
: vtkSmartPointer<vtkRenderWindow>::New();
if (!preview) rw->SetSize(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(kViewDefaultVariant.bg[0], kViewDefaultVariant.bg[1],
kViewDefaultVariant.bg[2]);
rw->AddRenderer(ren);
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
int added = 0;
for (const SurveyPlaced& sp : placed) {
double vmin = 0, vmax = 0;
vtkSmartPointer<vtkVolume> vol = makeSurveyPlacedVolume(
sp.img.Get(), sp.meta, refX, refY, exagg, vmin, vmax);
ren->AddVolume(vol);
++added;
}
std::cout << "[view-survey-all] 已加入场景体数=" << added << "\n";
ren->ResetCamera();
rw->Render();
if (capWin->textureError()) {
std::cerr << "[view-survey-all] 警告: 检测到 3D 纹理维度错误(某体超 GL 上限)"
"可增大 --level 取更粗层。\n";
}
if (preview) {
fs::create_directories(shotDir);
{ // 俯视(top):看 20 条按真实 CGCS 位置铺成测区(含路的弯)。
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
double fp[3];
cam->GetFocalPoint(fp);
const double* b = ren->ComputeVisiblePropBounds();
const double span = std::max({b[1] - b[0], b[3] - b[2], b[5] - b[4]});
cam->SetPosition(fp[0], fp[1], fp[2] + span * 2.0);
cam->SetFocalPoint(fp[0], fp[1], fp[2]);
cam->SetViewUp(0, 1, 0);
ren->ResetCameraClippingRange();
rw->Render();
const std::string p =
(fs::path(shotDir) / "view-survey-all-top.png").string();
savePng(rw.Get(), p);
std::cout << "[view-survey-all] 俯视图存: " << p << "\n";
}
{ // 斜视(oblique)。
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(35.0);
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
rw->Render();
const std::string p =
(fs::path(shotDir) / "view-survey-all-oblique.png").string();
savePng(rw.Get(), p);
std::cout << "[view-survey-all] 斜视图存: " << p << "\n";
}
rw->Render();
Stopwatch sw;
const int frames = 60;
for (int f = 0; f < frames; ++f) rw->Render();
const double ms = sw.elapsedMs();
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
const vtkIdType nb = countNonBlackPixels(rw.Get(), winW, winH);
std::cout << "\n=== view-survey-all --preview 测区全貌(测绘级世界对齐体)===\n";
std::cout << "参与体数 : " << placed.size() << "\n";
std::cout << "level : " << level << "\n";
std::cout << "exagg(Z) : " << exagg << "\n";
std::cout << "公共参考原点 : (" << std::fixed << refX << ", " << refY
<< ") CGCS2000 米\n";
std::cout << "非黑像素 : " << nb << " / " << (winW * winH) << "\n";
std::cout << "fps(" << frames << "帧连渲) : " << fps << "\n";
std::cout << "俯视图 : "
<< (fs::path(shotDir) / "view-survey-all-top.png").string()
<< "\n";
std::cout << "斜视图 : "
<< (fs::path(shotDir) / "view-survey-all-oblique.png").string()
<< "\n";
writeMetricLine("view-survey-all,bodies=" + std::to_string(placed.size()) +
",level=" + std::to_string(level) +
",exagg=" + std::to_string(exagg) +
",nonBlack=" + std::to_string(nb) +
",fps=" + std::to_string(fps));
return nb > 0 ? 0 : 1;
}
rw->SetWindowName("gpr_poc view-survey-all —— 测绘级 CGCS2000 世界对齐体");
vtkNew<vtkRenderWindowInteractor> iren;
iren->SetRenderWindow(rw);
vtkNew<vtkInteractorStyleTrackballCamera> style;
iren->SetInteractorStyle(style);
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(35.0);
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
std::cout << "[view-survey-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出。\n";
iren->Initialize();
rw->Render();
iren->Start();
std::cout << "[view-survey-all] 窗口关闭,退出。\n";
return 0;
}
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] [--base] [--variant N] "
"[--gallery] [--frames 90]\n";
return 2;
}
const std::string dir = a.positional[0];
// 交互/preview/smoke 默认视觉参数 = kViewDefaultVariant(var4):配色/不透明度包络/
// exagg/背景全部走 var4故默认画面 == view-var4DRY与画廊同源。命令行 --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");
// C3-8 验收用:--preview --shots 额外从【真 view 场景(base+hires+cropping)】多旋转角
// 离屏出图,用于人工核对「路细长不胖 / 拼接无缝无白 / 旋转无移动白斑」。只加图,不改
// 默认行为(无 --shots 时与原 preview 完全一致)。
const bool shots = hasFlag("shots");
// C3-6 底图预览:--preview --base或 --base模拟「交互态:只渲常驻粗底图」——
// 隐去高清叠加层、只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、绝不空白。
const bool basePreview = hasFlag("base");
// 拉近预览Task 12d-singletex--preview --variant near或 --near走与真窗口
// 完全相同的单纹理拉近路径viewRefreshSingle 选 level0 局部子区域),供控制方 Read
// 验证「拉近后」单图非空、完整、fps 高。
const bool nearPreview =
hasFlag("near") || (a.kv.count("variant") && a.get("variant", "") == "near");
// 画廊出图目录:--out <dir> 显式指定;否则默认存到 store 目录P4gallery 落在
// tmp/line001_proc便于控制方就地对照
const std::string galleryShotDir = a.get("out", dir);
// 画廊模式Task 12d渲 4 组视觉调参图供挑选。优先于其余路径。
if (hasFlag("gallery")) {
return cmdViewGallery(dir, frames, galleryShotDir);
}
// 单变体view --preview --variant NN=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,
galleryShotDir);
}
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 分块(保留参数以兼容旧命令行)。
// 渲染源 = ViewAdaptiveVolumeSource(C2)。exagg 走 actor 的 SetScale不烘进
// 几何,避免与 SetScale 重复夸张),故源构造 exagg=1.0。视口高/宽高比注入给
// C1 选层(分辨率密度 + 视锥裁剪)。
geopro::render::ViewAdaptiveVolumeSource source(dir, /*exagg=*/1.0);
source.setViewportHeight(winH);
source.setAspect(static_cast<double>(winW) / winH);
const auto& m = source.meta();
// P3 首帧高清段(沿线中段 level0):直读 store 建该段单图——既作传函分位标定的基准
// (该段实际值,对比比整卷底图更punchy),又供 viewSetupDefaultFrame 直接喂高清 mapper
// (绕开异步 LOD 在「框一段」视距下仍选最粗层的问题)。同 gallery 的 buildLocalLevel0Image
// 直读路径,非 LOD 算法。读不到则回退底图/全域。
vtkSmartPointer<vtkImageData> segImg;
{
geopro::data::ChunkedVolumeStore store(dir);
const int totBx = store.bricksX(0);
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
segImg = buildLocalLevel0Image(store, m, bx0, localBx);
}
// P3 传函分位标定:色阶/不透明度端点按【实际值 2%/98% 分位】(物理域),裁离群。
// 处理后体值多集中 ±窄带、少量离群 ±9000按 meta 全域(±9249)映射 → 窄带近透明 → 全黑。
// 基准用常驻底图(整卷代表):band 较窄 → 窄带信号映到更饱和色、可见度更高;且代表整线
// 典型信号、不受单段局部高能离群影响。底图缺失再回退首帧段。退化(无样本)回退全域。
double vmin = m.vminPhys, vmax = m.vmaxPhys;
vtkImageData* pcBasis =
source.baseImage() != nullptr ? source.baseImage() : segImg.Get();
if (pcBasis != nullptr) {
const ScalarPercentiles pc =
sampleScalarPercentiles(pcBasis, m.quant, 0.02, 0.98);
if (pc.samples > 0) {
vmin = pc.lo;
vmax = pc.hi;
std::cout << "[view] 传函分位标定("
<< (source.baseImage() != nullptr ? "底图" : "局部段")
<< ",样本 " << pc.samples << "): 2%=" << vmin << " 98%=" << vmax
<< " (全域 [" << m.vminPhys << ", " << m.vmaxPhys
<< "] → 裁离群)\n";
}
}
// 配色/不透明度包络取自 var4seismic + V 形实体包络(floor/mid + opacity 作峰值)。
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
// C4默认变体(var4)开了梯度不透明度 → 从常驻底图实测梯度分布标定阈值。底图恒非空。
GradStats dvGs;
if (dv.useGradientOpacity && source.baseImage() != nullptr) {
dvGs = sampleGradientMagnitude(source.baseImage());
std::cout << "[view] 梯度幅值分布(底图,量化域,样本 " << dvGs.samples
<< "): median=" << dvGs.median << " p90=" << dvGs.p90
<< " p99=" << dvGs.p99 << "\n";
}
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
dv, m.quant, cs, vmin, vmax, opacity,
dv.useGradientOpacity ? &dvGs : nullptr);
// 渲染窗口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);
// C3-6 常驻粗底图层(底图永不空白的命脉):第一个 vtkVolume = 整卷最粗
// 「各轴 ≤16384」层单纹理source.baseImage(),构造时主线程一次建成、永远持有)。
// 它【永远在场、永远渲染、绝不释放、绝不被异步路径触碰】→ 任何相机/任何运动中都
// 盖住整个体 → 拖动/缩放绝不空白。高清层(下方 mapper)叠在其上、就绪后局部覆盖。
vtkNew<vtkSmartVolumeMapper> baseMapper;
baseMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
baseMapper->SetAutoAdjustSampleDistances(1);
baseMapper->SetInteractiveAdjustSampleDistances(1);
auto baseVolume = vtkSmartPointer<vtkVolume>::New();
if (source.baseImage() != nullptr) { // 退化 store 防护(理论恒非空)
baseMapper->SetInputData(source.baseImage()); // 常驻输入,永不改
baseMapper->Update();
baseVolume->SetMapper(baseMapper);
baseVolume->SetProperty(prop); // 与高清层共用传函(同配色/不透明度)
baseVolume->SetScale(1.0, 1.0, exagg); // 垂向夸张只放大深度(Z);横向路宽(Y)不动 → 与高清层空间对齐
ren->AddVolume(baseVolume); // 先加底图 → 底层常渲
}
// 高清叠加层:单 vtkSmartVolumeMapperGPU 光线投射,整张 3D 纹理),与 --preview /
// gallery 同一 mapper 类型。叠在底图之上currentImages 就绪后摆到对应世界位置局部
// 覆盖底图;没就绪则无输入(只显底图,不空)。运动中高清滞后由底图兜底,绝不空白。
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
// C3-5交互式采样距离自适应修长板填屏 ray-march 慢的关键。POC 当初为离屏
// 基准把这两个关掉(0/0)以求恒定全质量,但交互场景必须【开启】:拖动时按渲染窗口的
// DesiredUpdateRate 拉大 SampleDistance(降采样→快)、停下恢复小步长(全质量→可慢)。
// 长板填屏慢的根因是每像素沿超长轴海量采样,LOD/异步都不缩短 ray-march 长度,只有
// 拖动期降采样才把交互态降到可跟手帧率。
mapper->SetAutoAdjustSampleDistances(1);
mapper->SetInteractiveAdjustSampleDistances(1);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, 1.0, exagg); // 垂向夸张只放大深度(Z);横向路宽(Y)保持真实比例(修 GPR 路被压胖)
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.source = &source;
st.mapper = mapper.Get();
st.baseMapper = baseMapper.Get(); // 供 viewSyncBaseCropping 按高清块挖空底图
st.ren = ren.Get();
st.fpsText = fpsText.Get();
st.rw = rw.Get();
st.exagg = exagg;
st.dir = dir; // 供首帧直读 level0 局部段(绕开 LOD 选粗层)
st.seedSegImg = segImg; // cmdView 已为分位标定建好的首帧高清段,直接复用
// 相机初始定向(修复 1默认框「局部段」而非整卷。整线横截面 1:34框整卷
// 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口
// 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。
std::size_t warm = viewSetupDefaultFrame(&st, ren);
rw->Render();
// C3-6 底图预览(模拟「交互态:只渲常驻粗底图」):隐去高清叠加层、把相机框到整卷
// 包围盒exagg 后),只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、非空。证明
// 拖动/缩放时即使高清全部缺位,底图也独立盖住整个体(永不空白的命脉)。
if (preview && basePreview) {
volume->SetVisibility(0); // 隐去高清层 → 只剩常驻底图
baseMapper->SetCropping(0); // 高清隐了 → 底图取消挖空,整卷全渲(证明永不空白)
// 框整卷(无参 ResetCamera 按场景中可见 actor 包围盒;高清隐了 → 框底图全卷)。
ren->ResetCamera();
st.cam = ren->GetActiveCamera();
st.cam->Elevation(kViewDefaultVariant.elevation);
st.cam->Azimuth(kViewDefaultVariant.azimuth);
ren->ResetCameraClippingRange();
rw->Render();
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
const std::string pngPath = (shotDir / "view-base.png").string();
savePng(rw.Get(), pngPath);
auto countStructPx = [&]() -> 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 baseStruct = countStructPx();
int bd[3] = {0, 0, 0};
if (source.baseImage()) source.baseImage()->GetDimensions(bd);
const bool texErrB = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const bool okB = !texErrB && source.baseImage() != nullptr && baseStruct > 0;
std::cout << "\n=== view --preview --base 常驻粗底图验证(整卷概览)===\n";
std::cout << "底图 level : " << source.baseLevel()
<< "(整卷最粗 ≤16384 层)\n";
std::cout << "底图维度 : " << bd[0] << " x " << bd[1] << " x " << bd[2]
<< "\n";
std::cout << "存图 : " << pngPath << "\n";
std::cout << "结构像素(>50) : " << baseStruct << " / " << (winW * winH)
<< " (" << (100.0 * baseStruct / (winW * winH)) << "%)\n";
std::cout << "纹理维度错误 : " << (texErrB ? "是(!!)" : "") << "\n";
std::cout << "base 结果 : "
<< (okB ? "OK ✔ 底图盖全非空" : "FAIL ✘") << "\n";
writeMetricLine(
"view-base,dir=" + dir + ",baseLevel=" +
std::to_string(source.baseLevel()) + ",bx=" + std::to_string(bd[0]) +
",by=" + std::to_string(bd[1]) + ",bz=" + std::to_string(bd[2]) +
",structPixels=" + std::to_string(baseStruct) +
",ok=" + std::to_string(okB ? 1 : 0) + ",png=" + pngPath);
return okB ? 0 : 1;
}
// 拉近预览:在默认取景基础上拉近相机,再走阻塞刷新(与真窗口缩放后完全相同的
// 单纹理选区路径level0 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
if (nearPreview) {
st.cam->Dolly(2.5); // 拉近
ren->ResetCameraClippingRange();
warm = viewRefreshBlocking(&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);
// C3-8多旋转角出图同一真 view 场景base+hires 共用 SetScale(1,1,exagg) + 底图
// 按高清块 cropping 挖空)。从默认相机起取若干 (azimuth,elevation) 离屏存图,供人工
// 核对路细长比例 / 拼接无缝 / 旋转无移动白斑。无 --shots 不执行。
if (shots) {
const struct {
const char* name;
double az;
double el;
} kAngles[] = {
{"view-shot-az0", 0.0, 0.0}, {"view-shot-az30", 30.0, 0.0},
{"view-shot-az60", 60.0, 0.0}, {"view-shot-az-30", -30.0, 0.0},
{"view-shot-el20", 0.0, 20.0}, {"view-shot-az45el15", 45.0, 15.0},
};
for (const auto& s : kAngles) {
st.cam->Azimuth(s.az);
st.cam->Elevation(s.el);
ren->ResetCameraClippingRange();
rw->Render();
const std::string sp = (shotDir / (std::string(s.name) + ".png")).string();
savePng(rw.Get(), sp);
std::cout << "[view] 旋转角出图: " << sp << " (az=" << s.az
<< " el=" << s.el << ")\n";
st.cam->Elevation(-s.el); // 复位到默认朝向,下一角从默认起算
st.cam->Azimuth(-s.az);
}
}
// 结构像素计数:背景为深蓝灰(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();
// C3-5两态 fps 对照。AutoAdjustSampleDistances 据渲染窗口的 DesiredUpdateRate
// 决定 SampleDistance——高 DesiredUpdateRate=拖动态(大步长/降采样/快),低=静止态
// (小步长/全质量/可慢)。离屏无真交互,故显式设 DesiredUpdateRate 模拟两态,旋相机
// 测真实 Render() 帧率,量化拖动降采样提速。
//
// ① 全质量静态StillUpdateRate(慢档),mapper 用最小 SampleDistance。
rw->SetDesiredUpdateRate(0.5); // 静止目标帧率(慢)→全质量
rw->Render(); // 预热一帧(管线热 + 采样距离按此档生效)
Stopwatch swStill;
for (int f = 0; f < frames; ++f) {
st.cam->Azimuth(360.0 / frames);
rw->Render();
}
const double msStill = swStill.elapsedMs();
const double fps = msStill > 0 ? frames * 1000.0 / msStill : 0.0;
// ② 交互态(降采样):高 DesiredUpdateRate(拖动目标 15fps)→ mapper 拉大 SampleDistance。
rw->SetDesiredUpdateRate(15.0); // 拖动目标帧率→降采样
rw->Render(); // 预热一帧让降采样步长生效
Stopwatch swInteract;
for (int f = 0; f < frames; ++f) {
st.cam->Azimuth(360.0 / frames);
rw->Render();
}
const double msInteract = swInteract.elapsedMs();
const double fpsInteract =
msInteract > 0 ? frames * 1000.0 / msInteract : 0.0;
rw->SetDesiredUpdateRate(0.5); // 复位
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";
const double speedup = fps > 0 ? fpsInteract / fps : 0.0;
std::cout << "全质量静态 fps : " << (ok ? std::to_string(fps) : "INVALID")
<< " (" << frames << " 帧旋相机, DesiredUpdateRate=0.5)\n";
std::cout << "交互态降采样 fps: "
<< (ok ? std::to_string(fpsInteract) : "INVALID") << " (" << frames
<< " 帧旋相机, DesiredUpdateRate=15)\n";
std::cout << "拖动态提速 : " << (ok ? std::to_string(speedup) : "INVALID")
<< "x (交互态/全质量)\n";
std::cout << "交互级(≥15fps) : "
<< (ok ? (fpsInteract >= 15.0 ? "是 ✔" : "否(未达拖动目标)")
: "INVALID")
<< "\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) +
",fpsStill=" + (ok ? std::to_string(fps) : "INVALID") +
",fpsInteract=" + (ok ? std::to_string(fpsInteract) : "INVALID") +
",speedup=" + (ok ? std::to_string(speedup) : "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 = viewRefreshBlocking(&st);
const int lvlFar = st.lastLevel;
rw->Render();
st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LODlevel0 局部子区域)
ren->ResetCameraClippingRange();
viewRefreshBlocking(&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);
// C3-5交互/静止目标帧率。interactor 在交互(拖动)中把渲染窗口 DesiredUpdateRate
// 拉到 DesiredUpdateRate(15)→mapper 自适应降采样(快、跟手);松手后落回 StillUpdateRate
// (0.5)→恢复小步长全质量。配合上面 mapper 的 AutoAdjust/InteractiveAdjust 才生效。
iren->SetDesiredUpdateRate(15.0);
iren->SetStillUpdateRate(0.5);
vtkNew<vtkCallbackCommand> cb;
cb->SetCallback(viewOnInteract);
cb->SetClientData(&st);
// EndInteraction旋转/缩放松手后提交新目标 + 刷 fps仅松手触发一次不自激
// 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent
// 形成无限递归重渲窗口卡死、fps≈0。fps 文本在松手时刷新即可。
iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
// C3-2拖动进行中持续提交目标非阻塞主线程不被重组卡住 → 跟手。
vtkNew<vtkCallbackCommand> cbInteract;
cbInteract->SetCallback(viewOnInteracting);
cbInteract->SetClientData(&st);
iren->AddObserver(vtkCommand::InteractionEvent, cbInteract);
// C3-2周期定时器非阻塞拉取后台已就绪纹理换上新 LOD 备好即显示,拖动不卡)。
vtkNew<vtkCallbackCommand> cbTimer;
cbTimer->SetCallback(viewOnTimer);
cbTimer->SetClientData(&st);
iren->AddObserver(vtkCommand::TimerEvent, cbTimer);
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
iren->Initialize();
iren->CreateRepeatingTimer(33); // ~30Hz 拉取后台就绪纹理(不阻塞主线程)
rw->Render();
iren->Start();
std::cout << "[view] 窗口关闭,退出。\n";
return 0;
}
// ============================================================================
// 12d-polish梯度不透明度 + 光照 打磨探针(验证"体内部白雾"能否靠打磨解决)
// ============================================================================
//
// 当前体绘制对道路 GPR 水平层数据,体中间是均匀白雾、只有端面有层次。本探针在同一
// 全分辨率(level0)局部段 + 同一「看进体内部」视角(斜穿俯视,视线穿过体内部而非只看
// 端面)下渲 3 张离屏对比图,验证:给体加【梯度不透明度】(均匀区透明、层界面显出) +
// 【光照/明暗】能否让内部层状结构"浮"出来:
// polish-a-value.png 基线:按数值的不透明度(V形包络),无梯度不透明度、无光照
// polish-b-grad.png + 梯度不透明度(SetGradientOpacity)
// polish-c-grad-shade.png + 梯度不透明度 + 光照(ShadeOn, Ambient/Diffuse/Specular)
//
// 梯度不透明度的 piecewise 按【实际梯度幅值分布】标定阈值(不靠猜):先在量化域逐体素
// 采样 6 邻居中心差分梯度幅值,取分位数(median / p90)作斜坡控制点。
// 量化域标量不透明度峰值。基线(a)用 0.15(与默认体绘制同档→均匀积分白雾);开了梯度
// 不透明度(b/c)后均匀区被梯度门压成透明,可放心把标量峰值提到 0.6,让【层界面】这类
// 高梯度处的净不透明度(标量×梯度)足够高、层面真正"浮"成实面,而非仍是淡影。
constexpr double kPolishMaxOpacityFog = 0.15; // a基线白雾
constexpr double kPolishMaxOpacityGrad = 0.6; // b/c梯度门控后可提高
// 在 VTK_SHORT 局部体上采样梯度幅值分布(量化域,中心差分),返回有序的若干分位数。
// 跳过 kBlank 体素及其邻居(空值不参与梯度)。返回 {median, p75, p90, p99, max}。
// GradStats 已在文件上方前置声明,供 C4 画廊共用。)
GradStats sampleGradientMagnitude(vtkImageData* img) {
int dims[3];
img->GetDimensions(dims);
const int nx = dims[0], ny = dims[1], nz = dims[2];
auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
GradStats gs;
if (!arr || nx < 3 || ny < 3 || nz < 3) return gs;
const std::int16_t blank = geopro::core::ScalarVolumeI16::kBlank;
auto at = [&](int i, int j, int k) -> std::int16_t {
const vtkIdType id = (static_cast<vtkIdType>(k) * ny + j) * nx + i;
return arr->GetValue(id);
};
std::vector<double> mags;
mags.reserve(static_cast<std::size_t>(nx) * ny * nz / 8 + 1);
// 步长抽样:大体不必逐体素,间隔取样即可代表分布(≤~50万样本
const vtkIdType total = static_cast<vtkIdType>(nx - 2) * (ny - 2) * (nz - 2);
const int stride = static_cast<int>(std::max<vtkIdType>(1, total / 500000));
vtkIdType counter = 0;
for (int k = 1; k < nz - 1; ++k) {
for (int j = 1; j < ny - 1; ++j) {
for (int i = 1; i < nx - 1; ++i) {
if ((counter++ % stride) != 0) continue;
const std::int16_t c = at(i, j, k);
if (c == blank) continue;
const std::int16_t xm = at(i - 1, j, k), xp = at(i + 1, j, k);
const std::int16_t ym = at(i, j - 1, k), yp = at(i, j + 1, k);
const std::int16_t zm = at(i, j, k - 1), zp = at(i, j, k + 1);
if (xm == blank || xp == blank || ym == blank || yp == blank ||
zm == blank || zp == blank) {
continue;
}
const double gx = 0.5 * (xp - xm);
const double gy = 0.5 * (yp - ym);
const double gz = 0.5 * (zp - zm);
mags.push_back(std::sqrt(gx * gx + gy * gy + gz * gz));
}
}
}
if (mags.empty()) return gs;
std::sort(mags.begin(), mags.end());
auto q = [&](double p) {
const std::size_t idx = static_cast<std::size_t>(
std::min<double>(mags.size() - 1, p * (mags.size() - 1)));
return mags[idx];
};
gs.median = q(0.50);
gs.p75 = q(0.75);
gs.p90 = q(0.90);
gs.p99 = q(0.99);
gs.mx = mags.back();
gs.samples = mags.size();
return gs;
}
// 标量分位标定实现P3步长抽样该 VTK_SHORT 体的非空体素值,排序取 pLo/pHi 分位,
// 用 quant.toPhys 反算成物理端点。失败/退化(无样本或 lo>=hi)返回 samples=0由调用方
// 回退到 meta 全量化域。仿 sampleGradientMagnitude 的 stride/blank 处理,自适应该体值域。
ScalarPercentiles sampleScalarPercentiles(vtkImageData* img,
const geopro::core::Quant& q,
double pLo, double pHi) {
ScalarPercentiles out;
if (!img) return out;
int dims[3];
img->GetDimensions(dims);
const vtkIdType npts =
static_cast<vtkIdType>(dims[0]) * dims[1] * dims[2];
auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
if (!arr || npts <= 0) return out;
const std::int16_t blank = geopro::core::ScalarVolumeI16::kBlank;
std::vector<std::int16_t> vals;
vals.reserve(static_cast<std::size_t>(npts) / 4 + 1);
// 步长抽样:~50 万样本足以代表分布(与梯度采样同档)。
const vtkIdType stride = std::max<vtkIdType>(1, npts / 500000);
for (vtkIdType id = 0; id < npts; id += stride) {
const std::int16_t v = arr->GetValue(id);
if (v == blank) continue;
vals.push_back(v);
}
if (vals.empty()) return out;
std::sort(vals.begin(), vals.end());
auto pick = [&](double p) {
const std::size_t idx = static_cast<std::size_t>(
std::min<double>(vals.size() - 1, p * (vals.size() - 1)));
return static_cast<double>(vals[idx]);
};
const double qLo = pick(pLo);
const double qHi = pick(pHi);
if (!(qLo < qHi)) return out; // 退化(常数体)→ 调用方回退全域
out.lo = q.toPhys(static_cast<std::int16_t>(std::lround(qLo)));
out.hi = q.toPhys(static_cast<std::int16_t>(std::lround(qHi)));
out.samples = vals.size();
return out;
}
// 标量不透明度V 形包络(与 makeSolidVolumeProperty 同思路floor/mid/max三图共用
// 保证唯一变量是梯度不透明度 / 光照。
void setPolishScalarOpacity(vtkVolumeProperty* prop, const geopro::core::Quant& q,
double vminPhys, double vmaxPhys, double maxOpacity) {
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));
const double qmid = 0.5 * (qminD + qmaxD);
const double half = 0.5 * (qmaxD - qminD);
const double floorOp = 0.4 * maxOpacity; // 中段背景按峰值比例压低V 形)
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
opacity->AddPoint(qminD, maxOpacity);
opacity->AddPoint(qmid - 0.55 * half, floorOp);
opacity->AddPoint(qmid, 0.2 * maxOpacity);
opacity->AddPoint(qmid + 0.55 * half, floorOp);
opacity->AddPoint(qmaxD, maxOpacity);
prop->SetScalarOpacity(opacity);
}
// 颜色传函量化域seismic 红白蓝,与其余探针同思路)。
void setPolishColor(vtkVolumeProperty* prop, const geopro::core::Quant& q,
const geopro::core::ColorScale& cs, double vminPhys,
double vmaxPhys) {
constexpr int kSamples = 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 < kSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kSamples - 1);
const auto qv = static_cast<std::int16_t>(std::lround(qd));
const auto c = cs.colorAt(q.toPhys(qv));
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
}
prop->SetColor(color);
}
// 共用:把局部体喂单 SmartVolumeMapper按一个「看进体内部」的相机取景渲一帧 + 存 PNG
// 报告 结构像素 / 平均亮度 / fps。mode: 0=value 基线 1=+grad 2=+grad+shade。
int renderPolishOne(vtkImageData* locImg, const geopro::core::Quant& quant,
const geopro::core::ColorScale& cs, double vmin, double vmax,
const GradStats& gs, int mode, double exagg,
const std::string& pngPath, int frames, double elevation,
double azimuth, double zoom) {
const int winW = 1280, winH = 800;
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.05, 0.05, 0.09);
rw->AddRenderer(ren);
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetInputData(locImg);
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
setPolishColor(prop, quant, cs, vmin, vmax);
const double scalarMax =
(mode == 0) ? kPolishMaxOpacityFog : kPolishMaxOpacityGrad;
setPolishScalarOpacity(prop, quant, vmin, vmax, scalarMax);
prop->SetInterpolationTypeToLinear();
// 梯度不透明度mode>=1梯度小(均匀区)→透明、梯度大(层界面)→不透明。
// 阈值按实测分布median 处仍接近 0压住均匀积分雾p90 升到 0.5p99 到 0.9。
if (mode >= 1) {
vtkNew<vtkPiecewiseFunction> grad;
grad->AddPoint(0.0, 0.0);
grad->AddPoint(std::max(1.0, gs.median), 0.0);
grad->AddPoint(std::max(2.0, gs.p90), 0.5);
grad->AddPoint(std::max(3.0, gs.p99), 0.9);
prop->SetGradientOpacity(grad);
}
// 光照mode>=2ShadeOn + Ambient/Diffuse/Specular让层界面带立体明暗。
if (mode >= 2) {
prop->ShadeOn();
prop->SetAmbient(0.3);
prop->SetDiffuse(0.7);
prop->SetSpecular(0.2);
prop->SetSpecularPower(10.0);
} else {
prop->ShadeOff();
}
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:薄轴放大,截面结构才看得出
ren->AddVolume(volume);
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// 「看进体内部」取景:斜穿俯视——较大 Elevation 让视线从上方斜穿过体内部(而非只看
// 端面)配合垂向夸张后体呈板状俯视看穿层间。Zoom 填满画面。
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(elevation);
cam->Azimuth(azimuth);
cam->Zoom(zoom);
ren->ResetCameraClippingRange();
rw->Render();
// 结构像素:任一通道 >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;
};
// 高于背景像素:背景为 (0.05,0.05,0.09)≈RGB(13,13,23),阈值 35 干净地把渲出的体
// 结构与背景分开(区别于结构像素>50>35 能纳入梯度门控后偏暗但确有的层面)。
auto countAboveBg = [&]() -> 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) > 35 || px->GetComponent(i, 1) > 35 ||
px->GetComponent(i, 2) > 35) {
++n;
}
}
return n;
};
const vtkIdType structPx = countStructPixels();
const vtkIdType aboveBg = countAboveBg();
const double bright = meanBrightness(rw.Get(), winW, winH);
savePng(rw.Get(), pngPath);
// fps旋相机 frames 帧(保持大俯角,绕 Azimuth
rw->Render();
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);
// 有效判据:无纹理错 + 渲出高于背景的像素(>35。结构像素(>50)仅作亮度强弱度量,
// 不作有效门——梯度门控后层面偏暗但确有渲出,不应误判为空。
const bool ok = !texErr && aboveBg > 0;
const char* label =
mode == 0 ? "a-value(基线)"
: (mode == 1 ? "b-grad(+梯度不透明度)" : "c-grad-shade(+梯度+光照)");
std::cout << "\n--- polish " << label << " ---\n";
std::cout << "存图 : " << pngPath << "\n";
std::cout << "高于背景像素(>35): " << aboveBg << " / " << (winW * winH) << " ("
<< (100.0 * aboveBg / (winW * winH)) << "%)\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(
"polish," + std::string(mode == 0 ? "a-value" : mode == 1 ? "b-grad"
: "c-grad-shade") +
",aboveBg=" + std::to_string(aboveBg) +
",structPx=" + std::to_string(structPx) +
",bright=" + std::to_string(bright) +
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
",texErr=" + std::to_string(texErr ? 1 : 0) + ",png=" + pngPath);
return ok ? 0 : 1;
}
int cmdPolish(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc polish <storeDir> [--exagg 8] [--frames 90] "
"[--localBricks 4]\n";
return 2;
}
const std::string dir = a.positional[0];
const double exagg = std::stod(a.get("exagg", "8"));
const int frames = std::stoi(a.get("frames", "90"));
const int localBricks = std::stoi(a.get("localBricks", "4"));
// 「看进体内部」取景:斜穿俯视。默认 El45/Az30/Zoom1.5(视线从上方斜穿层间,
// 既不是纯端面也不至于过陡退化成边缘线)。可命令行覆盖以微调。
const double elevation = std::stod(a.get("elevation", "45"));
const double azimuth = std::stod(a.get("azimuth", "30"));
const double zoom = std::stod(a.get("zoom", "1.5"));
std::cout << "[polish] storeDir=" << dir << " exagg=" << exagg
<< " frames=" << frames << " localBricks=" << localBricks << "\n";
std::cout << "[polish] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[polish] 闸门失败,中止。\n";
return 1;
}
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 = makeSeismicColorScale(vmin, vmax);
// 全分辨率 level0 局部段(沿线中段),三图共用同一体。
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);
std::cout << "[polish] level0 局部段 " << locDims[0] << "x" << locDims[1] << "x"
<< locDims[2] << " (brick列 [" << bx0 << "," << (bx0 + localBx)
<< ") / " << totBx << ")\n";
// 标定梯度不透明度阈值:采样实际梯度幅值分布。
const GradStats gs = sampleGradientMagnitude(locImg.Get());
std::cout << "[polish] 梯度幅值分布(量化域,样本 " << gs.samples
<< "): median=" << gs.median << " p75=" << gs.p75
<< " p90=" << gs.p90 << " p99=" << gs.p99 << " max=" << gs.mx
<< "\n";
std::cout << "[polish] 梯度不透明度 piecewise: grad<=" << std::max(1.0, gs.median)
<< "→0.0 grad=" << std::max(2.0, gs.p90) << "→0.5 grad>="
<< std::max(3.0, gs.p99) << "→0.9\n";
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
int rc = 0;
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/0,
exagg, (shotDir / "polish-a-value.png").string(), frames,
elevation, azimuth, zoom);
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/1,
exagg, (shotDir / "polish-b-grad.png").string(), frames,
elevation, azimuth, zoom);
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/2,
exagg, (shotDir / "polish-c-grad-shade.png").string(),
frames, elevation, azimuth, zoom);
std::cout << "\n[polish] 完成3 张对比图存于 " << shotDir.string()
<< " (polish-a-value / polish-b-grad / polish-c-grad-shade)\n";
return rc;
}
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 build-stream <dir> [--cellXY 0.05] [--cellZ 0.05] "
"[--out <storeDir>] [--levels 3] [--sliceXBricks 8] "
"[--maxLines N]\n"
" gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] "
"[--out <storeDir>] [--levels 4] [--maxLines N] [--curvilinear]\n"
" gpr_poc build-line <lineDir> <linePrefix> --out <storeDir> "
"[--levels 3] [--coarse F]\n"
" gpr_poc build-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--minFreeGB 3]\n"
" gpr_poc build-survey-line <lineDir> <linePrefix> --out "
"<storeDir> [--levels 3] [--coarse F] [--cell 0.05] "
"[--radius 0.5] [--gps <path>]\n"
" gpr_poc build-survey-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--cell 0.05] [--radius 0.5] "
"[--minFreeGB 3]\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"
" gpr_poc view-all <storesDir> <gpsDir> [--preview] "
"[--exagg 8] [--level 1] [--spread M] [--shotDir <dir>]\n"
" gpr_poc view-survey-all <storesDir> [--preview] [--exagg 8] "
"[--level 1] [--shotDir <dir>]\n"
" gpr_poc polish <storeDir> [--exagg 8] [--frames 90] "
"[--localBricks 4]\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 == "build-stream") return cmdBuildStream(argc, argv);
if (cmd == "build-geo") return cmdBuildGeo(argc, argv);
if (cmd == "build-line") return cmdBuildLine(argc, argv);
if (cmd == "build-all") return cmdBuildAll(argc, argv);
if (cmd == "build-survey-line") return cmdBuildSurveyLine(argc, argv);
if (cmd == "build-survey-all") return cmdBuildSurveyAll(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);
if (cmd == "view-all") return cmdViewAll(argc, argv);
if (cmd == "view-survey-all") return cmdViewSurveyAll(argc, argv);
if (cmd == "polish") return cmdPolish(argc, argv);
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << "\n";
return 1;
}
usage();
return 2;
}