Qt Quick 3D Physics - Custom Shapes Example▲
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.ShadowMapQualityVeryHigh
}
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() &
lt; 0.25 ? "meshes/icosahedron.mesh"
:
Math.random() &
lt; 0.5 ? "meshes/dodecahedron.mesh"
:
Math.random() &
lt; 0.75 ? "meshes/octahedron.mesh"
:
"meshes/tetrahedron.mesh"
}
Model {
id
:
thisModel
source
:
diceShape.source
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 &
lt;
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 &
gt;=
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