Three.js 中的 Raycast,只有一个投影矩阵

Posted

技术标签:

【中文标题】Three.js 中的 Raycast,只有一个投影矩阵【英文标题】:Raycast in Three.js with only a projection matrix 【发布时间】:2020-03-28 11:53:47 【问题描述】:

我在 this example 之后的 Mapbox GL JS 页面中使用 Three.js 渲染一些自定义层。我想添加光线投射来确定用户点击了哪个对象。

问题是我只从 Mapbox 获得了一个投影矩阵,我用它来渲染场​​景:

class CustomLayer 
  type = 'custom';
  renderingMode = '3d';

  onAdd(map, gl) 
    this.map = map;
    this.camera = new THREE.Camera();
    this.renderer = new THREE.WebGLRenderer(
      canvas: map.getCanvas(),
      context: gl,
      antialias: true,
    );
    this.scene = new THREE.Scene();
    // ...
  

  render(gl, matrix) 
    this.camera.projectionMatrix = new THREE.Matrix4()
      .fromArray(matrix)
      .multiply(this.cameraTransform);
    this.renderer.state.reset();
    this.renderer.render(this.scene, this.camera);
  

这渲染得非常好,并在我平移/旋转/缩放地图时跟踪视图的变化。

不幸的是,当我尝试添加光线投射时出现错误:

  raycast(point) 
    var mouse = new THREE.Vector2();
    mouse.x = ( point.x / this.map.transform.width ) * 2 - 1;
    mouse.y = 1 - ( point.y / this.map.transform.height ) * 2;
    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, this.camera);
    console.log(raycaster.intersectObjects(this.scene.children, true));
  

这给了我一个例外:

THREE.Raycaster: Unsupported camera type.

我可以从通用的THREE.Camera 更改为THREE.PerspectiveCamera 而不会影响场景的渲染:

this.camera = new THREE.PerspectiveCamera(28, window.innerWidth / window.innerHeight, 0.1, 1e6);

这修复了异常,但也不会导致任何对象被记录。稍微挖掘一下发现相机的projectionMatrixInverse都是NaNs,我们可以通过计算来修复:

  raycast(point) 
    var mouse = new THREE.Vector2();
    mouse.x = ( point.x / this.map.transform.width ) * 2 - 1;
    mouse.y = 1 - ( point.y / this.map.transform.height ) * 2;
    this.camera.projectionMatrixInverse.getInverse(this.camera.projectionMatrix);  // <--
    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, this.camera);
    console.log(raycaster.intersectObjects(this.scene.children, true));
  

现在,无论我点击哪里,我都会得到两个交叉点,以及立方体的两个面。它们的距离为 0:

[
   distance: 0, faceIndex: 10, point: Vector3  x: 0, y: 0, z: 0 , uv: Vector2 x: 0.5, y: 0.5, ... ,
   distance: 0, faceIndex: 11, point: Vector3  x: 0, y: 0, z: 0 , uv: Vector2 x: 0.5, y: 0.5, ... ,
]

很明显有些东西在这里不起作用。查看code for setCamera,它涉及projectionMatrixmatrixWorld。有没有办法可以设置matrixWorld,或者只使用投影矩阵直接构造光线投射器的光线?看来我只需要投影矩阵来渲染场景,所以我希望它也是我投射光线所需要的。

完整示例in this codepen。

【问题讨论】:

"有什么方法可以设置matrixWorld"你试过updateMatrixWorld()吗?所有的相机也是 Object3Ds... @Barthy 我尝试在 codepen 中添加 this.camera.updateMatrixWorld(true); 来代替 this.camera.projectionMatrixInverse 行,但无济于事。同样的行为。 @Barthy 具体来说,this.camera.matrixWorld 是调用updateMatrixWorld 之前和之后的单位矩阵。 还有一些关于这个 mapbox-gl 问题的有趣材料github.com/mapbox/mapbox-gl-js/issues/7395 @abhishekranjan 下面的答案是解决方案,包括。一个功能齐全的 JS Fiddle。如果喜欢,请点赞。晚上我也会看看你的问题。 【参考方案1】:

难以调试,但已解决!

TL;DR:此工作Fiddle 中的完整代码。

    我认为没有来自 MapBox 的相机世界矩阵不是主要问题,而是坐标空间的不兼容。 Mapbox 提供了一个左手系统,z 指向上方。三使用右手系统,y 指向上方。

    在调试过程中,我创建了 raycaster 设置函数的简化副本,以控制一切,并获得了回报。

    立方体不是调试的最佳对象,它过于对称。最好的是不对称的基元或复合对象。我更喜欢使用 3D 坐标交叉来直接查看坐标轴方向。

坐标系

无需更改 BoxCustomLayer 构造函数中的 DefaultUp,因为目标是使所有内容都与 Three 中的默认坐标对齐。 您在 onAdd() 方法中翻转 y 轴,但随后在初始化组时也缩放所有对象。这样,您在 raycast() 中反转投影后操作的坐标不再以米为单位。因此,让我们将缩放部分与this.cameraTransform 结合起来。左手到右手的转换也应该在这里完成。我决定翻转 z 轴并沿 x 轴旋转 90 度以获得 y 指向上方的右手系统:
const centerLngLat = map.getCenter();
this.center = MercatorCoordinate.fromLngLat(centerLngLat, 0);
const x, y, z = this.center;

const s = this.center.meterInMercatorCoordinateUnits();

const scale = new THREE.Matrix4().makeScale(s, s, -s);
const rotation = new THREE.Matrix4().multiplyMatrices(
        new THREE.Matrix4().makeRotationX(-0.5 * Math.PI),
        new THREE.Matrix4().makeRotationY(Math.PI)); //optional Y rotation

this.cameraTransform = new THREE.Matrix4()
        .multiplyMatrices(scale, rotation)
        .setPosition(x, y, z);

    确保从makeScene 中的组中删除缩放,实际上您不再需要该组了。

    无需触摸render函数。

光线投射

这里有点棘手,基本上是Raycaster 会做的,但我省略了一些不必要的函数调用,例如相机世界矩阵是恒等式 => 无需乘以它。

    获取投影矩阵的逆矩阵。分配Object3D.projectionMatrix 时它不会更新,所以让我们手动计算它。 使用它来获取 3D 中的相机和鼠标位置。这相当于Vector3.unprojectviewDirection 只是从 cameraPositionmousePosition 的归一化向量。
const camInverseProjection = 
        new THREE.Matrix4().getInverse(this.camera.projectionMatrix);
const cameraPosition =
        new THREE.Vector3().applyMatrix4(camInverseProjection);
const mousePosition =
        new THREE.Vector3(mouse.x, mouse.y, 1)
        .applyMatrix4(camInverseProjection);
const viewDirection = mousePosition.clone()
        .sub(cameraPosition).normalize();
    设置光线投射器光线
this.raycaster.set(cameraPosition, viewDirection);
    其余的可以保持不变。

完整代码

Fiddle 用于演示。

我添加了mousemove 以使用 jQuery 显示实时调试信息。 点击后,有关点击的完整信息会记录到控制台。

【讨论】:

太棒了,谢谢@isolin!不兼容的坐标系真的很烦人。我相信DefaultUp 的变化来自于我为这个小提琴移除的材料和灯光问题。 (我必须将纹理从里到外翻转才能显示出来。)我会看看我是否可以重现它。但无论如何,非常感谢您的解决方案。确实“难以调试”! @Isolin 感谢您的回答!但是,我还没有完全理解它是如何工作的,你能解释一下吗? PerspectiveCamera 的 Raycaster.setFromCamera 方法的工作原理如下: - 它选择原点作为相机的位置(相机的 matrixWorld 的位置分量) - 对于方向,它使用相机的 projectionMatrix(和 matrixWorld)将传递的鼠标坐标取消投影到 3d 空间)。由于 matrixWorld 是我们代码中的标识,因此与您的答案的唯一区别是您也取消了原点的投影。我不明白为什么需要这样做。

以上是关于Three.js 中的 Raycast,只有一个投影矩阵的主要内容,如果未能解决你的问题,请参考以下文章

Three.js:平面只有一半时间可见

Three.js 完全冻结 Chrome,GLTF 模型中的巨大纹理

three.js使用gpu选取物体并计算交点位置

Three.js raycaster 可以与组相交吗?

THREE.JS 2.0 场景及场景中的对象

如何在three.js中使用gltf模型投射阴影?