4226 lines
190 KiB
C++
4226 lines
190 KiB
C++
// gpr_poc —— POC-B headless 度量 CLI。
|
||
//
|
||
// 串起整条地基:发现 14 通道 .iprb + .ord → assembleGprSurvey → buildGprVolume
|
||
// → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load),
|
||
// 在真实/合成数据上输出可测的真实指标(耗时/维度/体积/压缩比/加载/峰值内存)。
|
||
//
|
||
// 子命令:
|
||
// gpr_poc build <dir> [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out <storeDir>] [--levels 2]
|
||
// gpr_poc load <storeDir>
|
||
// gpr_poc selftest
|
||
// gpr_poc offscreen-smoke —— 离屏 GL 闸门冒烟
|
||
// gpr_poc renderB <storeDir> [--frames 120] —— 离屏体绘制/切片 fps 基准
|
||
|
||
#include <algorithm>
|
||
#include <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/Gpr3dvVolumeBridge.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 推 GridSpec:X 沿测线,Y 跨通道,Z 深度。
|
||
geopro::core::GridSpec specFromSurvey(const geopro::core::GprSurvey& s,
|
||
double cellXY, double cellZ) {
|
||
geopro::core::GridSpec spec{};
|
||
|
||
const double rangeX =
|
||
(s.ntraces > 1) ? (s.ntraces - 1) * s.dx : 0.0;
|
||
const double y0 = s.channelY.empty() ? 0.0 : s.channelY.front();
|
||
const double y1 = s.channelY.empty() ? 0.0 : s.channelY.back();
|
||
const double rangeY = y1 - y0;
|
||
const double rangeZ =
|
||
(s.samples > 1) ? (s.samples - 1) * s.dz : 0.0;
|
||
|
||
auto cells = [](double range, double cell) {
|
||
if (cell <= 0.0) return 1;
|
||
return static_cast<int>(std::ceil(range / cell)) + 1;
|
||
};
|
||
|
||
spec.ox = s.x0;
|
||
spec.oy = y0;
|
||
spec.oz = s.z0;
|
||
spec.dx = cellXY;
|
||
spec.dy = cellXY;
|
||
spec.dz = cellZ;
|
||
spec.nx = cells(rangeX, cellXY);
|
||
spec.ny = cells(rangeY, cellXY);
|
||
spec.nz = cells(rangeZ, cellZ);
|
||
spec.power = 2.0;
|
||
spec.maxDist = cellXY * 2.0;
|
||
return spec;
|
||
}
|
||
|
||
// 落盘 data.bin 体积(所有 data*.bin 之和,含金字塔各级)。
|
||
std::int64_t storeDataBytes(const std::string& dir) {
|
||
std::int64_t total = 0;
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
if (name.rfind("data", 0) == 0 &&
|
||
e.path().extension().string() == ".bin") {
|
||
total += static_cast<std::int64_t>(e.file_size());
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
int cmdBuild(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const std::string line = a.get("line", "001");
|
||
const double cellXY = std::stod(a.get("cellXY", "0.2"));
|
||
const double cellZ = std::stod(a.get("cellZ", "0.05"));
|
||
const int levels = std::stoi(a.get("levels", "2"));
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / ("gpr_store_" + line)).string());
|
||
|
||
std::cout << "[build] dir=" << dir << " line=" << line
|
||
<< " cellXY=" << cellXY << " cellZ=" << cellZ
|
||
<< " levels=" << levels << " out=" << out << "\n";
|
||
|
||
const LineFiles lf = discoverLine(dir, line);
|
||
std::cout << "[build] 发现通道数=" << lf.iprb.size()
|
||
<< " ord=" << (lf.ord.empty() ? "(无)" : lf.ord) << "\n";
|
||
if (lf.iprb.size() != static_cast<std::size_t>(kChannels)) {
|
||
std::cerr << "[build] 警告: 通道数 != " << kChannels
|
||
<< "(仍按发现数继续)\n";
|
||
}
|
||
if (lf.iprb.empty() || lf.ord.empty()) {
|
||
std::cerr << "[build] 错误: 未发现 .iprb 或 .ord\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 装配
|
||
Stopwatch swAsm;
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(lf.iprb, lf.ord);
|
||
const double asmMs = swAsm.elapsedMs();
|
||
std::cout << "[build] 装配完成 ntraces=" << survey.ntraces
|
||
<< " samples=" << survey.samples
|
||
<< " channels=" << survey.channelY.size()
|
||
<< " dx=" << survey.dx << " dz=" << survey.dz << "\n";
|
||
|
||
// 2) 建体
|
||
const geopro::core::GridSpec spec = specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[build] GridSpec nx=" << spec.nx << " ny=" << spec.ny
|
||
<< " nz=" << spec.nz << " dx=" << spec.dx << " dy=" << spec.dy
|
||
<< " dz=" << spec.dz << " maxDist=" << spec.maxDist << "\n";
|
||
|
||
Stopwatch swBuild;
|
||
geopro::core::BuiltI16 built = geopro::core::buildGprVolume(survey, spec);
|
||
const double buildMs = swBuild.elapsedMs();
|
||
|
||
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(), nz = built.vol.nz();
|
||
const std::int64_t voxels = nx * ny * nz;
|
||
const std::int64_t rawBytes = voxels * 2; // int16
|
||
|
||
// 3) 落盘 + 金字塔
|
||
fs::create_directories(out);
|
||
Stopwatch swWrite;
|
||
geopro::data::ChunkedVolumeStore::write(out, built);
|
||
const double writeMs = swWrite.elapsedMs();
|
||
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramid(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build 指标 ===\n";
|
||
std::cout << "装配耗时(ms) : " << asmMs << "\n";
|
||
std::cout << "建体耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "体维度 : " << nx << " x " << ny << " x " << nz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build,line=" + line + ",cellXY=" + std::to_string(cellXY) +
|
||
",cellZ=" + std::to_string(cellZ) + ",nx=" + std::to_string(nx) +
|
||
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) + ",asmMs=" + std::to_string(asmMs) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",writeMs=" + std::to_string(writeMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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";
|
||
|
||
// 每线网格 spec(origin 沿 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 分 slab(brick 对齐),每 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; // 线局部世界 X(spec.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;
|
||
// 线局部 spec:x0=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 覆盖的合并 brick:X 列 [bcol,bxEnd) → 合并列 +xBrickOffset[i],
|
||
// Y/Z 全程(含该线 ny/nz 之外的合并余量 → blank)。
|
||
for (int bz = 0; bz < bZ; ++bz) {
|
||
for (int by = 0; by < bY; ++by) {
|
||
for (int lbx = bcol; lbx < bxEnd; ++lbx) {
|
||
const int mbx = xBrickOffset[i] + lbx; // 合并 brick X 索引
|
||
const int bw = extentOf(mergedNx, mbx, kStreamBrick);
|
||
const int bh = extentOf(mergedNy, by, kStreamBrick);
|
||
const int bd = extentOf(mergedNz, bz, kStreamBrick);
|
||
std::vector<std::int16_t> voxels(
|
||
static_cast<std::size_t>(bw) * bh * bd);
|
||
// 该 brick 是否落在线自身覆盖范围内(线 brick 网格内)。
|
||
const bool inLine =
|
||
(lbx < lineBx && by < lineBy && bz < lineBz && hasTraces);
|
||
if (!inLine) {
|
||
std::fill(voxels.begin(), voxels.end(),
|
||
geopro::core::ScalarVolumeI16::kBlank);
|
||
} else {
|
||
const int i0 = lbx * kStreamBrick, j0 = by * kStreamBrick,
|
||
k0 = bz * kStreamBrick;
|
||
std::size_t wi = 0;
|
||
for (int kk = 0; kk < bd; ++kk) {
|
||
for (int jj = 0; jj < bh; ++jj) {
|
||
for (int ii = 0; ii < bw; ++ii) {
|
||
const int gi = i0 + ii, gj = j0 + jj, gk = k0 + kk;
|
||
// 线网格之外的余格(合并 brick 比线 brick 大)→ blank。
|
||
if (gi >= spec.nx || gj >= spec.ny || gk >= spec.nz) {
|
||
voxels[wi++] = geopro::core::ScalarVolumeI16::kBlank;
|
||
} else {
|
||
voxels[wi++] = geopro::core::sampleGprPoint(
|
||
slab, localSpec, gi, gj, gk, quant);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
writer.writeBrick(mbx, by, bz, voxels);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
std::cout << "[build-stream] 线 " << lineNos[i] << " 写入完成 ("
|
||
<< (i + 1) << "/" << files.size()
|
||
<< ") 峰值内存(MB)=" << Probe::peakMemMB() << "\n";
|
||
}
|
||
writer.finalize();
|
||
const double buildMs = swBuild.elapsedMs();
|
||
|
||
// 7) 流式金字塔。
|
||
std::cout << "[build-stream] 流式金字塔 levels=" << levels << "...\n";
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramidStreaming(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(mergedNx) * mergedNy * mergedNz;
|
||
const std::int64_t rawBytes = voxels * 2;
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double totalMs = swTotal.elapsedMs();
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build-stream 指标(多线合并大体)===\n";
|
||
std::cout << "合并方式 : 沿 X 顺序排列(退路近似,brick 对齐)\n";
|
||
std::cout << "测线数 : " << files.size() << "\n";
|
||
std::cout << "合并体维度 : " << mergedNx << " x " << mergedNy << " x "
|
||
<< mergedNz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "扫量化耗时(ms) : " << scanMs << "\n";
|
||
std::cout << "建体耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "总耗时(ms) : " << totalMs << "\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build-stream,lines=" + std::to_string(files.size()) +
|
||
",cellXY=" + std::to_string(cellXY) + ",cellZ=" + std::to_string(cellZ) +
|
||
",nx=" + std::to_string(mergedNx) + ",ny=" + std::to_string(mergedNy) +
|
||
",nz=" + std::to_string(mergedNz) + ",voxels=" + std::to_string(voxels) +
|
||
",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) + ",scanMs=" + std::to_string(scanMs) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) +
|
||
",totalMs=" + std::to_string(totalMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// build-geo:按真实 RTK 几何把多线插值进统一路向网格(Task G1)
|
||
// ============================================================================
|
||
//
|
||
// 消除 build-stream 顺序拼接的退化扁带:各线 .gps RTK 轨迹 → 经纬 → 局部米 →
|
||
// PCA 路向旋转 → 道按里程均匀分布定位、14 通道横偏垂直航向摆放 → 全部插进统一
|
||
// 路向网格(≈4472×43×81)重叠取均值 → 量化写 ChunkedVolumeStore + 金字塔。
|
||
int cmdBuildGeo(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] "
|
||
"[--out <storeDir>] [--levels 4] [--maxLines N] "
|
||
"[--curvilinear]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const double cellXY = std::stod(a.get("cellXY", "0.5"));
|
||
const double cellZ = std::stod(a.get("cellZ", "0.1"));
|
||
const int levels = std::stoi(a.get("levels", "4"));
|
||
const int maxLines = std::stoi(a.get("maxLines", "0"));
|
||
const bool curvilinear = a.kv.count("curvilinear") > 0; // 曲线版(G2)
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / "gpr_store_geo").string());
|
||
|
||
std::cout << "[build-geo] dir=" << dir << " cellXY=" << cellXY
|
||
<< " cellZ=" << cellZ << " levels=" << levels
|
||
<< " mode=" << (curvilinear ? "曲线(中心线)" : "PCA")
|
||
<< " out=" << out << "\n";
|
||
|
||
// 发现线号 → 各线 iprb/ord(复用 discoverLine)+ .gps(按线号匹配)。
|
||
std::vector<std::string> lineNos = discoverLines(dir);
|
||
if (maxLines > 0 && static_cast<int>(lineNos.size()) > maxLines)
|
||
lineNos.resize(maxLines);
|
||
std::cout << "[build-geo] 发现测线数=" << lineNos.size() << "\n";
|
||
if (lineNos.empty()) {
|
||
std::cerr << "[build-geo] 错误: 未发现任何 .ord 测线\n";
|
||
return 1;
|
||
}
|
||
|
||
std::vector<geopro::core::GeoLineInput> lines;
|
||
for (const std::string& ln : lineNos) {
|
||
const LineFiles lf = discoverLine(dir, ln);
|
||
if (lf.iprb.empty() || lf.ord.empty()) {
|
||
std::cerr << "[build-geo] 警告: 线 " << ln << " 缺 iprb/ord,跳过\n";
|
||
continue;
|
||
}
|
||
// .gps:匹配 "*_<ln>.gps"。
|
||
std::string gps;
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
if (e.path().extension().string() != ".gps") continue;
|
||
const std::string stem = e.path().stem().string();
|
||
const std::size_t us = stem.find_last_of('_');
|
||
if (us != std::string::npos && stem.substr(us + 1) == ln) {
|
||
gps = e.path().string();
|
||
break;
|
||
}
|
||
}
|
||
if (gps.empty()) {
|
||
std::cerr << "[build-geo] 警告: 线 " << ln << " 缺 .gps,跳过\n";
|
||
continue;
|
||
}
|
||
geopro::core::GeoLineInput in;
|
||
in.iprb = lf.iprb;
|
||
in.ord = lf.ord;
|
||
in.gps = gps;
|
||
lines.push_back(std::move(in));
|
||
}
|
||
if (lines.empty()) {
|
||
std::cerr << "[build-geo] 错误: 无可用测线\n";
|
||
return 1;
|
||
}
|
||
std::cout << "[build-geo] 可用测线数=" << lines.size() << "\n";
|
||
|
||
fs::create_directories(out);
|
||
Stopwatch sw;
|
||
const geopro::core::GeoGridSpec spec{cellXY, cellZ};
|
||
const geopro::core::GeoBuildResult r =
|
||
geopro::core::buildGeoVolume(lines, spec, out, levels, curvilinear);
|
||
const double buildMs = sw.elapsedMs();
|
||
|
||
const double fillRate =
|
||
r.total > 0 ? static_cast<double>(r.filled) / r.total : 0.0;
|
||
const double rotDeg = r.rotRad * 180.0 / 3.14159265358979323846;
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build-geo 指标(真实 RTK 几何统一路向体)===\n";
|
||
std::cout << "网格模式 : " << (curvilinear ? "曲线(中心线展开)" : "PCA 旋转")
|
||
<< "\n";
|
||
std::cout << "测线数 : " << lines.size() << "\n";
|
||
std::cout << "体维度 : " << r.nx << " x " << r.ny << " x " << r.nz
|
||
<< "\n";
|
||
std::cout << "总 cell : " << r.total << "\n";
|
||
std::cout << "非空 cell : " << r.filled << "\n";
|
||
std::cout << "填充率 : " << (fillRate * 100.0) << " %\n";
|
||
std::cout << "路向旋转角 : " << rotDeg << " ° (" << r.rotRad << " rad)\n";
|
||
std::cout << "网格原点(m) : (" << r.oxM << ", " << r.oyM << ")\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "建体总耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build-geo,lines=" + std::to_string(lines.size()) +
|
||
",cellXY=" + std::to_string(cellXY) + ",cellZ=" + std::to_string(cellZ) +
|
||
",nx=" + std::to_string(r.nx) + ",ny=" + std::to_string(r.ny) +
|
||
",nz=" + std::to_string(r.nz) + ",total=" + std::to_string(r.total) +
|
||
",filled=" + std::to_string(r.filled) +
|
||
",fillRate=" + std::to_string(fillRate) +
|
||
",rotDeg=" + std::to_string(rotDeg) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// build-line:走 gpr3dv(P1) 处理链→处理后立方体→geopro 量化体→分块+金字塔(P2)
|
||
// ============================================================================
|
||
//
|
||
// 与 build/build-stream/build-geo 不同:本命令不走 geopro 自家解析+采样核建体,
|
||
// 而是【复用 vendored gpr3dv 的读+处理链】(算法零改动)产出处理后立方体
|
||
// volumeData[通道][道][样本],再桥接(Gpr3dvVolumeBridge)成 geopro int16 量化体
|
||
// (轴 X=道/Y=通道/Z=样本),落 ChunkedVolumeStore + buildPyramid。原样渲(14 格 Y
|
||
// 薄体),不做横向插值加密。
|
||
int cmdBuildLine(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.size() < 2) {
|
||
std::cerr << "用法: gpr_poc build-line <lineDir> <linePrefix> "
|
||
"--out <storeDir> [--levels 3]\n"
|
||
"例: gpr_poc build-line \"D:/Downloads/明星路\" 明星路_001 "
|
||
"--out tmp/line001_proc --levels 3\n";
|
||
return 2;
|
||
}
|
||
const std::string lineDir = a.positional[0];
|
||
const std::string linePrefix = a.positional[1];
|
||
const int levels = std::stoi(a.get("levels", "3"));
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / "gpr_store_line").string());
|
||
|
||
std::cout << "[build-line] lineDir=" << lineDir << " linePrefix=" << linePrefix
|
||
<< " levels=" << levels << " out=" << out << "\n";
|
||
|
||
// 1) gpr3dv 处理链 → 处理后立方体 → 桥接量化体。
|
||
Stopwatch swBridge;
|
||
geopro::io::gpr::BridgeMetrics bm;
|
||
geopro::core::BuiltI16 built =
|
||
geopro::io::gpr::buildLineVolumeFromGpr3dv(lineDir, linePrefix, &bm);
|
||
const double bridgeMs = swBridge.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
|
||
|
||
std::cout << "[build-line] 处理前后平均绝对幅值: " << bm.meanAbsBefore << " → "
|
||
<< bm.meanAbsAfter << " (处理"
|
||
<< (std::abs(bm.meanAbsAfter - bm.meanAbsBefore) > 1e-9 ? "已生效"
|
||
: "未变化")
|
||
<< ")\n";
|
||
std::cout << "[build-line] 体维度(道×通道×样本) = " << nx << " x " << ny
|
||
<< " x " << nz << "\n";
|
||
std::cout << "[build-line] 世界 spacing dx=" << bm.dx << " dy=" << bm.dy
|
||
<< " dz=" << bm.dz << " (m)\n";
|
||
|
||
// 2) 落盘 + 金字塔(道很长 45305>16384 → 需 levels≥2 使最粗层≤16384)。
|
||
fs::create_directories(out);
|
||
Stopwatch swWrite;
|
||
geopro::data::ChunkedVolumeStore::write(out, built);
|
||
const double writeMs = swWrite.elapsedMs();
|
||
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramid(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build-line 指标(gpr3dv 处理后真三维体)===\n";
|
||
std::cout << "桥接耗时(ms) : " << bridgeMs << " (含 读 " << bm.loadMs
|
||
<< " + 处理 " << bm.pipelineMs << " + 量化填体 " << bm.fillMs
|
||
<< ")\n";
|
||
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "体维度 : " << nx << " x " << ny << " x " << nz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "处理后值域 : [" << bm.vminPhys << ", " << bm.vmaxPhys
|
||
<< "] 量化 scale=" << built.quant.scale
|
||
<< " offset=" << built.quant.offset << "\n";
|
||
std::cout << "世界 spacing : dx=" << bm.dx << " dy=" << bm.dy
|
||
<< " dz=" << bm.dz << " (m)\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build-line,prefix=" + linePrefix + ",nx=" + std::to_string(nx) +
|
||
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",vmin=" + std::to_string(bm.vminPhys) +
|
||
",vmax=" + std::to_string(bm.vmaxPhys) +
|
||
",dx=" + std::to_string(bm.dx) + ",dy=" + std::to_string(bm.dy) +
|
||
",dz=" + std::to_string(bm.dz) + ",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) +
|
||
",bridgeMs=" + std::to_string(bridgeMs) +
|
||
",writeMs=" + std::to_string(writeMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
int cmdLoad(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc load <storeDir>\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
std::cout << "[load] storeDir=" << dir << "\n";
|
||
|
||
Stopwatch sw;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = sw.elapsedMs();
|
||
|
||
const auto& m = src.meta();
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== load 指标 ===\n";
|
||
std::cout << "加载耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "整卷维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine("load,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) +
|
||
",nz=" + std::to_string(m.nz) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ---- selftest:合成极小数据走完整 build→load 管线 ----
|
||
|
||
// 写一个极小通道的 .iprb + .iprh(samples 采样、traces 道,值 = base + t + s)。
|
||
void writeSyntheticChannel(const fs::path& iprbPath, int samples, int traces,
|
||
std::int16_t base) {
|
||
const fs::path iprhPath =
|
||
fs::path(iprbPath).replace_extension(".iprh");
|
||
std::ofstream h(iprhPath);
|
||
h << "SAMPLES: " << samples << "\n";
|
||
h << "LAST TRACE: " << (traces - 1) << "\n";
|
||
h << "CHANNELS: 2\n";
|
||
h << "TIMEWINDOW: 100.0\n";
|
||
h << "SOIL VELOCITY: 100.0\n"; // m/µs → ×1e6 → 1e8 m/s
|
||
h << "DISTANCE INTERVAL: 0.05\n";
|
||
h.close();
|
||
|
||
std::ofstream b(iprbPath, std::ios::binary);
|
||
// 布局 [trace*samples + s],s 最快。
|
||
for (int t = 0; t < traces; ++t) {
|
||
for (int s = 0; s < samples; ++s) {
|
||
const std::int16_t v =
|
||
static_cast<std::int16_t>(base + t + s);
|
||
b.write(reinterpret_cast<const char*>(&v), sizeof(v));
|
||
}
|
||
}
|
||
}
|
||
|
||
int cmdSelftest() {
|
||
std::cout << "[selftest] 构造极小合成 survey(2 通道)...\n";
|
||
const fs::path tmp =
|
||
fs::temp_directory_path() / "gpr_poc_selftest";
|
||
std::error_code ec;
|
||
fs::remove_all(tmp, ec);
|
||
fs::create_directories(tmp);
|
||
|
||
const int samples = 8;
|
||
const int traces = 12;
|
||
|
||
// 2 通道 .iprb/.iprh + .ord(末列==1 标记有效通道,第 2 列为横偏 Y)。
|
||
writeSyntheticChannel(tmp / "syn_001_A01.iprb", samples, traces,
|
||
/*base=*/100);
|
||
writeSyntheticChannel(tmp / "syn_001_A02.iprb", samples, traces,
|
||
/*base=*/200);
|
||
{
|
||
std::ofstream ord(tmp / "syn_001.ord");
|
||
ord << "0 0.000000 -1.5 1\n";
|
||
ord << "1 1.000000 -1.5 1\n";
|
||
}
|
||
|
||
const std::vector<std::string> iprb = {
|
||
(tmp / "syn_001_A01.iprb").string(),
|
||
(tmp / "syn_001_A02.iprb").string()};
|
||
const std::string ord = (tmp / "syn_001.ord").string();
|
||
|
||
bool ok = true;
|
||
auto check = [&](bool cond, const std::string& msg) {
|
||
if (!cond) {
|
||
std::cerr << "[selftest] FAIL: " << msg << "\n";
|
||
ok = false;
|
||
}
|
||
};
|
||
|
||
try {
|
||
// 装配
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(iprb, ord);
|
||
check(survey.ntraces == traces, "ntraces");
|
||
check(survey.samples == samples, "samples");
|
||
check(survey.channelY.size() == 2, "channels");
|
||
// channelY 升序:A01 偏移 0.0 在前,A02 偏移 1.0 在后。
|
||
check(survey.channelY.front() < survey.channelY.back(), "channelY 升序");
|
||
|
||
// 建体:cellXY 取通道间距 1.0 → ny=2;cellZ 较细确保 nz>1。
|
||
const double cellXY = 1.0;
|
||
const double cellZ = std::max(survey.dz, 1e-12);
|
||
const geopro::core::GridSpec spec =
|
||
specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[selftest] GridSpec " << spec.nx << "x" << spec.ny << "x"
|
||
<< spec.nz << " dz=" << spec.dz << "\n";
|
||
check(spec.ny == 2, "ny==2");
|
||
|
||
geopro::core::BuiltI16 built =
|
||
geopro::core::buildGprVolume(survey, spec);
|
||
check(built.vol.nx() == spec.nx, "built nx");
|
||
check(built.vol.ny() == spec.ny, "built ny");
|
||
check(built.vol.nz() == spec.nz, "built nz");
|
||
|
||
// 落盘 + 金字塔
|
||
const std::string store = (tmp / "store").string();
|
||
fs::create_directories(store);
|
||
geopro::data::ChunkedVolumeStore::write(store, built, /*brick=*/4);
|
||
{
|
||
geopro::data::ChunkedVolumeStore s(store);
|
||
s.buildPyramid(1);
|
||
check(s.levels() == 2, "金字塔层数==2");
|
||
}
|
||
|
||
// 加载整卷,校验维度一致
|
||
geopro::render::WholeVolumeSource src(store);
|
||
check(src.meta().nx == spec.nx, "load nx");
|
||
check(src.meta().ny == spec.ny, "load ny");
|
||
check(src.meta().nz == spec.nz, "load nz");
|
||
|
||
// 某体素值合理性:x0/y0 角点应有非 blank 量化值(落格命中首道首通道)。
|
||
const std::int16_t q = built.vol.at(0, 0, 0);
|
||
check(q != geopro::core::ScalarVolumeI16::kBlank, "(0,0,0) 非 blank");
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "[selftest] 异常: " << e.what() << "\n";
|
||
ok = false;
|
||
}
|
||
|
||
fs::remove_all(tmp, ec);
|
||
std::cout << "[selftest] " << (ok ? "PASS" : "FAIL") << "\n";
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 离屏 GPU 渲染基准(POC-B)
|
||
// ============================================================================
|
||
|
||
// 捕获 VTK 错误输出的 OutputWindow:用于侦测体绘制时 vtkVolumeTexture 报的
|
||
// "Invalid texture dimensions" / "MAX_3D_TEXTURE_SIZE" —— 一旦出现,说明整卷
|
||
// 单张 3D 纹理上传失败,体绘制 fps 无意义,必须如实标 INVALID(绝不当真上报)。
|
||
class CapturingOutputWindow : public vtkOutputWindow {
|
||
public:
|
||
static CapturingOutputWindow* New();
|
||
vtkTypeMacro(CapturingOutputWindow, vtkOutputWindow);
|
||
|
||
void DisplayText(const char* txt) override {
|
||
if (txt) {
|
||
const std::string s(txt);
|
||
captured_ += s;
|
||
if (s.find("texture dimensions") != std::string::npos ||
|
||
s.find("MAX_3D_TEXTURE_SIZE") != std::string::npos) {
|
||
textureError_ = true;
|
||
}
|
||
}
|
||
// 仍透传到 stderr,便于人工查看。
|
||
if (txt) std::cerr << txt;
|
||
}
|
||
|
||
bool textureError() const { return textureError_; }
|
||
const std::string& captured() const { return captured_; }
|
||
|
||
private:
|
||
std::string captured_;
|
||
bool textureError_ = false;
|
||
};
|
||
vtkStandardNewMacro(CapturingOutputWindow);
|
||
|
||
// 创建一个离屏 vtkRenderWindow(VTK9.6:SetShowWindow(false)+OffScreenRenderingOn)。
|
||
vtkSmartPointer<vtkRenderWindow> makeOffscreenWindow(int w, int h) {
|
||
auto rw = vtkSmartPointer<vtkRenderWindow>::New();
|
||
rw->SetOffScreenRendering(1);
|
||
rw->SetShowWindow(false);
|
||
rw->SetSize(w, h);
|
||
return rw;
|
||
}
|
||
|
||
// 闸门:最小离屏渲染冒烟。返回 0=OK,非 0=离屏 GL 起不来(BLOCKED_OFFSCREEN)。
|
||
// 流程:离屏窗口 → 加一个 cube actor → Render() → 读回像素,确认非全黑/读得到。
|
||
int cmdOffscreenSmoke() {
|
||
std::cout << "[offscreen-smoke] 创建离屏 vtkRenderWindow...\n";
|
||
try {
|
||
auto rw = makeOffscreenWindow(256, 256);
|
||
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.1, 0.1, 0.2);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkNew<vtkCubeSource> cube;
|
||
cube->SetXLength(1.0);
|
||
cube->SetYLength(1.0);
|
||
cube->SetZLength(1.0);
|
||
|
||
vtkNew<vtkPolyDataMapper> mapper;
|
||
mapper->SetInputConnection(cube->GetOutputPort());
|
||
|
||
vtkNew<vtkActor> actor;
|
||
actor->SetMapper(mapper);
|
||
actor->GetProperty()->SetColor(1.0, 0.6, 0.2);
|
||
ren->AddActor(actor);
|
||
ren->ResetCamera();
|
||
|
||
// Render():若 GL 上下文创建失败,VTK 会输出错误(多数返回,少数抛)。
|
||
rw->Render();
|
||
|
||
// 读回像素验证:取整窗 RGB,确认能读到且非全 0。
|
||
const int* sz = rw->GetSize();
|
||
const int w = sz[0], h = sz[1];
|
||
if (w <= 0 || h <= 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 窗口尺寸为 0(上下文未建立)\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
// GetRGBACharPixelData(x0,y0,x1,y1,front,arr):front=1 读前缓冲。
|
||
const int ok =
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, pixels);
|
||
if (ok == 0 || pixels->GetNumberOfTuples() == 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 读不到像素\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
// 统计非背景像素(cube 应渲出橙色,存在像素 R 通道明显高于背景)。
|
||
vtkIdType nonBlack = 0;
|
||
const vtkIdType n = pixels->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < n; ++i) {
|
||
const double r = pixels->GetComponent(i, 0);
|
||
const double g = pixels->GetComponent(i, 1);
|
||
const double b = pixels->GetComponent(i, 2);
|
||
if (r > 80 || g > 80 || b > 80) ++nonBlack;
|
||
}
|
||
|
||
const char* caps = rw->ReportCapabilities();
|
||
std::cout << "[offscreen-smoke] 读回像素 " << n << " 个,非背景像素 "
|
||
<< nonBlack << "\n";
|
||
std::cout << "[offscreen-smoke] GL 能力:\n"
|
||
<< (caps ? caps : "(无)") << "\n";
|
||
|
||
if (nonBlack == 0) {
|
||
std::cout << "[offscreen-smoke] FAIL: 渲染结果全为背景(actor 未画出)\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
|
||
std::cout << "[offscreen-smoke] OK:离屏 GL 可用,可继续真实基准。\n";
|
||
std::cout << "STATUS=OK\n";
|
||
return 0;
|
||
} catch (const std::exception& e) {
|
||
std::cout << "[offscreen-smoke] FAIL: 异常 " << e.what() << "\n";
|
||
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// 体绘制 fps:每帧绕 azimuth 旋相机再 Render(),避免被驱动优化成空渲染。
|
||
double benchVolumeFps(vtkRenderWindow* rw, vtkRenderer* ren, int frames) {
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
rw->Render(); // 预热一帧(首帧含上传显存/编译 shader,不计时)
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames); // 每帧转一点,扫满一圈
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
|
||
}
|
||
|
||
// 切片扫描 fps:沿 K 轴(深度)逐偏移 reslice 取轴向切面 + 纹理渲染,每帧推进偏移。
|
||
double benchSliceFps(vtkRenderWindow* rw, vtkRenderer* ren,
|
||
vtkImageData* full, vtkLookupTable* lut, int frames) {
|
||
// reslice:固定轴向(XY 平面),沿 Z 改变 ResliceAxesOrigin 扫过整卷。
|
||
vtkNew<vtkImageReslice> reslice;
|
||
reslice->SetInputData(full);
|
||
reslice->SetOutputDimensionality(2);
|
||
reslice->SetInterpolationModeToLinear();
|
||
|
||
vtkNew<vtkImageMapToColors> colorize;
|
||
colorize->SetLookupTable(lut);
|
||
colorize->SetInputConnection(reslice->GetOutputPort());
|
||
|
||
vtkNew<vtkImageActor> imgActor;
|
||
imgActor->GetMapper()->SetInputConnection(colorize->GetOutputPort());
|
||
ren->AddViewProp(imgActor);
|
||
ren->ResetCamera();
|
||
|
||
double bounds[6];
|
||
full->GetBounds(bounds);
|
||
const double zMin = bounds[4], zMax = bounds[5];
|
||
const double ox = 0.5 * (bounds[0] + bounds[1]);
|
||
const double oy = 0.5 * (bounds[2] + bounds[3]);
|
||
|
||
rw->Render(); // 预热
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
const double t = static_cast<double>(f) / std::max(1, frames - 1);
|
||
const double z = zMin + (zMax - zMin) * t;
|
||
reslice->SetResliceAxesOrigin(ox, oy, z);
|
||
reslice->Modified();
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
|
||
}
|
||
|
||
// 由 ColorScale 物理区间建 256 级 VTK LUT(切片纹理着色用,与体绘制色阶同源)。
|
||
vtkSmartPointer<vtkLookupTable> makeLut(const geopro::core::ColorScale& cs,
|
||
double vmin, double vmax) {
|
||
auto lut = vtkSmartPointer<vtkLookupTable>::New();
|
||
const int n = 256;
|
||
lut->SetNumberOfTableValues(n);
|
||
lut->SetRange(vmin, vmax);
|
||
for (int i = 0; i < n; ++i) {
|
||
const double v = vmin + (vmax - vmin) * i / (n - 1);
|
||
const auto c = cs.colorAt(v);
|
||
lut->SetTableValue(i, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0);
|
||
}
|
||
lut->Build();
|
||
return lut;
|
||
}
|
||
|
||
// 简单蓝-白-红色阶(与 test_color_scale 同款最简构造)。
|
||
geopro::core::ColorScale makeColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double mid = 0.5 * (vmin + vmax);
|
||
cs.addStop(vmin, geopro::core::Rgba{0, 0, 255, 255});
|
||
cs.addStop(mid, geopro::core::Rgba{255, 255, 255, 255});
|
||
cs.addStop(vmax, geopro::core::Rgba{255, 0, 0, 255});
|
||
return cs;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 视觉调优共享构件(Task 12d ①)
|
||
// ============================================================================
|
||
//
|
||
// 结构化配色:地震/雷达体常用的「结构色阶」——深蓝(强负)→青→白(零)→黄→红(强正),
|
||
// 比单纯蓝-白-红更易拉开正负反射层次。值域用数据 vmin/vmax,无需手调控制点。
|
||
geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 140, 255}); // 深蓝
|
||
cs.addStop(at(0.25), geopro::core::Rgba{0, 160, 220, 255}); // 青
|
||
cs.addStop(at(0.50), geopro::core::Rgba{245, 245, 245, 255}); // 白(零附近)
|
||
cs.addStop(at(0.75), geopro::core::Rgba{250, 190, 30, 255}); // 黄
|
||
cs.addStop(at(1.00), geopro::core::Rgba{170, 0, 0, 255}); // 暗红
|
||
return cs;
|
||
}
|
||
|
||
// 地震高对比色阶(seismic 红-白-蓝):两端饱和亮色(强正=亮红、强负=亮蓝),
|
||
// 零附近白。比 structural 更亮、对比更狠,正负反射一眼分开。
|
||
// 前置声明(实现在 polish 段):梯度幅值分位统计,供 C4 画廊的梯度不透明度标定。
|
||
struct GradStats {
|
||
double median = 0, p75 = 0, p90 = 0, p99 = 0, mx = 0;
|
||
std::size_t samples = 0;
|
||
};
|
||
GradStats sampleGradientMagnitude(vtkImageData* img);
|
||
|
||
geopro::core::ColorScale makeSeismicColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{30, 60, 255, 255}); // 亮蓝(强负)
|
||
cs.addStop(at(0.30), geopro::core::Rgba{120, 180, 255, 255}); // 浅蓝
|
||
cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 白(零)
|
||
cs.addStop(at(0.70), geopro::core::Rgba{255, 170, 120, 255}); // 浅橙
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 40, 30, 255}); // 亮红(强正)
|
||
return cs;
|
||
}
|
||
|
||
// jet 类高饱和色阶(蓝-青-绿-黄-红):全程高亮高饱和,最大化色彩动态范围,
|
||
// 弱信号也能映到鲜明色相,适合「一眼铺满层次」的取向。
|
||
geopro::core::ColorScale makeJetColorScale(double vmin, double vmax) {
|
||
geopro::core::ColorScale cs;
|
||
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||
auto at = [&](double t) { return vmin + span * t; };
|
||
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 200, 255}); // 蓝
|
||
cs.addStop(at(0.25), geopro::core::Rgba{0, 200, 255, 255}); // 青
|
||
cs.addStop(at(0.50), geopro::core::Rgba{0, 230, 60, 255}); // 绿
|
||
cs.addStop(at(0.75), geopro::core::Rgba{255, 230, 0, 255}); // 黄
|
||
cs.addStop(at(1.00), geopro::core::Rgba{255, 30, 0, 255}); // 红
|
||
return cs;
|
||
}
|
||
|
||
// 「实体感」不透明度包络(Task 12d gallery):与 structural 双端斜坡不同,这里让
|
||
// 中高值段普遍可见——背景(近零)仍压低但不归零,中高段从 floorOpacity 平滑升到
|
||
// maxOpacity,使体读起来像半透明实心块、内部层次(而非只剩两端薄壳)可见。
|
||
// floorOpacity:近零背景的最低不透明度(0.05~0.12,压住但不消失)
|
||
// maxOpacity :强反射端的不透明度峰值(0.85 时近实心)
|
||
// midOpacity :中值段(半幅处)的不透明度(0.3~0.5,决定「半透明实心」观感)
|
||
vtkSmartPointer<vtkVolumeProperty> makeSolidVolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys, double floorOpacity, double midOpacity,
|
||
double maxOpacity) {
|
||
constexpr int kTransferSamples = 64;
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
|
||
vtkNew<vtkColorTransferFunction> color;
|
||
for (int t = 0; t < kTransferSamples; ++t) {
|
||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||
const double phys = q.toPhys(qvLevel);
|
||
const auto c = cs.colorAt(phys);
|
||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
|
||
// 不透明度:V 形(中段=零附近背景=floor,正负两端=max),但全程 ≥floor 且中值
|
||
// 段≈mid → 整体半透明实心、内部层次可见,而非两端薄壳。
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
const double qmid = 0.5 * (qminD + qmaxD);
|
||
const double half = 0.5 * (qmaxD - qminD);
|
||
opacity->AddPoint(qminD, maxOpacity); // 强负反射:近实心
|
||
opacity->AddPoint(qmid - 0.55 * half, midOpacity); // 中负段:半透明实心
|
||
opacity->AddPoint(qmid, floorOpacity); // 近零背景:压低但可见
|
||
opacity->AddPoint(qmid + 0.55 * half, midOpacity); // 中正段:半透明实心
|
||
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:近实心
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。
|
||
// 不透明度调高时光线提前终止,fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。
|
||
vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys, double maxOpacity,
|
||
bool structuralOpacity = true) {
|
||
constexpr int kTransferSamples = 64;
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
|
||
vtkNew<vtkColorTransferFunction> color;
|
||
for (int t = 0; t < kTransferSamples; ++t) {
|
||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||
const double phys = q.toPhys(qvLevel);
|
||
const auto c = cs.colorAt(phys);
|
||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
|
||
// 不透明度:
|
||
// - 原始(structuralOpacity=false):线性单斜坡 [qmin,qmax]→[0,maxOpacity],
|
||
// 与 VoxelActor 默认一致,作调优前对照基线。
|
||
// - 调优(structuralOpacity=true):双端斜坡。GPR/地震体值多集中在零附近(背景),
|
||
// 强反射在正负两端;线性单斜坡会让占多数的近零背景填满体、遮住结构。改为
|
||
// 「中段(零附近)透明 + 正负两端不透明」——抑制背景、凸显强反射层,截面结构才看得出。
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
if (structuralOpacity) {
|
||
const double qmid = 0.5 * (qminD + qmaxD);
|
||
const double half = 0.5 * (qmaxD - qminD);
|
||
opacity->AddPoint(qminD, maxOpacity); // 强负反射:不透明
|
||
opacity->AddPoint(qmid - 0.30 * half, 0.0); // 近零背景:透明
|
||
opacity->AddPoint(qmid + 0.30 * half, 0.0);
|
||
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:不透明
|
||
} else {
|
||
opacity->AddPoint(qminD, 0.0);
|
||
opacity->AddPoint(qmaxD, maxOpacity);
|
||
}
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 由预构建 VTK_SHORT 图像建一个「视觉调优」体:自定义不透明度 + 垂向夸张。
|
||
// 垂向夸张用 vtkVolume::SetScale(1, exagg, exagg) 缩放跨通道(Y)与深度(Z)两薄轴,
|
||
// 不改图像数据;体物理极扁(X≈2.2km vs Y≈1.5m/Z≈8m),放大薄轴截面结构才看得出。
|
||
vtkSmartPointer<vtkVolume> buildTunedVolume(vtkImageData* shortImg,
|
||
const geopro::core::Quant& q,
|
||
const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys,
|
||
double maxOpacity, double exagg,
|
||
bool structuralOpacity = true) {
|
||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||
mapper->SetInputData(shortImg);
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
mapper->SetAutoAdjustSampleDistances(0);
|
||
mapper->SetInteractiveAdjustSampleDistances(0);
|
||
|
||
auto prop = makeTunedVolumeProperty(q, cs, vminPhys, vmaxPhys, maxOpacity,
|
||
structuralOpacity);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:放大 Y/Z 薄轴
|
||
return volume;
|
||
}
|
||
|
||
int cmdRenderB(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderB <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderB] storeDir=" << dir << " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:renderB 前先确认离屏可用(避免在不可渲染机上跑出假数据)。
|
||
std::cout << "[renderB] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderB] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 加载整卷(VTK_SHORT)。
|
||
Stopwatch swLoad;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
std::cout << "[renderB] 整卷 " << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 体素=" << voxels << " 字节=" << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
|
||
<< "ms\n";
|
||
|
||
auto images = src.currentImages();
|
||
if (images.empty() || !images.front()) {
|
||
std::cerr << "[renderB] 错误: currentImages 为空\n";
|
||
return 1;
|
||
}
|
||
vtkImageData* shortImg = images.front().Get();
|
||
|
||
// 色阶用 meta 的物理区间。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
|
||
// 2) 体绘制(离屏)。
|
||
auto rw = makeOffscreenWindow(1024, 768);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkSmartPointer<vtkVolume> volume =
|
||
geopro::render::buildVoxelI16FromImage(shortImg, m.quant, cs, vmin, vmax);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装上捕获式 OutputWindow:拦截体绘制时的 3D 纹理维度错误。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
std::cout << "[renderB] 体绘制基准(" << frames << " 帧旋转相机)...\n";
|
||
const double volFpsRaw = benchVolumeFps(rw, ren, frames);
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr); // 还原默认输出窗口
|
||
|
||
// 进显存判据:SmartVolumeMapper 实际用的渲染模式(2=GPURenderMode)。
|
||
int renderMode = -1;
|
||
bool lowResResample = false;
|
||
if (auto* svm =
|
||
vtkSmartVolumeMapper::SafeDownCast(volume->GetMapper())) {
|
||
renderMode = svm->GetLastUsedRenderMode();
|
||
// 大体可能触发降质重采样(GPU 显存不足时 SmartVolumeMapper 走低分辨率)。
|
||
lowResResample = (svm->GetInteractiveAdjustSampleDistances() == 0 &&
|
||
renderMode != vtkSmartVolumeMapper::GPURenderMode);
|
||
}
|
||
const bool onGpu = (renderMode == vtkSmartVolumeMapper::GPURenderMode);
|
||
|
||
// 任一维度超过 GL_MAX_3D_TEXTURE_SIZE(本机实测 16384)→ 整卷无法成单张 3D 纹理。
|
||
constexpr int kMax3DTexObserved = 16384;
|
||
const bool dimOversize =
|
||
(m.nx > kMax3DTexObserved || m.ny > kMax3DTexObserved ||
|
||
m.nz > kMax3DTexObserved);
|
||
// 体绘制 fps 是否可信:上传成功(无纹理错误且未超限)才算真实整卷体绘制帧率。
|
||
const bool volFpsValid = !textureErr && !dimOversize;
|
||
const double volFps = volFpsValid ? volFpsRaw : -1.0;
|
||
|
||
std::cout << "[renderB] 体绘制 raw_fps=" << volFpsRaw
|
||
<< " 渲染模式=" << renderMode << (onGpu ? "(GPU)" : "(非GPU)")
|
||
<< " 纹理维度错误=" << (textureErr ? "是" : "否")
|
||
<< " 超 16384=" << (dimOversize ? "是" : "否") << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << "[renderB] 警告: 整卷未能成单张 3D 纹理(X=" << m.nx
|
||
<< " > " << kMax3DTexObserved
|
||
<< "),体绘制 fps 无意义 → 标 INVALID。\n";
|
||
}
|
||
|
||
// 3) 切片扫描(离屏,沿 Z 扫整卷)。
|
||
vtkNew<vtkRenderer> ren2;
|
||
ren2->SetBackground(0.0, 0.0, 0.0);
|
||
auto rw2 = makeOffscreenWindow(1024, 768);
|
||
rw2->AddRenderer(ren2);
|
||
vtkSmartPointer<vtkLookupTable> lut = makeLut(cs, vmin, vmax);
|
||
|
||
std::cout << "[renderB] 切片扫描基准(" << frames << " 帧沿 Z 推进)...\n";
|
||
const double sliceFps =
|
||
benchSliceFps(rw2, ren2, src.sliceSource(), lut, frames);
|
||
std::cout << "[renderB] 切片 fps=" << sliceFps << "\n";
|
||
|
||
const double peak = Probe::peakMemMB();
|
||
const std::string vram = "N/A"; // VTK 安装未带 GLEW 头,无法直查 NVX 显存
|
||
|
||
// 4) 汇总打印。
|
||
const std::string volFpsStr =
|
||
volFpsValid ? std::to_string(volFps) : "INVALID(整卷超 3D 纹理上限)";
|
||
|
||
std::cout << "\n=== renderB GPU 指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "体绘制 fps : " << volFpsStr << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << " (raw_fps=" << volFpsRaw
|
||
<< " 为空纹理渲染,X=" << m.nx << " > 16384,不可信)\n";
|
||
}
|
||
std::cout << "切片扫描 fps : " << sliceFps << " (2D 纹理,无 3D 上限约束)\n";
|
||
std::cout << "渲染模式 : " << renderMode
|
||
<< (onGpu ? " (GPU 路径)" : " (非 GPU)") << "\n";
|
||
std::cout << "整卷进显存 : "
|
||
<< (volFpsValid && onGpu ? "是(单张 3D 纹理)"
|
||
: "否(超 GL_MAX_3D_TEXTURE_SIZE 16384)")
|
||
<< "\n";
|
||
std::cout << "降质重采样 : " << (lowResResample ? "是" : "否") << "\n";
|
||
std::cout << "GPU 显存 : " << vram << "\n";
|
||
std::cout << "进程峰值内存(MB): " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderB,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",volFps=" + volFpsStr +
|
||
",volFpsRaw=" + std::to_string(volFpsRaw) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",sliceFps=" + std::to_string(sliceFps) +
|
||
",renderMode=" + std::to_string(renderMode) +
|
||
",onGpu=" + std::to_string(onGpu ? 1 : 0) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 核外分块体绘制基准(POC-C,命门探针)
|
||
// ============================================================================
|
||
|
||
// 量化域传函(与 VoxelActor::buildVoxelI16FromImage 同逻辑):颜色对每量化级 qv 用
|
||
// q.toPhys(qv) 反查 ColorScale;不透明度 kBlank→0、[qmin,qmax] 线性到 kMaxOpacity。
|
||
// MultiBlock 全块共用同一 vtkVolumeProperty(挂在单个 vtkVolume 上)。
|
||
vtkSmartPointer<vtkVolumeProperty> makeI16VolumeProperty(
|
||
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||
double vminPhys, double vmaxPhys) {
|
||
constexpr int kTransferSamples = 64;
|
||
constexpr double kMaxOpacity = 0.15;
|
||
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||
|
||
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||
|
||
vtkNew<vtkColorTransferFunction> color;
|
||
for (int t = 0; t < kTransferSamples; ++t) {
|
||
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||
const double phys = q.toPhys(qvLevel);
|
||
const auto c = cs.colorAt(phys);
|
||
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||
}
|
||
|
||
vtkNew<vtkPiecewiseFunction> opacity;
|
||
opacity->AddPoint(
|
||
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||
opacity->AddPoint(qminD, 0.0);
|
||
opacity->AddPoint(qmaxD, kMaxOpacity);
|
||
|
||
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||
prop->SetColor(color);
|
||
prop->SetScalarOpacity(opacity);
|
||
prop->SetInterpolationTypeToLinear();
|
||
prop->ShadeOff();
|
||
return prop;
|
||
}
|
||
|
||
// 由当前工作集图像组装 vtkMultiBlockDataSet(每块一个 vtkImageData)。
|
||
vtkSmartPointer<vtkMultiBlockDataSet> makeMultiBlock(
|
||
const std::vector<vtkSmartPointer<vtkImageData>>& imgs) {
|
||
auto mb = vtkSmartPointer<vtkMultiBlockDataSet>::New();
|
||
mb->SetNumberOfBlocks(static_cast<unsigned int>(imgs.size()));
|
||
for (unsigned int i = 0; i < imgs.size(); ++i) {
|
||
mb->SetBlock(i, imgs[i].Get());
|
||
}
|
||
return mb;
|
||
}
|
||
|
||
int cmdRenderC(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const std::size_t budget =
|
||
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderC] storeDir=" << dir << " budget=" << budget
|
||
<< " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderC] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderC] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 核外源(读 meta + 建 pager,不载整卷)。
|
||
Stopwatch swLoad;
|
||
geopro::render::OutOfCoreSource src(dir, budget);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
|
||
const int winW = 1024, winH = 768;
|
||
src.setAspect(static_cast<double>(winW) / winH);
|
||
|
||
std::cout << "[renderC] 体 " << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 体素=" << voxels << " (整卷 X=" << m.nx
|
||
<< " > 16384 → renderB INVALID),源构造 " << loadMs << "ms\n";
|
||
|
||
// 色阶用 meta 物理区间。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
|
||
|
||
// 2) 离屏 + MultiBlock 体绘制。
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
vtkNew<vtkMultiBlockVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装捕获式 OutputWindow:拦截每块上传时的 3D 纹理维度错误(应无,因块 ≤64³)。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 相机:先以全体定向(看整卷),首帧 update 选出工作集后再 ResetCamera 到
|
||
// 实际驻留块的 mapper 包围盒(budget<视野总块时工作集只覆盖体的一部分,框住它
|
||
// 才能确证核外体绘制真渲出;这是 budget 受限下的诚实测法,报告说明)。
|
||
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
|
||
m.origin[1], m.origin[1] + m.ny * m.spacing[1],
|
||
m.origin[2], m.origin[2] + m.nz * m.spacing[2]);
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
|
||
auto refreshBlocks = [&]() {
|
||
src.update(cam);
|
||
auto imgs = src.currentImages();
|
||
auto mb = makeMultiBlock(imgs);
|
||
mapper->SetInputDataObject(mb);
|
||
mapper->Update();
|
||
return imgs.size();
|
||
};
|
||
|
||
const std::size_t warmBlocks = refreshBlocks();
|
||
// 用工作集(mapper)实际包围盒重置相机,框住驻留块。
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
if (b[0] <= b[1]) {
|
||
ren->ResetCamera(b);
|
||
} else {
|
||
ren->ResetCamera();
|
||
}
|
||
}
|
||
rw->Render(); // 预热(上传显存 + 编译 shader,不计时)
|
||
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
std::cout << "[renderC] 工作集包围盒 x[" << b[0] << "," << b[1] << "] y["
|
||
<< b[2] << "," << b[3] << "] z[" << b[4] << "," << b[5] << "]\n";
|
||
}
|
||
|
||
std::cout << "[renderC] 预热:level=" << src.lastLevel()
|
||
<< " 视野块=" << src.lastVisibleCount() << "/"
|
||
<< src.lastLevelBrickTotal()
|
||
<< " 驻留=" << src.residentCount() << " 渲染块=" << warmBlocks
|
||
<< "\n";
|
||
|
||
std::size_t maxResident = src.residentCount();
|
||
std::size_t sumBlocks = 0;
|
||
|
||
// 3a) 静态工作集体绘制 fps:工作集固定(不每帧换块),只旋相机 + Render。
|
||
// 隔离"纯 GPU MultiBlock 体绘制"成本(剔除分块换页/解压/重建 mapper 开销),
|
||
// 直接对照 renderB 整卷 fps,回答未知 #6(真实体绘制 fps)。
|
||
std::cout << "[renderC] 静态工作集体绘制基准(" << frames << " 帧旋相机)...\n";
|
||
Stopwatch swStatic;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render(); // 工作集不变,仅旋转
|
||
}
|
||
const double staticMs = swStatic.elapsedMs();
|
||
const double staticFps = staticMs > 0 ? frames * 1000.0 / staticMs : 0.0;
|
||
std::cout << "[renderC] 静态工作集 fps=" << staticFps << "\n";
|
||
|
||
// 3b) 动态换页体绘制 fps:每帧 update(cam)(重选 LOD/视野块,含 qUncompress 解压
|
||
// 换入的块 + 重建 MultiBlock)+ Render。回答未知 #4(热路径解压是否拖垮 fps)
|
||
// 与 #5(内存恒定)。同时累计 update 耗时占比。
|
||
std::cout << "[renderC] 动态换页体绘制基准(" << frames << " 帧旋相机)...\n";
|
||
double updateMsTotal = 0.0;
|
||
Stopwatch swDyn;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
Stopwatch swU;
|
||
const std::size_t blocks = refreshBlocks(); // update + 重建 MultiBlock
|
||
updateMsTotal += swU.elapsedMs();
|
||
sumBlocks += blocks;
|
||
maxResident = std::max(maxResident, src.residentCount());
|
||
rw->Render();
|
||
}
|
||
const double dynMs = swDyn.elapsedMs();
|
||
const double dynFps = dynMs > 0 ? frames * 1000.0 / dynMs : 0.0;
|
||
const double rawFps = dynFps; // 主报告口径:含换页的真实交互 fps
|
||
std::cout << "[renderC] 动态换页 fps=" << dynFps
|
||
<< " (其中 update/换页/重建 平均 " << (updateMsTotal / frames)
|
||
<< " ms/帧)\n";
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 4) 正确性判据:渲出非空像素(非全背景)。
|
||
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, pixels);
|
||
vtkIdType nonBlack = 0;
|
||
const vtkIdType npx = pixels->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < npx; ++i) {
|
||
if (pixels->GetComponent(i, 0) > 10 || pixels->GetComponent(i, 1) > 10 ||
|
||
pixels->GetComponent(i, 2) > 10) {
|
||
++nonBlack;
|
||
}
|
||
}
|
||
const bool renderedNonEmpty = (nonBlack > 0);
|
||
|
||
// 渲染模式(MultiBlock 内部每块一个 SmartVolumeMapper;此处取一块代表性查询)。
|
||
// MultiBlock 不直接暴露 LastUsedRenderMode,故以纹理无错 + 非空像素为体绘制真出证据。
|
||
const bool volFpsValid = !textureErr && renderedNonEmpty;
|
||
|
||
const double peak = Probe::peakMemMB();
|
||
const double avgBlocks =
|
||
frames > 0 ? static_cast<double>(sumBlocks) / frames : 0.0;
|
||
|
||
std::cout << "\n=== renderC 核外体绘制指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< " (整卷 X 超 16384,renderB=INVALID)\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "budget(块) : " << budget << "\n";
|
||
std::cout << "峰值驻留(块) : " << maxResident
|
||
<< (maxResident <= budget ? " (≤budget,内存恒定 OK)"
|
||
: " (!! 超 budget)")
|
||
<< "\n";
|
||
std::cout << "末帧 level : " << src.lastLevel() << "\n";
|
||
std::cout << "末帧视野块/总块 : " << src.lastVisibleCount() << " / "
|
||
<< src.lastLevelBrickTotal() << "\n";
|
||
std::cout << "平均渲染块/帧 : " << avgBlocks << "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (非背景像素=" << nonBlack << ")\n";
|
||
std::cout << "静态工作集 fps : "
|
||
<< (volFpsValid ? std::to_string(staticFps)
|
||
: std::string("INVALID(纹理错或空渲染)"))
|
||
<< " (纯 GPU MultiBlock 体绘制)\n";
|
||
std::cout << "动态换页 fps : "
|
||
<< (volFpsValid ? std::to_string(dynFps)
|
||
: std::string("INVALID(纹理错或空渲染)"))
|
||
<< " (含每帧 update/解压/重建 mapper)\n";
|
||
std::cout << " 换页均耗时/帧 : " << (updateMsTotal / frames) << " ms\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "对照 renderB : 整卷 INVALID(超 3D 纹理上限);renderC "
|
||
<< (volFpsValid ? "真渲出 ✔" : "未渲出 ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderC,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",budget=" + std::to_string(budget) +
|
||
",maxResident=" + std::to_string(maxResident) +
|
||
",lastLevel=" + std::to_string(src.lastLevel()) +
|
||
",lastVisible=" + std::to_string(src.lastVisibleCount()) +
|
||
",lastLevelTotal=" + std::to_string(src.lastLevelBrickTotal()) +
|
||
",avgBlocks=" + std::to_string(avgBlocks) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",nonBlack=" + std::to_string(nonBlack) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",staticFps=" + (volFpsValid ? std::to_string(staticFps) : "INVALID") +
|
||
",dynFps=" + (volFpsValid ? std::to_string(dynFps) : "INVALID") +
|
||
",updateMsPerFrame=" + std::to_string(updateMsTotal / frames) +
|
||
",rawFps=" + std::to_string(rawFps) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return volFpsValid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 单 mapper SetPartitions 整卷体绘制基准(POC-C-partitioned,去风险探针)
|
||
// ============================================================================
|
||
//
|
||
// 验"对的架构":整卷喂【单个】vtkGPUVolumeRayCastMapper(其 OpenGL 实现 =
|
||
// vtkOpenGLGPUVolumeRayCastMapper),用 SetPartitions(ceil(nx/16384),...) 让同一
|
||
// mapper 内部把体沿轴分区上传(每区 ≤16384 绕过 GL_MAX_3D_TEXTURE_SIZE),一次
|
||
// ray cast。对照 9c 整卷单 SmartVolumeMapper(INVALID,纹理墙) 与 12 MultiBlock
|
||
// (每块一 mapper,9.5 静态/1.45 换页)。
|
||
//
|
||
// 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
|
||
int cmdRenderCPartitioned(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr
|
||
<< "用法: gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderC-partitioned] storeDir=" << dir << " frames=" << frames
|
||
<< "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderC-partitioned] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderC-partitioned] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) WholeVolumeSource 重组整卷 VTK_SHORT image(常驻内存,约 400MB)。
|
||
Stopwatch swLoad;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = swLoad.elapsedMs();
|
||
const auto& m = src.meta();
|
||
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
std::cout << "[renderC-partitioned] 整卷 " << m.nx << "x" << m.ny << "x"
|
||
<< m.nz << " 体素=" << voxels << " 字节=" << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
|
||
<< "ms\n";
|
||
|
||
auto images = src.currentImages();
|
||
if (images.empty() || !images.front()) {
|
||
std::cerr << "[renderC-partitioned] 错误: currentImages 为空\n";
|
||
return 1;
|
||
}
|
||
vtkImageData* shortImg = images.front().Get();
|
||
|
||
// 2) 分区数:任一轴 > 16384 → ceil(dim/16384) 个分区,其余轴 1。
|
||
constexpr int kMax3DTex = 16384;
|
||
auto partCount = [](int dim) {
|
||
return static_cast<unsigned short>((dim + kMax3DTex - 1) / kMax3DTex);
|
||
};
|
||
const unsigned short px = partCount(m.nx);
|
||
const unsigned short py = partCount(m.ny);
|
||
const unsigned short pz = partCount(m.nz);
|
||
std::cout << "[renderC-partitioned] SetPartitions(" << px << "," << py << ","
|
||
<< pz << ") 每区上限 ≤" << kMax3DTex << " (沿线 " << m.nx << "/"
|
||
<< px << "=" << (m.nx + px - 1) / px << ")\n";
|
||
|
||
// 3) 量化域传函(复用现有 makeI16VolumeProperty:qmin/qmax + kBlank 透明)。
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
vtkSmartPointer<vtkVolumeProperty> prop =
|
||
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
|
||
|
||
// 4) 离屏 + 单个 GPU ray cast mapper + SetPartitions。
|
||
const int winW = 1024, winH = 768;
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
|
||
// vtkGPUVolumeRayCastMapper 抽象基类无 SetPartitions(在 OpenGL 实现上);
|
||
// 直接建 OpenGL 具体类(工厂默认产物同此),喂【整卷单 image】不预切块。
|
||
vtkNew<vtkOpenGLGPUVolumeRayCastMapper> mapper;
|
||
mapper->SetInputData(shortImg);
|
||
mapper->SetPartitions(px, py, pz);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
ren->AddVolume(volume);
|
||
|
||
// 装捕获式 OutputWindow:拦截分区上传时的 3D 纹理维度错误。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 相机:用 mapper 实际包围盒定向(整卷,非工作集);体极扁长(44476:29:162),
|
||
// ResetCamera 全体后再倾斜抬高视角,让薄维度可见(否则边缘视角近乎不可见)。
|
||
{
|
||
double b[6];
|
||
mapper->GetBounds(b);
|
||
if (b[0] <= b[1]) {
|
||
ren->ResetCamera(b);
|
||
} else {
|
||
ren->ResetCamera();
|
||
}
|
||
}
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(30.0); // 抬高,避免纯边缘视角看不到薄板
|
||
cam->Azimuth(30.0);
|
||
ren->ResetCameraClippingRange();
|
||
|
||
// 每帧旋相机 + Render 测 fps;同时多帧采样非背景像素取最大值
|
||
// (区分"真渲不出"与"末帧恰好边缘视角空"——后者只是采样时机)。
|
||
auto countNonBlack = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType nb = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
|
||
px->GetComponent(i, 2) > 10) {
|
||
++nb;
|
||
}
|
||
}
|
||
return nb;
|
||
};
|
||
|
||
std::cout << "[renderC-partitioned] 单 mapper 整卷体绘制基准(" << frames
|
||
<< " 帧旋相机)...\n";
|
||
rw->Render(); // 预热(分区上传 + 编译 shader,不计时)
|
||
vtkIdType maxNonBlack = countNonBlack();
|
||
const int sampleEvery = std::max(1, frames / 8);
|
||
Stopwatch swBench;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
if (f % sampleEvery == 0) {
|
||
maxNonBlack = std::max(maxNonBlack, countNonBlack());
|
||
}
|
||
}
|
||
const double benchMs = swBench.elapsedMs();
|
||
const double volFpsRaw =
|
||
benchMs > 0.0 ? frames * 1000.0 / benchMs : 0.0;
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 5) 正确性判据:整个旋转扫描中的最大非背景像素(非空才算真渲出)。
|
||
const vtkIdType nonBlack = maxNonBlack;
|
||
const bool renderedNonEmpty = (nonBlack > 0);
|
||
|
||
// 双闸:无纹理错 + 非空像素 → fps 可信。
|
||
const bool volFpsValid = !textureErr && renderedNonEmpty;
|
||
const double volFps = volFpsValid ? volFpsRaw : -1.0;
|
||
const double peak = Probe::peakMemMB();
|
||
const bool interactive = volFpsValid && volFps >= 15.0;
|
||
|
||
const std::string volFpsStr =
|
||
volFpsValid ? std::to_string(volFps)
|
||
: std::string("INVALID(纹理错或空渲染)");
|
||
|
||
std::cout << "\n=== renderC-partitioned 单 mapper SetPartitions 指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "分区数(px,py,pz) : " << px << "," << py << "," << pz << "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (非背景像素=" << nonBlack << ")\n";
|
||
std::cout << "体绘制 fps : " << volFpsStr << "\n";
|
||
if (!volFpsValid) {
|
||
std::cout << " (raw_fps=" << volFpsRaw << " 不可信)\n";
|
||
}
|
||
std::cout << "达交互级(≥15fps) : "
|
||
<< (interactive ? "是 ✔" : "否 ✘") << "\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "对照 renderB : 整卷单 SmartVolumeMapper=INVALID(纹理墙);"
|
||
"renderC MultiBlock=9.5 静态/1.45 换页;本探针="
|
||
<< (volFpsValid ? volFpsStr + "fps" : "INVALID") << "\n";
|
||
|
||
writeMetricLine(
|
||
"renderC-partitioned,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",px=" + std::to_string(px) + ",py=" + std::to_string(py) +
|
||
",pz=" + std::to_string(pz) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",nonBlack=" + std::to_string(nonBlack) +
|
||
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
|
||
",volFps=" + volFpsStr + ",volFpsRaw=" + std::to_string(volFpsRaw) +
|
||
",interactive=" + std::to_string(interactive ? 1 : 0) +
|
||
",loadMs=" + std::to_string(loadMs) + ",peakMB=" + std::to_string(peak));
|
||
|
||
// 写报告文件(覆盖式,含对照表)。
|
||
{
|
||
const fs::path repo =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||
fs::create_directories(repo.parent_path());
|
||
std::ofstream rf(repo.string());
|
||
if (rf) {
|
||
rf << "# POC-C 单 mapper SetPartitions 整卷体绘制探针结果\n\n";
|
||
rf << "## 体\n";
|
||
rf << "- 维度: " << m.nx << " x " << m.ny << " x " << m.nz << " (体素 "
|
||
<< voxels << ")\n";
|
||
rf << "- 整卷字节: " << wholeBytes << " B ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB, VTK_SHORT)\n";
|
||
rf << "- store: " << dir << "\n\n";
|
||
rf << "## 单 mapper SetPartitions\n";
|
||
rf << "- mapper: vtkOpenGLGPUVolumeRayCastMapper (整卷单 image,不预切块)\n";
|
||
rf << "- 分区数: SetPartitions(" << px << ", " << py << ", " << pz
|
||
<< ") 每区上限 ≤" << kMax3DTex << "\n";
|
||
rf << "- 纹理维度错误: " << (textureErr ? "是" : "否") << "\n";
|
||
rf << "- 渲出非空像素: " << (renderedNonEmpty ? "是" : "否") << " (非背景像素 "
|
||
<< nonBlack << ")\n";
|
||
rf << "- 体绘制 fps: " << volFpsStr << "\n";
|
||
rf << "- 达交互级(≥15fps): " << (interactive ? "是" : "否") << "\n";
|
||
rf << "- 进程峰值内存: " << peak << " MB\n";
|
||
rf << "- 源构造耗时: " << loadMs << " ms\n\n";
|
||
rf << "## 对照表\n\n";
|
||
rf << "| 路径 | 是否渲出 | fps |\n";
|
||
rf << "|---|---|---|\n";
|
||
rf << "| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙) | — |\n";
|
||
rf << "| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态/1.45 换页 |\n";
|
||
rf << "| renderC-partitioned 单 mapper SetPartitions | "
|
||
<< (volFpsValid ? "渲出" : "未渲出") << " | "
|
||
<< (volFpsValid ? volFpsStr : std::string("INVALID")) << " |\n\n";
|
||
rf << "## 判据结论\n";
|
||
if (volFpsValid && interactive) {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【真渲出且达交互级】(" << volFps
|
||
<< " fps ≥15)。C production 路线钉死可行。\n";
|
||
} else if (volFpsValid) {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(" << volFps
|
||
<< " fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。\n";
|
||
} else {
|
||
rf << "单 mapper SetPartitions 整卷体绘制【未真渲出】(纹理错="
|
||
<< (textureErr ? "是" : "否") << ",非空像素="
|
||
<< (renderedNonEmpty ? "是" : "否")
|
||
<< ")。SetPartitions 未能绕过纹理墙,如实记录。\n";
|
||
}
|
||
std::cout << "[renderC-partitioned] 报告写入 " << repo.string() << "\n";
|
||
}
|
||
}
|
||
|
||
return volFpsValid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// LOD-fps 探针(POC-C 最后一根链子,Task 12c)
|
||
// ============================================================================
|
||
//
|
||
// 12b 已证整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限,fps 杠杆只有 LOD
|
||
// (渲更少体素)。本探针在【真实金字塔 store】上验四件事,全离屏、双闸防假帧率:
|
||
// (a) 粗层概览 fps:level2 整卷(单轴 <16384 → 单 SmartVolumeMapper)。
|
||
// (b) 全分辨率局部 fps:level0 一段 brick 列(沿线局部)。
|
||
// (c) LOD 切换动态过渡:相机从远观(level2)逐步拉近到近观局部(level0),跨越
|
||
// LOD 切换那一下逐帧记帧耗时,标切换帧尖峰/stall。
|
||
// (d) 截图:lod-overview.png / lod-fullres-local.png / lod-transition-mid.png。
|
||
//
|
||
// 双闸(同 9c,绝不把空纹理假帧率当性能):
|
||
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
|
||
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
|
||
|
||
// 把金字塔某 level 重组成整卷 VTK_SHORT vtkImageData(逻辑同 WholeVolumeSource,
|
||
// 但按 level 维度 + spacing×2^level,使物理范围与 level0 一致)。
|
||
vtkSmartPointer<vtkImageData> buildLevelImage(
|
||
const geopro::data::ChunkedVolumeStore& store, int level,
|
||
const geopro::data::StoreMeta& m) {
|
||
int nx = 0, ny = 0, nz = 0;
|
||
store.dims(level, nx, ny, nz);
|
||
const int brick = m.brick;
|
||
const double sc = static_cast<double>(1 << level); // 2^level
|
||
|
||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||
img->SetDimensions(nx, ny, nz);
|
||
img->SetOrigin(m.origin[0], m.origin[1], m.origin[2]);
|
||
img->SetSpacing(m.spacing[0] * sc, m.spacing[1] * sc, m.spacing[2] * sc);
|
||
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
|
||
|
||
for (int bz = 0; bz < store.bricksZ(level); ++bz) {
|
||
for (int by = 0; by < store.bricksY(level); ++by) {
|
||
for (int bx = 0; bx < store.bricksX(level); ++bx) {
|
||
const std::vector<std::int16_t> raw = store.readBrick(level, bx, by, bz);
|
||
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
|
||
const int bw = (nx - i0 < brick) ? (nx - i0) : brick;
|
||
const int bh = (ny - j0 < brick) ? (ny - j0) : brick;
|
||
const int bd = (nz - k0 < brick) ? (nz - k0) : brick;
|
||
std::size_t w = 0;
|
||
for (int kk = 0; kk < bd; ++kk) {
|
||
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
|
||
for (int jj = 0; jj < bh; ++jj) {
|
||
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
|
||
vtkIdType id = (gk * ny + gj) * nx + i0;
|
||
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
img->GetPointData()->SetScalars(arr);
|
||
return img;
|
||
}
|
||
|
||
// 取 level0 一段 brick 列 [bx0, bx0+bxCount) × 全 Y × 全 Z 重组成局部整卷
|
||
// VTK_SHORT image(X 维 = bxCount*brick ≤ 几百,远 <16384,单 3D 纹理)。
|
||
// Origin 沿 X 偏移到该段起点,spacing 用 level0 原值。
|
||
vtkSmartPointer<vtkImageData> buildLocalLevel0Image(
|
||
const geopro::data::ChunkedVolumeStore& store,
|
||
const geopro::data::StoreMeta& m, int bx0, int bxCount) {
|
||
const int brick = m.brick;
|
||
const int nx0 = m.nx, ny0 = m.ny, nz0 = m.nz;
|
||
const int totBx = store.bricksX(0);
|
||
bx0 = std::max(0, std::min(bx0, totBx - 1));
|
||
bxCount = std::max(1, std::min(bxCount, totBx - bx0));
|
||
|
||
const int i0Global = bx0 * brick;
|
||
const int localNx = std::min(bxCount * brick, nx0 - i0Global);
|
||
|
||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||
img->SetDimensions(localNx, ny0, nz0);
|
||
img->SetOrigin(m.origin[0] + i0Global * m.spacing[0], m.origin[1],
|
||
m.origin[2]);
|
||
img->SetSpacing(m.spacing[0], m.spacing[1], m.spacing[2]);
|
||
|
||
vtkNew<vtkShortArray> arr;
|
||
arr->SetName("v");
|
||
arr->SetNumberOfTuples(static_cast<vtkIdType>(localNx) * ny0 * nz0);
|
||
|
||
for (int bz = 0; bz < store.bricksZ(0); ++bz) {
|
||
for (int by = 0; by < store.bricksY(0); ++by) {
|
||
for (int bx = bx0; bx < bx0 + bxCount; ++bx) {
|
||
const std::vector<std::int16_t> raw = store.readBrick(0, bx, by, bz);
|
||
const int gi0 = bx * brick, j0 = by * brick, k0 = bz * brick;
|
||
const int li0 = gi0 - i0Global; // 局部 X 起点
|
||
const int bw = (nx0 - gi0 < brick) ? (nx0 - gi0) : brick;
|
||
const int bh = (ny0 - j0 < brick) ? (ny0 - j0) : brick;
|
||
const int bd = (nz0 - k0 < brick) ? (nz0 - k0) : brick;
|
||
std::size_t w = 0;
|
||
for (int kk = 0; kk < bd; ++kk) {
|
||
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
|
||
for (int jj = 0; jj < bh; ++jj) {
|
||
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
|
||
vtkIdType id = (gk * ny0 + gj) * localNx + li0;
|
||
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
img->GetPointData()->SetScalars(arr);
|
||
return img;
|
||
}
|
||
|
||
// 统计当前窗口前缓冲非背景像素(>10 任一通道)。
|
||
vtkIdType countNonBlackPixels(vtkRenderWindow* rw, int w, int h) {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, px);
|
||
vtkIdType nb = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
|
||
px->GetComponent(i, 2) > 10) {
|
||
++nb;
|
||
}
|
||
}
|
||
return nb;
|
||
}
|
||
|
||
// 离屏窗口截图 → PNG。
|
||
void savePng(vtkRenderWindow* rw, const std::string& path) {
|
||
rw->Render();
|
||
vtkNew<vtkWindowToImageFilter> w2i;
|
||
w2i->SetInput(rw);
|
||
w2i->SetInputBufferTypeToRGB();
|
||
w2i->ReadFrontBufferOff();
|
||
w2i->Update();
|
||
vtkNew<vtkPNGWriter> writer;
|
||
writer->SetFileName(path.c_str());
|
||
writer->SetInputConnection(w2i->GetOutputPort());
|
||
writer->Write();
|
||
}
|
||
|
||
// 画面平均亮度(0~255):取前缓冲 RGB 求 luma 均值。Task 12d gallery 报告用,
|
||
// 量化「整体偏暗 vs 变亮」——背景占多数,故这是含背景的全屏均亮(横向对比有效)。
|
||
double meanBrightness(vtkRenderWindow* rw, int w, int h) {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, px);
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
if (np == 0) return 0.0;
|
||
double sum = 0.0;
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
const double r = px->GetComponent(i, 0);
|
||
const double g = px->GetComponent(i, 1);
|
||
const double b = px->GetComponent(i, 2);
|
||
sum += 0.299 * r + 0.587 * g + 0.114 * b;
|
||
}
|
||
return sum / static_cast<double>(np);
|
||
}
|
||
|
||
int cmdRenderLOD(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc renderLOD <storeDir> [--frames 120]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
std::cout << "[renderLOD] storeDir=" << dir << " frames=" << frames << "\n";
|
||
|
||
// 闸门复检:不可渲染机不产假 fps。
|
||
std::cout << "[renderLOD] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[renderLOD] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int totLevels = store.levels();
|
||
std::cout << "[renderLOD] level0=" << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 总层数=" << totLevels << "\n";
|
||
if (totLevels < 3) {
|
||
std::cout << "[renderLOD] 警告: 金字塔层数 <3(需 build --levels 3)。\n";
|
||
}
|
||
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
|
||
const int winW = 1024, winH = 768;
|
||
|
||
// 共用一个捕获式 OutputWindow,贯穿三段渲染。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// ---- (a) 粗层概览 fps:level2 整卷 ----
|
||
const int ovLevel = std::min(2, totLevels - 1);
|
||
std::cout << "[renderLOD] (a) 建 level" << ovLevel << " 整卷 image...\n";
|
||
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
|
||
int ovNx, ovNy, ovNz;
|
||
store.dims(ovLevel, ovNx, ovNy, ovNz);
|
||
|
||
auto rwOv = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renOv;
|
||
renOv->SetBackground(0.0, 0.0, 0.0);
|
||
rwOv->AddRenderer(renOv);
|
||
vtkSmartPointer<vtkVolume> ovVol =
|
||
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
renOv->AddVolume(ovVol);
|
||
// 先测 fps(benchVolumeFps 内部会 ResetCamera + 旋满一圈)。
|
||
const double ovFps = benchVolumeFps(rwOv.Get(), renOv, frames);
|
||
// 截图前重设一个利于人眼的取景:整线物理纵横比极扁(~2200m×1.5m×8m),俯视角
|
||
// 看宽面才能呈现整条带(而非边缘线)。
|
||
renOv->ResetCamera();
|
||
renOv->GetActiveCamera()->Elevation(55.0);
|
||
renOv->GetActiveCamera()->Azimuth(20.0);
|
||
renOv->ResetCameraClippingRange();
|
||
rwOv->Render();
|
||
const vtkIdType ovNonBlack = countNonBlackPixels(rwOv.Get(), winW, winH);
|
||
savePng(rwOv.Get(), (shotDir / "lod-overview.png").string());
|
||
std::cout << "[renderLOD] (a) 概览 fps=" << ovFps << " 非空像素=" << ovNonBlack
|
||
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
|
||
<< ")\n";
|
||
|
||
// ---- (b) 全分辨率局部 fps:level0 一段 brick 列 ----
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(4, totBx); // 4 brick 列 ≈ 256 体素宽
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 取沿线中段
|
||
std::cout << "[renderLOD] (b) 建 level0 局部 image (brick列 [" << bx0 << ","
|
||
<< (bx0 + localBx) << ") / " << totBx << ")...\n";
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int locDims[3];
|
||
locImg->GetDimensions(locDims);
|
||
|
||
auto rwLoc = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renLoc;
|
||
renLoc->SetBackground(0.0, 0.0, 0.0);
|
||
rwLoc->AddRenderer(renLoc);
|
||
vtkSmartPointer<vtkVolume> locVol =
|
||
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
renLoc->AddVolume(locVol);
|
||
const double locFps = benchVolumeFps(rwLoc.Get(), renLoc, frames);
|
||
// 截图取景:局部块(256×29×162)斜俯视,呈现全分辨率细节供与概览对比。
|
||
renLoc->ResetCamera();
|
||
renLoc->GetActiveCamera()->Elevation(35.0);
|
||
renLoc->GetActiveCamera()->Azimuth(25.0);
|
||
renLoc->ResetCameraClippingRange();
|
||
rwLoc->Render();
|
||
const vtkIdType locNonBlack = countNonBlackPixels(rwLoc.Get(), winW, winH);
|
||
savePng(rwLoc.Get(), (shotDir / "lod-fullres-local.png").string());
|
||
std::cout << "[renderLOD] (b) 局部 fps=" << locFps << " 非空像素="
|
||
<< locNonBlack << " (level0 局部 " << locDims[0] << "x" << locDims[1]
|
||
<< "x" << locDims[2] << ")\n";
|
||
|
||
// ---- (c) LOD 切换动态过渡 ----
|
||
// 同一窗口:相机从远观(看整卷,用 level2 概览体)逐步 dolly 拉近,到一半处
|
||
// 跨越 LOD 切换——把体从 level2 整卷换成 level0 局部体(重设 mapper 输入/相机
|
||
// 目标),逐帧记帧耗时,标切换帧尖峰。
|
||
std::cout << "[renderLOD] (c) LOD 切换动态过渡(" << frames << " 帧 dolly)...\n";
|
||
auto rwTr = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renTr;
|
||
renTr->SetBackground(0.0, 0.0, 0.0);
|
||
rwTr->AddRenderer(renTr);
|
||
|
||
// 远观体 = level2 概览(新建一份,避免与 (a) 共享 actor 状态)。
|
||
vtkSmartPointer<vtkVolume> farVol =
|
||
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
// 近观体 = level0 局部(复用 (b) 的 image)。
|
||
vtkSmartPointer<vtkVolume> nearVol =
|
||
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
|
||
vmax);
|
||
|
||
renTr->AddVolume(farVol);
|
||
renTr->ResetCamera(); // 框住整卷(level2 与 level0 物理范围一致)
|
||
vtkCamera* camTr = renTr->GetActiveCamera();
|
||
camTr->Elevation(20.0);
|
||
renTr->ResetCameraClippingRange();
|
||
rwTr->Render(); // 预热远观
|
||
|
||
// dolly 目标:从当前(远)拉近到局部段中心。
|
||
double locCenter[3];
|
||
locImg->GetCenter(locCenter);
|
||
const int switchFrame = frames / 2;
|
||
const double dollyPerFrame =
|
||
std::pow(6.0, 1.0 / std::max(1, switchFrame)); // 切换前累计 dolly≈6×
|
||
|
||
std::vector<double> frameMs(frames, 0.0);
|
||
bool switched = false;
|
||
double switchStallMs = 0.0;
|
||
|
||
for (int f = 0; f < frames; ++f) {
|
||
Stopwatch swF;
|
||
if (f == switchFrame && !switched) {
|
||
// —— LOD 切换那一下 ——:换体 + 把相机焦点移到局部段中心。
|
||
renTr->RemoveVolume(farVol);
|
||
renTr->AddVolume(nearVol);
|
||
camTr->SetFocalPoint(locCenter[0], locCenter[1], locCenter[2]);
|
||
renTr->ResetCameraClippingRange();
|
||
switched = true;
|
||
}
|
||
// 渐进拉近(切换前 dolly 进;切换后继续推近 + 轻微环绕,逐步框满局部块)。
|
||
camTr->Dolly(switched ? 1.04 : dollyPerFrame);
|
||
if (switched) camTr->Azimuth(0.5);
|
||
renTr->ResetCameraClippingRange();
|
||
rwTr->Render();
|
||
frameMs[f] = swF.elapsedMs();
|
||
if (f == switchFrame) switchStallMs = frameMs[f];
|
||
// 切换后推近一小段再截“过渡中间帧”,使局部块已明显呈现(而非切换瞬间仍很远)。
|
||
if (f == switchFrame + (frames - switchFrame) / 3) {
|
||
savePng(rwTr.Get(), (shotDir / "lod-transition-mid.png").string());
|
||
}
|
||
}
|
||
|
||
// 过渡帧耗时统计:平均、最大、切换帧、切换帧相对邻帧的尖峰倍数。
|
||
double sum = 0, mx = 0;
|
||
for (double v : frameMs) {
|
||
sum += v;
|
||
mx = std::max(mx, v);
|
||
}
|
||
const double avgMs = frames > 0 ? sum / frames : 0.0;
|
||
const double preMs =
|
||
switchFrame > 0 ? frameMs[switchFrame - 1] : avgMs;
|
||
const double spikeRatio = preMs > 0 ? switchStallMs / preMs : 0.0;
|
||
// 可感知卡顿判据(绝对耗时为准,尖峰倍数仅作次级信号):当两端帧耗时是亚毫秒
|
||
// 时,一次性换体的 ~9ms 抖动倍数虽大但仍 <1 个 60Hz 帧(16.7ms),人眼不可感。
|
||
// 故:切换帧 >1 个 60Hz 帧(16.7ms)才记“轻微”,>2 帧(33ms)记“可感知卡顿”。
|
||
constexpr double kFrame60Ms = 1000.0 / 60.0; // 16.7ms
|
||
const bool perceptibleStall = switchStallMs > 2.0 * kFrame60Ms; // >33ms
|
||
const bool minorHitch =
|
||
!perceptibleStall && switchStallMs > kFrame60Ms; // 16.7~33ms 轻微
|
||
const vtkIdType trNonBlack = countNonBlackPixels(rwTr.Get(), winW, winH);
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
|
||
// 双闸:无纹理错 + 三段均渲出非空像素。
|
||
const bool renderedNonEmpty =
|
||
(ovNonBlack > 0) && (locNonBlack > 0) && (trNonBlack > 0);
|
||
const bool valid = !textureErr && renderedNonEmpty;
|
||
|
||
const double ovFpsV = valid ? ovFps : -1.0;
|
||
const double locFpsV = valid ? locFps : -1.0;
|
||
const bool ovInteractive = valid && ovFps >= 15.0;
|
||
const bool locInteractive = valid && locFps >= 15.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
const char* stallTxt =
|
||
perceptibleStall ? "可感知卡顿" : (minorHitch ? "轻微抖动(<2帧)" : "无");
|
||
std::cout << "[renderLOD] (c) 过渡帧耗时 avg=" << avgMs << "ms max=" << mx
|
||
<< "ms 切换帧=" << switchStallMs << "ms (邻帧 " << preMs << "ms, 尖峰 "
|
||
<< spikeRatio << "×) 卡顿=" << stallTxt << "\n";
|
||
|
||
std::cout << "\n=== renderLOD LOD-fps 探针指标 ===\n";
|
||
std::cout << "离屏闸门 : OK\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "三段均渲出非空 : " << (renderedNonEmpty ? "是" : "否(!!)")
|
||
<< " (概览=" << ovNonBlack << " 局部=" << locNonBlack
|
||
<< " 过渡=" << trNonBlack << ")\n";
|
||
std::cout << "(a) 粗层概览 fps : "
|
||
<< (valid ? std::to_string(ovFpsV) : std::string("INVALID"))
|
||
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
|
||
<< ") 交互级=" << (ovInteractive ? "是 ✔" : "否 ✘") << "\n";
|
||
std::cout << "(b) 全分辨率局部fps: "
|
||
<< (valid ? std::to_string(locFpsV) : std::string("INVALID"))
|
||
<< " (level0 局部 " << locDims[0] << "x" << locDims[1] << "x"
|
||
<< locDims[2] << ") 交互级=" << (locInteractive ? "是 ✔" : "否 ✘")
|
||
<< "\n";
|
||
std::cout << "(c) 过渡平均/最大 : " << avgMs << " / " << mx << " ms\n";
|
||
std::cout << " 切换帧耗时 : " << switchStallMs << " ms (邻帧 " << preMs
|
||
<< " ms, 尖峰 " << spikeRatio << "×)\n";
|
||
std::cout << " 可感知卡顿 : " << stallTxt
|
||
<< (perceptibleStall ? " ✘" : " ✔") << " (判据:切换帧 >33ms 才记卡顿"
|
||
"; 1 帧 60Hz=16.7ms)\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
std::cout << "截图 : " << shotDir.string()
|
||
<< " (lod-overview / lod-fullres-local / lod-transition-mid)\n";
|
||
|
||
writeMetricLine(
|
||
"renderLOD,dir=" + dir + ",totLevels=" + std::to_string(totLevels) +
|
||
",ovLevel=" + std::to_string(ovLevel) +
|
||
",ovDims=" + std::to_string(ovNx) + "x" + std::to_string(ovNy) + "x" +
|
||
std::to_string(ovNz) +
|
||
",ovFps=" + (valid ? std::to_string(ovFpsV) : "INVALID") +
|
||
",ovNonBlack=" + std::to_string(ovNonBlack) +
|
||
",locDims=" + std::to_string(locDims[0]) + "x" +
|
||
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
|
||
",locFps=" + (valid ? std::to_string(locFpsV) : "INVALID") +
|
||
",locNonBlack=" + std::to_string(locNonBlack) +
|
||
",trAvgMs=" + std::to_string(avgMs) + ",trMaxMs=" + std::to_string(mx) +
|
||
",switchMs=" + std::to_string(switchStallMs) +
|
||
",switchSpike=" + std::to_string(spikeRatio) +
|
||
",stall=" + std::to_string(perceptibleStall ? 1 : 0) +
|
||
",trNonBlack=" + std::to_string(trNonBlack) +
|
||
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
|
||
",valid=" + std::to_string(valid ? 1 : 0) +
|
||
",peakMB=" + std::to_string(peak));
|
||
|
||
// 写 poc-results-C.md 的 LOD 段(追加,不覆盖 renderC-partitioned 段)。
|
||
{
|
||
const fs::path repo =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||
fs::create_directories(repo.parent_path());
|
||
std::ofstream rf(repo.string(), std::ios::app);
|
||
if (rf) {
|
||
rf << "\n\n# POC-C LOD-fps 探针结果(Task 12c)\n\n";
|
||
rf << "金字塔 store: " << dir << "(level0=" << m.nx << "x" << m.ny << "x"
|
||
<< m.nz << ",总 " << totLevels << " 层)\n\n";
|
||
rf << "| 项 | 维度 | 结果 |\n|---|---|---|\n";
|
||
rf << "| (a) 粗层概览 fps | level" << ovLevel << " " << ovNx << "x" << ovNy
|
||
<< "x" << ovNz << " | " << (valid ? std::to_string(ovFpsV) : "INVALID")
|
||
<< " fps " << (ovInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
|
||
rf << "| (b) 全分辨率局部 fps | level0 局部 " << locDims[0] << "x"
|
||
<< locDims[1] << "x" << locDims[2] << " | "
|
||
<< (valid ? std::to_string(locFpsV) : "INVALID") << " fps "
|
||
<< (locInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
|
||
rf << "| (c) LOD 切换过渡 | 切换帧 " << switchFrame << "/" << frames
|
||
<< " | 平均 " << avgMs << "ms,切换帧 " << switchStallMs << "ms(尖峰 "
|
||
<< spikeRatio << "×),"
|
||
<< (perceptibleStall ? "可感知卡顿"
|
||
: (minorHitch ? "轻微抖动" : "无可感知卡顿"))
|
||
<< " |\n\n";
|
||
rf << "- 卡顿判据:切换帧绝对耗时 >33ms(2 个 60Hz 帧)才记可感知卡顿;"
|
||
"16.7~33ms 记轻微抖动;亚毫秒基线下尖峰倍数大但绝对值低不算卡顿。\n";
|
||
rf << "- 双闸:纹理维度错误=" << (textureErr ? "是" : "否")
|
||
<< ";三段均渲出非空像素=" << (renderedNonEmpty ? "是" : "否")
|
||
<< "(概览 " << ovNonBlack << " / 局部 " << locNonBlack << " / 过渡 "
|
||
<< trNonBlack << ")。\n";
|
||
rf << "- 截图(人眼判“概览糊→拉近清晰”):docs/superpowers/plans/poc-lod-shots/"
|
||
"lod-overview.png、lod-fullres-local.png、lod-transition-mid.png\n";
|
||
rf << "- 进程峰值内存: " << peak << " MB\n\n";
|
||
rf << "## 判据结论\n";
|
||
if (valid && ovInteractive && locInteractive && !perceptibleStall) {
|
||
rf << "粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ "
|
||
"LOD-based C 路线钉死可行。\n";
|
||
} else if (valid && ovInteractive && !locInteractive) {
|
||
rf << "粗层快但全分辨率局部仍慢 → VTK 体绘制有真实天花板,记录,"
|
||
"评估 OpenVDS/自建 GL。\n";
|
||
} else if (valid && perceptibleStall) {
|
||
rf << "两端 fps 可接受但切换卡顿明显(切换帧 " << switchStallMs
|
||
<< "ms)→ 为后续 morphing/淡入提供依据。\n";
|
||
} else if (!valid) {
|
||
rf << "双闸未过(纹理错或空渲染)→ 数字不可信,如实标 INVALID。\n";
|
||
} else {
|
||
rf << "部分达标,详见上表。\n";
|
||
}
|
||
rf << "\n**最低配未验声明**:本探针仅在本机(RTX 3060)跑得上限数字,"
|
||
"最低配机器未验证,需用户在目标机跑或提供型号。\n";
|
||
}
|
||
std::cout << "[renderLOD] 报告追加写入 " << repo.string() << "\n";
|
||
}
|
||
|
||
return valid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ① 视觉调优:出一帧能看结构的图 + 调优前后 fps 对照(Task 12d)
|
||
// ============================================================================
|
||
//
|
||
// 在【真实金字塔 store】上对局部段(level0 一段 brick 列)与粗层概览(level2 整卷)
|
||
// 各跑两遍体绘制 fps:调优前(默认色阶 0.15 不透明度 无夸张) vs 调优后(结构色阶 +
|
||
// --opacity + --exagg 垂向夸张),离屏存 lod-tuned-local.png / lod-tuned-overview.png,
|
||
// 并打印前后 fps 对照——证实「视觉调优对 fps 近乎中性」这一探针认知。双闸防假帧率。
|
||
int cmdTune(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
|
||
"[--frames 120] [--localBricks 4]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const double opacity = std::stod(a.get("opacity", "0.5"));
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
const int frames = std::stoi(a.get("frames", "120"));
|
||
const int localBricks = std::stoi(a.get("localBricks", "4"));
|
||
std::cout << "[tune] storeDir=" << dir << " opacity=" << opacity
|
||
<< " exagg=" << exagg << " frames=" << frames << "\n";
|
||
|
||
std::cout << "[tune] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[tune] 闸门失败,中止。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int totLevels = store.levels();
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
|
||
const geopro::core::ColorScale csPlain = makeColorScale(vmin, vmax);
|
||
const geopro::core::ColorScale csTuned = makeStructuralColorScale(vmin, vmax);
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const int winW = 1024, winH = 768;
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// ---- 局部段:level0 一段 brick 列(沿线中段)----
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(localBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int locDims[3];
|
||
locImg->GetDimensions(locDims);
|
||
|
||
// 调优前局部 fps(默认色阶 0.15 无夸张)。
|
||
auto rwA = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renA;
|
||
renA->SetBackground(0.0, 0.0, 0.0);
|
||
rwA->AddRenderer(renA);
|
||
vtkSmartPointer<vtkVolume> volA =
|
||
buildTunedVolume(locImg.Get(), m.quant, csPlain, vmin, vmax, 0.15, 1.0,
|
||
/*structuralOpacity=*/false); // 原始线性单斜坡基线
|
||
renA->AddVolume(volA);
|
||
const double locFpsBefore = benchVolumeFps(rwA.Get(), renA, frames);
|
||
|
||
// 调优后局部 fps(结构色阶 + opacity + exagg)。
|
||
auto rwB = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renB;
|
||
renB->SetBackground(0.04, 0.04, 0.08); // 深蓝灰背景,衬托体
|
||
rwB->AddRenderer(renB);
|
||
vtkSmartPointer<vtkVolume> volB =
|
||
buildTunedVolume(locImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
|
||
exagg);
|
||
renB->AddVolume(volB);
|
||
const double locFpsAfter = benchVolumeFps(rwB.Get(), renB, frames);
|
||
// 调优后取景:夸张后块更"立体",斜俯视呈现截面层次;Zoom 拉近填满画面。
|
||
renB->ResetCamera();
|
||
renB->GetActiveCamera()->Elevation(28.0);
|
||
renB->GetActiveCamera()->Azimuth(30.0);
|
||
renB->GetActiveCamera()->Zoom(1.7);
|
||
renB->ResetCameraClippingRange();
|
||
rwB->Render();
|
||
const vtkIdType locNonBlack = countNonBlackPixels(rwB.Get(), winW, winH);
|
||
savePng(rwB.Get(), (shotDir / "lod-tuned-local.png").string());
|
||
|
||
// ---- 概览:level2 整卷(接受它就是细带)----
|
||
const int ovLevel = std::min(2, totLevels - 1);
|
||
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
|
||
auto rwO = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> renO;
|
||
renO->SetBackground(0.04, 0.04, 0.08);
|
||
rwO->AddRenderer(renO);
|
||
vtkSmartPointer<vtkVolume> volO =
|
||
buildTunedVolume(ovImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
|
||
exagg);
|
||
renO->AddVolume(volO);
|
||
const double ovFpsAfter = benchVolumeFps(rwO.Get(), renO, frames);
|
||
renO->ResetCamera();
|
||
renO->GetActiveCamera()->Elevation(50.0);
|
||
renO->GetActiveCamera()->Azimuth(20.0);
|
||
renO->ResetCameraClippingRange();
|
||
rwO->Render();
|
||
const vtkIdType ovNonBlack = countNonBlackPixels(rwO.Get(), winW, winH);
|
||
savePng(rwO.Get(), (shotDir / "lod-tuned-overview.png").string());
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool valid =
|
||
!textureErr && locNonBlack > 0 && ovNonBlack > 0;
|
||
|
||
const double dropPct =
|
||
locFpsBefore > 0 ? (locFpsBefore - locFpsAfter) / locFpsBefore * 100.0
|
||
: 0.0;
|
||
|
||
std::cout << "\n=== tune 视觉调优指标 ===\n";
|
||
std::cout << "局部段维度 : " << locDims[0] << "x" << locDims[1] << "x"
|
||
<< locDims[2] << " (level0)\n";
|
||
std::cout << "调优前局部 fps : "
|
||
<< (valid ? std::to_string(locFpsBefore) : "INVALID")
|
||
<< " (默认蓝白红, 不透明度 0.15, 无夸张)\n";
|
||
std::cout << "调优后局部 fps : "
|
||
<< (valid ? std::to_string(locFpsAfter) : "INVALID")
|
||
<< " (结构色阶, 不透明度 " << opacity << ", 夸张 " << exagg
|
||
<< "x)\n";
|
||
std::cout << "fps 变化 : " << dropPct
|
||
<< "% (正=变慢/负=变快; 探针预期近乎中性)\n";
|
||
std::cout << "调优后概览 fps : "
|
||
<< (valid ? std::to_string(ovFpsAfter) : "INVALID") << " (level"
|
||
<< ovLevel << ")\n";
|
||
std::cout << "双闸 : 纹理错=" << (textureErr ? "是" : "否")
|
||
<< " 局部非空=" << locNonBlack << " 概览非空=" << ovNonBlack
|
||
<< " → " << (valid ? "可信" : "INVALID") << "\n";
|
||
std::cout << "截图 : " << shotDir.string()
|
||
<< " (lod-tuned-local.png / lod-tuned-overview.png)\n";
|
||
|
||
writeMetricLine(
|
||
"tune,dir=" + dir + ",opacity=" + std::to_string(opacity) +
|
||
",exagg=" + std::to_string(exagg) +
|
||
",locDims=" + std::to_string(locDims[0]) + "x" +
|
||
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
|
||
",locFpsBefore=" + (valid ? std::to_string(locFpsBefore) : "INVALID") +
|
||
",locFpsAfter=" + (valid ? std::to_string(locFpsAfter) : "INVALID") +
|
||
",dropPct=" + std::to_string(dropPct) +
|
||
",ovFpsAfter=" + (valid ? std::to_string(ovFpsAfter) : "INVALID") +
|
||
",locNonBlack=" + std::to_string(locNonBlack) +
|
||
",ovNonBlack=" + std::to_string(ovNonBlack) +
|
||
",valid=" + std::to_string(valid ? 1 : 0));
|
||
return valid ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ② fps 预算:递增全分辨率(level0)窗口找「每帧体素预算」(Task 12d)
|
||
// ============================================================================
|
||
//
|
||
// 对递增的 level0 brick 列段(4,16,64,128,256 brick,可 --bricks 覆盖)各重组成
|
||
// 局部整卷 image 跑体绘制 fps,输出表 brick数/体素数/fps,找出 fps 跌破 30 的体素
|
||
// 阈值 = production LOD 每帧渲染的全分辨率块数上限。双闸防假帧率。
|
||
int cmdFpsBudget(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc fps-budget <storeDir> [--frames 90] "
|
||
"[--bricks 4,16,64,128,256]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const int frames = std::stoi(a.get("frames", "90"));
|
||
const double opacity = std::stod(a.get("opacity", "0.5"));
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
|
||
// 解析 brick 段列表(逗号分隔)。
|
||
std::vector<int> brickSteps;
|
||
{
|
||
const std::string raw = a.get("bricks", "4,16,64,128,256");
|
||
std::string cur;
|
||
for (char ch : raw) {
|
||
if (ch == ',') {
|
||
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
|
||
cur.clear();
|
||
} else {
|
||
cur.push_back(ch);
|
||
}
|
||
}
|
||
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
|
||
}
|
||
|
||
std::cout << "[fps-budget] storeDir=" << dir << " frames=" << frames << "\n";
|
||
std::cout << "[fps-budget] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[fps-budget] 闸门失败,中止,不产出 fps。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const int totBx = store.bricksX(0);
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
|
||
|
||
std::cout << "[fps-budget] level0=" << m.nx << "x" << m.ny << "x" << m.nz
|
||
<< " 总 brick列=" << totBx << " brick=" << m.brick << "\n";
|
||
|
||
struct Row {
|
||
int bricks;
|
||
long long voxels;
|
||
double fps;
|
||
bool valid;
|
||
};
|
||
std::vector<Row> rows;
|
||
|
||
constexpr double kTargetFps = 30.0;
|
||
long long budgetVoxels = -1; // fps 跌破 30 前的最大体素数
|
||
int budgetBricks = -1;
|
||
long long firstBelowVoxels = -1;
|
||
int firstBelowBricks = -1;
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
for (int nb : brickSteps) {
|
||
const int localBx = std::min(nb, totBx);
|
||
if (localBx <= 0) continue;
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> img =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int d[3];
|
||
img->GetDimensions(d);
|
||
const long long voxels =
|
||
static_cast<long long>(d[0]) * d[1] * d[2];
|
||
|
||
auto rw = makeOffscreenWindow(1024, 768);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(0.0, 0.0, 0.0);
|
||
rw->AddRenderer(ren);
|
||
vtkSmartPointer<vtkVolume> vol =
|
||
buildTunedVolume(img.Get(), m.quant, cs, vmin, vmax, opacity, exagg);
|
||
ren->AddVolume(vol);
|
||
|
||
const double fps = benchVolumeFps(rw.Get(), ren, frames);
|
||
// 双闸:纹理无错 + 该段渲出非空像素。
|
||
ren->ResetCamera();
|
||
rw->Render();
|
||
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), 1024, 768);
|
||
const bool valid = !capWin->textureError() && nonBlack > 0;
|
||
|
||
rows.push_back({localBx, voxels, fps, valid});
|
||
std::cout << "[fps-budget] brick=" << localBx << " (" << d[0] << "x" << d[1]
|
||
<< "x" << d[2] << ") 体素=" << voxels << " fps="
|
||
<< (valid ? std::to_string(fps) : "INVALID")
|
||
<< " 非空=" << nonBlack << "\n";
|
||
|
||
if (valid) {
|
||
if (fps >= kTargetFps) {
|
||
if (voxels > budgetVoxels) {
|
||
budgetVoxels = voxels;
|
||
budgetBricks = localBx;
|
||
}
|
||
} else if (firstBelowVoxels < 0) {
|
||
firstBelowVoxels = voxels;
|
||
firstBelowBricks = localBx;
|
||
}
|
||
}
|
||
}
|
||
|
||
const bool textureErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== fps-budget 每帧体素预算表 ===\n";
|
||
std::cout << "| brick段 | 维度体素数 | 体绘制 fps | ≥30 |\n";
|
||
std::cout << "|---|---|---|---|\n";
|
||
for (const auto& r : rows) {
|
||
std::cout << "| " << r.bricks << " | " << r.voxels << " | "
|
||
<< (r.valid ? std::to_string(r.fps) : std::string("INVALID"))
|
||
<< " | " << (r.valid && r.fps >= kTargetFps ? "是" : "否")
|
||
<< " |\n";
|
||
}
|
||
std::cout << "\n每帧体素预算(fps≥30 上限) : "
|
||
<< (budgetVoxels >= 0 ? std::to_string(budgetVoxels) +
|
||
" 体素 (" + std::to_string(budgetBricks) +
|
||
" brick列)"
|
||
: std::string("未触达(所有测点均 ≥30)"))
|
||
<< "\n";
|
||
std::cout << "首个跌破 30 的窗口 : "
|
||
<< (firstBelowVoxels >= 0
|
||
? std::to_string(firstBelowVoxels) + " 体素 (" +
|
||
std::to_string(firstBelowBricks) + " brick列)"
|
||
: std::string("无(测点未跌破; 需更大 --bricks)"))
|
||
<< "\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否")
|
||
<< "\n";
|
||
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||
|
||
// 落 last-metrics + 追加写 poc-results-C.md。
|
||
for (const auto& r : rows) {
|
||
writeMetricLine(
|
||
"fps-budget,dir=" + dir + ",bricks=" + std::to_string(r.bricks) +
|
||
",voxels=" + std::to_string(r.voxels) +
|
||
",fps=" + (r.valid ? std::to_string(r.fps) : "INVALID") +
|
||
",valid=" + std::to_string(r.valid ? 1 : 0));
|
||
}
|
||
|
||
{
|
||
const fs::path repo =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||
fs::create_directories(repo.parent_path());
|
||
std::ofstream rf(repo.string(), std::ios::app);
|
||
if (rf) {
|
||
rf << "\n\n# POC-C fps 预算探针结果(Task 12d ②)\n\n";
|
||
rf << "金字塔 store: " << dir << "(level0=" << m.nx << "x" << m.ny << "x"
|
||
<< m.nz << ",brick=" << m.brick << ")\n\n";
|
||
rf << "递增 level0 局部窗口(沿线中段 brick 列)体绘制 fps:\n\n";
|
||
rf << "| brick段 | 体素数 | 体绘制 fps | ≥30fps |\n|---|---|---|---|\n";
|
||
for (const auto& r : rows) {
|
||
rf << "| " << r.bricks << " | " << r.voxels << " | "
|
||
<< (r.valid ? std::to_string(r.fps) : "INVALID") << " | "
|
||
<< (r.valid && r.fps >= kTargetFps ? "是" : "否") << " |\n";
|
||
}
|
||
rf << "\n- **每帧体素预算(fps≥30 上限)**: "
|
||
<< (budgetVoxels >= 0
|
||
? std::to_string(budgetVoxels) + " 体素(" +
|
||
std::to_string(budgetBricks) + " brick 列)"
|
||
: "未触达,所有测点 ≥30fps")
|
||
<< "\n";
|
||
rf << "- 首个跌破 30 的窗口: "
|
||
<< (firstBelowVoxels >= 0
|
||
? std::to_string(firstBelowVoxels) + " 体素(" +
|
||
std::to_string(firstBelowBricks) + " brick 列)"
|
||
: "无(需更大 --bricks 段触达天花板)")
|
||
<< "\n";
|
||
rf << "- 双闸:纹理维度错误=" << (textureErr ? "是" : "否")
|
||
<< ";每段均按非空像素校验。\n";
|
||
rf << "- production LOD 应把【每帧渲染的全分辨率块】卡在此预算以内。\n";
|
||
rf << "- **本机 RTX 3060 上限数;最低配需用户在目标机跑 fps-budget/view。**\n";
|
||
}
|
||
std::cout << "[fps-budget] 报告追加写入 " << repo.string() << "\n";
|
||
}
|
||
|
||
return textureErr ? 1 : 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 视觉调参画廊(Task 12d gallery):view --preview --variant N
|
||
// ============================================================================
|
||
//
|
||
// 同一局部段(沿线中段 kViewDefaultLocalBricks 列全分辨率) + 同一相机框法
|
||
// (ResetCamera→Elevation/Azimuth→Zoom),只换「不透明度包络 / 配色 / 取景角度 /
|
||
// 背景」四组视觉参数,各存一张 PNG 供控制方挑选。fps 对视觉调参近乎中性,每组实测验证。
|
||
//
|
||
// 注:交互窗口(无 flag 的 view)默认即采用 var4(kViewDefaultVariant)——配色/不透明度
|
||
// 包络/取景/exagg/背景全部走同一份 var4 参数,故「交互默认画面 == view-var4」。
|
||
enum class OpacityProfile {
|
||
kSolid, // V 形实体感:中高值段普遍可见,半透明实心块
|
||
kStructural, // 现有双端斜坡:仅正负两端不透明(对照基线)
|
||
};
|
||
enum class ColorChoice { kStructural, kSeismic, kJet };
|
||
|
||
struct GalleryVariant {
|
||
const char* name; // 文件名后缀:view-<name>.png
|
||
OpacityProfile profile;
|
||
ColorChoice color;
|
||
double floorOpacity; // 近零背景不透明度(kSolid 用)
|
||
double midOpacity; // 中值段不透明度(kSolid 用)
|
||
double maxOpacity; // 两端峰值不透明度
|
||
double exagg; // 垂向夸张
|
||
double elevation; // ResetCamera 后 Elevation
|
||
double azimuth; // 再 Azimuth
|
||
double zoom; // 再 Zoom 填满画面
|
||
double bg[3]; // 背景 RGB
|
||
const char* desc; // 报告用中文说明
|
||
// ---- C4 视觉调优(梯度不透明度 + 光照),默认关 → 不改既有变体行为 ----
|
||
bool useGradientOpacity = false; // SetGradientOpacity:均匀层透明、界面/异常显形
|
||
bool useShade = false; // SetShade:层界面带立体明暗
|
||
};
|
||
|
||
// C4 视觉调优:4 组对照(纯标量 / +梯度不透明度 / +光照 / 全开)。
|
||
// 同一局部段、同一 seismic 配色、同一斜穿俯视取景(El45/Az30,视线穿进体内部而非
|
||
// 只看端面)——唯一变量是「梯度不透明度 / 光照」,供肉眼直读"内部结构是否透出"。
|
||
// maxOpacity 字段:纯标量组用低峰值(0.45,避免均匀积分糊成实心);开了梯度门的组用
|
||
// 高峰值(0.6)——均匀区被梯度门压透明后,层界面的净不透明度(标量×梯度)才够高、浮成实面。
|
||
// 末项 var4(全开) = kViewDefaultVariant → 交互窗口默认走全开。
|
||
const GalleryVariant kGalleryVariants[] = {
|
||
// var1:纯标量不透明度(基线)——无梯度门、无光照。GPR 均匀层段沿射线积分糊成
|
||
// 半透明实心块,只外表面/端面可读,内部水平分层难分辨(对照基准)。
|
||
{"var1", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.04, 0.30, 0.45, 8.0, 45.0, 30.0, 1.5, {0.05, 0.05, 0.09},
|
||
"纯标量不透明度(基线):V形包络(floor0.04/mid0.30/max0.45)+seismic,"
|
||
"无梯度门/无光照,均匀层积分糊成半透明块、仅端面可读",
|
||
/*useGradientOpacity=*/false, /*useShade=*/false},
|
||
// var2:+梯度不透明度——低梯度(均匀层)透明、高梯度(层界面/异常)显形,射线穿透
|
||
// 均匀雾、内部界面浮出。标量峰值提到 0.6(梯度门压透明后层面才够实)。
|
||
{"var2", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.04, 0.30, 0.60, 8.0, 45.0, 30.0, 1.5, {0.05, 0.05, 0.09},
|
||
"+梯度不透明度:低梯度(均匀层)透明、高梯度(界面/异常)显形,标量峰值0.6,"
|
||
"射线穿透均匀雾、内部界面浮出",
|
||
/*useGradientOpacity=*/true, /*useShade=*/false},
|
||
// var3:+光照——在梯度门基础上 ShadeOn,层界面带立体明暗(非纯平涂)。
|
||
{"var3", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.04, 0.30, 0.60, 8.0, 45.0, 30.0, 1.5, {0.05, 0.05, 0.09},
|
||
"+光照(梯度门+Shade):层界面带立体明暗(Ambient0.3/Diffuse0.7/Specular0.2),"
|
||
"界面起伏更有体积感",
|
||
/*useGradientOpacity=*/true, /*useShade=*/true},
|
||
// var4:全开——梯度不透明度 + 光照 + seismic 双端斜坡配色 + 略亮冷灰背景。
|
||
// 综合最佳,选作交互窗口默认(kViewDefaultVariant)。
|
||
{"var4", OpacityProfile::kSolid, ColorChoice::kSeismic,
|
||
0.04, 0.30, 0.60, 8.0, 45.0, 30.0, 1.5, {0.07, 0.08, 0.11},
|
||
"全开(综合最佳):梯度不透明度+光照+seismic+略亮冷灰背景,内部分层界面/异常"
|
||
"从均匀块透出 → 交互窗口默认",
|
||
/*useGradientOpacity=*/true, /*useShade=*/true},
|
||
};
|
||
|
||
// 交互窗口(无 flag 的 view)的默认视觉变体 = var4(kGalleryVariants 末项)。
|
||
// 交互默认与 view-var4 走同一份参数 → 二者画面一致(DRY,不复制粘贴漂移)。
|
||
const GalleryVariant& kViewDefaultVariant =
|
||
kGalleryVariants[sizeof(kGalleryVariants) / sizeof(kGalleryVariants[0]) - 1];
|
||
|
||
geopro::core::ColorScale pickColor(ColorChoice c, double vmin, double vmax) {
|
||
switch (c) {
|
||
case ColorChoice::kSeismic: return makeSeismicColorScale(vmin, vmax);
|
||
case ColorChoice::kJet: return makeJetColorScale(vmin, vmax);
|
||
case ColorChoice::kStructural:
|
||
default: return makeStructuralColorScale(vmin, vmax);
|
||
}
|
||
}
|
||
|
||
// 按变体的不透明度包络建体属性(gallery / 交互默认共用,DRY)。kSolid 走 V 形实体
|
||
// 包络(floor/mid/max),kStructural 走双端斜坡(仅 maxOpacity)。
|
||
//
|
||
// C4 视觉调优:若变体开了 useGradientOpacity 且传入了实测梯度统计 gs,再叠加
|
||
// 1) 梯度不透明度(SetGradientOpacity):低梯度(均匀层)→透明、高梯度(界面/异常)→显形,
|
||
// 让射线穿透均匀雾、内部界面浮出(阈值按 gs 的 median/p90/p99 标定,不靠猜);
|
||
// 2) 光照(useShade → SetShade + Ambient/Diffuse/Specular):层界面带立体明暗。
|
||
vtkSmartPointer<vtkVolumeProperty> makeVariantProperty(
|
||
const GalleryVariant& v, const geopro::core::Quant& q,
|
||
const geopro::core::ColorScale& cs, double vmin, double vmax,
|
||
double maxOpacity, const GradStats* gs = nullptr) {
|
||
vtkSmartPointer<vtkVolumeProperty> prop;
|
||
if (v.profile == OpacityProfile::kSolid) {
|
||
prop = makeSolidVolumeProperty(q, cs, vmin, vmax, v.floorOpacity,
|
||
v.midOpacity, maxOpacity);
|
||
} else {
|
||
prop = makeTunedVolumeProperty(q, cs, vmin, vmax, maxOpacity);
|
||
}
|
||
|
||
// 梯度不透明度:均匀层(低梯度)透明,层界面/异常边缘(高梯度)显形。阈值按实测分布:
|
||
// median 处仍 0(压住均匀积分雾),p90 升到 0.5,p99 到 0.9。无 gs(未测)则跳过。
|
||
if (v.useGradientOpacity && gs != nullptr && gs->samples > 0) {
|
||
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);
|
||
}
|
||
|
||
// 光照:ShadeOn + Ambient/Diffuse,保留立体明暗;Specular 压到 0.05(近乎关)避免
|
||
// 旋转时视角相关的高光在体表游走形成「移动白斑」。保留 ambient/diffuse 立体感。
|
||
if (v.useShade) {
|
||
prop->ShadeOn();
|
||
prop->SetAmbient(0.3);
|
||
prop->SetDiffuse(0.7);
|
||
prop->SetSpecular(0.05);
|
||
prop->SetSpecularPower(10.0);
|
||
}
|
||
return prop;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ③ view:真窗口可交互(给用户肉眼测 + 最低配机跑)(Task 12d)
|
||
// ============================================================================
|
||
//
|
||
// 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂
|
||
// OutOfCoreSource:相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保
|
||
// 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。
|
||
// 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。
|
||
//
|
||
// 离屏 smoke:--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。
|
||
|
||
// 整卷单张 3D 纹理的轴上限(同 renderLOD/renderB 实测 GL_MAX_3D_TEXTURE_SIZE)。
|
||
constexpr int kViewMax3DTex = 16384;
|
||
|
||
// 单纹理统一渲染(Task 12d-singletex):
|
||
//
|
||
// 交互 view 不再用 vtkMultiBlockVolumeMapper + budget 分块(缺块、个位数 fps)。
|
||
// 任何相机位置都只渲【一张 vtkImageData + 单个 vtkSmartVolumeMapper】,与 --preview
|
||
// 走完全同一条产单图 + 同一 mapper 的路径,保证一致、高 fps(与预览 184fps 同档)。
|
||
//
|
||
// LOD 选层规则(拉近变细、拉远变粗):
|
||
// - 远观/中景(相机选中粗层)→ 升到最细的「整卷各轴 ≤16384」层(本数据 L2:11119、
|
||
// L3:5560),整卷重组成一张纹理,任何缩放都显示完整体,绝不缺块。
|
||
// - 拉近(相机选中 level0/1,X 超 16384 无法整卷成单纹理)→ 取当前视野在 level0 的
|
||
// X 子区域(沿线裁一段,使子体各轴 ≤16384)重组一张纹理。
|
||
// 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。
|
||
|
||
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。
|
||
//
|
||
// 渲染源 = ViewAdaptiveVolumeSource(C2):每次交互结束 source->update(cam) 用 C1
|
||
// selectLod 选层选区 → 从分块存储重组【当前视野区域单图】→ 喂单 SmartVolumeMapper。
|
||
// 退掉旧 POC 简化路径(viewPickLevel/wholeVolumeLevelFor/viewLocalBrickRange/缓存
|
||
// 三件套/MultiBlock 分块全部由 C1+C2 承担)。
|
||
struct ViewState {
|
||
geopro::render::ViewAdaptiveVolumeSource* source = nullptr;
|
||
vtkSmartVolumeMapper* mapper = nullptr; // 高清层:单 SmartVolumeMapper(叠在底图上)
|
||
vtkSmartVolumeMapper* baseMapper = nullptr; // 常驻底图层 mapper(用于按高清块挖空 cropping)
|
||
vtkRenderer* ren = nullptr;
|
||
vtkCamera* cam = nullptr;
|
||
vtkTextActor* fpsText = nullptr;
|
||
vtkRenderWindow* rw = nullptr;
|
||
double exagg = 8.0;
|
||
int lastLevel = -1;
|
||
// 持有当前高清单图引用,避免被释放(mapper 仅持裸指针)。
|
||
vtkSmartPointer<vtkImageData> currentImg;
|
||
// 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。
|
||
bool inCb = false;
|
||
};
|
||
|
||
// C3-8 底图按高清块挖空:用 vtkVolumeMapper 的 Cropping 在【底图 mapper】上裁掉高清块
|
||
// 覆盖的那一块,使任意时刻 = 底图(高清区外) + 高清(高清区内),无空间重叠 → 不再双渲发白。
|
||
//
|
||
// 裁剪平面用高清单图的【模型坐标包围盒】(GetBounds)。底图与高清两 actor 用同一份
|
||
// SetScale(1,1,exagg),且高清单图自带绝对世界 origin(buildLocalLevel0Image 沿 X 偏移),
|
||
// 与底图同坐标系 → 高清块的模型盒平面直接作底图 cropping 平面即对齐(两层 scale 一致)。
|
||
//
|
||
// CroppingRegionFlags:6 个平面把空间分成 3×3×3=27 区,中心区(盒内)= bit 0x0002000
|
||
// (VTK_CROP_SUBVOLUME)。要「渲盒外、挖掉盒内」→ 全 27 区减中心区 = 0x7ffffff & ~0x0002000。
|
||
void viewSyncBaseCropping(ViewState* st) {
|
||
if (st->baseMapper == nullptr) return;
|
||
if (st->currentImg == nullptr) { // 高清未就绪:底图不裁剪,全渲(绝不空白)
|
||
st->baseMapper->SetCropping(0);
|
||
return;
|
||
}
|
||
double b[6];
|
||
st->currentImg->GetBounds(b); // 模型坐标盒(含绝对 X origin),与底图同系
|
||
st->baseMapper->SetCroppingRegionPlanes(b[0], b[1], b[2], b[3], b[4], b[5]);
|
||
st->baseMapper->SetCroppingRegionFlags(0x7ffffff & ~VTK_CROP_SUBVOLUME);
|
||
st->baseMapper->SetCropping(1);
|
||
}
|
||
|
||
// C3-2 非阻塞拉取:把最新已就绪单图喂 mapper(若有新结果)。不阻塞主线程——
|
||
// 后台 builder 没新结果就沿用上一帧(拖动跟手的关键)。返回 1=喂了新图,0=无变化。
|
||
std::size_t viewPickLatest(ViewState* st) {
|
||
auto imgs = st->source->currentImages(); // 内部 takeLatest(非阻塞)
|
||
if (imgs.empty() || imgs[0] == nullptr) return 0; // 无新结果:保留上一帧
|
||
if (imgs[0] == st->currentImg) return 0; // 同一张:无需重喂
|
||
st->currentImg = imgs[0];
|
||
st->lastLevel = st->source->lastLevel();
|
||
st->mapper->SetInputData(st->currentImg);
|
||
st->mapper->Update();
|
||
viewSyncBaseCropping(st); // 高清块换位 → 同步更新底图挖空盒,保持无缝无重叠
|
||
return 1;
|
||
}
|
||
|
||
// 单纹理刷新(C3-2 异步):source->update(cam) 只【提交目标】(非阻塞),随后非阻塞
|
||
// 拉一次最新就绪。拖动中主线程不被重组卡住——新纹理由后台备好、下一帧/定时器换上。
|
||
// 返回当前喂入的块数(1=有就绪单图,0=尚无就绪/视锥外)。
|
||
std::size_t viewRefreshSingle(ViewState* st) {
|
||
st->source->update(st->cam); // 提交目标,立即返回
|
||
viewPickLatest(st);
|
||
return st->currentImg != nullptr ? 1 : 0;
|
||
}
|
||
|
||
// 阻塞式刷新:提交目标后【轮询到就绪】再返回(带超时)。仅用于 preview/smoke/默认
|
||
// 取景这类「需要保证拿到一张图」的离屏/初始化场景——交互路径绝不用此(会卡主线程)。
|
||
std::size_t viewRefreshBlocking(ViewState* st, int maxTries = 3000,
|
||
int sleepMs = 2) {
|
||
st->source->update(st->cam); // 提交目标
|
||
for (int i = 0; i < maxTries; ++i) {
|
||
if (viewPickLatest(st)) return 1;
|
||
if (st->currentImg != nullptr) return 1; // 已有上一就绪且无新结果
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||
}
|
||
return st->currentImg != nullptr ? 1 : 0;
|
||
}
|
||
|
||
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
|
||
//
|
||
// fps 修复(Task 12d-fix3):之前用 frameTimer(上次回调到本次的墙钟)算 fps,把
|
||
// 用户思考/不动的空闲时间也算进去,显示的是「空闲间隔」(如 0.2fps),不可信。改为
|
||
// 松手时连渲 kFpsProbeFrames 帧、累计「实际 Render 耗时」取均值,得到真实渲染帧率。
|
||
void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewState*>(clientData);
|
||
// 防重入:本回调内部会 st->rw->Render(),若该 Render 再触发观察者进本回调
|
||
// 将无限递归。已在回调中则直接返回(双保险)。
|
||
if (st->inCb) return;
|
||
st->inCb = true;
|
||
// EndInteraction 时重选 LOD + 重组单图(仅松手触发一次,避免拖动中卡)。
|
||
const std::size_t blocks = viewRefreshSingle(st);
|
||
st->ren->ResetCameraClippingRange();
|
||
const int lvl = st->lastLevel;
|
||
|
||
// 真实渲染帧率:连渲若干帧,只累计 Render() 本身耗时(不含空闲)。首帧含切换后
|
||
// 的纹理上传/shader 编译,故跑 kFpsProbeFrames 帧取均值更可信。
|
||
constexpr int kFpsProbeFrames = 3;
|
||
Stopwatch swR;
|
||
for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render();
|
||
const double renderMs = swR.elapsedMs() / kFpsProbeFrames;
|
||
const double fps = renderMs > 0 ? 1000.0 / renderMs : 0.0;
|
||
|
||
char buf[256];
|
||
std::snprintf(buf, sizeof(buf),
|
||
"fps: %.1f | LOD level: %d | blocks: %zu | exagg: %.0fx",
|
||
fps, lvl, blocks, st->exagg);
|
||
st->fpsText->SetInput(buf);
|
||
st->lastLevel = lvl;
|
||
st->rw->Render(); // 末帧带上更新后的 fps 文本
|
||
st->inCb = false;
|
||
}
|
||
|
||
// C3-2 拖动跟手核心:交互进行中(旋转/缩放每次相机变化)只【提交目标】(非阻塞),
|
||
// 绝不在主线程重组——主线程立刻继续响应输入,画面用上一张已就绪纹理(跟手)。
|
||
void viewOnInteracting(vtkObject*, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewState*>(clientData);
|
||
st->source->update(st->cam); // 提交最新视野目标,立即返回(supersede 旧目标)
|
||
}
|
||
|
||
// C3-2 定时器:周期性非阻塞拉取后台已就绪的新纹理换上 → 拖动中/松手后新 LOD 备好
|
||
// 即自然显示,主线程从不被重组卡住。无新结果则什么也不做(不重渲、不抖)。
|
||
void viewOnTimer(vtkObject* caller, unsigned long, void* clientData, void*) {
|
||
auto* st = static_cast<ViewState*>(clientData);
|
||
if (st->inCb) return; // 与 fps 探针回调互斥,避免重入
|
||
if (viewPickLatest(st)) {
|
||
st->ren->ResetCameraClippingRange();
|
||
st->rw->Render(); // 仅在确有新纹理时重渲
|
||
}
|
||
(void)caller;
|
||
}
|
||
|
||
// 默认取景宽度:沿测线取约 256 道(=4 brick 列×64)的一段作首帧局部段。整线横截面
|
||
// 相对长度 1:34,框整卷只会看到一条隐形细带;框这个局部段,层状结构才充满视野
|
||
// (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面
|
||
// 越填不满画面;256 道是 ① cmdTune 出 lod-tuned-local.png(有清晰层状结构)的取景,
|
||
// 沿用之以保证首帧同等可读。
|
||
constexpr int kViewDefaultLocalBricks = 4;
|
||
|
||
// 建立 view 的「默认取景」:把 level0 一段局部体(沿线中段)整卷单块喂 mapper,再
|
||
// ResetCamera 到该局部段(actor 已 SetScale(1,exagg,exagg)),置相机为能看出层状
|
||
// 结构的角度。真窗口 / --smoke / --preview 三条路径共用此函数 → 渲的是同一画面。
|
||
// 取景角度(Elevation/Azimuth/Zoom)取自 kViewDefaultVariant(var4),与 view-var4 一致。
|
||
//
|
||
// 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0(默认即全分辨率局部段)。
|
||
std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
|
||
geopro::render::ViewAdaptiveVolumeSource& source = *st->source;
|
||
const geopro::data::StoreMeta& m = source.meta();
|
||
|
||
// 首帧默认取景:先把相机放到沿线中段一个局部窗口(~kViewDefaultLocalBricks 列)
|
||
// 正前方近观 → 触发 C1 选 level0 + 视野子区间,C2 重组该视野单图。先用「局部段
|
||
// 包围盒」ResetCamera 把相机框到该段,再 source->update(cam) 让 C1/C2 选区重组。
|
||
const int brick = m.brick;
|
||
const int totBricksX = (m.nx + brick - 1) / brick;
|
||
const int localBx = std::min(kViewDefaultLocalBricks, totBricksX);
|
||
const int bx0 = std::max(0, totBricksX / 2 - localBx / 2); // 沿线中段
|
||
// 该局部段世界 X 范围(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);
|
||
|
||
auto rw = makeOffscreenWindow(winW, winH);
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]);
|
||
rw->AddRenderer(ren);
|
||
|
||
// 局部段(沿线中段,同 viewSetupDefaultFrame 的取段法)。先建体 → 实测梯度分布
|
||
// 用于 C4 梯度不透明度标定(仅开了 useGradientOpacity 的变体需要)。
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
|
||
GradStats gs;
|
||
if (v.useGradientOpacity) {
|
||
gs = sampleGradientMagnitude(locImg.Get());
|
||
std::cout << "[gallery " << v.name << "] 梯度幅值分布(量化域,样本 " << gs.samples
|
||
<< "): median=" << gs.median << " p90=" << gs.p90
|
||
<< " p99=" << gs.p99 << " max=" << gs.mx << "\n";
|
||
}
|
||
|
||
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
|
||
v, m.quant, cs, vmin, vmax, v.maxOpacity,
|
||
v.useGradientOpacity ? &gs : nullptr);
|
||
|
||
vtkNew<vtkMultiBlockVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, v.exagg, v.exagg);
|
||
ren->AddVolume(volume);
|
||
|
||
std::vector<vtkSmartPointer<vtkImageData>> one{locImg};
|
||
auto mb = makeMultiBlock(one);
|
||
mapper->SetInputDataObject(mb);
|
||
mapper->Update();
|
||
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(v.elevation);
|
||
cam->Azimuth(v.azimuth);
|
||
cam->Zoom(v.zoom);
|
||
ren->ResetCameraClippingRange();
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
rw->Render();
|
||
|
||
const fs::path shotDir =
|
||
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-var4(DRY,与画廊同源)。命令行 --exagg /
|
||
// --opacity 若用户显式传则覆盖 var4 对应值,否则用 var4 的 exagg / maxOpacity。
|
||
const GalleryVariant& dv = kViewDefaultVariant;
|
||
const double exagg =
|
||
a.kv.count("exagg") ? std::stod(a.get("exagg", "8")) : dv.exagg;
|
||
const double opacity = a.kv.count("opacity")
|
||
? std::stod(a.get("opacity", "0.5"))
|
||
: dv.maxOpacity;
|
||
const std::size_t budget =
|
||
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
|
||
const int frames = std::stoi(a.get("frames", "90"));
|
||
auto hasFlag = [&](const char* name) {
|
||
return a.kv.count(name) > 0 ||
|
||
std::find(a.positional.begin(), a.positional.end(),
|
||
std::string("--") + name) != a.positional.end();
|
||
};
|
||
const bool smoke = hasFlag("smoke");
|
||
const bool preview = hasFlag("preview");
|
||
// C3-8 验收用:--preview --shots 额外从【真 view 场景(base+hires+cropping)】多旋转角
|
||
// 离屏出图,用于人工核对「路细长不胖 / 拼接无缝无白 / 旋转无移动白斑」。只加图,不改
|
||
// 默认行为(无 --shots 时与原 preview 完全一致)。
|
||
const bool shots = hasFlag("shots");
|
||
// C3-6 底图预览:--preview --base(或 --base)模拟「交互态:只渲常驻粗底图」——
|
||
// 隐去高清叠加层、只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、绝不空白。
|
||
const bool basePreview = hasFlag("base");
|
||
// 拉近预览(Task 12d-singletex):--preview --variant near(或 --near)走与真窗口
|
||
// 完全相同的单纹理拉近路径(viewRefreshSingle 选 level0 局部子区域),供控制方 Read
|
||
// 验证「拉近后」单图非空、完整、fps 高。
|
||
const bool nearPreview =
|
||
hasFlag("near") || (a.kv.count("variant") && a.get("variant", "") == "near");
|
||
|
||
// 画廊模式(Task 12d):渲 4 组视觉调参图供挑选。优先于其余路径。
|
||
if (hasFlag("gallery")) {
|
||
return cmdViewGallery(dir, frames);
|
||
}
|
||
// 单变体:view --preview --variant N(N=1..4),只渲第 N 组(near 不走此路)。
|
||
if (preview && a.kv.count("variant") && !nearPreview) {
|
||
const int vi = std::stoi(a.get("variant", "1"));
|
||
const int n = static_cast<int>(sizeof(kGalleryVariants) /
|
||
sizeof(kGalleryVariants[0]));
|
||
if (vi < 1 || vi > n) {
|
||
std::cerr << "[view] --variant 需在 1.." << n << " 之间\n";
|
||
return 2;
|
||
}
|
||
std::cout << "[view] storeDir=" << dir << " 单变体 variant=" << vi << "\n";
|
||
if (cmdOffscreenSmoke() != 0) return 1;
|
||
return runGalleryVariant(dir, kGalleryVariants[vi - 1], frames);
|
||
}
|
||
|
||
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
|
||
<< " opacity=" << opacity << " budget=" << budget
|
||
<< (preview ? " [PREVIEW 离屏存图+测fps]"
|
||
: (smoke ? " [SMOKE 离屏]" : " [真窗口交互]"))
|
||
<< "\n";
|
||
|
||
// preview/smoke 走离屏。
|
||
const bool offscreen = smoke || preview;
|
||
const int winW = 1280, winH = 800;
|
||
|
||
// 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产
|
||
// 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。
|
||
(void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。
|
||
// 渲染源 = 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;
|
||
// 配色/不透明度包络取自 var4:seismic + V 形实体包络(floor/mid + opacity 作峰值)。
|
||
const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax);
|
||
// C4:默认变体(var4)开了梯度不透明度 → 从常驻底图实测梯度分布标定阈值。底图恒非空。
|
||
GradStats dvGs;
|
||
if (dv.useGradientOpacity && source.baseImage() != nullptr) {
|
||
dvGs = sampleGradientMagnitude(source.baseImage());
|
||
std::cout << "[view] 梯度幅值分布(底图,量化域,样本 " << dvGs.samples
|
||
<< "): median=" << dvGs.median << " p90=" << dvGs.p90
|
||
<< " p99=" << dvGs.p99 << "\n";
|
||
}
|
||
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
|
||
dv, m.quant, cs, vmin, vmax, opacity,
|
||
dv.useGradientOpacity ? &dvGs : nullptr);
|
||
|
||
// 渲染窗口:preview/smoke 走离屏,否则真窗口。
|
||
vtkSmartPointer<vtkRenderWindow> rw;
|
||
if (offscreen) {
|
||
rw = makeOffscreenWindow(winW, winH);
|
||
} else {
|
||
rw = vtkSmartPointer<vtkRenderWindow>::New();
|
||
rw->SetSize(winW, winH);
|
||
rw->SetWindowName("gpr_poc view —— 核外 LOD 体绘制 (滚轮缩放切 LOD, 左键旋转)");
|
||
}
|
||
|
||
vtkNew<vtkRenderer> ren;
|
||
ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景
|
||
rw->AddRenderer(ren);
|
||
|
||
// C3-6 常驻粗底图层(底图永不空白的命脉):第一个 vtkVolume = 整卷最粗
|
||
// 「各轴 ≤16384」层单纹理(source.baseImage(),构造时主线程一次建成、永远持有)。
|
||
// 它【永远在场、永远渲染、绝不释放、绝不被异步路径触碰】→ 任何相机/任何运动中都
|
||
// 盖住整个体 → 拖动/缩放绝不空白。高清层(下方 mapper)叠在其上、就绪后局部覆盖。
|
||
vtkNew<vtkSmartVolumeMapper> baseMapper;
|
||
baseMapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
baseMapper->SetAutoAdjustSampleDistances(1);
|
||
baseMapper->SetInteractiveAdjustSampleDistances(1);
|
||
auto baseVolume = vtkSmartPointer<vtkVolume>::New();
|
||
if (source.baseImage() != nullptr) { // 退化 store 防护(理论恒非空)
|
||
baseMapper->SetInputData(source.baseImage()); // 常驻输入,永不改
|
||
baseMapper->Update();
|
||
baseVolume->SetMapper(baseMapper);
|
||
baseVolume->SetProperty(prop); // 与高清层共用传函(同配色/不透明度)
|
||
baseVolume->SetScale(1.0, 1.0, exagg); // 垂向夸张只放大深度(Z);横向路宽(Y)不动 → 与高清层空间对齐
|
||
ren->AddVolume(baseVolume); // 先加底图 → 底层常渲
|
||
}
|
||
|
||
// 高清叠加层:单 vtkSmartVolumeMapper(GPU 光线投射,整张 3D 纹理),与 --preview /
|
||
// gallery 同一 mapper 类型。叠在底图之上:currentImages 就绪后摆到对应世界位置局部
|
||
// 覆盖底图;没就绪则无输入(只显底图,不空)。运动中高清滞后由底图兜底,绝不空白。
|
||
vtkNew<vtkSmartVolumeMapper> mapper;
|
||
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||
// C3-5:交互式采样距离自适应(修长板填屏 ray-march 慢的关键)。POC 当初为离屏
|
||
// 基准把这两个关掉(0/0)以求恒定全质量,但交互场景必须【开启】:拖动时按渲染窗口的
|
||
// DesiredUpdateRate 拉大 SampleDistance(降采样→快)、停下恢复小步长(全质量→可慢)。
|
||
// 长板填屏慢的根因是每像素沿超长轴海量采样,LOD/异步都不缩短 ray-march 长度,只有
|
||
// 拖动期降采样才把交互态降到可跟手帧率。
|
||
mapper->SetAutoAdjustSampleDistances(1);
|
||
mapper->SetInteractiveAdjustSampleDistances(1);
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, 1.0, exagg); // 垂向夸张只放大深度(Z);横向路宽(Y)保持真实比例(修 GPR 路被压胖)
|
||
ren->AddVolume(volume); // 后加高清 → 叠在底图上
|
||
|
||
// 屏幕左上角实时 fps 文本。
|
||
vtkNew<vtkTextActor> fpsText;
|
||
fpsText->SetInput("fps: -- | LOD level: --");
|
||
fpsText->GetTextProperty()->SetFontSize(20);
|
||
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
|
||
fpsText->SetDisplayPosition(12, winH - 30);
|
||
ren->AddViewProp(fpsText);
|
||
|
||
// 捕获式 OutputWindow(拦截块上传纹理错)。
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
ViewState st;
|
||
st.source = &source;
|
||
st.mapper = mapper.Get();
|
||
st.baseMapper = baseMapper.Get(); // 供 viewSyncBaseCropping 按高清块挖空底图
|
||
st.ren = ren.Get();
|
||
st.fpsText = fpsText.Get();
|
||
st.rw = rw.Get();
|
||
st.exagg = exagg;
|
||
|
||
// 相机初始定向(修复 1):默认框「局部段」而非整卷。整线横截面 1:34,框整卷
|
||
// 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口
|
||
// 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。
|
||
std::size_t warm = viewSetupDefaultFrame(&st, ren);
|
||
rw->Render();
|
||
|
||
// C3-6 底图预览(模拟「交互态:只渲常驻粗底图」):隐去高清叠加层、把相机框到整卷
|
||
// 包围盒(exagg 后),只渲整卷最粗 ≤16384 层单纹理 → 整卷概览、盖全、非空。证明
|
||
// 拖动/缩放时即使高清全部缺位,底图也独立盖住整个体(永不空白的命脉)。
|
||
if (preview && basePreview) {
|
||
volume->SetVisibility(0); // 隐去高清层 → 只剩常驻底图
|
||
baseMapper->SetCropping(0); // 高清隐了 → 底图取消挖空,整卷全渲(证明永不空白)
|
||
// 框整卷(无参 ResetCamera 按场景中可见 actor 包围盒;高清隐了 → 框底图全卷)。
|
||
ren->ResetCamera();
|
||
st.cam = ren->GetActiveCamera();
|
||
st.cam->Elevation(kViewDefaultVariant.elevation);
|
||
st.cam->Azimuth(kViewDefaultVariant.azimuth);
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const std::string pngPath = (shotDir / "view-base.png").string();
|
||
savePng(rw.Get(), pngPath);
|
||
|
||
auto countStructPx = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType baseStruct = countStructPx();
|
||
int bd[3] = {0, 0, 0};
|
||
if (source.baseImage()) source.baseImage()->GetDimensions(bd);
|
||
const bool texErrB = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool okB = !texErrB && source.baseImage() != nullptr && baseStruct > 0;
|
||
|
||
std::cout << "\n=== view --preview --base 常驻粗底图验证(整卷概览)===\n";
|
||
std::cout << "底图 level : " << source.baseLevel()
|
||
<< "(整卷最粗 ≤16384 层)\n";
|
||
std::cout << "底图维度 : " << bd[0] << " x " << bd[1] << " x " << bd[2]
|
||
<< "\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "结构像素(>50) : " << baseStruct << " / " << (winW * winH)
|
||
<< " (" << (100.0 * baseStruct / (winW * winH)) << "%)\n";
|
||
std::cout << "纹理维度错误 : " << (texErrB ? "是(!!)" : "否") << "\n";
|
||
std::cout << "base 结果 : "
|
||
<< (okB ? "OK ✔ 底图盖全非空" : "FAIL ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-base,dir=" + dir + ",baseLevel=" +
|
||
std::to_string(source.baseLevel()) + ",bx=" + std::to_string(bd[0]) +
|
||
",by=" + std::to_string(bd[1]) + ",bz=" + std::to_string(bd[2]) +
|
||
",structPixels=" + std::to_string(baseStruct) +
|
||
",ok=" + std::to_string(okB ? 1 : 0) + ",png=" + pngPath);
|
||
return okB ? 0 : 1;
|
||
}
|
||
|
||
// 拉近预览:在默认取景基础上拉近相机,再走阻塞刷新(与真窗口缩放后完全相同的
|
||
// 单纹理选区路径,level0 局部子区域),轮询到就绪验证「拉近后」单图非空、完整。
|
||
if (nearPreview) {
|
||
st.cam->Dolly(2.5); // 拉近
|
||
ren->ResetCameraClippingRange();
|
||
warm = viewRefreshBlocking(&st);
|
||
rw->Render();
|
||
}
|
||
|
||
std::cout << "[view] 预热(" << (nearPreview ? "拉近局部段" : "默认局部段")
|
||
<< "): level=" << st.lastLevel << " 渲染块=" << warm << "\n";
|
||
|
||
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH);
|
||
const bool textureErr = capWin->textureError();
|
||
const bool renderedOk = !textureErr && nonBlack > 0;
|
||
|
||
if (preview) {
|
||
// 修复 2:用与真窗口完全相同的默认相机/source/exagg/传函(viewSetupDefaultFrame
|
||
// 已建好),离屏渲一帧存图 → 控制方先 Read 确认开窗默认画面非空、有结构。
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
const std::string pngPath =
|
||
(shotDir / (nearPreview ? "view-near.png" : "view-default.png"))
|
||
.string();
|
||
savePng(rw.Get(), pngPath);
|
||
// C3-8:多旋转角出图(同一真 view 场景:base+hires 共用 SetScale(1,1,exagg) + 底图
|
||
// 按高清块 cropping 挖空)。从默认相机起取若干 (azimuth,elevation) 离屏存图,供人工
|
||
// 核对路细长比例 / 拼接无缝 / 旋转无移动白斑。无 --shots 不执行。
|
||
if (shots) {
|
||
const struct {
|
||
const char* name;
|
||
double az;
|
||
double el;
|
||
} kAngles[] = {
|
||
{"view-shot-az0", 0.0, 0.0}, {"view-shot-az30", 30.0, 0.0},
|
||
{"view-shot-az60", 60.0, 0.0}, {"view-shot-az-30", -30.0, 0.0},
|
||
{"view-shot-el20", 0.0, 20.0}, {"view-shot-az45el15", 45.0, 15.0},
|
||
};
|
||
for (const auto& s : kAngles) {
|
||
st.cam->Azimuth(s.az);
|
||
st.cam->Elevation(s.el);
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
const std::string sp = (shotDir / (std::string(s.name) + ".png")).string();
|
||
savePng(rw.Get(), sp);
|
||
std::cout << "[view] 旋转角出图: " << sp << " (az=" << s.az
|
||
<< " el=" << s.el << ")\n";
|
||
st.cam->Elevation(-s.el); // 复位到默认朝向,下一角从默认起算
|
||
st.cam->Azimuth(-s.az);
|
||
}
|
||
}
|
||
// 结构像素计数:背景为深蓝灰(R/G≈10,B≈20),countNonBlackPixels(>10) 会把整屏
|
||
// 背景都算「非空」,对验证「画面有结构」无意义。改为只数明显亮于背景的像素
|
||
// (任一通道 >50),作为「确有渲出的体结构」的诚实判据。
|
||
auto countStructPixels = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType defStruct = countStructPixels();
|
||
|
||
// C3-5:两态 fps 对照。AutoAdjustSampleDistances 据渲染窗口的 DesiredUpdateRate
|
||
// 决定 SampleDistance——高 DesiredUpdateRate=拖动态(大步长/降采样/快),低=静止态
|
||
// (小步长/全质量/可慢)。离屏无真交互,故显式设 DesiredUpdateRate 模拟两态,旋相机
|
||
// 测真实 Render() 帧率,量化拖动降采样提速。
|
||
//
|
||
// ① 全质量静态:StillUpdateRate(慢档),mapper 用最小 SampleDistance。
|
||
rw->SetDesiredUpdateRate(0.5); // 静止目标帧率(慢)→全质量
|
||
rw->Render(); // 预热一帧(管线热 + 采样距离按此档生效)
|
||
Stopwatch swStill;
|
||
for (int f = 0; f < frames; ++f) {
|
||
st.cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double msStill = swStill.elapsedMs();
|
||
const double fps = msStill > 0 ? frames * 1000.0 / msStill : 0.0;
|
||
|
||
// ② 交互态(降采样):高 DesiredUpdateRate(拖动目标 15fps)→ mapper 拉大 SampleDistance。
|
||
rw->SetDesiredUpdateRate(15.0); // 拖动目标帧率→降采样
|
||
rw->Render(); // 预热一帧让降采样步长生效
|
||
Stopwatch swInteract;
|
||
for (int f = 0; f < frames; ++f) {
|
||
st.cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double msInteract = swInteract.elapsedMs();
|
||
const double fpsInteract =
|
||
msInteract > 0 ? frames * 1000.0 / msInteract : 0.0;
|
||
rw->SetDesiredUpdateRate(0.5); // 复位
|
||
|
||
const bool texErr2 = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool ok = !texErr2 && defStruct > 0;
|
||
|
||
std::cout << "\n=== view --preview 离屏默认视角验证 ===\n";
|
||
std::cout << "默认局部段维度 : " << kViewDefaultLocalBricks
|
||
<< " brick 列(沿线中段) level0\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "结构像素(>50) : " << defStruct << " / " << (winW * winH)
|
||
<< " (" << (100.0 * defStruct / (winW * winH))
|
||
<< "%, 已排除深蓝灰背景)\n";
|
||
std::cout << "纹理维度错误 : " << (texErr2 ? "是(!!)" : "否") << "\n";
|
||
const double speedup = fps > 0 ? fpsInteract / fps : 0.0;
|
||
std::cout << "全质量静态 fps : " << (ok ? std::to_string(fps) : "INVALID")
|
||
<< " (" << frames << " 帧旋相机, DesiredUpdateRate=0.5)\n";
|
||
std::cout << "交互态降采样 fps: "
|
||
<< (ok ? std::to_string(fpsInteract) : "INVALID") << " (" << frames
|
||
<< " 帧旋相机, DesiredUpdateRate=15)\n";
|
||
std::cout << "拖动态提速 : " << (ok ? std::to_string(speedup) : "INVALID")
|
||
<< "x (交互态/全质量)\n";
|
||
std::cout << "交互级(≥15fps) : "
|
||
<< (ok ? (fpsInteract >= 15.0 ? "是 ✔" : "否(未达拖动目标)")
|
||
: "INVALID")
|
||
<< "\n";
|
||
std::cout << "preview 结果 : "
|
||
<< (ok ? "OK ✔ 默认视角有结构" : "FAIL ✘") << "\n";
|
||
|
||
writeMetricLine(
|
||
"view-preview,dir=" + dir + ",exagg=" + std::to_string(exagg) +
|
||
",opacity=" + std::to_string(opacity) +
|
||
",localBricks=" + std::to_string(kViewDefaultLocalBricks) +
|
||
",structPixels=" + std::to_string(defStruct) +
|
||
",fpsStill=" + (ok ? std::to_string(fps) : "INVALID") +
|
||
",fpsInteract=" + (ok ? std::to_string(fpsInteract) : "INVALID") +
|
||
",speedup=" + (ok ? std::to_string(speedup) : "INVALID") +
|
||
",textureErr=" + std::to_string(texErr2 ? 1 : 0) +
|
||
",ok=" + std::to_string(ok ? 1 : 0) +
|
||
",png=" + pngPath);
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
if (smoke) {
|
||
// 离屏 smoke:模拟一次缩放 → 验 LOD 切换 + 不崩(单纹理路径)。
|
||
const int lvlNear = st.lastLevel;
|
||
st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD(整卷粗层单纹理)
|
||
ren->ResetCameraClippingRange();
|
||
const std::size_t blocksFar = viewRefreshBlocking(&st);
|
||
const int lvlFar = st.lastLevel;
|
||
rw->Render();
|
||
st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LOD(level0 局部子区域)
|
||
ren->ResetCameraClippingRange();
|
||
viewRefreshBlocking(&st);
|
||
const int lvlNear2 = st.lastLevel;
|
||
rw->Render();
|
||
const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH);
|
||
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
const bool lodSwitched = (lvlFar != lvlNear) || (lvlNear2 != lvlFar);
|
||
const bool ok = renderedOk && nb2 > 0 && !capWin->textureError();
|
||
std::cout << "\n=== view --smoke 离屏冒烟 ===\n";
|
||
std::cout << "近观 level=" << lvlNear << " → 拉远 level=" << lvlFar
|
||
<< " → 再拉近 level=" << lvlNear2 << "\n";
|
||
std::cout << "LOD 随缩放切换 : " << (lodSwitched ? "是 ✔" : "否(测点档位未跨界)")
|
||
<< " (blocksFar=" << blocksFar << ")\n";
|
||
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||
std::cout << "渲出非空像素 : " << (renderedOk ? "是" : "否(!!)")
|
||
<< " (近=" << nonBlack << " 远拉近=" << nb2 << ")\n";
|
||
std::cout << "smoke 结果 : " << (ok ? "OK ✔ 不崩" : "FAIL ✘") << "\n";
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
if (!renderedOk) {
|
||
std::cout << "[view] 警告: 首帧未渲出非空像素(纹理错=" << textureErr
|
||
<< ");窗口仍开,供人工排查。\n";
|
||
}
|
||
|
||
// 真窗口交互:TrackballCamera + 每次交互结束重选 LOD + 刷 fps 文本。
|
||
vtkNew<vtkRenderWindowInteractor> iren;
|
||
iren->SetRenderWindow(rw);
|
||
vtkNew<vtkInteractorStyleTrackballCamera> style;
|
||
iren->SetInteractorStyle(style);
|
||
|
||
// C3-5:交互/静止目标帧率。interactor 在交互(拖动)中把渲染窗口 DesiredUpdateRate
|
||
// 拉到 DesiredUpdateRate(15)→mapper 自适应降采样(快、跟手);松手后落回 StillUpdateRate
|
||
// (0.5)→恢复小步长全质量。配合上面 mapper 的 AutoAdjust/InteractiveAdjust 才生效。
|
||
iren->SetDesiredUpdateRate(15.0);
|
||
iren->SetStillUpdateRate(0.5);
|
||
|
||
vtkNew<vtkCallbackCommand> cb;
|
||
cb->SetCallback(viewOnInteract);
|
||
cb->SetClientData(&st);
|
||
// EndInteraction:旋转/缩放松手后提交新目标 + 刷 fps(仅松手触发一次,不自激)。
|
||
// 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent
|
||
// 形成无限递归重渲(窗口卡死、fps≈0)。fps 文本在松手时刷新即可。
|
||
iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
|
||
|
||
// C3-2:拖动进行中持续提交目标(非阻塞),主线程不被重组卡住 → 跟手。
|
||
vtkNew<vtkCallbackCommand> cbInteract;
|
||
cbInteract->SetCallback(viewOnInteracting);
|
||
cbInteract->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::InteractionEvent, cbInteract);
|
||
|
||
// C3-2:周期定时器非阻塞拉取后台已就绪纹理换上(新 LOD 备好即显示,拖动不卡)。
|
||
vtkNew<vtkCallbackCommand> cbTimer;
|
||
cbTimer->SetCallback(viewOnTimer);
|
||
cbTimer->SetClientData(&st);
|
||
iren->AddObserver(vtkCommand::TimerEvent, cbTimer);
|
||
|
||
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
|
||
iren->Initialize();
|
||
iren->CreateRepeatingTimer(33); // ~30Hz 拉取后台就绪纹理(不阻塞主线程)
|
||
rw->Render();
|
||
iren->Start();
|
||
|
||
std::cout << "[view] 窗口关闭,退出。\n";
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 12d-polish:梯度不透明度 + 光照 打磨探针(验证"体内部白雾"能否靠打磨解决)
|
||
// ============================================================================
|
||
//
|
||
// 当前体绘制对道路 GPR 水平层数据,体中间是均匀白雾、只有端面有层次。本探针在同一
|
||
// 全分辨率(level0)局部段 + 同一「看进体内部」视角(斜穿俯视,视线穿过体内部而非只看
|
||
// 端面)下渲 3 张离屏对比图,验证:给体加【梯度不透明度】(均匀区透明、层界面显出) +
|
||
// 【光照/明暗】能否让内部层状结构"浮"出来:
|
||
// polish-a-value.png 基线:按数值的不透明度(V形包络),无梯度不透明度、无光照
|
||
// polish-b-grad.png + 梯度不透明度(SetGradientOpacity)
|
||
// polish-c-grad-shade.png + 梯度不透明度 + 光照(ShadeOn, Ambient/Diffuse/Specular)
|
||
//
|
||
// 梯度不透明度的 piecewise 按【实际梯度幅值分布】标定阈值(不靠猜):先在量化域逐体素
|
||
// 采样 6 邻居中心差分梯度幅值,取分位数(median / p90)作斜坡控制点。
|
||
|
||
// 量化域标量不透明度峰值。基线(a)用 0.15(与默认体绘制同档→均匀积分白雾);开了梯度
|
||
// 不透明度(b/c)后均匀区被梯度门压成透明,可放心把标量峰值提到 0.6,让【层界面】这类
|
||
// 高梯度处的净不透明度(标量×梯度)足够高、层面真正"浮"成实面,而非仍是淡影。
|
||
constexpr double kPolishMaxOpacityFog = 0.15; // a:基线白雾
|
||
constexpr double kPolishMaxOpacityGrad = 0.6; // b/c:梯度门控后可提高
|
||
|
||
// 在 VTK_SHORT 局部体上采样梯度幅值分布(量化域,中心差分),返回有序的若干分位数。
|
||
// 跳过 kBlank 体素及其邻居(空值不参与梯度)。返回 {median, p75, p90, p99, max}。
|
||
// (GradStats 已在文件上方前置声明,供 C4 画廊共用。)
|
||
GradStats sampleGradientMagnitude(vtkImageData* img) {
|
||
int dims[3];
|
||
img->GetDimensions(dims);
|
||
const int nx = dims[0], ny = dims[1], nz = dims[2];
|
||
auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars());
|
||
GradStats gs;
|
||
if (!arr || nx < 3 || ny < 3 || nz < 3) return gs;
|
||
const std::int16_t blank = geopro::core::ScalarVolumeI16::kBlank;
|
||
auto at = [&](int i, int j, int k) -> std::int16_t {
|
||
const vtkIdType id = (static_cast<vtkIdType>(k) * ny + j) * nx + i;
|
||
return arr->GetValue(id);
|
||
};
|
||
std::vector<double> mags;
|
||
mags.reserve(static_cast<std::size_t>(nx) * ny * nz / 8 + 1);
|
||
// 步长抽样:大体不必逐体素,间隔取样即可代表分布(≤~50万样本)。
|
||
const vtkIdType total = static_cast<vtkIdType>(nx - 2) * (ny - 2) * (nz - 2);
|
||
const int stride = static_cast<int>(std::max<vtkIdType>(1, total / 500000));
|
||
vtkIdType counter = 0;
|
||
for (int k = 1; k < nz - 1; ++k) {
|
||
for (int j = 1; j < ny - 1; ++j) {
|
||
for (int i = 1; i < nx - 1; ++i) {
|
||
if ((counter++ % stride) != 0) continue;
|
||
const std::int16_t c = at(i, j, k);
|
||
if (c == blank) continue;
|
||
const std::int16_t xm = at(i - 1, j, k), xp = at(i + 1, j, k);
|
||
const std::int16_t ym = at(i, j - 1, k), yp = at(i, j + 1, k);
|
||
const std::int16_t zm = at(i, j, k - 1), zp = at(i, j, k + 1);
|
||
if (xm == blank || xp == blank || ym == blank || yp == blank ||
|
||
zm == blank || zp == blank) {
|
||
continue;
|
||
}
|
||
const double gx = 0.5 * (xp - xm);
|
||
const double gy = 0.5 * (yp - ym);
|
||
const double gz = 0.5 * (zp - zm);
|
||
mags.push_back(std::sqrt(gx * gx + gy * gy + gz * gz));
|
||
}
|
||
}
|
||
}
|
||
if (mags.empty()) return gs;
|
||
std::sort(mags.begin(), mags.end());
|
||
auto q = [&](double p) {
|
||
const std::size_t idx = static_cast<std::size_t>(
|
||
std::min<double>(mags.size() - 1, p * (mags.size() - 1)));
|
||
return mags[idx];
|
||
};
|
||
gs.median = q(0.50);
|
||
gs.p75 = q(0.75);
|
||
gs.p90 = q(0.90);
|
||
gs.p99 = q(0.99);
|
||
gs.mx = mags.back();
|
||
gs.samples = mags.size();
|
||
return gs;
|
||
}
|
||
|
||
// 标量不透明度: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.5,p99 到 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>=2):ShadeOn + Ambient/Diffuse/Specular,让层界面带立体明暗。
|
||
if (mode >= 2) {
|
||
prop->ShadeOn();
|
||
prop->SetAmbient(0.3);
|
||
prop->SetDiffuse(0.7);
|
||
prop->SetSpecular(0.2);
|
||
prop->SetSpecularPower(10.0);
|
||
} else {
|
||
prop->ShadeOff();
|
||
}
|
||
|
||
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||
volume->SetMapper(mapper);
|
||
volume->SetProperty(prop);
|
||
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:薄轴放大,截面结构才看得出
|
||
ren->AddVolume(volume);
|
||
|
||
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||
vtkOutputWindow::SetInstance(capWin);
|
||
|
||
// 「看进体内部」取景:斜穿俯视——较大 Elevation 让视线从上方斜穿过体内部(而非只看
|
||
// 端面),配合垂向夸张后体呈板状,俯视看穿层间。Zoom 填满画面。
|
||
ren->ResetCamera();
|
||
vtkCamera* cam = ren->GetActiveCamera();
|
||
cam->Elevation(elevation);
|
||
cam->Azimuth(azimuth);
|
||
cam->Zoom(zoom);
|
||
ren->ResetCameraClippingRange();
|
||
rw->Render();
|
||
|
||
// 结构像素:任一通道 >50(排除暗背景)。
|
||
auto countStructPixels = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
|
||
px->GetComponent(i, 2) > 50) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
// 高于背景像素:背景为 (0.05,0.05,0.09)≈RGB(13,13,23),阈值 35 干净地把渲出的体
|
||
// 结构与背景分开(区别于结构像素>50:>35 能纳入梯度门控后偏暗但确有的层面)。
|
||
auto countAboveBg = [&]() -> vtkIdType {
|
||
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
|
||
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
|
||
vtkIdType n = 0;
|
||
const vtkIdType np = px->GetNumberOfTuples();
|
||
for (vtkIdType i = 0; i < np; ++i) {
|
||
if (px->GetComponent(i, 0) > 35 || px->GetComponent(i, 1) > 35 ||
|
||
px->GetComponent(i, 2) > 35) {
|
||
++n;
|
||
}
|
||
}
|
||
return n;
|
||
};
|
||
const vtkIdType structPx = countStructPixels();
|
||
const vtkIdType aboveBg = countAboveBg();
|
||
const double bright = meanBrightness(rw.Get(), winW, winH);
|
||
|
||
savePng(rw.Get(), pngPath);
|
||
|
||
// fps:旋相机 frames 帧(保持大俯角,绕 Azimuth)。
|
||
rw->Render();
|
||
Stopwatch sw;
|
||
for (int f = 0; f < frames; ++f) {
|
||
cam->Azimuth(360.0 / frames);
|
||
rw->Render();
|
||
}
|
||
const double ms = sw.elapsedMs();
|
||
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
|
||
const bool texErr = capWin->textureError();
|
||
vtkOutputWindow::SetInstance(nullptr);
|
||
// 有效判据:无纹理错 + 渲出高于背景的像素(>35)。结构像素(>50)仅作亮度强弱度量,
|
||
// 不作有效门——梯度门控后层面偏暗但确有渲出,不应误判为空。
|
||
const bool ok = !texErr && aboveBg > 0;
|
||
|
||
const char* label =
|
||
mode == 0 ? "a-value(基线)"
|
||
: (mode == 1 ? "b-grad(+梯度不透明度)" : "c-grad-shade(+梯度+光照)");
|
||
std::cout << "\n--- polish " << label << " ---\n";
|
||
std::cout << "存图 : " << pngPath << "\n";
|
||
std::cout << "高于背景像素(>35): " << aboveBg << " / " << (winW * winH) << " ("
|
||
<< (100.0 * aboveBg / (winW * winH)) << "%)\n";
|
||
std::cout << "结构像素(>50) : " << structPx << " / " << (winW * winH) << " ("
|
||
<< (100.0 * structPx / (winW * winH)) << "%)\n";
|
||
std::cout << "平均亮度(0-255) : " << bright << "\n";
|
||
std::cout << "真实 fps : " << (ok ? std::to_string(fps) : "INVALID")
|
||
<< " (" << frames << " 帧旋相机)\n";
|
||
std::cout << "结果 : " << (ok ? "OK" : "FAIL(纹理错或空渲染)") << "\n";
|
||
|
||
writeMetricLine(
|
||
"polish," + std::string(mode == 0 ? "a-value" : mode == 1 ? "b-grad"
|
||
: "c-grad-shade") +
|
||
",aboveBg=" + std::to_string(aboveBg) +
|
||
",structPx=" + std::to_string(structPx) +
|
||
",bright=" + std::to_string(bright) +
|
||
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
|
||
",texErr=" + std::to_string(texErr ? 1 : 0) + ",png=" + pngPath);
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
int cmdPolish(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc polish <storeDir> [--exagg 8] [--frames 90] "
|
||
"[--localBricks 4]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const double exagg = std::stod(a.get("exagg", "8"));
|
||
const int frames = std::stoi(a.get("frames", "90"));
|
||
const int localBricks = std::stoi(a.get("localBricks", "4"));
|
||
// 「看进体内部」取景:斜穿俯视。默认 El45/Az30/Zoom1.5(视线从上方斜穿层间,
|
||
// 既不是纯端面也不至于过陡退化成边缘线)。可命令行覆盖以微调。
|
||
const double elevation = std::stod(a.get("elevation", "45"));
|
||
const double azimuth = std::stod(a.get("azimuth", "30"));
|
||
const double zoom = std::stod(a.get("zoom", "1.5"));
|
||
std::cout << "[polish] storeDir=" << dir << " exagg=" << exagg
|
||
<< " frames=" << frames << " localBricks=" << localBricks << "\n";
|
||
|
||
std::cout << "[polish] 离屏闸门复检...\n";
|
||
if (cmdOffscreenSmoke() != 0) {
|
||
std::cout << "[polish] 闸门失败,中止。\n";
|
||
return 1;
|
||
}
|
||
|
||
geopro::data::ChunkedVolumeStore store(dir);
|
||
const geopro::data::StoreMeta& m = store.meta();
|
||
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||
const geopro::core::ColorScale cs = makeSeismicColorScale(vmin, vmax);
|
||
|
||
// 全分辨率 level0 局部段(沿线中段),三图共用同一体。
|
||
const int totBx = store.bricksX(0);
|
||
const int localBx = std::min(localBricks, totBx);
|
||
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||
vtkSmartPointer<vtkImageData> locImg =
|
||
buildLocalLevel0Image(store, m, bx0, localBx);
|
||
int locDims[3];
|
||
locImg->GetDimensions(locDims);
|
||
std::cout << "[polish] level0 局部段 " << locDims[0] << "x" << locDims[1] << "x"
|
||
<< locDims[2] << " (brick列 [" << bx0 << "," << (bx0 + localBx)
|
||
<< ") / " << totBx << ")\n";
|
||
|
||
// 标定梯度不透明度阈值:采样实际梯度幅值分布。
|
||
const GradStats gs = sampleGradientMagnitude(locImg.Get());
|
||
std::cout << "[polish] 梯度幅值分布(量化域,样本 " << gs.samples
|
||
<< "): median=" << gs.median << " p75=" << gs.p75
|
||
<< " p90=" << gs.p90 << " p99=" << gs.p99 << " max=" << gs.mx
|
||
<< "\n";
|
||
std::cout << "[polish] 梯度不透明度 piecewise: grad<=" << std::max(1.0, gs.median)
|
||
<< "→0.0 grad=" << std::max(2.0, gs.p90) << "→0.5 grad>="
|
||
<< std::max(3.0, gs.p99) << "→0.9\n";
|
||
|
||
const fs::path shotDir =
|
||
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||
fs::create_directories(shotDir);
|
||
|
||
int rc = 0;
|
||
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/0,
|
||
exagg, (shotDir / "polish-a-value.png").string(), frames,
|
||
elevation, azimuth, zoom);
|
||
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/1,
|
||
exagg, (shotDir / "polish-b-grad.png").string(), frames,
|
||
elevation, azimuth, zoom);
|
||
rc |= renderPolishOne(locImg.Get(), m.quant, cs, vmin, vmax, gs, /*mode=*/2,
|
||
exagg, (shotDir / "polish-c-grad-shade.png").string(),
|
||
frames, elevation, azimuth, zoom);
|
||
|
||
std::cout << "\n[polish] 完成,3 张对比图存于 " << shotDir.string()
|
||
<< " (polish-a-value / polish-b-grad / polish-c-grad-shade)\n";
|
||
return rc;
|
||
}
|
||
|
||
void usage() {
|
||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n"
|
||
" gpr_poc build-stream <dir> [--cellXY 0.05] [--cellZ 0.05] "
|
||
"[--out <storeDir>] [--levels 3] [--sliceXBricks 8] "
|
||
"[--maxLines N]\n"
|
||
" gpr_poc build-geo <dir> [--cellXY 0.5] [--cellZ 0.1] "
|
||
"[--out <storeDir>] [--levels 4] [--maxLines N] [--curvilinear]\n"
|
||
" gpr_poc build-line <lineDir> <linePrefix> --out <storeDir> "
|
||
"[--levels 3]\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 == "build-line") return cmdBuildLine(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;
|
||
}
|