11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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);

まとめ

今回やったこと:

  1. stb_imageで画像読み込み
  2. テクスチャをGPUにアップロード
  3. UV座標でマッピング
  4. EBOでインデックス描画

テクスチャが貼れるようになると、見た目が一気にリッチになる。

次回は3Dモデルを読み込む!

次回予告

Part6: 3Dモデル読み込み、objファイルパーサー自作

OBJファイルを解析して、3Dモデルを表示!

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?