mirror of
https://github.com/overte-org/overte.git
synced 2025-08-08 07:37:31 +02:00
Merge branch 'master' into feature/ik-solver-init-config
This commit is contained in:
commit
93b8dc550c
58 changed files with 2199 additions and 522 deletions
4
cmake/externals/wasapi/CMakeLists.txt
vendored
4
cmake/externals/wasapi/CMakeLists.txt
vendored
|
@ -6,8 +6,8 @@ if (WIN32)
|
||||||
include(ExternalProject)
|
include(ExternalProject)
|
||||||
ExternalProject_Add(
|
ExternalProject_Add(
|
||||||
${EXTERNAL_NAME}
|
${EXTERNAL_NAME}
|
||||||
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip
|
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi8.zip
|
||||||
URL_MD5 bc2861e50852dd590cdc773a14a041a7
|
URL_MD5 b01510437ea15527156bc25cdf733bd9
|
||||||
CONFIGURE_COMMAND ""
|
CONFIGURE_COMMAND ""
|
||||||
BUILD_COMMAND ""
|
BUILD_COMMAND ""
|
||||||
INSTALL_COMMAND ""
|
INSTALL_COMMAND ""
|
||||||
|
|
|
@ -35,6 +35,11 @@
|
||||||
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
|
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
|
||||||
|
|
||||||
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] },
|
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] },
|
||||||
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }
|
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] },
|
||||||
|
{ "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", "when": [ "Application.InHMD"] },
|
||||||
|
{ "from": "Vive.RightFoot", "to" : "Standard.RightFoot", "when": [ "Application.InHMD"] },
|
||||||
|
{ "from": "Vive.Hips", "to" : "Standard.Hips", "when": [ "Application.InHMD"] },
|
||||||
|
{ "from": "Vive.Spine2", "to" : "Standard.Spine2", "when": [ "Application.InHMD"] },
|
||||||
|
{ "from": "Vive.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,19 +17,20 @@ Rectangle {
|
||||||
property alias pixelSize: label.font.pixelSize;
|
property alias pixelSize: label.font.pixelSize;
|
||||||
property bool selected: false
|
property bool selected: false
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
property bool enabled: false
|
|
||||||
property int spacing: 2
|
property int spacing: 2
|
||||||
property var action: function () {}
|
property var action: function () {}
|
||||||
property string enabledColor: hifi.colors.blueHighlight
|
property string enabledColor: hifi.colors.blueHighlight
|
||||||
property string disabledColor: hifi.colors.blueHighlight
|
property string disabledColor: hifi.colors.blueHighlight
|
||||||
property string highlightColor: hifi.colors.blueHighlight;
|
|
||||||
width: label.width + 64
|
width: label.width + 64
|
||||||
height: 32
|
height: 32
|
||||||
color: hifi.colors.white
|
color: hifi.colors.white
|
||||||
|
enabled: false
|
||||||
|
|
||||||
HifiConstants { id: hifi }
|
HifiConstants { id: hifi }
|
||||||
|
|
||||||
RalewaySemiBold {
|
RalewaySemiBold {
|
||||||
id: label;
|
id: label;
|
||||||
color: enabledColor
|
color: enabled ? enabledColor : disabledColor
|
||||||
font.pixelSize: 15;
|
font.pixelSize: 15;
|
||||||
anchors {
|
anchors {
|
||||||
horizontalCenter: parent.horizontalCenter;
|
horizontalCenter: parent.horizontalCenter;
|
||||||
|
@ -37,7 +38,6 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: indicator
|
id: indicator
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
|
@ -8,6 +8,7 @@ import "../styles" as HifiStyles
|
||||||
import "../styles-uit"
|
import "../styles-uit"
|
||||||
import "../"
|
import "../"
|
||||||
import "."
|
import "."
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: web
|
id: web
|
||||||
HifiConstants { id: hifi }
|
HifiConstants { id: hifi }
|
||||||
|
@ -22,17 +23,14 @@ Item {
|
||||||
property bool keyboardRaised: false
|
property bool keyboardRaised: false
|
||||||
property bool punctuationMode: false
|
property bool punctuationMode: false
|
||||||
property bool isDesktop: false
|
property bool isDesktop: false
|
||||||
property string initialPage: ""
|
|
||||||
property bool startingUp: true
|
|
||||||
property alias webView: webview
|
property alias webView: webview
|
||||||
property alias profile: webview.profile
|
property alias profile: webview.profile
|
||||||
property bool remove: false
|
property bool remove: false
|
||||||
property var urlList: []
|
|
||||||
property var forwardList: []
|
|
||||||
|
|
||||||
|
// Manage own browse history because WebEngineView history is wiped when a new URL is loaded via
|
||||||
property int currentPage: -1 // used as a model for repeater
|
// onNewViewRequested, e.g., as happens when a social media share button is clicked.
|
||||||
property alias pagesModel: pagesModel
|
property var history: []
|
||||||
|
property int historyIndex: -1
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: buttons
|
id: buttons
|
||||||
|
@ -51,21 +49,22 @@ Item {
|
||||||
|
|
||||||
TabletWebButton {
|
TabletWebButton {
|
||||||
id: back
|
id: back
|
||||||
enabledColor: hifi.colors.baseGray
|
enabledColor: hifi.colors.darkGray
|
||||||
enabled: false
|
disabledColor: hifi.colors.lightGrayText
|
||||||
|
enabled: historyIndex > 0
|
||||||
text: "BACK"
|
text: "BACK"
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: goBack()
|
onClicked: goBack()
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TabletWebButton {
|
TabletWebButton {
|
||||||
id: close
|
id: close
|
||||||
enabledColor: hifi.colors.darkGray
|
enabledColor: hifi.colors.darkGray
|
||||||
|
disabledColor: hifi.colors.lightGrayText
|
||||||
|
enabled: true
|
||||||
text: "CLOSE"
|
text: "CLOSE"
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
@ -75,7 +74,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
RalewaySemiBold {
|
RalewaySemiBold {
|
||||||
id: displayUrl
|
id: displayUrl
|
||||||
color: hifi.colors.baseGray
|
color: hifi.colors.baseGray
|
||||||
|
@ -90,7 +88,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
|
@ -98,29 +95,10 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: pagesModel
|
|
||||||
onCountChanged: {
|
|
||||||
currentPage = count - 1;
|
|
||||||
if (currentPage > 0) {
|
|
||||||
back.enabledColor = hifi.colors.darkGray;
|
|
||||||
} else {
|
|
||||||
back.enabledColor = hifi.colors.baseGray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (webview.canGoBack) {
|
if (historyIndex > 0) {
|
||||||
forwardList.push(webview.url);
|
historyIndex--;
|
||||||
webview.goBack();
|
loadUrl(history[historyIndex]);
|
||||||
} else if (web.urlList.length > 0) {
|
|
||||||
var url = web.urlList.pop();
|
|
||||||
loadUrl(url);
|
|
||||||
} else if (web.forwardList.length > 0) {
|
|
||||||
var url = web.forwardList.pop();
|
|
||||||
loadUrl(url);
|
|
||||||
web.forwardList = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,19 +115,12 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
function goForward() {
|
function goForward() {
|
||||||
if (currentPage < pagesModel.count - 1) {
|
if (historyIndex < history.length - 1) {
|
||||||
currentPage++;
|
historyIndex++;
|
||||||
|
loadUrl(history[historyIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoPage(url) {
|
|
||||||
urlAppend(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUrlLoaded(url) {
|
|
||||||
return (pagesModel.get(currentPage).webUrl === url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadPage() {
|
function reloadPage() {
|
||||||
view.reloadAndBypassCache()
|
view.reloadAndBypassCache()
|
||||||
view.setActiveFocusOnPress(true);
|
view.setActiveFocusOnPress(true);
|
||||||
|
@ -161,36 +132,8 @@ Item {
|
||||||
web.url = webview.url;
|
web.url = webview.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInitialPage(url) {
|
|
||||||
return (url === webview.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function urlAppend(url) {
|
|
||||||
var lurl = decodeURIComponent(url)
|
|
||||||
if (lurl[lurl.length - 1] !== "/") {
|
|
||||||
lurl = lurl + "/"
|
|
||||||
}
|
|
||||||
web.urlList.push(url);
|
|
||||||
setBackButtonStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBackButtonStatus() {
|
|
||||||
if (web.urlList.length > 0 || webview.canGoBack) {
|
|
||||||
back.enabledColor = hifi.colors.darkGray;
|
|
||||||
back.enabled = true;
|
|
||||||
} else {
|
|
||||||
back.enabledColor = hifi.colors.baseGray;
|
|
||||||
back.enabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onUrlChanged: {
|
onUrlChanged: {
|
||||||
loadUrl(url);
|
loadUrl(url);
|
||||||
if (startingUp) {
|
|
||||||
web.initialPage = webview.url;
|
|
||||||
startingUp = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
|
@ -258,6 +201,17 @@ Item {
|
||||||
grantFeaturePermission(securityOrigin, feature, true);
|
grantFeaturePermission(securityOrigin, feature, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUrlChanged: {
|
||||||
|
// Record history, skipping null and duplicate items.
|
||||||
|
var urlString = url + "";
|
||||||
|
urlString = urlString.replace(/\//g, "%2F"); // Consistent representation of "/"s to avoid false differences.
|
||||||
|
if (urlString.length > 0 && (historyIndex === -1 || urlString !== history[historyIndex])) {
|
||||||
|
historyIndex++;
|
||||||
|
history = history.slice(0, historyIndex);
|
||||||
|
history.push(urlString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onLoadingChanged: {
|
onLoadingChanged: {
|
||||||
keyboardRaised = false;
|
keyboardRaised = false;
|
||||||
punctuationMode = false;
|
punctuationMode = false;
|
||||||
|
@ -277,17 +231,11 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WebEngineView.LoadSucceededStatus == loadRequest.status) {
|
if (WebEngineView.LoadSucceededStatus == loadRequest.status) {
|
||||||
if (startingUp) {
|
|
||||||
web.initialPage = webview.url;
|
|
||||||
startingUp = false;
|
|
||||||
}
|
|
||||||
webview.forceActiveFocus();
|
webview.forceActiveFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewViewRequested: {
|
onNewViewRequested: {
|
||||||
var currentUrl = webview.url;
|
|
||||||
urlAppend(currentUrl);
|
|
||||||
request.openIn(webview);
|
request.openIn(webview);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -844,7 +844,7 @@ Rectangle {
|
||||||
boxSize: 24;
|
boxSize: 24;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
var newValue = model.connection !== "friend";
|
var newValue = model.connection !== "friend";
|
||||||
connectionsUserModel.setProperty(model.userIndex, styleData.role, newValue);
|
connectionsUserModel.setProperty(model.userIndex, styleData.role, (newValue ? "friend" : "connection"));
|
||||||
connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming
|
connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming
|
||||||
pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName});
|
pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName});
|
||||||
|
|
||||||
|
|
|
@ -1688,7 +1688,6 @@ void Application::updateHeartbeat() const {
|
||||||
|
|
||||||
void Application::aboutToQuit() {
|
void Application::aboutToQuit() {
|
||||||
emit beforeAboutToQuit();
|
emit beforeAboutToQuit();
|
||||||
DependencyManager::get<AudioClient>()->beforeAboutToQuit();
|
|
||||||
|
|
||||||
foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) {
|
foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) {
|
||||||
if (inputPlugin->isActive()) {
|
if (inputPlugin->isActive()) {
|
||||||
|
@ -1789,14 +1788,13 @@ void Application::cleanupBeforeQuit() {
|
||||||
_snapshotSoundInjector->stop();
|
_snapshotSoundInjector->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop audio after QML, as there are unexplained audio crashes originating in qtwebengine
|
// FIXME: something else is holding a reference to AudioClient,
|
||||||
|
// so it must be explicitly synchronously stopped here
|
||||||
// stop the AudioClient, synchronously
|
|
||||||
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
|
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
|
||||||
"stop", Qt::BlockingQueuedConnection);
|
"cleanupBeforeQuit", Qt::BlockingQueuedConnection);
|
||||||
|
|
||||||
|
|
||||||
// destroy Audio so it and its threads have a chance to go down safely
|
// destroy Audio so it and its threads have a chance to go down safely
|
||||||
|
// this must happen after QML, as there are unexplained audio crashes originating in qtwebengine
|
||||||
DependencyManager::destroy<AudioClient>();
|
DependencyManager::destroy<AudioClient>();
|
||||||
DependencyManager::destroy<AudioInjectorManager>();
|
DependencyManager::destroy<AudioInjectorManager>();
|
||||||
|
|
||||||
|
@ -4331,13 +4329,6 @@ void Application::update(float deltaTime) {
|
||||||
if (nearbyEntitiesAreReadyForPhysics()) {
|
if (nearbyEntitiesAreReadyForPhysics()) {
|
||||||
_physicsEnabled = true;
|
_physicsEnabled = true;
|
||||||
getMyAvatar()->updateMotionBehaviorFromMenu();
|
getMyAvatar()->updateMotionBehaviorFromMenu();
|
||||||
} else {
|
|
||||||
auto characterController = getMyAvatar()->getCharacterController();
|
|
||||||
if (characterController) {
|
|
||||||
// if we have a character controller, disable it here so the avatar doesn't get stuck due to
|
|
||||||
// a non-loading collision hull.
|
|
||||||
characterController->setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (domainLoadingInProgress) {
|
} else if (domainLoadingInProgress) {
|
||||||
|
|
|
@ -197,7 +197,7 @@ Menu::Menu() {
|
||||||
0, // QML Qt::Key_Apostrophe,
|
0, // QML Qt::Key_Apostrophe,
|
||||||
qApp, SLOT(resetSensors()));
|
qApp, SLOT(resetSensors()));
|
||||||
|
|
||||||
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true,
|
addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableAvatarCollisions, 0, true,
|
||||||
avatar.get(), SLOT(updateMotionBehaviorFromMenu()));
|
avatar.get(), SLOT(updateMotionBehaviorFromMenu()));
|
||||||
|
|
||||||
// Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here.
|
// Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here.
|
||||||
|
|
|
@ -96,7 +96,7 @@ namespace MenuOption {
|
||||||
const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene";
|
const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene";
|
||||||
const QString EchoLocalAudio = "Echo Local Audio";
|
const QString EchoLocalAudio = "Echo Local Audio";
|
||||||
const QString EchoServerAudio = "Echo Server Audio";
|
const QString EchoServerAudio = "Echo Server Audio";
|
||||||
const QString EnableCharacterController = "Collide with world";
|
const QString EnableAvatarCollisions = "Enable Avatar Collisions";
|
||||||
const QString EnableInverseKinematics = "Enable Inverse Kinematics";
|
const QString EnableInverseKinematics = "Enable Inverse Kinematics";
|
||||||
const QString EntityScriptServerLog = "Entity Script Server Log";
|
const QString EntityScriptServerLog = "Entity Script Server Log";
|
||||||
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";
|
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";
|
||||||
|
|
70
interface/src/avatar/MyAvatar.cpp
Normal file → Executable file
70
interface/src/avatar/MyAvatar.cpp
Normal file → Executable file
|
@ -151,8 +151,6 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) :
|
||||||
// when we leave a domain we lift whatever restrictions that domain may have placed on our scale
|
// when we leave a domain we lift whatever restrictions that domain may have placed on our scale
|
||||||
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::clearScaleRestriction);
|
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::clearScaleRestriction);
|
||||||
|
|
||||||
_characterController.setEnabled(true);
|
|
||||||
|
|
||||||
_bodySensorMatrix = deriveBodyFromHMDSensor();
|
_bodySensorMatrix = deriveBodyFromHMDSensor();
|
||||||
|
|
||||||
using namespace recording;
|
using namespace recording;
|
||||||
|
@ -166,12 +164,14 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) :
|
||||||
if (recordingInterface->getPlayFromCurrentLocation()) {
|
if (recordingInterface->getPlayFromCurrentLocation()) {
|
||||||
setRecordingBasis();
|
setRecordingBasis();
|
||||||
}
|
}
|
||||||
_wasCharacterControllerEnabled = _characterController.isEnabled();
|
_previousCollisionGroup = _characterController.computeCollisionGroup();
|
||||||
_characterController.setEnabled(false);
|
_characterController.setCollisionless(true);
|
||||||
} else {
|
} else {
|
||||||
clearRecordingBasis();
|
clearRecordingBasis();
|
||||||
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
|
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
|
||||||
_characterController.setEnabled(_wasCharacterControllerEnabled);
|
if (_previousCollisionGroup != BULLET_COLLISION_GROUP_COLLISIONLESS) {
|
||||||
|
_characterController.setCollisionless(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto audioIO = DependencyManager::get<AudioClient>();
|
auto audioIO = DependencyManager::get<AudioClient>();
|
||||||
|
@ -554,12 +554,12 @@ void MyAvatar::simulate(float deltaTime) {
|
||||||
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
|
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
|
||||||
if (entityTree) {
|
if (entityTree) {
|
||||||
bool flyingAllowed = true;
|
bool flyingAllowed = true;
|
||||||
bool ghostingAllowed = true;
|
bool collisionlessAllowed = true;
|
||||||
entityTree->withWriteLock([&] {
|
entityTree->withWriteLock([&] {
|
||||||
std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone();
|
std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone();
|
||||||
if (zone) {
|
if (zone) {
|
||||||
flyingAllowed = zone->getFlyingAllowed();
|
flyingAllowed = zone->getFlyingAllowed();
|
||||||
ghostingAllowed = zone->getGhostingAllowed();
|
collisionlessAllowed = zone->getGhostingAllowed();
|
||||||
}
|
}
|
||||||
auto now = usecTimestampNow();
|
auto now = usecTimestampNow();
|
||||||
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
|
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
|
||||||
|
@ -590,9 +590,7 @@ void MyAvatar::simulate(float deltaTime) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_characterController.setFlyingAllowed(flyingAllowed);
|
_characterController.setFlyingAllowed(flyingAllowed);
|
||||||
if (!_characterController.isEnabled() && !ghostingAllowed) {
|
_characterController.setCollisionlessAllowed(collisionlessAllowed);
|
||||||
_characterController.setEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvatarEntities();
|
updateAvatarEntities();
|
||||||
|
@ -1455,7 +1453,8 @@ void MyAvatar::updateMotors() {
|
||||||
_characterController.clearMotors();
|
_characterController.clearMotors();
|
||||||
glm::quat motorRotation;
|
glm::quat motorRotation;
|
||||||
if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) {
|
if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) {
|
||||||
if (_characterController.getState() == CharacterController::State::Hover) {
|
if (_characterController.getState() == CharacterController::State::Hover ||
|
||||||
|
_characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
|
||||||
motorRotation = getMyHead()->getCameraOrientation();
|
motorRotation = getMyHead()->getCameraOrientation();
|
||||||
} else {
|
} else {
|
||||||
// non-hovering = walking: follow camera twist about vertical but not lift
|
// non-hovering = walking: follow camera twist about vertical but not lift
|
||||||
|
@ -1501,6 +1500,7 @@ void MyAvatar::prepareForPhysicsSimulation() {
|
||||||
qDebug() << "Warning: getParentVelocity failed" << getID();
|
qDebug() << "Warning: getParentVelocity failed" << getID();
|
||||||
parentVelocity = glm::vec3();
|
parentVelocity = glm::vec3();
|
||||||
}
|
}
|
||||||
|
_characterController.handleChangedCollisionGroup();
|
||||||
_characterController.setParentVelocity(parentVelocity);
|
_characterController.setParentVelocity(parentVelocity);
|
||||||
|
|
||||||
_characterController.setPositionAndOrientation(getPosition(), getOrientation());
|
_characterController.setPositionAndOrientation(getPosition(), getOrientation());
|
||||||
|
@ -1889,8 +1889,9 @@ void MyAvatar::updateActionMotor(float deltaTime) {
|
||||||
|
|
||||||
glm::vec3 direction = forward + right;
|
glm::vec3 direction = forward + right;
|
||||||
CharacterController::State state = _characterController.getState();
|
CharacterController::State state = _characterController.getState();
|
||||||
if (state == CharacterController::State::Hover) {
|
if (state == CharacterController::State::Hover ||
|
||||||
// we're flying --> support vertical motion
|
_characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
|
||||||
|
// we can fly --> support vertical motion
|
||||||
glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
|
glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
|
||||||
direction += up;
|
direction += up;
|
||||||
}
|
}
|
||||||
|
@ -1912,7 +1913,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
|
||||||
float finalMaxMotorSpeed = getUniformScale() * MAX_ACTION_MOTOR_SPEED;
|
float finalMaxMotorSpeed = getUniformScale() * MAX_ACTION_MOTOR_SPEED;
|
||||||
float speedGrowthTimescale = 2.0f;
|
float speedGrowthTimescale = 2.0f;
|
||||||
float speedIncreaseFactor = 1.8f;
|
float speedIncreaseFactor = 1.8f;
|
||||||
motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale , 0.0f, 1.0f) * speedIncreaseFactor;
|
motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor;
|
||||||
const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED;
|
const float maxBoostSpeed = getUniformScale() * MAX_BOOST_SPEED;
|
||||||
|
|
||||||
if (_isPushing) {
|
if (_isPushing) {
|
||||||
|
@ -1955,9 +1956,17 @@ void MyAvatar::updatePosition(float deltaTime) {
|
||||||
measureMotionDerivatives(deltaTime);
|
measureMotionDerivatives(deltaTime);
|
||||||
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
|
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
|
||||||
} else {
|
} else {
|
||||||
// physics physics simulation updated elsewhere
|
|
||||||
float speed2 = glm::length2(velocity);
|
float speed2 = glm::length2(velocity);
|
||||||
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
|
_moving = speed2 > MOVING_SPEED_THRESHOLD_SQUARED;
|
||||||
|
|
||||||
|
if (_moving) {
|
||||||
|
// scan for walkability
|
||||||
|
glm::vec3 position = getPosition();
|
||||||
|
MyCharacterController::RayShotgunResult result;
|
||||||
|
glm::vec3 step = deltaTime * (getRotation() * _actionMotorVelocity);
|
||||||
|
_characterController.testRayShotgun(position, step, result);
|
||||||
|
_characterController.setStepUpEnabled(result.walkable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// capture the head rotation, in sensor space, when the user first indicates they would like to move/fly.
|
// capture the head rotation, in sensor space, when the user first indicates they would like to move/fly.
|
||||||
|
@ -2194,30 +2203,33 @@ void MyAvatar::updateMotionBehaviorFromMenu() {
|
||||||
} else {
|
} else {
|
||||||
_motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;
|
_motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED;
|
||||||
}
|
}
|
||||||
|
setCollisionsEnabled(menu->isOptionChecked(MenuOption::EnableAvatarCollisions));
|
||||||
setCharacterControllerEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyAvatar::setCharacterControllerEnabled(bool enabled) {
|
void MyAvatar::setCollisionsEnabled(bool enabled) {
|
||||||
|
|
||||||
if (QThread::currentThread() != thread()) {
|
if (QThread::currentThread() != thread()) {
|
||||||
QMetaObject::invokeMethod(this, "setCharacterControllerEnabled", Q_ARG(bool, enabled));
|
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ghostingAllowed = true;
|
_characterController.setCollisionless(!enabled);
|
||||||
auto entityTreeRenderer = qApp->getEntities();
|
}
|
||||||
if (entityTreeRenderer) {
|
|
||||||
std::shared_ptr<ZoneEntityItem> zone = entityTreeRenderer->myAvatarZone();
|
bool MyAvatar::getCollisionsEnabled() {
|
||||||
if (zone) {
|
// may return 'false' even though the collisionless option was requested
|
||||||
ghostingAllowed = zone->getGhostingAllowed();
|
// because the zone may disallow collisionless avatars
|
||||||
}
|
return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
|
||||||
}
|
}
|
||||||
_characterController.setEnabled(ghostingAllowed ? enabled : true);
|
|
||||||
|
void MyAvatar::setCharacterControllerEnabled(bool enabled) {
|
||||||
|
qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead.";
|
||||||
|
setCollisionsEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MyAvatar::getCharacterControllerEnabled() {
|
bool MyAvatar::getCharacterControllerEnabled() {
|
||||||
return _characterController.isEnabled();
|
qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead.";
|
||||||
|
return getCollisionsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyAvatar::clearDriveKeys() {
|
void MyAvatar::clearDriveKeys() {
|
||||||
|
|
|
@ -96,7 +96,7 @@ class MyAvatar : public Avatar {
|
||||||
* @property rightHandTipPose {Pose} READ-ONLY. Returns a pose offset 30 cm from MyAvatar.rightHandPose
|
* @property rightHandTipPose {Pose} READ-ONLY. Returns a pose offset 30 cm from MyAvatar.rightHandPose
|
||||||
* @property hmdLeanRecenterEnabled {bool} This can be used disable the hmd lean recenter behavior. This behavior is what causes your avatar
|
* @property hmdLeanRecenterEnabled {bool} This can be used disable the hmd lean recenter behavior. This behavior is what causes your avatar
|
||||||
* to follow your HMD as you walk around the room, in room scale VR. Disabling this is useful if you desire to pin the avatar to a fixed location.
|
* to follow your HMD as you walk around the room, in room scale VR. Disabling this is useful if you desire to pin the avatar to a fixed location.
|
||||||
* @property characterControllerEnabled {bool} This can be used to disable collisions between the avatar and the world.
|
* @property collisionsEnabled {bool} This can be used to disable collisions between the avatar and the world.
|
||||||
* @property useAdvancedMovementControls {bool} Stores the user preference only, does not change user mappings, this is done in the defaultScript
|
* @property useAdvancedMovementControls {bool} Stores the user preference only, does not change user mappings, this is done in the defaultScript
|
||||||
* "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js".
|
* "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js".
|
||||||
*/
|
*/
|
||||||
|
@ -128,6 +128,7 @@ class MyAvatar : public Avatar {
|
||||||
Q_PROPERTY(float isAway READ getIsAway WRITE setAway)
|
Q_PROPERTY(float isAway READ getIsAway WRITE setAway)
|
||||||
|
|
||||||
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
|
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
|
||||||
|
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
|
||||||
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
|
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
|
||||||
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
|
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
|
||||||
|
|
||||||
|
@ -470,8 +471,10 @@ public:
|
||||||
|
|
||||||
bool hasDriveInput() const;
|
bool hasDriveInput() const;
|
||||||
|
|
||||||
Q_INVOKABLE void setCharacterControllerEnabled(bool enabled);
|
Q_INVOKABLE void setCollisionsEnabled(bool enabled);
|
||||||
Q_INVOKABLE bool getCharacterControllerEnabled();
|
Q_INVOKABLE bool getCollisionsEnabled();
|
||||||
|
Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated
|
||||||
|
Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated
|
||||||
|
|
||||||
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override;
|
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override;
|
||||||
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override;
|
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override;
|
||||||
|
@ -615,7 +618,7 @@ private:
|
||||||
SharedSoundPointer _collisionSound;
|
SharedSoundPointer _collisionSound;
|
||||||
|
|
||||||
MyCharacterController _characterController;
|
MyCharacterController _characterController;
|
||||||
bool _wasCharacterControllerEnabled { true };
|
int16_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR };
|
||||||
|
|
||||||
AvatarWeakPointer _lookAtTargetAvatar;
|
AvatarWeakPointer _lookAtTargetAvatar;
|
||||||
glm::vec3 _targetAvatarPosition;
|
glm::vec3 _targetAvatarPosition;
|
||||||
|
|
286
interface/src/avatar/MyCharacterController.cpp
Normal file → Executable file
286
interface/src/avatar/MyCharacterController.cpp
Normal file → Executable file
|
@ -15,11 +15,15 @@
|
||||||
|
|
||||||
#include "MyAvatar.h"
|
#include "MyAvatar.h"
|
||||||
|
|
||||||
// TODO: improve walking up steps
|
|
||||||
// TODO: make avatars able to walk up and down steps/slopes
|
|
||||||
// TODO: make avatars stand on steep slope
|
// TODO: make avatars stand on steep slope
|
||||||
// TODO: make avatars not snag on low ceilings
|
// TODO: make avatars not snag on low ceilings
|
||||||
|
|
||||||
|
|
||||||
|
void MyCharacterController::RayShotgunResult::reset() {
|
||||||
|
hitFraction = 1.0f;
|
||||||
|
walkable = true;
|
||||||
|
}
|
||||||
|
|
||||||
MyCharacterController::MyCharacterController(MyAvatar* avatar) {
|
MyCharacterController::MyCharacterController(MyAvatar* avatar) {
|
||||||
|
|
||||||
assert(avatar);
|
assert(avatar);
|
||||||
|
@ -30,37 +34,33 @@ MyCharacterController::MyCharacterController(MyAvatar* avatar) {
|
||||||
MyCharacterController::~MyCharacterController() {
|
MyCharacterController::~MyCharacterController() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MyCharacterController::setDynamicsWorld(btDynamicsWorld* world) {
|
||||||
|
CharacterController::setDynamicsWorld(world);
|
||||||
|
if (world) {
|
||||||
|
initRayShotgun(world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MyCharacterController::updateShapeIfNecessary() {
|
void MyCharacterController::updateShapeIfNecessary() {
|
||||||
if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) {
|
if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) {
|
||||||
_pendingFlags &= ~PENDING_FLAG_UPDATE_SHAPE;
|
_pendingFlags &= ~PENDING_FLAG_UPDATE_SHAPE;
|
||||||
|
|
||||||
// compute new dimensions from avatar's bounding box
|
|
||||||
float x = _boxScale.x;
|
|
||||||
float z = _boxScale.z;
|
|
||||||
_radius = 0.5f * sqrtf(0.5f * (x * x + z * z));
|
|
||||||
_halfHeight = 0.5f * _boxScale.y - _radius;
|
|
||||||
float MIN_HALF_HEIGHT = 0.1f;
|
|
||||||
if (_halfHeight < MIN_HALF_HEIGHT) {
|
|
||||||
_halfHeight = MIN_HALF_HEIGHT;
|
|
||||||
}
|
|
||||||
// NOTE: _shapeLocalOffset is already computed
|
|
||||||
|
|
||||||
if (_radius > 0.0f) {
|
if (_radius > 0.0f) {
|
||||||
// create RigidBody if it doesn't exist
|
// create RigidBody if it doesn't exist
|
||||||
if (!_rigidBody) {
|
if (!_rigidBody) {
|
||||||
|
btCollisionShape* shape = computeShape();
|
||||||
|
|
||||||
// HACK: use some simple mass property defaults for now
|
// HACK: use some simple mass property defaults for now
|
||||||
const float DEFAULT_AVATAR_MASS = 100.0f;
|
const btScalar DEFAULT_AVATAR_MASS = 100.0f;
|
||||||
const btVector3 DEFAULT_AVATAR_INERTIA_TENSOR(30.0f, 8.0f, 30.0f);
|
const btVector3 DEFAULT_AVATAR_INERTIA_TENSOR(30.0f, 8.0f, 30.0f);
|
||||||
|
|
||||||
btCollisionShape* shape = new btCapsuleShape(_radius, 2.0f * _halfHeight);
|
|
||||||
_rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR);
|
_rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR);
|
||||||
} else {
|
} else {
|
||||||
btCollisionShape* shape = _rigidBody->getCollisionShape();
|
btCollisionShape* shape = _rigidBody->getCollisionShape();
|
||||||
if (shape) {
|
if (shape) {
|
||||||
delete shape;
|
delete shape;
|
||||||
}
|
}
|
||||||
shape = new btCapsuleShape(_radius, 2.0f * _halfHeight);
|
shape = computeShape();
|
||||||
_rigidBody->setCollisionShape(shape);
|
_rigidBody->setCollisionShape(shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,12 +72,262 @@ void MyCharacterController::updateShapeIfNecessary() {
|
||||||
if (_state == State::Hover) {
|
if (_state == State::Hover) {
|
||||||
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
|
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
|
||||||
} else {
|
} else {
|
||||||
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
|
_rigidBody->setGravity(_gravity * _currentUp);
|
||||||
}
|
}
|
||||||
//_rigidBody->setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT);
|
_rigidBody->setCollisionFlags(_rigidBody->getCollisionFlags() &
|
||||||
|
~(btCollisionObject::CF_KINEMATIC_OBJECT | btCollisionObject::CF_STATIC_OBJECT));
|
||||||
} else {
|
} else {
|
||||||
// TODO: handle this failure case
|
// TODO: handle this failure case
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MyCharacterController::testRayShotgun(const glm::vec3& position, const glm::vec3& step, RayShotgunResult& result) {
|
||||||
|
btVector3 rayDirection = glmToBullet(step);
|
||||||
|
btScalar stepLength = rayDirection.length();
|
||||||
|
if (stepLength < FLT_EPSILON) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rayDirection /= stepLength;
|
||||||
|
|
||||||
|
// get _ghost ready for ray traces
|
||||||
|
btTransform transform = _rigidBody->getWorldTransform();
|
||||||
|
btVector3 newPosition = glmToBullet(position);
|
||||||
|
transform.setOrigin(newPosition);
|
||||||
|
_ghost.setWorldTransform(transform);
|
||||||
|
btMatrix3x3 rotation = transform.getBasis();
|
||||||
|
_ghost.refreshOverlappingPairCache();
|
||||||
|
|
||||||
|
CharacterRayResult rayResult(&_ghost);
|
||||||
|
CharacterRayResult closestRayResult(&_ghost);
|
||||||
|
btVector3 rayStart;
|
||||||
|
btVector3 rayEnd;
|
||||||
|
|
||||||
|
// compute rotation that will orient local ray start points to face step direction
|
||||||
|
btVector3 forward = rotation * btVector3(0.0f, 0.0f, -1.0f);
|
||||||
|
btVector3 adjustedDirection = rayDirection - rayDirection.dot(_currentUp) * _currentUp;
|
||||||
|
btVector3 axis = forward.cross(adjustedDirection);
|
||||||
|
btScalar lengthAxis = axis.length();
|
||||||
|
if (lengthAxis > FLT_EPSILON) {
|
||||||
|
// we're walking sideways
|
||||||
|
btScalar angle = acosf(lengthAxis / adjustedDirection.length());
|
||||||
|
if (rayDirection.dot(forward) < 0.0f) {
|
||||||
|
angle = PI - angle;
|
||||||
|
}
|
||||||
|
axis /= lengthAxis;
|
||||||
|
rotation = btMatrix3x3(btQuaternion(axis, angle)) * rotation;
|
||||||
|
} else if (rayDirection.dot(forward) < 0.0f) {
|
||||||
|
// we're walking backwards
|
||||||
|
rotation = btMatrix3x3(btQuaternion(_currentUp, PI)) * rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan the top
|
||||||
|
// NOTE: if we scan an extra distance forward we can detect flat surfaces that are too steep to walk on.
|
||||||
|
// The approximate extra distance can be derived with trigonometry.
|
||||||
|
//
|
||||||
|
// minimumForward = [ (maxStepHeight + radius / cosTheta - radius) * (cosTheta / sinTheta) - radius ]
|
||||||
|
//
|
||||||
|
// where: theta = max angle between floor normal and vertical
|
||||||
|
//
|
||||||
|
// if stepLength is not long enough we can add the difference.
|
||||||
|
//
|
||||||
|
btScalar cosTheta = _minFloorNormalDotUp;
|
||||||
|
btScalar sinTheta = sqrtf(1.0f - cosTheta * cosTheta);
|
||||||
|
const btScalar MIN_FORWARD_SLOP = 0.12f; // HACK: not sure why this is necessary to detect steepest walkable slope
|
||||||
|
btScalar forwardSlop = (_maxStepHeight + _radius / cosTheta - _radius) * (cosTheta / sinTheta) - (_radius + stepLength) + MIN_FORWARD_SLOP;
|
||||||
|
if (forwardSlop < 0.0f) {
|
||||||
|
// BIG step, no slop necessary
|
||||||
|
forwardSlop = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btScalar backSlop = 0.04f;
|
||||||
|
for (int32_t i = 0; i < _topPoints.size(); ++i) {
|
||||||
|
rayStart = newPosition + rotation * _topPoints[i] - backSlop * rayDirection;
|
||||||
|
rayEnd = rayStart + (backSlop + stepLength + forwardSlop) * rayDirection;
|
||||||
|
if (_ghost.rayTest(rayStart, rayEnd, rayResult)) {
|
||||||
|
if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) {
|
||||||
|
closestRayResult = rayResult;
|
||||||
|
}
|
||||||
|
if (result.walkable) {
|
||||||
|
if (rayResult.m_hitNormalWorld.dot(_currentUp) < _minFloorNormalDotUp) {
|
||||||
|
result.walkable = false;
|
||||||
|
// the top scan wasn't walkable so don't bother scanning the bottom
|
||||||
|
// remove both forwardSlop and backSlop
|
||||||
|
result.hitFraction = glm::min(1.0f, (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop) - backSlop) / stepLength);
|
||||||
|
return result.hitFraction < 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_state == State::Hover) {
|
||||||
|
// scan the bottom just like the top
|
||||||
|
for (int32_t i = 0; i < _bottomPoints.size(); ++i) {
|
||||||
|
rayStart = newPosition + rotation * _bottomPoints[i] - backSlop * rayDirection;
|
||||||
|
rayEnd = rayStart + (backSlop + stepLength + forwardSlop) * rayDirection;
|
||||||
|
if (_ghost.rayTest(rayStart, rayEnd, rayResult)) {
|
||||||
|
if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) {
|
||||||
|
closestRayResult = rayResult;
|
||||||
|
}
|
||||||
|
if (result.walkable) {
|
||||||
|
if (rayResult.m_hitNormalWorld.dot(_currentUp) < _minFloorNormalDotUp) {
|
||||||
|
result.walkable = false;
|
||||||
|
// the bottom scan wasn't walkable
|
||||||
|
// remove both forwardSlop and backSlop
|
||||||
|
result.hitFraction = glm::min(1.0f, (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop) - backSlop) / stepLength);
|
||||||
|
return result.hitFraction < 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// scan the bottom looking for nearest step point
|
||||||
|
// remove forwardSlop
|
||||||
|
result.hitFraction = (closestRayResult.m_closestHitFraction * (backSlop + stepLength + forwardSlop)) / (backSlop + stepLength);
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < _bottomPoints.size(); ++i) {
|
||||||
|
rayStart = newPosition + rotation * _bottomPoints[i] - backSlop * rayDirection;
|
||||||
|
rayEnd = rayStart + (backSlop + stepLength) * rayDirection;
|
||||||
|
if (_ghost.rayTest(rayStart, rayEnd, rayResult)) {
|
||||||
|
if (rayResult.m_closestHitFraction < closestRayResult.m_closestHitFraction) {
|
||||||
|
closestRayResult = rayResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove backSlop
|
||||||
|
// NOTE: backSlop removal can produce a NEGATIVE hitFraction!
|
||||||
|
// which means the shape is actually in interpenetration
|
||||||
|
result.hitFraction = ((closestRayResult.m_closestHitFraction * (backSlop + stepLength)) - backSlop) / stepLength;
|
||||||
|
}
|
||||||
|
return result.hitFraction < 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
btConvexHullShape* MyCharacterController::computeShape() const {
|
||||||
|
// HACK: the avatar collides using convex hull with a collision margin equal to
|
||||||
|
// the old capsule radius. Two points define a capsule and additional points are
|
||||||
|
// spread out at chest level to produce a slight taper toward the feet. This
|
||||||
|
// makes the avatar more likely to collide with vertical walls at a higher point
|
||||||
|
// and thus less likely to produce a single-point collision manifold below the
|
||||||
|
// _maxStepHeight when walking into against vertical surfaces --> fixes a bug
|
||||||
|
// where the "walk up steps" feature would allow the avatar to walk up vertical
|
||||||
|
// walls.
|
||||||
|
const int32_t NUM_POINTS = 6;
|
||||||
|
btVector3 points[NUM_POINTS];
|
||||||
|
btVector3 xAxis = btVector3(1.0f, 0.0f, 0.0f);
|
||||||
|
btVector3 yAxis = btVector3(0.0f, 1.0f, 0.0f);
|
||||||
|
btVector3 zAxis = btVector3(0.0f, 0.0f, 1.0f);
|
||||||
|
points[0] = _halfHeight * yAxis;
|
||||||
|
points[1] = -_halfHeight * yAxis;
|
||||||
|
points[2] = (0.75f * _halfHeight) * yAxis - (0.1f * _radius) * zAxis;
|
||||||
|
points[3] = (0.75f * _halfHeight) * yAxis + (0.1f * _radius) * zAxis;
|
||||||
|
points[4] = (0.75f * _halfHeight) * yAxis - (0.1f * _radius) * xAxis;
|
||||||
|
points[5] = (0.75f * _halfHeight) * yAxis + (0.1f * _radius) * xAxis;
|
||||||
|
btConvexHullShape* shape = new btConvexHullShape(reinterpret_cast<btScalar*>(points), NUM_POINTS);
|
||||||
|
shape->setMargin(_radius);
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyCharacterController::initRayShotgun(const btCollisionWorld* world) {
|
||||||
|
// In order to trace rays out from the avatar's shape surface we need to know where the start points are in
|
||||||
|
// the local-frame. Since the avatar shape is somewhat irregular computing these points by hand is a hassle
|
||||||
|
// so instead we ray-trace backwards to the avatar to find them.
|
||||||
|
//
|
||||||
|
// We trace back a regular grid (see below) of points against the shape and keep any that hit.
|
||||||
|
// ___
|
||||||
|
// + / + \ +
|
||||||
|
// |+ +|
|
||||||
|
// +| + | +
|
||||||
|
// |+ +|
|
||||||
|
// +| + | +
|
||||||
|
// |+ +|
|
||||||
|
// + \ + / +
|
||||||
|
// ---
|
||||||
|
// The shotgun will send rays out from these same points to see if the avatar's shape can proceed through space.
|
||||||
|
|
||||||
|
// helper class for simple ray-traces against character
|
||||||
|
class MeOnlyResultCallback : public btCollisionWorld::ClosestRayResultCallback {
|
||||||
|
public:
|
||||||
|
MeOnlyResultCallback (btRigidBody* me) : btCollisionWorld::ClosestRayResultCallback(btVector3(0.0f, 0.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)) {
|
||||||
|
_me = me;
|
||||||
|
m_collisionFilterGroup = BULLET_COLLISION_GROUP_DYNAMIC;
|
||||||
|
m_collisionFilterMask = BULLET_COLLISION_MASK_DYNAMIC;
|
||||||
|
}
|
||||||
|
virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult,bool normalInWorldSpace) override {
|
||||||
|
if (rayResult.m_collisionObject != _me) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
return ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace);
|
||||||
|
}
|
||||||
|
btRigidBody* _me;
|
||||||
|
};
|
||||||
|
|
||||||
|
const btScalar fullHalfHeight = _radius + _halfHeight;
|
||||||
|
const btScalar divisionLine = -fullHalfHeight + _maxStepHeight; // line between top and bottom
|
||||||
|
const btScalar topHeight = fullHalfHeight - divisionLine;
|
||||||
|
const btScalar slop = 0.02f;
|
||||||
|
|
||||||
|
const int32_t NUM_ROWS = 5; // must be odd number > 1
|
||||||
|
const int32_t NUM_COLUMNS = 5; // must be odd number > 1
|
||||||
|
btVector3 reach = (2.0f * _radius) * btVector3(0.0f, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
{ // top points
|
||||||
|
_topPoints.clear();
|
||||||
|
_topPoints.reserve(NUM_ROWS * NUM_COLUMNS);
|
||||||
|
btScalar stepY = (topHeight - slop) / (btScalar)(NUM_ROWS - 1);
|
||||||
|
btScalar stepX = 2.0f * (_radius - slop) / (btScalar)(NUM_COLUMNS - 1);
|
||||||
|
|
||||||
|
btTransform transform = _rigidBody->getWorldTransform();
|
||||||
|
btVector3 position = transform.getOrigin();
|
||||||
|
btMatrix3x3 rotation = transform.getBasis();
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < NUM_ROWS; ++i) {
|
||||||
|
int32_t maxJ = NUM_COLUMNS;
|
||||||
|
btScalar offsetX = -(btScalar)((NUM_COLUMNS - 1) / 2) * stepX;
|
||||||
|
if (i % 2 == 1) {
|
||||||
|
// odd rows have one less point and start a halfStep closer
|
||||||
|
maxJ -= 1;
|
||||||
|
offsetX += 0.5f * stepX;
|
||||||
|
}
|
||||||
|
for (int32_t j = 0; j < maxJ; ++j) {
|
||||||
|
btVector3 localRayEnd(offsetX + (btScalar)(j) * stepX, divisionLine + (btScalar)(i) * stepY, 0.0f);
|
||||||
|
btVector3 localRayStart = localRayEnd - reach;
|
||||||
|
MeOnlyResultCallback result(_rigidBody);
|
||||||
|
world->rayTest(position + rotation * localRayStart, position + rotation * localRayEnd, result);
|
||||||
|
if (result.m_closestHitFraction < 1.0f) {
|
||||||
|
_topPoints.push_back(localRayStart + result.m_closestHitFraction * reach);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // bottom points
|
||||||
|
_bottomPoints.clear();
|
||||||
|
_bottomPoints.reserve(NUM_ROWS * NUM_COLUMNS);
|
||||||
|
|
||||||
|
btScalar steepestStepHitHeight = (_radius + 0.04f) * (1.0f - DEFAULT_MIN_FLOOR_NORMAL_DOT_UP);
|
||||||
|
btScalar stepY = (_maxStepHeight - slop - steepestStepHitHeight) / (btScalar)(NUM_ROWS - 1);
|
||||||
|
btScalar stepX = 2.0f * (_radius - slop) / (btScalar)(NUM_COLUMNS - 1);
|
||||||
|
|
||||||
|
btTransform transform = _rigidBody->getWorldTransform();
|
||||||
|
btVector3 position = transform.getOrigin();
|
||||||
|
btMatrix3x3 rotation = transform.getBasis();
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < NUM_ROWS; ++i) {
|
||||||
|
int32_t maxJ = NUM_COLUMNS;
|
||||||
|
btScalar offsetX = -(btScalar)((NUM_COLUMNS - 1) / 2) * stepX;
|
||||||
|
if (i % 2 == 1) {
|
||||||
|
// odd rows have one less point and start a halfStep closer
|
||||||
|
maxJ -= 1;
|
||||||
|
offsetX += 0.5f * stepX;
|
||||||
|
}
|
||||||
|
for (int32_t j = 0; j < maxJ; ++j) {
|
||||||
|
btVector3 localRayEnd(offsetX + (btScalar)(j) * stepX, (divisionLine - slop) - (btScalar)(i) * stepY, 0.0f);
|
||||||
|
btVector3 localRayStart = localRayEnd - reach;
|
||||||
|
MeOnlyResultCallback result(_rigidBody);
|
||||||
|
world->rayTest(position + rotation * localRayStart, position + rotation * localRayEnd, result);
|
||||||
|
if (result.m_closestHitFraction < 1.0f) {
|
||||||
|
_bottomPoints.push_back(localRayStart + result.m_closestHitFraction * reach);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,10 +24,34 @@ public:
|
||||||
explicit MyCharacterController(MyAvatar* avatar);
|
explicit MyCharacterController(MyAvatar* avatar);
|
||||||
~MyCharacterController ();
|
~MyCharacterController ();
|
||||||
|
|
||||||
virtual void updateShapeIfNecessary() override;
|
void setDynamicsWorld(btDynamicsWorld* world) override;
|
||||||
|
void updateShapeIfNecessary() override;
|
||||||
|
|
||||||
|
// Sweeping a convex shape through the physics simulation can be expensive when the obstacles are too
|
||||||
|
// complex (e.g. small 20k triangle static mesh) so instead we cast several rays forward and if they
|
||||||
|
// don't hit anything we consider it a clean sweep. Hence this "Shotgun" code.
|
||||||
|
class RayShotgunResult {
|
||||||
|
public:
|
||||||
|
void reset();
|
||||||
|
float hitFraction { 1.0f };
|
||||||
|
bool walkable { true };
|
||||||
|
};
|
||||||
|
|
||||||
|
/// return true if RayShotgun hits anything
|
||||||
|
bool testRayShotgun(const glm::vec3& position, const glm::vec3& step, RayShotgunResult& result);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void initRayShotgun(const btCollisionWorld* world);
|
||||||
|
|
||||||
|
private:
|
||||||
|
btConvexHullShape* computeShape() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
MyAvatar* _avatar { nullptr };
|
MyAvatar* _avatar { nullptr };
|
||||||
|
|
||||||
|
// shotgun scan data
|
||||||
|
btAlignedObjectArray<btVector3> _topPoints;
|
||||||
|
btAlignedObjectArray<btVector3> _bottomPoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_MyCharacterController_h
|
#endif // hifi_MyCharacterController_h
|
||||||
|
|
|
@ -37,7 +37,14 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||||
Head* head = _owningAvatar->getHead();
|
Head* head = _owningAvatar->getHead();
|
||||||
|
|
||||||
// make sure lookAt is not too close to face (avoid crosseyes)
|
// make sure lookAt is not too close to face (avoid crosseyes)
|
||||||
glm::vec3 lookAt = _owningAvatar->isMyAvatar() ? head->getLookAtPosition() : head->getCorrectedLookAtPosition();
|
glm::vec3 lookAt = head->getLookAtPosition();
|
||||||
|
glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition();
|
||||||
|
float focusDistance = glm::length(focusOffset);
|
||||||
|
const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f;
|
||||||
|
if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) {
|
||||||
|
lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset;
|
||||||
|
}
|
||||||
|
|
||||||
MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
|
MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
|
||||||
|
|
||||||
Rig::HeadParameters headParams;
|
Rig::HeadParameters headParams;
|
||||||
|
@ -140,6 +147,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||||
auto orientation = myAvatar->getLocalOrientation();
|
auto orientation = myAvatar->getLocalOrientation();
|
||||||
_rig->computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState);
|
_rig->computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState);
|
||||||
|
|
||||||
|
// evaluate AnimGraph animation and update jointStates.
|
||||||
|
Model::updateRig(deltaTime, parentTransform);
|
||||||
|
|
||||||
Rig::EyeParameters eyeParams;
|
Rig::EyeParameters eyeParams;
|
||||||
eyeParams.eyeLookAt = lookAt;
|
eyeParams.eyeLookAt = lookAt;
|
||||||
eyeParams.eyeSaccade = head->getSaccade();
|
eyeParams.eyeSaccade = head->getSaccade();
|
||||||
|
@ -149,8 +159,5 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||||
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
|
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
|
||||||
|
|
||||||
_rig->updateFromEyeParameters(eyeParams);
|
_rig->updateFromEyeParameters(eyeParams);
|
||||||
|
|
||||||
// evaluate AnimGraph animation and update jointStates.
|
|
||||||
Parent::updateRig(deltaTime, parentTransform);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,42 +76,58 @@ using Mutex = std::mutex;
|
||||||
using Lock = std::unique_lock<Mutex>;
|
using Lock = std::unique_lock<Mutex>;
|
||||||
static Mutex _deviceMutex;
|
static Mutex _deviceMutex;
|
||||||
|
|
||||||
// background thread that continuously polls for device changes
|
class BackgroundThread : public QThread {
|
||||||
class CheckDevicesThread : public QThread {
|
|
||||||
public:
|
public:
|
||||||
const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000;
|
BackgroundThread(AudioClient* client) : QThread((QObject*)client), _client(client) {}
|
||||||
|
virtual void join() = 0;
|
||||||
|
protected:
|
||||||
|
AudioClient* _client;
|
||||||
|
};
|
||||||
|
|
||||||
CheckDevicesThread(AudioClient* audioClient)
|
// background thread continuously polling device changes
|
||||||
: _audioClient(audioClient) {
|
class CheckDevicesThread : public BackgroundThread {
|
||||||
}
|
public:
|
||||||
|
CheckDevicesThread(AudioClient* client) : BackgroundThread(client) {}
|
||||||
void beforeAboutToQuit() {
|
|
||||||
Lock lock(_checkDevicesMutex);
|
void join() override {
|
||||||
_quit = true;
|
_shouldQuit = true;
|
||||||
|
std::unique_lock<std::mutex> lock(_joinMutex);
|
||||||
|
_joinCondition.wait(lock, [&]{ return !_isRunning; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
void run() override {
|
void run() override {
|
||||||
while (true) {
|
while (!_shouldQuit) {
|
||||||
{
|
_client->checkDevices();
|
||||||
Lock lock(_checkDevicesMutex);
|
|
||||||
if (_quit) {
|
const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000;
|
||||||
break;
|
|
||||||
}
|
|
||||||
_audioClient->checkDevices();
|
|
||||||
}
|
|
||||||
QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS);
|
QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS);
|
||||||
}
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(_joinMutex);
|
||||||
|
_isRunning = false;
|
||||||
|
_joinCondition.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AudioClient* _audioClient { nullptr };
|
std::atomic<bool> _shouldQuit { false };
|
||||||
Mutex _checkDevicesMutex;
|
bool _isRunning { true };
|
||||||
bool _quit { false };
|
std::mutex _joinMutex;
|
||||||
|
std::condition_variable _joinCondition;
|
||||||
};
|
};
|
||||||
|
|
||||||
void AudioInjectorsThread::prepare() {
|
// background thread buffering local injectors
|
||||||
_audio->prepareLocalAudioInjectors();
|
class LocalInjectorsThread : public BackgroundThread {
|
||||||
}
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
LocalInjectorsThread(AudioClient* client) : BackgroundThread(client) {}
|
||||||
|
|
||||||
|
void join() override { return; }
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void prepare() { _client->prepareLocalAudioInjectors(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
#include "AudioClient.moc"
|
||||||
|
|
||||||
static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) {
|
static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) {
|
||||||
for (int i = 0; i < numSamples/2; i++) {
|
for (int i = 0; i < numSamples/2; i++) {
|
||||||
|
@ -179,7 +195,6 @@ AudioClient::AudioClient() :
|
||||||
_inputToNetworkResampler(NULL),
|
_inputToNetworkResampler(NULL),
|
||||||
_networkToOutputResampler(NULL),
|
_networkToOutputResampler(NULL),
|
||||||
_localToOutputResampler(NULL),
|
_localToOutputResampler(NULL),
|
||||||
_localAudioThread(this),
|
|
||||||
_audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT),
|
_audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT),
|
||||||
_outgoingAvatarAudioSequenceNumber(0),
|
_outgoingAvatarAudioSequenceNumber(0),
|
||||||
_audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this),
|
_audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this),
|
||||||
|
@ -210,13 +225,14 @@ AudioClient::AudioClient() :
|
||||||
|
|
||||||
// start a thread to detect any device changes
|
// start a thread to detect any device changes
|
||||||
_checkDevicesThread = new CheckDevicesThread(this);
|
_checkDevicesThread = new CheckDevicesThread(this);
|
||||||
_checkDevicesThread->setObjectName("CheckDevices Thread");
|
_checkDevicesThread->setObjectName("AudioClient CheckDevices Thread");
|
||||||
_checkDevicesThread->setPriority(QThread::LowPriority);
|
_checkDevicesThread->setPriority(QThread::LowPriority);
|
||||||
_checkDevicesThread->start();
|
_checkDevicesThread->start();
|
||||||
|
|
||||||
// start a thread to process local injectors
|
// start a thread to process local injectors
|
||||||
_localAudioThread.setObjectName("LocalAudio Thread");
|
_localInjectorsThread = new LocalInjectorsThread(this);
|
||||||
_localAudioThread.start();
|
_localInjectorsThread->setObjectName("AudioClient LocalInjectors Thread");
|
||||||
|
_localInjectorsThread->start();
|
||||||
|
|
||||||
configureReverb();
|
configureReverb();
|
||||||
|
|
||||||
|
@ -231,18 +247,32 @@ AudioClient::AudioClient() :
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioClient::~AudioClient() {
|
AudioClient::~AudioClient() {
|
||||||
delete _checkDevicesThread;
|
|
||||||
stop();
|
|
||||||
if (_codec && _encoder) {
|
if (_codec && _encoder) {
|
||||||
_codec->releaseEncoder(_encoder);
|
_codec->releaseEncoder(_encoder);
|
||||||
_encoder = nullptr;
|
_encoder = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioClient::beforeAboutToQuit() {
|
void AudioClient::customDeleter() {
|
||||||
static_cast<CheckDevicesThread*>(_checkDevicesThread)->beforeAboutToQuit();
|
deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AudioClient::cleanupBeforeQuit() {
|
||||||
|
// FIXME: this should be put in customDeleter, but there is still a reference to this when it is called,
|
||||||
|
// so this must be explicitly, synchronously stopped
|
||||||
|
|
||||||
|
stop();
|
||||||
|
|
||||||
|
if (_checkDevicesThread) {
|
||||||
|
static_cast<BackgroundThread*>(_checkDevicesThread)->join();
|
||||||
|
delete _checkDevicesThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_localInjectorsThread) {
|
||||||
|
static_cast<BackgroundThread*>(_localInjectorsThread)->join();
|
||||||
|
delete _localInjectorsThread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
|
void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
|
||||||
qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec;
|
qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec;
|
||||||
|
@ -1096,11 +1126,19 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) {
|
||||||
handleAudioInput(audioBuffer);
|
handleAudioInput(audioBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioClient::prepareLocalAudioInjectors() {
|
void AudioClient::prepareLocalAudioInjectors(std::unique_ptr<Lock> localAudioLock) {
|
||||||
|
bool doSynchronously = localAudioLock.operator bool();
|
||||||
|
if (!localAudioLock) {
|
||||||
|
localAudioLock.reset(new Lock(_localAudioMutex));
|
||||||
|
}
|
||||||
|
|
||||||
int samplesNeeded = std::numeric_limits<int>::max();
|
int samplesNeeded = std::numeric_limits<int>::max();
|
||||||
while (samplesNeeded > 0) {
|
while (samplesNeeded > 0) {
|
||||||
|
if (!doSynchronously) {
|
||||||
// unlock between every write to allow device switching
|
// unlock between every write to allow device switching
|
||||||
Lock lock(_localAudioMutex);
|
localAudioLock->unlock();
|
||||||
|
localAudioLock->lock();
|
||||||
|
}
|
||||||
|
|
||||||
// in case of a device switch, consider bufferCapacity volatile across iterations
|
// in case of a device switch, consider bufferCapacity volatile across iterations
|
||||||
if (_outputPeriod == 0) {
|
if (_outputPeriod == 0) {
|
||||||
|
@ -1154,16 +1192,16 @@ void AudioClient::prepareLocalAudioInjectors() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
|
// check the flag for injectors before attempting to lock
|
||||||
QVector<AudioInjector*> injectorsToRemove;
|
if (!_localInjectorsAvailable.load(std::memory_order_acquire)) {
|
||||||
|
|
||||||
// lock the injector vector
|
|
||||||
Lock lock(_injectorsMutex);
|
|
||||||
|
|
||||||
if (_activeLocalAudioInjectors.size() == 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lock the injectors
|
||||||
|
Lock lock(_injectorsMutex);
|
||||||
|
|
||||||
|
QVector<AudioInjector*> injectorsToRemove;
|
||||||
|
|
||||||
memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float));
|
memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float));
|
||||||
|
|
||||||
for (AudioInjector* injector : _activeLocalAudioInjectors) {
|
for (AudioInjector* injector : _activeLocalAudioInjectors) {
|
||||||
|
@ -1242,6 +1280,9 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
_activeLocalAudioInjectors.removeOne(injector);
|
_activeLocalAudioInjectors.removeOne(injector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update the flag
|
||||||
|
_localInjectorsAvailable.exchange(!_activeLocalAudioInjectors.empty(), std::memory_order_release);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1328,7 +1369,10 @@ bool AudioClient::outputLocalInjector(AudioInjector* injector) {
|
||||||
|
|
||||||
// move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop())
|
// move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop())
|
||||||
injectorBuffer->setParent(nullptr);
|
injectorBuffer->setParent(nullptr);
|
||||||
injectorBuffer->moveToThread(&_localAudioThread);
|
injectorBuffer->moveToThread(_localInjectorsThread);
|
||||||
|
|
||||||
|
// update the flag
|
||||||
|
_localInjectorsAvailable.exchange(true, std::memory_order_release);
|
||||||
} else {
|
} else {
|
||||||
qCDebug(audioclient) << "injector exists in active list already";
|
qCDebug(audioclient) << "injector exists in active list already";
|
||||||
}
|
}
|
||||||
|
@ -1455,7 +1499,7 @@ void AudioClient::outputNotify() {
|
||||||
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
|
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
|
||||||
bool supportedFormat = false;
|
bool supportedFormat = false;
|
||||||
|
|
||||||
Lock lock(_localAudioMutex);
|
Lock localAudioLock(_localAudioMutex);
|
||||||
_localSamplesAvailable.exchange(0, std::memory_order_release);
|
_localSamplesAvailable.exchange(0, std::memory_order_release);
|
||||||
|
|
||||||
// cleanup any previously initialized device
|
// cleanup any previously initialized device
|
||||||
|
@ -1525,14 +1569,23 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
|
||||||
connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) {
|
connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) {
|
||||||
if (state == QAudio::ActiveState) {
|
if (state == QAudio::ActiveState) {
|
||||||
// restrict device callback to _outputPeriod samples
|
// restrict device callback to _outputPeriod samples
|
||||||
_outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2;
|
_outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE;
|
||||||
|
// device callback may exceed reported period, so double it to avoid stutter
|
||||||
|
_outputPeriod *= 2;
|
||||||
|
|
||||||
_outputMixBuffer = new float[_outputPeriod];
|
_outputMixBuffer = new float[_outputPeriod];
|
||||||
_outputScratchBuffer = new int16_t[_outputPeriod];
|
_outputScratchBuffer = new int16_t[_outputPeriod];
|
||||||
|
|
||||||
// size local output mix buffer based on resampled network frame size
|
// size local output mix buffer based on resampled network frame size
|
||||||
_networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
|
int networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
|
||||||
_localOutputMixBuffer = new float[_networkPeriod];
|
_localOutputMixBuffer = new float[networkPeriod];
|
||||||
|
|
||||||
|
// local period should be at least twice the output period,
|
||||||
|
// in case two device reads happen before more data can be read (worst case)
|
||||||
int localPeriod = _outputPeriod * 2;
|
int localPeriod = _outputPeriod * 2;
|
||||||
|
// round up to an exact multiple of networkPeriod
|
||||||
|
localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod;
|
||||||
|
// this ensures lowest latency without stutter from underrun
|
||||||
_localInjectorsStream.resizeForFrameSize(localPeriod);
|
_localInjectorsStream.resizeForFrameSize(localPeriod);
|
||||||
|
|
||||||
int bufferSize = _audioOutput->bufferSize();
|
int bufferSize = _audioOutput->bufferSize();
|
||||||
|
@ -1547,6 +1600,9 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
|
||||||
qCDebug(audioclient) << "local buffer (samples):" << localPeriod;
|
qCDebug(audioclient) << "local buffer (samples):" << localPeriod;
|
||||||
|
|
||||||
disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0);
|
disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0);
|
||||||
|
|
||||||
|
// unlock to avoid a deadlock with the device callback (which always succeeds this initialization)
|
||||||
|
localAudioLock.unlock();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify);
|
connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify);
|
||||||
|
@ -1685,12 +1741,24 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
|
||||||
int injectorSamplesPopped = 0;
|
int injectorSamplesPopped = 0;
|
||||||
{
|
{
|
||||||
bool append = networkSamplesPopped > 0;
|
bool append = networkSamplesPopped > 0;
|
||||||
// this does not require a lock as of the only two functions adding to _localSamplesAvailable (samples count):
|
// check the samples we have available locklessly; this is possible because only two functions add to the count:
|
||||||
// - prepareLocalAudioInjectors will only increase samples count
|
// - prepareLocalAudioInjectors will only increase samples count
|
||||||
// - switchOutputToAudioDevice will zero samples count
|
// - switchOutputToAudioDevice will zero samples count,
|
||||||
// stop the device, so that readData will exhaust the existing buffer or see a zeroed 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
|
// and start the device - which can then only see a zeroed samples count
|
||||||
samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire));
|
int samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire);
|
||||||
|
|
||||||
|
// if we do not have enough samples buffered despite having injectors, buffer them synchronously
|
||||||
|
if (samplesAvailable < samplesRequested && _audio->_localInjectorsAvailable.load(std::memory_order_acquire)) {
|
||||||
|
// try_to_lock, in case the device is being shut down already
|
||||||
|
std::unique_ptr<Lock> localAudioLock(new Lock(_audio->_localAudioMutex, std::try_to_lock));
|
||||||
|
if (localAudioLock->owns_lock()) {
|
||||||
|
_audio->prepareLocalAudioInjectors(std::move(localAudioLock));
|
||||||
|
samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
samplesRequested = std::min(samplesRequested, samplesAvailable);
|
||||||
if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) {
|
if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) {
|
||||||
_audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release);
|
_audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release);
|
||||||
qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested);
|
qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested);
|
||||||
|
@ -1698,7 +1766,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare injectors for the next callback
|
// prepare injectors for the next callback
|
||||||
QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection);
|
QMetaObject::invokeMethod(_audio->_localInjectorsThread, "prepare", Qt::QueuedConnection);
|
||||||
|
|
||||||
int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped);
|
int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped);
|
||||||
int framesPopped = samplesPopped / AudioConstants::STEREO;
|
int framesPopped = samplesPopped / AudioConstants::STEREO;
|
||||||
|
|
|
@ -71,19 +71,6 @@ class QIODevice;
|
||||||
class Transform;
|
class Transform;
|
||||||
class NLPacket;
|
class NLPacket;
|
||||||
|
|
||||||
class AudioInjectorsThread : public QThread {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
AudioInjectorsThread(AudioClient* audio) : _audio(audio) {}
|
|
||||||
|
|
||||||
public slots :
|
|
||||||
void prepare();
|
|
||||||
|
|
||||||
private:
|
|
||||||
AudioClient* _audio;
|
|
||||||
};
|
|
||||||
|
|
||||||
class AudioClient : public AbstractAudioInterface, public Dependency {
|
class AudioClient : public AbstractAudioInterface, public Dependency {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
SINGLETON_DEPENDENCY
|
SINGLETON_DEPENDENCY
|
||||||
|
@ -158,7 +145,7 @@ public:
|
||||||
|
|
||||||
Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
|
Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
|
||||||
|
|
||||||
void checkDevices();
|
bool outputLocalInjector(AudioInjector* injector) override;
|
||||||
|
|
||||||
static const float CALLBACK_ACCELERATOR_RATIO;
|
static const float CALLBACK_ACCELERATOR_RATIO;
|
||||||
|
|
||||||
|
@ -169,6 +156,7 @@ public:
|
||||||
public slots:
|
public slots:
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
void stop();
|
||||||
|
void cleanupBeforeQuit();
|
||||||
|
|
||||||
void handleAudioEnvironmentDataPacket(QSharedPointer<ReceivedMessage> message);
|
void handleAudioEnvironmentDataPacket(QSharedPointer<ReceivedMessage> message);
|
||||||
void handleAudioDataPacket(QSharedPointer<ReceivedMessage> message);
|
void handleAudioDataPacket(QSharedPointer<ReceivedMessage> message);
|
||||||
|
@ -184,8 +172,6 @@ public slots:
|
||||||
void audioMixerKilled();
|
void audioMixerKilled();
|
||||||
void toggleMute();
|
void toggleMute();
|
||||||
|
|
||||||
void beforeAboutToQuit();
|
|
||||||
|
|
||||||
virtual void setIsStereoInput(bool stereo) override;
|
virtual void setIsStereoInput(bool stereo) override;
|
||||||
|
|
||||||
void toggleAudioNoiseReduction() { _isNoiseGateEnabled = !_isNoiseGateEnabled; }
|
void toggleAudioNoiseReduction() { _isNoiseGateEnabled = !_isNoiseGateEnabled; }
|
||||||
|
@ -198,8 +184,6 @@ public slots:
|
||||||
|
|
||||||
int setOutputBufferSize(int numFrames, bool persist = true);
|
int setOutputBufferSize(int numFrames, bool persist = true);
|
||||||
|
|
||||||
void prepareLocalAudioInjectors();
|
|
||||||
bool outputLocalInjector(AudioInjector* injector) override;
|
|
||||||
bool shouldLoopbackInjectors() override { return _shouldEchoToServer; }
|
bool shouldLoopbackInjectors() override { return _shouldEchoToServer; }
|
||||||
|
|
||||||
bool switchInputToAudioDevice(const QString& inputDeviceName);
|
bool switchInputToAudioDevice(const QString& inputDeviceName);
|
||||||
|
@ -242,13 +226,16 @@ protected:
|
||||||
AudioClient();
|
AudioClient();
|
||||||
~AudioClient();
|
~AudioClient();
|
||||||
|
|
||||||
virtual void customDeleter() override {
|
virtual void customDeleter() override;
|
||||||
deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class CheckDevicesThread;
|
||||||
|
friend class LocalInjectorsThread;
|
||||||
|
|
||||||
void outputFormatChanged();
|
void outputFormatChanged();
|
||||||
void handleAudioInput(QByteArray& audioBuffer);
|
void handleAudioInput(QByteArray& audioBuffer);
|
||||||
|
void checkDevices();
|
||||||
|
void prepareLocalAudioInjectors(std::unique_ptr<Lock> localAudioLock = nullptr);
|
||||||
bool mixLocalAudioInjectors(float* mixBuffer);
|
bool mixLocalAudioInjectors(float* mixBuffer);
|
||||||
float azimuthForSource(const glm::vec3& relativePosition);
|
float azimuthForSource(const glm::vec3& relativePosition);
|
||||||
float gainForSource(float distance, float volume);
|
float gainForSource(float distance, float volume);
|
||||||
|
@ -295,8 +282,9 @@ private:
|
||||||
AudioRingBuffer _inputRingBuffer;
|
AudioRingBuffer _inputRingBuffer;
|
||||||
LocalInjectorsStream _localInjectorsStream;
|
LocalInjectorsStream _localInjectorsStream;
|
||||||
// In order to use _localInjectorsStream as a lock-free pipe,
|
// In order to use _localInjectorsStream as a lock-free pipe,
|
||||||
// use it with a single producer/consumer, and track available samples
|
// use it with a single producer/consumer, and track available samples and injectors
|
||||||
std::atomic<int> _localSamplesAvailable { 0 };
|
std::atomic<int> _localSamplesAvailable { 0 };
|
||||||
|
std::atomic<bool> _localInjectorsAvailable { false };
|
||||||
MixedProcessedAudioStream _receivedAudioStream;
|
MixedProcessedAudioStream _receivedAudioStream;
|
||||||
bool _isStereoInput;
|
bool _isStereoInput;
|
||||||
|
|
||||||
|
@ -337,19 +325,17 @@ private:
|
||||||
// for network audio (used by network audio thread)
|
// for network audio (used by network audio thread)
|
||||||
int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
|
int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
|
||||||
|
|
||||||
// for local audio (used by audio injectors thread)
|
|
||||||
int _networkPeriod { 0 };
|
|
||||||
float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
|
|
||||||
int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
|
|
||||||
float* _localOutputMixBuffer { NULL };
|
|
||||||
AudioInjectorsThread _localAudioThread;
|
|
||||||
Mutex _localAudioMutex;
|
|
||||||
|
|
||||||
// for output audio (used by this thread)
|
// for output audio (used by this thread)
|
||||||
int _outputPeriod { 0 };
|
int _outputPeriod { 0 };
|
||||||
float* _outputMixBuffer { NULL };
|
float* _outputMixBuffer { NULL };
|
||||||
int16_t* _outputScratchBuffer { NULL };
|
int16_t* _outputScratchBuffer { NULL };
|
||||||
|
|
||||||
|
// for local audio (used by audio injectors thread)
|
||||||
|
float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
|
||||||
|
int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
|
||||||
|
float* _localOutputMixBuffer { NULL };
|
||||||
|
Mutex _localAudioMutex;
|
||||||
|
|
||||||
AudioLimiter _audioLimiter;
|
AudioLimiter _audioLimiter;
|
||||||
|
|
||||||
// Adds Reverb
|
// Adds Reverb
|
||||||
|
@ -392,12 +378,13 @@ private:
|
||||||
QString _selectedCodecName;
|
QString _selectedCodecName;
|
||||||
Encoder* _encoder { nullptr }; // for outbound mic stream
|
Encoder* _encoder { nullptr }; // for outbound mic stream
|
||||||
|
|
||||||
QThread* _checkDevicesThread { nullptr };
|
|
||||||
|
|
||||||
RateCounter<> _silentOutbound;
|
RateCounter<> _silentOutbound;
|
||||||
RateCounter<> _audioOutbound;
|
RateCounter<> _audioOutbound;
|
||||||
RateCounter<> _silentInbound;
|
RateCounter<> _silentInbound;
|
||||||
RateCounter<> _audioInbound;
|
RateCounter<> _audioInbound;
|
||||||
|
|
||||||
|
QThread* _checkDevicesThread { nullptr };
|
||||||
|
QThread* _localInjectorsThread { nullptr };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,12 +32,12 @@ public:
|
||||||
const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale,
|
const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale,
|
||||||
PacketType packetType, QString codecName = QString(""));
|
PacketType packetType, QString codecName = QString(""));
|
||||||
|
|
||||||
public slots:
|
|
||||||
// threadsafe
|
// threadsafe
|
||||||
// moves injector->getLocalBuffer() to another thread (so removes its parent)
|
// moves injector->getLocalBuffer() to another thread (so removes its parent)
|
||||||
// take care to delete it when ~AudioInjector, as parenting Qt semantics will not work
|
// take care to delete it when ~AudioInjector, as parenting Qt semantics will not work
|
||||||
virtual bool outputLocalInjector(AudioInjector* injector) = 0;
|
virtual bool outputLocalInjector(AudioInjector* injector) = 0;
|
||||||
|
|
||||||
|
public slots:
|
||||||
virtual bool shouldLoopbackInjectors() { return false; }
|
virtual bool shouldLoopbackInjectors() { return false; }
|
||||||
|
|
||||||
virtual void setIsStereoInput(bool stereo) = 0;
|
virtual void setIsStereoInput(bool stereo) = 0;
|
||||||
|
|
|
@ -369,7 +369,9 @@ void Avatar::simulate(float deltaTime, bool inView) {
|
||||||
PerformanceTimer perfTimer("simulate");
|
PerformanceTimer perfTimer("simulate");
|
||||||
{
|
{
|
||||||
PROFILE_RANGE(simulation, "updateJoints");
|
PROFILE_RANGE(simulation, "updateJoints");
|
||||||
if (inView && _hasNewJointData) {
|
if (inView) {
|
||||||
|
Head* head = getHead();
|
||||||
|
if (_hasNewJointData) {
|
||||||
_skeletonModel->getRig()->copyJointsFromJointData(_jointData);
|
_skeletonModel->getRig()->copyJointsFromJointData(_jointData);
|
||||||
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
|
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
|
||||||
_skeletonModel->getRig()->computeExternalPoses(rootTransform);
|
_skeletonModel->getRig()->computeExternalPoses(rootTransform);
|
||||||
|
@ -384,8 +386,8 @@ void Avatar::simulate(float deltaTime, bool inView) {
|
||||||
if (!_skeletonModel->getHeadPosition(headPosition)) {
|
if (!_skeletonModel->getHeadPosition(headPosition)) {
|
||||||
headPosition = getPosition();
|
headPosition = getPosition();
|
||||||
}
|
}
|
||||||
Head* head = getHead();
|
|
||||||
head->setPosition(headPosition);
|
head->setPosition(headPosition);
|
||||||
|
}
|
||||||
head->setScale(getUniformScale());
|
head->setScale(getUniformScale());
|
||||||
head->simulate(deltaTime);
|
head->simulate(deltaTime);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -73,12 +73,13 @@ void SkeletonModel::initJointStates() {
|
||||||
|
|
||||||
// Called within Model::simulate call, below.
|
// Called within Model::simulate call, below.
|
||||||
void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||||
|
assert(!_owningAvatar->isMyAvatar());
|
||||||
const FBXGeometry& geometry = getFBXGeometry();
|
const FBXGeometry& geometry = getFBXGeometry();
|
||||||
|
|
||||||
Head* head = _owningAvatar->getHead();
|
Head* head = _owningAvatar->getHead();
|
||||||
|
|
||||||
// make sure lookAt is not too close to face (avoid crosseyes)
|
// make sure lookAt is not too close to face (avoid crosseyes)
|
||||||
glm::vec3 lookAt = _owningAvatar->isMyAvatar() ? head->getLookAtPosition() : head->getCorrectedLookAtPosition();
|
glm::vec3 lookAt = head->getCorrectedLookAtPosition();
|
||||||
glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition();
|
glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition();
|
||||||
float focusDistance = glm::length(focusOffset);
|
float focusDistance = glm::length(focusOffset);
|
||||||
const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f;
|
const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f;
|
||||||
|
@ -86,7 +87,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||||
lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset;
|
lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_owningAvatar->isMyAvatar()) {
|
|
||||||
// no need to call Model::updateRig() because otherAvatars get their joint state
|
// no need to call Model::updateRig() because otherAvatars get their joint state
|
||||||
// copied directly from AvtarData::_jointData (there are no Rig animations to blend)
|
// copied directly from AvtarData::_jointData (there are no Rig animations to blend)
|
||||||
_needsUpdateClusterMatrices = true;
|
_needsUpdateClusterMatrices = true;
|
||||||
|
@ -117,10 +117,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||||
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
|
eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
|
||||||
|
|
||||||
_rig->updateFromEyeParameters(eyeParams);
|
_rig->updateFromEyeParameters(eyeParams);
|
||||||
}
|
|
||||||
|
|
||||||
// evaluate AnimGraph animation and update jointStates.
|
|
||||||
Parent::updateRig(deltaTime, parentTransform);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SkeletonModel::updateAttitude() {
|
void SkeletonModel::updateAttitude() {
|
||||||
|
|
|
@ -273,10 +273,9 @@ std::tuple<bool, QByteArray> requestData(QUrl& url) {
|
||||||
return std::make_tuple(false, QByteArray());
|
return std::make_tuple(false, QByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
request->send();
|
|
||||||
|
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
QObject::connect(request, &ResourceRequest::finished, &loop, &QEventLoop::quit);
|
QObject::connect(request, &ResourceRequest::finished, &loop, &QEventLoop::quit);
|
||||||
|
request->send();
|
||||||
loop.exec();
|
loop.exec();
|
||||||
|
|
||||||
if (request->getResult() == ResourceRequest::Success) {
|
if (request->getResult() == ResourceRequest::Success) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// BulletUtil.h
|
// BulletUtil.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.11.02
|
// Created by Andrew Meadows 2014.11.02
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
393
libraries/physics/src/CharacterController.cpp
Normal file → Executable file
393
libraries/physics/src/CharacterController.cpp
Normal file → Executable file
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// CharacterControllerInterface.cpp
|
// CharacterControllerInterface.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2015.10.21
|
// Created by Andrew Meadows 2015.10.21
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
@ -13,8 +13,8 @@
|
||||||
|
|
||||||
#include <NumericalConstants.h>
|
#include <NumericalConstants.h>
|
||||||
|
|
||||||
#include "PhysicsCollisionGroups.h"
|
|
||||||
#include "ObjectMotionState.h"
|
#include "ObjectMotionState.h"
|
||||||
|
#include "PhysicsHelpers.h"
|
||||||
#include "PhysicsLogging.h"
|
#include "PhysicsLogging.h"
|
||||||
|
|
||||||
const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f);
|
const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f);
|
||||||
|
@ -62,10 +62,6 @@ CharacterController::CharacterMotor::CharacterMotor(const glm::vec3& vel, const
|
||||||
}
|
}
|
||||||
|
|
||||||
CharacterController::CharacterController() {
|
CharacterController::CharacterController() {
|
||||||
_halfHeight = 1.0f;
|
|
||||||
|
|
||||||
_enabled = false;
|
|
||||||
|
|
||||||
_floorDistance = MAX_FALL_HEIGHT;
|
_floorDistance = MAX_FALL_HEIGHT;
|
||||||
|
|
||||||
_targetVelocity.setValue(0.0f, 0.0f, 0.0f);
|
_targetVelocity.setValue(0.0f, 0.0f, 0.0f);
|
||||||
|
@ -107,6 +103,7 @@ bool CharacterController::needsAddition() const {
|
||||||
|
|
||||||
void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
|
void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
|
||||||
if (_dynamicsWorld != world) {
|
if (_dynamicsWorld != world) {
|
||||||
|
// remove from old world
|
||||||
if (_dynamicsWorld) {
|
if (_dynamicsWorld) {
|
||||||
if (_rigidBody) {
|
if (_rigidBody) {
|
||||||
_dynamicsWorld->removeRigidBody(_rigidBody);
|
_dynamicsWorld->removeRigidBody(_rigidBody);
|
||||||
|
@ -114,17 +111,23 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
|
||||||
}
|
}
|
||||||
_dynamicsWorld = nullptr;
|
_dynamicsWorld = nullptr;
|
||||||
}
|
}
|
||||||
|
int16_t collisionGroup = computeCollisionGroup();
|
||||||
if (world && _rigidBody) {
|
if (world && _rigidBody) {
|
||||||
|
// add to new world
|
||||||
_dynamicsWorld = world;
|
_dynamicsWorld = world;
|
||||||
_pendingFlags &= ~PENDING_FLAG_JUMP;
|
_pendingFlags &= ~PENDING_FLAG_JUMP;
|
||||||
// Before adding the RigidBody to the world we must save its oldGravity to the side
|
_dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
|
||||||
// because adding an object to the world will overwrite it with the default gravity.
|
|
||||||
btVector3 oldGravity = _rigidBody->getGravity();
|
|
||||||
_dynamicsWorld->addRigidBody(_rigidBody, BULLET_COLLISION_GROUP_MY_AVATAR, BULLET_COLLISION_MASK_MY_AVATAR);
|
|
||||||
_dynamicsWorld->addAction(this);
|
_dynamicsWorld->addAction(this);
|
||||||
// restore gravity settings
|
// restore gravity settings because adding an object to the world overwrites its gravity setting
|
||||||
_rigidBody->setGravity(oldGravity);
|
_rigidBody->setGravity(_gravity * _currentUp);
|
||||||
|
btCollisionShape* shape = _rigidBody->getCollisionShape();
|
||||||
|
assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE);
|
||||||
|
_ghost.setCharacterShape(static_cast<btConvexHullShape*>(shape));
|
||||||
}
|
}
|
||||||
|
_ghost.setCollisionGroupAndMask(collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR & (~ collisionGroup));
|
||||||
|
_ghost.setCollisionWorld(_dynamicsWorld);
|
||||||
|
_ghost.setRadiusAndHalfHeight(_radius, _halfHeight);
|
||||||
|
_ghost.setWorldTransform(_rigidBody->getWorldTransform());
|
||||||
}
|
}
|
||||||
if (_dynamicsWorld) {
|
if (_dynamicsWorld) {
|
||||||
if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) {
|
if (_pendingFlags & PENDING_FLAG_UPDATE_SHAPE) {
|
||||||
|
@ -138,38 +141,78 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const float COS_PI_OVER_THREE = cosf(PI / 3.0f);
|
bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) {
|
||||||
|
bool pushing = _targetVelocity.length2() > FLT_EPSILON;
|
||||||
|
|
||||||
|
btDispatcher* dispatcher = collisionWorld->getDispatcher();
|
||||||
|
int numManifolds = dispatcher->getNumManifolds();
|
||||||
|
bool hasFloor = false;
|
||||||
|
|
||||||
|
btTransform rotation = _rigidBody->getWorldTransform();
|
||||||
|
rotation.setOrigin(btVector3(0.0f, 0.0f, 0.0f)); // clear translation part
|
||||||
|
|
||||||
bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) const {
|
|
||||||
int numManifolds = collisionWorld->getDispatcher()->getNumManifolds();
|
|
||||||
for (int i = 0; i < numManifolds; i++) {
|
for (int i = 0; i < numManifolds; i++) {
|
||||||
btPersistentManifold* contactManifold = collisionWorld->getDispatcher()->getManifoldByIndexInternal(i);
|
btPersistentManifold* contactManifold = dispatcher->getManifoldByIndexInternal(i);
|
||||||
const btCollisionObject* obA = static_cast<const btCollisionObject*>(contactManifold->getBody0());
|
if (_rigidBody == contactManifold->getBody1() || _rigidBody == contactManifold->getBody0()) {
|
||||||
const btCollisionObject* obB = static_cast<const btCollisionObject*>(contactManifold->getBody1());
|
bool characterIsFirst = _rigidBody == contactManifold->getBody0();
|
||||||
if (obA == _rigidBody || obB == _rigidBody) {
|
|
||||||
int numContacts = contactManifold->getNumContacts();
|
int numContacts = contactManifold->getNumContacts();
|
||||||
|
int stepContactIndex = -1;
|
||||||
|
float highestStep = _minStepHeight;
|
||||||
for (int j = 0; j < numContacts; j++) {
|
for (int j = 0; j < numContacts; j++) {
|
||||||
btManifoldPoint& pt = contactManifold->getContactPoint(j);
|
// check for "floor"
|
||||||
|
btManifoldPoint& contact = contactManifold->getContactPoint(j);
|
||||||
|
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 hasFloor;
|
||||||
|
}
|
||||||
|
|
||||||
// check to see if contact point is touching the bottom sphere of the capsule.
|
void CharacterController::updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) {
|
||||||
// and the contact normal is not slanted too much.
|
preStep(collisionWorld);
|
||||||
float contactPointY = (obA == _rigidBody) ? pt.m_localPointA.getY() : pt.m_localPointB.getY();
|
playerStep(collisionWorld, deltaTime);
|
||||||
btVector3 normal = (obA == _rigidBody) ? pt.m_normalWorldOnB : -pt.m_normalWorldOnB;
|
|
||||||
if (contactPointY < -_halfHeight && normal.dot(_currentUp) > COS_PI_OVER_THREE) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::preStep(btCollisionWorld* collisionWorld) {
|
void CharacterController::preStep(btCollisionWorld* collisionWorld) {
|
||||||
// trace a ray straight down to see if we're standing on the ground
|
// trace a ray straight down to see if we're standing on the ground
|
||||||
const btTransform& xform = _rigidBody->getWorldTransform();
|
const btTransform& transform = _rigidBody->getWorldTransform();
|
||||||
|
|
||||||
// rayStart is at center of bottom sphere
|
// rayStart is at center of bottom sphere
|
||||||
btVector3 rayStart = xform.getOrigin() - _halfHeight * _currentUp;
|
btVector3 rayStart = transform.getOrigin() - _halfHeight * _currentUp;
|
||||||
|
|
||||||
// rayEnd is some short distance outside bottom sphere
|
// rayEnd is some short distance outside bottom sphere
|
||||||
const btScalar FLOOR_PROXIMITY_THRESHOLD = 0.3f * _radius;
|
const btScalar FLOOR_PROXIMITY_THRESHOLD = 0.3f * _radius;
|
||||||
|
@ -183,21 +226,16 @@ void CharacterController::preStep(btCollisionWorld* collisionWorld) {
|
||||||
if (rayCallback.hasHit()) {
|
if (rayCallback.hasHit()) {
|
||||||
_floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius;
|
_floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasSupport = checkForSupport(collisionWorld);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const btScalar MIN_TARGET_SPEED = 0.001f;
|
const btScalar MIN_TARGET_SPEED = 0.001f;
|
||||||
const btScalar MIN_TARGET_SPEED_SQUARED = MIN_TARGET_SPEED * MIN_TARGET_SPEED;
|
const btScalar MIN_TARGET_SPEED_SQUARED = MIN_TARGET_SPEED * MIN_TARGET_SPEED;
|
||||||
|
|
||||||
void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) {
|
void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar dt) {
|
||||||
|
_stepHeight = _minStepHeight; // clears memory of last step obstacle
|
||||||
|
_hasSupport = checkForSupport(collisionWorld);
|
||||||
btVector3 velocity = _rigidBody->getLinearVelocity() - _parentVelocity;
|
btVector3 velocity = _rigidBody->getLinearVelocity() - _parentVelocity;
|
||||||
computeNewVelocity(dt, velocity);
|
computeNewVelocity(dt, velocity);
|
||||||
_rigidBody->setLinearVelocity(velocity + _parentVelocity);
|
|
||||||
|
|
||||||
// Dynamicaly compute a follow velocity to move this body toward the _followDesiredBodyTransform.
|
|
||||||
// Rather than add this velocity to velocity the RigidBody, we explicitly teleport the RigidBody towards its goal.
|
|
||||||
// This mirrors the computation done in MyAvatar::FollowHelper::postPhysicsUpdate().
|
|
||||||
|
|
||||||
const float MINIMUM_TIME_REMAINING = 0.005f;
|
const float MINIMUM_TIME_REMAINING = 0.005f;
|
||||||
const float MAX_DISPLACEMENT = 0.5f * _radius;
|
const float MAX_DISPLACEMENT = 0.5f * _radius;
|
||||||
|
@ -231,6 +269,47 @@ void CharacterController::playerStep(btCollisionWorld* dynaWorld, btScalar dt) {
|
||||||
_rigidBody->setWorldTransform(btTransform(endRot, endPos));
|
_rigidBody->setWorldTransform(btTransform(endRot, endPos));
|
||||||
}
|
}
|
||||||
_followTime += dt;
|
_followTime += dt;
|
||||||
|
|
||||||
|
if (_steppingUp) {
|
||||||
|
float horizontalTargetSpeed = (_targetVelocity - _targetVelocity.dot(_currentUp) * _currentUp).length();
|
||||||
|
if (horizontalTargetSpeed > FLT_EPSILON) {
|
||||||
|
// compute a stepUpSpeed that will reach the top of the step in the time it would take
|
||||||
|
// to move over the _stepPoint at target speed
|
||||||
|
float horizontalDistance = (_stepPoint - _stepPoint.dot(_currentUp) * _currentUp).length();
|
||||||
|
float timeToStep = horizontalDistance / horizontalTargetSpeed;
|
||||||
|
float stepUpSpeed = _stepHeight / timeToStep;
|
||||||
|
|
||||||
|
// magically clamp stepUpSpeed to a fraction of horizontalTargetSpeed
|
||||||
|
// to prevent the avatar from moving unreasonably fast according to human eye
|
||||||
|
const float MAX_STEP_UP_SPEED = 0.65f * horizontalTargetSpeed;
|
||||||
|
if (stepUpSpeed > MAX_STEP_UP_SPEED) {
|
||||||
|
stepUpSpeed = MAX_STEP_UP_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add minimum velocity to counteract gravity's displacement during one step
|
||||||
|
// Note: the 0.5 factor comes from the fact that we really want the
|
||||||
|
// average velocity contribution from gravity during the step
|
||||||
|
stepUpSpeed -= 0.5f * _gravity * timeToStep; // remember: _gravity is negative scalar
|
||||||
|
|
||||||
|
btScalar vDotUp = velocity.dot(_currentUp);
|
||||||
|
if (vDotUp < stepUpSpeed) {
|
||||||
|
// character doesn't have enough upward velocity to cover the step so we help using a "sky hook"
|
||||||
|
// which uses micro-teleports rather than applying real velocity
|
||||||
|
// to prevent the avatar from popping up after the step is done
|
||||||
|
btTransform transform = _rigidBody->getWorldTransform();
|
||||||
|
transform.setOrigin(transform.getOrigin() + (dt * stepUpSpeed) * _currentUp);
|
||||||
|
_rigidBody->setWorldTransform(transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't allow the avatar to fall downward when stepping up
|
||||||
|
// since otherwise this would tend to defeat the step-up behavior
|
||||||
|
if (vDotUp < 0.0f) {
|
||||||
|
velocity -= vDotUp * _currentUp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_rigidBody->setLinearVelocity(velocity + _parentVelocity);
|
||||||
|
_ghost.setWorldTransform(_rigidBody->getWorldTransform());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::jump() {
|
void CharacterController::jump() {
|
||||||
|
@ -272,95 +351,100 @@ void CharacterController::setState(State desiredState) {
|
||||||
#ifdef DEBUG_STATE_CHANGE
|
#ifdef DEBUG_STATE_CHANGE
|
||||||
qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason;
|
qCDebug(physics) << "CharacterController::setState" << stateToStr(desiredState) << "from" << stateToStr(_state) << "," << reason;
|
||||||
#endif
|
#endif
|
||||||
if (desiredState == State::Hover && _state != State::Hover) {
|
|
||||||
// hover enter
|
|
||||||
if (_rigidBody) {
|
|
||||||
_rigidBody->setGravity(btVector3(0.0f, 0.0f, 0.0f));
|
|
||||||
}
|
|
||||||
} else if (_state == State::Hover && desiredState != State::Hover) {
|
|
||||||
// hover exit
|
|
||||||
if (_rigidBody) {
|
|
||||||
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_state = desiredState;
|
_state = desiredState;
|
||||||
|
updateGravity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale) {
|
void CharacterController::updateGravity() {
|
||||||
_boxScale = scale;
|
int16_t collisionGroup = computeCollisionGroup();
|
||||||
|
if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) {
|
||||||
|
_gravity = 0.0f;
|
||||||
|
} else {
|
||||||
|
const float DEFAULT_CHARACTER_GRAVITY = -5.0f;
|
||||||
|
_gravity = DEFAULT_CHARACTER_GRAVITY;
|
||||||
|
}
|
||||||
|
if (_rigidBody) {
|
||||||
|
_rigidBody->setGravity(_gravity * _currentUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float x = _boxScale.x;
|
void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale) {
|
||||||
float z = _boxScale.z;
|
float x = scale.x;
|
||||||
|
float z = scale.z;
|
||||||
float radius = 0.5f * sqrtf(0.5f * (x * x + z * z));
|
float radius = 0.5f * sqrtf(0.5f * (x * x + z * z));
|
||||||
float halfHeight = 0.5f * _boxScale.y - radius;
|
float halfHeight = 0.5f * scale.y - radius;
|
||||||
float MIN_HALF_HEIGHT = 0.1f;
|
float MIN_HALF_HEIGHT = 0.1f;
|
||||||
if (halfHeight < MIN_HALF_HEIGHT) {
|
if (halfHeight < MIN_HALF_HEIGHT) {
|
||||||
halfHeight = MIN_HALF_HEIGHT;
|
halfHeight = MIN_HALF_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// compare dimensions
|
// compare dimensions
|
||||||
float radiusDelta = glm::abs(radius - _radius);
|
if (glm::abs(radius - _radius) > FLT_EPSILON || glm::abs(halfHeight - _halfHeight) > FLT_EPSILON) {
|
||||||
float heightDelta = glm::abs(halfHeight - _halfHeight);
|
_radius = radius;
|
||||||
if (radiusDelta < FLT_EPSILON && heightDelta < FLT_EPSILON) {
|
_halfHeight = halfHeight;
|
||||||
// shape hasn't changed --> nothing to do
|
const btScalar DEFAULT_MIN_STEP_HEIGHT_FACTOR = 0.005f;
|
||||||
} else {
|
const btScalar DEFAULT_MAX_STEP_HEIGHT_FACTOR = 0.65f;
|
||||||
|
_minStepHeight = DEFAULT_MIN_STEP_HEIGHT_FACTOR * (_halfHeight + _radius);
|
||||||
|
_maxStepHeight = DEFAULT_MAX_STEP_HEIGHT_FACTOR * (_halfHeight + _radius);
|
||||||
|
|
||||||
if (_dynamicsWorld) {
|
if (_dynamicsWorld) {
|
||||||
// must REMOVE from world prior to shape update
|
// must REMOVE from world prior to shape update
|
||||||
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION;
|
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION;
|
||||||
}
|
}
|
||||||
_pendingFlags |= PENDING_FLAG_UPDATE_SHAPE;
|
_pendingFlags |= PENDING_FLAG_UPDATE_SHAPE;
|
||||||
// only need to ADD back when we happen to be enabled
|
|
||||||
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
|
// it's ok to change offset immediately -- there are no thread safety issues here
|
||||||
_shapeLocalOffset = corner + 0.5f * _boxScale;
|
_shapeLocalOffset = minCorner + 0.5f * scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::setEnabled(bool enabled) {
|
void CharacterController::setCollisionless(bool collisionless) {
|
||||||
if (enabled != _enabled) {
|
if (collisionless != _collisionless) {
|
||||||
if (enabled) {
|
_collisionless = collisionless;
|
||||||
// Don't bother clearing REMOVE bit since it might be paired with an UPDATE_SHAPE bit.
|
_pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP;
|
||||||
// Setting the ADD bit here works for all cases so we don't even bother checking other bits.
|
}
|
||||||
_pendingFlags |= PENDING_FLAG_ADD_TO_SIMULATION;
|
}
|
||||||
|
|
||||||
|
int16_t CharacterController::computeCollisionGroup() const {
|
||||||
|
if (_collisionless) {
|
||||||
|
return _collisionlessAllowed ? BULLET_COLLISION_GROUP_COLLISIONLESS : BULLET_COLLISION_GROUP_MY_AVATAR;
|
||||||
} else {
|
} 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) {
|
if (_dynamicsWorld) {
|
||||||
_pendingFlags |= PENDING_FLAG_REMOVE_FROM_SIMULATION;
|
_dynamicsWorld->removeRigidBody(_rigidBody);
|
||||||
|
int16_t collisionGroup = computeCollisionGroup();
|
||||||
|
_dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
|
||||||
}
|
}
|
||||||
_pendingFlags &= ~ PENDING_FLAG_ADD_TO_SIMULATION;
|
_pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP;
|
||||||
}
|
updateGravity();
|
||||||
SET_STATE(State::Hover, "setEnabled");
|
|
||||||
_enabled = enabled;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::updateUpAxis(const glm::quat& rotation) {
|
void CharacterController::updateUpAxis(const glm::quat& rotation) {
|
||||||
btVector3 oldUp = _currentUp;
|
|
||||||
_currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS);
|
_currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS);
|
||||||
if (_state != State::Hover) {
|
if (_state != State::Hover && _rigidBody) {
|
||||||
const btScalar MIN_UP_ERROR = 0.01f;
|
_rigidBody->setGravity(_gravity * _currentUp);
|
||||||
if (oldUp.distance(_currentUp) > MIN_UP_ERROR) {
|
|
||||||
_rigidBody->setGravity(DEFAULT_CHARACTER_GRAVITY * _currentUp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::setPositionAndOrientation(
|
void CharacterController::setPositionAndOrientation(
|
||||||
const glm::vec3& position,
|
const glm::vec3& position,
|
||||||
const glm::quat& orientation) {
|
const glm::quat& orientation) {
|
||||||
// TODO: update gravity if up has changed
|
|
||||||
updateUpAxis(orientation);
|
updateUpAxis(orientation);
|
||||||
|
_rotation = glmToBullet(orientation);
|
||||||
btQuaternion bodyOrientation = glmToBullet(orientation);
|
_position = glmToBullet(position + orientation * _shapeLocalOffset);
|
||||||
btVector3 bodyPosition = glmToBullet(position + orientation * _shapeLocalOffset);
|
|
||||||
_characterBodyTransform = btTransform(bodyOrientation, bodyPosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const {
|
void CharacterController::getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const {
|
||||||
if (_enabled && _rigidBody) {
|
if (_rigidBody) {
|
||||||
const btTransform& avatarTransform = _rigidBody->getWorldTransform();
|
const btTransform& avatarTransform = _rigidBody->getWorldTransform();
|
||||||
rotation = bulletToGLM(avatarTransform.getRotation());
|
rotation = bulletToGLM(avatarTransform.getRotation());
|
||||||
position = bulletToGLM(avatarTransform.getOrigin()) - rotation * _shapeLocalOffset;
|
position = bulletToGLM(avatarTransform.getOrigin()) - rotation * _shapeLocalOffset;
|
||||||
|
@ -428,16 +512,19 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
|
||||||
btScalar angle = motor.rotation.getAngle();
|
btScalar angle = motor.rotation.getAngle();
|
||||||
btVector3 velocity = worldVelocity.rotate(axis, -angle);
|
btVector3 velocity = worldVelocity.rotate(axis, -angle);
|
||||||
|
|
||||||
if (_state == State::Hover || motor.hTimescale == motor.vTimescale) {
|
int16_t collisionGroup = computeCollisionGroup();
|
||||||
|
if (collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ||
|
||||||
|
_state == State::Hover || motor.hTimescale == motor.vTimescale) {
|
||||||
// modify velocity
|
// modify velocity
|
||||||
btScalar tau = dt / motor.hTimescale;
|
btScalar tau = dt / motor.hTimescale;
|
||||||
if (tau > 1.0f) {
|
if (tau > 1.0f) {
|
||||||
tau = 1.0f;
|
tau = 1.0f;
|
||||||
}
|
}
|
||||||
velocity += (motor.velocity - velocity) * tau;
|
velocity += tau * (motor.velocity - velocity);
|
||||||
|
|
||||||
// rotate back into world-frame
|
// rotate back into world-frame
|
||||||
velocity = velocity.rotate(axis, angle);
|
velocity = velocity.rotate(axis, angle);
|
||||||
|
_targetVelocity += (tau * motor.velocity).rotate(axis, angle);
|
||||||
|
|
||||||
// store the velocity and weight
|
// store the velocity and weight
|
||||||
velocities.push_back(velocity);
|
velocities.push_back(velocity);
|
||||||
|
@ -445,12 +532,26 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
|
||||||
} else {
|
} else {
|
||||||
// compute local UP
|
// compute local UP
|
||||||
btVector3 up = _currentUp.rotate(axis, -angle);
|
btVector3 up = _currentUp.rotate(axis, -angle);
|
||||||
|
btVector3 motorVelocity = motor.velocity;
|
||||||
|
|
||||||
|
// save these non-adjusted components for later
|
||||||
|
btVector3 vTargetVelocity = motorVelocity.dot(up) * up;
|
||||||
|
btVector3 hTargetVelocity = motorVelocity - vTargetVelocity;
|
||||||
|
|
||||||
|
if (_stepHeight > _minStepHeight && !_steppingUp) {
|
||||||
|
// there is a step --> compute velocity direction to go over step
|
||||||
|
btVector3 motorVelocityWF = motorVelocity.rotate(axis, angle);
|
||||||
|
if (motorVelocityWF.dot(_stepNormal) < 0.0f) {
|
||||||
|
// the motor pushes against step
|
||||||
|
_steppingUp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// split velocity into horizontal and vertical components
|
// split velocity into horizontal and vertical components
|
||||||
btVector3 vVelocity = velocity.dot(up) * up;
|
btVector3 vVelocity = velocity.dot(up) * up;
|
||||||
btVector3 hVelocity = velocity - vVelocity;
|
btVector3 hVelocity = velocity - vVelocity;
|
||||||
btVector3 vTargetVelocity = motor.velocity.dot(up) * up;
|
btVector3 vMotorVelocity = motorVelocity.dot(up) * up;
|
||||||
btVector3 hTargetVelocity = motor.velocity - vTargetVelocity;
|
btVector3 hMotorVelocity = motorVelocity - vMotorVelocity;
|
||||||
|
|
||||||
// modify each component separately
|
// modify each component separately
|
||||||
btScalar maxTau = 0.0f;
|
btScalar maxTau = 0.0f;
|
||||||
|
@ -460,7 +561,7 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
|
||||||
tau = 1.0f;
|
tau = 1.0f;
|
||||||
}
|
}
|
||||||
maxTau = tau;
|
maxTau = tau;
|
||||||
hVelocity += (hTargetVelocity - hVelocity) * tau;
|
hVelocity += (hMotorVelocity - hVelocity) * tau;
|
||||||
}
|
}
|
||||||
if (motor.vTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) {
|
if (motor.vTimescale < MAX_CHARACTER_MOTOR_TIMESCALE) {
|
||||||
btScalar tau = dt / motor.vTimescale;
|
btScalar tau = dt / motor.vTimescale;
|
||||||
|
@ -470,11 +571,12 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
|
||||||
if (tau > maxTau) {
|
if (tau > maxTau) {
|
||||||
maxTau = tau;
|
maxTau = tau;
|
||||||
}
|
}
|
||||||
vVelocity += (vTargetVelocity - vVelocity) * tau;
|
vVelocity += (vMotorVelocity - vVelocity) * tau;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add components back together and rotate into world-frame
|
// add components back together and rotate into world-frame
|
||||||
velocity = (hVelocity + vVelocity).rotate(axis, angle);
|
velocity = (hVelocity + vVelocity).rotate(axis, angle);
|
||||||
|
_targetVelocity += maxTau * (hTargetVelocity + vTargetVelocity).rotate(axis, angle);
|
||||||
|
|
||||||
// store velocity and weights
|
// store velocity and weights
|
||||||
velocities.push_back(velocity);
|
velocities.push_back(velocity);
|
||||||
|
@ -492,6 +594,8 @@ void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) {
|
||||||
velocities.reserve(_motors.size());
|
velocities.reserve(_motors.size());
|
||||||
std::vector<btScalar> weights;
|
std::vector<btScalar> weights;
|
||||||
weights.reserve(_motors.size());
|
weights.reserve(_motors.size());
|
||||||
|
_targetVelocity = btVector3(0.0f, 0.0f, 0.0f);
|
||||||
|
_steppingUp = false;
|
||||||
for (int i = 0; i < (int)_motors.size(); ++i) {
|
for (int i = 0; i < (int)_motors.size(); ++i) {
|
||||||
applyMotor(i, dt, velocity, velocities, weights);
|
applyMotor(i, dt, velocity, velocities, weights);
|
||||||
}
|
}
|
||||||
|
@ -507,14 +611,18 @@ void CharacterController::computeNewVelocity(btScalar dt, btVector3& velocity) {
|
||||||
for (size_t i = 0; i < velocities.size(); ++i) {
|
for (size_t i = 0; i < velocities.size(); ++i) {
|
||||||
velocity += (weights[i] / totalWeight) * velocities[i];
|
velocity += (weights[i] / totalWeight) * velocities[i];
|
||||||
}
|
}
|
||||||
|
_targetVelocity /= totalWeight;
|
||||||
}
|
}
|
||||||
if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) {
|
if (velocity.length2() < MIN_TARGET_SPEED_SQUARED) {
|
||||||
velocity = btVector3(0.0f, 0.0f, 0.0f);
|
velocity = btVector3(0.0f, 0.0f, 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'thrust' is applied at the very end
|
// 'thrust' is applied at the very end
|
||||||
|
_targetVelocity += dt * _linearAcceleration;
|
||||||
velocity += dt * _linearAcceleration;
|
velocity += dt * _linearAcceleration;
|
||||||
_targetVelocity = velocity;
|
// Note the differences between these two variables:
|
||||||
|
// _targetVelocity = ideal final velocity according to input
|
||||||
|
// velocity = real final velocity after motors are applied to current velocity
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) {
|
void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) {
|
||||||
|
@ -523,45 +631,48 @@ void CharacterController::computeNewVelocity(btScalar dt, glm::vec3& velocity) {
|
||||||
velocity = bulletToGLM(btVelocity);
|
velocity = bulletToGLM(btVelocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::preSimulation() {
|
void CharacterController::updateState() {
|
||||||
if (_enabled && _dynamicsWorld && _rigidBody) {
|
if (!_dynamicsWorld) {
|
||||||
quint64 now = usecTimestampNow();
|
return;
|
||||||
|
}
|
||||||
// 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 = _characterBodyTransform.getOrigin();
|
|
||||||
|
|
||||||
// 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 FLY_TO_GROUND_THRESHOLD = 0.1f * _radius;
|
||||||
const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight;
|
const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight;
|
||||||
const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND;
|
const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND;
|
||||||
const btScalar MIN_HOVER_HEIGHT = 2.5f;
|
const btScalar MIN_HOVER_HEIGHT = 2.5f;
|
||||||
const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND;
|
const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND;
|
||||||
const btScalar MAX_WALKING_SPEED = 2.5f;
|
|
||||||
const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND;
|
// scan for distant floor
|
||||||
|
// rayStart is at center of bottom sphere
|
||||||
|
btVector3 rayStart = _position;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
ClosestNotMe rayCallback(_rigidBody);
|
ClosestNotMe rayCallback(_rigidBody);
|
||||||
rayCallback.m_closestHitFraction = 1.0f;
|
rayCallback.m_closestHitFraction = 1.0f;
|
||||||
_dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback);
|
_dynamicsWorld->rayTest(rayStart, rayEnd, rayCallback);
|
||||||
bool rayHasHit = rayCallback.hasHit();
|
bool rayHasHit = rayCallback.hasHit();
|
||||||
|
quint64 now = usecTimestampNow();
|
||||||
if (rayHasHit) {
|
if (rayHasHit) {
|
||||||
_rayHitStartTime = now;
|
_rayHitStartTime = now;
|
||||||
_floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight);
|
_floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight);
|
||||||
} else if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) {
|
} else {
|
||||||
|
const quint64 RAY_HIT_START_PERIOD = 500 * MSECS_PER_SECOND;
|
||||||
|
if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) {
|
||||||
rayHasHit = true;
|
rayHasHit = true;
|
||||||
} else {
|
} else {
|
||||||
_floorDistance = FLT_MAX;
|
_floorDistance = FLT_MAX;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// record a time stamp when the jump button was first pressed.
|
// record a time stamp when the jump button was first pressed.
|
||||||
|
bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP;
|
||||||
if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) {
|
if ((_previousFlags & PENDING_FLAG_JUMP) != (_pendingFlags & PENDING_FLAG_JUMP)) {
|
||||||
if (_pendingFlags & PENDING_FLAG_JUMP) {
|
if (_pendingFlags & PENDING_FLAG_JUMP) {
|
||||||
_jumpButtonDownStartTime = now;
|
_jumpButtonDownStartTime = now;
|
||||||
|
@ -569,11 +680,11 @@ void CharacterController::preSimulation() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool jumpButtonHeld = _pendingFlags & PENDING_FLAG_JUMP;
|
btVector3 velocity = _preSimulationVelocity;
|
||||||
|
|
||||||
btVector3 actualHorizVelocity = velocity - velocity.dot(_currentUp) * _currentUp;
|
|
||||||
bool flyingFast = _state == State::Hover && actualHorizVelocity.length() > (MAX_WALKING_SPEED * 0.75f);
|
|
||||||
|
|
||||||
|
// disable normal state transitions while collisionless
|
||||||
|
const btScalar MAX_WALKING_SPEED = 2.65f;
|
||||||
|
if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) {
|
||||||
switch (_state) {
|
switch (_state) {
|
||||||
case State::Ground:
|
case State::Ground:
|
||||||
if (!rayHasHit && !_hasSupport) {
|
if (!rayHasHit && !_hasSupport) {
|
||||||
|
@ -613,6 +724,9 @@ void CharacterController::preSimulation() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case State::Hover:
|
case State::Hover:
|
||||||
|
btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length();
|
||||||
|
bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f);
|
||||||
|
|
||||||
if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
|
if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
|
||||||
SET_STATE(State::InAir, "near ground");
|
SET_STATE(State::InAir, "near ground");
|
||||||
} else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
|
} else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
|
||||||
|
@ -620,6 +734,28 @@ void CharacterController::preSimulation() {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// when collisionless: only switch between State::Ground and State::Hover
|
||||||
|
// and bypass state debugging
|
||||||
|
if (rayHasHit) {
|
||||||
|
if (velocity.length() > (MAX_WALKING_SPEED)) {
|
||||||
|
_state = State::Hover;
|
||||||
|
} else {
|
||||||
|
_state = State::Ground;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state = State::Hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterController::preSimulation() {
|
||||||
|
if (_rigidBody) {
|
||||||
|
// slam body transform and remember velocity
|
||||||
|
_rigidBody->setWorldTransform(btTransform(btTransform(_rotation, _position)));
|
||||||
|
_preSimulationVelocity = _rigidBody->getLinearVelocity();
|
||||||
|
|
||||||
|
updateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
_previousFlags = _pendingFlags;
|
_previousFlags = _pendingFlags;
|
||||||
|
@ -631,14 +767,11 @@ void CharacterController::preSimulation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::postSimulation() {
|
void CharacterController::postSimulation() {
|
||||||
// postSimulation() exists for symmetry and just in case we need to do something here later
|
if (_rigidBody) {
|
||||||
if (_enabled && _dynamicsWorld && _rigidBody) {
|
_velocityChange = _rigidBody->getLinearVelocity() - _preSimulationVelocity;
|
||||||
btVector3 velocity = _rigidBody->getLinearVelocity();
|
|
||||||
_velocityChange = velocity - _preSimulationVelocity;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) {
|
bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) {
|
||||||
if (!_rigidBody) {
|
if (!_rigidBody) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -651,11 +784,17 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio
|
||||||
}
|
}
|
||||||
|
|
||||||
void CharacterController::setFlyingAllowed(bool value) {
|
void CharacterController::setFlyingAllowed(bool value) {
|
||||||
if (_flyingAllowed != value) {
|
if (value != _flyingAllowed) {
|
||||||
_flyingAllowed = value;
|
_flyingAllowed = value;
|
||||||
|
|
||||||
if (!_flyingAllowed && _state == State::Hover) {
|
if (!_flyingAllowed && _state == State::Hover) {
|
||||||
SET_STATE(State::InAir, "flying not allowed");
|
SET_STATE(State::InAir, "flying not allowed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CharacterController::setCollisionlessAllowed(bool value) {
|
||||||
|
if (value != _collisionlessAllowed) {
|
||||||
|
_collisionlessAllowed = value;
|
||||||
|
_pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// CharacterControllerInterface.h
|
// CharacterControllerInterface.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2015.10.21
|
// Created by Andrew Meadows 2015.10.21
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
@ -9,8 +9,8 @@
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifndef hifi_CharacterControllerInterface_h
|
#ifndef hifi_CharacterController_h
|
||||||
#define hifi_CharacterControllerInterface_h
|
#define hifi_CharacterController_h
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
@ -19,14 +19,18 @@
|
||||||
#include <BulletDynamics/Character/btCharacterControllerInterface.h>
|
#include <BulletDynamics/Character/btCharacterControllerInterface.h>
|
||||||
|
|
||||||
#include <GLMHelpers.h>
|
#include <GLMHelpers.h>
|
||||||
|
#include <NumericalConstants.h>
|
||||||
|
#include <PhysicsCollisionGroups.h>
|
||||||
|
|
||||||
#include "BulletUtil.h"
|
#include "BulletUtil.h"
|
||||||
|
#include "CharacterGhostObject.h"
|
||||||
|
|
||||||
const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0;
|
const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0;
|
||||||
const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1;
|
const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1;
|
||||||
const uint32_t PENDING_FLAG_UPDATE_SHAPE = 1U << 2;
|
const uint32_t PENDING_FLAG_UPDATE_SHAPE = 1U << 2;
|
||||||
const uint32_t PENDING_FLAG_JUMP = 1U << 3;
|
const uint32_t PENDING_FLAG_JUMP = 1U << 3;
|
||||||
|
const uint32_t PENDING_FLAG_UPDATE_COLLISION_GROUP = 1U << 4;
|
||||||
const float DEFAULT_CHARACTER_GRAVITY = -5.0f;
|
const float DEFAULT_MIN_FLOOR_NORMAL_DOT_UP = cosf(PI / 3.0f);
|
||||||
|
|
||||||
class btRigidBody;
|
class btRigidBody;
|
||||||
class btCollisionWorld;
|
class btCollisionWorld;
|
||||||
|
@ -44,7 +48,7 @@ public:
|
||||||
|
|
||||||
bool needsRemoval() const;
|
bool needsRemoval() const;
|
||||||
bool needsAddition() const;
|
bool needsAddition() const;
|
||||||
void setDynamicsWorld(btDynamicsWorld* world);
|
virtual void setDynamicsWorld(btDynamicsWorld* world);
|
||||||
btCollisionObject* getCollisionObject() { return _rigidBody; }
|
btCollisionObject* getCollisionObject() { return _rigidBody; }
|
||||||
|
|
||||||
virtual void updateShapeIfNecessary() = 0;
|
virtual void updateShapeIfNecessary() = 0;
|
||||||
|
@ -56,10 +60,7 @@ public:
|
||||||
virtual void warp(const btVector3& origin) override { }
|
virtual void warp(const btVector3& origin) override { }
|
||||||
virtual void debugDraw(btIDebugDraw* debugDrawer) override { }
|
virtual void debugDraw(btIDebugDraw* debugDrawer) override { }
|
||||||
virtual void setUpInterpolate(bool value) override { }
|
virtual void setUpInterpolate(bool value) override { }
|
||||||
virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override {
|
virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override;
|
||||||
preStep(collisionWorld);
|
|
||||||
playerStep(collisionWorld, deltaTime);
|
|
||||||
}
|
|
||||||
virtual void preStep(btCollisionWorld *collisionWorld) override;
|
virtual void preStep(btCollisionWorld *collisionWorld) override;
|
||||||
virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override;
|
virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override;
|
||||||
virtual bool canJump() const override { assert(false); return false; } // never call this
|
virtual bool canJump() const override { assert(false); return false; } // never call this
|
||||||
|
@ -69,6 +70,7 @@ public:
|
||||||
void clearMotors();
|
void clearMotors();
|
||||||
void addMotor(const glm::vec3& velocity, const glm::quat& rotation, float horizTimescale, float vertTimescale = -1.0f);
|
void addMotor(const glm::vec3& velocity, const glm::quat& rotation, float horizTimescale, float vertTimescale = -1.0f);
|
||||||
void applyMotor(int index, btScalar dt, btVector3& worldVelocity, std::vector<btVector3>& velocities, std::vector<btScalar>& weights);
|
void applyMotor(int index, btScalar dt, btVector3& worldVelocity, std::vector<btVector3>& velocities, std::vector<btScalar>& weights);
|
||||||
|
void setStepUpEnabled(bool enabled) { _stepUpEnabled = enabled; }
|
||||||
void computeNewVelocity(btScalar dt, btVector3& velocity);
|
void computeNewVelocity(btScalar dt, btVector3& velocity);
|
||||||
void computeNewVelocity(btScalar dt, glm::vec3& velocity);
|
void computeNewVelocity(btScalar dt, glm::vec3& velocity);
|
||||||
|
|
||||||
|
@ -103,16 +105,20 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
State getState() const { return _state; }
|
State getState() const { return _state; }
|
||||||
|
void updateState();
|
||||||
|
|
||||||
void setLocalBoundingBox(const glm::vec3& corner, const glm::vec3& scale);
|
void setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale);
|
||||||
|
|
||||||
bool isEnabled() const { return _enabled; } // thread-safe
|
bool isEnabledAndReady() const { return _dynamicsWorld; }
|
||||||
void setEnabled(bool enabled);
|
|
||||||
bool isEnabledAndReady() const { return _enabled && _dynamicsWorld; }
|
void setCollisionless(bool collisionless);
|
||||||
|
int16_t computeCollisionGroup() const;
|
||||||
|
void handleChangedCollisionGroup();
|
||||||
|
|
||||||
bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation);
|
bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation);
|
||||||
|
|
||||||
void setFlyingAllowed(bool value);
|
void setFlyingAllowed(bool value);
|
||||||
|
void setCollisionlessAllowed(bool value);
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -122,8 +128,9 @@ protected:
|
||||||
void setState(State state);
|
void setState(State state);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void updateGravity();
|
||||||
void updateUpAxis(const glm::quat& rotation);
|
void updateUpAxis(const glm::quat& rotation);
|
||||||
bool checkForSupport(btCollisionWorld* collisionWorld) const;
|
bool checkForSupport(btCollisionWorld* collisionWorld);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
struct CharacterMotor {
|
struct CharacterMotor {
|
||||||
|
@ -136,6 +143,7 @@ protected:
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<CharacterMotor> _motors;
|
std::vector<CharacterMotor> _motors;
|
||||||
|
CharacterGhostObject _ghost;
|
||||||
btVector3 _currentUp;
|
btVector3 _currentUp;
|
||||||
btVector3 _targetVelocity;
|
btVector3 _targetVelocity;
|
||||||
btVector3 _parentVelocity;
|
btVector3 _parentVelocity;
|
||||||
|
@ -144,6 +152,8 @@ protected:
|
||||||
btTransform _followDesiredBodyTransform;
|
btTransform _followDesiredBodyTransform;
|
||||||
btScalar _followTimeRemaining;
|
btScalar _followTimeRemaining;
|
||||||
btTransform _characterBodyTransform;
|
btTransform _characterBodyTransform;
|
||||||
|
btVector3 _position;
|
||||||
|
btQuaternion _rotation;
|
||||||
|
|
||||||
glm::vec3 _shapeLocalOffset;
|
glm::vec3 _shapeLocalOffset;
|
||||||
|
|
||||||
|
@ -155,13 +165,23 @@ protected:
|
||||||
quint32 _jumpButtonDownCount;
|
quint32 _jumpButtonDownCount;
|
||||||
quint32 _takeoffJumpButtonID;
|
quint32 _takeoffJumpButtonID;
|
||||||
|
|
||||||
btScalar _halfHeight;
|
// data for walking up steps
|
||||||
btScalar _radius;
|
btVector3 _stepPoint { 0.0f, 0.0f, 0.0f };
|
||||||
|
btVector3 _stepNormal { 0.0f, 0.0f, 0.0f };
|
||||||
|
bool _steppingUp { false };
|
||||||
|
btScalar _stepHeight { 0.0f };
|
||||||
|
btScalar _minStepHeight { 0.0f };
|
||||||
|
btScalar _maxStepHeight { 0.0f };
|
||||||
|
btScalar _minFloorNormalDotUp { DEFAULT_MIN_FLOOR_NORMAL_DOT_UP };
|
||||||
|
|
||||||
|
btScalar _halfHeight { 0.0f };
|
||||||
|
btScalar _radius { 0.0f };
|
||||||
|
|
||||||
btScalar _floorDistance;
|
btScalar _floorDistance;
|
||||||
|
bool _stepUpEnabled { true };
|
||||||
bool _hasSupport;
|
bool _hasSupport;
|
||||||
|
|
||||||
btScalar _gravity;
|
btScalar _gravity { 0.0f };
|
||||||
|
|
||||||
btScalar _jumpSpeed;
|
btScalar _jumpSpeed;
|
||||||
btScalar _followTime;
|
btScalar _followTime;
|
||||||
|
@ -169,7 +189,6 @@ protected:
|
||||||
btQuaternion _followAngularDisplacement;
|
btQuaternion _followAngularDisplacement;
|
||||||
btVector3 _linearAcceleration;
|
btVector3 _linearAcceleration;
|
||||||
|
|
||||||
std::atomic_bool _enabled;
|
|
||||||
State _state;
|
State _state;
|
||||||
bool _isPushingUp;
|
bool _isPushingUp;
|
||||||
|
|
||||||
|
@ -179,6 +198,8 @@ protected:
|
||||||
uint32_t _previousFlags { 0 };
|
uint32_t _previousFlags { 0 };
|
||||||
|
|
||||||
bool _flyingAllowed { true };
|
bool _flyingAllowed { true };
|
||||||
|
bool _collisionlessAllowed { true };
|
||||||
|
bool _collisionless { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_CharacterControllerInterface_h
|
#endif // hifi_CharacterController_h
|
||||||
|
|
99
libraries/physics/src/CharacterGhostObject.cpp
Executable file
99
libraries/physics/src/CharacterGhostObject.cpp
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
62
libraries/physics/src/CharacterGhostObject.h
Executable file
62
libraries/physics/src/CharacterGhostObject.h
Executable 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
|
31
libraries/physics/src/CharacterGhostShape.cpp
Normal file
31
libraries/physics/src/CharacterGhostShape.cpp
Normal 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;
|
||||||
|
}
|
25
libraries/physics/src/CharacterGhostShape.h
Normal file
25
libraries/physics/src/CharacterGhostShape.h
Normal 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
|
31
libraries/physics/src/CharacterRayResult.cpp
Executable file
31
libraries/physics/src/CharacterRayResult.cpp
Executable 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);
|
||||||
|
}
|
44
libraries/physics/src/CharacterRayResult.h
Normal file
44
libraries/physics/src/CharacterRayResult.h
Normal 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
|
42
libraries/physics/src/CharacterSweepResult.cpp
Executable file
42
libraries/physics/src/CharacterSweepResult.cpp
Executable 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);
|
||||||
|
}
|
45
libraries/physics/src/CharacterSweepResult.h
Normal file
45
libraries/physics/src/CharacterSweepResult.h
Normal 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
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// CollisionRenderMeshCache.cpp
|
// CollisionRenderMeshCache.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2016.07.13
|
// Created by Andrew Meadows 2016.07.13
|
||||||
// Copyright 2016 High Fidelity, Inc.
|
// Copyright 2016 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// CollisionRenderMeshCache.h
|
// CollisionRenderMeshCache.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2016.07.13
|
// Created by Andrew Meadows 2016.07.13
|
||||||
// Copyright 2016 High Fidelity, Inc.
|
// Copyright 2016 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ContactEvent.cpp
|
// ContactEvent.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2015.01.20
|
// Created by Andrew Meadows 2015.01.20
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ContactEvent.h
|
// ContactEvent.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2015.01.20
|
// Created by Andrew Meadows 2015.01.20
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ObjectAction.cpp
|
// ObjectAction.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Seth Alves 2015-6-2
|
// Created by Seth Alves 2015-6-2
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ObjectAction.h
|
// ObjectAction.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Seth Alves 2015-6-2
|
// Created by Seth Alves 2015-6-2
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ObjectMotionState.cpp
|
// ObjectMotionState.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.11.05
|
// Created by Andrew Meadows 2014.11.05
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ObjectMotionState.h
|
// ObjectMotionState.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.11.05
|
// Created by Andrew Meadows 2014.11.05
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// PhysicalEntitySimulation.cpp
|
// PhysicalEntitySimulation.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2015.04.27
|
// Created by Andrew Meadows 2015.04.27
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// PhysicalEntitySimulation.h
|
// PhysicalEntitySimulation.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2015.04.27
|
// Created by Andrew Meadows 2015.04.27
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// PhysicsEngine.cpp
|
// PhysicsEngine.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.10.29
|
// Created by Andrew Meadows 2014.10.29
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// PhysicsEngine.h
|
// PhysicsEngine.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.10.29
|
// Created by Andrew Meadows 2014.10.29
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ShapeFactory.cpp
|
// ShapeFactory.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.12.01
|
// Created by Andrew Meadows 2014.12.01
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ShapeFactory.h
|
// ShapeFactory.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.12.01
|
// Created by Andrew Meadows 2014.12.01
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ShapeManager.cpp
|
// ShapeManager.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.10.29
|
// Created by Andrew Meadows 2014.10.29
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ShapeManager.h
|
// ShapeManager.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.10.29
|
// Created by Andrew Meadows 2014.10.29
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -250,6 +250,10 @@ static void addButtonProxyToQmlTablet(QQuickItem* qmlTablet, TabletButtonProxy*
|
||||||
if (QThread::currentThread() != qmlTablet->thread()) {
|
if (QThread::currentThread() != qmlTablet->thread()) {
|
||||||
connectionType = Qt::BlockingQueuedConnection;
|
connectionType = Qt::BlockingQueuedConnection;
|
||||||
}
|
}
|
||||||
|
if (buttonProxy == NULL){
|
||||||
|
qCCritical(scriptengine) << "TabletScriptingInterface addButtonProxyToQmlTablet buttonProxy is NULL";
|
||||||
|
return;
|
||||||
|
}
|
||||||
bool hasResult = QMetaObject::invokeMethod(qmlTablet, "addButtonProxy", connectionType,
|
bool hasResult = QMetaObject::invokeMethod(qmlTablet, "addButtonProxy", connectionType,
|
||||||
Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, buttonProxy->getProperties()));
|
Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, buttonProxy->getProperties()));
|
||||||
if (!hasResult) {
|
if (!hasResult) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// BackgroundMode.h
|
// BackgroundMode.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Copyright 2015 High Fidelity, Inc.
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ShapeInfo.cpp
|
// ShapeInfo.cpp
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.10.29
|
// Created by Andrew Meadows 2014.10.29
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// ShapeInfo.h
|
// ShapeInfo.h
|
||||||
// libraries/physcis/src
|
// libraries/physics/src
|
||||||
//
|
//
|
||||||
// Created by Andrew Meadows 2014.10.29
|
// Created by Andrew Meadows 2014.10.29
|
||||||
// Copyright 2014 High Fidelity, Inc.
|
// Copyright 2014 High Fidelity, Inc.
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
#include "ViveControllerManager.h"
|
#include "ViveControllerManager.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include <PerfStat.h>
|
#include <PerfStat.h>
|
||||||
#include <PathUtils.h>
|
#include <PathUtils.h>
|
||||||
|
@ -20,7 +21,11 @@
|
||||||
#include <NumericalConstants.h>
|
#include <NumericalConstants.h>
|
||||||
#include <ui-plugins/PluginContainer.h>
|
#include <ui-plugins/PluginContainer.h>
|
||||||
#include <UserActivityLogger.h>
|
#include <UserActivityLogger.h>
|
||||||
|
#include <NumericalConstants.h>
|
||||||
#include <OffscreenUi.h>
|
#include <OffscreenUi.h>
|
||||||
|
#include <GLMHelpers.h>
|
||||||
|
#include <glm/ext.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
|
||||||
#include <controllers/UserInputMapper.h>
|
#include <controllers/UserInputMapper.h>
|
||||||
|
@ -36,14 +41,32 @@ void releaseOpenVrSystem();
|
||||||
|
|
||||||
|
|
||||||
static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b";
|
static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b";
|
||||||
|
const quint64 CALIBRATION_TIMELAPSE = 2 * USECS_PER_SECOND;
|
||||||
|
|
||||||
static const char* MENU_PARENT = "Avatar";
|
static const char* MENU_PARENT = "Avatar";
|
||||||
static const char* MENU_NAME = "Vive Controllers";
|
static const char* MENU_NAME = "Vive Controllers";
|
||||||
static const char* MENU_PATH = "Avatar" ">" "Vive Controllers";
|
static const char* MENU_PATH = "Avatar" ">" "Vive Controllers";
|
||||||
static const char* RENDER_CONTROLLERS = "Render Hand Controllers";
|
static const char* RENDER_CONTROLLERS = "Render Hand Controllers";
|
||||||
|
static const int MIN_PUCK_COUNT = 2;
|
||||||
|
static const int MIN_FEET_AND_HIPS = 3;
|
||||||
|
static const int MIN_FEET_HIPS_CHEST = 4;
|
||||||
|
static const int FIRST_FOOT = 0;
|
||||||
|
static const int SECOND_FOOT = 1;
|
||||||
|
static const int HIP = 2;
|
||||||
|
static const int CHEST = 3;
|
||||||
|
|
||||||
const char* ViveControllerManager::NAME { "OpenVR" };
|
const char* ViveControllerManager::NAME { "OpenVR" };
|
||||||
|
|
||||||
|
static glm::mat4 computeOffset(glm::mat4 defaultToReferenceMat, glm::mat4 defaultJointMat, controller::Pose puckPose) {
|
||||||
|
glm::mat4 poseMat = createMatFromQuatAndPos(puckPose.rotation, puckPose.translation);
|
||||||
|
glm::mat4 referenceJointMat = defaultToReferenceMat * defaultJointMat;
|
||||||
|
return glm::inverse(poseMat) * referenceJointMat;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sortPucksYPosition(std::pair<uint32_t, controller::Pose> firstPuck, std::pair<uint32_t, controller::Pose> secondPuck) {
|
||||||
|
return (firstPuck.second.translation.y < firstPuck.second.translation.y);
|
||||||
|
}
|
||||||
|
|
||||||
bool ViveControllerManager::isSupported() const {
|
bool ViveControllerManager::isSupported() const {
|
||||||
return openVrSupported();
|
return openVrSupported();
|
||||||
}
|
}
|
||||||
|
@ -125,6 +148,7 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu
|
||||||
void ViveControllerManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) {
|
void ViveControllerManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) {
|
||||||
_poseStateMap.clear();
|
_poseStateMap.clear();
|
||||||
_buttonPressedMap.clear();
|
_buttonPressedMap.clear();
|
||||||
|
_validTrackedObjects.clear();
|
||||||
|
|
||||||
// While the keyboard is open, we defer strictly to the keyboard values
|
// While the keyboard is open, we defer strictly to the keyboard values
|
||||||
if (isOpenVrKeyboardShown()) {
|
if (isOpenVrKeyboardShown()) {
|
||||||
|
@ -143,6 +167,7 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle
|
||||||
// collect poses for all generic trackers
|
// collect poses for all generic trackers
|
||||||
for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) {
|
for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) {
|
||||||
handleTrackedObject(i, inputCalibrationData);
|
handleTrackedObject(i, inputCalibrationData);
|
||||||
|
handleHmd(i, inputCalibrationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle haptics
|
// handle haptics
|
||||||
|
@ -164,10 +189,27 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle
|
||||||
numTrackedControllers++;
|
numTrackedControllers++;
|
||||||
}
|
}
|
||||||
_trackedControllers = numTrackedControllers;
|
_trackedControllers = numTrackedControllers;
|
||||||
|
|
||||||
|
if (checkForCalibrationEvent()) {
|
||||||
|
quint64 currentTime = usecTimestampNow();
|
||||||
|
if (!_timeTilCalibrationSet) {
|
||||||
|
_timeTilCalibrationSet = true;
|
||||||
|
_timeTilCalibration = currentTime + CALIBRATION_TIMELAPSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime > _timeTilCalibration && !_triggersPressedHandled) {
|
||||||
|
_triggersPressedHandled = true;
|
||||||
|
calibrateOrUncalibrate(inputCalibrationData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_triggersPressedHandled = false;
|
||||||
|
_timeTilCalibrationSet = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCalibratedLimbs();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) {
|
void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) {
|
||||||
|
|
||||||
uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex;
|
uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex;
|
||||||
|
|
||||||
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
|
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
|
||||||
|
@ -185,12 +227,129 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde
|
||||||
// transform into avatar frame
|
// transform into avatar frame
|
||||||
glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat;
|
glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat;
|
||||||
_poseStateMap[poseIndex] = pose.transform(controllerToAvatar);
|
_poseStateMap[poseIndex] = pose.transform(controllerToAvatar);
|
||||||
|
_validTrackedObjects.push_back(std::make_pair(poseIndex, _poseStateMap[poseIndex]));
|
||||||
} else {
|
} else {
|
||||||
controller::Pose invalidPose;
|
controller::Pose invalidPose;
|
||||||
_poseStateMap[poseIndex] = invalidPose;
|
_poseStateMap[poseIndex] = invalidPose;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ViveControllerManager::InputDevice::calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration) {
|
||||||
|
if (!_calibrated) {
|
||||||
|
calibrate(inputCalibration);
|
||||||
|
} else {
|
||||||
|
uncalibrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibrationData& inputCalibration) {
|
||||||
|
// convert the hmd head from sensor space to avatar space
|
||||||
|
glm::mat4 hmdSensorFlippedMat = inputCalibration.hmdSensorMat * Matrices::Y_180;
|
||||||
|
glm::mat4 sensorToAvatarMat = glm::inverse(inputCalibration.avatarMat) * inputCalibration.sensorToWorldMat;
|
||||||
|
glm::mat4 hmdAvatarMat = sensorToAvatarMat * hmdSensorFlippedMat;
|
||||||
|
|
||||||
|
// cancel the roll and pitch for the hmd head
|
||||||
|
glm::quat hmdRotation = cancelOutRollAndPitch(glmExtractRotation(hmdAvatarMat));
|
||||||
|
glm::vec3 hmdTranslation = extractTranslation(hmdAvatarMat);
|
||||||
|
glm::mat4 currentHmd = createMatFromQuatAndPos(hmdRotation, hmdTranslation);
|
||||||
|
|
||||||
|
// calculate the offset from the centerOfEye to defaultHeadMat
|
||||||
|
glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat;
|
||||||
|
|
||||||
|
glm::mat4 currentHead = currentHmd * defaultHeadOffset;
|
||||||
|
|
||||||
|
// calculate the defaultToRefrenceXform
|
||||||
|
glm::mat4 defaultToReferenceMat = currentHead * glm::inverse(inputCalibration.defaultHeadMat);
|
||||||
|
|
||||||
|
int puckCount = (int)_validTrackedObjects.size();
|
||||||
|
if (puckCount == MIN_PUCK_COUNT) {
|
||||||
|
_config = Config::Feet;
|
||||||
|
} else if (puckCount == MIN_FEET_AND_HIPS) {
|
||||||
|
_config = Config::FeetAndHips;
|
||||||
|
} else if (puckCount >= MIN_FEET_HIPS_CHEST) {
|
||||||
|
_config = Config::FeetHipsAndChest;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksYPosition);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
auto& firstFoot = _validTrackedObjects[FIRST_FOOT];
|
||||||
|
auto& secondFoot = _validTrackedObjects[SECOND_FOOT];
|
||||||
|
controller::Pose& firstFootPose = firstFoot.second;
|
||||||
|
controller::Pose& secondFootPose = secondFoot.second;
|
||||||
|
|
||||||
|
if (firstFootPose.translation.x < secondFootPose.translation.x) {
|
||||||
|
_jointToPuckMap[controller::LEFT_FOOT] = firstFoot.first;
|
||||||
|
_pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, firstFootPose);
|
||||||
|
_jointToPuckMap[controller::RIGHT_FOOT] = secondFoot.first;
|
||||||
|
_pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, secondFootPose);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
_jointToPuckMap[controller::LEFT_FOOT] = secondFoot.first;
|
||||||
|
_pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, secondFootPose);
|
||||||
|
_jointToPuckMap[controller::RIGHT_FOOT] = firstFoot.first;
|
||||||
|
_pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, firstFootPose);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config == Config::Feet) {
|
||||||
|
// done
|
||||||
|
} else if (_config == Config::FeetAndHips) {
|
||||||
|
_jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first;
|
||||||
|
_pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second);
|
||||||
|
} else if (_config == Config::FeetHipsAndChest) {
|
||||||
|
_jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first;
|
||||||
|
_pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second);
|
||||||
|
_jointToPuckMap[controller::SPINE2] = _validTrackedObjects[CHEST].first;
|
||||||
|
_pucksOffset[_validTrackedObjects[CHEST].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultSpine2, _validTrackedObjects[CHEST].second);
|
||||||
|
}
|
||||||
|
_calibrated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViveControllerManager::InputDevice::uncalibrate() {
|
||||||
|
_pucksOffset.clear();
|
||||||
|
_jointToPuckMap.clear();
|
||||||
|
_calibrated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViveControllerManager::InputDevice::updateCalibratedLimbs() {
|
||||||
|
_poseStateMap[controller::LEFT_FOOT] = addOffsetToPuckPose(controller::LEFT_FOOT);
|
||||||
|
_poseStateMap[controller::RIGHT_FOOT] = addOffsetToPuckPose(controller::RIGHT_FOOT);
|
||||||
|
_poseStateMap[controller::HIPS] = addOffsetToPuckPose(controller::HIPS);
|
||||||
|
_poseStateMap[controller::SPINE2] = addOffsetToPuckPose(controller::SPINE2);
|
||||||
|
}
|
||||||
|
|
||||||
|
controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(int joint) const {
|
||||||
|
auto puck = _jointToPuckMap.find(joint);
|
||||||
|
if (puck != _jointToPuckMap.end()) {
|
||||||
|
uint32_t puckIndex = puck->second;
|
||||||
|
auto puckPose = _poseStateMap.find(puckIndex);
|
||||||
|
auto puckOffset = _pucksOffset.find(puckIndex);
|
||||||
|
|
||||||
|
if ((puckPose != _poseStateMap.end()) && (puckOffset != _pucksOffset.end())) {
|
||||||
|
return puckPose->second.postTransform(puckOffset->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return controller::Pose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViveControllerManager::InputDevice::handleHmd(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) {
|
||||||
|
uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex;
|
||||||
|
|
||||||
|
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
|
||||||
|
_system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_HMD &&
|
||||||
|
_nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) {
|
||||||
|
|
||||||
|
const mat4& mat = _nextSimPoseData.poses[deviceIndex];
|
||||||
|
const vec3 linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex];
|
||||||
|
const vec3 angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex];
|
||||||
|
|
||||||
|
handleHeadPoseEvent(inputCalibrationData, mat, linearVelocity, angularVelocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ViveControllerManager::InputDevice::handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand) {
|
void ViveControllerManager::InputDevice::handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand) {
|
||||||
|
|
||||||
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
|
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
|
||||||
|
@ -276,6 +435,14 @@ enum ViveButtonChannel {
|
||||||
RIGHT_APP_MENU
|
RIGHT_APP_MENU
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool ViveControllerManager::InputDevice::checkForCalibrationEvent() {
|
||||||
|
auto& endOfMap = _buttonPressedMap.end();
|
||||||
|
auto& leftTrigger = _buttonPressedMap.find(controller::LT);
|
||||||
|
auto& rightTrigger = _buttonPressedMap.find(controller::RT);
|
||||||
|
auto& leftAppButton = _buttonPressedMap.find(LEFT_APP_MENU);
|
||||||
|
auto& rightAppButton = _buttonPressedMap.find(RIGHT_APP_MENU);
|
||||||
|
return ((leftTrigger != endOfMap && leftAppButton != endOfMap) && (rightTrigger != endOfMap && rightAppButton != endOfMap));
|
||||||
|
}
|
||||||
|
|
||||||
// These functions do translation from the Steam IDs to the standard controller IDs
|
// These functions do translation from the Steam IDs to the standard controller IDs
|
||||||
void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand) {
|
void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand) {
|
||||||
|
@ -305,6 +472,19 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ViveControllerManager::InputDevice::handleHeadPoseEvent(const controller::InputCalibrationData& inputCalibrationData, const mat4& mat,
|
||||||
|
const vec3& linearVelocity, const vec3& angularVelocity) {
|
||||||
|
|
||||||
|
//perform a 180 flip to make the HMD face the +z instead of -z, beacuse the head faces +z
|
||||||
|
glm::mat4 matYFlip = mat * Matrices::Y_180;
|
||||||
|
controller::Pose pose(extractTranslation(matYFlip), glmExtractRotation(matYFlip), linearVelocity, angularVelocity);
|
||||||
|
|
||||||
|
glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat;
|
||||||
|
glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat;
|
||||||
|
controller::Pose hmdHeadPose = pose.transform(sensorToAvatar);
|
||||||
|
_poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset);
|
||||||
|
}
|
||||||
|
|
||||||
void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData,
|
void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData,
|
||||||
const mat4& mat, const vec3& linearVelocity,
|
const mat4& mat, const vec3& linearVelocity,
|
||||||
const vec3& angularVelocity, bool isLeftHand) {
|
const vec3& angularVelocity, bool isLeftHand) {
|
||||||
|
@ -404,6 +584,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI
|
||||||
// 3d location of controller
|
// 3d location of controller
|
||||||
makePair(LEFT_HAND, "LeftHand"),
|
makePair(LEFT_HAND, "LeftHand"),
|
||||||
makePair(RIGHT_HAND, "RightHand"),
|
makePair(RIGHT_HAND, "RightHand"),
|
||||||
|
makePair(LEFT_FOOT, "LeftFoot"),
|
||||||
|
makePair(RIGHT_FOOT, "RightFoot"),
|
||||||
|
makePair(HIPS, "Hips"),
|
||||||
|
makePair(SPINE2, "Spine2"),
|
||||||
|
makePair(HEAD, "Head"),
|
||||||
|
|
||||||
// 16 tracked poses
|
// 16 tracked poses
|
||||||
makePair(TRACKED_OBJECT_00, "TrackedObject00"),
|
makePair(TRACKED_OBJECT_00, "TrackedObject00"),
|
||||||
|
|
|
@ -14,9 +14,11 @@
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <GLMHelpers.h>
|
#include <GLMHelpers.h>
|
||||||
|
|
||||||
#include <model/Geometry.h>
|
#include <model/Geometry.h>
|
||||||
#include <gpu/Texture.h>
|
#include <gpu/Texture.h>
|
||||||
#include <controllers/InputDevice.h>
|
#include <controllers/InputDevice.h>
|
||||||
|
@ -58,13 +60,21 @@ private:
|
||||||
|
|
||||||
bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override;
|
bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override;
|
||||||
void hapticsHelper(float deltaTime, bool leftHand);
|
void hapticsHelper(float deltaTime, bool leftHand);
|
||||||
|
void calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration);
|
||||||
|
void calibrate(const controller::InputCalibrationData& inputCalibration);
|
||||||
|
void uncalibrate();
|
||||||
|
controller::Pose addOffsetToPuckPose(int joint) const;
|
||||||
|
void updateCalibratedLimbs();
|
||||||
|
bool checkForCalibrationEvent();
|
||||||
void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand);
|
void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand);
|
||||||
|
void handleHmd(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData);
|
||||||
void handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData);
|
void handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData);
|
||||||
void handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand);
|
void handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand);
|
||||||
void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand);
|
void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand);
|
||||||
void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat,
|
void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat,
|
||||||
const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand);
|
const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand);
|
||||||
|
void handleHeadPoseEvent(const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity,
|
||||||
|
const vec3& angularVelocity);
|
||||||
void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton);
|
void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton);
|
||||||
|
|
||||||
class FilteredStick {
|
class FilteredStick {
|
||||||
|
@ -90,10 +100,14 @@ private:
|
||||||
float _timer { 0.0f };
|
float _timer { 0.0f };
|
||||||
glm::vec2 _stick { 0.0f, 0.0f };
|
glm::vec2 _stick { 0.0f, 0.0f };
|
||||||
};
|
};
|
||||||
|
enum class Config { Feet, FeetAndHips, FeetHipsAndChest, NoConfig };
|
||||||
|
Config _config { Config::NoConfig };
|
||||||
FilteredStick _filteredLeftStick;
|
FilteredStick _filteredLeftStick;
|
||||||
FilteredStick _filteredRightStick;
|
FilteredStick _filteredRightStick;
|
||||||
|
|
||||||
|
std::vector<std::pair<uint32_t, controller::Pose>> _validTrackedObjects;
|
||||||
|
std::map<uint32_t, glm::mat4> _pucksOffset;
|
||||||
|
std::map<int, uint32_t> _jointToPuckMap;
|
||||||
// perform an action when the InputDevice mutex is acquired.
|
// perform an action when the InputDevice mutex is acquired.
|
||||||
using Locker = std::unique_lock<std::recursive_mutex>;
|
using Locker = std::unique_lock<std::recursive_mutex>;
|
||||||
template <typename F>
|
template <typename F>
|
||||||
|
@ -101,10 +115,14 @@ private:
|
||||||
|
|
||||||
int _trackedControllers { 0 };
|
int _trackedControllers { 0 };
|
||||||
vr::IVRSystem*& _system;
|
vr::IVRSystem*& _system;
|
||||||
|
quint64 _timeTilCalibration { 0.0f };
|
||||||
float _leftHapticStrength { 0.0f };
|
float _leftHapticStrength { 0.0f };
|
||||||
float _leftHapticDuration { 0.0f };
|
float _leftHapticDuration { 0.0f };
|
||||||
float _rightHapticStrength { 0.0f };
|
float _rightHapticStrength { 0.0f };
|
||||||
float _rightHapticDuration { 0.0f };
|
float _rightHapticDuration { 0.0f };
|
||||||
|
bool _triggersPressedHandled { false };
|
||||||
|
bool _calibrated { false };
|
||||||
|
bool _timeTilCalibrationSet { false };
|
||||||
mutable std::recursive_mutex _lock;
|
mutable std::recursive_mutex _lock;
|
||||||
|
|
||||||
friend class ViveControllerManager;
|
friend class ViveControllerManager;
|
||||||
|
|
|
@ -122,7 +122,8 @@
|
||||||
function debug() {
|
function debug() {
|
||||||
var stateString = "<" + STATE_STRINGS[state] + ">";
|
var stateString = "<" + STATE_STRINGS[state] + ">";
|
||||||
var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]";
|
var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]";
|
||||||
print.apply(null, [].concat.apply([LABEL, stateString, JSON.stringify(waitingList), connecting],
|
var current = "[" + currentHand + "/" + currentHandJointIndex + "]"
|
||||||
|
print.apply(null, [].concat.apply([LABEL, stateString, current, JSON.stringify(waitingList), connecting],
|
||||||
[].map.call(arguments, JSON.stringify)));
|
[].map.call(arguments, JSON.stringify)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -759,7 +760,10 @@
|
||||||
break;
|
break;
|
||||||
case "done":
|
case "done":
|
||||||
delete waitingList[senderID];
|
delete waitingList[senderID];
|
||||||
if (state === STATES.CONNECTING && connectingId === senderID) {
|
if (connectingId !== senderID) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (state === STATES.CONNECTING) {
|
||||||
// if they are done, and didn't connect us, terminate our
|
// if they are done, and didn't connect us, terminate our
|
||||||
// connecting
|
// connecting
|
||||||
if (message.connectionId !== MyAvatar.sessionUUID) {
|
if (message.connectionId !== MyAvatar.sessionUUID) {
|
||||||
|
@ -768,11 +772,20 @@
|
||||||
// value for isKeyboard, as we should not change the animation
|
// value for isKeyboard, as we should not change the animation
|
||||||
// state anyways (if any)
|
// state anyways (if any)
|
||||||
startHandshake();
|
startHandshake();
|
||||||
|
} else {
|
||||||
|
// they just created a connection request to us, and we are connecting to
|
||||||
|
// them, so lets just stop connecting and make connection..
|
||||||
|
makeConnection(connectingId);
|
||||||
|
stopConnecting();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (state == STATES.MAKING_CONNECTION) {
|
||||||
|
// we are making connection, they just started, so lets reset the
|
||||||
|
// poll count just in case
|
||||||
|
pollCount = 0;
|
||||||
} else {
|
} else {
|
||||||
// if waiting or inactive, lets clear the connecting id. If in makingConnection,
|
// if waiting or inactive, lets clear the connecting id. If in makingConnection,
|
||||||
// do nothing
|
// do nothing
|
||||||
if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) {
|
|
||||||
clearConnecting();
|
clearConnecting();
|
||||||
if (state !== STATES.INACTIVE) {
|
if (state !== STATES.INACTIVE) {
|
||||||
startHandshake();
|
startHandshake();
|
||||||
|
|
|
@ -268,7 +268,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
|
||||||
break;
|
break;
|
||||||
case 'refreshConnections':
|
case 'refreshConnections':
|
||||||
print('Refreshing Connections...');
|
print('Refreshing Connections...');
|
||||||
getConnectionData();
|
getConnectionData(false);
|
||||||
UserActivityLogger.palAction("refresh_connections", "");
|
UserActivityLogger.palAction("refresh_connections", "");
|
||||||
break;
|
break;
|
||||||
case 'removeConnection':
|
case 'removeConnection':
|
||||||
|
@ -281,25 +281,27 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
|
||||||
print("Error: unable to remove connection", connectionUserName, error || response.status);
|
print("Error: unable to remove connection", connectionUserName, error || response.status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getConnectionData();
|
getConnectionData(false);
|
||||||
});
|
});
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'removeFriend':
|
case 'removeFriend':
|
||||||
friendUserName = message.params;
|
friendUserName = message.params;
|
||||||
|
print("Removing " + friendUserName + " from friends.");
|
||||||
request({
|
request({
|
||||||
uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName,
|
uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName,
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
}, function (error, response) {
|
}, function (error, response) {
|
||||||
if (error || (response.status !== 'success')) {
|
if (error || (response.status !== 'success')) {
|
||||||
print("Error: unable to unfriend", friendUserName, error || response.status);
|
print("Error: unable to unfriend " + friendUserName, error || response.status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getConnectionData();
|
getConnectionData(friendUserName);
|
||||||
});
|
});
|
||||||
break
|
break
|
||||||
case 'addFriend':
|
case 'addFriend':
|
||||||
friendUserName = message.params;
|
friendUserName = message.params;
|
||||||
|
print("Adding " + friendUserName + " to friends.");
|
||||||
request({
|
request({
|
||||||
uri: METAVERSE_BASE + '/api/v1/user/friends',
|
uri: METAVERSE_BASE + '/api/v1/user/friends',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -312,7 +314,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
|
||||||
print("Error: unable to friend " + friendUserName, error || response.status);
|
print("Error: unable to friend " + friendUserName, error || response.status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getConnectionData(); // For now, just refresh all connection data. Later, just refresh the one friended row.
|
getConnectionData(friendUserName);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -360,8 +362,6 @@ function getProfilePicture(username, callback) { // callback(url) if successfull
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
|
function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
|
||||||
// The back end doesn't do user connections yet. Fake it by getting all users that have made themselves accessible to us,
|
|
||||||
// and pretending that they are all connections.
|
|
||||||
url = METAVERSE_BASE + '/api/v1/users?'
|
url = METAVERSE_BASE + '/api/v1/users?'
|
||||||
if (domain) {
|
if (domain) {
|
||||||
url += 'status=' + domain.slice(1, -1); // without curly braces
|
url += 'status=' + domain.slice(1, -1); // without curly braces
|
||||||
|
@ -369,25 +369,22 @@ function getAvailableConnections(domain, callback) { // callback([{usename, loca
|
||||||
url += 'filter=connections'; // regardless of whether online
|
url += 'filter=connections'; // regardless of whether online
|
||||||
}
|
}
|
||||||
requestJSON(url, function (connectionsData) {
|
requestJSON(url, function (connectionsData) {
|
||||||
// The back end doesn't include the profile picture data, but we can add that here.
|
callback(connectionsData.users);
|
||||||
// For our current purposes, there's no need to be fancy and try to reduce latency by doing some number of requests in parallel,
|
|
||||||
// so these requests are all sequential.
|
|
||||||
var users = connectionsData.users;
|
|
||||||
function addPicture(index) {
|
|
||||||
if (index >= users.length) {
|
|
||||||
return callback(users);
|
|
||||||
}
|
|
||||||
var user = users[index];
|
|
||||||
getProfilePicture(user.username, function (url) {
|
|
||||||
user.profileUrl = url;
|
|
||||||
addPicture(index + 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addPicture(0);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function getInfoAboutUser(specificUsername, callback) {
|
||||||
function getConnectionData(domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick.
|
url = METAVERSE_BASE + '/api/v1/users?filter=connections'
|
||||||
|
requestJSON(url, function (connectionsData) {
|
||||||
|
for (user in connectionsData.users) {
|
||||||
|
if (connectionsData.users[user].username === specificUsername) {
|
||||||
|
callback(connectionsData.users[user]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getConnectionData(specificUsername, domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick.
|
||||||
function frob(user) { // get into the right format
|
function frob(user) { // get into the right format
|
||||||
var formattedSessionId = user.location.node_id || '';
|
var formattedSessionId = user.location.node_id || '';
|
||||||
if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) {
|
if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) {
|
||||||
|
@ -397,10 +394,19 @@ function getConnectionData(domain) { // Update all the usernames that I am entit
|
||||||
sessionId: formattedSessionId,
|
sessionId: formattedSessionId,
|
||||||
userName: user.username,
|
userName: user.username,
|
||||||
connection: user.connection,
|
connection: user.connection,
|
||||||
profileUrl: user.profileUrl,
|
profileUrl: user.images.thumbnail,
|
||||||
placeName: (user.location.root || user.location.domain || {}).name || ''
|
placeName: (user.location.root || user.location.domain || {}).name || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (specificUsername) {
|
||||||
|
getInfoAboutUser(specificUsername, function (user) {
|
||||||
|
if (user) {
|
||||||
|
updateUser(frob(user));
|
||||||
|
} else {
|
||||||
|
print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
getAvailableConnections(domain, function (users) {
|
getAvailableConnections(domain, function (users) {
|
||||||
if (domain) {
|
if (domain) {
|
||||||
users.forEach(function (user) {
|
users.forEach(function (user) {
|
||||||
|
@ -410,6 +416,7 @@ function getConnectionData(domain) { // Update all the usernames that I am entit
|
||||||
sendToQml({ method: 'connections', params: users.map(frob) });
|
sendToQml({ method: 'connections', params: users.map(frob) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -486,7 +493,7 @@ function populateNearbyUserList(selectData, oldAudioData) {
|
||||||
data.push(avatarPalDatum);
|
data.push(avatarPalDatum);
|
||||||
print('PAL data:', JSON.stringify(avatarPalDatum));
|
print('PAL data:', JSON.stringify(avatarPalDatum));
|
||||||
});
|
});
|
||||||
getConnectionData(location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain).
|
getConnectionData(false, location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain).
|
||||||
conserveResources = Object.keys(avatarsOfInterest).length > 20;
|
conserveResources = Object.keys(avatarsOfInterest).length > 20;
|
||||||
sendToQml({ method: 'nearbyUsers', params: data });
|
sendToQml({ method: 'nearbyUsers', params: data });
|
||||||
if (selectData) {
|
if (selectData) {
|
||||||
|
|
179
unpublishedScripts/interaction/Interaction.js
Normal file
179
unpublishedScripts/interaction/Interaction.js
Normal 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");
|
||||||
|
});
|
179
unpublishedScripts/interaction/NPCHelpers.js
Normal file
179
unpublishedScripts/interaction/NPCHelpers.js
Normal 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);
|
||||||
|
}
|
102
unpublishedScripts/interaction/NPC_AC.js
Normal file
102
unpublishedScripts/interaction/NPC_AC.js
Normal 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);
|
||||||
|
}
|
159
unpublishedScripts/interaction/Sphinx.json
Normal file
159
unpublishedScripts/interaction/Sphinx.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue