NoesisGUI

NoesisGUI Integration Tutorial

github Tutorial Data

This tutorial focuses on the steps you must follow to integrate NoesisGUI in your own application and render interfaces with it. Although we provide an open source framework for using NoesisGUI in all our supported platforms, the Application Framework, this guide will show you the minimal steps to achieve it.

Note

Integration and IntegrationGLUT are fully annotated samples included inside the SDK. It is highly recommended following those samples when reading this guide.

Prerequisites

This tutorial assumes that you are familiar about the following:

SDK Directories

The SDK is structured using the following root folders:

  • /Bin: this is the directory where Noesis dynamic library can be found. Your executable must be able to reach this path. The easiest way is by copying it to the executable location in a post-build step.
  • /Include: directory for public headers. You must add this path to the Additional Include Directories of your project
  • /Lib: object library to link against with is stored in this directory. Like the include directory, you must add this path to you Additional Libraries Directory. Apart from adding this directory to the project you must also link with the corresponding Noesis library.
  • /Build: project for building all samples is included in this directory.
  • /Data: location of many small samples for testing your application and XamlPlayer.

Initialization

Before being able to render any XAML, Noesis must be initialized by invoking Noesis::GUI::Init() and optionally passing an error handler, log handler and memory allocator.

void LogHandler(const char* filename, uint32_t line, uint32_t level, const char* channel,
    const char* message)
{
    if (strcmp(channel, "") == 0)
    {
        // [TRACE] [DEBUG] [INFO] [WARNING] [ERROR]
        const char* prefixes[] = { "T", "D", "I", "W", "E" };
        const char* prefix = level < NS_COUNTOF(prefixes) ? prefixes[level] : " ";
        fprintf(stderr, "[NOESIS/%s] %s\n", prefix, message);
    }
}

void main()
{
    Noesis::GUI::Init(nullptr, LogHandler, nullptr);

    // ...
}

Note

The default error handler just redirects to the log handler. So, just setting only the log handler is probably enough. If you want to have control about Noesis allocations you must pass a memory allocator though. For simplicity's sake we are not doing it here.

Resource Providers

After initialization, a resource provider must be installed for each kind of asset your application is going to load. For example, loading resources from the current directory is achieved this way:

Noesis::GUI::SetXamlProvider(MakePtr<LocalXamlProvider>("."));
Noesis::GUI::SetTextureProvider(MakePtr<LocalTextureProvider>("."));
Noesis::GUI::SetFontProvider(MakePtr<LocalFontProvider>("."));

Note

Each time a resource (xaml, texture, font) is needed, the corresponding provider is invoked to get a stream to the content. You must install a provider for each needed resource. There are a few implementations available in the app framework (like LocalXamlProvider to load from disk)

View creation

A view is needed to render the user interface and interact with it. A view holds a tree of elements. The easiest way to build interface trees is by loading them from XAML files. This can be done using the helper function LoadXaml. Once the XAML is loaded you must create a view with it and specify its dimensions. Each time your window or surface dimensions change you must indicate it to the view.

Ptr<FrameworkElement> xaml = Noesis::GUI::LoadXaml<FrameworkElement>("Reflections.xaml");
Ptr<IView> view = Noesis::GUI::CreateView(xaml);
view->SetSize(1024, 768);

Once the view is created, its renderer must be initialized with a render device. You should provide your own implementation although we provide several reference implementations within the Application Framework.

Ptr<RenderDevice> device = *new GLRenderDevice();
view->GetRenderer()->Init(device);

Note

If you are using a separate thread for rendering. This last step must be performed in that thread

Registering classes

In case you are Extending Noesis with new classes, you must register them after Noesis initialization.

NsRegisterComponent<Scoreboard::MainWindow>();
NsRegisterComponent<Scoreboard::App>();
NsRegisterComponent<Scoreboard::ThousandConverter>();
NsRegisterComponent<EnumConverter<Scoreboard::Team>>();
NsRegisterComponent<EnumConverter<Scoreboard::Class>>();

Attaching to events

To have interaction with the user interface you need hooking to events. As described in the Events tutorial, there are many ways to achieve this. An easy way is connecting control events with local delegates. Just find each desired control by name and connect to a delegate.

Slider* slider = view->GetContent()->FindName<Slider>("Luminance");
slider->ValueChanged() += &LuminanceChanged;

Input Management

Once per frame you must gather input events from keyboard, mouse, touch and gamepad and send them to each view. For specific details about how to translate events from each window subsystem to Noesis we provide implementations for each platform in the Application Framework: Win32Display, AppKitDisplay, UIKitDisplay, XDisplay, etc.

Mouse

The following functions are available to indicate when the mouse moves, when is clicked and when the horizontal and vertical wheel are rotated.

void MouseButtonDown(int x, int y, MouseButton button);
void MouseButtonUp(int x, int y, MouseButton button);
void MouseDoubleClick(int x, int y, MouseButton button);
void MouseMove(int x, int y);
void MouseWheel(int x, int y, int wheelRotation);
void MouseHWheel(int x, int y, int wheelRotation);

Keyboard

When a key is pressed you must use KeyDown() and KeyUp(). To send already processed UTF-32 characters, Char is provided.

void KeyDown(Key key);
void KeyUp(Key key);
void Char(uint32_t ch);

Touch

For tracking fingers TouchDown, TouchMove and TouchUp are available. Several fingers can be tracked separately, each one with a different id, to support multi-touch interaction.

void TouchDown(int x, int y, uint64_t id);
void TouchMove(int x, int y, uint64_t id);
void TouchUp(int x, int y, uint64_t id);

Gamepad

In the Key enumeration used to send keyboard events, there are a few virtual codes specifically added to support hardware gamepad buttons:

Noesis Key Xbox mapping
Key_GamepadLeft D-pad left
Key_GamepadUp D-pad up
Key_GamepadRight D-pad right
Key_GamepadDown D-pad down
Key_GamepadAccept A button
Key_GamepadCancel B button
Key_GamepadMenu Menu button
Key_GamepadView View button
Key_GamepadPageUp Left trigger
Key_GamepadPageDown Right trigger
Key_GamepadPageLeft Left bumper
Key_GamepadPageRight Right bumper
Key_GamepadContext1 X button
Key_GamepadContext2 Y button
Key_GamepadContext3 Left stick
Key_GamepadContext4 Right stick

Besides that, there are also two functions to send scrolling feedback to the view. You normally map this to the right analog stick.

void Scroll(float value);
void HScroll(float value);

The following image is an example about how to map the Xbox controller to Noesis events.

SDKGuideImg1.jpg

Update

Once per frame the view needs to be updated with the global time. At this point things like layout and animation are internally calculated and the current state is prepared to be displayed.

double time = GetGlobalTime();
view->Update(time);

Note

A common error here is passing a delta time instead of a global one

Render

After updating, the view is ready to be rendered. Before sending commands to the GPU, first thing you must do is updating the renderer to collect commands from the last update performed.

view->GetRenderer()->UpdateRenderTree();

Note

UpdateRenderTree returns whether there are changes from the last render. This boolean can be used to skip rendering in case you have a valid copy of the last frame.

After updating the renderer, offscreen textures must be generated. This step populates all the internal textures that are needed for the current frame. From a performance point of view, it is critical to apply this step before binding the main render target.

view->GetRenderer()->RenderOffscreen();

After that, the main render target and viewport must be bound. Note that the above step for rendering offscreen textures modifies the GPU state, so you need to restore it. This is also the right moment to render you 3D scene in case you using a HUD (head-up display) interface.

Note

Because the Render() function modifies the GPU state, you must restore it properly to a sane state for your application. For performance reasons it is not done automatically. The most straightforward solution is to save device state before the call to RenderOffscreen() and restore it afterwards. Greater performance can be achieved if your application implements a way to invalidate its required states. This is faster because you avoid getting the current state from the driver.

glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT));

glClearColor(0.0f, 0.0f, 0.25f, 0.0f);
glClearStencil(0);
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

And finally, the interface is rendered into the current render target.

view->GetRenderer()->Render();

Note

Masking, used to hide part of UI elements, is implemented in NoesisGUI using the StencilBuffer. Make sure that you are binding a a stencil buffer with at least 8 bits to properly visualize masks. And also make sure to clear it to zero before doing the on-screen UI rendering.

Finalization

Before exiting your application each view renderer must be shutdown. This must be done from the render thread if you are using any. Besides, each Ptr you own must be Reset(). After cleaning all objects, Noesis must be properly closed by invoking the Shutdown() function. This releases all internal allocated resources.

view->GetRenderer()->Shutdown();
view.Reset();
Noesis::GUI::Shutdown();
© 2017 Noesis Technologies