Revision control

Copy as Markdown

// 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