Java 3D LWJGL GitBook 〜ゲームループの実装 Chapter02〜

第二章ゲームループ

前回に引き続き3Dモデルを表示させるために、LWJGLを使用して実現したいと思っています。LWJGLの資料には3Dモデルの詳細が説明されているので、
Gitbookで作成されているドキュメントを参考にして、学習および、実行します。

の章では、ゲーム ループを作成してゲーム エンジンの開発を開始します。ゲーム ループは、すべてのゲームのコア コンポーネントです。これは基本的に無限ループであり、定期的にユーザー入力を処理し、ゲームの状態を更新し、画面にレンダリングします。

注意

このドキュメントは英語なのでそれをブラウザで翻訳して読んでいます。
そして、このドキュメントの内容を補足する内容を記載しています。

ゲームループの基本

次のスニペットは、ゲーム ループの構造を示しています。

while (keepOnRunning) {
    input();
    update();
    render();
}

このinputメソッドは、ユーザー入力 (キーストローク、マウスの動きなど) を処理します。このupdateメソッドは、ゲームの状態 (敵の位置、AI など) を更新する役割を担っています。ゲームループは終わりましたか?まあ、まだです。上記のスニペットには多くの落とし穴があります。まず、ゲーム ループの実行速度は、実行するマシンによって異なります。マシンが十分に高速な場合、ユーザーはゲームで何が起こっているかを確認することさえできません。さらに、そのゲーム ループはすべてのマシン リソースを消費します。

まず、ゲームの状態が更新される期間と、ゲームが画面にレンダリングされる期間を別々に制御したい場合があります。なぜこれを行うのですか?ゲームの状態を一定の速度で更新することは、特に物理エンジンを使用している場合には重要です。逆に、レンダリングが間に合わない場合、ゲーム ループの処理中に古いフレームをレンダリングしても意味がありません。一部のフレームをスキップする柔軟性があります。

ゲームループの処理「loop()」
  • input(): キーボード・マウスなどの入力のの処理をする。
  • update(): ゲームの状態(敵の位置、AI など)を更新する。
  • render(): ゲーム画面の更新をする。

クラス図

クラス名 役目 概要
Main ゲームプログラム起動 ゲームを起動して、IAppLogicをimplements(実装)する
Window GLFWのすべての呼び出しを行う ウィンドウ ハンドル、その幅と高さ、およびウィンドウのサイズが変更など
Scene 3D シーンの将来の要素 (モデルなど) を保持 現状では空のプレースホルダー
Render 画面の描画、レンダリングを行う 現状では画面をクリアする別のプレースホルダー
Engine ゲームロジックを動かす ゲームループの実行を行う

ここで、「カプセル化」という言葉について補足します。

カプセル化とは
一つの機能を実現するための処理をまとめて管理するためのクラスの作り方です。

つまり、Windowクラスは、ウィンドウの表示、設定などの処理をひとまとめにして、持っているのでウィンドウの操作をしたければ
このクラスを使用すればよい。という形でプログラムを組むことができます。
同様に、Engineクラス、Scene、Renderとあります。もちろんMainクラスも同様です。

では、各クラスの詳細を見ていきます。

IAppLogicインターフェース

ゲーム ループを調べる前に、エンジンのコアを形成するサポート クラスを作成しましょう。まず、ゲーム ロジックをカプセル化するインターフェイスを作成します。これにより、ゲーム エンジンをさまざまな章で再利用できるようになります。このインターフェイスには、ゲーム アセットの初期化 ( init)、ユーザー入力の処理 ( input)、ゲーム ステートの更新 ( update)、およびリソースのクリーンアップ( ) を行うメソッドが含まれますcleanup。

このインターフェースは、このインターフェースを実装(implements)するクラスに定義したメソッドをオーバーライドする事を強制するので、
このインターフェースを実装(implements)するクラスは、必ず定義したメソッドを実装します。

つまり、MainクラスはIAppLogicを実装(implements)するので、

ゲーム アセットの初期化 ( init)、ユーザー入力の処理 ( input)、ゲーム ステートの更新 ( update)、およびリソースのクリーンアップ( ) を行う

ということです。

package org.lwjglb.engine;

import org.lwjglb.engine.graph.Render;
import org.lwjglb.engine.scene.Scene;

public interface IAppLogic {

    void cleanup();

    void init(Window window, Scene scene, Render render);

    void input(Window window, Scene scene, long diffTimeMillis);

    void update(Window window, Scene scene, long diffTimeMillis);
}

Windowクラス

ご覧のとおり、まだ定義していないいくつかのクラス インスタンス ( Window、Sceneおよび) と、これらのメソッドの呼び出しの間に渡されるミリ秒を保持するRenderという名前のパラメーターがあります。diffTimeMillis

Windowクラスから始めましょう。ウィンドウを作成および管理するための GLFW ライブラリへのすべての呼び出しをこのクラスにカプセル化します。その構造は次のようになります。

主に、ウィンドウを起動するために必要な処理を行います。インナークラス(内部クラス)にて、ウィンドウサイズを保持しています。
Window#WindowOptionの重要な部分(プロパティ(フィールド変数))を抜粋しておきます。

compatibleProfile
これは、以前のバージョンの古い関数 (非推奨の関数) を使用するかどうかを制御します。
fps
1 秒あたりの目標フレーム数 (FPS) を定義します。値がゼロより小さい場合は、ターゲットを設定したくないが、モニターのリフレッシュをターゲット FPS として使用することを意味します。そのために、v-sync を使用します (これはglfwSwapBuffers、バッファーをスワップして戻る前に、呼び出された時点から待機する画面更新の数です)。
height
目的のウィンドウの高さ。
width
目的のウィンドウの幅。
ups
1 秒あたりの更新の目標数を定義します (既定値に初期化されます)。
package org.lwjglb.engine;

import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.system.MemoryUtil;
import org.tinylog.Logger;

import java.util.concurrent.Callable;

import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryUtil.NULL;

public class Window {

    private final long windowHandle;
    private int height;
    private Callable<Void> resizeFunc;
    private int width;
    ...
    ...
    public static class WindowOptions {
        public boolean compatibleProfile;
        public int fps;
        public int height;
        public int ups = Engine.TARGET_UPS;
        public int width;
    }
}

ご覧のとおり、ウィンドウ ハンドル、その幅と高さ、およびウィンドウのサイズが変更されたときに呼び出されるコールバック関数を格納するためのいくつかの属性が定義されています。また、ウィンドウの作成を制御するいくつかのオプションを設定するための内部クラスも定義します。

compatibleProfile: これは、以前のバージョンの古い関数 (非推奨の関数) を使用するかどうかを制御します。
fps: 1 秒あたりの目標フレーム数 (FPS) を定義します。値がゼロより小さい場合は、ターゲットを設定したくないが、モニターのリフレッシュをターゲット FPS として使用することを意味します。そのために、v-sync を使用します (これはglfwSwapBuffers、バッファーをスワップして戻る前に、呼び出された時点から待機する画面更新の数です)。
・height: 目的のウィンドウの高さ。
・width: 希望のウィンドウ幅:
・ups: 1 秒あたりの更新の目標数を定義します (既定値に初期化されます)。
`Window+ クラスのコンストラクターを調べてみましょう。

public class Window {
    ...
    public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
        this.resizeFunc = resizeFunc;
        if (!glfwInit()) {
            throw new IllegalStateException("Unable to initialize GLFW");
        }
        glfwDefaultWindowHints();
        glfwWindowHint(GLFW_VISIBLE, GL_FALSE);
        glfwWindowHint(GLFW_RESIZABLE, GL_TRUE);

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
        if (opts.compatibleProfile) {
            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
        } else {
            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
            glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        }

        if (opts.width > 0 && opts.height > 0) {
            this.width = opts.width;
            this.height = opts.height;
        } else {
            glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
            GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
            width = vidMode.width();
            height = vidMode.height();
        }
       windowHandle = glfwCreateWindow(width, height, title, NULL, NULL);
        if (windowHandle == NULL) {
            throw new RuntimeException("Failed to create the GLFW window");
        }

        glfwSetFramebufferSizeCallback(windowHandle, (window, w, h) -> resized(w, h));

        glfwSetErrorCallback((int errorCode, long msgPtr) ->
                Logger.error("Error code [{}], msg [{]]", errorCode, MemoryUtil.memUTF8(msgPtr))
        );
        glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> {
            if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
                glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
            }
        });

        glfwMakeContextCurrent(windowHandle);

        if (opts.fps > 0) {
            glfwSwapInterval(0);
        } else {
            glfwSwapInterval(1);
        }

        glfwShowWindow(windowHandle);

        int[] arrWidth = new int[1];
        int[] arrHeight = new int[1];
        glfwGetFramebufferSize(windowHandle, arrWidth, arrHeight);
        width = arrWidth[0];
        height = arrHeight[0];
    }
    ...

ウィンドウヒントを設定してウィンドウを非表示にし、サイズ変更可能に設定することから始めます。その後、OpenGL のバージョンを設定し、ウィンドウ オプションに応じてコアまたは互換プロファイルを設定します。次に、適切な幅と高さを設定していない場合は、プライマリ モニターのサイズを取得してウィンドウ サイズを設定します。次に、 を呼び出してウィンドウを作成し、ウィンドウのglfwCreateWindowサイズが変更されたとき、またはウィンドウの終了 (ESCキーが押されたとき) を検出するためにいくつかのコールバックを設定します。ターゲット FPS を手動で設定する場合は、呼び出しglfwSwapInterval(0)て v-sync を無効にし、最後にウィンドウを表示してフレーム バッファー サイズ (render() に使用されるウィンドウの部分) を取得します。

クラスの残りのメソッドは、Windowリソースのクリーンアップ、サイズ変更コールバック、ウィンドウ サイズのいくつかのゲッター、およびイベントをポーリングし、ウィンドウを閉じる必要があるかどうかを確認するメソッドです。

public class Window {
    ...
    public void cleanup() {
        glfwFreeCallbacks(windowHandle);
        glfwDestroyWindow(windowHandle);
        glfwTerminate();
        GLFWErrorCallback callback = glfwSetErrorCallback(null);
        if (callback != null) {
            callback.free();
        }
    }

    public int getHeight() {
        return height;
    }

    public int getWidth() {
        return width;
    }

    public boolean isKeyPressed(int keyCode) {
        return glfwGetKey(windowHandle, keyCode) == GLFW_PRESS;
    }

    public void pollEvents() {
        glfwPollEvents();
    }

    protected void resized(int width, int height) {
        this.width = width;
        this.height = height;
        try {
            resizeFunc.call();
        } catch (Exception excp) {
            Logger.error("Error calling resize callback", excp);
        }
    }

    public void update() {
        glfwSwapBuffers(windowHandle);
    }

    public boolean windowShouldClose() {
        return glfwWindowShouldClose(windowHandle);
    }
    ...
}

このSceneクラスは、3D シーンの将来の要素 (モデルなど) を保持します。今では空のプレースホルダーです。

package org.lwjglb.engine.scene;

public class Scene {

    public Scene() {
    }

    public void cleanup() {
        // Nothing to be done here yet
    }
}

Renderクラスは、画面をクリアする別のプレースホルダーになりました。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL;
import org.lwjglb.engine.Window;
import org.lwjglb.engine.scene.Scene;

import static org.lwjgl.opengl.GL11.*;

public class Render {

    public Render() {
        GL.createCapabilities();
    }

    public void cleanup() {
        // Nothing to be done here yet
    }

    public void render(Window window, Scene scene) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }
}

Engineこれで、次のように始まる名前の新しいクラスにゲーム ループを実装できます。

package org.lwjglb.engine;

import org.lwjglb.engine.graph.Render;
import org.lwjglb.engine.scene.Scene;

public class Engine {

    public static final int TARGET_UPS = 30;
    private final IAppLogic appLogic;
    private final Window window;
    private Render render;
    private boolean running;
    private Scene scene;
    private int targetFps;
    private int targetUps;

    public Engine(String windowTitle, Window.WindowOptions opts, IAppLogic appLogic) {
        window = new Window(windowTitle, opts, () -> {
            resize();
            return null;
        });
        targetFps = opts.fps;
        targetUps = opts.ups;
        this.appLogic = appLogic;
        render = new Render();
        scene = new Scene();
        appLogic.init(window, scene, render);
        running = true;
    }

    private void cleanup() {
        appLogic.cleanup();
        render.cleanup();
        scene.cleanup();
        window.cleanup();
    }

    private void resize() {
        // Nothing to be done yet
    }
    ...
}

このEngineクラスは、コンストラクターでウィンドウのタイトル、ウィンドウ オプション、およびIAppLogicインターフェイスの実装への参照を受け取ります。Windowコンストラクターでは、、RenderおよびSceneクラスのインスタンスを作成します。このcleanupメソッドは、他のクラスcleanupリソースを呼び出すだけです。ゲーム ループは、次のrunように定義されるメソッドで定義されます。

public class Engine {
    ...
    private void run() {
        long initialTime = System.currentTimeMillis();
        float timeU = 1000.0f / targetUps;
        float timeR = targetFps > 0 ? 1000.0f / targetFps : 0;
        float deltaUpdate = 0;
        float deltaFps = 0;

        long updateTime = initialTime;
        while (running && !window.windowShouldClose()) {
            window.pollEvents();

            long now = System.currentTimeMillis();
            deltaUpdate += (now - initialTime) / timeU;
            deltaFps += (now - initialTime) / timeR;

            if (targetFps <= 0 || deltaFps >= 1) {
                appLogic.input(window, scene, now - initialTime);
            }

            if (deltaUpdate >= 1) {
                long diffTimeMillis = now - updateTime;
                appLogic.update(window, scene, diffTimeMillis);
                updateTime = now;
                deltaUpdate--;
            }

            if (targetFps <= 0 || deltaFps >= 1) {
                render.render(window, scene);
                deltaFps--;
                window.update();
            }
            initialTime = now;
        }

        cleanup();
    }
    ...
}

ループは、更新 ( ) とレンダリング呼び出し ( )の間の最大経過時間をミリ秒単位で制御するtimeUとの2 つのパラメーターを計算することから始まります。これらの期間が消費された場合、ゲームの状態を更新するかレンダリングする必要があります。後者の場合、ターゲット FPS が 0 に設定されている場合、v-sync リフレッシュ レートに依存するため、値を に設定するだけです。ループは、ウィンドウを介してイベントをポーリングすることから始まります。その後、現在の時間をミリ秒単位で取得します。その後、更新呼び出しとレンダリング呼び出しの間の経過時間を取得します。レンダリング (または v-sync のリレー) の最大経過時間を過ぎた場合、 を呼び出してユーザー入力を処理します。最大更新経過時間を超えた場合は、呼び出してゲームの状態を更新しますtimeRtimeUtimeR0appLogic.inputappLogic.update. レンダリング (または v-sync のリレー) の最大経過時間を過ぎた場合、 を呼び出してレンダリング呼び出しをトリガーしますrender.render。

ループの最後で、cleanupメソッドを呼び出してリソースを解放します。

最後に、次のEngineように完了します。

public class Engine {
    ...
    public void start() {
        running = true;
        run();
    }

    public void stop() {
        running = false;
    }
}

スレッドについて少し注意してください。GLFW はメインスレッドから初期化する必要があります。イベントのポーリングもそのスレッドで行う必要があります。したがって、ゲームでよく見られるゲーム ループ用の別のスレッドを作成する代わりに、メイン スレッドからすべてを実行します。Threadこれが、startメソッドでnew を作成しない理由です。

Main最後に、クラスを単純化して次のようにします。

package org.lwjglb.game;

import org.lwjglb.engine.*;
import org.lwjglb.engine.graph.Render;
import org.lwjglb.engine.scene.Scene;

public class Main implements IAppLogic {

    public static void main(String[] args) {
        Main main = new Main();
        Engine gameEng = new Engine("chapter-02", new Window.WindowOptions(), main);
        gameEng.start();
    }

    @Override
    public void cleanup() {
        // Nothing to be done yet
    }

    @Override
    public void init(Window window, Scene scene, Render render) {
        // Nothing to be done yet
    }

    @Override
    public void input(Window window, Scene scene, long diffTimeMillis) {
        // Nothing to be done yet
    }

    @Override
    public void update(Window window, Scene scene, long diffTimeMillis) {
        // Nothing to be done yet
    }
}

インスタンスを作成し、メソッドEngineで起動するだけです。mainこのMainクラスはIAppLogic、今では空になっているインターフェースも実装しています。

ゲームループについて

以下のような説明がありました。

これは基本的に無限ループであり、定期的にユーザー入力を処理し、ゲームの状態を更新し、画面にレンダリングします。

この通りなのですが、 少しかみ砕いて記載したいと思います。やることは以下の通り

  1. 無限ループ内で実行する
  2. 定期的にユーザー入力を処理
  3. ゲームの状態(データの状態)を更新
  4. 画面を再描画(レンダリング)

これをコードに落とすと次のようになります。

// 無限ループ
while () {
    // ユーザー入力
    input();
    // データの更新
    update();
    // 画面の更新
    rendaer();
}

input(), update(), render()の各メソッドは別途実装します。ただ、実装する内容に関しては作成するものによって変わるのでインターフェースにしておきます。
それが説明にある「IAppLogic」です。
このようにインターフェースを作成し、implementsしてやれば必ず上記の3メソッドを実装することになるので、クラス構成がわかりやすくなります。

処理の流れ

Githubにアップされているコードをどのような順序で処理しているのか一通り調べました。  
次のような順序で処理していました。

メインクラス:メインメソッドのあるクラス
インターフェースIAppLogicを実装し、自身(Mainクラス)をEngineクラスのコンストラクタに渡してEngine#start()メソッドを実行しています。
Engineクラス
WindowOptionの値、IAppLogicをフィールド変数にセットしていつでも呼び出せるようにしています。
またstart()メソッドからrun()メソッドを実行し、run()メソッドでは、ゲームループを実行しています。

プログラムの実行結果としては、何も表示されない(画面が黒い)状態になります。

Windowクラス

オブジェクト指向プログラミングの基本「役割分担」を行った結果「画面関連の処理を担当するクラス」をWindowクラスとして作成します。
ドキュメントに以下の記載があるように、クラスを作成します。※コードはGithubにアップされていますが。。。

ウィンドウを作成および管理するための GLFW ライブラリへのすべての呼び出しをこのクラスにカプセル化

WindowOption

ウィンドウを作成するのに必要な情報をインナークラス(内部クラス)で管理します。

public class Window {

    private final long windowHandle;
    private int height;
    private Callable<Void> resizeFunc;
    private int width;

    /** インナークラス */
    public static class WindowOptions {
        public boolean compatibleProfile;
        public int fps;
        public int height;
        public int ups = Engine.TARGET_UPS;
        public int width;
    }
}

各フィールド(オプションの値)は次のようになっています。

compatibleProfile: これは、以前のバージョンの古い関数 (非推奨の関数) を使用するかどうかを制御します。
fps: 1 秒あたりの目標フレーム数 (FPS) を定義します。値がゼロより小さい場合は、ターゲットを設定したくないが、
モニターのリフレッシュをターゲット FPS として使用することを意味します。そのために、v-sync を使用します
(これはglfwSwapBuffers、バッファーをスワップして戻る前に、呼び出された時点から待機する画面更新の数です)。
height: 目的のウィンドウの高さ。
width: 希望のウィンドウ幅:
ups: 1 秒あたりの更新の目標数を定義します (既定値に初期化されます)。

Windowクラスのコンストラクタ

まずは、コンストラクタの定義を見てみます。

 public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
    // 省略
 }

第一引数にタイトル、第二引数にWindowOption(Windowクラスのインナークラス)、第三引数にCallBackがあります。
第三引数のクラスがよくわからないので、呼び出し元を調べます。

window = new Window(windowTitle, opts, () -> {
    resize();
    return null;
});

Engineクラスで呼び出されています。変数「window」はWindowクラス型のフィールド変数です。
つまりは、Windowクラスをインスタンス化してwindow変数にセットしているというわけです。
注目するのは、第三引数です。渡しているのは「() -> { ... };」の部分です。これは、いわゆる関数型という書き方で
Functionalインターフェースの理解が必要になります。
なので、今回は、「引数に関数(メソッド)を渡しているのだな。」と理解してください。

まとめると、第三引数ひは、resize()メソッドを呼び出してから、nullを返す処理を行っているメソッドを渡しています。

スレッドについて少し注意してください。GLFW はメインスレッドから初期化する必要があります。イベントのポーリングもそのスレッドで行う必要があります。したがって、ゲームでよく見られるゲーム ループ用の別のスレッドを作成する代わりに、メイン スレッドからすべてを実行します。Threadこれが、startメソッドでnew を作成しない理由です。

いろいろ書いていますが、メインメソッドが動いているスレッドでGLFWの初期化を行いましょう。ということです。

まとめ

IAppLogicクラスにゲームループの中身、cleanup(), init(), input(), update()を作成しています。
Window, EngineクラスはIAppLogicを動かすためにゲームループを作成したり、ウィンドウの処理を行ったりとそれぞれに役割が分担されています。
ここから、ゲームの実装方法、OpenGLの操作方法法を学習していくと思われます。

WindowOptionについて

画面のサイズを保持するクラスですが、このクラスの高さ(height)と幅(width)は、Windowクラスのコンストラクタ、以下の処理部分で値がセットされているようです。ちなみに、int型のデフォルト値(宣言しただけの時にセットされる値は0です。

        if (opts.width > 0 && opts.height > 0) {
            System.out.println("*** In True ***");
            this.width = opts.width;
            this.height = opts.height;
        } else {
            System.out.println("*** In False ***");
            glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
            GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
            width = vidMode.width();
            height = vidMode.height();
        }

<<< 前回 次回 >>>

Java 3D LWJGL 〜Tutorial #3 OBJファイルをロードする〜

前回に引き続き、次はOBJファイルのロード(読み込み)を行います。

Git bookのページはこちら
参照するGithubはこちらです。

OBJファイルロード

作成したものは、以下のものです。(コピーして作成)

手順

Gitをクローンする

まずは、学習に使用するGitプロジェクト(LWJGL3のチュートリアル)をクローンします。
ここのリンクにGit bookがある(PDF)のでこのプロジェクトで行なっている内容の解説をしています(英語ですが。。。)

プロジェクトを移植する

これは、プロジェクトをクローンした時に、フォルダの階層が、1プロジェクト=chapter1-29になっているのでプロジェクトとして動きません。

まとめてクローンした場合は、プロジェクト単位で抜き出してやる必要があります。ビルドパスの関係ですね。。。

自分の場合は以下のようにやりました。

単純にクローン下プロジェクトの必要な部分(プログラムのパッケージ以下)をコピーして貼り付けてやるだけです。
そして、プログラムのビルドエラーが出ると思うのでそれを解消してやる必要があります。

ビルドエラーの原因としては、「クラスが見つからない」がほとんどです。なので、参照されるクラスが全てビルドパス上に存在すれば良いということです。

セットアップ時には、resourcesファイルも使用するのでこれも忘れないようにコピーしてあげましょう。

OBJファイルのロード

OBJファイルを読み込んでロードする。これの詳細は[Gitbookを見た方が良い]()のですが、如何せん英語なんだなぁ。。。コードと翻訳機能で、読んで見た所、以下のサイトを参考にしました。

こちらも同様に参考にしました。

OBJファイルをロードするのには

An OBJ file defines the vertices, texture coordinates and polygons that compose a 3D model.

と記載があるように、

  1. 各頂点
  2. テクスチャを貼り付けるための座標
  3. ポリゴンの座標

上記のデータをもとに、表示しますのでここの情報をOBJファイルからロードして必要な情報をモデルに設定します。

学習の進め方

これは自分の一番やりやすかった方法ですが、記載しておきます。

  1. Githubから参考にするプロジェクト(ソース)をクローン
  2. 説明などがあるドキュメント(今回はGit book)を用意
  3. 対象のChapterを読みながら、プログラムを読む。
  4. アプリを動かす。

シンプルかつ、時間がかかるけど一番効率の良い方法だと思います。

ポイントになるコード


上にあるように、DummyGameクラスがポイントになります。

public class DummyGame implements IGameLogic
上に示すように、このクラスにはIGameLogicというインターフェースが実装されているので、このインターフェースクラスで定義されているメソッドをオーバーライドすれば、基本的には動くものが作成できます。

サンプルとして「DummyGame」があると思うと飲み込みやすいのではないでしょうか?

そして実装するメソッドは以下のIGameLogoicの中身を見ればわかります。

public interface IGameLogic {
   // 初期表示する内容を実装します、今回はロードしたOBJファイルです。
    void init(Window window) throws Exception;
    // ここには、入力した時どのようなアクションを起こすか定義します。
    void input(Window window, MouseInput mouseInput);
   // 画面の更新処理です。
    void update(float interval, MouseInput mouseInput);
    // レンダリング(画面更新):ここら辺がまだ理解できていません。。。
    void render(Window window);
    // 画面の初期化、再描画するのに必要
    void cleanup();
}

上記の処理が大まかな処理です。

Objファイルをロードする処理

Gitからソースをクローンしてから、chapter-09のフォルダを開き、Mainクラスを実行します。するとエラーが出ます。
ファイルの参照ができないエラーです

[LWJGL] [ThreadLocalUtil] Unsupported JNI version detected, this may result in a crash. Please inform LWJGL developers.
Exception in thread "main" java.lang.RuntimeException: Image file [resources/models/default/default_texture.png] not loaded: > > Unable to open file
at org.lwjglb.engine.graph.Texture.(Texture.java:29)
at org.lwjglb.engine.graph.TextureCache.(TextureCache.java:13)
at org.lwjglb.engine.scene.Scene.(Scene.java:17)
at org.lwjglb.engine.Engine.(Engine.java:26)
at org.lwjglb.game.Main.main(Main.java:19)

これは、resources/models/default/default_texture.pngのようなファイル参照のパスが違っているためです。
これの原因は、IDEのビルドパス設定ですが、面倒なので「chapter-09/」を追加して実行しました。

まとめると、「resources/XXX/XXX/XX.XXX」のように書いている部分を「chapter-09/resources/XXX/XXX/XX.XXX」のように修正すればよいということです。エラーのある部分から、パス(ファイル)を指定しているコードを見つけるのが面倒ですが。。。

OBJLoaderを使用する(Runtimeエラーがあり使用不能でした。。。)

理論部分では、ちゃんとしたものですが、細かい部分Objファイルのmtlファイルの参照部分が読み込めないなど、致命的なミスがありました。。。

こちらのページにOBJLoaderクラスがありました。これを使用することを考えていこうと思います。
OBJファイルのロード方法、考え方に関しては上記の方法がわかりやすいとドキュメントにはありましたが、こちらのOBJLoaderには、クラスにまとまった形で実装されているようです。

OBJファイルのロード処理は次のような説明が参考になりました。

.obj ファイルでは、各行は要素のタイプを識別するトークンで始まります。

コメントは # で始まる行です。

  • トークン「v」は、座標 (x、y、z、w) を持つ幾何学的頂点を定義します。例: v 0.155 0.211 0.32 1.0。
  • トークン「vn」は、座標 (x、y、z) を持つ頂点法線を定義します。例: vn 0.71 0.21 0.82。これについては後で詳しく説明します。
  • トークン「vt」は、テクスチャ座標を定義します。例: vt 0.500 1.
  • トークン「f」は顔を定義します。これらの行に含まれる情報を使用して、インデックス配列を作成します。面が三角形としてエクスポートされる場合のみを扱います。いくつかのバリアントを持つことができます:
  • 頂点位置 (f v1 v2 v3) のみを定義できます。例: f 6 3 1. この場合、この三角形は、位置 6、3、および 1 を占める幾何学的頂点によって定義されます (頂点インデックスは常に 1 から始まります)。
  • 頂点位置、テクスチャ座標、法線 (f v1/t1/n1 v2/t2/n2 V3/t3/n3) を定義できます。例: f 6/4/1 3/5/3 7/6/5. 最初のブロックは「6/4/1」で、座標、テクスチャ座標、法線頂点を定義します。ここに表示されているのは位置です。つまり、6 番目のジオメトリ頂点、4 番目のテクスチャ座標、1 番目の頂点法線を選択します。

Gitのソースを眺める

上記のリンクOBJLoaderには次のクラスが定義されています。

軽く舐め回す程度にコードを眺めてみると次のような形で実装されていることがわかりました。

Obj.java:Objファイルを読み込み
Objフィルの中身を読んで、vertex(点),
OBJLoader.java:モデルのロード

これを理解するために参考にしたサイトを以下に照会します。参考になりました。

  1. (x, y, z, w)の意味が分からなかった。:「Unityのサイト:同次座標系:Wの正体」が参考になりました。
  2. 同時座標系の細かい部分

でわでわ。。。

関連ページのメニュー

LWJGLの学習履歴
Git bookの内容を学習しました。



Java 3D LWJGL 〜Tutorial #2 入力を受け付ける〜

前回作成したコードでは、画面を表示するだけでした。
今度は、入力を受け付ける処理を追加します。
参考にするチュートリアルは動画ですが、こちらになります。

入力を受け付ける

入力、つまりボタンを押下した時のハンドリング部分の実装になります。
くだけた言い方をすると、ボタンを押した時の動きを実装する部分(メソッド)を作る準備をするということです。

早速、コードを写経したので、それを見て見ましょう。
<メインクラス>

public class GameMain {
    public static void main(String[] args) {
        Window win = new Window(800, 500, "LWJGL Tutorial");
        win.create();

        //// ゲームループ ////
        while (!win.closed()) {
            win.update();
            if (win.isKeyPressed(GLFW.GLFW_KEY_A)) {
                System.out.println("Pressed Key A");
            }
            if (win.isKeyReleased(GLFW.GLFW_KEY_A)) {
                System.out.println("Released Key A");
            }
            win.swapBuffers();
        }
    }
}

<ウィンドウクラス>

public class Window {
    /** 画面の幅 */
    private int width;
    /** 画面の高さ */
    private int height;
    /** 画面のタイトル */
    private String title;
    /** 画面の(プログラムレベルの)番号 */
    private long window;

    /** キーボート配列の数だけ配列を用意 */
    private boolean[] keys = new boolean[GLFW.GLFW_KEY_LAST];
    /** マウスのボタン数だけ配列を用意 */
    private boolean[] mouseButtons = new boolean[GLFW.GLFW_MOUSE_BUTTON_LAST];

    /**
     * 画面を作成する、コンストラクター。
     * 
     * @param width 画面の幅
     * @param height 画面の高さ
     * @param title 画面のタイトル
     */
    public Window(int width, int height, String title) {
        this.width = width;
        this.height = height;
        this.title = title;
    }

    /**
     * 画面を作成する処理。
     */
    public void create() {
        if (!GLFW.glfwInit()) {
            System.err.println("Error: Coudn not GLFW");
            System.exit(-1);
        }
        window = GLFW.glfwCreateWindow(width, height, title, 0, 0);
        if (window == 0) {
            System.err.println("Error: window coud not be created");
            System.exit(-1);
        }
        GLFWVidMode videoMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
        GLFW.glfwSetWindowPos(window, (videoMode.width() - width) / 2, (videoMode.height() - height) / 2);
    }

    /** 画面を閉じる */
    public boolean closed() {
        return GLFW.glfwWindowShouldClose(window);
    }

    /** 画面を更新する */
    public void update() {
        for (int i = 0; i < GLFW.GLFW_KEY_LAST; i++) keys[i] = isKeyDown(i);
        for (int i = 0; i < GLFW.GLFW_MOUSE_BUTTON_LAST; i++) keys[i] = isMouseDown(i);
        GLFW.glfwPollEvents();
    }

    public void swapBuffers() {
        GLFW.glfwSwapBuffers(window);
    }

    /** 
     * キーボートの入力を受付
     * @param keyCode this{@link #keys}の配列番号
     * @return キーボードが押下されているか判定した結果
     */
    public boolean isKeyDown(int keyCode) {
        return GLFW.glfwGetKey(window, keyCode) == 1;
    }

    /** 
     * マウスの入力を受付
     * @param mouseButton this{@link #mouseButtons}の配列番号
     * @return マウスのボタンが押されているか判定した結果
     */
    public boolean isMouseDown(int mouseButton) {
        return GLFW.glfwGetMouseButton(window, mouseButton) == 1;
    }

    /** キーボードのボタンが押下されているか判定 */
    public boolean isKeyPressed(int keyCode) {
        return isKeyDown(keyCode) && !keys[keyCode];
    }

    /** キーボードのボタンが離されているか判定 */
    public boolean isKeyReleased(int keyCode) {
        return isKeyDown(keyCode) && keys[keyCode];
    }

    /** マウスのボタンが押下されているか判定 */
    public boolean isMouseButtonPressed(int mouseButton) {
        return isKeyDown(mouseButton) && !mouseButtons[mouseButton];
    }

    /** マウスのボタンが離されているか判定 */
    public boolean isMouseButtonReleased(int mouseButton) {
        return isKeyDown(mouseButton) && mouseButtons[mouseButton];
    }

    /** マウスのX軸座標取得 */
    @Deprecated
    public double getMouseX() {
        DoubleBuffer buf = BufferUtils.createDoubleBuffer(1);
        GLFW.glfwGetCursorPos(window, buf, null);
        return buf.get(0);
    }

    /** マウスのY軸座標取得 */
    @Deprecated
    public double getMouseY() {
        DoubleBuffer buf = BufferUtils.createDoubleBuffer(1);
        GLFW.glfwGetCursorPos(window, null, buf);
        return buf.get(0);
    }
}

とりあえず写経して動かして見ました。

しかし、マウスのイベントが取れない事象に遭遇したので、それをなんとかしてどうにかしました。

原因は、イベントが取得できるであろうと思った下のコードでイベントが取得できなかったところです。

/** 
 * マウスの入力を受付
 * @param mouseButton this{@link #mouseButtons}の配列番号
 * @return マウスのボタンが押されているか判定した結果
 */
public boolean isMouseDown(int mouseButton) {
    return GLFW.glfwGetMouseButton(window, mouseButton) == 1;
}

こちらのサイトで見てみると、イベントハンドラが実装されているようなので、そちらを使用します。
コードは下の2種類あるようです。
PatternA: コールバッククラスを設定する

= new GLFWCursorPosCallback() {
    @Override
    public void invoke(long window, double xpos, double ypos) {

    }
});

PatternB: ラムダ式でコールバック処理を設定する

// Java 8
glfwSetCursorPosCallback(window, posCallback = GLFWCursorPosCallback.create((window, xpos, ypos) -> {
    // ここにマウスが押下された時の処理を実装する
}));

背景の実装

下の2つの動画で、出てきた。変数FPSについて自分が理解したことを記載します。

この動画のはじめの部分

最終的に、追加実装したコードは下のようなものです。GetterとSetterを覗きます。(フィールド変数のGetterを作成しました。)
Githubにソースをコミットしました。

一部抜粋してきます。
FPSをWindowクラスに追加してやりました。これは、ボタン押下時の反応速度?(フレーム数)がなんとかとチュートリアルで言っていたのを聞いた程度しか理解していません。

public boolean isUpdating() {
    double nextTime = getTime();
    double passedTime = nextTime - time;
    processedTime += passedTime;
    time = nextTime;

while(processedTime >  1.0/fps_cap) {
processedTime -= 1.0/fps_cap;
        return true;
    }
    return false;
}

この処理で、どうやらフレーム数がなんとか。。。課題として今後理解する必要がありそうだ。

実行した結果は下のような形で、背景も赤くなりました。

関連ページ一覧

http://zenryokuservice.com/wp/2018/10/27/java-game-lwjgl-gitbook-chapter5-2%E3%80%9C%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E8%A9%B3%E7%B4%B0%E3%80%9C/

<開発準備>

http://zenryokuservice.com/wp/2018/05/02/set-up-1-eclipse-java/

<LWJGLのGitBokに関して>

  1. Chapter1[外枠の表示のみ]
  2. Chapter2-1〜クラスの構成〜
  3. Chapter2-2〜インターフェースの使い方と詳細〜
  4. Chapter2-3〜GameEngineクラス(サンプルクラス)〜/li>
  5. Chapter2-4〜Windowクラス(サンプルクラス)〜
  6. Chapter3〜描画処理を読む〜
  7. Chapter4〜シェーダについて〜
  8. Chapter5-1〜レンダリングについて〜

<Java Basic>

  1. Java Basic Level 1 〜Hello Java〜
  2. Java Basic Level2 〜Arithmetic Calculate〜
  3. Java Basic Level3 〜About String class〜
  4. Java Basic Level 4〜Boolean〜
  5. Java Basic Level 5〜If Statement〜
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 〜Traning of If statement〜
  8. Java Basic Level8 〜How to use for statement〜
  9. Java Basic Level 8.5 〜Array〜
  10. Java Basic Level 8.5 〜Array〜
  11. Java Basic Level 10 〜While statement 〜
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2〜


Java 3D LWJGL GitBook: Chapter01〜環境構築 ハローワールド 〜

イントロダクション

Javaで3Dモデルを操作することができ、ゲーム作成のためのAPI(ライブラリ)、LEJGL(Light Weight Java Game Library)を
使用して、3Dモデルを表示させようと思います。
<参照するもの>

注意

このドキュメントは英語なのでそれをブラウザで翻訳して読んでいます。

早い話が。。。

OBJファイルを読み込んで3DモデルをJavaで表示するためにLWJGLを使用することにしました。

LWJGLは下のような特徴があります。

  1. OBJファイルは、3Dモデルを作成したときにできるファイルです。
  2. OpenGLというライブラリも使用しているので、3Dモデルや通常の2Dイメージも表示できるのがLWJGLです。
  3. やはり、プログラムなのでゲームを作成するのでなくても、部分的に、例えば3Dモデルの表示のみでも使える

Blenderなどで作成した3Dモデル(Objファイル)を表示するのが目的です。
JOGL(他のライブラリ)でやろうと思ったけど、チュートリアルや、ドキュメントが少ないので、やはり今回も使わない方向でいきます。

参考

LWJGLのGitbookです、合わせてプログラムソースもGithubにアップされているので、そちらも参考にします。

LWJGL GitBook Chapter-01

今回は、LWJGLを起動することを確認するプログラムコードを動かしましょう。というものです。
はじめのプログラム「ハローワールド」のLWJGL版です。基本的にハローワルドはDB接続、フレームワークの使用をするときにそれらがちゃんと動かせるかどうか確認するために実行するプログラムです。

今回の学習内容

学習を始めるのに参考にするもののリンクを張っておきます。

IntelliJ IDEAを使用して、環境構築(プログラムを動かす設定)をおこない、ハローワールドを実行するということが今回の目的になります。
早い話が、LWJGLが動く確認をします

環境構築

参考サイトにあるように、環境構築を行います。下の文言を参考にします。

。IntelliJ は無​​料のオープン ソース バージョンであるコミュニティ バージョンを提供しており、 https ://www.jetbrains.com/idea/download/ からダウンロードできます。

このリンクから、IntelliJをダウンロードして、インストールしたら、早速必要なライブラリ(依存関係)をダウンロードします。
Mavenで実行しているようなので、Maven(メイベン)を使用します。

Mavenの使用

用途としては、必要なライブラリをダウンロードしてくることです。そのためにビルドツールとしてMavenを使用します。他にもGradleが有名です。
まずは、IntelliJ起動してください。そしたらGithubからGitbookのプロジェクトをチェックアウトします。

プロジェクトのチェックアウト(クローン)

起動した、IntelliJのプロジェクトを未選択の状態で開きます。初回の軌道であればそのままで大丈夫です。

ここで、赤枠の部分Get from VCSをクリックします。「VCS」は バージョンコントロールシステムのことです。つまり、バージョン管理を行うアプリケーションのことです。

そして、下の項目を入力します。

URLに関しては、Githubのページを開いたら、したのようなボタンがあるのでそれをクリックします。

必要なライブラリのダウンロード

クローンしてきたプロジェクトにはpom.xmlというファイルがあると思います。これを右クリックして次のようにします。

  1. Mavenを選択
  2. Reload Projectをクリック

これで必要なライブラリがダウンロードされます。
早速、ハローワールドを実行してみましょう。下のような実行結果が見れると思います。

Gitbookの内容

まずは、環境構築のために必要なリポジトリ、依存関係のダウンロードを行うためのPOMファイルへの記述内容の説明があります。

POMファイル(pom.xml)の概要

参考にしたページには書いていませんが、pom.xmlでは、次のようなものを定義しています。

  • project: 作成するプロジェクト
  • parent: 親プロジェクト
  • artifactId: このファイルの定義するプロジェクトのID
  • dependencies: 依存関係
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.lwjglb</groupId>
        <artifactId>book</artifactId>
        <version>1.0</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>chapter-01</artifactId>
    <version>1.0</version>

    <dependencies>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>

        <!-- Natives -->
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

この本では、 OpenGLを使用して 3D ゲームを開発する際の主なテクニックを学びます。Java でサンプルを開発し、Lightweight Java Game Library LWJGLを使用します。LWJGL ライブラリを使用すると、Java から OpenGL などの低レベル API (アプリケーション プログラミング インターフェイス) にアクセスできます。

LWJGL は、OpenGL のラッパーのように機能する低レベル API です。したがって、短期間で 3D ゲームの作成を開始することを考えている場合は、jMonkeyEngineやUnityなどの他の代替手段を検討する必要があります。この低レベルの API を使用すると、結果を確認する前に、多くの概念を調べ、多くのコード行を記述する必要があります。このようにすることの利点は、3D グラフィックスをよりよく理解できるようになり、常にコントロールできるようになることです。

Java に関しては、少なくとも Java 18 が必要です。したがって、そのバージョンがインストールされていない場合の最初のステップは、Java SDK をダウンロードすることです。ここからOpenJDK バイナリをダウンロードするか、Windows および macOS 用のインストーラーがあるAdoptiumからダウンロードできます。Adoptium ビルドをダウンロードするときは、「JRE」ではなく「JDK」をダウンロードしてください。お使いのオペレーティング システムに適したインストーラーを選択してインストールするだけです。いずれにせよ、この本は Java 言語についてある程度理解していることを前提としています。そうでない場合は、まずその言語について適切な知識を得る必要があります。

サンプルを実行するために必要な Java IDE を使用できます。Java を適切にサポートする IntelliJ IDEA をダウンロードできます。IntelliJ は無​​料のオープン ソース バージョンであるコミュニティ バージョンを提供しており、 https ://www.jetbrains.com/idea/download/ からダウンロードできます。

IDE でソース コードを開くと、すべての章 (親プロジェクト) を含むルート フォルダーを開くか、各章を個別に開くことができます。最初のケースでは、各章の作業ディレクトリを章のルート フォルダに適切に設定することを忘れないでください。サンプルは、ルート フォルダーがチャプター ベース フォルダーであると想定して、相対パスを使用してファイルにアクセスしようとします。

サンプルのビルドにはMavenを使用します。Maven はほとんどの IDE に既に統合されており、それらのさまざまなサンプルを直接開くことができます。章のサンプルを含むフォルダーを開くだけで、IntelliJ はそれが Maven プロジェクトであることを検出します。

Maven は、(プロジェクト オブジェクト モデル) という名前の XML ファイルに基づいてプロジェクトをビルドします。このファイルは、pom.xmlプロジェクトの依存関係 (使用する必要があるライブラリ) と、ビルド プロセス中に実行する手順を管理します。Maven は、構成よりも規則の原則に従います。つまり、標準のプロジェクト構造と命名規則に固執する場合、構成ファイルは、ソース ファイルの場所やコンパイルされたクラスの場所を明示的に指定する必要はありません。

この本は Maven のチュートリアルを意図したものではないので、必要に応じて Web で情報を見つけてください。ソース コード フォルダーは、使用するプラグインを定義し、使用するライブラリのバージョンを収集する親プロジェクトを定義します。

LWJGL 3.1 では、プロジェクトのビルド方法にいくつかの変更が導入されました。現在、ベース コードはよりモジュール化されており、巨大なモノリシック jar ファイルを使用する代わりに、使用するパッケージをより選択することができます。これには代償が伴います。依存関係を 1 つずつ慎重に指定する必要があります。ただし、ダウンロードページには、pom ファイルを生成する優れたツールが含まれています。この場合、最初に GLFW と OpenGL バインディングを使用します。ソースコードでpomファイルがどのように見えるかを確認できます。

LWJGL プラットフォームの依存関係は、プラットフォームのネイティブ ライブラリの展開を既に処理しているため、他のプラグイン ( などmavennatives) を使用する必要はありません。LWJGL プラットフォームを構成するプロパティを設定するために、3 つのプロファイルを設定するだけです。プロファイルは、Windows、Linux、および Mac OS ファミリのそのプロパティの正しい値を設定します。

    <profiles>
        <profile>
            <id>windows-profile</id>
            <activation>
                <os>
                    <family>Windows</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-windows</native.target>
            </properties>                
        </profile>
        <profile>
            <id>linux-profile</id>
            <activation>
                <os>
                    <family>Linux</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-linux</native.target>
            </properties>                
        </profile>
        <profile>
            <id>OSX-profile</id>
            <activation>
                <os>
                    <family>mac</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-osx</native.target>
            </properties>
        </profile>
    </profiles>

各プロジェクト内で、LWJGL プラットフォームの依存関係は、現在のプラットフォームのプロファイルで確立された正しいプロパティを使用します。

        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-platform</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
        </dependency>

それに加えて、すべてのプロジェクトは実行可能な jar (java -jar name_of_the_jar.jar と入力して実行できるもの) を生成します。MANIFEST.MFこれは、正しい値を持つファイルで jar を作成する maven-jar-plugin を使用して実現されます。そのファイルの最も重要な属性はMain-Class、プログラムのエントリ ポイントを設定する です。さらに、すべての依存関係は、Class-Pathそのファイルの属性のエントリとして設定されます。別のコンピューターで実行するには、ターゲット ディレクトリの下にあるメインの jar ファイルと lib ディレクトリ (そこに含まれるすべての jar ファイルを含む) をコピーするだけです。

LWJGL クラスを含む jar には、ネイティブ ライブラリも含まれています。また、LWJGL はそれらを抽出し、JVM がライブラリを探すパスに追加します。

この章のソース コードは、LWJGL サイトhttp://www.lwjgl.org/guideの入門用サンプルから直接取得したものです。非常によく文書化されていますが、ソース コードを見て、最も関連性の高い部分を説明しましょう。クラスごとにソースコードを貼り付けると読めなくなってしまうので、フラグメントを含めます。特定の各フラグメントが属するクラスをよりよく理解するために、各フラグメントには常にクラス ヘッダーが含まれます。3 つのドット ( ...) を使用して、フラグメントの前後にさらにコードがあることを示します。HelloWorldサンプルは、次のように始まる名前の単一のクラスに含まれています。

ソースコード解析

プログラムの内容を確認します。初回のLWJGL学習なので市登場するクラスは1つです。
ポイントは、OpenGLを使用している処理部分です。
具体的には、以下の通りです。

  1. 赤い四角を描画している処理
  2. 描画の準備をする処理
  3. プログラムの構成

プログラムの構成

  • main: runメソッドを起動。
  • run: init, loopを呼び出す。
  • init: アプリケーションを初期化するメソッド。
  • loop: 基本的にウィンドウにレンダリングする無限ループであるメソッド。

メインメソッド

以下のサンプルコードにあるように、メインメソッドの処理内容が書いてあります。
単純に「HelloWorld#run()」を呼び出しています。
とてもシンプルです。言葉に直すのであれば「『ハローワールド』を起動する」という言い方になります。

package org.lwjglb;

import org.lwjgl.Version;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.MemoryStack;

import java.nio.IntBuffer;

import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;

public class HelloWorld {

    // The window handle
    private long window;

    public static void main(String[] args) {
        new HelloWorld().run();
    }
    ...
}

run()メソッド

mainこのクラスは、メソッドを呼び出すメソッド内に、ウィンドウ ハンドルへの参照を格納するだけです (これが何を意味するかは後で説明します) run。そのメソッドの分析を始めましょう。

このメソッドがプログラムの処理内容を具体的に呼び出している部分です。
具体的に、以下の処理を行っています。正確には「呼び出しています」といったほうが良いかもしれません。

  1. LWJGLのバージョン情報表示。
  2. プログラムの初期処理「init()」を呼び出す。
  3. プログラムの主要部分「loop()」を呼び出す。
  4. プログラムの終了処理。

1~4の各処理に対して、細かい部分は各メソッド(init(), loop())の中身を見ることにして、
この「run()メソッド」では、上記の処理(呼び出し)を行っています。

public class HelloWorld {
    ...
    public void run() {
        System.out.println("Hello LWJGL " + Version.getVersion() + "!");

        init();
        loop();

        // Free the window callbacks and destroy the window
        glfwFreeCallbacks(window);
        glfwDestroyWindow(window);

        // Terminate GLFW and free the error callback
        glfwTerminate();
        glfwSetErrorCallback(null).free();
    }
    ...
}

init()メソッド

このメソッドはinit、アプリケーションを初期化するメソッドを呼び出してから、loop基本的にウィンドウにレンダリングする無限ループであるメソッドを呼び出します。メソッドが終了したら、loop初期化中に作成されたいくつかのリソースを解放する必要があります (GLFW ウィンドウ)。メソッドから始めましょうinit。

public class HelloWorld {
    ...
    private void init() {
        // Setup an error callback. The default implementation
        // will print the error message in System.err.
        GLFWErrorCallback.createPrint(System.err).set();

        // Initialize GLFW. Most GLFW functions will not work before doing this.
        if (!glfwInit())
            throw new IllegalStateException("Unable to initialize GLFW");

        // Configure GLFW
        glfwDefaultWindowHints(); // optional, the current window hints are already the default
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // the window will stay hidden after creation
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // the window will be resizable

        // Create the window
        window = glfwCreateWindow(300, 300, "Hello World!", NULL, NULL);
        if (window == NULL)
            throw new RuntimeException("Failed to create the GLFW window");

        // Setup a key callback. It will be called every time a key is pressed, repeated or released.
        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE)
                glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
        });
        ...
    }
    ...
}

GLFWを呼び出すことから始めます。これは、GUI コンポーネント (Windows など) とイベント (キーの押下、マウスの動きなど) を処理するライブラリであり、OpenGL コンテキストが簡単な方法でアタッチされています。現在、Swing または AWT を直接使用して OpenGL をレンダリングすることはできません。AWT を使用したい場合はlwjgl3-awt を確認できますが、この本では GLFW に固執します。最初に、GLFW ライブラリを初期化し、ウィンドウの初期化のためのいくつかのパラメーターを設定することから始めます (サイズ変更可能かどうかなど)。ウィンドウは、glfwCreateWindowウィンドウの幅と高さ、およびウィンドウのタイトルを受け取ります。この関数は、他の GLFW 関連関数で使用できるように保存する必要があるハンドルを返します。その後、キーが押されたときに呼び出される関数であるキーボード コールバックを設定します。ESCこの場合、ウィンドウを閉じるためにキーが押されたかどうかを検出したいだけです。メソッドを続けましょうinit:

public class HelloWorld {
    ...
    private void init() {
        ...
        // Get the thread stack and push a new frame
        try (MemoryStack stack = stackPush()) {
            IntBuffer pWidth = stack.mallocInt(1); // int*
            IntBuffer pHeight = stack.mallocInt(1); // int*

            // Get the window size passed to glfwCreateWindow
            glfwGetWindowSize(window, pWidth, pHeight);

            // Get the resolution of the primary monitor
            GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());

            // Center the window
            glfwSetWindowPos(
                    window,
                    (vidmode.width() - pWidth.get(0)) / 2,
                    (vidmode.height() - pHeight.get(0)) / 2
            );
        } // the stack frame is popped automatically

        // Make the OpenGL context current
        glfwMakeContextCurrent(window);
        // Enable v-sync
        glfwSwapInterval(1);

        // Make the window visible
        glfwShowWindow(window);
    }
    ...
}

次の章で説明しますが、ここでは LWJGL の主要なクラスであるMemoryStack. 前に述べたように、LJWGL はネイティブ ライブラリ (C ベースの関数) のラッパーを提供します。Java にはポインターの概念がないため (少なくとも C の用語ではタイヒンク)、C 関数に構造体を渡すのは単純な作業ではありません。これらの構造を共有し、上記の例のように参照パラメーターを渡すには、ネイティブ コードからアクセスできるメモリを割り当てる必要があります。LWJGL は、MemoryStackネイティブ アクセス可能なメモリ/構造を割り当てることができるクラスを提供します。これにより、自動的にクリーンアップされます (実際には、再利用できるように strcuture のようなプールに返されます)。stackPushメソッドが呼び出されます。ネイティブにアクセス可能なすべてのメモリ/構造は、このスタック クラスを通じてインスタンス化されます。上記のサンプルでは、glfwGetWindowSize​​ウィンドウの寸法を取得するために を呼び出す必要があります。値は参照渡しのアプローチを使用して返されるため、2 つの int を (2 の形式でIntBuffer) 割り当てる必要があります。その情報とモニターの寸法を使用して、ウィンドウを中央に配置し、OpenGL をセットアップし、v-sync を有効にして (これについては次の章で詳しく説明します)、最後にウィンドウを表示します。

ここで、何かを継続的にレンダリングするための無限ループが必要です。

まとめると...

次のようになります。前提としてLWJGLはOpenGLを使用しています。

  • 既存のJava Swing(GUI作成をするAPI)、AWT(これもGUIを作成するAPI)などを直接使用してOpenGLをレンダリングはできない。
  • GLFW ライブラリを初期化(glfwInit())ウィンドウの初期化のためのいくつかのパラメーターを設定する「(コメントの)Configure GLFW部分」
  • glfwGetWindowSize()はウィンドウの幅と高さ、およびウィンドウのタイトルを受け取ります。そして、他の GLFW 関連関数で使用できるように保存する必要があるハンドルを返します。「」
  • キーが押されたときに呼び出される関数であるキーボード コールバックを設定します。「glfwSetKeyCallback()」

loop()メソッド

<行っている処理>

  • OpenGL コンテキストを作成
  • クリア カラーを設定
  • 各ループで (カラーおよび深度バッファに対して) クリア操作
  • ウィンドウを閉じる必要があるかどうかを検出するためにキーボード イベントをポーリング
public class HelloWorld {
    ...
    private void loop() {
        // This line is critical for LWJGL's interoperation with GLFW's
        // OpenGL context, or any context that is managed externally.
        // LWJGL detects the context that is current in the current thread,
        // creates the GLCapabilities instance and makes the OpenGL
        // bindings available for use.
        GL.createCapabilities();

        // Set the clear color
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);

        // Run the rendering loop until the user has attempted to close
        // the window or has pressed the ESCAPE key.
        while (!glfwWindowShouldClose(window)) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the framebuffer

            glfwSwapBuffers(window); // swap the color buffers

            // Poll for window events. The key callback above will only be
            // invoked during this call.
            glfwPollEvents();
        }
    }
    ...
}

最初に OpenGL コンテキストを作成し、クリア カラーを設定して、各ループで (カラーおよび深度バッファに対して) クリア操作を実行し、ウィンドウを閉じる必要があるかどうかを検出するためにキーボード イベントをポーリングします。これらの概念については、次の章で詳しく説明します。ただし、完全を期すために、レンダリングはターゲットに対して行われます。この場合は、色情報と深度値 (3D の場合) を含むバッファーに対して行われます。これらのバッファーに対するレンダリングが終了したら、GLFW に通知する必要があります。このバッファは、 を呼び出して表示する準備ができていglfwSwapBuffersます。GLFW はいくつかのバッファーを維持するため、1 つのバッファーでレンダリング操作を実行しながら、もう 1 つのバッファーがウィンドウに表示されます (そうでない場合、ちらつきアーティファクトが発生します)。

環境が正しくセットアップされていれば、それを実行して赤い背景のウィンドウを見ることができるはずです。

クラス図

わかりやすいようにクラス図を作成しました。

今回のコードは、クラスが一つですべての処理を行っているので、上記のようになります。

プログラムの実行内容

実行しているコードを説明しています。自分の理解では次のようになります。詳細は次の章(Chapter)で説明するそうです。

  1. メインメソッドの説明
    フィールド変数にwindowというint型の変数があり、runというスタティックメソッドを実行している
  2. run()メソッドの実行している内容の説明
    init()メソッドとloop()メソッドを呼び出している、loop()メソッドは無限ループになっている
  3. init()メソッドの説明
    GLFWを呼び出すことから始めています。GLFWは”GUI コンポーネント (Windows など) とイベント (キーの押下、マウスの動きなど) を処理するライブラリであり、OpenGL コンテキストが簡単な方法でアタッチされています”とあるように、画面コントロールのライブラリのようです。実行内容に関しては本文を参照ください。
  4. loop()メソッドの説明
    本文にありますが、何かを継続的にレンダリングするための無限ループということでした

loop()の処理内容

  1. glClearColor()で画面をクリア
  2. glfwSwapBuffers()でGLFWに通知

上記の処理を行っているようですが、細かいところは、次の章で明らかになると思います。。。

まとめ

環境構築編

Mavenの依存関係をPOMファイルで設定、Mevenによる依存関係の解決(ライブラリのダウンロード)を行い。
LWJGLが実行できるように環境を構築する使用しているIDEはIntelliJ IDEA

実装編

プログラムの実行、処理の内容を解説。

  1. init()で画面コントロール、表示するサイズなどの設定を行う
  2. loop()メソッドで、”何かを継続的にレンダリングするための無限ループ”

を実装するというところを理解した。

今回のハローワールドでは、赤い画面を表示するだけだったが、LWJGLの動作の基本が、おおまかに理解できる。

次回 >>>

Tutorial#1(別途行ったものです)

今回からLWJGLのチュートリアルを進めてきます。
このチュートリアルには、セットアップ(環境構築)手順も載っていました。

まずは、写経から入ります。
この動画で作成しているコードを写経していきます。
作成したコードは、下のようなコードになりました。

2クラスあります。
画面を起動するためのMainクラス自分が作成したものは「GameMainクラス」にしています。

public class GameMain {
    public static void main(String[] args) {
        Window win = new Window(800, 500, "LWJGL Tutorial");
        win.create();
        while (!win.closed()) {
            win.update();
            win.swapBuffers();
        }
    }
}

「Windowクラス」

public class Window {
    private int width;
    private int height;
    private String title;
    private long window;

    public Window(int width, int height, String title) {
        this.width = width;
        this.height = height;
        this.title = title;
    }

    public void create() {
        if (!GLFW.glfwInit()) {
            System.err.println("Error: Coudn not GLFW");
            System.exit(-1);
        }
        window = GLFW.glfwCreateWindow(width, height, title, 0, 0);
        if (window == 0) {
            System.err.println("Error: window coud not be created");
            System.exit(-1);
        }
        GLFWVidMode videoMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
        GLFW.glfwSetWindowPos(window, (videoMode.width() - width) / 2, (videoMode.height() - height) / 2);
    }

    public boolean closed() {
        return GLFW.glfwWindowShouldClose(window);
    }

    public void update() {
        GLFW.glfwPollEvents();
    }

    public void swapBuffers() {
        GLFW.glfwSwapBuffers(window);
    }
}

今回の作成した2つのクラスは、以下のような役割分担になっています。
GameMain: Windowクラスを起動する
Window: LWJGLで使用しているGLFWクラスを使用して画面を作成する処理を書きます。

今回はGitにあるソースをコピーとか参照することができなかったので、動画を見て写経しました。それで、下のコードを間違えていました、すると表示した時に画面が端っこに行ってしまいました。
GLFW.glfwSetWindowPos(window, (videoMode.width() + width) / 2, (videoMode.height() + height) / 2);

そして、このコードがよくわかりませんでした。
どの値を計算しているか?これがわからなかったという次第です。

なのでちょいと、いじって見ました、
タイトルの部分と、上の計算している部分のコードをチョチョっといじりました。

結局は、上のよくわからなかった部分は画面表示の初期位置を指定するものでした。

次回は、Chapter02をやります。

関連ページ一覧

http://zenryokuservice.com/wp/2018/10/27/java-game-lwjgl-gitbook-chapter5-2%E3%80%9C%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E8%A9%B3%E7%B4%B0%E3%80%9C/

<開発準備>

http://zenryokuservice.com/wp/2018/05/02/set-up-1-eclipse-java/

<LWJGLのGitBokに関して>

  1. Chapter1[外枠の表示のみ]
  2. Chapter2-1〜クラスの構成〜
  3. Chapter2-2〜インターフェースの使い方と詳細〜
  4. Chapter2-3〜GameEngineクラス(サンプルクラス)〜/li>
  5. Chapter2-4〜Windowクラス(サンプルクラス)〜
  6. Chapter3〜描画処理を読む〜
  7. Chapter4〜シェーダについて〜
  8. Chapter5-1〜レンダリングについて〜

<Java Basic>

  1. Java Basic Level 1 〜Hello Java〜
  2. Java Basic Level2 〜Arithmetic Calculate〜
  3. Java Basic Level3 〜About String class〜
  4. Java Basic Level 4〜Boolean〜
  5. Java Basic Level 5〜If Statement〜
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 〜Traning of If statement〜
  8. Java Basic Level8 〜How to use for statement〜
  9. Java Basic Level 8.5 〜Array〜
  10. Java Basic Level 8.5 〜Array〜
  11. Java Basic Level 10 〜While statement 〜
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2〜


Java 3D LWJGL 〜セットアップ手順〜

LWJGLのセットアップを行います。
JOGLとか、Java3D系のフレームワークを試して見ましたが、3Dモデルをゲーム的に使用するのであれば、LWJGLが一番適当なフレームワークに思えるのでこちらを使用するための手順を記載します。

LWJGLの学習

を参考に、主にコピーして実行して。。。という形でやりました。

IntelliJ IDEA(新しい)

上記のLWJGLのチュートリアル・テキストを参考にセットアップしました。

ハローワールド(LWJGL版)

チャプター1を見ながら、実行しました。
まずは、LWJGLの起動確認を行わないと始まりません。

IntelliJ IDEAでセットアップ

まずは、IntelliJでプロジェクトの作成を行いますが、チュートリアルによると、ビルドツールとしてMavenを使用しているようなので、こちらもそのようにします。

そして、POMファイルを編集します。まずは\タグ、使用しているPCのOSによりそれぞれ設定(記述)を行いました。

    <profiles>
        <!-- Windows -->
        <profile>
            <id>windows-profile</id>
            <activation>
                <os>
                    <family>Windows</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-windows</native.target>
            </properties>
        </profile>
    </profiles>

次に、依存関係(dependency)の追加を行います。

        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-platform</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
        </dependency>

最終的には、次のような形になりました。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>jp.zenryoku</groupId>
    <artifactId>3dloader</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <built.by>lwjglgamedev</built.by>
        <main.class>org.lwjglb.game.Main</main.class>
        <!-- Versions -->
        <imgui-java.version>1.86.4</imgui-java.version>
        <exec-maven-plugin.version>3.0.0</exec-maven-plugin.version>
        <java.version>18</java.version>
        <joml.version>1.10.5</joml.version>
        <joml-primitives.version>1.10.4</joml-primitives.version>
        <lwjgl.version>3.3.1</lwjgl.version>
        <maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version>
        <maven-dependency-plugin.version>3.3.0</maven-dependency-plugin.version>
        <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
        <maven-resources-plugin.version>3.2.0</maven-resources-plugin.version>
        <tinylog.version>2.5.0</tinylog.version>
    </properties>

    <profiles>
        <!-- Windows -->
        <profile>
            <id>windows-profile</id>
            <activation>
                <os>
                    <family>Windows</family>
                </os>
            </activation>
            <properties>
                <native.target>natives-windows</native.target>
            </properties>
        </profile>
    </profiles>

    <dependencies>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>

        <!-- Natives -->
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${native.target}</classifier>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

ソースのダウンロード

プロジェクトのソースpom.xmlを右クリックしてソースのダウンロードを選択します。
これが結構な時間を必要としました。(Wifiのスピードが。。。)

ビルドして起動

Githubにあるソースをコピペーして、プログラムを実行しました。

package org.lwjglb;

import org.lwjgl.Version;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.MemoryStack;

import java.nio.IntBuffer;

import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;

public class HelloWorld {

    // The window handle
    private long window;

    public static void main(String[] args) {
        new HelloWorld().run();
    }

    private void init() {
        // Setup an error callback. The default implementation
        // will print the error message in System.err.
        GLFWErrorCallback.createPrint(System.err).set();

        // Initialize GLFW. Most GLFW functions will not work before doing this.
        if (!glfwInit())
            throw new IllegalStateException("Unable to initialize GLFW");

        // Configure GLFW
        glfwDefaultWindowHints(); // optional, the current window hints are already the default
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // the window will stay hidden after creation
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // the window will be resizable

        // Create the window
        window = glfwCreateWindow(300, 300, "Hello World!", NULL, NULL);
        if (window == NULL)
            throw new RuntimeException("Failed to create the GLFW window");

        // Setup a key callback. It will be called every time a key is pressed, repeated or released.
        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE)
                glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
        });

        // Get the thread stack and push a new frame
        try (MemoryStack stack = stackPush()) {
            IntBuffer pWidth = stack.mallocInt(1); // int*
            IntBuffer pHeight = stack.mallocInt(1); // int*

            // Get the window size passed to glfwCreateWindow
            glfwGetWindowSize(window, pWidth, pHeight);

            // Get the resolution of the primary monitor
            GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());

            // Center the window
            glfwSetWindowPos(
                    window,
                    (vidmode.width() - pWidth.get(0)) / 2,
                    (vidmode.height() - pHeight.get(0)) / 2
            );
        } // the stack frame is popped automatically

        // Make the OpenGL context current
        glfwMakeContextCurrent(window);
        // Enable v-sync
        glfwSwapInterval(1);

        // Make the window visible
        glfwShowWindow(window);
    }

    private void loop() {
        // This line is critical for LWJGL's interoperation with GLFW's
        // OpenGL context, or any context that is managed externally.
        // LWJGL detects the context that is current in the current thread,
        // creates the GLCapabilities instance and makes the OpenGL
        // bindings available for use.
        GL.createCapabilities();

        // Set the clear color
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);

        // Run the rendering loop until the user has attempted to close
        // the window or has pressed the ESCAPE key.
        while (!glfwWindowShouldClose(window)) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the framebuffer

            glfwSwapBuffers(window); // swap the color buffers

            // Poll for window events. The key callback above will only be
            // invoked during this call.
            glfwPollEvents();
        }
    }

    public void run() {
        System.out.println("Hello LWJGL " + Version.getVersion() + "!");

        init();
        loop();

        // Free the window callbacks and destroy the window
        glfwFreeCallbacks(window);
        glfwDestroyWindow(window);

        // Terminate GLFW and free the error callback
        glfwTerminate();
        glfwSetErrorCallback(null).free();
    }

}

実行結果

OBJLoaderを使う

GithubのOBJLoaderクラスをコピペして実装します。

  1. 次のクラスファイルをコピーして次のように、javaファイルを作成
    OBJLoader.java、Obj.java

  2. pomファイルにリポジトリと依存関係を追加してソースをダウンロード

しかし、手順2で、次のようなエラーが出ました。

org.lwjgl:lwjgl-util:pom:2.7.1 failed to transfer from https://mvnrepository.com/artifact/org.lwjgl/lwjgl-util during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of utils has elapsed or updates are forced. Original error: Could not transfer artifact org.lwjgl:lwjgl-util:pom:2.7.1 from/to utils (https://mvnrepository.com/artifact/org.lwjgl/lwjgl-util): authorization failed for https://mvnrepository.com/artifact/org.lwjgl/lwjgl-util/org/lwjgl/lwjgl-util/2.7.1/lwjgl-util-2.7.1.pom, status: 403 Forbidden

なので、次の手順で解決しました。

  1. こちらのページからlwjgl-util-2.7.1.jarをダウンロード
  2. C:\Users\ユーザー名\.m2\repository\org\lwjgl\lwjgl-util\2.7.1のフォルダにJARファイルを配置

もしかしたら、良いこのみんなはやらないほうが良いかもしれません。。。

作成後のイメージは下のような感じです。

行った事のまとめ

  • OBJLoader.javaの作成(コピペ)
  • Obj.javaの作成(コピペ)
  • lwjgl-util-2.7.1.jarのダウンロードとファイルの配置
    配置先はC:\Users\ユーザー名\.m2\repository\org\lwjgl\lwjgl-util\2.7.1

Eclipseでの方法(古い)

参考にするサイトはこちらです。(英語)

ライブラリの作成した時の動画を下に載せます。Eclipseでのライブラリ追加を行いました。

Zipのダウンロード

こちらのリンクからダウンロードサイトに移動できます。そして、下の方に「〜.zip」があるのでそれをダウンロードします。

そして、ダウンロードできたら、ライブラリ用のフォルダを作成します。上の動画では「LWJGL3」というフォルダを作成し、それをライブラリとして使用しました。細かいところは動画で実行しています。

追伸、全てをダウンロードすると(カスタムでは無い方)動画のように、jarファイルの仕分けをしないといけませんが、リンク先のとこだと、対象になるOSを選択してくれるので楽です。

Eclipseにライブラリを追加

上の動画にもありますが、手順を記載しておきます。

  1. 対象になるプロジェクトを右クリック
  2. 「Add Library」ライブラリの追加でユーザーライブラリを選択
  3. ライブラリを作成してあれば下のようにライブラリを指定する
    下のものはライブラリを作成済みで、追加済みなのでエラーになっています。
  4. ライブラリが無い時は作成する
    3の画像で右のほうに「User Libraries」というボタンがあるのでそれをクリック
  5. 下のような画面が立ち上がる
  6. Newボタンを押下
  7. 適当な名前を入力してライブラリ名にする(今回はテスト的に「NEW」と入力)
  8. NEWライブラリ(フォルダ)にJARファイルを追加(Add External Jarを選択する)

そして、忘れてはいけないのが、Nativeのことです。

上のような感じで、

  1. ライブラリを開く
  2. Nativeライブラリロケーションを指定
    これは、ダウンロードしたnative-<OS名>のあるディレクトリを指定するだけで良いです

これで、ライブラリフォルダが作成できるので作成したライブラリを選択してApplyしてやればオK!

実行テストをする

コードをコピーして、動かそうと思ったら、下のコードでエラーが出ました。

import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.system.MemoryUtil.*;

この部分は、Nativeライブラリのロケーションを追加していないと出ます。

そして実行!

WARNING: nextEventMatchingMask should only be called from the Main Thread!

はて?何だろう?調べてみるとこちらのサイトで解決策がありました。MacOSの場合、上のようなエラーが出るということなので。。。
-XstartOnFirstThread をVMの引数に渡してやればOK!ということでした。
実行結果です。

とりあえずは、LWJGLを動かせたので、これでセットアップは完了ということになります。

Vertex3fクラスが無い

しかし、これで終わりではありませんでした。
今回の目的は、*.objファイルから3Dモデルを読み込みことなので、「Vertex3f」などのクラスも使用したい。。。

なので、こちらのJARファイルも必要なのでビルドパスに追加します。

そして、こちらのGithubリポジトリからObjFileLoaderをダウンロードします。 (パッケージ名などの細かいエラーは省略)

Displayクラスが無い

まだまだありました。。。org.lwjgl.opengl.Displayがlwjgl.jarにあるはずだけど、見つからず・・・。
探してみたところorg.lwjgl.utilにあるということでJARファイルをダウンロードしてきました

MavenでDisplayModeクラスを追加

Mavenを使用する場合です。POM.xmlファイルに下の依存関係を追加します。
注意としては<dependencies>タグの中に追加するところです。

<dependency>
    <groupId>org.lwjgl.lwjgl</groupId>
    <artifactId>lwjgl</artifactId>
    <version>2.9.1</version>
</dependency>

ビルドエラーはとりあえず解消

今までのファイルをインポートすることで、ビルドエラーは解消できると思います。
そして、チュートリアルのページを見つけました。とりあえずこれを進めないことには、Objファイルが読み込みできないと思いました。

まとめ

  1. セットアップの手順はここのページを見て行う
  2. JARファイルのダウンロードは、ここのページで行う、使用しているOSに対応したJARをダウンロードできる
  3. lwjgl_util.jarをダウンロード(追加)する
  4. pom.xmlにlwjglを追加する(上記の依存関係を追加する)
    <dependency>
    <groupId>org.lwjgl.lwjgl</groupId>
    <artifactId>lwjgl</artifactId>
    <version>2.9.1</version>
    </dependency>

でわ、でわ。。。