【问题标题】:Is there a fast alternative to creating a Texture2D from a Bitmap object in XNA?从 XNA 中的位图对象创建 Texture2D 是否有快速的替代方法?
【发布时间】:2011-02-21 14:59:53
【问题描述】:

我环顾四周,发现从位图创建 Texture2D 的唯一方法是:

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

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

其中 rgbValues 是一个字节数组,其中包含 32 位 ARGB 格式的位图像素数据。

我的问题是,有没有更快的方法可以尝试?

我正在编写一个地图编辑器,它必须读取自定义格式的图像(地图图块)并将它们转换为 Texture2D 纹理以显示。以前版本的编辑器是一个 C++ 实现,首先将图像转换为位图,然后再转换为要使用 DirectX 绘制的纹理。我在这里尝试了相同的方法,但是上述两种方法都太慢了。在合理规格的计算机上,将地图所需的所有纹理加载到内存中,第一种方法大约需要 250 秒,第二种方法大约需要 110 秒(相比之下,C++ 代码大约需要 5 秒)。如果有直接编辑纹理数据的方法(例如使用 Bitmap 类的 LockBits 方法),那么我将能够将自定义格式的图像直接转换为 Texture2D,并有望节省处理时间。

非常感谢任何帮助。

谢谢

【问题讨论】:

    标签: c# xna bitmap texture2d


    【解决方案1】:

    您想要 LockBits 吗?你得到了 LockBits。

    在我的实现中,我从调用者传入了 GraphicsDevice,因此我可以使这个方法成为通用和静态的。

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

    【讨论】:

    • 不应该是“来自调用者”而不是“被调用者”吗?从这段代码的用户的目的来看,这个函数就是被调用者。
    • 感谢您的回复,但我的意思是在我最初的问题中我尝试过这种方法 - 使用 SetData()。我进行了一些进一步的分析,使用 LockBits 创建位图需要 2.7 秒。当我添加到该代码时,对于生成的每个位图: Texture2D tx = new Texture2D(device, bmp.Width, bmp.Height, 0, TextureUsage.None, SurfaceFormat.Color);将所需时间提高到 74 秒!!只是为每个位图创建一个空白纹理,甚至不填充它。这种加载时间对于 2D 游戏来说是不可接受的。我没想到 XNA 会这么慢。 ://
    • @Matthew,现在我重新阅读了您的问题,我发现我没有正确理解它。对于那个很抱歉。您能否大致估计一下您最终将创建多少和多大的 Texture2D?
    • 我需要加载的一张较大的地图包含 1235 个瓦片,每个瓦片代表地图地形使用的地图瓦片的合成图像。为了绘制地图,我需要为每一个瓦片创建一个纹理。考虑到它们必须是 2 的幂时,我需要的纹理大小范围从 128x128px 到非常大(2048x2048px 甚至可能是 4096px)。大纹理将代表地图对象,例如房屋和建筑物(我给出的图块图没有包括地图对象 - 这些将根据需要加载到内存中或从内存中加载出来)。
    • 如果你的颜色变反了,请看下面 Jaska 的回答。
    【解决方案2】:

    他们在 XNA 4.0 中将格式从 bgra 更改为 rgba,因此该方法给出了奇怪的颜色,需要切换红色和蓝色通道。这是我写的一个超级快的方法! (在大约 3 秒内加载 1500x 256x256 像素纹理)。

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

    【讨论】:

    • 不错!但是你不应该修改位图数据,因为你用 ReadOnly 锁定它吗?您可以先复制它然后进行转换。或者更简单,只需一步完成转换和复制! (赋值给 imgData[i] 而不是 byteData[i],那么你根本不需要副本!)
    • +1 这很方便,尽管(正如@Cameron 建议的那样)如果你不更改原始位图会更好,而是直接处理imgData 数组(你不会在这种情况下甚至必须使用unsafe)。
    • 执行@Groo 建议的确切细节:1) 将Marshal.Copy 行移到切换字节顺序的for 循环之前。 2) 用imgData 替换循环内的byteData 引用。 3) remove unsafeuint* byteData = (uint*)origData.Scan0;and byteData = null; 4) 也可以将 imgData 更改为 uint[] imgData,而不是 int,以便公式正常工作和/或不工作如果在checked 模式下运行会导致数字溢出 - 但可能不会;我没有测试过。
    • 更简单的方法是将纹理的表面格式更改为SurfaceFormat.Bgra32 以匹配位图。似乎不会对性能造成影响。 texture = new Texture2D(GraphicsDevice, bitmap_size.Width, bitmap_size.Height, false, SurfaceFormat.Bgra32);
    【解决方案3】:

    我发现在使用 LockBits 时,我必须将 PixelFormat 指定为 .Format32bppArgb,因为您建议使用它来获取网络摄像头图像。

            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;
    

    【讨论】:

      【解决方案4】:

      当我第一次阅读这个问题时,我认为SetData 的性能是极限。然而,在最佳答案中阅读 OP 的 cmets 时,他似乎分配了 lotlarge Texture2D。

      作为替代方案,考虑拥有一个 Texture2D 池,您可以根据需要分配,在不再需要时返回池中。

      第一次需要每个纹理文件时(或在进程开始时的“预加载”中,取决于您希望延迟的位置),将每个文件加载到 byte[] 数组中。 (将这些 byte[] 数组存储在 LRU 缓存 中 - 除非您确定有足够的内存来始终保持它们。)然后,当您需要其中一个纹理时,抓住其中一个池纹理,(分配一个新的,如果没有合适的大小可用),从你的字节数组中设置数据 - 中提琴,你有一个纹理。

      [我省略了重要的细节,例如需要将纹理与特定设备关联 - 但您可以确定从参数到您正在调用的方法的任何需求。我要说的一点是尽量减少对 Texture2D 构造函数的调用,尤其是在你有很多大纹理的情况下。]

      如果您真的很喜欢,并且正在处理许多不同大小的纹理,您还可以将 LRU 缓存 原则应用于池。具体来说,跟踪池中“空闲”对象的总字节数。如果该总数超过您设置的某个阈值(可能与“空闲”对象的总数相结合),那么在下一次请求时,丢弃最旧的空闲池项目(大小错误或其他错误参数),以保持低于您的允许“浪费”缓存空间的阈值。

      顺便说一句,您只需跟踪阈值并在超过阈值时丢弃 所有 个空闲对象就可以了。缺点是下次分配一堆新纹理时会出现短暂的打嗝——如果你有关于应该保留什么尺寸的信息,你可以改善这种情况。如果这还不够好,那么您需要 LRU

      【讨论】:

      • 虽然已经 8 年了,但我很感谢您回复我的问题。我认为如果对 Texture2D 构造函数的调用是瓶颈,您的方法确实可以很好地工作。
      猜你喜欢
      • 1970-01-01
      • 2013-08-07
      • 2013-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多