文字列をテクスチャ化する

OpenGL ES は文字列が扱えないので、文字列をテクスチャ化するためのクラスを作ってみた。

class string_texture
{
    context_ptr context_;
    brew_ptr<IDisplay> display_;
    brew_ptr<IDIB> dib_; // 文字描画用のテンポラリ

public:
    string_texture(const context_ptr& c, IDisplay* disp)
        : context_(c), display_(disp)
    {
    }

    shared_ptr<texture> make_texture(const font& f, const AECHAR* str, bool linear)
    {
        // 文字の長さと高さを取得
        int w = f.string_width(str);
        int h = f.height();

        // dib_ を適切な大きさにして
        resize(w, h);
        // dib_ に描画して
        draw_string(f, str);
        // dib_ のピクセルデータをインデックス化
        auto_array<byte> buf;
        int size;
        make_index(w, h, buf, size);
        // テクスチャ生成
        return shared_ptr<texture>(new texture(
            context_, texture_image_type::index4, texture_palette_type::rgba4444,
            w, h, floor(w), floor(h),
            buf.get(), size, linear));
    }

private:
    static int32 floor(uint32 val)
    {
        uint32 n = math::floor(val);
        return std::max<int32>(static_cast<int32>(n), 4);
    }
    static void throw_if_failed(int result)
    {
        if (result != SUCCESS)
        {
            throw brew_exception(result);
        }
    }

    void resize(int w, int h)
    {
        if (!dib_ || dib_->cx < w || dib_->cy < h)
        {
            brew_ptr<IBitmap> device;
            throw_if_failed(IDISPLAY_GetDeviceBitmap(display_.get(), device.address()));
            int w2 = dib_ ? std::max<int32>(w, dib_->cx) : w;
            int h2 = dib_ ? std::max<int32>(h, dib_->cy) : h;
            brew_ptr<IBitmap> bitmap;
            throw_if_failed(IBITMAP_CreateCompatibleBitmap(device.get(), bitmap.address(), w2, h2));
            dib_.reset();
            throw_if_failed(IBITMAP_QueryInterface(bitmap.get(), AEECLSID_DIB, reinterpret_cast<void**>(dib_.address())));
        }
    }

    struct dest_guard
    {
        IDisplay* p_;
        IBitmap* old_;
        dest_guard(IDisplay* p, IBitmap* bmp) : p_(p)
        {
            old_ = IDISPLAY_GetDestination(p);
            if (old_ == nullptr) throw brew_exception();
            IDISPLAY_SetDestination(p, bmp);
        }
        ~dest_guard()
        {
            IDISPLAY_SetDestination(p_, old_);
            IBITMAP_Release(old_);
        }
    };
    void draw_string(const font& f, const AECHAR* str)
    {
        dest_guard guard(display_.get(), IDIB_TO_IBITMAP(dib_.get()));
        AEERect r = { 0, 0, dib_->cx, dib_->cy };
        // 背景を黒くして
        IDISPLAY_FillRect(display_.get(), &r, MAKE_RGB(0, 0, 0));
        // 白の文字で描画
        f.draw_string(str, 0, 0, color::white);
        IDISPLAY_Update(display_.get());
    }

    static void write4(int value, void* buf, int index)
    {
        // index が偶数から処理されることを前提に書いている
        if ((index & 1) == 0)
        {
            static_cast<byte*>(buf)[index >> 1] = value << 4;
        }
        else
        {
            static_cast<byte*>(buf)[index >> 1] |= value;
        }
    }
    void make_index(int w, int h, auto_array<byte>& buf, int& size)
    {
        // RGB565 以外は無いものと考える
        if (dib_->nColorScheme != IDIB_COLORSCHEME_565 || dib_->nDepth != 16)
        {
            throw gl_exception();
        }
        int rw = floor(w);
        int rp = rw >> 1;
        int rh = floor(h);
        size = 2 * 16 + rp * rh;
        buf.reset(new byte[size]);
        // パレットを生成
        // 色は白で、0〜15 までのアルファを持つようにする
        for (int i = 0; i < 16; i++)
        {
            *reinterpret_cast<uint16*>(&buf[i * 2]) = 0xfff0 + i;
        }
        uint16* src = reinterpret_cast<uint16*>(dib_->pBmp);
        byte* dst = buf.get() + 2 * 16;
        for (int y = 0; y < h; y++)
        {
            // 青のデータを 4bit にして書き込んでいく
            for (int x = 0; x < w; x++)
            {
                write4((src[x] & 0x001f) >> 1, dst, x);
            }
            for (int x = w; x < rw; x++)
            {
                write4(0, dst, x);
            }
            src = reinterpret_cast<uint16*>(reinterpret_cast<byte*>(src) + dib_->nPitch);
            dst = dst + rp;
        }
        for (int y = h; y < rh; y++)
        {
            MEMSET(dst, 0, rp);
        }
    }
};

いろいろと自作のクラスがあったりするけど何となく分かるかと。


ここでのキモは、黒の背景(0x00)に白(0xff)の文字列を描画してること。
文字列にアンチエイリアスが掛かっている場合はアルファを考慮する必要があったりするんだけど、黒の背景に白で描画すると、あるピクセルがアルファ 50% で描画された場合にはグレイ(0x7f)になるし、0 % の場合には真っ黒になる。こうすることで描画された文字列からアルファ情報が取れる。まあ今の BREWアンチエイリアスの処理は無いと思うけど。
しかも 0〜15 のアルファを順番にパレットに割り当ててインデックスを取ると、真っ黒の場所は 0 番のインデックスになって、これはアルファ 0 の透過パレットなのでうまく透過される。ほんとこれのために 4bit インデックスと RGBA4444 パレットの組み合わせがあるんじゃないかってぐらいにうまく嵌ってる気がする。


あと、テクスチャを作るためのテンポラリのバッファをケチる方法もある。文字列描画用のビットマップとテンポラリバッファを共有させてやればいい。
いろいろと面倒な処理が必要なのでやらなかったけど、どうしても必要になってきたらやるかも。


そういえば大事なこと忘れてた。
この文字テクスチャは全部白色なんだけど、もし他の色で描画したいなら glColorx() で色を調節して描画すればいい。緑で描画するなら glColorx(0, 65536, 0, 0) とか。
これも描画色を白でやってる理由の一つで、白にすると glColorx() の色と実際に描画される色が同じになる。