File point_octree.hpp
File List > gui > point_octree.hpp
Go to the documentation of this file
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstdint>
#include <functional>
#include <memory>
#include <random>
#include <vector>
#include "gui/frustum.hpp"
#include "utilities/coordinate.hpp"
#include "utilities/tracked_allocator.hpp"
// AoS layout: one struct per point, contiguous in m_points. Matches GPU interleaved
// attributes and allows zero-copy glBufferSubData slices per octree leaf.
struct OctreePoint {
float x;
float y;
float z;
uint16_t intensity = 0;
uint8_t classification = 0;
uint8_t file_r = 0;
uint8_t file_g = 0;
uint8_t file_b = 0;
uint8_t has_file_rgb = 0;
};
using OctreePointVector = blaze::memory_tracker::LasVector<OctreePoint>;
// Fast Fisher-Yates shuffle with xorshift64 — shared by both classes.
inline void octree_shuffle_range(OctreePointVector& points, size_t begin, size_t end) {
if (end <= begin + 1) return;
static thread_local uint64_t rng_state = std::random_device{}();
for (size_t i = end - 1; i > begin; --i) {
uint64_t x = rng_state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
rng_state = x;
std::swap(points[i], points[begin + static_cast<size_t>(x % (i - begin + 1))]);
}
}
class PointOctreeNode {
public:
static constexpr size_t MAX_POINTS = 32'000;
static constexpr int MAX_DEPTH = 20;
Extent3D bounds;
size_t begin_index = 0;
size_t end_index = 0;
std::array<std::unique_ptr<PointOctreeNode>, 8> children;
int depth = 0;
size_t point_count() const { return end_index - begin_index; }
bool has_children() const {
return std::any_of(children.begin(), children.end(),
[](const auto& child) { return child != nullptr; });
}
int child_index(const OctreePoint& point) const {
const double midx = (bounds.minx + bounds.maxx) * 0.5;
const double midy = (bounds.miny + bounds.maxy) * 0.5;
const double midz = (bounds.minz + bounds.maxz) * 0.5;
int index = 0;
if (point.x >= midx) index |= 1;
if (point.y >= midy) index |= 2;
if (point.z >= midz) index |= 4;
return index;
}
void ensure_child(int idx) {
if (children[static_cast<size_t>(idx)]) {
return;
}
const double midx = (bounds.minx + bounds.maxx) * 0.5;
const double midy = (bounds.miny + bounds.maxy) * 0.5;
const double midz = (bounds.minz + bounds.maxz) * 0.5;
const double minx = (idx & 1) ? midx : bounds.minx;
const double maxx = (idx & 1) ? bounds.maxx : midx;
const double miny = (idx & 2) ? midy : bounds.miny;
const double maxy = (idx & 2) ? bounds.maxy : midy;
const double minz = (idx & 4) ? midz : bounds.minz;
const double maxz = (idx & 4) ? bounds.maxz : midz;
children[static_cast<size_t>(idx)] = std::make_unique<PointOctreeNode>();
children[static_cast<size_t>(idx)]->bounds = Extent3D(minx, maxx, miny, maxy, minz, maxz);
children[static_cast<size_t>(idx)]->depth = depth + 1;
}
void shuffle_range(OctreePointVector& points) const {
octree_shuffle_range(points, begin_index, end_index);
}
void shuffle_recursive(OctreePointVector& points) {
shuffle_range(points);
for (const auto& child : children) {
if (child) {
child->shuffle_recursive(points);
}
}
}
};
class PointOctree {
std::unique_ptr<PointOctreeNode> m_root;
OctreePointVector m_points;
size_t m_total_points = 0;
static Extent3D cubic_root_bounds(const Extent3D& data_bounds) {
const double cx = 0.5 * (data_bounds.minx + data_bounds.maxx);
const double cy = 0.5 * (data_bounds.miny + data_bounds.maxy);
const double cz = 0.5 * (data_bounds.minz + data_bounds.maxz);
const double dx = data_bounds.maxx - data_bounds.minx;
const double dy = data_bounds.maxy - data_bounds.miny;
const double dz = data_bounds.maxz - data_bounds.minz;
double half = 0.5 * std::max({dx, dy, dz});
if (half < 1e-3) {
half = 0.5;
}
return {cx - half, cx + half, cy - half, cy + half, cz - half, cz + half};
}
public:
PointOctree() {
m_root = std::make_unique<PointOctreeNode>();
m_root->bounds = Extent3D(0.0, 1.0, 0.0, 1.0, 0.0, 1.0);
}
explicit PointOctree(const Extent3D& bounds) {
m_root = std::make_unique<PointOctreeNode>();
m_root->bounds = cubic_root_bounds(bounds);
}
size_t total_points() const { return m_total_points; }
const PointOctreeNode* root() const { return m_root.get(); }
const OctreePointVector& points() const { return m_points; }
// Builds the octree from points (taking ownership). progress, if set, is
// called with (points_processed, total). cancel, if set and observed true,
// aborts the build promptly so a destructor join does not stall.
void insert_batch(OctreePointVector&& points,
const std::function<void(size_t, size_t)>& progress = {},
const std::atomic<bool>* cancel = nullptr);
void shuffle_leaves() {
if (m_root) {
m_root->shuffle_recursive(m_points);
}
}
struct VisibleNode {
const PointOctreeNode* node = nullptr;
size_t chunk_size = 1;
double lod_distance = 0.0;
};
static size_t node_draw_chunk_size(size_t point_count, double lod_distance, double quality) {
if (point_count <= 1) {
return point_count;
}
constexpr double DRAW_ALL_DISTANCE = 100.0;
const double dist = std::max(10.0, lod_distance);
const double clamped_quality = std::clamp(quality, 0.05, 64.0);
const double desired_fraction =
std::min(1.0, clamped_quality * std::pow(DRAW_ALL_DISTANCE / dist, 2.0));
return std::max(size_t{1}, static_cast<size_t>(std::ceil(point_count * desired_fraction)));
}
// The frustum's clip matrix and camera_local must already be expressed in this
// octree's file-local coordinates (the caller folds the layer offset into the
// matrix), so node bounds are tested directly with no per-node translation.
void collect_visible(const Frustum& frustum, double quality,
const Coordinate3D<double>& camera_local,
std::vector<VisibleNode>& out) const {
out.clear();
if (!m_root) {
return;
}
collect_visible_recursive(*m_root, frustum, quality, camera_local, out);
}
private:
static double lod_distance_to_node(const Extent3D& bounds,
const Coordinate3D<double>& camera_local) {
const Coordinate3D<double> center(0.5 * (bounds.minx + bounds.maxx),
0.5 * (bounds.miny + bounds.maxy),
0.5 * (bounds.minz + bounds.maxz));
const double dx = center.x() - camera_local.x();
const double dy = center.y() - camera_local.y();
const double dz = center.z() - camera_local.z();
const double camera_distance = std::sqrt(dx * dx + dy * dy + dz * dz);
const double diag_radius = 0.5 * bounds.max_extent() * std::sqrt(3.0);
return std::max(10.0, camera_distance - diag_radius);
}
void collect_visible_recursive(const PointOctreeNode& node, const Frustum& frustum,
double quality, const Coordinate3D<double>& camera_local,
std::vector<VisibleNode>& out) const {
if (!frustum.intersects(node.bounds)) {
return;
}
if (node.has_children()) {
for (const auto& child : node.children) {
if (child) {
collect_visible_recursive(*child, frustum, quality, camera_local, out);
}
}
}
const size_t point_count = node.point_count();
if (point_count == 0) {
return;
}
const double lod_distance = lod_distance_to_node(node.bounds, camera_local);
out.push_back({&node, node_draw_chunk_size(point_count, lod_distance, quality), lod_distance});
}
};