C

Qt Quick Ultralite perspective_transforms Example

Shows how to implement perspective (2.5D) transformations in a Qt Quick Ultralite application.

Overview

The perspective_transforms example implements perspective transformations and quasi-3D effects using Matrix4x4 QML transform type. It demonstrates this using a music album cover flow component.

The example's UI has a group of radio buttons on the top, enabling to switch between the following cover flow styles:

  • Carousel
  • Circle
  • Circle 2D (using orthogonal transformations)
  • Perspective

The cover flow component is rendered in the center. It animates the album cover movement according to the selected style.

At the very bottom, a group of slider controls let the user adjust the camera parameters, such as elevation or tilt angle.

Additionally, the example has a "demo" mode, which is enabled if there is user interaction for more than 5 seconds. This mode animates switching between the available covers and view types. The animations stops when the user starts interacting with the application.

Target platforms

Project structure

Application project structure has no special features related to perspective transformations, however it's a bit more complex than minimal. It contains the following subdirectories:

  • controls - contains implementation for custom UI controls used in the example, such as CheckBox, Slider or RadioButton
  • imports - content of a QML modules used in the project - mostly for project wide constants (using singletons)
  • resources - graphical assets - mostly album covers
CMake project file

The CMakeLists.txt for this example defines perspective_transforms as the main executable target.

...
    qul_add_target(perspective_transforms
        QML_PROJECT
        mcu_perspective_transforms.qmlproject
        SELECTORS no_controls
        GENERATE_ENTRYPOINT
    )
elseif(NOT QUL_PLATFORM MATCHES "^stm32f769i" AND NOT QUL_PLATFORM MATCHES "^stm32f469i" AND NOT QUL_PLATFORM MATCHES "^mimxrt1170-evk")
    qul_add_target(perspective_transforms
        QML_PROJECT
        mcu_perspective_transforms.qmlproject
        SELECTORS small_controls
        GENERATE_ENTRYPOINT
    )
...
QmlProject file

All relevant qml files are specified in the QmlProject file.

...
        QmlFiles {
                files: [
                "CoverFlow.qml",
                "CoverFlowState.qml",
                "Cover.qml",
                "IdleTimer.qml",
                "perspective_transforms.qml"
                ]
        }

        QmlFiles {
                files: [
                "controls/CheckBox.qml",
                "controls/RadioButton.qml",
                "controls/Slider.qml"
                ]
        }
...

All the image assets are added with ImageFiles.MCU.resourceOptimizeForRotation property enabled. This will improve transformations performance on supported platforms.

        ImageFiles {
                files: [
                "resources/cover0.jpg",
                "resources/cover1.jpg",
                "resources/cover2.jpg",
                "resources/cover3.jpg",
                "resources/cover4.jpg",
                "resources/cover5.jpg",
                "resources/cover6.jpg",
                "resources/cover7.jpg",
                "resources/cover8.jpg",
                "resources/cover9.jpg",
                ]
                MCU.base: "resources"

                // Optimize all assets for transformations
                MCU.resourceOptimizeForRotation: true

In addition, a module holding project-wide constants is defined using constants.qmlproject and added to the main project file.

        ModuleFiles {
                files: [
        "imports/constants/constants.qmlproject",
        "configuration/configuration.qmlproject"
        ]
                MCU.qulModules: ["Controls"]
        }
Application UI

The perspective_transforms.qml file defines the application's user interface.

It lays out the main UI components, such as RadioButtons and Sliders. These are the custom controls that are defined in the controls subdirectory.

import "controls"

IdleTimer takes care of animating UI when user inactivity is detected. This is intended for platforms without a touch screen. It exposes two signals, which are used to independently switch between the cover flow types and current album selection.

    IdleTimer {
        id: idleTimer
        property int coverDir: 1
        onSwitchCover: {
            ...
        }
        onSwitchFlowType: {
            ...
        }
    }

Most relevant part is the instantiation of Cover Flow components. This starts with creating an object that holds current state of the CoverFlow component.

    CoverFlowState {
        id: currentState
        screenWidth: root.width
        screenHeight: root.height
    }
    ...

Then, the CoverFlow component takes care of actual rendering.

    CoverFlow {
        anchors.fill: parent
        currentState: currentState
    }
CoverFlowState

The CoverFlowState.qml file holds all the parameters that affects how CoverFlow is rendered. It configures the following parameters:

  • Screen/Canvas size
  • CoverFlow size and position
  • Number of covers to render and currently selected cover index
  • Camera settings like FOV or clipping distance, elevation, tilt, etc.
  • Settings of particular cover flow types
  • Information about current view type and parameters related to type transition

A morph ratio is a property that is used for animated transitions between different cover flow types.

    property real morphRatio: 1
    property int currentViewType: CoverFlowType.Carousel
    property int previousViewType: CoverFlowType.Carousel

    NumberAnimation on morphRatio {
        id: morphAnimation
        from: 0.0
        to: 1.0
        duration: 200
    }

Switching between cover flow types is done using switchViewType.

    function switchViewType(newType : int){
         previousViewType = currentViewType
         currentViewType = newType
         morphAnimation.start()
    }
CoverFlow

The CoverFlow.qml file implements the cover flow component. It is responsible for creating multiple instances of Cover components, where actual rendering logic is implemented.

This component has a single property holding its current state.

Item {
    id: root
    property CoverFlowState currentState
    ...

A Repeater takes care of dynamically creating a number of Covers based on the value defined in the cover flow state.

    Repeater {
        model: root.currentState.numberOfCovers
        delegate: Cover {
            required property int index

            texture: "cover" + index + ".jpg"
            coverIndex: index
            state: root.currentState
            postMatrix: root.globalPostMatrix
            preMatrix: root.globalPreMatrix
            reflectionTransform: root.reflectionTransform
        }
    }
Cover

The Cover.qml implements all matrix calculations needed to render a cover (and its reflections) using right transformations. The Cover contorl is a Rectangle with Image items (with proper transforms applied) as its children. One image represents the actual cover image, whereas the second one is used to render a reflection of the first. Both Image items are using the Matrix4x4 transformation, where the matrix property is bound to appropriate functions returning matrix4x4 QML basic type (please distinct Matrix4x4 transform object from matrix4x4 basic type).

Rectangle {
    id: cover

    property string texture
    property int coverIndex
    property CoverFlowState state

    property matrix4x4 coverTransform: calcCoverTransform()
    property matrix4x4 postMatrix
    property matrix4x4 preMatrix
    property matrix4x4 reflectionTransform
    z: calcFinalZ()

    Image {
        id: coverImageBase
        source: cover.texture
        transform: Matrix4x4 { matrix: coverTransform }
        opacity: 1.0
    }

    Image {
        id: mirrorImageBase
        visible: cover.state.showReflection
        source: cover.texture
        transform: [
            Matrix4x4 { matrix: reflectionTransform },
            Matrix4x4 { matrix: coverTransform }
        ]

        opacity: 0.1
    }
    ...

A new matrix4x4 can be created using the Qt::matrix4x4() factory method, by giving all the matrix components. Let's take a translation matrix for example:

    function mtxTranslate(x : real, y : real, z : real) : matrix4x4 {
        return Qt.matrix4x4(1, 0, 0, x,
                            0, 1, 0, y,
                            0, 0, 1, z,
                            0, 0, 0, 1)
    }

The follwoing arithmetic operations are available for the matrix4x4 type:

All of them are used in the calcCoverTransform() function:

    function calcCoverTransform() : matrix4x4 {
        var previousViewType = state.previousViewType
        var currentViewType = state.currentViewType
        if (state.morphRatio == 0) {
            currentViewType = previousViewType
        } else if (state.morphRatio == 1) {

A matrix4x4 type has sixteen values, each accessible via the properties m11 through m44 (in row/column order). This is used when calculating the z position of the cover as a whole.

    function calcFinalZ() : real {
        var coverTransform = cover.coverTransform

        var x = state.coverSize/2
        var y = state.coverSize/2

        var inv_d = 1.0 / (coverTransform.m41 * x + coverTransform.m42 * y + coverTransform.m44)
        var fX = (coverTransform.m11 * x + coverTransform.m12 * y + coverTransform.m14) * inv_d
        var fZ = (coverTransform.m31 * x + coverTransform.m32 * y + coverTransform.m34) * inv_d

        var littleX = Math.abs(fX - state.coverFlowX - state.coverFlowW * 0.5)
        return -fZ * 100000 - littleX
    }

The Cover.qml implements multiple functions operating on the matrices, which can be used as a reference. These functions can be grouped into the following categories:

  • Elementary matrix generations functions (translation, scale, rotation, perspective)
  • Calculating pre and post transformation matrices (camera matrix and initial cover transformation)
  • Calculating final transforms for individual cover flow types

All these basic building blocks are sufficient to implement a complex 2.5D effects.

Files:

Images:

See also Matrix4x4, matrix4x4, and Qt::matrix4x4().

Available under certain Qt licenses.
Find out more.