65.9K
CodeProject 正在变化。 阅读更多。
Home

游戏中的图像包装技巧

2009 年 12 月 24 日

CPOL

6分钟阅读

viewsIcon

59022

downloadIcon

2269

解释视频游戏中使用的无尽图片循环技巧。

EDs.JPG

引言

你是否曾想过视频游戏中不断移动的背景图片到底有多长?你一定注意到在某个时候,它会不断重复,这正是我们在本文中要学习的内容。两天前,我和我的孩子在公园里,注意到人行道砖块的铺设方式,形成了一个无限重复的形状,那时我决定从数学方法来研究这个问题。

错觉

hypnosquare.gif

通过颜色和纹理来操控图像一直是用来欺骗我们的大脑,从而产生一种隐含的错觉。为了制造平铺的错觉,首先要解决的问题是让一张图片无缝地“平铺”,使其边缘能够吻合,通过重复其中的一部分来构成整体的图像。换句话说,就是在不留下可见接缝的情况下,将一块纹理放在另一块旁边,并确保整体图像中没有引人注目的重复点。

值得一提的是,并非所有图片都适用,你需要有一些重复的模式,至少在图片的边缘。现在图片已经选好,请确保你满足两个基本要求:

  1. 图片应以 90° 角拍摄。
  2. 图片的不同部分不应有明显的光照差异。

提示:为了轻松识别图片中不同的光照差异,可以尝试应用灰度滤镜,然后根据需要调整不同部分的亮度/暗度。

NYC.JPG

现在我们的图片已经准备好,在平铺时我们有两种选择。我们可以选择只在一维上平铺,即水平/垂直平铺,这样图片就能形成一行/一列,或者通过确保所有边缘都匹配来使其完全可以平铺。然而,如果你打算制作一张完全可以平铺的图片,最好将其制作成一个基于 2 的幂(即 2^5 = 32)的完美正方形,原因主要有两点:

  1. 许多商业图像滤镜(尤其是云彩滤镜)可以创建无缝的平铺,但前提是原始图像必须基于 2 的幂。
  2. 2 的幂次方在处理图片时易于管理和检查,甚至你的显示器分辨率也是基于 2 的幂!

背景

为了使图片可以平铺,你需要做的第一件事是将其水平和/或垂直偏移,最好是其原始尺寸的 50%。这最好通过图片来演示。例如,在下面这张图片中,恐怖的外星人似乎从图片的一侧移出,并在另一侧重新出现。“抱歉,鲍勃”。

BOBs.JPG

既然我们已经确保边缘无缝匹配,让我们来看看无尽循环是如何工作的……我们需要以不同的方式看待这张图片,将整个图像看作是由像素列和行组成的,这些像素相遇时会形成一个完美的圆柱体。

ShiftedClips.JPG

现在,为了产生向右移动的图片效果,我们只需遍历图片的列,取出最右边的一列或几列;这取决于移动的平滑度(一次移动的列越多,图片移动的平滑度就越低),然后取出这些列,将剩余的图片向左移动以完全填补取出的列造成的空隙,然后我们将取出的列重新附加到图片的开头,填补之前为了覆盖取出的列(Clip2)而移动(Clip1)所造成的空隙。接下来的图片通过对一个 4X4 的样本图片的像素进行索引来展示移动技术。

Matrix.JPG

请注意蓝色区域索引的移动,以将相应图像从左向右移动,绿色区域索引则用于产生向下移动的图片的错觉。同时请注意,如果移动了多于一列,序列仍然保持连续排列。

Using the Code

Ocean.JPG

我将只解释 Endless Ocean 的解决方案,因为它涵盖了两个维度的循环技巧。

请注意,您可以通过按住 Control 键并按下相应的箭头键来更改两个样本的方向。

为了移动图片,我们首先需要按照上述说明进行裁剪,这是执行此操作的函数:

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;
}

该函数将源位图作为第一个参数,并将指定的矩形区域剪切出来,考虑到裁剪的起始位置和尺寸,然后将该部分作为 Bitmap 对象返回。

我们还使用了一些从 PictureBox 类继承的用户控件来支持背景透明。

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;
            }
        }
    }
}

编译后,您将在工具箱窗格顶部的解决方案组件列表中看到该控件,标签为 TransparentPictureBox

现在,让我们看一下完整的代码列表,并分别讨论每个部分。

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();
        }
    }
}

运动的错觉是通过 tmr_MoveBG 定时器启动和执行的。每次定时器滴答时,都会调用 MoveImage 函数,并将图片框当前修改的 Image 的克隆传递回去,并将其分配给 PictureBox 控件,以便在下一次滴答时再次传递。

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();
}

MoveImage 函数将源图像作为 Bitmap 对象作为第一个参数,以及要裁剪的边距和所需的移动方向。

方向参数通过 switch case 块进行评估,以裁剪和重绘右侧图像,最后将其返回设置回 PictureBox 控件,该控件将再次被用作克隆,成为传递给函数的新的源图像。

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;
}

为了改变图像的方向,我们捕获 keyCode 并将其作为 string 分配给 sDirection 变量,然后简单地翻转飞船图像以匹配新的方向。请注意,背景图像会随着 sDirection 变量值的改变而改变方向,因为定时器仍在运行,并使用它来确定移动方向。

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();
    }
}

反转效果的新设置是通过切换剪辑的位置来处理的,这样较大的剪辑(Clip 1)就占据了较小剪辑(Clip 2)的位置,反之亦然,从而产生移动拉伸效果以适应不匹配大小的剪辑。

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);
}

最后,我们通过增加/减少定时器的间隔值来确定移动速度。

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

当然,我们可以增加每次剪辑时取出的列数,通过在 MoveImage 函数中增加边距来牺牲一些平滑度。

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

请注意,在本例中 iSpeed 值已在 form_load 事件中与 sDirection 值一起预先确定。

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

关注点

此技术的一个主要缺点是新绘制的图像会遮盖住上方的 PictureBox 图像,因此定时器在每次滴答时都会不断刷新它,从而导致图像闪烁,这是很不方便的。

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