Java 3D LWJGL GitBook 〜Chapter10:ImGui を使用した GUI 描画〜

Imgui を使用した GUI 描画

今まで、3Dモデルの描画を中心に学習してきましたが、GUI作成の処理を学習します。

Dear ImGui は、OpenGL や Vulkan などの複数のバックエンドを使用できるユーザー インターフェイス ライブラリです。これを使用して、GUI コントロールを表示したり、HUD を開発したりします。複数のウィジェットを提供し、外観は簡単にカスタマイズできます。

参照するドキュメントとプログラムソースへのリンクは以下になります。

やはり、ライブラリを追加しますのでPOMファイルに追記します。

最初に、Java Imgui ラッパーの maven 依存関係をプロジェクト pom.xml に追加します。コンパイル時と実行時の依存関係を追加する必要があります。

<dependency>
   <groupId>io.github.spair</groupId>
   <artifactId>imgui-java-binding</artifactId>
   <version>${imgui-java.version}</version>
</dependency>
<dependency>
    <groupId>io.github.spair</groupId>
    <artifactId>imgui-java-${native.target}</artifactId>
    <version>${imgui-java.version}</version>
    <scope>runtime</scope>
</dependency>

Imgui を使用すると、2D 形状のみを使用して、他の 3D モデルをレンダリングするように、ウィンドウ、パネルなどをレンダリングできます。使用するコントロールを設定すると、Imgui がそれを、シェーダーを使用してレンダリングできる一連の頂点バッファーに変換します。これが、あらゆるバックエンドで使用できる理由です。

各頂点に対して、Imgui はその座標 (2D 座標)、テクスチャ座標、および関連する色を定義します。したがって、Gui メッシュをモデル化し、関連する VAO と VBO を作成する新しいクラスを作成する必要があります。という名前のクラスは、GuiMesh次のように定義されます。

こんな感じの表示が行われます。

GuiMeshの作成

package org.lwjglb.engine.graph;

import imgui.ImDrawData;

import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;

public class GuiMesh {

    private int indicesVBO;
    private int vaoId;
    private int verticesVBO;

    public GuiMesh() {
        vaoId = glGenVertexArrays();
        glBindVertexArray(vaoId);

        // Single VBO
        verticesVBO = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, verticesVBO);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 2, GL_FLOAT, false, ImDrawData.SIZEOF_IM_DRAW_VERT, 0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, false, ImDrawData.SIZEOF_IM_DRAW_VERT, 8);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, true, ImDrawData.SIZEOF_IM_DRAW_VERT, 16);

        indicesVBO = glGenBuffers();

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
    }

    public void cleanup() {
        glDeleteBuffers(indicesVBO);
        glDeleteBuffers(verticesVBO);
        glDeleteVertexArrays(vaoId);
    }

    public int getIndicesVBO() {
        return indicesVBO;
    }

    public int getVaoId() {
        return vaoId;
    }

    public int getVerticesVBO() {
        return verticesVBO;
    }
}

ご覧のとおり、単一の VBO を使用していますが、位置、テクスチャ座標、および色に対していくつかの属性を定義しています。この場合、バッファにデータを入力しません。使用方法については後で説明します。

IGuiInstanceの作成

また、アプリケーションが GUI コントロールを作成し、ユーザー入力に反応できるようにする必要もあります。これをサポートするために、IGuiInstance次のように定義される名前の新しいインターフェースを定義します。

package org.lwjglb.engine;

import org.lwjglb.engine.scene.Scene;

public interface IGuiInstance {
    void drawGui();

    boolean handleGuiInput(Scene scene, Window window);
}

Sceneの更新

このメソッドは、drawGuiGUI の構築に使用されます。ここで、GUI メッシュの構築に使用されるウィンドウとウィジェットを定義します。このメソッドを使用して、handleGuiInputGUI で入力イベントを処理します。入力が GUI によって処理されたかどうかを示すブール値を返します。たとえば、オーバーラップ ウィンドウを表示する場合、ゲーム ロジックでキーストロークを処理し続けることに関心がない場合があります。戻り値を使用してそれを制御できます。IGuiInstanceインターフェイスの特定の実装をクラスに格納しますScene。

public class Scene {
    ...
    private IGuiInstance guiInstance;
    ...
    public IGuiInstance getGuiInstance() {
        return guiInstance;
    }
    ...
    public void setGuiInstance(IGuiInstance guiInstance) {
        this.guiInstance = guiInstance;
    }
}

GuiRenderの作成

次のステップは、GUI をレンダリングするための新しいクラスを作成することですGuiRender。

package org.lwjglb.engine.graph;

import imgui.*;
import imgui.type.ImInt;
import org.joml.Vector2f;
import org.lwjglb.engine.*;
import org.lwjglb.engine.scene.Scene;

import java.nio.ByteBuffer;
import java.util.*;

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

public class GuiRender {

    private GuiMesh guiMesh;
    private Vector2f scale;
    private ShaderProgram shaderProgram;
    private Texture texture;
    private UniformsMap uniformsMap;

    public GuiRender(Window window) {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/gui.vert", GL_VERTEX_SHADER));
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/gui.frag", GL_FRAGMENT_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
        createUniforms();
        createUIResources(window);
    }

    public void cleanup() {
        shaderProgram.cleanup();
        texture.cleanup();
    }
    ...
}

ご覧のとおり、ここにあるもののほとんどは非常になじみ深いもので、シェーダーとユニフォームをセットアップしただけです。createUIResourcesただし、次のように定義された新しいメソッドが呼び出されます。

public class GuiRender {
    ...
    private void createUIResources(Window window) {
        ImGui.createContext();

        ImGuiIO imGuiIO = ImGui.getIO();
        imGuiIO.setIniFilename(null);
        imGuiIO.setDisplaySize(window.getWidth(), window.getHeight());

        ImFontAtlas fontAtlas = ImGui.getIO().getFonts();
        ImInt width = new ImInt();
        ImInt height = new ImInt();
        ByteBuffer buf = fontAtlas.getTexDataAsRGBA32(width, height);
        texture = new Texture(width.get(), height.get(), buf);

        guiMesh = new GuiMesh();
    }
    ...
}

上記の方法では、Imgui をセットアップします。最初にコンテキスト (操作を実行するために必要) を作成し、表示サイズをウィンドウ サイズに設定します。Imgui はステータスを ini ファイルに保存します。実行間でステータスを保持したくないため、null に設定する必要があります。次のステップは、フォント アトラスを初期化し、シェーダーで使用されるテクスチャを設定して、テキストなどを適切にレンダリングできるようにすることです。最後のステップは、インスタンスを作成することですGuiMesh。

は、createUniformsスケール用に単一の 2 つのフロートを作成するだけです (使用方法については後で説明します)。

public class GuiRender {
    ...
    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("scale");
        scale = new Vector2f();
    }
    ...
}

メソッドを見てみましょうrender。

public class GuiRender {
    ...
    public void render(Scene scene) {
        IGuiInstance guiInstance = scene.getGuiInstance();
        if (guiInstance == null) {
            return;
        }
        guiInstance.drawGui();

        shaderProgram.bind();

        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
        glDisable(GL_DEPTH_TEST);
        glDisable(GL_CULL_FACE);

        glBindVertexArray(guiMesh.getVaoId());

        glBindBuffer(GL_ARRAY_BUFFER, guiMesh.getVerticesVBO());
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, guiMesh.getIndicesVBO());

        ImGuiIO io = ImGui.getIO();
        scale.x = 2.0f / io.getDisplaySizeX();
        scale.y = -2.0f / io.getDisplaySizeY();
        uniformsMap.setUniform("scale", scale);

        ImDrawData drawData = ImGui.getDrawData();
        int numLists = drawData.getCmdListsCount();
        for (int i = 0; i < numLists; i++) {
            glBufferData(GL_ARRAY_BUFFER, drawData.getCmdListVtxBufferData(i), GL_STREAM_DRAW);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, drawData.getCmdListIdxBufferData(i), GL_STREAM_DRAW);

            int numCmds = drawData.getCmdListCmdBufferSize(i);
            for (int j = 0; j < numCmds; j++) {
                final int elemCount = drawData.getCmdListCmdBufferElemCount(i, j);
                final int idxBufferOffset = drawData.getCmdListCmdBufferIdxOffset(i, j);
                final int indices = idxBufferOffset * ImDrawData.SIZEOF_IM_DRAW_IDX;

                texture.bind();
                glDrawElements(GL_TRIANGLES, elemCount, GL_UNSIGNED_SHORT, indices);
            }
        }

        glEnable(GL_DEPTH_TEST);
        glEnable(GL_CULL_FACE);
        glDisable(GL_BLEND);
    }
    ...
}

最初に行うことは、インターフェースの実装をセットアップしたかどうかを確認することですIGuiInstance。インスタンスがない場合は、何もレンダリングする必要はなく、単に戻ります。その後、drawGuiメソッドを呼び出します。つまり、各レンダー呼び出しでそのメソッドを呼び出して、Imgui がそのステータスを更新し、適切な頂点データを生成できるようにします。シェーダーをバインドした後、まず透明度を使用できるブレンドを有効にします。ブレンディングを有効にするだけでは、透明度は表示されません。ブレンディングがどのように適用されるかについて、OpenGL に指示する必要もあります。これは、関数を介して行われますglBlendFunc。ここで適用できるさまざまな機能の詳細についての優れた説明を確認できます。

その後、Imgui が適切に機能するように深度テストと顔カリングを無効にする必要があります。次に、データの構造を定義する gui メッシュをバインドし、データとインデックス バッファーをバインドします。Imgui は画面座標を使用して頂点データを生成します。つまり、x値は[0, screen width]範囲をカバーし、y値は をカバーします[0, screen height]。ユニフォームを使用してscale、その座標系から[-1, 1]OpenGL のクリップ スペースの範囲にマップします。

その後、Imgui によって生成されたデータを取得して、GUI をレンダリングします。IMgui はまず、コマンド リストと呼ばれるものにデータを整理します。各コマンド リストには、頂点とインデックスのデータを格納するバッファーがあるため、最初に を呼び出してデータを GPU にダンプしますglBufferData。各コマンド リストは、ドロー コールを生成するために使用するコマンドのセットとしても定義します。各コマンドは、描画される要素の数とバッファに適用されるオフセットをコマンド リストに保存します。すべての要素を描画したら、深度テストを再度有効にできます。

resize最後に、 Imgui の表示サイズを調整するためにウィンドウのサイズが変更されるたびに呼び出されるメソッドを追加する必要があります。

public class GuiRender {
    ...
    public void resize(int width, int height) {
        ImGuiIO imGuiIO = ImGui.getIO();
        imGuiIO.setDisplaySize(width, height);
    }
}

UniformsMapの更新

UniformsMap2D ベクトルのサポートを追加するには、クラスを更新する必要があります。

public class UniformsMap {
    ...
    public void setUniform(String uniformName, Vector2f value) {
        glUniform2f(getUniformLocation(uniformName), value.x, value.y);
    }
}

scene.flagの更新

GUI のレンダリングに使用される頂点シェーダーは非常に単純です ( gui.vert)。座標が範囲内に収まるように座標を変換し[-1, 1]、フラグメント シェーダーで使用できるようにテクスチャ座標と色を出力するだけです。

#version 330

layout (location=0) in vec2 inPos;
layout (location=1) in vec2 inTextCoords;
layout (location=2) in vec4 inColor;

out vec2 frgTextCoords;
out vec4 frgColor;

uniform vec2 scale;

void main()
{
    frgTextCoords = inTextCoords;
    frgColor = inColor;
    gl_Position = vec4(inPos * scale + vec2(-1.0, 1.0), 0.0, 1.0);
}

フラグメント シェーダー ( gui.frag) では、テクスチャ座標に関連付けられた頂点カラーとテクスチャ カラーの組み合わせを出力するだけです。

#version 330

in vec2 frgTextCoords;
in vec4 frgColor;

uniform sampler2D txtSampler;

out vec4 outColor;

void main()
{
    outColor = frgColor  * texture(txtSampler, frgTextCoords);
}

すべてをまとめる

全体的には下のような感じです。

ここで、GUI をレンダリングするために、以前のすべての写真を接着する必要があります。最初に、新しいGuiRenderクラスをそのクラスに使用することから始めますRender。

public class Render {
    ...
    private GuiRender guiRender;
    ...
    public Render(Window window) {
        ...
        guiRender = new GuiRender(window);
    }

    public void cleanup() {
        ...
        guiRender.cleanup();
    }

    public void render(Window window, Scene scene) {
        ...
        guiRender.render(scene);
    }

    public void resize(int width, int height) {
        guiRender.resize(width, height);
    }
}

また、更新ループにEngine含めるようにクラスを変更し、その戻り値を使用して入力が消費されたかどうかを示す必要があります。IGuiInstance

public class Engine {
    ...
    public Engine(String windowTitle, Window.WindowOptions opts, IAppLogic appLogic) {
        ...
        render = new Render(window);
        ...
    }
    ...
    private void resize() {
        int width = window.getWidth();
        int height = window.getHeight();
        scene.resize(width, height);
        render.resize(width, height);
    }

    private void run() {
        ...
        IGuiInstance iGuiInstance = scene.getGuiInstance();
        while (running && !window.windowShouldClose()) {
            ...
            if (targetFps <= 0 || deltaFps >= 1) {
                boolean inputConsumed = iGuiInstance != null ? iGuiInstance.handleGuiInput(scene, window) : false;
                appLogic.input(window, scene, now - initialTime, inputConsumed);
            }
            ...
        }
        ...
    }
    ...
}

入力消費された戻り値を使用するようにインターフェイスを更新する必要もありますIAppLogic。

public interface IAppLogic {
    ...
    void update(Window window, Scene scene, long diffTimeMillis);
    ...
}

ImGuiの描画部分

最後に、IGuiInstance in the Main` クラスを実装します。

ImGuiはでも画面の作成メソッドが実装されています。それが ImGui.showDemoWindow();です。
画面の作成方法は、「ImGui」クラスからstaticメソッドを呼び出して作成します。
詳細はこちらの記事に記載しています。

ImGuiのハローワールド(デモ表示)

ImGUiをいじってみる

ハローワールドの次

public class Main implements IAppLogic, IGuiInstance {
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-10", new Window.WindowOptions(), main);
        ...
    }

    ...
    @Override
    public void drawGui() {
        ImGui.newFrame();
        ImGui.setNextWindowPos(0, 0, ImGuiCond.Always);
        ImGui.showDemoWindow();
        ImGui.endFrame();
        ImGui.render();
    }

    @Override
    public boolean handleGuiInput(Scene scene, Window window) {
        ImGuiIO imGuiIO = ImGui.getIO();
        MouseInput mouseInput = window.getMouseInput();
        Vector2f mousePos = mouseInput.getCurrentPos();
        imGuiIO.setMousePos(mousePos.x, mousePos.y);
        imGuiIO.setMouseDown(0, mouseInput.isLeftButtonPressed());
        imGuiIO.setMouseDown(1, mouseInput.isRightButtonPressed());

        return imGuiIO.getWantCaptureMouse() || imGuiIO.getWantCaptureKeyboard();
    }
    ...
    public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
        ...
    }
}

このdrawGuiメソッドでは、新しいフレームとウィンドウの位置を設定し、 を呼び出してshowDemoWindowImgui のデモ ウィンドウを生成するだけです。フレームを終了した後、これを呼び出すことが非常に重要ですrender。これは、以前に定義された GUI 構造で一連のコマンドを生成するものです。1handleGuiInputつ目は、マウスの位置を取得し、その情報とマウス ボタンの状態で IMgui の IO クラスを更新します。入力が Imgui によってキャプチャされたことを示すブール値も返します。最後に、そのフラグを受け取るようにメソッドを更新する必要がありますinput(まだ何もしていませんが、実装しているインターフェイスにあります)。

これらすべての変更により、Imgui デモ ウィンドウが回転する立方体に重なっているのを確認できます。パネルのさまざまな方法を操作して、Imgui の機能を垣間見ることができます。

動かしてみた(IntelliJ IDEA)

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。

コメントを残す