Revision control
+ // Copyright (C) 2026 The Qt Company Ltd.
+ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+ // Qt-Security score:significant reason:default
+
+ #include "qraycaster_p.h"
+
+ #include "qabstractphysicsnode_p.h"
+ #include "qphysicsutils_p.h"
+ #include "qphysicsworld_p.h"
+
+ QT_BEGIN_NAMESPACE
+
+ /*!
+ \qmltype Raycaster
+ \inherits Node
+ \inqmlmodule QtQuick3D.Physics
+ \since 6.13
+ \brief Casts a ray into the physics scene on every physics frame.
+
+ Raycaster casts a ray from its scene position in \l direction and reports the closest physics
+ body it intersects. The \l hit property is updated once per physics frame, after the bodies
+ have been synchronized with the scene and before \l PhysicsWorld::frameDone, so a frameDone
+ handler reads the result of the frame it is reporting on.
+
+ \qml
+ Raycaster {
+ direction: Qt.vector3d(0, -1, 0)
+ maxDistance: 50
+ includeDynamic: false
+ onBodyEntered: (body) => console.log("Hit", body)
+ }
+ \endqml
+
+ The ray is a scene query, so it sees the same bodies as
+ \l {PhysicsWorld::singleRaycastQuery} {PhysicsWorld.singleRaycastQuery} and is bound by the
+ same rules: \l {PhysicsNode::filterGroup} {filterGroup} and
+ \l {PhysicsNode::filterIgnoreGroups} {filterIgnoreGroups} have no effect on it, and a
+ \l TriggerBody never appears in the result. Use \l includeStatic and \l includeDynamic to
+ narrow that by kind, and \l excludeParentBody and \l excludedBodies to leave out particular
+ bodies. Those two matter more here than for a query method, since the ray reports only its
+ closest hit and there is nothing left to filter afterwards.
+
+ \sa PhysicsWorld, locationHit, {Qt Quick 3D Physics Scene Queries},
+ {Qt Quick 3D Physics - Raycaster Example}
+ */
+
+ /*!
+ \qmlproperty bool Raycaster::enabled
+
+ Controls whether the ray is cast on each physics frame. Disabling the ray clears \l hit and
+ sets \l isColliding to false.
+
+ The default value is \c true.
+ */
+
+ /*!
+ \qmlproperty vector3d Raycaster::direction
+
+ The ray direction in local coordinates. It is transformed to scene coordinates and normalized
+ before the ray is cast. A zero vector produces no hit. Non-finite values are ignored.
+
+ The default value is \c{Qt.vector3d(0, 0, -1)}.
+ */
+
+ /*!
+ \qmlproperty real Raycaster::maxDistance
+
+ The maximum distance of the ray in scene units. A value of zero produces no hit. Negative and
+ non-finite values are ignored.
+
+ The default value is \c 100.
+ */
+
+ /*!
+ \qmlproperty bool Raycaster::excludeParentBody
+
+ When true, every \l PhysicsNode ancestor of the ray is excluded from the query, not only the
+ nearest one. A ray mounted on a body starts inside that body's own shapes, and a raycast does
+ report the shape it starts inside, so without this the ray reports the body carrying it at a
+ distance of zero and never sees anything beyond.
+
+ The default value is \c true.
+
+ \sa excludedBodies
+ */
+
+ /*!
+ \qmlproperty list<PhysicsNode> Raycaster::excludedBodies
+
+ Bodies the ray never reports, whether or not they are ancestors of it. Use this for a body the
+ ray has to ignore but is not mounted on, such as the character carrying the weapon a ray is
+ fired from, where the two are siblings rather than one inside the other.
+
+ Because the ray reports only its closest hit, there is no way to filter its result afterwards
+ the way the \l PhysicsWorld query methods allow, so anything that has to be left out has to be
+ named here or covered by \l excludeParentBody.
+
+ The list is empty by default. A body destroyed after it was added stops being excluded and
+ reads back as \c null, rather than being removed from the list.
+
+ \sa excludeParentBody
+ */
+
+ /*!
+ \qmlproperty bool Raycaster::includeStatic
+
+ Whether static physics bodies are included in the query.
+
+ The default value is \c true.
+ */
+
+ /*!
+ \qmlproperty bool Raycaster::includeDynamic
+
+ Whether dynamic physics bodies are included in the query.
+
+ The default value is \c true.
+
+ When both \l includeStatic and includeDynamic are false, the ray produces no hit.
+ */
+
+ /*!
+ \qmlproperty bool Raycaster::isColliding
+ \readonly
+
+ Whether the ray currently intersects a physics body.
+ */
+
+ /*!
+ \qmlproperty locationHit Raycaster::hit
+ \readonly
+
+ The closest intersection reported by the ray. The value contains the intersected body and
+ collision shape, along with the scene-space position and normal and the distance from the ray
+ origin. It is a default-constructed \l locationHit when \l isColliding is false.
+ */
+
+ /*!
+ \qmlsignal Raycaster::bodyEntered(PhysicsNode body)
+
+ Emitted when the ray starts intersecting \a body. When the intersected body changes,
+ \l bodyExited is emitted for the old body before this signal is emitted for the new body.
+ */
+
+ /*!
+ \qmlsignal Raycaster::bodyExited(PhysicsNode body)
+
+ Emitted when the ray stops intersecting \a body.
+ */
+
+ QRaycaster::QRaycaster(QQuick3DNode *parent) : QQuick3DNode(parent)
+ {
+ QPhysicsWorld::registerRaycaster(this);
+ }
+
+ QRaycaster::~QRaycaster()
+ {
+ QPhysicsWorld::deregisterRaycaster(this);
+ }
+
+ bool QRaycaster::enabled() const
+ {
+ return m_enabled;
+ }
+
+ void QRaycaster::setEnabled(bool enabled)
+ {
+ if (m_enabled == enabled)
+ return;
+ m_enabled = enabled;
+ if (!m_enabled)
+ clearHit();
+ emit enabledChanged();
+ }
+
+ QVector3D QRaycaster::direction() const
+ {
+ return m_direction;
+ }
+
+ void QRaycaster::setDirection(const QVector3D &direction)
+ {
+ if (m_direction == direction)
+ return;
+ if (!QPhysicsUtils::isFinite(direction)) {
+ qWarning() << "Raycaster: direction must be finite, ignoring.";
+ return;
+ }
+ m_direction = direction;
+ if (m_direction.isNull())
+ clearHit();
+ emit directionChanged();
+ }
+
+ float QRaycaster::maxDistance() const
+ {
+ return m_maxDistance;
+ }
+
+ void QRaycaster::setMaxDistance(float maxDistance)
+ {
+ if (qFuzzyCompare(m_maxDistance, maxDistance))
+ return;
+ if (!qIsFinite(maxDistance) || maxDistance < 0.0f) {
+ qWarning() << "Raycaster: maxDistance must be finite and non-negative, ignoring.";
+ return;
+ }
+ m_maxDistance = maxDistance;
+ if (m_maxDistance == 0.0f)
+ clearHit();
+ emit maxDistanceChanged();
+ }
+
+ bool QRaycaster::excludeParentBody() const
+ {
+ return m_excludeParentBody;
+ }
+
+ void QRaycaster::setExcludeParentBody(bool excludeParentBody)
+ {
+ if (m_excludeParentBody == excludeParentBody)
+ return;
+ m_excludeParentBody = excludeParentBody;
+ emit excludeParentBodyChanged();
+ }
+
+ bool QRaycaster::includeStatic() const
+ {
+ return m_includeStatic;
+ }
+
+ void QRaycaster::setIncludeStatic(bool includeStatic)
+ {
+ if (m_includeStatic == includeStatic)
+ return;
+ m_includeStatic = includeStatic;
+ if (!m_includeStatic && !m_includeDynamic)
+ clearHit();
+ emit includeStaticChanged();
+ }
+
+ bool QRaycaster::includeDynamic() const
+ {
+ return m_includeDynamic;
+ }
+
+ void QRaycaster::setIncludeDynamic(bool includeDynamic)
+ {
+ if (m_includeDynamic == includeDynamic)
+ return;
+ m_includeDynamic = includeDynamic;
+ if (!m_includeStatic && !m_includeDynamic)
+ clearHit();
+ emit includeDynamicChanged();
+ }
+
+ bool QRaycaster::isColliding() const
+ {
+ return m_isColliding;
+ }
+
+ QQuick3DPhysicsLocationHit QRaycaster::hit() const
+ {
+ return m_hit;
+ }
+
+ QQmlListProperty<QAbstractPhysicsNode> QRaycaster::excludedBodies()
+ {
+ return QQmlListProperty<QAbstractPhysicsNode>(
+ this, nullptr, QRaycaster::qmlAppendExcludedBody, QRaycaster::qmlExcludedBodyCount,
+ QRaycaster::qmlExcludedBodyAt, QRaycaster::qmlClearExcludedBodies);
+ }
+
+ void QRaycaster::qmlAppendExcludedBody(QQmlListProperty<QAbstractPhysicsNode> *list,
+ QAbstractPhysicsNode *body)
+ {
+ if (body)
+ static_cast<QRaycaster *>(list->object)->m_excludedBodies.push_back(body);
+ }
+
+ QAbstractPhysicsNode *QRaycaster::qmlExcludedBodyAt(QQmlListProperty<QAbstractPhysicsNode> *list,
+ qsizetype index)
+ {
+ auto *self = static_cast<QRaycaster *>(list->object);
+ if (index < 0 || index >= self->m_excludedBodies.size())
+ return nullptr;
+ return self->m_excludedBodies.at(index).data();
+ }
+
+ qsizetype QRaycaster::qmlExcludedBodyCount(QQmlListProperty<QAbstractPhysicsNode> *list)
+ {
+ return static_cast<QRaycaster *>(list->object)->m_excludedBodies.size();
+ }
+
+ void QRaycaster::qmlClearExcludedBodies(QQmlListProperty<QAbstractPhysicsNode> *list)
+ {
+ static_cast<QRaycaster *>(list->object)->m_excludedBodies.clear();
+ }
+
+ // Four inline covers what scenes produce, one ancestor body being the usual answer.
+ void QRaycaster::gatherExcludedBodies(
+ QVarLengthArray<const QAbstractPhysicsNode *, 4> &excluded) const
+ {
+ if (m_excludeParentBody) {
+ // Every body above the ray, not only the nearest. Bodies nest, and a ray inside the
+ // inner one of a pair would otherwise report the outer one at distance 0.
+ for (QQuick3DNode *node = parentNode(); node; node = node->parentNode()) {
+ if (auto *body = qobject_cast<QAbstractPhysicsNode *>(node))
+ excluded.append(body);
+ }
+ }
+
+ if (m_excludedBodies.isEmpty())
+ return;
+
+ excluded.reserve(excluded.size() + m_excludedBodies.size());
+ for (const QPointer<QAbstractPhysicsNode> &body : m_excludedBodies) {
+ if (!body.isNull())
+ excluded.append(body.data());
+ }
+ }
+
+ void QRaycaster::updateRaycast(QPhysicsWorld *world)
+ {
+ if (!m_enabled || m_maxDistance == 0.0f || (!m_includeStatic && !m_includeDynamic)) {
+ clearHit();
+ return;
+ }
+
+ // Null for a null direction, and also for a ray whose node is scaled to zero.
+ const QVector3D direction = mapDirectionToScene(m_direction);
+ if (direction.isNull()) {
+ clearHit();
+ return;
+ }
+
+ QVarLengthArray<const QAbstractPhysicsNode *, 4> excluded;
+ gatherExcludedBodies(excluded);
+ updateHit(world->closestRaycastHit(scenePosition(), direction, m_maxDistance, m_includeStatic,
+ m_includeDynamic, excluded));
+ }
+
+ // Holds both bodies and this ray weakly, since the first of the reports below can leave the
+ // rest with a body or a ray that is gone: they run inside the frame, so a handler deletes on
+ // the spot rather than between frames. The same reason QTriggerBody::reportEntered does it.
+ void QRaycaster::updateHit(const QQuick3DPhysicsLocationHit &hit)
+ {
+ const QPointer<QRaycaster> self(this);
+ const QPointer<QAbstractPhysicsNode> previousBody(m_hit.body());
+ const QPointer<QAbstractPhysicsNode> body(hit.body());
+ const bool isColliding = !body.isNull();
+ const bool hitHasChanged = m_hit.body() != body || m_hit.shape() != hit.shape()
+ || m_hit.position() != hit.position() || m_hit.normal() != hit.normal()
+ || !qFuzzyCompare(m_hit.distance(), hit.distance());
+ const bool collisionChanged = m_isColliding != isColliding;
+
+ m_hit = hit;
+ m_isColliding = isColliding;
+
+ if (hitHasChanged)
+ emit hitChanged();
+ if (collisionChanged && !self.isNull())
+ emit isCollidingChanged();
+
+ if (!self.isNull() && previousBody != body) {
+ if (!previousBody.isNull())
+ emit bodyExited(previousBody);
+ if (!self.isNull() && !body.isNull())
+ emit bodyEntered(body);
+ }
+ }
+
+ void QRaycaster::clearHit()
+ {
+ updateHit({});
+ }
+
+ QT_END_NAMESPACE