目录
Three 之 three.js (webgl)鼠标/手指通过射线移动物体的简单整理封装
一、简单介绍
二、实现原理
三、注意事项
四、效果预览
五、案例实现步骤
六、关键代码
Three js 开发的一些知识整理,方便后期遇到类似的问题,能够及时查阅使用。
本节介绍, three.js (webgl) 中,PC 端移动通过鼠标移动物体,移动端通过手指交互移动物体的整理,主要是通过对应的touchstart、touchmove、touchend ,以及 Threejs 中的 Raycaster 。其中,如果有不足之处,欢迎指出,或者你有更好的方法,欢迎留言。
1、touchstart 点击屏幕,发射射线,选择物体
2、旋转对应移动物体,touchmove 中,发射射线,与地面交点的动态变化量,作为移动物体移动的移动量
3、touchend 取消物体选中,物体移动结束
1、这里简单封装的是与地面交互移动,主要移动物体的 x 和 z 的值
2、其中,也添加了物体开始移动,和结束移动的事件,可以根据需要,在移动开始和结束的时候添加对应的事件处理
3、注意如果Threejs 渲染窗口不是全屏,需要注意 touch 触控点转换到对应的 container 中,作为Threejs 中的射线发射点
1、为了方便学习,这里是基于 Github 代码,进行开发的,大家可以下载官网代码,很多值得学习的案例
GitHub - mrdoob/three.js: JavaScript 3D Library.
gitcode:mirrors / mrdoob / three.js · GitCode
2、在上面的基础上,添加一个 html ,用来实现案例效果,引入相关包
3、初始化构建 3D 场景
4、其中, 场景中添加3个移动的 cube 、和一个地面 plane
5、添加场景移动物体功能RayCasterMoveObjectsWrapper,然后把要移动和Cube组、交互移动的地面,开始结束移动事件传入,还有 Threejs 渲染的容器 container
6、并且在 animation 中 Update 更新移动
7、一切准备好,运行场景、效果如下
1、TestTouchRaycasterMoveObject.html
30TestTouchRaycasterMoveObject
three.js - dashed lines example
2、RayCasterMoveObjectsWrapper.js
import {
Raycaster,
BufferGeometry,
Line,
LineBasicMaterial,
Vector3
} from 'three'
/*
* 射线移动物体封装
* PC 端鼠标操作移动,移动端手指点击操作移动
* 使用说明
* 1、new 创建 RayCasterMoveObjectsWrapper 实例
* 2、disableMouseMoveObjs 使能移动功能,disableMouseMoveObjs 禁用移动功能
* 3、raycaseterUpdate 在 Updata 中实时监听移动
*/
export class RayCasterMoveObjectsWrapper {
/**
* 构造函数
* moveObjsDataArray 要移动的物体数组
* container threejs渲染的 容器
* floorArray 物体移动交互的地面
* onMoveStart 开始移动的事件
* onMoveEnd 移动结束的事件
*/
constructor(moveObjsDataArray, container, floorArray = [], onMoveStart = null, onMoveEnd = null) {
this.container = container;
this.raycaster;
// 开始把初始的鼠标位置设置到屏幕外,避免干扰
this.mouse = {
x: -10000,
y: -10000
}
this.INTERSECTED;
this.moveObjsDataArray = moveObjsDataArray
this.isCanMoveObject = false;
this.curMouseX = null;
this.curMouseY = null;
this.onDocumentTouchStart = null
this.onDocumentTouchEnd = null
this.onDocumentTouchMove = null
this.isEnableMoveObjs = false
// 特殊处理的 Dining -table
this.diningTable = null
this.diningChairArray = []
this.floor = null
// 使用射线进行移动家具
this.raycasterForMove = null
this.floorArray = floorArray // 射线交互的地板
// 移动事件
this.mOnMoveStart = onMoveStart
this.mOnMoveEnd = onMoveEnd
this.initRayccaster();
}
/**
* 使能射线移动功能
*/
enableMouseMoveObjs() {
this.isEnableMoveObjs = true
this.container.addEventListener('touchmove', this.onDocumentTouchMove, true);
this.container.addEventListener('touchstart', this.onDocumentTouchStart, true);
this.container.addEventListener('touchend', this.onDocumentTouchEnd, true);
}
/**
* 禁用射线移动功能
*/
disableMouseMoveObjs() {
this.container.removeEventListener('touchmove', this.onDocumentTouchMove, true);
this.container.removeEventListener('touchstart', this.onDocumentTouchStart, true);
this.container.removeEventListener('touchend', this.onDocumentTouchEnd, true);
this.isEnableMoveObjs = false
}
/**
* 射线移动的更新函数
* @param {Object} curCamera 当前场景的相机
*/
raycaseterUpdate(curCamera) {
if (this.isEnableMoveObjs === false) {
return
}
if (this.isCanMoveObject === false) {
return
}
this.raycasterSelectObject(curCamera)
this.raycasterMoveObject(curCamera)
}
/**
* 初始化射线
*/
initRayccaster() {
// 创建射线
this.raycaster = new Raycaster();
this.raycasterForMove = new Raycaster();
this.initTouchMoveFunction();
}
/**
* 初始化移动交互事件
*/
initTouchMoveFunction() {
this.onDocumentTouchMove = (event) => {
var touch = event.touches[0];
// 取消默认动作
// event.preventDefault();
// 数值归一化 介于 -1 与 1之间 这是一个固定公式
if (this.container == null) {
// 全屏幕的
this.mouse.x = (parseInt(touch.pageX) / window.innerWidth) * 2 - 1;
this.mouse.y = -(parseInt(touch.pageY) / window.innerHeight) * 2 + 1;
} else {
// 局部的
this.mouse.x = ((parseInt(touch.pageX) - this.container.offsetLeft) / this.container
.clientWidth) * 2 - 1
this.mouse.y = -((parseInt(touch.pageY) - this.container.offsetTop) / this.container
.clientHeight) * 2 + 1
}
if (this.isCanMoveObject && this.INTERSECTED) {
// let x = parseInt(touch.pageX) - this.curMouseX
// let z = parseInt(touch.pageY)- this.curMouseY
// let smooth =2
if (this.curMouseX === null || this.curMouseY === null) {
if (this.floorRaycasterInfo) {
this.curMouseX = this.floorRaycasterInfo.point.x
this.curMouseY = this.floorRaycasterInfo.point.z
}
// console.error("ddd xxx ")
return
}
let x = 0
let z = 0
if (this.floorRaycasterInfo) {
x = this.floorRaycasterInfo.point.x - this.curMouseX
z = this.floorRaycasterInfo.point.z - this.curMouseY
}
let smooth = 1.0
if (this.INTERSECTED !== null) {
this.INTERSECTED.position.x += x * smooth
this.INTERSECTED.position.z += z * smooth
}
if (this.floorRaycasterInfo) {
this.curMouseX = this.floorRaycasterInfo.point.x
this.curMouseY = this.floorRaycasterInfo.point.z
}
}
};
this.onDocumentTouchStart = (event) => {
// 取消默认动作
event.preventDefault();
var touch = event.touches[0];
if (this.container == null) {
// 全屏幕的
this.mouse.x = (parseInt(touch.pageX) / window.innerWidth) * 2 - 1;
this.mouse.y = -(parseInt(touch.pageY) / window.innerHeight) * 2 + 1;
} else {
// 局部的
this.mouse.x = ((parseInt(touch.pageX) - this.container.offsetLeft) / this.container
.clientWidth) * 2 - 1
this.mouse.y = -((parseInt(touch.pageY) - this.container.offsetTop) / this.container
.clientHeight) * 2 + 1
}
this.isCanMoveObject = true
// console.log('dddd move down ')
};
this.onDocumentTouchEnd = (event) => {
// 取消默认动作
event.preventDefault();
// console.log('dddd move up ')
this.mouse = {
x: -10000,
y: -10000
}
this.curMouseX = null;
this.curMouseY = null;
this.isCanMoveObject = false
// 恢复上一个对象颜色并置空变量
if (this.INTERSECTED) this.INTERSECTED.material.color.setHex(this.INTERSECTED.currentHex);
this.INTERSECTED = null;
// 恢复上一个对象颜色并置空变量
if (this.floor) this.floor.material.color.setHex(this.floor.currentHex);
this.floor = null;
if (this.mOnMoveEnd) {
this.mOnMoveEnd()
}
};
}
/**
* 射线旋转移动物体功能
* @param {Object} curCamera
*/
raycasterSelectObject(curCamera) {
this.raycaster.setFromCamera(this.mouse, curCamera);
/**
* intersectObjects 检测所有在射线与这些物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个),相交部分和.intersectObject所返回的格式是相同的。
* objects —— 检测和射线相交的一组物体。
* recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为false。
* optionalTarget —— (可选)(可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。
*/
// var intersects = raycaster.intersectObjects( scene.children );
var intersects = this.raycaster.intersectObjects(this.moveObjsDataArray);
if (intersects.length > 0) {
if (this.INTERSECTED != intersects[0].object) {
//emissive:该材质发射的属性
if (this.INTERSECTED) this.INTERSECTED.material.color.setHex(this.INTERSECTED.currentHex);
// 记录当前对象
this.INTERSECTED = intersects[0].object;
// 记录当前对象本身颜色
this.INTERSECTED.currentHex = this.INTERSECTED.material.color.getHex();
// 设置颜色为红色
this.INTERSECTED.material.color.setHex(0xffff00);
// console.log(" INTERSECTED ", this.INTERSECTED)
// console.log(" intersects[ 0 ] ", intersects[ 0 ])
if (this.mOnMoveStart) {
this.mOnMoveStart()
}
}
} else {
// 恢复上一个对象颜色并置空变量
// if ( this.INTERSECTED ) this.INTERSECTED.material.color.setHex( this.INTERSECTED.currentHex );
// this.INTERSECTED = null;
}
}
/**
* 射线移动物体
* @param {Object} curCamera
*/
raycasterMoveObject(curCamera) {
this.raycaster.setFromCamera(this.mouse, curCamera);
/**
* intersectObjects 检测所有在射线与这些物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个),相交部分和.intersectObject所返回的格式是相同的。
* objects —— 检测和射线相交的一组物体。
* recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为false。
* optionalTarget —— (可选)(可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。
*/
// var intersects = raycaster.intersectObjects( scene.children );
var intersects = this.raycaster.intersectObjects(this.floorArray);
if (intersects.length > 0) {
if (this.floor != intersects[0].object) {
//emissive:该材质发射的属性
if (this.floor) this.floor.material.color.setHex(this.floor.currentHex);
// 记录当前对象
this.floor = intersects[0].object;
// 记录当前对象本身颜色
this.floor.currentHex = this.floor.material.color.getHex();
// 设置颜色为红色
this.floor.material.color.setHex(0xff00ff);
// console.log(" intersects.point ", intersects[0].point)
}
this.floorRaycasterInfo = intersects[0]
} else {
// 恢复上一个对象颜色并置空变量
if (this.floor) this.floor.material.color.setHex(this.floor.currentHex);
this.floor = null;
this.floorRaycasterInfo = null
}
}
/**
* 射线辅助线
* @constructor
*/
rayLinePaintHelper(scene) {
const ori = this.raycaster.ray.origin
const dir = this.raycaster.ray.direction
const dirT = new Vector3(
ori.x + dir.x * 10000,
ori.y + dir.y * 10000,
ori.z + dir.z * 10000
)
// console.log(' mRaycaster.dirT ' + dirT.x + ' ' + dirT.y)
const material = new LineBasicMaterial({
color: 0x0000ff,
})
const points = []
points.push(ori)
points.push(dirT)
const geometry = new BufferGeometry().setFromPoints(points)
const line = new Line(geometry, material)
scene.add(line)
}
/**
* 资源释放
*/
dispose() {
this.container = null
this.raycaster = null
this.mouse = null
this.INTERSECTED = null
this.canMoveObjectArray = null
this.dashLinesBoxArray = null
this.diningTable = null
this.diningChairArray = null
this.isCanMoveObject = false
this.curMouseX = 0
this.curMouseY = 0
}
}