ポーズについて
最終更新: 2023年1月26日
ポーズ機能とは
ポーズとは複数の同種パーツのうち、単一のパーツだけをモーションに基づいて切り替えをフェード表示する機能です。
複数の同種パーツとは右手Aと右手Bなど同時に表示されると表示的に矛盾を起こすものを指します。
モーション内のパーツ不透明度の操作を最終的にパーツへ適用する処理部分でもあります。
このページへの理解のために「モデルについて」の「CubismModelクラスでモデルに存在しないパラメータIDを使う」を事前に読むことをお勧めします。
モーションからのパーツ不透明度操作の流れ
モーション再生でのパーツ不透明度操作
パーツ不透明度の整合性を保つため、Original WorkFlow の Frameworkでのモーション再生単体では直接パーツ不透明度に対して操作を行いません。
モーション再生のパーツの不透明度操作は、かわりにパーツと同じIDパラメータへ対しての上書きに置き換わります。
このときモデルに存在しないパラメータIDは仮想パラメータとして値の保持だけ行われます。
モーション切り替えに伴うフェード処理などは行われず、ただ上書きのみが行われます。
OW SDKではモーションへのパーツ不透明度操作の補間方法はステップが適用されることを想定しています。
// C++ void CubismMotion::DoUpdateParameters(CubismModel* model, csmFloat32 timeSeconds, csmFloat32 fadeWeight, CubismMotionQueueEntry* motionQueueEntry) { /*省略*/ for (; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_PartOpacity; ++c) { // Find parameter index. parameterIndex = model->GetParameterIndex(curves[c].Id); // Skip curve evaluation if no value in sink. if (parameterIndex == -1) { continue; } // Evaluate curve and apply value. value = EvaluateCurve(_motionData, c, time); model->SetParameterValue(parameterIndex, value);// パーツ不透明度ではなく仮想パラメータへ書き込む。 } /*省略*/ }
// TypeScript public doUpdateParameters(model: CubismModel, userTimeSeconds: number, fadeWeight: number, motionQueueEntry: CubismMotionQueueEntry): void { /*省略*/ for(; c < this._motionData.curveCount && curves.at(c).type == CubismMotionCurveTarget.CubismMotionCurveTarget_PartOpacity; ++c) { // Find parameter index. parameterIndex = model.getParameterIndex(curves.at(c).id); // Skip curve evaluation if no value in sink. if(parameterIndex == -1) { continue; } // Evaluate curve and apply value. value = evaluateCurve(this._motionData, c, time); model.setParameterValueByIndex(parameterIndex, value); } /*省略*/ }
// Java public void doUpdateParameters(CubismModel model, float timeSeconds, float fadeWeight, CubismMotionQueueEntry motionQueueEntry){ /*省略*/ for (CubismMotionCurve curve : curves) { if (curve.type != CubismMotionCurveTarget.PARAMETER) { continue; } // Findparameter index. final int parameterIndex = model.getParameterIndex(curve.id); // Skip curve evaluation if no value. if (parameterIndex == -1) { continue; } // Evaluate curve and apply value. value = evaluateCurve(curve, time); ... model.setParameterValue(parameterIndex, v); // パーツ不透明度ではなく仮想パラメータへ書き込む } /*省略*/ }
ポーズの適用
モデルの更新処理の最後のフェーズでPose機能を適用することで、.pose3.jsonに記述された情報を基にグループごとにどのパーツを表示するか、仮想パラメータの値を参照して決定していきます。
Poseをモデルに適用するAPIは以下になります
// C++ CubismPose::UpdateParameters()
// TypeScript CubismPose.updateParameters()
// Java CubismPose.updateParameters();
パーツ操作のカーブはステップを推奨されていますがリニアなどを選択している場合、0.001より大きくなった時点で表示状態と認識します。
表示パーツの決定時に操作前のパーツ不透明度から差分時間との比例計算で新しい不透明度がリニア補間として決定します。
// C++ void CubismPose::DoFade(CubismModel* model, csmFloat32 deltaTimeSeconds, csmInt32 beginIndex, csmInt32 partGroupCount) { csmInt32 visiblePartIndex = -1; csmFloat32 newOpacity = 1.0f; const csmFloat32 Phi = 0.5f; const csmFloat32 BackOpacityThreshold = 0.15f; // 現在、表示状態になっているパーツを取得 for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i) { csmInt32 partIndex = _partGroups[i].PartIndex; csmInt32 paramIndex = _partGroups[i].ParameterIndex; if (model->GetParameterValue(paramIndex) > Epsilon) // const csmFloat32 Epsilon= 0.001f; { if (visiblePartIndex >= 0) { break; } visiblePartIndex = i; newOpacity = model->GetPartOpacity(partIndex); // 新しい不透明度を計算 newOpacity += (deltaTimeSeconds / _fadeTimeSeconds); if (newOpacity > 1.0f) { newOpacity = 1.0f; } } } /*省略*/ }
// TypeScript public doFade(model: CubismModel, deltaTimeSeconds: number, beginIndex: number, partGroupCount: number): void { let visiblePartIndex: number = -1; let newOpacity: number = 1.0; const phi: number = 0.5; const backOpacityThreshold: number = 0.15; // 現在、表示状態になっているパーツを取得 for(let i: number = beginIndex; i < beginIndex + partGroupCount; ++i) { const partIndex: number = this._partGroups.at(i).partIndex; const paramIndex: number = this._partGroups.at(i).parameterIndex; if(model.getParameterValueByIndex(paramIndex) > Epsilon) { if(visiblePartIndex >= 0) { break; } visiblePartIndex = i; newOpacity = model.getPartOpacityByIndex(partIndex); // 新しい不透明度を計算 newOpacity += (deltaTimeSeconds / this._fadeTimeSeconds); if(newOpacity > 1.0) { newOpacity = 1.0; } } } /*省略*/ }
// Java public void doFade(CubismModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount) { int visiblePartIndex = -1; float newOpacity = 1.0f; // 現在、表示状態になっているパーツを取得 for (int i = beginIndex; i < beginIndex + partGroupCount; i++) { final int paramIndex = partGroups.get(i).parameterIndex; if (model.getParameterValue(paramIndex) > EPSILON) { if (visiblePartIndex >= 0) { break; } // 新しい不透明度を計算 newOpacity = calculateOpacity(model, i, deltaTimeSeconds); visiblePartIndex = i; } } /*省略*/ }
表示パーツとその新しい不透明度が決定したあと、グループ全体に対して不透明度の上書き処理が実行されます。
非表示パーツの新しい不透明度は表示するパーツの不透明度との関係で背景が透けないレベルで不透明度を低下させていきます。
// C++ void CubismPose::DoFade(CubismModel* model, csmFloat32 deltaTimeSeconds, csmInt32 beginIndex, csmInt32 partGroupCount) { csmInt32 visiblePartIndex = -1; csmFloat32 newOpacity = 1.0f; const csmFloat32 Phi = 0.5f; const csmFloat32 BackOpacityThreshold = 0.15f; /*省略*/ if (visiblePartIndex < 0) { visiblePartIndex = 0; newOpacity = 1.0f; } // 表示パーツ、非表示パーツの不透明度を設定する for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i) { csmInt32 partsIndex = _partGroups[i].PartIndex; // 表示パーツの設定 if (visiblePartIndex == i) { model->SetPartOpacity(partsIndex, newOpacity); //j/ 先に設定 } // 非表示パーツの設定 else { csmFloat32 opacity = model->GetPartOpacity(partsIndex); csmFloat32 a1; // 計算によって求められる不透明度 if (newOpacity < Phi) { a1 = newOpacity * (Phi - 1) / Phi + 1.0f; // (0,1),(phi,phi)を通る直線式 } else { a1 = (1 - newOpacity) * Phi / (1.0f - Phi); // (1,0),(phi,phi)を通る直線式 } // 背景の見える割合を制限する場合 csmFloat32 backOpacity = (1.0f - a1) * (1.0f - newOpacity); if (backOpacity > BackOpacityThreshold) { a1 = 1.0f - BackOpacityThreshold / (1.0f - newOpacity); } if (opacity > a1) { opacity = a1; // 計算の不透明度よりも大きければ(濃ければ)不透明度を上げる } model->SetPartOpacity(partsIndex, opacity); } } }
// TypeScript public doFade(model: CubismModel, deltaTimeSeconds: number, beginIndex: number, partGroupCount: number): void { let visiblePartIndex: number = -1; let newOpacity: number = 1.0; const phi: number = 0.5; const backOpacityThreshold: number = 0.15; /*省略*/ if(visiblePartIndex < 0) { visiblePartIndex = 0; newOpacity = 1.0; } // 表示パーツ、非表示パーツの不透明度を設定する for(let i: number = beginIndex; i < beginIndex + partGroupCount; ++i) { const partsIndex: number = this._partGroups.at(i).partIndex; // 表示パーツの設定 if(visiblePartIndex == i) { model.setPartOpacityByIndex(partsIndex, newOpacity); // 先に設定 } // 非表示パーツの設定 else { let opacity: number = model.getPartOpacityByIndex(partsIndex); let a1: number; // 計算によって求められる不透明度 if(newOpacity < phi) { a1 = newOpacity * (phi - 1) / phi + 1.0; // (0,1),(phi,phi)を通る直線式 } else { a1 = (1 - newOpacity) * phi / (1.0 - phi); // (1,0),(phi,phi)を通る直線式 } // 背景の見える割合を制限する場合 const backOpacity: number = (1.0 - a1) * (1.0 - newOpacity); if(backOpacity > backOpacityThreshold) { a1 = 1.0 - backOpacityThreshold / (1.0 - newOpacity); } if(opacity > a1) { opacity = a1; // 計算の不透明度よりも大きければ(濃ければ)不透明度を上げる } model.setPartOpacityByIndex(partsIndex, opacity); } } }
// Java public void doFade(CubismModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount) { int visiblePartIndex = -1; float newOpacity = 1.0f; /*省略*/ if (visiblePartIndex < 0) { visiblePartIndex = 0; newOpacity = 1.0f; } // 表示パーツ、非表示パーツの不透明度を設定する for (int i = beginIndex; i < beginIndex + partGroupCount; i++) { final int partsIndex = partGroups.get(i).partIndex; // 表示パーツの設定 if (visiblePartIndex == i) { model.setPartOpacity(partsIndex, newOpacity); } // 非表示パーツの設定 else { final float opacity = model.getPartOpacity(partsIndex); final float result = calcNonDisplayedPartsOpacity(opacity, newOpacity); model.setPartOpacity(partsIndex, result); } } }
Poseのデータ構造
Poseが取り扱う情報はパラメータ、パーツ情報へのアクセスを高速にするため、パーツIDでアクセスしたときのパラメータ、パーツのインデックスをPartData構造体で固めて保有します。
また連動するデータはLinkとしてPartDataの子要素として保有します。
// C++ /** * @brief パーツにまつわるデータを管理 * * パーツにまつわる諸々のデータを管理する。 */ struct PartData { /*省略*/ CubismIdHandle PartId; ///< パーツID csmInt32 ParameterIndex; ///< パラメータのインデックス csmInt32 PartIndex; ///< パーツのインデックス csmVector<PartData> Link; ///< 連動するパラメータ };
// TypeScript /** * パーツにまつわるデータを管理 */ export class PartData { /*省略*/ partId: CubismIdHandle; // パーツID parameterIndex: number; // パラメータのインデックス partIndex: number; // パーツのインデックス link: csmVector<PartData>; // 連動するパラメータ }
// Java /** * パーツにまつわる諸々のデータを管理する。 */ public class PartData{ /*省略*/ CubismId partId; // パーツID int parameterIndex; // パラメータのインデックス int partIndex; // パーツのインデックス List<PartData> link // 連動するパラメータ }
CubismPoseはグループ情報を一次元のPartData配列と各グループの個数情報の配列で表現します。
// C++ class CubismPose { /*省略*/ csmVector<PartData> _partGroups; ///< パーツグループ csmVector<csmInt32> _partGroupCounts; ///< それぞれのパーツグループの個数 csmFloat32 _fadeTimeSeconds; ///< フェード時間[秒] csmFloat32 _lastTimeSeconds; ///< 最後に実行した時刻[秒] CubismModel* _lastModel; ///< 前回操作したモデル };
// TypeScript export class CubismPose { /*省略*/ _partGroups: csmVector<PartData>; // パーツグループ _partGroupCounts: csmVector<number>; // それぞれのパーツグループの個数 _fadeTimeSeconds: number; // フェード時間[秒] _lastModel: CubismModel; // 前回操作したモデル }
// Java public class CubismPose{ /*省略*/ List<PartData> partGroups; // パーツグループ List<Integer> partGroupCounts; // それぞれのパーツグループの個数 float fadeTimeSeconds; // フェード時間[秒] float lastTimeSeconds; // 最後に実行した時刻[秒] CubismModel lastModel; // 前回操作したモデル }
一次元配列のPartDataは各グループの個数情報をもとに配列上の先頭位置と要素数をDoFadeへ渡すことによりグループとして処理がされます。
// C++ void CubismPose::UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds) { // 前回のモデルと同じではないときは初期化が必要 if (model != _lastModel) { // パラメータインデックスの初期化 Reset(model); } _lastModel = model; // 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間0として対応。 if (deltaTimeSeconds < 0.0f) { deltaTimeSeconds = 0.0f; } csmInt32 beginIndex = 0; for (csmUint32 i = 0; i < _partGroupCounts.GetSize(); i++) { csmInt32 partGroupCount = _partGroupCounts[i]; DoFade(model, deltaTimeSeconds, beginIndex, partGroupCount); beginIndex += partGroupCount; } CopyPartOpacities(model); }
// TypeScript public updateParameters(model: CubismModel, deltaTimeSeconds: number): void { // 前回のモデルと同じでない場合は初期化が必要 if(model != this._lastModel) { // パラメータインデックスの初期化 this.reset(model); } this._lastModel = model; // 設定から時間を変更すると、経過時間がマイナスになる事があるので、経過時間0として対応 if(deltaTimeSeconds < 0.0) { deltaTimeSeconds = 0.0; } let beginIndex: number = 0; for(let i = 0; i < this._partGroupCounts.getSize(); i++) { const partGroupCount: number = this._partGroupCounts.at(i); this.doFade(model, deltaTimeSeconds, beginIndex, partGroupCount); beginIndex += partGroupCount; } this.copyPartOpacities(model); }
// Java public void updateParameters(final CubismModel model, float deltaTimeSeconds) { // 前回のモデルと同じではないときは初期化が必要 if (model != lastModel) { reset(model); } lastModel = model; // 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間0として対応。 if (deltaTimeSeconds < 0.0f) { deltaTimeSeconds = 0.0f; } int beginIndex = 0; for (final int partGroupCount : partGroupCounts) { doFade(model, deltaTimeSeconds, beginIndex, partGroupCount); beginIndex += partGroupCount; } copyPartOpacities(model); }
Parent IDによる不透明度の連動
Pose適用処理の最後に呼ばれる以下の関数により、Linkに指定したパーツへ値を伝搬します。
// C++ CubismPose::CopyPartOpacities()
// TypeScript CubismPose.copyPartOpacities()
// Java CubismPose.copyPartOpacities()
このLinkはOWViewerでParent IDを表記されたPartIDがCubismIdHandleの配列で格納されます。
下の図であればPartManteL001のLinkにはPartArmLB001とPartArmLC001のCubismIdHandleが格納されます。
Linkと表記されていますが親子関係であり、親パーツへの不透明度操作がなければ連動がされないことに注意してください。
Linkに表記された子同士の連動はされません。
// C++ void CubismPose::CopyPartOpacities(CubismModel* model) { for (csmUint32 groupIndex = 0; groupIndex < _partGroups.GetSize(); ++groupIndex) { PartData& partData = _partGroups[groupIndex]; if (partData.Link.GetSize() == 0) { continue; // 連動するパラメータはない } csmInt32 partIndex = _partGroups[groupIndex].PartIndex; csmFloat32 opacity = model->GetPartOpacity(partIndex); for (csmUint32 linkIndex = 0; linkIndex < partData.Link.GetSize(); ++linkIndex) { PartData& linkPart = partData.Link[linkIndex]; csmInt32 linkPartIndex = linkPart.PartIndex; if (linkPartIndex < 0) { continue; } model->SetPartOpacity(linkPartIndex, opacity); } } }
// TypeScript public copyPartOpacities(model: CubismModel): void { for(let groupIndex: number = 0; groupIndex < this._partGroups.getSize(); ++groupIndex) { let partData: PartData = this._partGroups.at(groupIndex); if(partData.link.getSize() == 0) { continue; // 連動するパラメータはない } const partIndex: number = this._partGroups.at(groupIndex).partIndex; const opacity: number = model.getPartOpacityByIndex(partIndex); for(let linkIndex: number = 0; linkIndex < partData.link.getSize(); ++linkIndex) { let linkPart: PartData = partData.link.at(linkIndex); const linkPartIndex: number = linkPart.partIndex; if(linkPartIndex < 0) { continue; } model.setPartOpacityByIndex(linkPartIndex, opacity); } } }
// Java private void copyPartOpacities(CubismModel model) { for (PartData partData : partGroups) { if (partData.linkedParameter == null) { continue; } final int partIndex = partData.partIndex; final float opacity = model.getPartOpacity(partIndex); for (PartData linkedPart : partData.linkedParameter) { final int linkedPartIndex = linkedPart.partIndex; if (linkedPartIndex < 0) { continue; } model.setPartOpacity(linkedPartIndex, opacity); } } }
インスタンスの生成(.pose3.jsonファイルの読み込み)
ポーズは.pose3.jsonファイルに保存され、CubismPoseクラスを使用します。
生成には以下の関数を使用します。
// C++ CubismPose::Create()
// TypeScript CubismPose.create()
// Java CubismPose.create()
実装例
// C++ csmString path = _modelSetting->GetPoseFileName(); path = _modelHomeDir + path; buffer = CreateBuffer(path.GetRawString(), &size); CubismPose* pose = CubismPose::Create(buffer, size); DeleteBuffer(buffer, path.GetRawString());
// TypeScritp let path: string = _modelSetting.getPoseFileName(); path = _modelHomeDire + path; fetch(path).then( (response) => { return response.arrayBuffer(); } ).then( (arrayBuffer) => { let buffer: ArrayBuffer = arrayBuffer; let size: number = buffer.byteLength; let pose: CubismPose = CubismPose.create(buffer, size); deleteBuffer(buffer, path); } );
// Java String path = modelSetting.getPoseFileName(); path = modelHomeDir + path; buffer = CreateBuffer(path); CubismPose pose = CubismPose.create(buffer);
ポーズの適用
ポーズの適用には以下の関数を使用します。
// C++ CubismPose::UpdateParameters()
// TypeScript CubismPose.updateParameters()
// Java CubismPose.updateParameters()
事前にモーションの適用など仮想的なパラメータの計算を済ませておく必要があります。
// C++ void LAppModel::Update() { const csmFloat32 deltaTimeSeconds = LAppPal::GetDeltaTime(); _userTimeSeconds += deltaTimeSeconds; /*省略*/ //----------------------------------------------------------------- _model->LoadParameters(); // 前回セーブされた状態をロード if (_motionManager->IsFinished()) { // モーションの再生がない場合、待機モーションの中からランダムで再生する StartRandomMotion(MotionGroupIdle, PriorityIdle); } else { motionUpdated = _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 } _model->SaveParameters(); // 状態を保存 //----------------------------------------------------------------- /*省略*/ // ポーズの設定 if (_pose != NULL) { _pose->UpdateParameters(_model, deltaTimeSeconds); } _model->Update(); }
// TypeScript /** * 更新 */ public update(): void { if(this._state != LoadStep.CompleteSetup) return; const deltaTimeSeconds: number = LAppPal.getDeltaTime(); this._userTimeSeconds += deltaTimeSeconds; /*省略*/ //-------------------------------------------------------------------------- this._model.loadParameters(); // 前回セーブされた状態をロード if(this._motionManager.isFinished()) { // モーションの再生がない場合、待機モーションの中からランダムで再生する this.startRandomMotion(LAppDefine.MotionGroupIdle, LAppDefine.PriorityIdle); } else { motionUpdated = this._motionManager.updateMotion(this._model, deltaTimeSeconds); // モーションを更新 } this._model.saveParameters(); // 状態を保存 //-------------------------------------------------------------------------- /*省略*/ // ポーズの設定 if(this._pose != null) { this._pose.updateParameters(this._model, deltaTimeSeconds); } this._model.update(); }
// Java public void update() { final float deltaTimeSeconds = LAppPal.getDeltaTime(); userTimeSeconds += deltaTimeSeconds; /*省略*/ // ------------------------------------------------- model.loadParameters(); // 前回セーブされた状態をロード if (motionManager.isFinished()) { // モーションの再生がない場合、待機モーションの中からランダムで再生する startRandomMotion(LAppDefine.MotionGroup.IDLE.getId(), LAppDefine.Priority.IDLE.getPriority()); } else { isMotionUpdated = motionManager.updateMotion(model, deltaTimeSeconds); } model.saveParameters(); // 状態を保存 // ------------------------------------------------- /*省略*/ if (pose != null) { pose.updateParameters(model, deltaTimeSeconds); } model.update(); }
破棄
モデルが解放されるタイミングでCubismPoseインスタンスも破棄する必要があります。
// C++ CubismPose::Delete(pose);
// TypeScript CubismPose.delete(pose);
SDK for Javaの場合はガベージコレクションに解放を任せているので破棄する必要はありません。