Vue实现利用Three.js导入URDF机械臂模型并控制其运动记录

0. 前言

最近在搞毕设,其中一部分是做一个人机交互界面,打算使用的技术栈如下:

 本来前端没打算做可视化界面的,但是觉得如果没可视化界面的话就没啥亮点了哈哈,所以决定还是实现一个可视化模型界面。

1. 相关文章

本项目前端模型可视化的实现参考了这两篇文章,算是这两篇文章的结合:

更方便的数字孪生程序方案: threejs使用URDF

前端使用Threejs控制机械臂模型运动(我在CSDN的第一篇文章)

2. 实现效果 

这回先上效果:

3. 实现过程 

 3.1 机械臂控制界面

本次机械臂使用的界面直接参考的是博主rookie fish前端使用Threejs控制机械臂模型运动(我在CSDN的第一篇文章)

主要参考代码:

<!--
 * @Author: wangzhiyu <w19165802736@163.com>
 * @version: 1.0.0
 * @Date: 2024-02-20 15:14:10
 * @LastEditTime: 2024-02-20 11:05:28
 * @Descripttion: 菜单控制机械臂角度模块
-->
<template>
  <div>
    <el-drawer v-model="drawer" direction="ltr" size="20%">
      <el-aside>
        <Menu @sliderInput="sliderInput" />
      </el-aside>
    </el-drawer>
    <div class="btn" v-show="!drawer">
      <el-button type="primary" :icon="Operation" circle size="large" @click="drawerSwitch" />
    </div>
  </div>
</template>
<script setup>
/**
 * 旋转中心点
 * 2: 0.7,0.67,0
 * 3: 0.1,2.42,0
 * 4: 0.15,4.113,0
 * 5: 0.65,4.38,0
 * 6: 0.88,4.68,0
 */
import { Operation } from '@element-plus/icons-vue';
import { ref } from 'vue';
import Menu from './components/Menu/index.vue';
import * as THREE from 'three';
// 控制器
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// OBJ模型解析
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
import { onMounted } from 'vue';
// 机械臂零件模型数组
let mtlList = ['0.mtl', '1.mtl', '2.mtl', '3.mtl', '4.mtl', '5.mtl', '6.mtl'];
let objList = ['0.obj', '1.obj', '2.obj', '3.obj', '4.obj', '5.obj', '6.obj'];

// 创建场景->相机->渲染器->相机添加到场景中->渲染器渲染场景和相机->渲染器添加到dom中
let scene = '';
let camera = '';
let renderer = '';
// 轨道控制器
let controls = '';
let handList = [];
let circlePosition = '';
const drawer = ref(false);
const handConfig = [
  {
    name: '2.mtl',
    rotation: {
      x: 0.7,
      y: 0.63,
      z: 0,
    },
  },
  {
    name: '3.mtl',
    rotation: {
      x: 0.1,
      y: 2.42,
      z: 0,
    },
  },
  {
    name: '4.mtl',
    rotation: {
      x: 0.15,
      y: 4.113,
      z: 0,
    },
  },
  {
    name: '5.mtl',
    rotation: {
      x: 0.65,
      y: 4.38,
      z: 0,
    },
  },
  {
    name: '6.mtl',
    rotation: {
      x: 0.88,
      y: 4.68,
      z: 0,
    },
  },
];

// 设置各个关节的角度
function sliderInput(value, name, direction) {
  // 找到要设置的关节
  let target = handList.find(item => item.materialLibraries.join('') === name + '.mtl');
  target.rotation[direction] = value;
}

// 开关侧边栏控制栏
const drawerSwitch = () => {
  drawer.value = !drawer.value;
};

// 将后面的元素添加到前面元素的children列表中,这样某个节点运动时,节点的children都可以跟随运动
const addChildren = () => {
  // 对节点进行排序,避免添加错误的父级
  handList = handList.sort((a, b) => a.materialLibraries.join('')[0] - b.materialLibraries.join('')[0]);

  // 添加子级模型
  for (let i = 0; i < handList.length; i++) {
    // 当前模型后面还有其他模型时才会允许添加
    if (handList[i + 1]) {
      // 当前模型的类型为Group时,表示这里我已经使用了一个父级元素来包住模型了,以此来修改模型运动的中心点,如果没有包住,就表示这个模型的中心点是对的,不需要添加到Group中
      handList[i].children[0].type === 'Group' ? handList[i].children[0].add(handList[i + 1]) : handList[i].add(handList[i + 1]);
    }
  }
  console.log(handList[0], 'handList[0]');
  // 将最终整合的模型添加到场景中
  scene.add(handList[0]);
};

// 初始化
function initBase() {
  scene = new THREE.Scene();
  scene.position.set(0, -2, 0);
  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
  camera.position.set(6, 8, 6);

  // 相机添加到场景中
  scene.add(camera);

  // antialias:开启抗锯齿  logarithmicDepthBuffer:使用对数深度缓冲器,一般在单个场景处理较大的差异
  renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor('#fff');
}

// 添加光线
function addLight() {
  const positions = [
    { x: 10, y: 10, z: 10 },
    { x: -10, y: 10, z: -10 },
    { x: -30, y: 10, z: 0 },
    { x: 0, y: -10, z: 0 },
  ];
  positions.forEach(pos => {
    const light = new THREE.DirectionalLight('#8fbad3', 1);
    light.position.set(pos.x, pos.y, pos.z);
    scene.add(light);
  });
}

// 循环导入模型
for (let i = 0; i < mtlList.length; i++) {
  initIsland(mtlList[i], objList[i]);
}

// 添加机械臂模型
function initIsland(mtl, obj) {
  // obj解析器
  var objLoader = new OBJLoader();
  // mtl解析器
  var mtlLoader = new MTLLoader();

  mtlLoader.load(`./model/${mtl}`, function (materials) {
    // 将 MaterialCreator 对象应用到材质文件中
    materials.preload();

    // 将解析得到的材质赋值给 objLoader 对象
    objLoader.setMaterials(materials);

    // 加载 OBJ 模型文件
    objLoader.load(`./model/${obj}`, function (obj) {
      // 如果当前模型需要设置父级,父级将会保存到这个变量中,默认位空
      let objNew = null;

      // 获取模型的名称
      let objName = obj.materialLibraries.join('');

      // 获取当前模型对应handConfig对象中的某个配置对象,如果对应的话,就表示需要单独做一些处理
      let objInfo = handConfig.find(item => objName === item.name);

      // 判断是否对应
      if (objInfo) {
        // 创建一个Mesh
        objNew = new THREE.Mesh(new THREE.SphereGeometry(0, 32, 16), new THREE.MeshBasicMaterial({ color: 'rgba(0,0,0,1)' }));
        // 设置Mesh的位置
        objNew.position.set(objInfo.rotation.x, objInfo.rotation.y, objInfo.rotation.z);

        // 上面设置Mesh的位置会物体的位置也移动过去,这里将物体的位置移动回来
        obj.position.set(-objInfo.rotation.x, -objInfo.rotation.y, -objInfo.rotation.z);

        // 给Mesh设置名称,便于后续的查找与操作
        objNew.materialLibraries = [objInfo.name];

        // 将模型添加到Mesh中,这样模型的中心点就会以Mesh的坐标为中心了
        objNew.add(obj);

        // 调用回调函数,便于操作
        objInfo.callback(objNew || obj);
      }

      // 零件模型添加到数组中,便于后续的修改调试
      handList = [...handList, objNew || obj];

      // 加载完所有的模型后调用添加父级子级函数
      if (handList.length === objList.length) {
        // 调用函数,设置父级子级
        addChildren();
      }
    });
  });
}

// 轨道控制器
function initOrbitControls() {
  controls = new OrbitControls(camera, renderer.domElement);
  // 开启阻尼 更加真实
  controls.enableDamping = true;
}

// render渲染器
function render() {
  // 渲染器更新
  renderer.render(scene, camera);
  // 控制器更新
  controls.update();
  requestAnimationFrame(render);
}

// 辅助线
function addHelpLine() {
  // const arrowHelper = new THREE.AxesHelper(5);
  // scene.add(arrowHelper);

  const gridHelper = new THREE.GridHelper(100, 20);
  scene.add(gridHelper);
}

// 初始化
initBase();
// 添加灯光
addLight();
// 添加控制器
initOrbitControls();
// 添加辅助线和网格地板
addHelpLine();

onMounted(() => {
  // 将渲染器添加到页面中
  document.body.appendChild(renderer.domElement);
  render();
  // 窗口大小处理
  window.addEventListener('resize', () => {
    // 更新相机宽高比
    camera.aspect = window.innerWidth / window.innerHeight;
    // 更新相机的投影矩阵
    camera.updateProjectionMatrix();
    // 更新渲染器渲染的尺寸大小
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 设置渲染器的像素比(window.devicePixelRatio:当前设备的像素比)
    renderer.setPixelRatio(window.innerWidth / window.innerHeight);
  });
});
</script>

<style>
.btn {
  position: fixed;
  bottom: 5%;
  left: 50%;
  transform: translateX(-50%);
}
</style>

该博主采用的是OBJ和MTL同时导入three.js的方式,一个个导入零件模型。这里要注意的是,模型的放置路径是在public文件夹底下的:

上面的代码中:

mtlLoader.load(`./model/${mtl}`, function (materials) {

model文件夹是位于public文件夹中的,放在其他地方会无法加载模型。

 但是我发现这个模型旋转中心很难找到,因此考虑能不能在three.js中导入URDF模型并直接控制机械臂模型。

3.2 URDF模型导入three.js(重点

基于导入URDF的这个想法,我在百度和GitHub上进行了搜索,然后发现了这篇文章:

更方便的数字孪生程序方案: threejs使用URDF

其调用了urdf-loader包实现了URDF的加载。文中给出了操作流程,经过一番探索,我简化了部分无用的流程:

3.2.1 导入urdf-loader包

方法1(原文章方法,不推荐):

先在GitHub上搜索three.js urdf,然后第一个就是urdf-loader包

找到最新发行版然后下载:

然后根据原文章方法,解压文件并复制文件夹中javascript/src/中的jsts文件(注意是js和ts文件,不要按原文那样只复制js文件,ts文件有调用的) 

将这些文件粘贴到你自己项目(注意是自己的项目,不是urdf-loader)的node_modules/three/examples/jsm/loaders文件夹中

然后打开URDFLoader.js修改如下代码:

//这四行代码主要改的是导入文件的路径
import * as THREE from 'three';
import { STLLoader } from '../loaders/STLLoader.js'; //路径有变
import { ColladaLoader } from '../loaders/ColladaLoader.js'; //路径有变
import { URDFRobot, URDFJoint, URDFLink, URDFCollider, URDFVisual, URDFMimicJoint } from '../loaders/URDFClasses.js'; //路径有变

然后在最后一行添加:

export { URDFLoader }; //导出URDFLoader模块

 完成后即可采用如下方法在代码中引入URDF模型:

import URDFLoader from 'three/addons/loaders/URDFLoader.js';
 方法2 (npm安装,推荐):

在urdf-loader作者的github中我发现了urdf-loader的使用说明,发现作者是通过import一个叫"urdf-loader"包实现的URDFLoader导入:
 

我怀疑作者已经把这个项目封装成一个包,并传到了npm上,所以我去npm上搜索了一下,还真有,用法也是一样的,应该是同一个包:

所以我直接尝试:

npm i urdf-loader

然后和作者一样,选择:

import URDFLoader from 'urdf-loader';

这样的方式导入URDFLoader,感觉这样比原文章方法方便很多。 

3.2.2 在three.js中显示URDF模型

方法1(原文章方法):

原文章方法是利用urdf-loader的样例simple.html对urdf进行展示:

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <meta charset="utf-8"/>

        <title>Simple URDF Loading Example</title>

        <style>
            html, body {
                box-sizing: border-box;
                margin: 0;
                padding: 0;
                overflow: hidden;
            }
        </style>
		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>
		<script>
var scene, camera, renderer,  controls;
			var robot;
		</script>
		<script type="module">
			import {
				WebGLRenderer,
				PerspectiveCamera,
				Scene,
				Mesh,
				PlaneGeometry,
				ShadowMaterial,
				DirectionalLight,
				PCFSoftShadowMap,
				sRGBEncoding,
				Color,
				AmbientLight,
				Box3,
				LoadingManager,
				MathUtils,
			} from 'three';
			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
			import URDFLoader from 'three/addons/loaders/URDFLoader.js';
			


			
			setTimeout(function(){
			console.debug(111);
			init();
			render();
			ccc = 333;
			},1000);

			function init() {

				scene = new Scene();
				scene.background = new Color(0x263238);

				camera = new PerspectiveCamera();
				camera.position.set(10, 10, 10);
				camera.lookAt(0, 0, 0);

				renderer = new WebGLRenderer({ antialias: true });
				renderer.outputEncoding = sRGBEncoding;
				renderer.shadowMap.enabled = true;
				renderer.shadowMap.type = PCFSoftShadowMap;
				document.body.appendChild(renderer.domElement);

				const directionalLight = new DirectionalLight(0xffffff, 1.0);
				directionalLight.castShadow = true;
				directionalLight.shadow.mapSize.setScalar(1024);
				directionalLight.position.set(5, 30, 5);
				scene.add(directionalLight);

				const ambientLight = new AmbientLight(0xffffff, 0.2);
				scene.add(ambientLight);

				const ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ opacity: 0.25 }));
				ground.rotation.x = -Math.PI / 2;
				ground.scale.setScalar(30);
				ground.receiveShadow = true;
				scene.add(ground);

				controls = new OrbitControls(camera, renderer.domElement);
				controls.minDistance = 4;
				controls.target.y = 1;
				controls.update();

				// Load robot
				const manager = new LoadingManager();
				const loader = new URDFLoader(manager);
				//loader.load('models/urdf/T12/urdf/T12_flipped.URDF', result => {
				loader.load('models/urdf3/urdf3/urdf3.urdf', result => {
					robot = result;

				});

				// wait until all the geometry has loaded to add the model to the scene
				manager.onLoad = () => {

					robot.rotation.x = Math.PI / 2;
					robot.rotation.x *= (-1);
					robot.traverse(c => {
						c.castShadow = true;
					});
					for (let i = 1; i <= 6; i++) {

						//robot.joints[`HP${ i }`].setJointValue(MathUtils.degToRad(30));
						//robot.joints[`KP${ i }`].setJointValue(MathUtils.degToRad(120));
						//robot.joints[`AP${ i }`].setJointValue(MathUtils.degToRad(-60));

					}
					robot.updateMatrixWorld(true);

					const bb = new Box3();
					bb.setFromObject(robot);

					robot.position.y -= bb.min.y;
					scene.add(robot);

				};

				onResize();
				window.addEventListener('resize', onResize);

			}

			function onResize() {

				renderer.setSize(window.innerWidth, window.innerHeight);
				renderer.setPixelRatio(window.devicePixelRatio);

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

			}

			function render() {

				requestAnimationFrame(render);
				renderer.render(scene, camera);

			}

		</script>
		<script >
			var abc = {a:1,b:2,c:3};
		</script>
    </head>
    <body >

    </body>
</html>

修改了这个位置:

// Load robot
const manager = new LoadingManager();
const loader = new URDFLoader(manager);
//loader.load('models/urdf/T12/urdf/T12_flipped.URDF', result => {
loader.load('models/urdf3/urdf3/urdf3.urdf', result => {
			robot = result;
			});

这样修改后,运行urdf-loader项目:

npm start

访问localhost:8080/examples/simple.html 即可看到URDF模型展示在页面中。

但是这样我的这个项目不太好,vue可以嵌入外部html,但是参数传递总感觉很麻烦,同时考虑到机械臂的通信效果十分重要,这样的参数传递多少会有影响。

方法2 (在Vue中实现) :

 该方法的主要代码借鉴了这篇文章的:

前端使用Threejs控制机械臂模型运动(我在CSDN的第一篇文章)

先展示原文中的主要代码:

<!--
 * @Author: wangzhiyu <w19165802736@163.com>
 * @version: 1.0.0
 * @Date: 2024-02-20 15:14:10
 * @LastEditTime: 2024-02-20 11:05:28
 * @Descripttion: 菜单控制机械臂角度模块
-->
<template>
  <div>
    <el-drawer v-model="drawer" direction="ltr" size="20%">
      <el-aside>
        <Menu @sliderInput="sliderInput" />
      </el-aside>
    </el-drawer>
    <div class="btn" v-show="!drawer">
      <el-button type="primary" :icon="Operation" circle size="large" @click="drawerSwitch" />
    </div>
  </div>
</template>
<script setup>
/**
 * 旋转中心点
 * 2: 0.7,0.67,0
 * 3: 0.1,2.42,0
 * 4: 0.15,4.113,0
 * 5: 0.65,4.38,0
 * 6: 0.88,4.68,0
 */
import { Operation } from '@element-plus/icons-vue';
import { ref } from 'vue';
import Menu from './components/Menu/index.vue';
import * as THREE from 'three';
// 控制器
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// OBJ模型解析
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
import { onMounted } from 'vue';
// 机械臂零件模型数组
let mtlList = ['0.mtl', '1.mtl', '2.mtl', '3.mtl', '4.mtl', '5.mtl', '6.mtl'];
let objList = ['0.obj', '1.obj', '2.obj', '3.obj', '4.obj', '5.obj', '6.obj'];

// 创建场景->相机->渲染器->相机添加到场景中->渲染器渲染场景和相机->渲染器添加到dom中
let scene = '';
let camera = '';
let renderer = '';
// 轨道控制器
let controls = '';
let handList = [];
let circlePosition = '';
const drawer = ref(false);
const handConfig = [
  {
    name: '2.mtl',
    rotation: {
      x: 0.7,
      y: 0.63,
      z: 0,
    },
  },
  {
    name: '3.mtl',
    rotation: {
      x: 0.1,
      y: 2.42,
      z: 0,
    },
  },
  {
    name: '4.mtl',
    rotation: {
      x: 0.15,
      y: 4.113,
      z: 0,
    },
  },
  {
    name: '5.mtl',
    rotation: {
      x: 0.65,
      y: 4.38,
      z: 0,
    },
  },
  {
    name: '6.mtl',
    rotation: {
      x: 0.88,
      y: 4.68,
      z: 0,
    },
  },
];

// 设置各个关节的角度
function sliderInput(value, name, direction) {
  // 找到要设置的关节
  let target = handList.find(item => item.materialLibraries.join('') === name + '.mtl');
  target.rotation[direction] = value;
}

// 开关侧边栏控制栏
const drawerSwitch = () => {
  drawer.value = !drawer.value;
};

// 将后面的元素添加到前面元素的children列表中,这样某个节点运动时,节点的children都可以跟随运动
const addChildren = () => {
  // 对节点进行排序,避免添加错误的父级
  handList = handList.sort((a, b) => a.materialLibraries.join('')[0] - b.materialLibraries.join('')[0]);

  // 添加子级模型
  for (let i = 0; i < handList.length; i++) {
    // 当前模型后面还有其他模型时才会允许添加
    if (handList[i + 1]) {
      // 当前模型的类型为Group时,表示这里我已经使用了一个父级元素来包住模型了,以此来修改模型运动的中心点,如果没有包住,就表示这个模型的中心点是对的,不需要添加到Group中
      handList[i].children[0].type === 'Group' ? handList[i].children[0].add(handList[i + 1]) : handList[i].add(handList[i + 1]);
    }
  }
  console.log(handList[0], 'handList[0]');
  // 将最终整合的模型添加到场景中
  scene.add(handList[0]);
};

// 初始化
function initBase() {
  scene = new THREE.Scene();
  scene.position.set(0, -2, 0);
  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
  camera.position.set(6, 8, 6);

  // 相机添加到场景中
  scene.add(camera);

  // antialias:开启抗锯齿  logarithmicDepthBuffer:使用对数深度缓冲器,一般在单个场景处理较大的差异
  renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor('#fff');
}

// 添加光线
function addLight() {
  const positions = [
    { x: 10, y: 10, z: 10 },
    { x: -10, y: 10, z: -10 },
    { x: -30, y: 10, z: 0 },
    { x: 0, y: -10, z: 0 },
  ];
  positions.forEach(pos => {
    const light = new THREE.DirectionalLight('#8fbad3', 1);
    light.position.set(pos.x, pos.y, pos.z);
    scene.add(light);
  });
}

// 循环导入模型
for (let i = 0; i < mtlList.length; i++) {
  initIsland(mtlList[i], objList[i]);
}

// 添加机械臂模型
function initIsland(mtl, obj) {
  // obj解析器
  var objLoader = new OBJLoader();
  // mtl解析器
  var mtlLoader = new MTLLoader();

  mtlLoader.load(`./model/${mtl}`, function (materials) {
    // 将 MaterialCreator 对象应用到材质文件中
    materials.preload();

    // 将解析得到的材质赋值给 objLoader 对象
    objLoader.setMaterials(materials);

    // 加载 OBJ 模型文件
    objLoader.load(`./model/${obj}`, function (obj) {
      // 如果当前模型需要设置父级,父级将会保存到这个变量中,默认位空
      let objNew = null;

      // 获取模型的名称
      let objName = obj.materialLibraries.join('');

      // 获取当前模型对应handConfig对象中的某个配置对象,如果对应的话,就表示需要单独做一些处理
      let objInfo = handConfig.find(item => objName === item.name);

      // 判断是否对应
      if (objInfo) {
        // 创建一个Mesh
        objNew = new THREE.Mesh(new THREE.SphereGeometry(0, 32, 16), new THREE.MeshBasicMaterial({ color: 'rgba(0,0,0,1)' }));
        // 设置Mesh的位置
        objNew.position.set(objInfo.rotation.x, objInfo.rotation.y, objInfo.rotation.z);

        // 上面设置Mesh的位置会物体的位置也移动过去,这里将物体的位置移动回来
        obj.position.set(-objInfo.rotation.x, -objInfo.rotation.y, -objInfo.rotation.z);

        // 给Mesh设置名称,便于后续的查找与操作
        objNew.materialLibraries = [objInfo.name];

        // 将模型添加到Mesh中,这样模型的中心点就会以Mesh的坐标为中心了
        objNew.add(obj);

        // 调用回调函数,便于操作
        objInfo.callback(objNew || obj);
      }

      // 零件模型添加到数组中,便于后续的修改调试
      handList = [...handList, objNew || obj];

      // 加载完所有的模型后调用添加父级子级函数
      if (handList.length === objList.length) {
        // 调用函数,设置父级子级
        addChildren();
      }
    });
  });
}

// 轨道控制器
function initOrbitControls() {
  controls = new OrbitControls(camera, renderer.domElement);
  // 开启阻尼 更加真实
  controls.enableDamping = true;
}

// render渲染器
function render() {
  // 渲染器更新
  renderer.render(scene, camera);
  // 控制器更新
  controls.update();
  requestAnimationFrame(render);
}

// 辅助线
function addHelpLine() {
  // const arrowHelper = new THREE.AxesHelper(5);
  // scene.add(arrowHelper);

  const gridHelper = new THREE.GridHelper(100, 20);
  scene.add(gridHelper);
}

// 初始化
initBase();
// 添加灯光
addLight();
// 添加控制器
initOrbitControls();
// 添加辅助线和网格地板
addHelpLine();

onMounted(() => {
  // 将渲染器添加到页面中
  document.body.appendChild(renderer.domElement);
  render();
  // 窗口大小处理
  window.addEventListener('resize', () => {
    // 更新相机宽高比
    camera.aspect = window.innerWidth / window.innerHeight;
    // 更新相机的投影矩阵
    camera.updateProjectionMatrix();
    // 更新渲染器渲染的尺寸大小
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 设置渲染器的像素比(window.devicePixelRatio:当前设备的像素比)
    renderer.setPixelRatio(window.innerWidth / window.innerHeight);
  });
});
</script>

<style>
.btn {
  position: fixed;
  bottom: 5%;
  left: 50%;
  transform: translateX(-50%);
}
</style>

下面是我修改后的代码:

<!-- 用于3D预览机械臂的页面,会有左侧边栏用于实时控制机械臂(可收缩,element-ui折叠面板),
    中间是3D模型(使用three.js实现),右侧是机械臂反馈情况(可收缩,element-ui折叠面板) -->
    <template>
      <div class="common-layout">
          <el-container>
              <!-- <el-drawer v-model="drawer" direction="ltr" size="20%"> -->
              <el-aside>
                  <Menu @sliderInput="sliderInput" />
              </el-aside>
              <!-- </el-drawer> -->
              <!-- <div class="btn" v-show="!drawer"> -->
              <!-- <el-button type="primary" :icon="Operation" circle size="large" @click="drawerSwitch" /> -->
              <!-- </div> -->
              <div id="webgl"></div>
          </el-container>
      </div>
  </template>
  
  <script setup>
   import {
  LoadingManager,
  MathUtils,
} from 'three';
  import { Operation } from '@element-plus/icons-vue';
  import { ref } from 'vue';
  import Menu from '../views/robotContrlMenu.vue';
  import Menu2 from '../views/test2.vue';
  import * as THREE from 'three';
  // 控制器
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  //URDF模型解析
  //import URDFLoader from 'three/addons/loaders/URDFLoader.js';
  import URDFLoader from 'urdf-loader';
  import { onMounted } from 'vue';
  
  
  // 创建场景->相机->渲染器->相机添加到场景中->渲染器渲染场景和相机->渲染器添加到dom中
  var robot;
  let scene = '';
  let camera = '';
  let renderer = '';
  // 轨道控制器
  let controls = '';
  let handList = [];
  let circlePosition = '';
  const drawer = ref(false);
  var canvas = ref(null);
  
  
  // 设置各个关节的角度
  function sliderInput(value, name) {
    // 找到要设置的关节
    //let target = handList.find(item => item.materialLibraries.join('') === name + '.mtl');
    //target.rotation[direction] = value;
    //console.log("此时的ROBOT,value,name",robot,value,name);
    name = 'link'+String(name)+"_joint";
    robot.joints[name].setJointValue(MathUtils.degToRad(value));
    //robot.joints[name].setJointValue(value);
    //robot.updateMatrixWorld(true);
  }
  
  // 开关侧边栏控制栏
  const drawerSwitch = () => {
    drawer.value = !drawer.value;
  };
  
  
  // 初始化
  function initBase() {
    scene = new THREE.Scene();
    scene.position.set(0, 0.2, 0.8);
    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
    camera.position.set(1.86, 1.51, 1.02);
    //camera.position.set(0, 0, 0);
    //camera.lookAt(scene.position);
  
    // 相机添加到场景中
    scene.add(camera);
  
    // antialias:开启抗锯齿  logarithmicDepthBuffer:使用对数深度缓冲器,一般在单个场景处理较大的差异
    renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor('#DCDCDC');
  }
  
  // 添加光线
  function addLight() {
    const positions = [
      { x: 10, y: 10, z: 10 },
      { x: -10, y: 10, z: -10 },
      { x: -30, y: 10, z: 0 },
      { x: 0, y: -10, z: 0 },
    ];
    positions.forEach(pos => {
      const light = new THREE.DirectionalLight('#8fbad3', 1);
      light.position.set(pos.x, pos.y, pos.z);
      //将灯光添加到场景中
      scene.add(light);
    });
  }

  
  // 轨道控制器
  function initOrbitControls() {
    controls = new OrbitControls(camera, renderer.domElement);
    // 开启阻尼 更加真实
    controls.enableDamping = true;
  }
  
  // render渲染器
  function render() {
    // 渲染器更新
    renderer.render(scene, camera);
    // 控制器更新
    controls.update();
    requestAnimationFrame(render);
  }
  
  // 辅助线
  function addHelpLine() {
    //坐标轴辅助显示
    const arrowHelper = new THREE.AxesHelper(5);
    scene.add(arrowHelper);
  
    const gridHelper = new THREE.GridHelper(100, 20);
    scene.add(gridHelper);
  }
  
  // 初始化
  initBase();
  // 添加灯光
  addLight();
  // 添加控制器
  initOrbitControls();
  // 添加辅助线和网格地板
  addHelpLine();

  function initRobot(){
  // 导入Robot模型
  const manager = new LoadingManager();
      const loader = new URDFLoader(manager);
      loader.load('./lineRobot_20240229/urdf/lineRobot_20240229.urdf', result => {
          robot = result;
          //console.log("ROBOT:",robot);
          //设置ROBOT坐标z朝上
          robot.rotation.x = Math.PI / 2;
          robot.rotation.x *= (-1);
          //设置ROBOT在坐标原点
          robot.position.x = 0;
          robot.position.y = 0;
          robot.position.z = 0;
          scene.add(robot);
      });
  }
  initRobot();
  
  
  onMounted(() => {
    // 将渲染器添加到页面中
    //document.body.appendChild(renderer.domElement);
    document.getElementById('webgl').appendChild(renderer.domElement);
    render();
    // 窗口大小处理
    window.addEventListener('resize', () => {
      // 更新相机宽高比
      camera.aspect = window.innerWidth / window.innerHeight;
      // 更新相机的投影矩阵
      camera.updateProjectionMatrix();
      // 更新渲染器渲染的尺寸大小
      renderer.setSize(window.innerWidth, window.innerHeight);
      // 设置渲染器的像素比(window.devicePixelRatio:当前设备的像素比)
      renderer.setPixelRatio(window.innerWidth / window.innerHeight);

      // 获取相机位置
      // const cameraPosition = camera.position;
      // console.log('相机位置:', cameraPosition);
    });
  });
  </script>
  
  <style>
  .btn {
    position: fixed;
    bottom: 5%;
    left: 50%;
    transform: translateX(-50%);
  }
  </style>

主要有三个地方改动:

①引入URDFLoader:

import {
  LoadingManager,
  MathUtils,
} from 'three';
  import { Operation } from '@element-plus/icons-vue';
  import { ref } from 'vue';
  import Menu from '../views/robotContrlMenu.vue';
  import Menu2 from '../views/test2.vue';
  import * as THREE from 'three';
  // 控制器
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  //URDF模型解析
  //import URDFLoader from 'three/addons/loaders/URDFLoader.js';
  import URDFLoader from 'urdf-loader';
  import { onMounted } from 'vue';

②导入模型:

function initRobot(){
  // 导入Robot模型
  const manager = new LoadingManager();
      const loader = new URDFLoader(manager);
      loader.load('./lineRobot_20240229/urdf/lineRobot_20240229.urdf', result => {
          robot = result;
          //console.log("ROBOT:",robot);
          //设置ROBOT坐标z朝上
          robot.rotation.x = Math.PI / 2;
          robot.rotation.x *= (-1);
          //设置ROBOT在坐标原点
          robot.position.x = 0;
          robot.position.y = 0;
          robot.position.z = 0;
          scene.add(robot);
      });
  }
  initRobot();

③控制机械臂各关节角度:

// 设置各个关节的角度
  function sliderInput(value, name) {
    // 找到要设置的关节
    //let target = handList.find(item => item.materialLibraries.join('') === name + '.mtl');
    //target.rotation[direction] = value;
    //console.log("此时的ROBOT,value,name",robot,value,name);
    name = 'link'+String(name)+"_joint";
    robot.joints[name].setJointValue(MathUtils.degToRad(value));
    //robot.joints[name].setJointValue(value);
    //robot.updateMatrixWorld(true);
  }

这里说一下setJointValue这个函数,这个函数时urdf-loader包提供的,直接按这个格式给关节赋值就能让机械臂运动了,robot.joints[name]中name这个参数就是URDF文件中的各关节的关节名。

 

 

 

 

 

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值