概要
2013年の記事と、だいぶ古いものですが比較的シンプルだけどかっこいい表現だったので、以下の記事で紹介されているコードをコードリーディングしてみたいと思います。
(@i_saintさんに許可をいただいて掲載しています。ちなみに@i_saintさんのTwitterの背景は今回紹介するもののモノクロ版でした)
最終的な絵はこんな感じ↓
コード量からするとそこまで多くないですがかっこいいですよね。
しかも色々と使えそうなテクニックが盛り込まれているので読む価値ありです。
ちなみにこれを参考にして自分の理解の範囲で作ったのが以下の作品です↓
さて、最初の状態はボックスをXZ平面に繰り返し配置するところから始まります。
#ifdef GL_ES
precision mediump float;
#endif
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
float sdBox( vec2 p, vec2 b )
{
vec2 d = abs(p) - b;
return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}
float map(vec3 p)
{
float h = 1.8;
float grid = 0.4;
float grid_half = grid*0.5;
float cube = 0.175;
p = -abs(p);
float d1 = p.y + h;
vec2 p1 = mod(p.xz, vec2(grid)) - vec2(grid_half);
float c1 = sdBox(p1,vec2(cube));
return max(c1,d1);
}
void main()
{
vec2 pos = (gl_FragCoord.xy*2.0 - resolution.xy) / resolution.y;
vec3 camPos = vec3(-0.5,0.0,3.0);
vec3 camDir = normalize(vec3(0.3, 0.0, -1.0));
camPos -= vec3(0.0,0.0,time*3.0);
vec3 camUp = normalize(vec3(0.5, 1.0, 0.0));
vec3 camSide = cross(camDir, camUp);
float focus = 1.8;
vec3 rayDir = normalize(camSide*pos.x + camUp*pos.y + camDir*focus);
vec3 ray = camPos;
int march = 0;
float d = 0.0;
float total_d = 0.0;
const int MAX_MARCH = 64;
const float MAX_DIST = 100.0;
for(int mi=0; mi<MAX_MARCH; ++mi) {
d = map(ray);
march=mi;
total_d += d;
ray += rayDir * d;
if(d<0.001) {break; }
if(total_d>MAX_DIST) {
total_d = MAX_DIST;
march = MAX_MARCH-1;
break;
}
}
float fog = min(1.0, (1.0 / float(MAX_MARCH)) * float(march))*1.0;
vec3 fog2 = 0.01 * vec3(1, 1, 1.5) * total_d;
gl_FragColor = vec4(vec3(0.15, 0.15, 0.2)*fog + fog2, 1.0);
}
コードはたったこれだけです。
最後のfog
とmap
以外は普通のレイマーチングなので割愛します。
ちなみにGrow感があるのは、記事から引用させていただくと以下のような計算を行っています。
ray を march した回数というのは、ray が図形に到達するまで (もしくは打ち切られるまで) にかかった step 数で、オブジェクトの輪郭スレスレを通りつつ交差しなかった場合とかに数が多くなります。この数に応じて色を足すと、4k intro でよく見るオブジェクトの輪郭付近が発光してオーラが出てるようなエフェクトを実現できます。
具体的に言うと以下の部分です。
float fog = min(1.0, (1.0 / float(MAX_MARCH)) * float(march))*1.0;
マーチ回数を利用するっていうのがスマートですね。
map関数
さて、さっそくmap
関数がどうなっているか見てみましょう。
float sdBox( vec2 p, vec2 b )
{
vec2 d = abs(p) - b;
return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}
float map(vec3 p)
{
float h = 1.8;
float grid = 0.4;
float grid_half = grid*0.5;
float cube = 0.175;
p = -abs(p);
float d1 = p.y + h;
vec2 p1 = mod(p.xz, vec2(grid)) - vec2(grid_half);
float c1 = sdBox(p1,vec2(cube));
return max(c1,d1);
}
sdBox
関数の引数がvec2
であることに注意してください。
これはXZ平面に対してのみ繰り返し、Y軸方向については無限遠でのBoxをレンダリングするためだと思います。
冒頭では各パラメータが宣言されています。それぞれ
変数 | 意味 |
---|---|
h | 平面との距離 |
grid | 空間の大きさ(繰り返すサイズ) |
grid_half | 空間位置をオフセットするための値 |
cube | cubeサイズ |
という意味でしょう。
また、ひとつのトリックとして
p = -abs(p);
があります。abs
は(XY平面だけで考えた場合に)第一象限だけに絞るときによく使われる方法ですね。(そしてXYZ空間でも考え方は同様)
そしてさらにabs
の頭にマイナスがついています。これは第一象限ではなく第三象限に変換していると考えればいいでしょう。
(第三象限はx
もy
もマイナス)
ただこれはおそらく、以下の理由からマイナスにしているものと思われます。
元の計算はこうでした。
float d1 = p.y + h;
これは、
p = abs(p);
float d1 = h - p.y;
と考えても同様の意味になります。
つまり、h
が平面との距離を表しており、そこから現在の位置を引く、と考えます。それを簡易化するために-abs(p)
なのだと思います。
第一象限版を図にすると以下のような感じです。(XY平面だけに絞った図です)
赤の斜線部分がXの壁、緑の斜線部分がYの壁です。
そしてそれぞれ、Xの壁との距離が1.0
、Yの壁との距離が0.8
です。
距離の計算は、それぞれの値からP
の値を引いて計算しています。
余談
これを利用して、XYZ平面すべてに同様の計算を行うと以下のようにシンプルな形で記述できます。
p = abs(p);
vec3 d = vec3(0.5, 0.5, 0.3) - p;
return max(d.x, max(d.y, d.z));
これを視覚化すると以下のようになります。
前後してしまいますが、最後のところでmax(c1, d1)
としていることから、中心から離れた位置にある平面より向こう側にあるオブジェクトを描画している、というわけですね。
(max
関数による合成はAND合成)
さて、平面については以上です。それ以外にもCubeを複数描いています。
Cubeの複数描画は、p
を空間の繰り返しとして再定義します。以下の部分ですね。
vec2 p1 = mod(p.xz, vec2(grid)) - vec2(grid_half);
上の余談で話したものはXYZ平面それぞれについて計算したものでした。
これのXZ平面のみで実行したのがfloat d1 = p.y + h;
です。
つまり、上下の天井と床部分の距離関数、ということですね。
最後のmax(c1, d1)
によって繰り返されたCubeと天井・床部分がAND合成され、上のような絵が描かれる、というわけですね。
さらに反復を入れる
さて、次に書かれているのが、上記のCubeの繰り返しに、さらに別の繰り返しをAND合成することで、さらに見た目をよくしていきます。
さらにかっこよくなりました。
map
関数以外は大きく変わっていないので、map
関数だけ見ていきましょう。
(利用されていない変数などは削除してあります)
float map(vec3 p)
{
float h = 1.8;
float grid = 0.4;
float grid_half = grid*0.5;
float cube = 0.175;
p = -abs(p);
vec3 di = ceil(p/4.8);
p.y += di.x*1.0;
p.x += di.y*1.2;
p.xy = mod(p.xy, -4.8);
float d1 = p.y + h;
float d2 = p.x + h;
vec2 p1 = mod(p.xz, vec2(grid)) - vec2(grid_half);
float c1 = sdBox(p1,vec2(cube));
vec2 p2 = mod(p.yz, vec2(grid)) - vec2(grid_half);
float c2 = sdBox(p2,vec2(cube));
return max(max(c1,d1), max(c2,d2));
}
冒頭のパラメータは同じですね。
そしてやや異なっているのが、ceil
を利用しているあたりです。
ceil
関数は、「切り上げ」を行う関数です。
例えば、2.3
という数値を入力すると3.0
が返ってきます。
コードでは4.8
というマジックナンバーが出てきていますが、その下の空間の繰り返し(mod
)でも同じ数字が使われているので、なにかしらの繰り返し用途でしょう。
そして、計算結果を利用してp
の位置を変更しています。
さて、最後の部分は前回のものと比べて似たようなコードが2回登場しています。
これはすでに説明した、XZ平面とさらにYZ平面に対しての「壁」を合成したもの、と考えることができます。
なのでd2
はp.x + h
と計算され、p2
についてはp.yz
と、YZ平面に対して繰り返しを適用しています。
そしてこれらをすべてAND合成した結果が上の絵、というわけですね。
Boxに乱数を加える
次に行っているのは、各Boxに対して乱数を加え、それぞれの位置を少しずつずらす、という方法です。
適用すると以下のような絵になります。
これも、map
関数内のみの変化なので抜粋。
float map(vec3 p)
{
float h = 1.8;
float rh = 0.5;
float grid = 0.4;
float grid_half = grid*0.5;
float cube = 0.175;
vec3 orig = p;
vec3 g1 = vec3(ceil((orig.x)/grid), ceil((orig.y)/grid), ceil((orig.z)/grid));
vec3 rxz = nrand3(g1.xz);
vec3 ryz = nrand3(g1.yz);
p = -abs(p);
vec3 di = ceil(p/4.8);
p.y += di.x*1.0;
p.x += di.y*1.2;
p.xy = mod(p.xy, -4.8);
vec2 gap = vec2(rxz.x*rh, ryz.y*rh);
float d1 = p.y + h + gap.x;
float d2 = p.x + h + gap.y;
vec2 p1 = mod(p.xz, vec2(grid)) - vec2(grid_half);
float c1 = sdBox(p1,vec2(cube));
vec2 p2 = mod(p.yz, vec2(grid)) - vec2(grid_half);
float c2 = sdBox(p2,vec2(cube));
return max(max(c1,d1), max(c2,d2));
}
さて、乱数を生成している部分だけを抜き出してみると以下の部分です。
(ちょっと分かりやすく整形してます)
vec3 g1 = vec3(
ceil((orig.x) / grid),
ceil((orig.y) / grid),
ceil((orig.z) / grid));
// これは単に vec3 g1 = ceil(origi / grid); でも同じ。
vec3 rxz = nrand3(g1.xz);
vec3 ryz = nrand3(g1.yz);
// ...中略 ...
vec2 gap = vec2(rxz.x*rh, ryz.y*rh);
float d1 = p.y + h + gap.x;
float d2 = p.x + h + gap.y;
重要なのはceil
を使って整数を得ることと、その値からnrand3
関数によってランダムな値を得ているところですね。
ちなみにこのceil
を使った計算はfloor
に似た計算となっています。
これをGraph Toyというサイトでグラフ化してみるとそれぞれ以下のようになりました。
(オレンジがceil(x / 0.4)
の結果、青がfloor(x / 0.4)
の結果です)
floor
などは位置を特定するのに使われ、かつ乱数のシードとすることで毎回同じ値が取得できるので、こうした位置調整のための乱数としてはよく使われる手法ですね。
理由は上のグラフを見てもらうと一目瞭然ですが、floor
によって整数部分のみだけが取り出されるので、空間のIDとして機能するわけですね。
そしてそれをハッシュ関数などに渡すことでランダム性を出し、空間ごとに差をつけることができる、というわけです。
まとめ
巨大な平面を利用した空間を広げるという手法は大いにヒントになりました。
繰り返しによって広大な空間をいとも簡単に描けるのがレイマーチングの魅力ですね。
次回の記事は、とあるイベントでライブコーディングVJをしてきたときに得た知見をまとめる記事を書く予定です。
そこでも色々なテクニックが学べたので興味がある人は次の記事もぜひ読んでみてください。