// gpr_poc —— POC-B headless 度量 CLI。 // // 串起整条地基:发现 14 通道 .iprb + .ord → assembleGprSurvey → buildGprVolume // → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load), // 在真实/合成数据上输出可测的真实指标(耗时/维度/体积/压缩比/加载/峰值内存)。 // // 子命令: // gpr_poc build [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out ] [--levels 2] // gpr_poc load // gpr_poc selftest // gpr_poc offscreen-smoke —— 离屏 GL 闸门冒烟 // gpr_poc renderB [--frames 120] —— 离屏体绘制/切片 fps 基准 #include #include #include #include #include #include #include #include #include #include #include "Probe.hpp" #include "core/algo/GprVolumeBuilder.hpp" #include "core/algo/IInterpolator.hpp" #include "core/model/GprSurvey.hpp" #include "core/model/ColorScale.hpp" #include "core/model/ScalarVolumeI16.hpp" #include "data/store/ChunkedVolumeStore.hpp" #include "io/gpr/GprSurveyAssembler.hpp" #include "io/gpr/IprHeader.hpp" #include "render/actors/VoxelActor.hpp" #include "render/source/OutOfCoreSource.hpp" #include "render/source/WholeVolumeSource.hpp" // ---- VTK 离屏渲染 ---- #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include // 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 kv; std::vector 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 iprb; // 已按通道号排序 std::string ord; }; LineFiles discoverLine(const std::string& dir, const std::string& line) { LineFiles lf; std::map 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:优先匹配本线(含 "_" 且以 .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:匹配 "*__A.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(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(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(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 [--line 001] [--cellXY 0.2] " "[--cellZ 0.05] [--out ] [--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(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(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,取 "*_.ord" 的 NNN。 std::vector discoverLines(const std::string& dir) { std::vector 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(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 channelY; // 升序 int nx = 0, ny = 0, nz = 0; // 该线在合并网格下的体素维度(X 未对齐 brick) }; // 全线总道数 = min 通道(fileBytes/(samples*2)),与 assembleGprSurvey 对齐口径一致。 std::int64_t totalTracesOf(const std::vector& iprb, int samples) { std::int64_t minTr = std::numeric_limits::max(); const std::int64_t per = static_cast(samples) * 2; for (const auto& p : iprb) { const std::int64_t bytes = static_cast(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 [--cellXY 0.05] " "[--cellZ 0.05] [--out ] [--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 lineNos = discoverLines(dir); if (maxLines > 0 && static_cast(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 files; std::vector 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(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 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::infinity(); double vmax = -std::numeric_limits::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::max(); std::int64_t t1 = std::numeric_limits::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 voxels( static_cast(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(mergedNx) * mergedNy * mergedNz; const std::int64_t rawBytes = voxels * 2; const std::int64_t dataBytes = storeDataBytes(out); const double ratio = dataBytes > 0 ? static_cast(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; } int cmdLoad(int argc, char** argv) { const Args a = parseArgs(argc, argv, 2); if (a.positional.empty()) { std::cerr << "用法: gpr_poc load \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(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(base + t + s); b.write(reinterpret_cast(&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 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 makeOffscreenWindow(int w, int h) { auto rw = vtkSmartPointer::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 ren; ren->SetBackground(0.1, 0.1, 0.2); rw->AddRenderer(ren); vtkNew cube; cube->SetXLength(1.0); cube->SetYLength(1.0); cube->SetZLength(1.0); vtkNew mapper; mapper->SetInputConnection(cube->GetOutputPort()); vtkNew 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::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 reslice; reslice->SetInputData(full); reslice->SetOutputDimensionality(2); reslice->SetInterpolationModeToLinear(); vtkNew colorize; colorize->SetLookupTable(lut); colorize->SetInputConnection(reslice->GetOutputPort()); vtkNew 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(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 makeLut(const geopro::core::ColorScale& cs, double vmin, double vmax) { auto lut = vtkSmartPointer::New(); const int n = 256; lut->SetNumberOfTableValues(n); lut->SetRange(vmin, vmax); for (int i = 0; i < n; ++i) { const double v = vmin + (vmax - vmin) * i / (n - 1); const auto c = cs.colorAt(v); lut->SetTableValue(i, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0); } lut->Build(); return lut; } // 简单蓝-白-红色阶(与 test_color_scale 同款最简构造)。 geopro::core::ColorScale makeColorScale(double vmin, double vmax) { geopro::core::ColorScale cs; const double mid = 0.5 * (vmin + vmax); cs.addStop(vmin, geopro::core::Rgba{0, 0, 255, 255}); cs.addStop(mid, geopro::core::Rgba{255, 255, 255, 255}); cs.addStop(vmax, geopro::core::Rgba{255, 0, 0, 255}); return cs; } // ============================================================================ // 视觉调优共享构件(Task 12d ①) // ============================================================================ // // 结构化配色:地震/雷达体常用的「结构色阶」——深蓝(强负)→青→白(零)→黄→红(强正), // 比单纯蓝-白-红更易拉开正负反射层次。值域用数据 vmin/vmax,无需手调控制点。 geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) { geopro::core::ColorScale cs; const double span = (vmax > vmin) ? (vmax - vmin) : 1.0; auto at = [&](double t) { return vmin + span * t; }; cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 140, 255}); // 深蓝 cs.addStop(at(0.25), geopro::core::Rgba{0, 160, 220, 255}); // 青 cs.addStop(at(0.50), geopro::core::Rgba{245, 245, 245, 255}); // 白(零附近) cs.addStop(at(0.75), geopro::core::Rgba{250, 190, 30, 255}); // 黄 cs.addStop(at(1.00), geopro::core::Rgba{170, 0, 0, 255}); // 暗红 return cs; } // 地震高对比色阶(seismic 红-白-蓝):两端饱和亮色(强正=亮红、强负=亮蓝), // 零附近白。比 structural 更亮、对比更狠,正负反射一眼分开。 geopro::core::ColorScale makeSeismicColorScale(double vmin, double vmax) { geopro::core::ColorScale cs; const double span = (vmax > vmin) ? (vmax - vmin) : 1.0; auto at = [&](double t) { return vmin + span * t; }; cs.addStop(at(0.00), geopro::core::Rgba{30, 60, 255, 255}); // 亮蓝(强负) cs.addStop(at(0.30), geopro::core::Rgba{120, 180, 255, 255}); // 浅蓝 cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 白(零) cs.addStop(at(0.70), geopro::core::Rgba{255, 170, 120, 255}); // 浅橙 cs.addStop(at(1.00), geopro::core::Rgba{255, 40, 30, 255}); // 亮红(强正) return cs; } // jet 类高饱和色阶(蓝-青-绿-黄-红):全程高亮高饱和,最大化色彩动态范围, // 弱信号也能映到鲜明色相,适合「一眼铺满层次」的取向。 geopro::core::ColorScale makeJetColorScale(double vmin, double vmax) { geopro::core::ColorScale cs; const double span = (vmax > vmin) ? (vmax - vmin) : 1.0; auto at = [&](double t) { return vmin + span * t; }; cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 200, 255}); // 蓝 cs.addStop(at(0.25), geopro::core::Rgba{0, 200, 255, 255}); // 青 cs.addStop(at(0.50), geopro::core::Rgba{0, 230, 60, 255}); // 绿 cs.addStop(at(0.75), geopro::core::Rgba{255, 230, 0, 255}); // 黄 cs.addStop(at(1.00), geopro::core::Rgba{255, 30, 0, 255}); // 红 return cs; } // 「实体感」不透明度包络(Task 12d gallery):与 structural 双端斜坡不同,这里让 // 中高值段普遍可见——背景(近零)仍压低但不归零,中高段从 floorOpacity 平滑升到 // maxOpacity,使体读起来像半透明实心块、内部层次(而非只剩两端薄壳)可见。 // floorOpacity:近零背景的最低不透明度(0.05~0.12,压住但不消失) // maxOpacity :强反射端的不透明度峰值(0.85 时近实心) // midOpacity :中值段(半幅处)的不透明度(0.3~0.5,决定「半透明实心」观感) vtkSmartPointer 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(q.toQ(vminPhys)); const double qmaxD = static_cast(q.toQ(vmaxPhys)); vtkNew color; for (int t = 0; t < kTransferSamples; ++t) { const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1); const auto qvLevel = static_cast(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 opacity; opacity->AddPoint( static_cast(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::New(); prop->SetColor(color); prop->SetScalarOpacity(opacity); prop->SetInterpolationTypeToLinear(); prop->ShadeOff(); return prop; } // 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。 // 不透明度调高时光线提前终止,fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。 vtkSmartPointer 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(q.toQ(vminPhys)); const double qmaxD = static_cast(q.toQ(vmaxPhys)); vtkNew color; for (int t = 0; t < kTransferSamples; ++t) { const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1); const auto qvLevel = static_cast(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 opacity; opacity->AddPoint( static_cast(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::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 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 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::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 [--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(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 ren; ren->SetBackground(0.0, 0.0, 0.0); rw->AddRenderer(ren); vtkSmartPointer volume = geopro::render::buildVoxelI16FromImage(shortImg, m.quant, cs, vmin, vmax); ren->AddVolume(volume); // 装上捕获式 OutputWindow:拦截体绘制时的 3D 纹理维度错误。 auto capWin = vtkSmartPointer::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 ren2; ren2->SetBackground(0.0, 0.0, 0.0); auto rw2 = makeOffscreenWindow(1024, 768); rw2->AddRenderer(ren2); vtkSmartPointer 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 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(q.toQ(vminPhys)); const double qmaxD = static_cast(q.toQ(vmaxPhys)); vtkNew color; for (int t = 0; t < kTransferSamples; ++t) { const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1); const auto qvLevel = static_cast(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 opacity; opacity->AddPoint( static_cast(geopro::core::ScalarVolumeI16::kBlank), 0.0); opacity->AddPoint(qminD, 0.0); opacity->AddPoint(qmaxD, kMaxOpacity); auto prop = vtkSmartPointer::New(); prop->SetColor(color); prop->SetScalarOpacity(opacity); prop->SetInterpolationTypeToLinear(); prop->ShadeOff(); return prop; } // 由当前工作集图像组装 vtkMultiBlockDataSet(每块一个 vtkImageData)。 vtkSmartPointer makeMultiBlock( const std::vector>& imgs) { auto mb = vtkSmartPointer::New(); mb->SetNumberOfBlocks(static_cast(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 [--budget 64] [--frames 120]\n"; return 2; } const std::string dir = a.positional[0]; const std::size_t budget = static_cast(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(m.nx) * m.ny * m.nz; const int winW = 1024, winH = 768; src.setAspect(static_cast(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 prop = makeI16VolumeProperty(m.quant, cs, vmin, vmax); // 2) 离屏 + MultiBlock 体绘制。 auto rw = makeOffscreenWindow(winW, winH); vtkNew ren; ren->SetBackground(0.0, 0.0, 0.0); rw->AddRenderer(ren); vtkNew mapper; mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); auto volume = vtkSmartPointer::New(); volume->SetMapper(mapper); volume->SetProperty(prop); ren->AddVolume(volume); // 装捕获式 OutputWindow:拦截每块上传时的 3D 纹理维度错误(应无,因块 ≤64³)。 auto capWin = vtkSmartPointer::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::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(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 [--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(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((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 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 ren; ren->SetBackground(0.0, 0.0, 0.0); rw->AddRenderer(ren); // vtkGPUVolumeRayCastMapper 抽象基类无 SetPartitions(在 OpenGL 实现上); // 直接建 OpenGL 具体类(工厂默认产物同此),喂【整卷单 image】不预切块。 vtkNew mapper; mapper->SetInputData(shortImg); mapper->SetPartitions(px, py, pz); auto volume = vtkSmartPointer::New(); volume->SetMapper(mapper); volume->SetProperty(prop); ren->AddVolume(volume); // 装捕获式 OutputWindow:拦截分区上传时的 3D 纹理维度错误。 auto capWin = vtkSmartPointer::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::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 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(1 << level); // 2^level auto img = vtkSmartPointer::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 arr; arr->SetName("v"); arr->SetNumberOfTuples(static_cast(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 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(k0 + kk); for (int jj = 0; jj < bh; ++jj) { const vtkIdType gj = static_cast(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 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::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 arr; arr->SetName("v"); arr->SetNumberOfTuples(static_cast(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 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(k0 + kk); for (int jj = 0; jj < bh; ++jj) { const vtkIdType gj = static_cast(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::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 w2i; w2i->SetInput(rw); w2i->SetInputBufferTypeToRGB(); w2i->ReadFrontBufferOff(); w2i->Update(); vtkNew 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::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(np); } int cmdRenderLOD(int argc, char** argv) { const Args a = parseArgs(argc, argv, 2); if (a.positional.empty()) { std::cerr << "用法: gpr_poc renderLOD [--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::New(); vtkOutputWindow::SetInstance(capWin); // ---- (a) 粗层概览 fps:level2 整卷 ---- const int ovLevel = std::min(2, totLevels - 1); std::cout << "[renderLOD] (a) 建 level" << ovLevel << " 整卷 image...\n"; vtkSmartPointer ovImg = buildLevelImage(store, ovLevel, m); int ovNx, ovNy, ovNz; store.dims(ovLevel, ovNx, ovNy, ovNz); auto rwOv = makeOffscreenWindow(winW, winH); vtkNew renOv; renOv->SetBackground(0.0, 0.0, 0.0); rwOv->AddRenderer(renOv); vtkSmartPointer 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 locImg = buildLocalLevel0Image(store, m, bx0, localBx); int locDims[3]; locImg->GetDimensions(locDims); auto rwLoc = makeOffscreenWindow(winW, winH); vtkNew renLoc; renLoc->SetBackground(0.0, 0.0, 0.0); rwLoc->AddRenderer(renLoc); vtkSmartPointer 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 renTr; renTr->SetBackground(0.0, 0.0, 0.0); rwTr->AddRenderer(renTr); // 远观体 = level2 概览(新建一份,避免与 (a) 共享 actor 状态)。 vtkSmartPointer farVol = geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin, vmax); // 近观体 = level0 局部(复用 (b) 的 image)。 vtkSmartPointer 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 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 [--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::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 locImg = buildLocalLevel0Image(store, m, bx0, localBx); int locDims[3]; locImg->GetDimensions(locDims); // 调优前局部 fps(默认色阶 0.15 无夸张)。 auto rwA = makeOffscreenWindow(winW, winH); vtkNew renA; renA->SetBackground(0.0, 0.0, 0.0); rwA->AddRenderer(renA); vtkSmartPointer 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 renB; renB->SetBackground(0.04, 0.04, 0.08); // 深蓝灰背景,衬托体 rwB->AddRenderer(renB); vtkSmartPointer 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 ovImg = buildLevelImage(store, ovLevel, m); auto rwO = makeOffscreenWindow(winW, winH); vtkNew renO; renO->SetBackground(0.04, 0.04, 0.08); rwO->AddRenderer(renO); vtkSmartPointer 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 [--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 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 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::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 img = buildLocalLevel0Image(store, m, bx0, localBx); int d[3]; img->GetDimensions(d); const long long voxels = static_cast(d[0]) * d[1] * d[2]; auto rw = makeOffscreenWindow(1024, 768); vtkNew ren; ren->SetBackground(0.0, 0.0, 0.0); rw->AddRenderer(ren); vtkSmartPointer 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-.png OpacityProfile profile; ColorChoice color; double floorOpacity; // 近零背景不透明度(kSolid 用) double midOpacity; // 中值段不透明度(kSolid 用) double maxOpacity; // 两端峰值不透明度 double exagg; // 垂向夸张 double elevation; // ResetCamera 后 Elevation double azimuth; // 再 Azimuth double zoom; // 再 Zoom 填满画面 double bg[3]; // 背景 RGB const char* desc; // 报告用中文说明 }; // 4 组视觉参数。值经离屏实跑挑出(详见报告)。 const GalleryVariant kGalleryVariants[] = { // var1:高不透明度实体感——seismic 亮配色 + V 形包络(中段 0.40/两端 0.85), // floor 压到 0.04:近零层间隙近透明,亮层面浮出 → 内部层状结构可读。 {"var1", OpacityProfile::kSolid, ColorChoice::kSeismic, 0.04, 0.40, 0.85, 8.0, 22.0, 28.0, 1.9, {0.05, 0.05, 0.09}, "高不透明度实体感:V形包络(floor0.04/mid0.40/max0.85)+seismic 亮配色," "半透明实心、内部层次可见"}, // var2:高对比配色——jet 全程高饱和 + 中等不透明度 V 形包络。 {"var2", OpacityProfile::kSolid, ColorChoice::kJet, 0.04, 0.32, 0.70, 8.0, 22.0, 28.0, 1.9, {0.06, 0.06, 0.10}, "高对比配色:jet 蓝青绿黄红全程高饱和 + 中等 V 形包络(mid0.32/max0.70)"}, // var3:居中正对纵截面——低 Elevation/Azimuth 摆平、正对 X-Z 长侧面(层状反射沿 // X 延展最清晰)、Zoom2.0 填满 ~70%;floor 压更低让层间隙透明、层面立体。 {"var3", OpacityProfile::kSolid, ColorChoice::kSeismic, 0.03, 0.38, 0.82, 9.0, 10.0, 12.0, 2.0, {0.05, 0.05, 0.09}, "居中正对纵截面:低 El10/Az12 摆平正对 X-Z 长侧面、Zoom2.0 填满视野," "floor0.03 凸显层面,exagg9 放大薄轴"}, // var4:最像真实 GPR 三维图——seismic + 略提背景亮 + 微立体角 + 实体包络。 // 综合最佳,选作交互窗口默认(kViewDefaultVariant)。 {"var4", OpacityProfile::kSolid, ColorChoice::kSeismic, 0.035, 0.38, 0.84, 8.0, 18.0, 22.0, 2.0, {0.07, 0.08, 0.11}, "综合最佳:seismic + 实体包络(floor0.035/mid0.38/max0.84) + 微立体取景" "(El18/Az22/Zoom2.0) + 略亮冷灰背景"}, }; // 交互窗口(无 flag 的 view)的默认视觉变体 = var4(kGalleryVariants 末项)。 // 交互默认与 view-var4 走同一份参数 → 二者画面一致(DRY,不复制粘贴漂移)。 const GalleryVariant& kViewDefaultVariant = kGalleryVariants[sizeof(kGalleryVariants) / sizeof(kGalleryVariants[0]) - 1]; geopro::core::ColorScale pickColor(ColorChoice c, double vmin, double vmax) { switch (c) { case ColorChoice::kSeismic: return makeSeismicColorScale(vmin, vmax); case ColorChoice::kJet: return makeJetColorScale(vmin, vmax); case ColorChoice::kStructural: default: return makeStructuralColorScale(vmin, vmax); } } // 按变体的不透明度包络建体属性(gallery / 交互默认共用,DRY)。kSolid 走 V 形实体 // 包络(floor/mid/max),kStructural 走双端斜坡(仅 maxOpacity)。 vtkSmartPointer makeVariantProperty( const GalleryVariant& v, const geopro::core::Quant& q, const geopro::core::ColorScale& cs, double vmin, double vmax, double maxOpacity) { if (v.profile == OpacityProfile::kSolid) { return makeSolidVolumeProperty(q, cs, vmin, vmax, v.floorOpacity, v.midOpacity, maxOpacity); } return makeTunedVolumeProperty(q, cs, vmin, vmax, maxOpacity); } // ============================================================================ // ③ view:真窗口可交互(给用户肉眼测 + 最低配机跑)(Task 12d) // ============================================================================ // // 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂 // OutOfCoreSource:相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保 // 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。 // 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。 // // 离屏 smoke:--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。 // 整卷单张 3D 纹理的轴上限(同 renderLOD/renderB 实测 GL_MAX_3D_TEXTURE_SIZE)。 constexpr int kViewMax3DTex = 16384; // 单纹理统一渲染(Task 12d-singletex): // // 交互 view 不再用 vtkMultiBlockVolumeMapper + budget 分块(缺块、个位数 fps)。 // 任何相机位置都只渲【一张 vtkImageData + 单个 vtkSmartVolumeMapper】,与 --preview // 走完全同一条产单图 + 同一 mapper 的路径,保证一致、高 fps(与预览 184fps 同档)。 // // LOD 选层规则(拉近变细、拉远变粗): // - 远观/中景(相机选中粗层)→ 升到最细的「整卷各轴 ≤16384」层(本数据 L2:11119、 // L3:5560),整卷重组成一张纹理,任何缩放都显示完整体,绝不缺块。 // - 拉近(相机选中 level0/1,X 超 16384 无法整卷成单纹理)→ 取当前视野在 level0 的 // X 子区域(沿线裁一段,使子体各轴 ≤16384)重组一张纹理。 // 两条都用现成 buildLevelImage / buildLocalLevel0Image 产单图 → 单 SmartVolumeMapper。 // view 的每帧回调共享状态(挂到 interactor 的 EndInteraction 上)。 struct ViewState { geopro::data::ChunkedVolumeStore* store = nullptr; vtkSmartVolumeMapper* mapper = nullptr; // 单纹理:单 SmartVolumeMapper vtkRenderer* ren = nullptr; vtkCamera* cam = nullptr; vtkTextActor* fpsText = nullptr; vtkRenderWindow* rw = nullptr; double exagg = 8.0; int lastLevel = -1; // 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。 int cachedWholeLevel = -1; vtkSmartPointer cachedWholeImg; // 局部子区域 image 缓存(按 level0 brick 段缓存,仅在段变化时重组)。 int cachedLocalBx0 = -1; int cachedLocalCount = -1; vtkSmartPointer cachedLocalImg; // 回调防重入:回调内部会 Render(),若 Render 又触发观察者回调会无限递归。 bool inCb = false; }; // 某 level 整卷各轴是否都 ≤16384(可成单张 3D 纹理 → 整卷单 mapper 渲染)。 bool levelFitsSingleTexture(const geopro::data::ChunkedVolumeStore& store, int level) { int nx = 0, ny = 0, nz = 0; store.dims(level, nx, ny, nz); return nx <= kViewMax3DTex && ny <= kViewMax3DTex && nz <= kViewMax3DTex; } // 给定相机选中的 level,返回真正用于整卷渲染的 level:从 picked 起向粗逐层找, // 取第一个整卷各轴 ≤16384 的层(如 level0/1 长线 X 超 16384,则升到 level2)。 // 找不到(极端情况)返回 -1,调用方退回局部子区域路径。 int wholeVolumeLevelFor(const geopro::data::ChunkedVolumeStore& store, int picked) { const int maxLevel = store.levels() - 1; for (int lv = std::max(0, picked); lv <= maxLevel; ++lv) { if (levelFitsSingleTexture(store, lv)) { return lv; } } return -1; } // 由相机到体中心距离粗分档选 LOD level(移植 OutOfCoreSource::pickLevel,使交互 // view 不再依赖 OutOfCoreSource)。0=最细。cam==nullptr 或单层 → 0。 int viewPickLevel(const geopro::data::ChunkedVolumeStore& store, vtkCamera* cam) { const geopro::data::StoreMeta& m = store.meta(); const int maxLevel = store.levels() - 1; if (cam == nullptr || maxLevel <= 0) return 0; const double dx = m.nx * m.spacing[0]; const double dy = m.ny * m.spacing[1]; const double dz = m.nz * m.spacing[2]; const double diag = std::sqrt(dx * dx + dy * dy + dz * dz); if (diag <= 0.0) return 0; double pos[3]; cam->GetPosition(pos); const double cx = m.origin[0] + 0.5 * dx; const double cy = m.origin[1] + 0.5 * dy; const double cz = m.origin[2] + 0.5 * dz; const double ddx = pos[0] - cx, ddy = pos[1] - cy, ddz = pos[2] - cz; const double dist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz); const double ratio = dist / diag; int level = 0; if (ratio >= 1.0) level = 1; if (ratio >= 2.0) level = 2; if (ratio >= 4.0) level = 3; return std::min(level, maxLevel); } // 拉近时,level0 整卷 X(44476)>16384 无法成单纹理 → 只取相机视野覆盖的 X 段。 // 用相机视锥在 X 轴上的世界投影范围交体范围,换算成 level0 的 brick 列区间,并夹到 // 「段宽 ≤16384 体素」(=256 brick 列,远大于任何视野所需)。返回 [bx0, count)。 void viewLocalBrickRange(const geopro::data::ChunkedVolumeStore& store, vtkCamera* cam, int& bx0, int& count) { const geopro::data::StoreMeta& m = store.meta(); const int brick = m.brick; const int totBx = store.bricksX(0); const int maxBrickCols = kViewMax3DTex / brick; // 段宽上限(体素 ≤16384) // 默认:以体中段为中心取 kViewDefaultLocalBricks 列(无相机或退化时)。 int centerBx = totBx / 2; int halfCols = std::max(2, maxBrickCols / 4); // 视野估不出时给个稳妥宽度 if (cam != nullptr) { // 相机焦点 X 投影到 level0 brick 列;视野宽度由相机到焦点距离粗估。 double fp[3], pos[3]; cam->GetFocalPoint(fp); cam->GetPosition(pos); const double x0 = m.origin[0]; const double xspan = (m.nx > 1) ? (m.nx - 1) * m.spacing[0] : 1.0; const double fx = (fp[0] - x0) / (xspan > 0 ? xspan : 1.0); // 0..1 centerBx = std::clamp(static_cast(fx * totBx), 0, totBx - 1); // 视野半宽(世界)≈ 视距 × tan(半视角),再换成 brick 列;夹到合理区间。 const double ddx = pos[0] - fp[0], ddy = pos[1] - fp[1], ddz = pos[2] - fp[2]; const double viewDist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz); const double halfAngle = 0.5 * cam->GetViewAngle() * 3.14159265 / 180.0; const double halfWorld = viewDist * std::tan(halfAngle); const double colWorld = brick * m.spacing[0]; halfCols = std::clamp(static_cast(halfWorld / colWorld) + 1, 2, maxBrickCols / 2); } bx0 = std::clamp(centerBx - halfCols, 0, std::max(0, totBx - 1)); count = std::min(2 * halfCols, totBx - bx0); count = std::max(1, std::min(count, maxBrickCols)); } // 单纹理刷新:按相机选 LOD,产【一张 image】喂单 SmartVolumeMapper。返回喂入的块数 // (恒为 1,单纹理)。同步刷新 st->lastLevel(fps 文本用)。 std::size_t viewRefreshSingle(ViewState* st) { const int picked = viewPickLevel(*st->store, st->cam); // 概览/中远:升到最细的「整卷 ≤16384」层,整卷一张纹理(缓存,仅 level 变才重组)。 const int wlv = wholeVolumeLevelFor(*st->store, picked); if (wlv >= 0 && picked >= 1) { if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) { st->cachedWholeImg = buildLevelImage(*st->store, wlv, st->store->meta()); st->cachedWholeLevel = wlv; } st->mapper->SetInputData(st->cachedWholeImg); st->mapper->Update(); st->lastLevel = wlv; return 1; } // 拉近(picked==0,要全分辨率):取视野覆盖的 level0 X 子段,一张纹理。 int bx0 = 0, cnt = 1; viewLocalBrickRange(*st->store, st->cam, bx0, cnt); if (st->cachedLocalBx0 != bx0 || st->cachedLocalCount != cnt || st->cachedLocalImg == nullptr) { st->cachedLocalImg = buildLocalLevel0Image(*st->store, st->store->meta(), bx0, cnt); st->cachedLocalBx0 = bx0; st->cachedLocalCount = cnt; } st->mapper->SetInputData(st->cachedLocalImg); st->mapper->Update(); st->lastLevel = 0; return 1; } // interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。 // // fps 修复(Task 12d-fix3):之前用 frameTimer(上次回调到本次的墙钟)算 fps,把 // 用户思考/不动的空闲时间也算进去,显示的是「空闲间隔」(如 0.2fps),不可信。改为 // 松手时连渲 kFpsProbeFrames 帧、累计「实际 Render 耗时」取均值,得到真实渲染帧率。 void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) { auto* st = static_cast(clientData); // 防重入:本回调内部会 st->rw->Render(),若该 Render 再触发观察者进本回调 // 将无限递归。已在回调中则直接返回(双保险)。 if (st->inCb) return; st->inCb = true; // EndInteraction 时重选 LOD + 重组单图(仅松手触发一次,避免拖动中卡)。 const std::size_t blocks = viewRefreshSingle(st); st->ren->ResetCameraClippingRange(); const int lvl = st->lastLevel; // 真实渲染帧率:连渲若干帧,只累计 Render() 本身耗时(不含空闲)。首帧含切换后 // 的纹理上传/shader 编译,故跑 kFpsProbeFrames 帧取均值更可信。 constexpr int kFpsProbeFrames = 3; Stopwatch swR; for (int i = 0; i < kFpsProbeFrames; ++i) st->rw->Render(); const double renderMs = swR.elapsedMs() / kFpsProbeFrames; const double fps = renderMs > 0 ? 1000.0 / renderMs : 0.0; char buf[256]; std::snprintf(buf, sizeof(buf), "fps: %.1f | LOD level: %d | blocks: %zu | exagg: %.0fx", fps, lvl, blocks, st->exagg); st->fpsText->SetInput(buf); st->lastLevel = lvl; st->rw->Render(); // 末帧带上更新后的 fps 文本 st->inCb = false; } // 默认取景宽度:沿测线取约 256 道(=4 brick 列×64)的一段作首帧局部段。整线横截面 // 相对长度 1:34,框整卷只会看到一条隐形细带;框这个局部段,层状结构才充满视野 // (用户可再滚轮拉远看整体——细带是物理真实,拉近看细节)。段越宽 X 越细长、截面 // 越填不满画面;256 道是 ① cmdTune 出 lod-tuned-local.png(有清晰层状结构)的取景, // 沿用之以保证首帧同等可读。 constexpr int kViewDefaultLocalBricks = 4; // 建立 view 的「默认取景」:把 level0 一段局部体(沿线中段)整卷单块喂 mapper,再 // ResetCamera 到该局部段(actor 已 SetScale(1,exagg,exagg)),置相机为能看出层状 // 结构的角度。真窗口 / --smoke / --preview 三条路径共用此函数 → 渲的是同一画面。 // 取景角度(Elevation/Azimuth/Zoom)取自 kViewDefaultVariant(var4),与 view-var4 一致。 // // 返回喂给 mapper 的块数(=1)。同步更新 st->lastLevel=0(默认即全分辨率局部段)。 std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) { geopro::data::ChunkedVolumeStore& store = *st->store; const geopro::data::StoreMeta& m = store.meta(); const int totBx = store.bricksX(0); const int localBx = std::min(kViewDefaultLocalBricks, totBx); const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 沿线中段 vtkSmartPointer locImg = buildLocalLevel0Image(store, m, bx0, localBx); // 单纹理:一张 image 直接喂单 SmartVolumeMapper(与 --preview 同路径)。 st->mapper->SetInputData(locImg); st->mapper->Update(); st->cachedLocalImg = locImg; // 持有引用并作缓存键,避免被释放/重组 st->cachedLocalBx0 = bx0; st->cachedLocalCount = localBx; st->lastLevel = 0; // 框住局部段:用无参 ResetCamera(按 actor 的【已 SetScale(1,exagg,exagg)】缩放 // 后包围盒框,把 exagg 后的 Y/Z 一并纳入;mapper->GetBounds() 是未缩放的,不可用), // 相机角度沿用能看出结构的 Elevation/Azimuth,再 Zoom 拉近填满画面。 ren->ResetCamera(); st->cam = ren->GetActiveCamera(); st->cam->Elevation(kViewDefaultVariant.elevation); // var4 取景:El18 st->cam->Azimuth(kViewDefaultVariant.azimuth); // var4 取景:Az22 st->cam->Zoom(kViewDefaultVariant.zoom); // var4 取景:Zoom2.0 填满画面 ren->ResetCameraClippingRange(); return 1; // 单纹理:恒一张 image } // 渲一组画廊变体并存 PNG,报告 结构像素 / 平均亮度 / fps。返回 0=OK。 int runGalleryVariant(const std::string& dir, const GalleryVariant& v, int frames) { const int winW = 1280, winH = 800; geopro::data::ChunkedVolumeStore store(dir); const geopro::data::StoreMeta& m = store.meta(); const double vmin = m.vminPhys, vmax = m.vmaxPhys; const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax); vtkSmartPointer prop = makeVariantProperty(v, m.quant, cs, vmin, vmax, v.maxOpacity); auto rw = makeOffscreenWindow(winW, winH); vtkNew ren; ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]); rw->AddRenderer(ren); vtkNew mapper; mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); auto volume = vtkSmartPointer::New(); volume->SetMapper(mapper); volume->SetProperty(prop); volume->SetScale(1.0, v.exagg, v.exagg); ren->AddVolume(volume); // 局部段(沿线中段,同 viewSetupDefaultFrame 的取段法)。 const int totBx = store.bricksX(0); const int localBx = std::min(kViewDefaultLocalBricks, totBx); const int bx0 = std::max(0, totBx / 2 - localBx / 2); vtkSmartPointer locImg = buildLocalLevel0Image(store, m, bx0, localBx); std::vector> 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::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::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 [--exagg 8] [--opacity 0.5] " "[--budget 64] [--smoke] [--preview] [--variant N] " "[--gallery] [--frames 90]\n"; return 2; } const std::string dir = a.positional[0]; // 交互/preview/smoke 默认视觉参数 = kViewDefaultVariant(var4):配色/不透明度包络/ // exagg/背景全部走 var4,故默认画面 == view-var4(DRY,与画廊同源)。命令行 --exagg / // --opacity 若用户显式传则覆盖 var4 对应值,否则用 var4 的 exagg / maxOpacity。 const GalleryVariant& dv = kViewDefaultVariant; const double exagg = a.kv.count("exagg") ? std::stod(a.get("exagg", "8")) : dv.exagg; const double opacity = a.kv.count("opacity") ? std::stod(a.get("opacity", "0.5")) : dv.maxOpacity; const std::size_t budget = static_cast(std::stoul(a.get("budget", "64"))); const int frames = std::stoi(a.get("frames", "90")); auto hasFlag = [&](const char* name) { return a.kv.count(name) > 0 || std::find(a.positional.begin(), a.positional.end(), std::string("--") + name) != a.positional.end(); }; const bool smoke = hasFlag("smoke"); const bool preview = hasFlag("preview"); // 拉近预览(Task 12d-singletex):--preview --variant near(或 --near)走与真窗口 // 完全相同的单纹理拉近路径(viewRefreshSingle 选 level0 局部子区域),供控制方 Read // 验证「拉近后」单图非空、完整、fps 高。 const bool nearPreview = hasFlag("near") || (a.kv.count("variant") && a.get("variant", "") == "near"); // 画廊模式(Task 12d):渲 4 组视觉调参图供挑选。优先于其余路径。 if (hasFlag("gallery")) { return cmdViewGallery(dir, frames); } // 单变体:view --preview --variant N(N=1..4),只渲第 N 组(near 不走此路)。 if (preview && a.kv.count("variant") && !nearPreview) { const int vi = std::stoi(a.get("variant", "1")); const int n = static_cast(sizeof(kGalleryVariants) / sizeof(kGalleryVariants[0])); if (vi < 1 || vi > n) { std::cerr << "[view] --variant 需在 1.." << n << " 之间\n"; return 2; } std::cout << "[view] storeDir=" << dir << " 单变体 variant=" << vi << "\n"; if (cmdOffscreenSmoke() != 0) return 1; return runGalleryVariant(dir, kGalleryVariants[vi - 1], frames); } std::cout << "[view] storeDir=" << dir << " exagg=" << exagg << " opacity=" << opacity << " budget=" << budget << (preview ? " [PREVIEW 离屏存图+测fps]" : (smoke ? " [SMOKE 离屏]" : " [真窗口交互]")) << "\n"; // preview/smoke 走离屏。 const bool offscreen = smoke || preview; const int winW = 1280, winH = 800; // 单纹理统一路径(Task 12d-singletex):交互 view 只用 ChunkedVolumeStore 产 // 单图 + 单 SmartVolumeMapper,不再用 OutOfCoreSource/BrickPager/MultiBlock 分块。 (void)budget; // 交互 view 不再用 budget 分块(保留参数以兼容旧命令行)。 geopro::data::ChunkedVolumeStore store(dir); const auto& m = store.meta(); const double vmin = m.vminPhys, vmax = m.vmaxPhys; // 配色/不透明度包络取自 var4:seismic + V 形实体包络(floor/mid + opacity 作峰值)。 const geopro::core::ColorScale cs = pickColor(dv.color, vmin, vmax); vtkSmartPointer prop = makeVariantProperty(dv, m.quant, cs, vmin, vmax, opacity); // 渲染窗口:preview/smoke 走离屏,否则真窗口。 vtkSmartPointer rw; if (offscreen) { rw = makeOffscreenWindow(winW, winH); } else { rw = vtkSmartPointer::New(); rw->SetSize(winW, winH); rw->SetWindowName("gpr_poc view —— 核外 LOD 体绘制 (滚轮缩放切 LOD, 左键旋转)"); } vtkNew ren; ren->SetBackground(dv.bg[0], dv.bg[1], dv.bg[2]); // var4 略亮冷灰背景 rw->AddRenderer(ren); // 单纹理:单 vtkSmartVolumeMapper(GPU 光线投射,整张 3D 纹理),与 --preview / // gallery 同一 mapper 类型,保证交互画面 == 预览画面、fps 同档。 vtkNew mapper; mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); mapper->SetAutoAdjustSampleDistances(0); mapper->SetInteractiveAdjustSampleDistances(0); auto volume = vtkSmartPointer::New(); volume->SetMapper(mapper); volume->SetProperty(prop); volume->SetScale(1.0, exagg, exagg); // 垂向夸张(默认 var4 exagg) ren->AddVolume(volume); // 屏幕左上角实时 fps 文本。 vtkNew 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::New(); vtkOutputWindow::SetInstance(capWin); ViewState st; st.store = &store; st.mapper = mapper.Get(); st.ren = ren.Get(); st.fpsText = fpsText.Get(); st.rw = rw.Get(); st.exagg = exagg; // 相机初始定向(修复 1):默认框「局部段」而非整卷。整线横截面 1:34,框整卷 // 即便 exagg=8 也是一条隐形细带(看着空白);改为对准沿线中段一个 ~768 道窗口 // 的全分辨率局部体 → 开窗第一帧就看到一段有层状结构的体。三路径共用此取景。 std::size_t warm = viewSetupDefaultFrame(&st, ren); rw->Render(); // 拉近预览:在默认取景基础上拉近相机,再走 viewRefreshSingle(与真窗口缩放后 // 完全相同的单纹理路径,选 level0 局部子区域),验证「拉近后」单图非空、完整。 if (nearPreview) { st.cam->Dolly(2.5); // 拉近 ren->ResetCameraClippingRange(); warm = viewRefreshSingle(&st); rw->Render(); } std::cout << "[view] 预热(" << (nearPreview ? "拉近局部段" : "默认局部段") << "): level=" << st.lastLevel << " 渲染块=" << warm << "\n"; const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH); const bool textureErr = capWin->textureError(); const bool renderedOk = !textureErr && nonBlack > 0; if (preview) { // 修复 2:用与真窗口完全相同的默认相机/source/exagg/传函(viewSetupDefaultFrame // 已建好),离屏渲一帧存图 → 控制方先 Read 确认开窗默认画面非空、有结构。 const fs::path shotDir = fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots"; fs::create_directories(shotDir); const std::string pngPath = (shotDir / (nearPreview ? "view-near.png" : "view-default.png")) .string(); savePng(rw.Get(), pngPath); // 结构像素计数:背景为深蓝灰(R/G≈10,B≈20),countNonBlackPixels(>10) 会把整屏 // 背景都算「非空」,对验证「画面有结构」无意义。改为只数明显亮于背景的像素 // (任一通道 >50),作为「确有渲出的体结构」的诚实判据。 auto countStructPixels = [&]() -> vtkIdType { auto px = vtkSmartPointer::New(); rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px); vtkIdType n = 0; const vtkIdType np = px->GetNumberOfTuples(); for (vtkIdType i = 0; i < np; ++i) { if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 || px->GetComponent(i, 2) > 50) { ++n; } } return n; }; const vtkIdType defStruct = countStructPixels(); // 旋相机 N 帧测真实 fps(非首帧:首帧含纹理上传/shader 编译已在预热完成)。 rw->Render(); // 再预热一帧,确保管线热 Stopwatch sw; for (int f = 0; f < frames; ++f) { st.cam->Azimuth(360.0 / frames); rw->Render(); } const double ms = sw.elapsedMs(); const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0; const bool texErr2 = capWin->textureError(); vtkOutputWindow::SetInstance(nullptr); const bool ok = !texErr2 && defStruct > 0; std::cout << "\n=== view --preview 离屏默认视角验证 ===\n"; std::cout << "默认局部段维度 : " << kViewDefaultLocalBricks << " brick 列(沿线中段) level0\n"; std::cout << "存图 : " << pngPath << "\n"; std::cout << "结构像素(>50) : " << defStruct << " / " << (winW * winH) << " (" << (100.0 * defStruct / (winW * winH)) << "%, 已排除深蓝灰背景)\n"; std::cout << "纹理维度错误 : " << (texErr2 ? "是(!!)" : "否") << "\n"; std::cout << "真实渲染 fps : " << (ok ? std::to_string(fps) : "INVALID") << " (" << frames << " 帧旋相机, 非首帧)\n"; std::cout << "preview 结果 : " << (ok ? "OK ✔ 默认视角有结构" : "FAIL ✘") << "\n"; writeMetricLine( "view-preview,dir=" + dir + ",exagg=" + std::to_string(exagg) + ",opacity=" + std::to_string(opacity) + ",localBricks=" + std::to_string(kViewDefaultLocalBricks) + ",structPixels=" + std::to_string(defStruct) + ",fps=" + (ok ? std::to_string(fps) : "INVALID") + ",textureErr=" + std::to_string(texErr2 ? 1 : 0) + ",ok=" + std::to_string(ok ? 1 : 0) + ",png=" + pngPath); return ok ? 0 : 1; } if (smoke) { // 离屏 smoke:模拟一次缩放 → 验 LOD 切换 + 不崩(单纹理路径)。 const int lvlNear = st.lastLevel; st.cam->Dolly(0.02); // 大幅拉远 → 期望切到粗 LOD(整卷粗层单纹理) ren->ResetCameraClippingRange(); const std::size_t blocksFar = viewRefreshSingle(&st); const int lvlFar = st.lastLevel; rw->Render(); st.cam->Dolly(50.0); // 拉近回来 → 期望切回细 LOD(level0 局部子区域) ren->ResetCameraClippingRange(); viewRefreshSingle(&st); const int lvlNear2 = st.lastLevel; rw->Render(); const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH); vtkOutputWindow::SetInstance(nullptr); const bool lodSwitched = (lvlFar != lvlNear) || (lvlNear2 != lvlFar); const bool ok = renderedOk && nb2 > 0 && !capWin->textureError(); std::cout << "\n=== view --smoke 离屏冒烟 ===\n"; std::cout << "近观 level=" << lvlNear << " → 拉远 level=" << lvlFar << " → 再拉近 level=" << lvlNear2 << "\n"; std::cout << "LOD 随缩放切换 : " << (lodSwitched ? "是 ✔" : "否(测点档位未跨界)") << " (blocksFar=" << blocksFar << ")\n"; std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n"; std::cout << "渲出非空像素 : " << (renderedOk ? "是" : "否(!!)") << " (近=" << nonBlack << " 远拉近=" << nb2 << ")\n"; std::cout << "smoke 结果 : " << (ok ? "OK ✔ 不崩" : "FAIL ✘") << "\n"; return ok ? 0 : 1; } vtkOutputWindow::SetInstance(nullptr); if (!renderedOk) { std::cout << "[view] 警告: 首帧未渲出非空像素(纹理错=" << textureErr << ");窗口仍开,供人工排查。\n"; } // 真窗口交互:TrackballCamera + 每次交互结束重选 LOD + 刷 fps 文本。 vtkNew iren; iren->SetRenderWindow(rw); vtkNew style; iren->SetInteractorStyle(style); vtkNew cb; cb->SetCallback(viewOnInteract); cb->SetClientData(&st); // EndInteraction:旋转/缩放松手后重选 LOD + 刷 fps(仅松手触发一次,不自激)。 // 注意:绝不可在 rw 的 EndEvent 上注册——回调内部 Render() 会再触发 EndEvent // 形成无限递归重渲(窗口卡死、fps≈0)。fps 文本在松手时刷新即可。 iren->AddObserver(vtkCommand::EndInteractionEvent, cb); std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n"; iren->Initialize(); rw->Render(); iren->Start(); std::cout << "[view] 窗口关闭,退出。\n"; return 0; } // ============================================================================ // 12d-polish:梯度不透明度 + 光照 打磨探针(验证"体内部白雾"能否靠打磨解决) // ============================================================================ // // 当前体绘制对道路 GPR 水平层数据,体中间是均匀白雾、只有端面有层次。本探针在同一 // 全分辨率(level0)局部段 + 同一「看进体内部」视角(斜穿俯视,视线穿过体内部而非只看 // 端面)下渲 3 张离屏对比图,验证:给体加【梯度不透明度】(均匀区透明、层界面显出) + // 【光照/明暗】能否让内部层状结构"浮"出来: // polish-a-value.png 基线:按数值的不透明度(V形包络),无梯度不透明度、无光照 // polish-b-grad.png + 梯度不透明度(SetGradientOpacity) // polish-c-grad-shade.png + 梯度不透明度 + 光照(ShadeOn, Ambient/Diffuse/Specular) // // 梯度不透明度的 piecewise 按【实际梯度幅值分布】标定阈值(不靠猜):先在量化域逐体素 // 采样 6 邻居中心差分梯度幅值,取分位数(median / p90)作斜坡控制点。 // 量化域标量不透明度峰值。基线(a)用 0.15(与默认体绘制同档→均匀积分白雾);开了梯度 // 不透明度(b/c)后均匀区被梯度门压成透明,可放心把标量峰值提到 0.6,让【层界面】这类 // 高梯度处的净不透明度(标量×梯度)足够高、层面真正"浮"成实面,而非仍是淡影。 constexpr double kPolishMaxOpacityFog = 0.15; // a:基线白雾 constexpr double kPolishMaxOpacityGrad = 0.6; // b/c:梯度门控后可提高 // 在 VTK_SHORT 局部体上采样梯度幅值分布(量化域,中心差分),返回有序的若干分位数。 // 跳过 kBlank 体素及其邻居(空值不参与梯度)。返回 {median, p75, p90, p99, max}。 struct GradStats { double median = 0, p75 = 0, p90 = 0, p99 = 0, mx = 0; std::size_t samples = 0; }; GradStats sampleGradientMagnitude(vtkImageData* img) { int dims[3]; img->GetDimensions(dims); const int nx = dims[0], ny = dims[1], nz = dims[2]; auto* arr = vtkShortArray::SafeDownCast(img->GetPointData()->GetScalars()); GradStats gs; if (!arr || nx < 3 || ny < 3 || nz < 3) return gs; const std::int16_t blank = geopro::core::ScalarVolumeI16::kBlank; auto at = [&](int i, int j, int k) -> std::int16_t { const vtkIdType id = (static_cast(k) * ny + j) * nx + i; return arr->GetValue(id); }; std::vector mags; mags.reserve(static_cast(nx) * ny * nz / 8 + 1); // 步长抽样:大体不必逐体素,间隔取样即可代表分布(≤~50万样本)。 const vtkIdType total = static_cast(nx - 2) * (ny - 2) * (nz - 2); const int stride = static_cast(std::max(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::min(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(q.toQ(vminPhys)); const double qmaxD = static_cast(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 opacity; opacity->AddPoint( static_cast(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(q.toQ(vminPhys)); const double qmaxD = static_cast(q.toQ(vmaxPhys)); vtkNew color; for (int t = 0; t < kSamples; ++t) { const double qd = qminD + (qmaxD - qminD) * t / (kSamples - 1); const auto qv = static_cast(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 ren; ren->SetBackground(0.05, 0.05, 0.09); rw->AddRenderer(ren); vtkNew mapper; mapper->SetInputData(locImg); mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); mapper->SetAutoAdjustSampleDistances(0); mapper->SetInteractiveAdjustSampleDistances(0); auto prop = vtkSmartPointer::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 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::New(); volume->SetMapper(mapper); volume->SetProperty(prop); volume->SetScale(1.0, exagg, exagg); // 垂向夸张:薄轴放大,截面结构才看得出 ren->AddVolume(volume); auto capWin = vtkSmartPointer::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::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::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 [--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 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 [--line 001] [--cellXY 0.2] " "[--cellZ 0.05] [--out ] [--levels 2]\n" " gpr_poc build-stream [--cellXY 0.05] [--cellZ 0.05] " "[--out ] [--levels 3] [--sliceXBricks 8] " "[--maxLines N]\n" " gpr_poc load \n" " gpr_poc selftest\n" " gpr_poc offscreen-smoke\n" " gpr_poc renderB [--frames 120]\n" " gpr_poc renderC [--budget 64] [--frames 120]\n" " gpr_poc renderC-partitioned [--frames 120]\n" " gpr_poc renderLOD [--frames 120]\n" " gpr_poc tune [--opacity 0.5] [--exagg 8] " "[--frames 120] [--localBricks 4]\n" " gpr_poc fps-budget [--frames 90] " "[--bricks 4,16,64,128,256]\n" " gpr_poc view [--exagg 8] [--opacity 0.5] " "[--smoke] [--preview] [--near] [--variant N] [--gallery] " "[--frames 90]\n" " gpr_poc polish [--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 == "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; }