技術

通常のスキニングとデュアルクオータニオンによるスキニングの比較

作ってみた、というだけですので細かい説明には期待しないように。

左が通常のスキニング(線形ブレンド)、右がデュアルクオータニオンによるスキニングです。
ボーン数は見ての通り2、ポリゴン数は各々114です。
(ちなみにこの動画はiOSシミュレータで動作してるものを録画したものです。)

しかしまぁなんというか、デュアルクオータニオンには期待してたんですが、モデルが悪いからかどっちもどっちって感じがするところが残念です。
もっと生物的なものをモデルにするとマッチするのかもしれません。

ただ、品質は置いといてもボーン毎に必要な頂点シェーダのuniform変数のサイズが半分で済むのはかなりの利点かなと思います。
(ボーン毎に必要な4×4の変換行列が、ボーン毎の4×2のデュアルクオータニオンで済む)

それに、通常のスキニングの処理フローを大きく変える必要がないのも良いですね。
(ホストプログラム側でボーンの変換行列をデュアルクオータニオンに変換し、頂点シェーダでデュアルクオータニオンのままブレンドした後行列に戻して頂点や法線に適用するだけ)

以下各頂点シェーダのソース

NormalSkinning.vsh

#define BONE_NUM 2

attribute vec4 position;
attribute vec3 normal;
attribute vec2 weights;

varying lowp vec4 colorVarying;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 boneMatrices[BONE_NUM];

void main()
{
    // 法線の変換
    vec3 n = (mat3(boneMatrices[0]) * normal) * weights.x
           + (mat3(boneMatrices[1]) * normal) * weights.y;
    
    // ライティング
    vec3 eyeNormal = normalize(n);
    vec3 lightPosition = vec3(1.0, 0.0, 1.0);
    vec4 diffuseColor = vec4(0.4, 0.4, 1.0, 1.0);
    float nDotVP = max(0.0, dot(eyeNormal, normalize(lightPosition)));
    colorVarying = diffuseColor * nDotVP;
    
    // 頂点の変換
    vec4 p = (boneMatrices[0] * position) * weights.x
           + (boneMatrices[1] * position) * weights.y;
    gl_Position = projectionMatrix * modelViewMatrix * p;
}

DualQuaternionSkinning.vsh

#define BONE_NUM 2

attribute vec4 position;
attribute vec3 normal;
attribute vec2 weights;

varying lowp vec4 colorVarying;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec4 boneDQs[BONE_NUM * 2];

mat4 dq2matrix(vec4 Qn, vec4 Qd)
{
    // デュアルクオータニオンから4x4の行列を得る
    mat4 M = mat4(0.0);
    float len2 = dot(Qn, Qn);
    float w = Qn.w;
    float x = Qn.x;
    float y = Qn.y;
    float z = Qn.z;
    float t0 = Qd.w;
    float t1 = Qd.x;
    float t2 = Qd.y;
    float t3 = Qd.z;
    
    M[0][0] = w * w + x * x - y * y - z * z;
    M[1][0] = 2.0 * x * y - 2.0 * w * z;
    M[2][0] = 2.0 * x * z + 2.0 * w * y;
    
    M[0][1] = 2.0 * x * y + 2.0 * w * z;
    M[1][1] = w * w + y * y - x * x - z * z;
    M[2][1] = 2.0 * y * z - 2.0 * w * x;
    
    M[0][2] = 2.0 * x * z - 2.0 * w * y;
    M[1][2] = 2.0 * y * z + 2.0 * w * x;
    M[2][2] = w * w + z * z - x * x - y * y;
    
    M[3][0] = -2.0 * t0 * x + 2.0 * w * t1 - 2.0 * t2 * z + 2.0 * y * t3;
    M[3][1] = -2.0 * t0 * y + 2.0 * t1 * z - 2.0 * x * t3 + 2.0 * w * t2;
    M[3][2] = -2.0 * t0 * z + 2.0 * x * t2 + 2.0 * w * t3 - 2.0 * t1 * y;
    
    M[3][3] = len2;
    
    return M / len2;
}

void main()
{
    // デュアルクオータニオンを合成
    vec4 blendedDQ[2];
    blendedDQ[0] = boneDQs[0] * weights.x + boneDQs[2] * weights.y;
    blendedDQ[1] = boneDQs[1] * weights.x + boneDQs[3] * weights.y;
    
    // スキニング用の変換行列化
    mat4 skinTransform = dq2matrix(blendedDQ[0], blendedDQ[1]);
    
    // 法線の変換
    vec3 n = mat3(skinTransform) * normal;
    
    // ライティング
    vec3 eyeNormal = normalize(n);
    vec3 lightPosition = vec3(1.0, 0.0, 1.0);
    vec4 diffuseColor = vec4(0.4, 0.4, 1.0, 1.0);
    float nDotVP = max(0.0, dot(eyeNormal, normalize(lightPosition)));
    colorVarying = diffuseColor * nDotVP;
    
    // 頂点の変換
    gl_Position = projectionMatrix * modelViewMatrix * skinTransform * position;
}

参考

Skinning with Dual Quaternions

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください