ムラサメ研究ブログ

主にゲームやプログラミングのログ

UNITYシェーダー研究 Vertexライティング?

コード
https://github.com/YukiMiyatake/UnityLesson/tree/Shader_7

はじめに

前回まではピクセルシェーダにてライティング計算をしていました
これはクオリティの高いライティングになりますが、少し処理が重くなるので
ディフューズカラー、スペキュラを頂点シェーダで計算しようという計画

シェーダー入出力の説明

今まで説明省略してたのを大雑把に
頂点シェーダ(vert)、フラグメントシェーダ(frag)には引数がある

頂点シェーダの引数は構造体で表現し、頂点ごとのパラメータが入ってくる
例えば ポジション、法線、UVなど

そして頂点シェーダの返り値も構造体で、頂点毎の情報を設定する

そして 頂点毎にフラグメントシェーダに その時 ポジション(SV_POSITION)以外は線形補完されます
UVが保管され、テクスチャマッピングが正しく出来るのはわかると思うが
法線なども補完され、ライティングがスムースになります

シェーダ引数は、型のほかにセマンティクスがあり、目的別に指定します
細かい事は説明しません

たとえば前回は

 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;
    };

頂点シェーダの引数は ポジション、法線、UV座標
フラグメントシェーダ引数は ポジション、ワールド座標でのポジション、UV、法線

フラグメントシェーダ引数はピクセルごとに補完される

方針

1個目のライト(ForwardBase)は現状通りのピクセルでのライティングを行う

2個目以降のライト(ForwardAdd)は、頂点シェーダでライト計算を行い ピクセルシェーダでは補完された値を使う
少し精度はわるくなるが、計算量は削減されるはずである

具体的には ディフューズの値を補完することができる
スペキュラに関しては補完することも可能であるが、大きくクオリティが落ちる可能性がある
今回は例として スペキュラも補完して軽くしよう

コード

具体的には ディフューズとスペキュラの計算を頂点シェーダに持っていく
ディフューズとスペキュラの値を フラグメントシェーダに渡す

構造体は下記になる

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

    struct v2f
    {
        float4 vertex : SV_POSITION;
        float2 uv     : TEXCOORD1;

        float3 diffuse : TEXCOORD2;
        float3 specular : TEXCOORD3;
    };

頂点シェーダの入力に変更なし

フラグメントシェーダの入力は
仕様にて vertexは削れない
ワールド座標のポジションは不要 法線も不要

ディフューズ値とスペキュラ値を追加

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

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


        float3 L = normalize(_WorldSpaceLightPos0.xyz);
        float3 V = normalize(_WorldSpaceCameraPos - vertexW.xyz);
        float3 N = normal;
        float3 H = normalize(L + V);
        float3 lightCol = _LightColor0.rgb * LIGHT_ATTENUATION(i);
        float3 NdotL = dot(N, L);

        o.diffuse = (NdotL*0.5 + 0.5) * lightCol;
        o.specular = pow(max(0.0, dot(H, N)), _Spec1Power) *  _Spec1Color.xyz * lightCol;  // Half vector

        return o;
    }

    float3 frag(v2f i) : SV_Target
    {

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

        return i.diffuse * tex + i.specular;
    }

フラグメントシェーダで計算していたディフューズとスペキュラを頂点シェーダで計算
フラグメントシェーダでは 補完されたディフューズとスペキュラをそのまま利用する

通常
f:id:murasame-labo:20170925004107p:plain

軽量化 f:id:murasame-labo:20170925004122p:plain

心配になるぐらい 変わらなかった(心配

UNITYシェーダ研究 複数ライトの影響

コード
https://github.com/YukiMiyatake/UnityLesson/tree/Shader_6

前回まで

前回はAmbientも入れて だいたいライティング出来た
ただ、複数ライトを置いても1つ目のライトの影響しか受けない
ので、複数ライトの影響をうけるようにする

ライトの強さ

複数ライト対応するまえに、今までのライトではライトカラーの影響はうけるが
ライトの強さの影響が入っていないので追加する

ライトの種類により減衰しないライト(DirectionalLightなど)と 減衰するライト(PointLight等)があり
それらをまとめて計算するマクロが存在するので それを使う

AutoLight.cginc をIncludeし、LIGHT_ATTENUATION(i) でライトの強さが取得できる iはピクセルデータ

それをライトカラーに乗算することで、強さを反映したライト色が計算できる

float3 lightCol = _LightColor0.rgb * LIGHT_ATTENUATION(i);

ライトカラーは、ディフューズとスペキュラに影響するので、それぞれに乗算する

float3 diffuse = (NdotL*0.5 + 0.5) * lightCol;

float3 specular = pow(max(0.0, dot(H, N)), _Spec1Power) *  _Spec1Color.xyz * lightCol;  // Half vector

複数ライトに対応

UNITYでは、キャラに最も影響の強いライトは LightMode=ForwardBaseで計算するが
その他のライトは LightMode=ForwardAddで入ってくる
ので、ForwardAddのパスを増やせばいい

シェーダーの中身は 今回はほぼ、ForwardBaseのものをコピーする
ただし、Ambientは1回しか計算してはいけないので ForwardAddでは計算行わない事と
BlendModeを One Oneなどにしなければ、合成されない

BlendMode設定なし
f:id:murasame-labo:20170924035237p:plain

メインライトのみ
f:id:murasame-labo:20170924035250p:plain

ライト2個(赤ライト追加)
f:id:murasame-labo:20170924035334p:plain()

複数ライトが反映されるようになった

UNITYシェーダ研究 Ambient

コード
https://github.com/YukiMiyatake/UnityLesson/tree/Shader_5

前回のシェーダ

ディフューズとスペキュラを実装し、だいぶライティングができた
テクスチャマッピングもしている

でも、Ambientも与えていません

Ambientの設定

UNITYのバージョンにより違うけど
最近のバージョンでは
Window-Lighting-Setting から行う

f:id:murasame-labo:20170922015418p:plain

Environment Lightingのあたり

Ambientの取得

Ambinetカラーの取得は定数が用意されているので簡単だ

UNITY_LIGHTMODEL_AMBIENT.rgb;

これを計算式に加えるが
テクスチャ * Ambientの値を今までのものに足せばよいので最終的には

最終カラー = Ambientテクスチャ + Diffuseテクスチャ + スペキュラ
= (Ambient+Diffuse) * テクスチャ + スペキュラ
にした

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


                    //Ambient
                    float3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                    
                    // 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
                    float3 specular = pow(max(0.0, dot(H, N)), _Spec1Power) *  _Spec1Color.xyz;  // Half vector


                    return (ambient + diffuse) * tex + specular;
                }

カレーハッキング

これからは ニジマスの手に入るシーズンになるので、ニジマスカレーを極めようとおもう

1回作ったけど 普通に美味しいのだけど、これからさらにプラスしていく

まず前回はゴールデンカレールウを使った。普通に美味しいけど、ビーフ等にあう、濃厚なルーであった
また スパイスも自分で頑張りたいので、スパイス研究科 中村繭子パイセンや友達のアドバイスもらいつつ
自分のニジマスカレーを完成させたい

知見

一度作った時の知見であるが、野菜の多いカレーが好きだが、歯ざわりや触感が統一されてないと ん?? ってなる
例えばヤングコーン入れたがそれだけ固く ん?? だったので、煮込む野菜以外は 蒸し野菜でトッピングする

ニジマスはおろして骨などを取り除くと食べやすいが、身がくずれたり、骨のエキスが美味しいので、いまはぶつ切りで考える
将来的には、あら汁と分けたり、トッピングで乗せ身が崩れなくするなど 考えたい

デミ系のカレー好きだけど、私の求めるものではないので、ルウにと溶かす野菜は慎重にえらぶ

次への

インドのベンガル地方は川魚のカレーが多いので参考にする
川魚用のスパイス
フェンネル、クミン、ニゲラ、ブラウンマスタード、フェニュグリーク

魚はターメリックと塩とレモン汁で漬けておいて使うこと多い

酸味苦手ではあるが、酸につけて少し骨を柔らかくできるといいな

カレー  上野不忍池 サルガム

何か料理を極めたいのでカレーにしてみた
カレーは好きだし、みんな食べられる

前提として小麦アレルギーなので、ご飯にあうカレー
お米はジャポニカ米がベストだけど、場合によればインディカ米買ってもいい

今は色々なものを作るのが先 敵を知るにはまず己を知る必要がある

不忍池近くの インド&ネパール料理
こういう店は、インド料理ではなくネパールがメインのはず

とりあえず すべて入ってるコース頼む
バターチキンカレー、キーマカレー、サラダ、デザート、ナン、ライス、シシカバブ、タンドリーチキン、ラッシー

バターチキン、キーマカレーは甘い。辛口MAXだけど 私は辛さには強いので物足りないが、とにかく砂糖甘さ
デザートはインドらしく 激甘
ナン、ラッシーは甘さ控えめ

ナンは甘いのが好きだったので、残念
カレーも 味は普通だけど、好みというか目指す方向と大きく違う

そして 小麦アレルギーで速攻おなか壊し、営業に送れるという ダメなパターン・・

UNITYシェーダ研究 Blinn-Phong

コード
https://github.com/YukiMiyatake/UnityLesson/tree/Shader_4

はじめに

ディフューズを入れて 基本的なLambertモデル実装しました

次はこれにスペキュラを追加します(念のため DirectSpecularのみです)
Blinn-Phong的な事をします(定義間違ってたら教えて下さい)

スペキュラ

鏡面反射です
前回のディフューズは、ライトが面に当たった時のライティングで、View(カメラの位置)とは関係ありません
スペキュラでは、光の反射モデルなので 鏡に光源が写った時= 光の反射を直視した時のみ強烈に光ります
3DCGでは こういう事になります
光の入射角と法線のreflectionベクトル(面法線に対してライトが同じ角度で反射)と Viewベクトルの内積と正の相関関係

スペキュラの場合は、相関は Powでかけます。細かい物理的な理由は調べてないけど
スペキュラのパラメータは今回は、スペキュラ色と Powerにします

property

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

今回はスペキュラカラーとPowerが増えた

宣言

         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を送ってます
vertexには ビューポート変換まで行うので、World座標での位置情報が必要なためです

頂点シェーダ

         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;
            }

vertexWに対して World座標系への変換をかけています

フラグメントシェーダ

         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;
            }

Vは ViewVectorです。  _WorldSpaceCameraPosは World座標系でのカメラのポジションで World座標系でのポジションをマイナスすると、ピクセルからカメラに向かうベクトルになります

dot(reflect(-L, N), V) これが、反射ベクトルと Viewベクトルの内積部分
reflectというのは組み込み関数で この場合は 法線を中心として 反射したライトベクトルが取れる
それを maxで 0未満(裏面)の計算を無効にし
スペキュラの強さで累乗しスペキュラ色を乗算

スペキュラは加算なので単純にディフューズに掛け合わせる

f:id:murasame-labo:20170913102938p:plain

カメラを動かすと スペキュラのライティングだけ変わります
ディフューズはカメラ位置とは無関係なのもわかると思います

ハーフベクトル

上記の計算は 正確な反射ベクトルを計算した
具体的には reflect(-L,N)の部分だが、反射ベクトルは多少計算が重い
R = L - (2V・N) N
L - 2 * dot( V*2, N ) * N

スペキュラは ハーフベクトルを使って近似が出来る事が広く知られているのでそれを実装する

ハーフベクトルとは、ライトベクトルとViewベクトルの間のベクトルであり、それと法線の内積で近似出来る

ハーフベクトルは normalize(L+V); で表される
reflectより圧倒的に計算量が低い

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

f:id:murasame-labo:20170913105709p:plain

計算間違えてるのか、ライトの影響パワーが大きく異なるが、スペキュラはかかった

UNITYシェーダ研究 Lambertモデル実装

前回は 新規で作ったUNITシェーダの説明したので
簡単なシェーダ作ります

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

基本シェーダ(Albedo)

先ほどのシェーダからFOGを削除しシンプルにします
テクスチャは・・・雰囲気欲しいからつけておく

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
        }
    }
}

f:id:murasame-labo:20170911092726p:plain

テクスチャ(Albedo)のみですね

Lambert

最も簡単なライティングモデルの ランバート反射を作ります
先ほどのAlbedoはライトの影響をうけなかったけど今度はうけます

ランバート反射は、面にライトが当たる角度により影響の強さが変わる
具体的には 法線とライトベクトルの内積を乗算する

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
        }
    }
}

LightMode

Tags{
  "LightMode" = "ForwardBase"
}

これを付けないと安定しなかったので付けた
LightModeにより呼ばれるパスが変わる

ForwardBaseは Forwardレンダリングのメインのライトのパス
ForwardAddは Forwardレンダリングのメイン以外のライトがそれぞれ呼ばれる
Deferredは Deferredレンダリングのパス
ShadowCasterは 影のパス
など

構造体

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

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

法線が必要なので NORMALが追加

シェーダ中身

         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;
            }

o.normal = UnityObjectToWorldNormal(v.normal);
法線がローカル座標系なので World座標系に変換する

WorldSpaceLightPos0、LightColor0 はビルトイン定数(Lighting.cginc のinclude必要)
WorldSpaceLightPos0: 0番目のライトのポジション(ディレクショナルライトは方向) LightColor0: 0番目のライトの色

float3 NdotL = dot( i.normal, lightDir);
ここで 法線とライトの内積を計算する

ディフューズは ライトと法線の内積ライトカラーとテクスチャカラーを掛ければ求められる
内積は負の場合(裏面)があるので その場合は0にしておく

f:id:murasame-labo:20170911160322p:plain

ライトを動かせば ライティングも変わるようになった
でも少し暗い

またSphereを置けばわかりやすいが、陰のつき方が90度でわかれている
実際のオブジェクトは光の回析や吸収反射などで もっとゆるやかに出るべきである

ハーフランバート

先ほどのランバートだと、ライティングが90度で終わる
f:id:murasame-labo:20170911162221p:plain

もう少しゆるやかにしたいのでハーフランバートを適用してみる 先ほどのランバート反射では ライトと法線の内積が -1~1 であったが  0~1の範囲にリマップすれば ゆるやかになる

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

f:id:murasame-labo:20170911162420p:plain

もう少しピーキーにしたいなら 先ほどの内積を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;

f:id:murasame-labo:20170911162635p:plain

他にも float3 diffuse = (NdotL*0.75 + 0.25) * _LightColor0.rgb * col.xyz;

とか色々と考えれるので、好みで使い分けよう

f:id:murasame-labo:20170911172540p:plain