この文章は
K3 Advent Calendar 2024、11日目の記事です。
https://adventar.org/calendars/10132
ほとんどの方は初めまして、MachiaWorksと申します。
社会人しながらゲームやツール作ったりしてます。
今回、シェーダとその応用でシェーダ使って描画する件について、超平易な言葉で書いてみるものです。
1.シェーダとは
3Dプログラム上でのテクスチャに対する色付け処理の事。
シェーダは一応のパイプラインがあるけど、その制御内容は自分で書くことができる。
シェーダはGPUを使う。
よって、並列で動くことを考慮する必要がある。
例えば描画タイミングの制御は困難とか。
レンダリングパイプライン
レンダリング時にGPUが処理する構成要素と考えていい。
レンダリングパイプライン上の主な要素(他にもあるよ)
- 頂点シェーダ
頂点に対してシェーダプログラムで処理を実行する。
主に頂点を基準にした色の変更を行う。
また、モデルのローカル座標をスクリーン座標に変換することも行う。
光源の位置が3D空間上にある場合ここで色調変更するとかになるかと思う。 - ピクセルシェーダ(ジオメトリシェーダ)
画面の各ピクセルに対してシェーダプログラムで処理を実行し、テクスチャや光源などから実際の色を決定する。
主に光源の位置との内積をとって暗くする、明るくする等を行う。
他には一部描画を行わないなんてものもできる。(透過度を上げる。ディゾルブ処理がわかりやすいかな)
また、パイプラインによっては前のプロセスで色付けした内容を参照可能。
例えば頂点→ピクセルと色を引き継ぐことも可能。
上記のように、各頂点・ピクセル等を制御するためのプログラムがシェーダとなる。
他にはジオメトリーシェーダ(メッシュの頂点数の増減や変換を行う)・テッセレーションシェーダ(辺を細分化し曲面に近くする)なんてものもある。
主な使い方
- ライティング(光に対して反射・遮蔽物があれば影を付ける)
だいたいの人は正直ここまで読んでもらえばいいかなって思う。
ここ以降少し難しめの内容を記載するので、ギブアップだったらごめん。
2.シェーダでおえかきもできる
上記は勿論「光を反射しなくても使える」
言ってしまうと「光がなくても方法を指定すれば色を変更できる」
頂点の色付けは光がなくてもできるし、
ピクセル単位で色を付けられれば、お絵描きもできる。
上記を利用して、プログラムを書いて板ポリゴン上にお絵かきするもの。
メリット
GPUを使うので処理速度が早い
テクスチャのピクセルに依存しない描画処理が可能
メモリの転送も可能なので場合によっては一部処理を高速化可能(GPGPU)
デメリット
シェーダの計算量が膨大な場合、処理遅延により絵が出てこないフレームも発生しうる
専用のプログラミング言語が存在する
デバッグが大変
3.お試しに図形を書いてみる
図形の書き方
テクスチャの平面上にある点と図形の距離を計測して求める方法で検出してみる。
DistanceFunction
https://ja.wikipedia.org/wiki/%E8%B7%9D%E9%9B%A2%E5%87%BD%E6%95%B0
SDFそのものの説明については下記がわかりやすい。
https://note.com/kyndinfo/n/n55ccba788f5f
2Dの円を書いてみる
以下のサイトで、ブラウザ上でシェーダ(GLSL)を書く・動かすことができる。
https://www.shadertoy.com/
やってることは上記と同じで、「2枚の三角形(板ポリゴン)があって」「板ポリの上でシェーダが動く」環境です。
(そのほかにも曲流したり絵や動画の情報を参照できたりと色々あるけど省略)
以下ソースコードをコピペすれば動くものが出ると思う。(例を改造したやつだけど)
//SDF・円の距離を計測する
//今回は、円の中心からXY平面上の点までの距離を返してる
float sdCircle( in vec2 p, in float r, in vec2 mos )
{
vec2 cent = mos;
return length(p-cent)-r;
}
//色を決める関数(Shadertoy独自)
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
//テクスチャ上のXY座標
vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y;
//マウスをクリックした座標
vec2 m = (2.0*iMouse.xy-iResolution.xy)/iResolution.y;
//各ピクセルにおけるSDFを利用した比較結果
float d = sdCircle(p,0.5,m);
//=======================================================
//比較結果によって色を決める
//0より上(円の外側):オレンジ
//0以下(円の内側) :青っぽい色にする
vec3 col = (d>0.0) ? vec3(0.9,0.6,0.3) : vec3(0.65,0.85,1.0);
//円の周辺がぼやっとする
col *= 1.0 - exp(-6.0*abs(d));
//縞模様を描く
col *= 0.8 + 0.2*cos(150.0*d);
//円の周囲に白色を足す
col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
//最終的な出力結果として計算した値を渡す
fragColor = vec4(col,1.0);
}
他の図形が必要な場合、自分で式を立てるか下記参照
https://iquilezles.org/articles/distfunctions2d/
3.レイマーチングとの違い
ここまで書いてきたやつ、知ってる人は「レイマーチングだ」という話になってくると思うんだけど、そもそもレイマーチングとの違いはなんなの?という疑問があると思う。
少なくとも自分は、2DのSDFとレイマーチングを一緒に説明しているサイトも存在して混乱しているため、この文章を書いたところはある。
レイマーチングについて
レイトレーシングという光の通り道を計算して物体を描画するための手法があり、レイマーチングとはレイトレーシングを実現するための計算方法の一種となる。
とある点からスクリーンへ漸近的に光を飛ばし、3DのSDFで点の位置を比較していき、詳細な形状を描画するもの。
2Dと3DのSDFの違い
2DのSDFを利用した計算と3DのSDFを利用したレイマーチングについては、計算のやり方は同じではある。ただ、範囲がまったく違うのね。
2DのSDFによる描画は、あくまで2D平面上の描画がメインとなる。
それに対して、3DのSDFを利用した描画は「2D平面上をスクリーンと見立て、その奥に空間がある前提で、光が当たってるところを計算して立体を描画する」ことをやってる。
つまり、3Dのほうは超応用分野なわけですよ。
(多分2Dのほうは平面が範囲で3Dのほうは空間が比較範囲になるわけだよね)
よって、まず2D側で描画出来たほうが、理解しやすいかなって思う。
おまけ
UnityでSDFする方法
https://qiita.com/sune2/items/834b17f68f495c570c9f
Unityで2DのSDFを使って模様(エフェクトとか)を描画する方法について書かれてる。
UnlitShaderを新規作成し、その中身を変更することで同様の環境を実現してる。
この文章で公開している内容に近いもの。
Unityで実行可能。
https://nn-hokuson.hatenablog.com/entry/2018/05/24/195206
あとこれ
https://arumogina.hatenadiary.com/entry/2019/06/06/163444
4.終わりに
平易な言葉でざっくりと書いてるけど、内容が難しかったらごめん。