diff --git a/interface/src/InterfaceDynamicFactory.cpp b/interface/src/InterfaceDynamicFactory.cpp index 5951ccef9e..5acc0700c0 100644 --- a/interface/src/InterfaceDynamicFactory.cpp +++ b/interface/src/InterfaceDynamicFactory.cpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include #include #include "InterfaceDynamicFactory.h" @@ -38,9 +41,15 @@ EntityDynamicPointer interfaceDynamicFactory(EntityDynamicType type, const QUuid return std::make_shared(id, ownerEntity); case DYNAMIC_TYPE_FAR_GRAB: return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_SLIDER: + return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_BALL_SOCKET: + return std::make_shared(id, ownerEntity); + case DYNAMIC_TYPE_CONE_TWIST: + return std::make_shared(id, ownerEntity); } - Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown entity dynamic type"); + qDebug() << "Unknown entity dynamic type"; return EntityDynamicPointer(); } diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 98ff687eb3..627dc2ba02 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -113,7 +113,8 @@ void AvatarActionHold::prepareForPhysicsSimulation() { } bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, - glm::vec3& linearVelocity, glm::vec3& angularVelocity) { + glm::vec3& linearVelocity, glm::vec3& angularVelocity, + float& linearTimeScale, float& angularTimeScale) { auto avatarManager = DependencyManager::get(); auto holdingAvatar = std::static_pointer_cast(avatarManager->getAvatarBySessionID(_holderID)); @@ -213,6 +214,9 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm:: // update linearVelocity based on offset via _relativePosition; linearVelocity = linearVelocity + glm::cross(angularVelocity, position - palmPosition); + + linearTimeScale = _linearTimeScale; + angularTimeScale = _angularTimeScale; }); return true; diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index f0b42111ed..7eeda53e06 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -38,7 +38,8 @@ public: bool getAvatarRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); virtual bool getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, - glm::vec3& linearVelocity, glm::vec3& angularVelocity) override; + glm::vec3& linearVelocity, glm::vec3& angularVelocity, + float& linearTimeScale, float& angularTimeScale) override; virtual void prepareForPhysicsSimulation() override; diff --git a/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp index 2ab9a60397..57f86105b2 100644 --- a/libraries/entities/src/EntityDynamicInterface.cpp +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -117,6 +117,15 @@ EntityDynamicType EntityDynamicInterface::dynamicTypeFromString(QString dynamicT if (normalizedDynamicTypeString == "fargrab") { return DYNAMIC_TYPE_FAR_GRAB; } + if (normalizedDynamicTypeString == "slider") { + return DYNAMIC_TYPE_SLIDER; + } + if (normalizedDynamicTypeString == "ballsocket") { + return DYNAMIC_TYPE_BALL_SOCKET; + } + if (normalizedDynamicTypeString == "conetwist") { + return DYNAMIC_TYPE_CONE_TWIST; + } qCDebug(entities) << "Warning -- EntityDynamicInterface::dynamicTypeFromString got unknown dynamic-type name" << dynamicTypeString; @@ -139,6 +148,12 @@ QString EntityDynamicInterface::dynamicTypeToString(EntityDynamicType dynamicTyp return "hinge"; case DYNAMIC_TYPE_FAR_GRAB: return "far-grab"; + case DYNAMIC_TYPE_SLIDER: + return "slider"; + case DYNAMIC_TYPE_BALL_SOCKET: + return "ball-socket"; + case DYNAMIC_TYPE_CONE_TWIST: + return "cone-twist"; } assert(false); return "none"; diff --git a/libraries/entities/src/EntityDynamicInterface.h b/libraries/entities/src/EntityDynamicInterface.h index 93d9ffa43e..c04aaf67b2 100644 --- a/libraries/entities/src/EntityDynamicInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -31,7 +31,10 @@ enum EntityDynamicType { DYNAMIC_TYPE_HOLD = 3000, DYNAMIC_TYPE_TRAVEL_ORIENTED = 4000, DYNAMIC_TYPE_HINGE = 5000, - DYNAMIC_TYPE_FAR_GRAB = 6000 + DYNAMIC_TYPE_FAR_GRAB = 6000, + DYNAMIC_TYPE_SLIDER = 7000, + DYNAMIC_TYPE_BALL_SOCKET = 8000, + DYNAMIC_TYPE_CONE_TWIST = 9000 }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index adaa7a848c..82b4bf703d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_HINGE_CONSTRAINT; + return VERSION_ENTITIES_BULLET_DYNAMICS; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index f803b83887..746ae80361 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -208,6 +208,7 @@ const PacketVersion VERSION_ENTITIES_SERVER_SCRIPTS = 66; const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; const PacketVersion VERSION_ENTITIES_HINGE_CONSTRAINT = 69; +const PacketVersion VERSION_ENTITIES_BULLET_DYNAMICS = 70; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, diff --git a/libraries/physics/src/ObjectActionSpring.cpp b/libraries/physics/src/ObjectActionSpring.cpp index df7e5f87a3..8c73f43d42 100644 --- a/libraries/physics/src/ObjectActionSpring.cpp +++ b/libraries/physics/src/ObjectActionSpring.cpp @@ -16,6 +16,7 @@ #include "PhysicsLogging.h" const float SPRING_MAX_SPEED = 10.0f; +const float MAX_SPRING_TIMESCALE = 600.0f; // 10 min is a long time const uint16_t ObjectActionSpring::springVersion = 1; @@ -41,12 +42,65 @@ ObjectActionSpring::~ObjectActionSpring() { #endif } +SpatiallyNestablePointer ObjectActionSpring::getOther() { + SpatiallyNestablePointer other; + withWriteLock([&]{ + if (_otherID == QUuid()) { + // no other + return; + } + other = _other.lock(); + if (other && other->getID() == _otherID) { + // other is already up-to-date + return; + } + if (other) { + // we have a pointer to other, but it's wrong + other.reset(); + _other.reset(); + } + // we have an other-id but no pointer to other cached + QSharedPointer parentFinder = DependencyManager::get(); + if (!parentFinder) { + return; + } + EntityItemPointer ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + bool success; + _other = parentFinder->find(_otherID, success, ownerEntity->getParentTree()); + if (success) { + other = _other.lock(); + } + }); + return other; +} + bool ObjectActionSpring::getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, - glm::vec3& linearVelocity, glm::vec3& angularVelocity) { - rotation = _desiredRotationalTarget; - position = _desiredPositionalTarget; - linearVelocity = glm::vec3(); - angularVelocity = glm::vec3(); + glm::vec3& linearVelocity, glm::vec3& angularVelocity, + float& linearTimeScale, float& angularTimeScale) { + SpatiallyNestablePointer other = getOther(); + withReadLock([&]{ + linearTimeScale = _linearTimeScale; + angularTimeScale = _angularTimeScale; + + if (!_otherID.isNull()) { + if (other) { + rotation = _desiredRotationalTarget * other->getRotation(); + position = other->getRotation() * _desiredPositionalTarget + other->getPosition(); + } else { + // we should have an "other" but can't find it, so disable the spring. + linearTimeScale = FLT_MAX; + angularTimeScale = FLT_MAX; + } + } else { + rotation = _desiredRotationalTarget; + position = _desiredPositionalTarget; + } + linearVelocity = glm::vec3(); + angularVelocity = glm::vec3(); + }); return true; } @@ -61,8 +115,10 @@ bool ObjectActionSpring::prepareForSpringUpdate(btScalar deltaTimeStep) { glm::vec3 linearVelocity; glm::vec3 angularVelocity; - bool valid = false; - int springCount = 0; + bool linearValid = false; + int linearSpringCount = 0; + bool angularValid = false; + int angularSpringCount = 0; QList springDerivedActions; springDerivedActions.append(ownerEntity->getActionsOfType(DYNAMIC_TYPE_SPRING)); @@ -73,41 +129,55 @@ bool ObjectActionSpring::prepareForSpringUpdate(btScalar deltaTimeStep) { std::shared_ptr springAction = std::static_pointer_cast(action); glm::quat rotationForAction; glm::vec3 positionForAction; - glm::vec3 linearVelocityForAction, angularVelocityForAction; - bool success = springAction->getTarget(deltaTimeStep, rotationForAction, - positionForAction, linearVelocityForAction, - angularVelocityForAction); + glm::vec3 linearVelocityForAction; + glm::vec3 angularVelocityForAction; + float linearTimeScale; + float angularTimeScale; + bool success = springAction->getTarget(deltaTimeStep, + rotationForAction, positionForAction, + linearVelocityForAction, angularVelocityForAction, + linearTimeScale, angularTimeScale); if (success) { - springCount ++; - if (springAction.get() == this) { - // only use the rotation for this action - valid = true; - rotation = rotationForAction; + if (angularTimeScale < MAX_SPRING_TIMESCALE) { + angularValid = true; + angularSpringCount++; + angularVelocity += angularVelocityForAction; + if (springAction.get() == this) { + // only use the rotation for this action + rotation = rotationForAction; + } } - position += positionForAction; - linearVelocity += linearVelocityForAction; - angularVelocity += angularVelocityForAction; + if (linearTimeScale < MAX_SPRING_TIMESCALE) { + linearValid = true; + linearSpringCount++; + position += positionForAction; + linearVelocity += linearVelocityForAction; + } } } - if (valid && springCount > 0) { - position /= springCount; - linearVelocity /= springCount; - angularVelocity /= springCount; - + if ((angularValid && angularSpringCount > 0) || (linearValid && linearSpringCount > 0)) { withWriteLock([&]{ - _positionalTarget = position; - _rotationalTarget = rotation; - _linearVelocityTarget = linearVelocity; - _angularVelocityTarget = angularVelocity; - _positionalTargetSet = true; - _rotationalTargetSet = true; - _active = true; + if (linearValid && linearSpringCount > 0) { + position /= linearSpringCount; + linearVelocity /= linearSpringCount; + _positionalTarget = position; + _linearVelocityTarget = linearVelocity; + _positionalTargetSet = true; + _active = true; + } + if (angularValid && angularSpringCount > 0) { + angularVelocity /= angularSpringCount; + _rotationalTarget = rotation; + _angularVelocityTarget = angularVelocity; + _rotationalTargetSet = true; + _active = true; + } }); } - return valid; + return linearValid || angularValid; } @@ -133,8 +203,7 @@ void ObjectActionSpring::updateActionWorker(btScalar deltaTimeStep) { return; } - const float MAX_TIMESCALE = 600.0f; // 10 min is a long time - if (_linearTimeScale < MAX_TIMESCALE) { + if (_linearTimeScale < MAX_SPRING_TIMESCALE) { btVector3 targetVelocity(0.0f, 0.0f, 0.0f); btVector3 offset = rigidBody->getCenterOfMassPosition() - glmToBullet(_positionalTarget); float offsetLength = offset.length(); @@ -150,7 +219,7 @@ void ObjectActionSpring::updateActionWorker(btScalar deltaTimeStep) { rigidBody->setLinearVelocity(targetVelocity); } - if (_angularTimeScale < MAX_TIMESCALE) { + if (_angularTimeScale < MAX_SPRING_TIMESCALE) { btVector3 targetVelocity(0.0f, 0.0f, 0.0f); btQuaternion bodyRotation = rigidBody->getOrientation(); @@ -189,6 +258,8 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { float linearTimeScale; glm::quat rotationalTarget; float angularTimeScale; + QUuid otherID; + bool needUpdate = false; bool somethingChanged = ObjectDynamic::updateArguments(arguments); @@ -218,11 +289,19 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { angularTimeScale = _angularTimeScale; } + ok = true; + otherID = QUuid(EntityDynamicInterface::extractStringArgument("spring action", + arguments, "otherID", ok, false)); + if (!ok) { + otherID = _otherID; + } + if (somethingChanged || positionalTarget != _desiredPositionalTarget || linearTimeScale != _linearTimeScale || rotationalTarget != _desiredRotationalTarget || - angularTimeScale != _angularTimeScale) { + angularTimeScale != _angularTimeScale || + otherID != _otherID) { // something changed needUpdate = true; } @@ -234,6 +313,7 @@ bool ObjectActionSpring::updateArguments(QVariantMap arguments) { _linearTimeScale = glm::max(MIN_TIMESCALE, glm::abs(linearTimeScale)); _desiredRotationalTarget = rotationalTarget; _angularTimeScale = glm::max(MIN_TIMESCALE, glm::abs(angularTimeScale)); + _otherID = otherID; _active = true; auto ownerEntity = _ownerEntity.lock(); @@ -256,6 +336,8 @@ QVariantMap ObjectActionSpring::getArguments() { arguments["targetRotation"] = glmToQMap(_desiredRotationalTarget); arguments["angularTimeScale"] = _angularTimeScale; + + arguments["otherID"] = _otherID; }); return arguments; } @@ -270,6 +352,7 @@ void ObjectActionSpring::serializeParameters(QDataStream& dataStream) const { dataStream << _rotationalTargetSet; dataStream << localTimeToServerTime(_expires); dataStream << _tag; + dataStream << _otherID; }); } @@ -302,6 +385,8 @@ void ObjectActionSpring::deserializeParameters(QByteArray serializedArguments, Q dataStream >> _tag; + dataStream >> _otherID; + _active = true; }); } diff --git a/libraries/physics/src/ObjectActionSpring.h b/libraries/physics/src/ObjectActionSpring.h index de9562d3fa..8f810d7956 100644 --- a/libraries/physics/src/ObjectActionSpring.h +++ b/libraries/physics/src/ObjectActionSpring.h @@ -28,7 +28,8 @@ public: virtual void deserialize(QByteArray serializedArguments) override; virtual bool getTarget(float deltaTimeStep, glm::quat& rotation, glm::vec3& position, - glm::vec3& linearVelocity, glm::vec3& angularVelocity); + glm::vec3& linearVelocity, glm::vec3& angularVelocity, + float& linearTimeScale, float& angularTimeScale); protected: static const uint16_t springVersion; @@ -46,6 +47,10 @@ protected: glm::vec3 _linearVelocityTarget; glm::vec3 _angularVelocityTarget; + EntityItemID _otherID; + SpatiallyNestableWeakPointer _other; + SpatiallyNestablePointer getOther(); + virtual bool prepareForSpringUpdate(btScalar deltaTimeStep); void serializeParameters(QDataStream& dataStream) const; diff --git a/libraries/physics/src/ObjectConstraintBallSocket.cpp b/libraries/physics/src/ObjectConstraintBallSocket.cpp new file mode 100644 index 0000000000..35f138e840 --- /dev/null +++ b/libraries/physics/src/ObjectConstraintBallSocket.cpp @@ -0,0 +1,240 @@ +// +// ObjectConstraintBallSocket.cpp +// libraries/physics/src +// +// Created by Seth Alves 2017-4-29 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "QVariantGLM.h" + +#include "EntityTree.h" +#include "ObjectConstraintBallSocket.h" +#include "PhysicsLogging.h" + + +const uint16_t ObjectConstraintBallSocket::constraintVersion = 1; + + +ObjectConstraintBallSocket::ObjectConstraintBallSocket(const QUuid& id, EntityItemPointer ownerEntity) : + ObjectConstraint(DYNAMIC_TYPE_BALL_SOCKET, id, ownerEntity), + _pivotInA(glm::vec3(0.0f)), + _pivotInB(glm::vec3(0.0f)) +{ + #if WANT_DEBUG + qCDebug(physics) << "ObjectConstraintBallSocket::ObjectConstraintBallSocket"; + #endif +} + +ObjectConstraintBallSocket::~ObjectConstraintBallSocket() { + #if WANT_DEBUG + qCDebug(physics) << "ObjectConstraintBallSocket::~ObjectConstraintBallSocket"; + #endif +} + +QList ObjectConstraintBallSocket::getRigidBodies() { + QList result; + result += getRigidBody(); + QUuid otherEntityID; + withReadLock([&]{ + otherEntityID = _otherEntityID; + }); + if (!otherEntityID.isNull()) { + result += getOtherRigidBody(otherEntityID); + } + return result; +} + +void ObjectConstraintBallSocket::prepareForPhysicsSimulation() { +} + +void ObjectConstraintBallSocket::updateBallSocket() { + btPoint2PointConstraint* constraint { nullptr }; + + withReadLock([&]{ + constraint = static_cast(_constraint); + }); + + if (!constraint) { + return; + } + + constraint->setPivotA(glmToBullet(_pivotInA)); + constraint->setPivotB(glmToBullet(_pivotInB)); +} + + +btTypedConstraint* ObjectConstraintBallSocket::getConstraint() { + btPoint2PointConstraint* constraint { nullptr }; + QUuid otherEntityID; + glm::vec3 pivotInA; + glm::vec3 pivotInB; + + withReadLock([&]{ + constraint = static_cast(_constraint); + pivotInA = _pivotInA; + otherEntityID = _otherEntityID; + pivotInB = _pivotInB; + }); + if (constraint) { + return constraint; + } + + btRigidBody* rigidBodyA = getRigidBody(); + if (!rigidBodyA) { + qCDebug(physics) << "ObjectConstraintBallSocket::getConstraint -- no rigidBodyA"; + return nullptr; + } + + if (!otherEntityID.isNull()) { + // This constraint is between two entities... find the other rigid body. + + btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); + if (!rigidBodyB) { + return nullptr; + } + + constraint = new btPoint2PointConstraint(*rigidBodyA, *rigidBodyB, glmToBullet(pivotInA), glmToBullet(pivotInB)); + } else { + // This constraint is between an entity and the world-frame. + + constraint = new btPoint2PointConstraint(*rigidBodyA, glmToBullet(pivotInA)); + } + + withWriteLock([&]{ + _constraint = constraint; + }); + + // if we don't wake up rigidBodyA, we may not send the dynamicData property over the network + forceBodyNonStatic(); + activateBody(); + + updateBallSocket(); + + return constraint; +} + + +bool ObjectConstraintBallSocket::updateArguments(QVariantMap arguments) { + glm::vec3 pivotInA; + QUuid otherEntityID; + glm::vec3 pivotInB; + + bool needUpdate = false; + bool somethingChanged = ObjectDynamic::updateArguments(arguments); + withReadLock([&]{ + bool ok = true; + pivotInA = EntityDynamicInterface::extractVec3Argument("ball-socket constraint", arguments, "pivot", ok, false); + if (!ok) { + pivotInA = _pivotInA; + } + + ok = true; + otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("ball-socket constraint", + arguments, "otherEntityID", ok, false)); + if (!ok) { + otherEntityID = _otherEntityID; + } + + ok = true; + pivotInB = EntityDynamicInterface::extractVec3Argument("ball-socket constraint", arguments, "otherPivot", ok, false); + if (!ok) { + pivotInB = _pivotInB; + } + + if (somethingChanged || + pivotInA != _pivotInA || + otherEntityID != _otherEntityID || + pivotInB != _pivotInB) { + // something changed + needUpdate = true; + } + }); + + if (needUpdate) { + withWriteLock([&] { + _pivotInA = pivotInA; + _otherEntityID = otherEntityID; + _pivotInB = pivotInB; + + _active = true; + + auto ownerEntity = _ownerEntity.lock(); + if (ownerEntity) { + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); + } + }); + + updateBallSocket(); + } + + return true; +} + +QVariantMap ObjectConstraintBallSocket::getArguments() { + QVariantMap arguments = ObjectDynamic::getArguments(); + withReadLock([&] { + if (_constraint) { + arguments["pivot"] = glmToQMap(_pivotInA); + arguments["otherEntityID"] = _otherEntityID; + arguments["otherPivot"] = glmToQMap(_pivotInB); + } + }); + return arguments; +} + +QByteArray ObjectConstraintBallSocket::serialize() const { + QByteArray serializedConstraintArguments; + QDataStream dataStream(&serializedConstraintArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_BALL_SOCKET; + dataStream << getID(); + dataStream << ObjectConstraintBallSocket::constraintVersion; + + withReadLock([&] { + dataStream << localTimeToServerTime(_expires); + dataStream << _tag; + + dataStream << _pivotInA; + dataStream << _otherEntityID; + dataStream << _pivotInB; + }); + + return serializedConstraintArguments; +} + +void ObjectConstraintBallSocket::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + assert(type == getType()); + + QUuid id; + dataStream >> id; + assert(id == getID()); + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectConstraintBallSocket::constraintVersion) { + assert(false); + return; + } + + withWriteLock([&] { + quint64 serverExpires; + dataStream >> serverExpires; + _expires = serverTimeToLocalTime(serverExpires); + dataStream >> _tag; + + dataStream >> _pivotInA; + dataStream >> _otherEntityID; + dataStream >> _pivotInB; + + _active = true; + }); +} diff --git a/libraries/physics/src/ObjectConstraintBallSocket.h b/libraries/physics/src/ObjectConstraintBallSocket.h new file mode 100644 index 0000000000..9e0b942a6f --- /dev/null +++ b/libraries/physics/src/ObjectConstraintBallSocket.h @@ -0,0 +1,46 @@ +// +// ObjectConstraintBallSocket.h +// libraries/physics/src +// +// Created by Seth Alves 2017-4-29 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ObjectConstraintBallSocket_h +#define hifi_ObjectConstraintBallSocket_h + +#include "ObjectConstraint.h" + +// http://bulletphysics.org/Bullet/BulletFull/classbtBallSocketConstraint.html + +class ObjectConstraintBallSocket : public ObjectConstraint { +public: + ObjectConstraintBallSocket(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectConstraintBallSocket(); + + virtual void prepareForPhysicsSimulation() override; + + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; + + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; + + virtual QList getRigidBodies() override; + virtual btTypedConstraint* getConstraint() override; + +protected: + static const uint16_t constraintVersion; + + void updateBallSocket(); + + glm::vec3 _pivotInA; + + EntityItemID _otherEntityID; + glm::vec3 _pivotInB; +}; + +#endif // hifi_ObjectConstraintBallSocket_h diff --git a/libraries/physics/src/ObjectConstraintConeTwist.cpp b/libraries/physics/src/ObjectConstraintConeTwist.cpp new file mode 100644 index 0000000000..a0a9a5fe0c --- /dev/null +++ b/libraries/physics/src/ObjectConstraintConeTwist.cpp @@ -0,0 +1,367 @@ +// +// ObjectConstraintConeTwist.cpp +// libraries/physics/src +// +// Created by Seth Alves 2017-4-29 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "QVariantGLM.h" + +#include "EntityTree.h" +#include "ObjectConstraintConeTwist.h" +#include "PhysicsLogging.h" + + +const uint16_t ObjectConstraintConeTwist::constraintVersion = 1; + + +ObjectConstraintConeTwist::ObjectConstraintConeTwist(const QUuid& id, EntityItemPointer ownerEntity) : + ObjectConstraint(DYNAMIC_TYPE_CONE_TWIST, id, ownerEntity), + _pivotInA(glm::vec3(0.0f)), + _axisInA(glm::vec3(0.0f)) +{ + #if WANT_DEBUG + qCDebug(physics) << "ObjectConstraintConeTwist::ObjectConstraintConeTwist"; + #endif +} + +ObjectConstraintConeTwist::~ObjectConstraintConeTwist() { + #if WANT_DEBUG + qCDebug(physics) << "ObjectConstraintConeTwist::~ObjectConstraintConeTwist"; + #endif +} + +QList ObjectConstraintConeTwist::getRigidBodies() { + QList result; + result += getRigidBody(); + QUuid otherEntityID; + withReadLock([&]{ + otherEntityID = _otherEntityID; + }); + if (!otherEntityID.isNull()) { + result += getOtherRigidBody(otherEntityID); + } + return result; +} + +void ObjectConstraintConeTwist::prepareForPhysicsSimulation() { +} + +void ObjectConstraintConeTwist::updateConeTwist() { + btConeTwistConstraint* constraint { nullptr }; + float swingSpan1; + float swingSpan2; + float twistSpan; + float softness; + float biasFactor; + float relaxationFactor; + + withReadLock([&]{ + constraint = static_cast(_constraint); + swingSpan1 = _swingSpan1; + swingSpan2 = _swingSpan2; + twistSpan = _twistSpan; + softness = _softness; + biasFactor = _biasFactor; + relaxationFactor = _relaxationFactor; + }); + + if (!constraint) { + return; + } + + constraint->setLimit(swingSpan1, + swingSpan2, + twistSpan, + softness, + biasFactor, + relaxationFactor); +} + + +btTypedConstraint* ObjectConstraintConeTwist::getConstraint() { + btConeTwistConstraint* constraint { nullptr }; + QUuid otherEntityID; + glm::vec3 pivotInA; + glm::vec3 axisInA; + glm::vec3 pivotInB; + glm::vec3 axisInB; + + withReadLock([&]{ + constraint = static_cast(_constraint); + pivotInA = _pivotInA; + axisInA = _axisInA; + otherEntityID = _otherEntityID; + pivotInB = _pivotInB; + axisInB = _axisInB; + }); + if (constraint) { + return constraint; + } + + btRigidBody* rigidBodyA = getRigidBody(); + if (!rigidBodyA) { + qCDebug(physics) << "ObjectConstraintConeTwist::getConstraint -- no rigidBodyA"; + return nullptr; + } + + if (!otherEntityID.isNull()) { + // This coneTwist is between two entities... find the other rigid body. + + glm::quat rotA = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); + glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInB)); + + btTransform frameInA(glmToBullet(rotA), glmToBullet(pivotInA)); + btTransform frameInB(glmToBullet(rotB), glmToBullet(pivotInB)); + + btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); + if (!rigidBodyB) { + return nullptr; + } + + constraint = new btConeTwistConstraint(*rigidBodyA, *rigidBodyB, frameInA, frameInB); + } else { + // This coneTwist is between an entity and the world-frame. + + glm::quat rot = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); + + btTransform frameInA(glmToBullet(rot), glmToBullet(pivotInA)); + + constraint = new btConeTwistConstraint(*rigidBodyA, frameInA); + } + + withWriteLock([&]{ + _constraint = constraint; + }); + + // if we don't wake up rigidBodyA, we may not send the dynamicData property over the network + forceBodyNonStatic(); + activateBody(); + + updateConeTwist(); + + return constraint; +} + + +bool ObjectConstraintConeTwist::updateArguments(QVariantMap arguments) { + glm::vec3 pivotInA; + glm::vec3 axisInA; + QUuid otherEntityID; + glm::vec3 pivotInB; + glm::vec3 axisInB; + float swingSpan1; + float swingSpan2; + float twistSpan; + float softness; + float biasFactor; + float relaxationFactor; + + bool needUpdate = false; + bool somethingChanged = ObjectDynamic::updateArguments(arguments); + withReadLock([&]{ + bool ok = true; + pivotInA = EntityDynamicInterface::extractVec3Argument("coneTwist constraint", arguments, "pivot", ok, false); + if (!ok) { + pivotInA = _pivotInA; + } + + ok = true; + axisInA = EntityDynamicInterface::extractVec3Argument("coneTwist constraint", arguments, "axis", ok, false); + if (!ok) { + axisInA = _axisInA; + } + + ok = true; + otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("coneTwist constraint", + arguments, "otherEntityID", ok, false)); + if (!ok) { + otherEntityID = _otherEntityID; + } + + ok = true; + pivotInB = EntityDynamicInterface::extractVec3Argument("coneTwist constraint", arguments, "otherPivot", ok, false); + if (!ok) { + pivotInB = _pivotInB; + } + + ok = true; + axisInB = EntityDynamicInterface::extractVec3Argument("coneTwist constraint", arguments, "otherAxis", ok, false); + if (!ok) { + axisInB = _axisInB; + } + + ok = true; + swingSpan1 = EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "swingSpan1", ok, false); + if (!ok) { + swingSpan1 = _swingSpan1; + } + + ok = true; + swingSpan2 = EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "swingSpan2", ok, false); + if (!ok) { + swingSpan2 = _swingSpan2; + } + + ok = true; + twistSpan = EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "twistSpan", ok, false); + if (!ok) { + twistSpan = _twistSpan; + } + + ok = true; + softness = EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "softness", ok, false); + if (!ok) { + softness = _softness; + } + + ok = true; + biasFactor = EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "biasFactor", ok, false); + if (!ok) { + biasFactor = _biasFactor; + } + + ok = true; + relaxationFactor = + EntityDynamicInterface::extractFloatArgument("coneTwist constraint", arguments, "relaxationFactor", ok, false); + if (!ok) { + relaxationFactor = _relaxationFactor; + } + + if (somethingChanged || + pivotInA != _pivotInA || + axisInA != _axisInA || + otherEntityID != _otherEntityID || + pivotInB != _pivotInB || + axisInB != _axisInB || + swingSpan1 != _swingSpan1 || + swingSpan2 != _swingSpan2 || + twistSpan != _twistSpan || + softness != _softness || + biasFactor != _biasFactor || + relaxationFactor != _relaxationFactor) { + // something changed + needUpdate = true; + } + }); + + if (needUpdate) { + withWriteLock([&] { + _pivotInA = pivotInA; + _axisInA = axisInA; + _otherEntityID = otherEntityID; + _pivotInB = pivotInB; + _axisInB = axisInB; + _swingSpan1 = swingSpan1; + _swingSpan2 = swingSpan2; + _twistSpan = twistSpan; + _softness = softness; + _biasFactor = biasFactor; + _relaxationFactor = relaxationFactor; + + _active = true; + + auto ownerEntity = _ownerEntity.lock(); + if (ownerEntity) { + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); + } + }); + + updateConeTwist(); + } + + return true; +} + +QVariantMap ObjectConstraintConeTwist::getArguments() { + QVariantMap arguments = ObjectDynamic::getArguments(); + withReadLock([&] { + if (_constraint) { + arguments["pivot"] = glmToQMap(_pivotInA); + arguments["axis"] = glmToQMap(_axisInA); + arguments["otherEntityID"] = _otherEntityID; + arguments["otherPivot"] = glmToQMap(_pivotInB); + arguments["otherAxis"] = glmToQMap(_axisInB); + arguments["swingSpan1"] = _swingSpan1; + arguments["swingSpan2"] = _swingSpan2; + arguments["twistSpan"] = _twistSpan; + arguments["softness"] = _softness; + arguments["biasFactor"] = _biasFactor; + arguments["relaxationFactor"] = _relaxationFactor; + } + }); + return arguments; +} + +QByteArray ObjectConstraintConeTwist::serialize() const { + QByteArray serializedConstraintArguments; + QDataStream dataStream(&serializedConstraintArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_CONE_TWIST; + dataStream << getID(); + dataStream << ObjectConstraintConeTwist::constraintVersion; + + withReadLock([&] { + dataStream << localTimeToServerTime(_expires); + dataStream << _tag; + + dataStream << _pivotInA; + dataStream << _axisInA; + dataStream << _otherEntityID; + dataStream << _pivotInB; + dataStream << _axisInB; + dataStream << _swingSpan1; + dataStream << _swingSpan2; + dataStream << _twistSpan; + dataStream << _softness; + dataStream << _biasFactor; + dataStream << _relaxationFactor; + }); + + return serializedConstraintArguments; +} + +void ObjectConstraintConeTwist::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + assert(type == getType()); + + QUuid id; + dataStream >> id; + assert(id == getID()); + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectConstraintConeTwist::constraintVersion) { + assert(false); + return; + } + + withWriteLock([&] { + quint64 serverExpires; + dataStream >> serverExpires; + _expires = serverTimeToLocalTime(serverExpires); + dataStream >> _tag; + + dataStream >> _pivotInA; + dataStream >> _axisInA; + dataStream >> _otherEntityID; + dataStream >> _pivotInB; + dataStream >> _axisInB; + dataStream >> _swingSpan1; + dataStream >> _swingSpan2; + dataStream >> _twistSpan; + dataStream >> _softness; + dataStream >> _biasFactor; + dataStream >> _relaxationFactor; + + _active = true; + }); +} diff --git a/libraries/physics/src/ObjectConstraintConeTwist.h b/libraries/physics/src/ObjectConstraintConeTwist.h new file mode 100644 index 0000000000..02297e2b91 --- /dev/null +++ b/libraries/physics/src/ObjectConstraintConeTwist.h @@ -0,0 +1,55 @@ +// +// ObjectConstraintConeTwist.h +// libraries/physics/src +// +// Created by Seth Alves 2017-4-23 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ObjectConstraintConeTwist_h +#define hifi_ObjectConstraintConeTwist_h + +#include "ObjectConstraint.h" + +// http://bulletphysics.org/Bullet/BulletFull/classbtConeTwistConstraint.html + +class ObjectConstraintConeTwist : public ObjectConstraint { +public: + ObjectConstraintConeTwist(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectConstraintConeTwist(); + + virtual void prepareForPhysicsSimulation() override; + + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; + + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; + + virtual QList getRigidBodies() override; + virtual btTypedConstraint* getConstraint() override; + +protected: + static const uint16_t constraintVersion; + + void updateConeTwist(); + + glm::vec3 _pivotInA; + glm::vec3 _axisInA; + + EntityItemID _otherEntityID; + glm::vec3 _pivotInB; + glm::vec3 _axisInB; + + float _swingSpan1 { TWO_PI }; + float _swingSpan2 { TWO_PI };; + float _twistSpan { TWO_PI };; + float _softness { 1.0f }; + float _biasFactor {0.3f }; + float _relaxationFactor { 1.0f }; +}; + +#endif // hifi_ObjectConstraintConeTwist_h diff --git a/libraries/physics/src/ObjectConstraintHinge.cpp b/libraries/physics/src/ObjectConstraintHinge.cpp index 6c55d9c5dd..dd82de924c 100644 --- a/libraries/physics/src/ObjectConstraintHinge.cpp +++ b/libraries/physics/src/ObjectConstraintHinge.cpp @@ -48,23 +48,26 @@ QList ObjectConstraintHinge::getRigidBodies() { return result; } +void ObjectConstraintHinge::prepareForPhysicsSimulation() { +} + void ObjectConstraintHinge::updateHinge() { btHingeConstraint* constraint { nullptr }; + glm::vec3 axisInA; float low; float high; float softness; float biasFactor; float relaxationFactor; - float motorVelocity; withReadLock([&]{ + axisInA = _axisInA; constraint = static_cast(_constraint); low = _low; high = _high; - softness = _softness; biasFactor = _biasFactor; relaxationFactor = _relaxationFactor; - motorVelocity = _motorVelocity; + softness = _softness; }); if (!constraint) { @@ -72,12 +75,6 @@ void ObjectConstraintHinge::updateHinge() { } constraint->setLimit(low, high, softness, biasFactor, relaxationFactor); - if (motorVelocity != 0.0f) { - constraint->setMotorTargetVelocity(motorVelocity); - constraint->enableMotor(true); - } else { - constraint->enableMotor(false); - } } @@ -150,7 +147,6 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { float softness; float biasFactor; float relaxationFactor; - float motorVelocity; bool needUpdate = false; bool somethingChanged = ObjectDynamic::updateArguments(arguments); @@ -217,13 +213,6 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { relaxationFactor = _relaxationFactor; } - ok = true; - motorVelocity = EntityDynamicInterface::extractFloatArgument("hinge constraint", arguments, - "motorVelocity", ok, false); - if (!ok) { - motorVelocity = _motorVelocity; - } - if (somethingChanged || pivotInA != _pivotInA || axisInA != _axisInA || @@ -234,8 +223,7 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { high != _high || softness != _softness || biasFactor != _biasFactor || - relaxationFactor != _relaxationFactor || - motorVelocity != _motorVelocity) { + relaxationFactor != _relaxationFactor) { // something changed needUpdate = true; } @@ -253,7 +241,6 @@ bool ObjectConstraintHinge::updateArguments(QVariantMap arguments) { _softness = softness; _biasFactor = biasFactor; _relaxationFactor = relaxationFactor; - _motorVelocity = motorVelocity; _active = true; @@ -284,7 +271,6 @@ QVariantMap ObjectConstraintHinge::getArguments() { arguments["softness"] = _softness; arguments["biasFactor"] = _biasFactor; arguments["relaxationFactor"] = _relaxationFactor; - arguments["motorVelocity"] = _motorVelocity; arguments["angle"] = static_cast(_constraint)->getHingeAngle(); // [-PI,PI] } }); @@ -313,8 +299,6 @@ QByteArray ObjectConstraintHinge::serialize() const { dataStream << localTimeToServerTime(_expires); dataStream << _tag; - - dataStream << _motorVelocity; }); return serializedConstraintArguments; @@ -356,8 +340,6 @@ void ObjectConstraintHinge::deserialize(QByteArray serializedArguments) { dataStream >> _tag; - dataStream >> _motorVelocity; - _active = true; }); } diff --git a/libraries/physics/src/ObjectConstraintHinge.h b/libraries/physics/src/ObjectConstraintHinge.h index 7d2cac7511..07ce8eb8a3 100644 --- a/libraries/physics/src/ObjectConstraintHinge.h +++ b/libraries/physics/src/ObjectConstraintHinge.h @@ -21,6 +21,8 @@ public: ObjectConstraintHinge(const QUuid& id, EntityItemPointer ownerEntity); virtual ~ObjectConstraintHinge(); + virtual void prepareForPhysicsSimulation() override; + virtual bool updateArguments(QVariantMap arguments) override; virtual QVariantMap getArguments() override; @@ -42,12 +44,32 @@ protected: glm::vec3 _pivotInB; glm::vec3 _axisInB; - float _low { -2.0f * PI }; - float _high { 2.0f * PI }; + float _low { -TWO_PI }; + float _high { TWO_PI }; + + // https://gamedev.stackexchange.com/questions/71436/what-are-the-parameters-for-bthingeconstraintsetlimit + // + // softness: a negative measure of the friction that determines how much the hinge rotates for a given force. A high + // softness would make the hinge rotate easily like it's oiled then. + // biasFactor: an offset for the relaxed rotation of the hinge. It won't be right in the middle of the low and high angles + // anymore. 1.0f is the neural value. + // relaxationFactor: a measure of how much force is applied internally to bring the hinge in its central rotation. + // This is right in the middle of the low and high angles. For example, consider a western swing door. After + // walking through it will swing in both directions but at the end it stays right in the middle. + + // http://javadoc.jmonkeyengine.org/com/jme3/bullet/joints/HingeJoint.html + // + // _softness - the factor at which the velocity error correction starts operating, i.e. a softness of 0.9 means that + // the vel. corr starts at 90% of the limit range. + // _biasFactor - the magnitude of the position correction. It tells you how strictly the position error (drift) is + // corrected. + // _relaxationFactor - the rate at which velocity errors are corrected. This can be seen as the strength of the + // limits. A low value will make the the limits more spongy. + + float _softness { 0.9f }; float _biasFactor { 0.3f }; float _relaxationFactor { 1.0f }; - float _motorVelocity { 0.0f }; }; #endif // hifi_ObjectConstraintHinge_h diff --git a/libraries/physics/src/ObjectConstraintSlider.cpp b/libraries/physics/src/ObjectConstraintSlider.cpp new file mode 100644 index 0000000000..d7d4df78af --- /dev/null +++ b/libraries/physics/src/ObjectConstraintSlider.cpp @@ -0,0 +1,326 @@ +// +// ObjectConstraintSlider.cpp +// libraries/physics/src +// +// Created by Seth Alves 2017-4-23 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "QVariantGLM.h" + +#include "EntityTree.h" +#include "ObjectConstraintSlider.h" +#include "PhysicsLogging.h" + + +const uint16_t ObjectConstraintSlider::constraintVersion = 1; + + +ObjectConstraintSlider::ObjectConstraintSlider(const QUuid& id, EntityItemPointer ownerEntity) : + ObjectConstraint(DYNAMIC_TYPE_SLIDER, id, ownerEntity), + _pointInA(glm::vec3(0.0f)), + _axisInA(glm::vec3(0.0f)) +{ +} + +ObjectConstraintSlider::~ObjectConstraintSlider() { +} + +QList ObjectConstraintSlider::getRigidBodies() { + QList result; + result += getRigidBody(); + QUuid otherEntityID; + withReadLock([&]{ + otherEntityID = _otherEntityID; + }); + if (!otherEntityID.isNull()) { + result += getOtherRigidBody(otherEntityID); + } + return result; +} + +void ObjectConstraintSlider::prepareForPhysicsSimulation() { +} + +void ObjectConstraintSlider::updateSlider() { + btSliderConstraint* constraint { nullptr }; + + withReadLock([&]{ + constraint = static_cast(_constraint); + }); + + if (!constraint) { + return; + } + + // constraint->setFrames (const btTransform &frameA, const btTransform &frameB); + constraint->setLowerLinLimit(_linearLow); + constraint->setUpperLinLimit(_linearHigh); + constraint->setLowerAngLimit(_angularLow); + constraint->setUpperAngLimit(_angularHigh); + +} + + +btTypedConstraint* ObjectConstraintSlider::getConstraint() { + btSliderConstraint* constraint { nullptr }; + QUuid otherEntityID; + glm::vec3 pointInA; + glm::vec3 axisInA; + glm::vec3 pointInB; + glm::vec3 axisInB; + + withReadLock([&]{ + constraint = static_cast(_constraint); + pointInA = _pointInA; + axisInA = _axisInA; + otherEntityID = _otherEntityID; + pointInB = _pointInB; + axisInB = _axisInB; + }); + if (constraint) { + return constraint; + } + + btRigidBody* rigidBodyA = getRigidBody(); + if (!rigidBodyA) { + qCDebug(physics) << "ObjectConstraintSlider::getConstraint -- no rigidBodyA"; + return nullptr; + } + + if (!otherEntityID.isNull()) { + // This slider is between two entities... find the other rigid body. + + glm::quat rotA = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); + glm::quat rotB = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInB)); + + btTransform frameInA(glmToBullet(rotA), glmToBullet(pointInA)); + btTransform frameInB(glmToBullet(rotB), glmToBullet(pointInB)); + + btRigidBody* rigidBodyB = getOtherRigidBody(otherEntityID); + if (!rigidBodyB) { + return nullptr; + } + + constraint = new btSliderConstraint(*rigidBodyA, *rigidBodyB, frameInA, frameInB, true); + } else { + // This slider is between an entity and the world-frame. + + glm::quat rot = glm::rotation(glm::vec3(1.0f, 0.0f, 0.0f), glm::normalize(axisInA)); + + btTransform frameInA(glmToBullet(rot), glmToBullet(pointInA)); + + constraint = new btSliderConstraint(*rigidBodyA, frameInA, true); + } + + withWriteLock([&]{ + _constraint = constraint; + }); + + // if we don't wake up rigidBodyA, we may not send the dynamicData property over the network + forceBodyNonStatic(); + activateBody(); + + updateSlider(); + + return constraint; +} + + +bool ObjectConstraintSlider::updateArguments(QVariantMap arguments) { + glm::vec3 pointInA; + glm::vec3 axisInA; + QUuid otherEntityID; + glm::vec3 pointInB; + glm::vec3 axisInB; + float linearLow; + float linearHigh; + float angularLow; + float angularHigh; + + bool needUpdate = false; + bool somethingChanged = ObjectDynamic::updateArguments(arguments); + withReadLock([&]{ + bool ok = true; + pointInA = EntityDynamicInterface::extractVec3Argument("slider constraint", arguments, "point", ok, false); + if (!ok) { + pointInA = _pointInA; + } + + ok = true; + axisInA = EntityDynamicInterface::extractVec3Argument("slider constraint", arguments, "axis", ok, false); + if (!ok) { + axisInA = _axisInA; + } + + ok = true; + otherEntityID = QUuid(EntityDynamicInterface::extractStringArgument("slider constraint", + arguments, "otherEntityID", ok, false)); + if (!ok) { + otherEntityID = _otherEntityID; + } + + ok = true; + pointInB = EntityDynamicInterface::extractVec3Argument("slider constraint", arguments, "otherPoint", ok, false); + if (!ok) { + pointInB = _pointInB; + } + + ok = true; + axisInB = EntityDynamicInterface::extractVec3Argument("slider constraint", arguments, "otherAxis", ok, false); + if (!ok) { + axisInB = _axisInB; + } + + ok = true; + linearLow = EntityDynamicInterface::extractFloatArgument("slider constraint", arguments, "linearLow", ok, false); + if (!ok) { + linearLow = _linearLow; + } + + ok = true; + linearHigh = EntityDynamicInterface::extractFloatArgument("slider constraint", arguments, "linearHigh", ok, false); + if (!ok) { + linearHigh = _linearHigh; + } + + ok = true; + angularLow = EntityDynamicInterface::extractFloatArgument("slider constraint", arguments, "angularLow", ok, false); + if (!ok) { + angularLow = _angularLow; + } + + ok = true; + angularHigh = EntityDynamicInterface::extractFloatArgument("slider constraint", arguments, "angularHigh", ok, false); + if (!ok) { + angularHigh = _angularHigh; + } + + if (somethingChanged || + pointInA != _pointInA || + axisInA != _axisInA || + otherEntityID != _otherEntityID || + pointInB != _pointInB || + axisInB != _axisInB || + linearLow != _linearLow || + linearHigh != _linearHigh || + angularLow != _angularLow || + angularHigh != _angularHigh) { + // something changed + needUpdate = true; + } + }); + + if (needUpdate) { + withWriteLock([&] { + _pointInA = pointInA; + _axisInA = axisInA; + _otherEntityID = otherEntityID; + _pointInB = pointInB; + _axisInB = axisInB; + _linearLow = linearLow; + _linearHigh = linearHigh; + _angularLow = angularLow; + _angularHigh = angularHigh; + + _active = true; + + auto ownerEntity = _ownerEntity.lock(); + if (ownerEntity) { + ownerEntity->setDynamicDataDirty(true); + ownerEntity->setDynamicDataNeedsTransmit(true); + } + }); + + updateSlider(); + } + + return true; +} + +QVariantMap ObjectConstraintSlider::getArguments() { + QVariantMap arguments = ObjectDynamic::getArguments(); + withReadLock([&] { + if (_constraint) { + arguments["point"] = glmToQMap(_pointInA); + arguments["axis"] = glmToQMap(_axisInA); + arguments["otherEntityID"] = _otherEntityID; + arguments["otherPoint"] = glmToQMap(_pointInB); + arguments["otherAxis"] = glmToQMap(_axisInB); + arguments["linearLow"] = _linearLow; + arguments["linearHigh"] = _linearHigh; + arguments["angularLow"] = _angularLow; + arguments["angularHigh"] = _angularHigh; + arguments["linearPosition"] = static_cast(_constraint)->getLinearPos(); + arguments["angularPosition"] = static_cast(_constraint)->getAngularPos(); + } + }); + return arguments; +} + +QByteArray ObjectConstraintSlider::serialize() const { + QByteArray serializedConstraintArguments; + QDataStream dataStream(&serializedConstraintArguments, QIODevice::WriteOnly); + + dataStream << DYNAMIC_TYPE_SLIDER; + dataStream << getID(); + dataStream << ObjectConstraintSlider::constraintVersion; + + withReadLock([&] { + dataStream << localTimeToServerTime(_expires); + dataStream << _tag; + + dataStream << _pointInA; + dataStream << _axisInA; + dataStream << _otherEntityID; + dataStream << _pointInB; + dataStream << _axisInB; + dataStream << _linearLow; + dataStream << _linearHigh; + dataStream << _angularLow; + dataStream << _angularHigh; + }); + + return serializedConstraintArguments; +} + +void ObjectConstraintSlider::deserialize(QByteArray serializedArguments) { + QDataStream dataStream(serializedArguments); + + EntityDynamicType type; + dataStream >> type; + assert(type == getType()); + + QUuid id; + dataStream >> id; + assert(id == getID()); + + uint16_t serializationVersion; + dataStream >> serializationVersion; + if (serializationVersion != ObjectConstraintSlider::constraintVersion) { + assert(false); + return; + } + + withWriteLock([&] { + quint64 serverExpires; + dataStream >> serverExpires; + _expires = serverTimeToLocalTime(serverExpires); + dataStream >> _tag; + + dataStream >> _pointInA; + dataStream >> _axisInA; + dataStream >> _otherEntityID; + dataStream >> _pointInB; + dataStream >> _axisInB; + dataStream >> _linearLow; + dataStream >> _linearHigh; + dataStream >> _angularLow; + dataStream >> _angularHigh; + + _active = true; + }); +} diff --git a/libraries/physics/src/ObjectConstraintSlider.h b/libraries/physics/src/ObjectConstraintSlider.h new file mode 100644 index 0000000000..d616b9954c --- /dev/null +++ b/libraries/physics/src/ObjectConstraintSlider.h @@ -0,0 +1,54 @@ +// +// ObjectConstraintSlider.h +// libraries/physics/src +// +// Created by Seth Alves 2017-4-23 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ObjectConstraintSlider_h +#define hifi_ObjectConstraintSlider_h + +#include "ObjectConstraint.h" + +// http://bulletphysics.org/Bullet/BulletFull/classbtSliderConstraint.html + +class ObjectConstraintSlider : public ObjectConstraint { +public: + ObjectConstraintSlider(const QUuid& id, EntityItemPointer ownerEntity); + virtual ~ObjectConstraintSlider(); + + virtual void prepareForPhysicsSimulation() override; + + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; + + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; + + virtual QList getRigidBodies() override; + virtual btTypedConstraint* getConstraint() override; + +protected: + static const uint16_t constraintVersion; + + void updateSlider(); + + glm::vec3 _pointInA; + glm::vec3 _axisInA; + + EntityItemID _otherEntityID; + glm::vec3 _pointInB; + glm::vec3 _axisInB; + + float _linearLow { std::numeric_limits::max() }; + float _linearHigh { std::numeric_limits::min() }; + + float _angularLow { -TWO_PI }; + float _angularHigh { TWO_PI }; +}; + +#endif // hifi_ObjectConstraintSlider_h diff --git a/scripts/developer/tests/dynamics/dynamics-tests-interface.js b/scripts/developer/tests/dynamics/dynamics-tests-interface.js new file mode 100644 index 0000000000..7db8b86a05 --- /dev/null +++ b/scripts/developer/tests/dynamics/dynamics-tests-interface.js @@ -0,0 +1,76 @@ +// +// dynamics-tests-interface.js +// scripts/developer/tests/dynamics/ +// +// Created by Seth Alves 2017-4-30 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +"use strict"; +/* globals $, EventBridge */ + +var parameters = { + "lifetime":"integer" +}; + + +function getQueryArgByName(name, url) { + if (!url) { + url = window.location.href; + } + name = name.replace(/[\[\]]/g, "\\$&"); + var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, " ")); +} + + +function addCommandParameters(params) { + // copy from html elements into an associative-array which will get passed (as JSON) through the EventBridge + for (var parameterName in parameters) { + if (parameters.hasOwnProperty(parameterName)) { + var parameterType = parameters[parameterName]; + var strVal = $("#" + parameterName).val(); + if (parameterType == "integer") { + params[parameterName] = parseInt(strVal); + } else if (parameterType == "float") { + params[parameterName] = parseFloat(strVal); + } else { + params[parameterName] = strVal; + } + } + } + return params; +} + + +$(document).ready(function() { + // hook all buttons to EventBridge + $(":button").each(function(index) { + $(this).click(function() { + EventBridge.emitWebEvent(JSON.stringify(addCommandParameters({ "dynamics-tests-command": this.id }))); + }); + }); + + // copy parameters from query-args into elements + for (var parameterName in parameters) { + if (parameters.hasOwnProperty(parameterName)) { + var val = getQueryArgByName(parameterName); + if (val) { + var parameterType = parameters[parameterName]; + if (parameterType == "integer") { + val = parseInt(val); + } else if (parameterType == "float") { + val = parseFloat(val); + } + $("#" + parameterName).val(val.toString()); + } + } + } +}); diff --git a/scripts/developer/tests/dynamics/dynamics-tests.html b/scripts/developer/tests/dynamics/dynamics-tests.html new file mode 100644 index 0000000000..0f324e121c --- /dev/null +++ b/scripts/developer/tests/dynamics/dynamics-tests.html @@ -0,0 +1,37 @@ + + + Dynamics Tests + + + + + + + lifetime: +
+
+ A platform with a lever. The lever can be moved in a cone and rotated. A spring brings it back to its neutral position. +
+
+ A grabbable door with a hinge between it and world-space. +
+
+ A chain of blocks connected by hinges. +
+
+ The block can only move up and down over a range of 1/2 meter. +
+
+ A chain of blocks connected by slider constraints. +
+
+ A chain of spheres connected by ball-and-socket joints between the spheres. +
+
+ A chain of spheres connected by ball-and-socket joints coincident-with the spheres. +
+
+ A self-righting ragdoll. The head is on a weak spring vs the body. + + + diff --git a/scripts/developer/tests/dynamics/dynamicsTests.js b/scripts/developer/tests/dynamics/dynamicsTests.js new file mode 100644 index 0000000000..376eff182b --- /dev/null +++ b/scripts/developer/tests/dynamics/dynamicsTests.js @@ -0,0 +1,759 @@ +// +// dynamicsTests.js +// scripts/developer/tests/dynamics/ +// +// Created by Seth Alves 2017-4-30 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +"use strict"; + +/* global Entities, Script, Tablet, MyAvatar, Vec3 */ + +(function() { // BEGIN LOCAL_SCOPE + + var DYNAMICS_TESTS_URL = Script.resolvePath("dynamics-tests.html"); + var DEFAULT_LIFETIME = 120; // seconds + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + + var button = tablet.addButton({ + icon: Script.resolvePath("dynamicsTests.svg"), + text: "Dynamics", + sortOrder: 15 + }); + + + + function coneTwistAndSpringLeverTest(params) { + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: -0.5, z: -2})); + var lifetime = params.lifetime; + + var baseID = Entities.addEntity({ + name: "cone-twist test -- base", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: 0.5, y: 0.2, z: 0.5 }, + position: Vec3.sum(pos, { x: 0, y: 0, z:0 }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + var leverID = Entities.addEntity({ + name: "cone-twist test -- lever", + type: "Box", + color: { blue: 128, green: 100, red: 200 }, + dimensions: { x: 0.05, y: 1, z: 0.05 }, + position: Vec3.sum(pos, { x: 0, y: 0.7, z:0 }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addEntity({ + name: "cone-twist test -- handle", + type: "Box", + color: { blue: 30, green: 100, red: 200 }, + dimensions: { x: 0.1, y: 0.08, z: 0.08 }, + position: Vec3.sum(pos, { x: 0, y: 0.7 + 0.5, z:0 }), + dynamic: false, + collisionless: true, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + parentID: leverID, + userData: "{ \"grabbableKey\": { \"grabbable\": false } }" + }); + + Entities.addAction("cone-twist", baseID, { + pivot: { x: 0, y: 0.2, z: 0 }, + axis: { x: 0, y: 1, z: 0 }, + otherEntityID: leverID, + otherPivot: { x: 0, y: -0.55, z: 0 }, + otherAxis: { x: 0, y: 1, z: 0 }, + swingSpan1: Math.PI / 4, + swingSpan2: Math.PI / 4, + twistSpan: Math.PI / 2, + tag: "cone-twist test" + }); + + Entities.addAction("spring", leverID, { + targetRotation: { x: 0, y: 0, z: 0, w: 1 }, + angularTimeScale: 0.2, + tag: "cone-twist test spring" + }); + + + Entities.editEntity(baseID, { gravity: { x: 0, y: -5, z: 0 } }); + } + + function doorVSWorldTest(params) { + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})); + var lifetime = params.lifetime; + + var doorID = Entities.addEntity({ + name: "door test", + type: "Box", + color: { blue: 128, green: 20, red: 20 }, + dimensions: { x: 1.0, y: 2, z: 0.1 }, + position: pos, + dynamic: true, + collisionless: false, + lifetime: lifetime, + gravity: { x: 0, y: 0, z: 0 }, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("hinge", doorID, { + pivot: { x: -0.5, y: 0, z: 0 }, + axis: { x: 0, y: 1, z: 0 }, + low: 0, + high: Math.PI, + tag: "door hinge test" + }); + } + + function hingeChainTest(params) { + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})); + var lifetime = params.lifetime; + + var offset = 0.28; + var prevEntityID = null; + for (var i = 0; i < 5; i++) { + var newID = Entities.addEntity({ + name: "hinge test " + i, + type: "Box", + color: { blue: 128, green: 40 * i, red: 20 }, + dimensions: { x: 0.2, y: 0.2, z: 0.1 }, + position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}), + dynamic: true, + collisionless: false, + lifetime: lifetime, + gravity: { x: 0, y: 0, z: 0 }, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + if (prevEntityID) { + Entities.addAction("hinge", prevEntityID, { + pivot: { x: 0, y: offset / 2, z: 0 }, + axis: { x: 1, y: 0, z: 0 }, + otherEntityID: newID, + otherPivot: { x: 0, y: -offset / 2, z: 0 }, + otherAxis: { x: 1, y: 0, z: 0 }, + tag: "A/B hinge test " + i + }); + } + prevEntityID = newID; + } + } + + function sliderVSWorldTest(params) { + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})); + var lifetime = params.lifetime; + + var sliderEntityID = Entities.addEntity({ + name: "slider test", + type: "Box", + color: { blue: 128, green: 20, red: 20 }, + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + position: pos, + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("slider", sliderEntityID, { + point: { x: -0.5, y: 0, z: 0 }, + axis: { x: 0, y: 1, z: 0 }, + linearLow: 0, + linearHigh: 0.6, + tag: "slider test" + }); + } + + function sliderChainTest(params) { + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})); + var lifetime = params.lifetime; + + var offset = 0.28; + var prevEntityID = null; + for (var i = 0; i < 7; i++) { + var newID = Entities.addEntity({ + name: "hinge test " + i, + type: "Box", + color: { blue: 128, green: 40 * i, red: 20 }, + dimensions: { x: 0.2, y: 0.1, z: 0.2 }, + position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + if (prevEntityID) { + Entities.addAction("slider", prevEntityID, { + point: { x: 0, y: 0, z: 0 }, + axis: { x: 0, y: 1, z: 0 }, + otherEntityID: newID, + otherPoint: { x: 0, y: -offset / 2, z: 0 }, + otherAxis: { x: 0, y: 1, z: 0 }, + linearLow: 0, + linearHigh: 0.6, + tag: "A/B slider test " + i + }); + } + prevEntityID = newID; + } + } + + function ballSocketBetweenTest(params) { + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})); + var lifetime = params.lifetime; + + var offset = 0.2; + var diameter = offset - 0.01; + var prevEntityID = null; + for (var i = 0; i < 7; i++) { + var newID = Entities.addEntity({ + name: "ball and socket test " + i, + type: "Sphere", + color: { blue: 128, green: 40 * i, red: 20 }, + dimensions: { x: diameter, y: diameter, z: diameter }, + position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}), + dynamic: true, + collisionless: false, + lifetime: lifetime, + gravity: { x: 0, y: 0, z: 0 }, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + if (prevEntityID) { + Entities.addAction("ball-socket", prevEntityID, { + pivot: { x: 0, y: offset / 2, z: 0 }, + otherEntityID: newID, + otherPivot: { x: 0, y: -offset / 2, z: 0 }, + tag: "A/B ball-and-socket test " + i + }); + } + prevEntityID = newID; + } + } + + function ballSocketCoincidentTest(params) { + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})); + var lifetime = params.lifetime; + + var offset = 0.2; + var diameter = offset - 0.01; + var prevEntityID = null; + for (var i = 0; i < 7; i++) { + var newID = Entities.addEntity({ + name: "ball and socket test " + i, + type: "Sphere", + color: { blue: 128, green: 40 * i, red: 20 }, + dimensions: { x: diameter, y: diameter, z: diameter }, + position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}), + dynamic: true, + collisionless: false, + lifetime: lifetime, + gravity: { x: 0, y: 0, z: 0 }, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + if (prevEntityID) { + Entities.addAction("ball-socket", prevEntityID, { + pivot: { x: 0, y: 0, z: 0 }, + otherEntityID: newID, + otherPivot: { x: 0, y: offset, z: 0 }, + tag: "A/B ball-and-socket test " + i + }); + } + prevEntityID = newID; + } + } + + function ragdollTest(params) { + var scale = 1.6; + var lifetime = params.lifetime; + var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 1.0, z: -2})); + + var neckLength = scale * 0.05; + var shoulderGap = scale * 0.1; + var elbowGap = scale * 0.06; + var hipGap = scale * 0.07; + var kneeGap = scale * 0.08; + var ankleGap = scale * 0.06; + var ankleMin = 0; + var ankleMax = Math.PI / 4; + + var headSize = scale * 0.2; + + var bodyHeight = scale * 0.4; + var bodyWidth = scale * 0.3; + var bodyDepth = scale * 0.2; + + var upperArmThickness = scale * 0.05; + var upperArmLength = scale * 0.2; + + var lowerArmThickness = scale * 0.05; + var lowerArmLength = scale * 0.2; + + var legLength = scale * 0.3; + var legThickness = scale * 0.08; + + var shinLength = scale * 0.2; + var shinThickness = scale * 0.06; + + var footLength = scale * 0.2; + var footThickness = scale * 0.03; + var footWidth = scale * 0.08; + + + // + // body + // + + var bodyID = Entities.addEntity({ + name: "ragdoll body", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: bodyDepth, y: bodyHeight, z: bodyWidth }, + position: Vec3.sum(pos, { x: 0, y: scale * 0.0, z:0 }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + // + // head + // + + var headID = Entities.addEntity({ + name: "ragdoll head", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: headSize, y: headSize, z: headSize }, + position: Vec3.sum(pos, { x: 0, y: bodyHeight / 2 + headSize / 2 + neckLength, z:0 }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0.5, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("spring", headID, { + targetRotation: { x: 0, y: 0, z: 0, w: 1 }, + angularTimeScale: 2.0, + otherID: bodyID, + tag: "cone-twist test spring" + }); + + + var noseID = Entities.addEntity({ + name: "ragdoll nose", + type: "Box", + color: { blue: 128, green: 100, red: 100 }, + dimensions: { x: headSize / 5, y: headSize / 5, z: headSize / 5 }, + localPosition: { x: headSize / 2 + headSize / 10, y: 0, z: 0 }, + dynamic: false, + collisionless: true, + lifetime: lifetime, + parentID: headID, + userData: "{ \"grabbableKey\": { \"grabbable\": false } }" + }); + + Entities.addAction("cone-twist", headID, { + pivot: { x: 0, y: -headSize / 2 - neckLength / 2, z: 0 }, + axis: { x: 0, y: 1, z: 0 }, + otherEntityID: bodyID, + otherPivot: { x: 0, y: bodyHeight / 2 + neckLength / 2, z: 0 }, + otherAxis: { x: 0, y: 1, z: 0 }, + swingSpan1: Math.PI / 4, + swingSpan2: Math.PI / 4, + twistSpan: Math.PI / 2, + tag: "ragdoll neck joint" + }); + + // + // right upper arm + // + + var rightUpperArmID = Entities.addEntity({ + name: "ragdoll right arm", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: upperArmThickness, y: upperArmThickness, z: upperArmLength }, + position: Vec3.sum(pos, { x: 0, + y: bodyHeight / 2 + upperArmThickness / 2, + z: bodyWidth / 2 + shoulderGap + upperArmLength / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("cone-twist", bodyID, { + pivot: { x: 0, y: bodyHeight / 2 + upperArmThickness / 2, z: bodyWidth / 2 + shoulderGap / 2 }, + axis: { x: 0, y: 0, z: 1 }, + otherEntityID: rightUpperArmID, + otherPivot: { x: 0, y: 0, z: -upperArmLength / 2 - shoulderGap / 2 }, + otherAxis: { x: 0, y: 0, z: 1 }, + swingSpan1: Math.PI / 2, + swingSpan2: Math.PI / 2, + twistSpan: 0, + tag: "ragdoll right shoulder joint" + }); + + // + // left upper arm + // + + var leftUpperArmID = Entities.addEntity({ + name: "ragdoll left arm", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: upperArmThickness, y: upperArmThickness, z: upperArmLength }, + position: Vec3.sum(pos, { x: 0, + y: bodyHeight / 2 + upperArmThickness / 2, + z: -bodyWidth / 2 - shoulderGap - upperArmLength / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("cone-twist", bodyID, { + pivot: { x: 0, y: bodyHeight / 2 + upperArmThickness / 2, z: -bodyWidth / 2 - shoulderGap / 2 }, + axis: { x: 0, y: 0, z: -1 }, + otherEntityID: leftUpperArmID, + otherPivot: { x: 0, y: 0, z: upperArmLength / 2 + shoulderGap / 2 }, + otherAxis: { x: 0, y: 0, z: -1 }, + swingSpan1: Math.PI / 2, + swingSpan2: Math.PI / 2, + twistSpan: 0, + tag: "ragdoll left shoulder joint" + }); + + // + // right lower arm + // + + var rightLowerArmID = Entities.addEntity({ + name: "ragdoll right lower arm", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: lowerArmThickness, y: lowerArmThickness, z: lowerArmLength }, + position: Vec3.sum(pos, { x: 0, + y: bodyHeight / 2 - upperArmThickness / 2, + z: bodyWidth / 2 + shoulderGap + upperArmLength + elbowGap + lowerArmLength / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: -1, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("hinge", rightLowerArmID, { + pivot: { x: 0, y: 0, z: -lowerArmLength / 2 - elbowGap / 2 }, + axis: { x: 0, y: 1, z: 0 }, + otherEntityID: rightUpperArmID, + otherPivot: { x: 0, y: 0, z: upperArmLength / 2 + elbowGap / 2 }, + otherAxis: { x: 0, y: 1, z: 0 }, + low: Math.PI / -2, + high: 0, + tag: "ragdoll right elbow joint" + }); + + // + // left lower arm + // + + var leftLowerArmID = Entities.addEntity({ + name: "ragdoll left lower arm", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: lowerArmThickness, y: lowerArmThickness, z: lowerArmLength }, + position: Vec3.sum(pos, { x: 0, + y: bodyHeight / 2 - upperArmThickness / 2, + z: -bodyWidth / 2 - shoulderGap - upperArmLength - elbowGap - lowerArmLength / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: -1, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("hinge", leftLowerArmID, { + pivot: { x: 0, y: 0, z: lowerArmLength / 2 + elbowGap / 2 }, + axis: { x: 0, y: 1, z: 0 }, + otherEntityID: leftUpperArmID, + otherPivot: { x: 0, y: 0, z: -upperArmLength / 2 - elbowGap / 2 }, + otherAxis: { x: 0, y: 1, z: 0 }, + low: 0, + high: Math.PI / 2, + tag: "ragdoll left elbow joint" + }); + + // + // right leg + // + + var rightLegID = Entities.addEntity({ + name: "ragdoll right arm", + type: "Box", + color: { blue: 20, green: 200, red: 20 }, + dimensions: { x: legThickness, y: legLength, z: legThickness }, + position: Vec3.sum(pos, { x: 0, y: -bodyHeight / 2 - hipGap - legLength / 2, z: bodyWidth / 2 - legThickness / 2 }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("cone-twist", rightLegID, { + pivot: { x: 0, y: legLength / 2 + hipGap / 2, z: 0 }, + axis: { x: 0, y: 1, z: 0 }, + otherEntityID: bodyID, + otherPivot: { x: 0, y: -bodyHeight / 2 - hipGap / 2, z: bodyWidth / 2 - legThickness / 2 }, + otherAxis: Vec3.normalize({ x: -1, y: 1, z: 0 }), + swingSpan1: Math.PI / 4, + swingSpan2: Math.PI / 4, + twistSpan: 0, + tag: "ragdoll right hip joint" + }); + + // + // left leg + // + + var leftLegID = Entities.addEntity({ + name: "ragdoll left arm", + type: "Box", + color: { blue: 20, green: 200, red: 20 }, + dimensions: { x: legThickness, y: legLength, z: legThickness }, + position: Vec3.sum(pos, { x: 0, y: -bodyHeight / 2 - hipGap - legLength / 2, z: -bodyWidth / 2 + legThickness / 2 }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: 0, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("cone-twist", leftLegID, { + pivot: { x: 0, y: legLength / 2 + hipGap / 2, z: 0 }, + axis: { x: 0, y: 1, z: 0 }, + otherEntityID: bodyID, + otherPivot: { x: 0, y: -bodyHeight / 2 - hipGap / 2, z: -bodyWidth / 2 + legThickness / 2 }, + otherAxis: Vec3.normalize({ x: -1, y: 1, z: 0 }), + swingSpan1: Math.PI / 4, + swingSpan2: Math.PI / 4, + twistSpan: 0, + tag: "ragdoll left hip joint" + }); + + // + // right shin + // + + var rightShinID = Entities.addEntity({ + name: "ragdoll right shin", + type: "Box", + color: { blue: 20, green: 200, red: 20 }, + dimensions: { x: shinThickness, y: shinLength, z: shinThickness }, + position: Vec3.sum(pos, { x: 0, + y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength / 2, + z: bodyWidth / 2 - legThickness / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: -2, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("hinge", rightShinID, { + pivot: { x: 0, y: shinLength / 2 + kneeGap / 2, z: 0 }, + axis: { x: 0, y: 0, z: 1 }, + otherEntityID: rightLegID, + otherPivot: { x: 0, y: -legLength / 2 - kneeGap / 2, z: 0 }, + otherAxis: { x: 0, y: 0, z: 1 }, + low: 0, + high: Math.PI / 2, + tag: "ragdoll right knee joint" + }); + + + // + // left shin + // + + var leftShinID = Entities.addEntity({ + name: "ragdoll left shin", + type: "Box", + color: { blue: 20, green: 200, red: 20 }, + dimensions: { x: shinThickness, y: shinLength, z: shinThickness }, + position: Vec3.sum(pos, { x: 0, + y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength / 2, + z: -bodyWidth / 2 + legThickness / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: -2, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("hinge", leftShinID, { + pivot: { x: 0, y: shinLength / 2 + kneeGap / 2, z: 0 }, + axis: { x: 0, y: 0, z: 1 }, + otherEntityID: leftLegID, + otherPivot: { x: 0, y: -legLength / 2 - kneeGap / 2, z: 0 }, + otherAxis: { x: 0, y: 0, z: 1 }, + low: 0, + high: Math.PI / 2, + tag: "ragdoll left knee joint" + }); + + // + // right foot + // + + var rightFootID = Entities.addEntity({ + name: "ragdoll right foot", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: footLength, y: footThickness, z: footWidth }, + position: Vec3.sum(pos, { x: -shinThickness / 2 + footLength / 2, + y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength - ankleGap - footThickness / 2, + z: bodyWidth / 2 - legThickness / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: -5, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("hinge", rightFootID, { + pivot: { x: -footLength / 2 + shinThickness / 2, y: ankleGap / 2, z: 0 }, + axis: { x: 0, y: 0, z: 1 }, + otherEntityID: rightShinID, + otherPivot: { x: 0, y: -shinLength / 2 - ankleGap / 2, z: 0 }, + otherAxis: { x: 0, y: 0, z: 1 }, + low: ankleMin, + high: ankleMax, + tag: "ragdoll right ankle joint" + }); + + // + // left foot + // + + var leftFootID = Entities.addEntity({ + name: "ragdoll left foot", + type: "Box", + color: { blue: 128, green: 100, red: 20 }, + dimensions: { x: footLength, y: footThickness, z: footWidth }, + position: Vec3.sum(pos, { x: -shinThickness / 2 + footLength / 2, + y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength - ankleGap - footThickness / 2, + z: bodyWidth / 2 - legThickness / 2 + }), + dynamic: true, + collisionless: false, + gravity: { x: 0, y: -5, z: 0 }, + lifetime: lifetime, + userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }" + }); + + Entities.addAction("hinge", leftFootID, { + pivot: { x: -footLength / 2 + shinThickness / 2, y: ankleGap / 2, z: 0 }, + axis: { x: 0, y: 0, z: 1 }, + otherEntityID: leftShinID, + otherPivot: { x: 0, y: -shinLength / 2 - ankleGap / 2, z: 0 }, + otherAxis: { x: 0, y: 0, z: 1 }, + low: ankleMin, + high: ankleMax, + tag: "ragdoll left ankle joint" + }); + + } + + function onWebEventReceived(eventString) { + print("received web event: " + JSON.stringify(eventString)); + if (typeof eventString === "string") { + var event; + try { + event = JSON.parse(eventString); + } catch(e) { + return; + } + + if (event["dynamics-tests-command"]) { + var commandToFunctionMap = { + "cone-twist-and-spring-lever-test": coneTwistAndSpringLeverTest, + "door-vs-world-test": doorVSWorldTest, + "hinge-chain-test": hingeChainTest, + "slider-vs-world-test": sliderVSWorldTest, + "slider-chain-test": sliderChainTest, + "ball-socket-between-test": ballSocketBetweenTest, + "ball-socket-coincident-test": ballSocketCoincidentTest, + "ragdoll-test": ragdollTest + }; + + var cmd = event["dynamics-tests-command"]; + if (commandToFunctionMap.hasOwnProperty(cmd)) { + var func = commandToFunctionMap[cmd]; + func(event); + } + } + } + } + + + var onDynamicsTestsScreen = false; + var shouldActivateButton = false; + + function onClicked() { + if (onDynamicsTestsScreen) { + tablet.gotoHomeScreen(); + } else { + shouldActivateButton = true; + tablet.gotoWebScreen(DYNAMICS_TESTS_URL + + "?lifetime=" + DEFAULT_LIFETIME.toString() + ); + onDynamicsTestsScreen = true; + } + } + + function onScreenChanged() { + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: shouldActivateButton}); + shouldActivateButton = false; + onDynamicsTestsScreen = shouldActivateButton; + } + + function cleanup() { + button.clicked.disconnect(onClicked); + tablet.removeButton(button); + } + + button.clicked.connect(onClicked); + tablet.webEventReceived.connect(onWebEventReceived); + tablet.screenChanged.connect(onScreenChanged); + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE diff --git a/scripts/developer/tests/dynamics/dynamicsTests.svg b/scripts/developer/tests/dynamics/dynamicsTests.svg new file mode 100644 index 0000000000..a1407e87d4 --- /dev/null +++ b/scripts/developer/tests/dynamics/dynamicsTests.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + +