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