diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 9783590b05..fbfbbad2de 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -248,6 +248,16 @@ QVector AvatarManager::getLocalLights() const { return _localLights; } +QVector AvatarManager::getAvatarIdentifiers() { + QReadLocker locker(&_hashLock); + return _avatarHash.keys().toVector(); +} +AvatarData* AvatarManager::getAvatar(QUuid avatarID) { + QReadLocker locker(&_hashLock); + return _avatarHash[avatarID].get(); // Non-obvious: A bogus avatarID answers your own avatar. +} + + void AvatarManager::getObjectsToDelete(VectorOfMotionStates& result) { result.clear(); result.swap(_motionStatesToDelete); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 35c18dff0b..fa0593368b 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -52,6 +52,10 @@ public: Q_INVOKABLE void setLocalLights(const QVector& localLights); Q_INVOKABLE QVector getLocalLights() const; + // Currently, your own avatar will be included as the null avatar id. + Q_INVOKABLE QVector getAvatarIdentifiers(); + Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID); + void getObjectsToDelete(VectorOfMotionStates& motionStates); void getObjectsToAdd(VectorOfMotionStates& motionStates); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5f75b7cf14..d6199ae814 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -111,6 +111,18 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetailsByRole(const QString& role); Q_INVOKABLE AnimationDetails getAnimationDetails(const QString& url); void clearJointAnimationPriorities(); + // Adds handler(animStateDictionaryIn) => animStateDictionaryOut, which will be invoked just before each animGraph state update. + // The handler will be called with an animStateDictionaryIn that has all those properties specified by the (possibly empty) + // propertiesList argument. However for debugging, if the properties argument is null, all internal animGraph state is provided. + // The animStateDictionaryOut can be a different object than animStateDictionaryIn. Any properties set in animStateDictionaryOut + // will override those of the internal animation machinery. + // The animStateDictionaryIn may be shared among multiple handlers, and thus may contain additional properties specified when + // adding one of the other handlers. While any handler may change a value in animStateDictionaryIn (or supply different values in animStateDictionaryOut) + // a handler must not remove properties from animStateDictionaryIn, nor change property values that it does not intend to change. + // It is not specified in what order multiple handlers are called. + Q_INVOKABLE QScriptValue addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList) { return _rig->addAnimationStateHandler(handler, propertiesList); } + // Removes a handler previously added by addAnimationStateHandler. + Q_INVOKABLE void removeAnimationStateHandler(QScriptValue handler) { _rig->removeAnimationStateHandler(handler); } // get/set avatar data void saveData(); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 71422f6780..4c0372c474 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -111,9 +111,6 @@ static const PalmData* getPalmWithIndex(Hand* hand, int index) { const float PALM_PRIORITY = DEFAULT_PRIORITY; // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { - if (_owningAvatar->isMyAvatar()) { - _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); - } Head* head = _owningAvatar->getHead(); if (_owningAvatar->isMyAvatar()) { MyAvatar* myAvatar = static_cast(_owningAvatar); @@ -182,6 +179,7 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateFromHandParameters(handParams, deltaTime); + _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); // evaluate AnimGraph animation and update jointStates. Model::updateRig(deltaTime, parentTransform); @@ -196,7 +194,7 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateFromEyeParameters(eyeParams); - // rebuild the jointState transform for the eyes only + // rebuild the jointState transform for the eyes only. Must be after updateRig. _rig->updateJointState(eyeParams.leftEyeJointIndex, parentTransform); _rig->updateJointState(eyeParams.rightEyeJointIndex, parentTransform); diff --git a/libraries/animation/src/AnimVariant.cpp b/libraries/animation/src/AnimVariant.cpp new file mode 100644 index 0000000000..8d320195dd --- /dev/null +++ b/libraries/animation/src/AnimVariant.cpp @@ -0,0 +1,121 @@ +// +// AnimVariantMap.cpp +// library/animation +// +// Created by Howard Stearns on 10/15/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include "AnimVariant.h" // which has AnimVariant/AnimVariantMap + +QScriptValue AnimVariantMap::animVariantMapToScriptValue(QScriptEngine* engine, const QStringList& names, bool useNames) const { + if (QThread::currentThread() != engine->thread()) { + qCWarning(animation) << "Cannot create Javacript object from non-script thread" << QThread::currentThread(); + Q_ASSERT(false); + return QScriptValue(); + } + QScriptValue target = engine->newObject(); + auto setOne = [&] (const QString& name, const AnimVariant& value) { + switch (value.getType()) { + case AnimVariant::Type::Bool: + target.setProperty(name, value.getBool()); + break; + case AnimVariant::Type::Int: + target.setProperty(name, value.getInt()); + break; + case AnimVariant::Type::Float: + target.setProperty(name, value.getFloat()); + break; + case AnimVariant::Type::String: + target.setProperty(name, value.getString()); + break; + case AnimVariant::Type::Vec3: + target.setProperty(name, vec3toScriptValue(engine, value.getVec3())); + break; + case AnimVariant::Type::Quat: + target.setProperty(name, quatToScriptValue(engine, value.getQuat())); + break; + default: + // Note that we don't do mat4 in Javascript currently, and there's not yet a reason to start now. + assert("AnimVariant::Type" == "valid"); + } + }; + if (useNames) { // copy only the requested names + for (const QString& name : names) { + auto search = _map.find(name); + if (search != _map.end()) { // scripts are allowed to request names that do not exist + setOne(name, search->second); + } + } + + } else { // copy all of them + for (auto& pair : _map) { + setOne(pair.first, pair.second); + } + } + return target; +} +void AnimVariantMap::copyVariantsFrom(const AnimVariantMap& other) { + for (auto& pair : other._map) { + _map[pair.first] = pair.second; + } +} + +void AnimVariantMap::animVariantMapFromScriptValue(const QScriptValue& source) { + if (QThread::currentThread() != source.engine()->thread()) { + qCWarning(animation) << "Cannot examine Javacript object from non-script thread" << QThread::currentThread(); + Q_ASSERT(false); + return; + } + // POTENTIAL OPTIMIZATION: cache the types we've seen. I.e, keep a dictionary mapping property names to an enumeration of types. + // Whenever we identify a new outbound type in animVariantMapToScriptValue above, or a new inbound type in the code that follows here, + // we would enter it into the dictionary. Then switch on that type here, with the code that follow being executed only if + // the type is not known. One problem with that is that there is no checking that two different script use the same name differently. + QScriptValueIterator property(source); + // Note: QScriptValueIterator iterates only over source's own properties. It does not follow the prototype chain. + while (property.hasNext()) { + property.next(); + QScriptValue value = property.value(); + if (value.isBool()) { + set(property.name(), value.toBool()); + } else if (value.isString()) { + set(property.name(), value.toString()); + } else if (value.isNumber()) { + int asInteger = value.toInt32(); + float asFloat = value.toNumber(); + if (asInteger == asFloat) { + set(property.name(), asInteger); + } else { + set(property.name(), asFloat); + } + } else { // Try to get x,y,z and possibly w + if (value.isObject()) { + QScriptValue x = value.property("x"); + if (x.isNumber()) { + QScriptValue y = value.property("y"); + if (y.isNumber()) { + QScriptValue z = value.property("z"); + if (z.isNumber()) { + QScriptValue w = value.property("w"); + if (w.isNumber()) { + set(property.name(), glm::quat(x.toNumber(), y.toNumber(), z.toNumber(), w.toNumber())); + } else { + set(property.name(), glm::vec3(x.toNumber(), y.toNumber(), z.toNumber())); + } + continue; // we got either a vector or quaternion object, so don't fall through to warning + } + } + } + } + qCWarning(animation) << "Ignoring unrecognized data" << value.toString() << "for animation property" << property.name(); + Q_ASSERT(false); + } + } +} diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index cb886cd369..0d7c657058 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -12,11 +12,14 @@ #define hifi_AnimVariant_h #include +#include #include #include #include #include +#include #include "AnimationLogging.h" +#include "StreamUtils.h" class AnimVariant { public: @@ -58,8 +61,9 @@ public: void setString(const QString& value) { assert(_type == Type::String); _stringVal = value; } bool getBool() const { assert(_type == Type::Bool); return _val.boolVal; } - int getInt() const { assert(_type == Type::Int); return _val.intVal; } - float getFloat() const { assert(_type == Type::Float); return _val.floats[0]; } + int getInt() const { assert(_type == Type::Int || _type == Type::Float); return _type == Type::Float ? (int)_val.floats[0] : _val.intVal; } + float getFloat() const { assert(_type == Type::Float || _type == Type::Int); return _type == Type::Int ? (float)_val.intVal : _val.floats[0]; } + const glm::vec3& getVec3() const { assert(_type == Type::Vec3); return *reinterpret_cast(&_val); } const glm::quat& getQuat() const { assert(_type == Type::Quat); return *reinterpret_cast(&_val); } const glm::mat4& getMat4() const { assert(_type == Type::Mat4); return *reinterpret_cast(&_val); } @@ -156,8 +160,15 @@ public: void setTrigger(const QString& key) { _triggers.insert(key); } void clearTriggers() { _triggers.clear(); } + void clearMap() { _map.clear(); } bool hasKey(const QString& key) const { return _map.find(key) != _map.end(); } + // Answer a Plain Old Javascript Object (for the given engine) all of our values set as properties. + QScriptValue animVariantMapToScriptValue(QScriptEngine* engine, const QStringList& names, bool useNames) const; + // Side-effect us with the value of object's own properties. (No inherited properties.) + void animVariantMapFromScriptValue(const QScriptValue& object); + void copyVariantsFrom(const AnimVariantMap& other); + #ifdef NDEBUG void dump() const { qCDebug(animation) << "AnimVariantMap ="; @@ -196,4 +207,8 @@ protected: std::set _triggers; }; +typedef std::function AnimVariantResultHandler; +Q_DECLARE_METATYPE(AnimVariantResultHandler); +Q_DECLARE_METATYPE(AnimVariantMap) + #endif // hifi_AnimVariant_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 3fe4c2e83e..4ddae07375 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -576,6 +577,71 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _lastPosition = worldPosition; } +// Allow script to add/remove handlers and report results, from within their thread. +QScriptValue Rig::addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList) { // called in script thread + QMutexLocker locker(&_stateMutex); + // Find a safe id, even if there are lots of many scripts add and remove handlers repeatedly. + while (!_nextStateHandlerId || _stateHandlers.contains(_nextStateHandlerId)) { // 0 is unused, and don't reuse existing after wrap. + _nextStateHandlerId++; + } + StateHandler& data = _stateHandlers[_nextStateHandlerId]; + data.function = handler; + data.useNames = propertiesList.isArray(); + if (data.useNames) { + data.propertyNames = propertiesList.toVariant().toStringList(); + } + return QScriptValue(_nextStateHandlerId); // suitable for giving to removeAnimationStateHandler +} +void Rig::removeAnimationStateHandler(QScriptValue identifier) { // called in script thread + QMutexLocker locker(&_stateMutex); + _stateHandlers.remove(identifier.isNumber() ? identifier.toInt32() : 0); // silently continues if handler not present. 0 is unused +} +void Rig::animationStateHandlerResult(int identifier, QScriptValue result) { // called synchronously from script + QMutexLocker locker(&_stateMutex); + auto found = _stateHandlers.find(identifier); + if (found == _stateHandlers.end()) { + return; // Don't use late-breaking results that got reported after the handler was removed. + } + found.value().results.animVariantMapFromScriptValue(result); // Into our own copy. +} + +void Rig::updateAnimationStateHandlers() { // called on avatar update thread (which may be main thread) + QMutexLocker locker(&_stateMutex); + // It might pay to produce just one AnimVariantMap copy here, with a union of all the requested propertyNames, + // rather than having each callAnimationStateHandler invocation make its own copy. + // However, that copying is done on the script's own time rather than ours, so even if it's less cpu, it would be more + // work on the avatar update thread (which is possibly the main thread). + for (auto data = _stateHandlers.begin(); data != _stateHandlers.end(); data++) { + // call out: + int identifier = data.key(); + StateHandler& value = data.value(); + QScriptValue& function = value.function; + auto handleResult = [this, identifier](QScriptValue result) { // called in script thread to get the result back to us. + animationStateHandlerResult(identifier, result); + }; + // invokeMethod makes a copy of the args, and copies of AnimVariantMap do copy the underlying map, so this will correctly capture + // the state of _animVars and allow continued changes to _animVars in this thread without conflict. + QMetaObject::invokeMethod(function.engine(), "callAnimationStateHandler", Qt::QueuedConnection, + Q_ARG(QScriptValue, function), + Q_ARG(AnimVariantMap, _animVars), + Q_ARG(QStringList, value.propertyNames), + Q_ARG(bool, value.useNames), + Q_ARG(AnimVariantResultHandler, handleResult)); + // It turns out that, for thread-safety reasons, ScriptEngine::callAnimationStateHandler will invoke itself if called from other + // than the script thread. Thus the above _could_ be replaced with an ordinary call, which will then trigger the same + // invokeMethod as is done explicitly above. However, the script-engine library depends on this animation library, not vice versa. + // We could create an AnimVariantCallingMixin class in shared, with an abstract virtual slot + // AnimVariantCallingMixin::callAnimationStateHandler (and move AnimVariantMap/AnimVaraintResultHandler to shared), but the + // call site here would look like this instead of the above: + // dynamic_cast(function.engine())->callAnimationStateHandler(function, ..., handleResult); + // This works (I tried it), but the result would be that we would still have same runtime type checks as the invokeMethod above + // (occuring within the ScriptEngine::callAnimationStateHandler invokeMethod trampoline), _plus_ another runtime check for the dynamic_cast. + + // gather results in (likely from an earlier update): + _animVars.copyVariantsFrom(value.results); // If multiple handlers write the same anim var, the last registgered wins. (_map preserves order). + } +} + void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { if (_enableAnimGraph) { @@ -583,6 +649,7 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { return; } + updateAnimationStateHandlers(); // evaluate the animation AnimNode::Triggers triggersOut; AnimPoseVec poses = _animNode->evaluate(_animVars, deltaTime, triggersOut); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 6e0a88d768..c23ab3d506 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -37,6 +37,8 @@ #define __hifi__Rig__ #include +#include +#include #include "JointState.h" // We might want to change this (later) to something that doesn't depend on gpu, fbx and model. -HRS @@ -51,6 +53,12 @@ typedef std::shared_ptr RigPointer; class Rig : public QObject, public std::enable_shared_from_this { public: + struct StateHandler { + AnimVariantMap results; + QStringList propertyNames; + QScriptValue function; + bool useNames; + }; struct HeadParameters { float leanSideways = 0.0f; // degrees @@ -199,10 +207,14 @@ public: AnimNode::ConstPointer getAnimNode() const { return _animNode; } AnimSkeleton::ConstPointer getAnimSkeleton() const { return _animSkeleton; } bool disableHands {false}; // should go away with rig animation (and Rig::inverseKinematics) + QScriptValue addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList); + void removeAnimationStateHandler(QScriptValue handler); + void animationStateHandlerResult(int identifier, QScriptValue result); bool getModelOffset(glm::vec3& modelOffsetOut) const; protected: + void updateAnimationStateHandlers(); void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist); void updateNeckJoint(int index, const HeadParameters& params); @@ -241,6 +253,11 @@ public: float _desiredStateAge = 0.0f; float _leftHandOverlayAlpha = 0.0f; float _rightHandOverlayAlpha = 0.0f; + +private: + QMap _stateHandlers; + int _nextStateHandlerId {0}; + QMutex _stateMutex; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index b416b58910..79df1c3bb8 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -285,6 +285,27 @@ void ScriptEngine::errorInLoadingScript(const QUrl& url) { } } +// Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of +// callAnimationStateHandler requires that the type be registered. +// These two are meaningful, if we ever do want to use them... +static QScriptValue animVarMapToScriptValue(QScriptEngine* engine, const AnimVariantMap& parameters) { + QStringList unused; + return parameters.animVariantMapToScriptValue(engine, unused, false); +} +static void animVarMapFromScriptValue(const QScriptValue& value, AnimVariantMap& parameters) { + parameters.animVariantMapFromScriptValue(value); +} +// ... while these two are not. But none of the four are ever used. +static QScriptValue resultHandlerToScriptValue(QScriptEngine* engine, const AnimVariantResultHandler& resultHandler) { + qCCritical(scriptengine) << "Attempt to marshall result handler to javascript"; + assert(false); + return QScriptValue(); +} +static void resultHandlerFromScriptValue(const QScriptValue& value, AnimVariantResultHandler& resultHandler) { + qCCritical(scriptengine) << "Attempt to marshall result handler from javascript"; + assert(false); +} + void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -343,6 +364,8 @@ void ScriptEngine::init() { registerGlobalObject("Vec3", &_vec3Library); registerGlobalObject("Uuid", &_uuidLibrary); registerGlobalObject("AnimationCache", DependencyManager::get().data()); + qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); + qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); // constants globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); @@ -357,7 +380,7 @@ void ScriptEngine::init() { void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING - qDebug() << "*** WARNING *** ScriptEngine::registerValue() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; + qDebug() << "*** WARNING *** ScriptEngine::registerValue() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; #endif QMetaObject::invokeMethod(this, "registerValue", Q_ARG(const QString&, valueName), @@ -743,6 +766,27 @@ void ScriptEngine::stop() { } } +// Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread. +void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) { + if (QThread::currentThread() != thread()) { +#ifdef THREAD_DEBUGGING + qDebug() << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; +#endif + QMetaObject::invokeMethod(this, "callAnimationStateHandler", + Q_ARG(QScriptValue, callback), + Q_ARG(AnimVariantMap, parameters), + Q_ARG(QStringList, names), + Q_ARG(bool, useNames), + Q_ARG(AnimVariantResultHandler, resultHandler)); + return; + } + QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); + QScriptValueList callingArguments; + callingArguments << javascriptParameters; + QScriptValue result = callback.call(QScriptValue(), callingArguments); + resultHandler(result); +} + void ScriptEngine::timerFired() { QTimer* callingTimer = reinterpret_cast(sender()); QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); @@ -928,9 +972,8 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin if (QThread::currentThread() != thread()) { qDebug() << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; assert(false); - return; + return ; } - if (!_registeredHandlers.contains(entityID)) { return; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 89d651930b..f6e8ac8dc2 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -142,6 +143,9 @@ public: // NOTE - this is used by the TypedArray implemetation. we need to review this for thread safety ArrayBufferClass* getArrayBufferClass() { return _arrayBufferClass; } +public slots: + void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler); + signals: void scriptLoaded(const QString& scriptFilename); void errorLoadingScript(const QString& scriptFilename);