2. 図形の描画

めちゃくちゃ間があきましたが, SDL 1.3 の続きです. 今回は画像を使わない基本的な描画処理について. 相変わらずヘッダーとソースを見ながら何となく書いてるので, 間違ったことを書く可能性がありますがご容赦を.

注意: 前回の記事から今までに SDL 1.3 において, SDL_WindowID の代わりに SDL_Window* を使うように仕様の変更がありました. まだ開発中のライブラリーというだけあってこういう変更は今後もあるかもしれません. 大幅な改修はもうないと思いますが...

SDL 1.3 の画面描画は, レンダラー (レンダリングコンテキスト) を設定した上で各種の描画関数を呼び出す, という形で行います. レンダラーは各ウィンドウに一つずつ設定できます.

SDL_CreateRenderer 関数でレンダラーを作成し, SDL_SelectRenderer 関数でそれをカレントにします. 各種の描画関数は明示的にレンダラーを受け取りません. これらは SDL_SelectRenderer 関数で最後に設定されたレンダラーに対して描画を行うようです.

int SDL_CreateRenderer(SDL_Window* window, int index, Uint32 flags);
window に対するレンダラーを作成する. index はレンダリングドライバーのインデックスを表す. -1 を指定すると flags で指定した条件に合う最初のドライバーを自動的に選択する. flags は以下の enum 値の OR 結合を指定する. まず画面の更新方法について, SDL_RENDERER_SINGLEBUFFER でシングルバッファーを使う. SDL_RENDERER_PRESENTCOPY を指定するとダブルバッファーだが, 画面更新はフリップではなく全面コピーによって行う. SDL_RENDERER_PRESENTFLIP2 は一般的なダブルバッファーのフリップによる更新. SDL_RENDER_PRESENTFLIP3 はトリプルバッファー方式である. 残りのフラグは更新に関するオプションである. SDL_RENDERER_PRESENTDISCARD を指定すると, 画面更新後のバックバッファーは未定義状態となる. SDL_RENDERER_PRESENTVSYNC を指定すると垂直同期を行う. SDL_RENDERER_ACCLERATED を指定するとハードウェアアクセラレーションを利用しようとする. この関数は成功すれば 0 を, 失敗すれば -1 を返す.
int SDL_SelectRenderer(SDL_Window* window);
window に設定されたレンダラーをカレントにする. window に対してレンダラーが設定されていなければ -1 を返し, そうでなければ 0 を返す.
void SDL_DestroyRenderer(SDL_Window* window);
window に対して設定されたレンダラーを破棄し, そのレンダラーに紐付けられたテクスチャー (多分次回触れます) をすべて解放する.

設定したレンダラーに対して毎フレーム描画を行うことになります. 普通はまず画面全体を黒などで塗りつぶすことから始めるでしょう. この処理は SDL_SetRenderDrawColor 関数で描画色を設定した後に SDL_RenderClear 関数を呼び出して行います.

int SDL_SetRenderDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a);
描画色を設定する. この描画色は画面のクリアーおよび各種基本図形の描画すべてに使われる. カレントなレンダラーがない場合は -1 を, そうでなければ 0 を返す.
int SDL_RenderClear();
現在の描画色で画面全体を塗りつぶす. カレントなレンダラーがない場合は -1 を, そうでなければ 0 を返す.

関数名に Render と入っているものは大抵, カレントなレンダラーに対しての処理です. SDL_RenderClear は後述する SDL_RenderFillRect を引数 NULL で呼び出すのと等価ですが, レンダリングドライバーによって専用の関数が用意されている場合にはそれを利用します (おそらく効率的) *1.

上のように, 描画色にはアルファー値を設定することができますが, アルファーブレンドを使う場合には明示的に有効化する必要があります.

int SDL_SetRenderDrawBlendMode(int blendMode);
カレントのレンダラーのブレンドモードを設定する. SDL_BLENDMODE_NONE を設定するとブレンドを行わない. SDL_BLENDMODE_MASK を設定するとアルファー値が 0 でない場合のみ描画する. SDL_BLENDMODE_BLEND の場合は一般的なアルファーブレンドを行う. SDL_BLENDMODE_ADD を指定すると加算ブレンドを行う. SDL_BLENDMODE_MOD を指定すると乗算ブレンドを行う. この関数は, カレントのレンダラーが取得できない場合は -1 を, そうでなければ 0 を返す.
int SDL_GetRenderDrawBlendMode(int* blendMode);
カレントのレンダラーの現在のブレンドモードを取得し, *blendMode に代入する. カレントのレンダラーが取得できない場合は -1 を, そうでなければ 0 を返す.

SDL が描画できる図形は点, 線分, 矩形のみです. 矩形については中を塗りつぶすこともできるし, 枠だけを描画することもできます. この他に画像などを読み込んだテクスチャーから矩形を描画することもできますが, これは次回以降に.

以下がこれらの図形を描画する関数です. カレントなレンダラーに現在の描画色で描画する, という点はすべて共通です. また戻り値は, カレントのレンダラーが得られない場合は -1, そうでなければ 0 です.

int SDL_RenderDrawPoint(int x, int y);
座標 (x, y) に点を描画する.
int SDL_RenderDrawPoints(const SDL_Point* points, int count);
points で指定された count 個の座標に点を描画する.
int SDL_RenderDrawLine(int x1, int y1, int x2, int y2);
点 (x1, y1) と点 (x2, y2) を結ぶ線分を描画する.
int SDL_RenderDrawLines(const SDL_Point* points, int count);
points で指定された count 個の点を順に結ぶ折れ線を描画する.
int SDL_RenderDrawRect(const SDL_Rect* rect);
rect で指定された矩形のアウトラインを描画する. rect に NULL を指定した場合, 画面全体のアウトラインを描画する. SDL_RenderDrawRects(&rect, 1) と等価.
int SDL_RenderDrawRects(const SDL_Rect** rect, int count);
rect で指定された count 個の矩形の枠をすべて描画する.
int SDL_RenderFillRect(const SDL_Rect* rect);
rect で指定された矩形を塗りつぶす. rect に NULL を指定した場合, 画面全体を塗りつぶす (ただしその場合, 上述した通り SDL_RenderClear の方が適している). SDL_RenderFillRects(&rect, 1) と等価.
int SDL_RenderFillRects(const SDL_Rect** rect, int count);
rect で指定された count 個の矩形をすべて塗りつぶす.

SDL_Point は 2 次元の座標を表す構造体で, SDL_Rect は 2 次元の矩形を表す構造体です. それぞれ以下のような定義になっています.

// SDL_Rect.h
typedef struct
{
    int x;
    int y;
} SDL_Point;

typedef struct SDL_Rect
{
    int x, y;
    int w, h;
} SDL_Rect;

ダブルバッファーやトリプルバッファーを使用している場合, 描画が終わったら画面に反映させる必要があります. これは SDL_RenderPresent 関数で行います.

void SDL_RenderPresent(void);
バックバッファーの内容を画面に反映される. レンダラー作成時の設定に従い, フリップまたはコピーを行う.

これで SDL 1.3 による画像を使わない基本的な描画処理は終わりです. 最後に 2 枚の矩形をアルファーブレンドで描画するサンプルを上げておきます.

#include <iostream>
#include <SDL.h>

// 描画処理
void render() {
  // 画面を黒一色でクリアーする.
  SDL_SetRenderDrawColor(0, 0, 0, 255);
  SDL_RenderClear();

  // アルファーブレンドを有効にする.
  SDL_SetRenderDrawBlendMode(SDL_BLENDMODE_BLEND);

  // 矩形を赤で塗りつぶす.
  {
    SDL_SetRenderDrawColor(255, 0, 0, 255);
    SDL_Rect rect = { 50, 100, 200, 300 };
    SDL_RenderFillRect(&rect);
  }

  // 矩形を半透明の青で塗りつぶす.
  {
    SDL_SetRenderDrawColor(0, 0, 255, 128);
    SDL_Rect rect = { 100, 50, 300, 200 };
    SDL_RenderFillRect(&rect);
  }

  // 画面を更新する.
  SDL_RenderPresent();
}

int main(int, char**) {
  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER)) return 1;

  SDL_Window* window = SDL_CreateWindow("Render rectangles",
                                        SDL_WINDOWPOS_UNDEFINED,
                                        SDL_WINDOWPOS_UNDEFINED,
                                        640,
                                        480,
                                        SDL_WINDOW_SHOWN);

  if (SDL_CreateRenderer(window, 0, SDL_RENDERER_PRESENTFLIP2) == -1) {
    SDL_Quit();
    return 1;
  }

  SDL_Event event;
  for (bool quit = false;;) {
    // イベントがなくなるまで処理し続け, なければ描画処理を行う.
    while (SDL_PollEvent(&event)) {
      switch (event.type) {
        case SDL_QUIT:
          quit = true;
          break;
        case SDL_WINDOWEVENT:
          quit = event.window.event == SDL_WINDOWEVENT_CLOSE;
          break;
        default: break;
      }
    }
    if (quit) break;
    render();
    SDL_Delay(10);
  }

  SDL_Quit();
  return 0;
}

*1:裏技として SDL_RenderFillRect(s) で画像を描画することができる場合があります. たとえばレンダリングドライバーに OpenGL を用いている場合に, SDL を経由せずに直接 OpenGL テクスチャーをバインドしてから SDL_RenderFillRect(s) を呼び出せば, テクスチャーレンダリングを行ってくれます (内部的には glRecti (OpenGL の場合) や glDrawArrays (OpenGL ES の場合) を呼び出すだけなので). さらに変換行列を設定しておけば座標変換もしてくれます. このような気持ち悪いことをするならば, SDL_RenderFillRect(NULL) と SDL_RenderClear() は違う効果を生むことになります.