在 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/