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))
            }
        }
    }
}