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