Three 之 three.js (webgl)鼠标/手指通过射线移动物体的简单整理封装

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

<!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/[email protected]/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

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
	}
}

猜你喜欢

转载自blog.csdn.net/u014361280/article/details/128190669