ThreeJS:光线投射与3D场景交互
光线投射Raycaster
光线投射详细介绍可参考:https://en.wikipedia.org/wiki/Ray_casting,
ThreeJS中,提供了Raycaster类,用于进行鼠标拾取,即:当三维场景中鼠标移动时,利用光线投射,可计算出鼠标划过了哪些物体。

ThreeJS官方案例webgl_interactive_cubes演示了光线投射的具体应用,

通过Raycaster类实例的intersectObjects方法,可以获取到与射线Ray相交的一组物体;

通过Raycaster类实例的intersectObject方法,可以获取到与Ray射线相交的第一个物体,

此外,在鼠标点击或移动时,我们是需要不断去更新Ray射线的,这样才能选中正确的3D物体,可以通过setFromCamera方法,来更新射线,

接着,还有一个要解决的问题,就是这里的标准化设备坐标中的二维坐标,如何进行处理才是符合ThreeJS的接口规范的?
核心代码如下,
const mousePosition = new THREE.Vector2(); //记录鼠标位置
//TODO:监听鼠标移动事件
function onPointerMove( event ) {mousePosition.x = ( event.clientX / window.innerWidth ) * 2 - 1;mousePosition.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
document.addEventListener("mousemove",onPointerMove);
注意到:计算mousePosition.y的计算结果,相比mousePosition.x多乘了一个-1,这是因为屏幕坐标系和ThreeJS的XYZ空间直角坐标系,两者的Y轴是相反的。
3D场景鼠标交互案例
下面来实现一个利用光线投射控制鼠标选中物体高亮显示的案例,主要效果如下:
①鼠标选中物体,高亮显示,

②鼠标离开物体,取消高亮显示,

完整示例代码如下,
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; //轨道控制器
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js"; //lil-gui调试工具
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; //HDR加载器
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; //GLTF加载器
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";const gui = new GUI();//TODO:打印版本
console.warn("threejs版本:", THREE.REVISION);
//TODO:创建时钟
const clock = new THREE.Clock();
//TODO:创建场景
const scene = new THREE.Scene();
//TODO:创建透视相机
const camera = new THREE.PerspectiveCamera(45, //视角window.innerWidth / window.innerHeight,0.1, //近平面1000.0 //远平面
);//TODO:创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);//TODO:轨道控制器
const orbitControls = new OrbitControls(camera, renderer.domElement);
//设置阻尼效果
// orbitControls.enableDamping = true;
orbitControls.update();document.body.appendChild(renderer.domElement);//TODO:添加光源
const light = new THREE.PointLight(0xffffff, 1000, 1000);
light.position.set(15, 15, 15);
scene.add(light);scene.background = new THREE.Color(0xbfe3dd);//TODO:添加3个球体
const ball1 = new THREE.SphereGeometry(1);
const ballMaterial = new THREE.MeshBasicMaterial({color:0xffff00});
const ballMesh1 = new THREE.Mesh(ball1,ballMaterial);
ballMesh1.geometry.scale(0.5,0.5,0.5);
ballMesh1.position.set(-1.0,-1.0,-1.0);const ballMesh2 = new THREE.Mesh(ball1,ballMaterial);
ballMesh2.geometry.scale(0.5,0.5,0.5);
ballMesh2.position.set(1.0,-1.0,-1.0);const ballMesh3 = new THREE.Mesh(ball1,ballMaterial);
ballMesh3.geometry.scale(0.5,0.5,0.5);
ballMesh3.position.set(1.0,1.0,-1.0);const ballMesh4 = new THREE.Mesh(ball1,ballMaterial);
ballMesh4.geometry.scale(0.5,0.5,0.5);
ballMesh4.position.set(1.0,1.0,1.0);scene.add(ballMesh1);
scene.add(ballMesh2);
scene.add(ballMesh3);
scene.add(ballMesh4);//TODO:设置相机位置
camera.position.z = 5.0;
camera.position.y = 2.0;
camera.position.x = 2.0;
camera.lookAt(0, 0, 0);//TODO:利用光线投射,使用鼠标选中Mesh
const rayCaster = new THREE.Raycaster();
const mousePosition = new THREE.Vector2(); //记录鼠标位置
const hightLightColor = new THREE.Color(0xff0000);//高亮颜色
const highlightMaterial = new THREE.MeshBasicMaterial({color:hightLightColor});
const lastSelected = {originMaterial:null,//被选中的3D物体的materialinstance:null,//被选中的3D物体
}
//TODO:监听鼠标移动事件
function onPointerMove( event ) {mousePosition.x = ( event.clientX / window.innerWidth ) * 2 - 1;mousePosition.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
document.addEventListener("mousemove",onPointerMove);//TODO:还原上一次被选中的3D物体
function resetLastSelected(){if(lastSelected.instance && lastSelected.originMaterial){lastSelected.instance.material = lastSelected.originMaterial;lastSelected.instance = null;lastSelected.originMaterial = null;}
}function pick3DObject(){// 通过摄像机和鼠标位置更新射线_[使用一个新的原点和方向来更新射线。]rayCaster.setFromCamera(mousePosition,camera);/*** objects —— 检测和射线相交的一组物体。recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为true。*/const interscets = rayCaster.intersectObjects(scene.children,false);if(interscets.length>0){const interscetObject = interscets[ 0 ].object;console.log("interscets::",interscetObject);//TODO:还原上一次选中的3D物体resetLastSelected();if(interscetObject){//TODO:鼠标选中3D物体,设置高亮效果//记录当前被选中的物体lastSelected.instance = interscetObject;lastSelected.originMaterial = interscetObject.material;//设置高亮interscetObject.material = highlightMaterial;}}else{//TODO:鼠标离开3D物体,还原MaterialresetLastSelected();}
}//TODO:创建坐标辅助器
// const axesHelper = new THREE.AxesHelper(5);
// scene.add(axesHelper);//TODO:渲染函数
function animate() {requestAnimationFrame(animate);//TODO:旋转立方体// mesh.rotation.x += 0.01;// mesh.rotation.y += 0.01;pick3DObject();//TODO:更新轨道控制器orbitControls.update();//TODO:渲染renderer.render(scene, camera);
}
window.onresize = function () {//TODO:重置渲染器宽高比renderer.setSize(window.innerWidth, window.innerHeight);//TODO:重置相机宽高比camera.aspect = window.innerWidth / window.innerHeight;//TODO:更新相机投影矩阵camera.updateProjectionMatrix();
};animate();//TODO:lil-gui添加调试按钮
const myObject = {// myBoolean: true,fullScreenBtnFunction: function () {document.body.requestFullscreen();},exitFullScreenBtnFunction: function () {document.exitFullscreen();},wireframeMode: false,renderMode: 1,colorSpace: 0,
};gui.add(myObject, "fullScreenBtnFunction").name("全屏显示"); // Button
gui.add(myObject, "exitFullScreenBtnFunction").name("退出全屏"); // Button
//TODO:开启/关闭线框模式
gui.add(myObject, "wireframeMode").name("线框模式").onChange(function (value) {console.log(value);mesh.material.wireframe = value;});
// gui.add(myObject, "myString"); // Text Field
// gui.add(myObject, "myNumber"); // Number Field//TODO:控制Mesh网格正反面显示模式
gui.add(myObject, "renderMode", { 双面模式: 0, 正面模式: 1, 背面模式: 2 }).name("三角形正反面显示模式").onChange(function (value) {console.log(value);switch (value) {case 0: {triangleMesh.material.side = THREE.DoubleSide;break;}case 1: {triangleMesh.material.side = THREE.FrontSide;break;}case 2: {triangleMesh.material.side = THREE.BackSide;break;}}});//TODO:控制颜色空间
gui.add(myObject, "colorSpace", {NoColorSpace: 0,SRGBColorSpace: 1,LinearSRGBColorSpace: 2,}).onChange((value) => {console.log("修改颜色空间::", value);switch (value) {case 0: {diffuseMap.colorSpace = THREE.NoColorSpace;break;}case 1: {diffuseMap.colorSpace = THREE.SRGBColorSpace;break;}case 2: {diffuseMap.colorSpace = THREE.LinearSRGBColorSpace;break;}}});
相关文章:
ThreeJS:光线投射与3D场景交互
光线投射Raycaster 光线投射详细介绍可参考:https://en.wikipedia.org/wiki/Ray_casting, ThreeJS中,提供了Raycaster类,用于进行鼠标拾取,即:当三维场景中鼠标移动时,利用光线投射,…...
docker挂载数据卷-以nginx为例
目录 一、什么是数据卷 二、数据卷的作用 三、如何挂载数据卷 1、创建nginx容器挂载数据卷 2、查看数据卷 3、查看数据卷详情 4、尝试在宿主机修改数据卷 5、查看容器内对应的数据卷目录 6、 访问nginx查看效果 一、什么是数据卷 挂载数据卷本质上就是实…...
Docker-compose部署Fastapi项目
Docker-compose部署Fastapi、postgres、Redis、Nginx) 之前有写过使用容器部署的方式,这次尝试使用Docker-compose试一次大胆的尝试 使用容器的方式部署只是掌握这项技能的基础,在使用Docker-compose的过程中会有些稍许的不同。毕竟踩过的坑才算是跨过去…...
Eigen求解线性方程组
1、线性方程组的应用 线性方程组可以用来解决各种涉及线性关系的问题。以下是一些通常可以用线性方程组来解决的问题: 在实际工程和科学计算中,求解多项式方程的根有着广泛的应用。 在控制系统的设计中,我们经常需要求解特征方程的根来分析…...
7、Java基本数据类型的使用细节探讨(超详细版本)
Java基本数据类型的使用细节探讨 一、整数类型二、浮点数三、字符型四、布尔型 我觉得基本数据类型大家学计算机的应该都懂,但是韩顺平老师讲的基本类型的使用细节我觉得有必要记录一下,重新学的时候才发现有了新的感悟! 一、整数类型 使用细…...
MFC实现点击列表头进行排序
MFC实现点击列表头排序 1、添加消息处理函数 在列表窗口右键,类向导。选择 IDC_LIST1(我的列表控件的ID),消息选择LVN_COLUMNCLICK。 2、消息映射如下 然后会在 cpp 文件中生成以下函数 void CFLashSearchDlg::OnLvnColumnclic…...
用龙梦迷你电脑福珑2.0做web服务器
用龙梦迷你电脑福珑2.0上做web服务器是可行的。已将一个网站源码放到该电脑,在局域网里可以访问网站网页。另外通过在同一局域网内的一台windows10电脑上安装花生壳软件,也可以在外网访问该内网服务器网站网页。该电脑的操作系统属于LAMP。在该电脑上安装…...
秋招后端开发面试题 - JVM类加载机制
目录 JVM类加载机制前言面试题能说一下类的生命周期吗?类加载的过程知道吗?类加载器有哪些?什么是双亲委派机制?为什么要用双亲委派机制?如何破坏双亲委派机制?如何判断一个类是无用的类? JVM类…...
OceanBase 分布式数据库【信创/国产化】- OceanBase 配置项和系统变量概述
本心、输入输出、结果 文章目录 OceanBase 分布式数据库【信创/国产化】- OceanBase 配置项和系统变量概述前言OceanBase 数据更新架构OceanBase 配置项和系统变量概述配置项配置项分类配置项查询系统变量系统变量分类系统变量查询配置项与系统变量的区分OceanBase 分布式数据库…...
单单单单单の刁队列
在数据结构的学习中,队列是一种常用的线性数据结构,它遵循先进先出(FIFO)的原则。而单调队列是队列的一种变体,它在特定条件下保证了队列中的元素具有某种单调性质,例如单调递增或单调递减。单调队列在处理…...
电脑windows系统压缩解压软件-Bandizip
一、软件功能 Bandizip是一款功能强大的压缩和解压缩软件,具有快速拖放、高速压缩、多核心支持以及广泛的文件格式支持等特点。 Bandizip软件的功能主要包括: 1. 支持多种文件格式 Bandizip可以处理多种压缩文件格式,包括ZIP, 7Z, RAR, A…...
图片公式识别@文档公式识别@表格识别@在线和离线OCR工具
文章目录 abstract普通文字识别本地软件识别公式扩展插件下载小结 在线识别网站/API👺Quicker整合(推荐)可视化编辑和识别公式其他多模态大模型识别图片中的公式排版 开源模型 abstract 本文介绍免费图片文本识别(OCR)工具,包括普通文字识别,公式识别,甚至是手写公…...
Java高阶私房菜:JVM分代收集算法介绍和各垃圾收集器原理分解
目录 什么是分代收集算法 GC的分类和专业术语 什么是垃圾收集器 垃圾收集器的分类及组合 编辑 应关注的核心指标 Serial和ParNew收集器原理 Serial收集器 ParNew收集器 Parallel和CMS收集器原理 Parallel 收集器 CMS收集器 新一代垃圾收集器G1和ZGC G1垃圾收集器…...
为什么IB损失要在100epochs后再用?
在给定的代码中,参数start_ib_epoch用于控制从第几轮开始使用IB(Instance-Balanced)损失函数进行训练。具体来说,如果start_ib_epoch的值大于等于100,那么在训练的前100轮中将使用普通的交叉熵损失函数(CE&…...
《Video Mamba Suite》论文笔记(4)Mamba在时空建模中的作用
原文翻译 4.4 Mamba for Spatial-Temporal Modeling Tasks and datasets.最后,我们评估了 Mamba 的时空建模能力。与之前的小节类似,我们在 Epic-Kitchens-100 数据集 [13] 上评估模型在zero-shot多实例检索中的性能。 Baseline and competitor.ViViT…...
【备战软考(嵌入式系统设计师)】10 - 软件工程基础
这一部分的内容是概念比较多,不要理解,去感受。 涉及的知识点是嵌入式系统开发和维护的部分,也就是和管理相关的,而不是具体如何进行嵌入式系统开发的细节。 系统开发生命周期 按照顺序有下面几个阶段,我们主要要记…...
随手笔记-GNN(朴素图神经网络)
自己看代码随手写的一点备忘录,自己看的,不喜勿喷 GNN (《------ 代码) 刚开始我还在怀疑为什么没有加weigth bias,已经为什么权重才两个,原来是对node_feats进行的network的传播,而且自己内部直接进行了。 下面是一…...
C 语言指针怎么理解?
在今天的学习中,我注意到有位学员似乎对 C 语言指针的理解有些困惑。为了帮助大家更好地理解,我来举个例子。 C 语言指针就好比 Windows 桌面上常见的快捷方式。快捷方式可以指向某个游戏,这就是普通指针;它也可以指向另一个快捷…...
HTTP协议:通信机制、特点及实践应用
目录 前言 1. 运行机制 2. 通信方式 3. 主要特点 4. 统一资源标识符(URL) 5. HTTP报文 6. HTTP请求 7. HTTP响应 8. 实体 9. 持续连接 结语 前言 HTTP(Hypertext Transfer Protocol)是互联网上应用最广泛的一种协议&a…...
Leetcode—289. 生命游戏【中等】
2024每日刷题(126) Leetcode—289. 生命游戏 算法思想 实现代码 class Solution { public:void gameOfLife(vector<vector<int>>& board) {int rows board.size();int cols board[0].size();int neighbors[3] {0, 1, -1};vector<…...
Python中数据分块处理的实现方法
在实际的数据处理任务中,我们经常需要处理大规模数据集。一次性加载所有数据到内存可能会导致内存溢出,这时数据分块处理就显得尤为重要。本文将介绍Python中三种高效的数据分块处理方法。 一、为什么需要数据分块处理? 在处理大型数据集时…...
Nano语法高亮配置最佳实践:基于nanorc项目的经验分享
Nano语法高亮配置最佳实践:基于nanorc项目的经验分享 【免费下载链接】nanorc Improved Nano Syntax Highlighting Files 项目地址: https://gitcode.com/gh_mirrors/na/nanorc Nano语法高亮配置是提升命令行文本编辑体验的关键技巧。如果你经常使用Nano编辑…...
API密钥中转站,低成本实现Token自由
最近很多小伙伴都在用AI开发项目 编写程序,或者安装部署龙虾(OpenClaw),但是国内的模型很多又满足不了自己的要求,国外的模型要么是不方便购买,要么是价格太贵,每天都要消耗几十上百美元&#x…...
cv_unet_image-colorization新手入门:从安装到上色的完整流程
cv_unet_image-colorization新手入门:从安装到上色的完整流程 你是不是有一些珍贵的黑白老照片,想要让它们重现当年的色彩?或者你是一名开发者,想要快速体验AI图像上色的魅力?今天,我将带你从零开始&#…...
新手必看:5分钟学会用Visio绘制PAD图和N-S图(附实例解析)
零基础掌握Visio绘制PAD图与N-S图的实战指南 在软件设计领域,可视化工具的重要性不言而喻。对于刚入门的开发者来说,掌握如何将抽象的逻辑思维转化为直观的图表是一项必备技能。Visio作为微软旗下的专业绘图工具,以其友好的界面和强大的功能&…...
Python量化交易终极指南:MOOTDX通达信数据接口深度解析与实战应用
Python量化交易终极指南:MOOTDX通达信数据接口深度解析与实战应用 【免费下载链接】mootdx 通达信数据读取的一个简便使用封装 项目地址: https://gitcode.com/GitHub_Trending/mo/mootdx 在量化交易和金融数据分析领域,高效、稳定的数据获取是成…...
代码重构美学
代码重构美学大赛技术文章大纲大赛背景与意义代码重构的定义及其在软件开发中的重要性美学在代码重构中的体现:可读性、简洁性、可维护性大赛的宗旨:推动高质量代码实践,提升开发者审美与工程能力参赛作品评审标准功能性:重构后代…...
OpenClaw数据可视化:Qwen3.5-4B-Claude分析并绘制图表
OpenClaw数据可视化:Qwen3.5-4B-Claude分析并绘制图表 1. 为什么需要AI辅助的数据可视化 作为一个经常需要处理数据的开发者,我过去常常陷入这样的困境:花几个小时清洗和分析数据后,却在最后一步——可视化呈现上卡壳。该用折线…...
手把手教你用Python写一个高效图片爬虫(附代码+反爬策略)
大家好!今天分享一个我近期开发的Python图片爬虫程序,适合新手入门和进阶学习。项目包含多线程下载、反反爬机制、数据存储等核心功能,代码已开源并附详细注释。 一、项目背景 在数据采集场景中,图片下载是常见需求。但目标网站…...
当 AI 开始制造 AI:递归自我提升时代的人类工程师生存指南
1. 核心命题:人类瓶颈的消失与算力主导的新纪元 1.1 递归自我提升的拐点降临 1.1.1 从"人类设计"到"自我迭代"的范式转移 人工智能发展正经历一场深刻的范式革命,其核心特征是从"人类工程师主导设计"向"AI系统自主迭代进化"的根本…...
