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)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip
URL_MD5 bc2861e50852dd590cdc773a14a041a7
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi8.zip
URL_MD5 b01510437ea15527156bc25cdf733bd9
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""

View file

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

View file

@ -8,6 +8,7 @@ import "../styles" as HifiStyles
import "../styles-uit"
import "../"
import "."
Item {
id: web
HifiConstants { id: hifi }
@ -22,17 +23,14 @@ Item {
property bool keyboardRaised: false
property bool punctuationMode: false
property bool isDesktop: false
property string initialPage: ""
property bool startingUp: true
property alias webView: webview
property alias profile: webview.profile
property bool remove: false
property var urlList: []
property var forwardList: []
property int currentPage: -1 // used as a model for repeater
property alias pagesModel: pagesModel
// Manage own browse history because WebEngineView history is wiped when a new URL is loaded via
// onNewViewRequested, e.g., as happens when a social media share button is clicked.
property var history: []
property int historyIndex: -1
Rectangle {
id: buttons
@ -51,21 +49,22 @@ Item {
TabletWebButton {
id: back
enabledColor: hifi.colors.baseGray
enabled: false
enabledColor: hifi.colors.darkGray
disabledColor: hifi.colors.lightGrayText
enabled: historyIndex > 0
text: "BACK"
MouseArea {
anchors.fill: parent
onClicked: goBack()
hoverEnabled: true
}
}
TabletWebButton {
id: close
enabledColor: hifi.colors.darkGray
disabledColor: hifi.colors.lightGrayText
enabled: true
text: "CLOSE"
MouseArea {
@ -75,7 +74,6 @@ Item {
}
}
RalewaySemiBold {
id: displayUrl
color: hifi.colors.baseGray
@ -90,7 +88,6 @@ Item {
}
}
MouseArea {
anchors.fill: parent
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() {
if (webview.canGoBack) {
forwardList.push(webview.url);
webview.goBack();
} 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 = [];
if (historyIndex > 0) {
historyIndex--;
loadUrl(history[historyIndex]);
}
}
@ -137,19 +115,12 @@ Item {
}
function goForward() {
if (currentPage < pagesModel.count - 1) {
currentPage++;
if (historyIndex < history.length - 1) {
historyIndex++;
loadUrl(history[historyIndex]);
}
}
function gotoPage(url) {
urlAppend(url)
}
function isUrlLoaded(url) {
return (pagesModel.get(currentPage).webUrl === url);
}
function reloadPage() {
view.reloadAndBypassCache()
view.setActiveFocusOnPress(true);
@ -161,36 +132,8 @@ Item {
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: {
loadUrl(url);
if (startingUp) {
web.initialPage = webview.url;
startingUp = false;
}
}
QtObject {
@ -258,6 +201,17 @@ Item {
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: {
keyboardRaised = false;
punctuationMode = false;
@ -277,17 +231,11 @@ Item {
}
if (WebEngineView.LoadSucceededStatus == loadRequest.status) {
if (startingUp) {
web.initialPage = webview.url;
startingUp = false;
}
webview.forceActiveFocus();
}
}
onNewViewRequested: {
var currentUrl = webview.url;
urlAppend(currentUrl);
request.openIn(webview);
}
}

View file

@ -4331,13 +4331,6 @@ void Application::update(float deltaTime) {
if (nearbyEntitiesAreReadyForPhysics()) {
_physicsEnabled = true;
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) {
@ -5461,7 +5454,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance());
}
scriptEngine->registerGlobalObject("Overlays", &_overlays);
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
// 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>();
scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data());
scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance());
qScriptRegisterMetaType(scriptEngine, OverlayIDtoScriptValue, OverlayIDfromScriptValue);

View file

@ -197,7 +197,7 @@ Menu::Menu() {
0, // QML Qt::Key_Apostrophe,
qApp, SLOT(resetSensors()));
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true,
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableAvatarCollisions, 0, true,
avatar.get(), SLOT(updateMotionBehaviorFromMenu()));
// 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 EchoLocalAudio = "Echo Local 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 EntityScriptServerLog = "Entity Script Server Log";
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
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::clearScaleRestriction);
_characterController.setEnabled(true);
_bodySensorMatrix = deriveBodyFromHMDSensor();
using namespace recording;
@ -165,12 +163,14 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) :
if (recordingInterface->getPlayFromCurrentLocation()) {
setRecordingBasis();
}
_wasCharacterControllerEnabled = _characterController.isEnabled();
_characterController.setEnabled(false);
_previousCollisionGroup = _characterController.computeCollisionGroup();
_characterController.setCollisionless(true);
} else {
clearRecordingBasis();
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
_characterController.setEnabled(_wasCharacterControllerEnabled);
if (_previousCollisionGroup != BULLET_COLLISION_GROUP_COLLISIONLESS) {
_characterController.setCollisionless(false);
}
}
auto audioIO = DependencyManager::get<AudioClient>();
@ -552,12 +552,12 @@ void MyAvatar::simulate(float deltaTime) {
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
if (entityTree) {
bool flyingAllowed = true;
bool ghostingAllowed = true;
bool collisionlessAllowed = true;
entityTree->withWriteLock([&] {
std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone();
if (zone) {
flyingAllowed = zone->getFlyingAllowed();
ghostingAllowed = zone->getGhostingAllowed();
collisionlessAllowed = zone->getGhostingAllowed();
}
auto now = usecTimestampNow();
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
@ -588,9 +588,7 @@ void MyAvatar::simulate(float deltaTime) {
}
});
_characterController.setFlyingAllowed(flyingAllowed);
if (!_characterController.isEnabled() && !ghostingAllowed) {
_characterController.setEnabled(true);
}
_characterController.setCollisionlessAllowed(collisionlessAllowed);
}
updateAvatarEntities();
@ -1449,7 +1447,8 @@ void MyAvatar::updateMotors() {
_characterController.clearMotors();
glm::quat motorRotation;
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();
} else {
// non-hovering = walking: follow camera twist about vertical but not lift
@ -1495,6 +1494,7 @@ void MyAvatar::prepareForPhysicsSimulation() {
qDebug() << "Warning: getParentVelocity failed" << getID();
parentVelocity = glm::vec3();
}
_characterController.handleChangedCollisionGroup();
_characterController.setParentVelocity(parentVelocity);
_characterController.setPositionAndOrientation(getPosition(), getOrientation());
@ -1883,8 +1883,9 @@ void MyAvatar::updateActionMotor(float deltaTime) {
glm::vec3 direction = forward + right;
CharacterController::State state = _characterController.getState();
if (state == CharacterController::State::Hover) {
// we're flying --> support vertical motion
if (state == CharacterController::State::Hover ||
_characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
// we can fly --> support vertical motion
glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
direction += up;
}
@ -1906,7 +1907,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
float finalMaxMotorSpeed = getUniformScale() * MAX_ACTION_MOTOR_SPEED;
float speedGrowthTimescale = 2.0f;
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;
if (_isPushing) {
@ -1949,9 +1950,17 @@ void MyAvatar::updatePosition(float deltaTime) {
measureMotionDerivatives(deltaTime);
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
} else {
// physics physics simulation updated elsewhere
float speed2 = glm::length2(velocity);
_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.
@ -2188,30 +2197,33 @@ void MyAvatar::updateMotionBehaviorFromMenu() {
} else {
_motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;
}
setCharacterControllerEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController));
setCollisionsEnabled(menu->isOptionChecked(MenuOption::EnableAvatarCollisions));
}
void MyAvatar::setCharacterControllerEnabled(bool enabled) {
void MyAvatar::setCollisionsEnabled(bool enabled) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setCharacterControllerEnabled", Q_ARG(bool, enabled));
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
bool ghostingAllowed = true;
auto entityTreeRenderer = qApp->getEntities();
if (entityTreeRenderer) {
std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone();
if (zone) {
ghostingAllowed = zone->getGhostingAllowed();
}
}
_characterController.setEnabled(ghostingAllowed ? enabled : true);
_characterController.setCollisionless(!enabled);
}
bool MyAvatar::getCollisionsEnabled() {
// may return 'false' even though the collisionless option was requested
// because the zone may disallow collisionless avatars
return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
}
void MyAvatar::setCharacterControllerEnabled(bool enabled) {
qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead.";
setCollisionsEnabled(enabled);
}
bool MyAvatar::getCharacterControllerEnabled() {
return _characterController.isEnabled();
qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead.";
return getCollisionsEnabled();
}
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 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.
* @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
* "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js".
*/
@ -128,6 +128,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(float isAway READ getIsAway WRITE setAway)
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 useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
@ -470,8 +471,10 @@ public:
bool hasDriveInput() const;
Q_INVOKABLE void setCharacterControllerEnabled(bool enabled);
Q_INVOKABLE bool getCharacterControllerEnabled();
Q_INVOKABLE void setCollisionsEnabled(bool enabled);
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::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override;
@ -614,7 +617,7 @@ private:
SharedSoundPointer _collisionSound;
MyCharacterController _characterController;
bool _wasCharacterControllerEnabled { true };
int16_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR };
AvatarWeakPointer _lookAtTargetAvatar;
glm::vec3 _targetAvatarPosition;

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

@ -15,11 +15,15 @@
#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 not snag on low ceilings
void MyCharacterController::RayShotgunResult::reset() {
hitFraction = 1.0f;
walkable = true;
}
MyCharacterController::MyCharacterController(MyAvatar* avatar) {
assert(avatar);
@ -30,37 +34,33 @@ MyCharacterController::MyCharacterController(MyAvatar* avatar) {
MyCharacterController::~MyCharacterController() {
}
void MyCharacterController::setDynamicsWorld(btDynamicsWorld* world) {
CharacterController::setDynamicsWorld(world);
if (world) {
initRayShotgun(world);
}
}
void MyCharacterController::updateShapeIfNecessary() {
if (_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) {
// create RigidBody if it doesn't exist
if (!_rigidBody) {
btCollisionShape* shape = computeShape();
// 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);
btCollisionShape* shape = new btCapsuleShape(_radius, 2.0f * _halfHeight);
_rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR);
} else {
btCollisionShape* shape = _rigidBody->getCollisionShape();
if (shape) {
delete shape;
}
shape = new btCapsuleShape(_radius, 2.0f * _halfHeight);
shape = computeShape();
_rigidBody->setCollisionShape(shape);
}
@ -72,12 +72,262 @@ void MyCharacterController::updateShapeIfNecessary() {
if (_state == State::Hover) {
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
} 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 {
// 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);
~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:
MyAvatar* _avatar { nullptr };
// shotgun scan data
btAlignedObjectArray<btVector3> _topPoints;
btAlignedObjectArray<btVector3> _bottomPoints;
};
#endif // hifi_MyCharacterController_h

View file

@ -37,7 +37,14 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
Head* head = _owningAvatar->getHead();
// 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);
Rig::HeadParameters headParams;
@ -152,7 +159,5 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
_rig->updateFromEyeParameters(eyeParams);
Parent::updateRig(deltaTime, parentTransform);
}

View file

@ -34,7 +34,7 @@ class AvatarInputs : public QQuickItem {
public:
static AvatarInputs* getInstance();
float loudnessToAudioLevel(float loudness);
Q_INVOKABLE float loudnessToAudioLevel(float loudness);
AvatarInputs(QQuickItem* parent = nullptr);
void update();
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 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 GUTTER_PREVIOUS_COMMAND = "<span style=\"color: #57b8bb;\">&lt;</span>";
const QString GUTTER_ERROR = "<span style=\"color: #d13b22;\">X</span>";
const QString JSConsole::_consoleFileName { "about:console" };
JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) :
QWidget(parent),
_ui(new Ui::Console),
@ -77,6 +81,8 @@ void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) {
}
if (_scriptEngine != NULL) {
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);
if (_ownScriptEngine) {
_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
_ownScriptEngine = scriptEngine == NULL;
_scriptEngine = _ownScriptEngine ? DependencyManager::get<ScriptEngines>()->loadScript(QString(), false) : scriptEngine;
_ownScriptEngine = (scriptEngine == NULL);
_scriptEngine = _ownScriptEngine ? DependencyManager::get<ScriptEngines>()->loadScript(_consoleFileName, false) : scriptEngine;
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);
}
@ -107,11 +115,10 @@ void JSConsole::executeCommand(const QString& command) {
QScriptValue JSConsole::executeCommandInWatcher(const QString& command) {
QScriptValue result;
static const QString filename = "JSConcole";
QMetaObject::invokeMethod(_scriptEngine, "evaluate", Qt::ConnectionType::BlockingQueuedConnection,
Q_RETURN_ARG(QScriptValue, result),
Q_ARG(const QString&, command),
Q_ARG(const QString&, filename));
Q_ARG(const QString&, _consoleFileName));
return result;
}
@ -134,16 +141,26 @@ void JSConsole::commandFinished() {
resetCurrentCommandHistory();
}
void JSConsole::handleError(const QString& scriptName, const QString& message) {
void JSConsole::handleError(const QString& message, const QString& scriptName) {
Q_UNUSED(scriptName);
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);
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) {
_ui->promptTextEdit->setFocus();
}

View file

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

View file

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

View file

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

View file

@ -288,3 +288,10 @@ void ModelOverlay::locationChanged(bool tellPhysics) {
_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;
virtual QString getType() const override { return TYPE; }
virtual QString getName() const override;
ModelOverlay();
ModelOverlay(const ModelOverlay* modelOverlay);

View file

@ -1097,28 +1097,27 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) {
}
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();
while (samplesNeeded > 0) {
// lock for every write to avoid locking out the device callback
// this lock is intentional - the buffer is only lock-free in its use in the device callback
RecursiveLock lock(_localAudioMutex);
// unlock between every write to allow device switching
Lock 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);
if (samplesNeeded <= 0) {
if (samplesNeeded < maxOutputSamples) {
// avoid overwriting the buffer to prevent losing frames
break;
}
@ -1168,16 +1167,18 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float));
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;
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
memset(_localScratchBuffer, 0, bytesToRead);
if (0 < injector->getLocalBuffer()->readData((char*)_localScratchBuffer, bytesToRead)) {
if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) {
if (injector->isAmbisonic()) {
@ -1317,15 +1318,17 @@ void AudioClient::setIsStereoInput(bool isStereoInput) {
}
bool AudioClient::outputLocalInjector(AudioInjector* injector) {
Lock lock(_injectorsMutex);
if (injector->getLocalBuffer() && _audioInput ) {
// just add it to the vector of active local injectors, if
// not already there.
// Since this is invoked with invokeMethod, there _should_ be
// no reason to lock access to the vector of injectors.
AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer();
if (injectorBuffer) {
// local injectors are on the AudioInjectorsThread, so we must guard access
Lock lock(_injectorsMutex);
if (!_activeLocalAudioInjectors.contains(injector)) {
qCDebug(audioclient) << "adding new injector";
_activeLocalAudioInjectors.append(injector);
// move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop())
injectorBuffer->setParent(nullptr);
injectorBuffer->moveToThread(&_localAudioThread);
} else {
qCDebug(audioclient) << "injector exists in active list already";
}
@ -1333,7 +1336,7 @@ bool AudioClient::outputLocalInjector(AudioInjector* injector) {
return true;
} else {
// no local buffer or audio
// no local buffer
return false;
}
}
@ -1452,7 +1455,7 @@ void AudioClient::outputNotify() {
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
bool supportedFormat = false;
RecursiveLock lock(_localAudioMutex);
Lock lock(_localAudioMutex);
_localSamplesAvailable.exchange(0, std::memory_order_release);
// cleanup any previously initialized device
@ -1681,8 +1684,12 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
int injectorSamplesPopped = 0;
{
RecursiveLock lock(_audio->_localAudioMutex);
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));
if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) {
_audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release);

View file

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

View file

@ -33,7 +33,11 @@ public:
PacketType packetType, QString codecName = QString(""));
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 shouldLoopbackInjectors() { return false; }
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 {
return (_state & state) == state;
}
@ -87,11 +91,7 @@ void AudioInjector::finish() {
emit finished();
if (_localBuffer) {
_localBuffer->stop();
_localBuffer->deleteLater();
_localBuffer = NULL;
}
deleteLocalBuffer();
if (stateHas(AudioInjectorState::PendingDelete)) {
// we've been asked to delete after finishing, trigger a deleteLater here
@ -163,7 +163,7 @@ bool AudioInjector::injectLocally() {
if (_localAudioInterface) {
if (_audioData.size() > 0) {
_localBuffer = new AudioInjectorLocalBuffer(_audioData, this);
_localBuffer = new AudioInjectorLocalBuffer(_audioData);
_localBuffer->open(QIODevice::ReadOnly);
_localBuffer->setShouldLoop(_options.loop);
@ -172,7 +172,8 @@ bool AudioInjector::injectLocally() {
_localBuffer->setCurrentOffset(_currentSendOffset);
// 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) {
qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface";
@ -185,6 +186,14 @@ bool AudioInjector::injectLocally() {
return success;
}
void AudioInjector::deleteLocalBuffer() {
if (_localBuffer) {
_localBuffer->stop();
_localBuffer->deleteLater();
_localBuffer = nullptr;
}
}
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_IMMEDIATELY = 0;

View file

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

View file

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

View file

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

View file

@ -73,12 +73,13 @@ void SkeletonModel::initJointStates() {
// Called within Model::simulate call, below.
void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
assert(!_owningAvatar->isMyAvatar());
const FBXGeometry& geometry = getFBXGeometry();
Head* head = _owningAvatar->getHead();
// 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();
float focusDistance = glm::length(focusOffset);
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;
}
if (!_owningAvatar->isMyAvatar()) {
// no need to call Model::updateRig() because otherAvatars get their joint state
// copied directly from AvtarData::_jointData (there are no Rig animations to blend)
_needsUpdateClusterMatrices = true;
// no need to call Model::updateRig() because otherAvatars get their joint state
// copied directly from AvtarData::_jointData (there are no Rig animations to blend)
_needsUpdateClusterMatrices = true;
// 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
// 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()) {...
// 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.
// 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
// 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()) {...
// 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.
// If the head is not positioned, updateEyeJoints won't get the math right
glm::quat headOrientation;
_rig->getJointRotation(geometry.headJointIndex, headOrientation);
glm::vec3 eulers = safeEulerAngles(headOrientation);
head->setBasePitch(glm::degrees(-eulers.x));
head->setBaseYaw(glm::degrees(eulers.y));
head->setBaseRoll(glm::degrees(-eulers.z));
// If the head is not positioned, updateEyeJoints won't get the math right
glm::quat headOrientation;
_rig->getJointRotation(geometry.headJointIndex, headOrientation);
glm::vec3 eulers = safeEulerAngles(headOrientation);
head->setBasePitch(glm::degrees(-eulers.x));
head->setBaseYaw(glm::degrees(eulers.y));
head->setBaseRoll(glm::degrees(-eulers.z));
Rig::EyeParameters eyeParams;
eyeParams.eyeLookAt = lookAt;
eyeParams.eyeSaccade = glm::vec3(0.0f);
eyeParams.modelRotation = getRotation();
eyeParams.modelTranslation = getTranslation();
eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex;
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
Rig::EyeParameters eyeParams;
eyeParams.eyeLookAt = lookAt;
eyeParams.eyeSaccade = glm::vec3(0.0f);
eyeParams.modelRotation = getRotation();
eyeParams.modelTranslation = getTranslation();
eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex;
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
_rig->updateFromEyeParameters(eyeParams);
}
// evaluate AnimGraph animation and update jointStates.
Model::updateRig(deltaTime, parentTransform);
_rig->updateFromEyeParameters(eyeParams);
}
void SkeletonModel::updateAttitude() {
@ -136,7 +132,7 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) {
if (fullUpdate) {
setBlendshapeCoefficients(_owningAvatar->getHead()->getSummedBlendshapeCoefficients());
Model::simulate(deltaTime, fullUpdate);
Parent::simulate(deltaTime, fullUpdate);
// let rig compute the model offset
glm::vec3 registrationPoint;
@ -144,7 +140,7 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) {
setOffset(registrationPoint);
}
} else {
Model::simulate(deltaTime, fullUpdate);
Parent::simulate(deltaTime, fullUpdate);
}
if (!isActive() || !_owningAvatar->isMyAvatar()) {

View file

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

View file

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

View file

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

View file

@ -407,9 +407,11 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
// return QUuid();
// }
bool entityFound { false };
_entityTree->withReadLock([&] {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
if (entity) {
entityFound = true;
// make sure the properties has a type, so that the encode can know which properties to include
properties.setType(entity->getType());
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);
return id;
}
@ -1515,6 +1538,24 @@ bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) {
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> result;
if (!_entityTree) {

View file

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

View file

@ -990,6 +990,17 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
entityItemID, properties);
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()) {
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
// an existing entity... handle appropriately
if (validEditPacket) {
// search for the entity by EntityItemID
startLookup = usecTimestampNow();
EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID);
endLookup = usecTimestampNow();
startFilter = usecTimestampNow();
bool wasChanged = false;
// 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) {
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);
GLintptr rangeStart = batch._params[paramOffset + 1]._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) {
GLuint slot = batch._params[paramOffset + 1]._uint;
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;
}
@ -233,7 +237,7 @@ void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) {
void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) {
GLuint slot = batch._params[paramOffset + 1]._uint;
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;
}

View file

@ -19,8 +19,8 @@
#include <DependencyManager.h>
#ifdef Q_OS_WIN
#include <comdef.h>
#include <Wbemidl.h>
#include <Windows.h>
#include <winreg.h>
#endif //Q_OS_WIN
#ifdef Q_OS_MAC
@ -30,6 +30,9 @@
#endif //Q_OS_MAC
static const QString FALLBACK_FINGERPRINT_KEY = "fallbackFingerprint";
QUuid FingerprintUtils::_machineFingerprint { QUuid() };
QString FingerprintUtils::getMachineFingerprintString() {
QString uuidString;
#ifdef Q_OS_LINUX
@ -47,122 +50,32 @@ QString FingerprintUtils::getMachineFingerprintString() {
#endif //Q_OS_MAC
#ifdef Q_OS_WIN
HRESULT hres;
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;
}
HKEY cryptoKey;
// initialize WbemLocator
hres = CoCreateInstance(
CLSID_WbemLocator,
0,
CLSCTX_INPROC_SERVER,
IID_IWbemLocator, (LPVOID *) &pLoc);
// try and open the key that contains the machine GUID
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &cryptoKey) == ERROR_SUCCESS) {
DWORD type;
DWORD guidSize;
if (FAILED(hres)) {
qCDebug(networking) << "Failed to initialize WbemLocator";
return uuidString;
}
// Connect to WMI through the IWbemLocator::ConnectServer method
IWbemServices *pSvc = NULL;
const char* MACHINE_GUID_KEY = "MachineGuid";
// Connect to the root\cimv2 namespace with
// the current user and obtain pointer pSvc
// to make IWbemServices calls.
hres = pLoc->ConnectServer(
_bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace
NULL, // User name. NULL = current user
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
);
// try and retrieve the size of the GUID value
if (RegQueryValueEx(cryptoKey, MACHINE_GUID_KEY, NULL, &type, NULL, &guidSize) == ERROR_SUCCESS) {
// make sure that the value is a string
if (type == REG_SZ) {
// retrieve the machine GUID and return that as our UUID string
std::string machineGUID(guidSize / sizeof(char), '\0');
if (FAILED(hres)) {
pLoc->Release();
qCDebug(networking) << "Failed to connect to WMI";
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;
if (RegQueryValueEx(cryptoKey, MACHINE_GUID_KEY, NULL, NULL,
reinterpret_cast<LPBYTE>(&machineGUID[0]), &guidSize) == ERROR_SUCCESS) {
uuidString = QString::fromStdString(machineGUID);
}
}
}
VariantClear(&vtProp);
pclsObj->Release();
RegCloseKey(cryptoKey);
}
pEnumerator->Release();
// Cleanup
pSvc->Release();
pLoc->Release();
qCDebug(networking) << "Windows BIOS UUID: " << uuidString;
#endif //Q_OS_WIN
return uuidString;
@ -171,29 +84,36 @@ QString FingerprintUtils::getMachineFingerprintString() {
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()) {
// 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();
// 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()) {
// 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:
static QString getMachineFingerprintString();
static QUuid _machineFingerprint;
};
#endif // hifi_FingerprintUtils_h

View file

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

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

@ -1,6 +1,6 @@
//
// CharacterControllerInterface.cpp
// libraries/physcis/src
// libraries/physics/src
//
// Created by Andrew Meadows 2015.10.21
// Copyright 2015 High Fidelity, Inc.
@ -13,8 +13,8 @@
#include <NumericalConstants.h>
#include "PhysicsCollisionGroups.h"
#include "ObjectMotionState.h"
#include "PhysicsHelpers.h"
#include "PhysicsLogging.h"
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() {
_halfHeight = 1.0f;
_enabled = false;
_floorDistance = MAX_FALL_HEIGHT;
_targetVelocity.setValue(0.0f, 0.0f, 0.0f);
@ -107,6 +103,7 @@ bool CharacterController::needsAddition() const {
void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
if (_dynamicsWorld != world) {
// remove from old world
if (_dynamicsWorld) {
if (_rigidBody) {
_dynamicsWorld->removeRigidBody(_rigidBody);
@ -114,17 +111,23 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
}
_dynamicsWorld = nullptr;
}
int16_t collisionGroup = computeCollisionGroup();
if (world && _rigidBody) {
// add to new world
_dynamicsWorld = world;
_pendingFlags &= ~PENDING_FLAG_JUMP;
// Before adding the RigidBody to the world we must save its oldGravity to the side
// 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->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
_dynamicsWorld->addAction(this);
// restore gravity settings
_rigidBody->setGravity(oldGravity);
// restore gravity settings because adding an object to the world overwrites its gravity setting
_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 (_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++) {
btPersistentManifold* contactManifold = collisionWorld->getDispatcher()->getManifoldByIndexInternal(i);
const btCollisionObject* obA = static_cast<const btCollisionObject*>(contactManifold->getBody0());
const btCollisionObject* obB = static_cast<const btCollisionObject*>(contactManifold->getBody1());
if (obA == _rigidBody || obB == _rigidBody) {
btPersistentManifold* contactManifold = dispatcher->getManifoldByIndexInternal(i);
if (_rigidBody == contactManifold->getBody1() || _rigidBody == contactManifold->getBody0()) {
bool characterIsFirst = _rigidBody == contactManifold->getBody0();
int numContacts = contactManifold->getNumContacts();
int stepContactIndex = -1;
float highestStep = _minStepHeight;
for (int j = 0; j < numContacts; j++) {
btManifoldPoint& pt = contactManifold->getContactPoint(j);
// check to see if contact point is touching the bottom sphere of the capsule.
// and the contact normal is not slanted too much.
float contactPointY = (obA == _rigidBody) ? pt.m_localPointA.getY() : pt.m_localPointB.getY();
btVector3 normal = (obA == _rigidBody) ? pt.m_normalWorldOnB : -pt.m_normalWorldOnB;
if (contactPointY < -_halfHeight && normal.dot(_currentUp) > COS_PI_OVER_THREE) {
return true;
// check for "floor"
btManifoldPoint& contact = contactManifold->getContactPoint(j);
btVector3 pointOnCharacter = characterIsFirst ? contact.m_localPointA : contact.m_localPointB; // object-local-frame
btVector3 normal = characterIsFirst ? contact.m_normalWorldOnB : -contact.m_normalWorldOnB; // points toward character
btScalar hitHeight = _halfHeight + _radius + pointOnCharacter.dot(_currentUp);
if (hitHeight < _maxStepHeight && normal.dot(_currentUp) > _minFloorNormalDotUp) {
hasFloor = 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) {
// 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
btVector3 rayStart = xform.getOrigin() - _halfHeight * _currentUp;
btVector3 rayStart = transform.getOrigin() - _halfHeight * _currentUp;
// rayEnd is some short distance outside bottom sphere
const btScalar FLOOR_PROXIMITY_THRESHOLD = 0.3f * _radius;
@ -183,21 +226,16 @@ void CharacterController::preStep(btCollisionWorld* collisionWorld) {
if (rayCallback.hasHit()) {
_floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius;
}
_hasSupport = checkForSupport(collisionWorld);
}
const btScalar MIN_TARGET_SPEED = 0.001f;
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;
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 MAX_DISPLACEMENT = 0.5f * _radius;
@ -231,6 +269,47 @@ void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) {
_rigidBody->setWorldTransform(btTransform(endRot, endPos));
}
_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() {
@ -272,95 +351,100 @@ void CharacterController::setState(State desiredState) {
#ifdef DEBUG_STATE_CHANGE
qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason;
#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;
updateGravity();
}
}
void CharacterController::setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale) {
_boxScale = scale;
void CharacterController::updateGravity() {
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;
float z = _boxScale.z;
void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale) {
float x = scale.x;
float z = scale.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;
if (halfHeight < MIN_HALF_HEIGHT) {
halfHeight = MIN_HALF_HEIGHT;
}
// compare dimensions
float radiusDelta = glm::abs(radius - _radius);
float heightDelta = glm::abs(halfHeight - _halfHeight);
if (radiusDelta < FLT_EPSILON && heightDelta < FLT_EPSILON) {
// shape hasn't changed --> nothing to do
} else {
if (glm::abs(radius - _radius) > FLT_EPSILON || glm::abs(halfHeight - _halfHeight) > FLT_EPSILON) {
_radius = radius;
_halfHeight = halfHeight;
const btScalar DEFAULT_MIN_STEP_HEIGHT_FACTOR = 0.005f;
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) {
// must REMOVE from world prior to shape update
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION;
}
_pendingFlags |= PENDING_FLAG_UPDATE_SHAPE;
// only need to ADD back when we happen to be enabled
if (_enabled) {
_pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION;
}
_pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION;
}
// 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) {
if (enabled != _enabled) {
if (enabled) {
// Don't bother clearing REMOVE bit since it might be paired with an UPDATE_SHAPE bit.
// 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) {
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION;
}
_pendingFlags &= ~ PENDING_FLAG_ADD_TO_SIMULATION;
void CharacterController::setCollisionless(bool collisionless) {
if (collisionless != _collisionless) {
_collisionless = collisionless;
_pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP;
}
}
int16_t CharacterController::computeCollisionGroup() const {
if (_collisionless) {
return _collisionlessAllowed ? BULLET_COLLISION_GROUP_COLLISIONLESS : BULLET_COLLISION_GROUP_MY_AVATAR;
} 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");
_enabled = enabled;
_pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP;
updateGravity();
}
}
void CharacterController::updateUpAxis(const glm::quat& rotation) {
btVector3 oldUp = _currentUp;
_currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS);
if (_state != State::Hover) {
const btScalar MIN_UP_ERROR = 0.01f;
if (oldUp.distance(_currentUp) > MIN_UP_ERROR) {
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
}
if (_state != State::Hover && _rigidBody) {
_rigidBody->setGravity(_gravity * _currentUp);
}
}
void CharacterController::setPositionAndOrientation(
const glm::vec3& position,
const glm::quat& orientation) {
// TODO: update gravity if up has changed
updateUpAxis(orientation);
btQuaternion bodyOrientation = glmToBullet(orientation);
btVector3 bodyPosition = glmToBullet(position + orientation * _shapeLocalOffset);
_characterBodyTransform = btTransform(bodyOrientation, bodyPosition);
_rotation = glmToBullet(orientation);
_position = glmToBullet(position + orientation * _shapeLocalOffset);
}
void CharacterController::getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const {
if (_enabled && _rigidBody) {
if (_rigidBody) {
const btTransform& avatarTransform = _rigidBody->getWorldTransform();
rotation = bulletToGLM(avatarTransform.getRotation());
position = bulletToGLM(avatarTransform.getOrigin()) - rotation * _shapeLocalOffset;
@ -428,16 +512,19 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
btScalar angle = motor.rotation.getAngle();
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
btScalar tau = dt / motor.hTimescale;
if (tau > 1.0f) {
tau = 1.0f;
}
velocity += (motor.velocity - velocity) * tau;
velocity += tau * (motor.velocity - velocity);
// rotate back into world-frame
velocity = velocity.rotate(axis, angle);
_targetVelocity += (tau * motor.velocity).rotate(axis, angle);
// store the velocity and weight
velocities.push_back(velocity);
@ -445,12 +532,26 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
} else {
// compute local UP
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
btVector3 vVelocity = velocity.dot(up) * up;
btVector3 hVelocity = velocity - vVelocity;
btVector3 vTargetVelocity = motor.velocity.dot(up) * up;
btVector3 hTargetVelocity = motor.velocity - vTargetVelocity;
btVector3 vMotorVelocity = motorVelocity.dot(up) * up;
btVector3 hMotorVelocity = motorVelocity - vMotorVelocity;
// modify each component separately
btScalar maxTau = 0.0f;
@ -460,7 +561,7 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
tau = 1.0f;
}
maxTau = tau;
hVelocity += (hTargetVelocity - hVelocity) * tau;
hVelocity += (hMotorVelocity - hVelocity) * tau;
}
if (motor.vTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) {
btScalar tau = dt / motor.vTimescale;
@ -470,11 +571,12 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
if (tau > maxTau) {
maxTau = tau;
}
vVelocity += (vTargetVelocity - vVelocity) * tau;
vVelocity += (vMotorVelocity - vVelocity) * tau;
}
// add components back together and rotate into world-frame
velocity = (hVelocity + vVelocity).rotate(axis, angle);
_targetVelocity += maxTau * (hTargetVelocity + vTargetVelocity).rotate(axis, angle);
// store velocity and weights
velocities.push_back(velocity);
@ -492,6 +594,8 @@ void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) {
velocities.reserve(_motors.size());
std::vector<btScalar> weights;
weights.reserve(_motors.size());
_targetVelocity = btVector3(0.0f, 0.0f, 0.0f);
_steppingUp = false;
for (int i = 0; i < (int)_motors.size(); ++i) {
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) {
velocity += (weights[i] / totalWeight) * velocities[i];
}
_targetVelocity /= totalWeight;
}
if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) {
velocity = btVector3(0.0f, 0.0f, 0.0f);
}
// 'thrust' is applied at the very end
_targetVelocity += 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) {
@ -523,57 +631,60 @@ void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) {
velocity = bulletToGLM(btVelocity);
}
void CharacterController::preSimulation() {
if (_enabled && _dynamicsWorld && _rigidBody) {
quint64 now = usecTimestampNow();
void CharacterController::updateState() {
if (!_dynamicsWorld) {
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
_rigidBody->setWorldTransform(_characterBodyTransform);
btVector3 velocity = _rigidBody->getLinearVelocity();
_preSimulationVelocity = velocity;
// scan for distant floor
// rayStart is at center of bottom sphere
btVector3 rayStart = _position;
// scan for distant floor
// rayStart is at center of bottom sphere
btVector3 rayStart = _characterBodyTransform.getOrigin();
btScalar rayLength = _radius;
int16_t collisionGroup = computeCollisionGroup();
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
btScalar rayLength = _radius + MAX_FALL_HEIGHT;
btVector3 rayEnd = rayStart - rayLength * _currentUp;
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;
const btScalar MAX_WALKING_SPEED = 2.5f;
ClosestNotMe rayCallback(_rigidBody);
rayCallback.m_closestHitFraction = 1.0f;
_dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback);
bool rayHasHit = rayCallback.hasHit();
quint64 now = usecTimestampNow();
if (rayHasHit) {
_rayHitStartTime = now;
_floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight);
} else {
const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND;
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) {
if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) {
rayHasHit = true;
} else {
_floorDistance = FLT_MAX;
}
}
// record a time stamp when the jump button was first pressed.
if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) {
if (_pendingFlags & PENDING_FLAG_JUMP) {
_jumpButtonDownStartTime = now;
_jumpButtonDownCount++;
}
// record a time stamp when the jump button was first pressed.
bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP;
if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) {
if (_pendingFlags & PENDING_FLAG_JUMP) {
_jumpButtonDownStartTime = now;
_jumpButtonDownCount++;
}
}
bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP;
btVector3 actualHorizVelocity = velocity - velocity.dot(_currentUp) * _currentUp;
bool flyingFast = _state == State::Hover && actualHorizVelocity.length() > (MAX_WALKING_SPEED * 0.75f);
btVector3 velocity = _preSimulationVelocity;
// disable normal state transitions while collisionless
const btScalar MAX_WALKING_SPEED = 2.65f;
if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) {
switch (_state) {
case State::Ground:
if (!rayHasHit && !_hasSupport) {
@ -613,6 +724,9 @@ void CharacterController::preSimulation() {
break;
}
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) {
SET_STATE(State::InAir, "near ground");
} else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
@ -620,6 +734,28 @@ void CharacterController::preSimulation() {
}
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;
@ -631,14 +767,11 @@ void CharacterController::preSimulation() {
}
void CharacterController::postSimulation() {
// postSimulation() exists for symmetry and just in case we need to do something here later
if (_enabled && _dynamicsWorld && _rigidBody) {
btVector3 velocity = _rigidBody->getLinearVelocity();
_velocityChange = velocity - _preSimulationVelocity;
if (_rigidBody) {
_velocityChange = _rigidBody->getLinearVelocity() - _preSimulationVelocity;
}
}
bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) {
if (!_rigidBody) {
return false;
@ -651,11 +784,17 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio
}
void CharacterController::setFlyingAllowed(bool value) {
if (_flyingAllowed != value) {
if (value != _flyingAllowed) {
_flyingAllowed = value;
if (!_flyingAllowed && _state == State::Hover) {
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
// libraries/physcis/src
// libraries/physics/src
//
// Created by Andrew Meadows 2015.10.21
// Copyright 2015 High Fidelity, Inc.
@ -9,8 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_CharacterControllerInterface_h
#define hifi_CharacterControllerInterface_h
#ifndef hifi_CharacterController_h
#define hifi_CharacterController_h
#include <assert.h>
#include <stdint.h>
@ -19,14 +19,18 @@
#include <BulletDynamics/Character/btCharacterControllerInterface.h>
#include <GLMHelpers.h>
#include <NumericalConstants.h>
#include <PhysicsCollisionGroups.h>
#include "BulletUtil.h"
#include "CharacterGhostObject.h"
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_UPDATE_SHAPE = 1U << 2;
const uint32_t PENDING_FLAG_JUMP = 1U << 3;
const float DEFAULT_CHARACTER_GRAVITY = -5.0f;
const uint32_t PENDING_FLAG_UPDATE_COLLISION_GROUP = 1U << 4;
const float DEFAULT_MIN_FLOOR_NORMAL_DOT_UP = cosf(PI / 3.0f);
class btRigidBody;
class btCollisionWorld;
@ -44,7 +48,7 @@ public:
bool needsRemoval() const;
bool needsAddition() const;
void setDynamicsWorld(btDynamicsWorld* world);
virtual void setDynamicsWorld(btDynamicsWorld* world);
btCollisionObject* getCollisionObject() { return _rigidBody; }
virtual void updateShapeIfNecessary() = 0;
@ -56,10 +60,7 @@ public:
virtual void warp(const btVector3& origin) override { }
virtual void debugDraw(btIDebugDraw* debugDrawer) override { }
virtual void setUpInterpolate(bool value) override { }
virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override {
preStep(collisionWorld);
playerStep(collisionWorld, deltaTime);
}
virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override;
virtual void preStep(btCollisionWorld *collisionWorld) override;
virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override;
virtual bool canJump() const override { assert(false); return false; } // never call this
@ -69,6 +70,7 @@ public:
void clearMotors();
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 setStepUpEnabled(bool enabled) { _stepUpEnabled = enabled; }
void computeNewVelocity(btScalar dt, btVector3& velocity);
void computeNewVelocity(btScalar dt, glm::vec3& velocity);
@ -103,16 +105,20 @@ public:
};
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
void setEnabled(bool enabled);
bool isEnabledAndReady() const { return _enabled && _dynamicsWorld; }
bool isEnabledAndReady() const { return _dynamicsWorld; }
void setCollisionless(bool collisionless);
int16_t computeCollisionGroup() const;
void handleChangedCollisionGroup();
bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation);
void setFlyingAllowed(bool value);
void setCollisionlessAllowed(bool value);
protected:
@ -122,8 +128,9 @@ protected:
void setState(State state);
#endif
void updateGravity();
void updateUpAxis(const glm::quat& rotation);
bool checkForSupport(btCollisionWorld* collisionWorld) const;
bool checkForSupport(btCollisionWorld* collisionWorld);
protected:
struct CharacterMotor {
@ -136,6 +143,7 @@ protected:
};
std::vector<CharacterMotor> _motors;
CharacterGhostObject _ghost;
btVector3 _currentUp;
btVector3 _targetVelocity;
btVector3 _parentVelocity;
@ -144,6 +152,8 @@ protected:
btTransform _followDesiredBodyTransform;
btScalar _followTimeRemaining;
btTransform _characterBodyTransform;
btVector3 _position;
btQuaternion _rotation;
glm::vec3 _shapeLocalOffset;
@ -155,13 +165,23 @@ protected:
quint32 _jumpButtonDownCount;
quint32 _takeoffJumpButtonID;
btScalar _halfHeight;
btScalar _radius;
// data for walking up steps
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;
bool _stepUpEnabled { true };
bool _hasSupport;
btScalar _gravity;
btScalar _gravity { 0.0f };
btScalar _jumpSpeed;
btScalar _followTime;
@ -169,7 +189,6 @@ protected:
btQuaternion _followAngularDisplacement;
btVector3 _linearAcceleration;
std::atomic_bool _enabled;
State _state;
bool _isPushingUp;
@ -179,6 +198,8 @@ protected:
uint32_t _previousFlags { 0 };
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
// libraries/physcis/src
// libraries/physics/src
//
// Created by Andrew Meadows 2016.07.13
// Copyright 2016 High Fidelity, Inc.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,9 +16,10 @@
#include <QObject>
#include <QString>
#include <QtScript/QScriptable>
/// Scriptable Mat4 object. Used exclusively in the JavaScript API
class Mat4 : public QObject {
class Mat4 : public QObject, protected QScriptable {
Q_OBJECT
public slots:
@ -43,7 +44,7 @@ public slots:
glm::vec3 getRight(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

View file

@ -15,7 +15,9 @@
#include <OctreeConstants.h>
#include <GLMHelpers.h>
#include <glm/gtx/string_cast.hpp>
#include "ScriptEngineLogging.h"
#include "ScriptEngine.h"
#include "Quat.h"
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);
}
void Quat::print(const QString& label, const glm::quat& q) {
qCDebug(scriptengine) << qPrintable(label) << q.x << "," << q.y << "," << q.z << "," << q.w;
void Quat::print(const QString& label, const glm::quat& q, bool asDegrees) {
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) {

View file

@ -18,6 +18,7 @@
#include <QObject>
#include <QString>
#include <QtScript/QScriptable>
/**jsdoc
* A Quaternion
@ -30,7 +31,7 @@
*/
/// 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
public slots:
@ -58,7 +59,7 @@ public slots:
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);
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);
glm::quat cancelOutRollAndPitch(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();
}
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?
engine->globalObject().property("Script").property("print")
.call(engine->nullValue(), QScriptValueList({ message }));
if (ScriptEngine *scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
scriptEngine->print(message);
}
return QScriptValue();
}
@ -472,6 +472,11 @@ void ScriptEngine::scriptInfoMessage(const QString& message) {
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
// callAnimationStateHandler requires that the type be registered.
// 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 scriptWarningMessage(const QString& message);
void scriptInfoMessage(const QString& message);
void scriptPrintedMessage(const QString& message);
int getNumRunningEntityScripts() 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() != "https" &&
scriptFilename.scheme() != "atp" &&
scriptFilename.scheme() != "file")) {
scriptFilename.scheme() != "file" &&
scriptFilename.scheme() != "about")) {
// deal with a "url" like c:/something
scriptUrl = normalizeScriptURL(QUrl::fromLocalFile(scriptFilename.toString()));
} else {
@ -472,7 +473,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL
}, Qt::QueuedConnection);
if (scriptFilename.isEmpty()) {
if (scriptFilename.isEmpty() || !scriptUrl.isValid()) {
launchScriptEngine(scriptEngine);
} else {
// connect to the appropriate signals of this script engine

View file

@ -14,6 +14,7 @@
#include <QDebug>
#include "ScriptEngineLogging.h"
#include "ScriptEngine.h"
#include "ScriptUUID.h"
QUuid ScriptUUID::fromString(const QString& s) {
@ -36,6 +37,11 @@ bool ScriptUUID::isNull(const QUuid& id) {
return id.isNull();
}
void ScriptUUID::print(const QString& lable, const QUuid& id) {
qCDebug(scriptengine) << qPrintable(lable) << id.toString();
void ScriptUUID::print(const QString& label, const QUuid& id) {
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
#include <QUuid>
#include <QtScript/QScriptable>
/// 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
public slots:
@ -26,7 +27,7 @@ public slots:
QUuid generate();
bool isEqual(const QUuid& idA, const QUuid& idB);
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

View file

@ -14,20 +14,26 @@
#include <QDebug>
#include <GLMHelpers.h>
#include <glm/gtx/string_cast.hpp>
#include "ScriptEngineLogging.h"
#include "NumericalConstants.h"
#include "Vec3.h"
#include "ScriptEngine.h"
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));
return glm::degrees(radians);
}
void Vec3::print(const QString& lable, const glm::vec3& v) {
qCDebug(scriptengine) << qPrintable(lable) << v.x << "," << v.y << "," << v.z;
void Vec3::print(const QString& label, const glm::vec3& v) {
QString message = QString("%1 %2").arg(qPrintable(label));
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) {

View file

@ -17,6 +17,7 @@
#include <QtCore/QObject>
#include <QtCore/QString>
#include <QtScript/QScriptable>
#include "GLMHelpers.h"
@ -48,7 +49,7 @@
*/
/// 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_PROPERTY(glm::vec3 UNIT_X READ UNIT_X CONSTANT)
Q_PROPERTY(glm::vec3 UNIT_Y READ UNIT_Y CONSTANT)

View file

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

View file

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

View file

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

View file

@ -1138,3 +1138,17 @@ SpatiallyNestablePointer SpatiallyNestable::findByID(QUuid id, bool& success) {
}
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 void setID(const QUuid& id);
virtual QString getName() const { return "SpatiallyNestable"; }
virtual const QUuid getParentID() const;
virtual void setParentID(const QUuid& parentID);
@ -62,6 +64,8 @@ public:
static glm::vec3 localToWorldAngularVelocity(const glm::vec3& angularVelocity,
const QUuid& parentID, int parentJointIndex, bool& success);
static QString nestableTypeToString(NestableType nestableType);
// world frame
virtual const Transform getTransform(bool& success, int depth = 0) const;
virtual const Transform getTransform() const;

View file

@ -20,9 +20,10 @@
print('<span style="color:red">Tests completed with ' +
errorCount + ' ' + ERROR + '.<span>');
}
if (pending.length)
if (pending.length) {
print ('<span style="color:darkorange">disabled: <br />&nbsp;&nbsp;&nbsp;'+
pending.join('<br />&nbsp;&nbsp;&nbsp;')+'</span>');
}
print('Tests completed in ' + (endTime - startTime) + 'ms.');
};
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
// 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 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 () {
// backup/restore current skeletonModelURL
beforeAll(function() {
this.oldURL = MyAvatar.skeletonModelURL;
});
afterAll(function() {
MyAvatar.skeletonModelURL = this.oldURL;
});
// reload the avatar from scratch before each test.
beforeEach(function (done) {
MyAvatar.skeletonModelURL = DEFAULT_AVATAR_URL;
@ -20,12 +31,12 @@ describe("MyAvatar", function () {
MyAvatar.position = ORIGIN;
MyAvatar.orientation = ROT_IDENT;
// give the avatar 1/2 a second to settle on the ground in the idle pose.
Script.setTimeout(function () {
Script.setTimeout(function () {
done();
}, 500);
}
}, 500);
});
}, 10000 /* timeout -- allow time to download avatar*/);
// makes the assumption that there is solid ground somewhat underneath the avatar.
it("position and orientation getters", function () {

View file

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

View file

@ -1,3 +1,5 @@
/* eslint-env jasmine */
describe('Entity', function() {
var center = Vec3.sum(
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() {
boxEntity = Entities.addEntity(boxProps);
});
@ -62,4 +72,4 @@ describe('Entity', function() {
props = Entities.getEntityProperties(boxEntity);
expect(props.lastEdited).toBeGreaterThan(prevLastEdited);
});
});
});

View file

@ -1,13 +1,30 @@
/* eslint-env jasmine */
// Include testing library
Script.include('../../libraries/jasmine/jasmine.js');
Script.include('../../libraries/jasmine/hifi-boot.js')
Script.include('../../libraries/jasmine/hifi-boot.js');
// 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('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
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.
// 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.
var childType = Entities.getNestableType(childID);
if (_this.previousParentID[childID]) {
var previousParentID = _this.previousParentID[childID];
var previousParentJointIndex = _this.previousParentJointIndex[childID];
@ -3898,7 +3899,7 @@ function MyController(hand) {
}
_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
// used in tutorial.
Overlays.editOverlay(childID, {
@ -3906,12 +3907,20 @@ function MyController(hand) {
parentJointIndex: previousParentJointIndex
});
}
Entities.editEntity(childID, { parentID: previousParentID, parentJointIndex: previousParentJointIndex });
if (childType == "entity") {
Entities.editEntity(childID, {
parentID: previousParentID,
parentJointIndex: previousParentJointIndex
});
}
} else {
Entities.editEntity(childID, { parentID: NULL_UUID });
if (Overlays.getProperty(childID, "grabbable")) {
Overlays.editOverlay(childID, { parentID: NULL_UUID });
if (childType == "entity") {
Entities.editEntity(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() {
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) {
@ -338,7 +339,8 @@ WebTablet.prototype.geometryChanged = function (geometry) {
// compute position, rotation & parentJointIndex of the tablet
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 = {};
// compute position, rotation & parentJointIndex of the tablet
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.
// This should make the text more readable.
@ -530,7 +533,8 @@ WebTablet.prototype.cameraModeChanged = function (newMode) {
var tabletProperties = {};
// compute position, rotation & parentJointIndex of the tablet
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() {
var stateString = "<" + STATE_STRINGS[state] + ">";
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)));
}
@ -759,7 +760,10 @@
break;
case "done":
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
// connecting
if (message.connectionId !== MyAvatar.sessionUUID) {
@ -768,11 +772,20 @@
// value for isKeyboard, as we should not change the animation
// state anyways (if any)
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 {
// if waiting or inactive, lets clear the connecting id. If in makingConnection,
// do nothing
if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) {
if (state == STATES.MAKING_CONNECTION) {
// we are making connection, they just started, so lets reset the
// poll count just in case
pollCount = 0;
} else {
// if waiting or inactive, lets clear the connecting id. If in makingConnection,
// do nothing
clearConnecting();
if (state !== STATES.INACTIVE) {
startHandshake();

View file

@ -13,7 +13,7 @@
//
/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays,
MyAvatar, Menu, Vec3 */
MyAvatar, Menu, AvatarInputs, Vec3 */
(function() { // BEGIN LOCAL_SCOPE
var tabletRezzed = false;
@ -25,9 +25,18 @@
var debugTablet = false;
var tabletScalePercentage = 100.0;
UIWebTablet = null;
var MSECS_PER_SEC = 1000.0;
var MUTE_MICROPHONE_MENU_ITEM = "Mute Microphone";
var gTablet = null;
Script.include("../libraries/WebTablet.js");
function checkTablet() {
if (gTablet === null) {
gTablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
}
}
function tabletIsValid() {
if (!UIWebTablet) {
return false;
@ -49,7 +58,8 @@
}
function getTabletScalePercentageFromSettings() {
var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode;
checkTablet()
var toolbarMode = gTablet.toolbarMode;
var tabletScalePercentage = DEFAULT_TABLET_SCALE;
if (!toolbarMode) {
if (HMD.active) {
@ -77,6 +87,7 @@
if (debugTablet) {
print("TABLET rezzing");
}
checkTablet()
tabletScalePercentage = getTabletScalePercentageFromSettings();
UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml",
@ -92,7 +103,8 @@
}
function showTabletUI() {
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = true;
checkTablet()
gTablet.tabletShown = true;
if (!tabletRezzed || !tabletIsValid()) {
closeTabletUI();
@ -114,7 +126,8 @@
}
function hideTabletUI() {
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false;
checkTablet()
gTablet.tabletShown = false;
if (!UIWebTablet) {
return;
}
@ -130,7 +143,8 @@
}
function closeTabletUI() {
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false;
checkTablet()
gTablet.tabletShown = false;
if (UIWebTablet) {
if (UIWebTablet.onClose) {
UIWebTablet.onClose();
@ -149,17 +163,19 @@
print("TABLET closeTabletUI, UIWebTablet is null");
}
tabletRezzed = false;
gTablet = null
}
function updateShowTablet() {
var MSECS_PER_SEC = 1000.0;
var now = Date.now();
checkTablet()
// close the WebTablet if it we go into toolbar mode.
var tabletShown = Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown;
var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode;
var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape;
var tabletShown = gTablet.tabletShown;
var toolbarMode = gTablet.toolbarMode;
var landscape = gTablet.landscape;
if (tabletShown && toolbarMode) {
closeTabletUI();
@ -167,18 +183,20 @@
return;
}
//TODO: move to tablet qml?
if (tabletShown) {
var MUTE_MICROPHONE_MENU_ITEM = "Mute Microphone";
var currentMicEnabled = !Menu.isOptionChecked(MUTE_MICROPHONE_MENU_ITEM);
var currentMicLevel = getMicLevel();
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
tablet.updateMicEnabled(currentMicEnabled);
tablet.updateAudioBar(currentMicLevel);
gTablet.updateMicEnabled(currentMicEnabled);
gTablet.updateAudioBar(currentMicLevel);
}
updateTabletWidthFromSettings();
if (UIWebTablet) {
UIWebTablet.setLandscape(landscape);
if (validCheckTime - now > MSECS_PER_SEC/4) {
//each 250ms should be just fine
updateTabletWidthFromSettings();
if (UIWebTablet) {
UIWebTablet.setLandscape(landscape);
}
}
if (validCheckTime - now > MSECS_PER_SEC) {
@ -217,21 +235,20 @@
// also cause the stylus model to be loaded
var tmpStylusID = Overlays.addOverlay("model", {
name: "stylus",
url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx",
loadPriority: 10.0,
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 },
solid: true,
visible: true,
ignoreRayIntersection: true,
drawInFront: false,
lifetime: 3
});
name: "stylus",
url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx",
loadPriority: 10.0,
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 },
solid: true,
visible: true,
ignoreRayIntersection: true,
drawInFront: false,
lifetime: 3
});
Script.setTimeout(function() {
Overlays.deleteOverlay(tmpStylusID);
}, 300);
} else if (!tabletShown) {
hideTabletUI();
}
@ -246,7 +263,8 @@
}
if (channel === "home") {
if (UIWebTablet) {
Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape = false;
checkTablet()
gTablet.landscape = false;
}
}
}
@ -257,30 +275,10 @@
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
function getMicLevel() {
var LOUDNESS_FLOOR = 11.0;
var LOUDNESS_SCALE = 2.8 / 5.0;
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;
//reuse already existing C++ code
return AvatarInputs.loudnessToAudioLevel(MyAvatar.audioLoudness)
}
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 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() {
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": ""
}
]
}