MonoGame/FNA/XNA Tutorial: MeoMotion Character Animation (Vector Bone and Distortion Animator) Page 1

(Note: Newer source code with added feature support can be found here: >>NewMeoMotionCS<< )

(If you don't already have MeoMotion Animator - you can get it: here)
(PAGE 1, page 2, page 3)

GOAL: Make an efficient vector-based animation system which uses keyframed animations created in Alien Scribble Interactive's "MeoMotion" and show how you can optionally add support for making limbs point toward objects.
[NOTE: MeoMotion/MeoPlayer are sometimes updated and slight improvements may exist maintaining reverse compatibility]

On this page, we'll mostly be creating a version of SpriteBatch that can distort our sprites for nice animations.

[NOTE: MeoMotion files can contain multiple animations per character which we will want to program MeoMotor so we can simply say: Play("walk1",current_speed) or Play("run1",current_speed) or Play("Jump1",default_speed) for example]

Step 1) Create a new game project - I'll call mine MeoMotor. We'll start with changing game.cs a bit:

namespace MeoMotor
{    
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        KeyboardState kb, old_kb;


        #region C O N S T R U C T
        //-----------------------
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }
        #endregion


        #region I N I T
        //-------------
        protected override void Initialize()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            base.Initialize();
        }
        #endregion


        #region L O A D
        //-------------
        protected override void LoadContent()
        {
        }
        protected override void UnloadContent() { }
        #endregion


        #region U P D A T E
        //-----------------
        protected override void Update(GameTime gameTime)
        {
            kb = Keyboard.GetState();

            base.Update(gameTime);
        }
        #endregion


        #region D R A W
        //-------------
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            old_kb = kb;
            base.Draw(gameTime);
        }
        #endregion
    }
}

(I'm assuming you have a fairly good understanding of the above)

Step 2) I will start by adding a new class to the project called "input.cs"
(I like to do it this way lately but you can skip this step if you don't like doing it this way)
Here's the input class:

class Input
{
    public const ButtonState ButtonUp = ButtonState.Released;
    public const ButtonState ButtonDown = ButtonState.Pressed;
    private bool allow_escape = true;

    public bool KeyPressed(Keys key) { return (Game1.kb.IsKeyDown(key) && Game1.old_kb.IsKeyUp(key)); }
    public bool KeyDown(Keys key) { return Game1.kb.IsKeyDown(key); }
    public bool AltDown() { return (Game1.kb.IsKeyDown(Keys.RightAlt) || Game1.kb.IsKeyDown(Keys.LeftAlt)); }
    public bool ShiftDown() { return (Game1.kb.IsKeyDown(Keys.RightShift) || Game1.kb.IsKeyDown(Keys.LeftShift)); }
    public bool ControlDown() { return (Game1.kb.IsKeyDown(Keys.RightControl) || Game1.kb.IsKeyDown(Keys.LeftControl)); }        

    public bool LeftMouseClick() { if ((Game1.old_ms.LeftButton == ButtonUp) && (Game1.ms.LeftButton == ButtonDown)) return true; return false; }
    public bool LeftMouseDown() { if (Game1.ms.LeftButton == ButtonDown) return true; return false; }
    public bool LeftMouseUp() { if (Game1.ms.LeftButton == ButtonUp) return true; return false; }
    public bool RightMouseClick() { if ((Game1.old_ms.RightButton == ButtonUp) && (Game1.ms.RightButton == ButtonDown)) return true; return false; }
    public bool RightMouseDown() { if (Game1.ms.RightButton == ButtonDown) return true; return false; }
    public bool MiddleMouseClick() { if ((Game1.old_ms.MiddleButton == ButtonUp) && (Game1.ms.MiddleButton == ButtonDown)) return true; return false; }

    public bool EscapePressed()
    {
        if (allow_escape) {
            if ((Game1.kb.IsKeyDown(Keys.Escape) && Game1.old_kb.IsKeyUp(Keys.Escape))) { allow_escape = false; return true; }
            else return false;
        }
        else {
            if (Game1.kb.IsKeyUp(Keys.Escape)) allow_escape = true;
        }
        return false;
    }
}

You may see something like ButtonState underlined. Just right click it and click "Resolve" and then click "Using Microsoft.Xna.Framework.Input" and it will add the using statement for you. In order for this to work though, we'll need to change Game1.cs a bit.
(Note: The EscapePressed function is to prevent multiple escapes from happening in one loop (like if exiting from a sub-menu we don't want it to again completely exit from the main menu also))

So in Game1.cs we change kb and old_kb to static public so they can be accessed in input.cs
Here's the new Game1.cs:

public class Game1 : Microsoft.Xna.Framework.Game
{
    const int SCREENWIDTH = 800, SCREENHEIGHT = 600; //added
    const bool FULLSCREEN = false;                   //added
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    static public KeyboardState kb, old_kb;
    static public MouseState ms, old_ms;     //added 
    Input inp;                               //added


    #region C O N S T R U C T
    //-----------------------
    public Game1()
    {
        graphics = new GraphicsDeviceManager(this)
        {
            PreferredBackBufferWidth = SCREENWIDTH, PreferredBackBufferHeight = SCREENHEIGHT, IsFullScreen = FULLSCREEN, //added
            PreferredDepthStencilFormat = DepthFormat.Depth16                                                            //added
        };      
        Content.RootDirectory = "Content";
        inp = new Input(); // added
    }
    #endregion


    #region I N I T
    //-------------
    protected override void Initialize()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }
    #endregion


    #region L O A D
    //-------------
    protected override void LoadContent()
    { }
    protected override void UnloadContent() { }
    #endregion


    #region U P D A T E
    //-----------------
    protected override void Update(GameTime gameTime)
    {
        kb = Keyboard.GetState(); ms = Mouse.GetState(); // added
        if (inp.EscapePressed()) this.Exit(); // added

        base.Update(gameTime);
    }
    #endregion


    #region D R A W
    //-------------
    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.Black);

        old_kb = kb; old_ms = ms; // added
        base.Draw(gameTime);
    }
    #endregion
}

Step 3) We will need a sprite batch style class which can render distorted sprites acting much like spriteBatch. (it is preferable that most sprite images are on one texture) -- note that you can use the export-combine tool in MeoMotion to combine multiple character projects so you are using fewer(or only 1) sprite-sheets/textures. It will remember names of sub-sheets which can be used to distinguish between different characters.

Start by creating a new class called SpriteBatchDistort.cs

[note: again as you code this you may notice some underlines from intellisense which you can right click to resolve]

First we'll need some useful variables. Keep in mind when using an isometric view for 2D things can still have depth - only there is no perspective-based scaling (z-depth makes the coordinate closer to the center of the screen in 3D). I chose to have a default depth of sprites at 1f:

private const float default_depth = 1f;

Also we'll pass in the device during construction for easy access.

private GraphicsDevice device;

Also we'll need a boolean to determine if Begin has been called already. Anyway, here's the rest of the private variables I'm suggesting (I'll explain in the paragraph after if you want to know more about what they are for).
private bool beginCalled;
private float x1, y1, x2, y2, x3, y3, x4, y4, z, u1, v1, u2, v2;
private VertexPositionColorTexture[] vertices;
private VertexDeclaration vertexDeclaration;
private short[] indices;
private VertexBuffer vertexBuffer;
private IndexBuffer indexBuffer;
private Matrix wrld, view, proj, WorldViewProj, ViewProj;
private Rectangle tempRect = new Rectangle(0, 0, 0, 0); //used in setting texture coords (and flipping)        
private int vertex_count;        

x1,y1,...u1,v1... these will be used in calculating vertices to add to the vertex buffer (1 quad = 2 triangles = 4 verts)
vertices[] is the actual vertex list of a type that can hold position, color and texture-coords
vertexDeclaration basically holds the settings for what type of vertex information (position, color, normal, tex-coord) we're using.. just setting the variable = VertexPositionColorTexture.VertexDeclaration basically causes this to happen automatically (with magic).
indices[] (the index list - a list of what order the vertices should be selected for creating the triangles/quads -- this will be calculated once during construction and re-used [since the order will never change]).
vertexBuffer creates, stores and manages memory of vertex data to be moved and processed on the GPU/device(graphics card). (Same sort of concept for indexBuffer).
wrld - world matrix (effects final position and orientation of all existing vertices - good for shaking the screen or doing wierd effects (like 2.5D type things). NOTE: you will be able to change wrld with a SetWorld function we'll add later. view matrix (used to orient everything relative to camera using inverse of camera transform) - we'll just leave the view as an identity (we'll move the enivronment passed the camera with wrld or just by setting sprite coords). proj - transforms final vertices into a clip space -- I use a very generous -2000 to +2000 depth space and viewport width and hieght when creating the orthographic proj for 2D. The other 2 matrices here (WorldViewProj and ViewProj) just hold versions of these multiplied together(in the right order) so we don't need to repeat the calculation.
If we should fill the entire vertex buffer with 8192 vertices(which is an optimal size), then we "flush" (draw) those out and start filling the buffer again (thus we need vertex_count to keep track).

I find it good to have these publically accessable:

// PUBLIC
public Vector3 default_cam = new Vector3(400, 300, -720);
public Effect Shader = null;
public Effect fx = null; 
public Vector2 origin_default; //(set as 0,0 if you want to default like spritebatch and use corner for rotation)         
public Texture2D tex = null;
BlendState _blendState;
SamplerState _samplerState;
DepthStencilState _depthStencilState; //default is off    

default_cam is used with 3D projection if you switch to 3D. Shader will hold the default 2D shader. fx (when not null) will be used for other effects (like underwater distortion for example). origin_default can be used to modify default rotation behavior (if you don't specify an origin coordinate during a draw call). Note: origin_default goes from (0,0)-(1,1) [ie: 0.5, 0.5 is center] - normally however when specifying and origin, you'd use the actual pixel offsets on the bitmap from the rectangle's top-left corner.. tex holds the sprite sheet you're currently working with -- it is best to try as often as you can to minimize Begin and End calls and minimize texture switching, however its perfectly ok to do it (just if you do it very frequently it may slow things down a bit). This spriteBatch doesn't work like a normal one - the texture(tex) is assigned during Begin to encourage minimizing texture switching and avoid sorting entirely.
_blendState (set when calling Begin) is used to set blending modes such as NonPremultiplied (used most often), Opaque (fast for sprites that have no see-through parts), Additive (great for fire or magic effects), or AlphaBlend. _samplerState can be used to control how texture sampling is handled (ie: wrapping or clamping). _depthStencilState (off by default) is useful if you need to do something involving the depth stencil buffer.


Step 4) We need a constructor that creates an index buffer that winds vertices in the correct order (and won't change) and a dynamic vertex buffer we can place our sprites into. Also we'll need to load a default 2D shader .fx from the content pipeline. Here's the code of the default .fx (just download and right click and add the .fx file to your content pipeline: QuadEffect.fx)

#ifdef SM4 
// Macros for targetting shader model 4.0 (DX11) 
#define TECHNIQUE(name, vsname, psname ) \ 
  technique name { pass { VertexShader = compile vs_4_0_level_9_1 vsname (); PixelShader = compile ps_4_0_level_9_1 psname(); } }
#define BEGIN_CONSTANTS     cbuffer Parameters : register(b0) { 
#define MATRIX_CONSTANTS 
#define END_CONSTANTS       };  
#define _vs(r) 
#define _ps(r) 
#define _cb(r) 
#define DECLARE_TEXTURE(Name, index) \ 
  Texture2D<float4> Name : register(t##index); \ 
  sampler Name##Sampler : register(s##index) 
#define SAMPLE_TEXTURE(Name, texCoord)  Name.Sample(Name##Sampler, texCoord) 
#else 
// Macros for targetting shader model 2.0 (DX9) 
#define TECHNIQUE(name, vsname, psname ) technique name { pass { VertexShader = compile vs_2_0 vsname (); PixelShader = compile ps_2_0 psname(); } } 
#define BEGIN_CONSTANTS 
#define MATRIX_CONSTANTS 
#define END_CONSTANTS 
#define _vs(r)  : register(vs, r) 
#define _ps(r)  : register(ps, r) 
#define _cb(r) 
#define DECLARE_TEXTURE(Name, index) sampler2D Name : register(s##index);  
#define SAMPLE_TEXTURE(Name, texCoord)  tex2D(Name, texCoord) 
#endif 

DECLARE_TEXTURE(Texture, 0); 

BEGIN_CONSTANTS 
MATRIX_CONSTANTS 
float4x4 MatrixTransform    _vs(c0) _cb(c0); 
END_CONSTANTS 

struct VSOutput 
{ 
    float4 position        : SV_Position; 
    float4 color             : COLOR0; 
    float2 texCoord       : TEXCOORD0; 
};  
 
VSOutput SpriteVertexShader(float4 position    : SV_Position, 
                             float4 color    : COLOR0, 
                             float2 texCoord    : TEXCOORD0) 
{ 
    VSOutput output; 
    output.position = mul(position, MatrixTransform); 
    output.color = color; 
    output.texCoord = texCoord; 
    return output; 
} 
 
float4 SpritePixelShader(VSOutput input) : SV_Target0 
{     
     return SAMPLE_TEXTURE(Texture, input.texCoord) * input.color);      
     //return saturate(SAMPLE_TEXTURE(Texture, input.texCoord) * input.color*1.2);
} 
 
TECHNIQUE( SpriteBatch, SpriteVertexShader, SpritePixelShader ); 

The way this works, is Technique declares the names of the vertex shader and pixel shader functions to use and so any time a vertex is processed, the vertex shader is applied and any time a pixel is processed, the pixel shader is applied. All our vertex shader does is apply our wrld (world matrix) to the vertex (great for earthquakes, camera sweeps, scene rotation or whatever effect you like). The pixel shader actually shades the sprite(quad/triangle) by blending between the colors assigned to the 4 verts of the sprite. So if you wanted it to be just its normal solid color all 4 verts would be white but if you wanted one corner to be dark and the other to have a red tint, you'd set the bottom-left as dark and the top-right as a red color(when calling the draw function)-- NOTE that I commented out a multiply by 1.2 which you could use to brighten the colors when vertex color pixel-shading is being used(if you wanted)-- saturate actually just clips the color to the acceptable color range if it goes higher than 255 in each color channel). The #define's are just some helpful macro's which you could put in a seperate header .fxh file and include it in the .fx is you wanted (I found these somewhere but now I can't remember where).

So now back in SpriteBatchDistort.cs we will add a constructor that looks like this:

// C O N S T R U C T O R :
public SpriteBatchDistort(ContentManager content, GraphicsDevice graphics, bool CameraFor2D, string EffectFile, float camera_depth=-720.0f)
{                        
    default_cam = new Vector3(graphics.Viewport.Width / 2f, graphics.Viewport.Height / 2f, camera_depth);
    Shader = content.Load<Effect>(EffectFile); //default typical shader for this sort of thing 
    device = graphics;
    beginCalled = false;
            
    vertices = new VertexPositionColorTexture[8192]; //2048 sprites * 4 vertices per sprite IS 8192            
    vertexDeclaration = VertexPositionColorTexture.VertexDeclaration;

    // memorize the indices -- these will never change: 
    indices = new short[12288]; //2048 sprites * 6 inices per sprites IS 12288                       
    int c=0;
    for (int i = 0; i < 8192; i += 8) //2048 * 4 verts per sprite(0,1,2,3) so loop in increments of 4
    {
        indices[c] = (short)(i + 0); c++; indices[c] = (short)(i + 1); c++; indices[c] = (short)(i + 2); c++; 
        indices[c] = (short)(i + 2); c++; indices[c] = (short)(i + 3); c++; indices[c] = (short)(i + 0); c++;
        indices[c] = (short)(i + 4); c++; indices[c] = (short)(i + 5); c++; indices[c] = (short)(i + 6); c++;
        indices[c] = (short)(i + 6); c++; indices[c] = (short)(i + 7); c++; indices[c] = (short)(i + 4); c++; 
    }

    vertexBuffer = new DynamicVertexBuffer(graphics, typeof(VertexPositionColorTexture), 8192, BufferUsage.WriteOnly); //only plan to write to it
    indexBuffer = new IndexBuffer(graphics, typeof(short), 12288, BufferUsage.WriteOnly);
    indexBuffer.SetData(indices);
            
    view = Matrix.CreateLookAt(new Vector3(default_cam.X, default_cam.Y, default_cam.Z), new Vector3(default_cam.X, default_cam.Y, 0), Vector3.Down);                        
    proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, graphics.Viewport.AspectRatio, 0.1f, 2000.0f);                
    wrld = Matrix.Identity;
    WorldViewProj = ViewProj = wrld * view * proj;
            
    origin_default = new Vector2(0.5f, 0.5f); //rotate in middle of object (NOTE: if you send an origin by function call you must use actual pixel offset instead)
            
    if (CameraFor2D) Use2DCamera(); 
}  

(Note that default_cam is basically for when you switch to 3D) So we load the basic shader with content.Load using the name of the EffectFile passed into the constructor which we'll pass as "EffectQuad". I set the size for the vertex list to 8192 (fairly optimal - which is good for 2048 sprites per flush). Each sprite is 2 triangles which means a total of 6 vertices to process per quad(2 shared obviously). The index list optimizes the vertex processing - so * 6 means we need 12288 indices. I made it to create 2 quads(4 triangles) per loop(...just cuz). We generate a new vertex buffer (8192) and a new index buffer(12288) - as WriteOnly because that's all we plan to do with it.
The next bit of code probably isn't really necessary - it sets the default 3D camera which we change to 2D right after anyway.
The origin default is set to middle which I recommend leaving like that (good for scaling and rotating from center).
Next we'll make the 2D camera which is primarily what we should use for this class.

Step 5) Create a 2D camera. We need a projection matrix which will transform sprites to the screen coords favorably. We'll use CreateOrthographicOffCenter for this. I offset this by half a pixel to fix unwanted artifacts and make the pixels line up perfectly.

public void Use2DCamera()
{
    //near and far need to be huge for 3d world rotations to work 
    proj = Matrix.CreateOrthographicOffCenter(0.0f, device.Viewport.Width, device.Viewport.Height, 0.0f, -2000f, 2000.0f); 
    Matrix halfPixelOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0); proj = halfPixelOffset * proj; //<--fixes half-pixel offset problem
    view = Matrix.Identity;
    ViewProj = view * proj; WorldViewProj = wrld * ViewProj;            
}        
//note that default_cam is public and you can change it before making the call if you want:
public void Use3DCamera() {
    view = Matrix.CreateLookAt(default_cam, new Vector3(default_cam.X, default_cam.Y, 0), Vector3.Down);
    proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.1f, 500.0f);
    ViewProj = view * proj; WorldViewProj = WorldViewProj * ViewProj;            
}
        
public void SetDepth(float Depth)
{
    z = Depth;
}
public void SetEffect(Effect F_ect) { fx = F_ect; }
    
public void SetWorld(Matrix _world) { wrld = _world; }


        
//overload which simplifies the more extensive version (if you don't need vertex distortions and only using one color)
public void Draw(Rectangle sourceRect, Vector2 pos, Vector2 origin, Vector2 _scale, float rot, Color color, SpriteEffects flip = SpriteEffects.None)
{
    // I recommend replacing this call with a specialized version of the draw function(below) to reduce parameter passing/usage (better performance)
    Draw(sourceRect, pos, origin, _scale, rot, 0,0,0,0,0,0,0,0, color, color, color, color, flip);
}

Like I said before - in 2D we will leave the view matrix as identity and work with the world matrix to move the entire scene (if we ever want to do it that way). We might not use 3D camera - but here it is anyway... ;)
SetDepth can be used right before a draw call to draw to a particular depth value (useful if you didn't draw something in the correct order and you really need to move something behind [or in front] some layer). Note that we can chose to make it automatically reset the depth drawing to the default depth after a draw.
Later we may want to change the default fx for some special effect or alter the world matrix


Step 6) Let's make an extensive draw function that works like spriteBatch Draw except it is possible to pass coordinate offsets and individual colors for each vertex of the quad that makes up the sprite. Of course we'll need to create a Begin and End function later for this to work. It should accept the source rectangle (where we get image from on spritesheet), the position to draw on screen, the origin(null if centered), scale(null if 1,1), rotation, offsets of verts, colors of verts, and optionally alter the flip state to flip the image horizontally or vertically). Rotations and scaling should only be done if rotation!=0 and scale is something other thant 1,1 -- later we'll add an EndVerts for flushing vertices when we hit 8192 vertices in the list.

#region Extensive Draw Function 
// D R A W ------------------
public void Draw(Rectangle? sourceRect, Vector2 pos, Vector2? origin, Vector2? _scale, float rot, int x1off, int y1off, int x2off, int y2off, int x3off,
                             int y3off, int x4off, int y4off, Color c1, Color c2, Color c3, Color c4, SpriteEffects flip = SpriteEffects.None)
{
    if (!beginCalled) { Console.WriteLine("BEGIN not called before batch Draw. Draw aborted."); return; } 
    Vector2 scale = Vector2.One;
    float w, h;      // generate a TARGET WIDTH, HEIGHT based on the source (and scale it)
    float o_x, o_y;  // the origin for rotations also needs to be scaled            

    if (_scale.HasValue) { scale = _scale.Value; }
    if ((scale.X != 1) || (scale.Y != 1))
    {
        if (sourceRect.HasValue) { w = sourceRect.Value.Width * scale.X; h = sourceRect.Value.Height * scale.Y; }
        else { w = tex.Width * scale.X; h = tex.Height * scale.Y; } //default destination size is same size as entire texture

        if (origin.HasValue) { o_x = origin.Value.X * scale.X; o_y = origin.Value.Y * scale.Y; } 
        else { o_x = w * origin_default.X; o_y = h * origin_default.Y; } //Note: w and h are already scaled sizes

        //get scaled offsets:
        float xo1 = x1off * scale.X, yo1 = y1off * scale.Y; //scale the offsets too.. 
        float xo2 = x2off * scale.X, yo2 = y2off * scale.Y;
        float xo3 = x3off * scale.X, yo3 = y3off * scale.Y;
        float xo4 = x4off * scale.X, yo4 = y4off * scale.Y;
        //what is the point after offset
        x1 = pos.X + xo1; y1 = pos.Y + yo1;         //upper-left
        x2 = pos.X + w + xo2; y2 = pos.Y + yo2;     //upper-right
        x3 = pos.X + w + xo3; y3 = pos.Y + h + yo3; //lower-right
        x4 = pos.X + xo4; y4 = pos.Y + h + yo4;     //lower-left                
    }
    else //same with no scaling:
    {
        if (sourceRect.HasValue) { w = sourceRect.Value.Width; h = sourceRect.Value.Height; }
        else { w = tex.Width; h = tex.Height; } //default destination size is same size as entire texture
        if (origin.HasValue) { o_x = origin.Value.X; o_y = origin.Value.Y; } 
        else { o_x = w * origin_default.X; o_y = h * origin_default.Y; } //Note: w and h are already scaled sizes                        

        //need positions of destination                
        x1 = pos.X + x1off;     y1 = pos.Y + y1off;         //upper-left
        x2 = pos.X + w + x2off; y2 = pos.Y + y2off;         //upper-right
        x3 = pos.X + w + x3off; y3 = pos.Y + h + y3off;     //lower-right
        x4 = pos.X + x4off;     y4 = pos.Y + h + y4off;     //lower-left
    }
    if (rot != 0f)
    {
        float ox = pos.X + o_x, oy = pos.Y + o_y;
        float cos = (float)Math.Cos(rot), sin = (float)Math.Sin(rot); //this is actually quite fast on a modern computer
        float hd = x1 - ox, vd = y1 - oy;
        x1 = ox + hd * cos - vd * sin;
        y1 = oy + hd * sin + vd * cos;
        hd = x2 - ox; vd = y2 - oy;
        x2 = ox + hd * cos - vd * sin;
        y2 = oy + hd * sin + vd * cos;
        hd = x3 - ox; vd = y3 - oy;
        x3 = ox + hd * cos - vd * sin;
        y3 = oy + hd * sin + vd * cos;
        hd = x4 - ox; vd = y4 - oy;
        x4 = ox + hd * cos - vd * sin;
        y4 = oy + hd * sin + vd * cos;
    }

    if (sourceRect.HasValue) { tempRect = sourceRect.Value; } // (<--this should be the case. Unlikely to use this without a sourceRect)
    else { tempRect.X = 0; tempRect.Y = 0; tempRect.Width = tex.Width; tempRect.Height = tex.Height; }
    u1 = tempRect.X / (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
    v1 = tempRect.Y / (float)tex.Height;
    u2 = (tempRect.X + tempRect.Width) / (float)tex.Width;
    v2 = (tempRect.Y + tempRect.Height) / (float)tex.Height;
    if ((flip & SpriteEffects.FlipVertically) != 0)
    {
        var temp = v2; //BR_Y
        v2 = v1; v1 = temp;
    }
    if ((flip & SpriteEffects.FlipHorizontally) != 0)
    {
        var temp = u2; //BR_X
        u2 = u1; u1 = temp;
    }
    vertices[vertex_count].Position = new Vector3(x1, y1, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u1, v1);   //upper-left texCoord
    vertices[vertex_count].Color = c1; vertex_count++;
    vertices[vertex_count].Position = new Vector3(x2, y2, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u2, v1);   //upper-right texCoord
    vertices[vertex_count].Color = c2; vertex_count++;
    vertices[vertex_count].Position = new Vector3(x3, y3, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u2, v2);   //lower-right texCoord
    vertices[vertex_count].Color = c3; vertex_count++;
    vertices[vertex_count].Position = new Vector3(x4, y4, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u1, v2);   //lower-left texCoord
    vertices[vertex_count].Color = c4; vertex_count++;
    if ((vertex_count + 1) >= 8192)
    {
        EndVerts();
        beginCalled = true;
        vertex_count = 0;
    }
    z = default_depth; //return to default depth after use            
}//Draw (primary)
#endregion

First we made sure begin was called, apply scaling to width, height, origin coordinate, offsets, and update x1,y1,x2,y2,etc.... then we apply rotation(if any) [rotates horizontal, vertical offset of vertex around 0,0 and offsets it by the origin ox,oy] -- then we set the UV coords (if null then assume we use the entire texture) -- flip the UV coords if there is a flip effect. Lastly we record the new vertex data into each vertex of the quad (updating vertex_count along the way). If the count should exceed 8191(full) then we flush/draw the vertices (EndVerts) so we can re-use the vertex list. (I like to reset the z value after each draw - could use a boolean to control this behavior).


Step 7) For MeoMotion animations, we'll process the vertices a bit differently in the main animation class (which we'll make later). It will use a DrawTransformedVertices function here in SpriteBatchDistort.cs
It will take the image source rectangle, the "scene_origin" (which is where the character is in screen coordinates), 4 transformed points (p1,..,p4) and a color (c1) and option to flip the source image.

//  D R A W  T R A N S F O R M E D  V E R T I C E S 
//  (assumes vertices are already transformed)
public void DrawTransformedVertices(Rectangle? sourceRect, Vector2 scene_origin, Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, 
                                                               Color c1, SpriteEffects flip = SpriteEffects.None)
{
    if (!beginCalled) { Console.WriteLine("BEGIN not called before Batch Draw. Draw aborted."); return; } 
    p1 = (p1 + scene_origin);
    p2 = (p2 + scene_origin);
    p3 = (p3 + scene_origin);
    p4 = (p4 + scene_origin);
            
    if (sourceRect.HasValue) { tempRect = sourceRect.Value; } // (<--this should be the case. Unlikely to use this without a sourceRect)
    else { tempRect.X = 0; tempRect.Y = 0; tempRect.Width = tex.Width; tempRect.Height = tex.Height; }
    u1 = tempRect.X / (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
    v1 = tempRect.Y / (float)tex.Height;
    u2 = (tempRect.X + tempRect.Width) / (float)tex.Width;
    v2 = (tempRect.Y + tempRect.Height) / (float)tex.Height;
    if ((flip & SpriteEffects.FlipVertically) != 0)
    {
        var temp = v2; //BR_Y
        v2 = v1; v1 = temp;
    }
    if ((flip & SpriteEffects.FlipHorizontally) != 0)
    {
        var temp = u2; //BR_X
        u2 = u1; u1 = temp;
    }
    vertices[vertex_count].Position = new Vector3(p1, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u1, v1);   //upper-left texCoord
    vertices[vertex_count].Color = c1; vertex_count++;
    vertices[vertex_count].Position = new Vector3(p2, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u2, v1);   //upper-right texCoord
    vertices[vertex_count].Color = c1; vertex_count++;
    vertices[vertex_count].Position = new Vector3(p3, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u2, v2);   //lower-right texCoord
    vertices[vertex_count].Color = c1; vertex_count++;
    vertices[vertex_count].Position = new Vector3(p4, z);
    vertices[vertex_count].TextureCoordinate = new Vector2(u1, v2);   //lower-left texCoord
    vertices[vertex_count].Color = c1; vertex_count++;
    if ((vertex_count + 1) >= 8192)
    {
        EndVerts();
        beginCalled = true;
        vertex_count = 0;
    }
    z = default_depth; //return to default depth after use            
}//DrawTransformedVertices

So first it offsets all the coordinates relative to where the character is on the screen. Then it optionally flips the image. Then it simply records the new vertices and flushes(draw and reset) if necessary.


Step 8) Like I mentioned, we still need a Begin call to set up our states (like blend mode) and assign a texture (sprite sheet).

public void Begin(Texture2D texture, BlendState blendState)
{
    Begin(texture, blendState, SamplerState.LinearClamp, DepthStencilState.None, Matrix.Identity);
}
        
// B E G I N -------------------------------------
public void Begin(Texture2D texture, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Matrix world)
{
    if (texture == null) { Console.WriteLine("Batch Begin aborted because texture == null"); return; }
    if (beginCalled) { Console.WriteLine("Batch Begin already called."); return; }
    tex = texture;
    wrld = world;
    _blendState = blendState ?? BlendState.AlphaBlend; // ?? means set it to default if it is NULL
    _samplerState = samplerState ?? SamplerState.LinearClamp;
    _depthStencilState = depthStencilState ?? DepthStencilState.None; // note not doing anything with rasterizerstate as verts will always go 
                                                                                                                    // clockwise and won't make any difference            
    vertex_count = 0; 
    beginCalled = true;
}

We'll mostly use the overload version which only needs the texture and blendstate. Otherwise you could optionally update the world matrix, and set a different samplerState, or make use of depthStencilState.


Step 9) We need a function (EndVerts) to flush/draw the vertex list when it becomes too full and a regular End function which basically does the same thing (except begin called becomes false again). It is possible the states have changed so a setup function will reset them. Then we figure out the triangle count to use in DrawUserIndexedPrimitives. Also we need to call vertexBuffer.SetData to tell it where the vertices are to add to the vertexBuffer, where they start in the array and where they end (0,vertex_count).. We send the updated matrix to the vertex shader, apply a new pixel shader if there is one, assign the texture, and quickly draw out all the vertices in the vertexBuffer.

//E N D -----------------------------------------        
public void End()
{
    if (!beginCalled) { Console.WriteLine("call to END without begin called. Aborting End."); return; }
    if (vertex_count >= 3)
    { //nothing to draw
        Setup();
        //Draw entire vertex list:                    
        int triangle_count = vertex_count / 2; //int triangle_count = sprite_count * 2;                
        vertexBuffer.SetData(vertices, 0, vertex_count);
        device.SetVertexBuffer(vertexBuffer);
        WorldViewProj = ViewProj = wrld * view * proj; Shader.Parameters["MatrixTransform"].SetValue(WorldViewProj);
        Shader.CurrentTechnique.Passes[0].Apply(); //<--this must be applied first to get the vertex shader / world matrix transformation
        if (fx != null) fx.CurrentTechnique.Passes[0].Apply(); //this will simply modify the pixel shader used (be sure it multiplies by vertex color internally)                
        device.Textures[0] = tex;
        device.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, vertices, 0, vertex_count, indices, 0, triangle_count);
    }            
    beginCalled = false;
}

public void EndVerts()
{
    if (!beginCalled) { Console.WriteLine("call to END without begin called. Aborting End."); return; }
    beginCalled = false;
    if (vertex_count < 3) return;
    Setup();
    //Draw entire vertex list:                
    int triangle_count = vertex_count / 2;//sprite_count * 2;            
    vertexBuffer.SetData(vertices, 0, vertex_count);
    device.SetVertexBuffer(vertexBuffer); //indexBuffer.SetData(indices,0,index_count);            
    device.Textures[0] = tex;
    WorldViewProj = ViewProj = wrld * view * proj; Shader.Parameters["MatrixTransform"].SetValue(WorldViewProj);
    Shader.CurrentTechnique.Passes[0].Apply(); //<--this must be applied first to get the vertex shader / world matrix transformation
    if (fx != null) fx.CurrentTechnique.Passes[0].Apply(); //this will simply modify the pixel shader used (be sure it multiplies by vertex color internally)                
    device.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, vertices, 0, vertex_count, indices, 0, triangle_count);
}

private void Setup()
{
    device.BlendState = _blendState;
    device.DepthStencilState = _depthStencilState;
    device.SamplerStates[0] = _samplerState;
    if (device.RasterizerState.CullMode != CullMode.None) //don't bother unless necessary
    {
        RasterizerState rs = new RasterizerState();
        rs.CullMode = CullMode.None;
        device.RasterizerState = rs; //device state change requires new instances of RasterizerState...
    }
}

EndVerts does the same except it does not reset beginCalled which is important. So that's it for SpriteBatchDistort.cs (unless you want to add more specialized code (like draw overloads) which you may do later).

You may be wondering why I didn't add sort-by-texture, or front_to_back or others -- it is actually more optimal if programmers simply make sure their sprite sheets are arranged for layer of ordering. Since we're using a lot of alpha layering in 2D games, it is probably best to use a technique like this for example:
1) --- have backgrounds on a sheet(s) which you would render in your first begin-end pair (can also use render targets here too)
2) --- apply effects to first half of layering as needed
3) --- in a new begin-end pair - render character animations using the fewest number of spritesheets possible in a middle layer (again - can add effects) -- each time you must change a sheet, use a new begin-end pair.
4) --- render foreground layers using spritesheets (effects + final effects)

>>> Click HERE to go to PAGE 2 of MeoMotor tutorial <<<