From b31ebb96aac2123d70b8be4aee77397d6e0f1d10 Mon Sep 17 00:00:00 2001 From: Keb Helion <60008426+KebHelion@users.noreply.github.com> Date: Wed, 15 Apr 2020 16:35:21 -0400 Subject: [PATCH 1/5] Add files via upload Updated with the last fixes done by Silverfish modified spectator camera -added tonemapping selector. -fixed viewfinder scaling. -new camera model that is not 8 drawcalls. --- .../spectator-camera/SpectatorCamera.qml | 1406 +++++++-------- applications/spectator-camera/newcam.fbx | Bin 0 -> 34028 bytes .../spectator-camera/spectatorCamera.js | 1526 +++++++++-------- 3 files changed, 1512 insertions(+), 1420 deletions(-) create mode 100644 applications/spectator-camera/newcam.fbx diff --git a/applications/spectator-camera/SpectatorCamera.qml b/applications/spectator-camera/SpectatorCamera.qml index 74944d9..898f91c 100644 --- a/applications/spectator-camera/SpectatorCamera.qml +++ b/applications/spectator-camera/SpectatorCamera.qml @@ -1,672 +1,734 @@ -// -// SpectatorCamera.qml -// qml/hifi -// -// Spectator Camera v2.5 -// -// Created by Zach Fox on 2018-12-12 -// Copyright 2018 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import Hifi 1.0 as Hifi -import QtQuick 2.7 -import QtQuick.Controls 2.2 -import QtGraphicalEffects 1.0 - -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit -import "qrc:////qml//controls" as HifiControls -import "qrc:////qml//hifi" as Hifi - -Rectangle { - HifiStylesUit.HifiConstants { id: hifi; } - - id: root; - property bool uiReady: false; - property bool processingStillSnapshot: false; - property bool processing360Snapshot: false; - // Style - color: "#404040"; - - // The letterbox used for popup messages - Hifi.LetterboxMessage { - id: letterboxMessage; - z: 998; // Force the popup on top of everything else - } - function letterbox(headerGlyph, headerText, message) { - letterboxMessage.headerGlyph = headerGlyph; - letterboxMessage.headerText = headerText; - letterboxMessage.text = message; - letterboxMessage.visible = true; - letterboxMessage.popupRadius = 0; - } - - // - // TITLE BAR START - // - Rectangle { - id: titleBarContainer; - // Size - width: root.width; - height: 60; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - color: "#121212"; - - // "Spectator" text - HifiStylesUit.RalewaySemiBold { - id: titleBarText; - text: "Spectator Camera 2.5"; - // Anchors - anchors.left: parent.left; - anchors.leftMargin: 30; - width: paintedWidth; - height: parent.height; - size: 22; - // Style - color: hifi.colors.white; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - Switch { - id: masterSwitch; - focusPolicy: Qt.ClickFocus; - width: 65; - height: 30; - anchors.verticalCenter: parent.verticalCenter; - anchors.right: parent.right; - anchors.rightMargin: 30; - hoverEnabled: true; - - onHoveredChanged: { - if (hovered) { - switchHandle.color = hifi.colors.blueHighlight; - } else { - switchHandle.color = hifi.colors.lightGray; - } - } - - onClicked: { - if (!checked) { - flashCheckBox.checked = false; - } - sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); - sendToScript({method: 'updateCameravFoV', vFoV: fieldOfViewSlider.value}); - } - - background: Rectangle { - color: parent.checked ? "#1FC6A6" : hifi.colors.white; - implicitWidth: masterSwitch.width; - implicitHeight: masterSwitch.height; - radius: height/2; - } - - indicator: Rectangle { - id: switchHandle; - implicitWidth: masterSwitch.height - 4; - implicitHeight: implicitWidth; - radius: implicitWidth/2; - border.color: "#E3E3E3"; - color: "#404040"; - x: Math.max(4, Math.min(parent.width - width - 4, parent.visualPosition * parent.width - (width / 2) - 4)) - y: parent.height / 2 - height / 2; - Behavior on x { - enabled: !masterSwitch.down - SmoothedAnimation { velocity: 200 } - } - - } - } - } - // - // TITLE BAR END - // - - Rectangle { - z: 999; - id: processingSnapshot; - anchors.fill: parent; - visible: root.processing360Snapshot || !root.uiReady; - color: Qt.rgba(0.0, 0.0, 0.0, 0.85); - - // This object is always used in a popup. - // This MouseArea is used to prevent a user from being - // able to click on a button/mouseArea underneath the popup/section. - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - propagateComposedEvents: false; - } - - AnimatedImage { - id: processingImage; - source: "processing.gif" - width: 74; - height: width; - anchors.verticalCenter: parent.verticalCenter; - anchors.horizontalCenter: parent.horizontalCenter; - } - - HifiStylesUit.RalewaySemiBold { - text: root.uiReady ? "Processing..." : ""; - // Anchors - anchors.top: processingImage.bottom; - anchors.topMargin: 4; - anchors.horizontalCenter: parent.horizontalCenter; - width: paintedWidth; - // Text size - size: 26; - // Style - color: hifi.colors.white; - verticalAlignment: Text.AlignVCenter; - } - } - - // - // SPECTATOR CONTROLS START - // - Item { - id: spectatorControlsContainer; - // Anchors - anchors.top: titleBarContainer.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; - - // Instructions or Preview - Rectangle { - id: spectatorCameraImageContainer; - anchors.left: parent.left; - anchors.top: parent.top; - anchors.right: parent.right; - height: 250; - color: masterSwitch.checked ? "transparent" : "black"; - - AnimatedImage { - source: "static.gif" - visible: !masterSwitch.checked; - anchors.fill: parent; - opacity: 0.15; - } - - // Instructions (visible when display texture isn't set) - HifiStylesUit.FiraSansRegular { - id: spectatorCameraInstructions; - text: "Turn on Spectator Camera for a preview\nof " + (HMD.active ? "what your monitor shows." : "the camera's view."); - size: 16; - color: hifi.colors.white; - visible: !masterSwitch.checked; - anchors.fill: parent; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - - HifiStylesUit.FiraSansRegular { - text: ":)"; - size: 28; - color: hifi.colors.white; - visible: root.processing360Snapshot || root.processingStillSnapshot; - anchors.fill: parent; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - - // Spectator Camera Preview - Hifi.ResourceImageItem { - id: spectatorCameraPreview; - visible: masterSwitch.checked && !root.processing360Snapshot && !root.processingStillSnapshot; - url: showCameraView.checked || !HMD.active ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; - ready: masterSwitch.checked; - mirrorVertically: true; - anchors.fill: parent; - onVisibleChanged: { - ready = masterSwitch.checked; - update(); - } - } - - Item { - visible: HMD.active; - anchors.top: parent.top; - anchors.left: parent.left; - anchors.right: parent.right; - height: 40; - - LinearGradient { - anchors.fill: parent; - start: Qt.point(0, 0); - end: Qt.point(0, height); - gradient: Gradient { - GradientStop { position: 0.0; color: hifi.colors.black } - GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0) } - } - } - - HifiStylesUit.HiFiGlyphs { - id: monitorShowsSwitchLabelGlyph; - text: hifi.glyphs.screen; - size: 32; - color: hifi.colors.white; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.leftMargin: 16; - } - HifiStylesUit.RalewayLight { - id: monitorShowsSwitchLabel; - text: "Monitor View:"; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: monitorShowsSwitchLabelGlyph.right; - anchors.leftMargin: 8; - size: 20; - width: paintedWidth; - height: parent.height; - color: hifi.colors.white; - verticalAlignment: Text.AlignVCenter; - } - Item { - anchors.left: monitorShowsSwitchLabel.right; - anchors.leftMargin: 14; - anchors.right: parent.right; - anchors.rightMargin: 10; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - - HifiControlsUit.RadioButton { - id: showCameraView; - text: "Camera View"; - width: 125; - anchors.left: parent.left; - anchors.leftMargin: 10; - anchors.verticalCenter: parent.verticalCenter; - colorScheme: hifi.colorSchemes.dark; - onClicked: { - if (showHmdPreview.checked) { - showHmdPreview.checked = false; - } - if (!showCameraView.checked && !showHmdPreview.checked) { - showCameraView.checked = true; - } - } - onCheckedChanged: { - if (checked) { - sendToScript({method: 'setMonitorShowsCameraView', params: true}); - } - } - } - - HifiControlsUit.RadioButton { - id: showHmdPreview; - text: "VR Preview"; - anchors.left: showCameraView.right; - anchors.leftMargin: 10; - width: 125; - anchors.verticalCenter: parent.verticalCenter; - colorScheme: hifi.colorSchemes.dark; - onClicked: { - if (showCameraView.checked) { - showCameraView.checked = false; - } - if (!showCameraView.checked && !showHmdPreview.checked) { - showHmdPreview.checked = true; - } - } - onCheckedChanged: { - if (checked) { - sendToScript({method: 'setMonitorShowsCameraView', params: false}); - } - } - } - } - } - - HifiStylesUit.HiFiGlyphs { - id: flashGlyph; - visible: flashCheckBox.visible; - text: hifi.glyphs.lightning; - size: 26; - color: hifi.colors.white; - anchors.verticalCenter: flashCheckBox.verticalCenter; - anchors.right: flashCheckBox.left; - anchors.rightMargin: -2; - } - HifiControlsUit.CheckBox { - id: flashCheckBox; - visible: masterSwitch.checked; - color: hifi.colors.white; - colorScheme: hifi.colorSchemes.dark; - anchors.right: takeSnapshotButton.left; - anchors.rightMargin: -8; - anchors.verticalCenter: takeSnapshotButton.verticalCenter; - boxSize: 22; - onClicked: { - sendToScript({method: 'setFlashStatus', enabled: checked}); - } - } - HifiControlsUit.Button { - id: takeSnapshotButton; - enabled: masterSwitch.checked; - text: "SNAP PICTURE"; - colorScheme: hifi.colorSchemes.light; - color: hifi.buttons.white; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; - anchors.right: take360SnapshotButton.left; - anchors.rightMargin: 12; - width: 135; - height: 35; - onClicked: { - root.processingStillSnapshot = true; - sendToScript({method: 'takeSecondaryCameraSnapshot'}); - } - } - HifiControlsUit.Button { - id: take360SnapshotButton; - enabled: masterSwitch.checked; - text: "SNAP 360"; - colorScheme: hifi.colorSchemes.light; - color: hifi.buttons.white; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; - anchors.right: parent.right; - anchors.rightMargin: 12; - width: 135; - height: 35; - onClicked: { - root.processing360Snapshot = true; - sendToScript({method: 'takeSecondaryCamera360Snapshot'}); - } - } - } - - Item { - anchors.top: spectatorCameraImageContainer.bottom; - anchors.topMargin: 8; - anchors.left: parent.left; - anchors.leftMargin: 26; - anchors.right: parent.right; - anchors.rightMargin: 26; - anchors.bottom: parent.bottom; - - Item { - id: fieldOfView; - visible: masterSwitch.checked; - anchors.top: parent.top; - anchors.left: parent.left; - anchors.right: parent.right; - height: 35; - - HifiStylesUit.RalewaySemiBold { - id: fieldOfViewLabel; - text: "Field of View (" + fieldOfViewSlider.value + "\u00B0): "; - size: 20; - color: hifi.colors.white; - anchors.left: parent.left; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - width: 172; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - - HifiControlsUit.Slider { - id: fieldOfViewSlider; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.right: resetvFoV.left; - anchors.rightMargin: 8; - anchors.left: fieldOfViewLabel.right; - anchors.leftMargin: 8; - colorScheme: hifi.colorSchemes.dark; - from: 10.0; - to: 120.0; - value: 45.0; - stepSize: 1; - - onValueChanged: { - sendToScript({method: 'updateCameravFoV', vFoV: value}); - } - onPressedChanged: { - if (!pressed) { - sendToScript({method: 'updateCameravFoV', vFoV: value}); - } - } - } - - HifiControlsUit.GlyphButton { - id: resetvFoV; - anchors.verticalCenter: parent.verticalCenter; - anchors.right: parent.right; - anchors.rightMargin: -8; - height: parent.height - 8; - width: height; - glyph: hifi.glyphs.reload; - onClicked: { - fieldOfViewSlider.value = 45.0; - } - } - } - - Item { - visible: HMD.active; - anchors.top: fieldOfView.bottom; - anchors.topMargin: 18; - anchors.left: parent.left; - anchors.right: parent.right; - height: childrenRect.height; - - HifiStylesUit.RalewaySemiBold { - id: shortcutsHeaderText; - anchors.top: parent.top; - anchors.left: parent.left; - anchors.right: parent.right; - height: paintedHeight; - text: "Shortcuts"; - size: 20; - color: hifi.colors.white; - } - - // "Switch View From Controller" Checkbox - HifiControlsUit.CheckBox { - id: switchViewFromControllerCheckBox; - color: hifi.colors.white; - colorScheme: hifi.colorSchemes.dark; - anchors.left: parent.left; - anchors.top: shortcutsHeaderText.bottom; - anchors.topMargin: 8; - text: ""; - labelFontSize: 20; - labelFontWeight: Font.Normal; - boxSize: 24; - onClicked: { - sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); - } - } - - // "Take Snapshot" Checkbox - HifiControlsUit.CheckBox { - id: takeSnapshotFromControllerCheckBox; - color: hifi.colors.white; - colorScheme: hifi.colorSchemes.dark; - anchors.left: parent.left; - anchors.top: switchViewFromControllerCheckBox.bottom; - anchors.topMargin: 4; - text: ""; - labelFontSize: 20; - labelFontWeight: Font.Normal; - boxSize: 24; - onClicked: { - sendToScript({method: 'changeTakeSnapshotFromControllerPreference', params: checked}); - } - } - } - - HifiControlsUit.Button { - text: "Change Snapshot Location"; - colorScheme: hifi.colorSchemes.dark; - color: hifi.buttons.none; - anchors.bottom: spectatorDescriptionContainer.top; - anchors.bottomMargin: 16; - anchors.left: parent.left; - anchors.right: parent.right; - height: 35; - onClicked: { - sendToScript({method: 'openSettings'}); - } - } - - Item { - id: spectatorDescriptionContainer; - // Size - height: childrenRect.height; - // Anchors - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 20; - - // "Spectator" app description text - HifiStylesUit.RalewayRegular { - id: spectatorDescriptionText; - text: "While you're using a VR headset, you can use this app to change what your monitor shows. " + - "Try it when streaming or recording video."; - // Text size - size: 20; - // Size - height: paintedHeight; - // Anchors - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: parent.top; - // Style - color: hifi.colors.white; - wrapMode: Text.Wrap; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // "Learn More" text - HifiStylesUit.RalewayRegular { - id: spectatorLearnMoreText; - text: "Learn More About Spectator"; - // Text size - size: 20; - // Size - width: paintedWidth; - height: paintedHeight; - // Anchors - anchors.top: spectatorDescriptionText.bottom; - anchors.topMargin: 10; - anchors.left: parent.left; - anchors.right: parent.right; - // Style - color: hifi.colors.blueAccent; - wrapMode: Text.WordWrap; - font.underline: true; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - - MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; - onClicked: { - letterbox(hifi.glyphs.question, - "Spectator Camera", - "By default, your monitor shows a preview of what you're seeing in VR. " + - "Using the Spectator Camera app, your monitor can display the view " + - "from a virtual hand-held camera - perfect for taking selfies or filming " + - "your friends!
" + - "

Streaming and Recording

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

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

' + - 'Snapshots taken using Spectator Camera will be saved in your Snapshots Directory - change via Settings -> General.'); - } - onEntered: parent.color = hifi.colors.blueHighlight; - onExited: parent.color = hifi.colors.blueAccent; - } - } - } - } - } - // - // SPECTATOR CONTROLS END - // - - // - // FUNCTION DEFINITIONS START - // - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the SpectatorCamera JavaScript. - // Messages are in format "{method, params}", like json-rpc. - // - // Description: - // Called when a message is received from spectatorCamera.js. - // - function fromScript(message) { - switch (message.method) { - case 'initializeUI': - masterSwitch.checked = message.masterSwitchOn; - flashCheckBox.checked = message.flashCheckboxChecked; - showCameraView.checked = message.monitorShowsCamView; - showHmdPreview.checked = !message.monitorShowsCamView; - root.uiReady = true; - break; - case 'updateMonitorShowsSwitch': - showCameraView.checked = message.params; - showHmdPreview.checked = !message.params; - break; - case 'updateControllerMappingCheckbox': - switchViewFromControllerCheckBox.checked = message.switchViewSetting; - switchViewFromControllerCheckBox.enabled = true; - takeSnapshotFromControllerCheckBox.checked = message.takeSnapshotSetting; - takeSnapshotFromControllerCheckBox.enabled = true; - - if (message.controller === "OculusTouch") { - switchViewFromControllerCheckBox.text = "Left Thumbstick: Switch Monitor View"; - takeSnapshotFromControllerCheckBox.text = "Right Thumbstick: Take Snapshot"; - } else if (message.controller === "Vive") { - switchViewFromControllerCheckBox.text = "Left Thumb Pad: Switch Monitor View"; - takeSnapshotFromControllerCheckBox.text = "Right Thumb Pad: Take Snapshot"; - } else { - switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; - switchViewFromControllerCheckBox.checked = true; - switchViewFromControllerCheckBox.enabled = false; - takeSnapshotFromControllerCheckBox.visible = false; - } - break; - case 'finishedProcessing360Snapshot': - root.processing360Snapshot = false; - break; - case 'startedProcessingStillSnapshot': - root.processingStillSnapshot = true; - break; - case 'finishedProcessingStillSnapshot': - root.processingStillSnapshot = false; - break; - default: - console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); - } - } - signal sendToScript(var message); - - // - // FUNCTION DEFINITIONS END - // -} +// +// SpectatorCamera.qml +// qml/hifi +// +// Spectator Camera v2. +// +// Created by Zach Fox on 2018-12-12 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 + +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit +import "qrc:////qml//controls" as HifiControls +import "qrc:////qml//hifi" as Hifi + +Rectangle { + HifiStylesUit.HifiConstants { id: hifi; } + + id: root; + property bool uiReady: false; + property bool processingStillSnapshot: false; + property bool processing360Snapshot: false; + // Style + color: "#404040"; + + // The letterbox used for popup messages + Hifi.LetterboxMessage { + id: letterboxMessage; + z: 998; // Force the popup on top of everything else + } + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } + + // + // TITLE BAR START + // + Rectangle { + id: titleBarContainer; + // Size + width: root.width; + height: 60; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + color: "#121212"; + + // "Spectator" text + HifiStylesUit.RalewaySemiBold { + id: titleBarText; + text: "Spectator Camera 2.7"; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 30; + width: paintedWidth; + height: parent.height; + size: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + Switch { + id: masterSwitch; + focusPolicy: Qt.ClickFocus; + width: 65; + height: 30; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 30; + hoverEnabled: true; + + onHoveredChanged: { + if (hovered) { + switchHandle.color = hifi.colors.blueHighlight; + } else { + switchHandle.color = hifi.colors.lightGray; + } + } + + onClicked: { + if (!checked) { + flashCheckBox.checked = false; + } + sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); + sendToScript({method: 'updateCameravFoV', vFoV: fieldOfViewSlider.value}); + } + + background: Rectangle { + color: parent.checked ? "#1FC6A6" : hifi.colors.white; + implicitWidth: masterSwitch.width; + implicitHeight: masterSwitch.height; + radius: height/2; + } + + indicator: Rectangle { + id: switchHandle; + implicitWidth: masterSwitch.height - 4; + implicitHeight: implicitWidth; + radius: implicitWidth/2; + border.color: "#E3E3E3"; + color: "#404040"; + x: Math.max(4, Math.min(parent.width - width - 4, parent.visualPosition * parent.width - (width / 2) - 4)) + y: parent.height / 2 - height / 2; + Behavior on x { + enabled: !masterSwitch.down + SmoothedAnimation { velocity: 200 } + } + + } + } + } + // + // TITLE BAR END + // + + Rectangle { + z: 999; + id: processingSnapshot; + anchors.fill: parent; + visible: root.processing360Snapshot || !root.uiReady; + color: Qt.rgba(0.0, 0.0, 0.0, 0.85); + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup/section. + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + propagateComposedEvents: false; + } + + AnimatedImage { + id: processingImage; + source: "processing.gif" + width: 74; + height: width; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + } + + HifiStylesUit.RalewaySemiBold { + text: root.uiReady ? "Processing..." : ""; + // Anchors + anchors.top: processingImage.bottom; + anchors.topMargin: 4; + anchors.horizontalCenter: parent.horizontalCenter; + width: paintedWidth; + // Text size + size: 26; + // Style + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + } + } + + // + // SPECTATOR CONTROLS START + // + Item { + id: spectatorControlsContainer; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + // Instructions or Preview + Rectangle { + id: spectatorCameraImageContainer; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.right: parent.right; + height: 250; + color: masterSwitch.checked ? "transparent" : "black"; + + AnimatedImage { + source: "static.gif" + visible: !masterSwitch.checked; + anchors.fill: parent; + opacity: 0.15; + } + + // Instructions (visible when display texture isn't set) + HifiStylesUit.FiraSansRegular { + id: spectatorCameraInstructions; + text: "Turn on Spectator Camera for a preview\nof " + (HMD.active ? "what your monitor shows." : "the camera's view."); + size: 16; + color: hifi.colors.white; + visible: !masterSwitch.checked; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + HifiStylesUit.FiraSansRegular { + text: ":)"; + size: 28; + color: hifi.colors.white; + visible: root.processing360Snapshot || root.processingStillSnapshot; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // Spectator Camera Preview + Hifi.ResourceImageItem { + id: spectatorCameraPreview; + visible: masterSwitch.checked && !root.processing360Snapshot && !root.processingStillSnapshot; + url: showCameraView.checked || !HMD.active ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; + ready: masterSwitch.checked; + mirrorVertically: true; + anchors.fill: parent; + onVisibleChanged: { + ready = masterSwitch.checked; + update(); + } + } + + Item { + visible: HMD.active; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 40; + + LinearGradient { + anchors.fill: parent; + start: Qt.point(0, 0); + end: Qt.point(0, height); + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.black } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0) } + } + } + + HifiStylesUit.HiFiGlyphs { + id: monitorShowsSwitchLabelGlyph; + text: hifi.glyphs.screen; + size: 32; + color: hifi.colors.white; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + } + HifiStylesUit.RalewayLight { + id: monitorShowsSwitchLabel; + text: "Monitor View:"; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: monitorShowsSwitchLabelGlyph.right; + anchors.leftMargin: 8; + size: 20; + width: paintedWidth; + height: parent.height; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + } + Item { + anchors.left: monitorShowsSwitchLabel.right; + anchors.leftMargin: 14; + anchors.right: parent.right; + anchors.rightMargin: 10; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + + HifiControlsUit.RadioButton { + id: showCameraView; + text: "Camera View"; + width: 125; + anchors.left: parent.left; + anchors.leftMargin: 10; + anchors.verticalCenter: parent.verticalCenter; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (showHmdPreview.checked) { + showHmdPreview.checked = false; + } + if (!showCameraView.checked && !showHmdPreview.checked) { + showCameraView.checked = true; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setMonitorShowsCameraView', params: true}); + } + } + } + + HifiControlsUit.RadioButton { + id: showHmdPreview; + text: "VR Preview"; + anchors.left: showCameraView.right; + anchors.leftMargin: 10; + width: 125; + anchors.verticalCenter: parent.verticalCenter; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (showCameraView.checked) { + showCameraView.checked = false; + } + if (!showCameraView.checked && !showHmdPreview.checked) { + showHmdPreview.checked = true; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setMonitorShowsCameraView', params: false}); + } + } + } + } + } + + HifiStylesUit.HiFiGlyphs { + id: flashGlyph; + visible: flashCheckBox.visible; + text: hifi.glyphs.lightning; + size: 26; + color: hifi.colors.white; + anchors.verticalCenter: flashCheckBox.verticalCenter; + anchors.right: flashCheckBox.left; + anchors.rightMargin: -2; + } + HifiControlsUit.CheckBox { + id: flashCheckBox; + visible: masterSwitch.checked; + color: hifi.colors.white; + colorScheme: hifi.colorSchemes.dark; + anchors.right: takeSnapshotButton.left; + anchors.rightMargin: -8; + anchors.verticalCenter: takeSnapshotButton.verticalCenter; + boxSize: 22; + onClicked: { + sendToScript({method: 'setFlashStatus', enabled: checked}); + } + } + HifiControlsUit.Button { + id: takeSnapshotButton; + enabled: masterSwitch.checked; + text: "SNAP PICTURE"; + colorScheme: hifi.colorSchemes.light; + color: hifi.buttons.white; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.right: take360SnapshotButton.left; + anchors.rightMargin: 12; + width: 135; + height: 35; + onClicked: { + root.processingStillSnapshot = true; + sendToScript({method: 'takeSecondaryCameraSnapshot'}); + } + } + HifiControlsUit.Button { + id: take360SnapshotButton; + enabled: masterSwitch.checked; + text: "SNAP 360"; + colorScheme: hifi.colorSchemes.light; + color: hifi.buttons.white; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 12; + width: 135; + height: 35; + onClicked: { + root.processing360Snapshot = true; + sendToScript({method: 'takeSecondaryCamera360Snapshot'}); + } + } + } + + Item { + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.leftMargin: 26; + anchors.right: parent.right; + anchors.rightMargin: 26; + anchors.bottom: parent.bottom; + + Item { + id: fieldOfView; + visible: masterSwitch.checked; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 35; + + HifiStylesUit.RalewaySemiBold { + id: fieldOfViewLabel; + text: "Field of View (" + fieldOfViewSlider.value + "\u00B0): "; + size: 20; + color: hifi.colors.white; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: 172; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + + HifiControlsUit.Slider { + id: fieldOfViewSlider; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: resetvFoV.left; + anchors.rightMargin: 8; + anchors.left: fieldOfViewLabel.right; + anchors.leftMargin: 8; + colorScheme: hifi.colorSchemes.dark; + from: 10.0; + to: 120.0; + value: 45.0; + stepSize: 1; + + onValueChanged: { + sendToScript({method: 'updateCameravFoV', vFoV: value}); + } + onPressedChanged: { + if (!pressed) { + sendToScript({method: 'updateCameravFoV', vFoV: value}); + } + } + } + + HifiControlsUit.GlyphButton { + id: resetvFoV; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: -8; + height: parent.height - 8; + width: height; + glyph: hifi.glyphs.reload; + onClicked: { + fieldOfViewSlider.value = 45.0; + } + } + } + +//------------------------------------------------------------------------------------------ + Item { + id: toneMap; + visible: masterSwitch.checked; + anchors.top: fieldOfView.bottom; + anchors.topMargin: 18; + anchors.left: parent.left; + anchors.right: parent.right; + height: 35; + + HifiStylesUit.RalewaySemiBold { + id: toneMapLabel; + text: "Tone mapping curve: " + toneMapSlider.value; + size: 20; + color: hifi.colors.white; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: 220; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + + HifiControlsUit.Slider { + id: toneMapSlider; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: resetToneMap.left; + anchors.rightMargin: 8; + anchors.left: toneMapLabel.right; + anchors.leftMargin: 8; + colorScheme: hifi.colorSchemes.dark; + from: 0.0; + to: 3.0; + value: 1.0; + stepSize: 1; + + onValueChanged: { + sendToScript({method: 'updateToneMap', toneCurve: value}); + } + onPressedChanged: { + if (!pressed) { + sendToScript({method: 'updateToneMap', toneCurve: value}); + } + } + } + + HifiControlsUit.GlyphButton { + id: resetToneMap; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: -8; + height: parent.height - 8; + width: height; + glyph: hifi.glyphs.reload; + onClicked: { + toneMapSlider.value = 1.0; + } + } + } +//---------------------------------------------------------------------------------------- + + Item { + visible: HMD.active; + anchors.top: toneMap.bottom; + anchors.topMargin: 18; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + HifiStylesUit.RalewaySemiBold { + id: shortcutsHeaderText; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + text: "Shortcuts"; + size: 20; + color: hifi.colors.white; + } + + // "Switch View From Controller" Checkbox + HifiControlsUit.CheckBox { + id: switchViewFromControllerCheckBox; + color: hifi.colors.white; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: shortcutsHeaderText.bottom; + anchors.topMargin: 8; + text: ""; + labelFontSize: 20; + labelFontWeight: Font.Normal; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); + } + } + + // "Take Snapshot" Checkbox + HifiControlsUit.CheckBox { + id: takeSnapshotFromControllerCheckBox; + color: hifi.colors.white; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: switchViewFromControllerCheckBox.bottom; + anchors.topMargin: 4; + text: ""; + labelFontSize: 20; + labelFontWeight: Font.Normal; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeTakeSnapshotFromControllerPreference', params: checked}); + } + } + } + + HifiControlsUit.Button { + text: "Change Snapshot Location"; + colorScheme: hifi.colorSchemes.dark; + color: hifi.buttons.none; + anchors.bottom: spectatorDescriptionContainer.top; + anchors.bottomMargin: 16; + anchors.left: parent.left; + anchors.right: parent.right; + height: 35; + onClicked: { + sendToScript({method: 'openSettings'}); + } + } + + Item { + id: spectatorDescriptionContainer; + // Size + height: childrenRect.height; + // Anchors + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 20; + + // "Spectator" app description text + HifiStylesUit.RalewayRegular { + id: spectatorDescriptionText; + text: "While you're using a VR headset, you can use this app to change what your monitor shows. " + + "Try it when streaming or recording video."; + // Text size + size: 20; + // Size + height: paintedHeight; + // Anchors + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + // Style + color: hifi.colors.white; + wrapMode: Text.Wrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Learn More" text + HifiStylesUit.RalewayRegular { + id: spectatorLearnMoreText; + text: "Learn More About Spectator"; + // Text size + size: 20; + // Size + width: paintedWidth; + height: paintedHeight; + // Anchors + anchors.top: spectatorDescriptionText.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.right: parent.right; + // Style + color: hifi.colors.blueAccent; + wrapMode: Text.WordWrap; + font.underline: true; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + letterbox(hifi.glyphs.question, + "Spectator Camera", + "By default, your monitor shows a preview of what you're seeing in VR. " + + "Using the Spectator Camera app, your monitor can display the view " + + "from a virtual hand-held camera - perfect for taking selfies or filming " + + "your friends!
" + + "

Streaming and Recording

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

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

' + + 'Snapshots taken using Spectator Camera will be saved in your Snapshots Directory - change via Settings -> General.'); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.blueAccent; + } + } + } + } + } + // + // SPECTATOR CONTROLS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the SpectatorCamera JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from spectatorCamera.js. + // + function fromScript(message) { + switch (message.method) { + case 'initializeUI': + masterSwitch.checked = message.masterSwitchOn; + flashCheckBox.checked = message.flashCheckboxChecked; + showCameraView.checked = message.monitorShowsCamView; + showHmdPreview.checked = !message.monitorShowsCamView; + root.uiReady = true; + break; + case 'updateMonitorShowsSwitch': + showCameraView.checked = message.params; + showHmdPreview.checked = !message.params; + break; + case 'updateControllerMappingCheckbox': + switchViewFromControllerCheckBox.checked = message.switchViewSetting; + switchViewFromControllerCheckBox.enabled = true; + takeSnapshotFromControllerCheckBox.checked = message.takeSnapshotSetting; + takeSnapshotFromControllerCheckBox.enabled = true; + + if (message.controller === "OculusTouch") { + switchViewFromControllerCheckBox.text = "Left Thumbstick: Switch Monitor View"; + takeSnapshotFromControllerCheckBox.text = "Right Thumbstick: Take Snapshot"; + } else if (message.controller === "Vive") { + switchViewFromControllerCheckBox.text = "Left Thumb Pad: Switch Monitor View"; + takeSnapshotFromControllerCheckBox.text = "Right Thumb Pad: Take Snapshot"; + } else { + switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; + switchViewFromControllerCheckBox.checked = true; + switchViewFromControllerCheckBox.enabled = false; + takeSnapshotFromControllerCheckBox.visible = false; + } + break; + case 'finishedProcessing360Snapshot': + root.processing360Snapshot = false; + break; + case 'startedProcessingStillSnapshot': + root.processingStillSnapshot = true; + break; + case 'finishedProcessingStillSnapshot': + root.processingStillSnapshot = false; + break; + default: + console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/applications/spectator-camera/newcam.fbx b/applications/spectator-camera/newcam.fbx new file mode 100644 index 0000000000000000000000000000000000000000..81a2a9d06d964f4343c31de17bf6554243ee6af8 GIT binary patch literal 34028 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 literal 0 HcmV?d00001 diff --git a/applications/spectator-camera/spectatorCamera.js b/applications/spectator-camera/spectatorCamera.js index e7b58a7..f1d95cd 100644 --- a/applications/spectator-camera/spectatorCamera.js +++ b/applications/spectator-camera/spectatorCamera.js @@ -1,748 +1,778 @@ -"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. - // -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 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": 0.95, - "damping": 0.95, - "collidesWith": "static,dynamic,kinematic,", - "collisionMask": 7, - "dynamic": false, - "modelURL": Script.resolvePath("spectator-camera.fbx"), - "name": "Spectator Camera", - "registrationPoint": { - "x": 0.56, - "y": 0.545, - "z": 0.23 - }, - "rotation": cameraRotation, - "position": cameraPosition, - "shapeType": "simple-compound", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true}}", - "isVisibleInSecondaryCamera": false - }, true); - spectatorCameraConfig.attachedEntityId = camera; - updateOverlay(); - if (!HMD.active) { - setMonitorShowsCameraView(false); - } else { - setDisplay(monitorShowsCameraView); - } - // Change button to active when window is first opened OR if the camera is on, false otherwise. - if (button) { - button.editProperties({ isActive: onSpectatorCameraScreen || camera }); - } - Audio.playSound(SOUND_CAMERA_ON, { - volume: 0.15, - position: cameraPosition, - localOnly: true - }); - - // Remove the existing camera model from the domain if one exists. - // It's easy for this to happen if the user crashes while the Spectator Camera is on. - // We do this down here (after the new one is rezzed) so that we don't accidentally delete - // the newly-rezzed model. - var entityIDs = Entities.findEntitiesByName("Spectator Camera", MyAvatar.position, 100, false); - entityIDs.forEach(function (currentEntityID) { - var currentEntityOwner = Entities.getEntityProperties(currentEntityID, ['owningAvatarID']).owningAvatarID; - if (currentEntityOwner === MyAvatar.sessionUUID && currentEntityID !== camera) { - Entities.deleteEntity(currentEntityID); - } - }); - } - - // 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() { - if (flash) { - Entities.deleteEntity(flash); - flash = false; - } - if (camera) { - 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. - var button = false; - var buttonName = "SPECTATOR"; - function addOrRemoveButton(isShuttingDown) { - if (!tablet) { - print("Warning in addOrRemoveButton(): 'tablet' undefined!"); - return; - } - if (!button) { - if (!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 (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); - 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") : ""; - - // 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) { - setDisplay(showCameraView); - monitorShowsCameraView = 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(); - // if secondary camera is currently being used for mirror projection then don't update it's aspect ratio (will be done in spectatorCameraOn) - if (!spectatorCameraConfig.mirrorProjection) { - 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 setSwitchViewControllerMappingStatus(status) { - if (!switchViewControllerMapping) { - return; - } - if (status) { - switchViewControllerMapping.enable(); - } else { - switchViewControllerMapping.disable(); - } - } - function setSwitchViewFromController(setting) { - if (setting === switchViewFromController) { - return; - } - switchViewFromController = setting; - setSwitchViewControllerMappingStatus(switchViewFromController); - Settings.setValue('spectatorCamera/switchViewFromController', setting); - } - - const TAKE_SNAPSHOT_FROM_CONTROLLER_DEFAULT = false; - var takeSnapshotFromController = !!Settings.getValue('spectatorCamera/takeSnapshotFromController', TAKE_SNAPSHOT_FROM_CONTROLLER_DEFAULT); - function setTakeSnapshotControllerMappingStatus(status) { - if (!takeSnapshotControllerMapping) { - return; - } - if (status) { - takeSnapshotControllerMapping.enable(); - } else { - takeSnapshotControllerMapping.disable(); - } - } - function setTakeSnapshotFromController(setting) { - if (setting === takeSnapshotFromController) { - return; - } - takeSnapshotFromController = setting; - setTakeSnapshotControllerMappingStatus(takeSnapshotFromController); - Settings.setValue('spectatorCamera/takeSnapshotFromController', setting); - } - - // Function Name: registerButtonMappings() - // - // Description: - // -Updates controller button mappings for Spectator Camera. - // - // Relevant Variables: - // -switchViewControllerMappingName: The name of the controller mapping. - // -switchViewControllerMapping: The controller mapping itself. - // -takeSnapshotControllerMappingName: The name of the controller mapping. - // -takeSnapshotControllerMapping: The controller mapping itself. - // -controllerType: "OculusTouch", "Vive", "Other". - var switchViewControllerMapping; - var switchViewControllerMappingName = 'Hifi-SpectatorCamera-Mapping-SwitchView'; - function registerSwitchViewControllerMapping() { - switchViewControllerMapping = Controller.newMapping(switchViewControllerMappingName); - if (controllerType === "OculusTouch") { - switchViewControllerMapping.from(Controller.Standard.LS).to(function (value) { - if (value === 1.0) { - setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); - } - return; - }); - } else if (controllerType === "Vive") { - switchViewControllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) { - if (value === 1.0) { - setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); - } - return; - }); - } - } - var takeSnapshotControllerMapping; - var takeSnapshotControllerMappingName = 'Hifi-SpectatorCamera-Mapping-TakeSnapshot'; - - var flash = false; - function setFlashStatus(enabled) { - var cameraPosition = Entities.getEntityProperties(camera, ["positon"]).position; - if (enabled) { - if (camera) { - Audio.playSound(SOUND_FLASH_ON, { - position: cameraPosition, - localOnly: true, - volume: 0.8 - }); - flash = Entities.addEntity({ - "collidesWith": "", - "collisionMask": 0, - "color": { - "blue": 173, - "green": 252, - "red": 255 - }, - "cutoff": 90, - "dimensions": { - "x": 4, - "y": 4, - "z": 4 - }, - "dynamic": false, - "falloffRadius": 0.20000000298023224, - "intensity": 37, - "isSpotlight": true, - "localRotation": { w: 1, x: 0, y: 0, z: 0 }, - "localPosition": { x: 0, y: -0.005, z: -0.08 }, - "name": "Camera Flash", - "type": "Light", - "parentID": camera, - }, true); - } - } else { - if (flash) { - Audio.playSound(SOUND_FLASH_OFF, { - position: cameraPosition, - localOnly: true, - volume: 0.8 - }); - Entities.deleteEntity(flash); - flash = false; - } - } - } - - function onStillSnapshotTaken() { - Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 1; - sendToQml({ - method: 'finishedProcessingStillSnapshot' - }); - } - function maybeTakeSnapshot() { - if (camera) { - sendToQml({ - method: 'startedProcessingStillSnapshot' - }); - - Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 0; - // Wait a moment before taking the snapshot for the tonemapping curve to update - Script.setTimeout(function () { - Audio.playSound(SOUND_SNAPSHOT, { - position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, - localOnly: true, - volume: 1.0 - }); - Window.takeSecondaryCameraSnapshot(); - }, 250); - } else { - sendToQml({ - method: 'finishedProcessingStillSnapshot' - }); - } - } - function on360SnapshotTaken() { - if (monitorShowsCameraView) { - setDisplay(true); - } - sendToQml({ - method: 'finishedProcessing360Snapshot' - }); - } - function maybeTake360Snapshot() { - if (camera) { - Audio.playSound(SOUND_SNAPSHOT, { - position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, - localOnly: true, - volume: 1.0 - }); - if (HMD.active && monitorShowsCameraView) { - setDisplay(false); - } - Window.takeSecondaryCamera360Snapshot(Entities.getEntityProperties(camera, ["positon"]).position); - } - } - function registerTakeSnapshotControllerMapping() { - takeSnapshotControllerMapping = Controller.newMapping(takeSnapshotControllerMappingName); - if (controllerType === "OculusTouch") { - takeSnapshotControllerMapping.from(Controller.Standard.RS).to(function (value) { - if (value === 1.0) { - maybeTakeSnapshot(); - } - return; - }); - } else if (controllerType === "Vive") { - takeSnapshotControllerMapping.from(Controller.Standard.RightPrimaryThumb).to(function (value) { - if (value === 1.0) { - maybeTakeSnapshot(); - } - return; - }); - } - } - var controllerType = "Other"; - function registerButtonMappings() { - var VRDevices = Controller.getDeviceNames().toString(); - if (VRDevices) { - if (VRDevices.indexOf("Vive") !== -1) { - controllerType = "Vive"; - } else if (VRDevices.indexOf("OculusTouch") !== -1) { - controllerType = "OculusTouch"; - } else { - sendToQml({ - method: 'updateControllerMappingCheckbox', - switchViewSetting: switchViewFromController, - takeSnapshotSetting: takeSnapshotFromController, - controller: controllerType - }); - return; // Neither Vive nor Touch detected - } - } - - if (!switchViewControllerMapping) { - registerSwitchViewControllerMapping(); - } - setSwitchViewControllerMappingStatus(switchViewFromController); - - if (!takeSnapshotControllerMapping) { - registerTakeSnapshotControllerMapping(); - } - setTakeSnapshotControllerMappingStatus(switchViewFromController); - - sendToQml({ - method: 'updateControllerMappingCheckbox', - switchViewSetting: switchViewFromController, - takeSnapshotSetting: takeSnapshotFromController, - 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.resolvePath("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); - } - } - - function updateSpectatorCameraQML() { - sendToQml({ method: 'initializeUI', masterSwitchOn: !!camera, flashCheckboxChecked: !!flash, monitorShowsCamView: monitorShowsCameraView }); - registerButtonMappings(); - Menu.setIsOptionChecked("Disable Preview", false); - Menu.setIsOptionChecked("Mono Preview", true); - } - - var signalsWired = false; - function wireSignals(shouldWire) { - if (signalsWired === shouldWire) { - return; - } - - signalsWired = shouldWire; - - if (shouldWire) { - Window.stillSnapshotTaken.connect(onStillSnapshotTaken); - Window.snapshot360Taken.connect(on360SnapshotTaken); - } else { - Window.stillSnapshotTaken.disconnect(onStillSnapshotTaken); - Window.snapshot360Taken.disconnect(on360SnapshotTaken); - } - } - - // 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 }); - } - - // In the case of a remote QML app, it takes a bit of time - // for the event bridge to actually connect, so we have to wait... - Script.setTimeout(function () { - if (onSpectatorCameraScreen) { - updateSpectatorCameraQML(); - } - }, 700); - - wireSignals(onSpectatorCameraScreen); - } - - // 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) { - if (onSpectatorCameraScreen) { - 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; - case 'changeTakeSnapshotFromControllerPreference': - setTakeSnapshotFromController(message.params); - break; - case 'updateCameravFoV': - spectatorCameraConfig.vFoV = message.vFoV; - break; - case 'setFlashStatus': - setFlashStatus(message.enabled); - break; - case 'takeSecondaryCameraSnapshot': - maybeTakeSnapshot(); - break; - case 'takeSecondaryCamera360Snapshot': - maybeTake360Snapshot(); - break; - case 'openSettings': - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { - Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); - } else { - tablet.pushOntoStack("hifi/tablet/TabletGeneralPreferences.qml"); - } - break; - default: - print('Unrecognized message from SpectatorCamera.qml:', JSON.stringify(message)); - } - } - - // 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) { - registerButtonMappings(); - if (!isHMDMode) { - setMonitorShowsCameraView(false); - } else { - setDisplay(monitorShowsCameraView); - } - } - - // 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); - wireSignals(false); - addOrRemoveButton(true); - if (tablet) { - tablet.screenChanged.disconnect(onTabletScreenChanged); - if (onSpectatorCameraScreen) { - tablet.gotoHomeScreen(); - } - } - HMD.displayModeChanged.disconnect(onHMDChanged); - Controller.keyPressEvent.disconnect(keyPressEvent); - if (switchViewControllerMapping) { - switchViewControllerMapping.disable(); - } - if (takeSnapshotControllerMapping) { - takeSnapshotControllerMapping.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 SOUND_CAMERA_ON = SoundCache.getSound(Script.resolvePath("cameraOn.wav")); - var SOUND_SNAPSHOT = SoundCache.getSound(Script.resolvePath("snap.wav")); - var SOUND_FLASH_ON = SoundCache.getSound(Script.resolvePath("flashOn.wav")); - var SOUND_FLASH_OFF = SoundCache.getSound(Script.resolvePath("flashOff.wav")); - startup(); - Script.scriptEnding.connect(shutdown); - -}()); // END LOCAL_SCOPE \ No newline at end of file +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Script, */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// spectatorCamera.js 2.7 +// +// Created by Zach Fox on 2017-06-05 +// Copyright 2017 High Fidelity, Inc +// +// modified by Silverfish on 2020-01-22 +// +// +// 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 + // -viewFinderLocalEntity: The in-world local entity that displays the spectator camera's view. + // -camera: The in-world entity that corresponds to the spectator camera. + // -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 viewFinderLocalEntity. + // -viewFinderLocalEntityDim: The x, y, and z dimensions of the viewFinderLocalEntity. + // -camera: The camera model which is grabbable. + // -viewFinderLocalEntity: The preview of what the spectator camera is viewing, placed inside the glass pane. + var spectatorCameraConfig = Render.getConfig("SecondaryCamera"); + var viewFinderLocalEntity = false; + var camera = false; + var cameraRotation; + var cameraPosition; + //var glassPaneWidth = 0.30; + var cameraViewWidth = 0.25; + // var cameraViewHeight = 0.2; + var cameraViewAspect = 16/9; + var toneCurve = 1; + var cameraName = "Spectator Camera"; + + var viewFinderLocalEntityDim = { x: cameraViewWidth, y: cameraViewWidth, z: 0 }; + function spectatorCameraOn() { + Render.getConfig("SecondaryCameraJob.ToneMapping").curve = toneCurve; + + // 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": 0.95, + "damping": 0.95, + "collidesWith": "static,dynamic,kinematic,", + "collisionMask": 7, + "dynamic": false, + "modelURL": Script.resolvePath("newcam.fbx"), + "name": cameraName, + "registrationPoint": { + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "rotation": cameraRotation, + "position": cameraPosition, + "shapeType": "simple-hull", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true}}", + "isVisibleInSecondaryCamera": false + }, true); + spectatorCameraConfig.attachedEntityId = camera; + createLocalEntity(); + if (!HMD.active) { + setMonitorShowsCameraView(false); + } else { + setDisplay(monitorShowsCameraView); + } + // Change button to active when window is first opened OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + Audio.playSound(SOUND_CAMERA_ON, { + volume: 0.15, + position: cameraPosition, + localOnly: true + }); + + // Remove the existing camera model from the domain if one exists. + // It's easy for this to happen if the user crashes while the Spectator Camera is on. + // We do this down here (after the new one is rezzed) so that we don't accidentally delete + // the newly-rezzed model. + var entityIDs = Entities.findEntitiesByName(cameraName, MyAvatar.position, 100, false); + entityIDs.forEach(function (currentEntityID) { + var currentEntityOwner = Entities.getEntityProperties(currentEntityID, ['owningAvatarID']).owningAvatarID; + if (currentEntityOwner === MyAvatar.sessionUUID && currentEntityID !== camera) { + Entities.deleteEntity(currentEntityID); + } + }); + } + + // 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() { + if (flash) { + Entities.deleteEntity(flash); + flash = false; + } + if (camera) { + 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 (viewFinderLocalEntity) { + Entities.deleteEntity(viewFinderLocalEntity); + } + viewFinderLocalEntity = 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. + var button = false; + var buttonName = "SPECTATOR"; + function addOrRemoveButton(isShuttingDown) { + if (!tablet) { + print("Warning in addOrRemoveButton(): 'tablet' undefined!"); + return; + } + if (!button) { + if (!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 (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); + tablet.screenChanged.connect(onTabletScreenChanged); + Window.domainChanged.connect(onDomainChanged); + Window.geometryChanged.connect(resizeViewFinderLocalEntity); + Controller.keyPressEvent.connect(keyPressEvent); + HMD.displayModeChanged.connect(onHMDChanged); + viewFinderLocalEntity = 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") : ""; + + // 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) { + setDisplay(showCameraView); + monitorShowsCameraView = 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 createLocalEntity() { + if (viewFinderLocalEntity) { + Entities.deleteEntity(viewFinderLocalEntity); + } + viewFinderLocalEntity = Entities.addEntity({ + type: "Image", + name: "viewFinderLocalEntity", + imageURL: "resource://spectatorCameraFrame", + grab: {grabbable: false}, + emissive: true, + parentID: camera, + alpha: 1, + localRotation: Quat.fromPitchYawRollDegrees(0, 180, 180), + localPosition: { x: 0.01, y: -0.0, z: 0.0 }, + dimensions: viewFinderLocalEntityDim, + isVisibleInSecondaryCamera: false, + ignorePickIntersection: true + }); //"local"); + var windowGeometry = []; + windowGeometry.width = Window.innerWidth; + windowGeometry.height = Window.innerHeight; + resizeViewFinderLocalEntity(windowGeometry); + } + + // Function Name: resizeViewFinderLocalEntity() + // + // Description: + // -A function called when the window is moved/resized, which changes the viewFinderLocalEntity'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 viewFinderLocalEntity should be scaled if the window size is vertical. + // -squareScale: The amount the viewFinderLocalEntity should be scaled if the window size is not vertical but is more square than the + // glass pane's aspect ratio. + + function resizeViewFinderLocalEntity(geometryChanged) { + //var glassPaneRatio = 16 / 9; + //var verticalScale = 1 / glassPaneRatio; + //var squareScale = verticalScale * (1 + (1 - (1 / (geometryChanged.width / geometryChanged.height)))); + + var windowAspect = geometryChanged.width / geometryChanged.height; + var cameraViewHeight = cameraViewWidth * 1/(cameraViewAspect) + + if (windowAspect <= 1) { //window is more square than viewfinder + //print("portrait"); + viewFinderLocalEntityDim = { x: (cameraViewHeight), y: cameraViewHeight, z: 0 }; + } else if ((windowAspect > 1) && (windowAspect < cameraViewAspect)) {//landscape but less than viewfinder aspect + //print("landscape"); + viewFinderLocalEntityDim = { x: (cameraViewHeight * windowAspect), y: (cameraViewHeight * windowAspect), z: 0 }; + }else{ + //print("widescreen"); + viewFinderLocalEntityDim = { x: (cameraViewWidth), y: (cameraViewWidth), z: 0 }; + } + + Entities.editEntity(viewFinderLocalEntity, {dimensions: viewFinderLocalEntityDim}); + + // if secondary camera is currently being used for mirror projection then don't update it's aspect ratio (will be done in spectatorCameraOn) + if (!spectatorCameraConfig.mirrorProjection) { + 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 setSwitchViewControllerMappingStatus(status) { + if (!switchViewControllerMapping) { + return; + } + if (status) { + switchViewControllerMapping.enable(); + } else { + switchViewControllerMapping.disable(); + } + } + function setSwitchViewFromController(setting) { + if (setting === switchViewFromController) { + return; + } + switchViewFromController = setting; + setSwitchViewControllerMappingStatus(switchViewFromController); + Settings.setValue('spectatorCamera/switchViewFromController', setting); + } + + const TAKE_SNAPSHOT_FROM_CONTROLLER_DEFAULT = false; + var takeSnapshotFromController = !!Settings.getValue('spectatorCamera/takeSnapshotFromController', TAKE_SNAPSHOT_FROM_CONTROLLER_DEFAULT); + function setTakeSnapshotControllerMappingStatus(status) { + if (!takeSnapshotControllerMapping) { + return; + } + if (status) { + takeSnapshotControllerMapping.enable(); + } else { + takeSnapshotControllerMapping.disable(); + } + } + function setTakeSnapshotFromController(setting) { + if (setting === takeSnapshotFromController) { + return; + } + takeSnapshotFromController = setting; + setTakeSnapshotControllerMappingStatus(takeSnapshotFromController); + Settings.setValue('spectatorCamera/takeSnapshotFromController', setting); + } + + // Function Name: registerButtonMappings() + // + // Description: + // -Updates controller button mappings for Spectator Camera. + // + // Relevant Variables: + // -switchViewControllerMappingName: The name of the controller mapping. + // -switchViewControllerMapping: The controller mapping itself. + // -takeSnapshotControllerMappingName: The name of the controller mapping. + // -takeSnapshotControllerMapping: The controller mapping itself. + // -controllerType: "OculusTouch", "Vive", "Other". + var switchViewControllerMapping; + var switchViewControllerMappingName = 'Hifi-SpectatorCamera-Mapping-SwitchView'; + function registerSwitchViewControllerMapping() { + switchViewControllerMapping = Controller.newMapping(switchViewControllerMappingName); + if (controllerType === "OculusTouch") { + switchViewControllerMapping.from(Controller.Standard.LS).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } else if (controllerType === "Vive") { + switchViewControllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } + } + var takeSnapshotControllerMapping; + var takeSnapshotControllerMappingName = 'Hifi-SpectatorCamera-Mapping-TakeSnapshot'; + + var flash = false; + function setFlashStatus(enabled) { + var cameraPosition = Entities.getEntityProperties(camera, ["positon"]).position; + if (enabled) { + if (camera) { + Audio.playSound(SOUND_FLASH_ON, { + position: cameraPosition, + localOnly: true, + volume: 0.8 + }); + flash = Entities.addEntity({ + "collidesWith": "", + "collisionMask": 0, + "color": { + "blue": 173, + "green": 252, + "red": 255 + }, + "cutoff": 90, + "dimensions": { + "x": 4, + "y": 4, + "z": 4 + }, + "dynamic": false, + "falloffRadius": 0.20000000298023224, + "intensity": 37, + "isSpotlight": true, + "localRotation": { w: 1, x: 0, y: 0, z: 0 }, + "localPosition": { x: 0, y: -0.005, z: -0.08 }, + "name": "Camera Flash", + "type": "Light", + "parentID": camera, + }, true); + } + } else { + if (flash) { + Audio.playSound(SOUND_FLASH_OFF, { + position: cameraPosition, + localOnly: true, + volume: 0.8 + }); + Entities.deleteEntity(flash); + flash = false; + } + } + } + + function onStillSnapshotTaken() { + Render.getConfig("SecondaryCameraJob.ToneMapping").curve = toneCurve; + sendToQml({ + method: 'finishedProcessingStillSnapshot' + }); + } + function maybeTakeSnapshot() { + if (camera) { + sendToQml({ + method: 'startedProcessingStillSnapshot' + }); + + // Wait a moment before taking the snapshot for the tonemapping curve to update + Script.setTimeout(function () { + Audio.playSound(SOUND_SNAPSHOT, { + position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, + localOnly: true, + volume: 1.0 + }); + Window.takeSecondaryCameraSnapshot(); + }, 250); + } else { + sendToQml({ + method: 'finishedProcessingStillSnapshot' + }); + } + } + function on360SnapshotTaken() { + if (monitorShowsCameraView) { + setDisplay(true); + } + sendToQml({ + method: 'finishedProcessing360Snapshot' + }); + } + function maybeTake360Snapshot() { + if (camera) { + Audio.playSound(SOUND_SNAPSHOT, { + position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, + localOnly: true, + volume: 1.0 + }); + if (HMD.active && monitorShowsCameraView) { + setDisplay(false); + } + Window.takeSecondaryCamera360Snapshot(Entities.getEntityProperties(camera, ["positon"]).position); + } + } + function registerTakeSnapshotControllerMapping() { + takeSnapshotControllerMapping = Controller.newMapping(takeSnapshotControllerMappingName); + if (controllerType === "OculusTouch") { + takeSnapshotControllerMapping.from(Controller.Standard.RS).to(function (value) { + if (value === 1.0) { + maybeTakeSnapshot(); + } + return; + }); + } else if (controllerType === "Vive") { + takeSnapshotControllerMapping.from(Controller.Standard.RightPrimaryThumb).to(function (value) { + if (value === 1.0) { + maybeTakeSnapshot(); + } + return; + }); + } + } + var controllerType = "Other"; + function registerButtonMappings() { + var VRDevices = Controller.getDeviceNames().toString(); + if (VRDevices) { + if (VRDevices.indexOf("Vive") !== -1) { + controllerType = "Vive"; + } else if (VRDevices.indexOf("OculusTouch") !== -1) { + controllerType = "OculusTouch"; + } else { + sendToQml({ + method: 'updateControllerMappingCheckbox', + switchViewSetting: switchViewFromController, + takeSnapshotSetting: takeSnapshotFromController, + controller: controllerType + }); + return; // Neither Vive nor Touch detected + } + } + + if (!switchViewControllerMapping) { + registerSwitchViewControllerMapping(); + } + setSwitchViewControllerMappingStatus(switchViewFromController); + + if (!takeSnapshotControllerMapping) { + registerTakeSnapshotControllerMapping(); + } + setTakeSnapshotControllerMappingStatus(switchViewFromController); + + sendToQml({ + method: 'updateControllerMappingCheckbox', + switchViewSetting: switchViewFromController, + takeSnapshotSetting: takeSnapshotFromController, + 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.resolvePath("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); + } + } + + function updateSpectatorCameraQML() { + sendToQml({ method: 'initializeUI', masterSwitchOn: !!camera, flashCheckboxChecked: !!flash, monitorShowsCamView: monitorShowsCameraView }); + registerButtonMappings(); + Menu.setIsOptionChecked("Disable Preview", false); + Menu.setIsOptionChecked("Mono Preview", true); + } + + var signalsWired = false; + function wireSignals(shouldWire) { + if (signalsWired === shouldWire) { + return; + } + + signalsWired = shouldWire; + + if (shouldWire) { + Window.stillSnapshotTaken.connect(onStillSnapshotTaken); + Window.snapshot360Taken.connect(on360SnapshotTaken); + } else { + Window.stillSnapshotTaken.disconnect(onStillSnapshotTaken); + Window.snapshot360Taken.disconnect(on360SnapshotTaken); + } + } + + // 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 }); + } + + // In the case of a remote QML app, it takes a bit of time + // for the event bridge to actually connect, so we have to wait... + Script.setTimeout(function () { + if (onSpectatorCameraScreen) { + updateSpectatorCameraQML(); + } + }, 700); + + wireSignals(onSpectatorCameraScreen); + } + + // 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) { + if (onSpectatorCameraScreen) { + 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; + case 'changeTakeSnapshotFromControllerPreference': + setTakeSnapshotFromController(message.params); + break; + case 'updateCameravFoV': + spectatorCameraConfig.vFoV = message.vFoV; + break; + case 'updateToneMap': + toneCurve = message.toneCurve; + Render.getConfig("SecondaryCameraJob.ToneMapping").curve = toneCurve; + break; + case 'setFlashStatus': + setFlashStatus(message.enabled); + break; + case 'takeSecondaryCameraSnapshot': + maybeTakeSnapshot(); + break; + case 'takeSecondaryCamera360Snapshot': + maybeTake360Snapshot(); + break; + case 'openSettings': + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { + Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); + } else { + tablet.pushOntoStack("hifi/tablet/TabletGeneralPreferences.qml"); + } + break; + default: + print('Unrecognized message from SpectatorCamera.qml:', JSON.stringify(message)); + } + } + + // 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) { + registerButtonMappings(); + if (!isHMDMode) { + setMonitorShowsCameraView(false); + } else { + setDisplay(monitorShowsCameraView); + } + } + + // 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(resizeViewFinderLocalEntity); + wireSignals(false); + addOrRemoveButton(true); + if (tablet) { + tablet.screenChanged.disconnect(onTabletScreenChanged); + if (onSpectatorCameraScreen) { + tablet.gotoHomeScreen(); + } + } + HMD.displayModeChanged.disconnect(onHMDChanged); + Controller.keyPressEvent.disconnect(keyPressEvent); + if (switchViewControllerMapping) { + switchViewControllerMapping.disable(); + } + if (takeSnapshotControllerMapping) { + takeSnapshotControllerMapping.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 SOUND_CAMERA_ON = SoundCache.getSound(Script.resolvePath("cameraOn.wav")); + var SOUND_SNAPSHOT = SoundCache.getSound(Script.resolvePath("snap.wav")); + var SOUND_FLASH_ON = SoundCache.getSound(Script.resolvePath("flashOn.wav")); + var SOUND_FLASH_OFF = SoundCache.getSound(Script.resolvePath("flashOff.wav")); + startup(); + Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE From 6f3b4130987e480dcfb4f5e106a12d5539eded58 Mon Sep 17 00:00:00 2001 From: Keb Helion <60008426+KebHelion@users.noreply.github.com> Date: Wed, 15 Apr 2020 18:59:57 -0400 Subject: [PATCH 2/5] Activate Spect. Camera app and update description Activate Spectator Camera app and update the description to include instuction about QML Whithlist configuration required to have this app working. --- applications/metadata.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/metadata.js b/applications/metadata.js index 1bd1f54..3d1d9cc 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -9,10 +9,10 @@ var metadata = { "applications": [ "caption": "MIRROR" }, { - "isActive": false, + "isActive": true, "directory": "spectator-camera", "name": "Spectator Camera", - "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). Definitely a must.", + "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). A precious tool to generate coherent Ambient Sources for your Zone Entities. (REQUIRE A QML WHITELIST SETUP TO WORK: 1- In menu: \"Setting > Entity Script \/ QML Whitelist\". 2- Enable it. 3- Add this url to your whitelist: \"https:\/\/kasenvr.github.io\/community-apps\/applications\/spectator-camera\/SpectatorCamera.qml\". 4- Click \"Save Changes\".)", "jsfile": "spectator-camera/spectatorCamera.js", "icon": "spectator-camera/spectator-i.svg", "caption": "SPECTATOR" From c01d41cd3643fd9684b67065e13d8a65de71da05 Mon Sep 17 00:00:00 2001 From: Keb Helion <60008426+KebHelion@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:24:25 -0400 Subject: [PATCH 3/5] Text adjustments Text adjustments --- applications/metadata.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/metadata.js b/applications/metadata.js index 3d1d9cc..6a9263d 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -12,7 +12,7 @@ var metadata = { "applications": [ "isActive": true, "directory": "spectator-camera", "name": "Spectator Camera", - "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). A precious tool to generate coherent Ambient Sources for your Zone Entities. (REQUIRE A QML WHITELIST SETUP TO WORK: 1- In menu: \"Setting > Entity Script \/ QML Whitelist\". 2- Enable it. 3- Add this url to your whitelist: \"https:\/\/kasenvr.github.io\/community-apps\/applications\/spectator-camera\/SpectatorCamera.qml\". 4- Click \"Save Changes\".)", + "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). A precious tool to generate coherent Ambient Sources for your Zone Entities. (REQUIRE A QML WHITELIST SETUP TO WORK: 1- Go to menu \"Setting > Entity Script \/ QML Whitelist\". 2- Enable the whitelist. 3- Add this url to your whitelist: \"https:\/\/kasenvr.github.io\/community-apps\/applications\/spectator-camera\/SpectatorCamera.qml\". 4- Click \"Save Changes\".)", "jsfile": "spectator-camera/spectatorCamera.js", "icon": "spectator-camera/spectator-i.svg", "caption": "SPECTATOR" From 737f4af5fe59e69b42f7e28f6a0d12d3f06f7b14 Mon Sep 17 00:00:00 2001 From: Keb Helion <60008426+KebHelion@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:37:12 -0400 Subject: [PATCH 4/5] Adjustments Adjustments --- applications/metadata.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/metadata.js b/applications/metadata.js index 6a9263d..c276680 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -12,7 +12,7 @@ var metadata = { "applications": [ "isActive": true, "directory": "spectator-camera", "name": "Spectator Camera", - "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). A precious tool to generate coherent Ambient Sources for your Zone Entities. (REQUIRE A QML WHITELIST SETUP TO WORK: 1- Go to menu \"Setting > Entity Script \/ QML Whitelist\". 2- Enable the whitelist. 3- Add this url to your whitelist: \"https:\/\/kasenvr.github.io\/community-apps\/applications\/spectator-camera\/SpectatorCamera.qml\". 4- Click \"Save Changes\".)", + "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). A precious tool to generate coherent Ambient Sources for your Zone Entities. (Require a QML Whitelist configuration to work: 1- Go to menu \"Setting > Entity Script \/ QML Whitelist\". 2- Enable the whitelist. 3- Add this url to your whitelist: \"https:\/\/kasenvr.github.io\/community-apps\/applications\/spectator-camera\/SpectatorCamera.qml\". 4- Click \"Save Changes\".)", "jsfile": "spectator-camera/spectatorCamera.js", "icon": "spectator-camera/spectator-i.svg", "caption": "SPECTATOR" From fafde8af0aa956efdb1600bb876169754b82029d Mon Sep 17 00:00:00 2001 From: Keb Helion <60008426+KebHelion@users.noreply.github.com> Date: Thu, 16 Apr 2020 21:47:38 -0400 Subject: [PATCH 5/5] Spectator Camera description update Reverting the original description since the repository will be whitelisted by default. --- applications/metadata.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/metadata.js b/applications/metadata.js index c276680..50b1e6e 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -12,7 +12,7 @@ var metadata = { "applications": [ "isActive": true, "directory": "spectator-camera", "name": "Spectator Camera", - "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). A precious tool to generate coherent Ambient Sources for your Zone Entities. (Require a QML Whitelist configuration to work: 1- Go to menu \"Setting > Entity Script \/ QML Whitelist\". 2- Enable the whitelist. 3- Add this url to your whitelist: \"https:\/\/kasenvr.github.io\/community-apps\/applications\/spectator-camera\/SpectatorCamera.qml\". 4- Click \"Save Changes\".)", + "description": "Give you a video camera that can display its image on your monitor screen for video capture. It can capture from the camera or from the VR Headset. It can also take classic and spherical 360 snapshots (equirectangular format). Definitely a must.", "jsfile": "spectator-camera/spectatorCamera.js", "icon": "spectator-camera/spectator-i.svg", "caption": "SPECTATOR"