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
都是NaN
s,我们可以通过计算来修复:
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
,它涉及projectionMatrix
和matrixWorld
。有没有办法可以设置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.unproject
。
viewDirection
只是从 cameraPosition
到 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();
-
设置光线投射器光线
this.raycaster.set(cameraPosition, viewDirection);
-
其余的可以保持不变。
完整代码
Fiddle 用于演示。
我添加了mousemove
以使用 jQuery 显示实时调试信息。
点击后,有关点击的完整信息会记录到控制台。
【讨论】:
太棒了,谢谢@isolin!不兼容的坐标系真的很烦人。我相信DefaultUp
的变化来自于我为这个小提琴移除的材料和灯光问题。 (我必须将纹理从里到外翻转才能显示出来。)我会看看我是否可以重现它。但无论如何,非常感谢您的解决方案。确实“难以调试”!
@Isolin 感谢您的回答!但是,我还没有完全理解它是如何工作的,你能解释一下吗? PerspectiveCamera 的 Raycaster.setFromCamera
方法的工作原理如下: - 它选择原点作为相机的位置(相机的 matrixWorld
的位置分量) - 对于方向,它使用相机的 projectionMatrix(和 matrixWorld)将传递的鼠标坐标取消投影到 3d 空间)。由于 matrixWorld 是我们代码中的标识,因此与您的答案的唯一区别是您也取消了原点的投影。我不明白为什么需要这样做。以上是关于Three.js 中的 Raycast,只有一个投影矩阵的主要内容,如果未能解决你的问题,请参考以下文章