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:
As soon as the aspect ratio changes, either hidden objects are displayed or a part of the picture is cut off:
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:
- Lock Aspect Ratio
- Stretch Picture
- Fit Picture with Margin (Letterbox / Pillarbox)
- Fit Picture by Cutting (Zoom)
- Adjust Content
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->GetAppConfiguration(); engineConfig->SetProductName("WindowResize"); appConfig->SetWindowTitle("Window Resize powered by murl engine"); appConfig->SetDisplaySurfaceSize(800, 600); appConfig->SetFullScreenEnabled(false); appConfig->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:
<Camera 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:
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.
<Camera id="camera" viewId="view" fieldOfViewX="400" fieldOfViewY="300" aspectRatio="1" nearPlane="400" farPlane="2500" clearColorBuffer="1" />
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):
<Camera id="camera" viewId="view" fieldOfViewX="400" fieldOfViewY="300" aspectRatio="1" enableBorderMask="true" nearPlane="400" farPlane="2500" clearColorBuffer="1" />
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
.
<Camera id="camera" viewId="view" fieldOfViewX="400" fieldOfViewY="300" aspectRatio="1" enableAspectClipping="true" nearPlane="400" farPlane="2500" clearColorBuffer="1" />
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.
<Camera id="camera" viewId="view" fieldOfViewX="400" nearPlane="400" farPlane="2500" clearColorBuffer="1" />
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 methodConfigure()
, the header filemurl_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.
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. In order not to clear the contents previously rendered by the first view, we specify "no" for the clearColorBuffer
attribute, as for performance reasons a clear operation always clears the entire output surface.
<View id="view2" depthOrder="1" leftMaskCoord="100" rightMaskCoord="-100" topMaskCoord="-100" bottomMaskCoord="100" /> <Camera id="test_camera" viewId="view2" fieldOfViewX="400" nearPlane="400" farPlane="2500" clearColorBuffer="no" /> <CameraTransform cameraId="test_camera" posX="0" posY="0" posZ="800" /> <CameraState cameraId="test_camera" />
Within that new view we place another full-screen plane (800x600 units) in red.
<MaterialState materialId="/material/mat_color" slot="0" /> <ParametersState parametersId="/material/par_overlay_color" /> <PlaneGeometry id="background" scaleFactorX="800" scaleFactorY="600" depthOrder="0" posX="0" posY="0" posZ="0" />
The margin of the second view is always 100 pixels, even if the window size changes. We can now see that this new full-screen plane is actually clipped along that margin:
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" />