Help us understand the problem. What is going on with this article?

GLSLでテトリスをつくる

More than 1 year has passed since last update.

こんにちはphi16です。今年1月頃に作ってしまった謎のテトリスの経緯と実装のちょっとした解説をしようとおもいます。

それがこちら : https://www.shadertoy.com/view/4d33Dj
(見た目バグってますけど、最初にBufAタブ内のreturn 0;をコメントアウトしてください。稀にWebGLが落ちますけど。後述。) 2019年になったのでセーフティを抜きました ;D

image

Shadertoyについて

1日目に書かれていた通り一級品のシェーダがぽんぽん投稿されていくサイトです。 https://www.shadertoy.com
最初にGLSLだけでなんか作れそうだなぁ、と思って見つけてしまったのがコレだったので世界は広いんだなぁと実感しました。
意味のわからない激ヤバシェーダもいっぱいありますが、教育的な感じでわかりやすいコードのものもありますし。
意味のわからなくてもコードをパクることはできますし。コードを見て学べ、という感じですか。
まぁ一度は見たことあるであろうiq大先生のサイトに細かい解説が結構載っているので

Shadertoyで何か作ってみるのもいいとおもいます!

(ただトップページを開くだけでWebGLが落ちるのは・・・ほんとうに・・・どうにか・・・)(技術的に難しそう)

なんでテトリスを作ってしまったのか

私は前からちょっとShadertoyでちょこちょこしたものを作るのが好きだったのですが、いろんなものを表現できるとはいえゲームを作るには限界がありました。値の保存とか出来ないし。
(glslsandboxでは一応backbufferがあるようなので不可能ではなさそうですね・・・?)

そんなことを考えていたら1/4に謎の投稿が : https://www.shadertoy.com/view/MddGzf

げーむだ!

案の定iq大先生の作品なわけですけど、Shadertoyにbufferを4つまで作れるMultipass機能の追加の告知ということでした。
bufferを利用したサンプルとして最初にブロック崩しを出すのはさすがShadertoyという感じですが。
(まぁ想像できるように今後lifegameやpath tracingのサンプルがぽんぽん投稿されていきます。さすが。)

で、Shadertoyでできなかった「ゲームの制作」ができるようになってしまいました。
やるしかない。

ゲームのつくりかた

一般的なゲームでだいたい問題となるのはレンダリングですが、今回はそれは自明に解決されています。問題は状態の保存なのです。
基本的な方針としては、BufAに状態を持たせ、内部では自分の1つ前のフレームを参照して状態を更新。そしてImageの方ではその状態を利用してレンダリング、という感じになります。
テトリスではちょっとしたブラーを掛けたかったのでBufBでレンダリングをしてImageでそれを丸々コピー、という形を取っています。Imageの前フレーム情報は取れないのです。

フレームのピクセルデータそのものが状態となるため、容量にこまることはありません。今回は適当にBufAの左下らへんのピクセルに0~255の値をRに書き込んでいます。実際に読み取ってみるとこんな感じです(Rの値を100倍してます) :

image

image

フィールドの形がわかるとおもいます(回転してますけど)。ちなみに他のデータはボタンの長押し情報とか、ネクストとかホールドです。多分。

実装

と言っても原理がわかっているのでそれに沿っていけば良いです。あるピクセル位置のデータのread/write関数を作り、うまいことテトリスのコードを書き、また同じread関数を使ってBufBでレンダリングのコードを書きます。うごく!やったー!

保存データ

テトリスのデータは、10x20でフィールドの色を保存する領域、落下中ピースのX,Y,回転角度(R)をそれぞれ保存する領域、あとネクストとホールドの領域で出来ています。
落下させるときはX,Y,Rを参考にフィールドに当たり判定をしていきます。いっぱいforをまわします。
回転については一般的なテトリスで採用されているSuper Rotation Systemをそのまま実装していて、回転できないときは補正をしなおして回転する、を5回くらい試します。

ネクスト生成

テトリスのネクストは7つ毎に生成されています。7種類でランダムシャッフルしてネクストとする、を繰り返す感じです。
でも悲しいことに乱数が無いのでint(mod(iGlobalTime*1000.,5040.))を使って0から5039までの擬似乱数を作り出しています。
これを元にがんばって順列を計算し、ネクストに保存、という感じで書いてます。

レンダリング

背景の星は https://www.shadertoy.com/view/MslGWN の一部をパクってきました。こういうことができるからShadertoyはよい。
ブロックは左上の方をそれっぽく暗くして、右下の方をそれっぽく明るくして(具体的にはcolor += max(POW(e.x/0.9,3.),POW(e.y/0.9,3.));)
敢えて外側の枠には明るさの処理を入れないように、としてみたらなんだかそれっぽいのができました。
フィールドの背景なども単純で、pow(max(abs(e.x),abs(e.y)),3.)*0.1です。

あとやってる処理とすれば、全ての描画の前に自分の前フレームの結果を3x3でぼかしたもの(をさらに暗くしたもの)を最初に描画しています。
背景はこれに対して加算合成しているので最初にふわーっと浮かび上がる感じのエフェクトになります。
あとぼかしているおかげでネクストやホールドの表示にふわーっとしたのが付いてかっこよくなりました。
前フレーム背景を暗くしているのである程度ふわーってなったら収束します。いい感じのパラメータを取るのに苦労した覚えがあります。

こまったこと

ブラウザが落ちる問題

最初は私の残念なノートPCで実装していたのですが、ピースの移動らへんを組んだ時点でWebGLが落ちるようになってしまいました。
おうちに帰ってまともなGPUがあるPCの上で最後実装したのですが、勿論元のノートで動くはずもありません。
Shadertoyでは「iqさんのノートPCでクラッシュしたら公開できない」という基準があるようで、とりあえずと公開したテトリスは一瞬でdraftに戻されてしまいました。

どうもWebGLが落ちる原因はシェーダのコンパイルに時間がかかりすぎているから?みたいです。forをいっぱい使っているので展開された結果膨大なソースコードになっているんだとおもいます。
というわけでそれ以降はひたすら軽量化作業でした。元の形は既に思い出せないですが、ifを潰したり、同じ値を返す関数呼び出しをforの外に出すとか、forを自分で展開するとか、余計な条件分岐を吐かないように自分でいじるとか、いろいろやりました。

努力のおかげかなんとか私のノートでも動くようになり、またiqさんのノートでも動いてくれたので晴れて公開、となりました。

それでも重い

勿論コードの量が多いことには変わりありません。ダメなPCではクラッシュするだろうと思ったので、一応セーフティを掛けました。
手動軽量化をしているときに気づいたのですが、read関数の呼び出しが大量にあるせいで落ちている部分があるみたいなのです。
というわけで、ゲームは動かないけれどコンパイルはできるように、最初にread関数はreturn 0;しておくようにしました。
これは内部カウントのフレーム数が常に0になるので常時初期化になります。というわけで現在の挙動をするのです。

中々GLSLの最適化は謎で、まぁこのような明らかな定数関数はinline化してくれるんだとおも・・・ってますが、
実際に遭遇した例として

bool safe(int t,int x,int y,int r){
    return false;
    for(int i=0;i<4;i++){
        if(field(x+pposX(t,i,r),y+pposY(t,i,r))!=0){
            return false;
        }
    }
    return true;
}

この関数は明らかに定数関数なのでfalseに展開されそうで、実際コンパイルがちゃんとできます。しかし、

bool safe(int t,int x,int y,int r){
    return false;
}

こうするとWebGLがクラッシュします。うん?うん。
過去のコードなので再現はもうできないですが、まぁreturnらへんの扱いには結構気を付けなきゃいけないらしいです。
こわいこわい。

Macだと動かない

WebGLとか言うんだしMacのChromeでも動くやろーと思っていましたが、もうなんか悲惨な見た目になります。
結構Macでは正常に動かないコードがShadertoyにあったりしますが、自分も遭遇してしまったわけです。
「fragColorに何が入っているかは未定義なのでちゃんと初期化しようね」などの話は知っていたのですが(正常に動かないものの原因は大体これ)。

結局理由はfragColorの扱いでした。

void mainImage( out vec4 fragColor, in vec2 fragCoord )が描画用関数のシグネチャなのですが、まぁよくある問題はoutなわけです。out変数の初期値は未定。

で、状態を更新するための関数をいろいろ作ったわけですが、何も考えずそのシグネチャもvoid genNext( out vec4 fragColor, in vec2 fragCoord )と、outを使っていたのですね。正しく理解していなかった結果ですね。
Windowsだとこれでも動いてしまうようなのですが、まぁ間違いは間違いです。inoutにしたら正しく動いてくれました。

入力の取り方が謎

これはShadertoyの仕様ではあるんですが、キー入力はテクスチャとして与えられます。その中身についてのドキュメントがどこにもないのです。
しかたないのでキー入力テクスチャを直接出力するシェーダを書いてみました。
image
謎です。

どうやら一番下の段が現在押されているかを見ているようです。真ん中は押した瞬間を検知してくれます。一番上はおそらく押した瞬間でスイッチするトグルです。
テトリスで長押し処理をするのが面倒だったのでこのデータを直接つかえないかなー、と考えたのですが 長押しすると真ん中が点滅する(1フレーム間隔で認識する)みたいで確かにうまくいきそうだったのですけど

環境依存のようで。

諦めて自分でつくりました。

またどのキーがの位置に対応しているかも何もわからないので、左上から何列目なのかを数えることになりました。
ちなみに左上右下キーはそれぞれX=37.5/256,38.5/256,39.5/256,40.5/256みたいです。一応それでテトリスは動いています。
Shadertoyは機能がいっぱいあるものの活用しきるのが難しいですね・・・

まとめ

色々大変ですが、Shadertoyでは!!!ゲームが!!!つくれます!!!!!

何か目的を間違えている気はしますが・・・きっとそれがShadertoyなのです。

いろんな人がいろんな作品をつくっています。(※クラッシュ注意)
- ブロック崩しつくったり
- パックマンつくったり
- アクションゲームつくったり
- レースゲームつくったり
- ろくろ回しシミュレータつくったり
- ピンボールつくったり
- リバーシつくったり

というわけでみなさんもなにか (ゲームとは言いませんが) なにかShadertoyでも作ってみるのもいいとおもいます。
iq大先生は大体のシェーダを眺めているようなのでコメントが頂けてとてもうれしかったりします。うんうん。

みなさんも暖かいPCでHappy Shader lifeを!

phi16
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away