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 "qphysxactorbody_p.h"
#include "PxMaterial.h"
#include "PxPhysics.h"
#include "PxRigidDynamic.h"
#include "PxRigidActor.h"
#include "PxScene.h"
#include "physxnode/qphysxworld_p.h"
#include "qabstractphysicsbody_p.h"
#include "qheightfieldshape_p.h"
#include "qphysicsutils_p.h"
#include "qplaneshape_p.h"
#include "qstaticphysxobjects_p.h"
#include <QtGui/qquaternion.h>
#define PHYSX_RELEASE(x) \
if (x != nullptr) { \
x->release(); \
x = nullptr; \
}
QT_BEGIN_NAMESPACE
static physx::PxTransform getPhysXLocalTransform(const QQuick3DNode *node)
{
// Modify transforms to make the PhysX shapes match the QtQuick3D conventions
if (qobject_cast<const QPlaneShape *>(node) != nullptr) {
// Rotate the plane to make it match the built-in rectangle
const QQuaternion rotation = QPhysicsUtils::kMinus90YawRotation * node->rotation();
return physx::PxTransform(QPhysicsUtils::toPhysXType(node->position()),
QPhysicsUtils::toPhysXType(rotation));
} else if (auto *hf = qobject_cast<const QHeightFieldShape *>(node)) {
// Shift the height field so it's centered at the origin
return physx::PxTransform(QPhysicsUtils::toPhysXType(node->position() + hf->hfOffset()),
QPhysicsUtils::toPhysXType(node->rotation()));
}
const QQuaternion &rotation = node->rotation();
const QVector3D &localPosition = node->position();
const QVector3D &scale = node->sceneScale();
return physx::PxTransform(QPhysicsUtils::toPhysXType(localPosition * scale),
QPhysicsUtils::toPhysXType(rotation));
}
QPhysXActorBody::QPhysXActorBody(QAbstractPhysicsNode *frontEnd) : QAbstractPhysXNode(frontEnd) { }
void QPhysXActorBody::cleanup(QPhysXWorld *physX)
{
if (actor) {
physX->scene->removeActor(*actor);
PHYSX_RELEASE(actor);
}
QAbstractPhysXNode::cleanup(physX);
}
void QPhysXActorBody::init(QPhysicsWorld * /*world*/, QPhysXWorld *physX)
{
Q_ASSERT(!actor);
updateMaterial();
createActor(physX);
actor->userData = reinterpret_cast<void *>(frontendNode);
physX->scene->addActor(*actor);
setShapesDirty(true);
}
void QPhysXActorBody::sync(float /*deltaTime*/,
QHash<QQuick3DNode *, QMatrix4x4> & /*transformCache*/)
{
// The physicsMaterial property, and the properties of the material itself, can change while
// the simulation is running. Materials are shared between all nodes that need the same
// properties, so rather than modifying the material in place, the node is moved to the
// material with the new properties, and its shapes are pointed at it.
if (updateMaterial() && material) {
for (auto *shape : std::as_const(shapes))
shape->setMaterials(&material, 1);
}
}
void QPhysXActorBody::markDirtyShapes()
{
if (!frontendNode || !actor)
return;
// Go through the shapes and look for a change in pose (rotation, position)
// TODO: it is likely cheaper to connect a signal for changes on the position and rotation
// property and mark the node dirty then.
if (!shapesDirty()) {
const auto &collisionShapes = frontendNode->getCollisionShapesList();
const auto &physXShapes = shapes;
const int len = collisionShapes.size();
if (physXShapes.size() != len) {
// This should not really happen but check it anyway
setShapesDirty(true);
} else {
for (int i = 0; i < len; i++) {
auto poseNew = getPhysXLocalTransform(collisionShapes[i]);
auto poseOld = physXShapes[i]->getLocalPose();
if (!QPhysicsUtils::fuzzyEquals(poseNew, poseOld)) {
setShapesDirty(true);
break;
}
}
}
}
}
void QPhysXActorBody::rebuildDirtyShapes(QPhysicsWorld * /*world*/, QPhysXWorld *physX)
{
if (!shapesDirty())
return;
buildShapes(physX);
setShapesDirty(false);
}
void QPhysXActorBody::detachFrontend()
{
if (actor)
actor->userData = nullptr;
QAbstractPhysXNode::detachFrontend();
}
void QPhysXActorBody::createActor(QPhysXWorld * /*physX*/)
{
auto &s_physx = StaticPhysXObjects::getReference();
physx::PxTransform trf = QPhysicsUtils::toPhysXTransform(frontendNode->scenePosition(),
frontendNode->sceneRotation());
if (!trf.isSane()) {
qWarning() << "PhysicsNode: position/rotation is not finite, using identity instead.";
trf = physx::PxTransform(physx::PxIdentity);
}
auto *dynamicActor = s_physx.physics->createRigidDynamic(trf);
// A trigger has to be a dynamic actor, since PhysX only reports a trigger
// pair when one of the two is, but it is not simulated and nothing reads
// the velocity gravity would give it. Without gravity it stays where it is
// put and can fall asleep, which is what stops its pairs being tested:
// PhysX skips a trigger pair's overlap test only when both of its actors
// are asleep (see Sc::TriggerInteraction).
if (useTriggerFlag())
dynamicActor->setActorFlag(physx::PxActorFlag::eDISABLE_GRAVITY, true);
actor = dynamicActor;
}
bool QPhysXActorBody::debugGeometryCapability()
{
return true;
}
physx::PxTransform QPhysXActorBody::getGlobalPose()
{
return actor->getGlobalPose();
}
void QPhysXActorBody::buildShapes(QPhysXWorld * /*physX*/)
{
auto body = actor;
for (auto *shape : std::as_const(shapes)) {
body->detachShape(*shape);
PHYSX_RELEASE(shape);
}
// TODO: Only remove changed shapes?
shapes.clear();
for (const auto &collisionShape : frontendNode->getCollisionShapesList()) {
auto *geom = collisionShape->getPhysXGeometry();
if (!geom || !material)
continue;
auto &s_physx = StaticPhysXObjects::getReference();
// The shape is created exclusive to this actor: a shape that may be shared between
// actors cannot be modified once it is attached to one, and these shapes are modified
// while attached, for instance when their filter data changes.
auto physXShape = s_physx.physics->createShape(*geom, *material, true);
if (!physXShape) {
qWarning() << "QtQuick3DPhysics: could not create shape, invalid geometry.";
continue;
}
// Ties the PxShape back to the collision shape it was built for: a
// trigger pair is matched by those, which survive a rebuild replacing
// every PxShape of a node.
physXShape->userData = reinterpret_cast<void *>(collisionShape);
if (useTriggerFlag()) {
physXShape->setFlag(physx::PxShapeFlag::eSCENE_QUERY_SHAPE, false);
physXShape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, false);
physXShape->setFlag(physx::PxShapeFlag::eTRIGGER_SHAPE, true);
}
{ // Setup filtering
physx::PxFilterData filterData;
filterData.word0 = frontendNode->filterGroup();
filterData.word1 = frontendNode->filterIgnoreGroups();
physXShape->setSimulationFilterData(filterData);
}
physx::PxTransform localPose = getPhysXLocalTransform(collisionShape);
if (!localPose.isSane()) {
qWarning() << "QtQuick3DPhysics: collision shape position/rotation is not finite, "
"using identity local pose instead.";
localPose = physx::PxTransform(physx::PxIdentity);
}
physXShape->setLocalPose(localPose);
// A shape PhysX will not take is not kept: left in 'shapes' it would make
// markDirtyShapes() find the body's shapes to be in order, so nothing would
// ever rebuild them, and the next rebuild would detach a shape that had never
// been attached.
if (!body->attachShape(*physXShape)) {
qWarning() << "QtQuick3DPhysics: could not attach collision shape to body, it will "
"not take part in the simulation.";
PHYSX_RELEASE(physXShape);
continue;
}
shapes.push_back(physXShape);
}
// Filters are always clean after building shapes
setFiltersDirty(false);
}
void QPhysXActorBody::updateFilters()
{
if (!filtersDirty())
return;
// Go through all shapes and set the filter group and mask.
// TODO: What about shared shapes on several actors?
for (auto &physXShape : shapes) {
physx::PxFilterData filterData;
filterData.word0 = frontendNode->filterGroup();
filterData.word1 = frontendNode->filterIgnoreGroups();
physXShape->setSimulationFilterData(filterData);
}
setFiltersDirty(false);
}
QT_END_NAMESPACE