Merge branch 'master' of https://github.com/highfidelity/hifi into brown

This commit is contained in:
samcake 2017-05-09 10:46:38 -07:00
commit c36035d3eb
96 changed files with 2252 additions and 663 deletions

View file

@ -6,8 +6,8 @@ if (WIN32)
include(ExternalProject) include(ExternalProject)
ExternalProject_Add( ExternalProject_Add(
${EXTERNAL_NAME} ${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi8.zip
URL_MD5 bc2861e50852dd590cdc773a14a041a7 URL_MD5 b01510437ea15527156bc25cdf733bd9
CONFIGURE_COMMAND "" CONFIGURE_COMMAND ""
BUILD_COMMAND "" BUILD_COMMAND ""
INSTALL_COMMAND "" INSTALL_COMMAND ""

View file

@ -17,26 +17,26 @@ Rectangle {
property alias pixelSize: label.font.pixelSize; property alias pixelSize: label.font.pixelSize;
property bool selected: false property bool selected: false
property bool hovered: false property bool hovered: false
property bool enabled: false
property int spacing: 2 property int spacing: 2
property var action: function () {} property var action: function () {}
property string enabledColor: hifi.colors.blueHighlight property string enabledColor: hifi.colors.blueHighlight
property string disabledColor: hifi.colors.blueHighlight property string disabledColor: hifi.colors.blueHighlight
property string highlightColor: hifi.colors.blueHighlight;
width: label.width + 64 width: label.width + 64
height: 32 height: 32
color: hifi.colors.white color: hifi.colors.white
enabled: false
HifiConstants { id: hifi } HifiConstants { id: hifi }
RalewaySemiBold { RalewaySemiBold {
id: label; id: label;
color: enabledColor color: enabled ? enabledColor : disabledColor
font.pixelSize: 15; font.pixelSize: 15;
anchors { anchors {
horizontalCenter: parent.horizontalCenter; horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter; verticalCenter: parent.verticalCenter;
} }
} }
Rectangle { Rectangle {
id: indicator id: indicator

View file

@ -8,6 +8,7 @@ import "../styles" as HifiStyles
import "../styles-uit" import "../styles-uit"
import "../" import "../"
import "." import "."
Item { Item {
id: web id: web
HifiConstants { id: hifi } HifiConstants { id: hifi }
@ -22,17 +23,14 @@ Item {
property bool keyboardRaised: false property bool keyboardRaised: false
property bool punctuationMode: false property bool punctuationMode: false
property bool isDesktop: false property bool isDesktop: false
property string initialPage: ""
property bool startingUp: true
property alias webView: webview property alias webView: webview
property alias profile: webview.profile property alias profile: webview.profile
property bool remove: false property bool remove: false
property var urlList: []
property var forwardList: []
// Manage own browse history because WebEngineView history is wiped when a new URL is loaded via
property int currentPage: -1 // used as a model for repeater // onNewViewRequested, e.g., as happens when a social media share button is clicked.
property alias pagesModel: pagesModel property var history: []
property int historyIndex: -1
Rectangle { Rectangle {
id: buttons id: buttons
@ -51,21 +49,22 @@ Item {
TabletWebButton { TabletWebButton {
id: back id: back
enabledColor: hifi.colors.baseGray enabledColor: hifi.colors.darkGray
enabled: false disabledColor: hifi.colors.lightGrayText
enabled: historyIndex > 0
text: "BACK" text: "BACK"
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: goBack() onClicked: goBack()
hoverEnabled: true
} }
} }
TabletWebButton { TabletWebButton {
id: close id: close
enabledColor: hifi.colors.darkGray enabledColor: hifi.colors.darkGray
disabledColor: hifi.colors.lightGrayText
enabled: true
text: "CLOSE" text: "CLOSE"
MouseArea { MouseArea {
@ -75,7 +74,6 @@ Item {
} }
} }
RalewaySemiBold { RalewaySemiBold {
id: displayUrl id: displayUrl
color: hifi.colors.baseGray color: hifi.colors.baseGray
@ -90,7 +88,6 @@ Item {
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
preventStealing: true preventStealing: true
@ -98,29 +95,10 @@ Item {
} }
} }
ListModel {
id: pagesModel
onCountChanged: {
currentPage = count - 1;
if (currentPage > 0) {
back.enabledColor = hifi.colors.darkGray;
} else {
back.enabledColor = hifi.colors.baseGray;
}
}
}
function goBack() { function goBack() {
if (webview.canGoBack) { if (historyIndex > 0) {
forwardList.push(webview.url); historyIndex--;
webview.goBack(); loadUrl(history[historyIndex]);
} else if (web.urlList.length > 0) {
var url = web.urlList.pop();
loadUrl(url);
} else if (web.forwardList.length > 0) {
var url = web.forwardList.pop();
loadUrl(url);
web.forwardList = [];
} }
} }
@ -137,19 +115,12 @@ Item {
} }
function goForward() { function goForward() {
if (currentPage < pagesModel.count - 1) { if (historyIndex < history.length - 1) {
currentPage++; historyIndex++;
loadUrl(history[historyIndex]);
} }
} }
function gotoPage(url) {
urlAppend(url)
}
function isUrlLoaded(url) {
return (pagesModel.get(currentPage).webUrl === url);
}
function reloadPage() { function reloadPage() {
view.reloadAndBypassCache() view.reloadAndBypassCache()
view.setActiveFocusOnPress(true); view.setActiveFocusOnPress(true);
@ -161,36 +132,8 @@ Item {
web.url = webview.url; web.url = webview.url;
} }
function onInitialPage(url) {
return (url === webview.url);
}
function urlAppend(url) {
var lurl = decodeURIComponent(url)
if (lurl[lurl.length - 1] !== "/") {
lurl = lurl + "/"
}
web.urlList.push(url);
setBackButtonStatus();
}
function setBackButtonStatus() {
if (web.urlList.length > 0 || webview.canGoBack) {
back.enabledColor = hifi.colors.darkGray;
back.enabled = true;
} else {
back.enabledColor = hifi.colors.baseGray;
back.enabled = false;
}
}
onUrlChanged: { onUrlChanged: {
loadUrl(url); loadUrl(url);
if (startingUp) {
web.initialPage = webview.url;
startingUp = false;
}
} }
QtObject { QtObject {
@ -258,6 +201,17 @@ Item {
grantFeaturePermission(securityOrigin, feature, true); grantFeaturePermission(securityOrigin, feature, true);
} }
onUrlChanged: {
// Record history, skipping null and duplicate items.
var urlString = url + "";
urlString = urlString.replace(/\//g, "%2F"); // Consistent representation of "/"s to avoid false differences.
if (urlString.length > 0 && (historyIndex === -1 || urlString !== history[historyIndex])) {
historyIndex++;
history = history.slice(0, historyIndex);
history.push(urlString);
}
}
onLoadingChanged: { onLoadingChanged: {
keyboardRaised = false; keyboardRaised = false;
punctuationMode = false; punctuationMode = false;
@ -277,17 +231,11 @@ Item {
} }
if (WebEngineView.LoadSucceededStatus == loadRequest.status) { if (WebEngineView.LoadSucceededStatus == loadRequest.status) {
if (startingUp) {
web.initialPage = webview.url;
startingUp = false;
}
webview.forceActiveFocus(); webview.forceActiveFocus();
} }
} }
onNewViewRequested: { onNewViewRequested: {
var currentUrl = webview.url;
urlAppend(currentUrl);
request.openIn(webview); request.openIn(webview);
} }
} }

View file

@ -4331,13 +4331,6 @@ void Application::update(float deltaTime) {
if (nearbyEntitiesAreReadyForPhysics()) { if (nearbyEntitiesAreReadyForPhysics()) {
_physicsEnabled = true; _physicsEnabled = true;
getMyAvatar()->updateMotionBehaviorFromMenu(); getMyAvatar()->updateMotionBehaviorFromMenu();
} else {
auto characterController = getMyAvatar()->getCharacterController();
if (characterController) {
// if we have a character controller, disable it here so the avatar doesn't get stuck due to
// a non-loading collision hull.
characterController->setEnabled(false);
}
} }
} }
} else if (domainLoadingInProgress) { } else if (domainLoadingInProgress) {
@ -5461,7 +5454,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance());
} }
scriptEngine->registerGlobalObject("Overlays", &_overlays);
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
// hook our avatar and avatar hash map object into this script engine // hook our avatar and avatar hash map object into this script engine
@ -5560,6 +5552,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
auto entityScriptServerLog = DependencyManager::get<EntityScriptServerLogClient>(); auto entityScriptServerLog = DependencyManager::get<EntityScriptServerLogClient>();
scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data());
scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance());
qScriptRegisterMetaType(scriptEngine, OverlayIDtoScriptValue, OverlayIDfromScriptValue); qScriptRegisterMetaType(scriptEngine, OverlayIDtoScriptValue, OverlayIDfromScriptValue);

View file

@ -197,7 +197,7 @@ Menu::Menu() {
0, // QML Qt::Key_Apostrophe, 0, // QML Qt::Key_Apostrophe,
qApp, SLOT(resetSensors())); qApp, SLOT(resetSensors()));
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true, addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableAvatarCollisions, 0, true,
avatar.get(), SLOT(updateMotionBehaviorFromMenu())); avatar.get(), SLOT(updateMotionBehaviorFromMenu()));
// Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here. // Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here.

View file

@ -96,7 +96,7 @@ namespace MenuOption {
const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene"; const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene";
const QString EchoLocalAudio = "Echo Local Audio"; const QString EchoLocalAudio = "Echo Local Audio";
const QString EchoServerAudio = "Echo Server Audio"; const QString EchoServerAudio = "Echo Server Audio";
const QString EnableCharacterController = "Collide with world"; const QString EnableAvatarCollisions = "Enable Avatar Collisions";
const QString EnableInverseKinematics = "Enable Inverse Kinematics"; const QString EnableInverseKinematics = "Enable Inverse Kinematics";
const QString EntityScriptServerLog = "Entity Script Server Log"; const QString EntityScriptServerLog = "Entity Script Server Log";
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation"; const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";

70
interface/src/avatar/MyAvatar.cpp Normal file → Executable file
View file

@ -150,8 +150,6 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) :
// when we leave a domain we lift whatever restrictions that domain may have placed on our scale // when we leave a domain we lift whatever restrictions that domain may have placed on our scale
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::clearScaleRestriction); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::clearScaleRestriction);
_characterController.setEnabled(true);
_bodySensorMatrix = deriveBodyFromHMDSensor(); _bodySensorMatrix = deriveBodyFromHMDSensor();
using namespace recording; using namespace recording;
@ -165,12 +163,14 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) :
if (recordingInterface->getPlayFromCurrentLocation()) { if (recordingInterface->getPlayFromCurrentLocation()) {
setRecordingBasis(); setRecordingBasis();
} }
_wasCharacterControllerEnabled = _characterController.isEnabled(); _previousCollisionGroup = _characterController.computeCollisionGroup();
_characterController.setEnabled(false); _characterController.setCollisionless(true);
} else { } else {
clearRecordingBasis(); clearRecordingBasis();
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
_characterController.setEnabled(_wasCharacterControllerEnabled); if (_previousCollisionGroup != BULLET_COLLISION_GROUP_COLLISIONLESS) {
_characterController.setCollisionless(false);
}
} }
auto audioIO = DependencyManager::get<AudioClient>(); auto audioIO = DependencyManager::get<AudioClient>();
@ -552,12 +552,12 @@ void MyAvatar::simulate(float deltaTime) {
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
if (entityTree) { if (entityTree) {
bool flyingAllowed = true; bool flyingAllowed = true;
bool ghostingAllowed = true; bool collisionlessAllowed = true;
entityTree->withWriteLock([&] { entityTree->withWriteLock([&] {
std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone(); std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone();
if (zone) { if (zone) {
flyingAllowed = zone->getFlyingAllowed(); flyingAllowed = zone->getFlyingAllowed();
ghostingAllowed = zone->getGhostingAllowed(); collisionlessAllowed = zone->getGhostingAllowed();
} }
auto now = usecTimestampNow(); auto now = usecTimestampNow();
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
@ -588,9 +588,7 @@ void MyAvatar::simulate(float deltaTime) {
} }
}); });
_characterController.setFlyingAllowed(flyingAllowed); _characterController.setFlyingAllowed(flyingAllowed);
if (!_characterController.isEnabled() && !ghostingAllowed) { _characterController.setCollisionlessAllowed(collisionlessAllowed);
_characterController.setEnabled(true);
}
} }
updateAvatarEntities(); updateAvatarEntities();
@ -1449,7 +1447,8 @@ void MyAvatar::updateMotors() {
_characterController.clearMotors(); _characterController.clearMotors();
glm::quat motorRotation; glm::quat motorRotation;
if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) {
if (_characterController.getState() == CharacterController::State::Hover) { if (_characterController.getState() == CharacterController::State::Hover ||
_characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
motorRotation = getMyHead()->getCameraOrientation(); motorRotation = getMyHead()->getCameraOrientation();
} else { } else {
// non-hovering = walking: follow camera twist about vertical but not lift // non-hovering = walking: follow camera twist about vertical but not lift
@ -1495,6 +1494,7 @@ void MyAvatar::prepareForPhysicsSimulation() {
qDebug() << "Warning: getParentVelocity failed" << getID(); qDebug() << "Warning: getParentVelocity failed" << getID();
parentVelocity = glm::vec3(); parentVelocity = glm::vec3();
} }
_characterController.handleChangedCollisionGroup();
_characterController.setParentVelocity(parentVelocity); _characterController.setParentVelocity(parentVelocity);
_characterController.setPositionAndOrientation(getPosition(), getOrientation()); _characterController.setPositionAndOrientation(getPosition(), getOrientation());
@ -1883,8 +1883,9 @@ void MyAvatar::updateActionMotor(float deltaTime) {
glm::vec3 direction = forward + right; glm::vec3 direction = forward + right;
CharacterController::State state = _characterController.getState(); CharacterController::State state = _characterController.getState();
if (state == CharacterController::State::Hover) { if (state == CharacterController::State::Hover ||
// we're flying --> support vertical motion _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
// we can fly --> support vertical motion
glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
direction += up; direction += up;
} }
@ -1906,7 +1907,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
float finalMaxMotorSpeed = getUniformScale() * MAX_ACTION_MOTOR_SPEED; float finalMaxMotorSpeed = getUniformScale() * MAX_ACTION_MOTOR_SPEED;
float speedGrowthTimescale = 2.0f; float speedGrowthTimescale = 2.0f;
float speedIncreaseFactor = 1.8f; float speedIncreaseFactor = 1.8f;
motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale , 0.0f, 1.0f) * speedIncreaseFactor; motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor;
const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED; const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED;
if (_isPushing) { if (_isPushing) {
@ -1949,9 +1950,17 @@ void MyAvatar::updatePosition(float deltaTime) {
measureMotionDerivatives(deltaTime); measureMotionDerivatives(deltaTime);
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED; _moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
} else { } else {
// physics physics simulation updated elsewhere
float speed2 = glm::length2(velocity); float speed2 = glm::length2(velocity);
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED; _moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
if (_moving) {
// scan for walkability
glm::vec3 position = getPosition();
MyCharacterController::RayShotgunResult result;
glm::vec3 step = deltaTime * (getRotation() * _actionMotorVelocity);
_characterController.testRayShotgun(position, step, result);
_characterController.setStepUpEnabled(result.walkable);
}
} }
// capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly.
@ -2188,30 +2197,33 @@ void MyAvatar::updateMotionBehaviorFromMenu() {
} else { } else {
_motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; _motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;
} }
setCollisionsEnabled(menu->isOptionChecked(MenuOption::EnableAvatarCollisions));
setCharacterControllerEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController));
} }
void MyAvatar::setCharacterControllerEnabled(bool enabled) { void MyAvatar::setCollisionsEnabled(bool enabled) {
if (QThread::currentThread() != thread()) { if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setCharacterControllerEnabled", Q_ARG(bool, enabled)); QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
return; return;
} }
bool ghostingAllowed = true; _characterController.setCollisionless(!enabled);
auto entityTreeRenderer = qApp->getEntities(); }
if (entityTreeRenderer) {
std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone(); bool MyAvatar::getCollisionsEnabled() {
if (zone) { // may return 'false' even though the collisionless option was requested
ghostingAllowed = zone->getGhostingAllowed(); // because the zone may disallow collisionless avatars
} return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
} }
_characterController.setEnabled(ghostingAllowed ? enabled : true);
void MyAvatar::setCharacterControllerEnabled(bool enabled) {
qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead.";
setCollisionsEnabled(enabled);
} }
bool MyAvatar::getCharacterControllerEnabled() { bool MyAvatar::getCharacterControllerEnabled() {
return _characterController.isEnabled(); qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead.";
return getCollisionsEnabled();
} }
void MyAvatar::clearDriveKeys() { void MyAvatar::clearDriveKeys() {

View file

@ -96,7 +96,7 @@ class MyAvatar : public Avatar {
* @property rightHandTipPose {Pose} READ-ONLY. Returns a pose offset 30 cm from MyAvatar.rightHandPose * @property rightHandTipPose {Pose} READ-ONLY. Returns a pose offset 30 cm from MyAvatar.rightHandPose
* @property hmdLeanRecenterEnabled {bool} This can be used disable the hmd lean recenter behavior. This behavior is what causes your avatar * @property hmdLeanRecenterEnabled {bool} This can be used disable the hmd lean recenter behavior. This behavior is what causes your avatar
* to follow your HMD as you walk around the room, in room scale VR. Disabling this is useful if you desire to pin the avatar to a fixed location. * to follow your HMD as you walk around the room, in room scale VR. Disabling this is useful if you desire to pin the avatar to a fixed location.
* @property characterControllerEnabled {bool} This can be used to disable collisions between the avatar and the world. * @property collisionsEnabled {bool} This can be used to disable collisions between the avatar and the world.
* @property useAdvancedMovementControls {bool} Stores the user preference only, does not change user mappings, this is done in the defaultScript * @property useAdvancedMovementControls {bool} Stores the user preference only, does not change user mappings, this is done in the defaultScript
* "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js". * "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js".
*/ */
@ -128,6 +128,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(float isAway READ getIsAway WRITE setAway) Q_PROPERTY(float isAway READ getIsAway WRITE setAway)
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
@ -470,8 +471,10 @@ public:
bool hasDriveInput() const; bool hasDriveInput() const;
Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); Q_INVOKABLE void setCollisionsEnabled(bool enabled);
Q_INVOKABLE bool getCharacterControllerEnabled(); Q_INVOKABLE bool getCollisionsEnabled();
Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated
Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override;
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override;
@ -614,7 +617,7 @@ private:
SharedSoundPointer _collisionSound; SharedSoundPointer _collisionSound;
MyCharacterController _characterController; MyCharacterController _characterController;
bool _wasCharacterControllerEnabled { true }; int16_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR };
AvatarWeakPointer _lookAtTargetAvatar; AvatarWeakPointer _lookAtTargetAvatar;
glm::vec3 _targetAvatarPosition; glm::vec3 _targetAvatarPosition;

286
interface/src/avatar/MyCharacterController.cpp Normal file → Executable file
View file

@ -15,11 +15,15 @@
#include "MyAvatar.h" #include "MyAvatar.h"
// TODO: improve walking up steps
// TODO: make avatars able to walk up and down steps/slopes
// TODO: make avatars stand on steep slope // TODO: make avatars stand on steep slope
// TODO: make avatars not snag on low ceilings // TODO: make avatars not snag on low ceilings
void MyCharacterController::RayShotgunResult::reset() {
hitFraction = 1.0f;
walkable = true;
}
MyCharacterController::MyCharacterController(MyAvatar* avatar) { MyCharacterController::MyCharacterController(MyAvatar* avatar) {
assert(avatar); assert(avatar);
@ -30,37 +34,33 @@ MyCharacterController::MyCharacterController(MyAvatar* avatar) {
MyCharacterController::~MyCharacterController() { MyCharacterController::~MyCharacterController() {
} }
void MyCharacterController::setDynamicsWorld(btDynamicsWorld* world) {
CharacterController::setDynamicsWorld(world);
if (world) {
initRayShotgun(world);
}
}
void MyCharacterController::updateShapeIfNecessary() { void MyCharacterController::updateShapeIfNecessary() {
if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) { if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) {
_pendingFlags &= ~PENDING_FLAG_UPDATE_SHAPE; _pendingFlags &= ~PENDING_FLAG_UPDATE_SHAPE;
// compute new dimensions from avatar's bounding box
float x = _boxScale.x;
float z = _boxScale.z;
_radius = 0.5f * sqrtf(0.5f * (x * x + z * z));
_halfHeight = 0.5f * _boxScale.y - _radius;
float MIN_HALF_HEIGHT = 0.1f;
if (_halfHeight < MIN_HALF_HEIGHT) {
_halfHeight = MIN_HALF_HEIGHT;
}
// NOTE: _shapeLocalOffset is already computed
if (_radius > 0.0f) { if (_radius > 0.0f) {
// create RigidBody if it doesn't exist // create RigidBody if it doesn't exist
if (!_rigidBody) { if (!_rigidBody) {
btCollisionShape* shape = computeShape();
// HACK: use some simple mass property defaults for now // HACK: use some simple mass property defaults for now
const float DEFAULT_AVATAR_MASS = 100.0f; const btScalar DEFAULT_AVATAR_MASS = 100.0f;
const btVector3 DEFAULT_AVATAR_INERTIA_TENSOR(30.0f, 8.0f, 30.0f); const btVector3 DEFAULT_AVATAR_INERTIA_TENSOR(30.0f, 8.0f, 30.0f);
btCollisionShape* shape = new btCapsuleShape(_radius, 2.0f * _halfHeight);
_rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR); _rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR);
} else { } else {
btCollisionShape* shape = _rigidBody->getCollisionShape(); btCollisionShape* shape = _rigidBody->getCollisionShape();
if (shape) { if (shape) {
delete shape; delete shape;
} }
shape = new btCapsuleShape(_radius, 2.0f * _halfHeight); shape = computeShape();
_rigidBody->setCollisionShape(shape); _rigidBody->setCollisionShape(shape);
} }
@ -72,12 +72,262 @@ void MyCharacterController::updateShapeIfNecessary() {
if (_state == State::Hover) { if (_state == State::Hover) {
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f)); _rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
} else { } else {
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp); _rigidBody->setGravity(_gravity * _currentUp);
} }
//_rigidBody->setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT); _rigidBody->setCollisionFlags(_rigidBody->getCollisionFlags() &
~(btCollisionObject::CF_KINEMATIC_OBJECT | btCollisionObject::CF_STATIC_OBJECT));
} else { } else {
// TODO: handle this failure case // TODO: handle this failure case
} }
} }
} }
bool MyCharacterController::testRayShotgun(const glm::vec3& position, const glm::vec3& step, RayShotgunResult& result) {
btVector3 rayDirection = glmToBullet(step);
btScalar stepLength = rayDirection.length();
if (stepLength < FLT_EPSILON) {
return false;
}
rayDirection /= stepLength;
// get _ghost ready for ray traces
btTransform transform = _rigidBody->getWorldTransform();
btVector3 newPosition = glmToBullet(position);
transform.setOrigin(newPosition);
_ghost.setWorldTransform(transform);
btMatrix3x3 rotation = transform.getBasis();
_ghost.refreshOverlappingPairCache();
CharacterRayResult rayResult(&_ghost);
CharacterRayResult closestRayResult(&_ghost);
btVector3 rayStart;
btVector3 rayEnd;
// compute rotation that will orient local ray start points to face step direction
btVector3 forward = rotation * btVector3(0.0f, 0.0f, -1.0f);
btVector3 adjustedDirection = rayDirection - rayDirection.dot(_currentUp) * _currentUp;
btVector3 axis = forward.cross(adjustedDirection);
btScalar lengthAxis = axis.length();
if (lengthAxis > FLT_EPSILON) {
// we're walking sideways
btScalar angle = acosf(lengthAxis / adjustedDirection.length());
if (rayDirection.dot(forward) < 0.0f) {
angle = PI - angle;
}
axis /= lengthAxis;
rotation = btMatrix3x3(btQuaternion(axis, angle)) * rotation;
} else if (rayDirection.dot(forward) < 0.0f) {
// we're walking backwards
rotation = btMatrix3x3(btQuaternion(_currentUp, PI)) * rotation;
}
// scan the top
// NOTE: if we scan an extra distance forward we can detect flat surfaces that are too steep to walk on.
// The approximate extra distance can be derived with trigonometry.
//
// minimumForward = [ (maxStepHeight + radius / cosTheta - radius) * (cosTheta / sinTheta) - radius ]
//
// where: theta = max angle between floor normal and vertical
//
// if stepLength is not long enough we can add the difference.
//
btScalar cosTheta = _minFloorNormalDotUp;
btScalar sinTheta = sqrtf(1.0f - cosTheta * cosTheta);
const btScalar MIN_FORWARD_SLOP = 0.12f; // HACK: not sure why this is necessary to detect steepest walkable slope
btScalar forwardSlop = (_maxStepHeight + _radius / cosTheta - _radius) * (cosTheta / sinTheta) - (_radius + stepLength) + MIN_FORWARD_SLOP;
if (forwardSlop < 0.0f) {
// BIG step, no slop necessary
forwardSlop = 0.0f;
}
const btScalar backSlop = 0.04f;
for (int32_t i = 0; i < _topPoints.size(); ++i) {
rayStart = newPosition + rotation * _topPoints[i] - backSlop * rayDirection;
rayEnd = rayStart + (backSlop + stepLength + forwardSlop) * rayDirection;
if (_ghost.rayTest(rayStart, rayEnd, rayResult)) {
if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) {
closestRayResult = rayResult;
}
if (result.walkable) {
if (rayResult.m_hitNormalWorld.dot(_currentUp) < _minFloorNormalDotUp) {
result.walkable = false;
// the top scan wasn't walkable so don't bother scanning the bottom
// remove both forwardSlop and backSlop
result.hitFraction = glm::min(1.0f, (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop) - backSlop) / stepLength);
return result.hitFraction < 1.0f;
}
}
}
}
if (_state == State::Hover) {
// scan the bottom just like the top
for (int32_t i = 0; i < _bottomPoints.size(); ++i) {
rayStart = newPosition + rotation * _bottomPoints[i] - backSlop * rayDirection;
rayEnd = rayStart + (backSlop + stepLength + forwardSlop) * rayDirection;
if (_ghost.rayTest(rayStart, rayEnd, rayResult)) {
if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) {
closestRayResult = rayResult;
}
if (result.walkable) {
if (rayResult.m_hitNormalWorld.dot(_currentUp) < _minFloorNormalDotUp) {
result.walkable = false;
// the bottom scan wasn't walkable
// remove both forwardSlop and backSlop
result.hitFraction = glm::min(1.0f, (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop) - backSlop) / stepLength);
return result.hitFraction < 1.0f;
}
}
}
}
} else {
// scan the bottom looking for nearest step point
// remove forwardSlop
result.hitFraction = (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop)) / (backSlop + stepLength);
for (int32_t i = 0; i < _bottomPoints.size(); ++i) {
rayStart = newPosition + rotation * _bottomPoints[i] - backSlop * rayDirection;
rayEnd = rayStart + (backSlop + stepLength) * rayDirection;
if (_ghost.rayTest(rayStart, rayEnd, rayResult)) {
if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) {
closestRayResult = rayResult;
}
}
}
// remove backSlop
// NOTE: backSlop removal can produce a NEGATIVE hitFraction!
// which means the shape is actually in interpenetration
result.hitFraction = ((closestRayResult.m_closestHitFraction * (backSlop + stepLength)) - backSlop) / stepLength;
}
return result.hitFraction < 1.0f;
}
btConvexHullShape* MyCharacterController::computeShape() const {
// HACK: the avatar collides using convex hull with a collision margin equal to
// the old capsule radius. Two points define a capsule and additional points are
// spread out at chest level to produce a slight taper toward the feet. This
// makes the avatar more likely to collide with vertical walls at a higher point
// and thus less likely to produce a single-point collision manifold below the
// _maxStepHeight when walking into against vertical surfaces --> fixes a bug
// where the "walk up steps" feature would allow the avatar to walk up vertical
// walls.
const int32_t NUM_POINTS = 6;
btVector3 points[NUM_POINTS];
btVector3 xAxis = btVector3(1.0f, 0.0f, 0.0f);
btVector3 yAxis = btVector3(0.0f, 1.0f, 0.0f);
btVector3 zAxis = btVector3(0.0f, 0.0f, 1.0f);
points[0] = _halfHeight * yAxis;
points[1] = -_halfHeight * yAxis;
points[2] = (0.75f * _halfHeight) * yAxis - (0.1f * _radius) * zAxis;
points[3] = (0.75f * _halfHeight) * yAxis + (0.1f * _radius) * zAxis;
points[4] = (0.75f * _halfHeight) * yAxis - (0.1f * _radius) * xAxis;
points[5] = (0.75f * _halfHeight) * yAxis + (0.1f * _radius) * xAxis;
btConvexHullShape* shape = new btConvexHullShape(reinterpret_cast<btScalar*>(points), NUM_POINTS);
shape->setMargin(_radius);
return shape;
}
void MyCharacterController::initRayShotgun(const btCollisionWorld* world) {
// In order to trace rays out from the avatar's shape surface we need to know where the start points are in
// the local-frame. Since the avatar shape is somewhat irregular computing these points by hand is a hassle
// so instead we ray-trace backwards to the avatar to find them.
//
// We trace back a regular grid (see below) of points against the shape and keep any that hit.
// ___
// + / + \ +
// |+ +|
// +| + | +
// |+ +|
// +| + | +
// |+ +|
// + \ + / +
// ---
// The shotgun will send rays out from these same points to see if the avatar's shape can proceed through space.
// helper class for simple ray-traces against character
class MeOnlyResultCallback : public btCollisionWorld::ClosestRayResultCallback {
public:
MeOnlyResultCallback (btRigidBody* me) : btCollisionWorld::ClosestRayResultCallback(btVector3(0.0f, 0.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)) {
_me = me;
m_collisionFilterGroup = BULLET_COLLISION_GROUP_DYNAMIC;
m_collisionFilterMask = BULLET_COLLISION_MASK_DYNAMIC;
}
virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult,bool normalInWorldSpace) override {
if (rayResult.m_collisionObject != _me) {
return 1.0f;
}
return ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace);
}
btRigidBody* _me;
};
const btScalar fullHalfHeight = _radius + _halfHeight;
const btScalar divisionLine = -fullHalfHeight + _maxStepHeight; // line between top and bottom
const btScalar topHeight = fullHalfHeight - divisionLine;
const btScalar slop = 0.02f;
const int32_t NUM_ROWS = 5; // must be odd number > 1
const int32_t NUM_COLUMNS = 5; // must be odd number > 1
btVector3 reach = (2.0f * _radius) * btVector3(0.0f, 0.0f, 1.0f);
{ // top points
_topPoints.clear();
_topPoints.reserve(NUM_ROWS * NUM_COLUMNS);
btScalar stepY = (topHeight - slop) / (btScalar)(NUM_ROWS - 1);
btScalar stepX = 2.0f * (_radius - slop) / (btScalar)(NUM_COLUMNS - 1);
btTransform transform = _rigidBody->getWorldTransform();
btVector3 position = transform.getOrigin();
btMatrix3x3 rotation = transform.getBasis();
for (int32_t i = 0; i < NUM_ROWS; ++i) {
int32_t maxJ = NUM_COLUMNS;
btScalar offsetX = -(btScalar)((NUM_COLUMNS - 1) / 2) * stepX;
if (i % 2 == 1) {
// odd rows have one less point and start a halfStep closer
maxJ -= 1;
offsetX += 0.5f * stepX;
}
for (int32_t j = 0; j < maxJ; ++j) {
btVector3 localRayEnd(offsetX + (btScalar)(j) * stepX, divisionLine + (btScalar)(i) * stepY, 0.0f);
btVector3 localRayStart = localRayEnd - reach;
MeOnlyResultCallback result(_rigidBody);
world->rayTest(position + rotation * localRayStart, position + rotation * localRayEnd, result);
if (result.m_closestHitFraction < 1.0f) {
_topPoints.push_back(localRayStart + result.m_closestHitFraction * reach);
}
}
}
}
{ // bottom points
_bottomPoints.clear();
_bottomPoints.reserve(NUM_ROWS * NUM_COLUMNS);
btScalar steepestStepHitHeight = (_radius + 0.04f) * (1.0f - DEFAULT_MIN_FLOOR_NORMAL_DOT_UP);
btScalar stepY = (_maxStepHeight - slop - steepestStepHitHeight) / (btScalar)(NUM_ROWS - 1);
btScalar stepX = 2.0f * (_radius - slop) / (btScalar)(NUM_COLUMNS - 1);
btTransform transform = _rigidBody->getWorldTransform();
btVector3 position = transform.getOrigin();
btMatrix3x3 rotation = transform.getBasis();
for (int32_t i = 0; i < NUM_ROWS; ++i) {
int32_t maxJ = NUM_COLUMNS;
btScalar offsetX = -(btScalar)((NUM_COLUMNS - 1) / 2) * stepX;
if (i % 2 == 1) {
// odd rows have one less point and start a halfStep closer
maxJ -= 1;
offsetX += 0.5f * stepX;
}
for (int32_t j = 0; j < maxJ; ++j) {
btVector3 localRayEnd(offsetX + (btScalar)(j) * stepX, (divisionLine - slop) - (btScalar)(i) * stepY, 0.0f);
btVector3 localRayStart = localRayEnd - reach;
MeOnlyResultCallback result(_rigidBody);
world->rayTest(position + rotation * localRayStart, position + rotation * localRayEnd, result);
if (result.m_closestHitFraction < 1.0f) {
_bottomPoints.push_back(localRayStart + result.m_closestHitFraction * reach);
}
}
}
}
}

View file

@ -24,10 +24,34 @@ public:
explicit MyCharacterController(MyAvatar* avatar); explicit MyCharacterController(MyAvatar* avatar);
~MyCharacterController (); ~MyCharacterController ();
virtual void updateShapeIfNecessary() override; void setDynamicsWorld(btDynamicsWorld* world) override;
void updateShapeIfNecessary() override;
// Sweeping a convex shape through the physics simulation can be expensive when the obstacles are too
// complex (e.g. small 20k triangle static mesh) so instead we cast several rays forward and if they
// don't hit anything we consider it a clean sweep. Hence this "Shotgun" code.
class RayShotgunResult {
public:
void reset();
float hitFraction { 1.0f };
bool walkable { true };
};
/// return true if RayShotgun hits anything
bool testRayShotgun(const glm::vec3& position, const glm::vec3& step, RayShotgunResult& result);
protected:
void initRayShotgun(const btCollisionWorld* world);
private:
btConvexHullShape* computeShape() const;
protected: protected:
MyAvatar* _avatar { nullptr }; MyAvatar* _avatar { nullptr };
// shotgun scan data
btAlignedObjectArray<btVector3> _topPoints;
btAlignedObjectArray<btVector3> _bottomPoints;
}; };
#endif // hifi_MyCharacterController_h #endif // hifi_MyCharacterController_h

View file

@ -37,7 +37,14 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
Head* head = _owningAvatar->getHead(); Head* head = _owningAvatar->getHead();
// make sure lookAt is not too close to face (avoid crosseyes) // make sure lookAt is not too close to face (avoid crosseyes)
glm::vec3 lookAt = _owningAvatar->isMyAvatar() ? head->getLookAtPosition() : head->getCorrectedLookAtPosition(); glm::vec3 lookAt = head->getLookAtPosition();
glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition();
float focusDistance = glm::length(focusOffset);
const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f;
if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) {
lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset;
}
MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar); MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
Rig::HeadParameters headParams; Rig::HeadParameters headParams;
@ -152,7 +159,5 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex; eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
_rig->updateFromEyeParameters(eyeParams); _rig->updateFromEyeParameters(eyeParams);
Parent::updateRig(deltaTime, parentTransform);
} }

View file

@ -34,7 +34,7 @@ class AvatarInputs : public QQuickItem {
public: public:
static AvatarInputs* getInstance(); static AvatarInputs* getInstance();
float loudnessToAudioLevel(float loudness); Q_INVOKABLE float loudnessToAudioLevel(float loudness);
AvatarInputs(QQuickItem* parent = nullptr); AvatarInputs(QQuickItem* parent = nullptr);
void update(); void update();
bool showAudioTools() const { return _showAudioTools; } bool showAudioTools() const { return _showAudioTools; }

View file

@ -28,11 +28,15 @@ const int MAX_HISTORY_SIZE = 64;
const QString COMMAND_STYLE = "color: #266a9b;"; const QString COMMAND_STYLE = "color: #266a9b;";
const QString RESULT_SUCCESS_STYLE = "color: #677373;"; const QString RESULT_SUCCESS_STYLE = "color: #677373;";
const QString RESULT_INFO_STYLE = "color: #223bd1;";
const QString RESULT_WARNING_STYLE = "color: #d13b22;";
const QString RESULT_ERROR_STYLE = "color: #d13b22;"; const QString RESULT_ERROR_STYLE = "color: #d13b22;";
const QString GUTTER_PREVIOUS_COMMAND = "<span style=\"color: #57b8bb;\">&lt;</span>"; const QString GUTTER_PREVIOUS_COMMAND = "<span style=\"color: #57b8bb;\">&lt;</span>";
const QString GUTTER_ERROR = "<span style=\"color: #d13b22;\">X</span>"; const QString GUTTER_ERROR = "<span style=\"color: #d13b22;\">X</span>";
const QString JSConsole::_consoleFileName { "about:console" };
JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) : JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) :
QWidget(parent), QWidget(parent),
_ui(new Ui::Console), _ui(new Ui::Console),
@ -77,6 +81,8 @@ void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) {
} }
if (_scriptEngine != NULL) { if (_scriptEngine != NULL) {
disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint);
disconnect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo);
disconnect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning);
disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError);
if (_ownScriptEngine) { if (_ownScriptEngine) {
_scriptEngine->deleteLater(); _scriptEngine->deleteLater();
@ -84,10 +90,12 @@ void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) {
} }
// if scriptEngine is NULL then create one and keep track of it using _ownScriptEngine // if scriptEngine is NULL then create one and keep track of it using _ownScriptEngine
_ownScriptEngine = scriptEngine == NULL; _ownScriptEngine = (scriptEngine == NULL);
_scriptEngine = _ownScriptEngine ? DependencyManager::get<ScriptEngines>()->loadScript(QString(), false) : scriptEngine; _scriptEngine = _ownScriptEngine ? DependencyManager::get<ScriptEngines>()->loadScript(_consoleFileName, false) : scriptEngine;
connect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); connect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint);
connect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo);
connect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning);
connect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); connect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError);
} }
@ -107,11 +115,10 @@ void JSConsole::executeCommand(const QString& command) {
QScriptValue JSConsole::executeCommandInWatcher(const QString& command) { QScriptValue JSConsole::executeCommandInWatcher(const QString& command) {
QScriptValue result; QScriptValue result;
static const QString filename = "JSConcole";
QMetaObject::invokeMethod(_scriptEngine, "evaluate", Qt::ConnectionType::BlockingQueuedConnection, QMetaObject::invokeMethod(_scriptEngine, "evaluate", Qt::ConnectionType::BlockingQueuedConnection,
Q_RETURN_ARG(QScriptValue, result), Q_RETURN_ARG(QScriptValue, result),
Q_ARG(const QString&, command), Q_ARG(const QString&, command),
Q_ARG(const QString&, filename)); Q_ARG(const QString&, _consoleFileName));
return result; return result;
} }
@ -134,16 +141,26 @@ void JSConsole::commandFinished() {
resetCurrentCommandHistory(); resetCurrentCommandHistory();
} }
void JSConsole::handleError(const QString& scriptName, const QString& message) { void JSConsole::handleError(const QString& message, const QString& scriptName) {
Q_UNUSED(scriptName); Q_UNUSED(scriptName);
appendMessage(GUTTER_ERROR, "<span style='" + RESULT_ERROR_STYLE + "'>" + message.toHtmlEscaped() + "</span>"); appendMessage(GUTTER_ERROR, "<span style='" + RESULT_ERROR_STYLE + "'>" + message.toHtmlEscaped() + "</span>");
} }
void JSConsole::handlePrint(const QString& scriptName, const QString& message) { void JSConsole::handlePrint(const QString& message, const QString& scriptName) {
Q_UNUSED(scriptName); Q_UNUSED(scriptName);
appendMessage("", message); appendMessage("", message);
} }
void JSConsole::handleInfo(const QString& message, const QString& scriptName) {
Q_UNUSED(scriptName);
appendMessage("", "<span style='" + RESULT_INFO_STYLE + "'>" + message.toHtmlEscaped() + "</span>");
}
void JSConsole::handleWarning(const QString& message, const QString& scriptName) {
Q_UNUSED(scriptName);
appendMessage("", "<span style='" + RESULT_WARNING_STYLE + "'>" + message.toHtmlEscaped() + "</span>");
}
void JSConsole::mouseReleaseEvent(QMouseEvent* event) { void JSConsole::mouseReleaseEvent(QMouseEvent* event) {
_ui->promptTextEdit->setFocus(); _ui->promptTextEdit->setFocus();
} }

View file

@ -47,8 +47,10 @@ protected:
protected slots: protected slots:
void scrollToBottom(); void scrollToBottom();
void resizeTextInput(); void resizeTextInput();
void handlePrint(const QString& scriptName, const QString& message); void handlePrint(const QString& message, const QString& scriptName);
void handleError(const QString& scriptName, const QString& message); void handleInfo(const QString& message, const QString& scriptName);
void handleWarning(const QString& message, const QString& scriptName);
void handleError(const QString& message, const QString& scriptName);
void commandFinished(); void commandFinished();
private: private:
@ -66,6 +68,7 @@ private:
bool _ownScriptEngine; bool _ownScriptEngine;
QString _rootCommand; QString _rootCommand;
ScriptEngine* _scriptEngine; ScriptEngine* _scriptEngine;
static const QString _consoleFileName;
}; };

View file

@ -81,6 +81,10 @@ QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& propert
void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
QVariantMap properties = originalProperties; QVariantMap properties = originalProperties;
if (properties["name"].isValid()) {
setName(properties["name"].toString());
}
// carry over some legacy keys // carry over some legacy keys
if (!properties["position"].isValid() && !properties["localPosition"].isValid()) { if (!properties["position"].isValid() && !properties["localPosition"].isValid()) {
if (properties["p1"].isValid()) { if (properties["p1"].isValid()) {
@ -207,6 +211,9 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
} }
QVariant Base3DOverlay::getProperty(const QString& property) { QVariant Base3DOverlay::getProperty(const QString& property) {
if (property == "name") {
return _name;
}
if (property == "position" || property == "start" || property == "p1" || property == "point") { if (property == "position" || property == "start" || property == "p1" || property == "point") {
return vec3toVariant(getPosition()); return vec3toVariant(getPosition());
} }

View file

@ -26,6 +26,9 @@ public:
virtual OverlayID getOverlayID() const override { return OverlayID(getID().toString()); } virtual OverlayID getOverlayID() const override { return OverlayID(getID().toString()); }
void setOverlayID(OverlayID overlayID) override { setID(overlayID); } void setOverlayID(OverlayID overlayID) override { setID(overlayID); }
virtual QString getName() const override { return QString("Overlay:") + _name; }
void setName(QString name) { _name = name; }
// getters // getters
virtual bool is3D() const override { return true; } virtual bool is3D() const override { return true; }
@ -74,6 +77,8 @@ protected:
bool _drawInFront; bool _drawInFront;
bool _isAA; bool _isAA;
bool _isGrabbable { false }; bool _isGrabbable { false };
QString _name;
}; };
#endif // hifi_Base3DOverlay_h #endif // hifi_Base3DOverlay_h

View file

@ -288,3 +288,10 @@ void ModelOverlay::locationChanged(bool tellPhysics) {
_model->setTranslation(getPosition()); _model->setTranslation(getPosition());
} }
} }
QString ModelOverlay::getName() const {
if (_name != "") {
return QString("Overlay:") + getType() + ":" + _name;
}
return QString("Overlay:") + getType() + ":" + _url.toString();
}

View file

@ -22,6 +22,8 @@ public:
static QString const TYPE; static QString const TYPE;
virtual QString getType() const override { return TYPE; } virtual QString getType() const override { return TYPE; }
virtual QString getName() const override;
ModelOverlay(); ModelOverlay();
ModelOverlay(const ModelOverlay* modelOverlay); ModelOverlay(const ModelOverlay* modelOverlay);

View file

@ -1097,28 +1097,27 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) {
} }
void AudioClient::prepareLocalAudioInjectors() { void AudioClient::prepareLocalAudioInjectors() {
if (_outputPeriod == 0) {
return;
}
int bufferCapacity = _localInjectorsStream.getSampleCapacity();
if (_localToOutputResampler) {
// avoid overwriting the buffer,
// instead of failing on writes because the buffer is used as a lock-free pipe
bufferCapacity -=
_localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) *
AudioConstants::STEREO;
bufferCapacity += 1;
}
int samplesNeeded = std::numeric_limits<int>::max(); int samplesNeeded = std::numeric_limits<int>::max();
while (samplesNeeded > 0) { while (samplesNeeded > 0) {
// lock for every write to avoid locking out the device callback // unlock between every write to allow device switching
// this lock is intentional - the buffer is only lock-free in its use in the device callback Lock lock(_localAudioMutex);
RecursiveLock lock(_localAudioMutex);
// in case of a device switch, consider bufferCapacity volatile across iterations
if (_outputPeriod == 0) {
return;
}
int bufferCapacity = _localInjectorsStream.getSampleCapacity();
int maxOutputSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * AudioConstants::STEREO;
if (_localToOutputResampler) {
maxOutputSamples =
_localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) *
AudioConstants::STEREO;
}
samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed);
if (samplesNeeded <= 0) { if (samplesNeeded < maxOutputSamples) {
// avoid overwriting the buffer to prevent losing frames
break; break;
} }
@ -1168,16 +1167,18 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float));
for (AudioInjector* injector : _activeLocalAudioInjectors) { for (AudioInjector* injector : _activeLocalAudioInjectors) {
if (injector->getLocalBuffer()) { // the lock guarantees that injectorBuffer, if found, is invariant
AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer();
if (injectorBuffer) {
static const int HRTF_DATASET_INDEX = 1; static const int HRTF_DATASET_INDEX = 1;
int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO);
qint64 bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL;
// get one frame from the injector // get one frame from the injector
memset(_localScratchBuffer, 0, bytesToRead); memset(_localScratchBuffer, 0, bytesToRead);
if (0 < injector->getLocalBuffer()->readData((char*)_localScratchBuffer, bytesToRead)) { if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) {
if (injector->isAmbisonic()) { if (injector->isAmbisonic()) {
@ -1317,15 +1318,17 @@ void AudioClient::setIsStereoInput(bool isStereoInput) {
} }
bool AudioClient::outputLocalInjector(AudioInjector* injector) { bool AudioClient::outputLocalInjector(AudioInjector* injector) {
Lock lock(_injectorsMutex); AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer();
if (injector->getLocalBuffer() && _audioInput ) { if (injectorBuffer) {
// just add it to the vector of active local injectors, if // local injectors are on the AudioInjectorsThread, so we must guard access
// not already there. Lock lock(_injectorsMutex);
// Since this is invoked with invokeMethod, there _should_ be
// no reason to lock access to the vector of injectors.
if (!_activeLocalAudioInjectors.contains(injector)) { if (!_activeLocalAudioInjectors.contains(injector)) {
qCDebug(audioclient) << "adding new injector"; qCDebug(audioclient) << "adding new injector";
_activeLocalAudioInjectors.append(injector); _activeLocalAudioInjectors.append(injector);
// move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop())
injectorBuffer->setParent(nullptr);
injectorBuffer->moveToThread(&_localAudioThread);
} else { } else {
qCDebug(audioclient) << "injector exists in active list already"; qCDebug(audioclient) << "injector exists in active list already";
} }
@ -1333,7 +1336,7 @@ bool AudioClient::outputLocalInjector(AudioInjector* injector) {
return true; return true;
} else { } else {
// no local buffer or audio // no local buffer
return false; return false;
} }
} }
@ -1452,7 +1455,7 @@ void AudioClient::outputNotify() {
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
bool supportedFormat = false; bool supportedFormat = false;
RecursiveLock lock(_localAudioMutex); Lock lock(_localAudioMutex);
_localSamplesAvailable.exchange(0, std::memory_order_release); _localSamplesAvailable.exchange(0, std::memory_order_release);
// cleanup any previously initialized device // cleanup any previously initialized device
@ -1681,8 +1684,12 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
int injectorSamplesPopped = 0; int injectorSamplesPopped = 0;
{ {
RecursiveLock lock(_audio->_localAudioMutex);
bool append = networkSamplesPopped > 0; bool append = networkSamplesPopped > 0;
// this does not require a lock as of the only two functions adding to _localSamplesAvailable (samples count):
// - prepareLocalAudioInjectors will only increase samples count
// - switchOutputToAudioDevice will zero samples count
// stop the device, so that readData will exhaust the existing buffer or see a zeroed samples count
// and start the device, which can only see a zeroed samples count
samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire)); samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire));
if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) {
_audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release);

View file

@ -96,8 +96,6 @@ public:
using AudioPositionGetter = std::function<glm::vec3()>; using AudioPositionGetter = std::function<glm::vec3()>;
using AudioOrientationGetter = std::function<glm::quat()>; using AudioOrientationGetter = std::function<glm::quat()>;
using RecursiveMutex = std::recursive_mutex;
using RecursiveLock = std::unique_lock<RecursiveMutex>;
using Mutex = std::mutex; using Mutex = std::mutex;
using Lock = std::unique_lock<Mutex>; using Lock = std::unique_lock<Mutex>;
@ -345,7 +343,7 @@ private:
int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
float* _localOutputMixBuffer { NULL }; float* _localOutputMixBuffer { NULL };
AudioInjectorsThread _localAudioThread; AudioInjectorsThread _localAudioThread;
RecursiveMutex _localAudioMutex; Mutex _localAudioMutex;
// for output audio (used by this thread) // for output audio (used by this thread)
int _outputPeriod { 0 }; int _outputPeriod { 0 };

View file

@ -33,7 +33,11 @@ public:
PacketType packetType, QString codecName = QString("")); PacketType packetType, QString codecName = QString(""));
public slots: public slots:
// threadsafe
// moves injector->getLocalBuffer() to another thread (so removes its parent)
// take care to delete it when ~AudioInjector, as parenting Qt semantics will not work
virtual bool outputLocalInjector(AudioInjector* injector) = 0; virtual bool outputLocalInjector(AudioInjector* injector) = 0;
virtual bool shouldLoopbackInjectors() { return false; } virtual bool shouldLoopbackInjectors() { return false; }
virtual void setIsStereoInput(bool stereo) = 0; virtual void setIsStereoInput(bool stereo) = 0;

View file

@ -51,6 +51,10 @@ AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOpt
{ {
} }
AudioInjector::~AudioInjector() {
deleteLocalBuffer();
}
bool AudioInjector::stateHas(AudioInjectorState state) const { bool AudioInjector::stateHas(AudioInjectorState state) const {
return (_state & state) == state; return (_state & state) == state;
} }
@ -87,11 +91,7 @@ void AudioInjector::finish() {
emit finished(); emit finished();
if (_localBuffer) { deleteLocalBuffer();
_localBuffer->stop();
_localBuffer->deleteLater();
_localBuffer = NULL;
}
if (stateHas(AudioInjectorState::PendingDelete)) { if (stateHas(AudioInjectorState::PendingDelete)) {
// we've been asked to delete after finishing, trigger a deleteLater here // we've been asked to delete after finishing, trigger a deleteLater here
@ -163,7 +163,7 @@ bool AudioInjector::injectLocally() {
if (_localAudioInterface) { if (_localAudioInterface) {
if (_audioData.size() > 0) { if (_audioData.size() > 0) {
_localBuffer = new AudioInjectorLocalBuffer(_audioData, this); _localBuffer = new AudioInjectorLocalBuffer(_audioData);
_localBuffer->open(QIODevice::ReadOnly); _localBuffer->open(QIODevice::ReadOnly);
_localBuffer->setShouldLoop(_options.loop); _localBuffer->setShouldLoop(_options.loop);
@ -172,7 +172,8 @@ bool AudioInjector::injectLocally() {
_localBuffer->setCurrentOffset(_currentSendOffset); _localBuffer->setCurrentOffset(_currentSendOffset);
// call this function on the AudioClient's thread // call this function on the AudioClient's thread
success = QMetaObject::invokeMethod(_localAudioInterface, "outputLocalInjector", Q_ARG(AudioInjector*, this)); // this will move the local buffer's thread to the LocalInjectorThread
success = _localAudioInterface->outputLocalInjector(this);
if (!success) { if (!success) {
qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface";
@ -185,6 +186,14 @@ bool AudioInjector::injectLocally() {
return success; return success;
} }
void AudioInjector::deleteLocalBuffer() {
if (_localBuffer) {
_localBuffer->stop();
_localBuffer->deleteLater();
_localBuffer = nullptr;
}
}
const uchar MAX_INJECTOR_VOLUME = packFloatGainToByte(1.0f); const uchar MAX_INJECTOR_VOLUME = packFloatGainToByte(1.0f);
static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1; static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1;
static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0; static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0;

View file

@ -52,6 +52,7 @@ class AudioInjector : public QObject {
public: public:
AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions);
AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions);
~AudioInjector();
bool isFinished() const { return (stateHas(AudioInjectorState::Finished)); } bool isFinished() const { return (stateHas(AudioInjectorState::Finished)); }
@ -99,6 +100,7 @@ private:
int64_t injectNextFrame(); int64_t injectNextFrame();
bool inject(bool(AudioInjectorManager::*injection)(AudioInjector*)); bool inject(bool(AudioInjectorManager::*injection)(AudioInjector*));
bool injectLocally(); bool injectLocally();
void deleteLocalBuffer();
static AbstractAudioInterface* _localAudioInterface; static AbstractAudioInterface* _localAudioInterface;

View file

@ -11,8 +11,7 @@
#include "AudioInjectorLocalBuffer.h" #include "AudioInjectorLocalBuffer.h"
AudioInjectorLocalBuffer::AudioInjectorLocalBuffer(const QByteArray& rawAudioArray, QObject* parent) : AudioInjectorLocalBuffer::AudioInjectorLocalBuffer(const QByteArray& rawAudioArray) :
QIODevice(parent),
_rawAudioArray(rawAudioArray), _rawAudioArray(rawAudioArray),
_shouldLoop(false), _shouldLoop(false),
_isStopped(false), _isStopped(false),

View file

@ -19,7 +19,7 @@
class AudioInjectorLocalBuffer : public QIODevice { class AudioInjectorLocalBuffer : public QIODevice {
Q_OBJECT Q_OBJECT
public: public:
AudioInjectorLocalBuffer(const QByteArray& rawAudioArray, QObject* parent); AudioInjectorLocalBuffer(const QByteArray& rawAudioArray);
void stop(); void stop();

View file

@ -73,12 +73,13 @@ void SkeletonModel::initJointStates() {
// Called within Model::simulate call, below. // Called within Model::simulate call, below.
void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
assert(!_owningAvatar->isMyAvatar());
const FBXGeometry& geometry = getFBXGeometry(); const FBXGeometry& geometry = getFBXGeometry();
Head* head = _owningAvatar->getHead(); Head* head = _owningAvatar->getHead();
// make sure lookAt is not too close to face (avoid crosseyes) // make sure lookAt is not too close to face (avoid crosseyes)
glm::vec3 lookAt = _owningAvatar->isMyAvatar() ? head->getLookAtPosition() : head->getCorrectedLookAtPosition(); glm::vec3 lookAt = head->getCorrectedLookAtPosition();
glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition();
float focusDistance = glm::length(focusOffset); float focusDistance = glm::length(focusOffset);
const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f;
@ -86,41 +87,36 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset;
} }
if (!_owningAvatar->isMyAvatar()) { // no need to call Model::updateRig() because otherAvatars get their joint state
// no need to call Model::updateRig() because otherAvatars get their joint state // copied directly from AvtarData::_jointData (there are no Rig animations to blend)
// copied directly from AvtarData::_jointData (there are no Rig animations to blend) _needsUpdateClusterMatrices = true;
_needsUpdateClusterMatrices = true;
// This is a little more work than we really want. // This is a little more work than we really want.
// //
// Other avatars joint, including their eyes, should already be set just like any other joints // Other avatars joint, including their eyes, should already be set just like any other joints
// from the wire data. But when looking at me, we want the eyes to use the corrected lookAt. // from the wire data. But when looking at me, we want the eyes to use the corrected lookAt.
// //
// Thus this should really only be ... else if (_owningAvatar->getHead()->isLookingAtMe()) {... // Thus this should really only be ... else if (_owningAvatar->getHead()->isLookingAtMe()) {...
// However, in the !isLookingAtMe case, the eyes aren't rotating the way they should right now. // However, in the !isLookingAtMe case, the eyes aren't rotating the way they should right now.
// We will revisit that as priorities allow, and particularly after the new rig/animation/joints. // We will revisit that as priorities allow, and particularly after the new rig/animation/joints.
// If the head is not positioned, updateEyeJoints won't get the math right // If the head is not positioned, updateEyeJoints won't get the math right
glm::quat headOrientation; glm::quat headOrientation;
_rig->getJointRotation(geometry.headJointIndex, headOrientation); _rig->getJointRotation(geometry.headJointIndex, headOrientation);
glm::vec3 eulers = safeEulerAngles(headOrientation); glm::vec3 eulers = safeEulerAngles(headOrientation);
head->setBasePitch(glm::degrees(-eulers.x)); head->setBasePitch(glm::degrees(-eulers.x));
head->setBaseYaw(glm::degrees(eulers.y)); head->setBaseYaw(glm::degrees(eulers.y));
head->setBaseRoll(glm::degrees(-eulers.z)); head->setBaseRoll(glm::degrees(-eulers.z));
Rig::EyeParameters eyeParams; Rig::EyeParameters eyeParams;
eyeParams.eyeLookAt = lookAt; eyeParams.eyeLookAt = lookAt;
eyeParams.eyeSaccade = glm::vec3(0.0f); eyeParams.eyeSaccade = glm::vec3(0.0f);
eyeParams.modelRotation = getRotation(); eyeParams.modelRotation = getRotation();
eyeParams.modelTranslation = getTranslation(); eyeParams.modelTranslation = getTranslation();
eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex; eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex;
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex; eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
_rig->updateFromEyeParameters(eyeParams); _rig->updateFromEyeParameters(eyeParams);
}
// evaluate AnimGraph animation and update jointStates.
Model::updateRig(deltaTime, parentTransform);
} }
void SkeletonModel::updateAttitude() { void SkeletonModel::updateAttitude() {
@ -136,7 +132,7 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) {
if (fullUpdate) { if (fullUpdate) {
setBlendshapeCoefficients(_owningAvatar->getHead()->getSummedBlendshapeCoefficients()); setBlendshapeCoefficients(_owningAvatar->getHead()->getSummedBlendshapeCoefficients());
Model::simulate(deltaTime, fullUpdate); Parent::simulate(deltaTime, fullUpdate);
// let rig compute the model offset // let rig compute the model offset
glm::vec3 registrationPoint; glm::vec3 registrationPoint;
@ -144,7 +140,7 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) {
setOffset(registrationPoint); setOffset(registrationPoint);
} }
} else { } else {
Model::simulate(deltaTime, fullUpdate); Parent::simulate(deltaTime, fullUpdate);
} }
if (!isActive() || !_owningAvatar->isMyAvatar()) { if (!isActive() || !_owningAvatar->isMyAvatar()) {

View file

@ -23,6 +23,7 @@ using SkeletonModelWeakPointer = std::weak_ptr<SkeletonModel>;
/// A skeleton loaded from a model. /// A skeleton loaded from a model.
class SkeletonModel : public CauterizedModel { class SkeletonModel : public CauterizedModel {
using Parent = CauterizedModel;
Q_OBJECT Q_OBJECT
public: public:

View file

@ -357,6 +357,8 @@ class AvatarData : public QObject, public SpatiallyNestable {
public: public:
virtual QString getName() const override { return QString("Avatar:") + _displayName; }
static const QString FRAME_NAME; static const QString FRAME_NAME;
static void fromFrame(const QByteArray& frameData, AvatarData& avatar, bool useFrameSkeleton = true); static void fromFrame(const QByteArray& frameData, AvatarData& avatar, bool useFrameSkeleton = true);

View file

@ -281,7 +281,7 @@ public:
float getAngularDamping() const; float getAngularDamping() const;
void setAngularDamping(float value); void setAngularDamping(float value);
QString getName() const; virtual QString getName() const override;
void setName(const QString& value); void setName(const QString& value);
QString getDebugName(); QString getDebugName();

View file

@ -407,9 +407,11 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
// return QUuid(); // return QUuid();
// } // }
bool entityFound { false };
_entityTree->withReadLock([&] { _entityTree->withReadLock([&] {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
if (entity) { if (entity) {
entityFound = true;
// make sure the properties has a type, so that the encode can know which properties to include // make sure the properties has a type, so that the encode can know which properties to include
properties.setType(entity->getType()); properties.setType(entity->getType());
bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges(); bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges();
@ -464,6 +466,27 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
}); });
} }
}); });
if (!entityFound) {
// we've made an edit to an entity we don't know about, or to a non-entity. If it's a known non-entity,
// print a warning and don't send an edit packet to the entity-server.
QSharedPointer<SpatialParentFinder> parentFinder = DependencyManager::get<SpatialParentFinder>();
if (parentFinder) {
bool success;
auto nestableWP = parentFinder->find(id, success, static_cast<SpatialParentTree*>(_entityTree.get()));
if (success) {
auto nestable = nestableWP.lock();
if (nestable) {
NestableType nestableType = nestable->getNestableType();
if (nestableType == NestableType::Overlay || nestableType == NestableType::Avatar) {
qCWarning(entities) << "attempted edit on non-entity: " << id << nestable->getName();
return QUuid(); // null UUID to indicate failure
}
}
}
}
}
// we queue edit packets even if we don't know about the entity. This is to allow AC agents
// to edit entities they know only by ID.
queueEntityMessage(PacketType::EntityEdit, entityID, properties); queueEntityMessage(PacketType::EntityEdit, entityID, properties);
return id; return id;
} }
@ -1515,6 +1538,24 @@ bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) {
return isChild; return isChild;
} }
QString EntityScriptingInterface::getNestableType(QUuid id) {
QSharedPointer<SpatialParentFinder> parentFinder = DependencyManager::get<SpatialParentFinder>();
if (!parentFinder) {
return "unknown";
}
bool success;
SpatiallyNestableWeakPointer objectWP = parentFinder->find(id, success);
if (!success) {
return "unknown";
}
SpatiallyNestablePointer object = objectWP.lock();
if (!object) {
return "unknown";
}
NestableType nestableType = object->getNestableType();
return SpatiallyNestable::nestableTypeToString(nestableType);
}
QVector<QUuid> EntityScriptingInterface::getChildrenIDsOfJoint(const QUuid& parentID, int jointIndex) { QVector<QUuid> EntityScriptingInterface::getChildrenIDsOfJoint(const QUuid& parentID, int jointIndex) {
QVector<QUuid> result; QVector<QUuid> result;
if (!_entityTree) { if (!_entityTree) {

View file

@ -304,6 +304,8 @@ public slots:
Q_INVOKABLE QVector<QUuid> getChildrenIDsOfJoint(const QUuid& parentID, int jointIndex); Q_INVOKABLE QVector<QUuid> getChildrenIDsOfJoint(const QUuid& parentID, int jointIndex);
Q_INVOKABLE bool isChildOfParent(QUuid childID, QUuid parentID); Q_INVOKABLE bool isChildOfParent(QUuid childID, QUuid parentID);
Q_INVOKABLE QString getNestableType(QUuid id);
Q_INVOKABLE QUuid getKeyboardFocusEntity() const; Q_INVOKABLE QUuid getKeyboardFocusEntity() const;
Q_INVOKABLE void setKeyboardFocusEntity(QUuid id); Q_INVOKABLE void setKeyboardFocusEntity(QUuid id);

View file

@ -990,6 +990,17 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
entityItemID, properties); entityItemID, properties);
endDecode = usecTimestampNow(); endDecode = usecTimestampNow();
EntityItemPointer existingEntity;
if (!isAdd) {
// search for the entity by EntityItemID
startLookup = usecTimestampNow();
existingEntity = findEntityByEntityItemID(entityItemID);
endLookup = usecTimestampNow();
if (!existingEntity) {
// this is not an add-entity operation, and we don't know about the identified entity.
validEditPacket = false;
}
}
if (validEditPacket && !_entityScriptSourceWhitelist.isEmpty() && !properties.getScript().isEmpty()) { if (validEditPacket && !_entityScriptSourceWhitelist.isEmpty() && !properties.getScript().isEmpty()) {
bool passedWhiteList = false; bool passedWhiteList = false;
@ -1036,12 +1047,6 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
// If we got a valid edit packet, then it could be a new entity or it could be an update to // If we got a valid edit packet, then it could be a new entity or it could be an update to
// an existing entity... handle appropriately // an existing entity... handle appropriately
if (validEditPacket) { if (validEditPacket) {
// search for the entity by EntityItemID
startLookup = usecTimestampNow();
EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID);
endLookup = usecTimestampNow();
startFilter = usecTimestampNow(); startFilter = usecTimestampNow();
bool wasChanged = false; bool wasChanged = false;
// Having (un)lock rights bypasses the filter, unless it's a physics result. // Having (un)lock rights bypasses the filter, unless it's a physics result.

View file

@ -149,6 +149,10 @@ void GLBackend::resetUniformStage() {
void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) { void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) {
GLuint slot = batch._params[paramOffset + 3]._uint; GLuint slot = batch._params[paramOffset + 3]._uint;
if (slot >(GLuint)MAX_NUM_UNIFORM_BUFFERS) {
qCDebug(gpugllogging) << "GLBackend::do_setUniformBuffer: Trying to set a uniform Buffer at slot #" << slot << " which doesn't exist. MaxNumUniformBuffers = " << getMaxNumUniformBuffers();
return;
}
BufferPointer uniformBuffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); BufferPointer uniformBuffer = batch._buffers.get(batch._params[paramOffset + 2]._uint);
GLintptr rangeStart = batch._params[paramOffset + 1]._uint; GLintptr rangeStart = batch._params[paramOffset + 1]._uint;
GLsizeiptr rangeSize = batch._params[paramOffset + 0]._uint; GLsizeiptr rangeSize = batch._params[paramOffset + 0]._uint;
@ -203,7 +207,7 @@ void GLBackend::resetResourceStage() {
void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) { void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) {
GLuint slot = batch._params[paramOffset + 1]._uint; GLuint slot = batch._params[paramOffset + 1]._uint;
if (slot >= (GLuint)MAX_NUM_RESOURCE_BUFFERS) { if (slot >= (GLuint)MAX_NUM_RESOURCE_BUFFERS) {
// "GLBackend::do_setResourceBuffer: Trying to set a resource Buffer at slot #" + slot + " which doesn't exist. MaxNumResourceBuffers = " + getMaxNumResourceBuffers()); qCDebug(gpugllogging) << "GLBackend::do_setResourceBuffer: Trying to set a resource Buffer at slot #" << slot << " which doesn't exist. MaxNumResourceBuffers = " << getMaxNumResourceBuffers();
return; return;
} }
@ -233,7 +237,7 @@ void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) {
void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) {
GLuint slot = batch._params[paramOffset + 1]._uint; GLuint slot = batch._params[paramOffset + 1]._uint;
if (slot >= (GLuint) MAX_NUM_RESOURCE_TEXTURES) { if (slot >= (GLuint) MAX_NUM_RESOURCE_TEXTURES) {
// "GLBackend::do_setResourceTexture: Trying to set a resource Texture at slot #" + slot + " which doesn't exist. MaxNumResourceTextures = " + getMaxNumResourceTextures()); qCDebug(gpugllogging) << "GLBackend::do_setResourceTexture: Trying to set a resource Texture at slot #" << slot << " which doesn't exist. MaxNumResourceTextures = " << getMaxNumResourceTextures();
return; return;
} }

View file

@ -19,8 +19,8 @@
#include <DependencyManager.h> #include <DependencyManager.h>
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
#include <comdef.h> #include <Windows.h>
#include <Wbemidl.h> #include <winreg.h>
#endif //Q_OS_WIN #endif //Q_OS_WIN
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
@ -30,6 +30,9 @@
#endif //Q_OS_MAC #endif //Q_OS_MAC
static const QString FALLBACK_FINGERPRINT_KEY = "fallbackFingerprint"; static const QString FALLBACK_FINGERPRINT_KEY = "fallbackFingerprint";
QUuid FingerprintUtils::_machineFingerprint { QUuid() };
QString FingerprintUtils::getMachineFingerprintString() { QString FingerprintUtils::getMachineFingerprintString() {
QString uuidString; QString uuidString;
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
@ -47,122 +50,32 @@ QString FingerprintUtils::getMachineFingerprintString() {
#endif //Q_OS_MAC #endif //Q_OS_MAC
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
HRESULT hres; HKEY cryptoKey;
IWbemLocator *pLoc = NULL;
// initialize com. Interface already does, but other
// users of this lib don't necessarily do so.
hres = CoInitializeEx(0, COINIT_MULTITHREADED);
if (FAILED(hres)) {
qCDebug(networking) << "Failed to initialize COM library!";
return uuidString;
}
// initialize WbemLocator // try and open the key that contains the machine GUID
hres = CoCreateInstance( if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &cryptoKey) == ERROR_SUCCESS) {
CLSID_WbemLocator, DWORD type;
0, DWORD guidSize;
CLSCTX_INPROC_SERVER,
IID_IWbemLocator, (LPVOID *) &pLoc);
if (FAILED(hres)) { const char* MACHINE_GUID_KEY = "MachineGuid";
qCDebug(networking) << "Failed to initialize WbemLocator";
return uuidString;
}
// Connect to WMI through the IWbemLocator::ConnectServer method
IWbemServices *pSvc = NULL;
// Connect to the root\cimv2 namespace with // try and retrieve the size of the GUID value
// the current user and obtain pointer pSvc if (RegQueryValueEx(cryptoKey, MACHINE_GUID_KEY, NULL, &type, NULL, &guidSize) == ERROR_SUCCESS) {
// to make IWbemServices calls. // make sure that the value is a string
hres = pLoc->ConnectServer( if (type == REG_SZ) {
_bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace // retrieve the machine GUID and return that as our UUID string
NULL, // User name. NULL = current user std::string machineGUID(guidSize / sizeof(char), '\0');
NULL, // User password. NULL = current
0, // Locale. NULL indicates current
NULL, // Security flags.
0, // Authority (for example, Kerberos)
0, // Context object
&pSvc // pointer to IWbemServices proxy
);
if (FAILED(hres)) { if (RegQueryValueEx(cryptoKey, MACHINE_GUID_KEY, NULL, NULL,
pLoc->Release(); reinterpret_cast<LPBYTE>(&machineGUID[0]), &guidSize) == ERROR_SUCCESS) {
qCDebug(networking) << "Failed to connect to WMI"; uuidString = QString::fromStdString(machineGUID);
return uuidString; }
}
// Set security levels on the proxy
hres = CoSetProxyBlanket(
pSvc, // Indicates the proxy to set
RPC_C_AUTHN_WINNT, // RPC_C_AUTHN_xxx
RPC_C_AUTHZ_NONE, // RPC_C_AUTHZ_xxx
NULL, // Server principal name
RPC_C_AUTHN_LEVEL_CALL, // RPC_C_AUTHN_LEVEL_xxx
RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
NULL, // client identity
EOAC_NONE // proxy capabilities
);
if (FAILED(hres)) {
pSvc->Release();
pLoc->Release();
qCDebug(networking) << "Failed to set security on proxy blanket";
return uuidString;
}
// Use the IWbemServices pointer to grab the Win32_BIOS stuff
IEnumWbemClassObject* pEnumerator = NULL;
hres = pSvc->ExecQuery(
bstr_t("WQL"),
bstr_t("SELECT * FROM Win32_ComputerSystemProduct"),
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
NULL,
&pEnumerator);
if (FAILED(hres)) {
pSvc->Release();
pLoc->Release();
qCDebug(networking) << "query to get Win32_ComputerSystemProduct info";
return uuidString;
}
// Get the SerialNumber from the Win32_BIOS data
IWbemClassObject *pclsObj;
ULONG uReturn = 0;
SHORT sRetStatus = -100;
while (pEnumerator) {
HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
if(0 == uReturn){
break;
}
VARIANT vtProp;
// Get the value of the Name property
hr = pclsObj->Get(L"UUID", 0, &vtProp, 0, 0);
if (!FAILED(hres)) {
switch (vtProp.vt) {
case VT_BSTR:
uuidString = QString::fromWCharArray(vtProp.bstrVal);
break;
} }
} }
VariantClear(&vtProp);
pclsObj->Release(); RegCloseKey(cryptoKey);
} }
pEnumerator->Release();
// Cleanup
pSvc->Release();
pLoc->Release();
qCDebug(networking) << "Windows BIOS UUID: " << uuidString;
#endif //Q_OS_WIN #endif //Q_OS_WIN
return uuidString; return uuidString;
@ -171,29 +84,36 @@ QString FingerprintUtils::getMachineFingerprintString() {
QUuid FingerprintUtils::getMachineFingerprint() { QUuid FingerprintUtils::getMachineFingerprint() {
QString uuidString = getMachineFingerprintString(); if (_machineFingerprint.isNull()) {
QString uuidString = getMachineFingerprintString();
// now, turn into uuid. A malformed string will
// return QUuid() ("{00000...}"), which handles
// any errors in getting the string
QUuid uuid(uuidString);
// now, turn into uuid. A malformed string will
// return QUuid() ("{00000...}"), which handles
// any errors in getting the string
QUuid uuid(uuidString);
if (uuid == QUuid()) {
// if you cannot read a fallback key cuz we aren't saving them, just generate one for
// this session and move on
if (DependencyManager::get<Setting::Manager>().isNull()) {
return QUuid::createUuid();
}
// read fallback key (if any)
Settings settings;
uuid = QUuid(settings.value(FALLBACK_FINGERPRINT_KEY).toString());
qCDebug(networking) << "read fallback maching fingerprint: " << uuid.toString();
if (uuid == QUuid()) { if (uuid == QUuid()) {
// no fallback yet, set one // if you cannot read a fallback key cuz we aren't saving them, just generate one for
uuid = QUuid::createUuid(); // this session and move on
settings.setValue(FALLBACK_FINGERPRINT_KEY, uuid.toString()); if (DependencyManager::get<Setting::Manager>().isNull()) {
qCDebug(networking) << "no fallback machine fingerprint, setting it to: " << uuid.toString(); return QUuid::createUuid();
}
// read fallback key (if any)
Settings settings;
uuid = QUuid(settings.value(FALLBACK_FINGERPRINT_KEY).toString());
qCDebug(networking) << "read fallback maching fingerprint: " << uuid.toString();
if (uuid == QUuid()) {
// no fallback yet, set one
uuid = QUuid::createUuid();
settings.setValue(FALLBACK_FINGERPRINT_KEY, uuid.toString());
qCDebug(networking) << "no fallback machine fingerprint, setting it to: " << uuid.toString();
}
} }
_machineFingerprint = uuid;
} }
return uuid;
return _machineFingerprint;
} }

View file

@ -21,6 +21,7 @@ public:
private: private:
static QString getMachineFingerprintString(); static QString getMachineFingerprintString();
static QUuid _machineFingerprint;
}; };
#endif // hifi_FingerprintUtils_h #endif // hifi_FingerprintUtils_h

View file

@ -1,6 +1,6 @@
// //
// BulletUtil.h // BulletUtil.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.11.02 // Created by Andrew Meadows 2014.11.02
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

423
libraries/physics/src/CharacterController.cpp Normal file → Executable file
View file

@ -1,6 +1,6 @@
// //
// CharacterControllerInterface.cpp // CharacterControllerInterface.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2015.10.21 // Created by Andrew Meadows 2015.10.21
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.
@ -13,8 +13,8 @@
#include <NumericalConstants.h> #include <NumericalConstants.h>
#include "PhysicsCollisionGroups.h"
#include "ObjectMotionState.h" #include "ObjectMotionState.h"
#include "PhysicsHelpers.h"
#include "PhysicsLogging.h" #include "PhysicsLogging.h"
const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f); const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f);
@ -62,10 +62,6 @@ CharacterController::CharacterMotor::CharacterMotor(const glm::vec3& vel, const
} }
CharacterController::CharacterController() { CharacterController::CharacterController() {
_halfHeight = 1.0f;
_enabled = false;
_floorDistance = MAX_FALL_HEIGHT; _floorDistance = MAX_FALL_HEIGHT;
_targetVelocity.setValue(0.0f, 0.0f, 0.0f); _targetVelocity.setValue(0.0f, 0.0f, 0.0f);
@ -107,6 +103,7 @@ bool CharacterController::needsAddition() const {
void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
if (_dynamicsWorld != world) { if (_dynamicsWorld != world) {
// remove from old world
if (_dynamicsWorld) { if (_dynamicsWorld) {
if (_rigidBody) { if (_rigidBody) {
_dynamicsWorld->removeRigidBody(_rigidBody); _dynamicsWorld->removeRigidBody(_rigidBody);
@ -114,17 +111,23 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
} }
_dynamicsWorld = nullptr; _dynamicsWorld = nullptr;
} }
int16_t collisionGroup = computeCollisionGroup();
if (world && _rigidBody) { if (world && _rigidBody) {
// add to new world
_dynamicsWorld = world; _dynamicsWorld = world;
_pendingFlags &= ~PENDING_FLAG_JUMP; _pendingFlags &= ~PENDING_FLAG_JUMP;
// Before adding the RigidBody to the world we must save its oldGravity to the side _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
// because adding an object to the world will overwrite it with the default gravity.
btVector3 oldGravity = _rigidBody->getGravity();
_dynamicsWorld->addRigidBody(_rigidBody, BULLET_COLLISION_GROUP_MY_AVATAR, BULLET_COLLISION_MASK_MY_AVATAR);
_dynamicsWorld->addAction(this); _dynamicsWorld->addAction(this);
// restore gravity settings // restore gravity settings because adding an object to the world overwrites its gravity setting
_rigidBody->setGravity(oldGravity); _rigidBody->setGravity(_gravity * _currentUp);
btCollisionShape* shape = _rigidBody->getCollisionShape();
assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE);
_ghost.setCharacterShape(static_cast<btConvexHullShape*>(shape));
} }
_ghost.setCollisionGroupAndMask(collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR & (~ collisionGroup));
_ghost.setCollisionWorld(_dynamicsWorld);
_ghost.setRadiusAndHalfHeight(_radius, _halfHeight);
_ghost.setWorldTransform(_rigidBody->getWorldTransform());
} }
if (_dynamicsWorld) { if (_dynamicsWorld) {
if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) { if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) {
@ -138,38 +141,78 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
} }
} }
static const float COS_PI_OVER_THREE = cosf(PI / 3.0f); bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) {
bool pushing = _targetVelocity.length2() > FLT_EPSILON;
btDispatcher* dispatcher = collisionWorld->getDispatcher();
int numManifolds = dispatcher->getNumManifolds();
bool hasFloor = false;
btTransform rotation = _rigidBody->getWorldTransform();
rotation.setOrigin(btVector3(0.0f, 0.0f, 0.0f)); // clear translation part
bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) const {
int numManifolds = collisionWorld->getDispatcher()->getNumManifolds();
for (int i = 0; i < numManifolds; i++) { for (int i = 0; i < numManifolds; i++) {
btPersistentManifold* contactManifold = collisionWorld->getDispatcher()->getManifoldByIndexInternal(i); btPersistentManifold* contactManifold = dispatcher->getManifoldByIndexInternal(i);
const btCollisionObject* obA = static_cast<const btCollisionObject*>(contactManifold->getBody0()); if (_rigidBody == contactManifold->getBody1() || _rigidBody == contactManifold->getBody0()) {
const btCollisionObject* obB = static_cast<const btCollisionObject*>(contactManifold->getBody1()); bool characterIsFirst = _rigidBody == contactManifold->getBody0();
if (obA == _rigidBody || obB == _rigidBody) {
int numContacts = contactManifold->getNumContacts(); int numContacts = contactManifold->getNumContacts();
int stepContactIndex = -1;
float highestStep = _minStepHeight;
for (int j = 0; j < numContacts; j++) { for (int j = 0; j < numContacts; j++) {
btManifoldPoint& pt = contactManifold->getContactPoint(j); // check for "floor"
btManifoldPoint& contact = contactManifold->getContactPoint(j);
// check to see if contact point is touching the bottom sphere of the capsule. btVector3 pointOnCharacter = characterIsFirst ? contact.m_localPointA : contact.m_localPointB; // object-local-frame
// and the contact normal is not slanted too much. btVector3 normal = characterIsFirst ? contact.m_normalWorldOnB : -contact.m_normalWorldOnB; // points toward character
float contactPointY = (obA == _rigidBody) ? pt.m_localPointA.getY() : pt.m_localPointB.getY(); btScalar hitHeight = _halfHeight + _radius + pointOnCharacter.dot(_currentUp);
btVector3 normal = (obA == _rigidBody) ? pt.m_normalWorldOnB : -pt.m_normalWorldOnB; if (hitHeight < _maxStepHeight && normal.dot(_currentUp) > _minFloorNormalDotUp) {
if (contactPointY < -_halfHeight && normal.dot(_currentUp) > COS_PI_OVER_THREE) { hasFloor = true;
return true; if (!pushing) {
// we're not pushing against anything so we can early exit
// (all we need to know is that there is a floor)
break;
}
} }
if (pushing && _targetVelocity.dot(normal) < 0.0f) {
// remember highest step obstacle
if (!_stepUpEnabled || hitHeight > _maxStepHeight) {
// this manifold is invalidated by point that is too high
stepContactIndex = -1;
break;
} else if (hitHeight > highestStep && normal.dot(_targetVelocity) < 0.0f ) {
highestStep = hitHeight;
stepContactIndex = j;
hasFloor = true;
}
}
}
if (stepContactIndex > -1 && highestStep > _stepHeight) {
// remember step info for later
btManifoldPoint& contact = contactManifold->getContactPoint(stepContactIndex);
btVector3 pointOnCharacter = characterIsFirst ? contact.m_localPointA : contact.m_localPointB; // object-local-frame
_stepNormal = characterIsFirst ? contact.m_normalWorldOnB : -contact.m_normalWorldOnB; // points toward character
_stepHeight = highestStep;
_stepPoint = rotation * pointOnCharacter; // rotate into world-frame
}
if (hasFloor && !(pushing && _stepUpEnabled)) {
// early exit since all we need to know is that we're on a floor
break;
} }
} }
} }
return false; return hasFloor;
}
void CharacterController::updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) {
preStep(collisionWorld);
playerStep(collisionWorld, deltaTime);
} }
void CharacterController::preStep(btCollisionWorld* collisionWorld) { void CharacterController::preStep(btCollisionWorld* collisionWorld) {
// trace a ray straight down to see if we're standing on the ground // trace a ray straight down to see if we're standing on the ground
const btTransform& xform = _rigidBody->getWorldTransform(); const btTransform& transform = _rigidBody->getWorldTransform();
// rayStart is at center of bottom sphere // rayStart is at center of bottom sphere
btVector3 rayStart = xform.getOrigin() - _halfHeight * _currentUp; btVector3 rayStart = transform.getOrigin() - _halfHeight * _currentUp;
// rayEnd is some short distance outside bottom sphere // rayEnd is some short distance outside bottom sphere
const btScalar FLOOR_PROXIMITY_THRESHOLD = 0.3f * _radius; const btScalar FLOOR_PROXIMITY_THRESHOLD = 0.3f * _radius;
@ -183,21 +226,16 @@ void CharacterController::preStep(btCollisionWorld* collisionWorld) {
if (rayCallback.hasHit()) { if (rayCallback.hasHit()) {
_floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius; _floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius;
} }
_hasSupport = checkForSupport(collisionWorld);
} }
const btScalar MIN_TARGET_SPEED = 0.001f; const btScalar MIN_TARGET_SPEED = 0.001f;
const btScalar MIN_TARGET_SPEED_SQUARED = MIN_TARGET_SPEED * MIN_TARGET_SPEED; const btScalar MIN_TARGET_SPEED_SQUARED = MIN_TARGET_SPEED * MIN_TARGET_SPEED;
void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) { void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar dt) {
_stepHeight = _minStepHeight; // clears memory of last step obstacle
_hasSupport = checkForSupport(collisionWorld);
btVector3 velocity = _rigidBody->getLinearVelocity() - _parentVelocity; btVector3 velocity = _rigidBody->getLinearVelocity() - _parentVelocity;
computeNewVelocity(dt, velocity); computeNewVelocity(dt, velocity);
_rigidBody->setLinearVelocity(velocity + _parentVelocity);
// Dynamicaly compute a follow velocity to move this body toward the _followDesiredBodyTransform.
// Rather than add this velocity to velocity the RigidBody, we explicitly teleport the RigidBody towards its goal.
// This mirrors the computation done in MyAvatar::FollowHelper::postPhysicsUpdate().
const float MINIMUM_TIME_REMAINING = 0.005f; const float MINIMUM_TIME_REMAINING = 0.005f;
const float MAX_DISPLACEMENT = 0.5f * _radius; const float MAX_DISPLACEMENT = 0.5f * _radius;
@ -231,6 +269,47 @@ void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) {
_rigidBody->setWorldTransform(btTransform(endRot, endPos)); _rigidBody->setWorldTransform(btTransform(endRot, endPos));
} }
_followTime += dt; _followTime += dt;
if (_steppingUp) {
float horizontalTargetSpeed = (_targetVelocity - _targetVelocity.dot(_currentUp) * _currentUp).length();
if (horizontalTargetSpeed > FLT_EPSILON) {
// compute a stepUpSpeed that will reach the top of the step in the time it would take
// to move over the _stepPoint at target speed
float horizontalDistance = (_stepPoint - _stepPoint.dot(_currentUp) * _currentUp).length();
float timeToStep = horizontalDistance / horizontalTargetSpeed;
float stepUpSpeed = _stepHeight / timeToStep;
// magically clamp stepUpSpeed to a fraction of horizontalTargetSpeed
// to prevent the avatar from moving unreasonably fast according to human eye
const float MAX_STEP_UP_SPEED = 0.65f * horizontalTargetSpeed;
if (stepUpSpeed > MAX_STEP_UP_SPEED) {
stepUpSpeed = MAX_STEP_UP_SPEED;
}
// add minimum velocity to counteract gravity's displacement during one step
// Note: the 0.5 factor comes from the fact that we really want the
// average velocity contribution from gravity during the step
stepUpSpeed -= 0.5f * _gravity * timeToStep; // remember: _gravity is negative scalar
btScalar vDotUp = velocity.dot(_currentUp);
if (vDotUp < stepUpSpeed) {
// character doesn't have enough upward velocity to cover the step so we help using a "sky hook"
// which uses micro-teleports rather than applying real velocity
// to prevent the avatar from popping up after the step is done
btTransform transform = _rigidBody->getWorldTransform();
transform.setOrigin(transform.getOrigin() + (dt * stepUpSpeed) * _currentUp);
_rigidBody->setWorldTransform(transform);
}
// don't allow the avatar to fall downward when stepping up
// since otherwise this would tend to defeat the step-up behavior
if (vDotUp < 0.0f) {
velocity -= vDotUp * _currentUp;
}
}
}
_rigidBody->setLinearVelocity(velocity + _parentVelocity);
_ghost.setWorldTransform(_rigidBody->getWorldTransform());
} }
void CharacterController::jump() { void CharacterController::jump() {
@ -272,95 +351,100 @@ void CharacterController::setState(State desiredState) {
#ifdef DEBUG_STATE_CHANGE #ifdef DEBUG_STATE_CHANGE
qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason; qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason;
#endif #endif
if (desiredState == State::Hover && _state != State::Hover) {
// hover enter
if (_rigidBody) {
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
}
} else if (_state == State::Hover && desiredState != State::Hover) {
// hover exit
if (_rigidBody) {
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
}
}
_state = desiredState; _state = desiredState;
updateGravity();
} }
} }
void CharacterController::setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale) { void CharacterController::updateGravity() {
_boxScale = scale; int16_t collisionGroup = computeCollisionGroup();
if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) {
_gravity = 0.0f;
} else {
const float DEFAULT_CHARACTER_GRAVITY = -5.0f;
_gravity = DEFAULT_CHARACTER_GRAVITY;
}
if (_rigidBody) {
_rigidBody->setGravity(_gravity * _currentUp);
}
}
float x = _boxScale.x; void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale) {
float z = _boxScale.z; float x = scale.x;
float z = scale.z;
float radius = 0.5f * sqrtf(0.5f * (x * x + z * z)); float radius = 0.5f * sqrtf(0.5f * (x * x + z * z));
float halfHeight = 0.5f * _boxScale.y - radius; float halfHeight = 0.5f * scale.y - radius;
float MIN_HALF_HEIGHT = 0.1f; float MIN_HALF_HEIGHT = 0.1f;
if (halfHeight < MIN_HALF_HEIGHT) { if (halfHeight < MIN_HALF_HEIGHT) {
halfHeight = MIN_HALF_HEIGHT; halfHeight = MIN_HALF_HEIGHT;
} }
// compare dimensions // compare dimensions
float radiusDelta = glm::abs(radius - _radius); if (glm::abs(radius - _radius) > FLT_EPSILON || glm::abs(halfHeight - _halfHeight) > FLT_EPSILON) {
float heightDelta = glm::abs(halfHeight - _halfHeight); _radius = radius;
if (radiusDelta < FLT_EPSILON && heightDelta < FLT_EPSILON) { _halfHeight = halfHeight;
// shape hasn't changed --> nothing to do const btScalar DEFAULT_MIN_STEP_HEIGHT_FACTOR = 0.005f;
} else { const btScalar DEFAULT_MAX_STEP_HEIGHT_FACTOR = 0.65f;
_minStepHeight = DEFAULT_MIN_STEP_HEIGHT_FACTOR * (_halfHeight + _radius);
_maxStepHeight = DEFAULT_MAX_STEP_HEIGHT_FACTOR * (_halfHeight + _radius);
if (_dynamicsWorld) { if (_dynamicsWorld) {
// must REMOVE from world prior to shape update // must REMOVE from world prior to shape update
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION; _pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION;
} }
_pendingFlags |= PENDING_FLAG_UPDATE_SHAPE; _pendingFlags |= PENDING_FLAG_UPDATE_SHAPE;
// only need to ADD back when we happen to be enabled _pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION;
if (_enabled) {
_pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION;
}
} }
// it's ok to change offset immediately -- there are no thread safety issues here // it's ok to change offset immediately -- there are no thread safety issues here
_shapeLocalOffset = corner + 0.5f * _boxScale; _shapeLocalOffset = minCorner + 0.5f * scale;
} }
void CharacterController::setEnabled(bool enabled) { void CharacterController::setCollisionless(bool collisionless) {
if (enabled != _enabled) { if (collisionless != _collisionless) {
if (enabled) { _collisionless = collisionless;
// Don't bother clearing REMOVE bit since it might be paired with an UPDATE_SHAPE bit. _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP;
// Setting the ADD bit here works for all cases so we don't even bother checking other bits. }
_pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION; }
} else {
if (_dynamicsWorld) { int16_t CharacterController::computeCollisionGroup() const {
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION; if (_collisionless) {
} return _collisionlessAllowed ? BULLET_COLLISION_GROUP_COLLISIONLESS : BULLET_COLLISION_GROUP_MY_AVATAR;
_pendingFlags &= ~ PENDING_FLAG_ADD_TO_SIMULATION; } else {
return BULLET_COLLISION_GROUP_MY_AVATAR;
}
}
void CharacterController::handleChangedCollisionGroup() {
if (_pendingFlags & PENDING_FLAG_UPDATE_COLLISION_GROUP) {
// ATM the easiest way to update collision groups is to remove/re-add the RigidBody
if (_dynamicsWorld) {
_dynamicsWorld->removeRigidBody(_rigidBody);
int16_t collisionGroup = computeCollisionGroup();
_dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
} }
SET_STATE(State::Hover, "setEnabled"); _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP;
_enabled = enabled; updateGravity();
} }
} }
void CharacterController::updateUpAxis(const glm::quat& rotation) { void CharacterController::updateUpAxis(const glm::quat& rotation) {
btVector3 oldUp = _currentUp;
_currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS); _currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS);
if (_state != State::Hover) { if (_state != State::Hover && _rigidBody) {
const btScalar MIN_UP_ERROR = 0.01f; _rigidBody->setGravity(_gravity * _currentUp);
if (oldUp.distance(_currentUp) > MIN_UP_ERROR) {
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
}
} }
} }
void CharacterController::setPositionAndOrientation( void CharacterController::setPositionAndOrientation(
const glm::vec3& position, const glm::vec3& position,
const glm::quat& orientation) { const glm::quat& orientation) {
// TODO: update gravity if up has changed
updateUpAxis(orientation); updateUpAxis(orientation);
_rotation = glmToBullet(orientation);
btQuaternion bodyOrientation = glmToBullet(orientation); _position = glmToBullet(position + orientation * _shapeLocalOffset);
btVector3 bodyPosition = glmToBullet(position + orientation * _shapeLocalOffset);
_characterBodyTransform = btTransform(bodyOrientation, bodyPosition);
} }
void CharacterController::getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const { void CharacterController::getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const {
if (_enabled && _rigidBody) { if (_rigidBody) {
const btTransform& avatarTransform = _rigidBody->getWorldTransform(); const btTransform& avatarTransform = _rigidBody->getWorldTransform();
rotation = bulletToGLM(avatarTransform.getRotation()); rotation = bulletToGLM(avatarTransform.getRotation());
position = bulletToGLM(avatarTransform.getOrigin()) - rotation * _shapeLocalOffset; position = bulletToGLM(avatarTransform.getOrigin()) - rotation * _shapeLocalOffset;
@ -428,16 +512,19 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
btScalar angle = motor.rotation.getAngle(); btScalar angle = motor.rotation.getAngle();
btVector3 velocity = worldVelocity.rotate(axis, -angle); btVector3 velocity = worldVelocity.rotate(axis, -angle);
if (_state == State::Hover || motor.hTimescale == motor.vTimescale) { int16_t collisionGroup = computeCollisionGroup();
if (collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ||
_state == State::Hover || motor.hTimescale == motor.vTimescale) {
// modify velocity // modify velocity
btScalar tau = dt / motor.hTimescale; btScalar tau = dt / motor.hTimescale;
if (tau > 1.0f) { if (tau > 1.0f) {
tau = 1.0f; tau = 1.0f;
} }
velocity += (motor.velocity - velocity) * tau; velocity += tau * (motor.velocity - velocity);
// rotate back into world-frame // rotate back into world-frame
velocity = velocity.rotate(axis, angle); velocity = velocity.rotate(axis, angle);
_targetVelocity += (tau * motor.velocity).rotate(axis, angle);
// store the velocity and weight // store the velocity and weight
velocities.push_back(velocity); velocities.push_back(velocity);
@ -445,12 +532,26 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
} else { } else {
// compute local UP // compute local UP
btVector3 up = _currentUp.rotate(axis, -angle); btVector3 up = _currentUp.rotate(axis, -angle);
btVector3 motorVelocity = motor.velocity;
// save these non-adjusted components for later
btVector3 vTargetVelocity = motorVelocity.dot(up) * up;
btVector3 hTargetVelocity = motorVelocity - vTargetVelocity;
if (_stepHeight > _minStepHeight && !_steppingUp) {
// there is a step --> compute velocity direction to go over step
btVector3 motorVelocityWF = motorVelocity.rotate(axis, angle);
if (motorVelocityWF.dot(_stepNormal) < 0.0f) {
// the motor pushes against step
_steppingUp = true;
}
}
// split velocity into horizontal and vertical components // split velocity into horizontal and vertical components
btVector3 vVelocity = velocity.dot(up) * up; btVector3 vVelocity = velocity.dot(up) * up;
btVector3 hVelocity = velocity - vVelocity; btVector3 hVelocity = velocity - vVelocity;
btVector3 vTargetVelocity = motor.velocity.dot(up) * up; btVector3 vMotorVelocity = motorVelocity.dot(up) * up;
btVector3 hTargetVelocity = motor.velocity - vTargetVelocity; btVector3 hMotorVelocity = motorVelocity - vMotorVelocity;
// modify each component separately // modify each component separately
btScalar maxTau = 0.0f; btScalar maxTau = 0.0f;
@ -460,7 +561,7 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
tau = 1.0f; tau = 1.0f;
} }
maxTau = tau; maxTau = tau;
hVelocity += (hTargetVelocity - hVelocity) * tau; hVelocity += (hMotorVelocity - hVelocity) * tau;
} }
if (motor.vTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) { if (motor.vTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) {
btScalar tau = dt / motor.vTimescale; btScalar tau = dt / motor.vTimescale;
@ -470,11 +571,12 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
if (tau > maxTau) { if (tau > maxTau) {
maxTau = tau; maxTau = tau;
} }
vVelocity += (vTargetVelocity - vVelocity) * tau; vVelocity += (vMotorVelocity - vVelocity) * tau;
} }
// add components back together and rotate into world-frame // add components back together and rotate into world-frame
velocity = (hVelocity + vVelocity).rotate(axis, angle); velocity = (hVelocity + vVelocity).rotate(axis, angle);
_targetVelocity += maxTau * (hTargetVelocity + vTargetVelocity).rotate(axis, angle);
// store velocity and weights // store velocity and weights
velocities.push_back(velocity); velocities.push_back(velocity);
@ -492,6 +594,8 @@ void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) {
velocities.reserve(_motors.size()); velocities.reserve(_motors.size());
std::vector<btScalar> weights; std::vector<btScalar> weights;
weights.reserve(_motors.size()); weights.reserve(_motors.size());
_targetVelocity = btVector3(0.0f, 0.0f, 0.0f);
_steppingUp = false;
for (int i = 0; i < (int)_motors.size(); ++i) { for (int i = 0; i < (int)_motors.size(); ++i) {
applyMotor(i, dt, velocity, velocities, weights); applyMotor(i, dt, velocity, velocities, weights);
} }
@ -507,14 +611,18 @@ void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) {
for (size_t i = 0; i < velocities.size(); ++i) { for (size_t i = 0; i < velocities.size(); ++i) {
velocity += (weights[i] / totalWeight) * velocities[i]; velocity += (weights[i] / totalWeight) * velocities[i];
} }
_targetVelocity /= totalWeight;
} }
if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) { if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) {
velocity = btVector3(0.0f, 0.0f, 0.0f); velocity = btVector3(0.0f, 0.0f, 0.0f);
} }
// 'thrust' is applied at the very end // 'thrust' is applied at the very end
_targetVelocity += dt * _linearAcceleration;
velocity += dt * _linearAcceleration; velocity += dt * _linearAcceleration;
_targetVelocity = velocity; // Note the differences between these two variables:
// _targetVelocity = ideal final velocity according to input
// velocity = real final velocity after motors are applied to current velocity
} }
void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) { void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) {
@ -523,57 +631,60 @@ void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) {
velocity = bulletToGLM(btVelocity); velocity = bulletToGLM(btVelocity);
} }
void CharacterController::preSimulation() { void CharacterController::updateState() {
if (_enabled && _dynamicsWorld && _rigidBody) { if (!_dynamicsWorld) {
quint64 now = usecTimestampNow(); return;
}
const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius;
const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight;
const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND;
const btScalar MIN_HOVER_HEIGHT = 2.5f;
const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND;
// slam body to where it is supposed to be // scan for distant floor
_rigidBody->setWorldTransform(_characterBodyTransform); // rayStart is at center of bottom sphere
btVector3 velocity = _rigidBody->getLinearVelocity(); btVector3 rayStart = _position;
_preSimulationVelocity = velocity;
// scan for distant floor btScalar rayLength = _radius;
// rayStart is at center of bottom sphere int16_t collisionGroup = computeCollisionGroup();
btVector3 rayStart = _characterBodyTransform.getOrigin(); if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) {
rayLength += MAX_FALL_HEIGHT;
} else {
rayLength += MIN_HOVER_HEIGHT;
}
btVector3 rayEnd = rayStart - rayLength * _currentUp;
// rayEnd is straight down MAX_FALL_HEIGHT ClosestNotMe rayCallback(_rigidBody);
btScalar rayLength = _radius + MAX_FALL_HEIGHT; rayCallback.m_closestHitFraction = 1.0f;
btVector3 rayEnd = rayStart - rayLength * _currentUp; _dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback);
bool rayHasHit = rayCallback.hasHit();
const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius; quint64 now = usecTimestampNow();
const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight; if (rayHasHit) {
const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND; _rayHitStartTime = now;
const btScalar MIN_HOVER_HEIGHT = 2.5f; _floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight);
const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND; } else {
const btScalar MAX_WALKING_SPEED = 2.5f;
const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND; const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND;
if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) {
ClosestNotMe rayCallback(_rigidBody);
rayCallback.m_closestHitFraction = 1.0f;
_dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback);
bool rayHasHit = rayCallback.hasHit();
if (rayHasHit) {
_rayHitStartTime = now;
_floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight);
} else if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) {
rayHasHit = true; rayHasHit = true;
} else { } else {
_floorDistance = FLT_MAX; _floorDistance = FLT_MAX;
} }
}
// record a time stamp when the jump button was first pressed. // record a time stamp when the jump button was first pressed.
if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) { bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP;
if (_pendingFlags & PENDING_FLAG_JUMP) { if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) {
_jumpButtonDownStartTime = now; if (_pendingFlags & PENDING_FLAG_JUMP) {
_jumpButtonDownCount++; _jumpButtonDownStartTime = now;
} _jumpButtonDownCount++;
} }
}
bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP; btVector3 velocity = _preSimulationVelocity;
btVector3 actualHorizVelocity = velocity - velocity.dot(_currentUp) * _currentUp;
bool flyingFast = _state == State::Hover && actualHorizVelocity.length() > (MAX_WALKING_SPEED * 0.75f);
// disable normal state transitions while collisionless
const btScalar MAX_WALKING_SPEED = 2.65f;
if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) {
switch (_state) { switch (_state) {
case State::Ground: case State::Ground:
if (!rayHasHit && !_hasSupport) { if (!rayHasHit && !_hasSupport) {
@ -613,6 +724,9 @@ void CharacterController::preSimulation() {
break; break;
} }
case State::Hover: case State::Hover:
btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length();
bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f);
if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
SET_STATE(State::InAir, "near ground"); SET_STATE(State::InAir, "near ground");
} else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) { } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
@ -620,6 +734,28 @@ void CharacterController::preSimulation() {
} }
break; break;
} }
} else {
// when collisionless: only switch between State::Ground and State::Hover
// and bypass state debugging
if (rayHasHit) {
if (velocity.length() > (MAX_WALKING_SPEED)) {
_state = State::Hover;
} else {
_state = State::Ground;
}
} else {
_state = State::Hover;
}
}
}
void CharacterController::preSimulation() {
if (_rigidBody) {
// slam body transform and remember velocity
_rigidBody->setWorldTransform(btTransform(btTransform(_rotation, _position)));
_preSimulationVelocity = _rigidBody->getLinearVelocity();
updateState();
} }
_previousFlags = _pendingFlags; _previousFlags = _pendingFlags;
@ -631,14 +767,11 @@ void CharacterController::preSimulation() {
} }
void CharacterController::postSimulation() { void CharacterController::postSimulation() {
// postSimulation() exists for symmetry and just in case we need to do something here later if (_rigidBody) {
if (_enabled && _dynamicsWorld && _rigidBody) { _velocityChange = _rigidBody->getLinearVelocity() - _preSimulationVelocity;
btVector3 velocity = _rigidBody->getLinearVelocity();
_velocityChange = velocity - _preSimulationVelocity;
} }
} }
bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) { bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) {
if (!_rigidBody) { if (!_rigidBody) {
return false; return false;
@ -651,11 +784,17 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio
} }
void CharacterController::setFlyingAllowed(bool value) { void CharacterController::setFlyingAllowed(bool value) {
if (_flyingAllowed != value) { if (value != _flyingAllowed) {
_flyingAllowed = value; _flyingAllowed = value;
if (!_flyingAllowed && _state == State::Hover) { if (!_flyingAllowed && _state == State::Hover) {
SET_STATE(State::InAir, "flying not allowed"); SET_STATE(State::InAir, "flying not allowed");
} }
} }
} }
void CharacterController::setCollisionlessAllowed(bool value) {
if (value != _collisionlessAllowed) {
_collisionlessAllowed = value;
_pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP;
}
}

View file

@ -1,6 +1,6 @@
// //
// CharacterControllerInterface.h // CharacterControllerInterface.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2015.10.21 // Created by Andrew Meadows 2015.10.21
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.
@ -9,8 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// //
#ifndef hifi_CharacterControllerInterface_h #ifndef hifi_CharacterController_h
#define hifi_CharacterControllerInterface_h #define hifi_CharacterController_h
#include <assert.h> #include <assert.h>
#include <stdint.h> #include <stdint.h>
@ -19,14 +19,18 @@
#include <BulletDynamics/Character/btCharacterControllerInterface.h> #include <BulletDynamics/Character/btCharacterControllerInterface.h>
#include <GLMHelpers.h> #include <GLMHelpers.h>
#include <NumericalConstants.h>
#include <PhysicsCollisionGroups.h>
#include "BulletUtil.h" #include "BulletUtil.h"
#include "CharacterGhostObject.h"
const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0; const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0;
const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1; const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1;
const uint32_t PENDING_FLAG_UPDATE_SHAPE = 1U << 2; const uint32_t PENDING_FLAG_UPDATE_SHAPE = 1U << 2;
const uint32_t PENDING_FLAG_JUMP = 1U << 3; const uint32_t PENDING_FLAG_JUMP = 1U << 3;
const uint32_t PENDING_FLAG_UPDATE_COLLISION_GROUP = 1U << 4;
const float DEFAULT_CHARACTER_GRAVITY = -5.0f; const float DEFAULT_MIN_FLOOR_NORMAL_DOT_UP = cosf(PI / 3.0f);
class btRigidBody; class btRigidBody;
class btCollisionWorld; class btCollisionWorld;
@ -44,7 +48,7 @@ public:
bool needsRemoval() const; bool needsRemoval() const;
bool needsAddition() const; bool needsAddition() const;
void setDynamicsWorld(btDynamicsWorld* world); virtual void setDynamicsWorld(btDynamicsWorld* world);
btCollisionObject* getCollisionObject() { return _rigidBody; } btCollisionObject* getCollisionObject() { return _rigidBody; }
virtual void updateShapeIfNecessary() = 0; virtual void updateShapeIfNecessary() = 0;
@ -56,10 +60,7 @@ public:
virtual void warp(const btVector3& origin) override { } virtual void warp(const btVector3& origin) override { }
virtual void debugDraw(btIDebugDraw* debugDrawer) override { } virtual void debugDraw(btIDebugDraw* debugDrawer) override { }
virtual void setUpInterpolate(bool value) override { } virtual void setUpInterpolate(bool value) override { }
virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override { virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override;
preStep(collisionWorld);
playerStep(collisionWorld, deltaTime);
}
virtual void preStep(btCollisionWorld *collisionWorld) override; virtual void preStep(btCollisionWorld *collisionWorld) override;
virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override; virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override;
virtual bool canJump() const override { assert(false); return false; } // never call this virtual bool canJump() const override { assert(false); return false; } // never call this
@ -69,6 +70,7 @@ public:
void clearMotors(); void clearMotors();
void addMotor(const glm::vec3& velocity, const glm::quat& rotation, float horizTimescale, float vertTimescale = -1.0f); void addMotor(const glm::vec3& velocity, const glm::quat& rotation, float horizTimescale, float vertTimescale = -1.0f);
void applyMotor(int index, btScalar dt, btVector3& worldVelocity, std::vector<btVector3>& velocities, std::vector<btScalar>& weights); void applyMotor(int index, btScalar dt, btVector3& worldVelocity, std::vector<btVector3>& velocities, std::vector<btScalar>& weights);
void setStepUpEnabled(bool enabled) { _stepUpEnabled = enabled; }
void computeNewVelocity(btScalar dt, btVector3& velocity); void computeNewVelocity(btScalar dt, btVector3& velocity);
void computeNewVelocity(btScalar dt, glm::vec3& velocity); void computeNewVelocity(btScalar dt, glm::vec3& velocity);
@ -103,16 +105,20 @@ public:
}; };
State getState() const { return _state; } State getState() const { return _state; }
void updateState();
void setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale); void setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale);
bool isEnabled() const { return _enabled; } // thread-safe bool isEnabledAndReady() const { return _dynamicsWorld; }
void setEnabled(bool enabled);
bool isEnabledAndReady() const { return _enabled && _dynamicsWorld; } void setCollisionless(bool collisionless);
int16_t computeCollisionGroup() const;
void handleChangedCollisionGroup();
bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation);
void setFlyingAllowed(bool value); void setFlyingAllowed(bool value);
void setCollisionlessAllowed(bool value);
protected: protected:
@ -122,8 +128,9 @@ protected:
void setState(State state); void setState(State state);
#endif #endif
void updateGravity();
void updateUpAxis(const glm::quat& rotation); void updateUpAxis(const glm::quat& rotation);
bool checkForSupport(btCollisionWorld* collisionWorld) const; bool checkForSupport(btCollisionWorld* collisionWorld);
protected: protected:
struct CharacterMotor { struct CharacterMotor {
@ -136,6 +143,7 @@ protected:
}; };
std::vector<CharacterMotor> _motors; std::vector<CharacterMotor> _motors;
CharacterGhostObject _ghost;
btVector3 _currentUp; btVector3 _currentUp;
btVector3 _targetVelocity; btVector3 _targetVelocity;
btVector3 _parentVelocity; btVector3 _parentVelocity;
@ -144,6 +152,8 @@ protected:
btTransform _followDesiredBodyTransform; btTransform _followDesiredBodyTransform;
btScalar _followTimeRemaining; btScalar _followTimeRemaining;
btTransform _characterBodyTransform; btTransform _characterBodyTransform;
btVector3 _position;
btQuaternion _rotation;
glm::vec3 _shapeLocalOffset; glm::vec3 _shapeLocalOffset;
@ -155,13 +165,23 @@ protected:
quint32 _jumpButtonDownCount; quint32 _jumpButtonDownCount;
quint32 _takeoffJumpButtonID; quint32 _takeoffJumpButtonID;
btScalar _halfHeight; // data for walking up steps
btScalar _radius; btVector3 _stepPoint { 0.0f, 0.0f, 0.0f };
btVector3 _stepNormal { 0.0f, 0.0f, 0.0f };
bool _steppingUp { false };
btScalar _stepHeight { 0.0f };
btScalar _minStepHeight { 0.0f };
btScalar _maxStepHeight { 0.0f };
btScalar _minFloorNormalDotUp { DEFAULT_MIN_FLOOR_NORMAL_DOT_UP };
btScalar _halfHeight { 0.0f };
btScalar _radius { 0.0f };
btScalar _floorDistance; btScalar _floorDistance;
bool _stepUpEnabled { true };
bool _hasSupport; bool _hasSupport;
btScalar _gravity; btScalar _gravity { 0.0f };
btScalar _jumpSpeed; btScalar _jumpSpeed;
btScalar _followTime; btScalar _followTime;
@ -169,7 +189,6 @@ protected:
btQuaternion _followAngularDisplacement; btQuaternion _followAngularDisplacement;
btVector3 _linearAcceleration; btVector3 _linearAcceleration;
std::atomic_bool _enabled;
State _state; State _state;
bool _isPushingUp; bool _isPushingUp;
@ -179,6 +198,8 @@ protected:
uint32_t _previousFlags { 0 }; uint32_t _previousFlags { 0 };
bool _flyingAllowed { true }; bool _flyingAllowed { true };
bool _collisionlessAllowed { true };
bool _collisionless { false };
}; };
#endif // hifi_CharacterControllerInterface_h #endif // hifi_CharacterController_h

View file

@ -0,0 +1,99 @@
//
// CharacterGhostObject.cpp
// libraries/physics/src
//
// Created by Andrew Meadows 2016.08.26
// Copyright 2016 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 "CharacterGhostObject.h"
#include <stdint.h>
#include <assert.h>
#include <PhysicsHelpers.h>
#include "CharacterRayResult.h"
#include "CharacterGhostShape.h"
CharacterGhostObject::~CharacterGhostObject() {
removeFromWorld();
if (_ghostShape) {
delete _ghostShape;
_ghostShape = nullptr;
setCollisionShape(nullptr);
}
}
void CharacterGhostObject::setCollisionGroupAndMask(int16_t group, int16_t mask) {
_collisionFilterGroup = group;
_collisionFilterMask = mask;
// TODO: if this probe is in the world reset ghostObject overlap cache
}
void CharacterGhostObject::getCollisionGroupAndMask(int16_t& group, int16_t& mask) const {
group = _collisionFilterGroup;
mask = _collisionFilterMask;
}
void CharacterGhostObject::setRadiusAndHalfHeight(btScalar radius, btScalar halfHeight) {
_radius = radius;
_halfHeight = halfHeight;
}
// override of btCollisionObject::setCollisionShape()
void CharacterGhostObject::setCharacterShape(btConvexHullShape* shape) {
assert(shape);
// we create our own shape with an expanded Aabb for more reliable sweep tests
if (_ghostShape) {
delete _ghostShape;
}
_ghostShape = new CharacterGhostShape(static_cast<const btConvexHullShape*>(shape));
setCollisionShape(_ghostShape);
}
void CharacterGhostObject::setCollisionWorld(btCollisionWorld* world) {
if (world != _world) {
removeFromWorld();
_world = world;
addToWorld();
}
}
bool CharacterGhostObject::rayTest(const btVector3& start,
const btVector3& end,
CharacterRayResult& result) const {
if (_world && _inWorld) {
_world->rayTest(start, end, result);
}
return result.hasHit();
}
void CharacterGhostObject::refreshOverlappingPairCache() {
assert(_world && _inWorld);
btVector3 minAabb, maxAabb;
getCollisionShape()->getAabb(getWorldTransform(), minAabb, maxAabb);
// this updates both pairCaches: world broadphase and ghostobject
_world->getBroadphase()->setAabb(getBroadphaseHandle(), minAabb, maxAabb, _world->getDispatcher());
}
void CharacterGhostObject::removeFromWorld() {
if (_world && _inWorld) {
_world->removeCollisionObject(this);
_inWorld = false;
}
}
void CharacterGhostObject::addToWorld() {
if (_world && !_inWorld) {
assert(getCollisionShape());
setCollisionFlags(getCollisionFlags() | btCollisionObject::CF_NO_CONTACT_RESPONSE);
_world->addCollisionObject(this, _collisionFilterGroup, _collisionFilterMask);
_inWorld = true;
}
}

View file

@ -0,0 +1,62 @@
//
// CharacterGhostObject.h
// libraries/physics/src
//
// Created by Andrew Meadows 2016.08.26
// Copyright 2016 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_CharacterGhostObject_h
#define hifi_CharacterGhostObject_h
#include <stdint.h>
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/CollisionDispatch/btGhostObject.h>
#include <stdint.h>
#include "CharacterSweepResult.h"
#include "CharacterRayResult.h"
class CharacterGhostShape;
class CharacterGhostObject : public btPairCachingGhostObject {
public:
CharacterGhostObject() { }
~CharacterGhostObject();
void setCollisionGroupAndMask(int16_t group, int16_t mask);
void getCollisionGroupAndMask(int16_t& group, int16_t& mask) const;
void setRadiusAndHalfHeight(btScalar radius, btScalar halfHeight);
void setUpDirection(const btVector3& up);
void setCharacterShape(btConvexHullShape* shape);
void setCollisionWorld(btCollisionWorld* world);
bool rayTest(const btVector3& start,
const btVector3& end,
CharacterRayResult& result) const;
void refreshOverlappingPairCache();
protected:
void removeFromWorld();
void addToWorld();
protected:
btCollisionWorld* _world { nullptr }; // input, pointer to world
btScalar _halfHeight { 0.0f };
btScalar _radius { 0.0f };
btConvexHullShape* _characterShape { nullptr }; // input, shape of character
CharacterGhostShape* _ghostShape { nullptr }; // internal, shape whose Aabb is used for overlap cache
int16_t _collisionFilterGroup { 0 };
int16_t _collisionFilterMask { 0 };
bool _inWorld { false }; // internal, was added to world
};
#endif // hifi_CharacterGhostObject_h

View file

@ -0,0 +1,31 @@
//
// CharacterGhostShape.cpp
// libraries/physics/src
//
// Created by Andrew Meadows 2016.09.14
// Copyright 2016 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 "CharacterGhostShape.h"
#include <assert.h>
CharacterGhostShape::CharacterGhostShape(const btConvexHullShape* shape) :
btConvexHullShape(reinterpret_cast<const btScalar*>(shape->getUnscaledPoints()), shape->getNumPoints(), sizeof(btVector3)) {
assert(shape);
assert(shape->getUnscaledPoints());
assert(shape->getNumPoints() > 0);
setMargin(shape->getMargin());
}
void CharacterGhostShape::getAabb (const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const {
btConvexHullShape::getAabb(t, aabbMin, aabbMax);
// double the size of the Aabb by expanding both corners by half the extent
btVector3 expansion = 0.5f * (aabbMax - aabbMin);
aabbMin -= expansion;
aabbMax += expansion;
}

View file

@ -0,0 +1,25 @@
//
// CharacterGhostShape.h
// libraries/physics/src
//
// Created by Andrew Meadows 2016.09.14
// Copyright 2016 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_CharacterGhostShape_h
#define hifi_CharacterGhostShape_h
#include <BulletCollision/CollisionShapes/btConvexHullShape.h>
class CharacterGhostShape : public btConvexHullShape {
// Same as btConvexHullShape but reports an expanded Aabb for larger ghost overlap cache
public:
CharacterGhostShape(const btConvexHullShape* shape);
virtual void getAabb (const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const override;
};
#endif // hifi_CharacterGhostShape_h

View file

@ -0,0 +1,31 @@
//
// CharaterRayResult.cpp
// libraries/physics/src
//
// Created by Andrew Meadows 2016.09.05
// Copyright 2016 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 "CharacterRayResult.h"
#include <assert.h>
#include "CharacterGhostObject.h"
CharacterRayResult::CharacterRayResult (const CharacterGhostObject* character) :
btCollisionWorld::ClosestRayResultCallback(btVector3(0.0f, 0.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)),
_character(character)
{
assert(_character);
_character->getCollisionGroupAndMask(m_collisionFilterGroup, m_collisionFilterMask);
}
btScalar CharacterRayResult::addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) {
if (rayResult.m_collisionObject == _character) {
return 1.0f;
}
return ClosestRayResultCallback::addSingleResult (rayResult, normalInWorldSpace);
}

View file

@ -0,0 +1,44 @@
//
// CharaterRayResult.h
// libraries/physics/src
//
// Created by Andrew Meadows 2016.09.05
// Copyright 2016 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_CharacterRayResult_h
#define hifi_CharacterRayResult_h
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
class CharacterGhostObject;
class CharacterRayResult : public btCollisionWorld::ClosestRayResultCallback {
public:
CharacterRayResult (const CharacterGhostObject* character);
virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override;
protected:
const CharacterGhostObject* _character;
// Note: Public data members inherited from ClosestRayResultCallback
//
// btVector3 m_rayFromWorld;//used to calculate hitPointWorld from hitFraction
// btVector3 m_rayToWorld;
// btVector3 m_hitNormalWorld;
// btVector3 m_hitPointWorld;
//
// Note: Public data members inherited from RayResultCallback
//
// btScalar m_closestHitFraction;
// const btCollisionObject* m_collisionObject;
// short int m_collisionFilterGroup;
// short int m_collisionFilterMask;
};
#endif // hifi_CharacterRayResult_h

View file

@ -0,0 +1,42 @@
//
// CharaterSweepResult.cpp
// libraries/physics/src
//
// Created by Andrew Meadows 2016.09.01
// Copyright 2016 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 "CharacterSweepResult.h"
#include <assert.h>
#include "CharacterGhostObject.h"
CharacterSweepResult::CharacterSweepResult(const CharacterGhostObject* character)
: btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0f, 0.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)),
_character(character)
{
// set collision group and mask to match _character
assert(_character);
_character->getCollisionGroupAndMask(m_collisionFilterGroup, m_collisionFilterMask);
}
btScalar CharacterSweepResult::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool useWorldFrame) {
// skip objects that we shouldn't collide with
if (!convexResult.m_hitCollisionObject->hasContactResponse()) {
return btScalar(1.0);
}
if (convexResult.m_hitCollisionObject == _character) {
return btScalar(1.0);
}
return ClosestConvexResultCallback::addSingleResult(convexResult, useWorldFrame);
}
void CharacterSweepResult::resetHitHistory() {
m_hitCollisionObject = nullptr;
m_closestHitFraction = btScalar(1.0f);
}

View file

@ -0,0 +1,45 @@
//
// CharaterSweepResult.h
// libraries/physics/src
//
// Created by Andrew Meadows 2016.09.01
// Copyright 2016 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_CharacterSweepResult_h
#define hifi_CharacterSweepResult_h
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
class CharacterGhostObject;
class CharacterSweepResult : public btCollisionWorld::ClosestConvexResultCallback {
public:
CharacterSweepResult(const CharacterGhostObject* character);
virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool useWorldFrame) override;
void resetHitHistory();
protected:
const CharacterGhostObject* _character;
// NOTE: Public data members inherited from ClosestConvexResultCallback:
//
// btVector3 m_convexFromWorld; // unused except by btClosestNotMeConvexResultCallback
// btVector3 m_convexToWorld; // unused except by btClosestNotMeConvexResultCallback
// btVector3 m_hitNormalWorld;
// btVector3 m_hitPointWorld;
// const btCollisionObject* m_hitCollisionObject;
//
// NOTE: Public data members inherited from ConvexResultCallback:
//
// btScalar m_closestHitFraction;
// short int m_collisionFilterGroup;
// short int m_collisionFilterMask;
};
#endif // hifi_CharacterSweepResult_h

View file

@ -1,6 +1,6 @@
// //
// CollisionRenderMeshCache.cpp // CollisionRenderMeshCache.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2016.07.13 // Created by Andrew Meadows 2016.07.13
// Copyright 2016 High Fidelity, Inc. // Copyright 2016 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// CollisionRenderMeshCache.h // CollisionRenderMeshCache.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2016.07.13 // Created by Andrew Meadows 2016.07.13
// Copyright 2016 High Fidelity, Inc. // Copyright 2016 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ContactEvent.cpp // ContactEvent.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2015.01.20 // Created by Andrew Meadows 2015.01.20
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ContactEvent.h // ContactEvent.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2015.01.20 // Created by Andrew Meadows 2015.01.20
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ObjectAction.cpp // ObjectAction.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Seth Alves 2015-6-2 // Created by Seth Alves 2015-6-2
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ObjectAction.h // ObjectAction.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Seth Alves 2015-6-2 // Created by Seth Alves 2015-6-2
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ObjectMotionState.cpp // ObjectMotionState.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.11.05 // Created by Andrew Meadows 2014.11.05
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ObjectMotionState.h // ObjectMotionState.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.11.05 // Created by Andrew Meadows 2014.11.05
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// PhysicalEntitySimulation.cpp // PhysicalEntitySimulation.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2015.04.27 // Created by Andrew Meadows 2015.04.27
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// PhysicalEntitySimulation.h // PhysicalEntitySimulation.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2015.04.27 // Created by Andrew Meadows 2015.04.27
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// PhysicsEngine.cpp // PhysicsEngine.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.10.29 // Created by Andrew Meadows 2014.10.29
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// PhysicsEngine.h // PhysicsEngine.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.10.29 // Created by Andrew Meadows 2014.10.29
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ShapeFactory.cpp // ShapeFactory.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.12.01 // Created by Andrew Meadows 2014.12.01
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ShapeFactory.h // ShapeFactory.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.12.01 // Created by Andrew Meadows 2014.12.01
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ShapeManager.cpp // ShapeManager.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.10.29 // Created by Andrew Meadows 2014.10.29
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ShapeManager.h // ShapeManager.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.10.29 // Created by Andrew Meadows 2014.10.29
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -28,10 +28,10 @@ public:
const std::unordered_set<int>& getCauterizeBoneSet() const { return _cauterizeBoneSet; } const std::unordered_set<int>& getCauterizeBoneSet() const { return _cauterizeBoneSet; }
void setCauterizeBoneSet(const std::unordered_set<int>& boneSet) { _cauterizeBoneSet = boneSet; } void setCauterizeBoneSet(const std::unordered_set<int>& boneSet) { _cauterizeBoneSet = boneSet; }
void deleteGeometry() override; void deleteGeometry() override;
bool updateGeometry() override; bool updateGeometry() override;
void createVisibleRenderItemSet() override; void createVisibleRenderItemSet() override;
void createCollisionRenderItemSet() override; void createCollisionRenderItemSet() override;
virtual void updateClusterMatrices() override; virtual void updateClusterMatrices() override;
@ -41,7 +41,7 @@ public:
protected: protected:
std::unordered_set<int> _cauterizeBoneSet; std::unordered_set<int> _cauterizeBoneSet;
QVector<Model::MeshState> _cauterizeMeshStates; QVector<Model::MeshState> _cauterizeMeshStates;
bool _isCauterized { false }; bool _isCauterized { false };
bool _enableCauterization { false }; bool _enableCauterization { false };
}; };

View file

@ -573,7 +573,7 @@ bool Model::addToScene(const render::ScenePointer& scene,
bool somethingAdded = false; bool somethingAdded = false;
if (_collisionGeometry) { if (_collisionGeometry) {
if (_collisionRenderItems.empty()) { if (_collisionRenderItemsMap.empty()) {
foreach (auto renderItem, _collisionRenderItems) { foreach (auto renderItem, _collisionRenderItems) {
auto item = scene->allocateID(); auto item = scene->allocateID();
auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderItem); auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderItem);
@ -583,7 +583,7 @@ bool Model::addToScene(const render::ScenePointer& scene,
transaction.resetItem(item, renderPayload); transaction.resetItem(item, renderPayload);
_collisionRenderItemsMap.insert(item, renderPayload); _collisionRenderItemsMap.insert(item, renderPayload);
} }
somethingAdded = !_collisionRenderItems.empty(); somethingAdded = !_collisionRenderItemsMap.empty();
} }
} else { } else {
if (_modelMeshRenderItemsMap.empty()) { if (_modelMeshRenderItemsMap.empty()) {
@ -632,7 +632,7 @@ void Model::removeFromScene(const render::ScenePointer& scene, render::Transacti
transaction.removeItem(item); transaction.removeItem(item);
} }
_collisionRenderItems.clear(); _collisionRenderItems.clear();
_collisionRenderItems.clear(); _collisionRenderItemsMap.clear();
_addedToScene = false; _addedToScene = false;
_renderInfoVertexCount = 0; _renderInfoVertexCount = 0;

View file

@ -11,7 +11,9 @@
#include <QDebug> #include <QDebug>
#include <GLMHelpers.h> #include <GLMHelpers.h>
#include <glm/gtx/string_cast.hpp>
#include "ScriptEngineLogging.h" #include "ScriptEngineLogging.h"
#include "ScriptEngine.h"
#include "Mat4.h" #include "Mat4.h"
glm::mat4 Mat4::multiply(const glm::mat4& m1, const glm::mat4& m2) const { glm::mat4 Mat4::multiply(const glm::mat4& m1, const glm::mat4& m2) const {
@ -66,10 +68,12 @@ glm::vec3 Mat4::getUp(const glm::mat4& m) const {
return glm::vec3(m[0][1], m[1][1], m[2][1]); return glm::vec3(m[0][1], m[1][1], m[2][1]);
} }
void Mat4::print(const QString& label, const glm::mat4& m) const { void Mat4::print(const QString& label, const glm::mat4& m, bool transpose) const {
qCDebug(scriptengine) << qPrintable(label) << glm::dmat4 out = transpose ? glm::transpose(m) : m;
"row0 =" << m[0][0] << "," << m[1][0] << "," << m[2][0] << "," << m[3][0] << QString message = QString("%1 %2").arg(qPrintable(label));
"row1 =" << m[0][1] << "," << m[1][1] << "," << m[2][1] << "," << m[3][1] << message = message.arg(glm::to_string(out).c_str());
"row2 =" << m[0][2] << "," << m[1][2] << "," << m[2][2] << "," << m[3][2] << qCDebug(scriptengine) << message;
"row3 =" << m[0][3] << "," << m[1][3] << "," << m[2][3] << "," << m[3][3]; if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
scriptEngine->print(message);
}
} }

View file

@ -16,9 +16,10 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QtScript/QScriptable>
/// Scriptable Mat4 object. Used exclusively in the JavaScript API /// Scriptable Mat4 object. Used exclusively in the JavaScript API
class Mat4 : public QObject { class Mat4 : public QObject, protected QScriptable {
Q_OBJECT Q_OBJECT
public slots: public slots:
@ -43,7 +44,7 @@ public slots:
glm::vec3 getRight(const glm::mat4& m) const; glm::vec3 getRight(const glm::mat4& m) const;
glm::vec3 getUp(const glm::mat4& m) const; glm::vec3 getUp(const glm::mat4& m) const;
void print(const QString& label, const glm::mat4& m) const; void print(const QString& label, const glm::mat4& m, bool transpose = false) const;
}; };
#endif // hifi_Mat4_h #endif // hifi_Mat4_h

View file

@ -15,7 +15,9 @@
#include <OctreeConstants.h> #include <OctreeConstants.h>
#include <GLMHelpers.h> #include <GLMHelpers.h>
#include <glm/gtx/string_cast.hpp>
#include "ScriptEngineLogging.h" #include "ScriptEngineLogging.h"
#include "ScriptEngine.h"
#include "Quat.h" #include "Quat.h"
quat Quat::normalize(const glm::quat& q) { quat Quat::normalize(const glm::quat& q) {
@ -114,8 +116,17 @@ float Quat::dot(const glm::quat& q1, const glm::quat& q2) {
return glm::dot(q1, q2); return glm::dot(q1, q2);
} }
void Quat::print(const QString& label, const glm::quat& q) { void Quat::print(const QString& label, const glm::quat& q, bool asDegrees) {
qCDebug(scriptengine) << qPrintable(label) << q.x << "," << q.y << "," << q.z << "," << q.w; QString message = QString("%1 %2").arg(qPrintable(label));
if (asDegrees) {
message = message.arg(glm::to_string(glm::dvec3(safeEulerAngles(q))).c_str());
} else {
message = message.arg(glm::to_string(glm::dquat(q)).c_str());
}
qCDebug(scriptengine) << message;
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
scriptEngine->print(message);
}
} }
bool Quat::equal(const glm::quat& q1, const glm::quat& q2) { bool Quat::equal(const glm::quat& q1, const glm::quat& q2) {

View file

@ -18,6 +18,7 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QtScript/QScriptable>
/**jsdoc /**jsdoc
* A Quaternion * A Quaternion
@ -30,7 +31,7 @@
*/ */
/// Scriptable interface a Quaternion helper class object. Used exclusively in the JavaScript API /// Scriptable interface a Quaternion helper class object. Used exclusively in the JavaScript API
class Quat : public QObject { class Quat : public QObject, protected QScriptable {
Q_OBJECT Q_OBJECT
public slots: public slots:
@ -58,7 +59,7 @@ public slots:
glm::quat slerp(const glm::quat& q1, const glm::quat& q2, float alpha); glm::quat slerp(const glm::quat& q1, const glm::quat& q2, float alpha);
glm::quat squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h); glm::quat squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h);
float dot(const glm::quat& q1, const glm::quat& q2); float dot(const glm::quat& q1, const glm::quat& q2);
void print(const QString& label, const glm::quat& q); void print(const QString& label, const glm::quat& q, bool asDegrees = false);
bool equal(const glm::quat& q1, const glm::quat& q2); bool equal(const glm::quat& q1, const glm::quat& q2);
glm::quat cancelOutRollAndPitch(const glm::quat& q); glm::quat cancelOutRollAndPitch(const glm::quat& q);
glm::quat cancelOutRoll(const glm::quat& q); glm::quat cancelOutRoll(const glm::quat& q);

View file

@ -105,11 +105,11 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) {
} }
message += context->argument(i).toString(); message += context->argument(i).toString();
} }
qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline qCDebug(scriptengineScript).noquote() << message; // noquote() so that \n is treated as newline
// FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print? if (ScriptEngine *scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
engine->globalObject().property("Script").property("print") scriptEngine->print(message);
.call(engine->nullValue(), QScriptValueList({ message })); }
return QScriptValue(); return QScriptValue();
} }
@ -472,6 +472,11 @@ void ScriptEngine::scriptInfoMessage(const QString& message) {
emit infoMessage(message, getFilename()); emit infoMessage(message, getFilename());
} }
void ScriptEngine::scriptPrintedMessage(const QString& message) {
qCDebug(scriptengine) << message;
emit printedMessage(message, getFilename());
}
// Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of
// callAnimationStateHandler requires that the type be registered. // callAnimationStateHandler requires that the type be registered.
// These two are meaningful, if we ever do want to use them... // These two are meaningful, if we ever do want to use them...

View file

@ -221,6 +221,7 @@ public:
void scriptErrorMessage(const QString& message); void scriptErrorMessage(const QString& message);
void scriptWarningMessage(const QString& message); void scriptWarningMessage(const QString& message);
void scriptInfoMessage(const QString& message); void scriptInfoMessage(const QString& message);
void scriptPrintedMessage(const QString& message);
int getNumRunningEntityScripts() const; int getNumRunningEntityScripts() const;
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const; bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;

View file

@ -453,7 +453,8 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL
(scriptFilename.scheme() != "http" && (scriptFilename.scheme() != "http" &&
scriptFilename.scheme() != "https" && scriptFilename.scheme() != "https" &&
scriptFilename.scheme() != "atp" && scriptFilename.scheme() != "atp" &&
scriptFilename.scheme() != "file")) { scriptFilename.scheme() != "file" &&
scriptFilename.scheme() != "about")) {
// deal with a "url" like c:/something // deal with a "url" like c:/something
scriptUrl = normalizeScriptURL(QUrl::fromLocalFile(scriptFilename.toString())); scriptUrl = normalizeScriptURL(QUrl::fromLocalFile(scriptFilename.toString()));
} else { } else {
@ -472,7 +473,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL
}, Qt::QueuedConnection); }, Qt::QueuedConnection);
if (scriptFilename.isEmpty()) { if (scriptFilename.isEmpty() || !scriptUrl.isValid()) {
launchScriptEngine(scriptEngine); launchScriptEngine(scriptEngine);
} else { } else {
// connect to the appropriate signals of this script engine // connect to the appropriate signals of this script engine

View file

@ -14,6 +14,7 @@
#include <QDebug> #include <QDebug>
#include "ScriptEngineLogging.h" #include "ScriptEngineLogging.h"
#include "ScriptEngine.h"
#include "ScriptUUID.h" #include "ScriptUUID.h"
QUuid ScriptUUID::fromString(const QString& s) { QUuid ScriptUUID::fromString(const QString& s) {
@ -36,6 +37,11 @@ bool ScriptUUID::isNull(const QUuid& id) {
return id.isNull(); return id.isNull();
} }
void ScriptUUID::print(const QString& lable, const QUuid& id) { void ScriptUUID::print(const QString& label, const QUuid& id) {
qCDebug(scriptengine) << qPrintable(lable) << id.toString(); QString message = QString("%1 %2").arg(qPrintable(label));
message = message.arg(id.toString());
qCDebug(scriptengine) << message;
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
scriptEngine->print(message);
}
} }

View file

@ -15,9 +15,10 @@
#define hifi_ScriptUUID_h #define hifi_ScriptUUID_h
#include <QUuid> #include <QUuid>
#include <QtScript/QScriptable>
/// Scriptable interface for a UUID helper class object. Used exclusively in the JavaScript API /// Scriptable interface for a UUID helper class object. Used exclusively in the JavaScript API
class ScriptUUID : public QObject { class ScriptUUID : public QObject, protected QScriptable {
Q_OBJECT Q_OBJECT
public slots: public slots:
@ -26,7 +27,7 @@ public slots:
QUuid generate(); QUuid generate();
bool isEqual(const QUuid& idA, const QUuid& idB); bool isEqual(const QUuid& idA, const QUuid& idB);
bool isNull(const QUuid& id); bool isNull(const QUuid& id);
void print(const QString& lable, const QUuid& id); void print(const QString& label, const QUuid& id);
}; };
#endif // hifi_ScriptUUID_h #endif // hifi_ScriptUUID_h

View file

@ -14,20 +14,26 @@
#include <QDebug> #include <QDebug>
#include <GLMHelpers.h> #include <GLMHelpers.h>
#include <glm/gtx/string_cast.hpp>
#include "ScriptEngineLogging.h" #include "ScriptEngineLogging.h"
#include "NumericalConstants.h" #include "NumericalConstants.h"
#include "Vec3.h" #include "Vec3.h"
#include "ScriptEngine.h"
float Vec3::orientedAngle(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3) { float Vec3::orientedAngle(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3) {
float radians = glm::orientedAngle(glm::normalize(v1), glm::normalize(v2), glm::normalize(v3)); float radians = glm::orientedAngle(glm::normalize(v1), glm::normalize(v2), glm::normalize(v3));
return glm::degrees(radians); return glm::degrees(radians);
} }
void Vec3::print(const QString& label, const glm::vec3& v) {
void Vec3::print(const QString& lable, const glm::vec3& v) { QString message = QString("%1 %2").arg(qPrintable(label));
qCDebug(scriptengine) << qPrintable(lable) << v.x << "," << v.y << "," << v.z; message = message.arg(glm::to_string(glm::dvec3(v)).c_str());
qCDebug(scriptengine) << message;
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
scriptEngine->print(message);
}
} }
bool Vec3::withinEpsilon(const glm::vec3& v1, const glm::vec3& v2, float epsilon) { bool Vec3::withinEpsilon(const glm::vec3& v1, const glm::vec3& v2, float epsilon) {

View file

@ -17,6 +17,7 @@
#include <QtCore/QObject> #include <QtCore/QObject>
#include <QtCore/QString> #include <QtCore/QString>
#include <QtScript/QScriptable>
#include "GLMHelpers.h" #include "GLMHelpers.h"
@ -48,7 +49,7 @@
*/ */
/// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API /// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API
class Vec3 : public QObject { class Vec3 : public QObject, protected QScriptable {
Q_OBJECT Q_OBJECT
Q_PROPERTY(glm::vec3 UNIT_X READ UNIT_X CONSTANT) Q_PROPERTY(glm::vec3 UNIT_X READ UNIT_X CONSTANT)
Q_PROPERTY(glm::vec3 UNIT_Y READ UNIT_Y CONSTANT) Q_PROPERTY(glm::vec3 UNIT_Y READ UNIT_Y CONSTANT)

View file

@ -1,6 +1,6 @@
// //
// BackgroundMode.h // BackgroundMode.h
// libraries/physcis/src // libraries/physics/src
// //
// Copyright 2015 High Fidelity, Inc. // Copyright 2015 High Fidelity, Inc.
// //

View file

@ -1,6 +1,6 @@
// //
// ShapeInfo.cpp // ShapeInfo.cpp
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.10.29 // Created by Andrew Meadows 2014.10.29
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1,6 +1,6 @@
// //
// ShapeInfo.h // ShapeInfo.h
// libraries/physcis/src // libraries/physics/src
// //
// Created by Andrew Meadows 2014.10.29 // Created by Andrew Meadows 2014.10.29
// Copyright 2014 High Fidelity, Inc. // Copyright 2014 High Fidelity, Inc.

View file

@ -1138,3 +1138,17 @@ SpatiallyNestablePointer SpatiallyNestable::findByID(QUuid id, bool& success) {
} }
return parentWP.lock(); return parentWP.lock();
} }
QString SpatiallyNestable::nestableTypeToString(NestableType nestableType) {
switch(nestableType) {
case NestableType::Entity:
return "entity";
case NestableType::Avatar:
return "avatar";
case NestableType::Overlay:
return "overlay";
default:
return "unknown";
}
}

View file

@ -42,6 +42,8 @@ public:
virtual const QUuid getID() const; virtual const QUuid getID() const;
virtual void setID(const QUuid& id); virtual void setID(const QUuid& id);
virtual QString getName() const { return "SpatiallyNestable"; }
virtual const QUuid getParentID() const; virtual const QUuid getParentID() const;
virtual void setParentID(const QUuid& parentID); virtual void setParentID(const QUuid& parentID);
@ -62,6 +64,8 @@ public:
static glm::vec3 localToWorldAngularVelocity(const glm::vec3& angularVelocity, static glm::vec3 localToWorldAngularVelocity(const glm::vec3& angularVelocity,
const QUuid& parentID, int parentJointIndex, bool& success); const QUuid& parentID, int parentJointIndex, bool& success);
static QString nestableTypeToString(NestableType nestableType);
// world frame // world frame
virtual const Transform getTransform(bool& success, int depth = 0) const; virtual const Transform getTransform(bool& success, int depth = 0) const;
virtual const Transform getTransform() const; virtual const Transform getTransform() const;

View file

@ -20,9 +20,10 @@
print('<span style="color:red">Tests completed with ' + print('<span style="color:red">Tests completed with ' +
errorCount + ' ' + ERROR + '.<span>'); errorCount + ' ' + ERROR + '.<span>');
} }
if (pending.length) if (pending.length) {
print ('<span style="color:darkorange">disabled: <br />&nbsp;&nbsp;&nbsp;'+ print ('<span style="color:darkorange">disabled: <br />&nbsp;&nbsp;&nbsp;'+
pending.join('<br />&nbsp;&nbsp;&nbsp;')+'</span>'); pending.join('<br />&nbsp;&nbsp;&nbsp;')+'</span>');
}
print('Tests completed in ' + (endTime - startTime) + 'ms.'); print('Tests completed in ' + (endTime - startTime) + 'ms.');
}; };
this.suiteStarted = function(obj) { this.suiteStarted = function(obj) {

View file

@ -0,0 +1,39 @@
/* eslint-env jasmine */
// this test generates sample print, Script.print, etc. output
main();
function main() {
// to match with historical behavior, Script.print(message) output only triggers
// the printedMessage signal (and therefore doesn't show up in the application log)
Script.print('[Script.print] hello world');
// the rest of these should show up in both the application log and signaled print handlers
print('[print]', 'hello', 'world');
// note: these trigger the equivalent of an emit
Script.printedMessage('[Script.printedMessage] hello world', '{filename}');
Script.infoMessage('[Script.infoMessage] hello world', '{filename}');
Script.warningMessage('[Script.warningMessage] hello world', '{filename}');
Script.errorMessage('[Script.errorMessage] hello world', '{filename}');
{
Vec3.print('[Vec3.print]', Vec3.HALF);
var q = Quat.fromPitchYawRollDegrees(45, 45, 45);
Quat.print('[Quat.print]', q);
Quat.print('[Quat.print (euler)]', q, true);
function vec4(x,y,z,w) {
return { x: x, y: y, z: z, w: w };
}
var m = Mat4.createFromColumns(
vec4(1,2,3,4), vec4(5,6,7,8), vec4(9,10,11,12), vec4(13,14,15,16)
);
Mat4.print('[Mat4.print (col major)]', m);
Mat4.print('[Mat4.print (row major)]', m, true);
Uuid.print('[Uuid.print]', Uuid.fromString(Uuid.toString(0)));
}
}

View file

@ -1,5 +1,7 @@
/* eslint-env jasmine */
// Art3mis // Art3mis
// eslint-disable-next-line max-len
var DEFAULT_AVATAR_URL = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/contents/e76946cc-c272-4adf-9bb6-02cde0a4b57d/8fd984ea6fe1495147a3303f87fa6e23.fst?1460131758"; var DEFAULT_AVATAR_URL = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/contents/e76946cc-c272-4adf-9bb6-02cde0a4b57d/8fd984ea6fe1495147a3303f87fa6e23.fst?1460131758";
var ORIGIN = {x: 0, y: 0, z: 0}; var ORIGIN = {x: 0, y: 0, z: 0};
@ -8,6 +10,15 @@ var ROT_IDENT = {x: 0, y: 0, z: 0, w: 1};
describe("MyAvatar", function () { describe("MyAvatar", function () {
// backup/restore current skeletonModelURL
beforeAll(function() {
this.oldURL = MyAvatar.skeletonModelURL;
});
afterAll(function() {
MyAvatar.skeletonModelURL = this.oldURL;
});
// reload the avatar from scratch before each test. // reload the avatar from scratch before each test.
beforeEach(function (done) { beforeEach(function (done) {
MyAvatar.skeletonModelURL = DEFAULT_AVATAR_URL; MyAvatar.skeletonModelURL = DEFAULT_AVATAR_URL;
@ -20,12 +31,12 @@ describe("MyAvatar", function () {
MyAvatar.position = ORIGIN; MyAvatar.position = ORIGIN;
MyAvatar.orientation = ROT_IDENT; MyAvatar.orientation = ROT_IDENT;
// give the avatar 1/2 a second to settle on the ground in the idle pose. // give the avatar 1/2 a second to settle on the ground in the idle pose.
Script.setTimeout(function () { Script.setTimeout(function () {
done(); done();
}, 500); }, 500);
} }
}, 500); }, 500);
}); }, 10000 /* timeout -- allow time to download avatar*/);
// makes the assumption that there is solid ground somewhat underneath the avatar. // makes the assumption that there is solid ground somewhat underneath the avatar.
it("position and orientation getters", function () { it("position and orientation getters", function () {

View file

@ -1,3 +1,5 @@
/* eslint-env jasmine */
Script.include('../../../system/libraries/utils.js'); Script.include('../../../system/libraries/utils.js');
describe('Bind', function() { describe('Bind', function() {

View file

@ -1,3 +1,5 @@
/* eslint-env jasmine */
describe('Entity', function() { describe('Entity', function() {
var center = Vec3.sum( var center = Vec3.sum(
MyAvatar.position, MyAvatar.position,
@ -19,6 +21,14 @@ describe('Entity', function() {
}, },
}; };
it('serversExist', function() {
expect(Entities.serversExist()).toBe(true);
});
it('canRezTmp', function() {
expect(Entities.canRezTmp()).toBe(true);
});
beforeEach(function() { beforeEach(function() {
boxEntity = Entities.addEntity(boxProps); boxEntity = Entities.addEntity(boxProps);
}); });
@ -62,4 +72,4 @@ describe('Entity', function() {
props = Entities.getEntityProperties(boxEntity); props = Entities.getEntityProperties(boxEntity);
expect(props.lastEdited).toBeGreaterThan(prevLastEdited); expect(props.lastEdited).toBeGreaterThan(prevLastEdited);
}); });
}); });

View file

@ -1,13 +1,30 @@
/* eslint-env jasmine */
// Include testing library // Include testing library
Script.include('../../libraries/jasmine/jasmine.js'); Script.include('../../libraries/jasmine/jasmine.js');
Script.include('../../libraries/jasmine/hifi-boot.js') Script.include('../../libraries/jasmine/hifi-boot.js');
// Include unit tests // Include unit tests
// FIXME: Figure out why jasmine done() is not working. Script.include('avatarUnitTests.js');
// Script.include('avatarUnitTests.js');
Script.include('bindUnitTest.js'); Script.include('bindUnitTest.js');
Script.include('entityUnitTests.js'); Script.include('entityUnitTests.js');
describe("jasmine internal tests", function() {
it('should support async .done()', function(done) {
var start = new Date;
Script.setTimeout(function() {
expect((new Date - start)/1000).toBeCloseTo(0.5, 1);
done();
}, 500);
});
// jasmine pending test
xit('disabled test', function() {
expect(false).toBe(true);
});
});
// invoke Script.stop (after any async tests complete)
jasmine.getEnv().addReporter({ jasmineDone: Script.stop });
// Run the tests // Run the tests
jasmine.getEnv().execute(); jasmine.getEnv().execute();
Script.stop();

View file

@ -3881,6 +3881,7 @@ function MyController(hand) {
// we appear to be holding something and this script isn't in a state that would be holding something. // we appear to be holding something and this script isn't in a state that would be holding something.
// unhook it. if we previously took note of this entity's parent, put it back where it was. This // unhook it. if we previously took note of this entity's parent, put it back where it was. This
// works around some problems that happen when more than one hand or avatar is passing something around. // works around some problems that happen when more than one hand or avatar is passing something around.
var childType = Entities.getNestableType(childID);
if (_this.previousParentID[childID]) { if (_this.previousParentID[childID]) {
var previousParentID = _this.previousParentID[childID]; var previousParentID = _this.previousParentID[childID];
var previousParentJointIndex = _this.previousParentJointIndex[childID]; var previousParentJointIndex = _this.previousParentJointIndex[childID];
@ -3898,7 +3899,7 @@ function MyController(hand) {
} }
_this.previouslyUnhooked[childID] = now; _this.previouslyUnhooked[childID] = now;
if (Overlays.getProperty(childID, "grabbable")) { if (childType == "overlay" && Overlays.getProperty(childID, "grabbable")) {
// only auto-unhook overlays that were flagged as grabbable. this avoids unhooking overlays // only auto-unhook overlays that were flagged as grabbable. this avoids unhooking overlays
// used in tutorial. // used in tutorial.
Overlays.editOverlay(childID, { Overlays.editOverlay(childID, {
@ -3906,12 +3907,20 @@ function MyController(hand) {
parentJointIndex: previousParentJointIndex parentJointIndex: previousParentJointIndex
}); });
} }
Entities.editEntity(childID, { parentID: previousParentID, parentJointIndex: previousParentJointIndex }); if (childType == "entity") {
Entities.editEntity(childID, {
parentID: previousParentID,
parentJointIndex: previousParentJointIndex
});
}
} else { } else {
Entities.editEntity(childID, { parentID: NULL_UUID }); if (childType == "entity") {
if (Overlays.getProperty(childID, "grabbable")) { Entities.editEntity(childID, { parentID: NULL_UUID });
Overlays.editOverlay(childID, { parentID: NULL_UUID }); } else if (childType == "overlay") {
if (Overlays.getProperty(childID, "grabbable")) {
Overlays.editOverlay(childID, { parentID: NULL_UUID });
}
} }
} }
} }

View file

@ -275,7 +275,8 @@ WebTablet.prototype.getLocation = function() {
}; };
WebTablet.prototype.setHomeButtonTexture = function() { WebTablet.prototype.setHomeButtonTexture = function() {
Entities.editEntity(this.tabletEntityID, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); // TODO - is this still needed?
// Entities.editEntity(this.tabletEntityID, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})});
}; };
WebTablet.prototype.setURL = function (url) { WebTablet.prototype.setURL = function (url) {
@ -338,7 +339,8 @@ WebTablet.prototype.geometryChanged = function (geometry) {
// compute position, rotation & parentJointIndex of the tablet // compute position, rotation & parentJointIndex of the tablet
this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties); this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties);
Entities.editEntity(this.tabletEntityID, tabletProperties); // TODO -- is this still needed?
// Entities.editEntity(this.tabletEntityID, tabletProperties);
} }
}; };
@ -439,7 +441,8 @@ WebTablet.prototype.onHmdChanged = function () {
var tabletProperties = {}; var tabletProperties = {};
// compute position, rotation & parentJointIndex of the tablet // compute position, rotation & parentJointIndex of the tablet
this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties); this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties);
Entities.editEntity(this.tabletEntityID, tabletProperties); // TODO -- is this still needed?
// Entities.editEntity(this.tabletEntityID, tabletProperties);
// Full scene FXAA should be disabled on the overlay when the tablet in desktop mode. // Full scene FXAA should be disabled on the overlay when the tablet in desktop mode.
// This should make the text more readable. // This should make the text more readable.
@ -530,7 +533,8 @@ WebTablet.prototype.cameraModeChanged = function (newMode) {
var tabletProperties = {}; var tabletProperties = {};
// compute position, rotation & parentJointIndex of the tablet // compute position, rotation & parentJointIndex of the tablet
self.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties); self.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties);
Entities.editEntity(self.tabletEntityID, tabletProperties); // TODO -- is this still needed?
// Entities.editEntity(self.tabletEntityID, tabletProperties);
} }
}; };

View file

@ -122,7 +122,8 @@
function debug() { function debug() {
var stateString = "<" + STATE_STRINGS[state] + ">"; var stateString = "<" + STATE_STRINGS[state] + ">";
var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]"; var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]";
print.apply(null, [].concat.apply([LABEL, stateString, JSON.stringify(waitingList), connecting], var current = "[" + currentHand + "/" + currentHandJointIndex + "]"
print.apply(null, [].concat.apply([LABEL, stateString, current, JSON.stringify(waitingList), connecting],
[].map.call(arguments, JSON.stringify))); [].map.call(arguments, JSON.stringify)));
} }
@ -759,7 +760,10 @@
break; break;
case "done": case "done":
delete waitingList[senderID]; delete waitingList[senderID];
if (state === STATES.CONNECTING && connectingId === senderID) { if (connectionId !== senderID) {
break;
}
if (state === STATES.CONNECTING) {
// if they are done, and didn't connect us, terminate our // if they are done, and didn't connect us, terminate our
// connecting // connecting
if (message.connectionId !== MyAvatar.sessionUUID) { if (message.connectionId !== MyAvatar.sessionUUID) {
@ -768,11 +772,20 @@
// value for isKeyboard, as we should not change the animation // value for isKeyboard, as we should not change the animation
// state anyways (if any) // state anyways (if any)
startHandshake(); startHandshake();
} else {
// they just created a connection request to us, and we are connecting to
// them, so lets just stop connecting and make connection..
makeConnection(connectingId);
stopConnecting();
} }
} else { } else {
// if waiting or inactive, lets clear the connecting id. If in makingConnection, if (state == STATES.MAKING_CONNECTION) {
// do nothing // we are making connection, they just started, so lets reset the
if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) { // poll count just in case
pollCount = 0;
} else {
// if waiting or inactive, lets clear the connecting id. If in makingConnection,
// do nothing
clearConnecting(); clearConnecting();
if (state !== STATES.INACTIVE) { if (state !== STATES.INACTIVE) {
startHandshake(); startHandshake();

View file

@ -13,7 +13,7 @@
// //
/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, /* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays,
MyAvatar, Menu, Vec3 */ MyAvatar, Menu, AvatarInputs, Vec3 */
(function() { // BEGIN LOCAL_SCOPE (function() { // BEGIN LOCAL_SCOPE
var tabletRezzed = false; var tabletRezzed = false;
@ -25,9 +25,18 @@
var debugTablet = false; var debugTablet = false;
var tabletScalePercentage = 100.0; var tabletScalePercentage = 100.0;
UIWebTablet = null; UIWebTablet = null;
var MSECS_PER_SEC = 1000.0;
var MUTE_MICROPHONE_MENU_ITEM = "Mute Microphone";
var gTablet = null;
Script.include("../libraries/WebTablet.js"); Script.include("../libraries/WebTablet.js");
function checkTablet() {
if (gTablet === null) {
gTablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
}
}
function tabletIsValid() { function tabletIsValid() {
if (!UIWebTablet) { if (!UIWebTablet) {
return false; return false;
@ -49,7 +58,8 @@
} }
function getTabletScalePercentageFromSettings() { function getTabletScalePercentageFromSettings() {
var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; checkTablet()
var toolbarMode = gTablet.toolbarMode;
var tabletScalePercentage = DEFAULT_TABLET_SCALE; var tabletScalePercentage = DEFAULT_TABLET_SCALE;
if (!toolbarMode) { if (!toolbarMode) {
if (HMD.active) { if (HMD.active) {
@ -77,6 +87,7 @@
if (debugTablet) { if (debugTablet) {
print("TABLET rezzing"); print("TABLET rezzing");
} }
checkTablet()
tabletScalePercentage = getTabletScalePercentageFromSettings(); tabletScalePercentage = getTabletScalePercentageFromSettings();
UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml",
@ -92,7 +103,8 @@
} }
function showTabletUI() { function showTabletUI() {
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = true; checkTablet()
gTablet.tabletShown = true;
if (!tabletRezzed || !tabletIsValid()) { if (!tabletRezzed || !tabletIsValid()) {
closeTabletUI(); closeTabletUI();
@ -114,7 +126,8 @@
} }
function hideTabletUI() { function hideTabletUI() {
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false; checkTablet()
gTablet.tabletShown = false;
if (!UIWebTablet) { if (!UIWebTablet) {
return; return;
} }
@ -130,7 +143,8 @@
} }
function closeTabletUI() { function closeTabletUI() {
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false; checkTablet()
gTablet.tabletShown = false;
if (UIWebTablet) { if (UIWebTablet) {
if (UIWebTablet.onClose) { if (UIWebTablet.onClose) {
UIWebTablet.onClose(); UIWebTablet.onClose();
@ -149,17 +163,19 @@
print("TABLET closeTabletUI, UIWebTablet is null"); print("TABLET closeTabletUI, UIWebTablet is null");
} }
tabletRezzed = false; tabletRezzed = false;
gTablet = null
} }
function updateShowTablet() { function updateShowTablet() {
var MSECS_PER_SEC = 1000.0;
var now = Date.now(); var now = Date.now();
checkTablet()
// close the WebTablet if it we go into toolbar mode. // close the WebTablet if it we go into toolbar mode.
var tabletShown = Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown; var tabletShown = gTablet.tabletShown;
var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; var toolbarMode = gTablet.toolbarMode;
var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape; var landscape = gTablet.landscape;
if (tabletShown && toolbarMode) { if (tabletShown && toolbarMode) {
closeTabletUI(); closeTabletUI();
@ -167,18 +183,20 @@
return; return;
} }
//TODO: move to tablet qml?
if (tabletShown) { if (tabletShown) {
var MUTE_MICROPHONE_MENU_ITEM = "Mute Microphone";
var currentMicEnabled = !Menu.isOptionChecked(MUTE_MICROPHONE_MENU_ITEM); var currentMicEnabled = !Menu.isOptionChecked(MUTE_MICROPHONE_MENU_ITEM);
var currentMicLevel = getMicLevel(); var currentMicLevel = getMicLevel();
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); gTablet.updateMicEnabled(currentMicEnabled);
tablet.updateMicEnabled(currentMicEnabled); gTablet.updateAudioBar(currentMicLevel);
tablet.updateAudioBar(currentMicLevel);
} }
updateTabletWidthFromSettings(); if (validCheckTime - now > MSECS_PER_SEC/4) {
if (UIWebTablet) { //each 250ms should be just fine
UIWebTablet.setLandscape(landscape); updateTabletWidthFromSettings();
if (UIWebTablet) {
UIWebTablet.setLandscape(landscape);
}
} }
if (validCheckTime - now > MSECS_PER_SEC) { if (validCheckTime - now > MSECS_PER_SEC) {
@ -217,21 +235,20 @@
// also cause the stylus model to be loaded // also cause the stylus model to be loaded
var tmpStylusID = Overlays.addOverlay("model", { var tmpStylusID = Overlays.addOverlay("model", {
name: "stylus", name: "stylus",
url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx", url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx",
loadPriority: 10.0, loadPriority: 10.0,
position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})), position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2})),
dimensions: { x: 0.01, y: 0.01, z: 0.2 }, dimensions: { x: 0.01, y: 0.01, z: 0.2 },
solid: true, solid: true,
visible: true, visible: true,
ignoreRayIntersection: true, ignoreRayIntersection: true,
drawInFront: false, drawInFront: false,
lifetime: 3 lifetime: 3
}); });
Script.setTimeout(function() { Script.setTimeout(function() {
Overlays.deleteOverlay(tmpStylusID); Overlays.deleteOverlay(tmpStylusID);
}, 300); }, 300);
} else if (!tabletShown) { } else if (!tabletShown) {
hideTabletUI(); hideTabletUI();
} }
@ -246,7 +263,8 @@
} }
if (channel === "home") { if (channel === "home") {
if (UIWebTablet) { if (UIWebTablet) {
Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape = false; checkTablet()
gTablet.landscape = false;
} }
} }
} }
@ -257,30 +275,10 @@
Script.setInterval(updateShowTablet, 100); Script.setInterval(updateShowTablet, 100);
// Initialise variables used to calculate audio level
var accumulatedLevel = 0.0;
// Note: Might have to tweak the following two based on the rate we're getting the data
var AVERAGING_RATIO = 0.05;
// Calculate microphone level with the same scaling equation (log scale, exponentially averaged) in AvatarInputs and pal.js // Calculate microphone level with the same scaling equation (log scale, exponentially averaged) in AvatarInputs and pal.js
function getMicLevel() { function getMicLevel() {
var LOUDNESS_FLOOR = 11.0; //reuse already existing C++ code
var LOUDNESS_SCALE = 2.8 / 5.0; return AvatarInputs.loudnessToAudioLevel(MyAvatar.audioLoudness)
var LOG2 = Math.log(2.0);
var micLevel = 0.0;
accumulatedLevel = AVERAGING_RATIO * accumulatedLevel + (1 - AVERAGING_RATIO) * (MyAvatar.audioLoudness);
// Convert to log base 2
var logLevel = Math.log(accumulatedLevel + 1) / LOG2;
if (logLevel <= LOUDNESS_FLOOR) {
micLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE;
} else {
micLevel = (logLevel - (LOUDNESS_FLOOR - 1.0)) * LOUDNESS_SCALE;
}
if (micLevel > 1.0) {
micLevel = 1.0;
}
return micLevel;
} }
Script.scriptEnding.connect(function () { Script.scriptEnding.connect(function () {

View file

@ -42,7 +42,7 @@ const appIcon = path.join(__dirname, '../resources/console.png');
const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days
const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/;
const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-28.tar.gz"; const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC39.tar.gz";
function getBuildInfo() { function getBuildInfo() {
var buildInfoPath = null; var buildInfoPath = null;

3
tutorial/Changelog.md Normal file
View file

@ -0,0 +1,3 @@
* home-tutorial-34
* Update tutorial to only start if `HMD.active`
* Update builder's grid to use "Good - Sub-meshes" for collision shape type

View file

@ -0,0 +1,179 @@
//
// Interaction.js
// scripts/interaction
//
// Created by Trevor Berninger on 3/20/17.
// Copyright 2017 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
//
(function(){
print("loading interaction script");
var Avatar = false;
var NPC = false;
var previousNPC = false;
var hasCenteredOnNPC = false;
var distance = 10;
var r = 8;
var player = false;
var baselineX = 0;
var baselineY = 0;
var nodRange = 20;
var shakeRange = 20;
var ticker = false;
var heartbeatTimer = false;
function callOnNPC(message) {
if(NPC)
Messages.sendMessage("interactionComs", NPC + ":" + message);
else
Messages.sendMessage("interactionComs", previousNPC + ":" + message);
}
LimitlessSpeechRecognition.onFinishedSpeaking.connect(function(speech) {
print("Got: " + speech);
callOnNPC("voiceData:" + speech);
});
LimitlessSpeechRecognition.onReceivedTranscription.connect(function(speech) {
callOnNPC("speaking");
});
function setBaselineRotations(rot) {
baselineX = rot.x;
baselineY = rot.y;
}
function findLookedAtNPC() {
var intersection = AvatarList.findRayIntersection({origin: MyAvatar.position, direction: Quat.getFront(Camera.getOrientation())}, true);
if (intersection.intersects && intersection.distance <= distance){
var npcAvatar = AvatarList.getAvatar(intersection.avatarID);
if (npcAvatar.displayName.search("NPC") != -1) {
setBaselineRotations(Quat.safeEulerAngles(Camera.getOrientation()));
return intersection.avatarID;
}
}
return false;
}
function isStillFocusedNPC() {
var avatar = AvatarList.getAvatar(NPC);
if (avatar) {
var avatarPosition = avatar.position;
return Vec3.distance(MyAvatar.position, avatarPosition) <= distance && Math.abs(Quat.dot(Camera.getOrientation(), Quat.lookAtSimple(MyAvatar.position, avatarPosition))) > 0.6;
}
return false; // NPC reference died. Maybe it crashed or we teleported to a new world?
}
function onWeLostFocus() {
print("lost NPC: " + NPC);
callOnNPC("onLostFocused");
var baselineX = 0;
var baselineY = 0;
}
function onWeGainedFocus() {
print("found NPC: " + NPC);
callOnNPC("onFocused");
var rotation = Quat.safeEulerAngles(Camera.getOrientation());
baselineX = rotation.x;
baselineY = rotation.y;
LimitlessSpeechRecognition.setListeningToVoice(true);
}
function checkFocus() {
var newNPC = findLookedAtNPC();
if (NPC && newNPC != NPC && !isStillFocusedNPC()) {
onWeLostFocus();
previousNPC = NPC;
NPC = false;
}
if (!NPC && newNPC != false) {
NPC = newNPC;
onWeGainedFocus();
}
}
function checkGesture() {
var rotation = Quat.safeEulerAngles(Camera.getOrientation());
var deltaX = Math.abs(rotation.x - baselineX);
if (deltaX > 180) {
deltaX -= 180;
}
var deltaY = Math.abs(rotation.y - baselineY);
if (deltaY > 180) {
deltaY -= 180;
}
if (deltaX >= nodRange && deltaY <= shakeRange) {
callOnNPC("onNodReceived");
} else if (deltaY >= shakeRange && deltaX <= nodRange) {
callOnNPC("onShakeReceived");
}
}
function tick() {
checkFocus();
if (NPC) {
checkGesture();
}
}
function heartbeat() {
callOnNPC("beat");
}
Messages.subscribe("interactionComs");
Messages.messageReceived.connect(function (channel, message, sender) {
if(channel === "interactionComs" && player) {
var codeIndex = message.search('clientexec');
if(codeIndex != -1) {
var code = message.substr(codeIndex+11);
Script.evaluate(code, '');
}
}
});
this.enterEntity = function(id) {
player = true;
print("Something entered me: " + id);
LimitlessSpeechRecognition.setAuthKey("testKey");
if (!ticker) {
ticker = Script.setInterval(tick, 333);
}
if(!heartbeatTimer) {
heartbeatTimer = Script.setInterval(heartbeat, 1000);
}
};
this.leaveEntity = function(id) {
LimitlessSpeechRecognition.setListeningToVoice(false);
player = false;
print("Something left me: " + id);
if (previousNPC)
Messages.sendMessage("interactionComs", previousNPC + ":leftArea");
if (ticker) {
ticker.stop();
ticker = false;
}
if (heartbeatTimer) {
heartbeatTimer.stop();
heartbeatTimer = false;
}
};
this.unload = function() {
print("Okay. I'm Unloading!");
if (ticker) {
ticker.stop();
ticker = false;
}
};
print("finished loading interaction script");
});

View file

@ -0,0 +1,179 @@
//
// NPCHelpers.js
// scripts/interaction
//
// Created by Trevor Berninger on 3/20/17.
// Copyright 2017 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
//
var audioInjector = false;
var blocked = false;
var playingResponseAnim = false;
var storyURL = "";
var _qid = "start";
print("TESTTEST");
function strContains(str, sub) {
return str.search(sub) != -1;
}
function callbackOnCondition(conditionFunc, ms, callback, count) {
var thisCount = 0;
if (typeof count !== 'undefined') {
thisCount = count;
}
if (conditionFunc()) {
callback();
} else if (thisCount < 10) {
Script.setTimeout(function() {
callbackOnCondition(conditionFunc, ms, callback, thisCount + 1);
}, ms);
} else {
print("callbackOnCondition timeout");
}
}
function playAnim(animURL, looping, onFinished) {
print("got anim: " + animURL);
print("looping: " + looping);
// Start caching the animation if not already cached.
AnimationCache.getAnimation(animURL);
// Tell the avatar to animate so that we can tell if the animation is ready without crashing
Avatar.startAnimation(animURL, 30, 1, false, false, 0, 1);
// Continually check if the animation is ready
callbackOnCondition(function(){
var details = Avatar.getAnimationDetails();
// if we are running the request animation and are past the first frame, the anim is loaded properly
print("running: " + details.running);
print("url and animURL: " + details.url.trim().replace(/ /g, "%20") + " | " + animURL.trim().replace(/ /g, "%20"));
print("currentFrame: " + details.currentFrame);
return details.running && details.url.trim().replace(/ /g, "%20") == animURL.trim().replace(/ /g, "%20") && details.currentFrame > 0;
}, 250, function(){
var timeOfAnim = ((AnimationCache.getAnimation(animURL).frames.length / 30) * 1000) + 100; // frames to miliseconds plus a small buffer
print("animation loaded. length: " + timeOfAnim);
// Start the animation again but this time with frame information
Avatar.startAnimation(animURL, 30, 1, looping, true, 0, AnimationCache.getAnimation(animURL).frames.length);
if (typeof onFinished !== 'undefined') {
print("onFinished defined. setting the timeout with timeOfAnim");
timers.push(Script.setTimeout(onFinished, timeOfAnim));
}
});
}
function playSound(soundURL, onFinished) {
callbackOnCondition(function() {
return SoundCache.getSound(soundURL).downloaded;
}, 250, function() {
if (audioInjector) {
audioInjector.stop();
}
audioInjector = Audio.playSound(SoundCache.getSound(soundURL), {position: Avatar.position, volume: 1.0});
if (typeof onFinished !== 'undefined') {
audioInjector.finished.connect(onFinished);
}
});
}
function npcRespond(soundURL, animURL, onFinished) {
if (typeof soundURL !== 'undefined' && soundURL != '') {
print("npcRespond got soundURL!");
playSound(soundURL, function(){
print("sound finished");
var animDetails = Avatar.getAnimationDetails();
print("animDetails.lastFrame: " + animDetails.lastFrame);
print("animDetails.currentFrame: " + animDetails.currentFrame);
if (animDetails.lastFrame < animDetails.currentFrame + 1 || !playingResponseAnim) {
onFinished();
}
audioInjector = false;
});
}
if (typeof animURL !== 'undefined' && animURL != '') {
print("npcRespond got animURL!");
playingResponseAnim = true;
playAnim(animURL, false, function() {
print("anim finished");
playingResponseAnim = false;
print("injector: " + audioInjector);
if (!audioInjector || !audioInjector.isPlaying()) {
print("resetting Timer");
print("about to call onFinished");
onFinished();
}
});
}
}
function npcRespondBlocking(soundURL, animURL, onFinished) {
print("blocking response requested");
if (!blocked) {
print("not already blocked");
blocked = true;
npcRespond(soundURL, animURL, function(){
if (onFinished){
onFinished();
}blocked = false;
});
}
}
function npcContinueStory(soundURL, animURL, nextID, onFinished) {
if (!nextID) {
nextID = _qid;
}
npcRespondBlocking(soundURL, animURL, function(){
if (onFinished){
onFinished();
}setQid(nextID);
});
}
function setQid(newQid) {
print("setting quid");
print("_qid: " + _qid);
_qid = newQid;
print("_qid: " + _qid);
doActionFromServer("init");
}
function runOnClient(code) {
Messages.sendMessage("interactionComs", "clientexec:" + code);
}
function doActionFromServer(action, data, useServerCache) {
if (action == "start") {
ignoreCount = 0;
_qid = "start";
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://gserv_devel.studiolimitless.com/story", true);
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if (xhr.status == 200) {
print("200!");
print("evaluating: " + xhr.responseText);
Script.evaluate(xhr.responseText, "");
} else if (xhr.status == 444) {
print("Limitless Serv 444: API error: " + xhr.responseText);
} else {
print("HTTP Code: " + xhr.status + ": " + xhr.responseText);
}
}
};
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var postData = "url=" + storyURL + "&action=" + action + "&qid=" + _qid;
if (typeof data !== 'undefined' && data != '') {
postData += "&data=" + data;
}
if (typeof useServerCache !== 'undefined' && !useServerCache) {
postData += "&nocache=true";
}
print("Sending: " + postData);
xhr.send(postData);
}

View file

@ -0,0 +1,102 @@
//
// NPC_AC.js
// scripts/interaction
//
// Created by Trevor Berninger on 3/20/17.
// Copyright 2017 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
//
var currentlyUsedIndices = [];
var timers = [];
var currentlyEngaged = false;
var questionNumber = 0;
var heartbeatTimeout = false;
function getRandomRiddle() {
var randIndex = null;
do {
randIndex = Math.floor(Math.random() * 15) + 1;
} while (randIndex in currentlyUsedIndices);
currentlyUsedIndices.push(randIndex);
return randIndex.toString();
}
Script.include("https://raw.githubusercontent.com/Delamare2112/hifi/Interaction/unpublishedScripts/interaction/NPCHelpers.js", function(){
print("NPCHelpers included.");main();
});
var idleAnim = "https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/idle.fbx";
var FST = "https://s3.amazonaws.com/hifi-public/tony/fixed-sphinx/sphinx.fst";
Agent.isAvatar = true;
Avatar.skeletonModelURL = FST;
Avatar.displayName = "NPC";
Avatar.position = {x: 0.3, y: -23.4, z: 8.0};
Avatar.orientation = {x: 0, y: 1, z: 0, w: 0};
// Avatar.position = {x: 1340.3555, y: 4.078, z: -420.1562};
// Avatar.orientation = {x: 0, y: -0.707, z: 0, w: 0.707};
Avatar.scale = 2;
Messages.subscribe("interactionComs");
function endInteraction() {
print("ending interaction");
blocked = false;
currentlyEngaged = false;
if(audioInjector)
audioInjector.stop();
for (var t in timers) {
Script.clearTimeout(timers[t]);
}
if(_qid != "Restarting") {
npcRespondBlocking(
'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/EarlyExit_0' + (Math.floor(Math.random() * 2) + 1).toString() + '.wav',
'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/reversedSphinx.fbx',
function(){
Avatar.startAnimation('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx', 0);
}
);
}
}
function main() {
storyURL = "https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Sphinx.json";
Messages.messageReceived.connect(function (channel, message, sender) {
if(!strContains(message, 'beat'))
print(sender + " -> NPC @" + Agent.sessionUUID + ": " + message);
if (channel === "interactionComs" && strContains(message, Agent.sessionUUID)) {
if (strContains(message, 'beat')) {
if(heartbeatTimeout) {
Script.clearTimeout(heartbeatTimeout);
heartbeatTimeout = false;
}
heartbeatTimeout = Script.setTimeout(endInteraction, 1500);
}
else if (strContains(message, "onFocused") && !currentlyEngaged) {
blocked = false;
currentlyEngaged = true;
currentlyUsedIndices = [];
doActionFromServer("start");
} else if (strContains(message, "leftArea")) {
} else if (strContains(message, "speaking")) {
} else {
var voiceDataIndex = message.search("voiceData");
if (voiceDataIndex != -1) {
var words = message.substr(voiceDataIndex+10);
if (!isNaN(_qid) && (strContains(words, "repeat") || (strContains(words, "say") && strContains(words, "again")))) {
doActionFromServer("init");
} else {
doActionFromServer("words", words);
}
}
}
}
});
// Script.update.connect(updateGem);
Avatar.startAnimation("https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx", 0);
}

View file

@ -0,0 +1,159 @@
{
"Name": "10 Questions",
"Defaults":
{
"Actions":
{
"positive": "var x=function(){if(questionNumber>=2){setQid('Finished');return;}var suffix=['A', 'B'][questionNumber++] + '_0' + (Math.floor(Math.random() * 2) + 2).toString() + '.wav';npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/RightAnswer'+suffix, 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/RightAnswerB_02.fbx', getRandomRiddle());};x();",
"unknown": "var suffix=(Math.floor(Math.random() * 3) + 1).toString();npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/WrongAnswer_0' + suffix + '.wav','https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/WrongAnswer_0' + suffix + '.fbx', getRandomRiddle());",
"hint": "var suffix=(Math.floor(Math.random() * 2) + 1).toString();npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Hint_0' + suffix + '.wav','https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hint_0' + suffix + '.fbx')"
},
"Responses":
{
"positive": ["yes","yup","yeah","yahoo","sure","affirmative","okay","aye","right","exactly","course","naturally","unquestionably","positively","yep","definitely","certainly","fine","absolutely","positive","love","fantastic"],
"thinking": ["oh", "think about", "i know", "what was", "well", "not sure", "one before", "hold", "one moment", "one second", "1 second", "1 sec", "one sec"],
"hint": ["hint", "heads"]
}
},
"Story":
[
{
"QID": "start",
"init": "questionNumber=0;npcContinueStory('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/HiFi_Sphinx_Anim_Combined_Entrance_Audio.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx', getRandomRiddle());"
},
{
"QID": "1",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Blackboard.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Blackboard.fbx');",
"responses":
{
"positive": ["blackboard", "chalkboard", "chalk board", "slate"]
}
},
{
"QID": "2",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Breath.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Breath.fbx');",
"responses":
{
"positive": ["breath", "death"]
}
},
{
"QID": "3",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Clock.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Clock.fbx');",
"responses":
{
"positive": ["clock", "cock"]
}
},
{
"QID": "4",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Coffin.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Coffin.fbx');",
"responses":
{
"positive": ["coffin", "casket", "possum"]
}
},
{
"QID": "5",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Coin.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Coin.fbx');",
"responses":
{
"positive": ["coin", "boing", "coinage", "coin piece", "change", "join"]
}
},
{
"QID": "6",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Corn.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Corn.fbx');",
"responses":
{
"positive": ["corn", "born", "maize", "maze", "means", "torn", "horn", "worn", "porn"]
}
},
{
"QID": "7",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Darkness.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Darkness.fbx');",
"responses":
{
"positive": ["darkness", "dark", "blackness"]
}
},
{
"QID": "8",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Gloves.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Gloves.fbx');",
"responses":
{
"positive": ["gloves", "love"]
}
},
{
"QID": "9",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Gold.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Gold.fbx');",
"responses":
{
"positive": ["gold", "old", "bold", "cold", "told"]
}
},
{
"QID": "10",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_River.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_River.fbx');",
"responses":
{
"positive": ["river", "bigger", "stream", "creek", "brook"]
}
},
{
"QID": "11",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Secret.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Secret.fbx');",
"responses":
{
"positive": ["secret"]
}
},
{
"QID": "12",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Shadow.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Shadow.fbx');",
"responses":
{
"positive": ["shadow"]
}
},
{
"QID": "13",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Silence.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Silence.fbx');",
"responses":
{
"positive": ["silence", "lance", "quiet"]
}
},
{
"QID": "14",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Stairs.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Stairs.fbx');",
"responses":
{
"positive": ["stairs", "steps", "stair", "stairwell", "there's", "stairway"]
}
},
{
"QID": "15",
"init": "npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/Riddle_Umbrella.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Riddle_Umbrella.fbx');",
"responses":
{
"positive": ["umbrella"]
}
},
{
"QID": "Finished",
"init": "Script.clearTimeout(heartbeatTimeout);heartbeatTimeout = false;npcRespondBlocking('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/ScratchDialogue/ConclusionRight_02.wav', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/ConclusionRight_02.fbx', function(){runOnClient('MyAvatar.goToLocation({x: 5, y: -29, z: -63}, true, true);');setQid('Restarting');});",
"positive": "",
"negative": "",
"unknown": ""
},
{
"QID": "Restarting",
"init": "npcRespondBlocking('', 'https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/reversedSphinx.fbx', function(){Avatar.startAnimation('https://storage.googleapis.com/limitlessserv-144100.appspot.com/hifi%20assets/Animation/Hifi_Sphinx_Anim_Entrance_Kneel_Combined_with_Intro.fbx', 0);_qid='';});",
"positive": "",
"negative": "",
"unknown": ""
}
]
}