女子高生になりたい

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

Shaderやっていく〜異方性の鏡面反射〜

ちょっとVRまわりで遊びすぎてて更新が止まってしまいました。
前回に引き続き、シェーダーやっていきますーー

完成図

f:id:sakata_harumi:20190524131950p:plain:w200

使用テクスチャ

f:id:sakata_harumi:20190524133103p:plain:w200

異方性の鏡面反射

前回は等方性鏡面反射のシェーダーを実装してみました。これは滑らかな面を想定しており、どの角度から光があたったとしてもその分布は一様になります。

一方、異方性の鏡面反射では傷が入ったような金属面のように光の当たり方によっては特定方向の反射が強くなったりします。 例えば、CDやDVDの裏面の反射や、ステンレス素材の表面仕上げ方法の「ヘアライン加工」が施された面などが異方性の反射になります。

ただ、ちゃんと実装すると難しそうなので今回の実装では、表面方向の反射特性を再現するのではなく、ハイライトの方向を示す法線マップを用意し、それに従い反射成分を増強させていく、といった方法をとっていきます。

正確にはBRDF(双方向反射率分布関数)に基づく異方性反射のモデルが幾つかある(Ward、Ashikhmin等)ようなのですが、今の自分には難しいのでそのうち機会があれば見ていきたいと思います。

実装

実装はほぼ http://wiki.unity3d.com/index.php?title=Anisotropic_Highlight_Shader の引用です

Shader "Custom/Anisotropic"
{
    Properties 
    { 
        _MainTint ("Diffuse Tint", Color) = (1,1,1,1) 
        _MainTex ("Base (RGB)", 2D) = "white" {} 
        _SpecularColor ("Specular Color", Color) = (1,1,1,1) 
        _Specular ("Specular Amount", Range(0,1)) = 0.5 
        _SpecPower ("Specular Power", Range(0,1)) = 0.5 
        _AnisoDir ("Anisotropic Direction", 2D) = "" {} 
        _AnisoOffset ("Anisotropic Offset", Range(-1,1)) = -0.2 
    } 
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Anisotropic 

        #pragma target 3.0

        struct Input
        {
            float2 uv_MainTex;
            float2 uv_AnisoDir;
        };
        struct SurfaceAnisoOutput 
        { 
            fixed3 Albedo; 
            fixed3 Normal; 
            fixed3 Emission; 
            fixed3 AnisoDirection; 
            half Specular; 
            fixed Gloss; 
            fixed Alpha; 
        }; 

        sampler2D _MainTex; 
        sampler2D _AnisoDir; 
        float4 _MainTint; 
        float4 _SpecularColor; 
        float _AnisoOffset; 
        float _Specular; 
        float _SpecPower; 

        void surf(Input IN, inout SurfaceAnisoOutput o) 
        { 
            half4 c = tex2D(_MainTex, IN.uv_MainTex) * _MainTint; 
            float3 anisoTex = UnpackNormal(tex2D(_AnisoDir, IN.uv_AnisoDir)); 
            
            o.AnisoDirection = anisoTex; 
            o.Specular = _Specular; 
            o.Gloss = _SpecPower; 
            o.Albedo = c.rgb; 
            o.Alpha = c.a; 
        } 
        fixed4 LightingAnisotropic(SurfaceAnisoOutput s, fixed3 lightDir, half3 viewDir, fixed atten) 
        { 
            fixed3 halfVector = normalize(normalize(lightDir) + normalize(viewDir)); 
            float NdotL = saturate(dot(s.Normal, lightDir)); 
            
            fixed HdotA = dot(normalize(s.Normal + s.AnisoDirection), halfVector); 
            float aniso = max(0, sin(radians((HdotA + _AnisoOffset) * 180)));  
            float spec = saturate(pow(aniso, s.Gloss * 128) * s.Specular); 
            
            fixed4 c; 
            c.rgb = ((s.Albedo * _LightColor0.rgb * NdotL) + (_LightColor0.rgb * _SpecularColor.rgb * spec)) * atten; 
            c.a = s.Alpha; 
            return c; 
        } 
        ENDCG
    }
    FallBack "Diffuse"
}

カスタムライティングモデルの定義であるLightingAnisotropicの実装をみていきます。

fixed4 LightingAnisotropic(SurfaceAnisoOutput s, fixed3 lightDir, half3 viewDir, fixed atten) 
{ 
    fixed3 halfVector = normalize(normalize(lightDir) + normalize(viewDir)); 
    float NdotL = saturate(dot(s.Normal, lightDir)); 
    
    fixed HdotA = dot(normalize(s.Normal + s.AnisoDirection), halfVector); 
    float aniso = max(0, sin(radians((HdotA + _AnisoOffset) * 180)));  
    float spec = saturate(pow(aniso, s.Gloss * 128) * s.Specular); 
    
    fixed4 c; 
    c.rgb = ((s.Albedo * _LightColor0.rgb * NdotL) + (_LightColor0.rgb * _SpecularColor.rgb * spec)) * atten; 
    c.a = s.Alpha; 
    return c; 
} 

実装のベース自体はBlinnPhongとなっていますね。 ちなみにsaturate(x)はxを0.0~1.0の間にクランプする処理です。具体的にはmin(max(x, 0.0), 1.0))です。

fixed HdotA = dot(normalize(s.Normal + s.AnisoDirection), halfVector); 

HdotAですが、BlinnPhongのときは面の法線ベクトルとハーフベクトルの内積を取っていました。
今回は、法線マップで示されているハイライトの方向に従って法線方向をずらします。

float aniso = max(0, sin(radians((HdotA + _AnisoOffset) * 180)));  

_AnisoOffsetを加えることで、光の反射の仕方をコントロールしています。
単純に加えるだけだと鏡面反射成分がただ強くなるだけですので、このようにsin噛ませることで_AnisoOffsetを加えるほど光があたっている周囲の部分が負になり、aniso=0で返ります。
つまり、光があたっている中心部から離れたところに鏡面成分が0以外の値を返すことになるので、輪状の反射になります。

_AnisoOffsetを0 ~ 1.0へ変異させていくgifを貼っておきます。(初期状態が_AnisoOffset=0です)

f:id:sakata_harumi:20190609012334g:plain:w200

あとの部分は、BlinnPhongとほぼ同様ですね。s.Glossにかけている128の数字は主観的にセットした数字で根拠は特にないという認識ですが、もしかしたらソースがあるのかもしれません・・??

さいごに

原始的なライティングは一旦ここまで!で次はPBR・・じゃなくて頂点関数を活用したシェーダーを作っていこうと思います!

Shaderやっていく〜Phong反射とBlinn-Phong反射〜

完成図

f:id:sakata_harumi:20190518175030p:plain:w400

(左から、Phong、BilinnPhong、Standard)

Phong反射とは

前回は、拡散反射のみを扱う反射モデルであるランバート反射を取り扱いましたが、今回取り扱うPhong反射では鏡面反射も考慮することができます。

鏡面反射とは、一定方向からの光が別の一定方向へ出ていくような反射です。これにより、光沢、艶のある表面を表現することができます。

Phong反射では最終的な表面の光の強度はI = 拡散成分 + 鏡面成分で計算されます。
拡散成分は、ランバート反射のときと同様、面の法線ベクトルと入射光ベクトルの内積から計算されます。

では、鏡面成分ですがこれはS=(R \cdot V)^ pで計算されます。ここのpは鏡面反射の強度になります。 光の反射ベクトルRとViewベクトルのなす角が小さいほど(近いほど)反射は強くなる、ということを示していることがわかると思います。

f:id:sakata_harumi:20190518183056p:plain:w400

反射ベクトルRは、R=2N \cdot (N \cdot L) -Lで計算できます。

Phong反射の実装

Shader "Custom/Phong"
{
    Properties
    {
        _MainTint ("Diffuse Tint", Color) = (1,1,1,1) 
        _MainTex ("Base (RGB)", 2D) = "white" {} 
        _SpecularColor ("Specular Color", Color) = (1,1,1,1) 
        _SpecPower ("Specular Power", Range(0,30)) = 1 
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Phong
        #pragma target 3.0

        struct Input
        {
            float2 uv_MainTex;
        };

        float4 _SpecularColor; 
        sampler2D _MainTex; 
        float4 _MainTint; 
        float _SpecPower;

        void surf (Input IN, inout SurfaceOutput o)
        {
            half4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        fixed4 LightingPhong (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) 
        { 
            float NdotL = dot(s.Normal, lightDir); 
            float3 reflectionVector = normalize(2.0 * s.Normal * NdotL - lightDir); 
            
            float spec = pow(max(0, dot(reflectionVector, viewDir)), _SpecPower); 
            float3 finalSpec = _SpecularColor.rgb * spec; 

            fixed4 c; 
            c.rgb = (s.Albedo * _LightColor0.rgb * max(0,NdotL) * atten) + (_LightColor0.rgb * finalSpec); 
            c.a = s.Alpha; 
            return c; 
        }
        ENDCG
    }
    FallBack "Diffuse"
}

数式と一対一で対応しているので、特に言及するところはないのですが、一応見ていきます。

float3 reflectionVector = normalize(2.0 * s.Normal * NdotL - lightDir);
この部分が反射ベクトルR、R=2N \cdot (N \cdot L) -Lを計算している部分になります。

float spec = pow(max(0, dot(reflectionVector, viewDir)), _SpecPower);
ここで、鏡面成分であるS=(R \cdot V)^ pを計算しています。鏡面成分がマイナスにならないようにしています。

c.rgb = (s.Albedo * _LightColor0.rgb * max(0,NdotL) * atten) + (_LightColor0.rgb * finalSpec);
最後に、最終的な表面カラーを決定しています。拡散成分+鏡面成分です。
拡散成分は、前回みましたランバート反射になっていますね。

Blinn-Phong反射とは

Phong反射は、反射ベクトルRをR=2N \cdot (N \cdot L) -Lし、RとViewベクトルの内積で反射のパラメータを決定していました。
このRの計算量を減らすため、別のパラメータを用いるのがBlinn-Phong反射です。

Blinn-Phong反射ではRの代わりに、「ハーフベクトル」と呼ばれるViewベクトル+入射光ベクトルを正規化したものを採用します。
このハーフベクトルと、法線ベクトルとの内積を反射のパラメータとします。

f:id:sakata_harumi:20190518213916p:plain:w400

ハーフベクトルの計算は「Viewベクトル+入射光ベクトルを正規化したもの」ですので、H=\frac{V+L}{|V+L|}になります。

Blinn-Phong反射の実装

Shader "Custom/BlinnPhong"
{
    Properties
    {
        _MainTint ("Diffuse Tint", Color) = (1,1,1,1) 
        _MainTex ("Base (RGB)", 2D) = "white" {} 
        _SpecularColor ("Specular Color", Color) = (1,1,1,1) 
        _SpecPower ("Specular Power", Range(0,60)) = 3
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf CustomBlinnPhong 
        #pragma target 3.0

        struct Input
        {
            float2 uv_MainTex;
        };

        float4 _SpecularColor; 
        sampler2D _MainTex; 
        float4 _MainTint; 
        float _SpecPower;

        void surf (Input IN, inout SurfaceOutput o)
        {
            half4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        fixed4 LightingCustomBlinnPhong (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) 
        { 
            float NdotL = max(0,dot(s.Normal, lightDir)); 

            float3 halfVector = normalize(lightDir + viewDir); 
            float NdotH = max(0, dot(s.Normal, halfVector)); 
            float spec = pow(NdotH, _SpecPower) * _SpecularColor; 

            float4 color; 
            color.rgb = (s.Albedo * _LightColor0.rgb * NdotL) + 
                    (_LightColor0.rgb * _SpecularColor.rgb * spec) * atten; 
            color.a = s.Alpha; 
            return color; 
        } 
        ENDCG
    }
    FallBack "Diffuse"
}

LightingCustomBlinnPhongの実装だけ少しみていきたいと思います。

ハーフベクトルの計算 H=\frac{V+L}{|V+L|}normalize(lightDir + viewDir)で行えます。簡単ですね。

float NdotH = max(0, dot(s.Normal, halfVector));
反射の強さのパラメータは、ハーフベクトルと法線ベクトルの内積でした。

float spec = pow(NdotH, _SpecPower) * _SpecularColor;
_SpecPowerパラメータによって反射の強さ(表面の光沢具合)を調節できます。

おわりに

次で反射モデル系は最後にしたいと思います。等方性の拡散反射、鏡面反射をやったので、次は異方性の鏡面反射を見ていきたいと思います!

Shaderやっていく〜ランバート反射〜

完成図

f:id:sakata_harumi:20190517020535p:plain:w400

左がランバート反射のシェーダー。右がStandardシェーダー(Metallic、Smoothness共に0)になります。

光の当たっている方向は同じですが、影のつき方が全然違うことがわかります。

ランバート反射とは?

Unity4まで標準としてランバート反射に基づく拡散ライティングモデルが使われていました。

Wikipedia曰く、ランバート反射は「拡散反射表面を理想的に扱った反射モデル」になります。
拡散反射表面というと、入射光が様々な角度で反射しているかのように見えるもので、ざらざらした木の表面などが相当します。
逆に、金属面などの鏡面反射を伴うものの表面は表現することはできません。

ランバート反射では、ポリゴン表面が反射する光量は入射光と表面法線との角度に依存します。

光が90度の角度で表面に当たると、全ての光が反射して戻ってきますし、その角度が小さいほど反射される光が少なくなります。

f:id:sakata_harumi:20190517002526p:plain:w500

これら2つのベクトルは単位ベクトルとして入力されますので、それらの内積を取れば良いです。
内積が0に等しいときは、2つのベクトルは直交しておりなす角が90度であることがわかりますし、1(or -1)であるときそれらは互いに平行であることがわかります。

内積が負の場合は、光がポリゴンの反対側からきていることになります。そのため、内積が負の場合は反射しないものとして0を代入しておく場合もあります。
今回のような不透明ジオメトリの場合は、カメラの前に面していないポリゴンはカリングされレンダリングされないので、この問題は発生しません。

実装

以前の記事では組み込みのランバート反射に基づくライティングモデルを使用していましたが、今回は自分で実装してみましょう。

サーフェースシェーダーにおいて、カスタム反射モデルを使用する場合はhalf4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten);といったインターフェースに実装します。

上の例はビュー方向に依存していないライティングモデルのインターフェースですが、他に,ビュー方向に依存している場合の
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);
や、遅延ライティングパスに使用する
half4 Lighting<Name>_PrePass (SurfaceOutput s, half4 light); といったものもあります。

<Name>の部分を任意に変更して、関数の名前をコンパイラディレクティブとして宣言しておきます。
#pragma surface ... 命令は#pragma surface surfaceFunction lightModel [optionalparams]といった形でしたね。
ここのlightModelを独自に定義した関数名にしてあげるとカスタムライティングモデルが使用できます。

実装全体はこちらです。

Shader "Custom/SimpleLambert"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf SimpleLambert

        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
        }
        half4 LightingSimpleLambert(SurfaceOutput s, half3 lightDir, half atten)
        {
            half NdotL = dot(s.Normal, lightDir);
            half4 color;
            color.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
            color.a = s.Alpha;
            return color;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

SurfaceFunctionはテクスチャを貼ってるだけなので割愛します。

メインのカスタムライティングモデルを定義している部分をみていきます。

half4 LightingSimpleLambert(SurfaceOutput s, half3 lightDir, half atten)
{
    half NdotL = dot(s.Normal, lightDir);
    half4 color;
    color.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
    color.a = s.Alpha;
    return color;
}

見ていきます・・といっても非常にシンプルですね。

half NdotL = dot(s.Normal, lightDir)内積を計算しています。先程も書きましたが0だと直交(=光は反射されない)、1だと平行(=光を全て反射)でした。

color.rgb =...の部分で表面の色を決定しています。_LightColor0がライトのカラー情報を保持しており、attenが光の減衰率を表しています。
つまり、「SurfaceのAlbedoカラー」「ライトのカラー」「(内積 * 光の減衰率)」で最終的に描画するカラーを決定しています。

おわりに

今回は拡散反射モデルを実装したので、次は鏡面反射モデルを見ていきたいと思います。

Shaderやっていく〜床に丸を書くやつ〜

完成図

考え方

1) 床のマテリアルに円を書くようなShaderを実装します

2) キャラクターの位置にその円がくるようスクリプトからパラメータを変更します

あまり言及することはないのですが、強いて言うならShaderのプロパティをスクリプトから変更する、というのが新しい点で今回のポイントです。

実装

まず、Shaderから見ていきます。

Shader "Custom/RadiusShader"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Center("Center", Vector) = (200,0,200,0) 
        _Radius("Radius", Float) = 1
        _RadiusColor("Radius Color", Color) = (1,0,0,1) 
        _RadiusWidth("Radius Width", Float) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;
        float3 _Center; 
        float _Radius; 
        fixed4 _RadiusColor; 
        float _RadiusWidth;

        struct Input
        {
            float2 uv_MainTex;
            float3 worldPos;
        };

        void surf(Input IN, inout SurfaceOutputStandard o) 
        {
            float d = distance(_Center, IN.worldPos);
            if ((d > _Radius) && (d < (_Radius + _RadiusWidth)))
            {
                o.Albedo = _RadiusColor;
            }
            else
            {
                o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
            }
        }
        ENDCG
    }
    FallBack "Diffuse"
}

SurfaceFunctionの実装を見ていきます。非常にシンプルですね。 _Centerが円の中心としたい場所なので、その値と現在参照されている位置のワールド座標との距離によって、テクスチャを貼るか円の色を塗るかを決定しています。 条件もとてもシンプルなので特に解説するところはないですね。

次はスクリプトです。こちらも非常にシンプルです。

[ExecuteInEditMode]
public class SetRadiusProperties : MonoBehaviour
{
    public Material[] radiusMaterials; 
    public float radius = 1; 
    public Color color = Color.white; 
    void Update()
    {
        foreach(Material radiusMaterial in radiusMaterials)
        {
            radiusMaterial.SetVector("_Center", transform.position);
            radiusMaterial.SetFloat("_Radius", radius);
            radiusMaterial.SetColor("_RadiusColor", color);
        }
    }
}

円を書きたいマテリアルを入れておいて、それらのプロパティにtransform.positionを入れることで動かしているだけになります。

床のマテリアルにRadiusShaderを使用し、動かすキャラクターにSetRadiusPropertiesをアタッチすると動作確認ができるかと思います。

おわりに

シンプルに実用性の高そうなShaderが実装できました。 次からライティングモデルを勉強していきたいなーと思います。

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

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

おわりに

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

Shaderやっていく〜テクスチャを貼るだけのShader〜

はじめに

Shaderを勉強してみようと思い、先日のGWでPacktから出ている「Unity2018 Shaders and Effects Cookbook」を一通り読んで動かしました。
で、まあ一回読んで動かしただけじゃ概ね理解できてないので、復習を兼ねて適当にピックアップしてアウトプットしたいと思います。
何回かに続いて書いていく予定なんですけど、更新なくなったら飽きたんだなって思ってください。

なぜShaderを勉強しようと思ったのか?

日常的にはバックエンド側をやることが多いので(ゲームではない)、Shaderとの接点はもちろんUnityとの接点も全然ありませんでした。
大学時代に仕事で避けゲー一本と、個人で簡単なADVを開発した事はあるので全く触ったことがないわけではないんですけど、ほぼUnityの事はわからない状態です。

しかし、最近個人で3DのVTuber試してみたり(【Live2D】VTuberのつくりかた!【360度動画】 - YouTube)とか、職場のゼミ制度(googleの20%ルールみたいなやつ)でVRゼミに入ってみたりと突然VRやUnityとの接点が増えたわけです。。

そこで、表現の幅を広げるため、スタンドアロンVRバイスで高クオリティかつ高速な表現を実現するために勉強をはじめてみた、って感じです。

あとは、偶然raymarchingの例を見て感動した、っていうのが結構強い動機だったりします。。 Unity でレイマーチングするシェーダを簡単に作成できるツールを作ってみた - 凹みTips こちらの記事の例など感動します。
凹さんが生成ツールなるものを公開されておられるのですが、エンジニアは往々にして1回は自分で全て実装しないと気がすまない人種なので、自分も実装できるように頑張っていきたいです。

Surface Shaderって?

そもそもUnityにおいてShaderはShaderLabというCgとHLSLの異型(だいたい似てるらしい)の言語で記述します。
で、Shaderではライティングや影などの煩雑な処理を実装していかないといけないのですが、Surface Shaderを使うとパラメータを指定するだけで、その辺を自動生成してくれます。
https://docs.unity3d.com/ja/current/Manual/SL-SurfaceShaders.html

後々はちゃんと読むとして、しばらくは目をつぶってSurfaceShaderによってShaderに慣れていきたいと思います。

テクスチャを貼るShader

最初は、テクスチャを貼るだけのShaderを見ていきたいと思います。

2D画像のテクスチャはどのように3Dモデルへマッピングされるのか

ポリゴンの各頂点にはシェーダがアクセスして使用できるデータを格納することができます。
ここのデータにuv座標のデータが入っています。これはu,vそれぞれで0-1の値を取り、2D画像のマッピング位置を示しています。
ポリゴンの内側の点に関しては、頂点データからGPUが最も近しいuv座標を補完するようです。

uv座標のデータを3Dモデルに含んでいない場合は当然ですがテクスチャマッピングは行なえません。

詳細は テクスチャマッピング · けんごのお屋敷 の記事が詳しく非常にわかりやすいです。

実装

ShaderのテンプレートはShaders→Standard Surface Shaderから作成しています。
で、何とこのテンプレートの状態で既にテクスチャを貼れるShaderになっています。
ということで、実装することは何もありません><

今回は、テンプレートコードを見てShaderLabのシンタックスを見ていきたいと思います。

全体のコードとしては以下。

Shader "Custom/TextureShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

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

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Propertiesブロック

最初にあるのはPropertiesのブロックです。
これは、Inspectorウィンドウに表示してUnity側から変更することが可能なパラメータになります。もちろん必要なければ省略可能です。

今回は以下の4つが記述されています。
シンタックスは「プロパティアトリビュート プロパティ名 (“表示名”, データ型) = 初期値」です。
プロパティアトリビュートは省略可能な要素で、今回は全て省略されています。

_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0

だいたい自明な感じがしますが、2Dという型だけ普段は見ないと思います。
これが2Dのテクスチャを格納するデータ型です。
初期値には「”white”, “black”, “gray”, “bump”」という単色のテクスチャが指定可能です。
Inspectorにはテクスチャの画像に加えて、TilingとOffsetというパラメータが表示されます。
Tilingでテクスチャを何枚表示するか、Offsetはテクスチャの原点位置を指定します。

プロパティアトリビュートについては、ShaderLab: Properties - Unity マニュアル の「プロパティーの属性とDrawer」を参照できます。
Inspectorにパラメータを表示させないようにしたりだとか、テクスチャが法線マップで有ることを示したりとか(法線マップじゃなかったら警告が出る)のような、補助的な役割を担うみたいです。

SubShaderブロック

ここにShader本体を記述していきます。1シェーダにつき1SubShaderです。
SubShader宣言は複数行うことができ、その際は上から順にハードウェアがサポートするShaderかどうかを判定していき、一番最初に通ったものが表示されるようです。
最終的にどれも表示不可能だった場合、最後のFallBackに記載したシェーダが選択されます。

上から順番に見ていきます。

Tags { "RenderType"="Opaque" }

Tags宣言ではレンダリングが行われるタイミングや方法を指定することが可能です。
RenderTypeタグでは、Replaced Shadersといった特定のShaderを置き換えてくれる機能を利用するために用います。
とりあえず、普通のShaderはOpaqueに指定、透過を含むShaderの場合はTransparentを指定しておきます。
この2つは今後も頻用します。
その他指定できる要素は以下の公式docに記載されています。
Replaced Shaders でのレンダリング - Unity マニュアル

LOD宣言

Level of Detailの略で、距離に応じて使用するSubShaderを切り替えるときに使います。
遠くにあるときに高品質なShaderを使用してもあまり意味がない、、みたいな最適化ニーズのために存在します。
因みに、LODの値は自動的に設定するものではなく、ScriptからmaximumLODを設定しないといけません。
ゲーム中のUpdate内などで、距離が変わったらRendererの.mterial.shader.maximumLODの値を設定し直すみたいなコードを書く必要があります。
この値が、ShaderLODの値より大きい場合、そのShaderが適用されます。
この例だと、maximumLODが200以上であればこのShaderが適用されます。
また、仮にShaderLODが100と200のものがあって、maximumLODが200であれば、LOD200のShaderが適用されます。

ちなみに遠くにある場合ローポリにする方でも同じようにLODという言葉を使います。LOD - Unity マニュアル
Shaderの方はシェーダーLODって言葉で使い分けてるみたいですね。 シェーダー LOD - Unity マニュアル

CGPROGRAM

以降、ENDCGまでHLSL/Cgで記述するという意味。ここからがShader本体の記述になります。

#pragma surface surf Standard fullforwardshadows

コンパイラディレクティブです。
#pragma surface ... でこれがSurfaceShaderであることを示しています。
以降いくつかパラメータを宣言しないといけません。

1つ目のsurfに相当するのが、SurfaceFunctionの名前です。surfと書いておくとvoid surf (Input IN, inout SurfaceOutput o)というシグネチャでSurfaceFunctionを定義しておく必要があります。
これは必須パラメータです。

struct SurfaceOutput
{
    fixed3 Albedo;  // ディフューズ色
    fixed3 Normal;  // 書き込まれる場合は、接線空間法線
    fixed3 Emission;
    half Specular;  //  0..1 の範囲のスペキュラーパワー
    fixed Gloss;    // スペキュラー強度
    fixed Alpha;    // 透明度のアルファ
};

次のStandardはライティングモードの記述です。
物理ベースのライティングモデルである StandardとStandardSpecularはビルドインされており特にこれ以上の記述は必要ありません。
物理ベースのライティングモデルとは、自分も厳密には全然良くわかってないのですが、光の作用を物理的な現象をなるべく近似しようと実装しているライティングモデル・・みたいな認識です。エネルギーの保存、フレネル反射、平面を防ぐ方法などの物理的な法則に従って設計されています。

Standard Shaderでは、硬質表面を念頭に設計されており、石やガラスなどを想定したマテリアルを扱うことを想定しており、StandardSpecularは名前の通り金属など鏡面反射を伴うマテリアルを想定しているものです。

逆に、物理ベースでないライティングモデルとしてLambertBlinnPhongが指定できます。(これらの実装方法はそのうち書きます、多分) Lambert反射は古典的かつ軽量なライティングモデルです。Wikipedia曰く、ランバート反射表面の輝度は、どの角度から見ても一定であるとのことです。
シンプルな数式で表現できますが、物理的な正しさを念頭においてるわけではありませんので物理ベースのランディングモデルではありません。

Standardに指定した場合は、SurfaceFunctionのoutputの型(出力構造体)をSurfaceOutputStandardにする必要があります。

struct SurfaceOutputStandard
{
    fixed3 Albedo;      // ベース (ディフューズかスペキュラー) カラー
    fixed3 Normal;      // 書き込まれる場合は、接線空間法線
    half3 Emission;
    half Metallic;      // 0=非メタル, 1=メタル
    half Smoothness;    // 0=粗い, 1=滑らか
    half Occlusion;     // オクルージョン (デフォルト 1)
    fixed Alpha;        // 透明度のアルファ
};

また、StandardSpecularにした場合は、出力構造体はSurfaceOutputStandardSpecularにする必要があります。

struct SurfaceOutputStandardSpecular
{
    fixed3 Albedo;      // ディフューズ色
    fixed3 Specular;    // スペキュラー色
    fixed3 Normal;      // 書き込まれる場合は、接線空間法線
    half3 Emission;
    half Smoothness;    // 0=粗い, 1=滑らか
    half Occlusion;     // オクルージョン (デフォルト 1)
    fixed Alpha;        // 透明度のアルファ
};

最後、fullforwardshadowsですが公式doc曰く

フォワード レンダリングパスで、すべてのライトシャドウをサポートします。デフォルトでは、フォワードレンダリングで、シェーダーは一方向からのシャドウしかサポートしません (内部シェーダー変数の数を節約するため)。フォワードレンダリングでポイントかスポットライトが必要な場合は、このディレクティブを使用します。

らしいのですが、何故これがテンプレート時に既に挿入されてるのかはよくわかりません。
フォワードレンダリングでポイントかスポットライトが必要なシチュエーションというのが現状よくわかっていないので今後の課題にしたいと思います・・(例などわかる方いればご教授頂きたいです・・)

更にその下#pragma target 3.0ですが、コンパイルのターゲットレベルです。
大体のモバイル端末でも3.0くらいまでは動くらしいので、とりあえず3.0にしておいて差し支えなさそうです?(わからん) DirectXOpenGLのバージョンの対応などは公式docへ。
シェーダーコンパイルターゲットレベル - Unity マニュアル

変数マッピング

以下の部分です。

sampler2D _MainTex;

struct Input
{
    float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

プロパティとして宣言した変数をSubShader内で使用するためには、マッピングが必要です。
上記のように書くことで、同名のプロパティが自動的にマッピングされます。
2Dはsampler2D型にマッピングされ、Colorはfloat4/half4/fixed4にマッピングされます。
rangeは単一のfloat/half/fixedにマッピングされます。

Input構造体ですが、この構造体がSurfaceFunctionの入力値になります。
使用したいプロパティをここで宣言しておきます。

float2 uv_MainTexですが、「uv+テクスチャの名前」でテクスチャ座標(UV座標)が入力値として入ります。
つまり、この入力値を使用することで、テクスチャのどの座標の部分を表示すればモデルに正しくマッピングできるのかがわかります。

その他、入力値として取れるものは様々です。新らしいものを使うときに都度みていきたいと思います。
ドキュメントにinput構造体が何を取ることができるのかが書かれているので、気になる方はそちらを。
サーフェスシェーダーの記述 - Unity マニュアル

GPUインスタンシング

UNITY_INSTANCING_BUFFER_START(Props)からENDまでの間に記述します。
テンプレートでは何も書かれてないので、丸ごと消しても問題ありません。

この機能を使うと、少ないドローコールで同じメッシュ、マテリアルのモデルを複数一気にレンダリングすることができます。
木とか草など、同じオブジェクトを大量に表示したいときに使用するようです。
今回は使用しないのでスルーします。また今度遊んでみます!
ドキュメントはこちら。GPU インスタンシング - Unity マニュアル

SurfaceFunction

やっと本丸にきました・・・。
ここで、出力構造体(今回はSurfaceOutputStandard)に値をセットすることで、見た目を変えていきます。

SurfaceOutoputStandard構造体の定義を再掲しておきます。

struct SurfaceOutputStandard
{
    fixed3 Albedo;      // ベース (ディフューズかスペキュラー) カラー
    fixed3 Normal;      // 書き込まれる場合は、接線空間法線
    half3 Emission;
    half Metallic;      // 0=非メタル, 1=メタル
    half Smoothness;    // 0=粗い, 1=滑らか
    half Occlusion;     // オクルージョン (デフォルト 1)
    fixed Alpha;        // 透明度のアルファ
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    // Metallic and smoothness come from slider variables
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
}

重要なのは最初の2行だけで、MetallicやSmoothnessなどはインスペクタで設定された値を入れてるだけですね。

fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;

見ての通り、ここでモデルの表面を何色で描画するか、を決定しています。
Cgの関数であるtex2Dの第一引数にテクスチャデータ、第二引数にサンプリングする部分の座標を入力しています。
これでサンプリング部分のRGBA値が取れます。で、_Colorをかけてるのは色合いを変更するためですね。TintColorです。
で、o.Albedoにその取得したカラーを入れたら反映されます。

.rgbみたいな文法はスィズル演算子と呼ばれ、ベクトル型の各要素にアクセスするために用いられます。
rgbaのアルファベットの組み合わせ以外に、xyzwを用いることもできます。
例えばfloat4(a, b, c, d).x → float aになりますし、float3(a, b, c).yz→float2(b, c)になります。

おわりに

今回はSurfaceShaderのテンプレートを読んでみました。
もし飽きる事がなければ、様々なShaderをピックアップしつつ書いていきたいと思います。

【Django REST framework】POST時はForeignKeyをpkのみ指定し、GET時はネストしたオブジェクトを展開する

前置き

以下が、今回の例で使用するViewとModelです。

class PostViewSet(viewsets.ModelViewSet):
    authentication_classes = [FirebaseAuthentication]
    queryset = Post.objects.all()
    serializer_class = PostSerializer
class Post(models.Model):
    user = models.ForeignKey(User)
    comment = models.CharField(max_length=130, default='')

class User(models.Model):
    uid = models.CharField(primary_key=True, max_length=64)
    name = models.CharField(max_length=30)
class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer()

    class Meta:
        model = Post
        fields = ('id', 'user', 'comment')

このような実装でviewに対しGETでエンドポイントを叩くと、以下のようなレスポンスが帰ってきます。

[
    {
        "id": 1,
        "user": {
            "uid": "uid-1",
            "name": "hoge1"
        },
        "comment": "hoge",
    },
    {
        "id": 3,
        "user": {
            "uid": "uid-2",
            "name": "hoge1",
        },
        "comment": "hogehoge"
    }
]

完璧ですね。ネストしたフィールドであるuserが展開されています。

ただし、この状態だとPOSTするときは以下のようなjsonを投げないといけません。。

{
    "user": {
        "uid": "hoge",
        "name": "hoge"
    },
    "comment": "hoge"
}

これは思っているのと違います。userはuidを指定するようにしたいですね。

そこで、PostSerializerを以下のように変更してみます。

class PostSerializer(serializers.ModelSerializer):
    user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())

    class Meta:
        model = Post
        fields = ('id', 'user', 'comment')

これで、以下のようにuidでpostできるようになりました!

{
    "user": "hoge_uid",
    "comment": "",
}

ただし、GETが・・・

[
    {
        "id": 1,
        "user": "hoge_uid",
        "comment": "hoge"
    },
    {
        "id": 2,
        "user": "hoge_uid2",
        "comment": "hoge"
    }
]

userがuidしか帰ってこなくなってしまいました。。

これは思ってるのと違いますね。。両立する方法を模索してみます。

本題

ここから本題です。

先程の2つの要件「POST時はネストしたオブジェクトをpkで指定」「GET時はネストしたオブジェクトを展開」を満たすように実装をしていきます。

ModelとViewに変更はありません。Serializerだけ変えていきます。

まず、以下のようにuserのserializerをread onlyに指定します

class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)

これの状態だと、GET時はネストしたオブジェクトが展開されて返さられます。 が、POSTの際にuserの指定ができなくなってしまいます。

そこで、更にuid用のフィールドを追加します。

class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)
    user_uid = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), write_only=True)
    class Meta:
        model = Post
        fields = ('id', 'user', 'user_uid', 'comment')

このとき、write_onlyを指定することによりこのフィールドをGET時には出さないようにしておきます。

これで、GET時は展開され、POST時はpk(user_uid)を指定することが可能になりました。

ただし、この状態ではPOSTしたときに「user_uid」カラムがModelにないためエラーが吐かれます。 そこで、最後にcreateメソッドをオーバーライドします。

class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)
    user_uid = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), write_only=True)

    def create(self, validated_date):
        validated_date['user'] = validated_date.get('user_uid', None)

        if validated_date['user'] is None:
            raise serializers.ValidationError("user not found.") 

        del validated_date['user_uid']

        return Post.objects.create(**validated_date)

user_uidを指定すると、validated_date['user_uid']には指定されたpkの「Userオブジェクト」が入っています。

Modelのフィールド名はuser_uidでなくuserなので、そのようにマッピングを変更した上でPostオブジェクトをcreateしてあげればOKです。

これで、POST時は以下のようなフォーマットで、

{
    "user_uid": "hoge_uid",
    "comment": "hoge",
}

GET時は以下のように展開されて帰ってきます。

{
    "id": 1,
    "user": {
        "uid": "hoge_uid",
        "name": "hoge",
    },
    "comment": "hoge"
}

これで意図していた通りに実装ができました!

凄く簡単な要件ですが、少しだけ工夫が必要なんですねー。