geopro/tools/gpr_poc/main.cpp

3992 lines
177 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 <map>
#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/GprSurveyAssembler.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"
// ---- VTK 离屏渲染 ----
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCubeSource.h>
#include <vtkGPUVolumeRayCastMapper.h>
#include <vtkOpenGLGPUVolumeRayCastMapper.h>
#include <vtkImageActor.h>
#include <vtkImageData.h>
#include <vtkImageMapToColors.h>
#include <vtkImageMapper3D.h>
#include <vtkImageReslice.h>
#include <vtkLookupTable.h>
#include <vtkColorTransferFunction.h>
#include <vtkMultiBlockDataSet.h>
#include <vtkMultiBlockVolumeMapper.h>
#include <vtkNew.h>
#include <vtkPiecewiseFunction.h>
#include <vtkVolumeProperty.h>
#include <vtkObjectFactory.h>
#include <vtkOutputWindow.h>
#include <vtkPNGWriter.h>
#include <vtkPointData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkCallbackCommand.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h>
#include <vtkRenderer.h>
#include <vtkShortArray.h>
#include <vtkSmartPointer.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkUnsignedCharArray.h>
#include <vtkVolume.h>
#include <vtkWindowToImageFilter.h>
#ifdef _WIN32
#include <windows.h> // SetConsoleOutputCP修中文控制台 GBK 乱码)
#endif
namespace fs = std::filesystem;
using geopro::tools::Probe;
using geopro::tools::Stopwatch;
namespace {
constexpr int kChannels = 14;
// ---- 命令行参数解析(极简 --key value----
struct Args {
std::map<std::string, std::string> kv;
std::vector<std::string> positional;
std::string get(const std::string& key, const std::string& def) const {
auto it = kv.find(key);
return it != kv.end() ? it->second : def;
}
};
Args parseArgs(int argc, char** argv, int start) {
Args a;
for (int i = start; i < argc; ++i) {
std::string tok = argv[i];
if (tok.rfind("--", 0) == 0) {
// 下一个 token 也是 --xxx或本 token 是末尾)→ 本 token 是无值布尔旗标,
// 存空串(避免把后面的旗标误当成本旗标的值,如 --preview --near
const bool hasValue =
(i + 1 < argc) && std::string(argv[i + 1]).rfind("--", 0) != 0;
a.kv[tok.substr(2)] = hasValue ? argv[i + 1] : "";
if (hasValue) ++i;
} else {
a.positional.push_back(tok);
}
}
return a;
}
// 把一行指标追加写入 last-metrics.txt与可执行同目录的工具源目录无关
// 写到当前工作目录便于汇总CSV 风格一行)。
void writeMetricLine(const std::string& line) {
std::ofstream f("last-metrics.txt", std::ios::app);
if (f) f << line << "\n";
}
// 发现某线 14 通道 .iprb按通道号 A01..A14 排序)+ 该线 .ord。
struct LineFiles {
std::vector<std::string> iprb; // 已按通道号排序
std::string ord;
};
LineFiles discoverLine(const std::string& dir, const std::string& line) {
LineFiles lf;
std::map<int, std::string> byChannel; // 通道号 → 路径(自动按号排序)
std::string ordPath;
for (const auto& e : fs::directory_iterator(dir)) {
if (!e.is_regular_file()) continue;
const std::string name = e.path().filename().string();
const std::string ext = e.path().extension().string();
// .ord优先匹配本线含 "_<line>" 且以 .ord 结尾),否则记下工区任一 .ord 作兜底。
if (ext == ".ord") {
if (name.find("_" + line + ".") != std::string::npos) {
ordPath = e.path().string();
} else if (ordPath.empty()) {
ordPath = e.path().string();
}
continue;
}
// .iprb匹配 "*_<line>_A<NN>.iprb"。
if (ext != ".iprb") continue;
const std::string tag = "_" + line + "_A";
const std::size_t pos = name.find(tag);
if (pos == std::string::npos) continue;
const std::size_t numStart = pos + tag.size();
std::size_t numEnd = numStart;
while (numEnd < name.size() &&
std::isdigit(static_cast<unsigned char>(name[numEnd]))) {
++numEnd;
}
if (numEnd == numStart) continue;
const int ch = std::stoi(name.substr(numStart, numEnd - numStart));
byChannel[ch] = e.path().string();
}
for (const auto& [ch, path] : byChannel) lf.iprb.push_back(path);
lf.ord = ordPath;
return lf;
}
// 由 survey 推 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]\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 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 << " 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);
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 << "测线数 : " << 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;
}
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 更亮、对比更狠,正负反射一眼分开。
geopro::core::ColorScale makeSeismicColorScale(double vmin, double vmax) {
geopro::core::ColorScale cs;
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
auto at = [&](double t) { return vmin + span * t; };
cs.addStop(at(0.00), geopro::core::Rgba{30, 60, 255, 255}); // 亮蓝(强负)
cs.addStop(at(0.30), geopro::core::Rgba{120, 180, 255, 255}); // 浅蓝
cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 白(零)
cs.addStop(at(0.70), geopro::core::Rgba{255, 170, 120, 255}); // 浅橙
cs.addStop(at(1.00), geopro::core::Rgba{255, 40, 30, 255}); // 亮红(强正)
return cs;
}
// jet 类高饱和色阶(蓝-青-绿-黄-红):全程高亮高饱和,最大化色彩动态范围,
// 弱信号也能映到鲜明色相,适合「一眼铺满层次」的取向。
geopro::core::ColorScale makeJetColorScale(double vmin, double vmax) {
geopro::core::ColorScale cs;
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
auto at = [&](double t) { return vmin + span * t; };
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 200, 255}); // 蓝
cs.addStop(at(0.25), geopro::core::Rgba{0, 200, 255, 255}); // 青
cs.addStop(at(0.50), geopro::core::Rgba{0, 230, 60, 255}); // 绿
cs.addStop(at(0.75), geopro::core::Rgba{255, 230, 0, 255}); // 黄
cs.addStop(at(1.00), geopro::core::Rgba{255, 30, 0, 255}); // 红
return cs;
}
// 「实体感」不透明度包络Task 12d gallery与 structural 双端斜坡不同,这里让
// 中高值段普遍可见——背景(近零)仍压低但不归零,中高段从 floorOpacity 平滑升到
// maxOpacity使体读起来像半透明实心块、内部层次而非只剩两端薄壳可见。
// floorOpacity近零背景的最低不透明度0.05~0.12,压住但不消失)
// maxOpacity 强反射端的不透明度峰值0.85 时近实心)
// midOpacity 中值段半幅处的不透明度0.3~0.5,决定「半透明实心」观感)
vtkSmartPointer<vtkVolumeProperty> makeSolidVolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys, double floorOpacity, double midOpacity,
double maxOpacity) {
constexpr int kTransferSamples = 64;
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
const double qminD = static_cast<double>(q.toQ(vminPhys));
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
vtkNew<vtkColorTransferFunction> color;
for (int t = 0; t < kTransferSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
const double phys = q.toPhys(qvLevel);
const auto c = cs.colorAt(phys);
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
}
// 不透明度V 形(中段=零附近背景=floor正负两端=max但全程 ≥floor 且中值
// 段≈mid → 整体半透明实心、内部层次可见,而非两端薄壳。
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
const double qmid = 0.5 * (qminD + qmaxD);
const double half = 0.5 * (qmaxD - qminD);
opacity->AddPoint(qminD, maxOpacity); // 强负反射:近实心
opacity->AddPoint(qmid - 0.55 * half, midOpacity); // 中负段:半透明实心
opacity->AddPoint(qmid, floorOpacity); // 近零背景:压低但可见
opacity->AddPoint(qmid + 0.55 * half, midOpacity); // 中正段:半透明实心
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:近实心
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
prop->SetColor(color);
prop->SetScalarOpacity(opacity);
prop->SetInterpolationTypeToLinear();
prop->ShadeOff();
return prop;
}
// 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。
// 不透明度调高时光线提前终止fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。
vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys, double maxOpacity,
bool structuralOpacity = true) {
constexpr int kTransferSamples = 64;
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
const double qminD = static_cast<double>(q.toQ(vminPhys));
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
vtkNew<vtkColorTransferFunction> color;
for (int t = 0; t < kTransferSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
const double phys = q.toPhys(qvLevel);
const auto c = cs.colorAt(phys);
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
}
// 不透明度:
// - 原始(structuralOpacity=false):线性单斜坡 [qmin,qmax]→[0,maxOpacity]
// 与 VoxelActor 默认一致,作调优前对照基线。
// - 调优(structuralOpacity=true)双端斜坡。GPR/地震体值多集中在零附近(背景)
// 强反射在正负两端;线性单斜坡会让占多数的近零背景填满体、遮住结构。改为
// 「中段(零附近)透明 + 正负两端不透明」——抑制背景、凸显强反射层,截面结构才看得出。
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
if (structuralOpacity) {
const double qmid = 0.5 * (qminD + qmaxD);
const double half = 0.5 * (qmaxD - qminD);
opacity->AddPoint(qminD, maxOpacity); // 强负反射:不透明
opacity->AddPoint(qmid - 0.30 * half, 0.0); // 近零背景:透明
opacity->AddPoint(qmid + 0.30 * half, 0.0);
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:不透明
} else {
opacity->AddPoint(qminD, 0.0);
opacity->AddPoint(qmaxD, maxOpacity);
}
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
prop->SetColor(color);
prop->SetScalarOpacity(opacity);
prop->SetInterpolationTypeToLinear();
prop->ShadeOff();
return prop;
}
// 由预构建 VTK_SHORT 图像建一个「视觉调优」体:自定义不透明度 + 垂向夸张。
// 垂向夸张用 vtkVolume::SetScale(1, exagg, exagg) 缩放跨通道(Y)与深度(Z)两薄轴,
// 不改图像数据;体物理极扁(X≈2.2km vs Y≈1.5m/Z≈8m),放大薄轴截面结构才看得出。
vtkSmartPointer<vtkVolume> buildTunedVolume(vtkImageData* shortImg,
const geopro::core::Quant& q,
const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys,
double maxOpacity, double exagg,
bool structuralOpacity = true) {
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetInputData(shortImg);
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto prop = makeTunedVolumeProperty(q, cs, vminPhys, vmaxPhys, maxOpacity,
structuralOpacity);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:放大 Y/Z 薄轴
return volume;
}
int cmdRenderB(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc renderB <storeDir> [--frames 120]\n";
return 2;
}
const std::string dir = a.positional[0];
const int frames = std::stoi(a.get("frames", "120"));
std::cout << "[renderB] storeDir=" << dir << " frames=" << frames << "\n";
// 闸门复检renderB 前先确认离屏可用(避免在不可渲染机上跑出假数据)。
std::cout << "[renderB] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[renderB] 闸门失败,中止,不产出 fps。\n";
return 1;
}
// 1) 加载整卷VTK_SHORT
Stopwatch swLoad;
geopro::render::WholeVolumeSource src(dir);
const double loadMs = swLoad.elapsedMs();
const auto& m = src.meta();
const std::int64_t voxels =
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
std::cout << "[renderB] 整卷 " << m.nx << "x" << m.ny << "x" << m.nz
<< " 体素=" << voxels << " 字节=" << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
<< "ms\n";
auto images = src.currentImages();
if (images.empty() || !images.front()) {
std::cerr << "[renderB] 错误: currentImages 为空\n";
return 1;
}
vtkImageData* shortImg = images.front().Get();
// 色阶用 meta 的物理区间。
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
// 2) 体绘制(离屏)。
auto rw = makeOffscreenWindow(1024, 768);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
vtkSmartPointer<vtkVolume> volume =
geopro::render::buildVoxelI16FromImage(shortImg, m.quant, cs, vmin, vmax);
ren->AddVolume(volume);
// 装上捕获式 OutputWindow拦截体绘制时的 3D 纹理维度错误。
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
std::cout << "[renderB] 体绘制基准(" << frames << " 帧旋转相机)...\n";
const double volFpsRaw = benchVolumeFps(rw, ren, frames);
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr); // 还原默认输出窗口
// 进显存判据SmartVolumeMapper 实际用的渲染模式2=GPURenderMode
int renderMode = -1;
bool lowResResample = false;
if (auto* svm =
vtkSmartVolumeMapper::SafeDownCast(volume->GetMapper())) {
renderMode = svm->GetLastUsedRenderMode();
// 大体可能触发降质重采样GPU 显存不足时 SmartVolumeMapper 走低分辨率)。
lowResResample = (svm->GetInteractiveAdjustSampleDistances() == 0 &&
renderMode != vtkSmartVolumeMapper::GPURenderMode);
}
const bool onGpu = (renderMode == vtkSmartVolumeMapper::GPURenderMode);
// 任一维度超过 GL_MAX_3D_TEXTURE_SIZE本机实测 16384→ 整卷无法成单张 3D 纹理。
constexpr int kMax3DTexObserved = 16384;
const bool dimOversize =
(m.nx > kMax3DTexObserved || m.ny > kMax3DTexObserved ||
m.nz > kMax3DTexObserved);
// 体绘制 fps 是否可信:上传成功(无纹理错误且未超限)才算真实整卷体绘制帧率。
const bool volFpsValid = !textureErr && !dimOversize;
const double volFps = volFpsValid ? volFpsRaw : -1.0;
std::cout << "[renderB] 体绘制 raw_fps=" << volFpsRaw
<< " 渲染模式=" << renderMode << (onGpu ? "(GPU)" : "(非GPU)")
<< " 纹理维度错误=" << (textureErr ? "" : "")
<< " 超 16384=" << (dimOversize ? "" : "") << "\n";
if (!volFpsValid) {
std::cout << "[renderB] 警告: 整卷未能成单张 3D 纹理X=" << m.nx
<< " > " << kMax3DTexObserved
<< "),体绘制 fps 无意义 → 标 INVALID。\n";
}
// 3) 切片扫描(离屏,沿 Z 扫整卷)。
vtkNew<vtkRenderer> ren2;
ren2->SetBackground(0.0, 0.0, 0.0);
auto rw2 = makeOffscreenWindow(1024, 768);
rw2->AddRenderer(ren2);
vtkSmartPointer<vtkLookupTable> lut = makeLut(cs, vmin, vmax);
std::cout << "[renderB] 切片扫描基准(" << frames << " 帧沿 Z 推进)...\n";
const double sliceFps =
benchSliceFps(rw2, ren2, src.sliceSource(), lut, frames);
std::cout << "[renderB] 切片 fps=" << sliceFps << "\n";
const double peak = Probe::peakMemMB();
const std::string vram = "N/A"; // VTK 安装未带 GLEW 头,无法直查 NVX 显存
// 4) 汇总打印。
const std::string volFpsStr =
volFpsValid ? std::to_string(volFps) : "INVALID(整卷超 3D 纹理上限)";
std::cout << "\n=== renderB GPU 指标 ===\n";
std::cout << "离屏闸门 : OK\n";
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
<< "\n";
std::cout << "体素数 : " << voxels << "\n";
std::cout << "整卷字节(B) : " << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
std::cout << "体绘制 fps : " << volFpsStr << "\n";
if (!volFpsValid) {
std::cout << " (raw_fps=" << volFpsRaw
<< " 为空纹理渲染X=" << m.nx << " > 16384不可信)\n";
}
std::cout << "切片扫描 fps : " << sliceFps << " (2D 纹理,无 3D 上限约束)\n";
std::cout << "渲染模式 : " << renderMode
<< (onGpu ? " (GPU 路径)" : " (非 GPU)") << "\n";
std::cout << "整卷进显存 : "
<< (volFpsValid && onGpu ? "是(单张 3D 纹理)"
: "否(超 GL_MAX_3D_TEXTURE_SIZE 16384")
<< "\n";
std::cout << "降质重采样 : " << (lowResResample ? "" : "") << "\n";
std::cout << "GPU 显存 : " << vram << "\n";
std::cout << "进程峰值内存(MB): " << peak << "\n";
writeMetricLine(
"renderB,dir=" + dir + ",nx=" + std::to_string(m.nx) +
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
",voxels=" + std::to_string(voxels) +
",wholeB=" + std::to_string(wholeBytes) +
",volFps=" + volFpsStr +
",volFpsRaw=" + std::to_string(volFpsRaw) +
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
",sliceFps=" + std::to_string(sliceFps) +
",renderMode=" + std::to_string(renderMode) +
",onGpu=" + std::to_string(onGpu ? 1 : 0) +
",loadMs=" + std::to_string(loadMs) +
",peakMB=" + std::to_string(peak));
return 0;
}
// ============================================================================
// 核外分块体绘制基准POC-C命门探针
// ============================================================================
// 量化域传函(与 VoxelActor::buildVoxelI16FromImage 同逻辑):颜色对每量化级 qv 用
// q.toPhys(qv) 反查 ColorScale;不透明度 kBlank→0、[qmin,qmax] 线性到 kMaxOpacity。
// MultiBlock 全块共用同一 vtkVolumeProperty(挂在单个 vtkVolume 上)。
vtkSmartPointer<vtkVolumeProperty> makeI16VolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys) {
constexpr int kTransferSamples = 64;
constexpr double kMaxOpacity = 0.15;
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
const double qminD = static_cast<double>(q.toQ(vminPhys));
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
vtkNew<vtkColorTransferFunction> color;
for (int t = 0; t < kTransferSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
const double phys = q.toPhys(qvLevel);
const auto c = cs.colorAt(phys);
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
}
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
opacity->AddPoint(qminD, 0.0);
opacity->AddPoint(qmaxD, kMaxOpacity);
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
prop->SetColor(color);
prop->SetScalarOpacity(opacity);
prop->SetInterpolationTypeToLinear();
prop->ShadeOff();
return prop;
}
// 由当前工作集图像组装 vtkMultiBlockDataSet(每块一个 vtkImageData)。
vtkSmartPointer<vtkMultiBlockDataSet> makeMultiBlock(
const std::vector<vtkSmartPointer<vtkImageData>>& imgs) {
auto mb = vtkSmartPointer<vtkMultiBlockDataSet>::New();
mb->SetNumberOfBlocks(static_cast<unsigned int>(imgs.size()));
for (unsigned int i = 0; i < imgs.size(); ++i) {
mb->SetBlock(i, imgs[i].Get());
}
return mb;
}
int cmdRenderC(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n";
return 2;
}
const std::string dir = a.positional[0];
const std::size_t budget =
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
const int frames = std::stoi(a.get("frames", "120"));
std::cout << "[renderC] storeDir=" << dir << " budget=" << budget
<< " frames=" << frames << "\n";
// 闸门复检:不可渲染机不产假 fps。
std::cout << "[renderC] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[renderC] 闸门失败,中止,不产出 fps。\n";
return 1;
}
// 1) 核外源(读 meta + 建 pager,不载整卷)。
Stopwatch swLoad;
geopro::render::OutOfCoreSource src(dir, budget);
const double loadMs = swLoad.elapsedMs();
const auto& m = src.meta();
const std::int64_t voxels =
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
const int winW = 1024, winH = 768;
src.setAspect(static_cast<double>(winW) / winH);
std::cout << "[renderC] 体 " << m.nx << "x" << m.ny << "x" << m.nz
<< " 体素=" << voxels << " (整卷 X=" << m.nx
<< " > 16384 → renderB INVALID),源构造 " << loadMs << "ms\n";
// 色阶用 meta 物理区间。
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
// 2) 离屏 + MultiBlock 体绘制。
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
vtkNew<vtkMultiBlockVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
ren->AddVolume(volume);
// 装捕获式 OutputWindow:拦截每块上传时的 3D 纹理维度错误(应无,因块 ≤64³)。
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// 相机:先以全体定向(看整卷),首帧 update 选出工作集后再 ResetCamera 到
// 实际驻留块的 mapper 包围盒(budget<视野总块时工作集只覆盖体的一部分,框住它
// 才能确证核外体绘制真渲出;这是 budget 受限下的诚实测法,报告说明)。
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
m.origin[1], m.origin[1] + m.ny * m.spacing[1],
m.origin[2], m.origin[2] + m.nz * m.spacing[2]);
vtkCamera* cam = ren->GetActiveCamera();
auto refreshBlocks = [&]() {
src.update(cam);
auto imgs = src.currentImages();
auto mb = makeMultiBlock(imgs);
mapper->SetInputDataObject(mb);
mapper->Update();
return imgs.size();
};
const std::size_t warmBlocks = refreshBlocks();
// 用工作集(mapper)实际包围盒重置相机,框住驻留块。
{
double b[6];
mapper->GetBounds(b);
if (b[0] <= b[1]) {
ren->ResetCamera(b);
} else {
ren->ResetCamera();
}
}
rw->Render(); // 预热(上传显存 + 编译 shader,不计时)
{
double b[6];
mapper->GetBounds(b);
std::cout << "[renderC] 工作集包围盒 x[" << b[0] << "," << b[1] << "] y["
<< b[2] << "," << b[3] << "] z[" << b[4] << "," << b[5] << "]\n";
}
std::cout << "[renderC] 预热:level=" << src.lastLevel()
<< " 视野块=" << src.lastVisibleCount() << "/"
<< src.lastLevelBrickTotal()
<< " 驻留=" << src.residentCount() << " 渲染块=" << warmBlocks
<< "\n";
std::size_t maxResident = src.residentCount();
std::size_t sumBlocks = 0;
// 3a) 静态工作集体绘制 fps:工作集固定(不每帧换块),只旋相机 + Render。
// 隔离"纯 GPU MultiBlock 体绘制"成本(剔除分块换页/解压/重建 mapper 开销),
// 直接对照 renderB 整卷 fps,回答未知 #6(真实体绘制 fps)。
std::cout << "[renderC] 静态工作集体绘制基准(" << frames << " 帧旋相机)...\n";
Stopwatch swStatic;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
rw->Render(); // 工作集不变,仅旋转
}
const double staticMs = swStatic.elapsedMs();
const double staticFps = staticMs > 0 ? frames * 1000.0 / staticMs : 0.0;
std::cout << "[renderC] 静态工作集 fps=" << staticFps << "\n";
// 3b) 动态换页体绘制 fps:每帧 update(cam)(重选 LOD/视野块,含 qUncompress 解压
// 换入的块 + 重建 MultiBlock)+ Render。回答未知 #4(热路径解压是否拖垮 fps)
// 与 #5(内存恒定)。同时累计 update 耗时占比。
std::cout << "[renderC] 动态换页体绘制基准(" << frames << " 帧旋相机)...\n";
double updateMsTotal = 0.0;
Stopwatch swDyn;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
Stopwatch swU;
const std::size_t blocks = refreshBlocks(); // update + 重建 MultiBlock
updateMsTotal += swU.elapsedMs();
sumBlocks += blocks;
maxResident = std::max(maxResident, src.residentCount());
rw->Render();
}
const double dynMs = swDyn.elapsedMs();
const double dynFps = dynMs > 0 ? frames * 1000.0 / dynMs : 0.0;
const double rawFps = dynFps; // 主报告口径:含换页的真实交互 fps
std::cout << "[renderC] 动态换页 fps=" << dynFps
<< " (其中 update/换页/重建 平均 " << (updateMsTotal / frames)
<< " ms/帧)\n";
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
// 4) 正确性判据:渲出非空像素(非全背景)。
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, pixels);
vtkIdType nonBlack = 0;
const vtkIdType npx = pixels->GetNumberOfTuples();
for (vtkIdType i = 0; i < npx; ++i) {
if (pixels->GetComponent(i, 0) > 10 || pixels->GetComponent(i, 1) > 10 ||
pixels->GetComponent(i, 2) > 10) {
++nonBlack;
}
}
const bool renderedNonEmpty = (nonBlack > 0);
// 渲染模式(MultiBlock 内部每块一个 SmartVolumeMapper;此处取一块代表性查询)。
// MultiBlock 不直接暴露 LastUsedRenderMode,故以纹理无错 + 非空像素为体绘制真出证据。
const bool volFpsValid = !textureErr && renderedNonEmpty;
const double peak = Probe::peakMemMB();
const double avgBlocks =
frames > 0 ? static_cast<double>(sumBlocks) / frames : 0.0;
std::cout << "\n=== renderC 核外体绘制指标 ===\n";
std::cout << "离屏闸门 : OK\n";
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
<< " (整卷 X 超 16384,renderB=INVALID)\n";
std::cout << "体素数 : " << voxels << "\n";
std::cout << "budget(块) : " << budget << "\n";
std::cout << "峰值驻留(块) : " << maxResident
<< (maxResident <= budget ? " (≤budget,内存恒定 OK)"
: " (!! 超 budget)")
<< "\n";
std::cout << "末帧 level : " << src.lastLevel() << "\n";
std::cout << "末帧视野块/总块 : " << src.lastVisibleCount() << " / "
<< src.lastLevelBrickTotal() << "\n";
std::cout << "平均渲染块/帧 : " << avgBlocks << "\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "") << "\n";
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "" : "否(!!)")
<< " (非背景像素=" << nonBlack << ")\n";
std::cout << "静态工作集 fps : "
<< (volFpsValid ? std::to_string(staticFps)
: std::string("INVALID(纹理错或空渲染)"))
<< " (纯 GPU MultiBlock 体绘制)\n";
std::cout << "动态换页 fps : "
<< (volFpsValid ? std::to_string(dynFps)
: std::string("INVALID(纹理错或空渲染)"))
<< " (含每帧 update/解压/重建 mapper)\n";
std::cout << " 换页均耗时/帧 : " << (updateMsTotal / frames) << " ms\n";
std::cout << "进程峰值内存(MB) : " << peak << "\n";
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
std::cout << "对照 renderB : 整卷 INVALID(超 3D 纹理上限);renderC "
<< (volFpsValid ? "真渲出 ✔" : "未渲出 ✘") << "\n";
writeMetricLine(
"renderC,dir=" + dir + ",nx=" + std::to_string(m.nx) +
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
",voxels=" + std::to_string(voxels) +
",budget=" + std::to_string(budget) +
",maxResident=" + std::to_string(maxResident) +
",lastLevel=" + std::to_string(src.lastLevel()) +
",lastVisible=" + std::to_string(src.lastVisibleCount()) +
",lastLevelTotal=" + std::to_string(src.lastLevelBrickTotal()) +
",avgBlocks=" + std::to_string(avgBlocks) +
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
",nonBlack=" + std::to_string(nonBlack) +
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
",staticFps=" + (volFpsValid ? std::to_string(staticFps) : "INVALID") +
",dynFps=" + (volFpsValid ? std::to_string(dynFps) : "INVALID") +
",updateMsPerFrame=" + std::to_string(updateMsTotal / frames) +
",rawFps=" + std::to_string(rawFps) +
",loadMs=" + std::to_string(loadMs) +
",peakMB=" + std::to_string(peak));
return volFpsValid ? 0 : 1;
}
// ============================================================================
// 单 mapper SetPartitions 整卷体绘制基准POC-C-partitioned,去风险探针)
// ============================================================================
//
// 验"对的架构":整卷喂【单个】vtkGPUVolumeRayCastMapper(其 OpenGL 实现 =
// vtkOpenGLGPUVolumeRayCastMapper),用 SetPartitions(ceil(nx/16384),...) 让同一
// mapper 内部把体沿轴分区上传(每区 ≤16384 绕过 GL_MAX_3D_TEXTURE_SIZE),一次
// ray cast。对照 9c 整卷单 SmartVolumeMapper(INVALID,纹理墙) 与 12 MultiBlock
// (每块一 mapper,9.5 静态/1.45 换页)。
//
// 双闸(同 9c,绝不把空纹理假帧率当性能):
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
int cmdRenderCPartitioned(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr
<< "用法: gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
return 2;
}
const std::string dir = a.positional[0];
const int frames = std::stoi(a.get("frames", "120"));
std::cout << "[renderC-partitioned] storeDir=" << dir << " frames=" << frames
<< "\n";
// 闸门复检:不可渲染机不产假 fps。
std::cout << "[renderC-partitioned] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[renderC-partitioned] 闸门失败,中止,不产出 fps。\n";
return 1;
}
// 1) WholeVolumeSource 重组整卷 VTK_SHORT image(常驻内存,约 400MB)。
Stopwatch swLoad;
geopro::render::WholeVolumeSource src(dir);
const double loadMs = swLoad.elapsedMs();
const auto& m = src.meta();
const std::int64_t voxels =
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
std::cout << "[renderC-partitioned] 整卷 " << m.nx << "x" << m.ny << "x"
<< m.nz << " 体素=" << voxels << " 字节=" << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
<< "ms\n";
auto images = src.currentImages();
if (images.empty() || !images.front()) {
std::cerr << "[renderC-partitioned] 错误: currentImages 为空\n";
return 1;
}
vtkImageData* shortImg = images.front().Get();
// 2) 分区数:任一轴 > 16384 → ceil(dim/16384) 个分区,其余轴 1。
constexpr int kMax3DTex = 16384;
auto partCount = [](int dim) {
return static_cast<unsigned short>((dim + kMax3DTex - 1) / kMax3DTex);
};
const unsigned short px = partCount(m.nx);
const unsigned short py = partCount(m.ny);
const unsigned short pz = partCount(m.nz);
std::cout << "[renderC-partitioned] SetPartitions(" << px << "," << py << ","
<< pz << ") 每区上限 ≤" << kMax3DTex << " (沿线 " << m.nx << "/"
<< px << "=" << (m.nx + px - 1) / px << ")\n";
// 3) 量化域传函(复用现有 makeI16VolumeProperty:qmin/qmax + kBlank 透明)。
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
// 4) 离屏 + 单个 GPU ray cast mapper + SetPartitions。
const int winW = 1024, winH = 768;
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
// vtkGPUVolumeRayCastMapper 抽象基类无 SetPartitions(在 OpenGL 实现上);
// 直接建 OpenGL 具体类(工厂默认产物同此),喂【整卷单 image】不预切块。
vtkNew<vtkOpenGLGPUVolumeRayCastMapper> mapper;
mapper->SetInputData(shortImg);
mapper->SetPartitions(px, py, pz);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
ren->AddVolume(volume);
// 装捕获式 OutputWindow:拦截分区上传时的 3D 纹理维度错误。
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// 相机:用 mapper 实际包围盒定向(整卷,非工作集);体极扁长(44476:29:162),
// ResetCamera 全体后再倾斜抬高视角,让薄维度可见(否则边缘视角近乎不可见)。
{
double b[6];
mapper->GetBounds(b);
if (b[0] <= b[1]) {
ren->ResetCamera(b);
} else {
ren->ResetCamera();
}
}
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(30.0); // 抬高,避免纯边缘视角看不到薄板
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
// 每帧旋相机 + Render 测 fps;同时多帧采样非背景像素取最大值
// (区分"真渲不出"与"末帧恰好边缘视角空"——后者只是采样时机)。
auto countNonBlack = [&]() -> vtkIdType {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
vtkIdType nb = 0;
const vtkIdType np = px->GetNumberOfTuples();
for (vtkIdType i = 0; i < np; ++i) {
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
px->GetComponent(i, 2) > 10) {
++nb;
}
}
return nb;
};
std::cout << "[renderC-partitioned] 单 mapper 整卷体绘制基准(" << frames
<< " 帧旋相机)...\n";
rw->Render(); // 预热(分区上传 + 编译 shader,不计时)
vtkIdType maxNonBlack = countNonBlack();
const int sampleEvery = std::max(1, frames / 8);
Stopwatch swBench;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
rw->Render();
if (f % sampleEvery == 0) {
maxNonBlack = std::max(maxNonBlack, countNonBlack());
}
}
const double benchMs = swBench.elapsedMs();
const double volFpsRaw =
benchMs > 0.0 ? frames * 1000.0 / benchMs : 0.0;
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
// 5) 正确性判据:整个旋转扫描中的最大非背景像素(非空才算真渲出)。
const vtkIdType nonBlack = maxNonBlack;
const bool renderedNonEmpty = (nonBlack > 0);
// 双闸:无纹理错 + 非空像素 → fps 可信。
const bool volFpsValid = !textureErr && renderedNonEmpty;
const double volFps = volFpsValid ? volFpsRaw : -1.0;
const double peak = Probe::peakMemMB();
const bool interactive = volFpsValid && volFps >= 15.0;
const std::string volFpsStr =
volFpsValid ? std::to_string(volFps)
: std::string("INVALID(纹理错或空渲染)");
std::cout << "\n=== renderC-partitioned 单 mapper SetPartitions 指标 ===\n";
std::cout << "离屏闸门 : OK\n";
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
<< "\n";
std::cout << "体素数 : " << voxels << "\n";
std::cout << "整卷字节(B) : " << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
std::cout << "分区数(px,py,pz) : " << px << "," << py << "," << pz << "\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "") << "\n";
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "" : "否(!!)")
<< " (非背景像素=" << nonBlack << ")\n";
std::cout << "体绘制 fps : " << volFpsStr << "\n";
if (!volFpsValid) {
std::cout << " (raw_fps=" << volFpsRaw << " 不可信)\n";
}
std::cout << "达交互级(≥15fps) : "
<< (interactive ? "是 ✔" : "否 ✘") << "\n";
std::cout << "进程峰值内存(MB) : " << peak << "\n";
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
std::cout << "对照 renderB : 整卷单 SmartVolumeMapper=INVALID(纹理墙);"
"renderC MultiBlock=9.5 静态/1.45 换页;本探针="
<< (volFpsValid ? volFpsStr + "fps" : "INVALID") << "\n";
writeMetricLine(
"renderC-partitioned,dir=" + dir + ",nx=" + std::to_string(m.nx) +
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
",voxels=" + std::to_string(voxels) +
",wholeB=" + std::to_string(wholeBytes) +
",px=" + std::to_string(px) + ",py=" + std::to_string(py) +
",pz=" + std::to_string(pz) +
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
",nonBlack=" + std::to_string(nonBlack) +
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
",volFps=" + volFpsStr + ",volFpsRaw=" + std::to_string(volFpsRaw) +
",interactive=" + std::to_string(interactive ? 1 : 0) +
",loadMs=" + std::to_string(loadMs) + ",peakMB=" + std::to_string(peak));
// 写报告文件(覆盖式,含对照表)。
{
const fs::path repo =
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
fs::create_directories(repo.parent_path());
std::ofstream rf(repo.string());
if (rf) {
rf << "# POC-C 单 mapper SetPartitions 整卷体绘制探针结果\n\n";
rf << "## 体\n";
rf << "- 维度: " << m.nx << " x " << m.ny << " x " << m.nz << " (体素 "
<< voxels << ")\n";
rf << "- 整卷字节: " << wholeBytes << " B ("
<< wholeBytes / (1024.0 * 1024.0) << " MB, VTK_SHORT)\n";
rf << "- store: " << dir << "\n\n";
rf << "## 单 mapper SetPartitions\n";
rf << "- mapper: vtkOpenGLGPUVolumeRayCastMapper (整卷单 image,不预切块)\n";
rf << "- 分区数: SetPartitions(" << px << ", " << py << ", " << pz
<< ") 每区上限 ≤" << kMax3DTex << "\n";
rf << "- 纹理维度错误: " << (textureErr ? "" : "") << "\n";
rf << "- 渲出非空像素: " << (renderedNonEmpty ? "" : "") << " (非背景像素 "
<< nonBlack << ")\n";
rf << "- 体绘制 fps: " << volFpsStr << "\n";
rf << "- 达交互级(≥15fps): " << (interactive ? "" : "") << "\n";
rf << "- 进程峰值内存: " << peak << " MB\n";
rf << "- 源构造耗时: " << loadMs << " ms\n\n";
rf << "## 对照表\n\n";
rf << "| 路径 | 是否渲出 | fps |\n";
rf << "|---|---|---|\n";
rf << "| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙) | — |\n";
rf << "| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态/1.45 换页 |\n";
rf << "| renderC-partitioned 单 mapper SetPartitions | "
<< (volFpsValid ? "渲出" : "未渲出") << " | "
<< (volFpsValid ? volFpsStr : std::string("INVALID")) << " |\n\n";
rf << "## 判据结论\n";
if (volFpsValid && interactive) {
rf << "单 mapper SetPartitions 整卷体绘制【真渲出且达交互级】(" << volFps
<< " fps ≥15)。C production 路线钉死可行。\n";
} else if (volFpsValid) {
rf << "单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(" << volFps
<< " fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。\n";
} else {
rf << "单 mapper SetPartitions 整卷体绘制【未真渲出】(纹理错="
<< (textureErr ? "" : "") << ",非空像素="
<< (renderedNonEmpty ? "" : "")
<< ")。SetPartitions 未能绕过纹理墙,如实记录。\n";
}
std::cout << "[renderC-partitioned] 报告写入 " << repo.string() << "\n";
}
}
return volFpsValid ? 0 : 1;
}
// ============================================================================
// LOD-fps 探针POC-C 最后一根链子Task 12c
// ============================================================================
//
// 12b 已证整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限fps 杠杆只有 LOD
// (渲更少体素)。本探针在【真实金字塔 store】上验四件事全离屏、双闸防假帧率
// (a) 粗层概览 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 };
struct GalleryVariant {
const char* name; // 文件名后缀view-<name>.png
OpacityProfile profile;
ColorChoice color;
double floorOpacity; // 近零背景不透明度kSolid 用)
double midOpacity; // 中值段不透明度kSolid 用)
double maxOpacity; // 两端峰值不透明度
double exagg; // 垂向夸张
double elevation; // ResetCamera 后 Elevation
double azimuth; // 再 Azimuth
double zoom; // 再 Zoom 填满画面
double bg[3]; // 背景 RGB
const char* desc; // 报告用中文说明
};
// 4 组视觉参数。值经离屏实跑挑出(详见报告)。
const GalleryVariant kGalleryVariants[] = {
// var1高不透明度实体感——seismic 亮配色 + V 形包络(中段 0.40/两端 0.85)
// floor 压到 0.04:近零层间隙近透明,亮层面浮出 → 内部层状结构可读。
{"var1", OpacityProfile::kSolid, ColorChoice::kSeismic,
0.04, 0.40, 0.85, 8.0, 22.0, 28.0, 1.9, {0.05, 0.05, 0.09},
"高不透明度实体感V形包络(floor0.04/mid0.40/max0.85)+seismic 亮配色,"
"半透明实心、内部层次可见"},
// var2高对比配色——jet 全程高饱和 + 中等不透明度 V 形包络。
{"var2", OpacityProfile::kSolid, ColorChoice::kJet,
0.04, 0.32, 0.70, 8.0, 22.0, 28.0, 1.9, {0.06, 0.06, 0.10},
"高对比配色jet 蓝青绿黄红全程高饱和 + 中等 V 形包络(mid0.32/max0.70)"},
// var3居中正对纵截面——低 Elevation/Azimuth 摆平、正对 X-Z 长侧面(层状反射沿
// X 延展最清晰)、Zoom2.0 填满 ~70%floor 压更低让层间隙透明、层面立体。
{"var3", OpacityProfile::kSolid, ColorChoice::kSeismic,
0.03, 0.38, 0.82, 9.0, 10.0, 12.0, 2.0, {0.05, 0.05, 0.09},
"居中正对纵截面:低 El10/Az12 摆平正对 X-Z 长侧面、Zoom2.0 填满视野,"
"floor0.03 凸显层面exagg9 放大薄轴"},
// var4最像真实 GPR 三维图——seismic + 略提背景亮 + 微立体角 + 实体包络。
// 综合最佳选作交互窗口默认kViewDefaultVariant
{"var4", OpacityProfile::kSolid, ColorChoice::kSeismic,
0.035, 0.38, 0.84, 8.0, 18.0, 22.0, 2.0, {0.07, 0.08, 0.11},
"综合最佳seismic + 实体包络(floor0.035/mid0.38/max0.84) + 微立体取景"
"(El18/Az22/Zoom2.0) + 略亮冷灰背景"},
};
// 交互窗口(无 flag 的 view)的默认视觉变体 = 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::kStructural:
default: return makeStructuralColorScale(vmin, vmax);
}
}
// 按变体的不透明度包络建体属性gallery / 交互默认共用DRY。kSolid 走 V 形实体
// 包络(floor/mid/max)kStructural 走双端斜坡(仅 maxOpacity)。
vtkSmartPointer<vtkVolumeProperty> makeVariantProperty(
const GalleryVariant& v, const geopro::core::Quant& q,
const geopro::core::ColorScale& cs, double vmin, double vmax,
double maxOpacity) {
if (v.profile == OpacityProfile::kSolid) {
return makeSolidVolumeProperty(q, cs, vmin, vmax, v.floorOpacity,
v.midOpacity, maxOpacity);
}
return makeTunedVolumeProperty(q, cs, vmin, vmax, maxOpacity);
}
// ============================================================================
// ③ view真窗口可交互给用户肉眼测 + 最低配机跑Task 12d
// ============================================================================
//
// 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂
// OutOfCoreSource相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保
// 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。
// 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。
//
// 离屏 smoke--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。
// 整卷单张 3D 纹理的轴上限(同 renderLOD/renderB 实测 GL_MAX_3D_TEXTURE_SIZE
constexpr int kViewMax3DTex = 16384;
// 单纹理统一渲染Task 12d-singletex
//
// 交互 view 不再用 vtkMultiBlockVolumeMapper + budget 分块(缺块、个位数 fps
// 任何相机位置都只渲【一张 vtkImageData + 单个 vtkSmartVolumeMapper】与 --preview
// 走完全同一条产单图 + 同一 mapper 的路径,保证一致、高 fps与预览 184fps 同档)。
//
// LOD 选层规则(拉近变细、拉远变粗):
// - 远观/中景(相机选中粗层)→ 升到最细的「整卷各轴 ≤16384」层本数据 L2:11119、
// L3:5560整卷重组成一张纹理任何缩放都显示完整体绝不缺块。
// - 拉近(相机选中 level0/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叠在底图上
vtkRenderer* ren = nullptr;
vtkCamera* cam = nullptr;
vtkTextActor* fpsText = nullptr;
vtkRenderWindow* rw = nullptr;
double exagg = 8.0;
int lastLevel = -1;
// 持有当前高清单图引用避免被释放mapper 仅持裸指针)。
vtkSmartPointer<vtkImageData> currentImg;
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
bool inCb = false;
};
// 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();
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 范围level0
(void)bx0;
(void)localBx;
(void)totBricksX;
// 体exagg 后)世界尺寸与中心 + 包围球半径。
const double wx = std::max(1.0, m.nx * m.spacing[0]);
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 = m.origin[0] + 0.5 * wx;
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);
// 相机从 +X 看体中心,距离 = 半径 / tan(半视角) × 余量确保整体落入视锥C1
// selectLod 不会判 empty由视距/分辨率自然选层;近观靠交互再拉近切细层。
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 + dist, cy, cz);
st->cam->SetViewUp(0, 0, 1);
ren->ResetCameraClippingRange();
// 源选层选区 + 重组单图喂 mapper。初始化场景需保证拿到首图 → 阻塞轮询到就绪。
const std::size_t blocks = viewRefreshBlocking(st);
// 框住局部段:用无参 ResetCamera按 actor 的【已 SetScale(1,exagg,exagg)】缩放
// 后包围盒框),相机角度沿用能看出结构的 Elevation/Azimuth再 Zoom 拉近填满画面。
ren->ResetCamera();
st->cam = ren->GetActiveCamera();
st->cam->Elevation(kViewDefaultVariant.elevation); // var4 取景El18
st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景Az22
st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景Zoom2.0 填满画面
ren->ResetCameraClippingRange();
return blocks;
}
// 渲一组画廊变体并存 PNG报告 结构像素 / 平均亮度 / fps。返回 0=OK。
int runGalleryVariant(const std::string& dir, const GalleryVariant& v,
int frames) {
const int winW = 1280, winH = 800;
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeVariantProperty(v, m.quant, cs, vmin, vmax, v.maxOpacity);
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]);
rw->AddRenderer(ren);
vtkNew<vtkMultiBlockVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, v.exagg, v.exagg);
ren->AddVolume(volume);
// 局部段(沿线中段,同 viewSetupDefaultFrame 的取段法)。
const int totBx = store.bricksX(0);
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
vtkSmartPointer<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
std::vector<vtkSmartPointer<vtkImageData>> one{locImg};
auto mb = makeMultiBlock(one);
mapper->SetInputDataObject(mb);
mapper->Update();
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(v.elevation);
cam->Azimuth(v.azimuth);
cam->Zoom(v.zoom);
ren->ResetCameraClippingRange();
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
rw->Render();
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
const std::string pngPath =
(shotDir / (std::string("view-") + v.name + ".png")).string();
savePng(rw.Get(), pngPath);
// 结构像素:任一通道 >50排除暗背景度量「确有体结构」。
auto countStructPixels = [&]() -> vtkIdType {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
vtkIdType n = 0;
const vtkIdType np = px->GetNumberOfTuples();
for (vtkIdType i = 0; i < np; ++i) {
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
px->GetComponent(i, 2) > 50) {
++n;
}
}
return n;
};
const vtkIdType structPx = countStructPixels();
const double bright = meanBrightness(rw.Get(), winW, winH);
rw->Render(); // 预热再测 fps
Stopwatch sw;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
rw->Render();
}
const double ms = sw.elapsedMs();
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
const bool texErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const bool ok = !texErr && structPx > 0;
std::cout << "\n--- gallery " << v.name << " ---\n";
std::cout << "参数 : " << v.desc << "\n";
std::cout << "存图 : " << pngPath << "\n";
std::cout << "结构像素(>50) : " << structPx << " / " << (winW * winH)
<< " (" << (100.0 * structPx / (winW * winH)) << "%)\n";
std::cout << "平均亮度(0-255) : " << bright << "\n";
std::cout << "真实 fps : " << (ok ? std::to_string(fps) : "INVALID")
<< " (" << frames << " 帧旋相机)\n";
std::cout << "结果 : " << (ok ? "OK" : "FAIL") << "\n";
writeMetricLine(
"view-gallery," + std::string(v.name) + ",dir=" + dir +
",profile=" + (v.profile == OpacityProfile::kSolid ? "solid" : "struct") +
",floor=" + std::to_string(v.floorOpacity) +
",mid=" + std::to_string(v.midOpacity) +
",max=" + std::to_string(v.maxOpacity) +
",exagg=" + std::to_string(v.exagg) +
",el=" + std::to_string(v.elevation) +
",az=" + std::to_string(v.azimuth) + ",zoom=" + std::to_string(v.zoom) +
",structPx=" + std::to_string(structPx) +
",bright=" + std::to_string(bright) +
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
",png=" + pngPath);
return ok ? 0 : 1;
}
// view --gallery依次渲全部 4 组变体。
int cmdViewGallery(const std::string& dir, int frames) {
std::cout << "[view --gallery] storeDir=" << dir << " frames=" << frames
<< "\n[view --gallery] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[view --gallery] 闸门失败,中止。\n";
return 1;
}
int rc = 0;
for (const auto& v : kGalleryVariants) {
if (runGalleryVariant(dir, v, frames) != 0) rc = 1;
}
std::cout << "\n[view --gallery] 完成4 张图存于 "
"docs/superpowers/plans/poc-lod-shots/view-var{1..4}.png\n";
return rc;
}
int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke] [--preview] [--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-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");
// 画廊模式Task 12d渲 4 组视觉调参图供挑选。优先于其余路径。
if (hasFlag("gallery")) {
return cmdViewGallery(dir, frames);
}
// 单变体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);
}
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();
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
// 配色/不透明度包络取自 var4seismic + V 形实体包络(floor/mid + opacity 作峰值)。
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeVariantProperty(dv, m.quant, cs, vmin, vmax, opacity);
// 渲染窗口preview/smoke 走离屏,否则真窗口。
vtkSmartPointer<vtkRenderWindow> rw;
if (offscreen) {
rw = makeOffscreenWindow(winW, winH);
} else {
rw = vtkSmartPointer<vtkRenderWindow>::New();
rw->SetSize(winW, winH);
rw->SetWindowName("gpr_poc view —— 核外 LOD 体绘制 (滚轮缩放切 LOD, 左键旋转)");
}
vtkNew<vtkRenderer> ren;
ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景
rw->AddRenderer(ren);
// 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, exagg, exagg); // 同垂向夸张 → 与高清层空间对齐
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, exagg, exagg); // 垂向夸张(默认 var4 exagg
ren->AddVolume(volume); // 后加高清 → 叠在底图上
// 屏幕左上角实时 fps 文本。
vtkNew<vtkTextActor> fpsText;
fpsText->SetInput("fps: -- | LOD level: --");
fpsText->GetTextProperty()->SetFontSize(20);
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
fpsText->SetDisplayPosition(12, winH - 30);
ren->AddViewProp(fpsText);
// 捕获式 OutputWindow拦截块上传纹理错
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
ViewState st;
st.source = &source;
st.mapper = mapper.Get();
st.ren = ren.Get();
st.fpsText = fpsText.Get();
st.rw = rw.Get();
st.exagg = exagg;
// 相机初始定向(修复 1默认框「局部段」而非整卷。整线横截面 1:34框整卷
// 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口
// 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。
std::size_t warm = viewSetupDefaultFrame(&st, ren);
rw->Render();
// C3-6 底图预览(模拟「交互态:只渲常驻粗底图」):隐去高清叠加层、把相机框到整卷
// 包围盒exagg 后),只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、非空。证明
// 拖动/缩放时即使高清全部缺位,底图也独立盖住整个体(永不空白的命脉)。
if (preview && basePreview) {
volume->SetVisibility(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);
// 结构像素计数:背景为深蓝灰(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}。
struct GradStats {
double median = 0, p75 = 0, p90 = 0, p99 = 0, mx = 0;
std::size_t samples = 0;
};
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;
}
// 标量不透明度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]\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 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 == "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 == "polish") return cmdPolish(argc, argv);
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << "\n";
return 1;
}
usage();
return 2;
}