Posted at

D言語のBindBCでOpenGLを使う


はじめに

長年D言語erの間で使われているバインディングライブラリ集であるDerelictが、つい最近「limited maintenance mode」に入り、新たにBindBCというプロジェクトに移行されることがアナウンスされました。

該当ポストはこちら。

https://forum.dlang.org/post/gqdphmyymclunyprvcud@forum.dlang.org

14.5年もの間素晴らしいライブラリをメンテナンスしてくださった Mike Parker さんありがとう!

引き続きBindBCにもお世話になりそうです。


BindBCについて

BindBCでは、Derelictから下記の点が改善されているようです。


  • ライブラリとのスタティックリンク可能


    • Derelictと同じようにダイナミックリンクも可能



  • スタティックリンク時にBetterCモードで使用可能


  • @nogc nothrowをより徹底

  • コンパイル時にバージョン等の指定が可能

  • ライブラリのバージョンアップがより簡単にできる

使用頻度の高いSDL2(SDL2_image・SDL2_ttf・SDL2_mixerも含む)やOpenGL・GLFWについてはすでにリリースされているので、これらを早速使ってみたいと思います。

(モデルデータの読み込みを行うASSIMPのみ、まだBindBC化されていないので、Derelictを使ってしまいました……)


今回のソースコード

https://github.com/outlandkarasu-sandbox/dman


参考サイト


事前準備

SDL2SDL2_imageのダウンロードやインストールが必要です。

実行パスが通る場所にDLL(Macならdynlib、Linuxならso)があれば大丈夫だと思います。

モデルデータの読み込みには、ASSIMPのダウンロードやインストールが必要です。

こちらもパスの通った場所に配置すれば大丈夫だと思います。

MacではBrewで入れるとよいようです。

なお、最新のASSIMPはバージョン4ですが、Derelict-assimp3は名前の通り3のみ対応です。

Macではdylibのファイル名がlibassimp.3.dylib固定になっているので、シンボリックリンク等でlibassimp.3.dylibを用意する必要があります。

中身はバージョン4でも(今のところ)問題ないようです。


SDL2 + OpenGLの初期化


dub.jsonversion設定

さて、まずはSDL2とOpenGLを使えるようにします。

使用パッケージはこちらです。

BindBCではビルド時点で使用するライブラリのバージョン等を指定するということで、dub.jsonはこんな感じになります。


dub.json

{

"name": "dman",
"authors": [
"outland.karasu@gmail.com"
],
"description": "A d-man viewer.",
"copyright": "Copyright © 2018, outland.karasu@gmail.com",
"license": "BSL-1.0",
"dependencies": {
"bindbc-sdl": "~>0.4.1",
"bindbc-opengl": "~>0.3.2"
},
"versions": ["GL_33"]
}

"versions": ["GL_33"]で、D言語のversion文により使用可能な関数が設定されます。

今回はOpenGL 3.3を使用する設定になります。


ライブラリのロード

プロジェクトの準備ができたら、BindBCライブラリの初期化を行います。

まずはSDL2のライブラリのロードを行います。


SDL2ライブラリロード


app.d

import bindbc.sdl :

loadSDL,
sdlSupport,
SDLSupport
;

void main() {
// SDL2ライブラリのダイナミックロード。ロードできたライブラリのバージョンが返る。
immutable loadedVersion = loadSDL();
if(loadedVersion != sdlSupport) {
if(loadedVersion == SDLSupport.noLibrary) {
// SDL2なし
throw new Exception("SDL2 not found.");
} else if(loadedVersion == SDLSupport.badLibrary) {
// 未対応
throw new Exception("SDL2 bad library.");
}
}
}


起動してみてエラーが起きなければとりあえず成功です。


OpenGLコンテキスト作成

次はOpenGLですが、ロードの前にSDL2でOpenGLコンテキストを作成しておく必要があります。


app.d

// 前略


/// OpenGLバージョン
enum {
OPEN_GL_MAJOR_VERSION = 3,
OPEN_GL_MINOR_VERSION = 3,
}

void main() {
// 中略

// SDL初期化、OpenGLコンテキスト作成
SDL_Init(SDL_INIT_VIDEO);
scope(exit) SDL_Quit();

// OpenGLバージョン等設定。Macはこの辺りをちゃんとやらないとバージョンが2.1などになってしまう……。
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, OPEN_GL_MAJOR_VERSION);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, OPEN_GL_MINOR_VERSION);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

// メインウィンドウ生成
auto window = SDL_CreateWindow(
"dman-viewer",
0,
0,
400,
400,
SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);
scope(exit) SDL_DestroyWindow(window);

// OpenGLコンテキスト生成
auto openGlContext = SDL_GL_CreateContext(window);
scope(exit) SDL_GL_DeleteContext(openGlContext);
}



OpenGLライブラリロード

コンテキストが作成できて初めてBindBC-openglによるロードが行えます。


app.d


import bindbc.opengl :
loadOpenGL,
GLSupport
;

// 中略

// OpenGLライブラリのダイナミックロード。
immutable loadedVersion = loadOpenGL();
if(loadedVersion == GLSupport.noLibrary) {
// OpenGLなし
throw new Exception("OpenGL not found.");
} else if(loadedVersion == GLSupport.badLibrary) {
// 未対応
throw new Exception("OpenGL bad library.");
} else if(loadedVersion == GLSupport.noContext) {
// OpenGLコンテキストがまだ作られていない。
throw new Exception("OpenGL context not yet created.");
}
}


なかなか大変ですが、ここまででエラーが起きなければなんとか描画が行えるようになります。

イベントループ等を組めば、最低でもウィンドウは表示できるはずです。


app.d


// イベントループの例

/// FPS設定
enum FPS = 90;

// まだ何もしない描画関数
void draw() {}

// 1フレーム当たりのパフォーマンスカウンタ値計算。FPS制御のために使用する。
immutable performanceFrequency = SDL_GetPerformanceFrequency();
immutable countPerFrame = performanceFrequency / FPS;

// メインループ
mainLoop: for(;;) {
immutable frameStart = SDL_GetPerformanceCounter();

// イベントがキューにある限り処理を行う。
for(SDL_Event e; SDL_PollEvent(&e);) {
switch(e.type) {
case SDL_QUIT:
// 閉じるボタンを押されたら終了
break mainLoop;
default:
break;
}
}

// 描画実行
draw();

// 次フレームまで待機
immutable drawDelay = SDL_GetPerformanceCounter() - frameStart;
if(countPerFrame < drawDelay) {
SDL_Delay(0);
} else {
SDL_Delay(cast(uint)((countPerFrame - drawDelay) * 1000 / performanceFrequency));
}
}



三角形描画

さて、これからはごく普通のOpenGL入門記事になります。

OpenGLを学ぶ誰しもが通る最初の三角形描画を行いましょう。


OpenGLによる描画の概要

OpenGLでGPUを使って描画する場合、次のようなステップを踏むことになります。


  1. GPU側にバッファを作って各種データを転送


    • 頂点データ


      • 頂点の色、表面の向き(法線)・テクスチャの位置なども含む



    • 頂点を結んだ面


      • いわゆるポリゴン

      • 頂点データのインデックスを3〜4個ずつ組にして指定





  2. 頂点シェーダー・フラグメントシェーダーを指定

  3. モデルやカメラの位置、使用するテクスチャを指定

  4. 描画する

OpenGLの用語を使うと、下記のようになります。


  1. 初期化・生成


    1. 頂点データの Vertex Buffer Object(VBO) を生成

    2. 面データの Index Buffer Object(IBO) を生成

    3. 上記をまとめる Vertex Array Object(VAO) を生成

    4. シェーダー(GLSL)のコンパイル・ビルド



  2. 描画(毎フレーム実行)


    1. 使用するシェーダーを選択

    2. VAOを選択

    3. 使用テクスチャを設定

    4. 一様(uniform)変数でカメラ・モデル等の座標変換を設定

    5. 描画する




VBO生成

三角形描画のために、まずは三角形の頂点データを格納したVBOを作成します。

下記コードの通り、VBOに格納するのは頂点データの構造体の配列です。


app.d

    // 位置の型

struct Position {
GLfloat x;
GLfloat y;
GLfloat z;
}

// 色の型
struct Color {
GLubyte r;
GLubyte g;
GLubyte b;
GLubyte a;
}

// 頂点データ型。位置 + 色
struct Vertex {
Position position;
Color color;
}

// 頂点データ
immutable(Vertex)[] triangle = [
Vertex(Position(-0.5f, -0.5f, 0.0f), Color(255, 0, 0, 1)),
Vertex(Position( 0.5f, -0.5f, 0.0f), Color( 0, 255, 0, 1)),
Vertex(Position( 0.0f, 0.5f, 0.0f), Color( 0, 0, 255, 1)),
];

// 頂点データバッファ(VBO)の生成。
GLuint verticesBuffer;
glGenBuffers(1, &verticesBuffer);
scope(exit) glDeleteBuffers(1, &verticesBuffer);

// VBOにデータを設定する。
glBindBuffer(GL_ARRAY_BUFFER, verticesBuffer);
glBufferData(GL_ARRAY_BUFFER, triangle.length * Vertex.sizeof, triangle.ptr, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);


OpenGLでは、どこかグローバル変数か何かに操作対象のバッファ等を保持するようになっています。

(glBindBuffer(..., buffer)でグローバルな状態を変えている)

選択中のバッファに対する一連の操作が終わった後、glBindBuffer(..., 0)で選択解除しています。

このglBindBufferを忘れると嫌なことが色々起きます。


IBO生成

次は面のデータ、IBOを生成します。

IBOに格納するのは、頂点のインデックスを表す整数値の配列です。


app.d

    // 頂点0〜2を繋いで面にする。

immutable(GLushort)[] indices = [0, 1, 2];

// IBOの生成
GLuint elementBuffer;
glGenBuffers(1, &elementBuffer);
scope(exit) glDeleteBuffers(1, &elementBuffer);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.length * GLushort.sizeof, indices.ptr, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);


glBindBufferの引数がGL_ELEMENT_ARRAY_BUFFERになり、設定データがGLushortになった他は、VBOと似ていますね。


VAOの生成

上記VBO・IBOはまだ作っただけのため、描画時に選択・有効化する必要があります。

そのためにいくつも関数を呼ぶ必要があるのですが、描画のたびに何度も決まり切った関数を呼ぶのはだるいため、VAOという1発で済ませられる仕組みを使います。


app.d

    // VAOの生成

GLuint vao;
glGenVertexArrays(1, &vao);
scope(exit) glDeleteVertexArrays(1, &vao);

// VAOの内容設定開始
glBindVertexArray(vao);

// 頂点データの選択
glBindBuffer(GL_ARRAY_BUFFER, verticesBuffer);

// 位置情報(Position)の設定。GLfloat3つ。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, Vertex.sizeof, cast(const(GLvoid)*) 0);

// 位置情報の有効化
glEnableVertexAttribArray(0);

// 色情報(Color)の設定。GLubyte4つ。
// 構造体の2つめのメンバー変数なので、オフセット値を引数で指定する。
// D言語ではoffsetofが使えて便利
glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, Vertex.sizeof, cast(const(GLvoid)*) Vertex.color.offsetof);

// 色情報の有効化
glEnableVertexAttribArray(1);

// 面情報の選択
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);

// VAOの内容設定終了
glBindVertexArray(0);

// 設定済みのバッファを選択解除する。
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);


こうしておくと、あとでVAOをglBindVertexArray(vao)するだけで一連の設定が復活します。


シェーダーのビルド

次にバーテックスシェーダーとフラグメントシェーダーをコンパイルします。

各シェーダーは、GLSLというC言語のサブセットで書くGPU側の実行コードになります。

GPUにより次の処理が実行され、画面が描画されます。


  • バーテックスシェーダー


    • 頂点データごとの処理を行う。

    • 頂点データをカメラ位置などに応じて座標変換する。



  • フラグメントシェーダー


    • 画面のピクセルごとの処理を行う。

    • 頂点データから補間した値を元に、ピクセルの塗りつぶしを行う。



他にもポリゴン分割を行うジオメトリシェーダーや、計算のみを行うコンピュートシェーダーがあるそうですが、よく知らないので割愛します。

描画のためには、最低でも前述のバーテックスシェーダーとフラグメントシェーダーが必要とのことで、一番かんたんなものを書くことにします。


shader/dman.vert

#version 330 core


// VBOに格納した構造体のデータがここに入ってくる
layout(location = 0) in vec3 position;
layout(location = 1) in vec4 color;

// ここで頂点の色を設定して返す。
out vec4 vertexColor;

// 頂点データの設定処理
void main() {
// 頂点の座標をそのまま返す。同次座標なので4次元目は1.0固定
gl_Position = vec4(position, 1.0f);

// 頂点の色をそのまま返す
vertexColor = color;
}


基本的に頂点データの値をそのまま横流ししているだけですね。

次にフラグメントシェーダーです。


shader/dman.frag

#version 330 core


// バーテックスシェーダーのvertexColorが渡されてくる。
// ただし、頂点の間で線形補間された値になっている。
in vec4 vertexColor;

// ここでピクセルの色を設定して返す。
out vec4 color;

void main() {
color = vertexColor;
}


こちらもまさに横流しです。

シェーダーの中身は難しくないですが、ビルドは結構大変です……。


source/app.d

/**

* シェーダーをコンパイルする。
*
* Params:
* source = シェーダーのソースコード
* shaderType = シェーダーの種類
* Returns:
* コンパイルされたシェーダーのID
* Throws:
* OpenGlException エラー発生時にスロー
*/

GLuint compileShader(string source, GLenum shaderType) {
// シェーダー生成。エラー時は破棄する。
immutable shaderId = glCreateShader(shaderType);
scope(failure) glDeleteShader(shaderId);

// シェーダーのコンパイル
immutable length = cast(GLint) source.length;
const sourcePointer = source.ptr;
glShaderSource(shaderId, 1, &sourcePointer, &length);
glCompileShader(shaderId);

// コンパイル結果取得
GLint status;
glGetShaderiv(shaderId, GL_COMPILE_STATUS, &status);
if(status == GL_FALSE) {
// コンパイルエラー発生。ログを取得して例外を投げる。
GLint logLength;
glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logLength);
auto log = new GLchar[logLength];
glGetShaderInfoLog(shaderId, logLength, null, log.ptr);
throw new OpenGlException(assumeUnique(log));
}
return shaderId;
}

/**
* シェーダープログラムを生成する。
*
* Params:
* vertexShaderSource = 頂点シェーダーのソースコード
* fragmentShaderSource = フラグメントシェーダーのソースコード
* Returns:
* 生成されたシェーダープログラム
* Throws:
* OpenGlException コンパイルエラー等発生時にスロー
*/

GLuint createShaderProgram(string vertexShaderSource, string fragmentShaderSource) {
// 頂点シェーダーコンパイル
immutable vertexShaderId = compileShader(vertexShaderSource, GL_VERTEX_SHADER);
scope(exit) glDeleteShader(vertexShaderId);

// フラグメントシェーダーコンパイル
immutable fragmentShaderId = compileShader(fragmentShaderSource, GL_FRAGMENT_SHADER);
scope(exit) glDeleteShader(fragmentShaderId);

// プログラム生成
auto programId = glCreateProgram();
scope(failure) glDeleteProgram(programId);
glAttachShader(programId, vertexShaderId);
scope(exit) glDetachShader(programId, vertexShaderId);
glAttachShader(programId, fragmentShaderId);
scope(exit) glDetachShader(programId, fragmentShaderId);

// プログラムのリンク
glLinkProgram(programId);
GLint status;
glGetProgramiv(programId, GL_LINK_STATUS, &status);
if(status == GL_FALSE) {
// エラー発生時はメッセージを取得して例外を投げる
GLint logLength;
glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logLength);
auto log = new GLchar[logLength];
glGetProgramInfoLog(programId, logLength, null, log.ptr);
throw new OpenGlException(assumeUnique(log));
}

return programId;
}


上記の関数を動かしてようやくシェーダープログラムができます。


source/app.d

    // シェーダーの生成

immutable programId = createShaderProgram(import("dman.vert"), import("dman.frag"));
scope(exit) glDeleteProgram(programId);

このprogramIdを使用すると描画が行えます。


source/app.d

    /// 描画処理

void draw() {
// 画面のクリア
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// VAO・シェーダーを使用して描画する。
glUseProgram(programId);
glBindVertexArray(vao);

// IBOを使用して描画
glDrawElements(GL_TRIANGLES, cast(GLsizei) indices.length, GL_UNSIGNED_SHORT, cast(const(GLvoid)*) 0);

// VAO・シェーダーの選択解除
glBindVertexArray(0);
glUseProgram(0);
glFlush();

// 描画結果に差し替える。
SDL_GL_SwapWindow(window);
}



描画結果

DtGh7nlUUAE4tY2.png

OpenGL完全に理解した!


回転拡大縮小など

正方形のウィンドウに正面から三角形を描くだけで満足できるのならよいのですが、3DのCGをやるためには縦横斜めから物体を眺めなければなりません。

そのためには、座標変換を行なって頂点の位置を変えなければなりません。

そのためには、行列やベクトルといった線形代数のプログラミングが必要です。

線形代数がCGなどの画像加工でどんな役目を果たすのか、こちらの鯵坂もっちょさんの記事が(純粋に線形代数の観点からですが)恐ろしくわかりやすいので必読です。

この記事のリンクがあることだけが、この記事の差別化要因と言えるかもしれません……。

線形代数の知識ゼロから始めて行列式「だけ」理解する

で、D言語でOpenGL向けの線形代数をやるには、少し前までgl3nというライブラリがありました。

しかし2016年以降目立った動きがない……。

そこで、ちょっと前までPhobos入りも検討されていたmir-ndsliceを使ってみました。

ただ、mir-ndsliceはそもそも大規模な行列データのスライスやら転置やらを効率的に行うためのライブラリで、たかだか4*4次元の行列が扱えればよいOpenGLの座標変換はちょっと役不足気味でした……。(役不足の使い方、これで合ってるかな?)

実装ソースはこちらですが、使い方などまだまだ至らない点が多いかも……。

結果的には下記のような感じで物体の回転が行えるようになりました。


source/app.d

// 4*4配列をスライスして回転用のOpenGLのMat4行列を作る。

// 配列の中身は共有される。
float[4 * 4] rotateArray = void;
auto rotateMat = rotateArray.mat4ed;

// 適当に回転するよう設定
rotateMat.toRotateXYZ(1.57f, 1.57f, 1.57f);

// uniform変数に設定
immutable transformLocation = glGetUniformLocation(programId, "transform");

// OpenGLの行列はcolumn-majorなので転置する(true)よう指定
glUniformMatrix4fv(transformLocation, 1, true, rotateArray.ptr);


バーテックスシェーダーに上記の回転行列を取り込むようにします。


shader/dman.vert

#version 330 core


layout(location = 0) in vec3 position;
layout(location = 1) in vec4 color;

// 新たに追加
uniform mat4 transform;

out vec4 vertexColor;

void main() {
gl_Position = transform * vec4(position, 1.0f); // ここで座標変換
vertexColor = color;
}


以前に比べると、transformpositionに掛けている点が違います。

このように行列で座標変換を行い、拡大縮小・回転・移動などなどを行うことができます。線形代数の知識があれば……。

CGやるなら数学ちゃんと勉強しないとダメですね。


描画結果

rotate.png

三角形が移動・回転しました。OpenGLの理解が神の領域に近づきました。


ASSIMPでのモデルデータ読み込み

別に私は三角形を描きたいわけではありません。手作業の頂点データ作成では三角形が精一杯というだけです。

本当は他にも描画したいものがあるのです……。

それは、うえしたさんが公開されているD言語くんモデルです!

https://3d.nicovideo.jp/works/td28301

これをちゃんと描画できるよう、ここから頑張ります。


ASSIMPとは

たくさんの3Dモデルフォーマットの読み書きが行えるOSSのC言語のライブラリのようです。

D言語には純正のポーティングライブラリ(tangoとかあって古い……)や、Derelictがあります。

Derelictもやや古いですが、今回はとりあえずDerelictを使っていきます。


初期設定

何はともあれモデルデータをダウンロードし、ASSIMPを初期設定して読んでみます。


source/app.d

// このへんを使った。

import derelict.assimp3.assimp :
aiAttachLogStream,
aiDefaultLogStream_STDOUT,
aiGetPredefinedLogStream,
aiProcessPreset_TargetRealtime_MaxQuality,
aiImportFile,
aiReleaseImport,
DerelictASSIMP3
;

// 中略

/// 後述するテクスチャ貼り付けのためのテクスチャ座標の型
struct TextureCoord {
GLfloat u;
GLfloat v;
}

/// 頂点データにテクスチャ座標を追加しておく
struct Vertex {
Position position;
Color color;
TextureCoord textureCoord;
}

// 中略

// ASSIMPのロード
DerelictASSIMP3.load();

// 標準出力にログを出すよう設定
auto logStream = aiGetPredefinedLogStream(aiDefaultLogStream_STDOUT, null);
aiAttachLogStream(&logStream);

// D言語くん読み込み
auto scene = aiImportFile("./asset/Dman_2013.fbx", aiProcessPreset_TargetRealtime_MaxQuality);
scope(exit) aiReleaseImport(scene);

// メッシュから頂点情報を取得
auto meshes = scene.mMeshes[0 .. scene.mNumMeshes];
auto mesh = meshes[0];
auto vertices = mesh.mVertices[0 .. mesh.mNumVertices]
// 座標系がOpenGLと違って左手系?のため、X軸を反転させる。
.map!(v => Vertex(Position(-v.x, v.y, v.z), Color(255, 0, 0, 255)))
.array;

// テクスチャ座標の設定
if(mesh.mTextureCoords[0]) {
foreach(i, uv; mesh.mTextureCoords[0][0 .. vertices.length]) {
// OpenGLと違って0 = 上端なので、上下を逆転させる。
vertices[i].textureCoord = TextureCoord(uv.x, 1.0f - uv.y);
}
}

// 面情報の取得
auto indices = mesh.mFaces[0 .. mesh.mNumFaces]
.map!(f => f.mIndices[0 .. f.mNumIndices])
.joiner
.map!(i => cast(GLushort) i)
.array;


面倒な感じですね。

基本的にはaiImportFileaiScene構造体を読み込むだけですが、そのままではOpenGLで使えないので、バッファに展開できるよう加工する必要があります。

aiSceneの定義を見れば、どんなものがインポートされてくるかイメージが湧くかもしれません。


types.d

struct aiScene {

uint mFlags;
aiNode* mRootNode;
uint mNumMeshes; // メッシュ(立体)の数
aiMesh** mMeshes; // メッシュ(立体)の配列
uint mNumMaterials;
aiMaterial** mMaterials;
uint mNumAnimations;
aiAnimation** mAnimations;
uint mNumTextures;
aiTexture** mTextures;
uint mNumLights;
aiLight** mLights;
uint mNumCameras;
aiCamera** mCameras;
}

// メッシュ(立体)
struct aiMesh {
uint mPrimitiveTypes;
uint mNumVertices; // 頂点数
uint mNumFaces; // 面の数
aiVector3D* mVertices; // 頂点の配列
aiVector3D* mNormals;
aiVector3D* mTangents;
aiVector3D* mBitangents;
aiColor4D*[AI_MAX_NUMBER_OF_COLOR_SETS] mColors;
aiVector3D*[AI_MAX_NUMBER_OF_TEXTURECOORDS] mTextureCoords; // テクスチャ別のテクスチャ座標配列
uint[AI_MAX_NUMBER_OF_TEXTURECOORDS] mNumUVComponents;
aiFace* mFaces; // 面の配列
uint mNumBones;
aiBone** mBones;
uint mMaterialIndex;
aiString mName;
uint mNumAnimMeshes;
aiAnimMesh** mAnimMeshes;
}


コメントした部分が今回の描画で必要な箇所です。


古いfbxファイルを読み込む場合

古い(ASCII形式など)fbxファイルを読み込むことは、最新のASSIMPではできません。困ったことに、フリー3DCGソフト大手のBlendarでも読めませんでした……。

もし古いFBXを使用したくなった場合、最新のASSIMPを使うとしたら、FBX開発元のAutodeskが公開しているConverterでFBXの2013年形式に変換する必要があります。

https://www.autodesk.com/developer-network/platform-technologies/fbx-converter-archives

D言語くんモデルがまさにそうだったので、2013年形式に変換しました。変換後のモデルのライセンスは元と同じNYSLとします。(他のソースコードはBSL-1.0)

https://github.com/outlandkarasu-sandbox/dman/blob/master/asset/Dman_2013.fbx


テクスチャ設定

さて、先ほどASSIMPのaiMeshの中にテクスチャ情報もチラ見していたと思います。

我らがD言語くんモデルもテクスチャを使用しており、テクスチャがちゃんと貼れないとロクな見た目になりません。


SDL2_imageによるテクスチャ読み込み


SDL_imageライブラリのロード

さて、SDL2_imageの初期設定を行います。bindbc-sdlにSDL2_imageを扱う機能があるので、まずはこちらを有効化します。


dub.json

 "versions": ["GL_33", "BindSDL_Image"]


続けてSDL_imageのロードを行います。


source/app.d

import bindbc.sdl.image :

IMG_Init,
IMG_INIT_PNG,
IMG_Load,
IMG_Quit,
loadSDLImage,
sdlImageSupport
;

// 中略

// SDL_imageのロード
immutable sdlImageVersion = loadSDLImage();
writefln("SDL_image loaded: %s", sdlImageVersion);

// PNGを読み込むよう初期化
enforceSdl(IMG_Init(IMG_INIT_PNG) == IMG_INIT_PNG);
scope(exit) IMG_Quit();



テクスチャ読み込み

初期化が済んだらテクスチャとして読み込みます。


source/app.d

    // テクスチャの生成

GLuint texture;
glGenTextures(1, &texture);
scope(exit) glDeleteTextures(1, &texture);

// テクスチャ読み込み
auto textureSurface = enforceSdl(IMG_Load(DMAN_TEXTURE_PATH));
scope(exit) SDL_FreeSurface(textureSurface);

// 画像データの行のバイト数のアライメント。1行のサイズが1バイトの倍数とする。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

// 生成したテクスチャを選択し、各種設定とデータの転送を行う。
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

// PNG画像の形式の通り転送する。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureSurface.w, textureSurface.h, 0, GL_RGB, GL_UNSIGNED_BYTE, textureSurface.pixels);

// 設定終了。テクスチャ選択解除。
glBindTexture(GL_TEXTURE_2D, 0);



頂点にUV座標を載せる

さて、モデルデータ読み込みのところでVertex構造体にtextureCoordを追加しました。

あれを描画時に使用できるようにVAOへ追加しておきます。


source/app.d

    // VAOの内容設定

glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, verticesBuffer);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, Vertex.sizeof, cast(const(GLvoid)*) 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, Vertex.sizeof, cast(const(GLvoid)*) Vertex.color.offsetof);
glEnableVertexAttribArray(1);

// これを追加。指定方法自体は他の属性値と同様
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, Vertex.sizeof, cast(const(GLvoid)*) Vertex.textureCoord.offsetof);
glEnableVertexAttribArray(2);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);
glBindVertexArray(0);


これであとでバーテックスシェーダーでテクスチャ座標が参照できるようになります。


描画時のテクスチャ選択

そして、描画時にテクスチャを選択するようにします。


source/app.d

        // テクスチャ選択

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(textureLocation, 0);

// 描画
glDrawElements(GL_TRIANGLES, cast(GLsizei) indices.length, GL_UNSIGNED_SHORT, cast(const(GLvoid)*) 0);



シェーダーの修正

ここまででGPU側にテクスチャが転送されるようになっていますが、シェーダーの方でも描画処理を修正する必要があります。


shader/dman.vert

#version 330 core


layout(location = 0) in vec3 position;
layout(location = 1) in vec4 color;
layout(location = 2) in vec2 uv; // これを追加。頂点の持つテクスチャのUV座標

uniform mat4 transform;

out vec4 vertexColor;

// フラグメントシェーダーにテクスチャのUV座標を渡すためのout変数。
out vec2 vertexUv;

void main() {
gl_Position = transform * vec4(position, 1.0f);
vertexColor = color;
vertexUv = uv; // フラグメントシェーダーにテクスチャのUV座標を渡す。
}



shader/dman.frag

#version 330 core


in vec4 vertexColor;
in vec2 vertexUv; // バーテックスシェーダーからのUV座標(線形補間済み)
out vec4 color;

// ホスト側でglUniform1iにより設定したテクスチャを参照するsampler
uniform sampler2D textureSampler;

void main() {
// UV座標を元にテクスチャの色を取り出して描画
color = vec4(texture(textureSampler, vertexUv).rgb, 1.0f);
}



描画結果

dman.png

やった!やってやりました!!


今後

モデルデータを普通に描画できるところまではやりきりました。

回転や移動などのアニメーションをさせるなども結構簡単です。

今後はこれを元にシェーダーなどで遊んでみたいですね。

特に輪郭線が見えないのが寂しいので、トゥーンシェーディングとか試してみたいです。

また、今のところ正方形ウィンドウに表示しているだけなので、フルスクリーンにしてカメラ(ビュー)の座標変換もちゃんとやりたいところです。


ライセンス

クリエイティブ・コモンズ・ライセンス
この 作品 は クリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。