Revision control

Copy as Markdown

// 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