Skip to main content
GameDev.net gamedev.net
🔒 Locked 🎮 Unity

Dual-Quaternion Skinning vs. Linear-Based Quaternion Skinning

Started by Steve_Segreto Jun 24, 2010 at 5:10 PM 1 replies 7.7k views
Original Post
Steve_Segreto
Steve_Segreto
I just got this working from an NVIDIA DX10 sample on a DX9 project I'm working on and wanted to share the results so that others might comment on its use or try it themselves? I have noticed the combination of some rotations on certain skinned models results in "candy wrapper" twisting, which can be fixed either by adding "roll" bones and additional weighting information or by using the fast dual-quaternion algorithm presented in the code sample by NVIDIA.

Linear-based skinning DX9

Dual Quaternion Based Skinning

Jade: Linear Based Skinning

Jade: Dual Quat-Based Skinning

What do you guys think?
e3d_ALiVE
e3d_ALiVE
any tutorials or samples with DQS on dx9 will be welcome
Crazy dude smoking D3D11, AngelScript, PhysX, 3D Sound, Network, DB, FBX, and some other weird things, doing that on Visual Studio 2010 + Visual Assist X on Overclocked CPU
Steve_Segreto
Steve_Segreto
Here is the vertex declaration:


Vertex declaration 08F0DA20
{ Stream = 0, Offset = 0, Type = D3DDECLTYPE_FLOAT3, Method = D3DDECLMETHOD_DEFAULT, Usage = D3DDECLUSAGE_POSITION, UsageIndex = 0 },
{ Stream = 0, Offset = 12, Type = D3DDECLTYPE_FLOAT3, Method = D3DDECLMETHOD_DEFAULT, Usage = D3DDECLUSAGE_NORMAL, UsageIndex = 0 },
{ Stream = 0, Offset = 24, Type = D3DDECLTYPE_FLOAT3, Method = D3DDECLMETHOD_DEFAULT, Usage = D3DDECLUSAGE_BLENDWEIGHT, UsageIndex = 0 },
{ Stream = 0, Offset = 36, Type = D3DDECLTYPE_D3DCOLOR, Method = D3DDECLMETHOD_DEFAULT, Usage = D3DDECLUSAGE_BLENDINDICES, UsageIndex = 0 },
{ Stream = 0, Offset = 40, Type = D3DDECLTYPE_FLOAT2, Method = D3DDECLMETHOD_DEFAULT, Usage = D3DDECLUSAGE_TEXCOORD, UsageIndex = 0 }


Here is the vertex shader/pixel shader generated by the engine I'm working on.


//--------------------------------------------------------------------------------------
// Automatically generated vertex shader
//
// Copyright © Steve Segreto. All rights reserved.
//--------------------------------------------------------------------------------------
struct Mtrl
{
float4 ambient;
float4 diffuse;
float4 spec;
float specPower;
float4 emissive;
};

struct DirLight
{
float4 ambient;
float4 diffuse;
float4 spec;
float3 dirW;
};

//--------------------------------------------------------------------------------------
// Macro defines
//--------------------------------------------------------------------------------------
#define MATRIX_PALETTE_SIZE (50)

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
uniform extern float4x4 gWorld;
uniform extern float4x4 gInvWorld;
uniform extern float4x4 gWVP;
uniform extern float3 gEyePosW;
uniform extern float gFogRange = 250.0f;
uniform extern float gFogStart = 1.0f;
uniform extern Mtrl gMtrl;
uniform extern DirLight gLight;
uniform extern matrix amPalette[ MATRIX_PALETTE_SIZE ];
uniform extern float gNumBones;

//----------------------------------------------------------------------------
// Shader body - VS_ Skin
//----------------------------------------------------------------------------

//
// Define the inputs -- caller must fill this, usually right from the VB.
//
struct VS_SKIN_INPUT
{
float4 vPos;
float3 vNor;
float3 vBlendWeights;
float4 vBlendIndices;
};

//
// Return skinned position and normal
//
struct VS_SKIN_OUTPUT
{
float4 vPos;
float3 vNor;
};

//
// Call this function to skin VB position and normal.
//
VS_SKIN_OUTPUT VS_Skin( const VS_SKIN_INPUT vInput, int iNumBones )
{
VS_SKIN_OUTPUT vOutput = (VS_SKIN_OUTPUT) 0;
float afBlendWeights[ 3 ] = (float[ 3 ]) vInput.vBlendWeights;
float fLastWeight = 1.0f - afBlendWeights[ 0 ];
int aiIndices[ 4 ] = (int[ 4 ]) D3DCOLORtoUBYTE4( vInput.vBlendIndices );
float2x4 dual = (float2x4)0;
float2x4 m = (float2x4)amPalette[ aiIndices[ 0 ] ];
float4 dq0 = (float1x4)m;
float4 dq;

dual = afBlendWeights[ 0 ] * m;
for( int iBone = 1; (iBone < 3) && (iBone < iNumBones - 1); ++ iBone )
{
fLastWeight -= afBlendWeights[ iBone ];
m = (float2x4)amPalette[ aiIndices[ iBone ] ];
dq = (float1x4)m;
if (dot( dq0, dq ) < 0)
{
dual -= afBlendWeights[ iBone ] * m;
}
else
{
dual += afBlendWeights[ iBone ] * m;
}
}

m = (float2x4)amPalette[ aiIndices[ iNumBones - 1 ] ];
dq = (float1x4)m;
if (dot( dq0, dq ) < 0)
{
dual -= fLastWeight * m;
}
else
{
dual += fLastWeight * m;
}

//
// Fast dual-quaternion skinning.
//
float length = sqrt(dual[0].w * dual[0].w + dual[0].x * dual[0].x + dual[0].y * dual[0].y + dual[0].z * dual[0].z);
dual = dual / length;
vOutput.vPos.xyz = vInput.vPos.xyz + 2.0 * cross(dual[0].xyz, cross(dual[0].xyz, vInput.vPos.xyz) + dual[0].w * vInput.vPos.xyz);
float3 translation = 2.0 * (dual[0].w * dual[1].xyz - dual[1].w * dual[0].xyz + cross(dual[0].xyz, dual[1].xyz));
vOutput.vPos.xyz += translation;
vOutput.vNor = vInput.vNor + 2.0 * cross(dual[0].xyz, cross(dual[0].xyz, vInput.vNor) + dual[0].w * vInput.vNor);

return vOutput;
}

struct VS_in
{
float3 posL : POSITION0;
float3 normalL : NORMAL0;
float3 BlendWeights : BLENDWEIGHT;
float4 BlendIndices : BLENDINDICES;
float2 tex0 : TEXCOORD0;
};

struct VS_out
{
float4 posH : POSITION0;
float2 tex0 : TEXCOORD0;
float3 normalW : TEXCOORD1;
float3 posW : TEXCOORD2;
float4 color : COLOR0;
float fogLerpParam : COLOR1;
};

VS_out VS_Scene( VS_in i )
{
//
// Zero out our output.
//
VS_out o = (VS_out)0;

//
// Skin VB inputs
//
VS_SKIN_INPUT vsi = { float4( i.posL, 1.0f ), i.normalL, i.BlendWeights, i.BlendIndices };

VS_SKIN_OUTPUT vso = VS_Skin( vsi, gNumBones );

//
// Transform normal to world space and pass along to be interpolated by rasterizer.
//
o.normalW = normalize(mul(float4(vso.vNor, 0.0f), gInvWorld).xyz);

//
// Transform position to world space and pass along to be interpolated by rasterizer.
//
o.posW = mul( float4( vso.vPos.xyz, 1.0f ), gWorld).xyz;

//
// Pass along material color to be interpolated by rasterizer.
//
o.color = gMtrl.diffuse;

//
// Transform position to homogeneous clip space.
//
o.posH = mul( float4( vso.vPos.xyz, 1.0f ), gWVP);

//
// Pass on texture coordinates to be interpolated in rasterization.
//
o.tex0 = i.tex0;

//
// Compute vertex distance from camera in world space for fog calculation.
//
float dist = distance( float4( vso.vPos.xyz, 1.0f ), gEyePosW);
o.fogLerpParam = saturate((dist - gFogStart) / gFogRange);

//
// Done--return the output.
//
return o;
}



//--------------------------------------------------------------------------------------
// Automatically generated pixel shader
//
// Copyright © Steve Segreto. All rights reserved.
//--------------------------------------------------------------------------------------
struct Mtrl
{
float4 ambient;
float4 diffuse;
float4 spec;
float specPower;
float4 emissive;
};

struct DirLight
{
float4 ambient;
float4 diffuse;
float4 spec;
float3 dirW;
};

//--------------------------------------------------------------------------------------
// Macro defines
//--------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
uniform extern float3 gEyePosW;
uniform extern float3 gFogColor;
uniform extern Mtrl gMtrl;
uniform extern DirLight gLight;
uniform extern texture gTex0;

struct PS_in
{
float2 tex0 : TEXCOORD0;
float3 normalW : TEXCOORD1;
float3 posW : TEXCOORD2;
float4 color : COLOR0;
float fogLerpParam : COLOR1;
};

sampler TexS0 = sampler_state
{
Texture = <gTex0>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Point;
AddressU = Wrap;
AddressV = Wrap;
};

float4 PS_Scene( PS_in i ) : COLOR
{
//
// Interpolated normals can become unnormal.
//
i.normalW = normalize(i.normalW);

//
// Compute the vector from the vertex to the eye.
//
float3 toEye = normalize(gEyePosW - i.posW);

//
// Determine diffuse light intensity on vertex.
//
float s = saturate(dot(gLight.dirW, i.normalW));

//
// Compute the reflection vector.
//
float3 r = reflect( -gLight.dirW, i.normalW );

//
// Determine how much specular light makes it into the eye.
//
float t = pow(saturate(dot(r, toEye)), gMtrl.specPower);

//
// VERT_MODE_SRC_IGNORE
//
float3 matAmbient = saturate( gMtrl.ambient.rgb + float3( 0.8f, 0.8f, 0.8f ) );
float4 matDiffuse = gMtrl.diffuse;
float3 matEmissive = gMtrl.emissive.rgb;

//
// Incoming color.
//
float3 color_stage0 = (matAmbient * gLight.ambient) +
(s * (matDiffuse * gLight.diffuse).rgb) +
matEmissive + (t * (gMtrl.spec * gLight.spec).rgb);
float alpha_stage0 = matDiffuse.a;

//
// Sample textures.
//
float4 color0 = tex2D(TexS0, i.tex0);

//
// Apply texturing stages
//

//
// Diffuse map.
//
float3 color_stage1 = color_stage0 * color0.rgb;
alpha_stage0 = alpha_stage0 * color0.a;

//
// Final (pre-fog) color.
//
float4 texColor = float4( color_stage1.r, color_stage1.g, color_stage1.b, alpha_stage0 );

//
// Add fog
//
return ( lerp( texColor, float4( gFogColor, texColor.a ), i.fogLerpParam ));
}


This is the trickier part. If you adapt the DX9 MultiAnimation Sample, you can simply put this code in right after the sample creates
the "working" palette of 4x4 matrices and right before it uploads them to the GPU.


D3DXMatrixMultiply( &m_pSceneGraph->m_amxWorkingPalette[ dwPalEntry ],
&( pMC->m_amxBoneOffsets[ dwMatrixIndex ] ),
pMC->m_apmxBonePointers[ dwMatrixIndex ] );
//
// Add this for Dual-Quaternion Skinning.
//
D3DXQUATERNION q;
D3DXQuaternionRotationMatrix( &q, &m_pSceneGraph->m_amxWorkingPalette[ dwPalEntry ] );
D3DXVECTOR3 t = D3DXVECTOR3( m_pSceneGraph->m_amxWorkingPalette[ dwPalEntry ]._41,
m_pSceneGraph->m_amxWorkingPalette[ dwPalEntry ]._42,
m_pSceneGraph->m_amxWorkingPalette[ dwPalEntry ]._43 );
D3DXMATRIX temp_Mat;
D3DXVECTOR4 dual[2];
UQTtoUDQ( dual, q, t );
for (UINT j = 0; j < 2; j++)
{
temp_Mat.m[j][0] = dual[j].x;
temp_Mat.m[j][1] = dual[j].y;
temp_Mat.m[j][2] = dual[j].z;
temp_Mat.m[j][3] = dual[j].w;
}
m_pSceneGraph->m_amxWorkingPalette[ dwPalEntry ] = temp_Mat;


Also you will need to add this function somewhere in the MultiAnimationLib.cpp file:


// convert unit quaternion and translation to unit dual quaternion
void UQTtoUDQ( D3DXVECTOR4 dual[2], D3DXQUATERNION quat, D3DXVECTOR3 tran )
{
dual[0].x = quat.x;
dual[0].y = quat.y;
dual[0].z = quat.z;
dual[0].w = quat.w;
dual[1].x = 0.5f * ( tran[0] * quat.w + tran[1] * quat.z - tran[2] * quat.y );
dual[1].y = 0.5f * (-tran[0] * quat.z + tran[1] * quat.w + tran[2] * quat.x );
dual[1].z = 0.5f * ( tran[0] * quat.y - tran[1] * quat.x + tran[2] * quat.w );
dual[1].w = -0.5f * (tran[0] * quat.x + tran[1] * quat.y + tran[2] * quat.z );
}

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.