OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(7)-番外篇:点击的瞬间,发生了什么?)

25 阅读11分钟
  • 故事背景:点击的瞬间,发生了什么?

  • 第一步:射向虚拟世界的“激光束” (Ray Casting)

  • 换个视角:射线求交算法

  • 第二步:给场景套上“俄罗斯套娃” (BVH)

  • 换个视角:BVH 树结构

  • 第三步:比拾取更高级的“磁吸术” (Snapping)

  • 第四步:海量实体的“性能屏颈”与“分身术”

  • 给开发者的“番外”笔记

代码仓库入口:


系列文章规划:

  • (OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(1):从开发的视角看下CAD画出那些好看的图形们))

  • OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(2):看似“老派”的 C++ 底层优化,恰恰是这些前沿领域最需要的基础设施)

  • OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(3):你的 CAD 终于能画标准零件了,但用户想要“弧面”、“流线型”,怎么办?)

  • OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(4):GstarCAD / AutoCAD 客户端相关产品 —— 深入骨髓的数据库哲学)

  • OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(5)番外篇:给 CAD 加上“控制台”——让用户能实时“调参数、看性能”)

  • [OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(6)番外篇:让视图“活”起来——鼠标拖拽、缩放背后的数学魔法](blog.csdn.net/m0\\_524363…)

巨人的肩膀:

  • deepseek

  • gemini


@

  • 故事背景:点击的瞬间,发生了什么?

  • 第一步:射向虚拟世界的“激光束” (Ray Casting)

  • 换个视角:射线求交算法

  • 第二步:给场景套上“俄罗斯套娃” (BVH)

  • 换个视角:BVH 树结构

  • 第三步:比拾取更高级的“磁吸术” (Snapping)

  • 第四步:海量实体的“性能屏颈”与“分身术”

  • 给开发者的“番外”笔记


故事背景:点击的瞬间,发生了什么?

你已经搞定了数据库,画出了成千上万个零件。用户很高兴,但他很快又提出了一个看似简单、实则要命的要求:“我想用鼠标点中屏幕上那个直径 5mm 的螺栓。”

在用户看来,这只是点一下鼠标。但在你这个开发者眼里,屏幕只是一堆像素,显卡只管把三角形“扔”到屏幕上。你并没有一个天然的“鼠标点中物体”的反馈。

你面临的挑战是:在包含 10 万个螺栓、数百万个三角面片的复杂场景中,如何在一秒钟内(甚至微秒级)找到用户点的到底是哪一个面?


第一步:射向虚拟世界的“激光束” (Ray Casting)

你首先意识到,鼠标点击位置  可以转化为三维空间中的一根射线。 想象你的眼睛是原点,鼠标点击的像素点是方向,这根射线就像一把狙击枪的激光,从相机出发,穿过屏幕,射向深邃的模型空间。

其中  是相机位置, 是归一化后的点击方向。 你的任务变成了:求这根射线与场景中哪个三角形最早相交。

如果你用最笨的办法(遍历所有实体的所有三角形),当图纸里有 10 万个螺栓时,每点一下鼠标,程序都要计算几亿次“射线与三角形求交”。用户会发现,点一下鼠标,程序卡死三秒。这显然是不可接受的。

换个视角:射线求交算法

  • 包围盒相交 :使用 Slab 方法快速剔除不相交的空间,在 bvh.cpp:194-214 实现

  • 三角形相交 :使用 Möller-Trumbore 算法,在 bvh.cpp:216-268 实现

  • 命中检测 :记录最近命中的三角形,确保拾取精度


第二步:给场景套上“俄罗斯套娃” (BVH)

你决定引入 **BVH (Boundary Volume Hierarchy,边界体积层次结构)**。 你不再傻乎乎地去数每一个三角形,而是把物体装进一个个“盒子”里。

  1. 大盒子包小盒子:你给整个场景算一个巨大的包围盒(AABB)。然后把它切成两半,每半再装进中盒子,直到最后的小盒子只装几个三角形。

  2. 树状搜索:当射线射入时,如果连“大盒子”都没碰着,那里面成千上万个三角形直接“Pass”。如果碰到了大盒子,再去看它里面的两个中盒子……

  3. SAH 启发式算法:你为了让盒子的分布更科学,引入了 **SAH (Surface Area Heuristic)**。这就像是顺丰快递分拣中心:通过计算表面积,让射线碰到空盒子的概率降到最低,从而让搜索路径最短。

现在,原本  的线性搜索变成了 。10 万个对象,你只需要经过约 17 次“盒子测试”就能锁定目标。拾取延迟从“秒级”直接变成了“微秒级”。

换个视角:BVH 树结构

  • 树节点设计 :每个节点 64 字节,包含包围盒、子节点偏移/三角形索引、分割轴等信息

  • 构建算法 :使用 SAH (Surface Area Heuristic) 启发式分割,在 bvh.cpp:18-135 实现

  • 遍历优化 :非递归栈实现,避免函数调用开销,在 bvh.cpp:270-326 实现

#ifndef NOMINMAX
#define NOMINMAX
#endif

#pragma once

#include <array>
#include <vector>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <memory>
#include <limits>
#include <cstring>
#include "object_pool.h"
#include "stl_parser.h"

namespace hhb {
namespace core {

// Bounding box structure
struct Bounds {
    float min[3];
    float max[3];

    Bounds() {
        min[0] = min[1] = min[2] = 1e38f;
        max[0] = max[1] = max[2] = -1e38f;
    }

    Bounds(const float* min, const float* max) {
        std::memcpy(this->min, min, sizeof(float) * 3);
        std::memcpy(this->max, max, sizeof(float) * 3);
    }

    // Expand bounding box to include another point
    void expand(const float* point) {
        for (int i = 0; i < 3; ++i) {
            if (point[i] < min[i]) min[i] = point[i];
            if (point[i] > max[i]) max[i] = point[i];
        }
    }

    // Expand bounding box to include another bounding box
    void expand(const Bounds& other) {
        for (int i = 0; i < 3; ++i) {
            if (other.min[i] < min[i]) min[i] = other.min[i];
            if (other.max[i] > max[i]) max[i] = other.max[i];
        }
    }

    // Calculate surface area of bounding box
    float surface_area() const {
        float dx = max[0] - min[0];
        float dy = max[1] - min[1];
        float dz = max[2] - min[2];
        return 2.0f * (dx * dy + dy * dz + dz * dx);
    }

    // Calculate center of bounding box
    void center(float* c) const {
        for (int i = 0; i < 3; ++i) {
            c[i] = (min[i] + max[i]) * 0.5f;
        }
    }
};

// BVH node structure (64 bytes)
struct BVHNode {
    Bounds bounds;     // 24 bytes
    uint32_t left;     // 4 bytes - Left child offset or triangle index
    uint32_t right;    // 4 bytes - Right child offset or triangle count
    uint8_t split_axis; // 1 byte - Split axis
    uint8_t is_leaf;    // 1 byte - Whether it's a leaf node
    uint16_t padding;   // 2 bytes - Padding

    BVHNode() : left(0), right(0), split_axis(0), is_leaf(0), padding(0) {}
};

// BVH class
class BVH {
public:
    BVH() = default;
    ~BVH() = default;

    // Build BVH
    void build(std::vector<Triangle*>& triangles);

    // Ray-BVH intersection
    bool intersect(const float* ray_origin, const float* ray_direction, float& t_hit, Triangle*& hit_triangle) const;

    // Get BVH node count
    size_t node_count() const {
        return nodes_.size();
    }

    // Get BVH tree depth
    int depth() const {
        return tree_depth_;
    }

    // Get root bounding box
    Bounds get_root_bounds() const {
        if (nodes_.empty()) return Bounds();
        return nodes_[0].bounds;
    }

private:
    // BVH build parameters
    static constexpr size_t MAX_PRIMITIVES_PER_LEAF = 4;
    static constexpr float SAH_COST = 1.0f;
    static constexpr float TRAVERSAL_COST = 0.125f;

    // BVH build recursive function
    uint32_t build_recursive(std::vector<Triangle*>& triangles, size_t start, size_t end, int current_depth);

    // Calculate triangle bounding box
    Bounds compute_bounds(const Triangle* triangle) const;

    // SAH split algorithm
    uint32_t split_sah(std::vector<Triangle*>& triangles, size_t start, size_t end, Bounds& bounds, uint8_t& split_axis);

    // Ray-bounding box intersection
    bool intersect_bounds(const float* ray_origin, const float* ray_direction, const Bounds& bounds, float& t_min, float& t_max) const;

    // Ray-triangle intersection
    bool intersect_triangle(const float* ray_origin, const float* ray_direction, const Triangle* triangle, float& t_hit) const;

    std::vector<BVHNode> nodes_;        // BVH nodes
    std::vector<Triangle*> triangles_;   // Triangle pointers
    int tree_depth_;                     // BVH tree depth
};

} // namespace core
} // namespace hhb

第三步:比拾取更高级的“磁吸术” (Snapping)

用户又说了:“我不光要点中这个螺栓,我还要精准地连线到这个螺栓底面的中心点。”

这就是 AutoCAD 里著名的 **OSNAP (对象捕捉)**。 这本质上是“带权重的拾取”:

  • 你不仅射出一根线,还给这根线加了一个“感应半径”。

  • 在 BVH 搜索时,你寻找的不只是“相交”,而是寻找距离射线最近的特征点(端点、中点、圆心)。

  • 当鼠标靠近这些点时,你的 UI 逻辑会强制把坐标“吸”过去,并在屏幕上弹出一个绿色的小方框。

对于开发者来说,这需要你在 Bolt 类中预定义好这些拓扑特征点


第四步:海量实体的“性能屏颈”与“分身术”

当用户导入 10 万个螺栓时,除了拾取慢,渲染和遍历也成了噩梦。

问题原因 :

  • 传统线性遍历: O(n) 时间复杂度,10 万个对象需要遍历 10 万次

  • 无空间索引:每次选择都要检查所有对象

  • 重复几何:螺栓等标准件重复存储,浪费内存

你发现调用 GetEntityList() 然后一个个绘制,显卡驱动会因为频繁的指令调用(Draw Calls)而罢工。

你采用了两手准备:

  1. 空间索引优化:利用你建好的 BVH 或四叉树。渲染时,先做“视锥体剔除”——不在屏幕里的盒子,连送都不送给显卡。BVH 空间索引 :将时间复杂度降至 O(log n) ,10 万个对象只需约 17 次遍历

  2. Flyweight (享元模式) + 实例化渲染: 你意识到 10 万个螺栓其实长得都一样。你只在显存里存一份螺栓的几何数据(圆柱+螺旋线),然后给显卡发一个“名单”:这里有 10 万个位置偏移矩阵。显卡会用实例化 (Instancing) 技术,一瞬间把它们全部画出来。【实例化渲染 :相同螺栓共享几何数据,只存储变换矩阵】


**BVH (Bounding Volume Hierarchy)**一种基于物体空间划分的树形数据结构。与八叉树(Octree)划分空间不同,BVH 是通过包围盒层层包裹物体。在 CAD 领域,它是处理动态物体和复杂射线检测的首选方案。

**SAH (Surface Area Heuristic)**表面积启发式算法。在构建 BVH 树时,用于决定在何处进行切割。其核心思想是:射线碰到一个包围盒的概率与其表面积成正比。通过最小化各子节点的“表面积 × 物体数量”之和,可以大幅提升查询效率。

Möller-Trumbore 算法一种极其高效的射线-三角形相交检测算法。它不需要预先计算三角形所在的平面方程,直接利用重心坐标系(Barycentric Coordinates)进行线性代数运算,是高性能拾取引擎的标配。

**Jig (动态输入/拖拽反馈)**在 AutoCAD 开发中,Jig 是指在用户移动鼠标时,图形实时跟随并更新状态的交互模式。这要求渲染引擎具备极高的刷新率和低延迟的几何重算能力,通常需要与“临时图形缓存”配合使用。

**干涉检测 (Interference Detection)**工业设计中的高级需求。利用 BVH 树,开发者可以快速检测两个复杂 B-Rep 实体(如两个旋转的齿轮)是否在空间上发生了重叠。这通常涉及“粗略阶段(包围盒碰撞)”和“精确阶段(三角面片或 NURBS 求交)”的双重过滤。


给开发者的“番外”笔记

当你真正着手实现 Bolt 类的拾取时,你会发现一个有趣的现象: 如果你直接去点那个细碎的**螺纹 (Thread)**,由于三角形太小,很容易点空。

工业级的做法是:给螺栓做一个不可见的简化包围体(Proxy Geometry)。用户点击时,我们先对这个平滑的圆柱体做射线检测;只有当用户明确要求“精确分析”时,我们才去动用昂贵的计算资源去和那几万个螺纹面片较劲。

这就是 CAD 软件“看似流畅”背后的欺骗艺术——用简单的数学模型换取极致的用户体验。


  • 如果想了解一些成像系统、图像、人眼、颜色等等的小知识,快去看看视频吧  :

  • 认准一个头像,保你不迷路:在这里插入图片描述

  • 抖音:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传

  • 快手:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传

  • B站:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传

  • 您要是也想站在文章开头的巨人的肩膀啦,可以动动您发财的小指头,然后把您的想要展现的名称和公开信息发我,这些信息会跟随每篇文章,屹立在文章的顶部哦在这里插入图片描述