【Threejs基础教程-点线精灵篇】 4.5 射线(Ray)与射线拾取(Raycaster)

2023-11-14

学习ThreeJS的捷径

本段内容会写在0篇以外所有的,本人所编写的Threejs教程中

对,学习ThreeJS有捷径
当你有哪个函数不懂的时候,第一时间去翻一翻文档
当你有哪个效果不会做的时候,第一时间去翻一翻所有的案例,也许就能找到你想要的效果
最重要的一点,就是,绝对不要怕问问题,越怕找找别人问题,你的问题就会被拖的越久

如果你确定要走WebGL/ThreeJS的开发者路线的话,以下行为可以让你更快的学习ThreeJS

  1. 没事就把所有的文档翻一遍,哪怕看不懂,也要留个印象,至少要知道Threejs有什么
  2. 没事多看看案例效果,当你记忆的案例效果足够多时,下次再遇到相似问题时,你就有可能第一时间来找对应的案例,能更快解决你自己的问题
  3. 上述案例不只是官网的案例,郭隆邦技术博客,跃焱邵隼,暮志未晚等站点均有不少优质案例,记得一并收藏
    http://www.yanhuangxueyuan.com/ 郭隆邦技术博客
    https://www.wellyyss.cn/ 跃焱邵隼
    http://www.wjceo.com/ 暮志未晚
    这三个站点是我最常逛的站点,推荐各位有事没事逛一下,看看他们的案例和写法思路,绝对没坏处

射线介绍

在这里插入图片描述
如果你玩过激光笔的话,对射线的理解就再容易不过了

射线是由一个点,一个方向构成的线,由顶点向当前方向无限延伸的一条线,就叫射线

但是,我们一般不会直接用到THREE.Ray,因为射线平时最主要的用途,就是拾取物体,所以我们直接用Raycaster即可,对数学概念的射线有兴趣的同学可以自行查阅射线Ray的官方文档

射线官方文档

射线用途

射线最主要的用途就是拾取物体,简单说,我们在屏幕上添加一个点击事件,然后以屏幕的位置发射一条射线,执行拾取所有场景的物体,并拿到拾取到的物体,做对应的交互事件,这样我们一套点击事件就完成了

射线拾取操作过程

  1. 绑定点击事件
  2. 获取点击时的位置并创建映射
  3. 根据点击时的位置映射的顶点创建射线,threejs会自动执行射线穿过物体的算法,并将穿过的物体以数组形式返回
  4. 根据逻辑,让整个数组的物体发生交互事件,或者指定拾取到的第一个,让其发生交互事件

射线拾取Raycaster

以笔者的经验之谈,学习Threejs的新手90%以上会栽到射线拾取的问题上

我们先来简单介绍一下射线拾取

在这里插入图片描述

我们以透视相机为例,本质上,透视相机的近端面,你可以理解为已经映射到了屏幕上,实际的相机位置,在四条红线的交点处

我们的射线,就是从相机位置出发,以屏幕上点击的位置发射一条射线,然后做射线与物体的交叉计算,计算过程threejs已经解决了,所以不用考虑这个问题

我们以上述的四个步骤,来写射线拾取的代码

0. 创建射线

        //创建射线对象
        let raycaster = new THREE.Raycaster();
        //创建映射用,用于保存映射结果的顶点
        let mouse = new THREE.Vector2();

1. 绑定点击事件

这里我们用画布<canvas>来绑定事件

        //创建射线对象
        let raycaster = new THREE.Raycaster();
        //创建映射用,用于保存映射结果的顶点
        let mouse = new THREE.Vector2();

        renderer.domElement.addEventListener('click',e=>{
            //获取鼠标点击的位置
            let x = e.clientX;
            let y = e.clientY;
            
        })

2. 获取点击时的位置并创建映射顶点

        //创建射线对象
        let raycaster = new THREE.Raycaster();
        //创建映射用,用于保存映射结果的顶点
        let mouse = new THREE.Vector2();

        renderer.domElement.addEventListener('click',e=>{
            //获取鼠标点击的位置
            let x = e.clientX;
            let y = e.clientY;

            //我们最终点击的位置,要用映射的方式传给射线,射线根据计算的比例,计算出实际发射射线的方向,再发出射线
            mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1;
            mouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1;
            
        })

官方这里使用的方式是映射,而我们鼠标点击的位置,一般不是空间中的实际位置
在这里插入图片描述
所以我们要将屏幕的坐标的坐标系做一下转换,并计算当前点中的顶点在上图坐标系中表示的二维顶点 ,然后保存在 mouse对象上

新手常见错误1:完全不理解计算公式导致的映射坐标计算错误

	pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
	pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

上述写法为官方写法,但是有人直接无脑复制粘贴,结果自己的画布并不是分布在全屏的,发现射线拾取的计算偏了

上述写法是建立在:你的 < canvas > 覆盖满全屏的情况下
== 如果你的< canvas > 没有覆盖全屏,我们需要使用另一个公式来计算映射 ==

全屏的映射公式计算图解:

在这里插入图片描述
如果当前Canvas覆盖全屏,我们在Canvas上点击了P点

第一步:计算比例值,先获取A-PW/A-PH的距离,然后除以全屏宽度AB/全屏高度AC,得到横竖向的比例

第二步:转换坐标系,因为在前端的概念中,坐标系是右边为x轴正方向,下边为y轴正方向,原点在左上角,那么,我们要把坐标系移动了,然后做一下方向变换,这就是为什么y的计算是负值

第三步:处理比例差异,我们实际坐标系的最大长宽都是2,所以我们要给比例 * 2,由于坐标系移动了,我们的坐标也要向回移动坐标系一半的距离,这样才能保证拿到的是最终的映射坐标,y轴同理

非全屏的映射公式计算图解

在这里插入图片描述
第一步依然是先计算比例值:我们依然可以用e.clientX和e.clientY来拿点击的位置,但是这次我们计算的就不一样了,我们需要拿到canvas的左边的位置,我们可以用 dom.getBoundingClientRect().left 来获取H所在竖线的x值,然后用p.clientX 减掉这个值,得到PH的长度,然后再除以当前dom宽度,即可计算到比例值
第二第三步几乎没有什么大的变化,依然是坐标系转换还有比例值差异处理

非全屏下的射线拾取映射公式

    mouse.x = ((clientX - dom.getBoundingClientRect().left) / dom.offsetWidth) * 2 - 1
    mouse.y = -((clientY - dom.getBoundingClientRect().top) / dom.offsetHeight) * 2 + 1

3. 根据点击时的位置创建射线

        //创建射线对象
        let raycaster = new THREE.Raycaster();
        //创建映射用,用于保存映射结果的顶点
        let mouse = new THREE.Vector2();

        renderer.domElement.addEventListener('click',e=>{
            //获取鼠标点击的位置
            let x = e.clientX;
            let y = e.clientY;

            //我们最终点击的位置,要用映射的方式传给射线,射线根据计算的比例,计算出实际发射射线的方向,再发出射线
            mouse.x = ( x / window.innerWidth ) * 2 - 1;
            mouse.y = - ( y / window.innerHeight ) * 2 + 1;

            //使用当前相机和映射点修改当前射线属性
            raycaster.setFromCamera(mouse,camera);
            
        })

setFromCamera,根据相机和映射后的坐标设置射线

4.获取拾取的物体并做指定的操作

        //创建射线对象
        let raycaster = new THREE.Raycaster();
        //创建映射用,用于保存映射结果的顶点
        let mouse = new THREE.Vector2();

        renderer.domElement.addEventListener('click',e=>{
            //获取鼠标点击的位置
            let x = e.clientX;
            let y = e.clientY;

            //我们最终点击的位置,要用映射的方式传给射线,射线根据计算的比例,计算出实际发射射线的方向,再发出射线
            mouse.x = (x / window.innerWidth ) * 2 - 1;
            mouse.y = - ( y / window.innerHeight ) * 2 + 1;

            //使用当前相机和映射点修改当前射线属性
            raycaster.setFromCamera(mouse,camera);

            // 计算物体和射线的交点
            let intersects = raycaster.intersectObjects( scene.children );
            console.log(intersects);
        })

这样我们就拿到了射线拾取到的所有物体,至于怎么用,完全看你的需求,我们可以自行查看一下intersects下的所有元素

新手常见错误2,使用intersectObjects函数时只认识scene.children,使用拾取结果时,只用数组第一个元素

  1. 很多新手都问过这样的问题,在做射线拾取的时候,总是被什么东西挡住,拿不到我想要拾取的东西
    在早期threejs官方文档中,官方是默认这样写的
let intersects = raycaster.intertscetObjects(scene.children);
if(intersects.length > 0){
	let object = intersects[0].object;
	//对object的处理
}

有些人就特别喜欢直接复制粘贴,连代码都不分析,后面的业务逻辑就全写在if里面了,东西被挡住了就完全不知道怎么操作了,也有聪明的,反应过来intersects是个数组,然后就遍历循环数组,从数组中拿到想要的东西。。。
还有的人,用着巨大的模型,几千个小元素,然后用mousemove事件去拾取整个场景的元素,让场景卡到离谱,还总是在抱怨threejs太卡之类的问题。。。

上述这些问题有一个统一的解决办法,就是先理解 intersectObjects这个函数
intersectObjects ( array,recursive , optionalTarget)
array: 一个装满了Object3D对象的数组,可以是mesh,可以是line,可以是camera,任何Object3D及其继承类都可以,而官方的案例中偷了懒,因为scene.children,本身就是一个只能容纳Object3D类型元素的数组,所以这里特别容易混淆概念
recursive : 这个设置为true时,若你当前拾取的元素的children中还有子元素,那么这个函数就会检索你下面的子元素
optionalTarget :一个数组,用于接收拾取结果
返回值:一个数组,用于保存拾取计算的结果

上面三个问题,我们一个一个解决:

  1. 拾取时,东西被遮挡的问题,我们只需要单独let一个数组,然后将需要被拾取的东西添加进来即可
  2. 一次拾取的东西太多,要从中筛选,单独let一个数组,将你需要被拾取的东西添加进来即可
  3. 用拾取时特别卡,这个从三方面解决,一方面是优化你模型的总大小,以及你元素的总数,另一方面,也可以单独的创建一个数组,将需要被拾取的元素添加到数组中做拾取,这样可以极大的避免做过多无用的计算,第三种就是让你老板给你上4090 + 线程撕裂者,你的客户也要求必须用这样的机子

			let array = [];

			array.push(mesh1);
			array.push(mesh2);
            // 这样写的话,射线就只会针对mesh1和mesh2做射线拾取算法,而且拾取到的结果不会是这两个元素以外的元素
            let intersects = raycaster.intersectObjects( array );
            console.log(intersects);
            

射线练习Demo

创建无数随机的方块

        let geometry = new THREE.BoxGeometry(1,1,1);

        let canSelectedMesh = [];

        for(let i = 0;i< 200;i++){
            let material = new THREE.MeshStandardMaterial({color:0xffffff * Math.random()});
            let mesh = new THREE.Mesh(geometry,material)
            mesh.position.x = Math.random() * 20 - 10;
            mesh.position.y = Math.random() * 20 - 10;
            mesh.position.z = Math.random() * 20 - 10;
            canSelectedMesh.push(mesh);
            scene.add(mesh)
        }

创建射线并绑定事件

        //创建射线对象
        let raycaster = new THREE.Raycaster();
        //创建映射用,用于保存映射结果的顶点
        let mouse = new THREE.Vector2();

        renderer.domElement.addEventListener('click',e=>{
            //获取鼠标点击的位置
            let x = e.clientX;
            let y = e.clientY;

            //我们最终点击的位置,要用映射的方式传给射线,射线根据计算的比例,计算出实际发射射线的方向,再发出射线
            mouse.x = ( x / window.innerWidth ) * 2 - 1;
            mouse.y = - ( y / window.innerHeight ) * 2 + 1;

            //使用当前相机和映射点修改当前射线属性
            raycaster.setFromCamera(mouse,camera);

            // 计算物体和射线的焦点
            let intersects = raycaster.intersectObjects( canSelectedMesh );
        })

给方块变色


            //取第一个元素
            if(intersects.length > 0){
                let object = intersects[0].object;
                object.material.color = new THREE.Color("#000000");
            }

案例效果

在这里插入图片描述

案例完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>射线拾取</title>
    <style>
        canvas{
            display: block;
        }
        body {
            margin: 0;
            overscroll-behavior: none;
        }
        #btns{
            position: absolute;
            top:10%;
            width: 500px;
            height: 100px;
            left: 50%;
            transform:translateX(-50%);
        }
    </style>
</head>
<body>
<div id="btns"></div>

<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>

<script type="importmap">
			{
				"imports": {
					"three": "../three.js-master/build/three.module.js"
				}
			}
		</script>
<script type="module">
    import * as THREE from '../three.js-master/build/three.module.js';
    import {OrbitControls} from "../three.js-master/examples/jsm/controls/OrbitControls.js";

    let scene,renderer,camera,orbitControls;

    let mesh;

    function init(){
        scene = new THREE.Scene();
        renderer = new THREE.WebGLRenderer({
            alpha:true,
            antialias:true
        });
        renderer.setSize(window.innerWidth,window.innerHeight);
        document.body.appendChild(renderer.domElement);
        camera = new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,1,1000);
        camera.position.set(10,10,10);
        orbitControls = new OrbitControls(camera,renderer.domElement);
        let light = new THREE.PointLight();
        camera.add(light);
        scene.add(camera);

        let helper = new THREE.AxesHelper(5);
        scene.add(helper);

    }
    function addMesh(){

        let geometry = new THREE.BoxGeometry(1,1,1);

        let canSelectedMesh = [];

        for(let i = 0;i< 200;i++){
            let material = new THREE.MeshStandardMaterial({color:0xffffff * Math.random()});
            let mesh = new THREE.Mesh(geometry,material)
            mesh.position.x = Math.random() * 20 - 10;
            mesh.position.y = Math.random() * 20 - 10;
            mesh.position.z = Math.random() * 20 - 10;
            canSelectedMesh.push(mesh);
            scene.add(mesh)
        }


        //创建射线对象
        let raycaster = new THREE.Raycaster();
        //创建映射用,用于保存映射结果的顶点
        let mouse = new THREE.Vector2();

        renderer.domElement.addEventListener('click',e=>{
            //获取鼠标点击的位置
            let x = e.clientX;
            let y = e.clientY;

            //我们最终点击的位置,要用映射的方式传给射线,射线根据计算的比例,计算出实际发射射线的方向,再发出射线
            mouse.x = ( x / window.innerWidth ) * 2 - 1;
            mouse.y = - ( y / window.innerHeight ) * 2 + 1;

            //使用当前相机和映射点修改当前射线属性
            raycaster.setFromCamera(mouse,camera);

            // 计算物体和射线的焦点
            let intersects = raycaster.intersectObjects( canSelectedMesh );

            //取第一个元素
            if(intersects.length > 0){
                let object = intersects[0].object;
                object.material.color = new THREE.Color("#000000");
            }
        })

    }

    function render(){
        renderer.render(scene,camera);
        requestAnimationFrame(render);
    }
    init();
    addMesh();
    render();
</script>
</body>
</html>

Raycaster常用函数

**set( origin,direction ) **
设置射线的起点和方向

origin:一个Vector3对象,射线起点,我们做射线拾取也不是一定要从相机出发,用映射来拾取,我们还可以从空间中创建射线,比如说我们玩的游戏中,有激光的场景,如果人物碰到激光了,就会判定掉血,这个就是在空间中任意选取点创建的射线

direction,一个Vector3对象,用于保存射线方向

setFromCamera(coords,camera)
通过相机设置射线

coords:映射坐标,映射坐标的取值范围应在-1~1之间
camera:射线起点将会放到传入的相机上

intersectObject(Object,recursive,optionalTarget)
上面我们讲了intersectObjects,这个是object,仅一个字母之差,这个函数用来拾取指定物体而非数组,如果你只想拾取那一个目标物体,这个是比较好用的函数

object:一个Object3D对象
recursive : 这个设置为true时,若你当前拾取的元素的children中还有子元素,那么这个函数就会检索你下面的子元素
optionalTarget :一个数组,用于接收拾取结果
返回值:一个数组,用于保存拾取结果

这里要介绍一下返回值数组下面的元素
在这里插入图片描述
distance:射线起点射线与物体交点 的距离
face:与射线相交的平面
faceIndex:与射线相交的平面的index,这个在之前bufferGeometry中介绍过
object:与射线相交的物体,一般是Mesh,Point,Sprite,Line等
point:射线与物体相交的第一个顶点
uv:射线的交点在物体的纹理的uv的位置

intersectObject和intersectObjects这两个函数,最终返回的结果,都是上述形式的对象构成的数组,所以,我们要拿到点中的物体时,可以拿 res.object,我们需要拿点中的顶点时,就获取point即可

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Threejs基础教程-点线精灵篇】 4.5 射线(Ray)与射线拾取(Raycaster) 的相关文章

随机推荐

  • SD卡引脚 电路图及工作原理介绍

    对于SD卡的硬件结构 在官方的文档上有很详细的介绍 如SD卡内的存储器结构 存储单元组织方式等内容 要实现对它的读写 最核心的是它的时序 笔者在经过了实际的测试后 使用51单片机成功实现了对SD卡的扇区读写 并对其读写速度进行了评估 下面先
  • android log处理

    今天是新年上班的第一天 好高兴啊 感觉公司好亲切 可不是 开门红包抽中最高的啦 今天计划实现把导致软件崩溃的bug发送到服务器端 好让我根据bug 调试程序 通过查阅资料 遇到以下几个问题 1 把log文件发送到服务器 这个log是使程序崩
  • LINUX学习--页面认识和常用命令

    目录 前言 一 LINUX界面 1 Linux图形界面 2 Linux文本界面 3 界面切换 4 终端 判断自己是否有网 二 LINUX目录介绍 1 设计思想 2 常用目录 三 LINUX基本命令 1 LINUX命令格式 2 LINUX命令
  • AI绘图MidJourney提示词详解——美女画法(1)

    AI绘图MidJourney提示词详解 美女画法 1 前言 提示词基本公式 1 1 公式参数说明 编写提示词 Prompt 2 1 题目 2 2 提示词描述 2 3 生成提示词 获取更多信息 前言 Midjourney Prompt即提示词
  • 浙江大学【面板数据分析与STATA应用】——第一讲短面板数据分析

    基本概念 面板数据及分类 面板数据分类 短面板和长面板 动态面板和静态面板 平衡面板和非平衡面板 截面数大于时间数就是短面板 反之 则为长面板 解释变量包含被解释变量的滞后值则为动态面板 反之 则为静态面板 平衡面板 每个个体在想他的时间内
  • zabbix api无法获取已解决问题解决思路和总结

    zabbix api无法获取已解决问题解决思路和总结 事件背景 最近在对zabbix api进行二次开发 在设计获取problem的接口的时候发现了调用zabbix api无法获取已解决的problem的问题 在解决这个问题的过程之中对于z
  • aigc是什么,aigc和chatGPT的区别,aigc商业应用场景

    AIGC是Artificial Intelligence Graphics Computing的缩写 意为人工智能图形计算 它是一个涵盖了人工智能 计算机图形学和深度学习等领域技术的综合平台 其目的是将这些技术结合起来 实现更加高效 智能化
  • 深度学习发展历程全讲解

    深度学习发展历程 深度学习综述 1 基本概念 2人工智能发展和内容 2 1 AI目标 2 2 AI领域 3 机器学习发展内容 3 1 发展阶段 3 2 任务 3 3目标 3 4算法 3 5 学习方式 4深度学习发展内容 4 1 深度学习的发
  • TortoiseSvn不显示图标问题解决方法

    TortoiseSvn不显示图标问题解决方法 问题原因 windows系统中最多显示指定数量的图标 超过一定数量后 之后的图标不会显示 解决方式 右击自己的svn项目 TortoiseSvn gt Settings 按照上图的箭头操作 会打
  • easyExcel日期字符串格式统一处理

    1 遇到日期导入转换Date失败情况 excel里面日期是字符串 java实体类和数据库都是日期类型 导入转换 public class EasyExcelString2DateConvert implements Converter
  • 深度学习之图像分类学习笔记(一)图像卷积与滤波

    转载自 http blog csdn net zouxy09 article details 49080029 写的很清楚 解决了我对于卷积理解的困难 一 线性滤波与卷积的基本概念 线性滤波可以说是图像处理最基本的方法 它可以允许我们对图像
  • ASCII Unicode, UTF8 的关系,string和wstring转换

    目录 1 三大编码由来和转换 2 三大编码在计算机中应用 3 char string 和wchar t wstring 转换 写这篇文章遇到的的问题是c 操作正则的时候 遇到中文出现匹配失败 以及visual studio中中文乱码问题 当
  • 特殊字符集中营

    喆囍罓槑卍 回 卐
  • 基于QT的UDP通信

    简介 UDP User Datagram Protocol 用户数据报协议 是轻量的 不可靠的 面向数据报 datagram 的 无连接的协议 它可以用于对可靠性要求不高的场景 与TCP通信不同 两个程序之间进行UDP通信不需要预先建立持久
  • 基于Microsoft Learn的C#学习(入门-程序构建基块)

    参考文档 https learn microsoft com zh cn dotnet csharp tour of csharp program building blockshttps learn microsoft com zh cn
  • html制作的一个钢琴(可以弹奏)

    html制作的一个钢琴 可以弹奏 可以上传曲谱弹奏 如下图所示 点我下载资源 https download csdn net download weixin 43474701 63403221
  • echarts图表 定时刷新

    在制作大屏可视化时 需要有种动的效果 大概的思想是通过定时器太改变 1 组件传递时间参数 由于每个图表都是都是封装成一个组件 在父组件中 通过 传递时间
  • 将ip改成域名方式登陆

    昨天有同学问我如何将ip修改成域名 这样的话可以使程序变得更加健壮 别人不能直接看见你的ip地址 后来总结下分享给大家 首先找到hosts文件的位置 这个文件是系统dns默认查找的文件 windows 系统 C WINDOWS system
  • cmd上可以执行的命令在VSCODE报错:无法加载文件,因为在此系统上禁止运行脚本;xx既不是内部或外部命令,也不是可运行的程序

    有时候好好的项目 在vscode等编辑器上突然无法正常启动 提示 什么既不是内部或外部命令 也不是可运行的程序 或者什么在此系统禁止运行 如下 这时候尝试一下直接打开cmd cd到对应的项目位置 运行一下看是否可以正常运行 如果可以 那基本
  • 【Threejs基础教程-点线精灵篇】 4.5 射线(Ray)与射线拾取(Raycaster)

    射线和射线拾取 学习ThreeJS的捷径 射线介绍 射线用途 射线拾取操作过程 射线拾取Raycaster 0 创建射线 1 绑定点击事件 2 获取点击时的位置并创建映射顶点 新手常见错误1 完全不理解计算公式导致的映射坐标计算错误 全屏的