上一节中我们实现了物体沿指定轨迹移动的动画效果,这一节我们来实现让模型移动到鼠标点击的制定位置的动画效果。
先看下实现后的最终效果
要实现上面的动画效果,我们需要通过以下步骤来实现
第一步,监听鼠标事件
我们需要监听鼠标的点击事件,获取鼠标点击点相对浏览器可视区域左上角的距离,通过监听“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);
}
})
}