Этот довольно простой процесс, кажется, вызывает много проблем у многих людей. Здесь я привожу очень краткое руководство по дублированию анимации в Three.js, основанное на том, что я узнал на форумах и в документации.

Примечание: код, который я представляю, взят из моего приложения React (поэтому много ключевого слова this и т. д.). Я привожу код для объяснения, а не для копирования :)

Не все сразу

Во-первых, важно помнить, что простое копирование модели не позволит вам скопировать ее анимацию, даже если она определена в FBX или любом другом файле, который вы загружаете. Это отдельные этапы процесса.

Скопируйте модель

Копирование модели очень просто — используйте определенную функцию для копирования объекта геометрии.

Обратите внимание, что я сначала создаю и добавляю материал и устанавливаю другие свойства модели, а затем затемкопирую модель. После клонирования я немного сместил положение, чтобы иметь возможность представить результат.

this.model = obj;
var mat1 = new THREE.MeshPhongMaterial(
{
   color: 0xAA4444,
   skinning: true ,
   morphTargets :true,
   specular: 0x1d1c3a,
   reflectivity: 0.8,
   shininess: 20,
} );
this.model.traverse(o => {
  if (o.isMesh) {
    o.castShadow = true;
    o.receiveShadow = true;
    o.material = mat1;
}
});
// Set the models initial scale
this.model.scale.set(.2, .2,  .2);
this.model.position.y = -1;
this.model.position.x = 0;
this.model2 = this.model.clone();
this.model2.position.x = -4;

Примечание: вам нужно добавить в сцену обемодели.

this.scene.add(this.model);
this.scene.add(this.model2);

Итак, две одинаковые модели на сцене.

Копирование анимации

Теперь вам нужно создать совершенно новый микшер для вашей новой модели. Совет: чтобы легко использовать несколько микшеров, создайте массив, чтобы держать их под рукой.

this.mixers = [];

При создании микшеров на ваших моделях поместите их в этот массив.

this.mixer = new THREE.AnimationMixer(this.model);
this.mixers.push(this.mixer);
this.mixer2 = new THREE.AnimationMixer(this.model2);
this.mixers.push(this.mixer2);

Теперь, когда вы клипируете действие, вам нужно будет сделать это на двух микшерах отдельно:

let fileAnimations = obj.animations;
let anim = fileAnimations[0]; //I have one clip so I don't bother, but there are obviously neater ways to do this ;)
anim.optimize();
let act = this.mixer.clipAction(anim);
let act2 = this.mixer2.clipAction(anim);

Обратите внимание, что мы создаем два отдельных действия из двух отдельных микшеров в одном клипе. Клип не нужно копировать, его можно использовать.

Теперь идет бонусная часть, если она вам не нужна, просто перейдите к следующей.

Изменение нескольких свойств объекта

Допустим, вы хотите ускорить клип. Кроме того, играть без цикла было бы неплохо. О, и вы хотите, чтобы анимация останавливалась в конце клипа, а не возвращалась к первому кадру.

Вы, очевидно, можете сделать это следующим образом:

act.loop = THREE.LoopOnce;
act2.loop = THREE.LoopOnce;

И это для каждого свойства. Я уже засыпаю, когда пишу эти две строчки.

Конечно, сделать что-то вроде этого:

act = {
   loop: THREE.LoopOnce, 
   timeScale: 4
}

перезапишет объект, так что нет.

Выполнение таких сложных трюков:

let x = Object.assign({}, Object.create(act, Object.getOwnPropertyDescriptors({your properties})));

тоже не получится. Почему? act действительно является объектом, поэтому его можно копировать с помощью Object.assign, но он также является экземпляром прототипа AnimationAction. Что делает эта функция выше, так это создает копию экземпляра прототипа (что хорошо), но перезаписывает свойства объекта, а не прототипа — что и хорошо (мы не хотим возиться с прототипом), и плохо, потому что Three по-прежнему будет отдавать приоритет свойствам прототипа. Нам нужно настроить таргетинг на свойства непосредственно в экземпляре.

У меня нет черного пояса по JS, поэтому я не мог найти изящный способ добиться этого с помощью методов Object. Вместо этого я написал свою собственную однострочную функцию, которая делает именно то, что мне нужно.

let overwriteProps = (proto, object) => {
    Object.entries(object).map(entry => {
        proto[entry[0]] = entry[1];
    })   
    return proto;
}

Я использую это так:

let modified = {
   loop : THREE.LoopOnce,
   clampWhenFinished : true,
   timeScale : 4
}
this.action = overwriteProps(act, modified);
this.action2 = overwriteProps(act2, modified);

И у вас есть два действия с обновленными свойствами без разрушения базовых объектов. Аккуратный.

Все, что вам нужно сделать сейчас, это нажать кнопку воспроизведения:

this.action.play();
this.action2.play();

Обновление нескольких микшеров

И последнее, но не менее важное: нам нужно обновить микшеры в функции анимации. Мы должны использовать массив микшеров, чтобы этот процесс прошел гладко. Теперь все, что нам нужно, это простой цикл для перебора массива и обновления каждого микшера отдельно. Обязательно получите доступ к дельте перед циклом!

update=()=>{
   let delta = this.clock.getDelta();
   if(this.mixers.length !== 0){
       for ( let i = 0, l = this.mixers.length; i < l; i ++ ) {
          this.mixers[ i ].update( delta );
       }
   }
   this.renderer.render(this.scene, this.camera);
   requestAnimationFrame(this.update);
}

Последние замечания

И это все. Вот пример анимации, слегка улучшенной в DaVinci Resolve, чтобы она выглядела мило.

Обратите внимание, что в этом примере нам нужно было управлять только двумя копиями. Если вам нужно больше, возможно, вам пригодятся еще несколько массивов. Я уже вижу, что действия и модели могут быть упакованы так же, как микшеры, в массивы. Затем вы можете легко перебирать их и не терять место и время для создания всех этих временных переменных act3, ..4, ..182 и т. д.

Вот пример:

this.model.position.x = -10;
this.models.push(this.model);
for(let i =0; i<4; i++){ //let's make four copies
   let newModel = this.model.clone();
   newModel.position.x = this.model.position.x-4*(i+1);
   this.models.push(newModel);
}
this.models.forEach(model=>{
    this.scene.add(model);
    this.mixers.push(new THREE.AnimationMixer(model));
})
let fileAnimations = obj.animations;
let anim = fileAnimations[0];
anim.optimize();
let modified = {
  loop : THREE.LoopOnce,
  clampWhenFinished : true,
  timeScale : 4
}
this.mixers.forEach(mixer =>{
  this.actions.push(
    overwriteProps( //overwrite props while clipping
       mixer.clipAction(anim),
       modified
    )
  )
})
this.actions.forEach(action=>{
    action.play();
})

Проявите творческий подход. Мы всегда можем оптимизировать :).

Эта статья основана на части моей дипломной работы инженера. Все скриншоты принадлежат мне и изображают мою оригинальную работу.