Click here to Skip to main content
15,860,943 members
Articles / Desktop Programming / Windows Forms
Article

XNA Image Reflector. Easily Create Web2.0-like Reflected Images

Rate me:
Please Sign up or sign in to vote.
4.71/5 (39 votes)
23 Nov 2007CPOL7 min read 76K   1.4K   79   13
This article describes XNAImageReflector - a Windows application that integrates XNA rendering and maths for easily creating web2.0-like reflected images, with post-processing effects.
Screenshot - ui.jpg

Introduction

In the past months or years, every single PowerPoint or web page I have seen with a decent design and look, includes pictures that have been "reflected" vertically, as if they were on the top of a reflecting table or so... it's the so-called "web 2.0 reflection". Just like this one:

Screenshot - Final_Image2.png

This kind of effect can be easily achieved with Photoshop, Gimp or whatever, just reflecting the image and applying a distort and a gradient mask, but hey! those 5 minutes per image are priceless if you have to do it with 50 pictures! So I decided to make a small app that allows you to create this kind of effect in 5 seconds, adding some processing effects to improve its functionality.

There are already similar projects out there (see Points of Interest), but I do it just for fun and to practice some new things I'm explaining below.

Topics Covered by this Project

  • XNA and Windows Forms integration
  • PropertyGrid control and basic ComponentModel namespace
  • Basic bitmap manipulation (reflection, alpha, blur, etc.)
  • Dynamic creation of textures in XNA
  • Basic maths

Note: This project uses some code samples found in the XNA samples.

Specifications

The application includes the following features:

  • Automatic reflection using a reflection polygon to specify the shape of the form to reflect
  • Automatic reflection distortion
  • Automatic alpha gradient with different gradient modes
  • Image resizing and re-locating
  • Post-processing effects for reflection: Blur, Alpha Texture, ...
  • Supports following formats: BMP, JPEG, TGA, PNG, DDS, DIB, HDR, PPM, PFM

Technical Information

  • Developed in Visual C# Express
  • All the maths and graphics done with XNA
  • Shows integration between XNA and Windows Forms

Requirements

  • Visual C# Express with Service Pack 1
  • XNA Game Studio 1.0 Refresh
  • XNA-compatible hardware

Using the Application

The user interface is divided into four areas:

  • The tool strip holds three buttons (on the top): Add point to reflection mark, refresh processing and select a background color for the working area
  • The processing settings property grid (on the upper left): Lets you set all the properties of the processing and effects
  • Original bitmaps picture box (on the lower left): Shows the original bitmap
  • Working area (on the right): Shows the current bitmap state and the Reflection Mark (the border the reflection will follow)

To process a bitmap, the first step is to open it (File->Open). The following picture is a good example to work with, because it has perspective and an irregular shape, which makes the reflection more complex:

Screenshot - 1.png

As the bitmap occupies the area of almost the entire picture, you'll need to make some room for the reflection. To do so, select an AutoSizeMode to scale the bitmap down (processing settings property grid, 'Scaling' category).

Screenshot - 3.jpg

In this example, I selected div1_25, which will reduce the size of the bitmap by 1 quarter. Now the bitmap is smaller in the final result, so you will probably need to re-center it. Such a thing can be done in the property NewLocation (in the example, I translated the bitmap 150 pixels to the right, leaving it at the top of the picture, so the reflection will fit in the bottom part).

After that, move up the "Reflection Mark" so it fits the bottom part of the object to be reflected. Now you can see a normal vertical reflection, which does not fit the shape of the object properly in this case:

Screenshot - 4.jpg

Next, create some points to divide the reflection mark (select the star-like button of the toolstrip, and then click on a point inside the reflection mark). This way, it can be adjusted to the shape of the object. For the XBox, we created three points, as you can see in this picture:

Screenshot - 5.jpg

Now, move the points to adjust the reflection mark, and you will get a nice adjusted reflection:

Screenshot - 6.jpg

To make the alpha gradient more appropriate, adjust the range and set a negative offset to make the reflection more transparent. Finally, put some blur there to hide pixelation and aliasing effects:

Screenshot - 7.jpg

Et voilá! The final picture:

Screenshot - Final.png

Note 1: If the picture being processed has transparency, like the XBox one, you can choose to keep it or to save the final bitmap with the working area's back color as background. This is managed with the setting: KeepTransparency.

Note 2: In order to save the Alpha Gradient correctly, I recommend selecting PNG as the output format.

Using the Code

Note: Although some classes have been designed following the XNA GameComponent-like architecture, none of them really inherit from GameComponent. That's because this is mostly a WinForm application that uses XNA for rendering and maths, not an XNA-Game application that includes WinForms controls.

The structure of the application is quite simple.

First, I start using the XNAWinForm explained in my previous article about XNA integration with WindowsForm, and continue adding all the WinForms elements I'll need in this project: SplitterContainer, PropertyGrid, PictureBox, MenuStrip, ToolStrip, ...

This Form will also contain all the bitmap processing functionality that will be explained later.

Each time the bitmap is processed, it refreshes a texture shown in the screen by the XNAPictureBox class, which basically imitates System.Windows.Forms.PictureBox, rendering the texture with SizeMode = Zoom. XNAPictureBox also uses the PrimitiveBatch class (from XNA samples) to render lines for the picture frame.

Using the PrimitiveBatch class is very easy, as it imitates the SpriteBatch behavior, but to render primitives instead of textures. For example, to draw a line:

C#
// Draw frame
mPrimitiveBatch.Begin(PrimitiveType.LineList);
mPrimitiveBatch.AddVertex(new Vector2(0, 0), Color.Black);
mPrimitiveBatch.AddVertex(new Vector2(10, 10), Color.Black);
mPrimitiveBatch.End();

The bitmap manipulation process is configured by several settings, stored in the ProcessingSettings class. This class exposes properties for the values so they can be shown in a PropertyGrid. Using ComponentModel attributes, you can configure the appearance and behaviour of each property, like this one:

C#
[Category("Translation")]
[Description("Relocates the bitmap inside the canvas. Useful when resizing")]
public Point NewLocation
{
    get { return mNewLocation; }
    set { mNewLocation = value; }
}

Next, the ReflectionMark class, which defines the shape of the border used for the reflection, contains a collection of points, and manages point moving and creation based on the following internal state:

C#
public enum eMouseMode
{
    MovePoint,
    MoveMark,
    CreatePoint,
    None,
}

The MouseMode is changed in several situations:

  • When a MouseClick happens, the state is sometimes changed.
  • Other times it's changed from outside the ReflectionMark class. For example, when the user selects the "Point Creation" mode by pressing the first button of the ToolStrip.

In order to change the MouseMode state and to move points or the bar, the ReflectionMark should be informed when the user moves the mouse or clicks any button. This could be done reading mouse information through XNA, but in this case I just handled those events from the PanelViewport class.

In the opposite way, when the MouseMode state changes, or the reflection mark is moved or changed in any way, we should inform the XNAWinForm as well, so it can refresh the bitmap or synchronize buttons and controls. I defined some events on the ReflectionMark for that:

C#
public event EventHandler ReflectionMarkChanged = null;
public event EventHandler ReflectionMarkChanging = null;
public event EventHandler MouseModeChanged = null;

Finally, we come to Bitmap Processing. (Please note that a lot of improvement and optimization can be done here). Any suggestions are welcome!

This task is done in the XNAWinForm.ProcessBitmap() method, which first reflects the bitmap and then applies blur and texture alpha (if selected). When all the operations are completed, it refreshes XNAPictureBox texture colors with the result.

C#
private void ProcessBitmap()
{
    if (this.mBitmapOriginal == null)
        return;

    Cursor.Current = Cursors.WaitCursor;

    this.mProcessedColors = this.ReflectBitmap();

    if(mSettings.BlurSteps > 0)
        this.BlurBitmap();
    if (mSettings.TextureAlpha != null)
        this.TextureAlpha();

    this.mXNAPictureBox.Texture.SetData<uint />(mProcessedColors);

    Cursor.Current = Cursors.Default;
}

In fact, the ReflectBitmap method does four different things:

  • Handles the KeepTransparency property by cleaning the final texture to a transparent color or to the WorkingArea's back color.
  • Scales the original bitmap
  • Places it into the final texture, in the new location
  • Reflects the pixels below the ReflectionMark

IMPORTANT NOTE: For performance reasons, the following method is UNSAFE and uses pointer access to bitmap data. If you want to avoid this behaviour, you can rewrite it using Bitmap's GetPixel and SetPixel methods, which will probably be slower.

C#
public unsafe uint[] ReflectBitmap()
{
    if (this.mBitmapOriginal == null)
        return null;

    // Create a canvas of the correct size and with Alpha Pixel format
    Bitmap finalBitmap = new Bitmap(this.mBitmapOriginal.Width,
                                    this.mBitmapOriginal.Height,
                   System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    // Locate bitmap
    System.Drawing.Graphics g = Graphics.FromImage(finalBitmap);

    if(mSettings.KeepTransparency)
        g.Clear(System.Drawing.Color.Transparent);
    else g.Clear(this.panelViewport.BackColor);

    float sx = (float)this.mSettings.NewWidth /
               (float)this.mBitmapOriginal.Width;
    float sy = (float)this.mSettings.NewHeight /
               (float)this.mBitmapOriginal.Height;
    g.InterpolationMode =
        System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
    g.ScaleTransform(sx, sy);
    System.Drawing.Rectangle destRect =
         new System.Drawing.Rectangle(mSettings.NewLocation,
         finalBitmap.Size);
    System.Drawing.Rectangle srcRect =
         new System.Drawing.Rectangle(new System.Drawing.Point(),
         finalBitmap.Size);
    g.DrawImage(this.mBitmapOriginal,
                destRect, srcRect,
                GraphicsUnit.Pixel);

    System.Drawing.Imaging.BitmapData bmData = finalBitmap.LockBits(
           new System.Drawing.Rectangle(0, 0,
           finalBitmap.Width, finalBitmap.Height),
           System.Drawing.Imaging.ImageLockMode.ReadOnly,
           System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    int pixelBytes = 4;
    byte* pointer = (byte*)(void*)bmData.Scan0;

    // Make the reflection
    uint[] result = new uint[finalBitmap.Width * finalBitmap.Height];
    for (int x = 0; x < finalBitmap.Width; x++)
    {
        int pbX = (int)this.mXNAPictureBox.ImageToPictureBox(
                       new Vector2(x, 0)).X;
        int initialy = (int)this.mReflectionMark.GetYAt(pbX);
        if (initialy == float.MinValue)
            continue;
        int initialy_onImage =
        (int)this.mXNAPictureBox.PictureBoxToImage(new Vector2(0, initialy)).Y;
        int address, adBytes;
        for (int y = 0; y < finalBitmap.Height; y++)
        {
            address = (y * finalBitmap.Width) + x;
            if (y <= initialy_onImage)
            {
                adBytes = (y * bmData.Stride) + (x * pixelBytes);
                // alpha << 24 | red << 16 | green << 8 | blue;
                result[address] = (uint)(pointer[adBytes + 3] << 24 |
                pointer[adBytes + 2] << 16 |
                pointer[adBytes + 1] << 8 |
                pointer[adBytes]);
                continue;
            }
            int disty = y - initialy_onImage;
            int reflectY = initialy_onImage - disty;
            if (reflectY >= 0 && reflectY < finalBitmap.Height)
            {
                adBytes = (reflectY * bmData.Stride) + (x * pixelBytes);
                // alpha << 24 | red << 16 | green << 8 | blue;

                if (mSettings.KeepTransparency)
                {
                   int alpha = (int)((float)pointer[adBytes + 3] *
                   this.GetAlphaGradient(disty));
                   result[address] = (uint)(alpha << 24 |
                             pointer[adBytes + 2] << 16 |
                              pointer[adBytes + 1] << 8 |
                              pointer[adBytes]);
                }
                else
                {
                   // Mix up backColor and reflection color
                   float falpha = this.GetAlphaGradient(disty);
                   byte red = (byte)(((float)pointer[adBytes + 2] * falpha) +
                           ((float)panelViewport.BackColor.R * (1 - falpha)));
                   byte green = (byte)(((float)pointer[adBytes + 1] * falpha)+
                            ((float)panelViewport.BackColor.G * (1 - falpha)));
                   byte blue = (byte)(((float)pointer[adBytes] * falpha) +
                        ((float)panelViewport.BackColor.B * (1 - falpha)));

                   result[address] = (uint)((byte)255 << 24 |
                                     red << 16 |
                                     green << 8  |
                                     blue);
                }
            }
            else if (!mSettings.KeepTransparency)
            {
                // Just copy backcolor
               result[address] = (uint)(panelViewport.BackColor.A << 24 |
                                     panelViewport.BackColor.R << 16 |
                                      panelViewport.BackColor.G << 8 |
                                      panelViewport.BackColor.B);
            }
        }
    }

    return result;
}

In order to set each reflected pixel's alpha, another method is used:

C#
private float GetAlphaGradient(int disty)
{
    switch (this.mSettings.AlphaGradientMode)
    {
        case ProcessingSettings.eGradientMode.Linear:

            float alpha = 1f-((float)disty /
                this.mSettings.AlphaGradientRange);
            alpha += mSettings.AlphaGradientOffset;
            alpha = Math.Min(1f, alpha);
            alpha = Math.Max(0f, alpha);
            return alpha;
        default:
            return 1;
    }

}

It basically computes alpha values from the vertical distance to the reflection mark. If you want to extend the project, other GradientModes (non-linear) could be added here, like exponential, etc.

I guess this article is getting too long, so I'll leave the rest for you. Just check the source code and feel free to ask for any other information you may need.

Points of Interest

With a quick Google on the concept, I've found similar projects and tutorials:

  • A web based reflector
  • A tutorial on making reflections with WPF
  • A tutorial about making the same thing with Photoshop
  • Some tips about making it with ImageMagick
  • Something similar in C#
  • Another tutorial about doing it with Adobe Illustrator

History

  • November 23, 2007: First version
  • November 23, 2007: Second version - KeepTransparency bug fix

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Spain Spain
Inaki Ayucar is a Microsoft MVP in DirectX/XNA, and a software engineer involved in development since his first Spectrum 48k, in the year 1987. He is the founder and chief developer of The Simax Project (www.simaxvirt.com) and is very interested in DirectX/XNA, physics, game development, simulation, C++ and C#.

His blog is: http://graphicdna.blogspot.com

To contact Inaki: iayucar@simax.es

Comments and Discussions

 
GeneralExactly what I was looking for my XNA project Pin
Marcelo Ricardo de Oliveira13-Dec-09 5:06
mvaMarcelo Ricardo de Oliveira13-Dec-09 5:06 
GeneralTrouble running on the latest version of XNA... Pin
JoelBennett13-Mar-09 4:46
JoelBennett13-Mar-09 4:46 
GeneralGreat XNA info Pin
VCSKicks21-Aug-08 19:39
VCSKicks21-Aug-08 19:39 
GeneralError in demo project Pin
thany.nl28-Nov-07 1:13
thany.nl28-Nov-07 1:13 
GeneralRe: Error in demo project Pin
Inaki Ayucar28-Nov-07 1:55
Inaki Ayucar28-Nov-07 1:55 
GeneralRe: Error in demo project Pin
thany.nl28-Nov-07 7:39
thany.nl28-Nov-07 7:39 
GeneralRe: Error in demo project Pin
Inaki Ayucar28-Nov-07 8:10
Inaki Ayucar28-Nov-07 8:10 
GeneralRe: Error in demo project Pin
thany.nl29-Nov-07 23:13
thany.nl29-Nov-07 23:13 
GeneralPaint.NET plugin Pin
Rei Miyasaka24-Nov-07 16:03
Rei Miyasaka24-Nov-07 16:03 
GeneralRe: Paint.NET plugin Pin
Inaki Ayucar26-Nov-07 20:41
Inaki Ayucar26-Nov-07 20:41 
GeneralOutstanding Pin
rcollina23-Nov-07 4:12
rcollina23-Nov-07 4:12 
Generalvery nice sir Pin
Sacha Barber23-Nov-07 3:41
Sacha Barber23-Nov-07 3:41 
GeneralExcellent Pin
Armando Airo'23-Nov-07 3:32
Armando Airo'23-Nov-07 3:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.