ストップモーションでの利用

ストップモーションとは

ストップモーションとはキャラクターのアニメーションを意図的にコマ送りにすることで人形劇のような演出を行う技法です。
ここではMagicaClothをストップモーションに対応させる方法について解説します。
なお、ストップモーションの実装についてはある程度のコーディング知識が必要となります。

一般的な手法

Unityでストップモーションを実装する一般的な手法は、動作フレームでアニメーションの内容を記録し、停止フレームで記録したアニメーションの内容を書き戻すことです。
これはキャラクター全体のTransformの姿勢を記録し、そして書き戻すことで行われます。

この方式ではキャラクターのアニメーションは常に動作していることに留意してください。
停止フレームでは描画前にアニメーションの結果を記録した姿勢で強制的に上書きします。
これにより内部のアニメーションは更新されているが見た目は静止している状態を作り出せます。
そして、この2つの操作はアニメーション実行後であるLateUpdate()で行われるのが一般的です。

MagicaClothの対応方法

一般的な手法にMagicaClothを適用する場合は少し操作が必要です。
MagicaClothでも同様に動作フレームでは書き込みを許可し、停止フレームでは書き込みを禁止する必要があります。
書き込みを禁止することでシミュレーションの結果がTransformやMeshに書き込まれなくなります。
アニメーションと同じくMagicaClothのシミュレーションも常に動作していることに留意してください。
結果の反映のみを抑制します。

また、一般的な手法での一連の操作はLateUpdate()で行われましたが、MagicaClothを利用する場合は操作場所の変更が必要です。
基本的には次のようにMagicaClothのシミュレーション前後にそれぞれの処理を行う必要があります。

クロスシミュレーション前

ここでは今回のシミュレーション結果を書き込むか禁止するかを設定する必要があります。
動作フレームなら書き込みを許可し、停止フレームならば書き込みを禁止するようにMagicaClothに指示します。

クロスシミュレーション後

ここでは一般的な手法と同じくキャラクターの姿勢の記録と書き戻しを行います。
動作フレームならばキャラクターの姿勢を記録し、停止フレームならば記録した姿勢を書き戻します。
この部分は一般的な手法と同じです。

事例

ここでは事例として、ストップモーションの手法をサンプルコードとともに解説していきます。

サンプルコード

次はMagicaCloth利用時のストップモーションの実装サンプルコードです。
このコンポーネントはキャラクターのAnimatorと同じGameObjectに付与されることが前提となります。

using MagicaCloth2;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// MagicaClothを利用したストップモーションのテスト
/// このスクリプトをキャラクターのAnimatorと同じ場所に追加してください
/// Stop motion testing using MagicaCloth.
/// Add this script to the same location as your character's Animator.
/// </summary>
public class StopMotionTest : MonoBehaviour
{
  /// <summary>
  /// 姿勢を記録するルートTransform
  /// Route Transform that records posture.
  /// </summary>
  public Transform RootBone;

  /// <summary>
  /// 停止するフレーム数
  /// Number of frames to stop.
  /// </summary>
  public int StoppedFrameCount = 5;

  //=============================================================================================
  int recordedFrame = -1;
  bool isSkipWriting = false;
  List<Transform> transforms = null;
  List<STransform> renderedPositions = null;
  List<MagicaCloth> clothList = null;

  /// <summary>
  /// Transformの状態を記録および復元する構造体
  /// Record and restore Transform state.
  /// </summary>
  struct STransform
  {
    public Vector3 LocalPosition;
    public Quaternion LocalRotation;
    public Vector3 LocalScale;

    public static STransform FromTransform(Transform t)
    {
      return new STransform
      {
        LocalPosition = t.localPosition,
        LocalRotation = t.localRotation,
        LocalScale = t.localScale
      };
    }

    public void WriteTo(Transform t)
    {
      t.localPosition = LocalPosition;
      t.localRotation = LocalRotation;
      t.localScale = LocalScale;
    }
  }

  //=============================================================================================
  private void Awake()
  {
    // MagicaClothのシミュレーション前後にイベントを登録する
    // Register events before and after MagicaCloth simulation.
    MagicaManager.OnPreSimulation += OnPreSimulation;
    MagicaManager.OnPostSimulation += OnPostSimulation;

    clothList = new List<MagicaCloth>(GetComponentsInChildren<MagicaCloth>());
    transforms = new List<Transform>(RootBone.GetComponentsInChildren<Transform>());
  }

  private void OnDestroy()
  {
    // MagicaClothへのイベントを解除する
    // Cancel the event registered in MagicaCloth.
    MagicaManager.OnPreSimulation -= OnPreSimulation;
    MagicaManager.OnPostSimulation -= OnPostSimulation;
  }

  private void Update()
  {
    // ストップモーションの状態を更新
    // Update stop motion status.
    if (recordedFrame < 0 || Time.frameCount - recordedFrame >= StoppedFrameCount)
    {
      // 書き込み許可
      // write permission.
      isSkipWriting = false;
      recordedFrame = Time.frameCount;
    }
    else
    {
      // 書き込み停止
      // Write stop.
      isSkipWriting = true;
    }
  }

  void Reset()
  {
    var smr = GetComponentInChildren<SkinnedMeshRenderer>();

    if (smr != null)
    {
      RootBone = smr.rootBone;
    }
    else
    {
      RootBone = null;
    }
  }

  //=============================================================================================
  /// <summary>
  /// MagicaClothのシミュレーション実行前に呼び出されるイベント
  /// Event called before MagicaCloth simulation execution.
  /// </summary>
  void OnPreSimulation()
  {
    if (isSkipWriting)
    {
      // MagicaClothの書き込みを停止
      // Stop writing MagicaCloth.
      SetMagicaClothSkipWriting(true);
    }
    else
    {
      // MagicaClothの書き込みを許可
      // Allow MagicaCloth to write.
      SetMagicaClothSkipWriting(false);
    }
  }

  /// <summary>
  /// MagicaClothの書き込み停止フラグを更新する
  /// 書き込みを停止するとTransformおよびMeshへの書き込みが行われない
  /// Update MagicaCloth write stop flag.
  /// When writing is stopped, writing to Transform and Mesh is not done.
  /// </summary>
  /// <param name="sw"></param>
  void SetMagicaClothSkipWriting(bool sw)
  {
    if (clothList == null)
      return;

    foreach (var cloth in clothList)
    {
      cloth.SetSkipWriting(sw);
    }
  }

  //=============================================================================================
  /// <summary>
  /// MagicaClothのシミュレーション実行後に呼び出されるイベント
  /// Event called after MagicaCloth simulation run.
  /// </summary>
  void OnPostSimulation()
  {
    if (isSkipWriting)
    {
      // 停止時は記録したポーズを復元する
      // Restore the recorded pose when stopped.
      RestoreRecord();
    }
    else
    {
      // 更新時はアニメーションのポーズを記録する
      // Record animation pose when updating.
      RecordTransform();
    }
  }

  /// <summary>
  /// キャラクターの姿勢を記録する
  /// Record the character's posture.
  /// </summary>
  void RecordTransform()
  {
    if (transforms == null)
      return;

    if (renderedPositions == null)
    {
      renderedPositions = new List<STransform>(new STransform[transforms.Count]);
    }

    for (int i = 0; i < transforms.Count; i++)
    {
      renderedPositions[i] = STransform.FromTransform(transforms[i]);
    }
  }

  /// <summary>
  /// キャラクターの姿勢を復元する
  /// Restore the character's posture.
  /// </summary>
  void RestoreRecord()
  {
    if (renderedPositions == null)
      return;

    for (int i = 0; i < transforms.Count; i++)
    {
      renderedPositions[i].WriteTo(transforms[i]);
    }
  }
}

イベントの登録

private void Awake()
{
  // MagicaClothのシミュレーション前後にイベントを登録する
  // Register events before and after MagicaCloth simulation.
  MagicaManager.OnPreSimulation += OnPreSimulation;
  MagicaManager.OnPostSimulation += OnPostSimulation;

  clothList = new List<MagicaCloth>(GetComponentsInChildren<MagicaCloth>());
  transforms = new List<Transform>(RootBone.GetComponentsInChildren<Transform>());
}

private void OnDestroy()
{
  // MagicaClothへのイベントを解除する
  // Cancel the event registered in MagicaCloth.
  MagicaManager.OnPreSimulation -= OnPreSimulation;
  MagicaManager.OnPostSimulation -= OnPostSimulation;
}

まずクロスシミュレーションの前後に処理を追加できるようにイベントを登録します。
これにはMagicaManagerのOnPreSimulation/OnPostSimulationを利用します。

ストップモーションの状態更新

private void Update()
{
  // ストップモーションの状態を更新
  // Update stop motion status.
  if (recordedFrame < 0 || Time.frameCount - recordedFrame >= StoppedFrameCount)
  {
    // 書き込み許可
    // write permission.
    isSkipWriting = false;
    recordedFrame = Time.frameCount;
  }
  else
  {
    // 書き込み停止
    // Write stop.
    isSkipWriting = true;
  }
}

今回のフレームが動作フレームか停止フレームかを判定します。
このサンプルでは単純にStoppedFrameCountフレームだけ停止します。
停止時にはisSkipWritingフラグが設定されるようになっています。

シミュレーション実行前イベント

/// <summary>
/// MagicaClothのシミュレーション実行前に呼び出されるイベント
/// Event called before MagicaCloth simulation execution.
/// </summary>
void OnPreSimulation()
{
  if (isSkipWriting)
  {
    // MagicaClothの書き込みを停止
    // Stop writing MagicaCloth.
    SetMagicaClothSkipWriting(true);
  }
  else
  {
    // MagicaClothの書き込みを許可
    // Allow MagicaCloth to write.
    SetMagicaClothSkipWriting(false);
  }
}

/// <summary>
/// MagicaClothの書き込み停止フラグを更新する
/// 書き込みを停止するとTransformおよびMeshへの書き込みが行われない
/// Update MagicaCloth write stop flag.
/// When writing is stopped, writing to Transform and Mesh is not done.
/// </summary>
/// <param name="sw"></param>
void SetMagicaClothSkipWriting(bool sw)
{
  if (clothList == null)
    return;

  foreach (var cloth in clothList)
  {
    cloth.SetSkipWriting(sw);
  }
}

クロスシミュレーション開始前の処理を記述します。
キャラクターのMagicaClothに対してSetSkipWrite()APIを使って書き込みの有無を指示します。
書き込みが停止されるとシミュレーション後にTransformとMeshへの書き込みが行われなくなります。

シミュレーション実行後イベント

/// <summary>
/// MagicaClothのシミュレーション実行後に呼び出されるイベント
/// Event called after MagicaCloth simulation run.
/// </summary>
void OnPostSimulation()
{
  if (isSkipWriting)
  {
    // 停止時は記録したポーズを復元する
    // Restore the recorded pose when stopped.
    RestoreRecord();
  }
  else
  {
    // 更新時はアニメーションのポーズを記録する
    // Record animation pose when updating.
    RecordTransform();
  }
}

/// <summary>
/// キャラクターの姿勢を記録する
/// Record the character's posture.
/// </summary>
void RecordTransform()
{
  if (transforms == null)
    return;

  if (renderedPositions == null)
  {
    renderedPositions = new List<STransform>(new STransform[transforms.Count]);
  }

  for (int i = 0; i < transforms.Count; i++)
  {
    renderedPositions[i] = STransform.FromTransform(transforms[i]);
  }
}

/// <summary>
/// キャラクターの姿勢を復元する
/// Restore the character's posture.
/// </summary>
void RestoreRecord()
{
  if (renderedPositions == null)
    return;

  for (int i = 0; i < transforms.Count; i++)
  {
    renderedPositions[i].WriteTo(transforms[i]);
  }
}

クロスシミュレーション完了後の処理を記述します。
一般的な手法に則り、動作フレームならばアニメーションの姿勢を記録し、停止フレームであれば記録した姿勢を復元させます。

結果

このサンプルの実行結果は次のようになります。
左側が通常の状態であり、右側がストップモーションを適用した状態です。
停止フレームではアニメーションとMagicaClothの両方が視覚的に停止している点に注目してください。