Description/ Code
I have a Qt Quick 3D View and corresponding scene that was designed to be compiled on Qt 6.3.0
import QtQuick
import QtQml
import QtQuick3D
import QtQuick3D.Helpers
Window {
width: 800
height: 600
visible: true
property var selectedItem
property bool mousePressed: false
function multiply_vectors(vec1, vec2) {
return Qt.vector3d(vec1.x * vec2.x, vec1.y * vec2.y, vec1.z * vec2.z);
}
View3D {
renderMode: View3D.Inline
camera: camera
anchors.fill: parent
width: 800
height: 600
x: 0
y: 0
id: view
environment: SceneEnvironment {
clearColor: "black"
backgroundMode: SceneEnvironment.Color
depthTestEnabled: false
depthPrePassEnabled: true
}
Model {
id: rootEntity
pickable: true
source: "#Cube"
materials: PrincipledMaterial {
baseColor: "red"
roughness: 0.1
}
position: Qt.vector3d(25.0, 15.0, -60.0)
scale: Qt.vector3d(1.0, 1.0, 1.0)
}
PerspectiveCamera {
id: camera
position.z: 330.0
position.y: 0.75
eulerRotation.x: -12
clipNear: 0.0
clipFar: 1600.0
}
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
id: mouseArea
onPressed: function (mouse) {
var result = view.pick(mouse.x, mouse.y);
if (result.objectHit) {
selectedItem = result.objectHit;
mousePressed = true;
} else {
mousePressed = false;
}
}
onMouseXChanged: function(mouse) {
if (mousePressed) {
var viewCoords = view.mapFromGlobal(mouseArea.mapToGlobal(mouse.x, mouse.y));
var sceneCoords = Qt.vector3d(viewCoords.x, viewCoords.y, 0);
var worldCoords = view.mapTo3DScene(sceneCoords);
worldCoords.z = selectedItem.z
selectedItem.position = multiply_vectors(worldCoords, Qt.vector3d(Math.abs(camera.z - selectedItem.z), Math.abs(camera.z - selectedItem.z), 1.0))
}
}
onReleased: function (mouse) {
mousePressed = false
}
}
Component.onCompleted: {
camera.lookAt(rootEntity)
}
}
}
Overview
The use case is that whenever the mouse is pressed while pointing at the cube, whenever the mouse moves it will cause the cube to move along with it to the corresponding point in the 3d Scene.
This works great when looking from a point that is on the same z-axis. However when looking at the object from a point say along the x-axis, the model will move along the x-axis instead of following the mouse position.
Question
How can I modify the business logic in onMouseXChanged: function(mouse) { to correctly transform the matrix (or equivalent transform) to consistently match the mouse position irregardless of the camera's position relative to the Model?
If I understood you correctly, you need to move the object with the mouse parallel to the camera regardless of the camera position and model scaling? I admit that I don't have a solution, but still it's better than the original code. First of all, do not set the clipNear to 0, it would make the frustum degenerate and break the projection math.
Secondly, I would suppose that the code which sets the object position should look like
selectedItem.position = view.mapTo3DScene(
Qt.vector3d(mouse.x, mouse.y,
view.mapFrom3DScene(selectedItem.position).z))
The docs say that mapFrom3DScene/mapTo3DScene should interpret the z coordinate as the distance from the near clip plane of the frustum to the mapped position. However when I move it towards the sides of the window the object gets larger, whereas it should get smaller.
Here's the complete code with a few corrections of mine:
import QtQuick
import QtQml
import QtQuick3D
import QtQuick3D.Helpers
Window {
width: 800
height: 600
visible: true
property var selectedItem
property bool mousePressed: false
View3D {
renderMode: View3D.Inline
camera: camera
anchors.fill: parent
width: 800
height: 600
x: 0
y: 0
id: view
environment: SceneEnvironment {
clearColor: "black"
backgroundMode: SceneEnvironment.Color
depthTestEnabled: false
depthPrePassEnabled: true
}
Model {
id: rootEntity
pickable: true
source: "#Cube"
materials: PrincipledMaterial {
baseColor: "red"
roughness: 0.1
}
position: Qt.vector3d(25.0, 15.0, -60.0)
scale: Qt.vector3d(2.0, 1.0, 0.5)
}
PerspectiveCamera {
id: camera
position.z: 330.0
position.y: 100
position.x: 700
eulerRotation.x: -12
// Note 1: clipNear shouldn't be 0, otherwise
// it would break the math inside the projection matrix
clipNear: 1.0
clipFar: 1600.0
}
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
id: mouseArea
onPressed: function (mouse) {
var result = view.pick(mouse.x, mouse.y);
if (result.objectHit) {
selectedItem = result.objectHit;
mousePressed = true;
} else {
mousePressed = false;
}
}
onPositionChanged: function(mouse) {
if (mousePressed) {
// Note 2: recalculate the position, since MouseArea has
// the same geometry as View3D we can use coords directly
selectedItem.position = view.mapTo3DScene(
Qt.vector3d(mouse.x, mouse.y,
view.mapFrom3DScene(selectedItem.position).z))
}
}
onReleased: function (mouse) {
mousePressed = false
}
}
Component.onCompleted: {
camera.lookAt(rootEntity)
}
}
}
After spending a while experimenting with different approaches, I found that mapping the mouse coordinates to the 3d space wasn't fully supported by the Qt API in terms of when the mouse is not fixed over an active object.
So, instead, the way that I made a workout was by casting a new RayCast each time the mouse moves and storing the offset when the mouse is pressed originally and then translating the item based on the result of the raycast and lining up the offset by translating by the normalized matrix with a small scalar.
onMouseXChanged: function (mouse) {
if (mousePressed) {
if (selectedItem != null) {
var result = view.pick(mouse.x, mouse.y)
if (result.objectHit) {
if (result.objectHit == selectedItem) {
var mouseGlobalPos = mouseArea.mapToGlobal(
mouse.x, mouse.y)
var mouseViewPos = view.mapFromGlobal(
mouseGlobalPos)
var mouseScenePos = result.scenePosition
var resultPos = result.position
/* here we subtract the result of the new raycast by the starting offset and then normalize
* the result and multiply it by a scalar 3 to determine the amount of offset the Model
* under the mouse is from where the mouse was originally pressed, so we can translate it */
var differencePos = resultPos.minus(
startMousePressSelectedItemLocalDragOffset).normalized(
).times(3)
selectedItem.position = selectedItem.position.plus(
differencePos)
I want to render two 3d models with qml and control the rotation, the obj2 is the child of obj1, how to make obj2 follow obj1's rotation. I tried the qml like that(but i don't know how to assign the rotate angle of obj2), main.qml:
Item{
Scene3D {
id: scene3D
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
focus: true
aspects: "input"
}
obj1 {
id: obj1
obj2 {
id: obj2
}
}
}
obj2.qml
Entity {
id: sceneRoot
Transform {
id: j2Transform
property real rotate_x : 0
property real rotate_y: 0
property real rotate_z : 0
Scale {
scale: 0.5
}
Rotate {
id: rotate_x
angle: // how to assign?
axis: Qt.vector3d(1, 0, 0)
}
Rotate {
id: rotate_y
angle: // how to assign?
axis: Qt.vector3d(0, 1, 0)
}
Rotate {
id: rotate_z
angle:// how to assign?
axis: Qt.vector3d(0, 0, 1)
}
Translate {
property real translation: 1
dy: 0
}
}
Mesh {
id: j2Mesh
source: "../res/obj/obj2.obj"
}
}
Thank you.
It took quite some time for me to figure out a problem I had about rotating a QML object, so I decided to share this.
The question:
How can I dynamically update the rotation of a QML element (like image). I know I can set, e.g. the rotation using a transformation (in the example of a MapQuickItem):
MapQuickItem
{
sourceItem: Image
{
id: image
anchors.centerIn: parent
source: "image.png"
sourceSize.width: Screen.devicePixelRatio * 256
sourceSize.height: Screen.devicePixelRatio * 256
transform: Rotation {
origin { x: image.sourceSize.width/2;
y: image.sourceSize.height/2;
z: 0}
angle: 0
}
}
}
However, how can I update the angle (or other parts of the transform) dynamically?
A cleaner way to do it would be using a property or an alias:
MapQuickItem
{
sourceItem: Image
{
id: image
property alias rotationAngle: rotation.angle
anchors.centerIn: parent
source: "image.png"
sourceSize.width: Screen.devicePixelRatio * 256
sourceSize.height: Screen.devicePixelRatio * 256
transform: Rotation {
id: rotation
origin { x: image.sourceSize.width/2;
y: image.sourceSize.height/2;
z: 0}
angle: 0
}
}
}
with:
function updatePositionAndRotation(angleDeg)
{
image.rotationAngle = angleDeg
}
The key information was that transform is actually a list of transformations.
So one possible solution to the above question is a function like this (e.g. changing the rotation):
function updateRotation(angleDeg)
{
image.transform[0].angle = angleDeg;
}
I want to create some sort of Vocabulary Trainer.
I have a Card QML File what shoud represent some kind of a record card where you can see the Vocabulary. When you've answered, the card should turn around 180° and a new Word/Text should be visible on it.
So far I've created a Rectangle for the Card and a Transformation for the Rotation split up in two PropertyAnimations.
For the sake of simplicity I just want the animation to happen when I'm clicking on the Card. Then the Card turns from 0 to 90 degrees. Afterwards the text should be changed. And at last the Card should turn from -90 to 0 degrees. So I'm looking for a logic that allows me to execute an animation, changes a property (text) instantly and executing another animation as a sequence.
Here is my Code so far:
import QtQuick 2.2
import QtGraphicalEffects 1.0
Item {
Rectangle {
id: card
anchors.fill: parent
border.width: 1
border.color: "grey"
antialiasing: true
Text {
id: question
text: "test test test"
anchors.centerIn: card
}
transform: Rotation {
id: rotation
origin.x: (card.width / 2)
origin.y: (card.height / 2)
axis {
x: 0
y: 1
z: 0
}
angle: 0
}
MouseArea {
anchors.fill: card
onClicked: {
// Code for Turning Card arround
rotate_away.start()
question.text = "abcabcabc"
rotate_new.start()
}
}
PropertyAnimation {
id: rotate_away
target: rotation
properties: "angle"
from: 0
to: 90
duration: 1000
}
PropertyAnimation {
id: rotate_new
target: rotation
properties: "angle"
from: -90
to: 0
duration: 1000
}
}
}
So the problem is this part:
rotate_away.start()
question.text = "abcabcabc"
rotate_new.start()
The text changes but only the 2'nd animation will be executed.
I tried
while (rotate_away.running) {}
to wait for the 1st animation but then the application gets stuck.
I think the animations should be played sequently by using SequentialAnimation. Please revisit your code as follows:
MouseArea {
anchors.fill: card
onClicked: {
// Code for Turning Card around
// rotate_away.start()
// question.text = "abcabcabc"
// rotate_new.start()
fullRotate.start();
}
}
SequentialAnimation {
id: fullRotate
PropertyAnimation {
id: rotate_away
target: rotation
properties: "angle"
from: 0
to: 90
duration: 1000
}
PropertyAction {
target: question
property: "text"
value: "abcabcabc"
}
PropertyAnimation {
id: rotate_new
target: rotation
properties: "angle"
from: -90
to: 0
duration: 1000
}
}
Also, I recommend Flipable which is meant for flipping effects.
Is there something like a wait() function for qtquick? I have as mousearea that calls an animation and a different state. When you click the mousearea they are both fired at the same time I need the state change to fire after the animation is completed.
MouseArea {
id: movie_mouse_mm
x: 392
y: 364
width: 104
height: 100
onClicked:{
image6.state = "rotated"
page.state = 'State1'
Logic.get_db(5,0);
}
}
So I need to get
page.state ='state'
to run after
image6.state= "rotated"
You have to use transitions & animations like that:
Item {
//...
MouseArea {
//...
onClicked:{
parent.state = "rotate"
//...
}
}
transitions: [
Transition {
to: "rotate"
SequentialAnimation {
RotationAnimation { target: image6; duration: 1000; direction: RotationAnimation.Clockwise }
PropertyAction { target: page; property: "state"; value: "state" }
}
}
]
}
You can adjust duration
More information here.