Qt Quick 3D Physics - Custom Shapes Example | Qt Quick 3D Physics | Qt 6.12.0
Description: Demonstrates using different shapes.
Keywords: example

- Qt 6.12
- 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 tablecloth, a cup and a handful of dice. The cup is animated to collect spawning dice and put them in the dice tower. The dice will then roll down and out on the tablecloth.

#### Environment

As usual we have a PhysicsWorld 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 tablecloth 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, 25, 60)
         eulerRotation: Qt.vector3d(-6, -33, 0)
         clipFar: 1000
         clipNear: 0.1
     }

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

#### Tablecloth

We add the tablecloth 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)
                 source: "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
             source: "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 bottomPos: Qt.vector3d(11, 6, 0)
         property vector3d topPos: Qt.vector3d(11, 45, 0)
         property vector3d unloadPos: Qt.vector3d(0, 45, 0)
         position: bottomPos
         kinematicPivot: Qt.vector3d(0, 6, 0)
         kinematicPosition: bottomPos
         collisionShapes: TriangleMeshShape {
             id: cupShape
             source: "meshes/simpleCup.mesh"
         }
         Model {
             source: "meshes/cup.mesh"
             materials: PrincipledMaterial {
                 baseColor: "#cc9988"
                 roughness: 0.3
                 metalness: 1
             }
         }
     }

#### 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
             source: "meshes/tower.mesh"
         }
     }

#### Dice

To generate the dice we use a Component with a custom Die QML object and a Repeater3D.

     Component {
         id: diceComponent
         Die {}
     }

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

The Die object contains a DynamicRigidBody with a ConvexMeshShape and a Model. The position, color, scale and mesh source are randomly generated for each die.

     DynamicRigidBody {
         id: thisBody
         required property int index

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

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

         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)),
                                                        6 + 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
             source: 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.source
             receivesShadows: false
             materials: PrincipledMaterial {
                 metalness: 1.0
                 roughness: thisBody.randomInRange(0.2, 0.6)
                 baseColor: thisBody.baseCol
                 emissiveMap: numberFill
                 emissiveFactor: Qt.vector3d(1, 1, 1)
                 normalMap: numberNormal
                 normalStrength: 0.75
             }
         }
     }

#### Animation

To make the dice move from the cup to the dice tower we animate the cup and move it up and then tip it over. To make sure that the animation stays in sync with the physical simulation we use an AnimationController which we connect to the `onFrameDone` signal on the PhysicsWorld. After every simulated frame we progress the animation with the elapsed timestep.

     Connections {
         target: physicsWorld
         property real totalAnimationTime: 7500
         function onFrameDone(timeStep) {
             let progressStep = timeStep / totalAnimationTime
             animationController.progress += progressStep
             if (animationController.progress >= 1) {
                 animationController.completeToEnd()
                 animationController.reload()
                 animationController.progress = 0
             }
         }
     }

     AnimationController {
         id: animationController
         animation: SequentialAnimation {
             PauseAnimation { duration: 2500 }
             PropertyAnimation {
                 target: diceCup
                 property: "kinematicPosition"
                 to: diceCup.topPos
                 duration: 2500
             }
             ParallelAnimation {
                 PropertyAnimation {
                     target: diceCup
                     property: "kinematicEulerRotation.z"
                     to: 130
                     duration: 1500
                 }
                 PropertyAnimation {
                     target: diceCup
                     property: "kinematicPosition"
                     to: diceCup.unloadPos
                     duration: 1500
                 }
             }
             PauseAnimation { duration: 1000 }
             ParallelAnimation {
                 PropertyAnimation {
                     target: diceCup
                     property: "kinematicEulerRotation.z"
                     to: 0
                     duration: 1500
                 }
                 PropertyAnimation {
                     target: diceCup
                     property: "kinematicPosition"
                     to: diceCup.topPos
                     duration: 1500
                 }
             }
             PropertyAnimation { target: diceCup; property: "kinematicPosition"; to: diceCup.bottomPos; duration: 1500 }
             PauseAnimation { duration: 2000 }
             ScriptAction { script: dicePool.restore() }
         }
     }

#### 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/Die.qml
- 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