Vue3+ThreeJS实现3D机械臂控制和预览
一、效果预览
注:视频演示请点击阅读原文
-
PC端
-
移动端
二、技术栈
-
web端使用Vue3+element-plus UI -
3D显示使用three.js
三、过程
-
新建Vue3项目 -
清除无关的新手引导代码 -
安装vue-router 4 -
配置vue-router -
安装three.js -
新建src/views/home/index.vue 为主页 -
新建src/layout/index.vue layout主页 -
新建src/views/home/components/Menu组件 -
新建src/ views/home/components/Robot3d组件
三、核心代码
-
使用three.js构建3D机械臂
-
除去必要的3D场景元素外,initRobot方法是构建机械臂的核心
-
总共有活动关节5个分别是:D1~D5 -
总共有力臂4个分别是:B1~B4 -
根据结构从D1开始嵌套为子节点 -
最终渲染生成力臂
-
其中的setRobotRotation方法对外提供角度控制 -
其中的setControlsEnabled方法对外提供视角开关控制
// 文件路径:src/views/home/components/Robot3d/manager/BaseManager.js
import * as dat from "dat.gui";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
export default class baseManager {
constructor(canvas) {
//Gui
this.gui = new dat.GUI();
this.gui.hide();
//Canvas
this.canvas = canvas
//Sizes
this.sizes = {}
//Camera
this.camera = null
//Renderer
this.renderer = null
// Scene
this.scene = new THREE.Scene();
//AnimateTick
this.clock = new THREE.Clock();
this.previousTime = 0;
this.initWindowSizes()
this.initcamera()
this.inLights()
this.initHelper()
this.initControls()
this.initRobot()
this.initRenderer()
this.initAnimateTick()
}
/**
* Sizes
*/
initWindowSizes() {
/**
* Sizes
*/
const sizes = {
width: this.canvas.parentNode.clientWidth,
height: this.canvas.parentNode.clientHeight,
};
window.addEventListener("resize", () => {
// Update sizes
sizes.width = this.canvas.parentNode.clientWidth;
sizes.height = this.canvas.parentNode.clientHeight;
// Update camera
this.camera.aspect = sizes.width / sizes.height;
this.camera.updateProjectionMatrix();
// Update renderer
this.renderer.setSize(sizes.width, sizes.height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
this.sizes = sizes;
}
/**
* Camera
*/
initcamera() {
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
this.sizes.width / this.sizes.height,
0.1,
100
);
camera.position.set(0, 4, 10);
this.scene.add(camera);
this.camera = camera;
}
/**
* inLights
*/
inLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.set(1024, 1024);
directionalLight.shadow.camera.far = 25;
directionalLight.shadow.camera.left = -7;
directionalLight.shadow.camera.top = 7;
directionalLight.shadow.camera.right = 7;
directionalLight.shadow.camera.bottom = -7;
directionalLight.position.set(5, 5, 5);
this.scene.add(directionalLight);
}
/**
* Helper
*/
initHelper() {
const axes = new THREE.AxesHelper(20);
this.scene.add(axes);
const gridHelper = new THREE.GridHelper(100, 100);
this.scene.add(gridHelper);
}
/**
* Controls
*/
initControls() {
const controls = new OrbitControls(this.camera, this.canvas);
controls.target.set(0, 0.75, 0);
controls.enableDamping = true;
this.controls = controls;
}
setControlsEnabled(enabled) {
this.controls.enabled = enabled
}
/**
* Robot
*/
initRobot() {
const D1 = new THREE.Mesh(
new THREE.CylinderGeometry(1, 1, 0.5, 32),
new THREE.MeshStandardMaterial({
color: "#E45826",
metalness: 0,
roughness: 0.5,
})
);
this.gui.add(D1.rotation, "y").min(-Math.PI).max(Math.PI).step(0.01);
const D2 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 16),
new THREE.MeshStandardMaterial({
color: "#1B1A17",
metalness: 0,
roughness: 0.5,
})
);
D2.position.set(0, 0.75, 0);
// D2.rotation.z = Math.PI / 4;
D1.add(D2);
this.gui.add(D2.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
const B1 = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 3, 0.5),
new THREE.MeshStandardMaterial({
color: "#E45826",
metalness: 0,
roughness: 0.5,
})
);
B1.position.set(-1.5 * Math.sin(Math.PI / 4), 1, 0);
B1.rotation.z = Math.PI / 4;
D2.add(B1);
const D3 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 16),
new THREE.MeshStandardMaterial({
color: "#1B1A17",
metalness: 0,
roughness: 0.5,
})
);
D3.position.set(0, 1.5, 0);
// D3.rotation.z = -Math.PI / 2;
B1.add(D3);
this.gui.add(D3.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
const B2 = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 3, 0.5),
new THREE.MeshStandardMaterial({
color: "#E45826",
metalness: 0,
roughness: 0.5,
})
);
B2.position.set(1.5, 0, 0);
B2.rotation.z = -Math.PI / 2;
D3.add(B2);
const D4 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 16),
new THREE.MeshStandardMaterial({
color: "#1B1A17",
metalness: 0,
roughness: 0.5,
})
);
D4.position.set(0, 1.5, 0);
// D4.rotation.z = -Math.PI / 4;
B2.add(D4);
this.gui.add(D4.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
const B3 = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 3, 0.5),
new THREE.MeshStandardMaterial({
color: "#E45826",
metalness: 0,
roughness: 0.5,
})
);
// B3.position.set(0, 1.5, 0);
B3.position.set(1.5 * Math.cos(Math.PI / 4), 1.5 * Math.sin(Math.PI / 4), 0);
B3.rotation.z = -Math.PI / 4;
D4.add(B3);
const D5 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 16),
new THREE.MeshStandardMaterial({
color: "#1B1A17",
metalness: 0,
roughness: 0.5,
})
);
D5.position.set(0, 1.5, 0);
// D5.rotation.z = -Math.PI / 2;
B3.add(D5);
this.gui.add(D5.rotation, "z").min(-Math.PI).max(Math.PI).step(0.01);
const B4 = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 1, 0.5),
new THREE.MeshStandardMaterial({
color: "#E45826",
metalness: 0,
roughness: 0.5,
})
);
B4.position.set(0.5, 0, 0);
B4.rotation.z = -Math.PI / 2;
D5.add(B4);
D1.castShadow = true;
D2.castShadow = true;
B1.castShadow = true;
D3.castShadow = true;
B2.castShadow = true;
D4.castShadow = true;
B3.castShadow = true;
D5.castShadow = true;
B4.castShadow = true;
this.scene.add(D1);
this.D1 = D1
this.D2 = D2
this.D3 = D3
this.D4 = D4
this.D5 = D5
}
setRobotRotation(rotation, name, direction) {
this[name].rotation[direction] = rotation
}
/**
* Renderer
*/
initRenderer() {
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
});
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.setSize(this.sizes.width, this.sizes.height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// this.renderer.setClearColor("#fff");
}
/**
* AnimateTick
*/
initAnimateTick() {
const elapsedTime = this.clock.getElapsedTime();
const deltaTime = elapsedTime - this.previousTime;
this.previousTime = elapsedTime;
//Update controls
this.controls.update();
// Render
this.renderer.render(this.scene, this.camera);
// Call tick again on the next frame
window.requestAnimationFrame(() => {
this.initAnimateTick()
});
}
}
-
home/components/Robot3d/index.vue实现3D场景的呈现 -
调用BaseManager类完成渲染 -
并且定义控制角度和视角开关的方法
// 路径:src/views/home/components/Robot3d/index.vue
<template>
<canvas class="webgl" ref="webgl"></canvas>
</template>
<script setup>
import { defineExpose, onMounted } from "vue";
import BaseManager from "./manager/BaseManager.js";
let base = null;
onMounted(() => {
base = new BaseManager(document.querySelector("canvas.webgl"));
});
const setRobotRotation = (e, name, direction) => {
base.setRobotRotation(e, name, direction);
};
const setControlsEnabled = (enabled) => {
base.setControlsEnabled(enabled);
};
defineExpose({ setRobotRotation, setControlsEnabled });
</script>
<style scscope>
.webgl {
width: 100%;
height: 100%;
outline: none;
}
</style>
-
home/components/Menu/index.vue实现控制界面
// 路径:src/views/home/components/Menu/index.vue
<template>
<el-scrollbar height="100%">
<div class="slider-block">
<div class="slider-item">
<span class="demonstration">鼠标视角控制器</span>
<el-switch v-model="mouseValue" @change="switchChange" />
</div>
<div class="slider-item">
<span class="demonstration">关节一(绕Y轴旋转)</span>
<el-slider
v-model="value1"
show-input
:min="min"
:max="max"
:step="0.01"
@input="sliderInput($event, 'D1', 'y')"
/>
</div>
<div class="slider-item">
<span class="demonstration">关节二(绕Z轴旋转)</span>
<el-slider
v-model="value2"
show-input
:min="min"
:max="max"
:step="0.01"
@input="sliderInput($event, 'D2', 'z')"
/>
</div>
<div class="slider-item">
<span class="demonstration">关节三(绕Z轴旋转)</span>
<el-slider
v-model="value3"
show-input
:min="min"
:max="max"
:step="0.01"
@input="sliderInput($event, 'D3', 'z')"
/>
</div>
<div class="slider-item">
<span class="demonstration">关节四(绕Z轴旋转)</span>
<el-slider
v-model="value4"
show-input
:min="min"
:max="max"
:step="0.01"
@input="sliderInput($event, 'D4', 'z')"
/>
</div>
<div class="slider-item">
<p class="demonstration">关节五</p>
<span class="demonstration">绕x轴旋转</span>
<el-slider
v-model="value5_1"
show-input
:min="min"
:max="max"
:step="0.01"
@input="sliderInput($event, 'D5', 'x')"
/>
<span class="demonstration">绕y轴旋转</span>
<el-slider
v-model="value5_2"
show-input
:min="min"
:max="max"
:step="0.01"
@input="sliderInput($event, 'D5', 'y')"
/>
<span class="demonstration">绕Z轴旋转</span>
<el-slider
v-model="value5_3"
show-input
:min="min"
:max="max"
:step="0.01"
@input="sliderInput($event, 'D5', 'z')"
/>
</div>
</div>
</el-scrollbar>
</template>
<script setup>
import { ref, defineEmits } from "vue";
const mouseValue = ref(true);
const value1 = ref(0);
const value2 = ref(0);
const value3 = ref(0);
const value4 = ref(0);
const value5_1 = ref(0);
const value5_2 = ref(0);
const value5_3 = ref(0);
const min = ref(Number(-Math.PI.toFixed(2)));
const max = ref(Number(Math.PI.toFixed(2)));
const emit = defineEmits(["sliderInput", "switchChange"]);
const sliderInput = (e, name, direction) => {
emit("sliderInput", e, name, direction);
};
const switchChange = (e) => {
emit("switchChange", e);
};
</script>
<style scope>
.slider-block {
padding: 20px 10px;
}
.slider-item {
margin: 20px 0;
}
.demonstration {
margin: 0 10px 10px 0;
}
</style>
-
引入home页的两个自定义组件
// 路径:src/views/home/index.vue
<template>
<el-container>
<el-drawer v-model="drawer" direction="ltr" size="100%">
<el-aside>
<Menu @sliderInput="sliderInput" @switchChange="switchChange" />
</el-aside>
</el-drawer>
<el-main>
<div class="btn" v-show="!drawer">
<el-button
type="primary"
:icon="Operation"
circle
size="large"
@click="drawerSwitch"
/>
</div>
<Robot3d ref="Robot3dRef" />
</el-main>
</el-container>
</template>
<script setup>
import { ref } from "vue";
import Menu from "./components/Menu/index.vue";
import Robot3d from "./components/Robot3d/index.vue";
import { Operation } from "@element-plus/icons-vue";
const Robot3dRef = ref();
const sliderInput = (e, name, direction) => {
Robot3dRef.value.setRobotRotation(e, name, direction);
};
const switchChange = (e) => {
Robot3dRef.value.setControlsEnabled(e);
};
const drawer = ref(false);
const drawerSwitch = () => {
drawer.value = !drawer.value;
};
</script>
<style scscope>
.el-aside {
width: 100%;
background-color: #304156;
color: #eee;
}
.el-overlay {
max-width: 450px;
}
.el-drawer__header {
margin: 0;
background-color: #304156;
color: #eee;
}
.el-drawer__body {
background-color: #304156;
padding: 0;
}
.el-container {
height: 100%;
}
.el-main {
padding: 0;
margin: 0;
overflow: hidden;
outline: none;
}
.btn {
position: fixed;
bottom: 5%;
left: 50%;
transform: translateX(-50%);
}
</style>
-
通过上述核心代码就能构建机械臂控制和预览
四、源码地址
https://github.com/jiangzetian/vue3-robot