Qt Quick 3D Physics - Custom Shapes Example

Demonstrates using different shapes.

This example demonstrates loading and spawning several rigid body meshes as well as animating them. The scene consists of a dice tower, a weave, a cup and a handful of dices. The cup is animated to collect spawning dices and put them in the dice tower. The dices will then roll down and out on the weave.

QML

This is the full qml code for the example:

Window {
    width: 1280
    height: 720
    visible: true
    title: qsTr("QtQuick3DPhysics Custom Shapes")

    DynamicsWorld {
        id: physicsWorld
        running: true
        typicalLength: 2
        enableCCD: true
    }

    View3D {
        id: viewport
        anchors.fill: parent

        environment: SceneEnvironment {
            clearColor: "white"
            backgroundMode: SceneEnvironment.SkyBox
            antialiasingMode: SceneEnvironment.MSAA
            antialiasingQuality: SceneEnvironment.High
            lightProbe: proceduralSky
        }

        Texture {
            id: proceduralSky
            textureData: ProceduralSkyTextureData {
                sunLongitude: -115
            }
        }

        Texture {
            id: weaveNormal
            source: "maps/weave.png"
            scaleU: 200
            scaleV: 200
            generateMipmaps: true
            mipFilter: Texture.Linear
        }

        Texture {
            id: numberNormal
            source: "maps/numbers-normal.png"
        }

        Texture {
            id: numberFill
            source: "maps/numbers.png"
            generateMipmaps: true
            mipFilter: Texture.Linear
        }

        Node {
            id: scene
            scale: Qt.vector3d(2, 2, 2)
            PerspectiveCamera {
                id: camera
                position: Qt.vector3d(-45, 20, 60)
                eulerRotation: Qt.vector3d(-6, -13, 0)
                clipFar: 1000
                clipNear: 0.1
            }

            DirectionalLight {
                eulerRotation: Qt.vector3d(-45, 25, 0)
                castsShadow: true
                brightness: 1
                shadowMapQuality: Light.ShadowMapQualityVeryHigh
            }

            StaticRigidBody {
                position: Qt.vector3d(-15, -8, 0)
                id: tablecloth

                Model {
                    geometry: HeightFieldGeometry {
                        id: tableclothGeometry
                        extents: Qt.vector3d(150, 20, 150)
                        heightMap: "maps/cloth-heightmap.png"
                        smoothShading: false
                    }
                    materials: PrincipledMaterial {
                        baseColor: "#447722"
                        roughness: 0.8
                        normalMap: weaveNormal
                        normalStrength: 0.7
                    }
                }

                collisionShapes: HeightFieldShape {
                    id: hfShape
                    extents: tableclothGeometry.extents
                    heightMap: "maps/cloth-heightmap.png"
                }
            }

            DynamicRigidBody {
                id: diceCup
                isKinematic: true
                mass: 0
                property vector3d restPos: Qt.vector3d(11, 6, 0)
                position: restPos
                pivot: Qt.vector3d(0, 6, 0)
                collisionShapes: TriangleMeshShape {
                    id: cupShape
                    meshSource: "meshes/simpleCup.mesh"
                }
                Model {
                    source: "meshes/cup.mesh"
                    materials: PrincipledMaterial {
                        baseColor: "#cc9988"
                        roughness: 0.3
                        metalness: 1
                    }
                }
                Behavior on eulerRotation.z {
                    NumberAnimation { duration: 1500 }
                }
                Behavior on position {
                    PropertyAnimation { duration: 1500 }
                }
            }

            StaticRigidBody {
                id: diceTower
                x: -4
                Model {
                    id: testModel
                    source: "meshes/tower.mesh"
                    materials: [
                        PrincipledMaterial {
                            baseColor: "#ccccce"
                            roughness: 0.3
                        },
                        PrincipledMaterial {
                            id: glassMaterial
                            baseColor: "#aaaacc"
                            transmissionFactor: 0.95
                            thicknessFactor: 1
                            roughness: 0.05
                        }
                    ]
                }
                collisionShapes: TriangleMeshShape {
                    id: triShape
                    meshSource: "meshes/tower.mesh"
                }
            }

            Component {
                id: diceComponent

                DynamicRigidBody {
                    id: thisBody
                    function randomInRange(min, max) {
                        return Math.random() * (max - min) + min;
                    }

                    function restore() {
                        reset(initialPosition, eulerRotation)
                    }

                    scale: Qt.vector3d(scaleFactor, scaleFactor, scaleFactor)
                    eulerRotation: Qt.vector3d(randomInRange(0, 360),
                                               randomInRange(0, 360),
                                               randomInRange(0, 360))

                    property vector3d initialPosition: Qt.vector3d(11 + 1.5*Math.cos(index/(Math.PI/4)),
                                                                   5 + index * 1.5,
                                                                   0)
                    position: initialPosition

                    property real scaleFactor: randomInRange(0.8, 1.4)
                    property color baseCol: Qt.hsla(randomInRange(0, 1),
                                                    randomInRange(0.6, 1.0),
                                                    randomInRange(0.4, 0.7),
                                                    1.0)

                    collisionShapes: ConvexMeshShape {
                        id: diceShape
                        meshSource: Math.random() < 0.25 ? "meshes/icosahedron.mesh"
                                  : Math.random() < 0.5 ? "meshes/dodecahedron.mesh"
                                  : Math.random() < 0.75 ? "meshes/octahedron.mesh"
                                                         : "meshes/tetrahedron.mesh"
                    }

                    Model {
                        id: thisModel
                        source: diceShape.meshSource
                        materials: PrincipledMaterial {
                            metalness: 1.0
                            roughness: randomInRange(0.2, 0.6)
                            baseColor: baseCol
                            emissiveMap: numberFill
                            emissiveFactor: Qt.vector3d(1, 1, 1)
                            normalMap: numberNormal
                            normalStrength: 0.75
                        }
                    }
                }
            }

            Repeater3D {
                id: dicePool
                model: 25
                delegate: diceComponent
                function restore() {
                    for (let i = 0; i < count; i++) {
                        objectAt(i).restore()
                    }
                }
            }

            SequentialAnimation {
                running: physicsWorld.running
                PauseAnimation { duration: 1500 }
                ScriptAction { script: diceCup.position = Qt.vector3d(4, 45, 0) }
                PauseAnimation { duration: 1500 }
                ScriptAction { script: { diceCup.eulerRotation.z = 130; diceCup.position = Qt.vector3d(0, 45, 0) } }
                PauseAnimation { duration: 3000 }
                ScriptAction { script: { diceCup.eulerRotation.z = 0; diceCup.position = Qt.vector3d(4, 45, 0) } }
                PauseAnimation { duration: 1500 }
                ScriptAction { script: diceCup.position = diceCup.restPos }
                PauseAnimation { duration: 2000 }
                ScriptAction { script: dicePool.restore() }
                loops: Animation.Infinite
            }
        } // scene
    } // View3D

    WasdController {
        keysEnabled: true
        controlledObject: camera
        speed: 0.2
    }
}
Enivronment

As usual it contains a DynamicsWorld and a View3D. In the View3D we have our environment which sets up a lightprobe:

environment: SceneEnvironment {
    clearColor: "white"
    backgroundMode: SceneEnvironment.SkyBox
    antialiasingMode: SceneEnvironment.MSAA
    antialiasingQuality: SceneEnvironment.High
    lightProbe: proceduralSky
}
Textures

We define four textures which will be used for the skybox, the weave and the numbers on the dice:

Texture {
    id: proceduralSky
    textureData: ProceduralSkyTextureData {
        sunLongitude: -115
    }
}

Texture {
    id: weaveNormal
    source: "maps/weave.png"
    scaleU: 200
    scaleV: 200
    generateMipmaps: true
    mipFilter: Texture.Linear
}

Texture {
    id: numberNormal
    source: "maps/numbers-normal.png"
}

Texture {
    id: numberFill
    source: "maps/numbers.png"
    generateMipmaps: true
    mipFilter: Texture.Linear
}
Scene

We have a Node which contains our scene with the camera and a directional light:

id: scene
scale: Qt.vector3d(2, 2, 2)
PerspectiveCamera {
    id: camera
    position: Qt.vector3d(-45, 20, 60)
    eulerRotation: Qt.vector3d(-6, -13, 0)
    clipFar: 1000
    clipNear: 0.1
}

DirectionalLight {
    eulerRotation: Qt.vector3d(-45, 25, 0)
    castsShadow: true
    brightness: 1
    shadowMapQuality: Light.ShadowMapQualityVeryHigh
}
Weave

We add the weave which is a StaticRigidBody consisting of a model with a weave texture and a HeightFieldShape for collision.

StaticRigidBody {
    position: Qt.vector3d(-15, -8, 0)
    id: tablecloth

    Model {
        geometry: HeightFieldGeometry {
            id: tableclothGeometry
            extents: Qt.vector3d(150, 20, 150)
            heightMap: "maps/cloth-heightmap.png"
            smoothShading: false
        }
        materials: PrincipledMaterial {
            baseColor: "#447722"
            roughness: 0.8
            normalMap: weaveNormal
            normalStrength: 0.7
        }
    }

    collisionShapes: HeightFieldShape {
        id: hfShape
        extents: tableclothGeometry.extents
        heightMap: "maps/cloth-heightmap.png"
    }
}
Cup

We define the cup as a DynamicRigidBody with a Model and a TriangleMeshShape as the collision shape. It has a Behavior on the eulerRotation and position properties as these are part of an animation.

DynamicRigidBody {
    id: diceCup
    isKinematic: true
    mass: 0
    property vector3d restPos: Qt.vector3d(11, 6, 0)
    position: restPos
    pivot: Qt.vector3d(0, 6, 0)
    collisionShapes: TriangleMeshShape {
        id: cupShape
        meshSource: "meshes/simpleCup.mesh"
    }
    Model {
        source: "meshes/cup.mesh"
        materials: PrincipledMaterial {
            baseColor: "#cc9988"
            roughness: 0.3
            metalness: 1
        }
    }
    Behavior on eulerRotation.z {
        NumberAnimation { duration: 1500 }
    }
    Behavior on position {
        PropertyAnimation { duration: 1500 }
    }
}
Tower

The tower is just a StaticRigidBody with a Model and a TriangleMeshShape for collision.

StaticRigidBody {
    id: diceTower
    x: -4
    Model {
        id: testModel
        source: "meshes/tower.mesh"
        materials: [
            PrincipledMaterial {
                baseColor: "#ccccce"
                roughness: 0.3
            },
            PrincipledMaterial {
                id: glassMaterial
                baseColor: "#aaaacc"
                transmissionFactor: 0.95
                thicknessFactor: 1
                roughness: 0.05
            }
        ]
    }
    collisionShapes: TriangleMeshShape {
        id: triShape
        meshSource: "meshes/tower.mesh"
    }
}
Dices

To generate the dices we use a Component and a Repeater3D. The Component contains a DynamicRigidBody with a ConvexMeshShape and a Model. The position, color, scale and mesh source are randomly generated for each die.

Component {
    id: diceComponent

    DynamicRigidBody {
        id: thisBody
        function randomInRange(min, max) {
            return Math.random() * (max - min) + min;
        }

        function restore() {
            reset(initialPosition, eulerRotation)
        }

        scale: Qt.vector3d(scaleFactor, scaleFactor, scaleFactor)
        eulerRotation: Qt.vector3d(randomInRange(0, 360),
                                   randomInRange(0, 360),
                                   randomInRange(0, 360))

        property vector3d initialPosition: Qt.vector3d(11 + 1.5*Math.cos(index/(Math.PI/4)),
                                                       5 + index * 1.5,
                                                       0)
        position: initialPosition

        property real scaleFactor: randomInRange(0.8, 1.4)
        property color baseCol: Qt.hsla(randomInRange(0, 1),
                                        randomInRange(0.6, 1.0),
                                        randomInRange(0.4, 0.7),
                                        1.0)

        collisionShapes: ConvexMeshShape {
            id: diceShape
            meshSource: Math.random() < 0.25 ? "meshes/icosahedron.mesh"
                      : Math.random() < 0.5 ? "meshes/dodecahedron.mesh"
                      : Math.random() < 0.75 ? "meshes/octahedron.mesh"
                                             : "meshes/tetrahedron.mesh"
        }

        Model {
            id: thisModel
            source: diceShape.meshSource
            materials: PrincipledMaterial {
                metalness: 1.0
                roughness: randomInRange(0.2, 0.6)
                baseColor: baseCol
                emissiveMap: numberFill
                emissiveFactor: Qt.vector3d(1, 1, 1)
                normalMap: numberNormal
                normalStrength: 0.75
            }
        }
    }
}

Repeater3D {
    id: dicePool
    model: 25
    delegate: diceComponent
    function restore() {
        for (let i = 0; i < count; i++) {
            objectAt(i).restore()
        }
    }
}
Animation

To make the dices move from the cup to the dice tower we animate the cup and move it up and then tip it over. This is done using a SequentialAnimation:

SequentialAnimation {
    running: physicsWorld.running
    PauseAnimation { duration: 1500 }
    ScriptAction { script: diceCup.position = Qt.vector3d(4, 45, 0) }
    PauseAnimation { duration: 1500 }
    ScriptAction { script: { diceCup.eulerRotation.z = 130; diceCup.position = Qt.vector3d(0, 45, 0) } }
    PauseAnimation { duration: 3000 }
    ScriptAction { script: { diceCup.eulerRotation.z = 0; diceCup.position = Qt.vector3d(4, 45, 0) } }
    PauseAnimation { duration: 1500 }
    ScriptAction { script: diceCup.position = diceCup.restPos }
    PauseAnimation { duration: 2000 }
    ScriptAction { script: dicePool.restore() }
    loops: Animation.Infinite
}
Controller

Finally a WasdController is added to be able to control the camera using a keyboard:

WasdController {
    keysEnabled: true
    controlledObject: camera
    speed: 0.2
}

Files:

Images:

© 2022 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.