three.js+WebGL踩坑经验合集(4.1):THREE.Line2的射线检测问题(注意本篇说的是Line2,同样也不是阈值方面的问题)
上篇大家消化得如何了?
笔者说过,1级编号不同的两篇博文相对独立,所以这里笔者还是先给出完整代码,哪怕跟(3)没有太大区别。
这里我们把线的粗细调成5(排除难选中的因素),同时去掉参照物圆柱体。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>threeLine2_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>
<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>
</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.LineGeometry();
geometry.setPositions([0, 0, 600, 0, 0, -600]);
var material = new THREE.LineMaterial({linewidth: 5, color: 0xFFFFFF, resolution: new THREE.Vector2(window.innerWidth, window.innerHeight)});
var line = new THREE.Line2(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, 620);
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;
//把scene换成line,确保新增的圆柱体不参与射线检测
var intersects = raycaster.intersectObject(line, true);
for(let intersect of intersects)
{
intersect.object.material.color = overColor;
}
}
window.addEventListener("mousemove", onMouseMove);
</script>
</body>
</html>
运行效果如下,按照代码设定的线条位置和相机参数,它可以工作得很好,但是一旦用鼠标滚轮把镜头拉近,线就会莫名选不中了,只有当整根线都在屏幕范围内的时候,射线检测才能正常。
在排查问题之前,笔者先给大家简单介绍一下THREE.Line2的实现原理。核心逻辑在LineMaterial.js的shader里面,小伙伴们可以对照着代码进行理解。
1 以一段线为单位,取其start和end。
2 对start和end应用世界矩阵,相机矩阵和投影矩阵后,转换到WebGL使用的NDC坐标系上。此过程跟笔者前面的一篇博文中提到的THREE.Vector3.project方法有共通之处。three.js+WebGL踩坑经验合集(2):3D场景被相机裁切后,被裁切的部分依然可以被鼠标碰撞检测得到(射线检测)-CSDN博客
这一步的关键词为MVP矩阵,不懂的小伙伴可以自行搜索学习。
3 在步骤2的基础上,分别过S和E作垂直于SE(代码中为dir)的法线,共4条,如下图所示。
4 然后对法线向量进行缩放,使其长度等于linewidth的一半,这个法线向量在代码中对应offset属性。
此处为shader代码,需要把linewidth转换到ndc坐标系,但是shader在GPU层是无法直接获取到画布大小的,所以才有了LineMaterial的resolution属性,它是屏幕坐标转ndc坐标的条件之一。
5 取出上图中的ABCD点生成两个三角面。
6 此时若直接填充纯色,那么端点处就会不太光滑,所以后面还有一段代码,通过判断当前像素点到端点的距离来生成圆角。
笔者跳过了LineMaterial中的trimSegment代码,它虽然不是实现原理的主体部分,但是跟选不中的bug却有着密不可分的关系。
现在我们来看看Line2的射线检测代码,它的实现在父类LineSegments2上。
不难看出,射线检测的实现代码跟LineMaterial是配套编写的,过程极其相似,但是当时笔者并没有看懂LineMaterial上的trimSegment到底是干嘛用,就在那儿盲目断点了一把,这个过程分享出来没有任何意义,所以就直接给研究结果。
因为射线检测出错是发生在线条超出屏幕的那一刹那,所以我们看看端点在出界前后转换到NDC坐标系的结果。
这个状态下,在控制台输入
new THREE.Vector3(0, 0, 600).project(camera),得到的结果为
这个点是左侧端点,看百分比没有啥毛病
我们用滚轮拉近一下镜头再输出一下
嗯-0.82,基本对的上。
再滚轮一下,这个端点会穿出屏幕,结果如下
嗯,这下有毛病了,端点在左下角,应该是负一点多才是对的,然而变成了正数,飞到了另一侧去。
至此,我们发现project方法的一个大坑,对于超出可视范围的点,其计算结果并不可靠。
对于显示来说,这个bug太明显了,所以LineMaterial类加了trimSegment在边界处进行了修正。但是射线检测那个地方,大概没有人仔细地测试过,因此就留下了这样的bug。
笔者用跟trimSegment类似的方法进行修正
结果是对了,但是笔者在写这段代码的时候,实际上是还没看懂trimSegment的原理,所以用的变量是w而非z。但不管用的哪个变量,这背后隐藏着的,是齐次坐标以及透视相机矩阵公式等内容,比较复杂,所以笔者打算放到下一篇再写。因为即使笔者把trimSegment抄过来,也是没有解释为什么出界了之后,project的计算会有问题,还会再解释为什么trimSegment只需要修复z而不需要修复xy。
进入下一篇之前,还是先小结一下
1 THREE.Vector3的project方法在出界之后,结果可能会不正确
2 为确保界内部分的结果正确,我们应该把出界的部分裁剪掉,让线段的计算由始至终都在可靠的范围内进行
3 THREE.Line2的显示部分做了裁剪,但是射线检测没有,从而导致射线检测结果出错
好了,时间不早了,今天先到这里,希望明天还有时间来给大家继续分享,晚安喽~