突発的にやりたくなったので、NPR のネタの1つ、動的な墨絵シェーダーを作ってみました。
本当は、「波」を作ろうかと思ったのですけど、動的にテクスチャーを生成するネタで墨絵シェーダーが浮かんだので、実装しました。
マウスで画面の上をドラッグすると、点を打ちます。その点がいい感じにぼやけて広がっていきます。
ちなみに使用前
点を、置いているのがわかります。
まぁ、いつものように適当にファイルが入っています。
sumie.psh | 頂点シェーダー。 |
sumie.psh | ピクセルシェーダー。 |
draw.cpp | メインの描画部分。 |
draw.h | 描画の各関数の定義。 |
main.h | 基本的な定数など。今回も出番無し。 |
main.cpp | 描画に関係しないシステム的な部分。変更が無いので、出番無し。 |
load.cpp | ロード。 |
load.h | ロードのインターフェイス。 |
あと、実行ファイル及び、プロジェクトファイルが入っています。
ぼかすために、4つのテクスチャーを重ねています。
中心に基準となる1つと、ピクセルを1つ分ずらして、正三角形になるように配置しました。
頂点シェーダーは、透視変換済みの頂点をコピーし、テクスチャー座標は固定レジスタを使って基準の位置からずらし、4つおけるようにします。
0001: ; sumie.vsh 0002: 0003: vs.1.1 0004: 0005: mov oPos, v0 0006: 0007: add oT0, v7, c20 0008: add oT1, v7, c21 0009: add oT2, v7, c22 0010: add oT3, v7, c23
ピクセルシェーダープログラムですが、入力の型情報とプログラムで成り立っています。
0001: ; sumie.psh 0002: ; 0003: ps.1.0 0004: 0005: def c0, 0.05f, 0.05f, 0.05f, 0.0f 0006: 0007: ; テクスチャーの色を引っ張ってくる 0008: tex t0 ; 0:0 0 0 1:0 1 0 2:0 0 0 3:0 0 0 0009: tex t1 ; 0 1 0 0 0 0 0 0 0 0 0 0 0010: tex t2 ; 0 0 0 0 0 0 1 0 0 0 0 1 0011: tex t3 0012: 0013: mov r1, t0 0014: add r0, t1, -r1 0015: mad r0, r0, c0, t0 0016: 0017: add r1, t2, -r1 0018: mad r0, r1, c0, r0 0019: 0020: mov r1, t0 0021: add r1, t3, -r1 0022: mad r0, r1, c0, r0
さて、ここが曲者です。
ざっと見ると、
出力 = t0 + 0.05(t1-t0) + 0.05(t2-t0) + 0.05(t3-t0)
に見えます。
実はそうではありません。
なぜなら、上の計算では、周りの色が暗い限り散逸が発生するので、最終的には均一な色になるからです。
今回の計算は、アンダーフローを積極的に利用しています。
DirectXのピクセルシェーダーの精度は、最低 8 ビットと決まっています。しかし、8ビットしか持っていない場合がほとんどです
(将来的には精度は上がるでしょうから、そのときにはシェーダーを変更しなくてはなりません)。
-1~1の間の8ビットなので、実質、7ビットが小数の値として使えます。それぞれの値は、
2^-1=0.5 2^-2=0.25 2^-3=0.125 2^-4=0.0625 2^-5=0.03125 2^-6=0.015625 2^-7=0.0078125
です。この精度で、0.05は、0.05 = 2^-5 + 2^-6になります。
従って、t1-t0 の値が、2^-3以下だったら、その積は、2^-7よりも小さいので、アンダーフローを起こして0になります。
色にすると、2^-3=0.125に128倍して、16が有効な色の差になります。
隣との色の差が16以内だったら、同じ色として扱われて、散逸は発生しなくなります。
色が256段階とすると、16段階の階層分けが行われます。1テクセル1段階とすると、16テクセルになります。
墨絵の絵のにじみの大きさも、大体そんな具合ではないでしょうか。
と、考えているけど、ほんとかなぁ?
何にせよ、短くまとめてしまうと、散逸して黒い部分が消えてしまうので、絶妙なバランスをとっていることは確かです。
ほんとは、別の方法で実装しようと考えていたのですが、命令数が足りなくなって、この方法に落ち着きました。
また、一発芸をしました。
今回の方法のように、座標をずらしたテクスチャーを重ねれば、「波」が作れます。
Cg Shadersのcgshaders.org contestに、 この墨絵をCg言語で記述したものを応募してみました(結果は芳しくありませんでしたが)。
内容は似たようなもんです。
sumie.psh | 頂点シェーダー。 |
sumie.vsh | ピクセルシェーダー。 |
draw.cpp | メインの描画部分。 |
draw.h | 描画の各関数の定義。 |
main.h | 基本的な定数など。 |
main.cpp | 描画に関係しないシステム的な部分。 |
license.txt | フリー宣言のライセンス |
O.col = 0.15f*(t2-t0+t3-t0+t1-t0)+t0;
t0がいたるところにありますが、これを一箇所にまとめると、アンダーフローが起きなくなって墨は完全に紙に吸い込まれてしまいます。
同じ見た目を出すためにパラメータを調整した結果、係数は0.15になりました。3つ分足してるので、誤差が小さくなってるんですかね?
英語できちんと表現できる能力が欲しいと痛烈に感じた、今日この頃です。