1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qphysxcharactercontroller_p.h"
5
6#include "PxRigidDynamic.h"
7#include "characterkinematic/PxController.h"
8#include "characterkinematic/PxControllerManager.h"
9#include "characterkinematic/PxCapsuleController.h"
10
11#include "qcapsuleshape_p.h"
12#include "qphysicsutils_p.h"
13#include "qphysicsworld_p.h"
14#include "qcharactercontroller_p.h"
15
16#define PHYSX_RELEASE(x) \
17 if (x != nullptr) { \
18 x->release(); \
19 x = nullptr; \
20 }
21
22QT_BEGIN_NAMESPACE
23
24class ControllerCallback : public physx::PxUserControllerHitReport
25{
26public:
27 ControllerCallback(QPhysicsWorld *worldIn) : world(worldIn) { }
28
29 void onShapeHit(const physx::PxControllerShapeHit &hit) override
30 {
31 QMutexLocker locker(&world->m_removedPhysicsNodesMutex);
32
33 QAbstractPhysicsNode *other = static_cast<QAbstractPhysicsNode *>(hit.actor->userData);
34 QCharacterController *trigger =
35 static_cast<QCharacterController *>(hit.controller->getUserData());
36
37 if (!trigger || !other || !trigger->enableShapeHitCallback())
38 return;
39
40 QVector3D position = QPhysicsUtils::toQtType(vec: physx::toVec3(v: hit.worldPos));
41 QVector3D impulse = QPhysicsUtils::toQtType(vec: hit.dir * hit.length);
42 QVector3D normal = QPhysicsUtils::toQtType(vec: hit.worldNormal);
43
44 emit trigger->shapeHit(body: other, position, impulse, normal);
45 }
46 void onControllerHit(const physx::PxControllersHit & /*hit*/) override { }
47 void onObstacleHit(const physx::PxControllerObstacleHit & /*hit*/) override { }
48
49private:
50 QPhysicsWorld *world = nullptr;
51};
52
53QPhysXCharacterController::QPhysXCharacterController(QCharacterController *frontEnd)
54 : QAbstractPhysXNode(frontEnd)
55{
56}
57
58void QPhysXCharacterController::cleanup(QPhysXWorld *physX)
59{
60 PHYSX_RELEASE(controller);
61 delete reportCallback;
62 reportCallback = nullptr;
63 QAbstractPhysXNode::cleanup(physX);
64}
65
66void QPhysXCharacterController::init(QPhysicsWorld *world, QPhysXWorld *physX)
67{
68 Q_ASSERT(!controller);
69
70 auto *characterController = static_cast<QCharacterController *>(frontendNode);
71
72 auto shapes = characterController->getCollisionShapesList();
73 if (shapes.length() != 1) {
74 qWarning() << "CharacterController: invalid collision shapes list.";
75 return;
76 }
77 auto *capsule = qobject_cast<QCapsuleShape *>(object: shapes.first());
78 if (!capsule) {
79 qWarning() << "CharacterController: collision shape is not a capsule.";
80 return;
81 }
82 auto *mgr = world->controllerManager();
83 if (!mgr) {
84 qWarning() << "QtQuick3DPhysics internal error: missing controller manager.";
85 return;
86 }
87
88 createMaterial(physX);
89
90 const QVector3D scale = characterController->sceneScale();
91 const qreal heightScale = scale.y();
92 const qreal radiusScale = scale.x();
93 physx::PxCapsuleControllerDesc desc;
94 reportCallback = new ControllerCallback(world);
95 desc.reportCallback = reportCallback;
96 desc.radius = 0.5f * radiusScale * capsule->diameter();
97 desc.height = heightScale * capsule->height();
98 desc.stepOffset = 0.25f * desc.height; // TODO: API
99
100 desc.material = material;
101 const QVector3D pos = characterController->scenePosition();
102 desc.position = { pos.x(), pos.y(), pos.z() };
103 // Safe to static_cast since createController will always return a PxCapsuleController
104 // if desc is of type PxCapsuleControllerDesc
105 controller = static_cast<physx::PxCapsuleController *>(mgr->createController(desc));
106
107 if (!controller) {
108 qWarning() << "QtQuick3DPhysics internal error: could not create controller.";
109 return;
110 }
111
112 controller->setUserData(static_cast<void *>(frontendNode));
113
114 auto *actor = controller->getActor();
115 if (actor)
116 actor->userData = characterController;
117 else
118 qWarning() << "QtQuick3DPhysics internal error: CharacterController created without actor.";
119}
120
121void QPhysXCharacterController::sync(float deltaTime,
122 QHash<QQuick3DNode *, QMatrix4x4> & /*transformCache*/)
123{
124 if (controller == nullptr)
125 return;
126
127 auto *characterController = static_cast<QCharacterController *>(frontendNode);
128
129 // Update capsule height, radius, stepOffset
130 const auto &shapes = characterController->getCollisionShapesList();
131 auto capsule = shapes.length() == 1 ? qobject_cast<QCapsuleShape *>(object: shapes.front()) : nullptr;
132
133 if (shapes.length() != 1) {
134 qWarning() << "CharacterController: invalid collision shapes list.";
135 } else if (!capsule) {
136 qWarning() << "CharacterController: collision shape is not a capsule.";
137 } else {
138 const QVector3D sceneScale = characterController->sceneScale();
139 const qreal heightScale = sceneScale.y();
140 const qreal radiusScale = sceneScale.x();
141
142 // Update height
143 const float heightNew = heightScale * capsule->height();
144 if (!qFuzzyCompare(p1: controller->getHeight(), p2: heightNew))
145 controller->resize(height: heightNew);
146 // Update radius
147 const float radiusNew = 0.5f * radiusScale * capsule->diameter();
148 if (!qFuzzyCompare(p1: controller->getRadius(), p2: radiusNew))
149 controller->setRadius(radiusNew);
150 // Update stepOffset
151 const float stepOffsetNew = 0.25f * heightNew;
152 if (!qFuzzyCompare(p1: controller->getStepOffset(), p2: stepOffsetNew))
153 controller->setStepOffset(stepOffsetNew);
154 }
155
156 // update node from physX
157 QVector3D position = QPhysicsUtils::toQtType(vec: physx::toVec3(v: controller->getPosition()));
158 const QQuick3DNode *parentNode = static_cast<QQuick3DNode *>(characterController->parentItem());
159 if (!parentNode) {
160 // then it is the same space
161 characterController->setPosition(position);
162 } else {
163 characterController->setPosition(parentNode->mapPositionFromScene(scenePosition: position));
164 }
165
166 QVector3D teleportPos;
167 bool teleport = characterController->getTeleport(position&: teleportPos);
168 if (teleport) {
169 controller->setPosition({ teleportPos.x(), teleportPos.y(), teleportPos.z() });
170 } else if (deltaTime > 0) {
171 const auto displacement =
172 QPhysicsUtils::toPhysXType(qvec: characterController->getDisplacement(deltaTime));
173 auto collisions =
174 controller->move(disp: displacement, minDist: displacement.magnitude() / 100, elapsedTime: deltaTime, filters: {});
175 characterController->setCollisions(QCharacterController::Collisions(uint(collisions)));
176 }
177 // QCharacterController has a material property, but we don't inherit from
178 // QPhysXMaterialBody, so we create the material manually in init()
179 // TODO: handle material changes
180}
181
182void QPhysXCharacterController::createMaterial(QPhysXWorld *physX)
183{
184 createMaterialFromQtMaterial(
185 physX, qtMaterial: static_cast<QCharacterController *>(frontendNode)->physicsMaterial());
186}
187
188QT_END_NAMESPACE
189

source code of qtquick3dphysics/src/quick3dphysics/physxnode/qphysxcharactercontroller.cpp