はじめに
ゲームやアニメ等でよく見られる表現として、キャラクターのアウトラインがあります。
キャラクターやオブジェクトを強調させるためによく利用されますね。
今回はこのアウトラインの表現を、1つのシェーダーで2回レンダリングすることで実装していきます。
サーフェイスシェーダーと頂点・フラグメントシェーダーでは記述の方法がやや異なるため両方とも実装してみます。
基本的なシェーダーの記述に関しては、以下のURLを参考にしてください。
参考:サーフェイスシェーダー、頂点・フラグメントシェーダー入門(Qiita)
GitHub
今回使用するコードなどはこちらからダウンロードできます。
https://github.com/kakureusagi/UnityShaderTest/tree/master/Assets/Outline
アウトラインの作り方
作り方はとてもシンプルです。以下の2つを順番にレンダリングするだけです。
・少し大きくしたモデルを、アウトラインの色で描画
・通常の描画
これだけでアウトラインのついたモデルが表示できます。
頂点・フラグメントシェーダーで実装する場合
コード
Shader "Unlit/OutlineFlagmentShader" {
Properties {
_OutlineColor("Outline Color", Color) = (0, 0, 0, 0)
_OutlineWidth("Outline Width", float) = 0.1
_MainColor("Main Color", Color) = (1, 1, 1, 1)
}
SubShader {
Tags {
"Queue"="Geometry"
}
//1パス目.
Pass {
Cull Front
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
float4 _OutlineColor;
float _OutlineWidth;
float4 _MainColor;
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 vertex : SV_POSITION;
};
v2f vert (appdata v) {
float distance = UnityObjectToViewPos(v.vertex).z;
v.vertex.xyz += v.normal * -distance * _OutlineWidth;
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
return _OutlineColor;
}
ENDCG
}
//2パス目.
Pass {
Cull Back
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
float4 _MainColor;
struct appdata {
float4 vertex : POSITION;
};
struct v2f {
float4 vertex : SV_POSITION;
};
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
return _MainColor;
}
ENDCG
}
}
}
中身の説明
2回レンダリングするための記述
頂点・フラグメントシェーダーで2パスレンダリングを行うには、下記のようにPassブロックを2回記述します。
SubShader {
Pass {
//必要ならレンダリングステートの変更
//1回目のレンダリング。アウトラインを描画。
CGPROGRAM
~
ENDCG
}
Pass {
//必要ならレンダリングステートの変更
//2回目のレンダリング。通常の描画。
CGPROGRAM
~
ENDCG
}
}
参考:頂点・フラグメントシェーダーの例
参考:ShaderLab シンタックス(Unity)
モデルを大きくする
法線方向にモデルのローカル座標をずらせば、少しだけモデルが大きくなります。この処理を頂点シェーダー内で行います。
そのために頂点シェーダーで法線情報を使えるように宣言します。
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
以下のコードで法線方向にモデルを大きくすることができます。
v.vertex.xyz += v.normal * _OutlineWidth;
これでモデルを大きくすることが出来るのですが、大きくしているのはあくまでモデルのローカル座標ですので、モデルがカメラに近い時にはアウトラインも太く、カメラから遠いときにはアウトラインも細く表示されます。
アウトラインは対象のモデルを際立たせるために利用される場合が多いので、カメラから遠い場合でもしっかりアウトラインを描きたいところです。そこで今回は、カメラからの距離に依存せずに一定の幅が保たれるように、カメラからの距離に応じたスケールをかけることにします。
v2f vert (appdata v) {
float distance = -UnityObjectToViewPos(v.vertex).z;
v.vertex.xyz += v.normal * distance * _OutlineWidth;
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
まずはモデルをローカル座標からカメラ座標に変換し、カメラからの距離を計算します。
float distance = -UnityObjectToViewPos(v.vertex).z
UnityObjectToViewPos関数でカメラの前にモデルが移動します。ここで一つ注意点があります。通常、Unityは左手座標系(z軸は奥がプラス)なのですが、カメラ座標系にしたときには右手座標系(z軸の手前がプラス)が使われるようです。カメラからの距離が遠ければ遠いほどアウトラインの幅を大きくしたいので、ここではプラスの値が欲しいためマイナスをかけています。
その後、元々のモデルの頂点座標を法線方向に膨らませます。
v.vertex.xyz += v.normal * distance * _OutlineWidth;
これでモデルを大きくすることはできました。しかしこのままでは2Pass目の通常のモデルよりも手前に表示されてしまいます。そこで、ポリゴンの裏面だけ描画するようにレンダリングステートを指定します。レンダリングステートはPassの直下に指定します今回はCull Front
を指定しています。そして2パス目ではCull Back
を指定してデフォルトと同じようにモデルの前面だけ表示するように戻します。
後はフラグメントシェーダーでアウトラインの色を出力すれば完成です!
fixed4 frag (v2f i) : SV_Target {
return _OutlineColor;
}
参考:空間とプラットフォームの狭間で – Unityの座標変換にまつわるお話 –(ドリコム技術情報)
サーフェイスシェーダーで実装する場合
コード
Shader "Custom/OutlineSurfaceShader" {
Properties {
_OutlineColor("Outline Color", Color) = (0, 0, 0, 0)
_OutlineWidth("Outline Width", float) = 0.1
_MainColor("Main Color", Color) = (1, 1, 1, 1)
}
SubShader {
Tags {
"Queue"="Geometry"
}
//1パス目.
Cull Front
CGPROGRAM
#pragma surface surf Lambert vertex:vert
float4 _MainColor;
float4 _OutlineColor;
float _OutlineWidth;
struct Input {
float4 vertexColor : COLOR;
};
void vert(inout appdata_full v, out Input o) {
float distance = -UnityObjectToViewPos(v.vertex).z;
v.vertex.xyz += v.normal * distance * _OutlineWidth;
o.vertexColor = v.color;
}
void surf(Input IN, inout SurfaceOutput o) {
o.Albedo = _OutlineColor.rgb;
o.Emission = _OutlineColor.rgb;
}
ENDCG
//2パス目.
Cull Back
CGPROGRAM
#pragma surface surf Lambert
float4 _MainColor;
struct Input {
float4 vertexColor : COLOR;
};
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = _MainColor;
}
ENDCG
}
}
中身の説明
2回レンダリングするための記述
サーフェイスシェーダーで2回レンダリングを行うには下記のようにSubShaderブロックを記述します。
SubShader {
//必要ならレンダリングステートの変更
//1パス目.
CGPROGRAM
~
ENDCG
//必要ならレンダリングステートの変更
//2パス目.
CGPROGRAM
~
ENDCG
}
モデルを大きくする
基本的な考え方は頂点・フラグメントシェーダーでは同じです。
ただ、基本的なサーフェイスシェーダーでは頂点位置を変形できません。そこで「頂点モディファイア」関数を導入します。
#pragma surface surf Lambert vertex:vert
シェーダー関数を指定するところでvertex:vertと指定しています。頂点モディファイア関数としてvert関数を使うことを宣言しています。
頂点モディファイア関数は以下の構造をしています。
void vert(inout appdata_full v, out Input o) {}
頂点シェーダーでは入力の頂点のローカル座標をカメラのクリップ空間に変換していましたが、頂点モディファイアでは必要ありません。
appdata_fullはUnity組み込みの構造体で、位置、法線、接線、頂点カラー、テクスチャ座標×2を含んでいます。
頂点・フラグメントシェーダーと同様に、ここで頂点位置を変形します。
void vert(inout appdata_full v, out Input o) {
float distance = -UnityObjectToViewPos(v.vertex).z;
v.vertex.xyz += v.normal * distance * _OutlineWidth;
o.vertexColor = v.color;
}
これでモデルを大きくすることができました。
参考:頂点モディファイアのある法線押し出し(Unity)
参考:内蔵のシェーダー include ファイル(Unity)
色を決定する
上記でモデルを大きくすることはできましたが、一つ気になる点があります。それはアウトライン部分もライティングの影響を受けてしまうことです。ライティングの効果もカスタマイズすることで影響を調整することはできますが、ここでは簡単にEmissionにアウトラインの色を入れてしまいます。
void surf(Input IN, inout SurfaceOutput o) {
o.Albedo = _OutlineColor.rgb;
o.Emission = _OutlineColor.rgb;
}
完成シェーダーを確認する
完成です!
と言いたいところですが、今回実装した法線方向に膨らませる方法ではうまく表現できない場合があります。キューブを見ると分かる通り、ポリゴンの角度が鋭角な場合にはアウトラインに隙間が出来てしまいます。
課題はありますが、今回はここまでです。
おわりに
今回はサーフェイスシェーダー、頂点・フラグメントシェーダーそれぞれで2パスレンダリングする方法やサーフェイスシェーダーで頂点変形などを行う方法を学びました。
簡単な記述をするだけで、ゲームなどでよく見られる表現が出来上がっていくのはとても楽しいですね!
参考
こちらの記事でもアウトラインに関しての記述があります。
Unity のトゥーンシェーダについて調べてみた(凹みTips)