1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qphysxactorbody_p.h" |
5 | |
6 | #include "PxMaterial.h" |
7 | #include "PxPhysics.h" |
8 | #include "PxRigidDynamic.h" |
9 | #include "PxRigidActor.h" |
10 | #include "PxScene.h" |
11 | |
12 | #include "physxnode/qphysxworld_p.h" |
13 | #include "qabstractphysicsbody_p.h" |
14 | #include "qheightfieldshape_p.h" |
15 | #include "qphysicsutils_p.h" |
16 | #include "qplaneshape_p.h" |
17 | #include "qstaticphysxobjects_p.h" |
18 | |
19 | #define PHYSX_RELEASE(x) \ |
20 | if (x != nullptr) { \ |
21 | x->release(); \ |
22 | x = nullptr; \ |
23 | } |
24 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | static const QQuaternion kMinus90YawRotation = QQuaternion::fromEulerAngles(pitch: 0, yaw: -90, roll: 0); |
28 | |
29 | static inline bool fuzzyEquals(const physx::PxTransform &a, const physx::PxTransform &b) |
30 | { |
31 | return qFuzzyCompare(p1: a.p.x, p2: b.p.x) && qFuzzyCompare(p1: a.p.y, p2: b.p.y) && qFuzzyCompare(p1: a.p.z, p2: b.p.z) |
32 | && qFuzzyCompare(p1: a.q.x, p2: b.q.x) && qFuzzyCompare(p1: a.q.y, p2: b.q.y) |
33 | && qFuzzyCompare(p1: a.q.z, p2: b.q.z) && qFuzzyCompare(p1: a.q.w, p2: b.q.w); |
34 | } |
35 | |
36 | static physx::PxTransform getPhysXLocalTransform(const QQuick3DNode *node) |
37 | { |
38 | // Modify transforms to make the PhysX shapes match the QtQuick3D conventions |
39 | if (qobject_cast<const QPlaneShape *>(object: node) != nullptr) { |
40 | // Rotate the plane to make it match the built-in rectangle |
41 | const QQuaternion rotation = kMinus90YawRotation * node->rotation(); |
42 | return physx::PxTransform(QPhysicsUtils::toPhysXType(qvec: node->position()), |
43 | QPhysicsUtils::toPhysXType(qquat: rotation)); |
44 | } else if (auto *hf = qobject_cast<const QHeightFieldShape *>(object: node)) { |
45 | // Shift the height field so it's centered at the origin |
46 | return physx::PxTransform(QPhysicsUtils::toPhysXType(qvec: node->position() + hf->hfOffset()), |
47 | QPhysicsUtils::toPhysXType(qquat: node->rotation())); |
48 | } |
49 | |
50 | const QQuaternion &rotation = node->rotation(); |
51 | const QVector3D &localPosition = node->position(); |
52 | const QVector3D &scale = node->sceneScale(); |
53 | return physx::PxTransform(QPhysicsUtils::toPhysXType(qvec: localPosition * scale), |
54 | QPhysicsUtils::toPhysXType(qquat: rotation)); |
55 | } |
56 | |
57 | QPhysXActorBody::QPhysXActorBody(QAbstractPhysicsNode *frontEnd) : QAbstractPhysXNode(frontEnd) { } |
58 | |
59 | void QPhysXActorBody::cleanup(QPhysXWorld *physX) |
60 | { |
61 | if (actor) { |
62 | physX->scene->removeActor(actor&: *actor); |
63 | PHYSX_RELEASE(actor); |
64 | } |
65 | QAbstractPhysXNode::cleanup(physX); |
66 | } |
67 | |
68 | void QPhysXActorBody::init(QPhysicsWorld * /*world*/, QPhysXWorld *physX) |
69 | { |
70 | Q_ASSERT(!actor); |
71 | |
72 | createMaterial(physX); |
73 | createActor(physX); |
74 | |
75 | actor->userData = reinterpret_cast<void *>(frontendNode); |
76 | physX->scene->addActor(actor&: *actor); |
77 | setShapesDirty(true); |
78 | } |
79 | |
80 | void QPhysXActorBody::sync(float /*deltaTime*/, |
81 | QHash<QQuick3DNode *, QMatrix4x4> & /*transformCache*/) |
82 | { |
83 | auto *body = static_cast<QAbstractPhysicsBody *>(frontendNode); |
84 | if (QPhysicsMaterial *qtMaterial = body->physicsMaterial()) { |
85 | const float staticFriction = qtMaterial->staticFriction(); |
86 | const float dynamicFriction = qtMaterial->dynamicFriction(); |
87 | const float restitution = qtMaterial->restitution(); |
88 | if (material->getStaticFriction() != staticFriction) |
89 | material->setStaticFriction(staticFriction); |
90 | if (material->getDynamicFriction() != dynamicFriction) |
91 | material->setDynamicFriction(dynamicFriction); |
92 | if (material->getRestitution() != restitution) |
93 | material->setRestitution(restitution); |
94 | } |
95 | } |
96 | |
97 | void QPhysXActorBody::markDirtyShapes() |
98 | { |
99 | if (!frontendNode || !actor) |
100 | return; |
101 | |
102 | // Go through the shapes and look for a change in pose (rotation, position) |
103 | // TODO: it is likely cheaper to connect a signal for changes on the position and rotation |
104 | // property and mark the node dirty then. |
105 | if (!shapesDirty()) { |
106 | const auto &collisionShapes = frontendNode->getCollisionShapesList(); |
107 | const auto &physXShapes = shapes; |
108 | |
109 | const int len = collisionShapes.size(); |
110 | if (physXShapes.size() != len) { |
111 | // This should not really happen but check it anyway |
112 | setShapesDirty(true); |
113 | } else { |
114 | for (int i = 0; i < len; i++) { |
115 | auto poseNew = getPhysXLocalTransform(node: collisionShapes[i]); |
116 | auto poseOld = physXShapes[i]->getLocalPose(); |
117 | |
118 | if (!fuzzyEquals(a: poseNew, b: poseOld)) { |
119 | setShapesDirty(true); |
120 | break; |
121 | } |
122 | } |
123 | } |
124 | } |
125 | } |
126 | |
127 | void QPhysXActorBody::rebuildDirtyShapes(QPhysicsWorld * /*world*/, QPhysXWorld *physX) |
128 | { |
129 | if (!shapesDirty()) |
130 | return; |
131 | buildShapes(physX); |
132 | setShapesDirty(false); |
133 | } |
134 | |
135 | void QPhysXActorBody::createActor(QPhysXWorld * /*physX*/) |
136 | { |
137 | auto &s_physx = StaticPhysXObjects::getReference(); |
138 | const physx::PxTransform trf = QPhysicsUtils::toPhysXTransform(position: frontendNode->scenePosition(), |
139 | rotation: frontendNode->sceneRotation()); |
140 | actor = s_physx.physics->createRigidDynamic(pose: trf); |
141 | } |
142 | |
143 | bool QPhysXActorBody::debugGeometryCapability() |
144 | { |
145 | return true; |
146 | } |
147 | |
148 | physx::PxTransform QPhysXActorBody::getGlobalPose() |
149 | { |
150 | return actor->getGlobalPose(); |
151 | } |
152 | |
153 | void QPhysXActorBody::buildShapes(QPhysXWorld * /*physX*/) |
154 | { |
155 | auto body = actor; |
156 | for (auto *shape : shapes) { |
157 | body->detachShape(shape&: *shape); |
158 | PHYSX_RELEASE(shape); |
159 | } |
160 | |
161 | // TODO: Only remove changed shapes? |
162 | shapes.clear(); |
163 | |
164 | for (const auto &collisionShape : frontendNode->getCollisionShapesList()) { |
165 | // TODO: shapes can be shared between multiple actors. |
166 | // Do we need to create new ones for every body? |
167 | auto *geom = collisionShape->getPhysXGeometry(); |
168 | if (!geom || !material) |
169 | continue; |
170 | |
171 | auto &s_physx = StaticPhysXObjects::getReference(); |
172 | auto physXShape = s_physx.physics->createShape(geometry: *geom, material: *material); |
173 | |
174 | if (useTriggerFlag()) { |
175 | physXShape->setFlag(flag: physx::PxShapeFlag::eSIMULATION_SHAPE, value: false); |
176 | physXShape->setFlag(flag: physx::PxShapeFlag::eTRIGGER_SHAPE, value: true); |
177 | } |
178 | |
179 | shapes.push_back(t: physXShape); |
180 | physXShape->setLocalPose(getPhysXLocalTransform(node: collisionShape)); |
181 | body->attachShape(shape&: *physXShape); |
182 | } |
183 | } |
184 | |
185 | QT_END_NAMESPACE |
186 | |