Threejs进阶之十:让模型移动到鼠标点击的指定位置

news2024/10/3 6:35:08

上一节中我们实现了物体沿指定轨迹移动的动画效果,这一节我们来实现让模型移动到鼠标点击的制定位置的动画效果。
先看下实现后的最终效果
在这里插入图片描述
要实现上面的动画效果,我们需要通过以下步骤来实现

第一步,监听鼠标事件

我们需要监听鼠标的点击事件,获取鼠标点击点相对浏览器可视区域左上角的距离,通过监听“pointerdown”事件,获取点击点的clientX和clientY;clientX/Y获取到的是点击点相对浏览器可视区域左上角距离。

renderer.domElement.addEventListener('pointerdown',function(event) {
    event = event || window.event
    initPos = {
      x:event.clientX,
      y:event.clientY
    } 
})

第二步:获取鼠标点击点的屏幕坐标

通过Element.getBoundingClientRect()中的 top属性获取元素上边距离页面上边的距离,
通过left属性获取元素左边距离页面左边的距离,
通过计算 clientX 与domElement.getBoundingClientRect().left的差值,获取x点坐标;
通过计算 clientY 与domElement.getBoundingClientRect().top的差值,获取y点坐标;

  let x = event.clientX - renderer.domElement.getBoundingClientRect().left
  let y = event.clientY - renderer.domElement.getBoundingClientRect().top

第三步:获取画布的宽度和高度并归一化

通过坐标转换,将x和y的坐标归一化,即将屏幕坐标转换为threejs的世界坐标

 let canvasWidth = renderer.domElement.clientWidth 
 // clientWidth 返回元素的可视宽度,包括内边距,但不包括边框、滚动条或外边距,以像素计
 // canvas画布高度
 // clientHeight 返回元素的可视高度,包括内边距,但不包括边框、滚动条或外边距,以像素计。
 let canvasHeight = renderer.domElement.clientHeight
 // offsetHeight 属性返回元素的可视高度(以像素为单位),包括内边距、边框和滚动条,但不包括外边距。
 // offsetWidth 属性返回元素的可视宽度(以像素为单位),包括内边距、边框和滚动条,但不包括外边距。
 // 坐标转换(归一化的值) 
 const sx = -1 + (x / canvasWidth) * 2 
 const sy = 1 - (y / canvasHeight) * 2

第四步:创建光线投射器,并通过摄像机和鼠标点击的位置更新射线

// 光线投射器
const rayCaster = new THREE.Raycaster()
// 通过摄像机和鼠标位置更新射线
rayCaster.setFromCamera(new THREE.Vector2(sx,sy),camera) 

第五步:通过rayCaster.intersectObjects()方法检测射线与物体的交集

这里我们是点击在水面上,因此,将water作为参数传入,检测射线与水面的交集,将结果返回给intersects

const intersects = rayCaster.intersectObjects([water]) 

第六步:判断射线与物体是否有交集,如果有,获取坐标并处理逻辑

通过if语句判断intersects.length是否大于零,如果大于零,说明有交集,在if语句中处理如下逻辑:
1、通过intersects[0].point获取鼠标点击时射线与water相交点的坐标(新位置)存入newPos变量,
2、通过yacht.position.clone() 获取模型当前位置坐标(老位置)存入originPos变量
3、通过camera.position.clone() 获取相机当前位置坐标(老位置)存入cameraOriginPos 变量
4、通过向量减法获取鼠标点击点的向量和向量长度 存入vector 变量
5、创建一个四元数对象,通过.setFromUnitVectors将该四元数设置为从方向向量new THREE.Vector3(0,0,-1)旋转到方向向量 vector 单位向量 所需的旋转角度。
6、创建一个Threejs的Clock()对象
7、创建一个speed常量为100,设置移动速度
8、通过setInterval()方法启动定时器,通过speed和clock.getElapsedTime()失去时间的乘积,得到每次时间间隔移动的长度
9、通过每次时间间隔移动的长度除以向量总长度,获取每次间隔移动的比值,将该比值与重点向量相乘,得到每次间隔移动的向量坐标
10、将上面得到的每次间隔移动的向量坐标与模型原始位置向量坐标相加,得到模型每次时间间隔移动的重点向量坐标
11、将每次间隔移动的终点坐标复制给模型的position属性
12、将每次间隔移动的向量坐标与相机原始坐标相加得到每次时间间隔移动的终点坐标,并将给坐标复制给相机的position属性
13、将每次时间间隔的终点坐标movePos复制给控制器的target属性,使其始终朝向movePos位置
14、执行完成后清除定时器

if(intersects.length > 0) {	
      const newPos = intersects[0].point //新位置(鼠标点击时射线与water相交点的坐标)
      const originPos = yacht.position.clone() //老位置(模型的坐标)
      const cameraOriginPos = camera.position.clone() //相机老位置 
      // 向量减法.sub()和向量长度.length()
      // 两个向量相减就是求它们的差向量,其结果是以减向量的终点为起点,被减向量的终点为终点的向量
      // 几何意义:向量a,向量b相减,理解为以b的终点为始点,以a的终点为终点的向量,方向由b指向a (指向被减数)
      // 通过.sub()方法可以对两个向量进行减法运算,比如两个表示顶点坐标的Vector3对象进行减法运算返回一个新的Vector3对象就是两个点构成的向量。
      // 向量对象执行.length()方法会返回向量的长度。
      const vector = newPos.clone().sub(originPos)       
      const distance = vector.length() 
      // 修正方向 .setFromUnitVectors将该四元数设置为从方向向量 vFrom 旋转到方向向量 vTo 所需的旋转。
      // vector.clone().normalize() 将该向量转换为单位向量
      const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,0,-1),vector.clone().normalize())
      yacht.quaternion.copy(quaternion)
      
      const clock = new THREE.Clock()
      const speed = 100 //移动速度
      
	  timer_interval && clearInterval(timer_interval) //清楚timer
      timer_timeout && clearInterval(timer_timeout)

      //更新位置
      timer_interval = setInterval(()=> { 
        // 通过speed和clock.getElapsedTime()失去时间的乘积,得到每次时间间隔移动的长度
        const moveLength = clock.getElapsedTime()*speed
        // .multiplyScalar将该向量与所传入的标量s进行相乘。
        // 通过每次时间间隔移动的长度除以向量总长度,获取每次间隔移动的比值
        // 将该比值与重点向量相乘,得到每次间隔移动的向量坐标
        const moveVector = vector.clone().multiplyScalar(moveLength / distance)
        // 将上面得到的每次间隔移动的向量坐标与模型原始位置向量坐标相加,得到模型每次时间间隔移动的重点向量坐标
        const movePos = originPos.clone().add(moveVector) //将模型的坐标向量与moveVector相加
        // 将每次间隔移动的终点坐标复制给模型的position属性
        yacht.position.copy(movePos)
        // 将每次间隔移动的向量坐标与相机原始坐标相加得到每次时间间隔移动的终点坐标,并将给坐标复制给相机的position属性
        camera.position.copy(cameraOriginPos.clone().add(moveVector))
        // 将每次时间间隔的终点坐标movePos复制给控制器的target属性,使其始终朝向movePos位置
        controls.target.copy(movePos)
      },50)

      // 清定时器,并最终定位
      timer_timeout = setTimeout(()=>{
        clearInterval(timer_interval)
        yacht.position.copy(newPos)
        controls.target.copy(newPos)
      },distance / speed * 1000)
}

经过上面的处理,我们就完成了通过鼠标指定点击位置,让模型移动到该位置的动画操作。
核心代码如下,里面有比较详细的注释,不理解的小伙伴可以评论区讨论,今天就到这里吧,喜欢的小伙伴点赞关注加收藏哦!

// 跟随鼠标点击位置移动
function initMove() {
  let initPos = null
  let timer_interval = null
  let timer_timeout = null
  // 记录初始位置
  renderer.domElement.addEventListener('pointerdown',function(event) {
    event = event || window.event
    initPos = {
      x:event.clientX,
      y:event.clientY
    } 
    console.log(initPos);
  })
  // 移动实现
  renderer.domElement.addEventListener('pointerup',function(event) {
    event = event || window.event 
    if(!initPos || Math.abs(initPos.x - event.clientX) > 2 || Math.abs(initPos.y - event.clientY) > 2) return
    initPos = null
    // Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合, 即:是与该元素相关的CSS 边框集合。
    // 属性值:
    // top: 元素上边距离页面上边的距离
    // left: 元素左边距离页面左边的距离
    // right: 元素右边距离页面左边的距离
    // bottom: 元素下边距离页面上边的距离
    // width: 元素宽度
    // height: 元素高度

    // clientX/Y获取到的是触发点相对浏览器可视区域左上角距离,不随页面滚动而改变。
    // 触点坐标
    console.log(renderer.domElement.getBoundingClientRect());
    let x = event.clientX - renderer.domElement.getBoundingClientRect().left
    let y = event.clientY - renderer.domElement.getBoundingClientRect().top
    // let x = event.clientX  这里与上面的代码等同 renderer.domElement.getBoundingClientRect().left = 0
    // let y = event.clientY  这里与上面的代码等同 renderer.domElement.getBoundingClientRect().top = 0
    console.log(x,y);
    // canvas画布宽度
    let canvasWidth = renderer.domElement.clientWidth 
    // clientWidth 返回元素的可视宽度,包括内边距,但不包括边框、滚动条或外边距,以像素计
    // canvas画布高度
    // clientHeight 返回元素的可视高度,包括内边距,但不包括边框、滚动条或外边距,以像素计。
    let canvasHeight = renderer.domElement.clientHeight

    // offsetHeight 属性返回元素的可视高度(以像素为单位),包括内边距、边框和滚动条,但不包括外边距。
    // offsetWidth 属性返回元素的可视宽度(以像素为单位),包括内边距、边框和滚动条,但不包括外边距。
    // 坐标转换(归一化的值) 
    const sx = -1 + (x / canvasWidth) * 2 
    const sy = 1 - (y / canvasHeight) * 2
    // 光线投射器
    const rayCaster = new THREE.Raycaster()
    // 通过摄像机和鼠标位置更新射线
    rayCaster.setFromCamera(new THREE.Vector2(sx,sy),camera)
    const intersects = rayCaster.intersectObjects([water]) 
    // 拾取点击点坐标
    if(intersects.length > 0) {
      const newPos = intersects[0].point //新位置(鼠标点击时射线与water相交点的坐标)
      const originPos = yacht.position.clone() //老位置(模型的坐标)
      const cameraOriginPos = camera.position.clone() //相机老位置
      const clock = new THREE.Clock()
      const speed = 100 //移动速度
      // 向量减法.sub()和向量长度.length()
      // 两个向量相减就是求它们的差向量,其结果是以减向量的终点为起点,被减向量的终点为终点的向量
      // 几何意义:向量a,向量b相减,理解为以b的终点为始点,以a的终点为终点的向量,方向由b指向a (指向被减数)
      // 通过.sub()方法可以对两个向量进行减法运算,比如两个表示顶点坐标的Vector3对象进行减法运算返回一个新的Vector3对象就是两个点构成的向量。
      // 向量对象执行.length()方法会返回向量的长度。
      const vector = newPos.clone().sub(originPos)       
      const distance = vector.length() 
      // 修正方向 .setFromUnitVectors将该四元数设置为从方向向量 vFrom 旋转到方向向量 vTo 所需的旋转。
      // vector.clone().normalize() 将该向量转换为单位向量
      const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,0,-1),vector.clone().normalize())
      yacht.quaternion.copy(quaternion)

      timer_interval && clearInterval(timer_interval) //清楚timer
      timer_timeout && clearInterval(timer_timeout)

      //更新位置
      timer_interval = setInterval(()=>{
        // 通过speed和clock.getElapsedTime()失去时间的乘积,得到每次时间间隔移动的长度
        const moveLength = clock.getElapsedTime()*speed
        // .multiplyScalar将该向量与所传入的标量s进行相乘。
        // 通过每次时间间隔移动的长度除以向量总长度,获取每次间隔移动的比值
        // 将该比值与重点向量相乘,得到每次间隔移动的向量坐标
        const moveVector = vector.clone().multiplyScalar(moveLength / distance)
        // 将上面得到的每次间隔移动的向量坐标与模型原始位置向量坐标相加,得到模型每次时间间隔移动的重点向量坐标
        const movePos = originPos.clone().add(moveVector) //将模型的坐标向量与moveVector相加
        // 将每次间隔移动的终点坐标复制给模型的position属性
        yacht.position.copy(movePos)
        // 将每次间隔移动的向量坐标与相机原始坐标相加得到每次时间间隔移动的终点坐标,并将给坐标复制给相机的position属性
        camera.position.copy(cameraOriginPos.clone().add(moveVector))
        // 将每次时间间隔的终点坐标movePos复制给控制器的target属性,使其始终朝向movePos位置
        controls.target.copy(movePos)
      },50)

      // 清定时器,并最终定位
      timer_timeout = setTimeout(()=>{
        clearInterval(timer_interval)
        yacht.position.copy(newPos)
        controls.target.copy(newPos)

      },distance / speed * 1000)
      // console.log(newPos);
    }

    
  })
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/484362.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2 ROS2话题通讯基础(2)

ROS2话题通讯基础 2.5 自定义话题通讯2.5.1 自定义话题通讯的一般流程2.5.2 创建自定义话题消息简单例子2.5.3 快速创建C/C和Python自定义话题通讯的Student.msg 2.6 使用C/C实现自定义话题通讯2.6.1 创建C/C自定义话题发布方功能包并编写节点文件2.6.2 配置C/C自定义话题发布方…

如何选择适合企业的网盘?必须要考虑这几个方面

随着云存储技术的发展,传统的文件存储服务已逐渐不能满足企业日益增长的文件应用、共享和存储需求。越来越多的企业开始将目光转移到企业网盘上。 在选择企业网盘工具时,比较重要的有两个方面,一个是数据的安全性,一个是协同办公。…

Java 将增加虚拟线程,挑战 Go 协程

Java19 正式发布,带来了一个 Java 开发者垂涎已久的新特性 —— 虚拟线程。在 Java 有这个新特性之前,Go 语言的协程风靡已久,在并发编程领域可以说是叱咤风云。随着国内 Go 语言的快速发展与推广,协程好像成为了一个世界上最好语…

K8S二进制单节点 一键部署K8S_V1.21.x

1、安装前注意事项 安装shell脚本在文章最后位置 1、提前配置静态IP 把脚本的IP 192.168.1.31 换成你的IP 2、创建安装包路径 /home/software/shell 所有的tar包 shell脚本 放在这里 3、免密登录配置所有节点 提前下载镜像如下: [rootmaster01 ~]# docker image…

Ubuntu搜狗输入法安装指南

Ubuntu搜狗输入法安装指南 Ubuntu搜狗输入法安装指南搜狗输入法已支持Ubuntu1604、1804、1910、2004、2010Ubuntu20.04及以上安装搜狗输入法步骤 Ubuntu搜狗输入法安装指南 下载地址:https://shurufa.sogou.com/ 计算为amd64的选择x86_64,以下教程来源…

ORBBEC(奥比中光)AstraPro相机在ROS2下的标定与D2C(配准与对齐)

文章目录 1.rgb、depth相机标定矫正1.1.标定rgb相机1.2.标定depth相机1.3.rgb、depth相机一起标定(效果重复了)1.4.取得标定结果1.4.1.得到的标定结果的意义:1.5.IR、RGB相机分别应用标定结果1.5.1.openCV应用标定结果1.5.2.ros2工程应用标定…

[stable-diffusion-art] 指北-2 如何为sd提出好的prompt

https://stable-diffusion-art.com/how-to-come-up-with-good-prompts-for-ai-image-generation/https://stable-diffusion-art.com/how-to-come-up-with-good-prompts-for-ai-image-generation/1.prompt可以促使模型生成以前不存在的高质量的图片,例如:…

windows如何使用脚本打开多个软件

文章目录 windows如何使用脚本打开多个软件问题缘由省流版本制作脚本步骤新建文本找到文件的安装位置方法一:方法二: 总结 windows如何使用脚本打开多个软件 问题缘由 因为强迫症,不想让软件自启,会导致开机变慢,电脑…

Lecture7 处理多维特征的输入(Multiple Dimension Input)

以实际代码出发,逐行讲解。 完整代码: import numpy as np import torch import matplotlib.pyplot as plt# load data xy np.loadtxt(C:\\Users\\14185\\Desktop\\diabetes.csv, delimiter,, dtypenp.float32) x_data torch.from_numpy(xy[:, :-1])…

226. 翻转二叉树【58】

难度等级:容易 上一篇算法: 543. 二叉树的直径【71】 力扣此题地址: 226. 翻转二叉树 - 力扣(Leetcode) 1.题目:226. 翻转二叉树 给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返…

DAY 52 LVS+Keepalived群集

Keepalived工具介绍 普通集群容易出现的问题 企业应用中,单台服务器承担应用存在单点故障的危险。 单点故障一旦发生, 企业服务将发生中断,造成极大的危害。 Keepalived工具 Keepalived 是一个基于VRRP协议来实现的LVS服务高可用方案&…

v2c - 从Verilog 转换到 C语言的工具

文章目录 一、如何安装1.下载二进制文件2.基准测试 二、如何使用v2c的应用描述工具流程使用 v2c 转换器的工作示例 三、注意事项情形一:拼接:{4{x}}情形1-1 y&{x,x,x,x}情形1-2 y&{x,x,…

【C++】string 类的实现

目录 构造函数赋值重载关于浅拷贝 迭代器容量相关reserveresize 修改push_backappendinserterase关于npos 流运算符重载流插入流提取 构造函数 无参数构造和传参构造 通过对参数设置缺省值为空串""同时满足无参构造和传参构造成员 _size 和 _capacity 均是针对有效…

自动驾驶—连续系统LQR最优控制的黎卡提方程推导

1. Why use the Riccati equation? 最优控制算法LQR是Linear Quadratic Regulator的缩写,Q、R就是需要设计的半正定矩阵和正定矩阵。考虑根据实车的情况去标定此参数,从理论和工程层面去理解,如果增大Q、减小R,则此时控制系统响应速度比较快速(比较剧烈),直观反映方向…

5月1日 9H45min|5.2 8H20min+30min|时间轴复盘

8:00 起床 8:00-8:30 洗漱吃饭 8:30-10:40 temporary pools阅读真题精读 (真的很慢了 不知道什么原因 感觉也没有彻底完全弄懂)【2h+10min】 10:40-11:10 午餐+酸奶(423+174KJ) 11:20-12:30 三篇阅读【1h+10min】 13:10-14:50 健身 14:50-15:45诵默写list…

Ae:画笔工具

画笔工具 Brush Tool 快捷键:Ctrl B 画笔工具 Brush Tool仅能工作在图层 Layer面板上。 双击纯色图层、像素图层等可打开图层面板。 在 Ae 中的每次画笔绘制都将新建一条路径,然后通过对路径的描边来显示绘制结果,故又称为“绘画描边”或“…

函数-实现交换两个变量的内容

用函数实现交换两个变量的内容&#xff0c;对于该问题我们该如何实现呢&#xff1f;在这里我就用整型变量来说明。 题目&#xff1a;写一个函数可以交换两个整形变量的内容。 我们先来看看如下代码&#xff1a; #include <stdio.h> void swap(int x, int y) {int tem…

Android进阶之光:Dagger2原理简要分析

Dagger2注入框架原理简要分析 使用Dagger2需要的依赖: implementation com.google.dagger:dagger-android:2.46 implementation com.google.dagger:dagger-android-support:2.46 annotationProcessor com.google.dagger:dagger-android-processor:2.46 annotationProcessor c…

第二十七章 碰撞体Collision(下)

本章节我们继续研究碰撞体&#xff0c;并且探索一下碰撞体与刚体之间的联系。我们回到之前的工程&#xff0c;然后给我们的紫色球体Sphere1也添加一个刚体组件。如下所示 此时&#xff0c;两个球体都具备了碰撞体和刚体组件。接下来&#xff0c;我们Play运行查看效果 我们发现&…

从零开始带你开发橙光游戏AVG框架(仿 葬花 )

来源 从零开始带你开发橙光游戏AVG框架【55课数 收费】 从零开始带你开发橙光游戏AVG框架 unity教程【16课数 免费】 。。。。。。 挺大的&#xff0c;因为很多音频&#xff0c;.git就有 2.6G AVG_20230413_2020.2.23f1c1 介绍 QuickSheet使用 bug 包报错 可能是我换了un…