three.js+WebGL踩坑经验合集(2):3D场景被相机裁切后,被裁切的部分依然可以被鼠标碰撞检测得到(射线检测)

news/2025/1/31 17:52:09/

three.js内置了Raycaster类实现鼠标的碰撞检测,用它可以实现3D物体的鼠标点击,移入移出,触屏检测一类的业务功能。

该功能虽然强大,但同事们普遍反映不是那么好用,因为它不像其它配套了可视编辑的3D引擎一样,直接把这些交互事件挂载到3D物体上。

不好用没关系,只要研发团队有懂three.js的人,或者有人把写好的实现代码封装一下给到业务开发去用就可以了。

蛋疼的是,这个东西还有一些小bug,比如它不认相机裁剪。以下为测试用例代码

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>three_cameraNear</title><style>body {margin: 0;overflow: hidden;}</style><script src="three/build/three.js"></script><script src="three/examples/js/controls/OrbitControls.js"></script><script src="three/examples/js/libs/dat.gui.min.js"></script>
</head><body><script>var scene = new THREE.Scene();var geometry = new THREE.SphereGeometry(50, 100, 100);var srcColor = 0xFF6600;var material = new THREE.MeshLambertMaterial({color: srcColor});var mesh = new THREE.Mesh(geometry, material);scene.add(mesh);var light = new THREE.DirectionalLight({color: 0xFFFFFF, intensity: 0.5});light.position.set(-500, 500, 500);scene.add(light);var ambLight = new THREE.AmbientLight({color: 0xFFFFFF, intensity: 0.3});scene.add(ambLight);var width = window.innerWidth; var height = window.innerHeight; var camera = new THREE.PerspectiveCamera(60, width / height, 100, 20000);camera.position.set(0, 0, 150);  var renderer = new THREE.WebGLRenderer();renderer.setSize(width, height);renderer.setClearColor(0x000000, 1); document.body.appendChild(renderer.domElement); var gui = new dat.GUI(),folderCamera = gui.addFolder("相机"),propsCamera = {get '裁剪'() {return camera.near;},set '裁剪'( v ) {camera.near = v;camera.updateProjectionMatrix();},};folderCamera.add( propsCamera, '裁剪', 100, 150 );    folderCamera.open();function render() {renderer.render(scene, camera);requestAnimationFrame(render);}render();var controls = new THREE.OrbitControls(camera,renderer.domElement);controls.addEventListener('change', render);var raycaster = new THREE.Raycaster(); function onMouseMove(e){	//这里是屏幕坐标到ndc的转换,不懂的可以自行上webgl中文网学习var x = ((e.clientX - width * 0.5) / width * 2);var y = (-(e.clientY - height * 0.5) / height * 2);raycaster.setFromCamera(new THREE.Vector2(x, y), camera);var intersects = raycaster.intersectObject(scene, true);material.color = new THREE.Color().set(srcColor);for(let intersect of intersects){intersect.object.material.color = new THREE.Color().set(0x999900);}}window.addEventListener("mousemove", onMouseMove);</script>
</body>
</html>

场景上的球体在场景不被裁剪的时候工作得非常好,但是一旦用上了裁剪(设置camera的near/far,就会发现,Raycaster完全无视这一属性。

解决问题的时候,笔者还是先尝试去找现成的api,看是不是有设置项。果不其然,Raycaster就自带了near和far属性。

//在onMouseMove方法的intersects调用前加上这两行,同步相机的裁剪属性
raycaster.near = camera.near;
raycaster.far = camera.far;

这不是脱裤子放屁嘛,为什么就不直接在Raycaster内部去读取camera的这两条属性呢,非要业务层多此一举?

先不纠结这问题,我们看看这样写是否就能把问题解决掉。

有了显著改善,但是边缘处并不是那么准确,并且鼠标位置离画布中心越远,误差就越大。

笔者在解决这个问题之前,有大致了解过射线检测的原理,所以结合源码里的实现逻辑,盲猜到把相机改成正交就会立马变得非常准确。

var k = width / height;
var s = 100; 
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 100, 20000);

也就是说,near和far的同步适用于正交相机,透视相机用它不靠谱,这大概也就可以解释为什么three.js不直接在底层同步这两项属性了。

现在笔者就来跟大家解释下,为什么两种相机得到的结果会有所差异。

透视相机的特征是近大远小(实际上玩得花的可以搞成近小远大),其可视区域是下图中近裁剪面和远裁剪面及其顶点连线所围成的一个棱台,称作视锥体。

做基于射线检测的鼠标碰撞时,人眼的直观感受是从鼠标位置发射一条垂直于屏幕向里的射线,最先跟啥相交就算碰到谁。近裁剪面和远裁剪面在屏幕上将会被缩放到相同的大小从而形成近大远小的效果,那么垂直于屏幕向里的射线也将据此进行的倾斜变换(如上图的红绿蓝3条射线),图中P,A和A1在画面上是重叠的,P,蓝点和蓝箭头的头部也一样,可以看到,上图的3条射线跟近裁剪面交点和相机的距离不一样,绿点(屏幕中心)最近,红点次之,蓝点最远。

但是射线检测源码用的却是相机位置到交点的距离,因此对于透视相机而言,离屏幕中心越远,误差就越大,它用球面代替了平面来判断。

而正交相机则不存在正大远小的说法,它的视锥体是一个长方体,所以不管从哪里发出来的射线都是两两平行的。

下面给出解决方案,代码很简单,只有两行。

const proj = _intersectionPointWorld.clone().project(raycaster.camera);
if(proj.z > 1 || proj.z < -1) return null;

这段代码加到Mesh.js上

每个显示对象都有自己的raycast属性,其它对象的修改方法类似,不再赘述。

虽然只有两行,但这里的学问大着呢。

无论是OpenGL(WebGL是其子集)还是DirectX,这些GPU渲染底层使用的都是标准设备坐标系(Normal Device Coordinate,NDC),其xyz3轴的范围均为-1到1(不懂的小伙伴可以搜索NDC进一步学习)。上述代码中的方法,就是实现从屏幕像素坐标到NDC坐标的转换,它在three.js内已经封装好了。

NDC超出-1到1范围的物体将不予显示,因此该做法在视觉上是最准确的。但有同事不建议笔者这样改,他们认为能不动源码就不动,后续想要更新引擎也方便。但笔者当时坚持修改源码,因为这就是引擎的一个bug。

写本文的时候,笔者重新审视了一下,改源码也有它的不合理之处。因为它并不适用于所有的业务场景。比如线框材质,它的背面也是可见的,这时候,笔者的这一修改就反倒不正确。

由此可见,three.js不直接在raycaster里读取camera的near和far也是有他的道理。再者,如果哪天有人突发奇想,搞个哇哈哈效果那样的曲面相机,那机制又可能不是这么一回事了。

这也就使得three.js比其它引擎更加灵活,同时也变得没那么好用了。如果读者们有注意到笔者上面的滑块源码就会发现,在camera的near修改了之后,笔者还加了句updateProjectionMatrix触发刷新,这些都是灵活性高封装性不强的表现,对于萌新们来说,上手和查问题都会变得困难。这时候,笔者这一专栏的价值就体现出来了。

废话说完,来小结一下:

1 默认情况下,射线检测无视相机裁剪

2 正交相机,非线框材质的情况下,把camera的near和far属性同步到raycaster能完美解决问题

3 透视相机,非线框材质的情况下,用THREE.Vector3的project方法算出来的结果比用near和far要准确得多,同时此法也适用于正交相机

4 如果要纠结线框材质,那么可以通过修改Mesh.js源码来实现兼容

本文重点是project方法,事实上这个方法也有坑,后面还会提到,敬请期待!


http://www.ppmy.cn/news/1568204.html

相关文章

新月智能护甲系统CMIA--未来战场的守护者

新月智能护甲系统&#xff08;Crescent Moon Intelligent Armor System&#xff0c;简称CMIA&#xff09; 新月智能护甲系统&#xff08;CMIA&#xff09;是新月结合了她多年的研究成果&#xff0c;开发出的一款高度智能化的个人防护装备。这款护甲集成了先进的环境监测、生命…

机器学习 vs 深度学习

目录 一、机器学习 1、实现原理 2、实施方法 二、深度学习 1、与机器学习的联系与区别 2、神经网络的历史发展 3、神经网络的基本概念 一、机器学习 1、实现原理 训练&#xff08;归纳&#xff09;和预测&#xff08;演绎&#xff09; 归纳: 从具体案例中抽象一般规律…

Vivado生成X1或X4位宽mcs文件并固化到flash

1.生成mcs文件 01.在vivado里的菜单栏选择"tools"工具栏 02.在"tools"里选择"生成内存配置文件" 03.配置参数 按照FPGA板上的flash型号进行选型&#xff0c;相关配置步骤可参考下图。 注意&#xff1a;Flash数据传输位宽如果需要选择X4位宽&am…

jQuery小游戏(二)

jQuery小游戏&#xff08;二&#xff09; 今天是新年的第二天&#xff0c;本人在这里祝大家&#xff0c;新年快乐&#xff0c;万事胜意&#x1f495; 紧接jQuery小游戏&#xff08;一&#xff09;的内容&#xff0c;我们开始继续往下咯&#x1f61c; 游戏中使用到的方法 key…

【Linux】磁盘

没有被打开的文件 文件在磁盘中的存储 认识磁盘 磁盘的存储构成 磁盘的效率 与磁头运动频率有关。 磁盘的逻辑结构 把一面展开成线性。 通过扇区的下标编号可以推算出在磁盘的位置。 磁盘的寄存器 控制寄存器&#xff1a;负责告诉磁盘是读还是写。 数据寄存器&#xff1a;给…

[NVME] PMRCAP-Persistent Memory Region Capabilities

This register indicates capabilities of the Persistent Memory Region(持久内存区域) If the controller does not support the Persistent Memory Region feature, then this register shall be cleared to 0h BitsTypeResetDescription31:25RO 0hReserved24ROImpl Spec…

2025数学建模美赛|赛题翻译|B题

2025数学建模美赛&#xff0c;B题赛题翻译 更多内容持续更新...

题单:冒泡排序1

题目描述 给定 n 个元素的数组&#xff08;下标从 1 开始计&#xff09;&#xff0c;请使用冒泡排序对其进行排序&#xff08;升序&#xff09;。 请输出每一次冒泡过程后数组的状态。 要求&#xff1a;每次从第一个元素开始&#xff0c;将最大的元素冒泡至最后。 输入格式…