【Unity】Reflection Probe を回転する

# 概要
ベイク済みの ReflectionProbe の GameObject を回転させても ReflectionProbe 自体は回転しないので，ベイクで作成される Cubemap Texture を回転させます．

※ 本手法は XYZ 各軸周り 90° 単位の回転のみ可能です．またランタイムには対応しません．

# (準備) Texture の Read/Write プロパティについて
ベイクで作成される Cubemap Texture はデフォルトでは Read/Write が false になっていますが，この後 Set/GetPixels() を使うので true にしておく必要があります．

![readable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825773/4bef28d8-07de-fda2-db46-758404b28f27.png)

この操作もスクリプトで実行したいので，関数を用意しておきます．

```cs
static void SetReadable(Texture texture, bool isReadable)
{
    var path = AssetDatabase.GetAssetPath(texture);
    var importer = AssetImporter.GetAtPath(path) as TextureImporter;
    if (null == importer)
    {
        return;
    }
    if (importer.isReadable != isReadable)
    {    
        importer.isReadable = isReadable;
        importer.SaveAndReimport();
    }
}
```
この Read/Write というのは，テクスチャを CPU で扱うかどうかの設定らしいです．これが false になっているテクスチャを CPU に渡したい場合，Texture2D.ReadPixels() を使うのが一般的なようですが，うまくいかなかったのでパワープレイで解決しています．おそらく texture format の影響と思われます．

# Cubemap Texture を回転する
Y 軸周りに 180° 回転させます．順番を変えれば各軸周りの 90° 単位の回転が可能です．
```cs
static Cubemap RotateCubemap(Cubemap src)
{
    int sz = src.height;
    Cubemap dst = new Cubemap(sz, TextureFormat.RGBAFloat, src.mipmapCount, true);
    // Cubemap の面の順番を入れ替える (Y 軸周りに 180deg 回転させる場合)
    int[] face_order = {1,0,2,3,5,4};
    for (int i = 0; i < 6; i++)
    {
        var data = src.GetPixels((CubemapFace)face_order[i]);
        // +Y, -Y 面はピクセル単位で回転させる (Y 軸周りに 180deg 回転させる場合)
        if (2 <= i && i <= 3)
        {
            System.Array.Reverse(data);
        }
        dst.SetPixels(data, (CubemapFace)i);
    }
    dst.Apply(true);
    return dst;
}
```

# EXR 形式で保存する
ベイクで作成される Cubemap Texture は EXR 形式で保存されているので，同じ形式で保存します．ついでにインポート設定もします．

```cs
static void SaveAsEXR(Cubemap src, string path)
{
    int sz = src.height;
    Texture2D dst = new Texture2D(sz * 6, sz, TextureFormat.RGBAFloat, src.mipmapCount, false);
    for (int i = 0; i < 6; i++)
    {
        var src_data = src.GetPixels((CubemapFace)i);
        var dst_data = new Color[sz * sz];
        // EXR 形式で保存すると上下反転する(?)ので，予め反転させておく
        for (int j = 0; j < sz; j++)
        {
            System.Array.Copy(src_data, j * sz, dst_data, (sz - 1 - j) * sz, sz);
        }
        dst.SetPixels(i * sz, 0, sz, sz, dst_data);
    }
    dst.Apply(true);
    File.WriteAllBytes(path, ImageConversion.EncodeToEXR(dst));
    AssetDatabase.Refresh();

    // Cubemap Texture のデフォルト設定を適用
    var importer = AssetImporter.GetAtPath(path) as TextureImporter;
    var settings = new TextureImporterSettings();
    importer.ReadTextureSettings(settings);
    settings.cubemapConvolution = TextureImporterCubemapConvolution.Specular;
    settings.seamlessCubemap = true;
    importer.SetTextureSettings(settings);
    importer.textureShape = TextureImporterShape.TextureCube;
    importer.isReadable = false;
    importer.wrapMode = TextureWrapMode.Clamp;
    importer.filterMode = FilterMode.Trilinear;
    importer.anisoLevel = 0;
    importer.SaveAndReimport();
}
```

# コード全体
エディタ拡張としてまとめます．ReflectionProbe を渡すと，ベイク済みの Cubemap Texture と同じ階層に回転した結果を出力します．
```cs:ReflectionProbeRotator.cs
using UnityEngine;
using UnityEditor;
using System.IO;

public class ReflectionProbeRotator : ScriptableWizard
{
    public ReflectionProbe probe;

    [MenuItem("Utils/Reflection Probe Rotator")]
    static void Open()
    {
        DisplayWizard<ReflectionProbeRotator>("Reflection Probe Rotator");
    }

    void OnWizardCreate()
    {
        Cubemap src = (Cubemap)probe.bakedTexture;
        // ベイクで作成された Cubemap Texture はデフォルトで isReadable が false なので，true にする
        SetReadable(src, true);
        Cubemap dst = RotateCubemap(src);
        SetReadable(src, false);

        var src_path = AssetDatabase.GetAssetPath(src);
        var path = Path.GetDirectoryName(src_path) + "\\" + Path.GetFileNameWithoutExtension(src_path) + "-rotated.exr";
        SaveAsEXR(dst, path);
    }

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

    static Cubemap RotateCubemap(Cubemap src)
    {
        int sz = src.height;
        Cubemap dst = new Cubemap(sz, TextureFormat.RGBAFloat, src.mipmapCount, true);
        // Cubemap の面の順番を入れ替える (Y 軸周りに 180deg 回転させる場合)
        int[] face_order = {1,0,2,3,5,4};
        for (int i = 0; i < 6; i++)
        {
            var data = src.GetPixels((CubemapFace)face_order[i]);
            // +Y, -Y 面はピクセル単位で回転させる (Y 軸周りに 180deg 回転させる場合)
            if (2 <= i && i <= 3)
            {
                System.Array.Reverse(data);
            }
            dst.SetPixels(data, (CubemapFace)i);
        }
        dst.Apply(true);
        return dst;
    }

    static void SaveAsEXR(Cubemap src, string path)
    {
        int sz = src.height;
        Texture2D dst = new Texture2D(sz * 6, sz, TextureFormat.RGBAFloat, src.mipmapCount, false);
        for (int i = 0; i < 6; i++)
        {
            var src_data = src.GetPixels((CubemapFace)i);
            var dst_data = new Color[sz * sz];
            // EXR 形式で保存すると上下反転する(?)ので，予め反転させておく
            for (int j = 0; j < sz; j++)
            {
                System.Array.Copy(src_data, j * sz, dst_data, (sz - 1 - j) * sz, sz);
            }
            dst.SetPixels(i * sz, 0, sz, sz, dst_data);
        }
        dst.Apply(true);
        File.WriteAllBytes(path, ImageConversion.EncodeToEXR(dst));
        AssetDatabase.Refresh();

        // Cubemap Texture のデフォルト設定を適用
        var importer = AssetImporter.GetAtPath(path) as TextureImporter;
        var settings = new TextureImporterSettings();
        importer.ReadTextureSettings(settings);
        settings.cubemapConvolution = TextureImporterCubemapConvolution.Specular;
        settings.seamlessCubemap = true;
        importer.SetTextureSettings(settings);
        importer.textureShape = TextureImporterShape.TextureCube;
        importer.isReadable = false;
        importer.wrapMode = TextureWrapMode.Clamp;
        importer.filterMode = FilterMode.Trilinear;
        importer.anisoLevel = 0;
        importer.SaveAndReimport();
    }
}
```

# 雑記
任意軸周りで任意角度の回転をさせたい場合は，Skybox のシェーダーなどを使って回転したものを再度ベイクする必要がありそうです．

ランタイムで回転させたい場合は...いい感じのシェーダーを使って Blit するのでしょうか？まあ，ベイク済みの ReflectionProbe をランタイムで回転させたい状況というのがちょっと思いつかないですが...