File main_3d.cpp
File List > gui > main_3d.cpp
Go to the documentation of this file
#include <QApplication>
#include <QMainWindow>
#include <QMatrix4x4>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <functional>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include "gl_widget.hpp"
#include "main_3d_window.hpp"
#include "utilities/env.hpp"
#include "utilities/filesystem.hpp"
#include "utilities/resources.hpp"
#ifndef USE_PDAL
#include "gui/frustum.hpp"
#include "gui/octree_las_data.hpp"
#include "gui/point_octree.hpp"
#include "las/las_file.hpp"
#include "las_reader.hpp"
#endif
#ifdef _WIN32
#include <windows.h>
#endif
struct LaunchOptions {
std::optional<fs::path> import_path;
std::optional<fs::path> las_path;
bool exit_after_load = false;
bool exit_after_render = false;
bool bench_mode = false;
bool probe_las = false;
};
static void print_usage(const char* program) {
std::cerr
<< "Usage: " << program << " [options] [las_file]\n"
<< "\n"
<< "Options:\n"
<< " --import-blaze-output <dir> Import blaze outputs from directory on startup\n"
<< " --exit-after-load Exit once all layers finish loading (for automation)\n"
<< " --exit-after-render Exit after first full render (for automation)\n"
<< " --bench Load, render 60 frames headless, print stats, exit\n"
<< " --probe-las <file> Load first LAS batch and print diagnostics (no GUI)\n"
<< " --help Show this help\n"
<< "\n"
<< "Environment:\n"
<< " BLAZE3D_IMPORT_OUTPUT Same as --import-blaze-output\n"
<< " BLAZE_TRACE=<path> Write Chrome/Perfetto trace JSON on completion\n"
<< "\n"
<< "Examples:\n"
<< " " << program << " sample.laz\n"
<< " " << program << " --import-blaze-output assets/out/combined --exit-after-load\n";
}
static LaunchOptions parse_args(int argc, char* argv[]) {
LaunchOptions opts;
if (const char* env = blaze::get_env("BLAZE3D_IMPORT_OUTPUT")) {
if (env[0] != '\0') {
opts.import_path = env;
}
}
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
print_usage(argv[0]);
std::exit(0);
}
if (arg == "--exit-after-load") {
opts.exit_after_load = true;
} else if (arg == "--exit-after-render") {
opts.exit_after_render = true;
} else if (arg == "--bench") {
opts.exit_after_load = true;
opts.bench_mode = true;
} else if (arg == "--probe-las") {
if (i + 1 >= argc) {
std::cerr << "Missing path after --probe-las\n";
print_usage(argv[0]);
std::exit(1);
}
opts.probe_las = true;
opts.las_path = argv[++i];
} else if (arg == "--import-blaze-output") {
if (i + 1 >= argc) {
std::cerr << "Missing path after --import-blaze-output\n";
print_usage(argv[0]);
std::exit(1);
}
opts.import_path = argv[++i];
} else if (!arg.empty() && arg[0] == '-') {
std::cerr << "Unknown option: " << arg << "\n";
print_usage(argv[0]);
std::exit(1);
} else {
opts.las_path = arg;
}
}
return opts;
}
#ifndef USE_PDAL
static int probe_las_file(const fs::path& filename) {
const fs::path log_path = filename.parent_path() / "blaze_probe.log";
std::ofstream log(log_path, std::ios::trunc);
auto log_line = [&](const std::string& line) {
std::cout << line << '\n';
if (log) {
log << line << '\n';
}
};
laspp::LASReader reader(filename);
const auto& header = reader.header();
const Extent3D bounds = as_extent3d(header.bounds());
const auto& transform = header.transform();
log_line("LAS probe: " + filename.string());
log_line(" total_points: " + std::to_string(reader.num_points()));
log_line(" chunks: " + std::to_string(reader.num_chunks()));
log_line(" base_point_format: " + std::to_string(header.point_format() & 0x7Fu));
log_line(" point_record_length: " + std::to_string(header.point_data_record_length()));
log_line(" scale: (" + std::to_string(transform.scale_factors().x()) + ", " +
std::to_string(transform.scale_factors().y()) + ", " +
std::to_string(transform.scale_factors().z()) + ")");
log_line(" offset: (" + std::to_string(transform.offsets().x()) + ", " +
std::to_string(transform.offsets().y()) + ", " +
std::to_string(transform.offsets().z()) + ")");
log_line(" bounds: " + ([&] {
std::ostringstream os;
os << bounds;
return os.str();
})());
if (reader.num_chunks() > 0) {
const size_t first_chunk_points = reader.points_per_chunk()[0];
std::vector<laspp::LASPointFormat7> first_chunk(first_chunk_points);
reader.read_chunks_list(std::span<laspp::LASPointFormat7>(first_chunk), {0});
int32_t raw_minx = std::numeric_limits<int32_t>::max();
int32_t raw_maxx = std::numeric_limits<int32_t>::lowest();
int32_t raw_miny = std::numeric_limits<int32_t>::max();
int32_t raw_maxy = std::numeric_limits<int32_t>::lowest();
double world_minx = std::numeric_limits<double>::max();
double world_maxx = std::numeric_limits<double>::lowest();
double world_miny = std::numeric_limits<double>::max();
double world_maxy = std::numeric_limits<double>::lowest();
const size_t sample_stride = std::max(size_t{1}, first_chunk.size() / 4096);
for (size_t i = 0; i < first_chunk.size(); i += sample_stride) {
const auto& pt = first_chunk[i];
raw_minx = std::min(raw_minx, pt.x);
raw_maxx = std::max(raw_maxx, pt.x);
raw_miny = std::min(raw_miny, pt.y);
raw_maxy = std::max(raw_maxy, pt.y);
const auto coords = transform.transform_point(pt.x, pt.y, pt.z);
world_minx = std::min(world_minx, coords.x());
world_maxx = std::max(world_maxx, coords.x());
world_miny = std::min(world_miny, coords.y());
world_maxy = std::max(world_maxy, coords.y());
}
log_line(" first_chunk_points: " + std::to_string(first_chunk.size()));
log_line(" raw_x: [" + std::to_string(raw_minx) + ", " + std::to_string(raw_maxx) + "]");
log_line(" raw_y: [" + std::to_string(raw_miny) + ", " + std::to_string(raw_maxy) + "]");
log_line(" world_x: [" + std::to_string(world_minx) + ", " + std::to_string(world_maxx) + "]");
log_line(" world_y: [" + std::to_string(world_miny) + ", " + std::to_string(world_maxy) + "]");
}
size_t load_callbacks = 0;
AsyncOctreeLASData data(filename, AsyncProgressTracker(), {[&] { ++load_callbacks; }});
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(30);
while (!data.load_complete() && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
log_line(" points_loaded: " + std::to_string(data.points_loaded()));
log_line(" load_complete: " + std::string(data.load_complete() ? "yes" : "no"));
log_line(" load_callbacks: " + std::to_string(load_callbacks));
if (!data.load_complete()) {
log_line("LAS probe failed: load did not complete within timeout");
return 1;
}
const Coordinate3D<double>& origin = data.coordinate_origin();
log_line(" coordinate_origin: (" + std::to_string(origin.x()) + ", " +
std::to_string(origin.y()) + ", " + std::to_string(origin.z()) + ")");
std::vector<PointOctree::VisibleNode> visible_nodes;
const auto snap = data.snapshot();
if (!snap) {
log_line("LAS probe failed: no snapshot published");
return 1;
}
log_line(" octree_total_points: " + std::to_string(snap->octree.total_points()));
log_line(" preview_points: " + std::to_string(snap->preview_points.size()));
size_t nodes_with_points = 0;
size_t max_node_points = 0;
size_t points_in_nodes = 0;
int max_node_depth = 0;
const OctreePointVector& all_points = snap->octree.points();
std::function<void(const PointOctreeNode&)> walk = [&](const PointOctreeNode& node) {
max_node_depth = std::max(max_node_depth, node.depth);
const size_t count = node.point_count();
if (count > 0) {
nodes_with_points += 1;
max_node_points = std::max(max_node_points, count);
points_in_nodes += count;
}
for (const auto& child : node.children) {
if (child) {
walk(*child);
}
}
};
if (snap->octree.root()) {
walk(*snap->octree.root());
}
log_line(" nodes_with_points: " + std::to_string(nodes_with_points));
log_line(" max_node_points: " + std::to_string(max_node_points));
log_line(" points_in_nodes: " + std::to_string(points_in_nodes));
log_line(" max_node_depth: " + std::to_string(max_node_depth));
float sample_minx = std::numeric_limits<float>::max();
float sample_maxx = std::numeric_limits<float>::lowest();
float sample_miny = std::numeric_limits<float>::max();
float sample_maxy = std::numeric_limits<float>::lowest();
const PointOctreeNode* mega_node = nullptr;
std::function<void(const PointOctreeNode&)> find_mega = [&](const PointOctreeNode& node) {
const size_t count = node.point_count();
if (count > 0 && (!mega_node || count > mega_node->point_count())) {
mega_node = &node;
}
for (const auto& child : node.children) {
if (child) {
find_mega(*child);
}
}
};
if (snap->octree.root()) {
find_mega(*snap->octree.root());
}
if (mega_node != nullptr && mega_node->point_count() > 0) {
const size_t mega_count = mega_node->point_count();
for (size_t i = 0; i < mega_count; i += std::max(size_t{1}, mega_count / 4096)) {
const OctreePoint& pt = all_points[mega_node->begin_index + i];
sample_minx = std::min(sample_minx, pt.x);
sample_maxx = std::max(sample_maxx, pt.x);
sample_miny = std::min(sample_miny, pt.y);
sample_maxy = std::max(sample_maxy, pt.y);
}
log_line(" mega_node_depth: " + std::to_string(mega_node->depth));
log_line(" mega_node_bounds: " + ([&] {
std::ostringstream os;
os << mega_node->bounds;
return os.str();
})());
log_line(" mega_sample_x: [" + std::to_string(sample_minx) + ", " +
std::to_string(sample_maxx) + "]");
log_line(" mega_sample_y: [" + std::to_string(sample_miny) + ", " +
std::to_string(sample_maxy) + "]");
}
// Probe diagnostic: bounds and camera are already file-local here, so no
// offset needs to be folded into the (identity) clip matrix.
const Frustum frustum = Frustum::from_matrix(QMatrix4x4());
const Coordinate3D<double> camera_local(0.0, 0.0, 5000.0);
snap->octree.collect_visible(frustum, 1.0, camera_local, visible_nodes);
size_t drawable_points = 0;
for (const auto& visible : visible_nodes) {
if (!visible.node) {
continue;
}
drawable_points += std::min(visible.node->point_count(), visible.chunk_size);
}
log_line(" visible_nodes: " + std::to_string(visible_nodes.size()));
log_line(" approx_drawable_points: " + std::to_string(drawable_points));
log_line("LAS probe OK");
return 0;
}
#else
static int probe_las_file(const fs::path& filename) {
std::cerr << "LAS probe requires las++ (BLAZE_USE_PDAL=OFF)\n";
(void)filename;
return 1;
}
#endif
int main(int argc, char* argv[]) {
blaze::enable_discrete_gpu_offload();
#ifdef _WIN32
bool owned_console = false;
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
owned_console = AllocConsole() != FALSE;
}
if (GetConsoleWindow() != nullptr) {
FILE* unused = nullptr;
freopen_s(&unused, "CONOUT$", "w", stdout);
freopen_s(&unused, "CONOUT$", "w", stderr);
}
#endif
QSurfaceFormat format;
format.setVersion(3, 3);
format.setProfile(QSurfaceFormat::CoreProfile);
QSurfaceFormat::setDefaultFormat(format);
const LaunchOptions opts = parse_args(argc, argv);
if (opts.probe_las && opts.las_path) {
return probe_las_file(*opts.las_path);
}
QApplication a(argc, argv);
#ifdef _WIN32
if (owned_console) {
QObject::connect(&a, &QApplication::aboutToQuit, [] { FreeConsole(); });
}
#endif
Main3DWindow window;
window.set_exit_after_load(opts.exit_after_load);
window.set_exit_after_render(opts.exit_after_render);
window.set_bench_mode(opts.bench_mode);
window.show();
if (opts.las_path) {
window.add_layer(std::make_unique<LASLayer>(*opts.las_path, window.add_progress_tracker(),
window.scene_reference_crs()));
} else if (!opts.import_path) {
window.add_layer(std::make_unique<LASLayer>(AssetRetriever::get_asset("sample.laz"),
window.add_progress_tracker(),
window.scene_reference_crs()));
}
if (opts.import_path) {
window.set_defer_render_until_loaded(true);
window.import_blaze_output_from_path(opts.import_path->string());
}
return QApplication::exec();
}