Unity
シェーダ

UNITYで古典シェーダを作る

Part2 https://qiita.com/YukiMiyatake/items/e029a286e192250244e5

はじめに

知り合いより、物理ベースシェーディングが流行ってるが、物理ベースじゃないシェーディングって何? 魔法ベース?
って聞かれたので
自分の復習のためと UNITYのシェーダの理解のために テストした
間違えてるところあったら 突っ込み求む!

無駄なincludeとかはあるとおもうけど、まだ深くみてないもので

ただし、わけありで 1年前のUNITYです
今は少しシェーダコードかわってます。 特に今は Metal対応でコードがかなり変わっている・・

#古典シェーダとは?
最近は物理ベースシェーダ (PBS)が多い
これは、光を物理学にのっとった形で計算を行い、よりリアルに近づけた ライティングモデルです
物理ベースの計算は昔からあったが、マシンスペックの問題で、計算量の少ないモデルで近似する必要があった
それを 古典シェーダと 私は読んでます

余談ですが最近は レイトレーシングという更にリアルになったものもマシン性能向上の影響で増えてます

古典シェーダと物理ベースの違いはとても多いが
古典シェーダでは アンビエントは画面全体を照らし、平行光源は法線とのなす角で近似的に計算していた
またテクスチャカラーとして 光をうけて表面色を乗算して計算していた
物理ベースになると、光は物質に吸収され 特定の周波数だけ吸収され物質表面から放出される
アンビエントもグローバルイルミネーションとして リアルに計算される
肌などは特にBRDFという複雑な計算を行う

まずは、アンビエント、平行光源、テクスチャ、ディフューズ、スペキュラー
といった 古典的なシェーダをやってみる

フラットシェーディングとスムースシェーディング

3Dグラフィックではポリゴンという単位で描画を行う
最初の3Dでは テクスチャマッピングやスムーズシェーディングが無く
ポリゴン面に均一な色を塗っていたため、ポリゴンがそのまま見えていた
バーチャファイター1の画面をみれば 一目瞭然である
この頃は パソコンのスペックも低く、CPUで全て計算してたためポリゴン数もとても少ない

vf.jpeg

面のライティングに関しては、Lambertモデルのようなものを使っていると推測される
Lambertは 平行光源の逆ベクトルと面法線の内積をとり、それをもとに光の計算をする
たとえば 面に直角に光があたれば明るい色になるが、光の角度が浅くなれば暗い反射になる

フラットシェーディングがポリゴンポリゴンしているのは、面全体の法線が同じだからなので
面の法線を補完すれば、真っ直ぐなポリゴンだが ライティングはなめらかに見せる事ができる
これが スムーズシェーディングといい 主に
輝度のみ補完するグーローと 法線を補完するフォンがある

テクスチャそのまま表示するよ!

コードはこちら
https://github.com/YukiMiyatake/UnityLesson/tree/Shader_3

シェーディングの前に、テクスチャをそのままマッピングする
今回は中野シスターズで遊ぶ

シェーダーは下記

Shader "Unlit/yUnit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            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;

            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
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

とてもシンプルに、モデルのローカル座標を スクリーン座標に変換し
テクスチャをそのまま貼り付けているので
ライティングもなにもない

1.png

Lambert

ランバート反射モデル 俗に言うディフューズをつけます
ディフューズこそが 古典シェーダ。計算量が少なくそれっぽい結果を出せる 代表だと思います

計算式も単純に 法線と光ベクトルの内積をとり、それを乗算するだけです
今回は テクスチャカラーと光の色を計算にいれてみた

Shader "Unlit/yUnit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags{
                "LightMode" = "ForwardBase"
            }

            Cull Back

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"
           #include "Lighting.cginc"

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            float3 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = tex2D(_MainTex, i.uv);

                // Diffuse
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 NdotL = dot( i.normal, lightDir);
                float3 diffuse = max(0.0, NdotL ) * _LightColor0.rgb * col.xyz;
                return diffuse;
            }
            ENDCG
        }
    }
}

float3 NdotL = dot( i.normal, lightDir);
で 内積を計算し
float3 diffuse = max(0.0, NdotL ) * _LightColor0.rgb * col.xyz;
光源色、テクスチャカラーを乗算しているのは、

2.png

ちょっと暗い。理由は 上記のランバートでは 内積が 0以上 つまり光源からみて側面、裏側は全くライティングされない
本当は光は後ろ側にも回析してくるはずである

3.png

Half lambert

ので 上記の計算式

float3 NdotL = dot( i.normal, lightDir);

を改良する
先程のは 法線が ライトと平行が1で ライトと直行(側面)が0。裏側が-1であったが
それを 1~0にリマップしてあげると 側面は0.5で かなり後ろまで弱いライティングがかかり
前よりはスムースになるだろう

//              float3 diffuse = max(0.0, NdotL ) * _LightColor0.rgb * col.xyz;
                float3 diffuse = (NdotL*0.5 + 0.5) * _LightColor0.rgb * col.xyz;

4.png

しかしこれでは少し後ろも光すぎと思うかもしれない
そういう場合は例えば2乗し

//              float3 diffuse = max(0.0, NdotL ) * _LightColor0.rgb * col.xyz;
//             float3 diffuse = (NdotL*0.5 + 0.5) * _LightColor0.rgb * col.xyz;
                float3 diffuse = pow(NdotL*0.5 + 0.5, 2) * _LightColor0.rgb * col.xyz;

5.png

計算式は色々考えられるので 好みで軽い物を探してみるといい

6.png

Blinn-Phong

https://github.com/YukiMiyatake/UnityLesson/tree/Shader_4

ブリンフォンの説明だ
フォンは先程 スムーズシェーディングのときに出たが
フォンさんは3D業界に素晴らしい功績を残した人なので フォンがつくものが複数あってややこしい・・
これはスムーズシェーディングのフォンではなく、ライティングモデルのフォンだ

ともかく これはスペキュラ(鏡面反射)といえばいい

先程のLambert ディフューズでは、光源と面法線だけで色が決まっていたが
そこに 視線ベクトルも関係する光 反射光を加えると、より質感が出てくる

たとえば 鏡や金属、水では 面と光源は動かなさなくても 自分が動くと
光の反射光が直接目に反射し 強い光を感じる
これが 鏡面反射光 スペキュラである

property

スペキュラを設定する場合にスペキュラパワーを設定する
これはマテリアルごとに鏡面反射の強さが違うからだ
たとえば 金属や鏡ではより大きな反射があるが
肌や髪、シルクの布ではそれより柔らかい反射
マットな布などではほとんど反射をしない。
スペキュラの設定は 質感にかなりの影響を与える

また、なぜかスペキュラ色も設定することが多いので設定してみる

 Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Spec1Power("Specular Power", Range(0, 30)) = 1
        _Spec1Color("Specular Color", Color) = (0.5,0.5,0.5,1)
    }

宣言

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 vertexW: TEXCOORD0;
                float2 uv     : TEXCOORD1;
                float3 normal : TEXCOORD2;
            };

            uniform sampler2D _MainTex; uniform float4 _MainTex_ST;
            uniform float _Spec1Power;
            uniform float4 _Spec1Color;

スペキュラのパワーと色を追加
vertexW は、ワールド座標上での頂点の値です
フラグメントシェーダでの計算に使います

頂点シェーダ

         v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.vertexW = mul(unity_ObjectToWorld, v.vertex);

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

vertexにはView変換まで行った値が入ってますが
vertexWには ワールド座標を入れてます
vertexがあれば ピクセルシェーダにて 計算も可能ですが
頂点シェーダで設定するほうが計算量が軽いはずなので そうしてます

ピクセルシェーダ

         float3 frag (v2f i) : SV_Target
            {
                float3 L = normalize(_WorldSpaceLightPos0.xyz);
                float3 V = normalize(_WorldSpaceCameraPos -i.vertexW.xyz);
                float3 N = i.normal;


                // texture albedo
                float4 tex = tex2D(_MainTex, i.uv);

                // Diffuse(HalfLambert)
                float3 NdotL = dot(N, L);
                float3 diffuse = (NdotL*0.5 + 0.5) * _LightColor0.rgb ;

                // Speculer
                float3 specular = pow(max(0.0, dot(reflect(-L, N), V)), _Spec1Power) * _Spec1Color.xyz;  // reflection


                return diffuse*tex + specular;
            }

ここがキモです

float3 specular = pow(max(0.0, dot(reflect(-L, N), V)), _Spec1Power) * _Spec1Color.xyz;  // reflection

この部分が具体的な スペキュラ計算です
スペキュラは強烈な光なので 指数的な変化が良いので Powで計算しています
問題は dot(reflect(-L,N),V) でしょう
-Lは ライトのベクトル
Vは カメラのベクトル(視線)
Nは法線
ともに ワールド座標系で統一しています

reflectとは反射ベクトルを計算する組み込み関数です
ライトベクトルと法線の反射光と カメラベクトルの内積をもとめ それが大きいと
反射光が強くなるという計算です

ちなみに reflectの中身は
reflect = L - 2N dot(L,N)
という計算なので それが全てのピクセルで計算されると そこそこ重い

ディフューズにスペキュラを加算すればできる

7.png

ハーフベクトル

上記の reflectは少し計算量がおもい
そのため、多少誤差があるが 圧倒的に軽い ハーフベクトルという方法を使う

ハーフベクトルは、光源ベクトルとカメラベクトルの間のベクトルを計算し、法線と内積をとる
計算式では
dot( normalize(L+V), N)
圧倒的速さ

float3 H = normalize(L+V);

// Speculer
// float3 specular = pow(max(0.0, dot(reflect(-L, N), V)), _Spec1Power) * _Spec1Color.xyz;  // reflection
float3 specular = pow(max(0.0, dot(H, N)), _Spec1Power) *  _Spec1Color.xyz;  // Half vector

8.png

一般的には ハーフベクトルが使われる

つぎは 影、セルフシャドウ、輪郭線、トゥーンとかかな・・・