グラフィック・パターンの扱い

(1)パターンの取り込みと描画

ここでは、グラフィック・パターンの扱い方について説明します。グラフィック・パターンとは、グラフィック画面上の矩形領域のことを指します。


1) 矩形パターンの取り込みと描画

まずは、グラフィック・パターンの取り込みについて説明します。といっても処理自体は単純で、グラフィック上の各点の色を一次元の配列に順に取り込むだけです。

Graphic
00010203
04050607
08091011
12131415

上記のような、4 x 4 の矩形パターンがあった場合、下記のような一次元配列にデータを格納します。左上のピクセルを始点として、左→右の順でピクセルを読み込み、右端まで到達したら下側のラインを次に読み込む操作を繰り返します。一番下のラインまで読み込みが完了したら、処理終了です。

Pattern
00010203040506070809101112131415

矩形パターンを取り込むためのサンプル・プログラムを以下に示します。

/* グラフィックパターンの取り込み */

void get( int x1, int y1, int x2, int y2, int* pat )
{
  int px, py;

  for ( py = y1 ; py <= y2 ; py++ )
    for ( px = x1 ; px <= x2 ; px++ )
      *pat++ = point( px, py );
}

次に、取り込んだ矩形パターンを描画させてみます。これは先ほどの取り込みルーチンとは逆に、一次元の配列からに順に画面に描画することになります。

/* グラフィックパターンの描画 */

void put( int x1, int y1, int x2, int y2, int* pat )
{
  int px,py;

  for ( py = y1 ; py <= y2 ; py++ )
    for ( px = x1 ; px <= x2 ; px++ )
      pset( px, py, *pat++ );
}

上に示した処理の場合、いくつかの問題点が浮かんできます。
まず、矩形の取り込み時に、パターンのピクセル数が配列のサイズを上回った場合、確保した配列の外側に書き込みが発生してしまいます。これを回避するため、引数として配列のサイズを渡してやり、取り込みルーチン側で、配列のサイズと取り込むピクセル数のサイズを比較することにします。
また、取り込み開始座標は矩形の左上、取り込み終了座標は矩形の右下であることを前提にしているので、呼び出し側が異なる指定をしていた場合は処理がおかしくなります。よって、座標のチェックをして、必要ならば、座標の入れ替えをしておかなければなりません。
描画ルーチンでは、描画開始座標(上の例では左上の座標)の他に、描画終了座標を引数として渡していますが、これでは取り込んだパターンの大きさを把握しておかなければならず、描画開始座標にその大きさを加えて描画終了座標を計算する必要もあります。そこで、矩形パターンを取り込む時に、パターンの大きさを情報として持ち、使う側はパターンの大きさを覚えておく必要がないようにしておきます。

もう一つの大きな問題はクリッピング処理です。まず、パターンの取り込みでは、対象外の座標を指定した場合、クリッピングエリアの範囲に納まるように取り込み開始・終了座標を修正してからパターンを取り込む方法があります。また、異常終了としてパターンを取り込まない方法も考えられます。
描画ルーチンでは、エリア外の部分をカットしてから書き込む必要があります。

以上を踏まえて再度サンプル・プログラムを作成します。

/*
  値の交換

  T& t1, t2 : 交換する値
*/
template<class T> void swap( T& t1, T& t2 )
{
  T t = t1;
  t1 = t2;
  t2 = t;
}

/*
  RGB : RGB成分用パレットクラス
*/
class RGB
{
  :
};

/*
 座標定義用構造体
*/
template<class T> struct Coord
{
  T x;
  T y;

  // コンストラクタ
  Coord( T _x = 0, T _y = 0 ) : x( _x ), y ( _y ) {}
  :
};

/*****************************************************
 描画用オブジェクト用基底クラス
*****************************************************/
class DrawingArea_IF
{
public:
  // 指定した座標のパレットを取得する
  virtual bool point( Coord<int> c, RGB& palet ) const = 0;

  // 指定した座標にドットを描画する
  virtual bool pset( Coord<int> c, const RGB& palet ) = 0;

  // 画像サイズを返す
  virtual Coord<int> size() const = 0;
};

/**************************************************************
 GPattern : 矩形パターン
**************************************************************/
class GPattern
{
  vector<RGB> pattern; // 矩形パターン
  Coord<int> size;     // 矩形パターンのサイズ

public:

  /*
    コンストラクタ

    DrawingArea_IF& d : パターンを取得する描画オブジェクト
    Coord<int> s, e : パターンの範囲
  */
  GPattern( DrawingArea_IF& d, Coord<int> s, Coord<int> e );

  /*
    パターンの描画

    DrawingArea_IF& d : パターンを描画するオブジェクト
    Coord<int> s : パターンの描画開始位置
  */
  void put( DrawingArea_IF& d, Coord<int> s );
};

/*
  コンストラクタ

  DrawingArea_IF& d : パターンを取得する描画オブジェクト
  Coord<int> s, e : パターンの範囲
*/
GPattern::GPattern( DrawingArea_IF& d, Coord<int> s, Coord<int> e )
  : size( Coord<int>( 0, 0 ) )
{
  // 描画オブジェクトの大きさを取得
  Coord<int> dSize = d.size();

  // 座標を左上・右下に揃える
  if ( s.x > e.x ) swap( s.x, e.x );
  if ( s.y > e.y ) swap( s.y, e.y );

  // クリッピング処理
  if ( e.x < 0 || s.x >= dSize.x || e.y < 0 || s.y >= dSize.y ) return;
  if ( s.x < 0 ) s.x = 0;
  if ( e.x >= dSize.x ) e.x = dSize.x - 1;
  if ( s.y < 0 ) s.y = 0;
  if ( e.y >= dSize.y ) e.y = dSize.y - 1;

  // パターンサイズの取り込み
  size.x = e.x - s.x + 1;
  size.y = e.y - s.y + 1;

  // パターンの取り込み
  for ( int y = s.y ; y <= e.y ; ++y ) {
    for ( int x = s.x ; x <= e.x ; ++x ) {
      RGB rgb;
      d.point( Coord<int>( x, y ), rgb );
      pattern.push_back( rgb );
    }
  }
}

/*
  パターンの描画

  DrawingArea_IF& d : パターンを描画するオブジェクト
  Coord<int> s : パターンの描画開始位置
*/
void GPattern::put( DrawingArea_IF& d, Coord<int> s )
{
  // 描画オブジェクトの大きさを取得
  Coord<int> dSize = d.size();

  // 描画開始位置はクリッピングエリア範囲内か
  if ( s.x >= dSize.x || s.y >= dSize.y ) return;

  // パターンサイズを読み込んで右下の座標を決定
  Coord<int> e( s.x + size.x - 1, s.y + size.y - 1 );

  // パターンはクリッピングエリア範囲内か
  if ( e.x < 0 || e.y < 0 ) return;

  // パターンの読み込み開始位置
  vector<RGB>::const_iterator it = pattern.begin();
  // 次ラインの先頭への増分
  int dx = 0;

  // クリッピング処理
  if ( s.x < 0 ) {
    dx = -s.x;
    s.x = 0;
    it += dx;
  }
  if ( e.x >= dSize.x ) {
    dx += e.x - dSize.x + 1;
    e.x = dSize.x - 1;
  }
  if ( s.y < 0 ) {
    it += size.x * ( -s.y );
    s.y = 0;
  }
  if ( e.y >= dSize.y )
    e.y = dSize.y - 1;

  // パターンの描画
  for ( int y = s.y ; y <= e.y ; ++y ) {
    for ( int x = s.x ; x <= e.x ; ++x )
      d.pset( Coord<int>( x, y ), *it++ );
    it += dx;
  }
}

画像パターンは、GPatternクラスを使って管理できるようにしてあります。コンストラクタが画像パターンの抽出処理として利用され、それを描画するときはメンバ関数 putを使用します。DrawingArea_IFは描画対象を表すオブジェクト用の基底クラスで、インターフェースだけを定義した抽象クラスです。複数のウィンドウ間で描画パターンをコピーすることを想定して、パターン取得先と描画対象は別々にすることができるようになっています。


2) 特殊なパターン描画ルーチン

今度は少し特殊なパターン描画ルーチンについて説明します。

まずは、左右・上下反転して描画するルーチンです。処理方法はいたって簡単で、左右反転の場合は左から右に描画していたのを右から左に、上下反転の場合は上から下に描画していたのを下から上に描画するだけで対応できます。上下左右同時に反転すれば、180度回転した形になります。

/* 左右反転描画 */
for ( int y = s.y ; y <= e.y ; ++y ) {
  for ( int x = e.x ; x >= s.x ; --x )
    d.pset( Coord<int>( x, y ), *it++ );
  it += dx;
}

/* 上下反転描画 */
for ( int y = e.y ; y >= s.y ; --y ) {
  for ( int x = s.x ; x <= e.x ; ++x )
    d.pset( Coord<int>( x, y ), *it++ );
  it += dx;
}

/* 180度回転描画 */
for ( int y = e.y ; y >= s.y ; --y ) {
  for ( int x = e.x ; x >= s.x ; --x )
    d.pset( Coord<int>( x, y ), *it++ );
  it += dx;
}

このまま計四つのルーチンを用意する代わりに、描画部分だけを切り替えられるようにすれば、プログラム自体を短くすることができます。描画処理部分を個別に関数や関数オブジェクトの形で用意して、引数として関数ポインタや関数オブジェクトを渡すようなやり方が考えられますが、ここではループ部分のみを切り替えることで対応したいと思います。
通常の描画処理では、x方向が左から右、y方向が上から下へ順に処理するようになっています。これが、それぞれの方向に対して反対方向から処理できるように切り替えられればよいので、座標の初期値と終了値を使って初期化して、初期値から終了値まで順に進むような処理ができるオブジェクトを用意すればうまく対処できそうです。そこで、次のようなクラスを用意します。

/* ループ制御用反復子基底クラス */
class iterator_base
{
protected:

  int current; // 現在値
  int limit;   // 終了値

public:

  // 値のセット
  virtual void set( int i1, int i2 )
    { current = i1; limit = i2; }
  // 次の値へ移動する
  virtual void next() = 0;
  // 現在値が終了値を超えたか?
  virtual bool valid() const = 0;
  // 現在値を返す
  virtual int value() const { return( current ); }
};

/* 通常ループ制御用反復子クラス */
class iterator : public iterator_base
{
public:

  void set( int i1, int i2 )
    { iterator_base::set( i1, i2 ); if ( current > limit ) swap( current, limit ); }
  void next() { ++current; }
  bool valid() const { return( current <= limit ); }
};

/* 反転ループ制御用反復子クラス */
class reverse_iterator : public iterator_base
{
public:

  void set( int i1, int i2 )
    { iterator_base::set( i1, i2 ); if ( current < limit ) swap( current, limit ); }
  void next() { --current; }
  bool valid() const { return( current >= limit ); }
};

利用するメンバ関数は四つで、setで初期化し、nextで値を増減、validで終了値を超えたかを判断し、valueで現在値を返します。これを利用すると、ループは次のように記述することができます。

for ( y.set( s.y, e.y ) ; y.valid() ; y.next() ) {
  for ( x.set( s.x, e.x ) ; x.valid() ; x.next() ) {
    d.pset( Coord<int>( x.value(), y.value() ), *it++ );
  }
  it += dx;
}

x, yiterator_baseオブジェクトへのリファレンスとして渡され、その実体は iteratorreverse_iteratorのいずれかです。ループの初期値と最終値は、メンバ関数 set内で、値の大小関係から正しい形にセットするようになっているため、パラメータを入れ替えるような対応も不要になります。iteratorreverse_iteratorのどちらを利用するかによって、反転の有無を制御することができるようになり、ルーチンを一本化することができます。


次は、下地のパターンとの重ね合わせです。ゲームなどで、背景にキャラクタを重ね合わせるとき、矩形のパターンでは背景が四角形に切り取られたように表示されます。そのため、パターンに「透明色」を設けて、その部分は背景がそのまま透過表示されるようにする工夫が必要になります。昔のアーケードゲームやファミコンなどにはスプライト機能というものがあって、背景とパターンはハードウェアが重ね合わせて処理をしていましたが、現代では、重ね合わせ処理はソフトウェアで実現するのが一般的のようです。ちなみに、昔のパソコンでは、スプライト機能がなかった上にハードウェアの性能も非常に低かったので、重ね合わせ処理やスクロール処理(背景が上下左右に流れていくように見せる処理)をソフトウェアで実現するにはかなり高度なテクニックが必要でした。「全画面スクロール」などの言葉は、プログラムの性能の高さを強調するためのものだったわけです。
処理自体はさほど難しくなく、透明色を任意に設定しておいてパターンを取り込み、描画する時に透明色のピクセルは塗らないようにすれば実現できます。

下地と重ね合わせる比率を任意に変更したい場合、パターンのカラーと下地のカラーを RGB分解してそれぞれの成分について平均値を算出し、得られた結果を新しいカラーとして描画します。True Color(24bit) の場合、例えば

赤成分緑成分青成分
2322212019181716 1514131211100908 0706050403020100

のように、赤・青・緑の成分が 1バイトごとに格納された構成になっているため、この例の場合なら

Red = ( color >> 16 ) & 0xFF;
Green = ( color >> 8 ) & 0xFF;
Blue = color & 0xFF;

とすれば、それぞれの成分が得られます。

合成後は、逆に三つの成分を上記構成に格納し直せばよいので、

color = ( Red << 16 ) | ( Green << 8 ) | Blue;

で得ることができます。

下地との重ね合わせ処理は、ピクセルを描画する部分だけを変更すればよく、描画位置を決める処理はそのまま流用できるので、今までの描画処理と同様に、ピクセル描画用の関数だけを切り替えるようにできた方が便利です。それを踏まえて、次のようにプログラムを変更します。

/**********************************************
  GPixelOp : ピクセル操作用基底(抽象)クラス
**********************************************/
class GPixelOp
{
  DrawingArea_IF* draw; // 描画用オブジェクト

protected:

  // 指定した座標のパレットを取得する
  void point( Coord<int> c, RGB& palet ) const
    { if ( isValid() ) draw->point( c, palet ); }

  // 指定した座標にドットを描画する
  void pset( Coord<int> c, const RGB& palet )
    { if ( isValid() ) draw->pset( c, palet ); }

public:

  /*
    コンストラクタ(描画用オブジェクトを指定)

    DrawingArea_IF& d : 対象の描画用オブジェクト
  */
  GPixelOp()
    : draw( 0 ) {}
  GPixelOp( DrawingArea_IF& d )
    : draw( &d ) {}

  // 仮想デストラクタ
  virtual ~GPixelOp() {}

  // 描画可能か?
  bool isValid() const
    { return( draw != 0 ); }

  // 画像(平面)の大きさを返す
  virtual Coord<int> size() const
    { return( ( isValid() ) ? draw->size() : Coord<int>( 0, 0 ) ); }

  // 描画用オブジェクトと結合する
  void bind( DrawingArea_IF& d ) { draw = &d; }
  // 他のGPixelOpの描画用オブジェクトと結合する
  void bind( const GPixelOp& op ) { draw = op.draw; }

  /*
    ピクセル操作用関数(純粋仮想関数)

    const Coord<int>& c : 操作するピクセルの座標
  */
  virtual void operator()( const Coord<int>& c ) = 0;
};

/*----------------------
  GPSet : 点描画クラス
----------------------*/
class GPSet : public GPixelOp
{
public:

  /* 公開メンバ変数 */
  RGB col; // 描画色

  /*
    コンストラクタ

    DrawingArea_IF& d : 対象の描画用オブジェクト
    unsigned int c : 色コード
    const RGB& rgb : RGBコード
  */
  GPSet( unsigned int c = 0 )
    : col( c ) {}
  GPSet( const RGB& rgb )
    : col( rgb ) {}
  GPSet( DrawingArea_IF& d, unsigned int c = 0 )
    : GPixelOp( d ), col( c ) {}
  GPSet( DrawingArea_IF& d, const RGB& rgb )
    : GPixelOp( d ), col( rgb ) {}

  // 描画処理
  void operator()( const Coord<int>& c )
    { pset( c, col ); }
};

/*------------------------------
  GMixSet : 下地と合成して描画
------------------------------*/
class GMixSet : public GPSet
{
  static const double rateMax = UCHAR_MAX; // 比率の最大値

public:

  /* 公開メンバ変数 */
  unsigned char rate; // 比率(0-255)

  /*
    コンストラクタ

    DrawingArea_IF& d : 対象の描画用オブジェクト
    unsigned int c : 色コード
    const RGB& rgb : RGBコード
  */
  GMixSet( unsigned int c = 0, unsigned char r = 255 )
    : GPSet( c ), rate( r ) {}
  GMixSet( const RGB& rgb, unsigned char r = 255 )
    : GPSet( rgb ), rate( r ) {}
  GMixSet( DrawingArea_IF& d, unsigned int c = 0, unsigned char r = 255 )
    : GPSet( d, c ), rate( r ) {}
  GMixSet( DrawingArea_IF& d, const RGB& rgb, unsigned char r = 255 )
    : GPSet( d, rgb ), rate( r ) {}

  // 描画処理
  void operator()( const Coord<int>& c );
};

/*
  GMixSet::operator() : 下地と混合して描画

  const Coord<int>& c : 描画位置
*/
void GMixSet::operator()( const Coord<int>& c )
{
  if ( ! isValid() ) return;

  RGB p;
  point( c, p );
  p = RGB(
          (unsigned char)( ( (double)( col.r() ) * (double)rate + (double)( p.r() ) * ( rateMax - (double)rate ) ) / rateMax ),
          (unsigned char)( ( (double)( col.g() ) * (double)rate + (double)( p.g() ) * ( rateMax - (double)rate ) ) / rateMax ),
          (unsigned char)( ( (double)( col.b() ) * (double)rate + (double)( p.b() ) * ( rateMax - (double)rate ) ) / rateMax )
          );
  pset( c, p );
}

/*----------------------------------
  GOverlapSet : 背景を透過して描画
----------------------------------*/
class GOverlapSet : public GPSet
{
public:

  /* 公開メンバ変数 */
  RGB transCol; // 透明色

  /*
    コンストラクタ

    DrawingArea_IF& d : 対象の描画用オブジェクト
    unsigned int c : 色コード
    const RGB& rgb : RGBコード
    const RGB& trans : 透明色
  */
  GOverlapSet( unsigned int c = 0, unsigned int trans = 0 )
    : GPSet( c ), transCol( trans ) {}
  GOverlapSet( const RGB& rgb, unsigned int trans = 0 )
    : GPSet( rgb ), transCol( trans ) {}
  GOverlapSet( unsigned int c, const RGB& trans )
    : GPSet( c ), transCol( trans ) {}
  GOverlapSet( const RGB& rgb, const RGB& trans )
    : GPSet( rgb ), transCol( trans ) {}

  GOverlapSet( DrawingArea_IF& d, unsigned int c = 0, unsigned int trans = 0 )
    : GPSet( d, c ), transCol( trans ) {}
  GOverlapSet( DrawingArea_IF& d, const RGB& rgb, unsigned int trans = 0 )
    : GPSet( d, rgb ), transCol( trans ) {}
  GOverlapSet( DrawingArea_IF& d, unsigned int c, const RGB& trans )
    : GPSet( d, c ), transCol( trans ) {}
  GOverlapSet( DrawingArea_IF& d, const RGB& rgb, const RGB& trans )
    : GPSet( d, rgb ), transCol( trans ) {}

  // 描画処理
  void operator()( const Coord<int>& c )
    { if ( col != transCol ) GPSet::operator()( c ); }
};

/*
  パターンの描画

  DrawingArea& d : パターンを描画するオブジェクト
  Coord<int> s : パターンの描画開始位置
*/
void GPattern::put( GPSet& pset, Coord<int>& s, iterator_base& x, iterator_base& y )
{
  // 描画オブジェクトの大きさを取得
  Coord<int> dSize = pset.size();

  :

  // パターンの描画
  for ( y.set( s.y, e.y ) ; y.valid() ; y.next() ) {
    for ( x.set( s.x, e.x ) ; x.valid() ; x.next() ) {
      pset.col = *it++;
      pset( Coord<int>( x.value(), y.value() ) );
    }
    it += dx;
  }
}

GPixelOpは、画像上のピクセルに対して何らかの処理を行なうための関数オブジェクト用クラスであり、この中に描画対象オブジェクト DrawingArea_IFを保持しておきます。純粋仮想関数の operator() は、描画オブジェクトに対して描画処理を行なうために利用します。GPixelOpの派生クラスとして GPSetがあり、公開メンバ変数の colを持っています。パターン描画のときは、パターンを保持した配列から色コードを GPSetにセットして、operator()を使って描画処理を行なうことを繰り返す処理を行ないます。これによって、任意の描画スタイルでパターン描画を行なうことができるようになります。サンプルでは、通常のパターン描画(GPSet)の他に、下地との合成描画(GMixSet)と、背景を透過した描画(GOverlapSet)を用意してあります。
GMixSetでは、RGB用のコンストラクタとして、赤・緑・青それぞれの成分を引数として渡す形式のものが用意されていることを前提としています。また、r(), g(), b() がそれぞれ、赤・緑・青成分を戻り値として返すメンバ関数になります。


[Go Back]前に戻る [Back to HOME]タイトルに戻る
inserted by FC2 system