在 OpenGL ES for Android 中运行时创建大型纹理的最有效方法

Most efficient way of creating large textures at runtime in OpenGL ES for Android

本文关键字:大型纹理 有效 方法 创建 运行时 ES OpenGL for Android      更新时间:2023-10-16

我正在开发一个用Unity3D构建的Android应用程序,该应用程序需要在运行时根据不同的图像像素数据创建新纹理。

由于 Unity for Android 使用 OpenGL ES,而我的应用程序是一个图形应用程序,需要以理想的每秒 60 帧的速度运行,因此我创建了一个在 OpenGL 代码上运行的C++插件,而不仅仅是使用 Unity 的 Texture2D 慢速纹理构造。该插件允许我将像素数据上传到新的OpenGL纹理,然后通过其Texture2D的CreateExternalTexture()函数告知Unity。

不幸的是,由于在此设置中运行的OpenGL ES版本是单线程的,因此为了使事情在帧中保持运行,我使用已经生成TextureID进行了glTexImage2D()调用,但在第一帧中使用空数据。然后用我的像素数据缓冲区的一部分调用glTexSubImage2D(),在多个后续帧上填充整个纹理,本质上是同步进行纹理创建,但将操作分块到多个帧上!

现在,我遇到的问题是,每次我创建一个大尺寸的新纹理时,第一次glTexImage2D()调用仍然会导致帧输出,即使我在其中放置了空数据。我猜造成这种情况的原因是,在第一次glTexImage2D()调用时,后台仍然有一个相当大的内存分配,即使我直到以后才填充图像。

不幸的是,我正在为其创建纹理的这些图像具有不同的大小,我事先不知道,所以我不能只是在加载时预先创建一堆纹理,我需要每次为每个新纹理指定新的宽度和高度。 =(

无论如何,我可以避免这种内存分配,也许在开始时分配一个巨大的内存块并将其用作新纹理的池?我已经阅读了周围,人们似乎建议使用 FBO 的?我可能误解了,但在我看来,在将纹理附加到 FBO 之前,您仍然需要执行 glTexImage2D() 调用来分配纹理?

欢迎任何和所有的建议,提前感谢! =)

PS:我没有图形背景,所以我不知道OpenGL或其他图形库的最佳实践,我只是尝试在运行时创建新纹理而不取景!

我还没有处理过你面临的具体问题,但我发现纹理池在 OpenGL 中非常有用,无需花太多心思即可获得有效的结果。

就我而言,问题是我不能对延迟着色器的输入使用相同的纹理,因为用于输出结果的纹理。然而,我经常想这样做:

// Make the texture blurry.
blur(texture);

然而,我不得不创建 11 种不同分辨率的不同纹理,并且必须在它们之间切换,作为水平/垂直模糊着色器的输入和输出与 FBO 以获得体面的模糊。我从来都不太喜欢 GPU 编程,因为我遇到过的一些最复杂的状态管理经常在那里。由于着色器的纹理输入不能用作纹理输出这一基本要求,我需要去绘图板只是为了弄清楚如何最小化分配的纹理数量,这感觉非常错误。

所以我创建了一个纹理池和OMG,它简化了很多事情!它使我可以左右创建临时纹理对象而不必担心它,因为销毁纹理对象实际上并没有调用glDeleteTextures,它只是将它们返回到池中。所以我终于能够做到:

blur(texture);

。正如我一直想要的那样。出于某种有趣的原因,当我开始越来越多地使用游泳池时,它加快了帧速率。我想即使我花了所有心思来最小化分配的纹理数量,我仍然以消除池的方式分配了比我需要的更多的东西(请注意,实际的真实示例所做的不仅仅是模糊,包括 DOF、泛光、旁路、低通、CMAA 等,GLSL 代码实际上是基于可视化编程语言动态生成的,用户可以使用该语言来创建新的着色器苍蝇)。

所以我真的建议从探索这个想法开始。听起来这对您的问题很有帮助。就我而言,我使用了这个:

struct GlTextureDesc
{
...
};

。考虑到我们可以指定多少纹理参数(像素格式、颜色分量数量、LOD 级别、宽度、高度等),这是一个相当庞大的结构。

然而,该结构是可比较和可哈希的,最终被用作哈希表中的键(如unordered_multimap)以及实际的纹理句柄作为关联的值。

这允许我们这样做:

// Provides a pool of textures. This allows us to conveniently rapidly create,
// and destroy texture objects without allocating and freeing an excessive number 
// of textures.
class GlTexturePool
{
public:
// Creates an empty pool.
GlTexturePool();
// Cleans up any textures which haven't been accessed in a while.
void cleanup();
// Allocates a texture with the specified properties, retrieving an existing 
// one from the pool if available. The function returns a handle to the
// allocated texture.
GLuint allocate(const GlTextureDesc& desc);
// Returns the texture with the specified key and handle to the pool.
void free(const GlTextureDesc& desc, GLuint texture);
private:
...
};

此时我们可以左右创建临时纹理对象,而不必担心过多调用glTexImage2DglDeleteTextures.我发现它非常有帮助。

最后值得注意的是cleanup上面的功能。当我将纹理存储在哈希表中时,我会在它们上放置时间戳(使用系统实时)。我定期调用此清理函数,然后扫描哈希表中的纹理并检查时间戳。如果他们只是坐在游泳池中闲置了一段时间(比如 8 秒),我会打电话给glDeleteTextures并将它们从游泳池中移除。我使用单独的线程和条件变量来构建纹理列表,以便在下次有效上下文可用时通过定期扫描哈希表来删除,但是如果您的应用程序都是单线程的,您可能只是每隔几秒钟在主循环中调用此清理函数。

也就是说,我在视觉特效部门工作,它的实时要求不像AAA级游戏那么严格。在我的领域,人们更关注离线渲染,而我离 GPU 向导还很远。可能有更好的方法来解决这个问题。但是,我发现从这个纹理池开始非常有帮助,我认为这对您的情况也可能有所帮助。而且实施起来相当微不足道(只花了我半个小时左右)。

如果您请求分配/释放的纹理大小、格式和参数到处都是,这仍然可能最终分配和删除大量纹理。在那里,统一一下可能会有所帮助,例如至少使用 POT(两个幂)大小等,并决定要使用的最小像素格式数量。就我而言,这不是什么大问题,因为我只使用一种像素格式,而我想创建的大多数纹理临时文件正好是放大到天花板 POT 的视口大小。

至于 FBO,我不确定它们如何帮助您解决过度纹理分配/释放的直接问题。我将它们主要用于延迟着色,以便在以合成风格的方式以应用于生成的 2D 纹理的多个通道渲染几何体后,对 DOF 等效果进行后期处理。我自然地使用 FBO,但我想不出 FBO 如何立即减少您必须分配/解除分配的纹理数量,除非您可以将一个大纹理与 FBO 一起使用并将多个输入纹理渲染到屏幕外输出纹理。在这种情况下,FBO不会直接提供帮助,而只是能够创建一个巨大的纹理,您可以将其部分用作输入/输出,而不是许多较小的纹理。