Tutorial #05: Window Size

Ein grundsätzliches Problem bei der Entwicklung von Anwendungen für unterschiedliche Geräte sind die unterschiedlichen Bildschirmauflösungen und Seitenverhältnisse. In diesem Beispiel werden einige Möglichkeiten vorgestellt, wie man am besten darauf reagieren kann.

Als erstes erstellen wir ein Testprogramm mit einem 800x600 Pixel großen Fenster und zeichnen darin zwei Rechtecke auf der Z-Ebene 0. Das erste Rechteck soll bildschirmfüllend dargestellt werden. Das zweite Rechteck soll das erste überdecken und einen umlaufenden Rand von 25 Pixeln haben.

Version 1

Depth Order

Die Tiefensortierung erfolgt normalerweise pixelweise über einen Z-Buffer. Bei vielen Anwendungen ist aber eine manuelle Kontrolle auf Objektebene gewünscht. Dies kann mit dem Parameter depthOrder erreicht werden.

Objekte werden zuerst nach Ihrer Z-Position sortiert und, wenn dieser Wert gleich ist, nach dem depthOrder-Wert. Dabei überdeckt ein Objekt mit größerer depthOrder ein Objekt mit kleinerer depthOrder. Der zulässige Wertebereich für dieses Attribut entspricht einem SInt32.

Um eine depthOrder-Sortierung zu erreichen, muss beim Material das Attribut depthBufferMode auf "NONE" oder "READ_ONLY" gesetzt werden. Die zulässigen Werte für das Attribut depthBufferMode sind in der DepthBufferMode-Enumeration in der Datei murl_i_enums.h definiert:

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

Für unser Beispiel definieren wir ein passendes Material

        <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"
        />

und zeichnen zwei Rechtecke mit unterschiedlicher Farbe und passendem depthOrder Wert:

        <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"
        />

Zusätzlich zeichnen wir 4 "versteckte" Quadrate, die außerhalb des sichtbaren Bereichs platziert werden:

        <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"
        />

Im Fenster werden die überlagerten Rechtecke wie gewünscht angezeigt:

tut0105_window_v1_1.png
V1: Ausgabefenster in Originalgröße

Wenn wir allerdings die Fenstergröße ändern und sich damit das Seitenverhältnis ändert, sind entweder außerhalb liegende "versteckte" Objekte zu sehen oder ein Teil des Bildes wird abgeschnitten:

tut0105_window_v1_2.png
Ausgabefenster vertikal vergrößert
tut0105_window_v1_3.png
Ausgabefenster vertikal verkleinert

Wird für die Kamera fieldOfViewY anstelle von fieldOfViewX angegeben, ändert sich das Verhalten nur dahingehend, dass die versteckten Objekte links und rechts anstatt wie zuvor oben und unten zu sehen sind.

Es gibt mehrere Lösungsansätze für dieses Problem:

Im Abschnitt Bildschirmauflösungen wird auch noch gezeigt, wie die Bildschirmauflösung des primären Monitors abgefragt werden kann.

Seitenverhältnis sperren

Um das Seitenverhältnis des Fensters zu sperren genügt der Aufruf der Methode appConfig->SetLockWindowAspectEnabled(true) in der Methode 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;
}

Dadurch behält das Fenster immer das gleiche Seitenverhältnis und das Bild wird immer fensterfüllend angezeigt.

Das Problem besteht aber weiterhin, wenn der Bildschirm ein anderes Seitenverhältnis hat und die Anwendung im Vollbildmodus angezeigt wird. Apps auf Mobilgeräten werden üblicherweise immer im Vollbildmodus dargestellt und unter Windows kann mit der Tastenkombination ALT+Enter zwischen Fenstermodus und Vollbildmodus gewechselt werden.

Bild aufzoomen/verzerren (Stretch)

Dafür muss für den Kameraknoten sowohl fieldOfViewX als auch fieldOfViewY und ein aspectRatio von 0 angegeben werden:

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

Das Bild wird immer füllend im Fenster dargestellt, allerdings wird das Bild verzerrt:

tut0105_window_v1_stretch_1.png
Ausgabefenster horizontal verkleinert, mit fixem Field-Of-View
tut0105_window_v1_stretch_2.png
Ausgabefenster vertikal verkleinert, mit fixem Field-Of-View

Bild einpassen mit Rahmen (Letter Box / Pillar Box )

Soll der gewünschte Bildausschnitt (400x300) ohne Verzerrung dargestellt werden, muss das Attribut aspectRatio auf 1 gesetzt werden.

Mit dem Attribut enableAspectClipping des Kamera-Knotens kann bestimmt werden, ob das Bild durch Abschneiden (engl. "Clipping") eingepasst wird (true) oder mit Zugabe eines Rahmens (engl. "Border") eingepasst wird (false). Standardmäßig wird das Bild automatisch mit Rahmen in das Fenster eingepasst, sodass immer zumindest der angegebene Bildausschnitt sichtbar ist.

        <Camera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            fieldOfViewY="300"
            aspectRatio="1"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />
tut0105_window_v1_margin_1.png
Pillar Box ohne BorderMask
tut0105_window_v1_margin_2.png
Letter Box ohne BorderMask

Mit dem Attribut enableBorderMask="true" kann das Bild zusätzlich maskiert werden, sodass der außerhalb liegende Bereich (mit den versteckten Objekten) nicht gezeichnet wird:

        <Camera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            fieldOfViewY="300"
            aspectRatio="1"
            enableBorderMask="true"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />
tut0105_window_v1_margin_3.png
Pillar Box mit BorderMask
tut0105_window_v1_margin_4.png
Letter Box mit BorderMask

Bild einpassen mit Abschneiden (Zoom)

Für das Einpassen mit Abschneiden (Zoom), muss lediglich das Attribut enableAspectClipping für den Kameraknoten auf true gesetzt werden.

        <Camera
            id="camera"
            viewId="view"
            fieldOfViewX="400"
            fieldOfViewY="300"
            aspectRatio="1"
            enableAspectClipping="true"
            nearPlane="400" farPlane="2500"
            clearColorBuffer="1"
        />
tut0105_window_v1_cut_1.png
Zoom, Ausgabefenster horizontal verkleinert
tut0105_window_v1_cut_2.png
Zoom, Ausgabefenster vertikal verkleinert

Bildinhalt anpassen

Damit wir die Bildinhalte anpassen können, müssen wir über alle Änderungen der Fenstergröße Bescheid wissen. Das IAppConfiguration-Objekt bietet dafür die Methode HasDisplaySurfaceSizeChanged(). Um diese Methode nutzen zu können, benötigen wir allerdings noch eine Membervariable vom Typ ChangeInspector.

            ChangeInspector mDisplaySurfaceSizeInspector;

Nun können wir in der OnProcessTick()-Methode abfragen, ob sich die Größe geändert hat und entsprechend darauf reagieren.

Mit der Methode GetAppConfiguration() des IEngineConfiguration-Objekts erhalten wir einen Pointer auf das IAppConfiguration-Objekt. Das Objekt mDisplaySurfaceSizeInspector speichert die Framenummer der letzten Änderung. Die Methode HasDisplaySurfaceSizeChanged() prüft, ob sich die Größe der Display-Surface (und damit die Fentstergröße) vom aktuellen Frame im Vergleich zum Frame mit der gespeicherten Framenummer geändert hat.

Bei einer Änderung können die neue Breite und Höhe mit GetDisplaySurfaceSizeX() und GetDisplaySurfaceSizeY() ausgelesen werden. Die Framenummer im ChangeInspector wird automatisch upgedatet.

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();
    }
}

Nach demselben Schema kann auf alle Änderungen der Konfiguration reagiert werden. Zum Beispiel auf config->HasLanguageChanged() oder config->HasAccelerometerActiveChanged(). Dabei muss für jede Eigenschaft ein eigenes ChangeInspector Objekt definiert werden.

Die Rechtecke werden nun in der Methode RealignContent mit SetScaleFactor() passend geändert:

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);

Vor dem Testen müssen noch allfällige Änderungen der Kamera Attribute rückgängig gemacht werden.

<Camera
    id="camera"
    viewId="view"
    fieldOfViewX="400"
    nearPlane="400" farPlane="2500"
    clearColorBuffer="1"
/>
tut0105_window_v1_realign_1.png
Ausgabefenster horizontal verkleinert, mit angepasstem Bildinhalt
tut0105_window_v1_realign_2.png
Ausgabefenster vertikal verkleinert, mit angepasstem Bildinhalt

Bildschirmauflösungen

Die Bildschirmauflösung des primären Monitors kann mit dem IAppConfiguration-Objekt und den Methoden GetDisplaySurfaceSizeX() und GetDisplaySurfaceSizeY() in der Configure()-Methode abgefragt werden.

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;
}

Mit diesen Werten kann entsprechend reagiert werden, um z.B. unterschiedliche Ressourcen-Pakete zu laden.

Zu beachten
Achtung: Um die Methode Debug::Trace() in der Methode Configure() verwenden zu können, muss zuvor die Headerdatei murl_debug_trace.h inkludiert werden.

Version 2: View maskieren

Mit einer View-Maske kann der Zeichenbereich auf einen bestimmten Ausschnitt des Fensters eingeschränkt werden.

Für jeden View-Knoten kann mit den Attributen leftMaskCoord, rightMaskCoord, topMaskCoord und bottomMaskCoord eine Maske definiert werden. Die Angabe erfolgt in Pixel relativ zum Fensterrand bzw. zum korrespondierenden Ankerpunkt.

tut0105_anchors.png
Masken-Ankerpunkte

Mit der folgenden Angabe erzeugen wir einen zweiten View, der immer einen Abstand von genau 100 Pixel zum Fensterrand hat. Da der View eine größere depthOrder hat, wird er im Vordergrund des zweiten Views gezeichnet. Damit der zuvor vom ersten View gerenderte Inhalt nicht überschrieben wird, erhält das clearColorBuffer Attribut den Wert "no", da bei einer Löschoperation aus Performance-Gründen immer die komplette Output-Surface gelöscht wird.

    <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"
    />

Innerhalb dieses neuen Views plazieren wir eine weitere Vollbild-Plane (800x600 Einheiten) in Rot.

    <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"
    />

Der Abstand des zweiten Views von 100 Pixeln zum Fensterrand wird immer eingehalten, auch wenn sich die Fenstergröße ändert. Wir können nun beobachten, dass die neue Vollbild-Plane auch tatsächlich an diesem Bereich geclippt wird:

tut0105_window_v2_1.png
Ausgabefenster mit maskiertem rotem View

Ankerpunkte

Optional können die Ankerpunkte auf den gegenüberliegenden Fensterrand gelegt werden. Dadurch kann z.B. ein View an einem beliebigen Rand angedockt werden. Die Attribute dafür sind leftMaskAnchor und rightMaskAnchor mit den erlaubten Werten "LEFT", "RIGHT" und "CENTER", sowie topMaskAnchor und bottomMaskAnchor mit erlaubten Werten "TOP", "BOTTOM" und "CENTER". Siehe auch IEnums::AlignmentX und IEnums::AlignmentY.

Um z.B. den zweiten View am rechten Rand mit einer konstanten Breite von 200 Pixel anzudocken, genügt es den linken Anker nach rechts zu legen und die linke Maske auf 200 Pixel weiter links (also -200) festzulegen.

<View id="view2"
    depthOrder="1"
    leftMaskAnchor="RIGHT"
    leftMaskCoord="-200"
/>
tut0105_window_v2_2.png
Ausgabefenster mit maskiertem rotem View am rechten Fensterrand verankert


Copyright © 2011-2024 Spraylight GmbH.