【问题标题】:Non-blocking loading and copying of large Texture2D's in C# for Unity在 C# for Unity 中非阻塞加载和复制大型 Texture2D
【发布时间】:2017-03-19 22:50:30
【问题描述】:

我正在为 Android 构建一个 Unity 应用程序,该应用程序处理动态加载大量大纹理(所有图像的大小都超过 6MB,如 png 的)。这些纹理可以来自 Amazon S3 服务器,在这种情况下,它们以流的形式到达,也可以来自用户的设备本身。

在这两种情况下,我都能毫无问题地异步获取原始数据或纹理。在第一个中,我查询服务器并使用数据流获取回调,在第二个中,我使用 WWW 类通过“file://”协议获取纹理。

只要我想将此数据复制到 Texture2D 到某个我可以使用的地方,例如复制到 Texture2D 私有成员,问题就会发生。

我将流转换为字节[] 并尝试调用 LoadImage(),而对于 WWW 类,我只需尝试使用 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/

【问题讨论】:

  • 你在什么时候这样做?你能把它隐藏在加载屏幕或简介后面吗?

标签: c# android performance unity3d texture2d


【解决方案1】:

众所周知,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。这取代了 WWW 类。您必须拥有 Unity 5.2 及更高版本才能使用它。它具有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);
    }
}

如果以上三个选项都没有解决冻结问题,另一种解决方案是在带有GetPixelSetPixel 函数的协程函数中一个一个地复制像素。您添加一个计数器并设置您希望它等待的时间。随着时间的推移,它会间隔纹理复制。

5. 使用GetPixelSetPixel 函数逐一复制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
}

【讨论】:

  • 哇,这是一个令人难以置信的答案@Programmer!非常感谢您帮助我提出正确的问题,以及您深思熟虑的答案。所以不幸的是,我尝试了前 4 种方法,但没有运气。我正在尝试第 5 种方法,它看起来非常简单、干净且有前途,但我遇到了一个奇怪的问题。简单地迭代和调用 www.texture.GetPixel() 会导致应用程序在几次迭代后崩溃(不超过 8 次,如果我坚持一些 Debug.Log(),它会更快崩溃) - 我已经评论了 SetPixel( ) 以避免混淆=/
  • 如果我注释掉 www.texture.GetPixel() (这显然不是一个实际的解决方案),或者如果不是调用 www.texture.GetPixel,我可以阻止应用程序崩溃(),我之前尝试做一个副本:“tempTexture = www.texture; ... tempTexture.GetPixel();” - 但这显然不是一个解决方案,因为它重新引入了原来的“myTexture = www.texture;”问题。所以我相信在 Android 上的 www.texture 上调用 GetPixel() 的组合可能是这里的问题,虽然我不知道为什么 =/.您是否尝试在 Android 上运行您的代码?非常感谢!
  • 我提供的#5 代码在我这边工作 %100。如果您像我一样将其保留在 2 个单独的代码中,则更具可读性。请像我的回答一样对其进行测试。不要改变任何东西并说它不起作用。我在tempTexture = www.texture; 所做的代码中没有任何地方,所以请不要添加。 GetPixel();GetPixel(); 取代了它。将像素一一复制后,调用m_myTexture.Apply(); 将更改应用到目标纹理。您确实不需要需要tempTexture = www.texture;,因为它会取代它。
  • 好的!所以我可以通过 http 请求报告它确实可以在 PC 和 Android 上工作!在我尝试使用从设备本身抓取文件之前,您的解决方案似乎可以完美运行,换句话说,当我使用'WWW www = new WWW("file://" + filePath)' 时。只有当文件来自设备本身时,才会发生这种奇怪的崩溃。我想那是 Unity 中的一个错误,你不同意吗?
  • 抱歉没有早点回复,我的笔记本没电了,我不得不回家了。
【解决方案2】:

最终,通过创建一个 C++ 插件(通过 Android Studio 2.2 构建)解决了这个问题,该插件使用“stb_image.h”加载图像,使用 OpenGL 生成纹理并将一组扫描线映射到多个纹理上帧。然后通过 Texture2D.CreateExternalTexture() 将纹理交给 Unity。

此方法不会使工作异步,而是将加载成本分散到多个帧上,移除同步块和后续帧。

我无法使纹理创建异步,因为要使 OpenGL 函数工作,您需要从 Unity 的主渲染线程运行代码,因此必须通过 GL.IssuePluginEvent() - Unity 调用函数文档使用以下项目来解释如何使用此功能:https://bitbucket.org/Unity-Technologies/graphicsdemos/

我已经清理了我正在处理的测试存储库,并在 README 中编写了说明,以便尽可能容易地理解我得出的最终解决方案。我希望它在某些时候对某人有用,并且他们不必花费我所做的一切来解决这个问题! https://github.com/NeoSouldier/Texture2DTest/

【讨论】:

    【解决方案3】:

    问题是,当你创建一个 Texture2D 时,Unity 总是将整个图像加载到内存中,无论使用什么方法。这需要时间,而且没有办法避免。它不会解析文件并获取图像数据的位,或者每帧加载缓慢。这发生在 Unity 中任何事物的实例化中,无论是图像、地形、由 Instantiate() 创建的对象等。

    如果您只需要图像数据进行某些处理,我建议使用 libjpeg 或 libpng 之类的库(以 C# 形式)在另一个线程中获取数据(只要您不这样做,您就可以使用另一个线程'不调用 Unity 方法),但如果你必须显示它,我看不出有什么方法可以阻止延迟。

    【讨论】:

    • 不正确,您可以使用一些方法来避免延迟
    • 例如什么?
    • 查看我的答案 #4。它会随着时间的推移加载纹理。
    • 抱歉,找不到,可以链接吗?
    • 看看我回答中的#5。它会随着时间的推移加载图像。
    猜你喜欢
    • 1970-01-01
    • 2016-07-06
    • 1970-01-01
    • 2021-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-24
    相关资源
    最近更新 更多