Three.js 中的 Raycast 只有一个投影矩阵 [英] Raycast in Three.js with only a projection matrix

查看:35
本文介绍了Three.js 中的 Raycast 只有一个投影矩阵的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在 Mapbox GL JS 页面中使用 Three.js 渲染一些自定义图层,遵循

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

 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(鼠标,this.camera);console.log(raycaster.intersectObjects(this.scene.children, true));}

这给了我一个例外:

THREE.Raycaster:不支持的相机类型.

我可以在不影响场景渲染的情况下从通用的 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(鼠标,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,或仅使用投影矩阵直接构建光线投射器的光线?似乎我只需要投影矩阵来渲染场景,所以我希望它也是我投射光线所需的全部.

完整示例在此代码笔中.

解决方案

难调试,但解决了!

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

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

  2. 在调试过程中,我创建了一个精简版的 raycaster 设置函数,让一切尽在掌控之中,并且得到了回报.

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

坐标系

  • 无需更改 BoxCustomLayer 构造函数中的 DefaultUp,因为目标是使所有内容与三中的默认坐标对齐.
  • 您在 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 旋转 = 新的 THREE.Matrix4().multiplyMatrices(new THREE.Matrix4().makeRotationX(-0.5 * Math.PI),新的 THREE.Matrix4().makeRotationY(Math.PI));//可选的Y旋转this.cameraTransform = new THREE.Matrix4().multiplyMatrices(缩放,旋转).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 =新的 THREE.Vector3().applyMatrix4(camInverseProjection);常量鼠标位置 =new THREE.Vector3(mouse.x, mouse.y, 1).applyMatrix4(camInverseProjection);const viewDirection = mousePosition.clone().sub(cameraPosition).normalize();

  1. 设置光线投射器光线

this.raycaster.set(cameraPosition, viewDirection);

  1. 其余的可以保持不变.

完整代码

Fiddle 用于演示.

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

I'm rendering some custom layers using Three.js in a Mapbox GL JS page following this example. I'd like to add raycasting to determine which object a user has clicked on.

The issue is that I only get a projection matrix from Mapbox, which I use to render the scene:

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);
  }
}

This renders just great, and tracks changes in view when I pan/rotate/zoom the map.

Unfortunately, when I try to add raycasting I get an error:

  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));
  }

This gives me an exception:

THREE.Raycaster: Unsupported camera type.

I can change from a generic THREE.Camera to a THREE.PerspectiveCamera without affecting the rendering of the scene:

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

This fixes the exception but also doesn't result in any objects being logged. Digging a bit reveals that the camera's projectionMatrixInverse is all NaNs, which we can fix by calculating it:

  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));
  }

Now I get two intersections wherever I click, with two faces of the cube. Their distances are 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}, ... },
]

So clearly something isn't working here. Looking at the code for setCamera, it involves both projectionMatrix and matrixWorld. Is there a way I can set matrixWorld, or construct the raycaster's ray directly using only the projection matrix? It seems that I only need the projection matrix to render the scene, so I'd hope that it would also be all I need to cast a ray.

Full example in this codepen.

解决方案

Difficult to debug, but solved!

TL;DR: Full code in this working Fiddle.

  1. I think that not having the world matrix of the camera from MapBox is not the main problem, rather the incompatibility of the coordinate spaces. Mapbox delivers a left-handed system with z pointing up. Three uses a right-handed system with y pointing up.

  2. During the debugging I created a reduced copy of the raycaster setup functions to have everything under control and it paid off.

  3. A cube is not the best object for debugging, It is way too symmetric. The best are asymmetric primitives or compound objects. I prefer a 3D coordinate cross to see the axes orientation straight away.

Coordinate systems

  • There is no need for changing the DefaultUp in the BoxCustomLayer constructor, since the goals is to have everything aligned with the default coordinates in Three.
  • You flip the y-axis in the onAdd() method, but then you also scale all objects when initializing the group. This way, the coordinates you operate in after inverting the projection in raycast() are not in meters anymore. So let's join that scaling part with this.cameraTransform. The left-handed to right-handed conversion should be done here as well. I decided to flip the z-axis and rotate 90deg along x-axis to get a right-handed system with y pointing up:

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. Make sure to remove the scaling from the group in makeScene , in fact you don't need the group anymore.

  2. No need to touch the render function.

Raycasting

Here it gets a bit tricky, basically it's what Raycaster would do, but I left out some unnecessary function calls, e.g. the camera world matrix is identity => no need to multiply with it.

  1. Get the inverse of the projection matrix. It does not update when Object3D.projectionMatrix is assigned, so let's compute it manually.
  2. Use it to get the camera and mouse position in 3D. This is equivalent to Vector3.unproject.
  3. viewDirection is simply the normalized vector from the cameraPosition to the mousePosition.

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();

  1. Setup the raycaster ray

this.raycaster.set(cameraPosition, viewDirection);

  1. The rest can stay the same.

Full code

Fiddle for demonstration.

  • I added mousemove to display real-time debug info with jQuery.
  • The full info about the hit gets logged to the console upon click.

这篇关于Three.js 中的 Raycast 只有一个投影矩阵的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆