当前位置: 首页 > article >正文

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的,笔者将会在下一篇跟大家继续探讨。大家先好好消化本文,我们待会见!


http://www.kler.cn/a/519540.html

相关文章:

  • Docker 国内镜像源
  • 基于 WEB 开发的在线学习系统设计与开发
  • 深入MapReduce——引入
  • 数据结构基础之《(15)—排序算法小结》
  • Django 日志配置实战指南
  • 小识Java死锁是否会造成CPU100%?
  • IDEA2020同时使用SVN和GIT
  • DBO优化GRNN回归预测matlab
  • Altium Designer脚本开发不支持功能集锦
  • 接口(完)
  • 快速更改WampServer根目录php脚本
  • 如何写美赛(MCM/ICM)论文中的Summary部分
  • kafka-保姆级配置说明(consumer)
  • 【算法】递归型枚举与回溯剪枝初识
  • 基于Django的就业系统的设计与实现
  • 使用python gitlab包来实现更新gitlab wiki page
  • 25.日常算法
  • Linux查看服务器的内外网地址
  • 【Linux网络编程】数据链路层--以太网协议
  • 回顾2024,展望2025
  • BGP边界网关协议(Border Gateway Protocol)路由聚合详解
  • Gradle buildSrc模块详解:集中管理构建逻辑的利器
  • PyTorch张量操作reshape view permute transpose
  • Uniapp开发总结
  • 【Linux】21.基础IO(3)
  • Soul App创始人张璐团队引领平台入选2024上海软件和信息技术服务业百强