ティラノスクリプトの3D その8:3d_motion
初めに
今回は、既存タグ [3d_motion] の機能拡張版です。元の機能は、オブジェクトに内包しているモーションを選んでアニメーションさせるだけのものでたが、ここでのタグは、モーションデータのファイルを読み込んで、新しいアニメーションをさせる機能を付加してます。特に、モーションデータを別ファイルに持つ形式のMMDやVRM対応が新しいです。
アニメーションの機構
three.js でのアニメーション
three.js では、アニメーションする オブジェクトに対して、どんなアニメーションかを定義している、AnimationClip(以降 Clip) というデータがあります。この間をつなぐ役目がアニメーションシステムです。その中核に AnimationMixer(以降 Mixer) があります。これは、一つのオブジェクトに対して一つが対応します。一方、Clip には、AnimationAction(以降 Action) が一対一で対応しています。Mixer は、オブジェクトを指定して生成されます。そして、この Mixer の ClipAction() というメソッドに Clip を渡すことで、Action を返します。これによって、オブジェクトと Clip とが関連付けられます。特徴的な点は、一つの Mixer に複数の Action が対応できるという点です。このことで、夫々に重みづけしてモーションさせたり、クロスフェードのような効果を実現したり、アニメーションと、表情のモーションを重ね合わせて実行させたり出来るわけです。
実際にアニメーションさせるには、上記の Action に対して、Clip をどのように実行させるかの指定して、play() を行うとそのモーションが実行されます。他のメソッドとしては、fadeIn(),fadeOut(),reset() などがあります。一方、Mixer は、update() メソッドを定期的に呼び出されることで、アニメーションの進行を制御します。つまり、Action で play() しても、Mixer で update() を繰り返さないとアニメーションは実行されません。
ティラノスクリプトでのアニメーション
ティラノスクリプトの3Dアニメーションでは、各3Dオブジェクト単位で、ThreeModel クラスのオブジェクトを作り、TYRANO.kag.tmp.three.models 内に格納します。そして、アニメーション対応のオブジェクトの場合、そのオブジェクトの ThreeModel 内に、Mixer を格納します。そして、フレーム単位で呼ばれるコールバッグ関数内で、この models 内をスキャンして、Mixer があれば、それを update() します。
ですので、ティラノスクリプトでアニメーションさせるには、タグ内で three.js の作法で Mixer を生成し Action を得て、いろいろ設定して、最後のに ThreeModel オブジェクト内に Mixer を登録する必要があります。
MMDのアニメーション
MMDのアニメーションでは、アニメーション中に IK の制御、物理演算の計算という、three.js 自体では関与してない制御を実行する必要があります。そのために、MMDAnimationHelper(以降 Helper) というアニメーションヘルパーを Mixer の代わりに使います。この Helper は、Mixer を内包している形になっています。そして、MMDをちゃんとアニメーションさせるには、Helper を update() しなければいけません。Mixer を update() させると、物理とIKが効いてないモーションになります。
VRMのアニメーション
VRMのモーションに関しては、VRMAというモーションファイルが定義され、それを取り扱う three-vrm-animation.min.js が必要になります。この中には、vrma ファイルを読み込む VRMAnimationLoaderPlugin() と、VRM用の Clip を生成する createVRMAnimationClip() があります。但し、アニメーションを正しく動作させるためには、VRM モデルをロードする、three-vrm.min.js の VRMLoaderPlugin() でロードした内容の中の three.js 用のオブジェクトを内包した vrm というオブジェクトが必要になります。MMDの Helper と異なり、Mixer の代替ではなく、補正のようで、アニメーションの単位ごとに、通常の Mixer の update() と vrm の update() の両方を行う必要があります。
ですので、今回は苦肉の策として、VrmMixer というクラスを自作して、Mixer のフリをさせることで実現しました。
タグ説明
[3d_motion]
既に読み込み済みの3Dモデルを指定して、新たにモーションデータを読み込むか、既に内包しているモーションデータを指定してアニメーションを行います。また、ここで新規に読み込まれたモーションデータは、モデル内部に保存され、何時でも、名前を指定することで再利用することが可能です。
以下にパラメータを示します。
name [必須] アニメーション対象のモデル名です。既に読み込まれている必要があります。
storage [選択] モーションファイル名を指定します。拡張子まで含めて記述します。ファイル名の前に model フォルダ以下のフォルダを "/" 付きで一緒に書くことも可能です。もし、指定が無かった場合は、ファイルのロードは行わず、以下の motion の名前を既存のモーションから探します。デフォルトは、”” です。
読み込めるファイル形式は以下の通りです。
vmd/vpd :MMD用のモーションファイルおよび、ポーズファイルです。
vrma :VRM用のモーションファイルです。
bvh :bvh形式のモーションファイルです。
js/json :この形式で中身がモーションを定義していると見なして読み込みます。
gltf/glb :gltf型式の3Dファイルの animation 部分を読み込みます。中には複数の Clip があり得ます。
dae :Collada形式の3Dファイルの animation 部分を読み込みます。中には複数の Clip があり得ます。
fbx :FBX形式の3Dファイルの animation 部分を読み込みます。中には複数の Clip があり得ます。
上記のファイル形式のボーン名やモーフ名は、現状ではありのままロードして、オブジェクトとの対応は無視しています。
folder [選択] これを指定すると、dataフォルダ下の任意の場所にファイルを保存できます。但しこの時は、storage には、フォルダを指定しないようにしてください。デフォルトは、"" です。
motion_folder [選択] これを指定すると、others/3d/ 以下のフォルダー構成と見なして、そこから読み込みます。これを指定した場合は、folder を指定しないようにしてください。デフォルトは、"" です。
motion [選択] :モーション名です。もし、storage が無い場合は、必須になります。storage がある場合、その中に複数の Clip がある場合、そのどれをアニメーションするかを指定します。また、単一の Clip でも、名前なしの場合は、この名前が付けられます。もし名前付けが必要なのに、ここに名前が無い場合は、storage の文字列が名前になります。
target [選択]:もしモーションが永久ループでない場合、モーション終了時にスクリプトのこのラベルにジャンプします。飛び先は、*を付けてください。デフォルトは、"" です。
loop [選択]:ループ数を指定します。永久ループにしたい場合は、"-1" を指定します。デフォルトは、"-1" です。
clamp[選択] :モーションが永久ループでない場合、終了時最後のポーズを保持します。"false" の場合は、初期ポーズで、"true" の場合は、最後のポーズを保持します。デフォルトは、"false" です。
fadein [選択] :モーションを開始するときの、フェードイン時間を ms 単位で指定します。デフォルトでは、"0" です。
pmxflag [選択] :モーションが、pmxファイルの場合、うまくモーションされない場合、"true" に設定するとうまく動作する場合があります。通常は、"false" で構いません。デフォルトは、"false" です。
physics [選択] :MMDの場合に物理演算が不要な場合、"false" を指定します。デフォルトは、"true" です。
VPDUCode [選択] :MMDの場合、VPDポーズファイルの文字コードが、SJISの場合は、"false" を指定します。通常は、"false" です。
コード
VRMA対応のライブラリの追加とBVH対応のライブラリを追加しました。
init.ks も修正しました。
[iscript]
array_scripts = [
//"./data/others/plugin/extend3D/three.js",
//"./data/others/plugin/extend3D/loaders/GLTFLoader.js",
//"./data/others/plugin/extend3D/loaders/OBJLoader.js",
//"./data/others/plugin/extend3D/loaders/MTLLoader.js",
//"./tyrano/libs/three/controls/TransformControls.js",
"./data/others/plugin/extend3D/ammo.js",
"./data/others/plugin/extend3D/loaders/AmmoPhysics.js",
"./data/others/plugin/extend3D/animation/CCDIKSolver.js",
"./data/others/plugin/extend3D/loaders/MMDLoader.js",
"./data/others/plugin/extend3D/libs/mmdparser.js",
"./data/others/plugin/extend3D/animation/MMDAnimationHelper.js",
"./data/others/plugin/extend3D/animation/MMDPhysics.js",
"./data/others/plugin/extend3D/shaders/MMDToonShader.js",
"./data/others/plugin/extend3D/loaders/TGALoader.js",
//"./data/others/plugin/extend3D/controls/OrbitControls.js",
"./data/others/plugin/extend3D/effects/OutlineEffect.js",
"./data/others/plugin/extend3D/loaders/TDSLoader.js",
"./data/others/plugin/extend3D/loaders/ColladaLoader.js",
"./data/others/plugin/extend3D/loaders/FBXLoader.js",
"./data/others/plugin/extend3D/libs/fflate.min.js",
"./data/others/plugin/extend3D/vrm/three-vrm.min.js",
"./data/others/plugin/extend3D/vrm/three-vrm-animation.min.js",
"./data/others/plugin/extend3D/loaders/BVHLoader.js",
]
//debugger;
//複数のスクリプトを一括して読み込み、但し前のスクリプトの読み込みが終わるまで次の読み込みは行わない。
var array_size = array_scripts.length;
var arr_id = 0;
if( 0 < array_size ){
function get_Script(src) {
$.getScript(src, function() {
if( 0 < src.indexOf('ammo',20) ){
Ammo().then( function( AmmoLib ) {
Ammo = AmmoLib;
});
}
arr_id++;
if( arr_id < array_size ){
get_Script(array_scripts[arr_id]);
}
});
};
get_Script(array_scripts[arr_id]);
}
//https://unpkg.com/three@0.147.0/build/three.js';
[endscript]
[loadjs storage="/plugin/extend3d/extend3D.js"]
[macro name="3d_dome_new"]
[3d_dome *]
[endmacro]
[return]
arryに2個追加してます。
class VrmMixer extends THREE.AnimationMixer {
constructor ( vrm ){
super( vrm.scene );
this.vrm = vrm;
}
update( d ){
super.update(d);
this.vrm.update(d);
}
}
tyrano.plugin.kag.tag["3d_motion"] = {
vital : ["name"],
pm : {
name:"",
motion:"",
storage:"",
folder:"",
motion_folder: "model",
target:"",
loop:"-1",
clamp:"false",
fadein: "0",
pmxflag:"false",
physics:"true",
VPDUCode:"false",
visible:"true",
},
start : function(pm) {
let vpdCode = ( pm.VPDUCode =="false" || pm.VPDUCode == false ) ? false : true;
let pmxflag = ( pm.pmxFlag == "false" || pm.pmxflag == false ) ? false : true;
let physics = ( pm.physics == "false" || pm.physics == false ) ? false : true;
let loop = parseInt(pm.loop);
let motionname = pm.motion;
let three = this.kag.tmp.three;
let scene = three.scene;
//debugger;
if (0 != $.checkThreeModel(pm.name)) {
let model = three.models[pm.name] ;
let obj = model.model;
let mixer = model.mixer ;
if( pm.storage != ""){
// 新規のモーションの取り込み
if( motionname == "") motionname = pm.storage;
let storage_url = $.get_fullpath( pm.storage, pm.folder, pm.motion_folder);
var ext = $.getExt(pm.storage);
if( ext=="vmd" ){
//debugger;
// mmdのモーションの読み込み
let loder = new THREE.MMDLoader();
loder.loadAnimation(storage_url,obj, (animation)=>{
//debugger;
animation.name = motionname ;
obj.animations.push(animation);
if( obj.userData["type"] == "mmd" ){
// mmdモデルの場合
var helper = get_helper();
mixer = helper.objects.get( obj ).mixer;
//ティラノスクリプトでupdate()実行の為に mixerにhelperを忍び込ませる;
model.mixer = helper;
set_motion_action(animation);
}
else{
// 他のモデルの場合は、単なるanimetionデータと見なす
contro.log( "!!!MMD でないモデル:" + model.name + " にvmdアニメーションを適用しました!!!" );
set_motion_action( animation );
}
},function(xhr){},
function(error){ alert( storage_url + "のロードに失敗しました。" + error );}
);
}
else if( ext =="vpd"){
// mmdのポーズファイルの読み込み
if( obj.userData["type"] == "mmd" ){
let loder = new THREE.MMDLoader();
loder.loadVPD(storage_url,vpdCode, (pose)=>{
//if( mixer){ mixer.stopAllAction();}
let helper = get_helper();
helper.pose(obj,pose );
},function(xhr){},
function(error){ alert( storage_url + "のロードに失敗しました。" + error ); }
);
}
// ポーズの場合を他のモデルへの適用はベンディング
this.kag.ftag.nextOrder();
}
else if( ext == "vrma" ){
// vrm用モーションファイルvrma の読み込み
const loader = new THREE.GLTFLoader();
loader.register((parser) => {
return new THREE.VRMAnimationLoaderPlugin(parser);
});
loader.load( storage_url, (gltf) =>{
//debugger;
const animations = gltf.userData.vrmAnimations;
if (animations != null) {
let animation = animations[0];
//animation.name = motionname;
//obj.animations.push(animation);
//obj.animations = obj.animations.concat(animations);
//if (currentVrm && currentVrmAnimation) {
//currentMixer = new THREE.AnimationMixer(currentVrm.scene);
if( obj.userData["type"] == "vrm" ){
if( !mixer ){
mixer = new VrmMixer( model.vrm );
model.mixer = mixer;
}
const clip = THREE.createVRMAnimationClip(animation, model.vrm);
clip.name = motionname;
obj.animations.push(clip);
set_motion_action( clip);
}
else{
animation.name = motionname;
obj.animations.push(animation);
set_motion_action( animation );
}
}
},function(xhr){},
function(error){ alert( storage_url + "のロードに失敗しました。" + error );}
);
}
else if( ext == "bvh"){
let loader = new THREE.BVHLoader();
loader.load( storage_url, (data)=>{
debugger;
let animation = data.clip;
if(animation.name == ""){
animation.name = motionname;
}
obj.animations.push(animation);
set_motion_action(animation);
},function(error){},
function(error){
alert( storage_url + "のロードに失敗しました。" + error );
} );
}
else if( ext == "js" ){
//let jsondata = require(storage_url);
// json形式モーションファイルの場合
let loader = new THREE.AnimationLoader();
loader.load( storage_url , (animation)=>{
if(animation.name == ""){
animation.name = motionname;
}
obj.animations.push(animation);
set_motion_action(animation);
},function(error){},
function(error){
alert( storage_url + "のロードに失敗しました。" + error );
} );
}
else if( ext == "json" ){
let data = require(storage_url);
let animation = new THREE.AnimationClip(data.name, data.duration, data.tracks);
if(animation.name == ""){
animation.name = motionname;
}
obj.animations.push( animation ) ;
set_motion_action( animation );
}
else if( ext == "gltf" || ext == "glb" ){
// gltf形式モーションファイルの場合
let loader = new THREE.GLTFLoader();
loader.load( storage_url , (data)=>{
if( (motionname =="") && (data.animations.length > 0)) {
motionname = data.animations[0].name;
}
obj.animations = obj.animations.concat(data.animations);
let animation = obj.animations.find((clip)=>{
return (clip.name == motionname );
});
set_motion_action(animation);
},function(error){},
function(error){
alert( storage_url + "のロードに失敗しました。" + error );
} );
}
else if(ext=="dae"){
// Colladaファイルロード
var loader = new THREE.ColladaLoader();
loader.load(storage_url,(data)=>{
if( (motionname =="") && (data.scene.animations.length > 0)) {
motionname = data.animations[0].name;
}
obj.animations = obj.animations.concat(data.scene.animations);
let animation = obj.animations.find((clip)=>{
return (clip.name == motionname );
});
set_motion_action(animation);
},function(error){},
function(error){
alert( storage_url + "のロードに失敗しました。" + error );
} )
}else if(ext=="fbx"){
// fbxファイルロード
var loader = new THREE.FBXLoader();
loader.load(storage_url,(data)=>{
if( (motionname =="") && (data.animations.length > 0)) {
motionname = data.animations[0].name;
}
obj.animations = obj.animations.concat(data.animations);
let animation = obj.animations.find((clip)=>{
return (clip.name == motionname );
});
set_motion_action(animation);
},function(error){},
function(error){
alert( storage_url + "のロードに失敗しました。" + error );
} )
}
}
else{
// 既存のアニメーションから選択
//debugger;
if( motionname == ""){
alert( "モーション名が指定されてません。" )
}
else{
let animation = obj.animations.find((clip)=>{
return (clip.name == motionname );
});
if( animation ){
if( obj.userData["type"] == "mmd" ){
if( !model.mixer ){
var helper = get_helper();
model.mixer = helper;
}
}
set_motion_action(animation);
}
else{
alert( "モーション名:" + motionname + " がありません。")
}
}
}
function get_helper(){
if(obj.userData["helper"]){
helper=obj.userData["helper"];
return helper;
}
else{
helper = new THREE.MMDAnimationHelper({ pmxAnimation: pmxflag });
helper.add(obj, { animation: obj.animations, physics:physics } );
obj.userData["helper"] = helper;
return helper;
}
}
function set_motion_action( animation ){
if( !mixer ){
mixer = new THREE.AnimationMixer(obj);
model.mixer = mixer;
}
else{
mixer.stopAllAction();
}
var action = mixer.clipAction(animation);
obj.userData["action"] = action;
if( pm.target != "" && pm.target[0] == "*"){
let t = {"target" : pm.target };
mixer.addEventListener( 'finished', function( e ) {
kag.layer.showEventLayer();
kag.ftag.startTag("jump", t );
});
}
if( loop == 1 ){
action.setLoop(THREE.LoopOnce,1);
}else if( 1 < loop ){
action.setLoop(THREE.LoopRepeat,loop);
}
if( pm.clamp =="false" || pm.clamp ==false ){
action.clampWhenFinished = false;
}
else{
action.clampWhenFinished = true ;
}
obj.visible= true;
let fadein = parseFloat( pm.fadein );
fadein = fadein * 0.001;
if( 0 < fadein ){
action.reset().fadeIn().play()
}
else{
action.reset().play();
}
}
}
else{
alert( pm.name + "は、シーン内にありませんでした。" );
}
this.kag.ftag.nextOrder();
},
};
TYRANO.kag.ftag.master_tag["3d_motion"] = TYRANO.kag.tag["3d_motion"];
TYRANO.kag.ftag.master_tag["3d_motion"].kag = TYRANO.kag;
今後の課題
今回は、単一モーションでのアニメーションの動作に限定したものになってます。ただ、three.js では、複数のモーションの同時動作や、クロスフェード機能もあります。この場合、MMDのVPDのフェード機能が欲しい所ですが、今の所実現方法が不明です。
また、複数のオブジェクトに対して、同一のモーションを適用するための、AnimationObjectGroup という機能もあります。一人が複数の服を着替えることが可能になりそうな機能です。また、異なるファイル形式同士の相互ボーン名変換機能などもあると、ファイルの相互利用に便利になります。
ただ、今の所それらを一気に実現するのは、複雑すぎますので、適時機能追加していきたいと思います。
この記事が気に入ったらサポートをしてみませんか?