Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qphysicsworld.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include "physxnode/qabstractphysxnode_p.h"
8#include "physxnode/qphysxworld_p.h"
12#include "joints/qjoint_p.h"
14#include "qboxshape_p.h"
15#include "qsphereshape_p.h"
20#include "qplaneshape_p.h"
21#include "qraycaster_p.h"
23#include "qtriggerbody_p.h"
24
25#include "PxPhysicsAPI.h"
26#include "cooking/PxCooking.h"
27#include <foundation/PxSimpleTypes.h>
28
29#include <QtQuick/private/qquickframeanimation_p.h>
30#include <QtQuick3D/private/qquick3dobject_p.h>
31#include <QtQuick3D/private/qquick3dnode_p.h>
32#include <QtQuick3D/private/qquick3dmodel_p.h>
33#include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
34#include <QtQuick3DUtils/private/qssgutils_p.h>
35
36#include <QtCore/qvarlengtharray.h>
37#include <QtGui/qquaternion.h>
38
39#include <QtEnvironmentVariables>
40
42
43/*!
44 \qmltype PhysicsWorld
45 \inqmlmodule QtQuick3D.Physics
46 \since 6.4
47 \brief Controls the physics simulation.
48
49 The PhysicsWorld type controls the physics simulation. This node is used to create an instance of the physics world as well
50 as define its properties. There can only be one physics world. All collision nodes in the qml
51 will get added automatically to the physics world.
52*/
53
54/*!
55 \qmlproperty vector3d PhysicsWorld::gravity
56 This property defines the gravity vector of the physics world.
57 The default value is \c (0, -981, 0). Set the value to \c{Qt.vector3d(0, -9.81, 0)} if your
58 unit of measurement is meters and you are simulating Earth gravity.
59*/
60
61/*!
62 \qmlproperty bool PhysicsWorld::running
63 This property starts or stops the physical simulation. The default value is \c true.
64*/
65
66/*!
67 \qmlproperty bool PhysicsWorld::forceDebugDraw
68 This property enables debug drawing of all active shapes in the physics world. The default value
69 is \c false.
70*/
71
72/*!
73 \qmlproperty bool PhysicsWorld::enableCCD
74 \deprecated [6.13] Use DynamicRigidBody::ccd instead.
75
76 This property enables continuous collision detection globally for every dynamic body
77 in the scene that does not explicitly set its own \l DynamicRigidBody::ccd mode.
78 It reduces the risk of fast-moving bodies passing through geometry at high velocities
79 (also known as tunnelling).
80
81 For non-kinematic dynamic bodies, enabling this property uses sweep-based CCD.
82 For kinematic bodies, it automatically uses speculative CCD, as sweep-based CCD
83 is not supported for them.
84
85 Continuous collision detection can also be configured per body via
86 \l DynamicRigidBody::ccd, which avoids paying its performance cost for
87 objects that do not need it.
88
89 \warning Using trigger bodies with CCD enabled is not supported and can
90 result in missing or false trigger reports.
91
92 \default false
93
94 \sa DynamicRigidBody::ccd
95*/
96
97/*!
98 \qmlproperty real PhysicsWorld::typicalLength
99 This property defines the approximate size of objects in the simulation. This is used to
100 estimate certain length-related tolerances. Objects much smaller or much larger than this
101 size may not behave properly. The default value is \c 100.
102
103 Range: \c{[0, inf]}
104*/
105
106/*!
107 \qmlproperty real PhysicsWorld::typicalSpeed
108 This property defines the typical magnitude of velocities of objects in simulation. This is used
109 to estimate whether a contact should be treated as bouncing or resting based on its impact
110 velocity, and a kinetic energy threshold below which the simulation may put objects to sleep.
111
112 For normal physical environments, a good choice is the approximate speed of an object falling
113 under gravity for one second. The default value is \c 1000.
114
115 Range: \c{[0, inf]}
116*/
117
118/*!
119 \qmlproperty real PhysicsWorld::defaultDensity
120 This property defines the default density of dynamic objects, measured in kilograms per cubic
121 unit. This is equal to the weight of a cube with side \c 1.
122
123 The default value is \c 0.001, corresponding to 1 g/cm³: the density of water. If your unit of
124 measurement is meters, a good value would be \c 1000. Note that only positive values are
125 allowed.
126
127 Range: \c{(0, inf]}
128*/
129
130/*!
131 \qmlproperty Node PhysicsWorld::viewport
132 This property defines the viewport where debug components will be drawn if \l{forceDebugDraw}
133 is enabled. If unset the \l{scene} node will be used.
134
135 \sa forceDebugDraw, scene
136*/
137
138/*!
139 \qmlproperty real PhysicsWorld::minimumTimestep
140 This property defines the minimum simulation timestep in milliseconds. The default value is
141 \c{1}.
142
143 Range: \c{[0, maximumTimestep]}
144
145 \note The simulation timestep works in lockstep with the rendering,
146 meaning a new simulation frame will only be started after a rendered frame
147 has completed. This means that at most one simulation frame will run per
148 rendered frame.
149*/
150
151/*!
152 \qmlproperty real PhysicsWorld::maximumTimestep
153 This property defines the maximum simulation timestep in milliseconds. The default value is
154 \c{33.333}.
155
156 Range: \c{[0, inf]}
157
158 \note The simulation timestep works in lockstep with the rendering,
159 meaning a new simulation frame will only be started after a rendered frame
160 has completed. This means that at most one simulation frame will run per
161 rendered frame.
162*/
163
164/*!
165 \qmlproperty Node PhysicsWorld::scene
166
167 This property defines the top-most Node that contains all the nodes of the physical
168 simulation. All physics objects that are an ancestor of this node will be seen as part of this
169 PhysicsWorld.
170
171 \note Using the same scene node for several PhysicsWorld is unsupported.
172*/
173
174/*!
175 \qmlsignal PhysicsWorld::frameDone(float timestep)
176 \since 6.5
177
178 This signal is emitted when the physical simulation is done simulating a frame. The \a timestep
179 parameter is how long in milliseconds the timestep was in the simulation.
180*/
181
182/*!
183 \qmlproperty int PhysicsWorld::numThreads
184 \since 6.7
185
186 This property defines the number of threads used for the physical simulation. This is how the
187 range of values are interpreted:
188
189 \table
190 \header
191 \li Value
192 \li Range
193 \li Description
194 \row
195 \li Negative
196 \li \c{[-inf, -1]}
197 \li Automatic thread count. The application will try to query the number of threads from the
198 system.
199 \row
200 \li Zero
201 \li \c{{0}}
202 \li No threading, simulation will run sequentially.
203 \row
204 \li Positive
205 \li \c{[1, 256]}
206 \li Specific thread count. Values above 256 are clamped.
207 \endtable
208
209 The default value is \c{-1}, meaning automatic thread count.
210
211 \note Once the scene has started running it is not possible to change the number of threads.
212*/
213
214/*!
215 \qmlproperty bool PhysicsWorld::reportKinematicKinematicCollisions
216 \since 6.7
217
218 This property controls if collisions between pairs of \e{kinematic} dynamic rigid bodies will
219 trigger a contact report.
220
221 The default value is \c{false}.
222
223 \note Once the scene has started running it is not possible to change this setting.
224 \sa PhysicsWorld::reportStaticKinematicCollisions
225 \sa DynamicRigidBody
226 \sa PhysicsNode::bodyContact
227*/
228
229/*!
230 \qmlproperty bool PhysicsWorld::reportStaticKinematicCollisions
231 \since 6.7
232
233 This property controls if collisions between a static rigid body and a \e{kinematic} dynamic
234 rigid body will trigger a contact report.
235
236 The default value is \c{false}.
237
238 \note Once the scene has started running it is not possible to change this setting.
239 \sa PhysicsWorld::reportKinematicKinematicCollisions
240 \sa StaticRigidBody
241 \sa DynamicRigidBody
242 \sa PhysicsNode::bodyContact
243*/
244
245/*!
246 \qmlproperty QueryStructure PhysicsWorld::staticQueryStructure
247 \since 6.13
248
249 The spatial pruning structure type used to accelerate scene queries
250 (raycasts, sweeps, overlaps - used in CharacterController and a \e{kinematic} dynamic rigid body)
251 against static actors in the physics scene. It has no effect on rigid-body
252 contact/collision detection during simulation.
253
254 The following values are available:
255
256 \value PhysicsWorld.StaticTree
257 Uses a pre-baked static AABB tree. Offers maximum scene query performance with zero per-frame management overhead.
258 Best for fully static, immutable scenes. Inserting or removing objects at runtime causes heavy scene rebuilds and frame spikes.
259 \value PhysicsWorld.DynamicTree
260 Uses a dynamic self-balancing AABB tree. Allows fast, local runtime insertion (logarithmic time) and removal of static objects without freezing the frame.
261 Ideal for seamless open-world streaming where static chunks are loaded dynamically.
262
263 \default PhysicsWorld.DynamicTree
264
265 \note Once the scene has started running it is not possible to change this setting.
266 \note PhysicsWorld.NoStructure is not supported for staticQueryStructure
267
268 \sa PhysicsWorld::dynamicQueryStructure
269
270*/
271
272/*!
273 \qmlproperty QueryStructure PhysicsWorld::dynamicQueryStructure
274 \since 6.13
275
276 The spatial pruning structure type used to accelerate scene queries
277 (raycasts, sweeps, overlaps - used in CharacterController and a \e{kinematic} dynamic rigid body)
278 against dynamic actors in the physics scene. It has no effect on rigid-body
279 contact/collision detection during simulation.
280
281 The following values are available:
282
283 \value PhysicsWorld.NoStructure
284 Disables the scene query acceleration structure for dynamic actors.
285 Eliminates CPU overhead for tree maintenance when objects move, spawn, or are destroyed.
286 Scene queries will fall back to a linear search. Ideal when scene queries are not needed for dynamic objects.
287 \value PhysicsWorld.StaticTree
288 Uses a static AABB tree. Offers faster scene queries for dynamic actors, but updating the tree when objects move or spawn is very expensive.
289 Best when dynamic actors rarely move or spend most of their time sleeping.
290 \value PhysicsWorld.DynamicTree
291 Uses a dynamic self-balancing AABB tree. Allows fast, local runtime insertion (logarithmic time), movement, and removal of dynamic objects without freezing the frame.
292 Ideal for active scenes with frequently moving or spawning dynamic actors.
293
294 \default PhysicsWorld.DynamicTree
295
296 \note Once the scene has started running it is not possible to change this setting.
297 \sa PhysicsWorld::staticQueryStructure
298*/
299
300/*!
301 \qmlmethod bool PhysicsWorld::testRaycastQuery(vector3d origin, vector3d direction,
302 real maxDistance,
303 bool includeStatic = true,
304 bool includeDynamic = true)
305 \since 6.13
306
307 Performs a fast occlusion check along a ray without computing precise hit
308 geometry or returning hit data.
309
310 Returns \c true if the ray hits a body within \a maxDistance; otherwise returns
311 \c false. The query stops at the first body it finds, which makes it the cheapest
312 of the raycast queries.
313
314 \list
315 \li \a origin is the starting position of the ray, in world space.
316 \li \a direction is the direction of the ray. It must not be a null vector and does
317 not need to be normalized; the query normalizes it.
318 \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
319 must be greater than 0.
320 \li \a includeStatic includes static bodies in the query when \c true.
321 \li \a includeDynamic includes dynamic bodies in the query when \c true.
322 \endlist
323
324 \include qtquick3dphysics-queries.qdocinc include_flags
325
326 \include qtquick3dphysics-queries.qdocinc query_exclusions
327
328 \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
329
330 \sa singleRaycastQuery, multiRaycastQuery, {Qt Quick 3D Physics Scene Queries}
331*/
332
333/*!
334 \qmlmethod locationHit PhysicsWorld::singleRaycastQuery(vector3d origin,
335 vector3d direction,
336 real maxDistance,
337 bool includeStatic = true,
338 bool includeDynamic = true)
339 \since 6.13
340
341 Casts a ray through the physics scene and returns the closest body it hits.
342
343 The ray starts at \a origin and extends along \a direction up to \a maxDistance.
344 Returns a \l locationHit describing the closest intersection.
345
346 \list
347 \li \a origin is the starting position of the ray, in world space.
348 \li \a direction is the direction of the ray. It must not be a null vector and does
349 not need to be normalized; the query normalizes it.
350 \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
351 must be greater than 0.
352 \li \a includeStatic includes static bodies in the query when \c true.
353 \li \a includeDynamic includes dynamic bodies in the query when \c true.
354 \endlist
355
356 \include qtquick3dphysics-queries.qdocinc include_flags
357
358 \include qtquick3dphysics-queries.qdocinc hit_miss_return
359
360 \include qtquick3dphysics-queries.qdocinc query_exclusions
361
362 \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
363
364 \sa testRaycastQuery, multiRaycastQuery, {Qt Quick 3D Physics Scene Queries}
365*/
366
367/*!
368 \qmlmethod list<locationHit> PhysicsWorld::multiRaycastQuery(vector3d origin,
369 vector3d direction,
370 real maxDistance,
371 bool includeStatic = true,
372 bool includeDynamic = true)
373 \since 6.13
374
375 Casts a ray through the physics scene and returns every body it passes through,
376 rather than stopping at the closest one.
377
378 The ray starts at \a origin and extends along \a direction up to \a maxDistance.
379 Returns a \l locationHit for each body hit, or an empty list if the ray hits
380 nothing.
381
382 \list
383 \li \a origin is the starting position of the ray, in world space.
384 \li \a direction is the direction of the ray. It must not be a null vector and does
385 not need to be normalized; the query normalizes it.
386 \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
387 must be greater than 0.
388 \li \a includeStatic includes static bodies in the query when \c true.
389 \li \a includeDynamic includes dynamic bodies in the query when \c true.
390 \endlist
391
392 \include qtquick3dphysics-queries.qdocinc include_flags
393
394 \include qtquick3dphysics-queries.qdocinc query_exclusions
395
396 \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
397
398 \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
399
400 \include qtquick3dphysics-queries.qdocinc unordered_hits_note
401
402 \sa testRaycastQuery, singleRaycastQuery, {Qt Quick 3D Physics Scene Queries}
403*/
404
405/*!
406 \qmlmethod bool PhysicsWorld::testSweepQuery(CollisionShape shape, vector3d direction,
407 real maxDistance,
408 bool includeStatic = true,
409 bool includeDynamic = true)
410 \since 6.13
411
412 Sweeps a collision shape along a straight path and reports whether the path is
413 obstructed, without computing precise hit geometry.
414
415 Returns \c true if \a shape hits a body before travelling \a maxDistance; otherwise
416 returns \c false. The query stops at the first body it finds, which makes it the
417 cheapest of the sweep queries.
418
419 \list
420 \li \a shape is the \l CollisionShape to sweep through the scene.
421 \li \a direction is the direction of the sweep. It must not be a null vector and does
422 not need to be normalized; the query normalizes it.
423 \li \a maxDistance is the maximum distance along \a direction to travel. It must be
424 greater than 0.
425 \li \a includeStatic includes static bodies in the query when \c true.
426 \li \a includeDynamic includes dynamic bodies in the query when \c true.
427 \endlist
428
429 \include qtquick3dphysics-queries.qdocinc query_shape
430
431 \include qtquick3dphysics-queries.qdocinc include_flags
432
433 \include qtquick3dphysics-queries.qdocinc query_exclusions
434
435 \sa singleSweepQuery, multiSweepQuery, {Qt Quick 3D Physics Scene Queries}
436*/
437
438/*!
439 \qmlmethod locationHit PhysicsWorld::singleSweepQuery(CollisionShape shape,
440 vector3d direction,
441 real maxDistance,
442 bool includeStatic = true,
443 bool includeDynamic = true)
444 \since 6.13
445
446 Sweeps a collision shape along a straight path and returns the closest body it
447 hits.
448
449 Returns a \l locationHit describing the closest intersection encountered along the
450 sweep.
451
452 \list
453 \li \a shape is the \l CollisionShape to sweep through the scene.
454 \li \a direction is the direction of the sweep. It must not be a null vector and does
455 not need to be normalized; the query normalizes it.
456 \li \a maxDistance is the maximum distance along \a direction to travel. It must be
457 greater than 0.
458 \li \a includeStatic includes static bodies in the query when \c true.
459 \li \a includeDynamic includes dynamic bodies in the query when \c true.
460 \endlist
461
462 \include qtquick3dphysics-queries.qdocinc query_shape
463
464 \include qtquick3dphysics-queries.qdocinc include_flags
465
466 \include qtquick3dphysics-queries.qdocinc hit_miss_return
467
468 \include qtquick3dphysics-queries.qdocinc query_exclusions
469
470 \include qtquick3dphysics-queries.qdocinc mtd_overlap_note
471
472 \sa testSweepQuery, multiSweepQuery, {Qt Quick 3D Physics Scene Queries}
473*/
474
475/*!
476 \qmlmethod list<locationHit> PhysicsWorld::multiSweepQuery(CollisionShape shape,
477 vector3d direction,
478 real maxDistance,
479 bool includeStatic = true,
480 bool includeDynamic = true)
481 \since 6.13
482
483 Sweeps a collision shape along a straight path and returns every body it passes
484 through, rather than stopping at the closest one.
485
486 Returns a \l locationHit for each body hit, or an empty list if the sweep hits
487 nothing.
488
489 \list
490 \li \a shape is the \l CollisionShape to sweep through the scene.
491 \li \a direction is the direction of the sweep. It must not be a null vector and does
492 not need to be normalized; the query normalizes it.
493 \li \a maxDistance is the maximum distance along \a direction to travel. It must be
494 greater than 0.
495 \li \a includeStatic includes static bodies in the query when \c true.
496 \li \a includeDynamic includes dynamic bodies in the query when \c true.
497 \endlist
498
499 \include qtquick3dphysics-queries.qdocinc query_shape
500
501 \include qtquick3dphysics-queries.qdocinc include_flags
502
503 \include qtquick3dphysics-queries.qdocinc query_exclusions
504
505 \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
506
507 \include qtquick3dphysics-queries.qdocinc unordered_hits_note
508
509 \include qtquick3dphysics-queries.qdocinc mtd_overlap_note
510
511 \sa testSweepQuery, singleSweepQuery, {Qt Quick 3D Physics Scene Queries}
512*/
513
514/*!
515 \qmlmethod bool PhysicsWorld::testOverlapQuery(CollisionShape shape,
516 bool includeStatic = true,
517 bool includeDynamic = true)
518 \since 6.13
519
520 Reports whether any body occupies the volume of a stationary collision shape.
521
522 Returns \c true if \a shape overlaps a body; otherwise returns \c false. The query
523 stops at the first body it finds and builds no hit data, which makes it the cheapest
524 of the overlap queries.
525
526 \list
527 \li \a shape is the \l CollisionShape defining the overlap volume.
528 \li \a includeStatic includes static bodies in the query when \c true.
529 \li \a includeDynamic includes dynamic bodies in the query when \c true.
530 \endlist
531
532 \include qtquick3dphysics-queries.qdocinc query_shape
533
534 \include qtquick3dphysics-queries.qdocinc include_flags
535
536 \include qtquick3dphysics-queries.qdocinc query_exclusions
537
538 \sa multiOverlapQuery, {Qt Quick 3D Physics Scene Queries}
539*/
540
541/*!
542 \qmlmethod list<queryHit> PhysicsWorld::multiOverlapQuery(CollisionShape shape,
543 bool includeStatic = true,
544 bool includeDynamic = true)
545 \since 6.13
546
547 Returns every body that overlaps the volume of a stationary collision shape.
548
549 Returns a \l queryHit for each overlapping body, or an empty list if the volume is
550 unoccupied.
551
552 \list
553 \li \a shape is the \l CollisionShape defining the overlap volume.
554 \li \a includeStatic includes static bodies in the query when \c true.
555 \li \a includeDynamic includes dynamic bodies in the query when \c true.
556 \endlist
557
558 \include qtquick3dphysics-queries.qdocinc query_shape
559
560 \include qtquick3dphysics-queries.qdocinc include_flags
561
562 \include qtquick3dphysics-queries.qdocinc query_exclusions
563
564 \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
565
566 \include qtquick3dphysics-queries.qdocinc unordered_hits_note
567
568 \sa testOverlapQuery, {Qt Quick 3D Physics Scene Queries}
569*/
570
571Q_LOGGING_CATEGORY(lcQuick3dPhysics, "qt.quick3d.physics");
572
573// Setting QT_PHYSICS_TIMINGS_FILE to a filepath will generate a csv file with frame timings.
574// Note that if running several PhysicsWorld's the last one to be destructed will overwrite
575// the output file.
576//
577// To view it in gnuplot, run these two commands:
578//
579// set datafile separator ','
580// plot '<QT_PHYSICS_TIMINGS_FILE>' using 1:2 with lines
581//
582// Security note: This file is trusted since it is opened as write-only
583static const QString qtPhysicsTimingsFile = qEnvironmentVariable("QT_PHYSICS_TIMINGS_FILE");
584
585/////////////////////////////////////////////////////////////////////////////
586
587static bool validateQuery(const physx::PxScene *scene,
588 bool includeStatic,
589 bool includeDynamic)
590{
591 if (!scene) {
592 qWarning() << "QtQuick3DPhysics: cannot run a scene query before the physics world is "
593 "initialized.";
594 return false;
595 }
596
597 if (!(includeStatic || includeDynamic)) {
598 qWarning() << "QtQuick3DPhysics: a scene query needs includeStatic or includeDynamic to "
599 "be true.";
600 return false;
601 }
602
603 return true;
604}
605
606static bool raycastImpl(const physx::PxScene *scene,
607 const QVector3D &origin,
608 const QVector3D &direction,
609 float maxDistance,
610 bool includeStatic,
611 bool includeDynamic,
612 physx::PxRaycastCallback &hitCallback,
613 physx::PxHitFlags hitFlags,
614 physx::PxQueryFlags extraQueryFlags = {},
615 physx::PxQueryFilterCallback *filterCall = nullptr)
616{
617 if (!validateQuery(scene, includeStatic, includeDynamic))
618 return false;
619
620 const physx::PxVec3 pxOrigin = QPhysicsUtils::toPhysXType(origin);
621 if (!pxOrigin.isFinite()) {
622 qWarning() << "QtQuick3DPhysics: the query origin must be finite.";
623 return false;
624 }
625
626 const physx::PxVec3 pxDirection = QPhysicsUtils::toPhysXType(direction.normalized());
627 if (!pxDirection.isNormalized()) {
628 qWarning() << "QtQuick3DPhysics: the query direction must be a non-null, finite vector.";
629 return false;
630 }
631
632 if (!qIsFinite(maxDistance) || maxDistance <= 0) {
633 qWarning() << "QtQuick3DPhysics: the query distance must be finite and greater than 0.";
634 return false;
635 }
636
637 // Combine base flags with optional query flags (e.g., eANY_HIT or eNO_BLOCK)
638 physx::PxQueryFlags filter = extraQueryFlags;
639 if (includeStatic)
640 filter |= physx::PxQueryFlag::eSTATIC;
641 if (includeDynamic)
642 filter |= physx::PxQueryFlag::eDYNAMIC;
643
644 // Execute PhysX scene raycast
645 return scene->raycast(pxOrigin, pxDirection, maxDistance, hitCallback, hitFlags,
646 physx::PxQueryFilterData(filter), filterCall);
647}
648
649static const physx::PxGeometry *validateQueryShape(const physx::PxScene *scene,
650 QAbstractCollisionShape *shape,
651 bool includeStatic,
652 bool includeDynamic)
653{
654 if (!validateQuery(scene, includeStatic, includeDynamic)) {
655 return nullptr;
656 }
657
658 const physx::PxGeometry *geometry = shape ? shape->getPhysXGeometry() : nullptr;
659 if (!geometry) {
660 qWarning() << "QtQuick3DPhysics: the query shape has no geometry yet.";
661 return nullptr;
662 }
663
664 // PhysX only supports sphere, capsule, box and convex mesh as the query geometry. Anything
665 // else leaves Gu::ShapeData's bounds uninitialized in a release build.
666 switch (geometry->getType()) {
667 case physx::PxGeometryType::eSPHERE:
668 case physx::PxGeometryType::eCAPSULE:
669 case physx::PxGeometryType::eBOX:
670 case physx::PxGeometryType::eCONVEXMESH:
671 break;
672 default:
673 qWarning() << "QtQuick3DPhysics: sweep and overlap queries only support SphereShape, "
674 "CapsuleShape, BoxShape and ConvexMeshShape as the query shape.";
675 return nullptr;
676 }
677
678 return geometry;
679}
680
681static bool sweepImpl(const physx::PxScene *scene,
682 QAbstractCollisionShape *shape,
683 const QVector3D &direction,
684 float maxDistance,
685 bool includeStatic,
686 bool includeDynamic,
687 physx::PxSweepCallback &hitCallback,
688 physx::PxHitFlags hitFlags,
689 physx::PxQueryFlags extraQueryFlags = {})
690{
691 const physx::PxGeometry *geometry = validateQueryShape(scene, shape, includeStatic, includeDynamic);
692 if (!geometry) {
693 return false;
694 }
695
696 const physx::PxVec3 pxDirection = QPhysicsUtils::toPhysXType(direction.normalized());
697 if (!pxDirection.isNormalized()) {
698 qWarning() << "QtQuick3DPhysics: the query direction must be a non-null, finite vector.";
699 return false;
700 }
701
702 if (!qIsFinite(maxDistance) || maxDistance <= 0) {
703 qWarning() << "QtQuick3DPhysics: the query distance must be finite and greater than 0.";
704 return false;
705 }
706
707 // Combine base flags with optional query flags (e.g., eANY_HIT or eNO_BLOCK)
708 physx::PxQueryFlags filter = extraQueryFlags;
709 if (includeStatic)
710 filter |= physx::PxQueryFlag::eSTATIC;
711 if (includeDynamic)
712 filter |= physx::PxQueryFlag::eDYNAMIC;
713
714 const physx::PxTransform pose(QPhysicsUtils::toPhysXType(shape->scenePosition()),
715 QPhysicsUtils::toPhysXType(shape->sceneRotation()));
716 if (!pose.isSane()) {
717 qWarning() << "QtQuick3DPhysics: the query position/rotation is not finite.";
718 return false;
719 }
720
721 // Execute PhysX scene sweep
722 return scene->sweep(*geometry, pose, pxDirection, maxDistance, hitCallback, hitFlags,
723 physx::PxQueryFilterData(filter));
724}
725
726static bool overlapImpl(const physx::PxScene *scene,
727 QAbstractCollisionShape *shape,
728 bool includeStatic,
729 bool includeDynamic,
730 physx::PxOverlapCallback &hitCallback,
731 physx::PxQueryFlags extraQueryFlags = {})
732{
733
734 const physx::PxGeometry *geometry = validateQueryShape(scene, shape, includeStatic, includeDynamic);
735 if (!geometry) {
736 return false;
737 }
738
739 // Combine base static/dynamic flags with query behavior flags (e.g., eANY_HIT or eNO_BLOCK)
740 physx::PxQueryFlags filter = extraQueryFlags;
741 if (includeStatic)
742 filter |= physx::PxQueryFlag::eSTATIC;
743 if (includeDynamic)
744 filter |= physx::PxQueryFlag::eDYNAMIC;
745
746 const physx::PxTransform pose(QPhysicsUtils::toPhysXType(shape->scenePosition()),
747 QPhysicsUtils::toPhysXType(shape->sceneRotation()));
748 if (!pose.isSane()) {
749 qWarning() << "QtQuick3DPhysics: the query position/rotation is not finite.";
750 return false;
751 }
752
753 // Execute PhysX scene overlap query
754 return scene->overlap(*geometry, pose, hitCallback,
755 physx::PxQueryFilterData(filter));
756}
757
759{
761public:
763 {
764 // Needed to start the frame animation
765 classBegin();
767 }
768};
769
770namespace {
771
772// Passes over the actors a Raycaster must not report: the bodies it excludes, and any actor
773// whose body was deleted earlier in this frame, which stays in the scene until the next one
774// with nothing behind it. Filtering before a hit is recorded keeps the query blocking, so
775// PhysX still shortens the ray at the first surface that is kept.
776class ExcludedBodiesFilter : public physx::PxQueryFilterCallback
777{
778public:
779 explicit ExcludedBodiesFilter(QSpan<const QAbstractPhysicsNode *const> excludedBodies)
780 : m_excludedBodies(excludedBodies)
781 {
782 }
783
784 physx::PxQueryHitType::Enum preFilter(const physx::PxFilterData & /*filterData*/,
785 const physx::PxShape * /*shape*/,
786 const physx::PxRigidActor *actor,
787 physx::PxHitFlags & /*queryFlags*/) override
788 {
789 const auto *body = static_cast<const QAbstractPhysicsNode *>(actor->userData);
790 if (!body)
791 return physx::PxQueryHitType::eNONE;
792 const bool excluded = std::find(m_excludedBodies.begin(), m_excludedBodies.end(), body)
793 != m_excludedBodies.end();
794 return excluded ? physx::PxQueryHitType::eNONE : physx::PxQueryHitType::eBLOCK;
795 }
796
797 physx::PxQueryHitType::Enum postFilter(const physx::PxFilterData & /*filterData*/,
798 const physx::PxQueryHit & /*hit*/,
799 const physx::PxShape * /*shape*/,
800 const physx::PxRigidActor * /*actor*/) override
801 {
802 return physx::PxQueryHitType::eBLOCK;
803 }
804
805private:
806 QSpan<const QAbstractPhysicsNode *const> m_excludedBodies;
807};
808
809// Collects every touching hit from a scene query. PhysX needs a caller-owned buffer to sort
810// into, and hands the hits over in batches of at most BufferSize once it fills up, so the
811// buffer stays and processTouches() appends each batch to hits.
812// A struct because the PhysX callback bases declare all their members public.
813template<class PxCbT, class HitT, class PxHitT, physx::PxU32 BufferSize = 16>
814struct DynamicQueryCallback : public PxCbT
815{
816 DynamicQueryCallback() : PxCbT(localBuffer, BufferSize) {}
817
818 physx::PxAgain processTouches(const PxHitT *buffer, physx::PxU32 nbHits) override
819 {
820 hits.reserve(hits.size() + nbHits);
821 for (physx::PxU32 i = 0; i < nbHits; ++i)
822 hits.emplaceBack(buffer[i], buffer[i]);
823 return true;
824 }
825
826 PxHitT localBuffer[BufferSize];
827 QList<HitT> hits;
828};
829
830} // namespace
831
832/////////////////////////////////////////////////////////////////////////////
833
834void QPhysicsWorld::DebugModelHolder::releaseMeshPointer()
835{
836 if (auto base = static_cast<physx::PxBase *>(ptr); base)
837 base->release();
838 ptr = nullptr;
839}
840
841const QVector3D &QPhysicsWorld::DebugModelHolder::halfExtents() const
842{
843 return data;
844}
845void QPhysicsWorld::DebugModelHolder::setHalfExtents(const QVector3D &halfExtents)
846{
847 data = halfExtents;
848}
849float QPhysicsWorld::DebugModelHolder::radius() const
850{
851 return data.x();
852}
853void QPhysicsWorld::DebugModelHolder::setRadius(float radius)
854{
855 data.setX(radius);
856}
857float QPhysicsWorld::DebugModelHolder::heightScale() const
858{
859 return data.x();
860}
861void QPhysicsWorld::DebugModelHolder::setHeightScale(float heightScale)
862{
863 data.setX(heightScale);
864}
865float QPhysicsWorld::DebugModelHolder::halfHeight() const
866{
867 return data.y();
868}
869void QPhysicsWorld::DebugModelHolder::setHalfHeight(float halfHeight)
870{
871 data.setY(halfHeight);
872}
873float QPhysicsWorld::DebugModelHolder::rowScale() const
874{
875 return data.y();
876}
877void QPhysicsWorld::DebugModelHolder::setRowScale(float rowScale)
878{
879 data.setY(rowScale);
880}
881float QPhysicsWorld::DebugModelHolder::columnScale() const
882{
883 return data.z();
884}
885void QPhysicsWorld::DebugModelHolder::setColumnScale(float columnScale)
886{
887 data.setZ(columnScale);
888}
889physx::PxConvexMesh *QPhysicsWorld::DebugModelHolder::getConvexMesh()
890{
891 return static_cast<physx::PxConvexMesh *>(ptr);
892}
893void QPhysicsWorld::DebugModelHolder::setConvexMesh(physx::PxConvexMesh *mesh)
894{
895 ptr = static_cast<void *>(mesh);
896}
897physx::PxTriangleMesh *QPhysicsWorld::DebugModelHolder::getTriangleMesh()
898{
899 return static_cast<physx::PxTriangleMesh *>(ptr);
900}
901void QPhysicsWorld::DebugModelHolder::setTriangleMesh(physx::PxTriangleMesh *mesh)
902{
903 ptr = static_cast<void *>(mesh);
904}
905physx::PxHeightField *QPhysicsWorld::DebugModelHolder::getHeightField()
906{
907 return static_cast<physx::PxHeightField *>(ptr);
908}
909void QPhysicsWorld::DebugModelHolder::setHeightField(physx::PxHeightField *hf)
910{
911 ptr = static_cast<physx::PxHeightField *>(hf);
912}
913
914/////////////////////////////////////////////////////////////////////////////
915
923
924static QWorldManager worldManager = QWorldManager {};
925
926void QPhysicsWorld::registerNode(QAbstractPhysicsNode *physicsNode)
927{
928 auto world = getWorld(physicsNode);
929 if (world) {
930 world->m_newPhysicsNodes.push_back(physicsNode);
931 } else {
932 worldManager.orphanNodes.push_back(physicsNode);
933 }
934}
935
936void QPhysicsWorld::deregisterNode(QAbstractPhysicsNode *physicsNode)
937{
938 for (auto world : std::as_const(worldManager.worlds)) {
939 world->m_newPhysicsNodes.removeAll(physicsNode);
940 if (physicsNode->m_backendObject) {
941 Q_ASSERT(physicsNode->m_backendObject->frontendNode == physicsNode);
942 physicsNode->m_backendObject->detachFrontend();
943 physicsNode->m_backendObject->frontendNode = nullptr;
944 physicsNode->m_backendObject->isRemoved = true;
945 physicsNode->m_backendObject = nullptr;
946 }
947 world->m_removedPhysicsNodes.insert(physicsNode);
948 }
949 worldManager.orphanNodes.removeAll(physicsNode);
950}
951
952void QPhysicsWorld::registerJoint(QPhysicsJoint *joint)
953{
954 auto world = getWorld(joint);
955 if (world) {
956 world->m_joints.push_back(joint);
957 } else {
958 worldManager.orphanJoints.push_back(joint);
959 }
960}
961
962void QPhysicsWorld::deregisterJoint(QPhysicsJoint *joint)
963{
964 for (auto world : worldManager.worlds) {
965 world->m_removedJoints.insert(joint->getPhysXBackend());
966
967 // Swap erase since order does not matter
968 qsizetype idx = world->m_joints.indexOf(joint);
969 if (idx != -1) {
970 world->m_joints.swapItemsAt(idx, world->m_joints.size() - 1);
971 world->m_joints.pop_back();
972 }
973 }
974 worldManager.orphanJoints.removeAll(joint);
975}
976
977void QPhysicsWorld::registerRaycaster(QRaycaster *raycaster)
978{
979 if (auto *world = getWorld(raycaster))
980 world->m_raycasters.push_back(raycaster);
981 else
982 worldManager.orphanRaycasters.push_back(raycaster);
983}
984
985void QPhysicsWorld::deregisterRaycaster(QRaycaster *raycaster)
986{
987 for (auto *world : std::as_const(worldManager.worlds))
988 world->m_raycasters.removeAll(raycaster);
989 worldManager.orphanRaycasters.removeAll(raycaster);
990}
991
992void QPhysicsWorld::registerContact(QAbstractPhysicsNode *sender, QAbstractPhysicsNode *receiver,
993 const QVector<QVector3D> &positions,
994 const QVector<QVector3D> &impulses,
995 const QVector<QVector3D> &normals)
996{
997 // The callbacks come from fetchResults(), on this thread, but a contact is
998 // between two nodes and reporting the first is free to delete the second.
999 // So the contacts are saved and run at the end of the physics frame, when
1000 // it is known which of their nodes are still there.
1001
1002 BodyContact contact;
1003 contact.sender = sender;
1004 contact.receiver = receiver;
1005 contact.positions = positions;
1006 contact.impulses = impulses;
1007 contact.normals = normals;
1008
1009 m_registeredContacts.push_back(contact);
1010}
1011
1012QPhysicsWorld::QPhysicsWorld(QObject *parent) : QObject(parent)
1013{
1014 m_inDesignStudio = !qEnvironmentVariableIsEmpty("QML_PUPPET_MODE");
1015 m_physx = new QPhysXWorld;
1016 m_physx->createWorld();
1017
1018 worldManager.worlds.push_back(this);
1019 matchOrphanNodes();
1020 matchOrphanJoints();
1021 matchOrphanRaycasters();
1022
1023 m_frameAnimator = new FrameAnimator;
1024 connect(m_frameAnimator, &QQuickFrameAnimation::triggered, this,
1025 &QPhysicsWorld::simulateFrame);
1026}
1027
1028QPhysicsWorld::~QPhysicsWorld()
1029{
1030 if (m_frameAnimator) {
1031 m_frameAnimator->stop();
1032 delete m_frameAnimator;
1033 }
1034
1035 if (m_physx->scene && m_physx->isRunning && !m_frameFetched)
1036 m_physx->scene->fetchResults(true);
1037
1038 for (auto body : std::as_const(m_physXBodies)) {
1039 body->cleanup(m_physx);
1040 delete body;
1041 }
1042 m_physXBodies.clear();
1043 m_physx->deleteWorld();
1044 delete m_physx;
1045 worldManager.worlds.removeAll(this);
1046
1047 // A raycaster can outlive the world it was matched to, since it lives in the scene and
1048 // not in this list. Back to the orphans, so a later world over the same scene picks it
1049 // up again.
1050 worldManager.orphanRaycasters += m_raycasters;
1051 m_raycasters.clear();
1052
1053 if (!qtPhysicsTimingsFile.isEmpty()) {
1054 if (m_frameTimings.isEmpty()) {
1055 qWarning() << "No frame timings saved.";
1056 } else if (auto csvFile = QFile(qtPhysicsTimingsFile); csvFile.open(QIODevice::WriteOnly)) {
1057 QTextStream out(&csvFile);
1058 for (int i = 1; i < m_frameTimings.size(); i++) {
1059 out << i << "," << m_frameTimings[i] << '\n';
1060 }
1061 csvFile.close();
1062 } else {
1063 qWarning() << "Could not open timings file " << qtPhysicsTimingsFile;
1064 }
1065 }
1066}
1067
1068void QPhysicsWorld::classBegin() {}
1069
1070void QPhysicsWorld::componentComplete()
1071{
1072 if ((!m_running && !m_inDesignStudio) || m_physicsInitialized)
1073 return;
1074 initPhysics();
1075}
1076
1077QVector3D QPhysicsWorld::gravity() const
1078{
1079 return m_gravity;
1080}
1081
1082bool QPhysicsWorld::running() const
1083{
1084 return m_running;
1085}
1086
1087bool QPhysicsWorld::forceDebugDraw() const
1088{
1089 return m_forceDebugDraw;
1090}
1091
1092bool QPhysicsWorld::enableCCD() const
1093{
1094 return m_enableCCD;
1095}
1096
1097float QPhysicsWorld::typicalLength() const
1098{
1099 return m_typicalLength;
1100}
1101
1102float QPhysicsWorld::typicalSpeed() const
1103{
1104 return m_typicalSpeed;
1105}
1106
1107bool QPhysicsWorld::isNodeRemoved(QAbstractPhysicsNode *object)
1108{
1109 return m_removedPhysicsNodes.contains(object);
1110}
1111
1112void QPhysicsWorld::setGravity(QVector3D gravity)
1113{
1114 if (m_gravity == gravity)
1115 return;
1116
1117 if (!QPhysicsUtils::isFinite(gravity)) {
1118 qWarning() << "Warning: 'gravity' must be finite, ignored";
1119 return;
1120 }
1121
1122 m_gravity = gravity;
1123 if (m_physx->scene) {
1124 m_physx->scene->setGravity(QPhysicsUtils::toPhysXType(m_gravity));
1125 }
1126 emit gravityChanged(m_gravity);
1127}
1128
1129void QPhysicsWorld::setRunning(bool running)
1130{
1131 if (m_running == running)
1132 return;
1133
1134 m_running = running;
1135 if (!m_inDesignStudio && m_running && !m_physicsInitialized)
1136 initPhysics();
1137
1138 if (running)
1139 m_frameAnimator->start();
1140 else
1141 m_frameAnimator->stop();
1142
1143 emit runningChanged(m_running);
1144}
1145
1146void QPhysicsWorld::setForceDebugDraw(bool forceDebugDraw)
1147{
1148 if (m_forceDebugDraw == forceDebugDraw)
1149 return;
1150
1151 m_forceDebugDraw = forceDebugDraw;
1152 if (!m_forceDebugDraw)
1153 disableDebugDraw();
1154 else
1155 updateDebugDraw();
1156 emit forceDebugDrawChanged(m_forceDebugDraw);
1157}
1158
1159QQuick3DNode *QPhysicsWorld::viewport() const
1160{
1161 return m_viewport;
1162}
1163
1164void QPhysicsWorld::setHasIndividualDebugDraw()
1165{
1166 m_hasIndividualDebugDraw = true;
1167}
1168
1169void QPhysicsWorld::setViewport(QQuick3DNode *viewport)
1170{
1171 if (m_viewport == viewport)
1172 return;
1173
1174 m_viewport = viewport;
1175
1176 // TODO: test this
1177 for (auto material : std::as_const(m_debugMaterials))
1178 delete material;
1179 m_debugMaterials.clear();
1180
1181 for (auto &holder : m_collisionShapeDebugModels) {
1182 holder.releaseMeshPointer();
1183 delete holder.model;
1184 }
1185 m_collisionShapeDebugModels.clear();
1186
1187 emit viewportChanged(m_viewport);
1188}
1189
1190void QPhysicsWorld::setMinimumTimestep(float minTimestep)
1191{
1192 if (qFuzzyCompare(m_minTimestep, minTimestep))
1193 return;
1194
1195 if (!qIsFinite(minTimestep)) {
1196 qWarning("Minimum timestep must be finite, ignoring");
1197 return;
1198 }
1199
1200 if (minTimestep > m_maxTimestep) {
1201 qWarning("Minimum timestep greater than maximum timestep, value clamped");
1202 minTimestep = qMin(minTimestep, m_maxTimestep);
1203 }
1204
1205 if (minTimestep < 0.f) {
1206 qWarning("Minimum timestep less than zero, value clamped");
1207 minTimestep = qMax(minTimestep, 0.f);
1208 }
1209
1210 if (qFuzzyCompare(m_minTimestep, minTimestep))
1211 return;
1212
1213 m_minTimestep = minTimestep;
1214 emit minimumTimestepChanged(m_minTimestep);
1215}
1216
1217void QPhysicsWorld::setMaximumTimestep(float maxTimestep)
1218{
1219 if (qFuzzyCompare(m_maxTimestep, maxTimestep))
1220 return;
1221
1222 if (!qIsFinite(maxTimestep)) {
1223 qWarning("Maximum timestep must be finite, ignoring");
1224 return;
1225 }
1226
1227 if (maxTimestep < 0.f) {
1228 qWarning("Maximum timestep less than zero, value clamped");
1229 maxTimestep = qMax(maxTimestep, 0.f);
1230 }
1231
1232 if (qFuzzyCompare(m_maxTimestep, maxTimestep))
1233 return;
1234
1235 m_maxTimestep = maxTimestep;
1236 emit maximumTimestepChanged(maxTimestep);
1237}
1238
1239void QPhysicsWorld::setupDebugMaterials(QQuick3DNode *sceneNode)
1240{
1241 if (!m_debugMaterials.isEmpty())
1242 return;
1243
1244 const int lineWidth = m_inDesignStudio ? 1 : 3;
1245
1246 // These colors match the indices of DebugDrawBodyType enum
1247 for (auto color : { QColorConstants::Svg::chartreuse, QColorConstants::Svg::cyan,
1248 QColorConstants::Svg::lightsalmon, QColorConstants::Svg::red,
1249 QColorConstants::Svg::blueviolet, QColorConstants::Svg::black }) {
1250 auto debugMaterial = new QQuick3DPrincipledMaterial();
1251 debugMaterial->setLineWidth(lineWidth);
1252 debugMaterial->setParentItem(sceneNode);
1253 debugMaterial->setParent(sceneNode);
1254 debugMaterial->setBaseColor(color);
1255 debugMaterial->setLighting(QQuick3DPrincipledMaterial::NoLighting);
1256 debugMaterial->setCullMode(QQuick3DMaterial::NoCulling);
1257 m_debugMaterials.push_back(debugMaterial);
1258 }
1259}
1260
1261void QPhysicsWorld::updateDebugDraw()
1262{
1263 if (!(m_forceDebugDraw || m_hasIndividualDebugDraw)) {
1264 // Nothing to draw, trash all previous models (if any) and return
1265 for (auto &holder : m_collisionShapeDebugModels) {
1266 holder.releaseMeshPointer();
1267 delete holder.model;
1268 }
1269 m_collisionShapeDebugModels.clear();
1270 return;
1271 }
1272
1273 // Use scene node if no viewport has been specified
1274 auto sceneNode = m_viewport ? m_viewport : m_scene;
1275
1276 if (sceneNode == nullptr)
1277 return;
1278
1279 setupDebugMaterials(sceneNode);
1280 m_hasIndividualDebugDraw = false;
1281
1282 // Store the collision shapes we have now so we can clear out the removed ones
1283 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>> currentCollisionShapes;
1284 currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
1285
1286 for (QAbstractPhysXNode *node : std::as_const(m_physXBodies)) {
1287 // A node deleted from a report earlier in this frame keeps its backend
1288 // until the next one takes it out.
1289 if (!node->frontendNode || !node->debugGeometryCapability())
1290 continue;
1291
1292 const auto &collisionShapes = node->frontendNode->getCollisionShapesList();
1293 const int materialIdx = static_cast<int>(node->getDebugDrawBodyType());
1294 const int length = collisionShapes.length();
1295 for (int idx = 0; idx < length; idx++) {
1296 const auto collisionShape = collisionShapes[idx];
1297
1298 if (!m_forceDebugDraw && !collisionShape->enableDebugDraw())
1299 continue;
1300
1301 DebugModelHolder &holder =
1302 m_collisionShapeDebugModels[std::make_pair(collisionShape, node)];
1303 auto &model = holder.model;
1304
1305 currentCollisionShapes.insert(std::make_pair(collisionShape, node));
1306
1307 m_hasIndividualDebugDraw =
1308 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
1309
1310 // Create/Update debug view infrastructure
1311 if (!model) {
1312 model = new QQuick3DModel();
1313 model->setParentItem(sceneNode);
1314 model->setParent(sceneNode);
1315 model->setCastsShadows(false);
1316 model->setReceivesShadows(false);
1317 model->setCastsReflections(false);
1318 }
1319
1320 model->setVisible(true);
1321
1322 { // update or set material
1323 auto material = m_debugMaterials[materialIdx];
1324 QQmlListReference materialsRef(model, "materials");
1325 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
1326 materialsRef.clear();
1327 materialsRef.append(material);
1328 }
1329 }
1330
1331 // Special handling of CharacterController since it has collision shapes,
1332 // but not PhysX shapes
1333 if (qobject_cast<QCharacterController *>(node->frontendNode)) {
1334 QCapsuleShape *capsuleShape = qobject_cast<QCapsuleShape *>(collisionShape);
1335 if (!capsuleShape)
1336 continue;
1337
1338 const float radius = capsuleShape->diameter() * 0.5;
1339 const float halfHeight = capsuleShape->height() * 0.5;
1340
1341 if (!qFuzzyCompare(radius, holder.radius())
1342 || !qFuzzyCompare(halfHeight, holder.halfHeight())) {
1343 auto geom = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
1344 geom->setParent(model);
1345 model->setGeometry(geom);
1346 holder.setRadius(radius);
1347 holder.setHalfHeight(halfHeight);
1348 }
1349
1350 model->setPosition(node->frontendNode->scenePosition());
1351 model->setRotation(node->frontendNode->sceneRotation()
1352 * QQuaternion::fromEulerAngles(0, 0, 90));
1353 continue;
1354 }
1355
1356 if (node->shapes.length() < length)
1357 continue;
1358
1359 const auto physXShape = node->shapes[idx];
1360 auto localPose = physXShape->getLocalPose();
1361
1362 const physx::PxGeometry &geometry = physXShape->getGeometry();
1363
1364 switch (geometry.getType()) {
1365 case physx::PxGeometryType::eBOX: {
1366 const auto &boxGeometry = static_cast<const physx::PxBoxGeometry &>(geometry);
1367 const auto &halfExtentsOld = holder.halfExtents();
1368 const auto halfExtents = QPhysicsUtils::toQtType(boxGeometry.halfExtents);
1369 if (!qFuzzyCompare(halfExtentsOld, halfExtents)) {
1370 auto geom = QDebugDrawHelper::generateBoxGeometry(halfExtents);
1371 geom->setParent(model);
1372 model->setGeometry(geom);
1373 holder.setHalfExtents(halfExtents);
1374 }
1375
1376 }
1377 break;
1378
1379 case physx::PxGeometryType::eSPHERE: {
1380 const auto &sphereGeometry = static_cast<const physx::PxSphereGeometry &>(geometry);
1381 const float radius = holder.radius();
1382 if (!qFuzzyCompare(sphereGeometry.radius, radius)) {
1383 auto geom = QDebugDrawHelper::generateSphereGeometry(sphereGeometry.radius);
1384 geom->setParent(model);
1385 model->setGeometry(geom);
1386 holder.setRadius(sphereGeometry.radius);
1387 }
1388 }
1389 break;
1390
1391 case physx::PxGeometryType::eCAPSULE: {
1392 const auto &capsuleGeometry =
1393 static_cast<const physx::PxCapsuleGeometry &>(geometry);
1394 const float radius = holder.radius();
1395 const float halfHeight = holder.halfHeight();
1396
1397 if (!qFuzzyCompare(capsuleGeometry.radius, radius)
1398 || !qFuzzyCompare(capsuleGeometry.halfHeight, halfHeight)) {
1399 auto geom = QDebugDrawHelper::generateCapsuleGeometry(
1400 capsuleGeometry.radius, capsuleGeometry.halfHeight);
1401 geom->setParent(model);
1402 model->setGeometry(geom);
1403 holder.setRadius(capsuleGeometry.radius);
1404 holder.setHalfHeight(capsuleGeometry.halfHeight);
1405 }
1406 }
1407 break;
1408
1409 case physx::PxGeometryType::ePLANE:{
1410 // Special rotation
1411 const QQuaternion rotation =
1412 QPhysicsUtils::kMinus90YawRotation * QPhysicsUtils::toQtType(localPose.q);
1413 localPose = physx::PxTransform(localPose.p, QPhysicsUtils::toPhysXType(rotation));
1414
1415 if (model->geometry() == nullptr) {
1416 auto geom = QDebugDrawHelper::generatePlaneGeometry();
1417 geom->setParent(model);
1418 model->setGeometry(geom);
1419 }
1420 }
1421 break;
1422
1423 // For heightfield, convex mesh and triangle mesh we increase its reference count
1424 // to make sure it does not get dereferenced and deleted so that the new mesh will
1425 // have another memory address so we know when it has changed.
1426 case physx::PxGeometryType::eHEIGHTFIELD: {
1427 const auto &heightFieldGeometry =
1428 static_cast<const physx::PxHeightFieldGeometry &>(geometry);
1429 const float heightScale = holder.heightScale();
1430 const float rowScale = holder.rowScale();
1431 const float columnScale = holder.columnScale();
1432
1433 if (auto heightField = holder.getHeightField();
1434 heightField && heightField != heightFieldGeometry.heightField) {
1435 heightField->release();
1436 holder.setHeightField(nullptr);
1437 }
1438
1439 if (!qFuzzyCompare(heightFieldGeometry.heightScale, heightScale)
1440 || !qFuzzyCompare(heightFieldGeometry.rowScale, rowScale)
1441 || !qFuzzyCompare(heightFieldGeometry.columnScale, columnScale)
1442 || !holder.getHeightField()) {
1443 if (!holder.getHeightField()) {
1444 heightFieldGeometry.heightField->acquireReference();
1445 holder.setHeightField(heightFieldGeometry.heightField);
1446 }
1447 auto geom = QDebugDrawHelper::generateHeightFieldGeometry(
1448 heightFieldGeometry.heightField, heightFieldGeometry.heightScale,
1449 heightFieldGeometry.rowScale, heightFieldGeometry.columnScale);
1450 geom->setParent(model);
1451 model->setGeometry(geom);
1452 holder.setHeightScale(heightFieldGeometry.heightScale);
1453 holder.setRowScale(heightFieldGeometry.rowScale);
1454 holder.setColumnScale(heightFieldGeometry.columnScale);
1455 }
1456 }
1457 break;
1458
1459 case physx::PxGeometryType::eCONVEXMESH: {
1460 const auto &convexMeshGeometry =
1461 static_cast<const physx::PxConvexMeshGeometry &>(geometry);
1462 const auto rotation = convexMeshGeometry.scale.rotation * localPose.q;
1463 localPose = physx::PxTransform(localPose.p, rotation);
1464 model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry.scale.scale));
1465
1466 if (auto convexMesh = holder.getConvexMesh();
1467 convexMesh && convexMesh != convexMeshGeometry.convexMesh) {
1468 convexMesh->release();
1469 holder.setConvexMesh(nullptr);
1470 }
1471
1472 if (!model->geometry() || !holder.getConvexMesh()) {
1473 if (!holder.getConvexMesh()) {
1474 convexMeshGeometry.convexMesh->acquireReference();
1475 holder.setConvexMesh(convexMeshGeometry.convexMesh);
1476 }
1477 auto geom = QDebugDrawHelper::generateConvexMeshGeometry(
1478 convexMeshGeometry.convexMesh);
1479 geom->setParent(model);
1480 model->setGeometry(geom);
1481 }
1482 }
1483 break;
1484
1485 case physx::PxGeometryType::eTRIANGLEMESH: {
1486 const auto &triangleMeshGeometry =
1487 static_cast<const physx::PxTriangleMeshGeometry &>(geometry);
1488 const auto rotation = triangleMeshGeometry.scale.rotation * localPose.q;
1489 localPose = physx::PxTransform(localPose.p, rotation);
1490 model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry.scale.scale));
1491
1492 if (auto triangleMesh = holder.getTriangleMesh();
1493 triangleMesh && triangleMesh != triangleMeshGeometry.triangleMesh) {
1494 triangleMesh->release();
1495 holder.setTriangleMesh(nullptr);
1496 }
1497
1498 if (!model->geometry() || !holder.getTriangleMesh()) {
1499 if (!holder.getTriangleMesh()) {
1500 triangleMeshGeometry.triangleMesh->acquireReference();
1501 holder.setTriangleMesh(triangleMeshGeometry.triangleMesh);
1502 }
1503 auto geom = QDebugDrawHelper::generateTriangleMeshGeometry(
1504 triangleMeshGeometry.triangleMesh);
1505 geom->setParent(model);
1506 model->setGeometry(geom);
1507 }
1508 }
1509 break;
1510
1511 // Geometry types that no shape in Qt Quick 3D Physics can have
1512 case physx::PxGeometryType::eCONVEXCORE:
1513 case physx::PxGeometryType::ePARTICLESYSTEM:
1514 case physx::PxGeometryType::eTETRAHEDRONMESH:
1515 case physx::PxGeometryType::eCUSTOM:
1516 case physx::PxGeometryType::eINVALID:
1517 case physx::PxGeometryType::eGEOMETRY_COUNT:
1518 // should not happen
1519 Q_UNREACHABLE();
1520 }
1521
1522 auto globalPose = node->getGlobalPose();
1523 auto finalPose = globalPose.transform(localPose);
1524
1525 model->setRotation(QPhysicsUtils::toQtType(finalPose.q));
1526 model->setPosition(QPhysicsUtils::toQtType(finalPose.p));
1527 }
1528 }
1529
1530 // Remove old collision shapes
1531 m_collisionShapeDebugModels.removeIf(
1532 [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>,
1533 DebugModelHolder>::iterator it) {
1534 if (!currentCollisionShapes.contains(it.key())) {
1535 auto holder = it.value();
1536 holder.releaseMeshPointer();
1537 if (holder.model)
1538 delete holder.model;
1539 return true;
1540 }
1541 return false;
1542 });
1543}
1544
1545static void collectPhysicsNodes(QQuick3DObject *node, QList<QAbstractPhysicsNode *> &nodes)
1546{
1547 if (auto shape = qobject_cast<QAbstractPhysicsNode *>(node)) {
1548 nodes.push_back(shape);
1549 return;
1550 }
1551
1552 auto childItems = node->childItems();
1553 for (QQuick3DObject *child : std::as_const(childItems))
1554 collectPhysicsNodes(child, nodes);
1555}
1556
1557void QPhysicsWorld::updateDebugDrawDesignStudio()
1558{
1559 // Use scene node if no viewport has been specified
1560 auto sceneNode = m_viewport ? m_viewport : m_scene;
1561
1562 if (sceneNode == nullptr)
1563 return;
1564
1565 setupDebugMaterials(sceneNode);
1566
1567 // Store the collision shapes we have now so we can clear out the removed ones
1568 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>> currentCollisionShapes;
1569 currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
1570
1571 QList<QAbstractPhysicsNode *> activePhysicsNodes;
1572 activePhysicsNodes.reserve(m_collisionShapeDebugModels.size());
1573 collectPhysicsNodes(m_scene, activePhysicsNodes);
1574
1575 for (QAbstractPhysicsNode *node : std::as_const(activePhysicsNodes)) {
1576
1577 const auto &collisionShapes = node->getCollisionShapesList();
1578 const int materialIdx = 0; // Just take first material
1579 const int length = collisionShapes.length();
1580
1581 const bool isCharacterController = qobject_cast<QCharacterController *>(node) != nullptr;
1582
1583 for (int idx = 0; idx < length; idx++) {
1584 QAbstractCollisionShape *collisionShape = collisionShapes[idx];
1585 DebugModelHolder &holder =
1586 m_DesignStudioDebugModels[std::make_pair(collisionShape, node)];
1587 auto &model = holder.model;
1588
1589 currentCollisionShapes.insert(std::make_pair(collisionShape, node));
1590
1591 m_hasIndividualDebugDraw =
1592 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
1593
1594 // Create/Update debug view infrastructure
1595 {
1596 // Hack: we have to delete the model every frame so it shows up in QDS
1597 // whenever the code is updated, not sure why ¯\_(?)_/¯
1598 delete model;
1599 model = new QQuick3DModel();
1600 model->setParentItem(sceneNode);
1601 model->setParent(sceneNode);
1602 model->setCastsShadows(false);
1603 model->setReceivesShadows(false);
1604 model->setCastsReflections(false);
1605 }
1606
1607 const bool hasGeometry = holder.geometry != nullptr;
1608 QVector3D scenePosition = collisionShape->scenePosition();
1609 QQuaternion sceneRotation = collisionShape->sceneRotation();
1610 QQuick3DGeometry *newGeometry = nullptr;
1611
1612 if (isCharacterController)
1613 sceneRotation = sceneRotation * QQuaternion::fromEulerAngles(QVector3D(0, 0, 90));
1614
1615 { // update or set material
1616 auto material = m_debugMaterials[materialIdx];
1617 QQmlListReference materialsRef(model, "materials");
1618 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
1619 materialsRef.clear();
1620 materialsRef.append(material);
1621 }
1622 }
1623
1624 if (auto shape = qobject_cast<QBoxShape *>(collisionShape)) {
1625 const auto &halfExtentsOld = holder.halfExtents();
1626 const auto halfExtents = shape->sceneScale() * shape->extents() * 0.5f;
1627 if (!qFuzzyCompare(halfExtentsOld, halfExtents) || !hasGeometry) {
1628 newGeometry = QDebugDrawHelper::generateBoxGeometry(halfExtents);
1629 holder.setHalfExtents(halfExtents);
1630 }
1631 } else if (auto shape = qobject_cast<QSphereShape *>(collisionShape)) {
1632 const float radiusOld = holder.radius();
1633 const float radius = shape->sceneScale().x() * shape->diameter() * 0.5f;
1634 if (!qFuzzyCompare(radiusOld, radius) || !hasGeometry) {
1635 newGeometry = QDebugDrawHelper::generateSphereGeometry(radius);
1636 holder.setRadius(radius);
1637 }
1638 } else if (auto shape = qobject_cast<QCapsuleShape *>(collisionShape)) {
1639 const float radiusOld = holder.radius();
1640 const float halfHeightOld = holder.halfHeight();
1641 const float radius = shape->sceneScale().y() * shape->diameter() * 0.5f;
1642 const float halfHeight = shape->sceneScale().x() * shape->height() * 0.5f;
1643
1644 if ((!qFuzzyCompare(radiusOld, radius) || !qFuzzyCompare(halfHeightOld, halfHeight))
1645 || !hasGeometry) {
1646 newGeometry = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
1647 holder.setRadius(radius);
1648 holder.setHalfHeight(halfHeight);
1649 }
1650 } else if (qobject_cast<QPlaneShape *>(collisionShape)) {
1651 if (!hasGeometry)
1652 newGeometry = QDebugDrawHelper::generatePlaneGeometry();
1653 } else if (auto shape = qobject_cast<QHeightFieldShape *>(collisionShape)) {
1654 physx::PxHeightFieldGeometry *heightFieldGeometry =
1655 static_cast<physx::PxHeightFieldGeometry *>(shape->getPhysXGeometry());
1656 const float heightScale = holder.heightScale();
1657 const float rowScale = holder.rowScale();
1658 const float columnScale = holder.columnScale();
1659 scenePosition += shape->hfOffset();
1660 if (!heightFieldGeometry) {
1661 qWarning() << "Could not get height field";
1662 } else if (!qFuzzyCompare(heightFieldGeometry->heightScale, heightScale)
1663 || !qFuzzyCompare(heightFieldGeometry->rowScale, rowScale)
1664 || !qFuzzyCompare(heightFieldGeometry->columnScale, columnScale)
1665 || !hasGeometry) {
1666 newGeometry = QDebugDrawHelper::generateHeightFieldGeometry(
1667 heightFieldGeometry->heightField, heightFieldGeometry->heightScale,
1668 heightFieldGeometry->rowScale, heightFieldGeometry->columnScale);
1669 holder.setHeightScale(heightFieldGeometry->heightScale);
1670 holder.setRowScale(heightFieldGeometry->rowScale);
1671 holder.setColumnScale(heightFieldGeometry->columnScale);
1672 }
1673 } else if (auto shape = qobject_cast<QConvexMeshShape *>(collisionShape)) {
1674 auto convexMeshGeometry =
1675 static_cast<physx::PxConvexMeshGeometry *>(shape->getPhysXGeometry());
1676 if (!convexMeshGeometry) {
1677 qWarning() << "Could not get convex mesh";
1678 } else {
1679 model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry->scale.scale));
1680
1681 if (!hasGeometry) {
1682 newGeometry = QDebugDrawHelper::generateConvexMeshGeometry(
1683 convexMeshGeometry->convexMesh);
1684 }
1685 }
1686 } else if (auto shape = qobject_cast<QTriangleMeshShape *>(collisionShape)) {
1687 physx::PxTriangleMeshGeometry *triangleMeshGeometry =
1688 static_cast<physx::PxTriangleMeshGeometry *>(shape->getPhysXGeometry());
1689 if (!triangleMeshGeometry) {
1690 qWarning() << "Could not get triangle mesh";
1691 } else {
1692 model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry->scale.scale));
1693
1694 if (!hasGeometry) {
1695 newGeometry = QDebugDrawHelper::generateTriangleMeshGeometry(
1696 triangleMeshGeometry->triangleMesh);
1697 }
1698 }
1699 }
1700
1701 if (newGeometry) {
1702 delete holder.geometry;
1703 holder.geometry = newGeometry;
1704 }
1705
1706 model->setGeometry(holder.geometry);
1707 model->setVisible(true);
1708
1709 model->setRotation(sceneRotation);
1710 model->setPosition(scenePosition);
1711 }
1712 }
1713
1714 // Remove old debug models
1715 m_DesignStudioDebugModels.removeIf(
1716 [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>,
1717 DebugModelHolder>::iterator it) {
1718 if (!currentCollisionShapes.contains(it.key())) {
1719 auto holder = it.value();
1720 holder.releaseMeshPointer();
1721 if (holder.model) {
1722 delete holder.geometry;
1723 delete holder.model;
1724 }
1725 return true;
1726 }
1727 return false;
1728 });
1729}
1730
1731void QPhysicsWorld::disableDebugDraw()
1732{
1733 m_hasIndividualDebugDraw = false;
1734
1735 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1736 if (!body->frontendNode)
1737 continue;
1738
1739 const auto &collisionShapes = body->frontendNode->getCollisionShapesList();
1740 const int length = collisionShapes.length();
1741 for (int idx = 0; idx < length; idx++) {
1742 const auto collisionShape = collisionShapes[idx];
1743 if (collisionShape->enableDebugDraw()) {
1744 m_hasIndividualDebugDraw = true;
1745 return;
1746 }
1747 }
1748 }
1749}
1750
1751void QPhysicsWorld::setEnableCCD(bool enableCCD)
1752{
1753 if (m_enableCCD == enableCCD)
1754 return;
1755
1756 if (m_physicsInitialized) {
1757 qWarning()
1758 << "Warning: Changing 'enableCCD' after physics is initialized will have no effect";
1759 return;
1760 }
1761
1762 m_enableCCD = enableCCD;
1763 emit enableCCDChanged(m_enableCCD);
1764}
1765
1766void QPhysicsWorld::setTypicalLength(float typicalLength)
1767{
1768 if (qFuzzyCompare(typicalLength, m_typicalLength))
1769 return;
1770
1771 if (!qIsFinite(typicalLength) || typicalLength <= 0.f) {
1772 qWarning() << "Warning: 'typicalLength' value less than zero, ignored";
1773 return;
1774 }
1775
1776 if (m_physicsInitialized) {
1777 qWarning() << "Warning: Changing 'typicalLength' after physics is initialized will have "
1778 "no effect";
1779 return;
1780 }
1781
1782 m_typicalLength = typicalLength;
1783
1784 emit typicalLengthChanged(typicalLength);
1785}
1786
1787void QPhysicsWorld::setTypicalSpeed(float typicalSpeed)
1788{
1789 if (qFuzzyCompare(typicalSpeed, m_typicalSpeed))
1790 return;
1791
1792 if (!qIsFinite(typicalSpeed) || typicalSpeed <= 0.f) {
1793 qWarning() << "Warning: 'typicalSpeed' must be finite and greater than zero, ignored";
1794 return;
1795 }
1796
1797 if (m_physicsInitialized) {
1798 qWarning() << "Warning: Changing 'typicalSpeed' after physics is initialized will have "
1799 "no effect";
1800 return;
1801 }
1802
1803 m_typicalSpeed = typicalSpeed;
1804
1805 emit typicalSpeedChanged(typicalSpeed);
1806}
1807
1808float QPhysicsWorld::defaultDensity() const
1809{
1810 return m_defaultDensity;
1811}
1812
1813float QPhysicsWorld::minimumTimestep() const
1814{
1815 return m_minTimestep;
1816}
1817
1818float QPhysicsWorld::maximumTimestep() const
1819{
1820 return m_maxTimestep;
1821}
1822
1823void QPhysicsWorld::setDefaultDensity(float defaultDensity)
1824{
1825 defaultDensity = qBound(0.0000001f, defaultDensity, PX_MAX_F32);
1826
1827 if (qFuzzyCompare(m_defaultDensity, defaultDensity))
1828 return;
1829 m_defaultDensity = defaultDensity;
1830
1831 // Go through all dynamic rigid bodies and update the default density
1832 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1833 if (body->frontendNode)
1834 body->updateDefaultDensity(m_defaultDensity);
1835 }
1836
1837 emit defaultDensityChanged(defaultDensity);
1838}
1839
1840// Remove physics world items that no longer exist
1841
1842void QPhysicsWorld::cleanupRemovedNodes()
1843{
1844 m_physXBodies.removeIf([this](QAbstractPhysXNode *body) {
1845 return body->cleanupIfRemoved(m_physx);
1846 });
1847 m_removedPhysicsNodes.clear();
1848}
1849
1850// Tell the triggers which nodes have had their shapes replaced, and drop what
1851// the reports that followed the last such replacement did not mention
1852
1853void QPhysicsWorld::invalidateTriggerOverlaps(QSpan<QAbstractPhysXNode *const> rebuiltBodies)
1854{
1855 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1856 auto *trigger = qobject_cast<QTriggerBody *>(body->frontendNode);
1857 if (!trigger)
1858 continue;
1859 for (QAbstractPhysXNode *rebuilt : rebuiltBodies) {
1860 // The rebuilt nodes are held as backends, which outlive the frame,
1861 // since syncing the frame can run a handler that deletes a node
1862 // that was rebuilt earlier in it. deregisterNode() takes the
1863 // frontend off the backend of whatever is deleted.
1864 if (rebuilt->frontendNode)
1865 trigger->invalidateOverlaps(rebuilt->frontendNode);
1866 }
1867 }
1868
1869 m_triggerOverlapsInvalidated = true;
1870}
1871
1872void QPhysicsWorld::dropUnreportedTriggerOverlaps()
1873{
1874 // Nothing is waiting to be reported again unless shapes were replaced, so
1875 // this stays off the frame of a scene that never rebuilds any.
1876 if (!m_triggerOverlapsInvalidated)
1877 return;
1878 m_triggerOverlapsInvalidated = false;
1879
1880 // A body reported as having left can be deleted from the handler, and so can
1881 // a trigger that has not been reached yet. deregisterNode() takes the
1882 // frontend off whatever is deleted, so what still has one here is what
1883 // is left.
1884 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1885 if (auto *trigger = qobject_cast<QTriggerBody *>(body->frontendNode))
1886 trigger->dropUnreportedOverlaps();
1887 }
1888}
1889
1890void QPhysicsWorld::cleanupRemovedJoints()
1891{
1892 for (physx::PxJoint *joint : m_removedJoints) {
1893 if (joint)
1894 joint->release();
1895 }
1896 m_removedJoints.clear();
1897}
1898
1899void QPhysicsWorld::initPhysics()
1900{
1901 Q_ASSERT(!m_physicsInitialized);
1902
1903 const unsigned int numThreads = m_numThreads >= 0 ? m_numThreads : qMax(0, QThread::idealThreadCount());
1904 m_physx->createScene(m_typicalLength, m_typicalSpeed, m_gravity, this, numThreads);
1905 m_frameAnimator->start();
1906 m_physicsInitialized = true;
1907}
1908
1909void QPhysicsWorld::simulateFrame()
1910{
1911 constexpr double MILLIONTH = 0.000001;
1912 constexpr double THOUSANDTH = 0.001;
1913
1914 if (m_inDesignStudio) {
1915 frameFinishedDesignStudio();
1916 return;
1917 }
1918
1919 if (!m_physx->isRunning) {
1920 m_timer.start();
1921 m_physx->isRunning = true;
1922 const double minTimestepSecs = m_minTimestep * 0.001;
1923 m_physx->scene->simulate(minTimestepSecs);
1924 m_currTimeStep = minTimestepSecs;
1925 return;
1926 }
1927
1928 // Frame not ready yet
1929 if (!m_frameFetched && !m_physx->scene->checkResults()) {
1930 return;
1931 }
1932
1933 // Frame ready, fetch and finish it
1934 if (!m_frameFetched) {
1935 m_physx->scene->fetchResults(true);
1936 frameFinished(m_currTimeStep);
1937 m_frameFetched = true;
1938 if (Q_UNLIKELY(!qtPhysicsTimingsFile.isEmpty())) {
1939 const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
1940 m_frameTimings.append(deltaMS);
1941 }
1942 }
1943
1944 // Assuming: 0 <= minTimestep <= maxTimestep
1945 const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
1946 if (deltaMS < m_minTimestep)
1947 return;
1948 const double deltaSecs = qMin<double>(deltaMS, m_maxTimestep) * THOUSANDTH;
1949 m_timer.restart();
1950 m_physx->scene->simulate(deltaSecs);
1951 m_frameFetched = false;
1952 m_currTimeStep = deltaSecs;
1953}
1954
1955void QPhysicsWorld::frameFinished(float deltaTime)
1956{
1957 matchOrphanNodes();
1958 matchOrphanJoints();
1959 matchOrphanRaycasters();
1960
1961 // One round of reports has been fetched since the shapes rebuilt below were
1962 // replaced, so whatever it did not mention is no longer overlapping. Kept
1963 // ahead of cleanupRemovedNodes(), like the contact callbacks, since a body
1964 // reported as having left can be deleted from the handler.
1965 dropUnreportedTriggerOverlaps();
1966
1967 emitContactCallbacks();
1968 cleanupRemovedNodes();
1969 cleanupRemovedJoints();
1970
1971 for (auto *node : std::as_const(m_newPhysicsNodes)) {
1972 auto *body = node->createPhysXBackend();
1973 body->init(this, m_physx);
1974 m_physXBodies.push_back(body);
1975 }
1976 m_newPhysicsNodes.clear();
1977
1978 QHash<QQuick3DNode *, QMatrix4x4> transformCache;
1979 QVarLengthArray<QAbstractPhysXNode *, 8> rebuiltBodies;
1980
1981 // TODO: Use dirty flag/dirty list to avoid redoing things that didn't change
1982 for (auto *physXBody : std::as_const(m_physXBodies)) {
1983 // Syncing runs a node's bindings, and a character controller reports
1984 // what it hits from here, so one can be deleted while this loop runs.
1985 if (!physXBody->frontendNode)
1986 continue;
1987
1988 physXBody->markDirtyShapes();
1989 const bool wasDirty = physXBody->shapesDirty();
1990 physXBody->rebuildDirtyShapes(this, m_physx);
1991
1992 // Rebuilding reports too, when it has to force a body kinematic.
1993 if (!physXBody->frontendNode)
1994 continue;
1995
1996 // Dirty before and clean after is the backend having replaced the shapes
1997 // of the node, which the triggers holding them need to hear about.
1998 if (wasDirty && !physXBody->shapesDirty())
1999 rebuiltBodies.append(physXBody);
2000 physXBody->updateFilters();
2001
2002 // Sync the physics world and the scene
2003 physXBody->sync(deltaTime, transformCache);
2004 }
2005
2006 if (!rebuiltBodies.isEmpty())
2007 invalidateTriggerOverlaps(rebuiltBodies);
2008
2009 for (QPhysicsJoint *joint : std::as_const(m_joints)) {
2010 joint->updatePhysXBackend();
2011 }
2012
2013 const QList<QRaycaster *> raycasters = m_raycasters;
2014 for (QRaycaster *raycaster : raycasters) {
2015 if (m_raycasters.contains(raycaster))
2016 raycaster->updateRaycast(this);
2017 }
2018
2019 updateDebugDraw();
2020 emit frameDone(deltaTime * 1000);
2021}
2022
2023void QPhysicsWorld::frameFinishedDesignStudio()
2024{
2025 // Note sure if this is needed but do it anyway
2026 matchOrphanNodes();
2027 matchOrphanJoints();
2028 // Matched but never cast: no backends are created here, so the PhysX scene has no
2029 // bodies for a ray to hit.
2030 matchOrphanRaycasters();
2031 emitContactCallbacks();
2032 cleanupRemovedNodes();
2033 // Ignore new physics nodes, we find them from the scene node anyway
2034 m_newPhysicsNodes.clear();
2035
2036 updateDebugDrawDesignStudio();
2037}
2038
2039QPhysicsWorld *QPhysicsWorld::getWorld(QQuick3DNode *node)
2040{
2041 for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
2042 if (!world->m_scene) {
2043 continue;
2044 }
2045
2046 QQuick3DNode *nodeCurr = node;
2047
2048 // Maybe pointless but check starting node
2049 if (nodeCurr == world->m_scene)
2050 return world;
2051
2052 while (nodeCurr->parentNode()) {
2053 nodeCurr = nodeCurr->parentNode();
2054 if (nodeCurr == world->m_scene)
2055 return world;
2056 }
2057 }
2058
2059 return nullptr;
2060}
2061
2062QPhysicsWorld *QPhysicsWorld::getWorld(QPhysicsJoint *joint)
2063{
2064 for (QPhysicsWorld *world : worldManager.worlds) {
2065 if (!world->m_scene) {
2066 continue;
2067 }
2068
2069 QObject *nodeCurr = joint;
2070
2071 while (nodeCurr->parent()) {
2072 nodeCurr = nodeCurr->parent();
2073 if (nodeCurr == world->m_scene) {
2074 return world;
2075 } else if (auto view3d = qobject_cast<QQuick3DViewport *>(nodeCurr);
2076 view3d && view3d->scene() == world->m_scene) {
2077 // HACK? if the parent is a view3d, check against its "implicit" scene
2078 return world;
2079 }
2080 }
2081 }
2082
2083 return nullptr;
2084}
2085
2086void QPhysicsWorld::matchOrphanNodes()
2087{
2088 // FIXME: does this need thread safety?
2089 if (worldManager.orphanNodes.isEmpty())
2090 return;
2091
2092 qsizetype numNodes = worldManager.orphanNodes.length();
2093 qsizetype idx = 0;
2094
2095 while (idx < numNodes) {
2096 auto node = worldManager.orphanNodes[idx];
2097 auto world = getWorld(node);
2098 if (world == this) {
2099 world->m_newPhysicsNodes.push_back(node);
2100 // swap-erase
2101 worldManager.orphanNodes.swapItemsAt(idx, numNodes - 1);
2102 worldManager.orphanNodes.pop_back();
2103 numNodes--;
2104 } else {
2105 idx++;
2106 }
2107 }
2108}
2109
2110void QPhysicsWorld::matchOrphanJoints()
2111{
2112 if (worldManager.orphanJoints.isEmpty())
2113 return;
2114
2115 qsizetype numNodes = worldManager.orphanJoints.length();
2116 qsizetype idx = 0;
2117
2118 while (idx < numNodes) {
2119 auto node = worldManager.orphanJoints[idx];
2120 auto world = getWorld(node);
2121 if (world == this) {
2122 world->m_joints.push_back(node);
2123 // swap-erase
2124 worldManager.orphanJoints.swapItemsAt(idx, numNodes - 1);
2125 worldManager.orphanJoints.pop_back();
2126 numNodes--;
2127 } else {
2128 idx++;
2129 }
2130 }
2131}
2132
2133void QPhysicsWorld::matchOrphanRaycasters()
2134{
2135 qsizetype idx = 0;
2136 while (idx < worldManager.orphanRaycasters.size()) {
2137 QRaycaster *raycaster = worldManager.orphanRaycasters.at(idx);
2138 if (getWorld(raycaster) == this) {
2139 m_raycasters.push_back(raycaster);
2140 worldManager.orphanRaycasters.removeAt(idx);
2141 } else {
2142 ++idx;
2143 }
2144 }
2145}
2146
2147void QPhysicsWorld::findPhysicsNodes()
2148{
2149 // This method finds the physics nodes inside the scene pointed to by the
2150 // scene property. This method is necessary to run whenever the scene
2151 // property is changed.
2152 if (m_scene == nullptr)
2153 return;
2154
2155 // Recursively go through all children and add all QAbstractPhysicsNode's
2156 QList<QQuick3DObject *> children = m_scene->childItems();
2157 while (!children.empty()) {
2158 auto child = children.takeFirst();
2159 if (auto converted = qobject_cast<QAbstractPhysicsNode *>(child); converted != nullptr) {
2160 // This should never happen but check anyway.
2161 if (converted->m_backendObject != nullptr) {
2162 qWarning() << "Warning: physics node already associated with a backend node.";
2163 continue;
2164 }
2165
2166 m_newPhysicsNodes.push_back(converted);
2167 worldManager.orphanNodes.removeAll(converted); // No longer orphan
2168 }
2169 children.append(child->childItems());
2170 }
2171}
2172
2173void QPhysicsWorld::emitContactCallbacks()
2174{
2175 for (const QPhysicsWorld::BodyContact &contact : std::as_const(m_registeredContacts)) {
2176 if (m_removedPhysicsNodes.contains(contact.sender)
2177 || m_removedPhysicsNodes.contains(contact.receiver))
2178 continue;
2179 contact.receiver->registerContact(contact.sender, contact.positions, contact.impulses,
2180 contact.normals);
2181 }
2182
2183 m_registeredContacts.clear();
2184}
2185
2186physx::PxPhysics *QPhysicsWorld::getPhysics()
2187{
2188 return StaticPhysXObjects::getReference().physics;
2189}
2190
2191const physx::PxCookingParams *QPhysicsWorld::getCookingParams()
2192{
2193 // PhysX cooks meshes through free functions taking the parameters, instead of through a
2194 // cooking object, but they still need the foundation for allocation and error reporting,
2195 // so there is nothing to cook with until it has been created.
2196 if (!StaticPhysXObjects::getReference().foundationCreated)
2197 return nullptr;
2198
2199 // FIXME: does the tolerance matter?
2200 static const physx::PxCookingParams params { physx::PxTolerancesScale() };
2201 return &params;
2202}
2203
2204physx::PxControllerManager *QPhysicsWorld::controllerManager()
2205{
2206 if (m_physx->scene && !m_physx->controllerManager) {
2207 m_physx->controllerManager = PxCreateControllerManager(*m_physx->scene);
2208 qCDebug(lcQuick3dPhysics) << "Created controller manager" << m_physx->controllerManager;
2209 }
2210 return m_physx->controllerManager;
2211}
2212
2213QQuick3DNode *QPhysicsWorld::scene() const
2214{
2215 return m_scene;
2216}
2217
2218void QPhysicsWorld::setScene(QQuick3DNode *newScene)
2219{
2220 if (m_scene == newScene)
2221 return;
2222
2223 m_scene = newScene;
2224
2225 // Delete all nodes since they are associated with the previous scene. One
2226 // deleted from a report earlier in this frame has nothing left to
2227 // deregister.
2228 for (auto body : std::as_const(m_physXBodies)) {
2229 if (body->frontendNode)
2230 deregisterNode(body->frontendNode);
2231 }
2232
2233 // Raycasters are not physics nodes and have no backend to tear down, so hand them back
2234 // to be matched against the new scene below, without a hit from the old one. Clearing
2235 // reports to QML, where a handler can delete a raycaster, hence the check.
2236 const QList<QRaycaster *> raycasters = std::exchange(m_raycasters, {});
2237 worldManager.orphanRaycasters += raycasters;
2238 for (QRaycaster *raycaster : raycasters) {
2239 if (worldManager.orphanRaycasters.contains(raycaster))
2240 raycaster->clearHit();
2241 }
2242
2243 // Check if scene is already used by another world
2244 bool sceneOK = true;
2245 for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
2246 if (world != this && world->scene() == newScene) {
2247 sceneOK = false;
2248 qWarning() << "Warning: scene already associated with physics world";
2249 }
2250 }
2251
2252 if (sceneOK) {
2253 findPhysicsNodes();
2254 matchOrphanRaycasters();
2255 }
2256 emit sceneChanged();
2257}
2258
2259int QPhysicsWorld::numThreads() const
2260{
2261 return m_numThreads;
2262}
2263
2264void QPhysicsWorld::setNumThreads(int newNumThreads)
2265{
2266 newNumThreads = qMin(newNumThreads, 256);
2267
2268 if (m_numThreads == newNumThreads)
2269 return;
2270
2271 m_numThreads = newNumThreads;
2272 emit numThreadsChanged();
2273}
2274
2275bool QPhysicsWorld::reportKinematicKinematicCollisions() const
2276{
2277 return m_reportKinematicKinematicCollisions;
2278}
2279
2280void QPhysicsWorld::setReportKinematicKinematicCollisions(
2281 bool newReportKinematicKinematicCollisions)
2282{
2283 if (m_reportKinematicKinematicCollisions == newReportKinematicKinematicCollisions)
2284 return;
2285 m_reportKinematicKinematicCollisions = newReportKinematicKinematicCollisions;
2286 emit reportKinematicKinematicCollisionsChanged();
2287}
2288
2289bool QPhysicsWorld::reportStaticKinematicCollisions() const
2290{
2291 return m_reportStaticKinematicCollisions;
2292}
2293
2294void QPhysicsWorld::setReportStaticKinematicCollisions(bool newReportStaticKinematicCollisions)
2295{
2296 if (m_reportStaticKinematicCollisions == newReportStaticKinematicCollisions)
2297 return;
2298 m_reportStaticKinematicCollisions = newReportStaticKinematicCollisions;
2299 emit reportStaticKinematicCollisionsChanged();
2300}
2301
2302QPhysicsWorld::QueryStructure QPhysicsWorld::staticQueryStructure() const
2303{
2304 return m_staticQueryStructure;
2305}
2306
2307void QPhysicsWorld::setStaticQueryStructure(QueryStructure newStaticQueryStructure)
2308{
2309 if (m_staticQueryStructure == newStaticQueryStructure)
2310 return;
2311
2312 if (newStaticQueryStructure == QueryStructure::NoStructure) {
2313 qWarning()
2314 << "Warning: The QueryStructure::NoStructure is not supported for staticQueryStructure. Available options: [StaticTree, DynamicTree]";
2315 return;
2316 }
2317
2318 if (m_physicsInitialized) {
2319 qWarning()
2320 << "Warning: Changing 'staticQueryStructure' after physics is initialized will have no effect";
2321 return;
2322 }
2323
2324 m_staticQueryStructure = newStaticQueryStructure;
2325 emit staticQueryStructureChanged();
2326}
2327
2328QPhysicsWorld::QueryStructure QPhysicsWorld::dynamicQueryStructure() const
2329{
2330 return m_dynamicQueryStructure;
2331}
2332
2333void QPhysicsWorld::setDynamicQueryStructure(QueryStructure newDynamicQueryStructure)
2334{
2335 if (m_dynamicQueryStructure == newDynamicQueryStructure)
2336 return;
2337
2338 if (m_physicsInitialized) {
2339 qWarning()
2340 << "Warning: Changing 'dynamicQueryStructure' after physics is initialized will have no effect";
2341 return;
2342 }
2343
2344 m_dynamicQueryStructure = newDynamicQueryStructure;
2345 emit dynamicQueryStructureChanged();
2346}
2347
2348bool QPhysicsWorld::testRaycastQuery(const QVector3D &origin,
2349 const QVector3D &direction,
2350 float maxDistance,
2351 bool includeStatic,
2352 bool includeDynamic) const
2353{
2354 physx::PxRaycastBuffer hitBuffer;
2355 return raycastImpl(m_physx->scene, origin, direction,
2356 maxDistance, includeStatic, includeDynamic,
2357 hitBuffer,
2358 physx::PxHitFlags(),
2359 physx::PxQueryFlag::eANY_HIT);
2360}
2361
2362QQuick3DPhysicsLocationHit QPhysicsWorld::singleRaycastQuery(const QVector3D &origin,
2363 const QVector3D &direction,
2364 float maxDistance,
2365 bool includeStatic,
2366 bool includeDynamic) const
2367{
2368 physx::PxRaycastBuffer hitBuffer;
2369 const bool status = raycastImpl(m_physx->scene, origin, direction,
2370 maxDistance, includeStatic, includeDynamic,
2371 hitBuffer,
2372 physx::PxHitFlag::eDEFAULT);
2373
2374 if (status && hitBuffer.hasBlock) {
2375 return QQuick3DPhysicsLocationHit(hitBuffer.block,
2376 hitBuffer.block);
2377 }
2378
2379 return {};
2380}
2381
2382QList<QQuick3DPhysicsLocationHit> QPhysicsWorld::multiRaycastQuery(const QVector3D &origin,
2383 const QVector3D &direction,
2384 float maxDistance,
2385 bool includeStatic,
2386 bool includeDynamic) const
2387{
2388 DynamicQueryCallback<physx::PxRaycastCallback,
2389 QQuick3DPhysicsLocationHit,
2390 physx::PxRaycastHit> hitBuffer;
2391
2392 if (raycastImpl(m_physx->scene, origin, direction,
2393 maxDistance, includeStatic, includeDynamic,
2394 hitBuffer,
2395 physx::PxHitFlag::eDEFAULT,
2396 physx::PxQueryFlag::eNO_BLOCK)) {
2397
2398 return hitBuffer.hits;
2399 }
2400
2401 return {};
2402}
2403
2404QQuick3DPhysicsLocationHit
2405QPhysicsWorld::closestRaycastHit(const QVector3D &origin, const QVector3D &direction,
2406 float maxDistance, bool includeStatic, bool includeDynamic,
2407 QSpan<const QAbstractPhysicsNode *const> excludedBodies) const
2408{
2409 ExcludedBodiesFilter filter(excludedBodies);
2410 physx::PxRaycastBuffer hitBuffer;
2411 const bool status = raycastImpl(m_physx->scene, origin, direction, maxDistance, includeStatic,
2412 includeDynamic, hitBuffer, physx::PxHitFlag::eDEFAULT,
2413 physx::PxQueryFlag::ePREFILTER, &filter);
2414
2415 if (status && hitBuffer.hasBlock)
2416 return QQuick3DPhysicsLocationHit(hitBuffer.block, hitBuffer.block);
2417
2418 return {};
2419}
2420
2421bool QPhysicsWorld::testSweepQuery(QAbstractCollisionShape *shape,
2422 const QVector3D &direction,
2423 float maxDistance,
2424 bool includeStatic,
2425 bool includeDynamic) const
2426{
2427 physx::PxSweepBuffer hitBuffer;
2428 return sweepImpl(m_physx->scene, shape, direction,
2429 maxDistance, includeStatic, includeDynamic,
2430 hitBuffer,
2431 physx::PxHitFlags(),
2432 physx::PxQueryFlag::eANY_HIT);
2433}
2434
2435QQuick3DPhysicsLocationHit QPhysicsWorld::singleSweepQuery(QAbstractCollisionShape *shape,
2436 const QVector3D &direction,
2437 float maxDistance,
2438 bool includeStatic,
2439 bool includeDynamic) const
2440{
2441 physx::PxSweepBuffer hitBuffer;
2442 const bool status = sweepImpl(m_physx->scene, shape,
2443 direction, maxDistance, includeStatic, includeDynamic,
2444 hitBuffer,
2445 physx::PxHitFlag::eDEFAULT | physx::PxHitFlag::eMTD);
2446
2447 if (status && hitBuffer.hasBlock) {
2448 return QQuick3DPhysicsLocationHit(hitBuffer.block, hitBuffer.block);
2449 }
2450
2451 return {};
2452}
2453
2454QList<QQuick3DPhysicsLocationHit> QPhysicsWorld::multiSweepQuery(QAbstractCollisionShape *shape,
2455 const QVector3D &direction,
2456 float maxDistance,
2457 bool includeStatic,
2458 bool includeDynamic) const
2459{
2460 DynamicQueryCallback<physx::PxSweepCallback,
2461 QQuick3DPhysicsLocationHit,
2462 physx::PxSweepHit> hitBuffer;
2463
2464 if (sweepImpl(m_physx->scene, shape,
2465 direction, maxDistance, includeStatic, includeDynamic,
2466 hitBuffer,
2467 physx::PxHitFlag::eDEFAULT | physx::PxHitFlag::eMTD,
2468 physx::PxQueryFlag::eNO_BLOCK)) {
2469 return hitBuffer.hits;
2470 }
2471
2472 return {};
2473}
2474
2475bool QPhysicsWorld::testOverlapQuery(QAbstractCollisionShape *shape,
2476 bool includeStatic,
2477 bool includeDynamic) const
2478{
2479 physx::PxOverlapBuffer hitBuffer;
2480 return overlapImpl(m_physx->scene,
2481 shape, includeStatic, includeDynamic,
2482 hitBuffer,
2483 physx::PxQueryFlag::eANY_HIT);
2484}
2485
2486QList<QQuick3DPhysicsQueryHit> QPhysicsWorld::multiOverlapQuery(QAbstractCollisionShape *shape,
2487 bool includeStatic,
2488 bool includeDynamic) const
2489{
2490 DynamicQueryCallback<physx::PxOverlapCallback,
2491 QQuick3DPhysicsQueryHit,
2492 physx::PxOverlapHit> hitBuffer;
2493
2494 if (overlapImpl(m_physx->scene,
2495 shape, includeStatic, includeDynamic,
2496 hitBuffer,
2497 physx::PxQueryFlag::eNO_BLOCK)) {
2498 return hitBuffer.hits;
2499 }
2500
2501 return {};
2502}
2503
2505
2506#include "qphysicsworld.moc"
Definition qlist.h:82
The QVector3D class represents a vector or vertex in 3D space.
Definition qvectornd.h:178
PxTransformT< float > PxTransform
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
static QWorldManager worldManager
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)
static bool validateQuery(const physx::PxScene *scene, bool includeStatic, bool includeDynamic)
static const physx::PxGeometry * validateQueryShape(const physx::PxScene *scene, QAbstractCollisionShape *shape, bool includeStatic, bool includeDynamic)
static void collectPhysicsNodes(QQuick3DObject *node, QList< QAbstractPhysicsNode * > &nodes)
static const QString qtPhysicsTimingsFile
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={})
static bool overlapImpl(const physx::PxScene *scene, QAbstractCollisionShape *shape, bool includeStatic, bool includeDynamic, physx::PxOverlapCallback &hitCallback, physx::PxQueryFlags extraQueryFlags={})
#define QT_BEGIN_NAMESPACE
#define QT_END_NAMESPACE
#define Q_OBJECT
#define emit
QVector< QAbstractPhysicsNode * > orphanNodes
QVector< QPhysicsWorld * > worlds
QVector< QRaycaster * > orphanRaycasters
QVector< QPhysicsJoint * > orphanJoints