Qt Quick 3D - Particles 3D Testbed Example

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick3D
import QtQuick3D.Particles3D

Item {
    id: mainWindow

    readonly property real meterTicksAngle: 300
    readonly property bool particleNeedle: checkBoxParticlesNeedle.checked
    property real separation: sliderElementSeparation.sliderValue

    anchors.fill: parent

    Behavior on separation {
        SmoothedAnimation {
            velocity: 0.2
            duration: 1000
        }
    }

    View3D {
        id: view3D
        anchors.fill: parent

        environment: SceneEnvironment {
            clearColor: "#161610"
            backgroundMode: SceneEnvironment.Color
            antialiasingMode: AppSettings.antialiasingMode
            antialiasingQuality: AppSettings.antialiasingQuality
        }

        PerspectiveCamera {
            position: Qt.vector3d(0, 100, 600 - mainWindow.separation * 100)
        }

        PointLight {
            position: Qt.vector3d(200, 400, 300)
            brightness: 5
            ambientColor: Qt.rgba(0.3, 0.3, 0.3, 1.0)
        }

        // Background
        Model {
            source: "#Rectangle"
            scale: Qt.vector3d(25, 15, 0)
            z: -300
            materials: PrincipledMaterial {
                baseColor: "#505040"
            }
        }

        Node {
            id: speedometerComponent

            position: Qt.vector3d(0, 100, 0)
            eulerRotation.x: 90 - mainWindow.separation * 75

            // Light which follows the needle
            PointLight {
                // Calculates the direction of the needle
                function getNeedleAngle(startAngle) {
                    return Math.sin(startAngle +
                                    (-(180 / 360) +
                                     (-gaugeItem.value * mainWindow.meterTicksAngle / 360) +
                                     (mainWindow.meterTicksAngle / 2 + 180) / 360) * 2 * Math.PI);
                }

                readonly property real lightRadius: 120
                readonly property real posX: getNeedleAngle(Math.PI) * lightRadius
                readonly property real posY: getNeedleAngle(Math.PI / 2) * lightRadius

                position: Qt.vector3d(posX, 40, -posY)
                color: Qt.rgba(1.0, 0.8, 0.4, 1.0)
                brightness: sliderNeedleBrightness.sliderValue
                quadraticFade: 4.0
            }

            Model {
                y: -4 - mainWindow.separation * 100
                source: "meshes/meter_background.mesh"
                scale: Qt.vector3d(30, 30, 30)
                materials: PrincipledMaterial {
                    baseColor: "#505050"
                    metalness: 1.0
                    roughness: 0.6
                    normalMap: Texture {
                        source: "images/leather_n.png"
                    }
                    normalStrength: 0.4
                }
            }
            Model {
                y: -35 - mainWindow.separation * 100
                source: "#Sphere"
                scale: Qt.vector3d(1.6, 0.2, 1.6)
                materials: PrincipledMaterial {
                    baseColor: "#606060"
                }
            }
            Model {
                y: -mainWindow.separation * 60
                source: "#Cylinder"
                scale: Qt.vector3d(0.2, 0.8, 0.2)
                materials: PrincipledMaterial {
                    baseColor: "#606060"
                }
            }
            Model {
                y: 30 - mainWindow.separation * 60
                source: "#Sphere"
                scale: Qt.vector3d(0.4, 0.1, 0.4)
                materials: PrincipledMaterial {
                    baseColor: "#f0f0f0"
                }
            }

            // Speedometer labels
            Model {
                y: 25 + mainWindow.separation * 20
                source: "#Rectangle"
                scale: Qt.vector3d(4, 4, 4)
                eulerRotation.x: -90
                materials: PrincipledMaterial {
                    alphaMode: PrincipledMaterial.Blend
                    baseColorMap: Texture {
                        source: "images/speedometer_labels.png"
                    }
                }
            }

            Model {
                y: mainWindow.separation * 60
                source: "meshes/meter_edge.mesh"
                scale: Qt.vector3d(30, 30, 30)
                materials: PrincipledMaterial {
                    baseColor: "#b0b0b0"
                }
            }

            Node {
                id: gaugeItem

                property real value: 0
                property real needleSize: 180

                SequentialAnimation {
                    running: checkBoxAnimateSpeed.checked
                    loops: Animation.Infinite
                    NumberAnimation {
                        target: gaugeItem
                        property: "value"
                        duration: 8000
                        to: 1
                        easing.type: Easing.InOutQuad
                    }
                    NumberAnimation {
                        target: gaugeItem
                        property: "value"
                        duration: 8000
                        to: 0
                        easing.type: Easing.InOutQuad
                    }
                }

                y: 20 - mainWindow.separation * 60
                eulerRotation.z: 90
                eulerRotation.y: -mainWindow.meterTicksAngle * value + (mainWindow.meterTicksAngle/2 - 90)

                Model {
                    position.y: gaugeItem.needleSize / 2
                    source: "#Cylinder"
                    scale: Qt.vector3d(0.05, gaugeItem.needleSize * 0.01, 0.2)
                    materials: PrincipledMaterial {
                        baseColor: "#606060"
                        opacity: mainWindow.particleNeedle ? 0.0 : 1.0
                    }

                    ParticleEmitter3D {
                        system: psystemGauge
                        particle: particleSpark
                        y: 10
                        scale: Qt.vector3d(0.1, 0.8, 0.1)
                        shape: ParticleShape3D {
                            type: ParticleShape3D.Cube
                        }
                        particleScale: 2.0
                        particleScaleVariation: 1.0
                        particleRotationVariation: Qt.vector3d(0, 0, 180)
                        particleRotationVelocityVariation: Qt.vector3d(20, 20, 200);
                        emitRate: 500
                        lifeSpan: 2000
                        lifeSpanVariation: 1000
                    }

                    // Needle particle system
                    // This system rotates together with the needle
                    ParticleSystem3D {
                        id: psystemNeedle
                        running: mainWindow.particleNeedle
                        visible: running
                        SpriteParticle3D {
                            id: needleParticle
                            sprite: Texture {
                                source: "images/dot.png"
                            }
                            maxAmount: 500
                            fadeInDuration: 50
                            fadeOutDuration: 200
                            color: "#40808020"
                            colorVariation: Qt.vector4d(0.2, 0.2, 0.0, 0.2)
                            blendMode: SpriteParticle3D.Screen
                        }

                        ParticleEmitter3D {
                            particle: needleParticle
                            y: -50
                            scale: Qt.vector3d(0.2, 0.0, 0.2)
                            shape: ParticleShape3D {
                                type: ParticleShape3D.Cube
                            }

                            particleScale: 4.0
                            particleScaleVariation: 2.0
                            particleEndScale: 1.0
                            particleEndScaleVariation: 0.5
                            velocity: VectorDirection3D {
                                direction: Qt.vector3d(0, 110, 0)
                                directionVariation: Qt.vector3d(10, 10, 10)
                            }
                            emitRate: sliderNeedleEmitrate.sliderValue
                            lifeSpan: 1000
                        }
                    }
                }
            }
            // Gauge particle system
            ParticleSystem3D {
                id: psystemGauge
                // Particles coming under the needle
                SpriteParticle3D {
                    id: particleSpark
                    sprite: Texture {
                        source: "images/star3.png"
                    }
                    maxAmount: 1500
                    fadeInEffect: SpriteParticle3D.FadeNone
                    fadeOutDuration: 200
                    color: "#40ff0000"
                    colorVariation: Qt.vector4d(0.4, 0.2, 0.2, 0.2)
                    unifiedColorVariation: true
                    blendMode: SpriteParticle3D.Screen
                }
                // Particles rotating the gauge
                SpriteParticle3D {
                    id: particleSpark2
                    sprite: Texture {
                        source: "images/star3.png"
                    }
                    maxAmount: 30
                    fadeInDuration: 1000
                    fadeOutDuration: 1000
                    color: "#20ffffff"
                    colorVariation: Qt.vector4d(0.2, 0.2, 0.5, 0.1)
                    blendMode: SpriteParticle3D.Screen
                }
                ParticleEmitter3D {
                    y: 40 + mainWindow.separation * 60
                    particle: particleSpark2
                    scale: Qt.vector3d(3.8, 0, 3.8)
                    shape: ParticleShape3D {
                        type: ParticleShape3D.Cylinder
                    }
                    emitRate: 10
                    lifeSpan: 3000
                    particleScale: 4.0
                    particleScaleVariation: 3.0
                    particleRotationVariation: Qt.vector3d(20, 20, 180)
                    particleRotationVelocityVariation: Qt.vector3d(10, 10, 50)
                }
                Wander3D {
                    uniqueAmount: Qt.vector3d(10, 0, 10)
                    uniquePace: Qt.vector3d(0.1, 0.1, 0.1)
                    fadeInDuration: 2000
                }
                PointRotator3D {
                    particles: [ particleSpark2 ]
                    magnitude: -20
                }
            }
        }
    }

    SettingsView {
        CustomCheckBox {
            id: checkBoxAnimateSpeed
            text: "Animate Speed"
            checked: true
        }
        CustomLabel {
            text: "Speed"
            opacity: !checkBoxAnimateSpeed.checked ? 1.0 : 0.4
        }
        CustomSlider {
            id: sliderSpeedValue
            sliderValue: gaugeItem.value
            sliderEnabled: !checkBoxAnimateSpeed.checked
            fromValue: 0
            toValue: 1
            onSliderValueChanged: gaugeItem.value = sliderValue;
        }
        CustomCheckBox {
            id: checkBoxParticlesNeedle
            text: "Particles Needle"
            checked: true
        }
        CustomLabel {
            text: "Needle emitRate"
            opacity: checkBoxParticlesNeedle.checked ? 1.0 : 0.4
        }
        CustomSlider {
            id: sliderNeedleEmitrate
            sliderValue: 200
            sliderEnabled: checkBoxParticlesNeedle.checked
            fromValue: 50
            toValue: 500
        }
        CustomLabel {
            text: "Needle Brightness"
        }
        CustomSlider {
            id: sliderNeedleBrightness
            sliderValue: 10
            fromValue: 0
            toValue: 50
        }
        CustomLabel {
            text: "Elements Separation"
        }
        CustomSlider {
            id: sliderElementSeparation
            sliderValue: 0
            fromValue: 0
            toValue: 1
        }
    }

    LoggingView {
        anchors.bottom: parent.bottom
        particleSystems: [psystemGauge, psystemNeedle]
    }
}