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

Three.js 快速入门教程【十八】射线拾取模型——鼠标点击屏幕选中模型或物体

请添加图片描述

系列文章目录

Three.js 快速入门教程【一】开启你的 3D Web 开发之旅
Three.js 快速入门教程【二】透视投影相机
Three.js 快速入门教程【三】渲染器
Three.js 快速入门教程【四】三维坐标系
Three.js 快速入门教程【五】动画渲染循环
Three.js 快速入门教程【六】相机控件 OrbitControls
Three.js 快速入门教程【七】常见几何体类型
Three.js 快速入门教程【八】常见材质类型
Three.js 快速入门教程【九】光源类型
Three.js 快速入门教程【十】常见的纹理类型
Three.js 快速入门教程【十一】天空盒的多种实现方式
Three.js 快速入门教程【十二】外部模型加载
Three.js 快速入门教程【十三】外部模型加载后常见的处理操作
Three.js 快速入门教程【十四】使用Stats.js监控渲染帧率和性能优化
Three.js 快速入门教程【十五】交互神器DragControls使用详解,实现对物体或模型拖拽
Three.js 快速入门教程【十六】调试神器 gui.js使用详解,可视化面板控制场景参数提高开发效率
Three.js 快速入门教程【十七】射线拾取模型——射线与射线投射器Raycaster介绍
Three.js 快速入门教程【十八】射线拾取模型——鼠标点击屏幕选中模型或物体


文章目录

  • 系列文章目录
  • 一、前言
  • 二、射线拾取的核心原理回顾
  • 三、屏幕坐标转标准设备坐标
    • 3.1 屏幕坐标
    • 3.2标准设备坐标
    • 3.3 屏幕坐标转标准设备坐标
      • 通过clientX、clientY转换坐标
  • 四、从摄像机位置向三维空间发射一条无限延伸的射线
  • 五、鼠标点击拾取模型(物体)完整代码
  • 六、总结


一、前言

      在上一篇文章介绍了射线和射线投射器Raycaster使用,了解了射线拾取的核心原理。要实现鼠标点击选中模型的功能,还缺失关键的一步——如何将鼠标的2D屏幕坐标转换为三维空间坐标,并以此坐标为原点,相机与该坐标连线方向为射线方向检测这条射线与场景中模型是否相交。本教程将介绍该核心步骤实现。


二、射线拾取的核心原理回顾

  • 将鼠标的2D屏幕坐标转换为三维空间坐标

  • 从摄像机位置向三维空间发射一条无限延伸的射线

  • 检测这条射线与场景中物体的交点

  • 找出最近的碰撞物体进行处理

上篇文章已经介绍了3,4步骤实现相关知识点,本篇着重讲解1,2步骤实现过程


三、屏幕坐标转标准设备坐标

在 Three.js 等图形渲染库里,射线拾取等操作往往需要用到标准设备坐标(NDC,Normalized Device Coordinates)。而我们鼠标点击所获取到的坐标是屏幕坐标,因此需要把屏幕坐标转换为标准设备坐标。

3.1 屏幕坐标

鼠标点击事件回调对象中clientX 、clientY、offsetX、offsetY等表示坐标属性称为屏幕坐标,不同的坐标属性对应不同坐标系,不同的坐标系区别在于相对的坐标原点不同。

clientX 、clientY表示鼠标点击位置相对于浏览器窗口可视区域的坐标。浏览器窗口的左上角是坐标原点 (0, 0),x 轴正方向向右,y 轴正方向向下

dom.addEventListener('click',function(event){console.log(event.clientX,"clientX" );console.log(event.clientY,"clientY" );
})

offsetX、offsetY表示鼠标点击位置相对于点击HTML元素内填充区域的坐标位置,HTML元素左上角为坐标原点,水平向右方向为x轴正方向,竖直向下方向为y轴正方向。

dom.addEventListener('click',function(event){console.log(event.offsetX,"offsetX" );console.log(event.offsetY,"offsetY" );
})

如下图所示:

在这里插入图片描述

当点击的HTML元素距离浏览器窗口上、左为0时,两个坐标系原点重合,clientX 、clientY和offsetX、offsetY值相同。例如three.js开发中,通常将canvas画布尺寸设置为窗口宽高并直接插入body,当点击画布此时2个坐标系重合。

3.2标准设备坐标

Three.js Canvas画布具有一个标准设备坐标系,该坐标系的坐标原点在canvas画布的正中间位置,x轴水平向右,y轴竖直向上.
标准设备坐标的范围是[-1, 1], Canvas画布的左上角坐标是(-1, 1),右上角坐标是(1, 1),左下角坐标是(-1, -1),右下角坐标是(1, -1)。

如下图所示:

在这里插入图片描述

这种归一化的坐标系统使得在不同分辨率和尺寸的屏幕上,能够以统一的方式处理图形和交互操作

3.3 屏幕坐标转标准设备坐标

在这里插入图片描述

假设 画布宽高为width、height,则屏幕坐标转换为标准设备坐标计算公式如下

// 屏幕坐标转标准设备坐标
canvas.addEventListener('click',function(event){const x = (event.offsetX / width) * 2 - 1; //标准设备坐标xconst y = -(event.offsetY / height) * 2 + 1;//标准设备坐标y
})

解释:
event.offsetX / width值范围为0~1, (event.offsetX / width) * 2,值范围为0~2,(event.offsetX / width) * 2 - 1值范围为-1~1跟标准设备坐标范围一致。
同理event.offsetY / height值范围0-1,-(event.offsetY / height) * 2,值范围-2~0,-(event.offsetY / height) * 2+1值范围-1~1,因为y轴方向相反所以取相反数

而画布宽高可以通过如下方式获取:

// 获取画布宽高const width= renderer.domElement.clientWidth;const height= renderer.domElement.clientHeight;

最终为:

//渲染画布
let canvas=renderer.domElement
// 屏幕坐标转标准设备坐标
canvas.addEventListener('click',function(event){const x = (event.offsetX / canvas.clientWidth) * 2 - 1; //标准设备坐标xconst y = -(event.offsetY / canvas.clientHeight) * 2 + 1;//标准设备坐标y
})

通过clientX、clientY转换坐标

上述方式是通过offsetX 、offsetY 进行转换,也可以通过clientX、clientY计算转换
由3.1介绍我们知道了两个坐标系区别,假设渲染画布距离窗口左边距离为left,上边距离为top,能得出:

 const offsetX =clientX-leftconst offsetY =clientY-top

在这里插入图片描述

而left、top可通过DOM元素的边界矩形获取

// 获取渲染器DOM元素的边界矩形const rect = renderer.domElement.getBoundingClientRect();const left=rect.leftconst top=rect.top

最终为:

//渲染画布
let canvas=renderer.domElement
// 屏幕坐标转标准设备坐标
canvas.addEventListener('click',function(event){const rect = canvas.getBoundingClientRect();const left=rect.leftconst top=rect.topconst x = ((event.clientX-left) / canvas.clientWidth) * 2 - 1; //标准设备坐标xconst y = -((event.clientY-top) / canvas.clientHeight) * 2 + 1;//标准设备坐标y
})

四、从摄像机位置向三维空间发射一条无限延伸的射线

上一篇文章我们介绍了射线投射器(Raycaster),射线投射器实例还有一个setFromCamera方法未做介绍,它的作用是依据鼠标位置(以标准化设备坐标形式呈现)和相机信息设置射线的原点和方向。

setFromCamera(coords: Vector2, camera: Camera)

下面详细介绍其参数:

  • coords:该参数代表鼠标在标准化设备坐标中的位置,类型为 Vector2。
  • camera:此参数为场景中使用的相机对象,像 THREE.PerspectiveCamera 或 THREE.OrthographicCamera 这类

示例:

// 创建射线投射器
const raycaster = new THREE.Raycaster();
//渲染画布
let canvas=renderer.domElement
// 添加画布点击事件
canvas.addEventListener('click',function(event){
// 创建一个二维向量用于存储标准设备坐标const mouse = new THREE.Vector2();//屏幕坐标转标准设备坐标mouse.x = (event.offsetX / canvas.clientWidth) * 2 - 1; //标准设备坐标xmouse.y = -(event.offsetY / canvas.clientHeight) * 2 + 1;//标准设备坐标y// 通过鼠标位置和相机信息更新射线投射器raycaster.setFromCamera(mouse, camera);
})

最后结合模拾取模型核心原理3,4步骤就能检查鼠标点击位置发出的射线是否与模型有相交从何确定是否选中模型


五、鼠标点击拾取模型(物体)完整代码

完整示例代码——实现点击选中物体,并改变其材质为红色半透明:

import * as THREE from "three";
//引入相机控制器
import { OrbitControls } from "three/addons/controls/OrbitControls.js";// 创建场景
const scene = new THREE.Scene();// 创建相机
const camera = new THREE.PerspectiveCamera(75,window.innerWidth / window.innerHeight,0.1,3000
);
camera.position.set(1, 3, 6);// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 2);
scene.add(ambientLight);//添加平行光
const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(0, 2, 20);
scene.add(light);// 创建立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({ color: 0x8844aa });
const box = new THREE.Mesh(geometry, material);
box.position.set(0, 0, 0);
scene.add(box);// //创建圆柱体
const geometry2 = new THREE.CylinderGeometry(0.5, 0.5, 1);
const material2 = new THREE.MeshLambertMaterial({color: 0x0000ff,
});
const cylinder = new THREE.Mesh(geometry2, material2);
cylinder.position.set(-4, 0, 0);
scene.add(cylinder);// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);/**** 拾取模型相关代码*/// 创建射线投射器
const raycaster = new THREE.Raycaster();
// 创建一个二维向量用于存储标准设备坐标
const mouse = new THREE.Vector2();
//渲染画布
let canvas = renderer.domElement;
// 添加画布点击事件
canvas.addEventListener("click", function (event) {//屏幕坐标转标准设备坐标mouse.x = (event.offsetX / canvas.clientWidth) * 2 - 1; //标准设备坐标xmouse.y = -(event.offsetY / canvas.clientHeight) * 2 + 1; //标准设备坐标y// 通过鼠标位置和相机信息更新射线投射器raycaster.setFromCamera(mouse, camera);// 检测射线与立方体、圆柱体相交情况const intersects = raycaster.intersectObjects([box, cylinder]);//有选中处理选中逻辑if (intersects.length > 0) {// 选中的物体设置成红色半透明intersects[0].object.material = new THREE.MeshLambertMaterial({color: 0xff0000,transparent: true,opacity: 0.5,});}
});// 创建 OrbitControls 控件
const controls = new OrbitControls(camera, renderer.domElement);
// 渲染循环
function animate() {requestAnimationFrame(animate);controls.update();renderer.render(scene, camera);
}animate();

运行效果:

在这里插入图片描述


六、总结

       通过本教程,你应该已经了解了射线(Ray)、射线拾取模型(Raycaster)、屏幕坐标转标准设备坐标的相关概念和实现方法,以及相关 API 的使用。射线拾取模型是 Three.js 中非常重要的一个功能,它可以为你的 3D 应用程序增添更多的交互性。希望你可以将这些知识应用到实际项目中,创造出更加精彩的 3D 场景。

更多three.js入门知识点请关注该系列教程后续的更新。

相关文章:

Three.js 快速入门教程【十八】射线拾取模型——鼠标点击屏幕选中模型或物体

系列文章目录 Three.js 快速入门教程【一】开启你的 3D Web 开发之旅 Three.js 快速入门教程【二】透视投影相机 Three.js 快速入门教程【三】渲染器 Three.js 快速入门教程【四】三维坐标系 Three.js 快速入门教程【五】动画渲染循环 Three.js 快速入门教程【六】相机控件 Or…...

如何下载 Postman?快速指南!

Postman 是一款非常受欢迎的 API 测试工具。它最初是作为一个 Chrome 插件发布,后来发展成为一款独立的跨平台软件,支持 Windows、Mac、Linux 等操作系统。 Postman 怎么下载教程(2025最新版)?...

Shiro学习(一):Shiro介绍和基本使用

一、Shiro介绍 1、百科对shiro的定义如下: Apache Shiro 一个强大且易于使用的 Java 安全框架,它提供了身份验证、授权、加密和会话管理等功能。Shiro 的设计目标是简化企业级应用程序的安全性开发过程,同时保持代码的简洁和易于维护。 2、…...

【git】基本操作

添加文件进本地仓库 git add 文件名删除文件 git rm 文件名版本回退 git reset [--sort| -- mixed | -- hard] sort选项: 只回退版本库,不回退暂存区和工作区 mixed(reset的默认选项): 回退版本库和暂存区,不回退工作区 hard :…...

7.1 分治-快排专题:LeetCode 75. 颜色分类

1. 题目链接 LeetCode 75. 颜色分类 2. 题目描述 给定一个包含红色(0)、白色(1)和蓝色(2)的数组 nums,要求原地对数组进行排序,使得相同颜色的元素相邻,且按红、白、蓝…...

深度解析:TOML、XML、YAML及其他配置/数据格式对比

深度解析:TOML、XML、YAML及其他配置/数据格式对比 在软件开发和系统配置中,选择合适的配置或数据格式至关重要。本文将对比 TOML、XML、YAML 等常见格式,梳理它们的核心特性、适用场景及区别,并扩展介绍其他类似格式&#xff0c…...

开源软件许可证冲突的原因和解决方法

1、什么是开源许可证以及许可证冲突产生的问题 开源软件许可证是一种法律文件,它规定了软件用户、分发者和修改者使用、复制、修改和分发开源软件的权利和义务。开源许可证是由软件的版权所有者(通常是开发者或开发团队)发布的,它…...

详解java体系实用知识总结

0.java技术能力框架 基础模块应用模块综合模块技术岗位与面试流程常用工具集系统架构设计计算机基础常用框架微服务架构jvm原理缓存容器化多线程队列云计算(阿里云/aws)设计模式数据库数据结构与算法 1.常用设计模式与应用场景 工厂模式:s…...

node-ddk,electron,主进程通讯,窗口间通讯

node-ddk,electron,主进程通讯,窗口间通讯 https://blog.csdn.net/eli960/article/details/146207062 也可以下载demo直接演示 http://linuxmail.cn/go#node-ddk import 在主进程 import main, { NODEDDK } from "node-ddk/main"在渲染进程 import renderer, …...

kubectl 命令参数详解与示例

kubectl 命令参数详解与示例 kubectl 是 Kubernetes 的命令行工具,用于与 Kubernetes 集群交互。下面我将详细介绍 kubectl 的主要命令参数,并提供相应的使用示例。 一、基础命令 1. kubectl get - 获取资源信息 常用参数: -n, --namesp…...

在 Ubuntu 20.04 上重新启动网络

参考链接: 如何在 Ubuntu 22.04 上重新启动网络 执行以下两条命令,ok sudo nmcli networking off sudo nmcli networking on...

STM32 - 在机器人、自动化领域,LL库相比HAL优势明显

在机器人控制器、电机控制器等领域的开发,需要高实时性、精细化控制或者对代码执行效率、占用空间有较高要求。所以,大家常用的HAL库明显不符合要求。再加上,我们学习一门技术,一定要学会掌握底层的原理。MCU开发的底层就是寄存器…...

【区块链安全 | 第二篇】区块链概念详解

文章目录 概述1. 区块链类型2 区块链五层架构3 账本模型4. 节点(Node)5. 区块(Block)6. 区块链(Blockchain)7. 区块链工作流程 核心技术1. 共识机制2. 智能合约 主要组件1. 交易(Transaction&am…...

【开源宝藏】30天学会CSS - DAY6 第六课 流光文字动画

第 0 步&#xff1a;项目结构 lighting-text/├─ index.html└─ style.cssindex.html&#xff1a;包含列表 <ul>&#xff0c;其中每个 <li> 放一个字母或符号。style.css&#xff1a;设置背景、文字样式&#xff0c;以及关键帧动画&#xff08;lighting&#xf…...

linux - centos7 部署 redis6.0.5

事先说明 本篇文章只解决在部署redis中出现的问题&#xff0c;并没有部署redis的全过程&#xff0c;详细部署过程可以参考Linux安装部署Redis(超级详细) - 长沙大鹏 - 博客园 执行 make 命令时报错 原因&#xff1a;是因为gcc版本太低 升级gcc版本时 出现没有可用软件包 devt…...

Java反射机制详解:原理、应用与最佳实践

Java反射机制详解&#xff1a;原理、应用与最佳实践 1. 什么是反射&#xff1f; Java反射&#xff08;Reflection&#xff09;是指在运行时动态获取类的信息&#xff08;如类名、方法、字段、构造方法等&#xff09;并操作对象的能力。它允许程序在运行时检查和修改类的行为&…...

Swift实现嵌套json字典重排序并输出string

在网络请求或接口签名中&#xff0c;通常要求将参数按照一定规则拼接成字符串。一个常见的做法是对字典的 key 进行排序&#xff0c;然后按照 “keyvalue” 的格式拼接&#xff0c;多个参数之间以特定符号&#xff08;例如 &&#xff09;连接。 如果参数中包含嵌套的字典或…...

【Ai】--- 可视化 DeepSeek-r1 接入 Open WebUI(超详细)

在编程的艺术世界里,代码和灵感需要寻找到最佳的交融点,才能打造出令人为之惊叹的作品。而在这座秋知叶i博客的殿堂里,我们将共同追寻这种完美结合,为未来的世界留下属于我们的独特印记。【Ai】--- 可视化 DeepSeek-r1 接入 Open WebUI(超详细) 开发环境一、前情提要:你…...

VSCode加Cline插件加DeepSeek实现AI编程指南

VSCode加Cline插件加DeepSeek实现AI编程指南 简介 本文将详细介绍如何在VSCode中使用Cline插件结合DeepSeek AI实现高效的AI辅助编程&#xff0c;特别适合初学者快速上手。我们将通过实现一个TodoList应用的例子来演示整个流程。 环境准备 1. 安装VSCode 前往VSCode官网下…...

代码规范之Variable Names变量名

代码规范之Variable Names变量名 golang中 官方文档&#xff1a;https://go.dev/wiki/CodeReviewComments#variable-names Variable names in Go should be short rather than long. This is especially true for local variables with limited scope. Prefer c to lineCoun…...

2025春招市场迎AI热潮:生成式人工智能(GAI)认证如何重构人才竞争力

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已逐渐渗透到我们生活的方方面面&#xff0c;从智能家居到自动驾驶&#xff0c;从智能客服到医疗诊断&#xff0c;AI的身影无处不在。而在这股AI浪潮中&#xff0c;生成式人工智能&#xff08;Generative AI,…...

Flink基础简介和安装部署

文章目录 一、Flink基础简介1、什么是Flink2、Flink流处理特性3、Flink四大基石4、Flink中的角色 二、Flink集群搭建1、Local模式①上传Flink安装包②启动交互窗口③提交任务测试④访问WebUI页面查看⑤退出停止集群 2、Standalone模式①修改配置⽂件 conf/flink-conf.yaml②修改…...

SpringBoot分布式项目实战:观察者模式的高阶应用与避坑指南

一、痛点场景&#xff1a;当观察者遇上分布式 在某电商平台重构项目中&#xff0c;我们遭遇了这样的困境&#xff1a;订单中心完成支付后需要触发库存扣减、积分结算、物流调度等12个后续操作。最初的实现采用了硬编码调用&#xff1a; // 伪代码示例 public void paySuccess…...

How to use pgbench to test performance for PostgreSQL?

pgbench 是一个用于测试 PostgreSQL 数据库性能的基准测试工具。通过模拟多个客户端并发执行 SQL 查询&#xff0c;它可以帮助你评估数据库的性能。以下是使用 pgbench 的基本步骤&#xff1a; 安装 pgbench pgbench 是 PostgreSQL 的一部分&#xff0c;因此在安装 PostgreSQ…...

C#基础学习(五)函数中的ref和out

1. 引言&#xff1a;为什么需要ref和out&#xff1f; ​问题背景&#xff1a;函数参数默认按值传递&#xff0c;值类型在函数内修改不影响外部变量&#xff1b;引用类型重新赋值时外部对象不变。​核心作用&#xff1a;允许函数内部修改外部变量的值&#xff0c;实现“双向传参…...

从零构建大语言模型全栈开发指南:第二部分:模型架构设计与实现-2.2.2文本生成逻辑:Top-k采样与温度控制

👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 2.2.2 文本生成逻辑:Top-k采样与温度控制1. 文本生成的核心挑战与数学框架1.1 自回归生成的基本流程2. `Top-k`采样原理与工程实现2.1 数学定义与算法流程2.2 PyTorch实现优化3. 温度控制的数学本质与参…...

关于CodeJava的学习笔记——9

一、IO流 1、定义 IInput输入 OOutput输出 流 : 数据从源点传输到汇点的"管道"而已 2、流的分类 按照方向分: 输入流 输出流 *:参照物当前Java程序为参照物 按照单位分: 字节流 字符流 按照功能分: 节点流 过滤流(包装流 处…...

LeetCode算法题(Go语言实现)_11

题目 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;例如&#xff0c;"ace"是"abcde"的一个子序列&a…...

Python----数据分析(足球运动员数据分析)

一、数据展示 1.1、数据 1.2、列名 字段名备注Name姓名Nationality国籍National_Position国家队位置National_Kit国家队号码Club所在俱乐部Club_Position所在俱乐部位置Club_Kit俱乐部号码Club_Joining加入俱乐部时间Contract_Expiry合同到期时间Rating评分Height身高Weight体…...

Day38 | 1365. 有多少小于当前数字的数字、941. 有效的山脉数组、1207. 独一无二的出现次数、283. 移动零、189. 轮转数组

1365. 有多少小于当前数字的数字 题目链接&#xff1a;1365. 有多少小、于当前数字的数字 - 力扣&#xff08;LeetCode&#xff09; 题目难度&#xff1a;简单 代码&#xff1a; class Solution {public int[] smallerNumbersThanCurrent(int[] nums) {Map<Integer,Inte…...