OpenGLでPNGファイルを使う話

PNGファイルを読み込んで、OpenGLで描画する話

大体の手順

  • zlibのダウンロード/コンパイル
  • libpngのダウンロード/コンパイル
  • VisualStudioにzlib/libpngを設定
  • libpngを使ってPNGを読み込む
  • 読み込んだPNGをOpenGLに登録
  • 四角形に貼り付けて描画

OpenGLはデフォルトでPNGが読めない

そうです、読めないんです
なので、適当にライブラリを拾ってきてそれを使います

libpng

libpng
http://www.libpng.org/pub/png/libpng.html

PNGファイルを読み込むライブラリ
PNGの開発者が開発した公式なライブラリらしいです

ただ、このlibpngはソースコードのみが提供されており、
自分でコンパイルして利用する必要があります


libpngのコンパイル

libpngのダウンロード

バージョンが色々あるみたいですが、なんか1.6.18が安定版っぽいのでそれを落とします
ページのちょっと下あたりにある"Source code:"っていう項目のところから一番右の.zipをダウンロード
※まぁ.tar.xzとか.tar.gzが解凍できるツールを持ってる人はそれでもいいかと

libpng_download.png

libpngのコンパイル

...といきたいところですが、さっきのlibpngのページの先頭当たりにこんな記述が

libpng is available as ANSI C (C89) source code and requires zlib 1.0.4 or later
libpngは ANSI C (C89) のソースコードとして利用可能であり、そして、zlib1.0.4以降を必要とします

zlib1.0.4以降を必要とします
libpngの前に、zlibをダウンロードしてコンパイルしましょう


zlibのコンパイル

zlibは、データを圧縮/解凍するときに利用されるライブラリです
PNG形式では、zlibを画像データの圧縮に使用しているため、zlibが別途必要になるようです

zlibのダウンロード

zlib
http://www.zlib.net/

zlibの最新バージョンは1.2.8のようです
libpngの条件である1.0.4を満たしているため、これを使います

ページの真ん中辺りの"The current release is publicly available here:"からダウンロード
たぶんUSでもPick a mirrorでもどっちでも落とせます
※.tar.xzとか.tar.gzを解凍できる人は(ry

zlib_download.png

開発者コマンドプロンプト

開発者コマンドプロンプトは、VisualStudioのコマンドを利用できるように
あらかじめ設定が行われるコマンドプロンプトです

スタートメニューからMicrosoft Visual Studio 2012
→ Visual Studio Tools → VS2012の開発者コマンドプロンプトと辿ることで起動できます

vs_develop_cmd.png

zlibのコンパイル

以下開発者コマンドプロンプトで次のようにコマンドを打っていきます

# cdコマンドでzlibのフォルダへ移動
# 自分がダウンロードしたフォルダに合わせてパスは変更してください
> cd D:\Temp\zlib-1.2.8

# zlibのコンパイル
> nmake /f win32/Makefile.msc

# ここからなんかだーーって出力されてコンパイルが自動で行われる

# プロンプトが返ってきたらコンパイル完了
>

すると、zlibのフォルダにzlib.libというファイルが作られています

zlib_lib.png

これがzlibのライブラリファイルで、これをVisualStudioに設定することで
zlibの機能が利用できるようになります


libpngのコンパイル

zlibが用意できたので、libpngのコンパイルを行います

zlibを所定の場所に移動

libpngはzlibに依存しているため、zlibをlibpngが分かる場所に置いておく必要があります

libpngのフォルダと同じ場所にzlibというフォルダを作り、
その中にヘッダやライブラリファイルを入れることで、zlibを認識することができるようです

libpng_directory.png

また、zlibフォルダの中に入れるファイルは

  • zconf.h
  • zlib.h
  • zlib.lib

の3つだけでいいようです

コンパイル

開発者コマンドプロンプトで次のように実行

# libpngのフォルダへ移動
# 自分がダウンロードしたフォルダに合わせてパスは変更してください
> cd D:\Temp\lpng1618

# libpngのコンパイル
> nmake /f scripts/makefile.vcwin32

# ここからなんかだーーって出力されてコンパイルが自動で行われる

# プロンプトが返ってきたらコンパイル完了
>

すると、libpngのフォルダにlibpng.libというファイルが作られています

libpng_lib.png

ここまででzlibとlibpngの用意ができました
あとはこれらのライブラリをVisualStudioに登録することで
PNGを読み込む関数を利用することができます


VisualStudioにzlibとlibpngを設定

ライブラリファイルのコピー

glut.libがあるフォルダと同じ場所に

  • libpng.lib
  • png.h
  • pngconf.h
  • pngdebug.h
  • pnginfo.h
  • pnglibconf.h
  • pngpriv.h
  • pngstruct.h
  • zlib.lib

をコピーします

これらのファイルは、ダウンロードしてきたzlibとlibpngのフォルダの中に入ってるので、
それを探してきてコピーします

includeとライブラリの設定

次のようなコードをソースファイルの先頭に追加します

#include "png.h"
#pragma comment(lib, "zlib.lib")
#pragma comment(lib, "libpng.lib")

#pragma comment(lib, "zlib.lib")は、「こういう名前のライブラリを使います」っていう宣言です

もちろん、pragma comment構文を使用せずに、プロジェクトの設定を用いてライブラリを設定してもいいです


libpngでPNGファイルを読み込む

やっとOpenGLでのPNGの読み込みです
PNGファイルからOpenGLのテクスチャを作成する関数を作成してみました

GLuint createTextureFromPNGFile(const char *filename) {

    FILE *fp;
    png_structp pPng = NULL;
    png_infop pInfo = NULL;
    int depth, colorType, interlaceType;
    unsigned int width, height;
    int rowSize, imgSize;
    unsigned int i;
    unsigned char *data;
    GLuint texture;

    //PNGファイルを開く
    fopen_s(&fp, filename, "rb");
    if( !fp ) {
        fprintf(stderr, "createTextureFromPNGFile: Failed to fopen.");
        return 0;
    }

    //PNGファイルを読み込むための構造体を作成
    pPng = png_create_read_struct(
        PNG_LIBPNG_VER_STRING,
        NULL, NULL, NULL
    );
    pInfo = png_create_info_struct(pPng);

    //初期化
    png_init_io(pPng, fp);

    //画像情報を読み込み
    //画像の幅、高さ、ビット深度、色の表現方法、インターレースの情報を取得する
    png_read_info(pPng, pInfo);
    png_get_IHDR(pPng, pInfo,
        &width, &height,
        &depth, &colorType,
        &interlaceType, NULL, NULL
    );

    //RGBとRGBAのみに対応
    if( colorType != PNG_COLOR_TYPE_RGB && colorType != PNG_COLOR_TYPE_RGBA ) {
        fprintf(stderr, "createTextureFromPNGFile: Supprted color type are RGB and RGBA.");
        return 0;
    }

    //インターレースは非対応
    if( interlaceType != PNG_INTERLACE_NONE ) {
        fprintf(stderr, "createTextureFromPNGFile: Interlace image is not supprted.");
        return 0;
    }

    //1行のデータサイズと画像の高さから必要なメモリ量を計算して、メモリ確保
    rowSize = png_get_rowbytes(pPng, pInfo);
    imgSize = rowSize * height;
    data = malloc(imgSize);

    //ピクセルの読み込み
    for( i = 0; i < height; i++ ) {
        png_read_row(pPng, &data[i * rowSize], NULL);
    }

    png_read_end(pPng, pInfo);

    //OpenGLテクスチャの作成
    glGenTextures(1, &texture);

    //テクスチャを選択
    glBindTexture(GL_TEXTURE_2D, texture);

    //テクスチャにPNGファイルから読み込んだピクセルを書き込む
    glTexImage2D(
        GL_TEXTURE_2D, 0, GL_RGBA,
        width, height,
        0, GL_RGBA, GL_UNSIGNED_BYTE, data
    );

    //後片付け
    free(data);
    png_destroy_info_struct(pPng, &pInfo);
    png_destroy_read_struct(&pPng, NULL, NULL);
    fclose(fp);

    return texture;

}

OpenGLでテクスチャを描画

テクスチャの読み込み

これはさっき作ったcreateTextureFromPNGFile関数で読み込みます

テクスチャの読み込みは一回でいいので、初期化関数の中にでも書いておけばOKです

texture = createTextureFromPNGFile("ship.png");
if( !texture ) {
    fprintf(stderr, "Failed to createTextureFromPNGFile\n");
}

ship.pngのところは、自分が実際に読み込みたいテクスチャの名前に変更してください

戻り値が0でなければ、読み込み成功です

テクスチャフィルタ設定

テクスチャを拡大・縮小したときに、どのように処理を行うかを設定します

これも一回だけ行えばいいので初期化関数の中にでも書いておけばOKです

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

GL_TEXTURE_MAG_FILTERは拡大するとき、GL_TEXTURE_MIN_FILTERは縮小するときのフィルタを設定します
また、GL_LINEARは、線形補間を行うという意味です
GL_LINEAR以外にもGL_NEARESTがあり、これは補間を行わずに一番近いピクセルの色をそのまま使用するという意味です

GL_LINEAR GL_NEAREST
linear_fileter.png nearest_fileter.png

ちなみに、フィルタを設定しないと環境によってはテクスチャが全く表示されないことがあるので
必ず設定しておいたほうが良いでしょう

これを設定してなかったばっかりに、テクスチャが出ないどうしてだろうって小1時間くらい悩まされた...

透過設定

PNGファイルは背景を透過させることができるため、半透明の処理をOpenGLで有効化しておきます
glBlendFuncは半透明の計算方法を設定しており、ここを変更すると加算合成とか乗算構成とか
そういったことも出来るようになります
今回は通常の半透明の計算を行います

//透過設定
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

描画

いよいよテクスチャの描画です

通常OpenGLで四角形を描画するときは、こんな感じで描画しますよね

glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glBegin(GL_TRIANGLE_STRIP);
glVertex2f(-0.2f, 0.2f);
glVertex2f(0.2f, 0.2f);
glVertex2f(-0.2f, -0.2f);
glVertex2f(0.2f, -0.2f);
glEnd();

※glBeginでGL_TRIANGLE_STRIPじゃなくてGL_POLYGON使ってる人もいるかも知れないですが、
そこはあまり大きな違いではないので構わないのです

draw_rect.png

テクスチャは、この四角形に貼り付けるようなイメージで描画します

実際にテクスチャを貼り付けるソースコードは次のようなものです

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture);

glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glBegin(GL_TRIANGLE_STRIP);
glTexCoord2f(0.0f, 0.0f);
glVertex2f(-0.2f, 0.2f);
glTexCoord2f(1.0f, 0.0f);
glVertex2f(0.2f, 0.2f);
glTexCoord2f(0.0f, 1.0f);
glVertex2f(-0.2f, -0.2f);
glTexCoord2f(1.0f, 1.0f);
glVertex2f(0.2f, -0.2f);
glEnd();

glDisable(GL_TEXTURE_2D);

まず、描画を行う前に

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture);

を書くことでテクスチャを有効にし、使用するテクスチャを設定します

次に、座標を設定します

glTexCoord2f(0.0f, 0.0f);
glVertex2f(-0.2f, 0.2f);

今まで使っていたglVertex2fに加えて、glTexCoord2f関数でテクスチャ上の座標も設定します

texcoord.png

※パソコンで描くのが面倒だった...

ここで、テクスチャは左上が(0.0, 0.0)、右下が(1.0, 1.0)である点が注意です

  • 画面上の(-0.2, 0.2)はテクスチャ上の(0.0, 0.0)に対応
  • 画面上の(0.2, 0.2)はテクスチャ上の(1.0, 0.0)に対応
  • 画面上の(-0.2, -0.2)はテクスチャ上の(0.0, 1.0)に対応
  • 画面上の(0.2, -0.2)はテクスチャ上の(1.0, 1.0)に対応

ということをglVertex2fとglTexCoord2fで指定します

最後に、

glDisable(GL_TEXTURE_2D);

でテクスチャを無効状態に戻して終了です

実際に実行すると、テクスチャが表示されます
draw_texture.png

完成ソースコード

#include <stdio.h>
#include <stdlib.h>
#include "glut.h"
#include "png.h"

#pragma comment(lib, "zlib.lib")
#pragma comment(lib, "libpng.lib")

GLuint texture;

GLuint createTextureFromPNGFile(const char *filename) {
    //長いので省略
    //「libpngでPNGファイルを読み込む」のソース見てね
}

void display(void) {

    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, texture);

    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    glBegin(GL_TRIANGLE_STRIP);
    glTexCoord2f(0.0f, 0.0f);
    glVertex2f(-0.2f, 0.2f);
    glTexCoord2f(1.0f, 0.0f);
    glVertex2f(0.2f, 0.2f);
    glTexCoord2f(0.0f, 1.0f);
    glVertex2f(-0.2f, -0.2f);
    glTexCoord2f(1.0f, 1.0f);
    glVertex2f(0.2f, -0.2f);
    glEnd();

    glDisable(GL_TEXTURE_2D);

    glFlush();

}

int main(int argc, char *argv[]) {

    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA);
    glutCreateWindow(argv[0]);
    glutDisplayFunc(display);

    texture = createTextureFromPNGFile("ship.png");
    if( !texture ) {
        fprintf(stderr, "Failed to createTextureFromPNGFile\n");
    }

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    //テクスチャサンプリング設定
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    //透過設定
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glutMainLoop();

    return 0;

}

終わり

半日くらいで調べて作ったので、たぶん色々間違ってると思います
過度な信用はしないでくださいね

なんか下手な実験のレポートよりも書くの大変だったかも