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 physx::PxTransform getPhysXLocalTransform(const QQuick3DNode *node) |
28 | { |
29 | // Modify transforms to make the PhysX shapes match the QtQuick3D conventions |
30 | if (qobject_cast<const QPlaneShape *>(object: node) != nullptr) { |
31 | // Rotate the plane to make it match the built-in rectangle |
32 | const QQuaternion rotation = QPhysicsUtils::kMinus90YawRotation * node->rotation(); |
33 | return physx::PxTransform(QPhysicsUtils::toPhysXType(qvec: node->position()), |
34 | QPhysicsUtils::toPhysXType(qquat: rotation)); |
35 | } else if (auto *hf = qobject_cast<const QHeightFieldShape *>(object: node)) { |
36 | // Shift the height field so it's centered at the origin |
37 | return physx::PxTransform(QPhysicsUtils::toPhysXType(qvec: node->position() + hf->hfOffset()), |
38 | QPhysicsUtils::toPhysXType(qquat: node->rotation())); |
39 | } |
40 | |
41 | const QQuaternion &rotation = node->rotation(); |
42 | const QVector3D &localPosition = node->position(); |
43 | const QVector3D &scale = node->sceneScale(); |
44 | return physx::PxTransform(QPhysicsUtils::toPhysXType(qvec: localPosition * scale), |
45 | QPhysicsUtils::toPhysXType(qquat: rotation)); |
46 | } |
47 | |
48 | QPhysXActorBody::QPhysXActorBody(QAbstractPhysicsNode *frontEnd) : QAbstractPhysXNode(frontEnd) { } |
49 | |
50 | void QPhysXActorBody::cleanup(QPhysXWorld *physX) |
51 | { |
52 | if (actor) { |
53 | physX->scene->removeActor(actor&: *actor); |
54 | PHYSX_RELEASE(actor); |
55 | } |
56 | QAbstractPhysXNode::cleanup(physX); |
57 | } |
58 | |
59 | void QPhysXActorBody::init(QPhysicsWorld * /*world*/, QPhysXWorld *physX) |
60 | { |
61 | Q_ASSERT(!actor); |
62 | |
63 | createMaterial(physX); |
64 | createActor(physX); |
65 | |
66 | actor->userData = reinterpret_cast<void *>(frontendNode); |
67 | physX->scene->addActor(actor&: *actor); |
68 | setShapesDirty(true); |
69 | } |
70 | |
71 | void QPhysXActorBody::sync(float /*deltaTime*/, |
72 | QHash<QQuick3DNode *, QMatrix4x4> & /*transformCache*/) |
73 | { |
74 | auto *body = static_cast<QAbstractPhysicsBody *>(frontendNode); |
75 | if (QPhysicsMaterial *qtMaterial = body->physicsMaterial()) { |
76 | const float staticFriction = qtMaterial->staticFriction(); |
77 | const float dynamicFriction = qtMaterial->dynamicFriction(); |
78 | const float restitution = qtMaterial->restitution(); |
79 | if (material->getStaticFriction() != staticFriction) |
80 | material->setStaticFriction(staticFriction); |
81 | if (material->getDynamicFriction() != dynamicFriction) |
82 | material->setDynamicFriction(dynamicFriction); |
83 | if (material->getRestitution() != restitution) |
84 | material->setRestitution(restitution); |
85 | } |
86 | } |
87 | |
88 | void QPhysXActorBody::markDirtyShapes() |
89 | { |
90 | if (!frontendNode || !actor) |
91 | return; |
92 | |
93 | // Go through the shapes and look for a change in pose (rotation, position) |
94 | // TODO: it is likely cheaper to connect a signal for changes on the position and rotation |
95 | // property and mark the node dirty then. |
96 | if (!shapesDirty()) { |
97 | const auto &collisionShapes = frontendNode->getCollisionShapesList(); |
98 | const auto &physXShapes = shapes; |
99 | |
100 | const int len = collisionShapes.size(); |
101 | if (physXShapes.size() != len) { |
102 | // This should not really happen but check it anyway |
103 | setShapesDirty(true); |
104 | } else { |
105 | for (int i = 0; i < len; i++) { |
106 | auto poseNew = getPhysXLocalTransform(node: collisionShapes[i]); |
107 | auto poseOld = physXShapes[i]->getLocalPose(); |
108 | |
109 | if (!QPhysicsUtils::fuzzyEquals(a: poseNew, b: poseOld)) { |
110 | setShapesDirty(true); |
111 | break; |
112 | } |
113 | } |
114 | } |
115 | } |
116 | } |
117 | |
118 | void QPhysXActorBody::rebuildDirtyShapes(QPhysicsWorld * /*world*/, QPhysXWorld *physX) |
119 | { |
120 | if (!shapesDirty()) |
121 | return; |
122 | buildShapes(physX); |
123 | setShapesDirty(false); |
124 | } |
125 | |
126 | void QPhysXActorBody::createActor(QPhysXWorld * /*physX*/) |
127 | { |
128 | auto &s_physx = StaticPhysXObjects::getReference(); |
129 | const physx::PxTransform trf = QPhysicsUtils::toPhysXTransform(position: frontendNode->scenePosition(), |
130 | rotation: frontendNode->sceneRotation()); |
131 | actor = s_physx.physics->createRigidDynamic(pose: trf); |
132 | } |
133 | |
134 | bool QPhysXActorBody::debugGeometryCapability() |
135 | { |
136 | return true; |
137 | } |
138 | |
139 | physx::PxTransform QPhysXActorBody::getGlobalPose() |
140 | { |
141 | return actor->getGlobalPose(); |
142 | } |
143 | |
144 | void QPhysXActorBody::buildShapes(QPhysXWorld * /*physX*/) |
145 | { |
146 | auto body = actor; |
147 | for (auto *shape : shapes) { |
148 | body->detachShape(shape&: *shape); |
149 | PHYSX_RELEASE(shape); |
150 | } |
151 | |
152 | // TODO: Only remove changed shapes? |
153 | shapes.clear(); |
154 | |
155 | for (const auto &collisionShape : frontendNode->getCollisionShapesList()) { |
156 | // TODO: shapes can be shared between multiple actors. |
157 | // Do we need to create new ones for every body? |
158 | auto *geom = collisionShape->getPhysXGeometry(); |
159 | if (!geom || !material) |
160 | continue; |
161 | |
162 | auto &s_physx = StaticPhysXObjects::getReference(); |
163 | auto physXShape = s_physx.physics->createShape(geometry: *geom, material: *material); |
164 | |
165 | if (useTriggerFlag()) { |
166 | physXShape->setFlag(flag: physx::PxShapeFlag::eSIMULATION_SHAPE, value: false); |
167 | physXShape->setFlag(flag: physx::PxShapeFlag::eTRIGGER_SHAPE, value: true); |
168 | } |
169 | |
170 | { // Setup filtering |
171 | physx::PxFilterData filterData; |
172 | filterData.word0 = frontendNode->filterGroup(); |
173 | filterData.word1 = frontendNode->filterIgnoreGroups(); |
174 | physXShape->setSimulationFilterData(filterData); |
175 | } |
176 | |
177 | shapes.push_back(t: physXShape); |
178 | physXShape->setLocalPose(getPhysXLocalTransform(node: collisionShape)); |
179 | body->attachShape(shape&: *physXShape); |
180 | } |
181 | |
182 | // Filters are always clean after building shapes |
183 | setFiltersDirty(false); |
184 | } |
185 | |
186 | void QPhysXActorBody::updateFilters() |
187 | { |
188 | if (!filtersDirty()) |
189 | return; |
190 | |
191 | // Go through all shapes and set the filter group and mask. |
192 | // TODO: What about shared shapes on several actors? |
193 | for (auto &physXShape : shapes) { |
194 | physx::PxFilterData filterData; |
195 | filterData.word0 = frontendNode->filterGroup(); |
196 | filterData.word1 = frontendNode->filterIgnoreGroups(); |
197 | physXShape->setSimulationFilterData(filterData); |
198 | } |
199 | |
200 | setFiltersDirty(false); |
201 | } |
202 | |
203 | QT_END_NAMESPACE |
204 | |