QuatSkinningSample 解析メモ 2010/7/16 Na-7 筆者がソース解析した際のメモです。 他者への説明向け資料ではありませんのであしからず。 ○AnimationClip.cs(0.2h) ・AnimationClipクラスの実装  →全ボーンの全キーフレームをもつリストを持つ ○AnimationPlayer.cs(3h) ・/// AnimationClipからボーン行列を生成するアニメーションプレイヤー ・AnimationClip currentClipValue;  →現在再生中のモーションデータ ・TimeSpan currentTimeValue;  →再生位置 ・QuatTransform[] boneTransforms;  →ボーン変換行列(親のボーンへの変換行列) ・QuatTransform[] worldTransforms;  →ボーン変換行列(ワールド座標への変換行列)  →特定のキーフレームにおいて、親ボーンを基準に各子ボーンをトランスフォームした座標変換行列  →注意:オブジェクトのワールド座標変換は含まない(それはシェーダーで行う) ・Quaternion[] skinRotations;  →ボーン変換行列(バインドポーズへの変換行列)の回転部分 ・Vector3[] skinTranslations;  →ボーン変換行列(バインドポーズへの変換行列)の平行移動部分 ・// バインドポーズとスケルトン情報を取得するためのバックリンク  SkinningData skinningDataValue;  →コンストラクタの引数でSkinningData(通常はModelクラスのTagプロパティに設定されている)を受け取り保持する ・public void Update(TimeSpan time, bool relativeToCurrentTime)  →relativeToCurrentTimeがtrueの場合、前回のフレーム再生時間にtimeを加算したフレームを再生する(アニメ再生)  →relativeToCurrentTimeがfalseの場合、timeのフレームを再生する(ポーズ再生) ・/// BoneTransformsを更新する  /// Updateメソッドから呼ばれるヘルパーメソッド  public void UpdateBoneTransforms(TimeSpan time, bool relativeToCurrentTime)  →時間(アニメーション再生位置)やカレントキーフレームを更新している  →ボーン座標は更新していない ・if ((time < TimeSpan.Zero) || (time >= currentClipValue.Duration))   throw new ArgumentOutOfRangeException("time");  →(time >= currentClipValue.Duration)となるケースはありえるのではないか?? ・// 再生位置が過去方向に戻ったなら、キーフレームのインデックスをリセットする  →スキル不足のため必然性が理解できない ・// このキーフレームを使う  boneTransforms[keyframe.Bone] = keyframe.Transform;  →keyframe.Boneは、そのキーフレームのボーンインデックス  →キーフレームには、全ボーンの位置情報が格納されているものなのか??   →アニメデータ読込時に、キーフレーム毎に全ボーンの位置情報を展開している可能性がある ・int parentBone = skinningDataValue.SkeletonHierarchy[bone];  →skinningDataValue.SkeletonHierarchyは各ボーンの親のボーンのインデックス情報 ・worldTransforms[bone] = boneTransforms[bone] *   worldTransforms[parentBone];  →親ボーンの座標を基準に子ボーンの座標を変換している ・/// SkinTransformsの更新  /// Updateメソッドから呼ばれるヘルパーメソッド  public void UpdateSkinTransforms()  →各ボーンのバインドポーズ逆行列を基準に各ボーンの座標を変換している ○Keyframe.cs(0.1h) ・/// キーフレーム、時間とボーンの変換行列のペア ○QuatTransform.cs(1h) ・FromMatrix()では、スケール値が1以外のものはエラーとしている ・return ( Math.Abs( a - b ) < 1e-4f );  →"1E-06"は"0.000001"を表すので、"1e-4f"は"0.0001f"を表す  →0.0001f以下は誤差として扱っている ・public static QuatTransform operator *(QuatTransform value1, QuatTransform value2)  →"operator *"で演算オーバーロードを使用している ・回転した後に平行移動させている  →translationとrotationはそれぞれ回転させる必要があるらしい ○SkinningData.cs(0.25h) ・/// スキンアニメーションに必要な情報をもつSkinningDataクラス /// 通常、ModelクラスのTagプロパティに設定されている  →ModelクラスのTagプロパティに下記4点を保持する   ・複数のモーションデータ   ・各ボーンのバインドポーズ行列   ・各ボーンのバインドポーズ逆行列   ・各ボーンの親のボーンのインデックス情報 ・using System.Collections.Generic;  →呼び出し側で角かっこ [] 付きインデックスを使用可能とするために必要?? ・IDictionary animationClipsValue;  →アニメーションクリップ(1つのモーションデータ)を辞書形式で複数保持している ○TypeReaders.cs(1.5h) ・コンテントパイプラインの記述に関しては下記参照  http://blogs.msdn.com/b/ito/archive/2007/05/02/content-pipeline-part3.aspx  また、後述のTypeWriters.csと併読すると良い ・Content.Load()時に、ModelクラスのTagプロパティに下記4点を保持する   ・複数のモーションデータ   ・各ボーンのバインドポーズ行列   ・各ボーンのバインドポーズ逆行列   ・各ボーンの親のボーンのインデックス情報 ・SkinningDataReaderクラスは、TypeWriters.cs内の‘SkinningDataのコンテントタイプライタ’内の  GetRuntimeReader()内でランタイムリーダーとして指定されている ・animationClips = input.ReadObject>();  →input.ReadObjectは「現在のストリームからマネージ オブジェクトを 1 つ読み込みます。再帰呼び出し可能です。」  →AnimationClipを読み込む際に、下記のオーバーライドされたコンテントタイプリーダーが呼び出される ・protected override AnimationClip Read(ContentReader input,  →コンテントタイプリーダーのAnimationClip型のReadをオーバーライドすることにより、   AnimationClipデータをXNBファイルから読み込む際にこのコードが呼び出される ・KeyFrameとQuatTransformの読み込みもAnimationClipと同様 ○TypeWriters.cs(1.5h) ・コンテントパイプラインの記述に関しては下記参照  http://blogs.msdn.com/b/ito/archive/2007/05/02/content-pipeline-part3.aspx ・[ContentTypeWriter]  →[]はC#言語のアトリビュート(属性)である   http://www.atmarkit.co.jp/fdotnet/csharp_abc/csharp_abc_020/csharp_abc01.html  →XNAでは、[ContentTypeWriter]と属性記述するとコンテントタイプライタとして認識される ・public class SkinningDataWriter : ContentTypeWriter  →カスタムタイプライターを作るには、ContentTypeWriterジェネリッククラスから派生させて   [ContentTypeWriter]属性を記述し、WriteとGetRuntimeReaderメソッドをオーバーライドする必要がある ・protected override void Write(ContentWriter output, SkinningData value)  →Writeメソッドをオーバライドしている ・output.WriteObject(value.AnimationClips);  →カスタムタイプライターが必要なオブジェクトはWriteObjectメソッドを使う必要がある  →WriteObjectを使う場合、XNBファイルには書き込む型情報も保存される  →floatやint型のような単純なデータを書き込むにはWriteを使った方がXNBファイルサイズを小さくする事ができる ・public override string GetRuntimeReader(TargetPlatform targetPlatform)  →ランタイム時にデータを読み込む為に使うタイプリーダー(TypeReader)の型を文字列で返す  →ここでの型情報は、通常、"フルネームスペース,アセンブリ名"といった形式の文字列を返す   例:return "Sample.Runtime.TextMessageReader,Sample.Runtime"; ・return typeof(SkinningDataReader).AssemblyQualifiedName;  →型名 (名前空間を含む)、その後にコンマ、続いてアセンブリの表示名が返される ○SkinnedModelProcessor.cs(5.5h) ・コンテントパイプラインの記述に関しては下記参照  http://blogs.msdn.com/b/ito/archive/2007/05/02/content-pipeline-part3.aspx ・using System.IO;  →Path.GetFullPath()を使用するために追加している模様 ・[ContentProcessor]  public class SkinnedModelProcessor : ModelProcessor  →カスタムプロセッサの役割は、コンテント・パイプライン内の中間データであるNodeContent型から   他の型へ変換すること  →カスタムプロセッサはContentProcessorジェネリッククラスから派生させ、   [ContentProcessor]属性を記述し、Processメソッドをオーバーライドする必要がある ・ValidateMesh(input, context, null);  →このメッシュはスキンアニメーションに向いたものかチェックする ・BoneContent skeleton = MeshHelper.FindSkeleton(input);  →含まれているスケルトンのルートボーンを検索する ・IList bones = MeshHelper.FlattenSkeleton(skeleton);  →指定されたスケルトンに含まれるすべてのボーンのフラット化リストを取得する ・inverseBindPose.Add(QuatTransform.FromMatrix(Matrix.Invert(bone.AbsoluteTransform)));  →親のAbsoluteTransformが乗算されたローカル Transform プロパティの値を取得し、   逆行列を計算した値からQuatTransformを生成してリストに追加する ・skeletonHierarchy.Add(bones.IndexOf(bone.Parent as BoneContent));  →親ボーン(をBoneContent型に変換した値)のインデックスを追加する ・animationClips = ProcessAnimations(skeleton.Animations, bones);  →コンテント・パイプライン内の中間フォーマットであるAnimationContentDictionaryから、   ランタイムフォーマットであるAnimationClipフォーマットに変換する ・foreach (KeyValuePair animation in animations)  →KeyValuePair:設定または修得できる、キー/値ペアを定義する  →C#のforeachでIDictionary(TKey, TValue)の型を指定する一般的な手法の模様 ・foreach (KeyValuePair channel in animation.Channels)  →アニメーション チャンネルは、いずれか 1 つのボーンまたは剛体オブジェクトの動きを定義する、   キーフレームのコレクションのこと ・if (!boneMap.TryGetValue(channel.Key, out boneIndex))  →Dictionary(TKey, TValue).TryGetValue:指定したキーに関連付けられている値を取得する ・keyframes.Add(new Keyframe(boneIndex, keyframe.Time,   QuatTransform.FromMatrix(keyframe.Transform)));  →keyframeは引数で渡されたアニメデータの一部、keyframesは新規作成中のデータである  →アニメーションチャンネル内の各キーフレームについて、   QuatTransform型に変換したキーフレームを作成追加している ・keyframes.Sort(CompareKeyframeTimes);  →CompareKeyframeTimesは多分デリゲート ・return a.Time.CompareTo(b.Time);  →TimeSpan.CompareTo:指定したオブジェクトまたは TimeSpan オブジェクトと比べて、   このインスタンスが短い、等しい、または長いことを示す整数を返す ・context.Logger.LogWarning(null, null,  →ContentBuildLogger.LogWarning:コンテンツのインポーターまたはプロセッサからの警告メッセージを出力する ・if (!MeshHasSkinning(mesh))  →メッシュがスキニング情報をもっているか調べる ・mesh.Parent.Children.Remove(mesh);  →調査中にノードが消去される可能性がある ・// このノードがボーンなら、ボーン内を調査中であることを覚えておく  parentBoneName = node.Name;  →一度でもボーン調査すると、再帰調査中ずっとボーン調査中と記憶される ・foreach (GeometryContent geometry in mesh.Geometry)  →ジオメトリとは、単一マテリアルのインデックス付き頂点トライアングルリストを含む、   1回の描画呼び出しでGPUに送信可能な描画データ集合体 ・if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights()))  →VertexChannelCollection.Contains:指定された頂点チャンネルがコレクションに含まれているかどうかを確認する  →VertexChannelNames.Weights:アニメーションの重みのチャンネルの名前を取得する ・/// 全てが同じ座標空間になるように、不必要な変換行列を  /// モデル・ジオメトリに焼き付ける  →モデル内のメッシュがそれぞれ違うローカル座標を持っていると扱いが面倒なので、   全部焼いちゃう(メッシュの変換座標をあらかじめ頂点データに適用すること) ・continue;  →今回のループをここで終了させ、次回のループに制御を移動する ・MeshHelper.TransformScene(child, child.Transform);  →MeshHelper.TransformScene:シーン階層のコンテンツにトランスフォームを適用する  →モデル・ジオメトリ(GPU内の頂点バッファ等に格納されている?)の頂点データに対し、   トランスフォームを適用する模様 ・/// 全てのマテリアルがスキンモデル用のエフェクトを使うように変更する  protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context)  →ModelProcessor.ConvertMaterial:GeometryContent オブジェクトの MaterialContent プロパティが   入力ノード コレクション内で検出された場合に、フレームワークによって呼び出される  →オーバーライドすることにより、マテリアル処理時に自動的に呼び出される  →この処理により、エフェクトファイル複数使用不可になっているのではないか? ・throw new InvalidContentException(string.Format(  →InvalidContentException:処理中にコンテンツでエラーが発生した場合にスローする ・effectMaterial.Effect = new ExternalReference(effectPath);  →ExternalReference:コンテンツ アイテム用データ ファイルへの外部参照を指定する ○QuatSkinningSample.cs(1.0h) ・// このサンプルを実行するにはSM2.0が必要  graphics.MinimumVertexShaderProfile = ShaderProfile.VS_2_0;  graphics.MinimumPixelShaderProfile = ShaderProfile.PS_2_0;  →グラフィックカードをチェックし、仕様に満たない場合はポップアップでエラーメッセージを表示する ・currentModel = Content.Load( "dude" );  →モデルファイルのコンテンツプロセッサにSkinnedModelProcessorを指定しているので   Tagにスキン情報がセットされる ・animationPlayer.Update(gameTime.ElapsedGameTime, true);  →引数にtrueを指定しているのでアニメが更新される ・Matrix view = Matrix.CreateTranslation(0, -40, 0) *   Matrix.CreateRotationY(MathHelper.ToRadians(cameraRotation)) *   Matrix.CreateRotationX(MathHelper.ToRadians(cameraArc)) *   Matrix.CreateLookAt(new Vector3(0, 0, -cameraDistance),   new Vector3(0, 0, 0), Vector3.Up);  →注視点(0,0,0)を中心に回転するカメラワークに(0,-40,0)(少し上にずらす)の補正を追加した ・Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,   aspectRatio,   1,   10000);  →Matrix.CreatePerspectiveFieldOfView:視野に基づいて、パースペクティブ射影行列を作成する  →MathHelper.PiOver4:パイを4で割った値(パイ/4)を表す ・// ボーンのクォータニオンと平行移動部分を取得し、エフェクトに設定する  →animationPlayerの中でエフェクトパラメータ設定までやった方がコードがすっきりするのではないか??   →エフェクトパラメータセットを複数個所で行うとメンテナンスしにくいからここでまとめてセットしている?? ・cameraArc += currentGamePadState.ThumbSticks.Right.Y * time * 0.25f;  →GamePadThumbSticks.Right:Xbox 360 コントローラーの右スティックの位置を 2D ベクトルとして返す  →右スティックの位置を特定する 2D ベクトル。それぞれの軸は、?1.0 から 1.0 の浮動小数点値で表される ・cameraArc = MathHelper.Clamp( cameraArc, -90.0f, 90.0f );  →MathHelper.Clamp:値を指定された範囲内に制限する ○SkinnedModel.fx(2.0h) ・クォータニオン処理に関しては下記参照  →http://blogs.msdn.com/b/ito/archive/2009/05/05/more-bones-05.aspx ・// クォータニオンによる回転  →式の内容は理解不能  →ともかくこれでクォータニオンによる回転が出来る模様 ・// クォータニオンと平行移動による頂点変換  →式の内容は理解不能  →ともかくこれでクォータニオンと平行移動による頂点変換が出来る模様 ・// クォータニオンと平行移動から行列に変換する  // スキニングに使用する場合、単純にこのメソッドを4回呼ぶのが理想的だが  // SM2.0だと一時レジスタ(12個)を超えてしまうので、そのままでは使えない  →このソース内では未使用 ・// 4つクォータニオンと平行移動から行列に変換する  // SM2.0の一時レジスタ(12個)数制限を回避するために、一時レジスタの使用量を抑えるように  // 書き換えたもの  →式の内容は理解不能  →ともかくこれで4つクォータニオンと平行移動から行列への変換が出来る模様 ・skinTransform = mul( skinTransform, World );  →ワールド座標変換  →行列計算の順番は、   1.スキン変換行列の取得   2.ワールド座標変換   3.(input.Positionの)頂点変換   4.ビュー座標変換   5.斜影変換 ・// 法線変換  →法線とライトからカラーを算出する参考になる ・float3 light1 = max(dot(normal, Light1Direction), 0) * Light1Color;  →dot:2 つのベクトルの内積を返す  →max:x と y のうちの大きい方の値を選択する