美しい世界を自分の手で──シェーダを操る基礎のキソ

 リソースパックの最高峰、マインクラフトの世界を大きく美しく変えるシェーダですが、影を描く以外にも見た目の特別な演出を可能にしたりと、影に興味がなくてもなかなかに重要になりえるコンテンツです。

 さて今回、シェーダを使いながらも普段から影を描かないわたしがシェーダの作り方を教えてもいいんですが、すでにとても分かりやすいチュートリアルをあるひとが書いているので、わたしが教えても──というのがひとつ、そして何よりわたし自身そのチュートリアルをカンニングしながら教えることになりそうだったので、結局はあのチュートリアルでいいじゃないかと思いました。
 そこで今回わたしが書くのはそのチュートリアルを読んでもよくわからなかった、発展ができないというひとのための「基礎のキソ」を理解するための記事です。「基礎」じゃなくて「基礎のキソ」です。

 その「あるひとが書いたチュートリアル」ってなんだよ、と思ったかたはこちらです。もしくはこのチュートリアルを読んでいないけどシェーダを学ぶためにわたしの記事を読みにきたというひとはこちらのチュートリアルを先に読んだほうがいいと思います。

 このチュートリアルは必要最低限のことが無駄なく書いてあるので、わたしの書く何かと違って読む気も湧くし、さらっと勉強できる感じがします。エリンギさんの記事は良いというのは本音なんですが、彼もこう言っているとおり自分の力がわりと必要になってくるのでもしかしたら全然わからないということもあるかもしれません。
分かりやすく解説(当社比)していますが、丸写しで満足されては悲しいのであと一歩までの解説に留めていることもあります。
 実際わたしはこのチュートリアルに載っていないことも自分の思考で補って理解しました。極端に頭が悪くなければ(シェーダに挑戦しようと思える自信があるのなら)あのチュートリアルにかかれていることをいくつか真似して自分で応用させていけば普通にコツがつかめると思いますが、念のためです。

■ シェーダの世界は頭の使い方が全く違う

 まず何も知らない状態でシェーダの仕組みを考えてみると、大抵のひとは「物体の位置と、太陽などの光源の位置からあーだこーだ計算して影を作っている」と考えると思います。あのチュートリアルでもこの辺りには触れているのでわかるかもしれませんが、実は全く違います。
 ならどういう仕組みかと言うと、マインクラフトをやっているあなたの端末の画面に何がどの部分に映っているかを検知して、どんな色を塗るかを決める、という感じです。つまりBEのシェーダで影を作っているのは、暗い部分の色を明るい部分の色と大きく差をつけて影をはっきりさせているに過ぎないんです!
 悪いもの扱いするような言い方になりましたが、これを知らないと何にも検知できないといって絶望したり、したいことがあっても検知の仕方がわからなくて萎えたりします。とにかく、頭の使い方を完全に新しくする必要がありそうです。シェーダ専用の頭をインストールしてください。

■ コードの構成

 ほかのアドオンでjsonをやっていたとしても顔がひきつる構文。C言語を知らないと即敬遠です。言われてみればjsonでも最初はこんな感覚だったなあとしみじみ思ったこともありましたが。jsonでも同じですが、どの部分が何を表すのか大雑把でも掴んでおくと怖くなくなります。
 試しにglslのrenderchunk.fragmentでも開いてみてください。真ん中ぐらいに「void main ()」とありますね。これより下が本来の処理を書くところです。それより上の部分は、その処理をするための準備がズラっと並んでいます。準備の部分は、よくわかってないうちに触る必要はないでしょう。
 もう少し詳しく説明していきましょうか。処理の部分は、たとえて言えば「製造ライン」です。よくある工場のイメージの、長いコンベアの上に作るものが乗っていて、流れていく間に側(そば)で待ち構えている機械によって行程が加えられて、それが完成までコンベア一本で繋がっているような、まさにそんな感じです。
 比喩の中身を解説します。コードをよく見てみると、jsonの{}みたいにたくさん#ifdef(もしくは#ifndef)と#endifの組が並んでいることがわかります。これが材料や製造物に行程を加える機械たちです。一気に下までスクロールしてみましょう。最後に「gl_FragColor = diffuse;」というのがありますね。これが長いコンベアの旅を経てできた完成品です。もっと詳しく言うと、完成品diffuseをgl_FragColorに出荷しています
 このイメージがあれば、あのチュートリアルの#3で「gl_FragColor = vec4(1.0);」にして世界を真っ白にしているのが納得いくと思います。さんざん長いコンベアの上を流しておいて、最後には完成品じゃなくてどこからともなく持ってきた値を出荷している画(え)が浮かんでいるでしょうか

 次は途中にいる機械たちです。さきほどシェーダの仕組みについてざっくり説明しましたが、それがこの機械たちの正体に直結します。
 そこで、マインクラフトをプレイしているとしましょう。リスポーン地点としてありがちな、樫の木と白樺の木がある森にいるとします。まず画面の大半を占めているのはrenderchunk.fragment/vertexが担当する部分です。地面や木など、手やホットバー以外のブロックの色味や形の見え方を担います。その他インベントリや空、プレイヤーの手・手に持っているアイテムなどは別ファイルが担当するので今回は割愛です。renderchunk.fragmentが担当する箇所でも表示の方法が違うところがあります。色つきガラスを目の前に置けば、画面内に半透明という新しい表示を使っている領域ができます。それ以前に葉っぱも他の草ブロックや石ブロックなどの不透過ブロックとは違う描画方法です。そして、水に潜れば霧が表示されて遠くが見えなくなります。これもまた別です。
 描画方法がそれぞれ違う領域が画面内にたくさん現れたわけですが、当然 描画方法が違えば"工場で作る完成品"もそれぞれ違うものでないといけません。そのために、半透明の処理が必要な製造物にだけ行程を加える機械や霧の処理が必要な製造物にだけ行程を加える機械が側にいるわけです。バニラのrenderchunk.fragmentにはあんまりいろいろないんですが、半透明の処理なら#ifdef BLEND、透明の処理なら#ifdef ALPHA_TEST、霧の処理なら#ifdef FOGになります。これらとそれぞれの#endifの中身に「diffuse = vec4(1.0);」などと入れてみると、それぞれの処理がなされている部分だけ真っ白になると思います。gl_FragColorは出荷先なので複雑な書き方をしない限り最後にひとつ書くだけです、間違って「gl_FragColor = vec4(1.0);」と書かないように気を付けてください
 ここにきて、diffuseの説明をちゃんとしていませんでしたね。なんとなくわかると思いますが、途中でチラチラとわたしが「製造物」と表現していたものです。中には色が入っています。画面上の任意の一点における色です。あなたがマインクラフトをしているスマホの画面を適当に指差すとします。その点は色つきガラスを描画している領域の一部の点だとすると、そこの点のdiffuseは#ifdef BLENDをくぐってきたdiffuseの完成品が出荷された点ということになります。さらにそこは黄色の色つきガラスを通した草ブロックの緑の部分であるとすれば、草ブロックのもとのテクスチャの白黒+バイオームに合った緑色+色つきガラスの黄色=diffuseの色です。まさにシェーダの本質。あ、夜なら暗くなる分 色の量が引いてあって、さらに松明で照らされているなら明るい分 また色が足されて──と割と深いです。

■ 計算での頭の使い方

 あのチュートリアルにもグラフを用いて解説しているとおり、演算ができる世界ではだいたいグラフが頭に描けるかどうかで有利不利が決まってきます。逆に、グラフが頭のなかで描けてしまえば楽勝といっても過言ではないはずです(パーティクルもリソースのアニメーションも一緒)。あとは、丁寧にひとつずつ理解することが大切だと思います。それぞれの行程が何をしているのか、何を意味するのか、ちゃんとひとつずつ分解していってください。ちゃんと噛み砕かないと飲み込むものも飲み込めません。例えば、最終的に影はこれでできます:

float shadow = mix(0.5, 1.0, smoothstep(0.865, 0.875, uv1.y));
diffuse.rgb *= mix(shadow, 1.0, uv1.x);
とあのチュートリアルではパパっと解説していますが、初手であれをああなるほどとすぐに理解するのは無理があると、わたしも思います。彼を悪く言うわけではないですが、結局はすべて理解しているひとが書いたものです。
 でも「ふうん、影はこれmix(0.5, 1.0, smoothstep(0.865, 0.875, uv1.y));でできるんだ」と公式のように捉えたら今後 応用できません。
 と言いつつもわたしから解説することはほとんどないんですが。まずは関数の定義「smoothstep(a,b,x) = x < a ? 0.0: x > b ? 1.0: (LinearInterpolation)」「max(a,b,x) = x == 0.0 ? a: x == 1.0 ? b: (LinearInterpolation)」を覚えておきましょう(詳しくはチュートリアルで学んでください、molangで書きました、(LinearInterpolation)は適当です)。
 uv1.yをxとして「y=x」「y=smoothstep(0.865, 0.875, x)」「y=mix(0.5, 1.0, smoothstep(0.865, 0.875, x))」のグラフをイメージしましょう。したところであのチュートリアルにあるグラフが描けるんですが、先に自分で描いてみて答えと比べて「ああ確かにそうなるね」をやってください。「こういう式を作ればこういうグラフになるんだ、なるほど」だと理解できてません。なんとこれは学問としての数学にも通用します!この記事を読んでシェーダの基礎のキソを学ぶついでに数学が得意になるコツも学べるなんて、あなたラッキーですよ!(?)

■ fragmentだけじゃない

 ファイル名に「renderchunk」とあるのは「.fragment」ファイルだけじゃありません。真下に「.vertex」ファイルが同じ名前で存在します。こっちのファイルではブロックなどの形の見え方を操るわけですが、これもまた画面上の話になります。わかっていると思いますが、ブロックの位置をシェーダでずらしたからといって当たり判定はそのまま動かす前の位置に居座っています。ここまではいいと思います。ただしgl_Positionという、fragmentでいうところのgl_FragColorにあたるものですが──もちろんこれも完成品を出荷するところという認識で大丈夫です(コードの最後にないのは違いますが)──どう描画に影響するかを理解しようとすると脳がバグるかもしれません。
 説明は簡単です。表示するものの画面上における最終的な位置が出荷された完成品となります。というとブロックごとに位置を決めていると勘違いしがちですが、もちろんこれも画面上の任意の一点です。あなたがまたマインクラフトをしているスマホの画面を適当に指差すとします。シェーダのない状態でもともと草ブロックの上部分が映っていたとすると、「gl_Position.x += 1.0」ならばその位置から1.0だけ右にずれた場所に描画されます。簡単なように聞こえるでしょう、そこでどう映るかイメージしてみてください。ちなみに画面の中心(重心)を原点として右がx軸正、上がy軸正です。
 それでは実際にやってみます。ただし、AS_ENTITY_RENDERERの下にあるやつでやるとわかりにくい結果になるので、FOGの中身にイチから自分で書いてください。そのとき「+」を書き忘れると画面上の「x=1」である、中心からちょっと右にずれた縦一直線に世界が圧縮されます。そしてFOGの中身に書いたということは、水のなかで変化が大きく現れるということなので、水のなかで変化を見ましょう。
 どうでしょうか。普通に映ってると思いますか?ぐるぐると周囲を見回してみてください。そのときに壁に密着していると変化が分かりやすいです。改めてどうでしょうか。あなたの予想していたとおりでしょうか?
 思ったより気持ち悪いです。ひとによっては絶対酔いますね……。言われてみれば「見回す」ことは回転軌道を含むので結果も回転を含むような形になります。次元をまたぐ幾何学のつよつよじゃない限り何が起こっているのか感覚ではわかりません。実際何が起こっているのかは確かに先ほど説明したとおりで正しいのですが、それがいざ見た目で現れるとこういうことになるんです。
 まあこれが「画面上に何がどこに映っているか」で考えるということになりますが、基本は「gl_Position = pos;」であることを忘れなければ迷ったりしません。変になったら基本からやり直してみる、なんたらのつよつよじゃない限りはそれを繰り返して応用していきます。これに限ってはチュートリアルの式を公式化してもまあ最初のうちはいいかなといった感じです。
 地味にスルーしていましたが、実はgl_Positionのx,yはそれぞれ画面上の描画する座標と説明しましたが、gl_Positionは三次のベクトルです。つまりzがあります。画面は三次元だった……!とは思わないでください。そんなわけありませんよね。zはデプス(深度)を表します。まあ「デプス」なんて誰も呼んでいないんですが。奥行きっちゃ奥行きなんですが、画面上の世界で理解することを忘れてほしくないので新しい概念のように表現します。長々と焦らしましたが、つまりはテクスチャを手前に映すか、奥に映すかです。ペイントツールのレイヤーみたいなイメージですかね。たとえば、#ifdef BLENDの中身にgl_Position = 1.0;を書き足すと、色つきガラスや水が草ブロックなどのブロック越しに見えるようになります。つまり「z=1」という座標は草ブロックなど他のブロックより「手前」だということです。なんとなくデプス(深度)という言葉で表現されているのもわかると思います。

■ 最後に

 以上、このへんが「基礎のキソ」たる内容になると思います。あまりしゃべりすぎても記事が長くなる一方ですし、あのチュートリアルに書いてあることを二度書くことになりそうなので、終わりにしておきます。
 とにかく、新概念やコードの読み解き方を捉えるということです。それだけでも、実質難しいのでこれほどのボリュームになってしまいました。気合いをいれて勉強しましょう!

MinecraftBEアドオン-ヒント倉庫

MimecraftBEのアドオン作りで困ったときにここで解決できるようにと個人によって作られたものです

0コメント

  • 1000 / 1000