Theme / Language
UI Language UI言語
🔤
English
🎌
日本語
⚠️Each articles themselves will not be translated by this setting. Some of article has translation and some of them doesn't. You will notice if the article has its translation by its preamble!
⚠️記事自体は翻訳されません! 記事によって英語版があったりなかったりします。翻訳がある記事は文頭に記載があるよ!
Theme テーマ
😎
Wakey Wakey
オハヨー
🌚
Go dark
ダークモード
🦹‍♂️
I'm not a comic-book villain
私は漫画本の悪役とは違う
アイキャッチ背景

Unityのシェーダーグラフでベイヤーディザをやってみる

2019-11-09

blenderのコンポジットノードだと、スクリーン座標を取得するようなノードが無い(方法があれば是非教えていただきたく……)んですが、Unityのシェーダーグラフを触ってみたところ、そのままズバリ〈Screen Position〉というノードがある事を知りました。

これなら「ベイヤーディザ法による減色表示が実現できるのでは?」と頑張ってみたところ、なんとか形にはなったみたいなのでご紹介します。と言ってもあまり賢くなさそうな感じになっているので、もっとスマートな方法があれば是非教えて下さい🙇

Unity ShaderGraph Bayer-dithering Keycappie

キーキャッピーちゃんと喘息に欠かせないブロンの写真。右がオリジナル、左が今回作ったシェーダーグラフをマテリアルに指定したものです。シーンビューでピクセル等倍じゃないのでモアレが出てしまっていますが、概ね綺麗に減色できている気がします。

ベイヤーディザについて

先にベイヤーディザについて説明します。もう知ってるよという方は次の項まで飛ばして下さい。

ざっくり言うと、画像を減色する時のディザリング手法の一つです。Photoshopで言うところの“パターンディザ”。例えば白黒2値に減色する場合、ベイヤーフィルタと呼ばれるパターン(行列のようなもの)を画像に当てはめながら、ベイヤーフィルタの値をしきい値とし、画素の輝度がそれ以上なら白、以下なら黒にしていくと、不思議な網掛け模様を残しつつ、白黒2値だけでもそれっぽく見える画像に減色できます。これ、本当に不思議ですよね……

閾値と画素値の差は捨てちゃうので、差を周囲の画素に分散していく誤差拡散法ほどキレイにはなりませんが、独特のパターンからかもし出されるレトロな雰囲気がたまりません。みんな大好きゲームボーイのポケットカメラで撮影される画像にも使われています(断言しておいて申し訳ないのですが、本当のところは知りません。見えるパターンが同じなのでおそらくそうだと僕が思ってるだけです)

Gameboy PocketCamera

電池を入れたらバッチリ動く21年前のデジタルカメラ。数年ほっぽっただけで電池がすっからかんになって起動できなくなるリチウムイオン電池デバイスとは格が違うんだよー。

シェーダーグラフ全体図

Bayer-dithering ShaderGraph

さてこちらがグラフ全体図です。やっている事はシンプルなのですが、ベイヤーフィルタの値を取得する部分が大きくなってしまっています。最後にちょこっと画素値と比較して白か黒をアウトプットしています。ちなみにこれは2Dスプライト用のシェーダーです。

スクリーン座標の取得とベイヤーフィルタ上の位置の算出

Bayer-dithering ShaderGraph - Getting screen position

先頭のこの部分ではスプライトのスクリーン座標を取得しています。Screen PositionノードでXY座標がそのまま出てくるのかと思ったんですが、どうも0~1.0に正規化された値のようでした。そのため、Screenノードでスクリーンサイズを取得してMultiplyすることでピクセル単位に変換しています。

ピクセル座標が取得できたら、X, Yそれぞれを4とで余りを取って4x4のベイヤーフィルタにおいて参照すべき位置に変換します。これはX要素、Y要素に分けたあと、Floorノードで整数に変換しているだけです。これで全ての値が0..3の整数に収まりました。あとはベイヤーフィルタ上の対応する位置を取得して閾値とし、画素値と比較するだけです。

ベイヤーフィルタから任意の位置の値を取得する

Bayer-dithering ShaderGraph - Extract Bayer-filter value

ベイヤーフィルタは単純な4x4の行列で表せるのでMatrix4x4ノードを使おうと思ったのですが、シェーダーグラフ上でそのマトリックスから任意の位置にある値を取り出す方法がわかりませんでした。

そこで、頭の悪そうな方法ですが、〈Comparisonノード〉〈Andノード〉、それから〈Branchノード〉を組み合わせて、“X値が0かつY値が0なら1行目1列目の値”、“X値が0かつY値が1なら2行目1列目の値”、“X値が0かつY値が2なら3行目1列目の値”……、“X値が1かつY値が0なら1行目2列目の値”…… これを馬鹿正直に16回繰り返しています。それぞれ条件に当てはまらない場合は0を出力します。このため、16箇所で算出された値をすべて足し合わせれば、結果としてベイヤーフィルタから欲しかった位置にある値が取り出せたことになります。

なお、0..1.0の画素値と比較するために、取り出した値を最後に16で割ってスケールを合わせています。

画素値の取得とグレイスケール化

Bayer-dithering ShaderGraph - Picking up pixel value and grayscaling

一方、こちらはベイヤーフィルタと比較すべき画素値を取得している部分です。元々のスプライトの画素値はReferenceを"_MainTex"とした〈Texture2DのInputノード〉〈Sample Texture 2Dノード〉に繋げることで取得できるようです。

次にベイヤーフィルタと比較する前の下準備として、RGB値をグレイスケールに変換しました。グレースケール化では一般的なR * 0.299 + G * 0.587 + B * 0.114の式を使っています。

ベイヤーフィルタ値と画素値の比較、白黒の出力

Bayer-dithering ShaderGraph - Comparing pixel value to filter value and output black or white

最後に、画素値の値Aから、ベイヤーフィルタの値Bを引くことで両者を比較しています。画素値が閾値より大きければプラス、小さければマイナスになるので、〈Signノード〉を使って-1または1に変換し、それをそのままRGBの各チャンネルに分配して白または黒の色とし、最終的な〈Sprite Lit Masterノード〉のColorに繋げています。-1は黒(0)にクランプされるみたいです。

結果

Bayer-dithered Keycappie

というわけで、シェーダーグラフの見た目はともかく、ベイヤーディザそのものはおそらく実現できているのではないかと…… ひょっとしたら間違っているかもしれませんが、少なくとも見た目はそれっぽくなりました。

まだシェーダーグラフはよくわからなくて、これを書いている間にも〈“サブグラフ”〉という概念を知ったので、もう少しシンプルにまとめられそうな気がします。例えばこのグラフではAddノードで3つや4つの値を足す箇所が多いのですが、複数の値を足すようなサブグラフを作ればもう少しスッキリしそうです。