C#在Unity中非阻塞地加载和复制大型Texture2D

15

我正在为安卓构建Unity应用程序,其中需要动态加载大量的大型纹理(所有图像均为超过6MB的png格式)。这些纹理可以来自Amazon S3服务器,此时它们会作为流到达,或来自用户的设备本身。

在两种情况下,我都能异步获取原始数据或纹理而没有问题。对于第一种情况,我查询服务器并获取带有数据流的回调;对于第二种情况,我使用WWW类获取纹理,利用"file://"协议。

问题在于,当我想要将此数据复制到Texture2D中以便在某个地方使用(例如私有成员的Texture2D上)时,就会出现问题。

对于流,我将其转换为byte[]并尝试调用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似乎不喜欢在主线程之外的线程上运行这些操作,并且当我尝试时会抛出异常。

是否有一种方法可以将这些操作分块,以便它需要多个帧?还是进行某种快速的memcopy操作,不会使主线程停顿?

提前感谢!

PS:我已经在以下存储库中创建了一个解决问题的示例: https://github.com/NeoSouldier/Texture2DTest/


你在什么时候执行这个操作?你能否将它隐藏在加载屏幕或介绍之后呢? - Absinthe
3个回答

13

当下载大型纹理时,www.texture 会导致打嗝。

你应该尝试以下几点:

1.使用 WWWLoadImageIntoTexture 函数,它将已下载数据中的图像替换为现有 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。这个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
}

1
哇,@程序员,这是一个令人难以置信的答案!非常感谢您帮助我正确提出问题并给出深思熟虑的答案。不幸的是,我尝试了前4种方法,但没有成功。我正在尝试第5种方法,它似乎非常简单、干净和有前途,但我遇到了一个奇怪的问题。只是迭代并调用www.texture.GetPixel()会导致应用程序在几次迭代后崩溃(如果我添加一些Debug.Log(),它会更快地崩溃)- 我已经注释掉了SetPixel()以避免混淆 =/ - Arthur
1
我提供的第5个代码在我的端上百分之百可行。如果您像我一样将其保留为2个单独的代码,则更易读。请按照我的答案进行测试,不要更改任何内容并说它无法工作。在那段代码中没有地方是我执行了 tempTexture = www.texture;,所以请不要添加。GetPixel();GetPixel(); 代替了它。逐个复制像素后,调用 m_myTexture.Apply(); 将更改应用于目标纹理。您不需要 tempTexture = www.texture;,因为这会替换它。 - Programmer
1
好的!我可以回报,使用http请求确实可以在PC和Android上正常工作!看起来你的解决方案完美地运行,直到我尝试从设备本身抓取文件时出现问题,换句话说,当我使用“WWW www = new WWW(“file://”+ filePath)”时。这种奇怪的崩溃只会在文件来自设备本身时发生。我猜这是Unity的一个bug,你同意吗? - Arthur
1
抱歉没有早些回复,我的笔记本电脑没电了,我不得不回家。 - Arthur
1
我以为你已经修复了这个问题。在阅读了你的评论后,我不知道从哪里开始回答。该函数已在我的端上进行了测试,并且正常工作。我认为唯一帮助你的方法是创建一个简单的项目并尝试在其中复制此问题。之后,将项目上传到另一个网站并提供链接。我会查看并找出问题所在。如果没有访问项目的权限,我们都将浪费时间。 - Programmer
显示剩余23条评论

4
最终,这个问题通过创建一个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/


1
问题是,在创建Texture2D时,无论使用哪种方法,Unity都会始终将整个图像加载到内存中。这需要时间,并且没有办法避免它。它不会解析文件并获取图像数据的位,或每帧缓慢加载。这发生在Unity中任何实例化的东西上,无论是图像、地形、由Instantiate()创建的对象等。
如果您仅需要某些处理的图像数据,我建议使用类似于libjpeg或libpng(在其C#版本中)的库在另一个线程中获取数据(只要不调用Unity方法,就可以使用另一个线程),但如果您必须显示它,我看不到停止滞后的方法。

1
不是真的,有一些方法可以避免延迟。 - Programmer
例如什么? - OnionFan
请查看我的答案#4。它将允许随时间加载纹理。 - Programmer
@程序员,是不是_myTexture.Apply()这个操作最耗费资源?即使你每帧只设置一个像素,但调用Apply()仍会导致卡顿。 - Ruslan L.
@RuslanL。Apply操作虽然昂贵,但并非最昂贵的操作。我不会在每一帧都调用它。我会在所有SetPixel循环完成后调用它,这是可以接受的。 - Programmer
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接