- Qt 6.4
- Qt Quick 3D Physics
- Qt Quick 3D Physics Examples and Tutorials
- Qt Quick 3D Physics - Custom Shapes Example
- Qt Quick 3D Physics | Commercial or GPLv3

# 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:

- customshapes/CMakeLists.txt
- customshapes/customshapes.pro
- customshapes/main.cpp
- customshapes/main.qml
- customshapes/qml.qrc
- customshapes/resources.qrc

Images:

- customshapes/maps/cloth-heightmap.png
- customshapes/maps/numbers-normal.png
- customshapes/maps/numbers.png
- customshapes/maps/weave.png