Этот довольно простой процесс, кажется, вызывает много проблем у многих людей. Здесь я привожу очень краткое руководство по дублированию анимации в 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(); })
Проявите творческий подход. Мы всегда можем оптимизировать :).
Эта статья основана на части моей дипломной работы инженера. Все скриншоты принадлежат мне и изображают мою оригинальную работу.