0. 前言
最近在搞毕设,其中一部分是做一个人机交互界面,打算使用的技术栈如下:
本来前端没打算做可视化界面的,但是觉得如果没可视化界面的话就没啥亮点了哈哈,所以决定还是实现一个可视化模型界面。
1. 相关文章
本项目前端模型可视化的实现参考了这两篇文章,算是这两篇文章的结合:
前端使用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上进行了搜索,然后发现了这篇文章:
其调用了urdf-loader包实现了URDF的加载。文中给出了操作流程,经过一番探索,我简化了部分无用的流程:
3.2.1 导入urdf-loader包:
方法1(原文章方法,不推荐):
先在GitHub上搜索three.js urdf,然后第一个就是urdf-loader包
找到最新发行版然后下载:
然后根据原文章方法,解压文件并复制文件夹中javascript/src/中的js和ts文件(注意是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文件中的各关节的关节名。