Qt Quick 3D Physics - Raycaster 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.Physics

 Window {
     id: window
     width: 800
     height: 600
     visible: true
     title: qsTr("Qt Quick 3D Physics - Raycaster")

     PhysicsWorld {
         scene: view3D.scene
         gravity: Qt.vector3d(0, 0, 0)
     }

     View3D {
         id: view3D
         anchors.fill: parent

         environment: SceneEnvironment {
             antialiasingMode: SceneEnvironment.MSAA
             backgroundMode: SceneEnvironment.Color
             clearColor: "#f0f0f0"
         }

         PerspectiveCamera {
             position: Qt.vector3d(0, 300, 750)
             eulerRotation.x: -20
             clipFar: 2000
         }

         DirectionalLight {
             eulerRotation: Qt.vector3d(-45, 35, 0)
             castsShadow: true
             brightness: 1.2
             shadowFactor: 50
             shadowMapQuality: Light.ShadowMapQualityVeryHigh
         }

         StaticRigidBody {
             eulerRotation.x: -90
             collisionShapes: PlaneShape {}

             Model {
                 source: "#Rectangle"
                 scale: Qt.vector3d(12, 8, 1)
                 materials: PrincipledMaterial {
                     baseColor: "green"
                     roughness: 0.8
                 }
                 castsShadows: false
                 receivesShadows: true
             }
         }

         StaticRigidBody {
             position: Qt.vector3d(-130, 60, 0)
             collisionShapes: BoxShape {
                 extents: Qt.vector3d(90, 120, 90)
             }

             Model {
                 source: "#Cube"
                 scale: Qt.vector3d(0.9, 1.2, 0.9)
                 materials: PrincipledMaterial {
                     baseColor: "#d94141"
                 }
             }
         }

         DynamicRigidBody {
             id: sphereBody
             position: Qt.vector3d(130, 75, 0)
             isKinematic: true
             kinematicPosition: position
             collisionShapes: SphereShape {
                 diameter: 100
             }

             Model {
                 source: "#Sphere"
                 materials: PrincipledMaterial {
                     baseColor: "#3d70b2"
                     roughness: 0.4
                 }
             }
         }

         DynamicRigidBody {
             position: Qt.vector3d(0, 310, 0)
             isKinematic: true
             kinematicPosition: position
             collisionShapes: SphereShape {
                 diameter: 100
             }

             Model {
                 source: "#Sphere"
                 castsShadows: false
                 materials: PrincipledMaterial {
                     baseColor: "#7193b5"
                     opacity: 0.3
                     alphaMode: PrincipledMaterial.Blend
                     roughness: 0.35
                 }
             }

             Node {
                 id: rayOrigin
                 y: 70
                 // The slider holds the angle, whether the sweep or the user moves it.
                 eulerRotation.z: angleSlider.value

                 SequentialAnimation {
                     running: ray.enabled && sweepCheckBox.checked
                     loops: Animation.Infinite
                     NumberAnimation {
                         target: angleSlider
                         property: "value"
                         from: -25
                         to: 25
                         duration: 3000
                         easing.type: Easing.InOutSine
                     }
                     NumberAnimation {
                         target: angleSlider
                         property: "value"
                         from: 25
                         to: -25
                         duration: 3000
                         easing.type: Easing.InOutSine
                     }
                 }

                 Raycaster {
                     id: ray
                     enabled: enabledCheckBox.checked
                     direction: Qt.vector3d(0, -1, 0)
                     maxDistance: distanceSlider.value
                     includeStatic: staticCheckBox.checked
                     includeDynamic: dynamicCheckBox.checked
                     excludeParentBody: excludeParentCheckBox.checked
                     excludedBodies: excludeSphereCheckBox.checked ? [sphereBody] : []

                     Model {
                         readonly property real rayLength: ray.isColliding ? ray.hit.distance : ray.maxDistance

                         visible: ray.enabled
                         source: "#Cube"
                         y: -rayLength / 2
                         scale: Qt.vector3d(0.025, rayLength / 100, 0.025)
                         materials: PrincipledMaterial {
                             baseColor: ray.isColliding ? "#24a148" : "#da1e28"
                             lighting: PrincipledMaterial.NoLighting
                         }
                     }
                 }
             }
         }

         Model {
             visible: ray.enabled && ray.isColliding
             source: "#Sphere"
             position: ray.hit.position
             scale: Qt.vector3d(0.16, 0.16, 0.16)
             materials: PrincipledMaterial {
                 baseColor: "#f1c21b"
                 lighting: PrincipledMaterial.NoLighting
             }
         }
     }

     Frame {
         anchors.left: parent.left
         anchors.top: parent.top
         anchors.margins: 10

         background: Rectangle {
             color: "#c0c0c0"
             border.color: "#202020"
         }

         ColumnLayout {
             Label {
                 text: qsTr("Raycaster")
                 font.bold: true
             }
             Label {
                 color: !ray.enabled ? "#525252" : ray.isColliding ? "#106b2d" : "#a2191f"
                 text: !ray.enabled ? qsTr("Disabled") : ray.isColliding ? qsTr("Hit detected") : qsTr("No hit")
             }
             CheckBox {
                 id: enabledCheckBox
                 text: qsTr("Enabled")
                 checked: true
             }
             CheckBox {
                 id: staticCheckBox
                 text: qsTr("Include static bodies")
                 checked: true
             }
             CheckBox {
                 id: dynamicCheckBox
                 text: qsTr("Include dynamic bodies")
                 checked: true
             }
             CheckBox {
                 id: excludeParentCheckBox
                 text: qsTr("Exclude parent body")
                 checked: true
             }
             CheckBox {
                 id: excludeSphereCheckBox
                 text: qsTr("Exclude the blue sphere")
             }
             CheckBox {
                 id: sweepCheckBox
                 text: qsTr("Sweep ray")
                 checked: true
             }
             Label {
                 text: qsTr("Ray angle: %1°").arg(rayOrigin.eulerRotation.z.toFixed(0))
             }
             Slider {
                 id: angleSlider
                 Layout.preferredWidth: 220
                 enabled: !sweepCheckBox.checked
                 from: -25
                 to: 25
                 stepSize: 1
             }
             Label {
                 text: qsTr("Maximum distance: %1").arg(distanceSlider.value.toFixed(0))
             }
             Slider {
                 id: distanceSlider
                 Layout.preferredWidth: 220
                 from: 0
                 to: 600
                 value: 350
                 stepSize: 10
             }
             Label {
                 visible: ray.isColliding
                 text: qsTr("Hit distance: %1").arg(ray.hit.distance.toFixed(1))
             }
         }
     }
 }