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

这篇具有很好参考价值的文章主要介绍了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 中的射线发射点

四、效果预览

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

 

五、案例实现步骤

1、为了方便学习,这里是基于 Github 代码,进行开发的,大家可以下载官网代码,很多值得学习的案例

GitHub - mrdoob/three.js: JavaScript 3D Library.

gitcode:mirrors / mrdoob / three.js · GitCode

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

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

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

3、初始化构建 3D 场景

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

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

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

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

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

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

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

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

threejs鼠标移动物体,ThreeJS,javascript,webgl,threejs,Raycaster,射线移动物体

六、关键代码

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文章来源地址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模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • THREE.JS使用详细(three.js创建3d物体,three.js的使用方式)

    简述:three.js封装了WebGL的底层细节,是一款运行在浏览器中的 3D 引擎,可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象,目前在Git上已经拥有90k+的star,今天用three.js来构建一个三维模型; 1、首先,在项目中需要下载threejs的相关依赖; 2、在js页面引入使

    2024年01月23日
    浏览(84)
  • Three.js教程:WebGL渲染器设置(锯齿模糊)

    推荐:将 NSDT场景编辑器 加入你的3D工具链 其他系列工具: NSDT简石数字孪生 一般实际开发,threejs的WebGL渲染器需要进行一些通用的基础配置,本节课给大家简单介绍下,比如渲染模糊或锯齿问题。 渲染器锯齿属性 .antialias 设置渲染器锯齿属性 .antialias 的值可以直接在参数中

    2024年02月11日
    浏览(56)
  • 使用Three.js创建令人惊叹的WebGL 3D可视化

    WebGL 可视化 3D 绘图是一项新兴技术,具有广阔的应用前景。它允许开发人员在 Web 浏览器中创建和渲染 3D 图形,而无需安装额外的插件或软件。 本博客将介绍 Three.js,Three.js 是一个功能强大的 WebGL 框架,提供了丰富的 API 用于创建和渲染 3D 图形,接下来让我们通

    2024年01月19日
    浏览(65)
  • three.js中物体的灯光与阴影设置

    要让球体的阴影照射到平面上,需要使用阴影映射技术。具体步骤如下: 在渲染器中启用阴影: 创建一个平面和一个球体: 创建一个聚光灯: 设置球体和聚光灯的关系: 现在打开浏览器预览,就可以看到球体的阴影照射到了平面上。如果想让阴影更加自然,可以调整阴影

    2024年02月11日
    浏览(46)
  • Three.js--》理解光源对物体产生影响的重要性

    上篇文章 前端开发者掌握3d技术不再是梦,初识threejs 作为three.js入门篇讲解了许多内容但是没有深入了解其原理以及实现方法,仅仅只是展示了实现的内容及代码,本篇文章将深入讲解实现效果其背后用到的知识与原理。 目录 使用相机控件轨道控制器 理解光源影响 环境光

    2024年02月03日
    浏览(91)
  • Three.js -相机平滑移动

    一、安装 二、引入 三、使用 最后不要忘了在render中执行 TWEEN.update();

    2024年02月13日
    浏览(61)
  • WebGL+Three.js入门与实战——给画布换颜色、绘制一个点、三维坐标系

    个人简介 👀 个人主页: 前端杂货铺 🙋‍♂️ 学习方向: 主攻前端方向,正逐渐往全干发展 📃 个人状态: 研发工程师,现效力于中国工业软件事业 🚀 人生格言: 积跬步至千里,积小流成江海 🥇 推荐学习:🍍前端面试宝典 🍉Vue2 🍋Vue3 🍓Vue2/3项目实战 🥝Node.js🍒

    2024年02月04日
    浏览(62)
  • three.js鼠标控制场景旋转

    鼠标控制旋转

    2024年02月17日
    浏览(39)
  • THREE.JS镜头随鼠标晃动效果

    为了让动画更灵活并且简单 借助 gsap 让其具有更多可能,在未来更容易扩充其他动效 gsap Dom跟随鼠标移动 gsap.quickTo() 首先要监听鼠标移动,并且将移动的值转换到 -1 和 1 之间 方便处理 上面将 位置 / 屏幕宽高 将值缩放在 0 和 1 之间 然后通过 乘2减1 将其限制在 -1 和 1 之间

    2024年02月13日
    浏览(42)
  • Three.js移动端双指触屏控制旋转和缩放

    在移动端通过双指来控制物体的选择和缩放。旋转通过双指旋转操作,而缩放通过双指距离实现。实现平台是小程序基于three.js的AR版。 旋转:两个手指产生的两个点可以算出一个向量,那么我就通过程序前后两帧计算两个向量之间的夹角来判断旋转的角度信息。这里我通过

    2024年02月08日
    浏览(66)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包