Yo !
Bon alors après maints efforts et des bugs sournois de partout, j'y suis arrivé !
J'arrive à déterminer 3 paramètres critiques à partir de 2 matrices :
_ La vitesse linéaire
_ La vitesse angulaire
_ Le point de pivot
Contrairement à Zavie, je calcule tout en WORLD space, mes vitesses sont donc stockées en WORLD.
Voici le code C++ que vous pouvez utiliser pour calculer ces 3 infos :
/// <summary>
/// Helper method that helps to compute the difference in translation and rotation of an object moving from one frame to the other
/// </summary>
/// <param name="_Previous">The object's matrix at previous frame</param>
/// <param name="_Current">The object's matrix at current frame</param>
/// <param name="_DeltaPosition">Returns the difference in position from last frame</param>
/// <param name="_DeltaRotation">Returns the difference in rotation from last frame</param>
/// <param name="_Pivot">Returns the pivot position the object rotated about</param>
public static void ComputeObjectDeltaPositionRotation( ref Matrix _Previous, ref Matrix _Current, out Vector3 _DeltaPosition, out Quaternion _DeltaRotation, out Vector3 _Pivot )
{
// Compute the rotation the matrix sustained
Quaternion PreviousRotation = QuatFromMatrix( _Previous );
Quaternion CurrentRotation = QuatFromMatrix( _Current );
_DeltaRotation = QuatMultiply( QuatInvert( PreviousRotation ), CurrentRotation );
Vector3 PreviousPosition = (Vector3) _Previous.Row4;
Vector3 CurrentPosition = (Vector3) _Current.Row4;
// Retrieve the pivot point about which that rotation occurred
float RotationAngle = _DeltaRotation.Angle;
if ( Math.Abs( RotationAngle ) > 1e-4f )
{
Vector3 RotationAxis = _DeltaRotation.Axis;
_Pivot = PreviousPosition;
Vector3 Previous2Current = CurrentPosition - PreviousPosition;
float L = Previous2Current.Length();
if ( L > 1e-4f )
{
Previous2Current /= L;
Vector3 N = Vector3.Cross( Previous2Current, RotationAxis );
N.Normalize();
Vector3 MiddlePoint = 0.5f * (PreviousPosition + CurrentPosition);
float Distance2Pivot = 0.5f * L / (float) Math.Tan( 0.5f * RotationAngle );
_Pivot = MiddlePoint + N * Distance2Pivot;
}
// Rotate previous position about pivot, this should yield us current position
Vector3 RotatedPreviousPosition = RotateAbout( PreviousPosition, _Pivot, _DeltaRotation );
// Close the gap so we have node delta translation
PreviousPosition = CurrentPosition;
}
else
_Pivot = Vector3.Zero;
_DeltaPosition = CurrentPosition - PreviousPosition; // Easy !
}
static Quaternion QuatFromMatrix( Matrix M )
{
Quaternion q = new Quaternion();
float s = (float) System.Math.Sqrt( M.M11 + M.M22 + M.M33 + 1.0f );
q.W = s * 0.5f;
s = 0.5f / s;
q.X = (M.M32 - M.M23) * s;
q.Y = (M.M13 - M.M31) * s;
q.Z = (M.M21 - M.M12) * s;
return q;
}
static Quaternion QuatInvert( Quaternion q )
{
float fNorm = q.LengthSquared();
if ( fNorm < float.Epsilon )
return q;
float fINorm = -1.0f / fNorm;
q.X *= fINorm;
q.Y *= fINorm;
q.Z *= fINorm;
q.W *= -fINorm;
return q;
}
static Vector3 RotateAbout( Vector3 _Point, Vector3 _Pivot, Quaternion _Rotation )
{
Quaternion Q = new Quaternion( _Point - _Pivot, 0.0f );
Quaternion RotConjugate = _Rotation;
RotConjugate.Conjugate();
Quaternion Pr = QuatMultiply( QuatMultiply( _Rotation, Q ), RotConjugate );
Vector3 Protated = new Vector3( Pr.X, Pr.Y, Pr.Z );
return _Pivot + Protated;
}
static Quaternion QuatMultiply( Quaternion q0, Quaternion q1 )
{
Quaternion q;
Vector3 V0 = new Vector3( q0.X, q0.Y, q0.Z );
Vector3 V1 = new Vector3( q1.X, q1.Y, q1.Z );
q.W = q0.W * q1.W - Vector3.Dot( V0, V1 );
Vector3 V = q0.W * V1 + V0 * q1.W + Vector3.Cross( V0, V1 );
q.X = V.X;
q.Y = V.Y;
q.Z = V.Z;
return q;
}
On obtient la rotation effectuée par la matrice entre t et t-dt (i.e. la vitesse angulaire) en faisant :
Qr = Qp^-1 * Qc (1)
avec:
Qp^-1 l'inverse du quaternion de la matrice de la frame précédente
Qc le quaternion de la matrice courante.
Pour ceux qu'aiment pas les quaternions, c'est exactement la même chose que de résoudre :
Mp * R = Mc
avec
Mc votre matrice courante,
Mp votre matrice précédente
R la matrice de rotation, inconnue, qu'on cherche à trouver.
En composant à gauche par Mp^-1, l'inverse de la matrice précédente, on peut ré-écrire cette équation de cette manière :
R = Mp^-1 * Mc (2)
Convertissez (2) en quaternions à la place des matrices et vous obtenez (1). Simple !
Bon maintenant c'est bien beau, on sait de combien la matrice a tourné mais le plus important est bien entendu de déterminer le point de pivot !
(comme un con je l'avais tout simplement oublié au début, du coup ça marchait bien pour les objets qui tournent sur eux-mêmes mais pas du tout pour ma caméra, qui tourne autour d'une target !)
Le calcul est relativement simple, on connaît :
P(t-dt), la position de la matrice à la frame précédente
P(t), la position de la matrice à la frame courante
θ, l'angle de rotation
R, l'axe de rotation
Si vous regardez le schéma suivant, on doit en déduire la position du point de pivot :

On connaît également :
d = ||P(t) - P(t-dt)||
P(t-dt/2) = 0.5 * [P(t) + P(t-dt)]
N = [P(t) - P(t-dt)] ^ R (à normaliser)
On en déduit, grace à de la trigonométrie de 4ème, que D = 1/2 * d / tan( θ/2 ), la distance de P(t-dt/2) au pivot.
Et finalement, Pivot = P(t-dt/2) + D * N
Je pensais que j'allais être emmerdé par des problèmes de précisions, genre mouvements brusques et autres d'une frame à l'autre mais apparemment ça se comporte assez bien !

Pour utiliser tout ça, c'est simple aussi (tant mieux, vu qu'il faut le faire pour chaque vertex !) :
P' = P + Vl + (RotateAboutAxis( P, Pivot, Va ) - P)
avec :
P la position originale (en WORLD)
P' la nouvelle position après calcul (également en WORLD)
Vl notre vitesse linéaire
Va notre vitesse angulaire (quaternion)
Pivot notre point de pivot magique
RotateAboutAxis() est une fonction qui applique une rotation du point P relativement au point de pivot, autour d'un axe par un angle donnés.
J'ai utilisé la formule de rotation donnée ici
http://local.wasp.uwa.edu.au/~pbourke/geometry/rotate/ :
Point à rotater, converti en quaternion : Q1 = (WorldPos.X - Pivot.X, WorldPos.Y - Pivot.Y, WorldPos.Z - Pivot.Z, 0)
Rotation (notre quaternion de vitesse angulaire qu'on a déjà) : Q2 = (rx sin(θ/2), ry sin(θ/2), rz sin(θ/2), cos(θ/2))
Et : Q3 = Q2 Q1 Q2*
Plus qu'à récupérer le point rotaté dans XYZ du quaternion Q3, auquel on rajoute Pivot pour se retrouver en WORLD non offseté.
Et donc, notre vitesse finale, en WORLD, qu'on va écrire dans le buffer de vitesses pour la frame courante est simplement : V = P' - P
NOTE : On perd l'info de vitesse angulaire en écrivant cette vitesse dans le buffer, malheureusement. Mais pas pour la caméra, comme on le verra + loin !
Voici le code HLSL qui fait les calculs au runtime :
// Holds the method to compute the velocity of a WORLD position given the relative motion and rotation of the vertex's matrix
//
float3 DeltaPosition : MOTION_DELTA_POSITION; // Contains the WORLD previous->current matrix translation
float4 DeltaRotation : MOTION_DELTA_ROTATION; // Contains the WORLD previous->current matrix rotation (in the form of a quaternion)
float3 DeltaPivot : MOTION_DELTA_PIVOT; // Contains the WORLD pivot point about which the object rotated
float4 QuatProduct( float4 _Q0, float4 _Q1 )
{
return float4( (_Q0.w * _Q1.xyz) + (_Q0.xyz * _Q1.w) + cross( _Q0.xyz, _Q1.xyz ), (_Q0.w * _Q1.w) - dot( _Q0.xyz, _Q1.xyz ) );
}
// Rotates a point using a quaternion
// Maths are simple :
// 1) We form a quaternion Q = {Px,Py,Pz,0} from the point to rotate
// 2) We rotate Q using our rotation quaternion R by writing Q' = R P R* (R* being the conjugate of R)
//
float3 RotateAbout( float3 _Point, float3 _Pivot, float4 _R )
{
float4 Q = float4( _Point - _Pivot, 0.0 );
float4 Rc = float4( -_R.xyz, _R.w );
return _Pivot + QuatProduct( QuatProduct( _R, Q ), Rc ).xyz;
}
// Computes the velocity of a WORLD position given the delta position/rotation sustained by this point's LOCAL=>WORLD matrix
// _WorldPosition, the position of a point in its WORLD space
// _ObjectDeltaPosition, the delta position of the object (in WORLD space)
// _ObjectDeltaRotation, the delta rotation of the object (in WORLD space)
// _ObjectDeltaPivot, the rotation pivot of the object matrix (in WORLD space)
//
float3 ComputeVelocity( float3 _WorldPosition, float3 _ObjectDeltaPosition, float4 _ObjectDeltaRotation, float3 _ObjectDeltaPivot )
{
float3 Velocity = _ObjectDeltaPosition; // Really simple for translation : just add !
// Compute the delta-rotation matrix from the delta-rotation quaternion
float3 RotatedPosition = RotateAbout( _WorldPosition, _ObjectDeltaPivot, _ObjectDeltaRotation );
// Angular velocity is simply the difference between current and rotated position
Velocity += RotatedPosition - _WorldPosition;
return Velocity;
}
// Same but uses variables declared at the top
float3 ComputeVelocity( float3 _WorldPosition )
{
return ComputeVelocity( _WorldPosition, DeltaPosition, DeltaRotation, DeltaPivot );
}
Pour utiliser ce code, on doit calculer pour chaque objet sa vitesse linéaire, angulaire et le pivot en faisant :
Vector3 DeltaPosition, DeltaPivot;
Quaternion DeltaRotation;
ComputeObjectDeltaPositionRotation( ref PreviousLocal2World, ref CurrentLocal2World, out DeltaPosition, out DeltaRotation, out DeltaPivot );
Qu'on balance au shader, pour chaque objet.
Ensuite, le code HLSL pour calculer un vecteur vitesse WORLD dans un vertex shader quelconque :
float3 WorldVelocity = ComputeVelocity( WorldPosition ); // A passer au pixel shader et à écrire tel quel dans le buffer de vitesses !
Et finalement, pour la passe de motion blur, je calcule la vitesse linéaire, angulaire et le pivot de la caméra (comme pour n'importe quel autre objet) :
Vector3 DeltaPosition, DeltaPivot;
Quaternion DeltaRotation;
ComputeObjectDeltaPositionRotation( ref PreviousCamera2World, ref CurrentCamera2World, out DeltaPosition, out DeltaRotation, out DeltaPivot );
Que je balance au shader de motion blur, dont voici le code :
float4 PS( VS_IN _In ) : SV_TARGET0
{
// Read color, velocity & depth of the current pixel
float2 UV = _In.Position.xy * GBufferInvSize.xy;
float3 RGB = GBufferTexture0.SampleLevel( NearestClamp, UV, 0 ).xyz;
float3 WorldVelocity = GBufferTexture3.SampleLevel( NearestClamp, UV, 0 ).xyz;
float Depth = ReadDepth( UV );
// Rebuild WORLD position from depth
float3 View = float3( CameraData.y * CameraData.x * (2.0 * UV.x - 1.0), CameraData.x * (1.0 - 2.0 * UV.y), 1.0 );
float3 CameraPosition = View * Depth;
float3 WorldPosition = mul( float4( CameraPosition, 1.0 ), Camera2World ).xyz;
// Add camera velocity to the pixel's own velocity
WorldVelocity += ComputeVelocity( WorldPosition );
// Project into 2D
float4 VelocityProj = mul( float4( WorldPosition + WorldVelocity, 1.0 ), World2Proj );
VelocityProj /= VelocityProj.w;
float2 Velocity = 0.5 * float2( 1.0 + VelocityProj.x, 1.0 - VelocityProj.y ) - UV;
// Perform blur
float2 dUV = -BlurSize * Velocity / STEPS_COUNT; // <= J'utilise 16 steps
float SumWeights = 1.0;
for ( int StepIndex=0; StepIndex < STEPS_COUNT; StepIndex++ )
{
UV += dUV;
RGB += GBufferTexture0.SampleLevel( NearestClamp, UV, 0 ).xyz;
SumWeights += 1.0;
}
RGB *= BlurWeightFactor / SumWeights;
return float4( RGB, 1.0 );
}
Voici le résultat quand je tourne la caméra autour d'une target dont on aperçoit bien le centre, grace à super-pivot

:

(les trainées super énormes et rectilignes sur les particouilles qui sont sur fond noir, pas celles qui ont un mesh derrière elles, viennent du fait que le Z du pixel se retrouve à l'infini et on perçoit donc les particules comme étant à l'infini également, ce qui est une limitation de tous les motion blur cheapos quoi qu'il arrive)
Voici 2 images avec un seul objet tournant très rapidement (une boîte avec 2 boîtes filles attachées) :
SANS motion bleurre
AVEC motion bleurre
C'est évidemment pas parfait car les points en dehors des boîtes ont une vitesse angulaire nulle et la trainée de blur s'arrête brutalement. Il faudrait "étendre" le mesh d'une manière quelconque je pense. Peut-être l'afficher 2 fois : une fois à la position de la frame précédente grace à notre calcul de vitesse, et une seconde fois à sa position actuelle tel qu'on le fait normalement ?
Ou bien p'têt calculer une silhouette qu'on extruderait en suivant la vitesse ? Il me semble que j'ai déjà vu ce truc dans un exemple DirectX avec un vieux lion super moche, j'avais trouvé ça bien naze à l'époque...
Idéalement, il faudrait savoir où se trouve le centre de l'objet et extruder depuis ce centre je pense (je pourrais essayer avec mon pivot comme centre tiens, huhu !).
A noter 2 trucs que j'ai pas faits :
1) Limiter la vitesse projetée pour pas que ça bave de trop
2) Me servir du point de pivot et de la vitesse angulaire de la caméra pour calculer un arc de cercle, que je projetterais en 2D et que je suivrais, à la place de suivre une trajectoire bêtement rectiligne
Pour 2), je pensais simplement à projeter 3 points de l'arc de cercle en 2D : le point de départ, le point d'arrivée et le point milieu. J'approximerais ces 3 points par une parabole et l'inner loop de motion blur deviendrait donc :
float2 dUV = Some magic delta computed from the projected parabola
float2 ddUV = Some magic delta velocity also computed from the projected parabola
float SumWeights = 1.0;
for ( int StepIndex=0; StepIndex < STEPS_COUNT; StepIndex++ )
{
UV += dUV; // <== Vitesse
dUV += ddUV; // <== Accélération
RGB += GBufferTexture0.SampleLevel( NearestClamp, UV, 0 ).xyz;
SumWeights += 1.0;
}
RGB *= BlurWeightFactor / SumWeights;
return float4( RGB, 1.0 );
}
A creuser, mais chuis pas certain que ça se voie tant que ça d'interpoiler sur un arc plutôt qu'une ligne...