一、效果预览
PC端
移动端
二、技术栈
web端使用Vue3+element-plus UI
3D显示使用three.js
三、过程
新建Vue3项目
清除无关的新手引导代码
安装vue-router 4
安装three.js
新建src/views/home/index.vue 为主页
新建src/layout/index.vue layout主页
新建src/layout/components/Menu组件
新建src/layout/components/Robot3d组件
三、核心代码
使用three.js构建3D机械臂
除去必要的3D场景元素外,initRobot方法是构建机械臂的核心
总共有活动关节5个分别是:D1~D5
总共有力臂4个分别是:B1~B4
根据结构从D1开始嵌套为子节点
最终渲染生成力臂
其中的setRobotRotation方法对外提供角度控制
其中的setControlsEnabled方法对外提供视角开个控制
// 文件路径:src/views/home/components/Robot3d/manager/BaseManager.jsimport * 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://juejin.cn/post/7098350437723865124