【図解】OpenGLの基本を徹底解説!環境構築から最初の描画まで
はじめに
3Dグラフィックスの世界へようこそ!
あなたが今プレイしているゲームの美麗なキャラクターや風景、映画で使われる圧倒的なVFX、建築設計で使われるリアルなシミュレーション映像。これらの多くは、「グラフィックスAPI」と呼ばれる技術によって描画されています。その中でも、OpenGL (Open Graphics Library) は、最も歴史が長く、幅広いプラットフォームで利用されている標準的な3DグラフィックスAPIの一つです。
OpenGLを学ぶことで、あなたはコンピュータがどのようにして3Dの世界を2Dの画面に描き出しているのか、その根幹の仕組みを理解することができます。それはまるで、魔法の裏側を覗き見るような、知的好奇心を満たすエキサイティングな体験です。
この記事では、3Dグラフィックスの経験が全くない方でも、OpenGLの世界に第一歩を踏み出せるように、以下の内容を徹底的に、そして図解を交えながら分かりやすく解説していきます。
- OpenGLの基本的な仕組み: 3Dデータが画面に表示されるまでの「グラフィックスパイプライン」や、GPUを直接プログラミングする「シェーダー」といった核心的な概念を理解します。
- 開発環境の構築: Windows環境を例に、C++でOpenGLプログラミングを始めるための具体的な手順を、一つ一つ丁寧に説明します。
- 最初の描画: 実際にコードを書き、画面に最初の図形である「三角形」を描画します。これは、グラフィックスプログラミングにおける “Hello, World!” とも言える重要なステップです。
対象読者としては、C++の基本的な文法(変数、関数、ループなど)を理解しているプログラミング初学者から中級者の方、そして純粋に3Dグラフィックスの仕組みに興味がある方を想定しています。
この記事を読み終える頃には、あなたは自分の手でGPUを動かし、画面に図形を描き出す力を手に入れているはずです。それでは、奥深くも魅力的なOpenGLの世界への旅を始めましょう!
第1章: OpenGLを理解するための基礎知識
コードを書き始める前に、まずはOpenGLがどのような仕組みで動いているのか、その全体像を掴むことが非常に重要です。ここでは、3Dグラフィックスの心臓部とも言える4つの重要な概念について解説します。
1-1. グラフィックスパイプラインとは?
あなたがプログラムで用意した「ここに頂点がある」「この頂点とこの頂点を結んで面を作る」といった単なる数値データが、最終的に画面上の美しいピクセルとして表示されるまでには、一連の決まった処理の流れがあります。この一連の処理の流れをグラフィックスパイプライン (Graphics Pipeline) と呼びます。
パイプラインという名前の通り、データはまるで工場のベルトコンベアに乗せられたかのように、各ステージを順番に通過しながら加工されていきます。
【図解1: モダンOpenGLのグラフィックスパイプライン】
[ 頂点データ ] -> [ 頂点シェーダー ] -> [ ラスタライゼーション ] -> [ フラグメントシェーダー ] -> [ テストとブレンディング ] -> [ フレームバッファ ]
| | | | | |
3Dモデルの 頂点の座標を ピクセルの元となる ピクセルの色を 深度テストや 最終的な
頂点座標など 変換するステージ 「フラグメント」を 決定するステージ アルファブレンディング 描画結果
生成するステージ を行うステージ
モダンOpenGL(本記事で扱うバージョン)では、このパイプラインのいくつかのステージをプログラマーが自由に書き換えることができます。特に重要なのが、青色で示された頂点シェーダーとフラグメントシェーダーです。これらはGPU(Graphics Processing Unit)上で直接実行される小さなプログラムであり、OpenGLプログラミングの核となります。
- 旧来のOpenGL(固定機能パイプライン)との違い: 昔のOpenGLでは、パイプラインの各機能(ライティングや座標変換など)は固定されており、プログラマーは用意された関数のパラメータを調整することしかできませんでした。しかし、モダンOpenGLでは「シェーダー」をプログラミングすることで、より柔軟で高度な表現が可能になりました。この記事では、このモダンOpenGLのアプローチを学びます。
1-2. シェーダーとは?
シェーダー (Shader) とは、GPU上で実行するために書かれたプログラムのことです。シェーダーを使うことで、グラフィックスパイプラインの特定のステージの動作を我々自身で定義できます。何千、何万という頂点やピクセルに対して並列で実行されるため、非常に高速な処理が可能です。
OpenGLで最低限必要となるのは、以下の2つのシェーダーです。
-
頂点シェーダー (Vertex Shader)
- 役割: グラフィックスパイプラインの入力として与えられた個々の「頂点」データを受け取り、その位置を計算・変換することです。主な仕事は、3D空間内の頂点座標を、最終的に画面に表示される2D座標系へと変換することです。回転、移動、拡大・縮小といった処理もここで行います。
- 入力: 頂点の座標、法線、テクスチャ座標など。
- 出力: 変換後の頂点の座標(
gl_Position
という特別な変数に格納)。
-
フラグメントシェーダー (Fragment Shader)
- 役割: ラスタライゼーションステージで生成された「フラグメント」一つ一つの最終的な色を決定することです。フラグメントとは、画面上のピクセルになる可能性のある候補だと考えてください。ライティング計算による陰影付けや、テクスチャからの色のサンプリングなど、見た目に直接関わる処理の多くがここで行われます。
- 入力: 頂点シェーダーから補間されて渡されたデータ(座標、色、テクスチャ座標など)。
- 出力: ピクセルの最終的な色(RGBA値)。
これらのシェーダーは、GLSL (OpenGL Shading Language) という、C言語によく似た専用の言語で記述します。
【図解2: シェーダーの役割】
“`
// 3Dモデルの頂点データ
Vertex1(x,y,z), Vertex2(x,y,z), Vertex3(x,y,z)
↓ [ 頂点シェーダーが各頂点に対して実行される ]
// スクリーン上の位置に変換された頂点データ
ScreenPos1, ScreenPos2, ScreenPos3
↓ [ ラスタライザが頂点を結ぶ領域をピクセル候補(フラグメント)で埋める ]
// 三角形の領域内のフラグメント群
FragmentA, FragmentB, FragmentC, …
↓ [ フラグメントシェーダーが各フラグメントに対して実行される ]
// 色が決定されたピクセル群
PixelA(R,G,B,A), PixelB(R,G,B,A), PixelC(R,G,B,A), …
↓ [ フレームバッファに書き込まれ、画面に表示される ]
“`
1-3. 座標系を理解しよう
3D空間の点を画面に表示するには、いくつかの異なる座標系 (Coordinate System) を経由して座標を変換していく必要があります。これは、俳優が舞台(ワールド)で演じているのを、特定の場所からカメラで撮影し、最終的に写真(スクリーン)にするプロセスに似ています。
- ローカル空間 (Local Space / Object Space):
- オブジェクト(モデル)自身の原点を基準とした座標系です。3Dモデリングソフトでモデルを作成するときの座標がこれにあたります。
- ワールド空間 (World Space):
- シーン全体のグローバルな原点を基準とした座標系です。ローカル空間にあるオブジェクトを、シーン内の特定の位置に配置したり、回転させたりしてワールド空間に変換します。この変換を行うのがモデル行列 (Model Matrix) です。
- ビュー空間 (View Space / Eye Space):
- カメラ(視点)を原点とした座標系です。ワールド空間にあるオブジェクトを、カメラから見たときの相対的な位置に変換します。この変換を行うのがビュー行列 (View Matrix) です。
- クリップ空間 (Clip Space):
- カメラの視野(見える範囲、これをビュー錐台 (View Frustum) と呼びます)に入るオブジェクトだけを描画するための座標系です。この範囲外のものは描画されません(クリッピングされます)。ビュー空間からクリップ空間への変換を行うのがプロジェクション行列 (Projection Matrix) です。この変換後の座標は通常、-1.0から1.0の範囲に正規化されます。
- スクリーン空間 (Screen Space):
- 最終的にウィンドウのどのピクセルに描画されるかを決定する2Dの座標系です。クリップ空間の座標が、ウィンドウの解像度に合わせて変換されます。この処理はOpenGLが自動的に行います(ビューポート変換)。
【図解3: 座標変換の流れ】
[ローカル空間] --(モデル行列)--> [ワールド空間] --(ビュー行列)--> [ビュー空間] --(プロジェクション行列)--> [クリップ空間] --(ビューポート変換)--> [スクリーン空間]
今回の最初の三角形の描画では、話を簡単にするために、頂点座標を最初からクリップ空間の座標(-1.0から1.0の範囲)で直接定義します。
1-4. OpenGLの重要な概念
最後に、OpenGLを扱う上で知っておくべき2つの重要な設計思想について説明します。
- ステートマシン (State Machine): OpenGLは巨大な「ステートマシン」として設計されています。これは、OpenGLが現在の状態(例えば「今から描画する色は赤」「このテクスチャを使う」など)を内部で保持していることを意味します。
glColor3f(1.0, 0.0, 0.0)
のような関数を一度呼び出すと、次に別の色を指定するまで、描画されるオブジェクトはすべて赤色になります。モダンOpenGLでは、多くの状態がVAO (Vertex Array Object) のようなオブジェクトにカプセル化され、管理が容易になっています。 - オブジェクト (Object): OpenGLでは、描画に必要なデータや状態のまとまりを「オブジェクト」という単位で管理します。例えば、頂点データを格納するVBO(Vertex Buffer Object)、シェーダープログラム、テクスチャなどはすべてオブジェクトです。私たちはまずオブジェクトを生成し(
glGen...
)、それを特定の種類にバインド(結びつけ,glBind...
)し、設定を行い、最後に描画に使用します。この「生成 → バインド → 設定」という流れは、OpenGLプログラミングで頻繁に登場するパターンです。
これらの基礎知識を頭の片隅に置いた上で、いよいよ開発環境を構築していきましょう。
第2章: 開発環境の構築
ここでは、WindowsとVisual Studio 2022をベースに、C++でOpenGLプログラミングを行うための環境を構築します。macOSやLinuxでも同様のライブラリ(GLFW, GLAD)を使って構築可能ですが、手順が若干異なります。
2-1. 必要なツールとライブラリ
OpenGL自体はOSやグラフィックスドライバの一部として提供されていますが、それだけではウィンドウを作成したり、OSごとに異なるOpenGL関数のアドレスを取得したりすることができません。そのため、いくつかの補助ライブラリが必要になります。
- C++コンパイラとIDE: Visual Studio 2022 を使用します。「C++によるデスクトップ開発」ワークロードが必要です。
- ビルドシステム: CMake を使用します。CMakeは、ソースコードから各環境(Visual Studio, Makefileなど)に合わせたビルドファイルを生成してくれるツールで、クロスプラットフォーム開発の標準となっています。
- ウィンドウ管理ライブラリ: GLFW を使用します。OSに依存するウィンドウの作成、キーボードやマウス入力の受付といった面倒な処理を、簡単なAPIで実現してくれます。
- OpenGL関数ローダー: GLAD を使用します。OpenGLの関数は、実は実行時にグラフィックスドライバからそのアドレスを取得しないと呼び出せません。GLADは、この面倒なアドレス取得処理を自動で行ってくれるライブラリです。
2-2. 環境構築手順
Step 1: Visual Studio と CMake のインストール
- Visual Studio 2022: 公式サイト から “Community” 版をダウンロードします。インストーラーで「C++によるデスクトップ開発」ワークロードにチェックを入れてインストールしてください。
- CMake: 公式サイト から最新版のWindowsインストーラー (
.msi
) をダウンロードします。インストール中に「Add CMake to the system PATH for all users」または「… for current user」を選択し、コマンドプロンプトからCMakeが使えるようにしておきましょう。
Step 2: GLFW の準備
GLFWはソースコードから自分でビルドしてライブラリファイルを作成します。
- GLFWの公式サイト へ行き、「Source package」をダウンロードして、適当な場所(例:
C:\dev\libs
)に展開します。展開後のフォルダ名はglfw-3.3.8
のようになります。 - コマンドプロンプト(またはPowerShell)を開き、展開したGLFWのフォルダに移動します。
bash
cd C:\dev\libs\glfw-3.3.8 - CMakeを使ってVisual Studio用のプロジェクトファイルを生成します。
build
という名前のフォルダを生成先に指定します。
bash
cmake -S . -B build - ビルドを実行します。これにより、ライブラリファイルが生成されます。
bash
cmake --build build --config Release - ビルドが成功すると、
C:\dev\libs\glfw-3.3.8\build\src\Release
フォルダ内にglfw3.lib
というファイルが生成されます。このファイルが後で必要になるライブラリファイルです。ヘッダーファイルはC:\dev\libs\glfw-3.3.8\include
にあります。
Step 3: GLAD の準備
GLADはWebサービス上で必要なファイルを生成するユニークな方法を採用しています。
- GLADのWebサービス にアクセスします。
- 以下の設定を選択します。
- Language: C/C++
- Specification: OpenGL
- API / gl: Version 4.1 (またはそれ以上。4.1は比較的広くサポートされています)
- Profile: Core
- Options: 「Generate a loader」にチェックが入っていることを確認。
- ページ下部の「GENERATE」ボタンをクリックします。
glad.zip
というファイルがダウンロードされるので、これをクリックして中身をダウンロードします。- zipファイルの中には
include
フォルダとsrc
フォルダがあります。include/glad/glad.h
include/KHR/khrplatform.h
src/glad.c
- この3つのファイルがプログラムに必要になります。
2-3. プロジェクトのセットアップ (CMake)
いよいよ、これらを統合するOpenGLプロジェクトを作成します。
- 任意の場所にプロジェクト用のフォルダを作成します(例:
C:\dev\OpenGLProject
)。 OpenGLProject
フォルダ内に、以下のファイルとフォルダを配置します。main.cpp
(これから作成するメインのソースファイル)CMakeLists.txt
(CMakeのビルド設定ファイル)libs
フォルダ (サードパーティライブラリを置く場所)libs/glfw/include
libs/glfw/lib
libs/glad/include
- 先ほど準備したGLFWとGLADのファイルをコピーします。
C:\dev\libs\glfw-3.3.8\include\GLFW
フォルダをOpenGLProject\libs\glfw\include
にコピー。C:\dev\libs\glfw-3.3.8\build\src\Release\glfw3.lib
をOpenGLProject\libs\glfw\lib
にコピー。- GLADのzipから展開した
glad
とKHR
フォルダをOpenGLProject\libs\glad\include
にコピー。 src/glad.c
をOpenGLProject
のルートにコピーします。
最終的なフォルダ構成は以下のようになります。
OpenGLProject/
├── libs/
│ ├── glfw/
│ │ ├── include/
│ │ │ └── GLFW/
│ │ │ ├── glfw3.h
│ │ │ └── glfw3native.h
│ │ └── lib/
│ │ └── glfw3.lib
│ └── glad/
│ └── include/
│ ├── glad/
│ │ └── glad.h
│ └── KHR/
│ └── khrplatform.h
├── CMakeLists.txt
├── main.cpp
└── glad.c
-
CMakeLists.txt
をテキストエディタで開き、以下のように記述します。“`cmake
CMakeの最低バージョンを指定
cmake_minimum_required(VERSION 3.10)
プロジェクト名を定義
project(OpenGLProject)
C++標準を17に設定
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)実行ファイルを作成することを宣言
${PROJECT_NAME} は “OpenGLProject”
main.cpp と glad.c をソースファイルとして指定
add_executable(${PROJECT_NAME} main.cpp glad.c)
ヘッダーファイルの検索パスを追加
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/libs/glad/include
${CMAKE_CURRENT_SOURCE_DIR}/libs/glfw/include
)リンクするライブラリの検索パスを追加
target_link_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/libs/glfw/lib
)実際にリンクするライブラリを指定
glfw3.lib と、OpenGLの標準ライブラリである opengl32.lib
target_link_libraries(${PROJECT_NAME} PRIVATE
glfw3
opengl32
)
“` -
main.cpp
に、ウィンドウを表示するだけの最小限のコードを記述します。“`cpp
include
include
include
void framebuffer_size_callback(GLFWwindow window, int width, int height);
void processInput(GLFWwindow window);// 設定
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;int main()
{
// GLFW: 初期化と設定
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// GLFW: ウィンドウ作成 GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // GLAD: OpenGL関数のポインタをロードする if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // レンダリングループ (メインループ) while (!glfwWindowShouldClose(window)) { // 入力 processInput(window); // レンダリングコマンド glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // バッファの交換とイベント処理 glfwSwapBuffers(window); glfwPollEvents(); } // GLFW: リソースのクリーンアップ glfwTerminate(); return 0;
}
// ウィンドウサイズが変更されたときに呼び出されるコールバック関数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}// 入力処理: ESCキーが押されたらウィンドウを閉じる
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
“` -
コマンドプロンプトでプロジェクトフォルダに移動し、CMakeを実行してビルドします。
“`bash
Visual Studio 2022用のソリューションファイルを build フォルダに生成
cmake -S . -B build
プロジェクトをビルド
cmake –build build
``
build/Debug
7.フォルダに
OpenGLProject.exe` が生成されているはずです。これを実行すると、深緑色の背景のウィンドウが表示されれば、環境構築は成功です!
第3章: 最初の三角形を描画する
環境が整ったので、いよいよOpenGLの真骨頂である描画処理に入ります。ここでは、グラフィックスプログラミングの「Hello, World!」である、単一の色の三角形を描画します。
3-1. 描画データの準備 (頂点データ)
まず、描画したい図形(三角形)の頂点座標を定義します。OpenGLでは、3D座標は最終的に正規化デバイス座標 (Normalized Device Coordinates, NDC) と呼ばれる、x
, y
, z
全てが -1.0
から 1.0
の範囲に収まる特殊な座標系に変換されます。この範囲外の頂点は描画されません。
今回は簡単のため、最初からこの正規化デバイス座標で頂点を定義します。
main.cpp
の main
関数の先頭あたりに、float型の配列として3つの頂点を定義しましょう。各頂点は(x, y, z)の3つの要素を持ちます。
cpp
// ... main関数の中、glfwInit()の前あたり ...
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 左下の頂点
0.5f, -0.5f, 0.0f, // 右下の頂点
0.0f, 0.5f, 0.0f // 上の頂点
};
これにより、画面中央に底辺がx軸に平行な正三角形を定義したことになります。
3-2. データをGPUに送る (VBO, VAO)
定義した頂点データは、現在CPU側のメモリ(RAM)上にあります。これを高速な描画処理のためにGPU側のメモリ(VRAM)に転送する必要があります。この転送に使われるのがVBO (Vertex Buffer Object) です。
さらに、このVBOをどのように解釈するか(データが頂点位置なのか、色なのか等)という設定と、使用するVBO自体をセットで管理するためにVAO (Vertex Array Object) を使います。描画のたびにVBOや属性の設定をやり直すのは非効率なため、VAOに設定を「記録」しておき、描画時にはVAOをバインドするだけで済ませるのがモダンOpenGLの作法です。
【図解4: VAOとVBOの関係】
[ VAO (Vertex Array Object) ] "設定のレシピ"
|
|-- (参照) --> [ VBO (Vertex Buffer Object) ] "頂点データの塊"
|
`-- (設定) --> [ 頂点属性ポインタ ]
- VBOのどの部分が位置データか?
- データ型は?
- データ間の間隔は?
では、実際にVBOとVAOを生成し、設定するコードを追加しましょう。main
関数内のGLADの初期化後、レンダリングループの前に追加します。
“`cpp
// … gladLoadGLLoader(…) の後、whileループの前 …
// 1. VAOとVBOの生成
unsigned int VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 2. VAOをバインド (これ以降のVBOや属性設定は、このVAOに記録される)
glBindVertexArray(VAO);
// 3. VBOをバインドし、頂点データをGPUにコピー
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 4. 頂点属性ポインタの設定
// – 属性のロケーション (シェーダー側で layout(location=0) と対応)
// – 属性のサイズ (vec3なので3)
// – データの型 (float)
// – 正規化するかどうか (しない)
// – ストライド (次の頂点データまでのバイト数)
// – オフセット (データ開始位置からのオフセット)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // ロケーション0の頂点属性を有効化
// 5. 設定が終わったので、一旦VBOとVAOのバインドを解除してもOK
// (ただし、この簡単な例では必須ではない)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
“`
3-3. シェーダーの作成とコンパイル (GLSL)
次に、GPU上で実行される頂点シェーダーとフラグメントシェーダーをGLSLで記述します。通常は別のファイル (.vert
, .frag
) に書きますが、ここでは簡単のためにC++の文字列リテラルとしてコード内に直接記述します。
main.cpp
の main
関数の前に、2つのシェーダーのソースコードを定義します。
“`cpp
// … includeディレクティブの後 …
const char *vertexShaderSource = “#version 330 core\n”
“layout (location = 0) in vec3 aPos;\n”
“void main()\n”
“{\n”
” gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n”
“}\0”;
const char *fragmentShaderSource = “#version 330 core\n”
“out vec4 FragColor;\n”
“void main()\n”
“{\n”
” FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n” // オレンジ色
“}\n\0”;
“`
- 頂点シェーダー:
layout (location = 0)
で、先ほどglVertexAttribPointer
で設定したロケーション0の頂点属性を受け取ります。型はvec3
(3つのfloat)です。受け取ったaPos
をgl_Position
という組み込みの出力変数にセットしています。vec4
に変換しているのは、gl_Position
が4次元ベクトル(x, y, z, w)を要求するためです。 - フラグメントシェーダー:
out vec4 FragColor
で、出力する色を定義します。main
関数内で、このFragColor
にRGBA値を設定しています。vec4(1.0, 0.5, 0.2, 1.0)
は、R=1.0, G=0.5, B=0.2, A=1.0、つまり不透明なオレンジ色を意味します。
次に、この文字列をコンパイルし、リンクしてGPUで実行可能なシェーダープログラムを作成します。この処理も、VAO/VBOの設定の後、レンダリングループの前に行います。
“`cpp
// … VAO/VBO設定の後、whileループの前 …
// — シェーダープログラムのビルド —
// 頂点シェーダー
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// コンパイルエラーのチェック (省略)
// フラグメントシェーダー
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// コンパイルエラーのチェック (省略)
// シェーダープログラム
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// リンクエラーのチェック (省略)
// リンク後は個々のシェーダーオブジェクトは不要になるので削除
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
``
glGetShaderiv
※エラーチェックのコードは長くなるため省略していますが、実際の開発では必ず実装してください。や
glGetProgramivでステータスを確認し、
glGetShaderInfoLogや
glGetProgramInfoLog` でエラーメッセージを取得できます。
3-4. 描画ループで三角形を描く
準備はすべて整いました。最後に、メインのレンダリングループの中で、作成したシェーダープログラムとVAOを使って描画命令を実行します。
while
ループの中身を以下のように変更します。
“`cpp
// レンダリングループ (メインループ)
while (!glfwWindowShouldClose(window))
{
// 入力
processInput(window);
// レンダリングコマンド
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// --- ここからが描画処理 ---
// 1. 使用するシェーダープログラムを指定
glUseProgram(shaderProgram);
// 2. 使用するVAOを指定 (これにより関連するVBOと属性設定が有効になる)
glBindVertexArray(VAO);
// 3. 描画!
// - プリミティブのタイプ (三角形)
// - 頂点配列の開始インデックス
// - 描画する頂点の数
glDrawArrays(GL_TRIANGLES, 0, 3);
// -------------------------
// バッファの交換とイベント処理
glfwSwapBuffers(window);
glfwPollEvents();
}
``
glUseProgram
描画の命令はたった3行です。
1.: これからこのシェーダーを使います、と宣言します。
glBindVertexArray
2.: これからこの頂点データセット(と設定)を使います、と宣言します。
glDrawArrays
3.: VAOにバインドされている頂点データを使って、三角形(
GL_TRIANGLES`)を、0番目の頂点から3つ分描画しなさい、という命令です。
プログラムの最後、glfwTerminate()
の前に、作成したリソースを解放するコードも忘れずに追加しましょう。
“`cpp
// … whileループの後 …
// オプション: リソースの解放
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// GLFW: リソースのクリーンアップ
glfwTerminate();
return 0;
“`
3-5. 完成!実行してみよう
すべてのコードを統合し、再度ビルドして実行してみましょう (cmake --build build
)。
成功すれば、深緑色の背景に、鮮やかなオレンジ色の三角形が表示されるはずです!
これが、OpenGLプログラミングにおける記念すべき第一歩です。あなたは今、CPUからGPUにデータを送り、自作のシェーダーでGPUをプログラミングし、画面に図形を描画することに成功したのです。
3-6. (応用) インデックスバッファ (EBO) を使った描画
三角形1つでは頂点の重複はありませんでしたが、例えば四角形を描画する場合、2つの三角形で構成するため、頂点が重複してしまいます。
頂点0 --- 頂点1
| / |
| / |
頂点3 --- 頂点2
この四角形は (0, 1, 3) と (1, 2, 3) の2つの三角形からなります。頂点1と3が2回使われており、データに無駄があります。モデルが複雑になればなるほど、この無駄は大きくなります。
この問題を解決するのが EBO (Element Buffer Object)、または IBO (Index Buffer Object) です。これは、描画する頂点の「順序」(インデックス)を格納するためのバッファです。
- 重複しないユニークな頂点リストをVBOに格納します。
- EBOに、これらの頂点をどの順番で結んで三角形を作るかのインデックスリストを格納します。
四角形を描画するようにプログラムを修正してみましょう。
頂点とインデックスの定義:
cpp
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f // 左上
};
unsigned int indices[] = {
0, 1, 3, // 最初の三角形
1, 2, 3 // 2番目の三角形
};
EBOの生成と設定: VAO/VBOの設定部分にEBOを追加します。EBOはVAOにバインドされることに注意してください。
“`cpp
// …
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO); // EBOを生成
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// EBOをバインドし、インデックスデータをGPUにコピー
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// …
“`
描画コマンドの変更: 描画ループ内の glDrawArrays
を glDrawElements
に変更します。
cpp
// glDrawArrays(GL_TRIANGLES, 0, 3); // これをコメントアウトまたは削除
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// - プリミティブのタイプ
// - 描画するインデックスの数 (6個)
// - インデックスのデータ型 (unsigned int)
// - オフセット (0)
リソース解放部分に glDeleteBuffers(1, &EBO);
を追加することも忘れないでください。
これを実行すると、今度はオレンジ色の四角形が表示されるはずです。
第4章: 次のステップへ
おめでとうございます!あなたはOpenGLの基本的なワークフローをマスターしました。
- GLFWでウィンドウとOpenGLコンテキストを作成し、
- GLADでOpenGL関数をロードし、
- 頂点データを定義し、VBOとEBOでGPUに転送し、
- VAOで頂点の状態を管理し、
- GLSLで頂点シェーダーとフラグメントシェーダーを書き、
- それらをシェーダープログラムにリンクし、
- 最後に描画ループで
glDrawElements
を呼び出して図形を描画する。
この一連の流れは、どんなに複雑な3Dアプリケーションでも基本となるものです。
ここから先、あなたの前には広大な3Dグラフィックスの世界が広がっています。次に学ぶべき魅力的なトピックをいくつか紹介します。
- ユニフォーム変数 (Uniforms): シェーダーにCPU側から動的にデータを送る仕組みです。これを使えば、描画ループ内で色を変化させたり、オブジェクトをアニメーションさせたりできます。
- テクスチャ (Textures): モデルの表面に画像を貼り付ける技術です。これにより、オブジェクトにディテールとリアリティを与えることができます。
- 座標変換 (Coordinate Systems): 第1章で触れたモデル・ビュー・プロジェクション行列を実際に計算し、シェーダーに渡すことで、オブジェクトを3D空間内で自由に動かしたり、回転させたり、カメラを操作したりできるようになります。
- ライティング (Lighting): フォンシェーディングなどの照明モデルを学ぶことで、光源をシミュレートし、オブジェクトにリアルな陰影を付けることができます。
- モデルの読み込み (Model Loading):
Assimp
などのライブラリを使って、Blenderなどの3Dモデリングソフトで作成された.obj
や.fbx
ファイルを読み込み、自作のプログラムで表示します。
これらの学習を進める上で、LearnOpenGL.com (英語ですが、非常に質が高く、日本語翻訳版も存在します) は最高のオンラインリソースとなるでしょう。
まとめ
この記事では、OpenGLの基本概念から環境構築、そして最初の三角形と四角形の描画までを、図解を交えながら詳細に解説しました。最初は覚えることが多くて大変に感じるかもしれませんが、一つ一つの概念は論理的につながっています。何度もコードを書き、動かし、少しずつ変更を加えてみることで、その仕組みは自然と身についていきます。
OpenGLは、あなたの創造性を表現するための強力なキャンバスです。今日学んだことは、そのキャンバスに絵を描くための、最初の、しかし最も重要な一筆です。この知識を土台として、ぜひあなただけの3Dの世界を創り出してみてください。あなたのグラフィックスプログラミングの旅が、実り多いものになることを心から願っています。