【Unity】【エディタ拡張】HDRテクスチャをLDRにエンコードするウィンドウ

エディタ拡張でHDRテクスチャをLDRエンコードするウィンドウを作る方法です。

Unity2017.4

作るもの

モバイルでHDRのテクスチャを使いたい場合など、
HDRのテクスチャをLDRエンコードし、シェーダで使うときにデコードする手法があります。

light11.hatenadiary.com

エンコードの方法はいくつかありますが、今回は単純に設定した値でHDRテクスチャの値を割ってLDR化したテクスチャを作ります。
この方法は、割ってもなお1以上になる値は丸められてしまう上、精度は低めです。
ただ実装はシンプルだし計算量も少ないというメリットがあります。

シェーダ

まずはシェーダです。

Shader "Hidden/HDREncoder_Scaling"
{
    Properties
    {
        _MainTex ("Texture", 2D)         = "white" {}
        _ScaleFactor ("Scale Factor", Range(1, 10))    = 2
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Cull Off
            ZTest Always
            ZWrite Off

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _ScaleFactor;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex        = UnityObjectToClipPos(v.vertex);
                o.uv            = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col      = tex2D(_MainTex, i.uv);
                col             /= _ScaleFactor;
                return col;
            }
            ENDCG
        }
    }
}

入力されたテクスチャ色を単純にScale Factorで割ったものを返します。

ウィンドウ

次にテクスチャを作るためのウィンドウを作ります。

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.IO;

public class HDRTextureEncoderWindow : ScriptableWizard 
{
    private const string SHADER_NAME_SCALING         = "Hidden/HDREncoder_Scaling";

    [SerializeField]
    private Texture2D _source;
    [SerializeField, Range(1, 10)]
    private float _scaleFactor            = 2;

    [MenuItem("Window/HDR Texture Encoder")]
    private static void Open()
    {
        DisplayWizard<HDRTextureEncoderWindow>(ObjectNames.NicifyVariableName(typeof(HDRTextureEncoderWindow).Name), "Create");
    }

    protected override bool DrawWizardGUI ()
    {
        var so  = new SerializedObject(this);
        so.Update();

        using (var s = new EditorGUI.ChangeCheckScope()) {
            var sourceProp          = so.FindProperty("_source");
            var scaleFactorProp     = so.FindProperty("_scaleFactor");
            EditorGUILayout.PropertyField(sourceProp);
            scaleFactorProp.floatValue  = EditorGUILayout.Slider(scaleFactorProp.displayName, scaleFactorProp.floatValue, 1, 10);

            if (s.changed) {
                so.ApplyModifiedProperties();
                // 変更があった場合はtrueを返す
                return true;
            }
        }

        return false;
    }
    
    private void OnWizardUpdate()
    {
        if (_source == null) {
            isValid             = false;
            return;
        }
        // ソースとなるHDRテクスチャがBC6Hじゃなかったらエラー
        if (!(_hdrLightmap.format == TextureFormat.BC6H || _hdrLightmap.format == TextureFormat.RGBAFloat || _hdrLightmap.format == TextureFormat.RGBAHalf)) {
            isValid             = false;
            errorString         = "HDRのフォーマットが不正です。";
            return;
        }
        else {
            errorString         = "";
        }
        isValid     = true;
    }

    private void OnWizardCreate () 
    {
        // マテリアルを作成
        var material        = new Material(Shader.Find(SHADER_NAME_SCALING));
        material.SetFloat("_ScaleFactor", _scaleFactor);
        
        // RenderTextureに変換後の値を書き込む
        var renderTexture   = RenderTexture.GetTemporary(_source.width, _source.height, 0, RenderTextureFormat.ARGBFloat);
        Graphics.Blit(_source, renderTexture, material, 0);
        
        // RenderTextureの値をTextureに書きこむ
        var currentRT           = RenderTexture.active;
        RenderTexture.active    = renderTexture;
        var texture             = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBAFloat, false);
        texture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
        texture.Apply();
        RenderTexture.active    = currentRT;

        // エンコードして保存
        var savePath            = SaveAsset(texture.EncodeToPNG());

        // テクスチャインポート設定
        if (!string.IsNullOrEmpty(savePath)) {
            AssetDatabase.ImportAsset(savePath);
            var textureImporter             = AssetImporter.GetAtPath(savePath) as TextureImporter;
            textureImporter.alphaSource     = TextureImporterAlphaSource.None;
            textureImporter.isReadable      = false;
            textureImporter.mipmapEnabled   = true;
            textureImporter.wrapMode        = TextureWrapMode.Clamp;
            textureImporter.SaveAndReimport();
        }

        // RenderTextureを解放
        RenderTexture.ReleaseTemporary(renderTexture);
    }

    private string SaveAsset(byte[] target)
    {
        var sourcePath          = AssetDatabase.GetAssetPath(_source);
        var path                = Path.GetDirectoryName(sourcePath);
        var ext                 = "png";

        // ディレクトリが無ければ作る
        if (Directory.Exists(path) == false) {
            Directory.CreateDirectory(path);
        }

        // ファイル保存パネルを表示
        var fileName            = Path.GetFileNameWithoutExtension(sourcePath) + "." + ext;
        fileName                = Path.GetFileNameWithoutExtension(AssetDatabase.GenerateUniqueAssetPath(Path.Combine(path, fileName)));
        path                    = EditorUtility.SaveFilePanelInProject("Save Asset", fileName, ext, "", path);

        if (!string.IsNullOrEmpty(path)) {
            // ファイルを保存する
            File.WriteAllBytes(path, target);
            AssetDatabase.Refresh();
        }

        return path;
    }
}
#endif

説明はコメントの通りです。
前節のシェーダを使って元のテクスチャをLDRに変換しています。

ちなみにTexture2D.EncodeToEXR()を使えばLDRテクスチャからHDRテクスチャを作ることもできるはずです。

docs.unity3d.com

しかし、EncodeToEXR()がどうもうまく働かなくて今回は挫折しました。
Unityのバージョンの関係かなとも思うので必要になったら新しいバージョンで試してみたいと思います。

使い方

使い方はSourceにHDRテクスチャを入れて、Scale Factorを好きなように調整してCreateするだけです。

f:id:halya_11:20181130202044p:plain

関連

light11.hatenadiary.com

参考

docs.unity3d.com