"Decoding A City In A Bottle"を読んで
2022年4月に以下のツイートが一部界隈で話題になったようです。筆者は今更知りました。
A City in a Bottle 🌆
— Frank Force 🌻 (@KilledByAPixel) 2022年4月22日
<canvas style=width:99% id=c onclick=setInterval('for(c.width=w=99,++t,i=6e3;i--;c.getContext`2d`.fillRect(i%w,i/w|0,1-d*Z/w+s,1))for(a=i%w/50-1,s=b=1-i/4e3,X=t,Y=Z=d=1;++Z<w&(Y<6-(32<Z&27<X%w&&X/9^Z/8)*8%46||d|(s=(X&Y&Z)%3/Z,a=b=1,d=Z/w));Y-=b)X+=a',t=9)> pic.twitter.com/N3WElPqtMY
このコードは、以下のようなビル群っぽい風景をレイトレーシングした動画を生成するGenerative Artです。
元ツイートには実際の動画が貼り付けられていますので、ぜひ見てみてください。
有志の方々の解析
何人かの方が、読みやすく書き直したコードを紹介しています。
大まかにまとめると、A City In A Bottleは以下の処理を行なっているようです。
setInterval
で連続して描画処理を呼び出す。呼び出し毎にカメラ位置を右にずらす。- 一重ループでカメラの各ピクセル方向ごとにレイを飛ばす。ループ変数を除算と剰余で二次元の方向に分解することで、一重ループで二次元の走査を実現している。
- 一重ループでレイ=直線の媒介変数を少しずつ動かす。動かす毎に建物との衝突検知を判定する。
- 建物に衝突していたら窓枠っぽい模様を塗る。
- 一重ループでレイ=直線の媒介変数を少しずつ動かす。動かす毎に建物との衝突検知を判定する。
- 一重ループでカメラの各ピクセル方向ごとにレイを飛ばす。ループ変数を除算と剰余で二次元の方向に分解することで、一重ループで二次元の走査を実現している。
なお、座標系は左手系です。すなわち左右がx、高さがy、奥行きがzです。
筆者による解析(?)
レイの衝突判定に記述されている建物の形状を固定カメラで描画してみました。
以下は"Decoding A City In A Bottle"の解析の一部コードを少し改変したものです。
const GROUND_PLANE = 6; const CITY_DISTANCE = 32; const AVENUE_WIDTH = 27; const AVENUE_PERIOD = 99; const BUILDING_WIDTH = 9; const BUILDING_DEPTH = 8; const BUILDING_HEIGHT = 45; function buildingHeight(X, Z) { return (CITY_DISTANCE<Z&AVENUE_WIDTH<X%AVENUE_PERIOD&&X/BUILDING_WIDTH^Z/BUILDING_DEPTH)*8%(BUILDING_HEIGHT+1); }
ビルの高度マップ 3Dバージョン
x-z毎に高さがbuildingHeight
のキューブを並べて描画するThree.jsのコードです。
See the Pen
Decoding "A City In A Bottle", 3D version by Imai (@imai1)
on CodePen.
ビルの高度マップ 2Dバージョン
x-z毎にbuildingHeight
で色分けしてキャンバスにfillRect
で色付けしたものです。
See the Pen
Decoding "A City In A Bottle", 2D version by Imai (@imai1)
on CodePen.
ビルの高度マップ ランダムバージョン
以下のように、Math.rand
を使って生成した通常の乱数でも似たような結果になるようです。
const buffer = new Array(30); for (let z = 0; z < buffer.length; ++z) { buffer[z] = new Array(buffer.length); } function getBuffer(x, z) { const X = Math.floor(x / BUILDING_WIDTH); const Z = Math.floor(z / BUILDING_DEPTH); if (buffer[Z][X] == undefined) { buffer[Z][X] = (Math.random() - 0.1) * 10000; } return buffer[Z][X]; } function buildingHeight(X, Z) { return (CITY_DISTANCE<Z&AVENUE_WIDTH<X%AVENUE_PERIOD&&getBuffer(X, Z))*8%(BUILDING_HEIGHT+1); }
See the Pen
Untitled by Imai (@imai1)
on CodePen.
反対に言えば、多次元座標の各成分のXORをとると簡易的な乱数になるようですね。周期は短いのかもしれませんが、このような用途では便利そうです。