C++で作るゲームエンジン自作シリーズ
| Part1 設計編 | Part2 ウィンドウ | Part3 OpenGL | Part4 シェーダー | Part5 テクスチャ | Part6 3Dモデル | Part7 当たり判定 |
|---|---|---|---|---|---|---|
| - | - | - | - | 👈 Now | - | - |
はじめに
今まで描いてきたのは、色だけの三角形。
テクスチャ(画像)を貼ると、急にゲームっぽくなる。
テクスチャとは
テクスチャは、ポリゴンに貼り付ける画像のこと。
UV座標
テクスチャのどこを使うかを指定するのがUV座標。
テクスチャ画像
(0,1)┌─────────┐(1,1)
│ │
│ 画像 │
│ │
(0,0)└─────────┘(1,0)
UV座標は0.0〜1.0の範囲。
頂点ごとにUV座標を指定すると、OpenGLが自動で補間してくれる。
stb_imageで画像読み込み
C++で画像を読み込むのは面倒。stb_imageを使うと1行で済む。
stb_imageとは
ヘッダオンリーライブラリ。.hファイル1つで完結。
使い方
// stb_image.hをダウンロードして、1つのcppファイルで実装を有効化
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// 画像を読み込む
int width, height, channels;
unsigned char* data = stbi_load("texture.png", &width, &height, &channels, 0);
if (data) {
// 使用後は解放
stbi_image_free(data);
}
テクスチャの作成
OpenGLにテクスチャを登録する:
unsigned int texture;
glGenTextures(1, &texture);
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_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 画像データを転送
int width, height, channels;
stbi_set_flip_vertically_on_load(true); // OpenGLは左下が原点なので反転
unsigned char* data = stbi_load("texture.png", &width, &height, &channels, 0);
if (data) {
GLenum format = channels == 4 ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(data);
} else {
std::cerr << "Failed to load texture" << std::endl;
}
パラメータの意味
ラップモード(UV座標が0-1を超えたとき)
GL_REPEAT // 繰り返し(タイリング)
GL_MIRRORED_REPEAT // 鏡像繰り返し
GL_CLAMP_TO_EDGE // 端の色で埋める
GL_CLAMP_TO_BORDER // 指定した色で埋める
フィルタリング(拡大・縮小時)
GL_NEAREST // 最近傍補間(ドット絵向け)
GL_LINEAR // 線形補間(滑らか)
GL_LINEAR_MIPMAP_LINEAR // ミップマップ + 線形補間
ミップマップ
遠くのテクスチャ用に、小さいサイズのテクスチャを事前に作っておく技術。
オリジナル → 1/2 → 1/4 → 1/8 → ...
1024x1024 512x512 256x256 128x128
glGenerateMipmap で自動生成される。
頂点データにUV座標を追加
float vertices[] = {
// 位置 // UV座標
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // 右下
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // 右上
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f, // 左上
};
unsigned int indices[] = {
0, 1, 2, // 三角形1
0, 2, 3, // 三角形2
};
四角形を描くので、インデックスバッファ(EBO)を使う。
VAOの設定
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// UV属性
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
シェーダー
頂点シェーダー(texture.vert)
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main() {
gl_Position = transform * vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
フラグメントシェーダー(texture.frag)
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D texture1;
void main() {
FragColor = texture(texture1, TexCoord);
}
sampler2D がテクスチャ。texture() 関数でサンプリング。
テクスチャのバインド
描画時:
glActiveTexture(GL_TEXTURE0); // テクスチャユニット0を有効化
glBindTexture(GL_TEXTURE_2D, texture);
// シェーダーに伝える
glUniform1i(glGetUniformLocation(shader, "texture1"), 0);
複数テクスチャを使う場合:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(shader, "texture1"), 0);
glUniform1i(glGetUniformLocation(shader, "texture2"), 1);
EBO(Element Buffer Object)
同じ頂点を使い回すために、インデックスバッファを使う:
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 描画
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawArrays の代わりに glDrawElements を使う。
完全なコード
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <iostream>
int main() {
// GLFW初期化(省略)
// 頂点データ
float vertices[] = {
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
};
unsigned int indices[] = { 0, 1, 2, 0, 2, 3 };
// VAO, VBO, EBO作成
unsigned int VAO, VBO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
// テクスチャ作成
unsigned int texture;
glGenTextures(1, &texture);
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);
stbi_set_flip_vertically_on_load(true);
int w, h, ch;
unsigned char* data = stbi_load("texture.png", &w, &h, &ch, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
stbi_image_free(data);
// メインループ
while (!glfwWindowShouldClose(window)) {
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shader);
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
return 0;
}
実行結果
四角形にテクスチャが貼られる!
急にゲームっぽくなった!
テクスチャのテクニック
スプライトシート
1枚の画像に複数のスプライトを配置し、UV座標で切り出す:
// スプライトシートから特定のフレームを取得
vec2 spriteSize = vec2(1.0 / 4.0, 1.0 / 4.0); // 4x4グリッド
vec2 spriteOffset = vec2(frame % 4, frame / 4);
vec2 uv = (TexCoord + spriteOffset) * spriteSize;
FragColor = texture(spriteSheet, uv);
テクスチャブレンド
複数テクスチャを混ぜる:
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform float mixRatio;
void main() {
vec4 color1 = texture(texture1, TexCoord);
vec4 color2 = texture(texture2, TexCoord);
FragColor = mix(color1, color2, mixRatio);
}
トラブルシューティング
テクスチャが真っ白/真っ黒
- 画像のパスが正しいか確認
-
stbi_loadの戻り値をチェック - テクスチャのフォーマット(RGB/RGBA)を確認
テクスチャが上下逆
stbi_set_flip_vertically_on_load(true);
OpenGLは左下が原点なので、画像を反転する必要がある。
テクスチャがボケる
GL_NEAREST を使う(ドット絵向け):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
まとめ
今回やったこと:
- stb_imageで画像読み込み
- テクスチャをGPUにアップロード
- UV座標でマッピング
- EBOでインデックス描画
テクスチャが貼れるようになると、見た目が一気にリッチになる。
次回は3Dモデルを読み込む!
次回予告
Part6: 3Dモデル読み込み、objファイルパーサー自作
OBJファイルを解析して、3Dモデルを表示!