Generate mipmaps at runtime for a texture loaded with UnityWebRequest?

How can I generate mipmaps for a texture loaded from UnityWebRequest at runtime? Please note while I use ‘www’ as a reference, it is not using the WWW class.

I’m currently loading a texture from a UnityWebRequest using this code:

IEnumerator DownloadImage()
   {
       string url = videoThumbnailURL;
       UnityWebRequest www = UnityWebRequestTexture.GetTexture(url);
       DownloadHandler handle = www.downloadHandler;

       yield return www.SendWebRequest();
       if (www.isHttpError || www.isNetworkError)
       {
//do my error stuff
       }
       else
       {
           //Load Image
           Texture2D texture2d = DownloadHandlerTexture.GetContent(www);

           Sprite sprite = null;
           sprite = Sprite.Create(texture2d, new Rect(0, 0, texture2d.width, texture2d.height), Vector2.zero);

           if (sprite != null)
           {
               videoThumbnailImage.sprite = sprite;
           }
       }
   }

Which loads the texture but obviously doesn’t generate mipmaps. I’ve tried using

texture2d.SetPixels(texture2d.GetPixels(0, 0, texture2d.width, texture2d.height));
texture2d.Apply(true);

… per an old Unity forum discussion but that didn’t seem to work. Any thoughts?

1 Like

If you do the “texture2d.Apply(true);” it should generate mipmaps… or at least that was my understanding.

I’ve tried adding that below line 15 like this but it didn’t work.

//Load Image
Texture2D texture2d = DownloadHandlerTexture.GetContent(www);
texture2d.SetPixels(texture2d.GetPixels(0, 0, texture2d.width, texture2d.height));
texture2d.Apply(true);

Hmm, as far as I knew that should do it, but perhaps there is more to it. Here is a relevant unity answers question you might check out:

Try adding this line after the GetContent() call:

Debug.Log("MipMap Count = " + texture2d.mipmapCount);

If that prints 0 into the log, then the Texture2D that GetContent is creating does not have mip maps enabled, so nothing you do after that will make it work as there’s no way to enable mipmaps post creation of the Texture2D object. You’ll likely need to do something like this:

Texture2D wwwTex = DownloadHandlerTexture.GetContent(www);
Texture2D newTex = new Texture2D(wwwTex.width, wwwTex.height);
newTex.SetPixels(wwwTex.GetPixels(0));
newTex.Apply(); // default is to update mipmaps

You could also use newTex.Resize(wwwTex.width, wwwTex.height); and reuse the “newTex” Texture2D rather than creating a new one every time.

4 Likes
loadedTexture = new Texture2D(wwwTexture.width, wwwTexture.height, wwwTexture.format, true);
loadedTexture.LoadImage(uwr.downloadHandler.data);

I use dynamic load texture with mipmap in our main project and this works on my side.

wwwTexture is the texture you get from UnityWebRequest uwr, and loadedTexture is a new Texture2D, which is also the result texture with mipmap.

SetPixel should not be needed, at least not on my side. You may want to call Apply().

7 Likes

Thank you all for the help - this solution worked for me!

Hey, please note, there is Graphics.CopyTexture API which is much more effective than Get\SetPixels or LoadImage, when supported.

Here is an example:

if (SystemInfo.copyTextureSupport != CopyTextureSupport.None)
{
    Graphics.CopyTexture(wwwTexture, 0, 0, mipTexture, 0, 0); // copies mip 0
}
else
{
    mipTexture.LoadImage(www.downloadHandler.data);
}

mipTexture.Apply(true); // generates all other mips from mip 0

In my case, Get\SetPixels() took ~64k ticks, LoadImage took ~34k ticks, where Graphics.CopyTexture took only ~8k ticks.

6 Likes

Ok, whoa. Your blowing my mind right now. I went with a compute shader because it performed way better than getpixel and setpixel apply methods - but if what you say is true, then compatibility would be way better across more devices. I’m going to experiment with this, so thanks so much for sharing!

OK @cdytoby 's solution looks good but Unity why is there no DownloadHandlerTexture.GetContent(UnityWebRequest www, bool generateMipMaps) ?

It seems such an obvious option!

2 Likes

@codestage you’re explicitly copying mip 0. Do you have to deal with copying each individual mip if you want the final texture to support them?

@bgolus Am I correct that the DownloadHanderTexture only supports jpg and png and therefore does not support mipmaps at all?
Which in case of low performance devices is really bad because LoadImage has a huge impact on performance and using it continuously is not an option.
So in essence, there is no way you get a mipmapped texture out of a download unless you spend CPU on it, am I right?

Yes, you need to copy all mips to the final texture and do not call Apply after that on such texture. Here is a quote from Texture2D.Apply() API reference:

And from Graphics.CopyTexture API:

AFAIK, this is correct.

No, it’s not something you should be using continuously even on desktop. It’s something to use every so often to load a texture once. After that you use the Texture2D asset it creates. You don’t need to call it every frame to use a single if that’s what you’re thinking. Also decompressing jpg and png files is surprisingly slow, even on high end PCs. These are formats that were designed around reducing the file size as much as possible, not necessarily for real time usage.

If you’re looking to load .jpg or .png files from a web address, yes. You can pack all of the mipmaps into a single texture, like this example from the wikipedia article on mipmaps.

But you’d still need to use Graphics.CopyTexture() to copy the sections of the image that are for each mip into the new texture. Similar to the examples from @codestage but using a single texture rather than one for each mip.

However I think the real answer you might be looking for is don’t use DownloadHanderTexture at all. Use DownloadHandlerAssetBundle and create asset bundles with pre-compressed texture asset with mips already in it. That’ll be far more efficient to load.

1 Like

@bgolus Thanks for your awesome answer. I thought so. It’s actually not that easy :smile: Right now we are faking sharing a browser screen from one device to the others by reading its texture continuously and sending them over the internet. the clients basically start loading the next just after the previous image was finished. This is totally insane, I know, but for now its fine until we have a real streaming solution.

Some additional thoughts.

The fastest way to copy a Texture2D on the CPU (assuming identical dimensions) between two readable textures is by calling

textureDestination.LoadRawTextureData(
textureSource.GetRawTextureData<byte>())

avoid using GetPixels() or even the untemplated textureSource.GetRawTextureData() because these will create a copy first, potentially doing a conversion from Color32 to Color, before you then copy that again into the destination with SetPixels/LoadRawTextureData(). Using the approach above is 20x faster on the CPU than calling SetPixels(GetPixels) on each mip.

So if you have a texture without mipmaps, you can copy it on the CPU, then Apply(true) to copy it to the GPU resource and generate the mipmaps at the same time.

//texture with mips
textureDestination = new Texture2D(width,height, format, true);
//copy on the CPU
textureDestination.LoadRawTextureData(
textureSource.GetRawTextureData<byte>())
//copy to GPU and generate mips
textureDestination.Apply(true)

If you will not change the texture anymore then you can use
textureSource.Apply(true,true)
This will make the texture unreadable on the CPU and remove the CPU copy (and memory usage) after it is uploaded/copied to video memory.

Graphics.CopyTexture() is only meant to copy between the textures’ GPU resources. It is not intended to keep the CPU copies in sync. Apply should indeed not be used on a destination texture after you call Graphics.CopyTexture().

5 Likes

Hi, I’m trying to use this approach to create a texture with mipmaps at runtime, but when I call the LoadRawTextureData method I’ve the following error.

UnityException: LoadRawTextureData: not enough data provided (will result in overread).

I guess that’s due to the different size of textures, the destination one having room for mipmaps, the source one not.

Moreover, shouldn’t the Apply method be called on the destination texture rather than on the source, like this?

//copy to GPU and generate mips
textureDestination.Apply(true)

Anyway, the problem is the error on the LoadRawTextureData, have you some hints about it?

Hi Riccardo,
you are right, I had a typo, the last Apply should indeed be called on the destination texture to upload the new content to the GPU. And you are also right that this only works if the source and destination have exactly the same raw data size. So it won’t work if the mips or dimensions are different. It’s probably possible to get a sub array and load that one in the very specific case that the destination has less mipmaps.

1 Like

Thank you for your answer.

In fact, it’s the source the one with no mipmaps, not the destination. Anyway I tried to get the subarray and load it into the destination texture like this:

 //texture with mips
textureDestination = new Texture2D(width,height, format, true);

//copy on the CPU
textureDestination.LoadRawTextureData(textureSource.GetRawTextureData<byte>().GetSubArray(0, width*height));

//copy to GPU and generate mips
textureDestination.Apply(true);

But I get the same error as before:

UnityException: LoadRawTextureData: not enough data provided (will result in overread).

So far, of all the proposed methods, the only one that worked for me is:

textureSource = DownloadHandlerTexture.GetContent(webRequest);
textureDestination = new Texture2D(textureSource.width, textureSource.height, textureSource.format, true);
textureDestination.SetPixels32(textureSource.GetPixels32(0), 0);
textureDestination.Apply(true);

Of course, at the price of a noticeable hiccup.

It would be great to have a solid, fast and smooth way to create a texture with mipmaps at runtime, that’s a really important feature for a game engine.

Any further ideas?

GetPixelData should be the fastest approach in this case.
You can find a sample project comparing the different methods and a jobyfied approach here.

1 Like