ホーム < ゲームつくろー! < OpenGL技術編

その4 テクスチャを貼ってみよう


 ポリゴンのみの描画はどうやらできました。ワールド空間も作れました。なら次はテクスチャを貼りたくなるわけです。



@ OpenGLでのテクスチャ使用

 OpenGLでのテクスチャの使い方を調べてみたのですが、DirectXより少しだけ面倒かもしれません。DirectX9だったらD3DXCreateTextureFromFile関数で画像ファイルからテクスチャを作れますが、OpenGLにはテクスチャをファイル等から読み込んでくれるヘルパーはありません。ん〜…メンドイのぉ…。これについては後で考えます。

 OpenGLでは幾つかの方法でテクスチャを扱えるようです。その中で多分一番簡単なのは、テクスチャブロック(メモリブロック)をこちらで用意し、使う時にそれをOpenGLに教える方法かなと思います。

 取りあえず、ダミーのテクスチャ情報をハードコーディングで作ってみましょう。例えばこんな感じの色情報です:

ダミーテクスチャ
// ダミーテクスチャ
static unsigned char tex[] = {
    255, 255, 255, 255,     0,   0,   0, 255,   255, 255, 255 ,255,     0,   0,   0, 255,
    255,   0,   0, 255,     0, 255,   0, 255,     0,   0, 255 ,255,   255, 255, 255, 255,
    128,   0,   0, 255,     0, 128,   0, 255,     0,   0, 128 ,255,   128, 128, 128, 255,
    255, 255,   0, 255,   255,   0, 255, 255,     0, 255, 255 ,255,   255, 255, 255, 255,
};

こういう生データを直にがりっと書いてみるのは、仕組みを理解するのに非常に役立つんですよ〜。上のメモリブロックはRGBAな32bitテクスチャブロックで、幅4高4です。実際の色はこんな感じ:

こういうテクスチャカラーのメモリブロックがあるとして、それをOpenGLにセットするにはglTexImage2D関数を使います:

glTexImage2D関数
void glTexImage2D(
    GLenum target,
    GLint level,
    GLint internalformat,
    GLsizei width,
    GLsizei height,
    GLint border,
    GLenum format,
    GLenum type,
    const GLvoid *pixels
);

targetはテクスチャの形式で「GL_TEXTURE_2D」固定です。
levelはミップマップの数を指定します。0にするとミップマップを使いません。
internalformatはOpenGLが扱うテクスチャのフォーマットタイプです。沢山種類があるのですが、GL_RGBAかGL_RGBが一般的かなと思います。意味はわかりますよね。
widthheightはそれぞれテクスチャの幅と高さです。
borderは読み込むテクスチャの周囲にborder幅の境界線があるとみなして、その内側だけを採用するようです。面白いのですが…普通0ですね(^-^;
formatは読み込むテクスチャメモリブロックのフォーマットタイプです。internalformat同様にGL_RGBAかGL_RGBなどで指定します。
typeには1画素の1成分(RGBAそれぞれ)の型を指定します。RGBAならばunsigned charなのでGL_UNSIGNED_BYTEを指定します。浮動小数点テクスチャであればGL_FLOATになるのかな。
pixelsにはテクスチャメモリブロックの先頭ポインタを渡します。ここでOpenGLはテクスチャ情報を得られるわけです。当然ですが、内部では参照しているだけで、ハードコピーはしていないはずです。ですから、描画が終わるまでテクスチャメモリブロックはオンメモリにしておかないといけません。

 先のダミーテクスチャの場合はこうなります:

glTexImage2D関数使用例
glTexImage2D(
    GL_TEXTURE_2D,
    0,          // mipmap
    GL_RGBA,
    4,          // width
    4,          // height
    0,          // border
    GL_RGBA,
    GL_UNSIGNED_BYTE,
    tex
);

これでOpenGLは指定したテクスチャを内部にセットします。

 続いて、ミップマップ設定をします。拡大縮小した時にテクスチャをどう扱うかという設定です。これ、省いてもいいのかなぁと思ったら指定しないと駄目のようです。こんな感じで指定します:

ミップマップ設定
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // 拡大時近傍
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // 縮小時近傍

glTexParameteri関数で色々と指定するようです。そういうもんです。

 次にテクスチャをどう貼り付けるかをglTexEnvf関数で指定します。例えばポリゴンの色と掛け算したい時(乗算合成)は、

乗算合成
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

と設定します。最後のGL_MODULATEが掛け算のフラグです。ここをGL_DECALにするとデカールのようにベタっと貼り付けます。

 最後に「2Dテクスチャ使用を有効にしますよ〜」とOpenGLに教えるためにglEnable関数を使い、

テクスチャを有効に
glEnable(GL_TEXTURE_2D);

とフラグ指定します。これでテクスチャ側の準備はできました(^-^)/



A 頂点にUVを追加

 ポリゴンにテクスチャを貼るにはUV座標を頂点に指定しなければなりません。そこで頂点構造体にUV座標を追加しましょう:

頂点構造体にUVを
struct Vertex {
    float x, y, z;
    union {
        unsigned color;
        struct { unsigned char b, g, r, a; };
    };
    float u, v;
};

前章までのマルチライブラリなサンプルでは実際の頂点はApplication::initializeメソッドの定義していたのでした。それを新しくUV座標を追加した頂点情報に書き換えます:

頂点にUVを
Vertex v[4] = {
    {-10.0f, -10.0f, 0.0f, 0xffe0e060, 0.0f, 1.0f},
    {-10.0f,  10.0f, 0.0f, 0xffe0e060, 0.0f, 0.0f},
    { 10.0f, -10.0f, 0.0f, 0xffe0e060, 1.0f, 1.0f},
    { 10.0f,  10.0f, 0.0f, 0xffe0e060, 1.0f, 0.0f},
};

後は先程のテクスチャ情報をOpenGLRenderer::render内で設定するとちゃ〜んとポリゴンにテクスチャが貼られます:

うんうん(^-^)。



B さ、ここからが本番。ライブラリ非依存にします(OpenGL、DirectX両対応)

 さて、実際のゲームで上のテクスチャを貼る事を考えてみます。前章までのOpenGL/DirectX9の両方に対応しているシステムがゲームエンジンのサンプルになっています。このシステムで同じテクスチャ情報をOpenGL版とDirectX9版の両方に対応できればミッションクリアです(^-^)。

 まずどこかでテクスチャを作らなければなりませんね。前章までのシステムでは、Application::initializeメソッドの中で具体的なリソースを作っていたのでした。テクスチャのデータ元は普通ファイルにありますが、今はそれを読み込んだとしてこのメソッド内に情報を羅列してみます:

頂点にUVを
virtual bool initialize() {

    ....

    // テクスチャ生成
    unsigned char tex[] = {
        255, 255, 255, 255,    0,   0,   0, 255,    255, 255, 255, 255,    0,   0,   0, 255,
        255,   0,   0, 255,    0, 255,   0, 255,      0,   0, 255, 255,  255, 255, 255, 255,
        128,   0,   0, 255,    0, 128,   0, 255,      0,   0, 128, 255,  128, 128, 128, 255,
        255, 255,   0, 255,  255,   0, 255, 255,      0, 255, 255, 255,  255, 255, 255, 255,
    };

    // 依存性がない情報
    this->texWidth  = 4;
    this->texHeight = 4;
    this->texBit    = 32;
    this->texPitch  = 4 * texBit / 8;
    this->texBufferSize = sizeof(tex);
   this->txtBuffer = new unsigned char[texBufferSize];
    memcpy(this->txtBuffer, tex, texBufferSize);

    // むむ!依存性出現!
   this->texFormat     = GL_RGBA;
    this->texMinFilter  = GL_NEAREST;
    this->texMagFilter  = GL_NEAREST;
    this->texTexOperand = GL_DECAL;

    ....

    return true;
}

こんな感じでしょうか。ただコメントにもありますが、OpenGL専用の値が入っています。これ、どうしたもんかなぁなのです。

 当然の事ながら、DirectX9はGL_RGBAというマクロ値は知りません。DirectX9が知っているのはD3DFMT_RGBA8888という別のマクロ値です。両方のマクロは実質整数なので、「整数値」としては共通ですが、数値の互換性などもちろんありません。というよりも、ここにDirectX9やOpenGLに固有の情報を含めるのはもうアウトです。厳しいもんです、うん(-_-;

 ここでの共通項は「32bitでRGBAなフォーマット」というフォーマット形式、そしてMin/Mag共にNearest(近い方の色を採用)でDecal貼り」という「テクスチャの使い方」です。これはOpenGLでもDirectX9でも出来ます。であれば、エンジンが認識する「クラス」で両者の違いを吸収してしまえばライブラリ非依存になるのでは無いでしょうか。もちろん、上の依存性のない情報もそこに入れてしまえばよりすっきりします。

 何せテクスチャの情報ですから、作るクラス名は当然Textureクラスです。そこに依存性が無い情報をメンバ変数として追加します。Formatもテクスチャ固有の情報なので「整数値」として追加します。ただし「texBuffer」は入れません!これはOpenGLが使う情報で、入れるとしたら派生クラスであるOpenGLTextureクラスでしょうね。

 一方でGL_NEARESTやGL_DECALなどのように「テクスチャをどう貼り付けるか?」という情報はテクスチャ固有ではありません(同じテクスチャでも違う貼り方をしたい事があります)。そこでこれはTextureInstanceクラスというTextureクラスの複製物を表す物にその情報を入れます。「Instance」というのは「複製した物」というニュアンスがあります。もちろんテクスチャをバリバリにハードコピーしたらメモリがあっという間にぶっ飛びますので、TextureInstanceはTextureオブジェクトを参照するだけのナローコピーで複製を行います。それぞれのクラスは次のようになりますね:

Textureクラス
class Texture {
public:
    struct Desc {
        unsigned width, height, bit, pitch, bufferSize, format;
        Desc() : width(), height(), bit(), bufferSize(), pitch(), format() {}
    };

protected:
    Desc desc;

public:
    Texture() {}
    virtual ~Texture() {}

    // パラメータ取得
    const Desc &getDesc() const { return desc; }

    // リソース取得
    virtual void *getResource() = 0;
};
TextureInstanceクラス
#include "texture.h"
#include "oxsp.h"

class TextureInstance {
public:
    struct Desc {
        OX::sp<Texture> texture;
        unsigned minFilter, magFilter;
        Desc() : minFilter(), magFilter() {}
    };

protected:
    Desc desc;

public:
    TextureInstance() {}
    ~TextureInstance() {}

    // パラメータ取得
    const Desc &getDesc() const { return desc; }
    Desc &getDesc() { return desc; }
};

パラメータの取得は内部の構造体(Desc)を返すようにした方がイタズラにゲッターメソッドが増えずに便利です。またファイルからの入出力もパラメータが構造体にまとまっていた方がメモリブロックとしてドンと読み書きできるので扱いやすくなります。お勧めです(^-^)。

 Textureクラスには具体的なテクスチャバッファはありません。それは派生クラス(OpenGLTexture及びDX9Textureクラス)に置きます。でも、それらクラスのオブジェクトをApplication::initializeメソッド内で作ってはいけません。だって、そのクラスはOpenGLやDirectXに特化しているんですもの。じゃあどうするか?一つの策はTextureクラスにファクトリクラスを登録し、代わりに作ってもらうという案です。

 TextureFactoryクラスはcreateメソッドを持っていて、それをテクスチャ情報と共に呼ぶと、そういうテクスチャを作ってくれます。TextureFactoryクラスがOpenGL用のテクスチャを返すのかDX9用なのかは具体的にはわかりません。これでライブラリ非依存な状態になります。

 「ちょ、ま、だって、それじゃレンダラ(DX9Rendererとか)がテクスチャを認識できないじゃん。」と思われるかもしれません。そこは「暗黙の了解」で行きましょう。上のTextureクラスにgetResourceメソッドが純粋仮想関数で定義されています。これを通してテクスチャバッファを取得するようにします。

 ではそのファクトリクラス自体をどこで作るのかを次に考えます。OpenGL用のテクスチャファクトリはOpenGLTextureFactoryクラスとなりますが、ここにだって具体的なOpenGL用の何かが入るかもしれません。ライブラリ依存があるわけです。ただですね、前章までのエンジンでバリバリにライブラリ依存なコードを書いていた部分があります。それは「openglmain.cpp」、そうOpenGL用のメイン関数内です。ちょっと見てみましょう:

opengl/openglmain.cpp
#include <tchar.h>
#include "../application.h"
#include "../gamewindowwin.h"
#include "openglrenderer.h"

int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    // ゲームウィンドウ初期化
    GameWindowWin window;
    if (!window.create(hInstance, "OpenGLテスト", wndProc))
        return -1;

    // OpenGLレンダラ初期化
    OpenGLRenderer renderer(window.getDC());
    if (!renderer.initialize())
        return -2;

    // アプリスタート
    Application app(window, renderer);
        return app.run();
}

OpenGLRendererというOpenGL用のレンダラオブジェクトを作っていますよね。テクスチャのファクトリオブジェクトもここで作ればいいんです。ここなら、OpenGLTextureFactoryオブジェクトを作っても誰も文句は言いません。つまりは、こんな感じになるわけです:

opengl/openglmain.cpp
#include <tchar.h>

#include "../application.h"
#include "../gamewindowwin.h"
#include "../texture.h"
#include "openglrenderer.h"
#include "opengltexturefactory.h"


int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int)
{
    // ゲームウィンドウ初期化
    GameWindowWin window;
    if (!window.create(hInstance, "OpenGLテスト", wndProc))
        return -1;

    // OpenGLレンダラ初期化
    OpenGLRenderer renderer(window.getDC());
    if (!renderer.initialize())
        return -2;

    // テクスチャファクトリを登録
    OpenGLTextureFactory textureFactory;
    Texture::registerFactory(&textureFactory);

    // アプリスタート
    Application app(window, renderer);
    return app.run();
}

OpenGL用のテクスチャファクトリをメイン関数内で作り、それをTexture::registerFactoryメソッドで登録します。このメソッドはstaticなメソッドなので、以後このファクトリオブジェクトはTextureクラスのクラスメンバとして共用して使えます。

 テクスチャを作る時にはTexture::createNewメソッドを通して新しいテクスチャを貰います:

texture.h
class TextureFactory;
class Texture {
public:
    struct Desc {
        unsigned width, height, bit, pitch, bufferSize, format;
        Desc() : width(), height(), bit(), bufferSize(), pitch(), format() {}
    };

protected:
    static TextureFactory *factory;
    Desc desc;

public:
    Texture() {}
    virtual ~Texture() {}

    // テクスチャファクトリを登録
    static void registerFactory(TextureFactory* fct) { factory = fct; }

    // テクスチャ生成
    static Texture *createNew(const Desc &desc, void *buffer);

    // パラメータ取得
    const Desc &getDesc() const { return desc; }

    // リソース取得
    virtual void *getResource() = 0;
};

createNewメソッドに作成したいテクスチャの情報(Desc)とテクスチャバッファを渡せば作ってくれるという算段です。

 赤文字のTextureFactoryクラスを事前宣言しているのが注意点です。ここに#include "texturefactory.h"とヘッダーで宣言してはいけません。なぜなら、TextureFactoryクラス内でもTextureクラスは扱います。両方のクラスでそれぞれのヘッダーをインクルードすると「循環参照インクルード」になってしまい、コンパイラに怒られます。循環が起こる場合は、どちらかを事前宣言しなければならないんです。そのため、createNewメソッドの実体もヘッダーには書けませんので、texture.cppに具体的な実装を記述します:

texture.cpp
#include "texture.h"
#include "texturefactory.h"

TextureFactory *Texture::factory = 0;

// テクスチャ生成
Texture *Texture::createNew(const Desc &desc, void *buffer) {
    if (!factory)
        return 0;
    return factory->create(desc, buffer);
}

こうなるわけです。ここでファクトリクラスを通してテクスチャを作ってもらいます。

 OpenGL用のテクスチャファクトリクラスは、内部でOpenGLTextureオブジェクトを具体的に作成し、それをTexture*型として返します:

opengl/opengltexturefactory.h
#include "../texturefactory.h"
#include "opengltexture.h"

class OpenGLTextureFactory : public TextureFactory {
public:
    OpenGLTextureFactory() {}
    virtual ~OpenGLTextureFactory() {}

    // テクスチャ作成
    virtual Texture *create(const Texture::Desc &desc, void* buffer) {
        OpenGLTexture *newTex = new OpenGLTexture;
        newTex->create(desc, buffer);
        return newTex;
    }
};

実際はOpenGLTexture自体に情報を与えてテクスチャを作ってもらっているわけです。

 DirectX9の場合もこれと全く同じでDX9TextureFactoryクラスをTextureFactoryクラスから派生させて、上と同じように内部でDX9Textureクラスのオブジェクトをnewして生成すればいいんです。ファクトリオブジェクトはメイン関数でのみ呼ばれるので、その内部は思いっきりライブラリ依存になって構わないわけです。

 DX9Textureクラスは具体的に次のような実装になります:

dx9/dx9texture.h
#include "../oxcp.h"
#include "../texture.h"
#include <d3d9.h>

class DX9Texture : public Texture {
    OX::cp<IDirect3DTexture9> tex;

public:
    DX9Texture() {}
    virtual ~DX9Texture() {}

    // リソース取得
    virtual const void *getResource() const { return tex.getPtr(); }

    // テクスチャ作成
    void create(IDirect3DDevice9 *dev, const Desc &desc, void *buffer) {
        D3DFORMAT format = (desc.format == Texture::RGBA8888) ? D3DFMT_A8R8G8B8 : D3DFMT_R8G8B8;
        if (FAILED(dev->CreateTexture(desc.width, desc.height, 0, 0, format, D3DPOOL_MANAGED, tex.toCreator(), 0)))
            return;
        D3DLOCKED_RECT r;
        if (FAILED(tex->LockRect(0, &r, 0, 0)))
            return;
        memcpy(r.pBits, buffer, desc.bufferSize);
        tex->UnlockRect(0);
    }
};

DirectX9はIDirect3DTexture9インターフェイスがテクスチャを管理しますので、そこにテクスチャデータを流し込みます。引数に描画デバイスがありますね。これがOpenGLTextureと違います。これを与えるのはDX9TextureFactoryクラスです:

dx9/dx9texturefactory.h
#include "../texturefactory.h"
#include "dx9texture.h"

class DX9TextureFactory : public TextureFactory {
    IDirect3DDevice9 *dev;

public:
    DX9TextureFactory(IDirect3DDevice9 *dev) : dev(dev) {}
    virtual ~DX9TextureFactory() {}

    // テクスチャ作成
    virtual Texture *create(const Texture::Desc &desc, void* buffer) {
        DX9Texture *newTex = new DX9Texture;
        newTex->create(dev, desc, buffer);
        return newTex;
    }
};

コンストラクタの引数で描画デバイスを与えます。描画デバイスはdx9main.cpp内で作成されますので、その時にこのクラスのオブジェクトもも作ってしまいます。createメソッドでDX9Textureを作っていますね。


 さて…もう頭ごっちゃごちゃでしょうか(笑)。

 筋道はこういう事です。
 OpenGLの場合、openglmain.cppでOpenGLTextureFactoryオブジェクトを作ります。そのオブジェクトをTexture::registerFactoryメソッドに登録します。非依存性な場所でテクスチャが欲しい時、Texture::createNewメソッドにテクスチャの情報を与えます。すると登録したファクトリオブジェクト(OpenGLTextureFactory)がOpenGLTextureを作ります。これが返る時にはTextureオブジェクトにアップキャストされるため、依存性は無くなります。

 DirectX9の場合、dx9main.cppでDX9TextureFactoryオブジェクトを作ります。ここでIDirect3DDevice9も与えられます。そのファクトリをTexture::registerFactoryメソッドに登録します。非依存性な場所でテクスチャが欲しい時、Texture::createNewメソッドにテクスチャの情報を与えます。すると登録したファクトリオブジェクト(DX9TextureFactory)がDX9Textureを作ります。これが返る時にはTextureオブジェクトにアップキャストされるため、依存性は無くなります。

 依存性が全部メイン関数内で吸収されているため、エンジン側はライブラリを意識せずにテクスチャを作成し、それを描画する事ができます。ここまでのサンプルを挙げておりますので宜しければご参照下さい。


 テクスチャを単純に貼る事ができるだけでも、もう色付きの3Dモデルを描画する事ができるようになります。ただ、このままだとのっぺりとした3Dモデルになってしまいます。そう、陰影が無いわけです。次はマテリアルを導入して3Dモデルに影を付けてみましょう。