Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

The wrapping image game trick

0.00/5 (No votes)
25 Dec 2009 1  
Explaining the endless image wrapping trick used in video games.

EDs.JPG

Introduction

Ever wondered how long is the moving background picture of a video game? You must have noticed that at some point, it just keeps repeating, and this is what we are going to learn in this article.. Two days ago, I was in a park with my toddler when I noticed the way the footpath tiles are placed, making up an endless repeating shape, and that's when I decided to look up this problem in a mathematical approach.

The Illusion

hypnosquare.gif

Manipulating images through colors and textures has always been used in tricking our minds into giving an implied illusion. To make the tiling illusion, the first problem to tackle is to make a picture seamlessly "tileable" so that its ends fit to make up part of the overall picture by repeating part of it.. In other words, it's about placing a piece of texture next to each other without having a visible seam, and making sure not to have any eye-catching repeating spots in the overall picture.

It might be useful to say that not any picture will do, you need to have some sort of a repetitive pattern, at least at the edges of the picture. Now that the picture has been chosen, make sure you cover two basic requirements:

  1. The picture should be taken in a 90° angle.
  2. There shouldn't be much visible lightning scales in different parts of the picture.

Tip: To easily spot different lightning scales in a picture, try applying a gray-scale filter and then darken/lighten the different parts as required.

NYC.JPG

Now that our picture is ready, we have two options in the way we may tile it. We might want to tile it only in one dimension, i.e., horizontally/vertically so the picture fits in a row/column, or we could make it completely tileable by making sure that all ends match up. However, if you're intending to make a fully tileable picture, you'd better make it a perfect square of a size based on a power of 2 (i.e., 2^5 = 32), for two main reasons:

  1. Many commercial imaging filters (most notably clouds) will create already seamless tiles, but only if the original image is based on a power of 2.
  2. Powers of two are easy to manage and check when it comes to pictures, and even your monitor resolution is based on a power of two!

Background

To make the picture tileable, the first thing you need to do is to offset it horizontally and/or vertically, preferably by 50% of its original dimensions. This is best demonstrated by means of a picture. So in a picture like shown below, the creepy alien seems to be nudged over from the picture to reappear on the other side of it. "Sorry Bob".

BOBs.JPG

Now that we have made sure the edges match seamlessly, let's see how the endless wrapping works.. We need to look at the picture in a different way, recognizing the overall image as a beam of columns and rows of pixels, those when meet will make a perfect cylinder.

ShiftedClips.JPG

Now, to make an illusion of a picture moving to the right, we simply iterate through the columns of the picture, taking the most right column or columns; depending on how smooth the movement of the picture should be (the more columns you take out in a single movement, the less smoothness the movement of the picture will have), we take out that column(s), shift the rest of the picture to exactly cover the gap created by the taken column(s), and then we reattach those columns back to the picture, but to the gap created in the beginning of the picture when we moved (Clip1) to cover for the taken out columns (Clip2) in the first place. The following pictures show the movement technique by indexing the pixels of a sample picture of a 4X4.

Matrix.JPG

Notice the shift of the indexes in the blue to move the respective image from left to right, and the green one to give the illusion of a downward moving picture. Also notice that if more than one column is moved, the series still maintains to keep ordered in a contiguous manner.

Using the Code

Ocean.JPG

I'm going to explain only the endless ocean solution as it covers the wrapping trick for both dimensions.

Note that you can change the direction of both samples by holding down the Control key and pressing the respective Arrow key.

To move the picture, we first need to clip it as explained above, and here is the function to do it:

private Bitmap Crop(Bitmap srcBitmap, Rectangle rectClip)
{
    Bitmap tmpBmp = new Bitmap(rectClip.Width, rectClip.Height);
    Graphics g = Graphics.FromImage(tmpBmp);
 
    g.DrawImage(srcBitmap, 0, 0, rectClip, GraphicsUnit.Pixel);
    g.Dispose();
    return tmpBmp;
}

The function takes the source bitmap as its first parameter, and the rectangle to cut out, considering the start location of the clipping and the dimensions, then returns that part as a Bitmap object.

We also use some user controls inherited from the PictureBox class to support background transparency.

using System;
using System.Windows.Forms;
 
namespace UI_Test
{
    public class TransparentPictureBox : PictureBox
    {
        public TransparentPictureBox()
        {
            this.SetStyle(ControlStyles.Opaque, true);
            this.SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
        }
 
        protected override CreateParams CreateParams
        {
            get
            {
                CreateParams parms = base.CreateParams;
                parms.ExStyle |= 0x20; 
                return parms;
            }
        }
    }
}

Once compiled, you'll see the control added to your solution components list at the top of the toolbox pane with the label TransparentPictureBox.

Now, let's take a look at the entire code listing and discuss each part separately.

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Media;
 
namespace SampleGameII
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private enum Style
        {
            Left,
            Right,
            Up,
            Down
        }
 
        private void tmr_MoveBG_Tick(object sender, EventArgs e)
        {
            if (!chReverse.Checked)
                PIC1.Image = MoveImage((Bitmap)PIC1.Image.Clone(), iSpeed, sDirection);
            else
            {
                if (iSpeed >= Properties.Resources.Ocean.Width - iSpeed)
                    iSpeed = 3;
                else
                    iSpeed += 3;
                PIC1.Image = MoveImage(Properties.Resources.Ocean, iSpeed, sDirection);
            }
            Spaceship.Refresh();
        }
 
 
        private Bitmap Crop(Bitmap srcBitmap, Rectangle rectClip)
        {
            Bitmap tmpBmp = new Bitmap(rectClip.Width, rectClip.Height);
            Graphics g = Graphics.FromImage(tmpBmp);
 
            g.DrawImage(srcBitmap, 0, 0, rectClip, GraphicsUnit.Pixel);
            g.Dispose();
            return tmpBmp;
        }
 
        private Bitmap MoveImage(Bitmap srcBitmap, int iMargin, string sDirection)
        {
            Bitmap tmpBmp, Clip1, Clip2;
            tmpBmp = Clip1 = Clip2 = null;
 
            switch (sDirection)
            {
                case "Left":
                    {
                        tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                        Clip1 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(0, 0),
                                new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
                        Clip2 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(srcBitmap.Width - iMargin, 0),
                                new Size(iMargin, srcBitmap.Height)));
 
                        Graphics g = Graphics.FromImage(tmpBmp);
 
                        if (!chReverse.Checked)
                        {
                            g.DrawImage(Clip1, iMargin, 0, 
                               srcBitmap.Width - iMargin, srcBitmap.Height);
                            g.DrawImage(Clip2, 0, 0, iMargin, srcBitmap.Height);
                        }
                        else
                        {
                            g.DrawImage(Clip2, iMargin, 0, 
                               srcBitmap.Width - iMargin, srcBitmap.Height);
                            g.DrawImage(Clip1, 0, 0, iMargin, srcBitmap.Height);
                        }
 
                        g.Dispose();
 
                        break;
                    }
                case "Right":
                    {
                        tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                        Clip1 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(iMargin, 0),
                                new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
                        Clip2 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(0, 0),
                                new Size(iMargin, srcBitmap.Height)));
 
                        Graphics g = Graphics.FromImage(tmpBmp);
 
 
                        if (!chReverse.Checked)
                        {
                            g.DrawImage(Clip1, 0, 0, srcBitmap.Width - iMargin, 
                                        srcBitmap.Height);
                            g.DrawImage(Clip2, srcBitmap.Width - iMargin, 0, 
                                        iMargin, srcBitmap.Height);
                        }
                        else
                        {
                            g.DrawImage(Clip2, 0, 0, srcBitmap.Width - iMargin, 
                                        srcBitmap.Height);
                            g.DrawImage(Clip1, srcBitmap.Width - iMargin, 0, 
                                        iMargin, srcBitmap.Height);
                        }
                        g.Dispose();
 
                        break;
                    }
                case "Up":
                    {
                        tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                        Clip1 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(0, 0),
                                new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
                        Clip2 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(0, srcBitmap.Height - iMargin),
                                new Size(srcBitmap.Width, iMargin)));
 
                        Graphics g = Graphics.FromImage(tmpBmp);
 
 
                        if (!chReverse.Checked)
                        {
                            g.DrawImage(Clip1, 0, iMargin, srcBitmap.Width, 
                                        srcBitmap.Height - iMargin);
                            g.DrawImage(Clip2, 0, 0, srcBitmap.Width, iMargin);
                        }
                        else
                        {
                            g.DrawImage(Clip2, 0, iMargin, srcBitmap.Width, 
                                        srcBitmap.Height - iMargin);
                            g.DrawImage(Clip1, 0, 0, srcBitmap.Width, iMargin);
                        }
                        g.Dispose();
 
                        break;
                    }
                case "Down":
                    {
                        tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                        Clip1 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(0, iMargin),
                                new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
                        Clip2 = Crop(
                            srcBitmap,
                            new Rectangle(
                                new Point(0, 0),
                                new Size(srcBitmap.Width, iMargin)));
 
                        Graphics g = Graphics.FromImage(tmpBmp);
 
 
                        if (!chReverse.Checked)
                        {
                            g.DrawImage(Clip1, 0, 0, srcBitmap.Width, 
                                        srcBitmap.Height - iMargin);
                            g.DrawImage(Clip2, 0, srcBitmap.Height - iMargin, 
                                        srcBitmap.Width, iMargin);
                        }
                        else
                        {
                            g.DrawImage(Clip2, 0, 0, srcBitmap.Width, 
                                        srcBitmap.Height - iMargin);
                            g.DrawImage(Clip1, 0, srcBitmap.Height - iMargin, 
                                        srcBitmap.Width, iMargin);
                        }
                        g.Dispose();
 
                        break;
                    }
            }
 
            return tmpBmp;
        }
 
 
        int iSpeed;
        string sDirection;
 
        private void Form1_Load(object sender, EventArgs e)
        {
            sDirection = "Up";
            iSpeed = 3;
            tmr_MoveBG.Start();
        }
 
        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right
               || e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)
            {
                sDirection = e.KeyCode.ToString();
 
 
                iSpeed = 3;
                tmr_MoveBG.Start();
 
                Bitmap bm;
 
                    bm = new Bitmap(Properties.Resources.Spaceship2);
 
 
                if (e.KeyCode == Keys.Right)
                    bm.RotateFlip(RotateFlipType.Rotate90FlipNone);
                if (e.KeyCode == Keys.Left)
                    bm.RotateFlip(RotateFlipType.Rotate270FlipNone);
                if (e.KeyCode == Keys.Down)
                    bm.RotateFlip(RotateFlipType.Rotate180FlipNone);
                
                Spaceship.Image = bm;
            }
        }
 
        private void chReverse_CheckedChanged(object sender, EventArgs e)
        {
            if (!chReverse.Checked)
            {
                PIC1.Image = Properties.Resources.Ocean;
                tmr_MoveBG.Stop();
                iSpeed = 3;
                tmr_MoveBG.Start();
            }
        }
 
        private void tbSpeed_Scroll(object sender, EventArgs e)
        {
            tmr_MoveBG.Interval = 50 - (tbSpeed.Value * 5);
            lblSpeed.Text = "Speed: "+ (tbSpeed.Value+1).ToString();
        }
    }
}

The motion illusion is initiated and carried out using the tmr_MoveBG timer. The MoveImage function is called with every tick of the timer, passing a clone of the currently modified Image of the picture box and assigning it back to the PictureBox control to be passed again with the next tick.

private void tmr_MoveBG_Tick(object sender, EventArgs e)
{
    if (!chReverse.Checked)
        PIC1.Image = MoveImage((Bitmap)PIC1.Image.Clone(), iSpeed, sDirection);
    else
    {
        if (iSpeed >= Properties.Resources.Ocean.Width - iSpeed)
            iSpeed = 3;
        else
            iSpeed += 3;
        PIC1.Image = MoveImage(Properties.Resources.Ocean, iSpeed, sDirection);
    }
    Spaceship.Refresh();
}

The MoveImage function takes the source image as its first parameter as a Bitmap, along with the margin to clip and the desired direction to move to.

The direction parameter is evaluated through a switch case block to clip and redraw the right image, and then finally returns it to be set back to the PictureBox control, which will be taken again as a clone to be the new source image to pass to the function.

private Bitmap MoveImage(Bitmap srcBitmap, int iMargin, string sDirection)
{
    Bitmap tmpBmp, Clip1, Clip2;
    tmpBmp = Clip1 = Clip2 = null;
 
    switch (sDirection)
    {
        case "Left":
            {
                tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                Clip1 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(0, 0),
                        new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
                Clip2 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(srcBitmap.Width - iMargin, 0),
                        new Size(iMargin, srcBitmap.Height)));
 
                Graphics g = Graphics.FromImage(tmpBmp);
 
                if (!chReverse.Checked)
                {
                    g.DrawImage(Clip1, iMargin, 0, 
                          srcBitmap.Width - iMargin, srcBitmap.Height);
                    g.DrawImage(Clip2, 0, 0, iMargin, srcBitmap.Height);
                }
                else
                {
                    g.DrawImage(Clip2, iMargin, 0, 
                       srcBitmap.Width - iMargin, srcBitmap.Height);
                    g.DrawImage(Clip1, 0, 0, iMargin, srcBitmap.Height);
                }
 
                g.Dispose();
 
                break;
            }
        case "Right":
            {
                tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                Clip1 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(iMargin, 0),
                        new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
                Clip2 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(0, 0),
                        new Size(iMargin, srcBitmap.Height)));
 
                Graphics g = Graphics.FromImage(tmpBmp);
 
 
                if (!chReverse.Checked)
                {
                    g.DrawImage(Clip1, 0, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
                    g.DrawImage(Clip2, srcBitmap.Width - iMargin, 0, 
                                iMargin, srcBitmap.Height);
                }
                else
                {
                    g.DrawImage(Clip2, 0, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
                    g.DrawImage(Clip1, srcBitmap.Width - iMargin, 0, 
                                iMargin, srcBitmap.Height);
                }
                g.Dispose();
 
                break;
            }
        case "Up":
            {
                tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                Clip1 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(0, 0),
                        new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
                Clip2 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(0, srcBitmap.Height - iMargin),
                        new Size(srcBitmap.Width, iMargin)));
 
                Graphics g = Graphics.FromImage(tmpBmp);
 
 
                if (!chReverse.Checked)
                {
                    g.DrawImage(Clip1, 0, iMargin, srcBitmap.Width, 
                                srcBitmap.Height - iMargin);
                    g.DrawImage(Clip2, 0, 0, srcBitmap.Width, iMargin);
                }
                else
                {
                    g.DrawImage(Clip2, 0, iMargin, srcBitmap.Width, 
                                srcBitmap.Height - iMargin);
                    g.DrawImage(Clip1, 0, 0, srcBitmap.Width, iMargin);
                }
                g.Dispose();
 
                break;
            }
        case "Down":
            {
                tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
                Clip1 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(0, iMargin),
                        new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
                Clip2 = Crop(
                    srcBitmap,
                    new Rectangle(
                        new Point(0, 0),
                        new Size(srcBitmap.Width, iMargin)));
 
                Graphics g = Graphics.FromImage(tmpBmp);
 
 
                if (!chReverse.Checked)
                {
                    g.DrawImage(Clip1, 0, 0, srcBitmap.Width, srcBitmap.Height - iMargin);
                    g.DrawImage(Clip2, 0, srcBitmap.Height - iMargin, 
                                srcBitmap.Width, iMargin);
                }
                else
                {
                    g.DrawImage(Clip2, 0, 0, srcBitmap.Width, srcBitmap.Height - iMargin);
                    g.DrawImage(Clip1, 0, srcBitmap.Height - iMargin, 
                                srcBitmap.Width, iMargin);
                }
                g.Dispose();
 
                break;
            }
    }
 
 
    return tmpBmp;
}

In order to change the direction of the image, we capture the keyCode and assign it as a string to the sDirection variable and simply flip the spaceship image to match the new direction. Notice that the background image changes its direction as the sDirection variable value changes, for the timer is still running, and uses it to determine the moving direction.

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right
       || e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)
    {
        sDirection = e.KeyCode.ToString();
 
 
        iSpeed = 3;
        tmr_MoveBG.Start();
 
        Bitmap bm;
 
            bm = new Bitmap(Properties.Resources.Spaceship2);
 
 
        if (e.KeyCode == Keys.Right)
            bm.RotateFlip(RotateFlipType.Rotate90FlipNone);
        if (e.KeyCode == Keys.Left)
            bm.RotateFlip(RotateFlipType.Rotate270FlipNone);
        if (e.KeyCode == Keys.Down)
            bm.RotateFlip(RotateFlipType.Rotate180FlipNone);
        
        Spaceship.Image = bm;
    }
}

The reversed effect is decided by checking the "Reverse Effect" check box which simply restarts the moving timer to use the new settings:

private void chReverse_CheckedChanged(object sender, EventArgs e)
{
    if (!chReverse.Checked)
    {
        PIC1.Image = Properties.Resources.Ocean;
        tmr_MoveBG.Stop();
        iSpeed = 3;
        tmr_MoveBG.Start();
    }
}

The new setting for the reverse effect is handled by switching the placement of the clips so that the bigger clip (Clip 1) takes the place of the smaller one (Clip 2) and vice versa, and hence the moving stretch effect to fit the clips in the wrong sizes.

if (!chReverse.Checked)
{
    g.DrawImage(Clip1, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
    g.DrawImage(Clip2, 0, 0, iMargin, srcBitmap.Height);
}
else
{
    g.DrawImage(Clip2, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
    g.DrawImage(Clip1, 0, 0, iMargin, srcBitmap.Height);
}

Finally, we determine the speed of the movement by increasing/decreasing the interval value of the timer.

private void tbSpeed_Scroll(object sender, EventArgs e)
{
    tmr_MoveBG.Interval = 50 - (tbSpeed.Value * 5);
    lblSpeed.Text = "Speed: "+ (tbSpeed.Value+1).ToString();
}

Of course, we could have increased the number of columns taken out in a clip, sacrificing some of the smoothness by increasing the margin in the MoveImage function.

PIC1.Image = MoveImage((Bitmap)PIC1.Image.Clone(), iSpeed, sDirection);

Note that the iSpeed value is predetermined in this example in the form_load event along with the sDirection value.

private void Form1_Load(object sender, EventArgs e)
{
    sDirection = "Up";
    iSpeed = 3;
    tmr_MoveBG.Start();
}

Points of Interest

A major drawback of this technique is that the newly drawn image obscures the overlaying PictureBox image, and hence the timer keeps refreshing it with every tick, which generates this inconvenient flickering of the image.

private void tmr_MoveBG_Tick(object sender, EventArgs e)
{
    ...
    Spaceship.Refresh();
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here