Nominom / BCnEncoder.NET

Cross-platform texture encoding libary for .NET. With support for BC1-3/DXT, BC4-5/RGTC and BC6-7/BPTC compression. Outputs files in ktx or dds formats.
The Unlicense
108 stars 16 forks source link

Support for Unity3D #48

Closed ponahoum closed 3 years ago

ponahoum commented 3 years ago

Hi,

I currently use Unity which only supports .NET Standard 2.0 Dlls, is making the library compatible with .NET Standard 2.0 on the plans?

Thank you!

Nominom commented 3 years ago

Hey,

I have not personally tested this, but have you tried changing the Unity scripting runtime to 4.x?

I'll get back to this tomorrow if that doesn't solve it.

ponahoum commented 3 years ago

@Nominom thanks for your fast answer!

Yes, i also tried this, and I always end up with the following error:

Error: Could not load signature of BCnEncoder.Shared.Bc6Block:StoreIndices due to: Could not resolve type with token 0100001b (from typeref, class/assembly System.Span`1, netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51) assembly:netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 type:System.Span`1 member:(null) signature:<none>

I feel like System.Span is not available in stable versions of Unity as mentioned here : Unity Forum. It should be available in Unity 2022 with the support of .Net Standard 2.1 but we're far from a stable release of this from unity.

ponahoum commented 3 years ago

Quick update, I could import the library opening and building the net4.5 branch of the repo. Still using .net standard 2.0 but it works this time! Thank you :)

Edit: the library loads but can't use it properly with Unity, see below

Nominom commented 3 years ago

Yeah, there's a Net45 version available. It just hasn't been updated for a while and is missing some features. If it works enough for you then you can use that, but let me know if you run into any problems with it.

ponahoum commented 3 years ago

@Nominom re-opening the issue because I don't manage to make the library work correctly on Unity using KTX+BC1. Even though after a bit of battle the dlls load into Unity, I can't load the texture in Unity.

Here's an sample code that creates a BC1+KTX texture using a source PNG file, then try to load the generated .KTX into a Unity Texture2D object:

    public Texture2D loadedTexture;
    public string textureInput = @"C:\Users\Pierre-Olivier\Desktop\example.png";
    public string textureOutput = @"C:\Users\Pierre-Olivier\Desktop\example.ktx";

    void SaveTexture()
    {
        using (SixLabors.ImageSharp.Image<SixLabors.ImageSharp.PixelFormats.Rgba32> image = SixLabors.ImageSharp.Image.Load<SixLabors.ImageSharp.PixelFormats.Rgba32>(textureInput))
        {
            if (image.TryGetSinglePixelSpan(out var pixelSpan))
            {
                byte[] rgbaBytes = MemoryMarshal.AsBytes(pixelSpan).ToArray();
                BcEncoder encoder = new BcEncoder();

                encoder.OutputOptions.quality = CompressionQuality.Fast;
                encoder.OutputOptions.format = CompressionFormat.BC1;
                encoder.OutputOptions.fileFormat = OutputFileFormat.Ktx;

                using (FileStream fs = File.OpenWrite(textureOutput))
                {
                    encoder.Encode(rgbaBytes, image.Width, image.Height, fs);
                }
            }
        }
    }

    void LoadTexture()
    {
        BcDecoder decoder = new BcDecoder();
        using (FileStream fs = File.OpenRead(textureOutput))
        {
            var decompressedTexture = decoder.Decode(fs);
            loadedTexture = new Texture2D(decompressedTexture.Width, decompressedTexture.Height, TextureFormat.DXT1, false);
            loadedTexture.LoadRawTextureData(decompressedTexture.data);

            // Uploads texture from CPU to GPU
            loadedTexture.Apply();
        }
    }

The texture loads using LoadRawTextureData (no crash or exception, so the arrays must be right-sized) but doesn't look like anything in Unity. I also had to use TryGetSinglePixelSpan to get the bytes array to create the .ktx file, maybe I'm doing something wrong on this side.

Any clue?

Thank you!

Nominom commented 3 years ago

If I'm reading this correctly, looks like you're telling Unity that the input data would be in Dxt1 format, but the BcDecoder.Decode method will turn the given file back to Rgba format.

If you want the raw Dxt encoded data from the Ktx file, you can use KtxFile.Load to first load the file, then get the Dxt1 data from ktxfile.MipMaps[0].Faces[0].data

If you're looking to do this at run-time, there should be a EncodeToRawBytes method in the Encoder class to just get the Dxt1 data without saving it to a file first.

The ImageSharp part looks correct to me.

ponahoum commented 3 years ago

@Nominom thank you that's awesome, I managed to make it work and loaded a KTX DXT1 texture directly into Unity! One question though: is there some way to add some loss to the compression? For my textures, I'm currently using the crunch algorithm (.CRN) which compresses a 2K png texture into a 500kb file using DXT1 with a bit of loss. The same texture gives me a 2MB .KTX and I wish I could reduce this a bit. I tried the different settings (CompressionQuality) but none seem to have an noticeable effect.

Thanks again and here's some sample code for those coming after me.

    void SaveTexture()
    {
        using (SixLabors.ImageSharp.Image<SixLabors.ImageSharp.PixelFormats.Rgba32> image = SixLabors.ImageSharp.Image.Load<SixLabors.ImageSharp.PixelFormats.Rgba32>(textureInput))
        {
            if (image.TryGetSinglePixelSpan(out var pixelSpan))
            {
                byte[] rgbaBytes = MemoryMarshal.AsBytes(pixelSpan).ToArray();
                BcEncoder encoder = new BcEncoder();

                encoder.OutputOptions.quality = CompressionQuality.Fast;
                encoder.OutputOptions.format = CompressionFormat.BC1;
                encoder.OutputOptions.fileFormat = OutputFileFormat.Ktx;
                using (FileStream fs = File.OpenWrite(textureOutput))
                {
                    KtxFile file = encoder.EncodeToKtx(rgbaBytes, image.Width, image.Height);
                    file.Write(fs);
                }
            }
        }
    }

    void LoadTexture()
    {
        using (FileStream fs = File.OpenRead(textureOutput))
        {
            KtxFile file = KtxFile.Load(fs);
            KtxMipmap mipmap = file.MipMaps[0];
            byte[] rawData = mipmap.Faces[0].Data;
            loadedTexture = new Texture2D((int) mipmap.Width, (int)mipmap.Height, TextureFormat.DXT1, false);
            loadedTexture.LoadRawTextureData(rawData);

            // Uploads texture from CPU to GPU
            loadedTexture.Apply();
        }
    }
Nominom commented 3 years ago

Hmm,

My first thought about the extra size was the MipMaps, but if you're using the Net45 version it does not support them. DXT1 is a fixed-size algorithm so the quality of the encoding does not affect the file-size either. It could be a bug in the Net45 version, or if you got the real version working turn off the MipMap generation from encoder OutputOptions.

I can investigate it better in the weekend when I have a bit more free time.

ponahoum commented 3 years ago

@Nominom Unfortunately, I didn't manage to make the new version working with Unity, still because of the same error:

TypeLoadException: Could not resolve type with token 0100001b (from typeref, class/assembly System.Span`1, netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51)
BCnEncoder.Shared.ImageFiles.KtxFile.Load (System.IO.Stream s) (at <44e4fb09b69c444a9c0353d346f2f4fb>:0)

It would be very appreciated to have the latest version compatible with Net4.x or Net Standard 2.0 as Unity only supports those for now. In any case, thank you very much for your support :)

Nominom commented 3 years ago

I just did a quick calculation and the file size seems to be actually correct. One pixel takes up half a byte of storage with DXT1, so 2048x2048x0.5 should be about 2MB. Are you sure the crunch compression isn't doing anything else to the texture, like resizing, zipping or using a different algorithm?

You could also experiment with .NET's DeflateStream or GZipStream to try and bring the file-size down.

ponahoum commented 3 years ago

@Nominom After a bit of research, as mentioned in the Binomial doc, the CRN is an already highly compressed format. Adding a strong GZip compression on top of my 2K KTX files brings the files down to 250/400kb, which is around the same thing as crunch. I'm closing the ticket as you answered all of the questions I had, and I thank you once again for your support !

Nominom commented 3 years ago

I'm glad if I could help.

Adding full support for Standard 2.0 is currently quite of a hassle, so I'll just hope Unity hops on board the new .NET 5+ train soon. I might have to do it if enough people have a need for it though.

If you run into any more problems, don't hesitate to ask!