赞
踩
目录
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/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
- 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
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。