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 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)),
                                                        diceCup.bottomPos.y + 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: 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 (var i = 0; i < count; i++) {
             objectAt(i).restore()
         }
     }
 }

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:

Images: