diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index d9a399c162..88897a0fed 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -754,13 +754,13 @@ void Agent::processAgentAvatarAudio() { const int16_t* nextSoundOutput = NULL; if (_avatarSound) { - const QByteArray& soundByteArray = _avatarSound->getByteArray(); - nextSoundOutput = reinterpret_cast(soundByteArray.data() + auto audioData = _avatarSound->getAudioData(); + nextSoundOutput = reinterpret_cast(audioData->rawData() + _numAvatarSoundSentBytes); - int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL + int numAvailableBytes = (audioData->getNumBytes() - _numAvatarSoundSentBytes) > AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL ? AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL - : soundByteArray.size() - _numAvatarSoundSentBytes; + : audioData->getNumBytes() - _numAvatarSoundSentBytes; numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t); @@ -773,7 +773,7 @@ void Agent::processAgentAvatarAudio() { } _numAvatarSoundSentBytes += numAvailableBytes; - if (_numAvatarSoundSentBytes == soundByteArray.size()) { + if (_numAvatarSoundSentBytes == (int)audioData->getNumBytes()) { // we're done with this sound object - so set our pointer back to NULL // and our sent bytes back to zero _avatarSound.clear(); diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index d6f893c42e..e8568a7ff3 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -488,11 +488,8 @@ void AudioMixer::throttle(chrono::microseconds duration, int frame) { // target different mix and backoff ratios (they also have different backoff rates) // this is to prevent oscillation, and encourage throttling to find a steady state - const float TARGET = 0.9f; - // on a "regular" machine with 100 avatars, this is the largest value where - // - overthrottling can be recovered - // - oscillations will not occur after the recovery - const float BACKOFF_TARGET = 0.44f; + const float TARGET = _throttleStartTarget; + const float BACKOFF_TARGET = _throttleBackoffTarget; // the mixer is known to struggle at about 80 on a "regular" machine // so throttle 2/80 the streams to ensure smooth audio (throttling is linear) @@ -551,6 +548,24 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) { _slavePool.setNumThreads(numThreads); } } + + const QString THROTTLE_START_KEY = "throttle_start"; + const QString THROTTLE_BACKOFF_KEY = "throttle_backoff"; + + float settingsThrottleStart = audioThreadingGroupObject[THROTTLE_START_KEY].toDouble(_throttleStartTarget); + float settingsThrottleBackoff = audioThreadingGroupObject[THROTTLE_BACKOFF_KEY].toDouble(_throttleBackoffTarget); + + if (settingsThrottleBackoff > settingsThrottleStart) { + qCWarning(audio) << "Throttle backoff target cannot be higher than throttle start target. Using default values."; + } else if (settingsThrottleBackoff < 0.0f || settingsThrottleStart > 1.0f) { + qCWarning(audio) << "Throttle start and backoff targets must be greater than or equal to 0.0" + << "and lesser than or equal to 1.0. Using default values."; + } else { + _throttleStartTarget = settingsThrottleStart; + _throttleBackoffTarget = settingsThrottleBackoff; + } + + qCDebug(audio) << "Throttle Start:" << _throttleStartTarget << "Throttle Backoff:" << _throttleBackoffTarget; } if (settingsObject.contains(AUDIO_BUFFER_GROUP_KEY)) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index b8ea0d5c58..c7726d2fea 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -144,11 +144,13 @@ private: static std::map _availableCodecs; static QStringList _codecPreferenceOrder; - static std::vector _audioZones; static std::vector _zoneSettings; static std::vector _zoneReverbSettings; + float _throttleStartTarget = 0.9f; + float _throttleBackoffTarget = 0.44f; + AudioMixerSlave::SharedData _workerSharedData; }; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 5d1f725ab4..49023c9af8 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1012,6 +1012,24 @@ "placeholder": "1", "default": "1", "advanced": true + }, + { + "name": "throttle_start", + "type": "double", + "label": "Throttle Start Target", + "help": "Target percentage of frame time to start throttling", + "placeholder": "0.9", + "default": 0.9, + "advanced": true + }, + { + "name": "throttle_backoff", + "type": "double", + "label": "Throttle Backoff Target", + "help": "Target percentage of frame time to backoff throttling", + "placeholder": "0.44", + "default": 0.44, + "advanced": true } ] }, diff --git a/interface/resources/html/tabletHelp.html b/interface/resources/html/tabletHelp.html index 279213bbcb..dd7931d0ed 100644 --- a/interface/resources/html/tabletHelp.html +++ b/interface/resources/html/tabletHelp.html @@ -66,7 +66,7 @@ diff --git a/interface/resources/qml/controlsUit/SpinBox.qml b/interface/resources/qml/controlsUit/SpinBox.qml index d24c7c5e8c..34794d80c7 100644 --- a/interface/resources/qml/controlsUit/SpinBox.qml +++ b/interface/resources/qml/controlsUit/SpinBox.qml @@ -83,8 +83,10 @@ SpinBox { } validator: DoubleValidator { - bottom: Math.min(spinBox.from, spinBox.to) - top: Math.max(spinBox.from, spinBox.to) + decimals: spinBox.decimals + bottom: Math.min(spinBox.realFrom, spinBox.realTo) + top: Math.max(spinBox.realFrom, spinBox.realTo) + notation: DoubleValidator.StandardNotation } textFromValue: function(value, locale) { @@ -97,20 +99,37 @@ SpinBox { contentItem: TextInput { + id: spinboxText z: 2 color: isLightColorScheme ? (spinBox.activeFocus ? hifi.colors.black : hifi.colors.lightGray) : (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGrayText) selectedTextColor: hifi.colors.black selectionColor: hifi.colors.primaryHighlight - text: spinBox.textFromValue(spinBox.value, spinBox.locale) + suffix + text: spinBox.textFromValue(spinBox.value, spinBox.locale) inputMethodHints: spinBox.inputMethodHints validator: spinBox.validator verticalAlignment: Qt.AlignVCenter leftPadding: spinBoxLabelInside.visible ? 30 : hifi.dimensions.textPadding - //rightPadding: hifi.dimensions.spinnerSize width: spinBox.width - hifi.dimensions.spinnerSize onEditingFinished: spinBox.editingFinished() + + Text { + id: suffixText + x: metrics.advanceWidth(spinboxText.text + '*') + height: spinboxText.height + + FontMetrics { + id: metrics + font: spinboxText.font + } + + color: isLightColorScheme + ? (spinBox.activeFocus ? hifi.colors.black : hifi.colors.lightGray) + : (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGrayText) + text: suffix + verticalAlignment: Qt.AlignVCenter + } } up.indicator: Item { diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 6c6fc65be2..238c26236f 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -49,7 +49,6 @@ Item { property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); property int shadowHeight: 10; property bool hovered: false - property bool scrolling: false HifiConstants { id: hifi } @@ -238,31 +237,38 @@ Item { property var unhoverThunk: function () { }; Rectangle { anchors.fill: parent - visible: root.hovered && !root.scrolling + visible: root.hovered color: "transparent" border.width: 4 border.color: hifiStyleConstants.colors.primaryHighlight z: 1 } MouseArea { - anchors.fill: parent; - acceptedButtons: Qt.LeftButton; + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onContainsMouseChanged: { + // Use onContainsMouseChanged rather than onEntered and onExited because the latter aren't always + // triggered correctly - e.g., if drag rightwards from right hand side of a card to the next card + // onExited doesn't fire, in which case can end up with two cards highlighted. + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + hoverThunk(); + } else { + unhoverThunk(); + } + } + } + MouseArea { + // Separate MouseArea for click handling so that it doesn't interfere with hovering and interaction + // with containing ListView. + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: false onClicked: { Tablet.playSound(TabletEnums.ButtonClick); goFunction("hifi://" + hifiUrl); } - hoverEnabled: true; - onEntered: { - Tablet.playSound(TabletEnums.ButtonHover); - hoverThunk(); - } - onExited: unhoverThunk(); - onCanceled: unhoverThunk(); - } - MouseArea { - // This second mouse area causes onEntered to fire on the first if you scroll just a little and the cursor stays on - // the original card. I.e., the original card is re-highlighted if the cursor is on it after scrolling finishes. - anchors.fill: parent } StateImage { id: actionIcon; diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 6928fc6f02..1e89971938 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -141,7 +141,6 @@ Column { textSizeSmall: root.textSizeSmall; stackShadowNarrowing: root.stackShadowNarrowing; shadowHeight: root.stackedCardShadowHeight; - scrolling: scroll.moving hoverThunk: function () { hovered = true; diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index b754cb06ab..5c52af1c05 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -256,7 +256,7 @@ Rectangle { color: hifi.colors.baseGrayHighlight; HifiStylesUit.RalewaySemiBold { - text: "Wallet"; + text: "Secure Transactions"; anchors.fill: parent; anchors.leftMargin: 20; color: hifi.colors.white; @@ -287,7 +287,7 @@ Rectangle { HifiStylesUit.RalewaySemiBold { id: securityPictureText; - text: "Wallet Security Picture"; + text: "Security Picture"; // Anchors anchors.top: parent.top; anchors.bottom: parent.bottom; diff --git a/interface/resources/qml/hifi/tablet/ControllerSettings.qml b/interface/resources/qml/hifi/tablet/ControllerSettings.qml index b8bbd71f33..6727047eb0 100644 --- a/interface/resources/qml/hifi/tablet/ControllerSettings.qml +++ b/interface/resources/qml/hifi/tablet/ControllerSettings.qml @@ -9,6 +9,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 +import QtQuick.Window 2.2 import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import stylesUit 1.0 @@ -72,6 +73,11 @@ Item { initialItem: inputConfiguration property alias messageVisible: imageMessageBox.visible property string selectedPlugin: "" + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + Rectangle { id: inputConfiguration anchors { @@ -227,6 +233,8 @@ Item { anchors.right: parent.right anchors.top: inputConfiguration.bottom anchors.bottom: parent.bottom + anchors.bottomMargin: keyboard.height + Loader { id: loader asynchronous: false @@ -248,6 +256,29 @@ Item { } } + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + onRaisedChanged: { + if (raised) { + // delayed execution to allow loader and its content to adjust size + Qt.callLater(function() { + loader.item.bringToView(Window.activeFocusItem); + }) + } + } + + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + + Component.onCompleted: { + parent.keyboardEnabled = HMD.active; + } + } function inputPlugins() { if (checkBox.checked) { diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml index 2fc5cc4196..e18fdea444 100644 --- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml +++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml @@ -32,6 +32,18 @@ Flickable { } } + function bringToView(item) { + var yTop = item.mapToItem(contentItem, 0, 0).y; + var yBottom = yTop + item.height; + + var surfaceTop = contentY; + var surfaceBottom = contentY + height; + + if(yTop < surfaceTop || yBottom > surfaceBottom) { + contentY = yTop - height / 2 + item.height + } + } + Component.onCompleted: { page = config.createObject(flick.contentItem); } @@ -39,6 +51,8 @@ Flickable { id: config Rectangle { id: openVrConfiguration + anchors.fill: parent + property int leftMargin: 75 property int countDown: 0 property string pluginName: "" @@ -200,6 +214,7 @@ Flickable { onEditingFinished: { sendConfigurationSettings(); + openVrConfiguration.forceActiveFocus(); } } @@ -217,6 +232,7 @@ Flickable { onEditingFinished: { sendConfigurationSettings(); + openVrConfiguration.forceActiveFocus(); } } } @@ -309,6 +325,7 @@ Flickable { onEditingFinished: { sendConfigurationSettings(); + openVrConfiguration.forceActiveFocus(); } } @@ -325,6 +342,7 @@ Flickable { onEditingFinished: { sendConfigurationSettings(); + openVrConfiguration.forceActiveFocus(); } } } @@ -550,13 +568,15 @@ Flickable { width: 160 suffix: " cm" label: "Arm Circumference" - minimumValue: 0 + minimumValue: 10.0 + maximumValue: 50.0 realStepSize: 1.0 colorScheme: hifi.colorSchemes.dark realValue: 33.0 onEditingFinished: { sendConfigurationSettings(); + openVrConfiguration.forceActiveFocus(); } } @@ -565,7 +585,8 @@ Flickable { width: 160 label: "Shoulder Width" suffix: " cm" - minimumValue: 0 + minimumValue: 25.0 + maximumValue: 50.0 realStepSize: 1.0 decimals: 1 colorScheme: hifi.colorSchemes.dark @@ -573,6 +594,7 @@ Flickable { onEditingFinished: { sendConfigurationSettings(); + openVrConfiguration.forceActiveFocus(); } } } @@ -743,14 +765,17 @@ Flickable { anchors.left: parent.left anchors.leftMargin: leftMargin - minimumValue: 5 + minimumValue: 0 + maximumValue: 5 realValue: 5 + realStepSize: 1.0 colorScheme: hifi.colorSchemes.dark onEditingFinished: { calibrationTimer.interval = realValue * 1000; openVrConfiguration.countDown = realValue; numberAnimation.duration = calibrationTimer.interval; + openVrConfiguration.forceActiveFocus(); } } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index b8972378ad..ab0a98a8c5 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -56,7 +56,7 @@ StackView { Qt.callLater(function() { addressBarDialog.keyboardEnabled = HMD.active; addressLine.forceActiveFocus(); - addressBarDialog.raised = true; + addressBarDialog.keyboardRaised = true; }) } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 84b2bdd7a0..dbccc081e5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -371,6 +371,8 @@ static const QString INFO_HELP_PATH = "html/tabletHelp.html"; static const unsigned int THROTTLED_SIM_FRAMERATE = 15; static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; +static const int ENTITY_SERVER_ADDED_TIMEOUT = 5000; +static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000; static const uint32_t INVALID_FRAME = UINT32_MAX; @@ -1234,6 +1236,18 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo getOverlays().deleteOverlay(getTabletScreenID()); getOverlays().deleteOverlay(getTabletHomeButtonID()); getOverlays().deleteOverlay(getTabletFrameID()); + _failedToConnectToEntityServer = false; + }); + + _entityServerConnectionTimer.setSingleShot(true); + connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); + + connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { + if (!isServerlessMode()) { + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); + _entityServerConnectionTimer.start(); + _failedToConnectToEntityServer = false; + } }); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); @@ -3396,26 +3410,37 @@ void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; + static const QString VIVE_PLUGIN_NAME = "HTC Vive"; + static const QString OCULUS_RIFT_PLUGIN_NAME = "Oculus Rift"; + static const QString WINDOWS_MR_PLUGIN_NAME = "WindowsMR"; + static const QString TAB_KEYBOARD_MOUSE = "kbm"; static const QString TAB_GAMEPAD = "gamepad"; static const QString TAB_HAND_CONTROLLERS = "handControllers"; - QString handControllerName = HAND_CONTROLLER_NAME_VIVE; + QString handControllerName; QString defaultTab = TAB_KEYBOARD_MOUSE; - if (PluginUtils::isViveControllerAvailable()) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_VIVE; - } else if (PluginUtils::isOculusTouchControllerAvailable()) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; - } else if (qApp->getActiveDisplayPlugin()->getName() == "WindowMS") { + if (PluginUtils::isHMDAvailable(WINDOWS_MR_PLUGIN_NAME)) { defaultTab = TAB_HAND_CONTROLLERS; handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; + } else if (PluginUtils::isHMDAvailable(VIVE_PLUGIN_NAME)) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_VIVE; + } else if (PluginUtils::isHMDAvailable(OCULUS_RIFT_PLUGIN_NAME)) { + if (PluginUtils::isOculusTouchControllerAvailable()) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; + } else if (PluginUtils::isXboxControllerAvailable()) { + defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; + } } else if (PluginUtils::isXboxControllerAvailable()) { defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; } - // TODO need some way to detect windowsMR to load controls reference default tab in Help > Controls Reference menu. QUrlQuery queryString; queryString.addQueryItem("handControllerName", handControllerName); @@ -4075,8 +4100,7 @@ void Application::keyPressEvent(QKeyEvent* event) { _snapshotSoundInjector->setOptions(options); _snapshotSoundInjector->restart(); } else { - QByteArray samples = _snapshotSound->getByteArray(); - _snapshotSoundInjector = AudioInjector::playSound(samples, options); + _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); } } takeSnapshot(true); @@ -5847,6 +5871,7 @@ void Application::update(float deltaTime) { quint64 now = usecTimestampNow(); if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) { bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + if (gpuTextureMemSizeStable() || !enableInterstitial) { // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway _lastPhysicsCheckTime = now; @@ -6818,7 +6843,13 @@ void Application::resettingDomain() { } void Application::nodeAdded(SharedNodePointer node) const { - // nothing to do here + if (node->getType() == NodeType::EntityServer) { + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT); + _entityServerConnectionTimer.start(); + } + } } void Application::nodeActivated(SharedNodePointer node) { @@ -6846,6 +6877,10 @@ void Application::nodeActivated(SharedNodePointer node) { if (node->getType() == NodeType::EntityServer) { _queryExpiry = SteadyClock::now(); _octreeQuery.incrementConnectionID(); + + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + } } if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 4afba216be..e2547ccd60 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -310,6 +310,7 @@ public: bool isServerlessMode() const; bool isInterstitialMode() const { return _interstitialMode; } + bool failedToConnectToEntityServer() const { return _failedToConnectToEntityServer; } void replaceDomainContent(const QString& url); @@ -475,6 +476,7 @@ private slots: void loadSettings(); void saveSettings() const; + void setFailedToConnectToEntityServer() { _failedToConnectToEntityServer = true; } bool acceptSnapshot(const QString& urlString); bool askToSetAvatarUrl(const QString& url); @@ -740,6 +742,7 @@ private: bool _isForeground = true; // starts out assumed to be in foreground bool _isGLInitialized { false }; bool _physicsEnabled { false }; + bool _failedToConnectToEntityServer { false }; bool _reticleClickPressed { false }; @@ -786,6 +789,7 @@ private: QStringList _addAssetToWorldInfoMessages; // Info message QTimer _addAssetToWorldInfoTimer; QTimer _addAssetToWorldErrorTimer; + mutable QTimer _entityServerConnectionTimer; FileScriptingInterface* _fileDownload; AudioInjectorPointer _snapshotSoundInjector; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 4593eaafff..b9d09364a7 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -143,7 +143,7 @@ Menu::Menu() { assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets()); } - // Edit > Package Model as .fst... + // Edit > Package Avatar as .fst... addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index d26879c816..d3d9e5e674 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -141,7 +141,7 @@ namespace MenuOption { const QString OpenVrThreadedSubmit = "OpenVR Threaded Submit"; const QString OutputMenu = "Display"; const QString Overlays = "Show Overlays"; - const QString PackageModel = "Package Model as .fst..."; + const QString PackageModel = "Package Avatar as .fst..."; const QString Pair = "Pair"; const QString PhysicsShowOwned = "Highlight Simulation Ownership"; const QString VerboseLogging = "Verbose Logging"; diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 2e7b84c17d..c4f8ef5ddd 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -68,7 +68,6 @@ bool ModelPackager::selectModel() { ModelSelector selector; if(selector.exec() == QDialog::Accepted) { _modelFile = selector.getFileInfo(); - _modelType = selector.getModelType(); return true; } return false; @@ -122,28 +121,26 @@ bool ModelPackager::loadModel() { bool ModelPackager::editProperties() { // open the dialog to configure the rest - ModelPropertiesDialog properties(_modelType, _mapping, _modelFile.path(), *_hfmModel); + ModelPropertiesDialog properties(_mapping, _modelFile.path(), *_hfmModel); if (properties.exec() == QDialog::Rejected) { return false; } _mapping = properties.getMapping(); - if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { - // Make sure that a mapping for the root joint has been specified - QVariantHash joints = _mapping.value(JOINT_FIELD).toHash(); - if (!joints.contains("jointRoot")) { - qWarning() << "root joint not configured for skeleton."; + // Make sure that a mapping for the root joint has been specified + QVariantHash joints = _mapping.value(JOINT_FIELD).toHash(); + if (!joints.contains("jointRoot")) { + qWarning() << "root joint not configured for skeleton."; - QString message = "Your did not configure a root joint for your skeleton model.\n\nPackaging will be canceled."; - QMessageBox msgBox; - msgBox.setWindowTitle("Model Packager"); - msgBox.setText(message); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Warning); - msgBox.exec(); + QString message = "Your did not configure a root joint for your skeleton model.\n\nPackaging will be canceled."; + QMessageBox msgBox; + msgBox.setWindowTitle("Model Packager"); + msgBox.setText(message); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.exec(); - return false; - } + return false; } return true; @@ -237,8 +234,6 @@ bool ModelPackager::zipModel() { void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const hfm::Model& hfmModel) { - bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL; - // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file bool likelyMixamoFile = hfmModel.applicationName == "mixamo.com" || @@ -279,19 +274,17 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename joints.insert("jointNeck", hfmModel.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); } - if (isBodyType) { - if (!joints.contains("jointRoot")) { - joints.insert("jointRoot", "Hips"); - } - if (!joints.contains("jointLean")) { - joints.insert("jointLean", "Spine"); - } - if (!joints.contains("jointLeftHand")) { - joints.insert("jointLeftHand", "LeftHand"); - } - if (!joints.contains("jointRightHand")) { - joints.insert("jointRightHand", "RightHand"); - } + if (!joints.contains("jointRoot")) { + joints.insert("jointRoot", "Hips"); + } + if (!joints.contains("jointLean")) { + joints.insert("jointLean", "Spine"); + } + if (!joints.contains("jointLeftHand")) { + joints.insert("jointLeftHand", "LeftHand"); + } + if (!joints.contains("jointRightHand")) { + joints.insert("jointRightHand", "RightHand"); } if (!joints.contains("jointHead")) { @@ -301,13 +294,11 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename mapping.insert(JOINT_FIELD, joints); - if (isBodyType) { - if (!mapping.contains(FREE_JOINT_FIELD)) { - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); - } + if (!mapping.contains(FREE_JOINT_FIELD)) { + mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); } // If there are no blendshape mappings, and we detect that this is likely a mixamo file, diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 849f6ac3da..09dab5039f 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -41,7 +41,6 @@ private: QFileInfo _modelFile; QFileInfo _fbxInfo; - FSTReader::ModelType _modelType; QString _texDir; QString _scriptDir; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 49c57744a9..1bdb170b60 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -26,9 +26,8 @@ #include -ModelPropertiesDialog::ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, +ModelPropertiesDialog::ModelPropertiesDialog(const QVariantHash& originalMapping, const QString& basePath, const HFMModel& hfmModel) : -_modelType(modelType), _originalMapping(originalMapping), _basePath(basePath), _hfmModel(hfmModel) @@ -50,36 +49,19 @@ _hfmModel(hfmModel) _scale->setMaximum(FLT_MAX); _scale->setSingleStep(0.01); - if (_modelType != FSTReader::ENTITY_MODEL) { - if (_modelType == FSTReader::ATTACHMENT_MODEL) { - QHBoxLayout* translation = new QHBoxLayout(); - form->addRow("Translation:", translation); - translation->addWidget(_translationX = createTranslationBox()); - translation->addWidget(_translationY = createTranslationBox()); - translation->addWidget(_translationZ = createTranslationBox()); - form->addRow("Pivot About Center:", _pivotAboutCenter = new QCheckBox()); - form->addRow("Pivot Joint:", _pivotJoint = createJointBox()); - connect(_pivotAboutCenter, SIGNAL(toggled(bool)), SLOT(updatePivotJoint())); - _pivotAboutCenter->setChecked(true); + form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); + form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); + form->addRow("Neck Joint:", _neckJoint = createJointBox()); + form->addRow("Root Joint:", _rootJoint = createJointBox()); + form->addRow("Lean Joint:", _leanJoint = createJointBox()); + form->addRow("Head Joint:", _headJoint = createJointBox()); + form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); + form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); - } else { - form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); - form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); - form->addRow("Neck Joint:", _neckJoint = createJointBox()); - } - if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { - form->addRow("Root Joint:", _rootJoint = createJointBox()); - form->addRow("Lean Joint:", _leanJoint = createJointBox()); - form->addRow("Head Joint:", _headJoint = createJointBox()); - form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); - form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); - - form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); - QPushButton* newFreeJoint = new QPushButton("New Free Joint"); - _freeJoints->addWidget(newFreeJoint); - connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); - } - } + form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); + QPushButton* newFreeJoint = new QPushButton("New Free Joint"); + _freeJoints->addWidget(newFreeJoint); + connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Reset); @@ -93,14 +75,9 @@ _hfmModel(hfmModel) reset(); } - -QString ModelPropertiesDialog::getType() const { - return FSTReader::getNameFromType(_modelType); -} - QVariantHash ModelPropertiesDialog::getMapping() const { QVariantHash mapping = _originalMapping; - mapping.insert(TYPE_FIELD, getType()); + mapping.insert(TYPE_FIELD, FSTReader::getNameFromType(FSTReader::HEAD_AND_BODY_MODEL)); mapping.insert(NAME_FIELD, _name->text()); mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); mapping.insert(SCRIPT_FIELD, _scriptDirectory->text()); @@ -113,42 +90,24 @@ QVariantHash ModelPropertiesDialog::getMapping() const { } mapping.insert(JOINT_INDEX_FIELD, jointIndices); - if (_modelType != FSTReader::ENTITY_MODEL) { - QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); - if (_modelType == FSTReader::ATTACHMENT_MODEL) { - glm::vec3 pivot; - if (_pivotAboutCenter->isChecked()) { - pivot = (_hfmModel.meshExtents.minimum + _hfmModel.meshExtents.maximum) * 0.5f; - - } else if (_pivotJoint->currentIndex() != 0) { - pivot = extractTranslation(_hfmModel.joints.at(_pivotJoint->currentIndex() - 1).transform); - } - mapping.insert(TRANSLATION_X_FIELD, -pivot.x * (float)_scale->value() + (float)_translationX->value()); - mapping.insert(TRANSLATION_Y_FIELD, -pivot.y * (float)_scale->value() + (float)_translationY->value()); - mapping.insert(TRANSLATION_Z_FIELD, -pivot.z * (float)_scale->value() + (float)_translationZ->value()); - - } else { - insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); - insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); - insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); - } + QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); + insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); + insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); + insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); - if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { - insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); - insertJointMapping(joints, "jointLean", _leanJoint->currentText()); - insertJointMapping(joints, "jointHead", _headJoint->currentText()); - insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); - insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); + insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); + insertJointMapping(joints, "jointLean", _leanJoint->currentText()); + insertJointMapping(joints, "jointHead", _headJoint->currentText()); + insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); + insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); - mapping.remove(FREE_JOINT_FIELD); - for (int i = 0; i < _freeJoints->count() - 1; i++) { - QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); - mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); - } - } - mapping.insert(JOINT_FIELD, joints); + mapping.remove(FREE_JOINT_FIELD); + for (int i = 0; i < _freeJoints->count() - 1; i++) { + QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); + mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); } + mapping.insert(JOINT_FIELD, joints); return mapping; } @@ -165,36 +124,23 @@ void ModelPropertiesDialog::reset() { QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); - if (_modelType != FSTReader::ENTITY_MODEL) { - if (_modelType == FSTReader::ATTACHMENT_MODEL) { - _translationX->setValue(_originalMapping.value(TRANSLATION_X_FIELD).toDouble()); - _translationY->setValue(_originalMapping.value(TRANSLATION_Y_FIELD).toDouble()); - _translationZ->setValue(_originalMapping.value(TRANSLATION_Z_FIELD).toDouble()); - _pivotAboutCenter->setChecked(true); - _pivotJoint->setCurrentIndex(0); + setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString()); + setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); + setJointText(_neckJoint, jointHash.value("jointNeck").toString()); - } else { - setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString()); - setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); - setJointText(_neckJoint, jointHash.value("jointNeck").toString()); - } + setJointText(_rootJoint, jointHash.value("jointRoot").toString()); + setJointText(_leanJoint, jointHash.value("jointLean").toString()); + setJointText(_headJoint, jointHash.value("jointHead").toString()); + setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString()); + setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString()); - if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) { - setJointText(_rootJoint, jointHash.value("jointRoot").toString()); - setJointText(_leanJoint, jointHash.value("jointLean").toString()); - setJointText(_headJoint, jointHash.value("jointHead").toString()); - setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString()); - setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString()); - - while (_freeJoints->count() > 1) { - delete _freeJoints->itemAt(0)->widget(); - } - foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { - QString jointName = joint.toString(); - if (_hfmModel.jointIndices.contains(jointName)) { - createNewFreeJoint(jointName); - } - } + while (_freeJoints->count() > 1) { + delete _freeJoints->itemAt(0)->widget(); + } + foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { + QString jointName = joint.toString(); + if (_hfmModel.jointIndices.contains(jointName)) { + createNewFreeJoint(jointName); } } } diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index 0bf8075197..1058dbfa4e 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -29,7 +29,7 @@ class ModelPropertiesDialog : public QDialog { Q_OBJECT public: - ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, + ModelPropertiesDialog(const QVariantHash& originalMapping, const QString& basePath, const HFMModel& hfmModel); QVariantHash getMapping() const; @@ -45,9 +45,7 @@ private: QComboBox* createJointBox(bool withNone = true) const; QDoubleSpinBox* createTranslationBox() const; void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; - QString getType() const; - FSTReader::ModelType _modelType; QVariantHash _originalMapping; QString _basePath; HFMModel _hfmModel; @@ -71,4 +69,4 @@ private: QVBoxLayout* _freeJoints = nullptr; }; -#endif // hifi_ModelPropertiesDialog_h \ No newline at end of file +#endif // hifi_ModelPropertiesDialog_h diff --git a/interface/src/ModelSelector.cpp b/interface/src/ModelSelector.cpp index 7d91359a11..7e428a294d 100644 --- a/interface/src/ModelSelector.cpp +++ b/interface/src/ModelSelector.cpp @@ -27,18 +27,11 @@ ModelSelector::ModelSelector() { setWindowTitle("Select Model"); setLayout(form); - + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); _browseButton = new QPushButton("Browse", this); connect(_browseButton, &QPushButton::clicked, this, &ModelSelector::browse); form->addRow("Model File:", _browseButton); - _modelType = new QComboBox(this); - - _modelType->addItem(AVATAR_HEAD_AND_BODY_STRING); - _modelType->addItem(AVATAR_ATTACHEMENT_STRING); - _modelType->addItem(ENTITY_MODEL_STRING); - form->addRow("Model Type:", _modelType); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttons, &QDialogButtonBox::accepted, this, &ModelSelector::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); @@ -49,19 +42,6 @@ QFileInfo ModelSelector::getFileInfo() const { return _modelFile; } -FSTReader::ModelType ModelSelector::getModelType() const { - QString text = _modelType->currentText(); - -if (text == AVATAR_HEAD_AND_BODY_STRING) { - return FSTReader::HEAD_AND_BODY_MODEL; - } else if (text == AVATAR_ATTACHEMENT_STRING) { - return FSTReader::ATTACHMENT_MODEL; - } else if (text == ENTITY_MODEL_STRING) { - return FSTReader::ENTITY_MODEL; - } - Q_UNREACHABLE(); -} - void ModelSelector::accept() { if (!_modelFile.isFile()) { return; diff --git a/interface/src/ModelSelector.h b/interface/src/ModelSelector.h index ee9e75c17a..5bbeb4665e 100644 --- a/interface/src/ModelSelector.h +++ b/interface/src/ModelSelector.h @@ -29,7 +29,6 @@ public: ModelSelector(); QFileInfo getFileInfo() const; - FSTReader::ModelType getModelType() const; public slots: virtual void accept() override; @@ -40,7 +39,6 @@ public: private: QFileInfo _modelFile; QPushButton* _browseButton; - QComboBox* _modelType; }; #endif // hifi_ModelSelector_h diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 76ad49b1f4..7ca18ca258 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -562,8 +562,13 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents static const int MAX_INJECTOR_COUNT = 3; if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) { - auto injector = AudioInjector::playSound(collisionSound, energyFactorOfFull, AVATAR_STRETCH_FACTOR, - myAvatar->getWorldPosition()); + AudioInjectorOptions options; + options.stereo = collisionSound->isStereo(); + options.position = myAvatar->getWorldPosition(); + options.volume = energyFactorOfFull; + options.pitch = 1.0f / AVATAR_STRETCH_FACTOR; + + auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); _collisionInjectors.emplace_back(injector); } myAvatar->collisionWithEntity(collision); diff --git a/interface/src/octree/SafeLanding.cpp b/interface/src/octree/SafeLanding.cpp index db381d5350..9efad22d09 100644 --- a/interface/src/octree/SafeLanding.cpp +++ b/interface/src/octree/SafeLanding.cpp @@ -107,7 +107,7 @@ void SafeLanding::noteReceivedsequenceNumber(int sequenceNumber) { } bool SafeLanding::isLoadSequenceComplete() { - if (isEntityLoadingComplete() && isSequenceNumbersComplete()) { + if ((isEntityLoadingComplete() && isSequenceNumbersComplete()) || qApp->failedToConnectToEntityServer()) { Locker lock(_lock); _initialStart = INVALID_SEQUENCE; _initialEnd = INVALID_SEQUENCE; diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index 6b1677aecb..6589769ece 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -147,7 +147,11 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { _lastSoundAudioInjectorUpdateTimer.stop(); } - _lastSoundAudioInjector = AudioInjector::playSoundAndDelete(_lastSoundByteArray, options); + uint32_t numChannels = 1; + uint32_t numSamples = (uint32_t)_lastSoundByteArray.size() / sizeof(AudioData::AudioSample); + auto samples = reinterpret_cast(_lastSoundByteArray.data()); + auto newAudioData = AudioData::make(numSamples, numChannels, samples); + _lastSoundAudioInjector = AudioInjector::playSoundAndDelete(newAudioData, options); _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); #else diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index 52f6a3ebc0..a9ba165037 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -198,4 +198,14 @@ void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) { int TestScriptingInterface::getOtherAvatarsReplicaCount() { return qApp->getOtherAvatarsReplicaCount(); -} \ No newline at end of file +} + +QString TestScriptingInterface::getOperatingSystemType() { +#ifdef Q_OS_WIN + return "WINDOWS"; +#elif defined Q_OS_MAC + return "MACOS"; +#else + return "UNKNOWN"; +#endif +} diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 4a1d1a3eeb..26e967c9b5 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -163,6 +163,13 @@ public slots: */ Q_INVOKABLE int getOtherAvatarsReplicaCount(); + /**jsdoc + * Returns the Operating Sytem type + * @function Test.getOperatingSystemType + * @returns {string} "WINDOWS", "MACOS" or "UNKNOWN" + */ + QString getOperatingSystemType(); + private: bool waitForCondition(qint64 maxWaitMs, std::function condition); QString _testResultsLocation; diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index a527591221..2531374d83 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -486,7 +486,7 @@ void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent audioOptions.position = keyWorldPosition; audioOptions.volume = 0.1f; - AudioInjector::playSound(_keySound->getByteArray(), audioOptions); + AudioInjector::playSoundAndDelete(_keySound, audioOptions); int scanCode = key.getScanCode(_capsEnabled); QString keyString = key.getKeyString(_capsEnabled); diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 73a0891fbe..91ca2359b4 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -23,11 +23,39 @@ AnimSkeleton::AnimSkeleton(const HFMModel& hfmModel) { for (auto& joint : hfmModel.joints) { joints.push_back(joint); } - buildSkeletonFromJoints(joints); + buildSkeletonFromJoints(joints, hfmModel.jointRotationOffsets); + + // we make a copy of the inverseBindMatrices in order to prevent mutating the model bind pose + // when we are dealing with a joint offset in the model + for (int i = 0; i < (int)hfmModel.meshes.size(); i++) { + const HFMMesh& mesh = hfmModel.meshes.at(i); + std::vector dummyClustersList; + + for (int j = 0; j < mesh.clusters.size(); j++) { + std::vector bindMatrices; + // cast into a non-const reference, so we can mutate the FBXCluster + HFMCluster& cluster = const_cast(mesh.clusters.at(j)); + + HFMCluster localCluster; + localCluster.jointIndex = cluster.jointIndex; + localCluster.inverseBindMatrix = cluster.inverseBindMatrix; + localCluster.inverseBindTransform.evalFromRawMatrix(localCluster.inverseBindMatrix); + + // if we have a joint offset in the fst file then multiply its inverse by the + // model cluster inverse bind matrix + if (hfmModel.jointRotationOffsets.contains(cluster.jointIndex)) { + AnimPose localOffset(hfmModel.jointRotationOffsets[cluster.jointIndex], glm::vec3()); + localCluster.inverseBindMatrix = (glm::mat4)localOffset.inverse() * cluster.inverseBindMatrix; + localCluster.inverseBindTransform.evalFromRawMatrix(localCluster.inverseBindMatrix); + } + dummyClustersList.push_back(localCluster); + } + _clusterBindMatrixOriginalValues.push_back(dummyClustersList); + } } -AnimSkeleton::AnimSkeleton(const std::vector& joints) { - buildSkeletonFromJoints(joints); +AnimSkeleton::AnimSkeleton(const std::vector& joints, const QMap jointOffsets) { + buildSkeletonFromJoints(joints, jointOffsets); } int AnimSkeleton::nameToJointIndex(const QString& jointName) const { @@ -166,7 +194,8 @@ void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const { } } -void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) { +void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints, const QMap jointOffsets) { + _joints = joints; _jointsSize = (int)joints.size(); // build a cache of bind poses @@ -189,7 +218,7 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) // build relative and absolute default poses glm::mat4 relDefaultMat = glm::translate(_joints[i].translation) * preRotationTransform * glm::mat4_cast(_joints[i].rotation) * postRotationTransform; AnimPose relDefaultPose(relDefaultMat); - _relativeDefaultPoses.push_back(relDefaultPose); + int parentIndex = getParentIndex(i); if (parentIndex >= 0) { _absoluteDefaultPoses.push_back(_absoluteDefaultPoses[parentIndex] * relDefaultPose); @@ -198,6 +227,16 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) } } + for (int k = 0; k < _jointsSize; k++) { + if (jointOffsets.contains(k)) { + AnimPose localOffset(jointOffsets[k], glm::vec3()); + _absoluteDefaultPoses[k] = _absoluteDefaultPoses[k] * localOffset; + } + } + // re-compute relative poses + _relativeDefaultPoses = _absoluteDefaultPoses; + convertAbsolutePosesToRelative(_relativeDefaultPoses); + for (int i = 0; i < _jointsSize; i++) { _jointIndicesByName[_joints[i].name] = i; } diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 3a384388d0..ab89eb643d 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -24,7 +24,8 @@ public: using ConstPointer = std::shared_ptr; explicit AnimSkeleton(const HFMModel& hfmModel); - explicit AnimSkeleton(const std::vector& joints); + explicit AnimSkeleton(const std::vector& joints, const QMap jointOffsets); + int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; int getNumJoints() const; @@ -62,9 +63,10 @@ public: void dump(const AnimPoseVec& poses) const; std::vector lookUpJointIndices(const std::vector& jointNames) const; + const HFMCluster getClusterBindMatricesOriginalValues(const int meshIndex, const int clusterIndex) const { return _clusterBindMatrixOriginalValues[meshIndex][clusterIndex]; } protected: - void buildSkeletonFromJoints(const std::vector& joints); + void buildSkeletonFromJoints(const std::vector& joints, const QMap jointOffsets); std::vector _joints; int _jointsSize { 0 }; @@ -76,6 +78,7 @@ protected: std::vector _nonMirroredIndices; std::vector _mirrorMap; QHash _jointIndicesByName; + std::vector> _clusterBindMatrixOriginalValues; // no copies AnimSkeleton(const AnimSkeleton&) = delete; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 8b7b0ba916..128ac05b81 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -360,8 +360,10 @@ void Rig::initJointStates(const HFMModel& hfmModel, const glm::mat4& modelOffset void Rig::reset(const HFMModel& hfmModel) { _geometryOffset = AnimPose(hfmModel.offset); _invGeometryOffset = _geometryOffset.inverse(); + _animSkeleton = std::make_shared(hfmModel); + _internalPoseSet._relativePoses.clear(); _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); diff --git a/libraries/audio/src/AudioConstants.h b/libraries/audio/src/AudioConstants.h index 6d9678c368..f4235f42c5 100644 --- a/libraries/audio/src/AudioConstants.h +++ b/libraries/audio/src/AudioConstants.h @@ -22,7 +22,7 @@ namespace AudioConstants { const int STEREO = 2; const int AMBISONIC = 4; - typedef int16_t AudioSample; + using AudioSample = int16_t; const int SAMPLE_SIZE = sizeof(AudioSample); inline const char* getAudioFrameName() { return "com.highfidelity.recording.Audio"; } diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index d1b919292a..4af6e79caf 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -38,12 +38,14 @@ AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs) return lhs; }; -AudioInjector::AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions) : - AudioInjector(sound.getByteArray(), injectorOptions) +AudioInjector::AudioInjector(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions) : + _sound(sound), + _audioData(sound->getAudioData()), + _options(injectorOptions) { } -AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions) : +AudioInjector::AudioInjector(AudioDataPointer audioData, const AudioInjectorOptions& injectorOptions) : _audioData(audioData), _options(injectorOptions) { @@ -154,7 +156,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInj bool AudioInjector::injectLocally() { bool success = false; if (_localAudioInterface) { - if (_audioData.size() > 0) { + if (_audioData->getNumBytes() > 0) { _localBuffer = new AudioInjectorLocalBuffer(_audioData); @@ -220,22 +222,12 @@ int64_t AudioInjector::injectNextFrame() { if (!_currentPacket) { if (_currentSendOffset < 0 || - _currentSendOffset >= _audioData.size()) { + _currentSendOffset >= (int)_audioData->getNumBytes()) { _currentSendOffset = 0; } // make sure we actually have samples downloaded to inject - if (_audioData.size()) { - - int sampleSize = (_options.stereo ? 2 : 1) * sizeof(AudioConstants::AudioSample); - auto numSamples = static_cast(_audioData.size() / sampleSize); - auto targetSize = numSamples * sampleSize; - if (targetSize != _audioData.size()) { - qCDebug(audio) << "Resizing audio that doesn't end at multiple of sample size, resizing from " - << _audioData.size() << " to " << targetSize; - _audioData.resize(targetSize); - } - + if (_audioData && _audioData->getNumSamples() > 0) { _outgoingSequenceNumber = 0; _nextFrame = 0; @@ -307,19 +299,10 @@ int64_t AudioInjector::injectNextFrame() { _frameTimer->restart(); } - int totalBytesLeftToCopy = (_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - if (!_options.loop) { - // If we aren't looping, let's make sure we don't read past the end - totalBytesLeftToCopy = std::min(totalBytesLeftToCopy, _audioData.size() - _currentSendOffset); - } - - // Measure the loudness of this frame - _loudness = 0.0f; - for (int i = 0; i < totalBytesLeftToCopy; i += sizeof(int16_t)) { - _loudness += abs(*reinterpret_cast(_audioData.data() + ((_currentSendOffset + i) % _audioData.size()))) / - (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); - } - _loudness /= (float)(totalBytesLeftToCopy/ sizeof(int16_t)); + assert(loopbackOptionOffset != -1); + assert(positionOptionOffset != -1); + assert(volumeOptionOffset != -1); + assert(audioDataOffset != -1); _currentPacket->seek(0); @@ -339,19 +322,37 @@ int64_t AudioInjector::injectNextFrame() { _currentPacket->seek(audioDataOffset); - // This code is copying bytes from the _audioData directly into the packet, handling looping appropriately. + // This code is copying bytes from the _sound directly into the packet, handling looping appropriately. // Might be a reasonable place to do the encode step here. QByteArray decodedAudio; - while (totalBytesLeftToCopy > 0) { - int bytesToCopy = std::min(totalBytesLeftToCopy, _audioData.size() - _currentSendOffset); - decodedAudio.append(_audioData.data() + _currentSendOffset, bytesToCopy); - _currentSendOffset += bytesToCopy; - totalBytesLeftToCopy -= bytesToCopy; - if (_options.loop && _currentSendOffset >= _audioData.size()) { - _currentSendOffset = 0; - } + int totalBytesLeftToCopy = (_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + if (!_options.loop) { + // If we aren't looping, let's make sure we don't read past the end + int bytesLeftToRead = _audioData->getNumBytes() - _currentSendOffset; + totalBytesLeftToCopy = std::min(totalBytesLeftToCopy, bytesLeftToRead); } + + auto samples = _audioData->data(); + auto currentSample = _currentSendOffset / AudioConstants::SAMPLE_SIZE; + auto samplesLeftToCopy = totalBytesLeftToCopy / AudioConstants::SAMPLE_SIZE; + + using AudioConstants::AudioSample; + decodedAudio.resize(totalBytesLeftToCopy); + auto samplesOut = reinterpret_cast(decodedAudio.data()); + + // Copy and Measure the loudness of this frame + _loudness = 0.0f; + for (int i = 0; i < samplesLeftToCopy; ++i) { + auto index = (currentSample + i) % _audioData->getNumSamples(); + auto sample = samples[index]; + samplesOut[i] = sample; + _loudness += abs(sample) / (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); + } + _loudness /= (float)samplesLeftToCopy; + _currentSendOffset = (_currentSendOffset + totalBytesLeftToCopy) % + _audioData->getNumBytes(); + // FIXME -- good place to call codec encode here. We need to figure out how to tell the AudioInjector which // codec to use... possible through AbstractAudioInterface. QByteArray encodedAudio = decodedAudio; @@ -370,7 +371,7 @@ int64_t AudioInjector::injectNextFrame() { _outgoingSequenceNumber++; } - if (_currentSendOffset >= _audioData.size() && !_options.loop) { + if (_currentSendOffset == 0 && !_options.loop) { finishNetworkInjection(); return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } @@ -390,7 +391,7 @@ int64_t AudioInjector::injectNextFrame() { // If we are falling behind by more frames than our threshold, let's skip the frames ahead qCDebug(audio) << this << "injectNextFrame() skipping ahead, fell behind by " << (currentFrameBasedOnElapsedTime - _nextFrame) << " frames"; _nextFrame = currentFrameBasedOnElapsedTime; - _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (_options.stereo ? 2 : 1) % _audioData.size(); + _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (_options.stereo ? 2 : 1) % _audioData->getNumBytes(); } int64_t playNextFrameAt = ++_nextFrame * AudioConstants::NETWORK_FRAME_USECS; @@ -417,38 +418,25 @@ void AudioInjector::triggerDeleteAfterFinish() { } } -AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const float volume, - const float stretchFactor, const glm::vec3 position) { +AudioInjectorPointer AudioInjector::playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options) { + AudioInjectorPointer injector = playSound(sound, options); + + if (injector) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + return injector; +} + + +AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const AudioInjectorOptions& options) { if (!sound || !sound->isReady()) { return AudioInjectorPointer(); } - AudioInjectorOptions options; - options.stereo = sound->isStereo(); - options.position = position; - options.volume = volume; - options.pitch = 1.0f / stretchFactor; - - QByteArray samples = sound->getByteArray(); - - return playSoundAndDelete(samples, options); -} - -AudioInjectorPointer AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjectorPointer sound = playSound(buffer, options); - - if (sound) { - sound->_state |= AudioInjectorState::PendingDelete; - } - - return sound; -} - -AudioInjectorPointer AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { - if (options.pitch == 1.0f) { - AudioInjectorPointer injector = AudioInjectorPointer::create(buffer, options); + AudioInjectorPointer injector = AudioInjectorPointer::create(sound, options); if (!injector->inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::playSound failed to thread injector"; @@ -456,24 +444,31 @@ AudioInjectorPointer AudioInjector::playSound(const QByteArray& buffer, const Au return injector; } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit to 4 octaves + const int pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = SAMPLE_RATE / pitch; - const int standardRate = AudioConstants::SAMPLE_RATE; - const int resampledRate = AudioConstants::SAMPLE_RATE / glm::clamp(options.pitch, 1/16.0f, 16.0f); // limit to 4 octaves - const int numChannels = options.ambisonic ? AudioConstants::AMBISONIC : - (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO); + auto audioData = sound->getAudioData(); + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); AudioSRC resampler(standardRate, resampledRate, numChannels); // create a resampled buffer that is guaranteed to be large enough - const int nInputFrames = buffer.size() / (numChannels * sizeof(int16_t)); - const int maxOutputFrames = resampler.getMaxOutput(nInputFrames); - QByteArray resampledBuffer(maxOutputFrames * numChannels * sizeof(int16_t), '\0'); + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); - resampler.render(reinterpret_cast(buffer.data()), - reinterpret_cast(resampledBuffer.data()), - nInputFrames); + resampler.render(audioData->data(), bufferPtr, numFrames); - AudioInjectorPointer injector = AudioInjectorPointer::create(resampledBuffer, options); + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + AudioInjectorPointer injector = AudioInjectorPointer::create(newAudioData, options); if (!injector->inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; @@ -481,3 +476,49 @@ AudioInjectorPointer AudioInjector::playSound(const QByteArray& buffer, const Au return injector; } } + +AudioInjectorPointer AudioInjector::playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options) { + AudioInjectorPointer injector = playSound(audioData, options); + + if (injector) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + return injector; +} + +AudioInjectorPointer AudioInjector::playSound(AudioDataPointer audioData, const AudioInjectorOptions& options) { + if (options.pitch == 1.0f) { + AudioInjectorPointer injector = AudioInjectorPointer::create(audioData, options); + + if (!injector->inject(&AudioInjectorManager::threadInjector)) { + qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; + } + return injector; + } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit to 4 octaves + const int pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = SAMPLE_RATE / pitch; + + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); + + resampler.render(audioData->data(), bufferPtr, numFrames); + + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + return AudioInjector::playSound(newAudioData, options); + } +} \ No newline at end of file diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index fc197f7ba0..49faa61b91 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -52,8 +52,8 @@ AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs) class AudioInjector : public QObject, public QEnableSharedFromThis { Q_OBJECT public: - AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); - AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); + AudioInjector(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions); + AudioInjector(AudioDataPointer audioData, const AudioInjectorOptions& injectorOptions); ~AudioInjector(); bool isFinished() const { return (stateHas(AudioInjectorState::Finished)); } @@ -74,10 +74,11 @@ public: bool stateHas(AudioInjectorState state) const ; static void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - static AudioInjectorPointer playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjectorPointer playSound(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjectorPointer playSound(SharedSoundPointer sound, const float volume, - const float stretchFactor, const glm::vec3 position); + + static AudioInjectorPointer playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options); + static AudioInjectorPointer playSound(SharedSoundPointer sound, const AudioInjectorOptions& options); + static AudioInjectorPointer playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options); + static AudioInjectorPointer playSound(AudioDataPointer audioData, const AudioInjectorOptions& options); public slots: void restart(); @@ -106,7 +107,8 @@ private: static AbstractAudioInterface* _localAudioInterface; - QByteArray _audioData; + const SharedSoundPointer _sound; + AudioDataPointer _audioData; AudioInjectorOptions _options; AudioInjectorState _state { AudioInjectorState::NotFinished }; bool _hasSentFirstFrame { false }; diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.cpp b/libraries/audio/src/AudioInjectorLocalBuffer.cpp index 7834644baf..015d87e03b 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.cpp +++ b/libraries/audio/src/AudioInjectorLocalBuffer.cpp @@ -11,13 +11,9 @@ #include "AudioInjectorLocalBuffer.h" -AudioInjectorLocalBuffer::AudioInjectorLocalBuffer(const QByteArray& rawAudioArray) : - _rawAudioArray(rawAudioArray), - _shouldLoop(false), - _isStopped(false), - _currentOffset(0) +AudioInjectorLocalBuffer::AudioInjectorLocalBuffer(AudioDataPointer audioData) : + _audioData(audioData) { - } void AudioInjectorLocalBuffer::stop() { @@ -39,7 +35,7 @@ qint64 AudioInjectorLocalBuffer::readData(char* data, qint64 maxSize) { if (!_isStopped) { // first copy to the end of the raw audio - int bytesToEnd = _rawAudioArray.size() - _currentOffset; + int bytesToEnd = (int)_audioData->getNumBytes() - _currentOffset; int bytesRead = maxSize; @@ -47,7 +43,7 @@ qint64 AudioInjectorLocalBuffer::readData(char* data, qint64 maxSize) { bytesRead = bytesToEnd; } - memcpy(data, _rawAudioArray.data() + _currentOffset, bytesRead); + memcpy(data, _audioData->rawData() + _currentOffset, bytesRead); // now check if we are supposed to loop and if we can copy more from the beginning if (_shouldLoop && maxSize != bytesRead) { @@ -56,7 +52,7 @@ qint64 AudioInjectorLocalBuffer::readData(char* data, qint64 maxSize) { _currentOffset += bytesRead; } - if (_shouldLoop && _currentOffset == _rawAudioArray.size()) { + if (_shouldLoop && _currentOffset == (int)_audioData->getNumBytes()) { _currentOffset = 0; } @@ -70,12 +66,12 @@ qint64 AudioInjectorLocalBuffer::recursiveReadFromFront(char* data, qint64 maxSi // see how much we can get in this pass int bytesRead = maxSize; - if (bytesRead > _rawAudioArray.size()) { - bytesRead = _rawAudioArray.size(); + if (bytesRead > (int)_audioData->getNumBytes()) { + bytesRead = _audioData->getNumBytes(); } // copy that amount - memcpy(data, _rawAudioArray.data(), bytesRead); + memcpy(data, _audioData->rawData(), bytesRead); // check if we need to call ourselves again and pull from the front again if (bytesRead < maxSize) { diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.h b/libraries/audio/src/AudioInjectorLocalBuffer.h index 3065de5199..e0f8847883 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.h +++ b/libraries/audio/src/AudioInjectorLocalBuffer.h @@ -16,10 +16,12 @@ #include +#include "Sound.h" + class AudioInjectorLocalBuffer : public QIODevice { Q_OBJECT public: - AudioInjectorLocalBuffer(const QByteArray& rawAudioArray); + AudioInjectorLocalBuffer(AudioDataPointer audioData); void stop(); @@ -34,11 +36,10 @@ public: private: qint64 recursiveReadFromFront(char* data, qint64 maxSize); - QByteArray _rawAudioArray; - bool _shouldLoop; - bool _isStopped; - - int _currentOffset; + AudioDataPointer _audioData; + bool _shouldLoop { false }; + bool _isStopped { false }; + int _currentOffset { 0 }; }; #endif // hifi_AudioInjectorLocalBuffer_h diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 3807882b00..7c5dd3813b 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -33,48 +33,59 @@ #include "flump3dec.h" -QScriptValue soundSharedPointerToScriptValue(QScriptEngine* engine, const SharedSoundPointer& in) { - return engine->newQObject(new SoundScriptingInterface(in), QScriptEngine::ScriptOwnership); +int audioDataPointerMetaTypeID = qRegisterMetaType("AudioDataPointer"); + +using AudioConstants::AudioSample; + +AudioDataPointer AudioData::make(uint32_t numSamples, uint32_t numChannels, + const AudioSample* samples) { + // Compute the amount of memory required for the audio data object + const size_t bufferSize = numSamples * sizeof(AudioSample); + const size_t memorySize = sizeof(AudioData) + bufferSize; + + // Allocate the memory for the audio data object and the buffer + void* memory = ::malloc(memorySize); + auto audioData = reinterpret_cast(memory); + auto buffer = reinterpret_cast(audioData + 1); + assert(((char*)buffer - (char*)audioData) == sizeof(AudioData)); + + // Use placement new to construct the audio data object at the memory allocated + ::new(audioData) AudioData(numSamples, numChannels, buffer); + + // Copy the samples to the buffer + memcpy(buffer, samples, bufferSize); + + // Return shared_ptr that properly destruct the object and release the memory + return AudioDataPointer(audioData, [](AudioData* ptr) { + ptr->~AudioData(); + ::free(ptr); + }); } -void soundSharedPointerFromScriptValue(const QScriptValue& object, SharedSoundPointer& out) { - if (auto soundInterface = qobject_cast(object.toQObject())) { - out = soundInterface->getSound(); - } -} -SoundScriptingInterface::SoundScriptingInterface(const SharedSoundPointer& sound) : _sound(sound) { - // During shutdown we can sometimes get an empty sound pointer back - if (_sound) { - QObject::connect(_sound.data(), &Sound::ready, this, &SoundScriptingInterface::ready); - } -} - -Sound::Sound(const QUrl& url, bool isStereo, bool isAmbisonic) : - Resource(url), - _isStereo(isStereo), - _isAmbisonic(isAmbisonic), - _isReady(false) -{ -} +AudioData::AudioData(uint32_t numSamples, uint32_t numChannels, const AudioSample* samples) + : _numSamples(numSamples), + _numChannels(numChannels), + _data(samples) +{} void Sound::downloadFinished(const QByteArray& data) { + if (!_self) { + soundProcessError(301, "Sound object has gone out of scope"); + return; + } + // this is a QRunnable, will delete itself after it has finished running - SoundProcessor* soundProcessor = new SoundProcessor(_url, data, _isStereo, _isAmbisonic); + auto soundProcessor = new SoundProcessor(_self, data); connect(soundProcessor, &SoundProcessor::onSuccess, this, &Sound::soundProcessSuccess); connect(soundProcessor, &SoundProcessor::onError, this, &Sound::soundProcessError); QThreadPool::globalInstance()->start(soundProcessor); } -void Sound::soundProcessSuccess(QByteArray data, bool stereo, bool ambisonic, float duration) { +void Sound::soundProcessSuccess(AudioDataPointer audioData) { + qCDebug(audio) << "Setting ready state for sound file" << _url.fileName(); - qCDebug(audio) << "Setting ready state for sound file"; - - _byteArray = data; - _isStereo = stereo; - _isAmbisonic = ambisonic; - _duration = duration; - _isReady = true; + _audioData = std::move(audioData); finishedLoading(true); emit ready(); @@ -86,91 +97,101 @@ void Sound::soundProcessError(int error, QString str) { finishedLoading(false); } + +SoundProcessor::SoundProcessor(QWeakPointer sound, QByteArray data) : + _sound(sound), + _data(data) +{ +} + void SoundProcessor::run() { + auto sound = qSharedPointerCast(_sound.lock()); + if (!sound) { + emit onError(301, "Sound object has gone out of scope"); + return; + } - qCDebug(audio) << "Processing sound file"; - - // replace our byte array with the downloaded data - QByteArray rawAudioByteArray = QByteArray(_data); - QString fileName = _url.fileName().toLower(); + auto url = sound->getURL(); + QString fileName = url.fileName().toLower(); + qCDebug(audio) << "Processing sound file" << fileName; static const QString WAV_EXTENSION = ".wav"; static const QString MP3_EXTENSION = ".mp3"; static const QString RAW_EXTENSION = ".raw"; + static const QString STEREO_RAW_EXTENSION = ".stereo.raw"; + QString fileType; + + QByteArray outputAudioByteArray; + AudioProperties properties; if (fileName.endsWith(WAV_EXTENSION)) { - - QByteArray outputAudioByteArray; - - int sampleRate = interpretAsWav(rawAudioByteArray, outputAudioByteArray); - if (sampleRate == 0) { - qCWarning(audio) << "Unsupported WAV file type"; - emit onError(300, "Failed to load sound file, reason: unsupported WAV file type"); - return; - } - - downSample(outputAudioByteArray, sampleRate); - + fileType = "WAV"; + properties = interpretAsWav(_data, outputAudioByteArray); } else if (fileName.endsWith(MP3_EXTENSION)) { - - QByteArray outputAudioByteArray; - - int sampleRate = interpretAsMP3(rawAudioByteArray, outputAudioByteArray); - if (sampleRate == 0) { - qCWarning(audio) << "Unsupported MP3 file type"; - emit onError(300, "Failed to load sound file, reason: unsupported MP3 file type"); - return; - } - - downSample(outputAudioByteArray, sampleRate); - - } else if (fileName.endsWith(RAW_EXTENSION)) { + fileType = "MP3"; + properties = interpretAsMP3(_data, outputAudioByteArray); + } else if (fileName.endsWith(STEREO_RAW_EXTENSION)) { // check if this was a stereo raw file // since it's raw the only way for us to know that is if the file was called .stereo.raw - if (fileName.toLower().endsWith("stereo.raw")) { - _isStereo = true; - qCDebug(audio) << "Processing sound of" << rawAudioByteArray.size() << "bytes as stereo audio file."; - } - + qCDebug(audio) << "Processing sound of" << _data.size() << "bytes from" << fileName << "as stereo audio file."; // Process as 48khz RAW file - downSample(rawAudioByteArray, 48000); - + properties.numChannels = 2; + properties.sampleRate = 48000; + outputAudioByteArray = _data; + } else if (fileName.endsWith(RAW_EXTENSION)) { + // Process as 48khz RAW file + properties.numChannels = 1; + properties.sampleRate = 48000; + outputAudioByteArray = _data; } else { qCWarning(audio) << "Unknown sound file type"; emit onError(300, "Failed to load sound file, reason: unknown sound file type"); return; } - emit onSuccess(_data, _isStereo, _isAmbisonic, _duration); + if (properties.sampleRate == 0) { + qCWarning(audio) << "Unsupported" << fileType << "file type"; + emit onError(300, "Failed to load sound file, reason: unsupported " + fileType + " file type"); + return; + } + + auto data = downSample(outputAudioByteArray, properties); + + int numSamples = data.size() / AudioConstants::SAMPLE_SIZE; + auto audioData = AudioData::make(numSamples, properties.numChannels, + (const AudioSample*)data.constData()); + emit onSuccess(audioData); } -void SoundProcessor::downSample(const QByteArray& rawAudioByteArray, int sampleRate) { +QByteArray SoundProcessor::downSample(const QByteArray& rawAudioByteArray, + AudioProperties properties) { // we want to convert it to the format that the audio-mixer wants // which is signed, 16-bit, 24Khz - if (sampleRate == AudioConstants::SAMPLE_RATE) { + if (properties.sampleRate == AudioConstants::SAMPLE_RATE) { // no resampling needed - _data = rawAudioByteArray; - } else { - - int numChannels = _isAmbisonic ? AudioConstants::AMBISONIC : (_isStereo ? AudioConstants::STEREO : AudioConstants::MONO); - AudioSRC resampler(sampleRate, AudioConstants::SAMPLE_RATE, numChannels); - - // resize to max possible output - int numSourceFrames = rawAudioByteArray.size() / (numChannels * sizeof(AudioConstants::AudioSample)); - int maxDestinationFrames = resampler.getMaxOutput(numSourceFrames); - int maxDestinationBytes = maxDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample); - _data.resize(maxDestinationBytes); - - int numDestinationFrames = resampler.render((int16_t*)rawAudioByteArray.data(), - (int16_t*)_data.data(), - numSourceFrames); - - // truncate to actual output - int numDestinationBytes = numDestinationFrames * numChannels * sizeof(AudioConstants::AudioSample); - _data.resize(numDestinationBytes); + return rawAudioByteArray; } + + AudioSRC resampler(properties.sampleRate, AudioConstants::SAMPLE_RATE, + properties.numChannels); + + // resize to max possible output + int numSourceFrames = rawAudioByteArray.size() / (properties.numChannels * AudioConstants::SAMPLE_SIZE); + int maxDestinationFrames = resampler.getMaxOutput(numSourceFrames); + int maxDestinationBytes = maxDestinationFrames * properties.numChannels * AudioConstants::SAMPLE_SIZE; + QByteArray data(maxDestinationBytes, Qt::Uninitialized); + + int numDestinationFrames = resampler.render((int16_t*)rawAudioByteArray.data(), + (int16_t*)data.data(), + numSourceFrames); + + // truncate to actual output + int numDestinationBytes = numDestinationFrames * properties.numChannels * sizeof(AudioSample); + data.resize(numDestinationBytes); + + return data; } // @@ -218,7 +239,9 @@ struct WAVEFormat { }; // returns wavfile sample rate, used for resampling -int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray) { +SoundProcessor::AudioProperties SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, + QByteArray& outputAudioByteArray) { + AudioProperties properties; // Create a data stream to analyze the data QDataStream waveStream(const_cast(&inputAudioByteArray), QIODevice::ReadOnly); @@ -227,7 +250,7 @@ int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteA RIFFHeader riff; if (waveStream.readRawData((char*)&riff, sizeof(RIFFHeader)) != sizeof(RIFFHeader)) { qCWarning(audio) << "Not a valid WAVE file."; - return 0; + return AudioProperties(); } // Parse the "RIFF" chunk @@ -235,11 +258,11 @@ int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteA waveStream.setByteOrder(QDataStream::LittleEndian); } else { qCWarning(audio) << "Currently not supporting big-endian audio files."; - return 0; + return AudioProperties(); } if (strncmp(riff.type, "WAVE", 4) != 0) { qCWarning(audio) << "Not a valid WAVE file."; - return 0; + return AudioProperties(); } // Read chunks until the "fmt " chunk is found @@ -247,7 +270,7 @@ int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteA while (true) { if (waveStream.readRawData((char*)&fmt, sizeof(chunk)) != sizeof(chunk)) { qCWarning(audio) << "Not a valid WAVE file."; - return 0; + return AudioProperties(); } if (strncmp(fmt.id, "fmt ", 4) == 0) { break; @@ -259,26 +282,26 @@ int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteA WAVEFormat wave; if (waveStream.readRawData((char*)&wave, sizeof(WAVEFormat)) != sizeof(WAVEFormat)) { qCWarning(audio) << "Not a valid WAVE file."; - return 0; + return AudioProperties(); } // Parse the "fmt " chunk if (qFromLittleEndian(wave.audioFormat) != WAVEFORMAT_PCM && qFromLittleEndian(wave.audioFormat) != WAVEFORMAT_EXTENSIBLE) { qCWarning(audio) << "Currently not supporting non PCM audio files."; - return 0; + return AudioProperties(); } - if (qFromLittleEndian(wave.numChannels) == 2) { - _isStereo = true; - } else if (qFromLittleEndian(wave.numChannels) == 4) { - _isAmbisonic = true; - } else if (qFromLittleEndian(wave.numChannels) != 1) { + + properties.numChannels = qFromLittleEndian(wave.numChannels); + if (properties.numChannels != 1 && + properties.numChannels != 2 && + properties.numChannels != 4) { qCWarning(audio) << "Currently not supporting audio files with other than 1/2/4 channels."; - return 0; + return AudioProperties(); } if (qFromLittleEndian(wave.bitsPerSample) != 16) { qCWarning(audio) << "Currently not supporting non 16bit audio files."; - return 0; + return AudioProperties(); } // Skip any extra data in the "fmt " chunk @@ -289,7 +312,7 @@ int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteA while (true) { if (waveStream.readRawData((char*)&data, sizeof(chunk)) != sizeof(chunk)) { qCWarning(audio) << "Not a valid WAVE file."; - return 0; + return AudioProperties(); } if (strncmp(data.id, "data", 4) == 0) { break; @@ -300,17 +323,21 @@ int SoundProcessor::interpretAsWav(const QByteArray& inputAudioByteArray, QByteA // Read the "data" chunk quint32 outputAudioByteArraySize = qFromLittleEndian(data.size); outputAudioByteArray.resize(outputAudioByteArraySize); - if (waveStream.readRawData(outputAudioByteArray.data(), outputAudioByteArraySize) != (int)outputAudioByteArraySize) { + auto bytesRead = waveStream.readRawData(outputAudioByteArray.data(), outputAudioByteArraySize); + if (bytesRead != (int)outputAudioByteArraySize) { qCWarning(audio) << "Error reading WAV file"; - return 0; + return AudioProperties(); } - _duration = (float)(outputAudioByteArraySize / (wave.sampleRate * wave.numChannels * wave.bitsPerSample / 8.0f)); - return wave.sampleRate; + properties.sampleRate = wave.sampleRate; + return properties; } // returns MP3 sample rate, used for resampling -int SoundProcessor::interpretAsMP3(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray) { +SoundProcessor::AudioProperties SoundProcessor::interpretAsMP3(const QByteArray& inputAudioByteArray, + QByteArray& outputAudioByteArray) { + AudioProperties properties; + using namespace flump3dec; static const int MP3_SAMPLES_MAX = 1152; @@ -321,21 +348,19 @@ int SoundProcessor::interpretAsMP3(const QByteArray& inputAudioByteArray, QByteA // create bitstream Bit_stream_struc *bitstream = bs_new(); if (bitstream == nullptr) { - return 0; + return AudioProperties(); } // create decoder mp3tl *decoder = mp3tl_new(bitstream, MP3TL_MODE_16BIT); if (decoder == nullptr) { bs_free(bitstream); - return 0; + return AudioProperties(); } // initialize bs_set_data(bitstream, (uint8_t*)inputAudioByteArray.data(), inputAudioByteArray.size()); int frameCount = 0; - int sampleRate = 0; - int numChannels = 0; // skip ID3 tag, if present Mp3TlRetcode result = mp3tl_skip_id3(decoder); @@ -357,8 +382,8 @@ int SoundProcessor::interpretAsMP3(const QByteArray& inputAudioByteArray, QByteA << "channels =" << header->channels; // save header info - sampleRate = header->sample_rate; - numChannels = header->channels; + properties.sampleRate = header->sample_rate; + properties.numChannels = header->channels; // skip Xing header, if present result = mp3tl_skip_xing(decoder, header); @@ -388,14 +413,32 @@ int SoundProcessor::interpretAsMP3(const QByteArray& inputAudioByteArray, QByteA // free bitstream bs_free(bitstream); - int outputAudioByteArraySize = outputAudioByteArray.size(); - if (outputAudioByteArraySize == 0) { + if (outputAudioByteArray.isEmpty()) { qCWarning(audio) << "Error decoding MP3 file"; - return 0; + return AudioProperties(); } - _isStereo = (numChannels == 2); - _isAmbisonic = false; - _duration = (float)outputAudioByteArraySize / (sampleRate * numChannels * sizeof(int16_t)); - return sampleRate; + return properties; +} + + +QScriptValue soundSharedPointerToScriptValue(QScriptEngine* engine, const SharedSoundPointer& in) { + return engine->newQObject(new SoundScriptingInterface(in), QScriptEngine::ScriptOwnership); +} + +void soundSharedPointerFromScriptValue(const QScriptValue& object, SharedSoundPointer& out) { + if (auto soundInterface = qobject_cast(object.toQObject())) { + out = soundInterface->getSound(); + } +} + +SoundScriptingInterface::SoundScriptingInterface(const SharedSoundPointer& sound) : _sound(sound) { + // During shutdown we can sometimes get an empty sound pointer back + if (_sound) { + QObject::connect(_sound.data(), &Sound::ready, this, &SoundScriptingInterface::ready); + } +} + +Sound::Sound(const QUrl& url, bool isStereo, bool isAmbisonic) : Resource(url) { + _numChannels = isAmbisonic ? 4 : (isStereo ? 2 : 1); } diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index a0544870d0..836e28d582 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -19,61 +19,102 @@ #include +#include "AudioConstants.h" + +class AudioData; +using AudioDataPointer = std::shared_ptr; + +Q_DECLARE_METATYPE(AudioDataPointer); + +// AudioData is designed to be immutable +// All of its members and methods are const +// This makes it perfectly safe to access from multiple threads at once +class AudioData { +public: + using AudioSample = AudioConstants::AudioSample; + + // Allocates the buffer memory contiguous with the object + static AudioDataPointer make(uint32_t numSamples, uint32_t numChannels, + const AudioSample* samples); + + uint32_t getNumSamples() const { return _numSamples; } + uint32_t getNumChannels() const { return _numChannels; } + const AudioSample* data() const { return _data; } + const char* rawData() const { return reinterpret_cast(_data); } + + float isStereo() const { return _numChannels == 2; } + float isAmbisonic() const { return _numChannels == 4; } + float getDuration() const { return (float)_numSamples / (_numChannels * AudioConstants::SAMPLE_RATE); } + uint32_t getNumFrames() const { return _numSamples / _numChannels; } + uint32_t getNumBytes() const { return _numSamples * sizeof(AudioSample); } + +private: + AudioData(uint32_t numSamples, uint32_t numChannels, const AudioSample* samples); + + const uint32_t _numSamples { 0 }; + const uint32_t _numChannels { 0 }; + const AudioSample* const _data { nullptr }; +}; + class Sound : public Resource { Q_OBJECT public: Sound(const QUrl& url, bool isStereo = false, bool isAmbisonic = false); - bool isStereo() const { return _isStereo; } - bool isAmbisonic() const { return _isAmbisonic; } - bool isReady() const { return _isReady; } - float getDuration() const { return _duration; } - - const QByteArray& getByteArray() const { return _byteArray; } + bool isReady() const { return (bool)_audioData; } + + bool isStereo() const { return _audioData ? _audioData->isStereo() : false; } + bool isAmbisonic() const { return _audioData ? _audioData->isAmbisonic() : false; } + float getDuration() const { return _audioData ? _audioData->getDuration() : 0.0f; } + + AudioDataPointer getAudioData() const { return _audioData; } + + int getNumChannels() const { return _numChannels; } signals: void ready(); protected slots: - void soundProcessSuccess(QByteArray data, bool stereo, bool ambisonic, float duration); + void soundProcessSuccess(AudioDataPointer audioData); void soundProcessError(int error, QString str); private: - QByteArray _byteArray; - bool _isStereo; - bool _isAmbisonic; - bool _isReady; - float _duration; // In seconds - virtual void downloadFinished(const QByteArray& data) override; + + AudioDataPointer _audioData; + + // Only used for caching until the download has finished + int _numChannels { 0 }; }; class SoundProcessor : public QObject, public QRunnable { Q_OBJECT public: - SoundProcessor(const QUrl& url, const QByteArray& data, bool stereo, bool ambisonic) - : _url(url), _data(data), _isStereo(stereo), _isAmbisonic(ambisonic) - { - } + struct AudioProperties { + uint8_t numChannels { 0 }; + uint32_t sampleRate { 0 }; + }; + + SoundProcessor(QWeakPointer sound, QByteArray data); virtual void run() override; - void downSample(const QByteArray& rawAudioByteArray, int sampleRate); - int interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray); - int interpretAsMP3(const QByteArray& inputAudioByteArray, QByteArray& outputAudioByteArray); + QByteArray downSample(const QByteArray& rawAudioByteArray, + AudioProperties properties); + AudioProperties interpretAsWav(const QByteArray& inputAudioByteArray, + QByteArray& outputAudioByteArray); + AudioProperties interpretAsMP3(const QByteArray& inputAudioByteArray, + QByteArray& outputAudioByteArray); signals: - void onSuccess(QByteArray data, bool stereo, bool ambisonic, float duration); + void onSuccess(AudioDataPointer audioData); void onError(int error, QString str); private: - QUrl _url; - QByteArray _data; - bool _isStereo; - bool _isAmbisonic; - float _duration; + const QWeakPointer _sound; + const QByteArray _data; }; typedef QSharedPointer SharedSoundPointer; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 6e82d26f29..980ff8834c 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1031,7 +1031,14 @@ void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entit // Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2) const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f; const float stretchFactor = logf(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / logf(2.0f); - AudioInjector::playSound(collisionSound, volume, stretchFactor, collision.contactPoint); + + AudioInjectorOptions options; + options.stereo = collisionSound->isStereo(); + options.position = collision.contactPoint; + options.volume = volume; + options.pitch = 1.0f / stretchFactor; + + AudioInjector::playSoundAndDelete(collisionSound, options); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 3491688588..3d68a27bc9 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -954,11 +954,6 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { const QUuid myNodeID = nodeList->getSessionUUID(); if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { // don't delete other avatar's avatarEntities - // If you actually own the entity but the onwership property is not set because of a domain switch - // The lines below makes sure the entity is deleted once its properties are set. - auto avatarHashMap = DependencyManager::get(); - AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID); - myAvatar->insertDetachedEntityID(id); shouldSendDeleteToServer = false; return; } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index d5a372e093..dd10cd30b3 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -417,6 +417,30 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } +QMap getJointRotationOffsets(const QVariantHash& mapping) { + QMap jointRotationOffsets; + static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; + if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) { + auto offsets = mapping[JOINT_ROTATION_OFFSET_FIELD].toHash(); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + QString line = itr.value().toString(); + auto quatCoords = line.split(','); + if (quatCoords.size() == 4) { + float quatX = quatCoords[0].mid(1).toFloat(); + float quatY = quatCoords[1].toFloat(); + float quatZ = quatCoords[2].toFloat(); + float quatW = quatCoords[3].mid(0, quatCoords[3].size() - 1).toFloat(); + if (!isNaN(quatX) && !isNaN(quatY) && !isNaN(quatZ) && !isNaN(quatW)) { + glm::quat rotationOffset = glm::quat(quatW, quatX, quatY, quatZ); + jointRotationOffsets.insert(jointName, rotationOffset); + } + } + } + } + return jointRotationOffsets; +} + HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; @@ -1793,6 +1817,19 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& } } } + + auto offsets = getJointRotationOffsets(mapping); + hfmModel.jointRotationOffsets.clear(); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + glm::quat rotationOffset = itr.value(); + int jointIndex = hfmModel.getJointIndex(jointName); + if (jointIndex != -1) { + hfmModel.jointRotationOffsets.insert(jointIndex, rotationOffset); + } + qCDebug(modelformat) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; + } + return hfmModelPtr; } diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLTexelFormat.cpp index 4d94f8d8e7..fef823718f 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLTexelFormat.cpp @@ -329,6 +329,8 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { result = GL_RGBA8_SNORM; break; case gpu::NINT2_10_10_10: + result = GL_RGB10_A2; + break; case gpu::NUINT32: case gpu::NINT32: case gpu::COMPRESSED: @@ -729,9 +731,9 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_DEPTH_COMPONENT24; break; } + case gpu::NINT2_10_10_10: case gpu::COMPRESSED: case gpu::NUINT2: - case gpu::NINT2_10_10_10: case gpu::NUM_TYPES: { // quiet compiler Q_UNREACHABLE(); } @@ -893,9 +895,12 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.format = GL_RGBA; texel.internalFormat = GL_RGBA2; break; + case gpu::NINT2_10_10_10: + texel.format = GL_RGBA; + texel.internalFormat = GL_RGB10_A2; + break; case gpu::NUINT32: case gpu::NINT32: - case gpu::NINT2_10_10_10: case gpu::COMPRESSED: case gpu::NUM_TYPES: // quiet compiler Q_UNREACHABLE(); diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp index 43ae4691b9..cb0950c06d 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp @@ -49,26 +49,7 @@ void GL41Backend::do_draw(const Batch& batch, size_t paramOffset) { uint32 numVertices = batch._params[paramOffset + 1]._uint; uint32 startVertex = batch._params[paramOffset + 0]._uint; - if (isStereo()) { -#ifdef GPU_STEREO_DRAWCALL_INSTANCED - glDrawArraysInstanced(mode, startVertex, numVertices, 2); -#else - setupStereoSide(0); - glDrawArrays(mode, startVertex, numVertices); - setupStereoSide(1); - glDrawArrays(mode, startVertex, numVertices); -#endif - _stats._DSNumTriangles += 2 * numVertices / 3; - _stats._DSNumDrawcalls += 2; - - } else { - glDrawArrays(mode, startVertex, numVertices); - _stats._DSNumTriangles += numVertices / 3; - _stats._DSNumDrawcalls++; - } - _stats._DSNumAPIDrawcalls++; - - (void) CHECK_GL_ERROR(); + draw(mode, numVertices, startVertex); } void GL41Backend::do_drawIndexed(const Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp index e86eae2c2d..385ddca065 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -72,27 +72,7 @@ void GL45Backend::do_draw(const Batch& batch, size_t paramOffset) { uint32 numVertices = batch._params[paramOffset + 1]._uint; uint32 startVertex = batch._params[paramOffset + 0]._uint; - if (isStereo()) { -#ifdef GPU_STEREO_DRAWCALL_INSTANCED - glDrawArraysInstanced(mode, startVertex, numVertices, 2); -#else - setupStereoSide(0); - glDrawArrays(mode, startVertex, numVertices); - setupStereoSide(1); - glDrawArrays(mode, startVertex, numVertices); -#endif - - _stats._DSNumTriangles += 2 * numVertices / 3; - _stats._DSNumDrawcalls += 2; - - } else { - glDrawArrays(mode, startVertex, numVertices); - _stats._DSNumTriangles += numVertices / 3; - _stats._DSNumDrawcalls++; - } - _stats._DSNumAPIDrawcalls++; - - (void) CHECK_GL_ERROR(); + draw(mode, numVertices, startVertex); } void GL45Backend::do_drawIndexed(const Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp index cb40b4fc9b..12c1346c7b 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackend.cpp @@ -49,28 +49,7 @@ void GLESBackend::do_draw(const Batch& batch, size_t paramOffset) { uint32 numVertices = batch._params[paramOffset + 1]._uint; uint32 startVertex = batch._params[paramOffset + 0]._uint; - if (isStereo()) { -#ifdef GPU_STEREO_DRAWCALL_INSTANCED - glDrawArraysInstanced(mode, startVertex, numVertices, 2); -#else - - setupStereoSide(0); - glDrawArrays(mode, startVertex, numVertices); - setupStereoSide(1); - glDrawArrays(mode, startVertex, numVertices); - -#endif - _stats._DSNumTriangles += 2 * numVertices / 3; - _stats._DSNumDrawcalls += 2; - - } else { - glDrawArrays(mode, startVertex, numVertices); - _stats._DSNumTriangles += numVertices / 3; - _stats._DSNumDrawcalls++; - } - _stats._DSNumAPIDrawcalls++; - - (void) CHECK_GL_ERROR(); + draw(mode, numVertices, startVertex); } void GLESBackend::do_drawIndexed(const Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index e3ea210ecb..96b06ea6b0 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -13,6 +13,7 @@ #include #include +#include "ShaderConstants.h" #include "GPULogging.h" diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index dd0d510509..711059315e 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -14,6 +14,7 @@ #include "Frame.h" #include "GPULogging.h" +#include using namespace gpu; @@ -331,11 +332,20 @@ Size Context::getTextureResourcePopulatedGPUMemSize() { return Backend::textureResourcePopulatedGPUMemSize.getValue(); } +PipelinePointer Context::createMipGenerationPipeline(const ShaderPointer& ps) { + auto vs = gpu::Shader::createVertex(shader::gpu::vertex::DrawViewportQuadTransformTexcoord); + static gpu::StatePointer state(new gpu::State()); + + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + // Good to go add the brand new pipeline + return gpu::Pipeline::create(program, state); +} + Size Context::getTextureResourceIdealGPUMemSize() { return Backend::textureResourceIdealGPUMemSize.getValue(); } - BatchPointer Context::acquireBatch(const char* name) { Batch* rawBatch = nullptr; { diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 011f980957..654a34fe91 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -218,6 +218,8 @@ public: // Same as above but grabbed at every end of a frame void getFrameStats(ContextStats& stats) const; + static PipelinePointer createMipGenerationPipeline(const ShaderPointer& pixelShader); + double getFrameTimerGPUAverage() const; double getFrameTimerBatchAverage() const; diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 7d4479e681..05e48b6534 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -311,6 +311,8 @@ public: QString getModelNameOfMesh(int meshIndex) const; QList blendshapeChannelNames; + + QMap jointRotationOffsets; }; }; diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index c0c5a4d059..620ffb9641 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -234,7 +234,7 @@ private: #ifdef Q_OS_ANDROID Setting::Handle _enableInterstitialMode{ "enableInterstitialMode", false }; #else - Setting::Handle _enableInterstitialMode { "enableInterstitialMode", true }; + Setting::Handle _enableInterstitialMode { "enableInterstitialMode", false }; #endif QSet _domainConnectionRefusals; diff --git a/libraries/plugins/src/plugins/PluginUtils.cpp b/libraries/plugins/src/plugins/PluginUtils.cpp index ce67f7c585..f6419ccba0 100644 --- a/libraries/plugins/src/plugins/PluginUtils.cpp +++ b/libraries/plugins/src/plugins/PluginUtils.cpp @@ -65,6 +65,6 @@ bool PluginUtils::isOculusTouchControllerAvailable() { }; bool PluginUtils::isXboxControllerAvailable() { - return isSubdeviceContainingNameAvailable("X360 Controller"); + return isSubdeviceContainingNameAvailable("X360 Controller") || isSubdeviceContainingNameAvailable("XInput Controller"); }; diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index ced1570f37..0e380b6c02 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "RenderUtilsLogging.h" @@ -30,18 +31,28 @@ #include "DependencyManager.h" #include "ViewFrustum.h" - +gpu::PipelinePointer AmbientOcclusionEffect::_occlusionPipeline; +gpu::PipelinePointer AmbientOcclusionEffect::_bilateralBlurPipeline; +gpu::PipelinePointer AmbientOcclusionEffect::_mipCreationPipeline; +gpu::PipelinePointer AmbientOcclusionEffect::_gatherPipeline; +gpu::PipelinePointer AmbientOcclusionEffect::_buildNormalsPipeline; AmbientOcclusionFramebuffer::AmbientOcclusionFramebuffer() { } -void AmbientOcclusionFramebuffer::updateLinearDepth(const gpu::TexturePointer& linearDepthBuffer) { - //If the depth buffer or size changed, we need to delete our FBOs +bool AmbientOcclusionFramebuffer::update(const gpu::TexturePointer& linearDepthBuffer, int resolutionLevel, int depthResolutionLevel, bool isStereo) { + // If the depth buffer or size changed, we need to delete our FBOs bool reset = false; - if ((_linearDepthTexture != linearDepthBuffer)) { + if (_linearDepthTexture != linearDepthBuffer) { _linearDepthTexture = linearDepthBuffer; reset = true; } + if (_resolutionLevel != resolutionLevel || isStereo != _isStereo || _depthResolutionLevel != depthResolutionLevel) { + _resolutionLevel = resolutionLevel; + _depthResolutionLevel = depthResolutionLevel; + _isStereo = isStereo; + reset = true; + } if (_linearDepthTexture) { auto newFrameSize = glm::ivec2(_linearDepthTexture->getDimensions()); if (_frameSize != newFrameSize) { @@ -53,6 +64,8 @@ void AmbientOcclusionFramebuffer::updateLinearDepth(const gpu::TexturePointer& l if (reset) { clear(); } + + return reset; } void AmbientOcclusionFramebuffer::clear() { @@ -60,6 +73,8 @@ void AmbientOcclusionFramebuffer::clear() { _occlusionTexture.reset(); _occlusionBlurredFramebuffer.reset(); _occlusionBlurredTexture.reset(); + _normalFramebuffer.reset(); + _normalTexture.reset(); } gpu::TexturePointer AmbientOcclusionFramebuffer::getLinearDepthTexture() { @@ -67,19 +82,86 @@ gpu::TexturePointer AmbientOcclusionFramebuffer::getLinearDepthTexture() { } void AmbientOcclusionFramebuffer::allocate() { - - auto width = _frameSize.x; - auto height = _frameSize.y; +#if SSAO_BILATERAL_BLUR_USE_NORMAL + auto occlusionformat = gpu::Element{ gpu::VEC4, gpu::HALF, gpu::RGBA }; +#else + auto occlusionformat = gpu::Element{ gpu::VEC3, gpu::NUINT8, gpu::RGB }; +#endif - _occlusionTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); - _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusion")); - _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); + // Full frame + { + auto width = _frameSize.x; + auto height = _frameSize.y; + auto sampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - _occlusionBlurredTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); - _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusionBlurred")); - _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); + _occlusionTexture = gpu::Texture::createRenderBuffer(occlusionformat, width, height, gpu::Texture::SINGLE_MIP, sampler); + _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusion")); + _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); + + _occlusionBlurredTexture = gpu::Texture::createRenderBuffer(occlusionformat, width, height, gpu::Texture::SINGLE_MIP, sampler); + _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusionBlurred")); + _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); + } + + // Lower res frame + { + auto sideSize = _frameSize; + if (_isStereo) { + sideSize.x >>= 1; + } + sideSize >>= _resolutionLevel; + if (_isStereo) { + sideSize.x <<= 1; + } + auto width = sideSize.x; + auto height = sideSize.y; + auto format = gpu::Element{ gpu::VEC4, gpu::NINT2_10_10_10, gpu::RGBA }; + _normalTexture = gpu::Texture::createRenderBuffer(format, width, height, gpu::Texture::SINGLE_MIP, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT, gpu::Sampler::WRAP_CLAMP)); + _normalFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("ssaoNormals")); + _normalFramebuffer->setRenderBuffer(0, _normalTexture); + } + +#if SSAO_USE_QUAD_SPLIT + { + auto splitSize = _frameSize; + if (_isStereo) { + splitSize.x >>= 1; + } + splitSize = divideRoundUp(_frameSize >> _resolutionLevel, SSAO_SPLIT_COUNT); + if (_isStereo) { + splitSize.x <<= 1; + } + auto width = splitSize.x; + auto height = splitSize.y; + + _occlusionSplitTexture = gpu::Texture::createRenderBufferArray(occlusionformat, width, height, SSAO_SPLIT_COUNT*SSAO_SPLIT_COUNT, gpu::Texture::SINGLE_MIP, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP)); + for (int i = 0; i < SSAO_SPLIT_COUNT*SSAO_SPLIT_COUNT; i++) { + _occlusionSplitFramebuffers[i] = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusion")); + _occlusionSplitFramebuffers[i]->setRenderBuffer(0, _occlusionSplitTexture, i); + } + } +#endif } +#if SSAO_USE_QUAD_SPLIT +gpu::FramebufferPointer AmbientOcclusionFramebuffer::getOcclusionSplitFramebuffer(int index) { + assert(index < SSAO_SPLIT_COUNT*SSAO_SPLIT_COUNT); + if (!_occlusionSplitFramebuffers[index]) { + allocate(); + } + return _occlusionSplitFramebuffers[index]; +} + +gpu::TexturePointer AmbientOcclusionFramebuffer::getOcclusionSplitTexture() { + if (!_occlusionSplitTexture) { + allocate(); + } + return _occlusionSplitTexture; +} +#endif + gpu::FramebufferPointer AmbientOcclusionFramebuffer::getOcclusionFramebuffer() { if (!_occlusionFramebuffer) { allocate(); @@ -108,61 +190,114 @@ gpu::TexturePointer AmbientOcclusionFramebuffer::getOcclusionBlurredTexture() { return _occlusionBlurredTexture; } +gpu::FramebufferPointer AmbientOcclusionFramebuffer::getNormalFramebuffer() { + if (!_normalFramebuffer) { + allocate(); + } + return _normalFramebuffer; +} -class GaussianDistribution { -public: - - static double integral(float x, float deviation) { - return 0.5 * erf((double)x / ((double)deviation * sqrt(2.0))); +gpu::TexturePointer AmbientOcclusionFramebuffer::getNormalTexture() { + if (!_normalTexture) { + allocate(); } - - static double rangeIntegral(float x0, float x1, float deviation) { - return integral(x1, deviation) - integral(x0, deviation); - } - - static std::vector evalSampling(int samplingRadius, float deviation) { - std::vector coefs(samplingRadius + 1, 0.0f); - - // corner case when radius is 0 or under - if (samplingRadius <= 0) { - coefs[0] = 1.0f; - return coefs; - } - - // Evaluate all the samples range integral of width 1 from center until the penultimate one - float halfWidth = 0.5f; - double sum = 0.0; - for (int i = 0; i < samplingRadius; i++) { - float x = (float) i; - double sample = rangeIntegral(x - halfWidth, x + halfWidth, deviation); - coefs[i] = sample; - sum += sample; - } - - // last sample goes to infinity - float lastSampleX0 = (float) samplingRadius - halfWidth; - float largeEnough = lastSampleX0 + 1000.0f * deviation; - double sample = rangeIntegral(lastSampleX0, largeEnough, deviation); - coefs[samplingRadius] = sample; - sum += sample; - - return coefs; - } - - static void evalSampling(float* coefs, unsigned int coefsLength, int samplingRadius, float deviation) { - auto coefsVector = evalSampling(samplingRadius, deviation); - if (coefsLength> coefsVector.size() + 1) { - unsigned int coefsNum = 0; - for (auto s : coefsVector) { - coefs[coefsNum] = s; - coefsNum++; - } - for (;coefsNum < coefsLength; coefsNum++) { - coefs[coefsNum] = 0.0f; - } - } - } -}; + return _normalTexture; +} + +AmbientOcclusionEffectConfig::AmbientOcclusionEffectConfig() : + render::GPUJobConfig::Persistent(QStringList() << "Render" << "Engine" << "Ambient Occlusion", false), + perspectiveScale{ 1.0f }, + edgeSharpness{ 1.0f }, + blurRadius{ 4 }, + resolutionLevel{ 2 }, + + ssaoRadius{ 1.0f }, + ssaoObscuranceLevel{ 0.4f }, + ssaoFalloffAngle{ 0.15f }, + ssaoNumSpiralTurns{ 7.0f }, + ssaoNumSamples{ 32 }, + + hbaoRadius{ 0.7f }, + hbaoObscuranceLevel{ 0.75f }, + hbaoFalloffAngle{ 0.3f }, + hbaoNumSamples{ 1 }, + + horizonBased{ false }, + ditheringEnabled{ true }, + borderingEnabled{ true }, + fetchMipsEnabled{ true }, + jitterEnabled{ false }{ +} + +void AmbientOcclusionEffectConfig::setSSAORadius(float newRadius) { + ssaoRadius = std::max(0.01f, newRadius); emit dirty(); +} + +void AmbientOcclusionEffectConfig::setSSAOObscuranceLevel(float level) { + ssaoObscuranceLevel = std::max(0.01f, level); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setSSAOFalloffAngle(float bias) { + ssaoFalloffAngle = std::max(0.0f, std::min(bias, 1.0f)); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setSSAONumSpiralTurns(float turns) { + ssaoNumSpiralTurns = std::max(0.0f, (float)turns); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setSSAONumSamples(int samples) { + ssaoNumSamples = std::max(1.0f, (float)samples); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setHBAORadius(float newRadius) { + hbaoRadius = std::max(0.01f, newRadius); emit dirty(); +} + +void AmbientOcclusionEffectConfig::setHBAOObscuranceLevel(float level) { + hbaoObscuranceLevel = std::max(0.01f, level); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setHBAOFalloffAngle(float bias) { + hbaoFalloffAngle = std::max(0.0f, std::min(bias, 1.0f)); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setHBAONumSamples(int samples) { + hbaoNumSamples = std::max(1.0f, (float)samples); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setEdgeSharpness(float sharpness) { + edgeSharpness = std::max(0.0f, (float)sharpness); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setResolutionLevel(int level) { + resolutionLevel = std::max(0, std::min(level, MAX_RESOLUTION_LEVEL)); + emit dirty(); +} + +void AmbientOcclusionEffectConfig::setBlurRadius(int radius) { + blurRadius = std::max(0, std::min(MAX_BLUR_RADIUS, radius)); + emit dirty(); +} + +AmbientOcclusionEffect::AOParameters::AOParameters() { + _resolutionInfo = glm::vec4{ 0.0f }; + _radiusInfo = glm::vec4{ 0.0f }; + _ditheringInfo = glm::vec4{ 0.0f }; + _sampleInfo = glm::vec4{ 0.0f }; + _falloffInfo = glm::vec4{ 0.0f }; +} + +AmbientOcclusionEffect::BlurParameters::BlurParameters() { + _blurInfo = { 1.0f, 2.0f, 0.0f, 3.0f }; +} AmbientOcclusionEffect::AmbientOcclusionEffect() { } @@ -170,85 +305,215 @@ AmbientOcclusionEffect::AmbientOcclusionEffect() { void AmbientOcclusionEffect::configure(const Config& config) { DependencyManager::get()->setAmbientOcclusionEnabled(config.enabled); - bool shouldUpdateGaussian = false; + bool shouldUpdateBlurs = false; + bool shouldUpdateTechnique = false; - const double RADIUS_POWER = 6.0; - const auto& radius = config.radius; - if (radius != _parametersBuffer->getRadius()) { - auto& current = _parametersBuffer.edit().radiusInfo; - current.x = radius; - current.y = radius * radius; - current.z = (float)(1.0 / pow((double)radius, RADIUS_POWER)); - } - - if (config.obscuranceLevel != _parametersBuffer->getObscuranceLevel()) { - auto& current = _parametersBuffer.edit().radiusInfo; - current.w = config.obscuranceLevel; - } - - if (config.falloffBias != _parametersBuffer->getFalloffBias()) { - auto& current = _parametersBuffer.edit().ditheringInfo; - current.z = config.falloffBias; - } - - if (config.edgeSharpness != _parametersBuffer->getEdgeSharpness()) { - auto& current = _parametersBuffer.edit().blurInfo; - current.x = config.edgeSharpness; - } - - if (config.blurDeviation != _parametersBuffer->getBlurDeviation()) { - auto& current = _parametersBuffer.edit().blurInfo; - current.z = config.blurDeviation; - shouldUpdateGaussian = true; - } - - if (config.numSpiralTurns != _parametersBuffer->getNumSpiralTurns()) { - auto& current = _parametersBuffer.edit().sampleInfo; - current.z = config.numSpiralTurns; - } - - if (config.numSamples != _parametersBuffer->getNumSamples()) { - auto& current = _parametersBuffer.edit().sampleInfo; - current.x = config.numSamples; - current.y = 1.0f / config.numSamples; - } - - if (config.fetchMipsEnabled != _parametersBuffer->isFetchMipsEnabled()) { - auto& current = _parametersBuffer.edit().sampleInfo; - current.w = (float)config.fetchMipsEnabled; - } + _isJitterEnabled = config.jitterEnabled; if (!_framebuffer) { _framebuffer = std::make_shared(); - } - - if (config.perspectiveScale != _parametersBuffer->getPerspectiveScale()) { - _parametersBuffer.edit().resolutionInfo.z = config.perspectiveScale; - } - if (config.resolutionLevel != _parametersBuffer->getResolutionLevel()) { - auto& current = _parametersBuffer.edit().resolutionInfo; - current.x = (float) config.resolutionLevel; - } - - if (config.blurRadius != _parametersBuffer->getBlurRadius()) { - auto& current = _parametersBuffer.edit().blurInfo; - current.y = (float)config.blurRadius; - shouldUpdateGaussian = true; + shouldUpdateBlurs = true; } - if (config.ditheringEnabled != _parametersBuffer->isDitheringEnabled()) { - auto& current = _parametersBuffer.edit().ditheringInfo; + // Update bilateral blur + if (config.blurRadius != _hblurParametersBuffer->getBlurRadius() || _blurEdgeSharpness != config.edgeSharpness) { + const float BLUR_EDGE_DISTANCE_SCALE = float(10000 * SSAO_DEPTH_KEY_SCALE); + const float BLUR_EDGE_NORMAL_SCALE = 2.0f; + + auto& hblur = _hblurParametersBuffer.edit()._blurInfo; + auto& vblur = _vblurParametersBuffer.edit()._blurInfo; + float blurRadialSigma = float(config.blurRadius) * 0.5f; + float blurRadialScale = 1.0f / (2.0f*blurRadialSigma*blurRadialSigma); + glm::vec3 blurScales = -glm::vec3(blurRadialScale, glm::vec2(BLUR_EDGE_DISTANCE_SCALE, BLUR_EDGE_NORMAL_SCALE) * config.edgeSharpness); + + _blurEdgeSharpness = config.edgeSharpness; + + hblur.x = blurScales.x; + hblur.y = blurScales.y; + hblur.z = blurScales.z; + hblur.w = (float)config.blurRadius; + + vblur.x = blurScales.x; + vblur.y = blurScales.y; + vblur.z = blurScales.z; + vblur.w = (float)config.blurRadius; + } + + if (_aoParametersBuffer->isHorizonBased() != config.horizonBased) { + auto& current = _aoParametersBuffer.edit()._resolutionInfo; + current.y = config.horizonBased & 1; + shouldUpdateTechnique = true; + } + + if (config.fetchMipsEnabled != _aoParametersBuffer->isFetchMipsEnabled()) { + auto& current = _aoParametersBuffer.edit()._sampleInfo; + current.w = (float)config.fetchMipsEnabled; + } + + if (config.perspectiveScale != _aoParametersBuffer->getPerspectiveScale()) { + _aoParametersBuffer.edit()._resolutionInfo.z = config.perspectiveScale; + } + + if (config.resolutionLevel != _aoParametersBuffer->getResolutionLevel()) { + auto& current = _aoParametersBuffer.edit()._resolutionInfo; + current.x = (float)config.resolutionLevel; + shouldUpdateBlurs = true; + } + + if (config.ditheringEnabled != _aoParametersBuffer->isDitheringEnabled()) { + auto& current = _aoParametersBuffer.edit()._ditheringInfo; current.x = (float)config.ditheringEnabled; } - if (config.borderingEnabled != _parametersBuffer->isBorderingEnabled()) { - auto& current = _parametersBuffer.edit().ditheringInfo; + if (config.borderingEnabled != _aoParametersBuffer->isBorderingEnabled()) { + auto& current = _aoParametersBuffer.edit()._ditheringInfo; current.w = (float)config.borderingEnabled; } - if (shouldUpdateGaussian) { - updateGaussianDistribution(); + if (config.horizonBased) { + // Configure for HBAO + const auto& radius = config.hbaoRadius; + if (shouldUpdateTechnique || radius != _aoParametersBuffer->getRadius()) { + auto& current = _aoParametersBuffer.edit()._radiusInfo; + current.x = radius; + current.y = radius * radius; + current.z = 1.0f / current.y; + } + + if (shouldUpdateTechnique || config.hbaoObscuranceLevel != _aoParametersBuffer->getObscuranceLevel()) { + auto& current = _aoParametersBuffer.edit()._radiusInfo; + current.w = config.hbaoObscuranceLevel; + } + + if (shouldUpdateTechnique || config.hbaoFalloffAngle != _aoParametersBuffer->getFalloffAngle()) { + auto& current = _aoParametersBuffer.edit()._falloffInfo; + current.x = config.hbaoFalloffAngle; + current.y = 1.0f / (1.0f - current.x); + // Compute sin from cos + current.z = sqrtf(1.0f - config.hbaoFalloffAngle * config.hbaoFalloffAngle); + current.w = 1.0f / current.z; + } + + if (shouldUpdateTechnique || config.hbaoNumSamples != _aoParametersBuffer->getNumSamples()) { + auto& current = _aoParametersBuffer.edit()._sampleInfo; + current.x = config.hbaoNumSamples; + current.y = 1.0f / config.hbaoNumSamples; + updateRandomSamples(); + updateJitterSamples(); + } + } else { + // Configure for SSAO + const double RADIUS_POWER = 6.0; + const auto& radius = config.ssaoRadius; + if (shouldUpdateTechnique || radius != _aoParametersBuffer->getRadius()) { + auto& current = _aoParametersBuffer.edit()._radiusInfo; + current.x = radius; + current.y = radius * radius; + current.z = (float)(10.0 / pow((double)radius, RADIUS_POWER)); + } + + if (shouldUpdateTechnique || config.ssaoObscuranceLevel != _aoParametersBuffer->getObscuranceLevel()) { + auto& current = _aoParametersBuffer.edit()._radiusInfo; + current.w = config.ssaoObscuranceLevel; + } + + if (shouldUpdateTechnique || config.ssaoFalloffAngle != _aoParametersBuffer->getFalloffAngle()) { + auto& current = _aoParametersBuffer.edit()._falloffInfo; + current.x = config.ssaoFalloffAngle; + } + + if (shouldUpdateTechnique || config.ssaoNumSpiralTurns != _aoParametersBuffer->getNumSpiralTurns()) { + auto& current = _aoParametersBuffer.edit()._sampleInfo; + current.z = config.ssaoNumSpiralTurns; + } + + if (shouldUpdateTechnique || config.ssaoNumSamples != _aoParametersBuffer->getNumSamples()) { + auto& current = _aoParametersBuffer.edit()._sampleInfo; + current.x = config.ssaoNumSamples; + current.y = 1.0f / config.ssaoNumSamples; + updateRandomSamples(); + updateJitterSamples(); + } } + + if (shouldUpdateBlurs) { + updateBlurParameters(); + } +} + +void AmbientOcclusionEffect::updateRandomSamples() { + // Regenerate offsets + if (_aoParametersBuffer->isHorizonBased()) { + const int B = 3; + const float invB = 1.0f / (float)B; + float sampleScale = float(2.0 * M_PI / double(_aoParametersBuffer->getNumSamples())); + + for (size_t i = 0; i < _randomSamples.size(); i++) { + auto index = i + 1; // Indices start at 1, not 0 + float f = 1.0f; + float r = 0.0f; + + while (index > 0) { + f = f * invB; + r = r + f * (float)(index % B); + index = index / B; + } + _randomSamples[i] = r * sampleScale; + } + } else { + for (size_t i = 0; i < _randomSamples.size(); i++) { + _randomSamples[i] = randFloat() * float(2.0 * M_PI); + } + } +} +void AmbientOcclusionEffect::updateBlurParameters() { + const auto resolutionLevel = _aoParametersBuffer->getResolutionLevel(); + auto& vblur = _vblurParametersBuffer.edit(); + auto& hblur = _hblurParametersBuffer.edit(); + auto frameSize = _framebuffer->getSourceFrameSize(); + if (_framebuffer->isStereo()) { + frameSize.x >>= 1; + } + const auto occlusionSize = frameSize >> resolutionLevel; + + // Occlusion UV limit + hblur._blurAxis.z = occlusionSize.x / float(frameSize.x); + hblur._blurAxis.w = occlusionSize.y / float(frameSize.y); + + vblur._blurAxis.z = 1.0f; + vblur._blurAxis.w = occlusionSize.y / float(frameSize.y); + + // Occlusion axis + hblur._blurAxis.x = hblur._blurAxis.z / occlusionSize.x; + hblur._blurAxis.y = 0.0f; + + vblur._blurAxis.x = 0.0f; + vblur._blurAxis.y = vblur._blurAxis.w / occlusionSize.y; +} + +void AmbientOcclusionEffect::updateFramebufferSizes() { + auto& params = _aoParametersBuffer.edit(); + const int stereoDivide = _framebuffer->isStereo() & 1; + auto sourceFrameSideSize = _framebuffer->getSourceFrameSize(); + sourceFrameSideSize.x >>= stereoDivide; + + const int resolutionLevel = _aoParametersBuffer.get().getResolutionLevel(); + const int depthResolutionLevel = getDepthResolutionLevel(); + const auto occlusionFrameSize = sourceFrameSideSize >> resolutionLevel; + auto normalTextureSize = _framebuffer->getNormalTexture()->getDimensions(); + + normalTextureSize.x >>= stereoDivide; + + params._sideSizes[0].x = normalTextureSize.x; + params._sideSizes[0].y = normalTextureSize.y; + params._sideSizes[0].z = resolutionLevel; + params._sideSizes[0].w = depthResolutionLevel; + + params._sideSizes[1].x = params._sideSizes[0].x; + params._sideSizes[1].y = params._sideSizes[0].y; + auto occlusionSplitSize = divideRoundUp(occlusionFrameSize, SSAO_SPLIT_COUNT); + params._sideSizes[1].z = occlusionSplitSize.x; + params._sideSizes[1].w = occlusionSplitSize.y; } const gpu::PipelinePointer& AmbientOcclusionEffect::getOcclusionPipeline() { @@ -256,7 +521,7 @@ const gpu::PipelinePointer& AmbientOcclusionEffect::getOcclusionPipeline() { gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::ssao_makeOcclusion); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setColorWriteMask(true, true, true, false); + state->setColorWriteMask(true, true, true, true); // Good to go add the brand new pipeline _occlusionPipeline = gpu::Pipeline::create(program, state); @@ -264,37 +529,66 @@ const gpu::PipelinePointer& AmbientOcclusionEffect::getOcclusionPipeline() { return _occlusionPipeline; } - -const gpu::PipelinePointer& AmbientOcclusionEffect::getHBlurPipeline() { - if (!_hBlurPipeline) { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::ssao_makeHorizontalBlur); +const gpu::PipelinePointer& AmbientOcclusionEffect::getBilateralBlurPipeline() { + if (!_bilateralBlurPipeline) { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::ssao_bilateralBlur); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setColorWriteMask(true, true, true, false); // Good to go add the brand new pipeline - _hBlurPipeline = gpu::Pipeline::create(program, state); + _bilateralBlurPipeline = gpu::Pipeline::create(program, state); } - return _hBlurPipeline; + return _bilateralBlurPipeline; } -const gpu::PipelinePointer& AmbientOcclusionEffect::getVBlurPipeline() { - if (!_vBlurPipeline) { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::ssao_makeVerticalBlur); +const gpu::PipelinePointer& AmbientOcclusionEffect::getMipCreationPipeline() { + if (!_mipCreationPipeline) { + _mipCreationPipeline = gpu::Context::createMipGenerationPipeline(gpu::Shader::createPixel(shader::render_utils::fragment::ssao_mip_depth)); + } + return _mipCreationPipeline; +} + +const gpu::PipelinePointer& AmbientOcclusionEffect::getGatherPipeline() { + if (!_gatherPipeline) { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::ssao_gather); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - // Vertical blur write just the final result Occlusion value in the alpha channel - state->setColorWriteMask(true, true, true, false); + + state->setColorWriteMask(true, true, true, true); // Good to go add the brand new pipeline - _vBlurPipeline = gpu::Pipeline::create(program, state); + _gatherPipeline = gpu::Pipeline::create(program, state); } - return _vBlurPipeline; + return _gatherPipeline; } -void AmbientOcclusionEffect::updateGaussianDistribution() { - auto coefs = _parametersBuffer.edit()._gaussianCoefs; - GaussianDistribution::evalSampling(coefs, Parameters::GAUSSIAN_COEFS_LENGTH, _parametersBuffer->getBlurRadius(), _parametersBuffer->getBlurDeviation()); +const gpu::PipelinePointer& AmbientOcclusionEffect::getBuildNormalsPipeline() { + if (!_buildNormalsPipeline) { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::ssao_buildNormals); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setColorWriteMask(true, true, true, true); + + // Good to go add the brand new pipeline + _buildNormalsPipeline = gpu::Pipeline::create(program, state); + } + return _buildNormalsPipeline; +} + +int AmbientOcclusionEffect::getDepthResolutionLevel() const { + return std::min(1, _aoParametersBuffer->getResolutionLevel()); +} + +void AmbientOcclusionEffect::updateJitterSamples() { + if (_aoParametersBuffer->isHorizonBased()) { + for (int splitId = 0; splitId < SSAO_SPLIT_COUNT*SSAO_SPLIT_COUNT; splitId++) { + auto& sample = _aoFrameParametersBuffer[splitId].edit(); + sample._angleInfo.x = _randomSamples[splitId + SSAO_RANDOM_SAMPLE_COUNT * _frameId]; + } + } else { + auto& sample = _aoFrameParametersBuffer[0].edit(); + sample._angleInfo.x = _randomSamples[_frameId]; + } } void AmbientOcclusionEffect::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { @@ -306,9 +600,16 @@ void AmbientOcclusionEffect::run(const render::RenderContextPointer& renderConte const auto& frameTransform = inputs.get0(); const auto& linearDepthFramebuffer = inputs.get2(); - auto linearDepthTexture = linearDepthFramebuffer->getLinearDepthTexture(); + const int resolutionLevel = _aoParametersBuffer->getResolutionLevel(); + const auto depthResolutionLevel = getDepthResolutionLevel(); + const auto isHorizonBased = _aoParametersBuffer->isHorizonBased(); + + auto fullResDepthTexture = linearDepthFramebuffer->getLinearDepthTexture(); + auto occlusionDepthTexture = fullResDepthTexture; auto sourceViewport = args->_viewport; - auto occlusionViewport = sourceViewport; + auto occlusionViewport = sourceViewport >> resolutionLevel; + auto firstBlurViewport = sourceViewport; + firstBlurViewport.w = occlusionViewport.w; if (!_gpuTimer) { _gpuTimer = std::make_shared < gpu::RangeTimer>(__FUNCTION__); @@ -317,80 +618,202 @@ void AmbientOcclusionEffect::run(const render::RenderContextPointer& renderConte if (!_framebuffer) { _framebuffer = std::make_shared(); } - - if (_parametersBuffer->getResolutionLevel() > 0) { - linearDepthTexture = linearDepthFramebuffer->getHalfLinearDepthTexture(); - occlusionViewport = occlusionViewport >> _parametersBuffer->getResolutionLevel(); + + if (depthResolutionLevel > 0) { + occlusionDepthTexture = linearDepthFramebuffer->getHalfLinearDepthTexture(); } - _framebuffer->updateLinearDepth(linearDepthTexture); - + if (_framebuffer->update(fullResDepthTexture, resolutionLevel, depthResolutionLevel, args->isStereo())) { + updateBlurParameters(); + updateFramebufferSizes(); + } auto occlusionFBO = _framebuffer->getOcclusionFramebuffer(); auto occlusionBlurredFBO = _framebuffer->getOcclusionBlurredFramebuffer(); outputs.edit0() = _framebuffer; - outputs.edit1() = _parametersBuffer; - - auto framebufferSize = _framebuffer->getSourceFrameSize(); - - float sMin = occlusionViewport.x / (float)framebufferSize.x; - float sWidth = occlusionViewport.z / (float)framebufferSize.x; - float tMin = occlusionViewport.y / (float)framebufferSize.y; - float tHeight = occlusionViewport.w / (float)framebufferSize.y; - + outputs.edit1() = _aoParametersBuffer; auto occlusionPipeline = getOcclusionPipeline(); - auto firstHBlurPipeline = getHBlurPipeline(); - auto lastVBlurPipeline = getVBlurPipeline(); - + auto bilateralBlurPipeline = getBilateralBlurPipeline(); + auto mipCreationPipeline = getMipCreationPipeline(); +#if SSAO_USE_QUAD_SPLIT + auto gatherPipeline = getGatherPipeline(); + auto buildNormalsPipeline = getBuildNormalsPipeline(); + auto occlusionNormalFramebuffer = _framebuffer->getNormalFramebuffer(); + auto occlusionNormalTexture = _framebuffer->getNormalTexture(); + auto normalViewport = glm::ivec4{ 0, 0, occlusionNormalFramebuffer->getWidth(), occlusionNormalFramebuffer->getHeight() }; + auto splitSize = glm::ivec2(_framebuffer->getOcclusionSplitTexture()->getDimensions()); + auto splitViewport = glm::ivec4{ 0, 0, splitSize.x, splitSize.y }; +#endif + + // Update sample rotation + if (_isJitterEnabled) { + updateJitterSamples(); + _frameId = (_frameId + 1) % (SSAO_RANDOM_SAMPLE_COUNT); + } + gpu::doInBatch("AmbientOcclusionEffect::run", args->_context, [=](gpu::Batch& batch) { - batch.enableStereo(false); + PROFILE_RANGE_BATCH(batch, "SSAO"); + batch.enableStereo(false); _gpuTimer->begin(batch); - batch.setViewportTransform(occlusionViewport); - batch.setProjectionTransform(glm::mat4()); batch.resetViewTransform(); - Transform model; - model.setTranslation(glm::vec3(sMin, tMin, 0.0f)); - model.setScale(glm::vec3(sWidth, tHeight, 1.0f)); - batch.setModelTransform(model); + batch.setProjectionTransform(glm::mat4()); + batch.setModelTransform(Transform()); + + // We need this with the mips levels + batch.pushProfileRange("Depth Mips"); + if (isHorizonBased) { + batch.setPipeline(mipCreationPipeline); + batch.generateTextureMipsWithPipeline(occlusionDepthTexture); + } else { + batch.generateTextureMips(occlusionDepthTexture); + } + batch.popProfileRange(); + +#if SSAO_USE_QUAD_SPLIT + batch.pushProfileRange("Normal Gen."); + // Build face normals pass + batch.setModelTransform(Transform()); + batch.setViewportTransform(normalViewport); + batch.setPipeline(buildNormalsPipeline); + batch.setResourceTexture(render_utils::slot::texture::SsaoDepth, fullResDepthTexture); + batch.setResourceTexture(render_utils::slot::texture::SsaoNormal, nullptr); + batch.setUniformBuffer(render_utils::slot::buffer::DeferredFrameTransform, frameTransform->getFrameTransformBuffer()); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoParams, _aoParametersBuffer); + batch.setFramebuffer(occlusionNormalFramebuffer); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.popProfileRange(); +#endif + + // Occlusion pass + batch.pushProfileRange("Occlusion"); batch.setUniformBuffer(render_utils::slot::buffer::DeferredFrameTransform, frameTransform->getFrameTransformBuffer()); - batch.setUniformBuffer(render_utils::slot::buffer::SsaoParams, _parametersBuffer); - - - // We need this with the mips levels - batch.generateTextureMips(_framebuffer->getLinearDepthTexture()); - - // Occlusion pass - batch.setFramebuffer(occlusionFBO); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(1.0f)); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoParams, _aoParametersBuffer); batch.setPipeline(occlusionPipeline); - batch.setResourceTexture(render_utils::slot::texture::SsaoPyramid, _framebuffer->getLinearDepthTexture()); - batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.setResourceTexture(render_utils::slot::texture::SsaoDepth, occlusionDepthTexture); - - if (_parametersBuffer->getBlurRadius() > 0) { - // Blur 1st pass - batch.setFramebuffer(occlusionBlurredFBO); - batch.setPipeline(firstHBlurPipeline); - batch.setResourceTexture(render_utils::slot::texture::SsaoOcclusion, occlusionFBO->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); + if (_aoParametersBuffer->isHorizonBased()) { +#if SSAO_USE_QUAD_SPLIT + batch.setResourceTexture(render_utils::slot::texture::SsaoNormal, occlusionNormalTexture); + { + const auto uvScale = glm::vec3( + (splitSize.x * SSAO_SPLIT_COUNT) / float(occlusionViewport.z), + (splitSize.y * SSAO_SPLIT_COUNT) / float(occlusionViewport.w), + 1.0f); + const auto postPixelOffset = glm::vec2(0.5f) / glm::vec2(occlusionViewport.z, occlusionViewport.w); + const auto prePixelOffset = glm::vec2(0.5f * uvScale.x, 0.5f * uvScale.y) / glm::vec2(splitSize); + Transform model; - // Blur 2nd pass + batch.setViewportTransform(splitViewport); + + model.setScale(uvScale); + for (int y = 0; y < SSAO_SPLIT_COUNT; y++) { + for (int x = 0; x < SSAO_SPLIT_COUNT; x++) { + const int splitIndex = x + y * SSAO_SPLIT_COUNT; + const auto uvTranslate = glm::vec3( + postPixelOffset.x * (2 * x + 1) - prePixelOffset.x, + postPixelOffset.y * (2 * y + 1) - prePixelOffset.y, + 0.0f + ); + model.setTranslation(uvTranslate); + batch.setModelTransform(model); + batch.setFramebuffer(_framebuffer->getOcclusionSplitFramebuffer(splitIndex)); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoFrameParams, _aoFrameParametersBuffer[splitIndex]); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + } +#else + batch.setViewportTransform(occlusionViewport); + batch.setModelTransform(Transform()); batch.setFramebuffer(occlusionFBO); - batch.setPipeline(lastVBlurPipeline); - batch.setResourceTexture(render_utils::slot::texture::SsaoOcclusion, occlusionBlurredFBO->getRenderBuffer(0)); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoFrameParams, _aoFrameParametersBuffer[0]); + batch.draw(gpu::TRIANGLE_STRIP, 4); +#endif + } else { +#if SSAO_USE_QUAD_SPLIT + batch.setResourceTexture(render_utils::slot::texture::SsaoNormal, occlusionNormalTexture); +#endif + batch.setViewportTransform(occlusionViewport); + batch.setModelTransform(Transform()); + batch.setFramebuffer(occlusionFBO); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoFrameParams, _aoFrameParametersBuffer[0]); batch.draw(gpu::TRIANGLE_STRIP, 4); } - - - batch.setResourceTexture(render_utils::slot::texture::SsaoPyramid, nullptr); + + batch.popProfileRange(); + +#if SSAO_USE_QUAD_SPLIT + if (_aoParametersBuffer->isHorizonBased()) { + // Gather back the four separate renders into one interleaved one + batch.pushProfileRange("Gather"); + batch.setViewportTransform(occlusionViewport); + batch.setModelTransform(Transform()); + batch.setFramebuffer(occlusionFBO); + batch.setPipeline(gatherPipeline); + batch.setResourceTexture(render_utils::slot::texture::SsaoOcclusion, _framebuffer->getOcclusionSplitTexture()); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.popProfileRange(); + } +#endif + + { + PROFILE_RANGE_BATCH(batch, "Bilateral Blur"); + // Blur 1st pass + batch.pushProfileRange("Horiz."); + { + const auto uvScale = glm::vec3( + occlusionViewport.z / float(sourceViewport.z), + occlusionViewport.w / float(sourceViewport.w), + 1.0f); + Transform model; + model.setScale(uvScale); + batch.setModelTransform(model); + } + batch.setPipeline(bilateralBlurPipeline); + // Use full resolution depth for bilateral upscaling and blur + batch.setResourceTexture(render_utils::slot::texture::SsaoDepth, fullResDepthTexture); +#if SSAO_USE_QUAD_SPLIT + batch.setResourceTexture(render_utils::slot::texture::SsaoNormal, occlusionNormalTexture); +#else + batch.setResourceTexture(render_utils::slot::texture::SsaoNormal, nullptr); +#endif + batch.setViewportTransform(firstBlurViewport); + batch.setFramebuffer(occlusionBlurredFBO); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoBlurParams, _hblurParametersBuffer); + batch.setResourceTexture(render_utils::slot::texture::SsaoOcclusion, occlusionFBO->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.popProfileRange(); + + // Blur 2nd pass + batch.pushProfileRange("Vert."); + { + const auto uvScale = glm::vec3( + 1.0f, + occlusionViewport.w / float(sourceViewport.w), + 1.0f); + + Transform model; + model.setScale(uvScale); + batch.setModelTransform(model); + } + batch.setViewportTransform(sourceViewport); + batch.setFramebuffer(occlusionFBO); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoBlurParams, _vblurParametersBuffer); + batch.setResourceTexture(render_utils::slot::texture::SsaoOcclusion, occlusionBlurredFBO->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + batch.popProfileRange(); + } + + batch.setResourceTexture(render_utils::slot::texture::SsaoDepth, nullptr); + batch.setResourceTexture(render_utils::slot::texture::SsaoNormal, nullptr); batch.setResourceTexture(render_utils::slot::texture::SsaoOcclusion, nullptr); - + _gpuTimer->end(batch); }); @@ -399,8 +822,6 @@ void AmbientOcclusionEffect::run(const render::RenderContextPointer& renderConte config->setGPUBatchRunTime(_gpuTimer->getGPUAverage(), _gpuTimer->getBatchAverage()); } - - DebugAmbientOcclusion::DebugAmbientOcclusion() { } @@ -446,19 +867,18 @@ void DebugAmbientOcclusion::run(const render::RenderContextPointer& renderContex return; } - auto linearDepthTexture = linearDepthFramebuffer->getLinearDepthTexture(); + auto fullResDepthTexture = linearDepthFramebuffer->getLinearDepthTexture(); auto sourceViewport = args->_viewport; auto occlusionViewport = sourceViewport; auto resolutionLevel = ambientOcclusionUniforms->getResolutionLevel(); if (resolutionLevel > 0) { - linearDepthTexture = linearDepthFramebuffer->getHalfLinearDepthTexture(); + fullResDepthTexture = linearDepthFramebuffer->getHalfLinearDepthTexture(); occlusionViewport = occlusionViewport >> ambientOcclusionUniforms->getResolutionLevel(); } - - auto framebufferSize = glm::ivec2(linearDepthTexture->getDimensions()); + auto framebufferSize = glm::ivec2(fullResDepthTexture->getDimensions()); float sMin = occlusionViewport.x / (float)framebufferSize.x; float sWidth = occlusionViewport.z / (float)framebufferSize.x; @@ -481,14 +901,14 @@ void DebugAmbientOcclusion::run(const render::RenderContextPointer& renderContex batch.setUniformBuffer(render_utils::slot::buffer::DeferredFrameTransform, frameTransform->getFrameTransformBuffer()); batch.setUniformBuffer(render_utils::slot::buffer::SsaoParams, ambientOcclusionUniforms); - batch.setUniformBuffer(2, _parametersBuffer); + batch.setUniformBuffer(render_utils::slot::buffer::SsaoDebugParams, _parametersBuffer); batch.setPipeline(debugPipeline); - batch.setResourceTexture(render_utils::slot::texture::SsaoPyramid, linearDepthTexture); + batch.setResourceTexture(render_utils::slot::texture::SsaoDepth, fullResDepthTexture); batch.draw(gpu::TRIANGLE_STRIP, 4); - batch.setResourceTexture(render_utils::slot::texture::SsaoPyramid, nullptr); + batch.setResourceTexture(render_utils::slot::texture::SsaoDepth, nullptr); }); } diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index b3a93ab1de..864aef09e4 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -21,6 +21,8 @@ #include "DeferredFramebuffer.h" #include "SurfaceGeometryPass.h" +#include "ssao_shared.h" + class AmbientOcclusionFramebuffer { public: AmbientOcclusionFramebuffer(); @@ -30,13 +32,23 @@ public: gpu::FramebufferPointer getOcclusionBlurredFramebuffer(); gpu::TexturePointer getOcclusionBlurredTexture(); - + + gpu::FramebufferPointer getNormalFramebuffer(); + gpu::TexturePointer getNormalTexture(); + +#if SSAO_USE_QUAD_SPLIT + gpu::FramebufferPointer getOcclusionSplitFramebuffer(int index); + gpu::TexturePointer getOcclusionSplitTexture(); +#endif + // Update the source framebuffer size which will drive the allocation of all the other resources. - void updateLinearDepth(const gpu::TexturePointer& linearDepthBuffer); + bool update(const gpu::TexturePointer& linearDepthBuffer, int resolutionLevel, int depthResolutionLevel, bool isStereo); gpu::TexturePointer getLinearDepthTexture(); const glm::ivec2& getSourceFrameSize() const { return _frameSize; } - + bool isStereo() const { return _isStereo; } + protected: + void clear(); void allocate(); @@ -44,12 +56,22 @@ protected: gpu::FramebufferPointer _occlusionFramebuffer; gpu::TexturePointer _occlusionTexture; - + gpu::FramebufferPointer _occlusionBlurredFramebuffer; gpu::TexturePointer _occlusionBlurredTexture; - - + + gpu::FramebufferPointer _normalFramebuffer; + gpu::TexturePointer _normalTexture; + +#if SSAO_USE_QUAD_SPLIT + gpu::FramebufferPointer _occlusionSplitFramebuffers[SSAO_SPLIT_COUNT*SSAO_SPLIT_COUNT]; + gpu::TexturePointer _occlusionSplitTexture; +#endif + glm::ivec2 _frameSize; + int _resolutionLevel{ 0 }; + int _depthResolutionLevel{ 0 }; + bool _isStereo{ false }; }; using AmbientOcclusionFramebufferPointer = std::shared_ptr; @@ -57,53 +79,78 @@ using AmbientOcclusionFramebufferPointer = std::shared_ptr; @@ -115,59 +162,75 @@ public: void configure(const Config& config); void run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs); - // Class describing the uniform buffer with all the parameters common to the AO shaders - class Parameters { + class AOParameters : public AmbientOcclusionParams { public: - // Resolution info - glm::vec4 resolutionInfo { -1.0f, 0.0f, 1.0f, 0.0f }; - // radius info is { R, R^2, 1 / R^6, ObscuranceScale} - glm::vec4 radiusInfo{ 0.5f, 0.5f * 0.5f, 1.0f / (0.25f * 0.25f * 0.25f), 1.0f }; - // Dithering info - glm::vec4 ditheringInfo { 0.0f, 0.0f, 0.01f, 1.0f }; - // Sampling info - glm::vec4 sampleInfo { 11.0f, 1.0f/11.0f, 7.0f, 1.0f }; - // Blurring info - glm::vec4 blurInfo { 1.0f, 3.0f, 2.0f, 0.0f }; - // gaussian distribution coefficients first is the sampling radius (max is 6) - const static int GAUSSIAN_COEFS_LENGTH = 8; - float _gaussianCoefs[GAUSSIAN_COEFS_LENGTH]; - - Parameters() {} - int getResolutionLevel() const { return resolutionInfo.x; } - float getRadius() const { return radiusInfo.x; } - float getPerspectiveScale() const { return resolutionInfo.z; } - float getObscuranceLevel() const { return radiusInfo.w; } - float getFalloffBias() const { return (float)ditheringInfo.z; } - float getEdgeSharpness() const { return (float)blurInfo.x; } - float getBlurDeviation() const { return blurInfo.z; } + AOParameters(); + + int getResolutionLevel() const { return _resolutionInfo.x; } + float getRadius() const { return _radiusInfo.x; } + float getPerspectiveScale() const { return _resolutionInfo.z; } + float getObscuranceLevel() const { return _radiusInfo.w; } + float getFalloffAngle() const { return (float)_falloffInfo.x; } - float getNumSpiralTurns() const { return sampleInfo.z; } - int getNumSamples() const { return (int)sampleInfo.x; } - bool isFetchMipsEnabled() const { return sampleInfo.w; } + float getNumSpiralTurns() const { return _sampleInfo.z; } + int getNumSamples() const { return (int)_sampleInfo.x; } + bool isFetchMipsEnabled() const { return _sampleInfo.w; } + + bool isDitheringEnabled() const { return _ditheringInfo.x != 0.0f; } + bool isBorderingEnabled() const { return _ditheringInfo.w != 0.0f; } + bool isHorizonBased() const { return _resolutionInfo.y != 0.0f; } - int getBlurRadius() const { return (int)blurInfo.y; } - bool isDitheringEnabled() const { return ditheringInfo.x; } - bool isBorderingEnabled() const { return ditheringInfo.w; } }; - using ParametersBuffer = gpu::StructBuffer; + using AOParametersBuffer = gpu::StructBuffer; private: - void updateGaussianDistribution(); - - ParametersBuffer _parametersBuffer; - const gpu::PipelinePointer& getOcclusionPipeline(); - const gpu::PipelinePointer& getHBlurPipeline(); // first - const gpu::PipelinePointer& getVBlurPipeline(); // second + // Class describing the uniform buffer with all the parameters common to the bilateral blur shaders + class BlurParameters : public AmbientOcclusionBlurParams { + public: - gpu::PipelinePointer _occlusionPipeline; - gpu::PipelinePointer _hBlurPipeline; - gpu::PipelinePointer _vBlurPipeline; + BlurParameters(); + + float getEdgeSharpness() const { return (float)_blurInfo.x; } + int getBlurRadius() const { return (int)_blurInfo.w; } + + }; + using BlurParametersBuffer = gpu::StructBuffer; + + using FrameParametersBuffer = gpu::StructBuffer< AmbientOcclusionFrameParams>; + + void updateBlurParameters(); + void updateFramebufferSizes(); + void updateRandomSamples(); + void updateJitterSamples(); + + int getDepthResolutionLevel() const; + + AOParametersBuffer _aoParametersBuffer; + FrameParametersBuffer _aoFrameParametersBuffer[SSAO_SPLIT_COUNT*SSAO_SPLIT_COUNT]; + BlurParametersBuffer _vblurParametersBuffer; + BlurParametersBuffer _hblurParametersBuffer; + float _blurEdgeSharpness{ 0.0f }; + + static const gpu::PipelinePointer& getOcclusionPipeline(); + static const gpu::PipelinePointer& getBilateralBlurPipeline(); + static const gpu::PipelinePointer& getMipCreationPipeline(); + static const gpu::PipelinePointer& getGatherPipeline(); + static const gpu::PipelinePointer& getBuildNormalsPipeline(); + + static gpu::PipelinePointer _occlusionPipeline; + static gpu::PipelinePointer _bilateralBlurPipeline; + static gpu::PipelinePointer _mipCreationPipeline; + static gpu::PipelinePointer _gatherPipeline; + static gpu::PipelinePointer _buildNormalsPipeline; AmbientOcclusionFramebufferPointer _framebuffer; + std::array _randomSamples; + int _frameId{ 0 }; + bool _isJitterEnabled{ true }; gpu::RangeTimerPointer _gpuTimer; @@ -193,7 +256,7 @@ signals: class DebugAmbientOcclusion { public: - using Inputs = render::VaryingSet4; + using Inputs = render::VaryingSet4; using Config = DebugAmbientOcclusionConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index c31345bc55..86d4793aa5 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -114,18 +114,22 @@ void CauterizedModel::updateClusterMatrices() { for (int i = 0; i < (int)_meshStates.size(); i++) { Model::MeshState& state = _meshStates[i]; const HFMMesh& mesh = hfmModel.meshes.at(i); + int meshIndex = i; + for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); + int clusterIndex = j; + if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); Transform clusterTransform; - Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); state.clusterDualQuaternions[j].setCauterizationParameters(0.0f, jointPose.trans()); } else { auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]); } } } @@ -146,9 +150,11 @@ void CauterizedModel::updateClusterMatrices() { for (int i = 0; i < _cauterizeMeshStates.size(); i++) { Model::MeshState& state = _cauterizeMeshStates[i]; const HFMMesh& mesh = hfmModel.meshes.at(i); + int meshIndex = i; for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); + int clusterIndex = j; if (_useDualQuaternionSkinning) { if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) { @@ -157,7 +163,7 @@ void CauterizedModel::updateClusterMatrices() { } else { Transform jointTransform(cauterizePose.rot(), cauterizePose.scale(), cauterizePose.trans()); Transform clusterTransform; - Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); state.clusterDualQuaternions[j].setCauterizationParameters(1.0f, cauterizePose.trans()); } @@ -166,7 +172,7 @@ void CauterizedModel::updateClusterMatrices() { // not cauterized so just copy the value from the non-cauterized version. state.clusterMatrices[j] = _meshStates[i].clusterMatrices[j]; } else { - glm_mat4u_mul(cauterizeMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + glm_mat4u_mul(cauterizeMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]); } } } diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 9597ce1052..9bdfdbcda6 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -220,9 +220,7 @@ static const std::string DEFAULT_DEBUG_SCATTERING_SHADER{ static const std::string DEFAULT_AMBIENT_OCCLUSION_SHADER{ "vec4 getFragmentColor() {" - " return vec4(vec3(texture(obscuranceMap, uv).x), 1.0);" - // When drawing color " return vec4(vec3(texture(debugTexture0, uv).xyz), 1.0);" - // when drawing normal" return vec4(normalize(texture(debugTexture0, uv).xyz * 2.0 - vec3(1.0)), 1.0);" + " return vec4(vec3(texture(debugTexture0, uv).x), 1.0);" " }" }; static const std::string DEFAULT_AMBIENT_OCCLUSION_BLURRED_SHADER{ @@ -323,6 +321,8 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, const std::strin return DEFAULT_AMBIENT_OCCLUSION_SHADER; case AmbientOcclusionBlurredMode: return DEFAULT_AMBIENT_OCCLUSION_BLURRED_SHADER; + case AmbientOcclusionNormalMode: + return DEFAULT_HALF_NORMAL_SHADER; case VelocityMode: return DEFAULT_VELOCITY_SHADER; case CustomMode: @@ -470,6 +470,8 @@ void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const I batch.setResourceTexture(Textures::DebugTexture0, ambientOcclusionFramebuffer->getOcclusionTexture()); } else if (_mode == AmbientOcclusionBlurredMode) { batch.setResourceTexture(Textures::DebugTexture0, ambientOcclusionFramebuffer->getOcclusionBlurredTexture()); + } else if (_mode == AmbientOcclusionNormalMode) { + batch.setResourceTexture(Textures::DebugTexture0, ambientOcclusionFramebuffer->getNormalTexture()); } } const glm::vec4 color(1.0f, 1.0f, 1.0f, 1.0f); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index cdaf5db83a..166366e65b 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -91,6 +91,7 @@ protected: ScatteringDebugMode, AmbientOcclusionMode, AmbientOcclusionBlurredMode, + AmbientOcclusionNormalMode, VelocityMode, CustomMode, // Needs to stay last diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index 8a8805e928..9b3ad213c6 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -120,13 +120,22 @@ float getStereoSideHeight(int resolutionLevel) { return float(int(frameTransform._pixelInfo.w) >> resolutionLevel); } -vec2 getSideImageSize(int resolutionLevel) { - return vec2(float(int(frameTransform._stereoInfo.y) >> resolutionLevel), float(int(frameTransform._pixelInfo.w) >> resolutionLevel)); +vec2 getStereoSideSize(int resolutionLevel) { + return vec2(getStereoSideWidth(resolutionLevel), getStereoSideHeight(resolutionLevel)); +} + +ivec4 getStereoSideInfoFromWidth(int xPos, int sideWidth) { + return ivec4(xPos < sideWidth ? ivec2(0, 0) : ivec2(1, sideWidth), sideWidth, isStereo()); } ivec4 getStereoSideInfo(int xPos, int resolutionLevel) { int sideWidth = int(getStereoSideWidth(resolutionLevel)); - return ivec4(xPos < sideWidth ? ivec2(0, 0) : ivec2(1, sideWidth), sideWidth, isStereo()); + return getStereoSideInfoFromWidth(xPos, sideWidth); +} + + +int getStereoSide(ivec4 sideInfo) { + return sideInfo.x; } float evalZeyeFromZdb(float depth) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 7da7a45e83..9cefbf65a8 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1418,18 +1418,21 @@ void Model::updateClusterMatrices() { const HFMModel& hfmModel = getHFMModel(); for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; + int meshIndex = i; const HFMMesh& mesh = hfmModel.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); + int clusterIndex = j; + if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); Transform clusterTransform; - Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); } else { auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]); } } } diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index f26bad86b0..a6f82e1417 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -46,32 +46,25 @@ void SoftAttachmentModel::updateClusterMatrices() { for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; const HFMMesh& mesh = hfmModel.meshes.at(i); - + int meshIndex = i; for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); + int clusterIndex = j; // TODO: cache these look-ups as an optimization int jointIndexOverride = getJointIndexOverride(cluster.jointIndex); + glm::mat4 jointMatrix; + if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { + jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); + } else { + jointMatrix = _rig.getJointTransform(cluster.jointIndex); + } if (_useDualQuaternionSkinning) { - glm::mat4 jointMatrix; - if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { - jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); - } else { - jointMatrix = _rig.getJointTransform(cluster.jointIndex); - } - glm::mat4 m; - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, m); + glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, m); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(m); } else { - glm::mat4 jointMatrix; - if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { - jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); - } else { - jointMatrix = _rig.getJointTransform(cluster.jointIndex); - } - - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]); } } } diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index d32cba43db..83595b5a64 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -11,6 +11,7 @@ #include "SurfaceGeometryPass.h" #include +#include #include #include @@ -28,19 +29,27 @@ namespace ru { LinearDepthFramebuffer::LinearDepthFramebuffer() { } -void LinearDepthFramebuffer::updatePrimaryDepth(const gpu::TexturePointer& depthBuffer) { +void LinearDepthFramebuffer::update(const gpu::TexturePointer& depthBuffer, const gpu::TexturePointer& normalTexture, bool isStereo) { //If the depth buffer or size changed, we need to delete our FBOs bool reset = false; - if ((_primaryDepthTexture != depthBuffer)) { + if (_primaryDepthTexture != depthBuffer || _normalTexture != normalTexture) { _primaryDepthTexture = depthBuffer; + _normalTexture = normalTexture; reset = true; } if (_primaryDepthTexture) { auto newFrameSize = glm::ivec2(_primaryDepthTexture->getDimensions()); - if (_frameSize != newFrameSize) { + if (_frameSize != newFrameSize || _isStereo != isStereo) { _frameSize = newFrameSize; - _halfFrameSize = newFrameSize >> 1; - + _halfFrameSize = _frameSize; + if (isStereo) { + _halfFrameSize.x >>= 1; + } + _halfFrameSize >>= 1; + if (isStereo) { + _halfFrameSize.x <<= 1; + } + _isStereo = isStereo; reset = true; } } @@ -64,16 +73,22 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), width, height, gpu::Texture::SINGLE_MIP, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); + const uint16_t LINEAR_DEPTH_MAX_MIP_LEVEL = 5; + // Point sampling of the depth, as well as the clamp to edge, are needed for the AmbientOcclusionEffect with HBAO + const auto depthSamplerFull = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT, gpu::Sampler::WRAP_CLAMP); + _linearDepthTexture = gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), width, height, LINEAR_DEPTH_MAX_MIP_LEVEL, + depthSamplerFull); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: const uint16_t HALF_LINEAR_DEPTH_MAX_MIP_LEVEL = 5; - _halfLinearDepthTexture = gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), _halfFrameSize.x, _halfFrameSize.y, HALF_LINEAR_DEPTH_MAX_MIP_LEVEL, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); + // Point sampling of the depth, as well as the clamp to edge, are needed for the AmbientOcclusionEffect with HBAO + const auto depthSamplerHalf = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_POINT, gpu::Sampler::WRAP_CLAMP); + // The depth format here is half float as it increases performance in the AmbientOcclusion. But it might be needed elsewhere... + _halfLinearDepthTexture = gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RED), _halfFrameSize.x, _halfFrameSize.y, HALF_LINEAR_DEPTH_MAX_MIP_LEVEL, + depthSamplerHalf); _halfNormalTexture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _halfFrameSize.x, _halfFrameSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT)); @@ -97,6 +112,10 @@ gpu::TexturePointer LinearDepthFramebuffer::getLinearDepthTexture() { return _linearDepthTexture; } +gpu::TexturePointer LinearDepthFramebuffer::getNormalTexture() { + return _normalTexture; +} + gpu::FramebufferPointer LinearDepthFramebuffer::getDownsampleFramebuffer() { if (!_downsampleFramebuffer) { allocate(); @@ -141,11 +160,12 @@ void LinearDepthPass::run(const render::RenderContextPointer& renderContext, con if (!_linearDepthFramebuffer) { _linearDepthFramebuffer = std::make_shared(); } - _linearDepthFramebuffer->updatePrimaryDepth(deferredFramebuffer->getPrimaryDepthTexture()); auto depthBuffer = deferredFramebuffer->getPrimaryDepthTexture(); auto normalTexture = deferredFramebuffer->getDeferredNormalTexture(); + _linearDepthFramebuffer->update(depthBuffer, normalTexture, args->isStereo()); + auto linearDepthFBO = _linearDepthFramebuffer->getLinearDepthFramebuffer(); auto linearDepthTexture = _linearDepthFramebuffer->getLinearDepthTexture(); @@ -167,32 +187,34 @@ void LinearDepthPass::run(const render::RenderContextPointer& renderContext, con float clearLinearDepth = args->getViewFrustum().getFarClip() * 2.0f; gpu::doInBatch("LinearDepthPass::run", args->_context, [=](gpu::Batch& batch) { + PROFILE_RANGE_BATCH(batch, "LinearDepthPass"); _gpuTimer->begin(batch); + batch.enableStereo(false); - batch.setViewportTransform(depthViewport); batch.setProjectionTransform(glm::mat4()); batch.resetViewTransform(); - batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_linearDepthFramebuffer->getDepthFrameSize(), depthViewport)); batch.setUniformBuffer(ru::Buffer::DeferredFrameTransform, frameTransform->getFrameTransformBuffer()); // LinearDepth + batch.setViewportTransform(depthViewport); batch.setFramebuffer(linearDepthFBO); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(clearLinearDepth, 0.0f, 0.0f, 0.0f)); batch.setPipeline(linearDepthPipeline); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_linearDepthFramebuffer->getDepthFrameSize(), depthViewport)); batch.setResourceTexture(ru::Texture::SurfaceGeometryDepth, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); // Downsample batch.setViewportTransform(halfViewport); - batch.setFramebuffer(downsampleFBO); batch.setResourceTexture(ru::Texture::SurfaceGeometryDepth, linearDepthTexture); batch.setResourceTexture(ru::Texture::SurfaceGeometryNormal, normalTexture); batch.setPipeline(downsamplePipeline); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_linearDepthFramebuffer->getDepthFrameSize() >> 1, halfViewport)); batch.draw(gpu::TRIANGLE_STRIP, 4); - + _gpuTimer->end(batch); }); @@ -244,7 +266,7 @@ const gpu::PipelinePointer& LinearDepthPass::getDownsamplePipeline(const render: SurfaceGeometryFramebuffer::SurfaceGeometryFramebuffer() { } -void SurfaceGeometryFramebuffer::updateLinearDepth(const gpu::TexturePointer& linearDepthBuffer) { +void SurfaceGeometryFramebuffer::update(const gpu::TexturePointer& linearDepthBuffer) { //If the depth buffer or size changed, we need to delete our FBOs bool reset = false; if ((_linearDepthTexture != linearDepthBuffer)) { @@ -411,7 +433,7 @@ void SurfaceGeometryPass::run(const render::RenderContextPointer& renderContext, if (!_surfaceGeometryFramebuffer) { _surfaceGeometryFramebuffer = std::make_shared(); } - _surfaceGeometryFramebuffer->updateLinearDepth(linearDepthTexture); + _surfaceGeometryFramebuffer->update(linearDepthTexture); auto curvatureFramebuffer = _surfaceGeometryFramebuffer->getCurvatureFramebuffer(); auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture(); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.h b/libraries/render-utils/src/SurfaceGeometryPass.h index 367f599f67..6ea03fbda5 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.h +++ b/libraries/render-utils/src/SurfaceGeometryPass.h @@ -28,17 +28,17 @@ public: gpu::FramebufferPointer getLinearDepthFramebuffer(); gpu::TexturePointer getLinearDepthTexture(); + gpu::TexturePointer getNormalTexture(); gpu::FramebufferPointer getDownsampleFramebuffer(); gpu::TexturePointer getHalfLinearDepthTexture(); gpu::TexturePointer getHalfNormalTexture(); // Update the depth buffer which will drive the allocation of all the other resources according to its size. - void updatePrimaryDepth(const gpu::TexturePointer& depthBuffer); - gpu::TexturePointer getPrimaryDepthTexture(); + void update(const gpu::TexturePointer& depthBuffer, const gpu::TexturePointer& normalTexture, bool isStereo); const glm::ivec2& getDepthFrameSize() const { return _frameSize; } - void setResolutionLevel(int level); + void setResolutionLevel(int level) { _resolutionLevel = std::max(0, level); } int getResolutionLevel() const { return _resolutionLevel; } protected: @@ -49,6 +49,7 @@ protected: gpu::FramebufferPointer _linearDepthFramebuffer; gpu::TexturePointer _linearDepthTexture; + gpu::TexturePointer _normalTexture; gpu::FramebufferPointer _downsampleFramebuffer; gpu::TexturePointer _halfLinearDepthTexture; @@ -58,6 +59,7 @@ protected: glm::ivec2 _frameSize; glm::ivec2 _halfFrameSize; int _resolutionLevel{ 0 }; + bool _isStereo{ false }; }; using LinearDepthFramebufferPointer = std::shared_ptr; @@ -107,7 +109,7 @@ public: gpu::TexturePointer getBlurringTexture(); // Update the source framebuffer size which will drive the allocation of all the other resources. - void updateLinearDepth(const gpu::TexturePointer& linearDepthBuffer); + void update(const gpu::TexturePointer& linearDepthBuffer); gpu::TexturePointer getLinearDepthTexture(); const glm::ivec2& getSourceFrameSize() const { return _frameSize; } diff --git a/libraries/render-utils/src/render-utils/ShaderConstants.h b/libraries/render-utils/src/render-utils/ShaderConstants.h index 2d777d502f..8c289e62d1 100644 --- a/libraries/render-utils/src/render-utils/ShaderConstants.h +++ b/libraries/render-utils/src/render-utils/ShaderConstants.h @@ -86,7 +86,10 @@ // Ambient occlusion #define RENDER_UTILS_BUFFER_SSAO_PARAMS 2 #define RENDER_UTILS_BUFFER_SSAO_DEBUG_PARAMS 3 -#define RENDER_UTILS_TEXTURE_SSAO_PYRAMID 1 +#define RENDER_UTILS_BUFFER_SSAO_BLUR_PARAMS 4 +#define RENDER_UTILS_BUFFER_SSAO_FRAME_PARAMS 5 +#define RENDER_UTILS_TEXTURE_SSAO_DEPTH 1 +#define RENDER_UTILS_TEXTURE_SSAO_NORMAL 2 #define RENDER_UTILS_TEXTURE_SSAO_OCCLUSION 0 // Temporal anti-aliasing @@ -120,7 +123,6 @@ #define RENDER_UTILS_UNIFORM_TEXT_COLOR 0 #define RENDER_UTILS_UNIFORM_TEXT_OUTLINE 1 - // Debugging #define RENDER_UTILS_BUFFER_DEBUG_SKYBOX 5 #define RENDER_UTILS_DEBUG_TEXTURE0 11 @@ -144,7 +146,9 @@ enum Buffer { LightClusterContent = RENDER_UTILS_BUFFER_LIGHT_CLUSTER_CONTENT, SsscParams = RENDER_UTILS_BUFFER_SSSC_PARAMS, SsaoParams = RENDER_UTILS_BUFFER_SSAO_PARAMS, + SsaoFrameParams = RENDER_UTILS_BUFFER_SSAO_FRAME_PARAMS, SsaoDebugParams = RENDER_UTILS_BUFFER_SSAO_DEBUG_PARAMS, + SsaoBlurParams = RENDER_UTILS_BUFFER_SSAO_BLUR_PARAMS, LightIndex = RENDER_UTILS_BUFFER_LIGHT_INDEX, TaaParams = RENDER_UTILS_BUFFER_TAA_PARAMS, HighlightParams = RENDER_UTILS_BUFFER_HIGHLIGHT_PARAMS, @@ -183,7 +187,8 @@ enum Texture { TaaDepth = RENDER_UTILS_TEXTURE_TAA_DEPTH, TaaNext = RENDER_UTILS_TEXTURE_TAA_NEXT, SsaoOcclusion = RENDER_UTILS_TEXTURE_SSAO_OCCLUSION, - SsaoPyramid = RENDER_UTILS_TEXTURE_SSAO_PYRAMID, + SsaoDepth = RENDER_UTILS_TEXTURE_SSAO_DEPTH, + SsaoNormal = RENDER_UTILS_TEXTURE_SSAO_NORMAL, HighlightSceneDepth = RENDER_UTILS_TEXTURE_HIGHLIGHT_SCENE_DEPTH, HighlightDepth = RENDER_UTILS_TEXTURE_HIGHLIGHT_DEPTH, SurfaceGeometryDepth = RENDER_UTILS_TEXTURE_SG_DEPTH, diff --git a/libraries/render-utils/src/render-utils/ssao_bilateralBlur.slp b/libraries/render-utils/src/render-utils/ssao_bilateralBlur.slp new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/libraries/render-utils/src/render-utils/ssao_bilateralBlur.slp @@ -0,0 +1 @@ + diff --git a/libraries/render-utils/src/render-utils/ssao_makeHorizontalBlur.slp b/libraries/render-utils/src/render-utils/ssao_buildNormals.slp similarity index 100% rename from libraries/render-utils/src/render-utils/ssao_makeHorizontalBlur.slp rename to libraries/render-utils/src/render-utils/ssao_buildNormals.slp diff --git a/libraries/render-utils/src/render-utils/ssao_makeVerticalBlur.slp b/libraries/render-utils/src/render-utils/ssao_gather.slp similarity index 100% rename from libraries/render-utils/src/render-utils/ssao_makeVerticalBlur.slp rename to libraries/render-utils/src/render-utils/ssao_gather.slp diff --git a/libraries/render-utils/src/ssao.slh b/libraries/render-utils/src/ssao.slh index f0d522a41c..b683dc38f1 100644 --- a/libraries/render-utils/src/ssao.slh +++ b/libraries/render-utils/src/ssao.slh @@ -12,52 +12,94 @@ <@def SSAO_SLH@> <@include render-utils/ShaderConstants.h@> +<@include ssao_shared.h@> <@func declarePackOcclusionDepth()@> -const float FAR_PLANE_Z = -300.0; +float CSZToDepthKey(float z) { + return clamp(z * (-1.0 / SSAO_DEPTH_KEY_SCALE), 0.0, 1.0); +} -float CSZToDephtKey(float z) { - return clamp(z * (1.0 / FAR_PLANE_Z), 0.0, 1.0); -} -vec3 packOcclusionDepth(float occlusion, float depth) { +vec4 packOcclusionOutput(float occlusion, float depth, vec3 eyeNormal) { + depth = CSZToDepthKey(depth); +#if SSAO_BILATERAL_BLUR_USE_NORMAL + return vec4(occlusion, depth, eyeNormal.xy / eyeNormal.z); +#else // Round to the nearest 1/256.0 - float temp = floor(depth * 256.0); - return vec3(occlusion, temp * (1.0 / 256.0), depth * 256.0 - temp); + depth *= 256.0; + float temp = floor(depth); + return vec4(occlusion, temp * (1.0 / 256.0), depth - temp, 0.0); +#endif } -vec2 unpackOcclusionDepth(vec3 raw) { - float z = raw.y * (256.0 / 257.0) + raw.z * (1.0 / 257.0); - return vec2(raw.x, z); + +struct UnpackedOcclusion { + vec3 normal; + float depth; + float occlusion; +}; + +void unpackOcclusionOutput(vec4 raw, out UnpackedOcclusion result) { + result.occlusion = raw.x; +#if SSAO_BILATERAL_BLUR_USE_NORMAL + result.depth = raw.y; + result.normal = normalize(vec3(raw.zw, 1.0)); +#else + result.depth = (raw.y + raw.z / 256.0); + result.normal = vec3(0.0, 0.0, 1.0); +#endif } + +float unpackOcclusion(vec4 raw) { + return raw.x; +} + <@endfunc@> <@func declareAmbientOcclusion()@> <@include DeferredTransform.slh@> <$declareDeferredFrameTransform()$> -struct AmbientOcclusionParams { - vec4 _resolutionInfo; - vec4 _radiusInfo; - vec4 _ditheringInfo; - vec4 _sampleInfo; - vec4 _blurInfo; - float _gaussianCoefs[8]; -}; - LAYOUT(binding=RENDER_UTILS_BUFFER_SSAO_PARAMS) uniform ambientOcclusionParamsBuffer { AmbientOcclusionParams params; }; +LAYOUT(binding=RENDER_UTILS_BUFFER_SSAO_FRAME_PARAMS) uniform ambientOcclusionFrameParamsBuffer { + AmbientOcclusionFrameParams frameParams; +}; float getPerspectiveScale() { - return (params._resolutionInfo.z); } -int getResolutionLevel() { - +int getResolutionLevel() { return int(params._resolutionInfo.x); } +bool isHorizonBased() { + return params._resolutionInfo.y!=0.0; +} + +vec2 getNormalsSideSize() { + return params._sideSizes[0].xy; +} +int getNormalsResolutionLevel() { + return int(params._sideSizes[0].z); +} +int getDepthResolutionLevel() { + return int(params._sideSizes[0].w); +} +vec2 getOcclusionSideSize() { + return params._sideSizes[1].xy; +} +vec2 getOcclusionSplitSideSize() { + return params._sideSizes[1].zw; +} + +ivec2 getWidthHeightRoundUp(int resolutionLevel) { + ivec2 fullRes = ivec2(getWidthHeight(0)); + int resolutionDivisor = 1 << resolutionLevel; + return (fullRes + resolutionDivisor - 1) / resolutionDivisor; +} + float getRadius() { return params._radiusInfo.x; } @@ -65,24 +107,35 @@ float getRadius2() { return params._radiusInfo.y; } float getInvRadius6() { + return mix(params._radiusInfo.z, 1.0, isHorizonBased()); +} +float getInvRadius2() { return params._radiusInfo.z; } + float getObscuranceScaling() { - return params._radiusInfo.z * params._radiusInfo.w; + return getInvRadius6() * params._radiusInfo.w; } float isDitheringEnabled() { return params._ditheringInfo.x; } -float getFrameDithering() { - return params._ditheringInfo.y; -} float isBorderingEnabled() { return params._ditheringInfo.w; } -float getFalloffBias() { - return params._ditheringInfo.z; +float getFalloffCosAngle() { + return params._falloffInfo.x; +} +float getFalloffCosAngleScale() { + return params._falloffInfo.y; +} + +float getFalloffSinAngle() { + return params._falloffInfo.z; +} +float getFalloffSinAngleScale() { + return params._falloffInfo.w; } float getNumSamples() { @@ -99,37 +152,6 @@ int doFetchMips() { return int(params._sampleInfo.w); } -float getBlurEdgeSharpness() { - return params._blurInfo.x; -} - -#ifdef CONSTANT_GAUSSIAN -const int BLUR_RADIUS = 4; -const float gaussian[BLUR_RADIUS + 1] = -// KEEP this dead code for eventual performance improvment -// float[](0.356642, 0.239400, 0.072410, 0.009869); -// float[](0.398943, 0.241971, 0.053991, 0.004432, 0.000134); // stddev = 1.0 -float[](0.153170, 0.144893, 0.122649, 0.092902, 0.062970); // stddev = 2.0 -//float[](0.197413, 0.17467, 0.12098,0.065591,0.040059); -// float[](0.111220, 0.107798, 0.098151, 0.083953, 0.067458, 0.050920, 0.036108); // stddev = 3.0 - -int getBlurRadius() { - return BLUR_RADIUS; -} - -float getBlurCoef(int c) { - return gaussian[c]; -} -#else -int getBlurRadius() { - return int(params._blurInfo.y); -} - -float getBlurCoef(int c) { - return params._gaussianCoefs[c]; -} -#endif - <@endfunc@> <@func declareSamplingDisk()@> @@ -139,43 +161,57 @@ float getAngleDitheringWorldPos(in vec3 pixelWorldPos) { ivec3 pixelPos = ivec3(worldPosFract * 256.0); - return isDitheringEnabled() * float(((3 * pixelPos.x ^ pixelPos.y + pixelPos.x * pixelPos.y) + (3 * pixelPos.y ^ pixelPos.z + pixelPos.x * pixelPos.z)) * 10) + getFrameDithering(); + return isDitheringEnabled() * float(((3 * pixelPos.x ^ pixelPos.y + pixelPos.x * pixelPos.y) + (3 * pixelPos.y ^ pixelPos.z + pixelPos.x * pixelPos.z)) * 10); +} + +float getAngleDitheringSplit() { + return isDitheringEnabled() * frameParams._angleInfo.x; } float getAngleDithering(in ivec2 pixelPos) { +#if SSAO_USE_QUAD_SPLIT + return getAngleDitheringSplit(); +#else // Hash function used in the AlchemyAO paper - return isDitheringEnabled() * float((3 * pixelPos.x ^ pixelPos.y + pixelPos.x * pixelPos.y) * 10) + getFrameDithering(); + return getAngleDitheringPixelPos(pixelPos); +#endif } -float evalDiskRadius(float Zeye, vec2 imageSize) { +float getAngleDitheringPixelPos(in ivec2 pixelPos) { + // Hash function used in the AlchemyAO paper + return isDitheringEnabled() * float((3 * pixelPos.x ^ pixelPos.y + pixelPos.x * pixelPos.y) * 10); +} + +float evalDiskRadius(float Zeye, vec2 sideImageSize) { // Choose the screen-space sample radius // proportional to the projected area of the sphere - float ssDiskRadius = -( getProjScale(getResolutionLevel()) * getRadius() / Zeye ) * getPerspectiveScale(); + float diskPixelRadius = -( getProjScale(getResolutionLevel()) * getRadius() / Zeye ) * getPerspectiveScale(); // clamp the disk to fit in the image otherwise too many unknown - ssDiskRadius = min(ssDiskRadius, imageSize.y * 0.5); + diskPixelRadius = min(diskPixelRadius, sideImageSize.y * 0.5); - return ssDiskRadius; + return diskPixelRadius; } -const float TWO_PI = 6.28; +const float PI = 3.1415926; +const float TWO_PI = 6.2831852; -vec3 getUnitTapLocation(int sampleNumber, float spinAngle){ +vec3 getUnitTapLocation(int sampleNumber, float spiralTurns, float spinAngle, float angleRange){ // Radius relative to ssR - float alpha = (float(sampleNumber) + 0.5) * getInvNumSamples(); - float angle = alpha * (getNumSpiralTurns() * TWO_PI) + spinAngle; + float alpha = float(sampleNumber) * getInvNumSamples(); + float angle = alpha * (spiralTurns * angleRange) + spinAngle; return vec3(cos(angle), sin(angle), alpha); } -vec3 getTapLocation(int sampleNumber, float spinAngle, float outerRadius) { - vec3 tap = getUnitTapLocation(sampleNumber, spinAngle); +vec3 getTapLocationSSAO(int sampleNumber, float spinAngle, float outerRadius) { + vec3 tap = getUnitTapLocation(sampleNumber, getNumSpiralTurns(), spinAngle, TWO_PI); tap.xy *= tap.z; tap *= outerRadius; return tap; } -vec3 getTapLocationClamped(int sampleNumber, float spinAngle, float outerRadius, vec2 pixelPos, vec2 imageSize) { - vec3 tap = getTapLocation(sampleNumber, spinAngle, outerRadius); +vec3 getTapLocationClampedSSAO(int sampleNumber, float spinAngle, float outerRadius, vec2 pixelPos, vec2 sideImageSize) { + vec3 tap = getTapLocationSSAO(sampleNumber, spinAngle, outerRadius); vec2 tapPos = pixelPos + tap.xy; if (!(isBorderingEnabled() > 0.0)) { @@ -186,36 +222,19 @@ vec3 getTapLocationClamped(int sampleNumber, float spinAngle, float outerRadius, if ((tapPos.x < 0.5)) { tapPos.x = -tapPos.x; redoTap = true; - } else if ((tapPos.x > imageSize.x - 0.5)) { - tapPos.x -= (imageSize.x - tapPos.x); + } else if ((tapPos.x > sideImageSize.x - 0.5)) { + tapPos.x -= (sideImageSize.x - tapPos.x); redoTap = true; } if ((tapPos.y < 0.5)) { tapPos.y = -tapPos.y; redoTap = true; - } else if ((tapPos.y > imageSize.y - 0.5)) { - tapPos.y -= (imageSize.y - tapPos.y); - redoTap = true; - } -/* - if ((tapPos.x < 0.5)) { - tapPos.x = 0.5; - redoTap = true; - } else if ((tapPos.x > imageSize.x - 0.5)) { - tapPos.x = imageSize.x - 0.5; + } else if ((tapPos.y > sideImageSize.y - 0.5)) { + tapPos.y -= (sideImageSize.y - tapPos.y); redoTap = true; } - if ((tapPos.y < 0.5)) { - tapPos.y = 0.5; - redoTap = true; - } else if ((tapPos.y > imageSize.y - 0.5)) { - tapPos.y = imageSize.y - 0.5; - redoTap = true; - } -*/ - if (redoTap) { tap.xy = tapPos - pixelPos; tap.z = length(tap.xy); @@ -230,156 +249,341 @@ vec3 getTapLocationClamped(int sampleNumber, float spinAngle, float outerRadius, <@func declareFetchDepthPyramidMap()@> - // the depth pyramid texture -LAYOUT(binding=RENDER_UTILS_TEXTURE_SSAO_PYRAMID) uniform sampler2D pyramidMap; +LAYOUT(binding=RENDER_UTILS_TEXTURE_SSAO_DEPTH) uniform sampler2D depthPyramidTex; +LAYOUT(binding=RENDER_UTILS_TEXTURE_SSAO_NORMAL) uniform sampler2D normalTex; -float getZEye(ivec2 pixel, int level) { - return -texelFetch(pyramidMap, pixel, level).x; +vec2 getFramebufferUVFromSideUV(ivec4 side, vec2 uv) { + return mix(uv, vec2((uv.x + float(getStereoSide(side))) * 0.5, uv.y), float(isStereo())); +} + +vec2 getSideUVFromFramebufferUV(ivec4 side, vec2 uv) { + return mix(uv, vec2(uv.x * 2.0 - float(getStereoSide(side)), uv.y), float(isStereo())); +} + +vec2 getDepthTextureSize(int level) { + return vec2(textureSize(depthPyramidTex, level)); +} + +vec2 getDepthTextureSideSize(int level) { + ivec2 size = textureSize(depthPyramidTex, level); + size.x >>= int(isStereo()) & 1; + return vec2(size); +} + +vec2 getStereoSideSizeRoundUp(int resolutionLevel) { + ivec2 fullRes = ivec2(getStereoSideSize(0)); + int resolutionDivisor = 1 << resolutionLevel; + return vec2((fullRes + resolutionDivisor - 1) / resolutionDivisor); +} + +float getZEyeAtUV(vec2 texCoord, float level) { + return -textureLod(depthPyramidTex, texCoord, level).x; +} + +<@func getZEyeAtUVOffset(texCoord, level, texelOffset)@> +-textureLodOffset(depthPyramidTex, <$texCoord$>, <$level$>, <$texelOffset$>).x; +<@endfunc@> + +float getZEyeAtUV(ivec4 side, vec2 texCoord, float level) { + texCoord = getFramebufferUVFromSideUV(side, texCoord); + return getZEyeAtUV(texCoord, level); +} + +vec3 packNormal(vec3 normal) { + vec3 absNormal = abs(normal); + return 0.5 + normal * 0.5 / max(absNormal.x, max(absNormal.y, absNormal.z)); +} + +vec3 unpackNormal(vec3 packedNormal) { + return normalize(packedNormal*2.0 - 1.0); +} + +vec3 getNormalEyeAtUV(vec2 texCoord, float level) { + return unpackNormal(textureLod(normalTex, texCoord, level).xyz); +} + +vec3 getNormalEyeAtUV(ivec4 side, vec2 texCoord, float level) { + texCoord = getFramebufferUVFromSideUV(side, texCoord); + return getNormalEyeAtUV(texCoord, level); +} + +vec2 snapToTexel(vec2 uv, vec2 pixelSize) { + return (floor(uv * pixelSize - 0.5) + 0.5) / pixelSize; } -const int LOG_MAX_OFFSET = 3; -const int MAX_MIP_LEVEL = 5; int evalMipFromRadius(float radius) { - // mipLevel = floor(log(ssR / MAX_OFFSET)); + const int LOG_MAX_OFFSET = 2; + const int MAX_MIP_LEVEL = 5; return clamp(findMSB(int(radius)) - LOG_MAX_OFFSET, 0, MAX_MIP_LEVEL); } +vec2 fetchTap(ivec4 side, vec2 tapUV, float tapRadius) { + int mipLevel = evalMipFromRadius(tapRadius * float(doFetchMips())); -vec3 fetchTapUnfiltered(ivec4 side, ivec2 ssC, vec3 tap, vec2 imageSize) { - ivec2 ssP = ivec2(tap.xy) + ssC; - ivec2 ssPFull = ivec2(ssP.x + side.y, ssP.y); + vec2 fetchUV = clamp(tapUV, vec2(0), vec2(1)); + fetchUV = getFramebufferUVFromSideUV(side, fetchUV); - - vec2 tapUV = (vec2(ssP) + vec2(0.5)) / imageSize; - - vec2 fetchUV = vec2(tapUV.x + float(side.w) * 0.5 * (float(side.x) - tapUV.x), tapUV.y); - - vec3 P; - P.xy = tapUV; - P.z = -texture(pyramidMap, fetchUV).x; - - return P; + vec2 P; + P.x = float(mipLevel); + P.y = -textureLod(depthPyramidTex, fetchUV, P.x).x; + return P; } -vec3 fetchTap(ivec4 side, ivec2 ssC, vec3 tap, vec2 imageSize) { - int mipLevel = evalMipFromRadius(tap.z * float(doFetchMips())); - - ivec2 ssP = ivec2(tap.xy) + ssC; - ivec2 ssPFull = ivec2(ssP.x + side.y, ssP.y); - - // We need to divide by 2^mipLevel to read the appropriately scaled coordinate from a MIP-map. - // Manually clamp to the texture size because texelFetch bypasses the texture unit - // ivec2 mipSize = textureSize(pyramidMap, mipLevel); - ivec2 mipSize = max(ivec2(imageSize) >> mipLevel, ivec2(1)); - - ivec2 mipP = clamp(ssPFull >> mipLevel, ivec2(0), mipSize - ivec2(1)); - - vec2 tapUV = (vec2(ssP) + vec2(0.5)) / imageSize; - vec2 fetchUV = vec2(tapUV.x + float(side.w) * 0.5 * (float(side.x) - tapUV.x), tapUV.y); - // vec2 tapUV = (vec2(mipP) + vec2(0.5)) / vec2(mipSize); - - vec3 P; - P.xy = tapUV; - // P.z = -texelFetch(pyramidMap, mipP, mipLevel).x; - P.z = -textureLod(pyramidMap, fetchUV, float(mipLevel)).x; - - return P; +vec3 buildPosition(ivec4 side, vec2 fragUVPos) { + float Zeye = getZEyeAtUV(side, fragUVPos, 0.0); + return evalEyePositionFromZeye(side.x, Zeye, fragUVPos); } +<@func buildPositionOffset(side, fragUVPos, sideFragUVPos, texelOffset, deltaUV, position)@> +{ + float Zeye = <$getZEyeAtUVOffset($sideFragUVPos$, 0.0, $texelOffset$)$> + <$position$> = evalEyePositionFromZeye(<$side$>.x, Zeye, <$fragUVPos$> + vec2(<$texelOffset$>)*<$deltaUV$>); +} +<@endfunc@> +vec3 getMinDelta(vec3 centralPoint, vec3 offsetPointPos, vec3 offsetPointNeg) { + vec3 delta0 = offsetPointPos - centralPoint; + vec3 delta1 = centralPoint - offsetPointNeg; + float sqrLength0 = dot(delta0, delta0); + float sqrLength1 = dot(delta1, delta1); + + return mix(delta1, delta0, float(sqrLength0 < sqrLength1)); +} + +const ivec2 UV_RIGHT = ivec2(1,0); +const ivec2 UV_LEFT = ivec2(-1,0); +const ivec2 UV_TOP = ivec2(0,1); +const ivec2 UV_BOTTOM = ivec2(0,-1); + +vec3 buildNormal(ivec4 side, vec2 fragUVPos, vec3 fragPosition, vec2 deltaDepthUV) { + vec2 fullUVPos = getFramebufferUVFromSideUV(side, fragUVPos); + + vec3 fragPositionDxPos; + vec3 fragPositionDxNeg; + vec3 fragPositionDyPos; + vec3 fragPositionDyNeg; + + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_RIGHT, deltaDepthUV, fragPositionDxPos)$> + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_LEFT, deltaDepthUV, fragPositionDxNeg)$> + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_TOP, deltaDepthUV, fragPositionDyPos)$> + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_BOTTOM, deltaDepthUV, fragPositionDyNeg)$> + + vec3 fragDeltaDx = getMinDelta(fragPosition, fragPositionDxPos, fragPositionDxNeg); + vec3 fragDeltaDy = getMinDelta(fragPosition, fragPositionDyPos, fragPositionDyNeg); + + return normalize( cross(fragDeltaDx, fragDeltaDy) ); +} + +void buildTangentBinormal(ivec4 side, vec2 fragUVPos, vec3 fragPosition, vec3 fragNormal, vec2 deltaDepthUV, + out vec3 fragTangent, out vec3 fragBinormal) { + vec2 fullUVPos = getFramebufferUVFromSideUV(side, fragUVPos); + + vec3 fragPositionDxPos; + vec3 fragPositionDxNeg; + vec3 fragPositionDyPos; + vec3 fragPositionDyNeg; + + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_RIGHT, deltaDepthUV, fragPositionDxPos)$> + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_LEFT, deltaDepthUV, fragPositionDxNeg)$> + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_TOP, deltaDepthUV, fragPositionDyPos)$> + <$buildPositionOffset(side, fragUVPos, fullUVPos, UV_BOTTOM, deltaDepthUV, fragPositionDyNeg)$> + + vec3 fragDeltaDx = getMinDelta(fragPosition, fragPositionDxPos, fragPositionDxNeg); + vec3 fragDeltaDy = getMinDelta(fragPosition, fragPositionDyPos, fragPositionDyNeg); + + //fragTangent = normalize( cross(fragDeltaDy, fragNormal) ); + //fragBinormal = normalize( cross(fragNormal, fragDeltaDx) ); + + fragTangent = fragDeltaDx; + fragBinormal = fragDeltaDy; +} <@endfunc@> <@func declareEvalObscurance()@> -float evalAO(in vec3 C, in vec3 n_C, in vec3 Q) { - vec3 v = Q - C; - float vv = dot(v, v); - float vn = dot(v, n_C); +struct TBNFrame { + vec3 tangent; + vec3 binormal; + vec3 normal; +}; - // Fall off function as recommended in SAO paper +vec3 fastAcos(vec3 x) { + // [Eberly2014] GPGPU Programming for Games and Science + vec3 absX = abs(x); + vec3 res = absX * (-0.156583) + vec3(PI / 2.0); + res *= sqrt(vec3(1.0) - absX); + return mix(res, vec3(PI) - res, greaterThanEqual(x, vec3(0))); +} + +float evalVisibilitySSAO(in vec3 centerPosition, in vec3 centerNormal, in vec3 tapPosition) { + vec3 v = tapPosition - centerPosition; + float vv = dot(v, v); + float vn = dot(v, centerNormal); + + // Falloff function as recommended in SSAO paper const float epsilon = 0.01; float f = max(getRadius2() - vv, 0.0); - return f * f * f * max((vn - getFalloffBias()) / (epsilon + vv), 0.0); + return f * f * f * max((vn - getFalloffCosAngle()) / (epsilon + vv), 0.0); } +#define HBAO_USE_COS_ANGLE 1 +#define HBAO_USE_OVERHANG_HACK 0 + +float computeWeightForHorizon(float horizonLimit, float distanceSquared) { + return max(0.0, 1.0 - distanceSquared * getInvRadius2()); +} + +float computeWeightedHorizon(float horizonLimit, float distanceSquared) { + float radiusFalloff = computeWeightForHorizon(horizonLimit, distanceSquared); + +#if !HBAO_USE_COS_ANGLE + horizonLimit = getFalloffSinAngle() - horizonLimit; +#endif + horizonLimit *= radiusFalloff; +#if !HBAO_USE_COS_ANGLE + horizonLimit = getFalloffSinAngle() - horizonLimit; +#endif + + return horizonLimit; +} + +<@func computeHorizon()@> + if (tapUVPos.x<0.0 || tapUVPos.y<0.0 || tapUVPos.x>=1.0 || tapUVPos.y>=1.0) { + // Early exit because we've hit the borders of the frame + break; + } + vec2 tapMipZ = fetchTap(side, tapUVPos, radius); + vec3 tapPositionES = evalEyePositionFromZeye(side.x, tapMipZ.y, tapUVPos); + vec3 deltaVec = tapPositionES - fragPositionES; + float distanceSquared = dot(deltaVec, deltaVec); + float deltaDotNormal = dot(deltaVec, fragFrameES.normal); +#if HBAO_USE_COS_ANGLE + float tapHorizonLimit = deltaDotNormal; +#else + float tapHorizonLimit = dot(deltaVec, fragFrameES.tangent); +#endif + tapHorizonLimit *= inversesqrt(distanceSquared); + + if (distanceSquared < getRadius2() && deltaDotNormal>0.0) { +#if HBAO_USE_COS_ANGLE + float weight = computeWeightForHorizon(tapHorizonLimit, distanceSquared); + if (tapHorizonLimit > horizonLimit) { + occlusion += weight * (tapHorizonLimit - horizonLimit); + horizonLimit = tapHorizonLimit; + } +#if HBAO_USE_OVERHANG_HACK + else if (dot(deltaVec, fragFrameES.tangent) < 0.0) { + // This is a hack to try to handle the case where the occlusion angle is + // greater than 90° + occlusion = mix(occlusion, (occlusion+1.0) * 0.5, weight); + } +#endif +#else + if (tapHorizonLimit < horizonLimit) { + tapHorizonLimit = computeWeightedHorizon(tapHorizonLimit, distanceSquared); + horizonLimit = min(horizonLimit, tapHorizonLimit); + } +#endif + } <@endfunc@> -<@func declareBlurPass(axis)@> +#define HBAO_HORIZON_SEARCH_CONSTANT_STEP 0 -<$declarePackOcclusionDepth()$> -<$declareAmbientOcclusion()$> +float computeOcclusion(ivec4 side, vec2 fragUVPos, vec3 fragPositionES, TBNFrame fragFrameES, vec2 searchDir, float searchRadius, int stepCount) { + float occlusion = 0.0; +#if HBAO_USE_COS_ANGLE + float horizonLimit = getFalloffCosAngle(); +#else + float horizonLimit = getFalloffSinAngle(); +#endif -// the source occlusion texture -LAYOUT(binding=RENDER_UTILS_TEXTURE_SSAO_OCCLUSION) uniform sampler2D occlusionMap; + if (stepCount>0) { + vec2 deltaTapUV = searchDir / float(stepCount); + vec2 tapUVPos; + float deltaRadius = searchRadius / float(stepCount); + vec2 sideDepthSize = getDepthTextureSideSize(0); -vec2 fetchOcclusionDepthRaw(ivec2 coords, out vec3 raw) { - raw = texelFetch(occlusionMap, coords, 0).xyz; - return unpackOcclusionDepth(raw); -} +#if HBAO_HORIZON_SEARCH_CONSTANT_STEP + float radius = 0.0; + int stepIndex; -vec2 fetchOcclusionDepth(ivec2 coords) { - return unpackOcclusionDepth(texelFetch(occlusionMap, coords, 0).xyz); -} + for (stepIndex=0 ; stepIndex + } +#else + // Step is adapted to Mip level + float radius = deltaRadius; + float mipLevel = float(evalMipFromRadius(radius * float(doFetchMips()))); -vec2 evalTapWeightedValue(ivec3 side, int r, ivec2 ssC, float key) { - ivec2 tapOffset = <$axis$> * (r * RADIUS_SCALE); - ivec2 ssP = (ssC + tapOffset); + while (radius<=searchRadius) { + fragUVPos += deltaTapUV; + tapUVPos = snapToTexel(fragUVPos, sideDepthSize); - if ((ssP.x < side.y || ssP.x >= side.z + side.y) || (ssP.y < 0 || ssP.y >= int(getWidthHeight(getResolutionLevel()).y))) { - return vec2(0.0); - } - vec2 tapOZ = fetchOcclusionDepth(ssC + tapOffset); + <$computeHorizon()$> - // spatial domain: offset gaussian tap - float weight = BLUR_WEIGHT_OFFSET + getBlurCoef(abs(r)); - - // range domain (the "bilateral" weight). As depth difference increases, decrease weight. - weight *= max(0.0, 1.0 - (getBlurEdgeSharpness() * BLUR_EDGE_SCALE) * abs(tapOZ.y - key)); - - return vec2(tapOZ.x * weight, weight); -} - -vec3 getBlurredOcclusion(vec2 coord) { - ivec2 ssC = ivec2(coord); - - // Stereo side info - ivec4 side = getStereoSideInfo(ssC.x, getResolutionLevel()); - - vec3 rawSample; - vec2 occlusionDepth = fetchOcclusionDepthRaw(ssC, rawSample); - float key = occlusionDepth.y; - - // Central pixel contribution - float mainWeight = getBlurCoef(0); - vec2 weightedSums = vec2(occlusionDepth.x * mainWeight, mainWeight); - - // Accumulate weighted contributions along the bluring axis in the [-radius, radius] range - int blurRadius = getBlurRadius(); - // negative side first - for (int r = -blurRadius; r <= -1; ++r) { - weightedSums += evalTapWeightedValue(side.xyz, r, ssC, key); - } - // then positive side - for (int r = 1; r <= blurRadius; ++r) { - weightedSums += evalTapWeightedValue(side.xyz, r, ssC, key); + if (tapMipZ.x != mipLevel) { + mipLevel = tapMipZ.x; + deltaRadius *= 2.0; + deltaTapUV *= 2.0; + sideDepthSize = getDepthTextureSideSize(int(mipLevel)); + } + radius += deltaRadius; + } +#endif } - // Final normalization - const float epsilon = 0.0001; - float result = weightedSums.x / (weightedSums.y + epsilon); - - rawSample.x = result; - return rawSample; +#if HBAO_USE_COS_ANGLE + occlusion = min(occlusion * getFalloffCosAngleScale(), 1.0); +#else + occlusion = horizonLimit * mix(1.0, getFalloffSinAngleScale(), horizonLimit > 0.0); +#endif + + return occlusion; +} + +float evalVisibilityHBAO(ivec4 side, vec2 fragUVPos, vec2 invSideImageSize, vec2 deltaTap, float diskPixelRadius, + vec3 fragPositionES, vec3 fragNormalES) { + vec2 pixelSearchVec = deltaTap * diskPixelRadius; + vec2 searchDir = pixelSearchVec * invSideImageSize; + vec2 deltaTapUV = deltaTap * invSideImageSize; + float obscuranceH1 = 0.0; + float obscuranceH2 = 0.0; + pixelSearchVec = abs(pixelSearchVec); + int stepCount = int(ceil(max(pixelSearchVec.x, pixelSearchVec.y))); + TBNFrame fragFrameES; + + fragFrameES.tangent = vec3(0.0); + fragFrameES.binormal = vec3(0.0); + fragFrameES.normal = fragNormalES; + +#if HBAO_USE_OVERHANG_HACK || !HBAO_USE_COS_ANGLE + vec3 positionPos = buildPosition(side, fragUVPos + deltaTapUV); + vec3 positionNeg = buildPosition(side, fragUVPos - deltaTapUV); + + fragFrameES.tangent = getMinDelta(fragPositionES, positionPos, positionNeg); + fragFrameES.tangent -= dot(fragNormalES, fragFrameES.tangent) * fragNormalES; + fragFrameES.tangent = normalize(fragFrameES.tangent); +#endif + // Forward search for h1 + obscuranceH1 = computeOcclusion(side, fragUVPos, fragPositionES, fragFrameES, searchDir, diskPixelRadius, stepCount); + + // Backward search for h2 +#if HBAO_USE_OVERHANG_HACK || !HBAO_USE_COS_ANGLE + fragFrameES.tangent = -fragFrameES.tangent; +#endif + obscuranceH2 = computeOcclusion(side, fragUVPos, fragPositionES, fragFrameES, -searchDir, diskPixelRadius, stepCount); + + return obscuranceH1 + obscuranceH2; } <@endfunc@> -<@endif@> \ No newline at end of file +<@endif@> diff --git a/libraries/render-utils/src/ssao_bilateralBlur.slf b/libraries/render-utils/src/ssao_bilateralBlur.slf new file mode 100644 index 0000000000..52e7356e85 --- /dev/null +++ b/libraries/render-utils/src/ssao_bilateralBlur.slf @@ -0,0 +1,135 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// ssao_bilateralBlur.frag +// +// Created by Sam Gateau on 1/1/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include ssao.slh@> + +// Hack comment + +<$declareAmbientOcclusion()$> +<$declareFetchDepthPyramidMap()$> +<$declarePackOcclusionDepth()$> + +// the source occlusion texture +LAYOUT(binding=RENDER_UTILS_TEXTURE_SSAO_OCCLUSION) uniform sampler2D occlusionMap; + +LAYOUT(binding=RENDER_UTILS_BUFFER_SSAO_BLUR_PARAMS) uniform blurParamsBuffer { + AmbientOcclusionBlurParams blurParams; +}; + +vec2 getBlurOcclusionAxis() { + return blurParams._blurAxis.xy; +} + +vec2 getBlurOcclusionUVLimit() { + return blurParams._blurAxis.zw; +} + +vec3 getBlurScales() { + return blurParams._blurInfo.xyz; +} + +int getBlurRadius() { + return int(blurParams._blurInfo.w); +} + +vec4 fetchOcclusionPacked(ivec4 side, vec2 texCoord) { + texCoord.x = isStereo() ? (texCoord.x + float(getStereoSide(side)) * getBlurOcclusionUVLimit().x) * 0.5 : texCoord.x; + return textureLod(occlusionMap, texCoord, 0.0); +} + +float evalBlurCoefficient(vec3 blurScales, float radialDistance, float zDistance, float normalDistance) { + vec3 distances = vec3(radialDistance, zDistance, normalDistance); + return exp2(dot(blurScales, distances*distances)); +} + +const float BLUR_EDGE_NORMAL_LIMIT = 0.25; + +vec2 evalTapWeightedValue(vec3 blurScales, ivec4 side, int r, vec2 occlusionTexCoord, float fragDepth, vec3 fragNormal) { + vec2 occlusionTexCoordLimits = getBlurOcclusionUVLimit(); + + if (any(lessThan(occlusionTexCoord, vec2(0.0))) || any(greaterThanEqual(occlusionTexCoord, occlusionTexCoordLimits)) ) { + return vec2(0.0); + } + + vec4 tapOcclusionPacked = fetchOcclusionPacked(side, occlusionTexCoord); + UnpackedOcclusion tap; + unpackOcclusionOutput(tapOcclusionPacked, tap); + + // range domain (the "bilateral" weight). As depth difference increases, decrease weight. + float zDistance = tap.depth - fragDepth; +#if SSAO_BILATERAL_BLUR_USE_NORMAL + float normalDistance = BLUR_EDGE_NORMAL_LIMIT - min(BLUR_EDGE_NORMAL_LIMIT, dot(tap.normal, fragNormal)); +#else + float normalDistance = 0.0; +#endif + float weight = evalBlurCoefficient(blurScales, float(abs(r)), zDistance, normalDistance); + + return vec2(tap.occlusion * weight, weight); +} + +vec4 getBlurredOcclusion(ivec2 destPixelCoord, vec2 occlusionTexCoord, vec2 depthTexCoord) { + // Stereo side info + ivec4 side = getStereoSideInfo(destPixelCoord.x, 0); + + float fragDepth = getZEyeAtUV(depthTexCoord, 0.0); + float fragDepthKey = CSZToDepthKey(fragDepth); +#if SSAO_BILATERAL_BLUR_USE_NORMAL + vec3 fragNormal = getNormalEyeAtUV(depthTexCoord, 0.0); +#else + vec3 fragNormal = vec3(0.0, 0.0, 1.0); +#endif + vec2 weightedSums = vec2(0.0); + + // Accumulate weighted contributions along the bluring axis in the [-radius, radius] range + int blurRadius = getBlurRadius(); + vec3 blurScales = getBlurScales(); + int r; + + // From now on, occlusionTexCoord is the UV pos in the side + float sideTexCoord = occlusionTexCoord.x * 2.0 - float(getStereoSide(side)) * getBlurOcclusionUVLimit().x; + occlusionTexCoord.x = mix(occlusionTexCoord.x, sideTexCoord, isStereo()); + + occlusionTexCoord -= getBlurOcclusionAxis() * float(blurRadius); + + // negative side first + for (r = -blurRadius; r <= -1; r++) { + weightedSums += evalTapWeightedValue(blurScales, side, r, occlusionTexCoord, fragDepthKey, fragNormal); + occlusionTexCoord += getBlurOcclusionAxis(); + } + + // Central pixel contribution + float mainWeight = 1.0; + float pixelOcclusion = unpackOcclusion(fetchOcclusionPacked(side, occlusionTexCoord)); + weightedSums += vec2(pixelOcclusion * mainWeight, mainWeight); + occlusionTexCoord += getBlurOcclusionAxis(); + + // then positive side + for (r = 1; r <= blurRadius; ++r) { + weightedSums += evalTapWeightedValue(blurScales, side, r, occlusionTexCoord, fragDepthKey, fragNormal); + occlusionTexCoord += getBlurOcclusionAxis(); + } + + // Final normalization + const float epsilon = 0.0001; + float result = weightedSums.x / (weightedSums.y + epsilon); + + return packOcclusionOutput(result, fragDepth, fragNormal); +} + +layout(location=0) in vec4 varTexCoord0; + +layout(location=0) out vec4 outFragColor; + +void main(void) { + outFragColor = getBlurredOcclusion(ivec2(gl_FragCoord.xy), varTexCoord0.xy, varTexCoord0.zw); +} diff --git a/libraries/render-utils/src/ssao_bilateralBlur.slv b/libraries/render-utils/src/ssao_bilateralBlur.slv new file mode 100644 index 0000000000..d45fdf8360 --- /dev/null +++ b/libraries/render-utils/src/ssao_bilateralBlur.slv @@ -0,0 +1,42 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// ssao_bilateralBlur.vert +// +// Draw the unit quad [-1,-1 -> 1,1] filling in +// Simply draw a Triangle_strip of 2 triangles, no input buffers or index buffer needed +// +// Created by Olivier Prat on 9/12/2018 +// 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 +// + +<@include gpu/Transform.slh@> + +<$declareStandardTransform()$> + +layout(location=0) out vec4 varTexCoord0; + +void main(void) { + const vec4 UNIT_QUAD[4] = vec4[4]( + vec4(-1.0, -1.0, 0.0, 1.0), + vec4(1.0, -1.0, 0.0, 1.0), + vec4(-1.0, 1.0, 0.0, 1.0), + vec4(1.0, 1.0, 0.0, 1.0) + ); + vec4 pos = UNIT_QUAD[gl_VertexID]; + + // standard transform but applied to the Texcoord + vec2 fullTexCoord = (pos.xy + 1.0) * 0.5; + vec4 tc = vec4(fullTexCoord, pos.zw); + + TransformObject obj = getTransformObject(); + <$transformModelToWorldPos(obj, tc, tc)$> + + gl_Position = pos; + varTexCoord0.xy = tc.xy; + varTexCoord0.zw = fullTexCoord.xy; +} diff --git a/libraries/render-utils/src/ssao_buildNormals.slf b/libraries/render-utils/src/ssao_buildNormals.slf new file mode 100644 index 0000000000..0a733ff451 --- /dev/null +++ b/libraries/render-utils/src/ssao_buildNormals.slf @@ -0,0 +1,42 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// ssao_buildNormals.frag +// +// Created by Olivier Prat on 09/19/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include ssao.slh@> +<$declareAmbientOcclusion()$> +<$declareFetchDepthPyramidMap()$> + +layout(location=0) in vec2 varTexCoord0; + +layout(location=0) out vec4 outFragColor; + +void main(void) { + // Pixel being shaded + vec2 fragCoord = gl_FragCoord.xy; + ivec2 fragPixelPos = ivec2(fragCoord.xy); + vec2 fragUVPos = varTexCoord0; + + // Stereo side info based on the real viewport size of this pass + ivec2 sideNormalsSize = ivec2( getNormalsSideSize() ); + ivec4 side = getStereoSideInfoFromWidth(fragPixelPos.x, sideNormalsSize.x); + + vec2 deltaDepthUV = vec2(1.0) / getDepthTextureSideSize(0); + + // From now on, fragUVPos is the UV pos in the side + fragUVPos = getSideUVFromFramebufferUV(side, fragUVPos); + + // The position and normal of the pixel fragment in Eye space + vec3 fragPositionES = buildPosition(side, fragUVPos); + vec3 fragNormalES = buildNormal(side, fragUVPos, fragPositionES, deltaDepthUV); + + outFragColor = vec4(packNormal(fragNormalES), 1.0); +} diff --git a/libraries/render-utils/src/ssao_debugOcclusion.slf b/libraries/render-utils/src/ssao_debugOcclusion.slf index e15e52f448..75e3ed5194 100644 --- a/libraries/render-utils/src/ssao_debugOcclusion.slf +++ b/libraries/render-utils/src/ssao_debugOcclusion.slf @@ -37,96 +37,15 @@ vec2 getDebugCursorTexcoord(){ layout(location=0) out vec4 outFragColor; void main(void) { - vec2 imageSize = getSideImageSize(getResolutionLevel()); - - // In debug adjust the correct frag pixel based on base resolution - vec2 fragCoord = gl_FragCoord.xy; - if (getResolutionLevel() > 0) { - fragCoord /= float (1 << getResolutionLevel()); - } - + // Stereo side info based on the real viewport size of this pass + vec2 sideDepthSize = getDepthTextureSideSize(0); // Pixel Debugged vec2 cursorUV = getDebugCursorTexcoord(); - vec2 cursorPixelPos = cursorUV * imageSize; + vec2 cursorPixelPos = cursorUV * sideDepthSize; - ivec2 ssC = ivec2(cursorPixelPos); - - // Fetch the z under the pixel (stereo or not) - float Zeye = getZEye(ssC, 0); + ivec2 fragUVPos = ivec2(cursorPixelPos); - // Stereo side info - ivec4 side = getStereoSideInfo(ssC.x, getResolutionLevel()); + // TODO - // From now on, ssC is the pixel pos in the side - ssC.x -= side.y; - vec2 fragPos = (vec2(ssC) + vec2(0.5)) / imageSize; - - // The position and normal of the pixel fragment in Eye space - vec3 Cp = evalEyePositionFromZeye(side.x, Zeye, fragPos); - vec3 Cn = evalEyeNormal(Cp); - - // Choose the screen-space sample radius - float ssDiskRadius = evalDiskRadius(Cp.z, imageSize); - - vec2 fragToCursor = cursorPixelPos - fragCoord.xy; - if (dot(fragToCursor,fragToCursor) > ssDiskRadius * ssDiskRadius) { - discard; - } - - // Let's make noise - //float randomPatternRotationAngle = getAngleDithering(ssC); - vec3 wCp = (getViewInverse() * vec4(Cp, 1.0)).xyz; - float randomPatternRotationAngle = getAngleDitheringWorldPos(wCp); - - - // Accumulate the Obscurance for each samples - float sum = 0.0; - float keepTapRadius = 1.0; - int keepedMip = -1; - bool keep = false; - int sampleCount = int(getNumSamples()); - for (int i = 0; i < sampleCount; ++i) { - vec3 tap = getTapLocationClamped(i, randomPatternRotationAngle, ssDiskRadius, cursorPixelPos, imageSize); - - // The occluding point in camera space - vec2 fragToTap = vec2(ssC) + tap.xy - fragCoord.xy; - if (dot(fragToTap,fragToTap) < keepTapRadius) { - keep = true; - keepedMip = evalMipFromRadius(tap.z * float(doFetchMips())); - } - - vec3 tapUVZ = fetchTap(side, ssC, tap, imageSize); - - vec3 Q = evalEyePositionFromZeye(side.x, tapUVZ.z, tapUVZ.xy); - - sum += float(tap.z > 0.0) * evalAO(Cp, Cn, Q); - } - - - float A = max(0.0, 1.0 - sum * getObscuranceScaling() * 5.0 * getInvNumSamples()); - - - - outFragColor = vec4(packOcclusionDepth(A, CSZToDephtKey(Cp.z)), 1.0); - - if ((dot(fragToCursor,fragToCursor) < (100.0 * keepTapRadius * keepTapRadius) )) { - // outFragColor = vec4(vec3(A), 1.0); - outFragColor = vec4(vec3(A), 1.0); - return; - } - - if (!keep) { - outFragColor = vec4(0.1); - } else { - outFragColor.rgb = colorWheel(float(keepedMip)/float(MAX_MIP_LEVEL)); - } + outFragColor = packOcclusionOutput(0.0, 0.0, vec3(0.0, 0.0, 1.0)); } diff --git a/libraries/render-utils/src/ssao_gather.slf b/libraries/render-utils/src/ssao_gather.slf new file mode 100644 index 0000000000..1595678295 --- /dev/null +++ b/libraries/render-utils/src/ssao_gather.slf @@ -0,0 +1,36 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// ssao_gather.frag +// +// Created by Olivier Prat on 09/19/2018. +// 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 +// + +<@include ssao.slh@> + +// Hack comment + +<$declareAmbientOcclusion()$> + +// the source occlusion texture +LAYOUT(binding=RENDER_UTILS_TEXTURE_SSAO_OCCLUSION) uniform sampler2DArray occlusionMaps; + +layout(location=0) in vec4 varTexCoord0; + +layout(location=0) out vec4 outFragColor; + +void main(void) { + // Gather the four splits of the occlusion result back into an interleaved full size + // result (at the resolution level, of course) + ivec2 destPixelCoord = ivec2(gl_FragCoord.xy); + ivec2 sourcePixelCoord = destPixelCoord >> SSAO_SPLIT_LOG2_COUNT; + ivec2 modPixelCoord = destPixelCoord & (SSAO_SPLIT_COUNT-1); + int occlusionMapIndex = modPixelCoord.x + (modPixelCoord.y << SSAO_SPLIT_LOG2_COUNT); + + outFragColor = texelFetch(occlusionMaps, ivec3(sourcePixelCoord, occlusionMapIndex), 0); +} diff --git a/libraries/render-utils/src/ssao_makeHorizontalBlur.slf b/libraries/render-utils/src/ssao_makeHorizontalBlur.slf deleted file mode 100644 index 94dbb2b00c..0000000000 --- a/libraries/render-utils/src/ssao_makeHorizontalBlur.slf +++ /dev/null @@ -1,24 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// ssao_makeHorizontalBlur.frag -// -// Created by Sam Gateau on 1/1/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include ssao.slh@> - -const ivec2 horizontal = ivec2(1,0); -<$declareBlurPass(horizontal)$> - - -layout(location=0) out vec4 outFragColor; - -void main(void) { - outFragColor = vec4(getBlurredOcclusion(gl_FragCoord.xy), 1.0); -} diff --git a/libraries/render-utils/src/ssao_makeOcclusion.slf b/libraries/render-utils/src/ssao_makeOcclusion.slf index 3934b9eddc..5dfa879c69 100644 --- a/libraries/render-utils/src/ssao_makeOcclusion.slf +++ b/libraries/render-utils/src/ssao_makeOcclusion.slf @@ -19,71 +19,90 @@ <$declarePackOcclusionDepth()$> +#define SSAO_HBAO_MAX_RADIUS 300.0 + +layout(location=0) in vec2 varTexCoord0; + layout(location=0) out vec4 outFragColor; void main(void) { - vec2 imageSize = getSideImageSize(getResolutionLevel()); - // Pixel being shaded vec2 fragCoord = gl_FragCoord.xy; - ivec2 ssC = ivec2(fragCoord.xy); + ivec2 fragPixelPos = ivec2(fragCoord.xy); + vec2 fragUVPos = varTexCoord0; - // Fetch the z under the pixel (stereo or not) - float Zeye = getZEye(ssC, 0); +#if SSAO_USE_QUAD_SPLIT + vec3 fragNormalES = getNormalEyeAtUV(fragUVPos, 0.0); +#endif - // Stereo side info - ivec4 side = getStereoSideInfo(ssC.x, getResolutionLevel()); - - // From now on, ssC is the pixel pos in the side - ssC.x -= side.y; - vec2 fragPos = (vec2(ssC) + vec2(0.5)) / imageSize; - - // The position and normal of the pixel fragment in Eye space - vec3 Cp = evalEyePositionFromZeye(side.x, Zeye, fragPos); - vec3 Cn = evalEyeNormal(Cp); - - // Choose the screen-space sample radius - float ssDiskRadius = evalDiskRadius(Cp.z, imageSize); - - // Let's make noise - float randomPatternRotationAngle = getAngleDithering(ssC); - //vec3 wCp = (getViewInverse() * vec4(Cp, 1.0)).xyz; - //float randomPatternRotationAngle = getAngleDitheringWorldPos(wCp); - - // Accumulate the Obscurance for each samples - float sum = 0.0; - int sampleCount = int(getNumSamples()); - for (int i = 0; i < sampleCount; ++i) { - vec3 tap = getTapLocationClamped(i, randomPatternRotationAngle, ssDiskRadius, vec2(ssC), imageSize); - - vec3 tapUVZ = fetchTap(side, ssC, tap, imageSize); - - vec3 Q = evalEyePositionFromZeye(side.x, tapUVZ.z, tapUVZ.xy); - - sum += float(tap.z > 0.0) * evalAO(Cp, Cn, Q); + // Stereo side info based on the real viewport size of this pass + vec2 sideDepthSize = getDepthTextureSideSize(0); + ivec2 sideOcclusionSize; + if (isHorizonBased()) { + sideOcclusionSize = ivec2( getOcclusionSplitSideSize() ); + } else { + sideOcclusionSize = ivec2( getOcclusionSideSize() ); } + ivec4 side = getStereoSideInfoFromWidth(fragPixelPos.x, sideOcclusionSize.x); + // From now on, fragUVPos is the UV pos in the side + fragUVPos = getSideUVFromFramebufferUV(side, fragUVPos); + fragUVPos = snapToTexel(fragUVPos, sideDepthSize); - float A = max(0.0, 1.0 - sum * getObscuranceScaling() * 5.0 * getInvNumSamples()); + // The position and normal of the pixel fragment in Eye space + vec2 deltaDepthUV = vec2(2.0) / sideDepthSize; + vec3 fragPositionES = buildPosition(side, fragUVPos); +#if !SSAO_USE_QUAD_SPLIT + vec3 fragNormalES = buildNormal(side, fragUVPos, fragPositionES, deltaDepthUV); +#endif - // KEEP IT for Debugging - // Bilateral box-filter over a quad for free, respecting depth edges - // (the difference that this makes is subtle) - if (abs(dFdx(Cp.z)) < 0.02) { - A -= dFdx(A) * (float(ssC.x & 1) - 0.5); + float occlusion = 1.0; + + if (fragPositionES.z > (1.0-getPosLinearDepthFar())) { + // Choose the screen-space sample radius + float diskPixelRadius = evalDiskRadius(fragPositionES.z, sideDepthSize); + if (isHorizonBased()) { + diskPixelRadius = min(diskPixelRadius, SSAO_HBAO_MAX_RADIUS); + } + + // Let's make noise + float randomPatternRotationAngle = 0.0; + + // Accumulate the obscurance for each samples + float obscuranceSum = 0.0; + int numSamples = int(getNumSamples()); + float invNumSamples = getInvNumSamples(); + + if (isHorizonBased()) { + randomPatternRotationAngle = getAngleDithering(fragPixelPos); + + for (int i = 0; i < numSamples; ++i) { + vec3 deltaTap = getUnitTapLocation(i, 1.0, randomPatternRotationAngle, PI); + obscuranceSum += evalVisibilityHBAO(side, fragUVPos, deltaDepthUV, deltaTap.xy, diskPixelRadius, fragPositionES, fragNormalES); + } + obscuranceSum *= invNumSamples; +#if HBAO_USE_COS_ANGLE + obscuranceSum = 1.0 - obscuranceSum * getObscuranceScaling(); +#else + obscuranceSum = mix(1.0, obscuranceSum, getObscuranceScaling()); +#endif + } else { + // Steps are in the depth texture resolution + vec2 depthTexFragPixelPos = fragUVPos * sideDepthSize; + + randomPatternRotationAngle = getAngleDitheringPixelPos(fragPixelPos) + getAngleDitheringSplit(); + + for (int i = 0; i < numSamples; ++i) { + vec3 tap = getTapLocationClampedSSAO(i, randomPatternRotationAngle, diskPixelRadius, depthTexFragPixelPos, sideDepthSize); + vec2 tapUV = fragUVPos + tap.xy * deltaDepthUV; + vec2 tapMipZ = fetchTap(side, tapUV, tap.z); + vec3 tapPositionES = evalEyePositionFromZeye(side.x, tapMipZ.y, tapUV); + obscuranceSum += float(tap.z > 0.0) * evalVisibilitySSAO(fragPositionES, fragNormalES, tapPositionES); + } + obscuranceSum *= invNumSamples; + obscuranceSum = 1.0 - obscuranceSum * getObscuranceScaling(); + } + + occlusion = clamp(obscuranceSum, 0.0, 1.0); } - if (abs(dFdy(Cp.z)) < 0.02) { - A -= dFdy(A) * (float(ssC.y & 1) - 0.5); - } - - - outFragColor = vec4(packOcclusionDepth(A, CSZToDephtKey(Cp.z)), 1.0); - - /* { - vec3 tap = getTapLocationClamped(2, randomPatternRotationAngle, ssDiskRadius, ssC, imageSize); - vec3 tapUVZ = fetchTap(side, ssC, tap, imageSize); - vec2 fetchUV = vec2(tapUVZ.x + side.w * 0.5 * (side.x - tapUVZ.x), tapUVZ.y); - vec3 Q = evalEyePositionFromZeye(side.x, tapUVZ.z, tapUVZ.xy); - outFragColor = vec4(fetchUV, 0.0, 1.0); - }*/ - + outFragColor = packOcclusionOutput(occlusion, fragPositionES.z, fragNormalES); } diff --git a/libraries/render-utils/src/ssao_makeVerticalBlur.slf b/libraries/render-utils/src/ssao_makeVerticalBlur.slf deleted file mode 100644 index 0b9b5c7eaf..0000000000 --- a/libraries/render-utils/src/ssao_makeVerticalBlur.slf +++ /dev/null @@ -1,23 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// ssao_makeVerticalBlur.frag -// -// Created by Sam Gateau on 1/1/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -<@include ssao.slh@> - -const ivec2 vertical = ivec2(0,1); -<$declareBlurPass(vertical)$> - -layout(location=0) out vec4 outFragColor; - -void main(void) { - float occlusion = getBlurredOcclusion(gl_FragCoord.xy).x; - outFragColor = vec4(occlusion, 0.0, 0.0, occlusion); -} diff --git a/libraries/render-utils/src/ssao_mip_depth.slf b/libraries/render-utils/src/ssao_mip_depth.slf new file mode 100644 index 0000000000..92b9012556 --- /dev/null +++ b/libraries/render-utils/src/ssao_mip_depth.slf @@ -0,0 +1,28 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// ssao_mip_depth.frag +// fragment shader +// +// Created by Olivier Prat on 4/18/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +<@include gpu/ShaderConstants.h@> + +LAYOUT(binding=GPU_TEXTURE_MIP_CREATION_INPUT) uniform sampler2D depthTexture; + +layout(location=0) in vec2 varTexCoord0; + +layout(location=0) out vec4 outFragColor; + +void main(void) { + vec4 depths = textureGather(depthTexture, varTexCoord0); + // Keep the minimum depth + float outZ = min(depths.w, min(depths.z, min(depths.x, depths.y))); + + outFragColor = vec4(vec3(outZ), 1.0); +} diff --git a/libraries/render-utils/src/ssao_shared.h b/libraries/render-utils/src/ssao_shared.h new file mode 100644 index 0000000000..46e373c073 --- /dev/null +++ b/libraries/render-utils/src/ssao_shared.h @@ -0,0 +1,64 @@ +// + +// <@if not RENDER_UTILS_SSAO_SHARED_H@> +// <@def RENDER_UTILS_SSAO_SHARED_H@> + +// Hack comment to absorb the extra '//' scribe prepends + +#ifndef RENDER_UTILS_SSAO_SHARED_H +#define RENDER_UTILS_SSAO_SHARED_H + +#define SSAO_USE_QUAD_SPLIT 1 +#define SSAO_BILATERAL_BLUR_USE_NORMAL 0 + +#define SSAO_DEPTH_KEY_SCALE 300.0 + +#if SSAO_USE_QUAD_SPLIT +#define SSAO_SPLIT_LOG2_COUNT 2 +#else +#define SSAO_SPLIT_LOG2_COUNT 0 +#endif +#define SSAO_SPLIT_COUNT (1 << SSAO_SPLIT_LOG2_COUNT) + +// glsl / C++ compatible source as interface for ambient occlusion +#ifdef __cplusplus +# define SSAO_VEC4 glm::vec4 +# define SSAO_MAT4 glm::mat4 +#else +# define SSAO_VEC4 vec4 +# define SSAO_MAT4 mat4 +#endif + +struct AmbientOcclusionParams { + SSAO_VEC4 _resolutionInfo; + SSAO_VEC4 _radiusInfo; + SSAO_VEC4 _ditheringInfo; + SSAO_VEC4 _sampleInfo; + SSAO_VEC4 _falloffInfo; + SSAO_VEC4 _sideSizes[2]; +}; + +struct AmbientOcclusionFrameParams { + SSAO_VEC4 _angleInfo; +}; + +struct AmbientOcclusionBlurParams { + SSAO_VEC4 _blurInfo; + SSAO_VEC4 _blurAxis; +}; + +#endif // RENDER_UTILS_SHADER_CONSTANTS_H + +// <@if 1@> +// Trigger Scribe include +// <@endif@> + +// <@endif@> + +// Hack Comment \ No newline at end of file diff --git a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf index e4020dbdec..6759e21459 100644 --- a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf +++ b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf @@ -26,9 +26,7 @@ layout(location=1) out vec4 outNormal; void main(void) { // Gather 2 by 2 quads from texture and downsample - // Try different filters for Z vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); - // float Zeye = texture(linearDepthMap, varTexCoord0).x; vec4 rawNormalsX = textureGather(normalMap, varTexCoord0, 0); vec4 rawNormalsY = textureGather(normalMap, varTexCoord0, 1); diff --git a/libraries/render/src/render/BlurTask.slh b/libraries/render/src/render/BlurTask.slh index db6b8e3bab..fa4b9a4a4f 100644 --- a/libraries/render/src/render/BlurTask.slh +++ b/libraries/render/src/render/BlurTask.slh @@ -25,7 +25,7 @@ LAYOUT(binding=0) uniform blurParamsBuffer { BlurParameters parameters; }; -vec2 getViewportInvWidthHeight() { +vec2 getInvWidthHeight() { return parameters.resolutionInfo.zw; } diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index a87e2031f9..79557e3e44 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -480,8 +480,8 @@ public: protected: PayloadPointer _payload; ItemKey _key; - ItemCell _cell{ INVALID_CELL }; - Index _transitionId{ INVALID_INDEX }; + ItemCell _cell { INVALID_CELL }; + Index _transitionId { INVALID_INDEX }; friend class Scene; }; diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 8d12cfae25..1850261c99 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -391,7 +391,12 @@ void Scene::transitionItems(const Transaction::TransitionAdds& transactions) { // Remove pre-existing transition, if need be if (!TransitionStage::isIndexInvalid(transitionId)) { - resetItemTransition(itemId); + // Only remove if: + // transitioning to something other than none or we're transitioning to none from ELEMENT_LEAVE_DOMAIN or USER_LEAVE_DOMAIN + const auto& oldTransitionType = transitionStage->getTransition(transitionId).eventType; + if (transitionType != Transition::NONE || !(oldTransitionType == Transition::ELEMENT_LEAVE_DOMAIN || oldTransitionType == Transition::USER_LEAVE_DOMAIN)) { + resetItemTransition(itemId); + } } // Add a new one. diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index 68acfe8d0f..e05cb04532 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -157,7 +157,7 @@ public: // This next call are NOT threadsafe, you have to call them from the correct thread to avoid any potential issues - // Access a particular item form its ID + // Access a particular item from its ID // WARNING, There is No check on the validity of the ID, so this could return a bad Item const Item& getItem(const ItemID& id) const { return _items[id]; } diff --git a/libraries/render/src/render/blurGaussianDepthAwareH.slf b/libraries/render/src/render/blurGaussianDepthAwareH.slf index 09a92909ff..04894e3d51 100644 --- a/libraries/render/src/render/blurGaussianDepthAwareH.slf +++ b/libraries/render/src/render/blurGaussianDepthAwareH.slf @@ -19,6 +19,6 @@ layout(location=0) in vec2 varTexCoord0; layout(location=0) out vec4 outFragColor; void main(void) { - outFragColor = pixelShaderGaussianDepthAware(varTexCoord0, vec2(1.0, 0.0), getViewportInvWidthHeight()); + outFragColor = pixelShaderGaussianDepthAware(varTexCoord0, vec2(1.0, 0.0), getInvWidthHeight()); } diff --git a/libraries/render/src/render/blurGaussianDepthAwareV.slf b/libraries/render/src/render/blurGaussianDepthAwareV.slf index 4f9bc86c01..129f9f7ce3 100644 --- a/libraries/render/src/render/blurGaussianDepthAwareV.slf +++ b/libraries/render/src/render/blurGaussianDepthAwareV.slf @@ -19,6 +19,6 @@ layout(location=0) in vec2 varTexCoord0; layout(location=0) out vec4 outFragColor; void main(void) { - outFragColor = pixelShaderGaussianDepthAware(varTexCoord0, vec2(0.0, 1.0), getViewportInvWidthHeight()); + outFragColor = pixelShaderGaussianDepthAware(varTexCoord0, vec2(0.0, 1.0), getInvWidthHeight()); } diff --git a/libraries/render/src/render/blurGaussianH.slf b/libraries/render/src/render/blurGaussianH.slf index 1c13664907..251ef158f2 100644 --- a/libraries/render/src/render/blurGaussianH.slf +++ b/libraries/render/src/render/blurGaussianH.slf @@ -20,6 +20,6 @@ layout(location=0) in vec2 varTexCoord0; layout(location=0) out vec4 outFragColor; void main(void) { - outFragColor = pixelShaderGaussian(varTexCoord0, vec2(1.0, 0.0), getViewportInvWidthHeight()); + outFragColor = pixelShaderGaussian(varTexCoord0, vec2(1.0, 0.0), getInvWidthHeight()); } diff --git a/libraries/render/src/render/blurGaussianV.slf b/libraries/render/src/render/blurGaussianV.slf index f8351f9670..c1215e8553 100644 --- a/libraries/render/src/render/blurGaussianV.slf +++ b/libraries/render/src/render/blurGaussianV.slf @@ -19,6 +19,6 @@ layout(location=0) in vec2 varTexCoord0; layout(location=0) out vec4 outFragColor; void main(void) { - outFragColor = pixelShaderGaussian(varTexCoord0, vec2(0.0, 1.0), getViewportInvWidthHeight()); + outFragColor = pixelShaderGaussian(varTexCoord0, vec2(0.0, 1.0), getInvWidthHeight()); } diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 72918e33f6..0c4a593487 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -63,15 +63,15 @@ ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound optionsCopy.ambisonic = sound->isAmbisonic(); optionsCopy.localOnly = optionsCopy.localOnly || sound->isAmbisonic(); // force localOnly when Ambisonic - auto injector = AudioInjector::playSound(sound->getByteArray(), optionsCopy); + auto injector = AudioInjector::playSound(sound, optionsCopy); if (!injector) { - return NULL; + return nullptr; } return new ScriptAudioInjector(injector); } else { qCDebug(scriptengine) << "AudioScriptingInterface::playSound called with null Sound object."; - return NULL; + return nullptr; } } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 17afd3dbbd..fe8396bc50 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -766,8 +766,8 @@ protected: */ Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status); - EntityItemID currentEntityIdentifier {}; // Contains the defining entity script entity id during execution, if any. Empty for interface script execution. - QUrl currentSandboxURL {}; // The toplevel url string for the entity script that loaded the code being executed, else empty. + EntityItemID currentEntityIdentifier; // Contains the defining entity script entity id during execution, if any. Empty for interface script execution. + QUrl currentSandboxURL; // The toplevel url string for the entity script that loaded the code being executed, else empty. void doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function operation); void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args); diff --git a/libraries/shaders/headers/310es/header.glsl b/libraries/shaders/headers/310es/header.glsl index ac48d5c94c..9a0af85281 100644 --- a/libraries/shaders/headers/310es/header.glsl +++ b/libraries/shaders/headers/310es/header.glsl @@ -13,3 +13,4 @@ precision highp float; precision highp samplerBuffer; precision highp sampler2DShadow; precision highp sampler2DArrayShadow; +precision lowp sampler2DArray; diff --git a/libraries/shared/src/MathUtils.h b/libraries/shared/src/MathUtils.h new file mode 100644 index 0000000000..799890915e --- /dev/null +++ b/libraries/shared/src/MathUtils.h @@ -0,0 +1,21 @@ +// +// MathUtils.h +// libraries/shared/src +// +// Created by Olivier Prat on 9/21/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MathUtils_h +#define hifi_MathUtils_h + +template +T divideRoundUp(const T& numerator, int divisor) { + return (numerator + divisor - T(1)) / divisor; +} + +#endif // hifi_MathUtils_h + diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 52d359ad0d..8fc2d069ec 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -211,7 +211,7 @@ void TabletScriptingInterface::playSound(TabletAudioEvents aEvent) { options.ambisonic = sound->isAmbisonic(); options.localOnly = true; - AudioInjectorPointer injector = AudioInjector::playSoundAndDelete(sound->getByteArray(), options); + AudioInjectorPointer injector = AudioInjector::playSoundAndDelete(sound, options); } } diff --git a/libraries/ui/src/ui/types/SoundEffect.cpp b/libraries/ui/src/ui/types/SoundEffect.cpp index e35d009ef6..dc2328b33e 100644 --- a/libraries/ui/src/ui/types/SoundEffect.cpp +++ b/libraries/ui/src/ui/types/SoundEffect.cpp @@ -37,7 +37,6 @@ void SoundEffect::play(QVariant position) { _injector->setOptions(options); _injector->restart(); } else { - QByteArray samples = _sound->getByteArray(); - _injector = AudioInjector::playSound(samples, options); + _injector = AudioInjector::playSound(_sound, options); } } diff --git a/scripts/developer/utilities/render/ambientOcclusionPass.qml b/scripts/developer/utilities/render/ambientOcclusionPass.qml index 08334cf2aa..d6323c351a 100644 --- a/scripts/developer/utilities/render/ambientOcclusionPass.qml +++ b/scripts/developer/utilities/render/ambientOcclusionPass.qml @@ -7,48 +7,60 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 +import QtQuick 2.7 import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +import "qrc:///qml/styles-uit" +import "qrc:///qml/controls-uit" as HifiControls + import "configSlider" import "../lib/plotperf" -Column { - spacing: 8 +Rectangle { + HifiConstants { id: hifi;} + id: root; + anchors.margins: hifi.dimensions.contentMargin.x + + color: hifi.colors.baseGray; + Column { id: surfaceGeometry - spacing: 10 + spacing: 8 + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: hifi.dimensions.contentMargin.x - Column{ - Repeater { - model: [ - "Radius:radius:2.0:false", - "Level:obscuranceLevel:1.0:false", - "Num Taps:numSamples:32:true", - "Taps Spiral:numSpiralTurns:10.0:false", - "Falloff Bias:falloffBias:0.2:false", - "Edge Sharpness:edgeSharpness:1.0:false", - "Blur Radius:blurRadius:10.0:false", - ] - ConfigSlider { - label: qsTr(modelData.split(":")[0]) - integral: (modelData.split(":")[3] == 'true') - config: Render.getConfig("RenderMainView.AmbientOcclusion") - property: modelData.split(":")[1] - max: modelData.split(":")[2] - min: 0.0 - } + Repeater { + model: [ + "Blur Edge Sharpness:edgeSharpness:1.0:false", + "Blur Radius:blurRadius:15.0:true", + "Resolution Downscale:resolutionLevel:2:true", + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: (modelData.split(":")[3] == 'true') + config: Render.getConfig("RenderMainView.AmbientOcclusion") + property: modelData.split(":")[1] + max: modelData.split(":")[2] + min: 0.0 + height:38 } } - Row{ + + Row { + spacing: 10 Column { Repeater { model: [ - "resolutionLevel:resolutionLevel", + "horizonBased:horizonBased", + "jitterEnabled:jitterEnabled", "ditheringEnabled:ditheringEnabled", "fetchMipsEnabled:fetchMipsEnabled", "borderingEnabled:borderingEnabled" ] - CheckBox { + HifiControls.CheckBox { + boxSize: 20 text: qsTr(modelData.split(":")[0]) checked: Render.getConfig("RenderMainView.AmbientOcclusion")[modelData.split(":")[1]] onCheckedChanged: { Render.getConfig("RenderMainView.AmbientOcclusion")[modelData.split(":")[1]] = checked } @@ -60,7 +72,8 @@ Column { model: [ "debugEnabled:showCursorPixel" ] - CheckBox { + HifiControls.CheckBox { + boxSize: 20 text: qsTr(modelData.split(":")[0]) checked: Render.getConfig("RenderMainView.DebugAmbientOcclusion")[modelData.split(":")[1]] onCheckedChanged: { Render.getConfig("RenderMainView.DebugAmbientOcclusion")[modelData.split(":")[1]] = checked } @@ -84,5 +97,81 @@ Column { } ] } + + + + TabView { + anchors.left: parent.left + anchors.right: parent.right + height: 400 + + Tab { + title: "SSAO" + + Rectangle { + color: hifi.colors.baseGray; + + Column { + spacing: 8 + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: hifi.dimensions.contentMargin.x + + Repeater { + model: [ + "Radius:ssaoRadius:2.0:false", + "Level:ssaoObscuranceLevel:1.0:false", + "Num Taps:ssaoNumSamples:64:true", + "Taps Spiral:ssaoNumSpiralTurns:10.0:false", + "Falloff Angle:ssaoFalloffAngle:1.0:false", + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: (modelData.split(":")[3] == 'true') + config: Render.getConfig("RenderMainView.AmbientOcclusion") + property: modelData.split(":")[1] + max: modelData.split(":")[2] + min: 0.0 + height:38 + } + } + } + } + } + + Tab { + title: "HBAO" + + Rectangle { + color: hifi.colors.baseGray; + + Column { + spacing: 8 + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: hifi.dimensions.contentMargin.x + + Repeater { + model: [ + "Radius:hbaoRadius:2.0:false", + "Level:hbaoObscuranceLevel:1.0:false", + "Num Taps:hbaoNumSamples:6:true", + "Falloff Angle:hbaoFalloffAngle:1.0:false", + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: (modelData.split(":")[3] == 'true') + config: Render.getConfig("RenderMainView.AmbientOcclusion") + property: modelData.split(":")[1] + max: modelData.split(":")[2] + min: 0.0 + height:38 + } + } + } + } + } + } + } } diff --git a/scripts/developer/utilities/render/debugAmbientOcclusionPass.js b/scripts/developer/utilities/render/debugAmbientOcclusionPass.js index f70b3a5cc9..19c29c59a3 100644 --- a/scripts/developer/utilities/render/debugAmbientOcclusionPass.js +++ b/scripts/developer/utilities/render/debugAmbientOcclusionPass.js @@ -1,38 +1,53 @@ +"use strict"; + // -// debugSurfaceGeometryPass.js +// debugAmbientOcclusionPass.js +// tablet-sample-app // -// Created by Sam Gateau on 6/6/2016 -// Copyright 2016 High Fidelity, Inc. +// Created by Olivier Prat on April 19 2018. +// Copyright 2018 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// Set up the qml ui -var qml = Script.resolvePath('ambientOcclusionPass.qml'); -var window = new OverlayWindow({ - title: 'Ambient Occlusion Pass', - source: qml, - width: 400, height: 300, -}); -window.setPosition(Window.innerWidth - 420, 50 + 550 + 50); -window.closed.connect(function() { Script.stop(); }); +(function() { + var AppUi = Script.require('appUi'); + var onMousePressEvent = function (e) { + }; + Controller.mousePressEvent.connect(onMousePressEvent); -var moveDebugCursor = false; -Controller.mousePressEvent.connect(function (e) { - if (e.isMiddleButton) { - moveDebugCursor = true; - setDebugCursor(e.x, e.y); + var onMouseReleaseEvent = function () { + }; + Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); + + var onMouseMoveEvent = function (e) { + }; + Controller.mouseMoveEvent.connect(onMouseMoveEvent); + + function fromQml(message) { + } -}); -Controller.mouseReleaseEvent.connect(function() { moveDebugCursor = false; }); -Controller.mouseMoveEvent.connect(function (e) { if (moveDebugCursor) setDebugCursor(e.x, e.y); }); - -function setDebugCursor(x, y) { - nx = (x / Window.innerWidth); - ny = 1.0 - ((y) / (Window.innerHeight - 32)); - - Render.getConfig("RenderMainView").getConfig("DebugAmbientOcclusion").debugCursorTexcoord = { x: nx, y: ny }; -} + var ui; + function startup() { + ui = new AppUi({ + buttonName: "AO", + home: Script.resolvePath("ambientOcclusionPass.qml"), + onMessage: fromQml, + //normalButton: Script.resolvePath("../../../system/assets/images/ao-i.svg"), + //activeButton: Script.resolvePath("../../../system/assets/images/ao-a.svg") + }); + } + startup(); + Script.scriptEnding.connect(function () { + Controller.mousePressEvent.disconnect(onMousePressEvent); + Controller.mouseReleaseEvent.disconnect(onMouseReleaseEvent); + Controller.mouseMoveEvent.disconnect(onMouseMoveEvent); + pages.clear(); + // killEngineInspectorView(); + // killCullInspectorView(); + // killEngineLODWindow(); + }); +}()); diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 4bc4941358..78edf7939f 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -204,6 +204,7 @@ Rectangle { ListElement { text: "Debug Scattering"; color: "White" } ListElement { text: "Ambient Occlusion"; color: "White" } ListElement { text: "Ambient Occlusion Blurred"; color: "White" } + ListElement { text: "Ambient Occlusion Normal"; color: "White" } ListElement { text: "Velocity"; color: "White" } ListElement { text: "Custom"; color: "White" } } diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 0eccbacb3f..3a291bf27b 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -252,7 +252,7 @@ input.search:focus { box-shadow: 0 0 0 1px #00b4ef; } -input:disabled, textarea:disabled { +input:disabled, textarea:disabled, .draggable-number.text[disabled="disabled"] { background-color: #383838; color: #afafaf; } @@ -889,6 +889,9 @@ div.refresh input[type="button"] { border-color: #afafaf; } +.colpick { + z-index: 3; +} .colpick[disabled="disabled"] { display: none !important; } @@ -911,6 +914,79 @@ div.refresh input[type="button"] { clear: both; } +.draggable-number { + position: relative; +} +.draggable-number div { + height: 28px; + width: 92px; +} +.draggable-number.text { + display: inline-block; + color: #afafaf; + background-color: #252525; + font-family: FiraSans-SemiBold; + font-size: 15px; + margin: 0; + padding: 0 16px; + height: 28px; + width: 100%; + line-height: 2; +} +.draggable-number.text:hover { + cursor: ew-resize; +} +.draggable-number span { + position: absolute; + display: inline-block; + font-family: HiFi-Glyphs; + font-size: 20px; + z-index: 2; +} +.draggable-number span:hover { + cursor: default; +} +.draggable-number.left-arrow { + top: -5px; + right: 106px; + transform: rotate(180deg); +} +.draggable-number.right-arrow { + top: -5px; + left: 106px; +} +.draggable-number input[type=number] { + position: absolute; + right: 0; + width: 100%; +} +.draggable-number input[type=button] { + position: absolute; + top: 0; +} +.draggable-number input::-webkit-inner-spin-button { + -webkit-appearance: none; + visibility: hidden; +} +.draggable-number.fstuple { + height: 28px; + width: 124px; + left: 12px; +} +.draggable-number.fstuple + .draggable-number.fstuple { + padding-left: 28px; +} +.draggable-number.fstuple input { + right: -10px; +} +.draggable-number.fstuple .sublabel { + position: absolute; + top: 0; + left: -16px; + font-family: FiraSans-SemiBold; + font-size: 15px; +} + .row .property { width: auto; display: inline-block; @@ -927,10 +1003,10 @@ div.refresh input[type="button"] { .property.texture { display: block; } -.property.texture input{ +.property.texture input { margin: 0.4rem 0; } -.texture-image img{ +.texture-image img { padding: 0; margin: 0; width: 100%; @@ -1362,17 +1438,12 @@ input[type=button]#export { } input#property-scale-button-rescale { - margin-top: 6px; min-width: 50px; + left: 152px; } input#property-scale-button-reset { - margin-top: 6px; margin-right: 0; -} - -#property-userData-button-edit, -#property-materialData-button-clear { - margin: 6px 0 6px 0; + left: 250px; } #property-userData-static, @@ -1557,6 +1628,10 @@ input.number-slider { flex-flow: column; } +.flex-column + .flex-column { + padding-left: 50px; +} + .flex-center { align-items: center; } diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 6af8ee992a..01395368b0 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -22,6 +22,7 @@ + diff --git a/scripts/system/html/js/draggableNumber.js b/scripts/system/html/js/draggableNumber.js new file mode 100644 index 0000000000..1f4bc21441 --- /dev/null +++ b/scripts/system/html/js/draggableNumber.js @@ -0,0 +1,158 @@ +// draggableNumber.js +// +// Created by David Back on 7 Nov 2018 +// 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 + +const DELTA_X_FOCUS_THRESHOLD = 1; + +function DraggableNumber(min, max, step, decimals) { + this.min = min; + this.max = max; + this.step = step !== undefined ? step : 1; + this.decimals = decimals; + this.initialMouseEvent = null; + this.lastMouseEvent = null; + this.valueChangeFunction = null; + this.initialize(); +} + +DraggableNumber.prototype = { + mouseDown: function(event) { + if (event.target === this.elText) { + this.initialMouseEvent = event; + this.lastMouseEvent = event; + document.addEventListener("mousemove", this.onDocumentMouseMove); + document.addEventListener("mouseup", this.onDocumentMouseUp); + } + }, + + mouseUp: function(event) { + if (event.target === this.elText && this.initialMouseEvent) { + let dx = event.clientX - this.initialMouseEvent.clientX; + if (dx <= DELTA_X_FOCUS_THRESHOLD) { + this.elInput.style.visibility = "visible"; + this.elText.style.visibility = "hidden"; + } + this.initialMouseEvent = null; + } + }, + + documentMouseMove: function(event) { + if (this.lastMouseEvent) { + let initialValue = this.elInput.value; + let dx = event.clientX - this.lastMouseEvent.clientX; + let changeValue = dx !== 0; + if (changeValue) { + while (dx !== 0) { + if (dx > 0) { + this.stepUp(); + --dx; + } else { + this.stepDown(); + ++dx; + } + } + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } + } + this.lastMouseEvent = event; + } + }, + + documentMouseUp: function(event) { + this.lastMouseEvent = null; + document.removeEventListener("mousemove", this.onDocumentMouseMove); + document.removeEventListener("mouseup", this.onDocumentMouseUp); + }, + + stepUp: function() { + this.elInput.stepUp(); + this.inputChange(); + }, + + stepDown: function() { + this.elInput.stepDown(); + this.inputChange(); + }, + + setValue: function(newValue) { + if (newValue !== "" && this.decimals !== undefined) { + this.elInput.value = parseFloat(newValue).toFixed(this.decimals); + } else { + this.elInput.value = newValue; + } + this.elText.firstChild.data = this.elInput.value; + }, + + setValueChangeFunction: function(valueChangeFunction) { + if (this.valueChangeFunction) { + this.elInput.removeEventListener("change", this.valueChangeFunction); + } + this.valueChangeFunction = valueChangeFunction.bind(this.elInput); + this.elInput.addEventListener("change", this.valueChangeFunction); + }, + + inputChange: function() { + this.setValue(this.elInput.value); + }, + + inputBlur: function() { + this.elInput.style.visibility = "hidden"; + this.elText.style.visibility = "visible"; + }, + + initialize: function() { + this.onMouseDown = this.mouseDown.bind(this); + this.onMouseUp = this.mouseUp.bind(this); + this.onDocumentMouseMove = this.documentMouseMove.bind(this); + this.onDocumentMouseUp = this.documentMouseUp.bind(this); + this.onStepUp = this.stepUp.bind(this); + this.onStepDown = this.stepDown.bind(this); + this.onInputChange = this.inputChange.bind(this); + this.onInputBlur = this.inputBlur.bind(this); + + this.elDiv = document.createElement('div'); + this.elDiv.className = "draggable-number"; + + this.elText = document.createElement('label'); + this.elText.className = "draggable-number text"; + this.elText.innerText = " "; + this.elText.style.visibility = "visible"; + this.elText.addEventListener("mousedown", this.onMouseDown); + this.elText.addEventListener("mouseup", this.onMouseUp); + + this.elLeftArrow = document.createElement('span'); + this.elRightArrow = document.createElement('span'); + this.elLeftArrow.className = 'draggable-number left-arrow'; + this.elLeftArrow.innerHTML = 'D'; + this.elLeftArrow.addEventListener("click", this.onStepDown); + this.elRightArrow.className = 'draggable-number right-arrow'; + this.elRightArrow.innerHTML = 'D'; + this.elRightArrow.addEventListener("click", this.onStepUp); + + this.elInput = document.createElement('input'); + this.elInput.className = "draggable-number input"; + this.elInput.setAttribute("type", "number"); + if (this.min !== undefined) { + this.elInput.setAttribute("min", this.min); + } + if (this.max !== undefined) { + this.elInput.setAttribute("max", this.max); + } + if (this.step !== undefined) { + this.elInput.setAttribute("step", this.step); + } + this.elInput.style.visibility = "hidden"; + this.elInput.addEventListener("change", this.onInputChange); + this.elInput.addEventListener("blur", this.onInputBlur); + + this.elText.appendChild(this.elLeftArrow); + this.elText.appendChild(this.elInput); + this.elText.appendChild(this.elRightArrow); + this.elDiv.appendChild(this.elText); + } +}; diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 00c50169a6..84ad59df36 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -153,6 +153,9 @@ const ICON_FOR_TYPE = { Text: "l", }; +const DOUBLE_CLICK_TIMEOUT = 300; // ms +const RENAME_COOLDOWN = 400; // ms + // List of all entities let entities = []; // List of all entities, indexed by Entity ID @@ -181,6 +184,10 @@ let currentResizeEl = null; let startResizeEvent = null; let resizeColumnIndex = 0; let startThClick = null; +let renameTimeout = null; +let renameLastBlur = null; +let renameLastEntityID = null; +let isRenameFieldBeingMoved = false; let elEntityTable, elEntityTableHeader, @@ -204,7 +211,8 @@ let elEntityTable, elNoEntitiesMessage, elColumnsMultiselectBox, elColumnsOptions, - elToggleSpaceMode; + elToggleSpaceMode, + elRenameInput; const ENABLE_PROFILING = false; let profileIndent = ''; @@ -388,19 +396,20 @@ function loaded() { elEntityTableHeaderRow = document.querySelectorAll("#entity-table thead th"); - entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, - createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT); + entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, createRow, updateRow, + clearRow, preRefresh, postRefresh, preRefresh, WINDOW_NONVARIABLE_HEIGHT); entityListContextMenu = new EntityListContextMenu(); function startRenamingEntity(entityID) { + renameLastEntityID = entityID; let entity = entitiesByID[entityID]; if (!entity || entity.locked || !entity.elRow) { return; } let elCell = entity.elRow.childNodes[getColumnIndex("name")]; - let elRenameInput = document.createElement("input"); + elRenameInput = document.createElement("input"); elRenameInput.setAttribute('class', 'rename-entity'); elRenameInput.value = entity.name; let ignoreClicks = function(event) { @@ -415,6 +424,9 @@ function loaded() { }; elRenameInput.onblur = function(event) { + if (isRenameFieldBeingMoved) { + return; + } let value = elRenameInput.value; EventBridge.emitWebEvent(JSON.stringify({ type: 'rename', @@ -422,7 +434,10 @@ function loaded() { name: value })); entity.name = value; - elCell.innerText = value; + elRenameInput.parentElement.innerText = value; + + renameLastBlur = Date.now(); + elRenameInput = null; }; elCell.innerHTML = ""; @@ -431,6 +446,32 @@ function loaded() { elRenameInput.select(); } + function preRefresh() { + // move the rename input to the body + if (!isRenameFieldBeingMoved && elRenameInput) { + isRenameFieldBeingMoved = true; + document.body.appendChild(elRenameInput); + // keep the focus + elRenameInput.select(); + } + } + + function postRefresh() { + if (!elRenameInput || !isRenameFieldBeingMoved) { + return; + } + let entity = entitiesByID[renameLastEntityID]; + if (!entity || entity.locked || !entity.elRow) { + return; + } + let elCell = entity.elRow.childNodes[getColumnIndex("name")]; + elCell.innerHTML = ""; + elCell.appendChild(elRenameInput); + // keep the focus + elRenameInput.select(); + isRenameFieldBeingMoved = false; + } + entityListContextMenu.setOnSelectedCallback(function(optionName, selectedEntityID) { switch (optionName) { case "Cut": @@ -455,6 +496,11 @@ function loaded() { }); function onRowContextMenu(clickEvent) { + if (elRenameInput) { + // disallow the context menu from popping up while renaming + return; + } + let entityID = this.dataset.entityID; if (!selectedEntities.includes(entityID)) { @@ -478,6 +524,13 @@ function loaded() { entityListContextMenu.open(clickEvent, entityID, enabledContextMenuItems); } + let clearRenameTimeout = () => { + if (renameTimeout !== null) { + window.clearTimeout(renameTimeout); + renameTimeout = null; + } + }; + function onRowClicked(clickEvent) { let entityID = this.dataset.entityID; let selection = [entityID]; @@ -516,7 +569,15 @@ function loaded() { } else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) { // if reselecting the same entity then start renaming it if (selectedEntities[0] === entityID) { - startRenamingEntity(entityID); + if (renameLastBlur && renameLastEntityID === entityID && (Date.now() - renameLastBlur) < RENAME_COOLDOWN) { + + return; + } + clearRenameTimeout(); + renameTimeout = window.setTimeout(() => { + renameTimeout = null; + startRenamingEntity(entityID); + }, DOUBLE_CLICK_TIMEOUT); } } @@ -530,6 +591,8 @@ function loaded() { } function onRowDoubleClicked() { + clearRenameTimeout(); + let selection = [this.dataset.entityID]; updateSelectedEntities(selection, false); @@ -1100,12 +1163,12 @@ function loaded() { startResizeEvent = ev; } } - } + }; document.onmouseup = function(ev) { startResizeEvent = null; ev.stopPropagation(); - } + }; function setSpaceMode(spaceMode) { if (spaceMode === "local") { @@ -1219,7 +1282,7 @@ function loaded() { refreshSortOrder(); refreshEntities(); - window.onresize = updateColumnWidths; + window.addEventListener("resize", updateColumnWidths); }); augmentSpinButtons(); diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 16c503a0bb..c49e0d88f6 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -260,7 +260,7 @@ const GROUPS = [ buttons: [ { id: "copy", label: "Copy from Skybox", className: "black", onClick: copySkyboxURLToAmbientURL } ], propertyID: "copyURLToAmbient", - showPropertyRule: { "skyboxMode": "enabled" }, + showPropertyRule: { "ambientLightMode": "enabled" }, }, { label: "Haze", @@ -315,7 +315,7 @@ const GROUPS = [ }, { label: "Background Blend", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -337,7 +337,7 @@ const GROUPS = [ }, { label: "Glare Angle", - type: "slider", + type: "number", min: 0, max: 180, step: 1, @@ -353,7 +353,7 @@ const GROUPS = [ }, { label: "Bloom Intensity", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -363,7 +363,7 @@ const GROUPS = [ }, { label: "Bloom Threshold", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -373,7 +373,7 @@ const GROUPS = [ }, { label: "Bloom Size", - type: "slider", + type: "number", min: 0, max: 2, step: 0.01, @@ -621,7 +621,7 @@ const GROUPS = [ }, { label: "Lifespan", - type: "slider", + type: "number", unit: "s", min: 0.01, max: 10, @@ -631,7 +631,7 @@ const GROUPS = [ }, { label: "Max Particles", - type: "slider", + type: "number", min: 1, max: 10000, step: 1, @@ -652,7 +652,7 @@ const GROUPS = [ properties: [ { label: "Emit Rate", - type: "slider", + type: "number", min: 1, max: 1000, step: 1, @@ -660,7 +660,7 @@ const GROUPS = [ }, { label: "Emit Speed", - type: "slider", + type: "number", min: 0, max: 5, step: 0.01, @@ -669,7 +669,7 @@ const GROUPS = [ }, { label: "Speed Spread", - type: "slider", + type: "number", min: 0, max: 5, step: 0.01, @@ -688,7 +688,7 @@ const GROUPS = [ }, { label: "Emit Radius Start", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -723,7 +723,7 @@ const GROUPS = [ properties: [ { label: "Start", - type: "slider", + type: "number", min: 0, max: 4, step: 0.01, @@ -733,7 +733,7 @@ const GROUPS = [ }, { label: "Middle", - type: "slider", + type: "number", min: 0, max: 4, step: 0.01, @@ -742,7 +742,7 @@ const GROUPS = [ }, { label: "Finish", - type: "slider", + type: "number", min: 0, max: 4, step: 0.01, @@ -754,7 +754,7 @@ const GROUPS = [ }, { label: "Size Spread", - type: "slider", + type: "number", min: 0, max: 4, step: 0.01, @@ -810,7 +810,7 @@ const GROUPS = [ properties: [ { label: "Start", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -820,7 +820,7 @@ const GROUPS = [ }, { label: "Middle", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -829,7 +829,7 @@ const GROUPS = [ }, { label: "Finish", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -841,7 +841,7 @@ const GROUPS = [ }, { label: "Alpha Spread", - type: "slider", + type: "number", min: 0, max: 1, step: 0.01, @@ -886,7 +886,7 @@ const GROUPS = [ properties: [ { label: "Start", - type: "slider", + type: "number", min: -360, max: 360, step: 1, @@ -898,7 +898,7 @@ const GROUPS = [ }, { label: "Middle", - type: "slider", + type: "number", min: -360, max: 360, step: 1, @@ -909,7 +909,7 @@ const GROUPS = [ }, { label: "Finish", - type: "slider", + type: "number", min: -360, max: 360, step: 1, @@ -923,7 +923,7 @@ const GROUPS = [ }, { label: "Spin Spread", - type: "slider", + type: "number", min: 0, max: 360, step: 1, @@ -950,7 +950,7 @@ const GROUPS = [ properties: [ { label: "Start", - type: "slider", + type: "number", min: -180, max: 0, step: 1, @@ -961,7 +961,7 @@ const GROUPS = [ }, { label: "Finish", - type: "slider", + type: "number", min: 0, max: 180, step: 1, @@ -978,7 +978,7 @@ const GROUPS = [ properties: [ { label: "Start", - type: "slider", + type: "number", min: 0, max: 180, step: 1, @@ -989,7 +989,7 @@ const GROUPS = [ }, { label: "Finish", - type: "slider", + type: "number", min: 0, max: 180, step: 1, @@ -1264,6 +1264,7 @@ const GROUPS = [ label: "Linear Velocity", type: "vec3", vec3Type: "xyz", + step: 0.1, decimals: 4, subLabels: [ "x", "y", "z" ], unit: "m/s", @@ -1272,6 +1273,9 @@ const GROUPS = [ { label: "Linear Damping", type: "number", + min: 0, + max: 1, + step: 0.01, decimals: 2, propertyID: "damping", }, @@ -1288,24 +1292,36 @@ const GROUPS = [ { label: "Angular Damping", type: "number", + min: 0, + max: 1, + step: 0.01, decimals: 4, propertyID: "angularDamping", }, { label: "Bounciness", type: "number", + min: 0, + max: 1, + step: 0.01, decimals: 4, propertyID: "restitution", }, { label: "Friction", type: "number", + min: 0, + max: 10, + step: 0.1, decimals: 4, propertyID: "friction", }, { label: "Density", type: "number", + min: 100, + max: 10000, + step: 1, decimals: 4, propertyID: "density", }, @@ -1314,6 +1330,8 @@ const GROUPS = [ type: "vec3", vec3Type: "xyz", subLabels: [ "x", "y", "z" ], + step: 0.1, + decimals: 4, unit: "m/s2", propertyID: "gravity", }, @@ -1374,26 +1392,16 @@ const PROPERTY_NAME_DIVISION = { }; const VECTOR_ELEMENTS = { - X_INPUT: 0, - Y_INPUT: 1, - Z_INPUT: 2, + X_NUMBER: 0, + Y_NUMBER: 1, + Z_NUMBER: 2, }; const COLOR_ELEMENTS = { COLOR_PICKER: 0, - RED_INPUT: 1, - GREEN_INPUT: 2, - BLUE_INPUT: 3, -}; - -const SLIDER_ELEMENTS = { - SLIDER: 0, - NUMBER_INPUT: 1, -}; - -const ICON_ELEMENTS = { - ICON: 0, - LABEL: 1, + RED_NUMBER: 1, + GREEN_NUMBER: 2, + BLUE_NUMBER: 3, }; const TEXTURE_ELEMENTS = { @@ -1427,17 +1435,17 @@ function getPropertyInputElement(propertyID) { switch (property.data.type) { case 'string': case 'bool': - case 'number': - case 'slider': case 'dropdown': case 'textarea': case 'texture': return property.elInput; + case 'number': + return property.elNumber.elInput; case 'vec3': case 'vec2': - return { x: property.elInputX, y: property.elInputY, z: property.elInputZ }; + return { x: property.elNumberX.elInput, y: property.elNumberY.elInput, z: property.elNumberZ.elInput }; case 'color': - return { red: property.elInputR, green: property.elInputG, blue: property.elInputB }; + return { red: property.elNumberR.elInput, green: property.elNumberG.elInput, blue: property.elNumberB.elInput }; case 'icon': return property.elLabel; default: @@ -1460,10 +1468,11 @@ function disableChildren(el, selector) { } function enableProperties() { - enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + enableChildren(document.getElementById("properties-list"), + "input, textarea, checkbox, .dropdown dl, .color-picker , .draggable-number.text"); enableChildren(document, ".colpick"); + let elLocked = getPropertyInputElement("locked"); - if (elLocked.checked === false) { removeStaticUserData(); removeStaticMaterialData(); @@ -1471,13 +1480,14 @@ function enableProperties() { } function disableProperties() { - disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + disableChildren(document.getElementById("properties-list"), + "input, textarea, checkbox, .dropdown dl, .color-picker, .draggable-number.text"); disableChildren(document, ".colpick"); for (let pickKey in colorPickers) { colorPickers[pickKey].colpickHide(); } + let elLocked = getPropertyInputElement("locked"); - if (elLocked.checked === true) { if ($('#property-userData-editor').css('display') === "block") { showStaticUserData(); @@ -1507,32 +1517,28 @@ function resetProperties() { property.elInput.checked = false; break; } - case 'number': - case 'slider': { + case 'number': { if (propertyData.defaultValue !== undefined) { - property.elInput.value = propertyData.defaultValue; + property.elNumber.setValue(propertyData.defaultValue); } else { - property.elInput.value = ""; - } - if (property.elSlider !== undefined) { - property.elSlider.value = property.elInput.value; + property.elNumber.setValue(""); } break; } case 'vec3': case 'vec2': { - property.elInputX.value = ""; - property.elInputY.value = ""; - if (property.elInputZ !== undefined) { - property.elInputZ.value = ""; + property.elNumberX.setValue(""); + property.elNumberY.setValue(""); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(""); } break; } case 'color': { property.elColorPicker.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - property.elInputR.value = ""; - property.elInputG.value = ""; - property.elInputB.value = ""; + property.elNumberR.setValue(""); + property.elNumberG.setValue(""); + property.elNumberB.setValue(""); break; } case 'dropdown': { @@ -1611,6 +1617,20 @@ function getPropertyValue(originalPropertyName) { return propertyValue; } +function updateVisibleSpaceModeProperties() { + for (let propertyID in properties) { + if (properties.hasOwnProperty(propertyID)) { + let property = properties[propertyID]; + let propertySpaceMode = property.spaceMode; + if (propertySpaceMode !== PROPERTY_SPACE_MODE.ALL) { + showPropertyElement(propertyID, propertySpaceMode === currentSpaceMode); + } else { + showPropertyElement(propertyID, true); + } + } + } +} + /** * PROPERTY UPDATE FUNCTIONS @@ -1797,10 +1817,12 @@ function createBoolProperty(property, elProperty) { let subPropertyOf = propertyData.subPropertyOf; if (subPropertyOf !== undefined) { elInput.addEventListener('change', function() { - updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], elInput, propertyName, property.isParticleProperty); + updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], + elInput, propertyName, property.isParticleProperty); }); } else { - elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse, property.isParticleProperty)); + elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse, + property.isParticleProperty)); } return elInput; @@ -1811,98 +1833,28 @@ function createNumberProperty(property, elProperty) { let elementID = property.elementID; let propertyData = property.data; - elProperty.className = "number"; + elProperty.className = "draggable-number"; + + let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, + propertyData.step, propertyData.decimals); - let elInput = document.createElement('input'); - elInput.setAttribute("id", elementID); - elInput.setAttribute("type", "number"); - if (propertyData.min !== undefined) { - elInput.setAttribute("min", propertyData.min); - } - if (propertyData.max !== undefined) { - elInput.setAttribute("max", propertyData.max); - } - if (propertyData.step !== undefined) { - elInput.setAttribute("step", propertyData.step); + let defaultValue = propertyData.defaultValue; + if (defaultValue !== undefined) { + elDraggableNumber.elInput.value = defaultValue; } + + let valueChangeFunction = createEmitNumberPropertyUpdateFunction(propertyName, propertyData.multiplier, + property.isParticleProperty); + elDraggableNumber.setValueChangeFunction(valueChangeFunction); - let defaultValue = propertyData.defaultValue; - if (defaultValue !== undefined) { - elInput.value = defaultValue; - } - - elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, propertyData.multiplier, propertyData.decimals, property.isParticleProperty)); - - elProperty.appendChild(elInput); - + elDraggableNumber.elInput.setAttribute("id", elementID); + elProperty.appendChild(elDraggableNumber.elDiv); + if (propertyData.buttons !== undefined) { - addButtons(elProperty, elementID, propertyData.buttons, true); + addButtons(elDraggableNumber.elText, elementID, propertyData.buttons, false); } - return elInput; -} - -function createSliderProperty(property, elProperty) { - let propertyData = property.data; - - elProperty.className = "range"; - - let elDiv = document.createElement("div"); - elDiv.className = "slider-wrapper"; - - let elSlider = document.createElement("input"); - elSlider.setAttribute("type", "range"); - - let elInput = document.createElement("input"); - elInput.setAttribute("type", "number"); - - if (propertyData.min !== undefined) { - elInput.setAttribute("min", propertyData.min); - elSlider.setAttribute("min", propertyData.min); - } - if (propertyData.max !== undefined) { - elInput.setAttribute("max", propertyData.max); - elSlider.setAttribute("max", propertyData.max); - elSlider.setAttribute("data-max", propertyData.max); - } - if (propertyData.step !== undefined) { - elInput.setAttribute("step", propertyData.step); - elSlider.setAttribute("step", propertyData.step); - } - - elInput.onchange = function (event) { - let inputValue = event.target.value; - elSlider.value = inputValue; - if (propertyData.multiplier !== undefined) { - inputValue *= propertyData.multiplier; - } - updateProperty(property.name, inputValue, property.isParticleProperty); - }; - elSlider.oninput = function (event) { - let sliderValue = event.target.value; - if (propertyData.step === 1) { - if (sliderValue > 0) { - elInput.value = Math.floor(sliderValue); - } else { - elInput.value = Math.ceil(sliderValue); - } - } else { - elInput.value = sliderValue; - } - if (propertyData.multiplier !== undefined) { - sliderValue *= propertyData.multiplier; - } - updateProperty(property.name, sliderValue, property.isParticleProperty); - }; - - elDiv.appendChild(elSlider); - elDiv.appendChild(elInput); - elProperty.appendChild(elDiv); - - let elResult = []; - elResult[SLIDER_ELEMENTS.SLIDER] = elSlider; - elResult[SLIDER_ELEMENTS.NUMBER_INPUT] = elInput; - return elResult; + return elDraggableNumber; } function createVec3Property(property, elProperty) { @@ -1912,23 +1864,24 @@ function createVec3Property(property, elProperty) { elProperty.className = propertyData.vec3Type + " fstuple"; - let elInputX = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_INPUT], - propertyData.min, propertyData.max, propertyData.step); - let elInputY = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_INPUT], - propertyData.min, propertyData.max, propertyData.step); - let elInputZ = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Z_INPUT], - propertyData.min, propertyData.max, propertyData.step); + let elNumberX = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let elNumberY = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let elNumberZ = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Z_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); - let inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ, - propertyData.multiplier, property.isParticleProperty); - elInputX.addEventListener('change', inputChangeFunction); - elInputY.addEventListener('change', inputChangeFunction); - elInputZ.addEventListener('change', inputChangeFunction); + let valueChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elNumberX.elInput, elNumberY.elInput, + elNumberZ.elInput, propertyData.multiplier, + property.isParticleProperty); + elNumberX.setValueChangeFunction(valueChangeFunction); + elNumberY.setValueChangeFunction(valueChangeFunction); + elNumberZ.setValueChangeFunction(valueChangeFunction); let elResult = []; - elResult[VECTOR_ELEMENTS.X_INPUT] = elInputX; - elResult[VECTOR_ELEMENTS.Y_INPUT] = elInputY; - elResult[VECTOR_ELEMENTS.Z_INPUT] = elInputZ; + elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; + elResult[VECTOR_ELEMENTS.Y_NUMBER] = elNumberY; + elResult[VECTOR_ELEMENTS.Z_NUMBER] = elNumberZ; return elResult; } @@ -1944,19 +1897,19 @@ function createVec2Property(property, elProperty) { elProperty.appendChild(elTuple); - let elInputX = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_INPUT], - propertyData.min, propertyData.max, propertyData.step); - let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_INPUT], - propertyData.min, propertyData.max, propertyData.step); + let elNumberX = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let elNumberY = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); - let inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, elInputY, + let valueChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elNumberX.elInput, elNumberY.elInput, propertyData.multiplier, property.isParticleProperty); - elInputX.addEventListener('change', inputChangeFunction); - elInputY.addEventListener('change', inputChangeFunction); + elNumberX.setValueChangeFunction(valueChangeFunction); + elNumberY.setValueChangeFunction(valueChangeFunction); let elResult = []; - elResult[VECTOR_ELEMENTS.X_INPUT] = elInputX; - elResult[VECTOR_ELEMENTS.Y_INPUT] = elInputY; + elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; + elResult[VECTOR_ELEMENTS.Y_NUMBER] = elNumberY; return elResult; } @@ -1976,15 +1929,15 @@ function createColorProperty(property, elProperty) { elProperty.appendChild(elColorPicker); elProperty.appendChild(elTuple); - let elInputR = createTupleNumberInput(elTuple, elementID, "red", COLOR_MIN, COLOR_MAX, COLOR_STEP); - let elInputG = createTupleNumberInput(elTuple, elementID, "green", COLOR_MIN, COLOR_MAX, COLOR_STEP); - let elInputB = createTupleNumberInput(elTuple, elementID, "blue", COLOR_MIN, COLOR_MAX, COLOR_STEP); + let elNumberR = createTupleNumberInput(elTuple, elementID, "red", COLOR_MIN, COLOR_MAX, COLOR_STEP); + let elNumberG = createTupleNumberInput(elTuple, elementID, "green", COLOR_MIN, COLOR_MAX, COLOR_STEP); + let elNumberB = createTupleNumberInput(elTuple, elementID, "blue", COLOR_MIN, COLOR_MAX, COLOR_STEP); - let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB, - property.isParticleProperty); - elInputR.addEventListener('change', inputChangeFunction); - elInputG.addEventListener('change', inputChangeFunction); - elInputB.addEventListener('change', inputChangeFunction); + let valueChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elNumberR.elInput, elNumberG.elInput, + elNumberB.elInput, property.isParticleProperty); + elNumberR.setValueChangeFunction(valueChangeFunction); + elNumberG.setValueChangeFunction(valueChangeFunction); + elNumberB.setValueChangeFunction(valueChangeFunction); let colorPickerID = "#" + elementID; colorPickers[colorPickerID] = $(colorPickerID).colpick({ @@ -1997,9 +1950,9 @@ function createColorProperty(property, elProperty) { // The original color preview within the picker needs to be updated on show because // prior to the picker being shown we don't have access to the selections' starting color. colorPickers[colorPickerID].colpickSetColor({ - "r": elInputR.value, - "g": elInputG.value, - "b": elInputB.value + "r": elNumberR.elInput.value, + "g": elNumberG.elInput.value, + "b": elNumberB.elInput.value }); }, onHide: function(colpick) { @@ -2013,9 +1966,9 @@ function createColorProperty(property, elProperty) { let elResult = []; elResult[COLOR_ELEMENTS.COLOR_PICKER] = elColorPicker; - elResult[COLOR_ELEMENTS.RED_INPUT] = elInputR; - elResult[COLOR_ELEMENTS.GREEN_INPUT] = elInputG; - elResult[COLOR_ELEMENTS.BLUE_INPUT] = elInputB; + elResult[COLOR_ELEMENTS.RED_NUMBER] = elNumberR; + elResult[COLOR_ELEMENTS.GREEN_NUMBER] = elNumberG; + elResult[COLOR_ELEMENTS.BLUE_NUMBER] = elNumberB; return elResult; } @@ -2141,37 +2094,30 @@ function createButtonsProperty(property, elProperty, elLabel) { let propertyData = property.data; elProperty.className = "text"; - - let hasLabel = propertyData.label !== undefined; + if (propertyData.buttons !== undefined) { - addButtons(elProperty, elementID, propertyData.buttons, hasLabel); + addButtons(elProperty, elementID, propertyData.buttons, false); } return elProperty; } -function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, step) { +function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, step, decimals) { let elementID = propertyElementID + "-" + subLabel.toLowerCase(); - let elDiv = document.createElement('div'); let elLabel = document.createElement('label'); - elLabel.className = subLabel; + elLabel.className = "sublabel " + subLabel; elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1); elLabel.setAttribute("for", elementID); + elLabel.style.visibility = "visible"; - let elInput = document.createElement('input'); - elInput.className = subLabel + " number-slider"; - elInput.setAttribute("id", elementID); - elInput.setAttribute("type", "number"); - elInput.setAttribute("min", min); - elInput.setAttribute("max", max); - elInput.setAttribute("step", step); + let elDraggableNumber = new DraggableNumber(min, max, step, decimals); + elDraggableNumber.elInput.setAttribute("id", elementID); + elDraggableNumber.elDiv.className += " fstuple"; + elDraggableNumber.elText.insertBefore(elLabel, elDraggableNumber.elLeftArrow); + elTuple.appendChild(elDraggableNumber.elDiv); - elDiv.appendChild(elLabel); - elDiv.appendChild(elInput); - elTuple.appendChild(elDiv); - - return elInput; + return elDraggableNumber; } function addButtons(elProperty, propertyID, buttons, newRow) { @@ -2198,6 +2144,85 @@ function addButtons(elProperty, propertyID, buttons, newRow) { } } +function createProperty(propertyData, propertyElementID, propertyName, propertyID, elProperty) { + let property = { + data: propertyData, + elementID: propertyElementID, + name: propertyName, + elProperty: elProperty, + }; + let propertyType = propertyData.type; + + switch (propertyType) { + case 'string': { + property.elInput = createStringProperty(property, elProperty); + break; + } + case 'bool': { + property.elInput = createBoolProperty(property, elProperty); + break; + } + case 'number': { + property.elNumber = createNumberProperty(property, elProperty); + break; + } + case 'vec3': { + let elVec3 = createVec3Property(property, elProperty); + property.elNumberX = elVec3[VECTOR_ELEMENTS.X_NUMBER]; + property.elNumberY = elVec3[VECTOR_ELEMENTS.Y_NUMBER]; + property.elNumberZ = elVec3[VECTOR_ELEMENTS.Z_NUMBER]; + break; + } + case 'vec2': { + let elVec2 = createVec2Property(property, elProperty); + property.elNumberX = elVec2[VECTOR_ELEMENTS.X_NUMBER]; + property.elNumberY = elVec2[VECTOR_ELEMENTS.Y_NUMBER]; + break; + } + case 'color': { + let elColor = createColorProperty(property, elProperty); + property.elColorPicker = elColor[COLOR_ELEMENTS.COLOR_PICKER]; + property.elNumberR = elColor[COLOR_ELEMENTS.RED_NUMBER]; + property.elNumberG = elColor[COLOR_ELEMENTS.GREEN_NUMBER]; + property.elNumberB = elColor[COLOR_ELEMENTS.BLUE_NUMBER]; + break; + } + case 'dropdown': { + property.elInput = createDropdownProperty(property, propertyID, elProperty); + break; + } + case 'textarea': { + property.elInput = createTextareaProperty(property, elProperty); + break; + } + case 'icon': { + property.elSpan = createIconProperty(property, elProperty); + break; + } + case 'texture': { + let elTexture = createTextureProperty(property, elProperty); + property.elImage = elTexture[TEXTURE_ELEMENTS.IMAGE]; + property.elInput = elTexture[TEXTURE_ELEMENTS.TEXT_INPUT]; + break; + } + case 'buttons': { + property.elProperty = createButtonsProperty(property, elProperty); + break; + } + case 'placeholder': + case 'sub-header': { + break; + } + default: { + console.log("EntityProperties - Unknown property type " + + propertyType + " set to property " + propertyID); + break; + } + } + + return property; +} + /** * BUTTON CALLBACKS @@ -2717,104 +2742,6 @@ function showParentMaterialNameBox(number, elNumber, elString) { } } -function updateVisibleSpaceModeProperties() { - for (let propertyID in properties) { - if (properties.hasOwnProperty(propertyID)) { - let property = properties[propertyID]; - let propertySpaceMode = property.spaceMode; - if (propertySpaceMode !== PROPERTY_SPACE_MODE.ALL) { - showPropertyElement(propertyID, propertySpaceMode === currentSpaceMode); - } else { - showPropertyElement(propertyID, true); - } - } - } -} - -function createProperty(propertyData, propertyElementID, propertyName, propertyID, elProperty) { - let property = { - data: propertyData, - elementID: propertyElementID, - name: propertyName, - elProperty: elProperty, - }; - let propertyType = propertyData.type; - - switch (propertyType) { - case 'string': { - property.elInput = createStringProperty(property, elProperty); - break; - } - case 'bool': { - property.elInput = createBoolProperty(property, elProperty); - break; - } - case 'number': { - property.elInput = createNumberProperty(property, elProperty); - break; - } - case 'slider': { - let elSlider = createSliderProperty(property, elProperty); - property.elSlider = elSlider[SLIDER_ELEMENTS.SLIDER]; - property.elInput = elSlider[SLIDER_ELEMENTS.NUMBER_INPUT]; - break; - } - case 'vec3': { - let elVec3 = createVec3Property(property, elProperty); - property.elInputX = elVec3[VECTOR_ELEMENTS.X_INPUT]; - property.elInputY = elVec3[VECTOR_ELEMENTS.Y_INPUT]; - property.elInputZ = elVec3[VECTOR_ELEMENTS.Z_INPUT]; - break; - } - case 'vec2': { - let elVec2 = createVec2Property(property, elProperty); - property.elInputX = elVec2[VECTOR_ELEMENTS.X_INPUT]; - property.elInputY = elVec2[VECTOR_ELEMENTS.Y_INPUT]; - break; - } - case 'color': { - let elColor = createColorProperty(property, elProperty); - property.elColorPicker = elColor[COLOR_ELEMENTS.COLOR_PICKER]; - property.elInputR = elColor[COLOR_ELEMENTS.RED_INPUT]; - property.elInputG = elColor[COLOR_ELEMENTS.GREEN_INPUT]; - property.elInputB = elColor[COLOR_ELEMENTS.BLUE_INPUT]; - break; - } - case 'dropdown': { - property.elInput = createDropdownProperty(property, propertyID, elProperty); - break; - } - case 'textarea': { - property.elInput = createTextareaProperty(property, elProperty); - break; - } - case 'icon': { - property.elSpan = createIconProperty(property, elProperty); - break; - } - case 'texture': { - let elTexture = createTextureProperty(property, elProperty); - property.elImage = elTexture[TEXTURE_ELEMENTS.IMAGE]; - property.elInput = elTexture[TEXTURE_ELEMENTS.TEXT_INPUT]; - break; - } - case 'buttons': { - property.elProperty = createButtonsProperty(property, elProperty); - break; - } - case 'placeholder': - case 'sub-header': { - break; - } - default: { - console.log("EntityProperties - Unknown property type " + - propertyType + " set to property " + propertyID); - break; - } - } - - return property; -} function loaded() { openEventBridge(function() { @@ -2896,17 +2823,16 @@ function loaded() { if (propertyData.indentedLabel || propertyData.showPropertyRule !== undefined) { let elSpan = document.createElement('span'); elSpan.className = 'indented'; - elSpan.innerText = propertyData.label; + elSpan.innerText = propertyData.label !== undefined ? propertyData.label : ""; elLabel.appendChild(elSpan); } else { - elLabel.innerText = propertyData.label; + elLabel.innerText = propertyData.label !== undefined ? propertyData.label : ""; } elContainer.appendChild(elLabel); } else { elContainer = document.getElementById(propertyData.replaceID); } - if (elLabel) { createAppTooltip.registerTooltipElement(elLabel, propertyID); } @@ -3056,7 +2982,6 @@ function loaded() { let typeProperty = properties["type"]; typeProperty.elSpan.innerHTML = typeProperty.data.icons[type]; typeProperty.elSpan.style.display = "inline-block"; - typeProperty.elLabel.innerHTML = type + " (" + data.selections.length + ")"; disableProperties(); } else { @@ -3103,7 +3028,6 @@ function loaded() { let isPropertyNotNumber = false; switch (propertyData.type) { case 'number': - case 'slider': isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null; break; case 'vec3': @@ -3135,21 +3059,13 @@ function loaded() { } break; } - case 'number': - case 'slider': { + case 'number': { let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; let value = propertyValue / multiplier; if (propertyData.round !== undefined) { value = Math.round(value.round) / propertyData.round; } - if (propertyData.decimals !== undefined) { - property.elInput.value = value.toFixed(propertyData.decimals); - } else { - property.elInput.value = value; - } - if (property.elSlider !== undefined) { - property.elSlider.value = property.elInput.value; - } + property.elNumber.setValue(value); break; } case 'vec3': @@ -3164,16 +3080,16 @@ function loaded() { valueZ = Math.round(valueZ * propertyData.round) / propertyData.round; } if (propertyData.decimals !== undefined) { - property.elInputX.value = valueX.toFixed(propertyData.decimals); - property.elInputY.value = valueY.toFixed(propertyData.decimals); - if (property.elInputZ !== undefined) { - property.elInputZ.value = valueZ.toFixed(propertyData.decimals); + property.elNumberX.setValue(valueX.toFixed(propertyData.decimals)); + property.elNumberY.setValue(valueY.toFixed(propertyData.decimals)); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(valueZ.toFixed(propertyData.decimals)); } } else { - property.elInputX.value = valueX; - property.elInputY.value = valueY; - if (property.elInputZ !== undefined) { - property.elInputZ.value = valueZ; + property.elNumberX.setValue(valueX); + property.elNumberY.setValue(valueY); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(valueZ); } } break; @@ -3182,9 +3098,9 @@ function loaded() { property.elColorPicker.style.backgroundColor = "rgb(" + propertyValue.red + "," + propertyValue.green + "," + propertyValue.blue + ")"; - property.elInputR.value = propertyValue.red; - property.elInputG.value = propertyValue.green; - property.elInputB.value = propertyValue.blue; + property.elNumberR.setValue(propertyValue.red); + property.elNumberG.setValue(propertyValue.green); + property.elNumberB.setValue(propertyValue.blue); break; } case 'dropdown': { diff --git a/scripts/system/html/js/listView.js b/scripts/system/html/js/listView.js index 07aba53f1c..49a91388a5 100644 --- a/scripts/system/html/js/listView.js +++ b/scripts/system/html/js/listView.js @@ -13,8 +13,8 @@ debugPrint = function (message) { console.log(message); }; -function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction, - updateRowFunction, clearRowFunction, WINDOW_NONVARIABLE_HEIGHT) { +function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction, updateRowFunction, clearRowFunction, + preRefreshFunction, postRefreshFunction, preResizeFunction, WINDOW_NONVARIABLE_HEIGHT) { this.elTableBody = elTableBody; this.elTableScroll = elTableScroll; this.elTableHeaderRow = elTableHeaderRow; @@ -25,6 +25,9 @@ function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunctio this.createRowFunction = createRowFunction; this.updateRowFunction = updateRowFunction; this.clearRowFunction = clearRowFunction; + this.preRefreshFunction = preRefreshFunction; + this.postRefreshFunction = postRefreshFunction; + this.preResizeFunction = preResizeFunction; // the list of row elements created in the table up to max viewable height plus SCROLL_ROWS rows for scrolling buffer this.elRows = []; @@ -72,11 +75,6 @@ ListView.prototype = { } }, - onScroll: function() { - var that = this.listView; - that.scroll(); - }, - scroll: function() { let scrollTop = this.elTableScroll.scrollTop; let scrollHeight = this.getScrollHeight(); @@ -178,6 +176,7 @@ ListView.prototype = { }, refresh: function() { + this.preRefreshFunction(); // block refreshing before rows are initialized let numRows = this.getNumRows(); if (numRows === 0) { @@ -216,6 +215,7 @@ ListView.prototype = { this.lastRowShiftScrollTop = 0; } } + this.postRefreshFunction(); }, refreshBuffers: function() { @@ -235,7 +235,7 @@ ListView.prototype = { refreshRowOffset: function() { // make sure the row offset isn't causing visible rows to pass the end of the item list and is clamped to 0 - var numRows = this.getNumRows(); + let numRows = this.getNumRows(); if (this.rowOffset + numRows > this.itemData.length) { this.rowOffset = this.itemData.length - numRows; } @@ -244,18 +244,13 @@ ListView.prototype = { } }, - onResize: function() { - var that = this.listView; - that.resize(); - }, - resize: function() { if (!this.elTableBody || !this.elTableScroll) { debugPrint("ListView.resize - no valid table body or table scroll element"); return; } - - let prevScrollTop = this.elTableScroll.scrollTop; + this.preResizeFunction(); + let prevScrollTop = this.elTableScroll.scrollTop; // take up available window space this.elTableScroll.style.height = window.innerHeight - WINDOW_NONVARIABLE_HEIGHT; @@ -305,10 +300,10 @@ ListView.prototype = { this.elTableBody.appendChild(this.elBottomBuffer); this.elBottomBuffer.setAttribute("height", 0); - this.elTableScroll.listView = this; - this.elTableScroll.onscroll = this.onScroll; - window.listView = this; - window.onresize = this.onResize; + this.onScroll = this.scroll.bind(this); + this.elTableScroll.addEventListener("scroll", this.onScroll); + this.onResize = this.resize.bind(this); + window.addEventListener("resize", this.onResize); // initialize all row elements this.resize(); diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index b7c5809b3a..e4dc778985 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -197,7 +197,7 @@ var loadingBarProgress = Overlays.addOverlay("image3d", { name: "Loading-Bar-Progress", - localPosition: { x: 0.0, y: -0.86, z: 0.0 }, + localPosition: { x: 0.0, y: -0.91, z: 0.0 }, url: LOADING_BAR_PROGRESS, alpha: 1, dimensions: { x: TOTAL_LOADING_PROGRESS, y: 0.3}, @@ -274,6 +274,7 @@ previousCameraMode = Camera.mode; Camera.mode = "first person"; updateProgressBar(0.0); + scaleInterstitialPage(MyAvatar.sensorToWorldScale); timer = Script.setTimeout(update, 2000); } } @@ -482,7 +483,7 @@ var end = 0; var xLocalPosition = (progressPercentage * (end - start)) + start; var properties = { - localPosition: { x: xLocalPosition, y: -0.93, z: 0.0 }, + localPosition: { x: xLocalPosition, y: (HMD.active ? -0.93 : -0.91), z: 0.0 }, dimensions: { x: progress, y: 0.3 diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 8942c84f33..eeb16fd60d 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -17,7 +17,7 @@ var profileIndent = ''; const PROFILE_NOOP = function(_name, fn, args) { fn.apply(this, args); }; -PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) { +const PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) { console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin"); var previousIndent = profileIndent; profileIndent += ' '; diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 6ec9125ce8..ddf9a7b373 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -31,6 +31,6 @@ if (BUILD_TOOLS) add_subdirectory(oven) set_target_properties(oven PROPERTIES FOLDER "Tools") - add_subdirectory(auto-tester) - set_target_properties(auto-tester PROPERTIES FOLDER "Tools") + add_subdirectory(nitpick) + set_target_properties(nitpick PROPERTIES FOLDER "Tools") endif() diff --git a/tools/auto-tester/AppDataHighFidelity/Interface/AccountInfo.bin b/tools/auto-tester/AppDataHighFidelity/Interface/AccountInfo.bin deleted file mode 100644 index 65c971ea79..0000000000 Binary files a/tools/auto-tester/AppDataHighFidelity/Interface/AccountInfo.bin and /dev/null differ diff --git a/tools/auto-tester/AppDataHighFidelity/Interface.json b/tools/nitpick/AppDataHighFidelity/Interface.json similarity index 100% rename from tools/auto-tester/AppDataHighFidelity/Interface.json rename to tools/nitpick/AppDataHighFidelity/Interface.json diff --git a/tools/auto-tester/AppDataHighFidelity/Interface/avatarbookmarks.json b/tools/nitpick/AppDataHighFidelity/Interface/avatarbookmarks.json similarity index 100% rename from tools/auto-tester/AppDataHighFidelity/Interface/avatarbookmarks.json rename to tools/nitpick/AppDataHighFidelity/Interface/avatarbookmarks.json diff --git a/tools/auto-tester/AppDataHighFidelity/assignment-client/entities/models.json.gz b/tools/nitpick/AppDataHighFidelity/assignment-client/entities/models.json.gz similarity index 100% rename from tools/auto-tester/AppDataHighFidelity/assignment-client/entities/models.json.gz rename to tools/nitpick/AppDataHighFidelity/assignment-client/entities/models.json.gz diff --git a/tools/auto-tester/AppDataHighFidelity/domain-server/AccountInfo.bin b/tools/nitpick/AppDataHighFidelity/domain-server/AccountInfo.bin similarity index 100% rename from tools/auto-tester/AppDataHighFidelity/domain-server/AccountInfo.bin rename to tools/nitpick/AppDataHighFidelity/domain-server/AccountInfo.bin diff --git a/tools/auto-tester/AppDataHighFidelity/domain-server/config.json b/tools/nitpick/AppDataHighFidelity/domain-server/config.json similarity index 100% rename from tools/auto-tester/AppDataHighFidelity/domain-server/config.json rename to tools/nitpick/AppDataHighFidelity/domain-server/config.json diff --git a/tools/auto-tester/AppDataHighFidelity/domain-server/entities/models.json.gz b/tools/nitpick/AppDataHighFidelity/domain-server/entities/models.json.gz similarity index 100% rename from tools/auto-tester/AppDataHighFidelity/domain-server/entities/models.json.gz rename to tools/nitpick/AppDataHighFidelity/domain-server/entities/models.json.gz diff --git a/tools/auto-tester/CMakeLists.txt b/tools/nitpick/CMakeLists.txt similarity index 95% rename from tools/auto-tester/CMakeLists.txt rename to tools/nitpick/CMakeLists.txt index c8c0a336d2..543b9c9b47 100644 --- a/tools/auto-tester/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -1,4 +1,4 @@ -set (TARGET_NAME auto-tester) +set (TARGET_NAME nitpick) project(${TARGET_NAME}) # Automatically run UIC and MOC. This replaces the older WRAP macros @@ -22,7 +22,7 @@ set (QT_LIBRARIES Qt5::Core Qt5::Widgets QT::Gui Qt5::Xml) if (WIN32) # Do not show Console - set_property (TARGET auto-tester PROPERTY WIN32_EXECUTABLE true) + set_property (TARGET nitpick PROPERTY WIN32_EXECUTABLE true) endif() target_zlib() diff --git a/tools/auto-tester/Create.PNG b/tools/nitpick/Create.PNG similarity index 100% rename from tools/auto-tester/Create.PNG rename to tools/nitpick/Create.PNG diff --git a/tools/auto-tester/Evaluate.PNG b/tools/nitpick/Evaluate.PNG similarity index 100% rename from tools/auto-tester/Evaluate.PNG rename to tools/nitpick/Evaluate.PNG diff --git a/tools/auto-tester/README.md b/tools/nitpick/README.md similarity index 86% rename from tools/auto-tester/README.md rename to tools/nitpick/README.md index e029955edc..e8129ae29e 100644 --- a/tools/auto-tester/README.md +++ b/tools/nitpick/README.md @@ -1,11 +1,11 @@ -# Auto Tester +# nitpick -The auto-tester is a stand alone application that provides a mechanism for regression testing. The general idea is simple: +Nitpick is a stand alone application that provides a mechanism for regression testing. The general idea is simple: * Each test folder has a script that produces a set of snapshots. * The snapshots are compared to a 'canonical' set of images that have been produced beforehand. * The result, if any test failed, is a zipped folder describing the failure. -Auto-tester has 5 functions, separated into 4 tabs: +Nitpick has 5 functions, separated into 4 tabs: 1. Creating tests, MD files and recursive scripts 1. Windows task bar utility (Windows only) 1. Running tests @@ -14,19 +14,25 @@ Auto-tester has 5 functions, separated into 4 tabs: ## Installation ### Executable -1. Download the installer by browsing to [here](). +1. On Windows: download the installer by browsing to [here](). 2. Double click on the installer and install to a convenient location ![](./setup_7z.PNG) -3. To run the auto-tester, double click **auto-tester.exe**. +3. To run nitpick, double click **nitpick.exe**. ### Python -The TestRail interface requires Python 3 to be installed. Auto-Tester has been tested with Python 3.7.0 but should work with newer versions. +The TestRail interface requires Python 3 to be installed. Nitpick has been tested with Python 3.7.0 but should work with newer versions. Python 3 can be downloaded from: 1. Windows installer 2. Linux (source) (**Gzipped source tarball**) 3. Mac (**macOS 64-bit/32-bit installer** or **macOS 64-bit/32-bit installer**) +#### Windows After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable. + +#### Mac +After installation - run `open "/Applications/Python 3.6/Install Certificates.command"`. This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates. +Verify that `/usr/local/bin/python3` exists. + ### AWS interface #### Windows 1. Download the AWS CLI from `https://aws.amazon.com/cli/` @@ -35,9 +41,23 @@ After installation - create an environment variable called PYTHON_PATH and set i 1. Enter the AWS account number 1. Enter the secret key 1. Leave region name and ouput format as default [None] +1. Install the latest release of Boto3 via pip: +pip install boto3 +#### Mac +1. Install pip with the script provided by the Python Packaging Authority: +$ curl -O https://bootstrap.pypa.io/get-pip.py +$ python3 get-pip.py --user + +1. Use pip to install the AWS CLI. +$ pip3 install awscli --upgrade --user +This will install aws in your user. For user XXX, aws will be located in /Users/XXX/Library/Python/3.7/bin +1. Open a new command prompt and run `aws configure` +1. Enter the AWS account number +1. Enter the secret key +1. Leave region name and ouput format as default [None] +1. Install the latest release of Boto3 via pip: +pip3 install boto3 -1. Install the latest release of Boto3 via pip: ->pip install boto3 # Create ![](./Create.PNG) @@ -57,7 +77,7 @@ This function creates an MD file in the (user-selected) tests root folder. The This function creates a file named `test.md` from a `test.js` script. The user will be asked for the folder containing the test script: ### Details The process to produce the MD file is a simplistic parse of the test script. -- The string in the `autoTester.perform(...)` function call will be the title of the file +- The string in the `nitpick.perform(...)` function call will be the title of the file - Instructions to run the script are then provided: @@ -89,26 +109,26 @@ The various scripts are called in alphabetical order. An example of a recursive script is as follows: ``` -// This is an automatically generated file, created by auto-tester on Jul 5 2018, 10:19 +// This is an automatically generated file, created by nitpick on Jul 5 2018, 10:19 PATH_TO_THE_REPO_PATH_UTILS_FILE = "https://raw.githubusercontent.com/highfidelity/hifi_tests/master/tests/utils/branchUtils.js"; Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE); -var autoTester = createAutoTester(Script.resolvePath(".")); +var nitpick = createNitpick(Script.resolvePath(".")); -var testsRootPath = autoTester.getTestsRootPath(); +var testsRootPath = nitpick.getTestsRootPath(); if (typeof Test !== 'undefined') { Test.wait(10000); }; -autoTester.enableRecursive(); -autoTester.enableAuto(); +nitpick.enableRecursive(); +nitpick.enableAuto(); Script.include(testsRootPath + "content/overlay/layer/drawInFront/shape/test.js"); Script.include(testsRootPath + "content/overlay/layer/drawInFront/model/test.js"); Script.include(testsRootPath + "content/overlay/layer/drawHUDLayer/test.js"); -autoTester.runRecursive(); +nitpick.runRecursive(); ``` ## Create all Recursive Scripts ### Usage @@ -165,7 +185,7 @@ Evaluation proceeds in a number of steps: 1. The images are then pair-wise compared, using the SSIM algorithm. A fixed threshold is used to define a mismatch. 1. In interactive mode - a window is opened showing the expected image, actual image, difference image and error: -![](./autoTesterMismatchExample.PNG) +![](./nitpickMismatchExample.PNG) 1. If not in interactive mode, or the user has defined the results as an error, an error is written into the error folder. The error itself is a folder with the 3 images and a small text file containing details. diff --git a/tools/auto-tester/Run.PNG b/tools/nitpick/Run.PNG similarity index 100% rename from tools/auto-tester/Run.PNG rename to tools/nitpick/Run.PNG diff --git a/tools/auto-tester/TestRailSelector.PNG b/tools/nitpick/TestRailSelector.PNG similarity index 100% rename from tools/auto-tester/TestRailSelector.PNG rename to tools/nitpick/TestRailSelector.PNG diff --git a/tools/auto-tester/Web Interface.PNG b/tools/nitpick/Web Interface.PNG similarity index 100% rename from tools/auto-tester/Web Interface.PNG rename to tools/nitpick/Web Interface.PNG diff --git a/tools/auto-tester/WebInterface.PNG b/tools/nitpick/WebInterface.PNG similarity index 100% rename from tools/auto-tester/WebInterface.PNG rename to tools/nitpick/WebInterface.PNG diff --git a/tools/auto-tester/Windows.PNG b/tools/nitpick/Windows.PNG similarity index 100% rename from tools/auto-tester/Windows.PNG rename to tools/nitpick/Windows.PNG diff --git a/tools/auto-tester/autoTesterMismatchExample.PNG b/tools/nitpick/nitpickMismatchExample.PNG similarity index 100% rename from tools/auto-tester/autoTesterMismatchExample.PNG rename to tools/nitpick/nitpickMismatchExample.PNG diff --git a/tools/auto-tester/setup_7z.PNG b/tools/nitpick/setup_7z.PNG similarity index 100% rename from tools/auto-tester/setup_7z.PNG rename to tools/nitpick/setup_7z.PNG diff --git a/tools/auto-tester/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp similarity index 89% rename from tools/auto-tester/src/AWSInterface.cpp rename to tools/nitpick/src/AWSInterface.cpp index 628db5329c..e43ef8dc75 100644 --- a/tools/auto-tester/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -22,11 +22,11 @@ AWSInterface::AWSInterface(QObject* parent) : QObject(parent) { } void AWSInterface::createWebPageFromResults(const QString& testResults, - const QString& workingDirectory, + const QString& snapshotDirectory, QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { _testResults = testResults; - _workingDirectory = workingDirectory; + _snapshotDirectory = snapshotDirectory; _urlLineEdit = urlLineEdit; _urlLineEdit->setEnabled(false); @@ -42,16 +42,15 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, void AWSInterface::extractTestFailuresFromZippedFolder() { // For a test results zip file called `D:/tt/TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ].zip` // the folder will be called `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]` - // and, this folder will be in the workign directory + // and, this folder will be in the working directory QStringList parts =_testResults.split('/'); - QString zipFolderName = _workingDirectory + "/" + parts[parts.length() - 1].split('.')[0]; + QString zipFolderName = _snapshotDirectory + "/" + parts[parts.length() - 1].split('.')[0]; if (QDir(zipFolderName).exists()) { QDir dir = zipFolderName; dir.removeRecursively(); } - QDir().mkdir(_workingDirectory); - JlCompress::extractDir(_testResults, _workingDirectory); + JlCompress::extractDir(_testResults, _snapshotDirectory); } void AWSInterface::createHTMLFile() { @@ -61,7 +60,7 @@ void AWSInterface::createHTMLFile() { QString filename = pathComponents[pathComponents.length() - 1]; _resultsFolder = filename.left(filename.length() - 4); - QString resultsPath = _workingDirectory + "/" + _resultsFolder + "/"; + QString resultsPath = _snapshotDirectory + "/" + _resultsFolder + "/"; QDir().mkdir(resultsPath); _htmlFilename = resultsPath + HTML_FILENAME; @@ -157,7 +156,7 @@ void AWSInterface::writeTable(QTextStream& stream) { // Note that failures are processed first, then successes QStringList originalNamesFailures; QStringList originalNamesSuccesses; - QDirIterator it1(_workingDirectory.toStdString().c_str()); + QDirIterator it1(_snapshotDirectory.toStdString().c_str()); while (it1.hasNext()) { QString nextDirectory = it1.next(); @@ -191,10 +190,10 @@ void AWSInterface::writeTable(QTextStream& stream) { newNamesSuccesses.append(originalNamesSuccesses[i].split("--tests.")[1]); } - _htmlFailuresFolder = _workingDirectory + "/" + _resultsFolder + "/" + FAILURES_FOLDER; + _htmlFailuresFolder = _snapshotDirectory + "/" + _resultsFolder + "/" + FAILURES_FOLDER; QDir().mkdir(_htmlFailuresFolder); - _htmlSuccessesFolder = _workingDirectory + "/" + _resultsFolder + "/" + SUCCESSES_FOLDER; + _htmlSuccessesFolder = _snapshotDirectory + "/" + _resultsFolder + "/" + SUCCESSES_FOLDER; QDir().mkdir(_htmlSuccessesFolder); for (int i = 0; i < newNamesFailures.length(); ++i) { @@ -321,7 +320,7 @@ void AWSInterface::createEntry(int index, const QString& testResult, QTextStream } void AWSInterface::updateAWS() { - QString filename = _workingDirectory + "/updateAWS.py"; + QString filename = _snapshotDirectory + "/updateAWS.py"; if (QFile::exists(filename)) { QFile::remove(filename); } @@ -352,14 +351,23 @@ void AWSInterface::updateAWS() { QStringList parts = nextDirectory.split('/'); QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1]; - stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Actual Image.png" << "', 'rb')\n"; + stream << "data = open('" << _snapshotDirectory << "/" << filename << "/" + << "Actual Image.png" + << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n"; - stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Expected Image.png" << "', 'rb')\n"; + stream << "data = open('" << _snapshotDirectory << "/" << filename << "/" + << "Expected Image.png" + << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { - stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Difference Image.png" << "', 'rb')\n"; + stream << "data = open('" << _snapshotDirectory << "/" << filename << "/" + << "Difference Image.png" + << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; } } @@ -378,19 +386,28 @@ void AWSInterface::updateAWS() { QStringList parts = nextDirectory.split('/'); QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1]; - stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Actual Image.png" << "', 'rb')\n"; + stream << "data = open('" << _snapshotDirectory << "/" << filename << "/" + << "Actual Image.png" + << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n"; - stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Expected Image.png" << "', 'rb')\n"; + stream << "data = open('" << _snapshotDirectory << "/" << filename << "/" + << "Expected Image.png" + << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { - stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Difference Image.png" << "', 'rb')\n"; + stream << "data = open('" << _snapshotDirectory << "/" << filename << "/" + << "Difference Image.png" + << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; } } - stream << "data = open('" << _workingDirectory << "/" << _resultsFolder << "/" << HTML_FILENAME << "', 'rb')\n"; + stream << "data = open('" << _snapshotDirectory << "/" << _resultsFolder << "/" << HTML_FILENAME << "', 'rb')\n"; stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << _resultsFolder << "/" << HTML_FILENAME << "', Body=data, ContentType='text/html')\n"; @@ -408,6 +425,11 @@ void AWSInterface::updateAWS() { connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); +#ifdef Q_OS_WIN QStringList parameters = QStringList() << filename ; process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename; + process->start("sh", parameters); +#endif } diff --git a/tools/auto-tester/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h similarity index 94% rename from tools/auto-tester/src/AWSInterface.h rename to tools/nitpick/src/AWSInterface.h index c5be5f35bb..f4084f1a14 100644 --- a/tools/auto-tester/src/AWSInterface.h +++ b/tools/nitpick/src/AWSInterface.h @@ -26,7 +26,7 @@ public: explicit AWSInterface(QObject* parent = 0); void createWebPageFromResults(const QString& testResults, - const QString& workingDirectory, + const QString& snapshotDirectory, QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit); @@ -49,7 +49,7 @@ public: private: QString _testResults; - QString _workingDirectory; + QString _snapshotDirectory; QString _resultsFolder; QString _htmlFailuresFolder; QString _htmlSuccessesFolder; diff --git a/tools/auto-tester/src/Downloader.cpp b/tools/nitpick/src/Downloader.cpp similarity index 92% rename from tools/auto-tester/src/Downloader.cpp rename to tools/nitpick/src/Downloader.cpp index cb9863f34d..de768398b0 100644 --- a/tools/auto-tester/src/Downloader.cpp +++ b/tools/nitpick/src/Downloader.cpp @@ -17,8 +17,7 @@ Downloader::Downloader(QUrl fileURL, QObject *parent) : QObject(parent) { this, SLOT (fileDownloaded(QNetworkReply*)) ); - QNetworkRequest request(fileURL); - _networkAccessManager.get(request); + _networkAccessManager.get(QNetworkRequest(fileURL)); } void Downloader::fileDownloaded(QNetworkReply* reply) { @@ -37,4 +36,4 @@ void Downloader::fileDownloaded(QNetworkReply* reply) { QByteArray Downloader::downloadedData() const { return _downloadedData; -} \ No newline at end of file +} diff --git a/tools/auto-tester/src/Downloader.h b/tools/nitpick/src/Downloader.h similarity index 98% rename from tools/auto-tester/src/Downloader.h rename to tools/nitpick/src/Downloader.h index 6d1029698f..742a88b890 100644 --- a/tools/auto-tester/src/Downloader.h +++ b/tools/nitpick/src/Downloader.h @@ -37,7 +37,7 @@ public: signals: void downloaded(); - private slots: +private slots: void fileDownloaded(QNetworkReply* pReply); private: diff --git a/tools/auto-tester/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp similarity index 100% rename from tools/auto-tester/src/ImageComparer.cpp rename to tools/nitpick/src/ImageComparer.cpp diff --git a/tools/auto-tester/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h similarity index 100% rename from tools/auto-tester/src/ImageComparer.h rename to tools/nitpick/src/ImageComparer.h diff --git a/tools/auto-tester/src/PythonInterface.cpp b/tools/nitpick/src/PythonInterface.cpp similarity index 76% rename from tools/auto-tester/src/PythonInterface.cpp rename to tools/nitpick/src/PythonInterface.cpp index 4922b8a8df..9832ac9f8d 100644 --- a/tools/auto-tester/src/PythonInterface.cpp +++ b/tools/nitpick/src/PythonInterface.cpp @@ -14,17 +14,28 @@ #include PythonInterface::PythonInterface() { +#ifdef Q_OS_WIN if (QProcessEnvironment::systemEnvironment().contains("PYTHON_PATH")) { QString _pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH"); if (!QFile::exists(_pythonPath + "/" + _pythonExe)) { QMessageBox::critical(0, _pythonExe, QString("Python executable not found in ") + _pythonPath); + exit(-1); } + _pythonCommand = _pythonPath + "/" + _pythonExe; } else { QMessageBox::critical(0, "PYTHON_PATH not defined", "Please set PYTHON_PATH to directory containing the Python executable"); exit(-1); } +#elif defined Q_OS_MAC + _pythonCommand = "/usr/local/bin/python3"; + if (!QFile::exists(_pythonCommand)) { + QMessageBox::critical(0, "PYTHON_PATH not defined", + "python3 not found at " + _pythonCommand); + exit(-1); + } +#endif } QString PythonInterface::getPythonCommand() { diff --git a/tools/auto-tester/src/PythonInterface.h b/tools/nitpick/src/PythonInterface.h similarity index 83% rename from tools/auto-tester/src/PythonInterface.h rename to tools/nitpick/src/PythonInterface.h index f32a39a644..947b359037 100644 --- a/tools/auto-tester/src/PythonInterface.h +++ b/tools/nitpick/src/PythonInterface.h @@ -19,7 +19,13 @@ public: QString getPythonCommand(); private: +#ifdef Q_OS_WIN const QString _pythonExe{ "python.exe" }; +#else + // Both Mac and Linux use "python" + const QString _pythonExe{ "python" }; +#endif + QString _pythonCommand; }; diff --git a/tools/auto-tester/src/Test.cpp b/tools/nitpick/src/Test.cpp similarity index 93% rename from tools/auto-tester/src/Test.cpp rename to tools/nitpick/src/Test.cpp index 582f6209af..47458d00ee 100644 --- a/tools/auto-tester/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -19,8 +19,8 @@ #include #include -#include "ui/AutoTester.h" -extern AutoTester* autoTester; +#include "ui/Nitpick.h" +extern Nitpick* nitpick; #include @@ -30,9 +30,9 @@ Test::Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) { _mismatchWindow.setModal(true); - if (autoTester) { - autoTester->setUserText(GIT_HUB_DEFAULT_USER); - autoTester->setBranchText(GIT_HUB_DEFAULT_BRANCH); + if (nitpick) { + nitpick->setUserText(GIT_HUB_DEFAULT_USER); + nitpick->setBranchText(GIT_HUB_DEFAULT_BRANCH); } } @@ -167,7 +167,7 @@ void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestRe // Create text file describing the failure QTextStream stream(&descriptionFile); - stream << "Test failed in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' + stream << "Test in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' stream << "Expected image was " << testResult._expectedImageFilename << endl; stream << "Actual image was " << testResult._actualImageFilename << endl; stream << "Similarity index was " << testResult._error << endl; @@ -240,8 +240,8 @@ void Test::startTestsEvaluation(const bool isRunningFromCommandLine, _expectedImagesFilenames.clear(); _expectedImagesFullFilenames.clear(); - QString branch = (branchFromCommandLine.isNull()) ? autoTester->getSelectedBranch() : branchFromCommandLine; - QString user = (userFromCommandLine.isNull()) ? autoTester->getSelectedUser() : userFromCommandLine; + QString branch = (branchFromCommandLine.isNull()) ? nitpick->getSelectedBranch() : branchFromCommandLine; + QString user = (userFromCommandLine.isNull()) ? nitpick->getSelectedUser() : userFromCommandLine; foreach(QString currentFilename, sortedTestResultsFilenames) { QString fullCurrentFilename = _snapshotDirectory + "/" + currentFilename; @@ -268,7 +268,7 @@ void Test::startTestsEvaluation(const bool isRunningFromCommandLine, } } - autoTester->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this); + nitpick->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this); } void Test::finishTestsEvaluation() { int numberOfFailures = compareImageLists(); @@ -288,7 +288,7 @@ void Test::finishTestsEvaluation() { } if (_isRunningInAutomaticTestRun) { - autoTester->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); + nitpick->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); } } @@ -429,22 +429,22 @@ ExtractedText Test::getTestScriptLines(QString testFileName) { QString line = stream.readLine(); // Name of test is the string in the following line: - // autoTester.perform("Apply Material Entities to Avatars", Script.resolvePath("."), function(testType) {... + // nitpick.perform("Apply Material Entities to Avatars", Script.resolvePath("."), function(testType) {... const QString ws("\\h*"); //white-space character - const QString functionPerformName(ws + "autoTester" + ws + "\\." + ws + "perform"); + const QString functionPerformName(ws + "nitpick" + ws + "\\." + ws + "perform"); const QString quotedString("\\\".+\\\""); QString regexTestTitle(ws + functionPerformName + "\\(" + quotedString); QRegularExpression lineContainingTitle = QRegularExpression(regexTestTitle); // Each step is either of the following forms: - // autoTester.addStepSnapshot("Take snapshot"... - // autoTester.addStep("Clean up after test"... - const QString functionAddStepSnapshotName(ws + "autoTester" + ws + "\\." + ws + "addStepSnapshot"); + // nitpick.addStepSnapshot("Take snapshot"... + // nitpick.addStep("Clean up after test"... + const QString functionAddStepSnapshotName(ws + "nitpick" + ws + "\\." + ws + "addStepSnapshot"); const QString regexStepSnapshot(ws + functionAddStepSnapshotName + ws + "\\(" + ws + quotedString + ".*"); const QRegularExpression lineStepSnapshot = QRegularExpression(regexStepSnapshot); - const QString functionAddStepName(ws + "autoTester" + ws + "\\." + ws + "addStep"); + const QString functionAddStepName(ws + "nitpick" + ws + "\\." + ws + "addStep"); const QString regexStep(ws + functionAddStepName + ws + "\\(" + ws + quotedString + ".*"); const QRegularExpression lineStep = QRegularExpression(regexStep); @@ -620,7 +620,7 @@ void Test::createTestAutoScript() { } if (createTestAutoScript(_testDirectory)) { - QMessageBox::information(0, "Success", "'autoTester.js` script has been created"); + QMessageBox::information(0, "Success", "'nitpick.js` script has been created"); } } @@ -653,7 +653,7 @@ void Test::createAllTestAutoScripts() { } } - QMessageBox::information(0, "Success", "'autoTester.js' scripts have been created"); + QMessageBox::information(0, "Success", "'nitpick.js' scripts have been created"); } bool Test::createTestAutoScript(const QString& directory) { @@ -677,8 +677,8 @@ bool Test::createTestAutoScript(const QString& directory) { stream << "if (typeof PATH_TO_THE_REPO_PATH_UTILS_FILE === 'undefined') PATH_TO_THE_REPO_PATH_UTILS_FILE = 'https://raw.githubusercontent.com/highfidelity/hifi_tests/master/tests/utils/branchUtils.js';\n"; stream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);\n"; - stream << "var autoTester = createAutoTester(Script.resolvePath('.'));\n\n"; - stream << "autoTester.enableAuto();\n\n"; + stream << "var nitpick = createAutoTester(Script.resolvePath('.'));\n\n"; + stream << "nitpick.enableAuto();\n\n"; stream << "Script.include('./test.js?raw=true');\n"; testAutoScriptFile.close(); @@ -751,29 +751,29 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact textStream << "// This is an automatically generated file, created by auto-tester" << endl; // Include 'autoTest.js' - QString branch = autoTester->getSelectedBranch(); - QString user = autoTester->getSelectedUser(); + QString branch = nitpick->getSelectedBranch(); + QString user = nitpick->getSelectedUser(); textStream << "PATH_TO_THE_REPO_PATH_UTILS_FILE = \"https://raw.githubusercontent.com/" + user + "/hifi_tests/" + branch + "/tests/utils/branchUtils.js\";" << endl; textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl; - textStream << "var autoTester = createAutoTester(Script.resolvePath(\".\"));" << endl << endl; + textStream << "var nitpick = createAutoTester(Script.resolvePath(\".\"));" << endl << endl; - textStream << "var testsRootPath = autoTester.getTestsRootPath();" << endl << endl; + textStream << "var testsRootPath = nitpick.getTestsRootPath();" << endl << endl; // Wait 10 seconds before starting textStream << "if (typeof Test !== 'undefined') {" << endl; textStream << " Test.wait(10000);" << endl; textStream << "};" << endl << endl; - textStream << "autoTester.enableRecursive();" << endl; - textStream << "autoTester.enableAuto();" << endl << endl; + textStream << "nitpick.enableRecursive();" << endl; + textStream << "nitpick.enableAuto();" << endl << endl; // This is used to verify that the recursive test contains at least one test bool testFound{ false }; - // Directories are included in reverse order. The autoTester scripts use a stack mechanism, + // Directories are included in reverse order. The nitpick scripts use a stack mechanism, // so this ensures that the tests run in alphabetical order (a convenience when debugging) QStringList directories; @@ -819,7 +819,7 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact } textStream << endl; - textStream << "autoTester.runRecursive();" << endl; + textStream << "nitpick.runRecursive();" << endl; allTestsFilename.close(); } @@ -881,7 +881,7 @@ void Test::createTestsOutline() { // The directory name appears after the last slash (we are assured there is at least 1). QString directoryName = directory.right(directory.length() - directory.lastIndexOf("/") - 1); - // autoTester is run on a clone of the repository. We use relative paths, so we can use both local disk and GitHub + // nitpick is run on a clone of the repository. We use relative paths, so we can use both local disk and GitHub // For a test in "D:/GitHub/hifi_tests/tests/content/entity/zone/ambientLightInheritance" the // GitHub URL is "./content/entity/zone/ambientLightInheritance?raw=true" QString partialPath = directory.right(directory.length() - (directory.lastIndexOf("/tests/") + QString("/tests").length() + 1)); @@ -937,11 +937,11 @@ void Test::createTestRailTestCases() { } if (_testRailCreateMode == PYTHON) { - _testRailInterface.createTestSuitePython(_testDirectory, outputDirectory, autoTester->getSelectedUser(), - autoTester->getSelectedBranch()); + _testRailInterface.createTestSuitePython(_testDirectory, outputDirectory, nitpick->getSelectedUser(), + nitpick->getSelectedBranch()); } else { - _testRailInterface.createTestSuiteXML(_testDirectory, outputDirectory, autoTester->getSelectedUser(), - autoTester->getSelectedBranch()); + _testRailInterface.createTestSuiteXML(_testDirectory, outputDirectory, nitpick->getSelectedUser(), + nitpick->getSelectedBranch()); } } @@ -1052,11 +1052,11 @@ void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { return; } - QString tempDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store temporary files in", + QString snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store temporary files in", nullptr, QFileDialog::ShowDirsOnly); - if (tempDirectory.isNull()) { + if (snapshotDirectory.isNull()) { return; } - _awsInterface.createWebPageFromResults(testResults, tempDirectory, updateAWSCheckBox, urlLineEdit); + _awsInterface.createWebPageFromResults(testResults, snapshotDirectory, updateAWSCheckBox, urlLineEdit); } \ No newline at end of file diff --git a/tools/auto-tester/src/Test.h b/tools/nitpick/src/Test.h similarity index 98% rename from tools/auto-tester/src/Test.h rename to tools/nitpick/src/Test.h index f653a91782..a79252b92a 100644 --- a/tools/auto-tester/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -146,7 +146,7 @@ private: const QString DATETIME_FORMAT{ "yyyy-MM-dd_hh-mm-ss" }; - // NOTE: these need to match the appropriate var's in autoTester.js + // NOTE: these need to match the appropriate var's in nitpick.js // var advanceKey = "n"; // var pathSeparator = "."; const QString ADVANCE_KEY{ "n" }; diff --git a/tools/auto-tester/src/TestRailInterface.cpp b/tools/nitpick/src/TestRailInterface.cpp similarity index 97% rename from tools/auto-tester/src/TestRailInterface.cpp rename to tools/nitpick/src/TestRailInterface.cpp index f943935539..a0c0d74526 100644 --- a/tools/auto-tester/src/TestRailInterface.cpp +++ b/tools/nitpick/src/TestRailInterface.cpp @@ -357,8 +357,13 @@ void TestRailInterface::createAddTestCasesPythonScript(const QString& testDirect connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); +#ifdef Q_OS_WIN QStringList parameters = QStringList() << _outputDirectory + "/addTestCases.py"; process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + _outputDirectory + "/addTestCases.py"; + process->start("sh", parameters); +#endif } } @@ -482,8 +487,13 @@ void TestRailInterface::addRun() { connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); +#ifdef Q_OS_WIN QStringList parameters = QStringList() << _outputDirectory + "/addRun.py"; process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + _outputDirectory + "/addRun.py"; + process->start("sh", parameters); +#endif } } @@ -586,8 +596,13 @@ void TestRailInterface::updateRunWithResults() { connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); +#ifdef Q_OS_WIN QStringList parameters = QStringList() << _outputDirectory + "/updateRunWithResults.py"; process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + _outputDirectory + "/updateRunWithResults.py"; + process->start("sh", parameters); +#endif } } @@ -759,8 +774,13 @@ void TestRailInterface::getReleasesFromTestRail() { [=](int exitCode, QProcess::ExitStatus exitStatus) { updateReleasesComboData(exitCode, exitStatus); }); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); +#ifdef Q_OS_WIN QStringList parameters = QStringList() << filename; process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename; + process->start("sh", parameters); +#endif } void TestRailInterface::createTestSuitePython(const QString& testDirectory, @@ -1078,8 +1098,13 @@ void TestRailInterface::getTestSectionsFromTestRail() { [=](int exitCode, QProcess::ExitStatus exitStatus) { updateSectionsComboData(exitCode, exitStatus); }); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); +#ifdef Q_OS_WIN QStringList parameters = QStringList() << filename; process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename; + process->start("sh", parameters); +#endif } void TestRailInterface::getRunsFromTestRail() { @@ -1117,9 +1142,13 @@ void TestRailInterface::getRunsFromTestRail() { [=](int exitCode, QProcess::ExitStatus exitStatus) { updateRunsComboData(exitCode, exitStatus); }); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); +#ifdef Q_OS_WIN QStringList parameters = QStringList() << filename; - process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename; + process->start("sh", parameters); +#endif } void TestRailInterface::createTestRailRun(const QString& outputDirectory) { @@ -1164,4 +1193,4 @@ void TestRailInterface::extractTestFailuresFromZippedFolder(const QString& testR QDir dir = tempSubDirectory; dir.mkdir(tempSubDirectory); JlCompress::extractDir(testResults, tempSubDirectory); -} \ No newline at end of file +} diff --git a/tools/auto-tester/src/TestRailInterface.h b/tools/nitpick/src/TestRailInterface.h similarity index 100% rename from tools/auto-tester/src/TestRailInterface.h rename to tools/nitpick/src/TestRailInterface.h diff --git a/tools/auto-tester/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp similarity index 61% rename from tools/auto-tester/src/TestRunner.cpp rename to tools/nitpick/src/TestRunner.cpp index 01ec04f254..c7823ba751 100644 --- a/tools/auto-tester/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -13,14 +13,17 @@ #include #include -#include "ui/AutoTester.h" -extern AutoTester* autoTester; +#include "ui/Nitpick.h" +extern Nitpick* nitpick; #ifdef Q_OS_WIN #include #include #endif +// TODO: for debug +#include + TestRunner::TestRunner(std::vector dayCheckboxes, std::vector timeEditCheckboxes, std::vector timeEdits, @@ -40,27 +43,29 @@ TestRunner::TestRunner(std::vector dayCheckboxes, _url = url; _runNow = runNow; - installerThread = new QThread(); - installerWorker = new Worker(); - installerWorker->moveToThread(installerThread); - installerThread->start(); - connect(this, SIGNAL(startInstaller()), installerWorker, SLOT(runCommand())); - connect(installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete())); + _installerThread = new QThread(); + _installerWorker = new Worker(); + + _installerWorker->moveToThread(_installerThread); + _installerThread->start(); + connect(this, SIGNAL(startInstaller()), _installerWorker, SLOT(runCommand())); + connect(_installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete())); - interfaceThread = new QThread(); - interfaceWorker = new Worker(); - interfaceThread->start(); - interfaceWorker->moveToThread(interfaceThread); - connect(this, SIGNAL(startInterface()), interfaceWorker, SLOT(runCommand())); - connect(interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete())); + _interfaceThread = new QThread(); + _interfaceWorker = new Worker(); + + _interfaceThread->start(); + _interfaceWorker->moveToThread(_interfaceThread); + connect(this, SIGNAL(startInterface()), _interfaceWorker, SLOT(runCommand())); + connect(_interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete())); } TestRunner::~TestRunner() { - delete installerThread; - delete interfaceThread; + delete _installerThread; + delete _installerWorker; - delete interfaceThread; - delete interfaceWorker; + delete _interfaceThread; + delete _interfaceWorker; if (_timer) { delete _timer; @@ -84,15 +89,96 @@ void TestRunner::setWorkingFolder() { return; } +#ifdef Q_OS_WIN _installationFolder = _workingFolder + "/High Fidelity"; +#elif defined Q_OS_MAC + _installationFolder = _workingFolder + "/High_Fidelity"; +#endif + _logFile.setFileName(_workingFolder + "/log.txt"); - autoTester->enableRunTabControls(); + nitpick->enableRunTabControls(); _workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder)); _timer = new QTimer(this); connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime())); _timer->start(30 * 1000); //time specified in ms + +#ifdef Q_OS_MAC + // Create MAC shell scripts + QFile script; + + // This script waits for a process to start + script.setFileName(_workingFolder + "/waitForStart.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'waitForStart.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("PROCESS=\"$1\"\n"); + script.write("until (pgrep -x $PROCESS >nul)\n"); + script.write("do\n"); + script.write("\techo waiting for \"$1\" to start\n"); + script.write("\tsleep 2\n"); + script.write("done\n"); + script.write("echo \"$1\" \"started\"\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + // The Mac shell command returns immediately. This little script waits for a process to finish + script.setFileName(_workingFolder + "/waitForFinish.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'waitForFinish.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("PROCESS=\"$1\"\n"); + script.write("while (pgrep -x $PROCESS >nul)\n"); + script.write("do\n"); + script.write("\techo waiting for \"$1\" to finish\n"); + script.write("\tsleep 2\n"); + script.write("done\n"); + script.write("echo \"$1\" \"finished\"\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + // Create an AppleScript to resize Interface. This is needed so that snapshots taken + // with the primary camera will be the correct size. + // This will be run from a normal shell script + script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'setInterfaceSizeAndPosition.scpt'"); + exit(-1); + } + + script.write("set width to 960\n"); + script.write("set height to 540\n"); + script.write("set x to 100\n"); + script.write("set y to 100\n\n"); + script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n"); + + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'setInterfaceSizeAndPosition.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("echo resizing interface\n"); + script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str()); + script.write("echo resize complete\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); +#endif } void TestRunner::run() { @@ -102,8 +188,8 @@ void TestRunner::run() { _automatedTestIsRunning = true; // Initial setup - _branch = autoTester->getSelectedBranch(); - _user = autoTester->getSelectedUser(); + _branch = nitpick->getSelectedBranch(); + _user = nitpick->getSelectedUser(); // This will be restored at the end of the tests saveExistingHighFidelityAppDataFolder(); @@ -120,7 +206,7 @@ void TestRunner::run() { updateStatusLabel("Downloading Build XML"); buildXMLDownloaded = false; - autoTester->downloadFiles(urls, _workingFolder, filenames, (void*)this); + nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); // `downloadComplete` will run after download has completed } @@ -149,7 +235,7 @@ void TestRunner::downloadComplete() { updateStatusLabel("Downloading installer"); - autoTester->downloadFiles(urls, _workingFolder, filenames, (void*)this); + nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); // `downloadComplete` will run again after download has completed @@ -176,10 +262,43 @@ void TestRunner::runInstaller() { QString installerFullPath = _workingFolder + "/" + _installerFilename; - QString commandLine = - "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder); + QString commandLine; +#ifdef Q_OS_WIN + commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder); +#elif defined Q_OS_MAC + // Create installation shell script + QFile script; + script.setFileName(_workingFolder + "/install_app.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'install_app.sh'"); + exit(-1); + } + + if (!QDir().exists(_installationFolder)) { + QDir().mkdir(_installationFolder); + } + + // This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end + script.write("#!/bin/sh\n\n"); + script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n"); + + QString folderName {"High Fidelity"}; + if (!_runLatest->isChecked()) { + folderName += QString(" - ") + getPRNumberFromURL(_url->text()); + } - installerWorker->setCommandLine(commandLine); + script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); + script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); + + script.write("hdiutil detach \"$VOLUME\"\n"); + script.write("killall yes\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath; +#endif + appendLog(commandLine); + _installerWorker->setCommandLine(commandLine); emit startInstaller(); } @@ -213,11 +332,10 @@ void TestRunner::verifyInstallationSucceeded() { } void TestRunner::saveExistingHighFidelityAppDataFolder() { +#ifdef Q_OS_WIN QString dataDirectory{ "NOT FOUND" }; -#ifdef Q_OS_WIN dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming"; -#endif if (_runLatest->isChecked()) { _appDataFolder = dataDirectory + "\\High Fidelity"; @@ -238,6 +356,9 @@ void TestRunner::saveExistingHighFidelityAppDataFolder() { // Copy an "empty" AppData folder (i.e. no entities) copyFolder(QDir::currentPath() + "/AppDataHighFidelity", _appDataFolder.path()); +#elif defined Q_OS_MAC + // TODO: find Mac equivalent of AppData +#endif } void TestRunner::createSnapshotFolder() { @@ -307,44 +428,110 @@ void TestRunner::killProcesses() { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); exit(-1); } +#elif defined Q_OS_MAC + QString commandLine; + + commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface"; + system(commandLine.toStdString().c_str()); + + commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox"; + system(commandLine.toStdString().c_str()); + + commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console"; + system(commandLine.toStdString().c_str()); #endif } void TestRunner::startLocalServerProcesses() { -#ifdef Q_OS_WIN QString commandLine; - - commandLine = "start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\""; + +#ifdef Q_OS_WIN + commandLine = + "start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\""; system(commandLine.toStdString().c_str()); commandLine = "start \"assignment-client.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe\" -n 6"; system(commandLine.toStdString().c_str()); + +#elif defined Q_OS_MAC + commandLine = "open \"" +_installationFolder + "/Sandbox.app\""; + system(commandLine.toStdString().c_str()); #endif + // Give server processes time to stabilize QThread::sleep(20); } void TestRunner::runInterfaceWithTestScript() { - QString exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; - QString snapshotFolder = QString("\"") + QDir::toNativeSeparators(_snapshotFolder) + "\""; - QString url = QString("hifi://localhost"); if (_runServerless->isChecked()) { // Move to an empty area url = "file:///~serverless/tutorial.json"; } else { +#ifdef Q_OS_WIN url = "hifi://localhost"; +#elif defined Q_OS_MAC + // TODO: Find out Mac equivalent of AppData, then this won't be needed + url = "hifi://localhost/9999,9999,9999"; +#endif } QString testScript = QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; - QString commandLine = exeFile + " --url " + url + " --no-updater --no-login-suggestion" + " --testScript " + testScript + - " quitWhenFinished --testResultsLocation " + snapshotFolder; + QString commandLine; +#ifdef Q_OS_WIN + QString exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + commandLine = exeFile + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation " + _snapshotFolder; - interfaceWorker->setCommandLine(commandLine); + _interfaceWorker->setCommandLine(commandLine); emit startInterface(); +#elif defined Q_OS_MAC + // On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process + // has started. + // Before starting interface, start a process that will resize interface 10s after it opens + // This is performed by creating a bash script that runs to processes + QFile script; + script.setFileName(_workingFolder + "/runInterfaceTests.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'runInterfaceTests.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + + commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n"; + script.write(commandLine.toStdString().c_str()); + + commandLine = + "open \"" +_installationFolder + "/interface.app\" --args" + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation " + _snapshotFolder + + " && " + _workingFolder +"/waitForFinish.sh interface"; + + script.write(commandLine.toStdString().c_str()); + + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + commandLine = _workingFolder + "/runInterfaceTests.sh"; + _interfaceWorker->setCommandLine(commandLine); + + emit startInterface(); +#endif + + // Helpful for debugging + appendLog(commandLine); } void TestRunner::interfaceExecutionComplete() { @@ -352,9 +539,7 @@ void TestRunner::interfaceExecutionComplete() { QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt"); if (!testCompleted.exists()) { - QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts"); - _runNow->setEnabled(true); - return; + QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated"); } evaluateResults(); @@ -364,7 +549,7 @@ void TestRunner::interfaceExecutionComplete() { void TestRunner::evaluateResults() { updateStatusLabel("Evaluating results"); - autoTester->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user); + nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user); } void TestRunner::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) { @@ -404,11 +589,15 @@ void TestRunner::addBuildNumberToResults(QString zippedFolderName) { } void TestRunner::restoreHighFidelityAppDataFolder() { +#ifdef Q_OS_WIN _appDataFolder.removeRecursively(); if (_savedAppDataFolder != QDir()) { _appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path()); } +#elif defined Q_OS_MAC + // TODO: find Mac equivalent of AppData +#endif } // Copies a folder recursively @@ -473,7 +662,7 @@ void TestRunner::checkTime() { } void TestRunner::updateStatusLabel(const QString& message) { - autoTester->updateStatusLabel(message); + nitpick->updateStatusLabel(message); } void TestRunner::appendLog(const QString& message) { @@ -487,11 +676,12 @@ void TestRunner::appendLog(const QString& message) { _logFile.write("\n"); _logFile.close(); - autoTester->appendLogWindow(message); + nitpick->appendLogWindow(message); } QString TestRunner::getInstallerNameFromURL(const QString& url) { // An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe + // On Mac, replace `exe` with `dmg` try { QStringList urlParts = url.split("/"); return urlParts[urlParts.size() - 1]; @@ -509,7 +699,11 @@ QString TestRunner::getPRNumberFromURL(const QString& url) { QStringList urlParts = url.split("/"); QStringList filenameParts = urlParts[urlParts.size() - 1].split("-"); if (filenameParts.size() <= 3) { +#ifdef Q_OS_WIN throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`"; +#elif defined Q_OS_MAC + throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`"; +#endif } return filenameParts[filenameParts.size() - 2]; } catch (QString errorMessage) { @@ -536,6 +730,7 @@ void TestRunner::parseBuildInformation() { #elif defined(Q_OS_MAC) platformOfInterest = "mac"; #endif + QDomElement element = domDocument.documentElement(); // Verify first element is "projects" @@ -552,42 +747,48 @@ void TestRunner::parseBuildInformation() { throw("File is not from 'interface' build"); } - // Now loop over the platforms + // Now loop over the platforms, looking for ours + bool platformFound{ false }; + element = element.firstChild().toElement(); while (!element.isNull()) { - element = element.firstChild().toElement(); - if (element.tagName() != "platform" || element.attribute("name") != platformOfInterest) { - continue; + if (element.attribute("name") == platformOfInterest) { + platformFound = true; + break; } - - // Next element should be the build - element = element.firstChild().toElement(); - if (element.tagName() != "build") { - throw("File seems to be in wrong format"); - } - - // Next element should be the version - element = element.firstChild().toElement(); - if (element.tagName() != "version") { - throw("File seems to be in wrong format"); - } - - // Add the build number to the end of the filename - _buildInformation.build = element.text(); - - // First sibling should be stable_version element = element.nextSibling().toElement(); - if (element.tagName() != "stable_version") { - throw("File seems to be in wrong format"); - } - - // Next sibling should be url - element = element.nextSibling().toElement(); - if (element.tagName() != "url") { - throw("File seems to be in wrong format"); - } - _buildInformation.url = element.text(); } + if (!platformFound) { + throw("File seems to be in wrong format - platform " + platformOfInterest + " not found"); + } + + element = element.firstChild().toElement(); + if (element.tagName() != "build") { + throw("File seems to be in wrong format"); + } + + // Next element should be the version + element = element.firstChild().toElement(); + if (element.tagName() != "version") { + throw("File seems to be in wrong format"); + } + + // Add the build number to the end of the filename + _buildInformation.build = element.text(); + + // First sibling should be stable_version + element = element.nextSibling().toElement(); + if (element.tagName() != "stable_version") { + throw("File seems to be in wrong format"); + } + + // Next sibling should be url + element = element.nextSibling().toElement(); + if (element.tagName() != "url") { + throw("File seems to be in wrong format"); + } + _buildInformation.url = element.text(); + } catch (QString errorMessage) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); exit(-1); @@ -605,4 +806,4 @@ int Worker::runCommand() { int result = system(_commandLine.toStdString().c_str()); emit commandComplete(); return result; -} \ No newline at end of file +} diff --git a/tools/auto-tester/src/TestRunner.h b/tools/nitpick/src/TestRunner.h similarity index 90% rename from tools/auto-tester/src/TestRunner.h rename to tools/nitpick/src/TestRunner.h index e6cb7cd764..00f0f66ecf 100644 --- a/tools/auto-tester/src/TestRunner.h +++ b/tools/nitpick/src/TestRunner.h @@ -84,11 +84,18 @@ private slots: signals: void startInstaller(); void startInterface(); - + void startResize(); + private: bool _automatedTestIsRunning{ false }; +#ifdef Q_OS_WIN const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" }; +#elif defined(Q_OS_MAC) + const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.dmg" }; +#else + const QString INSTALLER_FILENAME_LATEST{ "" }; +#endif QString _installerURL; QString _installerFilename; @@ -124,11 +131,12 @@ private: QDateTime _testStartDateTime; - QThread* installerThread; - QThread* interfaceThread; - Worker* installerWorker; - Worker* interfaceWorker; + QThread* _installerThread; + QThread* _interfaceThread; + Worker* _installerWorker; + Worker* _interfaceWorker; + BuildInformation _buildInformation; }; @@ -144,8 +152,8 @@ signals: void commandComplete(); void startInstaller(); void startInterface(); - + private: QString _commandLine; }; -#endif // hifi_testRunner_h \ No newline at end of file +#endif // hifi_testRunner_h diff --git a/tools/auto-tester/src/common.h b/tools/nitpick/src/common.h similarity index 100% rename from tools/auto-tester/src/common.h rename to tools/nitpick/src/common.h diff --git a/tools/auto-tester/src/main.cpp b/tools/nitpick/src/main.cpp similarity index 89% rename from tools/auto-tester/src/main.cpp rename to tools/nitpick/src/main.cpp index ac4b4593c5..089a72e6ce 100644 --- a/tools/auto-tester/src/main.cpp +++ b/tools/nitpick/src/main.cpp @@ -8,11 +8,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include -#include "ui/AutoTester.h" +#include "ui/Nitpick.h" #include -AutoTester* autoTester; +Nitpick* nitpick; int main(int argc, char *argv[]) { // If no parameters then run in interactive mode @@ -62,13 +62,13 @@ int main(int argc, char *argv[]) { QApplication application(argc, argv); - autoTester = new AutoTester(); - autoTester->setup(); + nitpick = new Nitpick(); + nitpick->setup(); if (!testFolder.isNull()) { - autoTester->startTestsEvaluation(true ,false, testFolder, branch, user); + nitpick->startTestsEvaluation(true ,false, testFolder, branch, user); } else { - autoTester->show(); + nitpick->show(); } return application.exec(); diff --git a/tools/auto-tester/src/ui/BusyWindow.cpp b/tools/nitpick/src/ui/BusyWindow.cpp similarity index 100% rename from tools/auto-tester/src/ui/BusyWindow.cpp rename to tools/nitpick/src/ui/BusyWindow.cpp diff --git a/tools/auto-tester/src/ui/BusyWindow.h b/tools/nitpick/src/ui/BusyWindow.h similarity index 100% rename from tools/auto-tester/src/ui/BusyWindow.h rename to tools/nitpick/src/ui/BusyWindow.h diff --git a/tools/auto-tester/src/ui/BusyWindow.ui b/tools/nitpick/src/ui/BusyWindow.ui similarity index 100% rename from tools/auto-tester/src/ui/BusyWindow.ui rename to tools/nitpick/src/ui/BusyWindow.ui diff --git a/tools/auto-tester/src/ui/HelpWindow.cpp b/tools/nitpick/src/ui/HelpWindow.cpp similarity index 100% rename from tools/auto-tester/src/ui/HelpWindow.cpp rename to tools/nitpick/src/ui/HelpWindow.cpp diff --git a/tools/auto-tester/src/ui/HelpWindow.h b/tools/nitpick/src/ui/HelpWindow.h similarity index 100% rename from tools/auto-tester/src/ui/HelpWindow.h rename to tools/nitpick/src/ui/HelpWindow.h diff --git a/tools/auto-tester/src/ui/HelpWindow.ui b/tools/nitpick/src/ui/HelpWindow.ui similarity index 96% rename from tools/auto-tester/src/ui/HelpWindow.ui rename to tools/nitpick/src/ui/HelpWindow.ui index d2aa0da0d4..1ce6e8c321 100644 --- a/tools/auto-tester/src/ui/HelpWindow.ui +++ b/tools/nitpick/src/ui/HelpWindow.ui @@ -14,7 +14,7 @@ - AutoTester Help + Nitpick Help diff --git a/tools/auto-tester/src/ui/MismatchWindow.cpp b/tools/nitpick/src/ui/MismatchWindow.cpp similarity index 100% rename from tools/auto-tester/src/ui/MismatchWindow.cpp rename to tools/nitpick/src/ui/MismatchWindow.cpp diff --git a/tools/auto-tester/src/ui/MismatchWindow.h b/tools/nitpick/src/ui/MismatchWindow.h similarity index 100% rename from tools/auto-tester/src/ui/MismatchWindow.h rename to tools/nitpick/src/ui/MismatchWindow.h diff --git a/tools/auto-tester/src/ui/MismatchWindow.ui b/tools/nitpick/src/ui/MismatchWindow.ui similarity index 100% rename from tools/auto-tester/src/ui/MismatchWindow.ui rename to tools/nitpick/src/ui/MismatchWindow.ui diff --git a/tools/auto-tester/src/ui/AutoTester.cpp b/tools/nitpick/src/ui/Nitpick.cpp similarity index 72% rename from tools/auto-tester/src/ui/AutoTester.cpp rename to tools/nitpick/src/ui/Nitpick.cpp index 32457c2224..a4aef8fad5 100644 --- a/tools/auto-tester/src/ui/AutoTester.cpp +++ b/tools/nitpick/src/ui/Nitpick.cpp @@ -1,5 +1,5 @@ // -// AutoTester.cpp +// Nitpick.cpp // zone/ambientLightInheritence // // Created by Nissim Hadar on 2 Nov 2017. @@ -8,14 +8,14 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "AutoTester.h" +#include "Nitpick.h" #ifdef Q_OS_WIN #include #include #endif -AutoTester::AutoTester(QWidget* parent) : QMainWindow(parent) { +Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.setupUi(this); _ui.checkBoxInteractiveMode->setChecked(true); @@ -24,9 +24,9 @@ AutoTester::AutoTester(QWidget* parent) : QMainWindow(parent) { _signalMapper = new QSignalMapper(); - connect(_ui.actionClose, &QAction::triggered, this, &AutoTester::on_closeButton_clicked); - connect(_ui.actionAbout, &QAction::triggered, this, &AutoTester::about); - connect(_ui.actionContent, &QAction::triggered, this, &AutoTester::content); + connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closeButton_clicked); + connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about); + connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content); // The second tab hides and shows the Windows task bar #ifndef Q_OS_WIN @@ -36,13 +36,13 @@ AutoTester::AutoTester(QWidget* parent) : QMainWindow(parent) { _ui.statusLabel->setText(""); _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Auto Tester - v6.7"); + setWindowTitle("Nitpick - v1.0"); - // Coming soon to an auto-tester near you... + // Coming soon to a nitpick near you... //// _helpWindow.textBrowser->setText() } -AutoTester::~AutoTester() { +Nitpick::~Nitpick() { delete _signalMapper; if (_test) { @@ -54,7 +54,7 @@ AutoTester::~AutoTester() { } } -void AutoTester::setup() { +void Nitpick::setup() { if (_test) { delete _test; } @@ -87,7 +87,7 @@ void AutoTester::setup() { _testRunner = new TestRunner(dayCheckboxes, timeEditCheckboxes, timeEdits, _ui.workingFolderLabel, _ui.checkBoxServerless, _ui.checkBoxRunLatest, _ui.urlLineEdit, _ui.runNowButton); } -void AutoTester::startTestsEvaluation(const bool isRunningFromCommandLine, +void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, const bool isRunningInAutomaticTestRun, const QString& snapshotDirectory, const QString& branch, @@ -96,8 +96,13 @@ void AutoTester::startTestsEvaluation(const bool isRunningFromCommandLine, _test->startTestsEvaluation(isRunningFromCommandLine, isRunningInAutomaticTestRun, snapshotDirectory, branch, user); } -void AutoTester::on_tabWidget_currentChanged(int index) { +void Nitpick::on_tabWidget_currentChanged(int index) { +// Enable the GitHub edit boxes as required +#ifdef Q_OS_WIN if (index == 0 || index == 2 || index == 3) { +#else + if (index == 0 || index == 1 || index == 2) { +#endif _ui.userLineEdit->setDisabled(false); _ui.branchLineEdit->setDisabled(false); } else { @@ -106,73 +111,73 @@ void AutoTester::on_tabWidget_currentChanged(int index) { } } -void AutoTester::on_evaluateTestsButton_clicked() { +void Nitpick::on_evaluateTestsButton_clicked() { _test->startTestsEvaluation(false, false); } -void AutoTester::on_createRecursiveScriptButton_clicked() { +void Nitpick::on_createRecursiveScriptButton_clicked() { _test->createRecursiveScript(); } -void AutoTester::on_createAllRecursiveScriptsButton_clicked() { +void Nitpick::on_createAllRecursiveScriptsButton_clicked() { _test->createAllRecursiveScripts(); } -void AutoTester::on_createTestsButton_clicked() { +void Nitpick::on_createTestsButton_clicked() { _test->createTests(); } -void AutoTester::on_createMDFileButton_clicked() { +void Nitpick::on_createMDFileButton_clicked() { _test->createMDFile(); } -void AutoTester::on_createAllMDFilesButton_clicked() { +void Nitpick::on_createAllMDFilesButton_clicked() { _test->createAllMDFiles(); } -void AutoTester::on_createTestAutoScriptButton_clicked() { +void Nitpick::on_createTestAutoScriptButton_clicked() { _test->createTestAutoScript(); } -void AutoTester::on_createAllTestAutoScriptsButton_clicked() { +void Nitpick::on_createAllTestAutoScriptsButton_clicked() { _test->createAllTestAutoScripts(); } -void AutoTester::on_createTestsOutlineButton_clicked() { +void Nitpick::on_createTestsOutlineButton_clicked() { _test->createTestsOutline(); } -void AutoTester::on_createTestRailTestCasesButton_clicked() { +void Nitpick::on_createTestRailTestCasesButton_clicked() { _test->createTestRailTestCases(); } -void AutoTester::on_createTestRailRunButton_clicked() { +void Nitpick::on_createTestRailRunButton_clicked() { _test->createTestRailRun(); } -void AutoTester::on_setWorkingFolderButton_clicked() { +void Nitpick::on_setWorkingFolderButton_clicked() { _testRunner->setWorkingFolder(); } -void AutoTester::enableRunTabControls() { +void Nitpick::enableRunTabControls() { _ui.runNowButton->setEnabled(true); _ui.daysGroupBox->setEnabled(true); _ui.timesGroupBox->setEnabled(true); } -void AutoTester::on_runNowButton_clicked() { +void Nitpick::on_runNowButton_clicked() { _testRunner->run(); } -void AutoTester::on_checkBoxRunLatest_clicked() { +void Nitpick::on_checkBoxRunLatest_clicked() { _ui.urlLineEdit->setEnabled(!_ui.checkBoxRunLatest->isChecked()); } -void AutoTester::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { +void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { _testRunner->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); } -void AutoTester::on_updateTestRailRunResultsButton_clicked() { +void Nitpick::on_updateTestRailRunResultsButton_clicked() { _test->updateTestRailRunResult(); } @@ -180,7 +185,7 @@ void AutoTester::on_updateTestRailRunResultsButton_clicked() { // if (uState & ABS_AUTOHIDE) on_showTaskbarButton_clicked(); // else on_hideTaskbarButton_clicked(); // -void AutoTester::on_hideTaskbarButton_clicked() { +void Nitpick::on_hideTaskbarButton_clicked() { #ifdef Q_OS_WIN APPBARDATA abd = { sizeof abd }; UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); @@ -190,7 +195,7 @@ void AutoTester::on_hideTaskbarButton_clicked() { #endif } -void AutoTester::on_showTaskbarButton_clicked() { +void Nitpick::on_showTaskbarButton_clicked() { #ifdef Q_OS_WIN APPBARDATA abd = { sizeof abd }; UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); @@ -200,23 +205,23 @@ void AutoTester::on_showTaskbarButton_clicked() { #endif } -void AutoTester::on_closeButton_clicked() { +void Nitpick::on_closeButton_clicked() { exit(0); } -void AutoTester::on_createPythonScriptRadioButton_clicked() { +void Nitpick::on_createPythonScriptRadioButton_clicked() { _test->setTestRailCreateMode(PYTHON); } -void AutoTester::on_createXMLScriptRadioButton_clicked() { +void Nitpick::on_createXMLScriptRadioButton_clicked() { _test->setTestRailCreateMode(XML); } -void AutoTester::on_createWebPagePushButton_clicked() { +void Nitpick::on_createWebPagePushButton_clicked() { _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); } -void AutoTester::downloadFile(const QUrl& url) { +void Nitpick::downloadFile(const QUrl& url) { _downloaders.emplace_back(new Downloader(url, this)); connect(_downloaders[_index], SIGNAL(downloaded()), _signalMapper, SLOT(map())); @@ -225,7 +230,7 @@ void AutoTester::downloadFile(const QUrl& url) { ++_index; } -void AutoTester::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) { +void Nitpick::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) { connect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); _directoryName = directoryName; @@ -251,7 +256,7 @@ void AutoTester::downloadFiles(const QStringList& URLs, const QString& directory } } -void AutoTester::saveFile(int index) { +void Nitpick::saveFile(int index) { try { QFile file(_directoryName + "/" + _filenames[index]); file.open(QIODevice::WriteOnly); @@ -277,34 +282,34 @@ void AutoTester::saveFile(int index) { } } -void AutoTester::about() { +void Nitpick::about() { QMessageBox::information(0, "About", QString("Built ") + __DATE__ + ", " + __TIME__); } -void AutoTester::content() { +void Nitpick::content() { _helpWindow.show(); } -void AutoTester::setUserText(const QString& user) { +void Nitpick::setUserText(const QString& user) { _ui.userLineEdit->setText(user); } -QString AutoTester::getSelectedUser() { +QString Nitpick::getSelectedUser() { return _ui.userLineEdit->text(); } -void AutoTester::setBranchText(const QString& branch) { +void Nitpick::setBranchText(const QString& branch) { _ui.branchLineEdit->setText(branch); } -QString AutoTester::getSelectedBranch() { +QString Nitpick::getSelectedBranch() { return _ui.branchLineEdit->text(); } -void AutoTester::updateStatusLabel(const QString& status) { +void Nitpick::updateStatusLabel(const QString& status) { _ui.statusLabel->setText(status); } -void AutoTester::appendLogWindow(const QString& message) { +void Nitpick::appendLogWindow(const QString& message) { _ui.plainTextEdit->appendPlainText(message); -} \ No newline at end of file +} diff --git a/tools/auto-tester/src/ui/AutoTester.h b/tools/nitpick/src/ui/Nitpick.h similarity index 92% rename from tools/auto-tester/src/ui/AutoTester.h rename to tools/nitpick/src/ui/Nitpick.h index 429a8b60e1..21b917654b 100644 --- a/tools/auto-tester/src/ui/AutoTester.h +++ b/tools/nitpick/src/ui/Nitpick.h @@ -1,5 +1,5 @@ // -// AutoTester.h +// Nitpick.h // // Created by Nissim Hadar on 2 Nov 2017. // Copyright 2013 High Fidelity, Inc. @@ -7,13 +7,13 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_AutoTester_h -#define hifi_AutoTester_h +#ifndef hifi_Nitpick_h +#define hifi_Nitpick_h #include #include #include -#include "ui_AutoTester.h" +#include "ui_Nitpick.h" #include "../Downloader.h" #include "../Test.h" @@ -22,12 +22,12 @@ #include "../TestRunner.h" #include "../AWSInterface.h" -class AutoTester : public QMainWindow { +class Nitpick : public QMainWindow { Q_OBJECT public: - AutoTester(QWidget* parent = Q_NULLPTR); - ~AutoTester(); + Nitpick(QWidget* parent = Q_NULLPTR); + ~Nitpick(); void setup(); @@ -95,7 +95,7 @@ private slots: void content(); private: - Ui::AutoTesterClass _ui; + Ui::NitpickClass _ui; Test* _test{ nullptr }; TestRunner* _testRunner{ nullptr }; @@ -121,4 +121,4 @@ private: void* _caller; }; -#endif // hifi_AutoTester_h \ No newline at end of file +#endif // hifi_Nitpick_h \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.ui b/tools/nitpick/src/ui/Nitpick.ui similarity index 98% rename from tools/auto-tester/src/ui/AutoTester.ui rename to tools/nitpick/src/ui/Nitpick.ui index b277fbdb2a..5e20e75553 100644 --- a/tools/auto-tester/src/ui/AutoTester.ui +++ b/tools/nitpick/src/ui/Nitpick.ui @@ -1,7 +1,7 @@ - AutoTesterClass - + NitpickClass + 0 @@ -17,7 +17,7 @@ - AutoTester + Nitpick @@ -198,7 +198,7 @@ 10 160 161 - 28 + 51 @@ -525,7 +525,7 @@ 128 95 - 21 + 31 31 @@ -539,7 +539,7 @@ - 160 + 170 100 451 21 @@ -554,9 +554,9 @@ - 200 + 190 180 - 120 + 131 20 @@ -572,8 +572,8 @@ 330 170 - 101 - 40 + 181 + 51 @@ -684,10 +684,10 @@ - 240 + 270 30 160 - 40 + 51 @@ -699,7 +699,7 @@ 150 42 - 81 + 111 17 @@ -803,7 +803,7 @@ 0 0 720 - 21 + 22 diff --git a/tools/auto-tester/src/ui/TestRailResultsSelectorWindow.cpp b/tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp similarity index 100% rename from tools/auto-tester/src/ui/TestRailResultsSelectorWindow.cpp rename to tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp diff --git a/tools/auto-tester/src/ui/TestRailResultsSelectorWindow.h b/tools/nitpick/src/ui/TestRailResultsSelectorWindow.h similarity index 100% rename from tools/auto-tester/src/ui/TestRailResultsSelectorWindow.h rename to tools/nitpick/src/ui/TestRailResultsSelectorWindow.h diff --git a/tools/auto-tester/src/ui/TestRailResultsSelectorWindow.ui b/tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui similarity index 100% rename from tools/auto-tester/src/ui/TestRailResultsSelectorWindow.ui rename to tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui diff --git a/tools/auto-tester/src/ui/TestRailRunSelectorWindow.cpp b/tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp similarity index 100% rename from tools/auto-tester/src/ui/TestRailRunSelectorWindow.cpp rename to tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp diff --git a/tools/auto-tester/src/ui/TestRailRunSelectorWindow.h b/tools/nitpick/src/ui/TestRailRunSelectorWindow.h similarity index 100% rename from tools/auto-tester/src/ui/TestRailRunSelectorWindow.h rename to tools/nitpick/src/ui/TestRailRunSelectorWindow.h diff --git a/tools/auto-tester/src/ui/TestRailRunSelectorWindow.ui b/tools/nitpick/src/ui/TestRailRunSelectorWindow.ui similarity index 100% rename from tools/auto-tester/src/ui/TestRailRunSelectorWindow.ui rename to tools/nitpick/src/ui/TestRailRunSelectorWindow.ui diff --git a/tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.cpp b/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp similarity index 100% rename from tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.cpp rename to tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp diff --git a/tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.h b/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h similarity index 100% rename from tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.h rename to tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h diff --git a/tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.ui b/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui similarity index 100% rename from tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.ui rename to tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui