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: