Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qraycaster.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include "qraycaster_p.h"
6
10
12
13/*!
14 \qmltype Raycaster
15 \inherits Node
16 \inqmlmodule QtQuick3D.Physics
17 \since 6.13
18 \brief Casts a ray into the physics scene on every physics frame.
19
20 Raycaster casts a ray from its scene position in \l direction and reports the closest physics
21 body it intersects. The \l hit property is updated once per physics frame, after the bodies
22 have been synchronized with the scene and before \l PhysicsWorld::frameDone, so a frameDone
23 handler reads the result of the frame it is reporting on.
24
25 \qml
26 Raycaster {
27 direction: Qt.vector3d(0, -1, 0)
28 maxDistance: 50
29 includeDynamic: false
30 onBodyEntered: (body) => console.log("Hit", body)
31 }
32 \endqml
33
34 The ray is a scene query, so it sees the same bodies as
35 \l {PhysicsWorld::singleRaycastQuery} {PhysicsWorld.singleRaycastQuery} and is bound by the
36 same rules: \l {PhysicsNode::filterGroup} {filterGroup} and
37 \l {PhysicsNode::filterIgnoreGroups} {filterIgnoreGroups} have no effect on it, and a
38 \l TriggerBody never appears in the result. Use \l includeStatic and \l includeDynamic to
39 narrow that by kind, and \l excludeParentBody and \l excludedBodies to leave out particular
40 bodies. Those two matter more here than for a query method, since the ray reports only its
41 closest hit and there is nothing left to filter afterwards.
42
43 \sa PhysicsWorld, locationHit, {Qt Quick 3D Physics Scene Queries},
44 {Qt Quick 3D Physics - Raycaster Example}
45*/
46
47/*!
48 \qmlproperty bool Raycaster::enabled
49
50 Controls whether the ray is cast on each physics frame. Disabling the ray clears \l hit and
51 sets \l isColliding to false.
52
53 The default value is \c true.
54*/
55
56/*!
57 \qmlproperty vector3d Raycaster::direction
58
59 The ray direction in local coordinates. It is transformed to scene coordinates and normalized
60 before the ray is cast. A zero vector produces no hit. Non-finite values are ignored.
61
62 The default value is \c{Qt.vector3d(0, 0, -1)}.
63*/
64
65/*!
66 \qmlproperty real Raycaster::maxDistance
67
68 The maximum distance of the ray in scene units. A value of zero produces no hit. Negative and
69 non-finite values are ignored.
70
71 The default value is \c 100.
72*/
73
74/*!
75 \qmlproperty bool Raycaster::excludeParentBody
76
77 When true, every \l PhysicsNode ancestor of the ray is excluded from the query, not only the
78 nearest one. A ray mounted on a body starts inside that body's own shapes, and a raycast does
79 report the shape it starts inside, so without this the ray reports the body carrying it at a
80 distance of zero and never sees anything beyond.
81
82 The default value is \c true.
83
84 \sa excludedBodies
85*/
86
87/*!
88 \qmlproperty list<PhysicsNode> Raycaster::excludedBodies
89
90 Bodies the ray never reports, whether or not they are ancestors of it. Use this for a body the
91 ray has to ignore but is not mounted on, such as the character carrying the weapon a ray is
92 fired from, where the two are siblings rather than one inside the other.
93
94 Because the ray reports only its closest hit, there is no way to filter its result afterwards
95 the way the \l PhysicsWorld query methods allow, so anything that has to be left out has to be
96 named here or covered by \l excludeParentBody.
97
98 The list is empty by default. A body destroyed after it was added stops being excluded and
99 reads back as \c null, rather than being removed from the list.
100
101 \sa excludeParentBody
102*/
103
104/*!
105 \qmlproperty bool Raycaster::includeStatic
106
107 Whether static physics bodies are included in the query.
108
109 The default value is \c true.
110*/
111
112/*!
113 \qmlproperty bool Raycaster::includeDynamic
114
115 Whether dynamic physics bodies are included in the query.
116
117 The default value is \c true.
118
119 When both \l includeStatic and includeDynamic are false, the ray produces no hit.
120*/
121
122/*!
123 \qmlproperty bool Raycaster::isColliding
124 \readonly
125
126 Whether the ray currently intersects a physics body.
127*/
128
129/*!
130 \qmlproperty locationHit Raycaster::hit
131 \readonly
132
133 The closest intersection reported by the ray. The value contains the intersected body and
134 collision shape, along with the scene-space position and normal and the distance from the ray
135 origin. It is a default-constructed \l locationHit when \l isColliding is false.
136*/
137
138/*!
139 \qmlsignal Raycaster::bodyEntered(PhysicsNode body)
140
141 Emitted when the ray starts intersecting \a body. When the intersected body changes,
142 \l bodyExited is emitted for the old body before this signal is emitted for the new body.
143*/
144
145/*!
146 \qmlsignal Raycaster::bodyExited(PhysicsNode body)
147
148 Emitted when the ray stops intersecting \a body.
149*/
150
151QRaycaster::QRaycaster(QQuick3DNode *parent) : QQuick3DNode(parent)
152{
153 QPhysicsWorld::registerRaycaster(this);
154}
155
156QRaycaster::~QRaycaster()
157{
158 QPhysicsWorld::deregisterRaycaster(this);
159}
160
161bool QRaycaster::enabled() const
162{
163 return m_enabled;
164}
165
166void QRaycaster::setEnabled(bool enabled)
167{
168 if (m_enabled == enabled)
169 return;
170 m_enabled = enabled;
171 if (!m_enabled)
172 clearHit();
173 emit enabledChanged();
174}
175
176QVector3D QRaycaster::direction() const
177{
178 return m_direction;
179}
180
181void QRaycaster::setDirection(const QVector3D &direction)
182{
183 if (m_direction == direction)
184 return;
185 if (!QPhysicsUtils::isFinite(direction)) {
186 qWarning() << "Raycaster: direction must be finite, ignoring.";
187 return;
188 }
189 m_direction = direction;
190 if (m_direction.isNull())
191 clearHit();
192 emit directionChanged();
193}
194
195float QRaycaster::maxDistance() const
196{
197 return m_maxDistance;
198}
199
200void QRaycaster::setMaxDistance(float maxDistance)
201{
202 if (qFuzzyCompare(m_maxDistance, maxDistance))
203 return;
204 if (!qIsFinite(maxDistance) || maxDistance < 0.0f) {
205 qWarning() << "Raycaster: maxDistance must be finite and non-negative, ignoring.";
206 return;
207 }
208 m_maxDistance = maxDistance;
209 if (m_maxDistance == 0.0f)
210 clearHit();
211 emit maxDistanceChanged();
212}
213
214bool QRaycaster::excludeParentBody() const
215{
216 return m_excludeParentBody;
217}
218
219void QRaycaster::setExcludeParentBody(bool excludeParentBody)
220{
221 if (m_excludeParentBody == excludeParentBody)
222 return;
223 m_excludeParentBody = excludeParentBody;
224 emit excludeParentBodyChanged();
225}
226
227bool QRaycaster::includeStatic() const
228{
229 return m_includeStatic;
230}
231
232void QRaycaster::setIncludeStatic(bool includeStatic)
233{
234 if (m_includeStatic == includeStatic)
235 return;
236 m_includeStatic = includeStatic;
237 if (!m_includeStatic && !m_includeDynamic)
238 clearHit();
239 emit includeStaticChanged();
240}
241
242bool QRaycaster::includeDynamic() const
243{
244 return m_includeDynamic;
245}
246
247void QRaycaster::setIncludeDynamic(bool includeDynamic)
248{
249 if (m_includeDynamic == includeDynamic)
250 return;
251 m_includeDynamic = includeDynamic;
252 if (!m_includeStatic && !m_includeDynamic)
253 clearHit();
254 emit includeDynamicChanged();
255}
256
257bool QRaycaster::isColliding() const
258{
259 return m_isColliding;
260}
261
262QQuick3DPhysicsLocationHit QRaycaster::hit() const
263{
264 return m_hit;
265}
266
267QQmlListProperty<QAbstractPhysicsNode> QRaycaster::excludedBodies()
268{
269 return QQmlListProperty<QAbstractPhysicsNode>(
270 this, nullptr, QRaycaster::qmlAppendExcludedBody, QRaycaster::qmlExcludedBodyCount,
271 QRaycaster::qmlExcludedBodyAt, QRaycaster::qmlClearExcludedBodies);
272}
273
274void QRaycaster::qmlAppendExcludedBody(QQmlListProperty<QAbstractPhysicsNode> *list,
275 QAbstractPhysicsNode *body)
276{
277 if (body)
278 static_cast<QRaycaster *>(list->object)->m_excludedBodies.push_back(body);
279}
280
281QAbstractPhysicsNode *QRaycaster::qmlExcludedBodyAt(QQmlListProperty<QAbstractPhysicsNode> *list,
282 qsizetype index)
283{
284 auto *self = static_cast<QRaycaster *>(list->object);
285 if (index < 0 || index >= self->m_excludedBodies.size())
286 return nullptr;
287 return self->m_excludedBodies.at(index).data();
288}
289
290qsizetype QRaycaster::qmlExcludedBodyCount(QQmlListProperty<QAbstractPhysicsNode> *list)
291{
292 return static_cast<QRaycaster *>(list->object)->m_excludedBodies.size();
293}
294
295void QRaycaster::qmlClearExcludedBodies(QQmlListProperty<QAbstractPhysicsNode> *list)
296{
297 static_cast<QRaycaster *>(list->object)->m_excludedBodies.clear();
298}
299
300// Four inline covers what scenes produce, one ancestor body being the usual answer.
301void QRaycaster::gatherExcludedBodies(
302 QVarLengthArray<const QAbstractPhysicsNode *, 4> &excluded) const
303{
304 if (m_excludeParentBody) {
305 // Every body above the ray, not only the nearest. Bodies nest, and a ray inside the
306 // inner one of a pair would otherwise report the outer one at distance 0.
307 for (QQuick3DNode *node = parentNode(); node; node = node->parentNode()) {
308 if (auto *body = qobject_cast<QAbstractPhysicsNode *>(node))
309 excluded.append(body);
310 }
311 }
312
313 if (m_excludedBodies.isEmpty())
314 return;
315
316 excluded.reserve(excluded.size() + m_excludedBodies.size());
317 for (const QPointer<QAbstractPhysicsNode> &body : m_excludedBodies) {
318 if (!body.isNull())
319 excluded.append(body.data());
320 }
321}
322
323void QRaycaster::updateRaycast(QPhysicsWorld *world)
324{
325 if (!m_enabled || m_maxDistance == 0.0f || (!m_includeStatic && !m_includeDynamic)) {
326 clearHit();
327 return;
328 }
329
330 // Null for a null direction, and also for a ray whose node is scaled to zero.
331 const QVector3D direction = mapDirectionToScene(m_direction);
332 if (direction.isNull()) {
333 clearHit();
334 return;
335 }
336
337 QVarLengthArray<const QAbstractPhysicsNode *, 4> excluded;
338 gatherExcludedBodies(excluded);
339 updateHit(world->closestRaycastHit(scenePosition(), direction, m_maxDistance, m_includeStatic,
340 m_includeDynamic, excluded));
341}
342
343// Holds both bodies and this ray weakly, since the first of the reports below can leave the
344// rest with a body or a ray that is gone: they run inside the frame, so a handler deletes on
345// the spot rather than between frames. The same reason QTriggerBody::reportEntered does it.
346void QRaycaster::updateHit(const QQuick3DPhysicsLocationHit &hit)
347{
348 const QPointer<QRaycaster> self(this);
349 const QPointer<QAbstractPhysicsNode> previousBody(m_hit.body());
350 const QPointer<QAbstractPhysicsNode> body(hit.body());
351 const bool isColliding = !body.isNull();
352 const bool hitHasChanged = m_hit.body() != body || m_hit.shape() != hit.shape()
353 || m_hit.position() != hit.position() || m_hit.normal() != hit.normal()
354 || !qFuzzyCompare(m_hit.distance(), hit.distance());
355 const bool collisionChanged = m_isColliding != isColliding;
356
357 m_hit = hit;
358 m_isColliding = isColliding;
359
360 if (hitHasChanged)
361 emit hitChanged();
362 if (collisionChanged && !self.isNull())
363 emit isCollidingChanged();
364
365 if (!self.isNull() && previousBody != body) {
366 if (!previousBody.isNull())
367 emit bodyExited(previousBody);
368 if (!self.isNull() && !body.isNull())
369 emit bodyEntered(body);
370 }
371}
372
373void QRaycaster::clearHit()
374{
375 updateHit({});
376}
377
378QT_END_NAMESPACE
Combined button and popup list for selecting options.