シェーダめっちゃ楽しいですねw
最近はシェーダの勉強がメインになっていますが、ごにょごにょしてたらなんとなくそれっぽくなったので記事にしてみました。
処理の流れ
大まかな処理の流れは、
- 通常のオブジェクトをレンダリング
- 2Pass目で、法線方向に膨らませたオブジェクトを視点を考慮に透明度を計算
という感じです。(2Passってこういう使い方であってるんかな?)
コード
まずはコード。
Shader "Custom/GlowShader" {
Properties {
_Color ("Object's Color", Color) = (0, 1, 0, 1)
_GlowColor ("Glow's Color", Color) = (1, 0, 0, 0)
_Strength ("Glow Strength", Range(5.0, 1.0)) = 2.0
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
uniform float4 _Color;
float4 vert(float4 vertexPos : POSITION) : SV_POSITION {
return mul(UNITY_MATRIX_MVP, vertexPos);
}
float4 frag(void) : COLOR {
return _Color;
}
ENDCG
}
Pass {
Tags {
"LightMode" = "ForwardBase"
"Queue" = "Transparent"
"RenderType" = "Transparent"
}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
uniform float4 _GlowColor;
uniform float _Strength;
struct vInput {
float4 vertex : POSITION;
float4 normal : NORMAL;
};
struct v2f {
float4 position : SV_POSITION;
float3 normalDirection;
float3 viewDirection;
};
v2f vert(vInput i) {
v2f o;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
float3 normalDirection = normalize(mul(i.normal, modelMatrixInverse)).xyz;
float3 viewDirection = normalize(_WorldSpaceCameraPos - mul(modelMatrix, i.vertex).xyz);
float4 pos = i.vertex + (i.normal * 0.3);
o.position = mul(UNITY_MATRIX_MVP, pos);
o.normalDirection = normalDirection;
o.viewDirection = viewDirection;
return o;
}
float4 frag(v2f i) : COLOR {
float3 normalDirection = normalize(i.normalDirection);
float3 viewDirection = normalize(i.viewDirection);
float strength = abs(dot(viewDirection, normalDirection));
float opacity = pow(strength, _Strength);
return float4(_GlowColor.xyz, opacity);
}
ENDCG
}
}
}
解説
Properties
プロパティは、オブジェクト自体の色と、Glowの色、あとはGlowの強さを取ります。
1Pass目
1Pass目はなにもむずかしいことはしていません。
入ってきた頂点位置をそのままMVP行列を掛けて出力、色についてはプロパティで受け取ったものをそのまま出力しています。
(ライティングに関しては今回は対応してません。そもそもGlow適用するとライティングあんまし意味ないし)
2Pass目
今回のサンプルで一番大事な点がこちら。若干コードも長めです。
Tagsにいくつか指定を追加していますが、ここは色々参考にした記事のをそのまま持ってきたのと、コメントアウトしても見栄えが変わらなかったので、もしかしたら必要ないかもしれません。
ディレクティブ
それよりも重要な点は以下の2点です。
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Blendディレクティブは、ブレンディングの仕方を指示します。
上記は一般的なブレンディングの設定です。
左がSrcFactor、右がDstFactorです。
つまり、左が現在計算中のピクセルの色、右がすでにバッファに描かれているピクセルの色をそれぞれどう扱うか、という設定になります。
サンプルで言えば、計算中の色はそのままのアルファ値を、バッファに描かれている色は1
からSrcAlphaを引いた値を使う、という計算になります。まさにブレンド方法の設定ですね。
BlendについてはUnityのドキュメントに詳しく載っています。
頂点シェーダ
頂点シェーダでやっていることは以下の3つです。
- ワールド座標空間での法線の算出と正規化
- 各頂点から視点(カメラ)へのベクトルの計算と正規化
- 各頂点を、法線方向にほんの少しだけ含まらせる処理
v2f vert(vInput i) {
v2f o;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
float3 normalDirection = normalize(mul(i.normal, modelMatrixInverse)).xyz;
float3 viewDirection = normalize(_WorldSpaceCameraPos - mul(modelMatrix, i.vertex).xyz);
float4 pos = i.vertex + (i.normal * 0.3);
o.position = mul(UNITY_MATRIX_MVP, pos);
o.normalDirection = normalDirection;
o.viewDirection = viewDirection;
return o;
}
viewDirection
はベクトルの引き算なので大してむずかしいことはしていません。
問題は法線の計算部分です。
これは、法線に関しては普通の変換行列を掛けるだけだとうまく算出できません。
法線の場合は、変換行列の「逆転置行列」を掛ける必要があります。
(このあたりの話はこちらの記事を参照)
そこで、まず逆行列を準備します。
そしてベクトルと行列の掛ける順番を逆にすることで、実質の転置行列を掛けた状態を作り出している、というわけです。
あとは求めた値をフラグメントシェーダに渡してやります。
構造体の定義
次に注目する点は構造体の定義です。
normalDirection
とviewDirection
のふたつをフラグメントシェーダに出力しています。
このふたつは、法線の方向と、頂点から視点(カメラ)の方向へのベクトルとなります。
struct v2f {
float4 position : SV_POSITION;
float3 normalDirection;
float3 viewDirection;
};
これは、以前に書いた「Unityで半透明オブジェクトのシルエットを際立たせる方法」で解説しているものを応用したものになっています。
それをCg/HLSL用に書きなおしたものです。
ざっくりとなにをしているかを書くと、頂点から視点へのベクトルと頂点の法線との内積を取っています。
つまり、90度に近くなるほど値が0
になることを利用している、というわけです。(要は、淵に近づくにつれて徐々に色が薄くなっていく)
内積の計算
実際の計算を行っているのは以下です。
float4 frag(v2f i) : COLOR {
float3 normalDirection = normalize(i.normalDirection);
float3 viewDirection = normalize(i.viewDirection);
float strength = abs(dot(viewDirection, normalDirection));
float opacity = pow(strength, _Strength);
return float4(_GlowColor.xyz, opacity);
}
内積の計算を終えたら、あとはインスペクタから渡された色と、内積によって求めた値をアルファ値として設定して最終的な色として出力しています。
ちなみに_Strength
は、見て分かる通り内積によって得られた結果を何乗するかの値になっています。
これはSpecularなどでも利用されますが、要は数値が小さいものほど急速に減少していくようにしているわけです。
(1 * 1
は1
のままだが、0.1 * 0.1
は0.01
と小さくなる)