Loading Skybox Texture From Disk at Runtime

Hello, I’m trying to enable the user to select their own custom skybox texture from a local file on windows and linux. So far I seem to be able to get a Texture2D and set it using Texture2D.LoadImage(data) and Material.SetTexture(“_Tex”, texture), but this doesn’t work for the skybox because it needs a cubemap rather than a 2D texture. I’ve tried using Graphics.ConvertTexture() but it failed. Does anyone have suggestions on how to import a cubemap texture at runtime in standalone, or other solutions for user-customizable skybox?

I honestly don’t know, but what about using the 6 sided skybox variant instead of a cubemap?

I ended up importing an inverted-cubemap formatted png (see example attached) as Texture2D and using the following method to convert it to a cubemap, then set the skybox material to the resulting cube. It’s pretty slow, but this operation will only be done rarely by the operator.

public static Cubemap CubemapFromTexture2D(Texture2D texture)
    {
        int cubedim = texture.width / 4;
        Cubemap cube = new Cubemap(cubedim, TextureFormat.ARGB32, false);
        cube.SetPixels(texture.GetPixels(2 * cubedim, 2 * cubedim, cubedim, cubedim), CubemapFace.NegativeY);
        cube.SetPixels(texture.GetPixels(3 * cubedim, cubedim, cubedim, cubedim), CubemapFace.PositiveX);
        cube.SetPixels(texture.GetPixels(2 * cubedim, cubedim, cubedim, cubedim), CubemapFace.PositiveZ);
        cube.SetPixels(texture.GetPixels(cubedim, cubedim, cubedim, cubedim), CubemapFace.NegativeX);
        cube.SetPixels(texture.GetPixels(0, cubedim, cubedim, cubedim), CubemapFace.NegativeZ);
        cube.SetPixels(texture.GetPixels(2 * cubedim, 0, cubedim, cubedim), CubemapFace.PositiveY);
        cube.Apply();
        return cube;
    }

1 Like

Another hacky way of doing it, store cubemap in alternate 2d form (paraboloid, ocahedron, lat long) and use a sampling cubemap (ie a cubemap that store UV projection of the 2d form) to get the correct pixel. Basically at run time, load the sampling cubemap, use that to sample the 2d form, swap the 2d form when needed. Projecting the 2d for is as simple as uvwrapping a sphere onto the 2d projection data, bake a cubemap of the uv rendering of that sphere at edit time.

Just for future reference (2019.4 LTS).
Set material shader as Skybox/Panoramic and change at runtime with material.SetTexture(“_MainTex”, texture);

6876596--803033--upload_2021-2-25_17-5-43.png

1 Like

Sorry, I didn’t understand how to use the function CubemapFromTexture2D.
I am forced to use a Texture, not Texture2D.
I currently try to load an image from disk and I think it also loads.
In case of an error instead I assign the default texture to my variable “texture”.

I assign the new texture with
Material oam = RenderSettings.skybox;
oam.SetTexture(“_Tex”, texture);

If I try to do
oam.SetTexture(“_Tex”, CubemapFromTexture2D(texture));
I get error in compilation.

Instead with the old code, if I assign the original variable everything works correctly, but if I load a new image, even if it is the same one, I get Error assigning 2D texture to CUBE texture property ‘_Tex’: Dimensions must match

If you want to serialize / deserialize a cubemap with this inspector format

Don’t forget to make your cubemap read/write!

You could write some functions to refactor the code for each CubmapFace but i think its unnecessary for this.

Other tips : If you intend to save and load this at runtime in a game then consider deserializing the data using the job system and then finally assigning the cubemap face arrays to the cubemap on the mainthread then apply it. It would be faster especially if you intend to use 8K cubemaps etc.

Then you use some C# code like this to do it

        private byte[] Serialize(Cubemap cubemap)
        {
            if (cubemap == null)
            {
                return null;
            }
      
            using (var stream = new MemoryStream())
            {
                using (var writer = new BinaryWriter(stream))
                {
                    int mipCount = cubemap.mipmapCount;

                    writer.Write(cubemap.width);
                    writer.Write(cubemap.graphicsFormat.ToString());
                    writer.Write(mipCount);
              
                    for (int i = 0; i < mipCount; i++)
                    {
                        var data = cubemap.GetPixelData<Color32>(i, CubemapFace.PositiveX);
                        writer.Write(data.Length);
                        foreach(var color32 in data)
                        {
                            writer.Write(color32.r);
                            writer.Write(color32.g);
                            writer.Write(color32.b);
                            writer.Write(color32.a);
                        }

                        data = cubemap.GetPixelData<Color32>(i, CubemapFace.NegativeX);
                        writer.Write(data.Length);
                        foreach(var color32 in data)
                        {
                            writer.Write(color32.r);
                            writer.Write(color32.g);
                            writer.Write(color32.b);
                            writer.Write(color32.a);
                        }
                  
                        data = cubemap.GetPixelData<Color32>(i, CubemapFace.PositiveY);
                        writer.Write(data.Length);
                        foreach(var color32 in data)
                        {
                            writer.Write(color32.r);
                            writer.Write(color32.g);
                            writer.Write(color32.b);
                            writer.Write(color32.a);
                        }
                  
                        data = cubemap.GetPixelData<Color32>(i, CubemapFace.NegativeY);
                        writer.Write(data.Length);
                        foreach(var color32 in data)
                        {
                            writer.Write(color32.r);
                            writer.Write(color32.g);
                            writer.Write(color32.b);
                            writer.Write(color32.a);
                        }
                  
                        data = cubemap.GetPixelData<Color32>(i, CubemapFace.PositiveZ);
                        writer.Write(data.Length);
                        foreach(var color32 in data)
                        {
                            writer.Write(color32.r);
                            writer.Write(color32.g);
                            writer.Write(color32.b);
                            writer.Write(color32.a);
                        }
                  
                        data = cubemap.GetPixelData<Color32>(i, CubemapFace.NegativeZ);
                        writer.Write(data.Length);
                        foreach(var color32 in data)
                        {
                            writer.Write(color32.r);
                            writer.Write(color32.g);
                            writer.Write(color32.b);
                            writer.Write(color32.a);
                        }
                    }
            
                    return stream.ToArray();
                }
            }
        }
  
        private Cubemap Deserialize(byte[] data)
        {
            using (var stream = new MemoryStream(data))
            {
                using (var reader = new BinaryReader(stream))
                {
                    int width = reader.ReadInt32();
                    var format =  ParseEnum<GraphicsFormat>(reader.ReadString());
                    int mipCount = reader.ReadInt32();

                    Cubemap cubemap = new Cubemap(width, format, TextureCreationFlags.MipChain, mipCount);

                    for (int i = 0; i < mipCount; i++)
                    {
                        Color32[] color32Array = new Color32[reader.ReadInt32()];
                        for (int j=0; j<color32Array.Length; j++)
                        {
                            color32Array[j] = new Color32(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte());
                        }
                        cubemap.SetPixelData(color32Array, i, CubemapFace.PositiveX);
                  
                  
                        color32Array = new Color32[reader.ReadInt32()];
                        for (int j=0; j<color32Array.Length; j++)
                        {
                            color32Array[j] = new Color32(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte());
                        }
                        cubemap.SetPixelData(color32Array, i, CubemapFace.NegativeX);
                  
                        color32Array = new Color32[reader.ReadInt32()];
                        for (int j=0; j<color32Array.Length; j++)
                        {
                            color32Array[j] = new Color32(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte());
                        }
                        cubemap.SetPixelData(color32Array, i, CubemapFace.PositiveY);
                  
                        color32Array = new Color32[reader.ReadInt32()];
                        for (int j=0; j<color32Array.Length; j++)
                        {
                            color32Array[j] = new Color32(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte());
                        }
                        cubemap.SetPixelData(color32Array, i, CubemapFace.NegativeY);

                        color32Array = new Color32[reader.ReadInt32()];
                        for (int j=0; j<color32Array.Length; j++)
                        {
                            color32Array[j] = new Color32(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte());
                        }
                        cubemap.SetPixelData(color32Array, i, CubemapFace.PositiveZ);
                  
                        color32Array = new Color32[reader.ReadInt32()];
                        for (int j=0; j<color32Array.Length; j++)
                        {
                            color32Array[j] = new Color32(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte());
                        }
                        cubemap.SetPixelData(color32Array, i, CubemapFace.NegativeZ);
                    }

                    cubemap.Apply(false);
                    return cubemap;
                }
            }
        }

private T ParseEnum<T>(string value)
{
    return (T)System.Enum.Parse(typeof(T), value, true);
}