For my current project, we are using a custom scripting language (God knows why we're doing that) for game development. Sparing the details, the engine basically interprets and exports as either Flash or iOS.
So with this project, I was tasked with creating a Spine library for assistance with animation. For the most part, it isn't too difficult, as our engine is similar enough to AS3 that I can just translate it over.
Main issue I'm having now is rendering. The creators of this scripting language decided to use exclusively an affine coordinate space system to render positions and such. I've tried to wrap my head around it, but without much knowledge to how it works, I'm struggling to figure it out. All I need to be able to do is manually set the position by x and y, and manually set the rotation by angle. Any help would be immensely appreciated.
At any rate, here is my code to render the actual spine library stuff (this is based off of the SkeletonSprite.as class from the AS3 library):
package engine.spine.render;
//These have the same functionality as the Spine2d library for AS3
import engine.spine.IGBone;
import engine.spine.IGSkeleton;
import engine.spine.IGSkeletonData;
import engine.spine.IGSlot;
import engine.spine.atlas.IGAtlasRegion;
import engine.spine.attachments.IGRegionAttachment;
//This stuff is our engine libraries
import engine.graphics.*;
import engine.gui.*;
import engine.math.*;
import engine.tween.*;
import engine.util.IGFSM;
import engine.IGApplication;
public class IGSpineWidget extends IGUIWidget
{
private var m_skeleton : IGSkeleton;
private var m_lastTime : int;
private var m_image : IGImage;
private var m_wrappers : Map<IGRegionAttachment, IGSpinePosition> = new Map<IGRegionAttachment, IGSpinePosition>(); // Works the same way as a Dictionary in AS3
public function IGSpineWidget (skeletonData : IGSkeletonData, image : IGImage)
{
IGBone.yDown = true;
m_skeleton = new IGSkeleton(skeletonData);
m_skeleton.updateWorldTransform();
m_image = image;
}
protected override function render(g : IGGraphics) : void
{
m_skeleton.update(IGApplication.delta_time) ;
var drawOrder : Vector<IGSlot> = skeleton.drawOrder;
for (var i : int = 0; i < drawOrder.length; i++)
{
g.push();
{
var slot : IGSlot = drawOrder[i];
if (slot.attachment != null && slot.attachment.type != IGRegionAttachment.Region()) { continue; }
var regionAttachment : IGRegionAttachment = IGRegionAttachment(slot.attachment);
var wrapper : IGSpinePosition = m_wrappers[regionAttachment];
if(regionAttachment != null) {
if (wrapper == null)
{
wrapper = new IGSpinePosition();
var region : IGAtlasRegion = IGAtlasRegion(regionAttachment.rendererObject);
var regionHeight : double = region.rotate ? region.width : region.height;
var regionWidth : double = region.rotate ? region.height : region.width;
wrapper.x = region.x;
wrapper.y = region.y;
wrapper.height = regionHeight;
wrapper.width = regionWidth;
// Rotate and scale using default registration point (top left corner, y-down, cw) instead of image center
wrapper.affine.rotate(regionAttachment.rotation * Math.PI / 180); //This rotates the position of the drawn object, but I need to be able to set the actual rotation instead of translating a rotation
wrapper.affine.scale(regionAttachment.scaleX * (regionAttachment.width / region.width), regionAttachment.scaleY * (regionAttachment.height / region.height));
// Position using attachment translation, shifted as if scale and rotation were at image center
var radians : double = -regionAttachment.rotation * Math.PI / 180;
var cos : double = Math.cos(radians);
var sin : double = Math.sin(radians);
var shiftX : double = -regionAttachment.width / 2 * regionAttachment.scaleX;
var shiftY : double = -regionAttachment.height / 2 * regionAttachment.scaleY;
if (region.rotate)
{
wrapper.affine.rotate(90); // Again, I need to += the rotation by 90 degrees, but I dont have that functionality
shiftX += regionHeight * (regionAttachment.width / region.width);
}
wrapper.affine.translate(0, 0);//(regionAttachment.x + shiftX * cos - shiftY * sin, -regionAttachment.y + shiftX * sin + shiftY * cos);
m_wrappers[regionAttachment] = wrapper;
}
var bone : IGBone = slot.bone;
var flipX : int = skeleton.flipX ? -1 : 1;
var flipY : int = skeleton.flipY ? -1 : 1;
//This is the key part. I need to be able to set the wrapper's affine2d's x and y position, the the rotation by angle (ie. rotation = someAngle)
//wrapper.affine.translate(bone.worldX, bone.worldY);
//wrapper.affine.rotate(bone.worldRotationX * flipX * flipY);
wrapper.scaleX = bone.worldScaleX * flipX;
wrapper.scaleY = bone.worldScaleY * flipY;
g.scale(wrapper.scaleX/4, wrapper.scaleY/4);
g.translate(1000, 1600); // Set the position of the widget
g.rotate(bone.worldRotationX * flipX * flipY);
g.multTransform(wrapper.affine);
g.drawSubImage(m_image, wrapper.x, wrapper.y, wrapper.width, wrapper.height, 0, 0, wrapper.width, wrapper.height);
}
}
g.pop();
}
}
public function get skeleton () : IGSkeleton {
return m_skeleton;
}
}
And this is the Affine2d class:
package engine.math;
public class IGAffine2D
{
/////////////////////////////////////////////////////////////////////
// State
/////////////////////////////////////////////////////////////////////
public var m0:double;
public var m1:double;
public var m2:double;
public var m3:double;
public var m4:double;
public var m5:double;
/////////////////////////////////////////////////////////////////////
// Construction and Initialsation
/////////////////////////////////////////////////////////////////////
public final function IGAffine2D()
{
this.init();
}
public final function isIdentity() : bool
{
return m0 == 1 && m2 == 0 && m4 == 0 &&
m1 == 0 && m3 == 1 && m5 == 0;
}
public final function init() : IGAffine2D
{
m0 = 1.0;
m1 = 0.0;
m2 = 0.0;
m3 = 1.0;
m4 = 0.0;
m5 = 0.0;
return this;
}
public final function initColumnMajor(
a0 : double, a1 : double, a2 : double,
a3 : double, a4 : double, a5 : double) : IGAffine2D {
// represents the following 3x3 (2d affine) matrix
// m0 m2 m4
// m1 m3 m5
// 0 0 1
m0 = a0;
m1 = a1;
m2 = a2;
m3 = a3;
m4 = a4;
m5 = a5;
return this;
}
public final function initFromTransform(transform:IGAffine2D) : IGAffine2D
{
m0 = transform.m0;
m1 = transform.m1;
m2 = transform.m2;
m3 = transform.m3;
m4 = transform.m4;
m5 = transform.m5;
return this;
}
public final function initWithInverseFromTransform(other : IGAffine2D) : IGAffine2D
{
// this is 65 ops
var a : double= other.m0;
var b : double= other.m2;
var c : double= other.m1;
var d : double= other.m3;
var det_inv : double = 1.0 / (a*d - b*c);
m0 = d * det_inv;
m1 = -c * det_inv;
m2 = -b * det_inv;
m3 = a * det_inv;
var x : double= other.m4;
var y : double= other.m5;
m4 = -(x * m0 + y * m2);
m5 = -(x * m1 + y * m3);
return this;
}
//////////////////////////////////////////////////////////////////////
// Getting Properties of the Transform
//////////////////////////////////////////////////////////////////////
public final function get translate_x() : double
{
return m4;
}
public final function get translate_y() : double
{
return m5;
}
public final function get scale_x() : double
{
return m0;
}
public final function get scale_y() : double
{
return m3;
}
/**
* Determines whether or not the translation applied by this matrix
* will result in the coordinate being integer bound for a point
* around the origin
**/
public final function isIntegerTranslate() : bool
{
var tx : int = m4;
var ty : int = m5;
if (m0 == 1 && m1 == 0 && m2 == 0 && m3 == 1 && m4 == tx && m5 == ty) {
return true;
}
return false;
}
//////////////////////////////////////////////////////////////////////
// Performing Transformations
//////////////////////////////////////////////////////////////////////
public final function translate(dx:double, dy:double): void
{
m4 += (m0 * dx) + (m2 * dy);
m5 += (m1 * dx) + (m3 * dy);
}
public final function rotate (theta: double) : void
{
if(theta == 0) {
return;
}
var st :double = Math.sin(theta);
var ct :double = Math.cos(theta);
var r00 :double = (m0 * ct) + (m2 * st);
var r01 :double = (m0 * -st) + (m2 * ct);
var r02 :double = m4;
var r10 :double = (m1 * ct) + (m3 * st);
var r11 :double = (m1 * -st) + (m3 * ct);
var r12 :double = m5;
m0 = r00;
m2 = r01;
m4 = r02;
m1 = r10;
m3 = r11;
m5 = r12;
}
public final function scale(sx : double, sy : double) : void
{
m0 *= sx;
m1 *= sx;
m2 *= sy;
m3 *= sy;
}
// [m00 m01 m02] [1 shx 0]
// [m10 m11 m12] [shy 1 0]
// [ 0 0 1] [0 0 1]
public final function shear(shx:double, shy:double) : void
{
var r00 : double = m0 + m2 * shy;
var r01 : double = m0 * shx + m2;
var r02 : double = m4;
var r10 : double = m1 + m3 * shy;
var r11 : double = m1 * shx + m3;
var r12 : double = m5;
m0 = r00;
m2 = r01;
m4 = r02;
m1 = r10;
m3 = r11;
m5 = r12;
}
/**
* Performs a Translate/Scale/Rotate transformation around a pivot point.
* OPTIMIZED VERSION OF:
* - translate(dx + px, dy + py);
* - rotate(theta);
* - scale(sx, sy);
* - translate(-px, -py);
**/
public final function TRS(dx : double, dy : double,
px : double, py : double, // positive pivot point
sx : double, sy : double, theta : double): IGAffine2D
{
// early abort for standard case
if (sx == 1.0 && sy == 1.0 && theta == 0) {
m4 += dx;
m5 += dy;
return this;
}
var l0 : double = m0;
var l1 : double = m1;
var l2 : double = m2;
var l3 : double = m3;
var l4 : double = m4 + (dx + px)*l0 + (dy + py)*l2;
var l5 : double = m5 + (dx + px)*l1 + (dy + py)*l3;
if (theta != 0)
{
var s : double = -Math.sin(theta);
var c : double = Math.cos(theta);
var r0 : double = c*sx;
var r1 : double = -s*sy;
var r2 : double = s*sx;
var r3 : double = c*sy;
m0 = l0 * r0 + l2 * r1;
m1 = l1 * r0 + l3 * r1;
m2 = l0 * r2 + l2 * r3;
m3 = l1 * r2 + l3 * r3;
m4 = l4 - m0 * px - m2 * py;
m5 = l5 - m1 * px - m3 * py;
}
else
{
m0 = l0 * sx ;
m1 = l1 * sx ;
m2 = l2 * sy;
m3 = l3 * sy;
m4 = l4 - (m0 * px) - (m2 * py);
m5 = l5 - (m1 * px) - (m3 * py);
}
return this;
}
public final function initFromTransformWithOffset(other : IGAffine2D, dx : double, dy : double) : IGAffine2D
{
m0 = other.m0;
m1 = other.m1;
m2 = other.m2;
m3 = other.m3;
m4 = other.m4 + dx * m0 + dy * m2;
m5 = other.m5 + dx * m1 + dy * m3;
return this;
}
public final function initFromTransformWithTRS(
other : IGAffine2D,
dx : double, dy : double,
px : double, py : double, // positive pivot point
sx : double, sy : double, theta : double): IGAffine2D
{
var s :double = 0;
var c :double = 1;
if (theta != 0)
{
s = -Math.sin(theta);
c = Math.cos(theta);
}
var l0 : double = other.m0;
var l1 : double = other.m1;
var l2 : double = other.m2;
var l3 : double = other.m3;
var l4 : double = other.m4 + (dx + px)*l0 + (dy + py)*l2;
var l5 : double = other.m5 + (dx + px)*l1 + (dy + py)*l3;
var r0 : double = c*sx;
var r1 : double = -s*sy;
var r2 : double = s*sx;
var r3 : double = c*sy;
m0 = l0 * r0 + l2 * r1;
m1 = l1 * r0 + l3 * r1;
m2 = l0 * r2 + l2 * r3;
m3 = l1 * r2 + l3 * r3;
m4 = l4 - m0 * px - m2 * py;
m5 = l5 - m1 * px - m3 * py;
return this;
}
//////////////////////////////////////////////////////////////////////
// Performing Other Operations
//////////////////////////////////////////////////////////////////////
public final function invert(): IGAffine2D
{
var a : double= m0;
var b : double= m2;
var c : double= m1;
var d : double= m3;
var det_inv : double = 1.0 / (a*d - b*c);
m0 = d * det_inv;
m1 = -c * det_inv;
m2 = -b * det_inv;
m3 = a * det_inv;
var x : double= m4;
var y : double= m5;
m4 = -x * m0 + -y * m2;
m5 = -x * m1 + -y * m3;
return this;
}
public final function concat(t : IGAffine2D): IGAffine2D
{
var r00 : double = (m0 * t.m0) + (m2 * t.m1);
var r01 : double = (m0 * t.m2) + (m2 * t.m3);
var r02 : double = (m0 * t.m4) + (m2 * t.m5) + m4;
var r10 : double = (m1 * t.m0) + (m3 * t.m1);
var r11 : double = (m1 * t.m2) + (m3 * t.m3);
var r12 : double = (m1 * t.m4) + (m3 * t.m5) + m5;
m0 = r00;
m2 = r01;
m4 = r02;
m1 = r10;
m3 = r11;
m5 = r12;
return this;
}
//////////////////////////////////////////////////////////////////////
//
//////////////////////////////////////////////////////////////////////
public final function transformVector2(pin : IGVector2, pout : IGVector2): void
{
var x:double = pin.x * m0 + pin.y * m2 + m4;
var y:double = pin.x * m1 + pin.y * m3 + m5;
pout.x = x;
pout.y = y;
}
public final function debug() : String
{
return m0 + " \t" + m2 + "\t" + m4 + "\n" + m1 + "\t" + m3 + "\t" + m5;
}
}
I've figured it out. On each call that I need to redraw based on position, I reset the affine coordinates, then translate and rotate once to the correct position.