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:
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:
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:
- Seitenverhältnis sperren
- Bild aufzoomen/verzerren (Stretch)
- Bild einpassen mit Rahmen (Letter Box / Pillar Box )
- Bild einpassen mit Abschneiden (Zoom)
- Bildinhalt anpassen
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:
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" />
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" />
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" />
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" />
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 MethodeConfigure()
verwenden zu können, muss zuvor die Headerdateimurl_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.
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:
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" />