Three.js中的Raycast仅使用投影矩阵

14

我正在使用Three.js在Mapbox GL 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全是NaN,我们可以通过计算来解决这个问题:

  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}, ... },
]

很明显这里出了问题。查看setCamera代码,它涉及到projectionMatrixmatrixWorld。我能否设置matrixWorld或者直接使用投影矩阵构造射线?似乎只需要投影矩阵就可以渲染场景,所以我希望只需要投影矩阵就可以进行射线投射。

完整示例请参见此CodePen


@Barthy我尝试在codepen中添加this.camera.updateMatrixWorld(true);,但没有用。行为相同,这样做代替或额外添加this.camera.projectionMatrixInverse一行。 - danvk
特别提醒@Barthy,调用updateMatrixWorld前后,this.camera.matrixWorld都是单位矩阵。 - danvk
@danvk你解决了吗?你能看一下我的类似问题吗?https://stackoverflow.com/questions/61656054/getting-uneven-click-detection-on-3d-objects-on-mapbox-with-raycaster - abhishek ranjan
1
@abhishekranjan 下面的答案包括一个完全功能的JS Fiddle解决方案。如果您喜欢它,请点赞。我晚上也会看看您的问题。 - Isolin
@Isolin 谢谢,这是一个类似的问题,但不完全相同。当我点击我的3D对象底部时,数组保持为空,但在3D对象的特定点(随机)上,点击会产生非空数组。此外,当我靠近3D对象但没有点击对象本身时,也会发生上述不均匀的点击检测。我真的很感激任何形式的帮助。谢谢。 - abhishek ranjan
显示剩余2条评论
1个回答

11

难以调试,但已解决!

TL;DR:在此可查看完整代码 Fiddle

  1. 我认为问题并不在于没有MapBox相机的世界矩阵,而是坐标空间的不兼容。Mapbox采用左手系,z指向上方。而Three使用右手系,y指向上方。

  2. 在调试过程中,我创建了射线投射器设置函数的简化副本,以便完全掌控一切,这得到了回报。

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

坐标系

  • BoxCustomLayer构造函数中没有必要更改DefaultUp,因为目标是使所有内容与Three中的默认坐标对齐。
  • 您在onAdd()方法中翻转y轴,但然后在初始化组时也缩放了所有对象。这样,在翻转投影后操作的坐标不再是以米为单位的。因此,让我们将缩放部分与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);
  1. 确保从 makeScene 中的组中移除缩放,实际上你不再需要这个组了。

  2. 无需修改 render 函数。

射线投射

这里有点棘手,基本上就是 Raycaster 要做的事情,但我省略了一些不必要的函数调用,例如相机世界矩阵为单位矩阵 => 不需要与之相乘。

  1. 获取投影矩阵的逆矩阵。它在 Object3D.projectionMatrix 赋值时不会更新,因此让我们手动计算它。
  2. 使用它来获取相机和鼠标在 3D 空间中的位置。这相当于 Vector3.unproject
  3. viewDirection 就是从 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);
  1. 其他部分保持不变。

完整代码

演示用Fiddle链接

  • 我添加了 mousemove 以使用 jQuery 实时显示调试信息。
  • 有关点击的全部信息将记录在控制台中。

太棒了,感谢@isolin!不兼容的坐标系统真的很烦人。我相信DefaultUp的更改是由于材料和灯光问题而产生的,我已经在这个小例子中删除了它们。(我不得不将纹理翻转到内部才能显示它们。)我会看看是否能够重现这个问题。但无论如何,非常感谢您的解决方案。确实“难以调试”! - danvk
@Isolin谢谢你的回答!但是我还没有完全理解它是如何工作的,你能解释一下吗?Raycaster.setFromCamera方法用于PerspectiveCamera,其工作原理如下:
  • 它将相机的位置(相机的matrixWorld的位置组件)作为起点
  • 对于方向,它使用相机的投影矩阵(和matrixWorld)将传递的鼠标坐标反投影到3D空间中。
由于在我们的代码中matrixWorld是恒等的,因此与您的答案唯一的区别在于您也对起点进行了反投影。我不明白这是为什么需要的。
- Dmytro Marchuk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接