Tutorial #05: Window Size

A fundamental problem in the development of applications for different devices are the different screen resolutions and different aspect ratios. In this tutorial, some possibilities how to handle these situations will be presented.

At first, we create a test program with a window size of 800x600 pixels and draw two rectangles on the Z-plane 0. The first rectangle should fill the whole screen, while the second one should cover the first and have a margin of 25 pixels on all sides.

Version 1

Depth Order

Usually, depth ordering is done through a Z-buffer. However, a manual control on object level is often preferred. This can be achieved with the parameter depthOrder.

All objects are first sorted by their Z-position and, if one value appears more than once, by their depthOrder value with the larger depthOrder value covering the smaller one. The admissible value range for the attribute depthOrder is equal to one SInt32.

In order to sort objects by their depthOrder value, the material attribute depthBufferMode has to be set to "NONE" or "READ_ONLY. The admissible values for the attribute depthBufferMode are defined in the DepthBufferMode enumeration found in the file murl_i_enums.h :

  • murl/base/include/engine/murl_i_enums.h

For our tutorial we define a suitable program and material, together with two parameter sets representing two different shades of blue:

        <FixedProgram
            id="prg_color"
            coloringEnabled="yes"
        />
        <Material
            id="mat_color"
            programId="prg_color"
            depthBufferMode="NONE"
        />
        
        <FixedParameters
            id="par_cube_color"
            diffuseColor="0f, 0.50f, 0.75f, 1f"
        />       
        <FixedParameters
            id="par_background_color"
            diffuseColor="0.5f, 0.75f, 0.9f, 1f"
        />

and draw two rectangles with those colors and suitable depthOrder values:

        <ParametersState
            parametersId="/material/par_cube_color"
        />
        <PlaneGeometry
            id="rectangle"
            scaleFactorX="750"
            scaleFactorY="550"
            depthOrder="1"
            posX="0" posY="0" posZ="0"
        />
        
        <ParametersState
            parametersId="/material/par_background_color"
        />
        <PlaneGeometry
            id="background"
            scaleFactorX="800"
            scaleFactorY="600"
            depthOrder="0"
            posX="0" posY="0" posZ="0"
        />

In addition we draw four "hidden" squares, which are placed outside of the visible field of view:

        <PlaneGeometry
            id="hidden0"
            scaleFactor="42"
            posX="0" posY="350" posZ="0"
        />

        <PlaneGeometry
            id="hidden1"
            scaleFactor="42"
            posX="0" posY="-350" posZ="0"
        />

        <PlaneGeometry
            id="hidden2"
            scaleFactor="42"
            posX="450" posY="0" posZ="0"
        />

        <PlaneGeometry
            id="hidden3"
            scaleFactor="42"
            posX="-450" posY="0" posZ="0"
        />

As desired, the two overlain rectangles are displayed in the window:

tut0105_window_v1_1.png
V1 output window at original size

As soon as the aspect ratio changes, either hidden objects are displayed or a part of the picture is cut off:

tut0105_window_v1_2.png
V1 output window enlarged in vertical direction
tut0105_window_v1_3.png
V1 output window shrunk in vertical direction

If fieldOfViewY instead of fieldOfViewX is specified for the camera, the hidden objects on the left and right side are displayed rather than above and below the rectangles.

There is more than one solution for this problem:

The sub section Screen Resolution shows additionally how to query the resolution of the primary screen .

Lock Aspect Ratio

Locking the aspect ratio can simply be done by calling the IAppConfiguration's method SetLockWindowAspectEnabled(true) in the method Configure():

Bool App::WindowSizeApp::Configure(IEngineConfiguration* engineConfig, IFileInterface* fileInterface)
{
    IAppConfiguration* appConfig = engineConfig-&gt;GetAppConfiguration();

    engineConfig-&gt;SetProductName(&quot;WindowResize&quot;);
    appConfig-&gt;SetWindowTitle(&quot;Window Resize powered by murl engine&quot;);
    appConfig-&gt;SetDisplaySurfaceSize(800, 600);
    appConfig-&gt;SetFullScreenEnabled(false);

    appConfig-&gt;SetLockWindowAspectEnabled(true);

    return true;
}

By doing so, the aspect ratio constantly remains the same and the picture is always displayed filling the window.

However, there is still a problem, if the screen has a different aspect ratio and the application is run in full screen mode. Keep in mind that applications on mobile devices are always displayed full screen. On a Windows PC it is possible to switch between window and fullscreen mode by pressing the keystroke combination ALT+ENTER.

Stretch Picture

For this method, both fieldOfViewX and fieldOfViewY have to be specified and the aspectRatio has to be set to 0 for the camera node:

        <PerspectiveCamera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            fieldOfViewY="300"
            aspectRatio="0"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />

As a result, the picture is always displayed filling the whole window. It is, however, deformed:

tut0105_window_v1_stretch_1.png
V1 output window shrunk in horizontal direction, with locked field of view
tut0105_window_v1_stretch_2.png
V1 output window shrunk in vertical direction, with locked field of view

Fit Picture with Margin (Letterbox / Pillarbox)

If the picture (400x300) should be displayed without deformation the attribute aspectRatio hast to be set to 1.

The attribute enableAspectClipping of the camera node controls the fitting method: Use false to fit the picture with a margin into the window. Use true to fit the picture into the window by cutting off some parts. Per default the picture is fitted with a margin so that the whole picture is visible in any case.

        <PerspectiveCamera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            fieldOfViewY="300"
            aspectRatio="1"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />
tut0105_window_v1_margin_1.png
V1 pillarbox without border mask
tut0105_window_v1_margin_2.png
V1 letterbox without border mask

In addition the attribute enableBorderMask="true" can be used to mask the picture. Doing so avoids the drawing of areas outside of the picture (with the hidden objects):

        <PerspectiveCamera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            fieldOfViewY="300"
            aspectRatio="1"
            enableBorderMask="true"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />
tut0105_window_v1_margin_3.png
V1 pillarbox with border mask
tut0105_window_v1_margin_4.png
V1 letterbox with border mask

Fit Picture by Cutting (Zoom)

In order to fit the picture into the window by cutting off some parts, only the attribute enableAspectClipping of the camera node needs to be set to true.

        <PerspectiveCamera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            fieldOfViewY="300"
            aspectRatio="1"
            enableAspectClipping="true"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />
tut0105_window_v1_cut_1.png
V1 zoom, output window shrunk in horizontal direction
tut0105_window_v1_cut_2.png
V1 zoom, output window shrunk in vertical direction

Adjust Content

In order to be able to adjust the content, we need to determine any changes of the window size. For this situation, the IAppConfiguration object provides the method HasDisplaySurfaceSizeChanged(). Before using this method, we need another member variable of the type ChangeInspector.

            ChangeInspector mDisplaySurfaceSizeInspector;

We are now able to determine through the OnProcessTick() method, if the window size has changed, and to react accordingly.

By using the method GetAppConfiguration() on the IEngineConfiguration object retrieved from the given Logic::IState parameter we receive a pointer to the IAppConfiguration object. The object mDisplaySurfaceSizeInspector saves the frame number of the last change. The method HasDisplaySurfaceSizeChanged() now checks, if the display surface size (i.e. window size) of the current frame has changed compared to the frame with the saved frame number.

If the frame number has been changed, the new width and height can be queried by using GetDisplaySurfaceSizeX() and GetDisplaySurfaceSizeY(). The frame number in the ChangeInspector is automatically updated.

void App::WindowSizeLogic::OnProcessTick(const Logic::IState* state)
{
    Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
    
    const IAppConfiguration* appConfig = state->GetEngineConfiguration()->GetAppConfiguration();
    if (appConfig->HasDisplaySurfaceSizeChanged(mDisplaySurfaceSizeInspector))
    {
        UInt32 width = appConfig->GetDisplaySurfaceSizeX();
        UInt32 height = appConfig->GetDisplaySurfaceSizeY();
        Debug::Trace("New Screen Size %i %i", width, height);

        // Uncomment the desired adjustment method call below
        
        //(1) realign content
        // RealignContent(width,  height);
    }

    if (deviceHandler->WasRawKeyPressed(RAWKEY_ESCAPE) ||
        deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK))
    {
        deviceHandler->TerminateApp();
    }
}

Following the same scheme, it is possible to react to any changes within the configuration, e.g. config->HasLanguageChanged() or config->HasAccelerometerActiveChanged(). For this purpose, a ChangeInspector object has to be defined for every property.

The rectangles are now modified with SetScaleFactor() in the method RealignContent:

void App::WindowSizeLogic::RealignContent(UInt32 width, UInt32 height)
{
    int h = Real(800)*height/width;
    mRectBackground->SetScaleFactorY(h);
    if (h>50)
        mRectForeground->SetScaleFactorY(h-50);
    else
        mRectForeground->SetScaleFactorY(0);
}
        //(1) realign content
        RealignContent(width,  height);

Don't forget to change back any modifications you may have made to the camera node.

<PerspectiveCamera
    id="camera"
    viewId="view"
    fieldOfViewX="400"
    nearPlane="400" farPlane="2500"
    clearColorBuffer="1"
/>
tut0105_window_v1_realign_1.png
V1 output window shrunk in horizontal direction, with realigned contents
tut0105_window_v1_realign_2.png
V1 output window shrunk in vertical direction, with realigned contents

Screen Resolution

The resolution of the primary screen can be queried with the IAppConfiguration object and the methods GetDisplaySurfaceSizeX() and GetDisplaySurfaceSizeY() in the Configure() method.

Bool App::WindowSizeApp::Configure(IEngineConfiguration* engineConfig, IFileInterface* fileInterface)
{
    IAppConfiguration* appConfig = engineConfig->GetAppConfiguration();

    UInt32 width = appConfig->GetDisplaySurfaceSizeX();
    UInt32 height = appConfig->GetDisplaySurfaceSizeY();
    Debug::Trace("Primary Monitor Size %i x %i", width, height);

    engineConfig->SetProductName("WindowResize");
    appConfig->SetWindowTitle("Window Resize powered by murl engine");
    appConfig->SetDisplaySurfaceSize(800, 600);
    appConfig->SetFullScreenEnabled(false);

    //appConfig->SetLockWindowAspectEnabled(true);

    return true;
}

By using these values, it is possible to appropriately react e.g. in order to load different resource packages.

Note
In order to use the method Debug::Trace() within the method Configure(), the header file murl_debug_trace.h has to be included in advance.

Version 2: Mask View

Using a view mask, it is possible to restrict the drawing area to a specific section of the window.

A mask can be defined for every View node by using the attributes leftMaskCoord, rightMaskCoord, topMaskCoord or bottomMaskCoord. The values are specified in pixels relative to the window border and the corresponding anchor point.

tut0105_anchors.png
Mask anchors

With the following specifications we create a second view which has a margin of exactly 100 pixels to the window border. As the new view has a larger depthOrder, it is located in front of the existing one. The attribute colorClearValue defines an alternative color for deleting the buffer. In our case, the new view has a red colored background instead of a black one.

    <View
        id="view2"
        depthOrder="1"
        leftMaskCoord="100"
        rightMaskCoord="-100"
        topMaskCoord="-100"
        bottomMaskCoord="100"
    />
    
    <PerspectiveCamera
        id="test_camera"
        viewId="view2"
        fieldOfViewX="400"
        nearPlane="400" farPlane="2500"
        clearColorBuffer="yes"
        colorClearValue="1f, 0f, 0f"
    />
    <CameraTransform
        cameraId="test_camera"
        posX="0" posY="0" posZ="800"
    />
    <CameraState
        cameraId="test_camera"
    />

The margin of the second view is always 100 pixels, even if the window size changes.

tut0105_window_v2_1.png
V2 output window with masked red view

Mask Anchors

It is also possible to set anchor points to the opposite window border. By doing so, a view can e.g. be docked to any border. The attributes are leftMaskAnchor and rightMaskAnchor with the possible values "LEFT", "RIGHT" and "CENTER", and topMaskAnchor and bottomMaskAnchor with possible values "TOP", "BOTTOM" and "CENTER". See the IEnums::AlignmentX and IEnums::AlignmentY enumerations for details.

In order to dock e.g. the second view with a constant width of 200 pixels to the right border, it is sufficient to set the left anchor to "RIGHT" and to set the left mask by 200 pixels to the left (i.e. -200).

<View id="view2"
    depthOrder="1"
    leftMaskAnchor="RIGHT"
    leftMaskCoord="-200"
/>
tut0105_window_v2_2.png
V2 output window with masked red view anchored to the right window border


Copyright © 2011-2017 Spraylight GmbH.