The wrapping image game trick
By Muammar©解說 在電玩中所使用的讓 背景影像無休止的轉動之技巧
- Download EDs binary - 1.17 MB
- Download EDs source - 99.5 KB
- Download Endless Ocean binary - 3.54 MB
- Download Endless Ocean source - 95.6 KB
導言
你可曾想過電玩中的背景圖不斷的移動它到底有多長?
您必須注意到在某一時刻背景圖內容只是不斷的重複,這就是我們要學習這篇文章的重點.
兩天前, 我與我的孩子在公園散步時我注意到行人道上的磚塊排列方式是一個無止境的重複形狀,於是我決定用一個數學方法中來看這一問題。
幻覺
透過色彩與紋裡影像操作總是被使用在讓人產生錯覺的技法. 若要做出鋪磚錯覺效果來首先處理的第一個問題就是要讓圖片做出無縫連接 "tileable",以使其結束點能夠重複連接圖片產生連續的效果..
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.
現在,選擇圖片請確保包括兩個基本要求:
- 圖片的邊緣必須是 90° 角.
- 圖片邊緣不應該有光暈.
既然我們的圖片都準備好了,我們可平鋪就的方式有兩種選擇。 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:
- 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.
- 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!
背景
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".
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.
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.
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
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:
Collapse
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. Collapse
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.
Collapse
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((System.Drawing.RotateFlipType)Enum.Parse(
typeof(System.Drawing.RotateFlipType),
"Rotate90FlipNone", true));
if (e.KeyCode == Keys.Left)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(
typeof(System.Drawing.RotateFlipType),
"Rotate270FlipNone", true));
if (e.KeyCode == Keys.Down)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(
typeof(System.Drawing.RotateFlipType),
"Rotate180FlipNone", true));
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 thePictureBox
control to be passed again with the next tick. Collapse
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. Collapse
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. Collapse
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((System.Drawing.RotateFlipType)Enum.Parse(
typeof(System.Drawing.RotateFlipType),
"Rotate90FlipNone", true));
if (e.KeyCode == Keys.Left)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(
typeof(System.Drawing.RotateFlipType),
"Rotate270FlipNone", true));
if (e.KeyCode == Keys.Down)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(
typeof(System.Drawing.RotateFlipType),
"Rotate180FlipNone", true));
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:
Collapse
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.
Collapse
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.
Collapse
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. Collapse
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. Collapse
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. Collapse
private void tmr_MoveBG_Tick(object sender, EventArgs e)
{
...
Spaceship.Refresh();
}
沒有留言:
張貼留言