跳至主要內容

Unity3D中使用SkiaSharp处理Texture2D (下篇)

MonoLogueChiUnity大约 5 分钟

这一篇主要勘正上一篇文章的错误以及使用 Job 提高转换效率。

看本片文章之前,请先阅读我的上一篇文章。

上一篇的错误

上一篇文章中提到的从 GPU 读取数据的方法,没有考虑到本机不支持的格式。所有代码以本文为准。

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;
    }
  }
}