レイマーチング
基底ベクトルrightの向きが書籍では反転してたらしく、写真と結果が異なるので注意
メモ
シェーダの中で _ScreenParams を使う事で、スクリーン座標が取れる。
aspect = _ScreenParams.x / _ScreenParams.y でアスペクト比作ったり
UVの値域を0~1から-1~1にする事を、「正規化」と呼んでる
UnityのFOVは縦が基準らしく、width / heightで求める。
これをxに乗算して、正方形のUV空間をディスプレイに合わせてる
最初のレイマーチング
「レイマーチング」と称している処理の箇所
code:shader
// レイマーチング
float t = 0.0;// レイの進んだ距離
float3 p = cameraOrigin;// レイの先端の座標
int i = 0;// レイマーチングのループカウンター
bool hit = false;// オブジェクトに衝突したかどうか
for (i = 0; i < 99; i++)
{
float d = map(p);// 最短距離を計算します
// 最短距離を0に近似できるなら、オブジェクトに衝突したとみなして、ループを抜けます
if (d < 0.0001)
{
hit = true;
break;
}
t += d;// 最短距離だけレイを進めます
p = cameraOrigin + ray * t;// レイの先端の座標を更新します
}
始点と物体との距離を計算(float d = map(p);)して、その距離が閾値以下であれば衝突したと取り扱う考え方
物体との距離が閾値以下であれば、その距離の分、始点を動かす。
この時、動かす距離は、物体との距離だが、動かす方向は、レイの方向を維持する事に注意(物体との距離ベクトルを加算する訳ではない事に注意
float3 y = cross(z, x)的な感じ
どうも、この本が「レイ」と称している物は、自分が持ったイメージと異なるっぽい。
僕の世界観だと「レイの向き」と呼びそうな概念を「レイ」と呼称してる(ように感じる)
実際、「レイ」はnormalizeされている値で、
カメラの向きから作った基底ベクトルにUVの値を掛け合わせて作成してる code:shader
float3 ray = normalize(
right * uv.x +
up * uv.y +
forward / tan(fov / 360 * PI)
);
良く見たら足し算してるけど何だこれ
基底ベクトル(forward, right, up)は、カメラの向きが基準になるので、座標系のx,y,zとは一致しない。
right方向だからと言って、x軸の値にしか影響しない訳ではない
ベクトルの加算を使って、レイの向きを作ってる(そしてそれを単位ベクトルにして「レイ」とする) forwardの計算何だこれ
forward方向の計算はFOVに影響を受ける
https://gyazo.com/f44bc60c2a930529e37d94b79d2148ef
FOVによる、投影面の幅と高さの変化を、正規化したい
https://gyazo.com/fa085cb7a05d3524df466f7657b8312f
https://gyazo.com/ffab12fb2eb48da2abad6798f14388dc
ーーー余談
三角の高さの取得ってsinじゃなかったっけ
https://gyazo.com/819780a215a840e27365faedf78af96b
―――余談おわり
後は単に比率の問題で、どのFOVでも高さ(と言うか right * uv.xとup * uv.y)の値域が0~1になるようにfrontをtan(θ)で割る
ただし、上の例(ピタゴラスのてーり)だと、カメラの視界の上半分に限った話をしているので、計算に使う角度Θは、FOVの半分になる。
加えて、FOVはDegree(度)なので、Radianに変換する
その結果、front / tan(fov / 360 * PI) になる。
し、しんどい
法線の計算
code:calcNormal
float3 calcNormal(float3 p)
{
float eps = 0.001;
return normalize(float3(
map(p + float3(eps, 0.0, 0.0)) - map(p + float3(-eps, 0.0, 0.0)),
map(p + float3(0.0, eps, 0.0)) - map(p + float3(0.0, -eps, 0.0)),
map(p + float3(0.0, 0.0, eps)) - map(p + float3(0.0, 0.0, -eps))
));
}
法線ベクトルの求め方はここを参考にするのだそう
例題1 y=x^2 の (-1,1) における法線ベクトルを計算せよ。
で早速間違えたんだけど、$ y=x^2を$ y-x^2=0に変形して、$ (2, 1)だと思ったら間違えた。
正解は$ x^2-y=0に変形して$ (-2, -1)なのだそう
・・・それは何処を見れば分かるん? 移項にルールかなんかあんの?
移項の方向によって法線ベクトルの向きが変わる。ある線分における法線ベクトルは、表向きと裏向きの2つあるので、「法線ベクトルを求める」と言う文脈ではどっちでも良いのかもしれない(と、とりあえず思っておく)
分かったわ
そもそも関数の法線の公式が(微分表現は読む数学の微分対象の式を入れ込んだやつ参照) $ f(x, y) = 0 で表される曲線の $ (a, b) における法線は $ (\frac{d}{dx}f(a), \frac{d}{dy}f(b)) である
なので、まず$ y=x^2をこねくり回して$ f(x, y)=x^2-yな形式にしなきゃいけない。
は???? f(x, y)どうやって突っ込んだ????
整理する
そもそも例題で提示されてる$ y=x^2は関数である 法線の公式で使ってる$ f(x, y)=0って書き方は2変数関数と呼ぶ 2変数関数の良く見る書き方は$ z=f(x, y)
なので、$ y=x^2を$ z=x^2-yに変形する事で、やっと$ z=f(x, y)と差し替え出来るようになる
なんかもにょる。いきなりz出てきて良いんだっけ
1変数関数$ y=x^2を2変数関数に暗黙的にコンバートしてるからキモい
おてあげ
自分がやったのは$ y-x^2=zの形で、これは関数の(形|表現)として間違っている(?)
・・・という理解でいいのかなぁ
これを$ f(x, y)=0に突っ込むから$ x^2-y=0になる
プログラム上における微分の実装に関しては、上のサイトの「法線ベクトルの公式の導出」を参照
内積による単純なライティング
code:lighting
if(hit){
float3 normal = calcNormal(p); // 法線
float light = normalize(float3(-1, 1, -1)); // 平行光源の方向ベクトル
col = saturate(dot(normal, light)); // 内積でライティング(拡散反射)
}else{
col = float3(0, 0, 0);
}
法線の向きと光源の向きで、ベクトルの内積を取る事で、どれだけ平行に近いかを見る。 平行に近いほど値が大きくなる → その値を色に使ってるので光が当たってる面が明るくなる
内積による値の大きさは光源ベクトルと同じ向きであるほど大きいから、明るさは逆じゃない?
光源の方向ベクトルを、光源の位置ベクトルだと捉えると、道理に沿ってる気がする
max(0, min(1, value)) つまり0~1にClampしてるらしい
グラボによっては専用のプロセッサ?があるらしく、計算が速い
プロセッサが無いと遅くなる
saturateはclampとは違うの?
clampは値域を指定できる clamp(value, 0, 1)
saturateは値域を0-1にしか出来ない代わりに、グラフィックのサポートを受けられる(DirectXとかOpenGLとか)
床をシーンに追加
黙ってカメラ位置変更するのはどうかと思う
code:sdPlane
// nは正規化された法線(表面の向き)、hは原点からの距離
float sdPlane(float p, float3 n, float h)
{
return dot(p, n) + h;
}
レイの進んでる向きが、平面の法線と互い違いになってる具合を距離関数としてる
法線を回転させて平面の裏を見ようとすると、真っ暗になる(向きが同じなので距離が減らない
原点からの距離をh < -1にすると常に衝突してるので真っ白になる。h > -1までなら平行移動してくれる
多分内積の値域が-1~1だから?
平面の原点からの距離の効果があまり感じられないけどどうなんだろう
平面が無限に広がってるので、平面を上下させても地平線は変わらない。
球をめり込ませると、ちゃんとめり込むので、効果はある
マテリアル分けと、高度なライティング
オブジェクト毎に異なるマテリアルを割り当てるには
衝突検出後、改めて、各オブジェクトとの衝突判定を取り、具体的に何に当たったかを求める
code:shader
if(hit)
{
// ライティングのパラメーター
float3 normal = calcNormal(p); // 法線
float3 light = _WorldSpaceLightPos0; // 平行光源の方向ベクトル
// マテリアルのパラメーター
float3 albedo = float3(1, 1, 1); // アルベド
float metalness = 0.5; // メタルネス(金属の度合い)
// ボールのマテリアルを設定
if(dBall(p) < 0.0001)
{
albedo = _BallAlbedo;
metalness = 0.8;
}
// 床のマテリアルを設定
if(dFloor(p) < 0.0001)
{
float checker = mod(floor(p.x) + floor(p.z), 2.0);
albedo = lerp(_FloorAlbedoA, _FloorAlbedoB, checker);
metalness = 0.1;
}
.......続く
ライティング用のパラメータ
normal
レイが衝突した地点における法線ベクトル
light
平行光源の方向ベクトル
マテリアルのパラメータ
albedo
ざっくりマテリアルの色。
metalness
金属具合
0で非金属、1で金属
衝突した点を改めて各距離関数に食わせる事で、どのオブジェクトに衝突したのかを判定する
code:shader
if(dBall(p) < 0.0001) { /* ボールに衝突 */ }
if(dFloor(p) < 0.0001) { /* 床に衝突 */ }
チェッカー柄を作る計算式
code:shader
float checker = mod(floor(p.x) + floor(p.z), 2.0);
albedo = lerp(_FloorAlbedoA, _FloorAlbedoB, checker);
modって何
要するに「割り算の余り」
負の数で余りを求める($ -5\bmod 2)と、結果も負数になるが、この振る舞いは処理系依存
数学の世界では正の数になるらしい
このプログラムでは、自前で必ず正の数になる剰余演算を作ってる
code:mod
float mod(float x, float y){
return x- y * floor(x / y);
}
後はチェック柄演算の通りで、checker変数には0か1が入るので、その値に基づいてalbedoを選択してる
ライティングの計算
参考資料
diffuse
float diffuse = saturate(dot(normal, light));
拡散反射
このプログラムでやってる、法線ベクトルと光源に向かうベクトルとの内積の値を使った拡散反射を、ランバート反射と呼ぶ specular
float specular = pow(saturate(dot(reflect(light, normal), ray)), 10.0);
鏡面反射
reflectとは、入射ベクトルと法線ベクトルから、反射ベクトルを計算する組み込み関数 反射ベクトルと、視線ベクトル(カメラから見たベクトル)の一致具合(内積)で反射光の強さを決める
ここでは、rayがまさに視線ベクトルそのものなので、代用してる
powはべき乗の計算。pow(2,10)で$ 2 ^ {10} になる
ハチャメチャにデカい数字になる気がするけど、前段階でsaturateで値域を0~1にしてるので、イメージ的には値の変化の滑らかさをシャープにしてる
ao(アンビエントオクルージョン)
奥まってる所が暗くなる表現
本の中でもあまり解説されてないので・・・これは・・・ブラックボックスだなぁ・・・
shadow
影の計算。
衝突地点から光源に向けてレイマーチして、障害物があるか調べる
これもほぼブラックボックス・・・
各色の合成
code:shader
col += albedo * diffuse * shadow * (1 - metalness); // 直接光の拡散反射
col += albedo * specular * shadow * metalness; // 直接光の鏡面反射
col += albedo * ao * lerp(_SkyBottomColor, _SkyTopColor, 0.3); // 環境光
fogの計算
code:shader
float invFog = exp(-0.02 * t); // tはレイマーチングした時の「レイの進んだ距離」
col = lerp(_SkyBottomColor, col, invFog);
expは引数が0だと1になる。便利げ
トーンマッピング
シーンの設定によっては、ライティングの計算結果が1を上回る場合がある
トーンマッピングによって、HDR画像をLDR画像に変換する
code:shader
float3 acesFilm(float3 x)
{
const float a = 2.51;
const float b = 0.03;
const float c = 2.43;
const float d = 0.59;
const float e = 0.14;
return saturate((x * (a * x + b)) / (x * (c * x + d) + e));
}
col = acesFilm(col * 0.8); // カメラの露出のマイナス補正として0.8
ガンマ補正
ディスプレイのRGB出力を、線形に近づけるための補正
code:shader
col = pow(col, 1 / 2.2);
一般的なディスプレイでは、RGBの強さはそのまま出ず、少し暗くなるらしい。それを打ち消すための処理
Unityのカメラとの同期
カメラの位置は _WorldSpaceCameraPosで取得出来る
レイに関しては、クリップ空間からMVP変換行列の逆行列を乗算する事で、ワールド空間にまで持って行く
オブジェクト空間
資料によってはモデル空間とも。(個人的にはモデル空間の方が馴染みが有る)
オブジェクトの中心を原点とした空間
ワールド空間
所謂ワールド座標系
「世界の中心」を規定して、その相対距離で座標が決まる世界
ビュー空間
カメラを中心とした空間。カメラ空間とも
クリップ空間
視錐台を考慮した変形をした空間
0~1の立方体をした空間になる(?)
個人的には座標系と呼んでた物の事を、空間と呼んでる
code:shader
// カメラの座標(ワールド空間)
float3 cameraOrigin = _WorldSpaceCameraPos
// カメラ行列からレイの生成
float4 clipRay = float4(uv, 1, 1); // クリップ空間におけるレイ
float3 viewRay = normalize(mul(unity_CameraInvProjection, clipRay).xyz); // ビュー空間におけるレイ
float3 ray = mul(transpose((float3x3)UNITY_MATRIX_V), viewRay); // ワールド空間におけるレイ
Unityのライトとも同期をとる
_WorldSpaceLightPos0でUnityの平行光源の方向ベクトルが拾える
_LightColor0でUnityの平行光源の色が拾える。色の合成時に、diffuseとspecularの合成に混ぜると良い
レイマーチング用のQuadのカリング防止
現状、Sceneの中央に置いたQuadの描画時に(フルスクリーンな)レイマーチングが実施されている
そのため、Quadが描画領域外に出ると、レイマーチングで出てる絵も消える(カリングされる
スクリプトで超巨大なバウンディングボックスを持ったメッシュを生成して、常にカメラの描画領域内にQuadが存在するようにする
(前提知識)Unityのメッシュには、実際の頂点位置とは別に、描画立方とも呼ぶべきバウンディングボックスが存在している。Unityのカメラが、この見えないボックスを視界にとらえていれば、描画が実行される
↑みたいなMeshを作る、エディタスクリプトを書く
レイトレーシングによる鏡面反射
レイマーチして衝突が確認できた後、(ほぼ)その地点から、レイと衝突点との反射ベクトル方向に再度レイマーチングする。それぞれのレイマーチした結果の色を合成して、鏡面(鏡のような)反射を実現する
反射マーチの開始地点は、衝突近似値(このコードの場合0.0001)より十分離した場所からやらないと、即再衝突する
本当は再帰的に実施すべきだけど、シェーダーでは再帰呼び出しが出来ないので、3回のループで実現している
色の合成は、反射の減衰率 reflectionAttenuationで調整する。基本的には、反射するほど減衰率が上がっていく(=色に対する影響力が下がる)
反射の減衰率の計算
最初は float3 reflectionAttenuation = float3(1, 1, 1);
reflectionAttenuation *= albedo * fresnelSchlick(f0, dot(ref, normal)) * invFog;
素材の色が暗いほど、フレネル反射率が低いほど、遠くにあるものほど、減衰する
フレネル反射率ってなんやねん
色々バリエーションがある
フレネル項のSchlick近似
(光の入射角が0度の時の)素材の反射率と、光の入射角によって、その時の反射率を計算する
入射角が深くなればなるほど、反射率が上がる
海や湖を眺めると、近くのほうほど水面の底が見えやすく、遠くのほうほど空が反射します
距離関数の応用/無限複製編
距離関数に割り算の余りを使った計算を加えると、無限複製出来る
code:shader
float2 opRep(float2 p, float2 interval)
{
return mod(p, interval) - interval * 0.5;
}
float dBalls(float3 p)
{
p.xz = opRep(p.xz, _BallRepeat);
return sdSphere(p - float3(0, 1, 0), 1);
}
やってる事は割り算の余りを使ったリピートそのもの
- interval * 0.5は原点を中央にしたいから
リピートのどのグリッドに該当するのか
float2 grid = floor(p.xz / _BallRepeat);
割り算の方を使って、値を見る。小数点以下は同じグリッドに属しているので切り捨てる
HSVからRGBへの変換
code:shader
float3 hsvToRgb(float3 c)
{
float4 = K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y);
}
これはどっか専用にまとめたいなぁ
距離関数の応用/ブーリアン演算による合成
https://gyazo.com/4be568e0ab578eef62de5e24e3b04f49
距離関数のブーリアン演算を例示する。
この、ブーリアン演算によるモデリングを、ConstructiveSolidGeometry(CSG)表現 と呼ぶらしい
https://gyazo.com/7c9a89819608fb0421ad3bd7df75bf28
和集合(Union)
両方出す演算
code:shader
float opUnion(float d1, float d2)
{
return min(d1, d2);
}
単純に複数のモデルを並べる時も、minによる演算を利用する
積集合(Intersection)
重なった所だけ残す演算
code:shader
float opIntersection(float d1, float d2)
{
return max(d1, d2);
}
差集合(Subtraction)
片方のモデルを、もう一方のモデルでえぐり取る演算
code:shader
float opSubtraction(float d1, float d2)
{
return max(d1, -d2);
}
渡す距離の順番で、えぐられる数、えぐる数、みたいなのが決まる