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

Then, all relevant qml files are added.

qul_target_qml_sources(perspective_transforms
    perspective_transforms.qml
    Cover.qml
    CoverFlow.qml
    CoverFlowState.qml
    IdleTimer.qml
    controls/Slider.qml
    controls/RadioButton.qml
    controls/CheckBox.qml
)

# All images are free and downloaded from unsplash.com
# https://unsplash.com/license
...

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

set(IMAGES
    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
)

# Optimize all assets for transformations
set_source_files_properties(${IMAGES} PROPERTIES QUL_OPTIMIZE_FOR_ROTATION ON)
qul_add_resource(perspective_transforms FILES ${IMAGES} BASE resources)
...

In addition, a module holding project-wide constants is defined.

qul_add_qml_module(perspective_transforms_constants
    URI Constants
    QML_FILES
        imports/constants/Constants.qml
        imports/constants/CoverFlowType.qml
    OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/modules
)
...

Finally, all pieces are linked together.

target_link_libraries(perspective_transforms
    Qul::QuickUltraliteControlsStyleDefault
    perspective_transforms_constants
)
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
        }
    }
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()
    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: calcReflectionTransform() }
        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 postMatrix = calcPostMatrix(state.coverFlowX, state.coverFlowY, state.coverFlowW, state.coverFlowH, state.fov, state.viewDistance)
        var preMatrix = calcPreMatrix(state.coverSize)

        var currentShape = postMatrix.times(calcMatrixForType(state.currentViewType).times(preMatrix))
        var oldShape =  postMatrix.times(calcMatrixForType(state.previousViewType).times(preMatrix))

        return currentShape.times(state.morphRatio).plus(oldShape.times(1 - state.morphRatio))
    }

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 d = coverTransform.m41 * x + coverTransform.m42 * y + coverTransform.m44
        var fX = (coverTransform.m11 * x + coverTransform.m12 * y + coverTransform.m14) / d
        var fZ = (coverTransform.m31 * x + coverTransform.m32 * y + coverTransform.m34) / d

        var littleX = Math.abs(fX - state.coverFlowX - state.coverFlowW / 2)
        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.