Debug UI

Add an interactive debug UI overlay with windows, buttons, sliders, and more.

Debug UI

The engine includes a built-in immediate-mode debug UI system inspired by Nuklear. It renders on top of your 3D scene and works across all backends (OpenGL, Vulkan, WebGPU).

The UI is immediate-mode — you describe the interface every frame in your update() method. No retained widget objects, no callbacks. Just if-statements.

State

In immediate-mode UI, you own all the state. The UI system just reads and writes it each frame. Declare your state as regular fields:

1
2
3
4
5
private float cubeScale = 1.0f;
    private float cubeX = 0, cubeY = 0, cubeZ = 0;
    private boolean wireframe = false;
    private int materialChoice = 0;
    private Entity cube;

Scene Setup

Create a simple scene with a cube. The debug UI renders on top of whatever your scene contains.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
    protected void init() {
        cube = scene().createEntity();
        cube.add(PrimitiveMeshes.cube());
        cube.add(MaterialData.unlit(new Vec3(0.3f, 0.5f, 0.8f)));
        cube.add(Transform.IDENTITY);

        var ground = scene().createEntity();
        ground.add(PrimitiveMeshes.plane(10, 10));
        ground.add(MaterialData.unlit(new Vec3(0.15f, 0.15f, 0.2f)));
        ground.add(Transform.at(0, -1, 0).withScale(10f));

        camera().lookAt(new Vec3(0, 2, 5), Vec3.ZERO, Vec3.UNIT_Y);
        camera().setPerspective((float) Math.toRadians(60), 16f / 9f, 0.1f, 100f);
    }

Drawing the UI

Access the UI context via debugUi(). Each frame, call begin()/end() to create windows, and add widgets between them.

Windows

begin() creates a window. Pass the title, position, size, and flags. It returns true if the window is visible (not collapsed or closed). Always call end() — even if begin() returned false.

1
2
3
4
5
6
7
@Override
    protected void update(float deltaTime) {
        NkContext ui = debugUi();

        if (ui.begin("Inspector", 10, 10, 250, 300,
                NkContext.WINDOW_BORDER | NkContext.WINDOW_MOVABLE
                | NkContext.WINDOW_TITLE | NkContext.WINDOW_MINIMIZABLE)) {

Layout

Before adding widgets, set the row layout. layoutRowDynamic creates rows where items share the width equally. The first argument is the row height, the second is the number of columns.

1
2
3
ui.layoutRowDynamic(20, 1);
            ui.label("Transform");
            ui.separator();

Property Editors

propertyFloat creates a drag-to-edit value with +/- buttons. You can also put multiple properties on one row:

1
2
3
4
5
6
7
ui.layoutRowDynamic(22, 3);
            cubeX = ui.propertyFloat("X", -5, cubeX, 5, 0.1f, 0.02f);
            cubeY = ui.propertyFloat("Y", -5, cubeY, 5, 0.1f, 0.02f);
            cubeZ = ui.propertyFloat("Z", -5, cubeZ, 5, 0.1f, 0.02f);

            ui.layoutRowDynamic(22, 1);
            cubeScale = ui.propertyFloat("Scale", 0.1f, cubeScale, 5, 0.1f, 0.01f);

Sliders and Checkboxes

1
2
3
4
5
6
ui.layoutRowDynamic(20, 1);
            ui.label("Options");
            ui.separator();

            ui.layoutRowDynamic(20, 1);
            wireframe = ui.checkbox("Wireframe", wireframe);

Combo Boxes

1
2
3
4
5
ui.layoutRowDynamic(22, 1);
            materialChoice = ui.combo(
                new String[]{"Blue", "Red", "Green"}, materialChoice, 18);
        }
        ui.end();

Applying UI State to the Scene

The UI just modifies your state variables. Apply them to the scene in the same update() method:

1
2
3
4
5
6
7
8
Vec3 color = switch (materialChoice) {
            case 1 -> new Vec3(0.8f, 0.2f, 0.2f);
            case 2 -> new Vec3(0.2f, 0.8f, 0.2f);
            default -> new Vec3(0.3f, 0.5f, 0.8f);
        };
        cube.add(MaterialData.unlit(color));
        cube.add(Transform.at(cubeX, cubeY, cubeZ).withScale(cubeScale));
    }

Running the Example

The launcher is the same as any engine application:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
        var toolkit = new GlfwWindowToolkit(GlfwWindowToolkit.OPENGL_HINTS);
        var config = EngineConfig.builder()
                .window(WindowDescriptor.builder("Debug UI Tutorial").size(800, 600).build())
                .platform(DesktopPlatform.builder().build())
                .graphicsBackend(OpenGlBackend.factory(toolkit,
                        new dev.engine.providers.lwjgl.graphics.opengl.LwjglGlBindings()))
                .build();
        new T01_DebugUI().launch(config);
    }

What’s Next?

The debug UI supports many more widgets:

  • Labelslabel(), labelColored(), aligned text
  • Buttonsbutton() returns true when clicked
  • SliderssliderFloat(), sliderInt()
  • Progress barsprogress()
  • Text inputeditString()
  • Color pickercolorPicker(), colorPalette()
  • TreestreePush()/treePop(), treeNode() with selection
  • SectionssectionBegin()/sectionEnd() accordion headers
  • GroupsgroupBegin()/groupEnd() scrollable areas
  • Chartschart() live value graphs
  • Tooltipstooltip() on hover

All styling is customizable via ui.style(). Windows can be dragged, minimized, closed, and brought to front by clicking.

See UiSamplerExample.java for a comprehensive showcase of every widget.