Add ultimate xr

This commit is contained in:
2024-08-06 21:58:35 +02:00
parent 864033bf10
commit 7165bacd9d
3952 changed files with 2162037 additions and 35 deletions

View File

@@ -0,0 +1,182 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="Color32Ext.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Extensions.System;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Color32" /> extensions.
/// </summary>
public static class Color32Ext
{
#region Public Methods
/// <summary>
/// Transforms an array of bytes to a <see cref="Color32" /> component by component. If there are not enough values to
/// read, the remaining values are set to <see cref="byte.MinValue" /> (0) for RGB and <see cref="byte.MaxValue" />
/// (255) for A.
/// </summary>
/// <param name="data">Source data</param>
/// <returns>Result color</returns>
public static Color32 ToColor32(this byte[] data)
{
switch (data.Length)
{
case 0: return default;
case 1: return new Color32(data[0], byte.MinValue, byte.MinValue, byte.MaxValue);
case 2: return new Color32(data[0], data[1], byte.MinValue, byte.MaxValue);
case 3: return new Color32(data[0], data[1], data[2], byte.MaxValue);
default: return new Color32(data[0], data[1], data[2], data[3]);
}
}
/// <summary>
/// Transforms a <see cref="Color32" /> value into the int value it encodes the color in.
/// </summary>
/// <param name="self">Color</param>
/// <returns>Int value</returns>
public static int ToInt(this in Color32 self)
{
return self.r << 24 | self.g << 16 | self.b << 8 | self.a;
}
/// <summary>
/// Clamps <see cref="Color32" /> values component by component.
/// </summary>
/// <param name="self">Color whose components to clamp</param>
/// <param name="min">Minimum RGB values</param>
/// <param name="max">Maximum RGB values</param>
/// <returns>Clamped color</returns>
public static Color32 Clamp(this in Color32 self, in Color32 min, in Color32 max)
{
byte[] result = new byte[VectorLength];
for (int i = 0; i < VectorLength; ++i)
{
result[i] = (byte)Mathf.Clamp(self[i], min[i], max[i]);
}
return result.ToColor32();
}
/// <summary>
/// Multiplies two colors by multiplying each component.
/// </summary>
/// <param name="self">Operand A</param>
/// <param name="other">Operand B</param>
/// <returns>Result color</returns>
public static Color32 Multiply(this in Color32 self, in Color32 other)
{
return new Color32((byte)(self.r * other.r),
(byte)(self.g * other.g),
(byte)(self.b * other.b),
(byte)(self.a * other.a));
}
/// <summary>
/// Compares two colors.
/// </summary>
/// <param name="self">First color to compare</param>
/// <param name="other">Second color to compare</param>
/// <returns>Whether the two colors are the same</returns>
public static bool IsSameColor(this in Color32 self, in Color32 other)
{
return self.r == other.r && self.g == other.g && self.b == other.b && self.a == other.a;
}
/// <summary>
/// Converts the color to a HTML color value (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="self">Color to convert</param>
/// <returns>HTML color string</returns>
public static string ToHtml(this in Color32 self)
{
return self.a == 255 ? string.Format(StringFormatRGB, self.r, self.g, self.b) : string.Format(StringFormatRGBA, self.r, self.g, self.b, self.a);
}
/// <summary>
/// Tries to parse a <see cref="Color32" /> from an HTML string (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="html">Source HTML string</param>
/// <param name="result">Parsed color or the default color value if there was an error</param>
/// <returns>Whether the color was parsed successfully</returns>
public static bool TryParse(string html, out Color32 result)
{
try
{
result = Parse(html);
return true;
}
catch
{
result = default;
return false;
}
}
/// <summary>
/// Parses a <see cref="Color32" /> from an HTML string (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="html">Source HTML string</param>
/// <returns>The parsed color</returns>
/// <exception cref="FormatException">The string had an incorrect format</exception>
public static Color32 Parse(string html)
{
html.ThrowIfNull(nameof(html));
Match match = _regex.Match(html);
if (!match.Success)
{
throw new FormatException($"Input string [{html}] does not have the right format: #RRGGBB or #RRGGBBAA");
}
byte[] colorBytes = new byte[VectorLength];
for (int i = 0; i < VectorLength - 1; ++i)
{
string hex = match.Groups[i + 1].Value;
colorBytes[i] = byte.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);
}
string aa = match.Groups[VectorLength].Value;
colorBytes[VectorLength - 1] = aa == string.Empty
? byte.MaxValue
: byte.Parse(aa, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);
return colorBytes.ToColor32();
}
/// <summary>
/// Parses asynchronously a <see cref="Color32" /> from an HTML string (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="html">Source HTML string</param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <returns>An awaitable <see cref="Task" /> that returns the parsed color</returns>
/// <exception cref="FormatException">The string had an incorrect format</exception>
public static Task<Color32?> ParseAsync(string html, CancellationToken ct = default)
{
return Task.Run(() => TryParse(html, out Color32 result) ? result : (Color32?)null, ct);
}
#endregion
#region Private Types & Data
private const int VectorLength = 4;
private const string StringFormatRGBA = "#{0:X2}{1:X2}{2:X2}{3:X2}";
private const string StringFormatRGB = "#{0:X2}{1:X2}{2:X2}";
private const string RegexPattern = "^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})?$";
private static readonly Regex _regex = new Regex(RegexPattern);
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6f69c5ccb17b9474c88c59e005eb072a
timeCreated: 1621500585

View File

@@ -0,0 +1,205 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ColorExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Extensions.System;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Color" /> extensions.
/// </summary>
public static class ColorExt
{
#region Public Methods
/// <summary>
/// Transforms an array of floats to a <see cref="Color" /> component by component. If there are not enough values to
/// read, the remaining values are set to 0.0 for RGB and 1.0 for A.
/// </summary>
/// <param name="data">Source data</param>
/// <returns>Result color</returns>
public static Color ToColor(this float[] data)
{
return data.Length switch
{
0 => default,
1 => new Color(data[0], 0f, 0f, 1f),
2 => new Color(data[0], data[1], 0f, 1f),
3 => new Color(data[0], data[1], data[2], 1f),
_ => new Color(data[0], data[1], data[2], data[3])
};
}
/// <summary>
/// Clamps <see cref="Color" /> values component by component.
/// </summary>
/// <param name="self">Color whose components to clamp</param>
/// <param name="min">Minimum RGB values</param>
/// <param name="max">Maximum RGB values</param>
/// <returns>Clamped color</returns>
public static Color Clamp(this in Color self, in Color min, in Color max)
{
float[] result = new float[VectorLength];
for (int i = 0; i < VectorLength; ++i)
{
result[i] = Mathf.Clamp(self[i], min[i], max[i]);
}
return result.ToColor();
}
/// <summary>
/// Multiplies two colors by multiplying each component.
/// </summary>
/// <param name="self">Operand A</param>
/// <param name="other">Operand B</param>
/// <returns>Result color</returns>
public static Color Multiply(this in Color self, in Color other)
{
return new Color(self.r * other.r,
self.g * other.g,
self.b * other.b,
self.a * other.a);
}
/// <summary>
/// Creates a color based on an already existing color and an alpha value.
/// </summary>
/// <param name="color">Color value</param>
/// <param name="alpha">Alpha value</param>
/// <returns>
/// Result of combining the RGB of the <paramref name="color" /> value and alpha of <paramref name="alpha" />
/// </returns>
public static Color ColorAlpha(in Color color, float alpha)
{
return new Color(color.r, color.g, color.b, alpha);
}
/// <summary>
/// Creates a color based on an already existing color and an alpha value.
/// </summary>
/// <param name="self">Color value</param>
/// <param name="alpha">Alpha value</param>
/// <returns>
/// Result of combining the RGBA of the color value and <paramref name="alpha" />
/// </returns>
public static Color WithAlpha(this in Color self, float alpha)
{
return ColorAlpha(self, alpha);
}
/// <summary>
/// Creates a color based on an already existing color and a brightness scale value.
/// </summary>
/// <param name="color">Color value</param>
/// <param name="brightnessScale">The brightness scale factor</param>
/// <returns>Color with adjusted brightness</returns>
public static Color ScaleColorBrightness(this in Color color, float brightnessScale)
{
Color.RGBToHSV(color, out float h, out float s, out float v);
v *= brightnessScale;
return Color.HSVToRGB(h, s, v);
}
/// <summary>
/// Converts the color to a HTML color value (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="self">Color to convert</param>
/// <returns>HTML color string</returns>
public static string ToHtml(this in Color self)
{
return Mathf.Approximately(self.a, 1f)
? string.Format(StringFormatRGB,
(byte)Mathf.Round(Mathf.Clamp01(self.r) * byte.MaxValue),
(byte)Mathf.Round(Mathf.Clamp01(self.g) * byte.MaxValue),
(byte)Mathf.Round(Mathf.Clamp01(self.b) * byte.MaxValue))
: string.Format(StringFormatRGBA,
(byte)Mathf.Round(Mathf.Clamp01(self.r) * byte.MaxValue),
(byte)Mathf.Round(Mathf.Clamp01(self.g) * byte.MaxValue),
(byte)Mathf.Round(Mathf.Clamp01(self.b) * byte.MaxValue),
(byte)Mathf.Round(Mathf.Clamp01(self.a) * byte.MaxValue));
}
/// <summary>
/// Tries to parse a <see cref="Color" /> from an HTML string (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="html">Source HTML string</param>
/// <param name="result">Parsed color or the default color value if there was an error</param>
/// <returns>Whether the color was parsed successfully</returns>
public static bool TryParse(string html, out Color result)
{
try
{
result = Parse(html);
return true;
}
catch
{
result = default;
return false;
}
}
/// <summary>
/// Parses a <see cref="Color" /> from an HTML string (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="html">Source HTML string</param>
/// <returns>The parsed color</returns>
/// <exception cref="FormatException">The string had an incorrect format</exception>
public static Color Parse(string html)
{
html.ThrowIfNull(nameof(html));
Match match = _regex.Match(html);
if (!match.Success)
{
throw new FormatException($"Input string [{html}] does not have the right format: #RRGGBB or #RRGGBBAA");
}
float[] result = new float[VectorLength];
for (int i = 0; i < VectorLength - 1; ++i)
{
string hex = match.Groups[i + 1].Value;
result[i] = byte.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat) / (float)byte.MaxValue;
}
string aa = match.Groups[VectorLength].Value;
result[VectorLength - 1] = aa != string.Empty ? byte.Parse(aa, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat) / (float)byte.MaxValue : 1f;
return result.ToColor();
}
/// <summary>
/// Parses asynchronously a <see cref="Color" /> from an HTML string (#RRGGBB or #RRGGBBAA).
/// </summary>
/// <param name="html">Source HTML string</param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <returns>An awaitable <see cref="Task" /> that returns the parsed color</returns>
/// <exception cref="FormatException">The string had an incorrect format</exception>
public static Task<Color?> ParseAsync(string html, CancellationToken ct = default)
{
return Task.Run(() => TryParse(html, out Color result) ? result : (Color?)null, ct);
}
#endregion
#region Private Types & Data
private const int VectorLength = 4;
private const string StringFormatRGBA = "#{0:X2}{1:X2}{2:X2}{3:X2}";
private const string StringFormatRGB = "#{0:X2}{1:X2}{2:X2}";
private const string RegexPattern = "^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})?$";
private static readonly Regex _regex = new Regex(RegexPattern);
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 29cad001faae79346949b7a9251a414a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,112 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ImageExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Extensions.System;
using UltimateXR.Extensions.System.IO;
using UnityEngine.UI;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Image" /> extensions.
/// </summary>
public static class ImageExt
{
#region Public Methods
/// <summary>
/// Loads a sprite asynchronously from a base64 encoded string and assigns it to the
/// <see cref="Image.overrideSprite" /> property of an <see cref="Image" />.
/// </summary>
/// <param name="self">Target <see cref="Image" /></param>
/// <param name="base64">Base64 encoded string. See <see cref="SpriteExt.ReadSpriteBase64Async" /></param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <exception cref="ArgumentNullException"><paramref name="base64" /> is null or empty</exception>
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
/// <exception cref="FormatException">
/// The length of <paramref name="base64" />, ignoring white-space characters, is not
/// zero or a multiple of 4
/// </exception>
public static async Task OverrideSpriteFromBase64Async(this Image self, string base64, CancellationToken ct = default)
{
self.ThrowIfNull(nameof(self));
self.overrideSprite = await SpriteExt.ReadSpriteBase64Async(self, base64, ct);
}
/// <summary>
/// Loads a sprite asynchronously from an URI and assigns it to the <see cref="Image.overrideSprite" /> property of an
/// <see cref="Image" />.
/// </summary>
/// <param name="self">Target image</param>
/// <param name="uri">File location. See <see cref="FileExt.Read" /></param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"><paramref name="uri" /> is null or empty</exception>
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
/// <exception cref="FileNotFoundException">The file specified in <paramref name="uri" /> was not found</exception>
/// <exception cref="NotSupportedException"><paramref name="uri" /> is in an invalid format</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file</exception>
/// <exception cref="InvalidOperationException">The stream is currently in use by a previous read operation</exception>
public static async Task OverrideSpriteFromUriAsync(this Image self, string uri, CancellationToken ct = default)
{
self.ThrowIfNull(nameof(self));
self.overrideSprite = await SpriteExt.ReadSpriteFileAsync(self, uri, ct);
}
/// <summary>
/// Tries to load a sprite asynchronously from an URI and assign it to the <see cref="Image.overrideSprite" /> property
/// of an <see cref="Image" />.
/// </summary>
/// <param name="self">Target image</param>
/// <param name="uri">File location. See <see cref="FileExt.Read" /></param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <returns>
/// Whether the sprite was correctly load and the <see cref="Image" /> had its <see cref="Image.overrideSprite" />
/// assigned
/// </returns>
public static async Task<bool> TryOverrideSpriteFromUriAsync(this Image self, string uri, CancellationToken ct = default)
{
try
{
await self.OverrideSpriteFromUriAsync(uri, ct);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Tries to load a sprite asynchronously from a base64 encoded string and assign it to the
/// <see cref="Image.overrideSprite" /> property of an <see cref="Image" />.
/// </summary>
/// <param name="self">Target image</param>
/// <param name="base64">Base64 encoded string with the image file content</param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <returns>
/// Whether the sprite was correctly load and the <see cref="Image" /> had its <see cref="Image.overrideSprite" />
/// assigned
/// </returns>
public static async Task<bool> TryOverrideSpriteFromBase64Async(this Image self, string base64, CancellationToken ct)
{
try
{
await self.OverrideSpriteFromBase64Async(base64, ct);
return true;
}
catch
{
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a054e74bfe056a14bbbad631f4310243
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,155 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="LODGroupExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="LODGroup" /> extensions.
/// Most functionality has been copied from:
/// https://github.com/JulienHeijmans/EditorScripts/blob/master/Scripts/Utility/Editor/LODExtendedUtility.cs, which
/// in turn copied functionality from:
/// https://github.com/Unity-Technologies/AutoLOD/blob/master/Scripts/Extensions/LODGroupExtensions.cs
/// </summary>
public static class LODGroupExt
{
#region Public Methods
/// <summary>
/// Gets the LOD level index that should be enabled from a specific view.
/// </summary>
/// <param name="lodGroup">Component to check</param>
/// <param name="camera">Camera to use as point of view</param>
/// <returns>LOD level index that should be enabled</returns>
public static int GetVisibleLevel(this LODGroup lodGroup, Camera camera)
{
if (camera == null)
{
return lodGroup.lodCount - 1;
}
var lods = lodGroup.GetLODs();
var relativeHeight = GetRelativeHeight(lodGroup, camera);
int lodIndex = lodGroup.lodCount - 1;
for (int i = 0; i < lods.Length; i++)
{
var lod = lods[i];
if (relativeHeight >= lod.screenRelativeTransitionHeight)
{
lodIndex = i;
break;
}
}
return lodIndex;
}
/// <summary>
/// Manually enables all renderers belonging to a LOD level.
/// </summary>
/// <param name="lodGroup">Component to process</param>
/// <param name="level">Level whose renderers to enable</param>
public static void EnableLevelRenderers(this LODGroup lodGroup, int level)
{
var lods = lodGroup.GetLODs();
for (int i = 0; i < lods.Length; i++)
{
foreach (Renderer renderer in lods[i].renderers)
{
if (renderer != null)
{
renderer.enabled = i == level;
}
}
}
}
/// <summary>
/// Manually enables the renderers from all LOD levels.
/// </summary>
/// <param name="lodGroup">Component to process</param>
public static void EnableAllLevelRenderers(this LODGroup lodGroup)
{
var lods = lodGroup.GetLODs();
for (int i = 0; i < lods.Length; i++)
{
foreach (Renderer renderer in lods[i].renderers)
{
if (renderer != null)
{
renderer.enabled = true;
}
}
}
}
#endregion
#region Private Methods
/// <summary>
/// Computes the relative height in the camera view.
/// </summary>
/// <param name="lodGroup">Component to check</param>
/// <param name="camera">Camera to use as point of view</param>
/// <returns>Relative height</returns>
private static float GetRelativeHeight(LODGroup lodGroup, Camera camera)
{
var distance = (lodGroup.transform.TransformPoint(lodGroup.localReferencePoint) - camera.transform.position).magnitude;
return DistanceToRelativeHeight(camera, distance / QualitySettings.lodBias, GetWorldSpaceSize(lodGroup));
}
/// <summary>
/// Computes the relative height in the camera view.
/// </summary>
/// <param name="camera">Camera to use as point of view</param>
/// <param name="distance">Distance to the camera</param>
/// <param name="size">Largest axis in world-space</param>
/// <returns>Relative height</returns>
private static float DistanceToRelativeHeight(Camera camera, float distance, float size)
{
if (camera.orthographic)
{
return size * 0.5F / camera.orthographicSize;
}
var halfAngle = Mathf.Tan(Mathf.Deg2Rad * camera.fieldOfView * 0.5F);
var relativeHeight = size * 0.5F / (distance * halfAngle);
return relativeHeight;
}
/// <summary>
/// Computes the largest axis of the <see cref="LODGroup" /> in world-space.
/// </summary>
/// <param name="lodGroup">Component to process</param>
/// <returns>World space size</returns>
private static float GetWorldSpaceSize(LODGroup lodGroup)
{
return GetWorldSpaceScale(lodGroup.transform) * lodGroup.size;
}
/// <summary>
/// Computes the largest scale axis.
/// </summary>
/// <param name="transform">Transform to get the largest scale of</param>
/// <returns>Largest scale axis</returns>
private static float GetWorldSpaceScale(Transform transform)
{
var scale = transform.lossyScale;
float largestAxis = Mathf.Abs(scale.x);
largestAxis = Mathf.Max(largestAxis, Mathf.Abs(scale.y));
largestAxis = Mathf.Max(largestAxis, Mathf.Abs(scale.z));
return largestAxis;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5258a0f4700615643a99e29dd5f01350
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MeshExt.ExtractSubMeshOperation.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace UltimateXR.Extensions.Unity.Render
{
public static partial class MeshExt
{
#region Public Types & Data
/// <summary>
/// Enumerates possible mesh extraction algorithms.
/// </summary>
public enum ExtractSubMeshOperation
{
/// <summary>
/// Creates a new mesh copying all the mesh that is influenced by the bone or any of its children.
/// </summary>
BoneAndChildren,
/// <summary>
/// Creates a new mesh copying all the mesh that is not influenced by the reference bone or any of its children.
/// </summary>
NotFromBoneOrChildren
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7a64f8ffb3bd4e51b0f99a55e6a90f80
timeCreated: 1667922130

View File

@@ -0,0 +1,399 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MeshExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System.Collections.Generic;
using System.Linq;
using UltimateXR.Animation.Splines;
using UltimateXR.Core;
using UltimateXR.Extensions.System.Collections;
using UltimateXR.Extensions.Unity.Math;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Mesh" /> extensions.
/// </summary>
public static partial class MeshExt
{
#region Public Methods
/// <summary>
/// Creates a quad mesh <see cref="Mesh" />.
/// </summary>
/// <param name="size">Quad size (total width and height)</param>
/// <returns>Quad <see cref="Mesh" /></returns>
public static Mesh CreateQuad(float size)
{
Mesh quadMesh = new Mesh();
float halfSize = size * 0.5f;
quadMesh.vertices = new[] { new Vector3(halfSize, halfSize, 0.0f), new Vector3(halfSize, -halfSize, 0.0f), new Vector3(-halfSize, -halfSize, 0.0f), new Vector3(-halfSize, halfSize, 0.0f) };
quadMesh.uv = new[] { new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0.0f), new Vector2(0.0f, 0.0f), new Vector2(0.0f, 1.0f) };
quadMesh.triangles = new[] { 0, 2, 1, 0, 3, 2 };
return quadMesh;
}
/// <summary>
/// Creates a <see cref="Mesh" /> tessellating a <see cref="UxrSpline" />
/// </summary>
/// <param name="spline">Spline to evaluate</param>
/// <param name="subdivisions">Number of subdivisions along the spline axis</param>
/// <param name="sides">Number of subdivisions in the section</param>
/// <param name="radius">Section radius</param>
/// <returns><see cref="Mesh" /> with the tessellated <see cref="UxrSpline" /></returns>
public static Mesh CreateSpline(UxrSpline spline, int subdivisions, int sides, float radius)
{
subdivisions = Mathf.Max(subdivisions, 2);
sides = Mathf.Max(sides, 3);
Mesh splineMesh = new Mesh();
// Create mesh
Vector3[] vertices = new Vector3[subdivisions * (sides + 1)];
Vector3[] normals = new Vector3[vertices.Length];
Vector2[] mapping = new Vector2[vertices.Length];
int[] indices = new int[(subdivisions - 1) * sides * 2 * 3];
for (int sub = 0; sub < subdivisions; ++sub)
{
float arcLength = sub / (subdivisions - 1.0f) * spline.ArcLength;
float normalizedArcLength = arcLength / spline.ArcLength;
spline.EvaluateUsingArcLength(arcLength, out Vector3 splinePosition, out Vector3 splineDirection);
Vector3 perpendicular = splineDirection.GetPerpendicularVector().normalized * radius;
Vector3 vertexStart = splinePosition + perpendicular;
for (int side = 0; side < sides + 1; ++side)
{
int vertexIndex = sub * (sides + 1) + side;
int faceBase = sub * sides * 2 * 3 + side * 2 * 3;
float rotation = side / (float)sides;
float degrees = 360.0f * rotation;
vertices[vertexIndex] = vertexStart.GetRotationAround(splinePosition, splineDirection, degrees);
mapping[vertexIndex] = new Vector2(rotation, normalizedArcLength);
normals[vertexIndex] = (vertices[vertexIndex] - splinePosition).normalized;
if (side < sides && sub < subdivisions - 1)
{
indices[faceBase + 0] = vertexIndex;
indices[faceBase + 1] = vertexIndex + 1;
indices[faceBase + 2] = vertexIndex + sides + 1;
indices[faceBase + 3] = vertexIndex + 1;
indices[faceBase + 4] = vertexIndex + sides + 2;
indices[faceBase + 5] = vertexIndex + sides + 1;
}
}
}
splineMesh.vertices = vertices;
splineMesh.uv = mapping;
splineMesh.normals = normals;
splineMesh.triangles = indices;
return splineMesh;
}
/// <summary>
/// Creates a new mesh from a skinned mesh renderer based on a reference bone and an extract operation.
/// </summary>
/// <param name="skin">Skin to process</param>
/// <param name="bone">Reference bone</param>
/// <param name="extractOperation">Which part of the skinned mesh to extract</param>
/// <param name="weightThreshold">Bone weight threshold above which the vertices will be extracted</param>
/// <returns>New mesh</returns>
public static Mesh ExtractSubMesh(SkinnedMeshRenderer skin, Transform bone, ExtractSubMeshOperation extractOperation, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
{
Mesh newMesh = new Mesh();
// Create dictionary to check which bones belong to the hierarchy
Dictionary<int, bool> areHierarchyBones = new Dictionary<int, bool>();
Vector3[] vertices = skin.sharedMesh.vertices;
Vector3[] normals = skin.sharedMesh.normals;
Vector2[] uv = skin.sharedMesh.uv;
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
Transform[] bones = skin.bones;
for (int i = 0; i < bones.Length; ++i)
{
areHierarchyBones.Add(i, bones[i].HasParent(bone));
}
// Create filtered mesh
List<List<int>> newTriangles = new List<List<int>>();
Dictionary<int, int> old2New = new Dictionary<int, int>();
List<Vector3> newVertices = new List<Vector3>();
List<Vector3> newNormals = new List<Vector3>();
List<Vector2> newUV = new List<Vector2>();
List<BoneWeight> newBoneWeights = new List<BoneWeight>();
bool VertexMeetsRequirement(bool isFromHierarchy)
{
switch (extractOperation)
{
case ExtractSubMeshOperation.BoneAndChildren: return isFromHierarchy;
case ExtractSubMeshOperation.NotFromBoneOrChildren: return !isFromHierarchy;
}
return false;
}
for (int submesh = 0; submesh < skin.sharedMesh.subMeshCount; ++submesh)
{
int[] submeshIndices = skin.sharedMesh.GetTriangles(submesh);
List<int> newSubmeshIndices = new List<int>();
for (int t = 0; t < submeshIndices.Length / 3; t++)
{
float totalWeight = 0.0f;
for (int v = 0; v < 3; v++)
{
BoneWeight boneWeight = boneWeights[submeshIndices[t * 3 + v]];
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex0, out bool isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
{
totalWeight += boneWeight.weight0;
}
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex1, out isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
{
totalWeight += boneWeight.weight1;
}
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex2, out isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
{
totalWeight += boneWeight.weight2;
}
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex3, out isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
{
totalWeight += boneWeight.weight3;
}
}
if (totalWeight > weightThreshold)
{
for (int v = 0; v < 3; v++)
{
int oldIndex = submeshIndices[t * 3 + v];
if (!old2New.ContainsKey(oldIndex))
{
old2New.Add(oldIndex, old2New.Count);
newVertices.Add(vertices[oldIndex]);
newNormals.Add(normals[oldIndex]);
newUV.Add(uv[oldIndex]);
newBoneWeights.Add(boneWeights[oldIndex]);
}
newSubmeshIndices.Add(old2New[oldIndex]);
}
}
}
newTriangles.Add(newSubmeshIndices);
}
// Create new mesh
newMesh.vertices = newVertices.ToArray();
newMesh.normals = newNormals.ToArray();
newMesh.uv = newUV.ToArray();
newMesh.boneWeights = newBoneWeights.ToArray();
// Create and assign new triangle list
newMesh.subMeshCount = newTriangles.Count;
for (int submesh = 0; submesh < newTriangles.Count; ++submesh)
{
newMesh.SetTriangles(newTriangles[submesh].ToArray(), submesh);
}
return newMesh;
}
/// <summary>
/// Computes the number of vertices that a bone influences in a skinned mesh.
/// </summary>
/// <param name="skin">Skinned mesh</param>
/// <param name="bone">Bone to check</param>
/// <param name="weightThreshold">Weight above which will be considered significant influence</param>
/// <returns>
/// Number of vertices influenced by <paramref name="bone" /> with a weight above
/// <paramref name="weightThreshold" />.
/// </returns>
public static int GetBoneInfluenceVertexCount(SkinnedMeshRenderer skin, Transform bone, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
{
Transform[] skinBones = skin.bones;
int boneIndex = skinBones.IndexOf(bone);
if (boneIndex == -1)
{
return 0;
}
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
return boneWeights.Count(w => HasBoneInfluence(w, boneIndex, weightThreshold));
}
/// <summary>
/// Computes the number of vertices that a bone influences in a skinned mesh.
/// </summary>
/// <param name="skin">Skinned mesh</param>
/// <param name="bone">Bone to check</param>
/// <param name="weightThreshold">Weight above which to consider significant influence</param>
/// <returns>
/// Number of vertices influenced by <paramref name="bone" /> with a weight above
/// <paramref name="weightThreshold" />
/// </returns>
public static bool HasBoneInfluence(SkinnedMeshRenderer skin, Transform bone, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
{
Transform[] skinBones = skin.bones;
int boneIndex = skinBones.IndexOf(bone);
if (boneIndex == -1)
{
return false;
}
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
return boneWeights.Any(w => HasBoneInfluence(w, boneIndex, weightThreshold));
}
/// <summary>
/// Checks whether a given bone index has influence on a skinned mesh vertex.
/// </summary>
/// <param name="boneWeight">Vertex's bone weight information</param>
/// <param name="boneIndex">Bone index</param>
/// <param name="weightThreshold">Weight above which will be considered significant influence</param>
/// <returns>Whether the bone influences the vertex in a significant amount</returns>
public static bool HasBoneInfluence(in BoneWeight boneWeight, int boneIndex, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
{
if (boneWeight.boneIndex0 == boneIndex && boneWeight.weight0 > weightThreshold)
{
return true;
}
if (boneWeight.boneIndex1 == boneIndex && boneWeight.weight1 > weightThreshold)
{
return true;
}
if (boneWeight.boneIndex2 == boneIndex && boneWeight.weight2 > weightThreshold)
{
return true;
}
if (boneWeight.boneIndex3 == boneIndex && boneWeight.weight3 > weightThreshold)
{
return true;
}
return false;
}
/// <summary>
/// Computes the bounding box that contains all the vertices that a bone has influence on in a skinned mesh. The
/// bounding box is computed in local bone space.
/// </summary>
/// <param name="skin">Skinned mesh</param>
/// <param name="bone">Bone to check</param>
/// <param name="weightThreshold">Weight above which to consider significant influence</param>
/// <returns>
/// Bounding box in local <paramref name="bone" /> coordinates.
/// </returns>
public static Bounds GetBoneInfluenceBounds(SkinnedMeshRenderer skin, Transform bone, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
{
Transform[] skinBones = skin.bones;
int boneIndex = skinBones.IndexOf(bone);
if (boneIndex == -1)
{
return new Bounds();
}
Vector3[] vertices = skin.sharedMesh.vertices;
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
Transform[] bones = skin.bones;
Matrix4x4[] boneBindPoses = skin.sharedMesh.bindposes;
Vector3 min = Vector3.zero;
Vector3 max = Vector3.zero;
bool initialized = false;
for (int i = 0; i < boneWeights.Length; ++i)
{
if (HasBoneInfluence(boneWeights[i], boneIndex, weightThreshold))
{
Vector3 localVertex = bones[boneIndex].InverseTransformPoint(GetSkinnedWorldVertex(skin, boneWeights[i], vertices[i], bones, boneBindPoses));
if (!initialized)
{
initialized = true;
min = localVertex;
max = localVertex;
}
else
{
min = Vector3Ext.Min(localVertex, min);
max = Vector3Ext.Max(localVertex, max);
}
}
}
return new Bounds((min + max) * 0.5f, max - min);
}
/// <summary>
/// Gets a skinned vertex in world coordinates.
/// </summary>
/// <param name="skin">Skin</param>
/// <param name="boneWeight">Vertex bone weights info</param>
/// <param name="vertex">Vertex in local skin coordinates when the skin is in the bind pose</param>
/// <param name="bones">Bone list</param>
/// <param name="boneBindPoses">Bone bind poses</param>
/// <returns>Vertex in world coordinates</returns>
public static Vector3 GetSkinnedWorldVertex(SkinnedMeshRenderer skin, BoneWeight boneWeight, Vector3 vertex, Transform[] bones, Matrix4x4[] boneBindPoses)
{
Vector3 result = Vector3.zero;
if (boneWeight.weight0 > UxrConstants.Geometry.SmallestBoneWeight)
{
result += bones[boneWeight.boneIndex0].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex0].MultiplyPoint(vertex)) * boneWeight.weight0;
}
if (boneWeight.weight1 > UxrConstants.Geometry.SmallestBoneWeight)
{
result += bones[boneWeight.boneIndex1].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex1].MultiplyPoint(vertex)) * boneWeight.weight1;
}
if (boneWeight.weight2 > UxrConstants.Geometry.SmallestBoneWeight)
{
result += bones[boneWeight.boneIndex2].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex2].MultiplyPoint(vertex)) * boneWeight.weight2;
}
if (boneWeight.weight3 > UxrConstants.Geometry.SmallestBoneWeight)
{
result += bones[boneWeight.boneIndex3].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex3].MultiplyPoint(vertex)) * boneWeight.weight3;
}
return result;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e562213b88d80624a867d53e83926632
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="RendererExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System.Collections.Generic;
using UltimateXR.Extensions.System;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Renderer" /> extensions.
/// </summary>
public static class RendererExt
{
#region Public Methods
/// <summary>
/// Calculates the <see cref="Bounds" /> encapsulating a set of renderers.
/// </summary>
/// <param name="renderers">Renderers to compute the bounds for</param>
/// <returns><see cref="Bounds" /> encapsulating all renderers</returns>
public static Bounds CalculateBounds(this IEnumerable<Renderer> renderers)
{
renderers.ThrowIfNull(nameof(renderers));
Bounds bounds = default;
bool isFirst = true;
foreach (Renderer r in renderers)
{
Bounds b = r.bounds;
if (isFirst)
{
bounds = r.bounds;
isFirst = false;
}
else
{
bounds.Encapsulate(b);
}
}
return bounds;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5aa895f60ddf8374f801421c8b5b8c45
timeCreated: 1604664051

View File

@@ -0,0 +1,26 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ShaderExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Shader" /> extensions.
/// </summary>
public static class ShaderExt
{
#region Public Types & Data
public const string ShaderBase = "UltimateXR/";
public static Shader UnlitAdditiveColor => Shader.Find($"{ShaderBase}Basic Unlit/Unlit Additive Color");
public static Shader UnlitTransparentColor => Shader.Find($"{ShaderBase}Basic Unlit/Unlit Transparent Color");
public static Shader UnlitTransparentColorNoDepthTest => Shader.Find($"{ShaderBase}Basic Unlit/Unlit Transparent Color (No Depth Test)");
public static Shader UnlitOverlayFade => Shader.Find($"{ShaderBase}Basic Unlit/Overlay Fade");
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 109b8a7bffaf6ca4195de7a878d7ecef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,89 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SpriteExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Extensions.System.IO;
using UnityEngine;
using UnityEngine.UI;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Sprite" /> extensions.
/// </summary>
public static class SpriteExt
{
#region Public Methods
/// <summary>
/// Creates a sprite, for a given <see cref="Image" /> using a <see cref="Texture2D" />.
/// </summary>
/// <param name="targetImage">Image component the sprite will be used for</param>
/// <param name="texture2D">Texture</param>
/// <returns>Loaded sprite</returns>
public static Sprite FromTexture(Image targetImage, Texture2D texture2D)
{
RectTransform t = targetImage.rectTransform;
Vector2 size = t.sizeDelta;
Rect rect = new Rect(0.0f, 0.0f, size.x, size.y);
return Sprite.Create(texture2D, rect, t.pivot);
}
/// <summary>
/// Loads asynchronously a sprite from a given file <paramref name="uri" />. See <see cref="FileExt.Read" /> for
/// information on the file location.
/// </summary>
/// <param name="targetImage">Image component the sprite will be used for</param>
/// <param name="uri">File location. <see cref="FileExt.Read" /> for more information</param>
/// <param name="ct">Optional cancellation token, to cancel the operation.</param>
/// <returns>An awaitable <seealso cref="Task" /> that returns the loaded sprite</returns>
/// <exception cref="ArgumentNullException"><paramref name="uri" /> is null or empty</exception>
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
/// <exception cref="FileNotFoundException">The file specified in <paramref name="uri" /> was not found.</exception>
/// <exception cref="NotSupportedException"><paramref name="uri" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="InvalidOperationException">The stream is currently in use by a previous read operation.</exception>
public static async Task<Sprite> ReadSpriteFileAsync(Image targetImage, string uri, CancellationToken ct = default)
{
Texture2D texture2D = await Texture2DExt.FromFile(uri, ct);
RectTransform t = targetImage.rectTransform;
Vector2 size = t.sizeDelta;
Rect rect = new Rect(0.0f, 0.0f, size.x, size.y);
return Sprite.Create(texture2D, rect, t.pivot);
}
/// <summary>
/// Loads asynchronously a sprite encoded in a base64 <see cref="string" />.
/// </summary>
/// <param name="targetImage">Image component the sprite will be used for</param>
/// <param name="base64">String encoding the file in base64</param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <returns>An awaitable <seealso cref="Task" /> that returns the loaded sprite</returns>
/// <exception cref="ArgumentNullException"><paramref name="base64" /> is null or empty</exception>
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
/// <exception cref="FormatException">
/// The length of <paramref name="base64" />, ignoring white-space characters, is not
/// zero or a multiple of 4.
/// </exception>
public static async Task<Sprite> ReadSpriteBase64Async(Image targetImage, string base64, CancellationToken ct = default)
{
Texture2D texture2D = await Texture2DExt.FromBase64(base64, ct);
RectTransform t = targetImage.rectTransform;
Vector2 size = t.sizeDelta;
Rect rect = new Rect(0.0f, 0.0f, size.x, size.y);
return Sprite.Create(texture2D, rect, t.pivot);
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d67dd4d3dca574479f1d39dcd6f6049
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,102 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="Texture2DExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Extensions.System;
using UltimateXR.Extensions.System.IO;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Render
{
/// <summary>
/// <see cref="Texture2D" /> extensions.
/// </summary>
public static class Texture2DExt
{
#region Public Methods
/// <summary>
/// Creates a texture with a flat color.
/// </summary>
/// <param name="width">Width in pixels</param>
/// <param name="height">Height in pixels</param>
/// <param name="color">Color to fill the texture with</param>
/// <returns>The created <see cref="Texture2D" /> object</returns>
public static Texture2D Create(int width, int height, Color32 color)
{
Texture2D tex = new Texture2D(width, height);
Color32[] pixels = new Color32[width * height];
for (int i = 0; i < pixels.Length; ++i)
{
pixels[i] = color;
}
tex.SetPixels32(pixels);
tex.Apply();
return tex;
}
/// <summary>
/// Loads asynchronously a texture from a given file <paramref name="uri" />. See <see cref="FileExt.Read" /> for
/// information on the file location.
/// </summary>
/// <param name="uri">Location of the texture file. See <see cref="FileExt.Read" /></param>
/// <param name="ct">Optional cancellation token, to cancel the operation.</param>
/// <returns>An awaitable <seealso cref="Task" /> that returns the loaded texture</returns>
/// <exception cref="ArgumentNullException"><paramref name="uri" /> is null or empty</exception>
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
/// <exception cref="FileNotFoundException">The file specified in <paramref name="uri" /> was not found.</exception>
/// <exception cref="NotSupportedException"><paramref name="uri" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="InvalidOperationException">The stream is currently in use by a previous read operation. </exception>
public static async Task<Texture2D> FromFile(string uri, CancellationToken ct = default)
{
byte[] bytes = await FileExt.Read(uri, ct);
return FromBytes(bytes);
}
/// <summary>
/// Loads asynchronously a texture from a file encoded in a base64 <seealso cref="string" />.
/// </summary>
/// <param name="base64">The base 64 image string</param>
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
/// <exception cref="ArgumentNullException"><paramref name="base64" /> is null or empty</exception>
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
/// <exception cref="FormatException">
/// The length of <paramref name="base64" />, ignoring white-space characters, is not
/// zero or a multiple of 4.
/// </exception>
/// <returns>An awaitable <see cref="Task" /> that returns the loaded texture, or null if it could not be loaded</returns>
public static async Task<Texture2D> FromBase64(string base64, CancellationToken ct = default)
{
base64.ThrowIfNullOrWhitespace(nameof(base64));
// Screenshot is from a file embedded in a string in base64 format
byte[] bytes = await Task.Run(() => Convert.FromBase64String(base64), ct);
return FromBytes(bytes);
}
/// <summary>
/// Loads a texture from a file loaded in a byte array.
/// </summary>
/// <param name="bytes">Image file byte array</param>
/// <returns>The loaded <see cref="Texture2D" /></returns>
public static Texture2D FromBytes(byte[] bytes)
{
bytes.ThrowIfNull(nameof(bytes));
var tex = new Texture2D(2, 2);
tex.LoadImage(bytes);
return tex;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c80a81688bca2614891cdf6dc8fe4136
timeCreated: 1620807232