Merge pull request #2887 from ey6es/animenu

First pass at configurable animations for own avatar.
This commit is contained in:
Philip Rosedale 2014-05-21 11:39:16 -07:00
commit 67206af840
11 changed files with 509 additions and 2 deletions

View file

@ -217,6 +217,7 @@ public:
QNetworkAccessManager* getNetworkAccessManager() { return _networkAccessManager; }
GeometryCache* getGeometryCache() { return &_geometryCache; }
AnimationCache* getAnimationCache() { return &_animationCache; }
TextureCache* getTextureCache() { return &_textureCache; }
GlowEffect* getGlowEffect() { return &_glowEffect; }
ControllerScriptingInterface* getControllerScriptingInterface() { return &_controllerScriptingInterface; }

View file

@ -37,6 +37,7 @@
#include "Menu.h"
#include "scripting/MenuScriptingInterface.h"
#include "Util.h"
#include "ui/AnimationsDialog.h"
#include "ui/AttachmentsDialog.h"
#include "ui/InfoView.h"
#include "ui/MetavoxelEditor.h"
@ -193,6 +194,7 @@ Menu::Menu() :
QAction::PreferencesRole);
addActionToQMenuAndActionHash(editMenu, MenuOption::Attachments, 0, this, SLOT(editAttachments()));
addActionToQMenuAndActionHash(editMenu, MenuOption::Animations, 0, this, SLOT(editAnimations()));
addDisabledActionAndSeparator(editMenu, "Physics");
QObject* avatar = appInstance->getAvatar();
@ -862,6 +864,15 @@ void Menu::editAttachments() {
}
}
void Menu::editAnimations() {
if (!_animationsDialog) {
_animationsDialog = new AnimationsDialog();
_animationsDialog->show();
} else {
_animationsDialog->close();
}
}
void Menu::goToDomain(const QString newDomain) {
if (NodeList::getInstance()->getDomainHandler().getHostname() != newDomain) {
// send a node kill request, indicating to other clients that they should play the "disappeared" effect

View file

@ -64,6 +64,7 @@ struct ViewFrustumOffset {
class QSettings;
class AnimationsDialog;
class AttachmentsDialog;
class BandwidthDialog;
class LodToolsDialog;
@ -176,6 +177,7 @@ private slots:
void aboutApp();
void editPreferences();
void editAttachments();
void editAnimations();
void goToDomainDialog();
void goToLocation();
void nameLocation();
@ -260,6 +262,7 @@ private:
QAction* _loginAction;
QPointer<PreferencesDialog> _preferencesDialog;
QPointer<AttachmentsDialog> _attachmentsDialog;
QPointer<AnimationsDialog> _animationsDialog;
QAction* _chatAction;
QString _snapshotsLocation;
};
@ -269,6 +272,7 @@ namespace MenuOption {
const QString AlignForearmsWithWrists = "Align Forearms with Wrists";
const QString AlternateIK = "Alternate IK";
const QString AmbientOcclusion = "Ambient Occlusion";
const QString Animations = "Animations...";
const QString Atmosphere = "Atmosphere";
const QString Attachments = "Attachments...";
const QString AudioNoiseReduction = "Audio Noise Reduction";

View file

@ -428,6 +428,46 @@ void MyAvatar::setGravity(const glm::vec3& gravity) {
}
}
AnimationHandlePointer MyAvatar::addAnimationHandle() {
AnimationHandlePointer handle = _skeletonModel.createAnimationHandle();
handle->setLoop(true);
handle->start();
_animationHandles.append(handle);
return handle;
}
void MyAvatar::removeAnimationHandle(const AnimationHandlePointer& handle) {
handle->stop();
_animationHandles.removeOne(handle);
}
void MyAvatar::startAnimation(const QString& url, float fps, float priority, bool loop) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "startAnimation", Q_ARG(const QString&, url),
Q_ARG(float, fps), Q_ARG(float, priority), Q_ARG(bool, loop));
return;
}
AnimationHandlePointer handle = _skeletonModel.createAnimationHandle();
handle->setURL(url);
handle->setFPS(fps);
handle->setPriority(priority);
handle->setLoop(loop);
handle->start();
}
void MyAvatar::stopAnimation(const QString& url) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "stopAnimation", Q_ARG(const QString&, url));
return;
}
foreach (const AnimationHandlePointer& handle, _skeletonModel.getRunningAnimations()) {
if (handle->getURL() == url) {
handle->stop();
return;
}
}
}
void MyAvatar::saveData(QSettings* settings) {
settings->beginGroup("Avatar");
@ -466,6 +506,16 @@ void MyAvatar::saveData(QSettings* settings) {
}
settings->endArray();
settings->beginWriteArray("animationHandles");
for (int i = 0; i < _animationHandles.size(); i++) {
settings->setArrayIndex(i);
const AnimationHandlePointer& pointer = _animationHandles.at(i);
settings->setValue("url", pointer->getURL());
settings->setValue("fps", pointer->getFPS());
settings->setValue("priority", pointer->getPriority());
}
settings->endArray();
settings->setValue("displayName", _displayName);
settings->endGroup();
@ -516,6 +566,22 @@ void MyAvatar::loadData(QSettings* settings) {
settings->endArray();
setAttachmentData(attachmentData);
int animationCount = settings->beginReadArray("animationHandles");
while (_animationHandles.size() > animationCount) {
_animationHandles.takeLast()->stop();
}
while (_animationHandles.size() < animationCount) {
addAnimationHandle();
}
for (int i = 0; i < animationCount; i++) {
settings->setArrayIndex(i);
const AnimationHandlePointer& handle = _animationHandles.at(i);
handle->setURL(settings->value("url").toUrl());
handle->setFPS(loadSetting(settings, "fps", 30.0f));
handle->setPriority(loadSetting(settings, "priority", 1.0f));
}
settings->endArray();
setDisplayName(settings->value("displayName").toString());
settings->endGroup();
@ -1546,3 +1612,4 @@ void MyAvatar::applyCollision(const glm::vec3& contactPoint, const glm::vec3& pe
getHead()->addLeanDeltas(sideways, forward);
}
}

View file

@ -62,6 +62,17 @@ public:
glm::vec3 getUprightHeadPosition() const;
bool getShouldRenderLocally() const { return _shouldRender; }
const QList<AnimationHandlePointer>& getAnimationHandles() const { return _animationHandles; }
AnimationHandlePointer addAnimationHandle();
void removeAnimationHandle(const AnimationHandlePointer& handle);
/// Allows scripts to run animations.
Q_INVOKABLE void startAnimation(const QString& url, float fps = 30.0f,
float priority = 1.0f, bool loop = false);
/// Stops an animation as identified by a URL.
Q_INVOKABLE void stopAnimation(const QString& url);
// get/set avatar data
void saveData(QSettings* settings);
void loadData(QSettings* settings);
@ -151,6 +162,8 @@ private:
bool _billboardValid;
float _oculusYawOffset;
QList<AnimationHandlePointer> _animationHandles;
// private methods
void updateOrientation(float deltaTime);
void updateMotorFromKeyboard(float deltaTime, bool walking);

View file

@ -404,6 +404,22 @@ QSharedPointer<NetworkGeometry> NetworkGeometry::getLODOrFallback(float distance
return lod;
}
uint qHash(const QWeakPointer<Animation>& animation, uint seed = 0) {
return qHash(animation.data(), seed);
}
QVector<int> NetworkGeometry::getJointMappings(const AnimationPointer& animation) {
QVector<int> mappings = _jointMappings.value(animation);
if (mappings.isEmpty() && isLoaded() && animation && animation->isLoaded()) {
const FBXGeometry& animationGeometry = animation->getGeometry();
for (int i = 0; i < animationGeometry.joints.size(); i++) {
mappings.append(_geometry.jointIndices.value(animationGeometry.joints.at(i).name) - 1);
}
_jointMappings.insert(animation, mappings);
}
return mappings;
}
void NetworkGeometry::setLoadPriority(const QPointer<QObject>& owner, float priority) {
Resource::setLoadPriority(owner, priority);

View file

@ -22,6 +22,8 @@
#include <FBXReader.h>
#include <AnimationCache.h>
class Model;
class NetworkGeometry;
class NetworkMesh;
@ -90,6 +92,8 @@ public:
const FBXGeometry& getFBXGeometry() const { return _geometry; }
const QVector<NetworkMesh>& getMeshes() const { return _meshes; }
QVector<int> getJointMappings(const AnimationPointer& animation);
virtual void setLoadPriority(const QPointer<QObject>& owner, float priority);
virtual void setLoadPriorities(const QHash<QPointer<QObject>, float>& priorities);
virtual void clearLoadPriority(const QPointer<QObject>& owner);
@ -117,6 +121,8 @@ private:
QVector<NetworkMesh> _meshes;
QWeakPointer<NetworkGeometry> _lodParent;
QHash<QWeakPointer<Animation>, QVector<int> > _jointMappings;
};
/// The state associated with a single mesh part.

View file

@ -118,6 +118,7 @@ QVector<Model::JointState> Model::createJointStates(const FBXGeometry& geometry)
JointState state;
state.translation = joint.translation;
state.rotation = joint.rotation;
state.animationDisabled = false;
jointStates.append(state);
}
@ -601,6 +602,16 @@ QStringList Model::getJointNames() const {
return isActive() ? _geometry->getFBXGeometry().getJointNames() : QStringList();
}
uint qHash(const WeakAnimationHandlePointer& handle, uint seed) {
return qHash(handle.data(), seed);
}
AnimationHandlePointer Model::createAnimationHandle() {
AnimationHandlePointer handle(new AnimationHandle(this));
handle->_self = handle;
_animationHandles.insert(handle);
return handle;
}
void Model::clearShapes() {
for (int i = 0; i < _jointShapes.size(); ++i) {
@ -1013,6 +1024,11 @@ void Model::simulate(float deltaTime, bool fullUpdate) {
}
void Model::simulateInternal(float deltaTime) {
// update animations
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
handle->simulate(deltaTime);
}
// NOTE: this is a recursive call that walks all attachments, and their attachments
// update the world space transforms for all joints
for (int i = 0; i < _jointStates.size(); i++) {
@ -1020,9 +1036,8 @@ void Model::simulateInternal(float deltaTime) {
}
_shapesAreDirty = true;
const FBXGeometry& geometry = _geometry->getFBXGeometry();
// update the attachment transforms and simulate them
const FBXGeometry& geometry = _geometry->getFBXGeometry();
for (int i = 0; i < _attachments.size(); i++) {
const FBXAttachment& attachment = geometry.attachments.at(i);
Model* model = _attachments.at(i);
@ -1193,6 +1208,7 @@ bool Model::setJointRotation(int jointIndex, const glm::quat& rotation, bool fro
state.rotation = state.rotation * glm::inverse(state.combinedRotation) * rotation *
glm::inverse(fromBind ? _geometry->getFBXGeometry().joints.at(jointIndex).inverseBindRotation :
_geometry->getFBXGeometry().joints.at(jointIndex).inverseDefaultRotation);
state.animationDisabled = true;
return true;
}
@ -1225,6 +1241,7 @@ bool Model::restoreJointPosition(int jointIndex, float percent) {
const FBXJoint& joint = geometry.joints.at(index);
state.rotation = safeMix(state.rotation, joint.rotation, percent);
state.translation = glm::mix(state.translation, joint.translation, percent);
state.animationDisabled = false;
}
return true;
}
@ -1258,6 +1275,7 @@ void Model::applyRotationDelta(int jointIndex, const glm::quat& delta, bool cons
glm::quat newRotation = glm::quat(glm::clamp(eulers, joint.rotationMin, joint.rotationMax));
state.combinedRotation = state.combinedRotation * glm::inverse(state.rotation) * newRotation;
state.rotation = newRotation;
state.animationDisabled = true;
}
const int BALL_SUBDIVISIONS = 10;
@ -1433,6 +1451,16 @@ void Model::deleteGeometry() {
_meshStates.clear();
clearShapes();
for (QSet<WeakAnimationHandlePointer>::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) {
AnimationHandlePointer handle = it->toStrongRef();
if (handle) {
handle->_jointMappings.clear();
it++;
} else {
it = _animationHandles.erase(it);
}
}
if (_geometry) {
_geometry->clearLoadPriority(this);
}
@ -1628,3 +1656,103 @@ void Model::renderMeshes(float alpha, RenderMode mode, bool translucent) {
activeProgram->release();
}
}
void AnimationHandle::setURL(const QUrl& url) {
if (_url != url) {
_animation = Application::getInstance()->getAnimationCache()->getAnimation(_url = url);
_jointMappings.clear();
}
}
static void insertSorted(QList<AnimationHandlePointer>& handles, const AnimationHandlePointer& handle) {
for (QList<AnimationHandlePointer>::iterator it = handles.begin(); it != handles.end(); it++) {
if (handle->getPriority() < (*it)->getPriority()) {
handles.insert(it, handle);
return;
}
}
handles.append(handle);
}
void AnimationHandle::setPriority(float priority) {
if (_priority != priority) {
_priority = priority;
if (_running) {
_model->_runningAnimations.removeOne(_self);
insertSorted(_model->_runningAnimations, _self);
}
}
}
void AnimationHandle::setRunning(bool running) {
if ((_running = running)) {
if (!_model->_runningAnimations.contains(_self)) {
insertSorted(_model->_runningAnimations, _self);
}
_frameIndex = 0.0f;
} else {
_model->_runningAnimations.removeOne(_self);
}
}
AnimationHandle::AnimationHandle(Model* model) :
QObject(model),
_model(model),
_fps(30.0f),
_priority(1.0f),
_loop(false),
_running(false) {
}
void AnimationHandle::simulate(float deltaTime) {
_frameIndex += deltaTime * _fps;
// update the joint mappings if necessary/possible
if (_jointMappings.isEmpty()) {
if (_model->isActive()) {
_jointMappings = _model->getGeometry()->getJointMappings(_animation);
}
if (_jointMappings.isEmpty()) {
return;
}
}
const FBXGeometry& animationGeometry = _animation->getGeometry();
if (animationGeometry.animationFrames.isEmpty()) {
stop();
return;
}
int ceilFrameIndex = (int)glm::ceil(_frameIndex);
if (!_loop && ceilFrameIndex >= animationGeometry.animationFrames.size()) {
// passed the end; apply the last frame
const FBXAnimationFrame& frame = animationGeometry.animationFrames.last();
for (int i = 0; i < _jointMappings.size(); i++) {
int mapping = _jointMappings.at(i);
if (mapping != -1) {
Model::JointState& state = _model->_jointStates[mapping];
if (!state.animationDisabled) {
state.rotation = frame.rotations.at(i);
}
}
}
stop();
return;
}
// blend between the closest two frames
const FBXAnimationFrame& ceilFrame = animationGeometry.animationFrames.at(
ceilFrameIndex % animationGeometry.animationFrames.size());
const FBXAnimationFrame& floorFrame = animationGeometry.animationFrames.at(
(int)glm::floor(_frameIndex) % animationGeometry.animationFrames.size());
float frameFraction = glm::fract(_frameIndex);
for (int i = 0; i < _jointMappings.size(); i++) {
int mapping = _jointMappings.at(i);
if (mapping != -1) {
Model::JointState& state = _model->_jointStates[mapping];
if (!state.animationDisabled) {
state.rotation = safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction);
}
}
}
}

View file

@ -12,18 +12,25 @@
#ifndef hifi_Model_h
#define hifi_Model_h
#include <QBitArray>
#include <QObject>
#include <QUrl>
#include <CapsuleShape.h>
#include <AnimationCache.h>
#include "GeometryCache.h"
#include "InterfaceConfig.h"
#include "ProgramObject.h"
#include "TextureCache.h"
class AnimationHandle;
class Shape;
typedef QSharedPointer<AnimationHandle> AnimationHandlePointer;
typedef QWeakPointer<AnimationHandle> WeakAnimationHandlePointer;
/// A generic 3D model displaying geometry loaded from a URL.
class Model : public QObject {
Q_OBJECT
@ -185,6 +192,10 @@ public:
QStringList getJointNames() const;
AnimationHandlePointer createAnimationHandle();
const QList<AnimationHandlePointer>& getRunningAnimations() const { return _runningAnimations; }
void clearShapes();
void rebuildShapes();
void resetShapePositions();
@ -243,6 +254,7 @@ protected:
glm::quat rotation; // rotation relative to parent
glm::mat4 transform; // rotation to world frame + translation in model frame
glm::quat combinedRotation; // rotation from joint local to world frame
bool animationDisabled; // if true, animations do not affect this joint
};
bool _shapesAreDirty;
@ -299,6 +311,8 @@ protected:
private:
friend class AnimationHandle;
void applyNextGeometry();
void deleteGeometry();
void renderMeshes(float alpha, RenderMode mode, bool translucent);
@ -322,6 +336,10 @@ private:
QVector<Model*> _attachments;
QSet<WeakAnimationHandlePointer> _animationHandles;
QList<AnimationHandlePointer> _runningAnimations;
static ProgramObject _program;
static ProgramObject _normalMapProgram;
static ProgramObject _specularMapProgram;
@ -357,4 +375,48 @@ Q_DECLARE_METATYPE(QPointer<Model>)
Q_DECLARE_METATYPE(QWeakPointer<NetworkGeometry>)
Q_DECLARE_METATYPE(QVector<glm::vec3>)
/// Represents a handle to a model animation.
class AnimationHandle : public QObject {
Q_OBJECT
public:
void setURL(const QUrl& url);
const QUrl& getURL() const { return _url; }
void setFPS(float fps) { _fps = fps; }
float getFPS() const { return _fps; }
void setPriority(float priority);
float getPriority() const { return _priority; }
void setLoop(bool loop) { _loop = loop; }
bool getLoop() const { return _loop; }
void setRunning(bool running);
bool isRunning() const { return _running; }
void start() { setRunning(true); }
void stop() { setRunning(false); }
private:
friend class Model;
AnimationHandle(Model* model);
void simulate(float deltaTime);
Model* _model;
WeakAnimationHandlePointer _self;
AnimationPointer _animation;
QUrl _url;
float _fps;
float _priority;
bool _loop;
bool _running;
QVector<int> _jointMappings;
float _frameIndex;
};
#endif // hifi_Model_h

View file

@ -0,0 +1,130 @@
//
// AnimationsDialog.cpp
// interface/src/ui
//
// Created by Andrzej Kapolka on 5/19/14.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#include "AnimationsDialog.h"
#include "Application.h"
AnimationsDialog::AnimationsDialog() :
QDialog(Application::getInstance()->getWindow()) {
setWindowTitle("Edit Animations");
setAttribute(Qt::WA_DeleteOnClose);
QVBoxLayout* layout = new QVBoxLayout();
setLayout(layout);
QScrollArea* area = new QScrollArea();
layout->addWidget(area);
area->setWidgetResizable(true);
QWidget* container = new QWidget();
container->setLayout(_animations = new QVBoxLayout());
container->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
area->setWidget(container);
_animations->addStretch(1);
foreach (const AnimationHandlePointer& handle, Application::getInstance()->getAvatar()->getAnimationHandles()) {
_animations->insertWidget(_animations->count() - 1, new AnimationPanel(this, handle));
}
QPushButton* newAnimation = new QPushButton("New Animation");
connect(newAnimation, SIGNAL(clicked(bool)), SLOT(addAnimation()));
layout->addWidget(newAnimation);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok);
layout->addWidget(buttons);
connect(buttons, SIGNAL(accepted()), SLOT(deleteLater()));
_ok = buttons->button(QDialogButtonBox::Ok);
setMinimumSize(600, 600);
}
void AnimationsDialog::setVisible(bool visible) {
QDialog::setVisible(visible);
// un-default the OK button
if (visible) {
_ok->setDefault(false);
}
}
void AnimationsDialog::addAnimation() {
_animations->insertWidget(_animations->count() - 1, new AnimationPanel(
this, Application::getInstance()->getAvatar()->addAnimationHandle()));
}
AnimationPanel::AnimationPanel(AnimationsDialog* dialog, const AnimationHandlePointer& handle) :
_dialog(dialog),
_handle(handle),
_applying(false) {
setFrameStyle(QFrame::StyledPanel);
QFormLayout* layout = new QFormLayout();
layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
setLayout(layout);
QHBoxLayout* urlBox = new QHBoxLayout();
layout->addRow("URL:", urlBox);
urlBox->addWidget(_url = new QLineEdit(handle->getURL().toString()), 1);
connect(_url, SIGNAL(returnPressed()), SLOT(updateHandle()));
QPushButton* chooseURL = new QPushButton("Choose");
urlBox->addWidget(chooseURL);
connect(chooseURL, SIGNAL(clicked(bool)), SLOT(chooseURL()));
layout->addRow("FPS:", _fps = new QDoubleSpinBox());
_fps->setSingleStep(0.01);
_fps->setMaximum(FLT_MAX);
_fps->setValue(handle->getFPS());
connect(_fps, SIGNAL(valueChanged(double)), SLOT(updateHandle()));
layout->addRow("Priority:", _priority = new QDoubleSpinBox());
_priority->setSingleStep(0.01);
_priority->setMinimum(-FLT_MAX);
_priority->setMaximum(FLT_MAX);
_priority->setValue(handle->getPriority());
connect(_priority, SIGNAL(valueChanged(double)), SLOT(updateHandle()));
QPushButton* remove = new QPushButton("Delete");
layout->addRow(remove);
connect(remove, SIGNAL(clicked(bool)), SLOT(removeHandle()));
}
void AnimationPanel::chooseURL() {
QString directory = Application::getInstance()->lockSettings()->value("animation_directory").toString();
Application::getInstance()->unlockSettings();
QString filename = QFileDialog::getOpenFileName(this, "Choose Animation", directory, "Animation files (*.fbx)");
if (filename.isEmpty()) {
return;
}
Application::getInstance()->lockSettings()->setValue("animation_directory", QFileInfo(filename).path());
Application::getInstance()->unlockSettings();
_url->setText(QUrl::fromLocalFile(filename).toString());
emit _url->returnPressed();
}
void AnimationPanel::updateHandle() {
_handle->setURL(_url->text());
_handle->setFPS(_fps->value());
_handle->setPriority(_priority->value());
}
void AnimationPanel::removeHandle() {
Application::getInstance()->getAvatar()->removeAnimationHandle(_handle);
deleteLater();
}

View file

@ -0,0 +1,69 @@
//
// AnimationsDialog.h
// interface/src/ui
//
// Created by Andrzej Kapolka on 5/19/14.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_AnimationsDialog_h
#define hifi_AnimationsDialog_h
#include <QDialog>
#include <QFrame>
#include "avatar/MyAvatar.h"
class QDoubleSpinner;
class QLineEdit;
class QPushButton;
class QVBoxLayout;
/// Allows users to edit the avatar animations.
class AnimationsDialog : public QDialog {
Q_OBJECT
public:
AnimationsDialog();
virtual void setVisible(bool visible);
private slots:
void addAnimation();
private:
QVBoxLayout* _animations;
QPushButton* _ok;
};
/// A panel controlling a single animation.
class AnimationPanel : public QFrame {
Q_OBJECT
public:
AnimationPanel(AnimationsDialog* dialog, const AnimationHandlePointer& handle);
private slots:
void chooseURL();
void updateHandle();
void removeHandle();
private:
AnimationsDialog* _dialog;
AnimationHandlePointer _handle;
QLineEdit* _url;
QDoubleSpinBox* _fps;
QDoubleSpinBox* _priority;
bool _applying;
};
#endif // hifi_AnimationsDialog_h