実行時の構築

概要


MagicaCloth2は実行中の構築を完全にサポートします。
ここでは実行中にスクリプトからMagicaClothコンポーネントを構築する方法を解説します。
なお読者はUnityでのC#プログラミングを理解していることを前提としています。

サンプルシーン


実行時の構築についてはサンプルシーンが用意されています。
これらは次のフォルダにありテストする場合はお使いのレンダーパイプラインのシーンを選択してください。

このサンプルシーンにあるRuntimeBuildDemoにアタッチされているRuntimeBuildDemo.csがテストコードとなっています。
このページでもこのテストコードの内容を抜粋して説明してきます。

なお、シーンが分かれている理由は単に描画マテリアルの切り替えのためです。
内部に含まれるサンプルコードは同一です。

構築手順


構築は次の手順で行います。

  1. MagicaClothコンポーネントの生成
  2. パラメーターを設定する
  3. クロスデータの作成と実行を開始する

これらについては事例を元に解説していきます。
また、構築の際に留意すべき点が幾つかあります。

頂点属性の指定

MagicaClothでは動く頂点と動かない頂点を明確に指定する必要があります。
これを頂点属性と呼びます。
エディタ編集時はこの属性を頂点ペイント機能を使って手動で設定しました。
しかし、この方法は実行時構築には利用できません。
そのため、実行時構築ではクロスタイプに応じていくつかの方法で頂点属性を指定する必要があります。
これらの方法については事例で解説します。

実行開始の遅延

MagicaCloth2ではクロスデータは実行時に構築されます。
この構築は別スレッドで行われるためメインスレッドにはほとんど影響はありません。
しかし、構築には数フレームの時間が必要です。

そのためコンポーネントを構築してから実際にクロスシミュレーションが開始されるまで数フレームの遅延があります。

事例


ここに幾つかの構築事例を記載します。
事例はサンプルシーンのRuntimeBuildDemo.csから抜粋していますので、デモシーンも合わせて参照してください。
使われるAPIについてはScripttingAPIのページを参照してください。

コードに書かれるcharacterはキャラクターのGameObjectです。
またgameObjectContainerはキャラクターのGameObjectを名前から取得できる簡単なクラスです。

BoneCloth構築例(1)

/// <summary>
/// BoneCloth construction example (1).
/// Set all parameters from a script.
/// </summary>
void SetupHairTail_BoneCloth()
{
  if (character == null)
    return;

  var obj = new GameObject("HairTail_BoneCloth");
  obj.transform.SetParent(character.transform, false);

  // add Magica Cloth
  var cloth = obj.AddComponent<MagicaCloth>();
  var sdata = cloth.SerializeData;

  // bone cloth
  sdata.clothType = ClothProcess.ClothType.BoneCloth;
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairTail_00_B").transform);
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairTail_00_B").transform);

  // setup parameters
  sdata.gravity = 3.0f;
  sdata.damping.SetValue(0.05f);
  sdata.angleRestorationConstraint.stiffness.SetValue(0.15f, 1.0f, 0.15f, true);
  sdata.angleRestorationConstraint.velocityAttenuation = 0.6f;
  sdata.tetherConstraint.distanceCompression = 0.5f;
  sdata.inertiaConstraint.particleSpeedLimit.SetValue(true, 3.0f);
  sdata.colliderCollisionConstraint.mode = ColliderCollisionConstraint.Mode.None;

  // start build
  cloth.BuildAndRun();
}

この例ではBoneClothを作成して、すべてのパラメータをスクリプトから設定しています。
重要なのはSerializeDataクラスです。
スクリプトから操作可能なパラメータはすべてこのSerializeDataクラスが内包しています。
そのためこのクラスを書き換えることで設定を行います。

この事例では頂点属性を指定していませんが、BoneCloth/BoneSpringではルートボーンに指定したTransformが固定属性、それ以外は移動属性になるように自動的に設定されます。
そのため省略可能です。

最後にBuildAndRun()を実行します。
これによりスレッドでクロスデータの作成が開始され、完了すると自動的にシミュレーションが開始されます。

BoneCloth構築例(2)

/// <summary>
/// BoneCloth construction example (2).
/// Copy parameters from an existing component.
/// </summary>
void SetupFrontHair_BoneCloth()
{
  if (character == null || frontHairSource == null)
    return;

  var obj = new GameObject("HairFront_BoneCloth");
  obj.transform.SetParent(character.transform, false);

  // add Magica Cloth
  var cloth = obj.AddComponent<MagicaCloth>();
  var sdata = cloth.SerializeData;

  // bone cloth
  sdata.clothType = ClothProcess.ClothType.BoneCloth;
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairFront_00_B").transform);
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairSide2_00_B").transform);
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairSide_00_B").transform);
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairFront_00_B").transform);
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairSide2_00_B").transform);
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairSide_00_B").transform);

  // Normal direction setting for backstop
  sdata.normalAlignmentSetting.alignmentMode = NormalAlignmentSettings.AlignmentMode.Transform;
  sdata.normalAlignmentSetting.adjustmentTransform = gameObjectContainer.GetGameObject("HeadCenter").transform;

  // setup parameters
  // Copy from source settings
  sdata.Import(frontHairSource, false);

  // start build
  cloth.BuildAndRun();
}

この例ではパラメータを外部の別のクロスコンポーネントであるfrontHairSourceからインポートしています。
これによりパラメータの設定が簡略化できます。

ただしパラメータにはインポートできるものとできないものがあります。
これはソースコードにコメントとして記載されています。

/// [OK] Runtime changes.
/// [NG] Export/Import with Presets

この例ではnormalAlignmentSettingはインポートすることができないため手動で設定しています。

BoneCloth構築例(3)

/// <summary>
/// BoneCloth construction example (3).
/// Load parameters from saved presets.
/// </summary>
void SetupRibbon_BoneCloth()
{
  if (character == null || string.IsNullOrEmpty(ribbonPresetName))
    return;

  var obj = new GameObject("Ribbon_BoneCloth");
  obj.transform.SetParent(character.transform, false);

  // add Magica Cloth
  var cloth = obj.AddComponent<MagicaCloth>();
  var sdata = cloth.SerializeData;

  // bone cloth
  sdata.clothType = ClothProcess.ClothType.BoneCloth;
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HeadRibbon_00_B").transform);
  sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HeadRibbon_00_B").transform);

  // setup parameters
  // Load presets from the Resource folder.
  // Since presets are in TextAssets format, they can also be used as asset bundles.
  var presetText = Resources.Load<TextAsset>(ribbonPresetName);
  sdata.ImportJson(presetText.text);

  // start build
  cloth.BuildAndRun();
}

この例ではパラメータをプリセットファイル(Json)としてインポートしています。
パラメータはJson形式で外部にエクスポートできます。
これは単純なテキストファイルなのでResourcesフォルダやアセットバンドルとして配置することで実行時に読み込めます。

BoneClothでの頂点属性設定

BoneClothの構築時にはルートボーンを固定属性とし、それ以外は移動属性として自動設定されます。
そのため基本的には省略可能です。
しかし、ClothSerializeData2のboneAttributeDictを利用することで、これを手動で設定することもできます。
boneAttributeDictはTransformと属性のペアからなる辞書で次のように指定します。

// Transform Attribute
var sdata2 = cloth.GetSerializeData2();
sdata2.boneAttributeDict.Add(bone1, VertexAttribute.Fixed);
sdata2.boneAttributeDict.Add(bone2, VertexAttribute.Invalid);
sdata2.boneAttributeDict.Add(bone3, VertexAttribute.Move);

MeshCloth構築例(1)

/// <summary>
/// MeshCloth construction example (1).
/// Reads vertex attributes from a paintmap.
/// </summary>
void SetupSkirt_MeshCloth()
{
  if (character == null || skirtPaintMap == null)
    return;

  // skirt renderer
  var sobj = gameObjectContainer.GetGameObject(skirtName);
  if (sobj == null)
    return;
  Renderer skirtRenderer = sobj.GetComponent<Renderer>();
  if (skirtRenderer == null)
    return;

  // add Magica Cloth
  var obj = new GameObject("Skirt_MeshCloth");
  obj.transform.SetParent(character.transform, false);
  var cloth = obj.AddComponent<MagicaCloth>();
  var sdata = cloth.SerializeData;

  // mesh cloth
  sdata.clothType = ClothProcess.ClothType.MeshCloth;
  sdata.sourceRenderers.Add(skirtRenderer);

  // reduction settings
  sdata.reductionSetting.simpleDistance = 0.0212f;
  sdata.reductionSetting.shapeDistance = 0.0244f;

  // paint map settings
  // *** Paintmaps must have Read/Write attributes enabled! ***
  sdata.paintMode = ClothSerializeData.PaintMode.Texture_Fixed_Move;
  sdata.paintMaps.Add(skirtPaintMap);

  // setup parameters
  sdata.gravity = 1.0f;
  sdata.damping.SetValue(0.03f);
  sdata.angleRestorationConstraint.stiffness.SetValue(0.05f, 1.0f, 0.5f, true);
  sdata.angleRestorationConstraint.velocityAttenuation = 0.5f;
  sdata.angleLimitConstraint.useAngleLimit = true;
  sdata.angleLimitConstraint.limitAngle.SetValue(45.0f, 0.0f, 1.0f, true);
  sdata.distanceConstraint.stiffness.SetValue(0.5f, 1.0f, 0.5f, true);
  sdata.tetherConstraint.distanceCompression = 0.9f;
  sdata.inertiaConstraint.depthInertia = 0.7f;
  sdata.inertiaConstraint.movementSpeedLimit.SetValue(true, 3.0f);
  sdata.inertiaConstraint.particleSpeedLimit.SetValue(true, 3.0f);
  sdata.colliderCollisionConstraint.mode = ColliderCollisionConstraint.Mode.Point;

  // setup collider
  var lobj = new GameObject("CapsuleCollider_L");
  lobj.transform.SetParent(gameObjectContainer.GetGameObject("Character1_LeftUpLeg").transform);
  lobj.transform.localPosition = new Vector3(0.0049f, 0.0f, -0.0832f);
  lobj.transform.localEulerAngles = new Vector3(0.23f, 16.376f, -0.028f);
  var colliderL = lobj.AddComponent<MagicaCapsuleCollider>();
  colliderL.direction = MagicaCapsuleCollider.Direction.Z;
  colliderL.SetSize(0.082f, 0.094f, 0.3f);

  var robj = new GameObject("CapsuleCollider_R");
  robj.transform.SetParent(gameObjectContainer.GetGameObject("Character1_RightUpLeg").transform);
  robj.transform.localPosition = new Vector3(-0.0049f, 0.0f, -0.0832f);
  robj.transform.localEulerAngles = new Vector3(0.23f, -16.376f, -0.028f);
  var colliderR = robj.AddComponent<MagicaCapsuleCollider>();
  colliderR.direction = MagicaCapsuleCollider.Direction.Z;
  colliderR.SetSize(0.082f, 0.094f, 0.3f);

  sdata.colliderCollisionConstraint.colliderList.Add(colliderL);
  sdata.colliderCollisionConstraint.colliderList.Add(colliderR);

  // start build
  cloth.BuildAndRun();
}

この例ではMeshClothを構築しペイントマップにより頂点属性を付与しています。
ペイントマップは設定したレンダラーと同期していることに注意してください。
つまりレンダラーの数とペイントマップの数は同じである必要があります。

またカプセルコライダーを2つ作成してコライダーリストに追加しています。
コライダーの作成もUnityの一般的なコンポーネントを作成する手法と同じです。
それ以外はBoneClothの事例と同じです。

MeshClothでの頂点属性の直接指定

MeshClothではBoneClothと違い頂点属性を明確に指定する必要があります。
省略はできません。
これにはMeshCloth構築例(1)で解説したペイントマップを使う方法と、頂点配列と同じ数の属性配列を用意して指定する2つの方法があります。

ペイントマップの代わりに頂点属性配列を利用する場合は、ClothSerializeData2のvertexAttributeListを利用します。
まず、vertexAttributeListの要素数はMeshClothに登録したRendererの数と一致する必要があります。
そして、各要素は対応するRendereのメッシュ頂点数と一致する必要があります。

ペイントマップの代わりにvertexAttributeListを使った場合の事例は次のようになります。
この例ではレンダラーが1つの場合を想定しています。

// add vertex attribute
var sdata2 = cloth.GetSerializeData2();
var attributes = new VertexAttribute[VertexCount];

// Initialize with movement attributes
for(int i = 0; i < VertexCount; i++)
  attributes[i] = VertexAttribute.Move;

// Making a specific vertex a fixed attribute
attributes[0] = VertexAttribute.Fixed;
attributes[7] = VertexAttribute.Fixed;
attributes[21] = VertexAttribute.Fixed;

// Registering vertex attributes
sdata2.vertexAttributeList.Add(attributes);