Alexander Overvoordeによる素晴らしいチュートリアルをJavaに移植したものです。オリジナルのコードはこちら。
これらのチュートリアルは、C++のチュートリアルに沿って簡単に進められるように書かれています。
なお、JavaとLWJGLのスタイルに合わせるために、いくつかの変更が行われています。当リポジトリはオリジナルと同じ構造になっています。
各チャプターには、それぞれ別々のJavaのコードが用意されています。しかし、多くのチャプターで共通するクラスがいくつかあります。
- AlignmentUtils: 均一なバッファオブジェクトのアライメントを扱うためのユーティリティクラス。
- Frame: 実行中のフレームに必要なVulkanハンドル (画像が利用可能なセマフォ、レンダー完了セマフォ、フェンス)。
- ModelLoader: 3Dモデルを読み込むためのユーティリティクラス。Assimpを利用しています。
- ShaderSPIRVUtils: GLSLシェーダをランタイムでSPIRVのバイナリにコンパイルするためのユーティリティクラス。
数値計算には、グラフィック数学用のJavaのライブラリであるJOMLを使用します。これはGLMにかなりよく似ています。
最後に、各章にはそれぞれ.diffファイルが用意されており、章ごとの変更点をすぐ確認できます。
なお、本チュートリアルのJavaのコードはC/C++よりも冗長であり、その分ソースファイルが大きくなることをご了承ください。
今回は、LWJGLという極めて低レベルなGLFW、Vulkan、OpenGL、その他CライブラリのJavaバインディングを使用します。
もしあなたがLWJGLを知らないのであれば、このチュートリアルに登場する概念やパターンを理解するのが難しいかもしれません。
ここでは、コードを正しく理解するために必要な、最も重要な概念を簡単に説明します。
Vulkanには、VkImage、VkBuffer、VkCommandPoolなど、適切に名付けられた独自のハンドルがあります。
これらは裏では符号なしの整数値であり、Javaにはその型定義がありません。
そのため、これらのオブジェクトの全ての型としてlong型を使用する必要があります。
これが、long型の変数がたくさん登場する理由です。
構造体や関数の中には、他の変数への参照やポインタをパラメータとして受け取るものがあります。例えば、複数の値を出力するものが該当します。
この関数をC言語で考えてみましょう。
int width;
int height;
glfwGetWindowSize(window, &width, &height);
// widthとheightは、ウィンドウの縦横の大きさです。2つのint型のポインタを渡すと、関数はそのポインタが指すメモリを書き込みます。簡単で、しかも高速ですね。
では、Javaではどうなるでしょうか。そもそもポインタという概念がありませんよね。参照のコピーを渡して、関数の中でオブジェクトの内容を変更することはできますが、プリミティブ型ではそれができません。
方法は2つあります。まず、int型の配列を使用するか、JavaのNIOバッファを使用するかです。
LWJGLのバッファは、基本的にはウィンドウの配列で、内部に位置と制限があります。
後述しますが、これらのバッファを使用する際、ヒープから割り当てることができます。
上記のglfwGetWindowSize関数をNIOバッファで扱う場合は、以下のようになります。
IntBuffer width = BufferUtils.createIntBuffer(1);
IntBuffer height = BufferUtils.createIntBuffer(1);
glfwGetWindowSize(window, width, height);
// Print the values
System.out.println("width = " + width.get(0));
System.out.println("height = " + height.get(0));やりました!プリミティブな値のポインタを渡すことができましたね。…でも、たった2つの整数のために、2つも新しいオブジェクトを動的に割り当ててしまっています。
もしこの2つのオブジェクトが、短時間しか必要ないものだったらどうしましょう?ガベージコレクタが、これらの使い捨ての変数を掃除してくれるのを待たないといけませんね。
幸いなことに、LWJGLは独自のメモリ管理システムでこの問題を解決しています。詳しくはこちらで知ることができます。
C/C++では、スタック上に簡単にオブジェクトを割り当てることができます。
VkApplicationInfo appInfo = {};
// ...
しかし、Javaではこれができません。
が、これまた幸運なことに、LWJGLではスタック上に変数を割り当てることができるようになっています。そのためには、MemoryStackのインスタンスが必要です。
スタックフレームは、関数の最初にプッシュ(スタックの末尾に追加)され、最後にポップ(取り出し)します。そのため、途中で何が起こっても構いません。try-with-resources構文を使い、この動作を真似てみましょう。
try(MemoryStack stack = stackPush()) {
// ...
} // この行では、スタックがポップされ、このスタックフレーム内の全ての変数が解放されます。いいですね!Javaでスタックの割り当てができるようになりました。どんなものか見てみましょう。
try(MemoryStack stack = stackPush()) {
IntBuffer width = stack.mallocInt(1); // int型を1つ割り当て(未初期化)
IntBuffer height = stack.ints(0); // 0で初期化
glfwGetWindowSize(window, width, height);
// 値を出力
System.out.println("width = " + width.get(0));
System.out.println("height = " + height.get(0));
}さあ、MemoryStackを使った実際のVulkanの例を見てみましょう。
private void createInstance() {
try(MemoryStack stack = stackPush()) {
// 構造体を0で初期化するにはcallocを使います。そうしないと、ランダムな値が原因でプログラムがクラッシュすることがあります。
VkApplicationInfo appInfo = VkApplicationInfo.callocStack(stack);
appInfo.sType(VK_STRUCTURE_TYPE_APPLICATION_INFO);
appInfo.pApplicationName(stack.UTF8Safe("Hello Triangle"));
appInfo.applicationVersion(VK_MAKE_VERSION(1, 0, 0));
appInfo.pEngineName(stack.UTF8Safe("No Engine"));
appInfo.engineVersion(VK_MAKE_VERSION(1, 0, 0));
appInfo.apiVersion(VK_API_VERSION_1_0);
VkInstanceCreateInfo createInfo = VkInstanceCreateInfo.callocStack(stack);
createInfo.sType(VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO);
createInfo.pApplicationInfo(appInfo);
// enabledExtensionCountは、ppEnabledExtensionNamesを呼び出した際に暗黙的に設定されます。
createInfo.ppEnabledExtensionNames(glfwGetRequiredInstanceExtensions());
// enabledLayerCountでも同様です。
createInfo.ppEnabledLayerNames(null);
// 作成したインスタンスのポインタを取得する必要があります。
PointerBuffer instancePtr = stack.mallocPointer(1);
if(vkCreateInstance(createInfo, null, instancePtr) != VK_SUCCESS) {
throw new RuntimeException("Failed to create instance");
}
instance = new VkInstance(instancePtr.get(0), createInfo);
}
}シェーダーはshadercライブラリを用いてSPIRVにコンパイルされます。GLSLファイルはresources/shadersフォルダにあります。
(検証レイヤーのエラーが発生しますが、次の章で修正します。)
モデルの読み込みにはAssimpを使用します。これは、異なるフォーマットの3Dモデルを読み込むライブラリであり、LWJGLのバインディングの一部として含まれています。
今回は、Assimpを用いたモデルの読み込みに関する処理をModelLoaderクラスにまとめています。
アイコンはIcon Mafiaが制作しました。
当ドキュメントは、GenbuchanがJavaでVulkanを学習するために、独自で日本語化したものです。
そのため、原文と日本語版の文脈に齟齬がある可能性があります。
もし、ドキュメントの問題の指摘や改善に協力してくださる方がいらっしゃいましたら、IssueまたはPull Requestをお願いします。
翻訳の補助にはDeepL Translatorを使用しました。
