Qt Quick 3D Physics - Queries Example

 // Copyright (C) 2026 The Qt Company Ltd.
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 import QtQuick
 import QtQuick.Controls
 import QtQuick.Layouts
 import QtQuick3D
 import QtQuick3D.Helpers
 import QtQuick3D.Physics

 Window {
     id: window
     width: 1280
     height: 720
     visible: true
     title: qsTr("Qt Quick 3D Physics - Queries")

     property int currentMode: 0
     property real playerYaw: 0
     property real cameraPitch: -15

     property bool keyW: false
     property bool keyS: false
     property bool keyA: false
     property bool keyD: false
     property bool keySpace: false

     readonly property var modeDetails: [
         { name: "1. Single Probe", type: "PROBE", desc: "Closest body along a ray (singleRaycastQuery)" },
         { name: "2. Piercing Probe", type: "PROBE", desc: "Every body along a ray (multiRaycastQuery)" },
         { name: "3. Volume Probe", type: "PROBE", desc: "Closest body along a sweep (singleSweepQuery)" },
         { name: "4. Piercing Volume Probe", type: "PROBE", desc: "Every body along a sweep (multiSweepQuery)" },
         { name: "5. Radial Pulse", type: "PROBE", desc: "Every body in a volume (multiOverlapQuery)" },
         { name: "6. Line-of-Sight Scanner", type: "SCANNER", desc: "Is the ray blocked? (testRaycastQuery)" },
         { name: "7. Clearance Scanner", type: "SCANNER", desc: "Is the corridor clear? (testSweepQuery)" },
         { name: "8. Occupancy Scanner", type: "SCANNER", desc: "Is the volume occupied? (testOverlapQuery)" }
     ]

     Timer {
         id: autoFireTimer
         interval: 150
         repeat: true
         onTriggered: {
             if (window.keySpace) {
                 window.runQuery();
             } else {
                 autoFireTimer.stop();
             }
         }
     }

     function updateShootingState() {
         if (keySpace) {
             if (!autoFireTimer.running) {
                 runQuery();
                 autoFireTimer.start();
             }
         } else {
             autoFireTimer.stop();
         }
     }

     Item {
         id: inputController
         anchors.fill: parent
         focus: true

         Component.onCompleted: inputController.forceActiveFocus()

         Keys.onPressed: (event) => {
             let k = event.key;
             let t = event.text.toLowerCase();

             if (k === Qt.Key_W || t === "w") window.keyW = true;
             if (k === Qt.Key_S || t === "s") window.keyS = true;
             if (k === Qt.Key_A || t === "a") window.keyA = true;
             if (k === Qt.Key_D || t === "d") window.keyD = true;

             if (k === Qt.Key_Space && !event.isAutoRepeat) {
                 window.keySpace = true;
                 window.updateShootingState();
             }

             if (k >= Qt.Key_1 && k <= Qt.Key_8) {
                 window.currentMode = k - Qt.Key_1;
             }
         }

         Keys.onReleased: (event) => {
             let k = event.key;
             let t = event.text.toLowerCase();

             if (k === Qt.Key_W || t === "w") window.keyW = false;
             if (k === Qt.Key_S || t === "s") window.keyS = false;
             if (k === Qt.Key_A || t === "a") window.keyA = false;
             if (k === Qt.Key_D || t === "d") window.keyD = false;

             if (k === Qt.Key_Space) {
                 window.keySpace = false;
                 window.updateShootingState();
             }
         }
     }

     View3D {
         id: view
         anchors.fill: parent

         PhysicsWorld {
             id: physicsWorld
             scene: view.scene
             running: true
             gravity: Qt.vector3d(0, -981, 0)
         }

         // Shapes used for Sweep and Overlap queries
         SphereShape { id: querySphereShape; diameter: 200 }
         BoxShape { id: queryBoxShape; extents: Qt.vector3d(200, 200, 200) }

         MouseArea {
             anchors.fill: parent
             acceptedButtons: Qt.LeftButton | Qt.RightButton
             property point lastPos: Qt.point(0, 0)

             onPressed: (mouse) => {
                 inputController.forceActiveFocus();
                 lastPos = Qt.point(mouse.x, mouse.y);
             }

             onPositionChanged: (mouse) => {
                 if (pressedButtons & Qt.RightButton || pressedButtons & Qt.LeftButton) {
                     let dx = mouse.x - lastPos.x;
                     let dy = mouse.y - lastPos.y;

                     window.playerYaw -= dx * 0.25;
                     window.cameraPitch = Math.max(-60, Math.min(30, window.cameraPitch - dy * 0.25));
                     lastPos = Qt.point(mouse.x, mouse.y);
                 }
             }

             onWheel: (wheel) => {
                 if (wheel.angleDelta.y > 0) {
                     window.currentMode = (window.currentMode + 1) % window.modeDetails.length;
                 } else {
                     window.currentMode = (window.currentMode - 1 + window.modeDetails.length)
                                        % window.modeDetails.length;
                 }
             }
         }

         environment: SceneEnvironment {
             clearColor: "#0f111a"
             backgroundMode: SceneEnvironment.Color
         }

         CharacterController {
             id: player
             position: Qt.vector3d(0, 150, 1200)
             eulerRotation.y: window.playerYaw
             gravity: physicsWorld.gravity

             collisionShapes: CapsuleShape {
                 diameter: 80
                 height: 180
             }

             Model {
                 source: "#Cylinder"
                 scale: Qt.vector3d(0.8, 1.8, 0.8)
                 materials: [ PrincipledMaterial { baseColor: "#00d2d3" } ]
             }

             Node {
                 id: cameraRig
                 eulerRotation.x: window.cameraPitch

                 PerspectiveCamera {
                     id: camera
                     position: Qt.vector3d(0, 200, 500)
                     eulerRotation.x: -10
                     clipNear: 1
                 }
             }
         }

         DirectionalLight {
             eulerRotation: Qt.vector3d(-45, 30, 0)
             brightness: 1.5
         }

         StaticRigidBody {
             position: Qt.vector3d(0, -50, 0)
             collisionShapes: BoxShape { extents: Qt.vector3d(6000, 100, 6000) }
             Model {
                 source: "#Cube"
                 scale: Qt.vector3d(60, 1, 60)
                 materials: [ PrincipledMaterial { baseColor: "#1e272e" } ]
             }
         }

         component ArenaWall : StaticRigidBody {
             property vector3d wallSize: Qt.vector3d(100, 100, 100)
             collisionShapes: BoxShape { extents: wallSize }
             Model {
                 source: "#Cube"
                 scale: Qt.vector3d(wallSize.x * 0.01, wallSize.y * 0.01, wallSize.z * 0.01)
                 materials: [ PrincipledMaterial { baseColor: "#34495e" } ]
             }
         }

         ArenaWall { position: Qt.vector3d(0, 300, -3000); wallSize: Qt.vector3d(6000, 600, 200) }
         ArenaWall { position: Qt.vector3d(0, 300, 3000); wallSize: Qt.vector3d(6000, 600, 200) }
         ArenaWall { position: Qt.vector3d(-3000, 300, 0); wallSize: Qt.vector3d(200, 600, 6000) }
         ArenaWall { position: Qt.vector3d(3000, 300, 0); wallSize: Qt.vector3d(200, 600, 6000) }

         component DynamicCube : DynamicRigidBody {
             id: cubeRoot
             property color cubeColor: "#e67e22"
             collisionShapes: BoxShape { extents: Qt.vector3d(100, 100, 100) }
             Model {
                 source: "#Cube"
                 scale: Qt.vector3d(1, 1, 1)
                 materials: [ PrincipledMaterial { baseColor: cubeRoot.cubeColor } ]
             }
         }

         component DynamicSphere : DynamicRigidBody {
             id: sphereRoot
             property color sphereColor: "#1abc9c"
             collisionShapes: SphereShape { diameter: 100 }
             Model {
                 source: "#Sphere"
                 scale: Qt.vector3d(1, 1, 1)
                 materials: [ PrincipledMaterial { baseColor: sphereRoot.sphereColor } ]
             }
         }

         // --- MASSIVE DYNAMIC OBJECTS GENERATOR (500+ BODIES) ---
         Component { id: dynamicCubeComp; DynamicCube {} }
         Component { id: dynamicSphereComp; DynamicSphere {} }

         Component.onCompleted: {
             let colors = ["#e74c3c", "#e67e22", "#f1c40f", "#2ecc71", "#3498db", "#9b59b6", "#1abc9c", "#e84393"];

             // Generate a huge wall of dynamic objects (18 x 7 x 4 grid = ~500 objects)
             for (let x = -1000; x <= 1000; x += 120) {
                 for (let y = 50; y <= 850; y += 120) {
                     for (let z = -1000; z >= -1400; z -= 120) {
                         let isSphere = (Math.random() > 0.5);
                         let comp = isSphere ? dynamicSphereComp : dynamicCubeComp;
                         let randomColor = colors[Math.floor(Math.random() * colors.length)];

                         comp.createObject(view.scene, {
                             "position": Qt.vector3d(x + (Math.random() * 5), y, z + (Math.random() * 5)),
                             "cubeColor": randomColor,
                             "sphereColor": randomColor
                         });
                     }
                 }
             }
         }

         Node {
             id: tracerBeam
             visible: false
             Model {
                 id: tracerMesh
                 source: "#Cylinder"
                 scale: Qt.vector3d(1, 1, 1)
                 materials: [
                     PrincipledMaterial {
                         id: tracerMat
                         baseColor: "#f1c40f"
                         opacity: 0.5
                         alphaMode: PrincipledMaterial.Blend
                         lighting: PrincipledMaterial.NoLighting
                     }
                 ]
             }
         }
         Timer { id: tracerTimer; interval: 150; onTriggered: tracerBeam.visible = false }

         Model {
             id: volumeGhost
             visible: false
             materials: [
                 PrincipledMaterial {
                     id: ghostMat
                     baseColor: "#3300ff00"
                     alphaMode: PrincipledMaterial.Blend
                     opacity: 0.4
                     lighting: PrincipledMaterial.NoLighting
                 }
             ]
         }
     }

     FrameAnimation {
         running: true
         onTriggered: {
             let speed = 800;
             let moveX = 0;
             let moveZ = 0;

             if (window.keyW) moveZ -= 1;
             if (window.keyS) moveZ += 1;
             if (window.keyA) moveX -= 1;
             if (window.keyD) moveX += 1;

             let len = Math.sqrt(moveX * moveX + moveZ * moveZ);
             if (len > 0) {
                 moveX /= len;
                 moveZ /= len;
                 player.movement = Qt.vector3d(moveX * speed, 0, moveZ * speed);
             } else {
                 player.movement = Qt.vector3d(0, 0, 0);
             }

             let rad = player.eulerRotation.y * Math.PI / 180.0;
             let forwardWorld = Qt.vector3d(-Math.sin(rad), 0, -Math.cos(rad)).normalized();

             window.updateScannerState(forwardWorld);
         }
     }

     // The player's own collider is an ordinary hit candidate, so a query starting at
     // the character's center would hit its own capsule at distance 0. Start the query
     // slightly above and in front of the capsule instead.
     function probeOrigin(forwardDir) {
         return player.position.plus(Qt.vector3d(0, 50, 0)).plus(forwardDir.times(150));
     }

     // Only a DynamicRigidBody can take an impulse. The queries here also return the
     // static arena and the player's own character controller.
     function pushBody(body, impulse) {
         if (body instanceof DynamicRigidBody)
             body.applyCentralImpulse(impulse);
     }

     function runQuery() {
         if (currentMode >= 5) {
             statusText.text = "Scanner mode: modes 1-5 are triggered manually";
             return;
         }

         let rad = player.eulerRotation.y * Math.PI / 180.0;
         let pitchRad = cameraRig.eulerRotation.x * Math.PI / 180.0;

         let forwardDir = Qt.vector3d(
             -Math.sin(rad) * Math.cos(pitchRad),
             Math.sin(pitchRad),
             -Math.cos(rad) * Math.cos(pitchRad)
         ).normalized();

         let origin = window.probeOrigin(forwardDir);
         let maxDist = 3000;

         switch (currentMode) {

         case 0: // singleRaycastQuery
             let hit1 = physicsWorld.singleRaycastQuery(origin, forwardDir, maxDist, true, true);

             // hit.body === null is the miss test; distance is 0 on a miss.
             let dist1 = hit1.body ? hit1.distance : maxDist;
             renderTracer(origin, forwardDir, dist1, "#f1c40f", 10, 10, false);

             if (hit1.body) {
                 window.pushBody(hit1.body, forwardDir.times(250000));
                 statusText.text = "singleRaycastQuery: hit at " + Math.round(hit1.distance) + " cm";
             } else {
                 statusText.text = "singleRaycastQuery: missed";
             }
             break;

         case 1: // multiRaycastQuery
             let hits2 = physicsWorld.multiRaycastQuery(origin, forwardDir, maxDist, true, true);
             renderTracer(origin, forwardDir, maxDist, "#3498db", 10, 10, false);

             for (let i = 0; i < hits2.length; ++i)
                 window.pushBody(hits2[i].body, forwardDir.times(200000));

             statusText.text = "multiRaycastQuery: hit count " + hits2.length;
             break;

         case 2: // singleSweepQuery
             querySphereShape.position = origin;
             let hit3 = physicsWorld.singleSweepQuery(querySphereShape, forwardDir, maxDist, true, true);

             // A sweep that starts already overlapping a body reports a negative
             // distance, so test body rather than distance to detect a miss.
             let dist3 = hit3.body ? Math.max(hit3.distance, 0) : maxDist;

             // Cylinder matching the swept SphereShape diameter
             renderTracer(origin, forwardDir, dist3, "#e74c3c", 200, 200, false);

             if (hit3.body) {
                 window.pushBody(hit3.body, forwardDir.times(300000));
                 statusText.text = "singleSweepQuery: hit at " + Math.round(hit3.distance) + " cm";
             } else {
                 statusText.text = "singleSweepQuery: missed";
             }
             break;

         case 3: // multiSweepQuery
             queryBoxShape.position = origin;
             let hits4 = physicsWorld.multiSweepQuery(queryBoxShape, forwardDir, maxDist, true, true);

             // Translucent box corridor along the sweep trajectory
             renderTracer(origin, forwardDir, maxDist, "#9b59b6", 200, 200, true);

             for (let j = 0; j < hits4.length; ++j)
                 window.pushBody(hits4[j].body, forwardDir.times(250000));

             statusText.text = "multiSweepQuery: hit count " + hits4.length;
             break;

         case 4: // multiOverlapQuery
             let pulseCenter = origin.plus(forwardDir.times(600));
             querySphereShape.position = pulseCenter;

             let overlapHits = physicsWorld.multiOverlapQuery(querySphereShape, true, true);

             volumeGhost.source = "#Sphere";
             volumeGhost.position = pulseCenter;
             volumeGhost.scale = Qt.vector3d(2, 2, 2);
             ghostMat.baseColor = "#e74c3c";
             volumeGhost.visible = true;

             for (let k = 0; k < overlapHits.length; ++k) {
                 let body5 = overlapHits[k].body;
                 if (!body5)
                     continue;
                 let pushDir = body5.position.minus(pulseCenter);
                 pushDir = pushDir.length() > 0 ? pushDir.normalized() : Qt.vector3d(0, 1, 0);
                 window.pushBody(body5, pushDir.times(350000).plus(Qt.vector3d(0, 100000, 0)));
             }
             statusText.text = "multiOverlapQuery: hit count " + overlapHits.length;
             break;
         }
     }

     function updateScannerState(forwardDir) {
         if (currentMode < 5) {
             if (currentMode !== 4) volumeGhost.visible = false;
             return;
         }

         let origin = window.probeOrigin(forwardDir);
         let scanDist = 1200;

         switch (currentMode) {

         case 5: // testRaycastQuery
             let isBlocked5 = physicsWorld.testRaycastQuery(origin, forwardDir, scanDist, true, true);
             renderTracer(origin, forwardDir, scanDist, isBlocked5 ? "#ff2d55" : "#00ff66", 10, 10, false);

             statusText.text = isBlocked5 ? "testRaycastQuery: blocked" : "testRaycastQuery: path clear";
             statusText.color = isBlocked5 ? "#ff2d55" : "#00ff66";
             break;

         case 6: // testSweepQuery
             querySphereShape.position = origin;
             let isBlocked6 = physicsWorld.testSweepQuery(querySphereShape, forwardDir, scanDist, true, true);

             volumeGhost.source = "#Sphere";
             volumeGhost.position = origin.plus(forwardDir.times(scanDist));
             volumeGhost.scale = Qt.vector3d(2, 2, 2);
             ghostMat.baseColor = isBlocked6 ? "#ff2d55" : "#00ff66";
             volumeGhost.visible = true;

             statusText.text = isBlocked6 ? "testSweepQuery: corridor blocked" : "testSweepQuery: corridor clear";
             statusText.color = isBlocked6 ? "#ff2d55" : "#00ff66";
             break;

         case 7: // testOverlapQuery
             let checkZone = origin.plus(forwardDir.times(500));
             querySphereShape.position = checkZone;
             let isOccupied = physicsWorld.testOverlapQuery(querySphereShape, true, true);

             volumeGhost.source = "#Sphere";
             volumeGhost.position = checkZone;
             volumeGhost.scale = Qt.vector3d(2, 2, 2);
             ghostMat.baseColor = isOccupied ? "#ff2d55" : "#00ff66";
             volumeGhost.visible = true;

             statusText.text = isOccupied ? "testOverlapQuery: volume occupied" : "testOverlapQuery: volume empty";
             statusText.color = isOccupied ? "#ff2d55" : "#00ff66";
             break;
         }
     }

     function renderTracer(origin, forwardDir, dist, colorHex, thicknessX, thicknessY, isBox) {
         thicknessX = thicknessX !== undefined ? thicknessX : 10;
         thicknessY = thicknessY !== undefined ? thicknessY : 10;
         isBox = isBox !== undefined ? isBox : false;

         tracerMesh.source = isBox ? "#Cube" : "#Cylinder";
         tracerBeam.position = origin.plus(forwardDir.times(dist / 2));
         tracerBeam.eulerRotation = Qt.vector3d(cameraRig.eulerRotation.x, player.eulerRotation.y, 0);

         tracerMesh.scale = Qt.vector3d(thicknessX * 0.01, thicknessY * 0.01, dist * 0.01);
         tracerMat.baseColor = colorHex;
         tracerMat.opacity = (thicknessX > 50) ? 0.35 : 0.8;

         tracerBeam.visible = true;
         tracerTimer.restart();
     }

     ColumnLayout {
         anchors.left: parent.left
         anchors.top: parent.top
         anchors.margins: 15
         spacing: 10

         Rectangle {
             implicitWidth: 420
             implicitHeight: 200
             color: "#d910141d"
             radius: 8
             border.color: "#34495e"
             border.width: 1

             ColumnLayout {
                 anchors.fill: parent
                 anchors.margins: 12

                 Text {
                     text: "PHYSICS SCENE QUERIES"
                     color: "white"
                     font.bold: true
                     font.pixelSize: 14
                 }

                 ComboBox {
                     id: modeSelect
                     Layout.fillWidth: true
                     model: window.modeDetails.map(item => "[" + item.type + "] " + item.name)
                     currentIndex: window.currentMode
                     focusPolicy: Qt.NoFocus
                     onActivated: (index) => {
                         window.currentMode = index;
                         inputController.forceActiveFocus();
                     }
                 }

                 Text {
                     text: window.modeDetails[window.currentMode].desc
                     color: "#f1c40f"
                     font.pixelSize: 12
                     font.italic: true
                 }

                 Text {
                     text: "Controls:"
                           + "\n- WASD: Move character"
                           + "\n- Hold Mouse Drag: Rotate camera"
                           + "\n- Spacebar: Run the selected query"
                           + "\n- Keys 1-8: Switch query modes"
                     color: "#bdc3c7"
                     font.pixelSize: 11
                 }

                 Rectangle {
                     Layout.fillWidth: true
                     implicitHeight: 34
                     color: "#000000"
                     radius: 4

                     Text {
                         id: statusText
                         anchors.centerIn: parent
                         color: "#00ff66"
                         font.pixelSize: 11
                         font.bold: true
                     }
                 }
             }
         }
     }
 }