軽率Raymarching解説編
简单光线步进
https://gyazo.com/41c8f178bf475f08e5d9e3278749bed0
どうして立体的な絵になるのですか?
板ポリはただの平面で、左から右に行くx座標(コードではuと書かれている)、下から上に行くy座標(こちらはv)があるよ。
それに加えて手前から奥に向かうz座標があるということにして、レイマーチングのプログラムは書かれているよ。
座標を3次元として扱っているので、球は立体になり、ライティングを行ったので立体的な見た目を実現しているよ。
Raymarchingってどういうことですか?ぜんぜんわからん…
https://gyazo.com/922dcdc88587bd86e33cbdd15738531e
ディスタンスファンクション(距離関数、length)を使っているところがキモだよ。
レイが今いる座標からオブジェクトまでの距離を算出して、レイをその距離だけ前に進めるという繰り返しを行うよ。
カメラと板とオブジェクトみたいな説明図を見ても、何がどうしてこんなプログラムになるのかさっぱりイメージがつかめない…
なんというかこう、これはすごく説明がむつかしい。いくら良い図を描いてもやっぱりわからない気がする。
最近のおいらがイメージしているのは、「紙みたいな板ポリがレイマーチングループの中で『紙全体が』前に進んでいって、何か球や箱に当たったらそこの点がわかる。当たらない部分は無限に奥に進む。」みたいな処理イメージだけど、どう説明すればいいのかまったくわからない。
考えるんじゃない。感じるんだ。(これは毎晩レイマーチングを繰り返し書いて、なんとなく呑み込めてきた。)
どうして動くのですか?
時間を使って座標を動かしているよ。
float3 p = float3(0, 0, 0);
p += _Time.y;
これは1秒ごとに座標pがxyzそれぞれすべてが1ずつ増えて直線に移動することになるよ。
(0).xxxって何ですか?
スィズル(Swizzle)という書き方だよ。詳しくは以下の資料を読んでね。
なぜこれで回転できるのですか?
code:HLSL
float2 rot(float2 p, float a) {
return float2(p.x * cos(a) - p.y * sin(a), p.x * sin(a) + p.y * cos(a));
}
2次元の座標を回転行列に掛け算することで、回転を実現できるよ。
3次元についても、違う平面を使って2回計算することで回転できるよ。
p.xy = rot(p.xy, _Time.y);
p.xz = rot(p.xz, _Time.y);
回転行列について
https://gyazo.com/fb4afb48f69d8f636c648a392a8a273b
数学の授業で行列やベクトルの話はだいたいこんな説明がされるよ。
考えるんじゃない。感じるんだ。
dotってなんですか?
内積だよ!英語だとdot productって呼ぶよ。このキーワードを覚えておくと、ShaderGraphに触った時に内積をすぐ探し出せるようになるよ。
内積ってなんですか?
考えるんじゃない。感じるんだ。
高校生向けの数学解説ページで、「一方のベクトルが垂直にもう一方のベクトルに下りていった時の影の長さ」みたいな説明があるけど、正直わかりづらいし面白くないしなんの興味も持てない。
それより、ありけいさんの「キャラの顔に当たるライトと影」の説明や、やおもちゃラボさんのリムライトの話を熟読して自分でコードを書くのがよっぽど良いよ。
重要なのは、ライトと肌から垂直に出ている法線の角度を、dotを使えば1から-1の間の数字であらわせるということ(三角関数で書くとcos(θ)の値となる)。
1なら肌にまっすぐ光が当たっているのだから明るくピクセルを塗れば良い。0は垂直になっていて光は当たらない。-1は裏側から光が向いているので、ピクセルを暗く塗る。
もうひとつはありけいさんのこの解説。「VRで自分とキャラの目が合うことを内積で判定する」。
先のライティングでのdotの使い方がわかれば、こちらもわかってくるよ。
レイマーチングのコードに戻ると…
code:HLSL
float3 L = normalize(float3(-1.0, 0.5, -0.2));
dot(L, N)
Lはライトの向き。normalizeしているので、「大きさが1」になっている………これは角度だけを扱いたいから、ベクトルの大きさという要素を消したいため。
Nは法線。Normalの略。今回のコードでは、球の法線のnormalizeされたベクトル。
modってなんですか?
code:HLSL
float mod(float x, float y)
漢字だと剰余って書くけど、割り算をした時の余りのことだよ。
xをyで割った時の余りは、以下の式で計算できる。
x - floor(x / y) * y
2つ以上の数字でいっぺんに余りを計算する場合は、floatをそのままfloat2やfloat3に書き換えるだけで使えるようになるよ。
code:HLSL
float3 mod(float3 x, float3 y) {
return x - floor(x / y) * y;
}
Raymarchingではmodを使ってオブジェクトを繰り返し表示するという技がある。
code:HLSL
float dSphere(float3 p) {
float m = length(mod(p, 4) - 2) - 0.5; // modを使って繰り返し
今回の例では、球を繰り返し表示している。ここでは座標pからmodで周期を作り出している。
シェーダーでは周期を作り出すのに、他に時間を三角関数に与えたsin(_Time.y)や、小数点以下の数字だけを取り出すfrac(p * 10)のようなやり方があるよ。
ちなみにfmodを使うと負の数がうまく扱えないので、何も考えずにmod関数をコピペして使いましょう。
カメラとか3つの軸とか言ってるのがぜんぜんわからん
code:HLSL
// カメラ
float3 cam = float3(0, 0, -3);
// 上、前、横
float3 up = float3(0, 1, 0);
float3 fwd = normalize(-cam);
float3 side = normalize(cross(up, fwd));
考えるんじゃない!感じるんだ!!
crossってなんですか
外積だよ!英語だとcross productだよ。
外積ってなんですか
考えるんじゃない。感じるんだ。
crossに2つのベクトルを渡すと、その両方に垂直なベクトルを計算して出してくれる。
これですべてだよ。まずはとにかく「そういうものなんだ」ということにしてしまおう。書きなれるうちに、理論は後からついてくる。
crossは「斜めにかけて引く」という計算をすると求めることができるよ。
https://gyazo.com/4045d6d0a4a4e62ca3e8346405fc6001
バンプマップを使ってモデルに立体感を持たせる時には、マップテクスチャについてcrossを使ってタンジェントスペースを求めたりするよ。
これはテクスチャの座標をワールド座標に変換するために、タンジェントスペースと呼ばれる3つの軸を組み立てる必要があるからだよ。
なぜこれで法線になるの……???
code:HLSL
float3 normal(float3 p) {
float EPSILON = 0.0001;
return normalize(float3(
map(float3(p.x + EPSILON, p.y, p.z)) - map(float3(p.x - EPSILON, p.y, p.z)),
map(float3(p.x, p.y + EPSILON, p.z)) - map(float3(p.x, p.y - EPSILON, p.z)),
map(float3(p.x, p.y, p.z + EPSILON)) - map(float3(p.x, p.y, p.z - EPSILON))
));
}
考えるんじゃない。感じるんだ。
聞くところによると、これを理解するには「陰関数」を知っておく必要があるみたいだ。
y = f(x)みたいな書き方をするのが陽関数。f(x, y) = 0みたいな書き方をすると陰関数っぽい。
https://www.youtube.com/watch?v=4RYDwr_J8PM
シェーダーではuとvの座標から色を決める。これは陰関数。
f(u, v) = color
イプシロン(EPSILON)とは何ですか?
とても小さな数字のことをイプシロンと呼ぶことがあるよ。
レイマーチングだとレイの衝突判定とか、法線推測で使うよ。定数で定義しちゃうと楽ちん。
const float EPS = 0.001;
UnityでC#を書く場合は、float.Epsilonで使うことができるよ。