読者です 読者をやめる 読者になる 読者になる

OpenGLをC++11でラップしてみんとてするなり 第6回

それでは今回は、行列計算のラッパークラスを作りたいと思います。

●4x4行列

OpenGLでの行列のメモリモデルは、次のようになっています。

\left( \begin{array}{cccc} a_0 & a_4 & a_8 & a_12 \\ a_1 & a_5 & a_9 & a_13 \\ a_2 & a_6 & a_10 & a_14 \\ a_3 & a_7 & a_11 & a_15 \\ \end{array} \right)

次元数を任意に拡張して、さらに要素型も任意にできるよう、次のようなクラスにしてみます。
今回は長くなるので省きましたが、演算処理を真面目に実装しようとするとC++11の機能の1つ、decltype辺りの解説になってくれるはずです(要素型がT*Uになる必要が出てくるため)。

Matrix.h

#ifndef Matrix_h
#define Matrix_h

#include <stddef.h>

/**
 * M×N行列クラス。
 *
 * @param M 行の数。
 * @param N 列の数。
 * @param T 要素の型。
 */
template <size_t M, size_t N, class T = float>
class Matrix
{
  /**
   * 要素。
   */
  T m_elm[M * N] ;

public :
  /**
   * m_elm[] を初期化しないコンストラクタ。
   */
  Matrix() = default ;

  /**
   * コピーコンストラクタ。
   *
   * @param U       コピー元行列の要素型。
   * @param another コピー元行列
   */
  template <class U>
  Matrix(const Matrix<M, N, U>& another)
  {
    T *p = m_elm ;
    const U *q = (const U*)another ;
    for (T *pEnd = p + M*N ; p < pEnd ; ) *p++ = (T)*q++ ;
  }

  /**
   * 配列で値を指定して初期化。
   */
  Matrix(const T (&mat)[M][N])
  {
    for (size_t m = 0 ; m < M ; ++m) {
      for (size_t n = 0 ; n < N ; ++n) {
        m_elm[m + M * n] = mat[m][n] ;
      }
    }
  }

  /**
   * 要素の取得/設定。
   *
   * @param  m 行
   * @param  n 列
   * @return   m行n列の要素の値
   */
  T& at(size_t m, size_t n) { return m_elm[m + M * n] ; }
  const T& at(size_t m, size_t n) const { return m_elm[m + M * n] ; }

  /**
   * 要素配列へのポインタを取得。
   *
   * @return 要素(0,0)へのポインタ。
   */
  explicit operator const T *() const { return &m_elm[0]; }
  explicit operator T *() { return &m_elm[0]; }

public :
  /**
   * ゼロ行列。
   */
  static const Matrix O ;
} ;

template <size_t M, size_t N, class T>
const Matrix<M, N, T> Matrix<M, N, T>::O = {{0}};

// 4x4行列の場合、Matrix4<型名>と書けるようにする(C++11より登場)
template <class T = float>
using Matrix4 = Matrix<4, 4, T> ;

// さらに、4x4のfloat行列の場合、4x4のdouble配列の場合の別名を作る
typedef Matrix4<float> Matrix4f ;
typedef Matrix4<double> Matrix4d ;

#endif

危うくこのクラスの実装だけで満足してしまいそうになりますが、今回のメインはgl::ContextクラスにOpenGLの行列スタック操作処理を追加することでした。
glEnabled()をラップした時ほど丁寧にはやりませんが、大体このようになるはずです。

gl/Matrix.h

#ifndef gl__Matrix_h
#define gl__Matrix_h

#include "../noncopyable.h"
#include "../glall.h"
#include "../Matrix.h"
#include <stdexcept>

namespace gl
{
  /**
   * 一時的に行列スタックを変更するためのクラス。
   *
   * @param SetterConst glMatrixMode()に指定する定数。
   * @param GetterConst glGet*()に指定する定数。
   */
  template <GLenum SetterConst, GLenum GetterConst>
  class Matrix : noncopyable<Matrix<SetterConst, GetterConst>>
  {
    // gl::Contextクラスでのみインスタンス化可能。
    friend class Context ;

  private :
    /**
     * 現在値をスタックに保存。
     */
    Matrix()
    {
      // matrix modeを変更
      GLint mode ;
      glGetIntegerv(GL_MATRIX_MODE, &mode) ;
      if (mode != SetterConst) glMatrixMode(SetterConst) ;

      // 現在値を退避
      glPushMatrix() ;
      if (GL_STACK_OVERFLOW == glGetError()) {
        throw std::overflow_error("matrix stack overflow.") ;
      }

      // matri modeを元に戻す
      if (mode != SetterConst) glMatrixMode(mode) ;
    }

  public :
    /**
     * ムーブコンストラクタ。
     */
    Matrix(Matrix &&status) : m_moved(status.m_moved)
    {
      status.m_moved = true ;
    }

    /**
     * デストラクタ。
     */
    ~Matrix()
    {
      // 移動済みでない場合は初期値に戻す
      if (!m_moved) {
        // matrix modeを変更
        GLint mode ;
        glGetIntegerv(GL_MATRIX_MODE, &mode) ;
        if (mode != SetterConst) glMatrixMode(SetterConst) ;

        // 退避した値に戻す
        glPopMatrix() ;

        // matrix modeを元に戻す
        if (mode != SetterConst) glMatrixMode(mode) ;
      }
    }

    /**
     * 行列値を設定する。
     *
     * @param  enabled 新しい行列値。
     * @return         オブジェクト自身。
     */
    Matrix &operator = (const Matrix4f &mat)
    {
      // matrix modeを変更
      GLint mode ;
      glGetIntegerv(GL_MATRIX_MODE, &mode) ;
      if (mode != SetterConst) glMatrixMode(SetterConst) ;

      // 行列値の設定
      glLoadMatrixf((const float *)mat) ;

      // matrix modeを元に戻す
      if (mode != SetterConst) glMatrixMode(mode) ;

      return *this ;
    }

    /**
     * 行列値を取得。
     *
     * @return 現在の行列値。
     */
    operator Matrix4f() const
    {
      Matrix4f mat ;
      glGetFloatv(GetterConst, (float *)mat) ;

      return mat ;
    }

    /**
     * 行列を乗算する。
     */
    Matrix &operator *= (const Matrix4f &mat)
    {
      // matrix modeを変更
      GLint mode ;
      glGetIntegerv(GL_MATRIX_MODE, &mode) ;
      if (mode != SetterConst) glMatrixMode(SetterConst) ;

      // 乗算
      glMultMatrixf((const float *)mat) ;

      // matrix modeを元に戻す
      if (mode != SetterConst) glMatrixMode(mode) ;

      return *this ;
    }

    // glTranslate*()等は省略

  private :
    bool m_moved ;
  } ;
}

#endif

glPushMatrix()時にオーバーフローが起こった場合は例外を投げるようにしましたが、必要ならば現在値をglGet*()してデストラクタでglLoadMatrix*()するようにもできるかもしれません。
重要なのは、各種の処理の前後でglMatrixMode()を呼んでいる部分です。この部分で処理対象となる行列スタックを指定しているのがわかるかと思います。一方で、値の取得時は直接指定した行列スタックから値を取得できるため、行列スタックの指定処理が入っていません。

gl::Contextクラスに追加するコードは、次のようになるでしょう。

gl/Context.h

#define GLCONTEXT_MATRIX(ConstName, name)              \
    Matrix<ConstName, ConstName##_MATRIX> name() {     \
      return Matrix<ConstName, ConstName##_MATRIX>() ; \
    }

    GLCONTEXT_MATRIX(GL_PROJECTION, projection) ;
    GLCONTEXT_MATRIX(GL_MODELVIEW, modelview) ;
    GLCONTEXT_MATRIX(GL_TEXTURE, texture) ;
    GLCONTEXT_MATRIX(GL_COLOR, color) ;

#undef GLCONTEXT_MATRIX

サンプルコードのようにすると、上側の長方形だけが半分の幅で表示され、下側の長方形を描画する際には元に戻っていることが確認できます。