Three 之 three.js (webgl)鼠标/手指通过射线移动物体的简单整理封装
目录
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文章来源:https://www.toymoban.com/news/detail-577567.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>30TestTouchRaycasterMoveObject</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - dashed lines example
</div>
<div id="container"></div>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "../../../build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import Stats from '../../jsm/libs/stats.module.js';
import * as GeometryUtils from '../../jsm/utils/GeometryUtils.js';
import {
OrbitControls
} from './../../jsm/controls/OrbitControls.js';
import {
DashLinesBoxTool
} from './DashLinesBoxTool.js'
import {RayCasterMoveObjectsWrapper} from "./RayCasterMoveObjectsWrapper.js"
let renderer, scene, camera, stats, controls;;
const moveObjectsMeshArray = [];
const floorArray = []
const WIDTH = window.innerWidth,
HEIGHT = window.innerHeight;
let mRayCasterMoveObjectsWrapper = null
init();
animate();
function init() {
// 相机
camera = new THREE.PerspectiveCamera(60, WIDTH / HEIGHT, 1, 200);
camera.position.z = 10;
camera.position.y = 10;
// 场景 scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
scene.fog = new THREE.Fog(0x111111, 150, 200);
// 渲染
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(WIDTH, HEIGHT);
// 添加到 html 中
const container = document.getElementById('container');
container.appendChild(renderer.domElement);
// 性能监测
stats = new Stats();
container.appendChild(stats.dom);
// 控制相机场景中的轨道控制器
controls = new OrbitControls(camera, renderer.domElement);
// 窗口变化监控
window.addEventListener('resize', onWindowResize);
createLight();
// 测试添加各种几何体绘制尺寸虚线框
createObjects();
// 添加射线移动物体功能
AddRayCasterMoveObjectsFunc();
}
// 绘制立方体
function createObjects() {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00
});
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 0.5, 0);
scene.add(cube);
moveObjectsMeshArray.push(cube);
const material1 = new THREE.MeshPhongMaterial({
color: 0x00ffff
});
const cube1 = new THREE.Mesh(geometry, material1);
cube1.position.set(4, 0.5, 0);
scene.add(cube1);
moveObjectsMeshArray.push(cube1);
const material2 = new THREE.MeshPhongMaterial({
color: 0xff0000
});
const cube2 = new THREE.Mesh(geometry, material2);
cube2.position.set(-4, 0.5, 0);
scene.add(cube2);
moveObjectsMeshArray.push(cube2);
const geometryP = new THREE.PlaneGeometry(10, 10, 10);
const materialP = new THREE.MeshPhongMaterial({
color: 0xffffff
});
const plane = new THREE.Mesh(geometryP, materialP);
plane.position.set(0, 0, 0);
plane.rotation.x = -Math.PI/2;
scene.add(plane);
floorArray.push(plane);
}
// 创建光源
function createLight() {
// 添加环境光
scene.add( new THREE.AmbientLight( 0x222222 ) );
// 添加方向光
const light = new THREE.DirectionalLight( 0xffffff );
light.position.set( 1, 1, 1 );
scene.add( light );
}
// 添加射线移动物体功能
function AddRayCasterMoveObjectsFunc(){
mRayCasterMoveObjectsWrapper =
new RayCasterMoveObjectsWrapper (moveObjectsMeshArray, container, floorArray,()=>{
// 移动开始的时候,停止OrbitControls轨道控制器功能
controls.enabled = false;
},
()=>{
// 移动结束的时候,使能OrbitControls轨道控制器功能
controls.enabled = true;
});
// 使能点击移动功能
mRayCasterMoveObjectsWrapper.enableMouseMoveObjs();
}
// 窗口尺寸变化监听
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 动画
function animate() {
requestAnimationFrame(animate);
render();
stats.update();
controls.update()
// 实时监听射线移动功能
mRayCasterMoveObjectsWrapper?.raycaseterUpdate(camera);
}
// 渲染
function render() {
renderer.render(scene, camera);
}
</script>
</body>
</html>
2、RayCasterMoveObjectsWrapper.js文章来源地址https://www.toymoban.com/news/detail-577567.html
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
}
}
到了这里,关于Three 之 three.js (webgl)鼠标/手指通过射线移动物体的简单整理封装的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!