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