【Unity】【シェーダ】GrabPassを使って屈折シェーダを作る

f:id:halya_11:20181202182745g:plain

GrabPassを使った屈折を表現するシェーダの実装方法です。

Unity2018.2.0

GrabPass?

GrabPassの説明と基本的な使い方は下記の記事を参照してください。

light11.hatenadiary.com

シェーダ

さっそくですがシェーダの実装です。

Shader "GrabPassRefraction" 
{
    Properties 
    {
        _RelativeRefractionIndex("Relative Refraction Index", Range(0.0, 1.0)) = 0.67
        [PowerSlider(5)]_Distance("Distance", Range(0.0, 100.0)) = 10.0
    }
    
    SubShader 
    {
        Tags {"Queue"="Transparent" "RenderType"="Transparent" }
        
        Cull Back 
        ZWrite On
        ZTest LEqual
        ColorMask RGB

        GrabPass { "_GrabPassTexture" }

        Pass {

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

            struct appdata {
                half4 vertex                : POSITION;
                half4 texcoord              : TEXCOORD0;
                half3 normal                : NORMAL;
            };
                
            struct v2f {
                half4 vertex                : SV_POSITION;
                half2 samplingViewportPos   : TEXCOORD0;
            };
            
            sampler2D _GrabPassTexture;
            float _RelativeRefractionIndex;
            float _Distance;

            v2f vert (appdata v)
            {
                v2f o                   = (v2f)0;
                o.vertex                = UnityObjectToClipPos(v.vertex);
                float3 worldPos         = mul(unity_ObjectToWorld, v.vertex);
                half3 worldNormal       = UnityObjectToWorldNormal(v.normal);

                half3 viewDir           = normalize(worldPos - _WorldSpaceCameraPos.xyz);
                // 屈折方向を求める
                half3 refractDir        = refract(viewDir, worldNormal, _RelativeRefractionIndex);
                // 屈折方向の先にある位置をサンプリング位置とする
                half3 samplingPos       = worldPos + refractDir * _Distance;
                // サンプリング位置をプロジェクション変換
                half4 samplingScreenPos = mul(UNITY_MATRIX_VP, half4(samplingPos, 1.0));
                // ビューポート座標系に変換
                o.samplingViewportPos   = (samplingScreenPos.xy / samplingScreenPos.w) * 0.5 + 0.5;
               #if UNITY_UV_STARTS_AT_TOP
                    o.samplingViewportPos.y     = 1.0 - o.samplingViewportPos.y;
               #endif

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_GrabPassTexture, i.samplingViewportPos);
            }
            ENDCG
        }
    }
}

説明はコメントに書いた通りです。

疑似的に求めたワールド座標をビューポート座標に変換し、
GrabPassのテクスチャのUV座標としています。

使い方と結果

使い方としては、まず表現したい物質たちの相対屈折率を求めます。
これについては次節を説明します。

また、虫眼鏡のように近くのものを拡大する効果を得たい場合はDistanceを小さく、
遠景の屈折のように上下反転するような効果を得たい場合にはDistanceを大きくします。

これを踏まえて_RelativeRefractionIndexを0.645、_Distanceを14.8とした時の結果が以下です。
Sphereのメッシュに適用しています。

f:id:halya_11:20181202182745g:plain

いい感じに屈折していることが確認できます。

Inspectorを拡張する

上述のシェーダでは_RelativeRefractionIndexとして相対屈折率を指定します。
相対屈折率は静的に求められるものの、このままでは使いづらいのでInspectorを拡張します。

ちなみに相対屈折率については下記の記事で説明しています。

light11.hatenadiary.com

using UnityEngine;
using UnityEditor;

public class GrabPassRefractionInspector : ShaderGUI 
{
    private enum MaterialType
    {
        Acryl,
        Ice,
        Water,
        Diamond,
    }

    private MaterialType _materialType;

    public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
    {
        var relativeRefractionIndexProp = FindProperty("_RelativeRefractionIndex", properties);
        var distanceProp                = FindProperty("_Distance", properties);

        // 屈折率入力領域
        materialEditor.DefaultShaderProperty(relativeRefractionIndexProp, relativeRefractionIndexProp.displayName);
        using (new EditorGUI.IndentLevelScope()) {
            using (new EditorGUILayout.HorizontalScope()) {
                _materialType       = (MaterialType)EditorGUILayout.EnumPopup("From Material", _materialType);
                if (GUILayout.Button("Apply")) {
                    var refractionIndex                         = GetRefractionIndex(_materialType);
                    if (refractionIndex != -1) {
                        // 空気の絶対屈折率はほぼ1なので1.0fで計算する
                        var relativeRefractiveIndex                 = 1.0f / refractionIndex;
                        relativeRefractionIndexProp.floatValue      = relativeRefractiveIndex;
                    }
                }   
            }
        }

        // 距離入力領域
        materialEditor.DefaultShaderProperty(distanceProp, distanceProp.displayName);
    }

    private float GetRefractionIndex(MaterialType type)
    {
        switch (type) {
        case MaterialType.Acryl:
            return 1.49f;
        case MaterialType.Ice:
            return 1.309f;
        case MaterialType.Water:
            return 1.3334f;
        case MaterialType.Diamond:
            return 2.417f;
        default:
            return -1.0f;
        }
    }
}

よく使いそうな素材を選択すれば空気との相対屈折率が入力されるようにしてみました。
これをシェーダから指定します。

CustomEditor "GrabPassRefractionInspector"

Inspectorは次のようになります。

f:id:halya_11:20181115110634p:plain

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com