DXライブラリってユーザー定義の画像読み込み関数が使えるんだよ

DXライブラリ公式が対応した画像フォーマット以外読み込めないものだと思っていたが、そんなことはないらしい。

DXライブラリのソースコードを読んでいたところ、以下の関数を発見した。

extern int AddUserGraphLoadFunction4(int(*UserLoadFunc)(STREAMDATA *Src, BASEIMAGE *BaseImage));
extern int SubUserGraphLoadFunction4(int(*UserLoadFunc)(STREAMDATA *Src, BASEIMAGE *BaseImage));

Addのほうが読み込み関数の追加、Subが削除。
追加できる関数の上限数は MAX_USERIMAGEREAD_FUNCNUM マクロで定義されている。

UserLoadFunc関数ポインタの引数について

引数にとられている関数ポインタのシグネチャは次のようになっている。
戻り値として0を返せば読み込み成功となる。

int loader(STREAMDATA *, BASEIMAGE *);

STREAMDATAはDXライブラリ独自のストリームでここからデータを読み、画像データを表すBASEIMAGEに設定していく。

この関数はファイル、メモリからの画像データ読み込み時に呼ばれ、非同期読み込みを実行した場合は非同期に呼ばれる。

ストリームについて

ストリームが標準Cライブラリのものでないのは、メモリストリームの実装にあたってカスタマイズが必要だったからだと思われる。

この独自ストリームはメンバにストリームを表すハンドルと、ストリーム操作系の関数テーブルを含んでいる。

C言語標準ライブラリのファイルストリーム系と同じ関数が用意されており、それっぽく扱うためのマクロも用意されている。

ストリームから1バイト読み込むサンプル。

int loader(STREAMDATA *stream, BASEIMAGE *) {
    char byte;

    // マクロ使用
    STREAD(&byte, 1, 1, stream);
    // マクロ不使用
    stream->ReadShred.Read(&byte, 1, 1, stream->DataPoint);
}

使用できるマクロの一覧

STTELL(st)
STSEEK(st, pos, type)
STREAD(buf, len, num, st)
STEOF(st)

引数stは対象ストリームのポインタをとる。
STSEEKの引数typeは STREAM_SEEKTYPE_SET, STREAM_SEEKTYPE_CUR, STREAM_SEEKTYPE_END が使用できる。
それぞれの関数は標準Cライブラリのファイルストリーム関数と同じ動きをする。

画像データについて

BASEIMAGEのそれぞれのメンバについて簡単な解説を行う。

この構造体を作成するための関数がCreateRGB8ColorBaseImageなど、各フォーマットごとに存在する。

typedef struct {
    // 色情報
    COLORDATA ColorData;
    // 幅、高さ、ピッチ
    int Width, Height, Pitch;
    // グラフィックイメージ
    void *GraphData;
    // ミップマップの数
    int MipMapCount;
    // グラフィックイメージの数
    int GraphDataCount;
} BASEIMAGE;

ColorData

1ピクセル何バイト使用するか、実数を用いて色情報を保持するか、などのデータを保持する。

COLORDATA設定用の関数もDXライブラリは提供しており、例えばフルカラーDIBの色情報を構築する関数として、 CreateFullColorData 関数がある。

COLORDATAの定義は以下のようになっている。

typedef struct {
    // 圧縮フォーマット DX_BASEIMAGE_FORMAT_NORMAL など
    unsigned char Format;

    // チャンネル数
    unsigned char ChannelNum;
    // 1チャンネル当たりのビット数
    unsigned char ChannelBitDepth;
    // チャンネルに浮動小数点型を使用するか
    unsigned char FloatTypeFlag;
    // 1ピクセルあたりのバイト数
    unsigned char PixelByte;

    // 以下は ChannelNum 又は ChannelBitDepth が 0 の時のみ有効

    // 1ピクセルあたりのビット数
    unsigned char ColorBitDepth;
    // 使用されないビットアドレスとビット幅
    unsigned char NoneLoc, NoneWidth;
    // RGBAそれぞれのビット幅
    unsigned char RedWidth, GreenWidth, BlueWidth, AlphaWidth;
    // RGBAそれぞれのビットアドレス
    unsigned char RedLoc, GreenLoc, BlueLoc, AlphaLoc;
    // RGBAそれぞれのビット位置を表すマスク
    unsigned int RedMask, GreenMask, BlueMask, AlphaMask;
    // 使われていないビット位置を表すマスク
    unsigned int NoneMask;
    // 使用しているパレット番号の最大値 (0の場合は255とみなす)
    int MaxPaletteNo;

    // パレット (ColorBitDepthが8以下の時に有効)
    COLORPALETTEDATA Palette[256];
} COLORDATA;

コメントで書いてある通り、ChannelNul又はChannelBitDepthが0の時にのみ下半分のメンバ変数が有効となる。

DXライブラリの色情報構築関数での実装では、1ピクセルのビット数が32bitに収まる場合はChannelNumとChannelBitDepthを0に設定している。

このことから下半分のメンバ変数は32bit変数を各チャンネルごとに分割して使用する場合に使用されるのだろう。

Palette[256]

パレット画像用のパレット情報。
これは簡単で、単にARGB8の情報を保持する。

typedef struct {
    unsigned char Blue;
    unsigned char Green;
    unsigned char Red;
    unsigned char Alpha;
} COLORPALETTEDATA;

Width, Height, Pitch

Width、Heightはピクセル
Pitchはグラフィックイメージ上での横幅バイト数

gをグラフィックイメージのバイトポインタとしたとき、g += Pitch で1つ下のピクセル行に移る。

GraphData

グラフィックイメージへのポインタを代入する。
画像データ構築関数を使用しない場合は、メモリを自前で確保すること。
その場合、確保時はDxAllocを使用する。

先頭アドレスが画像の左上のピクセルになるように格納する。

MipMapCount

ミップマップを使用する際のミップマップレベル。
通常は0。

GraphDataCount

グラフィックイメージ数。
1つのグラフィックイメージしか使用しない場合は0を格納。

少し試してみた

独自フォーマットの画像データを読み込んでみる。

画像フォーマット

ヘッダー

シグネチャとして"stay"の文字列をファイルの先頭に含む。
次に符号なし4バイト整数2つでそれぞれ横幅と縦幅。

typedef struct {
    char signature[4];
    uint32_t width, height;
} stay_image_header;

データ

ヘッダのすぐあとに画像データ本体が存在するものとする。
1ピクセルは r g b のうち1色のみを表せ、1バイトである。
width * height の一次元配列としてファイルに記述される。

#define STAY_IMAGE_RED      ('r')
#define STAY_IMAGE_GREEN    ('g')
#define STAY_IMAGE_BLUE     ('b')

使用する画像データ

image.stay

実装

#include <DxLib.h>
#include <string.h>
#include <stdint.h>

static int loader(STREAMDATA *stream, BASEIMAGE *image);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    SetOutApplicationLogValidFlag(false);
    ChangeWindowMode(true);
    SetGraphMode(1280, 720, 32);
    if (DxLib_Init() < 0)return -1;
    SetDrawScreen(DX_SCREEN_BACK);

    AddUserGraphLoadFunction4(&loader);

    int handle = LoadGraph("image.stay");
    if (handle < 0) {
        MessageBox(NULL, "LoadGraph Failed", "Error", MB_OK);

        DxLib_End();
        return 1;
    }

    while (ProcessMessage() != -1) {
        ClearDrawScreen();

        DrawExtendGraph(0, 0, 1280, 720, handle, true);

        ScreenFlip();
    }

    DeleteGraph(handle);

    DxLib_End();
    return 0;
}

#define STAY_IMAGE_RED      ('r')
#define STAY_IMAGE_GREEN    ('g')
#define STAY_IMAGE_BLUE     ('b')

typedef struct {
    char signature[4];
    uint32_t width, height;
}stay_image_header;

static int loader(STREAMDATA *stream, BASEIMAGE *image) {
    stay_image_header header;

    if (STREAD(&header, sizeof(stay_image_header), 1, stream) < 1)return -1;
    if (strncmp(header.signature, "stay", 4) != 0)return -1;
    if (CreateRGB8ColorBaseImage((int)header.width, (int)header.width, image) < 0)return -1;

    char *dest = (char *)image->GraphData;
    const int line_step = image->Pitch;
    const int pixel_step = image->ColorData.PixelByte;

    for (uint32_t y = 0; y < header.height; ++y, dest += line_step) {
        char *current = dest;

        for (uint32_t x = 0; x < header.width; ++x, current += pixel_step) {
            char pixel;
            uint32_t color;

            if (STREAD(&pixel, 1, 1, stream) < 1)goto READ_ERR;

            switch (pixel) {
            case STAY_IMAGE_RED:
                color = (0xFF << image->ColorData.RedLoc);
                break;

            case STAY_IMAGE_GREEN:
                color = (0xFF << image->ColorData.GreenLoc);
                break;

            case STAY_IMAGE_BLUE:
                color = (0xFF << image->ColorData.BlueLoc);
                break;

            default:
                goto READ_ERR;
            }

            memcpy(current, &color, sizeof(uint32_t));
        }
    }

    return 0;

READ_ERR:
    DxFree(image->GraphData);
    return -1;
}

実行結果

screenshot.png

おわりに

これを書いていて気付いたが、CreateGraphFromBaseImageなる関数を使えばBaseImageから直接グラフィックハンドルを作成できるので、わざわざ今回の関数を使用しなくてもよさそう?

ただ、SoftImageをBaseImageから作成する関数は存在しないので、そんなときに有効なのかな。

MAX_USERIMAGEREAD_FUNCNUM は自分の環境では10と定義されており、結構少ないのでstd::mapなどを使用して1つのローダー関数で管理する機構を作っておくといいのかも。

まあでも大体の画像フォーマットにDXライブラリは対応してくれているので、今回のように自分でローダーを書くなんてことはなかなかないと思う。