【问题标题】:Is Graphics.DrawImage too slow for bigger images?Graphics.DrawImage 对于更大的图像来说太慢了吗?
【发布时间】:2012-06-16 18:07:09
【问题描述】:

我目前正在开发一款游戏,我希望有一个带有背景图片的主菜单。

但是,我发现Graphics.DrawImage() 方法真的很慢。我做了一些测量。假设 MenuBackground 是我的资源图像,分辨率为 800 x 1200 像素。我会将它绘制到另一个 800 x 1200 位图上(我首先将所有内容渲染到缓冲区位图,然后缩放它,最后将其绘制到屏幕上 - 这就是我处理多个玩家分辨率的可能性的方式。但它不应该影响无论如何,请参阅下一段)。

所以我测量了以下代码:

Stopwatch SW = new Stopwatch();
SW.Start();

// First let's render background image into original-sized bitmap:

OriginalRenderGraphics.DrawImage(Properties.Resources.MenuBackground,
   new Rectangle(0, 0, Globals.OriginalScreenWidth, Globals.OriginalScreenHeight));

SW.Stop();
System.Windows.Forms.MessageBox.Show(SW.ElapsedMilliseconds + " milliseconds");

结果出乎我的意料——Stopwatch 的测量值介于40 - 50 milliseconds 之间。而且因为背景图像不是唯一要绘制的,整个菜单的显示时间大约需要 100 多毫秒,这意味着可观察到的延迟。

我尝试将它绘制到 Paint 事件给出的 Graphics 对象,但结果是 30 - 40 milliseconds - 没有太大变化。

那么,这是否意味着Graphics.DrawImage() 不能用于绘制更大的图像?如果是这样,我应该怎么做才能提高我的游戏性能?

【问题讨论】:

  • 如果“无法使用”是指“像糖蜜中的蜗牛一样慢”,那么可以。正如其他人所说,试试 XNA。坚持使用 2D sprite 渲染,直到您习惯了 XNA 框架,然后如果您愿意,则转向 3D。
  • 我赞成这个比喻。 :)

标签: c# image c#-4.0 gdi+


【解决方案1】:

是的,太慢了。

几年前,我在开发 Paint.NET 时遇到了这个问题(实际上,从一开始,它就相当令人沮丧!)。渲染性能很糟糕,因为它总是与位图的大小成正比,而不是与被告知要重绘的区域的大小成正比。也就是说,帧率随着位图大小的增加而下降,而在实现 OnPaint() 和调用 Graphics.DrawImage() 时,帧率永远不会随着无效/重绘区域的大小下降而上升。一个小的位图,比如 800x600,总是可以正常工作,但较大的图像(例如 2400x1800)非常慢。 (无论如何,对于前面的段落,您可以假设没有发生任何额外的事情,例如使用一些昂贵的双三次滤波器进行缩放,这会对性能产生不利影响。)

可以强制 WinForms 使用 GDI 而不是 GDI+,甚至可以避免在幕后创建 Graphics 对象,此时您可以在其上叠加另一个渲染工具包(例如 Direct2D)。然而,这并不简单。我在 Paint.NET 中执行此操作,您可以通过在 SystemLayer DLL 中名为 GdiPaintControl 的类上使用 Reflector 之类的东西来查看需要什么,但对于您正在做的事情,我认为这是最后的手段。

但是,您使用的位图大小 (800x1200) 在 GDI+ 中应该仍然可以正常工作,而不必求助于高级互操作,除非您的目标是低至 300MHz Pentium II。以下是一些可能会有所帮助的提示:

  • 如果您在对 Graphics.DrawImage() 的调用中使用不透明位图(无 alpha/透明度),尤其是如果它是带有 alpha 通道的 32 位位图(但您知道它是不透明的,或者你不在乎),然后在调用DrawImage()之前将Graphics.CompositingMode设置为CompositingMode.SourceCopy(之后一定要设置回原来的值,否则常规的绘图图元会看起来很丑)。这会跳过每个像素的大量额外混合数学。
  • 确保Graphics.InterpolationMode 未设置为InterpolationMode.HighQualityBicubic 之类的内容。使用NearestNeighbor 将是最快的,尽管如果有任何拉伸,它可能看起来不太好(除非它正好拉伸2x、3x、4x 等)Bilinear 通常是一个很好的折衷方案。如果位图大小与您要绘制的区域(以像素为单位)匹配,则您不应使用除 NearestNeighbor 之外的任何内容。
  • 始终绘制到OnPaint() 中提供给您的Graphics 对象。
  • 始终在OnPaint 中进行绘图。如需重绘区域,请致电Invalidate()。如果您需要立即进行绘图,请在Invalidate() 之后致电Update()。这是一种合理的方法,因为 WM_PAINT 消息(导致调用OnPaint())是“低优先级”消息。窗口管理器的任何其他处理都将首先完成,因此您最终可能会出现大量跳帧和卡顿。
  • 使用System.Windows.Forms.Timer 作为帧速率/滴答计时器效果不佳。这些是使用 Win32 的 SetTimer 实现的,并产生 WM_TIMER 消息,然后引发 Timer.Tick 事件,而 WM_TIMER 是另一个低优先级消息,仅在消息队列为空时发送。你最好使用System.Threading.Timer,然后使用Control.Invoke()(以确保你在正确的线程上!)并调用Control.Update()
  • 一般情况下,不要使用Control.CreateGraphics()。 (推论'总是使用OnPaint()'和'总是使用OnPaint()给你的Graphics')
  • 我建议不要使用 Paint 事件处理程序。相反,在您编写的类中实现OnPaint(),它应该派生自Control。从另一个类派生,例如PictureBoxUserControl,要么不会为您增加任何价值,要么会增加额外的开销。 (顺便说一句,PictureBox 经常被误解。你可能几乎永远不想使用它。)

希望对您有所帮助。

【讨论】:

  • 你应该得到大量的支持。绘制背景网格有什么技巧吗? TextureBrush 看起来很快,但是当您使用缩放因子进行自定义缩放时它不起作用,因为它不匹配
  • 这篇文章对我来说最好的优化是插值模式。将 Bilinear 更改为 NearestNeighbor 可使绘图速度提高 8 倍。
  • 您能否澄清您关于 PictureBoxes 的最后评论?为什么不应该使用它们,它们是如何被误解的?
  • PictureBox 用于显示Image,仅此而已。如果你假装这个类是sealed,它的用例应该更清楚。这是被误解的,因为我经常看到创建一个派生自PictureBox 的新类的代码,然后作者覆盖OnPaint 或其他任何东西并进行自定义渲染。不要那样做——如果你想做自定义渲染,直接从Control 派生。如果您只想显示ImageBitmap,请使用PictureBox——但不要从中派生新类。假装它是密封的。
  • 哇,您制作了 PAINT.net?我会自己给你一个赞成票!
【解决方案2】:

虽然这是一个古老的问题,WinForms 是一个古老的框架,但我想分享一下我刚刚偶然发现的:将 Bitmap 绘制到 BufferedGraphics 中,然后将其渲染到 OnPaint 提供的图形上下文中 比直接将位图绘制到 OnPaint 的图形上下文更快的方式 - 至少在我的 Windows 10 机器上。

这令人惊讶,因为直觉上我认为复制数据两次会稍微慢一些(所以我认为这通常只有在想要手动进行双缓冲时才合理)。但显然 BufferedGraphics 对象还有更复杂的功能。

因此,在 Control 的构造函数中创建一个 BufferedGraphics 来承载位图(在我的例子中,我想绘制一个 1920x1080 的全屏位图):

        using (Graphics graphics = CreateGraphics())
        {
            graphicsBuffer = BufferedGraphicsManager.Current.Allocate(graphics, new Rectangle(0,0,Screen.PrimaryScreen.Bounds.Width,Screen.PrimaryScreen.Bounds.Height));
        }

并在 OnPaint 中使用它(同时取消 OnPaintBackground)

    protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}

    protected override void OnPaint(PaintEventArgs e)
    {   
        Graphics g = graphicsBuffer.Graphics;

        g.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);

        graphicsBuffer.Render(e.Graphics);
    }

而不是天真地定义

    protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}

    protected override void OnPaint(PaintEventArgs e)
    {   
        e.Graphics.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);
    }

查看以下屏幕截图以比较生成的 MouseMove 事件频率(我正在实现一个非常简单的位图草图控件)。顶部是直接绘制Bitmap的版本,底部是使用BufferedGraphics。在这两种情况下,我以大致相同的速度移动鼠标。

【讨论】:

    【解决方案3】:

    GDI+ 可能不是游戏的最佳选择。应该首选 DirectX/XNA 或 OpenGL,因为它们利用任何可能的图形加速并且非常快。

    【讨论】:

      【解决方案4】:

      GDI+ 无论如何都不是速度恶魔。任何严重的图像处理通常都必须进入事物的本机方面(pInvoke 调用和/或通过调用 LockBits 获得的指针进行操作。)

      您是否研究过 XNA/DirectX/OpenGL?这些是为游戏开发而设计的框架,比使用 WinForms 或 WPF 等 UI 框架更高效、更灵活。这些库都提供 C# 绑定。

      您可以使用BitBlt 之类的函数调用本机代码,但也存在与跨托管代码边界相关的开销。

      【讨论】:

      • 我也会推荐 OpenGL/DirectX。由于各种原因,XNA 本身并不是真正的最佳选择。易于使用,是的 - 但从长远来看,这有点不利。
      • @ananthonline:你说得对,值得一提。如果您不关心跨平台支持,DirectX 优于 OpenGL。您能否扩展您的评论“从长远来看[XNA]有点障碍”。我的知识可能有些漏洞,因为我不是一个认真的游戏开发者,只是我的一个爱好。
      • XNA 对其使用做了几个假设。我能想到的一个严重限制是多个渲染目标不能共享一个深度缓冲区,这使得像有效执行延迟渲染这样的常见场景变得不可能。请参阅此处了解更多信息 (forums.create.msdn.com/forums/p/64179/397431.aspx)
      • 另外 - 它在 Metro 上没有明确的未来。我不知道它是否会在 MonoGame (monogame.codeplex.com) 等项目之外得到支持
      • @ananthonline:谢谢。我带着“迄今为止最好的”评论走了很远。我认为在 C# 中使用 DirectX 或 OpenGL 会很麻烦,但在做了一些研究之后,似乎有一些我不知道的不错的 C# 接口。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-08
      • 2011-07-09
      • 1970-01-01
      • 1970-01-01
      • 2014-02-14
      • 1970-01-01
      相关资源
      最近更新 更多