女子高生になりたい

はるみちゃんのブログだよ。えへへ。

Shaderやっていく〜ホログラフィックシェーダー〜

今回は、ホログラフィックシェーダーを作っていきたいと思います。 ホログラフィックってゲームでかなり頻出する表現ですよね。 主に、服だけ透ける特殊能力者を伴うゲームでよくみます。

まずは完成図から。はるみちゃんの服をホログラフィックシェーダーに書き換えて透けさてみます。

f:id:sakata_harumi:20190512232106p:plain

↓ ホログラフィックシェーダー使用

f:id:sakata_harumi:20190512231741p:plain

^^

考え方

モデルのシルエットのみを表示するシェーダーです。見る角度に応じてそのアウトラインも変わります。

仕組みとしてはシンプルで、「法線方向がビュー方向に対して直交しているポリゴンがエッジになる」という考え方を利用して実装していきます。

完全に直交していると完全に見える状態、角度に差が出てくるにつれて徐々に見えない状態にしていくとこのような表示になります。

実装

まず、全体をそのまま貼ります。

Shader "Custom/Holographic"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _DotProduct("Rim effect", Range(-1,1)) = 0.25 
    }
    SubShader
    {
        Tags 
        { 
          "Queue" = "Transparent" 
          "IgnoreProjector" = "True" 
          "RenderType" = "Transparent" 
        } 
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Lambert alpha:fade

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex; 
            float3 worldNormal; 
            float3 viewDir; 
        };

        fixed4 _Color;
        float _DotProduct;

        void surf (Input IN, inout SurfaceOutput o)
        { 
            float4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; 
            o.Albedo = c.rgb; 
            float border = 1 - (abs(dot(IN.viewDir, IN.worldNormal))); 
            float alpha = (border * (1 - _DotProduct) + _DotProduct); 
            o.Alpha = c.a * alpha; 
        } 
        ENDCG
    }
    FallBack "Diffuse"
}

透明を伴うSurfaceShaderにおける変更点

今回はアルファを扱うシェーダーですので、前回のテンプレートからいくつかパラメータを変更してあげないといけません。 まずは、Tagsを見てみます。

Tags 
{ 
    "Queue" = "Transparent" 
    "IgnoreProjector" = "True" 
    "RenderType" = "Transparent" 
}

"Queue" = "Transparent"ですが、これはオブジェクトを描画する順番を指定するためのものです。透明な部分を伴いますので透明でないオブジェクトを先に描画しておかないと、本来透けてみえるはずのオブジェクトが見えない、といった状態になってしまいます。
Background、Geometry、AlphaTestを指定したタグより、Overlayより前にレンダリングされます。

"IgnoreProjector" = "True"は、名前の通りプロジェクターを無視するものです。部分的に透明を含むオブジェクトではうまく投影効果を表現できないため切っておきます。

"RenderType" = "Transparent"はReplaced Shaderの機能を使う場合に用いるものです。Unityビルドインシェーダーは殆どの部分が透明なシェーダーについてはTransparentをセットしているので、それに習ってセットしておきます。

次に、#pragma surface surf Lambert alpha:fadeの部分を見ていきます。

Lambertですが、これはライティングモードの記述で、テンプレートではStandardになっていました。
ビルドインされているStandardは物理ベースのライティングモデルですが、今回のホログラフィックシェーダーに関しては詳細なライティングは全く必要はありませんので、非常にコストが低い「ランバート反射」を指定しておきます。
ランバート反射の考え方や(組み込みを用いない)実装方法などはそのうち書いていこうと思います。

alphaでは、透明度の設定を可能にするものです。これを設定しないと透過されません。今回指定している:fadeや他にも:blend:premulなど様々なオプションが存在しますが、イマイチよくわかっていません。
詳細はドキュメントへ。 docs.unity3d.com

以上が、透明な部分を伴うShaderを実装する際のテンプレート的な部分です。 次に、実装の中身を見ていきましょう。

SurfaceFuntion

Input

struct Input
{
    float2 uv_MainTex; 
    float3 worldNormal; 
    float3 viewDir; 
};

前回のInputはテクスチャ情報のみでしたが、今回はworldNormal(=オブジェクトの法線ベクトル)とviewDir(=ビュー方向のベクトル)を追加しておきます。

SurfaceFunction

void surf (Input IN, inout SurfaceOutput o)
{ 
    float4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; 
    o.Albedo = c.rgb; 
    float border = 1 - (abs(dot(IN.viewDir, IN.worldNormal))); 
    float alpha = (border * (1 - _DotProduct) + _DotProduct); 
    o.Alpha = c.a * alpha; 
} 

最初の2行は前回見ました、テクスチャマッピングのコードです。

float border = 1 - (abs(dot(IN.viewDir, IN.worldNormal)));ここで、エッジ(輪郭)具合・・とでも言うんでしょうか、を計算しています。 法線ベクトルとビュー方向のベクトルが直行している(=内積が0)の場合、そこはエッジなので透明度は低く(alpha=1)になります。 (内積は組み込み関数dotで計算、ドット積ともよく呼ばれる)

で、内積が0でなくなっていくにつれ透明度を高く(alpha=0)にしていきます。このとき、鋭角だろうと鈍角だろうと見え方は変わりませんので絶対値を取ってしまって問題ありません。 また、入力の法線ベクトル、ビュー方向ベクトルは正規化されていますので内積は1以上にはなりません。

float alpha = (border * (1 - _DotProduct) + _DotProduct);で、先で算出したパラメータ(border)に線形補間をかけていきます。
もちろん、この部分を挟まずにalpha=borderにしてしまっても構いません。

この処理を挟み込むことで、_DotProductパラメータを通じて、ホログラフィックの見え方を変更することができるようになっています。

以下は服の透過を低めに、スカートの透過を高めにしてみた例です。

f:id:sakata_harumi:20190513004432p:plain

最後にテクスチャからのオリジナルパラメータと今回計算した係数を乗算して最終的な外観を決定し、完了です。

おわりに

知っておくといろいろ使えそうですね