Merge branch 'master' of https://github.com/highfidelity/hifi into gltf_brainstem_fix

This commit is contained in:
raveenajain 2019-04-15 20:28:01 +01:00
commit f458427067
139 changed files with 1988 additions and 1592 deletions

View file

@ -64,6 +64,10 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
"UDP port for this assignment client (or monitor)", "port");
parser.addOption(portOption);
const QCommandLineOption minChildListenPort(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION,
"Minimum UDP listen port", "port");
parser.addOption(minChildListenPort);
const QCommandLineOption walletDestinationOption(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION,
"set wallet destination", "wallet-uuid");
parser.addOption(walletDestinationOption);
@ -195,6 +199,11 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
assignmentServerPort = parser.value(assignmentServerPortOption).toInt();
}
quint16 childMinListenPort = 0;
if (argumentVariantMap.contains(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION)) {
childMinListenPort = argumentVariantMap.value(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION).toUInt();
}
// check for an overidden listen port
quint16 listenPort = 0;
if (argumentVariantMap.contains(ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION)) {
@ -234,8 +243,8 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
if (numForks || minForks || maxForks) {
AssignmentClientMonitor* monitor = new AssignmentClientMonitor(numForks, minForks, maxForks,
requestAssignmentType, assignmentPool,
listenPort, walletUUID, assignmentServerHostname,
requestAssignmentType, assignmentPool, listenPort,
childMinListenPort, walletUUID, assignmentServerHostname,
assignmentServerPort, httpStatusPort, logDirectory);
monitor->setParent(this);
connect(this, &QCoreApplication::aboutToQuit, monitor, &AssignmentClientMonitor::aboutToQuit);

View file

@ -20,6 +20,7 @@ const QString ASSIGNMENT_POOL_OPTION = "pool";
const QString ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION = "p";
const QString ASSIGNMENT_WALLET_DESTINATION_ID_OPTION = "wallet";
const QString CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION = "a";
const QString ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION = "min-listen-port";
const QString CUSTOM_ASSIGNMENT_SERVER_PORT_OPTION = "server-port";
const QString ASSIGNMENT_NUM_FORKS_OPTION = "n";
const QString ASSIGNMENT_MIN_FORKS_OPTION = "min";

View file

@ -40,7 +40,7 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen
const unsigned int minAssignmentClientForks,
const unsigned int maxAssignmentClientForks,
Assignment::Type requestAssignmentType, QString assignmentPool,
quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 listenPort, quint16 childMinListenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory) :
_httpManager(QHostAddress::LocalHost, httpStatusServerPort, "", this),
_numAssignmentClientForks(numAssignmentClientForks),
@ -50,8 +50,8 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen
_assignmentPool(assignmentPool),
_walletUUID(walletUUID),
_assignmentServerHostname(assignmentServerHostname),
_assignmentServerPort(assignmentServerPort)
_assignmentServerPort(assignmentServerPort),
_childMinListenPort(childMinListenPort)
{
qDebug() << "_requestAssignmentType =" << _requestAssignmentType;
@ -100,8 +100,13 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) {
}
}
void AssignmentClientMonitor::childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus) {
auto message = "Child process " + QString::number(pid) + " has %1 with exit code " + QString::number(exitCode) + ".";
void AssignmentClientMonitor::childProcessFinished(qint64 pid, quint16 listenPort, int exitCode, QProcess::ExitStatus exitStatus) {
auto message = "Child process " + QString::number(pid) + " on port " + QString::number(listenPort) +
"has %1 with exit code " + QString::number(exitCode) + ".";
if (listenPort) {
_childListenPorts.remove(listenPort);
}
if (_childProcesses.remove(pid)) {
message.append(" Removed from internal map.");
@ -153,6 +158,23 @@ void AssignmentClientMonitor::aboutToQuit() {
void AssignmentClientMonitor::spawnChildClient() {
QProcess* assignmentClient = new QProcess(this);
quint16 listenPort = 0;
// allocate a port
if (_childMinListenPort) {
for (listenPort = _childMinListenPort; _childListenPorts.contains(listenPort); listenPort++) {
if (_maxAssignmentClientForks &&
(listenPort >= _maxAssignmentClientForks + _childMinListenPort)) {
listenPort = 0;
qDebug() << "Insufficient listen ports";
break;
}
}
}
if (listenPort) {
_childListenPorts.insert(listenPort);
}
// unparse the parts of the command-line that the child cares about
QStringList _childArguments;
if (_assignmentPool != "") {
@ -176,6 +198,11 @@ void AssignmentClientMonitor::spawnChildClient() {
_childArguments.append(QString::number(_requestAssignmentType));
}
if (listenPort) {
_childArguments.append("-" + ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION);
_childArguments.append(QString::number(listenPort));
}
// tell children which assignment monitor port to use
// for now they simply talk to us on localhost
_childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION);
@ -247,8 +274,8 @@ void AssignmentClientMonitor::spawnChildClient() {
auto pid = assignmentClient->processId();
// make sure we hear that this process has finished when it does
connect(assignmentClient, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
this, [this, pid](int exitCode, QProcess::ExitStatus exitStatus) {
childProcessFinished(pid, exitCode, exitStatus);
this, [this, listenPort, pid](int exitCode, QProcess::ExitStatus exitStatus) {
childProcessFinished(pid, listenPort, exitCode, exitStatus);
});
qDebug() << "Spawned a child client with PID" << assignmentClient->processId();

View file

@ -37,14 +37,15 @@ class AssignmentClientMonitor : public QObject, public HTTPRequestHandler {
public:
AssignmentClientMonitor(const unsigned int numAssignmentClientForks, const unsigned int minAssignmentClientForks,
const unsigned int maxAssignmentClientForks, Assignment::Type requestAssignmentType,
QString assignmentPool, quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory);
QString assignmentPool, quint16 listenPort, quint16 childMinListenPort, QUuid walletUUID,
QString assignmentServerHostname, quint16 assignmentServerPort, quint16 httpStatusServerPort,
QString logDirectory);
~AssignmentClientMonitor();
void stopChildProcesses();
private slots:
void checkSpares();
void childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus);
void childProcessFinished(qint64 pid, quint16 port, int exitCode, QProcess::ExitStatus exitStatus);
void handleChildStatusPacket(QSharedPointer<ReceivedMessage> message);
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override;
@ -75,6 +76,9 @@ private:
QMap<qint64, ACProcess> _childProcesses;
quint16 _childMinListenPort;
QSet<quint16> _childListenPorts;
bool _wantsChildFileLogging { false };
};

View file

@ -98,7 +98,8 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
PacketType::RequestsDomainListData,
PacketType::PerAvatarGainSet,
PacketType::InjectorGainSet,
PacketType::AudioSoloRequest },
PacketType::AudioSoloRequest,
PacketType::StopInjector },
this, "queueAudioPacket");
// packets whose consequences are global should be processed on the main thread
@ -246,7 +247,8 @@ void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) {
if (injectorClientData) {
// stage the removal of this stream, workers handle when preparing mixes for listeners
_workerSharedData.removedStreams.emplace_back(injectorClientData->getNodeID(), injectorClientData->getNodeLocalID(),
_workerSharedData.removedStreams.emplace_back(injectorClientData->getNodeID(),
injectorClientData->getNodeLocalID(),
streamID);
}
}
@ -586,8 +588,8 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) {
// check the payload to see if we have asked for dynamicJitterBuffer support
const QString DYNAMIC_JITTER_BUFFER_JSON_KEY = "dynamic_jitter_buffer";
bool enableDynamicJitterBuffer = audioBufferGroupObject[DYNAMIC_JITTER_BUFFER_JSON_KEY].toBool();
if (enableDynamicJitterBuffer) {
qCDebug(audio) << "Enabling dynamic jitter buffers.";
if (!enableDynamicJitterBuffer) {
qCDebug(audio) << "Disabling dynamic jitter buffers.";
bool ok;
const QString DESIRED_JITTER_BUFFER_FRAMES_KEY = "static_desired_jitter_buffer_frames";
@ -597,7 +599,7 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) {
}
qCDebug(audio) << "Static desired jitter buffer frames:" << _numStaticJitterFrames;
} else {
qCDebug(audio) << "Disabling dynamic jitter buffers.";
qCDebug(audio) << "Enabling dynamic jitter buffers.";
_numStaticJitterFrames = DISABLE_STATIC_JITTER_FRAMES;
}

View file

@ -104,6 +104,9 @@ int AudioMixerClientData::processPackets(ConcurrentAddedStreams& addedStreams) {
case PacketType::AudioSoloRequest:
parseSoloRequest(packet, node);
break;
case PacketType::StopInjector:
parseStopInjectorPacket(packet);
break;
default:
Q_UNREACHABLE();
}
@ -574,6 +577,19 @@ int AudioMixerClientData::checkBuffersBeforeFrameSend() {
return (int)_audioStreams.size();
}
void AudioMixerClientData::parseStopInjectorPacket(QSharedPointer<ReceivedMessage> packet) {
auto streamID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
auto it = std::find_if(std::begin(_audioStreams), std::end(_audioStreams), [&](auto stream) {
return streamID == stream->getStreamIdentifier();
});
if (it != std::end(_audioStreams)) {
_audioStreams.erase(it);
emit injectorStreamFinished(streamID);
}
}
bool AudioMixerClientData::shouldSendStats(int frameNumber) {
return frameNumber == _frameToSendStats;
}

View file

@ -67,12 +67,11 @@ public:
void parseNodeIgnoreRequest(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& node);
void parseRadiusIgnoreRequest(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& node);
void parseSoloRequest(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& node);
void parseStopInjectorPacket(QSharedPointer<ReceivedMessage> packet);
// attempt to pop a frame from each audio stream, and return the number of streams from this client
int checkBuffersBeforeFrameSend();
void removeDeadInjectedStreams();
QJsonObject getAudioStreamStats();
void sendAudioStreamStatsPackets(const SharedNodePointer& destinationNode);
@ -163,7 +162,7 @@ public:
// end of methods called non-concurrently from single AudioMixerSlave
signals:
void injectorStreamFinished(const QUuid& streamIdentifier);
void injectorStreamFinished(const QUuid& streamID);
public slots:
void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec);

View file

@ -549,38 +549,28 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre
// grab the stream from the ring buffer
AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd->getLastPopOutput();
// stereo sources are not passed through HRTF
if (streamToAdd->isStereo()) {
// apply the avatar gain adjustment
gain *= mixableStream.hrtf->getGainAdjustment();
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
const float scale = 1 / 32768.0f; // int16_t to float
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
_mixSamples[2*i+0] += (float)streamPopOutput[2*i+0] * gain * scale;
_mixSamples[2*i+1] += (float)streamPopOutput[2*i+1] * gain * scale;
}
// stereo sources are not passed through HRTF
mixableStream.hrtf->mixStereo(_bufferSamples, _mixSamples, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.manualStereoMixes;
} else if (isEcho) {
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
// echo sources are not passed through HRTF
const float scale = 1/32768.0f; // int16_t to float
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
float sample = (float)streamPopOutput[i] * gain * scale;
_mixSamples[2*i+0] += sample;
_mixSamples[2*i+1] += sample;
}
mixableStream.hrtf->mixMono(_bufferSamples, _mixSamples, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.manualEchoMixes;
} else {
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
mixableStream.hrtf->render(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.hrtfRenders;
}
}

View file

@ -17,8 +17,8 @@ if (WIN32)
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.35.0.zip
URL_MD5 1e3e8b2101387af07ff9c841d0ea285e
URL https://public.highfidelity.com/dependencies/ovr_sdk_win_1.26.0_public.zip
URL_MD5 06804ff9727b910dcd04a37c800053b5
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" <SOURCE_DIR>/CMakeLists.txt
LOG_DOWNLOAD 1

View file

@ -481,3 +481,15 @@ function prepareAccessTokenPrompt(callback) {
swal.close();
});
}
function getMetaverseUrl(callback) {
$.ajax('/api/metaverse_info', {
success: function(data) {
callback(data.metaverse_url);
},
error: function() {
callback(URLs.METAVERSE_URL);
}
});
}

View file

@ -16,47 +16,55 @@ $(document).ready(function(){
Settings.extraGroupsAtEnd = Settings.extraDomainGroupsAtEnd;
Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex;
var METAVERSE_URL = URLs.METAVERSE_URL;
Settings.afterReloadActions = function() {
// call our method to setup the HF account button
setupHFAccountButton();
// call our method to setup the place names table
setupPlacesTable();
getMetaverseUrl(function(metaverse_url) {
METAVERSE_URL = metaverse_url;
setupDomainNetworkingSettings();
// setupDomainLabelSetting();
// call our method to setup the HF account button
setupHFAccountButton();
setupSettingsBackup();
// call our method to setup the place names table
setupPlacesTable();
if (domainIDIsSet()) {
// now, ask the API for what places, if any, point to this domain
reloadDomainInfo();
setupDomainNetworkingSettings();
// setupDomainLabelSetting();
// we need to ask the API what a shareable name for this domain is
getShareName(function(success, shareName) {
if (success) {
var shareLink = "https://hifi.place/" + shareName;
$('#visit-domain-link').attr("href", shareLink).show();
}
});
}
setupSettingsBackup();
if (Settings.data.values.wizard.cloud_domain) {
$('#manage-cloud-domains-link').show();
if (domainIDIsSet()) {
// now, ask the API for what places, if any, point to this domain
reloadDomainInfo();
var cloudWizardExit = qs["cloud-wizard-exit"];
if (cloudWizardExit != undefined) {
$('#cloud-domains-alert').show();
// we need to ask the API what a shareable name for this domain is
getShareName(function(success, shareName) {
if (success) {
var shareLink = "https://hifi.place/" + shareName;
$('#visit-domain-link').attr("href", shareLink).show();
}
});
} else if (accessTokenIsSet()) {
$('#' + Settings.GET_TEMPORARY_NAME_BTN_ID).show();
}
$(Settings.DOMAIN_ID_SELECTOR).siblings('span').append("</br><strong>Changing the domain ID for a Cloud Domain may result in an incorrect status for the domain on your Cloud Domains page.</strong>");
} else {
// append the domain selection modal
appendDomainIDButtons();
}
if (Settings.data.values.wizard.cloud_domain) {
$('#manage-cloud-domains-link').show();
handleAction();
var cloudWizardExit = qs["cloud-wizard-exit"];
if (cloudWizardExit != undefined) {
$('#cloud-domains-alert').show();
}
$(Settings.DOMAIN_ID_SELECTOR).siblings('span').append("</br><strong>Changing the domain ID for a Cloud Domain may result in an incorrect status for the domain on your Cloud Domains page.</strong>");
} else {
// append the domain selection modal
appendDomainIDButtons();
}
handleAction();
});
}
Settings.handlePostSettings = function(formJSON) {
@ -258,7 +266,7 @@ $(document).ready(function(){
buttonSetting.button_label = "Connect High Fidelity Account";
buttonSetting.html_id = Settings.CONNECT_ACCOUNT_BTN_ID;
buttonSetting.href = URLs.METAVERSE_URL + "/user/tokens/new?for_domain_server=true";
buttonSetting.href = METAVERSE_URL + "/user/tokens/new?for_domain_server=true";
// since we do not have an access token we change hide domain ID and auto networking settings
// without an access token niether of them can do anything
@ -645,7 +653,7 @@ $(document).ready(function(){
label: 'Places',
html_id: Settings.PLACES_TABLE_ID,
help: "The following places currently point to this domain.</br>To point places to this domain, "
+ " go to the <a href='" + URLs.METAVERSE_URL + "/user/places'>My Places</a> "
+ " go to the <a href='" + METAVERSE_URL + "/user/places'>My Places</a> "
+ "page in your High Fidelity Metaverse account.",
read_only: true,
can_add_new_rows: false,
@ -678,12 +686,9 @@ $(document).ready(function(){
var errorEl = createDomainLoadingError("There was an error retrieving your places.");
$("#" + Settings.PLACES_TABLE_ID).after(errorEl);
// do we have a domain ID?
if (!domainIDIsSet()) {
// we don't have a domain ID - add a button to offer the user a chance to get a temporary one
var temporaryPlaceButton = dynamicButton(Settings.GET_TEMPORARY_NAME_BTN_ID, 'Get a temporary place name');
$('#' + Settings.PLACES_TABLE_ID).after(temporaryPlaceButton);
}
var temporaryPlaceButton = dynamicButton(Settings.GET_TEMPORARY_NAME_BTN_ID, 'Get a temporary place name');
temporaryPlaceButton.hide();
$('#' + Settings.PLACES_TABLE_ID).after(temporaryPlaceButton);
if (accessTokenIsSet()) {
appendAddButtonToPlacesTable();
}
@ -774,8 +779,9 @@ $(document).ready(function(){
// check if we have owner_places (for a real domain) or a name (for a temporary domain)
if (data.status == "success") {
$('#' + Settings.GET_TEMPORARY_NAME_BTN_ID).hide();
$('.domain-loading-hide').show();
if (data.domain.owner_places) {
if (data.domain.owner_places && data.domain.owner_places.length > 0) {
// add a table row for each of these names
_.each(data.domain.owner_places, function(place){
$('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRowForPlaceObject(place));
@ -783,8 +789,9 @@ $(document).ready(function(){
} else if (data.domain.name) {
// add a table row for this temporary domain name
$('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRow(data.domain.name, '/', true));
} else {
$('#' + Settings.GET_TEMPORARY_NAME_BTN_ID).show();
}
// Update label
if (showOrHideLabel()) {
var label = data.domain.label;
@ -953,7 +960,7 @@ $(document).ready(function(){
modal_buttons["success"] = {
label: 'Create new domain',
callback: function() {
window.open(URLs.METAVERSE_URL + "/user/domains", '_blank');
window.open(METAVERSE_URL + "/user/domains", '_blank');
}
}
modal_body = "<p>You do not have any domains in your High Fidelity account." +
@ -1001,7 +1008,7 @@ $(document).ready(function(){
showSpinnerAlert('Creating temporary place name');
// make a get request to get a temporary domain
$.post(URLs.METAVERSE_URL + '/api/v1/domains/temporary', function(data){
$.post(METAVERSE_URL + '/api/v1/domains/temporary', function(data){
if (data.status == "success") {
var domain = data.data.domain;

View file

@ -1916,6 +1916,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_SETTINGS = "/settings";
const QString URI_CONTENT_UPLOAD = "/content/upload";
const QString URI_RESTART = "/restart";
const QString URI_API_METAVERSE_INFO = "/api/metaverse_info";
const QString URI_API_PLACES = "/api/places";
const QString URI_API_DOMAINS = "/api/domains";
const QString URI_API_DOMAINS_ID = "/api/domains/";
@ -2164,6 +2165,15 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
} else if (url.path() == URI_RESTART) {
connection->respond(HTTPConnection::StatusCode200);
restart();
return true;
} else if (url.path() == URI_API_METAVERSE_INFO) {
QJsonObject rootJSON {
{ "metaverse_url", NetworkingConstants::METAVERSE_SERVER_URL().toString() }
};
QJsonDocument docJSON{ rootJSON };
connectionPtr->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8());
return true;
} else if (url.path() == URI_API_DOMAINS) {
return forwardMetaverseAPIRequest(connection, "/api/v1/domains", "");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 267 KiB

View file

@ -53,6 +53,16 @@
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
}
#image_button {
position: absolute;
width: 463;
height: 410;
top: 155;
left: 8;
right: 8;
bottom: 146;
}
#report_problem {
position: fixed;
@ -67,17 +77,23 @@
var handControllerImageURL = null;
var index = 0;
var count = 3;
var handControllerRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#vr-controls";
var keyboardRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/desktop.html#movement-controls";
var gamepadRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#gamepad";
function showKbm() {
document.getElementById("main_image").setAttribute("src", "img/tablet-help-keyboard.jpg");
document.getElementById("image_button").setAttribute("href", keyboardRefURL);
}
function showHandControllers() {
document.getElementById("main_image").setAttribute("src", handControllerImageURL);
document.getElementById("image_button").setAttribute("href", handControllerRefURL);
}
function showGamepad() {
document.getElementById("main_image").setAttribute("src", "img/tablet-help-gamepad.jpg");
document.getElementById("image_button").setAttribute("href", gamepadRefURL);
}
function cycleRight() {
@ -171,6 +187,7 @@
<img id="main_image" src="img/tablet-help-keyboard.jpg" width="480px" height="720px"></img>
<a href="#" id="left_button" onmousedown="cycleLeft()"></a>
<a href="#" id="right_button" onmousedown="cycleRight()"></a>
<a href="#" id="image_button"></a>
</div>
<a href="mailto:support@highfidelity.com" id="report_problem">Report Problem</a>
</body>

View file

@ -336,6 +336,8 @@ Rectangle {
case Qt.Key_Return:
case Qt.Key_Enter:
event.accepted = true;
keypressTimer.stop();
root.searchString = searchField.text;
searchField.text = "";
getMarketplaceItems();

View file

@ -1451,6 +1451,34 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_overlays.init(); // do this before scripts load
DependencyManager::set<ContextOverlayInterface>();
auto offscreenUi = getOffscreenUI();
connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() {
// Now that we've loaded the menu and thus switched to the previous display plugin
// we can unlock the desktop repositioning code, since all the positions will be
// relative to the desktop size for this plugin
auto offscreenUi = getOffscreenUI();
auto desktop = offscreenUi->getDesktop();
if (desktop) {
desktop->setProperty("repositionLocked", false);
}
});
connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() {
#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML)
// Do not show login dialog if requested not to on the command line
QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY);
int index = arguments().indexOf(hifiNoLoginCommandLineKey);
if (index != -1) {
resumeAfterLoginDialogActionTaken();
return;
}
showLoginScreen();
#else
resumeAfterLoginDialogActionTaken();
#endif
});
// Initialize the user interface and menu system
// Needs to happen AFTER the render engine initialization to access its configuration
initializeUi();
@ -1805,34 +1833,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
updateVerboseLogging();
// Now that we've loaded the menu and thus switched to the previous display plugin
// we can unlock the desktop repositioning code, since all the positions will be
// relative to the desktop size for this plugin
auto offscreenUi = getOffscreenUI();
connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() {
auto offscreenUi = getOffscreenUI();
auto desktop = offscreenUi->getDesktop();
if (desktop) {
desktop->setProperty("repositionLocked", false);
}
});
connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() {
#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML)
// Do not show login dialog if requested not to on the command line
QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY);
int index = arguments().indexOf(hifiNoLoginCommandLineKey);
if (index != -1) {
resumeAfterLoginDialogActionTaken();
return;
}
showLoginScreen();
#else
resumeAfterLoginDialogActionTaken();
#endif
});
// Make sure we don't time out during slow operations at startup
updateHeartbeat();
QTimer* settingsTimer = new QTimer();
@ -3981,6 +3981,15 @@ static void dumpEventQueue(QThread* thread) {
}
#endif // DEBUG_EVENT_QUEUE
bool Application::notify(QObject * object, QEvent * event) {
if (thread() == QThread::currentThread()) {
PROFILE_RANGE_IF_LONGER(app, "notify", 2)
return QApplication::notify(object, event);
}
return QApplication::notify(object, event);
}
bool Application::event(QEvent* event) {
if (_aboutToQuit) {
@ -5472,6 +5481,13 @@ void Application::pauseUntilLoginDetermined() {
// disconnect domain handler.
nodeList->getDomainHandler().disconnect();
// From now on, it's permissible to call resumeAfterLoginDialogActionTaken()
_resumeAfterLoginDialogActionTaken_SafeToRun = true;
if (_resumeAfterLoginDialogActionTaken_WasPostponed) {
// resumeAfterLoginDialogActionTaken() was already called, but it aborted. Now it's safe to call it again.
resumeAfterLoginDialogActionTaken();
}
}
void Application::resumeAfterLoginDialogActionTaken() {
@ -5480,6 +5496,11 @@ void Application::resumeAfterLoginDialogActionTaken() {
return;
}
if (!_resumeAfterLoginDialogActionTaken_SafeToRun) {
_resumeAfterLoginDialogActionTaken_WasPostponed = true;
return;
}
if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) {
auto toolbar = DependencyManager::get<ToolbarScriptingInterface>()->getToolbar("com.highfidelity.interface.toolbar.system");
toolbar->writeProperty("visible", true);
@ -6723,11 +6744,6 @@ void Application::updateRenderArgs(float deltaTime) {
// Configure the type of display / stereo
appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR);
}
appRenderArgs._renderArgs._stencilMode = getActiveDisplayPlugin()->getStencilMaskMode();
if (appRenderArgs._renderArgs._stencilMode == StencilMode::MESH) {
appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator();
}
}
{

View file

@ -1,4 +1,4 @@
//
//
// Application.h
// interface/src
//
@ -156,6 +156,7 @@ public:
void updateCamera(RenderArgs& renderArgs, float deltaTime);
void resizeGL();
bool notify(QObject *, QEvent *) override;
bool event(QEvent* event) override;
bool eventFilter(QObject* object, QEvent* event) override;
@ -807,5 +808,8 @@ private:
bool _showTrackedObjects { false };
bool _prevShowTrackedObjects { false };
bool _resumeAfterLoginDialogActionTaken_WasPostponed { false };
bool _resumeAfterLoginDialogActionTaken_SafeToRun { false };
};
#endif // hifi_Application_h

View file

@ -19,14 +19,22 @@ class FancyCamera : public Camera {
Q_OBJECT
/**jsdoc
* @namespace
* @augments Camera
*/
// FIXME: JSDoc 3.5.5 doesn't augment @property definitions. The following definition is repeated in Camera.h.
/**jsdoc
* @property {Uuid} cameraEntity The ID of the entity that the camera position and orientation follow when the camera is in
* entity mode.
* The <code>Camera</code> API provides access to the "camera" that defines your view in desktop and HMD display modes.
*
* @namespace Camera
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @property {Vec3} position - The position of the camera. You can set this value only when the camera is in independent
* mode.
* @property {Quat} orientation - The orientation of the camera. You can set this value only when the camera is in
* independent mode.
* @property {Camera.Mode} mode - The camera mode.
* @property {ViewFrustum} frustum - The camera frustum.
* @property {Uuid} cameraEntity - The ID of the entity that is used for the camera position and orientation when the
* camera is in entity mode.
*/
Q_PROPERTY(QUuid cameraEntity READ getCameraEntity WRITE setCameraEntity)
@ -38,25 +46,25 @@ public:
public slots:
/**jsdoc
* Get the ID of the entity that the camera is set to use the position and orientation from when it's in entity mode. You can
* also get the entity ID using the <code>Camera.cameraEntity</code> property.
* @function Camera.getCameraEntity
* @returns {Uuid} The ID of the entity that the camera is set to follow when in entity mode; <code>null</code> if no camera
* entity has been set.
*/
/**jsdoc
* Gets the ID of the entity that the camera is set to follow (i.e., use the position and orientation from) when it's in
* entity mode. You can also get the entity ID using the {@link Camera|Camera.cameraEntity} property.
* @function Camera.getCameraEntity
* @returns {Uuid} The ID of the entity that the camera is set to follow when in entity mode; <code>null</code> if no
* camera entity has been set.
*/
QUuid getCameraEntity() const;
/**jsdoc
* Set the entity that the camera should use the position and orientation from when it's in entity mode. You can also set the
* entity using the <code>Camera.cameraEntity</code> property.
* @function Camera.setCameraEntity
* @param {Uuid} entityID The entity that the camera should follow when it's in entity mode.
* @example <caption>Move your camera to the position and orientation of the closest entity.</caption>
* Camera.setModeString("entity");
* var entity = Entities.findClosestEntity(MyAvatar.position, 100.0);
* Camera.setCameraEntity(entity);
*/
* Sets the entity that the camera should follow (i.e., use the position and orientation from) when it's in entity mode.
* You can also set the entity using the {@link Camera|Camera.cameraEntity} property.
* @function Camera.setCameraEntity
* @param {Uuid} entityID - The entity that the camera should follow when it's in entity mode.
* @example <caption>Move your camera to the position and orientation of the closest entity.</caption>
* Camera.setModeString("entity");
* var entity = Entities.findClosestEntity(MyAvatar.position, 100.0);
* Camera.setCameraEntity(entity);
*/
void setCameraEntity(QUuid entityID);
private:

View file

@ -116,7 +116,7 @@ Menu::Menu() {
// Edit > Delete
auto deleteAction = addActionToQMenuAndActionHash(editMenu, "Delete", QKeySequence::Delete);
connect(deleteAction, &QAction::triggered, [] {
QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Delete, Qt::ControlModifier);
QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
QCoreApplication::postEvent(QCoreApplication::instance(), keyEvent);
});

View file

@ -497,7 +497,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
// it might not fire until after we create a new instance for the same remote avatar, which creates a race
// on the creation of entities for that avatar instance and the deletion of entities for this instance
avatar->removeAvatarEntitiesFromTree();
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == KillAvatarReason::NoReason) {
if (removalReason != KillAvatarReason::AvatarDisconnected) {
emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID());
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
@ -509,7 +509,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
render::Transaction transaction;
avatar->removeFromScene(avatar, scene, transaction);
scene->enqueueTransaction(transaction);
} else if (removalReason == KillAvatarReason::AvatarDisconnected) {
} else {
// remove from node sets, if present
DependencyManager::get<NodeList>()->removeFromIgnoreMuteSets(avatar->getSessionUUID());
DependencyManager::get<UsersScriptingInterface>()->avatarDisconnected(avatar->getSessionUUID());
@ -932,6 +932,18 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV
}
}
/**jsdoc
* PAL (People Access List) data for an avatar.
* @typedef {object} AvatarManager.PalData
* @property {Uuid} sessionUUID - The avatar's session ID. <code>""</code> if the avatar is your own.
* @property {string} sessionDisplayName - The avatar's display name, sanitized and versioned, as defined by the avatar mixer.
* It is unique among all avatars present in the domain at the time.
* @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the
* domain.
* @property {boolean} isReplicated - <strong>Deprecated.</strong>
* @property {Vec3} position - The position of the avatar.
* @property {number} palOrbOffset - The vertical offset from the avatar's position that an overlay orb should be displayed at.
*/
QVariantMap AvatarManager::getPalData(const QStringList& specificAvatarIdentifiers) {
QJsonArray palData;

View file

@ -37,10 +37,11 @@
using SortedAvatar = std::pair<float, std::shared_ptr<Avatar>>;
/**jsdoc
* The AvatarManager API has properties and methods which manage Avatars within the same domain.
* The <code>AvatarManager</code> API provides information about avatars within the current domain. The avatars available are
* those that Interface has displayed and therefore knows about.
*
* <p><strong>Note:</strong> This API is also provided to Interface and client entity scripts as the synonym,
* <code>AvatarList</code>. For assignment client scripts, see the separate {@link AvatarList} API.
* <p><strong>Warning:</strong> This API is also provided to Interface, client entity, and avatar scripts as the synonym,
* "<code>AvatarList</code>". For assignment client scripts, see the separate {@link AvatarList} API.</p>
*
* @namespace AvatarManager
*
@ -48,8 +49,9 @@ using SortedAvatar = std::pair<float, std::shared_ptr<Avatar>>;
* @hifi-client-entity
* @hifi-avatar
*
* @borrows AvatarList.getAvatarIdentifiers as getAvatarIdentifiers
* @borrows AvatarList.getAvatarsInRange as getAvatarsInRange
* @borrows AvatarList.getAvatar as getAvatar
* @comment AvatarList.getAvatarIdentifiers as getAvatarIdentifiers - Don't borrow because behavior is slightly different.
* @comment AvatarList.getAvatarsInRange as getAvatarsInRange - Don't borrow because behavior is slightly different.
* @borrows AvatarList.avatarAddedEvent as avatarAddedEvent
* @borrows AvatarList.avatarRemovedEvent as avatarRemovedEvent
* @borrows AvatarList.avatarSessionChangedEvent as avatarSessionChangedEvent
@ -67,6 +69,31 @@ class AvatarManager : public AvatarHashMap {
public:
/**jsdoc
* Gets the IDs of all avatars known about in the domain.
* Your own avatar is included in the list as a <code>null</code> value.
* @function AvatarManager.getAvatarIdentifiers
* @returns {Uuid[]} The IDs of all known avatars in the domain.
* @example <caption>Report the IDS of all avatars within the domain.</caption>
* var avatars = AvatarManager.getAvatarIdentifiers();
* print("Avatars in the domain: " + JSON.stringify(avatars));
* // A null item is included for your avatar.
*/
/**jsdoc
* Gets the IDs of all avatars known about within a specified distance from a point.
* Your own avatar's ID is included in the list if it is in range.
* @function AvatarManager.getAvatarsInRange
* @param {Vec3} position - The point about which the search is performed.
* @param {number} range - The search radius.
* @returns {Uuid[]} The IDs of all known avatars within the search distance from the position.
* @example <caption>Report the IDs of all avatars within 10m of your avatar.</caption>
* var RANGE = 10;
* var avatars = AvatarManager.getAvatarsInRange(MyAvatar.position, RANGE);
* print("Nearby avatars: " + JSON.stringify(avatars));
* print("Own avatar: " + MyAvatar.sessionUUID);
*/
/// Registers the script types associated with the avatar manager.
static void registerMetaTypes(QScriptEngine* engine);
@ -79,9 +106,7 @@ public:
glm::vec3 getMyAvatarPosition() const { return _myAvatar->getWorldPosition(); }
/**jsdoc
* @function AvatarManager.getAvatar
* @param {Uuid} avatarID
* @returns {AvatarData}
* @comment Uses the base class's JSDoc.
*/
// Null/Default-constructed QUuids will return MyAvatar
Q_INVOKABLE virtual ScriptAvatarData* getAvatar(QUuid avatarID) override { return new ScriptAvatar(getAvatarBySessionID(avatarID)); }
@ -112,36 +137,53 @@ public:
void handleCollisionEvents(const CollisionEvents& collisionEvents);
/**jsdoc
* Gets the amount of avatar mixer data being generated by an avatar other than your own.
* @function AvatarManager.getAvatarDataRate
* @param {Uuid} sessionID
* @param {string} [rateName=""]
* @returns {number}
* @param {Uuid} sessionID - The ID of the avatar whose data rate you're retrieving.
* @param {AvatarDataRate} [rateName=""] - The type of avatar mixer data to get the data rate of.
* @returns {number} The data rate in kbps; <code>0</code> if the avatar is your own.
*/
Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
/**jsdoc
* Gets the update rate of avatar mixer data being generated by an avatar other than your own.
* @function AvatarManager.getAvatarUpdateRate
* @param {Uuid} sessionID
* @param {string} [rateName=""]
* @returns {number}
* @param {Uuid} sessionID - The ID of the avatar whose update rate you're retrieving.
* @param {AvatarUpdateRate} [rateName=""] - The type of avatar mixer data to get the update rate of.
* @returns {number} The update rate in Hz; <code>0</code> if the avatar is your own.
*/
Q_INVOKABLE float getAvatarUpdateRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
/**jsdoc
* Gets the simulation rate of an avatar other than your own.
* @function AvatarManager.getAvatarSimulationRate
* @param {Uuid} sessionID
* @param {string} [rateName=""]
* @returns {number}
* @param {Uuid} sessionID - The ID of the avatar whose simulation you're retrieving.
* @param {AvatarSimulationRate} [rateName=""] - The type of avatar data to get the simulation rate of.
* @returns {number} The simulation rate in Hz; <code>0</code> if the avatar is your own.
*/
Q_INVOKABLE float getAvatarSimulationRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
/**jsdoc
* Find the first avatar intersected by a {@link PickRay}.
* @function AvatarManager.findRayIntersection
* @param {PickRay} ray
* @param {Uuid[]} [avatarsToInclude=[]]
* @param {Uuid[]} [avatarsToDiscard=[]]
* @param {boolean} pickAgainstMesh
* @returns {RayToAvatarIntersectionResult}
* @param {PickRay} ray - The ray to use for finding avatars.
* @param {Uuid[]} [avatarsToInclude=[]] - If not empty then search is restricted to these avatars.
* @param {Uuid[]} [avatarsToDiscard=[]] - Avatars to ignore in the search.
* @param {boolean} [pickAgainstMesh=true] - If <code>true</code> then the exact intersection with the avatar mesh is
* calculated, if <code>false</code> then the intersection is approximate.
* @returns {RayToAvatarIntersectionResult} The result of the search for the first intersected avatar.
* @example <caption>Find the first avatar directly in front of you.</caption>
* var pickRay = {
* origin: MyAvatar.position,
* direction: Quat.getFront(MyAvatar.orientation)
* };
*
* var intersection = AvatarManager.findRayIntersection(pickRay);
* if (intersection.intersects) {
* print("Avatar found: " + JSON.stringify(intersection));
* } else {
* print("No avatar found.");
* }
*/
Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersection(const PickRay& ray,
const QScriptValue& avatarIdsToInclude = QScriptValue(),
@ -149,11 +191,12 @@ public:
bool pickAgainstMesh = true);
/**jsdoc
* @function AvatarManager.findRayIntersectionVector
* @param {PickRay} ray
* @param {Uuid[]} avatarsToInclude
* @param {Uuid[]} avatarsToDiscard
* @param {boolean} pickAgainstMesh
* @returns {RayToAvatarIntersectionResult}
* @param {PickRay} ray - Ray.
* @param {Uuid[]} avatarsToInclude - Avatars to include.
* @param {Uuid[]} avatarsToDiscard - Avatars to discard.
* @param {boolean} pickAgainstMesh - Pick against mesh.
* @returns {RayToAvatarIntersectionResult} Intersection result.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersectionVector(const PickRay& ray,
const QVector<EntityItemID>& avatarsToInclude,
@ -162,10 +205,11 @@ public:
/**jsdoc
* @function AvatarManager.findParabolaIntersectionVector
* @param {PickParabola} pick
* @param {Uuid[]} avatarsToInclude
* @param {Uuid[]} avatarsToDiscard
* @returns {ParabolaToAvatarIntersectionResult}
* @param {PickParabola} pick - Pick.
* @param {Uuid[]} avatarsToInclude - Avatars to include.
* @param {Uuid[]} avatarsToDiscard - Avatars to discard.
* @returns {ParabolaToAvatarIntersectionResult} Intersection result.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE ParabolaToAvatarIntersectionResult findParabolaIntersectionVector(const PickParabola& pick,
const QVector<EntityItemID>& avatarsToInclude,
@ -173,27 +217,31 @@ public:
/**jsdoc
* @function AvatarManager.getAvatarSortCoefficient
* @param {string} name
* @returns {number}
* @param {string} name - Name.
* @returns {number} Value.
* @deprecated This function is deprecated and will be removed.
*/
// TODO: remove this HACK once we settle on optimal default sort coefficients
Q_INVOKABLE float getAvatarSortCoefficient(const QString& name);
/**jsdoc
* @function AvatarManager.setAvatarSortCoefficient
* @param {string} name
* @param {number} value
* @param {string} name - Name
* @param {number} value - Value.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value);
/**jsdoc
* Used in the PAL for getting PAL-related data about avatars nearby. Using this method is faster
* than iterating over each avatar and obtaining data about them in JavaScript, as that method
* locks and unlocks each avatar's data structure potentially hundreds of times per update tick.
* Gets PAL (People Access List) data for one or more avatars. Using this method is faster than iterating over each avatar
* and obtaining data about each individually.
* @function AvatarManager.getPalData
* @param {string[]} [specificAvatarIdentifiers=[]] - The list of IDs of the avatars you want the PAL data for.
* If an empty list, the PAL data for all nearby avatars is returned.
* @returns {object[]} An array of objects, each object being the PAL data for an avatar.
* @param {string[]} [avatarIDs=[]] - The IDs of the avatars to get the PAL data for. If empty, then PAL data is obtained
* for all avatars.
* @returns {object<"data", AvatarManager.PalData[]>} An array of objects, each object being the PAL data for an avatar.
* @example <caption>Report the PAL data for an avatar nearby.</caption>
* var palData = AvatarManager.getPalData();
* print("PAL data for one avatar: " + JSON.stringify(palData.data[0]));
*/
Q_INVOKABLE QVariantMap getPalData(const QStringList& specificAvatarIdentifiers = QStringList());
@ -209,7 +257,8 @@ public:
public slots:
/**jsdoc
* @function AvatarManager.updateAvatarRenderStatus
* @param {boolean} shouldRenderAvatars
* @param {boolean} shouldRenderAvatars - Should render avatars.
* @deprecated This function is deprecated and will be removed.
*/
void updateAvatarRenderStatus(bool shouldRenderAvatars);

View file

@ -818,20 +818,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
if (_cauterizationNeedsUpdate) {
_cauterizationNeedsUpdate = false;
// Redisplay cauterized entities that are no longer children of the avatar.
auto cauterizedChild = _cauterizedChildrenOfHead.begin();
if (cauterizedChild != _cauterizedChildrenOfHead.end()) {
auto children = getChildren();
while (cauterizedChild != _cauterizedChildrenOfHead.end()) {
if (!children.contains(*cauterizedChild)) {
updateChildCauterization(*cauterizedChild, false);
cauterizedChild = _cauterizedChildrenOfHead.erase(cauterizedChild);
} else {
++cauterizedChild;
}
}
}
auto objectsToUncauterize = _cauterizedChildrenOfHead;
_cauterizedChildrenOfHead.clear();
// Update cauterization of entities that are children of the avatar.
auto headBoneSet = _skeletonModel->getCauterizeBoneSet();
forEachChild([&](SpatiallyNestablePointer object) {
@ -843,15 +831,19 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
updateChildCauterization(descendant, !_prevShouldDrawHead);
});
_cauterizedChildrenOfHead.insert(object);
} else if (_cauterizedChildrenOfHead.find(object) != _cauterizedChildrenOfHead.end()) {
// Redisplay cauterized children that are not longer children of the head.
updateChildCauterization(object, false);
objectsToUncauterize.erase(object);
} else if (objectsToUncauterize.find(object) == objectsToUncauterize.end()) {
objectsToUncauterize.insert(object);
object->forEachDescendant([&](SpatiallyNestablePointer descendant) {
updateChildCauterization(descendant, false);
objectsToUncauterize.insert(descendant);
});
_cauterizedChildrenOfHead.erase(object);
}
});
// Redisplay cauterized entities that are no longer children of the avatar.
for (auto cauterizedChild = objectsToUncauterize.begin(); cauterizedChild != objectsToUncauterize.end(); cauterizedChild++) {
updateChildCauterization(*cauterizedChild, false);
}
}
{
@ -3889,7 +3881,8 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette
// See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders
QVariantMap extraInfo;
EntityItemID entityID = entityTree->evalRayIntersection(startPointIn, directionIn, include, ignore,
PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE) | PickFilter::getBitMask(PickFilter::FlagBit::PRECISE)),
PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE) | PickFilter::getBitMask(PickFilter::FlagBit::PRECISE)
| PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES)), // exclude Local entities
element, distance, face, normalOut, extraInfo, lockType, accurateResult);
if (entityID.isNull()) {
return false;

View file

@ -334,7 +334,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
eyeParams.leftEyeJointIndex = _rig.indexOfJoint("LeftEye");
eyeParams.rightEyeJointIndex = _rig.indexOfJoint("RightEye");
_rig.updateFromEyeParameters(eyeParams);
if (_owningAvatar->getHasProceduralEyeFaceMovement()) {
_rig.updateFromEyeParameters(eyeParams);
}
updateFingers();
}

View file

@ -302,8 +302,11 @@ int main(int argc, const char* argv[]) {
PROFILE_SYNC_BEGIN(startup, "app full ctor", "");
Application app(argcExtended, const_cast<char**>(argvExtended.data()), startupTime, runningMarkerExisted);
PROFILE_SYNC_END(startup, "app full ctor", "");
#if defined(Q_OS_LINUX)
app.setWindowIcon(QIcon(PathUtils::resourcesPath() + "images/hifi-logo.svg"));
#endif
QTimer exitTimer;
if (traceDuration > 0.0f) {
exitTimer.setSingleShot(true);

View file

@ -156,10 +156,10 @@ void DialogsManager::hmdTools(bool showTools) {
}
_hmdToolsDialog->show();
_hmdToolsDialog->raise();
qApp->getWindow()->activateWindow();
} else {
hmdToolsClosed();
}
qApp->getWindow()->activateWindow();
}
void DialogsManager::hmdToolsClosed() {
@ -207,4 +207,4 @@ void DialogsManager::showDomainConnectionDialog() {
_domainConnectionDialog->show();
_domainConnectionDialog->raise();
}
}

View file

@ -45,18 +45,6 @@ private:
Q_DECLARE_METATYPE(AnimationPointer)
/**jsdoc
* @class AnimationObject
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
* @hifi-server-entity
* @hifi-assignment-client
*
* @property {string[]} jointNames
* @property {FBXAnimationFrame[]} frames
*/
/// An animation loaded from the network.
class Animation : public Resource {
Q_OBJECT
@ -72,16 +60,8 @@ public:
virtual bool isLoaded() const override;
/**jsdoc
* @function AnimationObject.getJointNames
* @returns {string[]}
*/
Q_INVOKABLE QStringList getJointNames() const;
/**jsdoc
* @function AnimationObject.getFrames
* @returns {FBXAnimationFrame[]}
*/
Q_INVOKABLE QVector<HFMAnimationFrame> getFrames() const;
const QVector<HFMAnimationFrame>& getFramesReference() const;

View file

@ -25,7 +25,8 @@ class AnimationCacheScriptingInterface : public ScriptableResourceCache, public
// Properties are copied over from ResourceCache (see ResourceCache.h for reason).
/**jsdoc
* API to manage animation cache resources.
* The <code>AnimationCache</code> API manages animation cache resources.
*
* @namespace AnimationCache
*
* @hifi-interface
@ -48,10 +49,10 @@ public:
AnimationCacheScriptingInterface();
/**jsdoc
* Returns animation resource for particular animation.
* Gets information about an animation resource.
* @function AnimationCache.getAnimation
* @param {string} url - URL to load.
* @returns {AnimationObject} animation
* @param {string} url - The URL of the animation.
* @returns {AnimationObject} An animation object.
*/
Q_INVOKABLE AnimationPointer getAnimation(const QString& url);
};

View file

@ -19,6 +19,20 @@
class QScriptEngine;
/**jsdoc
* Information about an animation resource, created by {@link AnimationCache.getAnimation}.
*
* @class AnimationObject
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
* @hifi-server-entity
* @hifi-assignment-client
*
* @property {string[]} jointNames - The names of the joints that are animated. <em>Read-only.</em>
* @property {AnimationFrameObject[]} frames - The frames in the animation. <em>Read-only.</em>
*/
/// Scriptable wrapper for animation pointers.
class AnimationObject : public QObject, protected QScriptable {
Q_OBJECT
@ -27,11 +41,34 @@ class AnimationObject : public QObject, protected QScriptable {
public:
/**jsdoc
* Gets the names of the joints that are animated.
* @function AnimationObject.getJointNames
* @returns {string[]} The names of the joints that are animated.
*/
Q_INVOKABLE QStringList getJointNames() const;
/**jsdoc
* Gets the frames in the animation.
* @function AnimationObject.getFrames
* @returns {AnimationFrameObject[]} The frames in the animation.
*/
Q_INVOKABLE QVector<HFMAnimationFrame> getFrames() const;
};
/**jsdoc
* Joint rotations in one frame of an animation.
*
* @class AnimationFrameObject
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
* @hifi-server-entity
* @hifi-assignment-client
*
* @property {Quat[]} rotations - Joint rotations. <em>Read-only.</em>
*/
/// Scriptable wrapper for animation frames.
class AnimationFrameObject : public QObject, protected QScriptable {
Q_OBJECT
@ -39,6 +76,11 @@ class AnimationFrameObject : public QObject, protected QScriptable {
public:
/**jsdoc
* Gets the joint rotations in the animation frame.
* @function AnimationFrameObject.getRotations
* @returns {Quat[]} The joint rotations in the animation frame.
*/
Q_INVOKABLE QVector<glm::quat> getRotations() const;
};

View file

@ -499,12 +499,12 @@ void Flow::calculateConstraints(const std::shared_ptr<AnimSkeleton>& skeleton,
bool toFloatSuccess;
QStringRef(&name, (int)(name.size() - j), 1).toString().toFloat(&toFloatSuccess);
if (!toFloatSuccess && (name.size() - j) > (int)simPrefix.size()) {
group = QStringRef(&name, (int)simPrefix.size(), (int)(name.size() - j + 1)).toString();
group = QStringRef(&name, (int)simPrefix.size(), (int)(name.size() - j + 1) - (int)simPrefix.size()).toString();
break;
}
}
if (group.isEmpty()) {
group = QStringRef(&name, (int)simPrefix.size(), name.size() - 1).toString();
group = QStringRef(&name, (int)simPrefix.size(), name.size() - (int)simPrefix.size()).toString();
}
qCDebug(animation) << "Sim joint added to flow: " << name;
} else {

View file

@ -1397,7 +1397,6 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
// spatialize into mixBuffer
injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX,
qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
} else if (options.stereo) {
if (options.positionSet) {
@ -1409,11 +1408,8 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
}
// direct mix into mixBuffer
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
mixBuffer[2*i+0] += convertToFloat(_localScratchBuffer[2*i+0]) * gain;
mixBuffer[2*i+1] += convertToFloat(_localScratchBuffer[2*i+1]) * gain;
}
injector->getLocalHRTF().mixStereo(_localScratchBuffer, mixBuffer, gain,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
} else { // injector is mono
if (options.positionSet) {
@ -1431,11 +1427,8 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
} else {
// direct mix into mixBuffer
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
float sample = convertToFloat(_localScratchBuffer[i]) * gain;
mixBuffer[2*i+0] += sample;
mixBuffer[2*i+1] += sample;
}
injector->getLocalHRTF().mixMono(_localScratchBuffer, mixBuffer, gain,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
}
}

View file

@ -882,14 +882,16 @@ static void convertInput_ref(int16_t* src, float *dst[4], float gain, int numFra
#endif
// in-place rotation of the soundfield
// crossfade between old and new rotation, to prevent artifacts
static void rotate_3x3_ref(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames) {
// in-place rotation and scaling of the soundfield
// crossfade between old and new matrix, to prevent artifacts
static void rotate_4x4_ref(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames) {
const float md[3][3] = {
{ m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2] },
{ m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2] },
{ m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2] },
// matrix difference
const float md[4][4] = {
{ m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2], m0[0][3] - m1[0][3] },
{ m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2], m0[1][3] - m1[1][3] },
{ m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2], m0[2][3] - m1[2][3] },
{ m0[3][0] - m1[3][0], m0[3][1] - m1[3][1], m0[3][2] - m1[3][2], m0[3][3] - m1[3][3] },
};
for (int i = 0; i < numFrames; i++) {
@ -898,22 +900,27 @@ static void rotate_3x3_ref(float* buf[4], const float m0[3][3], const float m1[3
// interpolate the matrix
float m00 = m1[0][0] + frac * md[0][0];
float m10 = m1[1][0] + frac * md[1][0];
float m20 = m1[2][0] + frac * md[2][0];
float m01 = m1[0][1] + frac * md[0][1];
float m11 = m1[1][1] + frac * md[1][1];
float m21 = m1[2][1] + frac * md[2][1];
float m31 = m1[3][1] + frac * md[3][1];
float m02 = m1[0][2] + frac * md[0][2];
float m12 = m1[1][2] + frac * md[1][2];
float m22 = m1[2][2] + frac * md[2][2];
float m32 = m1[3][2] + frac * md[3][2];
float m13 = m1[1][3] + frac * md[1][3];
float m23 = m1[2][3] + frac * md[2][3];
float m33 = m1[3][3] + frac * md[3][3];
// matrix multiply
float x = m00 * buf[1][i] + m01 * buf[2][i] + m02 * buf[3][i];
float y = m10 * buf[1][i] + m11 * buf[2][i] + m12 * buf[3][i];
float z = m20 * buf[1][i] + m21 * buf[2][i] + m22 * buf[3][i];
float w = m00 * buf[0][i];
float x = m11 * buf[1][i] + m12 * buf[2][i] + m13 * buf[3][i];
float y = m21 * buf[1][i] + m22 * buf[2][i] + m23 * buf[3][i];
float z = m31 * buf[1][i] + m32 * buf[2][i] + m33 * buf[3][i];
buf[0][i] = w;
buf[1][i] = x;
buf[2][i] = y;
buf[3][i] = z;
@ -932,7 +939,7 @@ void rfft512_AVX2(float buf[512]);
void rifft512_AVX2(float buf[512]);
void rfft512_cmadd_1X2_AVX2(const float src[512], const float coef0[512], const float coef1[512], float dst0[512], float dst1[512]);
void convertInput_AVX2(int16_t* src, float *dst[4], float gain, int numFrames);
void rotate_3x3_AVX2(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames);
void rotate_4x4_AVX2(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames);
static void rfft512(float buf[512]) {
static auto f = cpuSupportsAVX2() ? rfft512_AVX2 : rfft512_ref;
@ -954,8 +961,8 @@ static void convertInput(int16_t* src, float *dst[4], float gain, int numFrames)
(*f)(src, dst, gain, numFrames); // dispatch
}
static void rotate_3x3(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames) {
static auto f = cpuSupportsAVX2() ? rotate_3x3_AVX2 : rotate_3x3_ref;
static void rotate_4x4(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames) {
static auto f = cpuSupportsAVX2() ? rotate_4x4_AVX2 : rotate_4x4_ref;
(*f)(buf, m0, m1, win, numFrames); // dispatch
}
@ -965,7 +972,7 @@ static auto& rfft512 = rfft512_ref;
static auto& rifft512 = rifft512_ref;
static auto& rfft512_cmadd_1X2 = rfft512_cmadd_1X2_ref;
static auto& convertInput = convertInput_ref;
static auto& rotate_3x3 = rotate_3x3_ref;
static auto& rotate_4x4 = rotate_4x4_ref;
#endif
@ -1007,8 +1014,8 @@ ALIGN32 static const float crossfadeTable[FOA_BLOCK] = {
0.0020975362f, 0.0015413331f, 0.0010705384f, 0.0006852326f, 0.0003854819f, 0.0001713375f, 0.0000428362f, 0.0000000000f,
};
// convert quaternion to a column-major 3x3 rotation matrix
static void quatToMatrix_3x3(float w, float x, float y, float z, float m[3][3]) {
// convert quaternion to a column-major 4x4 rotation matrix
static void quatToMatrix_4x4(float w, float x, float y, float z, float m[4][4]) {
float xx = x * (x + x);
float xy = x * (y + y);
@ -1022,17 +1029,33 @@ static void quatToMatrix_3x3(float w, float x, float y, float z, float m[3][3])
float wy = w * (y + y);
float wz = w * (z + z);
m[0][0] = 1.0f - (yy + zz);
m[0][1] = xy - wz;
m[0][2] = xz + wy;
m[0][0] = 1.0f;
m[0][1] = 0.0f;
m[0][2] = 0.0f;
m[0][3] = 0.0f;
m[1][0] = xy + wz;
m[1][1] = 1.0f - (xx + zz);
m[1][2] = yz - wx;
m[1][0] = 0.0f;
m[1][1] = 1.0f - (yy + zz);
m[1][2] = xy - wz;
m[1][3] = xz + wy;
m[2][0] = xz - wy;
m[2][1] = yz + wx;
m[2][2] = 1.0f - (xx + yy);
m[2][0] = 0.0f;
m[2][1] = xy + wz;
m[2][2] = 1.0f - (xx + zz);
m[2][3] = yz - wx;
m[3][0] = 0.0f;
m[3][1] = xz - wy;
m[3][2] = yz + wx;
m[3][3] = 1.0f - (xx + yy);
}
static void scaleMatrix_4x4(float scale, float m[4][4]) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
m[i][j] *= scale;
}
}
}
// Ambisonic to binaural render
@ -1047,18 +1070,26 @@ void AudioFOA::render(int16_t* input, float* output, int index, float qw, float
ALIGN32 float inBuffer[4][FOA_BLOCK]; // deinterleaved input buffers
float* in[4] = { inBuffer[0], inBuffer[1], inBuffer[2], inBuffer[3] };
float rotation[3][3];
float rotation[4][4];
// convert input to deinterleaved float
convertInput(input, in, FOA_GAIN * gain, FOA_BLOCK);
convertInput(input, in, FOA_GAIN, FOA_BLOCK);
// convert quaternion to 3x3 rotation
quatToMatrix_3x3(qw, qx, qy, qz, rotation);
// convert quaternion to 4x4 rotation
quatToMatrix_4x4(qw, qx, qy, qz, rotation);
// rotate the soundfield
rotate_3x3(in, _rotationState, rotation, crossfadeTable, FOA_BLOCK);
// apply gain as uniform scale
scaleMatrix_4x4(gain, rotation);
// rotation history update
// disable interpolation from reset state
if (_resetState) {
memcpy(_rotationState, rotation, sizeof(_rotationState));
}
// rotate and scale the soundfield
rotate_4x4(in, _rotationState, rotation, crossfadeTable, FOA_BLOCK);
// new parameters become old
memcpy(_rotationState, rotation, sizeof(_rotationState));
//
@ -1093,4 +1124,6 @@ void AudioFOA::render(int16_t* input, float* output, int index, float qw, float
output[2*i+0] += accBuffer[0][i + FOA_OVERLAP];
output[2*i+1] += accBuffer[1][i + FOA_OVERLAP];
}
_resetState = false;
}

View file

@ -28,12 +28,7 @@ static_assert((FOA_BLOCK + FOA_OVERLAP) == FOA_NFFT, "FFT convolution requires L
class AudioFOA {
public:
AudioFOA() {
// identity matrix
_rotationState[0][0] = 1.0f;
_rotationState[1][1] = 1.0f;
_rotationState[2][2] = 1.0f;
};
AudioFOA() {};
//
// input: interleaved First-Order Ambisonic source
@ -55,8 +50,10 @@ private:
// input history, for overlap-save
float _fftState[4][FOA_OVERLAP] = {};
// orientation history
float _rotationState[3][3] = {};
// orientation and gain history
float _rotationState[4][4] = {};
bool _resetState = true;
};
#endif // AudioFOA_h

View file

@ -750,6 +750,43 @@ static void interpolate(const float* src0, const float* src1, float* dst, float
#endif
// apply gain crossfade with accumulation (interleaved)
static void gainfade_1x2(int16_t* src, float* dst, const float* win, float gain0, float gain1, int numFrames) {
gain0 *= (1/32768.0f); // int16_t to float
gain1 *= (1/32768.0f);
for (int i = 0; i < numFrames; i++) {
float frac = win[i];
float gain = gain1 + frac * (gain0 - gain1);
float x0 = (float)src[i] * gain;
dst[2*i+0] += x0;
dst[2*i+1] += x0;
}
}
// apply gain crossfade with accumulation (interleaved)
static void gainfade_2x2(int16_t* src, float* dst, const float* win, float gain0, float gain1, int numFrames) {
gain0 *= (1/32768.0f); // int16_t to float
gain1 *= (1/32768.0f);
for (int i = 0; i < numFrames; i++) {
float frac = win[i];
float gain = gain1 + frac * (gain0 - gain1);
float x0 = (float)src[2*i+0] * gain;
float x1 = (float)src[2*i+1] * gain;
dst[2*i+0] += x0;
dst[2*i+1] += x1;
}
}
// design a 2nd order Thiran allpass
static void ThiranBiquad(float f, float& b0, float& b1, float& b2, float& a1, float& a2) {
@ -1104,6 +1141,13 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth,
// apply global and local gain adjustment
gain *= _gainAdjust;
// disable interpolation from reset state
if (_resetState) {
_azimuthState = azimuth;
_distanceState = distance;
_gainState = gain;
}
// to avoid polluting the cache, old filters are recomputed instead of stored
setFilters(firCoef, bqCoef, delay, index, _azimuthState, _distanceState, _gainState, L0);
@ -1175,3 +1219,45 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth,
_resetState = false;
}
void AudioHRTF::mixMono(int16_t* input, float* output, float gain, int numFrames) {
assert(numFrames == HRTF_BLOCK);
// apply global and local gain adjustment
gain *= _gainAdjust;
// disable interpolation from reset state
if (_resetState) {
_gainState = gain;
}
// crossfade gain and accumulate
gainfade_1x2(input, output, crossfadeTable, _gainState, gain, HRTF_BLOCK);
// new parameters become old
_gainState = gain;
_resetState = false;
}
void AudioHRTF::mixStereo(int16_t* input, float* output, float gain, int numFrames) {
assert(numFrames == HRTF_BLOCK);
// apply global and local gain adjustment
gain *= _gainAdjust;
// disable interpolation from reset state
if (_resetState) {
_gainState = gain;
}
// crossfade gain and accumulate
gainfade_2x2(input, output, crossfadeTable, _gainState, gain, HRTF_BLOCK);
// new parameters become old
_gainState = gain;
_resetState = false;
}

View file

@ -50,6 +50,12 @@ public:
//
void render(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames);
//
// Non-spatialized direct mix (accumulates into existing output)
//
void mixMono(int16_t* input, float* output, float gain, int numFrames);
void mixStereo(int16_t* input, float* output, float gain, int numFrames);
//
// Fast path when input is known to be silent and state as been flushed
//

View file

@ -103,6 +103,8 @@ void AudioInjector::finishLocalInjection() {
void AudioInjector::finish() {
withWriteLock([&] {
_state |= AudioInjectorState::LocalInjectionFinished;
_state |= AudioInjectorState::NetworkInjectionFinished;
_state |= AudioInjectorState::Finished;
});
emit finished();
@ -252,7 +254,7 @@ int64_t AudioInjector::injectNextFrame() {
writeStringToStream(noCodecForInjectors, audioPacketStream);
// pack stream identifier (a generated UUID)
audioPacketStream << QUuid::createUuid();
audioPacketStream << _streamID;
// pack the stereo/mono type of the stream
audioPacketStream << options.stereo;
@ -402,4 +404,17 @@ int64_t AudioInjector::injectNextFrame() {
int64_t playNextFrameAt = ++_nextFrame * AudioConstants::NETWORK_FRAME_USECS;
return std::max(INT64_C(0), playNextFrameAt - currentTime);
}
}
void AudioInjector::sendStopInjectorPacket() {
auto nodeList = DependencyManager::get<NodeList>();
if (auto audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer)) {
// Build packet
auto stopInjectorPacket = NLPacket::create(PacketType::StopInjector);
stopInjectorPacket->write(_streamID.toRfc4122());
// Send packet
nodeList->sendUnreliablePacket(*stopInjectorPacket, *audioMixer);
}
}

View file

@ -100,6 +100,7 @@ private:
int64_t injectNextFrame();
bool inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&));
bool injectLocally();
void sendStopInjectorPacket();
static AbstractAudioInterface* _localAudioInterface;
@ -120,6 +121,9 @@ private:
// when the injector is local, we need this
AudioHRTF _localHRTF;
AudioFOA _localFOA;
QUuid _streamID { QUuid::createUuid() };
friend class AudioInjectorManager;
};

View file

@ -105,6 +105,8 @@ void AudioInjectorManager::run() {
if (nextCallDelta >= 0 && !injector->isFinished()) {
// enqueue the injector with the correct timing in our holding queue
heldInjectors.emplace(heldInjectors.end(), usecTimestampNow() + nextCallDelta, injector);
} else {
injector->sendStopInjectorPacket();
}
}
@ -354,4 +356,4 @@ void AudioInjectorManager::stop(const AudioInjectorPointer& injector) {
size_t AudioInjectorManager::getNumInjectors() {
Lock lock(_injectorsMutex);
return _injectors.size();
}
}

View file

@ -124,9 +124,9 @@ typedef QSharedPointer<Sound> SharedSoundPointer;
* An audio resource, created by {@link SoundCache.getSound}, to be played back using {@link Audio.playSound}.
* <p>Supported formats:</p>
* <ul>
* <li>WAV: 16-bit uncompressed WAV at any sample rate, with 1 (mono), 2(stereo), or 4 (ambisonic) channels.</li>
* <li>WAV: 16-bit uncompressed WAV at any sample rate, with 1 (mono), 2 (stereo), or 4 (ambisonic) channels.</li>
* <li>MP3: Mono or stereo, at any sample rate.</li>
* <li>RAW: 48khz 16-bit mono or stereo. Filename must include <code>".stereo"</code> to be interpreted as stereo.</li>
* <li>RAW: 48khz 16-bit mono or stereo. File name must include <code>".stereo"</code> to be interpreted as stereo.</li>
* </ul>
*
* @class SoundObject
@ -138,8 +138,8 @@ typedef QSharedPointer<Sound> SharedSoundPointer;
* @hifi-assignment-client
*
* @property {boolean} downloaded - <code>true</code> if the sound has been downloaded and is ready to be played, otherwise
* <code>false</code>.
* @property {number} duration - The duration of the sound, in seconds.
* <code>false</code>. <em>Read-only.</em>
* @property {number} duration - The duration of the sound, in seconds. <em>Read-only.</em>
*/
class SoundScriptingInterface : public QObject {
Q_OBJECT

View file

@ -25,7 +25,8 @@ class SoundCacheScriptingInterface : public ScriptableResourceCache, public Depe
// Properties are copied over from ResourceCache (see ResourceCache.h for reason).
/**jsdoc
* API to manage sound cache resources.
* The <code>SoundCache</code> API manages sound cache resources.
*
* @namespace SoundCache
*
* @hifi-interface

View file

@ -1289,14 +1289,16 @@ void convertInput_AVX2(int16_t* src, float *dst[4], float gain, int numFrames) {
#endif
// in-place rotation of the soundfield
// crossfade between old and new rotation, to prevent artifacts
void rotate_3x3_AVX2(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames) {
// in-place rotation and scaling of the soundfield
// crossfade between old and new matrix, to prevent artifacts
void rotate_4x4_AVX2(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames) {
const float md[3][3] = {
{ m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2] },
{ m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2] },
{ m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2] },
// matrix difference
const float md[4][4] = {
{ m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2], m0[0][3] - m1[0][3] },
{ m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2], m0[1][3] - m1[1][3] },
{ m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2], m0[2][3] - m1[2][3] },
{ m0[3][0] - m1[3][0], m0[3][1] - m1[3][1], m0[3][2] - m1[3][2], m0[3][3] - m1[3][3] },
};
assert(numFrames % 8 == 0);
@ -1307,30 +1309,35 @@ void rotate_3x3_AVX2(float* buf[4], const float m0[3][3], const float m1[3][3],
// interpolate the matrix
__m256 m00 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[0][0]), _mm256_broadcast_ss(&m1[0][0]));
__m256 m10 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][0]), _mm256_broadcast_ss(&m1[1][0]));
__m256 m20 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][0]), _mm256_broadcast_ss(&m1[2][0]));
__m256 m01 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[0][1]), _mm256_broadcast_ss(&m1[0][1]));
__m256 m11 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][1]), _mm256_broadcast_ss(&m1[1][1]));
__m256 m21 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][1]), _mm256_broadcast_ss(&m1[2][1]));
__m256 m31 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[3][1]), _mm256_broadcast_ss(&m1[3][1]));
__m256 m02 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[0][2]), _mm256_broadcast_ss(&m1[0][2]));
__m256 m12 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][2]), _mm256_broadcast_ss(&m1[1][2]));
__m256 m22 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][2]), _mm256_broadcast_ss(&m1[2][2]));
__m256 m32 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[3][2]), _mm256_broadcast_ss(&m1[3][2]));
__m256 m13 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][3]), _mm256_broadcast_ss(&m1[1][3]));
__m256 m23 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][3]), _mm256_broadcast_ss(&m1[2][3]));
__m256 m33 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[3][3]), _mm256_broadcast_ss(&m1[3][3]));
// matrix multiply
__m256 x = _mm256_mul_ps(m00, _mm256_loadu_ps(&buf[1][i]));
__m256 y = _mm256_mul_ps(m10, _mm256_loadu_ps(&buf[1][i]));
__m256 z = _mm256_mul_ps(m20, _mm256_loadu_ps(&buf[1][i]));
__m256 w = _mm256_mul_ps(m00, _mm256_loadu_ps(&buf[0][i]));
x = _mm256_fmadd_ps(m01, _mm256_loadu_ps(&buf[2][i]), x);
y = _mm256_fmadd_ps(m11, _mm256_loadu_ps(&buf[2][i]), y);
z = _mm256_fmadd_ps(m21, _mm256_loadu_ps(&buf[2][i]), z);
__m256 x = _mm256_mul_ps(m11, _mm256_loadu_ps(&buf[1][i]));
__m256 y = _mm256_mul_ps(m21, _mm256_loadu_ps(&buf[1][i]));
__m256 z = _mm256_mul_ps(m31, _mm256_loadu_ps(&buf[1][i]));
x = _mm256_fmadd_ps(m02, _mm256_loadu_ps(&buf[3][i]), x);
y = _mm256_fmadd_ps(m12, _mm256_loadu_ps(&buf[3][i]), y);
z = _mm256_fmadd_ps(m22, _mm256_loadu_ps(&buf[3][i]), z);
x = _mm256_fmadd_ps(m12, _mm256_loadu_ps(&buf[2][i]), x);
y = _mm256_fmadd_ps(m22, _mm256_loadu_ps(&buf[2][i]), y);
z = _mm256_fmadd_ps(m32, _mm256_loadu_ps(&buf[2][i]), z);
x = _mm256_fmadd_ps(m13, _mm256_loadu_ps(&buf[3][i]), x);
y = _mm256_fmadd_ps(m23, _mm256_loadu_ps(&buf[3][i]), y);
z = _mm256_fmadd_ps(m33, _mm256_loadu_ps(&buf[3][i]), z);
_mm256_storeu_ps(&buf[0][i], w);
_mm256_storeu_ps(&buf[1][i], x);
_mm256_storeu_ps(&buf[2][i], y);
_mm256_storeu_ps(&buf[3][i], z);

View file

@ -509,6 +509,26 @@ void Avatar::relayJointDataToChildren() {
_reconstructSoftEntitiesJointMap = false;
}
/**jsdoc
* An avatar has different types of data simulated at different rates, in Hz.
*
* <table>
* <thead>
* <tr><th>Rate Name</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td><code>"avatar" or ""</code></td><td>The rate at which the avatar is updated even if not in view.</td></tr>
* <tr><td><code>"avatarInView"</code></td><td>The rate at which the avatar is updated if in view.</td></tr>
* <tr><td><code>"skeletonModel"</code></td><td>The rate at which the skeleton model is being updated, even if there are no
* joint data available.</td></tr>
* <tr><td><code>"jointData"</code></td><td>The rate at which joint data are being updated.</td></tr>
* <tr><td><code>""</code></td><td>When no rate name is specified, the <code>"avatar"</code> update rate is
* provided.</td></tr>
* </tbody>
* </table>
*
* @typedef {string} AvatarSimulationRate
*/
float Avatar::getSimulationRate(const QString& rateName) const {
if (rateName == "") {
return _simulationRate.rate();
@ -1525,8 +1545,8 @@ void Avatar::rigReset() {
void Avatar::computeMultiSphereShapes() {
const Rig& rig = getSkeletonModel()->getRig();
glm::vec3 scale = extractScale(rig.getGeometryOffsetPose());
const HFMModel& geometry = getSkeletonModel()->getHFMModel();
glm::vec3 geometryScale = extractScale(rig.getGeometryOffsetPose());
int jointCount = rig.getJointStateCount();
_multiSphereShapes.clear();
_multiSphereShapes.reserve(jointCount);
@ -1535,9 +1555,10 @@ void Avatar::computeMultiSphereShapes() {
std::vector<btVector3> btPoints;
int lineCount = (int)shapeInfo.debugLines.size();
btPoints.reserve(lineCount);
glm::vec3 jointScale = rig.getJointPose(i).scale() / extractScale(rig.getGeometryToRigTransform());
for (int j = 0; j < lineCount; j++) {
const glm::vec3 &point = shapeInfo.debugLines[j];
auto rigPoint = scale * point;
auto rigPoint = jointScale * geometryScale * point;
btVector3 btPoint = glmToBullet(rigPoint);
btPoints.push_back(btPoint);
}

View file

@ -501,8 +501,8 @@ public:
/**jsdoc
* @function MyAvatar.getSimulationRate
* @param {string} [rateName=""] - Rate name.
* @returns {number} Simulation rate.
* @param {AvatarSimulationRate} [rateName=""] - Rate name.
* @returns {number} Simulation rate in Hz.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE float getSimulationRate(const QString& rateName = QString("")) const;

View file

@ -1545,7 +1545,6 @@ float AvatarData::getDataRate(const QString& rateName) const {
* <tr><th>Rate Name</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td><code>"globalPosition"</code></td><td>Global position.</td></tr>
* <tr><td><code>"localPosition"</code></td><td>Local position.</td></tr>
* <tr><td><code>"avatarBoundingBox"</code></td><td>Avatar bounding box.</td></tr>
@ -1559,7 +1558,6 @@ float AvatarData::getDataRate(const QString& rateName) const {
* <tr><td><code>"faceTracker"</code></td><td>Face tracker data.</td></tr>
* <tr><td><code>"jointData"</code></td><td>Joint data.</td></tr>
* <tr><td><code>"farGrabJointData"</code></td><td>Far grab joint data.</td></tr>
* <tr><td><code>""</code></td><td>When no rate name is specified, the overall update rate is provided.</td></tr>
* </tbody>
* </table>
@ -1721,7 +1719,6 @@ glm::vec3 AvatarData::getJointTranslation(const QString& name) const {
// on another thread in between the call to getJointIndex and getJointTranslation
// return getJointTranslation(getJointIndex(name));
return readLockWithNamedJointIndex<glm::vec3>(name, [this](int index) {
return _jointData.at(index).translation;
return getJointTranslation(index);
});
}
@ -1809,8 +1806,8 @@ glm::quat AvatarData::getJointRotation(const QString& name) const {
// Can't do this, not thread safe
// return getJointRotation(getJointIndex(name));
return readLockWithNamedJointIndex<glm::quat>(name, [&](int index) {
return _jointData.at(index).rotation;
return readLockWithNamedJointIndex<glm::quat>(name, [this](int index) {
return getJointRotation(index);
});
}
@ -2905,6 +2902,20 @@ glm::mat4 AvatarData::getControllerRightHandMatrix() const {
return _controllerRightHandMatrixCache.get();
}
/**jsdoc
* Information about a ray-to-avatar intersection.
* @typedef {object} RayToAvatarIntersectionResult
* @property {boolean} intersects - <code>true</code> if an avatar is intersected, <code>false</code> if it isn't.
* @property {string} avatarID - The ID of the avatar that is intersected.
* @property {number} distance - The distance from the ray origin to the intersection.
* @property {string} face - The name of the box face that is intersected; <code>"UNKNOWN_FACE"</code> if mesh was picked
* against.
* @property {Vec3} intersection - The ray intersection point in world coordinates.
* @property {Vec3} surfaceNormal - The surface normal at the intersection point.
* @property {number} jointIndex - The index of the joint intersected.
* @property {SubmeshIntersection} extraInfo - Extra information on the mesh intersected if mesh was picked against,
* <code>{}</code> if it wasn't.
*/
QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& value) {
QScriptValue obj = engine->newObject();
obj.setProperty("intersects", value.intersects);

View file

@ -479,7 +479,8 @@ class AvatarData : public QObject, public SpatiallyNestable {
* avatar. <em>Read-only.</em>
* @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's
* size in the virtual world. <em>Read-only.</em>
* @property {boolean} hasPriority - is the avatar in a Hero zone? <em>Read-only.</em>
* @property {boolean} hasPriority - <code>true</code> if the avatar is in a "hero" zone, <code>false</code> if it isn't.
* <em>Read-only.</em>
*/
Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript)
Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale)
@ -1751,14 +1752,11 @@ protected:
template <typename T, typename F>
T readLockWithNamedJointIndex(const QString& name, const T& defaultValue, F f) const {
int index = getFauxJointIndex(name);
QReadLocker readLock(&_jointDataLock);
// The first conditional is superfluous, but illustrative
if (index == -1 || index < _jointData.size()) {
int index = getJointIndex(name);
if (index == -1) {
return defaultValue;
}
return f(index);
}
@ -1769,8 +1767,8 @@ protected:
template <typename F>
void writeLockWithNamedJointIndex(const QString& name, F f) {
int index = getFauxJointIndex(name);
QWriteLocker writeLock(&_jointDataLock);
int index = getJointIndex(name);
if (index == -1) {
return;
}

View file

@ -229,8 +229,9 @@ AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID,
AvatarSharedPointer AvatarHashMap::findAvatar(const QUuid& sessionUUID) const {
QReadLocker locker(&_hashLock);
if (_avatarHash.contains(sessionUUID)) {
return _avatarHash.value(sessionUUID);
auto avatarIter = _avatarHash.find(sessionUUID);
if (avatarIter != _avatarHash.end()) {
return avatarIter.value();
}
return nullptr;
}

View file

@ -36,8 +36,10 @@ const int CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 50;
const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
/**jsdoc
* <strong>Note:</strong> An <code>AvatarList</code> API is also provided for Interface and client entity scripts: it is a
* synonym for the {@link AvatarManager} API.
* The <code>AvatarList</code> API provides information about avatars within the current domain.
*
* <p><strong>Warning:</strong> An API named "<code>AvatarList</code>" is also provided for Interface, client entity, and avatar
* scripts, however, it is a synonym for the {@link AvatarManager} API.</p>
*
* @namespace AvatarList
*
@ -78,23 +80,37 @@ public:
// Currently, your own avatar will be included as the null avatar id.
/**jsdoc
* Gets the IDs of all avatars in the domain.
* <p><strong>Warning:</strong> If the AC script is acting as an avatar (i.e., <code>Agent.isAvatar == true</code>) the
* avatar's ID is NOT included in results.</p>
* @function AvatarList.getAvatarIdentifiers
* @returns {Uuid[]}
* @returns {Uuid[]} The IDs of all avatars in the domain (excluding AC script's avatar).
* @example <caption>Report the IDS of all avatars within the domain.</caption>
* var avatars = AvatarList.getAvatarIdentifiers();
* print("Avatars in the domain: " + JSON.stringify(avatars));
*/
Q_INVOKABLE QVector<QUuid> getAvatarIdentifiers();
/**jsdoc
* Gets the IDs of all avatars within a specified distance from a point.
* <p><strong>Warning:</strong> If the AC script is acting as an avatar (i.e., <code>Agent.isAvatar == true</code>) the
* avatar's ID is NOT included in results.</p>
* @function AvatarList.getAvatarsInRange
* @param {Vec3} position
* @param {number} range
* @returns {Uuid[]}
* @param {Vec3} position - The point about which the search is performed.
* @param {number} range - The search radius.
* @returns {Uuid[]} The IDs of all avatars within the search distance from the position (excluding AC script's avatar).
* @example <caption>Report the IDs of all avatars within 10m of the origin.</caption>
* var RANGE = 10;
* var avatars = AvatarList.getAvatarsInRange(Vec3.ZERO, RANGE);
* print("Avatars near the origin: " + JSON.stringify(avatars));
*/
Q_INVOKABLE QVector<QUuid> getAvatarsInRange(const glm::vec3& position, float rangeMeters) const;
/**jsdoc
* Gets information about an avatar.
* @function AvatarList.getAvatar
* @param {Uuid} avatarID
* @returns {AvatarData}
* @param {Uuid} avatarID - The ID of the avatar.
* @returns {AvatarData} Information about the avatar.
*/
// Null/Default-constructed QUuids will return MyAvatar
Q_INVOKABLE virtual ScriptAvatarData* getAvatar(QUuid avatarID) { return new ScriptAvatarData(getAvatarBySessionID(avatarID)); }
@ -110,34 +126,57 @@ public:
signals:
/**jsdoc
* Triggered when an avatar arrives in the domain.
* @function AvatarList.avatarAddedEvent
* @param {Uuid} sessionUUID
* @param {Uuid} sessionUUID - The ID of the avatar that arrived in the domain.
* @returns {Signal}
* @example <caption>Report when an avatar arrives in the domain.</caption>
* AvatarManager.avatarAddedEvent.connect(function (sessionID) {
* print("Avatar arrived: " + sessionID);
* });
*
* // Note: If using from the AvatarList API, replace "AvatarManager" with "AvatarList".
*/
void avatarAddedEvent(const QUuid& sessionUUID);
/**jsdoc
* Triggered when an avatar leaves the domain.
* @function AvatarList.avatarRemovedEvent
* @param {Uuid} sessionUUID
* @param {Uuid} sessionUUID - The ID of the avatar that left the domain.
* @returns {Signal}
* @example <caption>Report when an avatar leaves the domain.</caption>
* AvatarManager.avatarRemovedEvent.connect(function (sessionID) {
* print("Avatar left: " + sessionID);
* });
*
* // Note: If using from the AvatarList API, replace "AvatarManager" with "AvatarList".
*/
void avatarRemovedEvent(const QUuid& sessionUUID);
/**jsdoc
* Triggered when an avatar's session ID changes.
* @function AvatarList.avatarSessionChangedEvent
* @param {Uuid} sessionUUID
* @param {Uuid} oldSessionUUID
* @param {Uuid} newSessionUUID - The new session ID.
* @param {Uuid} oldSessionUUID - The old session ID.
* @returns {Signal}
* @example <caption>Report when an avatar's session ID changes.</caption>
* AvatarManager.avatarSessionChangedEvent.connect(function (newSessionID, oldSessionID) {
* print("Avatar session ID changed from " + oldSessionID + " to " + newSessionID);
* });
*
* // Note: If using from the AvatarList API, replace "AvatarManager" with "AvatarList".
*/
void avatarSessionChangedEvent(const QUuid& sessionUUID,const QUuid& oldUUID);
public slots:
/**jsdoc
* Checks whether there is an avatar within a specified distance from a point.
* @function AvatarList.isAvatarInRange
* @param {string} position
* @param {string} range
* @returns {boolean}
* @param {string} position - The test position.
* @param {string} range - The test distance.
* @returns {boolean} <code>true</code> if there's an avatar within the specified distance of the point, <code>false</code>
* if not.
*/
bool isAvatarInRange(const glm::vec3 & position, const float range);
@ -145,36 +184,41 @@ protected slots:
/**jsdoc
* @function AvatarList.sessionUUIDChanged
* @param {Uuid} sessionUUID
* @param {Uuid} oldSessionUUID
* @param {Uuid} sessionUUID - New session ID.
* @param {Uuid} oldSessionUUID - Old session ID.
* @deprecated This function is deprecated and will be removed.
*/
void sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID);
/**jsdoc
* @function AvatarList.processAvatarDataPacket
* @param {} message
* @param {} sendingNode
* @param {object} message - Message.
* @param {object} sendingNode - Sending node.
* @deprecated This function is deprecated and will be removed.
*/
void processAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
/**jsdoc
* @function AvatarList.processAvatarIdentityPacket
* @param {} message
* @param {} sendingNode
* @param {object} message - Message.
* @param {object} sendingNode - Sending node.
* @deprecated This function is deprecated and will be removed.
*/
void processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
/**jsdoc
* @function AvatarList.processBulkAvatarTraits
* @param {} message
* @param {} sendingNode
* @param {object} message - Message.
* @param {object} sendingNode - Sending node.
* @deprecated This function is deprecated and will be removed.
*/
void processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
/**jsdoc
* @function AvatarList.processKillAvatar
* @param {} message
* @param {} sendingNode
* @param {object} message - Message.
* @param {object} sendingNode - Sending node.
* @deprecated This function is deprecated and will be removed.
*/
void processKillAvatar(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);

View file

@ -16,6 +16,52 @@
#include "AvatarData.h"
/**jsdoc
* Information about an avatar.
* @typedef {object} AvatarData
* @property {Vec3} position - The avatar's position.
* @property {number} scale - The target scale of the avatar without any restrictions on permissible values imposed by the
* domain.
* @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar but
* is otherwise not used or changed by Interface.
* @property {number} bodyPitch - The pitch of the avatar's body, in degrees.
* @property {number} bodyYaw - The yaw of the avatar's body, in degrees.
* @property {number} bodyRoll - The roll of the avatar's body, in degrees.
* @property {Quat} orientation - The orientation of the avatar's body.
* @property {Quat} headOrientation - The orientation of the avatar's head.
* @property {number} headPitch - The pitch of the avatar's head relative to the body, in degrees.
* @property {number} headYaw - The yaw of the avatar's head relative to the body, in degrees.
* @property {number} headRoll - The roll of the avatar's head relative to the body, in degrees.
*
* @property {Vec3} velocity - The linear velocity of the avatar.
* @property {Vec3} angularVelocity - The angular velocity of the avatar.
*
* @property {Uuid} sessionUUID - The avatar's session ID.
* @property {string} displayName - The avatar's display name.
* @property {string} sessionDisplayName - The avatar's display name, sanitized and versioned, as defined by the avatar mixer.
* It is unique among all avatars present in the domain at the time.
* @property {boolean} isReplicated - <strong>Deprecated.</strong>
* @property {boolean} lookAtSnappingEnabled - <code>true</code> if the avatar's eyes snap to look at another avatar's eyes
* when the other avatar is in the line of sight and also has <code>lookAtSnappingEnabled == true</code>.
*
* @property {string} skeletonModelURL - The avatar's FST file.
* @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.<br />
* <strong>Deprecated:</strong> Use avatar entities instead.
* @property {string[]} jointNames - The list of joints in the current avatar model.
*
* @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the
* domain.
* @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting into
* the domain.
*
* @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the
* avatar's size, orientation, and position in the virtual world.
* @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the avatar.
* @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the
* avatar.
*
* @property {boolean} hasPriority - <code>true</code> if the avatar is in a "hero" zone, <code>false</code> if it isn't.
*/
class ScriptAvatarData : public QObject {
Q_OBJECT

View file

@ -33,9 +33,8 @@
#include "ModelBakingLoggingCategory.h"
#include "TextureBaker.h"
FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
FBXBaker::FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputModelURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
if (hasBeenBaked) {
// Look for the original model file one directory higher. Perhaps this is an oven output directory.
QUrl originalRelativePath = QUrl("../original/" + inputModelURL.fileName().replace(BAKED_FBX_EXTENSION, FBX_EXTENSION));
@ -45,15 +44,6 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText
}
void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) {
_hfmModel = hfmModel;
if (shouldStop()) {
return;
}
// enumerate the models and textures found in the scene and start a bake for them
rewriteAndBakeSceneTextures();
if (shouldStop()) {
return;
}
@ -114,15 +104,15 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
int meshIndex = 0;
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
for (FBXNode& object : rootChild.children) {
if (object.name == "Geometry") {
if (object.properties.at(2) == "Mesh") {
for (auto object = rootChild.children.begin(); object != rootChild.children.end(); object++) {
if (object->name == "Geometry") {
if (object->properties.at(2) == "Mesh") {
int meshNum = meshIndexToRuntimeOrder[meshIndex];
replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
meshIndex++;
}
} else if (object.name == "Model") {
for (FBXNode& modelChild : object.children) {
} else if (object->name == "Model") {
for (FBXNode& modelChild : object->children) {
if (modelChild.name == "Properties60" || modelChild.name == "Properties70") {
// This is a properties node
// Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer
@ -142,10 +132,13 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
} else if (modelChild.name == "Vertices") {
// This model is also a mesh
int meshNum = meshIndexToRuntimeOrder[meshIndex];
replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
meshIndex++;
}
}
} else if (object->name == "Texture" || object->name == "Video") {
// this is an embedded texture, we need to remove it from the FBX
object = rootChild.children.erase(object);
}
if (hasErrors()) {
@ -154,82 +147,4 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
}
}
}
}
void FBXBaker::rewriteAndBakeSceneTextures() {
using namespace image::TextureUsage;
QHash<QString, image::TextureUsage::Type> textureTypes;
// enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID
for (const auto& material : _hfmModel->materials) {
if (material.normalTexture.isBumpmap) {
textureTypes[material.normalTexture.id] = BUMP_TEXTURE;
} else {
textureTypes[material.normalTexture.id] = NORMAL_TEXTURE;
}
textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE;
textureTypes[material.glossTexture.id] = GLOSS_TEXTURE;
textureTypes[material.roughnessTexture.id] = ROUGHNESS_TEXTURE;
textureTypes[material.specularTexture.id] = SPECULAR_TEXTURE;
textureTypes[material.metallicTexture.id] = METALLIC_TEXTURE;
textureTypes[material.emissiveTexture.id] = EMISSIVE_TEXTURE;
textureTypes[material.occlusionTexture.id] = OCCLUSION_TEXTURE;
textureTypes[material.lightmapTexture.id] = LIGHTMAP_TEXTURE;
}
// enumerate the children of the root node
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
// enumerate the objects
auto object = rootChild.children.begin();
while (object != rootChild.children.end()) {
if (object->name == "Texture") {
// double check that we didn't get an abort while baking the last texture
if (shouldStop()) {
return;
}
// enumerate the texture children
for (FBXNode& textureChild : object->children) {
if (textureChild.name == "RelativeFilename") {
QString hfmTextureFileName { textureChild.properties.at(0).toString() };
// grab the ID for this texture so we can figure out the
// texture type from the loaded materials
auto textureID { object->properties[0].toString() };
auto textureType = textureTypes[textureID];
// Compress the texture information and return the new filename to be added into the FBX scene
auto bakedTextureFile = compressTexture(hfmTextureFileName, textureType);
// If no errors or warnings have occurred during texture compression add the filename to the FBX scene
if (!bakedTextureFile.isNull()) {
textureChild.properties[0] = bakedTextureFile;
} else {
// if bake fails - return, if there were errors and continue, if there were warnings.
if (hasErrors()) {
return;
} else if (hasWarnings()) {
continue;
}
}
}
}
++object;
} else if (object->name == "Video") {
// this is an embedded texture, we need to remove it from the FBX
object = rootChild.children.erase(object);
} else {
++object;
}
}
}
}
}
}

View file

@ -31,20 +31,14 @@ using TextureBakerThreadGetter = std::function<QThread*()>;
class FBXBaker : public ModelBaker {
Q_OBJECT
public:
FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
protected:
virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) override;
private:
void rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists);
void rewriteAndBakeSceneTextures();
void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList);
hfm::Model::Pointer _hfmModel;
bool _pendingErrorEmission { false };
};
#endif // hifi_FBXBaker_h

View file

@ -27,21 +27,11 @@ std::function<QThread*()> MaterialBaker::_getNextOvenWorkerThreadOperator;
static int materialNum = 0;
namespace std {
template <>
struct hash<graphics::Material::MapChannel> {
size_t operator()(const graphics::Material::MapChannel& a) const {
return std::hash<size_t>()((size_t)a);
}
};
};
MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) :
MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) :
_materialData(materialData),
_isURL(isURL),
_bakedOutputDir(bakedOutputDir),
_textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)),
_destinationPath(destinationPath)
_textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++))
{
}
@ -64,6 +54,14 @@ void MaterialBaker::bake() {
}
}
void MaterialBaker::abort() {
Baker::abort();
for (auto& textureBaker : _textureBakers) {
textureBaker->abort();
}
}
void MaterialBaker::loadMaterial() {
if (!_isURL) {
qCDebug(material_baking) << "Loading local material" << _materialData;
@ -104,45 +102,42 @@ void MaterialBaker::processMaterial() {
for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) {
if (networkMaterial.second) {
auto textureMaps = networkMaterial.second->getTextureMaps();
for (auto textureMap : textureMaps) {
if (textureMap.second && textureMap.second->getTextureSource()) {
graphics::Material::MapChannel mapChannel = textureMap.first;
auto texture = textureMap.second->getTextureSource();
auto textures = networkMaterial.second->getTextures();
for (auto texturePair : textures) {
auto mapChannel = texturePair.first;
auto textureMap = texturePair.second;
if (textureMap.texture && textureMap.texture->_textureSource) {
auto type = textureMap.texture->getTextureType();
QUrl url = texture->getUrl();
QString cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString();
QByteArray content;
QUrl textureURL;
{
bool foundEmbeddedTexture = false;
auto textureContentMapIter = _textureContentMap.find(networkMaterial.second->getName());
if (textureContentMapIter != _textureContentMap.end()) {
auto textureUsageIter = textureContentMapIter->second.find(type);
if (textureUsageIter != textureContentMapIter->second.end()) {
content = textureUsageIter->second.first;
textureURL = textureUsageIter->second.second;
foundEmbeddedTexture = true;
}
}
if (!foundEmbeddedTexture && textureMap.texture->_textureSource) {
textureURL = textureMap.texture->_textureSource->getUrl().adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
}
}
QString cleanURL = textureURL.toDisplayString();
auto idx = cleanURL.lastIndexOf('.');
auto extension = idx >= 0 ? url.toDisplayString().mid(idx + 1).toLower() : "";
QString extension = idx >= 0 ? cleanURL.mid(idx + 1).toLower() : "";
if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) {
QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
// FIXME: this isn't properly handling bumpMaps or glossMaps
static std::unordered_map<graphics::Material::MapChannel, image::TextureUsage::Type> MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP;
if (MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.empty()) {
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::EMISSIVE_MAP] = image::TextureUsage::EMISSIVE_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ALBEDO_MAP] = image::TextureUsage::ALBEDO_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::METALLIC_MAP] = image::TextureUsage::METALLIC_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ROUGHNESS_MAP] = image::TextureUsage::ROUGHNESS_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::NORMAL_MAP] = image::TextureUsage::NORMAL_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::OCCLUSION_MAP] = image::TextureUsage::OCCLUSION_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::LIGHTMAP_MAP] = image::TextureUsage::LIGHTMAP_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::SCATTERING_MAP] = image::TextureUsage::SCATTERING_TEXTURE;
}
auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel);
if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) {
handleError("Unknown map channel");
return;
}
QPair<QUrl, image::TextureUsage::Type> textureKey(textureURL, it->second);
QPair<QUrl, image::TextureUsage::Type> textureKey(textureURL, type);
if (!_textureBakers.contains(textureKey)) {
auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), it->second);
auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), type);
QSharedPointer<TextureBaker> textureBaker {
new TextureBaker(textureURL, it->second, _textureOutputDir, "", baseTextureFileName),
new TextureBaker(textureURL, type, _textureOutputDir, "", baseTextureFileName, content),
&TextureBaker::deleteLater
};
textureBaker->setMapChannel(mapChannel);
@ -179,7 +174,7 @@ void MaterialBaker::handleFinishedTextureBaker() {
// Replace the old texture URLs
for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) {
networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(_destinationPath.resolved(relativeURL));
networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(relativeURL);
}
} else {
// this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from
@ -245,3 +240,30 @@ void MaterialBaker::outputMaterial() {
// emit signal to indicate the material baking is finished
emit finished();
}
void MaterialBaker::addTexture(const QString& materialName, image::TextureUsage::Type textureUsage, const hfm::Texture& texture) {
auto& textureUsageMap = _textureContentMap[materialName.toStdString()];
if (textureUsageMap.find(textureUsage) == textureUsageMap.end() && !texture.content.isEmpty()) {
textureUsageMap[textureUsage] = { texture.content, texture.filename };
}
};
void MaterialBaker::setMaterials(const QHash<QString, hfm::Material>& materials, const QString& baseURL) {
_materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
for (auto& material : materials) {
_materialResource->parsedMaterials.names.push_back(material.name.toStdString());
_materialResource->parsedMaterials.networkMaterials[material.name.toStdString()] = std::make_shared<NetworkMaterial>(material, baseURL);
// Store any embedded texture content
addTexture(material.name, image::TextureUsage::NORMAL_TEXTURE, material.normalTexture);
addTexture(material.name, image::TextureUsage::ALBEDO_TEXTURE, material.albedoTexture);
addTexture(material.name, image::TextureUsage::GLOSS_TEXTURE, material.glossTexture);
addTexture(material.name, image::TextureUsage::ROUGHNESS_TEXTURE, material.roughnessTexture);
addTexture(material.name, image::TextureUsage::SPECULAR_TEXTURE, material.specularTexture);
addTexture(material.name, image::TextureUsage::METALLIC_TEXTURE, material.metallicTexture);
addTexture(material.name, image::TextureUsage::EMISSIVE_TEXTURE, material.emissiveTexture);
addTexture(material.name, image::TextureUsage::OCCLUSION_TEXTURE, material.occlusionTexture);
addTexture(material.name, image::TextureUsage::SCATTERING_TEXTURE, material.scatteringTexture);
addTexture(material.name, image::TextureUsage::LIGHTMAP_TEXTURE, material.lightmapTexture);
}
}

View file

@ -24,16 +24,19 @@ static const QString BAKED_MATERIAL_EXTENSION = ".baked.json";
class MaterialBaker : public Baker {
Q_OBJECT
public:
MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath);
MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir);
QString getMaterialData() const { return _materialData; }
bool isURL() const { return _isURL; }
QString getBakedMaterialData() const { return _bakedMaterialData; }
void setMaterials(const QHash<QString, hfm::Material>& materials, const QString& baseURL);
static void setNextOvenWorkerThreadOperator(std::function<QThread*()> getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; }
public slots:
virtual void bake() override;
virtual void abort() override;
signals:
void originalMaterialLoaded();
@ -57,11 +60,18 @@ private:
QString _bakedOutputDir;
QString _textureOutputDir;
QString _bakedMaterialData;
QUrl _destinationPath;
QScriptEngine _scriptEngine;
static std::function<QThread*()> _getNextOvenWorkerThreadOperator;
TextureFileNamer _textureFileNamer;
void addTexture(const QString& materialName, image::TextureUsage::Type textureUsage, const hfm::Texture& texture);
struct TextureUsageHash {
std::size_t operator()(image::TextureUsage::Type textureUsage) const {
return static_cast<std::size_t>(textureUsage);
}
};
std::unordered_map<std::string, std::unordered_map<image::TextureUsage::Type, std::pair<QByteArray, QString>, TextureUsageHash>> _textureContentMap;
};
#endif // !hifi_MaterialBaker_h

View file

@ -42,12 +42,12 @@
#include "baking/BakerLibrary.h"
ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
#include <QJsonArray>
ModelBaker::ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
_modelURL(inputModelURL),
_bakedOutputDir(bakedOutputDirectory),
_originalOutputDir(originalOutputDirectory),
_textureThreadGetter(inputTextureThreadGetter),
_hasBeenBaked(hasBeenBaked)
{
auto bakedFilename = _modelURL.fileName();
@ -209,7 +209,6 @@ void ModelBaker::bakeSourceCopy() {
}
hifi::ByteArray modelData = modelFile.readAll();
hfm::Model::Pointer bakedModel;
std::vector<hifi::ByteArray> dracoMeshes;
std::vector<std::vector<hifi::ByteArray>> dracoMaterialLists; // Material order for per-mesh material lookup used by dracoMeshes
@ -245,40 +244,76 @@ void ModelBaker::bakeSourceCopy() {
// Begin hfm baking
baker.run();
bakedModel = baker.getHFMModel();
_hfmModel = baker.getHFMModel();
dracoMeshes = baker.getDracoMeshes();
dracoMaterialLists = baker.getDracoMaterialLists();
}
// Populate _textureContentMap with path to content mappings, for quick lookup by URL
for (auto materialIt = bakedModel->materials.cbegin(); materialIt != bakedModel->materials.cend(); materialIt++) {
static const auto addTexture = [](QHash<hifi::ByteArray, hifi::ByteArray>& textureContentMap, const hfm::Texture& texture) {
if (!textureContentMap.contains(texture.filename)) {
// Content may be empty, unless the data is inlined
textureContentMap[texture.filename] = texture.content;
}
};
const hfm::Material& material = *materialIt;
addTexture(_textureContentMap, material.normalTexture);
addTexture(_textureContentMap, material.albedoTexture);
addTexture(_textureContentMap, material.opacityTexture);
addTexture(_textureContentMap, material.glossTexture);
addTexture(_textureContentMap, material.roughnessTexture);
addTexture(_textureContentMap, material.specularTexture);
addTexture(_textureContentMap, material.metallicTexture);
addTexture(_textureContentMap, material.emissiveTexture);
addTexture(_textureContentMap, material.occlusionTexture);
addTexture(_textureContentMap, material.scatteringTexture);
addTexture(_textureContentMap, material.lightmapTexture);
}
// Do format-specific baking
bakeProcessedSource(bakedModel, dracoMeshes, dracoMaterialLists);
bakeProcessedSource(_hfmModel, dracoMeshes, dracoMaterialLists);
if (shouldStop()) {
return;
}
if (_hfmModel->materials.size() > 0) {
_materialBaker = QSharedPointer<MaterialBaker>(
new MaterialBaker(_modelURL.fileName(), true, _bakedOutputDir),
&MaterialBaker::deleteLater
);
_materialBaker->setMaterials(_hfmModel->materials, _modelURL.toString());
connect(_materialBaker.data(), &MaterialBaker::finished, this, &ModelBaker::handleFinishedMaterialBaker);
_materialBaker->bake();
} else {
outputBakedFST();
}
}
void ModelBaker::handleFinishedMaterialBaker() {
auto baker = qobject_cast<MaterialBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this MaterialBaker is done and everything went according to plan
qCDebug(model_baking) << "Adding baked material to FST mapping " << baker->getBakedMaterialData();
QString relativeBakedMaterialURL = _modelURL.fileName();
auto baseName = relativeBakedMaterialURL.left(relativeBakedMaterialURL.lastIndexOf('.'));
relativeBakedMaterialURL = baseName + BAKED_MATERIAL_EXTENSION;
// First we add the materials in the model
QJsonArray materialMapping;
for (auto material : _hfmModel->materials) {
QJsonObject json;
json["mat::" + material.name] = relativeBakedMaterialURL + "#" + material.name;
materialMapping.push_back(json);
}
// The we add any existing mappings from the mapping
if (_mapping.contains(MATERIAL_MAPPING_FIELD)) {
QByteArray materialMapValue = _mapping[MATERIAL_MAPPING_FIELD].toByteArray();
QJsonObject oldMaterialMapping = QJsonDocument::fromJson(materialMapValue).object();
for (auto key : oldMaterialMapping.keys()) {
QJsonObject json;
json[key] = oldMaterialMapping[key];
materialMapping.push_back(json);
}
}
_mapping[MATERIAL_MAPPING_FIELD] = QJsonDocument(materialMapping).toJson(QJsonDocument::Compact);
} else {
// this material failed to bake - this doesn't fail the entire bake but we need to add the errors from
// the material to our warnings
_warningList << baker->getWarnings();
}
} else {
handleWarning("Failed to bake the materials for model with URL " + _modelURL.toString());
}
outputBakedFST();
}
void ModelBaker::outputBakedFST() {
// Output FST file, copying over input mappings if available
QString outputFSTFilename = !_mappingURL.isEmpty() ? _mappingURL.fileName() : _modelURL.fileName();
auto extensionStart = outputFSTFilename.indexOf(".");
@ -291,8 +326,7 @@ void ModelBaker::bakeSourceCopy() {
auto outputMapping = _mapping;
outputMapping[FST_VERSION_FIELD] = FST_VERSION;
outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName();
// All textures will be found in the same directory as the model
outputMapping[TEXDIR_FIELD] = ".";
outputMapping.remove(TEXDIR_FIELD);
hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping);
QFile fstOutputFile { outputFSTURL };
@ -307,17 +341,16 @@ void ModelBaker::bakeSourceCopy() {
_outputFiles.push_back(outputFSTURL);
_outputMappingURL = outputFSTURL;
// check if we're already done with textures (in case we had none to re-write)
checkIfTexturesFinished();
exportScene();
qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL;
emit finished();
}
void ModelBaker::abort() {
Baker::abort();
// tell our underlying TextureBaker instances to abort
// the ModelBaker will wait until all are aborted before emitting its own abort signal
for (auto& textureBaker : _bakingTextures) {
textureBaker->abort();
if (_materialBaker) {
_materialBaker->abort();
}
}
@ -354,247 +387,6 @@ bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dr
return true;
}
QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) {
QFileInfo modelTextureFileInfo { modelTextureFileName.replace("\\", "/") };
if (modelTextureFileInfo.suffix().toLower() == BAKED_TEXTURE_KTX_EXT.mid(1)) {
// re-baking a model that already references baked textures
// this is an error - return from here
handleError("Cannot re-bake a file that already references compressed textures");
return QString::null;
}
if (!image::getSupportedFormats().contains(modelTextureFileInfo.suffix())) {
// this is a texture format we don't bake, skip it
handleWarning(modelTextureFileName + " is not a bakeable texture format");
return QString::null;
}
// make sure this texture points to something and isn't one we've already re-mapped
QString textureChild { QString::null };
if (!modelTextureFileInfo.filePath().isEmpty()) {
// check if this was an embedded texture that we already have in-memory content for
QByteArray textureContent;
// figure out the URL to this texture, embedded or external
if (!modelTextureFileInfo.filePath().isEmpty()) {
textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit());
}
auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull());
TextureKey textureKey { urlToTexture, textureType };
auto bakingTextureIt = _bakingTextures.find(textureKey);
if (bakingTextureIt == _bakingTextures.cend()) {
// construct the new baked texture file name and file path
// ensuring that the baked texture will have a unique name
// even if there was another texture with the same name at a different path
QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType);
QString bakedTextureFilePath {
_bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX
};
textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX;
_outputFiles.push_back(bakedTextureFilePath);
// bake this texture asynchronously
bakeTexture(textureKey, _bakedOutputDir, baseTextureFileName, textureContent);
} else {
// Fetch existing texture meta name
textureChild = (*bakingTextureIt)->getBaseFilename() + BAKED_META_TEXTURE_SUFFIX;
}
}
qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName
<< "to" << textureChild;
return textureChild;
}
void ModelBaker::bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
// start a bake for this texture and add it to our list to keep track of
QSharedPointer<TextureBaker> bakingTexture{
new TextureBaker(textureKey.first, textureKey.second, outputDir, "../", bakedFilename, textureContent),
&TextureBaker::deleteLater
};
// make sure we hear when the baking texture is done or aborted
connect(bakingTexture.data(), &Baker::finished, this, &ModelBaker::handleBakedTexture);
connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture);
// keep a shared pointer to the baking texture
_bakingTextures.insert(textureKey, bakingTexture);
// start baking the texture on one of our available worker threads
bakingTexture->moveToThread(_textureThreadGetter());
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
}
void ModelBaker::handleBakedTexture() {
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
qDebug() << "Handling baked texture" << bakedTexture->getTextureURL();
// make sure we haven't already run into errors, and that this is a valid texture
if (bakedTexture) {
if (!shouldStop()) {
if (!bakedTexture->hasErrors()) {
if (!_originalOutputDir.isEmpty()) {
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
// use the path to the texture being baked to determine if this was an embedded or a linked texture
// it is embeddded if the texure being baked was inside a folder with the name of the model
// since that is the fake URL we provide when baking external textures
if (!_modelURL.isParentOf(bakedTexture->getTextureURL())) {
// for linked textures we want to save a copy of original texture beside the original model
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
// check if we have a relative path to use for the texture
auto relativeTexturePath = texturePathRelativeToModel(_modelURL, bakedTexture->getTextureURL());
QFile originalTextureFile{
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
};
if (relativeTexturePath.length() > 0) {
// make the folders needed by the relative path
}
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
<< "for" << _modelURL;
} else {
handleError("Could not save original external texture " + originalTextureFile.fileName()
+ " for " + _modelURL.toString());
return;
}
}
}
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
checkIfTexturesFinished();
} else {
// there was an error baking this texture - add it to our list of errors
_errorList.append(bakedTexture->getErrors());
// we don't emit finished yet so that the other textures can finish baking first
_pendingErrorEmission = true;
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
} else {
// we have errors to attend to, so we don't do extra processing for this texture
// but we do need to remove that TextureBaker from our list
// and then check if we're done with all textures
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
checkIfTexturesFinished();
}
}
}
void ModelBaker::handleAbortedTexture() {
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
qDebug() << "Texture aborted: " << bakedTexture->getTextureURL();
if (bakedTexture) {
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
}
// since a texture we were baking aborted, our status is also aborted
_shouldAbort.store(true);
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded) {
QUrl urlToTexture;
if (isEmbedded) {
urlToTexture = _modelURL.toString() + "/" + textureFileInfo.filePath();
} else {
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
// set the texture URL to the local texture that we have confirmed exists
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
} else {
// external texture that we'll need to download or find
// this is a relative file path which will require different handling
// depending on the location of the original model
if (_modelURL.isLocalFile() && textureFileInfo.exists() && textureFileInfo.isFile()) {
// the absolute path we ran into for the texture in the model exists on this machine
// so use that file
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
} else {
// we didn't find the texture on this machine at the absolute path
// so assume that it is right beside the model to match the behaviour of interface
urlToTexture = _modelURL.resolved(textureFileInfo.fileName());
}
}
}
return urlToTexture;
}
QString ModelBaker::texturePathRelativeToModel(QUrl modelURL, QUrl textureURL) {
auto modelPath = modelURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
if (texturePath.startsWith(modelPath)) {
// texture path is a child of the model path, return the texture path without the model path
return texturePath.mid(modelPath.length());
} else {
// the texture path was not a child of the model path, return the empty string
return "";
}
}
void ModelBaker::checkIfTexturesFinished() {
// check if we're done everything we need to do for this model
// and emit our finished signal if we're done
if (_bakingTextures.isEmpty()) {
if (shouldStop()) {
// if we're checking for completion but we have errors
// that means one or more of our texture baking operations failed
if (_pendingErrorEmission) {
setIsFinished(true);
}
return;
} else {
qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL;
texturesFinished();
setIsFinished(true);
}
}
}
void ModelBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
@ -605,70 +397,6 @@ void ModelBaker::setWasAborted(bool wasAborted) {
}
}
void ModelBaker::texturesFinished() {
embedTextureMetaData();
exportScene();
}
void ModelBaker::embedTextureMetaData() {
std::vector<FBXNode> embeddedTextureNodes;
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
qlonglong maxId = 0;
for (auto &child : rootChild.children) {
if (child.properties.length() == 3) {
maxId = std::max(maxId, child.properties[0].toLongLong());
}
}
for (auto& object : rootChild.children) {
if (object.name == "Texture") {
QVariant relativeFilename;
for (auto& child : object.children) {
if (child.name == "RelativeFilename") {
relativeFilename = child.properties[0];
break;
}
}
if (relativeFilename.isNull()
|| !relativeFilename.toString().endsWith(BAKED_META_TEXTURE_SUFFIX)) {
continue;
}
if (object.properties.length() < 2) {
qWarning() << "Found texture with unexpected number of properties: " << object.name;
continue;
}
FBXNode videoNode;
videoNode.name = "Video";
videoNode.properties.append(++maxId);
videoNode.properties.append(object.properties[1]);
videoNode.properties.append("Clip");
QString bakedTextureFilePath {
_bakedOutputDir + "/" + relativeFilename.toString()
};
QFile textureFile { bakedTextureFilePath };
if (!textureFile.open(QIODevice::ReadOnly)) {
qWarning() << "Failed to open: " << bakedTextureFilePath;
continue;
}
videoNode.children.append({ "RelativeFilename", { relativeFilename }, { } });
videoNode.children.append({ "Content", { textureFile.readAll() }, { } });
rootChild.children.append(videoNode);
textureFile.close();
}
}
}
}
}
void ModelBaker::exportScene() {
auto fbxData = FBXWriter::encodeFBX(_rootNode);

View file

@ -18,17 +18,13 @@
#include <QtNetwork/QNetworkReply>
#include "Baker.h"
#include "TextureBaker.h"
#include "baking/TextureFileNamer.h"
#include "MaterialBaker.h"
#include "ModelBakingLoggingCategory.h"
#include <gpu/Texture.h>
#include <FBX.h>
#include <hfm/HFM.h>
using TextureBakerThreadGetter = std::function<QThread*()>;
using GetMaterialIDCallback = std::function <int(int)>;
static const QString FST_EXTENSION { ".fst" };
@ -42,10 +38,7 @@ class ModelBaker : public Baker {
Q_OBJECT
public:
using TextureKey = QPair<QUrl, image::TextureUsage::Type>;
ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
void setOutputURLSuffix(const QUrl& urlSuffix);
void setMappingURL(const QUrl& mappingURL);
@ -54,7 +47,6 @@ public:
void initializeOutputDirs();
bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList);
QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE);
virtual void setWasAborted(bool wasAborted) override;
QUrl getModelURL() const { return _modelURL; }
@ -71,20 +63,15 @@ public slots:
protected:
void saveSourceModel();
virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) = 0;
void checkIfTexturesFinished();
void texturesFinished();
void embedTextureMetaData();
void exportScene();
FBXNode _rootNode;
QHash<QByteArray, QByteArray> _textureContentMap;
QUrl _modelURL;
QUrl _outputURLSuffix;
QUrl _mappingURL;
hifi::VariantHash _mapping;
QString _bakedOutputDir;
QString _originalOutputDir;
TextureBakerThreadGetter _textureThreadGetter;
QString _originalOutputModelPath;
QString _outputMappingURL;
QUrl _bakedModelURL;
@ -92,23 +79,15 @@ protected:
protected slots:
void handleModelNetworkReply();
virtual void bakeSourceCopy();
private slots:
void handleBakedTexture();
void handleAbortedTexture();
void handleFinishedMaterialBaker();
private:
QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false);
void bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent);
QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL);
QMultiHash<TextureKey, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
bool _pendingErrorEmission { false };
void outputBakedFST();
bool _hasBeenBaked { false };
TextureFileNamer _textureFileNamer;
hfm::Model::Pointer _hfmModel;
QSharedPointer<MaterialBaker> _materialBaker;
};
#endif // hifi_ModelBaker_h

View file

@ -132,55 +132,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h
handleWarning("Baked mesh for OBJ model '" + _modelURL.toString() + "' is empty");
}
// Generating Texture Node
// iterate through mesh parts and process the associated textures
auto size = meshParts.size();
for (int i = 0; i < size; i++) {
QString material = meshParts[i].materialID;
HFMMaterial currentMaterial = hfmModel->materials[material];
if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) {
auto textureID = nextNodeID();
_mapTextureMaterial.emplace_back(textureID, i);
FBXNode textureNode;
{
textureNode.name = TEXTURE_NODE_NAME;
textureNode.properties = { textureID, "texture" + QString::number(textureID) };
}
// Texture node child - TextureName node
FBXNode textureNameNode;
{
textureNameNode.name = TEXTURENAME_NODE_NAME;
QByteArray propertyString = (!currentMaterial.albedoTexture.filename.isEmpty()) ? "Kd" : "Ka";
textureNameNode.properties = { propertyString };
}
// Texture node child - Relative Filename node
FBXNode relativeFilenameNode;
{
relativeFilenameNode.name = RELATIVEFILENAME_NODE_NAME;
}
QByteArray textureFileName = (!currentMaterial.albedoTexture.filename.isEmpty()) ? currentMaterial.albedoTexture.filename : currentMaterial.specularTexture.filename;
auto textureType = (!currentMaterial.albedoTexture.filename.isEmpty()) ? image::TextureUsage::Type::ALBEDO_TEXTURE : image::TextureUsage::Type::SPECULAR_TEXTURE;
// Compress the texture using ModelBaker::compressTexture() and store compressed file's name in the node
auto textureFile = compressTexture(textureFileName, textureType);
if (textureFile.isNull()) {
// Baking failed return
handleError("Failed to compress texture: " + textureFileName);
return;
}
relativeFilenameNode.properties = { textureFile };
textureNode.children = { textureNameNode, relativeFilenameNode };
objectNode.children.append(textureNode);
}
}
// Generating Connections node
connectionsNode.name = CONNECTIONS_NODE_NAME;
@ -199,29 +150,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h
cNode.properties = { CONNECTIONS_NODE_PROPERTY, materialID, modelID };
connectionsNode.children.append(cNode);
}
// Connect textures to materials
for (const auto& texMat : _mapTextureMaterial) {
FBXNode cAmbientNode;
cAmbientNode.name = C_NODE_NAME;
cAmbientNode.properties = {
CONNECTIONS_NODE_PROPERTY_1,
texMat.first,
_materialIDs[texMat.second],
"AmbientFactor"
};
connectionsNode.children.append(cAmbientNode);
FBXNode cDiffuseNode;
cDiffuseNode.name = C_NODE_NAME;
cDiffuseNode.properties = {
CONNECTIONS_NODE_PROPERTY_1,
texMat.first,
_materialIDs[texMat.second],
"DiffuseColor"
};
connectionsNode.children.append(cDiffuseNode);
}
}
// Set properties for material nodes

View file

@ -35,9 +35,7 @@ private:
void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel);
NodeID nextNodeID() { return _nodeID++; }
NodeID _nodeID { 0 };
std::vector<NodeID> _materialIDs;
std::vector<std::pair<NodeID, int>> _mapTextureMaterial;
};
#endif // hifi_OBJBaker_h

View file

@ -44,7 +44,7 @@ bool isModelBaked(const QUrl& bakeableModelURL) {
return beforeModelExtension.endsWith(".baked");
}
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) {
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath) {
auto filename = bakeableModelURL.fileName();
// Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique
@ -58,20 +58,20 @@ std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, TextureB
QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked";
QString originalOutputDirectory = contentOutputPath + subDirName + "/original";
return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
return getModelBakerWithOutputDirectories(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory);
}
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) {
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) {
auto filename = bakeableModelURL.fileName();
std::unique_ptr<ModelBaker> baker;
if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) {
baker = std::make_unique<FSTBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive));
baker = std::make_unique<FSTBaker>(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive));
} else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) {
baker = std::make_unique<FBXBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive));
baker = std::make_unique<FBXBaker>(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive));
} else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) {
baker = std::make_unique<OBJBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
baker = std::make_unique<OBJBaker>(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory);
//} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) {
//baker = std::make_unique<GLTFBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
} else {

View file

@ -23,9 +23,9 @@ bool isModelBaked(const QUrl& bakeableModelURL);
// Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored
// Returns an empty pointer if a baker could not be created
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath);
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath);
// Similar to getModelBaker, but gives control over where the output folders will be
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory);
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory);
#endif // hifi_BakerLibrary_h

View file

@ -18,9 +18,8 @@
#include <FSTReader.h>
FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputMappingURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
FSTBaker::FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputMappingURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
if (hasBeenBaked) {
// Look for the original model file one directory higher. Perhaps this is an oven output directory.
QUrl originalRelativePath = QUrl("../original/" + inputMappingURL.fileName().replace(BAKED_FST_EXTENSION, FST_EXTENSION));
@ -70,7 +69,7 @@ void FSTBaker::bakeSourceCopy() {
return;
}
auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir);
auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _bakedOutputDir, _originalOutputDir);
_modelBaker = std::unique_ptr<ModelBaker>(dynamic_cast<ModelBaker*>(baker.release()));
if (!_modelBaker) {
handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker");

View file

@ -18,8 +18,7 @@ class FSTBaker : public ModelBaker {
Q_OBJECT
public:
FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
virtual QUrl getFullOutputMappingURL() const override;

View file

@ -109,7 +109,6 @@ public:
Q_ASSERT(_context);
_context->makeCurrent();
CHECK_GL_ERROR();
_context->doneCurrent();
while (!_shutdown) {
if (_pendingOtherThreadOperation) {
PROFILE_RANGE(render, "MainThreadOp")
@ -129,6 +128,7 @@ public:
Lock lock(_mutex);
_condition.wait(lock, [&] { return _finishedOtherThreadOperation; });
}
_context->makeCurrent();
}
// Check for a new display plugin
@ -140,18 +140,16 @@ public:
if (newPlugin != currentPlugin) {
// Deactivate the old plugin
if (currentPlugin != nullptr) {
_context->makeCurrent();
currentPlugin->uncustomizeContext();
CHECK_GL_ERROR();
_context->doneCurrent();
// Force completion of all pending GL commands
glFinish();
}
if (newPlugin) {
bool hasVsync = true;
QThread::setPriority(newPlugin->getPresentPriority());
bool wantVsync = newPlugin->wantVsync();
_context->makeCurrent();
CHECK_GL_ERROR();
#if defined(Q_OS_MAC)
newPlugin->swapBuffers();
#endif
@ -163,7 +161,8 @@ public:
newPlugin->setVsyncEnabled(hasVsync);
newPlugin->customizeContext();
CHECK_GL_ERROR();
_context->doneCurrent();
// Force completion of all pending GL commands
glFinish();
}
currentPlugin = newPlugin;
_newPluginQueue.pop();
@ -180,7 +179,6 @@ public:
}
// Execute the frame and present it to the display device.
_context->makeCurrent();
{
PROFILE_RANGE(render, "PluginPresent")
gl::globalLock();
@ -188,9 +186,9 @@ public:
gl::globalRelease(false);
CHECK_GL_ERROR();
}
_context->doneCurrent();
}
_context->doneCurrent();
Lock lock(_mutex);
_context->moveToThread(qApp->thread());
_shutdown = false;

View file

@ -48,8 +48,6 @@ public:
void pluginUpdate() override {};
virtual StencilMode getStencilMaskMode() const override { return StencilMode::PAINT; }
signals:
void hmdMountedChanged();
void hmdVisibleChanged(bool visible);

View file

@ -166,7 +166,10 @@ ShapeKey EntityRenderer::getShapeKey() {
}
render::hifi::Tag EntityRenderer::getTagMask() const {
return _isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW;
render::hifi::Tag mask = render::hifi::TAG_NONE;
mask = (render::hifi::Tag)(mask | (!_cauterized * render::hifi::TAG_MAIN_VIEW));
mask = (render::hifi::Tag)(mask | (_isVisibleInSecondaryCamera * render::hifi::TAG_SECONDARY_VIEW));
return mask;
}
render::hifi::Layer EntityRenderer::getHifiRenderLayer() const {
@ -215,12 +218,7 @@ void EntityRenderer::render(RenderArgs* args) {
emit requestRenderUpdate();
}
auto& renderMode = args->_renderMode;
bool cauterized = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE &&
renderMode != RenderArgs::RenderMode::SECONDARY_CAMERA_RENDER_MODE &&
_cauterized);
if (_visible && !cauterized) {
if (_visible && (args->_renderMode != RenderArgs::RenderMode::DEFAULT_RENDER_MODE || !_cauterized)) {
doRender(args);
}
}

View file

@ -121,7 +121,11 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo
QString materialURL = entity->getMaterialURL();
if (materialURL != _materialURL) {
_materialURL = materialURL;
if (_materialURL.contains("?")) {
if (_materialURL.contains("#")) {
auto split = _materialURL.split("#");
newCurrentMaterialName = split.last().toStdString();
} else if (_materialURL.contains("?")) {
qDebug() << "DEPRECATED: Use # instead of ? for material URLS:" << _materialURL;
auto split = _materialURL.split("?");
newCurrentMaterialName = split.last().toStdString();
}
@ -358,7 +362,13 @@ void MaterialEntityRenderer::deleteMaterial(const QUuid& oldParentID, const QStr
return;
}
// if a remove fails, our parent is gone, so we don't need to retry
// if a remove fails, our parent is gone, so we don't need to retry, EXCEPT:
// MyAvatar can change UUIDs when you switch domains, which leads to a timing issue. Let's just make
// sure we weren't attached to MyAvatar by trying this (if we weren't, this will have no effect)
if (EntityTreeRenderer::removeMaterialFromAvatar(AVATAR_SELF_ID, material, oldParentMaterialNameStd)) {
_appliedMaterial = nullptr;
return;
}
}
void MaterialEntityRenderer::applyTextureTransform(std::shared_ptr<NetworkMaterial>& material) {

View file

@ -1066,13 +1066,6 @@ ItemKey ModelEntityRenderer::getKey() {
return _itemKey;
}
render::hifi::Tag ModelEntityRenderer::getTagMask() const {
// Default behavior for model is to not be visible in main view if cauterized (aka parented to the avatar's neck joint)
return _cauterized ?
(_isVisibleInSecondaryCamera ? render::hifi::TAG_SECONDARY_VIEW : render::hifi::TAG_NONE) :
Parent::getTagMask(); // calculate which views to be shown in
}
uint32_t ModelEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) {
if (_model) {
auto metaSubItems = _model->fetchRenderItemIDs();
@ -1409,6 +1402,10 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
model->setVisibleInScene(_visible, scene);
}
if (model->isCauterized() != _cauterized) {
model->setCauterized(_cauterized, scene);
}
render::hifi::Tag tagMask = getTagMask();
if (model->getTagMask() != tagMask) {
model->setTagMask(tagMask, scene);

View file

@ -161,8 +161,6 @@ protected:
virtual void doRender(RenderArgs* args) override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
render::hifi::Tag getTagMask() const override;
void setIsVisibleInSecondaryCamera(bool value) override;
void setRenderLayer(RenderLayer value) override;
void setPrimitiveMode(PrimitiveMode value) override;

View file

@ -3007,6 +3007,26 @@ void EntityItem::setPrimitiveMode(PrimitiveMode value) {
}
}
bool EntityItem::getCauterized() const {
return resultWithReadLock<bool>([&] {
return _cauterized;
});
}
void EntityItem::setCauterized(bool value) {
bool changed = false;
withWriteLock([&] {
if (_cauterized != value) {
changed = true;
_cauterized = value;
}
});
if (changed) {
emit requestRenderUpdate();
}
}
bool EntityItem::getIgnorePickIntersection() const {
return resultWithReadLock<bool>([&] {
return _ignorePickIntersection;

View file

@ -303,6 +303,9 @@ public:
bool getCanCastShadow() const;
void setCanCastShadow(bool value);
void setCauterized(bool value);
bool getCauterized() const;
inline bool isVisible() const { return getVisible(); }
inline bool isInvisible() const { return !getVisible(); }
@ -530,9 +533,6 @@ public:
static QString _marketplacePublicKey;
static void retrieveMarketplacePublicKey();
void setCauterized(bool value) { _cauterized = value; }
bool getCauterized() const { return _cauterized; }
float getBoundingRadius() const { return _boundingRadius; }
void setSpaceIndex(int32_t index);
int32_t getSpaceIndex() const { return _spaceIndex; }

View file

@ -976,7 +976,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
* by setting the <code>entityHostType</code> parameter in {@link Entities.addEntity} to <code>"avatar"</code>.
* Material entities render as non-scalable spheres if they don't have their parent set.
* @typedef {object} Entities.EntityProperties-Material
* @property {string} materialURL="" - URL to a {@link MaterialResource}. If you append <code>?name</code> to the URL, the
* @property {string} materialURL="" - URL to a {@link MaterialResource}. If you append <code>#name</code> to the URL, the
* material with that name in the {@link MaterialResource} will be applied to the entity. <br />
* Alternatively, set the property value to <code>"materialData"</code> to use the <code>materialData</code> property
* for the {@link MaterialResource} values.
@ -2630,11 +2630,11 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
ENTITY_ITEM_MIN_FRICTION, ENTITY_ITEM_MAX_FRICTION);
ADD_PROPERTY_TO_MAP(PROP_LIFETIME, Lifetime, lifetime, float);
ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, Collisionless, collisionless, bool);
ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, unused, ignoreForCollisions, unused); // legacy support
ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collisionMask, unused);
ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collidesWith, unused);
ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, collisionsWillMove, unused); // legacy support
ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, dynamic, unused);
ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, unused, ignoreForCollisions, bool); // legacy support
ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collisionMask, uint16_t);
ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collidesWith, uint16_t);
ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, collisionsWillMove, bool); // legacy support
ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, dynamic, bool);
ADD_PROPERTY_TO_MAP(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString);
ADD_PROPERTY_TO_MAP(PROP_ACTION_DATA, ActionData, actionData, QByteArray);

View file

@ -14,6 +14,9 @@
#include <stdint.h>
#include <limits>
#include <type_traits>
#include <glm/glm.hpp>
#include <glm/gtx/component_wise.hpp>
@ -85,6 +88,16 @@ struct EntityPropertyInfo {
QVariant maximum;
};
template <typename T>
EntityPropertyInfo makePropertyInfo(EntityPropertyList p, typename std::enable_if<!std::is_integral<T>::value>::type* = 0) {
return EntityPropertyInfo(p);
}
template <typename T>
EntityPropertyInfo makePropertyInfo(EntityPropertyList p, typename std::enable_if<std::is_integral<T>::value>::type* = 0) {
return EntityPropertyInfo(p, std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
}
/// A collection of properties of an entity item used in the scripting API. Translates between the actual properties of an
/// entity and a JavaScript style hash/QScriptValue storing a set of properties. Used in scripting to set/get the complete
/// set of entity item properties via JavaScript hashes/QScriptValues

View file

@ -416,9 +416,10 @@ inline QRect QRect_convertFromScriptValue(const QScriptValue& v, bool& isValid)
T _##n; \
static T _static##N;
#define ADD_PROPERTY_TO_MAP(P, N, n, T) \
{ \
EntityPropertyInfo propertyInfo = EntityPropertyInfo(P); \
EntityPropertyInfo propertyInfo { makePropertyInfo<T>(P) }; \
_propertyInfos[#n] = propertyInfo; \
_enumsToPropertyStrings[P] = #n; \
}

View file

@ -1459,7 +1459,7 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, this, [this, entityIDs, networkReply, minimumAgeToRemove, &certificateID] {
connect(networkReply, &QNetworkReply::finished, this, [this, entityIDs, networkReply, minimumAgeToRemove, certificateID] {
QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object();
jsonObject = jsonObject["data"].toObject();

View file

@ -148,9 +148,13 @@ bool EntityTreeElement::checkFilterSettings(const EntityItemPointer& entity, Pic
(!searchFilter.doesPickLocalEntities() && hostType == entity::HostType::LOCAL)) {
return false;
}
// We only check the collidable filters for non-local entities, because local entities are always collisionless
bool collidable = !entity->getCollisionless() && (entity->getShapeType() != SHAPE_TYPE_NONE);
// We only check the collidable filters for non-local entities, because local entities are always collisionless,
// but picks always include COLLIDABLE (see PickScriptingInterface::getPickFilter()), so if we were to respect
// the getCollisionless() property of Local entities then we would *never* intersect them in a pick.
// An unfortunate side effect of the following code is that Local entities are intersected even if the
// pick explicitly requested only COLLIDABLE entities (but, again, Local entities are always collisionless).
if (hostType != entity::HostType::LOCAL) {
bool collidable = !entity->getCollisionless() && (entity->getShapeType() != SHAPE_TYPE_NONE);
if ((collidable && !searchFilter.doesPickCollidable()) || (!collidable && !searchFilter.doesPickNonCollidable())) {
return false;
}

View file

@ -97,7 +97,7 @@ QString processID(const QString& id) {
return id.mid(id.lastIndexOf(':') + 1);
}
QString getName(const QVariantList& properties) {
QString getModelName(const QVariantList& properties) {
QString name;
if (properties.size() == 3) {
name = properties.at(1).toString();
@ -108,6 +108,17 @@ QString getName(const QVariantList& properties) {
return name;
}
QString getMaterialName(const QVariantList& properties) {
QString name;
if (properties.size() == 1 || properties.at(1).toString().isEmpty()) {
name = properties.at(0).toString();
name = processID(name.left(name.indexOf(QChar('\0'))));
} else {
name = processID(properties.at(1).toString());
}
return name;
}
QString getID(const QVariantList& properties, int index = 0) {
return processID(properties.at(index).toString());
}
@ -300,8 +311,6 @@ QString getString(const QVariant& value) {
return list.isEmpty() ? value.toString() : list.at(0).toString();
}
typedef std::vector<glm::vec3> ShapeVertices;
class AnimationCurve {
public:
QVector<float> values;
@ -510,7 +519,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
blendshapes.append(extracted);
}
} else if (object.name == "Model") {
QString name = getName(object.properties);
QString name = getModelName(object.properties);
QString id = getID(object.properties);
modelIDsToNames.insert(id, name);
@ -829,7 +838,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
} else if (object.name == "Material") {
HFMMaterial material;
MaterialParam materialParam;
material.name = (object.properties.at(1).toString());
material.name = getMaterialName(object.properties);
foreach (const FBXNode& subobject, object.children) {
bool properties = false;
@ -1352,8 +1361,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
}
// NOTE: shapeVertices are in joint-frame
std::vector<ShapeVertices> shapeVertices;
shapeVertices.resize(std::max(1, hfmModel.joints.size()) );
hfmModel.shapeVertices.resize(std::max(1, hfmModel.joints.size()) );
hfmModel.bindExtents.reset();
hfmModel.meshExtents.reset();
@ -1527,7 +1535,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
HFMJoint& joint = hfmModel.joints[jointIndex];
glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform;
ShapeVertices& points = shapeVertices.at(jointIndex);
ShapeVertices& points = hfmModel.shapeVertices.at(jointIndex);
for (int j = 0; j < cluster.indices.size(); j++) {
int oldIndex = cluster.indices.at(j);
@ -1601,7 +1609,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
// transform cluster vertices to joint-frame and save for later
glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform;
ShapeVertices& points = shapeVertices.at(jointIndex);
ShapeVertices& points = hfmModel.shapeVertices.at(jointIndex);
foreach (const glm::vec3& vertex, extracted.mesh.vertices) {
const glm::mat4 vertexTransform = meshToJoint * glm::translate(vertex);
points.push_back(extractTranslation(vertexTransform));
@ -1621,54 +1629,6 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
meshIDsToMeshIndices.insert(it.key(), meshIndex);
}
const float INV_SQRT_3 = 0.57735026918f;
ShapeVertices cardinalDirections = {
Vectors::UNIT_X,
Vectors::UNIT_Y,
Vectors::UNIT_Z,
glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3)
};
// now that all joints have been scanned compute a k-Dop bounding volume of mesh
for (int i = 0; i < hfmModel.joints.size(); ++i) {
HFMJoint& joint = hfmModel.joints[i];
// NOTE: points are in joint-frame
ShapeVertices& points = shapeVertices.at(i);
if (points.size() > 0) {
// compute average point
glm::vec3 avgPoint = glm::vec3(0.0f);
for (uint32_t j = 0; j < points.size(); ++j) {
avgPoint += points[j];
}
avgPoint /= (float)points.size();
joint.shapeInfo.avgPoint = avgPoint;
// compute a k-Dop bounding volume
for (uint32_t j = 0; j < cardinalDirections.size(); ++j) {
float maxDot = -FLT_MAX;
float minDot = FLT_MIN;
for (uint32_t k = 0; k < points.size(); ++k) {
float kDot = glm::dot(cardinalDirections[j], points[k] - avgPoint);
if (kDot > maxDot) {
maxDot = kDot;
}
if (kDot < minDot) {
minDot = kDot;
}
}
joint.shapeInfo.points.push_back(avgPoint + maxDot * cardinalDirections[j]);
joint.shapeInfo.dots.push_back(maxDot);
joint.shapeInfo.points.push_back(avgPoint + minDot * cardinalDirections[j]);
joint.shapeInfo.dots.push_back(-minDot);
}
generateBoundryLinesForDop14(joint.shapeInfo.dots, joint.shapeInfo.avgPoint, joint.shapeInfo.debugLines);
}
}
// attempt to map any meshes to a named model
for (QHash<QString, int>::const_iterator m = meshIDsToMeshIndices.constBegin();
m != meshIDsToMeshIndices.constEnd(); m++) {

View file

@ -32,6 +32,7 @@ static const QString JOINT_FIELD = "joint";
static const QString BLENDSHAPE_FIELD = "bs";
static const QString SCRIPT_FIELD = "script";
static const QString JOINT_NAME_MAPPING_FIELD = "jointMap";
static const QString MATERIAL_MAPPING_FIELD = "materialMap";
class FSTReader {
public:

View file

@ -891,12 +891,14 @@ HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::V
if (!objMaterial.used) {
continue;
}
hfmModel.materials[materialID] = HFMMaterial(objMaterial.diffuseColor,
objMaterial.specularColor,
objMaterial.emissiveColor,
objMaterial.shininess,
objMaterial.opacity);
HFMMaterial& hfmMaterial = hfmModel.materials[materialID];
HFMMaterial& hfmMaterial = hfmModel.materials[materialID] = HFMMaterial(objMaterial.diffuseColor,
objMaterial.specularColor,
objMaterial.emissiveColor,
objMaterial.shininess,
objMaterial.opacity);
hfmMaterial.name = materialID;
hfmMaterial.materialID = materialID;
hfmMaterial._material = std::make_shared<graphics::Material>();
graphics::MaterialPointer modelMaterial = hfmMaterial._material;

View file

@ -362,6 +362,7 @@ MeshPointer Mesh::createIndexedTriangles_P3F(uint32_t numVertices, uint32_t numI
mesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(numIndices * sizeof(uint32_t), (gpu::Byte*) indices), gpu::Element::INDEX_INT32));
}
std::vector<graphics::Mesh::Part> parts;
parts.push_back(graphics::Mesh::Part(0, numIndices, 0, graphics::Mesh::TRIANGLES));
mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(graphics::Mesh::Part), (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL));

View file

@ -154,3 +154,57 @@ QString HFMModel::getModelNameOfMesh(int meshIndex) const {
}
return QString();
}
void HFMModel::computeKdops() {
const float INV_SQRT_3 = 0.57735026918f;
ShapeVertices cardinalDirections = {
Vectors::UNIT_X,
Vectors::UNIT_Y,
Vectors::UNIT_Z,
glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3)
};
if (joints.size() != (int)shapeVertices.size()) {
return;
}
// now that all joints have been scanned compute a k-Dop bounding volume of mesh
for (int i = 0; i < joints.size(); ++i) {
HFMJoint& joint = joints[i];
// NOTE: points are in joint-frame
ShapeVertices& points = shapeVertices.at(i);
glm::quat rotOffset = jointRotationOffsets.contains(i) ? glm::inverse(jointRotationOffsets[i]) : quat();
if (points.size() > 0) {
// compute average point
glm::vec3 avgPoint = glm::vec3(0.0f);
for (uint32_t j = 0; j < points.size(); ++j) {
points[j] = rotOffset * points[j];
avgPoint += points[j];
}
avgPoint /= (float)points.size();
joint.shapeInfo.avgPoint = avgPoint;
// compute a k-Dop bounding volume
for (uint32_t j = 0; j < cardinalDirections.size(); ++j) {
float maxDot = -FLT_MAX;
float minDot = FLT_MIN;
for (uint32_t k = 0; k < points.size(); ++k) {
float kDot = glm::dot(cardinalDirections[j], points[k] - avgPoint);
if (kDot > maxDot) {
maxDot = kDot;
}
if (kDot < minDot) {
minDot = kDot;
}
}
joint.shapeInfo.points.push_back(avgPoint + maxDot * cardinalDirections[j]);
joint.shapeInfo.dots.push_back(maxDot);
joint.shapeInfo.points.push_back(avgPoint + minDot * cardinalDirections[j]);
joint.shapeInfo.dots.push_back(-minDot);
}
generateBoundryLinesForDop14(joint.shapeInfo.dots, joint.shapeInfo.avgPoint, joint.shapeInfo.debugLines);
}
}
}

View file

@ -53,6 +53,8 @@ using ColorType = glm::vec3;
const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
using ShapeVertices = std::vector<glm::vec3>;
// The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h
static const int DRACO_MESH_VERSION = 2;
@ -251,11 +253,6 @@ public:
bool wasCompressed { false };
};
/**jsdoc
* @typedef {object} FBXAnimationFrame
* @property {Quat[]} rotations
* @property {Vec3[]} translations
*/
/// A single animation frame.
class AnimationFrame {
public:
@ -332,10 +329,12 @@ public:
/// given a meshIndex this will return the name of the model that mesh belongs to if known
QString getModelNameOfMesh(int meshIndex) const;
void computeKdops();
QList<QString> blendshapeChannelNames;
QMap<int, glm::quat> jointRotationOffsets;
std::vector<ShapeVertices> shapeVertices;
FlowData flowData;
};

View file

@ -25,6 +25,32 @@ namespace image {
namespace TextureUsage {
/**jsdoc
* <p>Describes the type of texture.</p>
* <p>See also: {@link Material} and
* {@link https://docs.highfidelity.com/create/3d-models/pbr-materials-guide.html|PBR Materials Guide}.</p>
* <table>
* <thead>
* <tr><th>Value</th><th>Name</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td><code>0</code></td><td>Default</td><td>Basic color.</td></tr>
* <tr><td><code>1</code></td><td>Strict</td><td>Basic color. Quality never downgraded.</td></tr>
* <tr><td><code>2</code></td><td>Albedo</td><td>Color for PBR.</td></tr>
* <tr><td><code>3</code></td><td>Normal</td><td>Normal map.</td></tr>
* <tr><td><code>4</code></td><td>Bump</td><td>Bump map.</td></tr>
* <tr><td><code>5</code></td><td>Specular or metallic</td><td>Metallic or not.</td></tr>
* <tr><td><code>6</code></td><td>Roughness</td><td>Rough or matte.</td></tr>
* <tr><td><code>7</code></td><td>Gloss</td><td>Gloss or shine.</td></tr>
* <tr><td><code>8</code></td><td>Emissive</td><td>The amount of light reflected.</td></tr>
* <tr><td><code>9</code></td><td>Cube</td><td>Cubic image for sky boxes.</td></tr>
* <tr><td><code>10</code></td><td>Occlusion or scattering</td><td>How objects or human skin interact with light.</td></tr>
* <tr><td><code>11</code></td><td>Lightmap</td><td>Light map.</td></tr>
* <tr><td><code>12</code></td><td>Unused</td><td>Texture is not currently used.</td></tr>
* </tbody>
* </table>
* @typedef {number} TextureCache.TextureType
*/
enum Type {
DEFAULT_TEXTURE,
STRICT_TEXTURE,

View file

@ -559,8 +559,7 @@ void NetworkMaterial::setLightmapMap(const QUrl& url) {
}
NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) :
graphics::Material(*material._material),
_textures(MapChannel::NUM_MAP_CHANNELS)
graphics::Material(*material._material)
{
_name = material.name.toStdString();
if (!material.albedoTexture.filename.isEmpty()) {
@ -709,7 +708,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) {
bool NetworkMaterial::isMissingTexture() {
for (auto& networkTexture : _textures) {
auto& texture = networkTexture.texture;
auto& texture = networkTexture.second.texture;
if (!texture) {
continue;
}

View file

@ -36,15 +36,21 @@ public:
bool isMissingTexture();
void checkResetOpacityMap();
protected:
friend class Geometry;
class Texture {
public:
QString name;
NetworkTexturePointer texture;
};
using Textures = std::vector<Texture>;
struct MapChannelHash {
std::size_t operator()(MapChannel mapChannel) const {
return static_cast<std::size_t>(mapChannel);
}
};
using Textures = std::unordered_map<MapChannel, Texture, MapChannelHash>;
Textures getTextures() { return _textures; }
protected:
friend class Geometry;
Textures _textures;

View file

@ -25,7 +25,8 @@ class TextureCacheScriptingInterface : public ScriptableResourceCache, public De
// Properties are copied over from ResourceCache (see ResourceCache.h for reason).
/**jsdoc
* API to manage texture cache resources.
* The <code>TextureCache</code> API manages texture cache resources.
*
* @namespace TextureCache
*
* @hifi-interface
@ -47,11 +48,14 @@ public:
TextureCacheScriptingInterface();
/**jsdoc
* Prefetches a texture resource of specific type.
* @function TextureCache.prefetch
* @param {string} url
* @param {number} type
* @param {number} [maxNumPixels=67108864]
* @returns {ResourceObject}
* @variation 0
* @param {string} url - The URL of the texture to prefetch.
* @param {TextureCache.TextureType} type - The type of the texture.
* @param {number} [maxNumPixels=67108864] - The maximum number of pixels to use for the image. If the texture has more
* than this number it is downscaled.
* @returns {ResourceObject} A resource object.
*/
Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS);
@ -59,6 +63,7 @@ signals:
/**jsdoc
* @function TextureCache.spectatorCameraFramebufferReset
* @returns {Signal}
* @deprecated This signal is deprecated and will be removed.
*/
void spectatorCameraFramebufferReset();
};

View file

@ -113,6 +113,7 @@ namespace baker {
hfmModelOut->jointRotationOffsets = input.get3();
hfmModelOut->jointIndices = input.get4();
hfmModelOut->flowData = input.get5();
hfmModelOut->computeKdops();
output = hfmModelOut;
}
};

View file

@ -10,6 +10,62 @@
#include "ModelBakerLogging.h"
#include <QJsonArray>
void processMaterialMapping(MaterialMapping& materialMapping, const QJsonObject& materialMap, const hifi::URL& url) {
auto mappingKeys = materialMap.keys();
for (auto mapping : mappingKeys) {
auto mappingJSON = materialMap[mapping];
if (mappingJSON.isObject()) {
auto mappingValue = mappingJSON.toObject();
// Old subsurface scattering mapping
{
auto scatteringIter = mappingValue.find("scattering");
auto scatteringMapIter = mappingValue.find("scatteringMap");
if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) {
std::shared_ptr<NetworkMaterial> material = std::make_shared<NetworkMaterial>();
if (scatteringIter != mappingValue.end()) {
float scattering = (float)scatteringIter.value().toDouble();
material->setScattering(scattering);
}
if (scatteringMapIter != mappingValue.end()) {
QString scatteringMap = scatteringMapIter.value().toString();
material->setScatteringMap(scatteringMap);
}
material->setDefaultFallthrough(true);
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(),
[](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials.names.push_back("scattering");
materialResource->parsedMaterials.networkMaterials["scattering"] = material;
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>("mat::" + mapping.toStdString(), materialResource));
continue;
}
}
// Material JSON description
{
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(),
[](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url);
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), materialResource));
}
} else if (mappingJSON.isString()) {
auto mappingValue = mappingJSON.toString();
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(),
MaterialCache::instance().getMaterial(url.resolved(mappingValue))));
}
}
}
void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
const auto& mapping = input.get0();
const auto& url = input.get1();
@ -18,56 +74,18 @@ void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, con
auto mappingIter = mapping.find("materialMap");
if (mappingIter != mapping.end()) {
QByteArray materialMapValue = mappingIter.value().toByteArray();
QJsonObject materialMap = QJsonDocument::fromJson(materialMapValue).object();
if (materialMap.isEmpty()) {
QJsonDocument materialMapJSON = QJsonDocument::fromJson(materialMapValue);
if (materialMapJSON.isEmpty()) {
qCDebug(model_baker) << "Material Map found but did not produce valid JSON:" << materialMapValue;
} else if (materialMapJSON.isObject()) {
QJsonObject materialMap = materialMapJSON.object();
processMaterialMapping(materialMapping, materialMap, url);
} else {
auto mappingKeys = materialMap.keys();
for (auto mapping : mappingKeys) {
auto mappingJSON = materialMap[mapping];
if (mappingJSON.isObject()) {
auto mappingValue = mappingJSON.toObject();
// Old subsurface scattering mapping
{
auto scatteringIter = mappingValue.find("scattering");
auto scatteringMapIter = mappingValue.find("scatteringMap");
if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) {
std::shared_ptr<NetworkMaterial> material = std::make_shared<NetworkMaterial>();
if (scatteringIter != mappingValue.end()) {
float scattering = (float)scatteringIter.value().toDouble();
material->setScattering(scattering);
}
if (scatteringMapIter != mappingValue.end()) {
QString scatteringMap = scatteringMapIter.value().toString();
material->setScatteringMap(scatteringMap);
}
material->setDefaultFallthrough(true);
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials.names.push_back("scattering");
materialResource->parsedMaterials.networkMaterials["scattering"] = material;
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>("mat::" + mapping.toStdString(), materialResource));
continue;
}
}
// Material JSON description
{
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url);
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), materialResource));
}
} else if (mappingJSON.isString()) {
auto mappingValue = mappingJSON.toString();
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), MaterialCache::instance().getMaterial(url.resolved(mappingValue))));
QJsonArray materialMapArray = materialMapJSON.array();
for (auto materialMapIter : materialMapArray) {
if (materialMapIter.isObject()) {
QJsonObject materialMap = materialMapIter.toObject();
processMaterialMapping(materialMapping, materialMap, url);
}
}
}

View file

@ -437,8 +437,8 @@ const QVariantMap Geometry::getTextures() const {
QVariantMap textures;
for (const auto& material : _materials) {
for (const auto& texture : material->_textures) {
if (texture.texture) {
textures[texture.name] = texture.texture->getURL();
if (texture.second.texture) {
textures[texture.second.name] = texture.second.texture->getURL();
}
}
}
@ -467,7 +467,7 @@ void Geometry::setTextures(const QVariantMap& textureMap) {
for (auto& material : _materials) {
// Check if any material textures actually changed
if (std::any_of(material->_textures.cbegin(), material->_textures.cend(),
[&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.texture && textureMap.contains(it.name); })) {
[&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.second.texture && textureMap.contains(it.second.name); })) {
// FIXME: The Model currently caches the materials (waste of space!)
// so they must be copied in the Geometry copy-ctor

View file

@ -25,7 +25,8 @@ class ModelCacheScriptingInterface : public ScriptableResourceCache, public Depe
// Properties are copied over from ResourceCache (see ResourceCache.h for reason).
/**jsdoc
* API to manage model cache resources.
* The <code>ModelCache</code> API manages model cache resources.
*
* @namespace ModelCache
*
* @hifi-interface

View file

@ -91,6 +91,9 @@ private:
class ScriptableResource : public QObject {
/**jsdoc
* Information about a cached resource. Created by {@link AnimationCache.prefetch}, {@link ModelCache.prefetch},
* {@link SoundCache.prefetch}, or {@link TextureCache.prefetch}.
*
* @class ResourceObject
*
* @hifi-interface
@ -99,8 +102,8 @@ class ScriptableResource : public QObject {
* @hifi-server-entity
* @hifi-assignment-client
*
* @property {string} url - URL of this resource.
* @property {Resource.State} state - Current loading state.
* @property {string} url - URL of the resource. <em>Read-only.</em>
* @property {Resource.State} state - Current loading state. <em>Read-only.</em>
*/
Q_OBJECT
Q_PROPERTY(QUrl url READ getURL)
@ -109,12 +112,13 @@ class ScriptableResource : public QObject {
public:
/**jsdoc
* The loading state of a resource.
* @typedef {object} Resource.State
* @property {number} QUEUED - The resource is queued up, waiting to be loaded.
* @property {number} LOADING - The resource is downloading.
* @property {number} LOADED - The resource has finished downloaded by is not complete.
* @property {number} LOADED - The resource has finished downloading but is not complete.
* @property {number} FINISHED - The resource has completely finished loading and is ready.
* @property {number} FAILED - Downloading the resource has failed.
* @property {number} FAILED - The resource has failed to download.
*/
enum State {
QUEUED,
@ -129,7 +133,7 @@ public:
virtual ~ScriptableResource() = default;
/**jsdoc
* Release this resource.
* Releases the resource.
* @function ResourceObject#release
*/
Q_INVOKABLE void release();
@ -144,16 +148,16 @@ public:
signals:
/**jsdoc
* Triggered when download progress for this resource has changed.
* Triggered when the resource's download progress changes.
* @function ResourceObject#progressChanged
* @param {number} bytesReceived - Byytes downloaded so far.
* @param {number} bytesReceived - Bytes downloaded so far.
* @param {number} bytesTotal - Total number of bytes in the resource.
* @returns {Signal}
*/
void progressChanged(uint64_t bytesReceived, uint64_t bytesTotal);
/**jsdoc
* Triggered when resource loading state has changed.
* Triggered when the resource's loading state changes.
* @function ResourceObject#stateChanged
* @param {Resource.State} state - New state.
* @returns {Signal}
@ -317,30 +321,63 @@ public:
ScriptableResourceCache(QSharedPointer<ResourceCache> resourceCache);
/**jsdoc
* Get the list of all resource URLs.
* Gets the URLs of all resources in the cache.
* @function ResourceCache.getResourceList
* @returns {string[]}
* @returns {string[]} The URLs of all resources in the cache.
* @example <caption>Report cached resources.</caption>
* // Replace AnimationCache with ModelCache, SoundCache, or TextureCache as appropriate.
*
* var cachedResources = AnimationCache.getResourceList();
* print("Cached resources: " + JSON.stringify(cachedResources));
*/
Q_INVOKABLE QVariantList getResourceList();
/**jsdoc
* @function ResourceCache.updateTotalSize
* @param {number} deltaSize
* @param {number} deltaSize - Delta size.
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE void updateTotalSize(const qint64& deltaSize);
/**jsdoc
* Prefetches a resource.
* @function ResourceCache.prefetch
* @param {string} url - URL of the resource to prefetch.
* @returns {ResourceObject}
* @param {string} url - The URL of the resource to prefetch.
* @returns {ResourceObject} A resource object.
* @example <caption>Prefetch a resource and wait until it has loaded.</caption>
* // Replace AnimationCache with ModelCache, SoundCache, or TextureCache as appropriate.
* // TextureCache has its own version of this function.
*
* var resourceURL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx";
* var resourceObject = AnimationCache.prefetch(resourceURL);
*
* function checkIfResourceLoaded(state) {
* if (state === Resource.State.FINISHED) {
* print("Resource loaded and ready.");
* } else if (state === Resource.State.FAILED) {
* print("Resource not loaded.");
* }
* }
*
* // Resource may have already been loaded.
* print("Resource state: " + resourceObject.state);
* checkIfResourceLoaded(resourceObject.state);
*
* // Resource may still be loading.
* resourceObject.stateChanged.connect(function (state) {
* print("Resource state changed to: " + state);
* checkIfResourceLoaded(state);
* });
*/
Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr, std::numeric_limits<size_t>::max()); }
// FIXME: This function variation shouldn't be in the API.
Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, void* extra, size_t extraHash);
signals:
/**jsdoc
* Triggered when the cache content has changed.
* @function ResourceCache.dirty
* @returns {Signal}
*/

View file

@ -18,6 +18,8 @@
#include <DependencyManager.h>
/**jsdoc
* The <code>Resources</code> API enables the default location for different resource types to be overridden.
*
* @namespace Resources
*
* @hifi-interface
@ -32,15 +34,17 @@ class ResourceScriptingInterface : public QObject, public Dependency {
public:
/**jsdoc
* Overrides a path prefix with an alternative path.
* @function Resources.overrideUrlPrefix
* @param {string} prefix
* @param {string} replacement
* @param {string} prefix - The path prefix to override, e.g., <code>"atp:/"</code>.
* @param {string} replacement - The replacement path for the prefix.
*/
Q_INVOKABLE void overrideUrlPrefix(const QString& prefix, const QString& replacement);
/**jsdoc
* Restores the default path for a specified prefix.
* @function Resources.restoreUrlPrefix
* @param {string} prefix
* @param {string} prefix - The prefix of the resource to restore the path for.
*/
Q_INVOKABLE void restoreUrlPrefix(const QString& prefix) {
overrideUrlPrefix(prefix, "");

View file

@ -86,7 +86,8 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::MicrophoneAudioNoEcho:
case PacketType::MicrophoneAudioWithEcho:
case PacketType::AudioStreamStats:
return static_cast<PacketVersion>(AudioVersion::HighDynamicRangeVolume);
case PacketType::StopInjector:
return static_cast<PacketVersion>(AudioVersion::StopInjectors);
case PacketType::DomainSettings:
return 18; // replace min_avatar_scale and max_avatar_scale with min_avatar_height and max_avatar_height
case PacketType::Ping:

View file

@ -134,6 +134,7 @@ public:
BulkAvatarTraits,
AudioSoloRequest,
BulkAvatarTraitsAck,
StopInjector,
NUM_PACKET_TYPE
};
@ -369,6 +370,7 @@ enum class AudioVersion : PacketVersion {
SpaceBubbleChanges,
HasPersonalMute,
HighDynamicRangeVolume,
StopInjectors
};
enum class MessageDataVersion : PacketVersion {

View file

@ -436,7 +436,7 @@ void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const
float z = scale.z;
float radius = 0.5f * sqrtf(0.5f * (x * x + z * z));
float halfHeight = 0.5f * scale.y - radius;
float MIN_HALF_HEIGHT = 0.1f;
float MIN_HALF_HEIGHT = 0.0f;
if (halfHeight < MIN_HALF_HEIGHT) {
halfHeight = MIN_HALF_HEIGHT;
}

View file

@ -17,6 +17,14 @@ void SphereRegion::translate(const glm::vec3& translation) {
line.second += translation;
}
}
void SphereRegion::scale(float scale) {
for (auto &line : _lines) {
line.first *= scale;
line.second *= scale;
}
}
void SphereRegion::dump(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines) {
for (auto &line : _lines) {
outLines.push_back(line);
@ -127,7 +135,7 @@ bool MultiSphereShape::computeMultiSphereShape(int jointIndex, const QString& na
_jointIndex = jointIndex;
_name = name;
_mode = getExtractionModeByName(_name);
if (_mode == CollisionShapeExtractionMode::None || kdop.size() < 4 || kdop.size() > 200) {
if (_mode == CollisionShapeExtractionMode::None || kdop.size() < 4) {
return false;
}
std::vector<glm::vec3> points;
@ -151,7 +159,9 @@ bool MultiSphereShape::computeMultiSphereShape(int jointIndex, const QString& na
_midPoint /= (int)points.size();
glm::vec3 dimensions = max - min;
if (glm::length(dimensions) == 0.0f) {
return false;
}
for (size_t i = 0; i < points.size(); i++) {
glm::vec3 relPoint = points[i] - _midPoint;
relPoints.push_back(relPoint);
@ -343,6 +353,7 @@ void MultiSphereShape::connectSpheres(int index1, int index2, bool onlyEdges) {
}
void MultiSphereShape::calculateDebugLines() {
std::vector<float> radiuses;
if (_spheres.size() == 1) {
auto sphere = _spheres[0];
calculateSphereLines(_debugLines, sphere._position, sphere._radius);
@ -351,41 +362,25 @@ void MultiSphereShape::calculateDebugLines() {
} else if (_spheres.size() == 4) {
std::vector<glm::vec3> axes;
axes.resize(8);
const float AXIS_DOT_THRESHOLD = 0.3f;
for (size_t i = 0; i < CORNER_SIGNS.size(); i++) {
for (size_t j = 0; j < 4; j++) {
for (size_t j = 0; j < _spheres.size(); j++) {
auto axis = _spheres[j]._position - _midPoint;
glm::vec3 sign = { axis.x != 0.0f ? glm::abs(axis.x) / axis.x : 0.0f,
axis.x != 0.0f ? glm::abs(axis.y) / axis.y : 0.0f ,
axis.z != 0.0f ? glm::abs(axis.z) / axis.z : 0.0f };
bool add = false;
if (sign.x == 0.0f) {
if (sign.y == CORNER_SIGNS[i].y && sign.z == CORNER_SIGNS[i].z) {
add = true;
}
} else if (sign.y == 0.0f) {
if (sign.x == CORNER_SIGNS[i].x && sign.z == CORNER_SIGNS[i].z) {
add = true;
}
} else if (sign.z == 0.0f) {
if (sign.x == CORNER_SIGNS[i].x && sign.y == CORNER_SIGNS[i].y) {
add = true;
}
} else if (sign == CORNER_SIGNS[i]) {
add = true;
}
if (add) {
if (glm::length(axes[i]) == 0.0f && glm::length(axis) > 0.0f && glm::dot(CORNER_SIGNS[i], glm::normalize(axis)) > AXIS_DOT_THRESHOLD) {
radiuses.push_back(_spheres[j]._radius);
axes[i] = axis;
break;
}
}
}
}
calculateChamferBox(_debugLines, _spheres[0]._radius, axes, _midPoint);
calculateChamferBox(_debugLines, radiuses, axes, _midPoint);
} else if (_spheres.size() == 8) {
std::vector<glm::vec3> axes;
for (size_t i = 0; i < _spheres.size(); i++) {
radiuses.push_back(_spheres[i]._radius);
axes.push_back(_spheres[i]._position - _midPoint);
}
calculateChamferBox(_debugLines, _spheres[0]._radius, axes, _midPoint);
calculateChamferBox(_debugLines, radiuses, axes, _midPoint);
}
}
@ -398,9 +393,9 @@ void MultiSphereShape::connectEdges(std::vector<std::pair<glm::vec3, glm::vec3>>
}
}
void MultiSphereShape::calculateChamferBox(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines, const float& radius, const std::vector<glm::vec3>& axes, const glm::vec3& translation) {
void MultiSphereShape::calculateChamferBox(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines, const std::vector<float>& radiuses, const std::vector<glm::vec3>& axes, const glm::vec3& translation) {
std::vector<std::pair<glm::vec3, glm::vec3>> sphereLines;
calculateSphereLines(sphereLines, glm::vec3(0.0f), radius);
calculateSphereLines(sphereLines, glm::vec3(0.0f), radiuses[0]);
std::vector<SphereRegion> regions = {
SphereRegion({ 1.0f, 1.0f, 1.0f }),
@ -417,6 +412,7 @@ void MultiSphereShape::calculateChamferBox(std::vector<std::pair<glm::vec3, glm:
for (size_t i = 0; i < regions.size(); i++) {
regions[i].extractSphereRegion(sphereLines);
regions[i].scale(radiuses[i]/radiuses[0]);
regions[i].translate(translation + axes[i]);
regions[i].extractEdges(axes[i].y < 0);
regions[i].dump(outLines);

View file

@ -48,6 +48,7 @@ public:
void extractSphereRegion(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines);
void extractEdges(bool reverseY = false);
void translate(const glm::vec3& translation);
void scale(float scale);
void dump(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines);
const glm::vec3& getDirection() const { return _direction; }
const std::vector<glm::vec3>& getEdgesX() const { return _edgesX; }
@ -94,7 +95,7 @@ private:
void calculateSphereLines(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines, const glm::vec3& center, const float& radius,
const int& subdivisions = DEFAULT_SPHERE_SUBDIVISIONS, const glm::vec3& direction = Vectors::UNIT_Y,
const float& percentage = 1.0f, std::vector<glm::vec3>* edge = nullptr);
void calculateChamferBox(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines, const float& radius, const std::vector<glm::vec3>& axes, const glm::vec3& translation);
void calculateChamferBox(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines, const std::vector<float>& radiuses, const std::vector<glm::vec3>& axes, const glm::vec3& translation);
void connectEdges(std::vector<std::pair<glm::vec3, glm::vec3>>& outLines, const std::vector<glm::vec3>& edge1,
const std::vector<glm::vec3>& edge2, bool reverse = false);
void connectSpheres(int index1, int index2, bool onlyEdges = false);

View file

@ -27,7 +27,6 @@
#include <SimpleMovingAverage.h>
#include <gpu/Forward.h>
#include "Plugin.h"
#include "StencilMode.h"
class QOpenGLFramebufferObject;
@ -222,10 +221,6 @@ public:
// for updating plugin-related commands. Mimics the input plugin.
virtual void pluginUpdate() = 0;
virtual StencilMode getStencilMaskMode() const { return StencilMode::NONE; }
using StencilMaskMeshOperator = std::function<void(gpu::Batch&)>;
virtual StencilMaskMeshOperator getStencilMaskMeshOperator() { return nullptr; }
signals:
void recommendedFramebufferSizeChanged(const QSize& size);
void resetSensorsRequested();

View file

@ -416,7 +416,7 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, RenderArgs::RenderMo
void ModelMeshPartPayload::render(RenderArgs* args) {
PerformanceTimer perfTimer("ModelMeshPartPayload::render");
if (!args) {
if (!args || (args->_renderMode == RenderArgs::RenderMode::DEFAULT_RENDER_MODE && _cauterized)) {
return;
}

View file

@ -107,6 +107,7 @@ public:
void render(RenderArgs* args) override;
void setShapeKey(bool invalidateShapeKey, PrimitiveMode primitiveMode, bool useDualQuaternionSkinning);
void setCauterized(bool cauterized) { _cauterized = cauterized; }
// ModelMeshPartPayload functions to perform render
void bindMesh(gpu::Batch& batch) override;
@ -138,6 +139,8 @@ private:
gpu::BufferPointer _meshBlendshapeBuffer;
int _meshNumVertices;
render::ShapeKey _shapeKey { render::ShapeKey::Builder::invalid() };
bool _cauterized { false };
};
namespace render {

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