1-DAE简介
DAE全称 Digital Asset Exchange file,用于交互式3D应用程序间的数据传递。
DAE是基于COLLADA的 XML 文件。
COLLADA 是一种用于图形软件程序间,传递数字模型的开放式XML方案。
COLLADA 已被国际标准化组织采纳,定为公开可用的规范。
COLLADA 中定义了 Kinematics 运动学元素,通过Kinematics 可以在建模的时候就定义好模型的运动方式,比如旋转轴、旋转范围、模型类型等,这样我们可以在导入模型之后,更快捷的制作模型动画。
接下来我们要说的便是如何通过Kinematics 制作机械臂的运动。
效果演示:www.yxyy.name/examples/we…
2-vue3+three.js 实现机械臂运动
1.建立vue 项目。我选择vue并没有什么其它目的,你若喜欢,用react也行。
npm create vite
√ Project name: robot
√ Select a framework: » Vue
√ Select a variant: » TypeScript
Scaffolding project in D:\work\canvas引擎\canvas-stamp...
Done. Now run:
cd canvas-lmm
npm install
npm run dev
接下来按照提示,安装依赖,运行项目即可。
2.安装three.js
npm i @types/three
package.json文件如下:
{
"name": "hand",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@types/three": "^0.150.1",
"three": "^0.151.3",
"vue": "^3.2.47"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"typescript": "^4.9.3",
"vite": "^4.2.0",
"vue-tsc": "^1.2.0"
}
}
3.在App.vue 中导入dae模型,制作补间动画。
<script setup lang="ts">
import {
EquirectangularReflectionMapping,
Mesh,
MeshStandardMaterial,
PerspectiveCamera,
Scene,
WebGLRenderer,
MathUtils,
DirectionalLight,
} from 'three'
import { onMounted, ref } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader'
// 对应canvas 画布的Ref对象
const canvasRef = ref<HTMLCanvasElement>()
let renderer: WebGLRenderer
let controls: OrbitControls
const scene = new Scene()
const camera = new PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
10000
)
camera.position.set(1.5, 1.5, 3)
/* 环境光 */
new RGBELoader()
.loadAsync('https://ycyy-cdn.oss-cn-beijing.aliyuncs.com/box/env_shop.hdr')
.then((texture) => {
texture.mapping = EquirectangularReflectionMapping
scene.environment = texture
})
/* 灯光 */
{
const light = new DirectionalLight(0xffffff, 1)
light.position.set(0, 10, 0)
light.castShadow = true
scene.add(light)
}
let kinematics: any
// 补间起始时间
let startTime = new Date().getTime()
// 补间时间长度
let timeLen = getTimeLen()
// 插值
let inter = 0
// 关节数据
type Joint = {
name: string
rotate1: number
rotate2: number
}
// 关节补间数据
let joints: Joint[] = []
// 暂停
let pause = false
// 动画帧
let fm = 0
// 加载模型
const robot = new ColladaLoader().loadAsync(
'https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/models/robot.dae'
)
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) {
return
}
const ratio = window.devicePixelRatio
const { innerWidth, innerHeight } = window
canvas.width = innerWidth * ratio
canvas.height = innerHeight * ratio
canvas.style.width = innerWidth + 'px'
canvas.style.height = innerHeight + 'px'
renderer = new WebGLRenderer({ canvas })
renderer.shadowMap.enabled = true
controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0.7, 0)
controls.update()
robot.then((collada) => {
collada.scene.traverse((obj) => {
if (obj instanceof Mesh) {
obj.material = new MeshStandardMaterial({
color: 0xaaaaaa,
roughness: 0.4,
metalness: 1,
})
obj.geometry.computeVertexNormals()
obj.castShadow = true
obj.receiveShadow = true
}
})
kinematics = collada.kinematics
console.log(kinematics)
joints = getJoints()
scene.add(collada.scene)
animate()
})
})
// 空格暂停
window.addEventListener('keydown', (event) => {
if (event.code === 'Space') {
if (pause) {
pause = false
resetTween()
animate()
} else {
pause = true
cancelAnimationFrame(fm)
}
}
})
/* 补间数据 */
function tween() {
joints.forEach(({ name, rotate1, rotate2 }) => {
kinematics.setJointValue(name, rotate1 + (rotate2 - rotate1) * inter)
})
}
/* 随机时间 */
function getTimeLen() {
return MathUtils.randInt(1000, 2000)
}
/* 随机关节数据 */
function getJoints(): Joint[] {
const data: Joint[] = []
for (let [key, val] of Object.entries(kinematics.joints as object)) {
if (!val.static) {
const { min, max } = val.limits
data.push({
name: key,
rotate1: kinematics.getJointValue(key),
rotate2: MathUtils.randInt(min, max),
})
}
}
return data
}
function animate() {
inter = (new Date().getTime() - startTime) / timeLen
if (inter > 1) {
resetTween()
}
tween()
renderer.render(scene, camera)
fm = requestAnimationFrame(animate)
}
function resetTween() {
inter = 0
startTime = new Date().getTime()
joints = getJoints()
}
</script>
<template>
<canvas id="canvas" ref="canvasRef"></canvas>
</template>
<style scoped>
#canvas {
background-color: antiquewhite;
}
</style>
这是整体代码,接下来咱们详细解释一下。
3-代码解析
1.使用three.js 的ColladaLoader 可以加载dae模型。
const robot = new ColladaLoader().loadAsync(
'https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/models/robot.dae'
)
new ColladaLoader().loadAsync()返回的是Promise 对象。
2.在onMounted中,我们可以用robot.then() 方法接收模型。
onMounted(() => {
……
robot.then((collada) => {
collada.scene.traverse((obj) => {
if (obj instanceof Mesh) {
obj.material = new MeshStandardMaterial({
color: 0xaaaaaa,
roughness: 0.4,
metalness: 1,
})
obj.geometry.computeVertexNormals()
obj.castShadow = true
obj.receiveShadow = true
}
})
kinematics = collada.kinematics
console.log(kinematics)
joints = getJoints()
scene.add(collada.scene)
animate()
})
})
我用traverse()方法遍历出了所有的Mesh对象,然后用给其添加了一个金属效果的MeshStandardMaterial材质。
因为此模型中没有法线数据,所有我用geometry.computeVertexNormals() 自动计算了法线。
dae模型中会附带一个kinematics 运动学对象,其中带有机械关节数据,以及获取和设置机械关节的方法。
下面是我打印出的kinematics对象:
-
setJointValue(‘joint_1’, 90) 设置关节运动数据
-
getJointValue(‘joint_1’) 获取关节运动数据
-
joints 关节集合,joint_1、joint_2是关节名。
- axis 绕哪个轴旋转
- limits:{min, max} 运动范围
- static 是否是静态物体
关于DAE中的kinematics 的基本操作原理就这么简单。
3.制作机械臂的补间动画。
function tween() {
joints.forEach(({ name, rotate1, rotate2 }) => {
kinematics.setJointValue(name, rotate1 + (rotate2 - rotate1) * inter)
})
}
补间动画就是基于一个时间插值inter,在两个旋转状态间求补间值。
- 补间时间的长度是随机生成的:
function getTimeLen() {
return MathUtils.randInt(1000, 2000)
}
getTimeLen()会返回1s-2s的随机时间。
- 旋转目标值是在相应关节的旋转范围内随机生成的。
function getJoints(): Joint[] {
const data: Joint[] = []
for (let [key, val] of Object.entries(kinematics.joints as object)) {
if (!val.static) {
const { min, max } = val.limits
data.push({
name: key,
rotate1: kinematics.getJointValue(key),
rotate2: MathUtils.randInt(min, max),
})
}
}
return data
}
rotate1是关节旋转的初始状态。
rotate2 是关节旋转的旋转目标值。
- 补间插值inter=(当前时间-补间开始时间)/补间时间长度
function animate() {
inter = (new Date().getTime() - startTime) / timeLen
if (inter > 1) {
resetTween()
}
tween()
renderer.render(scene, camera)
fm = requestAnimationFrame(animate)
}
其余的都很简单,我就不再多说,大家可以参考之前贴出的完整代码。
参考链接
DAE:docs.fileformat.com/3d/dae/
COLLADA:www.khronos.org/files/colla…
原文链接:https://juejin.cn/post/7219249005904936997 作者:李伟_Li慢慢