DAE模型的kinematics刚体运动

1,266 阅读3分钟

1-DAE简介

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

DAE是基于COLLADA的 XML 文件。

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

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

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

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

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

1

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对象:

image-20230407204559212

  • 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…