読者です 読者をやめる 読者になる 読者になる

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity のシェーダの基礎を勉強してみたのでやる気出してまとめてみた

Unity Shader

はじめに

近年の GPU の進化に伴い 3D 周りの表現力がとても豊かになりました。そしてこの多彩な表現を可能としているのはシェーダによるところが大きく、シェーダを理解して書くことが出来ると、表現できることの幅がとても広がります。

Unity では素晴らしいことにシェーダを強力にサポートしていて、多様なデフォルトのシェーダに加え、カスタムシェーダを簡単に作るための土台が用意されています。しかしながら、パッとリファレンスや色々なサイトを見ただけでは、何がどうなっていて何をどうすれば良いのかなかなか分からないところがあります。

そこで、これから始める人の参考になればと思い、勉強しながら理解したことをまとめておこうと思います。調べながら書いているので、間違いなども多々あると思いますが、見つけた際はコメントや Twitter などでご指摘いただけると嬉しいです。

シェーダで色々出来る例

f:id:hecomi:20140316224537p:plain
下記エントリを参考にさせて頂いています。

羽ばたくアニメーションをするシェーダを割り当てたプレーンを Particle System で出しているので、千匹単位で出してもとても軽いです。

概要

Unity のシェーダは ShaderLab という、シェーダコードをラップする形で表現されます。CgFX および Direct3D Effects (.FX) 言語に似た形式とのことです。

上記ドキュメントに書かれている ShaderLab のコードを抜粋すると以下の形式になります。

Shader "MyShader" {
    Properties {
        _MyTexture ("My Texture", 2D) = "white" { }
        // カラーやベクトルなどのプロパティもこの箇所に記述
    }
    SubShader {
        // この箇所に以下の本体を記述
        //  - サーフェイスシェーダ または
        //  - 頂点およびフラグメントシェーダ または
        //  - 固定関数シェーダ
    }
    SubShader {
        // この箇所に、古いグラフィックスカードで実行される、より簡易のバージョンの前述のサブシェーダを記述
    }
}

実質シェーダを記述する SubShader の中では、シェーダ言語である Cg/HLSL で頂点シェーダやフラグメントシェーダ、サーフェイスシェーダ等を記述します。CgNVIDIA が開発したシェーダ言語、HLSLマイクロソフトが開発した Direct3D API 用のシェーダ言語です。

Cg/HLSL と書いてますが、2つの言語の知識が必要、というわけではなく、実質 Cg で書いているような形になってい(ると思い)ます。HLSL は NVIDIA と共同で開発したものなので言語的には Cg に似ているようで、基本的には同一とみなして問題ないようです。

上記フォーラムでは、Unity のグラフィック周りの中の開発者の Aras さんが、Cg で書いたコードも Xbox360 (HLSL) で動くよ!と仰っています。この手の Cg? HLSL? に関する疑問は皆持っているようで、英語で検索すると大量にヒットします。

勘の良い人は、ここで、あれ?iPhone / Android アプリも Unity で作れるけど、あっちは OpenGL ベースだから GLSL なんじゃない?と思うかもしれませんが、そこはとても良く出来ていて、Unity では、Cg/HLSL を GLSL にコンパイルしてくれるようです。このあたりの話は以下の Aras さんのエントリに書いてあります(英語です)。

昔は Windows / Xbox / PS だから HLSL とか Cg で良かったけど、iPhone とか Android とか WebGL とかで GLSL 必要になってきたんだよ、でも2個もシェーダ書くのツラミあるよね?的な導入から話が書いてあって面白いです。

ちなみに、条件付きですが、Cg/HLSL でなく GLSL を直接書くことも出来ます。

導入が長くなりましたが、次に Unity シェーダをより詳しく見て行きましょう。

シェーダの種類

Unity のシェーダは3種類あります。

  • サーフェイスシェーダ(Surface Shader)
    • ライティングやシャドウの影響の記述は大変ですが、これを自動生成されたコードとともに簡単に記述できます。
  • 頂点 / フラグメントシェーダ(Vertex / Fragment Shader)
    • ライティングが必要なかったり、イケイケな感じなのを書きたい人はコレです。ただ、ゴリゴリ書くのでつらみがあるようです。
  • 固定関数シェーダ(Fixed Function Shader)

おそらく、実際のコードを目にしないと何のことやらだと思いますので、次にコードを見ながら理解を深めていきたいと思います。

コードの例

シェーダの形式

冒頭でも触れましたが、ドキュメントからシェーダの構造の説明を抜粋すると、以下の形になります(一部改変しています)。

Shader "Structure Example" {
    Properties {
    	// インスペクタに表示されるプロパティ(色やテクスチャ等)
    }
    SubShader {
    	// ...頂点/断片プログラムを使用するサブシェーダ...
    }
    SubShader {
    	// ...パスごとに 4 つのテクスチャを使用するサブシェーダ...
    }
    SubShader {
    	// ...パスごとに 2 つのテクスチャを使用するサブシェーダ...
    }
    SubShader {
    	// ...見た目は悪いが、いずれでも実行するサブシェーダ :)
    }
    Fallback "Other Shader Name" // SubShader がいずれもダメだったときに試す別のシェーダ
} 

Properties と Fallback は省略可能で、結果的に、Shader "name" { [Properties] Subshaders [Fallback] } がシェーダの文法になります。ちなみに、"name" 部はスラッシュ("/")で区切ると、カテゴリで分類することが出来ます。
f:id:hecomi:20140314234209p:plain

固定関数シェーダ(Fixed Function Shader)

固定関数シェーダはプログラマブルシェーダをサポートしない GPU のために記述します。なのでスゴイことが出来るわけではないので、この項はあまり面白く無いかもしれません。文法は ShaderLab で記述します。

まず、シンプルなコードを見てみます。

Shader "Solid Red" {
    SubShader {
        Pass { Color (1,0,0,0) }
    }
} 

f:id:hecomi:20140314234844p:plain

Pass ブロックの中には色々な命令を記述でき、ここでは Color の指定を行っています。そして、固定関数シェーダは Pass のみからなるシェーダになります。インスペクタを見ると、Vertex shader / Fragment shader ともに「Fixed function」になっているのが見て取れます。このコードでは色をつけるだけなので、ライティングも関係なく真っ赤になります。

ではライティングを反映させてみます。

Shader "VertexLit White" {
    SubShader {
        Pass {
            Material {
                Diffuse (1,1,1,1)
                Ambient (1,1,1,1)
            }
            Lighting On
        }
    }
} 

f:id:hecomi:20140315002019p:plain

今度は Lighting On にして、Material ブロック内で Diffuse と Ambient を設定しています。Diffuse はオブジェクトのベースカラー(拡散色)、Ambient は Render Settings から変更できる、Ambient Light(環境光)の色です(指定しなければ影響を受けない形になります)。

続いて、色をインスペクタから設定できるようにしてみます。

Shader "VertexLit Simple" {
    Properties {
        _Color ("Main Color", COLOR) = (1,1,1,1)
    }
    SubShader {
        Pass {
            Material {
                Diffuse [_Color]
                Ambient [_Color]
            }
            Lighting On
        }
    }
} 

f:id:hecomi:20140315002632p:plain

Shader を適用した Material のインスペクタにプロパティで記述した Main Color が表示されるようになりました。これで色々と外部からシェーダへ情報を与えることが出来るようになります。

そして最後に、VertexLit 相当のシェーダを見てみます。

Shader "VertexLit" {
    Properties {
        _Color ("Main Color", Color) = (1,1,1,0)
        _SpecColor ("Spec Color", Color) = (1,1,1,1)
        _Emission ("Emmisive Color", Color) = (0,0,0,0)
        _Shininess ("Shininess", Range (0.01, 1)) = 0.7
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Pass {
            Material {
                Diffuse [_Color]
                Ambient [_Color]
                Shininess [_Shininess]
                Specular [_SpecColor]
                Emission [_Emission]
            }
            Lighting On
            SeparateSpecular On
            SetTexture [_MainTex] {
                Combine texture * primary DOUBLE, texture * primary
            }
        }
    }
} 

色々と新しい命令が出てきました。これらを覚えるのは大変ですが、すべてドキュメントに載っていますのでだいたい何をやってそうか把握できれば後は調べれば解決できると思います。

ちなみに、Properties ですが、例でみた Color の他にも以下の様なものを与えることが出来ます。

Properties {
    _RangeTest ("Range Test", Range (0.0, 1.0)) = 0.5
    _FloatTest ("Float Test", Float) = 0.5
    _ColorTest ("Color Test", Color) = (0.5, 0.5, 0.5, 0.5)
    _VectorTest ("Vector Test", Vector) = (0.5, 0.5, 0.5)
    _2DTest ("2D Test", 2D) = "white" {}
    _RectTest ("Rect Test", Rect) = "white" {}
    _CubeTest ("Cube Test", Cube) = "white" {}
}

f:id:hecomi:20140316185713p:plain

サーフェイスシェーダ(Surface Shader)

固定関数シェーダでは特に面白いことは出来ませんでしが、サーフェイスシェーダを使うと色々な表現が可能になります。シェーダは通常はライティングとの相互作用やレンダリングパスなど複雑なコードを書く必要があります。そこで Unity は、一段階抽象化されたコードを書くだけで、これをコンパイルして複雑なコードを自動生成してくれる仕組みとして、サーフェイスシェーダを提供してくれています。サーフェイスシェーダは Cg/HLSL で記述します。まず簡単なサンプルを見てみます。

Shader "Example/Diffuse Simple" {
    Properties {
        _MainColor("Color", Color) = (1,1,1)
    }
    SubShader {
        Tags { "RenderType" = "Opaque" }
        CGPROGRAM
        #pragma surface surf Lambert
        struct Input {
            float4 color : COLOR;
        };
        float4 _MainColor;
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = _MainColor.rgb;
        }
        ENDCG
    }
    Fallback "Diffuse"
}

f:id:hecomi:20140315173310p:plain

色々新しいものが出てきましたので1つずつ見て行きたいと思います。まず、サーフェイスシェーダは、CGPROGRAM 〜 ENDCG ブロック内に記述します。その最初の行にある pragma 文は、サーフェイスシェーダをどのようにコンパイルするかを指示しています。この指示は以下の形式で記述します。

#pragma surface surfaceFunction lightModel [optionalparams]

「surfaceFunction」はエントリポイントで、サンプルでは「surf」という名前の関数がそれですよ、と指定しています。「lightModel」は組み込みライティングモデルの「Lambert」(ランバート反射)を指定しています。ランバート反射は理想的な拡散モデルで、どの方向から見ても輝度が一定になるモデルです。他には BlinnPhong(スペキュラ)があり、自作のカスタムライティングモデルも指定できます(ドキュメント参照)。「optionalparams」には透過させたいときは alpha、頂点の変形をしたい場合は vertex などの指定が行えます。

では、このエントリ関数の surf を見てみます。

void surf (Input IN, inout SurfaceOutput o) {
    o.Albedo = _MainColor.rgb;
}

形式としては、Input を受け取り、それをゴニョゴニョと処理を行って SurfaceOutput につめる、という形になっています。ちなみに何もしないと真っ黒になります。Input でどういう情報が渡ってくるかについては直前の構造体に書いてあります。

struct Input {
    float4 color : COLOR;
};

Input では UV 座標などが渡ってきます(Properties 等で設定した変数ではないです)。ここでは未だ例が微妙なので詳しくは後で見ていきます。

SurfaceOutput には Albedo を指定しています。アルベドは反射の割合を示していて 0 にすると真っ黒になります。Albedo は SurfaceOutput 構造体のメンバになっていて、他には以下の様なメンバを有しています。

struct SurfaceOutput {
    half3 Albedo;
    half3 Normal;
    half3 Emission;
    half Specular;
    half Gloss;
    half Alpha;
};

ここで色々と指定するとオブジェクトの見え方が変わってくるわけです。例では、Properties で与えられた _MainColor の rgb を参照しているので、インスペクタから赤を設定すればオブジェクトも赤くなります。なお、Properties で与えられた変数を参照するためには、その変数を CGPROGRAM 〜 ENDCG ブロック内で宣言しておく必要があります。例では、surf 直前の行で宣言しています。

optionalparams について触れるためにちょっと上記コードを改良してみます。例えば、与えた色をちょっと赤くして透明度を 0.5 くらいにしたいとします。

Shader "Example/Diffuse Simple" {
    Properties {
        _MainColor("Color", Color) = (1,1,1)
    }
    SubShader {
        Tags { "RenderType" = "Transparent" }
        CGPROGRAM
        float4 _MainColor;
        #pragma surface surf Lambert alpha
        struct Input {
            float4 color : COLOR;
        };
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = _MainColor.rgb * half3(1, 0.5, 0.5);
            o.Alpha = 0.5;
        }
        ENDCG
    }
    Fallback "Diffuse"
}

pragma 文の後ろに alpha を追加しました。これは、シェーダがアルファブレンディングを行いますよ!ということを指示しているものです。この上で、surf の中で Alpha を 0.5 に設定しています。ついでに Albedo で、青と緑は 0.5 しか反射しないようにしています。optionalparams にどういうものが指定できるかについてはドキュメントを参照して下さい。

あとまだ触れていないのが Tags です。

Tags ブロックでは「いつ」「どのように」レンダリングを行うかの指定を Key-Value の形式で与えます。最初の例では RenderTypeOpaque (不透明)に指定しています。透明な方は Transparent を指定しています。が、あくまでタグ付けなので、透明なオブジェクトに Opaque を指定していても、透過自体は問題なく行われます。私もあまりよく分かっていないので...、詳細は下記エントリに丁寧にまとめられていましたのでご参照下さい。

  • (追記:2014/03/17)
    • Opaque にしていると SSAO の影響を受けるけれど、Transparent にすると無視されるみたいな違いはあるようです。

他には、描画順の Queue などが指定できます。

次に、飛ばしてしまった Input の話を見るために、テクスチャを適用したサンプルを見てみます。

Shader "Example/Diffuse Texture" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType" = "Opaque" }
        CGPROGRAM
        #pragma surface surf Lambert
        struct Input {
            float2 uv_MainTex;
        };
        sampler2D _MainTex;
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
        }
        ENDCG
    }
    Fallback "Diffuse"
}

f:id:hecomi:20140315184223p:plain

テクスチャの UV 座標を参照するためには Input 内で対象の変数の頭に uv プレフィックスを付加した変数を宣言します。これを Cg の組み込み関数の tex2D を使って、該当するテクスチャ座標の色を参照しています。Cg の組み込み関数については、ドキュメントをご参照下さい。

後は、法線マップを入力として陰影をつけたりなどを組込み関数と組み合わせてもりもりしていけば所望の画が得られるといった形になります。どういうコードを書けばどういうことが起きるのかはドキュメントが詳しく分かりやすいのでそちらに譲ります。ここまで理解していれば何となく何をしているかは把握できると思います。

面白そうなので、頂点の変形だけ試しにやってみます。球を Sin 関数で波打たせてみます。

Shader "Example/Transform Vertex" {
    SubShader {
        Tags { "RenderType" = "Opaque" }
        CGPROGRAM
        #pragma surface surf Lambert vertex:vert
        struct Input {
            float4 color : COLOR;
        };
        void vert (inout appdata_full v) {
            v.vertex.x += 0.2 * v.normal.x * sin(v.vertex.y * 3.14 * 16);
            v.vertex.z += 0.2 * v.normal.z * sin(v.vertex.y * 3.14 * 16);
        }
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = half3(1, 0.5, 0.5);
        }
        ENDCG
    }
    Fallback "Diffuse"
}

f:id:hecomi:20140315192004p:plain

Properties で変数を引っ張り出せばモーフィングとか出来ますね。ただし、上記例だと単に頂点位置をずらしてるだけなので、ライティングは球のままなので向きによっては変な感じに見えます。

ちなみに余談ですが、サーフェイスシェーダをインスペクタで見てみると以下のようになっています。

f:id:hecomi:20140315183729p:plain

固定関数シェーダの時は Fixed function だったのが、SM2.0 となっています。これは「シェーダモデル2.0」を意味しています。

SM2.0 はマイクロソフトインテルが両者の利点を活かして作ったスタンダードモデル(対応 GPU が多い)とのことです。

頂点シェーダ / フラグメントシェーダ(Vertex / Fragment Shader)

もっとも柔軟性の高いのが、この頂点シェーダ / フラグメントシェーダです。ライティングも行いたいならサーフェイスシェーダ、プログラマブルな形状やテクスチャを作成したいならこちら、みたいなイメージだと思います。
まずはコードから見てみましょう。

Shader "Custom/SolidColor" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            float4 vert(float4 v:POSITION) : SV_POSITION {
                return mul (UNITY_MATRIX_MVP, v);
            }
            fixed4 frag() : COLOR {
                return fixed4(1.0,0.0,0.0,1.0);
            }
            ENDCG
        }
    }
}

f:id:hecomi:20140315195333p:plain

今度は Pass ブロックの中に記述します。pragma 文で頂点シェーダおよびフラグメントシェーダの関数名を指定し、指定された関数を実装します。サンプルはとてもシンプルで、vert は頂点シェーダで、すべての頂点に対して座標変換を行います。ここでは、引数で与えられた頂点座標に UNITY_MATRIX_MVP という組み込み値である MVP 行列(モデル・ビュー・プロジェクション行列)をかけてモデルが2次元のディスプレイでどの位置に見えるかの計算を行っています。余談ですが組み込み値は以下の様なものがあります。

frag はフラグメントシェーダで、すべてのピクセルに対して計算を行います。ここでは不透明な赤を頂点に与えており、ライティングの計算は行っていないので、完全に真っ赤になっています。

ここまでは GLSL などに触れていた方は馴染みのある内容だと思いますが、「: POSITION」「: SV_POSITION」「: COLOR」となっている場所が気になりますね。これはセマンティクスと呼ばれるもので、グラフィックパイプラインの各ステージ間で伝達する変数が何なのかを伝えるタグの役割をします。GLSL の varying 変数みたいな感じですね。

つまり、vert は float4 型の引数を受け取るよう指定されていますが、float4 の何を貰えばいいのか?というのを指定している形になり、ここでは POSITION、つまり頂点のモデルに対するローカル位置座標を取得しています。セマンティクスは、頂点シェーダの入出力とフラグメントシェーダの入出力で計4つ与える箇所があります。例ではフラグメントシェーダの入力は省略されているので3つになっています。vert の出力が POSITION でなく、SV_POSITION となっているのは DX11 への互換性確保のためのようです。

そして、frag の出力が COLOR となっている形になります。セマンティクスは以下の様なもの(まだあると思います)が事前に与えられています。

頂点シェーダへの入力

Type(s) Tag Notes
float4 POSITION モデルのローカル座標
float3 NORMAL 法線
float4 TEXCOORD0 UV座標
float4 TEXCOORD1 2つめのUV座標
float4 TANGENT 接線
float4 COLOR

フラグメントシェーダへの入力(頂点シェーダの出力)

Type(s) Tag Description
float4 SV_POSITION MVP 変換後の座標
float3 NORMAL MVP 変換後の法線
float4 TEXCOORD0 1番目のテクスチャの UV 座標
float4 TEXCOORD1 2番目のテクスチャの UV 座標
float4 TANGENT 接線
float4, fixed4 COLOR0 線形補間された色
float4, fixed4 COLOR1 線形補間された色(1とは何が違う??)
Any タグを持たない何でも良い値

これらのような Pre-defined なセマンティクスを引数と返り値に与える以外にも、必要なセマンティクスをパックしたユーザ定義型を引数として与えることも可能です。

Shader "Custom/WindowCoordinates/Base" {
    SubShader {
        Pass {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            struct appdata_base {
                float4 vertex   : POSITION;
                float3 normal   : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            float4 vert(appdata_base v) : POSITION {
                return mul (UNITY_MATRIX_MVP, v.vertex);
            }

            fixed4 frag(float4 sp:WPOS) : COLOR {
                fixed2 red_green = sp.xy / _ScreenParams.xy;
                fixed  blue      = 0.0;
                fixed  alpha     = 1.0f;
                return fixed4(red_green, blue, alpha);
            }

            ENDCG
        }
    }
}

f:id:hecomi:20140316175959p:plain

appdata_base という構造体を用意し、この中で vertex、normal、texcoord の3種類のメンバをそれぞれ POSITION、NORMAL、TEXCOORD0 のセマンティクスを与えて宣言しています。そして、これを vert の引数として利用している形になります(中では結局 vertex しか使ってませんが...)。ちなみに、appdata_base は UnityCG.cginc というファイル内で宣言されており、これをインクルードすると使えるようにもなります。

Shader "Custom/WindowCoordinates/Base" {
    SubShader {
        Pass {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            #include "UnityCG.cginc"

            float4 vert(appdata_base v) : POSITION {
                return mul (UNITY_MATRIX_MVP, v.vertex);
            }

            fixed4 frag(float4 sp:WPOS) : COLOR {
                fixed2 red_green = sp.xy / _ScreenParams.xy;
                fixed  blue      = 0.0;
                fixed  alpha     = 1.0f;
                return fixed4(red_green, blue, alpha);
            }

            ENDCG
        }
    }
}

UnityCG.cginc の中には appdata_base のような構造体の他にも色々なヘルパ関数も用意してくれています。

全文は以下に上げてあります。

さて、もう一度シェーダのコードに戻ってみます。frag の引数を見てみると、WPOS というセマンティクスが与えられています。これはフラグメントシェーダでスクリーン座標を参照することが出来る変数になります。組み込み値の _ScreenParams.xy で割ってあげると、0 〜 1 でスクリーンの左下が (0, 0)、右上が (1, 1) になります。

f:id:hecomi:20140316181139p:plain

なお、WPOS を使うためには、シェーダモデルを 3.0 にしないとならないため、pragma 文で target を 3.0 に指定しています。インスペクタを見ると SM3.0 になっているのがわかると思います。

余談ですが、シェーダを普段あまり使わない人は return 文であれ?と思うかもしれませんが、シェーダは以下の様な演算が可能です。

return fixed4(fixed2(1.0), 0.0, 1.0);

fixed2(1.0) で (1.0, 1.0) を作り、fixed4 はこれを展開して、(1.0, 1.0, 0.0, 1.0)、すなわち黄色にするといった形です。

さて、先程はスクリーン座標を参照しましたが、もっと一般的には UV 座標を参照したいことのほうが多いと思います。その際は、UnityCG.cginc で定義されている頂点シェーダの vert_img と、そこから渡ってくる v2f_img を利用すると楽です。

Shader "Custom/TextureCoordinates/UV" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag

            #include "UnityCG.cginc"

            float4 frag(v2f_img i) : COLOR {
                return float4(i.uv, 0.0, 1.0);
            }
            ENDCG
        }
    }
}

f:id:hecomi:20140316182944p:plain

色がひとつの面で完結するようになりました。

最後に、Properties の値やテクスチャを使うには、以下のようにします。

Shader "Custom/TextureCoordinates/Hecomi" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Pass {
			CGPROGRAM
			#pragma vertex vert_img
			#pragma fragment frag

			#include "UnityCG.cginc"

			uniform sampler2D _MainTex;

			float4 frag(v2f_img i) : COLOR {
				return tex2D(_MainTex, i.uv);
			}
			ENDCG
		}
	}
}

f:id:hecomi:20140316183431p:plain

Properties で記載した変数を、uniform 修飾子をつけて CGPROGRAM ブロック内で宣言すると、頂点シェーダ / フラグメントシェーダで使えるようになります。

これで、なんとなく頂点シェーダとフラグメントシェーダの概要は掴めたのではないでしょうか。

実践例

冒頭のデモのコードを晒してみます。

Shader "Custom/Chou" {
    Properties {
        _BendScale("Bend Scale", Range(0.0, 1.0)) = 0.2
        _MainTex("Main Texture", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType" = "Transparent" }
        Cull Off Zwrite On
        CGPROGRAM
        #pragma surface surf Lambert alpha vertex:vert
        #define PI 3.14159
        struct Input {
            float2 uv_MainTex;
            float4 color : Color;
        };
        sampler2D _MainTex;
        float _BendScale;
        void vert (inout appdata_full v) {
            float bend = sin(PI * _Time.x * 1000 / 45 + v.vertex.y);
            float x = sin(v.texcoord.x * PI) - 1.0;
            float y = sin(v.texcoord.y * PI) - 1.0;
            v.vertex.y += _BendScale * bend * (x + y);
        }
        void surf (Input IN, inout SurfaceOutput o) {
            float4 tex = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = IN.color.rgb;
            o.Alpha  = IN.color.a * tex.a;
        }
        ENDCG
    }
    Fallback "Diffuse"
}

本当は vertex / fragment shader で書いた方良いと思うんですが手抜きです。でも割りとお手軽に出来て面白いですね。

(追記:2014/03/17)
vertex / fragment shader 版も書きました。

Shader "Custom/Chou" {
    Properties {
        _BendScale("Bend Scale", Range(0.0, 1.0)) = 0.1
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Pass {
            Blend SrcAlpha One
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #define PI 3.14159

            #include "UnityCG.cginc"

            uniform float _BendScale;
            uniform sampler2D _MainTex;

            struct v2f {
                float4 position : SV_POSITION;
                fixed4 color    : COLOR;
                float2 uv       : TEXCOORD0;
            };

            v2f vert(appdata_full v) {
                float bend = sin(PI * _Time.x * 1000 / 45 + v.vertex.y + v.vertex.x);
                float x = sin(v.texcoord.x * PI) - 1.0;
                float y = sin(v.texcoord.y * PI) - 1.0;
                v.vertex.y += _BendScale * bend * (x + y);

                v2f o;
                o.position = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv       = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
                o.color    = v.color;
                return o;
            }

            fixed4 frag(v2f i) : COLOR {
                fixed4 tex = tex2D(_MainTex, i.uv);
                tex.rgb *= i.color.rgb;
                tex.a   *= i.color.a;
                return tex;
            }
            ENDCG
        }
    }
}

加算にしてます。ライティングの計算をしない分軽くなったので、蝶の数を増やしてエフェクトも盛ってます。

おわりに

やっとスタート地点に立てた気がします。ライティングをいじったりトゥーンシェーディングとかに踏み込んだりは深淵な感じがするので、取り敢えずは冒頭のデモのようなエフェクト系を豪華にするために色々といじっていきたいと思います。