[コードリーディング vol.4] Cubeの折りたたみと平面によるくり抜きでかっこいい表現

## 概要

2013年の記事と、だいぶ古いものですが比較的シンプルだけどかっこいい表現だったので、以下の記事で紹介されているコードをコードリーディングしてみたいと思います。
（[@i_saintさん](https://twitter.com/i_saint)に許可をいただいて掲載しています。ちなみに[@i_saint](https://twitter.com/i_saint)さんのTwitterの背景は今回紹介するもののモノクロ版でした）

- [raymarching for games](http://i-saint.hatenablog.com/entry/2013/08/20/003046)

最終的な絵はこんな感じ↓
![最終結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/8ca0a434-6a2f-ad2b-09dc-8a6abf7d6a93.png)
コード量からするとそこまで多くないですがかっこいいですよね。

しかも色々と使えそうなテクニックが盛り込まれているので読む価値ありです。

ちなみにこれを参考にして自分の理解の範囲で作ったのが以下の作品です↓
![Pile space](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/c6f19298-8b10-543c-29cb-bc864dd86ac9.png)

- [Pile space](https://www.shadertoy.com/view/tljGRG)

さて、最初の状態はボックスをXZ平面に繰り返し配置するところから始まります。

![20190608-221418.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/534e2417-3ce1-51a6-ccc9-c3087ddb9f35.png)

```glsl
#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 でよく見るオブジェクトの輪郭付近が発光してオーラが出てるようなエフェクトを実現できます。

具体的に言うと以下の部分です。

```glsl
float fog = min(1.0, (1.0 / float(MAX_MARCH)) * float(march))*1.0;
```

マーチ回数を利用するっていうのがスマートですね。


### map関数

さて、さっそく`map`関数がどうなっているか見てみましょう。

```glsl
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サイズ  |

という意味でしょう。

また、ひとつのトリックとして

```glsl
p = -abs(p);
```

があります。`abs`は（XY平面だけで考えた場合に）第一象限だけに絞るときによく使われる方法ですね。（そしてXYZ空間でも考え方は同様）

そしてさらに`abs`の頭にマイナスがついています。これは第一象限ではなく第三象限に変換していると考えればいいでしょう。
（第三象限は`x`も`y`もマイナス）

ただこれはおそらく、以下の理由からマイナスにしているものと思われます。

元の計算はこうでした。

```glsl
float d1 = p.y + h;
```

これは、

```glsl
p = abs(p);
float d1 = h - p.y;
```

と考えても同様の意味になります。
つまり、`h`が平面との距離を表しており、そこから現在の位置を引く、と考えます。それを簡易化するために`-abs(p)`なのだと思います。

第一象限版を図にすると以下のような感じです。（XY平面だけに絞った図です）
![壁のイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/1c3a02f2-e061-266d-7c4d-8baeecd8e729.jpeg)

赤の斜線部分がXの壁、緑の斜線部分がYの壁です。
そしてそれぞれ、Xの壁との距離が`1.0`、Yの壁との距離が`0.8`です。
距離の計算は、それぞれの値から`P`の値を引いて計算しています。

--------------------------------------------

### 余談

これを利用して、XYZ平面すべてに同様の計算を行うと以下のようにシンプルな形で記述できます。

```glsl
p = abs(p);
    
vec3 d = vec3(0.5, 0.5, 0.3) - p;
return max(d.x, max(d.y, d.z));
```

これを視覚化すると以下のようになります。

![視覚化](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/1cdfe50f-2b72-4c07-a7d7-b4d4b68f9bd6.gif)
Shadertoyにも上がっています↓
- [XYZ Plane distance function](https://www.shadertoy.com/view/tlBGRK)

--------------------------------------------


前後してしまいますが、最後のところで`max(c1, d1)`としていることから、中心から離れた位置にある平面より向こう側にあるオブジェクトを描画している、というわけですね。
（`max`関数による合成はAND合成）

さて、平面については以上です。それ以外にもCubeを複数描いています。

Cubeの複数描画は、`p`を空間の繰り返しとして再定義します。以下の部分ですね。

```glsl
vec2 p1 = mod(p.xz, vec2(grid)) - vec2(grid_half);
```

上の余談で話したものはXYZ平面それぞれについて計算したものでした。
これのXZ平面のみで実行したのが`float d1 = p.y + h;`です。
つまり、上下の天井と床部分の距離関数、ということですね。

最後の`max(c1, d1)`によって繰り返されたCubeと天井・床部分がAND合成され、上のような絵が描かれる、というわけですね。


## さらに反復を入れる

さて、次に書かれているのが、上記のCubeの繰り返しに、さらに別の繰り返しをAND合成することで、さらに見た目をよくしていきます。

こんな感じ↓
![20190608-224841.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/00d596bd-4735-7f87-7ae7-ad1a64ffb135.png)

さらにかっこよくなりました。

`map`関数以外は大きく変わっていないので、`map`関数だけ見ていきましょう。
（利用されていない変数などは削除してあります）


```glsl
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に対して乱数を加え、それぞれの位置を少しずつずらす、という方法です。
適用すると以下のような絵になります。

![乱数を加える](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/20b1094f-2d5f-7397-2f5c-ce6e5daf6184.png)

これも、`map`関数内のみの変化なので抜粋。


```glsl
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));
}
```

さて、乱数を生成している部分だけを抜き出してみると以下の部分です。
（ちょっと分かりやすく整形してます）

```glsl
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](http://www.iquilezles.org/apps/graphtoy/)というサイトでグラフ化してみるとそれぞれ以下のようになりました。
（オレンジが`ceil(x / 0.4)`の結果、青が`floor(x / 0.4)`の結果です）
![グラフ化](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/8d0e1d19-cda9-cb41-d5a3-c5aa8feac5c2.png)

`floor`などは位置を特定するのに使われ、かつ乱数のシードとすることで毎回同じ値が取得できるので、こうした位置調整のための乱数としてはよく使われる手法ですね。

理由は上のグラフを見てもらうと一目瞭然ですが、`floor`によって**整数部分のみ**だけが取り出されるので、空間のIDとして機能するわけですね。
そしてそれをハッシュ関数などに渡すことでランダム性を出し、空間ごとに差をつけることができる、というわけです。


## まとめ

巨大な平面を利用した**空間を広げる**という手法は大いにヒントになりました。
繰り返しによって広大な空間をいとも簡単に描けるのがレイマーチングの魅力ですね。

次回の記事は、とあるイベントでライブコーディングVJをしてきたときに得た知見をまとめる記事を書く予定です。
そこでも色々なテクニックが学べたので興味がある人は次の記事もぜひ読んでみてください。