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

# 前書き

この記事は、2023のUnityアドカレの12/6の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい！

https://qiita.com/advent-calendar/2023/present-calendar#qiita_award

# TL;DR;

```csharp
// https://github.com/niepp/astc_encoder
GraphicsBuffer ConvertAstc(Texture2D raw);

Texture2D screenShot = ...;
using var astcBuff = ConvertAstc(screenShot);

var texture = new Texture2D(width, height, TextureFormat.ASTC_6x6);
using var map = texture.GetRawTextureData<byte>();
astcBuff.GetData(map);
texture.Apply(updateMipmaps: false, makeNoLongerReadable: true);
```

# 圧縮テクスチャ

一般的に画像の圧縮といいますと、JPEGやPNGが主流です。しかし、これらのフォーマットはGPUで解釈することができず、CPUでデコードし、GPUに送信するという手順を経て表示されます。

一方で、テクスチャ向け圧縮は、GPUがネイティブで解釈ができるように設計されています。PNGなどとの決定的な違いは、画像をブロック状に分割し、各ブロックのサイズが同じになっています。故に、読み出したいピクセルがあったときに、画像全体をデコードせずとも、ピクセルのデータがどのあたりにあるのかをアドレス計算で求めることができます。

主なテクスチャ向け圧縮は、ETC、DTX、ASTCなどがあります。昨今では、ASTCが圧縮率を高めても品質に優れているということで採用することが多くなっている印象です。iOS/Android/PCの大体で使えるようになってきたことも大きいです。

# `Texture2D.Compress`

Texture2Dをランタイムで圧縮したいときと言えば、`Texture2D.Compress`です。

https://docs.unity3d.com/ja/2019.4/ScriptReference/Texture2D.Compress.html

しかし、これは、PCならDXT1/DXT5、モバイルならETC2に固定されてしまっています。

```csharp
Texture2D screenShot = ...;
Debug.Log(screenShot.format); // RGBA32

screenShot.Compress(quality);
screenShot.Apply();
Debug.Log(screenShot.format); // ETC2
```

しかもこれはCPUで計算をするので、そこそこ重たい計算です。参考までに、私の作業PCの一つでは50msぐらいかかりました。しかもUnityAPIなのでスレッドセーフではありません。フレーム落ち不可避。

# ComputeShaderで圧縮する

ComputeShaderはGPUでグラフィック以外の処理をさせることができる（GPGPU）機能です。Vulkan、Metalでは必ずサポートされており、GLESも3.1でサポートされています。このComputeShaderでGPUパワーを使うことで、一瞬で圧縮ができます。

今回はこのShaderをUnityとグルーしてみます。

https://github.com/niepp/astc_encoder/blob/main/ASTC_Encode.hlsl#L554-L582

`ASTC_Encode.hlsl`にカーネルが書いてあるので、これをUnityが読み込めるようにラップします。

```HLSL:ASTC_Encode.compute
#define BLOCK_6X6
#define HAS_ALPHA
#pragma kernel MainCS
#inclide "astc_encoder/ASTC_Encode.hlsl"
```

コマンドを組み実行します。[`astc_encode.h`](https://github.com/niepp/astc_encoder/blob/main/astc_encode.h#L87-L194)で書かれているDirectX実装を移植します。

```csharp
Texture2D tex = ...; // 元画像

int DimSize = 6; // or 4
int TexWidth = tex.Width;
int TexHeight = tex.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(GraphicsBufferType.Constant, 1, sizeof(int) * 4);
using var outBuffer = new GraphicsBuffer(GraphicsBufferType.Structured, TotalBlockNum, 16), 

var cs = Resources.Load<ComputeShader>("ASTC_Encode");
constData.SetData(new [1]{ (TexWidth, TexHeight, GroupNumX, 0) });
cs.SetConstantBuffer(0, "constData", constData);
cs.SetTexture(0, "InTexture", tex);
cs.SetBuffer(0, "OutBuffer", outBuffer);
cs.Dispatch(0, GroupNumX, GroupNumY, 1);

var astc = new byte[TotalBlockNum * 16];
outBuffer.GetData<byte>(astc);
```

astc配列にASTC圧縮されたテクスチャデータが入っています。

# Texture2Dにロードする

`LoadRawTextureData` を使うことで、ピクセル単位ではなく生データとしてロードできます。

https://docs.unity3d.com/ScriptReference/Texture2D.LoadRawTextureData.html

```csharp
var astcTex = new Texture2D(tex.Width, tex.Height, TextureFormat.ASTC_6x6);
astcTex.LoadRawTextureData(astc)
astcTex.Apply(updateMipmaps: false, makeNoLongerReadable: true);
```

Texture2Dをランタイムでnewすると、VRAMだけでなくメインRAMの方にもCPU用コピーが確保されてしまいますが、`makeNoLongerReadable:false`でApplyすることで、これを開放することができます。

ASTCデータはただのバイト列ですので、ファイルにキャッシュしたり、サーバーに置くなり好きにすることができます。

# ライセンス表記

<details><summary>niepp/astc_encoder</summary>


```
MIT License

Copyright (c) 2021 niedap

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

</details>