Qt Quick 3D - Volumetric Rendering Example
// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick import QtQuick3D Item { id: root required property Node targetNode enum Axis { PositiveZ = 0, NegativeZ = 1, PositiveY = 2, NegativeY = 3, PositiveX = 4, NegativeX = 5 } // These are the 24 different rotations a rotation aligned on axes can have. // They are ordered in groups of 4 where the +Z,-Z,+Y,-Y,+X,-X axis is pointing // towards the screen (+Z). Inside this group the rotations are ordered to // rotate counter-clockwise. readonly property list<quaternion> rotations: [ // +Z Qt.quaternion(1, 0, 0, 0), Qt.quaternion(Math.SQRT1_2, 0, 0, -Math.SQRT1_2), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(Math.SQRT1_2, 0, 0, Math.SQRT1_2), // -Z Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -Math.SQRT1_2, -Math.SQRT1_2, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, Math.SQRT1_2, -Math.SQRT1_2, 0), // +Y Qt.quaternion(0.5, 0.5, 0.5, 0.5), Qt.quaternion(Math.SQRT1_2, Math.SQRT1_2, 0, 0), Qt.quaternion(-0.5, -0.5, 0.5, 0.5), Qt.quaternion(0, 0, -Math.SQRT1_2, -Math.SQRT1_2), // -Y Qt.quaternion(0.5, -0.5, 0.5, -0.5), Qt.quaternion(0, 0, Math.SQRT1_2, -Math.SQRT1_2), Qt.quaternion(-0.5, 0.5, 0.5, -0.5), Qt.quaternion(-Math.SQRT1_2, Math.SQRT1_2, 0, 0), // +X Qt.quaternion(-0.5, -0.5, 0.5, -0.5), Qt.quaternion(-Math.SQRT1_2, 0, Math.SQRT1_2, 0), Qt.quaternion(-0.5, 0.5, 0.5, 0.5), Qt.quaternion(0, Math.SQRT1_2, 0, Math.SQRT1_2), // -X Qt.quaternion(0, Math.SQRT1_2, 0, -Math.SQRT1_2), Qt.quaternion(0.5, -0.5, 0.5, 0.5), Qt.quaternion(Math.SQRT1_2, 0, Math.SQRT1_2, 0), Qt.quaternion(0.5, 0.5, 0.5, -0.5), ] readonly property list<quaternion> xRotationGoals : [ Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(-0, -1, -0, -0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(-0, -1, -0, -0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(-0, 1, -0, -0), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), ] readonly property list<quaternion> yRotationGoals : [ Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), ] readonly property list<quaternion> zRotationGoals : [ Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, 1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 0, 0, -1), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 0, -1, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, -1, 0, 0), Qt.quaternion(0, 0, 1, 0), Qt.quaternion(0, 1, 0, 0), Qt.quaternion(0, 0, -1, 0), ] // This function works by using a rotation to rotate x,y,z normal vectors // and see what axis-aligned rotation gives the closest distance to the // rotated normal vectors. function findClosestRotation(rotation, startI, stopI) { let rotationConjugated = rotation.conjugated(); let xRotated = rotation.times(Qt.quaternion(0, 1, 0, 0)).times(rotationConjugated); let yRotated = rotation.times(Qt.quaternion(0, 0, 1, 0)).times(rotationConjugated); let zRotated = rotation.times(Qt.quaternion(0, 0, 0, 1)).times(rotationConjugated); var closestIndex = 0; var closestDistance = 123456789; // big number for (var i = startI; i < stopI ; i++) { let distance = xRotated.minus(xRotationGoals[i]).length() + yRotated.minus(yRotationGoals[i]).length() + zRotated.minus(zRotationGoals[i]).length(); if (distance <= closestDistance) { closestDistance = distance; closestIndex = i; } } return closestIndex; } function quaternionAlign(rotation) { let closestIndex = findClosestRotation(rotation, 0, 24); return rotations[closestIndex]; } function quaternionForAxis(axis, rotation) { let closestIndex = findClosestRotation(rotation, axis*4, (axis + 1)*4); return rotations[closestIndex]; } function quaternionRotateLeft(rotation) { let closestIndex = findClosestRotation(rotation, 0, 24); let offset = (4 + closestIndex - 1) % 4; let group = Math.floor(closestIndex / 4); return rotations[offset + group * 4]; } function quaternionRotateRight(rotation) { let closestIndex = findClosestRotation(rotation, 0, 24); let offset = (closestIndex + 1) % 4; let group = Math.floor(closestIndex / 4); return rotations[offset + group * 4]; } signal axisClicked(int axis) signal ballMoved(vector2d velocity) QtObject { id: stylePalette property color white: "#fdf6e3" property color black: "#002b36" property color red: "#dc322f" property color green: "#859900" property color blue: "#268bd2" property color background: "#99002b36" } component LineRectangle : Rectangle { property vector2d startPoint: Qt.vector2d(0, 0) property vector2d endPoint: Qt.vector2d(0, 0) property real lineWidth: 5 transformOrigin: Item.Left height: lineWidth readonly property vector2d offset: startPoint.plus(endPoint).times(0.5); width: startPoint.minus(endPoint).length() rotation: Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x) * 180 / Math.PI } Rectangle { id: ballBackground anchors.centerIn: parent width: parent.width > parent.height ? parent.height : parent.width height: width radius: width / 2 color: ballBackgroundHoverHandler.hovered ? stylePalette.background : "transparent" readonly property real subBallWidth: width / 5 readonly property real subBallHalfWidth: subBallWidth * 0.5 readonly property real subBallOffset: radius - subBallWidth / 2 Item { anchors.centerIn: parent component SubBall : Rectangle { id: subBallRoot required property Node targetNode required property real offset property alias labelText: label.text property alias labelColor: label.color property alias labelVisible: label.visible property alias hovered: subBallHoverHandler.hovered property var initialPosition: Qt.vector3d(0, 0, 0) readonly property vector3d position: quaternionVectorMultiply(targetNode.rotation, initialPosition) signal tapped() function quaternionVectorMultiply(q, v) { var qv = Qt.vector3d(q.x, q.y, q.z) var uv = qv.crossProduct(v) var uuv = qv.crossProduct(uv) uv = uv.times(2.0 * q.scalar) uuv = uuv.times(2.0) return v.plus(uv).plus(uuv) } height: width radius: width / 2 x: offset * position.x - width / 2 y: offset * -position.y - height / 2 z: position.z HoverHandler { id: subBallHoverHandler } TapHandler { acceptedButtons: Qt.LeftButton onTapped: (eventPoint, button)=>{ subBallRoot.tapped() //eventPoint.accepted = true } } Text { id: label anchors.centerIn: parent } } SubBall { id: positiveX targetNode: root.targetNode width: ballBackground.subBallWidth offset: ballBackground.subBallOffset labelText: "X" labelColor: hovered ? stylePalette.white : stylePalette.black color: stylePalette.red initialPosition: Qt.vector3d(1, 0, 0) onTapped: { root.axisClicked(OriginGizmo.Axis.PositiveX) } } LineRectangle { endPoint: Qt.vector2d(positiveX.x + ballBackground.subBallHalfWidth, positiveX.y + ballBackground.subBallHalfWidth) color: stylePalette.red z: positiveX.z - 0.001 } SubBall { id: positiveY targetNode: root.targetNode width: ballBackground.subBallWidth offset: ballBackground.subBallOffset labelText: "Y" labelColor: hovered ? stylePalette.white : stylePalette.black color: stylePalette.green initialPosition: Qt.vector3d(0, 1, 0) onTapped: { root.axisClicked(OriginGizmo.Axis.PositiveY) } } LineRectangle { endPoint: Qt.vector2d(positiveY.x + ballBackground.subBallHalfWidth, positiveY.y + ballBackground.subBallHalfWidth) color: stylePalette.green z: positiveY.z - 0.001 } SubBall { id: positiveZ targetNode: root.targetNode width: ballBackground.subBallWidth offset: ballBackground.subBallOffset labelText: "Z" labelColor: hovered ? stylePalette.white : stylePalette.black color: stylePalette.blue initialPosition: Qt.vector3d(0, 0, 1) onTapped: { root.axisClicked(OriginGizmo.Axis.PositiveZ) } } LineRectangle { endPoint: Qt.vector2d(positiveZ.x + ballBackground.subBallHalfWidth, positiveZ.y + ballBackground.subBallHalfWidth) color: stylePalette.blue z: positiveZ.z - 0.001 } SubBall { targetNode: root.targetNode width: ballBackground.subBallWidth offset: ballBackground.subBallOffset labelText: "-X" labelColor: stylePalette.white labelVisible: hovered color: Qt.rgba(stylePalette.red.r, stylePalette.red.g, stylePalette.red.b, z + 1 * 0.5) border.color: stylePalette.red border.width: 2 initialPosition: Qt.vector3d(-1, 0, 0) onTapped: { root.axisClicked(OriginGizmo.Axis.NegativeX) } } SubBall { targetNode: root.targetNode width: ballBackground.subBallWidth offset: ballBackground.subBallOffset labelText: "-Y" labelColor: stylePalette.white labelVisible: hovered color: Qt.rgba(stylePalette.green.r, stylePalette.green.g, stylePalette.green.b, z + 1 * 0.5) border.color: stylePalette.green border.width: 2 initialPosition: Qt.vector3d(0, -1, 0) onTapped: { root.axisClicked(OriginGizmo.Axis.NegativeY) } } SubBall { targetNode: root.targetNode width: ballBackground.subBallWidth offset: ballBackground.subBallOffset labelText: "-Z" labelColor: stylePalette.white labelVisible: hovered color: Qt.rgba(stylePalette.blue.r, stylePalette.blue.g, stylePalette.blue.b, z + 1 * 0.5) border.color: stylePalette.blue border.width: 2 initialPosition: Qt.vector3d(0, 0, -1) onTapped: { root.axisClicked(OriginGizmo.Axis.NegativeZ) } } } HoverHandler { id: ballBackgroundHoverHandler acceptedDevices: PointerDevice.Mouse cursorShape: Qt.PointingHandCursor } DragHandler { id: dragHandler target: null enabled: ballBackground.visible onCentroidChanged: { if (centroid.velocity.x > 0 && centroid.velocity.y > 0) { root.ballMoved(centroid.velocity) } } } } }