はじめに
前回は、VBOとVAOを使った描画にインデックス・リストを組み合わせて、画面上に四角形を描画するところまで解説しました。ここまでで、シェーダを使った現代的なOpenGLの描画の基本はだいぶマスターしたと言っても良いでしょう。
今回はちょうどいいタイミングですので、ここでテクスチャを表示する方法を解説したいと思います。テクスチャ描画はそれほど難しくないトピックなのですが、シェーダのコードが難しくなったタイミングで説明している本が多いので、まるで難しいように感じてしまいます。
頂点データとして、座標と色情報だけを指定して、四角形を書いているだけのこのタイミングでテクスチャ描画をマスターしてしまうのは、とても分かりやすいと思います。
1. Shaderクラスを改良する
テクスチャを表示する場合には、これまで使ってきたin変数とout変数に加えて、uniform変数という種類の変数を利用することになります。これを使えるようにするために、Shaderクラスを少し改良します。
まずShader.hppです。
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <map>
#include <string>
/* 省略 */
class ShaderProgram
{
private:
GLuint program;
Shader *vshader;
Shader *fshader;
std::map<std::string, GLint> uniformLocationMap;
public:
ShaderProgram(const std::string& vshName, const std::string& fshName);
~ShaderProgram();
public:
void SetUniform(const std::string& name, int value);
void Use();
};
std::mapを利用するので、先頭でmapをインクルードします。そしてそのmapを使ったuniformLocationMap
変数と、uniform変数の値をセットするためのSetUniform()関数の2つをShaderProgramクラスに追加します。
次に、Shader.cppにSetUniform()関数の実装を追加します。
void ShaderProgram::SetUniform(const std::string& name, int value)
{
GLint location;
if (uniformLocationMap.find(name) == uniformLocationMap.end()) {
location = glGetUniformLocation(program, name.c_str());
if (location < 0) {
throw GameError("ShaderProgram::SetUniform() Cannot locate a uniform value: %s", name.c_str());
}
uniformLocationMap[name] = location;
} else {
location = uniformLocationMap[name];
}
glUniform1i(location, value);
}
この関数の中では、まずuniform変数の場所が取得済みかどうかをチェックします。uniformLocationMap
変数に登録されていない場合は未取得ですので、glGetUniformLocation()
関数を使って、uniform変数の場所(GPUの中で動いているシェーダの中で何番目の変数として扱われているかという数値)を取得します。uniform変数の場所はコンパイル時に決定されるため、このようにコンパイル後に変数名を渡して問い合わせる必要があるのです。一度取得したuniform変数の場所は、ふたたび問い合わせなくても良いように、uniformLocationMap
変数に変数名をキーとして登録しておきます。すでに変数名がキーとして登録されていれば場所は取得済みですので、マップから値を取り出して使用します。
そしてuniform変数の場所を渡して、glUniform〜()
という名前の関数を呼び出すことで、uniform変数の値をセットすることができます。
2. Textureクラスを追加する
次に、テクスチャを扱うためのTextureクラスを追加しましょう。
Shader.cppを右クリックして、メニューから[New File…]を実行します。
テンプレートの選択画面では、「macOS」の「C++ File」を選択して、「Next」ボタンを押します。
ファイル名に「Texture」と入力して「Next」ボタンを押します。次の画面で何も変更せずに「Create」ボタンを押すと、Texture.hppとTexture.cppの2つのファイルが作成されます。
作成されたTextureクラスのためのファイル。
Textureクラスの実装ではObjective-C++を使いますので、「Texture.cpp」ファイルの拡張子を「.mm」に変更して、「Texture.mm」としてください。
Texture.hppでは、次のようにTextureクラスを実装します。
#ifndef Texture_hpp
#define Texture_hpp
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <string>
class Texture
{
GLuint name;
float width;
float height;
public:
Texture(const std::string& filename);
~Texture();
public:
void Bind();
float GetWidth() const;
float GetHeight() const;
};
#endif /* Texture_hpp */
次にTexture.mmの実装です。
#include "Texture.hpp"
#include "StringSupport.hpp"
#import <Foundation/Foundation.h>
#import <GLKit/GLKit.h>
static NSURL *FindImageFile(const std::string& filename)
{
NSString *filenameStr = [NSString stringWithCString:filename.c_str() encoding:NSUTF8StringEncoding];
NSString *basename = [filenameStr stringByDeletingPathExtension];
NSString *ext = [filenameStr pathExtension];
return [[NSBundle mainBundle] URLForResource:basename withExtension:ext];
}
Texture::Texture(const std::string& filename)
{
NSURL *fileURL = FindImageFile(filename);
if (!fileURL) {
throw GameError("Texture::Texture() Cannot find a texture file: \"%s\"", filename.c_str());
}
NSDictionary *option = @{ GLKTextureLoaderApplyPremultiplication: @NO,
GLKTextureLoaderOriginBottomLeft: @YES };
NSError *error = nil;
GLKTextureInfo *texInfo = [GLKTextureLoader textureWithContentsOfURL:fileURL
options:option
error:&error];
if (!texInfo) {
throw GameError("Texture::Texture() Failed to load a texture: \"%s\"", filename.c_str());
}
name = texInfo.name;
width = texInfo.width;
height = texInfo.height;
}
Texture::~Texture()
{
glDeleteTextures(1, &name);
}
void Texture::Bind()
{
glBindTexture(GL_TEXTURE_2D, name);
}
float Texture::GetWidth() const
{
return width;
}
float Texture::GetHeight() const
{
return height;
}
Textureクラスのコンストラクタでは、ファイル名を指定するC++文字列を受け取ります。これをObjective-Cの文字列に変換して、ファイル名と拡張子に分解してから、アプリケーション・バンドルの中にあるリソースを -[NSBundle URLForResource:]
メソッドを使って検索することで、ファイル位置を表すURLが取得できます。
このURLを使って、GLKitフレームワークが提供するテクスチャを読み込むためのクラスであるGLKTextureLoaderクラスの -[GLKTextureLoader textureWithContentsOfURL:options:error:]
メソッドを使うと、OpenGLのテクスチャが生成されて、その中に画像データが読み込まれます。GLKitはOpenGLの実装を簡単にするために、Appleが2011年に提供を開始したフレームワークです。macOSでは10.8の時代からサポートされていますので、2017年現在はほぼすべての環境で利用できると考えて良いでしょう。
GLKitフレームワークのテクスチャ読み込みの機能は、画像の上下やアルファ値の取り扱いといったことを細かく指定できるようになっています。
生成されたテクスチャの情報は、リターンされたGLKTextureInfo
の中に格納されています。OpenGLのテクスチャの名前(int型の数値)を取り出して、name
変数に保存しておきます。また今後必要になるでしょうから、ピクセル単位での画像の横幅と縦のサイズも取得して、width
変数とheight
変数に格納しておきましょう。
3. シェーダを実装する
頂点シェーダを次のように実装します。
#version 410
layout (location=0) in vec3 vertex_pos;
layout (location=1) in vec4 vertex_color;
layout (location=2) in vec2 vertex_uv;
out vec4 color;
out vec2 uv;
void main()
{
gl_Position = vec4(vertex_pos, 1.0);
color = vertex_color;
uv = vertex_uv;
}
テクスチャのUV座標(X方向・Y方向ともに0.0〜1.0の数値でテクスチャのマッピング位置を指定したもの)を指定するためのin変数vertex_uv
を増やしました。2番目の頂点属性としてvertex_uv
で受け取った値は、同じく増やしたuv
という名前のout変数に代入することで、フラグメント・シェーダに受け渡されます。頂点間にラスタライズして生まれる中間のフラグメントに対しては、頂点ごとに指定されたテクスチャのUV座標が線形補間されて渡されることになります。
フラグメント・シェーダは、次のように実装します。
#version 410
in vec4 color;
in vec2 uv;
uniform sampler2D tex;
layout (location=0) out vec4 frag_color;
void main()
{
frag_color = texture(tex, uv) * color;
}
フラグメント・シェーダでは、頂点シェーダからUV座標を受け取るためのin変数uv
を増やしました。また、uniform変数としてsampler2Dという型のtex
変数を追加しています。この型のuniform変数は、OpenGLで複数同時に指定可能なテクスチャのうち、何番目のテクスチャを利用するのかを指定するために使われます。「sampler2D」という見慣れない型の変数ですが、実際はglUniform1i()
という関数を使って、int型の数値をセットすることで、「0番目のテクスチャを利用する」「1番目のテクスチャを利用する」といった指定をするだけですので、実質的に「sampler2D = intの別名」と捉えておいて問題ないかと思います。
そしてGLSLに用意されたtexture()
関数を使うことで、第1引数に何番目のテクスチャを利用するかを指定するsampler2D型のuniform変数を指定し、第2引数にUV座標を指定することで、4次元ベクトルとしてRGBAの色情報を取得することができます。
ここではこうして4次元ベクトとして取得したテクスチャ画像の色に対して、さらに頂点データとして指定されている色を掛け算することで、テクスチャ画像と頂点の色を合成しています。
ベクトル同士の掛け算は、X成分同士, Y成分同士, Z成分同士, W成分同士の掛け算です(もちろん計算結果もベクトルになります)。テクスチャ画像も頂点の色も、RGBAの各要素が0.0〜1.0の値で表されていますので、互いを掛け合わせても、0.0を下回ったり1.0を上回ったりすることのない値が出てきます。
なお、頂点シェーダでもフラグメント・シェーダでも、1行目の「
#version
」指定でGLSL 4.1のバージョンを指定しています。ただし、この数値はヒントにすぎませんので、実質GLSL 3.3の環境でも。
4. テクスチャを利用するコードを書く
Game.hppを次のように編集します。
#ifndef Game_hpp
#define Game_hpp
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include "Shader.hpp"
#include "Texture.hpp"
class Game
{
private:
ShaderProgram *program;
GLuint vbo;
GLuint indexBuffer;
GLuint vao;
Texture *tex;
public:
Game();
~Game();
public:
void Render();
};
#endif /* Game_hpp */
テクスチャを利用するために、Texture.hppをインクルードして、Textureクラスのインスタンスを参照するためのポインタ変数tex
をGameクラスに追加しました。
Game.cppでは、頂点データを表すVertexData構造体にUV座標を表すGLfloat型の値を2個分追加します。
struct VertexData
{
GLfloat pos[3];
GLfloat color[4];
GLfloat uv[2];
};
Gameクラスのコンストラクタの実装は次のとおりです。
Game::Game()
{
program = new ShaderProgram("myshader.vsh", "myshader.fsh");
std::vector<VertexData> data;
data.push_back({ { -0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f } });
data.push_back({ { 0.5f, -0.5f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f }, { 1.0f, 0.0f } });
data.push_back({ { 0.5f, 0.5f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f }, { 1.0f, 1.0f } });
data.push_back({ { -0.5f, 0.5f, 0.0f }, { 1.0f, 1.0f, 1.0f, 1.0f }, { 0.0f, 1.0f } });
std::vector<GLushort> indices;
indices.push_back(0);
indices.push_back(1);
indices.push_back(2);
indices.push_back(2);
indices.push_back(3);
indices.push_back(0);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(VertexData) * data.size(), &data[0], GL_STATIC_DRAW);
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * indices.size(), &indices[0], GL_STATIC_DRAW);
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), ((VertexData *)0)->pos);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(VertexData), ((VertexData *)0)->color);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData), ((VertexData *)0)->uv);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
tex = new Texture("photo.jpg");
glActiveTexture(GL_TEXTURE0);
tex->Bind();
program->Use();
program->SetUniform("tex", 0);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
VertexDataの値を追加するところに、UV座標の指定を追加しました。
インデックス・リストのセットアップ部分は以前のままです。
UV座標のための頂点属性が1つ増えましたので、そのフォーマット指定のためのglVertexAttribPointer()
関数の呼び出しがひとつ増えています。
テクスチャ画像の名前を指定してTextureクラスをnewしたら、0番目のテクスチャ画像を使うために、「GL_TEXTURE0」という定数を指定してglActiveTexture()
関数を呼び出して、利用するテクスチャの番号を指定します。そうしてTextureクラスのBind()
関数を呼び出すことで、GPU上に格納されている複数のテクスチャの中から、このTextureクラスが管理するテクスチャをGLSLのtexture()関数で参照できるようになるのです。
glActiveTexture()
関数で0番目のテクスチャ画像を使うと宣言したら、シェーダ・プログラムの方でもSetUniform()
関数を使ってuniform変数のtex
に「0」をセットして、フラグメント・シェーダの中で0番目のテクスチャが参照されるように設定しなければいけません。
筆者がテスト実行した環境では、シェーダの中ではデフォルトでsampler2D変数が0に初期化されていましたので、
SetUniform()
関数を使って0をセットしなくてもテクスチャが表示されました。しかし0で初期化されているという保証はありませんので、明示的に0をセットしておくことは重要だと思います。
Gameクラスのデストラクタに、tex
変数をdeleteするコードを追加します。
Game::~Game()
{
delete program;
delete tex;
...
}
Render()
関数の実装もほぼそのままですが、UV座標を送るための2番目の頂点属性をEnableするのを忘れないようにしなければいけません。
void Game::Render()
{
program->Use();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexArray(vao);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (void *)0);
}
5. テクスチャ画像の追加
テクスチャとして利用する画像を、Xcodeのプロジェクトに追加します。ここでは、次のような画像を「photo.jpg」という名前で用意しました。
この画像ファイルを、Xcodeの「Resources」グループの中にドラッグして追加します。
追加する時には次のようなダイアログが表示されますが、上と下にある2箇所のチェックボックスの両方に必ずチェックを入れてください。
これらのチェックボックスがチェックされていないと、アプリケーション本体の中に正常にコピーされないなどの問題が起きることがあります。
6. おわりに
ここまでできたら、プログラムを実行すると、次のように、前回表示した四角形の上に、テクスチャが重ねて表示されるようになります。
なお、各頂点の色をすべて { 1.0f, 1.0f, 1.0f, 1.0f } の白にすると、次のようにテクスチャ画像の色がそのまま表示されるようになります。ゲームなどでは、実際にこのようにテクスチャ画像の色だけを反映させることの方が多いでしょう。
ここまでのプロジェクト:MyGLGame_step2-4.zip