Qt Quick Ultralite perspective_transforms 范例

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

概述

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.

目标平台

工程结构

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 工程文件

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
)
							
应用程序 UI

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 组件。

    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

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

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

Cover.qml implements all matrix calculations needed to render a cover (and its reflections) using right transformations. The Cover contorl is a Rectangle with 图像 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
    }
    ...
							

新的 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 类型:

All of them are used in the calcCoverTransform() 函数:

    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
    }
							

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.

文件:

图像:

另请参阅 Matrix4x4 , matrix4x4 ,和 Qt::matrix4x4 ().