Merge pull request #6097 from howard-stearns/expose-anim-vars

Expose animation vars to Javascript
This commit is contained in:
Anthony Thibault 2015-10-30 10:50:42 -07:00
commit fcfb44207f
10 changed files with 300 additions and 9 deletions

View file

@ -248,6 +248,16 @@ QVector<AvatarManager::LocalLight> AvatarManager::getLocalLights() const {
return _localLights;
}
QVector<QUuid> 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);

View file

@ -52,6 +52,10 @@ public:
Q_INVOKABLE void setLocalLights(const QVector<AvatarManager::LocalLight>& localLights);
Q_INVOKABLE QVector<AvatarManager::LocalLight> getLocalLights() const;
// Currently, your own avatar will be included as the null avatar id.
Q_INVOKABLE QVector<QUuid> getAvatarIdentifiers();
Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID);
void getObjectsToDelete(VectorOfMotionStates& motionStates);
void getObjectsToAdd(VectorOfMotionStates& motionStates);

View file

@ -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();

View file

@ -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<MyAvatar*>(_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);

View file

@ -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 <QScriptEngine>
#include <QScriptValueIterator>
#include <QThread>
#include <RegisteredMetaTypes.h>
#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);
}
}
}

View file

@ -12,11 +12,14 @@
#define hifi_AnimVariant_h
#include <cassert>
#include <functional>
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <map>
#include <set>
#include <QScriptValue>
#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<const glm::vec3*>(&_val); }
const glm::quat& getQuat() const { assert(_type == Type::Quat); return *reinterpret_cast<const glm::quat*>(&_val); }
const glm::mat4& getMat4() const { assert(_type == Type::Mat4); return *reinterpret_cast<const glm::mat4*>(&_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<QString> _triggers;
};
typedef std::function<void(QScriptValue)> AnimVariantResultHandler;
Q_DECLARE_METATYPE(AnimVariantResultHandler);
Q_DECLARE_METATYPE(AnimVariantMap)
#endif // hifi_AnimVariant_h

View file

@ -13,6 +13,7 @@
#include <glm/gtx/vector_angle.hpp>
#include <queue>
#include <QScriptValueIterator>
#include <NumericalConstants.h>
#include <DebugDraw.h>
@ -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<AnimVariantCallingMixin*>(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);

View file

@ -37,6 +37,8 @@
#define __hifi__Rig__
#include <QObject>
#include <QMutex>
#include <QScriptValue>
#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<Rig> RigPointer;
class Rig : public QObject, public std::enable_shared_from_this<Rig> {
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<int, StateHandler> _stateHandlers;
int _nextStateHandlerId {0};
QMutex _stateMutex;
};
#endif /* defined(__hifi__Rig__) */

View file

@ -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<AnimationCache>().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<QTimer*>(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;
}

View file

@ -21,6 +21,7 @@
#include <QtScript/QScriptEngine>
#include <AnimationCache.h>
#include <AnimVariant.h>
#include <AvatarData.h>
#include <AvatarHashMap.h>
#include <LimitedNodeList.h>
@ -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);