インフィニットループ 技術ブログ

2022年12月14日 (水)

著者 : nob

PHPとSDLで始めるコンピューターグラフィックス

こんにちはサーバーインフラや社内の情報システムを担当しています nobuh です。

子供の頃から美術館や展覧会に行ったりするのが趣味の一つで、コンピューターでもゲーム同様グラフィクス関係についても興味があり、 ジェネレティブ・アート や デモシーン を鑑賞するのを楽しみにしています。

今回は鑑賞から一方踏み出して、ライブラリやフレームワーク、エンジンの支援をあまり受けずに、原始的な要素を直接使って根源的なところから自分でグラフィクスを作ってみようと思い立ちました。

ということで、PHP と SDL を使ってグラフィクスについて「作ってみた」ところを紹介したいと思います!

なぜ PHP?

グラフィクスを扱うプログラミングというと、 ProcessingopenFrameworks、 3D なら UnityUnreal Engine といったゲームエンジン、Three.js などのライブラリやそれらで使われている言語を使うのが定番です。

一方で創業期にブラウザゲームを手掛け、以来ゲームを司るアプリケーション・サーバーの開発を主な業務として来た弊社では、サーバー開発用言語としては主に PHP を(と最近は C# など他言語も)使っています。社内に PHP に詳しい人が多いので、困ったときに聞ける人がたくさんいること、かつ PHP でグラフィクスというのはあまりやっている人がいなくて未踏なところがあって面白そう。というところがあり、今回 PHP を選択してみました。

Simple DirectMedia Layer (SDL)

PHP でグラフィクスを描画する方法を探してみると。。。あまりありませんw かろうじて見つけたのが PHP で SDL を使う https://github.com/Ponup/php-sdl です。

Simple DirectMedia Layer (SDL) はマルチメディアのデバイスを扱うためのクロスプラットフォームかつ低レイヤーなライブラリで、Unity などの本格的なゲームエンジンを使わずに独自エンジンを作ったり、インディーゲームを作成する場合等で使われる、歴史のあるライブラリです。

SDL は C 言語で使うライブラリですが、たくさんの言語用のバインディング(ラッパー)が作成されており、PHP の拡張として作られたラッパーが今回使う php-sdl になります。

なお、SDL は version 1 系と version 2 系がありますが、php-sdl で使っているのは SDL2 になります。

php-sdl のインストール

動作には Linux ないし Mac が必要です。Windows の人は VirtualBox を使うなどして Linux の仮想マシンの準備をお願いします。

詳細は php-sdl の README を参照いただくとして、私が今回使っている環境は以下で、インストールすることが出来ました。

  • Ubuntu 22.04.01 Desktop LTS
  • README には PHP8.1 devel と書いていますが、Ubuntu で普通に apt でインストール可能な php8.1-cli で動きました
  • pecl コマンドを使いますが php8.1-xxxx ではなく php-pear パッケージに入っています

php-sdl を pecl insatll sdl-beta で無事インストール出来ましたら examples ディレクトリに入っているサンプルを実行してみてください。(例:php 011-draw-with-subpixels.php)以下のような画像が表示されれればインストールは成功です!

まずはウインドウと点と四角

  • 縦横 400 ピクセルの ウィンドウを作成し
  • 単色で塗りつぶしクリア
  • x と y が 100,100 に点を、200,200 に大きさ 50 ピクセルの四角を描く
  • ウィンドウを閉じられるか、マウスでクリックされた場合に終了する

というアプリを作ってみます。firstwindow.php というファイル名で作成します。まずは PHP のスクリプトですので

    <?php declare(strict_types=1);
    error_reporting(E_ALL);

で始まります。次にウィンドウのサイズを縦横 400 に決めて、SDL_Init で SDL の初期化とウィンドウの作成を行います

    const WINDOW_SIZE = 400;
    SDL_Init(SDL_INIT_VIDEO);
    $window = SDL_CreateWindow("最初のウィンドウ", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_SIZE, WINDOW_SIZE, SDL_WINDOW_SHOWN);

SDL_CreateWindow のパラメーターは、タイトル、出現位置 xy、サイズ xy 等になります。ウィンドウを作成したら、そこにレンダラーを作成します。基本的に速いものが好きなので、高速レンダラーを指定しています。

    $renderer = SDL_CreateRenderer($window, 0, SDL_RENDERER_ACCELERATED);

SDL のアプリケーションは、入力と描画の処理を1フレームとして、そのフレームを無限に繰り返すという方式を使います。 具体的には $quit 変数を最初 false にしておき、条件を満たすと true になる。という構成です。

    $quit = false;
    while (!$quit) {
        // 1フレーム毎
        // 入力  ここで終了操作があれば $quit = true
        // 描画
    }

まず入力処理ですが、SDL でマウスやキーボードなどのイベントが発生するとイベントを格納するキューに入れられます。そのキューから取り出したイベントを格納するのが $event です。

入力キューからイベントを取り出すのは SDL_PollEvent になりますが、この関数はキューに複数溜まっていても1個しか取り出してくれません。1回のフレーム中で何個イベントが溜まってるかわからず、取りこぼしがあると入力と描画状況が乖離してしまうため、フレームの入力部分で、溜まっているイベントはすべて読み取るような処理を行います。

    $event = new SDL_Event;
    $quit = false;
    while (!$quit) {
        // フレーム毎の処理
        // 入力  ここで終了操作があれば $quit = true
        while (SDL_PollEvent($event)) {

        }

        // 描画
    }

今回はウィンドウが閉じられる操作の SDL_QUIT 、マウスクリックの SDL_MOUSEBUTTONDOWN があれば終了することにします。

        // 入力  ここで終了操作があれば $quit = true
        while (SDL_PollEvent($event)) {
            if ($event->type == SDL_MOUSEBUTTONDOWN || $event->type == SDL_QUIT){
                $quit = true;
            }
        }

次は描画部です。SDL_SetRenderDrawColor で RGBと透過度を与えて、以後使う色を指定します。

        SDL_SetRenderDrawColor($renderer, 255, 255, 255, 255); //白

SDL_RenderClear で先に指定した色でウィンドウ全体をクリアします。

        SDL_RenderClear($renderer);

次に色を黒に変えて、100,100 に点を足します。

        SDL_SetRenderDrawColor($renderer, 0, 0, 0, 255); // 黒
        SDL_RenderDrawPoint($renderer, 100, 100);

四角の書き方は少し特殊です。先に四角オブジェクトを生成し、それから四角塗りつぶしの SDL_RenderFillRect を実行します。

        $rect = new SDL_Rect(200, 200, 50, 50);
        SDL_RenderFillRect($renderer, $rect);  

フレームのループの最後に RenderPresent を使って実際に表示します。


表示後、このままだと1フレームが速すぎますので、最速でも 60FPS にしかならないように 1000/60 ms のディレイを入れます。ループの外は終了時の後片付けだけです。

        SDL_RenderPresent($renderer);
        SDL_Delay(intval(1000/60));
    }

    // 片付け
    SDL_DestroyRenderer($renderer);
    SDL_DestroyWindow($window);
    SDL_Quit();

php firstwindow.php で実行すると以下に表示されます。100,100 の点が 1 ピクセルなので拡大しないと見えないかもしれないです。

1次元セル・オートマトン

ウインドウと四角が無事表示されましたら、次は1次元セル・オートマトンに挑戦します。

アプリケーションの枠組みは前述のものと一緒ですが、まずアプリケーションの状態をもたせるために先頭でデータを定義します。

    <?php declare(strict_types=1);
    error_reporting(E_ALL);

    // 初期値とグローバルな状態データ
    // フルスクリーンは想定していないので正方形で
    // ウィンドウサイズは 512 など 2 の累乗になっていること
    const WINDOW_SIZE = 512;
    $rule = 110;
    $scale = 1; // 2 の $scale 乗のサイズの点で描画する

1次元のセル・オートマトンは、自分と左右の状態に応じて、次の状態が 1 になるか 0 になるか決定します。

状態の 左、中心、右、の3ビットの 0, 1 の組み合わせ8通りに応じて、0 か 1 になるかでルールを表現できます。さらにこの 8 通りのルールを 8 ビットの値と見立てると、0 〜 255 の数値としてルールのすべてを表現出来ます。

https://ja.wikipedia.org/wiki/%E3%82%BB%E3%83%AB%E3%83%BB%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3 の図より

ルール 110 = 2進数で 01101110 となり、現在の状態とルールの数値をビット AND し、1 以上なら 中央のセルは次に 1 、0 なら 0 と決まります。

このルールを保存しているのが前述のコードにあった $rule で、とりあえず初期値は 110 にしました。

$scale は前述のようにサイズ1のピクセルだと小さすぎますので、ピクセルのサイズを 2 の $scale 乗で指定できるようにしておくためのものです。

データ部の次は、入力部を以下のようにします。

キーは SDL_KEYDOWN タイプのイベントで検知されます。検知後 SDL_GetKeyboardState(キーの数) が返すキーの状態を $keyboardState に格納し、$keyboardState[キーの種類] の true/false で押したキーを判別します。

        if ($event->type == SDL_MOUSEBUTTONDOWN || $event->type == SDL_QUIT){
            $quit = true;
        }
        if ($event->type == SDL_KEYDOWN){
            $keyboardState = SDL_GetKeyboardState($num);
            // 上下キーで点のスケール変更
            if($keyboardState[SDL_SCANCODE_UP]) {
                $scale++;
            }
            if($keyboardState[SDL_SCANCODE_DOWN]) {
                $scale--;
                $scale = $scale < 0 ? 0 : $scale;
            }
            // 左右キーでルール変更
            if($keyboardState[SDL_SCANCODE_RIGHT]) {
                $rule++;
                $rule = $rule >= 256 ? 0 : $rule;
            }
            if($keyboardState[SDL_SCANCODE_LEFT]) {
                $rule--;
                $rule = $rule < 0 ? 255 : $rule;
            }
            $title = "RULE:" . $rule . " SCALE:2^" . $scale;
            SDL_SetWindowTitle($window, $title);
        }

ここではコメントにあるように、左右キーでルールの値を、上下キーでドットのスケールを縮小拡大しています。

次は描画部の前半です。

    // セルの数はウィンドウのサイズを 2 の $scale 乗で割る
    $numCell = intval(WINDOW_SIZE / 2**$scale);

    // cell を 0 で初期化
    for ($i = 0; $i < $numCell; $i++) {
        $cell[$i] = 0;
    }

    // 中央 1 点だけ 1 に
    $cell[intval($numCell / 2)] = 1;

    // GB 風 4色カラーを使う
    // black 0x081820
    // white 0xe0f8d0

    // white でクリア
    SDL_SetRenderDrawColor($renderer, 0xE0, 0xF8, 0xD0, 0xFF);
    SDL_RenderClear($renderer);

    // 描画色を black に
    SDL_SetRenderDrawColor($renderer, 0x08, 0x18, 0x20, 0xFF);

ドットのスケールは刻々と変わりますので、描画フレーム毎にセルの数を計算し、0 で初期化して中央の1点だけ 1 にします。

色はゲームボーイの配色を拝借し、white でクリアし、black を描画色にしました。次が描画の後半部です。

    for ($y = 0; $y < $numCell; $y++) {
        for ($x = 0; $x < $numCell; $x++) {

            // 現在のセルが 1 のときだけ点を打つ
            if ($cell[$x] === 1) {
                if ($scale > 0) {
                    // $scale が 0 より大きい場合は四角で描画する
                    $rect = new SDL_Rect($x * 2**$scale, $y * 2**$scale, 2**$scale, 2**$scale);
                    SDL_RenderFillRect($renderer, $rect);    
                } else {
                    // $scale が 0 の場合は点で描画
                    SDL_RenderDrawPoint($renderer, $x, $y);    
                }
            }

            // 左右のインデックスは ウィンドウのサイズでラップ
            $left = $x > 0 ? $x - 1 : $numCell - 1;
            $right = $x < ($numCell - 1) ? $x + 1 : 0;

            // ルールとのビット AND で計算する
            $value = 2 ** ($cell[$left] * 4 + $cell[$x] * 2 + $cell[$right]);
            $next[$x] = ($value & $rule) > 0 ? 1 : 0;
        }

        // 次のセルの値を現在値で入れ替え
        $cell = $next;
    }

セルの x と y でループしながら描画し、右端左端はラップアラウンドさせながら、状態とルールとの AND によって次の状態を決定します。出来上がりを実行したものがこちらです!

まとめ

PHP での SDL グラフィクスいかがでしたでしょうか!

実際にやってみると手元でいろんな絵を描画するのはとても楽しいです!

弊社では PHP などの言語を使ったサーバー開発のお仕事に興味があり、そしてコンピューターに関連するいろんな事に興味がある方のご応募をお待ちしています!

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!