Pages

Tuesday, October 27, 2020

Per Pixel Collision Detection on Rotated and Scaled Sprites

This post about detecting collision between two rotated and/or scaled sprites where the sprites are part of a sprite sheet. I found lots of examples of detecting entire sprites but not so much for sprite sheets. This is an extension of my previous post about detecting if two sprites in sprite sheets collide.

For this to work you need to create a transformation matrix for each sprite. It is calculated by using the following formula: Matrix.CreateTranslation(new Vector3(-Origin, 0)) * Matrix.CreateScale(Scale) * Matrix.CreateRotationZ(Rotation) * Matrix.CreateTranslation(new Vector3(Position, 0)). Origin is calculated by (SourceRectangle.Width / 2, SourceRectangle.Height / 2).

The collision method takes six parameters: the source rectangle of sprite A, the transformation matrix of sprite A, the sprite sheet of sprite A, the source rectangle of sprite B, the transformation matrix of sprite B and the sprite sheet of sprite B. If a source rectangle is null the entire sprite sheet will be used.

It calls a second method that does the actual collision detection. It takes as parameters the source rectangle of sprite A, the transformation matrix of sprite A, the color data of sprite A, the source rectangle of sprite B, the transformation matrix of sprite B and the color data of sprite B.



/// <summary>
/// Checks if two sprites in a sprite sheet collide using per pixel collision detection
/// </summary>
/// <param name="sourceA">Nullable source rectangle of sprite A in the sprite sheet</param>
/// <param name="transformA">Transformation matrix for sprite A</param>
/// <param name="textureA">Texture for sprite sheet A</param>
/// <param name="transformB">Transformation matirx for sprite B</param>
/// <param name="sourceB">Nullable source rectangle of sprite B in the sprite sheet</param>
/// <param name="textureB">Texture for sprite sheet B</param>
/// <returns></returns>
public static bool SpriteSheetCollision(
    Rectangle? sourceA,
    Matrix transformA,
    Texture2D textureA,
    Rectangle? sourceB,
    Matrix transformB,
    Texture2D textureB)
{
    // If the sourceA is null use entire texture
    if (sourceA == null)
    {
        sourceA = new Rectangle(0, 0, textureA.Width, textureA.Height);
    }

    // Grab the texture data for checking if the pixels collide in the source rectangle
    Color[] textureDataA = new Color[sourceA.Value.Width * sourceA.Value.Height];
    textureA.GetData(0, sourceA, textureDataA, 0, textureDataA.Length);

    // If the sourceB is null use the entire texture as the source rectangle
    if (sourceB == null)
    {
        sourceB = new Rectangle(0, 0, textureB.Width, textureB.Height);
    }

    // Grab the texture data for checking if the pixels collide in the source rectangle
    Color[] textureDataB = new Color[sourceB.Value.Width * sourceB.Value.Height];
    textureB.GetData(0, sourceB, textureDataB, 0, textureDataB.Length);

    // Call the per pixel collision detection code
    return ColorDataCollides(sourceA.Value, transformA, textureDataA, sourceB.Value, transformB, textureDataB);
}

/// <summary>
/// Checks if two sprites collide using pixel perfect collision detection
/// where the sprites are rotated and/or scaled.
/// </summary>
/// <param name="sourceA">Source rectangle of sprite A in the sprite sheet</param>
/// <param name="transformA">The transformation matrix of sprite A</param>
/// <param name="textureDataA">The color data of sprite A</param>
/// <param name="sourceB">Source rectangle of sprite B in the sprite sheet</param>
/// <param name="transformB">The transformation matrix of sprite B</param>
/// <param name="textureDataB">The color data of sprite B</param>
/// <returns></returns>
private static bool ColorDataCollides(Rectangle sourceA, Matrix transformA, Color[] textureDataA, Rectangle sourceB, Matrix transformB, Color[] textureDataB)
{
    // Transformation of sprite A to sprite B
    Matrix mat1to2 = transformA * Matrix.Invert(transformB);

    // Loop over the source rectangle of sprite A
    for (int x1 = 0; x1 < sourceA.Width; x1++)
    {
        for (int y1 = 0; y1 < sourceA.Height; y1++)
        {
        
            // Calculate the position of the pixel in sprite A in sprite B
            Vector2 pos1 = new Vector2(x1, y1);
            Vector2 pos2 = Vector2.Transform(pos1, mat1to2);

            // Round to the nearest pixel
            int x2 = (int)Math.Round(pos2.X);
            int y2 = (int)Math.Round(pos2.Y);

            // Check to see if the pixel in sprite A is in the bounds of sprite B
            if ((x2 >= 0) && (x2 < sourceB.Width))
            {
                if ((y2 >= 0) && (y2 < sourceB.Height))
                {
                    // If the alpha channel of sprite A and sprite B is greater
                    // than zero we have a collision
                    if (textureDataA[x1 + y1 * sourceA.Width].A > 0)
                    {
                        if (textureDataB[x2 + y2 * sourceB.Width].A > 0)
                        {
                            return true;
                 }
                    }
                }
            }
        }
    }

    // No collision occurred
    return false;
}

The first method is similar to the previous method I posted the other day. It takes transformation matrices instead of destination rectangles. It uses the GetData method overload that takes a source rectangle. For starting position you need to use 0. If your texture has multiple levels you will want to repeat the testing for the different levels.

The second method does the actual collision detection. It calculates a matrix that is used to map the pixels in sprite A to the pixels in sprite B. That is done by multiplying the transformation matrix of sprite A by the inverse transformation of sprite B. I suggest reading up on matrix transformation if you're unclear about what is going on.

Next we loop over all of the pixels in sprite A and test if the collide with a pixel in sprite B. Inside the loops I create a vector for the current pixel in sprite A, or 1 in the code. I transform that vector next using the matrix we calculated earlier. I round the points to integers.

The next step is to check if the transformed pixel is inside the bounds of sprite B. If it is I compare the alpha channel of the two pixels. If their alpha channel are both greater than zero there is a collision and I return true. If a collision is not found I return false.

If you have questions leave a comment and I will answer them. I hope that you find the methods useful.

No comments:

Post a Comment