diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index e13c9875e8..6e4a3df010 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -27,6 +27,9 @@ Original.CheckBox { property bool wrap: true; readonly property int checkSize: Math.max(boxSize - 8, 10) readonly property int checkRadius: 2 + property string labelFontFamily: "Raleway" + property int labelFontSize: 14; + property int labelFontWeight: Font.DemiBold; focusPolicy: Qt.ClickFocus hoverEnabled: true @@ -105,6 +108,9 @@ Original.CheckBox { contentItem: Label { text: checkBox.text color: checkBox.color + font.family: checkBox.labelFontFamily; + font.pixelSize: checkBox.labelFontSize; + font.weight: checkBox.labelFontWeight; x: 2 verticalAlignment: Text.AlignVCenter wrapMode: checkBox.wrap ? Text.Wrap : Text.NoWrap diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ecfd5a9bf7..ceaaffd89c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4196,7 +4196,7 @@ bool Application::acceptSnapshot(const QString& urlString) { QUrl url(urlString); QString snapshotPath = url.toLocalFile(); - SnapshotMetaData* snapshotData = Snapshot::parseSnapshotData(snapshotPath); + SnapshotMetaData* snapshotData = DependencyManager::get()->parseSnapshotData(snapshotPath); if (snapshotData) { if (!snapshotData->getURL().toString().isEmpty()) { DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); @@ -7600,7 +7600,7 @@ void Application::loadAvatarBrowser() const { void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { // Get a screenshot and save it - QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, + QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, TestScriptingInterface::getInstance()->getTestResultsLocation()); // If we're not doing an animated snapshot as well... @@ -7616,17 +7616,23 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa void Application::takeSecondaryCameraSnapshot(const QString& filename) { postLambdaEvent([filename, this] { - QString snapshotPath = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, + QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, TestScriptingInterface::getInstance()->getTestResultsLocation()); emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, true); }); } +void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename) { + postLambdaEvent([filename, cubemapOutputFormat, cameraPosition] { + DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, filename); + }); +} + void Application::shareSnapshot(const QString& path, const QUrl& href) { postLambdaEvent([path, href] { // not much to do here, everything is done in snapshot code... - Snapshot::uploadSnapshot(path, href); + DependencyManager::get()->uploadSnapshot(path, href); }); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 17e28f0e6e..23692f7072 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -282,6 +282,7 @@ public: void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f, const QString& filename = QString()); void takeSecondaryCameraSnapshot(const QString& filename = QString()); + void takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename = QString()); void shareSnapshot(const QString& filename, const QUrl& href = QUrl("")); diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index acde535d2b..db51cf99c8 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -9,15 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "SecondaryCamera.h" - -#include - -#include -#include -#include - #include "Application.h" +#include "SecondaryCamera.h" +#include +#include +#include using RenderArgsPointer = std::shared_ptr; @@ -38,7 +34,6 @@ public: using JobModel = render::Job::ModelO; SecondaryCameraJob() { _cachedArgsPointer = std::make_shared(_cachedArgs); - _entityScriptingInterface = DependencyManager::get(); _attachedEntityPropertyFlags += PROP_POSITION; _attachedEntityPropertyFlags += PROP_ROTATION; } @@ -60,12 +55,16 @@ public: qWarning() << "ERROR: Cannot set mirror projection for SecondaryCamera without an attachedEntityId set."; return; } - - EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_attachedEntityId, - _attachedEntityPropertyFlags); - glm::vec3 mirrorPropertiesPosition = entityProperties.getPosition(); - glm::quat mirrorPropertiesRotation = entityProperties.getRotation(); - glm::vec3 mirrorPropertiesDimensions = entityProperties.getDimensions(); + EntityItemPointer attachedEntity = qApp->getEntities()->getTree()->findEntityByID(_attachedEntityId); + + if (!attachedEntity) { + qWarning() << "ERROR: Cannot get EntityItemPointer for _attachedEntityId."; + return; + } + + glm::vec3 mirrorPropertiesPosition = attachedEntity->getWorldPosition(); + glm::quat mirrorPropertiesRotation = attachedEntity->getWorldOrientation(); + glm::vec3 mirrorPropertiesDimensions = attachedEntity->getScaledDimensions(); glm::vec3 halfMirrorPropertiesDimensions = 0.5f * mirrorPropertiesDimensions; // setup mirror from world as inverse of world from mirror transformation using inverted x and z for mirrored image @@ -120,10 +119,13 @@ public: setMirrorProjection(srcViewFrustum); } else { if (!_attachedEntityId.isNull()) { - EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_attachedEntityId, - _attachedEntityPropertyFlags); - srcViewFrustum.setPosition(entityProperties.getPosition()); - srcViewFrustum.setOrientation(entityProperties.getRotation()); + EntityItemPointer attachedEntity = qApp->getEntities()->getTree()->findEntityByID(_attachedEntityId); + if (!attachedEntity) { + qWarning() << "ERROR: Cannot get EntityItemPointer for _attachedEntityId."; + return; + } + srcViewFrustum.setPosition(attachedEntity->getWorldPosition()); + srcViewFrustum.setOrientation(attachedEntity->getWorldOrientation()); } else { srcViewFrustum.setPosition(_position); srcViewFrustum.setOrientation(_orientation); @@ -155,7 +157,6 @@ private: int _textureHeight; bool _mirrorProjection; EntityPropertyFlags _attachedEntityPropertyFlags; - QSharedPointer _entityScriptingInterface; }; void SecondaryCameraJobConfig::setPosition(glm::vec3 pos) { @@ -216,4 +217,4 @@ void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inp task.addJob("RenderDeferredTask", items); } task.addJob("EndSecondaryCamera", cachedArg); -} +} \ No newline at end of file diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 9c46f9e98a..e6904aa667 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -431,6 +431,10 @@ void WindowScriptingInterface::takeSecondaryCameraSnapshot(const QString& filena qApp->takeSecondaryCameraSnapshot(filename); } +void WindowScriptingInterface::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename) { + qApp->takeSecondaryCamera360Snapshot(cameraPosition, cubemapOutputFormat, filename); +} + void WindowScriptingInterface::shareSnapshot(const QString& path, const QUrl& href) { qApp->shareSnapshot(path, href); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 348882e0f8..27e00442bc 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -370,6 +370,18 @@ public slots: */ void takeSecondaryCameraSnapshot(const QString& filename = QString()); + /**jsdoc + * Takes a 360 snapshot given a position of the secondary camera (which does not need to have been previously set up). + * @function Window.takeSecondaryCameraSnapshot + * @param {vec3} [cameraPosition] - The (x, y, z) position of the camera for the 360 snapshot + * @param {string} [filename=""] - If this parameter is not given, the image will be saved as 'hifi-snap-by--YYYY-MM-DD_HH-MM-SS'. + * If this parameter is "" then the image will be saved as ".jpg". + * Otherwise, the image will be saved to this filename, with an appended ".jpg". + * + * var filename = QString(); + */ + void takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat = false, const QString& filename = QString()); + /**jsdoc * Emit a {@link Window.connectionAdded|connectionAdded} or a {@link Window.connectionError|connectionError} signal that * indicates whether or not a user connection was successfully made using the Web API. @@ -578,6 +590,16 @@ signals: */ void stillSnapshotTaken(const QString& pathStillSnapshot, bool notify); + /**jsdoc + * Triggered when a still equirectangular snapshot has been taken by calling {@link Window.takeSecondaryCamera360Snapshot|takeSecondaryCamera360Snapshot} + * @function Window.snapshot360Taken + * @param {string} pathStillSnapshot - The path and name of the snapshot image file. + * @param {boolean} notify - The value of the notify parameter that {@link Window.takeSecondaryCamera360Snapshot|takeSecondaryCamera360Snapshot} + * was called with. + * @returns {Signal} + */ + void snapshot360Taken(const QString& path360Snapshot, bool notify); + /**jsdoc * Triggered when a snapshot submitted via {@link Window.shareSnapshot|shareSnapshot} is ready for sharing. The snapshot * may then be shared via the {@link Account.metaverseServerURL} Web API. diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 4c233b986c..6bb35fde41 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -132,8 +132,8 @@ void setupPreferences() { // Snapshots static const QString SNAPSHOTS { "Snapshots" }; { - auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; - auto setter = [](const QString& value) { Snapshot::snapshotsLocation.set(value); emit DependencyManager::get()->snapshotLocationSet(value); }; + auto getter = []()->QString { return DependencyManager::get()->_snapshotsLocation.get(); }; + auto setter = [](const QString& value) { DependencyManager::get()->_snapshotsLocation.set(value); emit DependencyManager::get()->snapshotLocationSet(value); }; auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 39fef1d742..9fc6b480ca 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -21,7 +21,8 @@ #include #include #include -#include +#include +#include #include #include @@ -31,20 +32,38 @@ #include #include #include +#include +#include #include "Application.h" +#include "scripting/WindowScriptingInterface.h" +#include "MainWindow.h" +#include "Snapshot.h" #include "SnapshotUploader.h" +#include "ToneMappingEffect.h" // filename format: hifi-snap-by-%username%-on-%date%_%time%_@-%location%.jpg // %1 <= username, %2 <= date and time, %3 <= current location const QString FILENAME_PATH_FORMAT = "hifi-snap-by-%1-on-%2.jpg"; - const QString DATETIME_FORMAT = "yyyy-MM-dd_hh-mm-ss"; const QString SNAPSHOTS_DIRECTORY = "Snapshots"; - const QString URL = "highfidelity_url"; +static const int SNAPSHOT_360_TIMER_INTERVAL = 350; -Setting::Handle Snapshot::snapshotsLocation("snapshotsLocation"); +Snapshot::Snapshot() { + _snapshotTimer.setSingleShot(false); + _snapshotTimer.setTimerType(Qt::PreciseTimer); + _snapshotTimer.setInterval(SNAPSHOT_360_TIMER_INTERVAL); + connect(&_snapshotTimer, &QTimer::timeout, this, &Snapshot::takeNextSnapshot); + + _snapshotIndex = 0; + _oldEnabled = false; + _oldAttachedEntityId = 0; + _oldOrientation = 0; + _oldvFoV = 0; + _oldNearClipPlaneDistance = 0; + _oldFarClipPlaneDistance = 0; +} SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) { @@ -88,6 +107,224 @@ QString Snapshot::saveSnapshot(QImage image, const QString& filename, const QStr return snapshotPath; } +static const float CUBEMAP_SIDE_PIXEL_DIMENSION = 2048.0f; +static const float SNAPSHOT_360_FOV = 90.0f; +static const float SNAPSHOT_360_NEARCLIP = 0.3f; +static const float SNAPSHOT_360_FARCLIP = 16384.0f; +static const glm::quat CAMERA_ORIENTATION_DOWN(glm::quat(glm::radians(glm::vec3(-90.0f, 0.0f, 0.0f)))); +static const glm::quat CAMERA_ORIENTATION_FRONT(glm::quat(glm::radians(glm::vec3(0.0f, 0.0f, 0.0f)))); +static const glm::quat CAMERA_ORIENTATION_LEFT(glm::quat(glm::radians(glm::vec3(0.0f, 90.0f, 0.0f)))); +static const glm::quat CAMERA_ORIENTATION_BACK(glm::quat(glm::radians(glm::vec3(0.0f, 180.0f, 0.0f)))); +static const glm::quat CAMERA_ORIENTATION_RIGHT(glm::quat(glm::radians(glm::vec3(0.0f, 270.0f, 0.0f)))); +static const glm::quat CAMERA_ORIENTATION_UP(glm::quat(glm::radians(glm::vec3(90.0f, 0.0f, 0.0f)))); +void Snapshot::save360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename) { + _snapshotFilename = filename; + _cubemapOutputFormat = cubemapOutputFormat; + SecondaryCameraJobConfig* secondaryCameraRenderConfig = static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + + // Save initial values of secondary camera render config + _oldEnabled = secondaryCameraRenderConfig->isEnabled(); + _oldAttachedEntityId = secondaryCameraRenderConfig->property("attachedEntityId"); + _oldOrientation = secondaryCameraRenderConfig->property("orientation"); + _oldvFoV = secondaryCameraRenderConfig->property("vFoV"); + _oldNearClipPlaneDistance = secondaryCameraRenderConfig->property("nearClipPlaneDistance"); + _oldFarClipPlaneDistance = secondaryCameraRenderConfig->property("farClipPlaneDistance"); + + if (!_oldEnabled) { + secondaryCameraRenderConfig->enableSecondaryCameraRenderConfigs(true); + } + + // Initialize some secondary camera render config options for 360 snapshot capture + static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))->setCurve(0); + + secondaryCameraRenderConfig->resetSizeSpectatorCamera(static_cast(CUBEMAP_SIDE_PIXEL_DIMENSION), static_cast(CUBEMAP_SIDE_PIXEL_DIMENSION)); + secondaryCameraRenderConfig->setProperty("attachedEntityId", ""); + secondaryCameraRenderConfig->setPosition(cameraPosition); + secondaryCameraRenderConfig->setProperty("vFoV", SNAPSHOT_360_FOV); + secondaryCameraRenderConfig->setProperty("nearClipPlaneDistance", SNAPSHOT_360_NEARCLIP); + secondaryCameraRenderConfig->setProperty("farClipPlaneDistance", SNAPSHOT_360_FARCLIP); + + // Setup for Down Image capture + secondaryCameraRenderConfig->setOrientation(CAMERA_ORIENTATION_DOWN); + + _snapshotIndex = 0; + + _snapshotTimer.start(SNAPSHOT_360_TIMER_INTERVAL); +} + +void Snapshot::takeNextSnapshot() { + SecondaryCameraJobConfig* config = static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + + // Order is: + // 0. Down + // 1. Front + // 2. Left + // 3. Back + // 4. Right + // 5. Up + if (_snapshotIndex < 6) { + _imageArray[_snapshotIndex] = qApp->getActiveDisplayPlugin()->getSecondaryCameraScreenshot(); + } + + if (_snapshotIndex == 0) { + // Setup for Front Image capture + config->setOrientation(CAMERA_ORIENTATION_FRONT); + } else if (_snapshotIndex == 1) { + // Setup for Left Image capture + config->setOrientation(CAMERA_ORIENTATION_LEFT); + } else if (_snapshotIndex == 2) { + // Setup for Back Image capture + config->setOrientation(CAMERA_ORIENTATION_BACK); + } else if (_snapshotIndex == 3) { + // Setup for Right Image capture + config->setOrientation(CAMERA_ORIENTATION_RIGHT); + } else if (_snapshotIndex == 4) { + // Setup for Up Image capture + config->setOrientation(CAMERA_ORIENTATION_UP); + } else if (_snapshotIndex > 5) { + _snapshotTimer.stop(); + + // Reset secondary camera render config + static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))->setCurve(1); + config->resetSizeSpectatorCamera(qApp->getWindow()->geometry().width(), qApp->getWindow()->geometry().height()); + config->setProperty("attachedEntityId", _oldAttachedEntityId); + config->setProperty("vFoV", _oldvFoV); + config->setProperty("nearClipPlaneDistance", _oldNearClipPlaneDistance); + config->setProperty("farClipPlaneDistance", _oldFarClipPlaneDistance); + + if (!_oldEnabled) { + config->enableSecondaryCameraRenderConfigs(false); + } + + // Process six QImages + if (_cubemapOutputFormat) { + QtConcurrent::run([this]() { convertToCubemap(); }); + } else { + QtConcurrent::run([this]() { convertToEquirectangular(); }); + } + } + + _snapshotIndex++; +} + +void Snapshot::convertToCubemap() { + float outputImageHeight = CUBEMAP_SIDE_PIXEL_DIMENSION * 3.0f; + float outputImageWidth = CUBEMAP_SIDE_PIXEL_DIMENSION * 4.0f; + + QImage outputImage(outputImageWidth, outputImageHeight, QImage::Format_RGB32); + + QPainter painter(&outputImage); + QPoint destPos; + + // Paint DownImage + destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION, CUBEMAP_SIDE_PIXEL_DIMENSION * 2.0f); + painter.drawImage(destPos, _imageArray[0]); + + // Paint FrontImage + destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION, CUBEMAP_SIDE_PIXEL_DIMENSION); + painter.drawImage(destPos, _imageArray[1]); + + // Paint LeftImage + destPos = QPoint(0, CUBEMAP_SIDE_PIXEL_DIMENSION); + painter.drawImage(destPos, _imageArray[2]); + + // Paint BackImage + destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION * 3.0f, CUBEMAP_SIDE_PIXEL_DIMENSION); + painter.drawImage(destPos, _imageArray[3]); + + // Paint RightImage + destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION * 2.0f, CUBEMAP_SIDE_PIXEL_DIMENSION); + painter.drawImage(destPos, _imageArray[4]); + + // Paint UpImage + destPos = QPoint(CUBEMAP_SIDE_PIXEL_DIMENSION, 0); + painter.drawImage(destPos, _imageArray[5]); + + painter.end(); + + emit DependencyManager::get()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename), true); +} + +void Snapshot::convertToEquirectangular() { + // I got help from StackOverflow while writing this code: + // https://stackoverflow.com/questions/34250742/converting-a-cubemap-into-equirectangular-panorama + + int cubeFaceWidth = static_cast(CUBEMAP_SIDE_PIXEL_DIMENSION); + int cubeFaceHeight = static_cast(CUBEMAP_SIDE_PIXEL_DIMENSION); + float outputImageHeight = CUBEMAP_SIDE_PIXEL_DIMENSION * 2.0f; + float outputImageWidth = outputImageHeight * 2.0f; + QImage outputImage(outputImageWidth, outputImageHeight, QImage::Format_RGB32); + outputImage.fill(0); + QRgb sourceColorValue; + float phi, theta; + + for (int j = 0; j < outputImageHeight; j++) { + theta = (1.0f - ((float)j / outputImageHeight)) * PI; + + for (int i = 0; i < outputImageWidth; i++) { + phi = ((float)i / outputImageWidth) * 2.0f * PI; + + float x = glm::sin(phi) * glm::sin(theta) * -1.0f; + float y = glm::cos(theta); + float z = glm::cos(phi) * glm::sin(theta) * -1.0f; + + float a = std::max(std::max(std::abs(x), std::abs(y)), std::abs(z)); + + float xa = x / a; + float ya = y / a; + float za = z / a; + + // Pixel in the source images + int xPixel, yPixel; + QImage sourceImage; + + if (xa == 1) { + // Right image + xPixel = (int)((((za + 1.0f) / 2.0f) - 1.0f) * cubeFaceWidth); + yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight); + sourceImage = _imageArray[4]; + } else if (xa == -1) { + // Left image + xPixel = (int)((((za + 1.0f) / 2.0f)) * cubeFaceWidth); + yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight); + sourceImage = _imageArray[2]; + } else if (ya == 1) { + // Down image + xPixel = (int)((((xa + 1.0f) / 2.0f)) * cubeFaceWidth); + yPixel = (int)((((za + 1.0f) / 2.0f) - 1.0f) * cubeFaceHeight); + sourceImage = _imageArray[0]; + } else if (ya == -1) { + // Up image + xPixel = (int)((((xa + 1.0f) / 2.0f)) * cubeFaceWidth); + yPixel = (int)((((za + 1.0f) / 2.0f)) * cubeFaceHeight); + sourceImage = _imageArray[5]; + } else if (za == 1) { + // Front image + xPixel = (int)((((xa + 1.0f) / 2.0f)) * cubeFaceWidth); + yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight); + sourceImage = _imageArray[1]; + } else if (za == -1) { + // Back image + xPixel = (int)((((xa + 1.0f) / 2.0f) - 1.0f) * cubeFaceWidth); + yPixel = (int)((((ya + 1.0f) / 2.0f)) * cubeFaceHeight); + sourceImage = _imageArray[3]; + } else { + qDebug() << "Unknown face encountered when processing 360 Snapshot"; + xPixel = 0; + yPixel = 0; + } + + xPixel = std::min(std::abs(xPixel), 2047); + yPixel = std::min(std::abs(yPixel), 2047); + + sourceColorValue = sourceImage.pixel(xPixel, yPixel); + outputImage.setPixel(i, j, sourceColorValue); + } + } + + emit DependencyManager::get()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename), true); +} + QTemporaryFile* Snapshot::saveTempSnapshot(QImage image) { // return whatever we get back from saved file for snapshot return static_cast(savedFileForSnapshot(image, true)); @@ -123,12 +360,12 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary, const QSt if (!userSelectedPathname.isNull()) { snapshotFullPath = userSelectedPathname; } else { - snapshotFullPath = snapshotsLocation.get(); + snapshotFullPath = _snapshotsLocation.get(); } if (snapshotFullPath.isEmpty()) { snapshotFullPath = OffscreenUi::getExistingDirectory(nullptr, "Choose Snapshots Directory", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); - snapshotsLocation.set(snapshotFullPath); + _snapshotsLocation.set(snapshotFullPath); } if (!snapshotFullPath.isEmpty()) { // not cancelled @@ -210,9 +447,9 @@ void Snapshot::uploadSnapshot(const QString& filename, const QUrl& href) { } QString Snapshot::getSnapshotsLocation() { - return snapshotsLocation.get(""); + return _snapshotsLocation.get(""); } void Snapshot::setSnapshotsLocation(const QString& location) { - snapshotsLocation.set(location); + _snapshotsLocation.set(location); } diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 606313f3c3..ba9bd21cea 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include @@ -38,12 +40,14 @@ class Snapshot : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY public: - static QString saveSnapshot(QImage image, const QString& filename, const QString& pathname = QString()); - static QTemporaryFile* saveTempSnapshot(QImage image); - static SnapshotMetaData* parseSnapshotData(QString snapshotPath); + Snapshot(); + QString saveSnapshot(QImage image, const QString& filename, const QString& pathname = QString()); + void save360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename); + QTemporaryFile* saveTempSnapshot(QImage image); + SnapshotMetaData* parseSnapshotData(QString snapshotPath); - static Setting::Handle snapshotsLocation; - static void uploadSnapshot(const QString& filename, const QUrl& href = QUrl("")); + Setting::Handle _snapshotsLocation{ "snapshotsLocation" }; + void uploadSnapshot(const QString& filename, const QUrl& href = QUrl("")); signals: void snapshotLocationSet(const QString& value); @@ -51,11 +55,28 @@ signals: public slots: Q_INVOKABLE QString getSnapshotsLocation(); Q_INVOKABLE void setSnapshotsLocation(const QString& location); + +private slots: + void takeNextSnapshot(); + private: - static QFile* savedFileForSnapshot(QImage& image, + QFile* savedFileForSnapshot(QImage& image, bool isTemporary, const QString& userSelectedFilename = QString(), const QString& userSelectedPathname = QString()); + QString _snapshotFilename; + bool _cubemapOutputFormat; + QTimer _snapshotTimer; + qint16 _snapshotIndex; + bool _oldEnabled; + QVariant _oldAttachedEntityId; + QVariant _oldOrientation; + QVariant _oldvFoV; + QVariant _oldNearClipPlaneDistance; + QVariant _oldFarClipPlaneDistance; + QImage _imageArray[6]; + void convertToCubemap(); + void convertToEquirectangular(); }; #endif // hifi_Snapshot_h diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index ba37f6ee4e..a28f343ad3 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -672,6 +672,7 @@ Menu.menuItemEvent.connect(menuItemEvent); Window.domainConnectionRefused.connect(onDomainConnectionRefused); Window.stillSnapshotTaken.connect(onSnapshotTaken); + Window.snapshot360Taken.connect(onSnapshotTaken); Window.processingGifStarted.connect(processingGif); Window.connectionAdded.connect(connectionAdded); Window.connectionError.connect(connectionError); diff --git a/unpublishedScripts/marketplace/spectator-camera/Spectator Camera Marketplace Image.jpg b/unpublishedScripts/marketplace/spectator-camera/Spectator Camera Marketplace Image.jpg new file mode 100644 index 0000000000..19696f88fb Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/Spectator Camera Marketplace Image.jpg differ diff --git a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml new file mode 100644 index 0000000000..1b3698acd8 --- /dev/null +++ b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml @@ -0,0 +1,623 @@ +// +// SpectatorCamera.qml +// qml/hifi +// +// Spectator Camera v2.0 +// +// Created by Zach Fox on 2018-04-18 +// Copyright 2018 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 +// + +import Hifi 1.0 as Hifi +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 + +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit +import "qrc:////qml//controls" as HifiControls +import "qrc:////qml//hifi" as Hifi + +Rectangle { + HifiStylesUit.HifiConstants { id: hifi; } + + id: root; + property bool processing360Snapshot: false; + // Style + color: "#404040"; + + // The letterbox used for popup messages + Hifi.LetterboxMessage { + id: letterboxMessage; + z: 998; // Force the popup on top of everything else + } + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } + + // + // TITLE BAR START + // + Rectangle { + id: titleBarContainer; + // Size + width: root.width; + height: 60; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + color: "#121212"; + + // "Spectator" text + HifiStylesUit.RalewaySemiBold { + id: titleBarText; + text: "Spectator Camera"; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 30; + width: paintedWidth; + height: parent.height; + size: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + Switch { + id: masterSwitch; + focusPolicy: Qt.ClickFocus; + width: 65; + height: 30; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 30; + hoverEnabled: true; + + onHoveredChanged: { + if (hovered) { + switchHandle.color = hifi.colors.blueHighlight; + } else { + switchHandle.color = hifi.colors.lightGray; + } + } + + onClicked: { + sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); + sendToScript({method: 'updateCameravFoV', vFoV: fieldOfViewSlider.value}); + } + + background: Rectangle { + color: parent.checked ? "#1FC6A6" : hifi.colors.white; + implicitWidth: masterSwitch.switchWidth; + implicitHeight: masterSwitch.height; + radius: height/2; + } + + indicator: Rectangle { + id: switchHandle; + implicitWidth: masterSwitch.height - 4; + implicitHeight: implicitWidth; + radius: implicitWidth/2; + border.color: "#E3E3E3"; + color: "#404040"; + x: Math.max(4, Math.min(parent.width - width - 4, parent.visualPosition * parent.width - (width / 2) - 4)) + y: parent.height / 2 - height / 2; + Behavior on x { + enabled: !masterSwitch.down + SmoothedAnimation { velocity: 200 } + } + + } + } + } + // + // TITLE BAR END + // + + Rectangle { + z: 999; + id: processingSnapshot; + anchors.fill: parent; + visible: root.processing360Snapshot; + color: Qt.rgba(0.0, 0.0, 0.0, 0.85); + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup/section. + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + propagateComposedEvents: false; + } + + AnimatedImage { + id: processingImage; + source: "processing.gif" + width: 74; + height: width; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + } + + HifiStylesUit.RalewaySemiBold { + text: "Processing..."; + // Anchors + anchors.top: processingImage.bottom; + anchors.topMargin: 4; + anchors.horizontalCenter: parent.horizontalCenter; + width: paintedWidth; + // Text size + size: 26; + // Style + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + } + } + + // + // SPECTATOR CONTROLS START + // + Item { + id: spectatorControlsContainer; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + // Instructions or Preview + Rectangle { + id: spectatorCameraImageContainer; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.right: parent.right; + height: 250; + color: masterSwitch.checked ? "transparent" : "black"; + + AnimatedImage { + source: "static.gif" + visible: !masterSwitch.checked; + anchors.fill: parent; + opacity: 0.15; + } + + // Instructions (visible when display texture isn't set) + HifiStylesUit.FiraSansRegular { + id: spectatorCameraInstructions; + text: "Turn on Spectator Camera for a preview\nof " + (HMD.active ? "what your monitor shows." : "the camera's view."); + size: 16; + color: hifi.colors.white; + visible: !masterSwitch.checked; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // Spectator Camera Preview + Hifi.ResourceImageItem { + id: spectatorCameraPreview; + visible: masterSwitch.checked; + url: showCameraView.checked || !HMD.active ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; + ready: masterSwitch.checked; + mirrorVertically: true; + anchors.fill: parent; + onVisibleChanged: { + ready = masterSwitch.checked; + update(); + } + } + + Item { + visible: HMD.active; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 40; + + LinearGradient { + anchors.fill: parent; + start: Qt.point(0, 0); + end: Qt.point(0, height); + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.black } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0) } + } + } + + HifiStylesUit.HiFiGlyphs { + id: monitorShowsSwitchLabelGlyph; + text: hifi.glyphs.screen; + size: 32; + color: hifi.colors.white; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + } + HifiStylesUit.RalewayLight { + id: monitorShowsSwitchLabel; + text: "Monitor View:"; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: monitorShowsSwitchLabelGlyph.right; + anchors.leftMargin: 8; + size: 20; + width: paintedWidth; + height: parent.height; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + } + Item { + anchors.left: monitorShowsSwitchLabel.right; + anchors.leftMargin: 14; + anchors.right: parent.right; + anchors.rightMargin: 10; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + + HifiControlsUit.RadioButton { + id: showCameraView; + text: "Camera View"; + width: 125; + anchors.left: parent.left; + anchors.leftMargin: 10; + anchors.verticalCenter: parent.verticalCenter; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (showHmdPreview.checked) { + showHmdPreview.checked = false; + } + if (!showCameraView.checked && !showHmdPreview.checked) { + showCameraView.checked = true; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setMonitorShowsCameraView', params: true}); + } + } + } + + HifiControlsUit.RadioButton { + id: showHmdPreview; + text: "VR Preview"; + anchors.left: showCameraView.right; + anchors.leftMargin: 10; + width: 125; + anchors.verticalCenter: parent.verticalCenter; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (showCameraView.checked) { + showCameraView.checked = false; + } + if (!showCameraView.checked && !showHmdPreview.checked) { + showHmdPreview.checked = true; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setMonitorShowsCameraView', params: false}); + } + } + } + } + } + + HifiControlsUit.Button { + id: takeSnapshotButton; + enabled: masterSwitch.checked; + text: "SNAP PICTURE"; + colorScheme: hifi.colorSchemes.light; + color: hifi.buttons.white; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.right: take360SnapshotButton.left; + anchors.rightMargin: 12; + width: 135; + height: 35; + onClicked: { + sendToScript({method: 'takeSecondaryCameraSnapshot'}); + } + } + HifiControlsUit.Button { + id: take360SnapshotButton; + enabled: masterSwitch.checked; + text: "SNAP 360"; + colorScheme: hifi.colorSchemes.light; + color: hifi.buttons.white; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 12; + width: 135; + height: 35; + onClicked: { + root.processing360Snapshot = true; + sendToScript({method: 'takeSecondaryCamera360Snapshot'}); + } + } + } + + Item { + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.leftMargin: 26; + anchors.right: parent.right; + anchors.rightMargin: 26; + anchors.bottom: parent.bottom; + + Item { + id: fieldOfView; + visible: masterSwitch.checked; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 35; + + HifiStylesUit.RalewaySemiBold { + id: fieldOfViewLabel; + text: "Field of View (" + fieldOfViewSlider.value + "\u00B0): "; + size: 20; + color: hifi.colors.white; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: 172; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + + HifiControlsUit.Slider { + id: fieldOfViewSlider; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: resetvFoV.left; + anchors.rightMargin: 8; + anchors.left: fieldOfViewLabel.right; + anchors.leftMargin: 8; + colorScheme: hifi.colorSchemes.dark; + from: 10.0; + to: 120.0; + value: 45.0; + stepSize: 1; + + onValueChanged: { + sendToScript({method: 'updateCameravFoV', vFoV: value}); + } + onPressedChanged: { + if (!pressed) { + sendToScript({method: 'updateCameravFoV', vFoV: value}); + } + } + } + + HifiControlsUit.GlyphButton { + id: resetvFoV; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: -8; + height: parent.height - 8; + width: height; + glyph: hifi.glyphs.reload; + onClicked: { + fieldOfViewSlider.value = 45.0; + } + } + } + + Item { + visible: HMD.active; + anchors.top: fieldOfView.bottom; + anchors.topMargin: 18; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + HifiStylesUit.RalewaySemiBold { + id: shortcutsHeaderText; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + text: "Shortcuts"; + size: 20; + color: hifi.colors.white; + } + + // "Switch View From Controller" Checkbox + HifiControlsUit.CheckBox { + id: switchViewFromControllerCheckBox; + color: hifi.colors.white; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: shortcutsHeaderText.bottom; + anchors.topMargin: 8; + text: ""; + labelFontSize: 20; + labelFontWeight: Font.Normal; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); + } + } + + // "Take Snapshot" Checkbox + HifiControlsUit.CheckBox { + id: takeSnapshotFromControllerCheckBox; + color: hifi.colors.white; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: switchViewFromControllerCheckBox.bottom; + anchors.topMargin: 4; + text: ""; + labelFontSize: 20; + labelFontWeight: Font.Normal; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeTakeSnapshotFromControllerPreference', params: checked}); + } + } + } + + HifiControlsUit.Button { + text: "Change Snapshot Location"; + colorScheme: hifi.colorSchemes.dark; + color: hifi.buttons.none; + anchors.bottom: spectatorDescriptionContainer.top; + anchors.bottomMargin: 16; + anchors.left: parent.left; + anchors.right: parent.right; + height: 35; + onClicked: { + sendToScript({method: 'openSettings'}); + } + } + + Item { + id: spectatorDescriptionContainer; + // Size + height: childrenRect.height; + // Anchors + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 20; + + // "Spectator" app description text + HifiStylesUit.RalewayRegular { + id: spectatorDescriptionText; + text: "While you're using a VR headset, you can use this app to change what your monitor shows. " + + "Try it when streaming or recording video."; + // Text size + size: 20; + // Size + height: paintedHeight; + // Anchors + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + // Style + color: hifi.colors.white; + wrapMode: Text.Wrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Learn More" text + HifiStylesUit.RalewayRegular { + id: spectatorLearnMoreText; + text: "Learn More About Spectator"; + // Text size + size: 20; + // Size + width: paintedWidth; + height: paintedHeight; + // Anchors + anchors.top: spectatorDescriptionText.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.right: parent.right; + // Style + color: hifi.colors.blueAccent; + wrapMode: Text.WordWrap; + font.underline: true; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + letterbox(hifi.glyphs.question, + "Spectator Camera", + "By default, your monitor shows a preview of what you're seeing in VR. " + + "Using the Spectator Camera app, your monitor can display the view " + + "from a virtual hand-held camera - perfect for taking selfies or filming " + + "your friends!
" + + "

Streaming and Recording

" + + "We recommend OBS for streaming and recording the contents of your monitor to services like " + + "Twitch, YouTube Live, and Facebook Live.

" + + "To get started using OBS, click this link now. The page will open in an external browser:
" + + 'OBS Official Overview Guide

' + + 'Snapshots taken using Spectator Camera will be saved in your Snapshots Directory - change via Settings -> General.'); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.blueAccent; + } + } + } + } + } + // + // SPECTATOR CONTROLS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the SpectatorCamera JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from spectatorCamera.js. + // + function fromScript(message) { + switch (message.method) { + case 'updateSpectatorCameraCheckbox': + masterSwitch.checked = message.params; + break; + case 'updateMonitorShowsSwitch': + showCameraView.checked = message.params; + showHmdPreview.checked = !message.params; + break; + case 'updateControllerMappingCheckbox': + switchViewFromControllerCheckBox.checked = message.switchViewSetting; + switchViewFromControllerCheckBox.enabled = true; + takeSnapshotFromControllerCheckBox.checked = message.takeSnapshotSetting; + takeSnapshotFromControllerCheckBox.enabled = true; + + if (message.controller === "OculusTouch") { + switchViewFromControllerCheckBox.text = "Left Thumbstick: Switch Monitor View"; + takeSnapshotFromControllerCheckBox.text = "Right Thumbstick: Take Snapshot"; + } else if (message.controller === "Vive") { + switchViewFromControllerCheckBox.text = "Left Thumb Pad: Switch Monitor View"; + takeSnapshotFromControllerCheckBox.text = "Right Thumb Pad: Take Snapshot"; + } else { + switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; + switchViewFromControllerCheckBox.checked = true; + switchViewFromControllerCheckBox.enabled = false; + takeSnapshotFromControllerCheckBox.visible = false; + } + break; + case 'finishedProcessing360Snapshot': + root.processing360Snapshot = false; + break; + default: + console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/unpublishedScripts/marketplace/spectator-camera/processing.gif b/unpublishedScripts/marketplace/spectator-camera/processing.gif new file mode 100644 index 0000000000..0536bd1884 Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/processing.gif differ diff --git a/unpublishedScripts/marketplace/spectator-camera/snap.wav b/unpublishedScripts/marketplace/spectator-camera/snap.wav new file mode 100644 index 0000000000..e5b86c0c71 Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/snap.wav differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.app.json b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.app.json new file mode 100644 index 0000000000..e71c657581 --- /dev/null +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.app.json @@ -0,0 +1,4 @@ +{ + "scriptURL": "http://mpassets-staging.highfidelity.com/26156ea5-cdff-43c2-9581-d6b0fa5e00ef-v1/spectatorCamera.js", + "homeURL": "http://mpassets-staging.highfidelity.com/26156ea5-cdff-43c2-9581-d6b0fa5e00ef-v1/SpectatorCamera.qml" +} \ No newline at end of file diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js index 730a5808b8..e95c05aef9 100644 --- a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js @@ -46,7 +46,6 @@ // -Far clip plane distance // -viewFinderOverlay: The in-world overlay that displays the spectator camera's view. // -camera: The in-world entity that corresponds to the spectator camera. - // -cameraIsDynamic: "false" for now - maybe it shouldn't be? False means that the camera won't drift when you let go... // -cameraRotation: The rotation of the spectator camera. // -cameraPosition: The position of the spectator camera. // -glassPaneWidth: The width of the glass pane above the spectator camera that holds the viewFinderOverlay. @@ -56,7 +55,6 @@ var spectatorCameraConfig = Render.getConfig("SecondaryCamera"); var viewFinderOverlay = false; var camera = false; - var cameraIsDynamic = false; var cameraRotation; var cameraPosition; var glassPaneWidth = 0.16; @@ -70,11 +68,11 @@ spectatorCameraConfig.resetSizeSpectatorCamera(Window.innerWidth, Window.innerHeight); cameraRotation = Quat.multiply(MyAvatar.orientation, Quat.fromPitchYawRollDegrees(15, -155, 0)), cameraPosition = inFrontOf(0.85, Vec3.sum(MyAvatar.position, { x: 0, y: 0.28, z: 0 })); camera = Entities.addEntity({ - "angularDamping": 1, - "damping": 1, + "angularDamping": 0.95, + "damping": 0.95, "collidesWith": "static,dynamic,kinematic,", "collisionMask": 7, - "dynamic": cameraIsDynamic, + "dynamic": false, "modelURL": Script.resolvePath("spectator-camera.fbx"), "registrationPoint": { "x": 0.56, @@ -89,8 +87,12 @@ }, true); spectatorCameraConfig.attachedEntityId = camera; updateOverlay(); - setDisplay(monitorShowsCameraView); - // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (!HMD.active) { + setMonitorShowsCameraView(false); + } else { + setDisplay(monitorShowsCameraView); + } + // Change button to active when window is first opened OR if the camera is on, false otherwise. if (button) { button.editProperties({ isActive: onSpectatorCameraScreen || camera }); } @@ -150,17 +152,15 @@ // Relevant Variables: // -button: The tablet button. // -buttonName: The name of the button. - // -showSpectatorInDesktop: Set to "true" to show the "SPECTATOR" app in desktop mode. var button = false; var buttonName = "SPECTATOR"; - var showSpectatorInDesktop = false; - function addOrRemoveButton(isShuttingDown, isHMDMode) { + function addOrRemoveButton(isShuttingDown) { if (!tablet) { print("Warning in addOrRemoveButton(): 'tablet' undefined!"); return; } if (!button) { - if ((isHMDMode || showSpectatorInDesktop) && !isShuttingDown) { + if (!isShuttingDown) { button = tablet.addButton({ text: buttonName, icon: "icons/tablet-icons/spectator-i.svg", @@ -169,7 +169,7 @@ button.clicked.connect(onTabletButtonClicked); } } else if (button) { - if ((!isHMDMode && !showSpectatorInDesktop) || isShuttingDown) { + if (isShuttingDown) { button.clicked.disconnect(onTabletButtonClicked); tablet.removeButton(button); button = false; @@ -189,10 +189,12 @@ var tablet = null; function startup() { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - addOrRemoveButton(false, HMD.active); + addOrRemoveButton(false); tablet.screenChanged.connect(onTabletScreenChanged); Window.domainChanged.connect(onDomainChanged); Window.geometryChanged.connect(resizeViewFinderOverlay); + Window.stillSnapshotTaken.connect(onStillSnapshotTaken); + Window.snapshot360Taken.connect(on360SnapshotTaken); Controller.keyPressEvent.connect(keyPressEvent); HMD.displayModeChanged.connect(onHMDChanged); viewFinderOverlay = false; @@ -238,9 +240,7 @@ // 3. Camera is on; "Monitor Shows" is "HMD Preview": "url" is "" // 4. Camera is on; "Monitor Shows" is "Camera View": "url" is "resource://spectatorCameraFrame" function setDisplay(showCameraView) { - var url = (camera) ? (showCameraView ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame") : ""; - sendToQml({ method: 'showPreviewTextureNotInstructions', setting: !!url, url: url}); // FIXME: temporary hack to avoid setting the display texture to hmdPreviewFrame // until it is the correct mono. @@ -253,11 +253,8 @@ const MONITOR_SHOWS_CAMERA_VIEW_DEFAULT = false; var monitorShowsCameraView = !!Settings.getValue('spectatorCamera/monitorShowsCameraView', MONITOR_SHOWS_CAMERA_VIEW_DEFAULT); function setMonitorShowsCameraView(showCameraView) { - if (showCameraView === monitorShowsCameraView) { - return; - } - monitorShowsCameraView = showCameraView; setDisplay(showCameraView); + monitorShowsCameraView = showCameraView; Settings.setValue('spectatorCamera/monitorShowsCameraView', showCameraView); } function setMonitorShowsCameraViewAndSendToQml(showCameraView) { @@ -320,14 +317,14 @@ const SWITCH_VIEW_FROM_CONTROLLER_DEFAULT = false; var switchViewFromController = !!Settings.getValue('spectatorCamera/switchViewFromController', SWITCH_VIEW_FROM_CONTROLLER_DEFAULT); - function setControllerMappingStatus(status) { - if (!controllerMapping) { + function setSwitchViewControllerMappingStatus(status) { + if (!switchViewControllerMapping) { return; } if (status) { - controllerMapping.enable(); + switchViewControllerMapping.enable(); } else { - controllerMapping.disable(); + switchViewControllerMapping.disable(); } } function setSwitchViewFromController(setting) { @@ -335,21 +332,120 @@ return; } switchViewFromController = setting; - setControllerMappingStatus(switchViewFromController); + setSwitchViewControllerMappingStatus(switchViewFromController); Settings.setValue('spectatorCamera/switchViewFromController', setting); } + const TAKE_SNAPSHOT_FROM_CONTROLLER_DEFAULT = false; + var takeSnapshotFromController = !!Settings.getValue('spectatorCamera/takeSnapshotFromController', TAKE_SNAPSHOT_FROM_CONTROLLER_DEFAULT); + function setTakeSnapshotControllerMappingStatus(status) { + if (!takeSnapshotControllerMapping) { + return; + } + if (status) { + takeSnapshotControllerMapping.enable(); + } else { + takeSnapshotControllerMapping.disable(); + } + } + function setTakeSnapshotFromController(setting) { + if (setting === takeSnapshotFromController) { + return; + } + takeSnapshotFromController = setting; + setTakeSnapshotControllerMappingStatus(takeSnapshotFromController); + Settings.setValue('spectatorCamera/takeSnapshotFromController', setting); + } + // Function Name: registerButtonMappings() // // Description: // -Updates controller button mappings for Spectator Camera. // // Relevant Variables: - // -controllerMappingName: The name of the controller mapping. - // -controllerMapping: The controller mapping itself. + // -switchViewControllerMappingName: The name of the controller mapping. + // -switchViewControllerMapping: The controller mapping itself. + // -takeSnapshotControllerMappingName: The name of the controller mapping. + // -takeSnapshotControllerMapping: The controller mapping itself. // -controllerType: "OculusTouch", "Vive", "Other". - var controllerMappingName; - var controllerMapping; + var switchViewControllerMapping; + var switchViewControllerMappingName = 'Hifi-SpectatorCamera-Mapping-SwitchView'; + function registerSwitchViewControllerMapping() { + switchViewControllerMapping = Controller.newMapping(switchViewControllerMappingName); + if (controllerType === "OculusTouch") { + switchViewControllerMapping.from(Controller.Standard.LS).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } else if (controllerType === "Vive") { + switchViewControllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } + } + var takeSnapshotControllerMapping; + var takeSnapshotControllerMappingName = 'Hifi-SpectatorCamera-Mapping-TakeSnapshot'; + function onStillSnapshotTaken() { + Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 1; + } + function maybeTakeSnapshot() { + if (camera) { + Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 0; + // Wait a moment before taking the snapshot for the tonemapping curve to update + Script.setTimeout(function () { + Audio.playSound(SNAPSHOT_SOUND, { + position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, + localOnly: true, + volume: 1.0 + }); + Window.takeSecondaryCameraSnapshot(); + }, 250); + } + } + function on360SnapshotTaken() { + if (monitorShowsCameraView) { + setDisplay(true); + } + sendToQml({ + method: 'finishedProcessing360Snapshot' + }); + } + function maybeTake360Snapshot() { + if (camera) { + Audio.playSound(SNAPSHOT_SOUND, { + position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, + localOnly: true, + volume: 1.0 + }); + if (HMD.active && monitorShowsCameraView) { + setDisplay(false); + } + Window.takeSecondaryCamera360Snapshot(Entities.getEntityProperties(camera, ["positon"]).position); + } + } + function registerTakeSnapshotControllerMapping() { + takeSnapshotControllerMapping = Controller.newMapping(takeSnapshotControllerMappingName); + if (controllerType === "OculusTouch") { + takeSnapshotControllerMapping.from(Controller.Standard.RS).to(function (value) { + if (value === 1.0) { + maybeTakeSnapshot(); + } + return; + }); + } else if (controllerType === "Vive") { + takeSnapshotControllerMapping.from(Controller.Standard.RightPrimaryThumb).to(function (value) { + if (value === 1.0) { + maybeTakeSnapshot(); + } + return; + }); + } + } var controllerType = "Other"; function registerButtonMappings() { var VRDevices = Controller.getDeviceNames().toString(); @@ -359,30 +455,32 @@ } else if (VRDevices.indexOf("OculusTouch") !== -1) { controllerType = "OculusTouch"; } else { - sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + sendToQml({ + method: 'updateControllerMappingCheckbox', + switchViewSetting: switchViewFromController, + takeSnapshotSetting: takeSnapshotFromController, + controller: controllerType + }); return; // Neither Vive nor Touch detected } } - controllerMappingName = 'Hifi-SpectatorCamera-Mapping'; - controllerMapping = Controller.newMapping(controllerMappingName); - if (controllerType === "OculusTouch") { - controllerMapping.from(Controller.Standard.LS).to(function (value) { - if (value === 1.0) { - setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); - } - return; - }); - } else if (controllerType === "Vive") { - controllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) { - if (value === 1.0) { - setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); - } - return; - }); + if (!switchViewControllerMapping) { + registerSwitchViewControllerMapping(); } - setControllerMappingStatus(switchViewFromController); - sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + setSwitchViewControllerMappingStatus(switchViewFromController); + + if (!takeSnapshotControllerMapping) { + registerTakeSnapshotControllerMapping(); + } + setTakeSnapshotControllerMappingStatus(switchViewFromController); + + sendToQml({ + method: 'updateControllerMappingCheckbox', + switchViewSetting: switchViewFromController, + takeSnapshotSetting: takeSnapshotFromController, + controller: controllerType + }); } // Function Name: onTabletButtonClicked() @@ -393,7 +491,7 @@ // Relevant Variables: // -SPECTATOR_CAMERA_QML_SOURCE: The path to the SpectatorCamera QML // -onSpectatorCameraScreen: true/false depending on whether we're looking at the spectator camera app. - var SPECTATOR_CAMERA_QML_SOURCE = "hifi/SpectatorCamera.qml"; + var SPECTATOR_CAMERA_QML_SOURCE = Script.resolvePath("SpectatorCamera.qml"); var onSpectatorCameraScreen = false; function onTabletButtonClicked() { if (!tablet) { @@ -405,18 +503,26 @@ tablet.gotoHomeScreen(); } else { tablet.loadQMLSource(SPECTATOR_CAMERA_QML_SOURCE); - sendToQml({ method: 'updateSpectatorCameraCheckbox', params: !!camera }); - sendToQml({ method: 'updateMonitorShowsSwitch', params: monitorShowsCameraView }); - if (!controllerMapping) { - registerButtonMappings(); - } else { - sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); - } - Menu.setIsOptionChecked("Disable Preview", false); - Menu.setIsOptionChecked("Mono Preview", true); } } + function updateSpectatorCameraQML() { + sendToQml({ method: 'updateSpectatorCameraCheckbox', params: !!camera }); + sendToQml({ method: 'updateMonitorShowsSwitch', params: monitorShowsCameraView }); + if (!switchViewControllerMapping || !takeSnapshotControllerMapping) { + registerButtonMappings(); + } else { + sendToQml({ + method: 'updateControllerMappingCheckbox', + switchViewSetting: switchViewFromController, + takeSnapshotSetting: takeSnapshotFromController, + controller: controllerType + }); + } + Menu.setIsOptionChecked("Disable Preview", false); + Menu.setIsOptionChecked("Mono Preview", true); + } + // Function Name: onTabletScreenChanged() // // Description: @@ -429,6 +535,10 @@ if (button) { button.editProperties({ isActive: onSpectatorCameraScreen || camera }); } + + if (onSpectatorCameraScreen) { + updateSpectatorCameraQML(); + } } // Function Name: sendToQml() @@ -459,6 +569,26 @@ case 'changeSwitchViewFromControllerPreference': setSwitchViewFromController(message.params); break; + case 'changeTakeSnapshotFromControllerPreference': + setTakeSnapshotFromController(message.params); + break; + case 'updateCameravFoV': + spectatorCameraConfig.vFoV = message.vFoV; + break; + case 'takeSecondaryCameraSnapshot': + maybeTakeSnapshot(); + break; + case 'takeSecondaryCamera360Snapshot': + maybeTake360Snapshot(); + break; + case 'openSettings': + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { + Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); + } else { + tablet.pushOntoStack("hifi/tablet/TabletGeneralPreferences.qml"); + } + break; default: print('Unrecognized message from SpectatorCamera.qml:', JSON.stringify(message)); } @@ -469,13 +599,13 @@ // Description: // -Called from C++ when HMD mode is changed. The argument "isHMDMode" is true if HMD is on; false otherwise. function onHMDChanged(isHMDMode) { - if (!controllerMapping) { + if (!switchViewControllerMapping || !takeSnapshotControllerMapping) { registerButtonMappings(); } - setDisplay(monitorShowsCameraView); - addOrRemoveButton(false, isHMDMode); - if (!isHMDMode && !showSpectatorInDesktop) { - spectatorCameraOff(); + if (!isHMDMode) { + setMonitorShowsCameraView(false); + } else { + setDisplay(monitorShowsCameraView); } } @@ -487,7 +617,9 @@ spectatorCameraOff(); Window.domainChanged.disconnect(onDomainChanged); Window.geometryChanged.disconnect(resizeViewFinderOverlay); - addOrRemoveButton(true, HMD.active); + Window.stillSnapshotTaken.disconnect(onStillSnapshotTaken); + Window.snapshot360Taken.disconnect(on360SnapshotTaken); + addOrRemoveButton(true); if (tablet) { tablet.screenChanged.disconnect(onTabletScreenChanged); if (onSpectatorCameraScreen) { @@ -496,8 +628,11 @@ } HMD.displayModeChanged.disconnect(onHMDChanged); Controller.keyPressEvent.disconnect(keyPressEvent); - if (controllerMapping) { - controllerMapping.disable(); + if (switchViewControllerMapping) { + switchViewControllerMapping.disable(); + } + if (takeSnapshotControllerMapping) { + takeSnapshotControllerMapping.disable(); } } @@ -511,6 +646,7 @@ // These functions will be called when the script is loaded. var CAMERA_ON_SOUND = SoundCache.getSound(Script.resolvePath("cameraOn.wav")); + var SNAPSHOT_SOUND = SoundCache.getSound(Script.resourcesPath() + "sounds/snapshot/snap.wav"); startup(); Script.scriptEnding.connect(shutdown); diff --git a/unpublishedScripts/marketplace/spectator-camera/static.gif b/unpublishedScripts/marketplace/spectator-camera/static.gif new file mode 100644 index 0000000000..fbe46f48e6 Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/static.gif differ