File octree_las_layer_renderer.cpp
File List > gui > octree_las_layer_renderer.cpp
Go to the documentation of this file
#include <QOpenGLContext>
#include <QOpenGLExtraFunctions>
#include <QPoint>
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <limits>
#include <mutex>
#include <unordered_map>
#include "gui/frustum.hpp"
#include "gui/gl_check.hpp"
#include "gui/layer_renderer.hpp"
#include "gui/shaders.hpp"
namespace {
class ScopedGpuTimer {
public:
ScopedGpuTimer(QOpenGLExtraFunctions* gl, GLuint query) : m_gl(gl), m_query(query) {
if (m_gl && m_query != 0) {
CHECK_GL(m_gl->glBeginQuery(GL_TIME_ELAPSED, m_query));
}
}
~ScopedGpuTimer() {
if (m_gl && m_query != 0) {
CHECK_GL(m_gl->glEndQuery(GL_TIME_ELAPSED));
}
}
explicit operator bool() const { return m_gl != nullptr && m_query != 0; }
private:
QOpenGLExtraFunctions* m_gl;
GLuint m_query;
};
constexpr double DRAW_CALL_OVERHEAD_MS = 0.012;
constexpr size_t MAX_PREVIEW_DRAW_VERTICES = 500'000;
constexpr float STREAM_VIEW_RESET_DISTANCE = 25.0f;
} // namespace
OctreeLASLayerRenderer::OctreeLASLayerRenderer(std::shared_ptr<LASLayer> layer,
const Coordinate3D<double>& /*offset*/)
: m_layer(layer) {}
OctreeLASLayerRenderer::~OctreeLASLayerRenderer() {
if (m_gpu_timer_query != 0) {
// Try to clean up the GPU query if a context is available.
// If the context has already been destroyed, the resource will
// be reclaimed by the driver on context teardown.
if (auto* ctx = QOpenGLContext::currentContext()) {
auto* gl = ctx->extraFunctions();
if (gl) {
gl->glDeleteQueries(1, &m_gpu_timer_query);
}
}
m_gpu_timer_query = 0;
}
}
void OctreeLASLayerRenderer::ensure_shader() {
if (m_shader) {
return;
}
m_shader = std::make_unique<QOpenGLShaderProgram>();
const QString vertex_src = get_point_vertex_shader();
const QString fragment_src = get_point_fragment_shader();
if (vertex_src.isEmpty() || fragment_src.isEmpty()) {
std::cout << "Failed to load point shaders from resources" << std::endl;
m_shader.reset();
return;
}
if (!m_shader->addShaderFromSourceCode(QOpenGLShader::Vertex, vertex_src) ||
!m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, fragment_src)) {
std::cout << "Point shader error: " << m_shader->log().toStdString() << std::endl;
m_shader.reset();
return;
}
m_shader->bindAttributeLocation("local_position", 0);
m_shader->bindAttributeLocation("classification", 1);
m_shader->bindAttributeLocation("file_color", 2);
m_shader->bindAttributeLocation("has_file_rgb", 3);
if (!m_shader->link()) {
std::cout << "Point shader link error: " << m_shader->log().toStdString() << std::endl;
m_shader.reset();
return;
}
m_view_matrix_loc = m_shader->uniformLocation("u_view");
m_proj_matrix_loc = m_shader->uniformLocation("u_proj");
m_point_radius_loc = m_shader->uniformLocation("point_radius_m");
m_viewport_height_loc = m_shader->uniformLocation("viewport_height");
m_fov_rad_loc = m_shader->uniformLocation("fov_rad");
m_color_mode_loc = m_shader->uniformLocation("color_mode");
m_fixed_color_loc = m_shader->uniformLocation("fixed_color");
m_point_alpha_loc = m_shader->uniformLocation("point_alpha");
m_point_offset_loc = m_shader->uniformLocation("point_offset");
m_shader_layer_slot_loc = m_shader->uniformLocation("u_layer_slot");
m_light_direction_eye_loc = m_shader->uniformLocation("u_light_direction_eye");
m_ambient_light_loc = m_shader->uniformLocation("u_ambient_light");
m_diffuse_light_loc = m_shader->uniformLocation("u_diffuse_light");
}
void OctreeLASLayerRenderer::reset_stream_cache() {
m_node_stream.clear();
m_stream_backlog = false;
}
void OctreeLASLayerRenderer::refresh_after_style_change() {
m_node_stream.clear();
m_stream_backlog = true;
m_lod_query_vertices = 0;
}
size_t OctreeLASLayerRenderer::visible_nodes_fingerprint(
const std::vector<PointOctree::VisibleNode>& visible_nodes) const {
size_t fingerprint = visible_nodes.size();
for (const auto& visible : visible_nodes) {
fingerprint ^= std::hash<const void*>{}(visible.node) + 0x9e3779b9 + (fingerprint << 6) +
(fingerprint >> 2);
}
return fingerprint;
}
bool OctreeLASLayerRenderer::stream_camera_changed(const Camera& camera) const {
if ((camera.position() - m_stream_camera_pos).lengthSquared() >
STREAM_VIEW_RESET_DISTANCE * STREAM_VIEW_RESET_DISTANCE) {
return true;
}
if ((camera.direction().normalized() - m_stream_camera_dir).lengthSquared() > 0.01f) {
return true;
}
return false;
}
bool OctreeLASLayerRenderer::visible_set_changed(
const std::vector<PointOctree::VisibleNode>& visible_nodes) const {
return visible_nodes_fingerprint(visible_nodes) != m_visible_fingerprint;
}
void OctreeLASLayerRenderer::sort_visible_by_lod(
std::vector<PointOctree::VisibleNode>& visible_nodes) {
if (visible_nodes.size() <= 1) {
return;
}
const bool needs_sort =
std::adjacent_find(visible_nodes.begin(), visible_nodes.end(),
[](const PointOctree::VisibleNode& a, const PointOctree::VisibleNode& b) {
return a.lod_distance > b.lod_distance;
}) != visible_nodes.end();
if (!needs_sort) {
return;
}
std::stable_sort(visible_nodes.begin(), visible_nodes.end(),
[](const PointOctree::VisibleNode& a, const PointOctree::VisibleNode& b) {
if (a.lod_distance != b.lod_distance) {
return a.lod_distance < b.lod_distance;
}
return a.node < b.node;
});
}
void OctreeLASLayerRenderer::collect_visible_octree_nodes(
const LasRenderSnapshot& snap, const Camera& camera, double vis_quality,
const Coordinate3D<double>& file_origin,
std::vector<PointOctree::VisibleNode>& visible_nodes) const {
const Coordinate3D<double>& scene_offset = camera.world_offset();
const QVector3D point_offset(static_cast<float>(file_origin.x() - scene_offset.x()),
static_cast<float>(file_origin.y() - scene_offset.y()),
static_cast<float>(file_origin.z() - scene_offset.z()));
QMatrix4x4 clip = camera.proj_matrix();
clip.translate(point_offset);
const Frustum frustum = Frustum::from_matrix(clip);
const Coordinate3D<double> camera_local(camera.position().x() - point_offset.x(),
camera.position().y() - point_offset.y(),
camera.position().z() - point_offset.z());
snap.octree.collect_visible(frustum, vis_quality, camera_local, visible_nodes);
sort_visible_by_lod(visible_nodes);
}
size_t OctreeLASLayerRenderer::estimate_draw_vertices(
const std::vector<PointOctree::VisibleNode>& visible_nodes, double quality,
bool incremental) const {
size_t total = 0;
for (const auto& visible : visible_nodes) {
if (!visible.node || visible.node->point_count() == 0) {
continue;
}
const size_t point_count = visible.node->point_count();
const size_t chunk_size =
PointOctree::node_draw_chunk_size(point_count, visible.lod_distance, quality);
if (incremental) {
const auto it = m_node_stream.find(visible.node);
const size_t streamed =
it != m_node_stream.end() ? std::min(it->second.streamed_count, point_count) : 0;
if (streamed >= point_count) {
continue;
}
total += std::min(chunk_size, point_count - streamed);
} else {
total += std::min(chunk_size, point_count);
}
}
return total;
}
double OctreeLASLayerRenderer::select_draw_quality(
const std::vector<PointOctree::VisibleNode>& visible_nodes, bool incremental,
bool lod_base_from_incremental, double target_draw_ms) const {
const double base = lod_base_from_incremental ? m_inc_lod_quality : m_lod_quality;
const size_t vertices = estimate_draw_vertices(visible_nodes, base, incremental);
const double est_ms = static_cast<double>(vertices) * m_ms_per_vertex +
(vertices > 0 ? DRAW_CALL_OVERHEAD_MS : 0.0);
if (est_ms <= 1e-6) {
return std::clamp(base, 0.05, 64.0);
}
return std::clamp(base * target_draw_ms / est_ms, 0.05, 64.0);
}
void OctreeLASLayerRenderer::record_lod_sample(size_t vertices, double ms) {
if (vertices == 0 || ms <= 0.0) {
return;
}
constexpr double ALPHA = 0.2;
constexpr double MIN_MS_PER_VERTEX = 30.0 / 2'000'000.0;
const double instant = ms / static_cast<double>(vertices);
m_ms_per_vertex =
m_ms_per_vertex > 0.0 ? (1.0 - ALPHA) * m_ms_per_vertex + ALPHA * instant : instant;
m_ms_per_vertex = std::max(m_ms_per_vertex, MIN_MS_PER_VERTEX);
}
void OctreeLASLayerRenderer::ensure_gpu_timer(QOpenGLExtraFunctions* gl) {
if (m_gpu_timer_query != 0 || !gl) {
return;
}
CHECK_GL(gl->glGenQueries(1, &m_gpu_timer_query));
}
void OctreeLASLayerRenderer::consume_gpu_timer_sample(QOpenGLExtraFunctions* gl) {
if (!gl || m_gpu_timer_query == 0 || m_lod_query_vertices == 0) {
return;
}
GLuint available = 0;
CHECK_GL(gl->glGetQueryObjectuiv(m_gpu_timer_query, GL_QUERY_RESULT_AVAILABLE, &available));
if (!available) {
return;
}
GLuint elapsed_ns = 0;
CHECK_GL(gl->glGetQueryObjectuiv(m_gpu_timer_query, GL_QUERY_RESULT, &elapsed_ns));
const double gpu_ms = static_cast<double>(elapsed_ns) / 1'000'000.0;
m_last_point_gpu_ms = gpu_ms;
double effective_ms = gpu_ms;
if (m_lod_query_cpu_ms > 0.0) {
// GL_TIME_ELAPSED under-reports with PRIME offload and async drivers.
effective_ms = std::max(effective_ms, m_lod_query_cpu_ms);
}
record_lod_sample(m_lod_query_vertices, effective_ms);
}
size_t OctreeLASLayerRenderer::draw_octree_nodes(
QOpenGLFunctions* f, const OctreePointVector& point_storage,
const std::vector<PointOctree::VisibleNode>& visible_nodes,
const Coordinate3D<double>& file_origin, const Coordinate3D<double>& scene_offset,
double quality, bool incremental) {
m_stream_backlog = false;
if (!m_shader) {
return 0;
}
m_point_gl.ensure_initialized(f);
m_point_gl.bind(f);
CHECK_GL_AFTER();
const QVector3D point_offset(static_cast<float>(file_origin.x() - scene_offset.x()),
static_cast<float>(file_origin.y() - scene_offset.y()),
static_cast<float>(file_origin.z() - scene_offset.z()));
m_shader->setUniformValue(m_point_offset_loc, point_offset);
CHECK_GL_AFTER();
if (!m_points_uploaded && !point_storage.empty()) {
m_point_gl.upload_points(f, point_storage.data(), point_storage.size());
m_points_uploaded = true;
CHECK_GL_AFTER();
}
if (!incremental) {
for (const auto& visible : visible_nodes) {
if (!visible.node) continue;
NodeStreamState& state = m_node_stream[visible.node];
state.point_count = visible.node->point_count();
state.streamed_count = 0;
state.locked_chunk_size = 0;
}
}
m_firsts.clear();
m_counts.clear();
for (const auto& visible : visible_nodes) {
if (!visible.node || visible.node->point_count() == 0) continue;
const PointOctreeNode* node = visible.node;
const size_t point_count = node->point_count();
NodeStreamState& state = m_node_stream[node];
if (state.point_count != point_count) {
state.point_count = point_count;
state.streamed_count = 0;
}
const size_t chunk_size =
PointOctree::node_draw_chunk_size(point_count, visible.lod_distance, quality);
state.locked_chunk_size = chunk_size;
size_t to_draw = 0;
if (incremental) {
if (state.streamed_count >= point_count) continue;
to_draw = std::min(state.locked_chunk_size, point_count - state.streamed_count);
} else {
to_draw = std::min(state.locked_chunk_size, point_count);
}
if (to_draw == 0) continue;
const size_t first_index = node->begin_index + state.streamed_count;
if (first_index > static_cast<size_t>(std::numeric_limits<GLint>::max())) {
m_stream_backlog = true;
continue;
}
m_firsts.push_back(static_cast<GLint>(first_index));
m_counts.push_back(static_cast<GLsizei>(to_draw));
state.streamed_count += to_draw;
if (state.streamed_count < point_count) {
m_stream_backlog = true;
}
}
m_shader->setUniformValue(m_shader_layer_slot_loc, m_layer_slot > 0 ? m_layer_slot : 0);
CHECK_GL_AFTER();
return m_point_gl.draw_leaves(f, m_firsts.data(), m_counts.data(), m_firsts.size());
}
size_t OctreeLASLayerRenderer::draw_preview_points(QOpenGLFunctions* f,
const OctreePointVector& preview,
const Coordinate3D<double>& file_origin,
const Coordinate3D<double>& scene_offset) {
if (preview.empty() || !m_shader) {
return 0;
}
m_point_gl.ensure_initialized(f);
m_point_gl.bind(f);
CHECK_GL_AFTER();
const size_t count = std::min(preview.size(), MAX_PREVIEW_DRAW_VERTICES);
const QVector3D point_offset(static_cast<float>(file_origin.x() - scene_offset.x()),
static_cast<float>(file_origin.y() - scene_offset.y()),
static_cast<float>(file_origin.z() - scene_offset.z()));
m_shader->setUniformValue(m_point_offset_loc, point_offset);
m_shader->setUniformValue(m_shader_layer_slot_loc, 0);
CHECK_GL_AFTER();
const size_t total_vertices = m_point_gl.draw_points(f, preview.data(), count);
m_stream_backlog = preview.size() > count;
return total_vertices;
}
bool OctreeLASLayerRenderer::can_fbo_pick() const {
auto layer = m_layer.lock();
if (!layer || m_layer_slot == 0) {
return false;
}
if (!layer->las_data().load_complete()) {
return false;
}
auto snap = layer->las_data().snapshot();
return snap && snap->octree.total_points() > 0;
}
std::optional<PointPickResult> OctreeLASLayerRenderer::point_from_index(
uint32_t layer_slot, uint32_t pick_index, const Coordinate3D<double>& scene_offset) const {
if (pick_index == 0 || layer_slot == 0) {
return std::nullopt;
}
if (layer_slot != static_cast<uint32_t>(m_layer_slot)) {
return std::nullopt;
}
const size_t idx = static_cast<size_t>(pick_index) - 1;
auto layer = m_layer.lock();
if (!layer) {
return std::nullopt;
}
auto snap = layer->las_data().snapshot();
if (!snap || idx >= snap->octree.points().size()) {
return std::nullopt;
}
const OctreePoint& p = snap->octree.points()[idx];
const Coordinate3D<double>& file_origin = layer->las_data().coordinate_origin();
PointPickResult result;
result.point = p;
result.world_x = static_cast<double>(p.x) + file_origin.x() - scene_offset.x();
result.world_y = static_cast<double>(p.y) + file_origin.y() - scene_offset.y();
result.world_z = static_cast<double>(p.z) + file_origin.z() - scene_offset.z();
result.layer_name = layer->name();
return result;
}
void OctreeLASLayerRenderer::render(const Camera& camera, const RenderContext& ctx) {
if (!m_visible) {
return;
}
auto layer = m_layer.lock();
if (!layer) {
return;
}
m_last_point_draw_ms = 0.0;
m_last_point_gpu_ms = 0.0;
m_last_point_vertices_drawn = 0;
const bool incremental = ctx.incremental_points;
std::vector<PointOctree::VisibleNode> visible_nodes;
size_t loaded_octree_points = 0;
bool load_complete = false;
bool use_preview = false;
const Coordinate3D<double>& scene_offset = camera.world_offset();
const Coordinate3D<double>& file_origin = layer->las_data().coordinate_origin();
const auto snap = layer->las_data().snapshot();
if (!snap) {
if (!layer->las_data().load_complete()) {
emit repaint_required();
}
return;
}
loaded_octree_points = snap->octree.total_points();
load_complete = layer->las_data().load_complete();
if (loaded_octree_points == 0 && snap->preview_points.empty()) {
if (!load_complete) {
emit repaint_required();
}
return;
}
use_preview = !load_complete && loaded_octree_points == 0 && !snap->preview_points.empty();
if (!use_preview) {
const double base_quality = std::max(m_lod_quality, 0.25);
const double adjusted_quality = base_quality * layer->lod_quality_multiplier();
collect_visible_octree_nodes(*snap, camera, adjusted_quality, file_origin, visible_nodes);
}
const bool camera_moved = stream_camera_changed(camera);
const bool visible_changed = load_complete && !use_preview && visible_set_changed(visible_nodes);
const bool reset_stream = camera_moved || visible_changed;
if (camera_moved || visible_changed) {
m_stream_camera_pos = camera.position();
m_stream_camera_dir = camera.direction().normalized();
}
if (visible_changed) {
m_visible_fingerprint = visible_nodes_fingerprint(visible_nodes);
}
if (m_data_update_required) {
if (load_complete) {
reset_stream_cache();
}
m_points_uploaded = false;
m_data_update_required = false;
}
if (reset_stream) {
reset_stream_cache();
if (visible_changed) {
emit stream_view_reset();
}
}
ensure_shader();
if (!m_shader) {
std::cerr << "LAS render: point shader failed to compile or link\n";
return;
}
QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions();
if (!f) {
return;
}
QOpenGLExtraFunctions* gl = QOpenGLContext::currentContext()->extraFunctions();
ensure_gpu_timer(gl);
consume_gpu_timer_sample(gl);
const double target_draw_ms = layer->point_stream_budget_ms();
const bool stream_mode_changed = incremental != m_prev_incremental_stream;
m_prev_incremental_stream = incremental;
double draw_quality = 1.0;
if (!use_preview) {
draw_quality =
select_draw_quality(visible_nodes, incremental, stream_mode_changed, target_draw_ms);
}
const auto draw_start = std::chrono::steady_clock::now();
if (!bind_shader(m_shader.get())) {
return;
}
CHECK_GL(f->glEnable(GL_PROGRAM_POINT_SIZE));
m_shader->setUniformValue(m_view_matrix_loc, camera.view_matrix());
m_shader->setUniformValue(m_proj_matrix_loc, camera.projection_matrix());
m_shader->setUniformValue(m_point_radius_loc, layer->point_radius_m());
m_shader->setUniformValue(m_viewport_height_loc, ctx.viewport_height);
m_shader->setUniformValue(m_fov_rad_loc, static_cast<float>(camera.fov_rad()));
m_shader->setUniformValue(m_color_mode_loc, static_cast<int>(layer->point_color_mode()));
const std::array<uint8_t, 3>& fixed = layer->fixed_point_color();
m_shader->setUniformValue(m_fixed_color_loc,
QVector3D(fixed[0] / 255.f, fixed[1] / 255.f, fixed[2] / 255.f));
m_shader->setUniformValue(m_point_alpha_loc, layer->point_alpha());
if (m_light_direction_eye_loc >= 0) {
m_shader->setUniformValue(m_light_direction_eye_loc, ctx.light_direction_eye);
}
if (m_ambient_light_loc >= 0) {
m_shader->setUniformValue(m_ambient_light_loc, ctx.point_ambient_light);
}
if (m_diffuse_light_loc >= 0) {
m_shader->setUniformValue(m_diffuse_light_loc, ctx.diffuse_light);
}
CHECK_GL_AFTER();
CHECK_GL(f->glEnable(GL_DEPTH_TEST));
CHECK_GL(f->glDepthFunc(ctx.incremental_points ? GL_LEQUAL : GL_LESS));
CHECK_GL(f->glDepthMask(GL_TRUE));
CHECK_GL(f->glDisable(GL_BLEND));
const size_t expected_vertices =
use_preview ? std::min(snap->preview_points.size(), MAX_PREVIEW_DRAW_VERTICES)
: estimate_draw_vertices(visible_nodes, draw_quality, incremental);
const bool time_draw = expected_vertices > 0;
ScopedGpuTimer gpu_timer(time_draw ? gl : nullptr, time_draw ? m_gpu_timer_query : 0);
size_t vertices_drawn = 0;
if (use_preview) {
vertices_drawn = draw_preview_points(f, snap->preview_points, file_origin, scene_offset);
} else {
vertices_drawn = draw_octree_nodes(f, snap->octree.points(), visible_nodes, file_origin,
scene_offset, draw_quality, incremental);
}
if (gpu_timer) {
m_lod_query_vertices = vertices_drawn;
}
m_shader->release();
CHECK_GL_AFTER();
const double draw_ms =
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - draw_start)
.count();
m_last_point_draw_ms = draw_ms;
m_last_point_vertices_drawn = vertices_drawn;
if (gpu_timer) {
m_lod_query_cpu_ms = draw_ms;
}
if (vertices_drawn == 0) {
if (!load_complete) {
emit repaint_required();
} else if (m_stream_backlog) {
emit repaint_required();
}
return;
}
if (incremental) {
m_inc_lod_quality = draw_quality;
if (m_stream_backlog && !stream_mode_changed) {
m_lod_quality = draw_quality;
}
} else {
m_lod_quality = draw_quality;
m_inc_lod_quality = draw_quality;
}
if (!load_complete) {
emit repaint_required();
} else if (m_stream_backlog) {
emit repaint_required();
}
}