Qt Quick Physique 3D - Exemple de formes personnalisées
Démonstration de l'utilisation de différentes formes.

Cet exemple démontre le chargement et l'apparition de plusieurs maillages de corps rigides ainsi que leur animation. La scène se compose d'une tour de dés, d'une nappe, d'une tasse et d'une poignée de dés. Le gobelet est animé pour collecter les dés et les placer dans la tour à dés. Les dés roulent ensuite sur la nappe.
Environnement
Comme d'habitude, nous avons un PhysicsWorld et un View3D. Dans le View3D, nous avons notre environnement qui met en place une sonde lumineuse :
environment: SceneEnvironment { clearColor: "white" backgroundMode: SceneEnvironment.SkyBox antialiasingMode: SceneEnvironment.MSAA antialiasingQuality: SceneEnvironment.High lightProbe: proceduralSky }
Textures
Nous définissons quatre textures qui seront utilisées pour la boîte à ciel, la nappe et les chiffres sur les dés :
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 }
Scène
Nous avons un Node qui contient notre scène avec la caméra et une lumière directionnelle :
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
Nous ajoutons la nappe qui est un StaticRigidBody composé d'un modèle avec une texture de tissage et un HeightFieldShape pour la 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" } }
Tasse
Nous définissons la tasse comme une DynamicRigidBody avec un modèle et une TriangleMeshShape comme forme de collision. Elle a un comportement sur les propriétés eulerRotation et position car celles-ci font partie d'une 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 } } }
Tour
La tour est simplement un StaticRigidBody avec un modèle et un TriangleMeshShape pour la 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" } }
Dés
Pour générer les dés, nous utilisons un composant et un Repeater3D. Le composant contient un DynamicRigidBody avec un ConvexMeshShape et un modèle. La position, la couleur, l'échelle et la source du maillage sont générées aléatoirement pour chaque dé.
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
Pour faire passer les dés du gobelet à la tour de dés, nous animons le gobelet et le déplaçons vers le haut, puis nous le faisons basculer. Pour s'assurer que l'animation reste synchronisée avec la simulation physique, nous utilisons un AnimationController que nous connectons au signal onFrameDone sur le PhysicsWorld. Après chaque image simulée, nous faisons progresser l'animation avec le pas de temps écoulé.
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() } } }
Contrôleur
Enfin, un WasdController est ajouté pour pouvoir contrôler la caméra à l'aide d'un clavier :
WasdController { keysEnabled: true controlledObject: camera speed: 0.2 }
Fichiers :
- customshapes/CMakeLists.txt
- customshapes/customshapes.pro
- customshapes/main.cpp
- customshapes/main.qml
- customshapes/qml.qrc
- customshapes/resources.qrc
Images :
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.