OpenGLの開発環境が整ったら、いよいよ実際に何かを画面に描画してみましょう。3Dグラフィックスの世界で最も基本的な図形は三角形です。この記事では、シェーダーの仕組みを理解しながら、画面にカラフルな三角形を表示するまでの手順をコード付きで解説します。
OpenGLの開発環境については、こちらの記事で解説しています!
前提条件
この記事は、以下のセットアップが完了していることを前提としています。
- Visual Studioで空のC++プロジェクトが作成済み
- GLFW・GLAD・GLMが導入済み
- ウィンドウ表示のテストコード(青緑色のウィンドウ)が動作している
まだ環境が整っていない場合は、先に開発環境の構築記事をご覧ください。
三角形を描画するまでの全体像
OpenGLで三角形を描画するには、以下の要素を準備する必要があります。
- 頂点データ: 三角形の3つの角の座標を定義する
- VAO・VBO: 頂点データをGPUに渡すためのバッファオブジェクトを作成する
- シェーダープログラム: 頂点の位置と色を処理するプログラムをGPUに送る
- 描画コール: メインループの中で描画命令を発行する
一度に全部を理解する必要はありません。順番にコードを書きながら進めていきましょう。
手順1: 頂点データの定義
まず、三角形の3つの頂点の座標を定義します。
OpenGLの座標系は正規化デバイス座標(NDC: Normalized Device Coordinates)を使います。画面の中心が (0, 0) で、各軸の範囲は -1.0 〜 1.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引数 | 3 | 1頂点あたりの要素数(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. 三角形が表示されません
以下のポイントを順番に確認してください。
- シェーダーのコンパイルエラー: コンソールにエラーメッセージが出ていないか確認する
glUseProgramの呼び出し忘れ: 描画前にシェーダープログラムを有効にしているかglBindVertexArrayの呼び出し忘れ: 描画前にVAOをバインドしているか- 頂点座標の範囲: x, y座標が
-1.0〜1.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を呼び出して毎フレーム描画する
最後までお読みいただき、ありがとうございました!
