tanaka's Programming Memo

プログラミングについてのメモ。

高さでグラデーションをかけるシェーダーをUnityで作ってみた

この記事は、シェーダーアドベントカレンダー Advent Calendar 2019の8日目の記事です。

qiita.com


ローポリでアンビエントオクルージョンが効かないような形だとのぺっとしてしまいます。頂点色を手動で設定するのは面倒だし、その程度のシェーダーなら自作できるかな?と思って作ってみたので公開します。

https://cdn-ak.f.st-hatena.com/images/fotolife/a/am1tanaka/20191209/20191209133511.png

2019/12/9 シェーダー言語はCgは廃止の方向であり、HLSLの方を調べるのがよいでしょうと、さやちゃんぐbotさんから教えていただきましたので追記しました。

2019/12/12 Cg/GLSLについて、さやちゃんぐbotさんから追加で教えていただいたことについても追記しました。

目次

高さグラデーションシェーダーHeightGradationShaderの使い方

まずは作成したシェーダーの使い方をご紹介します。

シェーダーの作成

  • UnityのProjectウィンドウから、Create > Shader > Unlitを選択して、Unlitシェーダーを作成します

f:id:am1tanaka:20191209131154p:plain
Unlitシェーダーを作成

  • 名前はHeightGradationUnlitShaderなどでOKです
  • 作成したシェーダーをダブルクリックしてエディターで開きます
  • 元のスクリプトを消して、以下のスクリプトを貼り付けて保存してください

以上でシェーダーの準備完了です。あとはマテリアルに設定すれば動作します。

マテリアルの作成とオブジェクトへのアタッチ

  • マテリアルを作成して、シェーダーをUnlit > HeightGradationShaderに変更します

f:id:am1tanaka:20191209132103p:plain
シェーダーをマテリアルに設定

  • SphereでもCapsuleでもQuadでも、適当な3Dオブジェクトを作成して、作成したマテリアルをアタッチします

f:id:am1tanaka:20191209133058p:plain
デフォルト設定でアタッチした例

パラメーター

設定できるのは、テクスチャー、上端の高さと色、下端の高さと色です。

f:id:am1tanaka:20191209133511p:plain
パラメーターの調整

以上です。

このシェーダーはUnlitなので光の影響は受けません。フォグは有効です。


ざっくりと考え方

シェーダーは時々気が向くと触る程度で、記憶には何も残っていない初心者です。天神いなさんのこちらのブログを読みつつ作成しました。基本的な知識はほぼこれで得られました。

amagamina.jp

やりたかったことは以下の通りです。

  • オブジェクトの上端と下端の高さと色を設定する
  • オブジェクトの各頂点の高さから、設定に合わせた頂点色を計算して頂点カラーを設定する

以上から、必要な仕様は以下の通り。

  • float型の値を2つと、色を2つ、マテリアルから設定できるようにする
  • 頂点の高さから、上端と下端に対する内分率を求める
  • 上端の色と下端の色と求めた内分率から、頂点の色を決定
  • 求めた頂点の色を適用する

実装の解説

ブログに従って、Unlitシェーダーから作成しました。

パラメーターの設定

マテリアルで設定するパラメーターはPropertiesで宣言します。高さはFloat、色はColorで宣言すればよいので、以下の通りです。

// 3:
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
        _TopY("TopY", float) = 0.5
        _TopColor("TopColor", Color) = (1, 1, 1, 1)
        _BottomY("BottomY", float) = -0.5
        _BottomColor("BottomColor", Color) = (0, 0, 0, 1)
    }

MainTexはもともとあったテクスチャの宣言で、それ以外が追加したパラメーターです。これでマテリアルから欲しいパラメーターを設定できるようになります。ここで使える変数型はこちら

処理に必要なデータの定義

頂点シェーダーからフラグメントシェーダーに渡すデータを定義します。v2fというのがそれです。頂点色を渡したいので、vert_colorという名前のfloat4型のパラメーターを追加しました。

// 32:
            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vert_color : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

プロパティの値を受け取るための変数を定義します。プロパティの最初に書いたのと同じ名前の変数を定義すればOKです。高さの値は制度はそれほど必要ないのでhalfにしました。floathalffixedのどれを使うかの基準はこちらにあります。

// 42:
            half _TopY;
            half _BottomY;
            float4 _TopColor;
            float4 _BottomColor;

頂点シェーダー

今回の肝はここです。追加したのは以下の52,53行目の2行です。

// 47:
            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                half t = (v.vertex.y - _BottomY) / (_TopY - _BottomY);
                o.vert_color = lerp(_BottomColor, _TopColor, saturate(t));
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

頂点の座標はv.vertexで渡されます。これはモデル座標系、つまり親のオブジェクトを原点とした座標です。今回やりたかったのは、正にこの座標系での高さでの判定だったのでそのままこの値が使えます。

与えられた高さを、下端なら0、上端なら1になるように変換します。

  • y座標から下端を引いて、下端の高さが0になるようにする
  • 求めた値を、上端-下端で割って、上端の高さが1になるようにする
  • saturateを使って、0未満は0に、1以上は1に丸め込む
  • lerpに下端の色、上端の色、求めた内分率を渡して、頂点に対応する色を求める

以上で求めた色を、宣言しておいたvert_colorに放り込んで頂点シェーダーは完了です。

saturateについて

saturateは組み込み関数です。最初はmaxminで求めましたが、Clampぐらいあるだろうと調べて見つけました。UnityのシェーダーはCg/HLSLと名乗っているnVIDIACg言語MicrosoftのHLSLを混ぜた怪しいやつなのですが、とりあえず同じようなものらしいので、どちらかのリファレンスに載ってるやつは使えるだろうということで試したら的中でした。

HLSLのリファレンスはこちら

Cgの組み込みライブラリのリファレンスはこちら

追記

CgとHLSLについて、さやちゃんぐbotさんからコメントいただきました!

HLSLの方で調べるのがよさそうです。この辺もやもやしてたのでスッキリしました^^

フラグメントシェーダー

フラグメントシェーダーは簡単です。受け取った色を、テクスチャーの色に掛けるだけです。以下の63行目がそれです。

// 58:
            fixed4 frag(v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // gradation
                col = col * i.vert_color;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }

これで出来上がり!!

Cgか、HLSLか、GLSLか(12/12追記)

今から始めるならどれだ、というのが悩みどころです。Unityは当初はCg言語を採用していたようですが、開発元のnVIDIACg言語の開発から手を引くと宣言してしまいました。Cgのリファレンスに「これからWindows向けにシェーダーをやるならHLSLがいいよ~」的に書いてあります。

この辺りの流れについて、さやちゃんぐbotさんの以下のスライドの11~14ページあたりで触れられています。

learning.unity3d.jp

しかし、ターゲットがスマホの場合はどうなんだ?となります。AndroidはGLSL、iOSは古いのはGLSLで最近のはMetalです。この辺りについては、Cg/HLSLでも問題が起きることがあるそうです。

UnityはGLSLには対応はしてはいるけど、HLSLの方を主に据えているのでGLSLを使うには注意点があるっぽい。iOSはMetalだからGLSLで書いたとしても安泰ではありません。結局、オールラウンドな選択肢はないというのが現状のようです。開発リソースが小さい個人開発の場合は、Unityさんの仕組みに乗っかってHLSLベースで開発して、不具合が発生したらGLSLやらMetalやらと仕様を突き合わせて対応する、というのが現実的っぽいです。

まとめ

高さ限定という用途が制限されまくりの習作シェーダーの完成です。

  • 必要なプロパティを定義
  • 利用する機能を指定
  • 受け渡したいデータを定義
  • 頂点とフラグメントシェーダーを書く

という流れがおおよそ把握できたので、簡単なシェーダーなら自作できそうです。画面中心から同心円状にグラデーションをかけるのも作りたかったので、それもおおよそ見当つきました。

今回のは高さ限定でしたのでとてもシンプルでしたが、高さ以外の方向を基準にする場合は、基準となる向きベクトルをプロパティに追加 > 向きベクトルを単位ベクトルにする > 単位向きベクトルと座標の内積を求める > 求めた内積の値に対して、今回と同じ処理をする、でいけると思います。

何かのお役に立てば幸いです!

参考URL