Revision control

++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qphysicsworld_p.h"
++
++ #include "physxnode/qabstractphysxnode_p.h"
++ #include "physxnode/qphysxworld_p.h"
++ #include "qabstractphysicsnode_p.h"
++ #include "qdebugdrawhelper_p.h"
++ #include "qphysicsutils_p.h"
++ #include "joints/qjoint_p.h"
++ #include "qstaticphysxobjects_p.h"
++ #include "qboxshape_p.h"
++ #include "qsphereshape_p.h"
++ #include "qconvexmeshshape_p.h"
++ #include "qtrianglemeshshape_p.h"
++ #include "qcharactercontroller_p.h"
++ #include "qcapsuleshape_p.h"
++ #include "qplaneshape_p.h"
++ #include "qraycaster_p.h"
++ #include "qheightfieldshape_p.h"
++ #include "qtriggerbody_p.h"
++
++ #include "PxPhysicsAPI.h"
++ #include "cooking/PxCooking.h"
++ #include <foundation/PxSimpleTypes.h>
++
++ #include <QtQuick/private/qquickframeanimation_p.h>
++ #include <QtQuick3D/private/qquick3dobject_p.h>
++ #include <QtQuick3D/private/qquick3dnode_p.h>
++ #include <QtQuick3D/private/qquick3dmodel_p.h>
++ #include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++
++ #include <QtCore/qvarlengtharray.h>
++ #include <QtGui/qquaternion.h>
++
++ #include <QtEnvironmentVariables>
++
++ QT_BEGIN_NAMESPACE
++
++ /*!
++ \qmltype PhysicsWorld
++ \inqmlmodule QtQuick3D.Physics
++ \since 6.4
++ \brief Controls the physics simulation.
++
++ The PhysicsWorld type controls the physics simulation. This node is used to create an instance of the physics world as well
++ as define its properties. There can only be one physics world. All collision nodes in the qml
++ will get added automatically to the physics world.
++ */
++
++ /*!
++ \qmlproperty vector3d PhysicsWorld::gravity
++ This property defines the gravity vector of the physics world.
++ The default value is \c (0, -981, 0). Set the value to \c{Qt.vector3d(0, -9.81, 0)} if your
++ unit of measurement is meters and you are simulating Earth gravity.
++ */
++
++ /*!
++ \qmlproperty bool PhysicsWorld::running
++ This property starts or stops the physical simulation. The default value is \c true.
++ */
++
++ /*!
++ \qmlproperty bool PhysicsWorld::forceDebugDraw
++ This property enables debug drawing of all active shapes in the physics world. The default value
++ is \c false.
++ */
++
++ /*!
++ \qmlproperty bool PhysicsWorld::enableCCD
++ \deprecated [6.13] Use DynamicRigidBody::ccd instead.
++
++ This property enables continuous collision detection globally for every dynamic body
++ in the scene that does not explicitly set its own \l DynamicRigidBody::ccd mode.
++ It reduces the risk of fast-moving bodies passing through geometry at high velocities
++ (also known as tunnelling).
++
++ For non-kinematic dynamic bodies, enabling this property uses sweep-based CCD.
++ For kinematic bodies, it automatically uses speculative CCD, as sweep-based CCD
++ is not supported for them.
++
++ Continuous collision detection can also be configured per body via
++ \l DynamicRigidBody::ccd, which avoids paying its performance cost for
++ objects that do not need it.
++
++ \warning Using trigger bodies with CCD enabled is not supported and can
++ result in missing or false trigger reports.
++
++ \default false
++
++ \sa DynamicRigidBody::ccd
++ */
++
++ /*!
++ \qmlproperty real PhysicsWorld::typicalLength
++ This property defines the approximate size of objects in the simulation. This is used to
++ estimate certain length-related tolerances. Objects much smaller or much larger than this
++ size may not behave properly. The default value is \c 100.
++
++ Range: \c{[0, inf]}
++ */
++
++ /*!
++ \qmlproperty real PhysicsWorld::typicalSpeed
++ This property defines the typical magnitude of velocities of objects in simulation. This is used
++ to estimate whether a contact should be treated as bouncing or resting based on its impact
++ velocity, and a kinetic energy threshold below which the simulation may put objects to sleep.
++
++ For normal physical environments, a good choice is the approximate speed of an object falling
++ under gravity for one second. The default value is \c 1000.
++
++ Range: \c{[0, inf]}
++ */
++
++ /*!
++ \qmlproperty real PhysicsWorld::defaultDensity
++ This property defines the default density of dynamic objects, measured in kilograms per cubic
++ unit. This is equal to the weight of a cube with side \c 1.
++
++ The default value is \c 0.001, corresponding to 1 g/cm³: the density of water. If your unit of
++ measurement is meters, a good value would be \c 1000. Note that only positive values are
++ allowed.
++
++ Range: \c{(0, inf]}
++ */
++
++ /*!
++ \qmlproperty Node PhysicsWorld::viewport
++ This property defines the viewport where debug components will be drawn if \l{forceDebugDraw}
++ is enabled. If unset the \l{scene} node will be used.
++
++ \sa forceDebugDraw, scene
++ */
++
++ /*!
++ \qmlproperty real PhysicsWorld::minimumTimestep
++ This property defines the minimum simulation timestep in milliseconds. The default value is
++ \c{1}.
++
++ Range: \c{[0, maximumTimestep]}
++
++ \note The simulation timestep works in lockstep with the rendering,
++ meaning a new simulation frame will only be started after a rendered frame
++ has completed. This means that at most one simulation frame will run per
++ rendered frame.
++ */
++
++ /*!
++ \qmlproperty real PhysicsWorld::maximumTimestep
++ This property defines the maximum simulation timestep in milliseconds. The default value is
++ \c{33.333}.
++
++ Range: \c{[0, inf]}
++
++ \note The simulation timestep works in lockstep with the rendering,
++ meaning a new simulation frame will only be started after a rendered frame
++ has completed. This means that at most one simulation frame will run per
++ rendered frame.
++ */
++
++ /*!
++ \qmlproperty Node PhysicsWorld::scene
++
++ This property defines the top-most Node that contains all the nodes of the physical
++ simulation. All physics objects that are an ancestor of this node will be seen as part of this
++ PhysicsWorld.
++
++ \note Using the same scene node for several PhysicsWorld is unsupported.
++ */
++
++ /*!
++ \qmlsignal PhysicsWorld::frameDone(float timestep)
++ \since 6.5
++
++ This signal is emitted when the physical simulation is done simulating a frame. The \a timestep
++ parameter is how long in milliseconds the timestep was in the simulation.
++ */
++
++ /*!
++ \qmlproperty int PhysicsWorld::numThreads
++ \since 6.7
++
++ This property defines the number of threads used for the physical simulation. This is how the
++ range of values are interpreted:
++
++ \table
++ \header
++ \li Value
++ \li Range
++ \li Description
++ \row
++ \li Negative
++ \li \c{[-inf, -1]}
++ \li Automatic thread count. The application will try to query the number of threads from the
++ system.
++ \row
++ \li Zero
++ \li \c{{0}}
++ \li No threading, simulation will run sequentially.
++ \row
++ \li Positive
++ \li \c{[1, 256]}
++ \li Specific thread count. Values above 256 are clamped.
++ \endtable
++
++ The default value is \c{-1}, meaning automatic thread count.
++
++ \note Once the scene has started running it is not possible to change the number of threads.
++ */
++
++ /*!
++ \qmlproperty bool PhysicsWorld::reportKinematicKinematicCollisions
++ \since 6.7
++
++ This property controls if collisions between pairs of \e{kinematic} dynamic rigid bodies will
++ trigger a contact report.
++
++ The default value is \c{false}.
++
++ \note Once the scene has started running it is not possible to change this setting.
++ \sa PhysicsWorld::reportStaticKinematicCollisions
++ \sa DynamicRigidBody
++ \sa PhysicsNode::bodyContact
++ */
++
++ /*!
++ \qmlproperty bool PhysicsWorld::reportStaticKinematicCollisions
++ \since 6.7
++
++ This property controls if collisions between a static rigid body and a \e{kinematic} dynamic
++ rigid body will trigger a contact report.
++
++ The default value is \c{false}.
++
++ \note Once the scene has started running it is not possible to change this setting.
++ \sa PhysicsWorld::reportKinematicKinematicCollisions
++ \sa StaticRigidBody
++ \sa DynamicRigidBody
++ \sa PhysicsNode::bodyContact
++ */
++
++ /*!
++ \qmlproperty QueryStructure PhysicsWorld::staticQueryStructure
++ \since 6.13
++
++ The spatial pruning structure type used to accelerate scene queries
++ (raycasts, sweeps, overlaps - used in CharacterController and a \e{kinematic} dynamic rigid body)
++ against static actors in the physics scene. It has no effect on rigid-body
++ contact/collision detection during simulation.
++
++ The following values are available:
++
++ \value PhysicsWorld.StaticTree
++ Uses a pre-baked static AABB tree. Offers maximum scene query performance with zero per-frame management overhead.
++ Best for fully static, immutable scenes. Inserting or removing objects at runtime causes heavy scene rebuilds and frame spikes.
++ \value PhysicsWorld.DynamicTree
++ Uses a dynamic self-balancing AABB tree. Allows fast, local runtime insertion (logarithmic time) and removal of static objects without freezing the frame.
++ Ideal for seamless open-world streaming where static chunks are loaded dynamically.
++
++ \default PhysicsWorld.DynamicTree
++
++ \note Once the scene has started running it is not possible to change this setting.
++ \note PhysicsWorld.NoStructure is not supported for staticQueryStructure
++
++ \sa PhysicsWorld::dynamicQueryStructure
++
++ */
++
++ /*!
++ \qmlproperty QueryStructure PhysicsWorld::dynamicQueryStructure
++ \since 6.13
++
++ The spatial pruning structure type used to accelerate scene queries
++ (raycasts, sweeps, overlaps - used in CharacterController and a \e{kinematic} dynamic rigid body)
++ against dynamic actors in the physics scene. It has no effect on rigid-body
++ contact/collision detection during simulation.
++
++ The following values are available:
++
++ \value PhysicsWorld.NoStructure
++ Disables the scene query acceleration structure for dynamic actors.
++ Eliminates CPU overhead for tree maintenance when objects move, spawn, or are destroyed.
++ Scene queries will fall back to a linear search. Ideal when scene queries are not needed for dynamic objects.
++ \value PhysicsWorld.StaticTree
++ Uses a static AABB tree. Offers faster scene queries for dynamic actors, but updating the tree when objects move or spawn is very expensive.
++ Best when dynamic actors rarely move or spend most of their time sleeping.
++ \value PhysicsWorld.DynamicTree
++ Uses a dynamic self-balancing AABB tree. Allows fast, local runtime insertion (logarithmic time), movement, and removal of dynamic objects without freezing the frame.
++ Ideal for active scenes with frequently moving or spawning dynamic actors.
++
++ \default PhysicsWorld.DynamicTree
++
++ \note Once the scene has started running it is not possible to change this setting.
++ \sa PhysicsWorld::staticQueryStructure
++ */
++
++ /*!
++ \qmlmethod bool PhysicsWorld::testRaycastQuery(vector3d origin, vector3d direction,
++ real maxDistance,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Performs a fast occlusion check along a ray without computing precise hit
++ geometry or returning hit data.
++
++ Returns \c true if the ray hits a body within \a maxDistance; otherwise returns
++ \c false. The query stops at the first body it finds, which makes it the cheapest
++ of the raycast queries.
++
++ \list
++ \li \a origin is the starting position of the ray, in world space.
++ \li \a direction is the direction of the ray. It must not be a null vector and does
++ not need to be normalized; the query normalizes it.
++ \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
++ must be greater than 0.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
++
++ \sa singleRaycastQuery, multiRaycastQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ /*!
++ \qmlmethod locationHit PhysicsWorld::singleRaycastQuery(vector3d origin,
++ vector3d direction,
++ real maxDistance,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Casts a ray through the physics scene and returns the closest body it hits.
++
++ The ray starts at \a origin and extends along \a direction up to \a maxDistance.
++ Returns a \l locationHit describing the closest intersection.
++
++ \list
++ \li \a origin is the starting position of the ray, in world space.
++ \li \a direction is the direction of the ray. It must not be a null vector and does
++ not need to be normalized; the query normalizes it.
++ \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
++ must be greater than 0.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc hit_miss_return
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
++
++ \sa testRaycastQuery, multiRaycastQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ /*!
++ \qmlmethod list<locationHit> PhysicsWorld::multiRaycastQuery(vector3d origin,
++ vector3d direction,
++ real maxDistance,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Casts a ray through the physics scene and returns every body it passes through,
++ rather than stopping at the closest one.
++
++ The ray starts at \a origin and extends along \a direction up to \a maxDistance.
++ Returns a \l locationHit for each body hit, or an empty list if the ray hits
++ nothing.
++
++ \list
++ \li \a origin is the starting position of the ray, in world space.
++ \li \a direction is the direction of the ray. It must not be a null vector and does
++ not need to be normalized; the query normalizes it.
++ \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
++ must be greater than 0.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
++
++ \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
++
++ \include qtquick3dphysics-queries.qdocinc unordered_hits_note
++
++ \sa testRaycastQuery, singleRaycastQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ /*!
++ \qmlmethod bool PhysicsWorld::testSweepQuery(CollisionShape shape, vector3d direction,
++ real maxDistance,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Sweeps a collision shape along a straight path and reports whether the path is
++ obstructed, without computing precise hit geometry.
++
++ Returns \c true if \a shape hits a body before travelling \a maxDistance; otherwise
++ returns \c false. The query stops at the first body it finds, which makes it the
++ cheapest of the sweep queries.
++
++ \list
++ \li \a shape is the \l CollisionShape to sweep through the scene.
++ \li \a direction is the direction of the sweep. It must not be a null vector and does
++ not need to be normalized; the query normalizes it.
++ \li \a maxDistance is the maximum distance along \a direction to travel. It must be
++ greater than 0.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc query_shape
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \sa singleSweepQuery, multiSweepQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ /*!
++ \qmlmethod locationHit PhysicsWorld::singleSweepQuery(CollisionShape shape,
++ vector3d direction,
++ real maxDistance,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Sweeps a collision shape along a straight path and returns the closest body it
++ hits.
++
++ Returns a \l locationHit describing the closest intersection encountered along the
++ sweep.
++
++ \list
++ \li \a shape is the \l CollisionShape to sweep through the scene.
++ \li \a direction is the direction of the sweep. It must not be a null vector and does
++ not need to be normalized; the query normalizes it.
++ \li \a maxDistance is the maximum distance along \a direction to travel. It must be
++ greater than 0.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc query_shape
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc hit_miss_return
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \include qtquick3dphysics-queries.qdocinc mtd_overlap_note
++
++ \sa testSweepQuery, multiSweepQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ /*!
++ \qmlmethod list<locationHit> PhysicsWorld::multiSweepQuery(CollisionShape shape,
++ vector3d direction,
++ real maxDistance,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Sweeps a collision shape along a straight path and returns every body it passes
++ through, rather than stopping at the closest one.
++
++ Returns a \l locationHit for each body hit, or an empty list if the sweep hits
++ nothing.
++
++ \list
++ \li \a shape is the \l CollisionShape to sweep through the scene.
++ \li \a direction is the direction of the sweep. It must not be a null vector and does
++ not need to be normalized; the query normalizes it.
++ \li \a maxDistance is the maximum distance along \a direction to travel. It must be
++ greater than 0.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc query_shape
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
++
++ \include qtquick3dphysics-queries.qdocinc unordered_hits_note
++
++ \include qtquick3dphysics-queries.qdocinc mtd_overlap_note
++
++ \sa testSweepQuery, singleSweepQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ /*!
++ \qmlmethod bool PhysicsWorld::testOverlapQuery(CollisionShape shape,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Reports whether any body occupies the volume of a stationary collision shape.
++
++ Returns \c true if \a shape overlaps a body; otherwise returns \c false. The query
++ stops at the first body it finds and builds no hit data, which makes it the cheapest
++ of the overlap queries.
++
++ \list
++ \li \a shape is the \l CollisionShape defining the overlap volume.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc query_shape
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \sa multiOverlapQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ /*!
++ \qmlmethod list<queryHit> PhysicsWorld::multiOverlapQuery(CollisionShape shape,
++ bool includeStatic = true,
++ bool includeDynamic = true)
++ \since 6.13
++
++ Returns every body that overlaps the volume of a stationary collision shape.
++
++ Returns a \l queryHit for each overlapping body, or an empty list if the volume is
++ unoccupied.
++
++ \list
++ \li \a shape is the \l CollisionShape defining the overlap volume.
++ \li \a includeStatic includes static bodies in the query when \c true.
++ \li \a includeDynamic includes dynamic bodies in the query when \c true.
++ \endlist
++
++ \include qtquick3dphysics-queries.qdocinc query_shape
++
++ \include qtquick3dphysics-queries.qdocinc include_flags
++
++ \include qtquick3dphysics-queries.qdocinc query_exclusions
++
++ \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
++
++ \include qtquick3dphysics-queries.qdocinc unordered_hits_note
++
++ \sa testOverlapQuery, {Qt Quick 3D Physics Scene Queries}
++ */
++
++ Q_LOGGING_CATEGORY(lcQuick3dPhysics, "qt.quick3d.physics");
++
++ // Setting QT_PHYSICS_TIMINGS_FILE to a filepath will generate a csv file with frame timings.
++ // Note that if running several PhysicsWorld's the last one to be destructed will overwrite
++ // the output file.
++ //
++ // To view it in gnuplot, run these two commands:
++ //
++ // set datafile separator ','
++ // plot '<QT_PHYSICS_TIMINGS_FILE>' using 1:2 with lines
++ //
++ // Security note: This file is trusted since it is opened as write-only
++ static const QString qtPhysicsTimingsFile = qEnvironmentVariable("QT_PHYSICS_TIMINGS_FILE");
++
++ /////////////////////////////////////////////////////////////////////////////
++
++ static bool validateQuery(const physx::PxScene *scene,
++ bool includeStatic,
++ bool includeDynamic)
++ {
++ if (!scene) {
++ qWarning() << "QtQuick3DPhysics: cannot run a scene query before the physics world is "
++ "initialized.";
++ return false;
++ }
++
++ if (!(includeStatic || includeDynamic)) {
++ qWarning() << "QtQuick3DPhysics: a scene query needs includeStatic or includeDynamic to "
++ "be true.";
++ return false;
++ }
++
++ return true;
++ }
++
++ static bool raycastImpl(const physx::PxScene *scene,
++ const QVector3D &origin,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic,
++ physx::PxRaycastCallback &hitCallback,
++ physx::PxHitFlags hitFlags,
++ physx::PxQueryFlags extraQueryFlags = {},
++ physx::PxQueryFilterCallback *filterCall = nullptr)
++ {
++ if (!validateQuery(scene, includeStatic, includeDynamic))
++ return false;
++
++ const physx::PxVec3 pxOrigin = QPhysicsUtils::toPhysXType(origin);
++ if (!pxOrigin.isFinite()) {
++ qWarning() << "QtQuick3DPhysics: the query origin must be finite.";
++ return false;
++ }
++
++ const physx::PxVec3 pxDirection = QPhysicsUtils::toPhysXType(direction.normalized());
++ if (!pxDirection.isNormalized()) {
++ qWarning() << "QtQuick3DPhysics: the query direction must be a non-null, finite vector.";
++ return false;
++ }
++
++ if (!qIsFinite(maxDistance) || maxDistance <= 0) {
++ qWarning() << "QtQuick3DPhysics: the query distance must be finite and greater than 0.";
++ return false;
++ }
++
++ // Combine base flags with optional query flags (e.g., eANY_HIT or eNO_BLOCK)
++ physx::PxQueryFlags filter = extraQueryFlags;
++ if (includeStatic)
++ filter |= physx::PxQueryFlag::eSTATIC;
++ if (includeDynamic)
++ filter |= physx::PxQueryFlag::eDYNAMIC;
++
++ // Execute PhysX scene raycast
++ return scene->raycast(pxOrigin, pxDirection, maxDistance, hitCallback, hitFlags,
++ physx::PxQueryFilterData(filter), filterCall);
++ }
++
++ static const physx::PxGeometry *validateQueryShape(const physx::PxScene *scene,
++ QAbstractCollisionShape *shape,
++ bool includeStatic,
++ bool includeDynamic)
++ {
++ if (!validateQuery(scene, includeStatic, includeDynamic)) {
++ return nullptr;
++ }
++
++ const physx::PxGeometry *geometry = shape ? shape->getPhysXGeometry() : nullptr;
++ if (!geometry) {
++ qWarning() << "QtQuick3DPhysics: the query shape has no geometry yet.";
++ return nullptr;
++ }
++
++ // PhysX only supports sphere, capsule, box and convex mesh as the query geometry. Anything
++ // else leaves Gu::ShapeData's bounds uninitialized in a release build.
++ switch (geometry->getType()) {
++ case physx::PxGeometryType::eSPHERE:
++ case physx::PxGeometryType::eCAPSULE:
++ case physx::PxGeometryType::eBOX:
++ case physx::PxGeometryType::eCONVEXMESH:
++ break;
++ default:
++ qWarning() << "QtQuick3DPhysics: sweep and overlap queries only support SphereShape, "
++ "CapsuleShape, BoxShape and ConvexMeshShape as the query shape.";
++ return nullptr;
++ }
++
++ return geometry;
++ }
++
++ static bool sweepImpl(const physx::PxScene *scene,
++ QAbstractCollisionShape *shape,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic,
++ physx::PxSweepCallback &hitCallback,
++ physx::PxHitFlags hitFlags,
++ physx::PxQueryFlags extraQueryFlags = {})
++ {
++ const physx::PxGeometry *geometry = validateQueryShape(scene, shape, includeStatic, includeDynamic);
++ if (!geometry) {
++ return false;
++ }
++
++ const physx::PxVec3 pxDirection = QPhysicsUtils::toPhysXType(direction.normalized());
++ if (!pxDirection.isNormalized()) {
++ qWarning() << "QtQuick3DPhysics: the query direction must be a non-null, finite vector.";
++ return false;
++ }
++
++ if (!qIsFinite(maxDistance) || maxDistance <= 0) {
++ qWarning() << "QtQuick3DPhysics: the query distance must be finite and greater than 0.";
++ return false;
++ }
++
++ // Combine base flags with optional query flags (e.g., eANY_HIT or eNO_BLOCK)
++ physx::PxQueryFlags filter = extraQueryFlags;
++ if (includeStatic)
++ filter |= physx::PxQueryFlag::eSTATIC;
++ if (includeDynamic)
++ filter |= physx::PxQueryFlag::eDYNAMIC;
++
++ const physx::PxTransform pose(QPhysicsUtils::toPhysXType(shape->scenePosition()),
++ QPhysicsUtils::toPhysXType(shape->sceneRotation()));
++ if (!pose.isSane()) {
++ qWarning() << "QtQuick3DPhysics: the query position/rotation is not finite.";
++ return false;
++ }
++
++ // Execute PhysX scene sweep
++ return scene->sweep(*geometry, pose, pxDirection, maxDistance, hitCallback, hitFlags,
++ physx::PxQueryFilterData(filter));
++ }
++
++ static bool overlapImpl(const physx::PxScene *scene,
++ QAbstractCollisionShape *shape,
++ bool includeStatic,
++ bool includeDynamic,
++ physx::PxOverlapCallback &hitCallback,
++ physx::PxQueryFlags extraQueryFlags = {})
++ {
++
++ const physx::PxGeometry *geometry = validateQueryShape(scene, shape, includeStatic, includeDynamic);
++ if (!geometry) {
++ return false;
++ }
++
++ // Combine base static/dynamic flags with query behavior flags (e.g., eANY_HIT or eNO_BLOCK)
++ physx::PxQueryFlags filter = extraQueryFlags;
++ if (includeStatic)
++ filter |= physx::PxQueryFlag::eSTATIC;
++ if (includeDynamic)
++ filter |= physx::PxQueryFlag::eDYNAMIC;
++
++ const physx::PxTransform pose(QPhysicsUtils::toPhysXType(shape->scenePosition()),
++ QPhysicsUtils::toPhysXType(shape->sceneRotation()));
++ if (!pose.isSane()) {
++ qWarning() << "QtQuick3DPhysics: the query position/rotation is not finite.";
++ return false;
++ }
++
++ // Execute PhysX scene overlap query
++ return scene->overlap(*geometry, pose, hitCallback,
++ physx::PxQueryFilterData(filter));
++ }
++
++ class FrameAnimator : public QQuickFrameAnimation
++ {
++ Q_OBJECT
++ public:
++ FrameAnimator() : QQuickFrameAnimation()
++ {
++ // Needed to start the frame animation
++ classBegin();
++ componentComplete();
++ }
++ };
++
++ namespace {
++
++ // Passes over the actors a Raycaster must not report: the bodies it excludes, and any actor
++ // whose body was deleted earlier in this frame, which stays in the scene until the next one
++ // with nothing behind it. Filtering before a hit is recorded keeps the query blocking, so
++ // PhysX still shortens the ray at the first surface that is kept.
++ class ExcludedBodiesFilter : public physx::PxQueryFilterCallback
++ {
++ public:
++ explicit ExcludedBodiesFilter(QSpan<const QAbstractPhysicsNode *const> excludedBodies)
++ : m_excludedBodies(excludedBodies)
++ {
++ }
++
++ physx::PxQueryHitType::Enum preFilter(const physx::PxFilterData & /*filterData*/,
++ const physx::PxShape * /*shape*/,
++ const physx::PxRigidActor *actor,
++ physx::PxHitFlags & /*queryFlags*/) override
++ {
++ const auto *body = static_cast<const QAbstractPhysicsNode *>(actor->userData);
++ if (!body)
++ return physx::PxQueryHitType::eNONE;
++ const bool excluded = std::find(m_excludedBodies.begin(), m_excludedBodies.end(), body)
++ != m_excludedBodies.end();
++ return excluded ? physx::PxQueryHitType::eNONE : physx::PxQueryHitType::eBLOCK;
++ }
++
++ physx::PxQueryHitType::Enum postFilter(const physx::PxFilterData & /*filterData*/,
++ const physx::PxQueryHit & /*hit*/,
++ const physx::PxShape * /*shape*/,
++ const physx::PxRigidActor * /*actor*/) override
++ {
++ return physx::PxQueryHitType::eBLOCK;
++ }
++
++ private:
++ QSpan<const QAbstractPhysicsNode *const> m_excludedBodies;
++ };
++
++ // Collects every touching hit from a scene query. PhysX needs a caller-owned buffer to sort
++ // into, and hands the hits over in batches of at most BufferSize once it fills up, so the
++ // buffer stays and processTouches() appends each batch to hits.
++ // A struct because the PhysX callback bases declare all their members public.
++ template<class PxCbT, class HitT, class PxHitT, physx::PxU32 BufferSize = 16>
++ struct DynamicQueryCallback : public PxCbT
++ {
++ DynamicQueryCallback() : PxCbT(localBuffer, BufferSize) {}
++
++ physx::PxAgain processTouches(const PxHitT *buffer, physx::PxU32 nbHits) override
++ {
++ hits.reserve(hits.size() + nbHits);
++ for (physx::PxU32 i = 0; i < nbHits; ++i)
++ hits.emplaceBack(buffer[i], buffer[i]);
++ return true;
++ }
++
++ PxHitT localBuffer[BufferSize];
++ QList<HitT> hits;
++ };
++
++ } // namespace
++
++ /////////////////////////////////////////////////////////////////////////////
++
++ void QPhysicsWorld::DebugModelHolder::releaseMeshPointer()
++ {
++ if (auto base = static_cast<physx::PxBase *>(ptr); base)
++ base->release();
++ ptr = nullptr;
++ }
++
++ const QVector3D &QPhysicsWorld::DebugModelHolder::halfExtents() const
++ {
++ return data;
++ }
++ void QPhysicsWorld::DebugModelHolder::setHalfExtents(const QVector3D &halfExtents)
++ {
++ data = halfExtents;
++ }
++ float QPhysicsWorld::DebugModelHolder::radius() const
++ {
++ return data.x();
++ }
++ void QPhysicsWorld::DebugModelHolder::setRadius(float radius)
++ {
++ data.setX(radius);
++ }
++ float QPhysicsWorld::DebugModelHolder::heightScale() const
++ {
++ return data.x();
++ }
++ void QPhysicsWorld::DebugModelHolder::setHeightScale(float heightScale)
++ {
++ data.setX(heightScale);
++ }
++ float QPhysicsWorld::DebugModelHolder::halfHeight() const
++ {
++ return data.y();
++ }
++ void QPhysicsWorld::DebugModelHolder::setHalfHeight(float halfHeight)
++ {
++ data.setY(halfHeight);
++ }
++ float QPhysicsWorld::DebugModelHolder::rowScale() const
++ {
++ return data.y();
++ }
++ void QPhysicsWorld::DebugModelHolder::setRowScale(float rowScale)
++ {
++ data.setY(rowScale);
++ }
++ float QPhysicsWorld::DebugModelHolder::columnScale() const
++ {
++ return data.z();
++ }
++ void QPhysicsWorld::DebugModelHolder::setColumnScale(float columnScale)
++ {
++ data.setZ(columnScale);
++ }
++ physx::PxConvexMesh *QPhysicsWorld::DebugModelHolder::getConvexMesh()
++ {
++ return static_cast<physx::PxConvexMesh *>(ptr);
++ }
++ void QPhysicsWorld::DebugModelHolder::setConvexMesh(physx::PxConvexMesh *mesh)
++ {
++ ptr = static_cast<void *>(mesh);
++ }
++ physx::PxTriangleMesh *QPhysicsWorld::DebugModelHolder::getTriangleMesh()
++ {
++ return static_cast<physx::PxTriangleMesh *>(ptr);
++ }
++ void QPhysicsWorld::DebugModelHolder::setTriangleMesh(physx::PxTriangleMesh *mesh)
++ {
++ ptr = static_cast<void *>(mesh);
++ }
++ physx::PxHeightField *QPhysicsWorld::DebugModelHolder::getHeightField()
++ {
++ return static_cast<physx::PxHeightField *>(ptr);
++ }
++ void QPhysicsWorld::DebugModelHolder::setHeightField(physx::PxHeightField *hf)
++ {
++ ptr = static_cast<physx::PxHeightField *>(hf);
++ }
++
++ /////////////////////////////////////////////////////////////////////////////
++
++ struct QWorldManager
++ {
++ QVector<QPhysicsWorld *> worlds;
++ QVector<QAbstractPhysicsNode *> orphanNodes;
++ QVector<QPhysicsJoint *> orphanJoints;
++ QVector<QRaycaster *> orphanRaycasters;
++ };
++
++ static QWorldManager worldManager = QWorldManager {};
++
++ void QPhysicsWorld::registerNode(QAbstractPhysicsNode *physicsNode)
++ {
++ auto world = getWorld(physicsNode);
++ if (world) {
++ world->m_newPhysicsNodes.push_back(physicsNode);
++ } else {
++ worldManager.orphanNodes.push_back(physicsNode);
++ }
++ }
++
++ void QPhysicsWorld::deregisterNode(QAbstractPhysicsNode *physicsNode)
++ {
++ for (auto world : std::as_const(worldManager.worlds)) {
++ world->m_newPhysicsNodes.removeAll(physicsNode);
++ if (physicsNode->m_backendObject) {
++ Q_ASSERT(physicsNode->m_backendObject->frontendNode == physicsNode);
++ physicsNode->m_backendObject->detachFrontend();
++ physicsNode->m_backendObject->frontendNode = nullptr;
++ physicsNode->m_backendObject->isRemoved = true;
++ physicsNode->m_backendObject = nullptr;
++ }
++ world->m_removedPhysicsNodes.insert(physicsNode);
++ }
++ worldManager.orphanNodes.removeAll(physicsNode);
++ }
++
++ void QPhysicsWorld::registerJoint(QPhysicsJoint *joint)
++ {
++ auto world = getWorld(joint);
++ if (world) {
++ world->m_joints.push_back(joint);
++ } else {
++ worldManager.orphanJoints.push_back(joint);
++ }
++ }
++
++ void QPhysicsWorld::deregisterJoint(QPhysicsJoint *joint)
++ {
++ for (auto world : worldManager.worlds) {
++ world->m_removedJoints.insert(joint->getPhysXBackend());
++
++ // Swap erase since order does not matter
++ qsizetype idx = world->m_joints.indexOf(joint);
++ if (idx != -1) {
++ world->m_joints.swapItemsAt(idx, world->m_joints.size() - 1);
++ world->m_joints.pop_back();
++ }
++ }
++ worldManager.orphanJoints.removeAll(joint);
++ }
++
++ void QPhysicsWorld::registerRaycaster(QRaycaster *raycaster)
++ {
++ if (auto *world = getWorld(raycaster))
++ world->m_raycasters.push_back(raycaster);
++ else
++ worldManager.orphanRaycasters.push_back(raycaster);
++ }
++
++ void QPhysicsWorld::deregisterRaycaster(QRaycaster *raycaster)
++ {
++ for (auto *world : std::as_const(worldManager.worlds))
++ world->m_raycasters.removeAll(raycaster);
++ worldManager.orphanRaycasters.removeAll(raycaster);
++ }
++
++ void QPhysicsWorld::registerContact(QAbstractPhysicsNode *sender, QAbstractPhysicsNode *receiver,
++ const QVector<QVector3D> &positions,
++ const QVector<QVector3D> &impulses,
++ const QVector<QVector3D> &normals)
++ {
++ // The callbacks come from fetchResults(), on this thread, but a contact is
++ // between two nodes and reporting the first is free to delete the second.
++ // So the contacts are saved and run at the end of the physics frame, when
++ // it is known which of their nodes are still there.
++
++ BodyContact contact;
++ contact.sender = sender;
++ contact.receiver = receiver;
++ contact.positions = positions;
++ contact.impulses = impulses;
++ contact.normals = normals;
++
++ m_registeredContacts.push_back(contact);
++ }
++
++ QPhysicsWorld::QPhysicsWorld(QObject *parent) : QObject(parent)
++ {
++ m_inDesignStudio = !qEnvironmentVariableIsEmpty("QML_PUPPET_MODE");
++ m_physx = new QPhysXWorld;
++ m_physx->createWorld();
++
++ worldManager.worlds.push_back(this);
++ matchOrphanNodes();
++ matchOrphanJoints();
++ matchOrphanRaycasters();
++
++ m_frameAnimator = new FrameAnimator;
++ connect(m_frameAnimator, &QQuickFrameAnimation::triggered, this,
++ &QPhysicsWorld::simulateFrame);
++ }
++
++ QPhysicsWorld::~QPhysicsWorld()
++ {
++ if (m_frameAnimator) {
++ m_frameAnimator->stop();
++ delete m_frameAnimator;
++ }
++
++ if (m_physx->scene && m_physx->isRunning && !m_frameFetched)
++ m_physx->scene->fetchResults(true);
++
++ for (auto body : std::as_const(m_physXBodies)) {
++ body->cleanup(m_physx);
++ delete body;
++ }
++ m_physXBodies.clear();
++ m_physx->deleteWorld();
++ delete m_physx;
++ worldManager.worlds.removeAll(this);
++
++ // A raycaster can outlive the world it was matched to, since it lives in the scene and
++ // not in this list. Back to the orphans, so a later world over the same scene picks it
++ // up again.
++ worldManager.orphanRaycasters += m_raycasters;
++ m_raycasters.clear();
++
++ if (!qtPhysicsTimingsFile.isEmpty()) {
++ if (m_frameTimings.isEmpty()) {
++ qWarning() << "No frame timings saved.";
++ } else if (auto csvFile = QFile(qtPhysicsTimingsFile); csvFile.open(QIODevice::WriteOnly)) {
++ QTextStream out(&csvFile);
++ for (int i = 1; i < m_frameTimings.size(); i++) {
++ out << i << "," << m_frameTimings[i] << '\n';
++ }
++ csvFile.close();
++ } else {
++ qWarning() << "Could not open timings file " << qtPhysicsTimingsFile;
++ }
++ }
++ }
++
++ void QPhysicsWorld::classBegin() {}
++
++ void QPhysicsWorld::componentComplete()
++ {
++ if ((!m_running && !m_inDesignStudio) || m_physicsInitialized)
++ return;
++ initPhysics();
++ }
++
++ QVector3D QPhysicsWorld::gravity() const
++ {
++ return m_gravity;
++ }
++
++ bool QPhysicsWorld::running() const
++ {
++ return m_running;
++ }
++
++ bool QPhysicsWorld::forceDebugDraw() const
++ {
++ return m_forceDebugDraw;
++ }
++
++ bool QPhysicsWorld::enableCCD() const
++ {
++ return m_enableCCD;
++ }
++
++ float QPhysicsWorld::typicalLength() const
++ {
++ return m_typicalLength;
++ }
++
++ float QPhysicsWorld::typicalSpeed() const
++ {
++ return m_typicalSpeed;
++ }
++
++ bool QPhysicsWorld::isNodeRemoved(QAbstractPhysicsNode *object)
++ {
++ return m_removedPhysicsNodes.contains(object);
++ }
++
++ void QPhysicsWorld::setGravity(QVector3D gravity)
++ {
++ if (m_gravity == gravity)
++ return;
++
++ if (!QPhysicsUtils::isFinite(gravity)) {
++ qWarning() << "Warning: 'gravity' must be finite, ignored";
++ return;
++ }
++
++ m_gravity = gravity;
++ if (m_physx->scene) {
++ m_physx->scene->setGravity(QPhysicsUtils::toPhysXType(m_gravity));
++ }
++ emit gravityChanged(m_gravity);
++ }
++
++ void QPhysicsWorld::setRunning(bool running)
++ {
++ if (m_running == running)
++ return;
++
++ m_running = running;
++ if (!m_inDesignStudio && m_running && !m_physicsInitialized)
++ initPhysics();
++
++ if (running)
++ m_frameAnimator->start();
++ else
++ m_frameAnimator->stop();
++
++ emit runningChanged(m_running);
++ }
++
++ void QPhysicsWorld::setForceDebugDraw(bool forceDebugDraw)
++ {
++ if (m_forceDebugDraw == forceDebugDraw)
++ return;
++
++ m_forceDebugDraw = forceDebugDraw;
++ if (!m_forceDebugDraw)
++ disableDebugDraw();
++ else
++ updateDebugDraw();
++ emit forceDebugDrawChanged(m_forceDebugDraw);
++ }
++
++ QQuick3DNode *QPhysicsWorld::viewport() const
++ {
++ return m_viewport;
++ }
++
++ void QPhysicsWorld::setHasIndividualDebugDraw()
++ {
++ m_hasIndividualDebugDraw = true;
++ }
++
++ void QPhysicsWorld::setViewport(QQuick3DNode *viewport)
++ {
++ if (m_viewport == viewport)
++ return;
++
++ m_viewport = viewport;
++
++ // TODO: test this
++ for (auto material : std::as_const(m_debugMaterials))
++ delete material;
++ m_debugMaterials.clear();
++
++ for (auto &holder : m_collisionShapeDebugModels) {
++ holder.releaseMeshPointer();
++ delete holder.model;
++ }
++ m_collisionShapeDebugModels.clear();
++
++ emit viewportChanged(m_viewport);
++ }
++
++ void QPhysicsWorld::setMinimumTimestep(float minTimestep)
++ {
++ if (qFuzzyCompare(m_minTimestep, minTimestep))
++ return;
++
++ if (!qIsFinite(minTimestep)) {
++ qWarning("Minimum timestep must be finite, ignoring");
++ return;
++ }
++
++ if (minTimestep > m_maxTimestep) {
++ qWarning("Minimum timestep greater than maximum timestep, value clamped");
++ minTimestep = qMin(minTimestep, m_maxTimestep);
++ }
++
++ if (minTimestep < 0.f) {
++ qWarning("Minimum timestep less than zero, value clamped");
++ minTimestep = qMax(minTimestep, 0.f);
++ }
++
++ if (qFuzzyCompare(m_minTimestep, minTimestep))
++ return;
++
++ m_minTimestep = minTimestep;
++ emit minimumTimestepChanged(m_minTimestep);
++ }
++
++ void QPhysicsWorld::setMaximumTimestep(float maxTimestep)
++ {
++ if (qFuzzyCompare(m_maxTimestep, maxTimestep))
++ return;
++
++ if (!qIsFinite(maxTimestep)) {
++ qWarning("Maximum timestep must be finite, ignoring");
++ return;
++ }
++
++ if (maxTimestep < 0.f) {
++ qWarning("Maximum timestep less than zero, value clamped");
++ maxTimestep = qMax(maxTimestep, 0.f);
++ }
++
++ if (qFuzzyCompare(m_maxTimestep, maxTimestep))
++ return;
++
++ m_maxTimestep = maxTimestep;
++ emit maximumTimestepChanged(maxTimestep);
++ }
++
++ void QPhysicsWorld::setupDebugMaterials(QQuick3DNode *sceneNode)
++ {
++ if (!m_debugMaterials.isEmpty())
++ return;
++
++ const int lineWidth = m_inDesignStudio ? 1 : 3;
++
++ // These colors match the indices of DebugDrawBodyType enum
++ for (auto color : { QColorConstants::Svg::chartreuse, QColorConstants::Svg::cyan,
++ QColorConstants::Svg::lightsalmon, QColorConstants::Svg::red,
++ QColorConstants::Svg::blueviolet, QColorConstants::Svg::black }) {
++ auto debugMaterial = new QQuick3DPrincipledMaterial();
++ debugMaterial->setLineWidth(lineWidth);
++ debugMaterial->setParentItem(sceneNode);
++ debugMaterial->setParent(sceneNode);
++ debugMaterial->setBaseColor(color);
++ debugMaterial->setLighting(QQuick3DPrincipledMaterial::NoLighting);
++ debugMaterial->setCullMode(QQuick3DMaterial::NoCulling);
++ m_debugMaterials.push_back(debugMaterial);
++ }
++ }
++
++ void QPhysicsWorld::updateDebugDraw()
++ {
++ if (!(m_forceDebugDraw || m_hasIndividualDebugDraw)) {
++ // Nothing to draw, trash all previous models (if any) and return
++ for (auto &holder : m_collisionShapeDebugModels) {
++ holder.releaseMeshPointer();
++ delete holder.model;
++ }
++ m_collisionShapeDebugModels.clear();
++ return;
++ }
++
++ // Use scene node if no viewport has been specified
++ auto sceneNode = m_viewport ? m_viewport : m_scene;
++
++ if (sceneNode == nullptr)
++ return;
++
++ setupDebugMaterials(sceneNode);
++ m_hasIndividualDebugDraw = false;
++
++ // Store the collision shapes we have now so we can clear out the removed ones
++ QSet<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>> currentCollisionShapes;
++ currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
++
++ for (QAbstractPhysXNode *node : std::as_const(m_physXBodies)) {
++ // A node deleted from a report earlier in this frame keeps its backend
++ // until the next one takes it out.
++ if (!node->frontendNode || !node->debugGeometryCapability())
++ continue;
++
++ const auto &collisionShapes = node->frontendNode->getCollisionShapesList();
++ const int materialIdx = static_cast<int>(node->getDebugDrawBodyType());
++ const int length = collisionShapes.length();
++ for (int idx = 0; idx < length; idx++) {
++ const auto collisionShape = collisionShapes[idx];
++
++ if (!m_forceDebugDraw && !collisionShape->enableDebugDraw())
++ continue;
++
++ DebugModelHolder &holder =
++ m_collisionShapeDebugModels[std::make_pair(collisionShape, node)];
++ auto &model = holder.model;
++
++ currentCollisionShapes.insert(std::make_pair(collisionShape, node));
++
++ m_hasIndividualDebugDraw =
++ m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
++
++ // Create/Update debug view infrastructure
++ if (!model) {
++ model = new QQuick3DModel();
++ model->setParentItem(sceneNode);
++ model->setParent(sceneNode);
++ model->setCastsShadows(false);
++ model->setReceivesShadows(false);
++ model->setCastsReflections(false);
++ }
++
++ model->setVisible(true);
++
++ { // update or set material
++ auto material = m_debugMaterials[materialIdx];
++ QQmlListReference materialsRef(model, "materials");
++ if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
++ materialsRef.clear();
++ materialsRef.append(material);
++ }
++ }
++
++ // Special handling of CharacterController since it has collision shapes,
++ // but not PhysX shapes
++ if (qobject_cast<QCharacterController *>(node->frontendNode)) {
++ QCapsuleShape *capsuleShape = qobject_cast<QCapsuleShape *>(collisionShape);
++ if (!capsuleShape)
++ continue;
++
++ const float radius = capsuleShape->diameter() * 0.5;
++ const float halfHeight = capsuleShape->height() * 0.5;
++
++ if (!qFuzzyCompare(radius, holder.radius())
++ || !qFuzzyCompare(halfHeight, holder.halfHeight())) {
++ auto geom = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
++ geom->setParent(model);
++ model->setGeometry(geom);
++ holder.setRadius(radius);
++ holder.setHalfHeight(halfHeight);
++ }
++
++ model->setPosition(node->frontendNode->scenePosition());
++ model->setRotation(node->frontendNode->sceneRotation()
++ * QQuaternion::fromEulerAngles(0, 0, 90));
++ continue;
++ }
++
++ if (node->shapes.length() < length)
++ continue;
++
++ const auto physXShape = node->shapes[idx];
++ auto localPose = physXShape->getLocalPose();
++
++ const physx::PxGeometry &geometry = physXShape->getGeometry();
++
++ switch (geometry.getType()) {
++ case physx::PxGeometryType::eBOX: {
++ const auto &boxGeometry = static_cast<const physx::PxBoxGeometry &>(geometry);
++ const auto &halfExtentsOld = holder.halfExtents();
++ const auto halfExtents = QPhysicsUtils::toQtType(boxGeometry.halfExtents);
++ if (!qFuzzyCompare(halfExtentsOld, halfExtents)) {
++ auto geom = QDebugDrawHelper::generateBoxGeometry(halfExtents);
++ geom->setParent(model);
++ model->setGeometry(geom);
++ holder.setHalfExtents(halfExtents);
++ }
++
++ }
++ break;
++
++ case physx::PxGeometryType::eSPHERE: {
++ const auto &sphereGeometry = static_cast<const physx::PxSphereGeometry &>(geometry);
++ const float radius = holder.radius();
++ if (!qFuzzyCompare(sphereGeometry.radius, radius)) {
++ auto geom = QDebugDrawHelper::generateSphereGeometry(sphereGeometry.radius);
++ geom->setParent(model);
++ model->setGeometry(geom);
++ holder.setRadius(sphereGeometry.radius);
++ }
++ }
++ break;
++
++ case physx::PxGeometryType::eCAPSULE: {
++ const auto &capsuleGeometry =
++ static_cast<const physx::PxCapsuleGeometry &>(geometry);
++ const float radius = holder.radius();
++ const float halfHeight = holder.halfHeight();
++
++ if (!qFuzzyCompare(capsuleGeometry.radius, radius)
++ || !qFuzzyCompare(capsuleGeometry.halfHeight, halfHeight)) {
++ auto geom = QDebugDrawHelper::generateCapsuleGeometry(
++ capsuleGeometry.radius, capsuleGeometry.halfHeight);
++ geom->setParent(model);
++ model->setGeometry(geom);
++ holder.setRadius(capsuleGeometry.radius);
++ holder.setHalfHeight(capsuleGeometry.halfHeight);
++ }
++ }
++ break;
++
++ case physx::PxGeometryType::ePLANE:{
++ // Special rotation
++ const QQuaternion rotation =
++ QPhysicsUtils::kMinus90YawRotation * QPhysicsUtils::toQtType(localPose.q);
++ localPose = physx::PxTransform(localPose.p, QPhysicsUtils::toPhysXType(rotation));
++
++ if (model->geometry() == nullptr) {
++ auto geom = QDebugDrawHelper::generatePlaneGeometry();
++ geom->setParent(model);
++ model->setGeometry(geom);
++ }
++ }
++ break;
++
++ // For heightfield, convex mesh and triangle mesh we increase its reference count
++ // to make sure it does not get dereferenced and deleted so that the new mesh will
++ // have another memory address so we know when it has changed.
++ case physx::PxGeometryType::eHEIGHTFIELD: {
++ const auto &heightFieldGeometry =
++ static_cast<const physx::PxHeightFieldGeometry &>(geometry);
++ const float heightScale = holder.heightScale();
++ const float rowScale = holder.rowScale();
++ const float columnScale = holder.columnScale();
++
++ if (auto heightField = holder.getHeightField();
++ heightField && heightField != heightFieldGeometry.heightField) {
++ heightField->release();
++ holder.setHeightField(nullptr);
++ }
++
++ if (!qFuzzyCompare(heightFieldGeometry.heightScale, heightScale)
++ || !qFuzzyCompare(heightFieldGeometry.rowScale, rowScale)
++ || !qFuzzyCompare(heightFieldGeometry.columnScale, columnScale)
++ || !holder.getHeightField()) {
++ if (!holder.getHeightField()) {
++ heightFieldGeometry.heightField->acquireReference();
++ holder.setHeightField(heightFieldGeometry.heightField);
++ }
++ auto geom = QDebugDrawHelper::generateHeightFieldGeometry(
++ heightFieldGeometry.heightField, heightFieldGeometry.heightScale,
++ heightFieldGeometry.rowScale, heightFieldGeometry.columnScale);
++ geom->setParent(model);
++ model->setGeometry(geom);
++ holder.setHeightScale(heightFieldGeometry.heightScale);
++ holder.setRowScale(heightFieldGeometry.rowScale);
++ holder.setColumnScale(heightFieldGeometry.columnScale);
++ }
++ }
++ break;
++
++ case physx::PxGeometryType::eCONVEXMESH: {
++ const auto &convexMeshGeometry =
++ static_cast<const physx::PxConvexMeshGeometry &>(geometry);
++ const auto rotation = convexMeshGeometry.scale.rotation * localPose.q;
++ localPose = physx::PxTransform(localPose.p, rotation);
++ model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry.scale.scale));
++
++ if (auto convexMesh = holder.getConvexMesh();
++ convexMesh && convexMesh != convexMeshGeometry.convexMesh) {
++ convexMesh->release();
++ holder.setConvexMesh(nullptr);
++ }
++
++ if (!model->geometry() || !holder.getConvexMesh()) {
++ if (!holder.getConvexMesh()) {
++ convexMeshGeometry.convexMesh->acquireReference();
++ holder.setConvexMesh(convexMeshGeometry.convexMesh);
++ }
++ auto geom = QDebugDrawHelper::generateConvexMeshGeometry(
++ convexMeshGeometry.convexMesh);
++ geom->setParent(model);
++ model->setGeometry(geom);
++ }
++ }
++ break;
++
++ case physx::PxGeometryType::eTRIANGLEMESH: {
++ const auto &triangleMeshGeometry =
++ static_cast<const physx::PxTriangleMeshGeometry &>(geometry);
++ const auto rotation = triangleMeshGeometry.scale.rotation * localPose.q;
++ localPose = physx::PxTransform(localPose.p, rotation);
++ model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry.scale.scale));
++
++ if (auto triangleMesh = holder.getTriangleMesh();
++ triangleMesh && triangleMesh != triangleMeshGeometry.triangleMesh) {
++ triangleMesh->release();
++ holder.setTriangleMesh(nullptr);
++ }
++
++ if (!model->geometry() || !holder.getTriangleMesh()) {
++ if (!holder.getTriangleMesh()) {
++ triangleMeshGeometry.triangleMesh->acquireReference();
++ holder.setTriangleMesh(triangleMeshGeometry.triangleMesh);
++ }
++ auto geom = QDebugDrawHelper::generateTriangleMeshGeometry(
++ triangleMeshGeometry.triangleMesh);
++ geom->setParent(model);
++ model->setGeometry(geom);
++ }
++ }
++ break;
++
++ // Geometry types that no shape in Qt Quick 3D Physics can have
++ case physx::PxGeometryType::eCONVEXCORE:
++ case physx::PxGeometryType::ePARTICLESYSTEM:
++ case physx::PxGeometryType::eTETRAHEDRONMESH:
++ case physx::PxGeometryType::eCUSTOM:
++ case physx::PxGeometryType::eINVALID:
++ case physx::PxGeometryType::eGEOMETRY_COUNT:
++ // should not happen
++ Q_UNREACHABLE();
++ }
++
++ auto globalPose = node->getGlobalPose();
++ auto finalPose = globalPose.transform(localPose);
++
++ model->setRotation(QPhysicsUtils::toQtType(finalPose.q));
++ model->setPosition(QPhysicsUtils::toQtType(finalPose.p));
++ }
++ }
++
++ // Remove old collision shapes
++ m_collisionShapeDebugModels.removeIf(
++ [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>,
++ DebugModelHolder>::iterator it) {
++ if (!currentCollisionShapes.contains(it.key())) {
++ auto holder = it.value();
++ holder.releaseMeshPointer();
++ if (holder.model)
++ delete holder.model;
++ return true;
++ }
++ return false;
++ });
++ }
++
++ static void collectPhysicsNodes(QQuick3DObject *node, QList<QAbstractPhysicsNode *> &nodes)
++ {
++ if (auto shape = qobject_cast<QAbstractPhysicsNode *>(node)) {
++ nodes.push_back(shape);
++ return;
++ }
++
++ auto childItems = node->childItems();
++ for (QQuick3DObject *child : std::as_const(childItems))
++ collectPhysicsNodes(child, nodes);
++ }
++
++ void QPhysicsWorld::updateDebugDrawDesignStudio()
++ {
++ // Use scene node if no viewport has been specified
++ auto sceneNode = m_viewport ? m_viewport : m_scene;
++
++ if (sceneNode == nullptr)
++ return;
++
++ setupDebugMaterials(sceneNode);
++
++ // Store the collision shapes we have now so we can clear out the removed ones
++ QSet<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>> currentCollisionShapes;
++ currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
++
++ QList<QAbstractPhysicsNode *> activePhysicsNodes;
++ activePhysicsNodes.reserve(m_collisionShapeDebugModels.size());
++ collectPhysicsNodes(m_scene, activePhysicsNodes);
++
++ for (QAbstractPhysicsNode *node : std::as_const(activePhysicsNodes)) {
++
++ const auto &collisionShapes = node->getCollisionShapesList();
++ const int materialIdx = 0; // Just take first material
++ const int length = collisionShapes.length();
++
++ const bool isCharacterController = qobject_cast<QCharacterController *>(node) != nullptr;
++
++ for (int idx = 0; idx < length; idx++) {
++ QAbstractCollisionShape *collisionShape = collisionShapes[idx];
++ DebugModelHolder &holder =
++ m_DesignStudioDebugModels[std::make_pair(collisionShape, node)];
++ auto &model = holder.model;
++
++ currentCollisionShapes.insert(std::make_pair(collisionShape, node));
++
++ m_hasIndividualDebugDraw =
++ m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
++
++ // Create/Update debug view infrastructure
++ {
++ // Hack: we have to delete the model every frame so it shows up in QDS
++ // whenever the code is updated, not sure why ¯\_(?)_/¯
++ delete model;
++ model = new QQuick3DModel();
++ model->setParentItem(sceneNode);
++ model->setParent(sceneNode);
++ model->setCastsShadows(false);
++ model->setReceivesShadows(false);
++ model->setCastsReflections(false);
++ }
++
++ const bool hasGeometry = holder.geometry != nullptr;
++ QVector3D scenePosition = collisionShape->scenePosition();
++ QQuaternion sceneRotation = collisionShape->sceneRotation();
++ QQuick3DGeometry *newGeometry = nullptr;
++
++ if (isCharacterController)
++ sceneRotation = sceneRotation * QQuaternion::fromEulerAngles(QVector3D(0, 0, 90));
++
++ { // update or set material
++ auto material = m_debugMaterials[materialIdx];
++ QQmlListReference materialsRef(model, "materials");
++ if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
++ materialsRef.clear();
++ materialsRef.append(material);
++ }
++ }
++
++ if (auto shape = qobject_cast<QBoxShape *>(collisionShape)) {
++ const auto &halfExtentsOld = holder.halfExtents();
++ const auto halfExtents = shape->sceneScale() * shape->extents() * 0.5f;
++ if (!qFuzzyCompare(halfExtentsOld, halfExtents) || !hasGeometry) {
++ newGeometry = QDebugDrawHelper::generateBoxGeometry(halfExtents);
++ holder.setHalfExtents(halfExtents);
++ }
++ } else if (auto shape = qobject_cast<QSphereShape *>(collisionShape)) {
++ const float radiusOld = holder.radius();
++ const float radius = shape->sceneScale().x() * shape->diameter() * 0.5f;
++ if (!qFuzzyCompare(radiusOld, radius) || !hasGeometry) {
++ newGeometry = QDebugDrawHelper::generateSphereGeometry(radius);
++ holder.setRadius(radius);
++ }
++ } else if (auto shape = qobject_cast<QCapsuleShape *>(collisionShape)) {
++ const float radiusOld = holder.radius();
++ const float halfHeightOld = holder.halfHeight();
++ const float radius = shape->sceneScale().y() * shape->diameter() * 0.5f;
++ const float halfHeight = shape->sceneScale().x() * shape->height() * 0.5f;
++
++ if ((!qFuzzyCompare(radiusOld, radius) || !qFuzzyCompare(halfHeightOld, halfHeight))
++ || !hasGeometry) {
++ newGeometry = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
++ holder.setRadius(radius);
++ holder.setHalfHeight(halfHeight);
++ }
++ } else if (qobject_cast<QPlaneShape *>(collisionShape)) {
++ if (!hasGeometry)
++ newGeometry = QDebugDrawHelper::generatePlaneGeometry();
++ } else if (auto shape = qobject_cast<QHeightFieldShape *>(collisionShape)) {
++ physx::PxHeightFieldGeometry *heightFieldGeometry =
++ static_cast<physx::PxHeightFieldGeometry *>(shape->getPhysXGeometry());
++ const float heightScale = holder.heightScale();
++ const float rowScale = holder.rowScale();
++ const float columnScale = holder.columnScale();
++ scenePosition += shape->hfOffset();
++ if (!heightFieldGeometry) {
++ qWarning() << "Could not get height field";
++ } else if (!qFuzzyCompare(heightFieldGeometry->heightScale, heightScale)
++ || !qFuzzyCompare(heightFieldGeometry->rowScale, rowScale)
++ || !qFuzzyCompare(heightFieldGeometry->columnScale, columnScale)
++ || !hasGeometry) {
++ newGeometry = QDebugDrawHelper::generateHeightFieldGeometry(
++ heightFieldGeometry->heightField, heightFieldGeometry->heightScale,
++ heightFieldGeometry->rowScale, heightFieldGeometry->columnScale);
++ holder.setHeightScale(heightFieldGeometry->heightScale);
++ holder.setRowScale(heightFieldGeometry->rowScale);
++ holder.setColumnScale(heightFieldGeometry->columnScale);
++ }
++ } else if (auto shape = qobject_cast<QConvexMeshShape *>(collisionShape)) {
++ auto convexMeshGeometry =
++ static_cast<physx::PxConvexMeshGeometry *>(shape->getPhysXGeometry());
++ if (!convexMeshGeometry) {
++ qWarning() << "Could not get convex mesh";
++ } else {
++ model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry->scale.scale));
++
++ if (!hasGeometry) {
++ newGeometry = QDebugDrawHelper::generateConvexMeshGeometry(
++ convexMeshGeometry->convexMesh);
++ }
++ }
++ } else if (auto shape = qobject_cast<QTriangleMeshShape *>(collisionShape)) {
++ physx::PxTriangleMeshGeometry *triangleMeshGeometry =
++ static_cast<physx::PxTriangleMeshGeometry *>(shape->getPhysXGeometry());
++ if (!triangleMeshGeometry) {
++ qWarning() << "Could not get triangle mesh";
++ } else {
++ model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry->scale.scale));
++
++ if (!hasGeometry) {
++ newGeometry = QDebugDrawHelper::generateTriangleMeshGeometry(
++ triangleMeshGeometry->triangleMesh);
++ }
++ }
++ }
++
++ if (newGeometry) {
++ delete holder.geometry;
++ holder.geometry = newGeometry;
++ }
++
++ model->setGeometry(holder.geometry);
++ model->setVisible(true);
++
++ model->setRotation(sceneRotation);
++ model->setPosition(scenePosition);
++ }
++ }
++
++ // Remove old debug models
++ m_DesignStudioDebugModels.removeIf(
++ [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>,
++ DebugModelHolder>::iterator it) {
++ if (!currentCollisionShapes.contains(it.key())) {
++ auto holder = it.value();
++ holder.releaseMeshPointer();
++ if (holder.model) {
++ delete holder.geometry;
++ delete holder.model;
++ }
++ return true;
++ }
++ return false;
++ });
++ }
++
++ void QPhysicsWorld::disableDebugDraw()
++ {
++ m_hasIndividualDebugDraw = false;
++
++ for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
++ if (!body->frontendNode)
++ continue;
++
++ const auto &collisionShapes = body->frontendNode->getCollisionShapesList();
++ const int length = collisionShapes.length();
++ for (int idx = 0; idx < length; idx++) {
++ const auto collisionShape = collisionShapes[idx];
++ if (collisionShape->enableDebugDraw()) {
++ m_hasIndividualDebugDraw = true;
++ return;
++ }
++ }
++ }
++ }
++
++ void QPhysicsWorld::setEnableCCD(bool enableCCD)
++ {
++ if (m_enableCCD == enableCCD)
++ return;
++
++ if (m_physicsInitialized) {
++ qWarning()
++ << "Warning: Changing 'enableCCD' after physics is initialized will have no effect";
++ return;
++ }
++
++ m_enableCCD = enableCCD;
++ emit enableCCDChanged(m_enableCCD);
++ }
++
++ void QPhysicsWorld::setTypicalLength(float typicalLength)
++ {
++ if (qFuzzyCompare(typicalLength, m_typicalLength))
++ return;
++
++ if (!qIsFinite(typicalLength) || typicalLength <= 0.f) {
++ qWarning() << "Warning: 'typicalLength' value less than zero, ignored";
++ return;
++ }
++
++ if (m_physicsInitialized) {
++ qWarning() << "Warning: Changing 'typicalLength' after physics is initialized will have "
++ "no effect";
++ return;
++ }
++
++ m_typicalLength = typicalLength;
++
++ emit typicalLengthChanged(typicalLength);
++ }
++
++ void QPhysicsWorld::setTypicalSpeed(float typicalSpeed)
++ {
++ if (qFuzzyCompare(typicalSpeed, m_typicalSpeed))
++ return;
++
++ if (!qIsFinite(typicalSpeed) || typicalSpeed <= 0.f) {
++ qWarning() << "Warning: 'typicalSpeed' must be finite and greater than zero, ignored";
++ return;
++ }
++
++ if (m_physicsInitialized) {
++ qWarning() << "Warning: Changing 'typicalSpeed' after physics is initialized will have "
++ "no effect";
++ return;
++ }
++
++ m_typicalSpeed = typicalSpeed;
++
++ emit typicalSpeedChanged(typicalSpeed);
++ }
++
++ float QPhysicsWorld::defaultDensity() const
++ {
++ return m_defaultDensity;
++ }
++
++ float QPhysicsWorld::minimumTimestep() const
++ {
++ return m_minTimestep;
++ }
++
++ float QPhysicsWorld::maximumTimestep() const
++ {
++ return m_maxTimestep;
++ }
++
++ void QPhysicsWorld::setDefaultDensity(float defaultDensity)
++ {
++ defaultDensity = qBound(0.0000001f, defaultDensity, PX_MAX_F32);
++
++ if (qFuzzyCompare(m_defaultDensity, defaultDensity))
++ return;
++ m_defaultDensity = defaultDensity;
++
++ // Go through all dynamic rigid bodies and update the default density
++ for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
++ if (body->frontendNode)
++ body->updateDefaultDensity(m_defaultDensity);
++ }
++
++ emit defaultDensityChanged(defaultDensity);
++ }
++
++ // Remove physics world items that no longer exist
++
++ void QPhysicsWorld::cleanupRemovedNodes()
++ {
++ m_physXBodies.removeIf([this](QAbstractPhysXNode *body) {
++ return body->cleanupIfRemoved(m_physx);
++ });
++ m_removedPhysicsNodes.clear();
++ }
++
++ // Tell the triggers which nodes have had their shapes replaced, and drop what
++ // the reports that followed the last such replacement did not mention
++
++ void QPhysicsWorld::invalidateTriggerOverlaps(QSpan<QAbstractPhysXNode *const> rebuiltBodies)
++ {
++ for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
++ auto *trigger = qobject_cast<QTriggerBody *>(body->frontendNode);
++ if (!trigger)
++ continue;
++ for (QAbstractPhysXNode *rebuilt : rebuiltBodies) {
++ // The rebuilt nodes are held as backends, which outlive the frame,
++ // since syncing the frame can run a handler that deletes a node
++ // that was rebuilt earlier in it. deregisterNode() takes the
++ // frontend off the backend of whatever is deleted.
++ if (rebuilt->frontendNode)
++ trigger->invalidateOverlaps(rebuilt->frontendNode);
++ }
++ }
++
++ m_triggerOverlapsInvalidated = true;
++ }
++
++ void QPhysicsWorld::dropUnreportedTriggerOverlaps()
++ {
++ // Nothing is waiting to be reported again unless shapes were replaced, so
++ // this stays off the frame of a scene that never rebuilds any.
++ if (!m_triggerOverlapsInvalidated)
++ return;
++ m_triggerOverlapsInvalidated = false;
++
++ // A body reported as having left can be deleted from the handler, and so can
++ // a trigger that has not been reached yet. deregisterNode() takes the
++ // frontend off whatever is deleted, so what still has one here is what
++ // is left.
++ for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
++ if (auto *trigger = qobject_cast<QTriggerBody *>(body->frontendNode))
++ trigger->dropUnreportedOverlaps();
++ }
++ }
++
++ void QPhysicsWorld::cleanupRemovedJoints()
++ {
++ for (physx::PxJoint *joint : m_removedJoints) {
++ if (joint)
++ joint->release();
++ }
++ m_removedJoints.clear();
++ }
++
++ void QPhysicsWorld::initPhysics()
++ {
++ Q_ASSERT(!m_physicsInitialized);
++
++ #if QT_CONFIG(thread)
++ const unsigned int numThreads = m_numThreads >= 0 ? m_numThreads : qMax(0, QThread::idealThreadCount());
++ #else
++ // PhysX cannot start worker threads, it would wait for them forever
++ const unsigned int numThreads = 0;
++ #endif
++ m_physx->createScene(m_typicalLength, m_typicalSpeed, m_gravity, this, numThreads);
++ m_frameAnimator->start();
++ m_physicsInitialized = true;
++ }
++
++ void QPhysicsWorld::simulateFrame()
++ {
++ constexpr double MILLIONTH = 0.000001;
++ constexpr double THOUSANDTH = 0.001;
++
++ if (m_inDesignStudio) {
++ frameFinishedDesignStudio();
++ return;
++ }
++
++ if (!m_physx->isRunning) {
++ m_timer.start();
++ m_physx->isRunning = true;
++ const double minTimestepSecs = m_minTimestep * 0.001;
++ m_physx->scene->simulate(minTimestepSecs);
++ m_currTimeStep = minTimestepSecs;
++ return;
++ }
++
++ // Frame not ready yet
++ if (!m_frameFetched && !m_physx->scene->checkResults()) {
++ return;
++ }
++
++ // Frame ready, fetch and finish it
++ if (!m_frameFetched) {
++ m_physx->scene->fetchResults(true);
++ frameFinished(m_currTimeStep);
++ m_frameFetched = true;
++ if (Q_UNLIKELY(!qtPhysicsTimingsFile.isEmpty())) {
++ const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
++ m_frameTimings.append(deltaMS);
++ }
++ }
++
++ // Assuming: 0 <= minTimestep <= maxTimestep
++ const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
++ if (deltaMS < m_minTimestep)
++ return;
++ const double deltaSecs = qMin<double>(deltaMS, m_maxTimestep) * THOUSANDTH;
++ m_timer.restart();
++ m_physx->scene->simulate(deltaSecs);
++ m_frameFetched = false;
++ m_currTimeStep = deltaSecs;
++ }
++
++ void QPhysicsWorld::frameFinished(float deltaTime)
++ {
++ matchOrphanNodes();
++ matchOrphanJoints();
++ matchOrphanRaycasters();
++
++ // One round of reports has been fetched since the shapes rebuilt below were
++ // replaced, so whatever it did not mention is no longer overlapping. Kept
++ // ahead of cleanupRemovedNodes(), like the contact callbacks, since a body
++ // reported as having left can be deleted from the handler.
++ dropUnreportedTriggerOverlaps();
++
++ emitContactCallbacks();
++ cleanupRemovedNodes();
++ cleanupRemovedJoints();
++
++ for (auto *node : std::as_const(m_newPhysicsNodes)) {
++ auto *body = node->createPhysXBackend();
++ body->init(this, m_physx);
++ m_physXBodies.push_back(body);
++ }
++ m_newPhysicsNodes.clear();
++
++ QHash<QQuick3DNode *, QMatrix4x4> transformCache;
++ QVarLengthArray<QAbstractPhysXNode *, 8> rebuiltBodies;
++
++ // TODO: Use dirty flag/dirty list to avoid redoing things that didn't change
++ for (auto *physXBody : std::as_const(m_physXBodies)) {
++ // Syncing runs a node's bindings, and a character controller reports
++ // what it hits from here, so one can be deleted while this loop runs.
++ if (!physXBody->frontendNode)
++ continue;
++
++ physXBody->markDirtyShapes();
++ const bool wasDirty = physXBody->shapesDirty();
++ physXBody->rebuildDirtyShapes(this, m_physx);
++
++ // Rebuilding reports too, when it has to force a body kinematic.
++ if (!physXBody->frontendNode)
++ continue;
++
++ // Dirty before and clean after is the backend having replaced the shapes
++ // of the node, which the triggers holding them need to hear about.
++ if (wasDirty && !physXBody->shapesDirty())
++ rebuiltBodies.append(physXBody);
++ physXBody->updateFilters();
++
++ // Sync the physics world and the scene
++ physXBody->sync(deltaTime, transformCache);
++ }
++
++ if (!rebuiltBodies.isEmpty())
++ invalidateTriggerOverlaps(rebuiltBodies);
++
++ for (QPhysicsJoint *joint : std::as_const(m_joints)) {
++ joint->updatePhysXBackend();
++ }
++
++ const QList<QRaycaster *> raycasters = m_raycasters;
++ for (QRaycaster *raycaster : raycasters) {
++ if (m_raycasters.contains(raycaster))
++ raycaster->updateRaycast(this);
++ }
++
++ updateDebugDraw();
++ emit frameDone(deltaTime * 1000);
++ }
++
++ void QPhysicsWorld::frameFinishedDesignStudio()
++ {
++ // Note sure if this is needed but do it anyway
++ matchOrphanNodes();
++ matchOrphanJoints();
++ // Matched but never cast: no backends are created here, so the PhysX scene has no
++ // bodies for a ray to hit.
++ matchOrphanRaycasters();
++ emitContactCallbacks();
++ cleanupRemovedNodes();
++ // Ignore new physics nodes, we find them from the scene node anyway
++ m_newPhysicsNodes.clear();
++
++ updateDebugDrawDesignStudio();
++ }
++
++ QPhysicsWorld *QPhysicsWorld::getWorld(QQuick3DNode *node)
++ {
++ for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
++ if (!world->m_scene) {
++ continue;
++ }
++
++ QQuick3DNode *nodeCurr = node;
++
++ // Maybe pointless but check starting node
++ if (nodeCurr == world->m_scene)
++ return world;
++
++ while (nodeCurr->parentNode()) {
++ nodeCurr = nodeCurr->parentNode();
++ if (nodeCurr == world->m_scene)
++ return world;
++ }
++ }
++
++ return nullptr;
++ }
++
++ QPhysicsWorld *QPhysicsWorld::getWorld(QPhysicsJoint *joint)
++ {
++ for (QPhysicsWorld *world : worldManager.worlds) {
++ if (!world->m_scene) {
++ continue;
++ }
++
++ QObject *nodeCurr = joint;
++
++ while (nodeCurr->parent()) {
++ nodeCurr = nodeCurr->parent();
++ if (nodeCurr == world->m_scene) {
++ return world;
++ } else if (auto view3d = qobject_cast<QQuick3DViewport *>(nodeCurr);
++ view3d && view3d->scene() == world->m_scene) {
++ // HACK? if the parent is a view3d, check against its "implicit" scene
++ return world;
++ }
++ }
++ }
++
++ return nullptr;
++ }
++
++ void QPhysicsWorld::matchOrphanNodes()
++ {
++ // FIXME: does this need thread safety?
++ if (worldManager.orphanNodes.isEmpty())
++ return;
++
++ qsizetype numNodes = worldManager.orphanNodes.length();
++ qsizetype idx = 0;
++
++ while (idx < numNodes) {
++ auto node = worldManager.orphanNodes[idx];
++ auto world = getWorld(node);
++ if (world == this) {
++ world->m_newPhysicsNodes.push_back(node);
++ // swap-erase
++ worldManager.orphanNodes.swapItemsAt(idx, numNodes - 1);
++ worldManager.orphanNodes.pop_back();
++ numNodes--;
++ } else {
++ idx++;
++ }
++ }
++ }
++
++ void QPhysicsWorld::matchOrphanJoints()
++ {
++ if (worldManager.orphanJoints.isEmpty())
++ return;
++
++ qsizetype numNodes = worldManager.orphanJoints.length();
++ qsizetype idx = 0;
++
++ while (idx < numNodes) {
++ auto node = worldManager.orphanJoints[idx];
++ auto world = getWorld(node);
++ if (world == this) {
++ world->m_joints.push_back(node);
++ // swap-erase
++ worldManager.orphanJoints.swapItemsAt(idx, numNodes - 1);
++ worldManager.orphanJoints.pop_back();
++ numNodes--;
++ } else {
++ idx++;
++ }
++ }
++ }
++
++ void QPhysicsWorld::matchOrphanRaycasters()
++ {
++ qsizetype idx = 0;
++ while (idx < worldManager.orphanRaycasters.size()) {
++ QRaycaster *raycaster = worldManager.orphanRaycasters.at(idx);
++ if (getWorld(raycaster) == this) {
++ m_raycasters.push_back(raycaster);
++ worldManager.orphanRaycasters.removeAt(idx);
++ } else {
++ ++idx;
++ }
++ }
++ }
++
++ void QPhysicsWorld::findPhysicsNodes()
++ {
++ // This method finds the physics nodes inside the scene pointed to by the
++ // scene property. This method is necessary to run whenever the scene
++ // property is changed.
++ if (m_scene == nullptr)
++ return;
++
++ // Recursively go through all children and add all QAbstractPhysicsNode's
++ QList<QQuick3DObject *> children = m_scene->childItems();
++ while (!children.empty()) {
++ auto child = children.takeFirst();
++ if (auto converted = qobject_cast<QAbstractPhysicsNode *>(child); converted != nullptr) {
++ // This should never happen but check anyway.
++ if (converted->m_backendObject != nullptr) {
++ qWarning() << "Warning: physics node already associated with a backend node.";
++ continue;
++ }
++
++ m_newPhysicsNodes.push_back(converted);
++ worldManager.orphanNodes.removeAll(converted); // No longer orphan
++ }
++ children.append(child->childItems());
++ }
++ }
++
++ void QPhysicsWorld::emitContactCallbacks()
++ {
++ for (const QPhysicsWorld::BodyContact &contact : std::as_const(m_registeredContacts)) {
++ if (m_removedPhysicsNodes.contains(contact.sender)
++ || m_removedPhysicsNodes.contains(contact.receiver))
++ continue;
++ contact.receiver->registerContact(contact.sender, contact.positions, contact.impulses,
++ contact.normals);
++ }
++
++ m_registeredContacts.clear();
++ }
++
++ physx::PxPhysics *QPhysicsWorld::getPhysics()
++ {
++ return StaticPhysXObjects::getReference().physics;
++ }
++
++ const physx::PxCookingParams *QPhysicsWorld::getCookingParams()
++ {
++ // PhysX cooks meshes through free functions taking the parameters, instead of through a
++ // cooking object, but they still need the foundation for allocation and error reporting,
++ // so there is nothing to cook with until it has been created.
++ if (!StaticPhysXObjects::getReference().foundationCreated)
++ return nullptr;
++
++ // FIXME: does the tolerance matter?
++ static const physx::PxCookingParams params { physx::PxTolerancesScale() };
++ return &params;
++ }
++
++ physx::PxControllerManager *QPhysicsWorld::controllerManager()
++ {
++ if (m_physx->scene && !m_physx->controllerManager) {
++ m_physx->controllerManager = PxCreateControllerManager(*m_physx->scene);
++ qCDebug(lcQuick3dPhysics) << "Created controller manager" << m_physx->controllerManager;
++ }
++ return m_physx->controllerManager;
++ }
++
++ QQuick3DNode *QPhysicsWorld::scene() const
++ {
++ return m_scene;
++ }
++
++ void QPhysicsWorld::setScene(QQuick3DNode *newScene)
++ {
++ if (m_scene == newScene)
++ return;
++
++ m_scene = newScene;
++
++ // Delete all nodes since they are associated with the previous scene. One
++ // deleted from a report earlier in this frame has nothing left to
++ // deregister.
++ for (auto body : std::as_const(m_physXBodies)) {
++ if (body->frontendNode)
++ deregisterNode(body->frontendNode);
++ }
++
++ // Raycasters are not physics nodes and have no backend to tear down, so hand them back
++ // to be matched against the new scene below, without a hit from the old one. Clearing
++ // reports to QML, where a handler can delete a raycaster, hence the check.
++ const QList<QRaycaster *> raycasters = std::exchange(m_raycasters, {});
++ worldManager.orphanRaycasters += raycasters;
++ for (QRaycaster *raycaster : raycasters) {
++ if (worldManager.orphanRaycasters.contains(raycaster))
++ raycaster->clearHit();
++ }
++
++ // Check if scene is already used by another world
++ bool sceneOK = true;
++ for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
++ if (world != this && world->scene() == newScene) {
++ sceneOK = false;
++ qWarning() << "Warning: scene already associated with physics world";
++ }
++ }
++
++ if (sceneOK) {
++ findPhysicsNodes();
++ matchOrphanRaycasters();
++ }
++ emit sceneChanged();
++ }
++
++ int QPhysicsWorld::numThreads() const
++ {
++ return m_numThreads;
++ }
++
++ void QPhysicsWorld::setNumThreads(int newNumThreads)
++ {
++ newNumThreads = qMin(newNumThreads, 256);
++
++ if (m_numThreads == newNumThreads)
++ return;
++
++ m_numThreads = newNumThreads;
++ emit numThreadsChanged();
++ }
++
++ bool QPhysicsWorld::reportKinematicKinematicCollisions() const
++ {
++ return m_reportKinematicKinematicCollisions;
++ }
++
++ void QPhysicsWorld::setReportKinematicKinematicCollisions(
++ bool newReportKinematicKinematicCollisions)
++ {
++ if (m_reportKinematicKinematicCollisions == newReportKinematicKinematicCollisions)
++ return;
++ m_reportKinematicKinematicCollisions = newReportKinematicKinematicCollisions;
++ emit reportKinematicKinematicCollisionsChanged();
++ }
++
++ bool QPhysicsWorld::reportStaticKinematicCollisions() const
++ {
++ return m_reportStaticKinematicCollisions;
++ }
++
++ void QPhysicsWorld::setReportStaticKinematicCollisions(bool newReportStaticKinematicCollisions)
++ {
++ if (m_reportStaticKinematicCollisions == newReportStaticKinematicCollisions)
++ return;
++ m_reportStaticKinematicCollisions = newReportStaticKinematicCollisions;
++ emit reportStaticKinematicCollisionsChanged();
++ }
++
++ QPhysicsWorld::QueryStructure QPhysicsWorld::staticQueryStructure() const
++ {
++ return m_staticQueryStructure;
++ }
++
++ void QPhysicsWorld::setStaticQueryStructure(QueryStructure newStaticQueryStructure)
++ {
++ if (m_staticQueryStructure == newStaticQueryStructure)
++ return;
++
++ if (newStaticQueryStructure == QueryStructure::NoStructure) {
++ qWarning()
++ << "Warning: The QueryStructure::NoStructure is not supported for staticQueryStructure. Available options: [StaticTree, DynamicTree]";
++ return;
++ }
++
++ if (m_physicsInitialized) {
++ qWarning()
++ << "Warning: Changing 'staticQueryStructure' after physics is initialized will have no effect";
++ return;
++ }
++
++ m_staticQueryStructure = newStaticQueryStructure;
++ emit staticQueryStructureChanged();
++ }
++
++ QPhysicsWorld::QueryStructure QPhysicsWorld::dynamicQueryStructure() const
++ {
++ return m_dynamicQueryStructure;
++ }
++
++ void QPhysicsWorld::setDynamicQueryStructure(QueryStructure newDynamicQueryStructure)
++ {
++ if (m_dynamicQueryStructure == newDynamicQueryStructure)
++ return;
++
++ if (m_physicsInitialized) {
++ qWarning()
++ << "Warning: Changing 'dynamicQueryStructure' after physics is initialized will have no effect";
++ return;
++ }
++
++ m_dynamicQueryStructure = newDynamicQueryStructure;
++ emit dynamicQueryStructureChanged();
++ }
++
++ bool QPhysicsWorld::testRaycastQuery(const QVector3D &origin,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ physx::PxRaycastBuffer hitBuffer;
++ return raycastImpl(m_physx->scene, origin, direction,
++ maxDistance, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxHitFlags(),
++ physx::PxQueryFlag::eANY_HIT);
++ }
++
++ QQuick3DPhysicsLocationHit QPhysicsWorld::singleRaycastQuery(const QVector3D &origin,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ physx::PxRaycastBuffer hitBuffer;
++ const bool status = raycastImpl(m_physx->scene, origin, direction,
++ maxDistance, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxHitFlag::eDEFAULT);
++
++ if (status && hitBuffer.hasBlock) {
++ return QQuick3DPhysicsLocationHit(hitBuffer.block,
++ hitBuffer.block);
++ }
++
++ return {};
++ }
++
++ QList<QQuick3DPhysicsLocationHit> QPhysicsWorld::multiRaycastQuery(const QVector3D &origin,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ DynamicQueryCallback<physx::PxRaycastCallback,
++ QQuick3DPhysicsLocationHit,
++ physx::PxRaycastHit> hitBuffer;
++
++ if (raycastImpl(m_physx->scene, origin, direction,
++ maxDistance, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxHitFlag::eDEFAULT,
++ physx::PxQueryFlag::eNO_BLOCK)) {
++
++ return hitBuffer.hits;
++ }
++
++ return {};
++ }
++
++ QQuick3DPhysicsLocationHit
++ QPhysicsWorld::closestRaycastHit(const QVector3D &origin, const QVector3D &direction,
++ float maxDistance, bool includeStatic, bool includeDynamic,
++ QSpan<const QAbstractPhysicsNode *const> excludedBodies) const
++ {
++ ExcludedBodiesFilter filter(excludedBodies);
++ physx::PxRaycastBuffer hitBuffer;
++ const bool status = raycastImpl(m_physx->scene, origin, direction, maxDistance, includeStatic,
++ includeDynamic, hitBuffer, physx::PxHitFlag::eDEFAULT,
++ physx::PxQueryFlag::ePREFILTER, &filter);
++
++ if (status && hitBuffer.hasBlock)
++ return QQuick3DPhysicsLocationHit(hitBuffer.block, hitBuffer.block);
++
++ return {};
++ }
++
++ bool QPhysicsWorld::testSweepQuery(QAbstractCollisionShape *shape,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ physx::PxSweepBuffer hitBuffer;
++ return sweepImpl(m_physx->scene, shape, direction,
++ maxDistance, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxHitFlags(),
++ physx::PxQueryFlag::eANY_HIT);
++ }
++
++ QQuick3DPhysicsLocationHit QPhysicsWorld::singleSweepQuery(QAbstractCollisionShape *shape,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ physx::PxSweepBuffer hitBuffer;
++ const bool status = sweepImpl(m_physx->scene, shape,
++ direction, maxDistance, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxHitFlag::eDEFAULT | physx::PxHitFlag::eMTD);
++
++ if (status && hitBuffer.hasBlock) {
++ return QQuick3DPhysicsLocationHit(hitBuffer.block, hitBuffer.block);
++ }
++
++ return {};
++ }
++
++ QList<QQuick3DPhysicsLocationHit> QPhysicsWorld::multiSweepQuery(QAbstractCollisionShape *shape,
++ const QVector3D &direction,
++ float maxDistance,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ DynamicQueryCallback<physx::PxSweepCallback,
++ QQuick3DPhysicsLocationHit,
++ physx::PxSweepHit> hitBuffer;
++
++ if (sweepImpl(m_physx->scene, shape,
++ direction, maxDistance, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxHitFlag::eDEFAULT | physx::PxHitFlag::eMTD,
++ physx::PxQueryFlag::eNO_BLOCK)) {
++ return hitBuffer.hits;
++ }
++
++ return {};
++ }
++
++ bool QPhysicsWorld::testOverlapQuery(QAbstractCollisionShape *shape,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ physx::PxOverlapBuffer hitBuffer;
++ return overlapImpl(m_physx->scene,
++ shape, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxQueryFlag::eANY_HIT);
++ }
++
++ QList<QQuick3DPhysicsQueryHit> QPhysicsWorld::multiOverlapQuery(QAbstractCollisionShape *shape,
++ bool includeStatic,
++ bool includeDynamic) const
++ {
++ DynamicQueryCallback<physx::PxOverlapCallback,
++ QQuick3DPhysicsQueryHit,
++ physx::PxOverlapHit> hitBuffer;
++
++ if (overlapImpl(m_physx->scene,
++ shape, includeStatic, includeDynamic,
++ hitBuffer,
++ physx::PxQueryFlag::eNO_BLOCK)) {
++ return hitBuffer.hits;
++ }
++
++ return {};
++ }
++
++ QT_END_NAMESPACE
++
++ #include "qphysicsworld.moc"