mirror of
https://github.com/RaindropViewer/RaindropViewer.git
synced 2026-07-27 22:11:39 +00:00
Add freeimage.so for arm_64 and simple read test.
The test reads j2p from disk and then saves it to disk using Texture2d.EncodeToJPG(). it is passing on android phone.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70cc050fb208c444687afffe93a677fa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8cf6376c8aac1e43b688324ad3f22de
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
public static partial class AsyncImageLoader {
|
||||
/// <summary> Load image synchronously. This variant uses the default <c>LoaderSettings</c>.</summary>
|
||||
/// <returns>True if the data can be loaded, false otherwise.</returns>
|
||||
public static bool LoadImage(Texture2D texture, byte[] data) {
|
||||
return LoadImage(texture, data, LoaderSettings.Default);
|
||||
}
|
||||
|
||||
/// <summary> Load image synchronously. This variant is similar to <c>ImageConversion.LoadImage</c>.</summary>
|
||||
/// <returns>True if the data can be loaded, false otherwise.</returns>
|
||||
public static bool LoadImage(Texture2D texture, byte[] data, bool markNonReadable) {
|
||||
var loaderSettings = LoaderSettings.Default;
|
||||
loaderSettings.markNonReadable = markNonReadable;
|
||||
return LoadImage(texture, data, loaderSettings);
|
||||
}
|
||||
|
||||
/// <summary> Load image synchronously. This variant accepts a <c>LoaderSettings</c>. Useful for debugging and profiling.</summary>
|
||||
/// <returns>True if the data can be loaded, false otherwise.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Load image asynchronously. This variant uses the default <c>LoaderSettings</c>.</summary>
|
||||
/// <returns>True if the data can be loaded, false otherwise.</returns>
|
||||
public static Task<bool> LoadImageAsync(Texture2D texture, byte[] data) {
|
||||
return LoadImageAsync(texture, data, LoaderSettings.Default);
|
||||
}
|
||||
|
||||
/// <summary> Load image asynchronously. This variant is similar to <c>ImageConversion.LoadImage</c>.</summary>
|
||||
/// <returns>True if the data can be loaded, false otherwise.</returns>
|
||||
public static Task<bool> LoadImageAsync(Texture2D texture, byte[] data, bool markNonReadable) {
|
||||
var loaderSettings = LoaderSettings.Default;
|
||||
loaderSettings.markNonReadable = markNonReadable;
|
||||
return LoadImageAsync(texture, data, loaderSettings);
|
||||
}
|
||||
|
||||
/// <summary>Load image asynchronously. This variant accepts a <c>LoaderSettings</c>.</summary>
|
||||
/// <returns>True if the data can be loaded, false otherwise.</returns>
|
||||
public static async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Create <c>Texture2D</c> from image data synchronously. This variant uses a default <c>LoaderSettings</c>. Linear and mipmap count can be specified.</summary>
|
||||
/// <returns><c>Texture2D</c> object if the data can be loaded, null otherwise.</returns>
|
||||
public static Texture2D CreateFromImage(byte[] data) {
|
||||
return CreateFromImage(data, LoaderSettings.Default);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Create <c>Texture2D</c> from image data synchronously. This variant accepts a <c>LoaderSettings</c>. Linear and mipmap count can be specified.</summary>
|
||||
/// <returns><c>Texture2D</c> object if the data can be loaded, null otherwise.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Create <c>Texture2D</c> from image data asynchronously. This variant uses a default <c>LoaderSettings</c>. Linear and mipmap count can be specified.</summary>
|
||||
/// <returns><c>Texture2D</c> object if the data can be loaded, null otherwise.</returns>
|
||||
public static Task<Texture2D> CreateFromImageAsync(byte[] data) {
|
||||
return CreateFromImageAsync(data, LoaderSettings.Default);
|
||||
}
|
||||
|
||||
/// <summary>Create <c>Texture2D</c> from image data asynchronously. This variant accepts a <c>LoaderSettings</c>. Linear and mipmap count can be specified.</summary>
|
||||
/// <returns><c>Texture2D</c> object if the data can be loaded, null otherwise.</returns>
|
||||
public static async Task<Texture2D> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Settings used by the image loader.</summary>
|
||||
public struct LoaderSettings {
|
||||
/// <summary>Create linear texture. Only applicable to methods that create new <c>Texture2D</c>. Defaults to false.</summary>
|
||||
public bool linear;
|
||||
/// <summary>Texture data won't be readable on the CPU after loading. Defaults to false.</summary>
|
||||
public bool markNonReadable;
|
||||
/// <summary>Whether or not to generate mipmaps. Defaults to true.</summary>
|
||||
public bool generateMipmap;
|
||||
/// <summary>Automatically calculate the number of mipmap levels. Defaults to true. Only applicable to methods that create new <c>Texture2D</c>.</summary>
|
||||
public bool autoMipmapCount;
|
||||
/// <summary>Mipmap count, including the base level. Must be greater than 1. Only applicable to methods that create new <c>Texture2D</c>.</summary>
|
||||
public int mipmapCount;
|
||||
/// <summary>Used to explicitly specify the image format. Defaults to FIF_UNKNOWN, which the image format will be automatically determined.</summary>
|
||||
public FreeImage.Format format;
|
||||
/// <summary>Whether or not to log exception caught by this method. Defaults to true.</summary>
|
||||
public bool logException;
|
||||
|
||||
public static LoaderSettings Default => new LoaderSettings {
|
||||
linear = false,
|
||||
markNonReadable = false,
|
||||
generateMipmap = true,
|
||||
autoMipmapCount = true,
|
||||
format = FreeImage.Format.FIF_UNKNOWN,
|
||||
logException = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f4efe3f88c91cd4fa0d8c97b0c9520e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: beb5906fae1246642a984f6b9eecc26b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<byte>();
|
||||
LoadRawTextureData(rawTextureView);
|
||||
ProcessRawTextureData(rawTextureView, mipmapCount);
|
||||
_finalJob.Complete();
|
||||
texture.Apply(false, _loaderSettings.markNonReadable);
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Texture2D> CreateNewTextureAsync() {
|
||||
var mipmapCount = CalculateMipmapCount();
|
||||
var texture = new Texture2D(_width, _height, _textureFormat, mipmapCount, _loaderSettings.linear);
|
||||
var rawTextureView = texture.GetRawTextureData<byte>();
|
||||
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<byte>();
|
||||
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<byte>();
|
||||
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<byte> rawTextureView) {
|
||||
using (LoadRawTextureDataMarker.Auto()) {
|
||||
var mipmapDimensions = CalculateMipmapDimensions(0);
|
||||
var mipmapSize = mipmapDimensions.x * mipmapDimensions.y;
|
||||
var mipmapSlice = new NativeSlice<byte>(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<byte> rawTextureView, int mipmapCount) {
|
||||
using (ProcessRawTextureDataMarker.Auto()) {
|
||||
var mipmapDimensions = CalculateMipmapDimensions(0);
|
||||
var mipmapSize = mipmapDimensions.x * mipmapDimensions.y;
|
||||
var mipmapSlice = new NativeSlice<byte>(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<byte>(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<byte> textureData, int index);
|
||||
|
||||
public static readonly FunctionPointer<BGRToRGBDelegate> BGR24ToRGB24FP = BurstCompiler.CompileFunctionPointer<BGRToRGBDelegate>(BGR24ToRGB24);
|
||||
public static readonly FunctionPointer<BGRToRGBDelegate> BGRA32ToRGBA32FP = BurstCompiler.CompileFunctionPointer<BGRToRGBDelegate>(BGRA32ToRGBA32);
|
||||
|
||||
[BurstCompile(CompileSynchronously = true)]
|
||||
static void BGR24ToRGB24(ref NativeSlice<byte> 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<byte> 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<byte> textureData;
|
||||
public FunctionPointer<BGRToRGBDelegate> 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<FilterMipmapDelegate> FilterMipmapRGB24FP = BurstCompiler.CompileFunctionPointer<FilterMipmapDelegate>(FilterMipmapRGB24);
|
||||
public static readonly FunctionPointer<FilterMipmapDelegate> FilterMipmapRGBA32FP = BurstCompiler.CompileFunctionPointer<FilterMipmapDelegate>(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<byte> inputMipmap;
|
||||
public int2 inputDimensions;
|
||||
|
||||
[WriteOnly, NativeDisableParallelForRestriction]
|
||||
public NativeSlice<byte> outputMipmap;
|
||||
public int2 outputDimensions;
|
||||
|
||||
public FunctionPointer<FilterMipmapDelegate> processFunction;
|
||||
|
||||
public void Execute(int outputIndex) => processFunction.Invoke(ref this, outputIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 982ed11d788bdef4bbc17e230826e010
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d659c5419ec688b449b87b1523b5a898
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4a4dff01c57c5f4186c1ed72b24f739
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1956ca74f8a237845b9812079f395acb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b91886e4bf859a440a9fae954967ec1e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7d8c6e5fb62fdd4ebc558fd50ca359f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ed7e246e33b91b4399f05539b727e68
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f6bbc813412485589e96009cd83a439
|
||||
timeCreated: 1642513051
|
||||
@@ -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<string> 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<AndroidJavaObject>("currentActivity"))
|
||||
{
|
||||
// Get all available external file directories (emulated and sdCards)
|
||||
AndroidJavaObject[] externalFilesDirectories = context.Call<AndroidJavaObject[],AndroidJavaObject[]>("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<bool> ("isExternalStorageRemovable", directory);
|
||||
bool isEmulated = environment.CallStatic<bool> ("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<string>("getAbsolutePath");
|
||||
else
|
||||
return emulated.Call<string>("getAbsolutePath");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60fbbe4414cd416eaa351e8b801a5c27
|
||||
timeCreated: 1642513066
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user