From 340096d45735da353de600f082ba8cd906eb08cb Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Sun, 1 Nov 2015 15:16:00 -0800 Subject: [PATCH 01/43] Initial version of AnimExpression class with minimal tokenizer --- libraries/animation/src/AnimExpression.cpp | 228 +++++++++++++++++++++ libraries/animation/src/AnimExpression.h | 78 +++++++ tests/animation/src/AnimTests.cpp | 53 ++++- tests/animation/src/AnimTests.h | 1 + 4 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 libraries/animation/src/AnimExpression.cpp create mode 100644 libraries/animation/src/AnimExpression.h diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp new file mode 100644 index 0000000000..f2ec0426be --- /dev/null +++ b/libraries/animation/src/AnimExpression.cpp @@ -0,0 +1,228 @@ +// +// AnimExpression.cpp +// +// Created by Anthony J. Thibault on 11/1/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 "AnimExpression.h" +#include "AnimationLogging.h" + + +#ifndef NDEBUG +void AnimExpression::Token::dump() { + switch (type) { + case End: + qCDebug(animation) << " End"; + break; + case Identifier: + qCDebug(animation) << " Identifer =" << strVal; + break; + case LiteralInt: + qCDebug(animation) << " LiteralInt =" << intVal; + break; + case LiteralFloat: + qCDebug(animation) << " LiteralFloat" << floatVal; + break; + case LiteralVec3: + qCDebug(animation) << " LiteralVec3" << vec3Val; + break; + case LiteralVec4: + qCDebug(animation) << " LiteralVec4" << vec4Val; + break; + case LiteralQuat: + qCDebug(animation) << " LiteralQuat" << quatVal; + break; + case And: + qCDebug(animation) << " And"; + break; + case Or: + qCDebug(animation) << " Or"; + break; + case GreaterThan: + qCDebug(animation) << " GreaterThan"; + break; + case GreaterThanEqual: + qCDebug(animation) << " GreaterThanEqual"; + break; + case LessThan: + qCDebug(animation) << " LessThan"; + break; + case LessThanEqual: + qCDebug(animation) << " LessThanEqual"; + break; + case Equal: + qCDebug(animation) << " Equal"; + break; + case NotEqual: + qCDebug(animation) << " NotEqual"; + break; + case LeftParen: + qCDebug(animation) << " LeftParen"; + break; + case RightParen: + qCDebug(animation) << " RightParen"; + break; + case Not: + qCDebug(animation) << " Not"; + break; + case Minus: + qCDebug(animation) << " Minus"; + break; + case Plus: + qCDebug(animation) << " Plus"; + break; + case Multiply: + qCDebug(animation) << " Multiply"; + break; + case Modulus: + qCDebug(animation) << " Modulus"; + break; + case Error: + qCDebug(animation) << " Error"; + break; + } +} +#endif + +AnimExpression::AnimExpression(const QString& str) : + _expression(str) { + parseExpression(_expression); +} + +bool AnimExpression::parseExpression(const QString& str) { + Token token(Token::End); + auto iter = str.begin(); + do { + token = consumeToken(str, iter); + switch(token.type) { + case Token::Error: + case Token::End: + return false; + } + } while(iter != str.end()); +} + +AnimExpression::Token AnimExpression::consumeToken(const QString& str, QString::const_iterator& iter) const { + while (iter != str.end()) { + if (iter->isSpace()) { + ++iter; + } else if (iter->isLetter()) { + return consumeIdentifier(str, iter); + } else if (iter->isDigit()) { + return consumeNumber(str, iter); + } else { + switch (iter->unicode()) { + case '&': return consumeAnd(str, iter); + case '|': return consumeOr(str, iter); + case '>': return consumeGreaterThan(str, iter); + case '<': return consumeLessThan(str, iter); + case '(': ++iter; return Token(Token::LeftParen); + case ')': ++iter; return Token(Token::RightParen); + case '!': return consumeNot(str, iter); + case '-': ++iter; return Token(Token::Minus); + case '+': ++iter; return Token(Token::Plus); + case '*': ++iter; return Token(Token::Multiply); + case '%': ++iter; return Token(Token::Modulus); + default: + qCCritical(animation) << "AnimExpression: unexpected char" << *iter << "at index " << (int)(iter - str.begin()); + return Token(Token::Error); + } + } + } + return Token(Token::End); +} + +AnimExpression::Token AnimExpression::consumeIdentifier(const QString& str, QString::const_iterator& iter) const { + assert(iter != str.end()); + assert(iter->isLetter()); + auto begin = iter; + while (iter->isLetter() && iter != str.end()) { + ++iter; + } + int pos = (int)(begin - str.begin()); + int len = (int)(iter - begin); + return Token(QStringRef(const_cast(&str), pos, len)); +} + +AnimExpression::Token AnimExpression::consumeNumber(const QString& str, QString::const_iterator& iter) const { + assert(iter != str.end()); + assert(iter->isDigit()); + auto begin = iter; + while (iter->isDigit() && iter != str.end()) { + ++iter; + } + int pos = (int)(begin - str.begin()); + int len = (int)(iter - begin); + QString sub = QStringRef(const_cast(&str), pos, len).toString(); + return Token(sub.toInt()); +} + +AnimExpression::Token AnimExpression::consumeAnd(const QString& str, QString::const_iterator& iter) const { + assert(iter != str.end()); + assert(iter->unicode() == '&'); + iter++; + if (iter->unicode() == '&') { + iter++; + return Token(Token::And); + } else { + qCCritical(animation) << "AnimExpression: unexpected char" << *iter << "at index " << (int)(iter - str.begin()); + return Token(Token::Error); + } +} + +AnimExpression::Token AnimExpression::consumeOr(const QString& str, QString::const_iterator& iter) const { + assert(iter != str.end()); + assert(iter->unicode() == '|'); + iter++; + if (iter->unicode() == '|') { + iter++; + return Token(Token::Or); + } else { + qCCritical(animation) << "AnimExpression: unexpected char" << *iter << "at index " << (int)(iter - str.begin()); + return Token(Token::Error); + } +} + +AnimExpression::Token AnimExpression::consumeGreaterThan(const QString& str, QString::const_iterator& iter) const { + assert(iter != str.end()); + assert(iter->unicode() == '>'); + iter++; + if (iter->unicode() == '=') { + iter++; + return Token(Token::GreaterThanEqual); + } else { + return Token(Token::GreaterThan); + } +} + +AnimExpression::Token AnimExpression::consumeLessThan(const QString& str, QString::const_iterator& iter) const { + assert(iter != str.end()); + assert(iter->unicode() == '<'); + iter++; + if (iter->unicode() == '=') { + iter++; + return Token(Token::LessThanEqual); + } else { + return Token(Token::LessThan); + } +} + +AnimExpression::Token AnimExpression::consumeNot(const QString& str, QString::const_iterator& iter) const { + assert(iter != str.end()); + assert(iter->unicode() == '!'); + iter++; + if (iter->unicode() == '=') { + iter++; + return Token(Token::NotEqual); + } else { + return Token(Token::Not); + } +} + diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h new file mode 100644 index 0000000000..a69b13c286 --- /dev/null +++ b/libraries/animation/src/AnimExpression.h @@ -0,0 +1,78 @@ +// +// AnimExpression.h +// +// Created by Anthony J. Thibault on 11/1/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 +// + +#ifndef hifi_AnimExpression +#define hifi_AnimExpression + +#include +#include +#include +#include + +class AnimExpression { +public: + friend class AnimTests; + AnimExpression(const QString& str); +protected: + struct Token { + enum Type { + End = 0, + Identifier, + LiteralInt, + LiteralFloat, + LiteralVec3, + LiteralVec4, + LiteralQuat, + And, + Or, + GreaterThan, + GreaterThanEqual, + LessThan, + LessThanEqual, + Equal, + NotEqual, + LeftParen, + RightParen, + Not, + Minus, + Plus, + Multiply, + Modulus, + Error + }; + Token(Type type) : type(type) {} + Token(const QStringRef& strRef) : type(Type::Identifier), strVal(strRef.toString()) {} + Token(int val) : type(Type::LiteralInt), intVal(val) {} + Type type = End; + QString strVal; + int intVal; + float floatVal; + glm::vec3 vec3Val; + glm::vec4 vec4Val; + glm::quat quatVal; +#ifndef NDEBUG + void dump(); +#endif + }; + bool parseExpression(const QString& str); + Token consumeToken(const QString& str, QString::const_iterator& iter) const; + Token consumeIdentifier(const QString& str, QString::const_iterator& iter) const; + Token consumeNumber(const QString& str, QString::const_iterator& iter) const; + Token consumeAnd(const QString& str, QString::const_iterator& iter) const; + Token consumeOr(const QString& str, QString::const_iterator& iter) const; + Token consumeGreaterThan(const QString& str, QString::const_iterator& iter) const; + Token consumeLessThan(const QString& str, QString::const_iterator& iter) const; + Token consumeNot(const QString& str, QString::const_iterator& iter) const; + + QString _expression; +}; + +#endif + diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 1b5bb4739a..08f13b7ec7 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -8,12 +8,13 @@ // #include "AnimTests.h" -#include "AnimNodeLoader.h" -#include "AnimClip.h" -#include "AnimBlendLinear.h" -#include "AnimationLogging.h" -#include "AnimVariant.h" -#include "AnimUtil.h" +#include +#include +#include +#include +#include +#include +#include #include <../QTestExtensions.h> @@ -322,4 +323,44 @@ void AnimTests::testAccumulateTimeWithParameters(float startFrame, float endFram QVERIFY(resultFrame == startFrame + 0.5f); QVERIFY(!triggers.empty() && triggers[0] == "testNodeOnLoop"); triggers.clear(); + +void AnimTests::testTokenizer() { + QString str = "(10 + x) >= 20 && (y != !z)"; + AnimExpression e(""); + auto iter = str.cbegin(); + AnimExpression::Token token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::LeftParen); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::LiteralInt); + QVERIFY(token.intVal == 10); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::Plus); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::Identifier); + QVERIFY(token.strVal == "x"); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::RightParen); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::GreaterThanEqual); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::LiteralInt); + QVERIFY(token.intVal == 20); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::And); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::LeftParen); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::Identifier); + QVERIFY(token.strVal == "y"); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::NotEqual); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::Not); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::Identifier); + QVERIFY(token.strVal == "z"); + token = e.consumeToken(str, iter); + QVERIFY(token.type == AnimExpression::Token::RightParen); + token = e.consumeToken(str, iter); } + diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index 7bd05369c7..bfea4eb086 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -26,6 +26,7 @@ private slots: void testLoader(); void testVariant(); void testAccumulateTime(); + void testTokenizer(); }; #endif // hifi_AnimTests_h From 7f0fc4f6eb5bc9a4f28f55c6641c516e32cddd81 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Sun, 1 Nov 2015 15:51:57 -0800 Subject: [PATCH 02/43] Added limited floating point support --- libraries/animation/src/AnimExpression.cpp | 38 +++++++++++++++++++++- libraries/animation/src/AnimExpression.h | 1 + tests/animation/src/AnimTests.cpp | 6 ++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index f2ec0426be..14b3e9da87 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -151,6 +151,21 @@ AnimExpression::Token AnimExpression::consumeIdentifier(const QString& str, QStr return Token(QStringRef(const_cast(&str), pos, len)); } +// TODO: not very efficient or accruate, but it's close enough for now. +static float computeFractionalPart(int fractionalPart) +{ + float frac = (float)fractionalPart; + while (fractionalPart) { + fractionalPart /= 10; + frac /= 10.0f; + } + return frac; +} + +static float computeFloat(int whole, int fraction) { + return (float)whole + computeFractionalPart(fraction); +} + AnimExpression::Token AnimExpression::consumeNumber(const QString& str, QString::const_iterator& iter) const { assert(iter != str.end()); assert(iter->isDigit()); @@ -158,10 +173,31 @@ AnimExpression::Token AnimExpression::consumeNumber(const QString& str, QString: while (iter->isDigit() && iter != str.end()) { ++iter; } + + // parse whole integer part int pos = (int)(begin - str.begin()); int len = (int)(iter - begin); QString sub = QStringRef(const_cast(&str), pos, len).toString(); - return Token(sub.toInt()); + int whole = sub.toInt(); + + // parse optional fractional part + if (iter->unicode() == '.') { + iter++; + auto begin = iter; + while (iter->isDigit() && iter != str.end()) { + ++iter; + } + + int pos = (int)(begin - str.begin()); + int len = (int)(iter - begin); + QString sub = QStringRef(const_cast(&str), pos, len).toString(); + int fraction = sub.toInt(); + + return Token(computeFloat(whole, fraction)); + + } else { + return Token(whole); + } } AnimExpression::Token AnimExpression::consumeAnd(const QString& str, QString::const_iterator& iter) const { diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index a69b13c286..25e1803721 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -50,6 +50,7 @@ protected: Token(Type type) : type(type) {} Token(const QStringRef& strRef) : type(Type::Identifier), strVal(strRef.toString()) {} Token(int val) : type(Type::LiteralInt), intVal(val) {} + Token(float val) : type(Type::LiteralFloat), floatVal(val) {} Type type = End; QString strVal; int intVal; diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 08f13b7ec7..5b6806ec09 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -325,7 +325,7 @@ void AnimTests::testAccumulateTimeWithParameters(float startFrame, float endFram triggers.clear(); void AnimTests::testTokenizer() { - QString str = "(10 + x) >= 20 && (y != !z)"; + QString str = "(10 + x) >= 20.1 && (y != !z)"; AnimExpression e(""); auto iter = str.cbegin(); AnimExpression::Token token = e.consumeToken(str, iter); @@ -343,8 +343,8 @@ void AnimTests::testTokenizer() { token = e.consumeToken(str, iter); QVERIFY(token.type == AnimExpression::Token::GreaterThanEqual); token = e.consumeToken(str, iter); - QVERIFY(token.type == AnimExpression::Token::LiteralInt); - QVERIFY(token.intVal == 20); + QVERIFY(token.type == AnimExpression::Token::LiteralFloat); + QVERIFY(fabsf(token.floatVal - 20.1f) < 0.0001f); token = e.consumeToken(str, iter); QVERIFY(token.type == AnimExpression::Token::And); token = e.consumeToken(str, iter); From 4394083138744a47fd8f2a14c46e1fd008b38772 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Sun, 1 Nov 2015 15:57:20 -0800 Subject: [PATCH 03/43] Added comma token --- libraries/animation/src/AnimExpression.cpp | 79 +--------------------- libraries/animation/src/AnimExpression.h | 4 +- 2 files changed, 2 insertions(+), 81 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index 14b3e9da87..fe056be81d 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -14,83 +14,6 @@ #include "AnimExpression.h" #include "AnimationLogging.h" - -#ifndef NDEBUG -void AnimExpression::Token::dump() { - switch (type) { - case End: - qCDebug(animation) << " End"; - break; - case Identifier: - qCDebug(animation) << " Identifer =" << strVal; - break; - case LiteralInt: - qCDebug(animation) << " LiteralInt =" << intVal; - break; - case LiteralFloat: - qCDebug(animation) << " LiteralFloat" << floatVal; - break; - case LiteralVec3: - qCDebug(animation) << " LiteralVec3" << vec3Val; - break; - case LiteralVec4: - qCDebug(animation) << " LiteralVec4" << vec4Val; - break; - case LiteralQuat: - qCDebug(animation) << " LiteralQuat" << quatVal; - break; - case And: - qCDebug(animation) << " And"; - break; - case Or: - qCDebug(animation) << " Or"; - break; - case GreaterThan: - qCDebug(animation) << " GreaterThan"; - break; - case GreaterThanEqual: - qCDebug(animation) << " GreaterThanEqual"; - break; - case LessThan: - qCDebug(animation) << " LessThan"; - break; - case LessThanEqual: - qCDebug(animation) << " LessThanEqual"; - break; - case Equal: - qCDebug(animation) << " Equal"; - break; - case NotEqual: - qCDebug(animation) << " NotEqual"; - break; - case LeftParen: - qCDebug(animation) << " LeftParen"; - break; - case RightParen: - qCDebug(animation) << " RightParen"; - break; - case Not: - qCDebug(animation) << " Not"; - break; - case Minus: - qCDebug(animation) << " Minus"; - break; - case Plus: - qCDebug(animation) << " Plus"; - break; - case Multiply: - qCDebug(animation) << " Multiply"; - break; - case Modulus: - qCDebug(animation) << " Modulus"; - break; - case Error: - qCDebug(animation) << " Error"; - break; - } -} -#endif - AnimExpression::AnimExpression(const QString& str) : _expression(str) { parseExpression(_expression); @@ -130,6 +53,7 @@ AnimExpression::Token AnimExpression::consumeToken(const QString& str, QString:: case '+': ++iter; return Token(Token::Plus); case '*': ++iter; return Token(Token::Multiply); case '%': ++iter; return Token(Token::Modulus); + case ',': ++iter; return Token(Token::Comma); default: qCCritical(animation) << "AnimExpression: unexpected char" << *iter << "at index " << (int)(iter - str.begin()); return Token(Token::Error); @@ -261,4 +185,3 @@ AnimExpression::Token AnimExpression::consumeNot(const QString& str, QString::co return Token(Token::Not); } } - diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 25e1803721..7e6c42f08a 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -45,6 +45,7 @@ protected: Plus, Multiply, Modulus, + Comma, Error }; Token(Type type) : type(type) {} @@ -58,9 +59,6 @@ protected: glm::vec3 vec3Val; glm::vec4 vec4Val; glm::quat quatVal; -#ifndef NDEBUG - void dump(); -#endif }; bool parseExpression(const QString& str); Token consumeToken(const QString& str, QString::const_iterator& iter) const; From 32c40d37c02694326abfd784010fc4c328764251 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Sun, 1 Nov 2015 16:01:29 -0800 Subject: [PATCH 04/43] Removed vec literals tokens and renamed int and float token types --- libraries/animation/src/AnimExpression.h | 11 ++++------- tests/animation/src/AnimTests.cpp | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 7e6c42f08a..7ba099ce2e 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -25,11 +25,8 @@ protected: enum Type { End = 0, Identifier, - LiteralInt, - LiteralFloat, - LiteralVec3, - LiteralVec4, - LiteralQuat, + Int, + Float, And, Or, GreaterThan, @@ -50,8 +47,8 @@ protected: }; Token(Type type) : type(type) {} Token(const QStringRef& strRef) : type(Type::Identifier), strVal(strRef.toString()) {} - Token(int val) : type(Type::LiteralInt), intVal(val) {} - Token(float val) : type(Type::LiteralFloat), floatVal(val) {} + Token(int val) : type(Type::Int), intVal(val) {} + Token(float val) : type(Type::Float), floatVal(val) {} Type type = End; QString strVal; int intVal; diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 5b6806ec09..44f9c05c22 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -331,7 +331,7 @@ void AnimTests::testTokenizer() { AnimExpression::Token token = e.consumeToken(str, iter); QVERIFY(token.type == AnimExpression::Token::LeftParen); token = e.consumeToken(str, iter); - QVERIFY(token.type == AnimExpression::Token::LiteralInt); + QVERIFY(token.type == AnimExpression::Token::Int); QVERIFY(token.intVal == 10); token = e.consumeToken(str, iter); QVERIFY(token.type == AnimExpression::Token::Plus); @@ -343,7 +343,7 @@ void AnimTests::testTokenizer() { token = e.consumeToken(str, iter); QVERIFY(token.type == AnimExpression::Token::GreaterThanEqual); token = e.consumeToken(str, iter); - QVERIFY(token.type == AnimExpression::Token::LiteralFloat); + QVERIFY(token.type == AnimExpression::Token::Float); QVERIFY(fabsf(token.floatVal - 20.1f) < 0.0001f); token = e.consumeToken(str, iter); QVERIFY(token.type == AnimExpression::Token::And); From a80ab0003c424e3c85ef36424a58e14f08f9b4c1 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 2 Nov 2015 20:46:29 -0800 Subject: [PATCH 05/43] Removed vec literal values --- libraries/animation/src/AnimExpression.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 7ba099ce2e..145350547b 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -12,7 +12,6 @@ #define hifi_AnimExpression #include -#include #include #include @@ -53,11 +52,7 @@ protected: QString strVal; int intVal; float floatVal; - glm::vec3 vec3Val; - glm::vec4 vec4Val; - glm::quat quatVal; }; - bool parseExpression(const QString& str); Token consumeToken(const QString& str, QString::const_iterator& iter) const; Token consumeIdentifier(const QString& str, QString::const_iterator& iter) const; Token consumeNumber(const QString& str, QString::const_iterator& iter) const; @@ -67,6 +62,8 @@ protected: Token consumeLessThan(const QString& str, QString::const_iterator& iter) const; Token consumeNot(const QString& str, QString::const_iterator& iter) const; + bool parseExpression(const QString& str); + QString _expression; }; From 61d1dd00d1d1d949887f230a84ea1d31518ad334 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 4 Nov 2015 12:04:34 -0800 Subject: [PATCH 06/43] Fixes for Mac build and crash on startup For some reason the qt code gen is getting confused by the #if 0 in 3DConnextionClient.h, so I added a stub implementation. In Application.cpp the change of moving idle into paintGL had a sideeffect of calling OffscreenGlCanvas before it was initialized. To work around this I added a guard to prevent calling idle before all the gl windows/widgets have been initialized. (cherry picked from commit 69f1cfbcb9125aa9d449a5d01b3734bcd51309e8) --- interface/src/Application.cpp | 12 ++++++------ interface/src/Application.h | 3 ++- interface/src/devices/3DConnexionClient.h | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d31d9de4a0..556664ec10 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -940,14 +940,12 @@ void Application::initializeGL() { qCDebug(interfaceapp) << "Created Display Window."; // initialize glut for shape drawing; Qt apparently initializes it on OS X - #ifndef __APPLE__ - static bool isInitialized = false; - if (isInitialized) { + if (_isGLInitialized) { return; } else { - isInitialized = true; + _isGLInitialized = true; } - #endif + // Where the gpuContext is initialized and where the TRUE Backend is created and assigned gpu::Context::init(); _gpuContext = std::make_shared(); @@ -1059,7 +1057,9 @@ void Application::paintGL() { _lastFramesPerSecondUpdate = now; } - idle(now); + if (_isGLInitialized) { + idle(now); + } PROFILE_RANGE(__FUNCTION__); PerformanceTimer perfTimer("paintGL"); diff --git a/interface/src/Application.h b/interface/src/Application.h index 212687c11e..5a5d5a015c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -414,7 +414,7 @@ private: bool _dependencyManagerIsSetup; - OffscreenGlCanvas* _offscreenContext; + OffscreenGlCanvas* _offscreenContext {nullptr}; DisplayPluginPointer _displayPlugin; InputPluginList _activeInputPlugins; @@ -548,6 +548,7 @@ private: quint64 _lastSimsPerSecondUpdate = 0; bool _isForeground = true; // starts out assumed to be in foreground bool _inPaint = false; + bool _isGLInitialized {false}; }; #endif // hifi_Application_h diff --git a/interface/src/devices/3DConnexionClient.h b/interface/src/devices/3DConnexionClient.h index 03a43d4c64..b6fa6a37c3 100755 --- a/interface/src/devices/3DConnexionClient.h +++ b/interface/src/devices/3DConnexionClient.h @@ -220,4 +220,19 @@ public: #endif +#include +#include + +// stub +class ConnexionClient : public QObject { + Q_OBJECT +public: + static ConnexionClient& getInstance(); + void init() {}; + void destroy() {}; + bool Is3dmouseAttached() { return false; }; +public slots: + void toggleConnexion(bool shouldEnable) {}; +}; + #endif // defined(hifi_3DConnexionClient_h) From 04d8a598da5d8f2ee684619cfb7492349853fdcc Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 4 Nov 2015 16:56:34 -0800 Subject: [PATCH 07/43] First step toward evaluation * added OpCodes * added first parser rules * removed mat4 support from AnimVariantMap --- libraries/animation/src/AnimExpression.cpp | 164 ++++++++++++++++----- libraries/animation/src/AnimExpression.h | 75 +++++++++- libraries/animation/src/AnimVariant.h | 74 ++++++---- libraries/shared/src/GLMHelpers.h | 1 + tests/animation/src/AnimTests.cpp | 34 +++-- tests/animation/src/AnimTests.h | 3 +- 6 files changed, 265 insertions(+), 86 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index fe056be81d..8807262028 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -16,58 +16,56 @@ AnimExpression::AnimExpression(const QString& str) : _expression(str) { - parseExpression(_expression); + auto iter = str.begin(); + parseExpression(_expression, iter); } -bool AnimExpression::parseExpression(const QString& str) { - Token token(Token::End); - auto iter = str.begin(); - do { - token = consumeToken(str, iter); - switch(token.type) { - case Token::Error: - case Token::End: - return false; - } - } while(iter != str.end()); +void AnimExpression::unconsumeToken(const Token& token) { + _tokenStack.push(token); } AnimExpression::Token AnimExpression::consumeToken(const QString& str, QString::const_iterator& iter) const { - while (iter != str.end()) { - if (iter->isSpace()) { - ++iter; - } else if (iter->isLetter()) { - return consumeIdentifier(str, iter); - } else if (iter->isDigit()) { - return consumeNumber(str, iter); - } else { - switch (iter->unicode()) { - case '&': return consumeAnd(str, iter); - case '|': return consumeOr(str, iter); - case '>': return consumeGreaterThan(str, iter); - case '<': return consumeLessThan(str, iter); - case '(': ++iter; return Token(Token::LeftParen); - case ')': ++iter; return Token(Token::RightParen); - case '!': return consumeNot(str, iter); - case '-': ++iter; return Token(Token::Minus); - case '+': ++iter; return Token(Token::Plus); - case '*': ++iter; return Token(Token::Multiply); - case '%': ++iter; return Token(Token::Modulus); - case ',': ++iter; return Token(Token::Comma); - default: - qCCritical(animation) << "AnimExpression: unexpected char" << *iter << "at index " << (int)(iter - str.begin()); - return Token(Token::Error); + if (!_tokenStack.empty()) { + Token top = _tokenStack.top(); + _tokenStack.pop(); + return top; + } else { + while (iter != str.end()) { + if (iter->isSpace()) { + ++iter; + } else if (iter->isLetter()) { + return consumeIdentifier(str, iter); + } else if (iter->isDigit()) { + return consumeNumber(str, iter); + } else { + switch (iter->unicode()) { + case '&': return consumeAnd(str, iter); + case '|': return consumeOr(str, iter); + case '>': return consumeGreaterThan(str, iter); + case '<': return consumeLessThan(str, iter); + case '(': ++iter; return Token(Token::LeftParen); + case ')': ++iter; return Token(Token::RightParen); + case '!': return consumeNot(str, iter); + case '-': ++iter; return Token(Token::Minus); + case '+': ++iter; return Token(Token::Plus); + case '*': ++iter; return Token(Token::Multiply); + case '%': ++iter; return Token(Token::Modulus); + case ',': ++iter; return Token(Token::Comma); + default: + qCCritical(animation) << "AnimExpression: unexpected char" << *iter << "at index " << (int)(iter - str.begin()); + return Token(Token::Error); + } } } + return Token(Token::End); } - return Token(Token::End); } AnimExpression::Token AnimExpression::consumeIdentifier(const QString& str, QString::const_iterator& iter) const { assert(iter != str.end()); assert(iter->isLetter()); auto begin = iter; - while (iter->isLetter() && iter != str.end()) { + while ((iter->isLetter() || iter->isDigit()) && iter != str.end()) { ++iter; } int pos = (int)(begin - str.begin()); @@ -185,3 +183,93 @@ AnimExpression::Token AnimExpression::consumeNot(const QString& str, QString::co return Token(Token::Not); } } + +bool AnimExpression::parseExpression(const QString& str, QString::const_iterator& iter) { + auto token = consumeToken(str, iter); + if (token.type == Token::Identifier) { + if (token.strVal == "true") { + _opCodes.push_back(OpCode {true}); + } else if (token.strVal == "false") { + _opCodes.push_back(OpCode {false}); + } else { + _opCodes.push_back(OpCode {token.strVal}); + } + return true; + } else if (token.type == Token::Int) { + _opCodes.push_back(OpCode {token.intVal}); + return true; + } else if (token.type == Token::Float) { + _opCodes.push_back(OpCode {token.floatVal}); + return true; + } else if (token.type == Token::LeftParen) { + if (parseUnaryExpression(str, iter)) { + token = consumeToken(str, iter); + if (token.type != Token::RightParen) { + qCCritical(animation) << "Error parsing expression, expected ')'"; + return false; + } else { + return true; + } + } else { + return false; + } + } else { + qCCritical(animation) << "Error parsing expression, unexpected symbol"; + return false; + } +} + +bool AnimExpression::parseUnaryExpression(const QString& str, QString::const_iterator& iter) { + auto token = consumeToken(str, iter); + if (token.type == Token::Plus) { + if (parseExpression(str, iter)) { + _opCodes.push_back(OpCode {OpCode::UnaryPlus}); + return true; + } else { + return false; + } + } else if (token.type == Token::Minus) { + if (parseExpression(str, iter)) { + _opCodes.push_back(OpCode {OpCode::UnaryMinus}); + return true; + } else { + return false; + } + } else if (token.type == Token::Not) { + if (parseExpression(str, iter)) { + _opCodes.push_back(OpCode {OpCode::Not}); + return true; + } else { + return false; + } + } else { + unconsumeToken(token); + return parseExpression(str, iter); + } +} + +AnimExpression::OpCode AnimExpression::evaluate(const AnimVariantMap& map) const { + std::stack stack; + for (auto& opCode : _opCodes) { + switch (opCode.type) { + case OpCode::Identifier: + case OpCode::Int: + case OpCode::Float: + stack.push(opCode); + break; + default: + switch (opCode.type) { + case OpCode::Not: + evalNot(map, stack); + break; + } + } + } + return stack.top(); +} + +void AnimExpression::evalNot(const AnimVariantMap& map, std::stack& stack) const { + bool lhs = stack.top().coerceBool(map); + stack.pop(); + stack.push(OpCode {!lhs}); +} diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 145350547b..8d216ca412 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -14,6 +14,9 @@ #include #include #include +#include +#include +#include "AnimVariant.h" class AnimExpression { public: @@ -44,15 +47,64 @@ protected: Comma, Error }; - Token(Type type) : type(type) {} - Token(const QStringRef& strRef) : type(Type::Identifier), strVal(strRef.toString()) {} - Token(int val) : type(Type::Int), intVal(val) {} - Token(float val) : type(Type::Float), floatVal(val) {} - Type type = End; + Token(Type type) : type {type} {} + Token(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} + Token(int val) : type {Type::Int}, intVal {val} {} + Token(float val) : type {Type::Float}, floatVal {val} {} + Type type {End}; QString strVal; - int intVal; - float floatVal; + int intVal {0}; + float floatVal {0.0f}; }; + + struct OpCode { + enum Type { + Identifier, + Bool, + Int, + Float, + And, + Or, + GreaterThan, + GreaterThanEqual, + LessThan, + LessThanEqual, + Equal, + NotEqual, + LeftParen, + RightParen, + Not, + Minus, + Plus, + Multiply, + Modulus, + UnaryPlus, + UnaryMinus + }; + OpCode(Type type) : type {type} {} + OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} + OpCode(const QString& str) : type {Type::Identifier}, strVal {str} {} + OpCode(int val) : type {Type::Int}, intVal {val} {} + OpCode(bool val) : type {Type::Bool}, intVal {(int)val} {} + OpCode(float val) : type {Type::Float}, floatVal {val} {} + + bool coerceBool(const AnimVariantMap& map) const { + if (type == Int || type == Bool) { + return intVal != 0; + } else if (type == Identifier) { + return map.lookup(strVal, false); + } else { + return true; + } + } + + Type type {Int}; + QString strVal; + int intVal {0}; + float floatVal {0.0f}; + }; + + void unconsumeToken(const Token& token); Token consumeToken(const QString& str, QString::const_iterator& iter) const; Token consumeIdentifier(const QString& str, QString::const_iterator& iter) const; Token consumeNumber(const QString& str, QString::const_iterator& iter) const; @@ -62,9 +114,16 @@ protected: Token consumeLessThan(const QString& str, QString::const_iterator& iter) const; Token consumeNot(const QString& str, QString::const_iterator& iter) const; - bool parseExpression(const QString& str); + bool parseExpression(const QString& str, QString::const_iterator& iter); + bool parseUnaryExpression(const QString& str, QString::const_iterator& iter); + + OpCode evaluate(const AnimVariantMap& map) const; + void evalNot(const AnimVariantMap& map, std::stack& stack) const; QString _expression; + mutable std::stack _tokenStack; + std::vector _opCodes; + }; #endif diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index 0d7c657058..ff7794a16a 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -18,8 +18,9 @@ #include #include #include +#include +#include #include "AnimationLogging.h" -#include "StreamUtils.h" class AnimVariant { public: @@ -29,7 +30,6 @@ public: Float, Vec3, Quat, - Mat4, String, NumTypes }; @@ -40,7 +40,6 @@ public: AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; } AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast(&_val) = value; } AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast(&_val) = value; } - AnimVariant(const glm::mat4& value) : _type(Type::Mat4) { *reinterpret_cast(&_val) = value; } AnimVariant(const QString& value) : _type(Type::String) { _stringVal = value; } bool isBool() const { return _type == Type::Bool; } @@ -48,7 +47,6 @@ public: bool isFloat() const { return _type == Type::Float; } bool isVec3() const { return _type == Type::Vec3; } bool isQuat() const { return _type == Type::Quat; } - bool isMat4() const { return _type == Type::Mat4; } bool isString() const { return _type == Type::String; } Type getType() const { return _type; } @@ -57,17 +55,52 @@ public: void setFloat(float value) { assert(_type == Type::Float); _val.floats[0] = value; } void setVec3(const glm::vec3& value) { assert(_type == Type::Vec3); *reinterpret_cast(&_val) = value; } void setQuat(const glm::quat& value) { assert(_type == Type::Quat); *reinterpret_cast(&_val) = value; } - void setMat4(const glm::mat4& value) { assert(_type == Type::Mat4); *reinterpret_cast(&_val) = value; } 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 || _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); } - const QString& getString() const { assert(_type == Type::String); return _stringVal; } + bool getBool() const { + if (_type == Type::Bool) { + return _val.boolVal; + } else if (_type == Type::Int) { + return _val.intVal != 0; + } else { + return false; + } + } + int getInt() const { + if (_type == Type::Int) { + return _val.intVal; + } else if (_type == Type::Float) { + return (int)_val.floats[0]; + } else { + return 0; + } + } + float getFloat() const { + if (_type == Type::Float) { + return _val.floats[0]; + } else if (_type == Type::Int) { + return (float)_val.intVal; + } else { + return 0.0f; + } + } + const glm::vec3& getVec3() const { + if (_type == Type::Vec3) { + return *reinterpret_cast(&_val); + } else { + return Vectors::ZERO; + } + } + const glm::quat& getQuat() const { + if (_type == Type::Quat) { + return *reinterpret_cast(&_val); + } else { + return Quaternions::IDENTITY; + } + } + const QString& getString() const { + return _stringVal; + } protected: Type _type; @@ -75,7 +108,7 @@ protected: union { bool boolVal; int intVal; - float floats[16]; + float floats[4]; } _val; }; @@ -130,15 +163,6 @@ public: } } - const glm::mat4& lookup(const QString& key, const glm::mat4& defaultValue) const { - if (key.isEmpty()) { - return defaultValue; - } else { - auto iter = _map.find(key); - return iter != _map.end() ? iter->second.getMat4() : defaultValue; - } - } - const QString& lookup(const QString& key, const QString& defaultValue) const { if (key.isEmpty()) { return defaultValue; @@ -153,7 +177,6 @@ public: void set(const QString& key, float value) { _map[key] = AnimVariant(value); } void set(const QString& key, const glm::vec3& value) { _map[key] = AnimVariant(value); } void set(const QString& key, const glm::quat& value) { _map[key] = AnimVariant(value); } - void set(const QString& key, const glm::mat4& value) { _map[key] = AnimVariant(value); } void set(const QString& key, const QString& value) { _map[key] = AnimVariant(value); } void unset(const QString& key) { _map.erase(key); } @@ -189,9 +212,6 @@ public: case AnimVariant::Type::Quat: qCDebug(animation) << " " << pair.first << "=" << pair.second.getQuat(); break; - case AnimVariant::Type::Mat4: - qCDebug(animation) << " " << pair.first << "=" << pair.second.getMat4(); - break; case AnimVariant::Type::String: qCDebug(animation) << " " << pair.first << "=" << pair.second.getString(); break; diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 9c1bbe23a4..e69d11629e 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -80,6 +80,7 @@ public: static const vec3& RIGHT; static const vec3& UP; static const vec3& FRONT; + static const vec3 ZERO4; }; // These pack/unpack functions are designed to start specific known types in as efficient a manner diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 44f9c05c22..94caed66f5 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -195,10 +195,7 @@ void AnimTests::testVariant() { auto floatVarNegative = AnimVariant(-1.0f); auto vec3Var = AnimVariant(glm::vec3(1.0f, -2.0f, 3.0f)); auto quatVar = AnimVariant(glm::quat(1.0f, 2.0f, -3.0f, 4.0f)); - auto mat4Var = AnimVariant(glm::mat4(glm::vec4(1.0f, 2.0f, 3.0f, 4.0f), - glm::vec4(5.0f, 6.0f, -7.0f, 8.0f), - glm::vec4(9.0f, 10.0f, 11.0f, 12.0f), - glm::vec4(13.0f, 14.0f, 15.0f, 16.0f))); + QVERIFY(defaultVar.isBool()); QVERIFY(defaultVar.getBool() == false); @@ -233,12 +230,6 @@ void AnimTests::testVariant() { QVERIFY(q.x == 2.0f); QVERIFY(q.y == -3.0f); QVERIFY(q.z == 4.0f); - - QVERIFY(mat4Var.isMat4()); - auto m = mat4Var.getMat4(); - QVERIFY(m[0].x == 1.0f); - QVERIFY(m[1].z == -7.0f); - QVERIFY(m[3].w == 16.0f); } void AnimTests::testAccumulateTime() { @@ -323,10 +314,11 @@ void AnimTests::testAccumulateTimeWithParameters(float startFrame, float endFram QVERIFY(resultFrame == startFrame + 0.5f); QVERIFY(!triggers.empty() && triggers[0] == "testNodeOnLoop"); triggers.clear(); +} -void AnimTests::testTokenizer() { +void AnimTests::testExpressionTokenizer() { QString str = "(10 + x) >= 20.1 && (y != !z)"; - AnimExpression e(""); + AnimExpression e("x"); auto iter = str.cbegin(); AnimExpression::Token token = e.consumeToken(str, iter); QVERIFY(token.type == AnimExpression::Token::LeftParen); @@ -364,3 +356,21 @@ void AnimTests::testTokenizer() { token = e.consumeToken(str, iter); } +void AnimTests::testExpressionParser() { + QString str = "(!x)"; + AnimExpression e(str); + QVERIFY(e._opCodes.size() == 2); + if (e._opCodes.size() == 2) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); + QVERIFY(e._opCodes[0].strVal == "x"); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); + } + + auto vars = AnimVariantMap(); + vars.set("x", false); + + auto opCode = e.evaluate(vars); + QVERIFY(opCode.type == AnimExpression::OpCode::Bool); + QVERIFY(opCode.coerceBool(vars) == true); +} + diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index bfea4eb086..94b3eddd25 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -26,7 +26,8 @@ private slots: void testLoader(); void testVariant(); void testAccumulateTime(); - void testTokenizer(); + void testExpressionTokenizer(); + void testExpressionParser(); }; #endif // hifi_AnimTests_h From 431a108c351431345d9aab4144ef55f80de84ed3 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 4 Nov 2015 20:13:17 -0800 Subject: [PATCH 08/43] Bugfixes to expression for !!x expressions Added stub eval methods. only boolean not, boolean and, boolean or and unary minus are implemented. --- libraries/animation/src/AnimExpression.cpp | 186 +++++++++++++++++++-- libraries/animation/src/AnimExpression.h | 25 ++- libraries/animation/src/AnimVariant.cpp | 2 + libraries/animation/src/AnimVariant.h | 11 ++ tests/animation/src/AnimTests.cpp | 87 +++++++++- 5 files changed, 280 insertions(+), 31 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index 8807262028..a03925f8f9 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -214,23 +214,27 @@ bool AnimExpression::parseExpression(const QString& str, QString::const_iterator return false; } } else { - qCCritical(animation) << "Error parsing expression, unexpected symbol"; - return false; + unconsumeToken(token); + if (parseUnaryExpression(str, iter)) { + return true; + } else { + qCCritical(animation) << "Error parsing expression"; + return false; + } } } bool AnimExpression::parseUnaryExpression(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); - if (token.type == Token::Plus) { + if (token.type == Token::Plus) { // unary plus is a no op. if (parseExpression(str, iter)) { - _opCodes.push_back(OpCode {OpCode::UnaryPlus}); return true; } else { return false; } } else if (token.type == Token::Minus) { if (parseExpression(str, iter)) { - _opCodes.push_back(OpCode {OpCode::UnaryMinus}); + _opCodes.push_back(OpCode {OpCode::Minus}); return true; } else { return false; @@ -255,21 +259,173 @@ AnimExpression::OpCode AnimExpression::evaluate(const AnimVariantMap& map) const case OpCode::Identifier: case OpCode::Int: case OpCode::Float: + case OpCode::Bool: stack.push(opCode); break; - default: - switch (opCode.type) { - case OpCode::Not: - evalNot(map, stack); - break; - } + case OpCode::And: evalAnd(map, stack); break; + case OpCode::Or: evalOr(map, stack); break; + case OpCode::GreaterThan: evalGreaterThan(map, stack); break; + case OpCode::GreaterThanEqual: evalGreaterThanEqual(map, stack); break; + case OpCode::LessThan: evalLessThan(map, stack); break; + case OpCode::LessThanEqual: evalLessThanEqual(map, stack); break; + case OpCode::Equal: evalEqual(map, stack); break; + case OpCode::NotEqual: evalNotEqual(map, stack); break; + case OpCode::Not: evalNot(map, stack); break; + case OpCode::Subtract: evalSubtract(map, stack); break; + case OpCode::Add: evalAdd(map, stack); break; + case OpCode::Multiply: evalMultiply(map, stack); break; + case OpCode::Modulus: evalModulus(map, stack); break; + case OpCode::Minus: evalMinus(map, stack); break; } } return stack.top(); } -void AnimExpression::evalNot(const AnimVariantMap& map, std::stack& stack) const { - bool lhs = stack.top().coerceBool(map); - stack.pop(); - stack.push(OpCode {!lhs}); +#define POP_BOOL(NAME) \ + const OpCode& NAME##_temp = stack.top(); \ + bool NAME = NAME##_temp.coerceBool(map); \ + stack.pop() + +#define PUSH(EXPR) \ + stack.push(OpCode {(EXPR)}) + +void AnimExpression::evalAnd(const AnimVariantMap& map, std::stack& stack) const { + POP_BOOL(lhs); + POP_BOOL(rhs); + PUSH(lhs && rhs); +} + +void AnimExpression::evalOr(const AnimVariantMap& map, std::stack& stack) const { + POP_BOOL(lhs); + POP_BOOL(rhs); + PUSH(lhs || rhs); +} + +void AnimExpression::evalGreaterThan(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(false); +} + +void AnimExpression::evalGreaterThanEqual(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(false); +} + +void AnimExpression::evalLessThan(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(false); +} + +void AnimExpression::evalLessThanEqual(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(false); +} + +void AnimExpression::evalEqual(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(false); +} + +void AnimExpression::evalNotEqual(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(false); +} + +void AnimExpression::evalNot(const AnimVariantMap& map, std::stack& stack) const { + POP_BOOL(rhs); + PUSH(!rhs); +} + +void AnimExpression::evalSubtract(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(0.0f); +} + +void AnimExpression::evalAdd(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(0.0f); +} + +void AnimExpression::evalMultiply(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH(0.0f); +} + +void AnimExpression::evalModulus(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = stack.top(); stack.pop(); + OpCode rhs = stack.top(); stack.pop(); + + // TODO: + PUSH((int)0); +} + +void AnimExpression::evalMinus(const AnimVariantMap& map, std::stack& stack) const { + OpCode rhs = stack.top(); stack.pop(); + + switch (rhs.type) { + case OpCode::Identifier: { + const AnimVariant& var = map.get(rhs.strVal); + switch (var.getType()) { + case AnimVariant::Type::Bool: + qCWarning(animation) << "AnimExpression: type missmatch for unary minus, expected a number not a bool"; + // interpret this as boolean not. + PUSH(!var.getBool()); + break; + case AnimVariant::Type::Int: + PUSH(-var.getInt()); + break; + case AnimVariant::Type::Float: + PUSH(-var.getFloat()); + break; + default: + // TODO: Vec3, Quat are unsupported + assert(false); + PUSH(false); + break; + } + } + case OpCode::Int: + PUSH(-rhs.intVal); + break; + case OpCode::Float: + PUSH(-rhs.floatVal); + break; + case OpCode::Bool: + qCWarning(animation) << "AnimExpression: type missmatch for unary minus, expected a number not a bool"; + // interpret this as boolean not. + PUSH(!rhs.coerceBool(map)); + break; + default: + qCCritical(animation) << "AnimExpression: ERRROR for unary minus, expected a number, type = " << rhs.type; + assert(false); + PUSH(false); + break; + } } diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 8d216ca412..afada44e86 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -71,15 +71,12 @@ protected: LessThanEqual, Equal, NotEqual, - LeftParen, - RightParen, Not, - Minus, - Plus, + Subtract, + Add, Multiply, Modulus, - UnaryPlus, - UnaryMinus + Minus }; OpCode(Type type) : type {type} {} OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} @@ -118,12 +115,24 @@ protected: bool parseUnaryExpression(const QString& str, QString::const_iterator& iter); OpCode evaluate(const AnimVariantMap& map) const; + void evalAnd(const AnimVariantMap& map, std::stack& stack) const; + void evalOr(const AnimVariantMap& map, std::stack& stack) const; + void evalGreaterThan(const AnimVariantMap& map, std::stack& stack) const; + void evalGreaterThanEqual(const AnimVariantMap& map, std::stack& stack) const; + void evalLessThan(const AnimVariantMap& map, std::stack& stack) const; + void evalLessThanEqual(const AnimVariantMap& map, std::stack& stack) const; + void evalEqual(const AnimVariantMap& map, std::stack& stack) const; + void evalNotEqual(const AnimVariantMap& map, std::stack& stack) const; void evalNot(const AnimVariantMap& map, std::stack& stack) const; + void evalSubtract(const AnimVariantMap& map, std::stack& stack) const; + void evalAdd(const AnimVariantMap& map, std::stack& stack) const; + void evalMultiply(const AnimVariantMap& map, std::stack& stack) const; + void evalModulus(const AnimVariantMap& map, std::stack& stack) const; + void evalMinus(const AnimVariantMap& map, std::stack& stack) const; QString _expression; - mutable std::stack _tokenStack; + mutable std::stack _tokenStack; // TODO: remove, only needed during parsing std::vector _opCodes; - }; #endif diff --git a/libraries/animation/src/AnimVariant.cpp b/libraries/animation/src/AnimVariant.cpp index 234e9cef09..cae6dce23d 100644 --- a/libraries/animation/src/AnimVariant.cpp +++ b/libraries/animation/src/AnimVariant.cpp @@ -15,6 +15,8 @@ #include #include "AnimVariant.h" // which has AnimVariant/AnimVariantMap +const AnimVariant AnimVariant::FALSE = AnimVariant(); + 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(); diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index ff7794a16a..b15b25f4e0 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -34,6 +34,8 @@ public: NumTypes }; + static const AnimVariant FALSE; + AnimVariant() : _type(Type::Bool) { memset(&_val, 0, sizeof(_val)); } AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; } AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; } @@ -186,6 +188,15 @@ public: void clearMap() { _map.clear(); } bool hasKey(const QString& key) const { return _map.find(key) != _map.end(); } + const AnimVariant& get(const QString& key) const { + auto iter = _map.find(key); + if (iter != _map.end()) { + return iter->second; + } else { + return AnimVariant::FALSE; + } + } + // 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.) diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 94caed66f5..58fd3a1d35 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -357,20 +357,91 @@ void AnimTests::testExpressionTokenizer() { } void AnimTests::testExpressionParser() { - QString str = "(!x)"; - AnimExpression e(str); + + auto vars = AnimVariantMap(); + vars.set("f", false); + vars.set("t", true); + vars.set("ten", (int)10); + vars.set("twenty", (int)20); + vars.set("five", (float)5.0f); + vars.set("forty", (float)40.0f); + + AnimExpression e("(!f)"); QVERIFY(e._opCodes.size() == 2); if (e._opCodes.size() == 2) { QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "x"); + QVERIFY(e._opCodes[0].strVal == "f"); QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); + + auto opCode = e.evaluate(vars); + QVERIFY(opCode.type == AnimExpression::OpCode::Bool); + QVERIFY((opCode.intVal != 0) == true); } - auto vars = AnimVariantMap(); - vars.set("x", false); + e = AnimExpression("!!f"); + QVERIFY(e._opCodes.size() == 3); + if (e._opCodes.size() == 3) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); + QVERIFY(e._opCodes[0].strVal == "f"); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY(opCode.coerceBool(vars) == true); + auto opCode = e.evaluate(vars); + QVERIFY(opCode.type == AnimExpression::OpCode::Bool); + QVERIFY((opCode.intVal != 0) == false); + } + + e = AnimExpression("!!(!f)"); + QVERIFY(e._opCodes.size() == 4); + if (e._opCodes.size() == 4) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); + QVERIFY(e._opCodes[0].strVal == "f"); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); + QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Not); + + auto opCode = e.evaluate(vars); + QVERIFY(opCode.type == AnimExpression::OpCode::Bool); + QVERIFY((opCode.intVal != 0) == true); + } +/* + e = AnimExpression("f || !f"); + QVERIFY(e._opCodes.size() == 4); + if (e._opCodes.size() == 4) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); + QVERIFY(e._opCodes[0].strVal == "f"); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Identifier); + QVERIFY(e._opCodes[1].strVal == "f"); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); + QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Or); + + auto opCode = e.evaluate(vars); + QVERIFY(opCode.type == AnimExpression::OpCode::Bool); + QVERIFY((opCode.intVal != 0) == true); + } +*/ + e = AnimExpression("-10"); + QVERIFY(e._opCodes.size() == 2); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 10); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Minus); + + auto opCode = e.evaluate(vars); + QVERIFY(opCode.type == AnimExpression::OpCode::Int); + QVERIFY(opCode.intVal == 10); + } + + e = AnimExpression("-ten"); + QVERIFY(e._opCodes.size() == 2); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); + QVERIFY(e._opCodes[0].strVal == "ten"); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Minus); + + auto opCode = e.evaluate(vars); + QVERIFY(opCode.type == AnimExpression::OpCode::Int); + QVERIFY(opCode.intVal == 10); + } } From 99223d0a3c8848fbef5d8ce16f834747c8efe2e5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 3 Dec 2015 15:02:00 -0800 Subject: [PATCH 09/43] AnimExpression: support for parsing simple expressions supports parens, binary +, -, / and *. / and * have higher precedence then + and - --- libraries/animation/src/AnimExpression.cpp | 349 +++++++++++++++++---- libraries/animation/src/AnimExpression.h | 24 +- tests/animation/src/AnimTests.cpp | 174 ++++++---- tests/animation/src/AnimTests.h | 1 + 4 files changed, 417 insertions(+), 131 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index a03925f8f9..b76aaff3f9 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -17,9 +17,16 @@ AnimExpression::AnimExpression(const QString& str) : _expression(str) { auto iter = str.begin(); - parseExpression(_expression, iter); + parseExpr(_expression, iter); + while(!_tokenStack.empty()) { + _tokenStack.pop(); + } } +// +// Tokenizer +// + void AnimExpression::unconsumeToken(const Token& token) { _tokenStack.push(token); } @@ -49,6 +56,7 @@ AnimExpression::Token AnimExpression::consumeToken(const QString& str, QString:: case '-': ++iter; return Token(Token::Minus); case '+': ++iter; return Token(Token::Plus); case '*': ++iter; return Token(Token::Multiply); + case '/': ++iter; return Token(Token::Divide); case '%': ++iter; return Token(Token::Modulus); case ',': ++iter; return Token(Token::Comma); default: @@ -184,73 +192,137 @@ AnimExpression::Token AnimExpression::consumeNot(const QString& str, QString::co } } -bool AnimExpression::parseExpression(const QString& str, QString::const_iterator& iter) { +// +// Parser +// + +/* +Expr → Term Expr' +Expr' → + Term Expr' + | – Term Expr' + | ε +Term → Factor Term' +Term' → * Term' + | / Term' + | ε +Factor → INT + | FLOAT + | IDENTIFIER + | (Expr) +*/ + +bool AnimExpression::parseExpr(const QString& str, QString::const_iterator& iter) { + if (!parseTerm(str, iter)) { + return false; + } + if (!parseExprPrime(str, iter)) { + return false; + } + return true; +} + +bool AnimExpression::parseExprPrime(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); - if (token.type == Token::Identifier) { - if (token.strVal == "true") { - _opCodes.push_back(OpCode {true}); - } else if (token.strVal == "false") { - _opCodes.push_back(OpCode {false}); - } else { - _opCodes.push_back(OpCode {token.strVal}); + if (token.type == Token::Plus) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; } + if (!parseExprPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Add}); return true; - } else if (token.type == Token::Int) { + } else if (token.type == Token::Minus) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; + } + if (!parseExprPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Subtract}); + return true; + } else { + unconsumeToken(token); + return true; + } +} + +bool AnimExpression::parseTerm(const QString& str, QString::const_iterator& iter) { + if (!parseFactor(str, iter)) { + return false; + } + if (!parseTermPrime(str, iter)) { + return false; + } + return true; +} + +bool AnimExpression::parseTermPrime(const QString& str, QString::const_iterator& iter) { + auto token = consumeToken(str, iter); + if (token.type == Token::Multiply) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; + } + if (!parseTermPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Multiply}); + return true; + } else if (token.type == Token::Divide) { + if (!parseTerm(str, iter)) { + unconsumeToken(token); + return false; + } + if (!parseTermPrime(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Divide}); + return true; + } else { + unconsumeToken(token); + return true; + } +} + +bool AnimExpression::parseFactor(const QString& str, QString::const_iterator& iter) { + auto token = consumeToken(str, iter); + if (token.type == Token::Int) { _opCodes.push_back(OpCode {token.intVal}); return true; } else if (token.type == Token::Float) { _opCodes.push_back(OpCode {token.floatVal}); return true; + } else if (token.type == Token::Identifier) { + _opCodes.push_back(OpCode {token.strVal}); + return true; } else if (token.type == Token::LeftParen) { - if (parseUnaryExpression(str, iter)) { - token = consumeToken(str, iter); - if (token.type != Token::RightParen) { - qCCritical(animation) << "Error parsing expression, expected ')'"; - return false; - } else { - return true; - } - } else { + if (!parseExpr(str, iter)) { + unconsumeToken(token); return false; } + auto nextToken = consumeToken(str, iter); + if (nextToken.type != Token::RightParen) { + unconsumeToken(nextToken); + unconsumeToken(token); + return false; + } + return true; } else { unconsumeToken(token); - if (parseUnaryExpression(str, iter)) { - return true; - } else { - qCCritical(animation) << "Error parsing expression"; - return false; - } + return false; } } -bool AnimExpression::parseUnaryExpression(const QString& str, QString::const_iterator& iter) { - auto token = consumeToken(str, iter); - if (token.type == Token::Plus) { // unary plus is a no op. - if (parseExpression(str, iter)) { - return true; - } else { - return false; - } - } else if (token.type == Token::Minus) { - if (parseExpression(str, iter)) { - _opCodes.push_back(OpCode {OpCode::Minus}); - return true; - } else { - return false; - } - } else if (token.type == Token::Not) { - if (parseExpression(str, iter)) { - _opCodes.push_back(OpCode {OpCode::Not}); - return true; - } else { - return false; - } - } else { - unconsumeToken(token); - return parseExpression(str, iter); - } -} +// +// Evaluator +// AnimExpression::OpCode AnimExpression::evaluate(const AnimVariantMap& map) const { std::stack stack; @@ -274,8 +346,9 @@ AnimExpression::OpCode AnimExpression::evaluate(const AnimVariantMap& map) const case OpCode::Subtract: evalSubtract(map, stack); break; case OpCode::Add: evalAdd(map, stack); break; case OpCode::Multiply: evalMultiply(map, stack); break; + case OpCode::Divide: evalDivide(map, stack); break; case OpCode::Modulus: evalModulus(map, stack); break; - case OpCode::Minus: evalMinus(map, stack); break; + case OpCode::UnaryMinus: evalUnaryMinus(map, stack); break; } } return stack.top(); @@ -362,15 +435,107 @@ void AnimExpression::evalSubtract(const AnimVariantMap& map, std::stack& PUSH(0.0f); } -void AnimExpression::evalAdd(const AnimVariantMap& map, std::stack& stack) const { - OpCode lhs = stack.top(); stack.pop(); - OpCode rhs = stack.top(); stack.pop(); +void AnimExpression::add(int lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs + rhs.intVal); + break; + case OpCode::Float: + PUSH((float)lhs + rhs.floatVal); + break; + default: + PUSH(lhs); + } +} - // TODO: - PUSH(0.0f); +void AnimExpression::add(float lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs + (float)rhs.intVal); + break; + case OpCode::Float: + PUSH(lhs + rhs.floatVal); + break; + default: + PUSH(lhs); + } +} + +void AnimExpression::evalAdd(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = coerseToValue(map, stack.top()); + stack.pop(); + OpCode rhs = coerseToValue(map, stack.top()); + stack.pop(); + + switch (lhs.type) { + case OpCode::Bool: + add(lhs.intVal, rhs, stack); + break; + case OpCode::Int: + add(lhs.intVal, rhs, stack); + break; + case OpCode::Float: + add(lhs.floatVal, rhs, stack); + break; + default: + add(0, rhs, stack); + break; + } } void AnimExpression::evalMultiply(const AnimVariantMap& map, std::stack& stack) const { + OpCode lhs = coerseToValue(map, stack.top()); + stack.pop(); + OpCode rhs = coerseToValue(map, stack.top()); + stack.pop(); + + switch(lhs.type) { + case OpCode::Bool: + mul(lhs.intVal, rhs, stack); + break; + case OpCode::Int: + mul(lhs.intVal, rhs, stack); + break; + case OpCode::Float: + mul(lhs.floatVal, rhs, stack); + break; + default: + mul(0, rhs, stack); + break; + } +} + +void AnimExpression::mul(int lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs * rhs.intVal); + break; + case OpCode::Float: + PUSH((float)lhs * rhs.floatVal); + break; + default: + PUSH(lhs); + } +} + +void AnimExpression::mul(float lhs, const OpCode& rhs, std::stack& stack) const { + switch (rhs.type) { + case OpCode::Bool: + case OpCode::Int: + PUSH(lhs * (float)rhs.intVal); + break; + case OpCode::Float: + PUSH(lhs * rhs.floatVal); + break; + default: + PUSH(lhs); + } +} + +void AnimExpression::evalDivide(const AnimVariantMap& map, std::stack& stack) const { OpCode lhs = stack.top(); stack.pop(); OpCode rhs = stack.top(); stack.pop(); @@ -386,7 +551,7 @@ void AnimExpression::evalModulus(const AnimVariantMap& map, std::stack& PUSH((int)0); } -void AnimExpression::evalMinus(const AnimVariantMap& map, std::stack& stack) const { +void AnimExpression::evalUnaryMinus(const AnimVariantMap& map, std::stack& stack) const { OpCode rhs = stack.top(); stack.pop(); switch (rhs.type) { @@ -429,3 +594,69 @@ void AnimExpression::evalMinus(const AnimVariantMap& map, std::stack& st break; } } + +AnimExpression::OpCode AnimExpression::coerseToValue(const AnimVariantMap& map, const OpCode& opCode) const { + switch (opCode.type) { + case OpCode::Identifier: + { + const AnimVariant& var = map.get(opCode.strVal); + switch (var.getType()) { + case AnimVariant::Type::Bool: + qCWarning(animation) << "AnimExpression: type missmatch, expected a number not a bool"; + return OpCode(0); + break; + case AnimVariant::Type::Int: + return OpCode(var.getInt()); + break; + case AnimVariant::Type::Float: + return OpCode(var.getFloat()); + break; + default: + // TODO: Vec3, Quat are unsupported + assert(false); + return OpCode(0); + break; + } + } + break; + case OpCode::Bool: + case OpCode::Int: + case OpCode::Float: + return opCode; + default: + qCCritical(animation) << "AnimExpression: ERROR expected a number, type = " << opCode.type; + assert(false); + return OpCode(0); + break; + } +} + +void AnimExpression::dumpOpCodes() const { + QString tmp; + for (auto& op : _opCodes) { + switch (op.type) { + case OpCode::Identifier: tmp += QString(" %1").arg(op.strVal); break; + case OpCode::Bool: tmp += QString(" %1").arg(op.intVal ? "true" : "false"); break; + case OpCode::Int: tmp += QString(" %1").arg(op.intVal); break; + case OpCode::Float: tmp += QString(" %1").arg(op.floatVal); break; + case OpCode::And: tmp += " &&"; break; + case OpCode::Or: tmp += " ||"; break; + case OpCode::GreaterThan: tmp += " >"; break; + case OpCode::GreaterThanEqual: tmp += " >="; break; + case OpCode::LessThan: tmp += " <"; break; + case OpCode::LessThanEqual: tmp += " <="; break; + case OpCode::Equal: tmp += " =="; break; + case OpCode::NotEqual: tmp += " !="; break; + case OpCode::Not: tmp += " !"; break; + case OpCode::Subtract: tmp += " -"; break; + case OpCode::Add: tmp += " +"; break; + case OpCode::Multiply: tmp += " *"; break; + case OpCode::Divide: tmp += " /"; break; + case OpCode::Modulus: tmp += " %"; break; + case OpCode::UnaryMinus: tmp += " unary-"; break; + default: tmp += " ???"; break; + } + } + qCDebug(animation).nospace().noquote() << "opCodes =" << tmp; + qCDebug(animation).resetFormat(); +} diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index afada44e86..6e204483d5 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -43,6 +43,7 @@ protected: Minus, Plus, Multiply, + Divide, Modulus, Comma, Error @@ -75,8 +76,9 @@ protected: Subtract, Add, Multiply, + Divide, Modulus, - Minus + UnaryMinus }; OpCode(Type type) : type {type} {} OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} @@ -111,8 +113,11 @@ protected: Token consumeLessThan(const QString& str, QString::const_iterator& iter) const; Token consumeNot(const QString& str, QString::const_iterator& iter) const; - bool parseExpression(const QString& str, QString::const_iterator& iter); - bool parseUnaryExpression(const QString& str, QString::const_iterator& iter); + bool parseExpr(const QString& str, QString::const_iterator& iter); + bool parseExprPrime(const QString& str, QString::const_iterator& iter); + bool parseTerm(const QString& str, QString::const_iterator& iter); + bool parseTermPrime(const QString& str, QString::const_iterator& iter); + bool parseFactor(const QString& str, QString::const_iterator& iter); OpCode evaluate(const AnimVariantMap& map) const; void evalAnd(const AnimVariantMap& map, std::stack& stack) const; @@ -126,13 +131,24 @@ protected: void evalNot(const AnimVariantMap& map, std::stack& stack) const; void evalSubtract(const AnimVariantMap& map, std::stack& stack) const; void evalAdd(const AnimVariantMap& map, std::stack& stack) const; + void add(int lhs, const OpCode& rhs, std::stack& stack) const; + void add(float lhs, const OpCode& rhs, std::stack& stack) const; void evalMultiply(const AnimVariantMap& map, std::stack& stack) const; + void mul(int lhs, const OpCode& rhs, std::stack& stack) const; + void mul(float lhs, const OpCode& rhs, std::stack& stack) const; + void evalDivide(const AnimVariantMap& map, std::stack& stack) const; void evalModulus(const AnimVariantMap& map, std::stack& stack) const; - void evalMinus(const AnimVariantMap& map, std::stack& stack) const; + void evalUnaryMinus(const AnimVariantMap& map, std::stack& stack) const; + + OpCode coerseToValue(const AnimVariantMap& map, const OpCode& opCode) const; QString _expression; mutable std::stack _tokenStack; // TODO: remove, only needed during parsing std::vector _opCodes; + +#ifndef NDEBUG + void dumpOpCodes() const; +#endif }; #endif diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 58fd3a1d35..6765553a25 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -366,82 +366,120 @@ void AnimTests::testExpressionParser() { vars.set("five", (float)5.0f); vars.set("forty", (float)40.0f); - AnimExpression e("(!f)"); - QVERIFY(e._opCodes.size() == 2); - if (e._opCodes.size() == 2) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == true); - } - - e = AnimExpression("!!f"); - QVERIFY(e._opCodes.size() == 3); - if (e._opCodes.size() == 3) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == false); - } - - e = AnimExpression("!!(!f)"); - QVERIFY(e._opCodes.size() == 4); - if (e._opCodes.size() == 4) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Not); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == true); - } -/* - e = AnimExpression("f || !f"); - QVERIFY(e._opCodes.size() == 4); - if (e._opCodes.size() == 4) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "f"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[1].strVal == "f"); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Not); - QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Or); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Bool); - QVERIFY((opCode.intVal != 0) == true); - } -*/ - e = AnimExpression("-10"); - QVERIFY(e._opCodes.size() == 2); + AnimExpression e("10"); + QVERIFY(e._opCodes.size() == 1); if (e._opCodes.size() == 1) { QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); QVERIFY(e._opCodes[0].intVal == 10); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Minus); - - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Int); - QVERIFY(opCode.intVal == 10); } - e = AnimExpression("-ten"); - QVERIFY(e._opCodes.size() == 2); + e = AnimExpression("(10)"); + QVERIFY(e._opCodes.size() == 1); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 10); + } + + e = AnimExpression("((10))"); + QVERIFY(e._opCodes.size() == 1); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 10); + } + + e = AnimExpression("12.5"); + QVERIFY(e._opCodes.size() == 1); + if (e._opCodes.size() == 1) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Float); + QVERIFY(e._opCodes[0].floatVal == 12.5f); + } + + e = AnimExpression("twenty"); + QVERIFY(e._opCodes.size() == 1); if (e._opCodes.size() == 1) { QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Identifier); - QVERIFY(e._opCodes[0].strVal == "ten"); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Minus); + QVERIFY(e._opCodes[0].strVal == "twenty"); + } - auto opCode = e.evaluate(vars); - QVERIFY(opCode.type == AnimExpression::OpCode::Int); - QVERIFY(opCode.intVal == 10); + e = AnimExpression("2 + 3"); + QVERIFY(e._opCodes.size() == 3); + if (e._opCodes.size() == 3) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 2); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[1].intVal == 3); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Add); + } + + e = AnimExpression("2 + 3 * 10"); + QVERIFY(e._opCodes.size() == 5); + if (e._opCodes.size() == 5) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 2); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[1].intVal == 3); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[2].intVal == 10); + QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Multiply); + QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::Add); + } + + e = AnimExpression("(2 + 3) * 10"); + QVERIFY(e._opCodes.size() == 5); + if (e._opCodes.size() == 5) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[0].intVal == 2); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[1].intVal == 3); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Add); + QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Int); + QVERIFY(e._opCodes[3].intVal == 10); + QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::Multiply); } } +void AnimTests::testExpressionEvaluator() { + auto vars = AnimVariantMap(); + vars.set("f", false); + vars.set("t", true); + vars.set("ten", (int)10); + vars.set("twenty", (int)20); + vars.set("five", (float)5.0f); + vars.set("forty", (float)40.0f); + + AnimExpression::OpCode result = AnimExpression("10").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 10); + + result = AnimExpression("(10)").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 10); + + result = AnimExpression("2 + 3").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 2 + 3); + + result = AnimExpression("2 + 3 * 5").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == 2 + 3 * 5); + + result = AnimExpression("(2 + 3) * 5").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == (2 + 3) * 5); + + result = AnimExpression("(2 + 3) * (5 + 3)").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == (2 + 3) * (5 + 3)); + + result = AnimExpression("(ten + twenty) * 5").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Int); + QVERIFY(result.intVal == (10 + 20) * 5); + + result = AnimExpression("(ten + twenty) * 5.0").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Float); + QVERIFY(result.floatVal == (10 + 20) * 5.0f); + + result = AnimExpression("five * forty").evaluate(vars); + QVERIFY(result.type == AnimExpression::OpCode::Float); + QVERIFY(result.floatVal == 5.0f * 40.0f); +} diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index 94b3eddd25..a70acc21f7 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -28,6 +28,7 @@ private slots: void testAccumulateTime(); void testExpressionTokenizer(); void testExpressionParser(); + void testExpressionEvaluator(); }; #endif // hifi_AnimTests_h From 22756d168b25af08f004d5b7b8de24dd59134db4 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 15 Dec 2015 10:35:35 -0800 Subject: [PATCH 10/43] Changed grammar to support boolean and and or. --- libraries/animation/src/AnimExpression.cpp | 64 ++++++++++------------ libraries/animation/src/AnimExpression.h | 16 +++--- tests/QTestExtensions.h | 2 +- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index b76aaff3f9..5fc1bc9b4f 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -78,7 +78,15 @@ AnimExpression::Token AnimExpression::consumeIdentifier(const QString& str, QStr } int pos = (int)(begin - str.begin()); int len = (int)(iter - begin); - return Token(QStringRef(const_cast(&str), pos, len)); + + QStringRef stringRef(const_cast(&str), pos, len); + if (stringRef == "true") { + return Token(true); + } else if (stringRef == "false") { + return Token(false); + } else { + return Token(stringRef); + } } // TODO: not very efficient or accruate, but it's close enough for now. @@ -198,19 +206,19 @@ AnimExpression::Token AnimExpression::consumeNot(const QString& str, QString::co /* Expr → Term Expr' -Expr' → + Term Expr' - | – Term Expr' +Expr' → '||' Term Expr' | ε Term → Factor Term' -Term' → * Term' - | / Term' +Term' → '&&' Term' | ε Factor → INT + | BOOL | FLOAT | IDENTIFIER - | (Expr) + | '(' Expr ')' */ +// Expr → Term Expr' bool AnimExpression::parseExpr(const QString& str, QString::const_iterator& iter) { if (!parseTerm(str, iter)) { return false; @@ -221,9 +229,10 @@ bool AnimExpression::parseExpr(const QString& str, QString::const_iterator& iter return true; } +// Expr' → '||' Term Expr' | ε bool AnimExpression::parseExprPrime(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); - if (token.type == Token::Plus) { + if (token.type == Token::Or) { if (!parseTerm(str, iter)) { unconsumeToken(token); return false; @@ -232,18 +241,7 @@ bool AnimExpression::parseExprPrime(const QString& str, QString::const_iterator& unconsumeToken(token); return false; } - _opCodes.push_back(OpCode {OpCode::Add}); - return true; - } else if (token.type == Token::Minus) { - if (!parseTerm(str, iter)) { - unconsumeToken(token); - return false; - } - if (!parseExprPrime(str, iter)) { - unconsumeToken(token); - return false; - } - _opCodes.push_back(OpCode {OpCode::Subtract}); + _opCodes.push_back(OpCode {OpCode::Or}); return true; } else { unconsumeToken(token); @@ -251,6 +249,7 @@ bool AnimExpression::parseExprPrime(const QString& str, QString::const_iterator& } } +// Term → Factor Term' bool AnimExpression::parseTerm(const QString& str, QString::const_iterator& iter) { if (!parseFactor(str, iter)) { return false; @@ -261,9 +260,10 @@ bool AnimExpression::parseTerm(const QString& str, QString::const_iterator& iter return true; } +// Term' → '&&' Term' | ε bool AnimExpression::parseTermPrime(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); - if (token.type == Token::Multiply) { + if (token.type == Token::And) { if (!parseTerm(str, iter)) { unconsumeToken(token); return false; @@ -272,18 +272,7 @@ bool AnimExpression::parseTermPrime(const QString& str, QString::const_iterator& unconsumeToken(token); return false; } - _opCodes.push_back(OpCode {OpCode::Multiply}); - return true; - } else if (token.type == Token::Divide) { - if (!parseTerm(str, iter)) { - unconsumeToken(token); - return false; - } - if (!parseTermPrime(str, iter)) { - unconsumeToken(token); - return false; - } - _opCodes.push_back(OpCode {OpCode::Divide}); + _opCodes.push_back(OpCode {OpCode::And}); return true; } else { unconsumeToken(token); @@ -291,11 +280,15 @@ bool AnimExpression::parseTermPrime(const QString& str, QString::const_iterator& } } +// Factor → INT | BOOL | FLOAT | IDENTIFIER | '(' Expr ')' bool AnimExpression::parseFactor(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); if (token.type == Token::Int) { _opCodes.push_back(OpCode {token.intVal}); return true; + } else if (token.type == Token::Bool) { + _opCodes.push_back(OpCode {(bool)token.intVal}); + return true; } else if (token.type == Token::Float) { _opCodes.push_back(OpCode {token.floatVal}); return true; @@ -351,7 +344,7 @@ AnimExpression::OpCode AnimExpression::evaluate(const AnimVariantMap& map) const case OpCode::UnaryMinus: evalUnaryMinus(map, stack); break; } } - return stack.top(); + return coerseToValue(map, stack.top()); } #define POP_BOOL(NAME) \ @@ -602,8 +595,7 @@ AnimExpression::OpCode AnimExpression::coerseToValue(const AnimVariantMap& map, const AnimVariant& var = map.get(opCode.strVal); switch (var.getType()) { case AnimVariant::Type::Bool: - qCWarning(animation) << "AnimExpression: type missmatch, expected a number not a bool"; - return OpCode(0); + return OpCode((bool)var.getBool()); break; case AnimVariant::Type::Int: return OpCode(var.getInt()); @@ -631,6 +623,7 @@ AnimExpression::OpCode AnimExpression::coerseToValue(const AnimVariantMap& map, } } +#ifndef NDEBUG void AnimExpression::dumpOpCodes() const { QString tmp; for (auto& op : _opCodes) { @@ -660,3 +653,4 @@ void AnimExpression::dumpOpCodes() const { qCDebug(animation).nospace().noquote() << "opCodes =" << tmp; qCDebug(animation).resetFormat(); } +#endif diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 6e204483d5..8a1961b326 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -27,6 +27,7 @@ protected: enum Type { End = 0, Identifier, + Bool, Int, Float, And, @@ -50,8 +51,9 @@ protected: }; Token(Type type) : type {type} {} Token(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} - Token(int val) : type {Type::Int}, intVal {val} {} - Token(float val) : type {Type::Float}, floatVal {val} {} + explicit Token(int val) : type {Type::Int}, intVal {val} {} + explicit Token(bool val) : type {Type::Bool}, intVal {val} {} + explicit Token(float val) : type {Type::Float}, floatVal {val} {} Type type {End}; QString strVal; int intVal {0}; @@ -81,11 +83,11 @@ protected: UnaryMinus }; OpCode(Type type) : type {type} {} - OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} - OpCode(const QString& str) : type {Type::Identifier}, strVal {str} {} - OpCode(int val) : type {Type::Int}, intVal {val} {} - OpCode(bool val) : type {Type::Bool}, intVal {(int)val} {} - OpCode(float val) : type {Type::Float}, floatVal {val} {} + explicit OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {} + explicit OpCode(const QString& str) : type {Type::Identifier}, strVal {str} {} + explicit OpCode(int val) : type {Type::Int}, intVal {val} {} + explicit OpCode(bool val) : type {Type::Bool}, intVal {(int)val} {} + explicit OpCode(float val) : type {Type::Float}, floatVal {val} {} bool coerceBool(const AnimVariantMap& map) const { if (type == Int || type == Bool) { diff --git a/tests/QTestExtensions.h b/tests/QTestExtensions.h index 16e51b41ee..b7b9795a9a 100644 --- a/tests/QTestExtensions.h +++ b/tests/QTestExtensions.h @@ -274,7 +274,7 @@ struct ByteData { QTextStream & operator << (QTextStream& stream, const ByteData & wrapper) { // Print bytes as hex - stream << QByteArray::fromRawData(wrapper.data, wrapper.length).toHex(); + stream << QByteArray::fromRawData(wrapper.data, (int)wrapper.length).toHex(); return stream; } From ab85e2967a9bca036abac6b49c5199fba4d4e54c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 15 Dec 2015 13:18:30 -0800 Subject: [PATCH 11/43] AnimExpression: support for unary not. --- libraries/animation/src/AnimExpression.cpp | 32 +++++++-- libraries/animation/src/AnimExpression.h | 1 + tests/animation/src/AnimTests.cpp | 77 ++++++++-------------- 3 files changed, 53 insertions(+), 57 deletions(-) diff --git a/libraries/animation/src/AnimExpression.cpp b/libraries/animation/src/AnimExpression.cpp index 5fc1bc9b4f..79004a72a6 100644 --- a/libraries/animation/src/AnimExpression.cpp +++ b/libraries/animation/src/AnimExpression.cpp @@ -208,9 +208,11 @@ AnimExpression::Token AnimExpression::consumeNot(const QString& str, QString::co Expr → Term Expr' Expr' → '||' Term Expr' | ε -Term → Factor Term' -Term' → '&&' Term' +Term → Unary Term' +Term' → '&&' Unary Term' | ε +Unary → '!' Unary + | Factor Factor → INT | BOOL | FLOAT @@ -249,9 +251,9 @@ bool AnimExpression::parseExprPrime(const QString& str, QString::const_iterator& } } -// Term → Factor Term' +// Term → Unary Term' bool AnimExpression::parseTerm(const QString& str, QString::const_iterator& iter) { - if (!parseFactor(str, iter)) { + if (!parseUnary(str, iter)) { return false; } if (!parseTermPrime(str, iter)) { @@ -260,11 +262,11 @@ bool AnimExpression::parseTerm(const QString& str, QString::const_iterator& iter return true; } -// Term' → '&&' Term' | ε +// Term' → '&&' Unary Term' | ε bool AnimExpression::parseTermPrime(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); if (token.type == Token::And) { - if (!parseTerm(str, iter)) { + if (!parseUnary(str, iter)) { unconsumeToken(token); return false; } @@ -280,6 +282,24 @@ bool AnimExpression::parseTermPrime(const QString& str, QString::const_iterator& } } +// Unary → '!' Unary | Factor +bool AnimExpression::parseUnary(const QString& str, QString::const_iterator& iter) { + + auto token = consumeToken(str, iter); + if (token.type == Token::Not) { + if (!parseUnary(str, iter)) { + unconsumeToken(token); + return false; + } + _opCodes.push_back(OpCode {OpCode::Not}); + return true; + } + unconsumeToken(token); + + return parseFactor(str, iter); +} + + // Factor → INT | BOOL | FLOAT | IDENTIFIER | '(' Expr ')' bool AnimExpression::parseFactor(const QString& str, QString::const_iterator& iter) { auto token = consumeToken(str, iter); diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 8a1961b326..468217f5b3 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -119,6 +119,7 @@ protected: bool parseExprPrime(const QString& str, QString::const_iterator& iter); bool parseTerm(const QString& str, QString::const_iterator& iter); bool parseTermPrime(const QString& str, QString::const_iterator& iter); + bool parseUnary(const QString& str, QString::const_iterator& iter); bool parseFactor(const QString& str, QString::const_iterator& iter); OpCode evaluate(const AnimVariantMap& map) const; diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 63eb39dc48..6812bb63b6 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -522,43 +522,19 @@ void AnimTests::testExpressionParser() { QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::And); } - /* - e = AnimExpression("2 + 3"); - QVERIFY(e._opCodes.size() == 3); - if (e._opCodes.size() == 3) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[0].intVal == 2); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[1].intVal == 3); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Add); + e = AnimExpression("!(true || false) && true"); + QVERIFY(e._opCodes.size() == 6); + if (e._opCodes.size() == 6) { + QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Bool); + QVERIFY(e._opCodes[0].intVal == (int)true); + QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Bool); + QVERIFY(e._opCodes[1].intVal == (int)false); + QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Or); + QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Not); + QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::Bool); + QVERIFY(e._opCodes[4].intVal == (int)true); + QVERIFY(e._opCodes[5].type == AnimExpression::OpCode::And); } - - e = AnimExpression("2 + 3 * 10"); - QVERIFY(e._opCodes.size() == 5); - if (e._opCodes.size() == 5) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[0].intVal == 2); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[1].intVal == 3); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[2].intVal == 10); - QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Multiply); - QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::Add); - } - - e = AnimExpression("(2 + 3) * 10"); - QVERIFY(e._opCodes.size() == 5); - if (e._opCodes.size() == 5) { - QVERIFY(e._opCodes[0].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[0].intVal == 2); - QVERIFY(e._opCodes[1].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[1].intVal == 3); - QVERIFY(e._opCodes[2].type == AnimExpression::OpCode::Add); - QVERIFY(e._opCodes[3].type == AnimExpression::OpCode::Int); - QVERIFY(e._opCodes[3].intVal == 10); - QVERIFY(e._opCodes[4].type == AnimExpression::OpCode::Multiply); - } - */ } #define TEST_BOOL_EXPR(EXPR) \ @@ -630,23 +606,22 @@ void AnimTests::testExpressionEvaluator() { TEST_BOOL_EXPR(f || false); TEST_BOOL_EXPR(f || true); -/* - result = AnimExpression("(2 + 3) * (5 + 3)").evaluate(vars); - QVERIFY(result.type == AnimExpression::OpCode::Int); - QVERIFY(result.intVal == (2 + 3) * (5 + 3)); + TEST_BOOL_EXPR(!true); + TEST_BOOL_EXPR(!false); + TEST_BOOL_EXPR(!true || true); - result = AnimExpression("(ten + twenty) * 5").evaluate(vars); - QVERIFY(result.type == AnimExpression::OpCode::Int); - QVERIFY(result.intVal == (10 + 20) * 5); + TEST_BOOL_EXPR(!true && !false || !true); + TEST_BOOL_EXPR(!true && !false || true); + TEST_BOOL_EXPR(!true && false || !true); + TEST_BOOL_EXPR(!true && false || true); + TEST_BOOL_EXPR(true && !false || !true); + TEST_BOOL_EXPR(true && !false || true); + TEST_BOOL_EXPR(true && false || !true); + TEST_BOOL_EXPR(true && false || true); - result = AnimExpression("(ten + twenty) * 5.0").evaluate(vars); - QVERIFY(result.type == AnimExpression::OpCode::Float); - QVERIFY(result.floatVal == (10 + 20) * 5.0f); - - result = AnimExpression("five * forty").evaluate(vars); - QVERIFY(result.type == AnimExpression::OpCode::Float); - QVERIFY(result.floatVal == 5.0f * 40.0f); -*/ + TEST_BOOL_EXPR(!(true && f) || !t); + TEST_BOOL_EXPR(!!!(t) && (!!f || true)); + TEST_BOOL_EXPR(!(true && f) && true); } From b25fc5be6cfde2bf383b2050693ff00da92f2452 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 17 Dec 2015 09:52:34 +1300 Subject: [PATCH 12/43] Don't send a domain-server check in when shutting down --- libraries/networking/src/NodeList.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index e591ee8a31..bc79ddd5fd 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -234,6 +234,7 @@ void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes) void NodeList::sendDomainServerCheckIn() { if (_isShuttingDown) { qCDebug(networking) << "Refusing to send a domain-server check in while shutting down."; + return; } if (_publicSockAddr.isNull()) { From 736977a4c2fd464a9ea10e05a67ca1d6ecda9085 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 18 Dec 2015 08:16:39 -0600 Subject: [PATCH 13/43] Using correct Icons for PR vs Relesae --- cmake/macros/ConsolidateStackComponents.cmake | 4 ++-- cmake/macros/IncludeApplicationVersion.cmake | 6 ++++++ stack-manager/assets/icon-beta.ico | Bin 0 -> 370070 bytes stack-manager/assets/icon-beta.png | Bin 0 -> 17905 bytes 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 stack-manager/assets/icon-beta.ico create mode 100644 stack-manager/assets/icon-beta.png diff --git a/cmake/macros/ConsolidateStackComponents.cmake b/cmake/macros/ConsolidateStackComponents.cmake index ca272f6485..33b50472bf 100644 --- a/cmake/macros/ConsolidateStackComponents.cmake +++ b/cmake/macros/ConsolidateStackComponents.cmake @@ -11,10 +11,10 @@ macro(CONSOLIDATE_STACK_COMPONENTS) # Copy icon files for interface and stack manager if (TARGET_NAME STREQUAL "interface" OR TARGET_NAME STREQUAL "stack-manager") if (TARGET_NAME STREQUAL "interface") - set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/icon/interface.ico") + set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/icon/${INTERFACE_ICON}") set (ICON_DESTINATION_NAME "interface.ico") elseif (TARGET_NAME STREQUAL "stack-manager") - set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/assets/icon.ico") + set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/assets/${STACK_MANAGER_ICON}") set (ICON_DESTINATION_NAME "stack-manager.ico") endif () add_custom_command( diff --git a/cmake/macros/IncludeApplicationVersion.cmake b/cmake/macros/IncludeApplicationVersion.cmake index 753a12a01c..a91f30f6cf 100644 --- a/cmake/macros/IncludeApplicationVersion.cmake +++ b/cmake/macros/IncludeApplicationVersion.cmake @@ -19,17 +19,23 @@ macro(INCLUDE_APPLICATION_VERSION) set(INSTALLER_COMPANY "High Fidelity") set(INSTALLER_DIRECTORY "${INSTALLER_COMPANY}") set(INSTALLER_NAME "interface-win64-${BUILD_SEQ}.exe") + set(INTERFACE_ICON "interface.ico") + set(STACK_MANAGER_ICON "icon.ico") elseif (DEFINED ENV{ghprbPullId}) set(DEPLOY_PACKAGE 1) set(BUILD_SEQ "PR-$ENV{ghprbPullId}") set(INSTALLER_COMPANY "High Fidelity - PR") set(INSTALLER_DIRECTORY "${INSTALLER_COMPANY}\\${BUILD_SEQ}") set(INSTALLER_NAME "pr-interface-win64-${BUILD_SEQ}.exe") + set(INTERFACE_ICON "interface-beta.ico") + set(STACK_MANAGER_ICON "icon-beta.ico") else () set(BUILD_SEQ "dev") set(INSTALLER_COMPANY "High Fidelity - Dev") set(INSTALLER_DIRECTORY "${INSTALLER_COMPANY}") set(INSTALLER_NAME "dev-interface-win64.exe") + set(INTERFACE_ICON "interface-beta.ico") + set(STACK_MANAGER_ICON "icon-beta.ico") endif () configure_file("${MACRO_DIR}/ApplicationVersion.h.in" "${PROJECT_BINARY_DIR}/includes/ApplicationVersion.h") include_directories("${PROJECT_BINARY_DIR}/includes") diff --git a/stack-manager/assets/icon-beta.ico b/stack-manager/assets/icon-beta.ico new file mode 100644 index 0000000000000000000000000000000000000000..720fd09a2cf99d3f3e9cf302a1b52950dbba66a9 GIT binary patch literal 370070 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 SNJvOXNJvOXNJvOxFaICz9f0%z literal 0 HcmV?d00001 diff --git a/stack-manager/assets/icon-beta.png b/stack-manager/assets/icon-beta.png new file mode 100644 index 0000000000000000000000000000000000000000..50f4abf4e7115a0347680f52c0af5d84c5073739 GIT binary patch literal 17905 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 TFAKSS00000NkvXXu0mjfAA2NA literal 0 HcmV?d00001 From c1bac2f9bd61df4841e03bf8f5a550a30a6f80da Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Fri, 18 Dec 2015 08:47:47 -0600 Subject: [PATCH 14/43] Fixing uninstall record --- interface/CMakeLists.txt | 3 +++ tools/nsis/release.nsi | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 87cf1a384c..e890096439 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -199,6 +199,9 @@ else (APPLE) COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PROJECT_SOURCE_DIR}/resources" $/resources + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${CMAKE_SOURCE_DIR}/examples" + $/scripts ) # link target to external libraries diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index 9bc75e5274..c5a568f165 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -88,7 +88,7 @@ Section "Registry Entries and Procotol Handler" SEC02 SectionIn RO WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR" - WriteRegStr HKLM "${uninstkey}" "DisplayName" "${company} (remove only)" + WriteRegStr HKLM "${uninstkey}" "DisplayName" "${install_directory} (remove only)" WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$INSTDIR\${uninstaller}"' WriteRegStr HKCR "${company}\Shell\open\command\" "" '"$INSTDIR\${interface_exec} "%1"' WriteRegStr HKCR "${company}\DefaultIcon" "" "$INSTDIR\${interface_icon}" From dcde640acdde332a861b6d8d9117855bf1685360 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 18 Dec 2015 19:13:59 -0800 Subject: [PATCH 15/43] Stub Perception Neuron input plugin --- interface/resources/controllers/neuron.json | 7 ++ plugins/neuron/CMakeLists.txt | 13 ++++ plugins/neuron/src/NeuronPlugin.cpp | 75 +++++++++++++++++++++ plugins/neuron/src/NeuronPlugin.h | 57 ++++++++++++++++ plugins/neuron/src/NeuronProvider.cpp | 45 +++++++++++++ plugins/neuron/src/plugin.json | 1 + 6 files changed, 198 insertions(+) create mode 100644 interface/resources/controllers/neuron.json create mode 100644 plugins/neuron/CMakeLists.txt create mode 100644 plugins/neuron/src/NeuronPlugin.cpp create mode 100644 plugins/neuron/src/NeuronPlugin.h create mode 100644 plugins/neuron/src/NeuronProvider.cpp create mode 100644 plugins/neuron/src/plugin.json diff --git a/interface/resources/controllers/neuron.json b/interface/resources/controllers/neuron.json new file mode 100644 index 0000000000..2d61f80c35 --- /dev/null +++ b/interface/resources/controllers/neuron.json @@ -0,0 +1,7 @@ +{ + "name": "Neuron to Standard", + "channels": [ + { "from": "Hydra.LeftHand", "to": "Standard.LeftHand" }, + { "from": "Hydra.RightHand", "to": "Standard.RightHand" } + ] +} diff --git a/plugins/neuron/CMakeLists.txt b/plugins/neuron/CMakeLists.txt new file mode 100644 index 0000000000..b86d310ab7 --- /dev/null +++ b/plugins/neuron/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Created by Anthony Thibault on 2015/12/18 +# Copyright 2015 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 +# + +set(TARGET_NAME neuron) +setup_hifi_plugin(Script Qml Widgets) +link_hifi_libraries(shared controllers plugins input-plugins) +# target_neuron() + diff --git a/plugins/neuron/src/NeuronPlugin.cpp b/plugins/neuron/src/NeuronPlugin.cpp new file mode 100644 index 0000000000..735b81a1ef --- /dev/null +++ b/plugins/neuron/src/NeuronPlugin.cpp @@ -0,0 +1,75 @@ +// +// NeuronPlugin.h +// input-plugins/src/input-plugins +// +// Created by Anthony Thibault on 12/18/2015. +// Copyright 2015 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 "NeuronPlugin.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(inputplugins) +Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") + +const QString NeuronPlugin::NAME = "Neuron"; +const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; + +bool NeuronPlugin::isSupported() const { + // TODO: + return true; +} + +void NeuronPlugin::activate() { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::activate"; +} + +void NeuronPlugin::deactivate() { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::deactivate"; +} + +void NeuronPlugin::pluginUpdate(float deltaTime, bool jointsCaptured) { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::pluginUpdate"; +} + +void NeuronPlugin::saveSettings() const { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::saveSettings"; +} + +void NeuronPlugin::loadSettings() { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::loadSettings"; +} + +controller::Input::NamedVector NeuronPlugin::InputDevice::getAvailableInputs() const { + // TODO: + static const controller::Input::NamedVector availableInputs { + makePair(controller::LEFT_HAND, "LeftHand"), + makePair(controller::RIGHT_HAND, "RightHand") + }; + return availableInputs; +} + +QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { + static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/neuron.json"; + return MAPPING_JSON; +} + +void NeuronPlugin::InputDevice::update(float deltaTime, bool jointsCaptured) { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::InputDevice::update"; +} + +void NeuronPlugin::InputDevice::focusOutEvent() { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::InputDevice::focusOutEvent"; +} diff --git a/plugins/neuron/src/NeuronPlugin.h b/plugins/neuron/src/NeuronPlugin.h new file mode 100644 index 0000000000..59e0f0a393 --- /dev/null +++ b/plugins/neuron/src/NeuronPlugin.h @@ -0,0 +1,57 @@ +// +// NeuronPlugin.h +// input-plugins/src/input-plugins +// +// Created by Anthony Thibault on 12/18/2015. +// Copyright 2015 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_NeuronPlugin_h +#define hifi_NeuronPlugin_h + +#include +#include +#include + +// Handles interaction with the Neuron SDK +class NeuronPlugin : public InputPlugin { + Q_OBJECT +public: + // Plugin functions + virtual bool isSupported() const override; + virtual bool isJointController() const override { return true; } + const QString& getName() const override { return NAME; } + const QString& getID() const override { return NEURON_ID_STRING; } + + virtual void activate() override; + virtual void deactivate() override; + + virtual void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } + virtual void pluginUpdate(float deltaTime, bool jointsCaptured) override; + + virtual void saveSettings() const override; + virtual void loadSettings() override; + +private: + class InputDevice : public controller::InputDevice { + public: + InputDevice() : controller::InputDevice("Neuron") {} + + // Device functions + virtual controller::Input::NamedVector getAvailableInputs() const override; + virtual QString getDefaultMappingConfig() const override; + virtual void update(float deltaTime, bool jointsCaptured) override; + virtual void focusOutEvent() override; + }; + + std::shared_ptr _inputDevice { std::make_shared() }; + + static const QString NAME; + static const QString NEURON_ID_STRING; +}; + +#endif // hifi_NeuronPlugin_h + diff --git a/plugins/neuron/src/NeuronProvider.cpp b/plugins/neuron/src/NeuronProvider.cpp new file mode 100644 index 0000000000..b171c5150d --- /dev/null +++ b/plugins/neuron/src/NeuronProvider.cpp @@ -0,0 +1,45 @@ +// +// Created by Anthony Thibault on 2015/12/18 +// Copyright 2015 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 + +#include +#include +#include + +#include +#include + +#include "NeuronPlugin.h" + +class NeuronProvider : public QObject, public InputProvider +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID InputProvider_iid FILE "plugin.json") + Q_INTERFACES(InputProvider) + +public: + NeuronProvider(QObject* parent = nullptr) : QObject(parent) {} + virtual ~NeuronProvider() {} + + virtual InputPluginList getInputPlugins() override { + static std::once_flag once; + std::call_once(once, [&] { + InputPluginPointer plugin(new NeuronPlugin()); + if (plugin->isSupported()) { + _inputPlugins.push_back(plugin); + } + }); + return _inputPlugins; + } + +private: + InputPluginList _inputPlugins; +}; + +#include "NeuronProvider.moc" diff --git a/plugins/neuron/src/plugin.json b/plugins/neuron/src/plugin.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/plugins/neuron/src/plugin.json @@ -0,0 +1 @@ +{} From 0459479c2b1a8130beecee5771d40aac490d1a9f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 21 Dec 2015 18:30:15 -0800 Subject: [PATCH 16/43] NeuronPlugin: Added external project for Neuron SDK Now builds on windows with actual Neuron SDK. Can to TCP server on localhost, and receive joint data. Will debug draw joint 6, (left foot?) --- cmake/externals/neuron/CMakeLists.txt | 49 ++++ cmake/macros/TargetNeuron.cmake | 14 ++ cmake/modules/FindNeuron.cmake | 28 +++ interface/CMakeLists.txt | 4 +- plugins/{neuron => hifiNeuron}/CMakeLists.txt | 4 +- plugins/hifiNeuron/src/NeuronPlugin.cpp | 222 ++++++++++++++++++ .../{neuron => hifiNeuron}/src/NeuronPlugin.h | 19 +- .../src/NeuronProvider.cpp | 0 .../{neuron => hifiNeuron}/src/plugin.json | 0 plugins/neuron/src/NeuronPlugin.cpp | 75 ------ 10 files changed, 336 insertions(+), 79 deletions(-) create mode 100644 cmake/externals/neuron/CMakeLists.txt create mode 100644 cmake/macros/TargetNeuron.cmake create mode 100644 cmake/modules/FindNeuron.cmake rename plugins/{neuron => hifiNeuron}/CMakeLists.txt (88%) create mode 100644 plugins/hifiNeuron/src/NeuronPlugin.cpp rename plugins/{neuron => hifiNeuron}/src/NeuronPlugin.h (79%) rename plugins/{neuron => hifiNeuron}/src/NeuronProvider.cpp (100%) rename plugins/{neuron => hifiNeuron}/src/plugin.json (100%) delete mode 100644 plugins/neuron/src/NeuronPlugin.cpp diff --git a/cmake/externals/neuron/CMakeLists.txt b/cmake/externals/neuron/CMakeLists.txt new file mode 100644 index 0000000000..324b3fb917 --- /dev/null +++ b/cmake/externals/neuron/CMakeLists.txt @@ -0,0 +1,49 @@ +include(ExternalProject) + +set(EXTERNAL_NAME neuron) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +set(NEURON_URL "https://s3.amazonaws.com/hifi-public/dependencies/neuron_datareader_b.12.zip") +set(NEURON_URL_MD5 "0ab54ca04c9cc8094e0fa046c226e574") + +ExternalProject_Add(${EXTERNAL_NAME} + URL ${NEURON_URL} + URL_MD5 ${NEURON_URL_MD5} + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1) + +ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) + +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +# set include dir +if(WIN32) + set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS "${SOURCE_DIR}/NeuronDataReader_Windows/include" CACHE TYPE INTERNAL) +elseif(APPLE) + set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS "${SOURCE_DIR}/NeuronDataReader_Mac/include" CACHE TYPE INTERNAL) +else() + # Unsupported +endif() + +if(WIN32) + + if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + set(ARCH_DIR "x64") + else() + set(ARCH_DIR "x86") + endif() + + set(${EXTERNAL_NAME_UPPER}_LIB_PATH "${SOURCE_DIR}/NeuronDataReader_Windows/lib/${ARCH_DIR}") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE "${${EXTERNAL_NAME_UPPER}_LIB_PATH}/NeuronDataReader.lib" CACHE TYPE INTERNAL) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES "${${EXTERNAL_NAME_UPPER}_LIB_PATH}/NeuronDataReader.lib" CACHE TYPE INTERNAL) + + add_paths_to_fixup_libs("${${EXTERNAL_NAME_UPPER}_LIB_PATH}") +elseif(APPLE) + # TODO +else() + # UNSUPPORTED +endif() + diff --git a/cmake/macros/TargetNeuron.cmake b/cmake/macros/TargetNeuron.cmake new file mode 100644 index 0000000000..01891ef525 --- /dev/null +++ b/cmake/macros/TargetNeuron.cmake @@ -0,0 +1,14 @@ +# +# Copyright 2015 High Fidelity, Inc. +# Created by Anthony J. Thibault on 2015/12/21 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_NEURON) + add_dependency_external_projects(neuron) + find_package(Neuron REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${NEURON_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${NEURON_LIBRARIES}) + add_definitions(-DHAVE_NEURON) +endmacro() diff --git a/cmake/modules/FindNeuron.cmake b/cmake/modules/FindNeuron.cmake new file mode 100644 index 0000000000..6a93b3a016 --- /dev/null +++ b/cmake/modules/FindNeuron.cmake @@ -0,0 +1,28 @@ +# +# FindNeuron.cmake +# +# Try to find the Perception Neuron SDK +# +# You must provide a NEURON_ROOT_DIR which contains lib and include directories +# +# Once done this will define +# +# NEURON_FOUND - system found Neuron SDK +# NEURON_INCLUDE_DIRS - the Neuron SDK include directory +# NEURON_LIBRARIES - Link this to use Neuron +# +# Created on 12/21/2015 by Anthony J. Thibault +# Copyright 2015 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(SelectLibraryConfigurations) +select_library_configurations(NEURON) + +set(NEURON_REQUIREMENTS NEURON_INCLUDE_DIRS NEURON_LIBRARIES) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Neuron DEFAULT_MSG NEURON_INCLUDE_DIRS NEURON_LIBRARIES) +mark_as_advanced(NEURON_LIBRARIES NEURON_INCLUDE_DIRS NEURON_SEARCH_DIRS) + diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 1d9557a835..5d96b95624 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -109,7 +109,9 @@ add_dependency_external_projects(sdl2) if (WIN32) add_dependency_external_projects(OpenVR) endif() - +if(WIN32 OR APPLE) + add_dependency_external_projects(neuron) +endif() # disable /OPT:REF and /OPT:ICF for the Debug builds # This will prevent the following linker warnings diff --git a/plugins/neuron/CMakeLists.txt b/plugins/hifiNeuron/CMakeLists.txt similarity index 88% rename from plugins/neuron/CMakeLists.txt rename to plugins/hifiNeuron/CMakeLists.txt index b86d310ab7..9c512fc877 100644 --- a/plugins/neuron/CMakeLists.txt +++ b/plugins/hifiNeuron/CMakeLists.txt @@ -6,8 +6,8 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -set(TARGET_NAME neuron) +set(TARGET_NAME hifiNeuron) setup_hifi_plugin(Script Qml Widgets) link_hifi_libraries(shared controllers plugins input-plugins) -# target_neuron() +target_neuron() diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp new file mode 100644 index 0000000000..c3f764da05 --- /dev/null +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -0,0 +1,222 @@ +// +// NeuronPlugin.h +// input-plugins/src/input-plugins +// +// Created by Anthony Thibault on 12/18/2015. +// Copyright 2015 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 "NeuronPlugin.h" + +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(inputplugins) +Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") + +#define __OS_XUN__ 1 +#define BOOL int +#include + +const QString NeuronPlugin::NAME = "Neuron"; +const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; + +enum JointIndex { + HipsPosition = 0, + Hips, + RightUpLeg, + RightLeg, + RightFoot, + LeftUpLeg, + LeftLeg, + LeftFoot, + Spine, + Spine1, + Spine2, + Spine3, + Neck, + Head, + RightShoulder, + RightArm, + RightForeArm, + RightHand, + RightHandThumb1, + RightHandThumb2, + RightHandThumb3, + RightInHandIndex, + RightHandIndex1, + RightHandIndex2, + RightHandIndex3, + RightInHandMiddle, + RightHandMiddle1, + RightHandMiddle2, + RightHandMiddle3, + RightInHandRing, + RightHandRing1, + RightHandRing2, + RightHandRing3, + RightInHandPinky, + RightHandPinky1, + RightHandPinky2, + RightHandPinky3, + LeftShoulder, + LeftArm, + LeftForeArm, + LeftHand, + LeftHandThumb1, + LeftHandThumb2, + LeftHandThumb3, + LeftInHandIndex, + LeftHandIndex1, + LeftHandIndex2, + LeftHandIndex3, + LeftInHandMiddle, + LeftHandMiddle1, + LeftHandMiddle2, + LeftHandMiddle3, + LeftInHandRing, + LeftHandRing1, + LeftHandRing2, + LeftHandRing3, + LeftInHandPinky, + LeftHandPinky1, + LeftHandPinky2, + LeftHandPinky3 +}; + +bool NeuronPlugin::isSupported() const { + // Because it's a client/server network architecture, we can't tell + // if the neuron is actually connected until we connect to the server. + return true; +} + +// NOTE: must be thread-safe +void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx* header, float* data) { + qCDebug(inputplugins) << "NeuronPlugin: received frame data, DataCount = " << header->DataCount; + + auto neuronPlugin = reinterpret_cast(context); + std::lock_guard guard(neuronPlugin->_jointsMutex); + + // Data is 6 floats: 3 position values, 3 rotation euler angles (degrees) + + // resize vector if necessary + const size_t NUM_FLOATS_PER_JOINT = 6; + const size_t NUM_JOINTS = header->DataCount / NUM_FLOATS_PER_JOINT; + if (neuronPlugin->_joints.size() != NUM_JOINTS) { + neuronPlugin->_joints.resize(NUM_JOINTS, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); + } + + assert(sizeof(NeuronPlugin::NeuronJoint) == (NUM_FLOATS_PER_JOINT * sizeof(float))); + + // copy the data + memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); +} + +// NOTE: must be thread-safe +static void CommandDataReceivedCallback(void* context, SOCKET_REF sender, CommandPack* pack, void* data) { + +} + +// NOTE: must be thread-safe +static void SocketStatusChangedCallback(void* context, SOCKET_REF sender, SocketStatus status, char* message) { + qCDebug(inputplugins) << "NeuronPlugin: socket status = " << message; +} + +void NeuronPlugin::activate() { + InputPlugin::activate(); + qCDebug(inputplugins) << "NeuronPlugin::activate"; + + // register c-style callbacks + BRRegisterFrameDataCallback((void*)this, FrameDataReceivedCallback); + BRRegisterCommandDataCallback((void*)this, CommandDataReceivedCallback); + BRRegisterSocketStatusCallback((void*)this, SocketStatusChangedCallback); + + // TODO: pull these from prefs! + _serverAddress = "localhost"; + _serverPort = 7001; + _socketRef = BRConnectTo((char*)_serverAddress.c_str(), _serverPort); + if (!_socketRef) { + // error + qCCritical(inputplugins) << "NeuronPlugin: error connecting to " << _serverAddress.c_str() << ":" << _serverPort << "error = " << BRGetLastErrorMessage(); + } + qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; +} + +void NeuronPlugin::deactivate() { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::deactivate"; + + if (_socketRef) { + BRCloseSocket(_socketRef); + } + InputPlugin::deactivate(); +} + +// convert between euler in degrees to quaternion +static quat eulerToQuat(vec3 euler) { + return (glm::angleAxis(euler.y * RADIANS_PER_DEGREE, Vectors::UNIT_Y) * + glm::angleAxis(euler.x * RADIANS_PER_DEGREE, Vectors::UNIT_X) * + glm::angleAxis(euler.z * RADIANS_PER_DEGREE, Vectors::UNIT_Z)); +} + +void NeuronPlugin::pluginUpdate(float deltaTime, bool jointsCaptured) { + + std::vector joints; + // copy the shared data + { + std::lock_guard guard(_jointsMutex); + joints = _joints; + } + + DebugDraw::getInstance().addMyAvatarMarker("LEFT_FOOT", + eulerToQuat(joints[6].rot), + joints[6].pos / 100.0f, + glm::vec4(1)); + + _inputDevice->update(deltaTime, jointsCaptured); +} + +void NeuronPlugin::saveSettings() const { + InputPlugin::saveSettings(); + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::saveSettings"; +} + +void NeuronPlugin::loadSettings() { + InputPlugin::loadSettings(); + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::loadSettings"; +} + +// +// InputDevice +// + +controller::Input::NamedVector NeuronPlugin::InputDevice::getAvailableInputs() const { + // TODO: + static const controller::Input::NamedVector availableInputs { + makePair(controller::LEFT_HAND, "LeftHand"), + makePair(controller::RIGHT_HAND, "RightHand") + }; + return availableInputs; +} + +QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { + static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/neuron.json"; + return MAPPING_JSON; +} + +void NeuronPlugin::InputDevice::update(float deltaTime, bool jointsCaptured) { + +} + +void NeuronPlugin::InputDevice::focusOutEvent() { + // TODO: + qCDebug(inputplugins) << "NeuronPlugin::InputDevice::focusOutEvent"; +} diff --git a/plugins/neuron/src/NeuronPlugin.h b/plugins/hifiNeuron/src/NeuronPlugin.h similarity index 79% rename from plugins/neuron/src/NeuronPlugin.h rename to plugins/hifiNeuron/src/NeuronPlugin.h index 59e0f0a393..5f67502e04 100644 --- a/plugins/neuron/src/NeuronPlugin.h +++ b/plugins/hifiNeuron/src/NeuronPlugin.h @@ -16,10 +16,15 @@ #include #include +struct _BvhDataHeaderEx; +void FrameDataReceivedCallback(void* context, void* sender, _BvhDataHeaderEx* header, float* data); + // Handles interaction with the Neuron SDK class NeuronPlugin : public InputPlugin { Q_OBJECT public: + friend void FrameDataReceivedCallback(void* context, void* sender, _BvhDataHeaderEx* header, float* data); + // Plugin functions virtual bool isSupported() const override; virtual bool isJointController() const override { return true; } @@ -35,7 +40,7 @@ public: virtual void saveSettings() const override; virtual void loadSettings() override; -private: +protected: class InputDevice : public controller::InputDevice { public: InputDevice() : controller::InputDevice("Neuron") {} @@ -51,6 +56,18 @@ private: static const QString NAME; static const QString NEURON_ID_STRING; + + std::string _serverAddress; + int _serverPort; + void* _socketRef; + + struct NeuronJoint { + glm::vec3 pos; + glm::vec3 rot; + }; + + std::vector _joints; + std::mutex _jointsMutex; }; #endif // hifi_NeuronPlugin_h diff --git a/plugins/neuron/src/NeuronProvider.cpp b/plugins/hifiNeuron/src/NeuronProvider.cpp similarity index 100% rename from plugins/neuron/src/NeuronProvider.cpp rename to plugins/hifiNeuron/src/NeuronProvider.cpp diff --git a/plugins/neuron/src/plugin.json b/plugins/hifiNeuron/src/plugin.json similarity index 100% rename from plugins/neuron/src/plugin.json rename to plugins/hifiNeuron/src/plugin.json diff --git a/plugins/neuron/src/NeuronPlugin.cpp b/plugins/neuron/src/NeuronPlugin.cpp deleted file mode 100644 index 735b81a1ef..0000000000 --- a/plugins/neuron/src/NeuronPlugin.cpp +++ /dev/null @@ -1,75 +0,0 @@ -// -// NeuronPlugin.h -// input-plugins/src/input-plugins -// -// Created by Anthony Thibault on 12/18/2015. -// Copyright 2015 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 "NeuronPlugin.h" - -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(inputplugins) -Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") - -const QString NeuronPlugin::NAME = "Neuron"; -const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; - -bool NeuronPlugin::isSupported() const { - // TODO: - return true; -} - -void NeuronPlugin::activate() { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::activate"; -} - -void NeuronPlugin::deactivate() { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::deactivate"; -} - -void NeuronPlugin::pluginUpdate(float deltaTime, bool jointsCaptured) { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::pluginUpdate"; -} - -void NeuronPlugin::saveSettings() const { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::saveSettings"; -} - -void NeuronPlugin::loadSettings() { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::loadSettings"; -} - -controller::Input::NamedVector NeuronPlugin::InputDevice::getAvailableInputs() const { - // TODO: - static const controller::Input::NamedVector availableInputs { - makePair(controller::LEFT_HAND, "LeftHand"), - makePair(controller::RIGHT_HAND, "RightHand") - }; - return availableInputs; -} - -QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { - static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/neuron.json"; - return MAPPING_JSON; -} - -void NeuronPlugin::InputDevice::update(float deltaTime, bool jointsCaptured) { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::InputDevice::update"; -} - -void NeuronPlugin::InputDevice::focusOutEvent() { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::InputDevice::focusOutEvent"; -} From 00b47cacea4b3af8f8d743f32f1db6b2d828dacc Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 22 Dec 2015 09:59:57 -0800 Subject: [PATCH 17/43] Fix crash when getting MyAvatar.sessionUUID from AvatarManager The previous code inadvertently added a default constructed shared pointer to the avatar hash, causing it to crash when dereferencing it in the update loop. --- interface/src/avatar/AvatarManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 312742e778..217cd28e61 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -310,8 +310,8 @@ QVector AvatarManager::getAvatarIdentifiers() { } AvatarData* AvatarManager::getAvatar(QUuid avatarID) { - QReadLocker locker(&_hashLock); - return _avatarHash[avatarID].get(); // Non-obvious: A bogus avatarID answers your own avatar. + // Null/Default-constructed QUuids will return MyAvatar + return getAvatarBySessionID(avatarID).get(); } From 6afe3bae5ecfe0bb7096391a29e8d7ce26326591 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 22 Dec 2015 17:21:33 -0800 Subject: [PATCH 18/43] Copy Neuron joints into controller poses This makes the accessible for controller mapping and to JavaScript. Added 'neuronAvatar.js' as an example of reading joints from the neuron and setting them on the avatar. NOTE: the rotations are currently in the wrong coordinate frame. --- examples/controllers/neuron/neuronAvatar.js | 141 ++++++++++++ libraries/animation/src/Rig.cpp | 6 +- .../src/controllers/StandardControls.h | 61 +++++- plugins/hifiNeuron/src/NeuronPlugin.cpp | 205 ++++++++++++++++-- plugins/hifiNeuron/src/NeuronPlugin.h | 21 +- 5 files changed, 400 insertions(+), 34 deletions(-) create mode 100644 examples/controllers/neuron/neuronAvatar.js diff --git a/examples/controllers/neuron/neuronAvatar.js b/examples/controllers/neuron/neuronAvatar.js new file mode 100644 index 0000000000..a7146e0759 --- /dev/null +++ b/examples/controllers/neuron/neuronAvatar.js @@ -0,0 +1,141 @@ +// maps controller joint names to avatar joint names +var JOINT_NAME_MAP = { + HipsPosition: "", + Hips: "Hips", + RightUpLeg: "RightUpLeg", + RightLeg: "RightLeg", + RightFoot: "RightFoot", + LeftUpLeg: "LeftUpLeg", + LeftLeg: "LeftLeg", + LeftFoot: "LeftFoot", + Spine: "Spine", + Spine1: "Spine1", + Spine2: "Spine2", + Spine3: "Spine3", + Neck: "Neck", + Head: "Head", + RightShoulder: "RightShoulder", + RightArm: "RightArm", + RightForeArm: "RightForeArm", + RightHand: "RightHand", + RightHandThumb1: "RightHandThumb2", + RightHandThumb2: "RightHandThumb3", + RightHandThumb3: "RightHandThumb4", + RightInHandIndex: "RightHandIndex1", + RightHandIndex1: "RightHandIndex2", + RightHandIndex2: "RightHandIndex3", + RightHandIndex3: "RightHandIndex4", + RightInHandMiddle: "RightHandMiddle1", + RightHandMiddle1: "RightHandMiddle2", + RightHandMiddle2: "RightHandMiddle3", + RightHandMiddle3: "RightHandMiddle4", + RightInHandRing: "RightHandRing1", + RightHandRing1: "RightHandRing2", + RightHandRing2: "RightHandRing3", + RightHandRing3: "RightHandRing4", + RightInHandPinky: "RightHandPinky1", + RightHandPinky1: "RightHandPinky2", + RightHandPinky2: "RightHandPinky3", + RightHandPinky3: "RightHandPinky4", + LeftShoulder: "LeftShoulder", + LeftArm: "LeftArm", + LeftForeArm: "LeftForeArm", + LeftHand: "LeftHand", + LeftHandThumb1: "LeftHandThumb2", + LeftHandThumb2: "LeftHandThumb3", + LeftHandThumb3: "LeftHandThumb4", + LeftInHandIndex: "LeftHandIndex1", + LeftHandIndex1: "LeftHandIndex2", + LeftHandIndex2: "LeftHandIndex3", + LeftHandIndex3: "LeftHandIndex4", + LeftInHandMiddle: "LeftHandMiddle1", + LeftHandMiddle1: "LeftHandMiddle2", + LeftHandMiddle2: "LeftHandMiddle3", + LeftHandMiddle3: "LeftHandMiddle4", + LeftInHandRing: "LeftHandRing1", + LeftHandRing1: "LeftHandRing2", + LeftHandRing2: "LeftHandRing3", + LeftHandRing3: "LeftHandRing4", + LeftInHandPinky: "LeftHandPinky1", + LeftHandPinky1: "LeftHandPinky2", + LeftHandPinky2: "LeftHandPinky3", + LeftHandPinky3: "LeftHandPinky4" +}; + +function dumpHardwareMapping() { + Object.keys(Controller.Hardware).forEach(function (deviceName) { + Object.keys(Controller.Hardware[deviceName]).forEach(function (input) { + print("Controller.Hardware." + deviceName + "." + input + ":" + Controller.Hardware[deviceName][input]); + }); + }); +} + +// ctor +function NeuronAvatar() { + var self = this; + Script.scriptEnding.connect(function () { + self.shutdown(); + }); + Controller.hardwareChanged.connect(function () { + self.hardwareChanged(); + }); + + if (Controller.Hardware.Neuron) { + this.activate(); + } else { + this.deactivate(); + } +} + +NeuronAvatar.prototype.shutdown = function () { + this.deactivate(); +}; + +NeuronAvatar.prototype.hardwareChanged = function () { + if (Controller.Hardware.Neuron) { + this.activate(); + } else { + this.deactivate(); + } +}; + +NeuronAvatar.prototype.activate = function () { + if (!this._active) { + Script.update.connect(updateCallback); + } + this._active = true; +}; + +NeuronAvatar.prototype.deactivate = function () { + if (this._active) { + var self = this; + Script.update.disconnect(updateCallback); + } + this._active = false; + MyAvatar.clearJointsData(); +}; + +NeuronAvatar.prototype.update = function (deltaTime) { + var keys = Object.keys(JOINT_NAME_MAP); + var i, l = keys.length; + for (i = 0; i < l; i++) { + var channel = Controller.Hardware.Neuron[keys[i]]; + if (channel) { + var pose = Controller.getPoseValue(channel); + var j = MyAvatar.getJointIndex(JOINT_NAME_MAP[keys[i]]); + var defaultRot = MyAvatar.getDefaultJointRotation(j); + if (keys[i] == "Hips") { + MyAvatar.setJointRotation(j, Quat.multiply(pose.rotation, defaultRot)); + } else { + MyAvatar.setJointRotation(j, defaultRot); + } + } + } +}; + +var neuronAvatar = new NeuronAvatar(); + +function updateCallback(deltaTime) { + neuronAvatar.update(deltaTime); +} + diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 68f382d2d9..4dd091f1d6 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -289,8 +289,10 @@ void Rig::clearJointState(int index) { void Rig::clearJointStates() { _internalPoseSet._overrideFlags.clear(); - _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints()); - _internalPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); + if (_animSkeleton) { + _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints()); + _internalPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); + } } void Rig::clearJointAnimationPriority(int index) { diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index bbd33c5cb3..feed8a0fad 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -88,9 +88,66 @@ namespace controller { // No correlation to SDL enum StandardPoseChannel { - LEFT_HAND = 0, - RIGHT_HAND, + HIPS_ROOT = 0, + HIPS, + RIGHT_UP_LEG, + RIGHT_LEG, + RIGHT_FOOT, + LEFT_UP_LEG, + LEFT_LEG, + LEFT_FOOT, + SPINE, + SPINE1, + SPINE2, + SPINE3, + NECK, HEAD, + RIGHT_SHOULDER, + RIGHT_ARM, + RIGHT_FORE_ARM, + RIGHT_HAND, + RIGHT_HAND_THUMB1, + RIGHT_HAND_THUMB2, + RIGHT_HAND_THUMB3, + RIGHT_IN_HAND_INDEX, + RIGHT_HAND_INDEX1, + RIGHT_HAND_INDEX2, + RIGHT_HAND_INDEX3, + RIGHT_IN_HAND_MIDDLE, + RIGHT_HAND_MIDDLE1, + RIGHT_HAND_MIDDLE2, + RIGHT_HAND_MIDDLE3, + RIGHT_IN_HANDRING, + RIGHT_HAND_RING1, + RIGHT_HAND_RING2, + RIGHT_HAND_RING3, + RIGHT_IN_HAND_PINKY, + RIGHT_HAND_PINKY1, + RIGHT_HAND_PINKY2, + RIGHT_HAND_PINKY3, + LEFT_SHOULDER, + LEFT_ARM, + LEFT_FORE_ARM, + LEFT_HAND, + LEFT_HAND_THUMB1, + LEFT_HAND_THUMB2, + LEFT_HAND_THUMB3, + LEFT_IN_HAND_INDEX, + LEFT_HAND_INDEX1, + LEFT_HAND_INDEX2, + LEFT_HAND_INDEX3, + LEFT_IN_HAND_MIDDLE, + LEFT_HAND_MIDDLE1, + LEFT_HAND_MIDDLE2, + LEFT_HAND_MIDDLE3, + LEFT_IN_HAND_RING, + LEFT_HAND_RING1, + LEFT_HAND_RING2, + LEFT_HAND_RING3, + LEFT_IN_HAND_PINKY, + LEFT_HAND_PINKY1, + LEFT_HAND_PINKY2, + LEFT_HAND_PINKY3, NUM_STANDARD_POSES }; diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index c3f764da05..4f52d8da98 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -1,5 +1,5 @@ // -// NeuronPlugin.h +// NeuronPlugin.cpp // input-plugins/src/input-plugins // // Created by Anthony Thibault on 12/18/2015. @@ -11,6 +11,7 @@ #include "NeuronPlugin.h" +#include #include #include #include @@ -27,6 +28,7 @@ Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") const QString NeuronPlugin::NAME = "Neuron"; const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; +// This matches controller::StandardPoseChannel enum JointIndex { HipsPosition = 0, Hips, @@ -87,7 +89,8 @@ enum JointIndex { LeftInHandPinky, LeftHandPinky1, LeftHandPinky2, - LeftHandPinky3 + LeftHandPinky3, + Size }; bool NeuronPlugin::isSupported() const { @@ -98,29 +101,81 @@ bool NeuronPlugin::isSupported() const { // NOTE: must be thread-safe void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx* header, float* data) { - qCDebug(inputplugins) << "NeuronPlugin: received frame data, DataCount = " << header->DataCount; auto neuronPlugin = reinterpret_cast(context); - std::lock_guard guard(neuronPlugin->_jointsMutex); - // Data is 6 floats: 3 position values, 3 rotation euler angles (degrees) + // version 1.0 + if (header->DataVersion.Major == 1 && header->DataVersion.Minor == 0) { - // resize vector if necessary - const size_t NUM_FLOATS_PER_JOINT = 6; - const size_t NUM_JOINTS = header->DataCount / NUM_FLOATS_PER_JOINT; - if (neuronPlugin->_joints.size() != NUM_JOINTS) { - neuronPlugin->_joints.resize(NUM_JOINTS, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); + std::lock_guard guard(neuronPlugin->_jointsMutex); + + // Data is 6 floats: 3 position values, 3 rotation euler angles (degrees) + + // resize vector if necessary + const size_t NUM_FLOATS_PER_JOINT = 6; + const size_t NUM_JOINTS = header->DataCount / NUM_FLOATS_PER_JOINT; + if (neuronPlugin->_joints.size() != NUM_JOINTS) { + neuronPlugin->_joints.resize(NUM_JOINTS, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); + } + + assert(sizeof(NeuronPlugin::NeuronJoint) == (NUM_FLOATS_PER_JOINT * sizeof(float))); + + // copy the data + memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); + + } else { + static bool ONCE = false; + if (!ONCE) { + qCCritical(inputplugins) << "NeuronPlugin: bad frame version number, expected 1.0"; + ONCE = true; + } } - - assert(sizeof(NeuronPlugin::NeuronJoint) == (NUM_FLOATS_PER_JOINT * sizeof(float))); - - // copy the data - memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); } // NOTE: must be thread-safe static void CommandDataReceivedCallback(void* context, SOCKET_REF sender, CommandPack* pack, void* data) { + DATA_VER version; + version._VersionMask = pack->DataVersion; + if (version.Major == 1 && version.Minor == 0) { + const char* str = "Unknown"; + switch (pack->CommandId) { + case Cmd_BoneSize: // Id can be used to request bone size from server or register avatar name command. + str = "BoneSize"; + break; + case Cmd_AvatarName: // Id can be used to request avatar name from server or register avatar name command. + str = "AvatarName"; + break; + case Cmd_FaceDirection: // Id used to request face direction from server + str = "FaceDirection"; + break; + case Cmd_DataFrequency: // Id can be used to request data frequency from server or register data frequency command. + str = "DataFrequency"; + break; + case Cmd_BvhInheritanceTxt: // Id can be used to request bvh header txt from server or register bvh header txt command. + str = "BvhInheritanceTxt"; + break; + case Cmd_AvatarCount: // Id can be used to request avatar count from server or register avatar count command. + str = "AvatarCount"; + break; + case Cmd_CombinationMode: // Id can be used to request combination mode from server or register combination mode command. + str = "CombinationMode"; + break; + case Cmd_RegisterEvent: // Id can be used to register event. + str = "RegisterEvent"; + break; + case Cmd_UnRegisterEvent: // Id can be used to unregister event. + str = "UnRegisterEvent"; + break; + } + qCDebug(inputplugins) << "NeuronPlugin: command data received CommandID = " << str; + } else { + static bool ONCE = false; + if (!ONCE) { + qCCritical(inputplugins) << "NeuronPlugin: bad command version number, expected 1.0"; + ONCE = true; + } + } } // NOTE: must be thread-safe @@ -130,6 +185,11 @@ static void SocketStatusChangedCallback(void* context, SOCKET_REF sender, Socket void NeuronPlugin::activate() { InputPlugin::activate(); + + // register with userInputMapper + auto userInputMapper = DependencyManager::get(); + userInputMapper->registerDevice(_inputDevice); + qCDebug(inputplugins) << "NeuronPlugin::activate"; // register c-style callbacks @@ -149,12 +209,18 @@ void NeuronPlugin::activate() { } void NeuronPlugin::deactivate() { - // TODO: qCDebug(inputplugins) << "NeuronPlugin::deactivate"; + // unregister from userInputMapper + if (_inputDevice->_deviceID != controller::Input::INVALID_DEVICE) { + auto userInputMapper = DependencyManager::get(); + userInputMapper->removeDevice(_inputDevice->_deviceID); + } + if (_socketRef) { BRCloseSocket(_socketRef); } + InputPlugin::deactivate(); } @@ -174,12 +240,14 @@ void NeuronPlugin::pluginUpdate(float deltaTime, bool jointsCaptured) { joints = _joints; } + /* DebugDraw::getInstance().addMyAvatarMarker("LEFT_FOOT", - eulerToQuat(joints[6].rot), + eulerToQuat(joints[6].euler), joints[6].pos / 100.0f, glm::vec4(1)); - - _inputDevice->update(deltaTime, jointsCaptured); + */ + _inputDevice->update(deltaTime, joints, _prevJoints); + _prevJoints = joints; } void NeuronPlugin::saveSettings() const { @@ -198,11 +266,86 @@ void NeuronPlugin::loadSettings() { // InputDevice // +static controller::StandardPoseChannel neuronJointIndexToPoseIndex(JointIndex i) { + // Currently they are the same. + // but that won't always be the case... + return (controller::StandardPoseChannel)i; +} + +static const char* neuronJointName(JointIndex i) { + switch (i) { + case HipsPosition: return "HipsPosition"; + case Hips: return "Hips"; + case RightUpLeg: return "RightUpLeg"; + case RightLeg: return "RightLeg"; + case RightFoot: return "RightFoot"; + case LeftUpLeg: return "LeftUpLeg"; + case LeftLeg: return "LeftLeg"; + case LeftFoot: return "LeftFoot"; + case Spine: return "Spine"; + case Spine1: return "Spine1"; + case Spine2: return "Spine2"; + case Spine3: return "Spine3"; + case Neck: return "Neck"; + case Head: return "Head"; + case RightShoulder: return "RightShoulder"; + case RightArm: return "RightArm"; + case RightForeArm: return "RightForeArm"; + case RightHand: return "RightHand"; + case RightHandThumb1: return "RightHandThumb1"; + case RightHandThumb2: return "RightHandThumb2"; + case RightHandThumb3: return "RightHandThumb3"; + case RightInHandIndex: return "RightInHandIndex"; + case RightHandIndex1: return "RightHandIndex1"; + case RightHandIndex2: return "RightHandIndex2"; + case RightHandIndex3: return "RightHandIndex3"; + case RightInHandMiddle: return "RightInHandMiddle"; + case RightHandMiddle1: return "RightHandMiddle1"; + case RightHandMiddle2: return "RightHandMiddle2"; + case RightHandMiddle3: return "RightHandMiddle3"; + case RightInHandRing: return "RightInHandRing"; + case RightHandRing1: return "RightHandRing1"; + case RightHandRing2: return "RightHandRing2"; + case RightHandRing3: return "RightHandRing3"; + case RightInHandPinky: return "RightInHandPinky"; + case RightHandPinky1: return "RightHandPinky1"; + case RightHandPinky2: return "RightHandPinky2"; + case RightHandPinky3: return "RightHandPinky3"; + case LeftShoulder: return "LeftShoulder"; + case LeftArm: return "LeftArm"; + case LeftForeArm: return "LeftForeArm"; + case LeftHand: return "LeftHand"; + case LeftHandThumb1: return "LeftHandThumb1"; + case LeftHandThumb2: return "LeftHandThumb2"; + case LeftHandThumb3: return "LeftHandThumb3"; + case LeftInHandIndex: return "LeftInHandIndex"; + case LeftHandIndex1: return "LeftHandIndex1"; + case LeftHandIndex2: return "LeftHandIndex2"; + case LeftHandIndex3: return "LeftHandIndex3"; + case LeftInHandMiddle: return "LeftInHandMiddle"; + case LeftHandMiddle1: return "LeftHandMiddle1"; + case LeftHandMiddle2: return "LeftHandMiddle2"; + case LeftHandMiddle3: return "LeftHandMiddle3"; + case LeftInHandRing: return "LeftInHandRing"; + case LeftHandRing1: return "LeftHandRing1"; + case LeftHandRing2: return "LeftHandRing2"; + case LeftHandRing3: return "LeftHandRing3"; + case LeftInHandPinky: return "LeftInHandPinky"; + case LeftHandPinky1: return "LeftHandPinky1"; + case LeftHandPinky2: return "LeftHandPinky2"; + case LeftHandPinky3: return "LeftHandPinky3"; + default: return "???"; + } +} + controller::Input::NamedVector NeuronPlugin::InputDevice::getAvailableInputs() const { // TODO: - static const controller::Input::NamedVector availableInputs { - makePair(controller::LEFT_HAND, "LeftHand"), - makePair(controller::RIGHT_HAND, "RightHand") + static controller::Input::NamedVector availableInputs; + + if (availableInputs.size() == 0) { + for (int i = 0; i < JointIndex::Size; i++) { + availableInputs.push_back(makePair(neuronJointIndexToPoseIndex((JointIndex)i), neuronJointName((JointIndex)i))); + } }; return availableInputs; } @@ -212,8 +355,24 @@ QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { return MAPPING_JSON; } -void NeuronPlugin::InputDevice::update(float deltaTime, bool jointsCaptured) { +void NeuronPlugin::InputDevice::update(float deltaTime, const std::vector& joints, const std::vector& prevJoints) { + for (int i = 0; i < joints.size(); i++) { + int poseIndex = neuronJointIndexToPoseIndex((JointIndex)i); + glm::vec3 linearVel, angularVel; + glm::vec3 pos = (joints[i].pos * METERS_PER_CENTIMETER); + glm::quat rot = eulerToQuat(joints[i].euler); + if (i < prevJoints.size()) { + linearVel = (pos - (prevJoints[i].pos * METERS_PER_CENTIMETER)) / deltaTime; + // quat log imag part points along the axis of rotation, and it's length will be the half angle. + glm::quat d = glm::log(rot * glm::inverse(eulerToQuat(prevJoints[i].euler))); + angularVel = glm::vec3(d.x, d.y, d.z) / (0.5f * deltaTime); + } + _poseStateMap[poseIndex] = controller::Pose(pos, rot, linearVel, angularVel); + if (glm::length(angularVel) > 0.5f) { + qCDebug(inputplugins) << "Movement in joint" << i << neuronJointName((JointIndex)i); + } + } } void NeuronPlugin::InputDevice::focusOutEvent() { diff --git a/plugins/hifiNeuron/src/NeuronPlugin.h b/plugins/hifiNeuron/src/NeuronPlugin.h index 5f67502e04..f787838ce2 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.h +++ b/plugins/hifiNeuron/src/NeuronPlugin.h @@ -41,15 +41,25 @@ public: virtual void loadSettings() override; protected: + + struct NeuronJoint { + glm::vec3 pos; + glm::vec3 euler; + }; + class InputDevice : public controller::InputDevice { public: + friend class NeuronPlugin; + InputDevice() : controller::InputDevice("Neuron") {} // Device functions virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; - virtual void update(float deltaTime, bool jointsCaptured) override; + virtual void update(float deltaTime, bool jointsCaptured) override {}; virtual void focusOutEvent() override; + + void update(float deltaTime, const std::vector& joints, const std::vector& prevJoints); }; std::shared_ptr _inputDevice { std::make_shared() }; @@ -61,13 +71,10 @@ protected: int _serverPort; void* _socketRef; - struct NeuronJoint { - glm::vec3 pos; - glm::vec3 rot; - }; - std::vector _joints; - std::mutex _jointsMutex; + std::mutex _jointsMutex; // used to guard access to _joints + + std::vector _prevJoints; }; #endif // hifi_NeuronPlugin_h From baf095f4af625e54071c4d284c10a769f3e2d66d Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Wed, 23 Dec 2015 14:16:49 -0600 Subject: [PATCH 19/43] Lots and lots of changes to installer --- cmake/macros/ConsolidateStackComponents.cmake | 56 ++-- cmake/macros/GenerateInstallers.cmake | 60 ++-- cmake/modules/FindOpenSSL.cmake | 3 +- tools/nsis/installer_vertical.bmp | Bin 0 -> 154544 bytes tools/nsis/release.nsi | 258 ++++++++++-------- 5 files changed, 204 insertions(+), 173 deletions(-) create mode 100644 tools/nsis/installer_vertical.bmp diff --git a/cmake/macros/ConsolidateStackComponents.cmake b/cmake/macros/ConsolidateStackComponents.cmake index 33b50472bf..2fc81d1595 100644 --- a/cmake/macros/ConsolidateStackComponents.cmake +++ b/cmake/macros/ConsolidateStackComponents.cmake @@ -1,28 +1,30 @@ -macro(CONSOLIDATE_STACK_COMPONENTS) - - if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE) - if (WIN32) - # Copy all the output for this target into the common deployment location - add_custom_command( - TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/full-stack-deployment - ) - - # Copy icon files for interface and stack manager - if (TARGET_NAME STREQUAL "interface" OR TARGET_NAME STREQUAL "stack-manager") - if (TARGET_NAME STREQUAL "interface") - set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/icon/${INTERFACE_ICON}") - set (ICON_DESTINATION_NAME "interface.ico") - elseif (TARGET_NAME STREQUAL "stack-manager") - set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/assets/${STACK_MANAGER_ICON}") - set (ICON_DESTINATION_NAME "stack-manager.ico") - endif () - add_custom_command( - TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy ${ICON_FILE_PATH} ${CMAKE_BINARY_DIR}/full-stack-deployment/${ICON_DESTINATION_NAME} - ) - endif () - endif () - endif () - +macro(CONSOLIDATE_STACK_COMPONENTS) + + if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE) + if (WIN32) + # Copy icon files for interface and stack manager + if (TARGET_NAME STREQUAL "interface" OR TARGET_NAME STREQUAL "stack-manager") + if (TARGET_NAME STREQUAL "interface") + set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/icon/${INTERFACE_ICON}") + set (ICON_DESTINATION_NAME "interface.ico") + set (DEPLOYMENT_PATH "front-end-deployment") + else () + set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/assets/${STACK_MANAGER_ICON}") + set (ICON_DESTINATION_NAME "stack-manager.ico") + set (DEPLOYMENT_PATH "back-end-deployment") + endif () + add_custom_command( + TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy ${ICON_FILE_PATH} ${CMAKE_BINARY_DIR}/${DEPLOYMENT_PATH}/${ICON_DESTINATION_NAME} + COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/${DEPLOYMENT_PATH} + ) + else () + add_custom_command( + TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/back-end-deployment + ) + endif () + endif () + endif () + endmacro() \ No newline at end of file diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index 570e24332b..b8860b3ddc 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -1,30 +1,32 @@ -# -# GenerateInstallers.cmake -# cmake/macros -# -# Created by Leonardo Murillo on 12/16/2015. -# Copyright 2015 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 -# - -macro(GENERATE_INSTALLERS) - if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE AND WIN32) - file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/full-stack-deployment") - find_program(MAKENSIS_COMMAND makensis PATHS [HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\NSIS]) - if (NOT MAKENSIS_COMMAND) - message(FATAL_ERROR "The Nullsoft Scriptable Install Systems is required for generating packaged installers on Windows (http://nsis.sourceforge.net/)") - endif () - add_custom_target( - build-package ALL - DEPENDS interface assignment-client domain-server stack-manager - COMMAND set INSTALLER_SOURCE_DIR=${CMAKE_BINARY_DIR}/full-stack-deployment - COMMAND set INSTALLER_NAME=${CMAKE_BINARY_DIR}/${INSTALLER_NAME} - COMMAND set INSTALLER_SCRIPTS_DIR=${CMAKE_SOURCE_DIR}/examples - COMMAND set INSTALLER_COMPANY=${INSTALLER_COMPANY} - COMMAND set INSTALLER_DIRECTORY=${INSTALLER_DIRECTORY} - COMMAND CMD /C "\"${MAKENSIS_COMMAND}\" ${CMAKE_SOURCE_DIR}/tools/nsis/release.nsi" - ) - endif () +# +# GenerateInstallers.cmake +# cmake/macros +# +# Created by Leonardo Murillo on 12/16/2015. +# Copyright 2015 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 +# + +macro(GENERATE_INSTALLERS) + if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE AND WIN32) + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/front-end-deployment") + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/back-end-deployment") + find_program(MAKENSIS_COMMAND makensis PATHS [HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\NSIS]) + if (NOT MAKENSIS_COMMAND) + message(FATAL_ERROR "The Nullsoft Scriptable Install Systems is required for generating packaged installers on Windows (http://nsis.sourceforge.net/)") + endif () + add_custom_target( + build-package ALL + DEPENDS interface assignment-client domain-server stack-manager + COMMAND set INSTALLER_FRONTEND_DIR=${CMAKE_BINARY_DIR}/front-end-deployment + COMMAND set INSTALLER_BACKEND_DIR=${CMAKE_BINARY_DIR}/back-end-deployment + COMMAND set INSTALLER_NAME=${CMAKE_BINARY_DIR}/${INSTALLER_NAME} + COMMAND set INSTALLER_SCRIPTS_DIR=${CMAKE_SOURCE_DIR}/examples + COMMAND set INSTALLER_COMPANY=${INSTALLER_COMPANY} + COMMAND set INSTALLER_DIRECTORY=${INSTALLER_DIRECTORY} + COMMAND CMD /C "\"${MAKENSIS_COMMAND}\" ${CMAKE_SOURCE_DIR}/tools/nsis/release.nsi" + ) + endif () endmacro() \ No newline at end of file diff --git a/cmake/modules/FindOpenSSL.cmake b/cmake/modules/FindOpenSSL.cmake index 9d9557ad9e..fbadcd24c8 100644 --- a/cmake/modules/FindOpenSSL.cmake +++ b/cmake/modules/FindOpenSSL.cmake @@ -258,7 +258,8 @@ if (WIN32) if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy ${OPENSSL_DLL_PATH}/ssleay32.dll ${CMAKE_BINARY_DIR}/full-stack-deployment/ + COMMAND "${CMAKE_COMMAND}" -E copy ${OPENSSL_DLL_PATH}/ssleay32.dll ${CMAKE_BINARY_DIR}/front-end-deployment/ + COMMAND "${CMAKE_COMMAND}" -E copy ${OPENSSL_DLL_PATH}/ssleay32.dll ${CMAKE_BINARY_DIR}/back-end-deployment/ ) endif () endif () diff --git a/tools/nsis/installer_vertical.bmp b/tools/nsis/installer_vertical.bmp new file mode 100644 index 0000000000000000000000000000000000000000..feefd2db8393d2a1b5eebdad741d59a525ab7335 GIT binary patch literal 154544 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 U03m=7KnNfN5CSnn;MJ@D4+83Kg8%>k literal 0 HcmV?d00001 diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index c5a568f165..1068ba17dc 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -1,164 +1,190 @@ !include LogicLib.nsh !include x64.nsh +!include MUI2.nsh + +;------------------------------------------------------------------------------------------------------ +; Source Directory Definition +; +; frontend_srcdir = Source directory for Interface +; backend_srcdir = Source directory for Stack Manager and server stack +; scripts_srcdir = Source directory for JS scripts + +!define frontend_srcdir "$%INSTALLER_FRONTEND_DIR%" +!define backend_srcdir "$%INSTALLER_BACKEND_DIR%" +!define scripts_srcdir "$%INSTALLER_SCRIPTS_DIR%" + +; Install Directories, Icons and Registry entries +; +; setup = Name of the installer executable that will be produced +; uninstaller = Name of the uninstaller executable +; company = String to use for company name, includes build type suffix [eg. High Fidelity - PR] for non-release installers +; install_directory = Subdirectory where this specific version will be installed, in the case of dev and pr builds, its a +; unique subdirectory inside a company parent [eg. \High Fidelity - PR\1234\ ] -!define srcdir "$%INSTALLER_SOURCE_DIR%" !define setup "$%INSTALLER_NAME%" -!define scriptsdir "$%INSTALLER_SCRIPTS_DIR%" +!define uninstaller "uninstall.exe" !define company "$%INSTALLER_COMPANY%" !define install_directory "$%INSTALLER_DIRECTORY%" + +; Executables and icons for GUI applications that will be added as shortcuts. !define interface_exec "interface.exe" !define stack_manager_exec "stack-manager.exe" !define interface_icon "interface.ico" !define stack_manager_icon "stack-manager.ico" -!define regkey "Software\${company}" -!define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${company}" -!define install_dir_company "$PROGRAMFILES64\${install_directory}" + +; Registry entries +!define regkey "Software\${install_directory}" +!define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${install_directory}" +!define frontend_instdir "$PROGRAMFILES64\${install_directory}" +!define backend_instdir "$APPDATA\${install_directory}" + +; Start Menu program group !define startmenu_company "$SMPROGRAMS\${install_directory}" -!define uninstaller "uninstall.exe" -;-------------------------------- +;------------------------------------------------------------------------------------------------------ +; Local Variables and Other Options + +Var ChosenFrontEndInstallDir +Var ChosenBackEndInstallDir -XPStyle on ShowInstDetails hide ShowUninstDetails hide - -Name "${company}" -Caption "${company}" - -!ifdef icon - Icon "${icon}" -!endif - -OutFile "${setup}" - +AutoCloseWindow true +ShowInstDetails show SetDateSave on SetDatablockOptimize on CRCCheck on SilentInstall normal +Icon "${frontend_srcdir}\${interface_icon}" +UninstallIcon "${frontend_srcdir}\${interface_icon}" +UninstallText "This will uninstall ${company}." +Name "${company}" +Caption "${company}" +OutFile "${setup}" -InstallDir "${install_dir_company}" -InstallDirRegKey HKLM "${regkey}" "" +;------------------------------------------------------------------------------------------------------ +; Components -; Page components -Page directory -Page components -Page instfiles - -UninstPage uninstConfirm -UninstPage instfiles - -;-------------------------------- - -AutoCloseWindow true -ShowInstDetails show - - -!ifdef screenimage - - ; set up background image - ; uses BgImage plugin - - Function .onGUIInit - ; extract background BMP into temp plugin directory - InitPluginsDir - File /oname=$PLUGINSDIR\1.bmp "${screenimage}" - - BgImage::SetBg /NOUNLOAD /FILLSCREEN $PLUGINSDIR\1.bmp - BgImage::Redraw /NOUNLOAD - FunctionEnd - - Function .onGUIEnd - ; Destroy must not have /NOUNLOAD so NSIS will be able to unload and delete BgImage before it exits - BgImage::Destroy - FunctionEnd - -!endif - -; Optional Component Selection Section /o "DDE Face Recognition" SEC01 - SetOutPath "$INSTDIR" - CreateDirectory $INSTDIR\dde - NSISdl::download "https://s3-us-west-1.amazonaws.com/hifi-production/optionals/dde-installer.exe" "$INSTDIR\dde-installer.exe" - ExecWait '"$INSTDIR\dde-installer.exe" /q:a /t:"$INSTDIR\dde"' + SetOutPath "$ChosenFrontEndInstallDir" + CreateDirectory $ChosenFrontEndInstallDir\dde + NSISdl::download "https://s3-us-west-1.amazonaws.com/hifi-production/optionals/dde-installer.exe" "$ChosenFrontEndInstallDir\dde-installer.exe" + ExecWait '"$ChosenFrontEndInstallDir\dde-installer.exe" /q:a /t:"$ChosenFrontEndInstallDir\dde"' SectionEnd -; beginning (invisible) section Section "Registry Entries and Procotol Handler" SEC02 - SectionIn RO - - WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR" + WriteRegStr HKLM "${regkey}" "Install_Dir" "$ChosenFrontEndInstallDir" WriteRegStr HKLM "${uninstkey}" "DisplayName" "${install_directory} (remove only)" - WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$INSTDIR\${uninstaller}"' - WriteRegStr HKCR "${company}\Shell\open\command\" "" '"$INSTDIR\${interface_exec} "%1"' - WriteRegStr HKCR "${company}\DefaultIcon" "" "$INSTDIR\${interface_icon}" + WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$ChosenFrontEndInstallDir\${uninstaller}"' + WriteRegStr HKCR "${company}\Shell\open\command\" "" '"$ChosenFrontEndInstallDir\${interface_exec} "%1"' + WriteRegStr HKCR "${company}\DefaultIcon" "" "$ChosenFrontEndInstallDir\${interface_icon}" ; hifi:// protocol handler registry entries WriteRegStr HKCR 'hifi' '' 'URL:Alert Protocol' WriteRegStr HKCR 'hifi' 'URL Protocol' '' - WriteRegStr HKCR 'hifi\DefaultIcon' '' '$INSTDIR\${interface_icon},1' - WriteRegStr HKCR 'hifi\shell\open\command' '' '$INSTDIR\${interface_exec} --url "%1"' + WriteRegStr HKCR 'hifi\DefaultIcon' '' '$ChosenFrontEndInstallDir\${interface_icon},1' + WriteRegStr HKCR 'hifi\shell\open\command' '' '$ChosenFrontEndInstallDir\${interface_exec} --url "%1"' - SetOutPath $INSTDIR - - ; package all files, recursively, preserving attributes - ; assume files are in the correct places - File /r "${srcdir}\" - File /a "${srcdir}\${interface_icon}" - File /a "${srcdir}\${stack_manager_icon}" - ; any application-specific files - !ifdef files - !include "${files}" - !endif - WriteUninstaller "${uninstaller}" - Exec '"$INSTDIR\2013_vcredist_x64.exe" /q /norestart' - Exec '"$INSTDIR\2010_vcredist_x86.exe" /q /norestart' + WriteUninstaller "$ChosenFrontEndInstallDir\${uninstaller}" + Exec '"$ChosenFrontEndInstallDir\2013_vcredist_x64.exe" /q /norestart' + Exec '"$ChosenFrontEndInstallDir\2010_vcredist_x86.exe" /q /norestart' SectionEnd -; create shortcuts -Section "Start Menu Shortcuts" SEC03 +Section "Interface Client" SEC03 + SetOutPath $ChosenFrontEndInstallDir + File /r "${frontend_srcdir}\" + File /a "${frontend_srcdir}\${interface_icon}" +SectionEnd - SectionIn RO +Section "Stack Manager Bundle" SEC04 + SetOutPath $ChosenBackEndInstallDir + File /r "${backend_srcdir}\" + File /a "${backend_srcdir}\${stack_manager_icon}" +SectionEnd - ; This should install the shortcuts for "All Users" +Section "Start Menu Shortcuts" SEC05 SetShellVarContext all CreateDirectory "${startmenu_company}" - SetOutPath $INSTDIR ; for working directory - CreateShortCut "${startmenu_company}\Interface.lnk" "$INSTDIR\${interface_exec}" "" "$INSTDIR\${interface_icon}" - CreateShortCut "${startmenu_company}\Stack Manager.lnk" "$INSTDIR\${stack_manager_exec}" "" "$INSTDIR\${stack_manager_icon}" - CreateShortCut "${startmenu_company}\Uninstall ${company}.lnk" "$INSTDIR\${uninstaller}" + SetOutPath $ChosenFrontEndInstallDir + CreateShortCut "${startmenu_company}\Interface.lnk" "$ChosenFrontEndInstallDir\${interface_exec}" "" "$ChosenFrontEndInstallDir\${interface_icon}" + CreateShortCut "${startmenu_company}\Stack Manager.lnk" "$ChosenBackEndInstallDir\${stack_manager_exec}" "" "$ChosenBackEndInstallDir\${stack_manager_icon}" + CreateShortCut "${startmenu_company}\Uninstall ${company}.lnk" "$ChosenFrontEndInstallDir\${uninstaller}" SectionEnd -; Uninstaller -; All section names prefixed by "Un" will be in the uninstaller - -UninstallText "This will uninstall ${company}." - -!ifdef icon - UninstallIcon "${interface_icon}" -!endif - -Section "Uninstall" SEC04 - - SectionIn RO - - ; Explicitly remove all added shortcuts +Section "Uninstall" SetShellVarContext all DELETE "${startmenu_company}\Interface.lnk" DELETE "${startmenu_company}\Stack Manager.lnk" DELETE "${startmenu_company}\Uninstall ${company}.lnk" - RMDIR "${startmenu_company}" - - RMDIR /r "$INSTDIR" - ; This should remove the High Fidelity folder in Program Files if it's empty - RMDIR "${install_dir_company}" - - !ifdef unfiles - !include "${unfiles}" - !endif - ; It's good practice to put the registry key removal at the very end + RMDIR /r "$ChosenBackEndInstallDir" + RMDIR /r "$ChosenFrontEndInstallDir" + RMDIR "${install_directory}" DeleteRegKey HKLM "${uninstkey}" DeleteRegKey HKLM "${regkey}" DeleteRegKey HKCR 'hifi' -SectionEnd \ No newline at end of file +SectionEnd + +;------------------------------------------------------------------------------------------------------ +; Functions + +Function .onInit + StrCpy $ChosenFrontEndInstallDir "${frontend_instdir}" + StrCpy $ChosenBackEndInstallDir "${backend_instdir}" +FunctionEnd + +Function isInterfaceSelected + ${IfNot} ${SectionIsSelected} ${SEC03} + Abort + ${EndIf} +FunctionEnd + +Function isStackManagerSelected + ${IfNot} ${SectionIsSelected} ${SEC04} + Abort + ${EndIf} +FunctionEnd + +;------------------------------------------------------------------------------------------------------ +; User interface macros and other definitions + +!define MUI_WELCOMEFINISHPAGE_BITMAP "installer_vertical.bmp" +!define MUI_WELCOMEPAGE_TITLE "High Fidelity - Integrated Installer" +!define MUI_WELCOMEPAGE_TEXT "Welcome to High Fidelity! This installer includes both the Interface client for VR access as well as the Stack Manager and required server components for you to host your own domain in the metaverse." +!insertmacro MUI_PAGE_WELCOME + +!define MUI_PAGE_HEADER_TEXT "Please select the components you want to install" +!define MUI_COMPONENTSPAGE_TEXT_DESCRIPTION_INFO "Hover over a component for a brief description" +!insertmacro MUI_PAGE_COMPONENTS + +!define MUI_PAGE_CUSTOMFUNCTION_PRE isInterfaceSelected +!define MUI_DIRECTORYPAGE_VARIABLE $ChosenFrontEndInstallDir +!define MUI_PAGE_HEADER_TEXT "Interface client" +!define MUI_PAGE_HEADER_SUBTEXT "" +!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose a location to install the High Fidelity Interface client. You will use the Interface client to connect to domains in the metaverse." +!define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install Directory" +!insertmacro MUI_PAGE_DIRECTORY + +!define MUI_PAGE_CUSTOMFUNCTION_PRE isStackManagerSelected +!define MUI_DIRECTORYPAGE_VARIABLE $ChosenBackEndInstallDir +!define MUI_PAGE_HEADER_TEXT "Stack Manager" +!define MUI_PAGE_HEADER_SUBTEXT "" +!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose a location to install the High Fidelity Stack Manager bundle, including back end components. NOTE: If you change the default path, make sure you're selecting an unprivileged location, otherwise you will be forced to run the application as administrator for correct functioning." +!define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install Directory - must be writable" +!insertmacro MUI_PAGE_DIRECTORY + +!insertmacro MUI_PAGE_INSTFILES + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${SEC01} "DDE enables facial gesture recognition using a standard 2D webcam" + !insertmacro MUI_DESCRIPTION_TEXT ${SEC02} "Registry entries are required by the system, we will also add a hifi:// protocol handler" + !insertmacro MUI_DESCRIPTION_TEXT ${SEC03} "Interface is the GUI client to access domains running the High Fidelity VR stack" + !insertmacro MUI_DESCRIPTION_TEXT ${SEC04} "The Stack Manager allows you to run a domain of your own and connect it to the High Fidelity metaverse" + !insertmacro MUI_DESCRIPTION_TEXT ${SEC05} "Adds a program group and shortcuts to the Start Menu" +!insertmacro MUI_FUNCTION_DESCRIPTION_END + +!insertmacro MUI_LANGUAGE "English" \ No newline at end of file From 63a1ea6282c5d6313b29a259253be2a8df2b9eb3 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Wed, 23 Dec 2015 14:26:03 -0600 Subject: [PATCH 20/43] Couple of bugs --- tools/nsis/release.nsi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index 1068ba17dc..6c24cac13b 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -86,6 +86,7 @@ Section "Registry Entries and Procotol Handler" SEC02 WriteRegStr HKCR 'hifi\DefaultIcon' '' '$ChosenFrontEndInstallDir\${interface_icon},1' WriteRegStr HKCR 'hifi\shell\open\command' '' '$ChosenFrontEndInstallDir\${interface_exec} --url "%1"' + SetOutPath $ChosenFrontEndInstallDir WriteUninstaller "$ChosenFrontEndInstallDir\${uninstaller}" Exec '"$ChosenFrontEndInstallDir\2013_vcredist_x64.exe" /q /norestart' Exec '"$ChosenFrontEndInstallDir\2010_vcredist_x86.exe" /q /norestart' @@ -114,9 +115,12 @@ SectionEnd Section "Uninstall" SetShellVarContext all + SetOutPath $TEMP + DELETE "${startmenu_company}\Interface.lnk" DELETE "${startmenu_company}\Stack Manager.lnk" DELETE "${startmenu_company}\Uninstall ${company}.lnk" + RMDIR "${startmenu_company}" RMDIR /r "$ChosenBackEndInstallDir" RMDIR /r "$ChosenFrontEndInstallDir" From 02d317dc24a28e06d2e7476a52538031c03f6575 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Wed, 23 Dec 2015 14:55:47 -0600 Subject: [PATCH 21/43] Better compression and fixing uninstaller --- tools/nsis/release.nsi | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index 6c24cac13b..a4814016dd 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -47,6 +47,7 @@ Var ChosenFrontEndInstallDir Var ChosenBackEndInstallDir +SetCompressor /SOLID lzma ShowInstDetails hide ShowUninstDetails hide AutoCloseWindow true @@ -116,15 +117,9 @@ SectionEnd Section "Uninstall" SetShellVarContext all SetOutPath $TEMP - - DELETE "${startmenu_company}\Interface.lnk" - DELETE "${startmenu_company}\Stack Manager.lnk" - DELETE "${startmenu_company}\Uninstall ${company}.lnk" - - RMDIR "${startmenu_company}" + RMDIR /r "${startmenu_company}" RMDIR /r "$ChosenBackEndInstallDir" RMDIR /r "$ChosenFrontEndInstallDir" - RMDIR "${install_directory}" DeleteRegKey HKLM "${uninstkey}" DeleteRegKey HKLM "${regkey}" DeleteRegKey HKCR 'hifi' From 8dc16f29bccfe5373c6054f2c405d5a2b2fa7f7d Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Wed, 23 Dec 2015 19:06:46 -0600 Subject: [PATCH 22/43] Fixes to uninstaller --- tools/nsis/release.nsi | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index a4814016dd..49a63612d4 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -47,7 +47,7 @@ Var ChosenFrontEndInstallDir Var ChosenBackEndInstallDir -SetCompressor /SOLID lzma +SetCompressor bzip2 ShowInstDetails hide ShowUninstDetails hide AutoCloseWindow true @@ -74,8 +74,10 @@ Section /o "DDE Face Recognition" SEC01 SectionEnd Section "Registry Entries and Procotol Handler" SEC02 + SetRegView 64 SectionIn RO WriteRegStr HKLM "${regkey}" "Install_Dir" "$ChosenFrontEndInstallDir" + WriteRegStr HKLM "${regkey}" "Backend_Install_Dir" "$ChosenBackEndInstallDir" WriteRegStr HKLM "${uninstkey}" "DisplayName" "${install_directory} (remove only)" WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$ChosenFrontEndInstallDir\${uninstaller}"' WriteRegStr HKCR "${company}\Shell\open\command\" "" '"$ChosenFrontEndInstallDir\${interface_exec} "%1"' @@ -114,14 +116,16 @@ Section "Start Menu Shortcuts" SEC05 CreateShortCut "${startmenu_company}\Uninstall ${company}.lnk" "$ChosenFrontEndInstallDir\${uninstaller}" SectionEnd -Section "Uninstall" - SetShellVarContext all - SetOutPath $TEMP - RMDIR /r "${startmenu_company}" - RMDIR /r "$ChosenBackEndInstallDir" - RMDIR /r "$ChosenFrontEndInstallDir" +Section "Uninstall" + SetRegView 64 + ReadRegStr $0 HKLM "${regkey}" "Backend_Install_Dir" + Delete "$INSTDIR\${uninstaller}" + RMDir /r "$INSTDIR" + RMDir /r "${startmenu_company}" + RMDir /r "$0" DeleteRegKey HKLM "${uninstkey}" DeleteRegKey HKLM "${regkey}" + DeleteRegKey HKCR "${company}" DeleteRegKey HKCR 'hifi' SectionEnd From c44f69b370387b06ab16b27583aaf5940f3cf59d Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 23 Dec 2015 17:13:52 -0800 Subject: [PATCH 23/43] WIP checkpoint Changed euler angle compisition based on experiments in maya. Also, the neuronAvatar.js attempts to transform the neuron input quaternions into a pose relative to the avatar's default pose, but doesn't it doesn't work. --- examples/controllers/neuron/neuronAvatar.js | 9 +++----- .../src/controllers/StandardControls.h | 3 +-- plugins/hifiNeuron/src/NeuronPlugin.cpp | 22 +++++++++++++------ 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/examples/controllers/neuron/neuronAvatar.js b/examples/controllers/neuron/neuronAvatar.js index a7146e0759..776cabd301 100644 --- a/examples/controllers/neuron/neuronAvatar.js +++ b/examples/controllers/neuron/neuronAvatar.js @@ -1,6 +1,5 @@ // maps controller joint names to avatar joint names var JOINT_NAME_MAP = { - HipsPosition: "", Hips: "Hips", RightUpLeg: "RightUpLeg", RightLeg: "RightLeg", @@ -124,11 +123,9 @@ NeuronAvatar.prototype.update = function (deltaTime) { var pose = Controller.getPoseValue(channel); var j = MyAvatar.getJointIndex(JOINT_NAME_MAP[keys[i]]); var defaultRot = MyAvatar.getDefaultJointRotation(j); - if (keys[i] == "Hips") { - MyAvatar.setJointRotation(j, Quat.multiply(pose.rotation, defaultRot)); - } else { - MyAvatar.setJointRotation(j, defaultRot); - } + var rot = Quat.multiply(Quat.inverse(defaultRot), Quat.multiply(pose.rotation, defaultRot)); + MyAvatar.setJointRotation(j, Quat.multiply(defaultRot, rot)); + //MyAvatar.setJointTranslation(j, Vec3.multiply(100.0, pose.translation)); } } }; diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index feed8a0fad..4294713238 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -88,8 +88,7 @@ namespace controller { // No correlation to SDL enum StandardPoseChannel { - HIPS_ROOT = 0, - HIPS, + HIPS = 0, RIGHT_UP_LEG, RIGHT_LEG, RIGHT_FOOT, diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 4f52d8da98..152bce913d 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -17,6 +17,7 @@ #include #include #include +#include Q_DECLARE_LOGGING_CATEGORY(inputplugins) Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") @@ -30,8 +31,7 @@ const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; // This matches controller::StandardPoseChannel enum JointIndex { - HipsPosition = 0, - Hips, + Hips = 0, RightUpLeg, RightLeg, RightFoot, @@ -204,8 +204,9 @@ void NeuronPlugin::activate() { if (!_socketRef) { // error qCCritical(inputplugins) << "NeuronPlugin: error connecting to " << _serverAddress.c_str() << ":" << _serverPort << "error = " << BRGetLastErrorMessage(); + } else { + qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; } - qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; } void NeuronPlugin::deactivate() { @@ -226,9 +227,9 @@ void NeuronPlugin::deactivate() { // convert between euler in degrees to quaternion static quat eulerToQuat(vec3 euler) { - return (glm::angleAxis(euler.y * RADIANS_PER_DEGREE, Vectors::UNIT_Y) * - glm::angleAxis(euler.x * RADIANS_PER_DEGREE, Vectors::UNIT_X) * - glm::angleAxis(euler.z * RADIANS_PER_DEGREE, Vectors::UNIT_Z)); + // euler.x and euler.y are swaped (thanks NOMICOM!) + glm::vec3 e = glm::vec3(euler.y, euler.x, euler.z) * RADIANS_PER_DEGREE; + return (glm::angleAxis(e.y, Vectors::UNIT_Y) * glm::angleAxis(e.x, Vectors::UNIT_X) * glm::angleAxis(e.z, Vectors::UNIT_Z)); } void NeuronPlugin::pluginUpdate(float deltaTime, bool jointsCaptured) { @@ -274,7 +275,6 @@ static controller::StandardPoseChannel neuronJointIndexToPoseIndex(JointIndex i) static const char* neuronJointName(JointIndex i) { switch (i) { - case HipsPosition: return "HipsPosition"; case Hips: return "Hips"; case RightUpLeg: return "RightUpLeg"; case RightLeg: return "RightLeg"; @@ -369,9 +369,17 @@ void NeuronPlugin::InputDevice::update(float deltaTime, const std::vector Date: Thu, 24 Dec 2015 17:14:17 -0800 Subject: [PATCH 24/43] neruonAvatar.js: now sets rotations in correct frame. The rotations from the neuron are effectively in world space and are deltas from the default pose. There still is an issue with the thumb, due to the missing joint from the Neuron. The Neuron only has 3 thumb joints, not 4. --- examples/controllers/neuron/neuronAvatar.js | 95 ++++++++++++++++++++- plugins/hifiNeuron/src/NeuronPlugin.cpp | 52 +++++------ 2 files changed, 119 insertions(+), 28 deletions(-) diff --git a/examples/controllers/neuron/neuronAvatar.js b/examples/controllers/neuron/neuronAvatar.js index 776cabd301..fae46330b1 100644 --- a/examples/controllers/neuron/neuronAvatar.js +++ b/examples/controllers/neuron/neuronAvatar.js @@ -61,6 +61,70 @@ var JOINT_NAME_MAP = { LeftHandPinky3: "LeftHandPinky4" }; +var JOINT_PARENT_MAP = { + Hips: "", + RightUpLeg: "Hips", + RightLeg: "RightUpLeg", + RightFoot: "RightLeg", + LeftUpLeg: "Hips", + LeftLeg: "LeftUpLeg", + LeftFoot: "LeftLeg", + Spine: "Hips", + Spine1: "Spine", + Spine2: "Spine1", + Spine3: "Spine2", + Neck: "Spine3", + Head: "Neck", + RightShoulder: "Spine3", + RightArm: "RightShoulder", + RightForeArm: "RightArm", + RightHand: "RightForeArm", + RightHandThumb1: "RightHand", + RightHandThumb2: "RightHandThumb1", + RightHandThumb3: "RightHandThumb2", + RightHandThumb4: "RightHandThumb3", + RightHandIndex1: "RightHand", + RightHandIndex2: "RightHandIndex1", + RightHandIndex3: "RightHandIndex2", + RightHandIndex4: "RightHandIndex3", + RightHandMiddle1: "RightHand", + RightHandMiddle2: "RightHandMiddle1", + RightHandMiddle3: "RightHandMiddle2", + RightHandMiddle4: "RightHandMiddle3", + RightHandRing1: "RightHand", + RightHandRing2: "RightHandRing1", + RightHandRing3: "RightHandRing2", + RightHandRing4: "RightHandRing3", + RightHandPinky1: "RightHand", + RightHandPinky2: "RightHandPinky1", + RightHandPinky3: "RightHandPinky2", + RightHandPinky4: "RightHandPinky3", + LeftShoulder: "Spine3", + LeftArm: "LeftShoulder", + LeftForeArm: "LeftArm", + LeftHand: "LeftForeArm", + LeftHandThumb1: "LeftHand", + LeftHandThumb2: "LeftHandThumb1", + LeftHandThumb3: "LeftHandThumb2", + LeftHandThumb4: "LeftHandThumb3", + LeftHandIndex1: "LeftHand", + LeftHandIndex2: "LeftHandIndex1", + LeftHandIndex3: "LeftHandIndex2", + LeftHandIndex4: "LeftHandIndex3", + LeftHandMiddle1: "LeftHand", + LeftHandMiddle2: "LeftHandMiddle1", + LeftHandMiddle3: "LeftHandMiddle2", + LeftHandMiddle4: "LeftHandMiddle3", + LeftHandRing1: "LeftHand", + LeftHandRing2: "LeftHandRing1", + LeftHandRing3: "LeftHandRing2", + LeftHandRing4: "LeftHandRing3", + LeftHandPinky1: "LeftHand", + LeftHandPinky2: "LeftHandPinky1", + LeftHandPinky3: "LeftHandPinky2", + LeftHandPinky: "LeftHandPinky3", +}; + function dumpHardwareMapping() { Object.keys(Controller.Hardware).forEach(function (deviceName) { Object.keys(Controller.Hardware[deviceName]).forEach(function (input) { @@ -103,6 +167,21 @@ NeuronAvatar.prototype.activate = function () { Script.update.connect(updateCallback); } this._active = true; + + // build absDefaultPoseMap + this._absDefaultRotMap = {}; + var keys = Object.keys(JOINT_NAME_MAP); + var i, l = keys.length; + for (i = 0; i < l; i++) { + var jointName = JOINT_NAME_MAP[keys[i]]; + var j = MyAvatar.getJointIndex(jointName); + var parentJointName = JOINT_PARENT_MAP[jointName]; + if (parentJointName === "") { + this._absDefaultRotMap[jointName] = MyAvatar.getDefaultJointRotation(j); + } else { + this._absDefaultRotMap[jointName] = Quat.multiply(this._absDefaultRotMap[parentJointName], MyAvatar.getDefaultJointRotation(j)); + } + } }; NeuronAvatar.prototype.deactivate = function () { @@ -117,14 +196,22 @@ NeuronAvatar.prototype.deactivate = function () { NeuronAvatar.prototype.update = function (deltaTime) { var keys = Object.keys(JOINT_NAME_MAP); var i, l = keys.length; + var absDefaultRot = {}; for (i = 0; i < l; i++) { var channel = Controller.Hardware.Neuron[keys[i]]; if (channel) { var pose = Controller.getPoseValue(channel); - var j = MyAvatar.getJointIndex(JOINT_NAME_MAP[keys[i]]); - var defaultRot = MyAvatar.getDefaultJointRotation(j); - var rot = Quat.multiply(Quat.inverse(defaultRot), Quat.multiply(pose.rotation, defaultRot)); - MyAvatar.setJointRotation(j, Quat.multiply(defaultRot, rot)); + var jointName = JOINT_NAME_MAP[keys[i]]; + var parentJointName = JOINT_PARENT_MAP[jointName]; + var j = MyAvatar.getJointIndex(jointName); + var defaultAbsRot = this._absDefaultRotMap[jointName]; + var parentDefaultAbsRot; + if (parentJointName === "") { + parentDefaultAbsRot = {x: 0, y: 0, z: 0, w: 1}; + } else { + parentDefaultAbsRot = this._absDefaultRotMap[parentJointName]; + } + MyAvatar.setJointRotation(j, Quat.multiply(Quat.inverse(parentDefaultAbsRot), Quat.multiply(pose.rotation, defaultAbsRot))); //MyAvatar.setJointTranslation(j, Vec3.multiply(100.0, pose.translation)); } } diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 152bce913d..ae438c10e0 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -107,21 +107,37 @@ void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx // version 1.0 if (header->DataVersion.Major == 1 && header->DataVersion.Minor == 0) { - std::lock_guard guard(neuronPlugin->_jointsMutex); - - // Data is 6 floats: 3 position values, 3 rotation euler angles (degrees) - - // resize vector if necessary - const size_t NUM_FLOATS_PER_JOINT = 6; - const size_t NUM_JOINTS = header->DataCount / NUM_FLOATS_PER_JOINT; - if (neuronPlugin->_joints.size() != NUM_JOINTS) { - neuronPlugin->_joints.resize(NUM_JOINTS, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); + // skip reference joint if present + if (header->WithReference && header->WithDisp) { + data += 6; + } else if (header->WithReference && !header->WithDisp) { + data += 3; } - assert(sizeof(NeuronPlugin::NeuronJoint) == (NUM_FLOATS_PER_JOINT * sizeof(float))); + if (header->WithDisp) { + // enter mutex + std::lock_guard guard(neuronPlugin->_jointsMutex); - // copy the data - memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); + // + // Data is 6 floats per joint: 3 position values, 3 rotation euler angles (degrees) + // + + // resize vector if necessary + const size_t NUM_FLOATS_PER_JOINT = 6; + const size_t NUM_JOINTS = header->DataCount / NUM_FLOATS_PER_JOINT; + if (neuronPlugin->_joints.size() != NUM_JOINTS) { + neuronPlugin->_joints.resize(NUM_JOINTS, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); + } + + assert(sizeof(NeuronPlugin::NeuronJoint) == (NUM_FLOATS_PER_JOINT * sizeof(float))); + + // copy the data + memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); + } else { + + qCCritical(inputplugins) << "NeruonPlugin: format not supported"; + + } } else { static bool ONCE = false; @@ -368,18 +384,6 @@ void NeuronPlugin::InputDevice::update(float deltaTime, const std::vector Date: Fri, 25 Dec 2015 09:57:50 -0800 Subject: [PATCH 25/43] Set up controller poses to match hifi standard skeleton Neuron plugin in fills in the gap (thumb1) with identity. Updated neuronAvatar script to work with new controller pose names. --- examples/controllers/neuron/neuronAvatar.js | 97 +----- .../src/controllers/StandardControls.h | 20 +- plugins/hifiNeuron/src/NeuronPlugin.cpp | 324 +++++++++++------- plugins/hifiNeuron/src/NeuronPlugin.h | 10 +- 4 files changed, 245 insertions(+), 206 deletions(-) diff --git a/examples/controllers/neuron/neuronAvatar.js b/examples/controllers/neuron/neuronAvatar.js index fae46330b1..cc53987f05 100644 --- a/examples/controllers/neuron/neuronAvatar.js +++ b/examples/controllers/neuron/neuronAvatar.js @@ -1,66 +1,3 @@ -// maps controller joint names to avatar joint names -var JOINT_NAME_MAP = { - Hips: "Hips", - RightUpLeg: "RightUpLeg", - RightLeg: "RightLeg", - RightFoot: "RightFoot", - LeftUpLeg: "LeftUpLeg", - LeftLeg: "LeftLeg", - LeftFoot: "LeftFoot", - Spine: "Spine", - Spine1: "Spine1", - Spine2: "Spine2", - Spine3: "Spine3", - Neck: "Neck", - Head: "Head", - RightShoulder: "RightShoulder", - RightArm: "RightArm", - RightForeArm: "RightForeArm", - RightHand: "RightHand", - RightHandThumb1: "RightHandThumb2", - RightHandThumb2: "RightHandThumb3", - RightHandThumb3: "RightHandThumb4", - RightInHandIndex: "RightHandIndex1", - RightHandIndex1: "RightHandIndex2", - RightHandIndex2: "RightHandIndex3", - RightHandIndex3: "RightHandIndex4", - RightInHandMiddle: "RightHandMiddle1", - RightHandMiddle1: "RightHandMiddle2", - RightHandMiddle2: "RightHandMiddle3", - RightHandMiddle3: "RightHandMiddle4", - RightInHandRing: "RightHandRing1", - RightHandRing1: "RightHandRing2", - RightHandRing2: "RightHandRing3", - RightHandRing3: "RightHandRing4", - RightInHandPinky: "RightHandPinky1", - RightHandPinky1: "RightHandPinky2", - RightHandPinky2: "RightHandPinky3", - RightHandPinky3: "RightHandPinky4", - LeftShoulder: "LeftShoulder", - LeftArm: "LeftArm", - LeftForeArm: "LeftForeArm", - LeftHand: "LeftHand", - LeftHandThumb1: "LeftHandThumb2", - LeftHandThumb2: "LeftHandThumb3", - LeftHandThumb3: "LeftHandThumb4", - LeftInHandIndex: "LeftHandIndex1", - LeftHandIndex1: "LeftHandIndex2", - LeftHandIndex2: "LeftHandIndex3", - LeftHandIndex3: "LeftHandIndex4", - LeftInHandMiddle: "LeftHandMiddle1", - LeftHandMiddle1: "LeftHandMiddle2", - LeftHandMiddle2: "LeftHandMiddle3", - LeftHandMiddle3: "LeftHandMiddle4", - LeftInHandRing: "LeftHandRing1", - LeftHandRing1: "LeftHandRing2", - LeftHandRing2: "LeftHandRing3", - LeftHandRing3: "LeftHandRing4", - LeftInHandPinky: "LeftHandPinky1", - LeftHandPinky1: "LeftHandPinky2", - LeftHandPinky2: "LeftHandPinky3", - LeftHandPinky3: "LeftHandPinky4" -}; - var JOINT_PARENT_MAP = { Hips: "", RightUpLeg: "Hips", @@ -170,10 +107,11 @@ NeuronAvatar.prototype.activate = function () { // build absDefaultPoseMap this._absDefaultRotMap = {}; - var keys = Object.keys(JOINT_NAME_MAP); + this._absDefaultRotMap[""] = {x: 0, y: 0, z: 0, w: 1}; + var keys = Object.keys(JOINT_PARENT_MAP); var i, l = keys.length; for (i = 0; i < l; i++) { - var jointName = JOINT_NAME_MAP[keys[i]]; + var jointName = keys[i]; var j = MyAvatar.getJointIndex(jointName); var parentJointName = JOINT_PARENT_MAP[jointName]; if (parentJointName === "") { @@ -194,24 +132,27 @@ NeuronAvatar.prototype.deactivate = function () { }; NeuronAvatar.prototype.update = function (deltaTime) { - var keys = Object.keys(JOINT_NAME_MAP); + var keys = Object.keys(JOINT_PARENT_MAP); var i, l = keys.length; var absDefaultRot = {}; + var jointName, channel, pose, parentJointName, j, parentDefaultAbsRot; for (i = 0; i < l; i++) { - var channel = Controller.Hardware.Neuron[keys[i]]; + var jointName = keys[i]; + var channel = Controller.Hardware.Neuron[jointName]; if (channel) { - var pose = Controller.getPoseValue(channel); - var jointName = JOINT_NAME_MAP[keys[i]]; - var parentJointName = JOINT_PARENT_MAP[jointName]; - var j = MyAvatar.getJointIndex(jointName); - var defaultAbsRot = this._absDefaultRotMap[jointName]; - var parentDefaultAbsRot; - if (parentJointName === "") { - parentDefaultAbsRot = {x: 0, y: 0, z: 0, w: 1}; - } else { - parentDefaultAbsRot = this._absDefaultRotMap[parentJointName]; - } + pose = Controller.getPoseValue(channel); + parentJointName = JOINT_PARENT_MAP[jointName]; + j = MyAvatar.getJointIndex(jointName); + defaultAbsRot = this._absDefaultRotMap[jointName]; + parentDefaultAbsRot = this._absDefaultRotMap[parentJointName]; + + // Rotations from the neuron controller are in world orientation but are delta's from the default pose. + // So first we build the absolute rotation of the default pose (local into world). + // Then apply the rotation from the controller, in world space. + // Then we transform back into joint local by multiplying by the inverse of the parents absolute rotation. MyAvatar.setJointRotation(j, Quat.multiply(Quat.inverse(parentDefaultAbsRot), Quat.multiply(pose.rotation, defaultAbsRot))); + + // TODO: //MyAvatar.setJointTranslation(j, Vec3.multiply(100.0, pose.translation)); } } diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index 4294713238..2b0613321e 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -108,22 +108,23 @@ namespace controller { RIGHT_HAND_THUMB1, RIGHT_HAND_THUMB2, RIGHT_HAND_THUMB3, - RIGHT_IN_HAND_INDEX, + RIGHT_HAND_THUMB4, RIGHT_HAND_INDEX1, RIGHT_HAND_INDEX2, RIGHT_HAND_INDEX3, - RIGHT_IN_HAND_MIDDLE, + RIGHT_HAND_INDEX4, RIGHT_HAND_MIDDLE1, RIGHT_HAND_MIDDLE2, RIGHT_HAND_MIDDLE3, - RIGHT_IN_HANDRING, + RIGHT_HAND_MIDDLE4, RIGHT_HAND_RING1, RIGHT_HAND_RING2, RIGHT_HAND_RING3, - RIGHT_IN_HAND_PINKY, + RIGHT_HAND_RING4, RIGHT_HAND_PINKY1, RIGHT_HAND_PINKY2, RIGHT_HAND_PINKY3, + RIGHT_HAND_PINKY4, LEFT_SHOULDER, LEFT_ARM, LEFT_FORE_ARM, @@ -131,28 +132,29 @@ namespace controller { LEFT_HAND_THUMB1, LEFT_HAND_THUMB2, LEFT_HAND_THUMB3, - LEFT_IN_HAND_INDEX, + LEFT_HAND_THUMB4, LEFT_HAND_INDEX1, LEFT_HAND_INDEX2, LEFT_HAND_INDEX3, - LEFT_IN_HAND_MIDDLE, + LEFT_HAND_INDEX4, LEFT_HAND_MIDDLE1, LEFT_HAND_MIDDLE2, LEFT_HAND_MIDDLE3, - LEFT_IN_HAND_RING, + LEFT_HAND_MIDDLE4, LEFT_HAND_RING1, LEFT_HAND_RING2, LEFT_HAND_RING3, - LEFT_IN_HAND_PINKY, + LEFT_HAND_RING4, LEFT_HAND_PINKY1, LEFT_HAND_PINKY2, LEFT_HAND_PINKY3, + LEFT_HAND_PINKY4, NUM_STANDARD_POSES }; enum StandardCounts { TRIGGERS = 2, ANALOG_STICKS = 2, - POSES = 2, // FIXME 3? if we want to expose the head? + POSES = NUM_STANDARD_POSES }; } diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index ae438c10e0..4edda64c22 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -29,8 +29,10 @@ Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") const QString NeuronPlugin::NAME = "Neuron"; const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; -// This matches controller::StandardPoseChannel -enum JointIndex { +// indices of joints of the Neuron standard skeleton. +// This is 'almost' the same as the High Fidelity standard skeleton. +// It is missing a thumb joint. +enum NeuronJointIndex { Hips = 0, RightUpLeg, RightLeg, @@ -93,12 +95,160 @@ enum JointIndex { Size }; -bool NeuronPlugin::isSupported() const { - // Because it's a client/server network architecture, we can't tell - // if the neuron is actually connected until we connect to the server. - return true; +// Almost a direct mapping except for LEFT_HAND_THUMB1 and RIGHT_HAND_THUMB1, +// which are not present in the Neuron standard skeleton. +static controller::StandardPoseChannel neuronJointIndexToPoseIndexMap[NeuronJointIndex::Size] = { + controller::HIPS, + controller::RIGHT_UP_LEG, + controller::RIGHT_LEG, + controller::RIGHT_FOOT, + controller::LEFT_UP_LEG, + controller::LEFT_LEG, + controller::LEFT_FOOT, + controller::SPINE, + controller::SPINE1, + controller::SPINE2, + controller::SPINE3, + controller::NECK, + controller::HEAD, + controller::RIGHT_SHOULDER, + controller::RIGHT_ARM, + controller::RIGHT_FORE_ARM, + controller::RIGHT_HAND, + controller::RIGHT_HAND_THUMB2, + controller::RIGHT_HAND_THUMB3, + controller::RIGHT_HAND_THUMB4, + controller::RIGHT_HAND_INDEX1, + controller::RIGHT_HAND_INDEX2, + controller::RIGHT_HAND_INDEX3, + controller::RIGHT_HAND_INDEX4, + controller::RIGHT_HAND_MIDDLE1, + controller::RIGHT_HAND_MIDDLE2, + controller::RIGHT_HAND_MIDDLE3, + controller::RIGHT_HAND_MIDDLE4, + controller::RIGHT_HAND_RING1, + controller::RIGHT_HAND_RING2, + controller::RIGHT_HAND_RING3, + controller::RIGHT_HAND_RING4, + controller::RIGHT_HAND_PINKY1, + controller::RIGHT_HAND_PINKY2, + controller::RIGHT_HAND_PINKY3, + controller::RIGHT_HAND_PINKY4, + controller::LEFT_SHOULDER, + controller::LEFT_ARM, + controller::LEFT_FORE_ARM, + controller::LEFT_HAND, + controller::LEFT_HAND_THUMB2, + controller::LEFT_HAND_THUMB3, + controller::LEFT_HAND_THUMB4, + controller::LEFT_HAND_INDEX1, + controller::LEFT_HAND_INDEX2, + controller::LEFT_HAND_INDEX3, + controller::LEFT_HAND_INDEX4, + controller::LEFT_HAND_MIDDLE1, + controller::LEFT_HAND_MIDDLE2, + controller::LEFT_HAND_MIDDLE3, + controller::LEFT_HAND_MIDDLE4, + controller::LEFT_HAND_RING1, + controller::LEFT_HAND_RING2, + controller::LEFT_HAND_RING3, + controller::LEFT_HAND_RING4, + controller::LEFT_HAND_PINKY1, + controller::LEFT_HAND_PINKY2, + controller::LEFT_HAND_PINKY3, + controller::LEFT_HAND_PINKY4 +}; + +static controller::StandardPoseChannel neuronJointIndexToPoseIndex(NeuronJointIndex i) { + assert(i >= 0 && i < NeuronJointIndex::Size); + if (i >= 0 && i < NeuronJointIndex::Size) { + return neuronJointIndexToPoseIndexMap[i]; + } else { + return (controller::StandardPoseChannel)0; // not sure what to do here, but don't crash! + } } +static const char* controllerJointName(controller::StandardPoseChannel i) { + switch (i) { + case controller::HIPS: return "Hips"; + case controller::RIGHT_UP_LEG: return "RightUpLeg"; + case controller::RIGHT_LEG: return "RightLeg"; + case controller::RIGHT_FOOT: return "RightFoot"; + case controller::LEFT_UP_LEG: return "LeftUpLeg"; + case controller::LEFT_LEG: return "LeftLeg"; + case controller::LEFT_FOOT: return "LeftFoot"; + case controller::SPINE: return "Spine"; + case controller::SPINE1: return "Spine1"; + case controller::SPINE2: return "Spine2"; + case controller::SPINE3: return "Spine3"; + case controller::NECK: return "Neck"; + case controller::HEAD: return "Head"; + case controller::RIGHT_SHOULDER: return "RightShoulder"; + case controller::RIGHT_ARM: return "RightArm"; + case controller::RIGHT_FORE_ARM: return "RightForeArm"; + case controller::RIGHT_HAND: return "RightHand"; + case controller::RIGHT_HAND_THUMB1: return "RightHandThumb1"; + case controller::RIGHT_HAND_THUMB2: return "RightHandThumb2"; + case controller::RIGHT_HAND_THUMB3: return "RightHandThumb3"; + case controller::RIGHT_HAND_THUMB4: return "RightHandThumb4"; + case controller::RIGHT_HAND_INDEX1: return "RightHandIndex1"; + case controller::RIGHT_HAND_INDEX2: return "RightHandIndex2"; + case controller::RIGHT_HAND_INDEX3: return "RightHandIndex3"; + case controller::RIGHT_HAND_INDEX4: return "RightHandIndex4"; + case controller::RIGHT_HAND_MIDDLE1: return "RightHandMiddle1"; + case controller::RIGHT_HAND_MIDDLE2: return "RightHandMiddle2"; + case controller::RIGHT_HAND_MIDDLE3: return "RightHandMiddle3"; + case controller::RIGHT_HAND_MIDDLE4: return "RightHandMiddle4"; + case controller::RIGHT_HAND_RING1: return "RightHandRing1"; + case controller::RIGHT_HAND_RING2: return "RightHandRing2"; + case controller::RIGHT_HAND_RING3: return "RightHandRing3"; + case controller::RIGHT_HAND_RING4: return "RightHandRing4"; + case controller::RIGHT_HAND_PINKY1: return "RightHandPinky1"; + case controller::RIGHT_HAND_PINKY2: return "RightHandPinky2"; + case controller::RIGHT_HAND_PINKY3: return "RightHandPinky3"; + case controller::RIGHT_HAND_PINKY4: return "RightHandPinky4"; + case controller::LEFT_SHOULDER: return "LeftShoulder"; + case controller::LEFT_ARM: return "LeftArm"; + case controller::LEFT_FORE_ARM: return "LeftForeArm"; + case controller::LEFT_HAND: return "LeftHand"; + case controller::LEFT_HAND_THUMB1: return "LeftHandThumb1"; + case controller::LEFT_HAND_THUMB2: return "LeftHandThumb2"; + case controller::LEFT_HAND_THUMB3: return "LeftHandThumb3"; + case controller::LEFT_HAND_THUMB4: return "LeftHandThumb4"; + case controller::LEFT_HAND_INDEX1: return "LeftHandIndex1"; + case controller::LEFT_HAND_INDEX2: return "LeftHandIndex2"; + case controller::LEFT_HAND_INDEX3: return "LeftHandIndex3"; + case controller::LEFT_HAND_INDEX4: return "LeftHandIndex4"; + case controller::LEFT_HAND_MIDDLE1: return "LeftHandMiddle1"; + case controller::LEFT_HAND_MIDDLE2: return "LeftHandMiddle2"; + case controller::LEFT_HAND_MIDDLE3: return "LeftHandMiddle3"; + case controller::LEFT_HAND_MIDDLE4: return "LeftHandMiddle4"; + case controller::LEFT_HAND_RING1: return "LeftHandRing1"; + case controller::LEFT_HAND_RING2: return "LeftHandRing2"; + case controller::LEFT_HAND_RING3: return "LeftHandRing3"; + case controller::LEFT_HAND_RING4: return "LeftHandRing4"; + case controller::LEFT_HAND_PINKY1: return "LeftHandPinky1"; + case controller::LEFT_HAND_PINKY2: return "LeftHandPinky2"; + case controller::LEFT_HAND_PINKY3: return "LeftHandPinky3"; + case controller::LEFT_HAND_PINKY4: return "LeftHandPinky4"; + default: return "???"; + } +} + +// convert between YXZ neuron euler angles in degrees to quaternion +// this is the default setting in the Axis Neuron server. +static quat eulerToQuat(vec3 euler) { + // euler.x and euler.y are swaped, WTF. + glm::vec3 e = glm::vec3(euler.y, euler.x, euler.z) * RADIANS_PER_DEGREE; + return (glm::angleAxis(e.y, Vectors::UNIT_Y) * + glm::angleAxis(e.x, Vectors::UNIT_X) * + glm::angleAxis(e.z, Vectors::UNIT_Z)); +} + +// +// neuronDataReader SDK callback functions +// + // NOTE: must be thread-safe void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx* header, float* data) { @@ -133,7 +283,28 @@ void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx // copy the data memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); + } else { + // enter mutex + std::lock_guard guard(neuronPlugin->_jointsMutex); + + // + // Data is 3 floats per joint: 3 rotation euler angles (degrees) + // + + // resize vector if necessary + const size_t NUM_FLOATS_PER_JOINT = 3; + const size_t NUM_JOINTS = header->DataCount / NUM_FLOATS_PER_JOINT; + if (neuronPlugin->_joints.size() != NUM_JOINTS) { + neuronPlugin->_joints.resize(NUM_JOINTS, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); + } + + assert(sizeof(NeuronPlugin::NeuronJoint) == (NUM_FLOATS_PER_JOINT * sizeof(float))); + + for (int i = 0; i < NUM_JOINTS; i++) { + // TODO: should probably just copy over default positions?! + memcpy(&(neuronPlugin->_joints[i].euler), data, sizeof(float) * NUM_FLOATS_PER_JOINT); + } qCCritical(inputplugins) << "NeruonPlugin: format not supported"; @@ -148,6 +319,9 @@ void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx } } +// I can't even get the SDK to send me a callback. +// BRCommandFetchAvatarDataFromServer & BRRegisterAutoSyncParmeter [sic] don't seem to work. +// So this is totally untested. // NOTE: must be thread-safe static void CommandDataReceivedCallback(void* context, SOCKET_REF sender, CommandPack* pack, void* data) { @@ -196,9 +370,20 @@ static void CommandDataReceivedCallback(void* context, SOCKET_REF sender, Comman // NOTE: must be thread-safe static void SocketStatusChangedCallback(void* context, SOCKET_REF sender, SocketStatus status, char* message) { + // just dump to log, later we might want to pop up a connection lost dialog or attempt to reconnect. qCDebug(inputplugins) << "NeuronPlugin: socket status = " << message; } +// +// NeuronPlugin +// + +bool NeuronPlugin::isSupported() const { + // Because it's a client/server network architecture, we can't tell + // if the neuron is actually connected until we connect to the server. + return true; +} + void NeuronPlugin::activate() { InputPlugin::activate(); @@ -206,28 +391,26 @@ void NeuronPlugin::activate() { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); - qCDebug(inputplugins) << "NeuronPlugin::activate"; - // register c-style callbacks BRRegisterFrameDataCallback((void*)this, FrameDataReceivedCallback); BRRegisterCommandDataCallback((void*)this, CommandDataReceivedCallback); BRRegisterSocketStatusCallback((void*)this, SocketStatusChangedCallback); - // TODO: pull these from prefs! + // TODO: Pull these from prefs dialog? + // localhost is fine for now. _serverAddress = "localhost"; - _serverPort = 7001; + _serverPort = 7001; // default port for TCP Axis Neuron server. + _socketRef = BRConnectTo((char*)_serverAddress.c_str(), _serverPort); if (!_socketRef) { // error - qCCritical(inputplugins) << "NeuronPlugin: error connecting to " << _serverAddress.c_str() << ":" << _serverPort << "error = " << BRGetLastErrorMessage(); + qCCritical(inputplugins) << "NeuronPlugin: error connecting to " << _serverAddress.c_str() << ":" << _serverPort << ", error = " << BRGetLastErrorMessage(); } else { qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; } } void NeuronPlugin::deactivate() { - qCDebug(inputplugins) << "NeuronPlugin::deactivate"; - // unregister from userInputMapper if (_inputDevice->_deviceID != controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); @@ -241,126 +424,35 @@ void NeuronPlugin::deactivate() { InputPlugin::deactivate(); } -// convert between euler in degrees to quaternion -static quat eulerToQuat(vec3 euler) { - // euler.x and euler.y are swaped (thanks NOMICOM!) - glm::vec3 e = glm::vec3(euler.y, euler.x, euler.z) * RADIANS_PER_DEGREE; - return (glm::angleAxis(e.y, Vectors::UNIT_Y) * glm::angleAxis(e.x, Vectors::UNIT_X) * glm::angleAxis(e.z, Vectors::UNIT_Z)); -} - void NeuronPlugin::pluginUpdate(float deltaTime, bool jointsCaptured) { - std::vector joints; - // copy the shared data { + // lock and copy std::lock_guard guard(_jointsMutex); joints = _joints; } - - /* - DebugDraw::getInstance().addMyAvatarMarker("LEFT_FOOT", - eulerToQuat(joints[6].euler), - joints[6].pos / 100.0f, - glm::vec4(1)); - */ _inputDevice->update(deltaTime, joints, _prevJoints); _prevJoints = joints; } void NeuronPlugin::saveSettings() const { InputPlugin::saveSettings(); - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::saveSettings"; } void NeuronPlugin::loadSettings() { InputPlugin::loadSettings(); - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::loadSettings"; } // // InputDevice // -static controller::StandardPoseChannel neuronJointIndexToPoseIndex(JointIndex i) { - // Currently they are the same. - // but that won't always be the case... - return (controller::StandardPoseChannel)i; -} - -static const char* neuronJointName(JointIndex i) { - switch (i) { - case Hips: return "Hips"; - case RightUpLeg: return "RightUpLeg"; - case RightLeg: return "RightLeg"; - case RightFoot: return "RightFoot"; - case LeftUpLeg: return "LeftUpLeg"; - case LeftLeg: return "LeftLeg"; - case LeftFoot: return "LeftFoot"; - case Spine: return "Spine"; - case Spine1: return "Spine1"; - case Spine2: return "Spine2"; - case Spine3: return "Spine3"; - case Neck: return "Neck"; - case Head: return "Head"; - case RightShoulder: return "RightShoulder"; - case RightArm: return "RightArm"; - case RightForeArm: return "RightForeArm"; - case RightHand: return "RightHand"; - case RightHandThumb1: return "RightHandThumb1"; - case RightHandThumb2: return "RightHandThumb2"; - case RightHandThumb3: return "RightHandThumb3"; - case RightInHandIndex: return "RightInHandIndex"; - case RightHandIndex1: return "RightHandIndex1"; - case RightHandIndex2: return "RightHandIndex2"; - case RightHandIndex3: return "RightHandIndex3"; - case RightInHandMiddle: return "RightInHandMiddle"; - case RightHandMiddle1: return "RightHandMiddle1"; - case RightHandMiddle2: return "RightHandMiddle2"; - case RightHandMiddle3: return "RightHandMiddle3"; - case RightInHandRing: return "RightInHandRing"; - case RightHandRing1: return "RightHandRing1"; - case RightHandRing2: return "RightHandRing2"; - case RightHandRing3: return "RightHandRing3"; - case RightInHandPinky: return "RightInHandPinky"; - case RightHandPinky1: return "RightHandPinky1"; - case RightHandPinky2: return "RightHandPinky2"; - case RightHandPinky3: return "RightHandPinky3"; - case LeftShoulder: return "LeftShoulder"; - case LeftArm: return "LeftArm"; - case LeftForeArm: return "LeftForeArm"; - case LeftHand: return "LeftHand"; - case LeftHandThumb1: return "LeftHandThumb1"; - case LeftHandThumb2: return "LeftHandThumb2"; - case LeftHandThumb3: return "LeftHandThumb3"; - case LeftInHandIndex: return "LeftInHandIndex"; - case LeftHandIndex1: return "LeftHandIndex1"; - case LeftHandIndex2: return "LeftHandIndex2"; - case LeftHandIndex3: return "LeftHandIndex3"; - case LeftInHandMiddle: return "LeftInHandMiddle"; - case LeftHandMiddle1: return "LeftHandMiddle1"; - case LeftHandMiddle2: return "LeftHandMiddle2"; - case LeftHandMiddle3: return "LeftHandMiddle3"; - case LeftInHandRing: return "LeftInHandRing"; - case LeftHandRing1: return "LeftHandRing1"; - case LeftHandRing2: return "LeftHandRing2"; - case LeftHandRing3: return "LeftHandRing3"; - case LeftInHandPinky: return "LeftInHandPinky"; - case LeftHandPinky1: return "LeftHandPinky1"; - case LeftHandPinky2: return "LeftHandPinky2"; - case LeftHandPinky3: return "LeftHandPinky3"; - default: return "???"; - } -} - controller::Input::NamedVector NeuronPlugin::InputDevice::getAvailableInputs() const { - // TODO: static controller::Input::NamedVector availableInputs; - if (availableInputs.size() == 0) { - for (int i = 0; i < JointIndex::Size; i++) { - availableInputs.push_back(makePair(neuronJointIndexToPoseIndex((JointIndex)i), neuronJointName((JointIndex)i))); + for (int i = 0; i < controller::NUM_STANDARD_POSES; i++) { + auto channel = static_cast(i); + availableInputs.push_back(makePair(channel, controllerJointName(channel))); } }; return availableInputs; @@ -373,21 +465,21 @@ QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { void NeuronPlugin::InputDevice::update(float deltaTime, const std::vector& joints, const std::vector& prevJoints) { for (int i = 0; i < joints.size(); i++) { - int poseIndex = neuronJointIndexToPoseIndex((JointIndex)i); glm::vec3 linearVel, angularVel; glm::vec3 pos = (joints[i].pos * METERS_PER_CENTIMETER); glm::quat rot = eulerToQuat(joints[i].euler); if (i < prevJoints.size()) { - linearVel = (pos - (prevJoints[i].pos * METERS_PER_CENTIMETER)) / deltaTime; - // quat log imag part points along the axis of rotation, and it's length will be the half angle. + linearVel = (pos - (prevJoints[i].pos * METERS_PER_CENTIMETER)) / deltaTime; // m/s + // quat log imaginary part points along the axis of rotation, with length of one half the angle of rotation. glm::quat d = glm::log(rot * glm::inverse(eulerToQuat(prevJoints[i].euler))); - angularVel = glm::vec3(d.x, d.y, d.z) / (0.5f * deltaTime); + angularVel = glm::vec3(d.x, d.y, d.z) / (0.5f * deltaTime); // radians/s } + int poseIndex = neuronJointIndexToPoseIndex((NeuronJointIndex)i); _poseStateMap[poseIndex] = controller::Pose(pos, rot, linearVel, angularVel); } -} -void NeuronPlugin::InputDevice::focusOutEvent() { - // TODO: - qCDebug(inputplugins) << "NeuronPlugin::InputDevice::focusOutEvent"; + // fill in missing thumb joints with identity. + // TODO: need a standard displacement + _poseStateMap[controller::RIGHT_HAND_THUMB1] = controller::Pose(glm::vec3(), glm::quat(), glm::vec3(), glm::vec3()); + _poseStateMap[controller::LEFT_HAND_THUMB1] = controller::Pose(glm::vec3(), glm::quat(), glm::vec3(), glm::vec3()); } diff --git a/plugins/hifiNeuron/src/NeuronPlugin.h b/plugins/hifiNeuron/src/NeuronPlugin.h index f787838ce2..c85a5dd383 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.h +++ b/plugins/hifiNeuron/src/NeuronPlugin.h @@ -57,7 +57,7 @@ protected: virtual controller::Input::NamedVector getAvailableInputs() const override; virtual QString getDefaultMappingConfig() const override; virtual void update(float deltaTime, bool jointsCaptured) override {}; - virtual void focusOutEvent() override; + virtual void focusOutEvent() override {}; void update(float deltaTime, const std::vector& joints, const std::vector& prevJoints); }; @@ -71,9 +71,13 @@ protected: int _serverPort; void* _socketRef; - std::vector _joints; - std::mutex _jointsMutex; // used to guard access to _joints + // used to guard multi-threaded access to _joints + std::mutex _jointsMutex; + // copy of data directly from the NeuronDataReader SDK + std::vector _joints; + + // one frame old copy of _joints, used to caluclate angular and linear velocity. std::vector _prevJoints; }; From fd3eed3d4132e58ff5e58284ac57e1feb9ad9ae1 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Sat, 26 Dec 2015 11:50:54 -0800 Subject: [PATCH 26/43] NeruonPlugin: translation support Warn if translations are not present. --- examples/controllers/neuron/neuronAvatar.js | 10 +- plugins/hifiNeuron/src/NeuronPlugin.cpp | 100 +++++++++++++++----- 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/examples/controllers/neuron/neuronAvatar.js b/examples/controllers/neuron/neuronAvatar.js index cc53987f05..a124316f24 100644 --- a/examples/controllers/neuron/neuronAvatar.js +++ b/examples/controllers/neuron/neuronAvatar.js @@ -62,6 +62,8 @@ var JOINT_PARENT_MAP = { LeftHandPinky: "LeftHandPinky3", }; +var USE_TRANSLATIONS = false; + function dumpHardwareMapping() { Object.keys(Controller.Hardware).forEach(function (deviceName) { Object.keys(Controller.Hardware[deviceName]).forEach(function (input) { @@ -152,8 +154,12 @@ NeuronAvatar.prototype.update = function (deltaTime) { // Then we transform back into joint local by multiplying by the inverse of the parents absolute rotation. MyAvatar.setJointRotation(j, Quat.multiply(Quat.inverse(parentDefaultAbsRot), Quat.multiply(pose.rotation, defaultAbsRot))); - // TODO: - //MyAvatar.setJointTranslation(j, Vec3.multiply(100.0, pose.translation)); + // translation proportions might be different from the neuron avatar and the user avatar skeleton. + // so this is disabled by default + if (USE_TRANSLATIONS) { + var localTranslation = Vec3.multiplyQbyV(Quat.inverse(parentDefaultAbsRot), pose.translation); + MyAvatar.setJointTranslation(j, localTranslation); + } } } }; diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 4edda64c22..0e338c30f8 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -159,6 +159,73 @@ static controller::StandardPoseChannel neuronJointIndexToPoseIndexMap[NeuronJoin controller::LEFT_HAND_PINKY4 }; +// in rig frame +static glm::vec3 rightHandThumb1DefaultAbsTranslation(-2.155500650405884, -0.7610001564025879, 2.685631036758423); +static glm::vec3 leftHandThumb1DefaultAbsTranslation(2.1555817127227783, -0.7603635787963867, 2.6856393814086914); + +// default translations (cm) +static glm::vec3 neuronJointTranslations[NeuronJointIndex::Size] = { + {131.901, 95.6602, -27.9815}, + {-9.55907, -1.58772, 0.0760284}, + {0.0144232, -41.4683, -0.105322}, + {1.59348, -41.5875, -0.557237}, + {9.72077, -1.68926, -0.280643}, + {0.0886684, -43.1586, -0.0111596}, + {-2.98473, -44.0517, 0.0694456}, + {0.110967, 16.3959, 0.140463}, + {0.0500451, 10.0238, 0.0731921}, + {0.061568, 10.4352, 0.0583075}, + {0.0500606, 10.0217, 0.0711083}, + {0.0317731, 10.7176, 0.0779325}, + {-0.0204253, 9.71067, 0.131734}, + {-3.24245, 7.13584, 0.185638}, + {-13.0885, -0.0877601, 0.176065}, + {-27.2674, 0.0688724, 0.0272146}, + {-26.7673, 0.0301916, 0.0102847}, + {-2.56017, 0.195537, 3.20968}, + {-3.78796, 0, 0}, + {-2.63141, 0, 0}, + {-3.31579, 0.522947, 2.03495}, + {-5.36589, -0.0939789, 1.02771}, + {-3.72278, 0, 0}, + {-2.11074, 0, 0}, + {-3.47874, 0.532042, 0.778358}, + {-5.32194, -0.0864, 0.322863}, + {-4.06232, 0, 0}, + {-2.54653, 0, 0}, + {-3.46131, 0.553263, -0.132632}, + {-4.76716, -0.0227368, -0.492632}, + {-3.54073, 0, 0}, + {-2.45634, 0, 0}, + {-3.25137, 0.482779, -1.23613}, + {-4.25937, -0.0227368, -1.12168}, + {-2.83528, 0, 0}, + {-1.79166, 0, 0}, + {3.25624, 7.13148, -0.131575}, + {13.149, -0.052598, -0.125076}, + {27.2903, 0.00282644, -0.0181535}, + {26.6602, 0.000969969, -0.0487599}, + {2.56017, 0.195537, 3.20968}, + {3.78796, 0, 0}, + {2.63141, 0, 0}, + {3.31579, 0.522947, 2.03495}, + {5.36589, -0.0939789, 1.02771}, + {3.72278, 0, 0}, + {2.11074, 0, 0}, + {3.47874, 0.532042, 0.778358}, + {5.32194, -0.0864, 0.322863}, + {4.06232, 0, 0}, + {2.54653, 0, 0}, + {3.46131, 0.553263, -0.132632}, + {4.76716, -0.0227368, -0.492632}, + {3.54073, 0, 0}, + {2.45634, 0, 0}, + {3.25137, 0.482779, -1.23613}, + {4.25937, -0.0227368, -1.12168}, + {2.83528, 0, 0}, + {1.79166, 0, 0} +}; + static controller::StandardPoseChannel neuronJointIndexToPoseIndex(NeuronJointIndex i) { assert(i >= 0 && i < NeuronJointIndex::Size); if (i >= 0 && i < NeuronJointIndex::Size) { @@ -285,31 +352,20 @@ void FrameDataReceivedCallback(void* context, SOCKET_REF sender, BvhDataHeaderEx memcpy(&(neuronPlugin->_joints[0]), data, sizeof(NeuronPlugin::NeuronJoint) * NUM_JOINTS); } else { + qCWarning(inputplugins) << "NeuronPlugin: unsuported binary format, please enable displacements"; + // enter mutex std::lock_guard guard(neuronPlugin->_jointsMutex); - // - // Data is 3 floats per joint: 3 rotation euler angles (degrees) - // - - // resize vector if necessary - const size_t NUM_FLOATS_PER_JOINT = 3; - const size_t NUM_JOINTS = header->DataCount / NUM_FLOATS_PER_JOINT; - if (neuronPlugin->_joints.size() != NUM_JOINTS) { - neuronPlugin->_joints.resize(NUM_JOINTS, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); + if (neuronPlugin->_joints.size() != NeuronJointIndex::Size) { + neuronPlugin->_joints.resize(NeuronJointIndex::Size, { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); } - assert(sizeof(NeuronPlugin::NeuronJoint) == (NUM_FLOATS_PER_JOINT * sizeof(float))); - - for (int i = 0; i < NUM_JOINTS; i++) { - // TODO: should probably just copy over default positions?! - memcpy(&(neuronPlugin->_joints[i].euler), data, sizeof(float) * NUM_FLOATS_PER_JOINT); + for (int i = 0; i < NeuronJointIndex::Size; i++) { + neuronPlugin->_joints[i].euler = glm::vec3(); + neuronPlugin->_joints[i].pos = neuronJointTranslations[i]; } - - qCCritical(inputplugins) << "NeruonPlugin: format not supported"; - } - } else { static bool ONCE = false; if (!ONCE) { @@ -466,7 +522,7 @@ QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { void NeuronPlugin::InputDevice::update(float deltaTime, const std::vector& joints, const std::vector& prevJoints) { for (int i = 0; i < joints.size(); i++) { glm::vec3 linearVel, angularVel; - glm::vec3 pos = (joints[i].pos * METERS_PER_CENTIMETER); + glm::vec3 pos = joints[i].pos; glm::quat rot = eulerToQuat(joints[i].euler); if (i < prevJoints.size()) { linearVel = (pos - (prevJoints[i].pos * METERS_PER_CENTIMETER)) / deltaTime; // m/s @@ -478,8 +534,6 @@ void NeuronPlugin::InputDevice::update(float deltaTime, const std::vector Date: Sat, 26 Dec 2015 12:13:29 -0800 Subject: [PATCH 27/43] NeuronPlugin: register for combination mode This will tell us, if user is using arms, upper-body or full body configurations. --- plugins/hifiNeuron/src/NeuronPlugin.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 0e338c30f8..97131a0a87 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -463,6 +463,8 @@ void NeuronPlugin::activate() { qCCritical(inputplugins) << "NeuronPlugin: error connecting to " << _serverAddress.c_str() << ":" << _serverPort << ", error = " << BRGetLastErrorMessage(); } else { qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; + + BRRegisterAutoSyncParmeter(_socketRef, Cmd_CombinationMode); } } @@ -474,6 +476,7 @@ void NeuronPlugin::deactivate() { } if (_socketRef) { + BRUnregisterAutoSyncParmeter(_socketRef, Cmd_CombinationMode); BRCloseSocket(_socketRef); } From 1f834a9c01f3343368cc89f1df87c4f1e7e6c957 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Sun, 27 Dec 2015 16:29:30 -0800 Subject: [PATCH 28/43] neruonAvatar.js: attempt to adjust hips for HMD mode. It attempts to adjust the hips so that the avatar's head is at the same location & orientation as the HMD in world space, however it's fighting with the internal c++ code that also attempts to adjust the hips as well. --- examples/controllers/neuron/neuronAvatar.js | 78 ++++++++++++++++++--- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/examples/controllers/neuron/neuronAvatar.js b/examples/controllers/neuron/neuronAvatar.js index a124316f24..abd51f2990 100644 --- a/examples/controllers/neuron/neuronAvatar.js +++ b/examples/controllers/neuron/neuronAvatar.js @@ -64,6 +64,27 @@ var JOINT_PARENT_MAP = { var USE_TRANSLATIONS = false; +// ctor +function Xform(rot, pos) { + this.rot = rot; + this.pos = pos; +}; +Xform.mul = function (lhs, rhs) { + var rot = Quat.multiply(lhs.rot, rhs.rot); + var pos = Vec3.sum(lhs.pos, Vec3.multiplyQbyV(lhs.rot, rhs.pos)); + return new Xform(rot, pos); +}; +Xform.prototype.inv = function () { + var invRot = Quat.inverse(this.rot); + var invPos = Vec3.multiply(-1, this.pos); + return new Xform(invRot, Vec3.multiplyQbyV(invRot, invPos)); +}; +Xform.prototype.toString = function () { + var rot = this.rot; + var pos = this.pos; + return "Xform rot = (" + rot.x + ", " + rot.y + ", " + rot.z + ", " + rot.w + "), pos = (" + pos.x + ", " + pos.y + ", " + pos.z + ")"; +}; + function dumpHardwareMapping() { Object.keys(Controller.Hardware).forEach(function (deviceName) { Object.keys(Controller.Hardware[deviceName]).forEach(function (input) { @@ -108,19 +129,19 @@ NeuronAvatar.prototype.activate = function () { this._active = true; // build absDefaultPoseMap - this._absDefaultRotMap = {}; - this._absDefaultRotMap[""] = {x: 0, y: 0, z: 0, w: 1}; + this._defaultAbsRotMap = {}; + this._defaultAbsPosMap = {}; + this._defaultAbsRotMap[""] = {x: 0, y: 0, z: 0, w: 1}; + this._defaultAbsPosMap[""] = {x: 0, y: 0, z: 0}; var keys = Object.keys(JOINT_PARENT_MAP); var i, l = keys.length; for (i = 0; i < l; i++) { var jointName = keys[i]; var j = MyAvatar.getJointIndex(jointName); var parentJointName = JOINT_PARENT_MAP[jointName]; - if (parentJointName === "") { - this._absDefaultRotMap[jointName] = MyAvatar.getDefaultJointRotation(j); - } else { - this._absDefaultRotMap[jointName] = Quat.multiply(this._absDefaultRotMap[parentJointName], MyAvatar.getDefaultJointRotation(j)); - } + this._defaultAbsRotMap[jointName] = Quat.multiply(this._defaultAbsRotMap[parentJointName], MyAvatar.getDefaultJointRotation(j)); + this._defaultAbsPosMap[jointName] = Vec3.sum(this._defaultAbsPosMap[parentJointName], + Quat.multiply(this._defaultAbsRotMap[parentJointName], MyAvatar.getDefaultJointTranslation(j))); } }; @@ -134,10 +155,16 @@ NeuronAvatar.prototype.deactivate = function () { }; NeuronAvatar.prototype.update = function (deltaTime) { + + var hmdActive = HMD.active; + var hmdXform = new Xform(HMD.orientation, HMD.position); + var keys = Object.keys(JOINT_PARENT_MAP); var i, l = keys.length; var absDefaultRot = {}; var jointName, channel, pose, parentJointName, j, parentDefaultAbsRot; + var localRotations = {}; + var localTranslations = {}; for (i = 0; i < l; i++) { var jointName = keys[i]; var channel = Controller.Hardware.Neuron[jointName]; @@ -145,23 +172,54 @@ NeuronAvatar.prototype.update = function (deltaTime) { pose = Controller.getPoseValue(channel); parentJointName = JOINT_PARENT_MAP[jointName]; j = MyAvatar.getJointIndex(jointName); - defaultAbsRot = this._absDefaultRotMap[jointName]; - parentDefaultAbsRot = this._absDefaultRotMap[parentJointName]; + defaultAbsRot = this._defaultAbsRotMap[jointName]; + parentDefaultAbsRot = this._defaultAbsRotMap[parentJointName]; // Rotations from the neuron controller are in world orientation but are delta's from the default pose. // So first we build the absolute rotation of the default pose (local into world). // Then apply the rotation from the controller, in world space. // Then we transform back into joint local by multiplying by the inverse of the parents absolute rotation. - MyAvatar.setJointRotation(j, Quat.multiply(Quat.inverse(parentDefaultAbsRot), Quat.multiply(pose.rotation, defaultAbsRot))); + var localRotation = Quat.multiply(Quat.inverse(parentDefaultAbsRot), Quat.multiply(pose.rotation, defaultAbsRot)); + if (!hmdActive || jointName !== "Hips") { + MyAvatar.setJointRotation(j, localRotation); + } + localRotations[jointName] = localRotation; // translation proportions might be different from the neuron avatar and the user avatar skeleton. // so this is disabled by default if (USE_TRANSLATIONS) { var localTranslation = Vec3.multiplyQbyV(Quat.inverse(parentDefaultAbsRot), pose.translation); MyAvatar.setJointTranslation(j, localTranslation); + localTranslations[jointName] = localTranslation; + } else { + localTranslations[jointName] = MyAvatar.getJointTranslation(j); } } } + + // TODO: Currrently does not work. + // it attempts to adjust the hips so that the avatar's head is at the same location & oreintation as the HMD. + // however it's fighting with the internal c++ code that also attempts to adjust the hips. + if (hmdActive) { + + var y180Xform = new Xform({x: 0, y: 1, z: 0, w: 0}, {x: 0, y: 0, z: 0}); + var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); + var headXform = new Xform(localRotations["Head"], localTranslations["Head"]); + + // transform eyes down the heirarchy chain into avatar space. + var hierarchy = ["Neck", "Spine3", "Spine2", "Spine1", "Spine"]; + var i, l = hierarchy.length; + for (i = 0; i < l; i++) { + var xform = new Xform(localRotations[hierarchy[i]], localTranslations[hierarchy[i]]); + headXform = Xform.mul(xform, headXform); + } + + // solve for the offset that will put the eyes at the hmd position & orientation. + var hipsXform = Xform.mul(y180Xform, Xform.mul(avatarXform.inv(), Xform.mul(hmdXform, Xform.mul(y180Xform, headXform.inv())))); + + MyAvatar.setJointRotation("Hips", hipsXform.rot); + MyAvatar.setJointTranslation("Hips", hipsXform.pos); + } }; var neuronAvatar = new NeuronAvatar(); From 36beea17fabf6e0829e21ba580d84d03890df788 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Mon, 28 Dec 2015 14:59:43 -0800 Subject: [PATCH 29/43] Improved hand controller reticle movement --- .../controllers/reticleHandRotationTest.js | 192 +++--------------- 1 file changed, 24 insertions(+), 168 deletions(-) diff --git a/examples/controllers/reticleHandRotationTest.js b/examples/controllers/reticleHandRotationTest.js index ece9283deb..a303e5e7b4 100644 --- a/examples/controllers/reticleHandRotationTest.js +++ b/examples/controllers/reticleHandRotationTest.js @@ -22,33 +22,13 @@ function length(posA, posB) { return length; } -var EXPECTED_CHANGE = 50; -var lastPos = Controller.getReticlePosition(); function moveReticleAbsolute(x, y) { var globalPos = Controller.getReticlePosition(); - var dX = x - globalPos.x; - var dY = y - globalPos.y; - - // some debugging to see if position is jumping around on us... - var distanceSinceLastMove = length(lastPos, globalPos); - if (distanceSinceLastMove > EXPECTED_CHANGE) { - debugPrint("------------------ distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------"); - } - - if (Math.abs(dX) > EXPECTED_CHANGE) { - debugPrint("surpressing unexpectedly large change dX:" + dX + "----------------------------"); - } - if (Math.abs(dY) > EXPECTED_CHANGE) { - debugPrint("surpressing unexpectedly large change dY:" + dY + "----------------------------"); - } - globalPos.x = x; globalPos.y = y; Controller.setReticlePosition(globalPos); - lastPos = globalPos; } - var MAPPING_NAME = "com.highfidelity.testing.reticleWithHandRotation"; var mapping = Controller.newMapping(MAPPING_NAME); mapping.from(Controller.Standard.LT).peek().constrainToInteger().to(Controller.Actions.ReticleClick); @@ -56,8 +36,6 @@ mapping.from(Controller.Standard.RT).peek().constrainToInteger().to(Controller.A mapping.enable(); -var lastRotatedLeft = Vec3.UNIT_NEG_Y; -var lastRotatedRight = Vec3.UNIT_NEG_Y; function debugPrint(message) { if (DEBUGGING) { @@ -65,24 +43,9 @@ function debugPrint(message) { } } -var MAX_WAKE_UP_DISTANCE = 0.005; -var MIN_WAKE_UP_DISTANCE = 0.001; -var INITIAL_WAKE_UP_DISTANCE = MIN_WAKE_UP_DISTANCE; -var INCREMENTAL_WAKE_UP_DISTANCE = 0.001; - -var MAX_SLEEP_DISTANCE = 0.0004; -var MIN_SLEEP_DISTANCE = 0.00001; //0.00002; -var INITIAL_SLEEP_DISTANCE = MIN_SLEEP_DISTANCE; -var INCREMENTAL_SLEEP_DISTANCE = 0.000002; // 0.00002; - -var leftAsleep = true; -var rightAsleep = true; - -var leftWakeUpDistance = INITIAL_WAKE_UP_DISTANCE; -var rightWakeUpDistance = INITIAL_WAKE_UP_DISTANCE; - -var leftSleepDistance = INITIAL_SLEEP_DISTANCE; -var rightSleepDistance = INITIAL_SLEEP_DISTANCE; +var leftRightBias = 0.0; +var filteredRotatedLeft = Vec3.UNIT_NEG_Y; +var filteredRotatedRight = Vec3.UNIT_NEG_Y; Script.update.connect(function(deltaTime) { @@ -96,153 +59,46 @@ Script.update.connect(function(deltaTime) { var rotatedRight = Vec3.multiplyQbyV(poseRight.rotation, Vec3.UNIT_NEG_Y); var rotatedLeft = Vec3.multiplyQbyV(poseLeft.rotation, Vec3.UNIT_NEG_Y); - var suppressRight = false; - var suppressLeft = false; - - // What I really want to do is to slowly increase the epsilon you have to move it - // to wake up, the longer you go without moving it - var leftDistance = Vec3.distance(rotatedLeft, lastRotatedLeft); - var rightDistance = Vec3.distance(rotatedRight, lastRotatedRight); - - // check to see if hand should wakeup or sleep - if (leftAsleep) { - if (leftDistance > leftWakeUpDistance) { - leftAsleep = false; - leftSleepDistance = INITIAL_SLEEP_DISTANCE; - leftWakeUpDistance = INITIAL_WAKE_UP_DISTANCE; - } else { - // grow the wake up distance to make it harder to wake up - leftWakeUpDistance = Math.min(leftWakeUpDistance + INCREMENTAL_WAKE_UP_DISTANCE, MAX_WAKE_UP_DISTANCE); - } - } else { - // we are awake, determine if we should fall asleep, if we haven't moved - // at least as much as our sleep distance then we sleep - if (leftDistance < leftSleepDistance) { - leftAsleep = true; - leftSleepDistance = INITIAL_SLEEP_DISTANCE; - leftWakeUpDistance = INITIAL_WAKE_UP_DISTANCE; - } else { - // if we moved more than the sleep amount, but we moved less than the max sleep - // amount, then increase our liklihood of sleep. - if (leftDistance < MAX_SLEEP_DISTANCE) { - print("growing sleep...."); - leftSleepDistance = Math.max(leftSleepDistance + INCREMENTAL_SLEEP_DISTANCE, MAX_SLEEP_DISTANCE); - } else { - // otherwise reset it to initial - leftSleepDistance = INITIAL_SLEEP_DISTANCE; - } - } - } - if (leftAsleep) { - suppressLeft = true; - debugPrint("suppressing left not moving enough"); - } - - // check to see if hand should wakeup or sleep - if (rightAsleep) { - if (rightDistance > rightWakeUpDistance) { - rightAsleep = false; - rightSleepDistance = INITIAL_SLEEP_DISTANCE; - rightWakeUpDistance = INITIAL_WAKE_UP_DISTANCE; - } else { - // grow the wake up distance to make it harder to wake up - rightWakeUpDistance = Math.min(rightWakeUpDistance + INCREMENTAL_WAKE_UP_DISTANCE, MAX_WAKE_UP_DISTANCE); - } - } else { - // we are awake, determine if we should fall asleep, if we haven't moved - // at least as much as our sleep distance then we sleep - if (rightDistance < rightSleepDistance) { - rightAsleep = true; - rightSleepDistance = INITIAL_SLEEP_DISTANCE; - rightWakeUpDistance = INITIAL_WAKE_UP_DISTANCE; - } else { - // if we moved more than the sleep amount, but we moved less than the max sleep - // amount, then increase our liklihood of sleep. - if (rightDistance < MAX_SLEEP_DISTANCE) { - print("growing sleep...."); - rightSleepDistance = Math.max(rightSleepDistance + INCREMENTAL_SLEEP_DISTANCE, MAX_SLEEP_DISTANCE); - } else { - // otherwise reset it to initial - rightSleepDistance = INITIAL_SLEEP_DISTANCE; - } - } - } - if (rightAsleep) { - suppressRight = true; - debugPrint("suppressing right not moving enough"); - } - - // check to see if hand is on base station - if (Vec3.equal(rotatedLeft, Vec3.UNIT_NEG_Y)) { - suppressLeft = true; - debugPrint("suppressing left on base station"); - } - if (Vec3.equal(rotatedRight, Vec3.UNIT_NEG_Y)) { - suppressRight = true; - debugPrint("suppressing right on base station"); - } - - // Keep track of last rotations, to detect resting (but not on base station hands) in the future - lastRotatedLeft = rotatedLeft; lastRotatedRight = rotatedRight; - if (suppressLeft && suppressRight) { - debugPrint("both hands suppressed bail out early"); - return; + + // Decide which hand should be controlling the pointer + // by comparing which one is moving more, and by + // tending to stay with the one moving more. + var BIAS_ADJUST_RATE = 0.5; + var BIAS_ADJUST_DEADZONE = 0.05; + leftRightBias += (Vec3.length(poseRight.angularVelocity) - Vec3.length(poseLeft.angularVelocity)) * BIAS_ADJUST_RATE; + if (leftRightBias < BIAS_ADJUST_DEADZONE) { + leftRightBias = 0.0; + } else if (leftRightBias > (1.0 - BIAS_ADJUST_DEADZONE)) { + leftRightBias = 1.0; } - if (suppressLeft) { - debugPrint("right only"); - rotatedLeft = rotatedRight; - } - if (suppressRight) { - debugPrint("left only"); - rotatedRight = rotatedLeft; - } - - // Average the two hand positions, if either hand is on base station, the - // other hand becomes the only used hand and the average is the hand in use - var rotated = Vec3.multiply(Vec3.sum(rotatedRight,rotatedLeft), 0.5); - - if (DEBUGGING) { - Vec3.print("rotatedRight:", rotatedRight); - Vec3.print("rotatedLeft:", rotatedLeft); - Vec3.print("rotated:", rotated); - } + // Velocity filter the hand rotation used to position reticle so that it is easier to target small things with the hand controllers + var VELOCITY_FILTER_GAIN = 1.0; + filteredRotatedLeft = Vec3.mix(filteredRotatedLeft, rotatedLeft, Math.clamp(Vec3.length(poseLeft.angularVelocity) * VELOCITY_FILTER_GAIN, 0.0, 1.0)); + filteredRotatedRight = Vec3.mix(filteredRotatedRight, rotatedRight, Math.clamp(Vec3.length(poseRight.angularVelocity) * VELOCITY_FILTER_GAIN, 0.0, 1.0)); + var rotated = Vec3.mix(filteredRotatedLeft, filteredRotatedRight, leftRightBias); var absolutePitch = rotated.y; // from 1 down to -1 up ... but note: if you rotate down "too far" it starts to go up again... var absoluteYaw = -rotated.x; // from -1 left to 1 right - if (DEBUGGING) { - print("absolutePitch:" + absolutePitch); - print("absoluteYaw:" + absoluteYaw); - Vec3.print("rotated:", rotated); - } - var ROTATION_BOUND = 0.6; var clampYaw = Math.clamp(absoluteYaw, -ROTATION_BOUND, ROTATION_BOUND); var clampPitch = Math.clamp(absolutePitch, -ROTATION_BOUND, ROTATION_BOUND); - if (DEBUGGING) { - print("clampYaw:" + clampYaw); - print("clampPitch:" + clampPitch); - } // using only from -ROTATION_BOUND to ROTATION_BOUND var xRatio = (clampYaw + ROTATION_BOUND) / (2 * ROTATION_BOUND); var yRatio = (clampPitch + ROTATION_BOUND) / (2 * ROTATION_BOUND); - if (DEBUGGING) { - print("xRatio:" + xRatio); - print("yRatio:" + yRatio); - } - var x = screenSizeX * xRatio; var y = screenSizeY * yRatio; - if (DEBUGGING) { - print("position x:" + x + " y:" + y); - } - if (!(xRatio == 0.5 && yRatio == 0)) { + // don't move the reticle with the hand controllers unless the controllers are actually being moved + var MINIMUM_CONTROLLER_ANGULAR_VELOCITY = 0.0001; + var angularVelocityMagnitude = Vec3.length(poseLeft.angularVelocity) * (1.0 - leftRightBias) + Vec3.length(poseRight.angularVelocity) * leftRightBias; + + if (!(xRatio == 0.5 && yRatio == 0) && (angularVelocityMagnitude > MINIMUM_CONTROLLER_ANGULAR_VELOCITY)) { moveReticleAbsolute(x, y); } }); From 4661152aa779c0e6dc1957e467f0fe237188816b Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 28 Dec 2015 17:57:24 -0800 Subject: [PATCH 30/43] NeuronAvatar.js: Now aligns the head with the HMD if active --- examples/controllers/neuron/neuronAvatar.js | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/examples/controllers/neuron/neuronAvatar.js b/examples/controllers/neuron/neuronAvatar.js index abd51f2990..7cebc13feb 100644 --- a/examples/controllers/neuron/neuronAvatar.js +++ b/examples/controllers/neuron/neuronAvatar.js @@ -157,8 +157,6 @@ NeuronAvatar.prototype.deactivate = function () { NeuronAvatar.prototype.update = function (deltaTime) { var hmdActive = HMD.active; - var hmdXform = new Xform(HMD.orientation, HMD.position); - var keys = Object.keys(JOINT_PARENT_MAP); var i, l = keys.length; var absDefaultRot = {}; @@ -192,18 +190,22 @@ NeuronAvatar.prototype.update = function (deltaTime) { MyAvatar.setJointTranslation(j, localTranslation); localTranslations[jointName] = localTranslation; } else { - localTranslations[jointName] = MyAvatar.getJointTranslation(j); + localTranslations[jointName] = MyAvatar.getDefaultJointTranslation(j); } } } - // TODO: Currrently does not work. // it attempts to adjust the hips so that the avatar's head is at the same location & oreintation as the HMD. // however it's fighting with the internal c++ code that also attempts to adjust the hips. if (hmdActive) { - + var UNIT_SCALE = 1 / 100; + var hmdXform = new Xform(HMD.orientation, Vec3.multiply(1 / UNIT_SCALE, HMD.position)); // convert to cm var y180Xform = new Xform({x: 0, y: 1, z: 0, w: 0}, {x: 0, y: 0, z: 0}); - var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); + var avatarXform = new Xform(MyAvatar.orientation, Vec3.multiply(1 / UNIT_SCALE, MyAvatar.position)); // convert to cm + var hipsJointIndex = MyAvatar.getJointIndex("Hips"); + var modelOffsetInvXform = new Xform({x: 0, y: 0, z: 0, w: 1}, MyAvatar.getDefaultJointTranslation(hipsJointIndex)); + var defaultHipsXform = new Xform(MyAvatar.getDefaultJointRotation(hipsJointIndex), MyAvatar.getDefaultJointTranslation(hipsJointIndex)); + var headXform = new Xform(localRotations["Head"], localTranslations["Head"]); // transform eyes down the heirarchy chain into avatar space. @@ -213,9 +215,16 @@ NeuronAvatar.prototype.update = function (deltaTime) { var xform = new Xform(localRotations[hierarchy[i]], localTranslations[hierarchy[i]]); headXform = Xform.mul(xform, headXform); } + headXform = Xform.mul(defaultHipsXform, headXform); + + var preXform = Xform.mul(headXform, y180Xform); + var postXform = Xform.mul(avatarXform, Xform.mul(y180Xform, modelOffsetInvXform.inv())); // solve for the offset that will put the eyes at the hmd position & orientation. - var hipsXform = Xform.mul(y180Xform, Xform.mul(avatarXform.inv(), Xform.mul(hmdXform, Xform.mul(y180Xform, headXform.inv())))); + var hipsOffsetXform = Xform.mul(postXform.inv(), Xform.mul(hmdXform, preXform.inv())); + + // now combine it with the default hips transform + var hipsXform = Xform.mul(hipsOffsetXform, defaultHipsXform); MyAvatar.setJointRotation("Hips", hipsXform.rot); MyAvatar.setJointTranslation("Hips", hipsXform.pos); From 7e514d2f4dd477356d53ffff6c362549bea7de6d Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 28 Dec 2015 18:42:03 -0800 Subject: [PATCH 31/43] Mac build fix --- cmake/externals/neuron/CMakeLists.txt | 9 ++++++++- plugins/hifiNeuron/src/NeuronPlugin.cpp | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cmake/externals/neuron/CMakeLists.txt b/cmake/externals/neuron/CMakeLists.txt index 324b3fb917..6936725571 100644 --- a/cmake/externals/neuron/CMakeLists.txt +++ b/cmake/externals/neuron/CMakeLists.txt @@ -41,8 +41,15 @@ if(WIN32) set(${EXTERNAL_NAME_UPPER}_LIBRARIES "${${EXTERNAL_NAME_UPPER}_LIB_PATH}/NeuronDataReader.lib" CACHE TYPE INTERNAL) add_paths_to_fixup_libs("${${EXTERNAL_NAME_UPPER}_LIB_PATH}") + elseif(APPLE) - # TODO + + set(${EXTERNAL_NAME_UPPER}_LIB_PATH "${SOURCE_DIR}/NeuronDataReader_Mac/dylib") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE "${${EXTERNAL_NAME_UPPER}_LIB_PATH}/NeuronDataReader.dylib" CACHE TYPE INTERNAL) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES "${${EXTERNAL_NAME_UPPER}_LIB_PATH}/NeuronDataReader.dylib" CACHE TYPE INTERNAL) + + add_paths_to_fixup_libs("${${EXTERNAL_NAME_UPPER}_LIB_PATH}") + else() # UNSUPPORTED endif() diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 97131a0a87..6132c16a43 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -523,7 +523,7 @@ QString NeuronPlugin::InputDevice::getDefaultMappingConfig() const { } void NeuronPlugin::InputDevice::update(float deltaTime, const std::vector& joints, const std::vector& prevJoints) { - for (int i = 0; i < joints.size(); i++) { + for (size_t i = 0; i < joints.size(); i++) { glm::vec3 linearVel, angularVel; glm::vec3 pos = joints[i].pos; glm::quat rot = eulerToQuat(joints[i].euler); From ea6850c42385bd83a0883ece1573472d9c4115f8 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Tue, 29 Dec 2015 10:16:27 -0600 Subject: [PATCH 32/43] Changes per convo with Philip --- cmake/macros/ConsolidateStackComponents.cmake | 8 +- cmake/macros/GenerateInstallers.cmake | 6 +- cmake/modules/FindOpenSSL.cmake | 3 +- tools/nsis/release.nsi | 153 ++++++++++-------- 4 files changed, 92 insertions(+), 78 deletions(-) diff --git a/cmake/macros/ConsolidateStackComponents.cmake b/cmake/macros/ConsolidateStackComponents.cmake index 2fc81d1595..2fc20f9506 100644 --- a/cmake/macros/ConsolidateStackComponents.cmake +++ b/cmake/macros/ConsolidateStackComponents.cmake @@ -7,21 +7,19 @@ macro(CONSOLIDATE_STACK_COMPONENTS) if (TARGET_NAME STREQUAL "interface") set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/icon/${INTERFACE_ICON}") set (ICON_DESTINATION_NAME "interface.ico") - set (DEPLOYMENT_PATH "front-end-deployment") else () set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/assets/${STACK_MANAGER_ICON}") set (ICON_DESTINATION_NAME "stack-manager.ico") - set (DEPLOYMENT_PATH "back-end-deployment") endif () add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy ${ICON_FILE_PATH} ${CMAKE_BINARY_DIR}/${DEPLOYMENT_PATH}/${ICON_DESTINATION_NAME} - COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/${DEPLOYMENT_PATH} + COMMAND "${CMAKE_COMMAND}" -E copy ${ICON_FILE_PATH} ${CMAKE_BINARY_DIR}/package-bundle/${ICON_DESTINATION_NAME} + COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/package-bundle ) else () add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/back-end-deployment + COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/package-bundle ) endif () endif () diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index b8860b3ddc..9cb53c5367 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -11,8 +11,7 @@ macro(GENERATE_INSTALLERS) if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE AND WIN32) - file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/front-end-deployment") - file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/back-end-deployment") + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/package-bundle") find_program(MAKENSIS_COMMAND makensis PATHS [HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\NSIS]) if (NOT MAKENSIS_COMMAND) message(FATAL_ERROR "The Nullsoft Scriptable Install Systems is required for generating packaged installers on Windows (http://nsis.sourceforge.net/)") @@ -20,8 +19,7 @@ macro(GENERATE_INSTALLERS) add_custom_target( build-package ALL DEPENDS interface assignment-client domain-server stack-manager - COMMAND set INSTALLER_FRONTEND_DIR=${CMAKE_BINARY_DIR}/front-end-deployment - COMMAND set INSTALLER_BACKEND_DIR=${CMAKE_BINARY_DIR}/back-end-deployment + COMMAND set INSTALLER_SOURCE_DIR=${CMAKE_BINARY_DIR}/package-bundle COMMAND set INSTALLER_NAME=${CMAKE_BINARY_DIR}/${INSTALLER_NAME} COMMAND set INSTALLER_SCRIPTS_DIR=${CMAKE_SOURCE_DIR}/examples COMMAND set INSTALLER_COMPANY=${INSTALLER_COMPANY} diff --git a/cmake/modules/FindOpenSSL.cmake b/cmake/modules/FindOpenSSL.cmake index fbadcd24c8..3a5394a917 100644 --- a/cmake/modules/FindOpenSSL.cmake +++ b/cmake/modules/FindOpenSSL.cmake @@ -258,8 +258,7 @@ if (WIN32) if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy ${OPENSSL_DLL_PATH}/ssleay32.dll ${CMAKE_BINARY_DIR}/front-end-deployment/ - COMMAND "${CMAKE_COMMAND}" -E copy ${OPENSSL_DLL_PATH}/ssleay32.dll ${CMAKE_BINARY_DIR}/back-end-deployment/ + COMMAND "${CMAKE_COMMAND}" -E copy ${OPENSSL_DLL_PATH}/ssleay32.dll ${CMAKE_BINARY_DIR}/package-bundle/ ) endif () endif () diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index 49a63612d4..5ceb45e32f 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -5,12 +5,10 @@ ;------------------------------------------------------------------------------------------------------ ; Source Directory Definition ; -; frontend_srcdir = Source directory for Interface -; backend_srcdir = Source directory for Stack Manager and server stack +; installer_srcdir = Source directory for Interface ; scripts_srcdir = Source directory for JS scripts -!define frontend_srcdir "$%INSTALLER_FRONTEND_DIR%" -!define backend_srcdir "$%INSTALLER_BACKEND_DIR%" +!define installer_srcdir "$%INSTALLER_SOURCE_DIR%" !define scripts_srcdir "$%INSTALLER_SCRIPTS_DIR%" ; Install Directories, Icons and Registry entries @@ -35,8 +33,7 @@ ; Registry entries !define regkey "Software\${install_directory}" !define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${install_directory}" -!define frontend_instdir "$PROGRAMFILES64\${install_directory}" -!define backend_instdir "$APPDATA\${install_directory}" +!define instdir "$PROGRAMFILES64\${install_directory}" ; Start Menu program group !define startmenu_company "$SMPROGRAMS\${install_directory}" @@ -44,8 +41,7 @@ ;------------------------------------------------------------------------------------------------------ ; Local Variables and Other Options -Var ChosenFrontEndInstallDir -Var ChosenBackEndInstallDir +var ChosenInstallDir SetCompressor bzip2 ShowInstDetails hide @@ -56,8 +52,8 @@ SetDateSave on SetDatablockOptimize on CRCCheck on SilentInstall normal -Icon "${frontend_srcdir}\${interface_icon}" -UninstallIcon "${frontend_srcdir}\${interface_icon}" +Icon "${installer_srcdir}\${interface_icon}" +UninstallIcon "${installer_srcdir}\${interface_icon}" UninstallText "This will uninstall ${company}." Name "${company}" Caption "${company}" @@ -66,60 +62,67 @@ OutFile "${setup}" ;------------------------------------------------------------------------------------------------------ ; Components -Section /o "DDE Face Recognition" SEC01 - SetOutPath "$ChosenFrontEndInstallDir" - CreateDirectory $ChosenFrontEndInstallDir\dde - NSISdl::download "https://s3-us-west-1.amazonaws.com/hifi-production/optionals/dde-installer.exe" "$ChosenFrontEndInstallDir\dde-installer.exe" - ExecWait '"$ChosenFrontEndInstallDir\dde-installer.exe" /q:a /t:"$ChosenFrontEndInstallDir\dde"' +Section /o "DDE Face Recognition" "DDE" + SetOutPath "$ChosenInstallDir" + CreateDirectory $ChosenInstallDir\dde + NSISdl::download "https://s3-us-west-1.amazonaws.com/hifi-production/optionals/dde-installer.exe" "$ChosenInstallDir\dde-installer.exe" + ExecWait '"$ChosenInstallDir\dde-installer.exe" /q:a /t:"$ChosenInstallDir\dde"' SectionEnd -Section "Registry Entries and Procotol Handler" SEC02 +Section "Registry Entries and Procotol Handler" "REGISTRY" SetRegView 64 SectionIn RO - WriteRegStr HKLM "${regkey}" "Install_Dir" "$ChosenFrontEndInstallDir" - WriteRegStr HKLM "${regkey}" "Backend_Install_Dir" "$ChosenBackEndInstallDir" + WriteRegStr HKLM "${regkey}" "Install_Dir" "$ChosenInstallDir" WriteRegStr HKLM "${uninstkey}" "DisplayName" "${install_directory} (remove only)" - WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$ChosenFrontEndInstallDir\${uninstaller}"' - WriteRegStr HKCR "${company}\Shell\open\command\" "" '"$ChosenFrontEndInstallDir\${interface_exec} "%1"' - WriteRegStr HKCR "${company}\DefaultIcon" "" "$ChosenFrontEndInstallDir\${interface_icon}" + WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$ChosenInstallDir\${uninstaller}"' + WriteRegStr HKCR "${company}\Shell\open\command\" "" '"$ChosenInstallDir\${interface_exec} "%1"' + WriteRegStr HKCR "${company}\DefaultIcon" "" "$ChosenInstallDir\${interface_icon}" ; hifi:// protocol handler registry entries WriteRegStr HKCR 'hifi' '' 'URL:Alert Protocol' WriteRegStr HKCR 'hifi' 'URL Protocol' '' - WriteRegStr HKCR 'hifi\DefaultIcon' '' '$ChosenFrontEndInstallDir\${interface_icon},1' - WriteRegStr HKCR 'hifi\shell\open\command' '' '$ChosenFrontEndInstallDir\${interface_exec} --url "%1"' + WriteRegStr HKCR 'hifi\DefaultIcon' '' '$ChosenInstallDir\${interface_icon},1' + WriteRegStr HKCR 'hifi\shell\open\command' '' '$ChosenInstallDir\${interface_exec} --url "%1"' - SetOutPath $ChosenFrontEndInstallDir - WriteUninstaller "$ChosenFrontEndInstallDir\${uninstaller}" - Exec '"$ChosenFrontEndInstallDir\2013_vcredist_x64.exe" /q /norestart' - Exec '"$ChosenFrontEndInstallDir\2010_vcredist_x86.exe" /q /norestart' + SetOutPath $ChosenInstallDir + WriteUninstaller "$ChosenInstallDir\${uninstaller}" + Exec '"$ChosenInstallDir\2013_vcredist_x64.exe" /q /norestart' + Exec '"$ChosenInstallDir\2010_vcredist_x86.exe" /q /norestart' SectionEnd -Section "Interface Client" SEC03 - SetOutPath $ChosenFrontEndInstallDir - File /r "${frontend_srcdir}\" - File /a "${frontend_srcdir}\${interface_icon}" +Section "Base Files" "BASE" + SectionIn RO + SetOutPath $ChosenInstallDir + File /r /x assignment-client.* /x domain-server.* /x interface.* /x stack-manager.* "${installer_srcdir}\" SectionEnd -Section "Stack Manager Bundle" SEC04 - SetOutPath $ChosenBackEndInstallDir - File /r "${backend_srcdir}\" - File /a "${backend_srcdir}\${stack_manager_icon}" +Section "HighFidelity Interface" "CLIENT" + SetOutPath $ChosenInstallDir + File /a "${installer_srcdir}\interface.*" + File /a "${installer_srcdir}\${interface_icon}" SectionEnd -Section "Start Menu Shortcuts" SEC05 +Section "HighFidelity Server" "SERVER" + SetOutPath $ChosenInstallDir + File /a "${installer_srcdir}\stack-manager.*" + File /a "${installer_srcdir}\domain-server.*" + File /a "${installer_srcdir}\assignment-client.*" + File /a "${installer_srcdir}\${stack_manager_icon}" +SectionEnd + +Section "Start Menu Shortcuts" "SHORTCUTS" SetShellVarContext all CreateDirectory "${startmenu_company}" - SetOutPath $ChosenFrontEndInstallDir - CreateShortCut "${startmenu_company}\Interface.lnk" "$ChosenFrontEndInstallDir\${interface_exec}" "" "$ChosenFrontEndInstallDir\${interface_icon}" - CreateShortCut "${startmenu_company}\Stack Manager.lnk" "$ChosenBackEndInstallDir\${stack_manager_exec}" "" "$ChosenBackEndInstallDir\${stack_manager_icon}" - CreateShortCut "${startmenu_company}\Uninstall ${company}.lnk" "$ChosenFrontEndInstallDir\${uninstaller}" + SetOutPath $ChosenInstallDir + CreateShortCut "${startmenu_company}\Client.lnk" "$ChosenInstallDir\${interface_exec}" "" "$ChosenInstallDir\${interface_icon}" + CreateShortCut "${startmenu_company}\Home Server.lnk" "$ChosenInstallDir\${stack_manager_exec}" "" "$ChosenInstallDir\${stack_manager_icon}" + CreateShortCut "${startmenu_company}\Uninstall ${company}.lnk" "$ChosenInstallDir\${uninstaller}" SectionEnd Section "Uninstall" SetRegView 64 - ReadRegStr $0 HKLM "${regkey}" "Backend_Install_Dir" Delete "$INSTDIR\${uninstaller}" + Delete "$SMSTARTUP\High Fidelity Home Server.lnk" RMDir /r "$INSTDIR" RMDir /r "${startmenu_company}" RMDir /r "$0" @@ -133,19 +136,39 @@ SectionEnd ; Functions Function .onInit - StrCpy $ChosenFrontEndInstallDir "${frontend_instdir}" - StrCpy $ChosenBackEndInstallDir "${backend_instdir}" + StrCpy $ChosenInstallDir "${instdir}" + SectionSetText ${REGISTRY} "" + SectionSetText ${SHORTCUTS} "" + SectionSetText ${BASE} "" FunctionEnd -Function isInterfaceSelected - ${IfNot} ${SectionIsSelected} ${SEC03} - Abort +var ServerCheckBox +var ServerCheckBox_state +var RunOnStartupCheckBox +var RunOnStartupCheckBox_state + +Function RunCheckboxes + ${If} ${SectionIsSelected} ${SERVER} + ${NSD_CreateCheckbox} 36.2% 56% 100% 10u "&Start Home Server" + Pop $ServerCheckBox + SetCtlColors $ServerCheckBox "" "ffffff" + ${NSD_CreateCheckbox} 36.2% 65% 100% 10u "&Always launch your Home Server on startup" + Pop $RunOnStartupCheckBox + SetCtlColors $RunOnStartupCheckBox "" "ffffff" ${EndIf} FunctionEnd -Function isStackManagerSelected - ${IfNot} ${SectionIsSelected} ${SEC04} - Abort +Function HandleCheckBoxes + ${If} ${SectionIsSelected} ${SERVER} + ${NSD_GetState} $ServerCheckBox $ServerCheckBox_state + ${If} $ServerCheckBox_state == ${BST_CHECKED} + SetOutPath $ChosenInstallDir + ExecShell "" '"$ChosenInstallDir\stack-manager.exe"' + ${EndIf} + ${NSD_GetState} $RunOnStartupCheckBox $RunOnStartupCheckBox_state + ${If} $ServerCheckBox_state == ${BST_CHECKED} + CreateShortCut "$SMSTARTUP\High Fidelity Home Server.lnk" "$ChosenInstallDir\${stack_manager_exec}" "" "$ChosenInstallDir\${stack_manager_icon}" + ${EndIf} ${EndIf} FunctionEnd @@ -154,40 +177,36 @@ FunctionEnd !define MUI_WELCOMEFINISHPAGE_BITMAP "installer_vertical.bmp" !define MUI_WELCOMEPAGE_TITLE "High Fidelity - Integrated Installer" -!define MUI_WELCOMEPAGE_TEXT "Welcome to High Fidelity! This installer includes both the Interface client for VR access as well as the Stack Manager and required server components for you to host your own domain in the metaverse." +!define MUI_WELCOMEPAGE_TEXT "Welcome to High Fidelity! This installer includes both High Fidelity Interface for VR access as well as the High Fidelity Server for you to host your own domain in the metaverse." !insertmacro MUI_PAGE_WELCOME !define MUI_PAGE_HEADER_TEXT "Please select the components you want to install" !define MUI_COMPONENTSPAGE_TEXT_DESCRIPTION_INFO "Hover over a component for a brief description" !insertmacro MUI_PAGE_COMPONENTS -!define MUI_PAGE_CUSTOMFUNCTION_PRE isInterfaceSelected -!define MUI_DIRECTORYPAGE_VARIABLE $ChosenFrontEndInstallDir -!define MUI_PAGE_HEADER_TEXT "Interface client" +!define MUI_DIRECTORYPAGE_VARIABLE $ChosenInstallDir +!define MUI_PAGE_HEADER_TEXT "High Fidelity" !define MUI_PAGE_HEADER_SUBTEXT "" -!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose a location to install the High Fidelity Interface client. You will use the Interface client to connect to domains in the metaverse." +!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose a location for your High Fidelity installation." !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install Directory" !insertmacro MUI_PAGE_DIRECTORY -!define MUI_PAGE_CUSTOMFUNCTION_PRE isStackManagerSelected -!define MUI_DIRECTORYPAGE_VARIABLE $ChosenBackEndInstallDir -!define MUI_PAGE_HEADER_TEXT "Stack Manager" -!define MUI_PAGE_HEADER_SUBTEXT "" -!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose a location to install the High Fidelity Stack Manager bundle, including back end components. NOTE: If you change the default path, make sure you're selecting an unprivileged location, otherwise you will be forced to run the application as administrator for correct functioning." -!define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install Directory - must be writable" -!insertmacro MUI_PAGE_DIRECTORY - !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES +!define MUI_PAGE_CUSTOMFUNCTION_SHOW RunCheckboxes +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE HandleCheckboxes +!define MUI_FINISHPAGE_RUN_NOTCHECKED +!define MUI_FINISHPAGE_RUN "$ChosenInstallDir\interface.exe" +!define MUI_FINISHPAGE_RUN_TEXT "Start High Fidelity Interface" +!insertmacro MUI_PAGE_FINISH + !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${SEC01} "DDE enables facial gesture recognition using a standard 2D webcam" - !insertmacro MUI_DESCRIPTION_TEXT ${SEC02} "Registry entries are required by the system, we will also add a hifi:// protocol handler" - !insertmacro MUI_DESCRIPTION_TEXT ${SEC03} "Interface is the GUI client to access domains running the High Fidelity VR stack" - !insertmacro MUI_DESCRIPTION_TEXT ${SEC04} "The Stack Manager allows you to run a domain of your own and connect it to the High Fidelity metaverse" - !insertmacro MUI_DESCRIPTION_TEXT ${SEC05} "Adds a program group and shortcuts to the Start Menu" + !insertmacro MUI_DESCRIPTION_TEXT ${DDE} "DDE enables facial gesture recognition using a standard 2D webcam" + !insertmacro MUI_DESCRIPTION_TEXT ${CLIENT} "The High Fidelity Interface Client for connection to domains in the metaverse." + !insertmacro MUI_DESCRIPTION_TEXT ${SERVER} "The High Fidelity Server - run your own home domain" !insertmacro MUI_FUNCTION_DESCRIPTION_END !insertmacro MUI_LANGUAGE "English" \ No newline at end of file From c35995b8e3363dda1ec0f129db5e6423cfd4c408 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 29 Dec 2015 08:51:09 -0800 Subject: [PATCH 33/43] Build fix for linux? --- cmake/macros/TargetNeuron.cmake | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cmake/macros/TargetNeuron.cmake b/cmake/macros/TargetNeuron.cmake index 01891ef525..11a92e68ad 100644 --- a/cmake/macros/TargetNeuron.cmake +++ b/cmake/macros/TargetNeuron.cmake @@ -6,9 +6,12 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # macro(TARGET_NEURON) - add_dependency_external_projects(neuron) - find_package(Neuron REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${NEURON_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${NEURON_LIBRARIES}) - add_definitions(-DHAVE_NEURON) + # Neuron data reader is only available on these platforms + if (WIN32 OR APPLE) + add_dependency_external_projects(neuron) + find_package(Neuron REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${NEURON_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${NEURON_LIBRARIES}) + add_definitions(-DHAVE_NEURON) + endif(WIN32 OR APPLE) endmacro() From e10cecd3101400d2228db6b3f5a9d195f357193c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 29 Dec 2015 09:23:03 -0800 Subject: [PATCH 34/43] Build fix for linux #2? --- plugins/hifiNeuron/src/NeuronPlugin.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 6132c16a43..a175ce8e06 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -24,7 +24,10 @@ Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") #define __OS_XUN__ 1 #define BOOL int + +#ifdef HAVE_NEURON #include +#endif const QString NeuronPlugin::NAME = "Neuron"; const QString NeuronPlugin::NEURON_ID_STRING = "Perception Neuron"; @@ -312,6 +315,8 @@ static quat eulerToQuat(vec3 euler) { glm::angleAxis(e.z, Vectors::UNIT_Z)); } +#ifdef HAVE_NEURON + // // neuronDataReader SDK callback functions // @@ -430,17 +435,24 @@ static void SocketStatusChangedCallback(void* context, SOCKET_REF sender, Socket qCDebug(inputplugins) << "NeuronPlugin: socket status = " << message; } +#endif // #ifdef HAVE_NEURON + // // NeuronPlugin // bool NeuronPlugin::isSupported() const { +#ifdef HAVE_NEURON // Because it's a client/server network architecture, we can't tell // if the neuron is actually connected until we connect to the server. return true; +#else + return false; +#endif } void NeuronPlugin::activate() { +#ifdef HAVE_NEURON InputPlugin::activate(); // register with userInputMapper @@ -466,9 +478,11 @@ void NeuronPlugin::activate() { BRRegisterAutoSyncParmeter(_socketRef, Cmd_CombinationMode); } +#endif } void NeuronPlugin::deactivate() { +#ifdef HAVE_NEURON // unregister from userInputMapper if (_inputDevice->_deviceID != controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); @@ -481,6 +495,7 @@ void NeuronPlugin::deactivate() { } InputPlugin::deactivate(); +#endif } void NeuronPlugin::pluginUpdate(float deltaTime, bool jointsCaptured) { From 5d596bcbc449b26d3e5a8c89461b2af4e3ba8738 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 29 Dec 2015 10:02:46 -0800 Subject: [PATCH 35/43] Added tests for GLMHelpers::safeEulerAngles. To verify that converting to and from quats and eulers will use the same angle order. Also I fixed some naming inconsistencies in GeometryUtilTests. --- tests/shared/src/GLMHelpersTests.cpp | 57 ++++++++++++++++++++++++++ tests/shared/src/GLMHelpersTests.h | 27 ++++++++++++ tests/shared/src/GeometryUtilTests.cpp | 2 +- tests/shared/src/GeometryUtilTests.h | 8 ++-- 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 tests/shared/src/GLMHelpersTests.cpp create mode 100644 tests/shared/src/GLMHelpersTests.h diff --git a/tests/shared/src/GLMHelpersTests.cpp b/tests/shared/src/GLMHelpersTests.cpp new file mode 100644 index 0000000000..afb634ecbd --- /dev/null +++ b/tests/shared/src/GLMHelpersTests.cpp @@ -0,0 +1,57 @@ +// +// GLMHelpersTests.cpp +// tests/shared/src +// +// Created by Anthony Thibault on 2015.12.29 +// Copyright 2015 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 "GLMHelpersTests.h" + +#include +#include + +#include <../QTestExtensions.h> + + +QTEST_MAIN(GLMHelpersTests) + +void GLMHelpersTests::testEulerDecomposition() { + // quat to euler and back again.... + + const glm::quat ROT_X_90 = glm::angleAxis(PI / 2.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); + const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + + const float EPSILON = 0.00001f; + + std::vector quatVec = { + glm::quat(), + ROT_X_90, + ROT_Y_180, + ROT_Z_30, + ROT_X_90 * ROT_Y_180 * ROT_Z_30, + ROT_X_90 * ROT_Z_30 * ROT_Y_180, + ROT_Y_180 * ROT_Z_30 * ROT_X_90, + ROT_Y_180 * ROT_X_90 * ROT_Z_30, + ROT_Z_30 * ROT_X_90 * ROT_Y_180, + ROT_Z_30 * ROT_Y_180 * ROT_X_90, + }; + + for (auto& q : quatVec) { + glm::vec3 euler = safeEulerAngles(q); + glm::quat r(euler); + + // when the axis and angle are flipped. + if (glm::dot(q, r) < 0.0f) { + r = -r; + } + + QCOMPARE_WITH_ABS_ERROR(q, r, EPSILON); + } +} + + diff --git a/tests/shared/src/GLMHelpersTests.h b/tests/shared/src/GLMHelpersTests.h new file mode 100644 index 0000000000..5e880899e8 --- /dev/null +++ b/tests/shared/src/GLMHelpersTests.h @@ -0,0 +1,27 @@ +// +// GLMHelpersTests.h +// tests/shared/src +// +// Created by Anthony thibault on 2015.12.29 +// Copyright 2015 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_GLMHelpersTests_h +#define hifi_GLMHelpersTests_h + +#include +#include + +class GLMHelpersTests : public QObject { + Q_OBJECT +private slots: + void testEulerDecomposition(); +}; + +float getErrorDifference(const float& a, const float& b); +float getErrorDifference(const glm::vec3& a, const glm::vec3& b); + +#endif // hifi_GLMHelpersTest_h diff --git a/tests/shared/src/GeometryUtilTests.cpp b/tests/shared/src/GeometryUtilTests.cpp index eaf1e7cd8a..7ba22ec13d 100644 --- a/tests/shared/src/GeometryUtilTests.cpp +++ b/tests/shared/src/GeometryUtilTests.cpp @@ -1,6 +1,6 @@ // // GeometryUtilTests.cpp -// tests/physics/src +// tests/shared/src // // Created by Andrew Meadows on 2015.07.27 // Copyright 2015 High Fidelity, Inc. diff --git a/tests/shared/src/GeometryUtilTests.h b/tests/shared/src/GeometryUtilTests.h index 6996c8bcea..daf740dcd3 100644 --- a/tests/shared/src/GeometryUtilTests.h +++ b/tests/shared/src/GeometryUtilTests.h @@ -1,6 +1,6 @@ // // GeometryUtilTests.h -// tests/physics/src +// tests/shared/src // // Created by Andrew Meadows on 2014.05.30 // Copyright 2014 High Fidelity, Inc. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_AngularConstraintTests_h -#define hifi_AngularConstraintTests_h +#ifndef hifi_GeometryUtilTests_h +#define hifi_GeometryUtilTests_h #include #include @@ -26,4 +26,4 @@ private slots: float getErrorDifference(const float& a, const float& b); float getErrorDifference(const glm::vec3& a, const glm::vec3& b); -#endif // hifi_AngularConstraintTests_h +#endif // hifi_GeometryUtilTests_h From 12fa22300491a5b3f26d22a60eb7cd10a9c8dfac Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 28 Dec 2015 16:23:19 -0800 Subject: [PATCH 36/43] Render SKY_DOME when SKY_MAP tex is loading --- interface/src/Application.cpp | 146 +++++++++--------- libraries/model/src/model/Skybox.cpp | 11 +- .../src/procedural/ProceduralSkybox.cpp | 9 +- 3 files changed, 87 insertions(+), 79 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d383ee3339..e81aa7ec52 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3486,78 +3486,86 @@ namespace render { // Background rendering decision auto skyStage = DependencyManager::get()->getSkyStage(); - if (skyStage->getBackgroundMode() == model::SunSkyStage::NO_BACKGROUND) { + auto backgroundMode = skyStage->getBackgroundMode(); + + if (backgroundMode == model::SunSkyStage::NO_BACKGROUND) { // this line intentionally left blank - } else if (skyStage->getBackgroundMode() == model::SunSkyStage::SKY_DOME) { - if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { - PerformanceTimer perfTimer("stars"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::payloadRender() ... stars..."); - // should be the first rendering pass - w/o depth buffer / lighting - - // compute starfield alpha based on distance from atmosphere - float alpha = 1.0f; - bool hasStars = true; - - if (Menu::getInstance()->isOptionChecked(MenuOption::Atmosphere)) { - // TODO: handle this correctly for zones - const EnvironmentData& closestData = background->_environment->getClosestData(args->_viewFrustum->getPosition()); // was theCamera instead of _viewFrustum - - if (closestData.getHasStars()) { - const float APPROXIMATE_DISTANCE_FROM_HORIZON = 0.1f; - const float DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON = 0.2f; - - glm::vec3 sunDirection = (args->_viewFrustum->getPosition()/*getAvatarPosition()*/ - closestData.getSunLocation()) - / closestData.getAtmosphereOuterRadius(); - float height = glm::distance(args->_viewFrustum->getPosition()/*theCamera.getPosition()*/, closestData.getAtmosphereCenter()); - if (height < closestData.getAtmosphereInnerRadius()) { - // If we're inside the atmosphere, then determine if our keyLight is below the horizon - alpha = 0.0f; - - if (sunDirection.y > -APPROXIMATE_DISTANCE_FROM_HORIZON) { - float directionY = glm::clamp(sunDirection.y, - -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) - + APPROXIMATE_DISTANCE_FROM_HORIZON; - alpha = (directionY / DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON); - } - - - } else if (height < closestData.getAtmosphereOuterRadius()) { - alpha = (height - closestData.getAtmosphereInnerRadius()) / - (closestData.getAtmosphereOuterRadius() - closestData.getAtmosphereInnerRadius()); - - if (sunDirection.y > -APPROXIMATE_DISTANCE_FROM_HORIZON) { - float directionY = glm::clamp(sunDirection.y, - -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) - + APPROXIMATE_DISTANCE_FROM_HORIZON; - alpha = (directionY / DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON); - } - } - } else { - hasStars = false; - } + } else { + if (backgroundMode == model::SunSkyStage::SKY_BOX) { + auto skybox = skyStage->getSkybox(); + if (skybox && skybox->getCubemap() && skybox->getCubemap()->isDefined()) { + PerformanceTimer perfTimer("skybox"); + skybox->render(batch, *(args->_viewFrustum)); + } else { + // If no skybox texture is available, render the SKY_DOME while it loads + backgroundMode = model::SunSkyStage::SKY_DOME; } - - // finally render the starfield - if (hasStars) { - background->_stars.render(args, alpha); - } - - // draw the sky dome - if (/*!selfAvatarOnly &&*/ Menu::getInstance()->isOptionChecked(MenuOption::Atmosphere)) { - PerformanceTimer perfTimer("atmosphere"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::displaySide() ... atmosphere..."); - - background->_environment->renderAtmospheres(batch, *(args->_viewFrustum)); - } - } - } else if (skyStage->getBackgroundMode() == model::SunSkyStage::SKY_BOX) { - PerformanceTimer perfTimer("skybox"); - auto skybox = skyStage->getSkybox(); - if (skybox) { - skybox->render(batch, *(args->_viewFrustum)); + if (backgroundMode == model::SunSkyStage::SKY_DOME) { + if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { + PerformanceTimer perfTimer("stars"); + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), + "Application::payloadRender() ... stars..."); + // should be the first rendering pass - w/o depth buffer / lighting + + // compute starfield alpha based on distance from atmosphere + float alpha = 1.0f; + bool hasStars = true; + + if (Menu::getInstance()->isOptionChecked(MenuOption::Atmosphere)) { + // TODO: handle this correctly for zones + const EnvironmentData& closestData = background->_environment->getClosestData(args->_viewFrustum->getPosition()); // was theCamera instead of _viewFrustum + + if (closestData.getHasStars()) { + const float APPROXIMATE_DISTANCE_FROM_HORIZON = 0.1f; + const float DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON = 0.2f; + + glm::vec3 sunDirection = (args->_viewFrustum->getPosition()/*getAvatarPosition()*/ - closestData.getSunLocation()) + / closestData.getAtmosphereOuterRadius(); + float height = glm::distance(args->_viewFrustum->getPosition()/*theCamera.getPosition()*/, closestData.getAtmosphereCenter()); + if (height < closestData.getAtmosphereInnerRadius()) { + // If we're inside the atmosphere, then determine if our keyLight is below the horizon + alpha = 0.0f; + + if (sunDirection.y > -APPROXIMATE_DISTANCE_FROM_HORIZON) { + float directionY = glm::clamp(sunDirection.y, + -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) + + APPROXIMATE_DISTANCE_FROM_HORIZON; + alpha = (directionY / DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON); + } + + + } else if (height < closestData.getAtmosphereOuterRadius()) { + alpha = (height - closestData.getAtmosphereInnerRadius()) / + (closestData.getAtmosphereOuterRadius() - closestData.getAtmosphereInnerRadius()); + + if (sunDirection.y > -APPROXIMATE_DISTANCE_FROM_HORIZON) { + float directionY = glm::clamp(sunDirection.y, + -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) + + APPROXIMATE_DISTANCE_FROM_HORIZON; + alpha = (directionY / DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON); + } + } + } else { + hasStars = false; + } + } + + // finally render the starfield + if (hasStars) { + background->_stars.render(args, alpha); + } + + // draw the sky dome + if (/*!selfAvatarOnly &&*/ Menu::getInstance()->isOptionChecked(MenuOption::Atmosphere)) { + PerformanceTimer perfTimer("atmosphere"); + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), + "Application::displaySide() ... atmosphere..."); + + background->_environment->renderAtmospheres(batch, *(args->_viewFrustum)); + } + + } } } } diff --git a/libraries/model/src/model/Skybox.cpp b/libraries/model/src/model/Skybox.cpp index 8c37359638..476ac2fa08 100755 --- a/libraries/model/src/model/Skybox.cpp +++ b/libraries/model/src/model/Skybox.cpp @@ -96,7 +96,12 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky } }); + // Render + gpu::TexturePointer skymap = skybox.getCubemap(); + // FIXME: skymap->isDefined may not be threadsafe + assert(skymap && skymap->isDefined()); + glm::mat4 projMat; viewFrustum.evalProjectionMatrix(projMat); @@ -106,11 +111,6 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky batch.setViewTransform(viewTransform); batch.setModelTransform(Transform()); // only for Mac - gpu::TexturePointer skymap; - if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { - skymap = skybox.getCubemap(); - } - batch.setPipeline(thePipeline); batch.setUniformBuffer(SKYBOX_CONSTANTS_SLOT, skybox._dataBuffer); batch.setResourceTexture(SKYBOX_SKYMAP_SLOT, skymap); @@ -118,6 +118,5 @@ void Skybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, const Sky batch.draw(gpu::TRIANGLE_STRIP, 4); batch.setResourceTexture(SKYBOX_SKYMAP_SLOT, nullptr); - } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index ce6f29c3d5..167d49cbaf 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -48,6 +48,10 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, } if (skybox._procedural && skybox._procedural->_enabled && skybox._procedural->ready()) { + gpu::TexturePointer skymap = skybox.getCubemap(); + // FIXME: skymap->isDefined may not be threadsafe + assert(skymap && skymap->isDefined()); + glm::mat4 projMat; viewFrustum.evalProjectionMatrix(projMat); @@ -56,10 +60,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setProjectionTransform(projMat); batch.setViewTransform(viewTransform); batch.setModelTransform(Transform()); // only for Mac - - if (skybox.getCubemap() && skybox.getCubemap()->isDefined()) { - batch.setResourceTexture(0, skybox.getCubemap()); - } + batch.setResourceTexture(0, skybox.getCubemap()); skybox._procedural->prepare(batch, glm::vec3(0), glm::vec3(1)); batch.draw(gpu::TRIANGLE_STRIP, 4); From 1a84b5c0f0c68a2f4f729bf80026b901691d4823 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Tue, 29 Dec 2015 18:13:34 -0800 Subject: [PATCH 37/43] Fixing issue with muzzle flash and hit point being out of sync for pistol --- examples/toybox/pistol/pistol.js | 206 +++++++++++++++++-------------- 1 file changed, 115 insertions(+), 91 deletions(-) diff --git a/examples/toybox/pistol/pistol.js b/examples/toybox/pistol/pistol.js index 8ef26b94c1..df249a0aaf 100644 --- a/examples/toybox/pistol/pistol.js +++ b/examples/toybox/pistol/pistol.js @@ -44,6 +44,7 @@ this.showLaser = false; + }; Pistol.prototype = { @@ -58,20 +59,36 @@ if (!this.equipped) { return; } - this.toggleWithTriggerPressure(); + this.updateProps(); if (this.showLaser) { this.updateLaser(); } + this.toggleWithTriggerPressure(); + + + }, + + updateProps: function() { + var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); + this.position = gunProps.position; + this.rotation = gunProps.rotation; + this.firingDirection = Quat.getFront(this.rotation); + var upVec = Quat.getUp(this.rotation); + this.barrelPoint = Vec3.sum(this.position, Vec3.multiply(upVec, this.laserOffsets.y)); + this.laserTip = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.laserLength)); + this.barrelPoint = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.firingOffsets.z)) + var pickRay = { + origin: this.barrelPoint, + direction: this.firingDirection + }; }, toggleWithTriggerPressure: function() { this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[this.hand]); if (this.triggerValue < RELOAD_THRESHOLD) { - // print('RELOAD'); this.canShoot = true; } if (this.canShoot === true && this.triggerValue === 1) { - // print('SHOOT'); this.fire(); this.canShoot = false; } @@ -91,17 +108,10 @@ }, updateLaser: function() { - var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); - var position = gunProps.position; - var rotation = gunProps.rotation; - this.firingDirection = Quat.getFront(rotation); - var upVec = Quat.getUp(rotation); - this.barrelPoint = Vec3.sum(position, Vec3.multiply(upVec, this.laserOffsets.y)); - var laserTip = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.laserLength)); - this.barrelPoint = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.firingOffsets.z)) + Overlays.editOverlay(this.laser, { start: this.barrelPoint, - end: laserTip, + end: this.laserTip, alpha: 1 }); }, @@ -114,19 +124,6 @@ }); }, - preload: function(entityID) { - this.entityID = entityID; - // this.initControllerMapping(); - this.laser = Overlays.addOverlay("line3d", { - start: ZERO_VECTOR, - end: ZERO_VECTOR, - color: COLORS.RED, - alpha: 1, - visible: true, - lineWidth: 2 - }); - }, - triggerPress: function(hand, value) { if (this.hand === hand && value === 1) { //We are pulling trigger on the hand we have the gun in, so fire @@ -135,17 +132,18 @@ }, fire: function() { - var pickRay = { - origin: this.barrelPoint, - direction: this.firingDirection - }; + Audio.playSound(this.fireSound, { position: this.barrelPoint, volume: this.fireVolume }); + var pickRay = { + origin: this.barrelPoint, + direction: this.firingDirection + }; this.createGunFireEffect(this.barrelPoint) - var intersection = Entities.findRayIntersectionBlocking(pickRay, true); + var intersection = Entities.findRayIntersection(pickRay, true); if (intersection.intersects) { this.createEntityHitEffect(intersection.intersection); if (Math.random() < this.playRichochetSoundChance) { @@ -170,11 +168,11 @@ }, createEntityHitEffect: function(position) { - var flash = Entities.addEntity({ + var sparks = Entities.addEntity({ type: "ParticleEffect", position: position, lifetime: 4, - "name": "Flash Emitter", + "name": "Sparks Emitter", "color": { red: 228, green: 128, @@ -228,7 +226,7 @@ }); Script.setTimeout(function() { - Entities.editEntity(flash, { + Entities.editEntity(sparks, { isEmitting: false }); }, 100); @@ -282,70 +280,96 @@ }); }, 100); - var flash = Entities.addEntity({ - type: "ParticleEffect", - position: position, - lifetime: 4, - "name": "Muzzle Flash", - "color": { - red: 228, - green: 128, - blue: 12 - }, - "maxParticles": 1000, - "lifespan": 0.1, - "emitRate": 1000, - "emitSpeed": 0.5, - "speedSpread": 0, - "emitOrientation": { - "x": -0.4, - "y": 1, - "z": -0.2, - "w": 0.7071068286895752 - }, - "emitDimensions": { - "x": 0, - "y": 0, - "z": 0 - }, - "polarStart": 0, - "polarFinish": Math.PI, - "azimuthStart": -3.1415927410125732, - "azimuthFinish": 2, - "emitAcceleration": { - "x": 0, - "y": 0, - "z": 0 - }, - "accelerationSpread": { - "x": 0, - "y": 0, - "z": 0 - }, - "particleRadius": 0.05, - "radiusSpread": 0.01, - "radiusStart": 0.05, - "radiusFinish": 0.05, - "colorSpread": { - red: 100, - green: 100, - blue: 20 - }, - "alpha": 1, - "alphaSpread": 0, - "alphaStart": 0, - "alphaFinish": 0, - "additiveBlending": true, - "textures": "http://ericrius1.github.io/PartiArt/assets/star.png" + Entities.editEntity(this.flash, { + isEmitting: true }); - Script.setTimeout(function() { - Entities.editEntity(flash, { + Entities.editEntity(_this.flash, { isEmitting: false }); }, 100) - } + }, + + preload: function(entityID) { + this.entityID = entityID; + this.laser = Overlays.addOverlay("line3d", { + start: ZERO_VECTOR, + end: ZERO_VECTOR, + color: COLORS.RED, + alpha: 1, + visible: true, + lineWidth: 2 + }); + + var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); + var position = gunProps.position; + var rotation = gunProps.rotation; + this.firingDirection = Quat.getFront(rotation); + var upVec = Quat.getUp(rotation); + this.barrelPoint = Vec3.sum(position, Vec3.multiply(upVec, this.laserOffsets.y)); + this.barrelPoint = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.firingOffsets.z)) + + // this.flash = Entities.addEntity({ + // type: "ParticleEffect", + // parentID: this.entityID, + // position: this.barrelPoint, + // "name": "Muzzle Flash", + // // isEmitting: false, + // "color": { + // red: 228, + // green: 128, + // blue: 12 + // }, + // "maxParticles": 1000, + // "lifespan": 0.1, + // "emitRate": 1000, + // "emitSpeed": 0.5, + // "speedSpread": 0, + // "emitOrientation": { + // "x": -0.4, + // "y": 1, + // "z": -0.2, + // "w": 0.7071068286895752 + // }, + // "emitDimensions": { + // "x": 0, + // "y": 0, + // "z": 0 + // }, + // "polarStart": 0, + // "polarFinish": Math.PI, + // "azimuthStart": -3.1415927410125732, + // "azimuthFinish": 2, + // "emitAcceleration": { + // "x": 0, + // "y": 0, + // "z": 0 + // }, + // "accelerationSpread": { + // "x": 0, + // "y": 0, + // "z": 0 + // }, + // "particleRadius": 0.05, + // "radiusSpread": 0.01, + // "radiusStart": 0.05, + // "radiusFinish": 0.05, + // "colorSpread": { + // red: 100, + // green: 100, + // blue: 20 + // }, + // "alpha": 1, + // "alphaSpread": 0, + // "alphaStart": 0, + // "alphaFinish": 0, + // "additiveBlending": true, + // "textures": "http://ericrius1.github.io/PartiArt/assets/star.png" + // }); + + }, + }; From 4cc94b44ba0f8ed1320f6dcb35e1cc67ca362292 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Tue, 29 Dec 2015 18:30:07 -0800 Subject: [PATCH 38/43] muzzle is in sync --- examples/toybox/pistol/pistol.js | 140 +++++++++++++++---------------- 1 file changed, 69 insertions(+), 71 deletions(-) diff --git a/examples/toybox/pistol/pistol.js b/examples/toybox/pistol/pistol.js index df249a0aaf..372c704219 100644 --- a/examples/toybox/pistol/pistol.js +++ b/examples/toybox/pistol/pistol.js @@ -29,20 +29,12 @@ this.equipped = false; this.forceMultiplier = 1; this.laserLength = 100; - this.laserOffsets = { - y: .095 - }; - this.firingOffsets = { - z: 0.16 - } + this.fireSound = SoundCache.getSound("https://s3.amazonaws.com/hifi-public/sounds/Guns/GUN-SHOT2.raw"); this.ricochetSound = SoundCache.getSound("https://s3.amazonaws.com/hifi-public/sounds/Guns/Ricochet.L.wav"); this.playRichochetSoundChance = 0.1; this.fireVolume = 0.2; this.bulletForce = 10; - - - this.showLaser = false; }; @@ -143,7 +135,7 @@ direction: this.firingDirection }; this.createGunFireEffect(this.barrelPoint) - var intersection = Entities.findRayIntersection(pickRay, true); + var intersection = Entities.findRayIntersectionBlocking(pickRay, true); if (intersection.intersects) { this.createEntityHitEffect(intersection.intersection); if (Math.random() < this.playRichochetSoundChance) { @@ -301,76 +293,82 @@ visible: true, lineWidth: 2 }); - + this.laserOffsets = { + y: .095 + }; + this.firingOffsets = { + z: 0.16 + } var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); var position = gunProps.position; - var rotation = gunProps.rotation; + var rotation = Quat.fromPitchYawRollDegrees(0, 0, 0); this.firingDirection = Quat.getFront(rotation); var upVec = Quat.getUp(rotation); this.barrelPoint = Vec3.sum(position, Vec3.multiply(upVec, this.laserOffsets.y)); this.barrelPoint = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.firingOffsets.z)) - // this.flash = Entities.addEntity({ - // type: "ParticleEffect", - // parentID: this.entityID, - // position: this.barrelPoint, - // "name": "Muzzle Flash", - // // isEmitting: false, - // "color": { - // red: 228, - // green: 128, - // blue: 12 - // }, - // "maxParticles": 1000, - // "lifespan": 0.1, - // "emitRate": 1000, - // "emitSpeed": 0.5, - // "speedSpread": 0, - // "emitOrientation": { - // "x": -0.4, - // "y": 1, - // "z": -0.2, - // "w": 0.7071068286895752 - // }, - // "emitDimensions": { - // "x": 0, - // "y": 0, - // "z": 0 - // }, - // "polarStart": 0, - // "polarFinish": Math.PI, - // "azimuthStart": -3.1415927410125732, - // "azimuthFinish": 2, - // "emitAcceleration": { - // "x": 0, - // "y": 0, - // "z": 0 - // }, - // "accelerationSpread": { - // "x": 0, - // "y": 0, - // "z": 0 - // }, - // "particleRadius": 0.05, - // "radiusSpread": 0.01, - // "radiusStart": 0.05, - // "radiusFinish": 0.05, - // "colorSpread": { - // red: 100, - // green: 100, - // blue: 20 - // }, - // "alpha": 1, - // "alphaSpread": 0, - // "alphaStart": 0, - // "alphaFinish": 0, - // "additiveBlending": true, - // "textures": "http://ericrius1.github.io/PartiArt/assets/star.png" - // }); + this.flash = Entities.addEntity({ + type: "ParticleEffect", + position: this.barrelPoint, + "name": "Muzzle Flash", + isEmitting: false, + "color": { + red: 228, + green: 128, + blue: 12 + }, + "maxParticles": 1000, + "lifespan": 0.1, + "emitRate": 1000, + "emitSpeed": 0.5, + "speedSpread": 0, + "emitOrientation": { + "x": -0.4, + "y": 1, + "z": -0.2, + "w": 0.7071068286895752 + }, + "emitDimensions": { + "x": 0, + "y": 0, + "z": 0 + }, + "polarStart": 0, + "polarFinish": Math.PI, + "azimuthStart": -3.1415927410125732, + "azimuthFinish": 2, + "emitAcceleration": { + "x": 0, + "y": 0, + "z": 0 + }, + "accelerationSpread": { + "x": 0, + "y": 0, + "z": 0 + }, + "particleRadius": 0.05, + "radiusSpread": 0.01, + "radiusStart": 0.05, + "radiusFinish": 0.05, + "colorSpread": { + red: 100, + green: 100, + blue: 20 + }, + "alpha": 1, + "alphaSpread": 0, + "alphaStart": 0, + "alphaFinish": 0, + "additiveBlending": true, + "textures": "http://ericrius1.github.io/PartiArt/assets/star.png" + }); + + Script.setTimeout(function() { + Entities.editEntity(_this.flash, {parentID: _this.entityID}); + }, 500) }, - - }; // entity scripts always need to return a newly constructed object of our type From 5caa6cbdbf99b1531d757976b539c6606b497b9b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 29 Dec 2015 18:38:57 -0800 Subject: [PATCH 39/43] Update pistol.js leading zeroes --- examples/toybox/pistol/pistol.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/toybox/pistol/pistol.js b/examples/toybox/pistol/pistol.js index 372c704219..7fb05d992f 100644 --- a/examples/toybox/pistol/pistol.js +++ b/examples/toybox/pistol/pistol.js @@ -251,11 +251,11 @@ "z": 0 }, "accelerationSpread": { - "x": .2, + "x": 0.2, "y": 0, - "z": .2 + "z": 0.2 }, - "radiusSpread": .04, + "radiusSpread": 0.04, "particleRadius": 0.07, "radiusStart": 0.07, "radiusFinish": 0.07, @@ -294,7 +294,7 @@ lineWidth: 2 }); this.laserOffsets = { - y: .095 + y: 0.095 }; this.firingOffsets = { z: 0.16 @@ -373,4 +373,4 @@ // entity scripts always need to return a newly constructed object of our type return new Pistol(); -}); \ No newline at end of file +}); From 6d857296f9a4acc3e81d2df5c95d50f28bbfaa2c Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 29 Dec 2015 23:37:10 -0800 Subject: [PATCH 40/43] show search sphere instead of beams, start at center of view --- examples/controllers/handControllerGrab.js | 96 ++++++++++++++++------ 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 3c8f1f0014..0dce72803c 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -96,7 +96,7 @@ var MSEC_PER_SEC = 1000.0; var LIFETIME = 10; var ACTION_TTL = 15; // seconds var ACTION_TTL_REFRESH = 5; -var PICKS_PER_SECOND_PER_HAND = 5; +var PICKS_PER_SECOND_PER_HAND = 60; var MSECS_PER_SEC = 1000.0; var GRABBABLE_PROPERTIES = [ "position", @@ -123,8 +123,8 @@ var blacklist = []; //we've created various ways of visualizing looking for and moving distant objects var USE_ENTITY_LINES_FOR_SEARCHING = false; -var USE_OVERLAY_LINES_FOR_SEARCHING = false; -var USE_PARTICLE_BEAM_FOR_SEARCHING = true; +var USE_OVERLAY_LINES_FOR_SEARCHING = true; +var USE_PARTICLE_BEAM_FOR_SEARCHING = false; var USE_ENTITY_LINES_FOR_MOVING = false; var USE_OVERLAY_LINES_FOR_MOVING = false; @@ -290,6 +290,11 @@ function MyController(hand) { this.spotlight = null; this.pointlight = null; this.overlayLine = null; + this.searchSphere = null; + + // how far from camera to search intersection? + this.intersectionDistance = 0.0; + this.searchSphereDistance = 0.0; this.ignoreIK = false; this.offsetPosition = Vec3.ZERO; @@ -409,6 +414,23 @@ function MyController(hand) { } }; + var SEARCH_SPHERE_ALPHA = 0.5; + this.searchSphereOn = function(location, size, color) { + if (this.searchSphere === null) { + var sphereProperties = { + position: location, + size: size, + color: color, + alpha: SEARCH_SPHERE_ALPHA, + solid: true, + visible: true + } + this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); + } else { + Overlays.editOverlay(this.searchSphere, { position: location, size: size, color: color, visible: true }); + } + } + this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { @@ -654,6 +676,17 @@ function MyController(hand) { this.overlayLine = null; }; + this.searchSphereOff = function() { + if (this.searchSphere !== null) { + //Overlays.editOverlay(this.searchSphere, { visible: false }); + Overlays.deleteOverlay(this.searchSphere); + this.searchSphere = null; + this.searchSphereDistance = 0.0; + this.intersectionDistance = 0.0; + } + + }; + this.particleBeamOff = function() { if (this.particleBeam !== null) { Entities.editEntity(this.particleBeam, { @@ -687,6 +720,7 @@ function MyController(hand) { if (USE_PARTICLE_BEAM_FOR_SEARCHING === true || USE_PARTICLE_BEAM_FOR_MOVING === true) { this.particleBeamOff(); } + this.searchSphereOff(); }; this.triggerPress = function(value) { @@ -712,11 +746,6 @@ function MyController(hand) { return this.triggerValue < TRIGGER_OFF_VALUE; }; - this.triggerSqueezed = function() { - var triggerValue = this.rawTriggerValue; - return triggerValue > TRIGGER_ON_VALUE; - }; - this.bumperSqueezed = function() { return _this.rawBumperValue > BUMPER_ON_VALUE; }; @@ -726,15 +755,15 @@ function MyController(hand) { }; this.off = function() { - if (this.triggerSmoothedSqueezed()) { + if (this.triggerSmoothedSqueezed() || this.bumperSqueezed()) { this.lastPickTime = 0; - this.setState(STATE_SEARCHING); - return; - } - if (this.bumperSqueezed()) { - this.lastPickTime = 0; - this.setState(STATE_EQUIP_SEARCHING); - return; + var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; + if (this.triggerSmoothedSqueezed()) { + this.setState(STATE_SEARCHING); + } else { + this.setState(STATE_EQUIP_SEARCHING); + } } }; @@ -748,9 +777,14 @@ function MyController(hand) { // the trigger is being pressed, do a ray test var handPosition = this.getHandPosition(); + + var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation; + var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation)); + var distantPickRay = { - origin: handPosition, - direction: Quat.getUp(this.getHandRotation()), + origin: Camera.position, + direction: Quat.getFront(Quat.multiply(Camera.orientation, handDeltaRotation)), length: PICK_MAX_DISTANCE }; @@ -789,7 +823,7 @@ function MyController(hand) { if (intersection.intersects) { // the ray is intersecting something we can move. - var intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); + this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, intersection.entityID, DEFAULT_GRABBABLE_DATA); @@ -800,11 +834,11 @@ function MyController(hand) { if (typeof grabbableData.grabbable !== 'undefined' && !grabbableData.grabbable) { continue; } - if (intersectionDistance > pickRay.length) { + if (this.intersectionDistance > pickRay.length) { // too far away for this ray. continue; } - if (intersectionDistance <= NEAR_PICK_MAX_DISTANCE) { + if (this.intersectionDistance <= NEAR_PICK_MAX_DISTANCE) { // the hand is very close to the intersected object. go into close-grabbing mode. if (grabbableData.wantsTrigger) { this.grabbedEntity = intersection.entityID; @@ -851,6 +885,7 @@ function MyController(hand) { } } + // forward ray test failed, try sphere test. if (WANT_DEBUG) { Entities.addEntity({ @@ -946,14 +981,23 @@ function MyController(hand) { this.lineOn(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } - if (USE_OVERLAY_LINES_FOR_SEARCHING === true) { - this.overlayLineOn(distantPickRay.origin, Vec3.sum(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH)), NO_INTERSECT_COLOR); - } - if (USE_PARTICLE_BEAM_FOR_SEARCHING === true) { this.handleParticleBeam(distantPickRay.origin, this.getHandRotation(), NO_INTERSECT_COLOR); } - + if (this.intersectionDistance > 0) { + var SPHERE_INTERSECTION_SIZE = 0.011; + var SEARCH_SPHERE_FOLLOW_RATE = 0.50; + var SEARCH_SPHERE_CHASE_DROP = 0.2; + this.searchSphereDistance = this.searchSphereDistance * SEARCH_SPHERE_FOLLOW_RATE + this.intersectionDistance * (1.0 - SEARCH_SPHERE_FOLLOW_RATE); + var searchSphereLocation = Vec3.sum(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); + searchSphereLocation.y -= ((this.intersectionDistance - this.searchSphereDistance) / this.intersectionDistance) * SEARCH_SPHERE_CHASE_DROP; + this.searchSphereOn(searchSphereLocation, SPHERE_INTERSECTION_SIZE * this.intersectionDistance, NO_INTERSECT_COLOR); + if (USE_OVERLAY_LINES_FOR_SEARCHING === true) { + var OVERLAY_BEAM_SETBACK = 0.9; + var startBeam = Vec3.sum(handPosition, Vec3.multiply(Vec3.subtract(searchSphereLocation, handPosition), OVERLAY_BEAM_SETBACK)); + this.overlayLineOn(startBeam, searchSphereLocation, NO_INTERSECT_COLOR); + } + } }; this.distanceHolding = function() { From e995b29712dd184a4473f79e0ca8da1a6b72bcca Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 30 Dec 2015 00:36:11 -0800 Subject: [PATCH 41/43] can target without grabbing --- examples/controllers/handControllerGrab.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 0dce72803c..e362eb22e0 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -23,8 +23,9 @@ var WANT_DEBUG = false; // these tune time-averaging and "on" value for analog trigger // -var TRIGGER_SMOOTH_RATIO = 0.1; // 0.0 disables smoothing of trigger value -var TRIGGER_ON_VALUE = 0.4; +var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing +var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab +var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; @@ -738,6 +739,10 @@ function MyController(hand) { (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); }; + this.triggerSmoothedGrab = function() { + return this.triggerValue > TRIGGER_GRAB_VALUE; + }; + this.triggerSmoothedSqueezed = function() { return this.triggerValue > TRIGGER_ON_VALUE; }; @@ -775,7 +780,7 @@ function MyController(hand) { return; } - // the trigger is being pressed, do a ray test + // the trigger is being pressed, so do a ray test to see what we are hitting var handPosition = this.getHandPosition(); var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; @@ -788,7 +793,7 @@ function MyController(hand) { length: PICK_MAX_DISTANCE }; - // don't pick 60x per second. + // Pick at some maximum rate, not always var pickRays = []; var now = Date.now(); if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { @@ -872,11 +877,11 @@ function MyController(hand) { // this.setState(STATE_EQUIP_SPRING); this.setState(STATE_EQUIP); return; - } else if (this.state == STATE_SEARCHING) { + } else if ((this.state == STATE_SEARCHING) && this.triggerSmoothedGrab()) { this.setState(STATE_DISTANCE_HOLDING); return; } - } else if (grabbableData.wantsTrigger) { + } else if (grabbableData.wantsTrigger && this.triggerSmoothedGrab()) { this.grabbedEntity = intersection.entityID; this.setState(STATE_FAR_TRIGGER); return; @@ -991,11 +996,11 @@ function MyController(hand) { this.searchSphereDistance = this.searchSphereDistance * SEARCH_SPHERE_FOLLOW_RATE + this.intersectionDistance * (1.0 - SEARCH_SPHERE_FOLLOW_RATE); var searchSphereLocation = Vec3.sum(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); searchSphereLocation.y -= ((this.intersectionDistance - this.searchSphereDistance) / this.intersectionDistance) * SEARCH_SPHERE_CHASE_DROP; - this.searchSphereOn(searchSphereLocation, SPHERE_INTERSECTION_SIZE * this.intersectionDistance, NO_INTERSECT_COLOR); + this.searchSphereOn(searchSphereLocation, SPHERE_INTERSECTION_SIZE * this.intersectionDistance, this.triggerSmoothedGrab() ? INTERSECT_COLOR : NO_INTERSECT_COLOR); if (USE_OVERLAY_LINES_FOR_SEARCHING === true) { var OVERLAY_BEAM_SETBACK = 0.9; var startBeam = Vec3.sum(handPosition, Vec3.multiply(Vec3.subtract(searchSphereLocation, handPosition), OVERLAY_BEAM_SETBACK)); - this.overlayLineOn(startBeam, searchSphereLocation, NO_INTERSECT_COLOR); + this.overlayLineOn(startBeam, searchSphereLocation, this.triggerSmoothedGrab() ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } } }; From 2bad5437a9f82c6b0483c62aeefd24aab358f425 Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Thu, 31 Dec 2015 09:07:11 -0600 Subject: [PATCH 42/43] Adding default content set as optional installer component --- tools/nsis/release.nsi | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index 5ceb45e32f..b918d58a42 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -69,6 +69,13 @@ Section /o "DDE Face Recognition" "DDE" ExecWait '"$ChosenInstallDir\dde-installer.exe" /q:a /t:"$ChosenInstallDir\dde"' SectionEnd +Section /o "Default Content Set" "CONTENT" + SetOutPath "$ChosenInstallDir/resources" + NSISdl::download "https://s3-us-west-1.amazonaws.com/hifi-production/content/temp.exe" "$ChosenInstallDir\content.exe" + ExecWait '"$ChosenInstallDir\content.exe" /S' + Delete "$ChosenInstallDir\content.exe" +SectionEnd + Section "Registry Entries and Procotol Handler" "REGISTRY" SetRegView 64 SectionIn RO @@ -205,8 +212,9 @@ FunctionEnd !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN !insertmacro MUI_DESCRIPTION_TEXT ${DDE} "DDE enables facial gesture recognition using a standard 2D webcam" + !insertmacro MUI_DESCRIPTION_TEXT ${CONTENT} "Demo content set for your home server" !insertmacro MUI_DESCRIPTION_TEXT ${CLIENT} "The High Fidelity Interface Client for connection to domains in the metaverse." !insertmacro MUI_DESCRIPTION_TEXT ${SERVER} "The High Fidelity Server - run your own home domain" !insertmacro MUI_FUNCTION_DESCRIPTION_END -!insertmacro MUI_LANGUAGE "English" \ No newline at end of file +!insertmacro MUI_LANGUAGE "English" From 0bd823e20ca107f96737bce2afab9a5979a7b2bd Mon Sep 17 00:00:00 2001 From: Leonardo Murillo Date: Thu, 31 Dec 2015 10:09:27 -0600 Subject: [PATCH 43/43] Fix to install path --- tools/nsis/release.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nsis/release.nsi b/tools/nsis/release.nsi index b918d58a42..5e711a3454 100644 --- a/tools/nsis/release.nsi +++ b/tools/nsis/release.nsi @@ -70,7 +70,7 @@ Section /o "DDE Face Recognition" "DDE" SectionEnd Section /o "Default Content Set" "CONTENT" - SetOutPath "$ChosenInstallDir/resources" + SetOutPath "$ChosenInstallDir\resources" NSISdl::download "https://s3-us-west-1.amazonaws.com/hifi-production/content/temp.exe" "$ChosenInstallDir\content.exe" ExecWait '"$ChosenInstallDir\content.exe" /S' Delete "$ChosenInstallDir\content.exe"