サイト内を検索

ランタイム画像圧縮

はじめに

皆様こんにちは。
クライエントエンジニアのKと申します。

ココネのサービスでは、ユーザーが画像を保存したり、他のユーザーと共有したりできる機能を提供しています。しかし、特にモバイル端末では、ユーザーが提供した大量の画像を読み込むと、メモリに負荷がかかることがあります。そのため、今回はいくつかの方法を調査し、Unityで比較しました。

問題

Unityでは、画像をPNG形式で保存し、サーバーにアップロードする方法をよく使用しますが、この方法では非圧縮のテクスチャになってしまいます。

// 保存
RenderTexture.active = rt;
Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false);
tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
RenderTexture.active = null;
byte[] bytes = tex.EncodeToPNG();

// ロード
Texture2D tex2 = new Texture2D(2, 2);
ImageConversion.LoadImage(tex2, bytes, markNonReadable: true);

 

たとえば、サイズ1080×1920の写真には5.9MBがあります。

それを解決するための2つの方法があります。1つはサイズ変更、もう1つは圧縮です。ここでは圧縮について話します。

圧縮

ETC2とASTCは、モバイルプラットフォームで最も一般的な圧縮フォーマットです。

圧縮されたデータをロードするには、Texture2D.LoadRawTextureDataを使用します。

var tex = new Texture2D(width, height, TextureFormat.ETC2_RGBA8);
tex.LoadRawTextureData(bytes);
tex.Apply(updateMipmaps: false, makeNoLongerReadable: true);

ETC2

Unity

Unity Editorでは、複数のETCコンプレッサーを提供しています。

そして、Unity 2021.1以降、Texture2D.CompressでランタイムETCの圧縮を提供します。

On Android, iOS and tvOS, this will compress the texture to the ETC/EAC family of formats.

 

使用方法は非常に簡単です。非コンパスリーズテクスチャでTexture2D.Compressを呼び出すだけです。Unityスタッフからの返信によると、内部にetcpakを使用しています。
etcpakは最も高速な圧縮ツールとされています。バージョン1.0以降では、「QuickETC2: Fast ETC2 texture compression using Luma differences」という論文で提案された手法を採用しています。

さらに興味深い点は、この著者がETC圧縮に焦点を当てており、最近2つの論文を提出したことです。
– H-ETC2: Design of a CPU-GPU Hybrid ETC2 Encoder
– QuickETC2-HQ: Improved ETC2 encoding techniques for real-time, high-quality texture compression
CPUとGPUの両方を使用するハイブリッドの方法を提供します。興味があれば 見てください。

Unreal

Unreal 4.26以降には、Compute Shaderを使用して実行時のETC圧縮を行う「ETCCompressionCommon.ush」というファイルがあります。

Unityで使用するには、C#とCompute Shaderを作成する必要があります。

 

– C#

var cs = Resources.Load<ComputeShader>("ETCCompress");

if (texture.format == TextureFormat.RGBA32) cs.EnableKeyword("HAS_ALPHA");
else cs.DisableKeyword("HAS_ALPHA");

var kernelHandle = cs.FindKernel("CSMain");

using var outBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 
    (texture.width / 4) * (texture.height / 4), 
    sizeof(uint) * (texture.format == TextureFormat.RGBA32 ? 4 : 2));
    
cs.SetBuffer(kernelHandle, ResultID, outBuffer);
cs.SetTexture(kernelHandle, RenderTexture0ID, texture);
cs.SetInts(DestRectID, new int[4] { texture.width / 4, texture.height / 4, texture.width, texture.height });
cs.Dispatch(kernelHandle, (texture.width / 4 + 7) / 8, (texture.height / 4 + 7) / 8, 1);

var bytes = new byte[outBuffer.count * outBuffer.stride];
outBuffer.GetData(bytes); // AsyncGPUReadbackの使用をお勧めします

 

– Compute Shader

#include "ETCCompressionCommon.hlsl"
#include "BlockCompressionCommon.hlsl" 

#pragma multi_compile_local __ HAS_ALPHA

#pragma kernel CSMain

#if HAS_ALPHA
RWStructuredBuffer<uint4> Result;
#else
RWStructuredBuffer<uint2> Result;
#endif
Texture2D<float4> RenderTexture0;
SamplerState samplerRenderTexture0;
uint4 DestRect;

[numthreads(8, 8, 1)]
void CSMain(uint3 ThreadId : SV_DispatchThreadID)
{
    uint2 SamplePos = ThreadId.xy * 4;
    if (any(SamplePos >= DestRect.zw))
        return;

    float2 TexelUVSize = 1.f / float2(DestRect.zw);
    float2 SampleUV = (float2(SamplePos) + 0.5f) * TexelUVSize;

    float3 BlockBaseColor[16];
    ReadBlockRGB(RenderTexture0, samplerRenderTexture0, SampleUV, TexelUVSize, BlockBaseColor);

    uint blockID = ThreadId.y * DestRect.x + ThreadId.x;
    
#if HAS_ALPHA
    float BlockA[16];
    ReadBlockAlpha(RenderTexture0, samplerRenderTexture0, SampleUV, TexelUVSize, BlockA);
    Result[blockID] = CompressBlock_ETC2_RGBA(BlockBaseColor, BlockA); 
#else 
    Result[blockID] = CompressBlock_ETC2_RGB(BlockBaseColor); 
#endif
}

元のスクリプトはRWTexture2Dを使用していますが、一部の端末で問題が発生したため、RWStructuredBufferを使用します。

Betsy

Betsy GPU compressor

Godotで使用されるテクスチャ コンプレッサーで、複数のフォーマットをサポートしており、GLSLを使用しています。Unityに移植する時間がなかったため、今回はテストしていません。

ASTC

ARM

Arm ASTC Encoder

最も有名なASTCコンプレッサーで、多くのカスタマイズをサポートしています。

Unityで使用するにはネイティブプラグインを作成する必要があります。

 

– Android

公式サイトではAndroid版が提供されていないため、自分でコンパイルする必要があります。

export NDK_PATH=/Applications/Unity/Hub/Editor/2022.3.30f1/PlaybackEngines/AndroidPlayer/NDK
# armeabi-v7aとarm64-v8aの両方をコンパイルする必要があります
cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=$NDK_PATH/build/cmake/android.toolchain.cmake \
  -DANDROID_ABI=arm64-v8a \
  -DANDROID_PLATFORM=android-21 \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX=../install_android \
  -DASTCENC_SHAREDLIB=ON \
  -DASTCENC_CLI=OFF

C#で必要な構造体を定義し、関数をインポートします。

#if UNITY_IOS && !UNITY_EDITOR
    [DllImport("__Internal")]
#else
    [DllImport("libastcenc-shared")]
#endif
    public static extern astcenc_error astcenc_config_init(
        astcenc_profile profile,
        uint block_x,
        uint block_y,
        uint block_z,
        float quality,
        uint flags,
        ref astcenc_config config
    );

#if UNITY_IOS && !UNITY_EDITOR
    [DllImport("__Internal")]
#else
    [DllImport("libastcenc-shared")]
#endif
    public static extern astcenc_error astcenc_context_alloc(
        ref astcenc_config config,
        uint thread_count,
        out IntPtr context
    );

#if UNITY_IOS && !UNITY_EDITOR
    [DllImport("__Internal")]
#else
    [DllImport("libastcenc-shared")]
#endif
    public static extern astcenc_error astcenc_compress_image(
        IntPtr context,
        ref astcenc_image image,
        ref astcenc_swizzle swizzle,
        IntPtr data_out,
        ulong data_len,
        uint thread_index
    );

#if UNITY_IOS && !UNITY_EDITOR
    [DllImport("__Internal")]
#else
    [DllImport("libastcenc-shared")]
#endif
    public static extern void astcenc_context_free(IntPtr context);

– iOS

iOSでは静的リンクを使用する必要がありますが、Unity にはすでにArm ASTC Encoderの一部が組み込まれており、実行時にメモリ アクセス エラーが発生するようです。

これを解決するには、ソースコードを調整して名前空間を追加します。そして、C#へのラッパーを提供します。

#include <cstddef>
#include <cstdint>
#include "astcenc.h"

#ifdef __cplusplus
extern "C"
{
#endif

	ASTCENC_PUBLIC Cocone::astcenc_error astcenc_config_init(
		Cocone::astcenc_profile profile,
		unsigned int block_x,
		unsigned int block_y,
		unsigned int block_z,
		float quality,
		unsigned int flags,
		Cocone::astcenc_config *config)
	{
		return Cocone::astcenc_config_init(profile, block_x, block_y, block_z, quality, flags, config);
	}

	ASTCENC_PUBLIC Cocone::astcenc_error astcenc_context_alloc(
		const Cocone::astcenc_config *config,
		unsigned int thread_count,
		Cocone::astcenc_context **context)
	{
		return Cocone::astcenc_context_alloc(config, thread_count, context);
	}

	ASTCENC_PUBLIC Cocone::astcenc_error astcenc_compress_image(
		Cocone::astcenc_context *context,
		Cocone::astcenc_image *image,
		const Cocone::astcenc_swizzle *swizzle,
		uint8_t *data_out,
		size_t data_len,
		unsigned int thread_index)
	{
		return Cocone::astcenc_compress_image(context, image, swizzle, data_out, data_len, thread_index);
	}

	ASTCENC_PUBLIC void astcenc_context_free(
		Cocone::astcenc_context *context)
	{
		return Cocone::astcenc_context_free(context);
	}

#ifdef __cplusplus
}
#endif

astc_encoder

real time ASTC texture compression by computer shader

これはこの記事で見た方法です。

Qiita ランタイムでTexture2DをASTCテクスチャに圧縮しロードする

ASTC圧縮を行うためにCompute Shaderを使用しており、処理を高速化するために、影響の大きい検索のみを実行します。詳細については、https://niepp.github.io/2021/12/18/Compute-ASTC.html (中国語) を参照してください。4×4と6×6のブロックサイズのみサポートします。

 

– C#

int DimSize = blockWidth == BlockWidth._4x4 ? 4 : 6;
int TexWidth = texture.width;
int TexHeight = texture.height;
int xBlockNum = (TexWidth + DimSize - 1) / DimSize;
int yBlockNum = (TexHeight + DimSize - 1) / DimSize;
int TotalBlockNum = xBlockNum * yBlockNum;

int GroupSize = THREAD_NUM_X * THREAD_NUM_Y;
int GroupNum = (TotalBlockNum + GroupSize - 1) / GroupSize;
int GroupNumX = (TexWidth + DimSize - 1) / DimSize;
int GroupNumY = (GroupNum + GroupNumX - 1) / GroupNumX;

using var constData = new GraphicsBuffer(GraphicsBuffer.Target.Constant, 1, sizeof(uint) * 4);
using var outBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, TotalBlockNum, BLOCK_BYTES);

var cs = Resources.Load<ComputeShader>("ASTC_Encode");

if (blockWidth == BlockWidth._6x6) cs.EnableKeyword("BLOCK_6X6");
else cs.DisableKeyword("BLOCK_6X6");

if (texture.format == TextureFormat.RGBA32) cs.EnableKeyword("HAS_ALPHA");
else cs.DisableKeyword("HAS_ALPHA");

constData.SetData(new CSConstantBuffer[] { new CSConstantBuffer((uint)TexWidth, (uint)TexHeight, (uint)GroupNumX, (uint)TotalBlockNum) });
cs.SetConstantBuffer(constDataID, constData, 0, constData.stride * constData.count);
cs.SetTexture(0, inTextureID, texture);
cs.SetBuffer(0, outBufferID, outBuffer);
cs.Dispatch(0, GroupNumX, GroupNumY, 1);

var bytes = new byte[TotalBlockNum * BLOCK_BYTES];
outBuffer.GetData(bytes);  // AsyncGPUReadbackの使用をお勧めします

– Compute Shader

#pragma multi_compile_local __ BLOCK_6X6
#pragma multi_compile_local __ HAS_ALPHA

#pragma kernel MainCS
#include "ASTC_Encode.hlsl"

Androidでは次の警告が表示される場合があります。これを解決するには、大きなconstの代わりに StructuredBufferを使用します。

HLSLcc: Large chunk of constant data detected. GLSL and Vulkan might have trouble handling this.
Please consider putting large constant data arrays into a separate buffer resource instead. at kernel MainCS

 

– Compute Shader

#if defined(SHADER_API_VULKAN) || defined(SHADER_API_GLES3)
StructuredBuffer<int> scramble_table;
#else
static const int scramble_table[12 * 32] = { ... }
#endif

– C#

if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Vulkan || SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES3)
{
    scrambleTableBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, scramble_table.Length, sizeof(int));
    scrambleTableBuffer.SetData(scramble_table);
    cs.SetBuffer(0, "scramble_table", scrambleTableBuffer);
}

実験

実験方法

モバイルプラットフォームでどちらが優れているかを判断するために、次の方法をテストします。

Arm ASTC Encoderでは最高速モードを使用します。

RGBAとRGBテクスチャの両方でテストします。各サイズは1080×1920です。

 

そしてサイズ、実行時間、画質評価を測定します。画像評価には最も一般的な方法を使用します。

  • SSIM (Structural similarity index measure)
    • 0 ~ 1 (高いほど良い)
  • PSNR (Peak signal-to-noise ratio)
    • 0 ~ 100 dB (高いほど良い)

各テクスチャは 3 回実行され、実行時間を平均します。

入力は Texture2D で、出力はバイトです。

実験環境

Unity 2022.3.30f1


結果

サイズ

・RGBA32:7.9 MB

・RGB24:5.9 MB

 

・RGBAの場合、ブロックが少なくとも6×6の場合、ASTCテクスチャは小さくなります。

・RGBの場合、ブロックが6×6より大きいとASTCテクスチャは小さくなります。

 

実行時間

  • Compute ShaderのパフォーマンスはAndroidではそれほど良くなく、問題を引き起こす可能性があります。
    • astc_encoderでは、Nokia 8の実行時にクラッシュしました。端末によっては、コンピュート シェーダーが十分にサポートされていない可能性があります。
  • Arm ASTC Encoderは非常に遅いので、サービスで使用するのは難しいかもしれません。
  • 実験ではGraphicsBuffer.GetDataを使用してデータを取得しますが、実際のサービスではメインスレッドをブロックしないようにAsyncGPUReadbackを使用することをお勧めします。

画面評価

SSIM

UnrealのETCCompressionCommonを除いて、すべて品質が良いです。

 

PSNR

30 dB を超えていれば品質は許容範囲内になります。

  • UnrealのETCCompressionCommonを除いて、すべて品質が良いです。
  • Arm ASTC Encoderは最高の品質を提供します。

 

結論

  • UnrealのETCCompressionCommonの圧縮画像の品質が低いです。
  • Arm ASTC Encoderは最高の品質と最小のサイズを提供しますが、速度が遅いです。
  • astc_encoderはiOSでは良好に動作しますが、Androidでは問題が発生することがあります。

UnityのTexture2D.Compressは、サイズや速度において最適ではありませんが、すべての端末で対応しており、品質も許容範囲内です。現時点では、それが最適な選択肢かもしれません。

ただし、Texture2D.CompressはUnityメイン スレッド上で実行する必要がありますが、GPUベースの方法では非同期で待機でき、Arm ASTC Encoderでも他のスレッドで実行できます。さらに、BetsyやApple Image I/Oフレームワークなど、まだテストされていない方法もあります。

現時点ではTexture2D.Compressを選択するかもしれませんが、将来的にはさらに優れた方法が出てくるかもしれません。

 

参考

  1. https://github.com/wolfpld/etcpak
  2. https://github.com/darksylinc/betsy
  3. https://zhuanlan.zhihu.com/p/327045410
  4. https://github.com/ARM-software/astc-encoder
  5. https://github.com/niepp/astc_encoder
  6. https://qiita.com/up-hash/items/76f6f879f3129bb1d23e
  7. https://niepp.github.io/2021/12/18/Compute-ASTC.html
  8. ChatGPT

 

ENSAPIA Engineeringでは一緒に働く仲間を募集中です。

ご興味のある方は、ぜひこちらのエンジニア採用サイトをご覧ください。

→ENSAPIA Engineering 株式会社エンジニアの求人一覧

Tag

Category

Tag