高效绘制2D精灵(位图)瓷砖

3
我正在编写一个2D基于瓷砖的引擎。目前,我的绘图例程使用C#绘图库在每次屏幕刷新时重新绘制每个可见的瓷砖。我已经使滚动和缩放正常工作,并且一切看起来都很好。但是程序运行速度慢。现在我正在尝试改进它,并有几个问题:
首先,我认为每次刷新都重新绘制瓷砖是不必要的(因为它们从未更改)。现在,我正在尝试更改算法,以便在初始化时将整个地图写入单个位图,然后在绘制时剪切正确的部分。您认为这是正确的方法吗? (我还考虑过将图像保留在背景中并在其上滚动。但是,我决定不想绘制视野范围之外的东西。不过,也许这比剪切/粘贴便宜?是内存与时间的问题吗?)
其次,据我所知,C#绘图例程没有充分利用GPU的全部功率。我认为应该尝试使用OpenGL进行绘图(或者DirectX,但我更喜欢前者,因为它是多平台的)。那样会有帮助吗?您是否了解OpenGL的任何平铺(或一般像素绘制)教程?书籍参考也可能有所帮助。
此外,我目前不进行多线程处理(实际上我对这是什么只有一个模糊的概念)。我应该尝试将drawer多线程处理吗?还是OpenGL使图形的多线程处理变得多余?
谢谢。

我提问后两周XNA4.0发布了。我尝试了一下,很喜欢它,于是就转用它了。XNA可以隐式地管理DirectX,所以你可以让你的精灵自由飘动而不必担心任何问题。非常好用。我的一个朋友使用WPF,那也很好,但我认为你需要了解一些DirectX(这不是什么大问题)。再次感谢大家给出的好答案。 - rtm
3个回答

2
我不熟悉OpenGL,但我已经用ManagedDX(后来转移到XNA)编写了一个基于瓷砖的引擎。ManagedDX已经被弃用,但有SlimDX项目仍在积极开发中。
使用DX,您可以将每个单独的瓷砖加载到Texture中。(例如使用Texture.FromFile()Texture.FromStream()),并且只需要一个单一的Sprite实例绘制它们。这样性能表现相当好。我将纹理分组到一个简单的类中,并使用Point或Vector2设置它们的位置,在位置改变时才进行设置,而不是每次调用绘制方法时都进行设置。我仅缓存屏幕上方及其附近的一两个瓷砖,没有必要超过这些,因为文件IO足够快,可以在滚动时获取新的瓷砖。
struct Tile
{
    public Point Location;
    public Texture Texture;
}

Device device;
Sprite sprite;
List<Tile> tiles = new List<Tile>();

.ctor() {
    this.device = new Device(...)
    this.sprite = new Sprite(this.device);
}

void Draw() {
    this.device.Clear(ClearFlags.Target, Color.CornflowerBlue, 1.0f, 0);
    this.device.BeginScene();
    this.sprite.Begin(SpriteFlags.AlphaBlend);
    foreach (Tile tile in this.tiles) {
        this.sprite.Draw2D(tile.Texture, 
            new Point(0, 0), 0.0f, tile.Location, Color.White);
    }
    this.sprite.End();
    this.device.EndScene();
    this.device.Present();
}

2
如果您想使用OpenGL进行2D图形处理,最好的选择是SDL。因为如果使用C#和OpenGL,由于会使用.NET包装器,所以可移植性会受到影响。 XNA是用于使用C#编写游戏的好工具,它应该比SDL提供更高的速度和灵活性(特别是.NET端口),并且还有更多功能(但也更加臃肿)。
对于您的剪切或滚动问题,最好的方法是滚动。当您使用GDI+(即System.Drawing使用的技术)进行绘制时,内存不是太大的问题,而CPU则更重要。如果地图非常大,您可以将其分成几个部分进行滚动,然后在必要时加载。

你说得对!我设法让裁剪算法运行起来了,但它甚至比之前的还要慢。然后我让滚动算法工作起来了(比我想象的要容易),看起来速度更快了(尽管它搞砸了缩放 - 现在我在缩放时重新绘制大位图,这会导致操作卡顿一秒钟)。感谢提供SDL链接,我会研究一下的。 - rtm
我听说过XNA。我几天前尝试安装它,但安装失败了,于是我就忘了。它真的值得吗?在我看来,它只是另一个“游戏制作者”类型的程序。 - rtm
不不不,XNA 确实 是一种更加用户友好的框架,但这并不意味着它与 GameMaker 有任何相似之处。看看一些教程,让自己感受一下它的工作方式,然后自行决定。正如我所说,它应该比 SDL 快得多,并提供了更多功能。此外,XNA 是为 C# 设计的 :) - Blam
我明白了!昨天试图重新安装它 - 发现问题在于我正在使用的是VS2010,而XNA3.1是为VS2008设计的。不过我想我在另一台计算机上安装了VS2008。我会去那里看看XNA。 - rtm

2
你计划使用什么应用程序框架?在WinForms(Win32)和WPF之间,有效绘图的技术非常不同。
你说得对,.NET绘图例程没有充分利用GPU。使用DirectX或OpenGL,一个立即的优化是使用图像列表或显示列表将所有图像块(或至少是立即视图区域所需的所有块加上一点)预加载到GPU内存中。然后,您将通过索引在表面上绘制瓷砖-在x,y处绘制瓷砖N。这通常比使用存储在主系统内存中的位图在GPU表面上绘制位图要快得多,因为每个绘制的瓷砖都必须将位图像素复制到GPU中,并且这需要大量时间。通过索引绘制还可以在输出中多次使用相同的图像块时使用更少的GPU内存。
OpenGL与DirectX取决于您的选择,但在我看来,DirectX正在以更快的速度发展,提供比OpenGL更多的硬件加速功能。Windows上的OpenGL驱动程序也因被硬件供应商忽略而声名狼藉。他们的主要重点是他们的DirectX驱动程序。
请考虑您是否真的需要OpenGL或DirectX来运行2D瓷砖应用程序。需要OpenGL或DirectX将减少可以运行您的应用程序的机器数量,特别是旧机器。OpenGL和DirectX可能过度了。如果您聪明地使用它,可以使用普通的GDI完成很多工作。
在您有一个真正好的理由去那里并且具有一些线程经验之前,请远离多线程。多线程为某些计算情况提供了一些性能提升的奖励,但也带来了新的头痛和新的性能问题。确保收益显着,然后再签署所有这些新头痛之前。
总的来说,在屏幕上移动像素通常不适合多线程。您只有一个显示设备(在大多数情况下),因此同时用多个线程尝试更改像素不起作用。借用项目管理中的表达式:如果一个女人可以在9个月内生一个孩子,那么你可以用9个女人在1个月内创造1个孩子吗?; >
努力确定系统中不需要访问相同资源或设备的部分。这些比将瓷砖拼贴到屏幕上更适合在并行线程中运行的候选项。
最佳优化是发现不需要完成的工作-例如减少重新绘制瓷砖的次数,或更改为索引模型,以便可以从GPU内存而不是系统内存中绘制瓷砖。

我正在使用Windows Forms。在你提到它之前,我甚至不知道WPF的存在。我看到它与Forms相似,只是它使用DirectX。也许我会尝试将我的应用程序移植到WPF,看看会发生什么。
我确实使用GDI将位图加载到主内存中-我会阅读并尝试将它们加载到GPU中。在你和Blam的回复之间,我认为我会选择DirectX,除非在GDI中可能存在内存加载。我现在会避免多线程。
非常感谢您的快速而良好的回复!
- rtm
因为WinForms应用程序只是Win32应用程序,所以每当其被遮挡然后再次暴露时,它们将被提示重新绘制其窗口区域。大多数窗口绘制代码采用简单的方法,只是重新绘制窗口中的所有内容。为了获得更好的性能,特别是在大量绘图负载下,您应该注意剪辑矩形并仅重新绘制实际无效的窗口部分。这对于滚动也很有用,因为向上滚动10个像素仅使窗口底部的10个像素无效,而不是整个窗口。 - dthorpe
WPF使用完全不同的绘图范式,并需要非常不同的优化技术。WPF缓存绘制的图像,以便在未覆盖时快速重新绘制,而无需调用您的代码。在WPF应用程序中,没有WM_PAINT循环(您参与其中)。WPF可能在其绘图下使用DirectX,因此与WinForms相比,您可能更有机会利用GPU资源使用普通的WPF控件和对象。 - dthorpe
为了获得更好的性能,特别是在大量绘图负载下,您应该注意裁剪矩形,并且只重新绘制实际无效的窗口部分。这就是我所做的。事实上,它运行得相当快。我刚刚解决了两个问题:以前,我每次绘制都要重新缩放所有位图 - 现在我只使用DrawUnscaled;我还修复了一个错误,我忘记清除列表,随着时间的推移,某些精灵会被绘制数百次。有了这些修复,它运行得很快。如果你聪明地使用它,你可以用普通的GDI做很多事情。- 你说得对。 - rtm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接