BlendedTexture2D






4.80/5 (2投票s)
一个RenderTarget2D派生类,用于在运行时混合纹理,并处理一些常见的棘手问题。
引言
BlendedTexture2D
是一个相当精简的XNA类,它派生自RenderTarget2D
(因此也是Texture2D
),用于处理在屏幕外缓冲区绘图以生成动态纹理时遇到的一些棘手问题。源码包含完整的BlendedTexture2D.cs类文件和一个简单的演示程序。左键单击可在棋盘格上进行绘制,然后惊叹地发现它能在最小化和恢复时毫发无损,甚至能从一个显示器拖到另一个显示器。其余文章将探讨其开发过程中的障碍以及我选择的解决方案。
在XNA 4.0中,RenderTarget2D
成为了Texture2D
的子类;BlendedTexture2D
依赖于这种关系,在早期版本的框架中将无法编译。
背景
问题
我与XNA中的渲染目标首次互动是在XNA 3.0中实现“在实时渲染的纹理模型上绘画”的功能,使用类似Photoshop的画笔。这涉及到了渲染目标的管理,以便首先绘制到屏幕外缓冲区。GraphicsDevice
的重置,最常见的情况是窗口最小化或在显示器之间拖动,会导致纹理恢复到其原始、未修改的状态,因此必须处理这些情况。我编写了一些代码,足以演示该功能,然后就暂时搁置了一段时间。
几年后,我正在开发一个关于猪的模拟游戏。这些猪有一个基础纹理,并且可以在上面绘制任何大小相同、alpha混合的叠加层(如瘙痒、腹泻等)。问题在于,我们当时正在使用Sunburn游戏引擎,并且不倾向于为了处理一个到四个纹理并按顺序绘制而匆忙编写一个Sunburn兼容的着色器。这似乎是一个绝佳的机会,可以重新打磨旧的“喷漆器”代码,并将其泛化成更性感、更优雅、最重要的是更少繁琐重用的东西。
隆重推出 BlendedTexture2D
目标是提供一个简单、清晰的接口,将多个纹理堆叠成一个Texture2D
。然后,我们可以将该纹理作为标准SunburnLightingEffect
的参数传递(BlendedTexture2D
本身不依赖于Sunburn)。过程中第一个,也是最明显的需要内部化的部分是精灵批处理和渲染目标管理,当绘制到屏幕外缓冲区时。紧随其后的是处理图形设备重置。当然,在此过程中还出现了其他问题。
渲染目标(内部处理)
这很直接,但在实际应用中,我们会很快遇到问题。我们的BlendedTexture2D
会清除到那种可爱的紫色,就像所有缓冲区在图形设备上激活时一样。顺便说一下,这包括前缓冲和后缓冲,您可以在新的XNA项目中通过在Draw(…)
中注释掉GraphicsDevice.Clear(…)
来快速确认——它的初始内容是默认的紫色。此外,RenderTarget2D
无法将自身绘制到自身(我们的API通过将其转换为Texture2D
并传递给AddDecal(…)
来显式允许这一点),如果我们尝试这样做,将会抛出异常。我们可以使用第二个RenderTarget2D
一石二鸟:我们将现有纹理绘制到它上面,然后在上面绘制贴花,然后切换回我们的BlendedTexture2D
并将最终结果绘制到其缓冲区中。因为绘制调用不一定立即发生,所以我们不能立即丢弃_tempBuffer
;因此,我将其作为一个字段维护了一个持久的RenderTarget2D
。
设备重置(内部处理)
许多事情都可能导致设备重置,包括最小化游戏窗口、运行另一个全屏的Direct3D应用程序,或在显示器之间拖动游戏窗口。发生这种情况时,我们所有的缓冲区(包括_tempBuffer
和我们的BlendedTexture2D
)都会被清除为基础的紫色。解决方案很简单:处理GraphicsDevice.DeviceResetting
将我们缓冲区的原始内容复制到系统内存,然后处理GraphicsDevice.DeviceReset
将其复制回来。GetData(…)
和SetData(…)
被认为是你能执行的最慢的图形操作之一,但一点点性能上的停顿总比丢失所有动态纹理要好。真正大量使用BlendedTexture2D
时,在恢复图形设备时可能需要一个简短的用户友好的加载指示器。如果系统内存有限,这可能也会成为一个问题。
设备重置:替代解决方案
在设备重置后重新创建动态纹理还有另一种解决方案,那就是实际进行一步一步的重新创建。最容易想到的朴素解决方案是存储基础纹理的引用以及你想要的关于贴花的任何信息(纹理引用、位置、缩放等),并在重置后迭代绘制它们。在大多数情况下,我预计它会更快。此外,它比我选择的解决方案更能处理设备丢失(见下文)。
话虽如此,我选择了GetData(…)
/SetData(…)
解决方案,因为
- 它更简单。
- 它不涉及从我们完全不受管理的
BlendedTexture2D
任意数量的对ContentManager
管理的内容的引用。 - 我不必解决重新创建
BlendedTexture2D
递归绘制到自身,或另一个BlendedTexture2D
等棘手问题。
调用SetData(…)在活动资源上(内部处理)
我在设备重置期间遇到了不一致的异常,抱怨说“在资源被主动设置在GraphicsDevice
上时,您可能无法调用SetData
。在调用SetData
之前,请先将其从设备中取消设置。”通过检查GraphicsDevice.Textures
中我们的纹理并简单地移除它来避免此问题。
// Make sure this isn't one of the active texture resources.
// 16 is a magic number that should apply to all ps_2_0 and ps_3_0 GPUs.
for (int i = 0; i < 16; i++)
{
if (GraphicsDevice.Textures[i] == this)
{
GraphicsDevice.Textures[i] = null;
}
}
设备丢失
有一样东西是BlendedTexture2D
绝对无法恢复的:完全的设备丢失,例如用户锁定计算机、移动主显示器、重置任何活动显示器的分辨率等。不幸的是,这将丢弃所有RenderTarget2D
的缓冲区,而不给游戏备份的机会。GraphicsDevice
重置的“替代解决方案”更能应对设备丢失,尽管即使那样,如果BlendedTexture2D
外部有大量代码来监控GraphicsDevice
的状态并相应地采取行动,其运行也会更顺畅。
接下来该何去何从?
我没有在AddDecal(…)
中实现完整的SpriteBatch.Draw(Texture2D, …)
参数行,因为我觉得它不方便,但这无疑会使它更灵活。完全设备丢失(较少见的)问题仍未解决。能够在AddDecal(...)
调用中设置一个 alpha 透明度级别,或者将其作为BlendedTexture2D
的属性,那将非常有用。不要求一个任意大小的BlendedTexture2D
有一个初始的Texture2D
基础纹理可能很有用,尽管我没有这个需求。
我还没有机会确认BlendedTexture2D
在MonoGame下的行为是否完全相同。
使用代码
API
除了从RenderTarget2D
继承的成员
public class BlendedTexture2D : RenderTarget2D
// Public Constructor
public BlendedTexture2D(Texture2D baseTexture, bool isDataCopied = true)
// Public Methods
public void AddDecal(Texture2D decal, …) // Draws decal over existing BlendedTexture2D
public Texture2D BakeToTexture() // Returns non-volatile copy of BlendedTexture2D
// Public Events
ContentRegained // Raised when the BlendedTexture2D internally recovers from device reset
重要提示:如果在着色器/Effect
中使用BlendedTexture2D
作为参数,您必须处理BlendedTexture2D.ContentRegained
并将纹理重新分配给Effect
(该Effect
在设备重置期间已丢失对易失性纹理的句柄)。
示例
示例应用程序的摘录
初始化纹理和BlendedTexture2D
// Loading the checkerboard texture to initialize blendedTexture from
Texture2D background = Content.Load<texture2d>("checkerboard");
_blendedTexture = new BlendedTexture2D(background);
// Precalculating distance from upper left corner to center of stamp,
// since we want it to appear centered on the cursor.
_stampTexture = Content.Load<texture2d>("stamp");
_stampOffset = new Vector2(-_stampTexture.Width / 2, -_stampTexture.Height / 2);
添加贴花
Vector2 stampPos = new Vector2(currentMouseState.X, currentMouseState.Y) + _stampOffset;
_blendedTexture.AddDecal(_stampTexture, stampPos);
将BlendedTexture2D
作为Texture2D
传递给Draw(…)
调用
spriteBatch.Begin();
spriteBatch.Draw(_blendedTexture, Vector2.Zero, Color.White);
spriteBatch.End();
最终评论
希望您觉得这篇文章有用或有趣。如果您对源码或文章内容本身有任何疑问或建议,请随时告诉我!
历史
- 2013年3月25日 - 原始提交。