ポーズについて

最終更新: 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がCubismIdHundleの配列で格納されます。
下の図であればPartManteL001のLinkにはPartArmLB001とPartArmLC001のCubismIdHundleが格納されます。

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の場合はガベージコレクションに解放を任せているので破棄する必要はありません。

この記事に関するご意見・
ご要望をお聞かせください。