Revision control
++ // Copyright (C) 2023 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qphysxcharactercontroller_p.h"
++
++ #include "PxRigidDynamic.h"
++ #include "PxShape.h"
++ #include "characterkinematic/PxController.h"
++ #include "characterkinematic/PxControllerManager.h"
++ #include "characterkinematic/PxCapsuleController.h"
++
++ #include "qcapsuleshape_p.h"
++ #include "qphysicsutils_p.h"
++ #include "qphysicsworld_p.h"
++ #include "qcharactercontroller_p.h"
++
++ #define PHYSX_RELEASE(x) \
++ if (x != nullptr) { \
++ x->release(); \
++ x = nullptr; \
++ }
++
++ QT_BEGIN_NAMESPACE
++
++ class ControllerCallback : public physx::PxUserControllerHitReport
++ {
++ public:
++ ControllerCallback(QPhysicsWorld *worldIn) : world(worldIn) { }
++
++ void onShapeHit(const physx::PxControllerShapeHit &hit) override
++ {
++ QAbstractPhysicsNode *other = static_cast<QAbstractPhysicsNode *>(hit.actor->userData);
++ QCharacterController *trigger =
++ static_cast<QCharacterController *>(hit.controller->getUserData());
++
++ if (!trigger || !other)
++ return;
++
++ // Before either is read: one PxController::move() reports every shape
++ // it hits, and the handler for the first is free to delete a node the
++ // second one points at.
++ if (world->isNodeRemoved(other) || world->isNodeRemoved(trigger))
++ return;
++
++ if (!trigger->enableShapeHitCallback())
++ return;
++
++ QVector3D position = QPhysicsUtils::toQtType(physx::toVec3(hit.worldPos));
++ QVector3D impulse = QPhysicsUtils::toQtType(hit.dir * hit.length);
++ QVector3D normal = QPhysicsUtils::toQtType(hit.worldNormal);
++
++ emit trigger->shapeHit(other, position, impulse, normal);
++ }
++ void onControllerHit(const physx::PxControllersHit & /*hit*/) override { }
++ void onObstacleHit(const physx::PxControllerObstacleHit & /*hit*/) override { }
++
++ private:
++ QPhysicsWorld *world = nullptr;
++ };
++
++ QPhysXCharacterController::QPhysXCharacterController(QCharacterController *frontEnd)
++ : QAbstractPhysXNode(frontEnd)
++ {
++ }
++
++ void QPhysXCharacterController::cleanup(QPhysXWorld *physX)
++ {
++ PHYSX_RELEASE(controller);
++ delete reportCallback;
++ reportCallback = nullptr;
++ QAbstractPhysXNode::cleanup(physX);
++ }
++
++ void QPhysXCharacterController::init(QPhysicsWorld *world, QPhysXWorld * /*physX*/)
++ {
++ Q_ASSERT(!controller);
++
++ auto *characterController = static_cast<QCharacterController *>(frontendNode);
++
++ auto shapes = characterController->getCollisionShapesList();
++ if (shapes.length() != 1) {
++ qWarning() << "CharacterController: invalid collision shapes list.";
++ return;
++ }
++ auto *capsule = qobject_cast<QCapsuleShape *>(shapes.first());
++ if (!capsule) {
++ qWarning() << "CharacterController: collision shape is not a capsule.";
++ return;
++ }
++ auto *mgr = world->controllerManager();
++ if (!mgr) {
++ qWarning() << "QtQuick3DPhysics internal error: missing controller manager.";
++ return;
++ }
++
++ updateMaterial();
++
++ const QVector3D scale = characterController->sceneScale();
++ const qreal heightScale = scale.y();
++ const qreal radiusScale = scale.x();
++ physx::PxCapsuleControllerDesc desc;
++ reportCallback = new ControllerCallback(world);
++ desc.reportCallback = reportCallback;
++ desc.radius = 0.5f * radiusScale * capsule->diameter();
++ desc.height = heightScale * capsule->height();
++ desc.stepOffset = 0.25f * desc.height; // TODO: API
++
++ desc.material = material;
++ const QVector3D pos = characterController->scenePosition();
++ desc.position = { pos.x(), pos.y(), pos.z() };
++ // Safe to static_cast since createController will always return a PxCapsuleController
++ // if desc is of type PxCapsuleControllerDesc
++ controller = static_cast<physx::PxCapsuleController *>(mgr->createController(desc));
++
++ if (!controller) {
++ qWarning() << "QtQuick3DPhysics internal error: could not create controller.";
++ return;
++ }
++
++ controller->setUserData(static_cast<void *>(frontendNode));
++
++ auto *actor = controller->getActor();
++ if (actor)
++ actor->userData = characterController;
++ else
++ qWarning() << "QtQuick3DPhysics internal error: CharacterController created without actor.";
++
++ // No collision shape in the userData here, unlike the shapes a body builds,
++ // which is how a trigger tells one body's pairs apart. A controller has
++ // exactly one shape, checked above, so its pairs are one per trigger shape
++ // either way, and pointing at the capsule would go stale: the controller's
++ // shape is never rebuilt, so swapping the capsule would re-report nothing.
++ }
++
++ void QPhysXCharacterController::sync(float deltaTime,
++ QHash<QQuick3DNode *, QMatrix4x4> & /*transformCache*/)
++ {
++ if (controller == nullptr)
++ return;
++
++ auto *characterController = static_cast<QCharacterController *>(frontendNode);
++
++ // Update capsule height, radius, stepOffset
++ const auto &shapes = characterController->getCollisionShapesList();
++ auto capsule = shapes.length() == 1 ? qobject_cast<QCapsuleShape *>(shapes.front()) : nullptr;
++
++ if (shapes.length() != 1) {
++ qWarning() << "CharacterController: invalid collision shapes list.";
++ } else if (!capsule) {
++ qWarning() << "CharacterController: collision shape is not a capsule.";
++ } else {
++ const QVector3D sceneScale = characterController->sceneScale();
++ const qreal heightScale = sceneScale.y();
++ const qreal radiusScale = sceneScale.x();
++
++ // Update height
++ const float heightNew = heightScale * capsule->height();
++ // Update radius
++ const float radiusNew = 0.5f * radiusScale * capsule->diameter();
++
++ if (!qIsFinite(heightNew) || heightNew <= 0.f || !qIsFinite(radiusNew)
++ || radiusNew <= 0.f) {
++ qWarning() << "CharacterController: scaled capsule height/radius must be finite "
++ "and positive, ignoring.";
++ } else {
++ if (!qFuzzyCompare(controller->getHeight(), heightNew))
++ controller->resize(heightNew);
++ if (!qFuzzyCompare(controller->getRadius(), radiusNew))
++ controller->setRadius(radiusNew);
++ }
++ // Update stepOffset
++ const float stepOffsetNew = 0.25f * heightNew;
++ if (!qFuzzyCompare(controller->getStepOffset(), stepOffsetNew))
++ controller->setStepOffset(stepOffsetNew);
++ }
++
++ // update node from physX
++ QVector3D position = QPhysicsUtils::toQtType(physx::toVec3(controller->getPosition()));
++ const QQuick3DNode *parentNode = static_cast<QQuick3DNode *>(characterController->parentItem());
++ if (!parentNode) {
++ // then it is the same space
++ characterController->setPosition(position);
++ } else {
++ characterController->setPosition(parentNode->mapPositionFromScene(position));
++ }
++
++ // setPosition() ran the controller's bindings, which can delete it.
++ if (!frontendNode)
++ return;
++
++ QVector3D teleportPos;
++ bool teleport = characterController->getTeleport(teleportPos);
++ if (teleport) {
++ controller->setPosition({ teleportPos.x(), teleportPos.y(), teleportPos.z() });
++ } else if (deltaTime > 0) {
++ const auto displacement =
++ QPhysicsUtils::toPhysXType(characterController->getDisplacement(deltaTime));
++ auto collisions =
++ controller->move(displacement, displacement.magnitude() / 100, deltaTime, {});
++ // PxController::move() reports what it hits, and a handler for that
++ // can delete the controller.
++ if (!frontendNode)
++ return;
++ characterController->setCollisions(QCharacterController::Collisions(uint(collisions)));
++ }
++
++ // setCollisions() ran the controller's bindings, which can delete it.
++ if (!frontendNode)
++ return;
++
++ // Materials are shared between nodes, so a change of the material properties moves this
++ // node to another material, which the shape of the controller then has to be pointed at.
++ // The shape is created exclusive to the actor of the controller, so it stays writable.
++ if (updateMaterial() && material) {
++ physx::PxShape *shape = nullptr;
++ auto *actor = controller->getActor();
++ // The controller is created with an actor holding exactly one shape, its capsule
++ if (actor && actor->getShapes(&shape, 1) == 1)
++ shape->setMaterials(&material, 1);
++ }
++ }
++
++ QPhysicsMaterial *QPhysXCharacterController::qtMaterial() const
++ {
++ return static_cast<QCharacterController *>(frontendNode)->physicsMaterial();
++ }
++
++ bool QPhysXCharacterController::debugGeometryCapability()
++ {
++ return true;
++ }
++
++ DebugDrawBodyType QPhysXCharacterController::getDebugDrawBodyType()
++ {
++ return DebugDrawBodyType::Character;
++ }
++
++ void QPhysXCharacterController::detachFrontend()
++ {
++ if (controller) {
++ controller->setUserData(nullptr);
++ if (auto *actor = controller->getActor())
++ actor->userData = nullptr;
++ }
++
++ QAbstractPhysXNode::detachFrontend();
++ }
++
++ QT_END_NAMESPACE