diff --git a/CMakeLists.txt b/CMakeLists.txt index a583d7d951..f20142d698 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,4 +39,5 @@ add_subdirectory(assignment-client) add_subdirectory(domain-server) add_subdirectory(interface) add_subdirectory(pairing-server) -add_subdirectory(voxel-edit) \ No newline at end of file +add_subdirectory(tests) +add_subdirectory(voxel-edit) diff --git a/examples/gameoflife.js b/examples/gameoflife.js index 6779941dc7..d72a72c2de 100644 --- a/examples/gameoflife.js +++ b/examples/gameoflife.js @@ -115,7 +115,6 @@ function sendNextCells() { var sentFirstBoard = false; function step() { -print("step()..."); if (sentFirstBoard) { // we've already sent the first full board, perform a step in time updateCells(); diff --git a/interface/resources/meshes/body.jpg b/interface/resources/meshes/body.jpg new file mode 100644 index 0000000000..376cf87d32 Binary files /dev/null and b/interface/resources/meshes/body.jpg differ diff --git a/interface/resources/meshes/defaultAvatar_body.fst b/interface/resources/meshes/defaultAvatar_body.fst new file mode 100644 index 0000000000..3e8fa3ef45 --- /dev/null +++ b/interface/resources/meshes/defaultAvatar_body.fst @@ -0,0 +1,18 @@ +scale=130 +joint = jointRoot = jointRoot +joint = jointLean = jointSpine +joint = jointNeck = jointNeck +joint = jointHead = jointHeadtop +joint = joint_L_shoulder = joint_L_shoulder +freeJoint = joint_L_arm +freeJoint = joint_L_elbow +joint = jointLeftHand = joint_L_hand +joint = joint_R_shoulder = joint_R_shoulder +freeJoint = joint_R_arm +freeJoint = joint_R_elbow +joint = jointRightHand = joint_R_hand + + + + + diff --git a/interface/resources/meshes/defaultAvatar_head.fst b/interface/resources/meshes/defaultAvatar_head.fst new file mode 100644 index 0000000000..1352652efc --- /dev/null +++ b/interface/resources/meshes/defaultAvatar_head.fst @@ -0,0 +1,45 @@ +# faceshift target mapping file +name= defaultAvatar_head +filename=../../../Avatars/Jelly/jellyrob_blue.fbx +texdir=../../../Avatars/Jelly +scale=80 +rx=0 +ry=0 +rz=0 +tx=0 +ty=0 +tz=0 +joint = jointNeck = jointNeck +bs = BrowsD_L = Leye1.BrowsD_L = 0.97 +bs = BrowsD_R = Reye1.BrowsD_R = 1 +bs = CheekSquint_L = Leye1.CheekSquint_L = 1 +bs = CheekSquint_R = Reye1.CheekSquint_R = 1 +bs = EyeBlink_L = Leye1.EyeBlink_L = 1 +bs = EyeBlink_R = Reye1.EyeBlink_R = 1 +bs = EyeDown_L = Leye1.EyeDown_L = 1 +bs = EyeDown_R = Reye1.EyeDown_R = 0.99 +bs = EyeIn_L = Leye1.EyeIn_L = 0.92 +bs = EyeIn_R = Reye1.EyeIn_R = 1 +bs = EyeOpen_L = Leye1.EyeOpen_L = 1 +bs = EyeOpen_R = Reye1.EyeOpen_R = 1 +bs = EyeOut_L = Leye1.EyeOut_L = 0.99 +bs = EyeOut_R = Reye1.EyeOut_R = 1 +bs = EyeUp_L = Leye1.EyeUp_L = 0.93 +bs = EyeUp_R = Reye1.EyeUp_R = 1 +bs = JawOpen = Mouth.JawOpen = 1 +bs = LipsFunnel = Mouth.LipsFunnel = 1 +bs = LipsLowerDown = Mouth.LipsLowerDown = 1 +bs = LipsPucker = Mouth.LipsPucker = 1 +bs = LipsStretch_L = Mouth.LipsStretch_L = 0.96 +bs = LipsStretch_R = Mouth.LipsStretch_R = 1 +bs = LipsUpperUp = Mouth.LipsUpperUp = 1 +bs = MouthDimple_L = Mouth.MouthDimple_L = 1 +bs = MouthDimple_R = Mouth.MouthDimple_R = 1 +bs = MouthFrown_L = Mouth.MouthFrown_L = 1 +bs = MouthFrown_R = Mouth.MouthFrown_R = 1 +bs = MouthLeft = Mouth.MouthLeft = 1 +bs = MouthRight = Mouth.MouthRight = 1 +bs = MouthSmile_L = Mouth.MouthSmile_L = 1 +bs = MouthSmile_R = Mouth.MouthSmile_R = 1 +bs = Puff = Mouth.Puff = 1 +bs = Sneer = Mouth.Sneer = 1 diff --git a/interface/resources/meshes/tail.jpg b/interface/resources/meshes/tail.jpg new file mode 100644 index 0000000000..1b5bb89750 Binary files /dev/null and b/interface/resources/meshes/tail.jpg differ diff --git a/interface/resources/meshes/visor.png b/interface/resources/meshes/visor.png new file mode 100644 index 0000000000..e4e6292b2c Binary files /dev/null and b/interface/resources/meshes/visor.png differ diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a063bfb1ac..9854a8b97f 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -162,14 +162,18 @@ Menu::Menu() : addDisabledActionAndSeparator(editMenu, "Physics"); addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::Gravity, Qt::SHIFT | Qt::Key_G, true); - addCheckableActionToQMenuAndActionHash(editMenu, - MenuOption::Collisions, - 0, - true, - appInstance->getAvatar(), - SLOT(setWantCollisionsOn(bool))); + addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ClickToFly); + + QMenu* collisionsOptionsMenu = editMenu->addMenu("Collision Options"); + + QObject* avatar = appInstance->getAvatar(); + addCheckableActionToQMenuAndActionHash(collisionsOptionsMenu, MenuOption::CollideWithEnvironment, 0, false, avatar, SLOT(updateCollisionFlags())); + addCheckableActionToQMenuAndActionHash(collisionsOptionsMenu, MenuOption::CollideWithAvatars, 0, false, avatar, SLOT(updateCollisionFlags())); + addCheckableActionToQMenuAndActionHash(collisionsOptionsMenu, MenuOption::CollideWithVoxels, 0, false, avatar, SLOT(updateCollisionFlags())); + // TODO: make this option work + //addCheckableActionToQMenuAndActionHash(collisionsOptionsMenu, MenuOption::CollideWithParticles, 0, false, avatar, SLOT(updateCollisionFlags())); QMenu* toolsMenu = addMenu("Tools"); @@ -774,11 +778,13 @@ void Menu::editPreferences() { QString faceURLString = applicationInstance->getAvatar()->getHead().getFaceModel().getURL().toString(); QLineEdit* faceURLEdit = new QLineEdit(faceURLString); faceURLEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH); + faceURLEdit->setPlaceholderText(DEFAULT_HEAD_MODEL_URL.toString()); form->addRow("Face URL:", faceURLEdit); QString skeletonURLString = applicationInstance->getAvatar()->getSkeletonModel().getURL().toString(); QLineEdit* skeletonURLEdit = new QLineEdit(skeletonURLString); skeletonURLEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH); + skeletonURLEdit->setPlaceholderText(DEFAULT_BODY_MODEL_URL.toString()); form->addRow("Skeleton URL:", skeletonURLEdit); QSlider* pupilDilation = new QSlider(Qt::Horizontal); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8720c914a4..9ccc5466e8 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -168,6 +168,10 @@ namespace MenuOption { const QString ChatCircling = "Chat Circling"; const QString CollisionProxies = "Collision Proxies"; const QString Collisions = "Collisions"; + const QString CollideWithAvatars = "Collide With Avatars"; + const QString CollideWithParticles = "Collide With Particles"; + const QString CollideWithVoxels = "Collide With Voxels"; + const QString CollideWithEnvironment = "Collide With World Boundaries"; const QString CopyVoxels = "Copy"; const QString CoverageMap = "Render Coverage Map"; const QString CoverageMapV2 = "Render Coverage Map V2"; diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index ef7e049d75..73ec791714 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include "InterfaceConfig.h" diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 131eb4d37f..761ed59db9 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -74,6 +74,7 @@ Avatar::Avatar() : _mouseRayDirection(0.0f, 0.0f, 0.0f), _moving(false), _owningAvatarMixer(), + _collisionFlags(0), _initialized(false) { // we may have been created in the network thread, but we live in the main thread @@ -159,7 +160,13 @@ void Avatar::render(bool forceRenderHead) { Glower glower(_moving && glm::length(toTarget) > GLOW_DISTANCE ? 1.0f : 0.0f); // render body - renderBody(forceRenderHead); + if (Menu::getInstance()->isOptionChecked(MenuOption::CollisionProxies)) { + _skeletonModel.renderCollisionProxies(1.f); + } + + if (Menu::getInstance()->isOptionChecked(MenuOption::Avatars)) { + renderBody(forceRenderHead); + } // render sphere when far away const float MAX_ANGLE = 10.f; @@ -264,34 +271,28 @@ bool Avatar::findRayIntersection(const glm::vec3& origin, const glm::vec3& direc return false; } -bool Avatar::findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, - glm::vec3& penetration, int skeletonSkipIndex) const { +bool Avatar::findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, + ModelCollisionList& collisions, int skeletonSkipIndex) { bool didPenetrate = false; - glm::vec3 totalPenetration; glm::vec3 skeletonPenetration; - if (_skeletonModel.findSpherePenetration(penetratorCenter, penetratorRadius, - skeletonPenetration, 1.0f, skeletonSkipIndex)) { - totalPenetration = addPenetrations(totalPenetration, skeletonPenetration); + ModelCollisionInfo collisionInfo; + if (_skeletonModel.findSphereCollision(penetratorCenter, penetratorRadius, collisionInfo, 1.0f, skeletonSkipIndex)) { + collisionInfo._model = &_skeletonModel; + collisions.push_back(collisionInfo); didPenetrate = true; } - glm::vec3 facePenetration; - if (_head.getFaceModel().findSpherePenetration(penetratorCenter, penetratorRadius, facePenetration)) { - totalPenetration = addPenetrations(totalPenetration, facePenetration); + if (_head.getFaceModel().findSphereCollision(penetratorCenter, penetratorRadius, collisionInfo)) { + collisionInfo._model = &(_head.getFaceModel()); + collisions.push_back(collisionInfo); didPenetrate = true; } - if (didPenetrate) { - penetration = totalPenetration; - return true; - } - return false; + return didPenetrate; } -bool Avatar::findSphereCollision(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision) { - // TODO: provide an early exit using bounding sphere of entire avatar - +bool Avatar::findSphereCollisionWithHands(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision) { const HandData* handData = getHandData(); if (handData) { - for (int i = 0; i < 2; i++) { + for (int i = 0; i < NUM_HANDS; i++) { const PalmData* palm = handData->getPalm(i); if (palm && palm->hasPaddle()) { // create a disk collision proxy where the hand is @@ -327,23 +328,31 @@ bool Avatar::findSphereCollision(const glm::vec3& sphereCenter, float sphereRadi } } } + return false; +} - if (_skeletonModel.findSpherePenetration(sphereCenter, sphereRadius, collision._penetration)) { +/* adebug TODO: make this work again +bool Avatar::findSphereCollisionWithSkeleton(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision) { + int jointIndex = _skeletonModel.findSphereCollision(sphereCenter, sphereRadius, collision._penetration); + if (jointIndex != -1) { collision._penetration /= (float)(TREE_SCALE); collision._addedVelocity = getVelocity(); return true; } return false; } +*/ void Avatar::setFaceModelURL(const QUrl &faceModelURL) { AvatarData::setFaceModelURL(faceModelURL); - _head.getFaceModel().setURL(faceModelURL); + const QUrl DEFAULT_FACE_MODEL_URL = QUrl::fromLocalFile("resources/meshes/defaultAvatar_head.fbx"); + _head.getFaceModel().setURL(_faceModelURL, DEFAULT_FACE_MODEL_URL); } void Avatar::setSkeletonModelURL(const QUrl &skeletonModelURL) { AvatarData::setSkeletonModelURL(skeletonModelURL); - _skeletonModel.setURL(skeletonModelURL); + const QUrl DEFAULT_SKELETON_MODEL_URL = QUrl::fromLocalFile("resources/meshes/defaultAvatar_body.fbx"); + _skeletonModel.setURL(_skeletonModelURL, DEFAULT_SKELETON_MODEL_URL); } int Avatar::parseData(const QByteArray& packet) { @@ -406,6 +415,22 @@ void Avatar::renderJointConnectingCone(glm::vec3 position1, glm::vec3 position2, glEnd(); } +void Avatar::updateCollisionFlags() { + _collisionFlags = 0; + if (Menu::getInstance()->isOptionChecked(MenuOption::CollideWithEnvironment)) { + _collisionFlags |= COLLISION_GROUP_ENVIRONMENT; + } + if (Menu::getInstance()->isOptionChecked(MenuOption::CollideWithAvatars)) { + _collisionFlags |= COLLISION_GROUP_AVATARS; + } + if (Menu::getInstance()->isOptionChecked(MenuOption::CollideWithVoxels)) { + _collisionFlags |= COLLISION_GROUP_VOXELS; + } + //if (Menu::getInstance()->isOptionChecked(MenuOption::CollideWithParticles)) { + // _collisionFlags |= COLLISION_GROUP_PARTICLES; + //} +} + void Avatar::setScale(float scale) { _scale = scale; @@ -420,6 +445,15 @@ float Avatar::getHeight() const { return extents.maximum.y - extents.minimum.y; } +bool Avatar::poke(ModelCollisionInfo& collision) { + // ATM poke() can only affect the Skeleton (not the head) + // TODO: make poke affect head + if (collision._model == &_skeletonModel && collision._jointIndex != -1) { + return _skeletonModel.poke(collision); + } + return false; +} + float Avatar::getPelvisFloatingHeight() const { return -_skeletonModel.getBindExtents().minimum.y; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index dfe72fdc18..8290115240 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -57,7 +57,7 @@ enum ScreenTintLayer { NUM_SCREEN_TINT_LAYERS }; -class MyAvatar; +typedef QVector ModelCollisionList; // Where one's own Avatar begins in the world (will be overwritten if avatar data file is found) // this is basically in the center of the ground plane. Slightly adjusted. This was asked for by @@ -97,18 +97,25 @@ public: /// Checks for penetration between the described sphere and the avatar. /// \param penetratorCenter the center of the penetration test sphere /// \param penetratorRadius the radius of the penetration test sphere - /// \param penetration[out] the vector in which to store the penetration + /// \param collisions[out] a list of collisions /// \param skeletonSkipIndex if not -1, the index of a joint to skip (along with its descendents) in the skeleton model /// \return whether or not the sphere penetrated - bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, - glm::vec3& penetration, int skeletonSkipIndex = -1) const; + bool findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, + ModelCollisionList& collisions, int skeletonSkipIndex = -1); - /// Checks for collision between the a sphere and the avatar. + /// Checks for collision between the a sphere and the avatar's (paddle) hands. /// \param collisionCenter the center of the penetration test sphere /// \param collisionRadius the radius of the penetration test sphere /// \param collision[out] the details of the collision point /// \return whether or not the sphere collided - virtual bool findSphereCollision(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision); + bool findSphereCollisionWithHands(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision); + + /// Checks for collision between the a sphere and the avatar's skeleton (including hand capsules). + /// \param collisionCenter the center of the penetration test sphere + /// \param collisionRadius the radius of the penetration test sphere + /// \param collision[out] the details of the collision point + /// \return whether or not the sphere collided + //bool findSphereCollisionWithSkeleton(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision); virtual bool isMyAvatar() { return false; } @@ -119,6 +126,15 @@ public: static void renderJointConnectingCone(glm::vec3 position1, glm::vec3 position2, float radius1, float radius2); + float getHeight() const; + + /// \param collision a data structure for storing info about collisions against Models + /// \return true if the collision affects the Avatar models + bool poke(ModelCollisionInfo& collision); + +public slots: + void updateCollisionFlags(); + protected: Head _head; Hand _hand; @@ -137,6 +153,8 @@ protected: bool _moving; ///< set when position is changing QWeakPointer _owningAvatarMixer; + uint32_t _collisionFlags; + // protected methods... glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getBodyUpDirection() const { return getOrientation() * IDENTITY_UP; } @@ -144,7 +162,6 @@ protected: glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void setScale(float scale); - float getHeight() const; float getPelvisFloatingHeight() const; float getPelvisToHeadLength() const; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1734b755be..51cea9d085 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -67,9 +67,6 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { } void AvatarManager::renderAvatars(bool forceRenderHead, bool selfAvatarOnly) { - if (!Menu::getInstance()->isOptionChecked(MenuOption::Avatars)) { - return; - } PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::renderAvatars()"); bool renderLookAtVectors = Menu::getInstance()->isOptionChecked(MenuOption::LookAtVectors); diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp index b441000cc1..c2ea3f8f31 100644 --- a/interface/src/avatar/Hand.cpp +++ b/interface/src/avatar/Hand.cpp @@ -90,9 +90,7 @@ void Hand::simulate(float deltaTime, bool isMine) { calculateGeometry(); if (isMine) { - // Iterate hand controllers, take actions as needed - for (size_t i = 0; i < getNumPalms(); ++i) { PalmData& palm = getPalms()[i]; if (palm.isActive()) { @@ -173,6 +171,7 @@ void Hand::updateCollisions() { int leftPalmIndex, rightPalmIndex; getLeftRightPalmIndices(leftPalmIndex, rightPalmIndex); + ModelCollisionList collisions; // check for collisions for (size_t i = 0; i < getNumPalms(); i++) { PalmData& palm = getPalms()[i]; @@ -182,69 +181,76 @@ void Hand::updateCollisions() { float scaledPalmRadius = PALM_COLLISION_RADIUS * _owningAvatar->getScale(); glm::vec3 totalPenetration; - // check other avatars - foreach (const AvatarSharedPointer& avatarPointer, Application::getInstance()->getAvatarManager().getAvatarHash()) { - Avatar* avatar = static_cast(avatarPointer.data()); - if (avatar == _owningAvatar) { - // don't collid with our own hands - continue; - } - if (Menu::getInstance()->isOptionChecked(MenuOption::PlaySlaps)) { - // Check for palm collisions - glm::vec3 myPalmPosition = palm.getPosition(); - float palmCollisionDistance = 0.1f; - bool wasColliding = palm.getIsCollidingWithPalm(); - palm.setIsCollidingWithPalm(false); - // If 'Play Slaps' is enabled, look for palm-to-palm collisions and make sound - for (size_t j = 0; j < avatar->getHand().getNumPalms(); j++) { - PalmData& otherPalm = avatar->getHand().getPalms()[j]; - if (!otherPalm.isActive()) { - continue; - } - glm::vec3 otherPalmPosition = otherPalm.getPosition(); - if (glm::length(otherPalmPosition - myPalmPosition) < palmCollisionDistance) { - palm.setIsCollidingWithPalm(true); - if (!wasColliding) { - const float PALM_COLLIDE_VOLUME = 1.f; - const float PALM_COLLIDE_FREQUENCY = 1000.f; - const float PALM_COLLIDE_DURATION_MAX = 0.75f; - const float PALM_COLLIDE_DECAY_PER_SAMPLE = 0.01f; - Application::getInstance()->getAudio()->startDrumSound(PALM_COLLIDE_VOLUME, - PALM_COLLIDE_FREQUENCY, - PALM_COLLIDE_DURATION_MAX, - PALM_COLLIDE_DECAY_PER_SAMPLE); - // If the other person's palm is in motion, move mine downward to show I was hit - const float MIN_VELOCITY_FOR_SLAP = 0.05f; - if (glm::length(otherPalm.getVelocity()) > MIN_VELOCITY_FOR_SLAP) { - // add slapback here + if (Menu::getInstance()->isOptionChecked(MenuOption::CollideWithAvatars)) { + // check other avatars + foreach (const AvatarSharedPointer& avatarPointer, Application::getInstance()->getAvatarManager().getAvatarHash()) { + Avatar* avatar = static_cast(avatarPointer.data()); + if (avatar == _owningAvatar) { + // don't collid with our own hands + continue; + } + if (Menu::getInstance()->isOptionChecked(MenuOption::PlaySlaps)) { + // Check for palm collisions + glm::vec3 myPalmPosition = palm.getPosition(); + float palmCollisionDistance = 0.1f; + bool wasColliding = palm.getIsCollidingWithPalm(); + palm.setIsCollidingWithPalm(false); + // If 'Play Slaps' is enabled, look for palm-to-palm collisions and make sound + for (size_t j = 0; j < avatar->getHand().getNumPalms(); j++) { + PalmData& otherPalm = avatar->getHand().getPalms()[j]; + if (!otherPalm.isActive()) { + continue; + } + glm::vec3 otherPalmPosition = otherPalm.getPosition(); + if (glm::length(otherPalmPosition - myPalmPosition) < palmCollisionDistance) { + palm.setIsCollidingWithPalm(true); + if (!wasColliding) { + const float PALM_COLLIDE_VOLUME = 1.f; + const float PALM_COLLIDE_FREQUENCY = 1000.f; + const float PALM_COLLIDE_DURATION_MAX = 0.75f; + const float PALM_COLLIDE_DECAY_PER_SAMPLE = 0.01f; + Application::getInstance()->getAudio()->startDrumSound(PALM_COLLIDE_VOLUME, + PALM_COLLIDE_FREQUENCY, + PALM_COLLIDE_DURATION_MAX, + PALM_COLLIDE_DECAY_PER_SAMPLE); + // If the other person's palm is in motion, move mine downward to show I was hit + const float MIN_VELOCITY_FOR_SLAP = 0.05f; + if (glm::length(otherPalm.getVelocity()) > MIN_VELOCITY_FOR_SLAP) { + // add slapback here + } } } - - } } - } - glm::vec3 avatarPenetration; - if (avatar->findSpherePenetration(palm.getPosition(), scaledPalmRadius, avatarPenetration)) { - totalPenetration = addPenetrations(totalPenetration, avatarPenetration); - // Check for collisions with the other avatar's leap palms + if (avatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, collisions)) { + for (size_t j = 0; j < collisions.size(); ++j) { + if (!avatar->poke(collisions[j])) { + totalPenetration = addPenetrations(totalPenetration, collisions[j]._penetration); + } + } + } } } if (Menu::getInstance()->isOptionChecked(MenuOption::HandsCollideWithSelf)) { // and the current avatar (ignoring everything below the parent of the parent of the last free joint) - glm::vec3 owningPenetration; + collisions.clear(); const Model& skeletonModel = _owningAvatar->getSkeletonModel(); int skipIndex = skeletonModel.getParentJointIndex(skeletonModel.getParentJointIndex( skeletonModel.getLastFreeJointIndex((i == leftPalmIndex) ? skeletonModel.getLeftHandJointIndex() : (i == rightPalmIndex) ? skeletonModel.getRightHandJointIndex() : -1))); - if (_owningAvatar->findSpherePenetration(palm.getPosition(), scaledPalmRadius, owningPenetration, skipIndex)) { - totalPenetration = addPenetrations(totalPenetration, owningPenetration); + if (_owningAvatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, collisions, skipIndex)) { + for (size_t j = 0; j < collisions.size(); ++j) { + totalPenetration = addPenetrations(totalPenetration, collisions[j]._penetration); + } } } // un-penetrate palm.addToPosition(-totalPenetration); + + // we recycle the collisions container, so we clear it for the next loop + collisions.clear(); } } @@ -278,14 +284,14 @@ void Hand::calculateGeometry() { FingerData& finger = palm.getFingers()[f]; if (finger.isActive()) { const float standardBallRadius = FINGERTIP_COLLISION_RADIUS; - _leapFingerTipBalls.resize(_leapFingerTipBalls.size() + 1); - HandBall& ball = _leapFingerTipBalls.back(); + HandBall ball; ball.rotation = getBaseOrientation(); ball.position = finger.getTipPosition(); ball.radius = standardBallRadius; ball.touchForce = 0.0; ball.isCollidable = true; ball.isColliding = false; + _leapFingerTipBalls.push_back(ball); } } } @@ -300,14 +306,14 @@ void Hand::calculateGeometry() { FingerData& finger = palm.getFingers()[f]; if (finger.isActive()) { const float standardBallRadius = 0.005f; - _leapFingerRootBalls.resize(_leapFingerRootBalls.size() + 1); - HandBall& ball = _leapFingerRootBalls.back(); + HandBall ball; ball.rotation = getBaseOrientation(); ball.position = finger.getRootPosition(); ball.radius = standardBallRadius; ball.touchForce = 0.0; ball.isCollidable = true; ball.isColliding = false; + _leapFingerRootBalls.push_back(ball); } } } @@ -473,8 +479,3 @@ void Hand::setLeapHands(const std::vector& handPositions, } } - - - - - diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 98eb9a4431..5628740770 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -209,21 +209,26 @@ void MyAvatar::simulate(float deltaTime) { _velocity += _scale * _gravity * (GRAVITY_EARTH * deltaTime); } - // Only collide if we are not moving to a target - if (_isCollisionsOn && (glm::length(_moveTarget) < EPSILON)) { - + if (_collisionFlags != 0) { Camera* myCamera = Application::getInstance()->getCamera(); + float radius = getHeight() * COLLISION_RADIUS_SCALE; if (myCamera->getMode() == CAMERA_MODE_FIRST_PERSON && !OculusManager::isConnected()) { - _collisionRadius = myCamera->getAspectRatio() * (myCamera->getNearClip() / cos(myCamera->getFieldOfView() / 2.f)); - _collisionRadius *= COLLISION_RADIUS_SCALAR; - } else { - _collisionRadius = getHeight() * COLLISION_RADIUS_SCALE; + radius = myCamera->getAspectRatio() * (myCamera->getNearClip() / cos(myCamera->getFieldOfView() / 2.f)); + radius *= COLLISION_RADIUS_SCALAR; } - updateCollisionWithEnvironment(deltaTime); - updateCollisionWithVoxels(deltaTime); - updateAvatarCollisions(deltaTime); + if (_collisionFlags & COLLISION_GROUP_ENVIRONMENT) { + updateCollisionWithEnvironment(deltaTime, radius); + } + if (_collisionFlags & COLLISION_GROUP_VOXELS) { + updateCollisionWithVoxels(deltaTime, radius); + } + if (_collisionFlags & COLLISION_GROUP_AVATARS) { + // Note, hand-vs-avatar collisions are done elsewhere + // This is where we avatar-vs-avatar bounding capsule + updateCollisionWithAvatars(deltaTime); + } } // add thrust to velocity @@ -476,7 +481,12 @@ void MyAvatar::renderDebugBodyPoints() { void MyAvatar::render(bool forceRenderHead) { // render body - renderBody(forceRenderHead); + if (Menu::getInstance()->isOptionChecked(MenuOption::CollisionProxies)) { + _skeletonModel.renderCollisionProxies(1.f); + } + if (Menu::getInstance()->isOptionChecked(MenuOption::Avatars)) { + renderBody(forceRenderHead); + } //renderDebugBodyPoints(); @@ -878,10 +888,9 @@ void MyAvatar::updateHandMovementAndTouching(float deltaTime) { } } -void MyAvatar::updateCollisionWithEnvironment(float deltaTime) { +void MyAvatar::updateCollisionWithEnvironment(float deltaTime, float radius) { glm::vec3 up = getBodyUpDirection(); - float radius = _collisionRadius; - const float ENVIRONMENT_SURFACE_ELASTICITY = 1.0f; + const float ENVIRONMENT_SURFACE_ELASTICITY = 0.0f; const float ENVIRONMENT_SURFACE_DAMPING = 0.01f; const float ENVIRONMENT_COLLISION_FREQUENCY = 0.05f; glm::vec3 penetration; @@ -896,8 +905,7 @@ void MyAvatar::updateCollisionWithEnvironment(float deltaTime) { } -void MyAvatar::updateCollisionWithVoxels(float deltaTime) { - float radius = _collisionRadius; +void MyAvatar::updateCollisionWithVoxels(float deltaTime, float radius) { const float VOXEL_ELASTICITY = 0.4f; const float VOXEL_DAMPING = 0.0f; const float VOXEL_COLLISION_FREQUENCY = 0.5f; @@ -917,8 +925,8 @@ void MyAvatar::applyHardCollision(const glm::vec3& penetration, float elasticity // Update the avatar in response to a hard collision. Position will be reset exactly // to outside the colliding surface. Velocity will be modified according to elasticity. // - // if elasticity = 1.0, collision is inelastic. - // if elasticity > 1.0, collision is elastic. + // if elasticity = 0.0, collision is 100% inelastic. + // if elasticity = 1.0, collision is elastic. // _position -= penetration; static float HALTING_VELOCITY = 0.2f; @@ -927,7 +935,7 @@ void MyAvatar::applyHardCollision(const glm::vec3& penetration, float elasticity if (penetrationLength > EPSILON) { _elapsedTimeSinceCollision = 0.0f; glm::vec3 direction = penetration / penetrationLength; - _velocity -= glm::dot(_velocity, direction) * direction * elasticity; + _velocity -= glm::dot(_velocity, direction) * direction * (1.f + elasticity); _velocity *= glm::clamp(1.f - damping, 0.0f, 1.0f); if ((glm::length(_velocity) < HALTING_VELOCITY) && (glm::length(_thrust) == 0.f)) { // If moving really slowly after a collision, and not applying forces, stop altogether @@ -966,11 +974,34 @@ void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTim } } -void MyAvatar::updateAvatarCollisions(float deltaTime) { +const float DEFAULT_HAND_RADIUS = 0.1f; +void MyAvatar::updateCollisionWithAvatars(float deltaTime) { // Reset detector for nearest avatar _distanceToNearestAvatar = std::numeric_limits::max(); - // loop through all the other avatars for potential interactions + const AvatarHash& avatars = Application::getInstance()->getAvatarManager().getAvatarHash(); + if (avatars.size() <= 1) { + // no need to compute a bunch of stuff if we have one or fewer avatars + return; + } + float myRadius = getHeight(); + + CollisionInfo collisionInfo; + foreach (const AvatarSharedPointer& avatarPointer, avatars) { + Avatar* avatar = static_cast(avatarPointer.data()); + if (static_cast(this) == avatar) { + // don't collide with ourselves + continue; + } + float distance = glm::length(_position - avatar->getPosition()); + if (_distanceToNearestAvatar > distance) { + _distanceToNearestAvatar = distance; + } + float theirRadius = avatar->getHeight(); + if (distance < myRadius + theirRadius) { + // TODO: Andrew to make avatar-avatar capsule collisions work here + } + } } class SortedAvatar { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7dfb8812dd..b912f6b0a7 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -113,7 +113,6 @@ private: bool _isCollisionsOn; bool _isThrustOn; float _thrustMultiplier; - float _collisionRadius; glm::vec3 _moveTarget; int _moveTargetStepCounter; QWeakPointer _lookAtTargetAvatar; @@ -126,9 +125,9 @@ private: void renderBody(bool forceRenderHead); void updateThrust(float deltaTime); void updateHandMovementAndTouching(float deltaTime); - void updateAvatarCollisions(float deltaTime); - void updateCollisionWithEnvironment(float deltaTime); - void updateCollisionWithVoxels(float deltaTime); + void updateCollisionWithAvatars(float deltaTime); + void updateCollisionWithEnvironment(float deltaTime, float radius); + void updateCollisionWithVoxels(float deltaTime, float radius); void applyHardCollision(const glm::vec3& penetration, float elasticity, float damping); void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void updateChatCircle(float deltaTime); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 60ba470c5b..ac08c52b49 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -70,10 +70,6 @@ bool SkeletonModel::render(float alpha) { Model::render(alpha); - if (Menu::getInstance()->isOptionChecked(MenuOption::CollisionProxies)) { - renderCollisionProxies(alpha); - } - return true; } diff --git a/interface/src/devices/SixenseManager.cpp b/interface/src/devices/SixenseManager.cpp index af1eafc85b..79feb5eb3f 100644 --- a/interface/src/devices/SixenseManager.cpp +++ b/interface/src/devices/SixenseManager.cpp @@ -98,15 +98,6 @@ void SixenseManager::update(float deltaTime) { // Compute current velocity from position change glm::vec3 rawVelocity = (position - palm->getRawPosition()) / deltaTime / 1000.f; palm->setRawVelocity(rawVelocity); // meters/sec - /* - if (i == 0) - { - printf("ADEBUG rawVelocity = [%e, %e, %e]\n", - rawVelocity.x, - rawVelocity.y, - rawVelocity.z); - } - */ palm->setRawPosition(position); // use the velocity to determine whether there's any movement (if the hand isn't new) diff --git a/interface/src/renderer/FBXReader.cpp b/interface/src/renderer/FBXReader.cpp index e4dfd7dd84..b4e7c4abc5 100644 --- a/interface/src/renderer/FBXReader.cpp +++ b/interface/src/renderer/FBXReader.cpp @@ -834,6 +834,12 @@ QString getTopModelID(const QMultiHash& parentMap, } } +QString getString(const QVariant& value) { + // if it's a list, return the first entry + QVariantList list = value.toList(); + return list.isEmpty() ? value.toString() : list.at(0).toString(); +} + FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) { QHash meshes; QVector blendshapes; @@ -847,14 +853,14 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QHash bumpTextures; QVariantHash joints = mapping.value("joint").toHash(); - QString jointEyeLeftName = processID(joints.value("jointEyeLeft", "jointEyeLeft").toString()); - QString jointEyeRightName = processID(joints.value("jointEyeRight", "jointEyeRight").toString()); - QString jointNeckName = processID(joints.value("jointNeck", "jointNeck").toString()); - QString jointRootName = processID(joints.value("jointRoot", "jointRoot").toString()); - QString jointLeanName = processID(joints.value("jointLean", "jointLean").toString()); - QString jointHeadName = processID(joints.value("jointHead", "jointHead").toString()); - QString jointLeftHandName = processID(joints.value("jointLeftHand", "jointLeftHand").toString()); - QString jointRightHandName = processID(joints.value("jointRightHand", "jointRightHand").toString()); + QString jointEyeLeftName = processID(getString(joints.value("jointEyeLeft", "jointEyeLeft"))); + QString jointEyeRightName = processID(getString(joints.value("jointEyeRight", "jointEyeRight"))); + QString jointNeckName = processID(getString(joints.value("jointNeck", "jointNeck"))); + QString jointRootName = processID(getString(joints.value("jointRoot", "jointRoot"))); + QString jointLeanName = processID(getString(joints.value("jointLean", "jointLean"))); + QString jointHeadName = processID(getString(joints.value("jointHead", "jointHead"))); + QString jointLeftHandName = processID(getString(joints.value("jointLeftHand", "jointLeftHand"))); + QString jointRightHandName = processID(getString(joints.value("jointRightHand", "jointRightHand"))); QVariantList jointLeftFingerNames = joints.values("jointLeftFinger"); QVariantList jointRightFingerNames = joints.values("jointRightFinger"); QVariantList jointLeftFingertipNames = joints.values("jointLeftFingertip"); @@ -1265,7 +1271,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) const glm::mat4& transform = geometry.joints.at(geometry.neckJointIndex).transform; geometry.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]); } - + geometry.bindExtents.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX); geometry.bindExtents.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp index 63a0c51f0b..3526fa5050 100644 --- a/interface/src/renderer/GeometryCache.cpp +++ b/interface/src/renderer/GeometryCache.cpp @@ -290,19 +290,24 @@ void GeometryCache::renderGrid(int xDivisions, int yDivisions) { buffer.release(); } -QSharedPointer GeometryCache::getGeometry(const QUrl& url) { +QSharedPointer GeometryCache::getGeometry(const QUrl& url, const QUrl& fallback) { + if (!url.isValid() && fallback.isValid()) { + return getGeometry(fallback); + } QSharedPointer geometry = _networkGeometry.value(url); if (geometry.isNull()) { - geometry = QSharedPointer(new NetworkGeometry(url)); + geometry = QSharedPointer(new NetworkGeometry(url, fallback.isValid() ? + getGeometry(fallback) : QSharedPointer())); _networkGeometry.insert(url, geometry); } return geometry; } -NetworkGeometry::NetworkGeometry(const QUrl& url) : +NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer& fallback) : _modelRequest(url), _modelReply(NULL), _mappingReply(NULL), + _fallback(fallback), _attempts(0) { if (!url.isValid()) { @@ -369,18 +374,37 @@ void NetworkGeometry::makeModelRequest() { void NetworkGeometry::handleModelReplyError() { QDebug debug = qDebug() << _modelReply->errorString(); + QNetworkReply::NetworkError error = _modelReply->error(); _modelReply->disconnect(this); _modelReply->deleteLater(); _modelReply = NULL; - // retry with increasing delays - const int MAX_ATTEMPTS = 8; - const int BASE_DELAY_MS = 1000; - if (++_attempts < MAX_ATTEMPTS) { - QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeModelRequest())); - debug << " -- retrying..."; - + // retry for certain types of failures + switch (error) { + case QNetworkReply::RemoteHostClosedError: + case QNetworkReply::TimeoutError: + case QNetworkReply::TemporaryNetworkFailureError: + case QNetworkReply::ProxyConnectionClosedError: + case QNetworkReply::ProxyTimeoutError: + case QNetworkReply::UnknownNetworkError: + case QNetworkReply::UnknownProxyError: + case QNetworkReply::UnknownContentError: + case QNetworkReply::ProtocolFailure: { + // retry with increasing delays + const int MAX_ATTEMPTS = 8; + const int BASE_DELAY_MS = 1000; + if (++_attempts < MAX_ATTEMPTS) { + QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeModelRequest())); + debug << " -- retrying..."; + return; + } + // fall through to final failure + } + default: + maybeLoadFallback(); + break; } + } void NetworkGeometry::handleMappingReplyError() { @@ -415,6 +439,7 @@ void NetworkGeometry::maybeReadModelWithMapping() { } catch (const QString& error) { qDebug() << "Error reading " << url << ": " << error; + maybeLoadFallback(); return; } @@ -507,6 +532,24 @@ void NetworkGeometry::maybeReadModelWithMapping() { _meshes.append(networkMesh); } + + emit loaded(); +} + +void NetworkGeometry::loadFallback() { + _geometry = _fallback->_geometry; + _meshes = _fallback->_meshes; + emit loaded(); +} + +void NetworkGeometry::maybeLoadFallback() { + if (_fallback) { + if (_fallback->isLoaded()) { + loadFallback(); + } else { + connect(_fallback.data(), SIGNAL(loaded()), SLOT(loadFallback())); + } + } } bool NetworkMeshPart::isTranslucent() const { diff --git a/interface/src/renderer/GeometryCache.h b/interface/src/renderer/GeometryCache.h index e65aed31d4..618796e907 100644 --- a/interface/src/renderer/GeometryCache.h +++ b/interface/src/renderer/GeometryCache.h @@ -37,7 +37,8 @@ public: void renderGrid(int xDivisions, int yDivisions); /// Loads geometry from the specified URL. - QSharedPointer getGeometry(const QUrl& url); + /// \param fallback a fallback URL to load if the desired one is unavailable + QSharedPointer getGeometry(const QUrl& url, const QUrl& fallback = QUrl()); private: @@ -58,7 +59,7 @@ class NetworkGeometry : public QObject { public: - NetworkGeometry(const QUrl& url); + NetworkGeometry(const QUrl& url, const QSharedPointer& fallback); ~NetworkGeometry(); bool isLoaded() const { return !_geometry.joints.isEmpty(); } @@ -69,18 +70,26 @@ public: /// Returns the average color of all meshes in the geometry. glm::vec4 computeAverageColor() const; +signals: + + void loaded(); + private slots: void makeModelRequest(); void handleModelReplyError(); void handleMappingReplyError(); void maybeReadModelWithMapping(); + void loadFallback(); private: + void maybeLoadFallback(); + QNetworkRequest _modelRequest; QNetworkReply* _modelReply; QNetworkReply* _mappingReply; + QSharedPointer _fallback; int _attempts; FBXGeometry _geometry; diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 6d61b2df68..b14ed1036d 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -390,7 +390,7 @@ float Model::getRightArmLength() const { return getLimbLength(getRightHandJointIndex()); } -void Model::setURL(const QUrl& url) { +void Model::setURL(const QUrl& url, const QUrl& fallback) { // don't recreate the geometry if it's the same URL if (_url == url) { return; @@ -401,7 +401,7 @@ void Model::setURL(const QUrl& url) { deleteGeometry(); _dilatedTextures.clear(); - _geometry = Application::getInstance()->getGeometryCache()->getGeometry(url); + _geometry = Application::getInstance()->getGeometryCache()->getGeometry(url, fallback); } glm::vec4 Model::computeAverageColor() const { @@ -437,11 +437,11 @@ bool Model::findRayIntersection(const glm::vec3& origin, const glm::vec3& direct return false; } -bool Model::findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, - glm::vec3& penetration, float boneScale, int skipIndex) const { +bool Model::findSphereCollision(const glm::vec3& penetratorCenter, float penetratorRadius, + ModelCollisionInfo& collisionInfo, float boneScale, int skipIndex) const { + int jointIndex = -1; const glm::vec3 relativeCenter = penetratorCenter - _translation; const FBXGeometry& geometry = _geometry->getFBXGeometry(); - bool didPenetrate = false; glm::vec3 totalPenetration; float radiusScale = extractUniformScale(_scale) * boneScale; for (int i = 0; i < _jointStates.size(); i++) { @@ -468,12 +468,16 @@ bool Model::findSpherePenetration(const glm::vec3& penetratorCenter, float penet if (findSphereCapsuleConePenetration(relativeCenter, penetratorRadius, start, end, startRadius, endRadius, bonePenetration)) { totalPenetration = addPenetrations(totalPenetration, bonePenetration); - didPenetrate = true; + // TODO: Andrew to try to keep the joint furthest toward the root + jointIndex = i; } outerContinue: ; } - if (didPenetrate) { - penetration = totalPenetration; + if (jointIndex != -1) { + // don't store collisionInfo._model at this stage, let the outer context do that + collisionInfo._penetration = totalPenetration; + collisionInfo._jointIndex = jointIndex; + collisionInfo._contactPoint = penetratorCenter + penetratorRadius * glm::normalize(totalPenetration); return true; } return false; @@ -548,6 +552,9 @@ bool Model::setJointPosition(int jointIndex, const glm::vec3& position, int last glm::vec3 relativePosition = position - _translation; const FBXGeometry& geometry = _geometry->getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; + if (freeLineage.isEmpty()) { + return false; + } if (lastFreeIndex == -1) { lastFreeIndex = freeLineage.last(); } @@ -706,6 +713,37 @@ void Model::renderCollisionProxies(float alpha) { glPopMatrix(); } +bool Model::poke(ModelCollisionInfo& collision) { + // This needs work. At the moment it can wiggle joints that are free to move (such as arms) + // but unmovable joints (such as torso) cannot be influenced at all. + glm::vec3 jointPosition(0.f); + if (getJointPosition(collision._jointIndex, jointPosition)) { + int jointIndex = collision._jointIndex; + const FBXJoint& joint = _geometry->getFBXGeometry().joints[jointIndex]; + if (joint.parentIndex != -1) { + // compute the approximate distance (travel) that the joint needs to move + glm::vec3 start; + getJointPosition(joint.parentIndex, start); + glm::vec3 contactPoint = collision._contactPoint - start; + glm::vec3 penetrationEnd = contactPoint + collision._penetration; + glm::vec3 axis = glm::cross(contactPoint, penetrationEnd); + float travel = glm::length(axis); + const float MIN_TRAVEL = 1.0e-8f; + if (travel > MIN_TRAVEL) { + // compute the new position of the joint + float angle = asinf(travel / (glm::length(contactPoint) * glm::length(penetrationEnd))); + axis = glm::normalize(axis); + glm::vec3 end; + getJointPosition(jointIndex, end); + glm::vec3 newEnd = start + glm::angleAxis(glm::degrees(angle), axis) * (end - start); + // try to move it + return setJointPosition(jointIndex, newEnd, -1, true); + } + } + } + return false; +} + void Model::deleteGeometry() { foreach (Model* attachment, _attachments) { delete attachment; diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 389020d1b1..423b2a4c81 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -17,6 +17,16 @@ #include "ProgramObject.h" #include "TextureCache.h" +class Model; + +// TODO: Andrew to move this into its own file +class ModelCollisionInfo : public CollisionInfo { +public: + ModelCollisionInfo() : CollisionInfo(), _model(NULL), _jointIndex(-1) {} + Model* _model; + int _jointIndex; +}; + /// A generic 3D model displaying geometry loaded from a URL. class Model : public QObject { Q_OBJECT @@ -51,7 +61,7 @@ public: void simulate(float deltaTime); bool render(float alpha); - Q_INVOKABLE void setURL(const QUrl& url); + Q_INVOKABLE void setURL(const QUrl& url, const QUrl& fallback = QUrl()); const QUrl& getURL() const { return _url; } /// Returns the extents of the model in its bind pose. @@ -149,8 +159,14 @@ public: bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const; - bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, - glm::vec3& penetration, float boneScale = 1.0f, int skipIndex = -1) const; + bool findSphereCollision(const glm::vec3& penetratorCenter, float penetratorRadius, + ModelCollisionInfo& collision, float boneScale = 1.0f, int skipIndex = -1) const; + + void renderCollisionProxies(float alpha); + + /// \param collisionInfo info about the collision + /// \return true if collision affects the Model + bool poke(ModelCollisionInfo& collisionInfo); protected: @@ -209,8 +225,6 @@ protected: void applyRotationDelta(int jointIndex, const glm::quat& delta, bool constrain = true); - void renderCollisionProxies(float alpha); - private: void deleteGeometry(); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index b2cd1cf076..c375f8b82d 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -63,7 +63,7 @@ QByteArray AvatarData::toByteArray() { if (!_headData) { _headData = new HeadData(this); } - // lazily allocate memory for HeadData in case we're not an Avatar instance + // lazily allocate memory for HandData in case we're not an Avatar instance if (!_handData) { _handData = new HandData(this); } @@ -301,13 +301,15 @@ QByteArray AvatarData::identityByteArray() { } void AvatarData::setFaceModelURL(const QUrl& faceModelURL) { - qDebug() << "Changing face model for avatar to" << faceModelURL.toString(); - _faceModelURL = faceModelURL; + _faceModelURL = faceModelURL.isEmpty() ? DEFAULT_HEAD_MODEL_URL : faceModelURL; + + qDebug() << "Changing face model for avatar to" << _faceModelURL.toString(); } void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { - qDebug() << "Changing skeleton model for avatar to" << skeletonModelURL.toString(); - _skeletonModelURL = skeletonModelURL; + _skeletonModelURL = skeletonModelURL.isEmpty() ? DEFAULT_BODY_MODEL_URL : skeletonModelURL; + + qDebug() << "Changing skeleton model for avatar to" << _skeletonModelURL.toString(); } void AvatarData::setClampedTargetScale(float targetScale) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 5c3c9ad9af..46d92c0f2e 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -52,6 +52,9 @@ static const float MIN_AVATAR_SCALE = .005f; const float MAX_AUDIO_LOUDNESS = 1000.0; // close enough for mouth animation +const QUrl DEFAULT_HEAD_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_head.fbx"); +const QUrl DEFAULT_BODY_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_body.fbx"); + enum KeyState { NO_KEY_DOWN = 0, INSERT_KEY_DOWN, @@ -131,16 +134,11 @@ public: virtual const glm::vec3& getVelocity() const { return vec3Zero; } - /// Checks for penetration between the described sphere and the avatar. - /// \param penetratorCenter the center of the penetration test sphere - /// \param penetratorRadius the radius of the penetration test sphere - /// \param penetration[out] the vector in which to store the penetration - /// \param skeletonSkipIndex if not -1, the index of a joint to skip (along with its descendents) in the skeleton model - /// \return whether or not the sphere penetrated - virtual bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, - glm::vec3& penetration, int skeletonSkipIndex = -1) const { return false; } + virtual bool findSphereCollisionWithHands(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision) { + return false; + } - virtual bool findSphereCollision(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision) { + virtual bool findSphereCollisionWithSkeleton(const glm::vec3& sphereCenter, float sphereRadius, CollisionInfo& collision) { return false; } diff --git a/libraries/metavoxels/src/AttributeRegistry.cpp b/libraries/metavoxels/src/AttributeRegistry.cpp index 431eaf62c9..9595d96e1f 100644 --- a/libraries/metavoxels/src/AttributeRegistry.cpp +++ b/libraries/metavoxels/src/AttributeRegistry.cpp @@ -82,6 +82,14 @@ bool AttributeValue::operator==(void* other) const { return _attribute && _attribute->equal(_value, other); } +bool AttributeValue::operator!=(const AttributeValue& other) const { + return _attribute != other._attribute || (_attribute && !_attribute->equal(_value, other._value)); +} + +bool AttributeValue::operator!=(void* other) const { + return !_attribute || !_attribute->equal(_value, other); +} + OwnedAttributeValue::OwnedAttributeValue(const AttributePointer& attribute, void* value) : AttributeValue(attribute, value) { } diff --git a/libraries/metavoxels/src/AttributeRegistry.h b/libraries/metavoxels/src/AttributeRegistry.h index db5e54cc4a..56cff7eeb4 100644 --- a/libraries/metavoxels/src/AttributeRegistry.h +++ b/libraries/metavoxels/src/AttributeRegistry.h @@ -105,6 +105,9 @@ public: bool operator==(const AttributeValue& other) const; bool operator==(void* other) const; + bool operator!=(const AttributeValue& other) const; + bool operator!=(void* other) const; + protected: AttributePointer _attribute; diff --git a/libraries/metavoxels/src/Bitstream.h b/libraries/metavoxels/src/Bitstream.h index 5a79b10766..cc776a742a 100644 --- a/libraries/metavoxels/src/Bitstream.h +++ b/libraries/metavoxels/src/Bitstream.h @@ -415,12 +415,16 @@ public: #define DECLARE_STREAMABLE_METATYPE(X) Q_DECLARE_METATYPE(X) \ Bitstream& operator<<(Bitstream& out, const X& obj); \ Bitstream& operator>>(Bitstream& in, X& obj); \ + bool operator==(const X& first, const X& second); \ + bool operator!=(const X& first, const X& second); \ static const int* _TypePtr##X = &X::Type; #else #define STRINGIFY(x) #x #define DECLARE_STREAMABLE_METATYPE(X) Q_DECLARE_METATYPE(X) \ Bitstream& operator<<(Bitstream& out, const X& obj); \ Bitstream& operator>>(Bitstream& in, X& obj); \ + bool operator==(const X& first, const X& second); \ + bool operator!=(const X& first, const X& second); \ static const int* _TypePtr##X = &X::Type; \ _Pragma(STRINGIFY(unused(_TypePtr##X))) #endif diff --git a/libraries/metavoxels/src/DatagramSequencer.cpp b/libraries/metavoxels/src/DatagramSequencer.cpp index af271d97ba..fcbe6b5e87 100644 --- a/libraries/metavoxels/src/DatagramSequencer.cpp +++ b/libraries/metavoxels/src/DatagramSequencer.cpp @@ -20,7 +20,8 @@ const int MAX_DATAGRAM_SIZE = MAX_PACKET_SIZE; const int DEFAULT_MAX_PACKET_SIZE = 3000; -DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader) : +DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader, QObject* parent) : + QObject(parent), _outgoingPacketStream(&_outgoingPacketData, QIODevice::WriteOnly), _outputStream(_outgoingPacketStream), _incomingDatagramStream(&_incomingDatagramBuffer), @@ -174,13 +175,13 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { } // read and dispatch the high-priority messages - int highPriorityMessageCount; + quint32 highPriorityMessageCount; _incomingPacketStream >> highPriorityMessageCount; int newHighPriorityMessages = highPriorityMessageCount - _receivedHighPriorityMessages; - for (int i = 0; i < highPriorityMessageCount; i++) { + for (quint32 i = 0; i < highPriorityMessageCount; i++) { QVariant data; _inputStream >> data; - if (i >= _receivedHighPriorityMessages) { + if ((int)i >= _receivedHighPriorityMessages) { handleHighPriorityMessage(data); } } @@ -192,10 +193,10 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { // read the reliable data, if any quint32 reliableChannels; _incomingPacketStream >> reliableChannels; - for (int i = 0; i < reliableChannels; i++) { + for (quint32 i = 0; i < reliableChannels; i++) { quint32 channelIndex; _incomingPacketStream >> channelIndex; - getReliableOutputChannel(channelIndex)->readData(_incomingPacketStream); + getReliableInputChannel(channelIndex)->readData(_incomingPacketStream); } _incomingPacketStream.device()->seek(0); @@ -311,6 +312,178 @@ void DatagramSequencer::handleHighPriorityMessage(const QVariant& data) { } } +const int INITIAL_CIRCULAR_BUFFER_CAPACITY = 16; + +CircularBuffer::CircularBuffer(QObject* parent) : + QIODevice(parent), + _data(INITIAL_CIRCULAR_BUFFER_CAPACITY, 0), + _position(0), + _size(0), + _offset(0) { +} + +void CircularBuffer::append(const char* data, int length) { + // resize to fit + int oldSize = _size; + resize(_size + length); + + // write our data in up to two segments: one from the position to the end, one from the beginning + int end = (_position + oldSize) % _data.size(); + int firstSegment = qMin(length, _data.size() - end); + memcpy(_data.data() + end, data, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + memcpy(_data.data(), data + firstSegment, secondSegment); + } +} + +void CircularBuffer::remove(int length) { + _position = (_position + length) % _data.size(); + _size -= length; +} + +QByteArray CircularBuffer::readBytes(int offset, int length) const { + // write in up to two segments + QByteArray array; + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + array.append(_data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + array.append(_data.constData(), secondSegment); + } + return array; +} + +void CircularBuffer::writeToStream(int offset, int length, QDataStream& out) const { + // write in up to two segments + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + out.writeRawData(_data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + out.writeRawData(_data.constData(), secondSegment); + } +} + +void CircularBuffer::readFromStream(int offset, int length, QDataStream& in) { + // resize to fit + int requiredSize = offset + length; + if (requiredSize > _size) { + resize(requiredSize); + } + + // read in up to two segments + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + in.readRawData(_data.data() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + in.readRawData(_data.data(), secondSegment); + } +} + +void CircularBuffer::appendToBuffer(int offset, int length, CircularBuffer& buffer) const { + // append in up to two segments + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + buffer.append(_data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + buffer.append(_data.constData(), secondSegment); + } +} + +bool CircularBuffer::atEnd() const { + return _offset >= _size; +} + +qint64 CircularBuffer::bytesAvailable() const { + return _size - _offset + QIODevice::bytesAvailable(); +} + +bool CircularBuffer::canReadLine() const { + for (int offset = _offset; offset < _size; offset++) { + if (_data.at((_position + offset) % _data.size()) == '\n') { + return true; + } + } + return false; +} + +bool CircularBuffer::open(OpenMode flags) { + return QIODevice::open(flags | QIODevice::Unbuffered); +} + +qint64 CircularBuffer::pos() const { + return _offset; +} + +bool CircularBuffer::seek(qint64 pos) { + if (pos < 0 || pos > _size) { + return false; + } + _offset = pos; + return true; +} + +qint64 CircularBuffer::size() const { + return _size; +} + +qint64 CircularBuffer::readData(char* data, qint64 length) { + int readable = qMin((int)length, _size - _offset); + + // read in up to two segments + int start = (_position + _offset) % _data.size(); + int firstSegment = qMin((int)length, _data.size() - start); + memcpy(data, _data.constData() + start, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + memcpy(data + firstSegment, _data.constData(), secondSegment); + } + _offset += readable; + return readable; +} + +qint64 CircularBuffer::writeData(const char* data, qint64 length) { + // resize to fit + int requiredSize = _offset + length; + if (requiredSize > _size) { + resize(requiredSize); + } + + // write in up to two segments + int start = (_position + _offset) % _data.size(); + int firstSegment = qMin((int)length, _data.size() - start); + memcpy(_data.data() + start, data, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + memcpy(_data.data(), data + firstSegment, secondSegment); + } + _offset += length; + return length; +} + +void CircularBuffer::resize(int size) { + if (size > _data.size()) { + // double our capacity until we can fit the desired length + int newCapacity = _data.size(); + do { + newCapacity *= 2; + } while (size > newCapacity); + + int oldCapacity = _data.size(); + _data.resize(newCapacity); + + int trailing = _position + _size - oldCapacity; + if (trailing > 0) { + memcpy(_data.data() + oldCapacity, _data.constData(), trailing); + } + } + _size = size; +} + SpanList::SpanList() : _totalSet(0) { } @@ -369,7 +542,7 @@ int SpanList::set(int offset, int length) { int SpanList::setSpans(QList::iterator it, int length) { int remainingLength = length; int totalRemoved = 0; - for (; it != _spans.end(); it++) { + for (; it != _spans.end(); it = _spans.erase(it)) { if (remainingLength < it->unset) { it->unset -= remainingLength; totalRemoved += remainingLength; @@ -378,7 +551,6 @@ int SpanList::setSpans(QList::iterator it, int length) { int combined = it->unset + it->set; remainingLength = qMax(remainingLength - combined, 0); totalRemoved += combined; - it = _spans.erase(it); _totalSet -= it->set; } return qMax(length, totalRemoved); @@ -424,14 +596,13 @@ void ReliableChannel::writeData(QDataStream& out, int bytes, QVector 0 && leftover > 0) { spanCount++; - remainingBytes -= getBytesToWrite(first, leftover); + remainingBytes -= getBytesToWrite(first, qMin(remainingBytes, leftover)); } } @@ -448,8 +619,9 @@ void ReliableChannel::writeData(QDataStream& out, int bytes, QVector 0 && position < _buffer.pos()) { - remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, (int)(_buffer.pos() - position)), spans); + int leftover = _buffer.pos() - position; + if (remainingBytes > 0 && leftover > 0) { + remainingBytes -= writeSpan(out, first, position, qMin(remainingBytes, leftover), spans); } } } @@ -473,16 +645,16 @@ int ReliableChannel::writeSpan(QDataStream& out, bool& first, int position, int spans.append(span); out << (quint32)span.offset; out << (quint32)length; - out.writeRawData(_buffer.data().constData() + position, length); + _buffer.writeToStream(position, length, out); return length; } void ReliableChannel::spanAcknowledged(const DatagramSequencer::ChannelSpan& span) { int advancement = _acknowledged.set(span.offset - _offset, span.length); if (advancement > 0) { - // TODO: better way of pruning buffer - _buffer.buffer() = _buffer.buffer().right(_buffer.size() - advancement); + _buffer.remove(advancement); _buffer.seek(_buffer.size()); + _offset += advancement; _writePosition = qMax(_writePosition - advancement, 0); } @@ -491,38 +663,40 @@ void ReliableChannel::spanAcknowledged(const DatagramSequencer::ChannelSpan& spa void ReliableChannel::readData(QDataStream& in) { quint32 segments; in >> segments; - for (int i = 0; i < segments; i++) { + bool readSome = false; + for (quint32 i = 0; i < segments; i++) { quint32 offset, size; in >> offset >> size; int position = offset - _offset; int end = position + size; - if (_assemblyBuffer.size() < end) { - _assemblyBuffer.resize(end); - } if (end <= 0) { in.skipRawData(size); + } else if (position < 0) { in.skipRawData(-position); - in.readRawData(_assemblyBuffer.data(), size + position); + _assemblyBuffer.readFromStream(0, end, in); + } else { - in.readRawData(_assemblyBuffer.data() + position, size); + _assemblyBuffer.readFromStream(position, size, in); } int advancement = _acknowledged.set(position, size); if (advancement > 0) { - // TODO: better way of pruning buffer - _buffer.buffer().append(_assemblyBuffer.constData(), advancement); - emit _buffer.readyRead(); - _assemblyBuffer = _assemblyBuffer.right(_assemblyBuffer.size() - advancement); + _assemblyBuffer.appendToBuffer(0, advancement, _buffer); + _assemblyBuffer.remove(advancement); _offset += advancement; + readSome = true; } } - // when the read head is sufficiently advanced into the buffer, prune it off. this along - // with other buffer usages should be replaced with a circular buffer - const int PRUNE_SIZE = 8192; - if (_buffer.pos() > PRUNE_SIZE) { - _buffer.buffer() = _buffer.buffer().right(_buffer.size() - _buffer.pos()); + // let listeners know that there's data to read + if (readSome) { + emit _buffer.readyRead(); + } + + // prune any read data from the buffer + if (_buffer.pos() > 0) { + _buffer.remove((int)_buffer.pos()); _buffer.seek(0); } } diff --git a/libraries/metavoxels/src/DatagramSequencer.h b/libraries/metavoxels/src/DatagramSequencer.h index 44d3c83116..27a4f05379 100644 --- a/libraries/metavoxels/src/DatagramSequencer.h +++ b/libraries/metavoxels/src/DatagramSequencer.h @@ -32,7 +32,7 @@ public: int firstPacketNumber; }; - DatagramSequencer(const QByteArray& datagramHeader = QByteArray()); + DatagramSequencer(const QByteArray& datagramHeader = QByteArray(), QObject* parent = NULL); /// Returns the packet number of the last packet sent. int getOutgoingPacketNumber() const { return _outgoingPacketNumber; } @@ -58,7 +58,7 @@ public: /// Returns the output channel at the specified index, creating it if necessary. ReliableChannel* getReliableOutputChannel(int index = 0); - /// Returns the intput channel at the + /// Returns the intput channel at the specified index, creating it if necessary. ReliableChannel* getReliableInputChannel(int index = 0); /// Starts a new packet for transmission. @@ -167,6 +167,56 @@ private: QHash _reliableInputChannels; }; +/// A circular buffer, where one may efficiently append data to the end or remove data from the beginning. +class CircularBuffer : public QIODevice { +public: + + CircularBuffer(QObject* parent = NULL); + + /// Appends data to the end of the buffer. + void append(const QByteArray& data) { append(data.constData(), data.size()); } + + /// Appends data to the end of the buffer. + void append(const char* data, int length); + + /// Removes data from the beginning of the buffer. + void remove(int length); + + /// Reads part of the data from the buffer. + QByteArray readBytes(int offset, int length) const; + + /// Writes part of the buffer to the supplied stream. + void writeToStream(int offset, int length, QDataStream& out) const; + + /// Reads part of the buffer from the supplied stream. + void readFromStream(int offset, int length, QDataStream& in); + + /// Appends part of the buffer to the supplied other buffer. + void appendToBuffer(int offset, int length, CircularBuffer& buffer) const; + + virtual bool atEnd() const; + virtual qint64 bytesAvailable() const; + virtual bool canReadLine() const; + virtual bool open(OpenMode flags); + virtual qint64 pos() const; + virtual bool seek(qint64 pos); + virtual qint64 size() const; + +protected: + + virtual qint64 readData(char* data, qint64 length); + virtual qint64 writeData(const char* data, qint64 length); + +private: + + void resize(int size); + + QByteArray _data; + int _position; + int _size; + int _offset; +}; + /// A list of contiguous spans, alternating between set and unset. Conceptually, the list is preceeded by a set /// span of infinite length and followed by an unset span of infinite length. Within those bounds, it alternates /// between unset and set. @@ -208,6 +258,7 @@ public: int getIndex() const { return _index; } + CircularBuffer& getBuffer() { return _buffer; } QDataStream& getDataStream() { return _dataStream; } Bitstream& getBitstream() { return _bitstream; } @@ -237,8 +288,8 @@ private: void readData(QDataStream& in); int _index; - QBuffer _buffer; - QByteArray _assemblyBuffer; + CircularBuffer _buffer; + CircularBuffer _assemblyBuffer; QDataStream _dataStream; Bitstream _bitstream; float _priority; diff --git a/libraries/metavoxels/src/MetavoxelUtil.cpp b/libraries/metavoxels/src/MetavoxelUtil.cpp index e59cf0fe21..d6b42b11fb 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.cpp +++ b/libraries/metavoxels/src/MetavoxelUtil.cpp @@ -11,12 +11,12 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include @@ -76,27 +76,48 @@ static QItemEditorFactory* getItemEditorFactory() { return factory; } +/// Because Windows doesn't necessarily have the staticMetaObject available when we want to create, +/// this class simply delays the value property name lookup until actually requested. +template class LazyItemEditorCreator : public QItemEditorCreatorBase { +public: + + virtual QWidget* createWidget(QWidget* parent) const { return new T(parent); } + + virtual QByteArray valuePropertyName() const; + +protected: + + QByteArray _valuePropertyName; +}; + +template QByteArray LazyItemEditorCreator::valuePropertyName() const { + if (_valuePropertyName.isNull()) { + const_cast*>(this)->_valuePropertyName = T::staticMetaObject.userProperty().name(); + } + return _valuePropertyName; +} + static QItemEditorCreatorBase* createDoubleEditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } static QItemEditorCreatorBase* createQColorEditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } static QItemEditorCreatorBase* createVec3EditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } static QItemEditorCreatorBase* createParameterizedURLEditorCreator() { - QItemEditorCreatorBase* creator = new QStandardItemEditorCreator(); + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } @@ -120,6 +141,12 @@ QUuid readSessionID(const QByteArray& data, const SharedNodePointer& sendingNode return QUuid::fromRfc4122(QByteArray::fromRawData(data.constData() + headerSize, UUID_BYTES)); } +QByteArray signal(const char* signature) { + static QByteArray prototype = SIGNAL(dummyMethod()); + QByteArray signal = prototype; + return signal.replace("dummyMethod()", signature); +} + bool Box::contains(const Box& other) const { return other.minimum.x >= minimum.x && other.maximum.x <= maximum.x && other.minimum.y >= minimum.y && other.maximum.y <= maximum.y && @@ -301,8 +328,7 @@ void ParameterizedURLEditor::continueUpdatingParameters() { QMetaProperty widgetProperty = widgetMetaObject->property(widgetMetaObject->indexOfProperty(valuePropertyName)); widgetProperty.write(widget, _url.getParameters().value(parameter.name)); if (widgetProperty.hasNotifySignal()) { - connect(widget, QByteArray(SIGNAL()).append(widgetProperty.notifySignal().methodSignature()), - SLOT(updateURL())); + connect(widget, signal(widgetProperty.notifySignal().methodSignature()), SLOT(updateURL())); } } } diff --git a/libraries/metavoxels/src/MetavoxelUtil.h b/libraries/metavoxels/src/MetavoxelUtil.h index 9fa721d21c..7cfedd7a62 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.h +++ b/libraries/metavoxels/src/MetavoxelUtil.h @@ -33,6 +33,9 @@ class NetworkProgram; /// \return the session ID, or a null ID if invalid (in which case a warning will be logged) QUuid readSessionID(const QByteArray& data, const SharedNodePointer& sendingNode, int& headerPlusIDSize); +/// Performs the runtime equivalent of Qt's SIGNAL macro, which is to attach a prefix to the signature. +QByteArray signal(const char* signature); + /// A streamable axis-aligned bounding box. class Box { STREAMABLE diff --git a/libraries/metavoxels/src/SharedObject.cpp b/libraries/metavoxels/src/SharedObject.cpp index a3af307297..f97e285bcf 100644 --- a/libraries/metavoxels/src/SharedObject.cpp +++ b/libraries/metavoxels/src/SharedObject.cpp @@ -13,6 +13,7 @@ #include #include "Bitstream.h" +#include "MetavoxelUtil.h" #include "SharedObject.h" SharedObject::SharedObject() : _referenceCount(0) { @@ -204,8 +205,7 @@ void SharedObjectEditor::updateType() { QMetaProperty widgetProperty = widgetMetaObject->property(widgetMetaObject->indexOfProperty(valuePropertyName)); widgetProperty.write(widget, property.read(newObject)); if (widgetProperty.hasNotifySignal()) { - connect(widget, QByteArray(SIGNAL()).append(widgetProperty.notifySignal().methodSignature()), - SLOT(propertyChanged())); + connect(widget, signal(widgetProperty.notifySignal().methodSignature()), SLOT(propertyChanged())); } } } diff --git a/libraries/octree/src/OctreeEditPacketSender.cpp b/libraries/octree/src/OctreeEditPacketSender.cpp index 1c1ed13f8d..38abd15dc8 100644 --- a/libraries/octree/src/OctreeEditPacketSender.cpp +++ b/libraries/octree/src/OctreeEditPacketSender.cpp @@ -39,6 +39,7 @@ OctreeEditPacketSender::OctreeEditPacketSender() : } OctreeEditPacketSender::~OctreeEditPacketSender() { + _pendingPacketsLock.lock(); while (!_preServerSingleMessagePackets.empty()) { EditPacketBuffer* packet = _preServerSingleMessagePackets.front(); delete packet; @@ -49,6 +50,7 @@ OctreeEditPacketSender::~OctreeEditPacketSender() { delete packet; _preServerPackets.erase(_preServerPackets.begin()); } + _pendingPacketsLock.unlock(); //printf("OctreeEditPacketSender::~OctreeEditPacketSender() [%p] destroyed... \n", this); } @@ -115,6 +117,7 @@ void OctreeEditPacketSender::processPreServerExistsPackets() { assert(serversExist()); // we should only be here if we have jurisdictions // First send out all the single message packets... + _pendingPacketsLock.lock(); while (!_preServerSingleMessagePackets.empty()) { EditPacketBuffer* packet = _preServerSingleMessagePackets.front(); queuePacketToNodes(&packet->_currentBuffer[0], packet->_currentSize); @@ -129,6 +132,7 @@ void OctreeEditPacketSender::processPreServerExistsPackets() { delete packet; _preServerPackets.erase(_preServerPackets.begin()); } + _pendingPacketsLock.unlock(); // if while waiting for the jurisdictions the caller called releaseQueuedMessages() // then we want to honor that request now. @@ -140,8 +144,10 @@ void OctreeEditPacketSender::processPreServerExistsPackets() { void OctreeEditPacketSender::queuePendingPacketToNodes(PacketType type, unsigned char* buffer, ssize_t length) { // If we're asked to save messages while waiting for voxel servers to arrive, then do so... + if (_maxPendingMessages > 0) { EditPacketBuffer* packet = new EditPacketBuffer(type, buffer, length); + _pendingPacketsLock.lock(); _preServerSingleMessagePackets.push_back(packet); // if we've saved MORE than our max, then clear out the oldest packet... int allPendingMessages = _preServerSingleMessagePackets.size() + _preServerPackets.size(); @@ -150,6 +156,7 @@ void OctreeEditPacketSender::queuePendingPacketToNodes(PacketType type, unsigned delete packet; _preServerSingleMessagePackets.erase(_preServerSingleMessagePackets.begin()); } + _pendingPacketsLock.unlock(); } } @@ -197,6 +204,7 @@ void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, unsigned ch if (!serversExist()) { if (_maxPendingMessages > 0) { EditPacketBuffer* packet = new EditPacketBuffer(type, codeColorBuffer, length); + _pendingPacketsLock.lock(); _preServerPackets.push_back(packet); // if we've saved MORE than out max, then clear out the oldest packet... @@ -206,12 +214,11 @@ void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, unsigned ch delete packet; _preServerPackets.erase(_preServerPackets.begin()); } - } + _pendingPacketsLock.unlock(); + } return; // bail early } - //qDebug() << "queueOctreeEditMessage() line:" << __LINE__; - // We want to filter out edit messages for servers based on the server's Jurisdiction // But we can't really do that with a packed message, since each edit message could be destined // for a different server... So we need to actually manage multiple queued packets... one @@ -229,18 +236,14 @@ void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, unsigned ch if ((*_serverJurisdictions).find(nodeUUID) != (*_serverJurisdictions).end()) { const JurisdictionMap& map = (*_serverJurisdictions)[nodeUUID]; isMyJurisdiction = (map.isMyJurisdiction(codeColorBuffer, CHECK_NODE_ONLY) == JurisdictionMap::WITHIN); - //qDebug() << "queueOctreeEditMessage() line:" << __LINE__ << " isMyJurisdiction=" << isMyJurisdiction; } else { isMyJurisdiction = false; - //qDebug() << "queueOctreeEditMessage() line:" << __LINE__; } } if (isMyJurisdiction) { EditPacketBuffer& packetBuffer = _pendingEditPackets[nodeUUID]; packetBuffer._nodeUUID = nodeUUID; - //qDebug() << "queueOctreeEditMessage() line:" << __LINE__; - // If we're switching type, then we send the last one and start over if ((type != packetBuffer._currentType && packetBuffer._currentSize > 0) || (packetBuffer._currentSize + length >= _maxPacketSize)) { @@ -259,11 +262,8 @@ void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, unsigned ch // fixup the buffer for any clock skew if (node->getClockSkewUsec() != 0) { adjustEditPacketForClockSkew(codeColorBuffer, length, node->getClockSkewUsec()); - //qDebug() << "queueOctreeEditMessage() line:" << __LINE__; } - //qDebug() << "queueOctreeEditMessage() line:" << __LINE__; - memcpy(&packetBuffer._currentBuffer[packetBuffer._currentSize], codeColorBuffer, length); packetBuffer._currentSize += length; } @@ -280,14 +280,12 @@ void OctreeEditPacketSender::releaseQueuedMessages() { } else { for (std::map::iterator i = _pendingEditPackets.begin(); i != _pendingEditPackets.end(); i++) { releaseQueuedPacket(i->second); - //qDebug() << "releaseQueuedMessages() line:" << __LINE__; } } } void OctreeEditPacketSender::releaseQueuedPacket(EditPacketBuffer& packetBuffer) { if (packetBuffer._currentSize > 0 && packetBuffer._currentType != PacketTypeUnknown) { - //qDebug() << "OctreeEditPacketSender::releaseQueuedPacket() line:" << __LINE__; queuePacketToNode(packetBuffer._nodeUUID, &packetBuffer._currentBuffer[0], packetBuffer._currentSize); } packetBuffer._currentSize = 0; diff --git a/libraries/octree/src/OctreeEditPacketSender.h b/libraries/octree/src/OctreeEditPacketSender.h index 3f20ed45a3..75ad02a1c6 100644 --- a/libraries/octree/src/OctreeEditPacketSender.h +++ b/libraries/octree/src/OctreeEditPacketSender.h @@ -105,8 +105,9 @@ protected: // These are packets that are waiting to be processed because we don't yet know if there are servers int _maxPendingMessages; bool _releaseQueuedMessagesPending; - std::vector _preServerPackets; // these will get packed into other larger packets - std::vector _preServerSingleMessagePackets; // these will go out as is + QMutex _pendingPacketsLock; + QVector _preServerPackets; // these will get packed into other larger packets + QVector _preServerSingleMessagePackets; // these will go out as is NodeToJurisdictionMap* _serverJurisdictions; diff --git a/libraries/particles/src/ParticleCollisionSystem.cpp b/libraries/particles/src/ParticleCollisionSystem.cpp index c718ab3ddc..bb0260c2bf 100644 --- a/libraries/particles/src/ParticleCollisionSystem.cpp +++ b/libraries/particles/src/ParticleCollisionSystem.cpp @@ -139,18 +139,22 @@ void ParticleCollisionSystem::updateCollisionWithParticles(Particle* particleA) // handle A particle particleA->setVelocity(particleA->getVelocity() - axialVelocity * (2.0f * massB / totalMass)); + particleA->setPosition(particleA->getPosition() - 0.5f * penetration); ParticleProperties propertiesA; ParticleID particleAid(particleA->getID()); propertiesA.copyFromParticle(*particleA); propertiesA.setVelocity(particleA->getVelocity() * (float)TREE_SCALE); + propertiesA.setPosition(particleA->getPosition() * (float)TREE_SCALE); _packetSender->queueParticleEditMessage(PacketTypeParticleAddOrEdit, particleAid, propertiesA); // handle B particle particleB->setVelocity(particleB->getVelocity() + axialVelocity * (2.0f * massA / totalMass)); + particleA->setPosition(particleB->getPosition() + 0.5f * penetration); ParticleProperties propertiesB; ParticleID particleBid(particleB->getID()); propertiesB.copyFromParticle(*particleB); propertiesB.setVelocity(particleB->getVelocity() * (float)TREE_SCALE); + propertiesB.setPosition(particleB->getPosition() * (float)TREE_SCALE); _packetSender->queueParticleEditMessage(PacketTypeParticleAddOrEdit, particleBid, propertiesB); _packetSender->releaseQueuedMessages(); @@ -182,7 +186,9 @@ void ParticleCollisionSystem::updateCollisionWithAvatars(Particle* particle) { CollisionInfo collisionInfo; collisionInfo._damping = DAMPING; collisionInfo._elasticity = ELASTICITY; - if (avatar->findSphereCollision(center, radius, collisionInfo)) { + if (avatar->findSphereCollisionWithHands(center, radius, collisionInfo)) { + // TODO: Andrew to resurrect particles-vs-avatar body collisions + //avatar->findSphereCollisionWithSkeleton(center, radius, collisionInfo)) { collisionInfo._addedVelocity /= (float)(TREE_SCALE); glm::vec3 relativeVelocity = collisionInfo._addedVelocity - particle->getVelocity(); if (glm::dot(relativeVelocity, collisionInfo._penetration) < 0.f) { diff --git a/libraries/shared/src/CollisionInfo.h b/libraries/shared/src/CollisionInfo.h index 38ae64e30c..1fa95cd83a 100644 --- a/libraries/shared/src/CollisionInfo.h +++ b/libraries/shared/src/CollisionInfo.h @@ -11,22 +11,28 @@ #include +const uint32_t COLLISION_GROUP_ENVIRONMENT = 1U << 0; +const uint32_t COLLISION_GROUP_AVATARS = 1U << 1; +const uint32_t COLLISION_GROUP_VOXELS = 1U << 2; +const uint32_t COLLISION_GROUP_PARTICLES = 1U << 3; + class CollisionInfo { public: CollisionInfo() : _damping(0.f), _elasticity(1.f), + _contactPoint(0.f), _penetration(0.f), - _addedVelocity(0.f) { - } + _addedVelocity(0.f) { + } ~CollisionInfo() {} - //glm::vec3 _point; //glm::vec3 _normal; float _damping; float _elasticity; - glm::vec3 _penetration; // depth that bodyA is penetrates bodyB + glm::vec3 _contactPoint; // world-frame point on bodyA that is deepest into bodyB + glm::vec3 _penetration; // depth that bodyA penetrates into bodyB glm::vec3 _addedVelocity; }; diff --git a/libraries/shared/src/Logging.cpp b/libraries/shared/src/Logging.cpp index bc0d4af084..a9f71e346e 100644 --- a/libraries/shared/src/Logging.cpp +++ b/libraries/shared/src/Logging.cpp @@ -98,6 +98,9 @@ const char* stringForLogType(QtMsgType msgType) { const char DATE_STRING_FORMAT[] = "%F %H:%M:%S %z"; void Logging::verboseMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + if (message.isEmpty()) { + return; + } // log prefix is in the following format // [DEBUG] [TIMESTAMP] [PID:PARENT_PID] [TARGET] logged string diff --git a/libraries/shared/src/NodeList.cpp b/libraries/shared/src/NodeList.cpp index 92f6f961d5..c0bc7f0010 100644 --- a/libraries/shared/src/NodeList.cpp +++ b/libraries/shared/src/NodeList.cpp @@ -81,13 +81,10 @@ NodeList::~NodeList() { } bool NodeList::packetVersionAndHashMatch(const QByteArray& packet) { - // currently this just checks if the version in the packet matches our return from versionForPacketType - // may need to be expanded in the future for types and versions that take > than 1 byte - if (packet[1] != versionForPacketType(packetTypeForPacket(packet)) && packetTypeForPacket(packet) != PacketTypeStunResponse) { PacketType mismatchType = packetTypeForPacket(packet); - int numPacketTypeBytes = arithmeticCodingValueFromBuffer(packet.data()); + int numPacketTypeBytes = numBytesArithmeticCodingFromBuffer(packet.data()); qDebug() << "Packet version mismatch on" << packetTypeForPacket(packet) << "- Sender" << uuidFromPacketHeader(packet) << "sent" << qPrintable(QString::number(packet[numPacketTypeBytes])) << "but" diff --git a/libraries/shared/src/PacketHeaders.cpp b/libraries/shared/src/PacketHeaders.cpp index b46a57d4aa..b1f6ef1730 100644 --- a/libraries/shared/src/PacketHeaders.cpp +++ b/libraries/shared/src/PacketHeaders.cpp @@ -57,6 +57,9 @@ PacketVersion versionForPacketType(PacketType type) { case PacketTypeDataServerConfirm: case PacketTypeDataServerSend: return 1; + case PacketTypeVoxelSet: + case PacketTypeVoxelSetDestructive: + return 1; default: return 0; } diff --git a/libraries/voxels/src/VoxelEditPacketSender.cpp b/libraries/voxels/src/VoxelEditPacketSender.cpp index fea75abf23..a6d3668207 100644 --- a/libraries/voxels/src/VoxelEditPacketSender.cpp +++ b/libraries/voxels/src/VoxelEditPacketSender.cpp @@ -14,24 +14,13 @@ #include #include "VoxelEditPacketSender.h" -////////////////////////////////////////////////////////////////////////////////////////// -// Function: createVoxelEditMessage() -// Description: creates an "insert" or "remove" voxel message for a voxel code -// corresponding to the closest voxel which encloses a cube with -// lower corners at x,y,z, having side of length S. -// The input values x,y,z range 0.0 <= v < 1.0 -// message should be either 'S' for SET or 'E' for ERASE -// -// IMPORTANT: The buffer is returned to you a buffer which you MUST delete when you are -// done with it. -// -// HACK ATTACK: Well, what if this is larger than the MTU? That's the caller's problem, we -// just truncate the message -// -// Complaints: Brad :) #define GUESS_OF_VOXELCODE_SIZE 10 #define MAXIMUM_EDIT_VOXEL_MESSAGE_SIZE 1500 #define SIZE_OF_COLOR_DATA sizeof(rgbColor) +/// creates an "insert" or "remove" voxel message for a voxel code corresponding to the closest voxel which encloses a cube +/// with lower corners at x,y,z, having side of length S. The input values x,y,z range 0.0 <= v < 1.0 message should be either +/// PacketTypeVoxelSet, PacketTypeVoxelSetDestructive, or PacketTypeVoxelErase. The buffer is returned to caller becomes +/// responsibility of caller and MUST be deleted by caller. bool createVoxelEditMessage(PacketType command, short int sequence, int voxelCount, VoxelDetail* voxelDetails, unsigned char*& bufferOut, int& sizeOut) { @@ -144,9 +133,9 @@ void VoxelEditPacketSender::queueVoxelEditMessages(PacketType type, int numberOf for (int i = 0; i < numberOfDetails; i++) { // use MAX_PACKET_SIZE since it's static and guarenteed to be larger than _maxPacketSize - static unsigned char bufferOut[MAX_PACKET_SIZE]; + unsigned char bufferOut[MAX_PACKET_SIZE]; int sizeOut = 0; - + if (encodeVoxelEditMessageDetails(type, 1, &details[i], &bufferOut[0], _maxPacketSize, sizeOut)) { queueOctreeEditMessage(type, bufferOut, sizeOut); } diff --git a/libraries/voxels/src/VoxelTree.cpp b/libraries/voxels/src/VoxelTree.cpp index e29cfda41d..08ec2cfc3c 100644 --- a/libraries/voxels/src/VoxelTree.cpp +++ b/libraries/voxels/src/VoxelTree.cpp @@ -521,6 +521,8 @@ bool VoxelTree::handlesEditPacketType(PacketType packetType) const { } } +const unsigned int REPORT_OVERFLOW_WARNING_INTERVAL = 100; +unsigned int overflowWarnings = 0; int VoxelTree::processEditPacketData(PacketType packetType, const unsigned char* packetData, int packetLength, const unsigned char* editData, int maxLength, const SharedNodePointer& node) { @@ -532,9 +534,17 @@ int VoxelTree::processEditPacketData(PacketType packetType, const unsigned char* int octets = numberOfThreeBitSectionsInCode(editData, maxLength); if (octets == OVERFLOWED_OCTCODE_BUFFER) { - printf("WARNING! Got voxel edit record that would overflow buffer in numberOfThreeBitSectionsInCode(), "); - printf("bailing processing of packet!\n"); - return 0; + overflowWarnings++; + if (overflowWarnings % REPORT_OVERFLOW_WARNING_INTERVAL == 1) { + qDebug() << "WARNING! Got voxel edit record that would overflow buffer in numberOfThreeBitSectionsInCode()" + " [NOTE: this is warning number" << overflowWarnings << ", the next" << + (REPORT_OVERFLOW_WARNING_INTERVAL-1) << "will be suppressed.]"; + + QDebug debug = qDebug(); + debug << "edit data contents:"; + outputBufferBits(editData, maxLength, &debug); + } + return maxLength; } const int COLOR_SIZE_IN_BYTES = 3; @@ -542,9 +552,17 @@ int VoxelTree::processEditPacketData(PacketType packetType, const unsigned char* int voxelDataSize = voxelCodeSize + COLOR_SIZE_IN_BYTES; if (voxelDataSize > maxLength) { - printf("WARNING! Got voxel edit record that would overflow buffer, bailing processing of packet!\n"); - printf("bailing processing of packet!\n"); - return 0; + overflowWarnings++; + if (overflowWarnings % REPORT_OVERFLOW_WARNING_INTERVAL == 1) { + qDebug() << "WARNING! Got voxel edit record that would overflow buffer." + " [NOTE: this is warning number" << overflowWarnings << ", the next" << + (REPORT_OVERFLOW_WARNING_INTERVAL-1) << "will be suppressed.]"; + + QDebug debug = qDebug(); + debug << "edit data contents:"; + outputBufferBits(editData, maxLength, &debug); + } + return maxLength; } readCodeColorBufferToTree(editData, destructive); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..e817ffe506 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8) + +# add the test directories +file(GLOB TEST_SUBDIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*) +foreach(DIR ${TEST_SUBDIRS}) + if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${DIR}) + add_subdirectory(${DIR}) + endif() +endforeach() + diff --git a/tests/metavoxels/CMakeLists.txt b/tests/metavoxels/CMakeLists.txt new file mode 100644 index 0000000000..416f398470 --- /dev/null +++ b/tests/metavoxels/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 2.8) + +set(TARGET_NAME metavoxel-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +find_package(Qt5Network REQUIRED) +find_package(Qt5Script REQUIRED) +find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(metavoxels ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) + +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + diff --git a/tests/metavoxels/src/MetavoxelTests.cpp b/tests/metavoxels/src/MetavoxelTests.cpp new file mode 100644 index 0000000000..15d7463742 --- /dev/null +++ b/tests/metavoxels/src/MetavoxelTests.cpp @@ -0,0 +1,272 @@ +// +// MetavoxelTests.cpp +// metavoxel-tests +// +// Created by Andrzej Kapolka on 2/7/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include + +#include + +#include "MetavoxelTests.h" + +MetavoxelTests::MetavoxelTests(int& argc, char** argv) : + QCoreApplication(argc, argv) { +} + +static int datagramsSent = 0; +static int datagramsReceived = 0; +static int highPriorityMessagesSent = 0; +static int highPriorityMessagesReceived = 0; +static int unreliableMessagesSent = 0; +static int unreliableMessagesReceived = 0; +static int streamedBytesSent = 0; +static int streamedBytesReceived = 0; +static int lowPriorityStreamedBytesSent = 0; +static int lowPriorityStreamedBytesReceived = 0; + +bool MetavoxelTests::run() { + + qDebug() << "Running metavoxel tests..."; + + // seed the random number generator so that our tests are reproducible + srand(0xBAAAAABE); + + // create two endpoints with the same header + QByteArray datagramHeader("testheader"); + Endpoint alice(datagramHeader), bob(datagramHeader); + + alice.setOther(&bob); + bob.setOther(&alice); + + // perform a large number of simulation iterations + const int SIMULATION_ITERATIONS = 100000; + for (int i = 0; i < SIMULATION_ITERATIONS; i++) { + if (alice.simulate(i) || bob.simulate(i)) { + return true; + } + } + + qDebug() << "Sent" << highPriorityMessagesSent << "high priority messages, received" << highPriorityMessagesReceived; + qDebug() << "Sent" << unreliableMessagesSent << "unreliable messages, received" << unreliableMessagesReceived; + qDebug() << "Sent" << streamedBytesSent << "streamed bytes, received" << streamedBytesReceived; + qDebug() << "Sent" << lowPriorityStreamedBytesSent << "low-priority streamed bytes, received" << + lowPriorityStreamedBytesReceived; + qDebug() << "Sent" << datagramsSent << "datagrams, received" << datagramsReceived; + + qDebug() << "All tests passed!"; + + return false; +} + +static QByteArray createRandomBytes(int minimumSize, int maximumSize) { + QByteArray bytes(randIntInRange(minimumSize, maximumSize), 0); + for (int i = 0; i < bytes.size(); i++) { + bytes[i] = rand(); + } + return bytes; +} + +static QByteArray createRandomBytes() { + const int MIN_BYTES = 4; + const int MAX_BYTES = 16; + return createRandomBytes(MIN_BYTES, MAX_BYTES); +} + +Endpoint::Endpoint(const QByteArray& datagramHeader) : + _sequencer(new DatagramSequencer(datagramHeader, this)), + _highPriorityMessagesToSend(0.0f) { + + connect(_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendDatagram(const QByteArray&))); + connect(_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readMessage(Bitstream&))); + connect(_sequencer, SIGNAL(receivedHighPriorityMessage(const QVariant&)), + SLOT(handleHighPriorityMessage(const QVariant&))); + connect(&_sequencer->getReliableInputChannel()->getBuffer(), SIGNAL(readyRead()), SLOT(readReliableChannel())); + connect(&_sequencer->getReliableInputChannel(1)->getBuffer(), SIGNAL(readyRead()), SLOT(readLowPriorityReliableChannel())); + + // enqueue a large amount of data in a low-priority channel + ReliableChannel* output = _sequencer->getReliableOutputChannel(1); + output->setPriority(0.25f); + const int MIN_LOW_PRIORITY_DATA = 100000; + const int MAX_LOW_PRIORITY_DATA = 200000; + QByteArray bytes = createRandomBytes(MIN_LOW_PRIORITY_DATA, MAX_LOW_PRIORITY_DATA); + _lowPriorityDataStreamed.append(bytes); + output->getBuffer().write(bytes); + lowPriorityStreamedBytesSent += bytes.size(); +} + +static QVariant createRandomMessage() { + switch (randIntInRange(0, 2)) { + case 0: { + TestMessageA message = { randomBoolean(), rand(), randFloat() }; + return QVariant::fromValue(message); + } + case 1: { + TestMessageB message = { createRandomBytes() }; + return QVariant::fromValue(message); + } + case 2: + default: { + TestMessageC message; + message.foo = randomBoolean(); + message.bar = rand(); + message.baz = randFloat(); + message.bong.foo = createRandomBytes(); + return QVariant::fromValue(message); + } + } +} + +static bool messagesEqual(const QVariant& firstMessage, const QVariant& secondMessage) { + int type = firstMessage.userType(); + if (secondMessage.userType() != type) { + return false; + } + if (type == TestMessageA::Type) { + return firstMessage.value() == secondMessage.value(); + } else if (type == TestMessageB::Type) { + return firstMessage.value() == secondMessage.value(); + } else if (type == TestMessageC::Type) { + return firstMessage.value() == secondMessage.value(); + } else { + return firstMessage == secondMessage; + } +} + +bool Endpoint::simulate(int iterationNumber) { + // update/send our delayed datagrams + for (QList >::iterator it = _delayedDatagrams.begin(); it != _delayedDatagrams.end(); ) { + if (it->second-- == 1) { + _other->_sequencer->receivedDatagram(it->first); + datagramsReceived++; + it = _delayedDatagrams.erase(it); + + } else { + it++; + } + } + + // enqueue some number of high priority messages + const float MIN_HIGH_PRIORITY_MESSAGES = 0.0f; + const float MAX_HIGH_PRIORITY_MESSAGES = 2.0f; + _highPriorityMessagesToSend += randFloatInRange(MIN_HIGH_PRIORITY_MESSAGES, MAX_HIGH_PRIORITY_MESSAGES); + while (_highPriorityMessagesToSend >= 1.0f) { + QVariant message = createRandomMessage(); + _highPriorityMessagesSent.append(message); + _sequencer->sendHighPriorityMessage(message); + highPriorityMessagesSent++; + _highPriorityMessagesToSend -= 1.0f; + } + + // stream some random data + const int MIN_BYTES_TO_STREAM = 10; + const int MAX_BYTES_TO_STREAM = 100; + QByteArray bytes = createRandomBytes(MIN_BYTES_TO_STREAM, MAX_BYTES_TO_STREAM); + _dataStreamed.append(bytes); + streamedBytesSent += bytes.size(); + _sequencer->getReliableOutputChannel()->getDataStream().writeRawData(bytes.constData(), bytes.size()); + + // send a packet + try { + Bitstream& out = _sequencer->startPacket(); + SequencedTestMessage message = { iterationNumber, createRandomMessage() }; + _unreliableMessagesSent.append(message); + unreliableMessagesSent++; + out << message; + _sequencer->endPacket(); + + } catch (const QString& message) { + qDebug() << message; + return true; + } + + return false; +} + +void Endpoint::sendDatagram(const QByteArray& datagram) { + datagramsSent++; + + // some datagrams are dropped + const float DROP_PROBABILITY = 0.1f; + if (randFloat() < DROP_PROBABILITY) { + return; + } + + // some are received out of order + const float REORDER_PROBABILITY = 0.1f; + if (randFloat() < REORDER_PROBABILITY) { + const int MIN_DELAY = 1; + const int MAX_DELAY = 5; + // have to copy the datagram; the one we're passed is a reference to a shared buffer + _delayedDatagrams.append(QPair(QByteArray(datagram.constData(), datagram.size()), + randIntInRange(MIN_DELAY, MAX_DELAY))); + + // and some are duplicated + const float DUPLICATE_PROBABILITY = 0.01f; + if (randFloat() > DUPLICATE_PROBABILITY) { + return; + } + } + + _other->_sequencer->receivedDatagram(datagram); + datagramsReceived++; +} + +void Endpoint::handleHighPriorityMessage(const QVariant& message) { + if (_other->_highPriorityMessagesSent.isEmpty()) { + throw QString("Received unsent/already sent high priority message."); + } + QVariant sentMessage = _other->_highPriorityMessagesSent.takeFirst(); + if (!messagesEqual(message, sentMessage)) { + throw QString("Sent/received high priority message mismatch."); + } + highPriorityMessagesReceived++; +} + +void Endpoint::readMessage(Bitstream& in) { + SequencedTestMessage message; + in >> message; + + for (QList::iterator it = _other->_unreliableMessagesSent.begin(); + it != _other->_unreliableMessagesSent.end(); it++) { + if (it->sequenceNumber == message.sequenceNumber) { + if (!messagesEqual(it->submessage, message.submessage)) { + throw QString("Sent/received unreliable message mismatch."); + } + _other->_unreliableMessagesSent.erase(_other->_unreliableMessagesSent.begin(), it + 1); + unreliableMessagesReceived++; + return; + } + } + throw QString("Received unsent/already sent unreliable message."); +} + +void Endpoint::readReliableChannel() { + CircularBuffer& buffer = _sequencer->getReliableInputChannel()->getBuffer(); + QByteArray bytes = buffer.read(buffer.bytesAvailable()); + if (_other->_dataStreamed.size() < bytes.size()) { + throw QString("Received unsent/already sent streamed data."); + } + QByteArray compare = _other->_dataStreamed.readBytes(0, bytes.size()); + _other->_dataStreamed.remove(bytes.size()); + if (compare != bytes) { + throw QString("Sent/received streamed data mismatch."); + } + streamedBytesReceived += bytes.size(); +} + +void Endpoint::readLowPriorityReliableChannel() { + CircularBuffer& buffer = _sequencer->getReliableInputChannel(1)->getBuffer(); + QByteArray bytes = buffer.read(buffer.bytesAvailable()); + if (_other->_lowPriorityDataStreamed.size() < bytes.size()) { + throw QString("Received unsent/already sent low-priority streamed data."); + } + QByteArray compare = _other->_lowPriorityDataStreamed.readBytes(0, bytes.size()); + _other->_lowPriorityDataStreamed.remove(bytes.size()); + if (compare != bytes) { + throw QString("Sent/received low-priority streamed data mismatch."); + } + lowPriorityStreamedBytesReceived += bytes.size(); +} diff --git a/tests/metavoxels/src/MetavoxelTests.h b/tests/metavoxels/src/MetavoxelTests.h new file mode 100644 index 0000000000..b73f7eb07e --- /dev/null +++ b/tests/metavoxels/src/MetavoxelTests.h @@ -0,0 +1,113 @@ +// +// MetavoxelTests.h +// metavoxel-tests +// +// Created by Andrzej Kapolka on 2/7/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__MetavoxelTests__ +#define __interface__MetavoxelTests__ + +#include +#include + +#include + +class SequencedTestMessage; + +/// Tests various aspects of the metavoxel library. +class MetavoxelTests : public QCoreApplication { + Q_OBJECT + +public: + + MetavoxelTests(int& argc, char** argv); + + /// Performs our various tests. + /// \return true if any of the tests failed. + bool run(); +}; + +/// Represents a simulated endpoint. +class Endpoint : public QObject { + Q_OBJECT + +public: + + Endpoint(const QByteArray& datagramHeader); + + void setOther(Endpoint* other) { _other = other; } + + /// Perform a simulation step. + /// \return true if failure was detected + bool simulate(int iterationNumber); + +private slots: + + void sendDatagram(const QByteArray& datagram); + void handleHighPriorityMessage(const QVariant& message); + void readMessage(Bitstream& in); + void readReliableChannel(); + void readLowPriorityReliableChannel(); + +private: + + DatagramSequencer* _sequencer; + Endpoint* _other; + QList > _delayedDatagrams; + float _highPriorityMessagesToSend; + QVariantList _highPriorityMessagesSent; + QList _unreliableMessagesSent; + CircularBuffer _dataStreamed; + CircularBuffer _lowPriorityDataStreamed; +}; + +/// A simple test message. +class TestMessageA { + STREAMABLE + +public: + + STREAM bool foo; + STREAM int bar; + STREAM float baz; +}; + +DECLARE_STREAMABLE_METATYPE(TestMessageA) + +// Another simple test message. +class TestMessageB { + STREAMABLE + +public: + + STREAM QByteArray foo; +}; + +DECLARE_STREAMABLE_METATYPE(TestMessageB) + +// A test message that demonstrates inheritance and composition. +class TestMessageC : public TestMessageA { + STREAMABLE + +public: + + STREAM TestMessageB bong; +}; + +DECLARE_STREAMABLE_METATYPE(TestMessageC) + +/// Combines a sequence number with a submessage; used for testing unreliable transport. +class SequencedTestMessage { + STREAMABLE + +public: + + STREAM int sequenceNumber; + STREAM QVariant submessage; +}; + +DECLARE_STREAMABLE_METATYPE(SequencedTestMessage) + +#endif /* defined(__interface__MetavoxelTests__) */ diff --git a/tests/metavoxels/src/main.cpp b/tests/metavoxels/src/main.cpp new file mode 100644 index 0000000000..10bf786957 --- /dev/null +++ b/tests/metavoxels/src/main.cpp @@ -0,0 +1,14 @@ +// +// main.cpp +// metavoxel-tests +// +// Created by Andrzej Kapolka on 2/7/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. + +#include + +#include "MetavoxelTests.h" + +int main(int argc, char** argv) { + return MetavoxelTests(argc, argv).run(); +} diff --git a/tools/mtc/src/main.cpp b/tools/mtc/src/main.cpp index 77f0a069b5..050fe0e418 100644 --- a/tools/mtc/src/main.cpp +++ b/tools/mtc/src/main.cpp @@ -90,7 +90,7 @@ void generateOutput (QTextStream& out, const QList& streamables) { foreach (const Streamable& str, streamables) { const QString& name = str.clazz.name; - out << "Bitstream& operator<< (Bitstream& out, const " << name << "& obj) {\n"; + out << "Bitstream& operator<<(Bitstream& out, const " << name << "& obj) {\n"; foreach (const QString& base, str.clazz.bases) { out << " out << static_cast(obj);\n"; } @@ -100,7 +100,7 @@ void generateOutput (QTextStream& out, const QList& streamables) { out << " return out;\n"; out << "}\n"; - out << "Bitstream& operator>> (Bitstream& in, " << name << "& obj) {\n"; + out << "Bitstream& operator>>(Bitstream& in, " << name << "& obj) {\n"; foreach (const QString& base, str.clazz.bases) { out << " in >> static_cast<" << base << "&>(obj);\n"; } @@ -110,6 +110,58 @@ void generateOutput (QTextStream& out, const QList& streamables) { out << " return in;\n"; out << "}\n"; + out << "bool operator==(const " << name << "& first, const " << name << "& second) {\n"; + if (str.clazz.bases.isEmpty() && str.fields.isEmpty()) { + out << " return true"; + } else { + out << " return "; + bool first = true; + foreach (const QString& base, str.clazz.bases) { + if (!first) { + out << " &&\n"; + out << " "; + } + out << "static_cast<" << base << "&>(first) == static_cast<" << base << "&>(second)"; + first = false; + } + foreach (const QString& field, str.fields) { + if (!first) { + out << " &&\n"; + out << " "; + } + out << "first." << field << " == second." << field; + first = false; + } + } + out << ";\n"; + out << "}\n"; + + out << "bool operator!=(const " << name << "& first, const " << name << "& second) {\n"; + if (str.clazz.bases.isEmpty() && str.fields.isEmpty()) { + out << " return false"; + } else { + out << " return "; + bool first = true; + foreach (const QString& base, str.clazz.bases) { + if (!first) { + out << " ||\n"; + out << " "; + } + out << "static_cast<" << base << "&>(first) != static_cast<" << base << "&>(second)"; + first = false; + } + foreach (const QString& field, str.fields) { + if (!first) { + out << " ||\n"; + out << " "; + } + out << "first." << field << " != second." << field; + first = false; + } + } + out << ";\n"; + out << "}\n"; + out << "const int " << name << "::Type = registerStreamableMetaType<" << name << ">();\n\n"; } }