1
votes

If I want to animate values from 1 transformation matrices to another, how can i find the transformation matrix that represents the transformation somewhere in between?

AS3 Matrix:

|  a  b  u  |
|  c  d  v  |
|  x  y  w  |

Matrix A (No scale, skew or rotation)

|  1  0  0  |
|  0  1  0  |
|  0  0  1  |

Matrix B (No scale or skew. Rotated 90°)

|  0  1  0  |
|  -1 0  0  |
|  0  0  1  |

To find the matrix for halfway between, which would be a rotation of 45°, my first guess was that I should simply find the values halfway between for each value in the matrix, which would be this:

|  0.5  0.5  0  |
|  -0.5 0.5  0  |
|  0    0    1  |

However, the result IS rotated 45°, BUT it is also scaled down.

I have figured out that the matrix for this case should actually be something close to this:

|  0.7  0.7  0  |
|  -0.7 0.7  0  |
|  0    0    1  |

But what formula or operation can I use to get the correct result given any 2 matrices?

Update 1 - Intended Usage: I need this to work for any 2 matrices, both of which may have translation, scale, skew, and/or rotation.

I am creating a tool that inspects keyframes on the timeline in Flash and exports the transformation values to be used in other environments, initially C# in Unity. The reason I was using matrices instead of the values of rotation, scale, and skew, is that Flash is inconsistent about how it reports skew and rotation, however the reported transformation matrix is reliable.

Also in Unity, I am applying the transformations to the points of a mesh, so it is helpful to have it as a matrix. Basically like this:

x' = (x * a) - (y * c);
y' = (x * b) - (y * d);

So what I am thinking from these very helpful answers is, instead of attempting to interpolate matrices themselves, perhaps I should:

  1. Grab the transform matrix.
  2. Based on the matrix, extract values for translation, scale, skew and rotation.
  3. During my tween, interpolate those values.
  4. Then create a matrix from the interpolated values, and apply the transformation.
3
For the case of pure rotation you have to use Theta=(Alpha1 + Alpha2)/2 and Cos(Theta), +/-Sin(Theta) as matrix elementsMBo
Don't forget, w is 1 in all your matrices.Vesper
Oops, updated the values for w.Dustin
Damn, that update really killed my answer. Well, bummer.Cimbali
I have a solution - somewhat, for this; but it only gives you the rotation not skew; if you're interested in the partial solution I'll post it below... mathb.in/51333 is sort of the gist of it; if you can convert the matrix to axis-angle it becomes trivial to solve between any two orienations.J Decker

3 Answers

0
votes

This is a very generic mathematical answer. If you only want to interpolate between rotations around a common axis, computing the angle of rotation, interpolating that and constructing a rotation matrix for the new angle would probably be sufficient. In that case, feel free to ignore the rest of this post.

Look at this post of mine on Math SE. To find a matrix half way between two given matrices A and B, you'd compute C=B A−1, diagonalize that to C=P D P−1, and then obtain a matrix half way in between as E=P D½P A. I.e. you take the square roots of the eigenvalues, which in turn might be conjugate complex numbers. If these square roots are complex, they should be conjugate to one another, so choose the branch of the complex square root appropriately.

Make sure to perform all of this on a square matrix which expresses your transformation by operating on homogeneous coordinates. Otherwise your choice of coordinate system will affect the result. If your 3×3 matrices are to represent planar transformations, I'd expect a 1 in the bottom right entry. If they are linear operations in space, then extend them to 4×4 by adding zeros to the right and bottom, but a one in the bottom right corner.

0
votes

You need to manually recalculate the matrix based on whatever transformation you intend to interpolate. Say, you want the object to slowly downscale AND rotate, then you use a function that'll compose the proper matrix based on the current tween position. An example:

var my_tween:Tween=new Tween(clip_mc,'alpha',Strong.easeOut,0,1,1,true);
my_tween.addEventListener(TweenEvent.MOTION_CHANGE,tweenToFinal);
function tweenToFinal(event:TweenEvent):void
{
    var mat:Matrix=new Matrix();
    mat.scale(startScale+(endScale-startScale)*event.position,startScale+(endScale-startScale)*event.position);
    mat.rotate(startAngle+(endAngle-startAngle)*event.position);
    yourObject.transform.matrix=mat;
}

This tween will dispatch events each frame that'll make tweenToFinal calculate a new matrix of your desired transform. Note that startScale and startAngle should be the values of starting transformation on the object, say if it was rotated 45 degrees counterclockwise, startAngle should be equal to Math.PI/4.

0
votes

There are more concrete things we can do than the very general approach to diagonalizing your matrix, supposing you only do scaling, rotating, and translating : no shearing (a.k.a. skewing).

Reflection is (a composition of rotations, translations, and) scaling by -1 along one coordinate, so we can safely consider it. If your transformation includes shearing, the easiest way is to refer to MvG's answer.

However if you know the start and end shear values, you may also use A0 and A1 below as your start and end matrices by removing the shear (multiplying them by the matrix of opposite shear at both points) and then use the matrix defined below multiplied by an interpolated shear.

Pieces of the puzzle

With our assumptions, any transformation matrix A is given and can be decomposed as follows :

    | a  c  tx |   | i*x  -i*y  u |
A = | b  d  ty | = | j*y   j*x  v |
    | 0  0  1  |   |  0     0   1 |

Where x and y are the respective sine and cosine of the final rotation angle. So to get the translating and scaling values, we can do the following :

u = A.tx;
v = A.ty;
i = Math.sqrt(A.a * A.a + A.c * A.c);
j = Math.sqrt(A.b * A.b + A.d * A.d);

Now we need to know by how much to rotate. Suppose for each t in 0..1, you have a rotation R(t) such that R(0) = matrix(a=x0, b=y0, c=y0, d=x0) is the rotation in your initial matrix A0, and similarly R(1) = matrix(a=x1, b=y1, c=y1, d=x1) is the rotation in your final matrix A1

Then the total rotation matrix you will be applying from the beginning to the end of the movement is T = R(0)^-1 R(1) and since R is a rotation, its inverse is its transposed. Thus :

    |  x0  y0  0 |   |  x1 -y1  0 |    | x0*x1 + y0*x1   -x0*y1 + y0*x1   0 |
T = | -y0  x0  0 | x |  y1  x1  0 | =  |-y0*x1 + x0*y1    y0*y1 + x0*x1   0 |
    |  0   0   1 |   |  0   0   1 |    |       0                0         1 |

This is still a rotation matrix. Thus we can get the angle of rotation of T :

angle = atan2( x0 * y1  -  y0 * x1,   y0 * y1  +  x0 * x1 )

which can be rewritten, by scaling both sides by j0*j1 (defined respectively from A0 and A1 as j is defined from A above. Scaling both components does not modify the returned value of atan2). :

angle = atan2(A0.d*A1.b - A0.b*A1.d, A0.b*A1.b + A0.d*A1.d)

Putting them together

Thus, supposing you have to matrices, A0 and A1, you start with the rotation at A0 and do an easy linear interpolation on the scaling and translating coordinates. Then you apply the partial rotation that you want, as a fraction of angle.

// compute i1 / i0 and j1 / j0
i1_0 = Math.sqrt( (A1.a * A1.a + A1.c * A1.c) / (A0.a * A0.a + A0.c * A0.c) );
j1_0 = Math.sqrt( (A1.b * A1.b + A1.d * A1.d) / (A0.b * A0.b + A0.d * A0.d) );

angle = Math.atan2(A0.d*A1.b - A0.b*A1.d, A0.b*A1.b + A0.d*A1.d);

for (i = 0; i < MAX; i++) 
{ 
    t = i/MAX;
    // the values of i and j at t are scaled by i and j at 0
    // in order to reuse easily A0's values, which include initial rotation
    it = (1-t) + t * i1_0;
    jt = (1-t) + t * j1_0;
    At = new Matrix(
        a = A0.a * it,
        b = A0.b * jt,
        c = A0.c * it,
        d = A0.d * jt,
        tx = (1-t) * A0.tx + t * A1.tx,
        ty = (1-t) * A0.ty + t * A1.ty
    );
    At.rotate( t * angle );

    // now apply matrix At instead of A0 or A1
    obj.transform.matrix = At;
}

Obviously, if you just want the matrix that is halfway, use only one t with a value of 0.5. We could get more clever and get half the rotation of T using Carnot's linearization formulas for double angles, thus without ever calling trigonometric formulas, but I don't think it's really worth it.

This code is also easily generalizable to any other programming language since it only uses very common functions.

The easiest way to showcase this is to include a javascript test snippet, see below. I have used actionscript naming conventions for coherence with the rest of the post (i.e. the .a .tx and so on).

function rotate_mat(A, angle)
{
    var x = Math.cos(angle);
    var y = Math.sin(angle);
    var B = {a:x*A.a+y*A.c, b:x*A.b+y*A.d, c:-y*A.a+x*A.c, d:-y*A.b+x*A.d, tx:A.tx, ty:A.ty};
    return B;
}

function apply(id,A)
{
    var mat="matrix("+A.a+","+A.b+","+A.c+","+A.d+","+A.tx+","+A.ty+")";
    document.getElementById(id).style.transform=mat;
}

function val(str) { return document.getElementById(str).value; }

document.getElementById("go").onclick = function(options)
{
    var A0 = {a:val("a0"), b:val("b0"), c:val("c0"), d:val("d0"), tx:val("tx0"), ty:val("ty0")};
    var A1 = {a:val("a1"), b:val("b1"), c:val("c1"), d:val("d1"), tx:val("tx1"), ty:val("ty1")};
    apply("guineapig", A0);
    
    var i1_0 = Math.sqrt( (A1.a * A1.a + A1.c * A1.c) / (A0.a * A0.a + A0.c * A0.c) );
    var j1_0 = Math.sqrt( (A1.b * A1.b + A1.d * A1.d) / (A0.b * A0.b + A0.d * A0.d) );
    
    var angle = Math.atan2(A0.d*A1.b - A0.b*A1.d, A0.b*A1.b + A0.d*A1.d);

    var timer;
    var MAX = +val("MAX");
    var i = 0;
    
    function update()
    {
        if( ++i == MAX )
            clearInterval(timer);
        
        var t = i/MAX;
        // the values of i and j at t are scaled by i and j at 0
        // in order to reuse easily A0's values, which include initial rotation
        var it = 1-t + t * i1_0;
        var jt = 1-t + t * j1_0;
        At = {
            a : A0.a * it,
            b : A0.b * jt,
            c : A0.c * it,
            d : A0.d * jt,
            tx : (1-t) * A0.tx + t * A1.tx,
            ty : (1-t) * A0.ty + t * A1.ty
        };
        //At.rotate( t * angle );
        At = rotate_mat(At, t * angle);
        
        // now apply matrix At instead of A0 or A1
        //obj.transform.matrix = At;
        apply("guineapig", At);
    }
    
    var step = +val("step");
    setTimeout(function(){timer=setInterval(update,step);},10*step);
};
<p>Matrix A0 :
    <label>a<input type="text" size="2" id ="a0" value="1" /></label>
    <label>b<input type="text" size="2" id ="b0" value="0" /></label>
    <label>c<input type="text" size="2" id ="c0" value="0" /></label>
    <label>d<input type="text" size="2" id ="d0" value="1" /></label>
    <label>tx<input type="text" size="2" id ="tx0" value="0" /></label>
    <label>ty<input type="text" size="2" id ="ty0" value="0" /></label>
</p>
<p>Matrix A1 :
    <label>a<input type="text" size="2" id ="a1" value="1.41421356237" /></label>
    <label>b<input type="text" size="2" id ="b1" value="1.41421356237" /></label>
    <label>c<input type="text" size="2" id ="c1" value="-1.41421356237" /></label>
    <label>d<input type="text" size="2" id ="d1" value="1.41421356237" /></label>
    <label>tx<input type="text" size="2" id ="tx1" value="40" /></label>
    <label>ty<input type="text" size="2" id ="ty1" value="40" /></label>
</p>
<p><label>frames<input type="text" size="2" id="MAX" value="100" /></label><label>interval (ms)<input type="text" size="2" id="step" value="10" /></label><button id="go">Animate !</button></p>
<div id="c" style="height:200px;width:200px;border:thin blue solid">
    <div id="guineapig" style="height:40px;width:40px;position:relative;top:80px;left:80px;background:green;" />
</div>

PS : You definitely need the homogeneous coordinates MvG mentions, otherwise you risk ending up with an interpolation movement that looks more like an arc of a spiral rather than a rotation and scaling along a linear movement.

Since Adobe's documentation is not very clear on that, and I know for a fact this happens with SVG transform matrices, here is how to compensate for that : apply the transformation to the x and y coordinates, and add the result to the translate values u and v, while setting x and y to 0. Thus, before you do anything :

correction = A0.transformPoint(new Point(x=obj.x, y=obj.y));
A0.tx += correction.x;
A0.ty += correction.y;

correction = A1.transformPoint(new Point(x=obj.x, y=obj.y));
A1.tx += correction.x;
A1.ty += correction.y;

obj.x = 0;
obj.y = 0;

Note that you can save these values x and y to undo this trick once your animation is done, if you want. But to be honest, once you have transformations, coordinates are superfluous.