mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-17 02:49:12 +00:00
Route bonsai through wgpu; delete the GL backend
Bonsai now drives the wgpu viewport for both sidecar and direct-IFC
loads. The GL viewer and its supporting state classes are gone.
SceneLoader rewire:
- Takes WgpuViewportWindow* instead of ViewportWindow*.
- Sidecar path reads metadata only (readSidecarMetadataOnly) and hands
the StreamingSidecar off to the new applyCachedModel. Field accesses
inside applySidecarData go through .meta.
- Direct-IFC path uses the wgpu A-path (upload{Mesh,Instance}Chunk +
finalizeModel). The applyLodExtension call is dropped — wgpu has no
live LOD1 splice; LOD1 still lands in the on-disk sidecar for the
next open.
Bonsai migration:
- ViewportWindow → WgpuViewportWindow across MainWindow, Measurement,
SessionState, and every modules/*/{Commands,Panel,View}.{h,cpp} —
116 sites total. Same s/OverlayRenderer::/WgpuOverlayRenderer::/
rename, 12 sites.
- Includes flipped from ../ifcviewer/ViewportWindow.h to
../ifcviewer-wgpu/WgpuViewportWindow.h. OverlayRenderer.h include
dropped (transitively reached via the viewport header).
- BonsaiViewer links IfcViewerWgpu in addition to IfcViewer for the
duration of the migration; the GL-side IfcViewer also publicly links
IfcViewerWgpu so SceneLoader can resolve WgpuViewportWindow.
GL backend deletion:
- src/ifcviewer/ViewportWindow.{cpp,h}, BvhAccel.*, OverlayRenderer.*,
Selection.*, Visibility.* all gone.
- src/ifcviewer-minimal/ removed entirely (MinimalWindow drove the GL
viewport).
- src/ifcviewer/tests: test_bvh_accel, test_selection, test_visibility
removed. The first has no replacement (wgpu doesn't use a per-instance
BVH); the latter two are ported separately. test_lod_builder,
test_sidecar_cache, test_instanced_geometry, test_federation remain
(backend-agnostic).
- IfcViewer's CMakeLists drops OpenGL, Qt::OpenGL, Qt::Widgets — none
of the surviving translation units reach for them.
Build flag plumbing:
- BUILD_BONSAIVIEWER now auto-enables BUILD_BONSAIVIEWER_WGPU since
SceneLoader requires the wgpu lib for its WgpuViewportWindow* arg.
- The wgpu subprojects add_subdirectory ahead of the GL one so
IfcViewerWgpu exists when IfcViewer's link evaluates.
- src/ifcviewer-minimal subdir reference removed from cmake/CMakeLists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,725 +0,0 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "OverlayRenderer.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QFontDatabase>
|
||||
#include <QFontMetrics>
|
||||
#include <QPainter>
|
||||
#include <QtGlobal>
|
||||
#include <QtOpenGL/QOpenGLPaintDevice>
|
||||
|
||||
namespace {
|
||||
|
||||
GLuint compile(QOpenGLFunctions_4_5_Core* gl, GLenum type, const char* src) {
|
||||
GLuint s = gl->glCreateShader(type);
|
||||
gl->glShaderSource(s, 1, &src, nullptr);
|
||||
gl->glCompileShader(s);
|
||||
GLint ok = 0;
|
||||
gl->glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048];
|
||||
gl->glGetShaderInfoLog(s, sizeof(log), nullptr, log);
|
||||
qWarning("OverlayRenderer shader compile error: %s", log);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
GLuint link(QOpenGLFunctions_4_5_Core* gl, GLuint vs, GLuint fs) {
|
||||
GLuint p = gl->glCreateProgram();
|
||||
gl->glAttachShader(p, vs);
|
||||
gl->glAttachShader(p, fs);
|
||||
gl->glLinkProgram(p);
|
||||
GLint ok = 0;
|
||||
gl->glGetProgramiv(p, GL_LINK_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048];
|
||||
gl->glGetProgramInfoLog(p, sizeof(log), nullptr, log);
|
||||
qWarning("OverlayRenderer program link error: %s", log);
|
||||
}
|
||||
gl->glDeleteShader(vs);
|
||||
gl->glDeleteShader(fs);
|
||||
return p;
|
||||
}
|
||||
|
||||
QFont overlayTextFont(int point_size) {
|
||||
QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
font.setPointSize(point_size);
|
||||
font.setStyleHint(QFont::TypeWriter);
|
||||
return font;
|
||||
}
|
||||
|
||||
// ---- Triangle program (flat color) ----
|
||||
|
||||
const char* TRI_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_pos;
|
||||
uniform mat4 u_view_proj;
|
||||
void main() {
|
||||
gl_Position = u_view_proj * vec4(in_pos, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* TRI_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_color;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
frag_color = u_color;
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Point sprite program (outlined disc via gl_PointCoord) ----
|
||||
|
||||
const char* POINT_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_pos;
|
||||
uniform mat4 u_view_proj;
|
||||
uniform float u_point_size;
|
||||
void main() {
|
||||
gl_Position = u_view_proj * vec4(in_pos, 1.0);
|
||||
gl_PointSize = u_point_size;
|
||||
}
|
||||
)";
|
||||
|
||||
// inner_radius_norm is the inner-disc radius as a fraction of the
|
||||
// half-sprite (so 1.0 = no stroke, smaller = thicker stroke). The
|
||||
// fragment shader reads gl_PointCoord (range [0,1] across the sprite),
|
||||
// computes the distance from the centre normalised against the half-
|
||||
// sprite, picks inner vs stroke with a sharp `step()` (no soft band),
|
||||
// then anti-aliases the *outer* edge only.
|
||||
const char* POINT_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_inner_color;
|
||||
uniform vec4 u_stroke_color;
|
||||
uniform float u_inner_radius_norm;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
vec2 c = gl_PointCoord - 0.5;
|
||||
float d = length(c) * 2.0; // 0 at centre, 1 at sprite edge
|
||||
if (d > 1.0) discard;
|
||||
float t_inner = step(u_inner_radius_norm, d);
|
||||
vec4 col = mix(u_inner_color, u_stroke_color, t_inner);
|
||||
float aa = fwidth(d);
|
||||
float outer_alpha = smoothstep(1.0, 1.0 - aa, d);
|
||||
frag_color = vec4(col.rgb, col.a * outer_alpha);
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Line program (screen-space-expanded quads with outline) ----
|
||||
//
|
||||
// Per-vertex layout: (in_a, in_b, in_side, in_along), 8 floats total.
|
||||
// The vertex shader projects both endpoints to screen pixels, computes
|
||||
// the screen-space perpendicular, and offsets *this* corner accordingly.
|
||||
// Output v_dist_px is the signed perpendicular distance from the line
|
||||
// axis at this corner; linear interpolation across the quad gives the
|
||||
// per-fragment distance the FS uses to discard / pick inner vs stroke.
|
||||
|
||||
const char* LINE_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 in_a;
|
||||
layout(location = 1) in vec3 in_b;
|
||||
layout(location = 2) in float in_side; // -1 or +1
|
||||
layout(location = 3) in float in_along; // 0 (at a) or 1 (at b)
|
||||
uniform mat4 u_view_proj;
|
||||
uniform vec2 u_screen_size; // physical pixels
|
||||
uniform float u_half_width; // inner half-width (px)
|
||||
uniform float u_stroke_extra; // halo per side (px)
|
||||
out float v_dist_px;
|
||||
out float v_along_px; // distance from segment start (px)
|
||||
void main() {
|
||||
vec4 clip_a = u_view_proj * vec4(in_a, 1.0);
|
||||
vec4 clip_b = u_view_proj * vec4(in_b, 1.0);
|
||||
|
||||
// Project to screen pixels.
|
||||
vec2 screen_a = (clip_a.xy / clip_a.w) * 0.5 * u_screen_size;
|
||||
vec2 screen_b = (clip_b.xy / clip_b.w) * 0.5 * u_screen_size;
|
||||
|
||||
vec2 delta = screen_b - screen_a;
|
||||
float len = length(delta);
|
||||
vec2 dir = (len > 1e-6) ? (delta / len) : vec2(1.0, 0.0);
|
||||
vec2 perp = vec2(-dir.y, dir.x);
|
||||
|
||||
// Offset this corner perpendicular to the line.
|
||||
vec4 clip_self = mix(clip_a, clip_b, in_along);
|
||||
vec2 screen_self = (clip_self.xy / clip_self.w) * 0.5 * u_screen_size;
|
||||
float total_half = u_half_width + u_stroke_extra;
|
||||
screen_self += perp * in_side * total_half;
|
||||
|
||||
// Back to NDC, then to clip space (multiply by w to undo the w-divide
|
||||
// GL is about to apply). Depth is preserved from the picked endpoint.
|
||||
vec2 ndc_out = screen_self / (u_screen_size * 0.5);
|
||||
gl_Position = vec4(ndc_out * clip_self.w, clip_self.z, clip_self.w);
|
||||
|
||||
v_dist_px = in_side * total_half;
|
||||
v_along_px = in_along * len;
|
||||
}
|
||||
)";
|
||||
|
||||
// ---- Screen-space rect program (label + HUD backgrounds) ----
|
||||
//
|
||||
// Skip QPainter::fillRect entirely — on QOpenGLPaintDevice it's
|
||||
// unreliable across drivers. Backgrounds are drawn as raw GL quads
|
||||
// using NDC-space coordinates; QPainter only renders the text on top.
|
||||
|
||||
const char* RECT_VS = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec2 in_ndc;
|
||||
void main() {
|
||||
gl_Position = vec4(in_ndc, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* RECT_FS = R"(
|
||||
#version 450 core
|
||||
uniform vec4 u_color;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
frag_color = u_color;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* LINE_FS = R"(
|
||||
#version 450 core
|
||||
in float v_dist_px;
|
||||
in float v_along_px;
|
||||
uniform vec4 u_inner_color;
|
||||
uniform vec4 u_stroke_color;
|
||||
uniform float u_half_width;
|
||||
uniform float u_stroke_extra;
|
||||
uniform float u_dash_period; // 0 = solid
|
||||
uniform float u_dash_on_ratio;
|
||||
out vec4 frag_color;
|
||||
void main() {
|
||||
if (u_dash_period > 0.0) {
|
||||
float t = mod(v_along_px, u_dash_period);
|
||||
if (t > u_dash_period * u_dash_on_ratio) discard;
|
||||
}
|
||||
float ad = abs(v_dist_px);
|
||||
float total = u_half_width + u_stroke_extra;
|
||||
if (ad > total) discard;
|
||||
|
||||
// Sharp inner-to-stroke transition; AA only the outer halo edge so
|
||||
// the line reads crisp instead of mushy.
|
||||
float t_stroke = step(u_half_width, ad);
|
||||
vec4 col = mix(u_inner_color, u_stroke_color, t_stroke);
|
||||
float outer_a = smoothstep(total, total - 1.0, ad);
|
||||
frag_color = vec4(col.rgb, col.a * outer_a);
|
||||
}
|
||||
)";
|
||||
|
||||
void uploadFloats(QOpenGLFunctions_4_5_Core* gl,
|
||||
GLuint vbo, size_t& capacity_bytes,
|
||||
const std::vector<float>& data) {
|
||||
const size_t bytes = data.size() * sizeof(float);
|
||||
if (bytes == 0) return;
|
||||
if (bytes > capacity_bytes) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(new_cap), nullptr, GL_DYNAMIC_DRAW);
|
||||
capacity_bytes = new_cap;
|
||||
}
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(bytes), data.data());
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
void uploadFloatBytes(QOpenGLFunctions_4_5_Core* gl,
|
||||
GLuint vbo, size_t& capacity_bytes,
|
||||
const float* data, size_t float_count) {
|
||||
const size_t bytes = float_count * sizeof(float);
|
||||
if (bytes == 0) return;
|
||||
if (bytes > capacity_bytes) {
|
||||
const size_t new_cap = bytes + bytes / 2;
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(new_cap), nullptr, GL_DYNAMIC_DRAW);
|
||||
capacity_bytes = new_cap;
|
||||
}
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
gl->glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(bytes), data);
|
||||
gl->glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
// CPU expansion of N segments (3 floats * 2 verts per segment, packed) into
|
||||
// 6 vertices per segment, each carrying (a, b, side, along) = 8 floats.
|
||||
void expandLineSegments(const std::vector<float>& endpoints,
|
||||
std::vector<float>& out) {
|
||||
out.clear();
|
||||
if (endpoints.size() < 6) return;
|
||||
const size_t n_segs = endpoints.size() / 6;
|
||||
out.reserve(n_segs * 6 * 8);
|
||||
static const float CORNERS[6][2] = {
|
||||
{-1.0f, 0.0f}, {+1.0f, 0.0f}, {-1.0f, 1.0f},
|
||||
{-1.0f, 1.0f}, {+1.0f, 0.0f}, {+1.0f, 1.0f},
|
||||
};
|
||||
for (size_t s = 0; s < n_segs; ++s) {
|
||||
const float* a = &endpoints[s * 6 + 0];
|
||||
const float* b = &endpoints[s * 6 + 3];
|
||||
for (int c = 0; c < 6; ++c) {
|
||||
out.push_back(a[0]); out.push_back(a[1]); out.push_back(a[2]);
|
||||
out.push_back(b[0]); out.push_back(b[1]); out.push_back(b[2]);
|
||||
out.push_back(CORNERS[c][0]);
|
||||
out.push_back(CORNERS[c][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OverlayRenderer::initialize(QOpenGLFunctions_4_5_Core* gl) {
|
||||
if (gl_) return;
|
||||
gl_ = gl;
|
||||
|
||||
// Triangle program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, TRI_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, TRI_FS);
|
||||
program_tri_ = link(gl_, vs, fs);
|
||||
u_tri_view_proj_ = gl_->glGetUniformLocation(program_tri_, "u_view_proj");
|
||||
u_tri_color_ = gl_->glGetUniformLocation(program_tri_, "u_color");
|
||||
}
|
||||
// Point program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, POINT_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, POINT_FS);
|
||||
program_pt_ = link(gl_, vs, fs);
|
||||
u_pt_view_proj_ = gl_->glGetUniformLocation(program_pt_, "u_view_proj");
|
||||
u_pt_point_size_ = gl_->glGetUniformLocation(program_pt_, "u_point_size");
|
||||
u_pt_inner_color_ = gl_->glGetUniformLocation(program_pt_, "u_inner_color");
|
||||
u_pt_stroke_color_ = gl_->glGetUniformLocation(program_pt_, "u_stroke_color");
|
||||
u_pt_inner_radius_ = gl_->glGetUniformLocation(program_pt_, "u_inner_radius_norm");
|
||||
}
|
||||
// Line program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, LINE_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, LINE_FS);
|
||||
program_ln_ = link(gl_, vs, fs);
|
||||
u_ln_view_proj_ = gl_->glGetUniformLocation(program_ln_, "u_view_proj");
|
||||
u_ln_screen_size_ = gl_->glGetUniformLocation(program_ln_, "u_screen_size");
|
||||
u_ln_half_width_ = gl_->glGetUniformLocation(program_ln_, "u_half_width");
|
||||
u_ln_stroke_extra_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_extra");
|
||||
u_ln_inner_color_ = gl_->glGetUniformLocation(program_ln_, "u_inner_color");
|
||||
u_ln_stroke_color_ = gl_->glGetUniformLocation(program_ln_, "u_stroke_color");
|
||||
u_ln_dash_period_ = gl_->glGetUniformLocation(program_ln_, "u_dash_period");
|
||||
u_ln_dash_on_ratio_ = gl_->glGetUniformLocation(program_ln_, "u_dash_on_ratio");
|
||||
}
|
||||
// Screen-space rect program.
|
||||
{
|
||||
GLuint vs = compile(gl_, GL_VERTEX_SHADER, RECT_VS);
|
||||
GLuint fs = compile(gl_, GL_FRAGMENT_SHADER, RECT_FS);
|
||||
program_rect_ = link(gl_, vs, fs);
|
||||
u_rect_color_ = gl_->glGetUniformLocation(program_rect_, "u_color");
|
||||
}
|
||||
|
||||
// Triangle VAO/VBO: one vec3 attribute.
|
||||
gl_->glCreateVertexArrays(1, &triangles_.vao);
|
||||
gl_->glCreateBuffers(1, &triangles_.vbo);
|
||||
gl_->glEnableVertexArrayAttrib(triangles_.vao, 0);
|
||||
gl_->glVertexArrayAttribFormat(triangles_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(triangles_.vao, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(triangles_.vao, 0, triangles_.vbo,
|
||||
0, 3 * sizeof(float));
|
||||
|
||||
// Point VAO/VBO: one vec3 attribute.
|
||||
gl_->glCreateVertexArrays(1, &points_.vao);
|
||||
gl_->glCreateBuffers(1, &points_.vbo);
|
||||
gl_->glEnableVertexArrayAttrib(points_.vao, 0);
|
||||
gl_->glVertexArrayAttribFormat(points_.vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(points_.vao, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(points_.vao, 0, points_.vbo,
|
||||
0, 3 * sizeof(float));
|
||||
|
||||
// Line VAO/VBO: 8 floats per vertex (a:vec3, b:vec3, side, along).
|
||||
// Shared across every group; line_draws_ records the (first, count)
|
||||
// slice for each.
|
||||
gl_->glCreateVertexArrays(1, &vao_lines_);
|
||||
gl_->glCreateBuffers(1, &vbo_lines_);
|
||||
const GLsizei stride = 8 * sizeof(float);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 0);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 0, 0);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 1);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float));
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 1, 0);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 2);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float));
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 2, 0);
|
||||
gl_->glEnableVertexArrayAttrib(vao_lines_, 3);
|
||||
gl_->glVertexArrayAttribFormat(vao_lines_, 3, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float));
|
||||
gl_->glVertexArrayAttribBinding(vao_lines_, 3, 0);
|
||||
gl_->glVertexArrayVertexBuffer(vao_lines_, 0, vbo_lines_, 0, stride);
|
||||
|
||||
// Screen-rect VAO/VBO: 2 floats per vertex (vec2 NDC).
|
||||
gl_->glCreateVertexArrays(1, &vao_rect_);
|
||||
gl_->glCreateBuffers(1, &vbo_rect_);
|
||||
gl_->glEnableVertexArrayAttrib(vao_rect_, 0);
|
||||
gl_->glVertexArrayAttribFormat(vao_rect_, 0, 2, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(vao_rect_, 0, 0);
|
||||
gl_->glVertexArrayVertexBuffer(vao_rect_, 0, vbo_rect_, 0, 2 * sizeof(float));
|
||||
}
|
||||
|
||||
void OverlayRenderer::release() {
|
||||
if (!gl_) return;
|
||||
if (triangles_.vbo) gl_->glDeleteBuffers(1, &triangles_.vbo);
|
||||
if (triangles_.vao) gl_->glDeleteVertexArrays(1, &triangles_.vao);
|
||||
if (points_.vbo) gl_->glDeleteBuffers(1, &points_.vbo);
|
||||
if (points_.vao) gl_->glDeleteVertexArrays(1, &points_.vao);
|
||||
if (vbo_lines_) gl_->glDeleteBuffers(1, &vbo_lines_);
|
||||
if (vao_lines_) gl_->glDeleteVertexArrays(1, &vao_lines_);
|
||||
if (vbo_rect_) gl_->glDeleteBuffers(1, &vbo_rect_);
|
||||
if (vao_rect_) gl_->glDeleteVertexArrays(1, &vao_rect_);
|
||||
if (program_tri_) gl_->glDeleteProgram(program_tri_);
|
||||
if (program_pt_) gl_->glDeleteProgram(program_pt_);
|
||||
if (program_ln_) gl_->glDeleteProgram(program_ln_);
|
||||
if (program_rect_) gl_->glDeleteProgram(program_rect_);
|
||||
triangles_ = {};
|
||||
points_ = {};
|
||||
line_draws_.clear();
|
||||
vao_lines_ = vbo_lines_ = 0;
|
||||
vbo_lines_capacity_ = 0;
|
||||
vao_rect_ = vbo_rect_ = 0;
|
||||
vbo_rect_capacity_ = 0;
|
||||
program_tri_ = program_pt_ = program_ln_ = program_rect_ = 0;
|
||||
gl_ = nullptr;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setHudText(const QString& text) {
|
||||
hud_text_ = text;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayLabels(const std::vector<Label>& labels) {
|
||||
labels_ = labels;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setSelectionRect(const QRect& rect_logical) {
|
||||
selection_rect_ = rect_logical;
|
||||
}
|
||||
|
||||
void OverlayRenderer::setHighlightTriangles(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a) {
|
||||
if (!gl_) return;
|
||||
triangles_.color[0] = r; triangles_.color[1] = g;
|
||||
triangles_.color[2] = b; triangles_.color[3] = a;
|
||||
triangles_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, triangles_.vbo, triangles_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayPoints(const std::vector<float>& world_xyz,
|
||||
float r, float g, float b, float a,
|
||||
float pixel_size,
|
||||
float sr, float sg, float sb, float sa,
|
||||
float stroke_extra) {
|
||||
if (!gl_) return;
|
||||
points_.inner_color[0] = r; points_.inner_color[1] = g;
|
||||
points_.inner_color[2] = b; points_.inner_color[3] = a;
|
||||
points_.stroke_color[0] = sr; points_.stroke_color[1] = sg;
|
||||
points_.stroke_color[2] = sb; points_.stroke_color[3] = sa;
|
||||
points_.pixel_size = pixel_size;
|
||||
points_.stroke_extra = stroke_extra;
|
||||
points_.vertex_count = GLsizei(world_xyz.size() / 3);
|
||||
uploadFloats(gl_, points_.vbo, points_.vbo_capacity, world_xyz);
|
||||
}
|
||||
|
||||
void OverlayRenderer::setOverlayLines(const std::vector<LineGroup>& groups) {
|
||||
if (!gl_) return;
|
||||
line_draws_.clear();
|
||||
|
||||
// Concatenate every group's CPU-expanded vertices into one big buffer
|
||||
// and remember each group's (first, count) slice + style so render()
|
||||
// can iterate without re-expanding.
|
||||
std::vector<float> combined;
|
||||
for (const auto& g : groups) {
|
||||
std::vector<float> exp;
|
||||
expandLineSegments(g.world_xyz, exp);
|
||||
if (exp.empty()) continue;
|
||||
LineDrawCall dc;
|
||||
std::memcpy(dc.color, g.color, sizeof(dc.color));
|
||||
std::memcpy(dc.stroke_color, g.stroke_color, sizeof(dc.stroke_color));
|
||||
dc.line_width = g.line_width;
|
||||
dc.stroke_extra = g.stroke_extra;
|
||||
dc.dash_period_px = g.dash_period_px;
|
||||
dc.dash_on_ratio = g.dash_on_ratio;
|
||||
dc.first = GLint(combined.size() / 8);
|
||||
dc.count = GLsizei(exp.size() / 8);
|
||||
line_draws_.push_back(dc);
|
||||
combined.insert(combined.end(), exp.begin(), exp.end());
|
||||
}
|
||||
uploadFloats(gl_, vbo_lines_, vbo_lines_capacity_, combined);
|
||||
}
|
||||
|
||||
void OverlayRenderer::render(const float view_proj[16],
|
||||
int pixel_w, int pixel_h, qreal dpr) {
|
||||
if (!gl_) return;
|
||||
|
||||
// Save GL state we touch.
|
||||
GLboolean prev_blend = gl_->glIsEnabled(GL_BLEND);
|
||||
GLboolean prev_cull = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_pt_size = gl_->glIsEnabled(GL_PROGRAM_POINT_SIZE);
|
||||
GLboolean prev_depth_msk = GL_TRUE;
|
||||
gl_->glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_msk);
|
||||
GLint prev_depth_func = GL_LESS;
|
||||
gl_->glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func);
|
||||
GLint prev_blend_src = GL_ONE, prev_blend_dst = GL_ZERO;
|
||||
gl_->glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
|
||||
gl_->glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
|
||||
|
||||
gl_->glEnable(GL_BLEND);
|
||||
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glDepthMask(GL_FALSE);
|
||||
gl_->glDepthFunc(GL_LEQUAL);
|
||||
gl_->glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
if (triangles_.vertex_count > 0 && triangles_.color[3] > 0.0f) {
|
||||
// Highlight triangles stay depth-aware (GL_LEQUAL) so they tint
|
||||
// the surface in place rather than poking through walls.
|
||||
gl_->glUseProgram(program_tri_);
|
||||
gl_->glUniformMatrix4fv(u_tri_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform4fv(u_tri_color_, 1, triangles_.color);
|
||||
gl_->glBindVertexArray(triangles_.vao);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, triangles_.vertex_count);
|
||||
}
|
||||
// Measurement annotations (lines + points) draw on top of every other
|
||||
// pass — the standard CAD convention. GL_ALWAYS wins every depth
|
||||
// compare; GL_LEQUAL is restored at the end of the function.
|
||||
gl_->glDepthFunc(GL_ALWAYS);
|
||||
if (!line_draws_.empty()) {
|
||||
gl_->glUseProgram(program_ln_);
|
||||
gl_->glUniformMatrix4fv(u_ln_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform2f(u_ln_screen_size_, float(pixel_w), float(pixel_h));
|
||||
gl_->glBindVertexArray(vao_lines_);
|
||||
for (const auto& dc : line_draws_) {
|
||||
if (dc.count == 0 || dc.color[3] <= 0.0f) continue;
|
||||
gl_->glUniform1f(u_ln_half_width_, dc.line_width * 0.5f);
|
||||
gl_->glUniform1f(u_ln_stroke_extra_, dc.stroke_extra);
|
||||
gl_->glUniform4fv(u_ln_inner_color_, 1, dc.color);
|
||||
gl_->glUniform4fv(u_ln_stroke_color_, 1, dc.stroke_color);
|
||||
gl_->glUniform1f(u_ln_dash_period_, dc.dash_period_px);
|
||||
gl_->glUniform1f(u_ln_dash_on_ratio_, dc.dash_on_ratio);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, dc.first, dc.count);
|
||||
}
|
||||
}
|
||||
if (points_.vertex_count > 0 && points_.inner_color[3] > 0.0f) {
|
||||
// Inner-radius ratio in [0, 1]: how much of the sprite is the
|
||||
// inner colour vs the stroke band. pixel_size is the inner-disc
|
||||
// diameter; the sprite (and gl_PointSize) is enlarged by
|
||||
// 2*stroke_extra so the halo has somewhere to draw.
|
||||
const float total = points_.pixel_size + 2.0f * points_.stroke_extra;
|
||||
const float inner_ratio = (total > 0.0f)
|
||||
? (points_.pixel_size / total) : 1.0f;
|
||||
gl_->glUseProgram(program_pt_);
|
||||
gl_->glUniformMatrix4fv(u_pt_view_proj_, 1, GL_FALSE, view_proj);
|
||||
gl_->glUniform1f(u_pt_point_size_, total);
|
||||
gl_->glUniform1f(u_pt_inner_radius_, inner_ratio);
|
||||
gl_->glUniform4fv(u_pt_inner_color_, 1, points_.inner_color);
|
||||
gl_->glUniform4fv(u_pt_stroke_color_, 1, points_.stroke_color);
|
||||
gl_->glBindVertexArray(points_.vao);
|
||||
gl_->glDrawArrays(GL_POINTS, 0, points_.vertex_count);
|
||||
}
|
||||
gl_->glBindVertexArray(0);
|
||||
|
||||
if (!prev_blend) gl_->glDisable(GL_BLEND);
|
||||
gl_->glBlendFunc(prev_blend_src, prev_blend_dst);
|
||||
if (prev_cull) gl_->glEnable(GL_CULL_FACE);
|
||||
if (!prev_pt_size) gl_->glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
gl_->glDepthMask(prev_depth_msk);
|
||||
gl_->glDepthFunc(prev_depth_func);
|
||||
|
||||
if (pixel_w <= 0 || pixel_h <= 0) return;
|
||||
|
||||
const float logical_w = float(pixel_w) / float(dpr ? dpr : 1.0);
|
||||
const float logical_h = float(pixel_h) / float(dpr ? dpr : 1.0);
|
||||
auto px_to_ndc_x = [logical_w](float px) {
|
||||
return (px / logical_w) * 2.0f - 1.0f;
|
||||
};
|
||||
auto px_to_ndc_y = [logical_h](float px) {
|
||||
return 1.0f - (px / logical_h) * 2.0f;
|
||||
};
|
||||
|
||||
// Box-select rectangle: translucent fill + 1-px outline drawn as
|
||||
// four thin rects. Comes before the HUD/label pass so the HUD
|
||||
// backgrounds still render on top of the rectangle if they overlap.
|
||||
if (selection_rect_.isValid()
|
||||
&& selection_rect_.width() > 0
|
||||
&& selection_rect_.height() > 0) {
|
||||
struct RectPx { float x0, y0, x1, y1; };
|
||||
const QRect& sr = selection_rect_;
|
||||
const float sx0 = float(sr.left());
|
||||
const float sy0 = float(sr.top());
|
||||
const float sx1 = float(sr.right() + 1);
|
||||
const float sy1 = float(sr.bottom() + 1);
|
||||
const RectPx pieces[5] = {
|
||||
// Fill
|
||||
{sx0, sy0, sx1, sy1},
|
||||
// Top edge
|
||||
{sx0, sy0, sx1, sy0 + 1.0f},
|
||||
// Bottom edge
|
||||
{sx0, sy1 - 1.0f, sx1, sy1},
|
||||
// Left edge
|
||||
{sx0, sy0, sx0 + 1.0f, sy1},
|
||||
// Right edge
|
||||
{sx1 - 1.0f, sy0, sx1, sy1},
|
||||
};
|
||||
const float colors[5][4] = {
|
||||
{0.30f, 0.65f, 1.0f, 0.18f}, // fill
|
||||
{0.30f, 0.65f, 1.0f, 0.9f}, // outline (each edge)
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
{0.30f, 0.65f, 1.0f, 0.9f},
|
||||
};
|
||||
|
||||
GLboolean prev_dt2 = gl_->glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prev_cf2 = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
GLboolean prev_bl2 = gl_->glIsEnabled(GL_BLEND);
|
||||
gl_->glDisable(GL_DEPTH_TEST);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glEnable(GL_BLEND);
|
||||
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl_->glUseProgram(program_rect_);
|
||||
gl_->glBindVertexArray(vao_rect_);
|
||||
|
||||
// Each piece is its own draw so we can switch alpha between
|
||||
// fill and outline. All five share the same VBO slot — we
|
||||
// stream-overwrite per draw.
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
const float x0 = px_to_ndc_x(pieces[i].x0);
|
||||
const float x1 = px_to_ndc_x(pieces[i].x1);
|
||||
const float y0 = px_to_ndc_y(pieces[i].y0);
|
||||
const float y1 = px_to_ndc_y(pieces[i].y1);
|
||||
const float ndc[12] = {
|
||||
x0, y0, x1, y0, x0, y1,
|
||||
x0, y1, x1, y0, x1, y1
|
||||
};
|
||||
uploadFloatBytes(gl_, vbo_rect_, vbo_rect_capacity_, ndc, 12);
|
||||
gl_->glUniform4f(u_rect_color_,
|
||||
colors[i][0], colors[i][1],
|
||||
colors[i][2], colors[i][3]);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
gl_->glBindVertexArray(0);
|
||||
if (prev_dt2) gl_->glEnable(GL_DEPTH_TEST);
|
||||
if (prev_cf2) gl_->glEnable(GL_CULL_FACE);
|
||||
if (!prev_bl2) gl_->glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
// Two-stage HUD/label pass: collect rect bounds (in logical pixels) +
|
||||
// text strings, draw all rect backgrounds via GL (screen-space NDC
|
||||
// quads, depth test off), then run a QPainter pass that *only* draws
|
||||
// text on top. Side-stepping QPainter::fillRect entirely avoids the
|
||||
// QOpenGLPaintDevice quirk where solid fills silently drop while
|
||||
// text continues to render.
|
||||
const bool any_painter = !hud_text_.isEmpty() || !labels_.empty();
|
||||
if (!any_painter) return;
|
||||
|
||||
QFont label_font = overlayTextFont(9);
|
||||
QFont hud_font = overlayTextFont(11);
|
||||
const QFontMetrics lfm(label_font);
|
||||
const QFontMetrics hfm(hud_font);
|
||||
|
||||
const int label_pad_x = 4, label_pad_y = 2;
|
||||
const int hud_pad_x = 10, hud_pad_y = 6;
|
||||
const int hud_margin = 12;
|
||||
|
||||
struct PaintItem { QRect bg; QString text; const QFont* font; int align; };
|
||||
std::vector<PaintItem> items;
|
||||
items.reserve(labels_.size() + 1);
|
||||
|
||||
// World-anchored label rects.
|
||||
for (const auto& lbl : labels_) {
|
||||
const float* p = lbl.world_pos;
|
||||
// Column-major: M[col*4 + row].
|
||||
const float wx = view_proj[0]*p[0] + view_proj[4]*p[1] + view_proj[8]*p[2] + view_proj[12];
|
||||
const float wy = view_proj[1]*p[0] + view_proj[5]*p[1] + view_proj[9]*p[2] + view_proj[13];
|
||||
const float ww = view_proj[3]*p[0] + view_proj[7]*p[1] + view_proj[11]*p[2] + view_proj[15];
|
||||
if (ww <= 0.0f) continue; // behind camera
|
||||
const float ndc_x = wx / ww;
|
||||
const float ndc_y = wy / ww;
|
||||
if (ndc_x < -1.0f || ndc_x > 1.0f
|
||||
|| ndc_y < -1.0f || ndc_y > 1.0f) continue;
|
||||
const float sx = (ndc_x * 0.5f + 0.5f) * logical_w;
|
||||
const float sy = (1.0f - (ndc_y * 0.5f + 0.5f)) * logical_h;
|
||||
const int tw = lfm.horizontalAdvance(lbl.text);
|
||||
const int th = lfm.height();
|
||||
QRect bg(int(sx) - tw / 2 - label_pad_x,
|
||||
int(sy) - th / 2 - label_pad_y,
|
||||
tw + 2 * label_pad_x,
|
||||
th + 2 * label_pad_y);
|
||||
items.push_back({bg, lbl.text, &label_font, Qt::AlignCenter});
|
||||
}
|
||||
// HUD rect (always top-left if any text).
|
||||
if (!hud_text_.isEmpty()) {
|
||||
const QStringList lines = hud_text_.split('\n');
|
||||
int tw = 0;
|
||||
for (const auto& ln : lines) tw = qMax(tw, hfm.horizontalAdvance(ln));
|
||||
const int th = hfm.height() * lines.size();
|
||||
QRect bg(hud_margin, hud_margin,
|
||||
tw + 2 * hud_pad_x,
|
||||
th + 2 * hud_pad_y);
|
||||
items.push_back({bg, hud_text_, &hud_font, int(Qt::AlignLeft | Qt::AlignTop)});
|
||||
}
|
||||
|
||||
// GL pass: draw all background rects as NDC-space triangles.
|
||||
if (!items.empty()) {
|
||||
std::vector<float> ndc;
|
||||
ndc.reserve(items.size() * 12); // 6 verts * 2 floats per rect
|
||||
for (const auto& it : items) {
|
||||
const float x0 = px_to_ndc_x(float(it.bg.left()));
|
||||
const float x1 = px_to_ndc_x(float(it.bg.right() + 1));
|
||||
const float y0 = px_to_ndc_y(float(it.bg.top()));
|
||||
const float y1 = px_to_ndc_y(float(it.bg.bottom() + 1));
|
||||
ndc.insert(ndc.end(), {
|
||||
x0, y0, x1, y0, x0, y1,
|
||||
x0, y1, x1, y0, x1, y1
|
||||
});
|
||||
}
|
||||
uploadFloats(gl_, vbo_rect_, vbo_rect_capacity_, ndc);
|
||||
|
||||
// GL_TRIANGLES respects GL_CULL_FACE; the NDC→window y-flip turns
|
||||
// our CCW NDC quads into window-CW which get back-culled if cull
|
||||
// is on (which it is by default in this app). Disable cull for
|
||||
// the rect pass — lines+points above were unaffected since
|
||||
// GL_LINES / GL_POINTS skip face culling entirely.
|
||||
GLboolean prev_depth_test = gl_->glIsEnabled(GL_DEPTH_TEST);
|
||||
GLboolean prev_cull_face = gl_->glIsEnabled(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_DEPTH_TEST);
|
||||
gl_->glDisable(GL_CULL_FACE);
|
||||
gl_->glDisable(GL_BLEND);
|
||||
gl_->glUseProgram(program_rect_);
|
||||
gl_->glUniform4f(u_rect_color_, 0.08f, 0.08f, 0.08f, 1.0f);
|
||||
gl_->glBindVertexArray(vao_rect_);
|
||||
gl_->glDrawArrays(GL_TRIANGLES, 0, GLsizei(items.size() * 6));
|
||||
gl_->glBindVertexArray(0);
|
||||
if (prev_depth_test) gl_->glEnable(GL_DEPTH_TEST);
|
||||
if (prev_cull_face) gl_->glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
// QPainter pass: text only, on top of the GL-drawn backgrounds.
|
||||
QOpenGLPaintDevice device(QSize(pixel_w, pixel_h));
|
||||
device.setDevicePixelRatio(dpr);
|
||||
QPainter painter(&device);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||||
painter.setPen(Qt::white);
|
||||
for (const auto& it : items) {
|
||||
painter.setFont(*it.font);
|
||||
const int px = it.font == &hud_font ? hud_pad_x : label_pad_x;
|
||||
const int py = it.font == &hud_font ? hud_pad_y : label_pad_y;
|
||||
painter.drawText(it.bg.adjusted(px, py, -px, -py), it.align, it.text);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user