-
故事背景:点击的瞬间,发生了什么?
-
第一步:射向虚拟世界的“激光束” (Ray Casting)
-
换个视角:射线求交算法
-
第二步:给场景套上“俄罗斯套娃” (BVH)
-
换个视角:BVH 树结构
-
第三步:比拾取更高级的“磁吸术” (Snapping)
-
第四步:海量实体的“性能屏颈”与“分身术”
-
给开发者的“番外”笔记
代码仓库入口:
-
github源码地址。(github.com/AIminminAI/…
-
gitee源码地址。(gitee.com/aiminminai/…
系列文章规划:
-
(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,边界体积层次结构)**。 你不再傻乎乎地去数每一个三角形,而是把物体装进一个个“盒子”里。
-
大盒子包小盒子:你给整个场景算一个巨大的包围盒(AABB)。然后把它切成两半,每半再装进中盒子,直到最后的小盒子只装几个三角形。
-
树状搜索:当射线射入时,如果连“大盒子”都没碰着,那里面成千上万个三角形直接“Pass”。如果碰到了大盒子,再去看它里面的两个中盒子……
-
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)而罢工。
你采用了两手准备:
-
空间索引优化:利用你建好的 BVH 或四叉树。渲染时,先做“视锥体剔除”——不在屏幕里的盒子,连送都不送给显卡。BVH 空间索引 :将时间复杂度降至 O(log n) ,10 万个对象只需约 17 次遍历
-
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站:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
-
您要是也想站在文章开头的巨人的肩膀啦,可以动动您发财的小指头,然后把您的想要展现的名称和公开信息发我,这些信息会跟随每篇文章,屹立在文章的顶部哦