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": [