【Unity】ReflectionProbe から CubemapArray をつくる 

# 概要
複数の ReflectionProbe のベイク済み Cubemap texture を CubemapArray にまとめます．

# 実装
エディタ拡張にしました．

```cs
using UnityEngine;
using System.IO;

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.Rendering;
using System.Collections.Generic;

public class ProbeToCubemapArray : ScriptableWizard
{
    public ReflectionProbe[] probes;

    [MenuItem("Utils/Probe To Cubemap Array")]
    static void Open()
    {
        DisplayWizard<ProbeToCubemapArray>("Probe To Cubemap Array");
    }

    void OnWizardCreate()
    {
        List<Cubemap> cubes = new (probes.Length);
        foreach (var p in probes)
        {
            if (p != null && p.mode == ReflectionProbeMode.Baked && p.bakedTexture != null)
            {
                cubes.Add((Cubemap)p.bakedTexture);
            }
        }
        if (cubes.Count == 0) return;
        CubemapArray cube_array = CubemapToCubemapArray(cubes.ToArray());

        var src_path = AssetDatabase.GetAssetPath(cubes[0]);
        var path = Path.GetDirectoryName(src_path) + "\\" + Path.GetFileNameWithoutExtension(src_path) + "-CubemapArray.asset";
        path = AssetDatabase.GenerateUniqueAssetPath(path);
        AssetDatabase.CreateAsset(cube_array, path);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        Debug.Log($"Saved as \"{path}\"");
    }

    public static CubemapArray CubemapToCubemapArray(Cubemap[] cubes)
    {
        var cube_array = new CubemapArray(cubes[0].width, cubes.Length, TextureFormat.BC6H, cubes[0].mipmapCount, false);
        for (int i = 0; i < cube_array.cubemapCount; i++)
        {
            bool readable = cubes[i].isReadable;
            if (!readable) SetReadable(cubes[i], true);
            // BC6H は mipmap が自動生成されないので，すべてコピーする．
            for (int mip = 0; mip < cube_array.mipmapCount; mip++)
            {
                for (int f = 0; f < 6; f++)
                {
                    cube_array.SetPixelData(cubes[i].GetPixelData<byte>(mip, (CubemapFace)f), mip, (CubemapFace)f, i);
                }
            }
            if (!readable) SetReadable(cubes[i], false);
        }
        cube_array.Apply(false, true);
        cube_array.filterMode = FilterMode.Trilinear;
        cube_array.anisoLevel = 0;
        cube_array.wrapMode = TextureWrapMode.Clamp;
        return cube_array;
    }

    static void SetReadable(Texture texture, bool isReadable)
    {
        if (texture.isReadable == isReadable)
        {
            return;
        }
        var path = AssetDatabase.GetAssetPath(texture);
        var importer = AssetImporter.GetAtPath(path) as TextureImporter;
        if (null == importer)
        {
            return;
        }
        importer.isReadable = isReadable;
        importer.SaveAndReimport();
    }
}
#endif
```

# 解説
ReflectionProbe をベイクすると，生成される Cubemap は BC6H というフォーマットになります（HDRの場合）．圧縮率が高く優秀なので，CubemapArray も BC6H で作成します．

Texture2D.ReadPixels() は BC6H に対応していないようなので，Get/SetPixel 系の関数で地道にコピーしていきます．その際，Get/SetPixel するテクスチャは readable である必要があります．

Texture.Apply(true) を実行すると mipmap が自動生成されますが，BC6H は mipmap の自動生成ができないため，すべての mipmap をコピーしていきます．

CubemapArray は拡張子を .asset として保存します．ファイルサイズは単純に元の Cubemap のサイズの総和になるようです．

# 雑記
CubemapArray を調べてみると，情報が非常に少なかったです．それもそのはずで，Culling が効くように適切に Scene を構成していればそもそも CubemapArray は必要ありません．ではなぜ CubemapArray が必要になったのか，という話は，また CubemapArray の使い方の記事でしようと思います．