- Qt 6.7
- Qt Quick 3D
- Qt Quick 3D Examples and Tutorials
- Qt Quick 3D - Volumetric Rendering Example
- Qt Quick 3D | Commercial or GPLv3

# 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)
                     }
                 }
             }
         }
     }