当前位置:   article > 正文

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

three鼠标画线

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.

gitcodemirrors / mrdoob / three.js · GitCode

2、在上面的基础上,添加一个 html ,用来实现案例效果,引入相关包     

3、初始化构建 3D 场景

4、其中, 场景中添加3个移动的 cube 、和一个地面 plane

 5、添加场景移动物体功能RayCasterMoveObjectsWrapper,然后把要移动和Cube组、交互移动的地面,开始结束移动事件传入,还有 Threejs 渲染的容器 container

6、并且在 animation 中 Update 更新移动

 7、一切准备好,运行场景、效果如下

六、关键代码

1、TestTouchRaycasterMoveObject.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>30TestTouchRaycasterMoveObject</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  7. <link type="text/css" rel="stylesheet" href="main.css">
  8. </head>
  9. <body>
  10. <div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - dashed lines example
  11. </div>
  12. <div id="container"></div>
  13. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. <script type="module">
  24. import * as THREE from 'three';
  25. import Stats from '../../jsm/libs/stats.module.js';
  26. import * as GeometryUtils from '../../jsm/utils/GeometryUtils.js';
  27. import {
  28. OrbitControls
  29. } from './../../jsm/controls/OrbitControls.js';
  30. import {
  31. DashLinesBoxTool
  32. } from './DashLinesBoxTool.js'
  33. import {RayCasterMoveObjectsWrapper} from "./RayCasterMoveObjectsWrapper.js"
  34. let renderer, scene, camera, stats, controls;;
  35. const moveObjectsMeshArray = [];
  36. const floorArray = []
  37. const WIDTH = window.innerWidth,
  38. HEIGHT = window.innerHeight;
  39. let mRayCasterMoveObjectsWrapper = null
  40. init();
  41. animate();
  42. function init() {
  43. // 相机
  44. camera = new THREE.PerspectiveCamera(60, WIDTH / HEIGHT, 1, 200);
  45. camera.position.z = 10;
  46. camera.position.y = 10;
  47. // 场景 scene
  48. scene = new THREE.Scene();
  49. scene.background = new THREE.Color(0x111111);
  50. scene.fog = new THREE.Fog(0x111111, 150, 200);
  51. // 渲染
  52. renderer = new THREE.WebGLRenderer({
  53. antialias: true
  54. });
  55. renderer.setPixelRatio(window.devicePixelRatio);
  56. renderer.setSize(WIDTH, HEIGHT);
  57. // 添加到 html 中
  58. const container = document.getElementById('container');
  59. container.appendChild(renderer.domElement);
  60. // 性能监测
  61. stats = new Stats();
  62. container.appendChild(stats.dom);
  63. // 控制相机场景中的轨道控制器
  64. controls = new OrbitControls(camera, renderer.domElement);
  65. // 窗口变化监控
  66. window.addEventListener('resize', onWindowResize);
  67. createLight();
  68. // 测试添加各种几何体绘制尺寸虚线框
  69. createObjects();
  70. // 添加射线移动物体功能
  71. AddRayCasterMoveObjectsFunc();
  72. }
  73. // 绘制立方体
  74. function createObjects() {
  75. const geometry = new THREE.BoxGeometry(1, 1, 1);
  76. const material = new THREE.MeshPhongMaterial({
  77. color: 0x00ff00
  78. });
  79. const cube = new THREE.Mesh(geometry, material);
  80. cube.position.set(0, 0.5, 0);
  81. scene.add(cube);
  82. moveObjectsMeshArray.push(cube);
  83. const material1 = new THREE.MeshPhongMaterial({
  84. color: 0x00ffff
  85. });
  86. const cube1 = new THREE.Mesh(geometry, material1);
  87. cube1.position.set(4, 0.5, 0);
  88. scene.add(cube1);
  89. moveObjectsMeshArray.push(cube1);
  90. const material2 = new THREE.MeshPhongMaterial({
  91. color: 0xff0000
  92. });
  93. const cube2 = new THREE.Mesh(geometry, material2);
  94. cube2.position.set(-4, 0.5, 0);
  95. scene.add(cube2);
  96. moveObjectsMeshArray.push(cube2);
  97. const geometryP = new THREE.PlaneGeometry(10, 10, 10);
  98. const materialP = new THREE.MeshPhongMaterial({
  99. color: 0xffffff
  100. });
  101. const plane = new THREE.Mesh(geometryP, materialP);
  102. plane.position.set(0, 0, 0);
  103. plane.rotation.x = -Math.PI/2;
  104. scene.add(plane);
  105. floorArray.push(plane);
  106. }
  107. // 创建光源
  108. function createLight() {
  109. // 添加环境光
  110. scene.add( new THREE.AmbientLight( 0x222222 ) );
  111. // 添加方向光
  112. const light = new THREE.DirectionalLight( 0xffffff );
  113. light.position.set( 1, 1, 1 );
  114. scene.add( light );
  115. }
  116. // 添加射线移动物体功能
  117. function AddRayCasterMoveObjectsFunc(){
  118. mRayCasterMoveObjectsWrapper =
  119. new RayCasterMoveObjectsWrapper (moveObjectsMeshArray, container, floorArray,()=>{
  120. // 移动开始的时候,停止OrbitControls轨道控制器功能
  121. controls.enabled = false;
  122. },
  123. ()=>{
  124. // 移动结束的时候,使能OrbitControls轨道控制器功能
  125. controls.enabled = true;
  126. });
  127. // 使能点击移动功能
  128. mRayCasterMoveObjectsWrapper.enableMouseMoveObjs();
  129. }
  130. // 窗口尺寸变化监听
  131. function onWindowResize() {
  132. camera.aspect = window.innerWidth / window.innerHeight;
  133. camera.updateProjectionMatrix();
  134. renderer.setSize(window.innerWidth, window.innerHeight);
  135. }
  136. // 动画
  137. function animate() {
  138. requestAnimationFrame(animate);
  139. render();
  140. stats.update();
  141. controls.update()
  142. // 实时监听射线移动功能
  143. mRayCasterMoveObjectsWrapper?.raycaseterUpdate(camera);
  144. }
  145. // 渲染
  146. function render() {
  147. renderer.render(scene, camera);
  148. }
  149. </script>
  150. </body>
  151. </html>

2、RayCasterMoveObjectsWrapper.js

  1. import {
  2. Raycaster,
  3. BufferGeometry,
  4. Line,
  5. LineBasicMaterial,
  6. Vector3
  7. } from 'three'
  8. /*
  9. * 射线移动物体封装
  10. * PC 端鼠标操作移动,移动端手指点击操作移动
  11. * 使用说明
  12. * 1、new 创建 RayCasterMoveObjectsWrapper 实例
  13. * 2、disableMouseMoveObjs 使能移动功能,disableMouseMoveObjs 禁用移动功能
  14. * 3、raycaseterUpdate 在 Updata 中实时监听移动
  15. */
  16. export class RayCasterMoveObjectsWrapper {
  17. /**
  18. * 构造函数
  19. * moveObjsDataArray 要移动的物体数组
  20. * container threejs渲染的 容器
  21. * floorArray 物体移动交互的地面
  22. * onMoveStart 开始移动的事件
  23. * onMoveEnd 移动结束的事件
  24. */
  25. constructor(moveObjsDataArray, container, floorArray = [], onMoveStart = null, onMoveEnd = null) {
  26. this.container = container;
  27. this.raycaster;
  28. // 开始把初始的鼠标位置设置到屏幕外,避免干扰
  29. this.mouse = {
  30. x: -10000,
  31. y: -10000
  32. }
  33. this.INTERSECTED;
  34. this.moveObjsDataArray = moveObjsDataArray
  35. this.isCanMoveObject = false;
  36. this.curMouseX = null;
  37. this.curMouseY = null;
  38. this.onDocumentTouchStart = null
  39. this.onDocumentTouchEnd = null
  40. this.onDocumentTouchMove = null
  41. this.isEnableMoveObjs = false
  42. // 特殊处理的 Dining -table
  43. this.diningTable = null
  44. this.diningChairArray = []
  45. this.floor = null
  46. // 使用射线进行移动家具
  47. this.raycasterForMove = null
  48. this.floorArray = floorArray // 射线交互的地板
  49. // 移动事件
  50. this.mOnMoveStart = onMoveStart
  51. this.mOnMoveEnd = onMoveEnd
  52. this.initRayccaster();
  53. }
  54. /**
  55. * 使能射线移动功能
  56. */
  57. enableMouseMoveObjs() {
  58. this.isEnableMoveObjs = true
  59. this.container.addEventListener('touchmove', this.onDocumentTouchMove, true);
  60. this.container.addEventListener('touchstart', this.onDocumentTouchStart, true);
  61. this.container.addEventListener('touchend', this.onDocumentTouchEnd, true);
  62. }
  63. /**
  64. * 禁用射线移动功能
  65. */
  66. disableMouseMoveObjs() {
  67. this.container.removeEventListener('touchmove', this.onDocumentTouchMove, true);
  68. this.container.removeEventListener('touchstart', this.onDocumentTouchStart, true);
  69. this.container.removeEventListener('touchend', this.onDocumentTouchEnd, true);
  70. this.isEnableMoveObjs = false
  71. }
  72. /**
  73. * 射线移动的更新函数
  74. * @param {Object} curCamera 当前场景的相机
  75. */
  76. raycaseterUpdate(curCamera) {
  77. if (this.isEnableMoveObjs === false) {
  78. return
  79. }
  80. if (this.isCanMoveObject === false) {
  81. return
  82. }
  83. this.raycasterSelectObject(curCamera)
  84. this.raycasterMoveObject(curCamera)
  85. }
  86. /**
  87. * 初始化射线
  88. */
  89. initRayccaster() {
  90. // 创建射线
  91. this.raycaster = new Raycaster();
  92. this.raycasterForMove = new Raycaster();
  93. this.initTouchMoveFunction();
  94. }
  95. /**
  96. * 初始化移动交互事件
  97. */
  98. initTouchMoveFunction() {
  99. this.onDocumentTouchMove = (event) => {
  100. var touch = event.touches[0];
  101. // 取消默认动作
  102. // event.preventDefault();
  103. // 数值归一化 介于 -1 与 1之间 这是一个固定公式
  104. if (this.container == null) {
  105. // 全屏幕的
  106. this.mouse.x = (parseInt(touch.pageX) / window.innerWidth) * 2 - 1;
  107. this.mouse.y = -(parseInt(touch.pageY) / window.innerHeight) * 2 + 1;
  108. } else {
  109. // 局部的
  110. this.mouse.x = ((parseInt(touch.pageX) - this.container.offsetLeft) / this.container
  111. .clientWidth) * 2 - 1
  112. this.mouse.y = -((parseInt(touch.pageY) - this.container.offsetTop) / this.container
  113. .clientHeight) * 2 + 1
  114. }
  115. if (this.isCanMoveObject && this.INTERSECTED) {
  116. // let x = parseInt(touch.pageX) - this.curMouseX
  117. // let z = parseInt(touch.pageY)- this.curMouseY
  118. // let smooth =2
  119. if (this.curMouseX === null || this.curMouseY === null) {
  120. if (this.floorRaycasterInfo) {
  121. this.curMouseX = this.floorRaycasterInfo.point.x
  122. this.curMouseY = this.floorRaycasterInfo.point.z
  123. }
  124. // console.error("ddd xxx ")
  125. return
  126. }
  127. let x = 0
  128. let z = 0
  129. if (this.floorRaycasterInfo) {
  130. x = this.floorRaycasterInfo.point.x - this.curMouseX
  131. z = this.floorRaycasterInfo.point.z - this.curMouseY
  132. }
  133. let smooth = 1.0
  134. if (this.INTERSECTED !== null) {
  135. this.INTERSECTED.position.x += x * smooth
  136. this.INTERSECTED.position.z += z * smooth
  137. }
  138. if (this.floorRaycasterInfo) {
  139. this.curMouseX = this.floorRaycasterInfo.point.x
  140. this.curMouseY = this.floorRaycasterInfo.point.z
  141. }
  142. }
  143. };
  144. this.onDocumentTouchStart = (event) => {
  145. // 取消默认动作
  146. event.preventDefault();
  147. var touch = event.touches[0];
  148. if (this.container == null) {
  149. // 全屏幕的
  150. this.mouse.x = (parseInt(touch.pageX) / window.innerWidth) * 2 - 1;
  151. this.mouse.y = -(parseInt(touch.pageY) / window.innerHeight) * 2 + 1;
  152. } else {
  153. // 局部的
  154. this.mouse.x = ((parseInt(touch.pageX) - this.container.offsetLeft) / this.container
  155. .clientWidth) * 2 - 1
  156. this.mouse.y = -((parseInt(touch.pageY) - this.container.offsetTop) / this.container
  157. .clientHeight) * 2 + 1
  158. }
  159. this.isCanMoveObject = true
  160. // console.log('dddd move down ')
  161. };
  162. this.onDocumentTouchEnd = (event) => {
  163. // 取消默认动作
  164. event.preventDefault();
  165. // console.log('dddd move up ')
  166. this.mouse = {
  167. x: -10000,
  168. y: -10000
  169. }
  170. this.curMouseX = null;
  171. this.curMouseY = null;
  172. this.isCanMoveObject = false
  173. // 恢复上一个对象颜色并置空变量
  174. if (this.INTERSECTED) this.INTERSECTED.material.color.setHex(this.INTERSECTED.currentHex);
  175. this.INTERSECTED = null;
  176. // 恢复上一个对象颜色并置空变量
  177. if (this.floor) this.floor.material.color.setHex(this.floor.currentHex);
  178. this.floor = null;
  179. if (this.mOnMoveEnd) {
  180. this.mOnMoveEnd()
  181. }
  182. };
  183. }
  184. /**
  185. * 射线旋转移动物体功能
  186. * @param {Object} curCamera
  187. */
  188. raycasterSelectObject(curCamera) {
  189. this.raycaster.setFromCamera(this.mouse, curCamera);
  190. /**
  191. * intersectObjects 检测所有在射线与这些物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个),相交部分和.intersectObject所返回的格式是相同的。
  192. * objects —— 检测和射线相交的一组物体。
  193. * recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为false。
  194. * optionalTarget —— (可选)(可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。
  195. */
  196. // var intersects = raycaster.intersectObjects( scene.children );
  197. var intersects = this.raycaster.intersectObjects(this.moveObjsDataArray);
  198. if (intersects.length > 0) {
  199. if (this.INTERSECTED != intersects[0].object) {
  200. //emissive:该材质发射的属性
  201. if (this.INTERSECTED) this.INTERSECTED.material.color.setHex(this.INTERSECTED.currentHex);
  202. // 记录当前对象
  203. this.INTERSECTED = intersects[0].object;
  204. // 记录当前对象本身颜色
  205. this.INTERSECTED.currentHex = this.INTERSECTED.material.color.getHex();
  206. // 设置颜色为红色
  207. this.INTERSECTED.material.color.setHex(0xffff00);
  208. // console.log(" INTERSECTED ", this.INTERSECTED)
  209. // console.log(" intersects[ 0 ] ", intersects[ 0 ])
  210. if (this.mOnMoveStart) {
  211. this.mOnMoveStart()
  212. }
  213. }
  214. } else {
  215. // 恢复上一个对象颜色并置空变量
  216. // if ( this.INTERSECTED ) this.INTERSECTED.material.color.setHex( this.INTERSECTED.currentHex );
  217. // this.INTERSECTED = null;
  218. }
  219. }
  220. /**
  221. * 射线移动物体
  222. * @param {Object} curCamera
  223. */
  224. raycasterMoveObject(curCamera) {
  225. this.raycaster.setFromCamera(this.mouse, curCamera);
  226. /**
  227. * intersectObjects 检测所有在射线与这些物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个),相交部分和.intersectObject所返回的格式是相同的。
  228. * objects —— 检测和射线相交的一组物体。
  229. * recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为false。
  230. * optionalTarget —— (可选)(可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。
  231. */
  232. // var intersects = raycaster.intersectObjects( scene.children );
  233. var intersects = this.raycaster.intersectObjects(this.floorArray);
  234. if (intersects.length > 0) {
  235. if (this.floor != intersects[0].object) {
  236. //emissive:该材质发射的属性
  237. if (this.floor) this.floor.material.color.setHex(this.floor.currentHex);
  238. // 记录当前对象
  239. this.floor = intersects[0].object;
  240. // 记录当前对象本身颜色
  241. this.floor.currentHex = this.floor.material.color.getHex();
  242. // 设置颜色为红色
  243. this.floor.material.color.setHex(0xff00ff);
  244. // console.log(" intersects.point ", intersects[0].point)
  245. }
  246. this.floorRaycasterInfo = intersects[0]
  247. } else {
  248. // 恢复上一个对象颜色并置空变量
  249. if (this.floor) this.floor.material.color.setHex(this.floor.currentHex);
  250. this.floor = null;
  251. this.floorRaycasterInfo = null
  252. }
  253. }
  254. /**
  255. * 射线辅助线
  256. * @constructor
  257. */
  258. rayLinePaintHelper(scene) {
  259. const ori = this.raycaster.ray.origin
  260. const dir = this.raycaster.ray.direction
  261. const dirT = new Vector3(
  262. ori.x + dir.x * 10000,
  263. ori.y + dir.y * 10000,
  264. ori.z + dir.z * 10000
  265. )
  266. // console.log(' mRaycaster.dirT ' + dirT.x + ' ' + dirT.y)
  267. const material = new LineBasicMaterial({
  268. color: 0x0000ff,
  269. })
  270. const points = []
  271. points.push(ori)
  272. points.push(dirT)
  273. const geometry = new BufferGeometry().setFromPoints(points)
  274. const line = new Line(geometry, material)
  275. scene.add(line)
  276. }
  277. /**
  278. * 资源释放
  279. */
  280. dispose() {
  281. this.container = null
  282. this.raycaster = null
  283. this.mouse = null
  284. this.INTERSECTED = null
  285. this.canMoveObjectArray = null
  286. this.dashLinesBoxArray = null
  287. this.diningTable = null
  288. this.diningChairArray = null
  289. this.isCanMoveObject = false
  290. this.curMouseX = 0
  291. this.curMouseY = 0
  292. }
  293. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/84974
推荐阅读
相关标签
  

闽ICP备14008679号