three.js+WebGL踩坑经验合集(3):THREE.Line的射线检测问题(不是阈值方面的,也不是难选中的问题)
笔者之所以要在标题里强调不是阈值方面,是因为网上的大多数文章提到线的射线检测问题,90%以上的文章都说是因为线太细所以难选中,然后让大家把线的阈值调大。
而本文所要探讨的问题则恰好相反,不是难选中,而是在某些角度下太容易被误选中了,下面放出测试用例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>threeLine_raycaster</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script src="three/build/three.js"></script>
<script src="three/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
<script>
var scene = new THREE.Scene();
var srcColor = new THREE.Color(1, 1, 1);
var overColor = new THREE.Color(0.8, 0.3, 0);
var geometry = new THREE.BufferGeometry();
var points = [new THREE.Vector3(0, 0, 1000), new THREE.Vector3(0, 0, -1000)];
geometry.setFromPoints(points);
var material = new THREE.LineBasicMaterial({color: srcColor});
var line = new THREE.Line(geometry, material);
scene.add(line);
var width = window.innerWidth;
var height = window.innerHeight;
var camera = new THREE.PerspectiveCamera(60, 1, 0.1, 20000);
camera.position.set(10, 10, 600);
camera.lookAt(0, 0, 0);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
renderer.setClearColor(0x000000, 1);
document.body.appendChild(renderer.domElement);
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
var controls = new THREE.OrbitControls(camera,renderer.domElement);
controls.addEventListener('change', render);
// var axisHelper = new THREE.AxesHelper(250);
// scene.add(axisHelper);
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);
material.color = srcColor;
var intersects = raycaster.intersectObject(scene, true);
for(let intersect of intersects)
{
intersect.object.material.color = overColor;
}
}
window.addEventListener("mousemove", onMouseMove);
</script>
</body>
</html>
按照笔者代码设置的相机参数,会发现鼠标在距离线还有一定距离的时候就已经出现移入效果了,然后旋转一下相机,鼠标又真的离线很近才会碰到,精度提升了不少。
为此笔者翻阅了Line.js的raycast代码,其实现的核心代码在这里
THREE.Ray.distanceSqToSegment计算的是射线到被检测线条(vStart和vEnd的连线)的距离,里面的代码看着让人犯困,各种不知名变量。笔者有想过按着代码演算一遍,但是很快就放弃了这一念头,因为除了可读性差,计算过程繁琐以外,笔者还发现了一个漏洞,就是距离的计算全是取的3D坐标,而透视相机因为具备近大远小的效果,所以越是靠近屏幕,两个3D点在投影到2D后的距离也会越大。
所以这里更像是跟一个具有实际厚度的圆柱体进行碰撞检测,下面我们在THREE.Line所在的位置添加一个半径为1的THREE.CylinderGeometry看看。此处我们不对圆柱体的射线检测做任何界面的反馈。
在创建line的后面追加创建圆柱体的代码:
var cyGeometry = new THREE.CylinderGeometry(1, 1, 1200, 100, 100)
var cyMaterial = new THREE.MeshBasicMaterial({color: srcColor, transparent: true, opacity: 0.25});
var cyLine = new THREE.Mesh(cyGeometry, cyMaterial);
cyLine.rotation.x = Math.PI * 0.5;
scene.add(cyLine);
然后射线检测代码做适当的调整,确保Raycaster只跟Line做射线检测:
//把scene换成line,确保新增的圆柱体不参与射线检测
var intersects = raycaster.intersectObject(line, true);
我们来看看运行的效果
是吧,射线检测的结果更接近于具备透视效果的圆柱体。所以单纯调阈值,对于THREE.Line来说意义不大(除非用的是正交相机,或者线的z跨度不大,或者相机角度被限制在一个很小的范围内)。
数学上,线是一个一维几何体,它不具备厚度这一特性。所谓的厚度,它没有任何几何意义,仅仅是为界面显示而设置的一个属性。
three.js使用原生WebGL中WebGLRendingContext(以下简称gl)的画线api进行线条的渲染。
gl.drawArrays/drawElements(gl.LINES,...);
该api严格按照线的几何概念进行呈现,不会对输入的厚度参数进行近大远小的变换,但是three.js中的Line就没有考虑到这一点,所以射线检测的结果跟视觉没有正确匹配上。
此外,THREE.Line使用原生的gl.lineWidth设置厚度,然而这个原生的api存在兼容问题,火狐和部分桌面程序能识别,但Chrome不认,那除了粗细为1的线以外,THREE.Line是没有办法玩下去的。
盲猜three.js因为浏览器兼容问题而没有花太大精力去修复THREE.Line射线检测不准确的问题,而是另外弄了一个THREE.Line2对象。
Line2不是three.js主包里面的内容,而是被放到了示例文件夹examples里面,因此使用前需要引入(笔者现在还是拿es5版本的three.js,若已使用较新版本的three.js则无需手动引入)
<script src="three/examples/js/lines/LineSegmentsGeometry.js"></script>
<script src="three/examples/js/lines/LineGeometry.js"></script>
<script src="three/examples/js/lines/LineMaterial.js"></script>
<script src="three/examples/js/lines/LineSegments2.js"></script>
<script src="three/examples/js/lines/Line2.js"></script>
然后再把THREE.Line换成THREE.Line2
var geometry = new THREE.LineGeometry();
geometry.setPositions([0, 0, 600, 0, 0, -600]);
var material = new THREE.LineMaterial({color: 0xFFFFFF, resolution: new THREE.Vector2(window.innerWidth, window.innerHeight)});
var line = new THREE.Line2(geometry, material);
scene.add(line);
可以发现这里不是简单把Line改成Line2就完事,geometry和material的类型都换了,api也不一样。这也是让还没习惯three.js的开发小伙伴们嗤之以鼻的槽点之一。
resolution这个参数也是怪怪的,但笔者打算下一篇再跟大家探讨,大家就先死记一下吧。
我们来看看换THREE.Line2后的效果。
这下真的跟线对应上了,而且某些位置还细得不太好选,这种情况下再调阈值就特别管用。
下面来小结本文的内容:
1 THREE.Line在显示上没有透视效果,但是射线检测的代码依然按着有透视效果的方式进行实现,所以在透视相机下,射线检测的结果跟显示不匹配。
2 THREE.Line在主流的Chrome浏览器下无法设置厚度(只能固定为1),需要用THREE.Line2代替。
3 调阈值的方法不适用于所有场景,遇到透视相机,线的z跨度较大的场合,阈值怎么调都是调不好的,这时也建议改用THREE.Line2。
THREE.Line2抛弃了原生的画线api,通过自己绘制三角面来模拟线的效果,跟THREE.Line相比,灵活性和可控性都高出不少,但与此同时也变得更加难用。
尽管如此,THREE.Line2在射线检测方面也是有bug的,笔者将会在下一篇跟大家继续探讨。大家先好好消化本文,我们待会见!