ゆとりーなの日記

日記的な事を書いて行くと思はれる

第5回〜画像を描画する〜

流石にそろそろ画像ファイルを描画したいでしょということで今回は画像の描画です。
と、実装に入る前にちょっと準備を。boost先生は便利ですがコンパイル時間が長いのが不便です。あと、何故かインクルード順番によってはエラーが出る組み合わせがあるみたいなので、それを防ぐためにもインクルード順番を徹底したくなりました。
そこでコンパイル時間が短くなると言われているプリコンパイル済みヘッダなるものを作ってみることにします。

// stdafx.h
#pragma once
#include <cassert>
#include <stdexcept>
#pragma warning (disable: 4996)
#include <memory>
#pragma warning (default: 4996)
#include <string>
#define _WIN32_WINNT 0x0501
#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#pragma warning (disable: 4512)
#include <boost/signals2.hpp>
#pragma warning (default: 4512)
#include <boost/flyweight.hpp>
#include <boost/flyweight/key_value.hpp>

以上でプリコンパイル済みヘッダは完成です。実際よく使いかつ変更がないヘッダと警告抑止、設定定義等を書いておくだけです。このヘッダをVC++のプロパティ/C++/プリコンパイル済みヘッダの設定のところで、プリコンパイル済みヘッダを使う、プリコンパイル済みヘッダ名"stdafx.h"と設定します。続いて、

// stdafx.cc
#include "stdafx.h"

というソースファイルを作ります。このソースファイルのプロパティを開いて、プリコンパイル済みヘッダを作成する、プリコンパイル済みヘッダ名"stdafx.h"と設定します。最後に、コマンドラインオプションでFI/"stdafx.h"と書けば、プリコンパイル済みヘッダの設定は完了です。
プリコンパイル済みヘッダはよく使うヘッダをあらかじめコンパイルしておいてコンパイル速度をかせぐ的なことをしているので、このヘッダやインクルードしているヘッダの中身をコロコロ変えてしまうと、あらかじめやるコンパイルをそのたびにやる羽目になるので意味がありません。というわけで、標準ライブラリや今回の講座で使っているライブラリのヘッダ以外をインクルードするべきではありません。
それでは実装に入ります。画像ファイルを表示するのには、IDirect3DTexture9*を使います。この子を作るのにも、IDirect3DDevice9*が必要です。というわけでまたGraphicDeviceクラスに画像ファイルからテクスチャを作成するメソッドを追加します。

// graphicdevice.h
#pragma once
#include "deleter.h"
#include "graphicfwd.h"

class GraphicDevice : private boost::noncopyable {
public:
    Font createFont(int size, bool italic, const std::string &font_name);
    Texture createTextureFromFile(const std::string &file_name);
private:
    // 内部実装関係
};

プリコンパイル済みヘッダを使うことにしたので、今までインクルードしていたライブラリのヘッダのインクルードは根こそぎ消滅しています。あと、今回新登場の"graphicfwd.h"についての説明をしときます。

// graphicfwd.h
#pragma once
#include "texturekey.h"
#include "textureimpl.h"

class FontImpl;

typedef std::shared_ptr<FontImpl> Font;
typedef boost::flyweights::flyweight<boost::flyweights::key_value<TextureKey, TextureImpl>> Texture;

この子はいままでのフォント絡みの長ったらしい型名のtypedefやら前方宣言を書いたヘッダになります。今回作るテクスチャ絡みの型もboostを絡めた長ったらしい型名になるのでtypedefしておいた方が幸せです。
で、今回テクスチャの管理には、boost.flyweightを使います。これは等価のオブジェクトが既に存在していた場合、オブジェクトを共有してくれる動作を自動でやってくれるライブラリです。同じファイルからテクスチャを作る場合は、毎回ファイルを読み込んでテクスチャを作るよりは、既にあるテクスチャを共有してあげた方がリソース面でも生成面でもいいはずです。というわけでテクスチャを管理するクラスをboost.flyweightで管理できるように作ってあげます。
先ずは等価かどうかを判断するキーとなるクラスを作ります。

// texture_key.h
#pragma once
#include "deleter.h"

class GraphicDevice;

class TextureKey {
public:
    TextureKey(const std::string &file_name, boost::intrusive_ptr<IDirect3DDevice9> graphic_device);
    bool operator ==(const TextureKey &rhs) const;
private:
    friend std::size_t hash_value(const TextureKey &texture);
    friend class TextureImpl;
    std::string file_name_;
    boost::intrusive_ptr<IDirect3DDevice9> graphic_device_;
};

boost.flyweightのキーとして使うクラスには、等価を判断するoperator ==とそのクラスの型を取るハッシュ関数が必要になるので定義してあげます。中身とハッシュ関数がこの子に自由にアクセスできるようにfriend指定してあげます。

// texturekey.cc
#include "texturekey.h"
#include <boost/functional/hash.hpp>

TextureKey::TextureKey(const std::string &file_name, boost::intrusive_ptr<IDirect3DDevice9> graphic_device) : file_name_(file_name), graphic_device_(graphic_device) {
}

bool TextureKey::operator ==(const TextureKey &rhs) const {
    return (file_name_ == rhs.file_name_) && (graphic_device_ == rhs.graphic_device_);
}

std::size_t hash_value(const TextureKey &texture) {
    std::size_t h(0);
    // ハッシュ値の計算
    boost::hash_combine(h, texture.file_name_);
    boost::hash_combine(h, texture.graphic_device_.get());
    return h;
}

コンストラクタでは、ファイル名とテクスチャを作るのに必要なIDirect3DDevice9*を受け取ります。
等価を判断するoperator ==では、ファイル名とIDirect3DDevice9*が同じかどうかで等価を判断します。
ハッシュ関数では、複数の値のハッシュ値を組み合わせてハッシュ値を計算してくれるboost::hash_combine関数を使ってファイル名とIDirect3DDevice9*の値を使ってハッシュ値を計算して返すようにします。
キーができたので中身の方に移ります。

// textureimpl.h
#pragma once
#include "deleter.h"

class TextureKey;

class TextureImpl {
public:
    explicit TextureImpl(const TextureKey &texture_key);
    float width() const;
    float height() const;
private:
    friend class DrawObject;
    float width_;
    float height_;
    boost::intrusive_ptr<IDirect3DTexture9> texture_;
};

コンストラクタでは、先のキーを取るようにします。また、画像のサイズを取得するメソッドを宣言しておきます。画像はこの子のIDirect3DTexture9*を使って描画する必要があるので、描画を担当するDrawObjectクラスをfriendにしておきます。

// textureimpl.cc
#include "textureimpl.h"
#include "graphicdevice.h"
#include "texturekey.h"

namespace {
IDirect3DTexture9 *createTextureFromFile(const std::string &file_name, boost::intrusive_ptr<IDirect3DDevice9> graphic_device, float *width, float *height) {
    assert(graphic_device.get());
    IDirect3DTexture9 *texture(nullptr);
    D3DXIMAGE_INFO image_info;
    // テクスチャを作成する
    if (FAILED(D3DXCreateTextureFromFileEx(graphic_device.get(), file_name.c_str(), D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_DEFAULT, D3DX_DEFAULT, 0, &image_info, nullptr, &texture))) {
        throw std::runtime_error("テクスチャの作成に失敗");
    }
    *width = static_cast<float>(image_info.Width);
    *height = static_cast<float>(image_info.Height);
    return texture;
}
}

TextureImpl::TextureImpl(const TextureKey &texture_key) : width_(0), height_(0), texture_(createTextureFromFile(texture_key.file_name_, texture_key.graphic_device_, &width_, &height_), false) {
}

float TextureImpl::width() const {
    return width_;
}

float TextureImpl::height() const {
    return height_;
}

例によってIDirect3DTecture9*のスマポの初期化を円滑にするために、boost::intrusive_ptrを返すcreateTextureFromFileを作ります。中身はIDierect3DDecice9*と画像サイズを保存する変数を受け取って、D3DXCreateTextureFromFileEx関数を使って画像ファイルからテクスチャを使います。引数は上記のままで特に問題ないです。image_infoに画像のサイズが入っているのでこれを受け取って画像サイズをポインタを使って書き換えてあげます。
画像サイズを返すメソッドは見たまんまです。
ここまでできたらあとはGraphicDevice::createTextureFromFileメソッドを実装できます。

// graphicdevice.cc
Texture GraphicDevice::createTextureFromFile(const std::string &file_name) {
    assert(graphic_device_.get());
    return Texture(TextureKey(file_name, graphic_device_));
}

TextureKeyをキー、TexureImplが中身のboost::flyweights::flyweight>オブジェクトを生成して返してやります。生成にはキーを引数として渡すの必要があるのでTextureKeyクラスにファイル名とIDirect3DDevice9*を与えて作成したキーを渡してあげます。
これで同じファイル名、同じIDirect3DDevice9*でテクスチャを作る要請が出た場合は、リソースが共有される仕組みができました。
続いてこのTextureことboost::flyweights::flyweight>を使って描画ができるようにDrawObject::drawImageメソッドを実装します。

// drawobject.h
#pragma once
#include "deleter.h"
#include "graphicfwd.h"

class DrawObject : private boost::noncopyable {
public:
    // インターフェイス
    void drawImage(float x, float y, const D3DXCOLOR &color, Texture texture);
private:
    // 内部実装関係
};

引数には左上座標と色、Textureを取るようにします。

drawobject.cc
namespace {
// 2D画像を描画するのに必要なバーテクス構造体
struct VertexImage2D {
    float x;
    float y;
    float z;
    float rhw;
    DWORD color;
    float u;
    float v;
};
// 2D画像を描画するのに必要なFVF
const DWORD kFvfVertexImage2D(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1);
}

void DrawObject::drawBox(float left, float top, float right, float bottom, const D3DXCOLOR &color) {
    assert(graphic_device_.get());
    const VertexBox2D vertex[4] = {
        {left, top, 0.f, 1.f, color},
        {right, top, 0.f, 1.f, color},
        {left, bottom, 0.f, 1.f, color},
        {right, bottom, 0.f, 1.f, color}
    };
    // テクスチャ描画を切る
    graphic_device_->SetTexture(0, nullptr);
    graphic_device_->SetFVF(kFvfVertexBox2D);
    graphic_device_->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, vertex, sizeof(vertex[0]));
}

void DrawObject::drawImage(float x, float y, const D3DXCOLOR &color, Texture texture) {
    assert(graphic_device_.get());
    assert(texture.get().texture_.get());
    const VertexImage2D vertex[4] = {
        {x, y, 0.f, 1.f, color, 0.f, 0.f},
        {x + texture.get().width(), y, 0.f, 1.f, color, 1.f, 0.f},
        {x, y + texture.get().height(), 0.f, 1.f, color, 0.f, 1.f},
        {x + texture.get().width(), y + texture.get().height(), 0.f, 1.f, color, 1.f, 1.f}
    };
    // 描画テクスチャを指定
    graphic_device_->SetTexture(0, texture.get().texture_.get());
    graphic_device_->SetFVF(kFvfVertexImage2D);
    graphic_device_->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, vertex, sizeof(vertex[0]));
}

バーテクス構造体が、四角形を描画した時に比べて少し増えます。画像のどこを使って描画するかを指定するu、vメンバを追加します。画像ファイルの左上が(0.f, 0.f)、右下が(1.f, 1.f)となります。また、描画テクスチャを指定するIDirect3DDevice9::SetTextureメソッドで、描画するIdirect3DTexture9*を指定します。boost::flyweights::flyweight>オブジェクトでは、get()メソッドで中のTextureImplオブジェクトが取れるので、これ経由でIdirect3DTexture9*取得します。その他座標の指定方法等は四角形の時と同じです。あと、描画テクスチャを指定した後、前に作った四角形を描画するメソッドを呼ぶと非常に残念なことになるので、DrawObject::drawBoxメソッド内では描画テクスチャなしにするため、IDirect3DDevice9::SetTextureにnullptrを渡してやります。
これで画像が描画できるようになりました。早速画像を描画してみましょう。今回描画する画像はこちら。

例によってアリスさん"alice.png"です。サイズ的に画面からはみ出ますが気にしません。

// testapplication.h
#pragma once
#include "application.h"
#include "graphicfwd.h"

class TestApplication : public Application {
public:
    TestApplication(const std::string &caption, int client_width, int client_height, bool windowed);
protected:
    virtual void update(DrawObject *draw_object);
private:
    Texture alice_;
};

// testapplication.cc
#include "testapplication.h"
#include "graphicdevice.h"
#include "drawobject.h"

TestApplication::TestApplication(const std::string &caption, int client_width, int client_height, bool windowed) : Application(caption, client_width, client_height, windowed), alice_(graphic_device_->createTextureFromFile("alice.png")) {
}

void TestApplication::update(DrawObject *draw_object) {
    assert(draw_object);
    draw_object->drawImage(0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), alice_);
}

実行結果

無事アリスさんが描画できました。