diff --git a/Assets/Plugins/asyncImageLoader.meta b/Assets/Plugins/asyncImageLoader.meta new file mode 100644 index 0000000..098f824 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 70cc050fb208c444687afffe93a677fa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/AsyncImageLoader.Runtime.asmdef b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.Runtime.asmdef new file mode 100644 index 0000000..c085569 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.Runtime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "AsyncImageLoader.Runtime", + "references": [ + "Unity.Burst", + "Unity.Mathematics" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Plugins/asyncImageLoader/AsyncImageLoader.Runtime.asmdef.meta b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.Runtime.asmdef.meta new file mode 100644 index 0000000..443e9cb --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a8cf6376c8aac1e43b688324ad3f22de +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/AsyncImageLoader.cs b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.cs new file mode 100644 index 0000000..5b775aa --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.cs @@ -0,0 +1,140 @@ +using System.Threading.Tasks; +using UnityEngine; + +public static partial class AsyncImageLoader { + /// Load image synchronously. This variant uses the default LoaderSettings. + /// True if the data can be loaded, false otherwise. + public static bool LoadImage(Texture2D texture, byte[] data) { + return LoadImage(texture, data, LoaderSettings.Default); + } + + /// Load image synchronously. This variant is similar to ImageConversion.LoadImage. + /// True if the data can be loaded, false otherwise. + public static bool LoadImage(Texture2D texture, byte[] data, bool markNonReadable) { + var loaderSettings = LoaderSettings.Default; + loaderSettings.markNonReadable = markNonReadable; + return LoadImage(texture, data, loaderSettings); + } + + /// Load image synchronously. This variant accepts a LoaderSettings. Useful for debugging and profiling. + /// True if the data can be loaded, false otherwise. + public static bool LoadImage(Texture2D texture, byte[] data, LoaderSettings loaderSettings) { + try { + if (data == null || data.Length == 0) throw new System.Exception("Input data is null or empty."); + + using (var importer = new ImageImporter(data, loaderSettings)) { + importer.LoadIntoTexture(texture); + } + + return true; + } catch (System.Exception e) { + if (loaderSettings.logException) Debug.LogException(e); + + return false; + } + } + + /// Load image asynchronously. This variant uses the default LoaderSettings. + /// True if the data can be loaded, false otherwise. + public static Task LoadImageAsync(Texture2D texture, byte[] data) { + return LoadImageAsync(texture, data, LoaderSettings.Default); + } + + /// Load image asynchronously. This variant is similar to ImageConversion.LoadImage. + /// True if the data can be loaded, false otherwise. + public static Task LoadImageAsync(Texture2D texture, byte[] data, bool markNonReadable) { + var loaderSettings = LoaderSettings.Default; + loaderSettings.markNonReadable = markNonReadable; + return LoadImageAsync(texture, data, loaderSettings); + } + + /// Load image asynchronously. This variant accepts a LoaderSettings. + /// True if the data can be loaded, false otherwise. + public static async Task LoadImageAsync(Texture2D texture, byte[] data, LoaderSettings loaderSettings) { + try { + if (data == null || data.Length == 0) throw new System.Exception("Input data is null or empty."); + + using (var importer = await Task.Run(() => new ImageImporter(data, loaderSettings))) { + await importer.LoadIntoTextureAsync(texture); + } + + return true; + } catch (System.Exception e) { + if (loaderSettings.logException) Debug.LogException(e); + + return false; + } + } + + /// Create Texture2D from image data synchronously. This variant uses a default LoaderSettings. Linear and mipmap count can be specified. + /// Texture2D object if the data can be loaded, null otherwise. + public static Texture2D CreateFromImage(byte[] data) { + return CreateFromImage(data, LoaderSettings.Default); + } + + + /// Create Texture2D from image data synchronously. This variant accepts a LoaderSettings. Linear and mipmap count can be specified. + /// Texture2D object if the data can be loaded, null otherwise. + public static Texture2D CreateFromImage(byte[] data, LoaderSettings loaderSettings) { + try { + if (data == null || data.Length == 0) throw new System.Exception("Input data is null or empty."); + + using (var importer = new ImageImporter(data, loaderSettings)) { + return importer.CreateNewTexture(); + } + } catch (System.Exception e) { + if (loaderSettings.logException) Debug.LogException(e); + + return null; + } + } + + /// Create Texture2D from image data asynchronously. This variant uses a default LoaderSettings. Linear and mipmap count can be specified. + /// Texture2D object if the data can be loaded, null otherwise. + public static Task CreateFromImageAsync(byte[] data) { + return CreateFromImageAsync(data, LoaderSettings.Default); + } + + /// Create Texture2D from image data asynchronously. This variant accepts a LoaderSettings. Linear and mipmap count can be specified. + /// Texture2D object if the data can be loaded, null otherwise. + public static async Task CreateFromImageAsync(byte[] data, LoaderSettings loaderSettings) { + try { + if (data == null || data.Length == 0) throw new System.Exception("Input data is null or empty."); + + using (var importer = await Task.Run(() => new ImageImporter(data, loaderSettings))) { + return await importer.CreateNewTextureAsync(); + } + } catch (System.Exception e) { + if (loaderSettings.logException) Debug.LogException(e); + + return null; + } + } + + /// Settings used by the image loader. + public struct LoaderSettings { + /// Create linear texture. Only applicable to methods that create new Texture2D. Defaults to false. + public bool linear; + /// Texture data won't be readable on the CPU after loading. Defaults to false. + public bool markNonReadable; + /// Whether or not to generate mipmaps. Defaults to true. + public bool generateMipmap; + /// Automatically calculate the number of mipmap levels. Defaults to true. Only applicable to methods that create new Texture2D. + public bool autoMipmapCount; + /// Mipmap count, including the base level. Must be greater than 1. Only applicable to methods that create new Texture2D. + public int mipmapCount; + /// Used to explicitly specify the image format. Defaults to FIF_UNKNOWN, which the image format will be automatically determined. + public FreeImage.Format format; + /// Whether or not to log exception caught by this method. Defaults to true. + public bool logException; + + public static LoaderSettings Default => new LoaderSettings { + linear = false, + markNonReadable = false, + generateMipmap = true, + autoMipmapCount = true, + format = FreeImage.Format.FIF_UNKNOWN, + logException = true, + }; + } +} diff --git a/Assets/Plugins/asyncImageLoader/AsyncImageLoader.cs.meta b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.cs.meta new file mode 100644 index 0000000..5e371a7 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/AsyncImageLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f4efe3f88c91cd4fa0d8c97b0c9520e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/FreeImage.cs b/Assets/Plugins/asyncImageLoader/FreeImage.cs new file mode 100644 index 0000000..c48237f --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/FreeImage.cs @@ -0,0 +1,105 @@ +using System; +using System.Runtime.InteropServices; + +public static partial class AsyncImageLoader { + public class FreeImage { + public enum Format { + FIF_UNKNOWN = -1, + FIF_BMP = 0, + FIF_ICO = 1, + FIF_JPEG = 2, + FIF_JNG = 3, + FIF_KOALA = 4, + FIF_LBM = 5, + FIF_IFF = FIF_LBM, + FIF_MNG = 6, + FIF_PBM = 7, + FIF_PBMRAW = 8, + FIF_PCD = 9, + FIF_PCX = 10, + FIF_PGM = 11, + FIF_PGMRAW = 12, + FIF_PNG = 13, + FIF_PPM = 14, + FIF_PPMRAW = 15, + FIF_RAS = 16, + FIF_TARGA = 17, + FIF_TIFF = 18, + FIF_WBMP = 19, + FIF_PSD = 20, + FIF_CUT = 21, + FIF_XBM = 22, + FIF_XPM = 23, + FIF_DDS = 24, + FIF_GIF = 25, + FIF_HDR = 26, + FIF_FAXG3 = 27, + FIF_SGI = 28, + FIF_EXR = 29, + FIF_J2K = 30, + FIF_JP2 = 31, + FIF_PFM = 32, + FIF_PICT = 33, + FIF_RAW = 34, + FIF_WEBP = 35, + FIF_JXR = 36 + } + + internal enum Type { + FIT_UNKNOWN = 0, + FIT_BITMAP = 1, + FIT_UINT16 = 2, + FIT_INT16 = 3, + FIT_UINT32 = 4, + FIT_INT32 = 5, + FIT_FLOAT = 6, + FIT_DOUBLE = 7, + FIT_COMPLEX = 8, + FIT_RGB16 = 9, + FIT_RGBA16 = 10, + FIT_RGBF = 11, + FIT_RGBAF = 12 + } + + internal enum ColorType { + FIC_MINISWHITE = 0, + FIC_MINISBLACK = 1, + FIC_RGB = 2, + FIC_PALETTE = 3, + FIC_RGBALPHA = 4, + FIC_CMYK = 5 + } + + const string FreeImageLibrary = "FreeImage"; + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_GetFileTypeFromMemory")] + internal static extern Format GetFileTypeFromMemory(IntPtr memory, int size); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_OpenMemory")] + internal static extern IntPtr OpenMemory(IntPtr data, uint size_in_bytes); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_CloseMemory")] + internal static extern IntPtr CloseMemory(IntPtr data); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_LoadFromMemory")] + internal static extern IntPtr LoadFromMemory(Format format, IntPtr stream, int flags); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_Unload")] + internal static extern void Unload(IntPtr dib); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_ConvertToRawBits")] + internal static extern void ConvertToRawBits(IntPtr bits, IntPtr dib, int pitch, uint bpp, uint red_mask, uint green_mask, uint blue_mask, bool topdown); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_GetWidth")] + internal static extern uint GetWidth(IntPtr handle); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_GetHeight")] + internal static extern uint GetHeight(IntPtr handle); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_GetImageType")] + internal static extern Type GetImageType(IntPtr dib); + + [DllImport(FreeImageLibrary, EntryPoint = "FreeImage_GetBPP")] + internal static extern int GetBPP(IntPtr dib); + } +} diff --git a/Assets/Plugins/asyncImageLoader/FreeImage.cs.meta b/Assets/Plugins/asyncImageLoader/FreeImage.cs.meta new file mode 100644 index 0000000..a550e74 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/FreeImage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: beb5906fae1246642a984f6b9eecc26b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/ImageImporter.cs b/Assets/Plugins/asyncImageLoader/ImageImporter.cs new file mode 100644 index 0000000..fcd7e31 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/ImageImporter.cs @@ -0,0 +1,337 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using Unity.Jobs; +using Unity.Profiling; +using UnityEngine; +using static Unity.Mathematics.math; + +public static partial class AsyncImageLoader { + class ImageImporter : IDisposable { +#if UNITY_2020_1_OR_NEWER + // Maximum texture size supported is 16K + const int MAX_TEXTURE_DIMENSION = 16384; + const int MAX_MIPMAP_COUNT = 15; +#else + // Maximum texture size supported is 8K + const int MAX_TEXTURE_DIMENSION = 8192; + const int MAX_MIPMAP_COUNT = 14; +#endif + + static readonly ProfilerMarker ConstructorMarker = new ProfilerMarker("ImageImporter.Constructor"); + static readonly ProfilerMarker CreateNewTextureMarker = new ProfilerMarker("ImageImporter.CreateNewTexture"); + static readonly ProfilerMarker LoadIntoTextureMarker = new ProfilerMarker("ImageImporter.LoadIntoTexture"); + static readonly ProfilerMarker LoadRawTextureDataMarker = new ProfilerMarker("ImageImporter.LoadRawTextureData"); + static readonly ProfilerMarker ProcessRawTextureDataMarker = new ProfilerMarker("ImageImporter.ProcessRawTextureData"); + + LoaderSettings _loaderSettings; + + IntPtr _bitmap; + int _width; + int _height; + TextureFormat _textureFormat; + int _pixelSize; // In bytes + + JobHandle _finalJob; + + public ImageImporter(byte[] imageData, LoaderSettings loaderSettings) { + using (ConstructorMarker.Auto()) { + _loaderSettings = loaderSettings; + _bitmap = IntPtr.Zero; + + IntPtr memoryStream = IntPtr.Zero; + + try { + memoryStream = FreeImage.OpenMemory( + Marshal.UnsafeAddrOfPinnedArrayElement(imageData, 0), + (uint)imageData.Length + ); + + if (_loaderSettings.format == FreeImage.Format.FIF_UNKNOWN) { + _loaderSettings.format = FreeImage.GetFileTypeFromMemory(memoryStream, imageData.Length); + } + + if (_loaderSettings.format == FreeImage.Format.FIF_UNKNOWN) { + throw new Exception("Cannot automatically determine the image format. Consider explicitly specifying image format."); + } + + _bitmap = FreeImage.LoadFromMemory(_loaderSettings.format, memoryStream, 0); + _width = (int)FreeImage.GetWidth(_bitmap); + _height = (int)FreeImage.GetHeight(_bitmap); + + if (_width > MAX_TEXTURE_DIMENSION || _height > MAX_TEXTURE_DIMENSION) { + Dispose(); + throw new Exception("Texture size exceed maximum dimension supported by Unity."); + } + + DetermineTextureFormat(); + } finally { + if (memoryStream != IntPtr.Zero) FreeImage.CloseMemory(memoryStream); + } + } + } + + public void Dispose() { + if (_bitmap != IntPtr.Zero) { + FreeImage.Unload(_bitmap); + _bitmap = IntPtr.Zero; + } + } + + public Texture2D CreateNewTexture() { + using (CreateNewTextureMarker.Auto()) { + var mipmapCount = CalculateMipmapCount(); + var texture = new Texture2D(_width, _height, _textureFormat, mipmapCount, _loaderSettings.linear); + var rawTextureView = texture.GetRawTextureData(); + LoadRawTextureData(rawTextureView); + ProcessRawTextureData(rawTextureView, mipmapCount); + _finalJob.Complete(); + texture.Apply(false, _loaderSettings.markNonReadable); + return texture; + } + } + + public async Task CreateNewTextureAsync() { + var mipmapCount = CalculateMipmapCount(); + var texture = new Texture2D(_width, _height, _textureFormat, mipmapCount, _loaderSettings.linear); + var rawTextureView = texture.GetRawTextureData(); + await Task.Run(() => LoadRawTextureData(rawTextureView)); + ProcessRawTextureData(rawTextureView, mipmapCount); + while (!_finalJob.IsCompleted) await Task.Yield(); + texture.Apply(false, _loaderSettings.markNonReadable); + return texture; + } + + public void LoadIntoTexture(Texture2D texture) { + using (LoadIntoTextureMarker.Auto()) { + var mipmapCount = CalculateMipmapCount(true); + texture.Resize(_width, _height, _textureFormat, _loaderSettings.generateMipmap); + var rawTextureView = texture.GetRawTextureData(); + LoadRawTextureData(rawTextureView); + ProcessRawTextureData(rawTextureView, mipmapCount); + _finalJob.Complete(); + texture.Apply(false, _loaderSettings.markNonReadable); + } + } + + public async Task LoadIntoTextureAsync(Texture2D texture) { + var mipmapCount = CalculateMipmapCount(true); + texture.Resize(_width, _height, _textureFormat, _loaderSettings.generateMipmap); + var rawTextureView = texture.GetRawTextureData(); + await Task.Run(() => LoadRawTextureData(rawTextureView)); + ProcessRawTextureData(rawTextureView, mipmapCount); + while (!_finalJob.IsCompleted) await Task.Yield(); + texture.Apply(false, _loaderSettings.markNonReadable); + } + + int CalculateMipmapCount(bool forceAutoCount = false) { + if (!_loaderSettings.generateMipmap) return 1; + + var maxDimension = Mathf.Max(_width, _height); + var mipmapCount = Mathf.FloorToInt(Mathf.Log(maxDimension, 2f)) + 1; + mipmapCount = Mathf.Clamp(mipmapCount, 2, MAX_MIPMAP_COUNT); + + if (!_loaderSettings.autoMipmapCount && !forceAutoCount) { + mipmapCount = Mathf.Clamp(_loaderSettings.mipmapCount, 2, mipmapCount); + } + + return mipmapCount; + } + + int2 CalculateMipmapDimensions(int mipmapLevel) { + // Base level + if (mipmapLevel == 0) { + return int2(_width, _height); + } else { + var mipmapFactor = Mathf.Pow(2f, -mipmapLevel); + var mipmapWidth = Mathf.Max(1, Mathf.FloorToInt(mipmapFactor * _width)); + var mipmapHeight = Mathf.Max(1, Mathf.FloorToInt(mipmapFactor * _height)); + return int2(mipmapWidth, mipmapHeight); + } + } + + void DetermineTextureFormat() { + var type = FreeImage.GetImageType(_bitmap); + + switch (type) { + case FreeImage.Type.FIT_BITMAP: + var bpp = FreeImage.GetBPP(_bitmap); + + switch (bpp) { + case 24: + _textureFormat = TextureFormat.RGB24; + _pixelSize = 3; + break; + case 32: + _textureFormat = TextureFormat.RGBA32; + _pixelSize = 4; + break; + default: + throw new Exception($"Bitmap bitdepth not supported: {bpp}"); + } + break; + case FreeImage.Type.FIT_RGB16: + case FreeImage.Type.FIT_RGBF: + _textureFormat = TextureFormat.RGB24; + _pixelSize = 3; + break; + case FreeImage.Type.FIT_RGBA16: + case FreeImage.Type.FIT_RGBAF: + _textureFormat = TextureFormat.RGBA32; + _pixelSize = 4; + break; + default: + throw new Exception($"Image type not supported: {type}"); + } + } + + void LoadRawTextureData(NativeArray rawTextureView) { + using (LoadRawTextureDataMarker.Auto()) { + var mipmapDimensions = CalculateMipmapDimensions(0); + var mipmapSize = mipmapDimensions.x * mipmapDimensions.y; + var mipmapSlice = new NativeSlice(rawTextureView, 0, _pixelSize * mipmapSize); + + unsafe { + FreeImage.ConvertToRawBits( + (IntPtr)mipmapSlice.GetUnsafePtr(), _bitmap, + _pixelSize * mipmapDimensions.x, + _textureFormat == TextureFormat.RGBA32 ? 32u : 24u, + 0, 0, 0, false + ); + } + } + } + + void ProcessRawTextureData(NativeArray rawTextureView, int mipmapCount) { + using (ProcessRawTextureDataMarker.Auto()) { + var mipmapDimensions = CalculateMipmapDimensions(0); + var mipmapSize = mipmapDimensions.x * mipmapDimensions.y; + var mipmapSlice = new NativeSlice(rawTextureView, 0, _pixelSize * mipmapSize); + var mipmapIndex = _pixelSize * mipmapSize; + + _finalJob = new BGRToRGBJob { + textureData = mipmapSlice, + processFunction = _textureFormat == TextureFormat.RGBA32 ? + BGRToRGBJob.BGRA32ToRGBA32FP : BGRToRGBJob.BGR24ToRGB24FP + }.Schedule(mipmapSize, 8192); + + for (int mipmapLevel = 1; mipmapLevel < mipmapCount; mipmapLevel++) { + var nextMipmapDimensions = CalculateMipmapDimensions(mipmapLevel); + mipmapSize = nextMipmapDimensions.x * nextMipmapDimensions.y; + var nextMipmapSlice = new NativeSlice(rawTextureView, mipmapIndex, _pixelSize * mipmapSize); + mipmapIndex += _pixelSize * mipmapSize; + + _finalJob = new FilterMipmapJob { + inputMipmap = mipmapSlice, + inputDimensions = mipmapDimensions, + outputMipmap = nextMipmapSlice, + outputDimensions = nextMipmapDimensions, + processFunction = _textureFormat == TextureFormat.RGBA32 ? + FilterMipmapJob.FilterMipmapRGBA32FP : FilterMipmapJob.FilterMipmapRGB24FP + }.Schedule(mipmapSize, 1024, _finalJob); + + mipmapDimensions = nextMipmapDimensions; + mipmapSlice = nextMipmapSlice; + } + } + } + + [BurstCompile(CompileSynchronously = true)] + struct BGRToRGBJob : IJobParallelFor { + public delegate void BGRToRGBDelegate(ref NativeSlice textureData, int index); + + public static readonly FunctionPointer BGR24ToRGB24FP = BurstCompiler.CompileFunctionPointer(BGR24ToRGB24); + public static readonly FunctionPointer BGRA32ToRGBA32FP = BurstCompiler.CompileFunctionPointer(BGRA32ToRGBA32); + + [BurstCompile(CompileSynchronously = true)] + static void BGR24ToRGB24(ref NativeSlice textureData, int index) { + var temp = textureData[mad(3, index, 0)]; + textureData[mad(3, index, 0)] = textureData[mad(3, index, 2)]; + textureData[mad(3, index, 2)] = temp; + } + + [BurstCompile(CompileSynchronously = true)] + static void BGRA32ToRGBA32(ref NativeSlice textureData, int index) { + var temp = textureData[mad(4, index, 0)]; + textureData[mad(4, index, 0)] = textureData[mad(4, index, 2)]; + textureData[mad(4, index, 2)] = temp; + } + + [NativeDisableParallelForRestriction] + public NativeSlice textureData; + public FunctionPointer processFunction; + + public void Execute(int index) => processFunction.Invoke(ref textureData, index); + } + + [BurstCompile(CompileSynchronously = true)] + struct FilterMipmapJob : IJobParallelFor { + public delegate void FilterMipmapDelegate(ref FilterMipmapJob job, int outputIndex); + + public static readonly FunctionPointer FilterMipmapRGB24FP = BurstCompiler.CompileFunctionPointer(FilterMipmapRGB24); + public static readonly FunctionPointer FilterMipmapRGBA32FP = BurstCompiler.CompileFunctionPointer(FilterMipmapRGBA32); + + [BurstCompile(CompileSynchronously = true)] + static void FilterMipmapRGB24(ref FilterMipmapJob job, int outputIndex) { + var outputX = outputIndex % job.outputDimensions.x; + var outputY = outputIndex / job.outputDimensions.x; + var outputColor = new uint3(); + + for (var offsetY = 0; offsetY < 2; offsetY++) { + for (var offsetX = 0; offsetX < 2; offsetX++) { + var inputX = min(mad(2, outputX, offsetX), job.inputDimensions.x - 1); + var inputY = min(mad(2, outputY, offsetY), job.inputDimensions.y - 1); + var inputIndex = mad(job.inputDimensions.x, inputY, inputX); + outputColor.x += (uint)job.inputMipmap[mad(3, inputIndex, 0)] >> 2; + outputColor.y += (uint)job.inputMipmap[mad(3, inputIndex, 1)] >> 2; + outputColor.z += (uint)job.inputMipmap[mad(3, inputIndex, 2)] >> 2; + } + } + + job.outputMipmap[mad(3, outputIndex, 0)] = (byte)outputColor.x; + job.outputMipmap[mad(3, outputIndex, 1)] = (byte)outputColor.y; + job.outputMipmap[mad(3, outputIndex, 2)] = (byte)outputColor.z; + } + + [BurstCompile(CompileSynchronously = true)] + static void FilterMipmapRGBA32(ref FilterMipmapJob job, int outputIndex) { + var outputX = outputIndex % job.outputDimensions.x; + var outputY = outputIndex / job.outputDimensions.x; + var outputColor = new uint4(); + + for (var offsetY = 0; offsetY < 2; offsetY++) { + for (var offsetX = 0; offsetX < 2; offsetX++) { + var inputX = min(mad(2, outputX, offsetX), job.inputDimensions.x - 1); + var inputY = min(mad(2, outputY, offsetY), job.inputDimensions.y - 1); + var inputIndex = mad(job.inputDimensions.x, inputY, inputX); + outputColor.x += (uint)job.inputMipmap[mad(4, inputIndex, 0)] >> 2; + outputColor.y += (uint)job.inputMipmap[mad(4, inputIndex, 1)] >> 2; + outputColor.z += (uint)job.inputMipmap[mad(4, inputIndex, 2)] >> 2; + outputColor.w += (uint)job.inputMipmap[mad(4, inputIndex, 3)] >> 2; + } + } + + job.outputMipmap[mad(4, outputIndex, 0)] = (byte)outputColor.x; + job.outputMipmap[mad(4, outputIndex, 1)] = (byte)outputColor.y; + job.outputMipmap[mad(4, outputIndex, 2)] = (byte)outputColor.z; + job.outputMipmap[mad(4, outputIndex, 3)] = (byte)outputColor.w; + } + + [ReadOnly] + public NativeSlice inputMipmap; + public int2 inputDimensions; + + [WriteOnly, NativeDisableParallelForRestriction] + public NativeSlice outputMipmap; + public int2 outputDimensions; + + public FunctionPointer processFunction; + + public void Execute(int outputIndex) => processFunction.Invoke(ref this, outputIndex); + } + } +} diff --git a/Assets/Plugins/asyncImageLoader/ImageImporter.cs.meta b/Assets/Plugins/asyncImageLoader/ImageImporter.cs.meta new file mode 100644 index 0000000..1fac2f8 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/ImageImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 982ed11d788bdef4bbc17e230826e010 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/android.meta b/Assets/Plugins/asyncImageLoader/android.meta new file mode 100644 index 0000000..3ab8d95 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d659c5419ec688b449b87b1523b5a898 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/android/arm64-v8a.meta b/Assets/Plugins/asyncImageLoader/android/arm64-v8a.meta new file mode 100644 index 0000000..33c794f --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/android/arm64-v8a.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b4a4dff01c57c5f4186c1ed72b24f739 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/android/arm64-v8a/FreeImage.so b/Assets/Plugins/asyncImageLoader/android/arm64-v8a/FreeImage.so new file mode 100644 index 0000000..b1e7e6d Binary files /dev/null and b/Assets/Plugins/asyncImageLoader/android/arm64-v8a/FreeImage.so differ diff --git a/Assets/Plugins/asyncImageLoader/android/arm64-v8a/FreeImage.so.meta b/Assets/Plugins/asyncImageLoader/android/arm64-v8a/FreeImage.so.meta new file mode 100644 index 0000000..edb6ac2 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/android/arm64-v8a/FreeImage.so.meta @@ -0,0 +1,70 @@ +fileFormatVersion: 2 +guid: d9466fb19c82dca4d80975d35c92ea24 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 0 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + Android: Android + second: + enabled: 1 + settings: + CPU: ARM64 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/windows.meta b/Assets/Plugins/asyncImageLoader/windows.meta new file mode 100644 index 0000000..7e8f8e0 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/windows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1956ca74f8a237845b9812079f395acb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/windows/x64.meta b/Assets/Plugins/asyncImageLoader/windows/x64.meta new file mode 100644 index 0000000..f53ee6d --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/windows/x64.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b91886e4bf859a440a9fae954967ec1e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/asyncImageLoader/windows/x64/FreeImage.dll b/Assets/Plugins/asyncImageLoader/windows/x64/FreeImage.dll new file mode 100644 index 0000000..ddf9120 Binary files /dev/null and b/Assets/Plugins/asyncImageLoader/windows/x64/FreeImage.dll differ diff --git a/Assets/Plugins/asyncImageLoader/windows/x64/FreeImage.dll.meta b/Assets/Plugins/asyncImageLoader/windows/x64/FreeImage.dll.meta new file mode 100644 index 0000000..351a870 --- /dev/null +++ b/Assets/Plugins/asyncImageLoader/windows/x64/FreeImage.dll.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: 893c926a36f9ebd4986f5c6e6beccf19 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/test.meta b/Assets/StreamingAssets/test.meta new file mode 100644 index 0000000..04068a0 --- /dev/null +++ b/Assets/StreamingAssets/test.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e7d8c6e5fb62fdd4ebc558fd50ca359f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/test/menhara.jp2 b/Assets/StreamingAssets/test/menhara.jp2 new file mode 100644 index 0000000..e7d0400 Binary files /dev/null and b/Assets/StreamingAssets/test/menhara.jp2 differ diff --git a/Assets/StreamingAssets/test/menhara.jp2.meta b/Assets/StreamingAssets/test/menhara.jp2.meta new file mode 100644 index 0000000..5b431dd --- /dev/null +++ b/Assets/StreamingAssets/test/menhara.jp2.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8ed7e246e33b91b4399f05539b727e68 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/ImagingTests.meta b/Assets/Tests/ImagingTests.meta new file mode 100644 index 0000000..ddab978 --- /dev/null +++ b/Assets/Tests/ImagingTests.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6f6bbc813412485589e96009cd83a439 +timeCreated: 1642513051 \ No newline at end of file diff --git a/Assets/Tests/ImagingTests/ImagingTests.cs b/Assets/Tests/ImagingTests/ImagingTests.cs new file mode 100644 index 0000000..1b3b819 --- /dev/null +++ b/Assets/Tests/ImagingTests/ImagingTests.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Raindrop.Tests.ImagingTests +{ + public class ImagingTests + { + // test if the j2p decode and encode can work. + // test if able to write to disk. + // have to manual check the images on the disk to see if the test passes. + [TestFixture()] + public class RaindropIntegrationTests + { + + public static string _appDataDir = Application.persistentDataPath; + public string outPath_persistentDataPath = _appDataDir + "/Pictures/menhara_out_persistentDataPath.jpg"; + + + + public string outFOlder_Pre = _appDataDir + "/Pictures/"; + + public static string _inputImageSubPath = "test/menhara.jp2"; + public static string _largeImageRelativePath = "test/largeimage.jp2"; + + public string inFolder_Pre = "test/jp2/"; + // (pathToTGAFolder, "*.tga", SearchOption.AllDirectories); + // public string testImgPath = BetterStreamingAssets + "\\test\\menhara.jp2"; + // + // #else + // + // public string _appDataDir = Application.persistentDataPath; + // + // public string testImgPath = _appDataDir + "\\Pictures\\menhara.jp2"; + // public string outPath = _appDataDir + "\\Pictures\\menhara_out.jpg"; + // #endif + // + + + //can read the image data from path. + [Test] + public void readBytes_ImagePath() + { + Assert.True(BetterStreamingAssets.FileExists(_inputImageSubPath)); + + byte[] thebytes = BetterStreamingAssets.ReadAllBytes(_inputImageSubPath); + + Assert.True(thebytes.Length > 0); + } + + //can read and decode the jp2 from streamingassets. then save to local caching directory. + [Test] + public void readAndDecodeAndSave_ImagePath() + { + BetterStreamingAssets.Initialize(); //fuck, this is easy to forget. + + float timeStart = Time.realtimeSinceStartup; + + Assert.True(BetterStreamingAssets.FileExists(_inputImageSubPath)); + + byte[] thebytes = BetterStreamingAssets.ReadAllBytes(_inputImageSubPath); + Assert.True(thebytes.Length > 0); + + + var texture = new Texture2D(1, 1, TextureFormat.ARGB32,false); // impt there is a jobs bug here + var loaderSettings = AsyncImageLoader.LoaderSettings.Default; + loaderSettings.generateMipmap = false; // impt there is a bug here + var loadSuccess = false; + + // Use the default LoaderSettings + loadSuccess = AsyncImageLoader.LoadImage(texture, thebytes, loaderSettings); + // ================================== + // Create new texture from image data + // ================================== + + Assert.True(loadSuccess); + + var outbytes = texture.EncodeToJPG(100); + #if UNITY_EDITOR + + WriteToFile(outbytes, outPath_persistentDataPath); + #elif UNITY_ANDROID //warn, this is true if we are in editor... + string outPath_GetAndroidExternalFilesDir_internal = + GetAndroidExternalFilesDir(false) + "/Pictures/menhara_out_GetAndroidExternalFilesDir_internal.jpg"; + string outPath_GetAndroidExternalFilesDir_external = + GetAndroidExternalFilesDir(true) + "/Pictures/menhara_out_GetAndroidExternalFilesDir_external.jpg"; + + WriteToFile(outbytes, outPath_GetAndroidExternalFilesDir_internal); + WriteToFile(outbytes, outPath_GetAndroidExternalFilesDir_external); + WriteToFile(outbytes, outPath_persistentDataPath); + + #endif + + + + float timeEnd = Time.realtimeSinceStartup; + Debug.Log($"took time to read jp2, convert to jpg, and save : {(timeEnd - timeStart).ToString()} "); + Debug.Log($"read : in StreamingAssets, - {_inputImageSubPath} "); + + } + + //filePath = fully-specified file path + private void WriteToFile(byte[] outbytes, string filePath) + { + //create parent subfolders + var parentDir = Path.GetDirectoryName(filePath); + System.IO.Directory.CreateDirectory(parentDir); + + //write file + System.IO.File.WriteAllBytes(filePath, outbytes); + Debug.Log($"write: {filePath} "); + } + + + [Test] + [UnityPlatform (RuntimePlatform.Android)] + public void printImportantPaths_Android() + { + //get from SA + // var j2p_bytes = BetterStreamingAssets.ReadAllBytes(_largeImageRelativePath); + //save to local disk. + // File.WriteAllBytes(Application.persistentDataPath + "largeImage_out.j2p", j2p_bytes); + // File.WriteAllBytes(Application.persistentDataPath + "largeImage_out.j2p", j2p_bytes); + + // List outputs; + // for (int i = 0; i < inputs.Length; i++) + // { + // outputs.Add(string.Format(outFOlder_Pre, ) ); + // } + // + // RDS(input, output); + + Debug.Log("Application.persistentDataPath" + Application.persistentDataPath); + // should be /storage/emulated/0/Android/data/com.UnityTestRunner.UnityTestRunner/files/Pictures/ + + Debug.Log("Application.sataPath" + Application.dataPath); + Debug.Log("GetAndroidExternalFilesDir internal"+ GetAndroidExternalFilesDir(true)); + // should be /storage/6106-8710/Android/data/com.UnityTestRunner.UnityTestRunner/files + + Debug.Log("GetAndroidExternalFilesDir prefersdcard"+ GetAndroidExternalFilesDir(false)); + //should be /storage/emulated/0/Android/data/com.UnityTestRunner.UnityTestRunner/files/Pictures/ + + } + + // returns SD card if the bool is true OR the sd card is not available. + // otherwise i will return internal-but-shared storage + private static string GetAndroidExternalFilesDir(bool preferSDcard) + { + using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + { + using (AndroidJavaObject context = unityPlayer.GetStatic("currentActivity")) + { + // Get all available external file directories (emulated and sdCards) + AndroidJavaObject[] externalFilesDirectories = context.Call("getExternalFilesDirs", null); + AndroidJavaObject emulated = null; + AndroidJavaObject sdCard = null; + + for (int i = 0; i < externalFilesDirectories.Length; i++) + { + AndroidJavaObject directory = externalFilesDirectories[i]; + using (AndroidJavaClass environment = new AndroidJavaClass("android.os.Environment")) + { + // Check which one is the emulated and which the sdCard. + bool isRemovable = environment.CallStatic ("isExternalStorageRemovable", directory); + bool isEmulated = environment.CallStatic ("isExternalStorageEmulated", directory); + if (isEmulated) + emulated = directory; + else if (isRemovable && isEmulated == false) + sdCard = directory; + } + } + // Return the sdCard if available + if (sdCard != null && preferSDcard) + return sdCard.Call("getAbsolutePath"); + else + return emulated.Call("getAbsolutePath"); + } + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Tests/ImagingTests/ImagingTests.cs.meta b/Assets/Tests/ImagingTests/ImagingTests.cs.meta new file mode 100644 index 0000000..b482311 --- /dev/null +++ b/Assets/Tests/ImagingTests/ImagingTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 60fbbe4414cd416eaa351e8b801a5c27 +timeCreated: 1642513066 \ No newline at end of file diff --git a/Assets/Tests/Tests.asmdef b/Assets/Tests/Tests.asmdef index 6f9a159..d0dd03f 100644 --- a/Assets/Tests/Tests.asmdef +++ b/Assets/Tests/Tests.asmdef @@ -7,14 +7,17 @@ "LibreMetaverseAssembly", "RaindropUnity", "LeanGUI", - "Unity.TextMeshPro" + "Unity.TextMeshPro", + "AsyncImageLoader.Runtime", + "Better" ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": true, "precompiledReferences": [ - "nunit.framework.dll" + "nunit.framework.dll", + "OpenJpegDotNet.dll" ], "autoReferenced": false, "defineConstraints": [