はじめに
今回作成するのは消えたり、現れたりするディザ抜き半透明shaderです。shader programなるものを初めて触ってみる、初めてshader programをUnityでアバターに適用してみる、という体験を主体にしています。説明が足りない点もあると思いますが、難しいところは読み流して楽しんでいただければ幸いです。
本shaderプログラムはlilxyzwさん OpenLItを元に、となりのJohnさんのご教授、ご支援のもと作成しました。となりのJohnさんには記事校閲もしていただきました。謝意をここに。
本記事では、shaderプログラム初めての人が感覚を掴んで貰う目的で作りました。触って楽しんでみてください。
エディタはVScode推奨です。こちらからダウンロード、インストールしてください。
BOOTH版庭子を使いますのでダウンロードしてください。import方法、VRChatへのアップロード方法はこちらの記事を参考にしてください。
VCCでプロジェクト作成
まずはVCCで「庭子shader入門」というプロジェクトを作りましょう。
下記4個のPackageは入れておくといいでしょう。liltoonは必須です。
Unity起動とBOOTH版庭子配置
Open ProjectでUnityを立ち上げます。
BOOTH版 庭子をimport、配置してください(参考)。太陽(ディレクショナルライト)の色、向きはお好みで調整してください。
OpenLit
OpenLitはCC0で配布されているトゥーン・シェーダーです。ひな形として使用します。
OpenLitはこちらにあります。「Code」から「Donload ZIP」でOpenLit-main.zipファイルを入手してください。
OpenList-main.zipファイル解凍して、OpenLit-main → Assets → OpenLit とフォルダを手繰ってください。metaファイルはおいておいて、core.hlsl とOpenToonLit.shader ファイルが見つかります。この二つを Assets→niwako-v01-decim→Materials にDrag&Dropしてください。
shader 変更
ではここからshaderを変更していきます。マテリアル collared-shirt-dither-s (庭子アバターでは動かないstaticなDither抜き半透明用マテリアル)を実験対象に使います。シャツのデフォルトマテリアルをcollared-shirt-dither-s にするために、Materialの下のcollared-shirt-dithier-s を、hierarchyのcollared-shirtへDrag&Dropしてください。
次にさきほどのMaterialで、OpenToonLitをcollared-shirt-ditherへDrag&Dropしてください。これだけでトゥーン調シャツ(ディザ抜き半透明では無い)になったはずです。UnityではshaderプログラムはAssets以下に置かれるだけで自動でコンパイルされて即時に適用されます。VScodeでファイル保存(Ctrl-S)した後も、UnityウィンドウをActiveにすると自動コンパイルされるようです。
では、OpenToonLit の中をみてみましょう。Matrialsの下のOpenToonLitをダブルクリックしてください。VScodeがインストールされていればVScodeが起動するはずです。もしVisual Studio 2019が起動した場合、Windowsで「.shader」の拡張子関連づけを変更してVScodeにしてください。
下記のように表示されます。
大まかに下記二つに分けられます。
- FowardBase: 一番あかるいディレクションライトなどのメイン光源処理、ディザ抜きなどもここで行う
- FowardAdd: ポイントライトなどFowardBase以外の光源処理
さらにそれぞれの中で vert と frag があります。これは下記 # pragma でvertex シェーダーとして vert が、fragmentシェーダーとしてfragが指定された物です。
- vert: 頂点単位の処理。座標変換、頂点単位光源処理(pixelへは補間して渡される)
- frag: ピクセル単位の処理。ピクセル単位で光源処理するのがもっとも綺麗だが計算コストが大きい
FowardRenderingの解説はLIGHT11さんブログやUnityマニュアルもわかりやすいです。
ディザ抜き半透明のサンプルシェーダーここにおきます。ダウンロードして解凍してください。
二つのファイルをMaterialsへDrag&Dropしてください。
そのうちのRealDithering-test1の方をcollared-shirt-dither-sへDrag&Dropしてください。
collared-shirt-dither-sをクリックしてInspectoarをみて、AnimationSpeedが0ならば2に変更してください。これでトゥーン調のディザ抜き半透明が適用されました。
ではファイルの中身をみてみましょう。RealDithering-test1をダブルクリックしてVScodeで開いてください。
そしてVScodeのRealDithering-test1の表示上で Ctrl-Shift-P から 「File: Compare Active File With …」を実行してください。比較相手にはOpenToonLitを選びます。
左側の赤い方が RealDithering-test1、右側の緑の方が OpenToonLit です。
(1)
コメント部は適切に変更。
1 |
Shader "RealDithering-test2" // for Dither: Change shader name |
と記述することでマテリアルに適用して、Inspectorで表示したときにShader名としてこの名前が表示される。
また
1 |
_ANIMATION_SPEED("AnimationSpeed", Range(0.0, 5.0)) = 2.0 // for Dither |
を記述することでマテリアルのInspectorでAnimationSpeedが変更できるパラメータとして表示されます。ここでは0.0から5.0の範囲で、デフォルト値は2.0を設定しています。
(2)
1 |
Cull Off //ポリゴンの裏表両方描画 // for Dither |
を追加することでポリゴンの裏表両面描画を設定しています。
(3)
1 |
float _ANIMATION_SPEED; // for Dither |
上記のAnimationSpeedの変数をfloatとして定義しています。
1 2 3 4 |
#include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #include "core.hlsl" |
これはOpenLit側2カ所ににある(A)(B)インクルードファイル定義を1カ所にまとめました。この後のcalcPattenでも使うためこの場所においています。SubShader内、Pass{}の外側で、HLSLINCLUDE-ENDHLSLでinclude記述を行うと、ForwardBaseやForwardAddなど各Passを通る際に共通に反映されます。
エラーが発生しました。後でもう一度やり直してください。 |
4×4のBayerマトリックスです。ディザを良い感じになめらかに散らします。
要素数字が0~63の場合は16で割ります。比べたのですが、1~64、17で割る方がディザ境界が目立たないように見えました。
8×8のBayerマトリックスを使いたい場合には下記と置き換えてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#define BAYER_D 8 #define BAYER_N 65 static const int pattern[BAYER_D][BAYER_D] = { // ディザパターン行列 // BayerMatrix: 8x8, 1...64 の場合は 65 で割って使う。 { 1, 33, 9, 41, 3, 35, 11, 43}, {49, 17, 57, 25, 51, 19, 59, 27}, {13, 45, 5, 37, 15, 47, 7, 39}, {61, 29, 53, 21, 63, 31, 55, 23}, { 4, 36, 12, 44, 2, 34, 10, 42}, {52, 20, 60, 28, 50, 18, 58, 26}, {16, 48, 8, 40, 14, 46, 6, 38}, {64, 32, 56, 24, 62, 30, 54, 22} }; |
shader programを変更した場合、VScode上でセーブ(Ctrl-s)すると、Unity上で即座にshaderコンパイルされ見た目に反映されます。便利です。紫色のテクスチャ・マテリアル表示になった場合はエラーが起きています。マテリアルのinspectorにエラー内容が表示されているはずです。よく見直してみてください。
次に実際にディザ抜き半透明を行うcalcPatternの定義です。
1 2 3 4 5 6 7 8 9 10 11 12 |
float calcPattern(float2 uv, float4 screenPos) { float2 viewportPos = screenPos.xy / screenPos.w; float2 my_screenPos = viewportPos * _ScreenParams.xy; // _Time: ステージのロードからの時間 (t/20, t, t*2, t*3)。シェーダー内アニメーション用 float rad = uv.y + _ANIMATION_SPEED*_Time.y; // sinの2乗で0.0~+1.0範囲。 int isA = (_ANIMATION_SPEED != 0.0); // アニメーションか?フラグ。if文は使わない。 float clip = sin(rad)*sin(rad)*isA + 0.5*(1-isA); // if isA : sin()^2 else 0.5 int2 idx = int2(fmod(my_screenPos, 4.0)); return 1.0/BAYER_N *(float)pattern[idx.y][idx.x] - clip; // return 値はclip()関数に渡す。clip(): 引数が0 未満の場合、現在のピクセルを破棄。 } |
_TimeはUnity予約後で単位はSceneです。
collared-shirtのテクスチャUV座標のy(V)によって消えたり、現れたりするように変数radをつかいます。そのため rad = uv.y としています。さらに_ANIMATION_SPEEDに_Time.yをかけたものを加算(rad = uv.y + _ANIMATION_SPEED*_Time.y)することで、時間経過とともにUV.y座標をシフトし、時間経過とともに消えたり現れたりするshaderアニメーションを実現しています。
shader programでは「 if 文を可能な限り使わないように」との方針があります。if分岐のどちらを通ったかで処理時間が異なってしまうのがpixel shaderでは良くないようです。
なので下記のようなトリッキーな記述をしています。
1 2 |
int isA = (_ANIMATION_SPEED != 0.0); // アニメーションか?フラグ。if文は使わない。 float clip = sin(rad)*sin(rad)*isA + 0.5*(1-isA); // if isA : sin()^2 else 0.5 |
ここでは isA フラグをもうけでアニメーション(_ANIMATION_SPEEDがゼロ以外の値)か、アニメーションでは無い(_ANIMATION_SPEEDがゼロ)かを判定して数式で記述しています。
本当は下記のように記述したいところです。
1 2 3 4 5 6 |
if (isA == 0) { clip = 0.5; } else { clip = sin(rad)*sin(rad); } |
このshader記述により、時間Time.yによって増えていくradをパラメータとして、sin(rad)の2乗(0.0~1.0範囲、下記グラフ参照)でUV座標の上から下に消えたり出たりするアニメーションとなります。
対応するテクスチャUVは下記の通りです。シャツの半袖も外側から消えていくように、UV座標上の向きを調整して配置しています。赤矢印が消えたり出たりする方向を示しています。
sin(rad)の2乗は は0~1.0の値をとります。Bayerマトリックスの値をBAYER_Nで割ったものは0~1.0未満の範囲になります。
1 |
return 1.0/BAYER_N *(float)pattern[idx.y][idx.x] - clip; |
それからclip(0.0~1.0範囲)を引くことで、-1.0~1.0範囲となります。後のclip()関数で0未満が非表示となり、結果として上から下に消えて模様が移動するようにみえるディザ抜き半透明アニメーションが実現されます。
(4)
スクリーンポジションでディザ抜きするため、v2f 構造体に screenPos を追加しておきます。
1 |
float4 screenPos : TEXCOORD11; // for Dither |
(5)
スクリーン座標を計算しておきます。座標計算はvertex shader(ここではvert())で、ピクセル計算はfragment shader(ここではfrag())で、というルールがありますのでここに記述します。
vert()で頂点毎に計算されたスクリーン座標などの値は、ポリゴン内の各ピクセル(fragment)へ数値補完されて渡されます。
1 2 3 |
// スクリーンポジションのピクセルでディザ抜きするためにスクリーン座標を計算 // for Dither // 座標変換計算なので vertex shader 側で実行 // for Dither o.screenPos = ComputeScreenPos(o.pos); |
(6)
v2f()の o.screenPos は frag() ではi.screenPos として渡されてきます。i.uv座標とi.screenPosを入力パラメータとして、calcPattern() を呼び出し、clip()関数でディザ抜きします。
1 2 |
//ディザマスク処理::clip関数は、引数が「0」より小さい場合は描画しない。「0」以上で描画 // for Dither clip(calcPattern(i.uv, i.screenPos)); |
shader-test1.shaderでの描画
このshader-test1.shaderを適用してみましょう。ToolsからGesture Manager Emulatorをクリックします。
Hierarchyに配置されたGestureManegerを選択して、Inspectorの「Enter Play-Mode」を押してください。画面上部の三角マークPlayボタンでも同じです。
Expression メニュー Emulatorが起動しますので、右下のEmulatorからExpressionを選択してください。
メニュー内のDither staticに今回のトゥーン調ディザ抜き半透明(shader-test1.shader)が入っていて、Dither animationに元々のリアル調ディザ抜き半透明(RealDithering-inc)が入っていれば成功です。このままアバターアップロードすることも可能です。
リアル調への変更
このままではトゥーン調です。庭子はリアル調なのでさらに変更します。
RealDithering-test2はtest1にリアル調シェーダーいれたものです。下記はtest1(右側緑)とtest2(左側赤)の比較。
リアル調(1)
shader名を「RealDithering-test2」にします。
_SHADOW_RATEという陰の強さをしめすパラメータを導入します。デフォルトは0.9です。
リアル調(2)
_SHADOW_RATEをfloatで宣言します。
リアル調(3)
FowardBase側のfrag()関数のライティング計算を変更します。
ランバート拡散反射の計算式を用います。
NdotLで物体の面に垂直な法線と光線との内積を計算し、陰影の色を決めます。それを_SHADOW_RATEに従って陰影の強さを調整し、lightingとします。
lerp(x, y, s)は線形補間関数で、x*(1-s) + y*s 式で線形補完します。ここではindirectLightとdirectLightの間を、NdotLと_SHADOW_RATEで計算したlightingによって線形補間しています。
リアル調(4)
FowardAdd側のfrag()も同様に変更します。こちらでは0.0とOPENLIT_LIGHT_COLORの間を、NdotLと_SHADOW_RATEで計算したLightingPatternによって線形補間しています。
最終結果
これで、先ほどと同様にRealDithering-test2をマテリアル collard-shirt-dither-s に適用(Drag & Drop)するととリアル調で表示されます。Dither animationとDither staticが同じリアル調ディザ抜き半透明になっているはずです。
配布版庭子ではReal-Dithering-inc.shaderをつかっています。これはインクルードファイルであるcore.hlslの記述を埋め込んだ物です。配布版でUnityPackage化するときcore.hlslをパッケージに入れてくれませんでした。インクルードファイルまで自動では依存関係チェックして貰えないようです。インクルードファイルを埋め込むことで対応しています。