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

はじめに

ウィンドウが開いたら、次は何かを描く

3Dグラフィックスの世界では、すべてが三角形でできている。

┌─────────────────────────┐
│         △              │
│        △ △             │
│       △   △            │
│      △     △           │
│     △△△△△△          │  ← 四角形も三角形2つ
└─────────────────────────┘

だから最初にやるべきは三角形を描くこと

三角形を描くのに必要なもの

  1. 頂点データ: 三角形の3つの頂点座標
  2. VBO: 頂点データをGPUに送る
  3. VAO: 頂点データの構造を定義
  4. シェーダー: GPUに描画方法を指示

順番に見ていこう。

頂点データ

三角形の3つの頂点を定義する。

float vertices[] = {
    // 位置              // 色
    -0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // 左下 (赤)
     0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // 右下 (緑)
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f,  // 上   (青)
};

座標系はNDC(Normalized Device Coordinates)

  • X: -1.0(左端)~ +1.0(右端)
  • Y: -1.0(下端)~ +1.0(上端)
  • Z: -1.0(奥)~ +1.0(手前)

今回はZ=0の2D平面上に描く。

VBO(Vertex Buffer Object)

頂点データをGPUのメモリに転送する。

unsigned int VBO;
glGenBuffers(1, &VBO);               // バッファを生成
glBindBuffer(GL_ARRAY_BUFFER, VBO);  // バインド(これから操作する対象として指定)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

GL_STATIC_DRAW は「データはほとんど変更されない」という意味。

他のオプション:

  • GL_DYNAMIC_DRAW: 頻繁に変更される
  • GL_STREAM_DRAW: 毎フレーム変更される

VAO(Vertex Array Object)

VBOのデータ構造を定義する。「このデータの何番目から何番目が位置で、何番目から何番目が色」という情報。

unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

// 位置属性 (location = 0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 色属性 (location = 1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

glVertexAttribPointer の引数:

  1. 属性のインデックス: シェーダーの location
  2. 要素数: vec3なら3
  3. : GL_FLOAT
  4. 正規化: GL_FALSE
  5. ストライド: 次の頂点までのバイト数
  6. オフセット: この属性の開始位置

データ構造のイメージ:

vertices[] = [
    位置X, 位置Y, 位置Z, 色R, 色G, 色B,  ← 頂点0
    位置X, 位置Y, 位置Z, 色R, 色G, 色B,  ← 頂点1
    位置X, 位置Y, 位置Z, 色R, 色G, 色B,  ← 頂点2
]
          ↑             ↑
          |             |
     offset=0      offset=12 (3*4bytes)
     
     stride = 24 (6*4bytes)

シェーダー

シェーダーはGPU上で動くプログラム。**GLSL(OpenGL Shading Language)**で書く。

頂点シェーダー(basic.vert)

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 vertexColor;

void main() {
    gl_Position = vec4(aPos, 1.0);
    vertexColor = aColor;
}
  • layout (location = 0) in vec3 aPos: VAOで設定した属性0を受け取る
  • gl_Position: 出力。頂点の最終位置
  • out vec3 vertexColor: フラグメントシェーダーに渡す

フラグメントシェーダー(basic.frag)

#version 330 core
in vec3 vertexColor;
out vec4 FragColor;

void main() {
    FragColor = vec4(vertexColor, 1.0);
}
  • in vec3 vertexColor: 頂点シェーダーから受け取る
  • FragColor: 出力。ピクセルの色(RGBA)

なぜ色がグラデーションになる?

頂点ごとに違う色を指定すると、OpenGLが自動的に補間してくれる。

これをラスタライズという。

シェーダーのコンパイル

unsigned int compileShader(unsigned int type, const std::string& source) {
    unsigned int shader = glCreateShader(type);
    const char* src = source.c_str();
    glShaderSource(shader, 1, &src, nullptr);
    glCompileShader(shader);

    // エラーチェック
    int success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetShaderInfoLog(shader, 512, nullptr, infoLog);
        std::cerr << "Shader compilation failed:\n" << infoLog << std::endl;
    }

    return shader;
}

シェーダーのエラーチェックは必須。これがないとデバッグ不可能。

シェーダープログラムのリンク

unsigned int createShaderProgram(const std::string& vertPath, const std::string& fragPath) {
    std::string vertSource = loadShaderSource(vertPath);
    std::string fragSource = loadShaderSource(fragPath);

    unsigned int vertShader = compileShader(GL_VERTEX_SHADER, vertSource);
    unsigned int fragShader = compileShader(GL_FRAGMENT_SHADER, fragSource);

    unsigned int program = glCreateProgram();
    glAttachShader(program, vertShader);
    glAttachShader(program, fragShader);
    glLinkProgram(program);

    // エラーチェック(省略)

    glDeleteShader(vertShader);
    glDeleteShader(fragShader);

    return program;
}

頂点シェーダーとフラグメントシェーダーをリンクしてプログラムを作る。

描画

// メインループ内
glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

glUseProgram(shaderProgram);  // シェーダーを使用
glBindVertexArray(VAO);        // VAOをバインド
glDrawArrays(GL_TRIANGLES, 0, 3);  // 三角形を描画

glfwSwapBuffers(window);
glfwPollEvents();

glDrawArrays(GL_TRIANGLES, 0, 3):

  • GL_TRIANGLES: 三角形として描画
  • 0: 開始インデックス
  • 3: 頂点数

実行結果

カラフルな三角形が表示される!

        🔵
       /  \
      /    \
     /      \
    🔴------🟢

頂点ごとに色を指定したから、グラデーションになってる。

ハマりポイント

1. VAOをバインドし忘れ

// 描画時にVAOをバインドし忘れると何も表示されない
glBindVertexArray(VAO);  // ← これ忘れがち
glDrawArrays(GL_TRIANGLES, 0, 3);

2. シェーダーのコンパイルエラー

GLSLは型に厳しい。floatintを混ぜるとエラー。

// NG
gl_Position = vec4(aPos, 1);  // 1はint

// OK
gl_Position = vec4(aPos, 1.0);  // 1.0はfloat

3. 頂点の順番(巻き方向)

OpenGLはデフォルトで反時計回りの三角形を表面とみなす。

    0
   /|
  / |
 /  |
2---1   反時計回り = 表面

時計回りだと裏面扱いになって、カリング(裏面除去)が有効だと表示されない。

リソースの解放

終了時にGPUリソースを解放:

glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);

C++はガベージコレクションがないから、自分で解放する必要がある。

完全なコード

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <sstream>

std::string loadShaderSource(const std::string& filepath) {
    std::ifstream file(filepath);
    std::stringstream buffer;
    buffer << file.rdbuf();
    return buffer.str();
}

unsigned int compileShader(unsigned int type, const std::string& source) {
    unsigned int shader = glCreateShader(type);
    const char* src = source.c_str();
    glShaderSource(shader, 1, &src, nullptr);
    glCompileShader(shader);
    
    int success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetShaderInfoLog(shader, 512, nullptr, infoLog);
        std::cerr << "Shader error: " << infoLog << std::endl;
    }
    return shader;
}

unsigned int createShaderProgram(const std::string& vertPath, const std::string& fragPath) {
    unsigned int vert = compileShader(GL_VERTEX_SHADER, loadShaderSource(vertPath));
    unsigned int frag = compileShader(GL_FRAGMENT_SHADER, loadShaderSource(fragPath));
    
    unsigned int program = glCreateProgram();
    glAttachShader(program, vert);
    glAttachShader(program, frag);
    glLinkProgram(program);
    
    glDeleteShader(vert);
    glDeleteShader(frag);
    return program;
}

int main() {
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "Triangle", nullptr, nullptr);
    glfwMakeContextCurrent(window);
    gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

    float vertices[] = {
        -0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,
         0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,
         0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f,
    };

    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    unsigned int shader = createShaderProgram("shaders/basic.vert", "shaders/basic.frag");

    while (!glfwWindowShouldClose(window)) {
        glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        
        glUseProgram(shader);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shader);
    glfwTerminate();
    return 0;
}

まとめ

今回やったこと:

  1. 頂点データを定義
  2. VBO/VAOでGPUにデータを送る
  3. シェーダーを書いてコンパイル
  4. glDrawArrays で描画

三角形が描けた瞬間、めちゃくちゃ感動する

次回はシェーダーをもっと深掘り!

次回予告

Part4: シェーダー書いたらGPUと対話できた

uniform変数を使って、色を動的に変える!

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?