1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qphysicsworld_p.h" |
5 | |
6 | #include "physxnode/qabstractphysxnode_p.h" |
7 | #include "physxnode/qphysxworld_p.h" |
8 | #include "qabstractphysicsnode_p.h" |
9 | #include "qdebugdrawhelper_p.h" |
10 | #include "qphysicsutils_p.h" |
11 | #include "qstaticphysxobjects_p.h" |
12 | #include "qboxshape_p.h" |
13 | #include "qsphereshape_p.h" |
14 | #include "qconvexmeshshape_p.h" |
15 | #include "qtrianglemeshshape_p.h" |
16 | #include "qcharactercontroller_p.h" |
17 | #include "qcapsuleshape_p.h" |
18 | #include "qplaneshape_p.h" |
19 | #include "qheightfieldshape_p.h" |
20 | |
21 | #include "PxPhysicsAPI.h" |
22 | #include "cooking/PxCooking.h" |
23 | |
24 | #include <QtQuick3D/private/qquick3dobject_p.h> |
25 | #include <QtQuick3D/private/qquick3dnode_p.h> |
26 | #include <QtQuick3D/private/qquick3dmodel_p.h> |
27 | #include <QtQuick3D/private/qquick3ddefaultmaterial_p.h> |
28 | #include <QtQuick3DUtils/private/qssgutils_p.h> |
29 | |
30 | #include <QtEnvironmentVariables> |
31 | |
32 | #define PHYSX_ENABLE_PVD 0 |
33 | |
34 | QT_BEGIN_NAMESPACE |
35 | |
36 | /*! |
37 | \qmltype PhysicsWorld |
38 | \inqmlmodule QtQuick3D.Physics |
39 | \since 6.4 |
40 | \brief Controls the physics simulation. |
41 | |
42 | The PhysicsWorld type controls the physics simulation. This node is used to create an instance of the physics world as well |
43 | as define its properties. There can only be one physics world. All collision nodes in the qml |
44 | will get added automatically to the physics world. |
45 | */ |
46 | |
47 | /*! |
48 | \qmlproperty vector3d PhysicsWorld::gravity |
49 | This property defines the gravity vector of the physics world. |
50 | The default value is \c (0, -981, 0). Set the value to \c{Qt.vector3d(0, -9.81, 0)} if your |
51 | unit of measurement is meters and you are simulating Earth gravity. |
52 | */ |
53 | |
54 | /*! |
55 | \qmlproperty bool PhysicsWorld::running |
56 | This property starts or stops the physical simulation. The default value is \c true. |
57 | */ |
58 | |
59 | /*! |
60 | \qmlproperty bool PhysicsWorld::forceDebugDraw |
61 | This property enables debug drawing of all active shapes in the physics world. The default value |
62 | is \c false. |
63 | */ |
64 | |
65 | /*! |
66 | \qmlproperty bool PhysicsWorld::enableCCD |
67 | This property enables continuous collision detection. This will reduce the risk of bodies going |
68 | through other bodies at high velocities (also known as tunnelling). The default value is \c |
69 | false. |
70 | */ |
71 | |
72 | /*! |
73 | \qmlproperty float PhysicsWorld::typicalLength |
74 | This property defines the approximate size of objects in the simulation. This is used to |
75 | estimate certain length-related tolerances. Objects much smaller or much larger than this |
76 | size may not behave properly. The default value is \c 100. |
77 | |
78 | Range: \c{[0, inf]} |
79 | */ |
80 | |
81 | /*! |
82 | \qmlproperty float PhysicsWorld::typicalSpeed |
83 | This property defines the typical magnitude of velocities of objects in simulation. This is used |
84 | to estimate whether a contact should be treated as bouncing or resting based on its impact |
85 | velocity, and a kinetic energy threshold below which the simulation may put objects to sleep. |
86 | |
87 | For normal physical environments, a good choice is the approximate speed of an object falling |
88 | under gravity for one second. The default value is \c 1000. |
89 | |
90 | Range: \c{[0, inf]} |
91 | */ |
92 | |
93 | /*! |
94 | \qmlproperty float PhysicsWorld::defaultDensity |
95 | This property defines the default density of dynamic objects, measured in kilograms per cubic |
96 | unit. This is equal to the weight of a cube with side \c 1. |
97 | |
98 | The default value is \c 0.001, corresponding to 1 g/cm³: the density of water. If your unit of |
99 | measurement is meters, a good value would be \c 1000. Note that only positive values are |
100 | allowed. |
101 | |
102 | Range: \c{(0, inf]} |
103 | */ |
104 | |
105 | /*! |
106 | \qmlproperty Node PhysicsWorld::viewport |
107 | This property defines the viewport where debug components will be drawn if \l{forceDebugDraw} |
108 | is enabled. If unset the \l{scene} node will be used. |
109 | |
110 | \sa forceDebugDraw, scene |
111 | */ |
112 | |
113 | /*! |
114 | \qmlproperty float PhysicsWorld::minimumTimestep |
115 | This property defines the minimum simulation timestep in milliseconds. The default value is |
116 | \c 16.667 which corresponds to \c 60 frames per second. |
117 | |
118 | Range: \c{[0, maximumTimestep]} |
119 | */ |
120 | |
121 | /*! |
122 | \qmlproperty float PhysicsWorld::maximumTimestep |
123 | This property defines the maximum simulation timestep in milliseconds. The default value is |
124 | \c 33.333 which corresponds to \c 30 frames per second. |
125 | |
126 | Range: \c{[0, inf]} |
127 | */ |
128 | |
129 | /*! |
130 | \qmlproperty Node PhysicsWorld::scene |
131 | |
132 | This property defines the top-most Node that contains all the nodes of the physical |
133 | simulation. All physics objects that are an ancestor of this node will be seen as part of this |
134 | PhysicsWorld. |
135 | |
136 | \note Using the same scene node for several PhysicsWorld is unsupported. |
137 | */ |
138 | |
139 | /*! |
140 | \qmlsignal PhysicsWorld::frameDone(float timestep) |
141 | \since 6.5 |
142 | |
143 | This signal is emitted when the physical simulation is done simulating a frame. The \a timestep |
144 | parameter is how long in milliseconds the timestep was in the simulation. |
145 | */ |
146 | |
147 | Q_LOGGING_CATEGORY(lcQuick3dPhysics, "qt.quick3d.physics" ); |
148 | |
149 | static const QQuaternion kMinus90YawRotation = QQuaternion::fromEulerAngles(pitch: 0, yaw: -90, roll: 0); |
150 | |
151 | ///////////////////////////////////////////////////////////////////////////// |
152 | |
153 | class SimulationWorker : public QObject |
154 | { |
155 | Q_OBJECT |
156 | public: |
157 | SimulationWorker(QPhysXWorld *physx) : m_physx(physx) { } |
158 | |
159 | public slots: |
160 | void simulateFrame(float minTimestep, float maxTimestep) |
161 | { |
162 | if (!m_physx->isRunning) { |
163 | m_timer.start(); |
164 | m_physx->isRunning = true; |
165 | } |
166 | |
167 | // Assuming: 0 <= minTimestep <= maxTimestep |
168 | |
169 | constexpr auto MILLIONTH = 0.000001; |
170 | |
171 | // If not enough time has elapsed we sleep until it has |
172 | auto deltaMS = m_timer.nsecsElapsed() * MILLIONTH; |
173 | while (deltaMS < minTimestep) { |
174 | auto sleepUSecs = (minTimestep - deltaMS) * 1000.f; |
175 | QThread::usleep(sleepUSecs); |
176 | deltaMS = m_timer.nsecsElapsed() * MILLIONTH; |
177 | } |
178 | m_timer.restart(); |
179 | |
180 | auto deltaSecs = qMin(a: float(deltaMS), b: maxTimestep) * 0.001f; |
181 | m_physx->scene->simulate(elapsedTime: deltaSecs); |
182 | m_physx->scene->fetchResults(block: true); |
183 | |
184 | emit frameDone(deltaTime: deltaSecs); |
185 | } |
186 | |
187 | void simulateFrameDesignStudio(float minTimestep, float maxTimestep) |
188 | { |
189 | Q_UNUSED(minTimestep); |
190 | Q_UNUSED(maxTimestep); |
191 | auto sleepUSecs = 16 * 1000.f; // 16 ms |
192 | QThread::usleep(sleepUSecs); |
193 | emit frameDoneDesignStudio(); |
194 | } |
195 | |
196 | signals: |
197 | void frameDone(float deltaTime); |
198 | void frameDoneDesignStudio(); |
199 | |
200 | private: |
201 | QPhysXWorld *m_physx = nullptr; |
202 | QElapsedTimer m_timer; |
203 | }; |
204 | |
205 | ///////////////////////////////////////////////////////////////////////////// |
206 | |
207 | struct QWorldManager |
208 | { |
209 | QVector<QPhysicsWorld *> worlds; |
210 | QVector<QAbstractPhysicsNode *> orphanNodes; |
211 | }; |
212 | |
213 | static QWorldManager worldManager = QWorldManager {}; |
214 | |
215 | void QPhysicsWorld::registerNode(QAbstractPhysicsNode *physicsNode) |
216 | { |
217 | auto world = getWorld(node: physicsNode); |
218 | if (world) { |
219 | world->m_newPhysicsNodes.push_back(t: physicsNode); |
220 | } else { |
221 | worldManager.orphanNodes.push_back(t: physicsNode); |
222 | } |
223 | } |
224 | |
225 | void QPhysicsWorld::deregisterNode(QAbstractPhysicsNode *physicsNode) |
226 | { |
227 | for (auto world : worldManager.worlds) { |
228 | world->m_newPhysicsNodes.removeAll(t: physicsNode); |
229 | if (physicsNode->m_backendObject) { |
230 | physicsNode->m_backendObject->isRemoved = true; |
231 | physicsNode->m_backendObject = nullptr; |
232 | } |
233 | QMutexLocker locker(&world->m_removedPhysicsNodesMutex); |
234 | world->m_removedPhysicsNodes.insert(value: physicsNode); |
235 | } |
236 | worldManager.orphanNodes.removeAll(t: physicsNode); |
237 | } |
238 | |
239 | QPhysicsWorld::QPhysicsWorld(QObject *parent) : QObject(parent) |
240 | { |
241 | m_inDesignStudio = !qEnvironmentVariableIsEmpty(varName: "QML_PUPPET_MODE" ); |
242 | m_physx = new QPhysXWorld; |
243 | m_physx->createWorld(); |
244 | |
245 | worldManager.worlds.push_back(t: this); |
246 | matchOrphanNodes(); |
247 | } |
248 | |
249 | QPhysicsWorld::~QPhysicsWorld() |
250 | { |
251 | m_workerThread.quit(); |
252 | m_workerThread.wait(); |
253 | for (auto body : m_physXBodies) { |
254 | body->cleanup(m_physx); |
255 | delete body; |
256 | } |
257 | m_physx->deleteWorld(); |
258 | delete m_physx; |
259 | worldManager.worlds.removeAll(t: this); |
260 | } |
261 | |
262 | void QPhysicsWorld::classBegin() {} |
263 | |
264 | void QPhysicsWorld::componentComplete() |
265 | { |
266 | if ((!m_running && !m_inDesignStudio) || m_physicsInitialized) |
267 | return; |
268 | initPhysics(); |
269 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
270 | } |
271 | |
272 | QVector3D QPhysicsWorld::gravity() const |
273 | { |
274 | return m_gravity; |
275 | } |
276 | |
277 | bool QPhysicsWorld::running() const |
278 | { |
279 | return m_running; |
280 | } |
281 | |
282 | bool QPhysicsWorld::forceDebugDraw() const |
283 | { |
284 | return m_forceDebugDraw; |
285 | } |
286 | |
287 | bool QPhysicsWorld::enableCCD() const |
288 | { |
289 | return m_enableCCD; |
290 | } |
291 | |
292 | float QPhysicsWorld::typicalLength() const |
293 | { |
294 | return m_typicalLength; |
295 | } |
296 | |
297 | float QPhysicsWorld::typicalSpeed() const |
298 | { |
299 | return m_typicalSpeed; |
300 | } |
301 | |
302 | bool QPhysicsWorld::isNodeRemoved(QAbstractPhysicsNode *object) |
303 | { |
304 | return m_removedPhysicsNodes.contains(value: object); |
305 | } |
306 | |
307 | void QPhysicsWorld::setGravity(QVector3D gravity) |
308 | { |
309 | if (m_gravity == gravity) |
310 | return; |
311 | |
312 | m_gravity = gravity; |
313 | if (m_physx->scene) { |
314 | m_physx->scene->setGravity(QPhysicsUtils::toPhysXType(qvec: m_gravity)); |
315 | } |
316 | emit gravityChanged(gravity: m_gravity); |
317 | } |
318 | |
319 | void QPhysicsWorld::setRunning(bool running) |
320 | { |
321 | if (m_running == running) |
322 | return; |
323 | |
324 | m_running = running; |
325 | if (!m_inDesignStudio) { |
326 | if (m_running && !m_physicsInitialized) |
327 | initPhysics(); |
328 | if (m_running) |
329 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
330 | } |
331 | emit runningChanged(running: m_running); |
332 | } |
333 | |
334 | void QPhysicsWorld::setForceDebugDraw(bool forceDebugDraw) |
335 | { |
336 | if (m_forceDebugDraw == forceDebugDraw) |
337 | return; |
338 | |
339 | m_forceDebugDraw = forceDebugDraw; |
340 | if (!m_forceDebugDraw) |
341 | disableDebugDraw(); |
342 | else |
343 | updateDebugDraw(); |
344 | emit forceDebugDrawChanged(forceDebugDraw: m_forceDebugDraw); |
345 | } |
346 | |
347 | QQuick3DNode *QPhysicsWorld::viewport() const |
348 | { |
349 | return m_viewport; |
350 | } |
351 | |
352 | void QPhysicsWorld::setHasIndividualDebugDraw() |
353 | { |
354 | m_hasIndividualDebugDraw = true; |
355 | } |
356 | |
357 | void QPhysicsWorld::setViewport(QQuick3DNode *viewport) |
358 | { |
359 | if (m_viewport == viewport) |
360 | return; |
361 | |
362 | m_viewport = viewport; |
363 | |
364 | // TODO: test this |
365 | for (auto material : m_debugMaterials) |
366 | delete material; |
367 | m_debugMaterials.clear(); |
368 | |
369 | for (auto &holder : m_collisionShapeDebugModels) { |
370 | delete holder.model; |
371 | } |
372 | m_collisionShapeDebugModels.clear(); |
373 | |
374 | emit viewportChanged(viewport: m_viewport); |
375 | } |
376 | |
377 | void QPhysicsWorld::setMinimumTimestep(float minTimestep) |
378 | { |
379 | if (qFuzzyCompare(p1: m_minTimestep, p2: minTimestep)) |
380 | return; |
381 | |
382 | if (minTimestep > m_maxTimestep) { |
383 | qWarning(msg: "Minimum timestep greater than maximum timestep, value clamped" ); |
384 | minTimestep = qMin(a: minTimestep, b: m_maxTimestep); |
385 | } |
386 | |
387 | if (minTimestep < 0.f) { |
388 | qWarning(msg: "Minimum timestep less than zero, value clamped" ); |
389 | minTimestep = qMax(a: minTimestep, b: 0.f); |
390 | } |
391 | |
392 | if (qFuzzyCompare(p1: m_minTimestep, p2: minTimestep)) |
393 | return; |
394 | |
395 | m_minTimestep = minTimestep; |
396 | emit minimumTimestepChanged(minimumTimestep: m_minTimestep); |
397 | } |
398 | |
399 | void QPhysicsWorld::setMaximumTimestep(float maxTimestep) |
400 | { |
401 | if (qFuzzyCompare(p1: m_maxTimestep, p2: maxTimestep)) |
402 | return; |
403 | |
404 | if (maxTimestep < 0.f) { |
405 | qWarning(msg: "Maximum timestep less than zero, value clamped" ); |
406 | maxTimestep = qMax(a: maxTimestep, b: 0.f); |
407 | } |
408 | |
409 | if (qFuzzyCompare(p1: m_maxTimestep, p2: maxTimestep)) |
410 | return; |
411 | |
412 | m_maxTimestep = maxTimestep; |
413 | emit maximumTimestepChanged(maxTimestep); |
414 | } |
415 | |
416 | void QPhysicsWorld::setupDebugMaterials(QQuick3DNode *sceneNode) |
417 | { |
418 | if (!m_debugMaterials.isEmpty()) |
419 | return; |
420 | |
421 | const int lineWidth = m_inDesignStudio ? 1 : 3; |
422 | |
423 | // These colors match the indices of DebugDrawBodyType enum |
424 | for (auto color : { QColorConstants::Svg::chartreuse, QColorConstants::Svg::cyan, |
425 | QColorConstants::Svg::lightsalmon, QColorConstants::Svg::red, |
426 | QColorConstants::Svg::black }) { |
427 | auto debugMaterial = new QQuick3DDefaultMaterial(); |
428 | debugMaterial->setLineWidth(lineWidth); |
429 | debugMaterial->setParentItem(sceneNode); |
430 | debugMaterial->setParent(sceneNode); |
431 | debugMaterial->setDiffuseColor(color); |
432 | debugMaterial->setLighting(QQuick3DDefaultMaterial::NoLighting); |
433 | debugMaterial->setCullMode(QQuick3DMaterial::NoCulling); |
434 | m_debugMaterials.push_back(t: debugMaterial); |
435 | } |
436 | } |
437 | |
438 | void QPhysicsWorld::updateDebugDraw() |
439 | { |
440 | if (!(m_forceDebugDraw || m_hasIndividualDebugDraw)) { |
441 | // Nothing to draw, trash all previous models (if any) and return |
442 | if (!m_collisionShapeDebugModels.isEmpty()) { |
443 | for (const auto& holder : std::as_const(t&: m_collisionShapeDebugModels)) |
444 | delete holder.model; |
445 | m_collisionShapeDebugModels.clear(); |
446 | } |
447 | return; |
448 | } |
449 | |
450 | // Use scene node if no viewport has been specified |
451 | auto sceneNode = m_viewport ? m_viewport : m_scene; |
452 | |
453 | if (sceneNode == nullptr) |
454 | return; |
455 | |
456 | setupDebugMaterials(sceneNode); |
457 | m_hasIndividualDebugDraw = false; |
458 | |
459 | // Store the collision shapes we have now so we can clear out the removed ones |
460 | QSet<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>> currentCollisionShapes; |
461 | currentCollisionShapes.reserve(size: m_collisionShapeDebugModels.size()); |
462 | |
463 | for (QAbstractPhysXNode *node : m_physXBodies) { |
464 | if (!node->debugGeometryCapability()) |
465 | continue; |
466 | |
467 | const auto &collisionShapes = node->frontendNode->getCollisionShapesList(); |
468 | const int materialIdx = static_cast<int>(node->getDebugDrawBodyType()); |
469 | const int length = collisionShapes.length(); |
470 | if (node->shapes.length() < length) |
471 | continue; // CharacterController has shapes, but not PhysX shapes |
472 | for (int idx = 0; idx < length; idx++) { |
473 | const auto collisionShape = collisionShapes[idx]; |
474 | |
475 | if (!m_forceDebugDraw && !collisionShape->enableDebugDraw()) |
476 | continue; |
477 | |
478 | const auto physXShape = node->shapes[idx]; |
479 | DebugModelHolder &holder = |
480 | m_collisionShapeDebugModels[std::make_pair(x: collisionShape, y&: node)]; |
481 | auto &model = holder.model; |
482 | |
483 | currentCollisionShapes.insert(value: std::make_pair(x: collisionShape, y&: node)); |
484 | |
485 | m_hasIndividualDebugDraw = |
486 | m_hasIndividualDebugDraw || collisionShape->enableDebugDraw(); |
487 | |
488 | auto localPose = physXShape->getLocalPose(); |
489 | |
490 | // Create/Update debug view infrastructure |
491 | if (!model) { |
492 | model = new QQuick3DModel(); |
493 | model->setParentItem(sceneNode); |
494 | model->setParent(sceneNode); |
495 | model->setCastsShadows(false); |
496 | model->setReceivesShadows(false); |
497 | model->setCastsReflections(false); |
498 | } |
499 | |
500 | { // update or set material |
501 | auto material = m_debugMaterials[materialIdx]; |
502 | QQmlListReference materialsRef(model, "materials" ); |
503 | if (materialsRef.count() == 0 || materialsRef.at(0) != material) { |
504 | materialsRef.clear(); |
505 | materialsRef.append(material); |
506 | } |
507 | } |
508 | |
509 | switch (physXShape->getGeometryType()) { |
510 | case physx::PxGeometryType::eBOX: { |
511 | physx::PxBoxGeometry boxGeometry; |
512 | physXShape->getBoxGeometry(geometry&: boxGeometry); |
513 | const auto &halfExtentsOld = holder.halfExtents(); |
514 | const auto halfExtents = QPhysicsUtils::toQtType(vec: boxGeometry.halfExtents); |
515 | if (!qFuzzyCompare(v1: halfExtentsOld, v2: halfExtents)) { |
516 | auto geom = QDebugDrawHelper::generateBoxGeometry(halfExtents); |
517 | geom->setParent(model); |
518 | model->setGeometry(geom); |
519 | holder.setHalfExtents(halfExtents); |
520 | } |
521 | |
522 | } |
523 | break; |
524 | |
525 | case physx::PxGeometryType::eSPHERE: { |
526 | physx::PxSphereGeometry sphereGeometry; |
527 | physXShape->getSphereGeometry(geometry&: sphereGeometry); |
528 | const float radius = holder.radius(); |
529 | if (!qFuzzyCompare(p1: sphereGeometry.radius, p2: radius)) { |
530 | auto geom = QDebugDrawHelper::generateSphereGeometry(radius: sphereGeometry.radius); |
531 | geom->setParent(model); |
532 | model->setGeometry(geom); |
533 | holder.setRadius(sphereGeometry.radius); |
534 | } |
535 | } |
536 | break; |
537 | |
538 | case physx::PxGeometryType::eCAPSULE: { |
539 | physx::PxCapsuleGeometry capsuleGeometry; |
540 | physXShape->getCapsuleGeometry(geometry&: capsuleGeometry); |
541 | const float radius = holder.radius(); |
542 | const float halfHeight = holder.halfHeight(); |
543 | |
544 | if (!qFuzzyCompare(p1: capsuleGeometry.radius, p2: radius) |
545 | || !qFuzzyCompare(p1: capsuleGeometry.halfHeight, p2: halfHeight)) { |
546 | auto geom = QDebugDrawHelper::generateCapsuleGeometry( |
547 | radius: capsuleGeometry.radius, halfHeight: capsuleGeometry.halfHeight); |
548 | geom->setParent(model); |
549 | model->setGeometry(geom); |
550 | holder.setRadius(capsuleGeometry.radius); |
551 | holder.setHalfHeight(capsuleGeometry.halfHeight); |
552 | } |
553 | } |
554 | break; |
555 | |
556 | case physx::PxGeometryType::ePLANE:{ |
557 | physx::PxPlaneGeometry planeGeometry; |
558 | physXShape->getPlaneGeometry(geometry&: planeGeometry); |
559 | // Special rotation |
560 | const QQuaternion rotation = |
561 | kMinus90YawRotation * QPhysicsUtils::toQtType(quat: localPose.q); |
562 | localPose = physx::PxTransform(localPose.p, QPhysicsUtils::toPhysXType(qquat: rotation)); |
563 | |
564 | if (model->geometry() == nullptr) { |
565 | auto geom = QDebugDrawHelper::generatePlaneGeometry(); |
566 | geom->setParent(model); |
567 | model->setGeometry(geom); |
568 | } |
569 | } |
570 | break; |
571 | |
572 | case physx::PxGeometryType::eHEIGHTFIELD: { |
573 | physx::PxHeightFieldGeometry heightFieldGeometry; |
574 | physXShape->getHeightFieldGeometry(geometry&: heightFieldGeometry); |
575 | const float heightScale = holder.heightScale(); |
576 | const float rowScale = holder.rowScale(); |
577 | const float columnScale = holder.columnScale(); |
578 | |
579 | if (!qFuzzyCompare(p1: heightFieldGeometry.heightScale, p2: heightScale) |
580 | || !qFuzzyCompare(p1: heightFieldGeometry.rowScale, p2: rowScale) |
581 | || !qFuzzyCompare(p1: heightFieldGeometry.columnScale, p2: columnScale)) { |
582 | |
583 | auto geom = QDebugDrawHelper::generateHeightFieldGeometry( |
584 | heightField: heightFieldGeometry.heightField, heightScale: heightFieldGeometry.heightScale, |
585 | rowScale: heightFieldGeometry.rowScale, columnScale: heightFieldGeometry.columnScale); |
586 | geom->setParent(model); |
587 | model->setGeometry(geom); |
588 | holder.setHeightScale(heightFieldGeometry.heightScale); |
589 | holder.setRowScale(heightFieldGeometry.rowScale); |
590 | holder.setColumnScale(heightFieldGeometry.columnScale); |
591 | } |
592 | } |
593 | break; |
594 | |
595 | case physx::PxGeometryType::eCONVEXMESH: { |
596 | physx::PxConvexMeshGeometry convexMeshGeometry; |
597 | physXShape->getConvexMeshGeometry(geometry&: convexMeshGeometry); |
598 | const auto rotation = convexMeshGeometry.scale.rotation * localPose.q; |
599 | localPose = physx::PxTransform(localPose.p, rotation); |
600 | model->setScale(QPhysicsUtils::toQtType(vec: convexMeshGeometry.scale.scale)); |
601 | |
602 | if (model->geometry() == nullptr) { |
603 | auto geom = QDebugDrawHelper::generateConvexMeshGeometry( |
604 | convexMesh: convexMeshGeometry.convexMesh); |
605 | geom->setParent(model); |
606 | model->setGeometry(geom); |
607 | } |
608 | } |
609 | break; |
610 | |
611 | case physx::PxGeometryType::eTRIANGLEMESH: { |
612 | physx::PxTriangleMeshGeometry triangleMeshGeometry; |
613 | physXShape->getTriangleMeshGeometry(geometry&: triangleMeshGeometry); |
614 | const auto rotation = triangleMeshGeometry.scale.rotation * localPose.q; |
615 | localPose = physx::PxTransform(localPose.p, rotation); |
616 | model->setScale(QPhysicsUtils::toQtType(vec: triangleMeshGeometry.scale.scale)); |
617 | |
618 | if (model->geometry() == nullptr) { |
619 | auto geom = QDebugDrawHelper::generateTriangleMeshGeometry( |
620 | triangleMesh: triangleMeshGeometry.triangleMesh); |
621 | geom->setParent(model); |
622 | model->setGeometry(geom); |
623 | } |
624 | } |
625 | break; |
626 | |
627 | case physx::PxGeometryType::eINVALID: |
628 | case physx::PxGeometryType::eGEOMETRY_COUNT: |
629 | // should not happen |
630 | Q_UNREACHABLE(); |
631 | } |
632 | |
633 | model->setVisible(true); |
634 | |
635 | auto globalPose = node->getGlobalPose(); |
636 | auto finalPose = globalPose.transform(src: localPose); |
637 | |
638 | model->setRotation(QPhysicsUtils::toQtType(quat: finalPose.q)); |
639 | model->setPosition(QPhysicsUtils::toQtType(vec: finalPose.p)); |
640 | } |
641 | } |
642 | |
643 | // Remove old collision shapes |
644 | m_collisionShapeDebugModels.removeIf( |
645 | pred: [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>, |
646 | DebugModelHolder>::iterator it) { |
647 | if (!currentCollisionShapes.contains(value: it.key())) { |
648 | auto holder = it.value(); |
649 | if (holder.model) |
650 | delete holder.model; |
651 | return true; |
652 | } |
653 | return false; |
654 | }); |
655 | } |
656 | |
657 | static void collectPhysicsNodes(QQuick3DObject *node, QList<QAbstractPhysicsNode *> &nodes) |
658 | { |
659 | if (auto shape = qobject_cast<QAbstractPhysicsNode *>(object: node)) { |
660 | nodes.push_back(t: shape); |
661 | return; |
662 | } |
663 | |
664 | for (QQuick3DObject *child : node->childItems()) |
665 | collectPhysicsNodes(node: child, nodes); |
666 | } |
667 | |
668 | void QPhysicsWorld::updateDebugDrawDesignStudio() |
669 | { |
670 | // Use scene node if no viewport has been specified |
671 | auto sceneNode = m_viewport ? m_viewport : m_scene; |
672 | |
673 | if (sceneNode == nullptr) |
674 | return; |
675 | |
676 | setupDebugMaterials(sceneNode); |
677 | |
678 | // Store the collision shapes we have now so we can clear out the removed ones |
679 | QSet<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>> currentCollisionShapes; |
680 | currentCollisionShapes.reserve(size: m_collisionShapeDebugModels.size()); |
681 | |
682 | QList<QAbstractPhysicsNode *> activePhysicsNodes; |
683 | activePhysicsNodes.reserve(size: m_collisionShapeDebugModels.size()); |
684 | collectPhysicsNodes(node: m_scene, nodes&: activePhysicsNodes); |
685 | |
686 | for (QAbstractPhysicsNode *node : activePhysicsNodes) { |
687 | |
688 | const auto &collisionShapes = node->getCollisionShapesList(); |
689 | const int materialIdx = 0; // Just take first material |
690 | const int length = collisionShapes.length(); |
691 | |
692 | const bool isCharacterController = qobject_cast<QCharacterController *>(object: node) != nullptr; |
693 | |
694 | for (int idx = 0; idx < length; idx++) { |
695 | QAbstractCollisionShape *collisionShape = collisionShapes[idx]; |
696 | DebugModelHolder &holder = |
697 | m_DesignStudioDebugModels[std::make_pair(x&: collisionShape, y&: node)]; |
698 | auto &model = holder.model; |
699 | |
700 | currentCollisionShapes.insert(value: std::make_pair(x&: collisionShape, y&: node)); |
701 | |
702 | m_hasIndividualDebugDraw = |
703 | m_hasIndividualDebugDraw || collisionShape->enableDebugDraw(); |
704 | |
705 | // Create/Update debug view infrastructure |
706 | { |
707 | // Hack: we have to delete the model every frame so it shows up in QDS |
708 | // whenever the code is updated, not sure why ¯\_(?)_/¯ |
709 | delete model; |
710 | model = new QQuick3DModel(); |
711 | model->setParentItem(sceneNode); |
712 | model->setParent(sceneNode); |
713 | model->setCastsShadows(false); |
714 | model->setReceivesShadows(false); |
715 | model->setCastsReflections(false); |
716 | } |
717 | |
718 | const bool hasGeometry = holder.geometry != nullptr; |
719 | QVector3D scenePosition = collisionShape->scenePosition(); |
720 | QQuaternion sceneRotation = collisionShape->sceneRotation(); |
721 | QQuick3DGeometry *newGeometry = nullptr; |
722 | |
723 | if (isCharacterController) |
724 | sceneRotation = sceneRotation * QQuaternion::fromEulerAngles(eulerAngles: QVector3D(0, 0, 90)); |
725 | |
726 | { // update or set material |
727 | auto material = m_debugMaterials[materialIdx]; |
728 | QQmlListReference materialsRef(model, "materials" ); |
729 | if (materialsRef.count() == 0 || materialsRef.at(0) != material) { |
730 | materialsRef.clear(); |
731 | materialsRef.append(material); |
732 | } |
733 | } |
734 | |
735 | if (auto shape = qobject_cast<QBoxShape *>(object: collisionShape)) { |
736 | const auto &halfExtentsOld = holder.halfExtents(); |
737 | const auto halfExtents = shape->sceneScale() * shape->extents() * 0.5f; |
738 | if (!qFuzzyCompare(v1: halfExtentsOld, v2: halfExtents) || !hasGeometry) { |
739 | newGeometry = QDebugDrawHelper::generateBoxGeometry(halfExtents); |
740 | holder.setHalfExtents(halfExtents); |
741 | } |
742 | } else if (auto shape = qobject_cast<QSphereShape *>(object: collisionShape)) { |
743 | const float radiusOld = holder.radius(); |
744 | const float radius = shape->sceneScale().x() * shape->diameter() * 0.5f; |
745 | if (!qFuzzyCompare(p1: radiusOld, p2: radius) || !hasGeometry) { |
746 | newGeometry = QDebugDrawHelper::generateSphereGeometry(radius); |
747 | holder.setRadius(radius); |
748 | } |
749 | } else if (auto shape = qobject_cast<QCapsuleShape *>(object: collisionShape)) { |
750 | const float radiusOld = holder.radius(); |
751 | const float halfHeightOld = holder.halfHeight(); |
752 | const float radius = shape->sceneScale().y() * shape->diameter() * 0.5f; |
753 | const float halfHeight = shape->sceneScale().x() * shape->height() * 0.5f; |
754 | |
755 | if ((!qFuzzyCompare(p1: radiusOld, p2: radius) || !qFuzzyCompare(p1: halfHeightOld, p2: halfHeight)) |
756 | || !hasGeometry) { |
757 | newGeometry = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight); |
758 | holder.setRadius(radius); |
759 | holder.setHalfHeight(halfHeight); |
760 | } |
761 | } else if (qobject_cast<QPlaneShape *>(object: collisionShape)) { |
762 | if (!hasGeometry) |
763 | newGeometry = QDebugDrawHelper::generatePlaneGeometry(); |
764 | } else if (auto shape = qobject_cast<QHeightFieldShape *>(object: collisionShape)) { |
765 | physx::PxHeightFieldGeometry *heightFieldGeometry = |
766 | static_cast<physx::PxHeightFieldGeometry *>(shape->getPhysXGeometry()); |
767 | const float heightScale = holder.heightScale(); |
768 | const float rowScale = holder.rowScale(); |
769 | const float columnScale = holder.columnScale(); |
770 | scenePosition += shape->hfOffset(); |
771 | if (!heightFieldGeometry) { |
772 | qWarning() << "Could not get height field" ; |
773 | } else if (!qFuzzyCompare(p1: heightFieldGeometry->heightScale, p2: heightScale) |
774 | || !qFuzzyCompare(p1: heightFieldGeometry->rowScale, p2: rowScale) |
775 | || !qFuzzyCompare(p1: heightFieldGeometry->columnScale, p2: columnScale) |
776 | || !hasGeometry) { |
777 | newGeometry = QDebugDrawHelper::generateHeightFieldGeometry( |
778 | heightField: heightFieldGeometry->heightField, heightScale: heightFieldGeometry->heightScale, |
779 | rowScale: heightFieldGeometry->rowScale, columnScale: heightFieldGeometry->columnScale); |
780 | holder.setHeightScale(heightFieldGeometry->heightScale); |
781 | holder.setRowScale(heightFieldGeometry->rowScale); |
782 | holder.setColumnScale(heightFieldGeometry->columnScale); |
783 | } |
784 | } else if (auto shape = qobject_cast<QConvexMeshShape *>(object: collisionShape)) { |
785 | auto convexMeshGeometry = |
786 | static_cast<physx::PxConvexMeshGeometry *>(shape->getPhysXGeometry()); |
787 | if (!convexMeshGeometry) { |
788 | qWarning() << "Could not get convex mesh" ; |
789 | } else { |
790 | model->setScale(QPhysicsUtils::toQtType(vec: convexMeshGeometry->scale.scale)); |
791 | |
792 | if (!hasGeometry) { |
793 | newGeometry = QDebugDrawHelper::generateConvexMeshGeometry( |
794 | convexMesh: convexMeshGeometry->convexMesh); |
795 | } |
796 | } |
797 | } else if (auto shape = qobject_cast<QTriangleMeshShape *>(object: collisionShape)) { |
798 | physx::PxTriangleMeshGeometry *triangleMeshGeometry = |
799 | static_cast<physx::PxTriangleMeshGeometry *>(shape->getPhysXGeometry()); |
800 | if (!triangleMeshGeometry) { |
801 | qWarning() << "Could not get triangle mesh" ; |
802 | } else { |
803 | model->setScale(QPhysicsUtils::toQtType(vec: triangleMeshGeometry->scale.scale)); |
804 | |
805 | if (!hasGeometry) { |
806 | newGeometry = QDebugDrawHelper::generateTriangleMeshGeometry( |
807 | triangleMesh: triangleMeshGeometry->triangleMesh); |
808 | } |
809 | } |
810 | } |
811 | |
812 | if (newGeometry) { |
813 | delete holder.geometry; |
814 | holder.geometry = newGeometry; |
815 | } |
816 | |
817 | model->setGeometry(holder.geometry); |
818 | model->setVisible(true); |
819 | |
820 | model->setRotation(sceneRotation); |
821 | model->setPosition(scenePosition); |
822 | } |
823 | } |
824 | |
825 | // Remove old debug models |
826 | m_DesignStudioDebugModels.removeIf( |
827 | pred: [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>, |
828 | DebugModelHolder>::iterator it) { |
829 | if (!currentCollisionShapes.contains(value: it.key())) { |
830 | auto holder = it.value(); |
831 | if (holder.model) { |
832 | delete holder.geometry; |
833 | delete holder.model; |
834 | } |
835 | return true; |
836 | } |
837 | return false; |
838 | }); |
839 | } |
840 | |
841 | void QPhysicsWorld::disableDebugDraw() |
842 | { |
843 | m_hasIndividualDebugDraw = false; |
844 | |
845 | for (QAbstractPhysXNode *body : m_physXBodies) { |
846 | const auto &collisionShapes = body->frontendNode->getCollisionShapesList(); |
847 | const int length = collisionShapes.length(); |
848 | for (int idx = 0; idx < length; idx++) { |
849 | const auto collisionShape = collisionShapes[idx]; |
850 | if (collisionShape->enableDebugDraw()) { |
851 | m_hasIndividualDebugDraw = true; |
852 | return; |
853 | } |
854 | } |
855 | } |
856 | } |
857 | |
858 | void QPhysicsWorld::setEnableCCD(bool enableCCD) |
859 | { |
860 | if (m_enableCCD == enableCCD) |
861 | return; |
862 | |
863 | if (m_physicsInitialized) { |
864 | qWarning() |
865 | << "Warning: Changing 'enableCCD' after physics is initialized will have no effect" ; |
866 | return; |
867 | } |
868 | |
869 | m_enableCCD = enableCCD; |
870 | emit enableCCDChanged(enableCCD: m_enableCCD); |
871 | } |
872 | |
873 | void QPhysicsWorld::setTypicalLength(float typicalLength) |
874 | { |
875 | if (qFuzzyCompare(p1: typicalLength, p2: m_typicalLength)) |
876 | return; |
877 | |
878 | if (typicalLength <= 0.f) { |
879 | qWarning() << "Warning: 'typicalLength' value less than zero, ignored" ; |
880 | return; |
881 | } |
882 | |
883 | if (m_physicsInitialized) { |
884 | qWarning() << "Warning: Changing 'typicalLength' after physics is initialized will have " |
885 | "no effect" ; |
886 | return; |
887 | } |
888 | |
889 | m_typicalLength = typicalLength; |
890 | |
891 | emit typicalLengthChanged(typicalLength); |
892 | } |
893 | |
894 | void QPhysicsWorld::setTypicalSpeed(float typicalSpeed) |
895 | { |
896 | if (qFuzzyCompare(p1: typicalSpeed, p2: m_typicalSpeed)) |
897 | return; |
898 | |
899 | if (m_physicsInitialized) { |
900 | qWarning() << "Warning: Changing 'typicalSpeed' after physics is initialized will have " |
901 | "no effect" ; |
902 | return; |
903 | } |
904 | |
905 | m_typicalSpeed = typicalSpeed; |
906 | |
907 | emit typicalSpeedChanged(typicalSpeed); |
908 | } |
909 | |
910 | float QPhysicsWorld::defaultDensity() const |
911 | { |
912 | return m_defaultDensity; |
913 | } |
914 | |
915 | float QPhysicsWorld::minimumTimestep() const |
916 | { |
917 | return m_minTimestep; |
918 | } |
919 | |
920 | float QPhysicsWorld::maximumTimestep() const |
921 | { |
922 | return m_maxTimestep; |
923 | } |
924 | |
925 | void QPhysicsWorld::setDefaultDensity(float defaultDensity) |
926 | { |
927 | if (qFuzzyCompare(p1: m_defaultDensity, p2: defaultDensity)) |
928 | return; |
929 | m_defaultDensity = defaultDensity; |
930 | |
931 | // Go through all dynamic rigid bodies and update the default density |
932 | for (QAbstractPhysXNode *body : m_physXBodies) |
933 | body->updateDefaultDensity(density: m_defaultDensity); |
934 | |
935 | emit defaultDensityChanged(defaultDensity); |
936 | } |
937 | |
938 | // Remove physics world items that no longer exist |
939 | |
940 | void QPhysicsWorld::cleanupRemovedNodes() |
941 | { |
942 | m_physXBodies.removeIf(pred: [this](QAbstractPhysXNode *body) { |
943 | return body->cleanupIfRemoved(physX: m_physx); |
944 | }); |
945 | // We don't need to lock the mutex here since the simulation |
946 | // worker is waiting |
947 | m_removedPhysicsNodes.clear(); |
948 | } |
949 | |
950 | void QPhysicsWorld::initPhysics() |
951 | { |
952 | Q_ASSERT(!m_physicsInitialized); |
953 | |
954 | m_physx->createScene(typicalLength: m_typicalLength, typicalSpeed: m_typicalSpeed, gravity: m_gravity, enableCCD: m_enableCCD, physicsWorld: this); |
955 | |
956 | // Setup worker thread |
957 | SimulationWorker *worker = new SimulationWorker(m_physx); |
958 | worker->moveToThread(thread: &m_workerThread); |
959 | connect(sender: &m_workerThread, signal: &QThread::finished, context: worker, slot: &QObject::deleteLater); |
960 | if (m_inDesignStudio) { |
961 | connect(sender: this, signal: &QPhysicsWorld::simulateFrame, context: worker, |
962 | slot: &SimulationWorker::simulateFrameDesignStudio); |
963 | connect(sender: worker, signal: &SimulationWorker::frameDoneDesignStudio, context: this, |
964 | slot: &QPhysicsWorld::frameFinishedDesignStudio); |
965 | } else { |
966 | connect(sender: this, signal: &QPhysicsWorld::simulateFrame, context: worker, slot: &SimulationWorker::simulateFrame); |
967 | connect(sender: worker, signal: &SimulationWorker::frameDone, context: this, slot: &QPhysicsWorld::frameFinished); |
968 | } |
969 | m_workerThread.start(); |
970 | |
971 | m_physicsInitialized = true; |
972 | } |
973 | |
974 | void QPhysicsWorld::frameFinished(float deltaTime) |
975 | { |
976 | matchOrphanNodes(); |
977 | cleanupRemovedNodes(); |
978 | for (auto *node : std::as_const(t&: m_newPhysicsNodes)) { |
979 | auto *body = node->createPhysXBackend(); |
980 | body->init(world: this, physX: m_physx); |
981 | m_physXBodies.push_back(t: body); |
982 | } |
983 | m_newPhysicsNodes.clear(); |
984 | |
985 | QHash<QQuick3DNode *, QMatrix4x4> transformCache; |
986 | |
987 | // TODO: Use dirty flag/dirty list to avoid redoing things that didn't change |
988 | for (auto *physXBody : std::as_const(t&: m_physXBodies)) { |
989 | physXBody->markDirtyShapes(); |
990 | physXBody->rebuildDirtyShapes(this, m_physx); |
991 | |
992 | // Sync the physics world and the scene |
993 | physXBody->sync(deltaTime, transformCache); |
994 | } |
995 | |
996 | updateDebugDraw(); |
997 | |
998 | if (m_running) |
999 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
1000 | emit frameDone(timestep: deltaTime * 1000); |
1001 | } |
1002 | |
1003 | void QPhysicsWorld::frameFinishedDesignStudio() |
1004 | { |
1005 | // Note sure if this is needed but do it anyway |
1006 | matchOrphanNodes(); |
1007 | cleanupRemovedNodes(); |
1008 | // Ignore new physics nodes, we find them from the scene node anyway |
1009 | m_newPhysicsNodes.clear(); |
1010 | |
1011 | updateDebugDrawDesignStudio(); |
1012 | |
1013 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
1014 | } |
1015 | |
1016 | QPhysicsWorld *QPhysicsWorld::getWorld(QQuick3DNode *node) |
1017 | { |
1018 | for (QPhysicsWorld *world : worldManager.worlds) { |
1019 | if (!world->m_scene) { |
1020 | continue; |
1021 | } |
1022 | |
1023 | QQuick3DNode *nodeCurr = node; |
1024 | |
1025 | // Maybe pointless but check starting node |
1026 | if (nodeCurr == world->m_scene) |
1027 | return world; |
1028 | |
1029 | while (nodeCurr->parentNode()) { |
1030 | nodeCurr = nodeCurr->parentNode(); |
1031 | if (nodeCurr == world->m_scene) |
1032 | return world; |
1033 | } |
1034 | } |
1035 | |
1036 | return nullptr; |
1037 | } |
1038 | |
1039 | void QPhysicsWorld::matchOrphanNodes() |
1040 | { |
1041 | // FIXME: does this need thread safety? |
1042 | if (worldManager.orphanNodes.isEmpty()) |
1043 | return; |
1044 | |
1045 | qsizetype numNodes = worldManager.orphanNodes.length(); |
1046 | qsizetype idx = 0; |
1047 | |
1048 | while (idx < numNodes) { |
1049 | auto node = worldManager.orphanNodes[idx]; |
1050 | auto world = getWorld(node); |
1051 | if (world == this) { |
1052 | world->m_newPhysicsNodes.push_back(t: node); |
1053 | // swap-erase |
1054 | worldManager.orphanNodes.swapItemsAt(i: idx, j: numNodes - 1); |
1055 | worldManager.orphanNodes.pop_back(); |
1056 | numNodes--; |
1057 | } else { |
1058 | idx++; |
1059 | } |
1060 | } |
1061 | } |
1062 | |
1063 | void QPhysicsWorld::findPhysicsNodes() |
1064 | { |
1065 | // This method finds the physics nodes inside the scene pointed to by the |
1066 | // scene property. This method is necessary to run whenever the scene |
1067 | // property is changed. |
1068 | if (m_scene == nullptr) |
1069 | return; |
1070 | |
1071 | // Recursively go through all children and add all QAbstractPhysicsNode's |
1072 | QList<QQuick3DObject *> children = m_scene->childItems(); |
1073 | while (!children.empty()) { |
1074 | auto child = children.takeFirst(); |
1075 | if (auto converted = qobject_cast<QAbstractPhysicsNode *>(object: child); converted != nullptr) { |
1076 | // This should never happen but check anyway. |
1077 | if (converted->m_backendObject != nullptr) { |
1078 | qWarning() << "Warning: physics node already associated with a backend node." ; |
1079 | continue; |
1080 | } |
1081 | |
1082 | m_newPhysicsNodes.push_back(t: converted); |
1083 | worldManager.orphanNodes.removeAll(t: converted); // No longer orphan |
1084 | } |
1085 | children.append(l: child->childItems()); |
1086 | } |
1087 | } |
1088 | |
1089 | physx::PxPhysics *QPhysicsWorld::getPhysics() |
1090 | { |
1091 | return StaticPhysXObjects::getReference().physics; |
1092 | } |
1093 | |
1094 | physx::PxCooking *QPhysicsWorld::getCooking() |
1095 | { |
1096 | return StaticPhysXObjects::getReference().cooking; |
1097 | } |
1098 | |
1099 | physx::PxControllerManager *QPhysicsWorld::controllerManager() |
1100 | { |
1101 | if (m_physx->scene && !m_physx->controllerManager) { |
1102 | m_physx->controllerManager = PxCreateControllerManager(scene&: *m_physx->scene); |
1103 | qCDebug(lcQuick3dPhysics) << "Created controller manager" << m_physx->controllerManager; |
1104 | } |
1105 | return m_physx->controllerManager; |
1106 | } |
1107 | |
1108 | QQuick3DNode *QPhysicsWorld::scene() const |
1109 | { |
1110 | return m_scene; |
1111 | } |
1112 | |
1113 | void QPhysicsWorld::setScene(QQuick3DNode *newScene) |
1114 | { |
1115 | if (m_scene == newScene) |
1116 | return; |
1117 | |
1118 | m_scene = newScene; |
1119 | |
1120 | // Delete all nodes since they are associated with the previous scene |
1121 | for (auto body : m_physXBodies) { |
1122 | deregisterNode(physicsNode: body->frontendNode); |
1123 | } |
1124 | |
1125 | // Check if scene is already used by another world |
1126 | bool sceneOK = true; |
1127 | for (QPhysicsWorld *world : worldManager.worlds) { |
1128 | if (world != this && world->scene() == newScene) { |
1129 | sceneOK = false; |
1130 | qWarning() << "Warning: scene already associated with physics world" ; |
1131 | } |
1132 | } |
1133 | |
1134 | if (sceneOK) |
1135 | findPhysicsNodes(); |
1136 | emit sceneChanged(); |
1137 | } |
1138 | |
1139 | QT_END_NAMESPACE |
1140 | |
1141 | #include "qphysicsworld.moc" |
1142 | |