diff --git a/interface/resources/icons/tablet-icons/spectator-a.svg b/interface/resources/icons/tablet-icons/spectator-a.svg new file mode 100644 index 0000000000..22ebde999b --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-a.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/spectator-i.svg b/interface/resources/icons/tablet-icons/spectator-i.svg new file mode 100644 index 0000000000..3e6c1a7dd9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-i.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/interface/resources/images/static.gif b/interface/resources/images/static.gif new file mode 100644 index 0000000000..fbe46f48e6 Binary files /dev/null and b/interface/resources/images/static.gif differ diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 36161e3c3e..b279b7ca8d 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -18,7 +18,7 @@ Original.CheckBox { id: checkBox property int colorScheme: hifi.colorSchemes.light - property string color: hifi.colors.lightGray + property string color: hifi.colors.lightGrayText readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property bool isRedCheck: false property int boxSize: 14 diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml new file mode 100644 index 0000000000..5a775221f6 --- /dev/null +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -0,0 +1,38 @@ +// +// Separator.qml +// +// Created by Zach Fox on 2017-06-06 +// 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 +// + +import QtQuick 2.5 +import "../styles-uit" + +Item { + // Size + height: 2; + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: height; + // Style + color: hifi.colors.baseGrayShadow; + } + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + // Style + color: hifi.colors.baseGrayHighlight; + } +} diff --git a/interface/resources/qml/controls-uit/Switch.qml b/interface/resources/qml/controls-uit/Switch.qml new file mode 100644 index 0000000000..d54f986717 --- /dev/null +++ b/interface/resources/qml/controls-uit/Switch.qml @@ -0,0 +1,156 @@ +// +// Switch.qml +// +// Created by Zach Fox on 2017-06-06 +// 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 as Original +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" + +Item { + id: rootSwitch; + + property int colorScheme: hifi.colorSchemes.light; + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light; + property int switchWidth: 70; + readonly property int switchRadius: height/2; + property string labelTextOff: ""; + property string labelGlyphOffText: ""; + property int labelGlyphOffSize: 32; + property string labelTextOn: ""; + property string labelGlyphOnText: ""; + property int labelGlyphOnSize: 32; + property alias checked: originalSwitch.checked; + signal onCheckedChanged; + signal clicked; + + Original.Switch { + id: originalSwitch; + activeFocusOnPress: true; + anchors.top: rootSwitch.top; + anchors.left: rootSwitch.left; + anchors.leftMargin: rootSwitch.width/2 - rootSwitch.switchWidth/2; + onCheckedChanged: rootSwitch.onCheckedChanged(); + onClicked: rootSwitch.clicked(); + + style: SwitchStyle { + + padding { + top: 3; + left: 3; + right: 3; + bottom: 3; + } + + groove: Rectangle { + color: "#252525"; + implicitWidth: rootSwitch.switchWidth; + implicitHeight: rootSwitch.height; + radius: rootSwitch.switchRadius; + } + + handle: Rectangle { + id: switchHandle; + implicitWidth: rootSwitch.height - padding.top - padding.bottom; + implicitHeight: implicitWidth; + radius: implicitWidth/2; + border.color: hifi.colors.lightGrayText; + color: hifi.colors.lightGray; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.lightGray; + } + } + } + } + + // OFF Label + Item { + anchors.right: originalSwitch.left; + anchors.rightMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOff; + text: labelTextOff; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF"; + anchors.top: parent.top; + anchors.right: parent.right; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOff; + text: labelGlyphOffText; + size: labelGlyphOffSize; + color: labelOff.color; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.right: labelOff.left; + anchors.rightMargin: 4; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelGlyphOff.left; + anchors.right: labelOff.right; + onClicked: { + originalSwitch.checked = false; + } + } + } + + // ON Label + Item { + anchors.left: originalSwitch.right; + anchors.leftMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOn; + text: labelTextOn; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText; + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOn; + text: labelGlyphOnText; + size: labelGlyphOnSize; + color: labelOn.color; + anchors.top: parent.top; + anchors.left: labelOn.right; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelOn.left; + anchors.right: labelGlyphOn.right; + onClicked: { + originalSwitch.checked = true; + } + } + } +} diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index 754876b2c1..fa9d7aa6f0 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -32,14 +32,15 @@ Item { radius: popupRadius } Rectangle { - width: Math.max(parent.width * 0.75, 400) + id: textContainer; + width: Math.max(parent.width * 0.8, 400) height: contentContainer.height + 50 anchors.centerIn: parent radius: popupRadius color: "white" Item { id: contentContainer - width: parent.width - 60 + width: parent.width - 50 height: childrenRect.height anchors.centerIn: parent Item { @@ -92,7 +93,7 @@ Item { anchors.top: parent.top anchors.topMargin: -20 anchors.right: parent.right - anchors.rightMargin: -25 + anchors.rightMargin: -20 MouseArea { anchors.fill: closeGlyphButton hoverEnabled: true @@ -127,11 +128,51 @@ Item { color: hifi.colors.darkGray wrapMode: Text.WordWrap textFormat: Text.StyledText + onLinkActivated: { + Qt.openUrlExternally(link) + } } } } + // Left gray MouseArea MouseArea { - anchors.fill: parent + anchors.left: parent.left; + anchors.right: textContainer.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Right gray MouseArea + MouseArea { + anchors.left: textContainer.left; + anchors.right: parent.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Top gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: textContainer.top; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Bottom gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: textContainer.bottom; + anchors.bottom: parent.bottom; acceptedButtons: Qt.LeftButton onClicked: { letterbox.visible = false diff --git a/interface/resources/qml/hifi/SpectatorCamera.qml b/interface/resources/qml/hifi/SpectatorCamera.qml new file mode 100644 index 0000000000..3a8559ab1e --- /dev/null +++ b/interface/resources/qml/hifi/SpectatorCamera.qml @@ -0,0 +1,374 @@ +// +// SpectatorCamera.qml +// qml/hifi +// +// Spectator Camera +// +// Created by Zach Fox on 2017-06-05 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../styles-uit" +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls + +// references HMD, XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: spectatorCamera; + // Style + color: hifi.colors.baseGray; + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage; + z: 999; // 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 + // + Item { + id: titleBarContainer; + // Size + width: spectatorCamera.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // "Spectator" text + RalewaySemiBold { + id: titleBarText; + text: "Spectator"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + // + // SPECTATOR APP DESCRIPTION START + // + Item { + id: spectatorDescriptionContainer; + // Size + width: spectatorCamera.width; + height: childrenRect.height; + // Anchors + anchors.left: parent.left; + anchors.top: titleBarContainer.bottom; + + // (i) Glyph + HiFiGlyphs { + id: spectatorDescriptionGlyph; + text: hifi.glyphs.info; + // Size + width: 20; + height: parent.height; + size: 60; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.top: parent.top; + anchors.topMargin: 0; + // Style + color: hifi.colors.lightGrayText; + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignTop; + } + + // "Spectator" app description text + RalewayLight { + id: spectatorDescriptionText; + text: "Spectator lets you change what your monitor displays while you're using a VR headset. Use Spectator when streaming and recording video."; + // Text size + size: 14; + // Size + width: 350; + height: paintedHeight; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: spectatorDescriptionGlyph.right; + anchors.leftMargin: 40; + // Style + color: hifi.colors.lightGrayText; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Learn More" text + RalewayRegular { + id: spectatorLearnMoreText; + text: "Learn More About Spectator"; + // Text size + size: 14; + // Size + width: paintedWidth; + height: paintedHeight; + // Anchors + anchors.top: spectatorDescriptionText.bottom; + anchors.topMargin: 10; + anchors.left: spectatorDescriptionText.anchors.left; + anchors.leftMargin: spectatorDescriptionText.anchors.leftMargin; + // 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'); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.blueAccent; + } + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: spectatorLearnMoreText.bottom; + anchors.topMargin: spectatorDescriptionText.anchors.topMargin; + } + } + // + // SPECTATOR APP DESCRIPTION END + // + + + // + // SPECTATOR CONTROLS START + // + Item { + id: spectatorControlsContainer; + // Size + height: spectatorCamera.height - spectatorDescriptionContainer.height - titleBarContainer.height; + // Anchors + anchors.top: spectatorDescriptionContainer.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 25; + anchors.right: parent.right; + anchors.rightMargin: anchors.leftMargin; + + // "Camera On" Checkbox + HifiControlsUit.CheckBox { + id: cameraToggleCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: parent.top; + text: "Spectator Camera On"; + boxSize: 24; + onClicked: { + sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); + spectatorCameraPreview.ready = checked; + } + } + + // Instructions or Preview + Rectangle { + id: spectatorCameraImageContainer; + anchors.left: parent.left; + anchors.top: cameraToggleCheckBox.bottom; + anchors.topMargin: 20; + anchors.right: parent.right; + height: 250; + color: cameraToggleCheckBox.checked ? "transparent" : "black"; + + AnimatedImage { + source: "../../images/static.gif" + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + opacity: 0.15; + } + + // Instructions (visible when display texture isn't set) + FiraSansRegular { + id: spectatorCameraInstructions; + text: "Turn on Spectator Camera for a preview\nof what your monitor shows."; + size: 16; + color: hifi.colors.lightGrayText; + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // Spectator Camera Preview + Hifi.ResourceImageItem { + id: spectatorCameraPreview; + visible: cameraToggleCheckbox.checked; + url: monitorShowsSwitch.checked ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; + ready: cameraToggleCheckBox.checked; + mirrorVertically: true; + anchors.fill: parent; + onVisibleChanged: { + ready = cameraToggleCheckBox.checked; + update(); + } + } + } + + + // "Monitor Shows" Switch Label Glyph + HiFiGlyphs { + id: monitorShowsSwitchLabelGlyph; + text: hifi.glyphs.screen; + size: 32; + color: hifi.colors.blueHighlight; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 13; + anchors.left: parent.left; + } + // "Monitor Shows" Switch Label + RalewayLight { + id: monitorShowsSwitchLabel; + text: "MONITOR SHOWS:"; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 20; + anchors.left: monitorShowsSwitchLabelGlyph.right; + anchors.leftMargin: 6; + size: 16; + width: paintedWidth; + height: paintedHeight; + color: hifi.colors.lightGrayText; + verticalAlignment: Text.AlignVCenter; + } + // "Monitor Shows" Switch + HifiControlsUit.Switch { + id: monitorShowsSwitch; + height: 30; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: monitorShowsSwitchLabel.bottom; + anchors.topMargin: 10; + labelTextOff: "HMD Preview"; + labelTextOn: "Camera View"; + labelGlyphOnText: hifi.glyphs.alert; + onCheckedChanged: { + sendToScript({method: 'setMonitorShowsCameraView', params: checked}); + } + } + + // "Switch View From Controller" Checkbox + HifiControlsUit.CheckBox { + id: switchViewFromControllerCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: monitorShowsSwitch.bottom; + anchors.topMargin: 25; + text: ""; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); + } + } + } + // + // 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': + cameraToggleCheckBox.checked = message.params; + break; + case 'updateMonitorShowsSwitch': + monitorShowsSwitch.checked = message.params; + break; + case 'updateControllerMappingCheckbox': + switchViewFromControllerCheckBox.checked = message.setting; + switchViewFromControllerCheckBox.enabled = true; + if (message.controller === "OculusTouch") { + switchViewFromControllerCheckBox.text = "Clicking Touch's Left Thumbstick Switches Monitor View"; + } else if (message.controller === "Vive") { + switchViewFromControllerCheckBox.text = "Clicking Left Thumb Pad Switches Monitor View"; + } else { + switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; + switchViewFromControllerCheckBox.checked = true; + switchViewFromControllerCheckBox.enabled = false; + } + break; + case 'showPreviewTextureNotInstructions': + console.log('showPreviewTextureNotInstructions recvd', JSON.stringify(message)); + spectatorCameraPreview.url = message.url; + spectatorCameraPreview.visible = message.setting; + break; + default: + console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index aa968c85ef..1556a9c0c0 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -50,7 +50,7 @@ Item { id: colors // Base colors - readonly property color baseGray: "#404040" + readonly property color baseGray: "#393939" readonly property color darkGray: "#121212" readonly property color baseGrayShadow: "#252525" readonly property color baseGrayHighlight: "#575757" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ddd1870723..a214068239 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -167,6 +167,7 @@ #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif +#include "ui/ResourceImageItem.h" #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" @@ -1003,7 +1004,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; } - + properties["first_run"] = firstRun.get(); // add the user's machine ID to the launch event @@ -1971,7 +1972,7 @@ void Application::initializeGL() { static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); _renderEngine->addJob("UpdateScene"); - _renderEngine->addJob("SecondaryCameraFrame", cullFunctor); + _renderEngine->addJob("SecondaryCameraJob", cullFunctor); _renderEngine->addJob("RenderMainView", cullFunctor, isDeferred); _renderEngine->load(); _renderEngine->registerScene(_main3DScene); @@ -2019,6 +2020,7 @@ void Application::initializeUi() { LoginDialog::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); + qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType("Hifi", 1, 0, "Preference"); auto offscreenUi = DependencyManager::get(); @@ -2749,7 +2751,7 @@ bool Application::event(QEvent* event) { idle(nsecsElapsed); postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); } - } + } return true; case Event::Paint: @@ -3725,8 +3727,8 @@ void updateCpuInformation() { // Update friendly structure auto& myCpuInfo = myCpuInfos[i]; myCpuInfo.update(cpuInfo); - PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { - { "kernel", myCpuInfo.kernelUsage }, + PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { + { "kernel", myCpuInfo.kernelUsage }, { "user", myCpuInfo.userUsage } }); } @@ -3793,7 +3795,7 @@ void getCpuUsage(vec3& systemAndUser) { void setupCpuMonitorThread() { initCpuUsage(); auto cpuMonitorThread = QThread::currentThread(); - + QTimer* timer = new QTimer(); timer->setInterval(50); QObject::connect(timer, &QTimer::timeout, [] { diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index f59d2fcc7a..56b8b3ef85 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -9,9 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "Application.h" #include "SecondaryCamera.h" #include #include +#include using RenderArgsPointer = std::shared_ptr; @@ -27,39 +29,32 @@ void MainRenderTask::build(JobModel& task, const render::Varying& inputs, render } } -void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" - bool wasEnabled = isEnabled(); - setEnabled(false); - auto textureCache = DependencyManager::get(); - textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg - setEnabled(wasEnabled); -} - -void SecondaryCameraRenderTaskConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. - resetSize(width, height); -} - -class BeginSecondaryCameraFrame { // Changes renderContext for our framebuffer and and view. +class SecondaryCameraJob { // Changes renderContext for our framebuffer and view. + QUuid _attachedEntityId{}; glm::vec3 _position{}; glm::quat _orientation{}; float _vFoV{}; float _nearClipPlaneDistance{}; float _farClipPlaneDistance{}; + EntityPropertyFlags _attachedEntityPropertyFlags; + QSharedPointer _entityScriptingInterface; public: - using Config = BeginSecondaryCameraFrameConfig; - using JobModel = render::Job::ModelO; - BeginSecondaryCameraFrame() { + using Config = SecondaryCameraJobConfig; + using JobModel = render::Job::ModelO; + SecondaryCameraJob() { _cachedArgsPointer = std::make_shared(_cachedArgs); + _entityScriptingInterface = DependencyManager::get(); + _attachedEntityPropertyFlags += PROP_POSITION; + _attachedEntityPropertyFlags += PROP_ROTATION; } void configure(const Config& config) { - if (config.enabled || config.alwaysEnabled) { - _position = config.position; - _orientation = config.orientation; - _vFoV = config.vFoV; - _nearClipPlaneDistance = config.nearClipPlaneDistance; - _farClipPlaneDistance = config.farClipPlaneDistance; - } + _attachedEntityId = config.attachedEntityId; + _position = config.position; + _orientation = config.orientation; + _vFoV = config.vFoV; + _nearClipPlaneDistance = config.nearClipPlaneDistance; + _farClipPlaneDistance = config.farClipPlaneDistance; } void run(const render::RenderContextPointer& renderContext, RenderArgsPointer& cachedArgs) { @@ -83,8 +78,14 @@ public: }); auto srcViewFrustum = args->getViewFrustum(); - srcViewFrustum.setPosition(_position); - srcViewFrustum.setOrientation(_orientation); + if (!_attachedEntityId.isNull()) { + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_attachedEntityId, _attachedEntityPropertyFlags); + srcViewFrustum.setPosition(entityProperties.getPosition()); + srcViewFrustum.setOrientation(entityProperties.getRotation()); + } else { + srcViewFrustum.setPosition(_position); + 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. @@ -99,6 +100,41 @@ protected: RenderArgsPointer _cachedArgsPointer; }; +void SecondaryCameraJobConfig::setPosition(glm::vec3 pos) { + if (attachedEntityId.isNull()) { + position = pos; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set position of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::setOrientation(glm::quat orient) { + if (attachedEntityId.isNull()) { + orientation = orient; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set orientation of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::enableSecondaryCameraRenderConfigs(bool enabled) { + qApp->getRenderEngine()->getConfiguration()->getConfig()->setEnabled(enabled); + setEnabled(enabled); +} + +void SecondaryCameraJobConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. + qApp->getRenderEngine()->getConfiguration()->getConfig()->resetSize(width, height); +} + +void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" + bool wasEnabled = isEnabled(); + setEnabled(false); + auto textureCache = DependencyManager::get(); + textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg + setEnabled(wasEnabled); +} + class EndSecondaryCameraFrame { // Restores renderContext. public: using JobModel = render::Job::ModelI; @@ -119,7 +155,7 @@ public: }; void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor) { - const auto cachedArg = task.addJob("BeginSecondaryCamera"); + const auto cachedArg = task.addJob("SecondaryCamera"); const auto items = task.addJob("FetchCullSort", cullFunctor); assert(items.canCast()); task.addJob("RenderDeferredTask", items); diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index 5ad19c9614..0941959c0a 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -28,34 +28,40 @@ public: void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred = true); }; -class BeginSecondaryCameraFrameConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. +class SecondaryCameraJobConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. Q_OBJECT - 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(QUuid attachedEntityId MEMBER attachedEntityId NOTIFY dirty) // entity whose properties define camera position and orientation + Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) // of viewpoint to render from + Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) // 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: + QUuid attachedEntityId{}; glm::vec3 position{}; glm::quat orientation{}; - float vFoV{ 45.0f }; - float nearClipPlaneDistance{ 0.1f }; - float farClipPlaneDistance{ 100.0f }; - BeginSecondaryCameraFrameConfig() : render::Task::Config(false) {} + float vFoV{ DEFAULT_FIELD_OF_VIEW_DEGREES }; + float nearClipPlaneDistance{ DEFAULT_NEAR_CLIP }; + float farClipPlaneDistance{ DEFAULT_FAR_CLIP }; + SecondaryCameraJobConfig() : render::Task::Config(false) {} signals: void dirty(); +public slots: + glm::vec3 getPosition() { return position; } + void setPosition(glm::vec3 pos); + glm::quat getOrientation() { return orientation; } + void setOrientation(glm::quat orient); + void enableSecondaryCameraRenderConfigs(bool enabled); + void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTaskConfig : public render::Task::Config { Q_OBJECT public: SecondaryCameraRenderTaskConfig() : render::Task::Config(false) {} -private: void resetSize(int width, int height); signals: void dirty(); -public slots: - void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTask { diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp new file mode 100644 index 0000000000..7b9592fa4c --- /dev/null +++ b/interface/src/ui/ResourceImageItem.cpp @@ -0,0 +1,114 @@ +// +// ResourceImageItem.cpp +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// 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 +// + +//#include "Application.h" +#include "ResourceImageItem.h" + +#include +#include +#include +#include + +ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() { + auto textureCache = DependencyManager::get(); + connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update())); +} + +void ResourceImageItem::setUrl(const QString& url) { + if (url != m_url) { + m_url = url; + update(); + } +} + +void ResourceImageItem::setReady(bool ready) { + if (ready != m_ready) { + m_ready = ready; + update(); + } +} + +void ResourceImageItemRenderer::onUpdateTimer() { + if (_ready) { + if (_networkTexture && _networkTexture->isLoaded()) { + if(_fboMutex.tryLock()) { + invalidateFramebufferObject(); + qApp->getActiveDisplayPlugin()->copyTextureToQuickFramebuffer(_networkTexture, _copyFbo, &_fenceSync); + _fboMutex.unlock(); + } else { + qDebug() << "couldn't get a lock, using last frame"; + } + } else { + _networkTexture = DependencyManager::get()->getTexture(_url); + } + } + update(); +} + +ResourceImageItemRenderer::ResourceImageItemRenderer() : QQuickFramebufferObject::Renderer() { + connect(&_updateTimer, SIGNAL(timeout()), this, SLOT(onUpdateTimer())); + auto textureCache = DependencyManager::get(); +} + +void ResourceImageItemRenderer::synchronize(QQuickFramebufferObject* item) { + ResourceImageItem* resourceImageItem = static_cast(item); + + resourceImageItem->setFlag(QQuickItem::ItemHasContents); + + _url = resourceImageItem->getUrl(); + _ready = resourceImageItem->getReady(); + _visible = resourceImageItem->isVisible(); + _window = resourceImageItem->window(); + + _networkTexture = DependencyManager::get()->getTexture(_url); + static const int UPDATE_TIMER_DELAY_IN_MS = 100; // 100 ms = 10 hz for now + if (_ready && _visible && !_updateTimer.isActive()) { + _updateTimer.start(UPDATE_TIMER_DELAY_IN_MS); + } else if (!(_ready && _visible) && _updateTimer.isActive()) { + _updateTimer.stop(); + } +} + +QOpenGLFramebufferObject* ResourceImageItemRenderer::createFramebufferObject(const QSize& size) { + if (_copyFbo) { + delete _copyFbo; + } + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + _copyFbo = new QOpenGLFramebufferObject(size, format); + _copyFbo->bind(); + return new QOpenGLFramebufferObject(size, format); +} + +void ResourceImageItemRenderer::render() { + auto f = QOpenGLContext::currentContext()->extraFunctions(); + + if (_fenceSync) { + f->glWaitSync(_fenceSync, 0, GL_TIMEOUT_IGNORED); + f->glDeleteSync(_fenceSync); + _fenceSync = 0; + } + if (_ready) { + _fboMutex.lock(); + _copyFbo->bind(); + QOpenGLFramebufferObject::blitFramebuffer(framebufferObject(), _copyFbo, GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + // this clears the copyFbo texture + // so next frame starts fresh - helps + // when aspect ratio changes + _copyFbo->takeTexture(); + _copyFbo->bind(); + _copyFbo->release(); + + _fboMutex.unlock(); + } + glFlush(); + _window->resetOpenGLState(); +} diff --git a/interface/src/ui/ResourceImageItem.h b/interface/src/ui/ResourceImageItem.h new file mode 100644 index 0000000000..985ab5a66e --- /dev/null +++ b/interface/src/ui/ResourceImageItem.h @@ -0,0 +1,63 @@ +// +// ResourceImageItem.h +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// 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 +// + +#pragma once +#ifndef hifi_ResourceImageItem_h +#define hifi_ResourceImageItem_h + +#include "Application.h" + +#include +#include +#include + +#include + +class ResourceImageItemRenderer : public QObject, public QQuickFramebufferObject::Renderer { + Q_OBJECT +public: + ResourceImageItemRenderer(); + QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override; + void synchronize(QQuickFramebufferObject* item) override; + void render() override; +private: + bool _ready; + QString _url; + bool _visible; + + NetworkTexturePointer _networkTexture; + QQuickWindow* _window; + QMutex _fboMutex; + QOpenGLFramebufferObject* _copyFbo { nullptr }; + GLsync _fenceSync { 0 }; + QTimer _updateTimer; +public slots: + void onUpdateTimer(); +}; + +class ResourceImageItem : public QQuickFramebufferObject { + Q_OBJECT + Q_PROPERTY(QString url READ getUrl WRITE setUrl) + Q_PROPERTY(bool ready READ getReady WRITE setReady) +public: + ResourceImageItem(); + QString getUrl() const { return m_url; } + void setUrl(const QString& url); + bool getReady() const { return m_ready; } + void setReady(bool ready); + QQuickFramebufferObject::Renderer* createRenderer() const override { return new ResourceImageItemRenderer; } + +private: + QString m_url; + bool m_ready { false }; + +}; + +#endif // hifi_ResourceImageItem_h diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index 182e7da978..f5668caa71 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -37,9 +37,11 @@ QVariant Billboard3DOverlay::getProperty(const QString &property) { return Planar3DOverlay::getProperty(property); } -void Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { +bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { + bool transformChanged = false; if (force || usecTimestampNow() > _transformExpiry) { - PanelAttachable::applyTransformTo(transform, true); - pointTransformAtCamera(transform, getOffsetRotation()); + transformChanged = PanelAttachable::applyTransformTo(transform, true); + transformChanged |= pointTransformAtCamera(transform, getOffsetRotation()); } + return transformChanged; } diff --git a/interface/src/ui/overlays/Billboard3DOverlay.h b/interface/src/ui/overlays/Billboard3DOverlay.h index d256a92afe..d429537b5b 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.h +++ b/interface/src/ui/overlays/Billboard3DOverlay.h @@ -27,7 +27,7 @@ public: QVariant getProperty(const QString& property) override; protected: - virtual void applyTransformTo(Transform& transform, bool force = false) override; + virtual bool applyTransformTo(Transform& transform, bool force = false) override; }; #endif // hifi_Billboard3DOverlay_h diff --git a/interface/src/ui/overlays/Billboardable.cpp b/interface/src/ui/overlays/Billboardable.cpp index a01d62bfd1..34a4ef6df5 100644 --- a/interface/src/ui/overlays/Billboardable.cpp +++ b/interface/src/ui/overlays/Billboardable.cpp @@ -28,7 +28,7 @@ QVariant Billboardable::getProperty(const QString &property) { return QVariant(); } -void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { +bool Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { if (isFacingAvatar()) { glm::vec3 billboardPos = transform.getTranslation(); glm::vec3 cameraPos = qApp->getCamera().getPosition(); @@ -38,5 +38,7 @@ void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offse glm::quat rotation(glm::vec3(elevation, azimuth, 0)); transform.setRotation(rotation); transform.postRotate(offsetRotation); + return true; } + return false; } diff --git a/interface/src/ui/overlays/Billboardable.h b/interface/src/ui/overlays/Billboardable.h index e2d29a2769..46d9ac6479 100644 --- a/interface/src/ui/overlays/Billboardable.h +++ b/interface/src/ui/overlays/Billboardable.h @@ -27,7 +27,7 @@ protected: void setProperties(const QVariantMap& properties); QVariant getProperty(const QString& property); - void pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); + bool pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); private: bool _isFacingAvatar = false; diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index c8c9c36a1d..7dfee2c491 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -99,10 +99,14 @@ void Image3DOverlay::render(RenderArgs* args) { const float MAX_COLOR = 255.0f; xColor color = getColor(); float alpha = getAlpha(); - + Transform transform = getTransform(); - applyTransformTo(transform, true); - setTransform(transform); + bool transformChanged = applyTransformTo(transform, true); + // If the transform is not modified, setting the transform to + // itself will cause drift over time due to floating point errors. + if (transformChanged) { + setTransform(transform); + } transform.postScale(glm::vec3(getDimensions(), 1.0f)); batch->setModelTransform(transform); diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index 421155083c..aae8893667 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -61,7 +61,7 @@ void PanelAttachable::setProperties(const QVariantMap& properties) { } } -void PanelAttachable::applyTransformTo(Transform& transform, bool force) { +bool PanelAttachable::applyTransformTo(Transform& transform, bool force) { if (force || usecTimestampNow() > _transformExpiry) { const quint64 TRANSFORM_UPDATE_PERIOD = 100000; // frequency is 10 Hz _transformExpiry = usecTimestampNow() + TRANSFORM_UPDATE_PERIOD; @@ -71,7 +71,9 @@ void PanelAttachable::applyTransformTo(Transform& transform, bool force) { transform.postTranslate(getOffsetPosition()); transform.postRotate(getOffsetRotation()); transform.postScale(getOffsetScale()); + return true; } #endif } + return false; } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index 4f37cd2258..1598aa4700 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -67,7 +67,7 @@ protected: /// set position, rotation and scale on transform based on offsets, and parent panel offsets /// if force is false, only apply transform if it hasn't been applied in the last .1 seconds - virtual void applyTransformTo(Transform& transform, bool force = false); + virtual bool applyTransformTo(Transform& transform, bool force = false); quint64 _transformExpiry = 0; private: diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 67bbb452ca..e1259fc5fc 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -16,6 +16,7 @@ #include #include +#include #if defined(Q_OS_MAC) #include #endif @@ -41,7 +42,7 @@ #include #include #include - +#include #include "CompositorHelper.h" #include "Logging.h" @@ -55,7 +56,7 @@ out vec4 outFragColor; float sRGBFloatToLinear(float value) { const float SRGB_ELBOW = 0.04045; - + return (value <= SRGB_ELBOW) ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4); } @@ -121,10 +122,10 @@ public: PROFILE_SET_THREAD_NAME("Present Thread"); // FIXME determine the best priority balance between this and the main thread... - // It may be dependent on the display plugin being used, since VR plugins should + // It may be dependent on the display plugin being used, since VR plugins should // have higher priority on rendering (although we could say that the Oculus plugin // doesn't need that since it has async timewarp). - // A higher priority here + // A higher priority here setPriority(QThread::HighPriority); OpenGLDisplayPlugin* currentPlugin{ nullptr }; Q_ASSERT(_context); @@ -233,7 +234,7 @@ public: // Move the context back to the presentation thread _context->moveToThread(this); - // restore control of the context to the presentation thread and signal + // restore control of the context to the presentation thread and signal // the end of the operation _finishedMainThreadOperation = true; lock.unlock(); @@ -291,7 +292,7 @@ bool OpenGLDisplayPlugin::activate() { if (!RENDER_THREAD) { RENDER_THREAD = _presentThread; } - + // Child classes may override this in order to do things like initialize // libraries, etc if (!internalActivate()) { @@ -411,7 +412,7 @@ void OpenGLDisplayPlugin::customizeContext() { gpu::Shader::makeProgram(*program); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(gpu::State::DepthTest(false)); - state->setBlendFunction(true, + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); _overlayPipeline = gpu::Pipeline::create(program, state); @@ -496,16 +497,48 @@ void OpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { _newFrameQueue.push(newFrame); }); } + void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor) { + renderFromTexture(batch, texture, viewport, scissor, gpu::FramebufferPointer()); +} + +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer copyFbo /*=gpu::FramebufferPointer()*/) { + auto fbo = gpu::FramebufferPointer(); batch.enableStereo(false); batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); + batch.setFramebuffer(fbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(scissor); batch.setViewportTransform(viewport); batch.setResourceTexture(0, texture); batch.setPipeline(_presentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); + if (copyFbo) { + gpu::Vec4i copyFboRect(0, 0, copyFbo->getWidth(), copyFbo->getHeight()); + gpu::Vec4i sourceRect(scissor.x, scissor.y, scissor.x + scissor.z, scissor.y + scissor.w); + float aspectRatio = (float)scissor.w / (float) scissor.z; // height/width + // scale width first + int xOffset = 0; + int yOffset = 0; + int newWidth = copyFbo->getWidth(); + int newHeight = std::round(aspectRatio * (float) copyFbo->getWidth()); + if (newHeight > copyFbo->getHeight()) { + // ok, so now fill height instead + newHeight = copyFbo->getHeight(); + newWidth = std::round((float)copyFbo->getHeight() / aspectRatio); + xOffset = (copyFbo->getWidth() - newWidth) / 2; + } else { + yOffset = (copyFbo->getHeight() - newHeight) / 2; + } + gpu::Vec4i copyRect(xOffset, yOffset, xOffset + newWidth, yOffset + newHeight); + batch.setFramebuffer(copyFbo); + + batch.resetViewTransform(); + batch.setViewportTransform(copyFboRect); + batch.setStateScissorRect(copyFboRect); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, {0.0f, 0.0f, 0.0f, 1.0f}); + batch.blit(fbo, sourceRect, copyFbo, copyRect); + } } void OpenGLDisplayPlugin::updateFrameData() { @@ -686,7 +719,7 @@ void OpenGLDisplayPlugin::resetPresentRate() { // _presentRate = RateCounter<100>(); } -float OpenGLDisplayPlugin::renderRate() const { +float OpenGLDisplayPlugin::renderRate() const { return _renderRate.rate(); } @@ -821,3 +854,53 @@ void OpenGLDisplayPlugin::updateCompositeFramebuffer() { _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); } } + +void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) { + auto glBackend = const_cast(*this).getGLBackend(); + withMainThreadContext([&] { + GLuint sourceTexture = glBackend->getTextureID(networkTexture->getGPUTexture()); + GLuint targetTexture = target->texture(); + GLuint fbo[2] {0, 0}; + + // need mipmaps for blitting texture + glGenerateTextureMipmap(sourceTexture); + + // create 2 fbos (one for initial texture, second for scaled one) + glCreateFramebuffers(2, fbo); + + // setup source fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sourceTexture, 0); + + GLint texWidth = networkTexture->getWidth(); + GLint texHeight = networkTexture->getHeight(); + + // setup destination fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targetTexture, 0); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + + // maintain aspect ratio, filling the width first if possible. If that makes the height too + // much, fill height instead. TODO: only do this when texture changes + GLint newX = 0; + GLint newY = 0; + float aspectRatio = (float)texHeight / (float)texWidth; + GLint newWidth = target->width(); + GLint newHeight = std::round(aspectRatio * (float) target->width()); + if (newHeight > target->height()) { + newHeight = target->height(); + newWidth = std::round((float)target->height() / aspectRatio); + newX = (target->width() - newWidth) / 2; + } else { + newY = (target->height() - newHeight) / 2; + } + glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // don't delete the textures! + glDeleteFramebuffers(2, fbo); + *fenceSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + }); +} + diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 7e7889ff47..2f93fa630d 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -38,7 +38,7 @@ protected: using Condition = std::condition_variable; public: ~OpenGLDisplayPlugin(); - // These must be final to ensure proper ordering of operations + // These must be final to ensure proper ordering of operations // between the main thread and the presentation thread bool activate() override final; void deactivate() override final; @@ -79,6 +79,8 @@ public: // Three threads, one for rendering, one for texture transfers, one reserved for the GL driver int getRequiredThreadCount() const override { return 3; } + void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override; + protected: friend class PresentThread; @@ -103,7 +105,7 @@ protected: // Returns true on successful activation virtual bool internalActivate() { return true; } virtual void internalDeactivate() {} - + // Returns true on successful activation of standby session virtual bool activateStandBySession() { return true; } virtual void deactivateSession() {} @@ -111,6 +113,7 @@ protected: // Plugin specific functionality to send the composed scene to the output window or device virtual void internalPresent(); + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer fbo); void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor); virtual void updateFrameData(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ea91890f33..b183850e7f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -134,7 +134,7 @@ void HmdDisplayPlugin::customizeContext() { state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - + gpu::Shader::BindingSet bindings; bindings.insert({ "lineData", LINE_DATA_SLOT });; gpu::Shader::makeProgram(*program, bindings); @@ -243,6 +243,8 @@ void HmdDisplayPlugin::internalPresent() { glm::ivec4 viewport = getViewportForSourceSize(sourceSize); glm::ivec4 scissor = viewport; + auto fbo = gpu::FramebufferPointer(); + render([&](gpu::Batch& batch) { if (_monoPreview) { @@ -285,11 +287,15 @@ void HmdDisplayPlugin::internalPresent() { viewport = ivec4(scissorOffset - scaledShiftLeftBy, viewportOffset, viewportSizeX, viewportSizeY); } + // TODO: only bother getting and passing in the hmdPreviewFramebuffer if the camera is on + fbo = DependencyManager::get()->getHmdPreviewFramebuffer(windowSize.x, windowSize.y); + viewport.z *= 2; } - renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor); + renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor, fbo); }); swapBuffers(); + } else if (_clearPreviewFlag) { QImage image; if (_vsyncEnabled) { @@ -312,7 +318,7 @@ void HmdDisplayPlugin::internalPresent() { _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); _previewTexture->setAutoGenerateMips(true); } - + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); render([&](gpu::Batch& batch) { @@ -323,7 +329,7 @@ void HmdDisplayPlugin::internalPresent() { } postPreview(); - // If preview is disabled, we need to check to see if the window size has changed + // If preview is disabled, we need to check to see if the window size has changed // and re-render the no-preview message if (_disablePreview) { auto window = _container->getPrimaryWidget(); @@ -510,7 +516,7 @@ void HmdDisplayPlugin::OverlayRenderer::build() { indices = std::make_shared(); //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm - + static const float fov = CompositorHelper::VIRTUAL_UI_TARGET_FOV.y; static const float aspectRatio = CompositorHelper::VIRTUAL_UI_ASPECT_RATIO; static const uint16_t stacks = 128; @@ -672,7 +678,7 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve _handLasers[1] = info; } }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // FIXME defer to a child class plugin to determine if hand lasers are actually // available based on the presence or absence of hand controllers return true; } @@ -687,7 +693,7 @@ bool HmdDisplayPlugin::setExtraLaser(HandLaserMode mode, const vec4& color, cons _extraLaserStart = sensorSpaceStart; }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // FIXME defer to a child class plugin to determine if hand lasers are actually // available based on the presence or absence of hand controllers return true; } @@ -702,7 +708,7 @@ void HmdDisplayPlugin::compositeExtra() { if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) { return; } - + render([&](gpu::Batch& batch) { batch.setFramebuffer(_compositeFramebuffer); batch.setModelTransform(Transform()); diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 20749cd567..85bde4c2f1 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -54,6 +54,7 @@ const std::string TextureCache::KTX_EXT { "ktx" }; static const QString RESOURCE_SCHEME = "resource"; static const QUrl SPECTATOR_CAMERA_FRAME_URL("resource://spectatorCameraFrame"); +static const QUrl HMD_PREVIEW_FRAME_URL("resource://hmdPreviewFrame"); static const float SKYBOX_LOAD_PRIORITY { 10.0f }; // Make sure skybox loads first static const float HIGH_MIPS_LOAD_PRIORITY { 9.0f }; // Make sure high mips loads after skybox but before models @@ -219,7 +220,7 @@ gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, co gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) { gpu::TexturePointer result; auto textureCache = DependencyManager::get(); - // Since this can be called on a background thread, there's a chance that the cache + // Since this can be called on a background thread, there's a chance that the cache // will be destroyed by the time we request it if (!textureCache) { return result; @@ -369,7 +370,7 @@ void NetworkTexture::makeRequest() { if (!_sourceIsKTX) { Resource::makeRequest(); return; - } + } // We special-handle ktx requests to run 2 concurrent requests right off the bat PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } }); @@ -908,7 +909,7 @@ void ImageReader::read() { } } - // If we found the texture either because it's in use or via KTX deserialization, + // If we found the texture either because it's in use or via KTX deserialization, // set the image and return immediately. if (texture) { QMetaObject::invokeMethod(resource.data(), "setImage", @@ -957,7 +958,7 @@ void ImageReader::read() { qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; } - // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different + // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will // be the winner texture = textureCache->cacheTextureByHash(hash, texture); @@ -969,23 +970,44 @@ void ImageReader::read() { Q_ARG(int, texture->getHeight())); } - NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) { gpu::TexturePointer texture; if (resourceTextureUrl == SPECTATOR_CAMERA_FRAME_URL) { if (!_spectatorCameraNetworkTexture) { _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); } - texture = _spectatorCameraFramebuffer->getRenderBuffer(0); - if (texture) { - _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); - return _spectatorCameraNetworkTexture; + if (_spectatorCameraFramebuffer) { + texture = _spectatorCameraFramebuffer->getRenderBuffer(0); + if (texture) { + _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _spectatorCameraNetworkTexture; + } + } + } + // FIXME: Generalize this, DRY up this code + if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) { + if (!_hmdPreviewNetworkTexture) { + _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); + } + if (_hmdPreviewFramebuffer) { + texture = _hmdPreviewFramebuffer->getRenderBuffer(0); + if (texture) { + _hmdPreviewNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _hmdPreviewNetworkTexture; + } } } return NetworkTexturePointer(); } +const gpu::FramebufferPointer& TextureCache::getHmdPreviewFramebuffer(int width, int height) { + if (!_hmdPreviewFramebuffer || _hmdPreviewFramebuffer->getWidth() != width || _hmdPreviewFramebuffer->getHeight() != height) { + _hmdPreviewFramebuffer.reset(gpu::Framebuffer::create("hmdPreview",gpu::Element::COLOR_SRGBA_32, width, height)); + } + return _hmdPreviewFramebuffer; +} + const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { if (!_spectatorCameraFramebuffer) { resetSpectatorCameraFramebuffer(2048, 1024); @@ -996,4 +1018,5 @@ const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { void TextureCache::resetSpectatorCameraFramebuffer(int width, int height) { _spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height)); _spectatorCameraNetworkTexture.reset(); + emit spectatorCameraFramebufferReset(); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 43edc3593d..f5a0ec5215 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -74,7 +74,7 @@ protected: virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; - + Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); @@ -170,6 +170,10 @@ public: NetworkTexturePointer getResourceTexture(QUrl resourceTextureUrl); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); void resetSpectatorCameraFramebuffer(int width, int height); + const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); + +signals: + void spectatorCameraFramebufferReset(); protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads @@ -202,6 +206,9 @@ private: NetworkTexturePointer _spectatorCameraNetworkTexture; gpu::FramebufferPointer _spectatorCameraFramebuffer; + + NetworkTexturePointer _hmdPreviewNetworkTexture; + gpu::FramebufferPointer _hmdPreviewFramebuffer; }; #endif // hifi_TextureCache_h diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 7bfdbddbc5..481a2609fc 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -22,9 +22,10 @@ #include #include #include - #include "Plugin.h" +class QOpenGLFramebufferObject; + class QImage; enum Eye { @@ -60,8 +61,12 @@ namespace gpu { using TexturePointer = std::shared_ptr; } +class NetworkTexture; +using NetworkTexturePointer = QSharedPointer; +typedef struct __GLsync *GLsync; + // Stereo display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isStereo returns true class StereoDisplay { public: @@ -78,7 +83,7 @@ public: }; // HMD display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isHmd returns true class HmdDisplay : public StereoDisplay { public: @@ -142,7 +147,7 @@ public: virtual float getTargetFrameRate() const { return 1.0f; } virtual bool hasAsyncReprojection() const { return false; } - /// Returns a boolean value indicating whether the display is currently visible + /// Returns a boolean value indicating whether the display is currently visible /// to the user. For monitor displays, false might indicate that a screensaver, /// or power-save mode is active. For HMDs it may reflect a sensor indicating /// whether the HMD is being worn @@ -204,10 +209,12 @@ public: // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } virtual bool getSupportsAutoSwitch() { return false; } - + // Hardware specific stats virtual QJsonObject getHardwareStats() const { return QJsonObject(); } + virtual void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) = 0; + uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) int64_t getPaintDelayUsecs() const; diff --git a/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav new file mode 100644 index 0000000000..76dbb647b1 Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx new file mode 100644 index 0000000000..6584264c0d Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js new file mode 100644 index 0000000000..f0b943ad92 --- /dev/null +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js @@ -0,0 +1,514 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Script, */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// spectatorCamera.js +// +// Created by Zach Fox on 2017-06-05 +// Copyright 2017 High Fidelity, Inc +// +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { // BEGIN LOCAL_SCOPE + + // FUNCTION VAR DECLARATIONS + var sendToQml, addOrRemoveButton, onTabletScreenChanged, fromQml, + onTabletButtonClicked, wireEventBridge, startup, shutdown, registerButtonMappings; + + // Function Name: inFrontOf() + // + // Description: + // -Returns the position in front of the given "position" argument, where the forward vector is based off + // the "orientation" argument and the amount in front is based off the "distance" argument. + function inFrontOf(distance, position, orientation) { + return Vec3.sum(position || MyAvatar.position, + Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation))); + } + + // Function Name: spectatorCameraOn() + // + // Description: + // -Call this function to set up the spectator camera and + // spawn the camera entity. + // + // Relevant Variables: + // -spectatorCameraConfig: The render configuration of the spectator camera + // render job. It controls various attributes of the Secondary Camera, such as: + // -The entity ID to follow + // -Position + // -Orientation + // -Rendered texture size + // -Vertical field of view + // -Near clip plane distance + // -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. + // -viewFinderOverlayDim: The x, y, and z dimensions of the viewFinderOverlay. + // -camera: The camera model which is grabbable. + // -viewFinderOverlay: The preview of what the spectator camera is viewing, placed inside the glass pane. + var spectatorCameraConfig = Render.getConfig("SecondaryCamera"); + var viewFinderOverlay = false; + var camera = false; + var cameraIsDynamic = false; + var cameraRotation; + var cameraPosition; + var glassPaneWidth = 0.16; + // The negative y dimension for viewFinderOverlay is necessary for now due to the way Image3DOverlay + // draws textures, but should be looked into at some point. Also the z dimension shouldn't affect + // the overlay since it is an Image3DOverlay so it is set to 0. + var viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + function spectatorCameraOn() { + // Sets the special texture size based on the window it is displayed in, which doesn't include the menu bar + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(true); + 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, + "collidesWith": "static,dynamic,kinematic,", + "collisionMask": 7, + "dynamic": cameraIsDynamic, + "modelURL": Script.resolvePath("spectator-camera.fbx"), + "registrationPoint": { + "x": 0.56, + "y": 0.545, + "z": 0.23 + }, + "rotation": cameraRotation, + "position": cameraPosition, + "shapeType": "simple-compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true}}" + }, 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 (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + Audio.playSound(CAMERA_ON_SOUND, { + volume: 0.15, + position: cameraPosition, + localOnly: true + }); + } + + // Function Name: spectatorCameraOff() + // + // Description: + // -Call this function to shut down the spectator camera and + // destroy the camera entity. "isChangingDomains" is true when this function is called + // from the "Window.domainChanged()" signal. + var WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS = 1 * 1000; + function spectatorCameraOff(isChangingDomains) { + function deleteCamera() { + Entities.deleteEntity(camera); + camera = false; + if (button) { + // Change button to active when window is first openend OR if the camera is on, false otherwise. + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + spectatorCameraConfig.attachedEntityId = false; + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(false); + if (camera) { + // Workaround for Avatar Entities not immediately having properties after + // the "Window.domainChanged()" signal is emitted. + // Should be removed after FB6155 is fixed. + if (isChangingDomains) { + Script.setTimeout(function () { + deleteCamera(); + spectatorCameraOn(); + }, WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS); + } else { + deleteCamera(); + } + } + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = false; + setDisplay(monitorShowsCameraView); + } + + // Function Name: addOrRemoveButton() + // + // Description: + // -Used to add or remove the "SPECTATOR" app button from the HUD/tablet. Set the "isShuttingDown" argument + // to true if you're calling this function upon script shutdown. Set the "isHMDmode" to true if the user is + // in HMD; otherwise set to false. + // + // 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) { + if (!tablet) { + print("Warning in addOrRemoveButton(): 'tablet' undefined!"); + return; + } + if (!button) { + if ((isHMDMode || showSpectatorInDesktop) && !isShuttingDown) { + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/spectator-i.svg", + activeIcon: "icons/tablet-icons/spectator-a.svg" + }); + button.clicked.connect(onTabletButtonClicked); + } + } else if (button) { + if ((!isHMDMode && !showSpectatorInDesktop) || isShuttingDown) { + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + button = false; + } + } else { + print("ERROR adding/removing Spectator button!"); + } + } + + // Function Name: startup() + // + // Description: + // -startup() will be called when the script is loaded. + // + // Relevant Variables: + // -tablet: The tablet instance to be modified. + var tablet = null; + function startup() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + addOrRemoveButton(false, HMD.active); + tablet.screenChanged.connect(onTabletScreenChanged); + Window.domainChanged.connect(onDomainChanged); + Window.geometryChanged.connect(resizeViewFinderOverlay); + Controller.keyPressEvent.connect(keyPressEvent); + HMD.displayModeChanged.connect(onHMDChanged); + viewFinderOverlay = false; + camera = false; + registerButtonMappings(); + } + + // Function Name: wireEventBridge() + // + // Description: + // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or + // disable to event bridge. + // + // Relevant Variables: + // -hasEventBridge: true/false depending on whether we've already connected the event bridge. + var hasEventBridge = false; + function wireEventBridge(on) { + if (!tablet) { + print("Warning in wireEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } + + // Function Name: setDisplay() + // + // Description: + // -There are two bool variables that determine what the "url" argument to "setDisplayTexture(url)" should be: + // Camera on/off switch, and the "Monitor Shows" on/off switch. + // This results in four possible cases for the argument. Those four cases are: + // 1. Camera is off; "Monitor Shows" is "HMD Preview": "url" is "" + // 2. Camera is off; "Monitor Shows" is "Camera View": "url" is "" + // 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. + if (url === "resource://hmdPreviewFrame") { + Window.setDisplayTexture(""); + } else { + Window.setDisplayTexture(url); + } + } + 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); + Settings.setValue('spectatorCamera/monitorShowsCameraView', showCameraView); + } + function setMonitorShowsCameraViewAndSendToQml(showCameraView) { + setMonitorShowsCameraView(showCameraView); + sendToQml({ method: 'updateMonitorShowsSwitch', params: showCameraView }); + } + function keyPressEvent(event) { + if ((event.text === "0") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && event.isControl && !event.isAlt) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + } + function updateOverlay() { + // The only way I found to update the viewFinderOverlay without turning the spectator camera on and off is to delete and recreate the + // overlay, which is inefficient but resizing the window shouldn't be performed often + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = Overlays.addOverlay("image3d", { + url: "resource://spectatorCameraFrame", + emissive: true, + parentID: camera, + alpha: 1, + localRotation: { w: 1, x: 0, y: 0, z: 0 }, + localPosition: { x: 0, y: 0.13, z: 0.126 }, + dimensions: viewFinderOverlayDim + }); + } + + // Function Name: resizeViewFinderOverlay() + // + // Description: + // -A function called when the window is moved/resized, which changes the viewFinderOverlay's texture and dimensions to be + // appropriately altered to fit inside the glass pane while not distorting the texture. The "geometryChanged" argument gives information + // on how the window changed, including x, y, width, and height. + // + // Relevant Variables: + // -glassPaneRatio: The aspect ratio of the glass pane, currently set as a 16:9 aspect ratio (change if model changes). + // -verticalScale: The amount the viewFinderOverlay should be scaled if the window size is vertical. + // -squareScale: The amount the viewFinderOverlay should be scaled if the window size is not vertical but is more square than the + // glass pane's aspect ratio. + function resizeViewFinderOverlay(geometryChanged) { + var glassPaneRatio = 16 / 9; + var verticalScale = 1 / glassPaneRatio; + var squareScale = verticalScale * (1 + (1 - (1 / (geometryChanged.width / geometryChanged.height)))); + + if (geometryChanged.height > geometryChanged.width) { //vertical window size + viewFinderOverlayDim = { x: (glassPaneWidth * verticalScale), y: (-glassPaneWidth * verticalScale), z: 0 }; + } else if ((geometryChanged.width / geometryChanged.height) < glassPaneRatio) { //square-ish window size, in-between vertical and horizontal + viewFinderOverlayDim = { x: (glassPaneWidth * squareScale), y: (-glassPaneWidth * squareScale), z: 0 }; + } else { //horizontal window size + viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + } + updateOverlay(); + spectatorCameraConfig.resetSizeSpectatorCamera(geometryChanged.width, geometryChanged.height); + setDisplay(monitorShowsCameraView); + } + + const SWITCH_VIEW_FROM_CONTROLLER_DEFAULT = false; + var switchViewFromController = !!Settings.getValue('spectatorCamera/switchViewFromController', SWITCH_VIEW_FROM_CONTROLLER_DEFAULT); + function setControllerMappingStatus(status) { + if (!controllerMapping) { + return; + } + if (status) { + controllerMapping.enable(); + } else { + controllerMapping.disable(); + } + } + function setSwitchViewFromController(setting) { + if (setting === switchViewFromController) { + return; + } + switchViewFromController = setting; + setControllerMappingStatus(switchViewFromController); + Settings.setValue('spectatorCamera/switchViewFromController', 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. + // -controllerType: "OculusTouch", "Vive", "Other". + var controllerMappingName; + var controllerMapping; + var controllerType = "Other"; + function registerButtonMappings() { + var VRDevices = Controller.getDeviceNames().toString(); + if (VRDevices) { + if (VRDevices.indexOf("Vive") !== -1) { + controllerType = "Vive"; + } else if (VRDevices.indexOf("OculusTouch") !== -1) { + controllerType = "OculusTouch"; + } else { + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, 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; + }); + } + setControllerMappingStatus(switchViewFromController); + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + } + + // Function Name: onTabletButtonClicked() + // + // Description: + // -Fired when the Spectator Camera app button is pressed. + // + // 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 = Script.resourcesPath() + "qml/hifi/SpectatorCamera.qml"; + var onSpectatorCameraScreen = false; + function onTabletButtonClicked() { + if (!tablet) { + print("Warning in onTabletButtonClicked(): 'tablet' undefined!"); + return; + } + if (onSpectatorCameraScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + 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 Name: onTabletScreenChanged() + // + // Description: + // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string + // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. + function onTabletScreenChanged(type, url) { + onSpectatorCameraScreen = (type === "QML" && url === SPECTATOR_CAMERA_QML_SOURCE); + wireEventBridge(onSpectatorCameraScreen); + // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + // Function Name: sendToQml() + // + // Description: + // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to + // SpectatorCamera QML in the format "{method, params}", like json-rpc. See also fromQml(). + function sendToQml(message) { + tablet.sendToQml(message); + } + + // Function Name: fromQml() + // + // Description: + // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the SpectatorCamera QML + // in the format "{method, params}", like json-rpc. See also sendToQml(). + function fromQml(message) { + switch (message.method) { + case 'spectatorCameraOn': + spectatorCameraOn(); + break; + case 'spectatorCameraOff': + spectatorCameraOff(); + break; + case 'setMonitorShowsCameraView': + setMonitorShowsCameraView(message.params); + break; + case 'changeSwitchViewFromControllerPreference': + setSwitchViewFromController(message.params); + break; + default: + print('Unrecognized message from SpectatorCamera.qml:', JSON.stringify(message)); + } + } + + // Function Name: onHMDChanged() + // + // 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) { + registerButtonMappings(); + } + setDisplay(monitorShowsCameraView); + addOrRemoveButton(false, isHMDMode); + if (!isHMDMode && !showSpectatorInDesktop) { + spectatorCameraOff(); + } + } + + // Function Name: shutdown() + // + // Description: + // -shutdown() will be called when the script ends (i.e. is stopped). + function shutdown() { + spectatorCameraOff(); + Window.domainChanged.disconnect(onDomainChanged); + Window.geometryChanged.disconnect(resizeViewFinderOverlay); + addOrRemoveButton(true, HMD.active); + if (tablet) { + tablet.screenChanged.disconnect(onTabletScreenChanged); + if (onSpectatorCameraScreen) { + tablet.gotoHomeScreen(); + } + } + HMD.displayModeChanged.disconnect(onHMDChanged); + Controller.keyPressEvent.disconnect(keyPressEvent); + if (controllerMapping) { + controllerMapping.disable(); + } + } + + // Function Name: onDomainChanged() + // + // Description: + // -A small utility function used when the Window.domainChanged() signal is fired. + function onDomainChanged() { + spectatorCameraOff(true); + } + + // These functions will be called when the script is loaded. + var CAMERA_ON_SOUND = SoundCache.getSound(Script.resolvePath("cameraOn.wav")); + startup(); + Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE