Advertisement

Dual Quaternion Skinning Artifacts

Started by October 21, 2018 02:31 PM
5 comments, last by CrazyCdn 6 years, 3 months ago

Hello,

I have regular matrix-based skinning on the GPU working for quite a while now and I stumbled upon an implementation of Dual Quaternion skinning.
I've had a go at implementing this in a shader and after spending a lot of time on making small changes to the formulas to make it work, I sort of got it working but there seems to be an issue when blending bones.
I found this pretty old topic on GameDev.net (

) which, I think, describes my problem pretty well but I haven't been able to find the problem.
Like in that post, if the blendweight of a vertex is 1, there is no problem. Once there is blending, I get artifacts
Just for the sake of just focussing on the shader side of things first, I upload the dual quaternions to GPU which are converted from regular matrices (because I knew they should work).

Below an image comparison between matrix skinning (left) and dual quaternion skinning (right):

Comparison.jpg.41447e74f3ceb5341c66bae4af2ce7f1.jpg

As you can see, especially on the shoulders, there are some serious issues.

It might be because of a silly typo however I'm surprised some parts of the mesh look perfectly fine.

Below some snippets:


//Blend bones
float2x4 BlendBoneTransformsToDualQuaternion(float4 boneIndices, float4 boneWeights)
{
    float2x4 dual = (float2x4)0;
    float4 dq0 = cSkinDualQuaternions[boneIndices.x][0];
    for(int i = 0; i < MAX_BONES_PER_VERTEX; ++i)
    {
        if(boneIndices[i] == -1)
        {
            break;
        }
        if(dot(dq0, cSkinDualQuaternions[boneIndices[i]][0]) < 0)
        {
            boneWeights[i] *= -1;
        }
        dual += boneWeights[i] * cSkinDualQuaternions[boneIndices[i]];
    }
    return dual / length(dual[0]);
}

//Used to transform the normal/tangent
float3 QuaternionRotateVector(float3 v, float4 quatReal)
{
    return v + 2.0f * cross(quatReal.xyz, quatReal.w * v + cross(quatReal.xyz, v));
}

//Used to transform the position
float3 DualQuatTransformPoint(float3 p, float4 quatReal, float4 quatDual)
{
    float3 t = 2 * (quatReal.w * quatDual.xyz - quatDual.w * quatReal.xyz + cross(quatDual.xyz, quatReal.xyz));
    return QuaternionRotateVector(p, quatReal) + t;
}

I've been staring at this for quite a while now so the solution might be obvious however I fail to see it.
Help would be hugely appreciated :) 

Cheers

Searched and found this link http://dev.theomader.com/dual-quaternion-skinning/

..and it seems he does "cross(real, dual)" and not "cross("dual, real)" in the TransformPoint part..?

 

.:vinterberg:.

Advertisement
6 minutes ago, vinterberg said:

Searched and found this link http://dev.theomader.com/dual-quaternion-skinning/

..and it seems he does "cross(real, dual)" and not "cross("dual, real)" in the TransformPoint part..?

 

Well spotted, I've been looking at this before too and following his methods exactly creates a worse result:

Screenshot_1.thumb.jpg.49a48886bf41326b64a0383b658c70ee.jpg

Update:

I've isolated the problem a bit more. I've created a function to convert the blended dual quaternion to a matrix and then transform the vertex positions/normals with that (instead of using QuaternionRotateVector and DualQuatTransformPoint above) and it produces the same exact artifacts:


float4 QuaternionMultiply(float4 q1, float4 q2)
{
    return float4(
        (q2[3] * q1[0]) + (q2[0] * q1[3]) + (q2[1] * q1[2]) - (q2[2] * q1[1]),
        (q2[3] * q1[1]) - (q2[0] * q1[2]) + (q2[1] * q1[3]) + (q2[2] * q1[0]),
        (q2[3] * q1[2]) + (q2[0] * q1[1]) - (q2[1] * q1[0]) + (q2[2] * q1[3]),
        (q2[3] * q1[3]) - (q2[0] * q1[0]) - (q2[1] * q1[1]) - (q2[2] * q1[2]));
}

float4x4 DualQuaternionToMatrix(float4 quatReal, float4 quatDual)
{
    float x = quatReal.x;
    float y = quatReal.y;
    float z = quatReal.z;
    float w = quatReal.w;

    float4x4 m;
    m._11 = w * w + x * x - y * y - z * z;
    m._12 = 2 * x * y + 2 * w * z;
    m._13 = 2 * x * z - 2 * w * y;
    m._14 = 0;

    m._21 = 2 * x * y - 2 * w * z;
    m._22 = w * w + y * y - x * x - z * z;
    m._23 = 2 * y * z + 2 * w * x;
    m._24 = 0;

    m._31 = 2 * x * z + 2 * w * y;
    m._32 = 2 * y * z - 2 * w * x;
    m._33 = w * w + z * z - x * x - y * y;
    m._34 = 0;

    float4 conj = float4(-quatReal.x, -quatReal.y, -quatReal.z, quatReal.w);
    float4 t = QuaternionMultiply((quatDual * 2.0f), conj);
    m._41 = t.x;
    m._42 = t.y;
    m._43 = t.z;
    m._44 = 1;

    return m;
}

This makes me think that there's either something wrong with the input quaternions or the function that does the blending.
However I'm quite certain the input data is right because when converting the dual quaternions back to matrices on the CPU, they match (Did this to test my ToMatrix function).

I've found the culprit.
Turns out the source link and vinterberg were right!
Besides the two arguments being in the wrong order, there wasn't an issue in the shader at all.
The problem lied within the function that created a dual quaternion from a position and a rotation.
Below the code before and after:


//Before
DualQuaternion(const Quaternion& rotation, const Vector3& position)
{
  rotation.Normalize(Real);
  Dual = Quaternion(position, 0) * Real * 0.5f;
}

//After
DualQuaternion(const Quaternion& rotation, const Vector3& position)
{
  rotation.Normalize(Real);
  Dual = 0.5f * Real * Quaternion(position, 0);
}

So problem solved :) It's really easy to make mistakes like this...

(Any way to mark this thread as "Solved"?)

1 hour ago, simco50 said:

(Any way to mark this thread as "Solved"?)

You just did.  We don't like to change the topic to solved etc as people might still have things to add to it that could be helpful to someone else later.  Glad you got it worked out though!

"Those who would give up essential liberty to purchase a little temporary safety deserve neither liberty nor safety." --Benjamin Franklin

This topic is closed to new replies.

Advertisement