DAE模型的kinematics刚体运动

1-DAE简介

DAE全称 Digital Asset Exchange file,用于交互式3D应用程序间的数据传递。

DAE是基于COLLADA的 XML 文件。

COLLADA 是一种用于图形软件程序间,传递数字模型的开放式XML方案。

COLLADA 已被国际标准化组织采纳,定为公开可用的规范。

COLLADA 中定义了 Kinematics 运动学元素,通过Kinematics 可以在建模的时候就定义好模型的运动方式,比如旋转轴、旋转范围、模型类型等,这样我们可以在导入模型之后,更快捷的制作模型动画。

接下来我们要说的便是如何通过Kinematics 制作机械臂的运动。

效果演示:www.yxyy.name/examples/we…

DAE模型的kinematics刚体运动

2-vue3+three.js 实现机械臂运动

1.建立vue 项目。我选择vue并没有什么其它目的,你若喜欢,用react也行。

npm create viteProject name: robotSelect a framework: » VueSelect 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对象:

DAE模型的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慢慢

(0)
上一篇 2023年4月8日 上午10:10
下一篇 2023年4月8日 上午10:20

相关推荐

发表回复

登录后才能评论