在 C# for Unity 中非阻塞加载和复制大型 Texture2D
Non-blocking loading and copying of large Texture2D's in C# for Unity
我正在为 Android 构建一个 Unity 应用程序,它处理动态加载大量大纹理(所有图像的大小都超过 6MB,如 png)。这些纹理可以来自 Amazon S3 服务器,在这种情况下它们作为流到达,也可以来自用户的设备本身。
在这两种情况下,我都能够毫无问题地异步获取原始数据或纹理。在第一个中,我查询服务器并获得数据流的回调,在第二个中,我使用 WWW class 通过 "file://" 协议获取纹理。
当我想将此数据复制到 Texture2D 到我可以使用的某个地方时,例如复制到 Texture2D 私有成员上,问题就发生了。
对于流,我将其转换为字节[] 并尝试调用 LoadImage(),对于 WWW class,我只是尝试使用 myTexture = www.texture 复制它。两次我都在加载或复制纹理时得到一个巨大的框架。我想根除这个框架,因为该应用程序无法随附它。
using (var stream = responseStream)
{
byte[] myBinary = ToByteArray(stream);
m_myTexture.LoadImage(myBinary); // Commenting this line removes frame out
}
...
WWW www = new WWW("file://" + filePath);
yield return www;
m_myTexture = www.texture; // Commenting this line removes frame out
不幸的是,Unity 似乎不喜欢 运行 这些操作在与主线程不同的线程上进行,因此当我尝试时会抛出异常。
是否有任何方法可以将这些操作分块以便需要多个帧?或者做一些不会拖延主线程的快速内存复制操作?
提前致谢!
PS:我在以下 repo 中创建了一个问题的工作示例:https://github.com/NeoSouldier/Texture2DTest/
问题是,无论使用何种方法,当您创建 Texture2D 时,Unity 总是会在内存中加载整个图像。这需要时间,而且没有办法避免。它不会解析文件并获取图像数据位,也不会每帧缓慢加载。这发生在 Unity 中的任何实例化中,无论是图像、地形、由 Instantiate() 创建的对象等。
如果您只需要图像数据进行某些处理,我建议使用像 libjpeg 或 libpng 这样的库(在它的 C# 版本中)在另一个线程中获取数据(您可以使用另一个线程,只要你不调用 Unity 方法),但如果你必须显示它,我看不出有什么方法可以阻止延迟。
众所周知,www.texture
在下载大型纹理时会导致打嗝。
你应该尝试的事情:
1。使用 WWW's
LoadImageIntoTexture
函数用下载数据中的图像替换现有 Texture2D
的内容。如果问题仍然未解决,请继续阅读。
WWW www = new WWW("file://" + filePath);
yield return www;
///////m_myTexture = www.texture; // Commenting this line removes frame out
www.LoadImageIntoTexture(m_myTexture);
2。使用www.textureNonReadable
变量
使用 www.textureNonReadable
而不是 www.texture
也可以加快加载时间。我时常看到这种情况发生。
3。使用函数Graphics.CopyTexture
从一个纹理复制到另一个纹理。这应该很快。如果问题仍然未解决,请继续阅读。
//Create new Empty texture with size that matches source info
m_myTexture = new Texture2D(www.texture.width, www.texture.height, www.texture.format, false);
Graphics.CopyTexture(www.texture, m_myTexture);
4。使用 Unity 的 UnityWebRequest
API. This replaced the WWW
class. You must have Unity 5.2 and above in order to use this. It has GetTexture
优化下载纹理的功能。
using (UnityWebRequest www = UnityWebRequest.GetTexture("http://www.my-server.com/image.png"))
{
yield return www.Send();
if (www.isError)
{
Debug.Log(www.error);
}
else
{
m_myTexture = DownloadHandlerTexture.GetContent(www);
}
}
如果上述三种方案都不能解决卡顿问题,另一种解决方案是在协程函数中用GetPixel
and SetPixel
函数逐个复制像素。您添加一个计数器并设置您希望它等待的时间。随着时间的推移,它间隔了纹理复制。
5.用GetPixel
and SetPixel
函数一个一个复制Texture2D
个像素。示例代码包括来自 Nasa 的 8K 纹理,用于测试目的。复制 Texture
时不会阻塞。如果是,请减小 copyTextureAsync
函数中 LOOP_TO_WAIT
变量的值。您还可以选择提供一个函数,当完成复制 Texture
.
时将调用该函数
public Texture2D m_myTexture;
void Start()
{
//Application.runInBackground = true;
StartCoroutine(downloadTexture());
}
IEnumerator downloadTexture()
{
//http://visibleearth.nasa.gov/view.php?id=79793
//http://eoimages.gsfc.nasa.gov/images/imagerecords/79000/79793/city_lights_africa_8k.jpg
string url = "http://eoimages.gsfc.nasa.gov/images/imagerecords/79000/79793/city_lights_africa_8k.jpg";
//WWW www = new WWW("file://" + filePath);
WWW www = new WWW(url);
yield return www;
//m_myTexture = www.texture; // Commenting this line removes frame out
Debug.Log("Downloaded Texture. Now copying it");
//Copy Texture to m_myTexture WITHOUT callback function
//StartCoroutine(copyTextureAsync(www.texture));
//Copy Texture to m_myTexture WITH callback function
StartCoroutine(copyTextureAsync(www.texture, false, finishedCopying));
}
IEnumerator copyTextureAsync(Texture2D source, bool useMipMap = false, System.Action callBack = null)
{
const int LOOP_TO_WAIT = 400000; //Waits every 400,000 loop, Reduce this if still freezing
int loopCounter = 0;
int heightSize = source.height;
int widthSize = source.width;
//Create new Empty texture with size that matches source info
m_myTexture = new Texture2D(widthSize, heightSize, source.format, useMipMap);
for (int y = 0; y < heightSize; y++)
{
for (int x = 0; x < widthSize; x++)
{
//Get color/pixel at x,y pixel from source Texture
Color tempSourceColor = source.GetPixel(x, y);
//Set color/pixel at x,y pixel to destintaion Texture
m_myTexture.SetPixel(x, y, tempSourceColor);
loopCounter++;
if (loopCounter % LOOP_TO_WAIT == 0)
{
//Debug.Log("Copying");
yield return null; //Wait after every LOOP_TO_WAIT
}
}
}
//Apply changes to the Texture
m_myTexture.Apply();
//Let our optional callback function know that we've done copying Texture
if (callBack != null)
{
callBack.Invoke();
}
}
void finishedCopying()
{
Debug.Log("Finished Copying Texture");
//Do something else
}
最终通过创建一个 C++ 插件(通过 Android Studio 2.2 构建)解决了这个问题,该插件使用 "stb_image.h" 加载图像,并使用 OpenGL 生成纹理并映射一组在多个帧上将扫描线扫描到纹理上。然后通过Texture2D.CreateExternalTexture().
将贴图交给Unity
此方法不会使工作异步,而是将加载成本分摊到多个帧上,移除同步块和后续帧输出。
我无法使纹理创建异步,因为为了使 OpenGL 功能正常工作,您需要 运行 来自 Unity 主渲染线程的代码,因此必须通过 GL.IssuePluginEvent() - Unity 的文档使用以下项目来解释如何使用此功能:https://bitbucket.org/Unity-Technologies/graphicsdemos/
我已经清理了我正在处理的测试 repo,并在 README 中编写了说明,以便尽可能容易地理解我得到的最终解决方案。我希望它在某些时候对某些人有用,并且他们不必像我那样花那么长时间来解决这个问题! https://github.com/NeoSouldier/Texture2DTest/
我正在为 Android 构建一个 Unity 应用程序,它处理动态加载大量大纹理(所有图像的大小都超过 6MB,如 png)。这些纹理可以来自 Amazon S3 服务器,在这种情况下它们作为流到达,也可以来自用户的设备本身。
在这两种情况下,我都能够毫无问题地异步获取原始数据或纹理。在第一个中,我查询服务器并获得数据流的回调,在第二个中,我使用 WWW class 通过 "file://" 协议获取纹理。
当我想将此数据复制到 Texture2D 到我可以使用的某个地方时,例如复制到 Texture2D 私有成员上,问题就发生了。
对于流,我将其转换为字节[] 并尝试调用 LoadImage(),对于 WWW class,我只是尝试使用 myTexture = www.texture 复制它。两次我都在加载或复制纹理时得到一个巨大的框架。我想根除这个框架,因为该应用程序无法随附它。
using (var stream = responseStream)
{
byte[] myBinary = ToByteArray(stream);
m_myTexture.LoadImage(myBinary); // Commenting this line removes frame out
}
...
WWW www = new WWW("file://" + filePath);
yield return www;
m_myTexture = www.texture; // Commenting this line removes frame out
不幸的是,Unity 似乎不喜欢 运行 这些操作在与主线程不同的线程上进行,因此当我尝试时会抛出异常。
是否有任何方法可以将这些操作分块以便需要多个帧?或者做一些不会拖延主线程的快速内存复制操作?
提前致谢!
PS:我在以下 repo 中创建了一个问题的工作示例:https://github.com/NeoSouldier/Texture2DTest/
问题是,无论使用何种方法,当您创建 Texture2D 时,Unity 总是会在内存中加载整个图像。这需要时间,而且没有办法避免。它不会解析文件并获取图像数据位,也不会每帧缓慢加载。这发生在 Unity 中的任何实例化中,无论是图像、地形、由 Instantiate() 创建的对象等。
如果您只需要图像数据进行某些处理,我建议使用像 libjpeg 或 libpng 这样的库(在它的 C# 版本中)在另一个线程中获取数据(您可以使用另一个线程,只要你不调用 Unity 方法),但如果你必须显示它,我看不出有什么方法可以阻止延迟。
众所周知,www.texture
在下载大型纹理时会导致打嗝。
你应该尝试的事情:
1。使用 WWW's
LoadImageIntoTexture
函数用下载数据中的图像替换现有 Texture2D
的内容。如果问题仍然未解决,请继续阅读。
WWW www = new WWW("file://" + filePath);
yield return www;
///////m_myTexture = www.texture; // Commenting this line removes frame out
www.LoadImageIntoTexture(m_myTexture);
2。使用www.textureNonReadable
变量
使用 www.textureNonReadable
而不是 www.texture
也可以加快加载时间。我时常看到这种情况发生。
3。使用函数Graphics.CopyTexture
从一个纹理复制到另一个纹理。这应该很快。如果问题仍然未解决,请继续阅读。
//Create new Empty texture with size that matches source info
m_myTexture = new Texture2D(www.texture.width, www.texture.height, www.texture.format, false);
Graphics.CopyTexture(www.texture, m_myTexture);
4。使用 Unity 的 UnityWebRequest
API. This replaced the WWW
class. You must have Unity 5.2 and above in order to use this. It has GetTexture
优化下载纹理的功能。
using (UnityWebRequest www = UnityWebRequest.GetTexture("http://www.my-server.com/image.png"))
{
yield return www.Send();
if (www.isError)
{
Debug.Log(www.error);
}
else
{
m_myTexture = DownloadHandlerTexture.GetContent(www);
}
}
如果上述三种方案都不能解决卡顿问题,另一种解决方案是在协程函数中用GetPixel
and SetPixel
函数逐个复制像素。您添加一个计数器并设置您希望它等待的时间。随着时间的推移,它间隔了纹理复制。
5.用GetPixel
and SetPixel
函数一个一个复制Texture2D
个像素。示例代码包括来自 Nasa 的 8K 纹理,用于测试目的。复制 Texture
时不会阻塞。如果是,请减小 copyTextureAsync
函数中 LOOP_TO_WAIT
变量的值。您还可以选择提供一个函数,当完成复制 Texture
.
public Texture2D m_myTexture;
void Start()
{
//Application.runInBackground = true;
StartCoroutine(downloadTexture());
}
IEnumerator downloadTexture()
{
//http://visibleearth.nasa.gov/view.php?id=79793
//http://eoimages.gsfc.nasa.gov/images/imagerecords/79000/79793/city_lights_africa_8k.jpg
string url = "http://eoimages.gsfc.nasa.gov/images/imagerecords/79000/79793/city_lights_africa_8k.jpg";
//WWW www = new WWW("file://" + filePath);
WWW www = new WWW(url);
yield return www;
//m_myTexture = www.texture; // Commenting this line removes frame out
Debug.Log("Downloaded Texture. Now copying it");
//Copy Texture to m_myTexture WITHOUT callback function
//StartCoroutine(copyTextureAsync(www.texture));
//Copy Texture to m_myTexture WITH callback function
StartCoroutine(copyTextureAsync(www.texture, false, finishedCopying));
}
IEnumerator copyTextureAsync(Texture2D source, bool useMipMap = false, System.Action callBack = null)
{
const int LOOP_TO_WAIT = 400000; //Waits every 400,000 loop, Reduce this if still freezing
int loopCounter = 0;
int heightSize = source.height;
int widthSize = source.width;
//Create new Empty texture with size that matches source info
m_myTexture = new Texture2D(widthSize, heightSize, source.format, useMipMap);
for (int y = 0; y < heightSize; y++)
{
for (int x = 0; x < widthSize; x++)
{
//Get color/pixel at x,y pixel from source Texture
Color tempSourceColor = source.GetPixel(x, y);
//Set color/pixel at x,y pixel to destintaion Texture
m_myTexture.SetPixel(x, y, tempSourceColor);
loopCounter++;
if (loopCounter % LOOP_TO_WAIT == 0)
{
//Debug.Log("Copying");
yield return null; //Wait after every LOOP_TO_WAIT
}
}
}
//Apply changes to the Texture
m_myTexture.Apply();
//Let our optional callback function know that we've done copying Texture
if (callBack != null)
{
callBack.Invoke();
}
}
void finishedCopying()
{
Debug.Log("Finished Copying Texture");
//Do something else
}
最终通过创建一个 C++ 插件(通过 Android Studio 2.2 构建)解决了这个问题,该插件使用 "stb_image.h" 加载图像,并使用 OpenGL 生成纹理并映射一组在多个帧上将扫描线扫描到纹理上。然后通过Texture2D.CreateExternalTexture().
将贴图交给Unity此方法不会使工作异步,而是将加载成本分摊到多个帧上,移除同步块和后续帧输出。
我无法使纹理创建异步,因为为了使 OpenGL 功能正常工作,您需要 运行 来自 Unity 主渲染线程的代码,因此必须通过 GL.IssuePluginEvent() - Unity 的文档使用以下项目来解释如何使用此功能:https://bitbucket.org/Unity-Technologies/graphicsdemos/
我已经清理了我正在处理的测试 repo,并在 README 中编写了说明,以便尽可能容易地理解我得到的最终解决方案。我希望它在某些时候对某些人有用,并且他们不必像我那样花那么长时间来解决这个问题! https://github.com/NeoSouldier/Texture2DTest/