お役立ち情報を毎日発信しています!!
閉じる

OpenGLで最初の三角形を描画しよう【シェーダーの基礎からコード解説まで】

開発 2026年4月26日 2026年4月26日

OpenGLの開発環境が整ったら、いよいよ実際に何かを画面に描画してみましょう。3Dグラフィックスの世界で最も基本的な図形は三角形です。この記事では、シェーダーの仕組みを理解しながら、画面にカラフルな三角形を表示するまでの手順をコード付きで解説します。

管理人
三角形が描画できれば、四角形も立方体もキャラクターも描けるようになります。3Dグラフィックスの第一歩を踏み出しましょう!

OpenGLの開発環境については、こちらの記事で解説しています!

OpenGL開発環境の構築手順【Visual Studio+GLFW+GLAD+GLMをWindows上にセットアップ】
OpenGLを使ったゲーム開発を始めるには、まず開発環境を整える必要があります。この記事では、Windo...
あわせて読みたい

前提条件

この記事は、以下のセットアップが完了していることを前提としています。

  • Visual Studioで空のC++プロジェクトが作成済み
  • GLFW・GLAD・GLMが導入済み
  • ウィンドウ表示のテストコード(青緑色のウィンドウ)が動作している

まだ環境が整っていない場合は、先に開発環境の構築記事をご覧ください。

三角形を描画するまでの全体像

OpenGLで三角形を描画するには、以下の要素を準備する必要があります。

  1. 頂点データ: 三角形の3つの角の座標を定義する
  2. VAO・VBO: 頂点データをGPUに渡すためのバッファオブジェクトを作成する
  3. シェーダープログラム: 頂点の位置と色を処理するプログラムをGPUに送る
  4. 描画コール: メインループの中で描画命令を発行する

一度に全部を理解する必要はありません。順番にコードを書きながら進めていきましょう。

手順1: 頂点データの定義

まず、三角形の3つの頂点の座標を定義します。

OpenGLの座標系は正規化デバイス座標(NDC: Normalized Device Coordinates)を使います。画面の中心が (0, 0) で、各軸の範囲は -1.01.0 です。

        (0.0, 0.5)
           /\
          /  \
         /    \
        /      \
       /________\
(-0.5, -0.5)  (0.5, -0.5)

この座標をC++のコードで表現すると以下のようになります。

float vertices[] = {
    // 位置(x, y, z)
    -0.5f, -0.5f, 0.0f,  // 左下
     0.5f, -0.5f, 0.0f,  // 右下
     0.0f,  0.5f, 0.0f   // 上中央
};

z座標は 0.0f にしていますが、これは2D的に表示するためです。

手順2: VAOとVBOの作成

頂点データをGPUに渡すために、VAO(Vertex Array Object)VBO(Vertex Buffer Object)を作成します。

VBO(Vertex Buffer Object)とは

VBOは、頂点データをGPUのメモリ(VRAM)に保存するためのバッファです。CPUのメモリからGPUのメモリにデータを転送しておくことで、描画時に高速にアクセスできます。

VAO(Vertex Array Object)とは

VAOは、頂点データの読み取り方のルールを記録するオブジェクトです。「この配列の何バイト目から何バイトずつが位置データで、何バイトずつが色データか」といった情報を保存します。

コード

// VAOとVBOの生成
unsigned int VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);

// VAOをバインド(以降の設定がこのVAOに記録される)
glBindVertexArray(VAO);

// VBOをバインドして頂点データを転送
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 頂点属性の設定(位置データ: location=0, 3要素, float型)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// バインドを解除
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

glVertexAttribPointer の各引数の意味は以下の通りです。

引数意味
第1引数0頂点属性のインデックス(シェーダーの location = 0 に対応)
第2引数31頂点あたりの要素数(x, y, zの3つ)
第3引数GL_FLOATデータ型
第4引数GL_FALSE正規化するかどうか
第5引数3 * sizeof(float)ストライド(次の頂点までのバイト数)
第6引数(void*)0データの開始位置のオフセット

手順3: シェーダーの作成

頂点シェーダー

頂点シェーダーは各頂点の位置を処理します。今回は受け取った座標をそのまま出力します。

const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos, 1.0);
}
)";
  • layout (location = 0)glVertexAttribPointer の第1引数と対応しています
  • gl_Position はOpenGLに「この頂点はここに表示して」と伝える特殊な変数です

フラグメントシェーダー

フラグメントシェーダーは各ピクセルの色を決定します。

const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);  // オレンジ色(R, G, B, A)
}
)";
  • FragColor がそのピクセルの最終的な色になります
  • 各色の値は 0.0(なし)〜 1.0(最大)です

シェーダーのコンパイルとリンク

シェーダーのソースコードをGPUが実行できるプログラムに変換する処理です。

// 頂点シェーダーのコンパイル
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);

// コンパイルエラーのチェック
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
    glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
    std::cerr << "頂点シェーダーのコンパイルに失敗: " << infoLog << std::endl;
}

// フラグメントシェーダーのコンパイル
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);

glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
    glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
    std::cerr << "フラグメントシェーダーのコンパイルに失敗: " << infoLog << std::endl;
}

// シェーダープログラムの作成とリンク
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
    glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
    std::cerr << "シェーダープログラムのリンクに失敗: " << infoLog << std::endl;
}

// リンク後は個別のシェーダーオブジェクトを削除
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

手順4: メインループでの描画

これまでに作成したすべての要素を組み合わせて、メインループ内で三角形を描画します。

// メインループ
while (!glfwWindowShouldClose(window))
{
    processInput(window);

    // 背景色でクリア
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // シェーダープログラムを使用
    glUseProgram(shaderProgram);

    // VAOをバインドして描画
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    glfwSwapBuffers(window);
    glfwPollEvents();
}

glDrawArrays(GL_TRIANGLES, 0, 3) は「三角形モードで、頂点0番目から3つの頂点を使って描画」という意味です。

完成コード

すべてをまとめた完成版のコードです。main.cpp を以下の内容に書き換えてください。

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

// シェーダーのソースコード
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos, 1.0);
}
)";

const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
)";

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

int main()
{
    // GLFWの初期化
    if (!glfwInit())
    {
        std::cerr << "GLFWの初期化に失敗しました" << std::endl;
        return -1;
    }

    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, "OpenGL Triangle", nullptr, nullptr);
    if (!window)
    {
        std::cerr << "ウィンドウの作成に失敗しました" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // GLADの初期化
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cerr << "GLADの初期化に失敗しました" << std::endl;
        return -1;
    }

    // --- シェーダーのコンパイル ---
    int success;
    char infoLog[512];

    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
        std::cerr << "頂点シェーダーのコンパイルに失敗: " << infoLog << std::endl;
    }

    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
        std::cerr << "フラグメントシェーダーのコンパイルに失敗: " << infoLog << std::endl;
    }

    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success)
    {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
        std::cerr << "シェーダープログラムのリンクに失敗: " << infoLog << std::endl;
    }

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // --- 頂点データの設定 ---
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.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, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // --- メインループ ---
    while (!glfwWindowShouldClose(window))
    {
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // リソースの解放
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    glfwTerminate();
    return 0;
}

実行結果

ビルドして実行すると、暗い青緑色の背景の中央にオレンジ色の三角形が表示されます。

コードの流れの整理

完成コードの処理の流れを整理しておきましょう。

1. GLFW初期化 → ウィンドウ作成
2. GLAD初期化 → OpenGL関数をロード
3. シェーダーのコンパイル・リンク → GPUプログラムを作成
4. 頂点データの設定 → VAO/VBOでGPUにデータを渡す
5. メインループ → 毎フレーム描画を繰り返す
6. 終了時にリソースを解放

次のステップ

三角形の描画が成功したら、以下のようなステップに挑戦してみましょう。

  • 色を変えてみる: フラグメントシェーダーの FragColor の値を変更する
  • 頂点ごとに色を付ける: 頂点データに色情報を追加し、頂点シェーダーからフラグメントシェーダーに色を渡す
  • 四角形を描く: EBO(Element Buffer Object)を使ってインデックス描画に挑戦する
  • テクスチャを貼る: 画像ファイルを読み込んで三角形に表示する
  • 座標変換: GLMの行列を使って図形を回転・移動・拡大する

よくある質問(FAQ)

Q. 三角形が表示されません

以下のポイントを順番に確認してください。

  1. シェーダーのコンパイルエラー: コンソールにエラーメッセージが出ていないか確認する
  2. glUseProgram の呼び出し忘れ: 描画前にシェーダープログラムを有効にしているか
  3. glBindVertexArray の呼び出し忘れ: 描画前にVAOをバインドしているか
  4. 頂点座標の範囲: x, y座標が -1.01.0 の範囲内にあるか

Q. VAOとVBOの違いがわかりません

  • VBO: 頂点データそのもの(座標値など)をGPUのメモリに保存する箱
  • VAO: VBOの中身の読み取り方のルール(「3つのfloatずつが1頂点」など)を記録するオブジェクト

VAOをバインドした状態でVBOの設定を行うと、そのルールがVAOに記録されます。描画時にはVAOをバインドするだけで、以前に設定したすべてのルールが自動的に復元されます。

Q. シェーダーを別ファイルに分けることはできますか?

できます。.glsl.vert / .frag という拡張子のファイルにシェーダーのソースコードを保存し、C++側でファイルを読み込んで文字列としてOpenGLに渡す方法が一般的です。プロジェクトが大きくなってきたら、シェーダーファイルを分離することをおすすめします。

まとめ

  • OpenGLで図形を描画するには頂点データVAO/VBOシェーダーの3つが必要
  • VBOで頂点データをGPUに渡し、VAOでデータの読み取りルールを記録する
  • 頂点シェーダーで位置を、フラグメントシェーダーで色を処理する
  • シェーダーはGLSLで記述し、コンパイル・リンクしてGPUプログラムとして実行する
  • メインループ内で glDrawArrays を呼び出して毎フレーム描画する
管理人
三角形が画面に表示された瞬間は感動的ですよね!ここから色を変えたり図形を増やしたり、どんどん楽しくなっていきますよ!

最後までお読みいただき、ありがとうございました!


よかったらシェアしてね
URLをコピーしました
URLをコピーしました

この記事を書いた人

くすんちゅ
このサイトの管理人。沖縄在住のフリーランスエンジニア。最近は陸だけでなく、海の中でも見かけられることがある。

関連記事

ここにはおすすめ商品の名前を入れます
ネットショップ広告商品名1
ここにはおすすめ商品の名前を入れます
ネットショップ広告商品名2