2011年11月5日土曜日

アニメーション付きColladaフォーマット出力完成

前回の続き。ようやく完成しました。

失敗してた原因ですが、前回の記事で「キーフレーム変換行列の回転部分におかしなところがある」というように書きましたが案の定。クォータニオン→オイラー角を取り出す処理に問題がありました。

以下のコードはCollada内のフレーム行列に関する記述の一部です
<node id="センター" type="JOINT">
  <translate sid="location">0.000000 6.681501 0.000000</translate>
  <rotate sid="rotateZ">0.000000 0.000000 1.000000 0.000000</rotate>
  <rotate sid="rotateY">0.000000 1.000000 0.000000 0.000000</rotate>
  <rotate sid="rotateX">1.000000 0.000000 0.000000 0.000000</rotate>
  <scale sid="scale">1.000000 1.000000 1.000000</scale>
  <node id="センター先" type="JOINT">
    <translate sid="location">0.000000 -6.474001 0.000000</translate>
    <rotate sid="rotateZ">0.000000 0.000000 1.000000 0.000000</rotate>
    <rotate sid="rotateY">0.000000 1.000000 0.000000 0.000000</rotate>
    <rotate sid="rotateX">1.000000 0.000000 0.000000 0.000000</rotate>
    <scale sid="scale">1.000000 1.000000 1.000000</scale>
  </node>
</node>

「translate」がローカル座標の値で左から順にX・Y・Z、「rotate」がローカル軸回転の値で左から順にX軸・Y軸・Z軸・角度(デグリー角)となっています。

次のコードはアニメーションの記述の一部です。
<!-- X軸回転 -->
<animation id="センター_RotationX">
  <source id="センター_input">
    <float_array id="センター_input-array" count="2">0.000000 89.500000</float_array>
    <technique_common>
      <accessor source="センター_input-array" count="2" stride="1">
        <param name="TIME" type="float" />
      </accessor>
    </technique_common>
  </source>
  <source id="センター_output">
    <float_array id="センター_output-array" count="32">0 90</float_array>
      <technique_common>
        <accessor source="#センター_output-array" count="2" stride="1">
          <param name="ANGLE" type="float" />
        </accessor>
      </technique_common>
    </source>
    <source id="センター_INTERPOLATION">
      <Name_array id="センター_INTERPOLATION-array" count="2">LINEAR LINEAR</Name_array>
      <technique_common>
        <accessor source="#センター_INTERPOLATION-array" count="2" stride="1">
          <param name="INTERPOLATION" type="Name" />
        </accessor>
      </technique_common>
    </source>
    <sampler id="センター_sampler">
      <input semantic="INPUT" source="#センター_intput" />
      <input semantic="OUTPUT" source="#センター_output" />
      <input semantic="INTERPOLATION" source="#センター_INTERPOLATION" />
    </sampler>
    <channel source="#センター_sampler" target="センター/rotateX.ANGLE" />
</animation>

<!-- 平行移動 -->
<animation id="センター_Location">
  <source id="センター_input">
    <float_array id="センター_input-array" count="2">0.000000 89.500000</float_array>
    <technique_common>
      <accessor source="センター_input-array" count="2" stride="1">
        <param name="TIME" type="float" />
      </accessor>
    </technique_common>
  </source>
  <source id="センター_output">
    <float_array id="センター_output-array" count="6">0.000000 6.681501 0.000000 0.000000 6.681501 10.000000</float_array>
      <technique_common>
        <accessor source="#センター_output-array" count="2" stride="3">
          <param name="X" type="float" />
          <param name="Y" type="float" />
          <param name="Z" type="float" />
        </accessor>
      </technique_common>
    </source>
    <source id="センター_INTERPOLATION">
      <Name_array id="センター_INTERPOLATION-array" count="2">LINEAR LINEAR</Name_array>
      <technique_common>
        <accessor source="#センター_INTERPOLATION-array" count="2" stride="1">
          <param name="INTERPOLATION" type="Name" />
        </accessor>
      </technique_common>
    </source>
    <sampler id="センター_sampler">
      <input semantic="INPUT" source="#センター_intput" />
      <input semantic="OUTPUT" source="#センター_output" />
      <input semantic="INTERPOLATION" source="#センター_INTERPOLATION" />
    </sampler>
    <channel source="#センター_sampler" target="センター/location" />
</animation>

フレーム宣言部のlocationとrotateの値をここで変更しています。
Colladaはクォータニオンをそのまま使うことが出来ないようで、rotateタグを使う必要があります。ボーン行列の回転量をクォータニオンで所持してるので、クォータニオンからX・Y・Z軸それぞれの回転量を取り出す必要となったわけです。

そこでひと通りのクォータニオン→軸回転抽出について色々調べてみました。が、全てうまくいきませんでした。今回参考にした計算式を幾つか書いておきます。

①回転量がθ、回転軸をX・Y・Zを表す
クォータニオン(w, x, y, z) = (cosθ/2, Xsinθ/2, Ysinθ/2, Zsinθ/2)

これはもう見たまんまですね。XNAで実際に計算するとこうなります。
Quaternion qt; // 既に値が設定されてるものとする
Vector3 angle = Vector3.Zero;

angle.Z = MathHelper.ToDegrees((float)Math.Asin(qt.Z) * 2.0f);
angle.Y = MathHelper.ToDegrees((float)Math.Asin(qt.Y) * 2.0f);
angle.X = MathHelper.ToDegrees((float)Math.Asin(qt.X) * 2.0f);
この方法を試してみたところ、回転軸が複数個混じってるものは正常な回転量が取り出せないようです。単一軸回転のものは正しい回転量が抽出できます。

②回転行列RがXYZ順で合成した回転行列の場合
回転量Z = atan2(R12, R11)
回転量Y = -asin(R13)
回転量X = asin(R23 / cos(回転量Y)) ※ R33が負数の場合、回転量X = 180 - 回転量X

これをXNAで計算するとこうなります。
Quaternion qt; // 既に値が設定されてるものとする
Matrix rotate = Matrix.CreateFromQuaternion(qt); // 行列化
Vector3 angle = Vector3.Zero;

angle.Z = MathHelper.ToDegrees(
    (float)Math.Atan2(rotate.M12, rotate.M11));

float radianY = -(float)Math.Asin(rotate.M13);
angle.Y = MathHelper.ToDegrees(radianY);

float cosY = (float)Math.Cos(radianY);
angle.X = MathHelper.ToDegrees(
    (float)Math.Asin(rotate.M23) / cosY);

if(rotate.M33 < 0)
  angle.X = 180 - angle.X;
これは回転行列の合成順によって使用する値が異なりますが、計算式は変わりません(全6パターン)。詳しく知りたい方はコチラを参照してください。

YawPitchRollなので合成順はYXZ。XNAは右手系なので合成順を入れ替えて回転行列の合成順をZXYとして計算してみました。

が、コチラもうまくいきませんでいた。Z・Y・Z軸単体回転のモーションをそれぞれ作りテストしてみたのですが、X軸回転だけうまくいきません。念のため6パターン全てのやり方で計算してみましたが、やはり一つの軸だけ正常な値が取り出せませんでした。上記の計算式には条件が足りないのでしょうか?

というわけでクォータニオンからの角度抽出がうまくいかずここ数日悩んでたんですが、先日あることを思い出しました。

そうだ!MATRIXタグ使ってアニメーションすればいいんじゃないかな?と。

むしろ今まで何故気付かなかったのかが今となっては謎ですが、「location」「rotate」「scale」タグを「matrix」タグに書き換えて出力しました。
<node id="センター" type="JOINT">
  <matrix sid="transform">1.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 6.681501 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 1.000000</matrix>
  <node id="センター先" type="JOINT">
    <matrix sid="transform">1.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 -6.474001 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 1.000000</matrix>
  </node> 
<animation id="センター_PoseMatrix">
  <source id="センター_input">
    <float_array id="センター_input-array" count="2">0.000000 0.333333</float_array>
    <technique_common>
      <accessor source="#センター_input-array" count="2" stride="1">
        <param name="TIME" type="float" />
      </accessor>
    </technique_common>
  </source>
  <source id="センター_output">
    <float_array id="センター_output-array" count="32">1.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 6.681501 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 6.681501 0.000000 0.000000 1.000000 -25.750000 0.000000 0.000000 0.000000 1.000000</float_array>
    <technique_common>
      <accessor source="#センター_output-array" count="2" stride="16">
        <param name="TRANSFORM" type="float4x4" />
      </accessor>
    </technique_common>
  </source>
  <source id="センター_INTERPOLATION">
    <Name_array id="センター_INTERPOLATION-array" count="2">LINEAR LINEAR</Name_array>
    <technique_common>
      <accessor source="センター_INTERPOLATION-array" count="2" stride="1">
        <param name="INTERPOLATION" type="Name" />
      </accessor>
    </technique_common>
  </source>
  <sampler id="センター_sampler">
    <input semantic="INPUT" source="#センター_input" />
    <input semantic="OUTPUT" source="#センター_output" />
    <input semantic="INTERPOLATION" source="#センター_INTERPOLATION" />
  </sampler>
  <channel source="#センター_sampler" target="センター/transform" />
</animation>

平行移動・スケール・回転を一つにまとめたのでコード量も少なくなりました。スッキリしていいですね。

Blender2.6で出力したデータを読み込んでみました。
video

以前までのものは読み込んでも型崩れを起こしてたんですが、今回の更新でそれもなくなりました。Papervision3Dでも動作確認済みです。ようやくうまくいきました。

さて、Collada出力が完成したので以前から言ってた通りGTAViewerを更新します。前身のPMDForColladaもクォータニオン→角度抽出処理をしてるので、コチラも再度更新しておきますね。

※追記
GTAViewer、及びPMDForColladaを更新しました。

※Colladaで行列を扱う場合の注意点
以前AndroidでColladaスキンメッシュアニメーションをやった時にも少し触れてたのですが(少し的外れだったけど)、Colladaの行列は「縦行列」のようです(値が行順ではなく列順)。なので出力する際にボーンのローカル行列を転置行列に変換する必要があります(Transpose)。これ、やってるとやってないじゃ大違いです。ツールによって規模が異なりますが、大抵型崩れを起こします。ちなみにBlenderだとこうなります。
video

もはや原型すらありません。自前でCollada入出力をする方がいらっしゃるなら、行列には注意しましょう。アニメーションでも、シーンでも、ジョイントでも、行列を扱う場合は必ず転置行列化するように!

2 件のコメント:

  1. はじめまして。
    MMDインポータ(blender2.58)をMacOSX10.6で利用した時、
    import_pmd.pyのline723 execite loadPMD、line392のloadPMD print
    がTracebackで表示され、unicodeencodeerror: 'ascii' codec can't encode characters in position 24-27: oridinal not in range(128)がでてしまい、pmdのインポートが出来ません、これはpmdファイルのパスが問題なのでしょうか?

    返信削除
  2. おそらくその通りだと思います。
    パスに日本語が含まれてると読み込みに失敗します。

    日本語名が含まれてないパスにファイルを移動させて再度確かめてみてください。

    返信削除

注: コメントを投稿できるのは、このブログのメンバーだけです。