あなたの最初の三角形
この章では、最初の三角形を画面にレンダリングし、プログラム可能なグラフィックス パイプラインの基礎を紹介します。しかし、その前に、まず座標系の基礎について説明します。後続の章で扱うテクニックとトピックをサポートするために、いくつかの基本的な数学的概念を簡単な方法で紹介しようとしています。読みやすさのために正確さを犠牲にする可能性のある単純化を想定しています。
アプリケーションを起動したときに、作成したプロジェクトとビルドパスの関係で、下のようなエラーが出るかもしれません。
Exception in thread "main" java.lang.RuntimeException: Error reading file [resources/shaders/scene.vert]
そんな時は、参照するパス指定が間違っているので、それを修正します。
自分の場合は、プロジェクトの構成が、lwjglbook/chapter-03/resources/...となっていたので、
「resources/shaders/scene.vert」の部分を「chapter-03/resources/shaders/scene.vert」に修正しました。
実行結果は、赤い三角形が表示されました。
では、これを実現するためにどのような実装を行っているのかを見ていきましょう。
座標についての簡単な説明
座標を指定することで、空間内のオブジェクトを見つけます。地図を考えてみてください。緯度または経度を指定して、地図上のポイントを指定します。数字のペアだけで、ポイントが正確に識別されます。その数値のペアはポイント座標です (マップは完全ではない楕円体である地球の投影であるため、実際にはもう少し複雑です。そのため、より多くのデータが必要ですが、それは良い例えです)。
2次元座標、(X,Y)
座標系は、1 つまたは複数の数値、つまり 1 つまたは複数のコンポーネントを使用して点の位置を一意に指定するシステムです。さまざまな座標系 (デカルト、極座標など) があり、座標をある座標系から別の座標系に変換できます。デカルト座標系を使用します。
デカルト座標系では、2 次元の座標は、2 つの直交軸 x と y までの符号付き距離を測定する 2 つの数値によって定義されます。
まずは、座標系の理解が必要らしいです。ポイントとしては、次の一言に尽きます。
数字のペアだけで、ポイントが正確に識別されます
地図の類推を続けると、座標系は原点を定義します。地理座標の場合、原点は赤道とゼロ子午線が交差するポイントに設定されます。原点をどこに設定するかによって、特定の点の座標が異なります。座標系は、軸の向きを定義することもできます。前の図では、右に移動すると x 座標が増加し、上に移動すると y 座標が増加します。しかし、別の座標を取得する別の軸方向を持つ別のデカルト座標系を定義することもできます。
なので、座標系の理解をします。単純に点(Vertex)を指定するのに2二次元であれば、下図のように
ご覧のとおり、座標を構成する数値のペアに適切な意味を与えるために、原点や軸方向などの任意のパラメーターを定義する必要があります。座標空間として、任意のパラメーターのセットを持つその座標系を参照します。一連の座標を操作するには、同じ座標空間を使用する必要があります。良いニュースは、平行移動と回転を実行するだけで、座標をある空間から別の空間に変換できることです。
3次元座標、(X,Y,Z)
3D 座標を扱う場合は、追加の軸である z 軸が必要です。3D 座標は、3 つの数値 (x、y、z) のセットによって形成されます。
3次元であれば、下図のようになります。
- 2次元の場合:x, yの2軸を使って座標を表現 (0, 1)
- 3次元の場合:x, y, zの3軸を使って座標を表現 (0, 1, 2)
そして、3次元の座標の取り方は2種類あります。
3D 座標は、左手と右手の 2 種類に分類できます。それがどのタイプかどうやってわかりますか?手を取り、親指と人差し指の間で「L」の字を作り、中指は他の 2 本と垂直な方向を向くようにします。親指は x 軸が増加する方向を指し、人差し指は y 軸が増加する方向を指し、中指は z 軸が増加する方向を指す必要があります。左手でそれができる場合は左利き、右手を使う必要がある場合は右利きです。
右手 |
左手 |
|
|
回転を適用する場合
2D 座標空間は、回転を適用することである空間から別の空間に変換できるため、すべて同等です。反対に、3D 座標空間はすべて等しいわけではありません。両方が同じ利き手である場合、つまり、両方が左利きまたは右利きである場合にのみ、回転を適用して一方から他方に変換できます。
いくつかの基本的なトピックを定義したので、3D グラフィックスを扱う際に一般的に使用される用語について説明しましょう。後の章で 3D モデルをレンダリングする方法を説明すると、異なる 3D 座標空間を使用することがわかります。これは、これらの座標空間のそれぞれにコンテキスト、目的があるためです。座標のセットは、何かを参照しない限り意味がありません。この座標 (40.438031, -3.676626) を調べると、彼らはあなたに何かを言うかもしれません。しかし、それらが幾何学的座標 (緯度と経度) であると言うと、マドリッドのある場所の座標であることがわかります。
3Dシーンについて
3D オブジェクトをロードすると、3D 座標のセットが取得されます。これらの座標は、オブジェクト座標空間と呼ばれる 3 次元座標空間で表されます。グラフィック デザイナーがこれらの 3D モデルを作成するとき、このモデルが表示される 3D シーンについて何も知らないため、モデルにのみ関連する座標空間を使用して座標を定義することしかできません。
ワールド座標について
3D シーンを描画する場合、すべての 3D オブジェクトは、いわゆるワールド空間座標空間に対して相対的になります。3D オブジェクト空間からワールド空間座標に変換する必要があります。一部のオブジェクトは、3D シーンで適切に表示するために、回転、引き伸ばし、または拡大および移動する必要があります。
また、表示される 3D 空間の範囲を制限する必要があります。これは、3D 空間でカメラを動かすようなものです。次に、ワールド空間座標をカメラまたはビュー空間座標に変換する必要があります。最後に、これらの座標を 2D の画面座標に変換する必要があるため、3D ビュー座標を 2D 画面座標空間に投影する必要があります。
次の図は、OpenGL 座標 (z 軸は画面に対して垂直) を示しており、座標は -1 から +1 の間です。
あなたの最初の三角形
これで、OpenGL を使用してシーンをレンダリングする際に行われるプロセスの学習を開始できます。古いバージョンの OpenGL、つまり固定機能のパイプラインに慣れている場合は、なぜそれほど複雑にする必要があるのか疑問に思ってこの章を終了するかもしれません。画面に単純な図形を描画するのに、それほど多くの概念やコード行は必要ないと考えるかもしれません。そう思っているあなたにアドバイスをさせてください。実際には、よりシンプルではるかに柔軟です。チャンスを与えるだけです。最新の OpenGL を使用すると、一度に 1 つの問題について考えることができ、コードとプロセスをより論理的な方法で整理できます。
グラフィックス パイプライン
最終的に 3D 表現を 2D 画面に描画する一連の手順は、グラフィックス パイプラインと呼ばれます。OpenGL の最初のバージョンでは、固定機能パイプラインと呼ばれるモデルが採用されていました。このモデルは、固定された一連の操作を定義するレンダリング プロセスの一連のステップを採用しました。プログラマーは、各ステップで使用できる関数のセットに制約され、いくつかのパラメーターを設定して微調整することができました。したがって、適用できる効果と操作は API 自体によって制限されていました (たとえば、「フォグの設定」または「ライトの追加」ですが、これらの機能の実装は固定されており、変更できませんでした)。
グラフィックス パイプラインは、次の手順で構成されています。
image |
text |
|
頂点と点のリスト |
変換とライティング |
プリミティブの組み立て |
ラスタライズ |
|
※ラスタライズ :ベクトル画像をビットマップ画像へと変換すること
※テクスチャリング:テクスチャを壁紙のように貼り付けること
OpenGL 2.0 では、プログラム可能なパイプラインの概念が導入されました。このモデルでは、シェーダーと呼ばれる一連の特定のプログラムを使用して、グラフィックス パイプラインを構成するさまざまなステップを制御またはプログラムできます。次の図は、OpenGL プログラマブル パイプラインの簡略化されたバージョンを示しています。
レンダリング
レンダリングは、頂点バッファーの形式で頂点のリストを入力として取り始めます。しかし、頂点とは何ですか?頂点は、シーンをレンダリングするための入力として使用できる任意のデータ構造です。
頂点(Vertex)
具体的には、(X,Y) (X,Y,Z)の座標のこと。英語では「Vertex」
ここまでで、2D または 3D 空間の点を表す構造として考えることができます。また、3D 空間内の点をどのように記述しますか? x、y、z 座標を指定する。そして、頂点バッファとは何ですか? 頂点バッファーは、頂点配列を使用してレンダリングする必要があるすべての頂点をパックし、その情報をグラフィックス パイプラインのシェーダーで利用できるようにするもう 1 つのデータ構造です。
これらの頂点は、スクリーン スペースへの各頂点の投影位置を計算することを主な目的とする頂点シェーダーによって処理されます。このシェーダは、色やテクスチャに関連する他の出力も生成できますが、その主な目的は頂点をスクリーン スペースに投影すること、つまりドットを生成することです。
ジオメトリ処理
※ジオメトリ:描画対象の形状や、形状を定義づける頂点の座標や線分
ジオメトリ処理段階では、頂点シェーダーによって変換された頂点を接続して三角形を形成します。これは、頂点が格納された順序を考慮し、異なるモデルを使用してそれらをグループ化することによって行われます。なぜ三角形?三角形は、グラフィック カードの基本的な作業単位のようなものです。これは、複雑な 3D シーンを構築するために組み合わせて変換できる単純な幾何学的形状です。このステージでは、特定のシェーダーを使用して頂点をグループ化することもできます。
<シェーダープログラム(scene.flag)>
#version 330
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
ラスター化(ラスタライズ)
ラスター化段階では、前の段階で生成された三角形を取得し、それらをクリップして、ピクセル サイズのフラグメントに変換します。これらのフラグメントは、フラグメント シェーダーによるフラグメント処理段階で使用され、フレームバッファーに書き込まれる最終的な色を割り当てるピクセルを生成します。フレームバッファは、グラフィックス パイプラインの最終結果です。画面に描画する必要がある各ピクセルの値を保持します。
3D カードは、上記のすべての操作を並列化するように設計されていることに注意してください。入力データは、最終的なシーンを生成するために並行して処理されます。
それでは、最初のシェーダー プログラムを書き始めましょう。シェーダーは、ANSI C に基づいた GLSL 言語 (OpenGL Shading Language) を使用して記述されます。まず、次の内容で、ディレクトリのscene.vert下に「 」(拡張子は Vertex Shader の場合)という名前のファイルを作成します。resources\shaders
頂点シェーダー
<シェーダ・プログラム(scene.vert)>
#version 330
layout (location=0) in vec3 inPosition;
void main()
{
gl_Position = vec4(inPosition, 3.0);
}
1行目
最初の行は、使用している GLSL 言語のバージョンを示すディレクティブです。次の表は、GLSL バージョン、そのバージョンに一致する OpenGL、および使用するディレクティブを関連付けています (Wikipedia: https://en.wikipedia.org/wiki/OpenGL_Shading_Language#Versions )。
GLS バージョン |
OpenGL バージョン |
シェーダープリプロセッサー |
1.10.59 |
2.0 |
#バージョン 110 |
1.20.8 |
2.1 |
#バージョン 120 |
1.30.10 |
3.0 |
#バージョン 130 |
1.40.08 |
3.1 |
#バージョン 140 |
1.50.11 |
3.2 |
#バージョン150 |
3.30.6 |
3.3 |
#バージョン 330 |
4.00.9 |
4.0 |
#バージョン 400 |
4.10.6 |
4.1 |
#バージョン 410 |
4.20.11 |
4.2 |
#バージョン 420 |
4.30.8 |
4.3 |
#バージョン 430 |
4.40 |
4.4 |
#バージョン 440 |
4.50 |
4.5 |
#バージョン 450 |
2行目
2 行目は、このシェーダーの入力形式を指定します。OpenGL バッファー内のデータは、私たちが望むものであれば何でもかまいません。つまり、言語は、事前定義されたセマンティックを持つ特定のデータ構造を渡すことを強制しません。シェーダーの観点からは、データを含むバッファーを受け取ることを期待しています。それは、位置、追加情報を含む位置、またはその他必要なものです。この例では、頂点シェーダーの観点からは、float の配列を受け取っているだけです。バッファーを埋めるとき、シェーダーによって処理されるバッファー チャンクを定義します。
mainの中
vec3: 3 つの属性で構成されるベクトル
vec4: 4 つの属性のベクトル
そのため、まずそのチャンクを意味のあるものに変換する必要があります。この場合、位置 0 から開始して、3 つの属性 (x、y、z) で構成されるベクトルを受け取ることを期待していると言っています。
シェーダーには、他の C プログラムと同様に、この場合は非常に単純なメイン ブロックがあります。gl_Position変換を適用せずに、受け取った位置を出力変数に返すだけです。なぜ 3 つの属性のベクトルが 4 つの属性のベクトル (vec4) に変換されたのか疑問に思われるかもしれません。それの訳はgl_Position同次座標を使用しているため、結果は vec4 形式であると予想されます。つまり、(x, y, z, w) の形式で何かを期待しています。ここで、w は余分な次元を表します。別の次元を追加する理由 後の章では、実行する必要がある操作のほとんどがベクトルと行列に基づいていることがわかります。その余分な次元がない場合、これらの操作の一部を組み合わせることができません。たとえば、回転操作と平行移動操作を組み合わせることができませんでした。(これについて詳しく知りたい場合は、この余分な次元により、アフィン変換と線形変換を組み合わせることができます。これについては、Fletcher Dunn と Ian Parberry による優れた本「グラフィックスとゲーム開発のための 3D 数学入門」を読むことで学習できます。 )。
フラグメントシェーダー
最初のフラグメント シェーダーを見てみましょう。scene.fragresources ディレクトリの下に、次の内容の「 」(拡張子は Fragment Shader の拡張子) という名前のファイルを作成します。
#version 330
out vec4 fragColor;
void main()
{
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
構造は頂点シェーダーとよく似ています。この場合、各フラグメントに固定色を設定します。出力変数は 2 行目に定義され、vec4 fragColor として設定されます。
シェーダーを使うクラスの作成
シェーダーを作成したので、それらをどのように使用しますか? という名前の新しいクラスを作成する必要があります。このクラスはShaderProgram基本的に、さまざまなシェーダー モジュール (頂点、フラグメント) のソース コードを受け取り、それらをコンパイルしてリンクし、シェーダー プログラムを生成します。これは、従う必要がある一連の手順です。
一連の手順
- OpenGL プログラムを作成します。
- シェーダー プログラム モジュール (頂点シェーダーまたはフラグメント シェーダー) を読み込みます。
- シェーダーごとに、新しいシェーダー モジュールを作成し、そのタイプ (頂点、フラグメント) を指定します。
- シェーダーをコンパイルします。
- シェーダーをプログラムにアタッチします。
- プログラムをリンクします。
最後に、シェーダー プログラムが GPU に読み込まれ、プログラム識別子という識別子を参照して使用できます。
ShaderProgramクラス
package org.lwjglb.engine.graph;
import org.lwjgl.opengl.GL30;
import org.lwjglb.engine.Utils;
import java.util.*;
import static org.lwjgl.opengl.GL30.*;
public class ShaderProgram {
private final int programId;
public ShaderProgram(List<ShaderModuleData> shaderModuleDataList) {
programId = glCreateProgram();
if (programId == 0) {
throw new RuntimeException("Could not create Shader");
}
List<Integer> shaderModules = new ArrayList<>();
shaderModuleDataList.forEach(s -> shaderModules.add(createShader(Utils.readFile(s.shaderFile), s.shaderType)));
link(shaderModules);
}
public void bind() {
glUseProgram(programId);
}
public void cleanup() {
unbind();
if (programId != 0) {
glDeleteProgram(programId);
}
}
protected int createShader(String shaderCode, int shaderType) {
int shaderId = glCreateShader(shaderType);
if (shaderId == 0) {
throw new RuntimeException("Error creating shader. Type: " + shaderType);
}
glShaderSource(shaderId, shaderCode);
glCompileShader(shaderId);
if (glGetShaderi(shaderId, GL_COMPILE_STATUS) == 0) {
throw new RuntimeException("Error compiling Shader code: " + glGetShaderInfoLog(shaderId, 1024));
}
glAttachShader(programId, shaderId);
return shaderId;
}
public int getProgramId() {
return programId;
}
private void link(List<Integer> shaderModules) {
glLinkProgram(programId);
if (glGetProgrami(programId, GL_LINK_STATUS) == 0) {
throw new RuntimeException("Error linking Shader code: " + glGetProgramInfoLog(programId, 1024));
}
shaderModules.forEach(s -> glDetachShader(programId, s));
shaderModules.forEach(GL30::glDeleteShader);
}
public void unbind() {
glUseProgram(0);
}
public void validate() {
glValidateProgram(programId);
if (glGetProgrami(programId, GL_VALIDATE_STATUS) == 0) {
throw new RuntimeException("Error validating Shader code: " + glGetProgramInfoLog(programId, 1024));
}
}
public record ShaderModuleData(String shaderFile, int shaderType) {
}
}
のコンストラクターは、シェーダー モジュール typ (頂点、フラグメントなど) を定義するインスタンスShaderProgramのリストと、シェーダー モジュール コードを含むソース ファイルへのパスを受け取ります。ShaderModuleDataコンストラクターは、(メソッドを呼び出して) 各シェーダー モジュールを最初にコンパイルしcreateShader、最後に (メソッドを呼び出して) すべてをリンクすることにより、新しい OpenGL シェーダー プログラムを作成することから始めますlink。シェーダ プログラムがリンクされると、コンパイルされた頂点シェーダとフラグメント シェーダを解放できます( を呼び出すことによりglDetachShader)。
メソッドはvalidate、基本的にglValidateProgram関数を呼び出します。この関数は主にデバッグ目的で使用され、ゲームが本番段階に達したときに使用しないでください。このメソッドは、現在の OpenGL の状態でシェーダーが正しいかどうかを検証しようとします。これは、シェーダーが正しくても、現在の状態がシェーダーを実行するのに十分ではないという事実 (一部のデータがまだアップロードされていない可能性がある) が原因で、場合によっては検証が失敗する可能性があることを意味します。必要なすべての入力データと出力データが適切にバインドされたときに呼び出す必要があります (描画呼び出しを実行する直前がよいでしょう)。
ShaderProgramまた、このプログラムをレンダリングに使用するメソッド、つまりバインド、バインドを解除する別のメソッド (使用しない場合)、および最後に、リソースが不要になったときにすべてのリソースを解放するクリーンアップ メソッドも提供します。
?
という名前のユーティリティ クラスを作成しますUtils。この場合、ファイルを にロードする public メソッドを定義しますString。
Utilsクラス(ファイル読み込み)
package org.lwjglb.engine;
import java.io.IOException;
import java.nio.file.*;
public class Utils {
private Utils() {
// Utility class
}
public static String readFile(String filePath) {
String str;
try {
str = new String(Files.readAllBytes(Paths.get(filePath)));
} catch (IOException excp) {
throw new RuntimeException("Error reading file [" + filePath + "]", excp);
}
return str;
}
}
Sceneクラス
Sceneモデル、ライトなどの 3D シーンの値を保持するという名前の新しいクラスも必要になります。今では、描画したいモデルのメッシュ (頂点のセット) を引き裂くだけです。これは、そのクラスのソース コードです。
package org.lwjglb.engine.scene;
import org.lwjglb.engine.graph.Mesh;
import java.util.*;
public class Scene {
private Map<String, Mesh> meshMap;
public Scene() {
meshMap = new HashMap<>();
}
public void addMesh(String meshId, Mesh mesh) {
meshMap.put(meshId, mesh);
}
public void cleanup() {
meshMap.values().stream().forEach(Mesh::cleanup);
}
public Map<String, Mesh> getMeshMap() {
return meshMap;
}
}
Meshクラスについて
ご覧のとおりMesh、後で描画に使用されるマップにインスタンスを格納するだけです。しかし、とは何Meshですか?これは基本的に、頂点データを GPU にロードしてレンダリングに使用できるようにする方法です。クラスの詳細を説明する前に、Meshクラスでどのように使用できるかを見てみましょうMain。
Meshクラスの使い方(呼び出し方)
public class Main implements IAppLogic {
public static void main(String[] args) {
Main main = new Main();
Engine gameEng = new Engine("chapter-03", new Window.WindowOptions(), main);
gameEng.start();
}
...
@Override
public void init(Window window, Scene scene, Render render) {
float[] positions = new float[]{
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
Mesh mesh = new Mesh(positions, 3);
scene.addMesh("triangle", mesh);
}
...
}
このinitメソッドでは、三角形の頂点の座標を含む float の配列を定義します。ご覧のとおり、その配列には構造がなく、そこにすべての座標をダンプするだけです。現状では、OpenGL は構造を認識できません。そのデータの。それは単なるフロートのシーケンスです。次の図は、座標系の三角形を示しています。
Meshクラス
そのデータの構造を定義して GPU にロードするMeshクラスは、次のように定義されるクラスです。
package org.lwjglb.engine.graph;
import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryStack;
import java.nio.FloatBuffer;
import java.util.*;
import static org.lwjgl.opengl.GL30.*;
public class Mesh {
private int numVertices;
private int vaoId;
private List<Integer> vboIdList;
public Mesh(float[] positions, int numVertices) {
try (MemoryStack stack = MemoryStack.stackPush()) {
this.numVertices = numVertices;
vboIdList = new ArrayList<>();
vaoId = glGenVertexArrays();
glBindVertexArray(vaoId);
// Positions VBO
int vboId = glGenBuffers();
vboIdList.add(vboId);
FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
positionsBuffer.put(0, positions);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
}
public void cleanup() {
vboIdList.stream().forEach(GL30::glDeleteBuffers);
glDeleteVertexArrays(vaoId);
}
public int getNumVertices() {
return numVertices;
}
public final int getVaoId() {
return vaoId;
}
}
VAOとVBO
ここでは、頂点配列オブジェクト (VAO) と頂点バッファー オブジェクト (VBO) という 2 つの重要な概念を紹介します。上記のコードで迷子になった場合は、最後に、描画したいオブジェクトをモデル化するデータをグラフィックス カード メモリに送信していることを思い出してください。保存すると、後で描画中に参照するための識別子が取得されます。
VBO(頂点バッファ オブジェクト)とは
まず頂点バッファ オブジェクト (VBO) から始めましょう。VBO は、頂点を格納するグラフィックス カード メモリに格納される単なるメモリ バッファです。これは、三角形をモデル化するフロートの配列を転送する場所です。前に述べたように、OpenGL はデータ構造について何も知りません。実際、座標だけでなく、テクスチャや色などの他の情報も保持できます。頂点配列オブジェクト (VAO) は、通常属性リストと呼ばれる 1 つ以上の VBO を含むオブジェクトです。各アトリビュート リストには、位置、色、テクスチャなどの 1 種類のデータを保持できます。各スロットには、必要なデータを自由に格納できます。
VAO(頂点配列オブジェクト)とは
VAO は、グラフィックス カードに格納されるデータの一連の定義をグループ化するラッパーのようなものです。VAO を作成すると、識別子が取得されます。その識別子を使用して、作成時に指定した定義を使用して、それとそれに含まれる要素をレンダリングします。
Meshクラスのコンストラクタ処理
それでは、上記のコードを確認してみましょう。最初に、VAO を作成し (glGenVertexArrays関数を呼び出して)、それをバインドします (glBindVertexArray関数を呼び出して)。その後、( を呼び出してglGenBuffers) VBO を作成し、データをそこに入れる必要があります。そのために、float の配列を に格納しますFloatBuffer。これは主に、C ベースの OpenGL ライブラリとインターフェイスする必要があるためです。そのため、float の配列をライブラリで管理できるものに変換する必要があります。
まとめると次のようになります。
- glGenVertexArrays関数を呼び出してVAOの作成
- glBindVertexArray関数でVAOのバインド
- VBOの生成
- VBOのIDをリストに追加
...
private int vaoId;
...
public Mesh(float[] positions, int numVertices) {
...
// VAOの生成
vaoId = glGenVertexArrays();
// VAOのバインド
glBindVertexArray(vaoId);
// VBOの生成
int vboId = glGenBuffers();
// VBOをリストに格納
vboIdList.add(vboId);
...
}
...
このクラスを使用してMemoryUtilオフヒープ メモリにバッファを作成し、OpenGL ライブラリからアクセスできるようにします。(put メソッドを使用して) データを保存した後、flip メソッドを使用してバッファーの位置を 0 の位置にリセットする必要があります (つまり、書き込みが終了したと言います)。Java オブジェクトは、ヒープと呼ばれる領域に割り当てられることに注意してください。ヒープは、JVM のプロセス メモリに確保された大量のメモリです。ヒープに格納されたメモリには、ネイティブ コードからアクセスできません (JNI、Java からネイティブ コードを呼び出すことを可能にするメカニズムでは、アクセスできません)。Java とネイティブ コードの間でメモリ データを共有する唯一の方法は、Java でメモリを直接割り当てることです。
以前のバージョンの LWJGL を使用している場合は、いくつかのトピックを強調することが重要です。BufferUtilsバッファーの作成にユーティリティ クラスを使用していないことに気付いたかもしれません。代わりにMemoryUtilクラスを使用します。これは、BufferUtilsがあまり効率的ではなく、下位互換性のためにのみ維持されているためです。代わりに、LWJGL 3 はバッファ管理の 2 つの方法を提案しています。
・自動管理バッファー、つまり、ガベージ コレクターによって自動的に収集されるバッファー。これらのバッファーは、主に短時間の操作、または GPU に転送され、プロセス メモリに存在する必要のないデータに使用されます。これは、org.lwjgl.system.MemoryStackクラスを使用して実現されます。
・手動で管理されるバッファー。この場合、終了したら慎重に解放する必要があります。これらのバッファは、長時間の操作または大量のデータを対象としています。これは、MemoryUtilクラスを使用して実現されます。
詳細については、 を参照して ください。
その後、( を呼び出して) VBO をバインドし、(関数glBindBufferを呼び出して) データ int ut をロードします。glBufferData次に、最も重要な部分です。データの構造を定義し、VAO の属性リストの 1 つに格納する必要があります。これは、次の行で行われます。
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
パラメータは次のとおりです。
・index: シェーダーがこのデータを予期する場所を指定します。
・size: 頂点属性ごとのコンポーネント数を指定します (1 から 4)。この場合、3D 座標を渡しているので、3 である必要があります。
・type: 配列内の各コンポーネントのタイプ (この場合は float) を指定します。
・normalized: 値を正規化するかどうかを指定します。
・stride: 連続する汎用頂点属性間のバイト オフセットを指定します。(後で説明します)。
・offset: バッファ内の最初のコンポーネントへのオフセットを指定します。
VBO と VAO が終了したら、バインドを解除できるようにします (0 にバインドします)。
glBindVertexArray(0);
自動管理バッファを使用しているため、try/catchブロックが終了すると、バッファは自動的にクリーンアップされます。
Meshクラスの概要
このMeshクラスは、cleanup基本的に VAO と VBO を解放するメソッドと、メッシュの頂点数と VAO の ID を取得するいくつかのゲッター メソッドによって完成されます。この要素をレンダリングするとき、描画操作を使用するときに VAO id を使用します。
それでは、これらすべてを機能させてみましょう。シーン内のすべてのモデルのレンダリングを実行するという名前の新しいクラスを作成し、SceneRender次のように定義します。
package org.lwjglb.engine.graph;
import org.lwjglb.engine.Window;
import org.lwjglb.engine.scene.Scene;
import java.util.*;
import static org.lwjgl.opengl.GL30.*;
public class SceneRender {
private ShaderProgram shaderProgram;
public SceneRender() {
List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/scene.vert", GL_VERTEX_SHADER));
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/scene.frag", GL_FRAGMENT_SHADER));
shaderProgram = new ShaderProgram(shaderModuleDataList);
}
public void cleanup() {
shaderProgram.cleanup();
}
public void render(Scene scene) {
shaderProgram.bind();
scene.getMeshMap().values().forEach(mesh -> {
glBindVertexArray(mesh.getVaoId());
glDrawArrays(GL_TRIANGLES, 0, mesh.getNumVertices());
}
);
glBindVertexArray(0);
shaderProgram.unbind();
}
}
ご覧のとおり、コンストラクターで 2 つのShaderModuleDataインスタンス (1 つは頂点シェーダー用、もう 1 つはフラグメント用) を作成し、シェーダー プログラムを作成します。cleanupリソース (この場合はシェーダー プログラム) を解放するrenderメソッドと、描画を実行するメソッドを定義します。このメソッドは、メソッドを呼び出してシェーダー プログラムを使用することから始まりbindます。Scene次に、インスタンスに保存されているメッシュを繰り返し処理し、 (関数を呼び出して) それらをバインドし、(glBindVertexArray関数を呼び出して) VAO の頂点を描画しglDrawArraysます。最後に、VAO とシェーダー プログラムのバインドを解除して、状態を復元します。
Renderクラスの修正
Render最後に、クラスを使用するようにクラスを更新するだけですSceneRender。
package org.lwjglb.engine.graph;
import org.lwjgl.opengl.GL;
import org.lwjglb.engine.Window;
import org.lwjglb.engine.scene.Scene;
public class Render {
private SceneRender sceneRender;
public Render() {
GL.createCapabilities();
sceneRender = new SceneRender();
}
public void cleanup() {
sceneRender.cleanup();
}
public void render(Window window, Scene scene) {
sceneRender.render(window, scene);
}
}
このrenderメソッドは、フレームバッファをクリアし、(glViewportメソッドを呼び出して) ビューポートをウィンドウのサイズに設定することから始めます。つまり、レンダリング領域をそれらの寸法に設定します (これはフレームごとに行う必要はありませんが、ウィンドウのサイズ変更をサポートしたい場合は、この方法で行い、各フレームの潜在的な変更に適応させることができます)。その後、インスタンスrenderに対してメソッドを呼び出すだけです。SceneRenderそして、それだけです!手順を注意深く実行すると、次のように表示されます。
理解できなかった部分
次の文章が重要そうなのに、理解できませんでした。よく読んで今後理解したいと思います。
しかし、2週目なので補足を追加します。
3D オブジェクトをロードすると、3D 座標のセットが取得されます。
Objファイルを読み込むとき「3D 座標のセットが取得されます。」Objファイルは、以下のようなものです。
これらの座標は、オブジェクト座標空間と呼ばれる 3 次元座標空間で表されます。グラフィック デザイナーがこれらの
3D モデルを作成するとき、このモデルが表示される 3D シーンについて何も知らないため、モデルにのみ関連する座標空間を使用して座標を定義することしかできません。
多分グラフィックデザイナーはシーンについて知る必要がないので、コミュニケーションを取る際には気を付けましょうということだと思います。
そして、各3Dモデルを描画するためには「シーン」というプログラム的な要素、状態があり、それをプログラムでいうところのSceneクラス。
大まかな流れで理解する
- Mainクラスのインスタンス化
- Engineクラスのインスタンス化に伴い以下の設定
-
- ロジッククラス(Mainクラス)のセット
-
- Rennderクラスのセット
-
- シーンクラスのセット
-
- Meshクラスをインスタンス化、シーンクラスにセット
- Engine#start()を起動、Engine#run()を起動する
Engine#run()はゲームループになっている。
三角形の描画(理論編)
ここまでで、描画処理の学習準備が整いました。3Dモデル(シーン)の描画学習を始めます。
その前に、下のような文言があります。
画面に単純な図形を描画するのに、それほど多くの概念やコード行は必要ないと考えるかもしれません。
そう思っているあなたにアドバイスをさせてください。実際には、よりシンプルではるかに柔軟です。チャンスを与えるだけです。
最新の OpenGL を使用すると、一度に 1 つの問題について考えることができ、コードとプロセスをより論理的な方法で整理できます。
これに関して、「今回のプログラム(三角形を描画するプログラム)は単純なのでWindowクラス、Engineクラスなど作成する必要がないのでは?」と疑問に思うかもしれないが
「実際にはよりシンプルではるかに柔軟です。チャンスを与えるだけです。
」ということです。
つまり、各処理の担当クラスを作成し今後そのクラスに処理(例えるならば、担当業務)を追加していくということです。
理論編 グラフィックスパイプライン
次のような処理フローをグラフィックスパイプラインと呼びます。
この処理フローは次のような特徴があります。
固定された一連の操作を定義するレンダリング プロセスの一連のステップを採用しました。
プログラマーは、各ステップで使用できる関数のセットに制約され、いくつかのパラメーターを設定して微調整することができました。
しかし、これは旧バージョンのOpenGLで、下のようなデメリットがありました。
適用できる効果と操作は API 自体によって制限されていました
(たとえば、「フォグの設定」または「ライトの追加」ですが、これらの機能の実装は固定されており、変更できませんでした)。
<旧バージョンの処理フロー>
|
- 点(Vertex)と点のインデックスリストの処理
- 変形とライトの処理
- プリミティブの組み立て
- 抽象度の高い形式で記述された画像データを、コンピュータが最終的に出力することのできる画素の集まり(ビットマップ形式/ラスター形式)に変換
- 3Dモデル表面の質感を表現するための手法で、3Dオブジェクトの表面にテクスチャを壁紙のように貼り付けること
- Frame Bufferに出力
|
<新バージョン処理フロー>
|
- 点(Vertex)と点のインデックスリストの処理:VertexShader処理を追加
- ジオメトリの処理:GeometoryShader処理を追加
- プリミティブの組み立て
- 抽象度の高い形式で記述された画像データを、コンピュータが最終的に出力することのできる画素の集まり(ビットマップ形式/ラスター形式)に変換
- 3Dモデル表面の質感を表現するための手法で、3Dオブジェクトの表面にテクスチャを壁紙のように貼り付けること
- Frame Bufferに出力
|
※
- ジオメトリとは
- ジオメトリとは、幾何学、形状、などの意味を持つ英単語。 コンピュータグラフィックスにおける描画対象の形状や、形状を定義づける頂点の座標や線分、
面などの図形を表す式の係数といったデータの組み合わせを意味することが多い。
- Vertexシェーダとは
- 3DCG では様々なモデルはポリゴン(面)で表示されています。ポリゴンは頂点と線で表現されます。
シェーダはこの頂点情報から面を作り、描画していくわけです。
レンダラが頂点情報を参照するときに呼び出すシェーダを、名前の通り Vertex シェーダ と呼びます。
- Frame Bufferとは
-
フレームバッファとは、コンピュータ内部で一画面分の表示内容を丸ごと記憶しておくことができるメモリ領域やメモリ装置のこと。
画面に何かを描画する際にはまずソフトウェアがフレームバッファの内容を書き換え、
その内容を一定のタイミングでディスプレイなどの表示装置に転送することで画面上に更新が反映される。
これにより、描画処理の過程やその途中の状態が利用者の目に触れることを防ぐことができる。
メインメモリ(RAM)の一部に専用の領域を確保してフレームバッファとして使用する場合と、専用のメモリ装置を使用する場合がある。
ビデオカードなどに内蔵されているビデオメモリ(VRAM:Video RAM)のことをフレームバッファと呼ぶ場合もある。
レンダリング処理
レンダリング処理の順序
- 頂点バッファーの形式で頂点のリストを入力として取り始めます。
- 頂点シェーダ処理
- ジオメトリ処理
- フレームバッファへ出力
3D カードは、上記のすべての操作を並列化するように設計されていることに注意してください。入力データは、最終的なシーンを生成するために並行して処理されます。
単語 |
意味 |
頂点バッファー |
頂点バッファーは、頂点配列を使用してレンダリングする必要があるすべての頂点をパックし、その情報をグラフィックス パイプラインのシェーダーで利用できるようにするデータ構造です |
頂点シェーダー |
スクリーン スペースへの各頂点の投影位置を計算することを主な目的とす
色やテクスチャに関連する他の出力も生成できますが、その主な目的は頂点をスクリーン スペースに投影すること、つまりドットを生成することです。 |
ジオメトリ処理 |
頂点シェーダーによって変換された頂点を接続して三角形を形成します。これは、頂点が格納された順序を考慮し、異なるモデルを使用してそれらをグループ化することによって行われます。 グラフィック カードの基本的な作業単位のようなものです。これは、複雑な 3D シーンを構築するために組み合わせて変換できる単純な幾何学的形状です。このステージでは、特定のシェーダーを使用して頂点をグループ化することもできます。 |
ラスター化(処理) |
ジオメトリ処理段階で生成された三角形を取得し、それらをクリップして、ピクセル サイズのフラグメントに変換します。 これらのフラグメントは、フラグメント シェーダーによるフラグメント処理段階で使用され、フレームバッファーに書き込まれる最終的な色を割り当てるピクセルを生成します。 |
フレームバッファ |
フレームバッファは、グラフィックス パイプラインの最終結果です。画面に描画する必要がある各ピクセルの値を保持します。 |
頂点シェーダーを書く
ドキュメントに下のような記述があります。
それでは、最初のシェーダー プログラムを書き始めましょう。シェーダーは、ANSI C に基づいた GLSL 言語 (OpenGL Shading Language) を使用して記述されます。
まず、次の内容で、ディレクトリのscene.vert下に「 」(拡張子は Vertex Shader の場合)という名前のファイルを作成します。
各行に対して、説明をコメントで追加しました。つまり、このコードは3次元ベクトルを4次元ベクトルに変換しています。
// 使用している GLSL 言語のバージョンを示すディレクティブです。
#version 330
// このシェーダーの入力形式を指定します。
layout (location=0) in vec3 inPosition;
void main()
{
// 4次元ベクトルをgl_Positionにセット
gl_Position = vec4(inPosition, 1.0);
}
フラグメントシェーダーを書く
ドキュメントの説明配下の通り
構造は頂点シェーダーとよく似ています。この場合、各フラグメントに固定色を設定します。出力変数は 2 行目に定義され、vec4 fragColor として設定されます。
// 使用している GLSL 言語のバージョンを示すディレクティブです。
#version 330
// このシェーダーの出力変数
out vec4 fragColor;
void main()
{
// フラグメントに固定色を設定
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
上記コードの場合は、フラグメントの色が固定で赤になっています。
var colors = [
1.0, 1.0, 1.0, 1.0, // 白
1.0, 0.0, 0.0, 1.0, // 赤
0.0, 1.0, 0.0, 1.0, // 緑
0.0, 0.0, 1.0, 1.0 // 青
];
シェーダーのまとめ
頂点シェーダーは入力の型を、フラグメントシェーダーは出力の色を指定している。
作成するクラスについて
今までのChapter-01, Chapter-02では記述していませんでしたが。ここで整理したいと思います。
使用しているクラス一覧
No |
パッケージ名 |
クラス名 |
役割 |
1 |
org.lwhglb.game |
Main |
メインメソッドを起動する |
2 |
org.lwhglb.engine |
Engine |
タイトル, WinoowOption, IAplogicを引数にして、画面タイトル、ウィンドウ設定、APロジックを起動する |
3 |
org.lwhglb.engine |
AppLogic |
画面クリア、初期化、入力、更新)処理を実装するインターフェース |
4 |
org.lwhglb.engine |
Utils |
ユーティリティクラス、現状ではファイル読みこみ処理の未実装 |
5 |
org.lwhglb.engine |
Window |
内部クラスにWindowOptionを持つ、画面の設定、表示など画面周りの処理を行う |
6 |
org.lwhglb.engine.graph |
Mesh |
まだ説明がない、三角形を描いている |
7 |
org.lwhglb.engine.graph |
Render |
Meshクラスを描画する |
8 |
org.lwhglb.engine.graph |
SceneRender |
各頂点のバインド、描画処理を行っている |
9 |
org.lwhglb.engine.graph |
ShaderProgram |
さまざまなシェーダー モジュール (頂点、フラグメント) のソース コードを受け取り、それらをコンパイルしてリンクし、シェーダー プログラムを生成 |
10 |
org.lwhglb.engine.scene |
Scene |
シーン描画のためのデータを管理するクラス |
一般的なシェーダープログラムの動き
- OpenGL プログラムを作成します。
- シェーダー プログラム モジュール (頂点シェーダーまたはフラグメント シェーダー) を読み込みます。
- シェーダーごとに、新しいシェーダー モジュールを作成し、そのタイプ (頂点、フラグメント) を指定します。
- シェーダーをコンパイルします。
- シェーダーをプログラムにアタッチします。
- プログラムをリンクします。
最後に、シェーダー プログラムが GPU に読み込まれ、プログラム識別子という識別子を参照して使用できます。
ShaderProgramクラスの動き
Javaで作成されているソースコードのShaderProgramクラスの動きです。
コンストラクタ
コンストラクタの引数に、ShaderProgramのインナークラスShaderModuleDataを持っていて、ANSCのシェーダーのファイル(scene.flag, scene.vert)へのパスと
タイプ指定用のint型を指定します。つまりは下のようなコードがかけるわけです。
import static org.lwjgl.opengl.GL30.*;
public class ShaderProgram {
public SceneRender() {
List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.vert", GL_VERTEX_SHADER));
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.frag", GL_FRAGMENT_SHADER));
shaderProgram = new ShaderProgram(shaderModuleDataList);
}
}
GL_VERTEX_SHADERとGL_FRAGMENT_SHADERは、org.lwjgl.opengl.GL30クラスで保持している定数です。
余談ですが、次のようなimport文は、クラスを宣言しなくても直接メソッドやフィールドにアクセスできいます。
下のコードは同じ結果を2回出力します。
import static org.lwjgl.opengl.GL30.*;
public class Sample {
public void test() {
System.out.println("定数1: " + GL_VERTEX_SHADER + "定数2: " + GL_FRAGMENT_SHADER);
System.out.println("定数1: " + GL30.GL_VERTEX_SHADER + "定数2: " + GL30.GL_FRAGMENT_SHADER);
}
}
Meshクラスについて
実際に、サンプルプログラムで表示する三角形を表すクラスです。このクラスは次のプロパティ(属性、フィールド変数の事)を持っている。
- 頂点数
- VAOのID
- VBOリスト(VBOのIDリスト)
VAOとVBO
ドキュメントに以下の説明があります。
頂点配列オブジェクト (VAO) と頂点バッファー オブジェクト (VBO) という 2 つの重要な概念を紹介します。上記のコードで迷子になった場合は、最後に、描画したいオブジェクトをモデル化するデータをグラフィックス カード メモリに送信していることを思い出してください。保存すると、後で描画中に参照するための識別子が取得されます。
- 頂点配列オブジェクト (VAO)
- VAOは、通常属性リストと呼ばれる 1 つ以上の VBO を含むオブジェクトです。各アトリビュート リストには、位置、色、テクスチャなどの 1 種類のデータを保持できます。各スロットには、必要なデータを自由に格納できます。
- 頂点バッファー オブジェクト (VBO)
- VBO は、頂点を格納するグラフィックス カード メモリに格納される単なるメモリ バッファです。これは、三角形をモデル化するフロートの配列を転送する場所です。前に述べたように、OpenGL はデータ構造について何も知りません。実際、座標だけでなく、テクスチャや色などの他の情報も保持できます。
Meshクラスの処理内容を確認
package org.lwjglb.engine.graph;
import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryStack;
import java.nio.FloatBuffer;
import java.util.*;
import static org.lwjgl.opengl.GL30.*;
public class Mesh {
private int numVertices;
private int vaoId;
private List<Integer> vboIdList;
public Mesh(float[] positions, int numVertices) {
try (MemoryStack stack = MemoryStack.stackPush()) {
this.numVertices = numVertices;
vboIdList = new ArrayList<>();
// VAOの生成とバインド
vaoId = glGenVertexArrays();
glBindVertexArray(vaoId);
// Positions VBO
int vboId = glGenBuffers();
vboIdList.add(vboId);
FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
positionsBuffer.put(0, positions);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
}
public void cleanup() {
vboIdList.stream().forEach(GL30::glDeleteBuffers);
glDeleteVertexArrays(vaoId);
}
public int getNumVertices() {
return numVertices;
}
public final int getVaoId() {
return vaoId;
}
}
- 最初に、glGenVertexArrays関数を呼び出してVAO を作成し 、glBindVertexArray関数を呼び出しそれをバインドします。
- glGenBuffers関数を呼び出して VBO を作成し、データをそこに入れる必要があります。そのために、float の配列を に格納しますFloatBuffer。
これは主に、C ベースの OpenGL ライブラリとインターフェイスする必要があるためです。そのため、float の配列をライブラリで管理できるものに変換する必要があります。
ここら辺の処理は、SceneRenderクラスで行っています。
三角形の描画(実践編)
今まで、長々と理論部分を記述してきましたがここからは、プログラムの実行をメインに記述します。
具体的に、プログラムを実行、コードを読んで処理の内容を理解しようということです。
とりあえず描画しているコード
赤い三角形を描画している部分は次のところでした。
Render#render()
glViewport(0, 0, window.getWidth(), window.getHeight());
上記のメソッドでは次のものを指定しています。
- 第一引数、第二引数で表示する位置
- 第三引数、第四引数で三角形の底辺、高さ
これの値を変更してプログラムを実行してみましょう。自分は以下のようにコードを変更して実行しました。
- ファイルの参照先を変更する、SceneRenderクラスのコンストラクタの一部を修正しました。
public SceneRender() {
List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.vert", GL_VERTEX_SHADER));
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("chapter-03/resources/shaders/scene.frag", GL_FRAGMENT_SHADER));
shaderProgram = new ShaderProgram(shaderModuleDataList);
}
- 三角形の描画処理を変更薄る
public void render(Window window, Scene scene) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//glViewport(0, 0, window.getWidth(), window.getHeight());
glViewport(1, 2, window.getWidth() / 3, window.getHeight() / 2);
sceneRender.render(scene);
}
そして実行結果外貨の通り
そして、さらにscene.flagの値を変更して下のようにします。
#version 330
out vec4 fragColor;
void main()
{
fragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
三角形の色が青くなりました。この部分で色を指定しているようです。
つぎはsceme.vert
#version 330
layout (location=0) in vec3 inPosition;
void main()
{
gl_Position = vec4(inPosition, 3.0);
}
三角形の大きさが小さく、位置もずれました。画面の最大サイズが(おそらく)3倍になったようです。
クラス図
次は、Chapter04を学習します。(これから作成します)
<<<前回 次回 >>>