先日、極座標マテリアルについて記事にしました。
この時はマテリアル関数の「ComputeMipLevel」の中身についてちゃんと理解してなかったので、分かったことを書いておきたいと思います。
上記の記事をすでに読まれていること前提で話を進めます。
Texture Sampleノードの MipValueMode を MipLevel にすることで「Level」入力ピンが表示され、そこに数値を与えることでテクスチャの MipLevel を直接指定できます。
そしてマテリアル関数「ComputeMipLevel」は「DDXとDDYを使ってMipLevelを決める」構成になっている‥という感じかと思います。
このたまに耳にするDDX・DDYが一体何なのか?については、ずし (@shiba_zushi) さんが教えてくださいました!ブログで解説されているもんしょ (@monsho1977) さんともども感謝です!
https://t.co/gJLYhg0ByS
— ずし (@shiba_zushi) June 14, 2018
ddx ddyに関してはもんしょさんの解説がわかりやすいかもです
ざっくり言うと自分と隣のピクセルの差です
コンピュートmipは隣のピクセルとuv値が大きく違えば荒いとみなして荒いmipを返す みたいな挙動をしてる感じです
せっかくなので新しい方のブログのURLも。
つまるところ、以下のような感じのようです。
・スクリーンスペースで2x2ピクセルごとに処理する
・DDXは入力された値に対し、左上のピクセルとその右のピクセルを比較して差を出す
・DDYは入力された値に対し、左上のピクセルとその下のピクセルを比較して差を出す
・出した結果は2x2ピクセル全てに同じ値を返す
試しにUVを繋いでDebugノードで数値を可視化してみました。
カメラから離すと隣接ピクセルのUV値の変化が大きくなるので、結果の値も大きくなっているのが分かります(カメラから離すだけでなくスケールで小さくしたりUVタイリング数を増やして小さくしても同様の結果になるかと思います)。
値がとても小さいのは板ポリの端〜端でUV座標の値が0〜1だからですね。つまりこの板ポリ全体がぴったり2ピクセル幅で表示されるくらい小さくなればやっと1が返ってくる理屈になるのかなと。
DDX・DDYは4チャンネルの値を入れたら4チャンネルそれぞれ比較して返すようです。
例えば下図は2チャンネルの値を渡した場合。双方の計算を返していますね。
こちらは、黒白で二値化した情報を入力した場合。
黒と白の境界で1の変化があるため、DDXでは縦に白いラインが入りました。
なるほど、これは色々利用できそうですね。。
という訳で、DDXとDDYについては分かりました。では次。
DDXとDDYの計算結果に対して、入力してきたテクスチャサイズを掛けて、DotProduct>If>Squart>Log2といった流れになっています。順に見ていきます。
DDXとDDYどちらにも、UとVの両方の情報を渡しています。それはポリゴンを回転させるとUとVのどちらの値もスクリーンスペース上で横方向の差に影響するためです。例えば45度ロール方向で回転させるとスクリーンスペース上ではUとVのどちらも同じ変化量になりますね(最初は勘違いしてDDXにVの値を渡す必要ないと思って構成を削ってしまっていました。こちらについて教えてくださった けだまじごく (@kedamazigoku) さん、ありがとうございます!)
そして結果にテクスチャサイズを掛けているのは、恐らくUV座標が0〜1なのを0〜テクスチャサイズに広げてやってるのだと思います。これで隣接ピクセルのUV座標の差がテクスチャのピクセルの差になります。テクスチャのピクセル数を反映しないと実際にどれだけ荒くなってるか判断できないからですね。例えばテクスチャが2*2ピクセルなら板ポリも2*2ピクセルで表示されれば適正サイズとなるイメージで。
次に DotProduct が挟まっていますが、こちらは後の工程の Sqrt ( Square Root / 平方根) と併せて、DDXやDDYの結果である2チャンネルの値を「1チャンネルの大きさ情報」にまとめるためのようです。
こちら、トリニ (@to_ri2) さんが教えてくださいました!感謝です!
DotProduct(A, A)の計算結果A.x * A.x + A.y * A.y + A.z * A.zはAベクトルの長さの2乗と一致するので、Squartを使うことで長さを取得しているように感じます
— トリニ (@to_ri2) June 17, 2018
そして If では、DDXの結果はAに・DDYの結果はBに入力してAとBを比較、Aの方が大きいならDDXの結果を出力し、Bの方が大きければDDYの結果を出力します。
つまりわざわざ If を使わなくても「Max」でいいじゃん、ということですね。
ちなみにAとBが同じ場合 (A==B) に指定が無いですが、その場合は A>B が返るようです。
結局「Ifで何をしているのか?」というと、隣のピクセルのU座標の差とV座標の差の大きい方をミップレベルに利用する‥ということですかね。つまり、UとVで差の大きさが違う場合に差が大きい方を選び、結果「MipLevelを高くする=粗くする」選択をしているということになるかと思います。
せっかくComputeMipLevelを使うのでしたら、極座標のUVをそのまま使って、ComputeMipLevelのMax処理(なぜかIfで書かれてる)をMinにして正攻法で攻めたほうが良いのかなと思いました。MinはMaxのときよりパフォーマンス不利ですが、Mipmapも考慮しつつアーティファクトがほぼ見えなくなります。 pic.twitter.com/5VdOh1kLWQ
— けだまじごく (@kedamazigoku) June 12, 2018
つまり、極座標の記事でも引用させていただいた、けだまじごく (@kedamazigoku) さんのこちらのツイートは、ここをMaxではなくMinにすることで「解像度を高めの方を選択した方が綺麗な結果になる (=アーティファクトを抑えられる) 」ということでしょうか。
という訳で、If を Min に入れ替えるとこんな感じ。
最後の Castumノードによる対数 (Log2) で、具体的なミップマップレベルを算出しているようです。「Log2(x)」は「2を底とする対数」の関数で「xは2の何乗か?」になります。
Mipmap が効くテクスチャのサイズは「2 の累乗」になるので、Log2を使って「何段階目の MipLevel なのか?」が求められる訳ですね。
ちなみに対数についてはこちらの解説がとても分かりやすかったです。