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

This commit is contained in:
Sam Gateau 2017-05-17 09:49:20 -07:00
commit 0f250e693d
34 changed files with 988 additions and 460 deletions

View file

@ -57,6 +57,7 @@ Agent::Agent(ReceivedMessage& message) :
ThreadedAssignment(message),
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES)
{
_entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT);
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
ResourceManager::init();

View file

@ -24,9 +24,6 @@
#include <ScriptEngine.h>
#include <ThreadedAssignment.h>
static const int DEFAULT_MAX_ENTITY_PPS = 9000;
static const int DEFAULT_ENTITY_PPS_PER_SCRIPT = 900;
class EntityScriptServer : public ThreadedAssignment {
Q_OBJECT

View file

@ -563,11 +563,8 @@ const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true;
const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false;
const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false;
Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) :
Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
QApplication(argc, argv),
_shouldRunServer(runServer),
_runServerPath(runServerPathOption),
_runningMarker(this, RUNNING_MARKER_FILENAME),
_window(new MainWindow(desktop())),
_sessionRunTimer(startupTimer),
_previousSessionCrashed(setupEssentials(argc, argv)),
@ -622,8 +619,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// make sure the debug draw singleton is initialized on the main thread.
DebugDraw::getInstance().removeMarker("");
_runningMarker.startRunningMarker();
PluginContainer* pluginContainer = dynamic_cast<PluginContainer*>(this); // set the container for any plugins that care
PluginManager::getInstance()->setContainer(pluginContainer);
@ -675,38 +670,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
static const QString OCULUS_STORE_ARG = "--oculus-store";
setProperty(hifi::properties::OCULUS_STORE, arguments().indexOf(OCULUS_STORE_ARG) != -1);
static const QString NO_UPDATER_ARG = "--no-updater";
static const bool noUpdater = arguments().indexOf(NO_UPDATER_ARG) != -1;
static const bool wantsSandboxRunning = shouldRunServer();
static bool determinedSandboxState = false;
static bool sandboxIsRunning = false;
SandboxUtils sandboxUtils;
// updateHeartbeat() because we are going to poll shortly...
updateHeartbeat();
sandboxUtils.ifLocalSandboxRunningElse([&]() {
qCDebug(interfaceapp) << "Home sandbox appears to be running.....";
determinedSandboxState = true;
sandboxIsRunning = true;
}, [&]() {
qCDebug(interfaceapp) << "Home sandbox does not appear to be running....";
if (wantsSandboxRunning) {
QString contentPath = getRunServerPath();
SandboxUtils::runLocalSandbox(contentPath, true, RUNNING_MARKER_FILENAME, noUpdater);
sandboxIsRunning = true;
}
determinedSandboxState = true;
});
// SandboxUtils::runLocalSandbox currently has 2 sec delay after spawning sandbox, so 4
// sec here is ok I guess. TODO: ping sandbox so we know it is up, perhaps?
quint64 MAX_WAIT_TIME = USECS_PER_SECOND * 4;
auto startWaiting = usecTimestampNow();
while (!determinedSandboxState && (usecTimestampNow() - startWaiting <= MAX_WAIT_TIME)) {
QCoreApplication::processEvents();
// updateHeartbeat() while polling so we don't scare the deadlock watchdog
updateHeartbeat();
usleep(USECS_PER_MSEC * 50); // 20hz
}
// start the nodeThread so its event loop is running
QThread* nodeThread = new QThread(this);
@ -1223,6 +1187,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
#endif
// If launched from Steam, let it handle updates
const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater";
bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1;
if (!noUpdater) {
auto applicationUpdater = DependencyManager::get<AutoUpdater>();
connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog);
@ -1465,110 +1431,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
const auto testScript = property(hifi::properties::TEST).toUrl();
scriptEngines->loadScript(testScript, false);
} else {
enum HandControllerType {
Vive,
Oculus
};
static const std::map<HandControllerType, int> MIN_CONTENT_VERSION = {
{ Vive, 1 },
{ Oculus, 27 }
};
// Get sandbox content set version
auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/";
auto contentVersionPath = acDirPath + "content-version.txt";
qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version";
int contentVersion = 0;
QFile contentVersionFile(contentVersionPath);
if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString line = contentVersionFile.readAll();
contentVersion = line.toInt(); // returns 0 if conversion fails
}
// Get controller availability
bool hasHandControllers = false;
HandControllerType handControllerType = Vive;
if (PluginUtils::isViveControllerAvailable()) {
hasHandControllers = true;
handControllerType = Vive;
} else if (PluginUtils::isOculusTouchControllerAvailable()) {
hasHandControllers = true;
handControllerType = Oculus;
}
// Check tutorial content versioning
bool hasTutorialContent = contentVersion >= MIN_CONTENT_VERSION.at(handControllerType);
// Check HMD use (may be technically available without being in use)
bool hasHMD = PluginUtils::isHMDAvailable();
bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd();
Setting::Handle<bool> tutorialComplete { "tutorialComplete", false };
Setting::Handle<bool> firstRun { Settings::firstRun, true };
bool isTutorialComplete = tutorialComplete.get();
bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete;
qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD;
qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent <<
", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial;
// when --url in command line, teleport to location
const QString HIFI_URL_COMMAND_LINE_KEY = "--url";
int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY);
QString addressLookupString;
if (urlIndex != -1) {
addressLookupString = arguments().value(urlIndex + 1);
}
const QString TUTORIAL_PATH = "/tutorial_begin";
if (shouldGoToTutorial) {
if (sandboxIsRunning) {
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox(TUTORIAL_PATH);
} else {
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
if (firstRun.get()) {
showHelp();
}
if (addressLookupString.isEmpty()) {
DependencyManager::get<AddressManager>()->goToEntry();
} else {
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
}
}
} else {
bool isFirstRun = firstRun.get();
if (isFirstRun) {
showHelp();
}
// If this is a first run we short-circuit the address passed in
if (isFirstRun) {
if (isUsingHMD) {
if (sandboxIsRunning) {
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox();
} else {
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
DependencyManager::get<AddressManager>()->goToEntry();
}
} else {
DependencyManager::get<AddressManager>()->goToEntry();
}
} else {
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
}
}
_connectionMonitor.init();
// After all of the constructor is completed, then set firstRun to false.
firstRun.set(false);
PROFILE_RANGE(render, "GetSandboxStatus");
auto reply = SandboxUtils::getStatus();
connect(reply, &QNetworkReply::finished, this, [=] {
handleSandboxStatus(reply);
});
}
// Monitor model assets (e.g., from Clara.io) added to the world that may need resizing.
@ -2474,6 +2341,118 @@ void Application::resizeGL() {
}
}
void Application::handleSandboxStatus(QNetworkReply* reply) {
PROFILE_RANGE(render, "HandleSandboxStatus");
bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll());
qDebug() << "HandleSandboxStatus" << sandboxIsRunning;
enum HandControllerType {
Vive,
Oculus
};
static const std::map<HandControllerType, int> MIN_CONTENT_VERSION = {
{ Vive, 1 },
{ Oculus, 27 }
};
// Get sandbox content set version
auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/";
auto contentVersionPath = acDirPath + "content-version.txt";
qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version";
int contentVersion = 0;
QFile contentVersionFile(contentVersionPath);
if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString line = contentVersionFile.readAll();
contentVersion = line.toInt(); // returns 0 if conversion fails
}
// Get controller availability
bool hasHandControllers = false;
HandControllerType handControllerType = Vive;
if (PluginUtils::isViveControllerAvailable()) {
hasHandControllers = true;
handControllerType = Vive;
} else if (PluginUtils::isOculusTouchControllerAvailable()) {
hasHandControllers = true;
handControllerType = Oculus;
}
// Check tutorial content versioning
bool hasTutorialContent = contentVersion >= MIN_CONTENT_VERSION.at(handControllerType);
// Check HMD use (may be technically available without being in use)
bool hasHMD = PluginUtils::isHMDAvailable();
bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd();
Setting::Handle<bool> tutorialComplete{ "tutorialComplete", false };
Setting::Handle<bool> firstRun{ Settings::firstRun, true };
bool isTutorialComplete = tutorialComplete.get();
bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete;
qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD;
qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent <<
", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial;
// when --url in command line, teleport to location
const QString HIFI_URL_COMMAND_LINE_KEY = "--url";
int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY);
QString addressLookupString;
if (urlIndex != -1) {
addressLookupString = arguments().value(urlIndex + 1);
}
const QString TUTORIAL_PATH = "/tutorial_begin";
if (shouldGoToTutorial) {
if (sandboxIsRunning) {
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox(TUTORIAL_PATH);
} else {
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
if (firstRun.get()) {
showHelp();
}
if (addressLookupString.isEmpty()) {
DependencyManager::get<AddressManager>()->goToEntry();
} else {
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
}
}
} else {
bool isFirstRun = firstRun.get();
if (isFirstRun) {
showHelp();
}
// If this is a first run we short-circuit the address passed in
if (isFirstRun) {
if (isUsingHMD) {
if (sandboxIsRunning) {
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox();
} else {
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
DependencyManager::get<AddressManager>()->goToEntry();
}
} else {
DependencyManager::get<AddressManager>()->goToEntry();
}
} else {
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
}
}
_connectionMonitor.init();
// After all of the constructor is completed, then set firstRun to false.
firstRun.set(false);
}
bool Application::importJSONFromURL(const QString& urlString) {
// we only load files that terminate in just .json (not .svo.json and not .ava.json)
// if they come from the High Fidelity Marketplace Assets CDN

View file

@ -112,17 +112,7 @@ class Application : public QApplication,
// TODO? Get rid of those
friend class OctreePacketProcessor;
private:
bool _shouldRunServer { false };
QString _runServerPath;
RunningMarker _runningMarker;
public:
// startup related getter/setters
bool shouldRunServer() const { return _shouldRunServer; }
bool hasRunServerPath() const { return !_runServerPath.isEmpty(); }
QString getRunServerPath() const { return _runServerPath; }
// virtual functions required for PluginContainer
virtual ui::Menu* getPrimaryMenu() override;
virtual void requestReset() override { resetSensors(true); }
@ -146,7 +136,7 @@ public:
static void initPlugins(const QStringList& arguments);
static void shutdownPlugins();
Application(int& argc, char** argv, QElapsedTimer& startup_time, bool runServer, QString runServerPathOption);
Application(int& argc, char** argv, QElapsedTimer& startup_time);
~Application();
void postLambdaEvent(std::function<void()> f) override;
@ -452,6 +442,8 @@ private slots:
void addAssetToWorldInfoTimeout();
void addAssetToWorldErrorTimeout();
void handleSandboxStatus(QNetworkReply* reply);
private:
static void initDisplay();
void init();

View file

@ -110,6 +110,9 @@ MyAvatar::MyAvatar(QThread* thread, RigPointer rig) :
_realWorldFieldOfView("realWorldFieldOfView",
DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES),
_useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false),
_smoothOrientationTimer(std::numeric_limits<float>::max()),
_smoothOrientationInitial(),
_smoothOrientationTarget(),
_hmdSensorMatrix(),
_hmdSensorOrientation(),
_hmdSensorPosition(),
@ -265,6 +268,17 @@ QVariant MyAvatar::getOrientationVar() const {
return quatToVariant(Avatar::getOrientation());
}
glm::quat MyAvatar::getOrientationOutbound() const {
// Allows MyAvatar to send out smoothed data to remote agents if required.
if (_smoothOrientationTimer > SMOOTH_TIME_ORIENTATION) {
return (getLocalOrientation());
}
// Smooth the remote avatar movement.
float t = _smoothOrientationTimer / SMOOTH_TIME_ORIENTATION;
float interp = Interpolate::easeInOutQuad(glm::clamp(t, 0.0f, 1.0f));
return (slerp(_smoothOrientationInitial, _smoothOrientationTarget, interp));
}
// virtual
void MyAvatar::simulateAttachments(float deltaTime) {
@ -393,6 +407,11 @@ void MyAvatar::update(float deltaTime) {
float tau = deltaTime / HMD_FACING_TIMESCALE;
_hmdSensorFacingMovingAverage = lerp(_hmdSensorFacingMovingAverage, _hmdSensorFacing, tau);
if (_smoothOrientationTimer < SMOOTH_TIME_ORIENTATION) {
_rotationChanged = usecTimestampNow();
_smoothOrientationTimer += deltaTime;
}
#ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE
glm::vec3 p = transformPoint(getSensorToWorldMatrix(), _hmdSensorPosition + glm::vec3(_hmdSensorFacingMovingAverage.x, 0.0f, _hmdSensorFacingMovingAverage.y));
DebugDraw::getInstance().addMarker("facing-avg", getOrientation(), p, glm::vec4(1.0f));
@ -1817,8 +1836,10 @@ void MyAvatar::updateOrientation(float deltaTime) {
// Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll
// get an instantaneous 15 degree turn. If you keep holding the key down you'll get another
// snap turn every half second.
bool snapTurn = false;
if (getDriveKey(STEP_YAW) != 0.0f) {
totalBodyYaw += getDriveKey(STEP_YAW);
snapTurn = true;
}
// use head/HMD orientation to turn while flying
@ -1851,10 +1872,17 @@ void MyAvatar::updateOrientation(float deltaTime) {
totalBodyYaw += (speedFactor * deltaAngle * (180.0f / PI));
}
// update body orientation by movement inputs
glm::quat initialOrientation = getOrientationOutbound();
setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f))));
if (snapTurn) {
// Whether or not there is an existing smoothing going on, just reset the smoothing timer and set the starting position as the avatar's current position, then smooth to the new position.
_smoothOrientationInitial = initialOrientation;
_smoothOrientationTarget = getOrientation();
_smoothOrientationTimer = 0.0f;
}
getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime);
if (qApp->isHMDMode()) {

View file

@ -190,6 +190,8 @@ public:
Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar);
Q_INVOKABLE QVariant getOrientationVar() const;
// A method intended to be overriden by MyAvatar for polling orientation for network transmission.
glm::quat getOrientationOutbound() const override;
// Pass a recent sample of the HMD to the avatar.
// This can also update the avatar's position to follow the HMD
@ -633,6 +635,14 @@ private:
Setting::Handle<float> _realWorldFieldOfView;
Setting::Handle<bool> _useAdvancedMovementControls;
// Smoothing.
const float SMOOTH_TIME_ORIENTATION = 0.5f;
// Smoothing data for blending from one position/orientation to another on remote agents.
float _smoothOrientationTimer;
glm::quat _smoothOrientationInitial;
glm::quat _smoothOrientationTarget;
// private methods
void updateOrientation(float deltaTime);
void updateActionMotor(float deltaTime);

View file

@ -11,6 +11,7 @@
#include <thread>
#include <QCommandLineParser>
#include <QtCore/QProcess>
#include <QDebug>
#include <QDir>
#include <QLocalSocket>
@ -20,6 +21,7 @@
#include <BuildInfo.h>
#include <gl/OpenGLVersionChecker.h>
#include <SandboxUtils.h>
#include <SharedUtil.h>
@ -28,7 +30,6 @@
#include "InterfaceLogging.h"
#include "UserActivityLogger.h"
#include "MainWindow.h"
#include <QtCore/QProcess>
#ifdef HAS_BUGSPLAT
#include <BugSplat.h>
@ -50,50 +51,49 @@ int main(int argc, const char* argv[]) {
disableQtBearerPoll(); // Fixes wifi ping spikes
QElapsedTimer startupTime;
startupTime.start();
// Set application infos
QCoreApplication::setApplicationName(BuildInfo::INTERFACE_NAME);
QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN);
QCoreApplication::setApplicationVersion(BuildInfo::VERSION);
const QString& applicationName = getInterfaceSharedMemoryName();
bool instanceMightBeRunning = true;
QStringList arguments;
for (int i = 0; i < argc; ++i) {
arguments << argv[i];
}
#ifdef Q_OS_WIN
// Try to create a shared memory block - if it can't be created, there is an instance of
// interface already running. We only do this on Windows for now because of the potential
// for crashed instances to leave behind shared memory instances on unix.
QSharedMemory sharedMemory { applicationName };
instanceMightBeRunning = !sharedMemory.create(1, QSharedMemory::ReadOnly);
#endif
// allow multiple interfaces to run if this environment variable is set.
if (QProcessEnvironment::systemEnvironment().contains("HIFI_ALLOW_MULTIPLE_INSTANCES")) {
instanceMightBeRunning = false;
}
QCommandLineParser parser;
QCommandLineOption urlOption("url", "", "value");
QCommandLineOption noUpdaterOption("no-updater", "Do not show auto-updater");
QCommandLineOption checkMinSpecOption("checkMinSpec", "Check if machine meets minimum specifications");
QCommandLineOption runServerOption("runServer", "Whether to run the server");
QCommandLineOption serverContentPathOption("serverContentPath", "Where to find server content", "serverContentPath");
QCommandLineOption allowMultipleInstancesOption("allowMultipleInstances", "Allow multiple instances to run");
parser.addOption(urlOption);
parser.addOption(noUpdaterOption);
parser.addOption(checkMinSpecOption);
parser.addOption(runServerOption);
parser.addOption(serverContentPathOption);
parser.addOption(allowMultipleInstancesOption);
parser.parse(arguments);
bool runServer = parser.isSet(runServerOption);
bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption);
QString serverContentPathOptionValue = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString();
bool allowMultipleInstances = parser.isSet(allowMultipleInstancesOption);
const QString& applicationName = getInterfaceSharedMemoryName();
bool instanceMightBeRunning = true;
#ifdef Q_OS_WIN
// Try to create a shared memory block - if it can't be created, there is an instance of
// interface already running. We only do this on Windows for now because of the potential
// for crashed instances to leave behind shared memory instances on unix.
QSharedMemory sharedMemory{ applicationName };
instanceMightBeRunning = !sharedMemory.create(1, QSharedMemory::ReadOnly);
#endif
// allow multiple interfaces to run if this environment variable is set.
bool allowMultipleInstances = parser.isSet(allowMultipleInstancesOption) ||
QProcessEnvironment::systemEnvironment().contains("HIFI_ALLOW_MULTIPLE_INSTANCES");
if (allowMultipleInstances) {
instanceMightBeRunning = false;
}
@ -108,11 +108,6 @@ int main(int argc, const char* argv[]) {
// Try to connect - if we can't connect, interface has probably just gone down
if (socket.waitForConnected(LOCAL_SERVER_TIMEOUT_MS)) {
QCommandLineParser parser;
QCommandLineOption urlOption("url", "", "value");
parser.addOption(urlOption);
parser.process(arguments);
if (parser.isSet(urlOption)) {
QUrl url = QUrl(parser.value(urlOption));
if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) {
@ -156,9 +151,6 @@ int main(int argc, const char* argv[]) {
}
}
QElapsedTimer startupTime;
startupTime.start();
// Debug option to demonstrate that the client's local time does not
// need to be in sync with any other network node. This forces clock
// skew for the individual client
@ -199,7 +191,21 @@ int main(int argc, const char* argv[]) {
int exitCode;
{
Application app(argc, const_cast<char**>(argv), startupTime, runServer, serverContentPathOptionValue);
RunningMarker runningMarker(nullptr, RUNNING_MARKER_FILENAME);
runningMarker.writeRunningMarkerFile();
bool noUpdater = parser.isSet(noUpdaterOption);
bool runServer = parser.isSet(runServerOption);
bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption);
QString serverContentPath = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString();
if (runServer) {
SandboxUtils::runLocalSandbox(serverContentPath, true, RUNNING_MARKER_FILENAME, noUpdater);
}
Application app(argc, const_cast<char**>(argv), startupTime);
// Now that the main event loop is setup, launch running marker thread
runningMarker.startRunningMarker();
// If we failed the OpenGLVersion check, log it.
if (override) {

View file

@ -338,34 +338,6 @@ void Avatar::simulate(float deltaTime, bool inView) {
_simulationInViewRate.increment();
}
if (!isMyAvatar()) {
if (_smoothPositionTimer < _smoothPositionTime) {
// Smooth the remote avatar movement.
_smoothPositionTimer += deltaTime;
if (_smoothPositionTimer < _smoothPositionTime) {
AvatarData::setPosition(
lerp(_smoothPositionInitial,
_smoothPositionTarget,
easeInOutQuad(glm::clamp(_smoothPositionTimer / _smoothPositionTime, 0.0f, 1.0f)))
);
updateAttitude();
}
}
if (_smoothOrientationTimer < _smoothOrientationTime) {
// Smooth the remote avatar movement.
_smoothOrientationTimer += deltaTime;
if (_smoothOrientationTimer < _smoothOrientationTime) {
AvatarData::setOrientation(
slerp(_smoothOrientationInitial,
_smoothOrientationTarget,
easeInOutQuad(glm::clamp(_smoothOrientationTimer / _smoothOrientationTime, 0.0f, 1.0f)))
);
updateAttitude();
}
}
}
PerformanceTimer perfTimer("simulate");
{
PROFILE_RANGE(simulation, "updateJoints");
@ -1371,31 +1343,13 @@ glm::quat Avatar::getUncachedRightPalmRotation() const {
}
void Avatar::setPosition(const glm::vec3& position) {
if (isMyAvatar()) {
// This is the local avatar, no need to handle any position smoothing.
AvatarData::setPosition(position);
updateAttitude();
return;
}
// Whether or not there is an existing smoothing going on, just reset the smoothing timer and set the starting position as the avatar's current position, then smooth to the new position.
_smoothPositionInitial = getPosition();
_smoothPositionTarget = position;
_smoothPositionTimer = 0.0f;
AvatarData::setPosition(position);
updateAttitude();
}
void Avatar::setOrientation(const glm::quat& orientation) {
if (isMyAvatar()) {
// This is the local avatar, no need to handle any position smoothing.
AvatarData::setOrientation(orientation);
updateAttitude();
return;
}
// Whether or not there is an existing smoothing going on, just reset the smoothing timer and set the starting position as the avatar's current position, then smooth to the new position.
_smoothOrientationInitial = getOrientation();
_smoothOrientationTarget = orientation;
_smoothOrientationTimer = 0.0f;
AvatarData::setOrientation(orientation);
updateAttitude();
}
void Avatar::updatePalms() {

View file

@ -36,7 +36,6 @@ namespace render {
}
static const float SCALING_RATIO = .05f;
static const float SMOOTHING_RATIO = .05f; // 0 < ratio < 1
extern const float CHAT_MESSAGE_SCALE;
extern const float CHAT_MESSAGE_HEIGHT;
@ -239,17 +238,8 @@ public:
bool hasNewJointData() const { return _hasNewJointData; }
inline float easeInOutQuad(float lerpValue) {
assert(!((lerpValue < 0.0f) || (lerpValue > 1.0f)));
if (lerpValue < 0.5f) {
return (2.0f * lerpValue * lerpValue);
}
return (lerpValue*(4.0f - 2.0f * lerpValue) - 1.0f);
}
float getBoundingRadius() const;
void addToScene(AvatarSharedPointer self, const render::ScenePointer& scene);
void ensureInScene(AvatarSharedPointer self, const render::ScenePointer& scene);
bool isInScene() const { return render::Item::isValidID(_renderItemID); }
@ -271,9 +261,6 @@ public slots:
void setModelURLFinished(bool success);
protected:
const float SMOOTH_TIME_POSITION = 0.125f;
const float SMOOTH_TIME_ORIENTATION = 0.075f;
virtual const QString& getSessionDisplayNameForTransport() const override { return _empty; } // Save a tiny bit of bandwidth. Mixer won't look at what we send.
QString _empty{};
virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) override { _sessionDisplayName = sessionDisplayName; } // don't use no-op setter!
@ -336,16 +323,6 @@ protected:
RateCounter<> _skeletonModelSimulationRate;
RateCounter<> _jointDataSimulationRate;
// Smoothing data for blending from one position/orientation to another on remote agents.
float _smoothPositionTime { SMOOTH_TIME_POSITION };
float _smoothPositionTimer { std::numeric_limits<float>::max() };
float _smoothOrientationTime { SMOOTH_TIME_ORIENTATION };
float _smoothOrientationTimer { std::numeric_limits<float>::max() };
glm::vec3 _smoothPositionInitial;
glm::vec3 _smoothPositionTarget;
glm::quat _smoothOrientationInitial;
glm::quat _smoothOrientationTarget;
private:
class AvatarEntityDataHash {
public:

View file

@ -310,7 +310,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
if (hasAvatarOrientation) {
auto startSection = destinationBuffer;
auto localOrientation = getLocalOrientation();
auto localOrientation = getOrientationOutbound();
destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, localOrientation);
int numBytes = destinationBuffer - startSection;
@ -1489,6 +1489,10 @@ void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& ide
}
glm::quat AvatarData::getOrientationOutbound() const {
return (getLocalOrientation());
}
static const QUrl emptyURL("");
QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const {
// We don't put file urls on the wire, but instead convert to empty.
@ -1496,13 +1500,18 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const {
}
void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, const qint64 clockSkew) {
quint64 identityPacketUpdatedAt = identity.updatedAt;
if (identityPacketUpdatedAt <= (uint64_t)(abs(clockSkew))) { // Incoming timestamp is bad - compute our own timestamp
identityPacketUpdatedAt = usecTimestampNow() + clockSkew;
}
// Consider the case where this packet is being processed on Client A, and Client A is connected to Sandbox B.
// If Client A's system clock is *ahead of* Sandbox B's system clock, "clockSkew" will be *negative*.
// If Client A's system clock is *behind* Sandbox B's system clock, "clockSkew" will be *positive*.
if ((_identityUpdatedAt > identity.updatedAt - clockSkew) && (_identityUpdatedAt != 0)) {
if ((_identityUpdatedAt > identityPacketUpdatedAt - clockSkew) && (_identityUpdatedAt != 0)) {
qCDebug(avatars) << "Ignoring late identity packet for avatar " << getSessionUUID()
<< "_identityUpdatedAt (" << _identityUpdatedAt << ") is greater than identity.updatedAt - clockSkew (" << identity.updatedAt << "-" << clockSkew << ")";
<< "_identityUpdatedAt (" << _identityUpdatedAt << ") is greater than identityPacketUpdatedAt - clockSkew (" << identityPacketUpdatedAt << "-" << clockSkew << ")";
return;
}
@ -1538,7 +1547,7 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC
// use the timestamp from this identity, since we want to honor the updated times in "server clock"
// this will overwrite any changes we made locally to this AvatarData's _identityUpdatedAt
_identityUpdatedAt = identity.updatedAt - clockSkew;
_identityUpdatedAt = identityPacketUpdatedAt - clockSkew;
}
QByteArray AvatarData::identityByteArray() const {
@ -2047,11 +2056,13 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) {
setSkeletonModelURL(bodyModelURL);
}
}
QString newDisplayName = "";
if (json.contains(JSON_AVATAR_DISPLAY_NAME)) {
auto newDisplayName = json[JSON_AVATAR_DISPLAY_NAME].toString();
if (newDisplayName != getDisplayName()) {
setDisplayName(newDisplayName);
}
newDisplayName = json[JSON_AVATAR_DISPLAY_NAME].toString();
}
if (newDisplayName != getDisplayName()) {
setDisplayName(newDisplayName);
}
auto currentBasis = getRecordingBasis();
@ -2081,14 +2092,16 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) {
setTargetScale((float)json[JSON_AVATAR_SCALE].toDouble());
}
QVector<AttachmentData> attachments;
if (json.contains(JSON_AVATAR_ATTACHMENTS) && json[JSON_AVATAR_ATTACHMENTS].isArray()) {
QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHMENTS].toArray();
QVector<AttachmentData> attachments;
for (auto attachmentJson : attachmentsJson) {
AttachmentData attachment;
attachment.fromJson(attachmentJson.toObject());
attachments.push_back(attachment);
}
}
if (attachments != getAttachmentData()) {
setAttachmentData(attachments);
}

View file

@ -604,6 +604,9 @@ public:
return _lastSentJointData;
}
// A method intended to be overriden by MyAvatar for polling orientation for network transmission.
virtual glm::quat getOrientationOutbound() const;
static const float OUT_OF_VIEW_PENALTY;
static void sortAvatars(

View file

@ -662,6 +662,25 @@ QVector<QUuid> EntityScriptingInterface::findEntitiesInFrustum(QVariantMap frust
return result;
}
QVector<QUuid> EntityScriptingInterface::findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const {
EntityTypes::EntityType type = EntityTypes::getEntityTypeFromName(entityType);
QVector<QUuid> result;
if (_entityTree) {
QVector<EntityItemPointer> entities;
_entityTree->withReadLock([&] {
_entityTree->findEntities(center, radius, entities);
});
foreach(EntityItemPointer entity, entities) {
if (entity->getType() == type) {
result << entity->getEntityItemID();
}
}
}
return result;
}
RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersection(const PickRay& ray, bool precisionPicking,
const QScriptValue& entityIdsToInclude, const QScriptValue& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) {
PROFILE_RANGE(script_entities, __FUNCTION__);

View file

@ -212,9 +212,16 @@ public slots:
/// - orientation
/// - projection
/// - centerRadius
/// this function will not find any models in script engine contexts which don't have access to models
/// this function will not find any models in script engine contexts which don't have access to entities
Q_INVOKABLE QVector<QUuid> findEntitiesInFrustum(QVariantMap frustum) const;
/// finds entities of the indicated type within a sphere given by the center point and radius
/// @param {QString} string representation of entity type
/// @param {vec3} center point
/// @param {float} radius to search
/// this function will not find any entities in script engine contexts which don't have access to entities
Q_INVOKABLE QVector<QUuid> findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const;
/// If the scripting context has visible entities, this will determine a ray intersection, the results
/// may be inaccurate if the engine is unable to access the visible entities, in which case result.accurate
/// will be false.

View file

@ -452,6 +452,13 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign
// NOTE: callers must lock the tree before using this method
DeleteEntityOperator theOperator(getThisPointer(), entityID);
existingEntity->forEachDescendant([&](SpatiallyNestablePointer descendant) {
auto descendantID = descendant->getID();
theOperator.addEntityIDToDeleteList(descendantID);
emit deletingEntity(descendantID);
});
recurseTreeWithOperator(&theOperator);
processRemovedEntities(theOperator);
_isDirty = true;

View file

@ -9,63 +9,52 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QDataStream>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include "SandboxUtils.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QProcess>
#include <QStandardPaths>
#include <QThread>
#include <QTimer>
#include <NumericalConstants.h>
#include <SharedUtil.h>
#include <RunningMarker.h>
#include "SandboxUtils.h"
#include "NetworkAccessManager.h"
#include "NetworkLogging.h"
namespace SandboxUtils {
void SandboxUtils::ifLocalSandboxRunningElse(std::function<void()> localSandboxRunningDoThis,
std::function<void()> localSandboxNotRunningDoThat) {
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* getStatus() {
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest sandboxStatus(SANDBOX_STATUS_URL);
sandboxStatus.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
sandboxStatus.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
QNetworkReply* reply = networkAccessManager.get(sandboxStatus);
connect(reply, &QNetworkReply::finished, this, [reply, localSandboxRunningDoThis, localSandboxNotRunningDoThat]() {
if (reply->error() == QNetworkReply::NoError) {
auto statusData = reply->readAll();
auto statusJson = QJsonDocument::fromJson(statusData);
if (!statusJson.isEmpty()) {
auto statusObject = statusJson.object();
auto serversValue = statusObject.value("servers");
if (!serversValue.isUndefined() && serversValue.isObject()) {
auto serversObject = serversValue.toObject();
auto serversCount = serversObject.size();
const int MINIMUM_EXPECTED_SERVER_COUNT = 5;
if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) {
localSandboxRunningDoThis();
return;
}
}
}
}
localSandboxNotRunningDoThat();
});
return networkAccessManager.get(sandboxStatus);
}
bool readStatus(QByteArray statusData) {
auto statusJson = QJsonDocument::fromJson(statusData);
void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater) {
QString applicationDirPath = QFileInfo(QCoreApplication::applicationFilePath()).path();
QString serverPath = applicationDirPath + "/server-console/server-console.exe";
qCDebug(networking) << "Application dir path is: " << applicationDirPath;
if (!statusJson.isEmpty()) {
auto statusObject = statusJson.object();
auto serversValue = statusObject.value("servers");
if (!serversValue.isUndefined() && serversValue.isObject()) {
auto serversObject = serversValue.toObject();
auto serversCount = serversObject.size();
const int MINIMUM_EXPECTED_SERVER_COUNT = 5;
if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) {
return true;
}
}
}
return false;
}
void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater) {
QString serverPath = "./server-console/server-console.exe";
qCDebug(networking) << "Running marker path is: " << runningMarkerName;
qCDebug(networking) << "Server path is: " << serverPath;
qCDebug(networking) << "autoShutdown: " << autoShutdown;
qCDebug(networking) << "noUpdater: " << noUpdater;
@ -80,7 +69,7 @@ void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QStri
}
if (hasContentPath) {
QString serverContentPath = applicationDirPath + "/" + contentPath;
QString serverContentPath = "./" + contentPath;
args << "--contentPath" << serverContentPath;
}
@ -93,10 +82,8 @@ void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QStri
args << "--noUpdater";
}
qCDebug(networking) << applicationDirPath;
qCDebug(networking) << "Launching sandbox with:" << args;
qCDebug(networking) << QProcess::startDetached(serverPath, args);
// Sleep a short amount of time to give the server a chance to start
usleep(2000000); /// do we really need this??
}
}

View file

@ -12,21 +12,16 @@
#ifndef hifi_SandboxUtils_h
#define hifi_SandboxUtils_h
#include <functional>
#include <QtCore/QObject>
#include <QtCore/QString>
class QNetworkReply;
const QString SANDBOX_STATUS_URL = "http://localhost:60332/status";
namespace SandboxUtils {
const QString SANDBOX_STATUS_URL = "http://localhost:60332/status";
class SandboxUtils : public QObject {
Q_OBJECT
public:
/// determines if the local sandbox is likely running. It does not account for custom setups, and is only
/// intended to detect the standard local sandbox install.
void ifLocalSandboxRunningElse(std::function<void()> localSandboxRunningDoThis,
std::function<void()> localSandboxNotRunningDoThat);
static void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater);
QNetworkReply* getStatus();
bool readStatus(QByteArray statusData);
void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName, bool noUpdater);
};
#endif // hifi_SandboxUtils_h

View file

@ -376,7 +376,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g
glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f));
glm::vec3 meshFrameDirection = glm::vec3(worldToMeshMatrix * glm::vec4(direction, 0.0f));
for (const auto& triangleSet : _modelSpaceMeshTriangleSets) {
for (auto& triangleSet : _modelSpaceMeshTriangleSets) {
float triangleSetDistance = 0.0f;
BoxFace triangleSetFace;
glm::vec3 triangleSetNormal;
@ -1052,7 +1052,6 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) {
void Model::computeMeshPartLocalBounds() {
for (auto& part : _modelMeshRenderItems) {
assert(part->_meshIndex < _modelMeshRenderItems.size());
const Model::MeshState& state = _meshStates.at(part->_meshIndex);
part->computeAdjustedLocalBound(state.clusterMatrices);
}

View file

@ -48,6 +48,8 @@ class QScriptEngineDebugger;
static const QString NO_SCRIPT("");
static const int SCRIPT_FPS = 60;
static const int DEFAULT_MAX_ENTITY_PPS = 9000;
static const int DEFAULT_ENTITY_PPS_PER_SCRIPT = 900;
class CallbackData {
public:

View file

@ -114,6 +114,10 @@ static bool isWithin(float value, float corner, float size) {
return value >= corner && value <= corner + size;
}
bool AABox::contains(const Triangle& triangle) const {
return contains(triangle.v0) && contains(triangle.v1) && contains(triangle.v2);
}
bool AABox::contains(const glm::vec3& point) const {
return isWithin(point.x, _corner.x, _scale.x) &&
isWithin(point.y, _corner.y, _scale.y) &&
@ -622,3 +626,40 @@ void AABox::transform(const glm::mat4& matrix) {
_corner = newCenter - newDir;
_scale = newDir * 2.0f;
}
AABox AABox::getOctreeChild(OctreeChild child) const {
AABox result(*this); // self
switch (child) {
case topLeftNear:
result._corner.y += _scale.y / 2.0f;
break;
case topLeftFar:
result._corner.y += _scale.y / 2.0f;
result._corner.z += _scale.z / 2.0f;
break;
case topRightNear:
result._corner.y += _scale.y / 2.0f;
result._corner.x += _scale.x / 2.0f;
break;
case topRightFar:
result._corner.y += _scale.y / 2.0f;
result._corner.x += _scale.x / 2.0f;
result._corner.z += _scale.z / 2.0f;
break;
case bottomLeftNear:
// _corner = same as parent
break;
case bottomLeftFar:
result._corner.z += _scale.z / 2.0f;
break;
case bottomRightNear:
result._corner.x += _scale.x / 2.0f;
break;
case bottomRightFar:
result._corner.x += _scale.x / 2.0f;
result._corner.z += _scale.z / 2.0f;
break;
}
result._scale /= 2.0f; // everything is half the scale
return result;
}

View file

@ -20,6 +20,7 @@
#include <QDebug>
#include "BoxBase.h"
#include "GeometryUtil.h"
#include "StreamUtils.h"
class AACube;
@ -58,6 +59,7 @@ public:
const glm::vec3& getMinimumPoint() const { return _corner; }
glm::vec3 getMaximumPoint() const { return calcTopFarLeft(); }
bool contains(const Triangle& triangle) const;
bool contains(const glm::vec3& point) const;
bool contains(const AABox& otherBox) const;
bool touches(const AABox& otherBox) const;
@ -112,6 +114,19 @@ public:
void clear() { _corner = INFINITY_VECTOR; _scale = glm::vec3(0.0f); }
typedef enum {
topLeftNear,
topLeftFar,
topRightNear,
topRightFar,
bottomLeftNear,
bottomLeftFar,
bottomRightNear,
bottomRightFar
} OctreeChild;
AABox getOctreeChild(OctreeChild child) const; // returns the AABox of the would be octree child of this AABox
private:
glm::vec3 getClosestPointOnFace(const glm::vec3& point, BoxFace face) const;
glm::vec3 getClosestPointOnFace(const glm::vec4& origin, const glm::vec4& direction, BoxFace face) const;

View file

@ -77,3 +77,13 @@ float Interpolate::calculateFadeRatio(quint64 start) {
const float EASING_SCALE = 1.001f;
return std::min(EASING_SCALE * fadeRatio, 1.0f);
}
float Interpolate::easeInOutQuad(float lerpValue) {
assert(!((lerpValue < 0.0f) || (lerpValue > 1.0f)));
if (lerpValue < 0.5f) {
return (2.0f * lerpValue * lerpValue);
}
return (lerpValue*(4.0f - 2.0f * lerpValue) - 1.0f);
}

View file

@ -30,6 +30,9 @@ public:
static float simpleNonLinearBlend(float fraction);
static float calculateFadeRatio(quint64 start);
// Basic ease-in-ease-out function for smoothing values.
static float easeInOutQuad(float lerpValue);
};
#endif // hifi_Interpolate_h

View file

@ -33,11 +33,11 @@ void RunningMarker::startRunningMarker() {
_runningMarkerThread->setObjectName("Running Marker Thread");
_runningMarkerThread->start();
writeRunningMarkerFiler(); // write the first file, even before timer
writeRunningMarkerFile(); // write the first file, even before timer
_runningMarkerTimer = new QTimer();
QObject::connect(_runningMarkerTimer, &QTimer::timeout, [=](){
writeRunningMarkerFiler();
writeRunningMarkerFile();
});
_runningMarkerTimer->start(RUNNING_STATE_CHECK_IN_MSECS);
@ -53,7 +53,7 @@ RunningMarker::~RunningMarker() {
_runningMarkerThread->deleteLater();
}
void RunningMarker::writeRunningMarkerFiler() {
void RunningMarker::writeRunningMarkerFile() {
QFile runningMarkerFile(getFilePath());
// always write, even it it exists, so that it touches the files

View file

@ -27,10 +27,11 @@ public:
QString getFilePath();
static QString getMarkerFilePath(QString name);
protected:
void writeRunningMarkerFiler();
void writeRunningMarkerFile();
void deleteRunningMarkerFile();
private:
QObject* _parent { nullptr };
QString _name;
QThread* _runningMarkerThread { nullptr };

View file

@ -12,9 +12,11 @@
#include "GLMHelpers.h"
#include "TriangleSet.h"
void TriangleSet::insert(const Triangle& t) {
_triangles.push_back(t);
void TriangleSet::insert(const Triangle& t) {
_isBalanced = false;
_triangles.push_back(t);
_bounds += t.v0;
_bounds += t.v1;
_bounds += t.v2;
@ -23,39 +25,31 @@ void TriangleSet::insert(const Triangle& t) {
void TriangleSet::clear() {
_triangles.clear();
_bounds.clear();
_isBalanced = false;
_triangleOctree.clear();
}
// Determine of the given ray (origin/direction) in model space intersects with any triangles
// in the set. If an intersection occurs, the distance and surface normal will be provided.
bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const {
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) {
bool intersectedSomething = false;
float boxDistance = std::numeric_limits<float>::max();
float bestDistance = std::numeric_limits<float>::max();
// reset our distance to be the max possible, lower level tests will store best distance here
distance = std::numeric_limits<float>::max();
if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) {
if (precision) {
for (const auto& triangle : _triangles) {
float thisTriangleDistance;
if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) {
if (thisTriangleDistance < bestDistance) {
bestDistance = thisTriangleDistance;
intersectedSomething = true;
surfaceNormal = triangle.getNormal();
distance = bestDistance;
}
}
}
} else {
intersectedSomething = true;
distance = boxDistance;
}
if (!_isBalanced) {
balanceOctree();
}
return intersectedSomething;
}
int trianglesTouched = 0;
auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched);
#if WANT_DEBUGGING
if (precision) {
qDebug() << "trianglesTouched :" << trianglesTouched << "out of:" << _triangleOctree._population << "_triangles.size:" << _triangles.size();
}
#endif
return result;
}
bool TriangleSet::convexHullContains(const glm::vec3& point) const {
if (!_bounds.contains(point)) {
@ -74,3 +68,198 @@ bool TriangleSet::convexHullContains(const glm::vec3& point) const {
return insideMesh;
}
void TriangleSet::debugDump() {
qDebug() << __FUNCTION__;
qDebug() << "bounds:" << getBounds();
qDebug() << "triangles:" << size() << "at top level....";
qDebug() << "----- _triangleOctree -----";
_triangleOctree.debugDump();
}
void TriangleSet::balanceOctree() {
_triangleOctree.reset(_bounds, 0);
// insert all the triangles
for (size_t i = 0; i < _triangles.size(); i++) {
_triangleOctree.insert(i);
}
_isBalanced = true;
#if WANT_DEBUGGING
debugDump();
#endif
}
// Determine of the given ray (origin/direction) in model space intersects with any triangles
// in the set. If an intersection occurs, the distance and surface normal will be provided.
bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) {
bool intersectedSomething = false;
float boxDistance = distance;
float bestDistance = distance;
if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) {
// if our bounding box intersects at a distance greater than the current known
// best distance, than we can safely not check any of our triangles
if (boxDistance > bestDistance) {
return false;
}
if (precision) {
for (const auto& triangleIndex : _triangleIndices) {
const auto& triangle = _allTriangles[triangleIndex];
float thisTriangleDistance;
trianglesTouched++;
if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) {
if (thisTriangleDistance < bestDistance) {
bestDistance = thisTriangleDistance;
intersectedSomething = true;
surfaceNormal = triangle.getNormal();
distance = bestDistance;
}
}
}
} else {
intersectedSomething = true;
distance = boxDistance;
}
}
return intersectedSomething;
}
static const int MAX_DEPTH = 4; // for now
static const int MAX_CHILDREN = 8;
TriangleSet::TriangleOctreeCell::TriangleOctreeCell(std::vector<Triangle>& allTriangles, const AABox& bounds, int depth) :
_allTriangles(allTriangles)
{
reset(bounds, depth);
}
void TriangleSet::TriangleOctreeCell::clear() {
_population = 0;
_triangleIndices.clear();
_bounds.clear();
_children.clear();
}
void TriangleSet::TriangleOctreeCell::reset(const AABox& bounds, int depth) {
clear();
_bounds = bounds;
_depth = depth;
}
void TriangleSet::TriangleOctreeCell::debugDump() {
qDebug() << __FUNCTION__;
qDebug() << "bounds:" << getBounds();
qDebug() << "depth:" << _depth;
qDebug() << "population:" << _population << "this level or below"
<< " ---- triangleIndices:" << _triangleIndices.size() << "in this cell";
qDebug() << "child cells:" << _children.size();
if (_depth < MAX_DEPTH) {
int childNum = 0;
for (auto& child : _children) {
qDebug() << "child:" << childNum;
child.second.debugDump();
childNum++;
}
}
}
void TriangleSet::TriangleOctreeCell::insert(size_t triangleIndex) {
const Triangle& triangle = _allTriangles[triangleIndex];
_population++;
// if we're not yet at the max depth, then check which child the triangle fits in
if (_depth < MAX_DEPTH) {
for (int child = 0; child < MAX_CHILDREN; child++) {
AABox childBounds = getBounds().getOctreeChild((AABox::OctreeChild)child);
// if the child AABox would contain the triangle...
if (childBounds.contains(triangle)) {
// if the child cell doesn't yet exist, create it...
if (_children.find((AABox::OctreeChild)child) == _children.end()) {
_children.insert(
std::pair<AABox::OctreeChild, TriangleOctreeCell>
((AABox::OctreeChild)child, TriangleOctreeCell(_allTriangles, childBounds, _depth + 1)));
}
// insert the triangleIndex in the child cell
_children.at((AABox::OctreeChild)child).insert(triangleIndex);
return;
}
}
}
// either we're at max depth, or the triangle doesn't fit in one of our
// children and so we want to just record it here
_triangleIndices.push_back(triangleIndex);
}
bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) {
if (_population < 1) {
return false; // no triangles below here, so we can't intersect
}
float bestLocalDistance = distance;
BoxFace bestLocalFace;
glm::vec3 bestLocalNormal;
bool intersects = false;
// if the ray intersects our bounding box, then continue
if (getBounds().findRayIntersection(origin, direction, bestLocalDistance, bestLocalFace, bestLocalNormal)) {
// if the intersection with our bounding box, is greater than the current best distance (the distance passed in)
// then we know that none of our triangles can represent a better intersection and we can return
if (bestLocalDistance > distance) {
return false;
}
bestLocalDistance = distance;
float childDistance = distance;
BoxFace childFace;
glm::vec3 childNormal;
// if we're not yet at the max depth, then check which child the triangle fits in
if (_depth < MAX_DEPTH) {
for (auto& child : _children) {
// check each child, if there's an intersection, it will return some distance that we need
// to compare against the other results, because there might be multiple intersections and
// we will always choose the best (shortest) intersection
if (child.second.findRayIntersection(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) {
if (childDistance < bestLocalDistance) {
bestLocalDistance = childDistance;
bestLocalFace = childFace;
bestLocalNormal = childNormal;
intersects = true;
}
}
}
}
// also check our local triangle set
if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) {
if (childDistance < bestLocalDistance) {
bestLocalDistance = childDistance;
bestLocalFace = childFace;
bestLocalNormal = childNormal;
intersects = true;
}
}
}
if (intersects) {
distance = bestLocalDistance;
face = bestLocalFace;
surfaceNormal = bestLocalNormal;
}
return intersects;
}

View file

@ -15,19 +15,64 @@
#include "GeometryUtil.h"
class TriangleSet {
public:
void reserve(size_t size) { _triangles.reserve(size); } // reserve space in the datastructure for size number of triangles
size_t size() const { return _triangles.size(); }
const Triangle& getTriangle(size_t t) const { return _triangles[t]; }
class TriangleOctreeCell {
public:
TriangleOctreeCell(std::vector<Triangle>& allTriangles) :
_allTriangles(allTriangles)
{ }
void insert(size_t triangleIndex);
void reset(const AABox& bounds, int depth = 0);
void clear();
bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched);
const AABox& getBounds() const { return _bounds; }
void debugDump();
protected:
TriangleOctreeCell(std::vector<Triangle>& allTriangles, const AABox& bounds, int depth);
// checks our internal list of triangles
bool findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched);
std::vector<Triangle>& _allTriangles;
std::map<AABox::OctreeChild, TriangleOctreeCell> _children;
int _depth{ 0 };
int _population{ 0 };
AABox _bounds;
std::vector<size_t> _triangleIndices;
friend class TriangleSet;
};
public:
TriangleSet() :
_triangleOctree(_triangles)
{}
void debugDump();
void insert(const Triangle& t);
bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision);
void balanceOctree();
void reserve(size_t size) { _triangles.reserve(size); } // reserve space in the datastructure for size number of triangles
size_t size() const { return _triangles.size(); }
void clear();
// Determine if the given ray (origin/direction) in model space intersects with any triangles in the set. If an
// intersection occurs, the distance and surface normal will be provided.
bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const;
// note: this might side-effect internal structures
bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched);
// Determine if a point is "inside" all the triangles of a convex hull. It is the responsibility of the caller to
// determine that the triangle set is indeed a convex hull. If the triangles added to this set are not in fact a
@ -35,7 +80,10 @@ public:
bool convexHullContains(const glm::vec3& point) const;
const AABox& getBounds() const { return _bounds; }
private:
protected:
bool _isBalanced{ false };
TriangleOctreeCell _triangleOctree;
std::vector<Triangle> _triangles;
AABox _bounds;
};

View file

@ -40,7 +40,7 @@ void releaseOpenVrSystem();
static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b";
const quint64 CALIBRATION_TIMELAPSE = 2 * USECS_PER_SECOND;
const quint64 CALIBRATION_TIMELAPSE = 1 * USECS_PER_SECOND;
static const char* MENU_PARENT = "Avatar";
static const char* MENU_NAME = "Vive Controllers";

View file

@ -0,0 +1,131 @@
//
// rayPickingPerformance.js
// examples
//
// Created by Brad Hefta-Gaub on 5/13/2017
// 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 MIN_RANGE = -3;
var MAX_RANGE = 3;
var RANGE_DELTA = 0.5;
var OUTER_LOOPS = 10;
// NOTE: These expected results depend completely on the model, and the range settings above
var EXPECTED_TESTS = 1385 * OUTER_LOOPS;
var EXPECTED_INTERSECTIONS = 1286 * OUTER_LOOPS;
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation())));
var model_url = "http://hifi-content.s3.amazonaws.com/caitlyn/production/Scansite/buddhaReduced.fbx";
var rayPickOverlays = Array();
var modelEntity = Entities.addEntity({
type: "Model",
modelURL: model_url,
dimensions: {
x: 0.671,
y: 1.21,
z: 0.938
},
position: center
});
function rayCastTest() {
var tests = 0;
var intersections = 0;
var testStart = Date.now();
for (var t = 0; t < OUTER_LOOPS; t++) {
print("beginning loop:" + t);
for (var x = MIN_RANGE; x < MAX_RANGE; x += RANGE_DELTA) {
for (var y = MIN_RANGE; y < MAX_RANGE; y += RANGE_DELTA) {
for (var z = MIN_RANGE; z < MAX_RANGE; z += RANGE_DELTA) {
if ((x <= -2 || x >= 2) ||
(y <= -2 || y >= 2) ||
(z <= -2 || z >= 2)) {
tests++;
var origin = { x: center.x + x,
y: center.y + y,
z: center.z + z };
var direction = Vec3.subtract(center, origin);
var pickRay = {
origin: origin,
direction: direction
};
var pickResults = Entities.findRayIntersection(pickRay, true);
var color;
var visible;
if (pickResults.intersects && pickResults.entityID == modelEntity) {
intersections++;
color = {
red: 0,
green: 255,
blue: 0
};
visible = false;
} else {
/*
print("NO INTERSECTION?");
Vec3.print("origin:", origin);
Vec3.print("direction:", direction);
*/
color = {
red: 255,
green: 0,
blue: 0
};
visible = true;
}
var overlayID = Overlays.addOverlay("line3d", {
color: color,
alpha: 1,
visible: visible,
lineWidth: 2,
start: origin,
end: Vec3.sum(origin,Vec3.multiply(5,direction))
});
rayPickOverlays.push(overlayID);
}
}
}
}
print("ending loop:" + t);
}
var testEnd = Date.now();
var testElapsed = testEnd - testStart;
print("EXPECTED tests:" + EXPECTED_TESTS + " intersections:" + EXPECTED_INTERSECTIONS);
print("ACTUAL tests:" + tests + " intersections:" + intersections);
print("ELAPSED TIME:" + testElapsed + " ms");
}
function cleanup() {
Entities.deleteEntity(modelEntity);
rayPickOverlays.forEach(function(item){
Overlays.deleteOverlay(item);
});
}
Script.scriptEnding.connect(cleanup);
rayCastTest(); // run ray cast test immediately

View file

@ -309,7 +309,7 @@ var toolBar = (function () {
gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 }
});
}
}
}
}
function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
@ -482,22 +482,52 @@ var toolBar = (function () {
createNewEntity({
type: "ParticleEffect",
isEmitting: true,
emitterShouldTrail: true,
color: {
red: 200,
green: 200,
blue: 200
},
colorSpread: {
red: 0,
green: 0,
blue: 0
},
colorStart: {
red: 200,
green: 200,
blue: 200
},
colorFinish: {
red: 0,
green: 0,
blue: 0
},
emitAcceleration: {
x: 0,
y: -1,
z: 0
x: -0.5,
y: 2.5,
z: -0.5
},
accelerationSpread: {
x: 5,
y: 0,
z: 5
x: 0.5,
y: 1,
z: 0.5
},
emitSpeed: 1,
lifespan: 1,
particleRadius: 0.025,
emitRate: 5.5,
emitSpeed: 0,
speedSpread: 0,
lifespan: 1.5,
maxParticles: 10,
particleRadius: 0.25,
radiusStart: 0,
radiusFinish: 0.1,
radiusSpread: 0,
alpha: 0,
alphaStart: 1,
alphaFinish: 0,
emitRate: 100,
textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png"
polarStart: 0,
polarFinish: 0,
textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png"
});
});
@ -656,7 +686,7 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) {
return;
var data = JSON.parse(message);
if (data.method === "selectOverlay") {
print("setting selection to overlay " + data.overlayID);
var entity = entityIconOverlayManager.findEntity(data.overlayID);
@ -664,7 +694,7 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) {
if (entity !== null) {
selectionManager.setSelections([entity]);
}
}
}
}
Messages.subscribe("entityToolUpdates");
@ -774,7 +804,7 @@ function wasTabletClicked(event) {
var result = Overlays.findRayIntersection(rayPick, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]);
return result.intersects;
}
function mouseClickEvent(event) {
var wantDebug = false;
var result, properties, tabletClicked;
@ -784,7 +814,7 @@ function mouseClickEvent(event) {
if (tabletClicked) {
return;
}
if (result === null || result === undefined) {
if (!event.isShifted) {
selectionManager.clearSelections();
@ -2062,7 +2092,7 @@ function selectParticleEntity(entityID) {
selectedParticleEntity = entityID;
particleExplorerTool.setActiveParticleEntity(entityID);
particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData));
particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData));
// Switch to particle explorer
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");

View file

@ -359,7 +359,7 @@ function showUploadingMessage(selectedID, destination) {
shareBarHelp.classList.add("uploading");
shareBarHelp.setAttribute("data-destination", destination);
}
function hideUploadingMessageAndShare(selectedID, storyID) {
function hideUploadingMessageAndMaybeShare(selectedID, storyID) {
if (selectedID.id) {
selectedID = selectedID.id; // sometimes (?), `containerID` is passed as an HTML object to these functions; we just want the ID
}
@ -382,21 +382,28 @@ function hideUploadingMessageAndShare(selectedID, storyID) {
var facebookButton = document.getElementById(selectedID + "facebookButton");
window.open(facebookButton.getAttribute("href"), "_blank");
shareBarHelp.innerHTML = facebookShareText;
// This emitWebEvent() call isn't necessary in the "hifi" and "blast" cases
// because the "removeFromStoryIDsToMaybeDelete()" call happens
// in snapshot.js when sharing with that method.
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "removeFromStoryIDsToMaybeDelete",
story_id: storyID
}));
break;
case 'twitter':
var twitterButton = document.getElementById(selectedID + "twitterButton");
window.open(twitterButton.getAttribute("href"), "_blank");
shareBarHelp.innerHTML = twitterShareText;
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "removeFromStoryIDsToMaybeDelete",
story_id: storyID
}));
break;
}
shareBarHelp.setAttribute("data-destination", "");
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "removeFromStoryIDsToMaybeDelete",
story_id: storyID
}));
}
}
function updateShareInfo(containerID, storyID) {
@ -417,7 +424,7 @@ function updateShareInfo(containerID, storyID) {
twitterButton.setAttribute("target", "_blank");
twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelityinc&hashtags=VR,HiFi');
hideUploadingMessageAndShare(containerID, storyID);
hideUploadingMessageAndMaybeShare(containerID, storyID);
}
function blastToConnections(selectedID, isGif) {
if (selectedID.id) {
@ -552,6 +559,12 @@ function shareButtonClicked(destination, selectedID) {
if (!storyID) {
showUploadingMessage(selectedID, destination);
} else {
EventBridge.emitWebEvent(JSON.stringify({
type: "snapshot",
action: "removeFromStoryIDsToMaybeDelete",
story_id: storyID
}));
}
}

View file

@ -1020,7 +1020,7 @@ function loaded() {
elTextText.value = properties.text;
elTextLineHeight.value = properties.lineHeight.toFixed(4);
elTextFaceCamera = properties.faceCamera;
elTextFaceCamera.checked = properties.faceCamera;
elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + properties.textColor.green + "," + properties.textColor.blue + ")";
elTextTextColorRed.value = properties.textColor.red;
elTextTextColorGreen.value = properties.textColor.green;

View file

@ -14,6 +14,7 @@
var APP_NAME = "PLAYBACK",
HIFI_RECORDER_CHANNEL = "HiFi-Recorder-Channel",
RECORDER_COMMAND_ERROR = "error",
HIFI_PLAYER_CHANNEL = "HiFi-Player-Channel",
PLAYER_COMMAND_PLAY = "play",
PLAYER_COMMAND_STOP = "stop",
@ -47,9 +48,17 @@
searchState = SEARCH_IDLE,
otherPlayersPlaying,
otherPlayersPlayingCounts,
pauseCount;
pauseCount,
isDestroyLater = false,
destroy;
function onUpdateTimestamp() {
if (isDestroyLater) {
destroy();
return;
}
userData.timestamp = Date.now();
Entities.editEntity(entityID, { userData: JSON.stringify(userData) });
EntityViewer.queryOctree(); // Keep up to date ready for find().
@ -69,12 +78,14 @@
if (sender !== scriptUUID) {
message = JSON.parse(message);
index = otherPlayersPlaying.indexOf(message.entity);
if (index !== -1) {
otherPlayersPlayingCounts[index] += 1;
} else {
otherPlayersPlaying.push(message.entity);
otherPlayersPlayingCounts.push(1);
if (message.playing !== undefined) {
index = otherPlayersPlaying.indexOf(message.entity);
if (index !== -1) {
otherPlayersPlayingCounts[index] += 1;
} else {
otherPlayersPlaying.push(message.entity);
otherPlayersPlayingCounts.push(1);
}
}
}
}
@ -83,10 +94,11 @@
// Create a new persistence entity (even if already have one but that should never occur).
var properties;
log("Create recording " + filename);
log("Create recording entity for " + filename);
if (updateTimestampTimer !== null) {
Script.clearInterval(updateTimestampTimer); // Just in case.
if (updateTimestampTimer !== null) { // Just in case.
Script.clearInterval(updateTimestampTimer);
updateTimestampTimer = null;
}
searchState = SEARCH_IDLE;
@ -114,6 +126,7 @@
return true;
}
log("Could not create recording entity for " + filename);
return false;
}
@ -224,7 +237,7 @@
return result;
}
function destroy() {
destroy = function () {
// Delete current persistence entity.
if (entityID !== null) { // Just in case.
Entities.deleteEntity(entityID);
@ -233,7 +246,13 @@
}
if (updateTimestampTimer !== null) { // Just in case.
Script.clearInterval(updateTimestampTimer);
updateTimestampTimer = null;
}
};
function destroyLater() {
// Schedules a call to destroy() when timer threading suits.
isDestroyLater = true;
}
function setUp() {
@ -254,6 +273,7 @@
create: create,
find: find,
destroy: destroy,
destroyLater: destroyLater,
setUp: setUp,
tearDown: tearDown
};
@ -261,41 +281,78 @@
Player = (function () {
// Recording playback functions.
var isPlayingRecording = false,
var userID = null,
isPlayingRecording = false,
recordingFilename = "",
autoPlayTimer = null,
autoPlay,
playRecording;
function play(recording, position, orientation) {
function error(message) {
// Send error message to user.
Messages.sendMessage(HIFI_RECORDER_CHANNEL, JSON.stringify({
command: RECORDER_COMMAND_ERROR,
user: userID,
message: message
}));
}
function play(user, recording, position, orientation) {
var errorMessage;
if (autoPlayTimer) { // Cancel auto-play.
// FIXME: Once in a while Script.clearTimeout() fails.
// [DEBUG] [hifi.scriptengine] [3748] [agent] stopTimer -- not in _timerFunctionMap QObject(0x0)
Script.clearTimeout(autoPlayTimer);
autoPlayTimer = null;
}
userID = user;
if (Entity.create(recording, position, orientation)) {
log("Play new recording " + recordingFilename);
isPlayingRecording = true;
log("Play recording " + recording);
isPlayingRecording = true; // Immediate feedback.
recordingFilename = recording;
playRecording(recordingFilename, position, orientation);
playRecording(recordingFilename, position, orientation, true);
} else {
log("Could not create entity to play new recording " + recordingFilename);
errorMessage = "Could not persist recording " + recording.slice(4); // Remove leading "atp:".
log(errorMessage);
error(errorMessage);
autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_ERROR_INTERVAL); // Resume auto-play later.
}
}
function autoPlay() {
autoPlay = function () {
var recording,
AUTOPLAY_SEARCH_DELTA = 1000;
// Random delay to help reduce collisions between AC scripts.
Script.setTimeout(function () {
// Guard against Script.clearTimeout() in play() not always working.
if (isPlayingRecording) {
return;
}
recording = Entity.find();
if (recording) {
log("Play persisted recording " + recordingFilename);
playRecording(recording.recording, recording.position, recording.orientation);
log("Play persisted recording " + recording.recording);
userID = null;
autoPlayTimer = null;
isPlayingRecording = true; // Immediate feedback.
recordingFilename = recording.recording;
playRecording(recording.recording, recording.position, recording.orientation, false);
} else {
autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_SEARCH_INTERVAL); // Try again soon.
}
}, Math.random() * AUTOPLAY_SEARCH_DELTA);
}
};
playRecording = function (recording, position, orientation) {
playRecording = function (recording, position, orientation, isManual) {
Recording.loadRecording(recording, function (success) {
var errorMessage;
if (success) {
Users.disableIgnoreRadius();
@ -310,15 +367,22 @@
Recording.setPlayerLoop(true);
Recording.setPlayerUseSkeletonModel(true);
isPlayingRecording = true;
recordingFilename = recording;
Recording.setPlayerTime(0.0);
Recording.startPlaying();
UserActivityLogger.logAction("playRecordingAC_play_recording");
} else {
log("Failed to load recording " + recording);
if (isManual) {
// Delete persistence entity if manual play request.
Entity.destroyLater(); // Schedule for deletion; works around timer threading issues.
}
errorMessage = "Could not load recording " + recording.slice(4); // Remove leading "atp:".
log(errorMessage);
error(errorMessage);
isPlayingRecording = false;
recordingFilename = "";
autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_ERROR_INTERVAL); // Try again later.
}
});
@ -374,7 +438,15 @@
recording: Player.recording(),
entity: Entity.id()
}));
heartbeatTimer = Script.setTimeout(sendHeartbeat, HEARTBEAT_INTERVAL);
}
function onHeartbeatTimer() {
sendHeartbeat();
heartbeatTimer = Script.setTimeout(onHeartbeatTimer, HEARTBEAT_INTERVAL);
}
function startHeartbeat() {
onHeartbeatTimer();
}
function stopHeartbeat() {
@ -394,7 +466,7 @@
switch (message.command) {
case PLAYER_COMMAND_PLAY:
if (!Player.isPlaying()) {
Player.play(message.recording, message.position, message.orientation);
Player.play(sender, message.recording, message.position, message.orientation);
} else {
log("Didn't start playing " + message.recording + " because already playing " + Player.recording());
}
@ -418,7 +490,7 @@
Messages.subscribe(HIFI_PLAYER_CHANNEL);
Player.autoPlay();
sendHeartbeat();
startHeartbeat();
UserActivityLogger.logAction("playRecordingAC_script_load");
}

View file

@ -37,6 +37,12 @@
Window.alert(message);
}
function logDetails() {
return {
current_domain: location.placename
};
}
RecordingIndicator = (function () {
// Displays "recording" overlay.
@ -181,7 +187,7 @@
recordingState = IDLE;
log("Finish recording");
UserActivityLogger.logAction("record_finish_recording");
UserActivityLogger.logAction("record_finish_recording", logDetails());
playSound(finishRecordingSound);
Recording.stopRecording();
RecordingIndicator.hide();
@ -269,6 +275,7 @@
Player = (function () {
var HIFI_RECORDER_CHANNEL = "HiFi-Recorder-Channel",
RECORDER_COMMAND_ERROR = "error",
HIFI_PLAYER_CHANNEL = "HiFi-Player-Channel",
PLAYER_COMMAND_PLAY = "play",
PLAYER_COMMAND_STOP = "stop",
@ -277,7 +284,6 @@
playerIsPlayings = [], // True if AC player script is playing a recording.
playerRecordings = [], // Assignment client mappings of recordings being played.
playerTimestamps = [], // Timestamps of last heartbeat update from player script.
playerStartupTimeouts = [], // Timers that check that recording has started playing.
updateTimer,
UPDATE_INTERVAL = 5000; // Must be > player's HEARTBEAT_INTERVAL.
@ -298,7 +304,6 @@
playerIsPlayings.splice(i, 1);
playerRecordings.splice(i, 1);
playerTimestamps.splice(i, 1);
playerStartupTimeouts.splice(i, 1);
}
}
@ -309,8 +314,7 @@
}
function playRecording(recording, position, orientation) {
var index,
CHECK_PLAYING_TIMEOUT = 10000;
var index;
// Optional function parameters.
if (position === undefined) {
@ -334,26 +338,9 @@
position: position,
orientation: orientation
}));
playerStartupTimeouts[index] = Script.setTimeout(function () {
if ((!playerIsPlayings[index] || playerRecordings[index] !== recording) && playerStartupTimeouts[index]) {
error("Didn't start playing recording "
+ recording.slice(4) + "!"); // Remove leading "atp:" from recording.
}
playerStartupTimeouts[index] = null;
}, CHECK_PLAYING_TIMEOUT);
}
function stopPlayingRecording(playerID) {
var index;
// Cancel check that recording started playing.
index = playerIDs.indexOf(playerID);
if (index !== -1 && playerStartupTimeouts[index] !== null) {
// Cannot clearTimeout() without program log error, so just set null.
playerStartupTimeouts[index] = null;
}
Messages.sendMessage(HIFI_PLAYER_CHANNEL, JSON.stringify({
player: playerID,
command: PLAYER_COMMAND_STOP
@ -370,15 +357,21 @@
message = JSON.parse(message);
index = playerIDs.indexOf(sender);
if (index === -1) {
index = playerIDs.length;
playerIDs[index] = sender;
if (message.command === RECORDER_COMMAND_ERROR) {
if (message.user === MyAvatar.sessionUUID) {
error(message.message);
}
} else {
index = playerIDs.indexOf(sender);
if (index === -1) {
index = playerIDs.length;
playerIDs[index] = sender;
}
playerIsPlayings[index] = message.playing;
playerRecordings[index] = message.recording;
playerTimestamps[index] = Date.now();
Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs);
}
playerIsPlayings[index] = message.playing;
playerRecordings[index] = message.recording;
playerTimestamps[index] = Date.now();
Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs);
}
function reset() {
@ -386,7 +379,6 @@
playerIsPlayings = [];
playerRecordings = [];
playerTimestamps = [];
playerStartupTimeouts = [];
Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs);
}
@ -519,10 +511,11 @@
value: Player.numberOfPlayers()
}));
updateRecordingStatus(!Recorder.isIdle());
UserActivityLogger.logAction("record_open_dialog");
UserActivityLogger.logAction("record_open_dialog", logDetails());
break;
case STOP_PLAYING_RECORDING_ACTION:
// Stop the specified player.
log("Unload recording " + message.value);
Player.stopPlayingRecording(message.value);
break;
case LOAD_RECORDING_ACTION:
@ -530,7 +523,7 @@
recording = Window.browseAssets("Select Recording to Play", "recordings", "*.hfr");
if (recording) {
log("Load recording " + recording);
UserActivityLogger.logAction("record_load_recording");
UserActivityLogger.logAction("record_load_recording", logDetails());
Player.playRecording("atp:" + recording, MyAvatar.position, MyAvatar.orientation);
}
break;
@ -660,7 +653,7 @@
isConnected = Window.location.isConnected;
Script.update.connect(onUpdate);
UserActivityLogger.logAction("record_run_script");
UserActivityLogger.logAction("record_run_script", logDetails());
}
function tearDown() {

View file

@ -51,6 +51,11 @@ function openLoginWindow() {
}
}
function removeFromStoryIDsToMaybeDelete(story_id) {
storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(story_id), 1);
print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete));
}
function onMessage(message) {
// Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following:
// 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.)
@ -191,6 +196,7 @@ function onMessage(message) {
return;
} else {
print("SUCCESS uploading announcement story! Story ID:", response.user_story.id);
removeFromStoryIDsToMaybeDelete(message.story_id); // Don't delete original "for_url" story
}
});
}
@ -230,13 +236,13 @@ function onMessage(message) {
return;
} else {
print("SUCCESS changing audience" + (message.isAnnouncement ? " and posting announcement!" : "!"));
removeFromStoryIDsToMaybeDelete(message.story_id);
}
});
}
break;
case 'removeFromStoryIDsToMaybeDelete':
storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1);
print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete));
removeFromStoryIDsToMaybeDelete(message.story_id);
break;
default:
print('Unknown message action received by snapshot.js!');