Three.js: 功能请求:通过空间搜索树提高 raycaster 碰撞检测性能

创建于 2017-12-11  ·  36评论  ·  资料来源: mrdoob/three.js

我已经构建了一个类似于WebVR-Vive-Dragging的 VR 应用程序,它允许使用 VR 控制器与众多 3d 对象进行交互。 这意味着用户可以使用 VR 控制器抓取对象并可以移动或缩放它。

问题:当场景中有复杂的 3d 对象时,即THREE.Mesh对象的几何形状具有非常多的顶点,那么碰撞检测期间的光线投射会变得非常缓慢。 因此,问题在于一个对象的几何形状的复杂性。

有用于快速空间搜索的树数据结构,例如OctreeR-Tree 。 我发现threeocttree允许将几何体分割成更小的块,但它似乎有点过时了(Three.js r60)。

据我所知,在THREE.Mesh对象的raycast方法中已经有一些性能优化(在进行实际光线投射之前首先检查边界框和球体)。 也许使用空间搜索树进行另一个这样的检查阶段是有意义的?! 你怎么看?

亲切的问候

Enhancement

最有用的评论

过去我曾建议将空间索引包含在 threejs 中。 在这个方向上已经做了一些工作,现有的八叉树示例就是其中之一。 最终,我认为核心社区对于将这个功能作为三等公民缺乏兴趣。

如果有足够的兴趣和意图将其包含在主要发行版中(不是示例),我很乐意将我的 BVH 代码捐赠给该项目。
我的代码侧重于两个方面:

  • 动态场景

    • 快速插入

    • 快速删除

    • 改装(节点无需重新插入即可改变形状的能力)

  • 查询速度

我有 2 个实现:

  • 二进制BVH

    • 非常紧凑,使用 ByteBuffer

    • 通过 DataView 混合 UintX 和 FloatX 类型

    • 非常适合序列化

    • 可以使用 lzma 库等标准工具进行压缩

    • 不可变

    • 构建速度高度优化

    • 仅适用于 BufferGeometry

    • SAH 优化

    • 射线查询

  • BVH

    • 对象树,因此比二进制实现大得多

    • 可变的

    • 快速批量插入

    • 快速单项插入

    • 改装

    • 堆栈、递归和无堆栈遍历实现(不同的性能特征)

    • SAH 优化

    • 平截头体查询

    • 射线查询

就个人而言,我不能没有空间索引,这是 n^2 和 log(n) 性能之间的差异。 例如,在我当前的项目中,以下部分依赖于空间索引:

  • 树叶系统(树木、花卉、灌木等)
  • 所有对象放置(角色和房屋)
  • 拾取(用于交互,例如鼠标点击)

http://server1.lazy-kitty.com/komrade

地形有大约 1m 的多边形,在上面放置数千棵树并运行实时光线投射简直是不行的,尤其是对于低端设备。

所有36条评论

冒着说明显而易见的风险,但是您是否尝试过已经在 repo 中的threeocttree版本( examples/js/Octree.js )?

我不赞成在three.js中硬连线地使用空间搜索树。 这种算法的开销/复杂性超过了许多应用程序中的性能增益。

在进行实际光线投射之前先检查边界框和球体

这是合理的,应该足够了。 如果用户在更高级的用例环境中需要更高的性能,他们可以使用上述示例作为起点。 八叉树可能是一个不错的选择。 但我从未在交互式 3D 应用程序中看到过 R-Tree 解决方案,因为它的方法非常复杂(算法执行数据而不是空间分区)。

您好,感谢您的回复,
@Mugen87我同意许多用例不需要如此快速的搜索。 但是,具有与 3D 对象交互的机制(我猜这就是它最常用的情况), THREE.Raycaster的步骤是首先检查边界框/球体(我完全同意这是合理的) ) 假设在进行实际光线投射时,线性时间工作量相当大。 我可以想象一种“按几何搜索树”而不是一个全局搜索树。 只要几何不改变(通常变换比几何改变更有可能),就不需要更新搜索树。

这就是为什么我认为,这种使用空间搜索树的优化可能是three.js更不可或缺的一部分。 但我也明白这会导致额外的复杂性和开销。

@moraxy我将看看示例版本。 我只是想知道这样的功能是否有意义。

再次感谢和亲切的问候

也许可以将更简单的搜索树集成到THREE.BufferGeometry中。 例如,对computeBoundingBox的调用也可以构建一种引用相应顶点的只读搜索树。 因为THREE.BufferGeometry

最适合静态对象,在实例化后不需要太多操作几何图形

此搜索树在初始化后不需要更改。 这将减少更新/删除的一些开销。 八叉树将是一个很好的起点( BabylonJS 中的类似概念)。

最适合静态对象,在实例化后不需要太多操作几何图形

FWIW,我不相信这是一个真实的说法。

@WestLangley引用来自THREE.BufferGeometry文档

三.BufferGeometry 文档

它可能应该被删除——无论如何,我不认为这意味着任何类型的技术声明,而是意味着用户在创建几何图形后更难操作它。

它可能应该被删除

明确地。

我刚刚在 PlaneBufferGeometry 上实现了一种更好的光线投射方法。 我使用 far 参数并找到第一个和最后一个索引位置。 这适用于平面缓冲区,因为索引数组是 x/y 顺序。 一旦超出 x/y 边界框,所有碰撞都必须在远距离范围之外。 有没有想在某个地方犯下这个的愿望? 当前的实现特定于我的用例,但如果需要,我愿意尝试制作更通用的。 我能够在大型平面缓冲区(2.5 秒到 10 毫秒)上显着降低我的光线投射性能

@kpetrow我认为three.js 八叉树示例就足够了。 如果您觉得对他人有用,您当然可以在 GitHub 上自由分享您的代码。

过去我曾建议将空间索引包含在 threejs 中。 在这个方向上已经做了一些工作,现有的八叉树示例就是其中之一。 最终,我认为核心社区对于将这个功能作为三等公民缺乏兴趣。

如果有足够的兴趣和意图将其包含在主要发行版中(不是示例),我很乐意将我的 BVH 代码捐赠给该项目。
我的代码侧重于两个方面:

  • 动态场景

    • 快速插入

    • 快速删除

    • 改装(节点无需重新插入即可改变形状的能力)

  • 查询速度

我有 2 个实现:

  • 二进制BVH

    • 非常紧凑,使用 ByteBuffer

    • 通过 DataView 混合 UintX 和 FloatX 类型

    • 非常适合序列化

    • 可以使用 lzma 库等标准工具进行压缩

    • 不可变

    • 构建速度高度优化

    • 仅适用于 BufferGeometry

    • SAH 优化

    • 射线查询

  • BVH

    • 对象树,因此比二进制实现大得多

    • 可变的

    • 快速批量插入

    • 快速单项插入

    • 改装

    • 堆栈、递归和无堆栈遍历实现(不同的性能特征)

    • SAH 优化

    • 平截头体查询

    • 射线查询

就个人而言,我不能没有空间索引,这是 n^2 和 log(n) 性能之间的差异。 例如,在我当前的项目中,以下部分依赖于空间索引:

  • 树叶系统(树木、花卉、灌木等)
  • 所有对象放置(角色和房屋)
  • 拾取(用于交互,例如鼠标点击)

http://server1.lazy-kitty.com/komrade

地形有大约 1m 的多边形,在上面放置数千棵树并运行实时光线投射简直是不行的,尤其是对于低端设备。

我想看到任何使光线投射优化的东西。 在不增加复杂性的情况下是否有优化空间? Octree 需要所有新库,以及所有新添加和更新等。

我注意到的一件事是,一旦将几何图形转换为索引缓冲区数组,它就不再巧妙地使用原始几何图形的属性。 如上所述,平面缓冲区几何可以通过知道 arrayBuffer 来自 PlaneGeometry 来进行超级优化。 也许覆盖 MESH.raycast ( raycaster, intersects ) 方法以基于几何类型?

另一种方法是将 Raycaster 作为一个单独的插件——就像交互式控件和加载器一样。 然后,添加一个类似于@Usnul建议的高性能光线投射器会很棒。

我喜欢拥有可插拔 Raycaster 的想法,但我认为这有点混乱。 我认为有用的是空间索引,而不是 Raycaster 本身。 空间索引允许以下内容:

  • 空间剔除(想想平截头体剔除)
  • 进行可见性查询
  • 构建路径跟踪渲染器
  • 快速排序(利用数据局部性)

目前,three.js 明确执行其中 2 项(排序和剔除),并通过示例执行 1 项(光线跟踪渲染器)

在内部维护空间索引将加速排序和剔除,以存储索引所需的额外 RAM 为代价,逐渐为更大的场景带来更多好处。

为什么不直接使用 GPU 拾取? 周围有一些例子,它很容易实现。 表演是白天和黑夜。 在我们的用例中,拥有超过一百万个多边形的模型,选择从近一秒到以 60fps 的速度实时运行,同时将物体拖到模型上。

https://github.com/brianxu/GPUPicker

@hccampos
GPU 选择不一定比在 CPU 上更快,对于 GPU,您必须以与所需精度成比例的分辨率进行渲染,因此如果您想要像素精度 - 您必须以 1:1 的屏幕分辨率进行渲染。 请记住,这是一个单独的渲染通道,您将对象渲染为单独的不同颜色。 额外的渲染通道意味着图形内存使用(典型的延迟管道)。
使用二进制空间分区索引可为您提供 log(n) 时间配置文件,因此对于 1,000,000 个多边形,您将查看大约 14 个操作来解决射线查询。 根据您的数据结构 - 可能需要几微秒,在您开始减少帧预算之前允许您进行数千次查询。

让我们做一个比较,假设您有一个 1m 的多边形模型,并且您希望将一条光线从屏幕空间直接沿着相机方向投射到场景中(选择用例)。 假设您使用桶底分辨率“全高清”或 1920 × 1080。假设您仅渲染 RGB(每像素 24 位),您将需要 6220800 字节(约 6 Mb)或图形 RAM 来渲染那。 如果您使用 CPU 解决方案,假设您使用 AABB BVH,每个叶子有 10 个多边形,这意味着您需要大约 200,000 个节点,假设每个节点大约 6 * 8 字节的坐标,中间节点有额外的 2 * 4 字节的子指针叶节点有 10 * 4 字节的多边形指针,即 14,400,000 字节(约 14Mb)。 当您考虑到与 GPU 情况相比,您的 BVH 查询对 RAM 带宽的需求非常小,并且与渲染完整的 1m 多边形几何体相比,每个查询只涉及少数操作时,主要区别就会发挥作用。
如果您采用更典型的桌面分辨率,例如 2560x1440,您最终会得到 11059200 字节的渲染目标 (11Mb)。

如果您有大量的图形内存带宽预算和相当不错的着色器核心数量 - 当然,这是一种简单直接的选择方式。

@Usnul绝对是,但它可能是最简单的实现解决方案。 实现和维护空间索引数据结构对于threejs 维护者来说可能是一个不小的负担。

话虽如此,我绝对希望拥有一个良好的、经过良好测试和维护的空间索引数据结构,作为三者的一部分或作为单独的 npm 包。

我很好奇为我使用的几何图形实现这样的空间索引,所以我实现了一个非常简单的八叉树(仅创建和搜索),它拆分了我的 BufferGeomtry。 通过这个简单的解决方案,我取得了非常有希望的结果:应用于具有大约 500K 顶点的几何体,光线投射时间从 ~120ms 减少到 ~2.3ms。 树的构建目前需要大约 500 毫秒,但由于几何和树的创建只在应用程序启动时在 Web Worker 内完成一次,所以这不是一个问题。

我猜该算法可以很容易地集成到THREE.BufferGeometry中,并且可以使用诸如THREE.BufferAttributedynamic之类的标志来打开或关闭。 这样,它只能在 BufferGeometry 不应该更改时使用。 不幸的是,我不得不覆盖THREE.Meshraycast方法,尽管我只需要更改其中的两行(八叉树搜索调用和位置数组迭代)。

无论如何,在我的 VR 应用程序中,我必须在渲染循环期间检测对象控制器冲突,而不仅仅是在鼠标单击时检测一次。 因此,我必须依靠我目前的解决方案。 如果我可以进一步改进它,我会尝试。

更新我在测量光线投射时间时犯了一个错误。 正确的值是~2.3ms(而不是~0.3ms)。 我相应地更改了上面的值。

我认为在基于 three.js 构建的大多数更复杂的应用程序中,需要一种选择加入的方式来进行快速空间查找。 例如,3D 编辑器。 我已经玩弄了一些实现,但是它们要么不能很好地集成,要么过于紧密地包装到特定版本的 three.js 中。 因此,如果有人可以选择加入而不破坏更新版本的three.js 的升级路径,那就太好了。

@matthias-w 我为 obj 模型尝试了 raycaster,但控制器没有检测到任何东西。 光线穿过模型。 你能告诉我你是怎么解决这个问题的吗

关于具有可选空间索引的主题。 我相信即使渲染器本身(例如:剔除)成为引擎的一个组成部分,它也足够有用。 这是一个相关的讨论:

13909

嘿! 我对此也有点兴趣(尽管我后来找到了其他解决方案来满足我们的光线投射需求),但我想我也会贡献一些我的实验。 这有点粗糙,但几个月前我把它放在一起:

https://github.com/gkjohnson/threejs-fast-raycast

它将computeBoundsTree函数添加到三个几何对象并覆盖 raycast 函数以使用它(并且仅返回第一个命中作为添加的优化)。 它只对静态的、高度复杂的网格真正有益,并且不会对场景进行空间分割。 这是演示,您可以在其中看到 80,000 个三角形网格上的光线投射性能差异。 计算边界树有点慢,但稍加工作可能会更平滑。

对于我的看法,我觉得有点矛盾。 这感觉就像是可以很好地构建为 THREE 的扩展。 最终,无论如何,对复杂或动画网格进行每个三角形检查/碰撞/投射似乎并不是最佳的。 在简单的情况下可能很好,但似乎使用典型的平面/立方体/球体/胶囊表示或简化的网格更适合在复杂情况下启用超快速光线投射(或遮挡剔除、碰撞等)。 当然,如果您正在寻找像素完美的演员表,这并不好用,但正确的解决方案确实取决于您的用例。

@Usnul @matthias-w 您的 octree / BVH 实现是开源的还是在线可用的? 我肯定有兴趣看看!

@gkjohnson

您的 octree / BVH 实现是开源的还是在线可用的? 我肯定有兴趣看看!

该代码目前不是开源的。 如果您私下联系我,我可以为您提供消息来源。

我为实例化网格做了一个例子:
http://server1.lazy-kitty.com/tests/instanced-foliage-1mil/
上面的例子包括以下内容:

  • BVH 在新实例插入树时动态更新
  • BVH 在运行中使用深度 1-2 旋转进行增量优化
  • BVH 使用平截头体查询进行采样,要渲染到哪些实例

我在我正在开发的游戏中运行了它的一个版本:
http://server1.lazy-kitty.com/komrade/

关于您的实施。 我喜欢它,我的在两个主要方面有所不同:

  • 我专注于表现
  • 我专注于内存占用
  • 我避免在很多地方收集垃圾

一些更具体的点:

  • 我还根据您想要对 BVH 执行的操作,透明地使用混合拆分策略
  • 我的 BVH 基于 AABB
  • 我支持通过改装更新节点边界

@Usnul我看不到您的电子邮件地址或任何东西,但我对研究您的空间索引解决方案非常感兴趣。

目前我只是遍历所有相关的场景对象并计算与相机的距离。 不是最优化的方法。

@titansoftime
它是
travnick at gmail com
没有意识到它不是公开的。 我的错。

@Usnul我也对 100 万个项目感兴趣。 如果可能的话,甚至可能是 10 或 2000 万。 请给我发电子邮件:kaori.nakamoto。 [email protected]

非常感谢!

回复晚了非常抱歉。
@gkjohnson代码不是开源的。 此外,您的解决方案听起来比我的要复杂得多。 因此,我想从我的解决方案中学到的东西并不多。

@sid3007我不确定我是否理解你的问题。 我可以解释我的方法。 也许它是有帮助的。

我的用例非常简单。 当我的应用程序启动时,会加载不同模型的几何图形。 这些模型可以由用户转换。 几何形状不会改变。 没有几何变形。 因此,我实现了一个非常简单的八叉树,它在应用程序启动时为每个几何图形构建一次。 八叉树不是动态的。 它是基于给定几何的边界框和顶点数组构建的。 在其构造过程中,它检查八分圆是否包含顶点或八分圆的Box3边界框是否与三角形(非索引几何中的三个连续顶点)相交,并将顶点数组引用与八叉树的节点一起存储。
然后将每个网格的八叉树与网格一起存储。 我还覆盖了我的网格实例的THREE.Meshraycast方法(仅一行),以便实际调用八叉树交集测试方法。 这将返回网格的默认相交逻辑可以使用的几何体面顶点索引。
我做了一个优化:八叉树的创建是在应用程序启动时在 WebWorker(实际上是一个工作池)内完成的。 原因是大型几何图形的树创建需要相当长的时间(几秒钟)。 这会阻止浏览器 UI,因此我将其移至另一个线程。
我希望我的方法变得清晰。

@matthias-w 听起来很棒! 我独立完成了几乎相同的工作,但我认为我的性能提升几乎没有那么大。 https://discourse.threejs.org/t/octree-injection-for-faster-raytracing/8291/2 (面向对象的八叉树实现,对Mesh.raycast的修改较少)

您是否有可能开源/贡献 Octree 实现,允许其他人进行更多实验?

@EliasHasle谢谢。 实际上,性能提升并不是那么好,因为我在第一次测量时犯了一个错误。 我更正了这个值(见我上面的帖子)。 考虑到 Octree 的对数搜索复杂性,现在的时间安排更有意义。 不幸的是,我目前无法提供代码,但可能会在今年晚些时候提供。 无论如何,我的实现非常简单(而且不是那么干净 BTW ;))。 所以我想不会有任何特别的见解。

@matthias-w 我认为 500k 顶点网格上的 raycast 的 2.3 ms 与 120 ms 仍然是一个显着的改进,例如,可以在游戏中实时解析发射的子弹(在相当大的部分计算中)每帧预算)。

你也尝试过光线追踪吗?

您的实现是否基于 JS 对象树? 文字对象或原型实例? 我的是完全自相似的,所以每个节点都是一个八叉树,有方法和所有。

@EliasHasle

如果您对针对静态几何体的光线投射感兴趣,我和其他人已经在三网格 bvh上投入了大量精力,它使用 BVH 来索引三角形并允许高性能光线投射以及针对静态复杂几何体的相交检测。 树的构建过程更复杂,因此需要更长的时间,但在对具有数十万个三角形的几何体进行光线投射时,需要将光线投射降低到一毫秒以下,通常不到 0.1 毫秒。

可以通过几种不同的方式改进构建时间,但对于我想要用它做的事情来说已经足够好了——不过,改进已经在列表中了。

我想构建一个基于动态场景的八叉树,以便为具有大量网格的场景提供更好的光线投射和碰撞检测,但我自己没有一个好的用例。 也许有一天!

@EliasHasle我的实现是一棵JS 类对象树(我使用ES6 类,即原型实例)。 基本上,节点是我的实现中的一棵树。 但是,我有一个额外的八叉树类,它保存根节点并提供构建和搜索(以及一些调试和可视化)树的方法。

我没有对最大树节点级别和每个叶节点的最大顶点数的值进行太多实验。 也许有更好的组合。
我还使用包围体层次结构来加速相交测试。 所以我在实际检查网格几何的八叉树之前测试了网格的边界球和边界框。

我可以推荐一本很好的碰撞检测方法合集的书:C. Ericson,Real-Time Collision Detection,CRC Press,2004

@gkjohnson看起来很棒! 表演令人印象深刻。 您使用的是哪种 BVH?

@matthias-w 谢谢!

有几个拆分策略选项,但在最长边的中心拆分 BVH 节点可以最快地构建树。 三角形根据边界的中心划分为边,树节点被扩展以完全包含子三角形(因此树节点有少量重叠)。

@gkjohnson那么BVH实现是一种不平衡的Kd树,其轴选择取决于框的形状,对吗? 如果是这样,我正在考虑在根 BB 离立方体太远的情况下做类似的事情,然后在条件分割后使用八叉树。 这大概适用于@Usnul的地图世界等情况,其中初始拆分将在两个“地图”维度中进行,随后的拆分将在 3D 中进行。 我觉得比我的方案好很多,就是把根BB展开成同心的bounding cube,然后一路使用八叉树分裂。

我刚刚在 PlaneBufferGeometry 上实现了一种更好的光线投射方法。

@kpetrow我认为在PlaneBufferGeometry上优化光线投射,顶点位置不会改变的假设对于线性搜索不足的高分辨率几何图形很可能无效。 据我所知,高分辨率PlaneBufferGeometry的主要用途是通过移动顶点来重塑它,同时保持拓扑结构,例如构建地形。

以#13909 结束。

此页面是否有帮助?
0 / 5 - 0 等级