Sign up for the KDAB Newsletter
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Here, at KDAB, we get to spend 10% of our time on learning what we don't know or practicing and improving what we already know. Recently, I decided to use that time to learn more about the Qt Quick Rendering Engine. The best way to do so, I found, is to use it in a way it wasn't intended to be used: for making simple 3D graphics — creating my own little 3D paintings, as one would in Minecraft, starting with a ground plane. I'd like to take this time to share with you how to play.
My mini voxel-editor should be using Isometric Perspective to display colorful cubes, which can be placed in a world. Therefore, I create a QML Rectangle that contains the transformations necessary for isometric perspective.
By combining three transforms, I create my ground plane:
These actions can be achieved by combining three 4-by-4 matrices for each of the transformations, via multiplication. Note that, although QML comes with predefined Transform Items, I use raw matrices for each transform, to keep the code consistent.
//The ISO perspective transform:
transform: Matrix4x4 {
id: isometric_perspective
property real rotationZ: 45/360 * Math.PI * 2
property matrix4x4 flipMatrix:
Qt.matrix4x4(1, 0, 0, 0,
0, -1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
property matrix4x4 rotationZMatrix:
Qt.matrix4x4(Math.cos(rotationZ), -Math.sin(rotationZ), 0, 0,
Math.sin(rotationZ), Math.cos(rotationZ), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
property real flatness: 0.5
property matrix4x4 scaleYMatrix:
Qt.matrix4x4(1, 0, 0, 0,
0, flatness, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
matrix: flipMatrix.times(scaleYMatrix).times(rotationZMatrix)
}
Because all transforms originate in the top-right of my window, I position my world-origin to the bottom-center of my window, to reveal the full world inside of it.
Since QML's scene graph is hierarchical, all of its child Items (Rectangles, Circles, …) are inherently in the right perspective and appear as lying on the ground of my iso-world.
Both the empty world and the world filled with Rectangle, Circle, and a star Image can be seen here:
Next would be the cubes. I already have the top faces, since they would be simple squares in my iso-world (just like the red rectangle in the image above). To "elevate" them to the cubes height (32px), I apply an offset of 32 in x, as well as in y, as a transform. Going up in the iso-world means adding the same value to x and y, thus moving straight out of the corner pointing towards you. Sometimes, this additional axis combined out of x and y is called the w axis, for the isometric perspectives. There is a certain ambiguity, still, since the viewer does not know if a shape was moved on the ground plane (a), or elevated (b). Small tricks in lighting, such as adding a shadow, can help here:
For the side faces, I also would need an upwards-effect to make them rise from the bottom to the top of my cubes. This, I achieve with a transform matrix, increasing x as well as y. For the right side, this happens in the first row of the transform matrix. Conversely, the left side has this double increase in its second row.
For the colors, I decided to simply make the top Qt.lighter(cubeColor), the left normal cubeColor, and the right Qt.darker(cubeColor). So, overall, it looks like light is coming from top left.
I found out that all MouseAreas are transformed, together with the visible Item (as they behave as transformed Rectangles). So, just by filling a MouseArea to each of the sides,
I could make them clickable, individually, also working with all the transforms. To reuse the MouseArea, I refactored out a universal FaceArea, which allows you to create a cube at an arbitrarily chosen vector offset.
So, to give an example, I instantiate it with the vector Qt.vector3D(0, 0, 1), for the top face to create the new cube on top, when clicking.
//FaceArea.qml
MouseArea {
property var creationOffset: Qt.vector3d(0, 0, 1)
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel: {isoCube.color = Qt.hsla(Math.random(),0.8, 0.5, 1)}
onClicked: {
if (mouse.button & Qt.LeftButton) {
isoWorld.createCubeAt(isoCube.xpos + creationOffset.x,
isoCube.ypos + creationOffset.y,
isoCube.level + creationOffset.z);
} else {
isoCube.destroy()
}
}
}
There are 3 possibilities of interaction:
Bonus: To make the whole thing a bit more vivid, I made it wobble up and down. I also added an indicator where new cubes are placed on the ground plane.
Here is the final result in a video:
Tip: To make the Cubes stack well, Z-ordering has to be done right. There are 3 simple rules:
So, I simply set the z to 1000 * stacklevel - x - y, and it worked well.
Although QML is intended to be used with 2D graphics, mainly 2.5D, including interaction is well possible through Item-specific transforms. The QML Scene Graph implicitly allows nested transforms and makes perspective drawings an easy task.
I had lots of fun while playing and learning through this small example. Maybe it's useful to you, or maybe it just makes you as happy as it made me.
The code for this mini Minecraft can be found and played with at: https://github.com/chsterz/isoworld
KDAB offers unique Qt expert services. Develop unique Qt and QML applications with KDAB's expertise. For more information on our services surrounding Qt and QML, click here.
About KDAB
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Upgrade your applications from Qt 5 to Qt 6 with KDAB’s migration services. Get a free migration assessment and join a hands-on workshop to prepare your team for a successful transition!
Learn more
3 Comments
24 - Feb - 2021
Dave
The entire scene moving up and down in the video looks disorientating and annoying for usability.
24 - Feb - 2021
Christoph Sterz
For usability, I can agree. Still, a static world was quite boring to look at :) So In the end, I still put in a bit of "wobble".
25 - Feb - 2021
Abdramane Sakone
Nice, i love it :)