Merge branch 'master' into M19345

This commit is contained in:
David Rowe 2018-11-19 09:47:05 +13:00
commit eb8c6c8974
144 changed files with 3233 additions and 1617 deletions

View file

@ -754,13 +754,13 @@ void Agent::processAgentAvatarAudio() {
const int16_t* nextSoundOutput = NULL;
if (_avatarSound) {
const QByteArray& soundByteArray = _avatarSound->getByteArray();
nextSoundOutput = reinterpret_cast<const int16_t*>(soundByteArray.data()
auto audioData = _avatarSound->getAudioData();
nextSoundOutput = reinterpret_cast<const int16_t*>(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();

View file

@ -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)) {

View file

@ -144,11 +144,13 @@ private:
static std::map<QString, CodecPluginPointer> _availableCodecs;
static QStringList _codecPreferenceOrder;
static std::vector<ZoneDescription> _audioZones;
static std::vector<ZoneSettings> _zoneSettings;
static std::vector<ReverbSettings> _zoneReverbSettings;
float _throttleStartTarget = 0.9f;
float _throttleBackoffTarget = 0.44f;
AudioMixerSlave::SharedData _workerSharedData;
};

View file

@ -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
}
]
},

View file

@ -66,7 +66,7 @@
<script>
var handControllerImageURL = null;
var index = 0;
var count = 5;
var count = 3;
function showKbm() {
document.getElementById("main_image").setAttribute("src", "img/tablet-help-keyboard.jpg");
@ -94,24 +94,14 @@
switch (index)
{
case 0:
handControllerImageURL = "img/tablet-help-oculus.jpg";
showHandControllers();
break;
case 1:
handControllerImageURL = "img/tablet-help-vive.jpg";
showHandControllers();
break;
case 2:
handControllerImageURL = "img/tablet-help-windowsMR.jpg";
showHandControllers();
break;
case 3:
showGamepad();
break;
case 4:
case 1:
showKbm();
break;
case 2:
showHandControllers();
break;
default:
}
}
@ -144,34 +134,33 @@
}
switch (params.handControllerName) {
case "oculus":
handControllerImageURL = "img/tablet-help-oculus.jpg";
index = 0;
break;
case "windowsMR":
handControllerImageURL = "img/tablet-help-windowsMR.jpg";
index = 2;
break;
case "vive":
default:
handControllerImageURL = "img/tablet-help-vive.jpg";
index = 1;
break;
case "oculus":
handControllerImageURL = "img/tablet-help-oculus.jpg";
break;
default:
handControllerImageURL = "";
count = 2;
}
switch (params.defaultTab) {
case "gamepad":
showGamepad();
index = 3;
break;
case "handControllers":
showHandControllers();
index = 2;
break;
case "gamepad":
showGamepad();
index = 0;
break;
case "kbm":
default:
showKbm();
index = 4;
index = 1;
}
}
</script>

View file

@ -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 {

View file

@ -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;

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -56,7 +56,7 @@ StackView {
Qt.callLater(function() {
addressBarDialog.keyboardEnabled = HMD.active;
addressLine.forceActiveFocus();
addressBarDialog.raised = true;
addressBarDialog.keyboardRaised = true;
})
}

View file

@ -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;
@ -1229,6 +1231,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);
@ -3417,26 +3431,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);
@ -4050,8 +4075,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);
@ -5733,6 +5757,7 @@ void Application::update(float deltaTime) {
quint64 now = usecTimestampNow();
if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) {
bool enableInterstitial = DependencyManager::get<NodeList>()->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;
@ -6664,7 +6689,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) {
@ -6692,6 +6723,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()) {

View file

@ -310,6 +310,7 @@ public:
bool isServerlessMode() const;
bool isInterstitialMode() const { return _interstitialMode; }
bool failedToConnectToEntityServer() const { return _failedToConnectToEntityServer; }
void replaceDomainContent(const QString& url);
@ -467,6 +468,7 @@ private slots:
void loadSettings();
void saveSettings() const;
void setFailedToConnectToEntityServer() { _failedToConnectToEntityServer = true; }
bool acceptSnapshot(const QString& urlString);
bool askToSetAvatarUrl(const QString& url);
@ -719,6 +721,7 @@ private:
bool _isForeground = true; // starts out assumed to be in foreground
bool _isGLInitialized { false };
bool _physicsEnabled { false };
bool _failedToConnectToEntityServer { false };
bool _reticleClickPressed { false };
@ -765,6 +768,7 @@ private:
QStringList _addAssetToWorldInfoMessages; // Info message
QTimer _addAssetToWorldInfoTimer;
QTimer _addAssetToWorldErrorTimer;
mutable QTimer _entityServerConnectionTimer;
FileScriptingInterface* _fileDownload;
AudioInjectorPointer _snapshotSoundInjector;

View file

@ -141,7 +141,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()));

View file

@ -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";

View file

@ -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,

View file

@ -41,7 +41,6 @@ private:
QFileInfo _modelFile;
QFileInfo _fbxInfo;
FSTReader::ModelType _modelType;
QString _texDir;
QString _scriptDir;

View file

@ -26,9 +26,8 @@
#include <OffscreenUi.h>
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<QComboBox*>(_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<QComboBox*>(_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);
}
}
}

View file

@ -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
#endif // hifi_ModelPropertiesDialog_h

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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<AudioData::AudioSample*>(_lastSoundByteArray.data());
auto newAudioData = AudioData::make(numSamples, numChannels, samples);
_lastSoundAudioInjector = AudioInjector::playSoundAndDelete(newAudioData, options);
_lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS);
#else

View file

@ -198,4 +198,14 @@ void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) {
int TestScriptingInterface::getOtherAvatarsReplicaCount() {
return qApp->getOtherAvatarsReplicaCount();
}
}
QString TestScriptingInterface::getOperatingSystemType() {
#ifdef Q_OS_WIN
return "WINDOWS";
#elif defined Q_OS_MAC
return "MACOS";
#else
return "UNKNOWN";
#endif
}

View file

@ -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<bool()> condition);
QString _testResultsLocation;

View file

@ -485,7 +485,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);

View file

@ -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"; }

View file

@ -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<int>(_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<int16_t*>(_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<AudioSample*>(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<AudioSample*>(resampledBuffer.data());
resampler.render(reinterpret_cast<const int16_t*>(buffer.data()),
reinterpret_cast<int16_t*>(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<AudioSample*>(resampledBuffer.data());
resampler.render(audioData->data(), bufferPtr, numFrames);
int numSamples = maxOutputFrames * numChannels;
auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr);
return AudioInjector::playSound(newAudioData, options);
}
}

View file

@ -52,8 +52,8 @@ AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs)
class AudioInjector : public QObject, public QEnableSharedFromThis<AudioInjector> {
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 };

View file

@ -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) {

View file

@ -16,10 +16,12 @@
#include <glm/common.hpp>
#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

View file

@ -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>("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<AudioData*>(memory);
auto buffer = reinterpret_cast<AudioSample*>(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<SoundScriptingInterface*>(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<Resource> sound, QByteArray data) :
_sound(sound),
_data(data)
{
}
void SoundProcessor::run() {
auto sound = qSharedPointerCast<Sound>(_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<QByteArray *>(&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<quint16>(wave.audioFormat) != WAVEFORMAT_PCM &&
qFromLittleEndian<quint16>(wave.audioFormat) != WAVEFORMAT_EXTENSIBLE) {
qCWarning(audio) << "Currently not supporting non PCM audio files.";
return 0;
return AudioProperties();
}
if (qFromLittleEndian<quint16>(wave.numChannels) == 2) {
_isStereo = true;
} else if (qFromLittleEndian<quint16>(wave.numChannels) == 4) {
_isAmbisonic = true;
} else if (qFromLittleEndian<quint16>(wave.numChannels) != 1) {
properties.numChannels = qFromLittleEndian<quint16>(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<quint16>(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<quint32>(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<SoundScriptingInterface*>(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);
}

View file

@ -19,61 +19,102 @@
#include <ResourceCache.h>
#include "AudioConstants.h"
class AudioData;
using AudioDataPointer = std::shared_ptr<const AudioData>;
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<const char*>(_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<Resource> 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<Resource> _sound;
const QByteArray _data;
};
typedef QSharedPointer<Sound> SharedSoundPointer;

View file

@ -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,

View file

@ -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<AvatarHashMap>();
AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID);
myAvatar->insertDetachedEntityID(id);
shouldSendDeleteToServer = false;
return;
}

View file

@ -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();

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -13,6 +13,7 @@
#include <string.h>
#include <QDebug>
#include "ShaderConstants.h"
#include "GPULogging.h"

View file

@ -14,6 +14,7 @@
#include "Frame.h"
#include "GPULogging.h"
#include <shaders/Shaders.h>
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;
{

View file

@ -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;

View file

@ -234,7 +234,7 @@ private:
#ifdef Q_OS_ANDROID
Setting::Handle<bool> _enableInterstitialMode{ "enableInterstitialMode", false };
#else
Setting::Handle<bool> _enableInterstitialMode { "enableInterstitialMode", true };
Setting::Handle<bool> _enableInterstitialMode { "enableInterstitialMode", false };
#endif
QSet<QString> _domainConnectionRefusals;

View file

@ -65,6 +65,6 @@ bool PluginUtils::isOculusTouchControllerAvailable() {
};
bool PluginUtils::isXboxControllerAvailable() {
return isSubdeviceContainingNameAvailable("X360 Controller");
return isSubdeviceContainingNameAvailable("X360 Controller") || isSubdeviceContainingNameAvailable("XInput Controller");
};

File diff suppressed because it is too large Load diff

View file

@ -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<AmbientOcclusionFramebuffer>;
@ -57,53 +79,78 @@ using AmbientOcclusionFramebufferPointer = std::shared_ptr<AmbientOcclusionFrame
class AmbientOcclusionEffectConfig : public render::GPUJobConfig::Persistent {
Q_OBJECT
Q_PROPERTY(bool enabled MEMBER enabled NOTIFY dirty)
Q_PROPERTY(bool horizonBased MEMBER horizonBased NOTIFY dirty)
Q_PROPERTY(bool ditheringEnabled MEMBER ditheringEnabled NOTIFY dirty)
Q_PROPERTY(bool borderingEnabled MEMBER borderingEnabled NOTIFY dirty)
Q_PROPERTY(bool fetchMipsEnabled MEMBER fetchMipsEnabled NOTIFY dirty)
Q_PROPERTY(float radius MEMBER radius WRITE setRadius)
Q_PROPERTY(float obscuranceLevel MEMBER obscuranceLevel WRITE setObscuranceLevel)
Q_PROPERTY(float falloffBias MEMBER falloffBias WRITE setFalloffBias)
Q_PROPERTY(float edgeSharpness MEMBER edgeSharpness WRITE setEdgeSharpness)
Q_PROPERTY(float blurDeviation MEMBER blurDeviation WRITE setBlurDeviation)
Q_PROPERTY(float numSpiralTurns MEMBER numSpiralTurns WRITE setNumSpiralTurns)
Q_PROPERTY(int numSamples MEMBER numSamples WRITE setNumSamples)
Q_PROPERTY(bool jitterEnabled MEMBER jitterEnabled NOTIFY dirty)
Q_PROPERTY(int resolutionLevel MEMBER resolutionLevel WRITE setResolutionLevel)
Q_PROPERTY(float edgeSharpness MEMBER edgeSharpness WRITE setEdgeSharpness)
Q_PROPERTY(int blurRadius MEMBER blurRadius WRITE setBlurRadius)
// SSAO
Q_PROPERTY(float ssaoRadius MEMBER ssaoRadius WRITE setSSAORadius)
Q_PROPERTY(float ssaoObscuranceLevel MEMBER ssaoObscuranceLevel WRITE setSSAOObscuranceLevel)
Q_PROPERTY(float ssaoFalloffAngle MEMBER ssaoFalloffAngle WRITE setSSAOFalloffAngle)
Q_PROPERTY(float ssaoNumSpiralTurns MEMBER ssaoNumSpiralTurns WRITE setSSAONumSpiralTurns)
Q_PROPERTY(int ssaoNumSamples MEMBER ssaoNumSamples WRITE setSSAONumSamples)
// HBAO
Q_PROPERTY(float hbaoRadius MEMBER hbaoRadius WRITE setHBAORadius)
Q_PROPERTY(float hbaoObscuranceLevel MEMBER hbaoObscuranceLevel WRITE setHBAOObscuranceLevel)
Q_PROPERTY(float hbaoFalloffAngle MEMBER hbaoFalloffAngle WRITE setHBAOFalloffAngle)
Q_PROPERTY(int hbaoNumSamples MEMBER hbaoNumSamples WRITE setHBAONumSamples)
public:
AmbientOcclusionEffectConfig() : render::GPUJobConfig::Persistent(QStringList() << "Render" << "Engine" << "Ambient Occlusion", false) {}
AmbientOcclusionEffectConfig();
const int MAX_RESOLUTION_LEVEL = 4;
const int MAX_BLUR_RADIUS = 6;
const int MAX_BLUR_RADIUS = 15;
void setRadius(float newRadius) { radius = std::max(0.01f, newRadius); emit dirty(); }
void setObscuranceLevel(float level) { obscuranceLevel = std::max(0.01f, level); emit dirty(); }
void setFalloffBias(float bias) { falloffBias = std::max(0.0f, std::min(bias, 0.2f)); emit dirty(); }
void setEdgeSharpness(float sharpness) { edgeSharpness = std::max(0.0f, (float)sharpness); emit dirty(); }
void setBlurDeviation(float deviation) { blurDeviation = std::max(0.0f, deviation); emit dirty(); }
void setNumSpiralTurns(float turns) { numSpiralTurns = std::max(0.0f, (float)turns); emit dirty(); }
void setNumSamples(int samples) { numSamples = std::max(1.0f, (float)samples); emit dirty(); }
void setResolutionLevel(int level) { resolutionLevel = std::max(0, std::min(level, MAX_RESOLUTION_LEVEL)); emit dirty(); }
void setBlurRadius(int radius) { blurRadius = std::max(0, std::min(MAX_BLUR_RADIUS, radius)); emit dirty(); }
void setEdgeSharpness(float sharpness);
void setResolutionLevel(int level);
void setBlurRadius(int radius);
float radius{ 0.5f };
float perspectiveScale{ 1.0f };
float obscuranceLevel{ 0.5f }; // intensify or dim down the obscurance effect
float falloffBias{ 0.01f };
float edgeSharpness{ 1.0f };
float blurDeviation{ 2.5f };
float numSpiralTurns{ 7.0f }; // defining an angle span to distribute the samples ray directions
int numSamples{ 16 };
int resolutionLevel{ 1 };
int blurRadius{ 4 }; // 0 means no blurring
bool ditheringEnabled{ true }; // randomize the distribution of taps per pixel, should always be true
bool borderingEnabled{ true }; // avoid evaluating information from non existing pixels out of the frame, should always be true
bool fetchMipsEnabled{ true }; // fetch taps in sub mips to otpimize cache, should always be true
void setSSAORadius(float newRadius);
void setSSAOObscuranceLevel(float level);
void setSSAOFalloffAngle(float bias);
void setSSAONumSpiralTurns(float turns);
void setSSAONumSamples(int samples);
void setHBAORadius(float newRadius);
void setHBAOObscuranceLevel(float level);
void setHBAOFalloffAngle(float bias);
void setHBAONumSamples(int samples);
float perspectiveScale;
float edgeSharpness;
int blurRadius; // 0 means no blurring
int resolutionLevel;
float ssaoRadius;
float ssaoObscuranceLevel; // intensify or dim down the obscurance effect
float ssaoFalloffAngle;
float ssaoNumSpiralTurns; // defining an angle span to distribute the samples ray directions
int ssaoNumSamples;
float hbaoRadius;
float hbaoObscuranceLevel; // intensify or dim down the obscurance effect
float hbaoFalloffAngle;
int hbaoNumSamples;
bool horizonBased; // Use horizon based AO
bool ditheringEnabled; // randomize the distribution of taps per pixel, should always be true
bool borderingEnabled; // avoid evaluating information from non existing pixels out of the frame, should always be true
bool fetchMipsEnabled; // fetch taps in sub mips to otpimize cache, should always be true
bool jitterEnabled; // Add small jittering to AO samples at each frame
signals:
void dirty();
};
#define SSAO_RANDOM_SAMPLE_COUNT 16
class AmbientOcclusionEffect {
public:
using Inputs = render::VaryingSet3<DeferredFrameTransformPointer, DeferredFramebufferPointer, LinearDepthFramebufferPointer>;
@ -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<Parameters>;
using AOParametersBuffer = gpu::StructBuffer<AOParameters>;
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<BlurParameters>;
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<float, SSAO_RANDOM_SAMPLE_COUNT * SSAO_SPLIT_COUNT*SSAO_SPLIT_COUNT> _randomSamples;
int _frameId{ 0 };
bool _isJitterEnabled{ true };
gpu::RangeTimerPointer _gpuTimer;
@ -193,7 +256,7 @@ signals:
class DebugAmbientOcclusion {
public:
using Inputs = render::VaryingSet4<DeferredFrameTransformPointer, DeferredFramebufferPointer, LinearDepthFramebufferPointer, AmbientOcclusionEffect::ParametersBuffer>;
using Inputs = render::VaryingSet4<DeferredFrameTransformPointer, DeferredFramebufferPointer, LinearDepthFramebufferPointer, AmbientOcclusionEffect::AOParametersBuffer>;
using Config = DebugAmbientOcclusionConfig;
using JobModel = render::Job::ModelI<DebugAmbientOcclusion, Inputs, Config>;

View file

@ -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);

View file

@ -91,6 +91,7 @@ protected:
ScatteringDebugMode,
AmbientOcclusionMode,
AmbientOcclusionBlurredMode,
AmbientOcclusionNormalMode,
VelocityMode,
CustomMode, // Needs to stay last

View file

@ -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) {

View file

@ -11,6 +11,7 @@
#include "SurfaceGeometryPass.h"
#include <limits>
#include <MathUtils.h>
#include <gpu/Context.h>
#include <shaders/Shaders.h>
@ -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>();
}
_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>();
}
_surfaceGeometryFramebuffer->updateLinearDepth(linearDepthTexture);
_surfaceGeometryFramebuffer->update(linearDepthTexture);
auto curvatureFramebuffer = _surfaceGeometryFramebuffer->getCurvatureFramebuffer();
auto curvatureTexture = _surfaceGeometryFramebuffer->getCurvatureTexture();

View file

@ -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<LinearDepthFramebuffer>;
@ -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; }

View file

@ -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,

View file

@ -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<stepCount ; stepIndex++) {
fragUVPos += deltaTapUV;
radius += deltaRadius;
tapUVPos = snapToTexel(fragUVPos, sideDepthSize);
const int RADIUS_SCALE = 1;
const float BLUR_WEIGHT_OFFSET = 0.05;
const float BLUR_EDGE_SCALE = 2000.0;
<$computeHorizon()$>
}
#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@>
<@endif@>

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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());
<! // 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) * ((ssC.x & 1) - 0.5);
}
if (abs(dFdy(Cp.z)) < 0.02) {
A -= dFdy(A) * ((ssC.y & 1) - 0.5);
}
!>
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));
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -0,0 +1,64 @@
// <!
// Created by Olivier Prat on 2018/09/18
// Copyright 2013-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
// !>
// <@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@> <!def that !>
// <@endif@>
// Hack Comment

View file

@ -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);

View file

@ -25,7 +25,7 @@ LAYOUT(binding=0) uniform blurParamsBuffer {
BlurParameters parameters;
};
vec2 getViewportInvWidthHeight() {
vec2 getInvWidthHeight() {
return parameters.resolutionInfo.zw;
}

View file

@ -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;
};

View file

@ -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.

View file

@ -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]; }

View file

@ -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());
}

View file

@ -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());
}

View file

@ -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());
}

View file

@ -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());
}

View file

@ -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;
}
}

View file

@ -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<void()> operation);
void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args);

View file

@ -13,3 +13,4 @@ precision highp float;
precision highp samplerBuffer;
precision highp sampler2DShadow;
precision highp sampler2DArrayShadow;
precision lowp sampler2DArray;

View file

@ -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 <class T>
T divideRoundUp(const T& numerator, int divisor) {
return (numerator + divisor - T(1)) / divisor;
}
#endif // hifi_MathUtils_h

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
}
}
}
}
}
}
}
}

View file

@ -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();
});
}());

View file

@ -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" }
}

View file

@ -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();

View file

@ -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();

View file

@ -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

View file

@ -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 += ' ';

View file

@ -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()

View file

@ -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()

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -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](<https://hifi-content.s3.amazonaws.com/nissim/autoTester/AutoTester-Installer-v6.6.exe>).
1. On Windows: download the installer by browsing to [here](<https://hifi-content.s3.amazonaws.com/nissim/nitpick/nitpick-installer-v1.0.exe>).
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 <https://www.python.org/downloads/>
2. Linux (source) <https://www.python.org/downloads/release/python-370/> (**Gzipped source tarball**)
3. Mac <https://www.python.org/downloads/release/python-370/> (**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.

Some files were not shown because too many files have changed in this diff Show more