Edited at

Unity、アウトラインを出してみる【Shader : 3】


はじめに

この記事は連載記事です。

記事を読むにあたってレンダリングパイプラインでどのような処理を行っているのか?を知っている事は前提になります。

非常に分かりやすい資料がありますので、レンダリングパイプラインを知らない方は読む前にご覧ください。

Unity道場 2019.2 シェーダを書けるプログラマになろう #1 シェーダを理解しよう

Unity道場 2019.2 シェーダを書けるプログラマになろう #2 GPUの神秘

アーカイブ

Unity、Shaderことはじめ【Shader : 0】

Unity、UnlitShader/textureのコードを追いかける【Shader : 1】

Unity、UVScroll(UVの処理)をやってみる【Shader : 2】


導入

ちょうど最近、技術書典がありレイマーチングの季節になりました。

なので、「ジュリア集合とかマンデルブロ集合を書いていきます」といいたい所ですが、この記事書いてる人は「フラクタル?アニメじゃないの?」という感じなので、地に足ついて基本的なShaderの書き方をまとめてゆきます。


アウトライン概要

感覚的には「1オブジェクトあたり1回のレンダリング」ですが、実際の所複数レンダリングできるみたいです。

ポピュラーな例?として、トゥーンレンダリングの輪郭線なんかがそれに該当します。

トゥーンレンダリングの輪郭線の前に…オブジェクトの裏面を描画するケースについて話します。

おそらく、メジャーな例ではSkydomeが該当すると思います。

skydome.png

球体のオブジェクトがSkydomeで、球体の中が表・球体の外が裏側となっています。

この状態でSkydomeの中にカメラを入れて描画すると、球体は空のように機能します。

プラネタリウムも同じ仕組みというと分かりやすそう?

sky.gif

そして、Skydomeの中にもう1つ球体を置くとこのようになります。

Skydome側は表からは遮るものがないので、中のオブジェクトがレンダリングできます。

さらに、Skydomeのテクスチャを黒にしてみると…

rinkaku.png

球体に黒い輪郭がついてるように見えます。

これが(リアルタイムレンダリングで)トゥーンシェーダーで輪郭線をレンダリングする仕組みです。


プリレンダの場合、ちゃんと輪郭線がどこなのか?のシュミレーションを行っている。複雑な形状だと輪郭を誤検知する事もある。


今回の場合、Skydomeと中の球体を2つ用意したわけですが、1つのオブジェクトでも「頂点を外側に拡大→裏面をレンダリングする→テクスチャの色ではなく輪郭線にしたい色で塗りつぶす」そして、そのあとに通常通り表面をレンダリングするという事をShader側でやってやれば、輪郭線が表示できるわけです。

というわけで「輪郭線のあるトゥーンシェーダーは2回分(2パスに分けて)レンダリングしている」というわけです。


複数Passの書き方


MultiPath.shader

Shader "MultiPath"

{
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

// 1回目のレンダリング
Pass
{
//裏面
Cull Front

CGPROGRAM
//(略)
ENDCG
}

// 2回目のレンダリング
Pass
{
//表面
Cull Back

CGPROGRAM
// (略)
ENDCG
}

// 3回目、4回目以降は下に Pass{} を増やしてゆく
}
}


単純に Pass{} でくくって CGPROGRAMENDCG の間に追加のレンダリング内容を書けば、複数回のパスでレンダリングされます。レンダリングは上から順に行われます。

上記のサンプルはよくあるパターンを想定して、1回目のパスでは裏面を。2回目のパスで表面をレンダリングしてますが、2回表面のパスを描く事もできますし、3回目以降も下に続けてゆけば書けます。

(その分、処理が増えますが)

Cull BackCull Front はカメラの向きを基準に表を描画するか、裏を描画するか?のプロパティです。

表面に関しては、省略しても問題ないですが、2パスある場合はあえて明示しておくと分かりやすいので書きました。

ShaderLab :Culling と Depth Testing


UnlitShaderにアウトラインの表示を追加したSample


UnlitOutLine.shader

Shader "Unlit/UnlitOutLine"

{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_LineColor("Line Color", Color) = (1, 1, 1, 1)
_LineWidth("Line Width", Range(0.001, 0.2)) = 0.01
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

//Line
Pass
{
//Back Side
Cull Front

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float _LineWidth;
fixed4 _LineColor;

v2f vert (appdata v)
{
//offset方向の計算
float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
float2 offset = TransformViewToProjection(normal.xy);

//OutLineとしてレンダリングされるポリゴン
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex.xy = o.vertex.xy + offset * _LineWidth;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 col = _LineColor;
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}

//Unlit
Pass
{
//Surface
Cull Back

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

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

sampler2D _MainTex;
float4 _MainTex_ST;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}

}
}



解説


UnlitOutLine.shader


struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float _LineWidth;
fixed4 _LineColor;

v2f vert (appdata v)
{
//offset方向の計算
float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
float2 offset = TransformViewToProjection(normal.xy);

//OutLineとしてレンダリングされるポリゴン
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex.xy = o.vertex.xy + offset * _LineWidth;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}


アウトラインのレンダリングでポイントとなるのは、アウトラインを描画すべき裏面の大きさを計算しているバーテクスシェーダーです。

そこさえわかれば、線の色に工夫しなくてよいなら、ピクセルシェーダーはプロパティで指定されたカラーを渡すだけなので簡単です。

まずここの行。

float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));

v.normal はモデル原点を基準とした法線方向なので、これをカメラから見た時の法線方向に変換する必要があります。

そこで、(float3x3)UNITY_MATRIX_IT_MV (モデルビュー行列の逆行列の転置行列) を mul()関数で掛けると、カメラから見た法線ベクトルが求まるそうです。

※mul()は掛け算、HLSLでは行列の掛け算で演算子を使わないらしい。

※UNITY_MATRIX_IT_MVを掛けると何故、カメラから見た時の状態になるのか?はそもそも行列とベクトルを理解する必要があります(なんもわからん…)

float2 offset = TransformViewToProjection(normal.xy);

次の行で、3次元ベクトルの法線情報を、2次元ベクトルに変換しています。

TransformViewToProjection() の関数がよしなにやってくれるそうですが、この関数はUnityCG.cgincのライブラリに定義されています。

UnityEditorにコンパイルされてるので、何もせずとも使えますが、ライブラリの中身を見たければダウンロードする必要があります。

Unity ダウンロード アーカイブ

※ドロップダウンから「ビルトインシェーダー」を選択してダウンロードし、その中にある「CGIncludes」の .cginc を読むと、関数の中身が分かる。

ビルトイン シェーダ includeファイル

v2f o;

o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex.xy = o.vertex.xy + offset * _LineWidth;

そして、最後はいつもどおりの処理で、オブジェクト座標をクリップ座標に変換。

クリップ座標に「offsetで求めたベクトル(外側の方向)×ラインの太さ(ベクトルの長さ)」

を足してやることで、実際のモデルよりも大きくなった場合のメッシュが求まります。

あとは、フラグメントシェーダーに渡してやって、そこのメッシュを塗った後…

2パス目でUnlitShaderをレンダリングするとアウトラインのあるUnlitShaderになります。

Outline.gif


おわり

行列ちゃんとやろう…Unityなんもわからん…


まだ