Jump to content

General Approach for a Desktop GUI with a sub-panel that renders 3D objects


MagnetarX10
 Share

Recommended Posts

I am new to Ultra Engine and never used Leadworks and have only a little bit of 3D programming experience. I looked as some youtube examples and read what I think are the relevant portions of the programming reference, but I am not 100% sure what to do.

I am trying to build something similar to the Editor interface I saw in the Gallery. I want a main window that has a left panel and a right panel with UI controls and a center panel that renders a 3D viewport. 

  • I have created the Window, FrameBuffer, World, and Interface Objects.
  • I have a Left and Right Panel on the Interface.
  • I setup 2 cameras, one for the 2D and one for the 3D Objects. 
  • I turned off the real-time rendering for the 2D camera.

I am sure this is simple, I don't know how to setup the 3D world for only that middle space inside the main window.  Currently, the 3D world draws over the 2D UI and I get a lot of flashing.

 

 

Link to comment
Share on other sites

This is definitely doable. Basically, you create a child window with no titlebar and manually set its shape any time the parent window moves or resizes. To make a resizable window with a framebuffer, you also need to call AsyncRender(false) at the very beginning of your program:
https://www.ultraengine.com/learn/AsyncRender?lang=cpp

This will make it so the rendering all takes place inside the World::Render() call, instead of running on a separate thread. This mode is for event-driven programs instead of games that are constantly rendering.

I'll put together an example for you tomorrow, but if you feel like taking a stab at it before then, look at the second example on this page:
https://github.com/UltraEngine/Documentation/blob/master/CPP/OpenGL.md

That is an old example using OpenGL with Ultra App Kit, but it's very similar to setting up a framebuffer in Ultra Engine.

I'll get a new example up tomorrow.

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

Here's an example on how to create a fully functioning application with a 3D viewport.

#include "UltraEngine.h"

using namespace UltraEngine;
const int TOOLBARHEIGHT = 48;
const int STATUSBARHEIGHT = 32;
const int SIDEPANELWIDTH = 300;
const int CONSOLEHEIGHT = 120;

int main(int argc, const char* argv[])
{
    AsyncRender(false);

    //Get displays
    auto displays = GetDisplays();

    //Create window
    auto mainwindow = CreateWindow("3D Viewport", 0, 0, 1024, 768, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR | WINDOW_RESIZABLE);
    mainwindow->SetMinSize(800, 600);

    //Create user interface
    auto ui = CreateInterface(mainwindow);
    iVec2 sz = ui->root->ClientSize();

    //-------------------------------------------------------
    // Create main menu
    //-------------------------------------------------------

    auto mainmenu = CreateMenu("", ui->root);

    //File menu
    auto menu_file = CreateMenu("File", mainmenu);
    CreateMenu("New", menu_file);
    CreateMenu("", menu_file);
    auto menu_open = CreateMenu("Open", menu_file);
    auto menu_save = CreateMenu("Save", menu_file);
    auto menu_saveas = CreateMenu("Save as...", menu_file);
    CreateMenu("", menu_file);
    auto menu_recentfiles = CreateMenu("Recent files", menu_file);
    std::array<shared_ptr<Widget>, 10> menu_recentfile;
    for (int n = 0; n < menu_recentfile.size(); ++n)
    {
        menu_recentfile[n] = CreateMenu("Recent file " + String(n + 1), menu_recentfiles);
    }
    CreateMenu("", menu_file);
    auto menu_exit = CreateMenu("Exit", menu_file);

    //Edit menu
    auto menu_edit = CreateMenu("Edit", mainmenu);
    CreateMenu("Undo", menu_edit);
    CreateMenu("Redo", menu_edit);
    CreateMenu("", menu_edit);
    CreateMenu("Cut", menu_edit);
    CreateMenu("Copy", menu_edit);
    CreateMenu("Past", menu_edit);
    CreateMenu("", menu_edit);
    CreateMenu("Select all", menu_edit);
    CreateMenu("Select none", menu_edit);
    CreateMenu("Invert selection", menu_edit);

    //View menu
    auto menu_view = CreateMenu("View", mainmenu);
    auto menu_perspective = CreateMenu("Perspective", menu_view);
    auto menu_top = CreateMenu("XZ - Top", menu_view);
    auto menu_side = CreateMenu("XZ - Side", menu_view);
    auto menu_front = CreateMenu("XY - Front", menu_view);
    menu_perspective->SetState(true);

    //Tools menu
    auto menu_tools = CreateMenu("Tools", mainmenu);
    auto menu_options = CreateMenu("Options", menu_tools);

    //Help menu
    auto menu_help = CreateMenu("Help", mainmenu);
    auto menu_helpcontents = CreateMenu("Help Contents", menu_help);
    auto menu_about = CreateMenu("About", menu_help);

    //-------------------------------------------------------
    // Create toolbar
    //-------------------------------------------------------

    auto toolbar = CreatePanel(0, mainmenu->size.y, sz.x, TOOLBARHEIGHT, ui->root);
    toolbar->SetLayout(1, 1, 1, 0);
    int x = 4, y = 4;

    auto toolbarbutton_open = CreateButton("", x, y, TOOLBARHEIGHT - 8, TOOLBARHEIGHT - 8, toolbar, BUTTON_TOOLBAR);
    toolbarbutton_open->SetFontScale(2);
    toolbarbutton_open->SetIcon(LoadIcon("https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Icons/open.svg"));
    x += TOOLBARHEIGHT;

    auto toolbarbutton_save = CreateButton("", x, y, TOOLBARHEIGHT - 8, TOOLBARHEIGHT - 8, toolbar, BUTTON_TOOLBAR);
    toolbarbutton_save->SetFontScale(2);
    toolbarbutton_save->SetIcon(LoadIcon("https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Icons/save.svg"));
    x += TOOLBARHEIGHT;

    auto toolbarbutton_options = CreateButton("", x, y, TOOLBARHEIGHT - 8, TOOLBARHEIGHT - 8, toolbar, BUTTON_TOOLBAR);
    toolbarbutton_options->SetFontScale(2);
    toolbarbutton_options->SetIcon(LoadIcon("https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Icons/settings.svg"));
    x += TOOLBARHEIGHT;

    auto toolbarbutton_help = CreateButton("", x, y, TOOLBARHEIGHT - 8, TOOLBARHEIGHT - 8, toolbar, BUTTON_TOOLBAR);
    toolbarbutton_help->SetIcon(LoadIcon("https://raw.githubusercontent.com/UltraEngine/Documentation/master/Assets/Icons/help.svg"));
    toolbarbutton_help->SetFontScale(2);

    //-------------------------------------------------------
    // Create status bar
    //-------------------------------------------------------

    auto statusbar = CreatePanel(0, sz.y - STATUSBARHEIGHT, sz.x, STATUSBARHEIGHT, ui->root);
    statusbar->SetLayout(1, 1, 0, 1);
    auto statusbarlabel_view = CreateLabel("Perspective", 4, 0, 300, statusbar->size.y, statusbar, LABEL_LEFT | LABEL_MIDDLE);

    //-------------------------------------------------------
    // Create main panel
    //-------------------------------------------------------

    auto mainpanel = CreatePanel(0, toolbar->position.y + toolbar->size.y, sz.x, sz.y - toolbar->size.y - toolbar->position.y - statusbar->size.y, ui->root);
    mainpanel->SetLayout(1, 1, 1, 1);
    sz = mainpanel->ClientSize();

    //Create console
    auto console = CreateTextArea(4, mainpanel->size.y - CONSOLEHEIGHT, mainpanel->size.x - SIDEPANELWIDTH - 8, CONSOLEHEIGHT - 28 - 4, mainpanel);
    console->SetLayout(1, 1, 0, 1);
    auto widget_input = CreateTextField(4, mainpanel->size.y - 28, mainpanel->size.x - SIDEPANELWIDTH - 8, 28, mainpanel, TEXTFIELD_ENTERKEYACTIONEVENT);
    widget_input->SetLayout(1, 1, 0, 1);

    //Main viewport
    auto mainviewport = CreatePanel(4, 4, mainpanel->size.x - SIDEPANELWIDTH - 8, mainpanel->size.y - 8 - CONSOLEHEIGHT, mainpanel, PANEL_BORDER);
    mainviewport->SetLayout(1, 1, 1, 1);
    mainviewport->SetColor(0, 0, 0);

    auto viewport = CreateWindow("", mainviewport->GetPosition(true).x, mainviewport->GetPosition(true).y, mainviewport->GetSize().x, mainviewport->GetSize().y, mainwindow, WINDOW_CHILD | WINDOW_CLIENTCOORDS | WINDOW_RESIZABLE);

    //Create a world
    auto world = CreateWorld();

    //Create a framebuffer
    auto framebuffer = CreateFramebuffer(viewport);

    //Create a camera
    auto camera = CreateCamera(world);
    camera->SetClearColor(0.125);
    camera->SetFov(70);
    camera->SetPosition(0, 0, -3);

    //Create a light
    auto light = CreateDirectionalLight(world);
    light->SetRotation(35, 45, 0);

    //Create a box
    auto box = CreateBox(world);


    //-------------------------------------------------------
    // Create side panel
    //-------------------------------------------------------   

    auto sidepanel = CreatePanel(sz.x - SIDEPANELWIDTH, 0, SIDEPANELWIDTH, sz.y, mainpanel);
    sidepanel->SetLayout(0, 1, 1, 1);
    auto tabber = CreateTabber(0, 0, SIDEPANELWIDTH, sz.y, sidepanel);
    tabber->SetLayout(1, 1, 1, 1);
    tabber->AddItem("Objects", true);
    tabber->AddItem("Scene");

    //Object panel
    sz = tabber->ClientSize();
    auto objectpanel = CreatePanel(0, 0, sz.x, sz.y, tabber);
    objectpanel->SetLayout(1, 1, 1, 1);
    //tabber->items[0].extra = objectpanel;

    //Scene panel
    auto scenepanel = CreatePanel(0, 0, sz.x, sz.y, tabber);
    scenepanel->SetHidden(true);
    scenepanel->SetLayout(1, 1, 1, 1);
    //tabber->items[1].extra = scenepanel;

    x = 8;
    y = 12;

    CreateLabel("Category:", x, y, 200, 30, objectpanel);
    y += 24;
    auto objectcategorybox = CreateComboBox(x, y, sz.x - x * 2, 30, objectpanel);
    objectcategorybox->SetLayout(1, 1, 1, 0);
    objectcategorybox->AddItem("Primitives", true);
    objectcategorybox->AddItem("Extended primitives");
    objectcategorybox->AddItem("Cameras");
    objectcategorybox->AddItem("Lights");
    objectcategorybox->AddItem("Splines");
    y += 44;

    CreateLabel("Object:", x, y, 200, 30, objectpanel);
    y += 24;
    auto objectbox = CreateComboBox(x, y, sz.x - x * 2, 30, objectpanel);
    objectbox->SetLayout(1, 1, 1, 0);
    objectbox->AddItem("Box", true);
    objectbox->AddItem("Wedge");
    objectbox->AddItem("Cylinder");
    objectbox->AddItem("Sphere");
    y += 44;

    x = 80;
    CreateButton("Create", x, y, sz.x - 2 * x, 28, objectpanel);

    x = 8;
    y = 12;
    auto scenebrowser = CreateTreeView(x, y, sz.x - 2 * x, 400 - y, scenepanel);
    scenebrowser->SetLayout(1, 1, 1, 1);
    auto node = scenebrowser->root->AddNode("Scene");
    node->Expand();
    node->AddNode("Box 1");
    node->AddNode("Box 2");
    node->AddNode("Box 3");
    y += scenebrowser->size.y + x;

    auto propertiespanel = CreatePanel(x, y, sz.x, sz.y - y, scenepanel);
    propertiespanel->SetLayout(1, 1, 0, 1);

    y = 8;
    CreateLabel("Name:", x, y + 4, 60, 30, propertiespanel);
    auto widget_name = CreateTextField(x * 2 + 60, y, sz.x - 4 * x - 60, 30, propertiespanel);
    widget_name->SetText("Box 1");
    y += 40;

    CreateLabel("Value:", x, y + 4, 60, 30, propertiespanel);
    CreateSlider(x * 2 + 60, y, sz.x - 4 * x - 60, 30, propertiespanel, SLIDER_HORIZONTAL | SLIDER_TRACKBAR);
    y += 40;

    //-------------------------------------------------------
    // Options window
    //-------------------------------------------------------

    auto optionswindow = CreateWindow("Options", 0, 0, 400, 500, mainwindow, WINDOW_HIDDEN | WINDOW_TITLEBAR | WINDOW_CENTER);
    auto optionsui = CreateInterface(optionswindow);
    sz = optionsui->root->ClientSize();

    auto button_option1 = CreateButton("Option 1", 12, 12, 300, 30, optionsui->root, BUTTON_CHECKBOX);
    button_option1->SetState(WIDGETSTATE_SELECTED);
    auto button_option2 = CreateButton("Option 2", 12, 12 + 32, 300, 30, optionsui->root, BUTTON_RADIO);
    button_option2->SetState(WIDGETSTATE_SELECTED);
    auto button_option3 = CreateButton("Option 3", 12, 12 + 32 * 2, 300, 30, optionsui->root, BUTTON_RADIO);

    auto button_applyoptions = CreateButton("OK", sz.x - 2 * (8 + 80), sz.y - 8 - 30, 80, 30, optionsui->root, BUTTON_OK);
    auto button_closeoptions = CreateButton("Cancel", sz.x - 8 - 80, sz.y - 8 - 30, 80, 30, optionsui->root, BUTTON_CANCEL);

    //-------------------------------------------------------
    // Main loop
    //-------------------------------------------------------

    while (true)
    {
        while (PeekEvent())
        {
            const Event event = WaitEvent();
            switch (event.id)
            {
            case EVENT_WINDOWSIZE:
                if (event.source == mainwindow)
                {
                    // If the window resize event is captured
                    auto window = event.source->As<UltraEngine::Window>();

                    // Get the new size of the applications window
                    UltraEngine::iVec2 sz = mainwindow->ClientSize();

                    // Set the position and size of the viewport window
                    viewport->SetShape(mainviewport->GetPosition(true).x, mainviewport->GetPosition(true).y, mainviewport->GetSize().x, mainviewport->GetSize().y);
                }
                break;

            case EVENT_PRINT:
                console->AddText(event.text + "\n");
                break;

            case EVENT_WIDGETACTION:
                if (event.source == menu_exit)
                {
                    EmitEvent(EVENT_WINDOWCLOSE, mainwindow);
                }
                else if (event.source == menu_open)
                {
                    RequestFile("Open File");
                }
                else if (event.source == menu_save or event.source == menu_saveas)
                {
                    RequestFile("Save File", "", "All Files", 0, true);
                }
                else if (event.source == menu_helpcontents)
                {
                    RunFile("https://www.ultraengine.com/learn");
                }
                else if (event.source == menu_about)
                {
                    Notify("3D Viewport");
                }
                else if (event.source == menu_perspective or event.source == menu_top or event.source == menu_side or event.source == menu_front)
                {
                    menu_perspective->SetState(WIDGETSTATE_UNSELECTED);
                    menu_top->SetState(WIDGETSTATE_UNSELECTED);
                    menu_side->SetState(WIDGETSTATE_UNSELECTED);
                    menu_front->SetState(WIDGETSTATE_UNSELECTED);
                    auto menuitem = event.source->As<Widget>();
                    menuitem->SetState(WIDGETSTATE_SELECTED);
                    statusbarlabel_view->SetText(menuitem->text);
                }
                else if (event.source == toolbarbutton_open)
                {
                    EmitEvent(EVENT_WIDGETACTION, menu_open);
                }
                else if (event.source == toolbarbutton_save)
                {
                    EmitEvent(EVENT_WIDGETACTION, menu_save);
                }
                else if (event.source == toolbarbutton_options)
                {
                    EmitEvent(EVENT_WIDGETACTION, menu_options);
                }
                else if (event.source == toolbarbutton_help)
                {
                    EmitEvent(EVENT_WIDGETACTION, menu_helpcontents);
                }
                else if (event.source == widget_input)
                {
                    if (!widget_input->text.empty())
                    {
                        console->AddText(widget_input->text + "\n");
                        widget_input->SetText("");
                    }
                    widget_input->Activate();
                }
                else if (event.source == menu_options)
                {
                    optionswindow->SetHidden(false);
                    optionswindow->Activate();
                    mainwindow->Disable();
                }
                else if (event.source == button_applyoptions or event.source == button_closeoptions)
                {
                    EmitEvent(EVENT_WINDOWCLOSE, optionswindow);
                }
                break;

            case EVENT_WIDGETSELECT:
                if (event.source == tabber)
                {
                    for (int n = 0; n < tabber->items.size(); ++n)
                    {
                        if (n == event.data)
                        {
                            objectpanel->SetHidden(true);
                            scenepanel->SetHidden(false);
                        }
                        else
                        {
                            objectpanel->SetHidden(false);
                            scenepanel->SetHidden(true);
                        }
                    }
                }
                break;

            case EVENT_WINDOWCLOSE:
                if (event.source == mainwindow)
                {
                    if (Confirm("Are you sure you want to quit?"))
                    {
                        return 0;
                    }
                }
                else if (event.source == optionswindow)
                {
                    mainwindow->Enable();
                    mainwindow->Activate();
                    optionswindow->SetHidden(true);
                }
            }
        }
        box->Turn(0, 45 / 60.0f, 0);
        world->Update();
        world->Render(framebuffer);
    }
    return 0;
}

 

  • Like 1
  • Thanks 1

Cyclone - Ultra Game System - Component PreprocessorTex2TGA - Darkness Awaits Template (Leadwerks)

If you like my work, consider supporting me on Patreon!

Link to comment
Share on other sites

@reepblue Thanks! This is very, very helpful.

I had worked on this a bit and got it largely working based on @Josh's info. 

The nice thing about using the ListenEvent() callback is that the 3D Scene will remain visible while the Application Window is resized. I also called Update() to keep it rotating, but I am not sure if that is a bad idea.

I added some buttons to change the rotation of the cube, but I do not understand is why it can take a few clicks to get the buttons to work. I assume there is a better way to get the button click or I need to select it based on mouse over?

Also, it has been a while since I have programmed C++ ( over a decade), so I don't remember if there is a way to cast the "shared_ptr<Object> extra" parameter in the ListenEvent() callback to a struct or class of objects instead of the single pointer so I don't need to setup the panels and world objects as globals.

#include "UltraEngine.h"
#include "ComponentSystem.h"

using namespace UltraEngine;

const int WIN_WIDTH = 1000;
const int WIN_HEIGHT = 1000;
const int PANEL_WIDTH = 200;
const int PADDING = 10;

//global "cheats" to pass more data into the resize callback
shared_ptr<Window> viewport;
shared_ptr<World> world;
shared_ptr<Framebuffer> framebuffer;
shared_ptr<Widget> left_panel;
shared_ptr<Widget> right_panel;

//-------------------------------------------------------
// Callback to handle dynamic resizing of the Viewport
// and panels
//-------------------------------------------------------

bool ResizeViewport(const Event& ev, shared_ptr<Object> extra) 
{
    // If the window resize event is captured
    auto window = ev.source->As<Window>();

    // Get the new size of the applications window
    iVec2 win_sz = window->ClientSize();

    auto viewport = extra->As<Window>();

    // Set the position and size of the viewport window AND the panels
    viewport->SetShape(PANEL_WIDTH + PADDING, PADDING, win_sz.x - (PANEL_WIDTH*2) - (PADDING * 2), win_sz.y - PADDING);
    if (left_panel != NULL)
        left_panel->SetShape(0, 0, PANEL_WIDTH, win_sz.y - PADDING);

    if (right_panel != NULL)
        right_panel->SetShape(win_sz.x - PANEL_WIDTH, 0, PANEL_WIDTH, win_sz.y - PADDING);

    //Render the Viewport to keep the 3D scene drawing during the resize
    world->Update();
    world->Render(framebuffer);

    return true;
}


int main(int argc, const char* argv[])
{
    //Disable multithreaded rendering for RESIZABLE Windows
    AsyncRender(false); 

    auto displays = GetDisplays();
    auto window = CreateWindow("Ultra Engine Test", 0, 0, WIN_WIDTH, WIN_HEIGHT, displays[0], WINDOW_TITLEBAR | WINDOW_RESIZABLE);
    auto font = LoadFont("Fonts/Arial.ttf");

    //-------------------------------------------------------
    // Create 2D User Interface
    //-------------------------------------------------------

    auto ui = CreateInterface(window);
    iVec2 win_sz = ui->root->ClientSize();
    left_panel = CreatePanel(0, 0, PANEL_WIDTH, win_sz.y - PADDING, ui->root);
    left_panel->SetColor(0.20f, 0.20f, 0.20f, 1);
    left_panel->SetLayout(1, 1, 1, 1);
    right_panel = CreatePanel(win_sz.x - PANEL_WIDTH, 0, PANEL_WIDTH, win_sz.y - PADDING, ui->root);
    right_panel->SetColor(0.20f, 0.20f, 0.20f, 1);
    right_panel->SetLayout(1, 1, 1, 1);
    //Add Buttons
    iVec2 left_sz = left_panel->ClientSize();
    iVec2 right_sz = right_panel->ClientSize();
    auto rotate_left_btn = CreateButton("Rotate Left", left_sz.x / 2 - 75, left_sz.y / 2 - 15, 150, 30, left_panel);
    auto rotate_right_btn = CreateButton("Rotate Right", right_sz.x / 2 - 75, right_sz.y / 2 - 15, 150, 30, right_panel);

    //-------------------------------------------------------
    // Create 3D Viewport
    //-------------------------------------------------------
    viewport = CreateWindow("", PANEL_WIDTH + PADDING, PADDING, win_sz.x - (PANEL_WIDTH*2) - (PADDING*2), win_sz.y - PADDING, window, WINDOW_CHILD);
    framebuffer = CreateFramebuffer(viewport);
    world = CreateWorld();

    // Adjust the size of the viewport when the applications window is resized (this will callback to the ResizeViewport() function)
    ListenEvent(EVENT_WINDOWSIZE, window, ResizeViewport, viewport);

    //-------------------------------------------------------
    // Setup 3D Scene
    //-------------------------------------------------------

    //Setup Camera
    auto camera_3D = CreateCamera(world);
    camera_3D->SetClearColor(0.125);
    camera_3D->SetFov(50);
    camera_3D->SetPosition(0, 0, -3);
    //Create a light
    auto light = CreateBoxLight(world);
    light->SetRotation(35, 45, 0);
    light->SetRange(-10, 10);
    //Create a box
    auto box = CreateBox(world);
    box->SetColor(0, 0, 1);
    //Entity component system
    auto actor = CreateActor(box);
    auto component = actor->AddComponent<Mover>();
    //Rotate the Box
    component->rotation.y = 45;

    //-------------------------------------------------------
    // Main Loop
    //-------------------------------------------------------

    while (true)
    {
        while (PeekEvent())
        {
            const Event ev = WaitEvent();
            switch (ev.id)
            {
            case EVENT_WIDGETACTION:
                //Set the rotation based on the button click
                if (ev.source == rotate_right_btn)
                    component->rotation.y = -45;
                else if (ev.source == rotate_left_btn)
                    component->rotation.y = +45;
                break;
            case EVENT_WINDOWCLOSE:
                if (ev.source == window)
                    return 0;
                break;
            default:
                ui->ProcessEvent(ev);
                break;
            }
        }

        world->Update();
        world->Render(framebuffer);
   
    }
    return 0;
}

 

Link to comment
Share on other sites

@reepblue I looked at your code again.  I am trying to figure out why it does not work the same as the ListenEvent() callback version I have. I assume that WaitEvent() returns an EVENT_WINDOWSIZE event on a different frequency than the ListenEvent() gets called. Your loop drops down to the Update() and Render() calls which seems like it should be functionally equivalent, but I see a dark window during the resize in your version. This means there is a difference.

It has been a very long time since I did Windows programming but maybe the  WaitEvent() triggers in the messages that signal a begin and end to the sizing rather than getting message for WM_SIZING which (I think) happens continuously as the user is resizing the window.

Link to comment
Share on other sites

Okee-dokee, I have updated the documentation with another example that shows how to create an event-driven desktop application with a 3D viewport embedded in the interface:. See the third example here:
https://www.ultraengine.com/learn/CreateInterface?lang=cpp

Modal Loops 

When a window is resized, the system goes into a loop and doesn't come out of that loop until the mouse is released. This is a "blocking" or modal loop. Windows, Linux, and Mac all work like this. It's an OS-level thing, and I think it must be done to make window resizing as snappy and responsive as possible. If you are using ListenEvent you will intercept WINDOWSIZE events during the loop, but if you are using WaitEvent() you won't see anything until the mouse is let go. Therefore, in order to update the viewport window as the window is being resized, you must use an event listener.

When to Render 

In my OpenGL example linked to above I redraw the viewport every time a WINDOWSIZE event occurs, inside the system loop. This is probably not a good idea with Vulkan, because every time the viewport changes size it has to allocate a new color and depth buffer. (OpenGL might also do this, but if so it is hidden from the user.) It's better to resize the viewport dynamically, but only re-render the scene once the resize operation is done, the modal loop exits, and the program execution goes back to the main loop (when the WaitEvent call  finishes). If you just resize the viewport without rendering again, the appearance will vary depending on the driver / manufacturer of your GPU. Nvidia drivers will stretch the image and Intel drivers will leave the image as-is, with a "hall of mirrors" effect if the window is made bigger. Either of these behaviors is fine.

Minimizing Renders 📷

When you do resize a window, you will see a bunch of WINDOWPAINT events coming out of WaitEvent(), one for each mouse movement you made. If you just render the viewport every time a WINDOWPAINT event occurs, it will make your program unresponsive for a moment after you resize the window, because it has to render the viewport 100 times for no good reason. To prevent this from happening, the example keeps track of the viewport state with the "dirty" variable. If a WINDOWPAINT event occurs and the viewport has not been invalidated yet, the viewport is invalidated (the "dirty" variable is set to true) and a custom VIEWPORTRENDER event is emitted. Since this event goes to the end of the event queue, it will be evaluated last, after all the remaining WINDOWPAINT events are skipped. The result is the viewport will only render once each time the window is resized. When you run the example, the printed output will show exactly what is happening.

A Bug! 🐛 🤯

Finally, this example is producing a crash on exit, so I will take a closer look today and get that fixed for you. Fix is available on branch 1.0.1!

  • Like 2

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

9 hours ago, MagnetarX10 said:

Also, it has been a while since I have programmed C++ ( over a decade), so I don't remember if there is a way to cast the "shared_ptr<Object> extra" parameter in the ListenEvent() callback to a struct or class of objects instead of the single pointer so I don't need to setup the panels and world objects as globals.

You can create a new class derived from the object class like this:

class Viewport : public Object
{
public:
	shared_ptr<Panel> panel;
	shared_ptr<Window> window;
	bool dirty;
	
	Viewport()
	{
		dirty = false;
	}
};

auto viewport = make_shared<Viewport>();

This class can be cast to an Object, so it can be supplied in the extra parameter of ListenEvent().

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

I'm trying to produce an error like this, but it's completely stable on my Nvidia card. Is there any reliable way to make it happen? Do you have version 1.0.1 of the engine installed? It will show the version you're on in the Updates tab in the client app.

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

Here is a video. I think it is when I click outside of the Viewport region. I think I was doing this accidently initially and did not notice. My assumption is that the viewport is getting invalidated but it does not seem to be sent a render message. I added a little bit of code to track which render event was being sent so I could easily tell when a new render message we sent:

 case EVENT_VIEWPORTRENDER:
            world->Render(framebuffer);
            dirty = false;
            std::stringstream sstm;
            sstm << "Viewport render (count=" << ++renderCount << ")";
            Print(sstm.str());
            break;

 

Link to comment
Share on other sites

I may have done this incorrectly, but I updated my Ultra Engine Client to 1.0.1. Then I created a new project. Then I copied and pasted the code from example 3 in Create Instance into main.cpp and rebuilt the project, but I am getting the same behavior as before.

I tried it twice and restarted the Ultra Engine Client in case that is a requirement after an update.

Link to comment
Share on other sites

I have the same issue with your EXE. 

Video link: https://drive.google.com/file/d/1VxprAr7jaH9Ne5sWD-Ecmea1TBPF3Rp4/view?usp=share_link

Something about my system, I guess. I could have some weird version issue with drivers or libs. I have two monitors. One is a wide screen monitor. I have seen other issues in the past that were particular to a wide monitor, but this happens on both of my screens.

Not sure if any of this information is useful, but here it is:

image.thumb.png.9167b93cb8eec4b89c737d47b6e4a849.png

image.thumb.png.bdd19991a0e52191444f64e64271fa33.png

image.thumb.png.48cce620785ef081315c071b1dad22ab.png

 

 

image.thumb.png.90cfd5130fb2b84be6d1bde69b066bb6.png

Link to comment
Share on other sites

@reepblue's code seems to work great, because it avoids trying to solve the issue of "when to re-render". I think this is the preferred way to go about an event driven system like this on Windows.

7 hours ago, MagnetarX10 said:

I may have done this incorrectly, but I updated my Ultra Engine Client to 1.0.1. Then I created a new project. Then I copied and pasted the code from example 3 in Create Instance into main.cpp and rebuilt the project, but I am getting the same behavior as before.

I tried it twice and restarted the Ultra Engine Client in case that is a requirement after an update.

I feel the Example 3 code is missing a lot of common render invalidation events (conceptually speaking), which are causing the issues you are seeing. I ran the example and can reproduce the exact issues you are having, so I think it's code related and not hardware/driver related. For reference, I have a RTX 3080 TI, also using 527.56 drivers running 2x 4k monitors.

For example, on WIndows, I feel you should dirty the render view to cause a redraw when:

  • The app is moved
  • The app is resized
  • When the parent window repaints

Consider the following modified Example 3:

#include "UltraEngine.h"

using namespace UltraEngine;

const int SidePanelWidth = 200;
const int Indent = 8;

// Callback function for resizing the viewport
bool ResizeViewport(const Event& ev, shared_ptr<Object> extra)
{
    // If the window resize event is captured
    auto window = ev.source->As<Window>();

    // Get the new size of the applications window
    iVec2 sz = window->ClientSize();

    auto viewport = extra->As<Window>();

    // Set the position and size of the viewport window
    viewport->SetShape(SidePanelWidth, Indent, sz.x - SidePanelWidth - Indent, sz.y - Indent * 2);

    return true;
}

//Custom event ID
const EventId EVENT_VIEWPORTRENDER = EventId(101);

int main(int argc, const char* argv[])
{
    // Disable asynchronous rendering so window resizing will work with 3D graphics
    AsyncRender(false);

    // Get the available displays
    auto displays = GetDisplays();

    // Create a window
    auto window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR | WINDOW_RESIZABLE);

    // Create user interface
    auto ui = CreateInterface(window);

    // Get the size of the user interface
    iVec2 sz = ui->background->ClientSize();

    // Create a treeview widget
    auto treeview = CreateTreeView(Indent, Indent, SidePanelWidth - Indent * 2, sz.y - Indent * 2, ui->root);

    // Anchor left, top and bottom of treeview widget
    treeview->SetLayout(1, 0, 1, 1);

    // Add nodes to the treeview widget
    treeview->root->AddNode("Object 1");
    treeview->root->AddNode("Object 2");
    treeview->root->AddNode("Object 3");

    // Create a viewport window
    auto viewport = CreateWindow("", SidePanelWidth, Indent, sz.x - SidePanelWidth - Indent, sz.y - Indent * 2, window, WINDOW_CHILD);

    // Adjust the size of the viewport when the applications window is resized (this will callback to our ResizeViewport() function)
    ListenEvent(EVENT_WINDOWSIZE, window, ResizeViewport, viewport);

    // Create a framebuffer
    auto framebuffer = CreateFramebuffer(viewport);

    // Create a world
    auto world = CreateWorld();

    // Create a camera
    auto camera = CreateCamera(world);
    camera->SetClearColor(0.125);
    camera->SetPosition(0, 0, -4);

    // Create a light
    auto light = CreateBoxLight(world);
    light->SetRotation(35, 45, 0);
    light->SetRange(-10, 10);

    // Create a model
    auto model = CreateSphere(world);
    model->SetColor(0, 0, 1);

    // This varialble will be used for viewport refreshing
    bool dirty = false;

    // Main loop
    while (true)
    {
        // Wait for event
        const Event ev = WaitEvent();

        // Evaluate event
        switch (ev.id)
        {
        case EVENT_WINDOWMOVE:
            Print("Window move");
            if (not dirty)
            {
                dirty = true;
                EmitEvent(EVENT_VIEWPORTRENDER, viewport);
                Print("viewport refresh");
            }
            break;
        case EVENT_WINDOWSIZE:
            Print("Window size");
            if (not dirty)
            {
                dirty = true;
                EmitEvent(EVENT_VIEWPORTRENDER, viewport);
                Print("viewport refresh");
            }
            break;
            //Close window when escape key is pressed
        case EVENT_KEYDOWN:
            if (ev.source == window and ev.data == KEY_ESCAPE) return 0;
            break;

        case EVENT_WINDOWCLOSE:
            if (ev.source == window) return 0;
            break;

        case EVENT_WINDOWPAINT:
            //if (ev.source == viewport)
            {
                Print("Window paint");
                if (not dirty)
                {
                    // This prevents excessive paint events from building up, especially during window resizing
                    // This event is added to the end of the event queue, so if a lot of paint events build up, it will 
                    // only cause a single render to be performed.
                    dirty = true;
                    EmitEvent(EVENT_VIEWPORTRENDER, viewport);
                    Print("viewport refresh");
                }
            }
            break;

        case EVENT_VIEWPORTRENDER:
            world->Render(framebuffer);
            dirty = false;
            Print("Viewport render");
            break;
        }
    }
    return 0;
}

This should solve the recent issues you've posted about because:

  • If you move the Windows offscreen and back on, that counts as a move, which triggers a render, ensuring the viewport doesn't stay invalidated from being offscreen.
  • When you resize the window, the render view gets invalided as expected during the resize itself, but it will redraw once you complete the resize.
  • When you click in the empty space between the two windows, the parent window gets a paint message in such a way the child is not currently invalidated, but should be, which is why the viewport disappears. That behavior is fixed by not checking the message source in the code for the paint message, which should also fix other invalidation issues stemming from the parent.

On one hand, maybe the engine could get some event processing changes to specifically address some of these issues, but from my experiences trying to track down and understand obscure Windows behaviors when it comes to events is usually not worth it. I just think it's far easier to model your code like reepblue did, and just avoid most of those issues in the first place by always updating/rendering.

The other alternative is to possibly render selectively to an image file, and then use simpler, but more comprehensive Win32 message processing to ensure only the minimal amount of redrawing happens outside of a rendering context where you can manage the HDC of a single HWND and not deal with multiple windows and different behaviors of messages across a parent/child setup. Doesn't sound like that's what you're after here though, but if you wanted to minimize 3d rendering due to needing to limit graphics resource usage, I'd consider something like that maybe.

  • Thanks 1
Link to comment
Share on other sites

Windows should emit a paint event whenever a window is invalidated, whether that comes from resizing or something else. But if you are saying you were able to produce and fix the problem then I can't argue with that.

My job is to make tools you love, with the features you want, and performance you can't live without.

Link to comment
Share on other sites

6 hours ago, Josh said:

Windows should emit a paint event whenever a window is invalidated, whether that comes from resizing or something else. But if you are saying you were able to produce and fix the problem then I can't argue with that.

I'm on Windows 10, and the WM_PAINT did happen on invalidation when clicking in the middle of the thin border between windows, but just for the parent window and not the child window. I had some code to PeekMessage and check what was going on before UltraEngine processed the events, so that's how I arrived at the conclusion to not check the window source.

I can't attach a MP4 file, but here's an example: https://gyazo.com/71f17118ad6d81f388b0f748257570cc

I wonder if making the main window the render window, and a second window the UI window would result in the intended behavior without additional modifications?

Link to comment
Share on other sites

@Josh Thanks for the help.  Here is a version that combines the OpenGL example. This seems to be working fine. This is exactly what I need.

 

#include "UltraEngine.h"
#include <GL/GL.h>
#pragma comment (lib, "opengl32.lib")

using namespace UltraEngine;

const int SidePanelWidth = 200;
const int Indent = 8;

//Custom event ID
const EventId EVENT_VIEWPORTRENDER = EventId(101);

void GLRender(shared_ptr<Window> viewport)
{
    // Get and set the current size of the viewport
    iVec2 sz = viewport->ClientSize();
    if (sz.x < 1 or sz.y < 1) return;

    glViewport(0, 0, sz.x, sz.y);

    // Set clear colour of viewport background
    glClearColor(0.15f, 0.15f, 0.15f, 1.0f);

    // Clear colour and depth buffers
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Render our triangle
    glBegin(GL_TRIANGLES);

    // Vertex 1
    glColor3f(1, 0, 0);
    glVertex3f(0, 0.5, 0);

    // Vertex 2
    glColor3f(0, 1, 0);
    glVertex3f(0.5, -0.5, 0);

    // Vertex 3
    glColor3f(0, 0, 1);
    glVertex3f(-0.5, -0.5, 0);

    glEnd();

    HWND hwnd = viewport->GetHandle();
    auto hdc = GetDC(hwnd);
    SwapBuffers(hdc);
    ReleaseDC(hwnd, hdc);
}

// Callback function for resizing the viewport
bool ResizeViewport(const Event& ev, shared_ptr<Object> extra)
{
    // If the window resize event is captured
    auto window = ev.source->As<Window>();

    // Get the new size of the applications window
    iVec2 sz = window->ClientSize();

    auto viewport = extra->As<Window>();

    // Set the position and size of the viewport window
    viewport->SetShape(SidePanelWidth, Indent, sz.x - SidePanelWidth - Indent, sz.y - Indent * 2);

    return true;
}

int main(int argc, const char* argv[])
{
    // Disable asynchronous rendering so window resizing will work with 3D graphics
    AsyncRender(false);

    // Get the available displays
    auto displays = GetDisplays();

    // Create a window
    auto window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR | WINDOW_RESIZABLE);

    // Create user interface
    auto ui = CreateInterface(window);

    // Get the size of the user interface
    iVec2 sz = ui->background->ClientSize();

    // Create a treeview widget
    auto treeview = CreateTreeView(Indent, Indent, SidePanelWidth - Indent * 2, sz.y - Indent * 2, ui->root);

    // Anchor left, top and bottom of treeview widget
    treeview->SetLayout(1, 0, 1, 1);

    // Add nodes to the treeview widget
    treeview->root->AddNode("Object 1");
    treeview->root->AddNode("Object 2");
    treeview->root->AddNode("Object 3");

    // Create a viewport window
    auto viewport = CreateWindow("", SidePanelWidth, Indent, sz.x - SidePanelWidth - Indent, sz.y - Indent * 2, window, WINDOW_CHILD);

    // Adjust the size of the viewport when the applications window is resized (this will callback to our ResizeViewport() function)
    ListenEvent(EVENT_WINDOWSIZE, window, ResizeViewport, viewport);

    // Initialize an OpenGL context (get a hdc)
    HWND hwnd = (HWND)(viewport->GetHandle());
    HDC hdc = GetDC(hwnd);

    // Specify the format of the default framebuffer
    PIXELFORMATDESCRIPTOR pfd =
    {
        sizeof(PIXELFORMATDESCRIPTOR),
        1,

        // Flags
        PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,

        // Framebuffer colour format (R, G, B, A)
        PFD_TYPE_RGBA,

        // Framebuffer colour depth (32 bit)
        32,
        0, 0, 0, 0, 0, 0,
        0,
        0,
        0,
        0, 0, 0, 0,

        // Number of bits for depth-buffer
        24,

        // Number of bits for stencil-buffer
        8,

        // Number of render-targets in default framebuffer
        0,
        PFD_MAIN_PLANE,
        0,
        0, 0, 0
    };

    // Select an appropriate pixel format that is supported by the hdc
    int format = ChoosePixelFormat(hdc, &pfd);

    if (SetPixelFormat(hdc, format, &pfd) == 0)
    {
        RuntimeError("SetPixelFormat() failed.");
    }

    // Create an OpenGL rendering context using our current hdc
    HGLRC glcontext = wglCreateContext(hdc);

    if (glcontext == NULL)
    {
        RuntimeError("wglCreateContext() failed.");
    }

    wglMakeCurrent(hdc, glcontext);

    // This varialble will be used for viewport refreshing
    bool dirty = false;

    // Main loop
    while (true)
    {
        // Wait for event
        const Event ev = WaitEvent();

        // Evaluate event
        switch (ev.id)
        {
        case EVENT_WINDOWMOVE:
            if (not dirty)
            {
                dirty = true;
                EmitEvent(EVENT_VIEWPORTRENDER, viewport);
            }
            break;
        case EVENT_WINDOWSIZE:
            if (not dirty)
            {
                dirty = true;
                EmitEvent(EVENT_VIEWPORTRENDER, viewport);
            }
            break;
            //Close window when escape key is pressed
        case EVENT_KEYDOWN:
            if (ev.source == window and ev.data == KEY_ESCAPE) return 0;
            break;

        case EVENT_WINDOWCLOSE:
            if (ev.source == window) return 0;
            break;

        case EVENT_WINDOWPAINT:
        {
            if (not dirty)
            {
                // This prevents excessive paint events from building up, especially during window resizing
                // This event is added to the end of the event queue, so if a lot of paint events build up, it will 
                // only cause a single render to be performed.
                dirty = true;
                EmitEvent(EVENT_VIEWPORTRENDER, viewport);
            }
        }
        break;

        case EVENT_VIEWPORTRENDER:
            GLRender(viewport);
            dirty = false;
            break;
        }
    }
    return 0;
}

 

  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...