Unity3D中使用SkiaSharp处理Texture2D
在 Unity 中,一些时候需要动态修改一些贴图,其中一种方式,就是使用图形处理库,例如 SkiaSharp 或 ImageSharp。这篇文章简单说一下在 Unity 中如何使用 SkiaSharp。
最近一段时间工作比较忙,一直没有更新博客,这一篇文章简单记录一些最近的收获。
这篇文章主要是讲 Texture2D 和 SKBitmap 之间的转换,至于 SkiaSharp 的 API,请自行查阅相关文档。
由于要讲的东西篇幅很长,循序渐进,由浅入深讲解,千万不要看到代码就抄,很多是中间的思考过程。
准备工作
需要安装 SkiaSharp ,可以直接使用我的 UnitySkiaSharp,相关代码我以及封装好了,这篇文章只是分析一下转换过程。
借助 PNG 编码转换
这是效率最低的一种方式,除非是特殊情况,请不要使用这种方式。
using SkiaSharp;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(RawImage))]
public class Example0 : MonoBehaviour
{
[SerializeField] private Texture2D tex;
private RawImage _rawImage;
private void Start()
{
_rawImage = GetComponent<RawImage>();
var pngData0 = tex.EncodeToPNG();
var bitmap = SKBitmap.Decode(pngData0);
var pngData1 = bitmap.Encode(SKEncodedImageFormat.Png, 0).ToArray();
var tex0 = new Texture2D(bitmap.Width, bitmap.Height);
tex0.LoadImage(pngData1);
_rawImage.texture = tex0;
_rawImage.GetComponent<RectTransform>().sizeDelta = new Vector2(tex0.width, tex0.height);
}
}
运行效果就不展示了。
使用颜色填充
前面介绍的这种方法,需要编码为 png 格式,然后再解码,中间的编码和解码过程需要浪费大量的性能。
试想一下,能否获取图片中的每一个点的颜色,然后再填充到新的图片中。
在此之前,需要先编写一个转换程序,用于在 Unity 和 SkiaSharp 之间转换颜色
using UnityEngine;
namespace SkiaSharp.Unity
{
/// <summary>
/// 颜色转换
/// </summary>
public static class ColorConverter
{
/// <summary>
/// 转换到Unity颜色
/// </summary>
/// <param name="skColorF"></param>
/// <returns></returns>
public static Color ToUnityColor(this SKColorF skColorF)
{
return new Color(skColorF.Red, skColorF.Green, skColorF.Blue, skColorF.Alpha);
}
/// <summary>
/// 转换到Unity颜色
/// </summary>
/// <param name="skColor"></param>
/// <returns></returns>
public static Color32 ToUnityColor32(this SKColor skColor)
{
return new Color32(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha);
}
/// <summary>
/// 转换到SKColorF
/// </summary>
/// <param name="color"></param>
/// <returns></returns>
public static SKColorF ToSkColorF(this Color color)
{
return new SKColorF(color.r, color.g, color.b, color.a);
}
/// <summary>
/// 转换到SKColor
/// </summary>
/// <param name="color32"></param>
/// <returns></returns>
public static SKColor ToSkColor(this Color32 color32)
{
return new SKColor(color32.r, color32.g, color32.b, color32.a);
}
}
}
然后使用 GetPixels32()
和 SetPixels32()
操作 Texture2D,简单写个示例。
using System.Linq;
using SkiaSharp;
using SkiaSharp.Unity;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(RawImage))]
public class Example1 : MonoBehaviour
{
[SerializeField] private string texPath;
private RawImage _rawImage;
private void Start()
{
_rawImage = GetComponent<RawImage>();
var bitmap = SKBitmap.Decode(texPath);
var colors = bitmap.Pixels.AsParallel().AsOrdered().Select(s => s.ToUnityColor32()).ToArray();
var tex0 = new Texture2D(bitmap.Width, bitmap.Height);
tex0.SetPixels32(colors);
tex0.Apply();
_rawImage.texture = tex0;
_rawImage.GetComponent<RectTransform>().sizeDelta = new Vector2(tex0.width, tex0.height);
}
}
运行完成后,会发现图片是反的。
这是 Unity 中 Texture2D 和 SkiaSharp 图片读取位置不一致导致的。简单画了一张示意图,可以看一下结构。
由图可知,需要调整颜色的像素点顺序。知道原理后,修改一下代码。
using System;
using System.Buffers;
using System.Linq;
using SkiaSharp;
using SkiaSharp.Unity;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(RawImage))]
public class Example1 : MonoBehaviour
{
[SerializeField] private string texPath;
private RawImage _rawImage;
private void Start()
{
_rawImage = GetComponent<RawImage>();
var bitmap = SKBitmap.Decode(texPath);
var skcolors = bitmap.Pixels.AsSpan();
var writer = new ArrayBufferWriter<SKColor>(bitmap.Width * bitmap.Height);
for (var i = bitmap.Height - 1; i >= 0; i--) writer.Write(skcolors.Slice(i * bitmap.Width, bitmap.Width));
var colors = writer.WrittenSpan.ToArray().AsParallel().AsOrdered().Select(s => s.ToUnityColor32()).ToArray();
var tex0 = new Texture2D(bitmap.Width, bitmap.Height);
tex0.SetPixels32(colors);
tex0.Apply();
_rawImage.texture = tex0;
_rawImage.GetComponent<RectTransform>().sizeDelta = new Vector2(tex0.width, tex0.height);
}
}
修改代码,再次运行之后就可以正确加载贴图了。
反过来,从 Texture2D 到 SKBitmap 也是一样的,使用 GetPixels32()
获取数据,这里就不再赘述。
直接填充数据
我们再思考一个问题,能否不去填充像素点的颜色,而是直接填充数据,这样转换效率会更高。
当 SKBitmap 的 ColorType 与 Texture2D 的 TextureFormat,对应时,内存数据一致,调整数据后直接填充进去就可以了。
我给出下面的示例代码,可以看一下。
using System;
using System.Buffers;
using SkiaSharp;
using UnityEngine;
using UnityEngine.UI;
public class Example2 : MonoBehaviour
{
[SerializeField] private Texture2D tex;
private RawImage _rawImage;
private void Start()
{
_rawImage = GetComponent<RawImage>();
// 以下代码都是 已知 tex 的 TextureFormat 为 RGBA32 的情况下
var bitmap = new SKBitmap(tex.width, tex.height, SKColorType.Rgba8888, SKAlphaType.Opaque);
var data = tex.GetPixelData<byte>(0).AsReadOnlySpan();
var writer = new ArrayBufferWriter<byte>(tex.width * tex.height * 4);
for (var i = tex.height - 1; i >= 0; i--) writer.Write(data.Slice(i * tex.width * 4, tex.width * 4));
var span = writer.WrittenSpan;
unsafe
{
fixed (byte* ptr = span)
{
bitmap.SetPixels((IntPtr)ptr);
}
}
var tex0 = new Texture2D(bitmap.Width, bitmap.Height, TextureFormat.RGBA32, false);
var data0 = bitmap.GetPixelSpan();
var writer0 = new ArrayBufferWriter<byte>(bitmap.Width * bitmap.Height * 4);
for (var i = bitmap.Height - 1; i >= 0; i--) writer0.Write(data0.Slice(i * bitmap.Width * 4, bitmap.Width * 4));
tex0.SetPixelData(writer0.WrittenSpan.ToArray(), 0);
tex0.Apply();
_rawImage.texture = tex0;
_rawImage.GetComponent<RectTransform>().sizeDelta = new Vector2(tex0.width, tex0.height);
}
}
从 GPU 读取数据
前面的所有代码,都是基于 Texture 数据在 CPU 内存中编写的,即 texture2d.isReadable = true
,如果数据不在 CPU 内存中,则需要在显卡中读取数据。
/// <summary>
/// 从GUP读取贴图
/// </summary>
/// <param name="texture"></param>
/// <returns></returns>
public static Texture2D GetTextureFromGpu(this Texture texture)
{
var width = texture.width;
var height = texture.height;
Texture2D tex2;
if (texture.GetTextureDataFromGpu(out var data))
{
tex2 = new Texture2D(width, height, texture.graphicsFormat, TextureCreationFlags.None);
tex2.SetPixelData(data, 0);
}
else
{
var renderTexture = new RenderTexture(width, height, 32);
Graphics.Blit(texture, renderTexture);
var tmpTexture = RenderTexture.active;
RenderTexture.active = renderTexture;
tex2 = new Texture2D(width, height);
tex2.ReadPixels(new Rect(0, 0, width, height), 0, 0);
RenderTexture.active = tmpTexture;
}
return tex2;
}
/// <summary>
/// 从GPU读取数据
/// </summary>
/// <param name="texture"></param>
/// <param name="data"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static bool GetTextureDataFromGpu(this Texture texture, out NativeArray<byte> data)
{
#if UNITY_2023_2_OR_NEWER
if (SystemInfo.IsFormatSupported(texture.graphicsFormat, GraphicsFormatUsage.ReadPixels))
#else
if (SystemInfo.IsFormatSupported(texture.graphicsFormat, FormatUsage.ReadPixels))
#endif
{
var request = AsyncGPUReadback.Request(texture, 0, texture.graphicsFormat);
request.WaitForCompletion();
if (request.hasError) throw new Exception("");
data = request.GetData<byte>();
return true;
}
data = new NativeArray<byte>();
return false;
}
然后修改前面所写的代码,判断是否可读,如果可读就按前面所写的做,如果不可读,就改为从 GPU 获取数据。
使用 Job 和位运算提高效率
NativeArray
要想使用 Job,就一定会需要 NativeArray<T>
,但是阅读上一篇的代码可知,使用更多的是 Span<T>
和 ReadOnlySpan<T>
,所以需要转换。
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
namespace SkiaSharp.Unity
{
public static class NativeArrayExt
{
/// <summary>
/// 转换到 NativeArray
/// </summary>
/// <param name="span"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static unsafe NativeArray<T> AsNativeArray<T>(this ReadOnlySpan<T> span) where T : unmanaged
{
fixed (void* source = span)
{
var data = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(source, span.Length, Allocator.None);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref data, AtomicSafetyHandle.Create());
#endif
return data;
}
}
/// <summary>
/// 转换到 NativeArray
/// </summary>
/// <param name="span"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static unsafe NativeArray<T> AsNativeArray<T>(this Span<T> span) where T : unmanaged
{
fixed (void* source = span)
{
var data = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(source, span.Length, Allocator.None);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref data, AtomicSafetyHandle.Create());
#endif
return data;
}
}
}
}
使用 Job 并行计算
使用 Job 并行计算,在一定程度上可以加快颜色格式转换效率,而且这里使用了位运算转换颜色格式。
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
namespace SkiaSharp.Unity
{
/// <summary>
/// 颜色转换
/// </summary>
public static class ColorConverter
{
/// <summary>
/// 转换到 Unity 颜色
/// </summary>
/// <param name="skColorF"></param>
/// <returns></returns>
public static Color ToUnityColor(this SKColorF skColorF)
{
return new Color(skColorF.Red, skColorF.Green, skColorF.Blue, skColorF.Alpha);
}
/// <summary>
/// 转换到 Unity 颜色
/// </summary>
/// <param name="skColor"></param>
/// <returns></returns>
public static Color32 ToUnityColor32(this SKColor skColor)
{
return new Color32(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha);
}
/// <summary>
/// 转换到 SKColorF
/// </summary>
/// <param name="color"></param>
/// <returns></returns>
public static SKColorF ToSkColorF(this Color color)
{
return new SKColorF(color.r, color.g, color.b, color.a);
}
/// <summary>
/// 转换到 SKColor
/// </summary>
/// <param name="color32"></param>
/// <returns></returns>
public static SKColor ToSkColor(this Color32 color32)
{
return new SKColor(color32.r, color32.g, color32.b, color32.a);
}
/// <summary>
/// 批量转换到 Color32
/// </summary>
/// <param name="colors"></param>
/// <param name="batchCount"></param>
/// <returns></returns>
public static NativeArray<Color32> ConvertToColor32(NativeArray<SKColor> colors, int batchCount = 512)
{
var handle = FastColorConverter(colors.Reinterpret<uint>(), out var data, batchCount);
handle.Complete();
return data.Reinterpret<Color32>();
}
/// <summary>
/// 批量转换到 SKColor
/// </summary>
/// <param name="colors"></param>
/// <param name="batchCount"></param>
/// <returns></returns>
public static NativeArray<SKColor> ConvertToSkColor(NativeArray<Color32> colors, int batchCount = 512)
{
var handle = FastColorConverter(colors.Reinterpret<uint>(), out var data, batchCount);
handle.Complete();
return data.Reinterpret<SKColor>();
}
/// <summary>
/// 快速转换颜色
/// </summary>
/// <param name="dataIn"></param>
/// <param name="dataOut"></param>
/// <param name="batchCount"></param>
public static JobHandle FastColorConverter(NativeArray<uint> dataIn, out NativeArray<uint> dataOut, int batchCount = 512)
{
dataOut = new NativeArray<uint>(dataIn.Length, Allocator.TempJob);
var job = new ColorConverterJob
{
DataIn = dataIn,
DataOut = dataOut
};
return job.Schedule(dataIn.Length, batchCount);
}
[BurstCompile]
private struct ColorConverterJob : IJobParallelFor
{
[ReadOnly] public NativeArray<uint> DataIn;
public NativeArray<uint> DataOut;
private const uint Mask0 = 0x00FF0000;
private const uint Mask1 = 0x000000FF;
public void Execute(int index)
{
var color = DataIn[index];
DataOut[index] = ((color & Mask0) >> 16) | ((color & Mask1) << 16) | (color & ~(Mask0 | Mask1));
}
}
}
}
贴图转换
using System;
using System.Buffers;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
namespace SkiaSharp.Unity
{
public static class Texture2DConverter
{
public static Texture2D ToTexture2D(this SKBitmap bitmap, int width = 0, int height = 0,
SKSamplingOptions? options = null)
{
var resize = width != 0 || height != 0;
width = width == 0 ? bitmap.Width : width;
height = height == 0 ? bitmap.Height : height;
if (resize) bitmap = bitmap.Resize(new SKSizeI(width, height), options ?? SKSamplingOptions.Default);
Texture2D texture2D;
var l = bitmap.ColorType.TryConvertToTextureFormat(out var textureFormat);
if (l > 0)
{
var data = bitmap.GetPixelSpan();
var writer = new ArrayBufferWriter<byte>(width * height * l);
for (var i = height - 1; i >= 0; i--) writer.Write(data.Slice(i * width * l, width * l));
texture2D = new Texture2D(width, height, textureFormat, false);
texture2D.SetPixelData(writer.WrittenSpan.ToArray(), 0);
}
else
{
var data0 = bitmap.Pixels.AsSpan();
var writer = new ArrayBufferWriter<SKColor>();
for (var i = height - 1; i >= 0; i--) writer.Write(data0.Slice(i * width, width));
var data1 = writer.WrittenSpan.AsNativeArray();
var colors = ColorConverter.ConvertToColor32(data1, width * 64);
texture2D = new Texture2D(width, height, textureFormat, false);
texture2D.SetPixels32(colors.ToArray());
data1.Dispose();
colors.Dispose();
}
texture2D.Apply();
return texture2D;
}
public static SKBitmap ToSkBitmap(this Texture2D texture2D, int width = 0, int height = 0,
SKSamplingOptions? options = null)
{
var resize = width != 0 || height != 0;
width = width == 0 ? texture2D.width : width;
height = height == 0 ? texture2D.height : height;
SKBitmap bitmap;
var l = texture2D.format.TryConvertSkColorTypes(out var skColorType);
if (l > 0 && texture2D.isReadable)
{
ReadOnlySpan<byte> data = texture2D.GetPixelData<byte>(0);
var writer = new ArrayBufferWriter<byte>(width * height * l);
for (var i = height - 1; i >= 0; i--) writer.Write(data.Slice(i * width * l, width * l));
var span = writer.WrittenSpan;
unsafe
{
fixed (byte* ptr = span)
{
bitmap = new SKBitmap(width, height, skColorType, SKAlphaType.Premul);
bitmap.SetPixels((IntPtr)ptr);
}
}
}
else if (l > 0 && texture2D.GetTextureDataFromGpu(out var textureData))
{
ReadOnlySpan<byte> data = textureData;
var writer = new ArrayBufferWriter<byte>(width * height * l);
for (var i = height - 1; i >= 0; i--) writer.Write(data.Slice(i * width * l, width * l));
var span = writer.WrittenSpan;
unsafe
{
fixed (byte* ptr = span)
{
bitmap = new SKBitmap(width, height, skColorType, SKAlphaType.Premul);
bitmap.SetPixels((IntPtr)ptr);
}
}
}
else
{
var data0 = (texture2D.isReadable ? texture2D : texture2D.GetTextureFromGpu()).GetPixels32();
var data1 = new NativeArray<Color32>(data0, Allocator.TempJob);
var data2 = ColorConverter.ConvertToSkColor(data1, width * 64);
var skColors = data2.AsSpan();
var writer = new ArrayBufferWriter<SKColor>();
for (var i = height - 1; i >= 0; i--) writer.Write(skColors.Slice(i * width, width));
bitmap = new SKBitmap(texture2D.width, texture2D.height, skColorType, SKAlphaType.Premul);
bitmap.Pixels = writer.WrittenSpan.ToArray();
data1.Dispose();
data2.Dispose();
}
if (resize) bitmap = bitmap.Resize(new SKSizeI(width, height), options ?? SKSamplingOptions.Default);
return bitmap;
}
/// <summary>
/// 从GUP读取贴图
/// </summary>
/// <param name="texture"></param>
/// <returns></returns>
public static Texture2D GetTextureFromGpu(this Texture texture)
{
var width = texture.width;
var height = texture.height;
Texture2D tex2;
if (texture.GetTextureDataFromGpu(out var data))
{
tex2 = new Texture2D(width, height, texture.graphicsFormat, TextureCreationFlags.None);
tex2.SetPixelData(data, 0);
}
else
{
var renderTexture = new RenderTexture(width, height, 32);
Graphics.Blit(texture, renderTexture);
var tmpTexture = RenderTexture.active;
RenderTexture.active = renderTexture;
tex2 = new Texture2D(width, height);
tex2.ReadPixels(new Rect(0, 0, width, height), 0, 0);
RenderTexture.active = tmpTexture;
}
return tex2;
}
/// <summary>
/// 从GPU读取数据
/// </summary>
/// <param name="texture"></param>
/// <param name="data"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static bool GetTextureDataFromGpu(this Texture texture, out NativeArray<byte> data)
{
#if UNITY_2023_2_OR_NEWER
if (SystemInfo.IsFormatSupported(texture.graphicsFormat, GraphicsFormatUsage.ReadPixels))
#else
if (SystemInfo.IsFormatSupported(texture.graphicsFormat, FormatUsage.ReadPixels))
#endif
{
var request = AsyncGPUReadback.Request(texture, 0, texture.graphicsFormat);
request.WaitForCompletion();
if (request.hasError) throw new Exception("");
data = request.GetData<byte>();
return true;
}
data = new NativeArray<byte>();
return false;
}
}
}
using System;
using UnityEngine;
namespace SkiaSharp.Unity
{
internal static class ColorTypeConverter
{
private static readonly SKColorType[] SkColorTypes =
{
SKColorType.Alpha8,
SKColorType.Rgb565,
SKColorType.Rgba8888,
SKColorType.Rgb888x,
SKColorType.Bgra8888,
SKColorType.RgbaF16,
SKColorType.RgbaF16Clamped,
SKColorType.RgbaF32,
SKColorType.Rg88,
SKColorType.RgF16,
SKColorType.Rg1616,
SKColorType.Rgba16161616,
SKColorType.Rgba1010102,
SKColorType.Rgb101010x,
SKColorType.Gray8,
SKColorType.AlphaF16,
SKColorType.Alpha16,
SKColorType.Bgra1010102,
SKColorType.Bgr101010x
};
private static readonly TextureFormat[] TextureFormats =
{
TextureFormat.Alpha8,
TextureFormat.RGB565,
TextureFormat.RGBA32,
TextureFormat.RGBA32,
TextureFormat.BGRA32,
TextureFormat.RGBAHalf,
TextureFormat.RGBAHalf,
TextureFormat.RGBAFloat,
TextureFormat.RG16,
TextureFormat.RGHalf,
TextureFormat.RG32,
TextureFormat.RGBA64,
TextureFormat.RGBA64,
TextureFormat.RGBA64,
TextureFormat.RGBA32,
TextureFormat.RGBAHalf,
TextureFormat.RGBA64,
TextureFormat.RGBA64,
TextureFormat.RGBA64
};
public static readonly int[] LInts = { 1, 2, 4, 4, 4, 8, 8, 16, 2, 4, 4, 8, 0, 0, 0, 0, 0, 0, 0 };
public static int TryConvertToTextureFormat(this SKColorType skColorType, out TextureFormat textureFormat)
{
var index = Array.IndexOf(SkColorTypes, skColorType);
if (index >= 0)
{
textureFormat = TextureFormats[index];
return LInts[index];
}
textureFormat = TextureFormat.RGBA32;
return 0;
}
public static int TryConvertSkColorTypes(this TextureFormat textureFormat, out SKColorType skColorType)
{
var index = Array.IndexOf(TextureFormats, textureFormat);
if (index >= 0)
{
skColorType = SkColorTypes[index];
return LInts[index];
}
skColorType = SKColorType.Rgba8888;
return 0;
}
}
}