Unity Shader

Unityのシェーダで遊んでみよう
-
Unityだと、マテリアルとシェーダが一対一で紐づく
-
Unityのサーフェイスシェーダは3つの工程あり
- Vertexシェーダ:頂点情報を処理
- Surfシェーダ:オブジェクトの表面の色を定義
- Lighting:オブジェクトの陰影を決める
-
VertexとLightingの工程はUnityが自動生成してくれる
-
VertexやLigthingの工程はトゥーンシェーダ(toonn shader)や頂点カラーシェーダ(vertex shader color shader)の時に書き換える

20行から始めるUnityミニマルシェーダ
シェーダの構造は規範は3つのパートに分かれる
-
Parameters
- インスペクタに公開する変数を記載
- public変数のようなもの
-
Shader Settings
- ライティングや透明度などのシェーダの設定項目を記述
-
Surface Shader
- シェーダ本体のプログラム
-
surfシェーダの位置づけ
- Vertexシェーダから出力された値(Input構造体)を入力に取り、オブジェクトの表面色(SurfaceOutputStandard)を出力する
- Input構造体で記述したフィールドだけがサーフェイスシェーダに渡される
入力変数名 | 意味 |
---|---|
uv_MainTex | テクスチャのuv座標 |
viewDir | 視線方向 |
worldPos | ワールド座標 |
screenPos | スクリーン座標 |
- Output構造体は次のような値を持つ
出力変数名 | 意味 |
---|---|
Albed | 基本色 |
Normal | 法線情報 |

シェーダのパラメータをインスペクタから設定する
//マテリアル
fixed4 _BaseColor;
void surf (Input IN, inout SurfaceOutputStandard o) {
o.Albedo = _BaseColor.rgb;
}
//C#スクリプト
GetComponent<Renderer> ().material.SetColor ("_BaseColor", Color.black);
- _BaseColor貶すんで指定した色がマテリアルの色になる
- 宣言は思った以上も簡単で、取得も簡単
- fixed4型ってなんだろう?(疑問)

透明なシェーダを作る
Tags { "Queue" = "Transparent" }
- Tagブロックの中のQueueは描画の優先度を指定
- 基本はカメラからみて遠くにあるものがら順に描画される
- 半透明のものを描画するときは、不透明オブジェクトの後に描画しないと描画結果が破綻する
- 「Backgroung」→「Geometry」→「AlphaTest」→「Transparent」→「Overlay」の順で描画
#pragma surface surf Standard alpha:fade
- 「alpha:fade」を指定することで、オブジェクトを半透明で描くことができるようになる
- その他のパラメーターは下記(あとでその他を試せるのら試す)
パラメータ | 説明 |
---|---|
alpha:auto | alpha:fadeとalpha:premulのあわせ技 |
alpha:blend | アルファブレンディングを可能にします |
alpha:fade | 従来の透過性のフェードイン/アウトを可能にします |
alpha:premul | プレマルチプライドアルファ透明度を可能にします |
- 窓ガラスやフェンスなど特定の部分だけを透明させたい場合は、テクスチャとは別のグレースケールの画像を用いて透明度を指定

氷のような半透明シェーダを作る
- 法線ベクトル
- worldNormal(オブジェクトの法線ベクトル)を入力
- 法線ベクトルはオブジェクトの表面に対して垂直方向のベクトル
- 視線ベクトル
- viewDir(視線ベクトル)を入力
- 視線ベクトルはカメラが向いている方向のベクトル
- worldNormalとviewDIrを使って輪郭部分では1、中央部分では0になるような計算式を考える
→ドラゴンんの輪郭部分の透明度が低くなっているのに対して、中央部分の渡欧メイドは高くなっている - 輪郭部分では視線ベクトルと法線ベクトルが垂直に近い角度で交わるのに対して、中央部分ではほぼ平行に近い角度で交わっている
→この2つのベクトルが交わる角度を透明度に変更すればよい
- 2つのベクトルのなす角度を調べるために、内積(dot product)を使う
- worldNormal * viewDir = |worldNormal| * |viewDir| * cosθ = cosθ
- 垂直に交わる場合は0、平行に交わる場合には1また-1となる計算ができる
→今回は、垂直に交わる場合には透明度1、平行の場合は透明度0にするため絶対値をとってから1を引く
alpha = 1 - abd(cosθ) - 1.5を掛けるのは見た目の調整をするためのパラメータ

リムライティングのシェーダを作る
- リムライティング
- 3Dモデルの後ろからライトが当たっている演出のこと
- 3Dモデルの後ろ側からライトの光が、モデルの手前側に回り込み輪郭部分を強調する
- オブジェクトの輪郭部分のエミッションを高くすることで、リムライティングのような演出にしている
- エミッション→熱・光なその放射、放出。または放出されるもの
- 周辺部分の色変更なので、オブジェクトの法線ベクトルと視線ベクトルを使用
struct Input
{
float2 uv_MainTex;
float3 worldNormal;//オブジェクトの法線ベクトル
float3 viweDir;//視線ベクトル
};
-
輪郭部分を強調するアルゴリズム
- 輪郭部分では視線ベクトルと法線ベクトルが垂直に近い角度で交わる
- 中央部分ではほぼ並行に近い角度で交わる
- つまり、リムライティングでは視線ベクトルと法線ベクトルが垂直近い場合だけ、エミッションの値を高くすれば良い!
-
視線ベクトルと法線ベクトルの交わる角度を求めるため、worldNormalとviweDirの内積をとる
worldNormal * viweDir = |worldNormal| * |viweDir| * cosθ = cosθ
-
垂直に交わる場合は0
-
並行に交わる場合は1または-1
-
2つのベクトルが垂直い交わる場合にはエミッションを1、並行の場合はエミッションを0にするために絶対値をとってから1から引く
- 垂直に交わる場合には1、並行に交わる場合には0になるようにした
rim = 1 - abd(cosθ)
- 垂直に交わる場合には1、並行に交わる場合には0になるようにした
-
rimの値は0 < rim < 1なので、このrim係数をrimColorにかけてから、SurfaceOutputStandardのEmissionに代入すればよい
o.Emission = rimColor * rim
-
光の減衰を調整
- 原因:rim変数の減衰がcos関数に従うため、中央付近まで光が減衰しないから
- rimを3乗したものをrimColorに乗算してからEmissionに代入
-
o.Emission = rimColor * pow(rim, 3)

テクスチャを表示する
- sampler2D型の_MainTex変数→テクスチャ格納用の変数
- サーフェイスシェーダでは1ピクセルごとにsurfメソッドが実行される
- サーフェイスシェーダは現在処理しているテクスチャの位置(uv座標)を知しないといけない
- 入力の構造体(Input)に、uv[テクスチャ変数名]という名前のメンバ変数を宣言
- サーフェイスシェーダには処理すべきテクスチャのuv座標が自動的に渡される
struct Input
{
float2 uv_MainTex;
};
-
surfメソッドの中でtex2Dメソッドを使って、_MainTexからuv_MainTexで指定された座標の色を取得して、それを出力する色としてAlbedoに指定
-
インスペクタからテクスチャを設定できるようにPropertiesブロックにテクスチャファイルを宣言
-
Propertiesブロックの書き方
- 「公開する変数名」「インスペクタ上の表示」「型名」「初期値」の順
-
_MainTex("Texture", 2D) = "white"{}
→公開する変数名が_MaxTex、インスペクタ上の表示がTexture、型名が2D、初期値が"white"になる
-
standard shaderを使えば、インスペクタにドラッグ&ドロップするだけでテクスチャが表示される
-
今回のプログラムを少し変更するだで、standard shaderでは実現できないような表現が可能になる

ステンドグラスのシェーダ
- テクスチャの黒い部分は不透明、カラーの部分は半透明
- テクスチャの色は、tex2Dで取得
- tex2Dメソッドで得られる色はRGBAカラー情報
→黒色に近いかどうか判定は少し面倒
→画像をグレースケールに変換して「明るい色か暗い色か」で判断する - グレースケールに変換することで、RGBAという4次元の値から、明度という1次元の値で扱える
- RGBの情報からグレースケール変更する式(簡略化)
grasyscale = 0.3 * R + 0.6 * G + 0.1 * B
- grayscaleの値が黒色付近(0.2以下)であれば透明度を1に、garyscale > 0.2であれば透明度を0.7にすれば良い
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = c.ragb;
o.Alpha = (c.r * 0.3 + c.g * 0.6 + c.b * 0.1 < 0.2) ? 1 : 0.7;
}
- surfメソッドの中で、tex2Dメソッドを使ってテクスチャの色情報を取得し、グレースケールに変更

uvスクロールで水面を動かす
- テクスチャの左下が(u, v) = (0, 0)、右上が(u, v) = (1, 1)
- テクスチャをスクロールするためには、フレームごとにuv座標にオフセットを足していく
- サーフェイスシェーダは前のフレームの状態を覚えておくことはできない
- わかるのはテククチャのどこを使うかだけ→前フレームのuv座標にオフセットを足していくことはできない
- そこでフレームごとにuv座標にオフセットを足すかわりに、uv座標にスクロール速度x時間を足す方法を使う
- スクロール速度x時間=移動距離
Shader "Custom/sample7"
{
Properties
{
_MainTex ("Water Texture", 2D) = "white" {}//インスペクタから水面テクスチャをセットできるよう
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;//サーフェイスシェーダが処理するテクスチャ座標を受け取るため
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed2 uv = IN.uv_MainTex;
uv.x += 0.1 * _Time;//スクロール速度を変えている
uv.y += 0.2 * _Time;//_Time変数はUnityのシェーダにデフォルトで用意されている
o.Albedo = tex2D(_MainTex, uv);
}
ENDCG
}
FallBack "Diffuse"
}

テクスチャをブレンドして自然な地形を表示する
- マスク画像が白に近い場合はテクスチャ1を強め、マスク画像が黒色に近い場合はテクチャ2を強める
- 式は
color = Texture1 * p + Texture2 * (1 - p)
- pはマスク画像の明度(0.0 ~ 1.0)
- p == 0のときテクスチャ1の画像、p == 1のときテクスチャ2の画像
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c1 = tex2D(_MainTex, IN.uv_MainTex);//テクスチャ1の色を代入
fixed4 c2 = tex2D(_SubTex, IN.uv_MainTex);//テクスチャ2の色を代入
fixed4 p = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = lerp(c1, c2, p);
}
- LerpとSlerp
- 2点間をなめらかに移動させるための関数
- Lerpが線形補間
- Slerpが球面線形補間

円やリングをかっこよく動かす方法
- ワールド座標を受け取れるように、Input構造体にはworldPosを宣言
void surf (Input IN, inout SurfaceOutputStandard o)
{
float dist = distance(fixed3(0, 0, 0), IN.worldPos);//2点間の距離を求めるメソッド
float radius = 2;
if(radius < dist)//ワールド座標と原点の距離がradiusより大きいなら紫
{
o.Albedo = fixed4(110/255.0, 87/255.0, 139/255.0, 1);
}
else//ワールド座標と原点の距離がradius以内なら白
{
o.Albedo = fixed4(1,1,1,1);
}
}
- 円の外周だけ描くように修正
void surf Input IN, inout SurfaceOutputStandard o)
{
float dist = distance(fixed3(0, 0, 0), IN.worldPos);
float radius = 2;
if(radius < dist && dist < radius + 0.2)//中心から一定距離の部分のみが白色
{
o.Albedo = fixed4(1,1,1,1);
}
else
{
o.Albedo = fixed4(110/255.0, 87/255.0, 139/255.0, 1);
}
}
radius + 0.2の場合
radius + 1の場合
- 複数のリングを描く
- 円の中心から距離を横軸にとる
- sin波の山の頂点付近のみを白、それ以外の部分を紫色にしてい
void surf (Input IN, inout SurfaceOutputStandard o)
{
float dist = distance(fixed3(0, 0, 0), IN.worldPos);
float val = abs(sin(dist * 3.0));
if(val > 0.98)
{
o.Albedo = fixed4(1,1,1,1);
}
else
{
o.Albedo = fixed4(110/255.0, 87/255.0, 139/255.0, 1);
}
}
- リングを外側に向かって動かすアニメーションを作る
- 外側に向かって波を動かすためには、現在の座標があたかも遠ざかっているように見せる必要がある
- 現在処理している座標から_Timeを引くことで、あたかも現在処理している座標が、時間とともに外側へ移動しているようび見せかけている
- 逆に_Timeを足すことで、内側び向かって波が動いているように見せかけられる

シェーダで作るノイズ5種盛り
- Unityはノイズを作るメソッドが用意されていないので、プログラムで作る必要あり
ランダムノイズ
- 時間や場所に依存せずにランダムに値を決めるノイズ
- randomメソッドは引数にuv座標をとって、座標からランダムな値を生成する
float random (fixed2 p)
{
return frac(sin(dot(p, fixed2(12.9898,78.233))) * 43758.5453);
}
-
frac
とは、shaderにおいて少数部分を取得するメソッド
ブロックノイズ
- ランダムノイズに比べると粒度が荒いノイズ
- ランダムノイズからブロックごとに代表的な1点を取り出してベタ塗りしている
- floorメソッドを使用→小数値の整数部分を返すメソッド
16x16ブロックノイズのとき
バリューノイズ
- ブロックノイズはブロック単位で色が分かれていた
- ブロックノイズを平滑化したノイズ
- ブロックノイズで使った四隅の色からブロック内の色を補間して決める
- 値を4隅の値から補間するためには、補間したい座標がブロック内の左下からどれくらい離れているか知る必要がある
- オフセット値をfracメソッドで求める
パーリンノイズ
- バリューノイスをさらになめらかにしたもの
- 炎や雲などにみられる自然な変化のノイズを表現するのによく使われる
- 四隅の色をなだらかに変化
- 格子点に設定されたランダムなベクトルと、ブロック内部の点から格子点に向かうベクトルの内積をとって、それを色情報として扱う
- 内部の点から格子点へのベクトルの内積をとっているので、格子点上では必ず黒色になっている
fBmノイズ
- 更に綺麗なノイズは、さまざまな解像度のノイズをずらしながら重畳させる
- これをfBm(Fractional Brownian Motion)と呼ぶ
- 地形や雲など幅広い表現に使われる
- いくつの階層のノイズを合成するかを表す値を「オクターブ」
- それぞれのノイズをどのような割合で合成するかを「パーシステンス」と呼ぶ

粘性のある液体をシェーダで作る
-
- GLSLというシェーダ言語で書かれている
-
Unityは、HLSLと呼ばれる言語で書かれている
-
移植場所はフラグメントシェーダ内(fragメソッド)
-
修正箇所
- fract → frac
- 小数点以下を計算するfractとがないので、frac
- vec → half
- ベクトル型使えないので、half型
- mix → lerp
- GLSLのmix関数がないので、lerp関数
- fract → frac

テクスチャの画面を描画する方法
-
Planeオブジェクトにテクスチャを貼り付けると、裏面は透明になる
- カリングのため
- 見えない場所は描画しない設定
-
Cull off
でカリングを切る処理 -
テクスチャの透明部分にも対応する
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha

トゥーンシェーダを自作
-
Rampテクスチャ
- 色をサンプリングするためのの専用テクスチャ
- 表面の暗い色はこれといった感じに取得するようになる
-
ライトの方向とオブジェクトの法線の内積
- ライトと法線方向が一致する場合は値が大きくなり、ライトと法線が垂直に近づくにつれて値が小さくなる
- 0 ~ 1にスケーリングしてRampテクスチャのu座標とする
-
ライティング用のメソッド名はLightingから始める必要がある
-
ライティングのメソッドをフックしたことをUnityに伝えるために、#pragmaの行にライティングメソッド名を追加する
LightingToonRamp
メソッドなので、Lighting
以外の部分ToonRamp
#pragma surface surf ToonRamp
-
surfシェーダでライティングの工程をフックした場合
- surf出力には
SurfaceOutputStandard型
を使うことができない - ここでは
SurfaceOutput型
に書き換え -
SurfaceOutput型
にはEmmision
やSmoothness
は定義されていないので、行を削除する]
- surf出力には

頂点カラーを表示するシェーダ
-
頂点カラー
- 頂点ごとに設定された色情報のこと
- 3Dモデルの頂点には、座標や法線ベクトル以外にも色情報を設定できる
-
頂点情報を扱うためには、頂点シェーダ(vertex shader)をフックし、頂点カラーを取り出す必要がある
-
取り出した頂点カラーシェーダからsurfシェーダに渡して表示
-
頂点シェーダ(vertex shader)をフック
-
#pragma
の行にvertex:頂点シェーダのメソッド名
で書く(Unityに頂点シェーダをカスタムで作成することを伝える)
-
-
頂点シェーダのメソッド
- 引数には頂点シェーダへの入力として、
appdata_full型
- 頂点シェーダからの出力に
Input型
- 引数には頂点シェーダへの入力として、
-
UNITY_INITIALIZE_OUTPUTで主力の値を初期化
-
appdata_full型
のcolor変数から頂点カラーの情報を取り出す -
Input型
のvertColor変数に値を詰める

シェーダで旗や水面をなびかせる
- 頂点を波の形に移動される(上下運動)
- 隣とは少しタイミングをずらす
- 頂点の座標データを操作する場合は、独自のvertexシェーダをカスタムで作る必要がある
#pragma surface surf Lambert vertex:vert
-
vertex:vert
がカスタムしたバーテックスシェーダ(vert)を使うことを宣言になる
float amp = 0.5*sin(_Time*100 + v.vertex.x * 100);
- 隣り合う頂点とは少し周期をずらすために、sin関数の引数に頂点のx座標を足している
- 頂点シェーダを使うことでアニメーションを作ることができる

Dissolve(溶けるような)シェーダをつくる
-
_DissolveTex ("DissolveTex (RGB)", 2D) = "white" {}
- Dissolve用のテクスチャを追加(インスペクタに表示される)
-
_Threshold ("Threshold", Range(0,1)) = 0.0
- 「消す・消さない」のしきい値を決めるための_Threshold変数追加
-
Gray = R * 0.3 + G * 0.6 + B * 0.1;
- グレースケースの変換式
- surfメソッド内ではDisolveテクスチャから値を取り出し、グレースケースに変換
- グレースケールに変換後、明度がしきい値(_Threshold)以下のものについては破棄(discard)する

シェーダを使って世界に雪を降らせよう
- オブジェクトの法線と雪が降ってくるベクトル(大体上向き)の内積を取る
- 内積の値が1に近ければ面が上に向いているという判断してテクスチャの色を白色にする