Is there a fast alternative to creating a Texture2D from a Bitmap object in XNA?
Asked Answered
T

4

12

I've looked around a lot and the only methods I've found for creating a Texture2D from a Bitmap are:

using  (MemoryStream s = new  MemoryStream())
{
   bmp.Save(s, System.Drawing.Imaging.ImageFormat.Png);
   s.Seek(0, SeekOrigin.Begin);
   Texture2D tx = Texture2D.FromFile(device, s);
}

and

Texture2D tx = new Texture2D(device, bmp.Width, bmp.Height,
                        0, TextureUsage.None, SurfaceFormat.Color);
tx.SetData<byte>(rgbValues, 0, rgbValues.Length, SetDataOptions.NoOverwrite);

Where rgbValues is a byte array containing the bitmap's pixel data in 32-bit ARGB format.

My question is, are there any faster approaches that I can try?

I am writing a map editor which has to read in custom-format images (map tiles) and convert them into Texture2D textures to display. The previous version of the editor, which was a C++ implementation, converted the images first into bitmaps and then into textures to be drawn using DirectX. I have attempted the same approach here, however both of the above approaches are significantly too slow. To load into memory all of the textures required for a map takes for the first approach ~250 seconds and for the second approach ~110 seconds on a reasonable spec computer (for comparison, C++ code took approximately 5 seconds). If there is a method to edit the data of a texture directly (such as with the Bitmap class's LockBits method) then I would be able to convert the custom-format images straight into a Texture2D and hopefully save processing time.

Any help would be very much appreciated.

Thanks

Tody answered 19/5, 2010 at 22:7 Comment(0)
H
10

You want LockBits? You get LockBits.

In my implementation I passed in the GraphicsDevice from the caller so I could make this method generic and static.

public static Texture2D GetTexture2DFromBitmap(GraphicsDevice device, Bitmap bitmap)
{
    Texture2D tex = new Texture2D(device, bitmap.Width, bitmap.Height, 1, TextureUsage.None, SurfaceFormat.Color);

    BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat);

    int bufferSize = data.Height * data.Stride;

    //create data buffer 
    byte[] bytes = new byte[bufferSize];    

    // copy bitmap data into buffer
    Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);

    // copy our buffer to the texture
    tex.SetData(bytes);

    // unlock the bitmap data
    bitmap.UnlockBits(data);

    return tex;
}
Hoban answered 20/5, 2010 at 0:52 Comment(5)
Shouldn't that be "from the caller" instead of "callee"? From the purposes of the user of this code, it's this function that is the callee.Betterment
Thanks for your response, however I meant in my original question that I had tried this approach - using SetData(). I ran some further analysis, and to create the Bitmaps using LockBits took 2.7 seconds. When I added to that code, for each Bitmap generated: Texture2D tx = new Texture2D(device, bmp.Width, bmp.Height, 0, TextureUsage.None, SurfaceFormat.Color); It upped the time required to 74 seconds!! Just to create a blank texture for each bitmap, and not even fill it. This kind of loading time is not acceptable for a 2D game. I did not think XNA would be this slow. :/Tody
@Matthew, now that I re-read your question I see that I didn't understand it properly. Sorry about that. Can you give a ballpark estimate of how many and how large of Texture2Ds your will end up creating?Hoban
One of the larger maps that I need to load contains 1235 tile sheets, each tile sheet representing a composite image of map tiles that are used by the map terrain. I need to create a texture for each of one those tile sheets in order to draw the map. The size of textures I need range from 128x128px to very large (2048x2048px or maybe even 4096px) when considering that they must be in powers of 2. Large textures would represent map objects such as houses and buildings (the tile sheet figure I gave does not include map objects - these would be loaded in and out of memory as required).Tody
If your colors are getting reversed see Jaska's answer below.Instrumental
D
10

they have changed the format from bgra to rgba in XNA 4.0, so that method gives strange colors, the red and blue channels needs to be switched. Here's a method i wrote that is super fast! (loads 1500x 256x256 pixel textures in about 3 seconds).

    private Texture2D GetTexture(GraphicsDevice dev, System.Drawing.Bitmap bmp)
    {
        int[] imgData = new int[bmp.Width * bmp.Height];
        Texture2D texture = new Texture2D(dev, bmp.Width, bmp.Height);

        unsafe
        {
            // lock bitmap
            System.Drawing.Imaging.BitmapData origdata = 
                bmp.LockBits(new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);

            uint* byteData = (uint*)origdata.Scan0;

            // Switch bgra -> rgba
            for (int i = 0; i < imgData.Length; i++)
            {
                byteData[i] = (byteData[i] & 0x000000ff) << 16 | (byteData[i] & 0x0000FF00) | (byteData[i] & 0x00FF0000) >> 16 | (byteData[i] & 0xFF000000);                        
            }                

            // copy data
            System.Runtime.InteropServices.Marshal.Copy(origdata.Scan0, imgData, 0, bmp.Width * bmp.Height);

            byteData = null;

            // unlock bitmap
            bmp.UnlockBits(origdata);
        }

        texture.SetData(imgData);

        return texture;
    }
Dougherty answered 12/9, 2011 at 21:18 Comment(4)
Nice! But shouldn't you not be modifying the bitmap data since you locked it with ReadOnly? You could just copy it first then do the conversion. Or even simpler, just do the conversion and copy in one step! (Assign to imgData[i] instead of byteData[i], then you don't need the copy at all!)Flimflam
+1 This is handy, although (as @Flimflam suggested) it would be better if you didn't change the original bitmap, but instead worked on the imgData array directly (you wouldn't even have to use unsafe in that case).Superfluous
Exact details to do what @Groo suggests: 1) Move Marshal.Copy line BEFORE the for loop that switches byte order. 2) replace byteData references inside that loop with imgData. 3) remove unsafe and uint* byteData = (uint*)origData.Scan0;and byteData = null; 4) maybe also change imgData to uint[] imgData, rather than int so that the formula works correctly and/or does not cause numeric overflow if running in checked mode - but maybe not; I haven't tested.Walls
A simpler approach is to is to change the Texture's surface format to SurfaceFormat.Bgra32 to match the bitmap's. Doesn't seem to cause a performance hit. texture = new Texture2D(GraphicsDevice, bitmap_size.Width, bitmap_size.Height, false, SurfaceFormat.Bgra32);Instrumental
G
3

I found I had to specify the PixelFormat as .Format32bppArgb when using LockBits as you suggest to grab webcam images.

        BitmapData bmd = bmp.LockBits(new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height),
            System.Drawing.Imaging.ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        int bufferSize = bmd.Height * bmd.Stride;
        //create data buffer 
        byte[] bytes = new byte[bufferSize];
        // copy bitmap data into buffer
        Marshal.Copy(bmd.Scan0, bytes, 0, bytes.Length);

        // copy our buffer to the texture
        Texture2D t2d = new Texture2D(_graphics.GraphicsDevice, bmp.Width, bmp.Height, 1, TextureUsage.None, SurfaceFormat.Color);
        t2d.SetData<byte>(bytes);
        // unlock the bitmap data
        bmp.UnlockBits(bmd);
        return t2d;
Goat answered 20/5, 2010 at 7:41 Comment(0)
W
1

When I first read this question, I assumed it was SetData performance that was the limit. However reading OP's comments in the top answer, he seems to be allocating a lot of large Texture2D's.

As an alternative, consider having a pool of Texture2D's, that you allocate as needed, return to the pool when no longer needed.

The first time each texture file is needed (or in a "pre-load" at start of your process, depending on where you want the delay), load each file into a byte[] array. (Store those byte[] arrays in an LRU Cache - unless you are sure you have enough memory to keep them all around all the time.) Then when you need one of those textures, grab one of the pool textures, (allocating a new one, if none of appropriate size is available), SetData from your byte array - viola, you have a texture.

[I've left out important details, such as the need for a texture to be associated with a specific device - but you can determine any needs from the parameters to the methods you are calling. The point I am making is to minimize calls to the Texture2D constructor, especially if you have a lot of large textures.]

If you get really fancy, and are dealing with many different size textures, you can also apply LRU Cache principles to the pool. Specifically, track the total number of bytes of "free" objects held in your pool. If that total exceeds some threshold you set (maybe combined with the total number of "free" objects), then on next request, throw away oldest free pool items (of the wrong size, or other wrong parameters), to stay below your allowed threshold of "wasted" cache space.

BTW, you might do fine simply tracking the threshold, and throwing away all free objects when threshold is exceeded. The downside is a momentary hiccup the next time you allocate a bunch of new textures - which you can ameliorate if you have information about what sizes you should keep around. If that isn't good enough, then you need LRU.

Walls answered 3/3, 2018 at 19:2 Comment(1)
Although it's been 8 years, I appreciate you writing a response to my question. I think that your approach could indeed work well if the call to the Texture2D constructor is the bottleneck.Tody

© 2022 - 2024 — McMap. All rights reserved.