WebGLでレイトレしてゲームを作ってみた

すべての色をつなげよう WHOLE MATCH PUZZLE

操作方法

 マウスのドラッグ(タッチパネルの場合はスライド)で玉を一列横か縦にスライドさせることが出来ます。

ルール

 同じ色同士ですべての玉をつなげると一面クリアです。そのとき100点が加算されます。

20170620164101
ひとつの解答例。赤は赤同士、青は青同士、上下左右どこかでつながっていればよい。

 クリアするたびに色が増えていき最大で5色まで難易度が上がります。
 パズルの完成度(パーセント)がスコアの下2桁になります。
 2分の制限時間内で可能な限り玉を消して高得点を狙ってください。
 目指せ1000点!

※最新のfirefoxとchromeで動作を確認しています。
 また、一部のスマホでも動作を確認しています。

結果をツイート
タイトルに戻る

はじめに

 ドーモ、セガゲームスは開発技術部所属の栃木と申します。
 業務ではライブラリの制作や絵周りのことでコードを書いていることが多いです。
 今回は、タイトルの通り、WebGLでレイトレをしてゲームを作ってみました。

目次

  • WebGLとは
  • レイトレとは(CG初心者対象)
  • ゲームを作ってみた(ゲームを作ってみたい人対象)

WebGLとは

20170620164051 ブラウザ上で動作するOpenGLのことです。このブログを読むような方は見聞きしたことがあるのではないでしょうか。
 今回はWebGLをつかうことでレイトレをGPUで高速に動作させています。 

レイトレとは

 Ray Tracing(レイトレーシング)の略です。レイトレーシングとはレイ(光線)を投げて当たったところの色を取ることでCGをレンダリングする手法です。
 レイトレはひたすらレイを投げて当たり判定を取るだけでそれっぽい絵が出来るので、CG入門としてはおすすめな手法です。透視変換行列を必要としませんし、シャドウマップも必要もありませんし、環境マップも必要ありません。すべてレイを投げることで解決します。
 ただどういうふうにレイを投げるかで出来上がってくる絵が変わってきます。
 「CGを学びたい」けど「DirectXやOpenGLがよくわからない」、けど「Unityまでいくといじれる所が少ない(どこをいじっていいかわからない)」と感じる人は、いっそそういったグラフィクスライブラリやゲームエンジンを使用せずに、CPUでレイトレーシングを書くと幸せになれるかもしれません。
 レイトレースの第一歩はライティングされていない単色の球を1個書いてみるところからです。
 まず目とスクリーンと球を規定します。球はスクリーンに収まるように配置しましょう。スクリーンはあらかじめ適当な色でクリアーしておきましょう。
 そして、目からスクリーンのピクセルの位置に対してレイ(直線)を投げます。あとはその直線と球の当たり判定をとって、当たっていたら球の色を描きます。これをすべてのピクセルで繰り返します。レイトレースの完成です。あとはこれをいろいろと拡張していくだけでいいのです。

20170620164057
目からスクリーンにレイを飛ばしまくる。

 ね?簡単でしょ?

 以下のサンプルコードは、レイトレースをLambertのライティングまで拡張したものになります。ボタンを押すと結果が描画されます。(このサンプルコードではCPUのみでレイトレースしています。)

function raytrace() 
{
    // 各種便利関数
    function setVec3( v, x, y, z )
    {
        v[0] = x; v[1] = y; v[2] = z;
    }
    function minus( a, b )
    {
        var ret = new Array(3);
        ret[0] = a[0] - b[0];
        ret[1] = a[1] - b[1];
        ret[2] = a[2] - b[2];
        return ret;
    }
    function dot( a, b )
    {
        return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
    }
    function mad( a, m, b )
    {
        var ret = new Array(3);
        ret[0] = a[0] * m + b[0];
        ret[1] = a[1] * m + b[1];
        ret[2] = a[2] * m + b[2];
        return ret;
    }
    function length( v )
    {
        return Math.sqrt( dot(v,v) );
    }
    function normalize( v )
    {
        var len = length( v );
        v[0] /= len;
        v[1] /= len;
        v[2] /= len;
    }

    // キャンバス取得
    var elem = document.getElementById("raytracesample");
    var canvas = elem.getContext( "2d" );
    
    // クリア
    canvas.fillStyle = "rgb(64,160,255)";
    canvas.fillRect(0,0,256,256);

    // 目の場所は( 0, 0, 128 )
    var eye = new Array(3);
    setVec3( eye, 0, 0, 128 );
    
    // 球の場所は( 0, 0, -64 )、大きさは32
    var sphere = new Array(3);
    setVec3( sphere, 0, 0, -64 );
    var spheresize = 32;

    // 平行光
    var dlight = new Array(3);
    setVec3( dlight, 1, 2, 2 );
    normalize( dlight );
 
    // ピクセル数レイを飛ばす
    var ray = new Array(3);
    var screen = new Array(3);
    for( var y = 0; y < 256; ++y )
    {
        for( var x = 0; x < 256; ++x )
        {
            // スクリーンは(-31.5,-31.5,0)-(+31.5,+31.5,0)のXY平面
            setVec3( screen, x*0.25 - 31.5, 31.5 - y*0.25, 0 ); // ピクセルに対するスクリーン位置

            // 目からスクリーンへのベクトルをレイとする
            ray = minus( screen, eye ); 
            normalize( ray ); // 正規化

            // レイと球の当たり判定
            var v = minus( sphere, eye );
            var d = dot( v, ray );
            if( d > 0.0 )
            {
                var p = mad( ray, d, eye );
                v = minus( sphere, p );
                var len = length( v );
                if( len < spheresize ) 
                {
                    // 球に当たったので各種情報を求める
                    len = len / spheresize;
                    d = Math.sqrt(-len*len + 1.0*1.0) * spheresize;
                    // 衝突座標
                    p = mad( ray ,-d, p ); 
                    // 法線
                    var nrm = minus( p, sphere ); 
                    normalize( nrm );
                    
                    // ライティング
                    d = Math.max( dot( nrm, dlight ), 0 );
                    
                    // 結果を描画
                    var col = Math.floor(255*d);
                    canvas.fillStyle = "rgb("+col+","+col+","+col+")";
                    canvas.fillRect( x, y, 1, 1 ); // 点を打つ機能がないのでfillRectで代用
                }
            }
        }
    }
}


 この程度の短いコードから始められるのがレイトレのいいところです。
 ちなみに、今回のゲームのレイトレでは、球16個、無限平面5面、魚眼、直接光(LambertとGGX、および直接光への遮蔽(影))、鏡面反射、自己発光、まで拡張したものをGPUで動作するようシェーダーで実装しています。
20170620164046興味がある方は、このページを右クリックしてソースを表示でコードを確認できますので覗いてみてください。
 頂点入力から頂点シェーダー(とフラグメントシェーダーの先頭)でレイを作成し、フラグメントシェーダーで当たり判定をとりシェーディングをしています。 

ゲームを作ってみた

 今回のゲームを作っていった過程を交えてゲームの作り方を紹介していこうと思います。
 一介のプログラマーの作り方の紹介なので、こういう作り方の人もいるんだなぐらいの温度感で読んでください。
 同人ソフトやGameJamなどでのゲームの作り方の参考の一つにでもなれば幸いと考えています。

手段の選択

 なんらかのソフトウェアを作れる手段が必要です。
 それは”プログラムを書ける”でもいいですし、”Unityを使える”でもかまいませんし、”RPGツクールがさわれる”でもかまいません。
 それが出来るのがあなたである必要もありませんが、他人に無理強いはやめましょう。
 いっそ、ゲームが作りたいという理由から、これらを学ぶというのもよいと思います。ゲームを作りたいという動機は、学ぶ原動力として大きく働くと思います。

 今回は、ブログに直接プレイできる形で載せたかったため、HTML5+Javascriptを選択しました。

ルールを考える

 とにもかくにも、ルールを考えなければ何も出来ません。
 「何を作りたいのか?」「何が出来るのか?」「ジャンルは何にするのか?」
 今回は私一人で作ったため、悩むことは少なかったですが、時間や予算、自分の技術力、チームで作る場合はチームの力量を踏まえて何がどこまで作れるかをあらかじめ想像しておかなければいけません。
 たとえば、無限のお金と時間があればだれでもAAA級タイトルを目指せるわけですが、現実に無限な資源など存在しないわけです。

20170620164041今回は技術ブログに掲載するということで、技術的にレイトレースを使うということは決めていました。
 そして、レイトレースの鏡面反射を正確に表現できるという特徴を利用するために、パズルゲームのルールを考えたのですが……。 

プロトタイプ的なものを作る

 当然といえば当然なのですが、最初に考えたルールが必ずしも面白いとは限りません。
 なので、ゲーム開発初期段階でなんらかのプロトタイプを作ってゲームが面白いかどうか、面白く出来るかどうかをおおよそつかむ必要があります。
 最近ですとゼルダの伝説BotWが初期に2Dプロトタイプを作って新しいゼルダの面白さを提示したというエピソードが話題になりましたが、あそこまでしっかりしたことをするかどうかは別として、なんらかのプロトタイプ的なものを作ることは重要です。

 今回のゲームの場合も、非常に荒削りな状態でゲームのルールだけ確認できるものを最初に用意しました。
 で、最初に思いついたルールを組み込んだプロトタイプをプレイしてみたところ……面白くないことがわかりました。
 最初に考えたパズルゲームのルールはこうでした。

4x4の玉を4面鏡に囲まれた箱の中に配置する。鏡の中も含めて同じ色を8個以上つなげたら消える。合わせ鏡で無限につながったら大きな得点を得られる。連続で消すとチェインボーナス得点を得られる。

 今回公開したものとは似ても似つかないルールです。
 脳内テストプレイでは「お、面白くなりそう。」と思っていたわけですが、実際に実装してプレイしてみたら、これがうーん、つまらない。想定どおり消えるし、得点も得られるけど、つまらない。
 いろいろと原因は考えられましたが、単純にパズルとして考えることが少なすぎるのがつまらなさの最大の原因でした。
 このルールでも解空間を広げることで面白さが出せるのではないかとも考えたのですが、レイトレースで実装することを考えるとオブジェクト数が増えることは処理負荷が増えることに直結しますし、また構想段階からできればスマホで動かしたいという思惑もあり、解空間を増やしての実験は行わずに、根本的にルールを見直すことを考えました。
 まぁ詳しくは省きますが、なんやかんやあって、ある程度面白さを感じられる今のルールに落ち着いたのです。

20170620164220このように、プロトタイプを作ることでつまらないゲームになることを事前にある程度防ぐことが出来るわけです。いうなれば、料理で言うところの”味見”みたいなものですね。
 ここで美味しくなるまで企画をちゃんと練るのが重要だと思います。 

作りこむ

 だいたいの面白さは確保できたら、作りこみフェーズに移行します。

  • グラフィック・BGM・テキストなどの必要なリソースを用意する。
  • タイトルなどのゲーム以外のシーケンスを用意する。
  • ゲームバランスを調整する。
  • エフェクトの追加など見た目を良くする。
  • 逐次、バグがあれば除去する。

 プロトタイプで出来た骨子に肉付けしていくイメージです。
 これを完成するまで続けます。
 ゲームのボリュームに比例して大変な作業となります。
 実際のプロジェクトではここからが作業の本番といえるでしょう。特にグラフィックやBGM、テキストなどのリソースを用意するのは物量を必要とするため一番大変です。
 最初に夢見がちな企画を考えてしまうと、ここであえなくオダブツとなってしまうため、実力と時間にみあった計画をしましょう。

20170620164215今回のゲームは1人で短期間で作るためにゲームの仕様を最小限に止めた為、ここではリソース作成の作業は発生しませんでした。(させませんでした。)
 せいぜい、タイトルやタイムアップ画面などの追加、ハイスコアの保存やゲームバランスの調整、ささやかな演出の追加など、コードの追加実装のみで作業は完結してます。 

完成

 ひと通りできたら完成です。
 完成したら公開することをお勧めします。身内に見せるだけでもいいですが、ネット上に公開したほうがより多くの人の目に付くので良いと思います。いろんな人にプレイしてもらえるだけでもモチベーションがあがります。
 なにより、人にプレイしてもらってこそのゲームなのです。
 同人ゲームだったらコミケに出すのもいいかもしれません。
 また、就職活動でゲーム会社を受けたときに、面接時に「こういうゲームを作りました」というアピールにも使えるでしょう。

20170620164210今作はブログにて公開と相成りました。皆さまプレイはしていただけたでしょうか?得点はいくらほど取れたでしょうか?面白い、楽しいと少しでも感じていただけたでしょうか?得点や感想などをブクマやツイートをしていただけると幸いにございます。 

最後に

 今回は技術ブログでゲームを公開するというチャレンジをしてみたわけですが、技術としては特段変わったことはしていないので、あまり書くことがなかったというのが執筆していてつらかったところです。
 あまり堅苦しいブログになってほしくない、ライトな層にも見ていただきたいという思いからゲームを公開してみようと思い立ったのですが、皆様のこころに少しでもささってくれればうれしいなと思います。

 また、セガでゲーム開発やその周りに携わりたい、もしくは興味があるという方がいらっしゃいましたら、下記の弊社グループ採用サイトをご確認ください。
 いっしょに働きましょう!

採用情報 | セガ企業情報サイト

Powered by はてなブログ