SkinnedMesh克隆后骨骼动画出错

512 阅读2分钟

SkinnedMesh克隆后骨骼动画出错

在生产中,碰到这样的一个需求,就是对于同一个模型文件,加载多次。在最初的想法中,考虑到材质的复用和效率问题,多次加载某个模型直接使用Object3D.clone()方法实现。

但是,在某次加载拥有骨骼动画的模型时出了问题,我们现在先来简单的复现一下这个问题:基于Three.js官方的例子进行修改

Three.js Bones Browser (threejs.org)

创建SkinnedMesh的代码如下

function initBones() {

    // ...

    const geometry = createGeometry(sizing);
    const bones = createBones(sizing);
    mesh = createMesh(geometry, bones);

    mesh.scale.multiplyScalar(1);
    // scene.add( mesh );

    const cmesh = mesh.clone()
    scene.add(cmesh)

    console.log(mesh.skeleton === cmesh.skeleton)
}

mesh没有被添加到场景中,取而代之的是它的克隆对象cmesh。现在,再通过GUI修改Bone对象的位置,会发现场景中的cmesh不会发生变化。

一定是在clone时出了问题,看一下clone()方法。在SkinnedMesh中,没有clone()方法,它的clone()方法继承自Object3D.clone()

// Object3D
clone(recursive) {
    return new this.constructor().copy(this, recursive);
}

可以看到,clone()间接的调用了copy()方法,在SkinnedMesh中,重写了这个方法:

// SkinnnedMesh
copy(source, recursive) {

    super.copy(source, recursive);

    this.bindMode = source.bindMode;
    this.bindMatrix.copy(source.bindMatrix);
    this.bindMatrixInverse.copy(source.bindMatrixInverse);

    this.skeleton = source.skeleton;

    return this;
}

骨骼可以控制模型,是因为Skeleton.update()方法。具体的内容可以通过骨骼原理及源码 - 掘金 (juejin.cn)了解到。

现在通过GUI做一些改动。把Bone 0position.x改为10.

image.png 然后在Skeleton.update()中打个断点。

image.png 可以看到,Bone 0matrix并没有更新。

因为Skeleton.bones中的Bone,都是mesh的子对象,只有当mesh在场景中时,才会更新matrix(Three.js更新矩阵的策略是在绘制时更新,而不是每次修改position就会更新)。

function createMesh(geometry, bones) {

    // ...
    mesh.add(bones[0]);
    mesh.bind(skeleton);
    // ...
    return mesh;
}

mesh进行clone,这些Bone也应该被clone,但是,对于这些Bone来说,是深拷贝,对于mesh.skeleton是浅拷贝,这就出现了错乱: image.png

skeleton依赖mesh.children中的Bone更新矩阵。在mesh.clone()时,对children进行深拷贝,cmesh.children中的Bonemesh.children中的Bone不是相同的对象;而skeleton使用浅拷贝,skeleton.bones需要依赖于mesh进行矩阵更新,而mesh没有出现在场景中,就导致了skeleton.bones的矩阵没有更新,从而cmesh没有办法被Bone控制。