Merge branch 'spectator-camera' of https://github.com/highfidelity/hifi into dk/spectatorCameraPreview

This commit is contained in:
David Kelly 2017-06-15 15:45:03 -07:00
commit f75cbfa087
30 changed files with 347 additions and 69 deletions

View file

@ -132,7 +132,7 @@ void AvatarMixer::start() {
auto start = usecTimestampNow(); auto start = usecTimestampNow();
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
std::for_each(cbegin, cend, [&](const SharedNodePointer& node) { std::for_each(cbegin, cend, [&](const SharedNodePointer& node) {
manageDisplayName(node); manageIdentityData(node);
++_sumListeners; ++_sumListeners;
}); });
}, &lockWait, &nodeTransform, &functor); }, &lockWait, &nodeTransform, &functor);
@ -183,8 +183,9 @@ void AvatarMixer::start() {
// NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData // NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData
// is guaranteed to not be accessed by other thread // is guaranteed to not be accessed by other thread
void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData()); AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
bool sendIdentity = false;
if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
AvatarData& avatar = nodeData->getAvatar(); AvatarData& avatar = nodeData->getAvatar();
const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); const QString& existingBaseDisplayName = nodeData->getBaseDisplayName();
@ -210,9 +211,39 @@ void AvatarMixer::manageDisplayName(const SharedNodePointer& node) {
soFar.second++; // refcount soFar.second++; // refcount
nodeData->flagIdentityChange(); nodeData->flagIdentityChange();
nodeData->setAvatarSessionDisplayNameMustChange(false); nodeData->setAvatarSessionDisplayNameMustChange(false);
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. sendIdentity = true;
qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID(); qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID();
} }
if (nodeData && nodeData->getAvatarSkeletonModelUrlMustChange()) { // never true for an empty _avatarWhitelist
nodeData->setAvatarSkeletonModelUrlMustChange(false);
AvatarData& avatar = nodeData->getAvatar();
static const QUrl emptyURL("");
QUrl url = avatar.cannonicalSkeletonModelURL(emptyURL);
if (!isAvatarInWhitelist(url)) {
qCDebug(avatars) << "Forbidden avatar" << nodeData->getNodeID() << avatar.getSkeletonModelURL() << "replaced with" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
avatar.setSkeletonModelURL(_replacementAvatar);
sendIdentity = true;
}
}
if (sendIdentity) {
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
}
}
bool AvatarMixer::isAvatarInWhitelist(const QUrl& url) {
// The avatar is in the whitelist if:
// 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND
// 2. The avatar's URL's path starts with the path of that same URL in the whitelist
for (const auto& whiteListedPrefix : _avatarWhitelist) {
auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix);
// check if this script URL matches the whitelist domain and, optionally, is beneath the path
if (url.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 &&
url.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) {
return true;
}
}
return false;
} }
void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) { void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) {
@ -402,13 +433,17 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity); AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
bool identityChanged = false; bool identityChanged = false;
bool displayNameChanged = false; bool displayNameChanged = false;
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged); bool skeletonModelUrlChanged = false;
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged, skeletonModelUrlChanged);
if (identityChanged) { if (identityChanged) {
QMutexLocker nodeDataLocker(&nodeData->getMutex()); QMutexLocker nodeDataLocker(&nodeData->getMutex());
nodeData->flagIdentityChange(); nodeData->flagIdentityChange();
if (displayNameChanged) { if (displayNameChanged) {
nodeData->setAvatarSessionDisplayNameMustChange(true); nodeData->setAvatarSessionDisplayNameMustChange(true);
} }
if (skeletonModelUrlChanged && !_avatarWhitelist.isEmpty()) {
nodeData->setAvatarSkeletonModelUrlMustChange(true);
}
} }
} }
} }
@ -764,4 +799,19 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale
<< "and a maximum avatar scale of" << _domainMaximumScale; << "and a maximum avatar scale of" << _domainMaximumScale;
const QString AVATAR_WHITELIST_DEFAULT{ "" };
static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist";
_avatarWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION].toString(AVATAR_WHITELIST_DEFAULT).split(',', QString::KeepEmptyParts);
static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar";
_replacementAvatar = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION].toString(REPLACEMENT_AVATAR_DEFAULT);
if ((_avatarWhitelist.count() == 1) && _avatarWhitelist[0].isEmpty()) {
_avatarWhitelist.clear(); // KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok).
}
if (_avatarWhitelist.isEmpty()) {
qCDebug(avatars) << "All avatars are allowed.";
} else {
qCDebug(avatars) << "Avatars other than" << _avatarWhitelist << "will be replaced by" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
}
} }

View file

@ -59,7 +59,12 @@ private:
void parseDomainServerSettings(const QJsonObject& domainSettings); void parseDomainServerSettings(const QJsonObject& domainSettings);
void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode); void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
void manageDisplayName(const SharedNodePointer& node); void manageIdentityData(const SharedNodePointer& node);
bool isAvatarInWhitelist(const QUrl& url);
const QString REPLACEMENT_AVATAR_DEFAULT{ "" };
QStringList _avatarWhitelist { };
QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT };
p_high_resolution_clock::time_point _lastFrameTimestamp; p_high_resolution_clock::time_point _lastFrameTimestamp;

View file

@ -65,6 +65,8 @@ public:
void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); } void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); }
bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; } bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; }
void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; } void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; }
bool getAvatarSkeletonModelUrlMustChange() const { return _avatarSkeletonModelUrlMustChange; }
void setAvatarSkeletonModelUrlMustChange(bool set = true) { _avatarSkeletonModelUrlMustChange = set; }
void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; } void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; }
void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; } void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; }
@ -146,6 +148,7 @@ private:
uint64_t _identityChangeTimestamp; uint64_t _identityChangeTimestamp;
bool _avatarSessionDisplayNameMustChange{ true }; bool _avatarSessionDisplayNameMustChange{ true };
bool _avatarSkeletonModelUrlMustChange{ false };
int _numAvatarsSentLastFrame = 0; int _numAvatarsSentLastFrame = 0;
int _numFramesSinceAdjustment = 0; int _numFramesSinceAdjustment = 0;

View file

@ -47,7 +47,7 @@ void OctreeInboundPacketProcessor::resetStats() {
_singleSenderStats.clear(); _singleSenderStats.clear();
} }
unsigned long OctreeInboundPacketProcessor::getMaxWait() const { uint32_t OctreeInboundPacketProcessor::getMaxWait() const {
// calculate time until next sendNackPackets() // calculate time until next sendNackPackets()
quint64 nextNackTime = _lastNackTime + TOO_LONG_SINCE_LAST_NACK; quint64 nextNackTime = _lastNackTime + TOO_LONG_SINCE_LAST_NACK;
quint64 now = usecTimestampNow(); quint64 now = usecTimestampNow();

View file

@ -80,7 +80,7 @@ protected:
virtual void processPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) override; virtual void processPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) override;
virtual unsigned long getMaxWait() const override; virtual uint32_t getMaxWait() const override;
virtual void preProcess() override; virtual void preProcess() override;
virtual void midProcess() override; virtual void midProcess() override;

View file

@ -866,6 +866,22 @@
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.", "help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.",
"placeholder": 3.0, "placeholder": 3.0,
"default": 3.0 "default": 3.0
},
{
"name": "avatar_whitelist",
"label": "Avatars Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "replacement_avatar",
"label": "Replacement Avatar for disallowed avatars",
"help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.",
"placeholder": "",
"default": "",
"advanced": true
} }
] ]
}, },

View file

@ -1723,6 +1723,10 @@ void Application::cleanupBeforeQuit() {
// Cleanup all overlays after the scripts, as scripts might add more // Cleanup all overlays after the scripts, as scripts might add more
_overlays.cleanupAllOverlays(); _overlays.cleanupAllOverlays();
// The cleanup process enqueues the transactions but does not process them. Calling this here will force the actual
// removal of the items.
// See https://highfidelity.fogbugz.com/f/cases/5328
_main3DScene->processTransactionQueue();
// first stop all timers directly or by invokeMethod // first stop all timers directly or by invokeMethod
// depending on what thread they run in // depending on what thread they run in
@ -5296,6 +5300,11 @@ void Application::nodeActivated(SharedNodePointer node) {
if (node->getType() == NodeType::AvatarMixer) { if (node->getType() == NodeType::AvatarMixer) {
// new avatar mixer, send off our identity packet on next update loop // new avatar mixer, send off our identity packet on next update loop
// Reset skeletonModelUrl if the last server modified our choice.
static const QUrl empty{};
if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->cannonicalSkeletonModelURL(empty)) {
getMyAvatar()->resetFullAvatarURL();
}
getMyAvatar()->markIdentityDataChanged(); getMyAvatar()->markIdentityDataChanged();
getMyAvatar()->resetLastSent(); getMyAvatar()->resetLastSent();
} }

View file

@ -42,6 +42,9 @@ void SecondaryCameraRenderTaskConfig::resetSizeSpectatorCamera(int width, int he
class BeginSecondaryCameraFrame { // Changes renderContext for our framebuffer and and view. class BeginSecondaryCameraFrame { // Changes renderContext for our framebuffer and and view.
glm::vec3 _position{}; glm::vec3 _position{};
glm::quat _orientation{}; glm::quat _orientation{};
float _vFoV{};
float _nearClipPlaneDistance{};
float _farClipPlaneDistance{};
public: public:
using Config = BeginSecondaryCameraFrameConfig; using Config = BeginSecondaryCameraFrameConfig;
using JobModel = render::Job::ModelO<BeginSecondaryCameraFrame, RenderArgsPointer, Config>; using JobModel = render::Job::ModelO<BeginSecondaryCameraFrame, RenderArgsPointer, Config>;
@ -53,6 +56,9 @@ public:
if (config.enabled || config.alwaysEnabled) { if (config.enabled || config.alwaysEnabled) {
_position = config.position; _position = config.position;
_orientation = config.orientation; _orientation = config.orientation;
_vFoV = config.vFoV;
_nearClipPlaneDistance = config.nearClipPlaneDistance;
_farClipPlaneDistance = config.farClipPlaneDistance;
} }
} }
@ -62,13 +68,14 @@ public:
gpu::FramebufferPointer destFramebuffer; gpu::FramebufferPointer destFramebuffer;
destFramebuffer = textureCache->getSpectatorCameraFramebuffer(); // FIXME: Change the destination based on some unimplemented config var destFramebuffer = textureCache->getSpectatorCameraFramebuffer(); // FIXME: Change the destination based on some unimplemented config var
if (destFramebuffer) { if (destFramebuffer) {
// Caching/restoring the old values doesn't seem to be needed. Is it because we happen to be last in the pipeline (which would be a bug waiting to happen)?
_cachedArgsPointer->_blitFramebuffer = args->_blitFramebuffer; _cachedArgsPointer->_blitFramebuffer = args->_blitFramebuffer;
_cachedArgsPointer->_viewport = args->_viewport; _cachedArgsPointer->_viewport = args->_viewport;
_cachedArgsPointer->_displayMode = args->_displayMode; _cachedArgsPointer->_displayMode = args->_displayMode;
_cachedArgsPointer->_renderMode = args->_renderMode;
args->_blitFramebuffer = destFramebuffer; args->_blitFramebuffer = destFramebuffer;
args->_viewport = glm::ivec4(0, 0, destFramebuffer->getWidth(), destFramebuffer->getHeight()); args->_viewport = glm::ivec4(0, 0, destFramebuffer->getWidth(), destFramebuffer->getHeight());
args->_displayMode = RenderArgs::MONO; args->_displayMode = RenderArgs::MONO;
args->_renderMode = RenderArgs::RenderMode::SECONDARY_CAMERA_RENDER_MODE;
gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { gpu::doInBatch(args->_context, [&](gpu::Batch& batch) {
batch.disableContextStereo(); batch.disableContextStereo();
@ -77,6 +84,10 @@ public:
auto srcViewFrustum = args->getViewFrustum(); auto srcViewFrustum = args->getViewFrustum();
srcViewFrustum.setPosition(_position); srcViewFrustum.setPosition(_position);
srcViewFrustum.setOrientation(_orientation); srcViewFrustum.setOrientation(_orientation);
srcViewFrustum.setProjection(glm::perspective(glm::radians(_vFoV), ((float)args->_viewport.z / (float)args->_viewport.w), _nearClipPlaneDistance, _farClipPlaneDistance));
// Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera,
// which is not what we want here.
srcViewFrustum.calculate();
args->pushViewFrustum(srcViewFrustum); args->pushViewFrustum(srcViewFrustum);
cachedArgs = _cachedArgsPointer; cachedArgs = _cachedArgsPointer;
} }
@ -97,6 +108,7 @@ public:
args->_viewport = cachedArgs->_viewport; args->_viewport = cachedArgs->_viewport;
args->popViewFrustum(); args->popViewFrustum();
args->_displayMode = cachedArgs->_displayMode; args->_displayMode = cachedArgs->_displayMode;
args->_renderMode = cachedArgs->_renderMode;
gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { gpu::doInBatch(args->_context, [&](gpu::Batch& batch) {
batch.restoreContextStereo(); batch.restoreContextStereo();

View file

@ -29,13 +29,19 @@ public:
void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred = true); void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred = true);
}; };
class BeginSecondaryCameraFrameConfig : public render::Task::Config { // Exposes view frustum position/orientation to javascript. class BeginSecondaryCameraFrameConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript.
Q_OBJECT Q_OBJECT
Q_PROPERTY(glm::vec3 position MEMBER position NOTIFY dirty) // of viewpoint to render from Q_PROPERTY(glm::vec3 position MEMBER position NOTIFY dirty) // of viewpoint to render from
Q_PROPERTY(glm::quat orientation MEMBER orientation NOTIFY dirty) // of viewpoint to render from Q_PROPERTY(glm::quat orientation MEMBER orientation NOTIFY dirty) // of viewpoint to render from
Q_PROPERTY(float vFoV MEMBER vFoV NOTIFY dirty) // Secondary camera's vertical field of view. In degrees.
Q_PROPERTY(float nearClipPlaneDistance MEMBER nearClipPlaneDistance NOTIFY dirty) // Secondary camera's near clip plane distance. In meters.
Q_PROPERTY(float farClipPlaneDistance MEMBER farClipPlaneDistance NOTIFY dirty) // Secondary camera's far clip plane distance. In meters.
public: public:
glm::vec3 position{}; glm::vec3 position{};
glm::quat orientation{}; glm::quat orientation{};
float vFoV{ 45.0f };
float nearClipPlaneDistance{ 0.1f };
float farClipPlaneDistance{ 100.0f };
BeginSecondaryCameraFrameConfig() : render::Task::Config(false) {} BeginSecondaryCameraFrameConfig() : render::Task::Config(false) {}
signals: signals:
void dirty(); void dirty();

View file

@ -1881,15 +1881,14 @@ void MyAvatar::preDisplaySide(RenderArgs* renderArgs) {
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.3f; const float RENDER_HEAD_CUTOFF_DISTANCE = 0.3f;
bool MyAvatar::cameraInsideHead() const { bool MyAvatar::cameraInsideHead(const glm::vec3& cameraPosition) const {
const glm::vec3 cameraPosition = qApp->getCamera().getPosition();
return glm::length(cameraPosition - getHeadPosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale()); return glm::length(cameraPosition - getHeadPosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale());
} }
bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
bool defaultMode = renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE; bool defaultMode = renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE;
bool firstPerson = qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON; bool firstPerson = qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON;
bool insideHead = cameraInsideHead(); bool insideHead = cameraInsideHead(renderArgs->getViewFrustum().getPosition());
return !defaultMode || !firstPerson || !insideHead; return !defaultMode || !firstPerson || !insideHead;
} }
@ -1961,6 +1960,32 @@ void MyAvatar::updateOrientation(float deltaTime) {
totalBodyYaw += (speedFactor * deltaAngle * (180.0f / PI)); totalBodyYaw += (speedFactor * deltaAngle * (180.0f / PI));
} }
// Use head/HMD roll to turn while walking or flying.
if (qApp->isHMDMode() && _hmdRollControlEnabled) {
// Turn with head roll.
const float MIN_CONTROL_SPEED = 0.01f;
float speed = glm::length(getVelocity());
if (speed >= MIN_CONTROL_SPEED) {
// Feather turn when stopping moving.
float speedFactor;
if (getDriveKey(TRANSLATE_Z) != 0.0f || _lastDrivenSpeed == 0.0f) {
_lastDrivenSpeed = speed;
speedFactor = 1.0f;
} else {
speedFactor = glm::min(speed / _lastDrivenSpeed, 1.0f);
}
float direction = glm::dot(getVelocity(), getRotation() * Vectors::UNIT_NEG_Z) > 0.0f ? 1.0f : -1.0f;
float rollAngle = glm::degrees(asinf(glm::dot(IDENTITY_UP, _hmdSensorOrientation * IDENTITY_RIGHT)));
float rollSign = rollAngle < 0.0f ? -1.0f : 1.0f;
rollAngle = fabsf(rollAngle);
rollAngle = rollAngle > _hmdRollControlDeadZone ? rollSign * (rollAngle - _hmdRollControlDeadZone) : 0.0f;
totalBodyYaw += speedFactor * direction * rollAngle * deltaTime * _hmdRollControlRate;
}
}
// update body orientation by movement inputs // update body orientation by movement inputs
glm::quat initialOrientation = getOrientationOutbound(); glm::quat initialOrientation = getOrientationOutbound();
setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f))));

View file

@ -132,6 +132,10 @@ class MyAvatar : public Avatar {
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)
Q_PROPERTY(bool hmdRollControlEnabled READ getHMDRollControlEnabled WRITE setHMDRollControlEnabled)
Q_PROPERTY(float hmdRollControlDeadZone READ getHMDRollControlDeadZone WRITE setHMDRollControlDeadZone)
Q_PROPERTY(float hmdRollControlRate READ getHMDRollControlRate WRITE setHMDRollControlRate)
public: public:
enum DriveKeys { enum DriveKeys {
TRANSLATE_X = 0, TRANSLATE_X = 0,
@ -337,6 +341,13 @@ public:
void setUseAdvancedMovementControls(bool useAdvancedMovementControls) void setUseAdvancedMovementControls(bool useAdvancedMovementControls)
{ _useAdvancedMovementControls.set(useAdvancedMovementControls); } { _useAdvancedMovementControls.set(useAdvancedMovementControls); }
void setHMDRollControlEnabled(bool value) { _hmdRollControlEnabled = value; }
bool getHMDRollControlEnabled() const { return _hmdRollControlEnabled; }
void setHMDRollControlDeadZone(float value) { _hmdRollControlDeadZone = value; }
float getHMDRollControlDeadZone() const { return _hmdRollControlDeadZone; }
void setHMDRollControlRate(float value) { _hmdRollControlRate = value; }
float getHMDRollControlRate() const { return _hmdRollControlRate; }
// get/set avatar data // get/set avatar data
void saveData(); void saveData();
void loadData(); void loadData();
@ -604,7 +615,7 @@ private:
float scale = 1.0f, bool isSoft = false, float scale = 1.0f, bool isSoft = false,
bool allowDuplicates = false, bool useSaved = true) override; bool allowDuplicates = false, bool useSaved = true) override;
bool cameraInsideHead() const; bool cameraInsideHead(const glm::vec3& cameraPosition) const;
void updateEyeContactTarget(float deltaTime); void updateEyeContactTarget(float deltaTime);
@ -687,6 +698,13 @@ private:
bool _useSnapTurn { true }; bool _useSnapTurn { true };
bool _clearOverlayWhenMoving { true }; bool _clearOverlayWhenMoving { true };
const float ROLL_CONTROL_DEAD_ZONE_DEFAULT = 8.0f; // deg
const float ROLL_CONTROL_RATE_DEFAULT = 2.5f; // deg/sec/deg
bool _hmdRollControlEnabled { true };
float _hmdRollControlDeadZone { ROLL_CONTROL_DEAD_ZONE_DEFAULT };
float _hmdRollControlRate { ROLL_CONTROL_RATE_DEFAULT };
float _lastDrivenSpeed { 0.0f };
// working copies -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access // working copies -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access
glm::mat4 _sensorToWorldMatrix { glm::mat4() }; glm::mat4 _sensorToWorldMatrix { glm::mat4() };

View file

@ -1504,7 +1504,7 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const {
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
} }
void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged) { void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, bool& skeletonModelUrlChanged) {
if (identity.sequenceId < _identitySequenceId) { if (identity.sequenceId < _identitySequenceId) {
qCDebug(avatars) << "Ignoring older identity packet for avatar" << getSessionUUID() qCDebug(avatars) << "Ignoring older identity packet for avatar" << getSessionUUID()
@ -1517,6 +1517,7 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) { if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
setSkeletonModelURL(identity.skeletonModelURL); setSkeletonModelURL(identity.skeletonModelURL);
identityChanged = true; identityChanged = true;
skeletonModelUrlChanged = true;
if (_firstSkeletonCheck) { if (_firstSkeletonCheck) {
displayNameChanged = true; displayNameChanged = true;
} }

View file

@ -368,6 +368,7 @@ public:
virtual ~AvatarData(); virtual ~AvatarData();
static const QUrl& defaultFullAvatarModelUrl(); static const QUrl& defaultFullAvatarModelUrl();
QUrl cannonicalSkeletonModelURL(const QUrl& empty) const;
virtual bool isMyAvatar() const { return false; } virtual bool isMyAvatar() const { return false; }
@ -536,9 +537,8 @@ public:
static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut);
// identityChanged returns true if identity has changed, false otherwise. // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange.
// displayNameChanged returns true if displayName has changed, false otherwise. void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, bool& skeletonModelUrlChanged);
void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged);
QByteArray identityByteArray() const; QByteArray identityByteArray() const;
@ -697,7 +697,6 @@ protected:
QVector<AttachmentData> _attachmentData; QVector<AttachmentData> _attachmentData;
QString _displayName; QString _displayName;
QString _sessionDisplayName { }; QString _sessionDisplayName { };
QUrl cannonicalSkeletonModelURL(const QUrl& empty) const;
QHash<QString, int> _jointIndices; ///< 1-based, since zero is returned for missing keys QHash<QString, int> _jointIndices; ///< 1-based, since zero is returned for missing keys
QStringList _jointNames; ///< in order of depth-first traversal QStringList _jointNames; ///< in order of depth-first traversal

View file

@ -148,8 +148,9 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); auto avatar = newOrExistingAvatar(identity.uuid, sendingNode);
bool identityChanged = false; bool identityChanged = false;
bool displayNameChanged = false; bool displayNameChanged = false;
bool skeletonModelUrlChanged = false;
// In this case, the "sendingNode" is the Avatar Mixer. // In this case, the "sendingNode" is the Avatar Mixer.
avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged); avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged, skeletonModelUrlChanged);
} }
} }

View file

@ -707,6 +707,7 @@ bool OpenGLDisplayPlugin::setDisplayTexture(const QString& name) {
// Note: it is the caller's responsibility to keep the network texture in cache. // Note: it is the caller's responsibility to keep the network texture in cache.
if (name.isEmpty()) { if (name.isEmpty()) {
_displayTexture.reset(); _displayTexture.reset();
onDisplayTextureReset();
return true; return true;
} }
auto textureCache = DependencyManager::get<TextureCache>(); auto textureCache = DependencyManager::get<TextureCache>();

View file

@ -58,6 +58,7 @@ public:
} }
virtual bool setDisplayTexture(const QString& name) override; virtual bool setDisplayTexture(const QString& name) override;
virtual bool onDisplayTextureReset() { return false; };
QImage getScreenshot(float aspectRatio = 0.0f) const override; QImage getScreenshot(float aspectRatio = 0.0f) const override;
float presentRate() const override; float presentRate() const override;

View file

@ -46,6 +46,8 @@ public:
float stutterRate() const override; float stutterRate() const override;
virtual bool onDisplayTextureReset() override { _clearPreviewFlag = true; return true; };
protected: protected:
virtual void hmdPresent() = 0; virtual void hmdPresent() = 0;
virtual bool isHmdMounted() const = 0; virtual bool isHmdMounted() const = 0;

View file

@ -119,7 +119,7 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo
size_t offset = _cameraUboSize * _cameras.size(); size_t offset = _cameraUboSize * _cameras.size();
_cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset)); _cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset));
if (stereo._enable) { if (stereo.isStereo()) {
#ifdef GPU_STEREO_CAMERA_BUFFER #ifdef GPU_STEREO_CAMERA_BUFFER
_cameras.push_back(CameraBufferElement(_camera.getEyeCamera(0, stereo, _view), _camera.getEyeCamera(1, stereo, _view))); _cameras.push_back(CameraBufferElement(_camera.getEyeCamera(0, stereo, _view), _camera.getEyeCamera(1, stereo, _view)));
#else #else
@ -151,7 +151,7 @@ void GLBackend::TransformStageState::update(size_t commandIndex, const StereoSta
#ifdef GPU_STEREO_CAMERA_BUFFER #ifdef GPU_STEREO_CAMERA_BUFFER
bindCurrentCamera(0); bindCurrentCamera(0);
#else #else
if (!stereo._enable) { if (!stereo.isStereo()) {
bindCurrentCamera(0); bindCurrentCamera(0);
} }
#endif #endif

View file

@ -584,7 +584,7 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
return matchingNode; return matchingNode;
} else { } else {
// we didn't have this node, so add them // we didn't have this node, so add them
Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret, this); Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret);
if (nodeType == NodeType::AudioMixer) { if (nodeType == NodeType::AudioMixer) {
LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer); LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer);
@ -617,24 +617,28 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
} }
// insert the new node and release our read lock // insert the new node and release our read lock
_nodeHash.insert(UUIDNodePair(newNode->getUUID(), newNodePointer)); _nodeHash.emplace(newNode->getUUID(), newNodePointer);
readLocker.unlock(); readLocker.unlock();
qCDebug(networking) << "Added" << *newNode; qCDebug(networking) << "Added" << *newNode;
auto weakPtr = newNodePointer.toWeakRef(); // We don't want the lambdas to hold a strong ref
emit nodeAdded(newNodePointer); emit nodeAdded(newNodePointer);
if (newNodePointer->getActiveSocket()) { if (newNodePointer->getActiveSocket()) {
emit nodeActivated(newNodePointer); emit nodeActivated(newNodePointer);
} else { } else {
connect(newNodePointer.data(), &NetworkPeer::socketActivated, this, [=] { connect(newNodePointer.data(), &NetworkPeer::socketActivated, this, [this, weakPtr] {
emit nodeActivated(newNodePointer); auto sharedPtr = weakPtr.lock();
disconnect(newNodePointer.data(), &NetworkPeer::socketActivated, this, 0); if (sharedPtr) {
emit nodeActivated(sharedPtr);
disconnect(sharedPtr.data(), &NetworkPeer::socketActivated, this, 0);
}
}); });
} }
// Signal when a socket changes, so we can start the hole punch over. // Signal when a socket changes, so we can start the hole punch over.
auto weakPtr = newNodePointer.toWeakRef(); // We don't want the lambda to hold a strong ref connect(newNodePointer.data(), &NetworkPeer::socketUpdated, this, [this, weakPtr] {
connect(newNodePointer.data(), &NetworkPeer::socketUpdated, this, [=] {
emit nodeSocketUpdated(weakPtr); emit nodeSocketUpdated(weakPtr);
}); });

View file

@ -40,7 +40,7 @@ public:
Node(const QUuid& uuid, NodeType_t type, Node(const QUuid& uuid, NodeType_t type,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(), const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(),
QObject* parent = 0); QObject* parent = nullptr);
bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; } bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; }
bool operator!=(const Node& otherNode) const { return !(*this == otherNode); } bool operator!=(const Node& otherNode) const { return !(*this == otherNode); }

View file

@ -20,6 +20,8 @@
class ReceivedPacketProcessor : public GenericThread { class ReceivedPacketProcessor : public GenericThread {
Q_OBJECT Q_OBJECT
public: public:
static const uint64_t MAX_WAIT_TIME { 100 }; // Max wait time in ms
ReceivedPacketProcessor(); ReceivedPacketProcessor();
/// Add packet from network receive thread to the processing queue. /// Add packet from network receive thread to the processing queue.
@ -63,8 +65,8 @@ protected:
/// Implements generic processing behavior for this thread. /// Implements generic processing behavior for this thread.
virtual bool process() override; virtual bool process() override;
/// Determines the timeout of the wait when there are no packets to process. Default value means no timeout /// Determines the timeout of the wait when there are no packets to process. Default value is 100ms to allow for regular event processing.
virtual unsigned long getMaxWait() const { return ULONG_MAX; } virtual uint32_t getMaxWait() const { return MAX_WAIT_TIME; }
/// Override to do work before the packets processing loop. Default does nothing. /// Override to do work before the packets processing loop. Default does nothing.
virtual void preProcess() { } virtual void preProcess() { }

View file

@ -35,14 +35,13 @@ void JurisdictionListener::nodeKilled(SharedNodePointer node) {
} }
bool JurisdictionListener::queueJurisdictionRequest() { bool JurisdictionListener::queueJurisdictionRequest() {
auto packet = NLPacket::create(PacketType::JurisdictionRequest, 0);
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
int nodeCount = 0; int nodeCount = 0;
nodeList->eachNode([&](const SharedNodePointer& node) { nodeList->eachNode([&](const SharedNodePointer& node) {
if (node->getType() == getNodeType() && node->getActiveSocket()) { if (node->getType() == getNodeType() && node->getActiveSocket()) {
auto packet = NLPacket::create(PacketType::JurisdictionRequest, 0);
_packetSender.queuePacketForSending(node, std::move(packet)); _packetSender.queuePacketForSending(node, std::move(packet));
nodeCount++; nodeCount++;
} }

View file

@ -41,8 +41,6 @@ bool JurisdictionSender::process() {
// call our ReceivedPacketProcessor base class process so we'll get any pending packets // call our ReceivedPacketProcessor base class process so we'll get any pending packets
if (continueProcessing && (continueProcessing = ReceivedPacketProcessor::process())) { if (continueProcessing && (continueProcessing = ReceivedPacketProcessor::process())) {
auto packet = (_jurisdictionMap) ? _jurisdictionMap->packIntoPacket()
: JurisdictionMap::packEmptyJurisdictionIntoMessage(getNodeType());
int nodeCount = 0; int nodeCount = 0;
lockRequestingNodes(); lockRequestingNodes();
@ -53,6 +51,8 @@ bool JurisdictionSender::process() {
SharedNodePointer node = DependencyManager::get<NodeList>()->nodeWithUUID(nodeUUID); SharedNodePointer node = DependencyManager::get<NodeList>()->nodeWithUUID(nodeUUID);
if (node && node->getActiveSocket()) { if (node && node->getActiveSocket()) {
auto packet = (_jurisdictionMap) ? _jurisdictionMap->packIntoPacket()
: JurisdictionMap::packEmptyJurisdictionIntoMessage(getNodeType());
_packetSender.queuePacketForSending(node, std::move(packet)); _packetSender.queuePacketForSending(node, std::move(packet));
nodeCount++; nodeCount++;
} }

View file

@ -29,7 +29,7 @@ void CauterizedMeshPartPayload::updateTransformForCauterizedMesh(
void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const {
// Still relying on the raw data from the model // Still relying on the raw data from the model
bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE); bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE && renderMode != RenderArgs::RenderMode::SECONDARY_CAMERA_RENDER_MODE);
if (useCauterizedMesh) { if (useCauterizedMesh) {
ModelPointer model = _model.lock(); ModelPointer model = _model.lock();
if (model) { if (model) {

View file

@ -10,6 +10,7 @@
// //
#include <QDebug> #include <QDebug>
#include <QtCore/QCoreApplication>
#include "GenericThread.h" #include "GenericThread.h"
@ -73,6 +74,7 @@ void GenericThread::threadRoutine() {
} }
while (!_stopThread) { while (!_stopThread) {
QCoreApplication::processEvents();
// override this function to do whatever your class actually does, return false to exit thread early // override this function to do whatever your class actually does, return false to exit thread early
if (!process()) { if (!process()) {

View file

@ -75,7 +75,7 @@ public:
class RenderArgs { class RenderArgs {
public: public:
enum RenderMode { DEFAULT_RENDER_MODE, SHADOW_RENDER_MODE, DIFFUSE_RENDER_MODE, NORMAL_RENDER_MODE, MIRROR_RENDER_MODE }; enum RenderMode { DEFAULT_RENDER_MODE, SHADOW_RENDER_MODE, DIFFUSE_RENDER_MODE, NORMAL_RENDER_MODE, MIRROR_RENDER_MODE, SECONDARY_CAMERA_RENDER_MODE };
enum DisplayMode { MONO, STEREO_MONITOR, STEREO_HMD }; enum DisplayMode { MONO, STEREO_MONITOR, STEREO_HMD };
enum DebugFlags { enum DebugFlags {
RENDER_DEBUG_NONE = 0, RENDER_DEBUG_NONE = 0,

View file

@ -0,0 +1,17 @@
//
// hmdRollControlDisable.js
//
// Created by David Rowe on 4 Jun 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 hmdRollControlEnabled = false;
//print("HMD roll control: " + hmdRollControlEnabled);
MyAvatar.hmdRollControlEnabled = hmdRollControlEnabled;
Script.stop();

View file

@ -0,0 +1,21 @@
//
// hmdRollControlEnable.js
//
// Created by David Rowe on 4 Jun 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 hmdRollControlEnabled = true;
var hmdRollControlDeadZone = 8.0; // deg
var hmdRollControlRate = 2.5; // deg/sec/deg
//print("HMD roll control: " + hmdRollControlEnabled + ", " + hmdRollControlDeadZone + ", " + hmdRollControlRate);
MyAvatar.hmdRollControlEnabled = hmdRollControlEnabled;
MyAvatar.hmdRollControlDeadZone = hmdRollControlDeadZone;
MyAvatar.hmdRollControlRate = hmdRollControlRate;
Script.stop();

View file

@ -17,12 +17,13 @@
// //
// FUNCTION VAR DECLARATIONS // FUNCTION VAR DECLARATIONS
// //
var sendToQml, onTabletScreenChanged, fromQml, onTabletButtonClicked, wireEventBridge, startup, shutdown; var sendToQml, addOrRemoveButton, onTabletScreenChanged, fromQml,
onTabletButtonClicked, wireEventBridge, startup, shutdown;
// //
// Function Name: inFrontOf(), flip() // Function Name: inFrontOf()
// //
// Description: // Description:
// Spectator camera utility functions and variables. // Spectator camera utility functions and variables.
@ -31,8 +32,6 @@
return Vec3.sum(position || MyAvatar.position, return Vec3.sum(position || MyAvatar.position,
Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation))); Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation)));
} }
var aroundY = Quat.fromPitchYawRollDegrees(0, 180, 0);
function flip(rotation) { return Quat.multiply(rotation, aroundY); }
// //
// Function Name: updateRenderFromCamera() // Function Name: updateRenderFromCamera()
@ -73,10 +72,6 @@
lastCameraPosition = cameraData.position; lastCameraPosition = cameraData.position;
beginSpectatorFrameRenderConfig.position = Vec3.sum(inFrontOf(0.17, lastCameraPosition, lastCameraRotation), {x: 0, y: 0.02, z: 0}); beginSpectatorFrameRenderConfig.position = Vec3.sum(inFrontOf(0.17, lastCameraPosition, lastCameraRotation), {x: 0, y: 0.02, z: 0});
} }
if (cameraIsDynamic) {
// BUG: image3d overlays don't retain their locations properly when parented to a dynamic object
Overlays.editOverlay(viewFinderOverlay, { orientation: flip(cameraData.rotation) });
}
} }
// //
@ -85,6 +80,11 @@
// Relevant Variables: // Relevant Variables:
// isUpdateRenderWired: Bool storing whether or not the camera's update // isUpdateRenderWired: Bool storing whether or not the camera's update
// function is wired. // function is wired.
// windowAspectRatio: The ratio of the Interface windows's sizeX/sizeY
// previewAspectRatio: The ratio of the camera preview's sizeX/sizeY
// vFoV: The vertical field of view of the spectator camera
// nearClipPlaneDistance: The near clip plane distance of the spectator camera
// farClipPlaneDistance: The far clip plane distance of the spectator camera
// //
// Arguments: // Arguments:
// None // None
@ -94,11 +94,22 @@
// spawn the camera entity. // spawn the camera entity.
// //
var isUpdateRenderWired = false; var isUpdateRenderWired = false;
var windowAspectRatio;
var previewAspectRatio = 16 / 9;
var vFoV = 45.0;
var nearClipPlaneDistance = 0.1;
var farClipPlaneDistance = 100.0;
function spectatorCameraOn() { function spectatorCameraOn() {
// Set the special texture size based on the window in which it will eventually be displayed. // Set the special texture size based on the window in which it will eventually be displayed.
var size = Controller.getViewportDimensions(); // FIXME: Need a signal to hook into when the dimensions change. var size = Controller.getViewportDimensions(); // FIXME: Need a signal to hook into when the dimensions change.
spectatorFrameRenderConfig.resetSizeSpectatorCamera(size.x, size.y); var sizeX = Window.innerWidth;
var sizeY = Window.innerHeight;
windowAspectRatio = sizeX/sizeY;
spectatorFrameRenderConfig.resetSizeSpectatorCamera(sizeX, sizeY);
spectatorFrameRenderConfig.enabled = beginSpectatorFrameRenderConfig.enabled = true; spectatorFrameRenderConfig.enabled = beginSpectatorFrameRenderConfig.enabled = true;
beginSpectatorFrameRenderConfig.vFoV = vFoV;
beginSpectatorFrameRenderConfig.nearClipPlaneDistance = nearClipPlaneDistance;
beginSpectatorFrameRenderConfig.farClipPlaneDistance = farClipPlaneDistance;
var cameraRotation = MyAvatar.orientation, cameraPosition = inFrontOf(1, Vec3.sum(MyAvatar.position, { x: 0, y: 0.3, z: 0 })); var cameraRotation = MyAvatar.orientation, cameraPosition = inFrontOf(1, Vec3.sum(MyAvatar.position, { x: 0, y: 0.3, z: 0 }));
Script.update.connect(updateRenderFromCamera); Script.update.connect(updateRenderFromCamera);
isUpdateRenderWired = true; isUpdateRenderWired = true;
@ -132,14 +143,17 @@
parentID: camera, parentID: camera,
alpha: 1, alpha: 1,
position: { x: 0.007, y: 0.15, z: -0.005 }, position: { x: 0.007, y: 0.15, z: -0.005 },
scale: -0.16, dimensions: { x: 0.16, y: -0.16 * windowAspectRatio / previewAspectRatio, z: 0 }
// Negative dimension for viewfinder is necessary for now due to the way Image3DOverlay
// draws textures.
// See Image3DOverlay.cpp:91. If you change the two lines there to:
// glm::vec2 topLeft(-x, -y);
// glm::vec2 bottomRight(x, y);
// the viewfinder will appear rightside up without this negative y-dimension.
// However, other Image3DOverlay textures (like the PAUSED one) will appear upside-down. *Why?*
// FIXME: This code will stretch the preview as the window aspect ratio changes. Fix that!
}); });
Entities.editEntity(camera, { position: cameraPosition, rotation: cameraRotation }); Entities.editEntity(camera, { position: cameraPosition, rotation: cameraRotation });
// FIXME: We shouldn't need the flip and the negative scale.
// e.g., This isn't necessary using an ordinary .jpg with lettering, above.
// Must be something about the view frustum projection matrix?
// But don't go changing that in (c++ code) without getting all the way to a desktop display!
Overlays.editOverlay(viewFinderOverlay, { orientation: flip(cameraRotation) });
setDisplay(monitorShowsCameraView); setDisplay(monitorShowsCameraView);
} }
@ -173,18 +187,50 @@
setDisplay(monitorShowsCameraView); setDisplay(monitorShowsCameraView);
} }
function onHMDChanged(isHMDMode) { //
// Will also eventually enable disable app, camera, etc. // Function Name: addOrRemoveButton()
setDisplay(monitorShowsCameraView); //
// Relevant Variables:
// button: The tablet button.
// buttonName: The name of the button.
// tablet: The tablet instance to be modified.
// showInDesktop: Set to "true" to show the "SPECTATOR" app in desktop mode
//
// Arguments:
// isShuttingDown: Set to "true" if you're calling this function upon script shutdown
// isHMDMode: "true" if user is in HMD; false otherwise
//
// Description:
// Used to add or remove the "SPECTATOR" app button from the HUD/tablet
//
var button = false;
var buttonName = "SPECTATOR";
var tablet = null;
var showSpectatorInDesktop = true;
function addOrRemoveButton(isShuttingDown, isHMDMode) {
if (!button) {
if ((isHMDMode || showSpectatorInDesktop) && !isShuttingDown) {
button = tablet.addButton({
text: buttonName
});
button.clicked.connect(onTabletButtonClicked);
}
} else if (button) {
if ((!showSpectatorInDesktop || isShuttingDown) && !isHMDMode) {
button.clicked.disconnect(onTabletButtonClicked);
tablet.removeButton(button);
button = false;
}
} else {
print("ERROR adding/removing Spectator button!");
}
} }
// //
// Function Name: startup() // Function Name: startup()
// //
// Relevant Variables: // Relevant Variables:
// button: The tablet button. // None
// buttonName: The name of the button.
// tablet: The tablet instance to be modified.
// //
// Arguments: // Arguments:
// None // None
@ -192,15 +238,9 @@
// Description: // Description:
// startup() will be called when the script is loaded. // startup() will be called when the script is loaded.
// //
var button;
var buttonName = "SPECTATOR";
var tablet = null;
function startup() { function startup() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
button = tablet.addButton({ addOrRemoveButton(false, HMD.active);
text: buttonName
});
button.clicked.connect(onTabletButtonClicked);
tablet.screenChanged.connect(onTabletScreenChanged); tablet.screenChanged.connect(onTabletScreenChanged);
Window.domainChanged.connect(spectatorCameraOff); Window.domainChanged.connect(spectatorCameraOff);
Controller.keyPressEvent.connect(keyPressEvent); Controller.keyPressEvent.connect(keyPressEvent);
@ -307,7 +347,9 @@
function onTabletScreenChanged(type, url) { function onTabletScreenChanged(type, url) {
wireEventBridge(shouldActivateButton); wireEventBridge(shouldActivateButton);
// for toolbar mode: change button to active when window is first openend, false otherwise. // for toolbar mode: change button to active when window is first openend, false otherwise.
button.editProperties({ isActive: shouldActivateButton }); if (button) {
button.editProperties({ isActive: shouldActivateButton });
}
shouldActivateButton = false; shouldActivateButton = false;
onSpectatorCameraScreen = false; onSpectatorCameraScreen = false;
} }
@ -361,6 +403,26 @@
} }
} }
//
// Function Name: onHMDChanged()
//
// Relevant Variables:
// None
//
// Arguments:
// isHMDMode: "true" if HMD is on; "false" otherwise
//
// Description:
// Called from C++ when HMD mode is changed
//
function onHMDChanged(isHMDMode) {
setDisplay(monitorShowsCameraView);
addOrRemoveButton(false, isHMDMode);
if (!isHMDMode && !showSpectatorInDesktop) {
spectatorCameraOff();
}
}
// //
// Function Name: shutdown() // Function Name: shutdown()
// //
@ -376,8 +438,7 @@
function shutdown() { function shutdown() {
spectatorCameraOff(); spectatorCameraOff();
Window.domainChanged.disconnect(spectatorCameraOff); Window.domainChanged.disconnect(spectatorCameraOff);
tablet.removeButton(button); addOrRemoveButton(true, HMD.active);
button.clicked.disconnect(onTabletButtonClicked);
tablet.screenChanged.disconnect(onTabletScreenChanged); tablet.screenChanged.disconnect(onTabletScreenChanged);
HMD.displayModeChanged.disconnect(onHMDChanged); HMD.displayModeChanged.disconnect(onHMDChanged);
Controller.keyPressEvent.disconnect(keyPressEvent); Controller.keyPressEvent.disconnect(keyPressEvent);

View file

@ -101,6 +101,10 @@ function getApplicationDataDirectory() {
return path.join(getRootHifiDataDirectory(), '/Server Console'); return path.join(getRootHifiDataDirectory(), '/Server Console');
} }
// Update lock filepath
const UPDATER_LOCK_FILENAME = ".updating";
const UPDATER_LOCK_FULL_PATH = getRootHifiDataDirectory() + "/" + UPDATER_LOCK_FILENAME;
// Configure log // Configure log
global.log = require('electron-log'); global.log = require('electron-log');
const logFile = getApplicationDataDirectory() + '/log.txt'; const logFile = getApplicationDataDirectory() + '/log.txt';
@ -630,11 +634,22 @@ function checkNewContent() {
userConfig.save(configPath); userConfig.save(configPath);
} }
}); });
} else if (fs.existsSync(UPDATER_LOCK_FULL_PATH)) {
backupResourceDirectoriesAndRestart();
} }
} }
}); });
} }
function removeIncompleteUpdate(acResourceDirectory, dsResourceDirectory) {
if (fs.existsSync(UPDATER_LOCK_FULL_PATH)) {
log.debug('Removing incomplete content update files before copying new update');
fs.emptyDirSync(dsResourceDirectory);
fs.emptyDirSync(acResourceDirectory);
} else {
fs.ensureFileSync(UPDATER_LOCK_FULL_PATH);
}
}
function maybeInstallDefaultContentSet(onComplete) { function maybeInstallDefaultContentSet(onComplete) {
// Check for existing data // Check for existing data
@ -673,7 +688,11 @@ function maybeInstallDefaultContentSet(onComplete) {
} }
log.debug("Found contentPath:" + argv.contentPath); log.debug("Found contentPath:" + argv.contentPath);
if (argv.contentPath) { if (argv.contentPath) {
// check if we're updating a data folder whose update is incomplete
removeIncompleteUpdate(acResourceDirectory, dsResourceDirectory);
fs.copy(argv.contentPath, getRootHifiDataDirectory(), function (err) { fs.copy(argv.contentPath, getRootHifiDataDirectory(), function (err) {
if (err) { if (err) {
log.debug('Could not copy home content: ' + err); log.debug('Could not copy home content: ' + err);
@ -682,12 +701,12 @@ function maybeInstallDefaultContentSet(onComplete) {
log.debug('Copied home content over to: ' + getRootHifiDataDirectory()); log.debug('Copied home content over to: ' + getRootHifiDataDirectory());
userConfig.set('homeContentLastModified', new Date()); userConfig.set('homeContentLastModified', new Date());
userConfig.save(configPath); userConfig.save(configPath);
fs.removeSync(UPDATER_LOCK_FULL_PATH);
onComplete(); onComplete();
}); });
return; return;
} }
// Show popup // Show popup
var window = new BrowserWindow({ var window = new BrowserWindow({
icon: appIcon, icon: appIcon,
@ -718,6 +737,9 @@ function maybeInstallDefaultContentSet(onComplete) {
var aborted = false; var aborted = false;
// check if we're updating a data folder whose update is incomplete
removeIncompleteUpdate(acResourceDirectory, dsResourceDirectory);
// Start downloading content set // Start downloading content set
var req = progress(request.get({ var req = progress(request.get({
url: HOME_CONTENT_URL url: HOME_CONTENT_URL
@ -763,6 +785,7 @@ function maybeInstallDefaultContentSet(onComplete) {
log.debug("Finished unarchiving home content set"); log.debug("Finished unarchiving home content set");
userConfig.set('homeContentLastModified', new Date()); userConfig.set('homeContentLastModified', new Date());
userConfig.save(configPath); userConfig.save(configPath);
fs.removeSync(UPDATER_LOCK_FULL_PATH);
sendStateUpdate('complete'); sendStateUpdate('complete');
}); });