0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

描いたRubyを読み取り、次の姿を生む — OUROBOROSのQuineを紐解く③

0
Posted at

Rubyのコードが輪の形に並び、実行するたびに次の形を生み出す。周期Quine「OUROBOROS」は、24世代で最初のソースへ戻る作品です。

公開作品はこちらです。

1本目では、自分のコードを再生成する仕組みを解説しました。2本目では、世代番号と数式から輪を作り、その中へコードを配置する処理を見てきました。

今回は、ブラウザで動く作品へつながる部分です。RubyをGLSLへ埋め込み、GPUでコードを描き、そのピクセルからRubyを復元して実行します。

一世代を進める経路

作品の世代交代は、次の順序で進みます。

現在のRubyを含むGLSL
  ↓ WebGL2でコードシートを描く
コードシートのピクセル
  ↓ 各文字に添えた8ビットの点列を読む
現在のRubyを復元
  ↓ ruby.wasmで通常実行
次の世代のRuby
  ↓ --shaderモードで実行
次の世代のGLSL
  ↓ glslkitで処理し、WebGL2で描く
次のコードシートと光の輪

最初のRubyとシェーダーは、起動時に読み込みます。その後、世代を進めるために実行するRubyは、コードシートのピクセルから復元したものです。

ソース文字列も比較用に保持しています。復元結果が現在のソースと一致することを確認してから、その復元結果をRuby処理系へ渡します。

Rubyに二つの出力モードを持たせる

生成されたRubyは、通常実行すると次の世代のRubyを出力します。

ruby public/ouroboros.rb > next.rb

もう一つの出力モードが --shader です。

ruby public/ouroboros.rb --shader > frame.frag

こちらは、その世代自身を描くためのGLSLを出力します。

実行方法 出力
通常実行 次の世代のRuby
--shader 現在の世代のRubyと描画処理を含むGLSL

世代交代では、まず現在のRubyを通常実行して次のRubyを得ます。続いて、次のRubyを --shader で実行し、次の姿を描くシェーダーを作ります。

GLSLのテンプレートは、圧縮されたデータとしてRuby本体に埋め込まれています。--shader ではそれを展開し、ソースのバイト列や世代番号などを差し込みます。

ソースを4バイトずつGLSLへ渡す

シェーダーには、Rubyのソース全体をデータとして持たせます。

Ruby側では、ソースをバイト列へ変換し、4バイト単位で32ビットの整数に詰めます。たとえば、先頭が次の4文字だったとします。

n=0x

各文字のバイト値は、次の通りです。

n    =    0    x
6e   3d   30   78

先頭のバイトを整数の下位側へ入れると、次の値になります。

0x78303d6eu

この値を、GLSLの uint 配列へ並べます。

const uint SRC[...]=uint[](...);

ソース末尾が4バイトに満たない場合は、ゼロを足して詰めます。元のソース長は LEN として別に保持するため、埋め合わせのゼロをソースとして扱うことはありません。

GLSL側では、次の関数で任意の一バイトを取り出します。

uint B(int i){
  return i>=0&&i<LEN
    ? (SRC[i>>2]>>(8*(i&3)))&255u
    : 0u;
}

i >> 2 で配列の何番目かを求め、i & 3 でその整数内の何バイト目かを求めます。必要な分だけ右へシフトし、255u とのANDで下位8ビットを取り出します。

この B(i) を使うと、コードの表示にも光の輪の文字にも、同じソースデータを使えます。

読むためのコードシートを描く

シェーダーは、u_mode に応じて描画を切り替えます。

モード 描くもの
0 ソースのバイト値をRGBへ出すデータ用の描画
1 作品として表示する光の輪
2 Rubyを表示・復元するコードシート

実際の世代交代で使うのは、モード2のコードシートです。

コードシートでは、一文字に横8ピクセル×縦9ピクセルの領域を割り当てています。

縦の位置   内容
0〜5       5×6ピクセルの字形
6          余白
7          文字のバイト値を示す8ビットの点列
8          余白

字形は、人がソースを見るためのものです。復元処理は、下に添えた8ビットの点列を読みます。

字形だけから読む場合、空白や改行を扱う必要があり、似た文字の判別も必要になります。各文字のバイト値を点列として描けば、これらを同じ方法で復元できます。

この作品で生成するRubyソースはASCIIで構成されています。そのため、一文字を一バイトとしてコードシートへ配置できます。

一文字を8個の点で表す

点列を描く処理は、この一行です。

if(gy==7)ink=float((c>>gx)&1u);

c は文字のバイト値、gx は文字領域内の横位置です。gx が0から7へ進むと、下位ビットから順に、そのビットが1か0かを調べます。

たとえば、A の値は65です。

通常の二進表記:01000001
描画する順序:  10000010

コードシートでは、左側を最下位ビットにしています。1の場所には明るい点を、0の場所には背景色を描きます。

空白は32、改行は10です。字形として見えない文字にも、その値に対応する点列があります。

各行には、文字の後に改行の点列も描きます。一行の最大幅200文字に改行の一文字分を加え、コードシートは201文字分、横1608ピクセルで描画しています。

GPUから表示Canvasへピクセルを渡す

コードシートは、WebGLのフレームバッファに接続した RGBA8 テクスチャへ描きます。

処理の流れを抜き出すと、次のようになります。描画先の設定などは省略しています。

gl.disable(gl.DITHER);
set('u_mode', 2);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

readPixels で、GPUが描いた結果をバイト配列として受け取ります。

WebGLの読み出しは左下を原点とし、表示用のCanvasでは左上を原点として扱います。そのため、行の順序を上下反転してから、2D Canvasへ putImageData で転送します。

ソース画面に表示されるコードシートは、この描画結果です。

表示した点列からRubyを復元する

復元時には、表示用Canvasの getImageData でピクセルを読み直します。

一文字分の復元処理は、次のようになっています。

let byte = 0;

for (let bit = 0; bit < 8; bit++) {
  const offset = ((row * 9 + 7) * w + col * 8 + bit) * 4;
  if (pixels[offset] > 128) byte |= 1 << bit;
}

row * 9 + 7 は、対象行の点列がある縦位置です。col * 8 + bit は、対象文字の各ビットがある横位置です。

RGBAの4成分のうち赤の値を調べ、128より大きければ1として組み立てます。コードシートでは、明るい点と暗い背景の赤成分が、この閾値を挟むようになっています。

復元した値が10なら改行なので、その行の読み取りを終えます。想定していないゼロや、改行を見つけられない行はエラーにします。

最後に、集めたバイト列をUTF-8としてデコードします。

return new TextDecoder('utf-8', { fatal: true })
  .decode(new Uint8Array(bytes));

この読み取り処理は、GLSLの SRC 配列や保存済みソース文字列を参照しません。文字の値は、表示Canvasのピクセルから組み立てています。

復元したRubyをブラウザ内で実行する

Rubyの実行には、ブラウザ内で動く ruby.wasm を使っています。この作品に組み込んだ処理系はCRuby 4.0です。

実行時には、Rubyの標準出力を StringIO へ差し替えます。そこへ復元したRubyの出力を受け取り、次の世代のソースとして取り出します。実行後には標準出力と ARGV を元に戻します。

世代交代の中心部分を抜き出すと、次の形です。

const drawn = checkCurrent();
const next = execute(drawn);
const built = bundle(execute(next, true));

install(built.fragment, built.manifest);
source = next;
drawCode();
checkCurrent();

checkCurrent() はピクセルからRubyを復元し、保持している現在のソースと一致するかを確認して、その復元結果を返します。

execute(drawn) で次のRubyを得て、execute(next, true) では --shader を付けて次のGLSLを作ります。新しいシェーダーを導入した後、コードシートを描き直し、その描画結果も検証します。

JavaScriptは処理の受け渡しを担当します。ソースに含まれる世代番号を進めるのは、実行されたRuby自身です。

glslkitは毎世代のシェーダーを処理する

Rubyが生成したGLSLは、ブラウザ内のglslkitへ渡します。

bundle = Glslkit::Bundle.build(
  resolver: resolver,
  name: 'ouroboros',
  vertex: 'ouroboros.vert',
  fragment: 'ouroboros.frag',
  line_directives: false
)

ここで前処理・検証・リフレクションを行い、処理済みのシェーダーとmanifestを取得します。その後、WebGLでコンパイルとリンクを行います。

manifestには、uniformを更新するためのsetterが含まれています。JavaScriptはその情報から uniform2fv などを呼び、画面サイズやポインター位置、描画モードを設定します。

glslkitは初期シェーダーの配信にも使っていますが、作品の動作中も毎世代、新しく生成されたGLSLを処理しています。

光の輪も世代番号から変わる

作品画面には、48本の光の糸で作った輪が表示されます。その糸に描く文字も、ソースデータを取り出す B(i) から取得しています。

絵の変化に使う位相は、シェーダー内の世代番号 PHASE から作ります。

float t=float(PHASE)*6.2831853/24.;

この t が、糸のうねりや色、文字の配置などを変えます。世代交代で新しいGLSLが導入されると、次の姿になります。

画面サイズとポインター位置も描画へ渡しているため、見え方はそれらにも影響されます。同じ画面サイズとポインター位置で比較すれば、世代が一周すると描画に使う位相とソースデータも元に戻ります。

一周後に何を確かめるか

「一周を検証」では、ピクセルからの復元とRubyの実行を24回繰り返し、終了時のソースが開始時のソースと一致するかを調べます。

加えて、各世代で描いたコードの復元結果を、その世代のソースと比較しています。

確認するのは、見た目が似ていることだけではありません。改行や空白を含め、次の実行へ渡すコードが一致していることです。作品で生成するソースはASCIIなので、この文字列の一致はソースのバイト列の一致にも対応します。

リポジトリには、Chrome / WebGL2でのブラウザ検証結果も記録しています。Safari / Firefoxでの動作は未検証です。

自分を描き、その描画から次を生む

3本を通して、OUROBOROSを構成する処理を見てきました。

  • コード本体を保持し、外枠を付けて再生成する。
  • 世代番号から輪の形を計算し、本体を配置する。
  • そのソースをGPUで描き、ピクセルから復元して実行する。

Quineとしての自己再生成は、Rubyが担っています。WebGLはそのコードを描き、JavaScriptは描画結果からコードを復元して、Ruby処理系へ渡します。

自分の中にあるコードを描き、その描画から次の自分を生み出す。24世代で閉じるRubyの循環に、描画と読み取りの経路を組み込んだ作品です。

参考・ソースコード

実装を読む場合は、RubyとGLSLを生成する build.rb、描画を担当する scene.frag.in、ピクセルの復元と世代交代を担当する public/gallery.js の順に追うと、この記事の流れと対応します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?