diff --git a/.gitignore b/.gitignore index 747f613a4b..4b0251156e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ android/**/src/main/assets android/**/gradle* *.class +# Visual Studio +/.vs + # VSCode # List taken from Github Global Ignores master@435c4d92 # https://github.com/github/gitignore/commits/master/Global/VisualStudioCode.gitignore diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 1559ece191..8820bda8f6 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -37,8 +37,14 @@ sudo apt-get -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 li Install build tools: ```bash +# For Ubuntu 18.04 sudo apt-get install cmake ``` +```bash +# For Ubuntu 16.04 +wget https://cmake.org/files/v3.9/cmake-3.9.5-Linux-x86_64.sh +sudo sh cmake-3.9.5-Linux-x86_64.sh --prefix=/usr/local --exclude-subdir +``` Install Python 3: ```bash @@ -61,7 +67,7 @@ git tags Then checkout last tag with: ```bash -git checkout tags/v0.71.0 +git checkout tags/v0.79.0 ``` ### Compiling diff --git a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp index 78a4487284..d5b87af7fa 100644 --- a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp +++ b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp @@ -117,7 +117,8 @@ void RenderThread::setup() { { std::unique_lock lock(_frameLock); } ovr::VrHandler::initVr(); - ovr::VrHandler::setHandler(this); + // Enable KHR_no_error for this context + ovr::VrHandler::setHandler(this, true); makeCurrent(); diff --git a/android/libraries/oculus/src/main/assets/shaders/present.frag b/android/libraries/oculus/src/main/assets/shaders/present.frag new file mode 100644 index 0000000000..f5d96932f8 --- /dev/null +++ b/android/libraries/oculus/src/main/assets/shaders/present.frag @@ -0,0 +1,39 @@ +#version 320 es + +precision highp float; +precision highp sampler2D; + +layout(location = 0) in vec4 vTexCoordLR; + +layout(location = 0) out vec4 FragColorL; +layout(location = 1) out vec4 FragColorR; + +uniform sampler2D sampler; + +// https://software.intel.com/en-us/node/503873 + +// sRGB ====> Linear +vec3 color_sRGBToLinear(vec3 srgb) { + return mix(pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)), srgb / vec3(12.92), vec3(lessThanEqual(srgb, vec3(0.04045)))); +} + +vec4 color_sRGBAToLinear(vec4 srgba) { + return vec4(color_sRGBToLinear(srgba.xyz), srgba.w); +} + +// Linear ====> sRGB +vec3 color_LinearTosRGB(vec3 lrgb) { + return mix(vec3(1.055) * pow(vec3(lrgb), vec3(0.41666)) - vec3(0.055), vec3(lrgb) * vec3(12.92), vec3(lessThan(lrgb, vec3(0.0031308)))); +} + +vec4 color_LinearTosRGBA(vec4 lrgba) { + return vec4(color_LinearTosRGB(lrgba.xyz), lrgba.w); +} + +// FIXME switch to texelfetch for getting from the source texture? +void main() { + //FragColorL = color_LinearTosRGBA(texture(sampler, vTexCoordLR.xy)); + //FragColorR = color_LinearTosRGBA(texture(sampler, vTexCoordLR.zw)); + FragColorL = texture(sampler, vTexCoordLR.xy); + FragColorR = texture(sampler, vTexCoordLR.zw); +} diff --git a/android/libraries/oculus/src/main/assets/shaders/present.vert b/android/libraries/oculus/src/main/assets/shaders/present.vert new file mode 100644 index 0000000000..dfd6b1412f --- /dev/null +++ b/android/libraries/oculus/src/main/assets/shaders/present.vert @@ -0,0 +1,21 @@ +#version 320 es + +layout(location = 0) out vec4 vTexCoordLR; + +void main(void) { + const float depth = 0.0; + const vec4 UNIT_QUAD[4] = vec4[4]( + vec4(-1.0, -1.0, depth, 1.0), + vec4(1.0, -1.0, depth, 1.0), + vec4(-1.0, 1.0, depth, 1.0), + vec4(1.0, 1.0, depth, 1.0) + ); + vec4 pos = UNIT_QUAD[gl_VertexID]; + gl_Position = pos; + vTexCoordLR.xy = pos.xy; + vTexCoordLR.xy += 1.0; + vTexCoordLR.y *= 0.5; + vTexCoordLR.x *= 0.25; + vTexCoordLR.zw = vTexCoordLR.xy; + vTexCoordLR.z += 0.5; +} diff --git a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java index 8ee22749c9..19865e7751 100644 --- a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java +++ b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java @@ -7,6 +7,7 @@ // package io.highfidelity.oculus; +import android.content.res.AssetManager; import android.os.Bundle; import android.util.Log; import android.view.Surface; @@ -24,7 +25,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca private static final String TAG = OculusMobileActivity.class.getSimpleName(); static { System.loadLibrary("oculusMobile"); } - private native void nativeOnCreate(); + private native void nativeOnCreate(AssetManager assetManager); private native static void nativeOnResume(); private native static void nativeOnPause(); private native static void nativeOnSurfaceChanged(Surface s); @@ -53,7 +54,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca mView = new SurfaceView(this); mView.getHolder().addCallback(this); - nativeOnCreate(); + nativeOnCreate(getAssets()); questNativeOnCreate(); } @@ -81,7 +82,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca Log.w(TAG, "QQQ onResume"); super.onResume(); //Reconnect the global reference back to handler - nativeOnCreate(); + nativeOnCreate(getAssets()); questNativeOnResume(); nativeOnResume(); diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 5c644cb132..3937d5f799 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -52,6 +52,8 @@ #include #include // TODO: consider moving to scriptengine.h +#include + #include "entities/AssignmentParentFinder.h" #include "AssignmentDynamicFactory.h" #include "RecordingScriptingInterface.h" @@ -99,6 +101,9 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + // Needed to ensure the creation of the DebugDraw instance on the main thread DebugDraw::getInstance(); @@ -819,6 +824,9 @@ void Agent::aboutToFinish() { DependencyManager::get()->cleanup(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); // cleanup the AudioInjectorManager (and any still running injectors) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 9fa7688778..b1a8aaff96 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -18,16 +18,25 @@ #include "Agent.h" /**jsdoc + * The Agent API enables an assignment client to emulate an avatar. Setting isAvatar = true connects + * the assignment client to the avatar and audio mixers, and enables the {@link Avatar} API to be used. + * * @namespace Agent * * @hifi-assignment-client * - * @property {boolean} isAvatar - * @property {boolean} isPlayingAvatarSound Read-only. - * @property {boolean} isListeningToAudioStream - * @property {boolean} isNoiseGateEnabled - * @property {number} lastReceivedAudioLoudness Read-only. - * @property {Uuid} sessionUUID Read-only. + * @property {boolean} isAvatar - true if the assignment client script is emulating an avatar, otherwise + * false. + * @property {boolean} isPlayingAvatarSound - true if the script has a sound to play, otherwise false. + * Sounds are played when isAvatar is true, from the position and with the orientation of the + * scripted avatar's head. Read-only. + * @property {boolean} isListeningToAudioStream - true if the agent is "listening" to the audio stream from the + * domain, otherwise false. + * @property {boolean} isNoiseGateEnabled - true if the noise gate is enabled, otherwise false. When + * enabled, the input audio stream is blocked (fully attenuated) if it falls below an adaptive threshold. + * @property {number} lastReceivedAudioLoudness - The current loudness of the audio input. Nominal range [0.0 (no + * sound) – 1.0 (the onset of clipping)]. Read-only. + * @property {Uuid} sessionUUID - The unique ID associated with the agent's current session in the domain. Read-only. */ class AgentScriptingInterface : public QObject { Q_OBJECT @@ -54,20 +63,43 @@ public: public slots: /**jsdoc + * Sets whether the script should emulate an avatar. * @function Agent.setIsAvatar - * @param {boolean} isAvatar + * @param {boolean} isAvatar - true if the script emulates an avatar, otherwise false. + * @example Make an assignment client script emulate an avatar. + * (function () { + * Agent.setIsAvatar(true); + * Avatar.displayName = "AC avatar"; + * print("Position: " + JSON.stringify(Avatar.position)); // 0, 0, 0 + * }()); */ void setIsAvatar(bool isAvatar) const { _agent->setIsAvatar(isAvatar); } /**jsdoc + * Checks whether the script is emulating an avatar. * @function Agent.isAvatar - * @returns {boolean} + * @returns {boolean} true if the script is emulating an avatar, otherwise false. + * @example Check whether the agent is emulating an avatar. + * (function () { + * print("Agent is avatar: " + Agent.isAvatar()); + * print("Agent is avatar: " + Agent.isAvatar); // Same result. + * }()); */ bool isAvatar() const { return _agent->isAvatar(); } /**jsdoc + * Plays a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless + * isAvatar == true. * @function Agent.playAvatarSound - * @param {object} avatarSound + * @param {SoundObject} avatarSound - The sound played. + * @example Play a sound from an emulated avatar. + * (function () { + * Agent.isAvatar = true; + * var sound = SoundCache.getSound(Script.resourcesPath() + "sounds/sample.wav"); + * Script.setTimeout(function () { // Give the sound time to load. + * Agent.playAvatarSound(sound); + * }, 1000); + * }()); */ void playAvatarSound(SharedSoundPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index f67c54239e..201e24d4b9 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -97,6 +97,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::RadiusIgnoreRequest, PacketType::RequestsDomainListData, PacketType::PerAvatarGainSet, + PacketType::InjectorGainSet, PacketType::AudioSoloRequest }, this, "queueAudioPacket"); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 90698bfac8..41b72c04d2 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -92,6 +92,9 @@ int AudioMixerClientData::processPackets(ConcurrentAddedStreams& addedStreams) { case PacketType::PerAvatarGainSet: parsePerAvatarGainSet(*packet, node); break; + case PacketType::InjectorGainSet: + parseInjectorGainSet(*packet, node); + break; case PacketType::NodeIgnoreRequest: parseNodeIgnoreRequest(packet, node); break; @@ -197,14 +200,25 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const if (avatarUUID.isNull()) { // set the MASTER avatar gain setMasterAvatarGain(gain); - qCDebug(audio) << "Setting MASTER avatar gain for " << uuid << " to " << gain; + qCDebug(audio) << "Setting MASTER avatar gain for" << uuid << "to" << gain; } else { // set the per-source avatar gain setGainForAvatar(avatarUUID, gain); - qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to " << gain; + qCDebug(audio) << "Setting avatar gain adjustment for hrtf[" << uuid << "][" << avatarUUID << "] to" << gain; } } +void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node) { + QUuid uuid = node->getUUID(); + + uint8_t packedGain; + message.readPrimitive(&packedGain); + float gain = unpackFloatGainFromByte(packedGain); + + setMasterInjectorGain(gain); + qCDebug(audio) << "Setting MASTER injector gain for" << uuid << "to" << gain; +} + void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { auto it = std::find_if(_streams.active.cbegin(), _streams.active.cend(), [nodeID](const MixableStream& mixableStream){ return mixableStream.nodeStreamID.nodeID == nodeID && mixableStream.nodeStreamID.streamID.isNull(); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 653749f619..4a1ca7f9b5 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -63,6 +63,7 @@ public: void negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node); void parseRequestsDomainListData(ReceivedMessage& message); void parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node); + void parseInjectorGainSet(ReceivedMessage& message, const SharedNodePointer& node); void parseNodeIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseRadiusIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); void parseSoloRequest(QSharedPointer message, const SharedNodePointer& node); @@ -84,6 +85,8 @@ public: float getMasterAvatarGain() const { return _masterAvatarGain; } void setMasterAvatarGain(float gain) { _masterAvatarGain = gain; } + float getMasterInjectorGain() const { return _masterInjectorGain; } + void setMasterInjectorGain(float gain) { _masterInjectorGain = gain; } AudioLimiter audioLimiter; @@ -189,6 +192,7 @@ private: int _frameToSendStats { 0 }; float _masterAvatarGain { 1.0f }; // per-listener mixing gain, applied only to avatars + float _masterInjectorGain { 1.0f }; // per-listener mixing gain, applied only to injectors CodecPluginPointer _codec; QString _selectedCodecName; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index a920b45161..cb90df58e5 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -50,8 +50,8 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); -inline float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); +inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -338,8 +338,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -363,8 +363,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } if (!isThrottling) { - updateHRTFParameters(stream, *listenerAudioStream, - listenerData->getMasterAvatarGain()); + updateHRTFParameters(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + listenerData->getMasterInjectorGain()); } return false; }); @@ -381,13 +381,13 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { stream.approximateVolume = approximateVolume(stream, listenerAudioStream); } else { if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) { - addStream(stream, *listenerAudioStream, 0.0f, isSoloing); + addStream(stream, *listenerAudioStream, 0.0f, 0.0f, isSoloing); streams.skipped.push_back(move(stream)); ++stats.activeToSkipped; return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -423,7 +423,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { return true; } - addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), + addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(), listenerData->getMasterInjectorGain(), isSoloing); if (shouldBeInactive(stream)) { @@ -491,7 +491,9 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing) { + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing) { ++stats.totalMixes; auto streamToAdd = mixableStream.positionalStream; @@ -502,13 +504,12 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); + float gain = isEcho ? 1.0f + : (isSoloing ? masterAvatarGain + : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance)); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterListenerGain; - if (!isSoloing) { - gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); - } - const int HRTF_DATASET_INDEX = 1; if (!streamToAdd->lastPopSucceeded()) { @@ -585,8 +586,9 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre } void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, - AvatarAudioStream& listeningNodeStream, - float masterListenerGain) { + AvatarAudioStream& listeningNodeStream, + float masterAvatarGain, + float masterInjectorGain) { auto streamToAdd = mixableStream.positionalStream; // check if this is a server echo of a source back to itself @@ -595,7 +597,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho); + float gain = isEcho ? 1.0f : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -720,6 +723,7 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // injector: skip master gain } // avatar: skip attenuation - it is too costly to approximate @@ -729,19 +733,25 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi float distance = glm::length(relativePosition); return gain / distance; - // avatar: skip master gain - it is constant for all streams + // avatar: skip master gain } -float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) { +float computeGain(float masterAvatarGain, + float masterInjectorGain, + const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition, + float distance) { float gain = 1.0f; // injector: apply attenuation if (streamToAdd.getType() == PositionalAudioStream::Injector) { gain *= reinterpret_cast(&streamToAdd)->getAttenuationRatio(); + // apply master gain + gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away - } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { + } else if (streamToAdd.getType() == PositionalAudioStream::Microphone) { glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition; // source directivity is based on angle of emission, in local coordinates @@ -754,8 +764,8 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo gain *= offAxisCoefficient; - // apply master gain, only to avatars - gain *= masterListenerGain; + // apply master gain + gain *= masterAvatarGain; } auto& audioZones = AudioMixer::getAudioZones(); @@ -797,8 +807,9 @@ float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNo return gain; } -float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, - const glm::vec3& relativePosition) { +float computeAzimuth(const AvatarAudioStream& listeningNodeStream, + const PositionalAudioStream& streamToAdd, + const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation()); glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; diff --git a/assignment-client/src/audio/AudioMixerSlave.h b/assignment-client/src/audio/AudioMixerSlave.h index 3d979da1fc..9765ea8639 100644 --- a/assignment-client/src/audio/AudioMixerSlave.h +++ b/assignment-client/src/audio/AudioMixerSlave.h @@ -57,10 +57,13 @@ private: bool prepareMix(const SharedNodePointer& listener); void addStream(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain, bool isSoloing); + float masterAvatarGain, + float masterInjectorGain, + bool isSoloing); void updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream, AvatarAudioStream& listeningNodeStream, - float masterListenerGain); + float masterAvatarGain, + float masterInjectorGain); void resetHRTFState(AudioMixerClientData::MixableStream& mixableStream); void addStreams(Node& listener, AudioMixerClientData& listenerData); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 33e1034128..9816cebf43 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -253,10 +253,29 @@ void AvatarMixer::start() { int lockWait, nodeTransform, functor; + // Set our query each frame { _entityViewer.queryOctree(); } + // Dirty the hero status if there's been an entity change. + { + if (_dirtyHeroStatus) { + _dirtyHeroStatus = false; + nodeList->nestedEach([](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + std::for_each(cbegin, cend, [](const SharedNodePointer& node) { + if (node->getType() == NodeType::Agent) { + NodeData* nodeData = node->getLinkedData(); + if (nodeData) { + auto& avatar = static_cast(nodeData)->getAvatar(); + avatar.setNeedsHeroCheck(); + } + } + }); + }); + } + } + // Allow nodes to process any pending/queued packets across our worker threads { auto start = usecTimestampNow(); @@ -827,7 +846,7 @@ void AvatarMixer::sendStatsPacket() { QJsonObject avatarsObject; auto nodeList = DependencyManager::get(); - // add stats for each listerner + // add stats for each listener nodeList->eachNode([&](const SharedNodePointer& node) { QJsonObject avatarStats; @@ -851,6 +870,12 @@ void AvatarMixer::sendStatsPacket() { avatarStats["delta_full_vs_avatar_data_kbps"] = (double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble(); } + + if (node->getType() != NodeType::Agent) { // Nodes that aren't avatars + const QString displayName + { node->getType() == NodeType::EntityScriptServer ? "ENTITY SCRIPT SERVER" : "ENTITY SERVER" }; + avatarStats["display_name"] = displayName; + } } avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats; @@ -973,19 +998,30 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { const QString CONNECTION_RATE = "connection_rate"; auto nodeList = DependencyManager::get(); - auto defaultConnectionRate = nodeList->getMaxConnectionRate(); - int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate); - nodeList->setMaxConnectionRate(connectionRate); + bool success; + int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toString().toInt(&success); + if (success) { + nodeList->setMaxConnectionRate(connectionRate); + } + } + + { // Fraction of downstream bandwidth reserved for 'hero' avatars: + static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; + if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { + float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); + _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); + qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; + } } const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; - float settingMinHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); + float settingMinHeight = avatarMixerGroupObject[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); _domainMinimumHeight = glm::clamp(settingMinHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); static const QString MAX_HEIGHT_OPTION = "max_avatar_height"; - float settingMaxHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); + float settingMaxHeight = avatarMixerGroupObject[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); _domainMaximumHeight = glm::clamp(settingMaxHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); // make sure that the domain owner didn't flip min and max @@ -997,11 +1033,11 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { << "and a maximum avatar height of" << _domainMaximumHeight; static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; - _slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION] + _slaveSharedData.skeletonURLWhitelist = avatarMixerGroupObject[AVATAR_WHITELIST_OPTION] .toString().split(',', QString::KeepEmptyParts); static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar"; - _slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION] + _slaveSharedData.skeletonReplacementURL = avatarMixerGroupObject[REPLACEMENT_AVATAR_OPTION] .toString(); if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) { @@ -1018,9 +1054,12 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { void AvatarMixer::setupEntityQuery() { _entityViewer.init(); + EntityTreePointer entityTree = _entityViewer.getTree(); DependencyManager::registerInheritance(); - DependencyManager::set(_entityViewer.getTree()); - _slaveSharedData.entityTree = _entityViewer.getTree(); + DependencyManager::set(entityTree); + + connect(entityTree.get(), &EntityTree::addingEntityPointer, this, &AvatarMixer::entityAdded); + connect(entityTree.get(), &EntityTree::deletingEntityPointer, this, &AvatarMixer::entityChange); // ES query: {"avatarPriority": true, "type": "Zone"} QJsonObject priorityZoneQuery; @@ -1028,6 +1067,7 @@ void AvatarMixer::setupEntityQuery() { priorityZoneQuery["type"] = "Zone"; _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery); + _slaveSharedData.entityTree = entityTree; } void AvatarMixer::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -1064,6 +1104,25 @@ void AvatarMixer::handleOctreePacket(QSharedPointer message, Sh } } +void AvatarMixer::entityAdded(EntityItem* entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + entity->registerChangeHandler([this](const EntityItemID& entityItemID) { + entityChange(); + }); + } +} + +void AvatarMixer::entityRemoved(EntityItem * entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + } +} + +void AvatarMixer::entityChange() { + _dirtyHeroStatus = true; +} + void AvatarMixer::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 9393ea6c56..10dff5e8a4 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -34,8 +34,8 @@ public: static bool shouldReplicateTo(const Node& from, const Node& to) { return to.getType() == NodeType::DownstreamAvatarMixer && - to.getPublicSocket() != from.getPublicSocket() && - to.getLocalSocket() != from.getLocalSocket(); + to.getPublicSocket() != from.getPublicSocket() && + to.getLocalSocket() != from.getLocalSocket(); } public slots: @@ -46,6 +46,11 @@ public slots: void sendStatsPacket() override; + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); + private slots: void queueIncomingPacket(QSharedPointer message, SharedNodePointer node); void handleAdjustAvatarSorting(QSharedPointer message, SharedNodePointer senderNode); @@ -80,6 +85,7 @@ private: // Attach to entity tree for avatar-priority zone info. EntityTreeHeadlessViewer _entityViewer; + bool _dirtyHeroStatus { true }; // Dirty the needs-hero-update // FIXME - new throttling - use these values somehow float _trailingMixRatio { 0.0f }; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index cef4383aee..1b86e0dff2 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -129,30 +129,24 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = getPosition(); + glm::vec3 oldPosition = _avatar->getClientGlobalPosition(); + bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload if (!_avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()))) { return false; } - auto newPosition = getPosition(); - if (newPosition != oldPosition) { -//#define AVATAR_HERO_TEST_HACK -#ifdef AVATAR_HERO_TEST_HACK - { - const static QString heroKey { "HERO" }; - _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey)); - } -#else + // Regardless of what the client says, restore the priority as we know it without triggering any update. + _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); + + auto newPosition = _avatar->getClientGlobalPosition(); + if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition, false } ; + FindPriorityZone findPriorityZone { newPosition } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); - //if (findPriorityZone.isInPriorityZone) { - // qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - //} -#endif + _avatar->setNeedsHeroCheck(false); } return true; @@ -337,7 +331,7 @@ void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedDa // the returned set traits packet uses the trait version from the incoming packet // so the client knows they should not overwrite if they have since changed the trait - _avatar->packTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion); + AvatarTraits::packVersionedTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion, *_avatar); auto nodeList = DependencyManager::get(); nodeList->sendPacket(std::move(packet), sendingNode); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e59c81f4b7..32c944f5b8 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -43,12 +43,14 @@ void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) { void AvatarMixerSlave::configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio) { + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction) { _begin = begin; _end = end; _lastFrameTimestamp = lastFrameTimestamp; _maxKbpsPerNode = maxKbpsPerNode; _throttlingRatio = throttlingRatio; + _avatarHeroFraction = priorityReservedFraction; } void AvatarMixerSlave::harvestStats(AvatarMixerSlaveStats& stats) { @@ -139,7 +141,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis if (lastReceivedVersion > lastSentVersionRef) { bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // there is an update to this trait, add it to the traits packet - bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion); + bytesWritten += AvatarTraits::packVersionedTrait(traitType, traitsPacketList, + lastReceivedVersion, *sendingAvatar); // update the last sent version lastSentVersionRef = lastReceivedVersion; // Remember which versions we sent in this particular packet @@ -194,7 +197,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // this instance version exists and has never been sent or is newer so we need to send it - bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion); + bytesWritten += AvatarTraits::packVersionedTraitInstance(traitType, instanceID, traitsPacketList, + receivedVersion, *sendingAvatar); if (sentInstanceIt != sentIDValuePairs.end()) { sentInstanceIt->value = receivedVersion; @@ -308,7 +312,6 @@ namespace { } // Close anonymous namespace. void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) { - const float AVATAR_HERO_FRACTION { 0.4f }; const Node* destinationNode = node.data(); auto nodeList = DependencyManager::get(); @@ -343,7 +346,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // max number of avatarBytes per frame (13 900, typical) const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); - const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical + const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * _avatarHeroFraction); // 5555, typical // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; @@ -469,8 +472,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) SortableAvatar(avatarNodeData, sourceAvatarNode, lastEncodeTime)); } - // If Avatar A's PAL WAS open but is no longer open, AND - // Avatar A is ignoring Avatar B OR Avatar B is ignoring Avatar A... + // If Node A's PAL WAS open but is no longer open, AND + // Node A is ignoring Avatar B OR Node B is ignoring Avatar A... // // This is a bit heavy-handed still - there are cases where a kill packet // will be sent when it doesn't need to be (but where it _should_ be OK to send). @@ -539,7 +542,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const MixerAvatar* sourceAvatar = sourceNodeData->getConstAvatarData(); // Typically all out-of-view avatars but such avatars' priorities will rise with time: - bool isLowerPriority = currentVariant != kHero && sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; // XXX: hero handling? + bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; if (isLowerPriority) { detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData; @@ -548,8 +551,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; destinationNodeData->incrementAvatarInView(); - // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO - // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. + // If the time that the mixer sent AVATAR DATA about Avatar B to Node A is BEFORE OR EQUAL TO + // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Node A. if (sourceAvatar->hasProcessedFirstIdentity() && destinationNodeData->getLastBroadcastTime(sourceNode->getLocalID()) <= sourceNodeData->getIdentityChangeTimestamp()) { identityBytesSent += sendIdentityPacket(*identityPacketList, sourceNodeData, *destinationNode); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index 8c5ad6b181..f14e50e11f 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -110,7 +110,8 @@ public: void configure(ConstIter begin, ConstIter end); void configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio); + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction); void processIncomingPackets(const SharedNodePointer& node); void broadcastAvatarData(const SharedNodePointer& node); @@ -140,6 +141,7 @@ private: p_high_resolution_clock::time_point _lastFrameTimestamp; float _maxKbpsPerNode { 0.0f }; float _throttlingRatio { 0.0f }; + float _avatarHeroFraction { 0.4f }; AvatarMixerSlaveStats _stats; SlaveSharedData* _sharedData; diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index 013d914cbe..357b347a94 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -76,7 +76,8 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; _configure = [=](AvatarMixerSlave& slave) { - slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); + slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio, + _priorityReservedFraction); }; run(begin, end); } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index 71a9ace0d3..b05abde2a3 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -73,7 +73,10 @@ public: void each(std::function functor); void setNumThreads(int numThreads); - int numThreads() { return _numThreads; } + int numThreads() const { return _numThreads; } + + void setPriorityReservedFraction(float fraction) { _priorityReservedFraction = fraction; } + float getPriorityReservedFraction() const { return _priorityReservedFraction; } private: void run(ConstIter begin, ConstIter end); @@ -91,7 +94,11 @@ private: ConditionVariable _poolCondition; void (AvatarMixerSlave::*_function)(const SharedNodePointer& node); std::function _configure; + + // Set from Domain Settings: + float _priorityReservedFraction { 0.4f }; int _numThreads { 0 }; + int _numStarted { 0 }; // guarded by _mutex int _numFinished { 0 }; // guarded by _mutex int _numStopped { 0 }; // guarded by _mutex diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 4c3ded4582..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -19,11 +19,12 @@ class MixerAvatar : public AvatarData { public: - bool getHasPriority() const { return _hasPriority; } - void setHasPriority(bool hasPriority) { _hasPriority = hasPriority; } + bool getNeedsHeroCheck() const { return _needsHeroCheck; } + void setNeedsHeroCheck(bool needsHeroCheck = true) + { _needsHeroCheck = needsHeroCheck; } private: - bool _hasPriority { false }; + bool _needsHeroCheck { false }; }; using MixerAvatarSharedPointer = std::shared_ptr; diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index e93be897d5..e5df411099 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -20,25 +20,29 @@ /**jsdoc * The Avatar API is used to manipulate scriptable avatars on the domain. This API is a subset of the - * {@link MyAvatar} API. + * {@link MyAvatar} API. To enable this API, set {@link Agent|Agent.isAvatar} to true. + * + *

For Interface, client entity, and avatar scripts, see {@link MyAvatar}.

* - *

Note: In the examples, use "Avatar" instead of "MyAvatar".

- * * @namespace Avatar * * @hifi-assignment-client * - * @property {Vec3} position - * @property {number} scale - * @property {number} density Read-only. - * @property {Vec3} handPosition - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. + * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. + * @property {Vec3} position - The position of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @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} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. * Yaw is sometimes called "heading". * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is * sometimes called "elevation". * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is * sometimes called "bank". - * @property {Quat} orientation + * @property {Quat} orientation - The orientation of the avatar. * @property {Quat} headOrientation - The orientation of the avatar's head. * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is * sometimes called "elevation". @@ -46,79 +50,37 @@ * head. Yaw is sometimes called "heading". * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @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 {string} displayName - The avatar's display name. + * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar mixer + * rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true 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 lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
+ * Deprecated: Use avatar entities instead. * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @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. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. * - * @borrows MyAvatar.getDomainMinScale as getDomainMinScale - * @borrows MyAvatar.getDomainMaxScale as getDomainMaxScale - * @borrows MyAvatar.canMeasureEyeHeight as canMeasureEyeHeight - * @borrows MyAvatar.getEyeHeight as getEyeHeight - * @borrows MyAvatar.getHeight as getHeight - * @borrows MyAvatar.setHandState as setHandState - * @borrows MyAvatar.getHandState as getHandState - * @borrows MyAvatar.setRawJointData as setRawJointData - * @borrows MyAvatar.setJointData as setJointData - * @borrows MyAvatar.setJointRotation as setJointRotation - * @borrows MyAvatar.setJointTranslation as setJointTranslation - * @borrows MyAvatar.clearJointData as clearJointData - * @borrows MyAvatar.isJointDataValid as isJointDataValid - * @borrows MyAvatar.getJointRotation as getJointRotation - * @borrows MyAvatar.getJointTranslation as getJointTranslation - * @borrows MyAvatar.getJointRotations as getJointRotations - * @borrows MyAvatar.getJointTranslations as getJointTranslations - * @borrows MyAvatar.setJointRotations as setJointRotations - * @borrows MyAvatar.setJointTranslations as setJointTranslations - * @borrows MyAvatar.clearJointsData as clearJointsData - * @borrows MyAvatar.getJointIndex as getJointIndex - * @borrows MyAvatar.getJointNames as getJointNames - * @borrows MyAvatar.setBlendshape as setBlendshape - * @borrows MyAvatar.getAttachmentsVariant as getAttachmentsVariant - * @borrows MyAvatar.setAttachmentsVariant as setAttachmentsVariant - * @borrows MyAvatar.updateAvatarEntity as updateAvatarEntity - * @borrows MyAvatar.clearAvatarEntity as clearAvatarEntity - * @borrows MyAvatar.setForceFaceTrackerConnected as setForceFaceTrackerConnected - * @borrows MyAvatar.getAttachmentData as getAttachmentData - * @borrows MyAvatar.setAttachmentData as setAttachmentData - * @borrows MyAvatar.attach as attach - * @borrows MyAvatar.detachOne as detachOne - * @borrows MyAvatar.detachAll as detachAll - * @borrows MyAvatar.getAvatarEntityData as getAvatarEntityData - * @borrows MyAvatar.setAvatarEntityData as setAvatarEntityData - * @borrows MyAvatar.getSensorToWorldMatrix as getSensorToWorldMatrix - * @borrows MyAvatar.getSensorToWorldScale as getSensorToWorldScale - * @borrows MyAvatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix - * @borrows MyAvatar.getControllerRightHandMatrix as getControllerRightHandMatrix - * @borrows MyAvatar.getDataRate as getDataRate - * @borrows MyAvatar.getUpdateRate as getUpdateRate - * @borrows MyAvatar.displayNameChanged as displayNameChanged - * @borrows MyAvatar.sessionDisplayNameChanged as sessionDisplayNameChanged - * @borrows MyAvatar.skeletonModelURLChanged as skeletonModelURLChanged - * @borrows MyAvatar.lookAtSnappingChanged as lookAtSnappingChanged - * @borrows MyAvatar.sessionUUIDChanged as sessionUUIDChanged - * @borrows MyAvatar.sendAvatarDataPacket as sendAvatarDataPacket - * @borrows MyAvatar.sendIdentityPacket as sendIdentityPacket - * @borrows MyAvatar.setJointMappingsFromNetworkReply as setJointMappingsFromNetworkReply - * @borrows MyAvatar.setSessionUUID as setSessionUUID - * @borrows MyAvatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - * @borrows MyAvatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - * @borrows MyAvatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - * @borrows MyAvatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - * @borrows MyAvatar.getTargetScale as getTargetScale - * @borrows MyAvatar.resetLastSent as resetLastSent + * @example Create a scriptable avatar. + * (function () { + * Agent.setIsAvatar(true); + * print("Position: " + JSON.stringify(Avatar.position)); // 0, 0, 0 + * }()); */ class ScriptableAvatar : public AvatarData, public Dependency { @@ -132,15 +94,17 @@ public: ScriptableAvatar(); /**jsdoc + * Starts playing an animation on the avatar. * @function Avatar.startAnimation - * @param {string} url - * @param {number} [fps=30] - * @param {number} [priority=1] - * @param {boolean} [loop=false] - * @param {boolean} [hold=false] - * @param {number} [firstFrame=0] - * @param {number} [lastFrame=3.403e+38] - * @param {string[]} [maskedJoints=[]] + * @param {string} url - The animation file's URL. Animation files need to be in the FBX format but only need to contain + * the avatar skeleton and animation data. + * @param {number} [fps=30] - The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. + * @param {number} [priority=1] - Not used. + * @param {boolean} [loop=false] - true if the animation should loop, false if it shouldn't. + * @param {boolean} [hold=false] - Not used. + * @param {number} [firstFrame=0] - The frame at which the animation starts. + * @param {number} [lastFrame=3.403e+38] - The frame at which the animation stops. + * @param {string[]} [maskedJoints=[]] - The names of joints that should not be animated. */ /// Allows scripts to run animations. Q_INVOKABLE void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false, @@ -148,39 +112,37 @@ public: const QStringList& maskedJoints = QStringList()); /**jsdoc + * Stops playing the current animation. * @function Avatar.stopAnimation */ Q_INVOKABLE void stopAnimation(); /**jsdoc + * Gets the details of the current avatar animation that is being or was recently played. * @function Avatar.getAnimationDetails - * @returns {Avatar.AnimationDetails} + * @returns {Avatar.AnimationDetails} The current or recent avatar animation. + * @example Report the current animation details. + * var animationDetails = Avatar.getAnimationDetails(); + * print("Animation details: " + JSON.stringify(animationDetails)); */ Q_INVOKABLE AnimationDetails getAnimationDetails(); /**jsdoc - * Get the names of all the joints in the current avatar. - * @function MyAvatar.getJointNames - * @returns {string[]} The joint names. - * @example Report the names of all the joints in your current avatar. - * print(JSON.stringify(MyAvatar.getJointNames())); - */ + * @comment Uses the base class's JSDoc. + */ Q_INVOKABLE virtual QStringList getJointNames() const override; /**jsdoc - * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. - * @function MyAvatar.getJointIndex - * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. - * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); - */ + * @comment Uses the base class's JSDoc. + */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const override; virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + /**jsdoc + * @comment Uses the base class's JSDoc. + */ int sendAvatarDataPacket(bool sendAll = false) override; virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override; @@ -192,32 +154,42 @@ public: void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } - /**jsdoc - * Potentially Very Expensive. Do not use. + /**jsdoc + * Gets details of all avatar entities. + *

Warning: Potentially an expensive call. Do not use if possible.

* @function Avatar.getAvatarEntityData - * @returns {object} + * @returns {AvatarEntityMap} Details of the avatar entities. + * @example Report the current avatar entities. + * var avatarEntityData = Avatar.getAvatarEntityData(); + * print("Avatar entities: " + JSON.stringify(avatarEntityData)); */ Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override; /**jsdoc - * @function MyAvatar.setAvatarEntityData - * @param {object} avatarEntityData - */ + * Sets all avatar entities from an object. + *

Warning: Potentially an expensive call. Do not use if possible.

+ * @function Avatar.setAvatarEntityData + * @param {AvatarEntityMap} avatarEntityData - Details of the avatar entities. + */ Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; /**jsdoc - * @function MyAvatar.updateAvatarEntity - * @param {Uuid} entityID - * @param {string} entityData + * @comment Uses the base class's JSDoc. */ Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; public slots: + /**jsdoc + * @function Avatar.update + * @param {number} deltaTime - Delta time. + * @deprecated This function is deprecated and will be removed. + */ void update(float deltatime); /**jsdoc - * @function MyAvatar.setJointMappingsFromNetworkReply - */ + * @function Avatar.setJointMappingsFromNetworkReply + * @deprecated This function is deprecated and will be removed. + */ void setJointMappingsFromNetworkReply(); private: diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 581d854909..06632dabb0 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include "../AssignmentDynamicFactory.h" @@ -471,77 +470,7 @@ void EntityServer::startDynamicDomainVerification() { qCDebug(entities) << "Starting Dynamic Domain Verification..."; EntityTreePointer tree = std::static_pointer_cast(_tree); - QHash localMap(tree->getEntityCertificateIDMap()); - - QHashIterator i(localMap); - qCDebug(entities) << localMap.size() << "entities in _entityCertificateIDMap"; - while (i.hasNext()) { - i.next(); - const auto& certificateID = i.key(); - const auto& entityID = i.value(); - - EntityItemPointer entity = tree->findEntityByEntityItemID(entityID); - - if (entity) { - if (!entity->getProperties().verifyStaticCertificateProperties()) { - qCDebug(entities) << "During Dynamic Domain Verification, a certified entity with ID" << entityID << "failed" - << "static certificate verification."; - // Delete the entity if it doesn't pass static certificate verification - tree->withWriteLock([&] { - tree->deleteEntity(entityID, true); - }); - } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest; - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); - requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); - QJsonObject request; - request["certificate_id"] = certificateID; - networkRequest.setUrl(requestURL); - - QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - - connect(networkReply, &QNetworkReply::finished, this, [this, entityID, networkReply] { - EntityTreePointer tree = std::static_pointer_cast(_tree); - - QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); - jsonObject = jsonObject["data"].toObject(); - - if (networkReply->error() == QNetworkReply::NoError) { - QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); - if (jsonObject["domain_id"].toString() != thisDomainID) { - EntityItemPointer entity = tree->findEntityByEntityItemID(entityID); - if (!entity) { - qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; - networkReply->deleteLater(); - return; - } - if (entity->getAge() > (_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS/MSECS_PER_SECOND)) { - qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() - << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; - tree->withWriteLock([&] { - tree->deleteEntity(entityID, true); - }); - } else { - qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; - } - } else { - qCDebug(entities) << "Entity passed dynamic domain verification:" << entityID; - } - } else { - qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; NOT deleting entity" << entityID - << "More info:" << jsonObject; - } - - networkReply->deleteLater(); - }); - } - } else { - qCWarning(entities) << "During DDV, an entity with ID" << entityID << "was NOT found in the Entity Tree!"; - } - } + tree->startDynamicDomainVerificationOnServer((float) _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS / MSECS_PER_SECOND); int nextInterval = qrand() % ((_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS + 1) - _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS) + _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; qCDebug(entities) << "Restarting Dynamic Domain Verification timer for" << nextInterval / 1000 << "seconds"; diff --git a/cmake/macros/TargetOculusMobile.cmake b/cmake/macros/TargetOculusMobile.cmake index 3eaa008b14..f5229845a9 100644 --- a/cmake/macros/TargetOculusMobile.cmake +++ b/cmake/macros/TargetOculusMobile.cmake @@ -1,6 +1,6 @@ macro(target_oculus_mobile) - set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculus/VrApi) + set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculus_1.22/VrApi) # Mobile SDK set(OVR_MOBILE_INCLUDE_DIRS ${INSTALL_DIR}/Include) diff --git a/cmake/macros/TargetPython.cmake b/cmake/macros/TargetPython.cmake index cd0ea0f34c..2c055cf8bc 100644 --- a/cmake/macros/TargetPython.cmake +++ b/cmake/macros/TargetPython.cmake @@ -1,7 +1,7 @@ macro(TARGET_PYTHON) if (NOT HIFI_PYTHON_EXEC) # Find the python interpreter - if (CAME_VERSION VERSION_LESS 3.12) + if (CMAKE_VERSION VERSION_LESS 3.12) # this logic is deprecated in CMake after 3.12 # FIXME eventually we should make 3.12 the min cmake verion and just use the Python3 find_package path set(Python_ADDITIONAL_VERSIONS 3) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 140c7d6c17..352106dcf7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1310,6 +1310,15 @@ "placeholder": "50", "default": "50", "advanced": true + }, + { + "name": "priority_fraction", + "type": "double", + "label": "Hero Bandwidth", + "help": "Fraction of downstream bandwidth reserved for avatars in 'Hero' zones", + "placeholder": "0.40", + "default": "0.40", + "advanced": true } ] }, diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8d5cb165cb..400dc3642d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1734,7 +1734,7 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointerreadPrimitive(&remoteHasExistingData); if (remoteHasExistingData) { constexpr size_t UUID_SIZE_BYTES = 16; auto idData = message->read(UUID_SIZE_BYTES); id = QUuid::fromRfc4122(idData); - message->readPrimitive(&version); - qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")"; + message->readPrimitive(&dataVersion); + qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << dataVersion << ")"; } else { qCDebug(domain_server) << "Entity server does not have existing data"; } @@ -1782,11 +1782,11 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerwritePrimitive(false); } else { - qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; QFile file(entityFilePath); if (file.open(QIODevice::ReadOnly)) { reply->writePrimitive(true); diff --git a/hifi_android.py b/hifi_android.py index b8a606a82f..42b472e960 100644 --- a/hifi_android.py +++ b/hifi_android.py @@ -45,10 +45,10 @@ ANDROID_PACKAGES = { 'sharedLibFolder': 'lib', 'includeLibs': ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so'] }, - 'oculus': { - 'file': 'ovr_sdk_mobile_1.19.0.zip', - 'versionId': 's_RN1vlEvUi3pnT7WPxUC4pQ0RJBs27y', - 'checksum': '98f0afb62861f1f02dd8110b31ed30eb', + 'oculus_1.22': { + 'file': 'ovr_sdk_mobile_1.22.zip', + 'versionId': 'InhomR5gwkzyiLAelH3X9k4nvV3iIpA_', + 'checksum': '1ac3c5b0521e5406f287f351015daff8', 'sharedLibFolder': 'VrApi/Libs/Android/arm64-v8a/Release', 'includeLibs': ['libvrapi.so'] }, diff --git a/interface/resources/controllers/keyboardMouse.json b/interface/resources/controllers/keyboardMouse.json index 74c11203ef..9b3c711c63 100644 --- a/interface/resources/controllers/keyboardMouse.json +++ b/interface/resources/controllers/keyboardMouse.json @@ -5,6 +5,7 @@ { "from": "Keyboard.D", "when": ["Keyboard.RightMouseButton", "!Keyboard.Control"], "to": "Actions.LATERAL_RIGHT" }, { "from": "Keyboard.E", "when": "!Keyboard.Control", "to": "Actions.LATERAL_RIGHT" }, { "from": "Keyboard.Q", "when": "!Keyboard.Control", "to": "Actions.LATERAL_LEFT" }, + { "from": "Keyboard.T", "when": "!Keyboard.Control", "to": "Actions.TogglePushToTalk" }, { "comment" : "Mouse turn need to be small continuous increments", "from": { "makeAxis" : [ diff --git a/interface/resources/icons/tablet-icons/mic-clip-i.svg b/interface/resources/icons/tablet-icons/mic-clip-i.svg new file mode 100644 index 0000000000..f912c1e744 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-clip-i.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-gate-i.svg b/interface/resources/icons/tablet-icons/mic-gate-i.svg new file mode 100644 index 0000000000..8255174532 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-gate-i.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/icons/tablet-icons/mic-mute-a.svg b/interface/resources/icons/tablet-icons/mic-mute-a.svg index 9dc2c53443..67eafc27c8 100644 --- a/interface/resources/icons/tablet-icons/mic-mute-a.svg +++ b/interface/resources/icons/tablet-icons/mic-mute-a.svg @@ -1,25 +1,3 @@ - - - - - - - - - - - - - - - + + diff --git a/interface/resources/icons/tablet-icons/mic-mute-i.svg b/interface/resources/icons/tablet-icons/mic-mute-i.svg index 9dc2c53443..63af1b0da8 100644 --- a/interface/resources/icons/tablet-icons/mic-mute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-mute-i.svg @@ -1,25 +1,3 @@ - - - - - - - - - - - - - - - + + diff --git a/interface/resources/icons/tablet-icons/mic-mute.svg b/interface/resources/icons/tablet-icons/mic-mute.svg deleted file mode 100644 index bd42fded05..0000000000 --- a/interface/resources/icons/tablet-icons/mic-mute.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-ptt-a.svg b/interface/resources/icons/tablet-icons/mic-ptt-a.svg new file mode 100644 index 0000000000..e6df3c69d7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-a.svg @@ -0,0 +1 @@ +mic-ptt-a \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-ptt-i.svg b/interface/resources/icons/tablet-icons/mic-ptt-i.svg new file mode 100644 index 0000000000..2141ea5229 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-ptt-i.svg @@ -0,0 +1,24 @@ + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-unmute-a.svg b/interface/resources/icons/tablet-icons/mic-unmute-a.svg index b1464f207d..0bf7677017 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-a.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-a.svg @@ -1,70 +1,3 @@ - - - -image/svg+xml \ No newline at end of file + + + diff --git a/interface/resources/icons/tablet-icons/mic-unmute-i.svg b/interface/resources/icons/tablet-icons/mic-unmute-i.svg index c4eda55cbc..0bf7677017 100644 --- a/interface/resources/icons/tablet-icons/mic-unmute-i.svg +++ b/interface/resources/icons/tablet-icons/mic-unmute-i.svg @@ -1,22 +1,3 @@ - - - - - - - - - - - - - + + diff --git a/interface/resources/icons/tablet-icons/mic.svg b/interface/resources/icons/tablet-icons/mic.svg index 30b46d18dd..c961351b14 100644 --- a/interface/resources/icons/tablet-icons/mic.svg +++ b/interface/resources/icons/tablet-icons/mic.svg @@ -59,4 +59,4 @@ d="m 27.9,20.9 c 0,0 0,-3.6 0,-3.8 0,-0.7 -0.6,-1.2 -1.3,-1.2 -0.7,0 -1.2,0.6 -1.2,1.3 0,0.2 0,3.4 0,3.7 0,2.6 -2.4,4.8 -5.3,4.8 -2.9,0 -5.3,-2.1 -5.3,-4.8 0,-0.3 0,-3.5 0,-3.8 0,-0.7 -0.5,-1.3 -1.2,-1.3 -0.7,0 -1.3,0.5 -1.3,1.2 0,0.2 0,3.9 0,3.9 0,3.6 2.9,6.6 6.6,7.2 l 0,2.4 -3.1,0 c -0.7,0 -1.3,0.6 -1.3,1.3 0,0.7 0.6,1.3 1.3,1.3 l 8.8,0 c 0.7,0 1.3,-0.6 1.3,-1.3 0,-0.7 -0.6,-1.3 -1.3,-1.3 l -3.2,0 0,-2.4 c 3.6,-0.5 6.5,-3.5 6.5,-7.2 z" id="path6952" inkscape:connector-curvature="0" - style="fill:#ffffff" /> \ No newline at end of file + style="fill:#ffffff" /> diff --git a/interface/resources/qml/+android_interface/Stats.qml b/interface/resources/qml/+android_interface/Stats.qml index fe56f3797b..54f6086a86 100644 --- a/interface/resources/qml/+android_interface/Stats.qml +++ b/interface/resources/qml/+android_interface/Stats.qml @@ -113,6 +113,10 @@ Item { visible: root.expanded text: "Avatars Updated: " + root.updatedAvatarCount } + StatText { + visible: root.expanded + text: "Heroes Count/Updated: " + root.heroAvatarCount + "/" + root.updatedHeroAvatarCount + } StatText { visible: root.expanded text: "Avatars NOT Updated: " + root.notUpdatedAvatarCount diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index 4a071d2d04..3f1f598991 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -7,24 +7,57 @@ // import Hifi 1.0 as Hifi -import QtQuick 2.4 +import QtQuick 2.5 +import QtGraphicalEffects 1.0 import "./hifi/audio" as HifiAudio +import TabletScriptingInterface 1.0 + Item { id: root; objectName: "AvatarInputsBar" property int modality: Qt.NonModal - width: audio.width; - height: audio.height; - x: 10; y: 5; - + readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; + x: 10; + y: 5; readonly property bool shouldReposition: true; + property bool hmdActive: HMD.active; + width: hmdActive ? audio.width : audioApplication.width; + height: hmdActive ? audio.height : audioApplication.height; + + Timer { + id: hmdActiveCheckTimer; + interval: 500; + repeat: true; + onTriggered: { + root.hmdActive = HMD.active; + } + + } HifiAudio.MicBar { id: audio; - visible: AvatarInputs.showAudioTools; + visible: AvatarInputs.showAudioTools && root.hmdActive; standalone: true; - dragTarget: parent; + dragTarget: parent; + } + + HifiAudio.MicBarApplication { + id: audioApplication; + visible: AvatarInputs.showAudioTools && !root.hmdActive; + standalone: true; + dragTarget: parent; + } + + Component.onCompleted: { + HMD.displayModeChanged.connect(function(isHmdMode) { + root.hmdActive = isHmdMode; + }); + } + + BubbleIcon { + dragTarget: parent + visible: !root.hmdActive; } } diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml new file mode 100644 index 0000000000..f4e99f136c --- /dev/null +++ b/interface/resources/qml/BubbleIcon.qml @@ -0,0 +1,100 @@ +// +// Created by Bradley Austin Davis on 2015/06/19 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "./hifi/audio" as HifiAudio + +import TabletScriptingInterface 1.0 + +Rectangle { + id: bubbleRect + width: bubbleIcon.width + 10 + height: bubbleIcon.height + 10 + radius: 5; + property var dragTarget: null; + property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; + + function updateOpacity() { + if (ignoreRadiusEnabled) { + bubbleRect.opacity = 1.0; + } else { + bubbleRect.opacity = 0.7; + } + } + + Component.onCompleted: { + updateOpacity(); + } + + onIgnoreRadiusEnabledChanged: { + updateOpacity(); + } + + color: "#00000000"; + border { + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; + color: "#80FFFFFF"; + } + anchors { + left: dragTarget ? dragTarget.right : undefined + top: dragTarget ? dragTarget.top : undefined + } + + // borders are painted over fill, so reduce the fill to fit inside the border + Rectangle { + color: "#55000000"; + width: 40; + height: 40; + + radius: 5; + + anchors { + verticalCenter: parent.verticalCenter; + horizontalCenter: parent.horizontalCenter; + } + } + + MouseArea { + id: mouseArea; + anchors.fill: parent + + hoverEnabled: true; + scrollGestureEnabled: false; + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + Users.toggleIgnoreRadius(); + } + drag.target: dragTarget; + onContainsMouseChanged: { + var rectOpacity = (ignoreRadiusEnabled && containsMouse) ? 1.0 : (containsMouse ? 1.0 : 0.7); + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + } + bubbleRect.opacity = rectOpacity; + } + } + Image { + id: bubbleIcon + source: "../icons/tablet-icons/bubble-i.svg"; + sourceSize: Qt.size(32, 32); + smooth: true; + anchors.top: parent.top + anchors.topMargin: (parent.height - bubbleIcon.height) / 2 + anchors.left: parent.left + anchors.leftMargin: (parent.width - bubbleIcon.width) / 2 + } + ColorOverlay { + id: bubbleIconOverlay + anchors.fill: bubbleIcon + source: bubbleIcon + color: "#FFFFFF"; + } +} diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index ebc677fb00..17d6a7d3b3 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -379,9 +379,9 @@ Item { Component.onCompleted: { // with the link. if (completeProfileBody.withOculus) { - termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") } else { - termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") } } } @@ -510,7 +510,7 @@ Item { console.log("Create Failed: " + error); if (completeProfileBody.withSteam || completeProfileBody.withOculus) { if (completeProfileBody.loginDialogPoppedUp) { - action = completeProfileBody.withSteam ? "Steam" : "Oculus"; + var action = completeProfileBody.withSteam ? "Steam" : "Oculus"; var data = { "action": "user failed to create a profile with " + action + " from the complete profile screen" } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 64df9089a1..69ac2f5a6c 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -395,7 +395,7 @@ Item { text: signUpBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") } } diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index 2c8e61a29a..d450b1e7bc 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -218,7 +218,7 @@ Item { text: usernameCollisionBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") } } diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 3b703d72e6..e10f86a947 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -115,6 +115,10 @@ Item { visible: root.expanded text: "Avatars Updated: " + root.updatedAvatarCount } + StatText { + visible: root.expanded + text: "Heroes Count/Updated: " + root.heroAvatarCount + "/" + root.updatedHeroAvatarCount + } StatText { visible: root.expanded text: "Avatars NOT Updated: " + root.notUpdatedAvatarCount @@ -228,6 +232,10 @@ Item { text: "Audio Codec: " + root.audioCodec + " Noise Gate: " + root.audioNoiseGate; } + StatText { + visible: root.expanded; + text: "Injectors (Local/NonLocal): " + root.audioInjectors.x + "/" + root.audioInjectors.y; + } StatText { visible: root.expanded; text: "Entity Servers In: " + root.entityPacketsInKbps + " kbps"; diff --git a/interface/resources/qml/controlsUit/Button.qml b/interface/resources/qml/controlsUit/Button.qml index 3c5626e29e..6da85ed6d3 100644 --- a/interface/resources/qml/controlsUit/Button.qml +++ b/interface/resources/qml/controlsUit/Button.qml @@ -143,6 +143,16 @@ Original.Button { horizontalAlignment: Text.AlignHCenter text: control.text Component.onCompleted: { + setTextPosition(); + } + onTextChanged: { + setTextPosition(); + } + function setTextPosition() { + // force TextMetrics to re-evaluate the text field and glyph sizes + // as for some reason it's not automatically being done. + buttonGlyphTextMetrics.text = buttonGlyph.text; + buttonTextMetrics.text = text; if (control.buttonGlyph !== "") { buttonText.x = buttonContentItem.width/2 - buttonTextMetrics.width/2 + (buttonGlyphTextMetrics.width + control.buttonGlyphRightMargin)/2; } else { diff --git a/interface/resources/qml/controlsUit/CheckBoxQQC2.qml b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml index 91d35ecd58..bd71025aa9 100644 --- a/interface/resources/qml/controlsUit/CheckBoxQQC2.qml +++ b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml @@ -24,6 +24,7 @@ CheckBox { leftPadding: 0 property int colorScheme: hifi.colorSchemes.light property string color: hifi.colors.lightGrayText + property int fontSize: hifi.fontSizes.inputLabel readonly property bool isLightColorScheme: colorScheme === hifi.colorSchemes.light property bool isRedCheck: false property bool isRound: false @@ -109,7 +110,7 @@ CheckBox { contentItem: Text { id: root - font.pixelSize: hifi.fontSizes.inputLabel + font.pixelSize: fontSize; font.family: "Raleway" font.weight: Font.DemiBold text: checkBox.text diff --git a/interface/resources/qml/controlsUit/Switch.qml b/interface/resources/qml/controlsUit/Switch.qml index 4e1c21c456..422b08b4eb 100644 --- a/interface/resources/qml/controlsUit/Switch.qml +++ b/interface/resources/qml/controlsUit/Switch.qml @@ -21,6 +21,7 @@ Item { property int switchWidth: 70; readonly property int switchRadius: height/2; property string labelTextOff: ""; + property int labelTextSize: hifi.fontSizes.inputLabel; property string labelGlyphOffText: ""; property int labelGlyphOffSize: 32; property string labelTextOn: ""; @@ -89,7 +90,7 @@ Item { RalewaySemiBold { id: labelOff; text: labelTextOff; - size: hifi.fontSizes.inputLabel; + size: labelTextSize; color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF"; anchors.top: parent.top; anchors.right: parent.right; @@ -130,7 +131,7 @@ Item { RalewaySemiBold { id: labelOn; text: labelTextOn; - size: hifi.fontSizes.inputLabel; + size: labelTextSize; color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText; anchors.top: parent.top; anchors.left: parent.left; diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ba5e162391..4eea3566b8 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -320,6 +320,7 @@ ModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 6c4e32dc5a..5bcc42f101 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -285,6 +285,7 @@ TabletModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/dialogs/TabletMessageBox.qml b/interface/resources/qml/dialogs/TabletMessageBox.qml index 1e6f0734ad..4411651a0f 100644 --- a/interface/resources/qml/dialogs/TabletMessageBox.qml +++ b/interface/resources/qml/dialogs/TabletMessageBox.qml @@ -28,7 +28,7 @@ TabletModalWindow { id: mouse; anchors.fill: parent } - + function click(button) { clickedButton = button; selected(button); diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 753b9c5a81..997407885b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -16,6 +16,8 @@ Rectangle { property bool keyboardRaised: false property bool punctuationMode: false + HifiConstants { id: hifi } + HifiControls.Keyboard { id: keyboard z: 1000 @@ -48,6 +50,7 @@ Rectangle { property var jointNames: [] property var currentAvatarSettings; + property bool wearablesFrozen; function fetchAvatarModelName(marketId, avatar) { var xmlhttp = new XMLHttpRequest(); @@ -187,6 +190,8 @@ Rectangle { updateCurrentAvatarInBookmarks(currentAvatar); } else if (message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); + } else if (message.method === 'wearablesFrozenChanged') { + wearablesFrozen = message.wearablesFrozen; } } @@ -507,6 +512,7 @@ Rectangle { } SquareLabel { + id: adjustLabel anchors.right: parent.right anchors.verticalCenter: wearablesLabel.verticalCenter glyphText: "\ue02e" @@ -515,6 +521,17 @@ Rectangle { adjustWearables.open(currentAvatar); } } + + SquareLabel { + anchors.right: adjustLabel.left + anchors.verticalCenter: wearablesLabel.verticalCenter + anchors.rightMargin: 15 + glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock; + + onClicked: { + emitSendToScript({'method' : 'toggleWearablesFrozen'}); + } + } } Rectangle { diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index fc49bcf048..9fb8067371 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -40,6 +40,7 @@ Item { property bool isConcurrency: action === 'concurrency'; property bool isAnnouncement: action === 'announcement'; property bool isStacked: !isConcurrency && drillDownToPlace; + property bool has3DHTML: PlatformInfo.has3DHTML(); property int textPadding: 10; @@ -298,7 +299,7 @@ Item { StateImage { id: actionIcon; - visible: !isAnnouncement; + visible: !isAnnouncement && has3DHTML; imageURL: "../../images/info-icon-2-state.svg"; size: 30; buttonState: messageArea.containsMouse ? 1 : 0; @@ -315,7 +316,7 @@ Item { } MouseArea { id: messageArea; - visible: !isAnnouncement; + visible: !isAnnouncement && has3DHTML; width: parent.width; height: messageHeight; anchors.top: lobby.bottom; diff --git a/interface/resources/qml/hifi/EditAvatarInputsBar.qml b/interface/resources/qml/hifi/EditAvatarInputsBar.qml new file mode 100644 index 0000000000..b27b0c8db2 --- /dev/null +++ b/interface/resources/qml/hifi/EditAvatarInputsBar.qml @@ -0,0 +1,152 @@ +// +// EditAvatarInputsBar.qml +// qml/hifi +// +// Audio setup +// +// Created by Wayne Chen on 3/20/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../windows" + +Rectangle { + id: editRect + + HifiConstants { id: hifi; } + + color: hifi.colors.baseGray; + + signal sendToScript(var message); + function emitSendToScript(message) { + sendToScript(message); + } + + function fromScript(message) { + } + + RalewayRegular { + id: title; + color: hifi.colors.white; + text: qsTr("Avatar Inputs Persistent UI Settings") + size: 20 + font.bold: true + anchors { + top: parent.top + left: parent.left + leftMargin: (parent.width - width) / 2 + } + } + + HifiControlsUit.Slider { + id: xSlider + anchors { + top: title.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "X OFFSET: " + value.toFixed(2); + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.2 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "x": value + }); + } + } + + HifiControlsUit.Slider { + id: ySlider + anchors { + top: xSlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "Y OFFSET: " + value.toFixed(2); + maximumValue: 1.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.125 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "y": value + }); + } + } + + HifiControlsUit.Slider { + id: zSlider + anchors { + top: ySlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + label: "Z OFFSET: " + value.toFixed(2); + maximumValue: 0.0 + minimumValue: -1.0 + stepSize: 0.05 + value: -0.5 + width: 300 + onValueChanged: { + emitSendToScript({ + "method": "reposition", + "z": value + }); + } + } + + HifiControlsUit.Button { + id: setVisibleButton; + text: setVisible ? "SET INVISIBLE" : "SET VISIBLE"; + width: 300; + property bool setVisible: true; + anchors { + top: zSlider.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + setVisible = !setVisible; + emitSendToScript({ + "method": "setVisible", + "visible": setVisible + }); + } + } + + HifiControlsUit.Button { + id: printButton; + text: "PRINT POSITIONS"; + width: 300; + anchors { + top: setVisibleButton.bottom + topMargin: 50 + left: parent.left + leftMargin: 20 + } + onClicked: { + emitSendToScript({ + "method": "print", + }); + } + } +} diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 718ebc9331..a9fde05d8d 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -54,7 +54,7 @@ Column { 'require_online=true', 'protocol=' + encodeURIComponent(Window.protocolSignature()) ]; - endpoint: '/api/v1/user_stories?' + options.join('&'); + endpoint: '/api/v1/user_stories?' + options.join('&') + (PlatformInfo.isStandalone() ? '&standalone_optimized=true' : '') itemsPerPage: 4; processPage: function (data) { return data.user_stories.map(makeModelData); diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 646fc881e1..7e8218b7df 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -46,6 +46,8 @@ Item { property string placeName: "" property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent")) property alias avImage: avatarImage + property bool has3DHTML: PlatformInfo.has3DHTML(); + Item { id: avatarImage visible: profileUrl !== "" && userName !== ""; @@ -94,10 +96,12 @@ Item { enabled: (selected && activeTab == "nearbyTab") || isMyCard; hoverEnabled: enabled onClicked: { - userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; - userInfoViewer.visible = true; + if (has3DHTML) { + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; + userInfoViewer.visible = true; + } } - onEntered: infoHoverImage.visible = true; + onEntered: infoHoverImage.visible = has3DHTML; onExited: infoHoverImage.visible = false; } } @@ -125,6 +129,7 @@ Item { height: 40 // Anchors anchors.top: avatarImage.top + anchors.topMargin: avatarImage.visible ? 18 : 0; anchors.left: avatarImage.right anchors.leftMargin: avatarImage.visible ? 5 : 0; anchors.rightMargin: 5; @@ -352,7 +357,7 @@ Item { } StateImage { id: nameCardConnectionInfoImage - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && has3DHTML imageURL: "../../images/info-icon-2-state.svg" // PLACEHOLDER!!! size: 32; buttonState: 0; @@ -364,8 +369,10 @@ Item { enabled: selected hoverEnabled: true onClicked: { - userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; - userInfoViewer.visible = true; + if (has3DHTML) { + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; + userInfoViewer.visible = true; + } } onEntered: { nameCardConnectionInfoImage.buttonState = 1; @@ -376,8 +383,7 @@ Item { } FiraSansRegular { id: nameCardConnectionInfoText - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" - width: parent.width + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && PlatformInfo.has3DHTML() height: displayNameTextPixelSize size: displayNameTextPixelSize - 4 anchors.left: nameCardConnectionInfoImage.right @@ -391,9 +397,10 @@ Item { id: nameCardRemoveConnectionImage visible: selected && !isMyCard && pal.activeTab == "connectionsTab" text: hifi.glyphs.close - size: 28; + size: 24; x: 120 anchors.verticalCenter: nameCardConnectionInfoImage.verticalCenter + anchors.left: has3DHTML ? nameCardConnectionInfoText.right + 10 : avatarImage.right } MouseArea { anchors.fill:nameCardRemoveConnectionImage diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 1c190a2b79..55f2bb80b1 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -1261,6 +1261,14 @@ Rectangle { case 'refreshConnections': refreshConnections(); break; + case 'connectionRemoved': + for (var i=0; i 10 ? true : false; + clip: true; - Separator { } + ScrollBar.vertical: ScrollBar { + policy: flickView.isScrolling ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff; + parent: flickView.parent; + anchors.top: flickView.top; + anchors.right: flickView.right; + anchors.bottom: flickView.bottom; + anchors.rightMargin: -verticalScrollWidth; //compensate flickView's right margin + background: Item { + implicitWidth: verticalScrollWidth; + Rectangle { + color: hifi.colors.darkGray30; + radius: 4; + anchors { + fill: parent; + topMargin: -1; // Finesse size + bottomMargin: -2; + } + } + } + contentItem: Item { + implicitWidth: verticalScrollShaft; + Rectangle { + radius: verticalScrollShaft/2; + color: hifi.colors.white30; + anchors { + fill: parent; + leftMargin: 2; // Finesse size and position. + topMargin: 1; + bottomMargin: 1; + } + } + } + } - RowLayout { + Separator { + id: firstSeparator; + anchors.top: parent.top; + } + + Item { + id: switchesContainer; x: 2 * margins.paddings; - spacing: columnOne.width; width: parent.width; + // switch heights + 2 * top margins + height: (root.switchHeight) * 3 + 48; + anchors.top: firstSeparator.bottom; + anchors.topMargin: 10; // mute is in its own row - ColumnLayout { - id: columnOne - spacing: 24; - x: margins.paddings + Item { + id: switchContainer; + x: margins.paddings; + width: parent.width / 2; + height: parent.height; + anchors.left: parent.left; HifiControlsUit.Switch { id: muteMic; height: root.switchHeight; switchWidth: root.switchWidth; labelTextOn: "Mute microphone"; + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; - checked: AudioScriptingInterface.muted; - onCheckedChanged: { - AudioScriptingInterface.muted = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding + checked: muted; + onClicked: { + if (pushToTalk && !checked) { + // disable push to talk if unmuting + if (bar.currentIndex === 0) { + AudioScriptingInterface.pushToTalkDesktop = false; + } + else { + AudioScriptingInterface.pushToTalkHMD = false; + } + } + if (bar.currentIndex === 0) { + AudioScriptingInterface.mutedDesktop = checked; + } + else { + AudioScriptingInterface.mutedHMD = checked; + } } } HifiControlsUit.Switch { - id: stereoInput; - height: root.switchHeight; - switchWidth: root.switchWidth; - labelTextOn: qsTr("Stereo input"); - backgroundOnColor: "#E3E3E3"; - checked: AudioScriptingInterface.isStereoInput; - onCheckedChanged: { - AudioScriptingInterface.isStereoInput = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding - } - } - } - - ColumnLayout { - spacing: 24; - HifiControlsUit.Switch { + id: noiseReductionSwitch; height: root.switchHeight; switchWidth: root.switchWidth; + anchors.top: muteMic.bottom; + anchors.topMargin: 24 + anchors.left: parent.left labelTextOn: "Noise Reduction"; + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.noiseReduction; onCheckedChanged: { @@ -153,11 +221,59 @@ Rectangle { } } + HifiControlsUit.Switch { + id: pttSwitch + height: root.switchHeight; + switchWidth: root.switchWidth; + anchors.top: noiseReductionSwitch.bottom + anchors.topMargin: 24 + anchors.left: parent.left + labelTextOn: (bar.currentIndex === 0) ? qsTr("Push To Talk (T)") : qsTr("Push To Talk"); + labelTextSize: 16; + backgroundOnColor: "#E3E3E3"; + checked: (bar.currentIndex === 0) ? AudioScriptingInterface.pushToTalkDesktop : AudioScriptingInterface.pushToTalkHMD; + onCheckedChanged: { + if (bar.currentIndex === 0) { + AudioScriptingInterface.pushToTalkDesktop = checked; + } else { + AudioScriptingInterface.pushToTalkHMD = checked; + } + } + } + } + + Item { + id: additionalSwitchContainer + width: switchContainer.width - margins.paddings; + height: parent.height; + anchors.top: parent.top + anchors.left: switchContainer.right; + HifiControlsUit.Switch { + id: warnMutedSwitch + height: root.switchHeight; + switchWidth: root.switchWidth; + anchors.top: parent.top + anchors.left: parent.left + labelTextOn: qsTr("Warn when muted in HMD"); + labelTextSize: 16; + backgroundOnColor: "#E3E3E3"; + checked: AudioScriptingInterface.warnWhenMuted; + onClicked: { + AudioScriptingInterface.warnWhenMuted = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding + } + } + + HifiControlsUit.Switch { id: audioLevelSwitch height: root.switchHeight; switchWidth: root.switchWidth; + anchors.top: warnMutedSwitch.bottom + anchors.topMargin: 24 + anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); + labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AvatarInputs.showAudioTools; onCheckedChanged: { @@ -165,47 +281,97 @@ Rectangle { checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding } } + + HifiControlsUit.Switch { + id: stereoInput; + height: root.switchHeight; + switchWidth: root.switchWidth; + anchors.top: audioLevelSwitch.bottom + anchors.topMargin: 24 + anchors.left: parent.left + labelTextOn: qsTr("Stereo input"); + labelTextSize: 16; + backgroundOnColor: "#E3E3E3"; + checked: AudioScriptingInterface.isStereoInput; + onCheckedChanged: { + AudioScriptingInterface.isStereoInput = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding + } + } + } } - Separator {} + Item { + id: pttTextContainer + anchors.top: switchesContainer.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + width: rightMostInputLevelPos; + height: pttText.height; + RalewayRegular { + id: pttText; + x: margins.paddings; + color: hifi.colors.white; + width: rightMostInputLevelPos; + height: paintedHeight; + wrapMode: Text.WordWrap; + font.italic: true; + size: 16; + + text: (bar.currentIndex === 0) ? qsTr("Press and hold the button \"T\" to talk.") : + qsTr("Press and hold grip triggers on both of your controllers to talk."); + } + } + + Separator { + id: secondSeparator; + anchors.top: pttTextContainer.visible ? pttTextContainer.bottom : switchesContainer.bottom; + anchors.topMargin: 10; + } + Item { + id: inputDeviceHeader x: margins.paddings; - width: parent.width - margins.paddings*2 - height: 36 + width: parent.width - margins.paddings*2; + height: 36; + anchors.top: secondSeparator.bottom; + anchors.topMargin: 10; HiFiGlyphs { - width: margins.sizeCheckBox + width: margins.sizeCheckBox; text: hifi.glyphs.mic; color: hifi.colors.white; - anchors.left: parent.left - anchors.leftMargin: -size/4 //the glyph has empty space at left about 25% + anchors.left: parent.left; + anchors.leftMargin: -size/4; //the glyph has empty space at left about 25% anchors.verticalCenter: parent.verticalCenter; size: 30; } RalewayRegular { anchors.verticalCenter: parent.verticalCenter; - width: margins.sizeText + margins.sizeLevel - anchors.left: parent.left - anchors.leftMargin: margins.sizeCheckBox - size: 16; + width: margins.sizeText + margins.sizeLevel; + anchors.left: parent.left; + anchors.leftMargin: margins.sizeCheckBox; + size: 22; color: hifi.colors.white; text: qsTr("Choose input device"); } } ListView { - id: inputView - width: parent.width - margins.paddings*2 + id: inputView; + width: rightMostInputLevelPos; + anchors.top: inputDeviceHeader.bottom; + anchors.topMargin: 10; x: margins.paddings - height: Math.min(150, contentHeight); + interactive: false; + height: contentHeight; spacing: 4; - snapMode: ListView.SnapToItem; clip: true; model: AudioScriptingInterface.devices.input; delegate: Item { - width: rightMostInputLevelPos + width: rightMostInputLevelPos - margins.paddings*2 height: margins.sizeCheckBox > checkBoxInput.implicitHeight ? margins.sizeCheckBox : checkBoxInput.implicitHeight @@ -221,9 +387,10 @@ Rectangle { boxSize: margins.sizeCheckBox / 2 isRound: true text: devicename + fontSize: 16; onPressed: { if (!checked) { - stereoMic.checked = false; + stereoInput.checked = false; AudioScriptingInterface.setStereoInput(false); // the next selected audio device might not support stereo AudioScriptingInterface.setInputDevice(info, bar.currentIndex === 1); } @@ -240,17 +407,28 @@ Rectangle { } } } + AudioControls.LoopbackAudio { + id: loopbackAudio x: margins.paddings + anchors.top: inputView.bottom; + anchors.topMargin: 10; visible: (bar.currentIndex === 1 && isVR) || (bar.currentIndex === 0 && !isVR); anchors { left: parent.left; leftMargin: margins.paddings } } - Separator {} + Separator { + id: thirdSeparator; + anchors.top: loopbackAudio.visible ? loopbackAudio.bottom : inputView.bottom; + anchors.topMargin: 10; + } Item { + id: outputDeviceHeader; + anchors.topMargin: 10; + anchors.top: thirdSeparator.bottom; x: margins.paddings; width: parent.width - margins.paddings*2 height: 36 @@ -270,7 +448,7 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: margins.sizeCheckBox anchors.verticalCenter: parent.verticalCenter; - size: 16; + size: 22; color: hifi.colors.white; text: qsTr("Choose output device"); } @@ -279,10 +457,12 @@ Rectangle { ListView { id: outputView width: parent.width - margins.paddings*2 - x: margins.paddings - height: Math.min(360 - inputView.height, contentHeight); + x: margins.paddings; + interactive: false; + height: contentHeight; + anchors.top: outputDeviceHeader.bottom; + anchors.topMargin: 10; spacing: 4; - snapMode: ListView.SnapToItem; clip: true; model: AudioScriptingInterface.devices.output; delegate: Item { @@ -299,6 +479,7 @@ Rectangle { checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD; checkable: !checked text: devicename + fontSize: 16 onPressed: { if (!checked) { AudioScriptingInterface.setOutputDevice(info, bar.currentIndex === 1); @@ -309,20 +490,22 @@ Rectangle { } Item { - id: gainContainer + id: avatarGainContainer x: margins.paddings; + anchors.top: outputView.bottom; + anchors.topMargin: 10; width: parent.width - margins.paddings*2 - height: gainSliderTextMetrics.height + height: avatarGainSliderTextMetrics.height HifiControlsUit.Slider { - id: gainSlider + id: avatarGainSlider anchors.right: parent.right height: parent.height width: 200 minimumValue: -60.0 maximumValue: 20.0 stepSize: 5 - value: Users.getAvatarGain(myAvatarUuid) + value: AudioScriptingInterface.getAvatarGain() onValueChanged: { updateMyAvatarGainFromQML(value, false); } @@ -338,7 +521,7 @@ Rectangle { // Do nothing. } onDoubleClicked: { - gainSlider.value = 0.0 + avatarGainSlider.value = 0.0 } onPressed: { // Pass through to Slider @@ -352,14 +535,136 @@ Rectangle { } } TextMetrics { - id: gainSliderTextMetrics - text: gainSliderText.text - font: gainSliderText.font + id: avatarGainSliderTextMetrics + text: avatarGainSliderText.text + font: avatarGainSliderText.font } RalewayRegular { // The slider for my card is special, it controls the master gain - id: gainSliderText; - text: "Avatar volume"; + id: avatarGainSliderText; + text: "People volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: injectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: injectorGainSliderTextMetrics.height + anchors.top: avatarGainContainer.bottom; + anchors.topMargin: 10; + + HifiControlsUit.Slider { + id: injectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getInjectorGain() + onValueChanged: { + updateInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + injectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: injectorGainSliderTextMetrics + text: injectorGainSliderText.text + font: injectorGainSliderText.font + } + RalewayRegular { + id: injectorGainSliderText; + text: "Environment volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: systemInjectorGainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: systemInjectorGainSliderTextMetrics.height + anchors.top: injectorGainContainer.bottom; + anchors.topMargin: 10; + + HifiControlsUit.Slider { + id: systemInjectorGainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: AudioScriptingInterface.getSystemInjectorGain() + onValueChanged: { + updateSystemInjectorGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateSystemInjectorGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + systemInjectorGainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: systemInjectorGainSliderTextMetrics + text: systemInjectorGainSliderText.text + font: systemInjectorGainSliderText.font + } + RalewayRegular { + id: systemInjectorGainSliderText; + text: "System Sound volume"; size: 16; anchors.left: parent.left; color: hifi.colors.white; @@ -369,11 +674,10 @@ Rectangle { } AudioControls.PlaySampleSound { + id: playSampleSound x: margins.paddings - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } + anchors.top: systemInjectorGainContainer.bottom; + anchors.topMargin: 10; } } } diff --git a/interface/resources/qml/hifi/audio/AudioTabButton.qml b/interface/resources/qml/hifi/audio/AudioTabButton.qml index 32331ccb6e..c81377e524 100644 --- a/interface/resources/qml/hifi/audio/AudioTabButton.qml +++ b/interface/resources/qml/hifi/audio/AudioTabButton.qml @@ -16,7 +16,7 @@ import stylesUit 1.0 TabButton { id: control - font.pixelSize: height / 2 + font.pixelSize: 14 HifiConstants { id: hifi; } diff --git a/interface/resources/qml/hifi/audio/InputPeak.qml b/interface/resources/qml/hifi/audio/InputPeak.qml index 00f7e63528..d8b166cee4 100644 --- a/interface/resources/qml/hifi/audio/InputPeak.qml +++ b/interface/resources/qml/hifi/audio/InputPeak.qml @@ -12,24 +12,26 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -Rectangle { +Item { property var peak; width: 70; height: 8; - color: "transparent"; - - Item { + QtObject { id: colors; + readonly property string unmuted: "#FFF"; readonly property string muted: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; + readonly property string yellow: "#C0C000"; readonly property string red: colors.muted; + readonly property string fill: "#55000000"; } + Text { id: status; @@ -79,23 +81,19 @@ Rectangle { anchors { fill: mask } source: mask start: Qt.point(0, 0); - end: Qt.point(70, 0); + end: Qt.point(bar.width, 0); gradient: Gradient { GradientStop { position: 0; color: colors.greenStart; } GradientStop { - position: 0.8; + position: 0.5; color: colors.greenEnd; } - GradientStop { - position: 0.801; - color: colors.red; - } GradientStop { position: 1; - color: colors.red; + color: colors.yellow; } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 8ec0ffc496..d3fd8ee075 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -17,17 +17,17 @@ import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit RowLayout { - property bool audioLoopedBack: AudioScriptingInterface.getServerEcho(); + property bool audioLoopedBack: AudioScriptingInterface.getLocalEcho(); function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; - AudioScriptingInterface.setServerEcho(true); + AudioScriptingInterface.setLocalEcho(true); } } function stopAudioLoopback() { if (audioLoopedBack) { audioLoopedBack = false; - AudioScriptingInterface.setServerEcho(false); + AudioScriptingInterface.setLocalEcho(false); } } @@ -44,8 +44,11 @@ RowLayout { } HifiControlsUit.Button { - text: audioLoopedBack ? qsTr("STOP TESTING YOUR VOICE") : qsTr("TEST YOUR VOICE"); + text: audioLoopedBack ? qsTr("STOP TESTING VOICE") : qsTr("TEST YOUR VOICE"); color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; + fontSize: 15; + width: 200; + height: 32; onClicked: { if (audioLoopedBack) { loopbackTimer.stop(); @@ -59,7 +62,7 @@ RowLayout { RalewayRegular { Layout.leftMargin: 2; - size: 14; + size: 18; color: "white"; font.italic: true text: audioLoopedBack ? qsTr("Speak in your input") : ""; diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 39f75a9182..55378589ec 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -11,18 +11,40 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 +import stylesUit 1.0 import TabletScriptingInterface 1.0 Rectangle { + id: micBar + HifiConstants { id: hifi; } + + property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; - + readonly property var clipping: AudioScriptingInterface.clipping; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + + readonly property var userSpeakingLevel: 0.4; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + HMD.displayModeChanged.connect(function() { + muted = AudioScriptingInterface.muted; + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.mutedChanged.connect(function() { + muted = AudioScriptingInterface.muted; + }); + AudioScriptingInterface.pushToTalkChanged.connect(function() { + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.pushingToTalkChanged.connect(function() { + pushingToTalk = AudioScriptingInterface.pushingToTalk; + }); } - + property bool standalone: false; property var dragTarget: null; @@ -64,7 +86,10 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { - AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + if (pushToTalk) { + return; + } + AudioScriptingInterface.muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; @@ -78,16 +103,16 @@ Rectangle { QtObject { id: colors; - readonly property string unmuted: "#FFF"; - readonly property string muted: "#E2334D"; + readonly property string unmutedColor: "#FFF"; + readonly property string mutedColor: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; readonly property string yellow: "#C0C000"; - readonly property string red: colors.muted; + readonly property string red: colors.mutedColor; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: AudioScriptingInterface.muted ? muted : unmuted; + readonly property string icon: muted ? colors.mutedColor : unmutedColor; } Item { @@ -106,9 +131,13 @@ Rectangle { Image { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; + readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; + readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; id: image; - source: AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : + clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; width: 30; height: 30; @@ -131,9 +160,7 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; - - visible: AudioScriptingInterface.muted; + visible: (pushToTalk && !pushingToTalk) || muted; anchors { left: parent.left; @@ -150,9 +177,9 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: parent.color; + color: colors.icon; - text: AudioScriptingInterface.muted ? "MUTED" : "MUTE"; + text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -162,9 +189,9 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } Rectangle { @@ -173,9 +200,9 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } } @@ -228,12 +255,12 @@ Rectangle { } } } - + Rectangle { id: gatedIndicator; visible: gated && !AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: "#0080FF"; @@ -242,12 +269,12 @@ Rectangle { verticalCenter: parent.verticalCenter; } } - + Rectangle { id: clippingIndicator; visible: AudioScriptingInterface.clipping - - radius: 4; + + radius: 4; width: 2 * radius; height: 2 * radius; color: colors.red; diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml new file mode 100644 index 0000000000..bc3f4dff89 --- /dev/null +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -0,0 +1,255 @@ +// +// MicBarApplication.qml +// qml/hifi/audio +// +// Created by Zach Pomerantz on 6/14/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import stylesUit 1.0 +import TabletScriptingInterface 1.0 + +Rectangle { + id: micBar; + readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var clipping: AudioScriptingInterface.clipping; + property var muted: AudioScriptingInterface.muted; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + readonly property var userSpeakingLevel: 0.4; + property bool gated: false; + Component.onCompleted: { + AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); + AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + HMD.displayModeChanged.connect(function() { + muted = AudioScriptingInterface.muted; + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.mutedChanged.connect(function() { + muted = AudioScriptingInterface.muted; + }); + AudioScriptingInterface.pushToTalkChanged.connect(function() { + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + } + + readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; + readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; + readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; + readonly property string clippingIcon: "../../../icons/tablet-icons/mic-clip-i.svg"; + readonly property string gatedIcon: "../../../icons/tablet-icons/mic-gate-i.svg"; + property bool standalone: false; + property var dragTarget: null; + + width: 44; + height: 44; + + radius: 5; + opacity: 0.7; + + onLevelChanged: { + var rectOpacity = (muted && (level >= userSpeakingLevel)) ? 1.0 : 0.7; + if (pushToTalk && !pushingToTalk) { + rectOpacity = (mouseArea.containsMouse) ? 1.0 : 0.7; + } else if (mouseArea.containsMouse && rectOpacity != 1.0) { + rectOpacity = 1.0; + } + micBar.opacity = rectOpacity; + } + + color: "#00000000"; + border { + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; + color: colors.border; + } + + // borders are painted over fill, so reduce the fill to fit inside the border + Rectangle { + color: standalone ? colors.fill : "#00000000"; + width: 40; + height: 40; + + radius: 5; + + anchors { + verticalCenter: parent.verticalCenter; + horizontalCenter: parent.horizontalCenter; + } + } + + MouseArea { + id: mouseArea; + + anchors { + left: icon.left; + right: bar.right; + top: icon.top; + bottom: icon.bottom; + } + + hoverEnabled: true; + scrollGestureEnabled: false; + onClicked: { + if (pushToTalk) { + return; + } + AudioScriptingInterface.muted = !muted; + Tablet.playSound(TabletEnums.ButtonClick); + muted = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding + } + drag.target: dragTarget; + onContainsMouseChanged: { + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + } + + QtObject { + id: colors; + + readonly property string unmutedColor: "#FFF"; + readonly property string gatedColor: "#00BDFF"; + readonly property string mutedColor: "#E2334D"; + readonly property string gutter: "#575757"; + readonly property string greenStart: "#39A38F"; + readonly property string greenEnd: "#1FC6A6"; + readonly property string yellow: "#C0C000"; + readonly property string fill: "#55000000"; + readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; + readonly property string icon: (muted || clipping) ? mutedColor : gated ? gatedColor : unmutedColor; + } + + Item { + id: icon; + + anchors { + left: parent.left; + top: parent.top; + } + + width: 40; + height: 40; + + Item { + Image { + id: image; + source: (pushToTalk) ? pushToTalkIcon : muted ? mutedIcon : + clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon; + width: 29; + height: 32; + anchors { + left: parent.left; + top: parent.top; + topMargin: 5; + } + } + + ColorOverlay { + id: imageOverlay + anchors { fill: image } + source: image; + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : colors.icon; + } + } + } + + Item { + id: status; + + visible: pushToTalk || (muted && (level >= userSpeakingLevel)); + + anchors { + left: parent.left; + top: icon.bottom; + topMargin: 2; + } + + width: parent.width; + height: statusTextMetrics.height; + + TextMetrics { + id: statusTextMetrics + text: statusText.text + font: statusText.font + } + + RalewaySemiBold { + id: statusText + anchors { + horizontalCenter: parent.horizontalCenter; + verticalCenter: parent.verticalCenter; + } + + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : (level >= userSpeakingLevel && muted) ? colors.mutedColor : colors.unmutedColor; + font.bold: true + + text: pushToTalk ? (HMD.active ? "PTT" : "PTT-(T)") : (muted ? "MUTED" : "MUTE"); + size: 12; + } + } + + Item { + id: bar; + + anchors { + right: parent.right; + rightMargin: 7; + top: parent.top + topMargin: 5 + } + + width: 8; + height: 32; + + Rectangle { // base + id: baseBar + radius: 4; + anchors { fill: parent } + color: colors.gutter; + } + + Rectangle { // mask + id: mask; + visible: (!(pushToTalk && !pushingToTalk)) + height: parent.height * level; + width: parent.width; + radius: 5; + anchors { + bottom: parent.bottom; + bottomMargin: 0; + left: parent.left; + leftMargin: 0; + } + } + + LinearGradient { + anchors { fill: mask } + visible: (!(pushToTalk && !pushingToTalk)) + source: mask + start: Qt.point(0, 0); + end: Qt.point(0, bar.height); + rotation: 180 + gradient: Gradient { + GradientStop { + position: 0.0; + color: colors.greenStart; + } + GradientStop { + position: 0.5; + color: colors.greenEnd; + } + GradientStop { + position: 1.0; + color: colors.yellow; + } + } + } + } +} diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index b9d9727dab..f1351b8cbf 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -56,14 +56,17 @@ RowLayout { HifiConstants { id: hifi; } HifiControlsUit.Button { - text: isPlaying ? qsTr("STOP TESTING YOUR SOUND") : qsTr("TEST YOUR SOUND"); + text: isPlaying ? qsTr("STOP TESTING") : qsTr("TEST YOUR SOUND"); color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); + fontSize: 15; + width: 200; + height: 32; } RalewayRegular { Layout.leftMargin: 2; - size: 14; + size: 18; color: "white"; font.italic: true text: isPlaying ? qsTr("Listen to your output") : ""; diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 8afc60fd90..278ce36362 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -133,7 +133,7 @@ Item { states: [ State { name: AvatarPackagerState.main - PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false } + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; videoEnabled: true; backButtonVisible: false } PropertyChanges { target: avatarPackagerMain; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } }, @@ -229,7 +229,11 @@ Item { } function openDocs() { - Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar"); + Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/package-avatar.html"); + } + + function openVideo() { + Qt.openUrlExternally("https://youtu.be/zrkEowu_yps"); } AvatarPackagerHeader { @@ -243,6 +247,9 @@ Item { onDocsButtonClicked: { avatarPackager.openDocs(); } + onVideoButtonClicked: { + avatarPackager.openVideo(); + } } Item { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 25201bf81e..31528a8557 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -13,6 +13,7 @@ ShadowRectangle { property string title: qsTr("Avatar Packager") property alias docsEnabled: docs.visible + property alias videoEnabled: video.visible property bool backButtonVisible: true // If false, is not visible and does not take up space property bool backButtonEnabled: true // If false, is not visible but does not affect space property bool canRename: false @@ -24,6 +25,7 @@ ShadowRectangle { signal backButtonClicked signal docsButtonClicked + signal videoButtonClicked RalewayButton { id: back @@ -126,6 +128,20 @@ ShadowRectangle { } } + RalewayButton { + id: video + visible: false + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: docs.left + anchors.rightMargin: 16 + + text: qsTr("Video") + + onClicked: videoButtonClicked() + } + RalewayButton { id: docs visible: false @@ -137,8 +153,6 @@ ShadowRectangle { text: qsTr("Docs") - onClicked: { - docsButtonClicked(); - } + onClicked: docsButtonClicked() } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index bf8c06d1b3..e5bffa7829 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -339,8 +339,8 @@ Item { visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors anchors { - top: notForSaleMessage.bottom - topMargin: 16 + top: notForSaleMessage.visible ? notForSaleMessage.bottom : infoMessage .bottom + bottom: showFilesText.top horizontalCenter: parent.horizontalCenter } diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 136d535b3f..391e4fab37 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -113,6 +113,7 @@ Rectangle { } else if (prop === 'dimensions') { scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x); } + modified = true; } } diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 68d437a346..e159344d5c 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -71,6 +71,7 @@ Item { onBalanceResult : { balanceText.text = result.data.balance; + sendButton.enabled = true; } onTransferAssetToNodeResult: { @@ -1371,6 +1372,7 @@ Item { height: 40; width: 100; text: "SUBMIT"; + enabled: false; onClicked: { if (root.assetCertID === "" && parseInt(amountTextField.text) > parseInt(balanceText.text)) { amountTextField.focus = true; @@ -2246,6 +2248,7 @@ Item { if (sendAssetStep.selectedRecipientUserName === "") { console.log("SendAsset: Script didn't specify a recipient username!"); sendAssetHome.visible = false; + root.nextActiveView = 'paymentFailure'; return; } diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 07ded49956..619547ef43 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -87,22 +87,11 @@ Rectangle { console.log("Failed to get Marketplace Categories", result.data.message); } else { categoriesModel.clear(); - categoriesModel.append({ - id: -1, - name: "Everything" - }); - categoriesModel.append({ - id: -1, - name: "Stand-alone Optimized" - }); - categoriesModel.append({ - id: -1, - name: "Stand-alone Compatible" - }); - result.data.items.forEach(function(category) { + result.data.categories.forEach(function(category) { categoriesModel.append({ id: category.id, - name: category.name + name: category.name, + count: category.count }); }); } @@ -359,9 +348,11 @@ Rectangle { } onAccepted: { - root.searchString = searchField.text; - getMarketplaceItems(); - searchField.forceActiveFocus(); + if (root.searchString !== searchField.text) { + root.searchString = searchField.text; + getMarketplaceItems(); + searchField.forceActiveFocus(); + } } onActiveFocusChanged: { @@ -382,6 +373,7 @@ Rectangle { id: categoriesDropdown anchors.fill: parent; + anchors.topMargin: 2 visible: false z: 10 @@ -396,12 +388,12 @@ Rectangle { Rectangle { anchors { - left: parent.left; - bottom: parent.bottom; - top: parent.top; - topMargin: 100; + left: parent.left + bottom: parent.bottom + top: parent.top + topMargin: 100 } - width: parent.width/3 + width: parent.width*2/3 color: hifi.colors.white @@ -420,6 +412,7 @@ Rectangle { model: categoriesModel delegate: ItemDelegate { + id: categoriesItemDelegate height: 34 width: parent.width @@ -431,34 +424,71 @@ Rectangle { color: hifi.colors.white visible: true + border.color: hifi.colors.blueHighlight + border.width: 0 - RalewayRegular { + RalewaySemiBold { id: categoriesItemText anchors.leftMargin: 15 - anchors.fill:parent + anchors.top:parent.top + anchors.bottom: parent.bottom + anchors.left: categoryItemCount.right + elide: Text.ElideRight text: model.name - color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray + color: categoriesItemDelegate.ListView.isCurrentItem ? hifi.colors.blueHighlight : hifi.colors.baseGray horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter size: 14 } + Rectangle { + id: categoryItemCount + anchors { + top: parent.top + bottom: parent.bottom + topMargin: 7 + bottomMargin: 7 + leftMargin: 10 + rightMargin: 10 + left: parent.left + } + width: childrenRect.width + color: hifi.colors.faintGray + radius: height/2 + + RalewaySemiBold { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 50 + + text: model.count + color: hifi.colors.lightGrayText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + size: 16 + } + } } - MouseArea { anchors.fill: parent z: 10 - hoverEnabled: true propagateComposedEvents: false - onEntered: { - categoriesItem.color = ListView.isCurrentItem ? hifi.colors.white : hifi.colors.lightBlueHighlight; + onPositionChanged: { + // Must use onPositionChanged and not onEntered + // due to a QML bug where a mouseenter event was + // being fired on open of the categories list even + // though the mouse was outside the borders + categoriesItem.border.width = 2; + } + onExited: { + categoriesItem.border.width = 0; } - onExited: { - categoriesItem.color = ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.white; + onCanceled: { + categoriesItem.border.width = 0; } onClicked: { @@ -476,9 +506,9 @@ Rectangle { parent: categoriesListView.parent anchors { - top: categoriesListView.top; - bottom: categoriesListView.bottom; - left: categoriesListView.right; + top: categoriesListView.top + bottom: categoriesListView.bottom + left: categoriesListView.right } contentItem.opacity: 1 @@ -559,6 +589,8 @@ Rectangle { standaloneOptimized: model.standalone_optimized onShowItem: { + // reset the edition back to -1 to clear the 'update item' status + marketplaceItem.edition = -1; MarketplaceScriptingInterface.getMarketplaceItem(item_id); } @@ -632,7 +664,7 @@ Rectangle { text: "LOG IN" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 605a68fe73..ce692c04d9 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -298,7 +298,7 @@ Rectangle { property bool isFreeSpecial: isStocking || isUpdate enabled: isFreeSpecial || (availability === 'available') buttonGlyph: (enabled && !isUpdate && (price > 0)) ? hifi.glyphs.hfc : "" - text: isUpdate ? "UPDATE FOR FREE" : (isStocking ? "FREE STOCK" : (enabled ? (price || "FREE") : availability)) + text: isUpdate ? "UPDATE" : (isStocking ? "FREE STOCK" : (enabled ? (price || "FREE") : availability)) color: hifi.buttons.blue buttonGlyphSize: 24 diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index a7b36eae36..0e3402a6a9 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -49,7 +49,7 @@ Item { property string wornEntityID; property string updatedItemId; property string upgradeTitle; - property bool updateAvailable: root.updateItemId && root.updateItemId !== ""; + property bool updateAvailable: root.updateItemId !== ""; property bool valid; property bool standaloneOptimized; property bool standaloneIncompatible; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 46bbb626c6..9a2bf62e08 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -523,9 +523,9 @@ Rectangle { item.cardBackVisible = false; item.isInstalled = root.installedApps.indexOf(item.id) > -1; item.wornEntityID = ''; + item.upgrade_id = item.upgrade_id ? item.upgrade_id : ""; }); sendToScript({ method: 'purchases_updateWearables' }); - return data.assets; } } @@ -545,7 +545,7 @@ Rectangle { delegate: PurchasedItem { itemName: title; itemId: id; - updateItemId: model.upgrade_id ? model.upgrade_id : ""; + updateItemId: model.upgrade_id itemPreviewImageUrl: preview; itemHref: download_url; certificateId: certificate_id; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ea74549084..7c2b86ef99 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -32,6 +32,7 @@ Rectangle { property string initialActiveViewAfterStatus5: "walletInventory"; property bool keyboardRaised: false; property bool isPassword: false; + property bool has3DHTML: PlatformInfo.has3DHTML(); anchors.fill: (typeof parent === undefined) ? undefined : parent; @@ -335,8 +336,10 @@ Rectangle { Connections { onSendSignalToWallet: { if (msg.method === 'transactionHistory_usernameLinkClicked') { - userInfoViewer.url = msg.usernameLink; - userInfoViewer.visible = true; + if (has3DHTML) { + userInfoViewer.url = msg.usernameLink; + userInfoViewer.visible = true; + } } else { sendToScript(msg); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index eb8aa0f809..06d07a28c9 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -24,6 +24,8 @@ Item { HifiConstants { id: hifi; } id: root; + + property bool has3DHTML: PlatformInfo.has3DHTML(); onVisibleChanged: { if (visible) { @@ -333,7 +335,9 @@ Item { onLinkActivated: { if (link.indexOf("users/") !== -1) { - sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); + if (has3DHTML) { + sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); + } } else { sendSignalToWallet({method: 'transactionHistory_linkClicked', itemId: model.marketplace_item}); } diff --git a/interface/resources/qml/hifi/dialogs/Audio.qml b/interface/resources/qml/hifi/dialogs/Audio.qml deleted file mode 100644 index 4ce9e14c42..0000000000 --- a/interface/resources/qml/hifi/dialogs/Audio.qml +++ /dev/null @@ -1,27 +0,0 @@ -// -// Audio.qml -// -// Created by Zach Pomerantz on 6/12/2017 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import "../../windows" -import "../audio" - -ScrollingWindow { - id: root; - - resizable: true; - destroyOnHidden: true; - width: 400; - height: 577; - minSize: Qt.vector2d(400, 500); - - Audio { id: audio; width: root.width } - - objectName: "AudioDialog"; - title: audio.title; -} diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 4edae017d1..1342e55b5d 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -35,6 +35,7 @@ StackView { property int cardWidth: 212; property int cardHeight: 152; property var tablet: null; + property bool has3DHTML: PlatformInfo.has3DHTML(); RootHttpRequest { id: http; } signal sendToScript(var message); @@ -75,8 +76,10 @@ StackView { } function goCard(targetString, standaloneOptimized) { if (0 !== targetString.indexOf('hifi://')) { - var card = tabletWebView.createObject(); - card.url = addressBarDialog.metaverseServerUrl + targetString; + if(has3DHTML) { + var card = tabletWebView.createObject(); + card.url = addressBarDialog.metaverseServerUrl + targetString; + } card.parentStackItem = root; root.push(card); return; diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index a1da69a44a..1a1e0a96ff 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -40,7 +40,7 @@ Item { } } - HifiAudio.MicBar { + HifiAudio.MicBarApplication { anchors { left: parent.left leftMargin: 30 diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 8d237d146a..5559c36fd1 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -117,7 +117,6 @@ Rectangle { if (loader.item.hasOwnProperty("gotoPreviousApp")) { loader.item.gotoPreviousApp = true; } - screenChanged("Web", url) }); } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index a27c7b59dc..36a37134bf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -279,6 +279,7 @@ Rectangle { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml index d5fab57501..995af90f0b 100644 --- a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml @@ -344,6 +344,7 @@ Item { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/resources/qml/stylesUit/HifiConstants.qml b/interface/resources/qml/stylesUit/HifiConstants.qml index 75f028cd4f..2394b36106 100644 --- a/interface/resources/qml/stylesUit/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/HifiConstants.qml @@ -330,6 +330,7 @@ QtObject { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6d9a1823a1..147b5b79a6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -338,6 +338,10 @@ Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTRE Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; +static const QUrl AVATAR_INPUTS_BAR_QML = PathUtils::qmlUrl("AvatarInputsBar.qml"); +static const QUrl MIC_BAR_APPLICATION_QML = PathUtils::qmlUrl("hifi/audio/MicBarApplication.qml"); +static const QUrl BUBBLE_ICON_QML = PathUtils::qmlUrl("BubbleIcon.qml"); + static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; @@ -1206,10 +1210,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); - } auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->deleteEntity(getTabletScreenID()); entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); @@ -1599,12 +1599,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { using namespace controller; auto tabletScriptingInterface = DependencyManager::get(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); { auto actionEnum = static_cast(action); int key = Qt::Key_unknown; static int lastKey = Qt::Key_unknown; bool navAxis = false; switch (actionEnum) { + case Action::TOGGLE_PUSHTOTALK: + if (state > 0.0f) { + audioScriptingInterface->setPushingToTalk(true); + } else if (state <= 0.0f) { + audioScriptingInterface->setPushingToTalk(false); + } + break; + case Action::UI_NAV_VERTICAL: navAxis = true; if (state > 0.0f) { @@ -1976,6 +1985,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return nullptr; }); + EntityTree::setEmitScriptEventOperator([this](const QUuid& id, const QVariant& message) { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + entity->emitScriptEvent(message); + } + }); + EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { auto entities = getEntities(); if (auto entity = entities->renderableForEntityId(id)) { @@ -2333,6 +2349,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return viewFrustum.getPosition(); }); + DependencyManager::get()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); }); + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { bool isTablet = url == TabletScriptingInterface::QML; if (htmlContent) { @@ -2358,7 +2376,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); }); auto rootItemLoadedFunctor = [webSurface, url, isTablet] { - Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); + Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString() || url == AVATAR_INPUTS_BAR_QML.toString() || + url == BUBBLE_ICON_QML.toString()); }; if (webSurface->getRootItem()) { rootItemLoadedFunctor(); @@ -2695,9 +2714,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); - if (_snapshotSoundInjector != nullptr) { - _snapshotSoundInjector->stop(); - } + _snapshotSoundInjector = nullptr; // destroy Audio so it and its threads have a chance to go down safely // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine @@ -2866,11 +2883,19 @@ void Application::initializeGL() { } #if !defined(DISABLE_QML) + QStringList chromiumFlags; + // Bug 21993: disable microphone and camera input + chromiumFlags << "--use-fake-device-for-media-stream"; // Disable signed distance field font rendering on ATI/AMD GPUs, due to // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app std::string vendor{ (const char*)glGetString(GL_VENDOR) }; if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + chromiumFlags << "--disable-distance-field-text"; + } + + // Ensure all Qt webengine processes launched from us have the appropriate command line flags + if (!chromiumFlags.empty()) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags.join(' ').toLocal8Bit()); } #endif @@ -3034,6 +3059,9 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/wallet/Wallet.qml" }, QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, QUrl{ "hifi/tablet/TabletAddressDialog.qml" }, + QUrl{ "hifi/Card.qml" }, + QUrl{ "hifi/Pal.qml" }, + QUrl{ "hifi/NameCard.qml" }, }, platformInfoCallback); QmlContextCallback ttsCallback = [](QQmlContext* context) { @@ -3276,6 +3304,41 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); offscreenUi->show(qml, "AvatarInputsBar"); #endif + _desktopRootItemCreated = true; +} + +void Application::userKickConfirmation(const QUuid& nodeID) { + auto avatarHashMap = DependencyManager::get(); + auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); + + QString userName; + + if (avatar) { + userName = avatar->getSessionDisplayName(); + } else { + userName = nodeID.toString(); + } + + QString kickMessage = "Do you wish to kick " + userName + " from your domain"; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage, + QMessageBox::Yes | QMessageBox::No); + + if (dlg->getDialogItem()) { + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool yes = (static_cast(answer.toInt()) == QMessageBox::Yes); + // ask the NodeList to kick the user with the given session ID + + if (yes) { + DependencyManager::get()->kickNodeBySessionID(nodeID); + } + + DependencyManager::get()->setWaitForKickResponse(false); + }); + DependencyManager::get()->setWaitForKickResponse(true); + } } void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { @@ -3673,14 +3736,11 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // If this is a first run we short-circuit the address passed in if (_firstRun.get()) { -#if !defined(Q_OS_ANDROID) DependencyManager::get()->goToEntry(); sentTo = SENT_TO_ENTRY; -#endif _firstRun.set(false); } else { -#if !defined(Q_OS_ANDROID) QString goingTo = ""; if (addressLookupString.isEmpty()) { if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { @@ -3694,7 +3754,6 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); DependencyManager::get()->loadSettings(addressLookupString); sentTo = SENT_TO_PREVIOUS_LOCATION; -#endif } UserActivityLogger::getInstance().logAction("startup_sent_to", { @@ -4216,10 +4275,9 @@ void Application::keyPressEvent(QKeyEvent* event) { Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; if (notificationSounds.get() && notificationSoundSnapshot.get()) { if (_snapshotSoundInjector) { - _snapshotSoundInjector->setOptions(options); - _snapshotSoundInjector->restart(); + DependencyManager::get()->setOptionsAndRestart(_snapshotSoundInjector, options); } else { - _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); + _snapshotSoundInjector = DependencyManager::get()->playSound(_snapshotSound, options); } } takeSnapshot(true); @@ -4310,6 +4368,7 @@ void Application::keyReleaseEvent(QKeyEvent* event) { if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->keyReleaseEvent(event); } + } void Application::focusOutEvent(QFocusEvent* event) { @@ -5241,6 +5300,9 @@ void Application::loadSettings() { } } + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->loadData(); + getMyAvatar()->loadData(); _settingsLoaded = true; } @@ -5250,6 +5312,9 @@ void Application::saveSettings() const { DependencyManager::get()->saveSettings(); DependencyManager::get()->saveSettings(); + auto audioScriptingInterface = reinterpret_cast(DependencyManager::get().data()); + audioScriptingInterface->saveData(); + Menu::getInstance()->saveSettings(); getMyAvatar()->saveData(); PluginManager::getInstance()->saveSettings(); @@ -5756,6 +5821,7 @@ void Application::reloadResourceCaches() { queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + getMyAvatar()->prepareAvatarEntityDataForReload(); // Clear the entities and their renderables getEntities()->clear(); @@ -6931,9 +6997,6 @@ void Application::updateWindowTitle() const { } void Application::clearDomainOctreeDetails(bool clearAll) { - // before we delete all entities get MyAvatar's AvatarEntityData ready - getMyAvatar()->prepareAvatarEntityDataForReload(); - // if we're about to quit, we really don't need to do the rest of these things... if (_aboutToQuit) { return; @@ -8919,6 +8982,38 @@ void Application::updateLoginDialogPosition() { } } +void Application::createAvatarInputsBar() { + const glm::vec3 LOCAL_POSITION { 0.0, 0.0, -1.0 }; + // DEFAULT_DPI / tablet scale percentage + const float DPI = 31.0f / (75.0f / 100.0f); + + EntityItemProperties properties; + properties.setType(EntityTypes::Web); + properties.setName("AvatarInputsBarEntity"); + properties.setSourceUrl(AVATAR_INPUTS_BAR_QML.toString()); + properties.setParentID(getMyAvatar()->getSelfID()); + properties.setParentJointIndex(getMyAvatar()->getJointIndex("_CAMERA_MATRIX")); + properties.setPosition(LOCAL_POSITION); + properties.setLocalRotation(Quaternions::IDENTITY); + //properties.setDimensions(LOGIN_DIMENSIONS); + properties.setPrimitiveMode(PrimitiveMode::SOLID); + properties.getGrab().setGrabbable(false); + properties.setIgnorePickIntersection(false); + properties.setAlpha(1.0f); + properties.setDPI(DPI); + properties.setVisible(true); + + auto entityScriptingInterface = DependencyManager::get(); + _avatarInputsBarID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); +} + +void Application::destroyAvatarInputsBar() { + auto entityScriptingInterface = DependencyManager::get(); + if (!_avatarInputsBarID.isNull()) { + entityScriptingInterface->deleteEntity(_avatarInputsBarID); + } +} + bool Application::hasRiftControllers() { return PluginUtils::isOculusTouchControllerAvailable(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index a8cc9450c5..99e57f1866 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -330,6 +330,9 @@ public: void createLoginDialog(); void updateLoginDialogPosition(); + void createAvatarInputsBar(); + void destroyAvatarInputsBar(); + // Check if a headset is connected bool hasRiftControllers(); bool hasViveControllers(); @@ -593,6 +596,7 @@ private: void toggleTabletUI(bool shouldOpen = false) const; static void setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties); + void userKickConfirmation(const QUuid& nodeID); MainWindow* _window; QElapsedTimer& _sessionRunTimer; @@ -703,12 +707,14 @@ private: int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS; bool _interstitialModeEnabled{ false }; - bool _loginDialogPoppedUp = false; + bool _loginDialogPoppedUp{ false }; + bool _desktopRootItemCreated{ false }; bool _developerMenuVisible{ false }; QString _previousAvatarSkeletonModel; float _previousAvatarTargetScale; CameraMode _previousCameraMode; QUuid _loginDialogID; + QUuid _avatarInputsBarID; LoginStateManager _loginStateManager; quint64 _lastFaceTrackerUpdate; diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 5fe35bd23f..54c67daab8 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -149,6 +149,9 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { emit bookmarkDeleted(bookmarkName); } +void AvatarBookmarks::deleteBookmark() { +} + void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) { auto myAvatar = DependencyManager::get()->getMyAvatar(); auto currentAvatarEntities = myAvatar->getAvatarEntityData(); diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 4623e7d929..df75fec865 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -76,6 +76,9 @@ protected: void readFromFile() override; QVariantMap getAvatarDataToBookmark(); +protected slots: + void deleteBookmark() override; + private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; const QString ENTRY_AVATAR_URL = "avatarUrl"; diff --git a/interface/src/Bookmarks.h b/interface/src/Bookmarks.h index 88510e4eda..56d26b55c6 100644 --- a/interface/src/Bookmarks.h +++ b/interface/src/Bookmarks.h @@ -51,13 +51,10 @@ protected: bool _isMenuSorted; protected slots: - /**jsdoc - * @function AvatarBookmarks.deleteBookmark - */ /**jsdoc * @function LocationBookmarks.deleteBookmark */ - void deleteBookmark(); + virtual void deleteBookmark(); private: static bool sortOrder(QAction* a, QAction* b); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 810e21daf5..394c07e842 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -270,10 +270,14 @@ Menu::Menu() { // Settings > Audio... action = addActionToQMenuAndActionHash(settingsMenu, "Audio..."); connect(action, &QAction::triggered, [] { - static const QUrl widgetUrl("hifi/dialogs/Audio.qml"); static const QUrl tabletUrl("hifi/audio/Audio.qml"); - static const QString name("AudioDialog"); - qApp->showDialog(widgetUrl, tabletUrl, name); + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + auto hmd = DependencyManager::get(); + tablet->pushOntoStack(tabletUrl); + + if (!hmd->getShouldShowTablet()) { + hmd->toggleShouldShowTablet(); + } }); // Settings > Graphics... diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index 04a426c3db..01a40e89fd 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -55,7 +55,7 @@ static QStringList HAND_MAPPING_SUFFIXES = { "HandThumb1", }; -const QUrl DEFAULT_DOCS_URL = QUrl("https://docs.highfidelity.com/create/avatars/create-avatars.html#create-your-own-avatar"); +const QUrl PACKAGE_AVATAR_DOCS_BASE_URL = QUrl("https://docs.highfidelity.com/create/avatars/package-avatar.html"); AvatarDoctor::AvatarDoctor(const QUrl& avatarFSTFileUrl) : _avatarFSTFileUrl(avatarFSTFileUrl) { @@ -85,53 +85,53 @@ void AvatarDoctor::startDiagnosing() { const auto resourceLoaded = [this, resource](bool success) { // MODEL if (!success) { - _errors.push_back({ "Model file cannot be opened.", DEFAULT_DOCS_URL }); + addError("Model file cannot be opened.", "missing-file"); emit complete(getErrors()); return; } _model = resource; const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); - if (!avatarModel.originalURL.endsWith(".fbx")) { - _errors.push_back({ "Unsupported avatar model format.", DEFAULT_DOCS_URL }); + if (!avatarModel.originalURL.toLower().endsWith(".fbx")) { + addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); return; } // RIG if (avatarModel.joints.isEmpty()) { - _errors.push_back({ "Avatar has no rig.", DEFAULT_DOCS_URL }); + addError("Avatar has no rig.", "no-rig"); } else { auto jointNames = avatarModel.getJointNames(); if (avatarModel.joints.length() > NETWORKED_JOINTS_LIMIT) { - _errors.push_back({tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), DEFAULT_DOCS_URL }); + addError(tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), "maximum-bone-limit"); } // Avatar does not have Hips bone mapped if (!jointNames.contains("Hips")) { - _errors.push_back({ "Hips are not mapped.", DEFAULT_DOCS_URL }); + addError("Hips are not mapped.", "hips-not-mapped"); } if (!jointNames.contains("Spine")) { - _errors.push_back({ "Spine is not mapped.", DEFAULT_DOCS_URL }); + addError("Spine is not mapped.", "spine-not-mapped"); } if (!jointNames.contains("Spine1")) { - _errors.push_back({ "Chest (Spine1) is not mapped.", DEFAULT_DOCS_URL }); + addError("Chest (Spine1) is not mapped.", "chest-not-mapped"); } if (!jointNames.contains("Neck")) { - _errors.push_back({ "Neck is not mapped.", DEFAULT_DOCS_URL }); + addError("Neck is not mapped.", "neck-not-mapped"); } if (!jointNames.contains("Head")) { - _errors.push_back({ "Head is not mapped.", DEFAULT_DOCS_URL }); + addError("Head is not mapped.", "head-not-mapped"); } if (!jointNames.contains("LeftEye")) { if (jointNames.contains("RightEye")) { - _errors.push_back({ "LeftEye is not mapped.", DEFAULT_DOCS_URL }); + addError("LeftEye is not mapped.", "eye-not-mapped"); } else { - _errors.push_back({ "Eyes are not mapped.", DEFAULT_DOCS_URL }); + addError("Eyes are not mapped.", "eye-not-mapped"); } } else if (!jointNames.contains("RightEye")) { - _errors.push_back({ "RightEye is not mapped.", DEFAULT_DOCS_URL }); + addError("RightEye is not mapped.", "eye-not-mapped"); } const auto checkJointAsymmetry = [jointNames] (const QStringList& jointMappingSuffixes) { @@ -159,13 +159,13 @@ void AvatarDoctor::startDiagnosing() { }; if (checkJointAsymmetry(ARM_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical arm bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical arm bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(HAND_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical hand bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical hand bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(LEG_MAPPING_SUFFIXES)) { - _errors.push_back({ "Asymmetrical leg bones.", DEFAULT_DOCS_URL }); + addError("Asymmetrical leg bones.", "asymmetrical-bones"); } // Multiple skeleton root joints checkup @@ -177,7 +177,7 @@ void AvatarDoctor::startDiagnosing() { } if (skeletonRootJoints > 1) { - _errors.push_back({ "Multiple top-level joints found.", DEFAULT_DOCS_URL }); + addError("Multiple top-level joints found.", "multiple-top-level-joints"); } Rig rig; @@ -191,9 +191,9 @@ void AvatarDoctor::startDiagnosing() { const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly too short.", DEFAULT_DOCS_URL }); + addError("Avatar is possibly too short.", "short-avatar"); } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { - _errors.push_back({ "Avatar is possibly too tall.", DEFAULT_DOCS_URL }); + addError("Avatar is possibly too tall.", "tall-avatar"); } // HipsNotOnGround @@ -204,7 +204,7 @@ void AvatarDoctor::startDiagnosing() { const auto hipJoint = avatarModel.joints.at(avatarModel.getJointIndex("Hips")); if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - _errors.push_back({ "Hips are on ground.", DEFAULT_DOCS_URL }); + addError("Hips are on ground.", "hips-on-ground"); } } } @@ -223,7 +223,7 @@ void AvatarDoctor::startDiagnosing() { const auto hipsToSpine = glm::length(hipsPosition - spinePosition); const auto spineToChest = glm::length(spinePosition - chestPosition); if (hipsToSpine < HIPS_SPINE_CHEST_MIN_SEPARATION && spineToChest < HIPS_SPINE_CHEST_MIN_SEPARATION) { - _errors.push_back({ "Hips/Spine/Chest overlap.", DEFAULT_DOCS_URL }); + addError("Hips/Spine/Chest overlap.", "overlap-error"); } } } @@ -240,21 +240,21 @@ void AvatarDoctor::startDiagnosing() { const auto& uniqueJointValues = jointValues.toSet(); for (const auto& jointName: uniqueJointValues) { if (jointValues.count(jointName) > 1) { - _errors.push_back({ tr("%1 is mapped multiple times.").arg(jointName), DEFAULT_DOCS_URL }); + addError(tr("%1 is mapped multiple times.").arg(jointName), "mapped-multiple-times"); } } } if (!isDescendantOfJointWhenJointsExist("Spine", "Hips")) { - _errors.push_back({ "Spine is not a child of Hips.", DEFAULT_DOCS_URL }); + addError("Spine is not a child of Hips.", "spine-not-child"); } if (!isDescendantOfJointWhenJointsExist("Spine1", "Spine")) { - _errors.push_back({ "Spine1 is not a child of Spine.", DEFAULT_DOCS_URL }); + addError("Spine1 is not a child of Spine.", "spine1-not-child"); } if (!isDescendantOfJointWhenJointsExist("Head", "Spine1")) { - _errors.push_back({ "Head is not a child of Spine1.", DEFAULT_DOCS_URL }); + addError("Head is not a child of Spine1.", "head-not-child"); } } @@ -300,7 +300,7 @@ void AvatarDoctor::startDiagnosing() { connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); } } else { - _errors.push_back({ "Model file cannot be opened", DEFAULT_DOCS_URL }); + addError("Model file cannot be opened", "missing-file"); emit complete(getErrors()); } } @@ -345,7 +345,7 @@ void AvatarDoctor::diagnoseTextures() { QUrl(avatarModel.originalURL)).resolved(QUrl("textures")); if (texturesFound == 0) { - _errors.push_back({ tr("No textures assigned."), DEFAULT_DOCS_URL }); + addError(tr("No textures assigned."), "no-textures-assigned"); } if (!externalTextures.empty()) { @@ -356,11 +356,10 @@ void AvatarDoctor::diagnoseTextures() { auto checkTextureLoadingComplete = [this]() mutable { if (_checkedTextureCount == _externalTextureCount) { if (_missingTextureCount > 0) { - _errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_DOCS_URL }); + addError(tr("Missing %n texture(s).","", _missingTextureCount), "missing-textures"); } if (_unsupportedTextureCount > 0) { - _errors.push_back({ tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), - DEFAULT_DOCS_URL }); + addError(tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), "unsupported-textures"); } emit complete(getErrors()); @@ -411,6 +410,12 @@ void AvatarDoctor::diagnoseTextures() { } } +void AvatarDoctor::addError(const QString& errorMessage, const QString& docFragment) { + QUrl documentationURL = PACKAGE_AVATAR_DOCS_BASE_URL; + documentationURL.setFragment(docFragment); + _errors.push_back({ errorMessage, documentationURL }); +} + QVariantList AvatarDoctor::getErrors() const { QVariantList result; for (const auto& error : _errors) { diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index 780f600bed..1465a5defc 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -40,6 +40,8 @@ signals: private: void diagnoseTextures(); + void addError(const QString& errorMessage, const QString& docFragment); + QUrl _avatarFSTFileUrl; QVector _errors; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 55025b3b23..7a64edae11 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include "Application.h" #include "InterfaceLogging.h" @@ -84,7 +85,6 @@ AvatarManager::AvatarManager(QObject* parent) : AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { AvatarSharedPointer avatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer); - const auto otherAvatar = std::static_pointer_cast(avatar); if (otherAvatar && _space) { std::unique_lock lock(_spaceLock); @@ -210,7 +210,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { { // lock the hash for read to check the size QReadLocker lock(&_hashLock); - if (_avatarHash.size() < 2 && _avatarsToFadeOut.isEmpty()) { + if (_avatarHash.size() < 2) { return; } } @@ -232,96 +232,142 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { auto avatarMap = getHashCopy(); const auto& views = qApp->getConicalViews(); - PrioritySortUtil::PriorityQueue sortedAvatars(views, - AvatarData::_avatarSortCoefficientSize, - AvatarData::_avatarSortCoefficientCenter, - AvatarData::_avatarSortCoefficientAge); - sortedAvatars.reserve(avatarMap.size() - 1); // don't include MyAvatar + // Prepare 2 queues for heros and for crowd avatars + using AvatarPriorityQueue = PrioritySortUtil::PriorityQueue; + // Keep two independent queues, one for heroes and one for the riff-raff. + enum PriorityVariants + { + kHero = 0, + kNonHero, + NumVariants + }; + AvatarPriorityQueue avatarPriorityQueues[NumVariants] = { + { views, + AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, + AvatarData::_avatarSortCoefficientAge }, + { views, + AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, + AvatarData::_avatarSortCoefficientAge } }; + // Reserve space + //avatarPriorityQueues[kHero].reserve(10); // just few + avatarPriorityQueues[kNonHero].reserve(avatarMap.size() - 1); // don't include MyAvatar // Build vector and compute priorities auto nodeList = DependencyManager::get(); AvatarHash::iterator itr = avatarMap.begin(); while (itr != avatarMap.end()) { - const auto& avatar = std::static_pointer_cast(*itr); + auto avatar = std::static_pointer_cast(*itr); // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. // DO NOT update or fade out uninitialized Avatars if (avatar != _myAvatar && avatar->isInitialized() && !nodeList->isPersonalMutingNode(avatar->getID())) { - sortedAvatars.push(SortableAvatar(avatar)); + if (avatar->getHasPriority()) { + avatarPriorityQueues[kHero].push(SortableAvatar(avatar)); + } else { + avatarPriorityQueues[kNonHero].push(SortableAvatar(avatar)); + } } ++itr; } - // Sort - const auto& sortedAvatarVector = sortedAvatars.getSortedVector(); + + _numHeroAvatars = (int)avatarPriorityQueues[kHero].size(); // process in sorted order uint64_t startTime = usecTimestampNow(); - uint64_t updateExpiry = startTime + MAX_UPDATE_AVATARS_TIME_BUDGET; + + const uint64_t MAX_UPDATE_HEROS_TIME_BUDGET = uint64_t(0.8 * MAX_UPDATE_AVATARS_TIME_BUDGET); + + uint64_t updatePriorityExpiries[NumVariants] = { startTime + MAX_UPDATE_HEROS_TIME_BUDGET, startTime + MAX_UPDATE_AVATARS_TIME_BUDGET }; + int numHerosUpdated = 0; int numAvatarsUpdated = 0; - int numAVatarsNotUpdated = 0; + int numAvatarsNotUpdated = 0; render::Transaction renderTransaction; workload::Transaction workloadTransaction; - for (auto it = sortedAvatarVector.begin(); it != sortedAvatarVector.end(); ++it) { - const SortableAvatar& sortData = *it; - const auto avatar = std::static_pointer_cast(sortData.getAvatar()); - if (!avatar->_isClientAvatar) { - avatar->setIsClientAvatar(true); - } - // TODO: to help us scale to more avatars it would be nice to not have to poll this stuff every update - if (avatar->getSkeletonModel()->isLoaded()) { - // remove the orb if it is there - avatar->removeOrb(); - if (avatar->needsPhysicsUpdate()) { - _avatarsToChangeInPhysics.insert(avatar); - } - } else { - avatar->updateOrbPosition(); - } + + for (int p = kHero; p < NumVariants; p++) { + auto& priorityQueue = avatarPriorityQueues[p]; + // Sorting the current queue HERE as part of the measured timing. + const auto& sortedAvatarVector = priorityQueue.getSortedVector(); - // for ALL avatars... - if (_shouldRender) { - avatar->ensureInScene(avatar, qApp->getMain3DScene()); - } - avatar->animateScaleChanges(deltaTime); + auto passExpiry = updatePriorityExpiries[p]; - uint64_t now = usecTimestampNow(); - if (now < updateExpiry) { - // we're within budget - bool inView = sortData.getPriority() > OUT_OF_VIEW_THRESHOLD; - if (inView && avatar->hasNewJointData()) { - numAvatarsUpdated++; + for (auto it = sortedAvatarVector.begin(); it != sortedAvatarVector.end(); ++it) { + const SortableAvatar& sortData = *it; + const auto avatar = std::static_pointer_cast(sortData.getAvatar()); + if (!avatar->_isClientAvatar) { + avatar->setIsClientAvatar(true); } - auto transitStatus = avatar->_transit.update(deltaTime, avatar->_serverPosition, _transitConfig); - if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) { - avatar->_transit.reset(); - avatar->setIsNewAvatar(false); - } - avatar->simulate(deltaTime, inView); - if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) { - _myAvatar->addAvatarHandsToFlow(avatar); - } - avatar->updateRenderItem(renderTransaction); - avatar->updateSpaceProxy(workloadTransaction); - avatar->setLastRenderUpdateTime(startTime); - } else { - // we've spent our full time budget --> bail on the rest of the avatar updates - // --> more avatars may freeze until their priority trickles up - // --> some scale animations may glitch - // --> some avatar velocity measurements may be a little off - - // no time to simulate, but we take the time to count how many were tragically missed - while (it != sortedAvatarVector.end()) { - const SortableAvatar& newSortData = *it; - const auto& newAvatar = newSortData.getAvatar(); - bool inView = newSortData.getPriority() > OUT_OF_VIEW_THRESHOLD; - // Once we reach an avatar that's not in view, all avatars after it will also be out of view - if (!inView) { - break; + // TODO: to help us scale to more avatars it would be nice to not have to poll this stuff every update + if (avatar->getSkeletonModel()->isLoaded()) { + // remove the orb if it is there + avatar->removeOrb(); + if (avatar->needsPhysicsUpdate()) { + _avatarsToChangeInPhysics.insert(avatar); } - numAVatarsNotUpdated += (int)(newAvatar->hasNewJointData()); - ++it; + } else { + avatar->updateOrbPosition(); } - break; + + // for ALL avatars... + if (_shouldRender) { + avatar->ensureInScene(avatar, qApp->getMain3DScene()); + } + + avatar->animateScaleChanges(deltaTime); + + uint64_t now = usecTimestampNow(); + if (now < passExpiry) { + // we're within budget + bool inView = sortData.getPriority() > OUT_OF_VIEW_THRESHOLD; + if (inView && avatar->hasNewJointData()) { + numAvatarsUpdated++; + } + auto transitStatus = avatar->_transit.update(deltaTime, avatar->_serverPosition, _transitConfig); + if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || + transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) { + avatar->_transit.reset(); + avatar->setIsNewAvatar(false); + } + avatar->simulate(deltaTime, inView); + if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) { + _myAvatar->addAvatarHandsToFlow(avatar); + } + avatar->updateRenderItem(renderTransaction); + avatar->updateSpaceProxy(workloadTransaction); + avatar->setLastRenderUpdateTime(startTime); + } else { + // we've spent our time budget for this priority bucket + // let's deal with the reminding avatars if this pass and BREAK from the for loop + + if (p == kHero) { + // Hero, + // --> put them back in the non hero queue + + auto& crowdQueue = avatarPriorityQueues[kNonHero]; + while (it != sortedAvatarVector.end()) { + crowdQueue.push(SortableAvatar((*it).getAvatar())); + ++it; + } + } else { + // Non Hero + // --> bail on the rest of the avatar updates + // --> more avatars may freeze until their priority trickles up + // --> some scale animations may glitch + // --> some avatar velocity measurements may be a little off + + // no time to simulate, but we take the time to count how many were tragically missed + numAvatarsNotUpdated = sortedAvatarVector.end() - it; + } + + // We had to cut short this pass, we must break out of the for loop here + break; + } + } + + if (p == kHero) { + numHerosUpdated = numAvatarsUpdated; } } @@ -329,17 +375,11 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { qApp->getMain3DScene()->enqueueTransaction(renderTransaction); } - if (!_spaceProxiesToDelete.empty() && _space) { - std::unique_lock lock(_spaceLock); - workloadTransaction.remove(_spaceProxiesToDelete); - _spaceProxiesToDelete.clear(); - } _space->enqueueTransaction(workloadTransaction); _numAvatarsUpdated = numAvatarsUpdated; - _numAvatarsNotUpdated = numAVatarsNotUpdated; - - simulateAvatarFades(deltaTime); + _numAvatarsNotUpdated = numAvatarsNotUpdated; + _numHeroAvatarsUpdated = numHerosUpdated; _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } @@ -353,31 +393,6 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen } } -void AvatarManager::simulateAvatarFades(float deltaTime) { - if (_avatarsToFadeOut.empty()) { - return; - } - - QReadLocker locker(&_hashLock); - QVector::iterator avatarItr = _avatarsToFadeOut.begin(); - const render::ScenePointer& scene = qApp->getMain3DScene(); - render::Transaction transaction; - while (avatarItr != _avatarsToFadeOut.end()) { - auto avatar = std::static_pointer_cast(*avatarItr); - avatar->updateFadingStatus(); - if (!avatar->isFading()) { - // fading to zero is such a rare event we push a unique transaction for each - if (avatar->isInScene()) { - avatar->removeFromScene(*avatarItr, scene, transaction); - } - avatarItr = _avatarsToFadeOut.erase(avatarItr); - } else { - ++avatarItr; - } - } - scene->enqueueTransaction(transaction); -} - AvatarSharedPointer AvatarManager::newSharedAvatar(const QUuid& sessionUUID) { auto otherAvatar = new OtherAvatar(qApp->thread()); otherAvatar->setSessionUUID(sessionUUID); @@ -405,7 +420,6 @@ void AvatarManager::buildPhysicsTransaction(PhysicsEngine::Transaction& transact transaction.objectsToRemove.push_back(mState); } avatar->resetDetailedMotionStates(); - } else { if (avatar->getDetailedMotionStates().size() == 0) { avatar->createDetailedMotionStates(avatar); @@ -473,10 +487,6 @@ void AvatarManager::removeDeadAvatarEntities(const SetOfEntities& deadEntities) void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { auto avatar = std::static_pointer_cast(removedAvatar); - { - std::unique_lock lock(_spaceLock); - _spaceProxiesToDelete.push_back(avatar->getSpaceIndex()); - } AvatarHashMap::handleRemovedAvatar(avatar, removalReason); avatar->tearDownGrabs(); @@ -487,16 +497,39 @@ 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) { + if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == KillAvatarReason::NoReason) { + emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); emit DependencyManager::get()->enteredIgnoreRadius(); + + workload::Transaction workloadTransaction; + workloadTransaction.remove(avatar->getSpaceIndex()); + _space->enqueueTransaction(workloadTransaction); + + const render::ScenePointer& scene = qApp->getMain3DScene(); + render::Transaction transaction; + avatar->removeFromScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); DependencyManager::get()->avatarDisconnected(avatar->getSessionUUID()); - avatar->fadeOut(qApp->getMain3DScene(), removalReason); + render::Transaction transaction; + auto scene = qApp->getMain3DScene(); + avatar->fadeOut(transaction, removalReason); + + workload::SpacePointer space = _space; + transaction.transitionFinishedOperator(avatar->getRenderItemID(), [space, avatar]() { + const render::ScenePointer& scene = qApp->getMain3DScene(); + render::Transaction transaction; + avatar->removeFromScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); + + workload::Transaction workloadTransaction; + workloadTransaction.remove(avatar->getSpaceIndex()); + space->enqueueTransaction(workloadTransaction); + }); + scene->enqueueTransaction(transaction); } - _avatarsToFadeOut.push_back(removedAvatar); } void AvatarManager::clearOtherAvatars() { @@ -582,8 +615,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // but most avatars are roughly the same size, so let's not be so fancy yet. const float AVATAR_STRETCH_FACTOR = 1.0f; - _collisionInjectors.remove_if( - [](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); }); + _collisionInjectors.remove_if([](const AudioInjectorPointer& injector) { return !injector; }); static const int MAX_INJECTOR_COUNT = 3; if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) { @@ -593,7 +625,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents options.volume = energyFactorOfFull; options.pitch = 1.0f / AVATAR_STRETCH_FACTOR; - auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); + auto injector = DependencyManager::get()->playSound(collisionSound, options, true); _collisionInjectors.emplace_back(injector); } } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 51352ec861..f9b82da0c1 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include // for SetOfEntities @@ -90,6 +90,8 @@ public: int getNumAvatarsUpdated() const { return _numAvatarsUpdated; } int getNumAvatarsNotUpdated() const { return _numAvatarsNotUpdated; } + int getNumHeroAvatars() const { return _numHeroAvatars; } + int getNumHeroAvatarsUpdated() const { return _numHeroAvatarsUpdated; } float getAvatarSimulationTime() const { return _avatarSimulationTime; } void updateMyAvatar(float deltaTime); @@ -218,8 +220,6 @@ private: explicit AvatarManager(QObject* parent = 0); explicit AvatarManager(const AvatarManager& other); - void simulateAvatarFades(float deltaTime); - AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID) override; // called only from the AvatarHashMap thread - cannot be called while this thread holds the @@ -229,26 +229,25 @@ private: KillAvatarReason removalReason = KillAvatarReason::NoReason) override; void handleTransitAnimations(AvatarTransit::Status status); - QVector _avatarsToFadeOut; - using SetOfOtherAvatars = std::set; SetOfOtherAvatars _avatarsToChangeInPhysics; std::shared_ptr _myAvatar; quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate. - std::list _collisionInjectors; + std::list> _collisionInjectors; RateCounter<> _myAvatarSendRate; int _numAvatarsUpdated { 0 }; int _numAvatarsNotUpdated { 0 }; + int _numHeroAvatars{ 0 }; + int _numHeroAvatarsUpdated{ 0 }; float _avatarSimulationTime { 0.0f }; bool _shouldRender { true }; bool _myAvatarDataPacketsPaused { false }; mutable std::mutex _spaceLock; workload::SpacePointer _space; - std::vector _spaceProxiesToDelete; AvatarTransit::TransitConfig _transitConfig; }; diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 53b37eba4f..28b07780b0 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -87,7 +87,7 @@ void MarketplaceItemUploader::doGetCategories() { if (error == QNetworkReply::NoError) { auto doc = QJsonDocument::fromJson(reply->readAll()); auto extractCategoryID = [&doc]() -> std::pair { - auto items = doc.object()["data"].toObject()["items"]; + auto items = doc.object()["data"].toObject()["categories"]; if (!items.isArray()) { qWarning() << "Categories parse error: data.items is not an array"; return { false, 0 }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9211be3b4f..9ffcd0184e 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -324,8 +324,11 @@ QString MyAvatar::getDominantHand() const { void MyAvatar::setDominantHand(const QString& hand) { if (hand == DOMINANT_LEFT_HAND || hand == DOMINANT_RIGHT_HAND) { - _dominantHand.set(hand); - emit dominantHandChanged(hand); + bool changed = (hand != _dominantHand.get()); + if (changed) { + _dominantHand.set(hand); + emit dominantHandChanged(hand); + } } } @@ -939,8 +942,6 @@ void MyAvatar::simulate(float deltaTime, bool inView) { } handleChangedAvatarEntityData(); - - updateFadingStatus(); } // As far as I know no HMD system supports a play area of a kilometer in radius. @@ -1570,7 +1571,7 @@ void MyAvatar::handleChangedAvatarEntityData() { entityTree->withWriteLock([&] { EntityItemPointer entity = entityTree->addEntity(id, properties); if (entity) { - packetSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, properties); + packetSender->queueEditAvatarEntityMessage(entityTree, id); } }); } @@ -2385,7 +2386,19 @@ void MyAvatar::clearWornAvatarEntities() { } } - +/**jsdoc + * Information about an avatar entity. + * + * + * + * + * + * + * + * + *
PropertyTypeDescription
idUuidEntity ID.
properties{@link Entities.EntityProperties}Entity properties.
+ * @typedef {object} MyAvatar.AvatarEntityData + */ QVariantList MyAvatar::getAvatarEntitiesVariant() { // NOTE: this method is NOT efficient QVariantList avatarEntitiesData; @@ -3450,6 +3463,42 @@ float MyAvatar::getGravity() { return _characterController.getGravity(); } +void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { + QUuid oldSessionID = getSessionUUID(); + Avatar::setSessionUUID(sessionUUID); + QUuid newSessionID = getSessionUUID(); + if (newSessionID != oldSessionID) { + auto treeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + bool sendPackets = !DependencyManager::get()->getSessionUUID().isNull(); + EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); + entityTree->withWriteLock([&] { + for (const auto& entityID : avatarEntityIDs) { + auto entity = entityTree->findEntityByID(entityID); + if (!entity) { + continue; + } + // update OwningAvatarID so entity can be identified as "ours" later + entity->setOwningAvatarID(newSessionID); + // NOTE: each attached AvatarEntity already have the correct updated parentID + // via magic in SpatiallyNestable, hence we check against newSessionID + if (sendPackets && entity->getParentID() == newSessionID) { + // but when we have a real session and the AvatarEntity is parented to MyAvatar + // we need to update the "packedAvatarEntityData" sent to the avatar-mixer + // because it contains a stale parentID somewhere deep inside + packetSender->queueEditAvatarEntityMessage(entityTree, entityID); + } + } + }); + } + } +} + void MyAvatar::increaseSize() { float minScale = getDomainMinScale(); float maxScale = getDomainMaxScale(); @@ -3527,6 +3576,12 @@ void MyAvatar::clearScaleRestriction() { _haveReceivedHeightLimitsFromDomain = false; } +/**jsdoc + * A teleport target. + * @typedef {object} MyAvatar.GoToProperties + * @property {Vec3} position - The avatar's new position. + * @property {Quat} [orientation] - The avatar's new orientation. + */ void MyAvatar::goToLocation(const QVariant& propertiesVar) { qCDebug(interfaceapp, "MyAvatar QML goToLocation"); auto properties = propertiesVar.toMap(); @@ -3883,6 +3938,13 @@ void MyAvatar::setCollisionWithOtherAvatarsFlags() { _characterController.setPendingFlagsUpdateCollisionMask(); } +/**jsdoc + * A collision capsule is a cylinder with hemispherical ends. It is often used to approximate the extents of an avatar. + * @typedef {object} MyAvatar.CollisionCapsule + * @property {Vec3} start - The bottom end of the cylinder, excluding the bottom hemisphere. + * @property {Vec3} end - The top end of the cylinder, excluding the top hemisphere. + * @property {number} radius - The radius of the cylinder and the hemispheres. + */ void MyAvatar::updateCollisionCapsuleCache() { glm::vec3 start, end; float radius; @@ -5332,6 +5394,24 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) } } +/**jsdoc + * Physics options to use in the flow simulation of a joint. + * @typedef {object} MyAvatar.FlowPhysicsOptions + * @property {boolean} [active=true] - true to enable flow on the joint, otherwise false. + * @property {number} [radius=0.01] - The thickness of segments and knots (needed for collisions). + * @property {number} [gravity=-0.0096] - Y-value of the gravity vector. + * @property {number} [inertia=0.8] - Rotational inertia multiplier. + * @property {number} [damping=0.85] - The amount of damping on joint oscillation. + * @property {number} [stiffness=0.0] - The stiffness of each thread. + * @property {number} [delta=0.55] - Delta time for every integration step. + */ +/**jsdoc + * Collision options to use in the flow simulation of a joint. + * @typedef {object} MyAvatar.FlowCollisionsOptions + * @property {string} [type="sphere"] - Currently, only "sphere" is supported. + * @property {number} [radius=0.05] - Collision sphere radius. + * @property {number} [offset=Vec3.ZERO] - Offset of the collision sphere from the joint. + */ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "useFlow", @@ -5381,7 +5461,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } auto collisionJoints = collisionsConfig.keys(); if (collisionJoints.size() > 0) { - collisionSystem.resetCollisions(); + collisionSystem.clearSelfCollisions(); for (auto &jointName : collisionJoints) { int jointIndex = getJointIndex(jointName); FlowCollisionSettings collisionsSettings; @@ -5396,9 +5476,43 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys collisionSystem.addCollisionSphere(jointIndex, collisionsSettings); } } + flow.updateScale(); } } +/**jsdoc + * Flow options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowData + * @property {boolean} initialized - true if flow has been initialized for the current avatar, false + * if it hasn't. + * @property {boolean} active - true if flow is enabled, false if it isn't. + * @property {boolean} colliding - true if collisions are enabled, false if they aren't. + * @property {Object} physicsData - The physics configuration for each group of joints + * that has been configured. + * @property {Object} collisions - The collisions configuration for each joint that + * has collisions configured. + * @property {Object} threads - The threads that have been configured, with the first joint's name as the + * ThreadName and value as an array of the indexes of all the joints in the thread. + */ +/**jsdoc + * A set of physics options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowPhysicsData + * @property {boolean} active - true to enable flow on the joint, otherwise false. + * @property {number} radius - The thickness of segments and knots. (Needed for collisions.) + * @property {number} gravity - Y-value of the gravity vector. + * @property {number} inertia - Rotational inertia multiplier. + * @property {number} damping - The amount of damping on joint oscillation. + * @property {number} stiffness - The stiffness of each thread. + * @property {number} delta - Delta time for every integration step. + * @property {number[]} jointIndices - The indexes of the joints the options are applied to. + */ +/**jsdoc + * A set of collision options currently used in flow simulation. + * @typedef {object} MyAvatar.FlowCollisionsData + * @property {number} radius - Collision sphere radius. + * @property {number} offset - Offset of the collision sphere from the joint. + * @property {number} jointIndex - The index of the joint the options are applied to. + */ QVariantMap MyAvatar::getFlowData() { QVariantMap result; if (QThread::currentThread() != thread()) { @@ -5495,14 +5609,14 @@ void MyAvatar::initFlowFromFST() { } } -void MyAvatar::sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { +void MyAvatar::sendPacket(const QUuid& entityID) const { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { entityTree->withWriteLock([&] { // force an update packet EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entityID, properties); + packetSender->queueEditAvatarEntityMessage(entityTree, entityID); }); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e516364f61..0859c20153 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -60,7 +60,9 @@ class MyAvatar : public Avatar { /**jsdoc * Your avatar is your in-world representation of you. The MyAvatar API is used to manipulate the avatar. * For example, you can customize the avatar's appearance, run custom avatar animations, - * change the avatar's position within the domain, or manage the avatar's collisions with other objects. + * change the avatar's position within the domain, or manage the avatar's collisions with the environment and other avatars. + * + *

For assignment client scripts, see {@link Avatar}.

* * @namespace MyAvatar * @@ -68,7 +70,59 @@ class MyAvatar : public Avatar { * @hifi-client-entity * @hifi-avatar * + * @comment IMPORTANT: This group of properties is copied from AvatarData.h; they should NOT be edited here. + * @property {Vec3} position - The position of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @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} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. + * Yaw is sometimes called "heading". + * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is + * sometimes called "elevation". + * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is + * sometimes called "bank". + * @property {Quat} orientation - The orientation of the avatar. + * @property {Quat} headOrientation - The orientation of the avatar's head. + * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is + * sometimes called "elevation". + * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's + * head. Yaw is sometimes called "heading". + * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is + * sometimes called "bank". + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @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 {string} displayName - The avatar's display name. + * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar + * mixer rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true 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 lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
+ * Deprecated: Use avatar entities instead. + * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @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. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. + * + * @comment IMPORTANT: This group of properties is copied from Avatar.h; they should NOT be edited here. + * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the + * registration point of the 3D model. + * * @property {Vec3} qmlPosition - A synonym for position for use by QML. + * * @property {boolean} shouldRenderLocally=true - If true then your avatar is rendered for you in Interface, * otherwise it is not rendered for you (but it is still rendered for other users). * @property {Vec3} motorVelocity=Vec3.ZERO - The target velocity of your avatar to be achieved by a scripted motor. @@ -83,107 +137,165 @@ class MyAvatar : public Avatar { * by the audio mixer, so all audio effectively plays back at a 24khz. 48kHz RAW files are also supported. * @property {number} audioListenerMode=0 - Specifies the listening position when hearing spatialized audio. Must be one * of the following property values:
- * audioListenerModeHead
- * audioListenerModeCamera
- * audioListenerModeCustom + * Myavatar.audioListenerModeHead
+ * Myavatar.audioListenerModeCamera
+ * Myavatar.audioListenerModeCustom * @property {number} audioListenerModeHead=0 - The audio listening position is at the avatar's head. Read-only. * @property {number} audioListenerModeCamera=1 - The audio listening position is at the camera. Read-only. * @property {number} audioListenerModeCustom=2 - The audio listening position is at a the position specified by set by the * customListenPosition and customListenOrientation property values. Read-only. - * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true. - * @property {boolean} hasProceduralBlinkFaceMovement=true - procedural blinking will be turned on if set to true. - * @property {boolean} hasProceduralEyeFaceMovement=true - procedural eye movement will be turned on if set to true. - * @property {boolean} hasAudioEnabledFaceMovement=true - If set to true, voice audio will move the mouth Blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. * @property {Vec3} customListenPosition=Vec3.ZERO - The listening position used when the audioListenerMode * property value is audioListenerModeCustom. - * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the + * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the * audioListenerMode property value is audioListenerModeCustom. + * @property {boolean} hasScriptedBlendshapes=false - true to transmit blendshapes over the network.
+ * Note: Currently doesn't work. Use {@link MyAvatar.setForceFaceTrackerConnected} instead. + * @property {boolean} hasProceduralBlinkFaceMovement=true - true if procedural blinking is turned on. + * @property {boolean} hasProceduralEyeFaceMovement=true - true if procedural eye movement is turned on. + * @property {boolean} hasAudioEnabledFaceMovement=true - true to move the mouth blendshapes with voice audio + * when MyAvatar.hasScriptedBlendshapes is enabled. + * @property {number} rotationRecenterFilterLength - Configures how quickly the avatar root rotates to recenter its facing + * direction to match that of the user's torso based on head and hands orientation. A smaller value makes the + * recentering happen more quickly. The minimum value is 0.01. + * @property {number} rotationThreshold - The angle in radians that the user's torso facing direction (based on head and + * hands orientation) can differ from that of the avatar before the avatar's root is rotated to match the user's torso. + * @property {boolean} enableStepResetRotation - If true then after the user's avatar takes a step, the + * avatar's root immediately rotates to recenter its facing direction to match that of the user's torso based on head + * and hands orientation. + * @property {boolean} enableDrawAverageFacing - If true, debug graphics are drawn that show the average + * facing direction of the user's torso (based on head and hands orientation). This can be useful if you want to try + * out different filter lengths and thresholds. + * * @property {Vec3} leftHandPosition - The position of the left hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. * @property {Vec3} rightHandPosition - The position of the right hand in avatar coordinates if it's being positioned by * controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - - * @property {Vec3} leftHandTipPosition - The position 30cm offset from the left hand in avatar coordinates if it's being - * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} rightHandTipPosition - The position 30cm offset from the right hand in avatar coordinates if it's being - * positioned by controllers, otherwise {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Pose} leftHandPose - The pose of the left hand as determined by the hand controllers. Read-only. - * @property {Pose} rightHandPose - The pose right hand position as determined by the hand controllers. Read-only. - * @property {Pose} leftHandTipPose - The pose of the left hand as determined by the hand controllers, with the position - * by 30cm. Read-only. - * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, with the position - * by 30cm. Read-only. - * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to the center of - * gravity model that balance the center of gravity over the base of support of the feet. Setting the value false - * will result in the default behaviour where the hips are placed under the head. - * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the + * @property {Vec3} leftHandTipPosition - The position 0.3m in front of the left hand's position, in the direction along the + * palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is + * {@link Vec3(0)|Vec3.ZERO}. Read-only. + * @property {Vec3} rightHandTipPosition - The position 0.3m in front of the right hand's position, in the direction along + * the palm, in avatar coordinates. If the hand isn't being positioned by a controller, the value is + * {@link Vec3(0)|Vec3.ZERO}. Read-only. + * + * @property {Pose} leftHandPose - The left hand's pose as determined by the hand controllers, relative to the avatar. + * Read-only. + * @property {Pose} rightHandPose - The right hand's pose as determined by the hand controllers, relative to the avatar. + * Read-only. + * @property {Pose} leftHandTipPose - The left hand's pose as determined by the hand controllers, relative to the avatar, + * with the position adjusted by 0.3m along the direction of the palm. Read-only. + * @property {Pose} rightHandTipPose - The right hand's pose as determined by the hand controllers, relative to the avatar, + * with the position adjusted by 0.3m along the direction of the palm. Read-only. + * + * @property {number} energy - Deprecated: This property will be removed from the API. + * @property {boolean} isAway - true if your avatar is away (i.e., inactive), false if it is + * active. + * + * @property {boolean} centerOfGravityModelEnabled=true - true if the avatar hips are placed according to + * the center of gravity model that balances the center of gravity over the base of support of the feet. Set the + * value to false for default behavior where the hips are positioned under the head. + * @property {boolean} hmdLeanRecenterEnabled=true - true IF the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. - * @property {boolean} collisionsEnabled - Set to true to enable collisions for the avatar, false - * to disable collisions. May return true even though the value was set false because the - * zone may disallow collisionless avatars. - * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled. + * @property {boolean} collisionsEnabled - Set to true to enable the avatar to collide with the environment, + * false to disable collisions with the environment. May return true even though the value + * was set false because the zone may disallow collisionless avatars. + * @property {boolean} otherAvatarsCollisionsEnabled - Set to true to enable the avatar to collide with other + * avatars, false to disable collisions with other avatars. + * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled.
* Deprecated: Use collisionsEnabled instead. * @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings > - * Walking and teleporting. Note: Setting the value has no effect unless Interface is restarted. - * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Show room boundaries - * while teleporting. Note: Setting the value has no effect unless Interface is restarted. - * @property {number} yawSpeed=75 - * @property {number} pitchSpeed=50 + * Controls > Walking. Note: Setting the value has no effect unless Interface is restarted. + * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Controls > Show room + * boundaries while teleporting.
+ * Note: Setting the value has no effect unless Interface is restarted. + * + * @property {number} yawSpeed=75 - The mouse X sensitivity value in Settings > General. Read-only. + * @property {number} pitchSpeed=50 - The mouse Y sensitivity value in Settings > General. Read-only. + * * @property {boolean} hmdRollControlEnabled=true - If true, the roll angle of your HMD turns your avatar * while flying. * @property {number} hmdRollControlDeadZone=8 - The amount of HMD roll, in degrees, required before your avatar turns if * hmdRollControlEnabled is enabled. - * @property {number} hmdRollControlRate If hmdRollControlEnabled is true, this value determines the maximum turn rate of - * your avatar when rolling your HMD in degrees per second. + * @property {number} hmdRollControlRate If MyAvatar.hmdRollControlEnabled is true, this value determines the + * maximum turn rate of your avatar when rolling your HMD in degrees per second. + * * @property {number} userHeight=1.75 - The height of the user in sensor space. * @property {number} userEyeHeight=1.65 - The estimated height of the user's eyes in sensor space. Read-only. + * * @property {Uuid} SELF_ID - UUID representing "my avatar". Only use for local-only entities in situations * where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). Note: Likely to be deprecated. * Read-only. - * @property {number} walkSpeed - * @property {number} walkBackwardSpeed - * @property {number} sprintSpeed - * @property {number} isInSittingState - * @property {number} userRecenterModel * - * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the - * registration point of the 3D model. + * @property {number} walkSpeed - The walk speed of your avatar. + * @property {number} walkBackwardSpeed - The walk backward speed of your avatar. + * @property {number} sprintSpeed - The sprint speed of your avatar. + * @property {MyAvatar.SitStandModelType} userRecenterModel - Controls avatar leaning and recentering behavior. + * @property {number} isInSittingState - true if your avatar is sitting (avatar leaning is disabled, + * recenntering is enabled), false if it is standing (avatar leaning is enabled, and avatar recenters if it + * leans too far). If userRecenterModel == 2 (i.e., auto) the property value automatically updates as the + * user sits or stands, unless isSitStandStateLocked == true. Setting the property value overrides the + * current siting / standing state, which is updated when the user next sits or stands unless + * isSitStandStateLocked == true. + * @property {boolean} isSitStandStateLocked - true to lock the avatar sitting/standing state, i.e., use this + * to disable automatically changing state. + * @property {boolean} allowTeleporting - true if teleporting is enabled in the Interface settings, + * false if it isn't. Read-only. * - * @property {Vec3} position - * @property {number} scale - Returns the clamped scale of the avatar. - * @property {number} density Read-only. - * @property {Vec3} handPosition - * @property {number} bodyYaw - The rotation left or right about an axis running from the head to the feet of the avatar. - * Yaw is sometimes called "heading". - * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is - * sometimes called "elevation". - * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is - * sometimes called "bank". - * @property {Quat} orientation - * @property {Quat} headOrientation - The orientation of the avatar's head. - * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is - * sometimes called "elevation". - * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's - * head. Yaw is sometimes called "heading". - * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is - * sometimes called "bank". - * @property {Vec3} velocity - * @property {Vec3} angularVelocity - * @property {number} audioLoudness - * @property {number} audioAverageLoudness - * @property {string} displayName - * @property {string} sessionDisplayName - Sanitized, defaulted version displayName that is defined by the AvatarMixer - * rather than by Interface clients. The result is unique among all avatars present at the time. - * @property {boolean} lookAtSnappingEnabled - * @property {string} skeletonModelURL - * @property {AttachmentData[]} attachmentData - * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. - * @property {Uuid} sessionUUID Read-only. - * @property {Mat4} sensorToWorldMatrix Read-only. - * @property {Mat4} controllerLeftHandMatrix Read-only. - * @property {Mat4} controllerRightHandMatrix Read-only. - * @property {number} sensorToWorldScale Read-only. + * @borrows Avatar.getDomainMinScale as getDomainMinScale + * @borrows Avatar.getDomainMaxScale as getDomainMaxScale + * @borrows Avatar.getEyeHeight as getEyeHeight + * @borrows Avatar.getHeight as getHeight + * @borrows Avatar.setHandState as setHandState + * @borrows Avatar.getHandState as getHandState + * @borrows Avatar.setRawJointData as setRawJointData + * @borrows Avatar.setJointData as setJointData + * @borrows Avatar.setJointRotation as setJointRotation + * @borrows Avatar.setJointTranslation as setJointTranslation + * @borrows Avatar.clearJointData as clearJointData + * @borrows Avatar.isJointDataValid as isJointDataValid + * @borrows Avatar.getJointRotation as getJointRotation + * @borrows Avatar.getJointTranslation as getJointTranslation + * @borrows Avatar.getJointRotations as getJointRotations + * @borrows Avatar.getJointTranslations as getJointTranslations + * @borrows Avatar.setJointRotations as setJointRotations + * @borrows Avatar.setJointTranslations as setJointTranslations + * @borrows Avatar.clearJointsData as clearJointsData + * @borrows Avatar.getJointIndex as getJointIndex + * @borrows Avatar.getJointNames as getJointNames + * @borrows Avatar.setBlendshape as setBlendshape + * @borrows Avatar.getAttachmentsVariant as getAttachmentsVariant + * @borrows Avatar.setAttachmentsVariant as setAttachmentsVariant + * @borrows Avatar.updateAvatarEntity as updateAvatarEntity + * @borrows Avatar.clearAvatarEntity as clearAvatarEntity + * @borrows Avatar.setForceFaceTrackerConnected as setForceFaceTrackerConnected + * @borrows Avatar.getAttachmentData as getAttachmentData + * @borrows Avatar.setAttachmentData as setAttachmentData + * @borrows Avatar.attach as attach + * @borrows Avatar.detachOne as detachOne + * @borrows Avatar.detachAll as detachAll + * @comment Avatar.getAvatarEntityData as getAvatarEntityData - Don't borrow because implementation is different. + * @comment Avatar.setAvatarEntityData as setAvatarEntityData - Don't borrow because implementation is different. + * @borrows Avatar.getSensorToWorldMatrix as getSensorToWorldMatrix + * @borrows Avatar.getSensorToWorldScale as getSensorToWorldScale + * @borrows Avatar.getControllerLeftHandMatrix as getControllerLeftHandMatrix + * @borrows Avatar.getControllerRightHandMatrix as getControllerRightHandMatrix + * @borrows Avatar.getDataRate as getDataRate + * @borrows Avatar.getUpdateRate as getUpdateRate + * @borrows Avatar.displayNameChanged as displayNameChanged + * @borrows Avatar.sessionDisplayNameChanged as sessionDisplayNameChanged + * @borrows Avatar.skeletonModelURLChanged as skeletonModelURLChanged + * @borrows Avatar.lookAtSnappingChanged as lookAtSnappingChanged + * @borrows Avatar.sessionUUIDChanged as sessionUUIDChanged + * @borrows Avatar.sendAvatarDataPacket as sendAvatarDataPacket + * @borrows Avatar.sendIdentityPacket as sendIdentityPacket + * @borrows Avatar.setSessionUUID as setSessionUUID + * @comment Avatar.getAbsoluteJointRotationInObjectFrame as getAbsoluteJointRotationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.getAbsoluteJointTranslationInObjectFrame as getAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.setAbsoluteJointRotationInObjectFrame as setAbsoluteJointRotationInObjectFrame - Don't borrow because implementation is different. + * @comment Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different. + * @borrows Avatar.getTargetScale as getTargetScale + * @borrows Avatar.resetLastSent as resetLastSent + * @borrows Avatar.hasPriority as hasPriority */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) @@ -196,11 +308,11 @@ class MyAvatar : public Avatar { Q_PROPERTY(QString motorMode READ getScriptedMotorMode WRITE setScriptedMotorMode) Q_PROPERTY(QString collisionSoundURL READ getCollisionSoundURL WRITE setCollisionSoundURL) Q_PROPERTY(AudioListenerMode audioListenerMode READ getAudioListenerMode WRITE setAudioListenerMode) - Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) - Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(AudioListenerMode audioListenerModeHead READ getAudioListenerModeHead) Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera) Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) + Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) + Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(bool hasScriptedBlendshapes READ getHasScriptedBlendshapes WRITE setHasScriptedBlendshapes) Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement) Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement) @@ -260,6 +372,40 @@ class MyAvatar : public Avatar { using TimePoint = Clock::time_point; public: + + /**jsdoc + *

Logical keys that drive your avatar and camera.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ValueNameDescription
0TRANSLATE_XMove the user's avatar in the direction of its x-axis, if the + * camera isn't in independent or mirror modes.
1TRANSLATE_YMove the user's avatar in the direction of its y-axis, if the + * camera isn't in independent or mirror modes.
2TRANSLATE_ZMove the user's avatar in the direction of its z-axis, if the + * camera isn't in independent or mirror modes
3YAWRotate the user's avatar about its y-axis at a rate proportional to the + * control value, if the camera isn't in independent or mirror modes.
4STEP_TRANSLATE_XNo action.
5STEP_TRANSLATE_YNo action.
6STEP_TRANSLATE_ZNo action.
7STEP_YAWRotate the user's avatar about its y-axis in a step increment, if + * the camera isn't in independent or mirror modes.
8PITCHRotate the user's avatar head and attached camera about its negative + * x-axis (i.e., positive values pitch down) at a rate proportional to the control value, if the camera isn't in HMD, + * independent, or mirror modes.
9ZOOMZoom the camera in or out.
10DELTA_YAWRotate the user's avatar about its y-axis by an amount proportional + * to the control value, if the camera isn't in independent or mirror modes.
11DELTA_PITCHRotate the user's avatar head and attached camera about its + * negative x-axis (i.e., positive values pitch down) by an amount proportional to the control value, if the camera + * isn't in HMD, independent, or mirror modes.
+ * @typedef {number} MyAvatar.DriveKeys + */ enum DriveKeys { TRANSLATE_X = 0, TRANSLATE_Y, @@ -277,6 +423,28 @@ public: }; Q_ENUM(DriveKeys) + /**jsdoc + *

Specifies different avatar leaning and recentering behaviors.

+ * + * + * + * + * + * + * + * + * + * + *
ValueNameDescription
0ForceSitAssumes the user is seated in the real world. Disables avatar + * leaning regardless of what the avatar is doing in the virtual world (i.e., avatar always recenters).
1ForceStandAssumes the user is standing in the real world. Enables avatar + * leaning regardless of what the avatar is doing in the virtual world (i.e. avatar leans, then if leans too far it + * recenters).
2AutoInterface detects when the user is standing or seated in the real world. + * Avatar leaning is disabled when the user is sitting (i.e., avatar always recenters), and avatar leaning is enabled + * when the user is standing (i.e., avatar leans, then if leans too far it recenters).
3DisableHMDLeanBoth avatar leaning and recentering are disabled regardless of + * what the user is doing in the real world and no matter what their avatar is doing in the virtual world. Enables + * the avatar to sit on the floor when the user sits on the floor.
Note: Experimental.
+ * @typedef {number} MyAvatar.SitStandModelType + */ enum SitStandModelType { ForceSit = 0, ForceStand, @@ -303,20 +471,23 @@ public: void setCollisionWithOtherAvatarsFlags() override; /**jsdoc + * Resets the sensor positioning of your HMD (if in use) and recenters your avatar body and head. * @function MyAvatar.resetSensorsAndBody */ Q_INVOKABLE void resetSensorsAndBody(); /**jsdoc - * Moves and orients the avatar, such that it is directly underneath the HMD, with toes pointed forward. + * Moves and orients the avatar, such that it is directly underneath the HMD, with toes pointed forward in the direction of + * the HMD. * @function MyAvatar.centerBody */ Q_INVOKABLE void centerBody(); // thread-safe /**jsdoc - * The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to forget this history, to prevent - * contorted joints. + * Clears inverse kinematics joint limit history. + *

The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to + * forget this history to prevent contorted joints, e.g., after finishing with an override animation.

* @function MyAvatar.clearIKJointLimitHistory */ Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe @@ -330,14 +501,16 @@ public: const glm::quat& getHMDSensorOrientation() const { return _hmdSensorOrientation; } /**jsdoc + * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.setOrientationVar - * @param {object} newOrientationVar + * @param {object} newOrientationVar - The avatar's orientation. */ Q_INVOKABLE void setOrientationVar(const QVariant& newOrientationVar); /**jsdoc + * Gets the avatar orientation. Suitable for use in QML. * @function MyAvatar.getOrientationVar - * @returns {object} + * @returns {object} The avatar's orientation. */ Q_INVOKABLE QVariant getOrientationVar() const; @@ -363,7 +536,7 @@ public: void setRealWorldFieldOfView(float realWorldFov) { _realWorldFieldOfView.set(realWorldFov); } /**jsdoc - * Get the position in world coordinates of the point directly between your avatar's eyes assuming your avatar was in its + * Gets the position in world coordinates of the point directly between your avatar's eyes assuming your avatar was in its * default pose. This is a reference position; it does not change as your avatar's head moves relative to the avatar * position. * @function MyAvatar.getDefaultEyePosition @@ -377,17 +550,18 @@ public: float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } /**jsdoc - * The avatar animation system includes a set of default animations along with rules for how those animations are blended - * together with procedural data (such as look at vectors, hand sensors etc.). overrideAnimation() is used to completely - * override all motion from the default animation system (including inverse kinematics for hand and head controllers) and - * play a set of specified animations. To end these animations and restore the default animations, use - * {@link MyAvatar.restoreAnimation}.
+ * Overrides the default avatar animations. + *

The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). overrideAnimation() is used to + * completely override all motion from the default animation system (including inverse kinematics for hand and head + * controllers) and play a set of specified animations. To end these animations and restore the default animations, use + * {@link MyAvatar.restoreAnimation}.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards.

+ * Avatar Standards.

* @function MyAvatar.overrideAnimation - * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the + * @param url {string} The URL to the animation file. Animation files need to be FBX format, but only need to contain the * avatar skeleton and animation data. * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param loop {boolean} Set to true if the animation should loop. @@ -399,15 +573,18 @@ public: * MyAvatar.overrideAnimation(ANIM_URL, 30, true, 0, 53); * Script.setTimeout(function () { * MyAvatar.restoreAnimation(); + * MyAvatar.clearIKJointLimitHistory(); * }, 3000); */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc - * The avatar animation system includes a set of default animations along with rules for how those animations are blended together with - * procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will override the default animations. - * restoreAnimation() is used to restore all motion from the default animation system including inverse kinematics for hand and head - * controllers. If you aren't currently playing an override animation, this function will have no effect. + * Restores the default animations. + *

The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will + * override the default animations. restoreAnimation() is used to restore all motion from the default + * animation system including inverse kinematics for hand and head controllers. If you aren't currently playing an override + * animation, this function has no effect.

* @function MyAvatar.restoreAnimation * @example Play a clapping animation on your avatar for three seconds. * // Clap your hands for 3 seconds then restore animation back to the avatar. @@ -420,10 +597,12 @@ public: Q_INVOKABLE void restoreAnimation(); /**jsdoc - * Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together with procedural data - * (such as look at vectors, hand sensors etc.). Each animation specified in the avatar-animation.json file is known as an animation role. - * Animation roles map to easily understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd." - * getAnimationRoles() is used get the list of animation roles defined in the avatar-animation.json. + * Gets the current animation roles. + *

Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together + * with procedural data (such as look at vectors, hand sensors etc.). Each animation specified in the avatar-animation.json + * file is known as an animation role. Animation roles map to easily understandable actions that the avatar can perform, + * such as "idleStand", "idleTalk", or "walkFwd". getAnimationRoles() + * is used get the list of animation roles defined in the avatar-animation.json.

* @function MyAvatar.getAnimationRoles * @returns {string[]} Array of role strings. * @example Print the list of animation roles defined in the avatar's avatar-animation.json file to the debug log. @@ -436,20 +615,22 @@ public: Q_INVOKABLE QStringList getAnimationRoles(); /**jsdoc - * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions - * that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). - * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended - * together with procedural data (such as look at vectors, hand sensors etc.). - * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation role. To end - * the animations and restore the default animations, use {@link MyAvatar.restoreRoleAnimation}.
+ * Overrides a specific animation role. + *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily + * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or + * "walkFwd". To get the full list of roles, use {@ link MyAvatar.getAnimationRoles}. + * For each role, the avatar-animation.json defines when the animation is used, the animation clip (FBX) used, and how + * animations are blended together with procedural data (such as look at vectors, hand sensors etc.). + * overrideRoleAnimation() is used to change the animation clip (FBX) associated with a specified animation + * role. To end the role animation and restore the default, use {@link MyAvatar.restoreRoleAnimation}.

*

Note: Hand roles only affect the hand. Other 'main' roles, like 'idleStand', 'idleTalk', 'takeoffStand' are full body.

*

Note: When using pre-built animation data, it's critical that the joint orientation of the source animation and target * rig are equivalent, since the animation data applies absolute values onto the joints. If the orientations are different, * the avatar will move in unpredictable ways. For more information about avatar joint orientation standards, see - * Avatar Standards. + * Avatar Standards. * @function MyAvatar.overrideRoleAnimation * @param role {string} The animation role to override - * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the avatar skeleton and animation data. + * @param url {string} The URL to the animation file. Animation files need to be in FBX format, but only need to contain the avatar skeleton and animation data. * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. * @param loop {boolean} Set to true if the animation should loop * @param firstFrame {number} The frame the animation should start at @@ -470,13 +651,15 @@ public: Q_INVOKABLE void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc - * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions that - * the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). For each role, - * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended together with - * procedural data (such as look at vectors, hand sensors etc.). You can change the animation clip (.FBX) associated with a specified animation - * role using overrideRoleAnimation(). - * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have not specified an override animation - * for the specified role, this function will have no effect. + * Restores a default role animation. + *

Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily + * understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or + * "walkFwd". To get the full list of roles, use {@link MyAvatar.getAnimationRoles}. For each role, + * the avatar-animation.json defines when the animation is used, the animation clip (FBX) used, and how animations are + * blended together with procedural data (such as look-at vectors, hand sensors etc.). You can change the animation clip + * (FBX) associated with a specified animation role using {@link MyAvatar.overrideRoleAnimation}. + * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have + * not specified an override animation for the specified role, this function has no effect. * @function MyAvatar.restoreRoleAnimation * @param role {string} The animation role clip to restore. */ @@ -491,90 +674,160 @@ public: // adding one of the other handlers. While any handler may change a value in animStateDictionaryIn (or supply different values in animStateDictionaryOut) // a handler must not remove properties from animStateDictionaryIn, nor change property values that it does not intend to change. // It is not specified in what order multiple handlers are called. + /**jsdoc + * Adds an animation state handler function that is invoked just before each animation graph update. More than one + * animation state handler function may be added by calling addAnimationStateHandler multiple times. It is not + * specified in what order multiple handlers are called. + *

The animation state handler function is called with an {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} + * "animStateDictionaryIn" parameter and is expected to return an + * {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} "animStateDictionaryOut" object. The + * animStateDictionaryOut object can be the same object as animStateDictionaryIn, or it can be a + * different object. The animStateDictionaryIn may be shared among multiple handlers and thus may contain + * additional properties specified when adding the different handlers.

+ *

A handler may change a value from animStateDictionaryIn or add different values in the + * animStateDictionaryOut returned. Any property values set in animStateDictionaryOut will + * override those of the internal animation machinery.|null} propertiesList - The list of {@link MyAvatar.AnimStateDictionary|AnimStateDictionary} + * properties that should be included in the parameter that the handler function is called with. If null + * then all properties are included in the call parameter. + * @returns {number} The ID of the animation state handler function if successfully added, undefined if not. + * @example Log all the animation state dictionary parameters for a short while. + * function animStateHandler(dictionary) { + * print("Anim state dictionary: " + JSON.stringify(dictionary)); + * } + * + * var handler = MyAvatar.addAnimationStateHandler(animStateHandler, null); + * + * Script.setTimeout(function () { + * MyAvatar.removeAnimationStateHandler(handler); + * }, 100); + */ Q_INVOKABLE QScriptValue addAnimationStateHandler(QScriptValue handler, QScriptValue propertiesList) { return _skeletonModel->getRig().addAnimationStateHandler(handler, propertiesList); } /**jsdoc + * Removes an animation state handler function. * @function MyAvatar.removeAnimationStateHandler - * @param {number} handler + * @param {number} handler - The ID of the animation state handler function to remove. */ // Removes a handler previously added by addAnimationStateHandler. Q_INVOKABLE void removeAnimationStateHandler(QScriptValue handler) { _skeletonModel->getRig().removeAnimationStateHandler(handler); } /**jsdoc + * Gets whether you do snap turns in HMD mode. * @function MyAvatar.getSnapTurn - * @returns {boolean} + * @returns {boolean} true if you do snap turns in HMD mode; false if you do smooth turns in HMD + * mode. */ Q_INVOKABLE bool getSnapTurn() const { return _useSnapTurn; } /**jsdoc + * Sets whether your should do snap turns or smooth turns in HMD mode. * @function MyAvatar.setSnapTurn - * @param {boolean} on + * @param {boolean} on - true to do snap turns in HMD mode; false to do smooth turns in HMD mode. */ Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } /**jsdoc + * Sets the avatar's dominant hand. * @function MyAvatar.setDominantHand - * @param {string} hand + * @param {string} hand - The dominant hand: "left" for the left hand or "right" for the right + * hand. Any other value has no effect. */ Q_INVOKABLE void setDominantHand(const QString& hand); + /**jsdoc + * Gets the avatar's dominant hand. * @function MyAvatar.getDominantHand - * @returns {string} + * @returns {string} "left" for the left hand, "right" for the right hand. */ Q_INVOKABLE QString getDominantHand() const; /**jsdoc * @function MyAvatar.setHmdAvatarAlignmentType - * @param {string} hand + * @param {string} type - "head" to align your head and your avatar's head, "eyes" to align your + * eyes and your avatar's eyes. + * */ - Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); + Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& type); + /**jsdoc - * @function MyAvatar.setHmdAvatarAlignmentType - * @returns {string} + * Gets the HMD alignment for your avatar. + * @function MyAvatar.getHmdAvatarAlignmentType + * @returns {string} "head" if aligning your head and your avatar's head, "eyes" if aligning your + * eyes and your avatar's eyes. */ Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc - * @function MyAvatar.setCenterOfGravityModelEnabled - * @param {boolean} enabled - */ + * Sets whether the avatar's hips are balanced over the feet or positioned under the head. + * @function MyAvatar.setCenterOfGravityModelEnabled + * @param {boolean} enabled - true to balance the hips over the feet, false to position the hips + * under the head. + */ Q_INVOKABLE void setCenterOfGravityModelEnabled(bool value) { _centerOfGravityModelEnabled = value; } + /**jsdoc - * @function MyAvatar.getCenterOfGravityModelEnabled - * @returns {boolean} - */ + * Gets whether the avatar hips are being balanced over the feet or placed under the head. + * @function MyAvatar.getCenterOfGravityModelEnabled + * @returns {boolean} true if the hips are being balanced over the feet, false if the hips are + * being positioned under the head. + */ Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } + /**jsdoc + * Sets whether the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * causes your avatar to follow your HMD as you walk around the room. Disabling recentering is useful if you want to pin + * the avatar to a fixed position. * @function MyAvatar.setHMDLeanRecenterEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to recenter the avatar under the head as it moves, false to + * disable recentering. */ Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } + /**jsdoc + * Gets whether the avatar's position updates to recenter the avatar under the head. In room-scale VR, recentering + * causes your avatar to follow your HMD as you walk around the room. * @function MyAvatar.getHMDLeanRecenterEnabled - * @returns {boolean} + * @returns {boolean} true if recentering is enabled, false if not. */ Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + /**jsdoc - * Request to enable hand touch effect globally + * Requests that the hand touch effect is disabled for your avatar. Any resulting change in the status of the hand touch + * effect will be signaled by {@link MyAvatar.shouldDisableHandTouchChanged}. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.requestEnableHandTouch */ Q_INVOKABLE void requestEnableHandTouch(); + /**jsdoc - * Request to disable hand touch effect globally + * Requests that the hand touch effect is enabled for your avatar. Any resulting change in the status of the hand touch + * effect will be signaled by {@link MyAvatar.shouldDisableHandTouchChanged}. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.requestDisableHandTouch */ Q_INVOKABLE void requestDisableHandTouch(); + /**jsdoc - * Disables hand touch effect on a specific entity + * Disables the hand touch effect on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.disableHandTouchForID - * @param {Uuid} entityID - ID of the entity that will disable hand touch effect + * @param {Uuid} entityID - The entity that the hand touch effect will be disabled for. */ Q_INVOKABLE void disableHandTouchForID(const QUuid& entityID); + /**jsdoc - * Enables hand touch effect on a specific entity + * Enables the hand touch effect on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.enableHandTouchForID - * @param {Uuid} entityID - ID of the entity that will enable hand touch effect + * @param {Uuid} entityID - The entity that the hand touch effect will be enabled for. */ Q_INVOKABLE void enableHandTouchForID(const QUuid& entityID); @@ -612,29 +865,43 @@ public: float getDriveKey(DriveKeys key) const; /**jsdoc + * Gets the value of a drive key, regardless of whether it is disabled. * @function MyAvatar.getRawDriveKey - * @param {DriveKeys} key - * @returns {number} + * @param {MyAvatar.DriveKeys} key - The drive key. + * @returns {number} The value of the drive key. */ Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; void relayDriveKeysToCharacterController(); /**jsdoc + * Disables the action associated with a drive key. * @function MyAvatar.disableDriveKey - * @param {DriveKeys} key + * @param {MyAvatar.DriveKeys} key - The drive key to disable. + * @example Disable rotating your avatar using the keyboard for a couple of seconds. + * var YAW = 3; + * print("Disable"); + * MyAvatar.disableDriveKey(YAW); + * Script.setTimeout(function () { + * print("Enable"); + * MyAvatar.enableDriveKey(YAW); + * }, 5000); */ Q_INVOKABLE void disableDriveKey(DriveKeys key); /**jsdoc + * Enables the action associated with a drive key. The action may have been disabled with + * {@link MyAvatar.disableDriveKey|disableDriveKey}. * @function MyAvatar.enableDriveKey - * @param {DriveKeys} key + * @param {MyAvatar.DriveKeys} key - The drive key to enable. */ Q_INVOKABLE void enableDriveKey(DriveKeys key); + /**jsdoc + * Checks whether a drive key is disabled. * @function MyAvatar.isDriveKeyDisabled - * @param {DriveKeys} key - * @returns {boolean} + * @param {DriveKeys} key - The drive key to check. + * @returns {boolean} true if the drive key is disabled, false if it isn't. */ Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; @@ -649,7 +916,7 @@ public: /**jsdoc * Recenter the avatar in the horizontal direction, if {@link MyAvatar|MyAvatar.hmdLeanRecenterEnabled} is * false. - * @ function MyAvatar.triggerHorizontalRecenter + * @function MyAvatar.triggerHorizontalRecenter */ Q_INVOKABLE void triggerHorizontalRecenter(); @@ -660,10 +927,10 @@ public: Q_INVOKABLE void triggerRotationRecenter(); /**jsdoc - *The isRecenteringHorizontally function returns true if MyAvatar - *is translating the root of the Avatar to keep the center of gravity under the head. - *isActive(Horizontal) is returned. - *@function MyAvatar.isRecenteringHorizontally + * Gets whether the avatar is configured to keep its center of gravity under its head. + * @function MyAvatar.isRecenteringHorizontally + * @returns {boolean} true if the avatar is keeping its center of gravity under its head position, + * false if not. */ Q_INVOKABLE bool isRecenteringHorizontally() const; @@ -672,7 +939,7 @@ public: const MyHead* getMyHead() const; /**jsdoc - * Get the current position of the avatar's "Head" joint. + * Gets the current position of the avatar's "Head" joint. * @function MyAvatar.getHeadPosition * @returns {Vec3} The current position of the avatar's "Head" joint. * @example Report the current position of your avatar's head. @@ -681,31 +948,37 @@ public: Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } /**jsdoc + * Gets the yaw of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalYaw - * @returns {number} + * @returns {number} The yaw of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalYaw() const { return getHead()->getFinalYaw(); } /**jsdoc + * Gets the roll of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalRoll - * @returns {number} + * @returns {number} The roll of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } /**jsdoc + * Gets the pitch of the avatar's head relative to its body. * @function MyAvatar.getHeadFinalPitch - * @returns {number} + * @returns {number} The pitch of the avatar's head, in degrees. */ Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } /**jsdoc + * If a face tracker is connected and being used, gets the estimated pitch of the user's head scaled. This is scale such + * that the avatar looks at the edge of the view frustum when the user looks at the edge of their screen. * @function MyAvatar.getHeadDeltaPitch - * @returns {number} + * @returns {number} The pitch that the avatar's head should be if a face tracker is connected and being used, otherwise + * 0, in degrees. */ Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } /**jsdoc - * Get the current position of the point directly between the avatar's eyes. + * Gets the current position of the point directly between the avatar's eyes. * @function MyAvatar.getEyePosition * @returns {Vec3} The current position of the point directly between the avatar's eyes. * @example Report your avatar's current eye position. @@ -715,22 +988,29 @@ public: Q_INVOKABLE glm::vec3 getEyePosition() const { return getHead()->getEyePosition(); } /**jsdoc + * Gets the position of the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatarPosition - * @returns {Vec3} The position of the avatar you're currently looking at. + * @returns {Vec3} The position of the avatar beeing looked at. * @example Report the position of the avatar you're currently looking at. * print(JSON.stringify(MyAvatar.getTargetAvatarPosition())); */ + // FIXME: If not looking at an avatar, the most recently looked-at position is returned. This should be fixed to return + // undefined or {NaN, NaN, NaN} or similar. Q_INVOKABLE glm::vec3 getTargetAvatarPosition() const { return _targetAvatarPosition; } /**jsdoc + * Gets information on the avatar your avatar is currently looking at. * @function MyAvatar.getTargetAvatar - * @returns {AvatarData} + * @returns {AvatarData} Information on the avatar being looked at. */ + // FIXME: The return type doesn't have a conversion to a script value so the function always returns undefined in + // JavaScript. Note: When fixed, JSDoc is needed for the return type. Q_INVOKABLE ScriptAvatarData* getTargetAvatar() const; /**jsdoc - * Get the position of the avatar's left hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's left hand, relative to the avatar, as positioned by a hand controller (e.g., Oculus + * Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getLeftHandPosition @@ -742,7 +1022,8 @@ public: Q_INVOKABLE glm::vec3 getLeftHandPosition() const; /**jsdoc - * Get the position of the avatar's right hand as positioned by a hand controller (e.g., Oculus Touch or Vive).
+ * Gets the position of the avatar's right hand, relative to the avatar, as positioned by a hand controller (e.g., Oculus + * Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.)

* @function MyAvatar.getRightHandPosition @@ -754,53 +1035,71 @@ public: Q_INVOKABLE glm::vec3 getRightHandPosition() const; /**jsdoc + * Gets the position 0.3m in front of the left hand's position in the direction along the palm, in avatar coordinates, as + * positioned by a hand controller. * @function MyAvatar.getLeftHandTipPosition - * @returns {Vec3} + * @returns {Vec3} The position 0.3m in front of the left hand's position in the direction along the palm, in avatar + * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ Q_INVOKABLE glm::vec3 getLeftHandTipPosition() const; /**jsdoc + * Gets the position 0.3m in front of the right hand's position in the direction along the palm, in avatar coordinates, as + * positioned by a hand controller. * @function MyAvatar.getRightHandTipPosition - * @returns {Vec3} + * @returns {Vec3} The position 0.3m in front of the right hand's position in the direction along the palm, in avatar + * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ Q_INVOKABLE glm::vec3 getRightHandTipPosition() const; /**jsdoc - * Get the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a - * hand controller (e.g., Oculus Touch or Vive).
+ * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a + * hand controller (e.g., Oculus Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller. * @example Report the pose of your avatar's left hand. * print(JSON.stringify(MyAvatar.getLeftHandPose())); */ Q_INVOKABLE controller::Pose getLeftHandPose() const; /**jsdoc - * Get the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a - * hand controller (e.g., Oculus Touch or Vive).
+ * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand as positioned by a + * hand controller (e.g., Oculus Touch or Vive). *

Note: The Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints * for hand animation.) If you are using the Leap Motion, the return value's valid property will be * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller. * @example Report the pose of your avatar's right hand. * print(JSON.stringify(MyAvatar.getRightHandPose())); */ Q_INVOKABLE controller::Pose getRightHandPose() const; /**jsdoc + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's left hand, relative to the avatar, as + * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. + *

Note: Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using Leap Motion, the return value's valid property will be + * false and any pose values returned will not be meaningful.

* @function MyAvatar.getLeftHandTipPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's left hand, relative to the avatar, as positioned by a hand controller, and + * translated 0.3m along the palm. */ Q_INVOKABLE controller::Pose getLeftHandTipPose() const; /**jsdoc + * Gets the pose (position, rotation, velocity, and angular velocity) of the avatar's right hand, relative to the avatar, as + * positioned by a hand controller (e.g., Oculus Touch or Vive), and translated 0.3m along the palm. + *

Note: Leap Motion isn't part of the hand controller input system. (Instead, it manipulates the avatar's joints + * for hand animation.) If you are using Leap Motion, the return value's valid property will be + * false and any pose values returned will not be meaningful.

* @function MyAvatar.getRightHandTipPose - * @returns {Pose} + * @returns {Pose} The pose of the avatar's right hand, relative to the avatar, as positioned by a hand controller, and + * translated 0.3m along the palm. */ Q_INVOKABLE controller::Pose getRightHandTipPose() const; @@ -823,38 +1122,44 @@ public: virtual void clearJointsData() override; /**jsdoc + * Sets and locks a joint's position and orientation. + *

Note: Only works on the hips joint.

* @function MyAvatar.pinJoint - * @param {number} index - * @param {Vec3} position - * @param {Quat} orientation - * @returns {boolean} + * @param {number} index - The index of the joint. + * @param {Vec3} position - The position of the joint in world coordinates. + * @param {Quat} orientation - The orientation of the joint in world coordinates. + * @returns {boolean} true if the joint was pinned, false if it wasn't. */ Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation); bool isJointPinned(int index); /**jsdoc + * Clears a lock on a joint's position and orientation, as set by {@link MyAvatar.pinJoint|pinJoint}. + *

Note: Only works on the hips joint.

* @function MyAvatar.clearPinOnJoint - * @param {number} index - * @returns {boolean} + * @param {number} index - The index of the joint. + * @returns {boolean} true if the joint was unpinned, false if it wasn't. */ Q_INVOKABLE bool clearPinOnJoint(int index); /**jsdoc + * Gets the maximum error distance from the most recent inverse kinematics (IK) solution. * @function MyAvatar.getIKErrorOnLastSolve - * @returns {number} + * @returns {number} The maximum IK error distance. */ Q_INVOKABLE float getIKErrorOnLastSolve() const; /**jsdoc + * Changes the user's avatar and associated descriptive name. * @function MyAvatar.useFullAvatarURL - * @param {string} fullAvatarURL - * @param {string} [modelName=""] + * @param {string} fullAvatarURL - The URL of the avatar's .fst file. + * @param {string} [modelName=""] - Descriptive name of the avatar. */ Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); /**jsdoc - * Get the complete URL for the current avatar. + * Gets the complete URL for the current avatar. * @function MyAvatar.getFullAvatarURLFromPreferences * @returns {string} The full avatar model name. * @example Report the URL for the current avatar. @@ -863,7 +1168,7 @@ public: Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } /**jsdoc - * Get the full avatar model name for the current avatar. + * Gets the full avatar model name for the current avatar. * @function MyAvatar.getFullAvatarModelName * @returns {string} The full avatar model name. * @example Report the current full avatar model name. @@ -934,24 +1239,24 @@ public: bool hasDriveInput() const; /**jsdoc - * Function returns list of avatar entities - * @function MyAvatar.getAvatarEntitiesVariant() - * @returns {object[]} - */ + * Gets the list of avatar entities and their properties. + * @function MyAvatar.getAvatarEntitiesVariant + * @returns {MyAvatar.AvatarEntityData[]} The list of avatar entities and their properties. + */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); + void removeWornAvatarEntity(const EntityItemID& entityID); void clearWornAvatarEntities(); /**jsdoc - * Check whether your avatar is flying or not. + * Checks whether your avatar is flying. * @function MyAvatar.isFlying - * @returns {boolean} true if your avatar is flying and not taking off or falling, otherwise - * false. + * @returns {boolean} true if your avatar is flying and not taking off or falling, false if not. */ Q_INVOKABLE bool isFlying(); /**jsdoc - * Check whether your avatar is in the air or not. + * Checks whether your avatar is in the air. * @function MyAvatar.isInAir * @returns {boolean} true if your avatar is taking off, flying, or falling, otherwise false * because your avatar is on the ground. @@ -959,7 +1264,7 @@ public: Q_INVOKABLE bool isInAir(); /**jsdoc - * Set your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends + * Sets your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends * on whether the domain you're in allows you to fly. * @function MyAvatar.setFlyingEnabled * @param {boolean} enabled - Set true if you want to enable flying in your current desktop or HMD display @@ -968,7 +1273,7 @@ public: Q_INVOKABLE void setFlyingEnabled(bool enabled); /**jsdoc - * Get your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends + * Gets your preference for flying in your current desktop or HMD display mode. Note that your ability to fly also depends * on whether the domain you're in allows you to fly. * @function MyAvatar.getFlyingEnabled * @returns {boolean} true if your preference is to enable flying in your current desktop or HMD display mode, @@ -977,7 +1282,7 @@ public: Q_INVOKABLE bool getFlyingEnabled(); /**jsdoc - * Set your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain + * Sets your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.setFlyingDesktopPref * @param {boolean} enabled - Set true if you want to enable flying in desktop display mode, otherwise set @@ -986,7 +1291,7 @@ public: Q_INVOKABLE void setFlyingDesktopPref(bool enabled); /**jsdoc - * Get your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain + * Gets your preference for flying in desktop display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.getFlyingDesktopPref * @returns {boolean} true if your preference is to enable flying in desktop display mode, otherwise @@ -995,7 +1300,7 @@ public: Q_INVOKABLE bool getFlyingDesktopPref(); /**jsdoc - * Set your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain + * Sets your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.setFlyingHMDPref * @param {boolean} enabled - Set true if you want to enable flying in HMD display mode, otherwise set @@ -1004,7 +1309,7 @@ public: Q_INVOKABLE void setFlyingHMDPref(bool enabled); /**jsdoc - * Get your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain + * Gets your preference for flying in HMD display mode. Note that your ability to fly also depends on whether the domain * you're in allows you to fly. * @function MyAvatar.getFlyingHMDPref * @returns {boolean} true if your preference is to enable flying in HMD display mode, otherwise @@ -1012,65 +1317,104 @@ public: */ Q_INVOKABLE bool getFlyingHMDPref(); - /**jsdoc + * Gets the target scale of the avatar. The target scale is the desired scale of the avatar without any restrictions on + * permissible scale values imposed by the domain. * @function MyAvatar.getAvatarScale - * @returns {number} + * @returns {number} The target scale for the avatar, range 0.0051000.0. */ Q_INVOKABLE float getAvatarScale(); /**jsdoc + * Sets the target scale of the avatar. The target scale is the desired scale of the avatar without any restrictions on + * permissible scale values imposed by the domain. * @function MyAvatar.setAvatarScale - * @param {number} scale + * @param {number} scale - The target scale for the avatar, range 0.0051000.0. */ Q_INVOKABLE void setAvatarScale(float scale); - /**jsdoc + * Sets whether the avatar should collide with entities. + *

Note: A false value won't disable collisions if the avatar is in a zone that disallows + * collisionless avatars. However, the false value will be set so that collisions are disabled as soon as the + * avatar moves to a position where collisionless avatars are allowed. * @function MyAvatar.setCollisionsEnabled - * @param {boolean} enabled + * @param {boolean} enabled - true to enable the avatar to collide with entities, false to + * disable. */ Q_INVOKABLE void setCollisionsEnabled(bool enabled); /**jsdoc + * Gets whether the avatar will currently collide with entities. + *

Note: The avatar will always collide with entities if in a zone that disallows collisionless avatars. * @function MyAvatar.getCollisionsEnabled - * @returns {boolean} + * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. */ Q_INVOKABLE bool getCollisionsEnabled(); /**jsdoc - * @function MyAvatar.setOtherAvatarsCollisionsEnabled - * @param {boolean} enabled - */ + * Sets whether the avatar should collide with other avatars. + * @function MyAvatar.setOtherAvatarsCollisionsEnabled + * @param {boolean} enabled - true to enable the avatar to collide with other avatars, false + * to disable. + */ Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled); /**jsdoc - * @function MyAvatar.getOtherAvatarsCollisionsEnabled - * @returns {boolean} - */ + * Gets whether the avatar will collide with other avatars. + * @function MyAvatar.getOtherAvatarsCollisionsEnabled + * @returns {boolean} true if the avatar will collide with other avatars, false if it won't. + */ Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled(); /**jsdoc - * @function MyAvatar.getCollisionCapsule - * @returns {object} - */ + * Gets the avatar's collision capsule: a cylinder with hemispherical ends that approximates the extents or the avatar. + *

Warning: The values returned are in world coordinates but aren't necessarily up to date with the + * avatar's current position.

+ * @function MyAvatar.getCollisionCapsule + * @returns {MyAvatar.CollisionCapsule} The avatar's collision capsule. + */ Q_INVOKABLE QVariantMap getCollisionCapsule() const; /**jsdoc * @function MyAvatar.setCharacterControllerEnabled - * @param {boolean} enabled - * @deprecated + * @param {boolean} enabled - true to enable the avatar to collide with entities, false to + * disable. + * @deprecated Use {@link MyAvatar.setCollisionsEnabled} instead. */ Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated /**jsdoc * @function MyAvatar.getCharacterControllerEnabled - * @returns {boolean} - * @deprecated + * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. + * @deprecated Use {@link MyAvatar.getCollisionsEnabled} instead. */ Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated + /**jsdoc + * @comment Different behavior to the Avatar version of this method. + * Gets the rotation of a joint relative to the avatar. + * @function MyAvatar.getAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. + * @returns {Quat} The rotation of the joint relative to the avatar. + * @example Report the rotation of your avatar's head joint relative to your avatar. + * var headIndex = MyAvatar.getJointIndex("Head"); + * var headRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex); + * print("Head rotation: " + JSON.stringify(Quat.safeEulerAngles(headRotation))); // Degrees + */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; + + /**jsdoc + * @comment Different behavior to the Avatar version of this method. + * Gets the translation of a joint relative to the avatar. + * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. + * @returns {Vec3} The translation of the joint relative to the avatar. + * @example Report the translation of your avatar's head joint relative to your avatar. + * var headIndex = MyAvatar.getJointIndex("Head"); + * var headTranslation = MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex); + * print("Head translation: " + JSON.stringify(headTranslation)); + */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; // all calibration matrices are in absolute sensor space. @@ -1104,16 +1448,22 @@ public: glm::mat4 deriveBodyUsingCgModel(); /**jsdoc + * Tests whether a vector is pointing in the general direction of the avatar's "up" direction (i.e., dot product of vectors + * is > 0). * @function MyAvatar.isUp - * @param {Vec3} direction - * @returns {boolean} + * @param {Vec3} direction - The vector to test. + * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's "up" + * direction. */ Q_INVOKABLE bool isUp(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up. /**jsdoc + * Tests whether a vector is pointing in the general direction of the avatar's "down" direction (i.e., dot product of + * vectors is < 0). * @function MyAvatar.isDown - * @param {Vec3} direction - * @returns {boolean} + * @param {Vec3} direction - The vector to test. + * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's + * "down" direction. */ Q_INVOKABLE bool isDown(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; }; @@ -1122,6 +1472,7 @@ public: float getUserEyeHeight() const; virtual SpatialParentTree* getParentTree() const override; + virtual glm::vec3 scaleForChildren() const override { return glm::vec3(getSensorToWorldScale()); } const QUuid& getSelfID() const { return AVATAR_SELF_ID; } @@ -1160,110 +1511,164 @@ public: void prepareAvatarEntityDataForReload(); /**jsdoc - * Create a new grab. + * Creates a new grab that grabs an entity. * @function MyAvatar.grab - * @param {Uuid} targetID - id of grabbed thing - * @param {number} parentJointIndex - avatar joint being used to grab - * @param {Vec3} offset - target's positional offset from joint - * @param {Quat} rotationalOffset - target's rotational offset from joint - * @returns {Uuid} id of the new grab + * @param {Uuid} targetID - The ID of the entity to grab. + * @param {number} parentJointIndex - The avatar joint to use to grab the entity. + * @param {Vec3} offset - The target's local position relative to the joint. + * @param {Quat} rotationalOffset - The target's local rotation relative to the joint. + * @returns {Uuid} The ID of the new grab. + * @example Create and grab an entity for a short while. + * var entityPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })); + * var entityRotation = MyAvatar.orientation; + * var entityID = Entities.addEntity({ + * type: "Box", + * position: entityPosition, + * rotation: entityRotation, + * dimensions: { x: 0.5, y: 0.5, z: 0.5 } + * }); + * var rightHandJoint = MyAvatar.getJointIndex("RightHand"); + * var relativePosition = Entities.worldToLocalPosition(entityPosition, MyAvatar.SELF_ID, rightHandJoint); + * var relativeRotation = Entities.worldToLocalRotation(entityRotation, MyAvatar.SELF_ID, rightHandJoint); + * var grabID = MyAvatar.grab(entityID, rightHandJoint, relativePosition, relativeRotation); + * + * Script.setTimeout(function () { + * MyAvatar.releaseGrab(grabID); + * Entities.deleteEntity(entityID); + * }, 10000); */ Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex, glm::vec3 positionalOffset, glm::quat rotationalOffset); /**jsdoc - * Release (delete) a grab. + * Releases (deletes) a grab to stop grabbing an entity. * @function MyAvatar.releaseGrab - * @param {Uuid} grabID - id of grabbed thing + * @param {Uuid} grabID - The ID of the grab to release. */ Q_INVOKABLE void releaseGrab(const QUuid& grabID); + /**jsdoc + * Gets details of all avatar entities. + * @function MyAvatar.getAvatarEntityData + * @returns {AvatarEntityMap} Details of the avatar entities. + * @example Report the current avatar entities. + * var avatarEntityData = MyAvatar.getAvatarEntityData(); + * print("Avatar entities: " + JSON.stringify(avatarEntityData)); + */ AvatarEntityMap getAvatarEntityData() const override; + + /**jsdoc + * Sets all avatar entities from an object. + * @function MyAvatar.setAvatarEntityData + * @param {AvatarEntityMap} avatarEntityData - Details of the avatar entities. + */ void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; + + /**jsdoc + * @comment Uses the base class's JSDoc. + */ void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; + void avatarEntityDataToJson(QJsonObject& root) const override; + + /**jsdoc + * @comment Uses the base class's JSDoc. + */ int sendAvatarDataPacket(bool sendAll = false) override; void addAvatarHandsToFlow(const std::shared_ptr& otherAvatar); /**jsdoc - * Init flow simulation on avatar. - * @function MyAvatar.useFlow - * @param {boolean} - Set to true to activate flow simulation. - * @param {boolean} - Set to true to activate collisions. - * @param {Object} physicsConfig - object with the customized physic parameters - * i.e. {"hair": {"active": true, "stiffness": 0.0, "radius": 0.04, "gravity": -0.035, "damping": 0.8, "inertia": 0.8, "delta": 0.35}} - * @param {Object} collisionsConfig - object with the customized collision parameters - * i.e. {"Spine2": {"type": "sphere", "radius": 0.14, "offset": {"x": 0.0, "y": 0.2, "z": 0.0}}} - */ + * Enables and disables flow simulation of physics on the avatar's hair, clothes, and body parts. See + * {@link https://docs.highfidelity.com/create/avatars/add-flow.html|Add Flow to Your Avatar} for more + * information. + * @function MyAvatar.useFlow + * @param {boolean} isActive - true if flow simulation is enabled on the joint, false if it isn't. + * @param {boolean} isCollidable - true to enable collisions in the flow simulation, false to + * disable. + * @param {Object} [physicsConfig>] - Physics configurations for particular entity + * and avatar joints. + * @param {Object} [collisionsConfig] - Collision configurations for particular + * entity and avatar joints. + */ Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); /**jsdoc - * @function MyAvatar.getFlowData - * @returns {object} - */ + * Gets the current flow configuration. + * @function MyAvatar.getFlowData + * @returns {MyAvatar.FlowData} + */ Q_INVOKABLE QVariantMap getFlowData(); /**jsdoc - * returns the indices of every colliding flow joint - * @function MyAvatar.getCollidingFlowJoints - * @returns {int[]} - */ + * Gets the indexes of currently colliding flow joints. + * @function MyAvatar.getCollidingFlowJoints + * @returns {number[]} The indexes of currently colliding flow joints. + */ Q_INVOKABLE QVariantList getCollidingFlowJoints(); public slots: + /**jsdoc + * @comment Uses the base class's JSDoc. + */ + virtual void setSessionUUID(const QUuid& sessionUUID) override; + /**jsdoc - * Increase the avatar's scale by five percent, up to a minimum scale of 1000. + * Increases the avatar's scale by five percent, up to a minimum scale of 1000. * @function MyAvatar.increaseSize * @example Reset your avatar's size to default then grow it 5 times. * MyAvatar.resetSize(); * * for (var i = 0; i < 5; i++){ - * print ("Growing by 5 percent"); + * print("Growing by 5 percent"); * MyAvatar.increaseSize(); * } */ void increaseSize(); /**jsdoc - * Decrease the avatar's scale by five percent, down to a minimum scale of 0.25. + * Decreases the avatar's scale by five percent, down to a minimum scale of 0.25. * @function MyAvatar.decreaseSize * @example Reset your avatar's size to default then shrink it 5 times. * MyAvatar.resetSize(); * * for (var i = 0; i < 5; i++){ - * print ("Shrinking by 5 percent"); + * print("Shrinking by 5 percent"); * MyAvatar.decreaseSize(); * } */ void decreaseSize(); /**jsdoc - * Reset the avatar's scale back to the default scale of 1.0. + * Resets the avatar's scale back to the default scale of 1.0. * @function MyAvatar.resetSize */ void resetSize(); /**jsdoc * @function MyAvatar.animGraphLoaded + * @deprecated This function is deprecated and will be removed. */ void animGraphLoaded(); /**jsdoc + * Sets the amount of gravity applied to the avatar in the y-axis direction. (Negative values are downward.) * @function MyAvatar.setGravity - * @param {number} gravity + * @param {number} gravity - The amount of gravity to be applied to the avatar, in m/s2. */ void setGravity(float gravity); /**jsdoc + * Sets the amount of gravity applied to the avatar in the y-axis direction. (Negative values are downward.) The default + * value is -5 m/s2. * @function MyAvatar.getGravity - * @returns {number} + * @returns {number} The amount of gravity currently applied to the avatar, in m/s2. */ float getGravity(); /**jsdoc - * Move the avatar to a new position and/or orientation in the domain, while taking into account Avatar leg-length. + * Moves the avatar to a new position and/or orientation in the domain, while taking into account Avatar leg-length. * @function MyAvatar.goToFeetLocation * @param {Vec3} position - The new position for the avatar, in world coordinates. * @param {boolean} [hasOrientation=false] - Set to true to set the orientation of the avatar. @@ -1271,13 +1676,12 @@ public slots: * @param {boolean} [shouldFaceLocation=false] - Set to true to position the avatar a short distance away from * the new position and orientate the avatar to face the position. */ - void goToFeetLocation(const glm::vec3& newPosition, bool hasOrientation, const glm::quat& newOrientation, bool shouldFaceLocation); /**jsdoc - * Move the avatar to a new position and/or orientation in the domain. + * Moves the avatar to a new position and/or orientation in the domain. * @function MyAvatar.goToLocation * @param {Vec3} position - The new position for the avatar, in world coordinates. * @param {boolean} [hasOrientation=false] - Set to true to set the orientation of the avatar. @@ -1290,133 +1694,183 @@ public slots: bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), bool shouldFaceLocation = false, bool withSafeLanding = true); /**jsdoc + * Moves the avatar to a new position and (optional) orientation in the domain. * @function MyAvatar.goToLocation - * @param {object} properties + * @param {MyAvatar.GoToProperties} target - The goto target. */ void goToLocation(const QVariant& properties); /**jsdoc + * Moves the avatar to a new position and then enables collisions. * @function MyAvatar.goToLocationAndEnableCollisions - * @param {Vec3} position + * @param {Vec3} position - The new position for the avatar, in world coordinates. */ void goToLocationAndEnableCollisions(const glm::vec3& newPosition); /**jsdoc * @function MyAvatar.safeLanding - * @param {Vec3} position - * @returns {boolean} + * @param {Vec3} position -The new position for the avatar, in world coordinates. + * @returns {boolean} true if the avatar was moved, false if it wasn't. + * @deprecated This function is deprecated and will be removed. */ bool safeLanding(const glm::vec3& position); /**jsdoc * @function MyAvatar.restrictScaleFromDomainSettings - * @param {objecct} domainSettingsObject + * @param {object} domainSettings - Domain settings. + * @deprecated This function is deprecated and will be removed. */ void restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject); /**jsdoc * @function MyAvatar.clearScaleRestriction + * @deprecated This function is deprecated and will be removed from the API. */ void clearScaleRestriction(); /**jsdoc + * Adds a thrust to your avatar's current thrust to be applied for a short while. * @function MyAvatar.addThrust - * @param {Vec3} thrust + * @param {Vec3} thrust - The thrust direction and magnitude. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ // Set/Get update the thrust that will move the avatar around void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; /**jsdoc + * Gets the thrust currently being applied to your avatar. * @function MyAvatar.getThrust - * @returns {vec3} + * @returns {Vec3} The thrust currently being applied to your avatar. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ glm::vec3 getThrust() { return _thrust; }; /**jsdoc + * Sets the thrust to be applied to your avatar for a short while. * @function MyAvatar.setThrust - * @param {Vec3} thrust + * @param {Vec3} thrust - The thrust direction and magnitude. + * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. */ void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } /**jsdoc + * Updates avatar motion behavior from the Developer > Avatar > Enable Default Motor Control and Enable Scripted + * Motor Control menu items. * @function MyAvatar.updateMotionBehaviorFromMenu */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); /**jsdoc - * @function MyAvatar.setToggleHips - * @param {boolean} enabled - */ + * @function MyAvatar.setToggleHips + * @param {boolean} enabled - Enabled. + * @deprecated This function is deprecated and will be removed. + */ void setToggleHips(bool followHead); + /**jsdoc - * @function MyAvatar.setEnableDebugDrawBaseOfSupport - * @param {boolean} enabled - */ + * Displays the base of support area debug graphics if in HMD mode. If your head goes outside this area your avatar's hips + * are moved to counterbalance your avatar, and if your head moves too far then your avatar's position is moved (i.e., a + * step happens). + * @function MyAvatar.setEnableDebugDrawBaseOfSupport + * @param {boolean} enabled - true to show the debug graphics, false to hide. + */ void setEnableDebugDrawBaseOfSupport(bool isEnabled); + /**jsdoc + * Displays default pose debug graphics. * @function MyAvatar.setEnableDebugDrawDefaultPose - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawDefaultPose(bool isEnabled); + /**jsdoc + * Displays animation debug graphics. * @function MyAvatar.setEnableDebugDrawAnimPose - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawAnimPose(bool isEnabled); + /**jsdoc + * Displays position debug graphics. * @function MyAvatar.setEnableDebugDrawPosition - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawPosition(bool isEnabled); + /**jsdoc + * Displays controller hand target debug graphics. * @function MyAvatar.setEnableDebugDrawHandControllers - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawHandControllers(bool isEnabled); + /**jsdoc + * Displays sensor-to-world matrix debug graphics. * @function MyAvatar.setEnableDebugDrawSensorToWorldMatrix - * @param {boolean} enabled + * @param {boolean} enable - true to show the debug graphics, false to hide. */ void setEnableDebugDrawSensorToWorldMatrix(bool isEnabled); + /**jsdoc + * Displays inverse kinematics targets debug graphics. * @function MyAvatar.setEnableDebugDrawIKTargets - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKTargets(bool isEnabled); + /**jsdoc + * Displays inverse kinematics constraints debug graphics. * @function MyAvatar.setEnableDebugDrawIKConstraints - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKConstraints(bool isEnabled); + /**jsdoc + * Displays inverse kinematics chains debug graphics. * @function MyAvatar.setEnableDebugDrawIKChains - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawIKChains(bool isEnabled); + /**jsdoc + * Displays detailed collision debug graphics. * @function MyAvatar.setEnableDebugDrawDetailedCollision - * @param {boolean} enabled + * @param {boolean} enabled - true to show the debug graphics, false to hide. */ void setEnableDebugDrawDetailedCollision(bool isEnabled); /**jsdoc - * Get whether or not your avatar mesh is visible. + * Gets whether your avatar mesh is visible. * @function MyAvatar.getEnableMeshVisible * @returns {boolean} true if your avatar's mesh is visible, otherwise false. */ bool getEnableMeshVisible() const override; + /**jsdoc + * @function MyAvatar.storeAvatarEntityDataPayload + * @deprecated This function is deprecated and will be removed. + */ void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; + + /**jsdoc + * @comment Uses the base class's JSDoc. + */ void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override; + + /**jsdoc + * @function MyAvatar.sanitizeAvatarEntityProperties + * @param {EntityItemProperties} properties - Properties. + * @deprecated This function is deprecated and will be removed. + */ void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const; /**jsdoc - * Set whether or not your avatar mesh is visible. + * Sets whether your avatar mesh is visible to you. * @function MyAvatar.setEnableMeshVisible - * @param {boolean} visible - true to set your avatar mesh visible; false to set it invisible. + * @param {boolean} enabled - true to show your avatar mesh, false to hide. * @example Make your avatar invisible for 10s. * MyAvatar.setEnableMeshVisible(false); * Script.setTimeout(function () { @@ -1426,57 +1880,79 @@ public slots: virtual void setEnableMeshVisible(bool isEnabled) override; /**jsdoc + * Sets whether inverse kinematics (IK) is enabled for your avatar. * @function MyAvatar.setEnableInverseKinematics - * @param {boolean} enabled + * @param {boolean} enabled - true to enable IK, false to disable. */ void setEnableInverseKinematics(bool isEnabled); /**jsdoc + * Gets the URL of the override animation graph. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.getAnimGraphOverrideUrl - * @returns {string} + * @returns {string} The URL of the override animation graph JSON file. "" if there is no override animation + * graph. */ QUrl getAnimGraphOverrideUrl() const; // thread-safe /**jsdoc + * Sets the animation graph to use in preference to the default animation graph. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.setAnimGraphOverrideUrl - * @param {string} url + * @param {string} url - The URL of the animation graph JSON file to use. Set to "" to clear an override. */ void setAnimGraphOverrideUrl(QUrl value); // thread-safe /**jsdoc + * Gets the URL of animation graph (i.e., the avatar animation JSON) that's currently being used for avatar animations. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.getAnimGraphUrl - * @returns {string} + * @returns {string} The URL of the current animation graph JSON file. + * @Example Report the current avatar animation JSON being used. + * print(MyAvatar.getAnimGraphUrl()); */ QUrl getAnimGraphUrl() const; // thread-safe /**jsdoc + * Sets the current animation graph (i.e., the avatar animation JSON) to use for avatar animations and makes it the default. + *

See {@link https://docs.highfidelity.com/create/avatars/custom-animations.html|Custom Avatar Animations} for + * information on animation graphs.

* @function MyAvatar.setAnimGraphUrl - * @param {string} url + * @param {string} url - The URL of the animation graph JSON file to use. */ void setAnimGraphUrl(const QUrl& url); // thread-safe /**jsdoc + * Gets your listening position for spatialized audio. The position depends on the value of the + * {@link Myavatar|audioListenerMode} property. * @function MyAvatar.getPositionForAudio - * @returns {Vec3} + * @returns {Vec3} Your listening position. */ glm::vec3 getPositionForAudio(); /**jsdoc + * Gets the orientation of your listening position for spatialized audio. The orientation depends on the value of the + * {@link Myavatar|audioListenerMode} property. * @function MyAvatar.getOrientationForAudio - * @returns {Quat} + * @returns {Quat} The orientation of your listening position. */ glm::quat getOrientationForAudio(); /**jsdoc * @function MyAvatar.setModelScale - * @param {number} scale + * @param {number} scale - The scale. + * @deprecated This function is deprecated and will be removed. */ virtual void setModelScale(float scale) override; signals: /**jsdoc + * Triggered when the {@link MyAvatar|audioListenerMode} property value changes. * @function MyAvatar.audioListenerModeChanged * @returns {Signal} */ @@ -1485,12 +1961,14 @@ signals: /**jsdoc * @function MyAvatar.transformChanged * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void transformChanged(); /**jsdoc + * Triggered when the {@link MyAvatar|collisionSoundURL} property value changes. * @function MyAvatar.newCollisionSoundURL - * @param {string} url + * @param {string} url - The URL of the new collision sound. * @returns {Signal} */ void newCollisionSoundURL(const QUrl& url); @@ -1498,7 +1976,7 @@ signals: /**jsdoc * Triggered when the avatar collides with an entity. * @function MyAvatar.collisionWithEntity - * @param {Collision} collision + * @param {Collision} collision - Details of the collision. * @returns {Signal} * @example Report each time your avatar collides with an entity. * MyAvatar.collisionWithEntity.connect(function (collision) { @@ -1508,37 +1986,44 @@ signals: void collisionWithEntity(const Collision& collision); /**jsdoc - * Triggered when collisions with avatar enabled or disabled + * Triggered when collisions with the environment are enabled or disabled. * @function MyAvatar.collisionsEnabledChanged - * @param {boolean} enabled + * @param {boolean} enabled - true if collisions with the environment are enabled, false if + * they're not. * @returns {Signal} */ void collisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when collisions with other avatars enabled or disabled - * @function MyAvatar.otherAvatarsCollisionsEnabledChanged - * @param {boolean} enabled - * @returns {Signal} - */ + * Triggered when collisions with other avatars are enabled or disabled. + * @function MyAvatar.otherAvatarsCollisionsEnabledChanged + * @param {boolean} enabled - true if collisions with other avatars are enabled, false if they're + * not. + * @returns {Signal} + */ void otherAvatarsCollisionsEnabledChanged(bool enabled); /**jsdoc - * Triggered when avatar's animation url changes + * Triggered when the avatar's animation graph being used changes. * @function MyAvatar.animGraphUrlChanged - * @param {url} url + * @param {string} url - The URL of the new animation graph JSON file. * @returns {Signal} - */ + * @example Report when the current avatar animation JSON being used changes. + * MyAvatar.animGraphUrlChanged.connect(function (url) { + * print("Avatar animation JSON changed to: " + url); + * }); */ void animGraphUrlChanged(const QUrl& url); /**jsdoc * @function MyAvatar.energyChanged - * @param {number} energy + * @param {number} energy - Avatar energy. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void energyChanged(float newEnergy); /**jsdoc + * Triggered when the avatar has been moved to a new position by one of the MyAvatar "goTo" functions. * @function MyAvatar.positionGoneTo * @returns {Signal} */ @@ -1546,75 +2031,102 @@ signals: void positionGoneTo(); /**jsdoc + * Triggered when the avatar's model finishes loading. * @function MyAvatar.onLoadComplete * @returns {Signal} */ void onLoadComplete(); /**jsdoc + * Triggered when your avatar changes from being active to being away. * @function MyAvatar.wentAway * @returns {Signal} + * @example Report when your avatar goes away. + * MyAvatar.wentAway.connect(function () { + * print("My avatar went away"); + * }); + * // In desktop mode, pres the Esc key to go away. */ void wentAway(); /**jsdoc + * Triggered when your avatar changes from being away to being active. * @function MyAvatar.wentActive * @returns {Signal} */ void wentActive(); /**jsdoc + * Triggered when the avatar's model (i.e., {@link MyAvatar|skeletonModelURL} property value) is changed. + *

Synonym of {@link MyAvatar.skeletonModelURLChanged|skeletonModelURLChanged}.

* @function MyAvatar.skeletonChanged * @returns {Signal} */ void skeletonChanged(); /**jsdoc + * Triggered when the avatar's dominant hand changes. * @function MyAvatar.dominantHandChanged - * @param {string} hand + * @param {string} hand - The dominant hand: "left" for the left hand, "right" for the right hand. * @returns {Signal} */ void dominantHandChanged(const QString& hand); /**jsdoc + * Triggered when the HMD alignment for your avatar changes. * @function MyAvatar.hmdAvatarAlignmentTypeChanged - * @param {string} type + * @param {string} type - "head" if aligning your head and your avatar's head, "eyes" if aligning + * your eyes and your avatar's eyes. * @returns {Signal} */ void hmdAvatarAlignmentTypeChanged(const QString& type); /**jsdoc + * Triggered when the avatar's sensorToWorldScale property value changes. * @function MyAvatar.sensorToWorldScaleChanged - * @param {number} scale + * @param {number} scale - The scale that transforms dimensions in the user's real world to the avatar's size in the virtual + * world. * @returns {Signal} */ void sensorToWorldScaleChanged(float sensorToWorldScale); /**jsdoc + * Triggered when the a model is attached to or detached from one of the avatar's joints using one of + * {@link MyAvatar.attach|attach}, {@link MyAvatar.detachOne|detachOne}, {@link MyAvatar.detachAll|detachAll}, or + * {@link MyAvatar.setAttachmentData|setAttachmentData}. * @function MyAvatar.attachmentsChanged * @returns {Signal} + * @deprecated Use avatar entities instead. */ void attachmentsChanged(); /**jsdoc + * Triggered when the avatar's size changes. This can be due to the user changing the size of their avatar or the domain + * limiting the size of their avatar. * @function MyAvatar.scaleChanged * @returns {Signal} */ void scaleChanged(); /**jsdoc - * Triggered when hand touch is globally enabled or disabled + * Triggered when the hand touch effect is enabled or disabled for the avatar. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.shouldDisableHandTouchChanged - * @param {boolean} shouldDisable + * @param {boolean} disabled - true if the hand touch effect is disabled for the avatar, + * false if it isn't disabled. * @returns {Signal} */ void shouldDisableHandTouchChanged(bool shouldDisable); /**jsdoc - * Triggered when hand touch is enabled or disabled for an specific entity + * Triggered when the hand touch is enabled or disabled on a specific entity. + *

The hand touch effect makes the avatar's fingers adapt to the shape of any object grabbed, creating the effect that + * it is really touching that object.

* @function MyAvatar.disableHandTouchForIDChanged - * @param {Uuid} entityID - ID of the entity that will enable hand touch effect - * @param {boolean} disable + * @param {Uuid} entityID - The entity that the hand touch effect has been enabled or disabled for. + * @param {boolean} disabled - true if the hand touch effect is disabled for the entity, + * false if it isn't disabled. * @returns {Signal} */ void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); @@ -1659,7 +2171,7 @@ private: bool getEnableStepResetRotation() const { return _stepResetRotationEnabled; } void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; } bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; } - bool isMyAvatar() const override { return true; } + virtual bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; int _skeletonModelChangeCount { 0 }; @@ -1912,7 +2424,7 @@ private: bool didTeleport(); bool getIsAway() const { return _isAway; } void setAway(bool value); - void sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const override; + void sendPacket(const QUuid& entityID) const override; std::mutex _pinnedJointsMutex; std::vector _pinnedJoints; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 7848c46eee..2e775a20c3 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -200,17 +200,6 @@ void OtherAvatar::resetDetailedMotionStates() { void OtherAvatar::setWorkloadRegion(uint8_t region) { _workloadRegion = region; - QString printRegion = ""; - if (region == workload::Region::R1) { - printRegion = "R1"; - } else if (region == workload::Region::R2) { - printRegion = "R2"; - } else if (region == workload::Region::R3) { - printRegion = "R3"; - } else { - printRegion = "invalid"; - } - qCDebug(avatars) << "Setting workload region to " << printRegion; computeShapeLOD(); } @@ -235,7 +224,6 @@ void OtherAvatar::computeShapeLOD() { if (newLOD != _bodyLOD) { _bodyLOD = newLOD; if (isInPhysicsSimulation()) { - qCDebug(avatars) << "Changing to body LOD " << newLOD; _needsReinsertion = true; } } @@ -368,7 +356,6 @@ void OtherAvatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "grabs"); applyGrabChanges(); } - updateFadingStatus(); } void OtherAvatar::handleChangedAvatarEntityData() { @@ -377,7 +364,7 @@ void OtherAvatar::handleChangedAvatarEntityData() { // AVATAR ENTITY UPDATE FLOW // - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload() // - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated, - // - ClientTraitsHandler::sendChangedTraitsToMixea() sends the entity bytes to the mixer which relays them to other interfaces + // - ClientTraitsHandler::sendChangedTraitsToMixer() sends the entity bytes to the mixer which relays them to other interfaces // - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance() // - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true // - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged @@ -507,6 +494,18 @@ void OtherAvatar::handleChangedAvatarEntityData() { const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); entity->setParentID(NULL_ID); entity->setParentID(oldParentID); + + if (entity->stillHasMyGrabAction()) { + // For this case: we want to ignore transform+velocities coming from authoritative OtherAvatar + // because the MyAvatar is grabbing and we expect the local grab state + // to have enough information to prevent simulation drift. + // + // Clever readers might realize this could cause problems. For example, + // if an ignored OtherAvagtar were to simultanously grab the object then there would be + // a noticeable discrepancy between participants in the distributed physics simulation, + // however the difference would be stable and would not drift. + properties.clearTransformOrVelocityChanges(); + } if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); } else { diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 2e18f34c8d..64528e617d 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "AccountManager.h" @@ -65,7 +66,7 @@ signals: void availableUpdatesResult(QJsonObject result); void updateItemResult(QJsonObject result); - void updateCertificateStatus(const QString& certID, uint certStatus); + void updateCertificateStatus(const EntityItemID& entityID, uint certStatus); public slots: void buySuccess(QNetworkReply* reply); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 3217b8a1f9..99b3e32e8b 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -19,6 +19,7 @@ #include +#include #include class QmlCommerce : public QObject, public Dependency { @@ -49,7 +50,7 @@ signals: void availableUpdatesResult(QJsonObject result); void updateItemResult(QJsonObject result); - void updateCertificateStatus(const QString& certID, uint certStatus); + void updateCertificateStatus(const EntityItemID& entityID, uint certStatus); void transferAssetToNodeResult(QJsonObject result); void transferAssetToUsernameResult(QJsonObject result); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 0e9ad7d79a..37f28960e5 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -816,18 +816,18 @@ void Wallet::handleChallengeOwnershipPacket(QSharedPointer pack bool challengeOriginatedFromClient = packet->getType() == PacketType::ChallengeOwnershipRequest; int status; - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; int challengingNodeUUIDByteArraySize; - packet->readPrimitive(&certIDByteArraySize); + packet->readPrimitive(&idByteArraySize); packet->readPrimitive(&textByteArraySize); // returns a cast char*, size if (challengeOriginatedFromClient) { packet->readPrimitive(&challengingNodeUUIDByteArraySize); } // "encryptedText" is now a series of random bytes, a nonce - QByteArray certID = packet->read(certIDByteArraySize); + QByteArray id = packet->read(idByteArraySize); QByteArray text = packet->read(textByteArraySize); QByteArray challengingNodeUUID; if (challengeOriginatedFromClient) { @@ -853,32 +853,32 @@ void Wallet::handleChallengeOwnershipPacket(QSharedPointer pack textByteArray = sig.toUtf8(); } textByteArraySize = textByteArray.size(); - int certIDSize = certID.size(); + int idSize = id.size(); // setup the packet if (challengeOriginatedFromClient) { auto textPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, - certIDSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), + idSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), true); - textPacket->writePrimitive(certIDSize); + textPacket->writePrimitive(idSize); textPacket->writePrimitive(textByteArraySize); textPacket->writePrimitive(challengingNodeUUIDByteArraySize); - textPacket->write(certID); + textPacket->write(id); textPacket->write(textByteArray); textPacket->write(challengingNodeUUID); - qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for CertID" << certID; + qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for id" << id; nodeList->sendPacket(std::move(textPacket), *sendingNode); } else { - auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, certIDSize + textByteArraySize + 2 * sizeof(int), true); + auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, idSize + textByteArraySize + 2 * sizeof(int), true); - textPacket->writePrimitive(certIDSize); + textPacket->writePrimitive(idSize); textPacket->writePrimitive(textByteArraySize); - textPacket->write(certID); + textPacket->write(id); textPacket->write(textByteArray); - qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for CertID" << certID; + qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for id" << id; nodeList->sendPacket(std::move(textPacket), *sendingNode); } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..caae946116 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -25,6 +25,8 @@ QString Audio::DESKTOP { "Desktop" }; QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; +Setting::Handle enableWarnWhenMutedSetting { QStringList { Audio::AUDIO, "WarnWhenMuted" }, true }; + float Audio::loudnessToLevel(float loudness) { float level = loudness * (1/32768.0f); // level in [0, 1] @@ -37,10 +39,13 @@ Audio::Audio() : _devices(_contextIsHMD) { auto client = DependencyManager::get().data(); connect(client, &AudioClient::muteToggled, this, &Audio::setMuted); connect(client, &AudioClient::noiseReductionChanged, this, &Audio::enableNoiseReduction); + connect(client, &AudioClient::warnWhenMutedChanged, this, &Audio::enableWarnWhenMuted); connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); + connect(this, &Audio::pushingToTalkChanged, this, &Audio::handlePushedToTalk); enableNoiseReduction(enableNoiseReductionSetting.get()); + enableWarnWhenMuted(enableWarnWhenMutedSetting.get()); onContextChanged(); } @@ -63,26 +68,177 @@ void Audio::stopRecording() { } bool Audio::isMuted() const { - return resultWithReadLock([&] { - return _isMuted; - }); + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getMutedHMD(); + } else { + return getMutedDesktop(); + } } void Audio::setMuted(bool isMuted) { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setMutedHMD(isMuted); + } else { + setMutedDesktop(isMuted); + } +} + +void Audio::setMutedDesktop(bool isMuted) { bool changed = false; withWriteLock([&] { - if (_isMuted != isMuted) { - _isMuted = isMuted; + if (_mutedDesktop != isMuted) { + changed = true; + _mutedDesktop = isMuted; auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); - changed = true; } }); if (changed) { emit mutedChanged(isMuted); + emit mutedDesktopChanged(isMuted); } } +bool Audio::getMutedDesktop() const { + return resultWithReadLock([&] { + return _mutedDesktop; + }); +} + +void Audio::setMutedHMD(bool isMuted) { + bool changed = false; + withWriteLock([&] { + if (_mutedHMD != isMuted) { + changed = true; + _mutedHMD = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + } + }); + if (changed) { + emit mutedChanged(isMuted); + emit mutedHMDChanged(isMuted); + } +} + +bool Audio::getMutedHMD() const { + return resultWithReadLock([&] { + return _mutedHMD; + }); +} + +bool Audio::getPTT() { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + return getPTTHMD(); + } else { + return getPTTDesktop(); + } +} + +void scripting::Audio::setPushingToTalk(bool pushingToTalk) { + bool changed = false; + withWriteLock([&] { + if (_pushingToTalk != pushingToTalk) { + changed = true; + _pushingToTalk = pushingToTalk; + } + }); + if (changed) { + emit pushingToTalkChanged(pushingToTalk); + } +} + +bool Audio::getPushingToTalk() const { + return resultWithReadLock([&] { + return _pushingToTalk; + }); +} + +void Audio::setPTT(bool enabled) { + bool isHMD = qApp->isHMDMode(); + if (isHMD) { + setPTTHMD(enabled); + } else { + setPTTDesktop(enabled); + } +} + +void Audio::setPTTDesktop(bool enabled) { + bool changed = false; + withWriteLock([&] { + if (_pttDesktop != enabled) { + changed = true; + _pttDesktop = enabled; + } + }); + if (!enabled) { + // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. + setMutedDesktop(true); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedDesktop(true); + } + + if (changed) { + emit pushToTalkChanged(enabled); + emit pushToTalkDesktopChanged(enabled); + } +} + +bool Audio::getPTTDesktop() const { + return resultWithReadLock([&] { + return _pttDesktop; + }); +} + +void Audio::setPTTHMD(bool enabled) { + bool changed = false; + withWriteLock([&] { + if (_pttHMD != enabled) { + changed = true; + _pttHMD = enabled; + } + }); + if (!enabled) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. + setMutedHMD(false); + } else { + // Should be muted when not pushing to talk while PTT is enabled. + setMutedHMD(true); + } + + if (changed) { + emit pushToTalkChanged(enabled); + emit pushToTalkHMDChanged(enabled); + } +} + +void Audio::saveData() { + _mutedDesktopSetting.set(getMutedDesktop()); + _mutedHMDSetting.set(getMutedHMD()); + _pttDesktopSetting.set(getPTTDesktop()); + _pttHMDSetting.set(getPTTHMD()); +} + +void Audio::loadData() { + setMutedDesktop(_mutedDesktopSetting.get()); + setMutedHMD(_mutedHMDSetting.get()); + setPTTDesktop(_pttDesktopSetting.get()); + setPTTHMD(_pttHMDSetting.get()); + + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); +} + +bool Audio::getPTTHMD() const { + return resultWithReadLock([&] { + return _pttHMD; + }); +} + bool Audio::noiseReductionEnabled() const { return resultWithReadLock([&] { return _enableNoiseReduction; @@ -105,6 +261,28 @@ void Audio::enableNoiseReduction(bool enable) { } } +bool Audio::warnWhenMutedEnabled() const { + return resultWithReadLock([&] { + return _enableWarnWhenMuted; + }); +} + +void Audio::enableWarnWhenMuted(bool enable) { + bool changed = false; + withWriteLock([&] { + if (_enableWarnWhenMuted != enable) { + _enableWarnWhenMuted = enable; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setWarnWhenMuted", Q_ARG(bool, enable), Q_ARG(bool, false)); + enableWarnWhenMutedSetting.set(enable); + changed = true; + } + }); + if (changed) { + emit warnWhenMutedChanged(enable); + } +} + float Audio::getInputVolume() const { return resultWithReadLock([&] { return _inputVolume; @@ -179,11 +357,38 @@ void Audio::onContextChanged() { changed = true; } }); + if (isHMD) { + setMuted(getMutedHMD()); + } else { + setMuted(getMutedDesktop()); + } if (changed) { emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); } } +void Audio::handlePushedToTalk(bool enabled) { + if (getPTT()) { + if (enabled) { + setMuted(false); + } else { + setMuted(true); + } + } +} + +void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseInputDevice(device, isHMD); + }); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { + withWriteLock([&] { + _devices.chooseOutputDevice(device, isHMD); + }); +} + void Audio::setReverb(bool enable) { withWriteLock([&] { DependencyManager::get()->setReverb(enable); @@ -196,14 +401,66 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { }); } -void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { +void Audio::setAvatarGain(float gain) { withWriteLock([&] { - _devices.chooseInputDevice(device, isHMD); + // ask the NodeList to set the master avatar gain + DependencyManager::get()->setAvatarGain(QUuid(), gain); }); } -void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { - withWriteLock([&] { - _devices.chooseOutputDevice(device, isHMD); +float Audio::getAvatarGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getAvatarGain(QUuid()); + }); +} + +void Audio::setInjectorGain(float gain) { + withWriteLock([&] { + // ask the NodeList to set the audio injector gain + DependencyManager::get()->setInjectorGain(gain); + }); +} + +float Audio::getInjectorGain() { + return resultWithReadLock([&] { + return DependencyManager::get()->getInjectorGain(); + }); +} + +void Audio::setLocalInjectorGain(float gain) { + withWriteLock([&] { + if (_localInjectorGain != gain) { + _localInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); + DependencyManager::get()->setLocalInjectorGain(gain); + } + }); +} + +float Audio::getLocalInjectorGain() { + return resultWithReadLock([&] { + return _localInjectorGain; + }); +} + +void Audio::setSystemInjectorGain(float gain) { + withWriteLock([&] { + if (_systemInjectorGain != gain) { + _systemInjectorGain = gain; + // convert dB to amplitude + gain = fastExp2f(gain / 6.02059991f); + // quantize and limit to match NodeList::setInjectorGain() + gain = unpackFloatGainFromByte(packFloatGainToByte(gain)); + DependencyManager::get()->setSystemInjectorGain(gain); + } + }); +} + +float Audio::getSystemInjectorGain() { + return resultWithReadLock([&] { + return _systemInjectorGain; }); } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index fcf3c181da..00da566b30 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -12,6 +12,7 @@ #ifndef hifi_scripting_Audio_h #define hifi_scripting_Audio_h +#include #include "AudioScriptingInterface.h" #include "AudioDevices.h" #include "AudioEffectOptions.h" @@ -19,6 +20,9 @@ #include "AudioFileWav.h" #include +using MutedGetter = std::function; +using MutedSetter = std::function; + namespace scripting { class Audio : public AudioScriptingInterface, protected ReadWriteLockable { @@ -37,16 +41,17 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * @hifi-assignment-client * * @property {boolean} muted - true if the audio input is muted, otherwise false. - * @property {boolean} noiseReduction - true if noise reduction is enabled, otherwise false. When - * enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just + * @property {boolean} mutedDesktop - true if the audio input is muted, otherwise false. + * @property {boolean} noiseReduction - true if noise reduction is enabled, otherwise false. When + * enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just * above the noise floor. - * @property {number} inputLevel - The loudness of the audio input, range 0.0 (no sound) – + * @property {number} inputLevel - The loudness of the audio input, range 0.0 (no sound) – * 1.0 (the onset of clipping). Read-only. * @property {boolean} clipping - true if the audio input is clipping, otherwise false. - * @property {number} inputVolume - Adjusts the volume of the input audio; range 0.01.0. - * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some + * @property {number} inputVolume - Adjusts the volume of the input audio; range 0.01.0. + * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some * devices, and others might only support values of 0.0 and 1.0. - * @property {boolean} isStereoInput - true if the input audio is being used in stereo, otherwise + * @property {boolean} isStereoInput - true if the input audio is being used in stereo, otherwise * false. Some devices do not support stereo, in which case the value is always false. * @property {string} context - The current context of the audio: either "Desktop" or "HMD". * Read-only. @@ -58,11 +63,18 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged) + Q_PROPERTY(bool warnWhenMuted READ warnWhenMutedEnabled WRITE enableWarnWhenMuted NOTIFY warnWhenMutedChanged) Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged) Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged) Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) + Q_PROPERTY(bool mutedDesktop READ getMutedDesktop WRITE setMutedDesktop NOTIFY mutedDesktopChanged) + Q_PROPERTY(bool mutedHMD READ getMutedHMD WRITE setMutedHMD NOTIFY mutedHMDChanged) + Q_PROPERTY(bool pushToTalk READ getPTT WRITE setPTT NOTIFY pushToTalkChanged); + Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) + Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) + Q_PROPERTY(bool pushingToTalk READ getPushingToTalk WRITE setPushingToTalk NOTIFY pushingToTalkChanged) public: static QString AUDIO; @@ -75,6 +87,7 @@ public: bool isMuted() const; bool noiseReductionEnabled() const; + bool warnWhenMutedEnabled() const; float getInputVolume() const; float getInputLevel() const; bool isClipping() const; @@ -82,10 +95,30 @@ public: void showMicMeter(bool show); + // Mute setting setters and getters + void setMutedDesktop(bool isMuted); + bool getMutedDesktop() const; + void setMutedHMD(bool isMuted); + bool getMutedHMD() const; + void setPTT(bool enabled); + bool getPTT(); + void setPushingToTalk(bool pushingToTalk); + bool getPushingToTalk() const; + + // Push-To-Talk setters and getters + void setPTTDesktop(bool enabled); + bool getPTTDesktop() const; + void setPTTHMD(bool enabled); + bool getPTTHMD() const; + + // Settings handlers + void saveData(); + void loadData(); + /**jsdoc * @function Audio.setInputDevice * @param {object} device - * @param {boolean} isHMD + * @param {boolean} isHMD * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device, bool isHMD); @@ -99,8 +132,8 @@ public: Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device, bool isHMD); /**jsdoc - * Enable or disable reverberation. Reverberation is done by the client, on the post-mix audio. The reverberation options - * come from either the domain's audio zone if used — configured on the server — or as scripted by + * Enable or disable reverberation. Reverberation is done by the client, on the post-mix audio. The reverberation options + * come from either the domain's audio zone if used — configured on the server — or as scripted by * {@link Audio.setReverbOptions|setReverbOptions}. * @function Audio.setReverb * @param {boolean} enable - true to enable reverberation, false to disable. @@ -110,13 +143,13 @@ public: * var injectorOptions = { * position: MyAvatar.position * }; - * + * * Script.setTimeout(function () { * print("Reverb OFF"); * Audio.setReverb(false); * injector = Audio.playSound(sound, injectorOptions); * }, 1000); - * + * * Script.setTimeout(function () { * var reverbOptions = new AudioEffectOptions(); * reverbOptions.roomSize = 100; @@ -124,26 +157,86 @@ public: * print("Reverb ON"); * Audio.setReverb(true); * }, 4000); - * + * * Script.setTimeout(function () { * print("Reverb OFF"); * Audio.setReverb(false); * }, 8000); */ Q_INVOKABLE void setReverb(bool enable); - + /**jsdoc * Configure reverberation options. Use {@link Audio.setReverb|setReverb} to enable or disable reverberation. * @function Audio.setReverbOptions * @param {AudioEffectOptions} options - The reverberation options. */ Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); - + + /**jsdoc + * Sets the avatar gain at the server. + * Units are Decibels (dB) + * @function Audio.setAvatarGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setAvatarGain(float gain); + + /**jsdoc + * Gets the avatar gain at the server. + * @function Audio.getAvatarGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getAvatarGain(); + + /**jsdoc + * Sets the injector gain at the server. + * Units are Decibels (dB) + * @function Audio.setInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setInjectorGain(float gain); + + /**jsdoc + * Gets the injector gain at the server. + * @function Audio.getInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getInjectorGain(); + + /**jsdoc + * Sets the local injector gain in the client. + * Units are Decibels (dB) + * @function Audio.setLocalInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setLocalInjectorGain(float gain); + + /**jsdoc + * Gets the local injector gain in the client. + * @function Audio.getLocalInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getLocalInjectorGain(); + + /**jsdoc + * Sets the injector gain for system sounds. + * Units are Decibels (dB) + * @function Audio.setSystemInjectorGain + * @param {number} gain (in dB) + */ + Q_INVOKABLE void setSystemInjectorGain(float gain); + + /**jsdoc + * Gets the injector gain for system sounds. + * @function Audio.getSystemInjectorGain + * @returns {number} gain (in dB) + */ + Q_INVOKABLE float getSystemInjectorGain(); + /**jsdoc * Starts making an audio recording of the audio being played in-world (i.e., not local-only audio) to a file in WAV format. * @function Audio.startRecording - * @param {string} filename - The path and name of the file to make the recording in. Should have a .wav + * @param {string} filename - The path and name of the file to make the recording in. Should have a .wav * extension. The file is overwritten if it already exists. - * @returns {boolean} true if the specified file could be opened and audio recording has started, otherwise + * @returns {boolean} true if the specified file could be opened and audio recording has started, otherwise * false. * @example Make a 10 second audio recording. * var filename = File.getTempDir() + "/audio.wav"; @@ -152,13 +245,13 @@ public: * Audio.stopRecording(); * print("Audio recording made in: " + filename); * }, 10000); - * + * * } else { * print("Could not make an audio recording in: " + filename); * } */ Q_INVOKABLE bool startRecording(const QString& filename); - + /**jsdoc * Finish making an audio recording started with {@link Audio.startRecording|startRecording}. * @function Audio.stopRecording @@ -193,6 +286,46 @@ signals: */ void mutedChanged(bool isMuted); + /**jsdoc + * Triggered when desktop audio input is muted or unmuted. + * @function Audio.mutedDesktopChanged + * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. + * @returns {Signal} + */ + void mutedDesktopChanged(bool isMuted); + + /**jsdoc + * Triggered when HMD audio input is muted or unmuted. + * @function Audio.mutedHMDChanged + * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. + * @returns {Signal} + */ + void mutedHMDChanged(bool isMuted); + + /** + * Triggered when Push-to-Talk has been enabled or disabled. + * @function Audio.pushToTalkChanged + * @param {boolean} enabled - true if Push-to-Talk is enabled, otherwise false. + * @returns {Signal} + */ + void pushToTalkChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for desktop mode. + * @function Audio.pushToTalkDesktopChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for Desktop mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkDesktopChanged(bool enabled); + + /** + * Triggered when Push-to-Talk has been enabled or disabled for HMD mode. + * @function Audio.pushToTalkHMDChanged + * @param {boolean} enabled - true if Push-to-Talk is emabled for HMD mode, otherwise false. + * @returns {Signal} + */ + void pushToTalkHMDChanged(bool enabled); + /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged @@ -201,12 +334,20 @@ signals: */ void noiseReductionChanged(bool isEnabled); + /**jsdoc + * Triggered when "warn when muted" is enabled or disabled. + * @function Audio.warnWhenMutedChanged + * @param {boolean} isEnabled - true if "warn when muted" is enabled, otherwise false. + * @returns {Signal} + */ + void warnWhenMutedChanged(bool isEnabled); + /**jsdoc * Triggered when the input audio volume changes. * @function Audio.inputVolumeChanged - * @param {number} volume - The requested volume to be applied to the audio input, range 0.0 – - * 1.0. The resulting value of Audio.inputVolume depends on the capabilities of the device: - * for example, the volume can't be changed on some devices, and others might only support values of 0.0 + * @param {number} volume - The requested volume to be applied to the audio input, range 0.0 – + * 1.0. The resulting value of Audio.inputVolume depends on the capabilities of the device: + * for example, the volume can't be changed on some devices, and others might only support values of 0.0 * and 1.0. * @returns {Signal} */ @@ -215,7 +356,7 @@ signals: /**jsdoc * Triggered when the input audio level changes. * @function Audio.inputLevelChanged - * @param {number} level - The loudness of the input audio, range 0.0 (no sound) – 1.0 (the + * @param {number} level - The loudness of the input audio, range 0.0 (no sound) – 1.0 (the * onset of clipping). * @returns {Signal} */ @@ -237,6 +378,14 @@ signals: */ void contextChanged(const QString& context); + /**jsdoc + * Triggered when pushing to talk. + * @function Audio.pushingToTalkChanged + * @param {boolean} talking - true if broadcasting with PTT, false otherwise. + * @returns {Signal} + */ + void pushingToTalkChanged(bool talking); + public slots: /**jsdoc @@ -245,9 +394,12 @@ public slots: */ void onContextChanged(); + void handlePushedToTalk(bool enabled); + private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); + void enableWarnWhenMuted(bool enable); void setInputVolume(float volume); void onInputLoudnessChanged(float loudness, bool isClipping); @@ -259,12 +411,23 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; + float _localInjectorGain { 0.0f }; // in dB + float _systemInjectorGain { 0.0f }; // in dB bool _isClipping { false }; - bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. + bool _enableWarnWhenMuted { true }; bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; + Setting::Handle _mutedDesktopSetting{ QStringList { Audio::AUDIO, "mutedDesktop" }, true }; + Setting::Handle _mutedHMDSetting{ QStringList { Audio::AUDIO, "mutedHMD" }, true }; + Setting::Handle _pttDesktopSetting{ QStringList { Audio::AUDIO, "pushToTalkDesktop" }, false }; + Setting::Handle _pttHMDSetting{ QStringList { Audio::AUDIO, "pushToTalkHMD" }, false }; + bool _mutedDesktop{ true }; + bool _mutedHMD{ false }; + bool _pttDesktop{ false }; + bool _pttHMD{ false }; + bool _pushingToTalk{ false }; }; }; diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index 6589769ece..325e1ff649 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -66,7 +66,7 @@ void TTSScriptingInterface::updateLastSoundAudioInjector() { if (_lastSoundAudioInjector) { AudioInjectorOptions options; options.position = DependencyManager::get()->getMyAvatarPosition(); - _lastSoundAudioInjector->setOptions(options); + DependencyManager::get()->setOptions(_lastSoundAudioInjector, options); _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); } } @@ -143,7 +143,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { options.position = DependencyManager::get()->getMyAvatarPosition(); if (_lastSoundAudioInjector) { - _lastSoundAudioInjector->stop(); + DependencyManager::get()->stop(_lastSoundAudioInjector); _lastSoundAudioInjectorUpdateTimer.stop(); } @@ -151,7 +151,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { uint32_t numSamples = (uint32_t)_lastSoundByteArray.size() / sizeof(AudioData::AudioSample); auto samples = reinterpret_cast(_lastSoundByteArray.data()); auto newAudioData = AudioData::make(numSamples, numChannels, samples); - _lastSoundAudioInjector = AudioInjector::playSoundAndDelete(newAudioData, options); + _lastSoundAudioInjector = DependencyManager::get()->playSound(newAudioData, options, true); _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); #else @@ -161,7 +161,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { void TTSScriptingInterface::stopLastSpeech() { if (_lastSoundAudioInjector) { - _lastSoundAudioInjector->stop(); - _lastSoundAudioInjector = NULL; + DependencyManager::get()->stop(_lastSoundAudioInjector); + _lastSoundAudioInjector = nullptr; } } diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index a9ba165037..c3aeb2643b 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -199,13 +199,3 @@ void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) { int TestScriptingInterface::getOtherAvatarsReplicaCount() { return qApp->getOtherAvatarsReplicaCount(); } - -QString TestScriptingInterface::getOperatingSystemType() { -#ifdef Q_OS_WIN - return "WINDOWS"; -#elif defined Q_OS_MAC - return "MACOS"; -#else - return "UNKNOWN"; -#endif -} diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 26e967c9b5..4a1d1a3eeb 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -163,13 +163,6 @@ public slots: */ Q_INVOKABLE int getOtherAvatarsReplicaCount(); - /**jsdoc - * Returns the Operating Sytem type - * @function Test.getOperatingSystemType - * @returns {string} "WINDOWS", "MACOS" or "UNKNOWN" - */ - QString getOperatingSystemType(); - private: bool waitForCondition(qint64 maxWaitMs, std::function condition); QString _testResultsLocation; diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 0aa352de23..80604f354b 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "Application.h" #include "Menu.h" @@ -30,6 +31,10 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) { _showAudioTools = showAudioToolsSetting.get(); + auto nodeList = DependencyManager::get(); + auto usersScriptingInterface = DependencyManager::get(); + connect(nodeList.data(), &NodeList::ignoreRadiusEnabledChanged, this, &AvatarInputs::ignoreRadiusEnabledChanged); + connect(usersScriptingInterface.data(), &UsersScriptingInterface::enteredIgnoreRadius, this, &AvatarInputs::enteredIgnoreRadiusChanged); } #define AI_UPDATE(name, src) \ @@ -83,6 +88,10 @@ void AvatarInputs::setShowAudioTools(bool showAudioTools) { emit showAudioToolsChanged(_showAudioTools); } +bool AvatarInputs::getIgnoreRadiusEnabled() const { + return DependencyManager::get()->getIgnoreRadiusEnabled(); +} + void AvatarInputs::toggleCameraMute() { FaceTracker* faceTracker = qApp->getSelectedFaceTracker(); if (faceTracker) { diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 6569792807..f53adc1749 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -42,6 +42,8 @@ class AvatarInputs : public QObject { AI_PROPERTY(bool, isHMD, false) Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) + Q_PROPERTY(bool ignoreRadiusEnabled READ getIgnoreRadiusEnabled NOTIFY ignoreRadiusEnabledChanged) + //Q_PROPERTY(bool enteredIgnoreRadius READ getEnteredIgnoreRadius NOTIFY enteredIgnoreRadiusChanged) public: static AvatarInputs* getInstance(); @@ -55,7 +57,9 @@ public: AvatarInputs(QObject* parent = nullptr); void update(); - bool showAudioTools() const { return _showAudioTools; } + bool showAudioTools() const { return _showAudioTools; } + bool getIgnoreRadiusEnabled() const; + //bool getEnteredIgnoreRadius() const; public slots: @@ -93,6 +97,34 @@ signals: */ void showAudioToolsChanged(bool show); + /**jsdoc + * @function AvatarInputs.avatarEnteredIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarEnteredIgnoreRadius(QUuid avatarID); + + /**jsdoc + * @function AvatarInputs.avatarLeftIgnoreRadius + * @param {QUuid} avatarID + * @returns {Signal} + */ + void avatarLeftIgnoreRadius(QUuid avatarID); + + /**jsdoc + * @function AvatarInputs.ignoreRadiusEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void ignoreRadiusEnabledChanged(bool enabled); + + /**jsdoc + * @function AvatarInputs.enteredIgnoreRadiusChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void enteredIgnoreRadiusChanged(); + protected: /**jsdoc @@ -106,6 +138,8 @@ protected: Q_INVOKABLE void toggleCameraMute(); private: + void onAvatarEnteredIgnoreRadius(); + void onAvatarLeftIgnoreRadius(); float _trailingAudioLoudness{ 0 }; bool _showAudioTools { false }; }; diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index 9b75f78e67..1cbe31f1eb 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include @@ -537,7 +537,7 @@ void Keyboard::handleTriggerBegin(const QUuid& id, const PointerEvent& event) { audioOptions.position = keyWorldPosition; audioOptions.volume = 0.05f; - AudioInjector::playSoundAndDelete(_keySound, audioOptions); + DependencyManager::get()->playSound(_keySound, audioOptions, true); int scanCode = key.getScanCode(_capsEnabled); QString keyString = key.getKeyString(_capsEnabled); diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index b3358e486d..51e5e0571f 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -19,9 +19,9 @@ #include #include #include +#include #include #include -#include #include #include diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index e3697ee8ec..022b57c0d9 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -125,8 +125,10 @@ void Stats::updateStats(bool force) { auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves STAT_UPDATE(avatarCount, avatarManager->size() - 1); + STAT_UPDATE(heroAvatarCount, avatarManager->getNumHeroAvatars()); STAT_UPDATE(physicsObjectCount, qApp->getNumCollisionObjects()); STAT_UPDATE(updatedAvatarCount, avatarManager->getNumAvatarsUpdated()); + STAT_UPDATE(updatedHeroAvatarCount, avatarManager->getNumHeroAvatarsUpdated()); STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated()); STAT_UPDATE(serverCount, (int)nodeList->size()); STAT_UPDATE_FLOAT(renderrate, qApp->getRenderLoopRate(), 0.1f); @@ -264,6 +266,11 @@ void Stats::updateStats(bool force) { } STAT_UPDATE(audioCodec, audioClient->getSelectedAudioFormat()); STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); + { + int localInjectors = audioClient->getNumLocalInjectors(); + size_t nonLocalInjectors = DependencyManager::get()->getNumInjectors(); + STAT_UPDATE(audioInjectors, QVector2D(localInjectors, nonLocalInjectors)); + } STAT_UPDATE(entityPacketsInKbps, octreeServerCount ? totalEntityKbps / octreeServerCount : -1); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 36e92b00af..3134b223d6 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -49,8 +49,10 @@ private: \ * @property {number} presentdroprate - Read-only. * @property {number} gameLoopRate - Read-only. * @property {number} avatarCount - Read-only. + * @property {number} heroAvatarCount - Read-only. * @property {number} physicsObjectCount - Read-only. * @property {number} updatedAvatarCount - Read-only. + * @property {number} updatedHeroAvatarCount - Read-only. * @property {number} notUpdatedAvatarCount - Read-only. * @property {number} packetInCount - Read-only. * @property {number} packetOutCount - Read-only. @@ -85,6 +87,7 @@ private: \ * @property {number} audioPacketLoss - Read-only. * @property {string} audioCodec - Read-only. * @property {string} audioNoiseGate - Read-only. + * @property {Vec2} audioInjectors - Read-only. * @property {number} entityPacketsInKbps - Read-only. * * @property {number} downloads - Read-only. @@ -203,8 +206,10 @@ class Stats : public QQuickItem { STATS_PROPERTY(float, presentdroprate, 0) STATS_PROPERTY(int, gameLoopRate, 0) STATS_PROPERTY(int, avatarCount, 0) + STATS_PROPERTY(int, heroAvatarCount, 0) STATS_PROPERTY(int, physicsObjectCount, 0) STATS_PROPERTY(int, updatedAvatarCount, 0) + STATS_PROPERTY(int, updatedHeroAvatarCount, 0) STATS_PROPERTY(int, notUpdatedAvatarCount, 0) STATS_PROPERTY(int, packetInCount, 0) STATS_PROPERTY(int, packetOutCount, 0) @@ -239,6 +244,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioPacketLoss, 0) STATS_PROPERTY(QString, audioCodec, QString()) STATS_PROPERTY(QString, audioNoiseGate, QString()) + STATS_PROPERTY(QVector2D, audioInjectors, QVector2D()); STATS_PROPERTY(int, entityPacketsInKbps, 0) STATS_PROPERTY(int, downloads, 0) @@ -436,6 +442,13 @@ signals: */ void avatarCountChanged(); + /**jsdoc + * Triggered when the value of the heroAvatarCount property changes. + * @function Stats.heroAvatarCountChanged + * @returns {Signal} + */ + void heroAvatarCountChanged(); + /**jsdoc * Triggered when the value of the updatedAvatarCount property changes. * @function Stats.updatedAvatarCountChanged @@ -443,6 +456,13 @@ signals: */ void updatedAvatarCountChanged(); + /**jsdoc + * Triggered when the value of the updatedHeroAvatarCount property changes. + * @function Stats.updatedHeroAvatarCountChanged + * @returns {Signal} + */ + void updatedHeroAvatarCountChanged(); + /**jsdoc * Triggered when the value of the notUpdatedAvatarCount property changes. * @function Stats.notUpdatedAvatarCountChanged @@ -674,6 +694,13 @@ signals: */ void audioNoiseGateChanged(); + /**jsdoc + * Triggered when the value of the audioInjectors property changes. + * @function Stats.audioInjectorsChanged + * @returns {Signal} + */ + void audioInjectorsChanged(); + /**jsdoc * Triggered when the value of the entityPacketsInKbps property changes. * @function Stats.entityPacketsInKbpsChanged diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index e5cec70f64..1c8a9019ea 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -292,7 +292,7 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID setLastInspectedEntity(entityID); - EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_lastInspectedEntity, _entityPropertyFlags); + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityID, _entityPropertyFlags); auto nodeList = DependencyManager::get(); @@ -328,31 +328,31 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID } else { QString ownerKey = jsonObject["transfer_recipient_key"].toString(); - QByteArray certID = entityProperties.getCertificateID().toUtf8(); - QByteArray text = DependencyManager::get()->getTree()->computeNonce(certID, ownerKey); + QByteArray id = entityID.toByteArray(); + QByteArray text = DependencyManager::get()->getTree()->computeNonce(entityID, ownerKey); QByteArray nodeToChallengeByteArray = entityProperties.getOwningAvatarID().toRfc4122(); - int certIDByteArraySize = certID.length(); + int idByteArraySize = id.length(); int textByteArraySize = text.length(); int nodeToChallengeByteArraySize = nodeToChallengeByteArray.length(); auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnershipRequest, - certIDByteArraySize + textByteArraySize + nodeToChallengeByteArraySize + 3 * sizeof(int), + idByteArraySize + textByteArraySize + nodeToChallengeByteArraySize + 3 * sizeof(int), true); - challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(idByteArraySize); challengeOwnershipPacket->writePrimitive(textByteArraySize); challengeOwnershipPacket->writePrimitive(nodeToChallengeByteArraySize); - challengeOwnershipPacket->write(certID); + challengeOwnershipPacket->write(id); challengeOwnershipPacket->write(text); challengeOwnershipPacket->write(nodeToChallengeByteArray); nodeList->sendPacket(std::move(challengeOwnershipPacket), *entityServer); // Kickoff a 10-second timeout timer that marks the cert if we don't get an ownership response in time if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer"); + QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer", Q_ARG(const EntityItemID&, entityID)); return; } else { - startChallengeOwnershipTimer(); + startChallengeOwnershipTimer(entityID); } } } else { @@ -370,14 +370,14 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID // so they always pass Ownership Verification. It's necessary to emit this signal // so that the Inspection Certificate can continue its information-grabbing process. auto ledger = DependencyManager::get(); - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); + emit ledger->updateCertificateStatus(entityID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); } } else { auto ledger = DependencyManager::get(); _challengeOwnershipTimeoutTimer.stop(); - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED)); - emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); - qCDebug(context_overlay) << "Entity" << _lastInspectedEntity << "failed static certificate verification!"; + emit ledger->updateCertificateStatus(entityID, (uint)(ledger->CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED)); + emit DependencyManager::get()->ownershipVerificationFailed(entityID); + qCDebug(context_overlay) << "Entity" << entityID << "failed static certificate verification!"; } } @@ -395,14 +395,14 @@ void ContextOverlayInterface::deletingEntity(const EntityItemID& entityID) { } } -void ContextOverlayInterface::startChallengeOwnershipTimer() { +void ContextOverlayInterface::startChallengeOwnershipTimer(const EntityItemID& entityItemID) { auto ledger = DependencyManager::get(); - EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_lastInspectedEntity, _entityPropertyFlags); + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); connect(&_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() { - qCDebug(entities) << "Ownership challenge timed out for" << _lastInspectedEntity; - emit ledger->updateCertificateStatus(entityProperties.getCertificateID(), (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_TIMEOUT)); - emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); + qCDebug(entities) << "Ownership challenge timed out for" << entityItemID; + emit ledger->updateCertificateStatus(entityItemID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_TIMEOUT)); + emit DependencyManager::get()->ownershipVerificationFailed(entityItemID); }); _challengeOwnershipTimeoutTimer.start(5000); @@ -413,23 +413,22 @@ void ContextOverlayInterface::handleChallengeOwnershipReplyPacket(QSharedPointer _challengeOwnershipTimeoutTimer.stop(); - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; - packet->readPrimitive(&certIDByteArraySize); + packet->readPrimitive(&idByteArraySize); packet->readPrimitive(&textByteArraySize); - QString certID(packet->read(certIDByteArraySize)); + EntityItemID id(packet->read(idByteArraySize)); QString text(packet->read(textByteArraySize)); - EntityItemID id; - bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(certID, text, id); + bool verificationSuccess = DependencyManager::get()->getTree()->verifyNonce(id, text); if (verificationSuccess) { - emit ledger->updateCertificateStatus(certID, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); - emit DependencyManager::get()->ownershipVerificationSuccess(_lastInspectedEntity); + emit ledger->updateCertificateStatus(id, (uint)(ledger->CERTIFICATE_STATUS_VERIFICATION_SUCCESS)); + emit DependencyManager::get()->ownershipVerificationSuccess(id); } else { - emit ledger->updateCertificateStatus(certID, (uint)(ledger->CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED)); - emit DependencyManager::get()->ownershipVerificationFailed(_lastInspectedEntity); + emit ledger->updateCertificateStatus(id, (uint)(ledger->CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED)); + emit DependencyManager::get()->ownershipVerificationFailed(id); } } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index b1b62aa0c4..e688d1c115 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -46,7 +46,7 @@ public: ContextOverlayInterface(); Q_INVOKABLE QUuid getCurrentEntityWithContextOverlay() { return _currentEntityWithContextOverlay; } void setCurrentEntityWithContextOverlay(const QUuid& entityID) { _currentEntityWithContextOverlay = entityID; } - void setLastInspectedEntity(const QUuid& entityID) { _challengeOwnershipTimeoutTimer.stop(); _lastInspectedEntity = entityID; } + void setLastInspectedEntity(const QUuid& entityID) { _challengeOwnershipTimeoutTimer.stop(); } void setEnabled(bool enabled); bool getEnabled() { return _enabled; } bool getIsInMarketplaceInspectionMode() { return _isInMarketplaceInspectionMode; } @@ -83,7 +83,6 @@ private: EntityItemID _mouseDownEntity; quint64 _mouseDownEntityTimestamp; EntityItemID _currentEntityWithContextOverlay; - EntityItemID _lastInspectedEntity; QString _entityMarketplaceID; bool _contextOverlayJustClicked { false }; @@ -94,7 +93,7 @@ private: void deletingEntity(const EntityItemID& entityItemID); - Q_INVOKABLE void startChallengeOwnershipTimer(); + Q_INVOKABLE void startChallengeOwnershipTimer(const EntityItemID& entityItemID); QTimer _challengeOwnershipTimeoutTimer; }; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 1bcb040a77..2ade5edda5 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -1711,9 +1711,9 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if * parentID is an avatar skeleton. A value of 65535 means "no joint". * - * @property {boolean} isFacingAvatar - If true< / code>, the overlay is rotated to face the user's camera about an axis + * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis * parallel to the user's avatar's "up" direction. - * @property {string} text="" - The text to display.Text does not automatically wrap; use \n< / code> for a line break. + * @property {string} text="" - The text to display.Text does not automatically wrap; use \n for a line break. * @property {number} textAlpha=1 - The text alpha value. * @property {Color} backgroundColor=0,0,0 - The background color. * @property {number} backgroundAlpha=0.7 - The background alpha value. @@ -1876,7 +1876,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a - * parentID set, otherwise the same value as rotation. Synonym: localOrientation. + * parentID set, otherwise the same value as rotation. Synonym: localOrientation. * @property {boolean} ignorePickIntersection=false - If true, picks ignore the overlay. ignoreRayIntersection is a synonym. * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of objects in the world, but behind the HUD. * @property {boolean} drawHUDLayer=false - If true, the overlay is rendered in front of everything, including the HUD. @@ -1916,7 +1916,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a - * parentID set, otherwise the same value as rotation. Synonym: localOrientation. + * parentID set, otherwise the same value as rotation. Synonym: localOrientation. * @property {boolean} isSolid=false - Synonyms: solid, isFilled, and filled. * Antonyms: isWire and wire. * @property {boolean} ignorePickIntersection=false - If true, picks ignore the overlay. ignoreRayIntersection is a synonym. @@ -1927,46 +1927,46 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if * parentID is an avatar skeleton. A value of 65535 means "no joint". * - * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at, in degrees. - * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at, in degrees. - * @property {number} outerRadius = 1 - The outer radius of the overlay, in meters.Synonym: radius< / code>. - * @property {number} innerRadius = 0 - The inner radius of the overlay, in meters. - * @property {Color} color = 255, 255, 255 - The color of the overlay.Setting this value also sets the values of - * innerStartColor< / code>, innerEndColor< / code>, outerStartColor< / code>, and outerEndColor< / code>. - * @property {Color} startColor - Sets the values of innerStartColor< / code> and outerStartColor< / code>. - * Write - only.< / em> - * @property {Color} endColor - Sets the values of innerEndColor< / code> and outerEndColor< / code>. - * Write - only.< / em> - * @property {Color} innerColor - Sets the values of innerStartColor< / code> and innerEndColor< / code>. - * Write - only.< / em> - * @property {Color} outerColor - Sets the values of outerStartColor< / code> and outerEndColor< / code>. - * Write - only.< / em> + * @property {number} startAt = 0 - The counter - clockwise angle from the overlay's x-axis that drawing starts at in degrees. + * @property {number} endAt = 360 - The counter - clockwise angle from the overlay's x-axis that drawing ends at in degrees. + * @property {number} outerRadius = 1 - The outer radius of the overlay in meters. Synonym: radius. + * @property {number} innerRadius = 0 - The inner radius of the overlay in meters. + * @property {Color} color = 255, 255, 255 - The color of the overlay. Setting this value also sets the values of + * innerStartColor, innerEndColor, outerStartColor, and outerEndColor. + * @property {Color} startColor - Sets the values of innerStartColor and outerStartColor. + * Write - only. + * @property {Color} endColor - Sets the values of innerEndColor and outerEndColor. + * Write - only. + * @property {Color} innerColor - Sets the values of innerStartColor and innerEndColor. + * Write - only. + * @property {Color} outerColor - Sets the values of outerStartColor and outerEndColor. + * Write - only. * @property {Color} innerStartcolor - The color at the inner start point of the overlay. * @property {Color} innerEndColor - The color at the inner end point of the overlay. * @property {Color} outerStartColor - The color at the outer start point of the overlay. * @property {Color} outerEndColor - The color at the outer end point of the overlay. - * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0< / code> -1.0< / code>.Setting this value also sets - * the values of innerStartAlpha< / code>, innerEndAlpha< / code>, outerStartAlpha< / code>, and - * outerEndAlpha< / code>.Synonym: Alpha< / code>; write - only< / em>. - * @property {number} startAlpha - Sets the values of innerStartAlpha< / code> and outerStartAlpha< / code>. - * Write - only.< / em> - * @property {number} endAlpha - Sets the values of innerEndAlpha< / code> and outerEndAlpha< / code>. - * Write - only.< / em> - * @property {number} innerAlpha - Sets the values of innerStartAlpha< / code> and innerEndAlpha< / code>. - * Write - only.< / em> - * @property {number} outerAlpha - Sets the values of outerStartAlpha< / code> and outerEndAlpha< / code>. - * Write - only.< / em> + * @property {number} alpha = 0.5 - The opacity of the overlay, 0.0 -1.0. Setting this value also sets + * the values of innerStartAlpha, innerEndAlpha, outerStartAlpha, and + * outerEndAlpha. Synonym: Alpha; write - only. + * @property {number} startAlpha - Sets the values of innerStartAlpha and outerStartAlpha. + * Write - only. + * @property {number} endAlpha - Sets the values of innerEndAlpha and outerEndAlpha. + * Write - only. + * @property {number} innerAlpha - Sets the values of innerStartAlpha and innerEndAlpha. + * Write - only. + * @property {number} outerAlpha - Sets the values of outerStartAlpha and outerEndAlpha. + * Write - only. * @property {number} innerStartAlpha = 0 - The alpha at the inner start point of the overlay. * @property {number} innerEndAlpha = 0 - The alpha at the inner end point of the overlay. * @property {number} outerStartAlpha = 0 - The alpha at the outer start point of the overlay. * @property {number} outerEndAlpha = 0 - The alpha at the outer end point of the overlay. * - * @property {boolean} hasTickMarks = false - If true< / code>, tick marks are drawn. + * @property {boolean} hasTickMarks = false - If true, tick marks are drawn. * @property {number} majorTickMarksAngle = 0 - The angle between major tick marks, in degrees. * @property {number} minorTickMarksAngle = 0 - The angle between minor tick marks, in degrees. - * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters.A positive value draws tick marks + * @property {number} majorTickMarksLength = 0 - The length of the major tick marks, in meters. A positive value draws tick marks * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. - * @property {number} minorTickMarksLength = 0 - The length of the minor tick marks, in meters.A positive value draws tick marks + * @property {number} minorTickMarksLength = 0 - The length of the minor tick marks, in meters. A positive value draws tick marks * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. * @property {Color} majorTickMarksColor = 0, 0, 0 - The color of the major tick marks. * @property {Color} minorTickMarksColor = 0, 0, 0 - The color of the minor tick marks. diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 4fe02e9307..1a922e507d 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -124,41 +124,45 @@ void AnimClip::copyFromNetworkAnim() { _anim.resize(animFrameCount); // find the size scale factor for translation in the animation. - const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarSkeleton->nameToJointIndex("Hips")); - const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); - const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarSkeleton->nameToJointIndex("Hips")); - const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animSkeleton.nameToJointIndex("Hips")); - - // the get the units and the heights for the animation and the avatar - const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).y; - const float animationUnitScale = extractScale(animModel.offset).y; - const float avatarHeightInMeters = avatarUnitScale * avatarHipsAbsoluteDefaultPose.trans().y; - const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; - - // get the parent scales for the avatar and the animation - float avatarHipsParentScale = 1.0f; - if (avatarHipsParentIndex >= 0) { - const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); - avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; - } - float animHipsParentScale = 1.0f; - if (animHipsParentIndex >= 0) { - const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); - animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; - } - - const float EPSILON = 0.0001f; float boneLengthScale = 1.0f; - // compute the ratios for the units, the heights in meters, and the parent scales - if ((fabsf(animHeightInMeters) > EPSILON) && (animationUnitScale > EPSILON) && (animHipsParentScale > EPSILON)) { - const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; - const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); - const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); + const int avatarHipsIndex = avatarSkeleton->nameToJointIndex("Hips"); + const int animHipsIndex = animSkeleton.nameToJointIndex("Hips"); + if (avatarHipsIndex != -1 && animHipsIndex != -1) { + const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarHipsIndex); + const int animHipsParentIndex = animSkeleton.getParentIndex(animHipsIndex); - boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsIndex); + const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsIndex); + + // the get the units and the heights for the animation and the avatar + const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).y; + const float animationUnitScale = extractScale(animModel.offset).y; + const float avatarHeightInMeters = avatarUnitScale * avatarHipsAbsoluteDefaultPose.trans().y; + const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; + + // get the parent scales for the avatar and the animation + float avatarHipsParentScale = 1.0f; + if (avatarHipsParentIndex != -1) { + const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); + avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; + } + float animHipsParentScale = 1.0f; + if (animHipsParentIndex != -1) { + const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); + animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; + } + + const float EPSILON = 0.0001f; + // compute the ratios for the units, the heights in meters, and the parent scales + if ((fabsf(animHeightInMeters) > EPSILON) && (animationUnitScale > EPSILON) && (animHipsParentScale > EPSILON)) { + const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; + const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); + const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); + + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + } } - for (int frame = 0; frame < animFrameCount; frame++) { const HFMAnimationFrame& animFrame = animModel.animationFrames[frame]; diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 0136b7d125..6d58ecedec 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -59,6 +59,46 @@ public: float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + /**jsdoc + *

Specifies the initial conditions of the IK solver.

+ * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0RelaxToUnderPosesThis is a blend: it is 15/16 PreviousSolution + * and 1/16 UnderPoses. This provides some of the benefits of using UnderPoses so that the + * underlying animation is still visible, while at the same time converging faster then using the + * UnderPoses as the only initial solution.
1RelaxToLimitCenterPosesThis is a blend: it is 15/16 + * PreviousSolution and 1/16 LimitCenterPoses. This should converge quickly because it is + * close to the previous solution, but still provides the benefits of avoiding limb locking.
2PreviousSolutionThe IK system will begin to solve from the same position and + * orientations for each joint that was the result from the previous frame.
+ * Pros: As the end effectors typically do not move much from frame to frame, this is likely to converge quickly + * to a valid solution.
+ * Cons: If the previous solution resulted in an awkward or uncomfortable posture, the next frame will also be + * awkward and uncomfortable. It can also result in locked elbows and knees.
3UnderPosesThe IK occurs at one of the top-most layers. It has access to the + * full posture that was computed via canned animations and blends. We call this animated set of poses the "under + * pose". The under poses are what would be visible if IK was completely disabled. Using the under poses as the + * initial conditions of the CCD solve will cause some of the animated motion to be blended into the result of the + * IK. This can result in very natural results, especially if there are only a few IK targets enabled. On the other + * hand, because the under poses might be quite far from the desired end effector, it can converge slowly in some + * cases, causing it to never reach the IK target in the allotted number of iterations. Also, in situations where all + * of the IK targets are being controlled by external sensors, sometimes starting from the under poses can cause + * awkward motions from the underlying animations to leak into the IK result.
4LimitCenterPosesThis pose is taken to be the center of all the joint + * constraints. This can prevent the IK solution from getting locked or stuck at a particular constraint. For + * example, if the arm is pointing straight outward from the body, as the end effector moves towards the body, at + * some point the elbow should bend to accommodate. However, because the CCD solver is stuck at a local maximum, it + * will not rotate the elbow, unless the initial conditions already have the elbow bent, which is the case for + * LimitCenterPoses. When all the IK targets are enabled, this result will provide a consistent starting + * point for each IK solve, hopefully resulting in a consistent, natural result.
+ * @typedef {number} MyAvatar.AnimIKSolutionSource + */ enum class SolutionSource { RelaxToUnderPoses = 0, RelaxToLimitCenterPoses, diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 70929bd4e4..1ad4e100db 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -24,6 +24,37 @@ class AnimOverlay : public AnimNode { public: friend class AnimTests; + /**jsdoc + *

Specifies sets of joints.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0FullBodyBoneSetAll joints.
1UpperBodyBoneSetOnly the "Spine" joint and its children.
2LowerBodyBoneSetOnly the leg joints and their children.
3LeftArmBoneSetJoints that are the children of the "LeftShoulder" + * joint.
4RightArmBoneSetJoints that are the children of the "RightShoulder" + * joint.
5AboveTheHeadBoneSetJoints that are the children of the "Head" + * joint.
6BelowTheHeadBoneSetJoints that are NOT the children of the "head" + * joint.
7HeadOnlyBoneSetThe "Head" joint.
8SpineOnlyBoneSetThe "Spine" joint.
9EmptyBoneSetNo joints.
10LeftHandBoneSetjoints that are the children of the "LeftHand" + * joint.
11RightHandBoneSetJoints that are the children of the "RightHand" + * joint.
12HipsOnlyBoneSetThe "Hips" joint.
13BothFeetBoneSetThe "LeftFoot" and "RightFoot" joints.
+ * @typedef {number} MyAvatar.AnimOverlayBoneSet + */ enum BoneSet { FullBodyBoneSet = 0, UpperBodyBoneSet, diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 5bc2021e5e..1f9e72bf28 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -67,17 +67,23 @@ void FlowCollisionSystem::addCollisionSphere(int jointIndex, const FlowCollision auto collision = FlowCollisionSphere(jointIndex, settings, isTouch); collision.setPosition(position); if (isSelfCollision) { - _selfCollisions.push_back(collision); + if (!isTouch) { + _selfCollisions.push_back(collision); + } else { + _selfTouchCollisions.push_back(collision); + } } else { _othersCollisions.push_back(collision); } - }; + void FlowCollisionSystem::resetCollisions() { _allCollisions.clear(); _othersCollisions.clear(); + _selfTouchCollisions.clear(); _selfCollisions.clear(); } + FlowCollisionResult FlowCollisionSystem::computeCollision(const std::vector collisions) { FlowCollisionResult result; if (collisions.size() > 1) { @@ -106,6 +112,10 @@ void FlowCollisionSystem::setScale(float scale) { _selfCollisions[j]._radius = _selfCollisions[j]._initialRadius * scale; _selfCollisions[j]._offset = _selfCollisions[j]._initialOffset * scale; } + for (size_t j = 0; j < _selfTouchCollisions.size(); j++) { + _selfTouchCollisions[j]._radius = _selfTouchCollisions[j]._initialRadius * scale; + _selfTouchCollisions[j]._offset = _selfTouchCollisions[j]._initialOffset * scale; + } }; std::vector FlowCollisionSystem::checkFlowThreadCollisions(FlowThread* flowThread) { @@ -178,9 +188,9 @@ void FlowCollisionSystem::setCollisionSettingsByJoint(int jointIndex, const Flow } void FlowCollisionSystem::prepareCollisions() { _allCollisions.clear(); - _allCollisions.resize(_selfCollisions.size() + _othersCollisions.size()); - std::copy(_selfCollisions.begin(), _selfCollisions.begin() + _selfCollisions.size(), _allCollisions.begin()); - std::copy(_othersCollisions.begin(), _othersCollisions.begin() + _othersCollisions.size(), _allCollisions.begin() + _selfCollisions.size()); + _allCollisions.insert(_allCollisions.end(), _selfCollisions.begin(), _selfCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _othersCollisions.begin(), _othersCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _selfTouchCollisions.begin(), _selfTouchCollisions.end()); _othersCollisions.clear(); } @@ -273,18 +283,20 @@ void FlowJoint::setRecoveryPosition(const glm::vec3& recoveryPosition) { } void FlowJoint::update(float deltaTime) { - glm::vec3 accelerationOffset = glm::vec3(0.0f); - if (_settings._stiffness > 0.0f) { - glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; - float recoveryFactor = powf(_settings._stiffness, 3.0f); - accelerationOffset = recoveryVector * recoveryFactor; - } - FlowNode::update(deltaTime, accelerationOffset); - if (_anchored) { - if (!_isHelper) { - _currentPosition = _updatedPosition; - } else { - _currentPosition = _parentPosition; + if (_settings._active) { + glm::vec3 accelerationOffset = glm::vec3(0.0f); + if (_settings._stiffness > 0.0f) { + glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; + float recoveryFactor = powf(_settings._stiffness, 3.0f); + accelerationOffset = recoveryVector * recoveryFactor; + } + FlowNode::update(deltaTime, accelerationOffset); + if (_anchored) { + if (!_isHelper) { + _currentPosition = _updatedPosition; + } else { + _currentPosition = _parentPosition; + } } } }; @@ -674,6 +686,14 @@ bool Flow::updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t thr return true; } +void Flow::updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses) { + glm::quat jointRotation; + getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); + getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); + glm::vec3 worldOffset = jointRotation * collision._offset; + collision._position = collision._position + worldOffset; +} + void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) { updateAbsolutePoses(relativePoses, absolutePoses); for (auto &jointData : _flowJointData) { @@ -695,11 +715,11 @@ void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) } auto &selfCollisions = _collisionSystem.getSelfCollisions(); for (auto &collision : selfCollisions) { - glm::quat jointRotation; - getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); - getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); - glm::vec3 worldOffset = jointRotation * collision._offset; - collision._position = collision._position + worldOffset; + updateCollisionJoint(collision, absolutePoses); + } + auto &selfTouchCollisions = _collisionSystem.getSelfTouchCollisions(); + for (auto &collision : selfTouchCollisions) { + updateCollisionJoint(collision, absolutePoses); } _collisionSystem.prepareCollisions(); } @@ -710,7 +730,7 @@ void Flow::setJoints(AnimPoseVec& relativePoses, const std::vector& overri for (int jointIndex : joints) { auto &joint = _flowJointData[jointIndex]; if (jointIndex >= 0 && jointIndex < (int)relativePoses.size() && !overrideFlags[jointIndex]) { - relativePoses[jointIndex].rot() = joint.getCurrentRotation(); + relativePoses[jointIndex].rot() = joint.getSettings()._active ? joint.getCurrentRotation() : joint.getInitialRotation(); } } } diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h index ad81c2be77..5dc1a3ba3e 100644 --- a/libraries/animation/src/Flow.h +++ b/libraries/animation/src/Flow.h @@ -140,6 +140,7 @@ public: std::vector checkFlowThreadCollisions(FlowThread* flowThread); std::vector& getSelfCollisions() { return _selfCollisions; }; + std::vector& getSelfTouchCollisions() { return _selfTouchCollisions; }; void setOthersCollisions(const std::vector& othersCollisions) { _othersCollisions = othersCollisions; } void prepareCollisions(); void resetCollisions(); @@ -150,9 +151,11 @@ public: void setActive(bool active) { _active = active; } bool getActive() const { return _active; } const std::vector& getCollisions() const { return _selfCollisions; } + void clearSelfCollisions() { _selfCollisions.clear(); } protected: std::vector _selfCollisions; std::vector _othersCollisions; + std::vector _selfTouchCollisions; std::vector _allCollisions; float _scale { 1.0f }; bool _active { false }; @@ -210,7 +213,7 @@ public: bool isHelper() const { return _isHelper; } const FlowPhysicsSettings& getSettings() { return _settings; } - void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; } + void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; _initialRadius = _settings._radius; } const glm::vec3& getCurrentPosition() const { return _currentPosition; } int getIndex() const { return _index; } @@ -222,6 +225,7 @@ public: const glm::quat& getCurrentRotation() const { return _currentRotation; } const glm::vec3& getCurrentTranslation() const { return _initialTranslation; } const glm::vec3& getInitialPosition() const { return _initialPosition; } + const glm::quat& getInitialRotation() const { return _initialRotation; } bool isColliding() const { return _colliding; } protected: @@ -297,6 +301,7 @@ public: void setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings); const std::map& getGroupSettings() const { return _groupSettings; } void cleanUp(); + void updateScale() { setScale(_scale); } signals: void onCleanup(); @@ -311,6 +316,7 @@ private: void setJoints(AnimPoseVec& relativePoses, const std::vector& overrideFlags); void updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); + void updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses); bool updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex); void updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings); void setScale(float scale); diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 57eaff9c30..564dba7f05 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -16,6 +16,27 @@ const float HACK_HMD_TARGET_WEIGHT = 8.0f; class IKTarget { public: + /**jsdoc + *

An IK target type.

+ * + * + * + * + * + * + * + * + * + * + * + * + *
ValueName

Description
0RotationAndPositionAttempt to reach the rotation and position end + * effector.
1RotationOnlyAttempt to reach the end effector rotation only.
2HmdHeadDeprecated: A special mode of IK that would attempt + * to prevent unnecessary bending of the spine.
3HipsRelativeRotationAndPositionAttempt to reach a rotation and position end + * effector that is not in absolute rig coordinates but is offset by the avatar hips translation.
4SplineUse a cubic Hermite spline to model the human spine. This prevents + * kinks in the spine and allows for a small amount of stretch and squash.
5UnknownIK is disabled.
+ * @typedef {number} MyAvatar.IKTargetType + */ enum class Type { RotationAndPosition, RotationOnly, diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 07bdfde189..43e94d23e8 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -88,6 +88,218 @@ static const QString MAIN_STATE_MACHINE_RIGHT_HAND_ROTATION("mainStateMachineRig static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRightHandPosition"); +/**jsdoc + *

An AnimStateDictionary object may have the following properties. It may also have other properties, set by + * scripts.

+ *

Warning: These properties are subject to change. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameType

Description
userAnimNonebooleantrue when no user overrideAnimation is + * playing.
userAnimAbooleantrue when a user overrideAnimation is + * playing.
userAnimBbooleantrue when a user overrideAnimation is + * playing.
sinenumberOscillating sine wave.
moveForwardSpeednumberControls the blend between the various forward walking + * & running animations.
moveBackwardSpeednumberControls the blend between the various backward walking + * & running animations.
moveLateralSpeednumberControls the blend between the various sidestep walking + * & running animations.
isMovingForwardbooleantrue if the avatar is moving + * forward.
isMovingBackwardbooleantrue if the avatar is moving + * backward.
isMovingRightbooleantrue if the avatar is moving to the + * right.
isMovingLeftbooleantrue if the avatar is moving to the + * left.
isMovingRightHmdbooleantrue if the avatar is moving to the right + * while the user is in HMD mode.
isMovingLeftHmdbooleantrue if the avatar is moving to the left while + * the user is in HMD mode.
isNotMovingbooleantrue if the avatar is stationary.
isTurningRightbooleantrue if the avatar is turning + * clockwise.
isTurningLeftbooleantrue if the avatar is turning + * counter-clockwise.
isNotTurningbooleantrue if the avatar is not turning.
isFlyingbooleantrue if the avatar is flying.
isNotFlyingbooleantrue if the avatar is not flying.
isTakeoffStandbooleantrue if the avatar is about to execute a + * standing jump.
isTakeoffRunbooleantrue if the avatar is about to execute a running + * jump.
isNotTakeoffbooleantrue if the avatar is not jumping.
isInAirStandbooleantrue if the avatar is in the air after a standing + * jump.
isInAirRunbooleantrue if the avatar is in the air after a running + * jump.
isNotInAirbooleantrue if the avatar on the ground.
inAirAlphanumberUsed to interpolate between the up, apex, and down in-air + * animations.
ikOverlayAlphanumberThe blend between upper body and spline IK versus the + * underlying animation
headPosition{@link Vec3}The desired position of the Head joint in + * rig coordinates.
headRotation{@link Quat}The desired orientation of the Head joint in + * rig coordinates.
headType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * head.
headWeightnumberHow strongly the head chain blends with the other IK + * chains.
leftHandPosition{@link Vec3}The desired position of the LeftHand + * joint in rig coordinates.
leftHandRotation{@link Quat}The desired orientation of the LeftHand + * joint in rig coordinates.
leftHandType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * left arm.
leftHandPoleVectorEnabledbooleanWhen true, the elbow angle is + * controlled by the rightHandPoleVector property value. Otherwise the elbow direction comes from the + * underlying animation.
leftHandPoleReferenceVector{@link Vec3}The direction of the elbow in the local + * coordinate system of the elbow.
leftHandPoleVector{@link Vec3}The direction the elbow should point in rig + * coordinates.
rightHandPosition{@link Vec3}The desired position of the RightHand + * joint in rig coordinates.
rightHandRotation{@link Quat}The desired orientation of the + * RightHand joint in rig coordinates.
rightHandType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for + * the right arm.
rightHandPoleVectorEnabledbooleanWhen true, the elbow angle is + * controlled by the rightHandPoleVector property value. Otherwise the elbow direction comes from the + * underlying animation.
rightHandPoleReferenceVector{@link Vec3}The direction of the elbow in the local + * coordinate system of the elbow.
rightHandPoleVector{@link Vec3}The direction the elbow should point in rig + * coordinates.
leftFootIKEnabledbooleantrue if IK is enabled for the left + * foot.
rightFootIKEnabledbooleantrue if IK is enabled for the right + * foot.
leftFootIKPositionVarstringThe name of the source for the desired position + * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used.
leftFootIKRotationVarstringThe name of the source for the desired rotation + * of the LeftFoot joint. If not set, the foot rotation of the underlying animation will be used.
leftFootPoleVectorEnabledbooleanWhen true, the knee angle is + * controlled by the leftFootPoleVector property value. Otherwise the knee direction comes from the + * underlying animation.
leftFootPoleVector{@link Vec3}The direction the knee should face in rig + * coordinates.
rightFootIKPositionVarstringThe name of the source for the desired position + * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used.
rightFootIKRotationVarstringThe name of the source for the desired rotation + * of the RightFoot joint. If not set, the foot rotation of the underlying animation will be used.
rightFootPoleVectorEnabledbooleanWhen true, the knee angle is + * controlled by the rightFootPoleVector property value. Otherwise the knee direction comes from the + * underlying animation.
rightFootPoleVector{@link Vec3}The direction the knee should face in rig + * coordinates.
isTalkingbooleantrue if the avatar is talking.
notIsTalkingbooleantrue if the avatar is not talking.
solutionSource{@link MyAvatar.AnimIKSolutionSource|AnimIKSolutionSource}Determines the initial conditions of the IK solver.
defaultPoseOverlayAlphanumberControls the blend between the main animation state + * machine and the default pose. Mostly used during full body tracking so that walking & jumping animations do not + * affect the IK of the figure.
defaultPoseOverlayBoneSet{@link MyAvatar.AnimOverlayBoneSet|AnimOverlayBoneSet}Specifies which bones will be replace by the source overlay.
hipsType{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * hips.
hipsPosition{@link Vec3}The desired position of Hips joint in rig + * coordinates.
hipsRotation{@link Quat}the desired orientation of the Hips joint in + * rig coordinates.
spine2Type{@link MyAvatar.IKTargetType|IKTargetType}The type of IK used for the + * Spine2 joint.
spine2Position{@link Vec3}The desired position of the Spine2 joint + * in rig coordinates.
spine2Rotation{@link Quat}The desired orientation of the Spine2 + * joint in rig coordinates.
leftFootIKAlphanumberBlends between full IK for the leg and the underlying + * animation.
rightFootIKAlphanumberBlends between full IK for the leg and the underlying + * animation.
hipsWeightnumberHow strongly the hips target blends with the IK solution for + * other IK chains.
leftHandWeightnumberHow strongly the left hand blends with IK solution of other + * IK chains.
rightHandWeightnumberHow strongly the right hand blends with IK solution of other + * IK chains.
spine2WeightnumberHow strongly the spine2 chain blends with the rest of the IK + * solution.
leftHandOverlayAlphanumberUsed to blend in the animated hand gesture poses, such + * as point and thumbs up.
leftHandGraspAlphanumberUsed to blend between an open hand and a closed hand. + * Usually changed as you squeeze the trigger of the hand controller.
rightHandOverlayAlphanumberUsed to blend in the animated hand gesture poses, + * such as point and thumbs up.
rightHandGraspAlphanumberUsed to blend between an open hand and a closed hand. + * Usually changed as you squeeze the trigger of the hand controller.
isLeftIndexPointbooleantrue if the left hand should be + * pointing.
isLeftThumbRaisebooleantrue if the left hand should be + * thumbs-up.
isLeftIndexPointAndThumbRaisebooleantrue if the left hand should be + * pointing and thumbs-up.
isLeftHandGraspbooleantrue if the left hand should be at rest, + * grasping the controller.
isRightIndexPointbooleantrue if the right hand should be + * pointing.
isRightThumbRaisebooleantrue if the right hand should be + * thumbs-up.
isRightIndexPointAndThumbRaisebooleantrue if the right hand should + * be pointing and thumbs-up.
isRightHandGraspbooleantrue if the right hand should be at rest, + * grasping the controller.
+ *

Note: Rig coordinates are +z forward and +y up.

+ * @typedef {object} MyAvatar.AnimStateDictionary + */ +// Note: The following animVars are intentionally not documented: +// - leftFootPosition +// - leftFootRotation +// - rightFooKPosition +// - rightFooKRotation +// Note: The following items aren't set in the code below but are still intentionally documented: +// - leftFootIKAlpha +// - rightFootIKAlpha +// - hipsWeight +// - leftHandWeight +// - rightHandWeight +// - spine2Weight +// - rightHandOverlayAlpha +// - rightHandGraspAlpha +// - leftHandOverlayAlpha +// - leftHandGraspAlpha +// - isRightIndexPoint +// - isRightThumbRaise +// - isRightIndexPointAndThumbRaise +// - isRightHandGrasp +// - isLeftIndexPoint +// - isLeftThumbRaise +// - isLeftIndexPointAndThumbRaise +// - isLeftHandGrasp Rig::Rig() { // Ensure thread-safe access to the rigRegistry. std::lock_guard guard(rigRegistryMutex); @@ -1210,7 +1422,8 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons _networkAnimState.blendTime += deltaTime; alpha = _computeNetworkAnimation ? (_networkAnimState.blendTime / TOTAL_BLEND_TIME) : (1.0f - (_networkAnimState.blendTime / TOTAL_BLEND_TIME)); alpha = glm::clamp(alpha, 0.0f, 1.0f); - for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + size_t numJoints = std::min(_networkPoseSet._relativePoses.size(), _internalPoseSet._relativePoses.size()); + for (size_t i = 0; i < numJoints; i++) { _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], alpha); } } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b2e6167ffa..c537fea646 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1052,7 +1052,7 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); - if (_muted || !_audioOutput || (!_shouldEchoLocally && !hasReverb)) { + if ((_muted && !_shouldEchoLocally) || !_audioOutput || (!_shouldEchoLocally && !hasReverb)) { return; } @@ -1354,26 +1354,30 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { // the lock guarantees that injectorBuffer, if found, is invariant - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + auto injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { + auto options = injector->getOptions(); + static const int HRTF_DATASET_INDEX = 1; - int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); + int numChannels = options.ambisonic ? AudioConstants::AMBISONIC : (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO); size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - float gain = injector->getVolume(); + bool isSystemSound = !options.positionSet && !options.ambisonic; - if (injector->isAmbisonic()) { + float gain = options.volume * (isSystemSound ? _systemInjectorGain : _localInjectorGain); - if (injector->isPositionSet()) { + if (options.ambisonic) { + + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); } @@ -1382,7 +1386,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // Calculate the soundfield orientation relative to the listener. // Injector orientation can be used to align a recording to our world coordinates. // - glm::quat relativeOrientation = injector->getOrientation() * glm::inverse(_orientationGetter()); + glm::quat relativeOrientation = options.orientation * glm::inverse(_orientationGetter()); // convert from Y-up (OpenGL) to Z-up (Ambisonic) coordinate system float qw = relativeOrientation.w; @@ -1394,12 +1398,12 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } else if (injector->isStereo()) { + } else if (options.stereo) { - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); } @@ -1412,10 +1416,10 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { // injector is mono - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); @@ -1437,21 +1441,21 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { - qCDebug(audioclient) << "injector has no more data, marking finished for removal"; + //qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; + //qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } for (const AudioInjectorPointer& injector : injectorsToRemove) { - qCDebug(audioclient) << "removing injector"; + //qCDebug(audioclient) << "removing injector"; _activeLocalAudioInjectors.removeOne(injector); } @@ -1531,6 +1535,15 @@ void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { } } +void AudioClient::setWarnWhenMuted(bool enable, bool emitSignal) { + if (_warnWhenMuted != enable) { + _warnWhenMuted = enable; + if (emitSignal) { + emit warnWhenMutedChanged(_warnWhenMuted); + } + } +} + bool AudioClient::setIsStereoInput(bool isStereoInput) { bool stereoInputChanged = false; if (isStereoInput != _isStereoInput && _inputDeviceInfo.supportedChannelCounts().contains(2)) { @@ -1562,15 +1575,13 @@ bool AudioClient::setIsStereoInput(bool isStereoInput) { } bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + auto injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { // local injectors are on the AudioInjectorsThread, so we must guard access Lock lock(_injectorsMutex); if (!_activeLocalAudioInjectors.contains(injector)) { - qCDebug(audioclient) << "adding new injector"; + //qCDebug(audioclient) << "adding new injector"; _activeLocalAudioInjectors.append(injector); - // move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop()) - injectorBuffer->setParent(nullptr); // update the flag _localInjectorsAvailable.exchange(true, std::memory_order_release); @@ -1586,6 +1597,11 @@ bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { } } +int AudioClient::getNumLocalInjectors() { + Lock lock(_injectorsMutex); + return _activeLocalAudioInjectors.size(); +} + void AudioClient::outputFormatChanged() { _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 87e0f68e72..7608bf5cdb 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -181,6 +181,8 @@ public: bool isHeadsetPluggedIn() { return _isHeadsetPluggedIn; } #endif + int getNumLocalInjectors(); + public slots: void start(); void stop(); @@ -210,6 +212,9 @@ public slots: void setNoiseReduction(bool isNoiseGateEnabled, bool emitSignal = true); bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } + void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true); + bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; } + virtual bool getLocalEcho() override { return _shouldEchoLocally; } virtual void setLocalEcho(bool localEcho) override { _shouldEchoLocally = localEcho; } virtual void toggleLocalEcho() override { _shouldEchoLocally = !_shouldEchoLocally; } @@ -236,6 +241,8 @@ public slots: void setInputVolume(float volume, bool emitSignal = true); void setReverb(bool reverb); void setReverbOptions(const AudioEffectOptions* options); + void setLocalInjectorGain(float gain) { _localInjectorGain = gain; }; + void setSystemInjectorGain(float gain) { _systemInjectorGain = gain; }; void outputNotify(); @@ -246,6 +253,7 @@ signals: void inputVolumeChanged(float volume); void muteToggled(bool muted); void noiseReductionChanged(bool noiseReductionEnabled); + void warnWhenMutedChanged(bool warnWhenMutedEnabled); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); void inputLoudnessChanged(float loudness, bool isClipping); @@ -365,6 +373,7 @@ private: bool _shouldEchoLocally; bool _shouldEchoToServer; bool _isNoiseGateEnabled; + bool _warnWhenMuted; bool _reverb; AudioEffectOptions _scriptReverbOptions; @@ -388,6 +397,8 @@ private: int16_t* _outputScratchBuffer { NULL }; // for local audio (used by audio injectors thread) + std::atomic _localInjectorGain { 1.0f }; + std::atomic _systemInjectorGain { 1.0f }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 1581990e0c..4911917bf0 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -24,9 +24,10 @@ #include "AudioRingBuffer.h" #include "AudioLogging.h" #include "SoundCache.h" -#include "AudioSRC.h" #include "AudioHelpers.h" +int metaType = qRegisterMetaType("AudioInjectorPointer"); + AbstractAudioInterface* AudioInjector::_localAudioInterface{ nullptr }; AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { @@ -51,26 +52,30 @@ AudioInjector::AudioInjector(AudioDataPointer audioData, const AudioInjectorOpti { } -AudioInjector::~AudioInjector() { - deleteLocalBuffer(); -} +AudioInjector::~AudioInjector() {} bool AudioInjector::stateHas(AudioInjectorState state) const { - return (_state & state) == state; + return resultWithReadLock([&] { + return (_state & state) == state; + }); } void AudioInjector::setOptions(const AudioInjectorOptions& options) { // since options.stereo is computed from the audio stream, // we need to copy it from existing options just in case. - bool currentlyStereo = _options.stereo; - bool currentlyAmbisonic = _options.ambisonic; - _options = options; - _options.stereo = currentlyStereo; - _options.ambisonic = currentlyAmbisonic; + withWriteLock([&] { + bool currentlyStereo = _options.stereo; + bool currentlyAmbisonic = _options.ambisonic; + _options = options; + _options.stereo = currentlyStereo; + _options.ambisonic = currentlyAmbisonic; + }); } void AudioInjector::finishNetworkInjection() { - _state |= AudioInjectorState::NetworkInjectionFinished; + withWriteLock([&] { + _state |= AudioInjectorState::NetworkInjectionFinished; + }); // if we are already finished with local // injection, then we are finished @@ -80,35 +85,31 @@ void AudioInjector::finishNetworkInjection() { } void AudioInjector::finishLocalInjection() { - _state |= AudioInjectorState::LocalInjectionFinished; - if(_options.localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "finishLocalInjection"); + return; + } + + bool localOnly = false; + withWriteLock([&] { + _state |= AudioInjectorState::LocalInjectionFinished; + localOnly = _options.localOnly; + }); + + if(localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { finish(); } } void AudioInjector::finish() { - _state |= AudioInjectorState::Finished; - + withWriteLock([&] { + _state |= AudioInjectorState::Finished; + }); emit finished(); - - deleteLocalBuffer(); + _localBuffer = nullptr; } void AudioInjector::restart() { - // grab the AudioInjectorManager - auto injectorManager = DependencyManager::get(); - - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "restart"); - - if (!_options.localOnly) { - // notify the AudioInjectorManager to wake up in case it's waiting for new injectors - injectorManager->notifyInjectorReadyCondition(); - } - - return; - } - // reset the current send offset to zero _currentSendOffset = 0; @@ -121,19 +122,23 @@ void AudioInjector::restart() { // check our state to decide if we need extra handling for the restart request if (stateHas(AudioInjectorState::Finished)) { - if (!inject(&AudioInjectorManager::restartFinishedInjector)) { + if (!inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::restart failed to thread injector"; } } } bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)) { - _state = AudioInjectorState::NotFinished; + AudioInjectorOptions options; + withWriteLock([&] { + _state = AudioInjectorState::NotFinished; + options = _options; + }); int byteOffset = 0; - if (_options.secondOffset > 0.0f) { - int numChannels = _options.ambisonic ? 4 : (_options.stereo ? 2 : 1); - byteOffset = (int)(AudioConstants::SAMPLE_RATE * _options.secondOffset * numChannels); + if (options.secondOffset > 0.0f) { + int numChannels = options.ambisonic ? 4 : (options.stereo ? 2 : 1); + byteOffset = (int)(AudioConstants::SAMPLE_RATE * options.secondOffset * numChannels); byteOffset *= AudioConstants::SAMPLE_SIZE; } _currentSendOffset = byteOffset; @@ -143,7 +148,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInj } bool success = true; - if (!_options.localOnly) { + if (!options.localOnly) { auto injectorManager = DependencyManager::get(); if (!(*injectorManager.*injection)(sharedFromThis())) { success = false; @@ -158,7 +163,8 @@ bool AudioInjector::injectLocally() { if (_localAudioInterface) { if (_audioData->getNumBytes() > 0) { - _localBuffer = new AudioInjectorLocalBuffer(_audioData); + _localBuffer = QSharedPointer(new AudioInjectorLocalBuffer(_audioData), &AudioInjectorLocalBuffer::deleteLater); + _localBuffer->moveToThread(thread()); _localBuffer->open(QIODevice::ReadOnly); _localBuffer->setShouldLoop(_options.loop); @@ -181,14 +187,6 @@ bool AudioInjector::injectLocally() { return success; } -void AudioInjector::deleteLocalBuffer() { - if (_localBuffer) { - _localBuffer->stop(); - _localBuffer->deleteLater(); - _localBuffer = nullptr; - } -} - const uchar MAX_INJECTOR_VOLUME = packFloatGainToByte(1.0f); static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1; static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0; @@ -220,6 +218,10 @@ int64_t AudioInjector::injectNextFrame() { static int volumeOptionOffset = -1; static int audioDataOffset = -1; + AudioInjectorOptions options = resultWithReadLock([&] { + return _options; + }); + if (!_currentPacket) { if (_currentSendOffset < 0 || _currentSendOffset >= (int)_audioData->getNumBytes()) { @@ -253,7 +255,7 @@ int64_t AudioInjector::injectNextFrame() { audioPacketStream << QUuid::createUuid(); // pack the stereo/mono type of the stream - audioPacketStream << _options.stereo; + audioPacketStream << options.stereo; // pack the flag for loopback, if requested loopbackOptionOffset = _currentPacket->pos(); @@ -262,15 +264,16 @@ int64_t AudioInjector::injectNextFrame() { // pack the position for injected audio positionOptionOffset = _currentPacket->pos(); - audioPacketStream.writeRawData(reinterpret_cast(&_options.position), - sizeof(_options.position)); + audioPacketStream.writeRawData(reinterpret_cast(&options.position), + sizeof(options.position)); // pack our orientation for injected audio - audioPacketStream.writeRawData(reinterpret_cast(&_options.orientation), - sizeof(_options.orientation)); + audioPacketStream.writeRawData(reinterpret_cast(&options.orientation), + sizeof(options.orientation)); + + audioPacketStream.writeRawData(reinterpret_cast(&options.position), + sizeof(options.position)); - audioPacketStream.writeRawData(reinterpret_cast(&_options.position), - sizeof(_options.position)); glm::vec3 boxCorner = glm::vec3(0); audioPacketStream.writeRawData(reinterpret_cast(&boxCorner), sizeof(glm::vec3)); @@ -283,7 +286,7 @@ int64_t AudioInjector::injectNextFrame() { volumeOptionOffset = _currentPacket->pos(); quint8 volume = MAX_INJECTOR_VOLUME; audioPacketStream << volume; - audioPacketStream << _options.ignorePenumbra; + audioPacketStream << options.ignorePenumbra; audioDataOffset = _currentPacket->pos(); @@ -313,10 +316,10 @@ int64_t AudioInjector::injectNextFrame() { _currentPacket->writePrimitive((uchar)(_localAudioInterface && _localAudioInterface->shouldLoopbackInjectors())); _currentPacket->seek(positionOptionOffset); - _currentPacket->writePrimitive(_options.position); - _currentPacket->writePrimitive(_options.orientation); + _currentPacket->writePrimitive(options.position); + _currentPacket->writePrimitive(options.orientation); - quint8 volume = packFloatGainToByte(_options.volume); + quint8 volume = packFloatGainToByte(options.volume); _currentPacket->seek(volumeOptionOffset); _currentPacket->writePrimitive(volume); @@ -326,8 +329,8 @@ int64_t AudioInjector::injectNextFrame() { // Might be a reasonable place to do the encode step here. QByteArray decodedAudio; - int totalBytesLeftToCopy = (_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - if (!_options.loop) { + int totalBytesLeftToCopy = (options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + if (!options.loop) { // If we aren't looping, let's make sure we don't read past the end int bytesLeftToRead = _audioData->getNumBytes() - _currentSendOffset; totalBytesLeftToCopy = std::min(totalBytesLeftToCopy, bytesLeftToRead); @@ -342,14 +345,16 @@ int64_t AudioInjector::injectNextFrame() { auto samplesOut = reinterpret_cast(decodedAudio.data()); // Copy and Measure the loudness of this frame - _loudness = 0.0f; - for (int i = 0; i < samplesLeftToCopy; ++i) { - auto index = (currentSample + i) % _audioData->getNumSamples(); - auto sample = samples[index]; - samplesOut[i] = sample; - _loudness += abs(sample) / (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); - } - _loudness /= (float)samplesLeftToCopy; + withWriteLock([&] { + _loudness = 0.0f; + for (int i = 0; i < samplesLeftToCopy; ++i) { + auto index = (currentSample + i) % _audioData->getNumSamples(); + auto sample = samples[index]; + samplesOut[i] = sample; + _loudness += abs(sample) / (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); + } + _loudness /= (float)samplesLeftToCopy; + }); _currentSendOffset = (_currentSendOffset + totalBytesLeftToCopy) % _audioData->getNumBytes(); @@ -371,7 +376,7 @@ int64_t AudioInjector::injectNextFrame() { _outgoingSequenceNumber++; } - if (_currentSendOffset == 0 && !_options.loop) { + if (_currentSendOffset == 0 && !options.loop) { finishNetworkInjection(); return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } @@ -391,134 +396,10 @@ int64_t AudioInjector::injectNextFrame() { // If we are falling behind by more frames than our threshold, let's skip the frames ahead qCDebug(audio) << this << "injectNextFrame() skipping ahead, fell behind by " << (currentFrameBasedOnElapsedTime - _nextFrame) << " frames"; _nextFrame = currentFrameBasedOnElapsedTime; - _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (_options.stereo ? 2 : 1) % _audioData->getNumBytes(); + _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (options.stereo ? 2 : 1) % _audioData->getNumBytes(); } int64_t playNextFrameAt = ++_nextFrame * AudioConstants::NETWORK_FRAME_USECS; return std::max(INT64_C(0), playNextFrameAt - currentTime); -} - -void AudioInjector::stop() { - // trigger a call on the injector's thread to change state to finished - QMetaObject::invokeMethod(this, "finish"); -} - -void AudioInjector::triggerDeleteAfterFinish() { - // make sure this fires on the AudioInjector thread - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "triggerDeleteAfterFinish", Qt::QueuedConnection); - return; - } - - if (stateHas(AudioInjectorState::Finished)) { - stop(); - } else { - _state |= AudioInjectorState::PendingDelete; - } -} - -AudioInjectorPointer AudioInjector::playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options) { - AudioInjectorPointer injector = playSound(sound, options); - - if (injector) { - injector->_state |= AudioInjectorState::PendingDelete; - } - - return injector; -} - - -AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const AudioInjectorOptions& options) { - if (!sound || !sound->isReady()) { - return AudioInjectorPointer(); - } - - if (options.pitch == 1.0f) { - - AudioInjectorPointer injector = AudioInjectorPointer::create(sound, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread injector"; - } - return injector; - - } else { - using AudioConstants::AudioSample; - using AudioConstants::SAMPLE_RATE; - const int standardRate = SAMPLE_RATE; - // limit pitch to 4 octaves - const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); - const int resampledRate = glm::round(SAMPLE_RATE / pitch); - - auto audioData = sound->getAudioData(); - auto numChannels = audioData->getNumChannels(); - auto numFrames = audioData->getNumFrames(); - - AudioSRC resampler(standardRate, resampledRate, numChannels); - - // create a resampled buffer that is guaranteed to be large enough - const int maxOutputFrames = resampler.getMaxOutput(numFrames); - const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); - QByteArray resampledBuffer(maxOutputSize, '\0'); - auto bufferPtr = reinterpret_cast(resampledBuffer.data()); - - resampler.render(audioData->data(), bufferPtr, numFrames); - - int numSamples = maxOutputFrames * numChannels; - auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); - - AudioInjectorPointer injector = AudioInjectorPointer::create(newAudioData, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; - } - return injector; - } -} - -AudioInjectorPointer AudioInjector::playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options) { - AudioInjectorPointer injector = playSound(audioData, options); - - if (injector) { - injector->_state |= AudioInjectorState::PendingDelete; - } - - return injector; -} - -AudioInjectorPointer AudioInjector::playSound(AudioDataPointer audioData, const AudioInjectorOptions& options) { - if (options.pitch == 1.0f) { - AudioInjectorPointer injector = AudioInjectorPointer::create(audioData, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; - } - return injector; - } else { - using AudioConstants::AudioSample; - using AudioConstants::SAMPLE_RATE; - const int standardRate = SAMPLE_RATE; - // limit pitch to 4 octaves - const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); - const int resampledRate = glm::round(SAMPLE_RATE / pitch); - - auto numChannels = audioData->getNumChannels(); - auto numFrames = audioData->getNumFrames(); - - AudioSRC resampler(standardRate, resampledRate, numChannels); - - // create a resampled buffer that is guaranteed to be large enough - const int maxOutputFrames = resampler.getMaxOutput(numFrames); - const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); - QByteArray resampledBuffer(maxOutputSize, '\0'); - auto bufferPtr = reinterpret_cast(resampledBuffer.data()); - - resampler.render(audioData->data(), bufferPtr, numFrames); - - int numSamples = maxOutputFrames * numChannels; - auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); - - return AudioInjector::playSound(newAudioData, options); - } } \ No newline at end of file diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 3c21d2eccf..555af84025 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -19,6 +19,8 @@ #include #include +#include + #include #include @@ -49,7 +51,7 @@ AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs) // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object // until it dies. -class AudioInjector : public QObject, public QEnableSharedFromThis { +class AudioInjector : public QObject, public QEnableSharedFromThis, public ReadWriteLockable { Q_OBJECT public: AudioInjector(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions); @@ -61,40 +63,34 @@ public: int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } - AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } + QSharedPointer getLocalBuffer() const { return _localBuffer; } AudioHRTF& getLocalHRTF() { return _localHRTF; } AudioFOA& getLocalFOA() { return _localFOA; } - bool isLocalOnly() const { return _options.localOnly; } - float getVolume() const { return _options.volume; } - bool isPositionSet() const { return _options.positionSet; } - glm::vec3 getPosition() const { return _options.position; } - glm::quat getOrientation() const { return _options.orientation; } - bool isStereo() const { return _options.stereo; } - bool isAmbisonic() const { return _options.ambisonic; } + float getLoudness() const { return resultWithReadLock([&] { return _loudness; }); } + bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } + + bool isLocalOnly() const { return resultWithReadLock([&] { return _options.localOnly; }); } + float getVolume() const { return resultWithReadLock([&] { return _options.volume; }); } + bool isPositionSet() const { return resultWithReadLock([&] { return _options.positionSet; }); } + glm::vec3 getPosition() const { return resultWithReadLock([&] { return _options.position; }); } + glm::quat getOrientation() const { return resultWithReadLock([&] { return _options.orientation; }); } + bool isStereo() const { return resultWithReadLock([&] { return _options.stereo; }); } + bool isAmbisonic() const { return resultWithReadLock([&] { return _options.ambisonic; }); } + + AudioInjectorOptions getOptions() const { return resultWithReadLock([&] { return _options; }); } + void setOptions(const AudioInjectorOptions& options); bool stateHas(AudioInjectorState state) const ; static void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - static AudioInjectorPointer playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options); - static AudioInjectorPointer playSound(SharedSoundPointer sound, const AudioInjectorOptions& options); - static AudioInjectorPointer playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options); - static AudioInjectorPointer playSound(AudioDataPointer audioData, const AudioInjectorOptions& options); + void restart(); + void finish(); + + void finishNetworkInjection(); public slots: - void restart(); - - void stop(); - void triggerDeleteAfterFinish(); - - const AudioInjectorOptions& getOptions() const { return _options; } - void setOptions(const AudioInjectorOptions& options); - - float getLoudness() const { return _loudness; } - bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } - void finish(); void finishLocalInjection(); - void finishNetworkInjection(); signals: void finished(); @@ -104,7 +100,6 @@ private: int64_t injectNextFrame(); bool inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)); bool injectLocally(); - void deleteLocalBuffer(); static AbstractAudioInterface* _localAudioInterface; @@ -116,7 +111,7 @@ private: float _loudness { 0.0f }; int _currentSendOffset { 0 }; std::unique_ptr _currentPacket { nullptr }; - AudioInjectorLocalBuffer* _localBuffer { nullptr }; + QSharedPointer _localBuffer { nullptr }; int64_t _nextFrame { 0 }; std::unique_ptr _frameTimer { nullptr }; @@ -128,4 +123,6 @@ private: friend class AudioInjectorManager; }; +Q_DECLARE_METATYPE(AudioInjectorPointer) + #endif // hifi_AudioInjector_h diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.cpp b/libraries/audio/src/AudioInjectorLocalBuffer.cpp index 015d87e03b..680513abf5 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.cpp +++ b/libraries/audio/src/AudioInjectorLocalBuffer.cpp @@ -16,6 +16,10 @@ AudioInjectorLocalBuffer::AudioInjectorLocalBuffer(AudioDataPointer audioData) : { } +AudioInjectorLocalBuffer::~AudioInjectorLocalBuffer() { + stop(); +} + void AudioInjectorLocalBuffer::stop() { _isStopped = true; @@ -30,9 +34,8 @@ bool AudioInjectorLocalBuffer::seek(qint64 pos) { } } - qint64 AudioInjectorLocalBuffer::readData(char* data, qint64 maxSize) { - if (!_isStopped) { + if (!_isStopped && _audioData) { // first copy to the end of the raw audio int bytesToEnd = (int)_audioData->getNumBytes() - _currentOffset; diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.h b/libraries/audio/src/AudioInjectorLocalBuffer.h index e0f8847883..2f73e5b313 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.h +++ b/libraries/audio/src/AudioInjectorLocalBuffer.h @@ -22,6 +22,7 @@ class AudioInjectorLocalBuffer : public QIODevice { Q_OBJECT public: AudioInjectorLocalBuffer(AudioDataPointer audioData); + ~AudioInjectorLocalBuffer(); void stop(); diff --git a/libraries/audio/src/AudioInjectorManager.cpp b/libraries/audio/src/AudioInjectorManager.cpp index f30d3093ec..e5ffc77798 100644 --- a/libraries/audio/src/AudioInjectorManager.cpp +++ b/libraries/audio/src/AudioInjectorManager.cpp @@ -14,11 +14,14 @@ #include #include +#include #include "AudioConstants.h" #include "AudioInjector.h" #include "AudioLogging.h" +#include "AudioSRC.h" + AudioInjectorManager::~AudioInjectorManager() { _shouldStop = true; @@ -30,7 +33,7 @@ AudioInjectorManager::~AudioInjectorManager() { auto& timePointerPair = _injectors.top(); // ask it to stop and be deleted - timePointerPair.second->stop(); + timePointerPair.second->finish(); _injectors.pop(); } @@ -46,6 +49,8 @@ AudioInjectorManager::~AudioInjectorManager() { _thread->quit(); _thread->wait(); } + + moveToThread(qApp->thread()); } void AudioInjectorManager::createThread() { @@ -55,6 +60,8 @@ void AudioInjectorManager::createThread() { // when the thread is started, have it call our run to handle injection of audio connect(_thread, &QThread::started, this, &AudioInjectorManager::run, Qt::DirectConnection); + moveToThread(_thread); + // start the thread _thread->start(); } @@ -141,36 +148,7 @@ bool AudioInjectorManager::wouldExceedLimits() { // Should be called inside of a bool AudioInjectorManager::threadInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { - qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; - return false; - } - - // guard the injectors vector with a mutex - Lock lock(_injectorsMutex); - - if (wouldExceedLimits()) { - return false; - } else { - if (!_thread) { - createThread(); - } - - // move the injector to the QThread - injector->moveToThread(_thread); - - // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), injector); - - // notify our wait condition so we can inject two frames for this injector immediately - _injectorReady.notify_one(); - - return true; - } -} - -bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& injector) { - if (_shouldStop) { - qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; } @@ -188,3 +166,192 @@ bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& i } return true; } + +AudioInjectorPointer AudioInjectorManager::playSound(const SharedSoundPointer& sound, const AudioInjectorOptions& options, bool setPendingDelete) { + if (_shouldStop) { + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + return nullptr; + } + + AudioInjectorPointer injector = nullptr; + if (sound && sound->isReady()) { + if (options.pitch == 1.0f) { + injector = QSharedPointer(new AudioInjector(sound, options), &AudioInjector::deleteLater); + } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit pitch to 4 octaves + const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = glm::round(SAMPLE_RATE / pitch); + + auto audioData = sound->getAudioData(); + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); + + resampler.render(audioData->data(), bufferPtr, numFrames); + + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + injector = QSharedPointer(new AudioInjector(newAudioData, options), &AudioInjector::deleteLater); + } + } + + if (!injector) { + return nullptr; + } + + if (setPendingDelete) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + injector->moveToThread(_thread); + injector->inject(&AudioInjectorManager::threadInjector); + + return injector; +} + +AudioInjectorPointer AudioInjectorManager::playSound(const AudioDataPointer& audioData, const AudioInjectorOptions& options, bool setPendingDelete) { + if (_shouldStop) { + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + return nullptr; + } + + AudioInjectorPointer injector = nullptr; + if (options.pitch == 1.0f) { + injector = QSharedPointer(new AudioInjector(audioData, options), &AudioInjector::deleteLater); + } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit pitch to 4 octaves + const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = glm::round(SAMPLE_RATE / pitch); + + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); + + resampler.render(audioData->data(), bufferPtr, numFrames); + + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + injector = QSharedPointer(new AudioInjector(newAudioData, options), &AudioInjector::deleteLater); + } + + if (!injector) { + return nullptr; + } + + if (setPendingDelete) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + injector->moveToThread(_thread); + injector->inject(&AudioInjectorManager::threadInjector); + + return injector; +} + +void AudioInjectorManager::setOptionsAndRestart(const AudioInjectorPointer& injector, const AudioInjectorOptions& options) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "setOptionsAndRestart", Q_ARG(const AudioInjectorPointer&, injector), Q_ARG(const AudioInjectorOptions&, options)); + _injectorReady.notify_one(); + return; + } + + injector->setOptions(options); + injector->restart(); +} + +void AudioInjectorManager::restart(const AudioInjectorPointer& injector) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "restart", Q_ARG(const AudioInjectorPointer&, injector)); + _injectorReady.notify_one(); + return; + } + + injector->restart(); +} + +void AudioInjectorManager::setOptions(const AudioInjectorPointer& injector, const AudioInjectorOptions& options) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "setOptions", Q_ARG(const AudioInjectorPointer&, injector), Q_ARG(const AudioInjectorOptions&, options)); + _injectorReady.notify_one(); + return; + } + + injector->setOptions(options); +} + +AudioInjectorOptions AudioInjectorManager::getOptions(const AudioInjectorPointer& injector) { + if (!injector) { + return AudioInjectorOptions(); + } + + return injector->getOptions(); +} + +float AudioInjectorManager::getLoudness(const AudioInjectorPointer& injector) { + if (!injector) { + return 0.0f; + } + + return injector->getLoudness(); +} + +bool AudioInjectorManager::isPlaying(const AudioInjectorPointer& injector) { + if (!injector) { + return false; + } + + return injector->isPlaying(); +} + +void AudioInjectorManager::stop(const AudioInjectorPointer& injector) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "stop", Q_ARG(const AudioInjectorPointer&, injector)); + _injectorReady.notify_one(); + return; + } + + injector->finish(); +} + +size_t AudioInjectorManager::getNumInjectors() { + Lock lock(_injectorsMutex); + return _injectors.size(); +} \ No newline at end of file diff --git a/libraries/audio/src/AudioInjectorManager.h b/libraries/audio/src/AudioInjectorManager.h index 9aca3014e3..cb243f23de 100644 --- a/libraries/audio/src/AudioInjectorManager.h +++ b/libraries/audio/src/AudioInjectorManager.h @@ -30,8 +30,27 @@ class AudioInjectorManager : public QObject, public Dependency { SINGLETON_DEPENDENCY public: ~AudioInjectorManager(); + + AudioInjectorPointer playSound(const SharedSoundPointer& sound, const AudioInjectorOptions& options, bool setPendingDelete = false); + AudioInjectorPointer playSound(const AudioDataPointer& audioData, const AudioInjectorOptions& options, bool setPendingDelete = false); + + size_t getNumInjectors(); + +public slots: + void setOptionsAndRestart(const AudioInjectorPointer& injector, const AudioInjectorOptions& options); + void restart(const AudioInjectorPointer& injector); + + void setOptions(const AudioInjectorPointer& injector, const AudioInjectorOptions& options); + AudioInjectorOptions getOptions(const AudioInjectorPointer& injector); + + float getLoudness(const AudioInjectorPointer& injector); + bool isPlaying(const AudioInjectorPointer& injector); + + void stop(const AudioInjectorPointer& injector); + private slots: void run(); + private: using TimeInjectorPointerPair = std::pair; @@ -49,11 +68,10 @@ private: using Lock = std::unique_lock; bool threadInjector(const AudioInjectorPointer& injector); - bool restartFinishedInjector(const AudioInjectorPointer& injector); void notifyInjectorReadyCondition() { _injectorReady.notify_one(); } bool wouldExceedLimits(); - AudioInjectorManager() {}; + AudioInjectorManager() { createThread(); } AudioInjectorManager(const AudioInjectorManager&) = delete; AudioInjectorManager& operator=(const AudioInjectorManager&) = delete; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index f3e671143b..74940b44cc 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -43,7 +43,6 @@ using namespace std; -const int NUM_BODY_CONE_SIDES = 9; const float CHAT_MESSAGE_SCALE = 0.0015f; const float CHAT_MESSAGE_HEIGHT = 0.1f; const float DISPLAYNAME_FADE_TIME = 0.5f; @@ -373,13 +372,6 @@ bool Avatar::applyGrabChanges() { target->removeGrab(grab); _avatarGrabs.erase(itr); grabAddedOrRemoved = true; - if (isMyAvatar()) { - const EntityItemPointer& entity = std::dynamic_pointer_cast(target); - if (entity && entity->getEntityHostType() == entity::HostType::AVATAR && entity->getSimulationOwner().getID() == getID()) { - EntityItemProperties properties = entity->getProperties(); - sendPacket(entity->getID(), properties); - } - } } else { undeleted.push_back(id); } @@ -660,9 +652,8 @@ void Avatar::fadeIn(render::ScenePointer scene) { scene->enqueueTransaction(transaction); } -void Avatar::fadeOut(render::ScenePointer scene, KillAvatarReason reason) { +void Avatar::fadeOut(render::Transaction& transaction, KillAvatarReason reason) { render::Transition::Type transitionType = render::Transition::USER_LEAVE_DOMAIN; - render::Transaction transaction; if (reason == KillAvatarReason::YourAvatarEnteredTheirBubble) { transitionType = render::Transition::BUBBLE_ISECT_TRESPASSER; @@ -670,7 +661,6 @@ void Avatar::fadeOut(render::ScenePointer scene, KillAvatarReason reason) { transitionType = render::Transition::BUBBLE_ISECT_OWNER; } fade(transaction, transitionType); - scene->enqueueTransaction(transaction); } void Avatar::fade(render::Transaction& transaction, render::Transition::Type type) { @@ -680,19 +670,6 @@ void Avatar::fade(render::Transaction& transaction, render::Transition::Type typ transaction.addTransitionToItem(itemId, type, _renderItemID); } } - _isFading = true; -} - -void Avatar::updateFadingStatus() { - if (_isFading) { - render::Transaction transaction; - transaction.queryTransitionOnItem(_renderItemID, [this](render::ItemID id, const render::Transition* transition) { - if (!transition || transition->isFinished) { - _isFading = false; - } - }); - AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); - } } void Avatar::removeFromScene(AvatarSharedPointer self, const render::ScenePointer& scene, render::Transaction& transaction) { @@ -1661,60 +1638,6 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) { return bytesRead; } -int Avatar::_jointConesID = GeometryCache::UNKNOWN_ID; - -// render a makeshift cone section that serves as a body part connecting joint spheres -void Avatar::renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, - float radius1, float radius2, const glm::vec4& color) { - - auto geometryCache = DependencyManager::get(); - - if (_jointConesID == GeometryCache::UNKNOWN_ID) { - _jointConesID = geometryCache->allocateID(); - } - - glm::vec3 axis = position2 - position1; - float length = glm::length(axis); - - if (length > 0.0f) { - - axis /= length; - - glm::vec3 perpSin = glm::vec3(1.0f, 0.0f, 0.0f); - glm::vec3 perpCos = glm::normalize(glm::cross(axis, perpSin)); - perpSin = glm::cross(perpCos, axis); - - float angleb = 0.0f; - QVector points; - - for (int i = 0; i < NUM_BODY_CONE_SIDES; i ++) { - - // the rectangles that comprise the sides of the cone section are - // referenced by "a" and "b" in one dimension, and "1", and "2" in the other dimension. - int anglea = angleb; - angleb = ((float)(i+1) / (float)NUM_BODY_CONE_SIDES) * TWO_PI; - - float sa = sinf(anglea); - float sb = sinf(angleb); - float ca = cosf(anglea); - float cb = cosf(angleb); - - glm::vec3 p1a = position1 + perpSin * sa * radius1 + perpCos * ca * radius1; - glm::vec3 p1b = position1 + perpSin * sb * radius1 + perpCos * cb * radius1; - glm::vec3 p2a = position2 + perpSin * sa * radius2 + perpCos * ca * radius2; - glm::vec3 p2b = position2 + perpSin * sb * radius2 + perpCos * cb * radius2; - - points << p1a << p1b << p2a << p1b << p2a << p2b; - } - - PROFILE_RANGE_BATCH(batch, __FUNCTION__); - // TODO: this is really inefficient constantly recreating these vertices buffers. It would be - // better if the avatars cached these buffers for each of the joints they are rendering - geometryCache->updateVertices(_jointConesID, points, color); - geometryCache->renderVertices(batch, gpu::TRIANGLES, _jointConesID); - } -} - float Avatar::getSkeletonHeight() const { Extents extents = _skeletonModel->getBindExtents(); return extents.maximum.y - extents.minimum.y; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 6c31f9fc93..aef5ac09e9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -127,7 +127,12 @@ private: class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaModelPayload { Q_OBJECT - // This property has JSDoc in MyAvatar.h. + /*jsdoc + * @comment IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h. + * + * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the + * registration point of the 3D model. + */ Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) public: @@ -175,7 +180,6 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - virtual bool isMyAvatar() const override { return false; } virtual void createOrb() { } enum class LoadingStatus { @@ -196,36 +200,52 @@ public: virtual QStringList getJointNames() const override; /**jsdoc + * Gets the default rotation of a joint (in the current avatar) relative to its parent. + *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function MyAvatar.getDefaultJointRotation - * @param {number} index - * @returns {Quat} + * @param {number} index - The joint index. + * @returns {Quat} The default rotation of the joint if the joint index is valid, otherwise {@link Quat(0)|Quat.IDENTITY}. */ Q_INVOKABLE virtual glm::quat getDefaultJointRotation(int index) const; /**jsdoc + * Gets the default translation of a joint (in the current avatar) relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

* @function MyAvatar.getDefaultJointTranslation - * @param {number} index - * @returns {Vec3} + * @param {number} index - The joint index. + * @returns {Vec3} The default translation of the joint (in model coordinates) if the joint index is valid, otherwise + * {@link Vec3(0)|Vec3.ZERO}. */ Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; /**jsdoc - * Provides read only access to the default joint rotations in avatar coordinates. + * Gets the default joint rotations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame - * @param index {number} index number - * @returns {Quat} The rotation of this joint in avatar coordinates. + * @param index {number} - The joint index. + * @returns {Quat} The default rotation of the joint in avatar coordinates. + * @example Report the default rotation of your avatar's head joint relative to your avatar. + * var headIndex = MyAvatar.getJointIndex("Head"); + * var defaultHeadRotation = MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex); + * print("Default head rotation: " + JSON.stringify(Quat.safeEulerAngles(defaultHeadRotation))); // Degrees */ Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; /**jsdoc - * Provides read only access to the default joint translations in avatar coordinates. + * Gets the default joint translations in avatar coordinates. * The default pose of the avatar is defined by the position and orientation of all bones * in the avatar's model file. Typically this is a T-pose. * @function MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame - * @param index {number} index number - * @returns {Vec3} The position of this joint in avatar coordinates. + * @param index {number} - The joint index. + * @returns {Vec3} The default position of the joint in avatar coordinates. + * @example Report the default translation of your avatar's head joint relative to your avatar. + * var headIndex = MyAvatar.getJointIndex("Head"); + * var defaultHeadTranslation = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex); + * print("Default head translation: " + JSON.stringify(defaultHeadTranslation)); */ Q_INVOKABLE virtual glm::vec3 getAbsoluteDefaultJointTranslationInObjectFrame(int index) const; @@ -233,59 +253,88 @@ public: virtual glm::vec3 getAbsoluteJointScaleInObjectFrame(int index) const override; virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; + + /**jsdoc + * Sets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the MyAvatar API.

+ * @function MyAvatar.setAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. + * @returns {boolean} false. + */ virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } + + /**jsdoc + * Sets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the MyAvatar API.

+ * @function MyAvatar.setAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Vec3} translation - The translation of the joint relative to the avatar. Not used. + * @returns {boolean} false. + */ virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } virtual glm::vec3 getSpine2SplineOffset() const { return _spine2SplineOffset; } virtual float getSpine2SplineRatio() const { return _spine2SplineRatio; } // world-space to avatar-space rigconversion functions /**jsdoc - * @function MyAvatar.worldToJointPoint - * @param {Vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * Transforms a position in world coordinates to a position in a joint's coordinates, or avatar coordinates if no joint is + * specified. + * @function MyAvatar.worldToJointPoint + * @param {Vec3} position - The position in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The position in the joint's coordinate system, or avatar coordinate system if no joint is specified. + */ Q_INVOKABLE glm::vec3 worldToJointPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.worldToJointDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * Transforms a direction in world coordinates to a direction in a joint's coordinates, or avatar coordinates if no joint + * is specified. + * @function MyAvatar.worldToJointDirection + * @param {Vec3} direction - The direction in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The direction in the joint's coordinate system, or avatar coordinate system if no joint is specified. + */ Q_INVOKABLE glm::vec3 worldToJointDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.worldToJointRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} + * Transforms a rotation in world coordinates to a rotation in a joint's coordinates, or avatar coordinates if no joint is + * specified. + * @function MyAvatar.worldToJointRotation + * @param {Quat} rotation - The rotation in world coordinates. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Quat} The rotation in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ Q_INVOKABLE glm::quat worldToJointRotation(const glm::quat& rotation, const int jointIndex = -1) const; - /**jsdoc - * @function MyAvatar.jointToWorldPoint - * @param {vec3} position - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * Transforms a position in a joint's coordinates, or avatar coordinates if no joint is specified, to a position in world + * coordinates. + * @function MyAvatar.jointToWorldPoint + * @param {Vec3} position - The position in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The position in world coordinates. + */ Q_INVOKABLE glm::vec3 jointToWorldPoint(const glm::vec3& position, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.jointToWorldDirection - * @param {Vec3} direction - * @param {number} [jointIndex=-1] - * @returns {Vec3} - */ + * Transforms a direction in a joint's coordinates, or avatar coordinates if no joint is specified, to a direction in world + * coordinates. + * @function MyAvatar.jointToWorldDirection + * @param {Vec3} direction - The direction in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Vec3} The direction in world coordinates. + */ Q_INVOKABLE glm::vec3 jointToWorldDirection(const glm::vec3& direction, const int jointIndex = -1) const; /**jsdoc - * @function MyAvatar.jointToWorldRotation - * @param {Quat} rotation - * @param {number} [jointIndex=-1] - * @returns {Quat} - */ + * Transforms a rotation in a joint's coordinates, or avatar coordinates if no joint is specified, to a rotation in world + * coordinates. + * @function MyAvatar.jointToWorldRotation + * @param {Quat} rotation - The rotation in joint coordinates, or avatar coordinates if no joint is specified. + * @param {number} [jointIndex=-1] - The index of the joint. + * @returns {Quat} The rotation in world coordinates. + */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; @@ -296,11 +345,8 @@ public: virtual int parseDataFromBuffer(const QByteArray& buffer) override; - static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, - float radius1, float radius2, const glm::vec4& color); - /**jsdoc - * Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, + * Sets the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. * @function MyAvatar.setSkeletonOffset * @param {Vec3} offset - The skeleton offset to set. @@ -316,7 +362,7 @@ public: Q_INVOKABLE void setSkeletonOffset(const glm::vec3& offset); /**jsdoc - * Get the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, + * Gets the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. * @function MyAvatar.getSkeletonOffset * @returns {Vec3} The current skeleton offset. @@ -328,7 +374,7 @@ public: virtual glm::vec3 getSkeletonPosition() const; /**jsdoc - * Get the position of a joint in the current avatar. + * Gets the position of a joint in the current avatar. * @function MyAvatar.getJointPosition * @param {number} index - The index of the joint. * @returns {Vec3} The position of the joint in world coordinates. @@ -336,7 +382,7 @@ public: Q_INVOKABLE glm::vec3 getJointPosition(int index) const; /**jsdoc - * Get the position of a joint in the current avatar. + * Gets the position of a joint in the current avatar. * @function MyAvatar.getJointPosition * @param {string} name - The name of the joint. * @returns {Vec3} The position of the joint in world coordinates. @@ -346,7 +392,7 @@ public: Q_INVOKABLE glm::vec3 getJointPosition(const QString& name) const; /**jsdoc - * Get the position of the current avatar's neck in world coordinates. + * Gets the position of the current avatar's neck in world coordinates. * @function MyAvatar.getNeckPosition * @returns {Vec3} The position of the neck in world coordinates. * @example Report the position of your avatar's neck. @@ -355,8 +401,9 @@ public: Q_INVOKABLE glm::vec3 getNeckPosition() const; /**jsdoc + * Gets the current acceleration of the avatar. * @function MyAvatar.getAcceleration - * @returns {Vec3} + * @returns {Vec3} The current acceleration of the avatar. */ Q_INVOKABLE glm::vec3 getAcceleration() const { return _acceleration; } @@ -380,47 +427,55 @@ public: void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); float computeMass(); /**jsdoc - * Get the position of the current avatar's feet (or rather, bottom of its collision capsule) in world coordinates. + * Gets the position of the current avatar's feet (or rather, bottom of its collision capsule) in world coordinates. * @function MyAvatar.getWorldFeetPosition * @returns {Vec3} The position of the avatar's feet in world coordinates. - */ + */ Q_INVOKABLE glm::vec3 getWorldFeetPosition(); void setPositionViaScript(const glm::vec3& position) override; void setOrientationViaScript(const glm::quat& orientation) override; /**jsdoc + * Gets the ID of the entity of avatar that the avatar is parented to. * @function MyAvatar.getParentID - * @returns {Uuid} + * @returns {Uuid} The ID of the entity or avatar that the avatar is parented to. {@link Uuid|Uuid.NULL} if not parented. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual const QUuid getParentID() const override { return SpatiallyNestable::getParentID(); } /**jsdoc + * Sets the ID of the entity of avatar that the avatar is parented to. * @function MyAvatar.setParentID - * @param {Uuid} parentID + * @param {Uuid} parentID - The ID of the entity or avatar that the avatar should be parented to. Set to + * {@link Uuid|Uuid.NULL} to unparent. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual void setParentID(const QUuid& parentID) override; /**jsdoc + * Gets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.getParentJointIndex - * @returns {number} + * @returns {number} The joint of the entity or avatar that the avatar is parented to. 65535 or + * -1 if parented to the entity or avatar's position and orientation rather than a joint. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); } /**jsdoc + * Sets the joint of the entity or avatar that the avatar is parented to. * @function MyAvatar.setParentJointIndex - * @param {number} parentJointIndex + * @param {number} parentJointIndex - he joint of the entity or avatar that the avatar should be parented to. Use + * 65535 or -1 to parent to the entity or avatar's position and orientation rather than a + * joint. */ // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; /**jsdoc - * Returns an array of joints, where each joint is an object containing name, index, and parentIndex fields. + * Gets information on all the joints in the avatar's skeleton. * @function MyAvatar.getSkeleton - * @returns {MyAvatar.SkeletonJoint[]} A list of information about each joint in this avatar's skeleton. + * @returns {MyAvatar.SkeletonJoint[]} Information about each joint in the avatar's skeleton. */ /**jsdoc * Information about a single joint in an Avatar's skeleton hierarchy. @@ -446,8 +501,9 @@ public: /**jsdoc * @function MyAvatar.getSimulationRate - * @param {string} [rateName=""] - * @returns {number} + * @param {string} [rateName=""] - Rate name. + * @returns {number} Simulation rate. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE float getSimulationRate(const QString& rateName = QString("")) const; @@ -464,9 +520,7 @@ public: bool isMoving() const { return _moving; } void fadeIn(render::ScenePointer scene); - void fadeOut(render::ScenePointer scene, KillAvatarReason reason); - bool isFading() const { return _isFading; } - void updateFadingStatus(); + void fadeOut(render::Transaction& transaction, KillAvatarReason reason); // JSDoc is in AvatarData.h. Q_INVOKABLE virtual float getEyeHeight() const override; @@ -503,6 +557,13 @@ public: uint32_t appendSubMetaItems(render::ItemIDs& subItems); signals: + /**jsdoc + * Triggered when the avatar's target scale is changed. The target scale is the desired scale of the avatar without any + * restrictions on permissible scale values imposed by the domain. + * @function MyAvatar.targetScaleChanged + * @param {number} targetScale - The avatar's target scale. + * @returns Signal + */ void targetScaleChanged(float targetScale); public slots: @@ -511,7 +572,7 @@ public slots: // thread safe, will return last valid palm from cache /**jsdoc - * Get the position of the left palm in world coordinates. + * Gets the position of the left palm in world coordinates. * @function MyAvatar.getLeftPalmPosition * @returns {Vec3} The position of the left palm in world coordinates. * @example Report the position of your avatar's left palm. @@ -520,15 +581,16 @@ public slots: glm::vec3 getLeftPalmPosition() const; /**jsdoc - * Get the rotation of the left palm in world coordinates. + * Gets the rotation of the left palm in world coordinates. * @function MyAvatar.getLeftPalmRotation * @returns {Quat} The rotation of the left palm in world coordinates. * @example Report the rotation of your avatar's left palm. * print(JSON.stringify(MyAvatar.getLeftPalmRotation())); */ glm::quat getLeftPalmRotation() const; + /**jsdoc - * Get the position of the right palm in world coordinates. + * Gets the position of the right palm in world coordinates. * @function MyAvatar.getRightPalmPosition * @returns {Vec3} The position of the right palm in world coordinates. * @example Report the position of your avatar's right palm. @@ -545,21 +607,26 @@ public slots: */ glm::quat getRightPalmRotation() const; + /**jsdoc + * @function MyAvatar.setModelURLFinished + * @param {boolean} success + * @deprecated This function is deprecated and will be removed. + */ // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); /**jsdoc * @function MyAvatar.rigReady - * @returns {Signal} + * @deprecated This function is deprecated and will be removed. */ // Hooked up to Model::rigReady signal void rigReady(); /**jsdoc * @function MyAvatar.rigReset - * @returns {Signal} + * @deprecated This function is deprecated and will be removed. */ - // Jooked up to Model::rigReset signal + // Hooked up to Model::rigReset signal void rigReset(); protected: @@ -608,7 +675,7 @@ protected: // protected methods... bool isLookingAtMe(AvatarSharedPointer avatar) const; - virtual void sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { } + virtual void sendPacket(const QUuid& entityID) const { } bool applyGrabChanges(); void relayJointDataToChildren(); @@ -658,15 +725,12 @@ protected: bool _initialized { false }; bool _isAnimatingScale { false }; bool _mustFadeIn { false }; - bool _isFading { false }; bool _reconstructSoftEntitiesJointMap { false }; float _modelScale { 1.0f }; AvatarTransit _transit; std::mutex _transitLock; - static int _jointConesID; - int _voiceSphereID; float _displayNameTargetAlpha { 1.0f }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index ea71ff128c..fbcf36a8c9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -338,24 +338,20 @@ void SkeletonModel::computeBoundingShape() { void SkeletonModel::renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& batch, float scale, float alpha) { auto geometryCache = DependencyManager::get(); // draw a blue sphere at the capsule top point - glm::vec3 topPoint = _translation + getRotation() * (scale * (_boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * Vectors::UNIT_Y)); - + glm::vec3 topPoint = _translation + _rotation * (scale * (_boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * Vectors::UNIT_Y)); batch.setModelTransform(Transform().setTranslation(topPoint).postScale(scale * _boundingCapsuleRadius)); geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.6f, 0.6f, 0.8f, alpha)); // draw a yellow sphere at the capsule bottom point - glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, scale * _boundingCapsuleHeight, 0.0f); - glm::vec3 axis = topPoint - bottomPoint; - + glm::vec3 bottomPoint = topPoint - _rotation * glm::vec3(0.0f, scale * _boundingCapsuleHeight, 0.0f); batch.setModelTransform(Transform().setTranslation(bottomPoint).postScale(scale * _boundingCapsuleRadius)); geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.8f, 0.8f, 0.6f, alpha)); // draw a green cylinder between the two points - glm::vec3 origin(0.0f); - batch.setModelTransform(Transform().setTranslation(bottomPoint)); - geometryCache->bindSimpleProgram(batch); - Avatar::renderJointConnectingCone(batch, origin, axis, scale * _boundingCapsuleRadius, scale * _boundingCapsuleRadius, - glm::vec4(0.6f, 0.8f, 0.6f, alpha)); + float capsuleDiameter = 2.0f * _boundingCapsuleRadius; + glm::vec3 cylinderDimensions = glm::vec3(capsuleDiameter, _boundingCapsuleHeight, capsuleDiameter); + batch.setModelTransform(Transform().setScale(scale * cylinderDimensions).setRotation(_rotation).setTranslation(0.5f * (topPoint + bottomPoint))); + geometryCache->renderSolidShapeInstance(args, batch, GeometryCache::Shape::Cylinder, glm::vec4(0.6f, 0.8f, 0.6f, alpha)); } bool SkeletonModel::hasSkeleton() { diff --git a/libraries/avatars/src/AssociatedTraitValues.h b/libraries/avatars/src/AssociatedTraitValues.h index e3060a8097..0df8cd9bb5 100644 --- a/libraries/avatars/src/AssociatedTraitValues.h +++ b/libraries/avatars/src/AssociatedTraitValues.h @@ -28,9 +28,10 @@ namespace AvatarTraits { template class AssociatedTraitValues { + using SimpleTypesArray = std::array; public: // constructor that pre-fills _simpleTypes with the default value specified by the template - AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {} + AssociatedTraitValues() { std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); } /// inserts the given value for the given simple trait type void insert(TraitType type, T value) { _simpleTypes[type] = value; } @@ -71,12 +72,12 @@ namespace AvatarTraits { } /// const iterators for the vector of simple type values - typename std::vector::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } - typename std::vector::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } + typename SimpleTypesArray::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } + typename SimpleTypesArray::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } /// non-const iterators for the vector of simple type values - typename std::vector::iterator simpleBegin() { return _simpleTypes.begin(); } - typename std::vector::iterator simpleEnd() { return _simpleTypes.end(); } + typename SimpleTypesArray::iterator simpleBegin() { return _simpleTypes.begin(); } + typename SimpleTypesArray::iterator simpleEnd() { return _simpleTypes.end(); } struct TraitWithInstances { TraitType traitType; @@ -96,7 +97,7 @@ namespace AvatarTraits { typename std::vector::iterator instancedEnd() { return _instancedTypes.end(); } private: - std::vector _simpleTypes; + SimpleTypesArray _simpleTypes; /// return the iterator to the matching TraitWithInstances object for a given instanced trait type typename std::vector::iterator instancesForTrait(TraitType traitType) { diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c733cfa291..a2b0b808ba 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -564,6 +564,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS); } + // Avatar has hero priority + if (getHasPriority()) { + setAtBit16(flags, HAS_HERO_PRIORITY); + } + data->flags = flags; destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags); @@ -1138,10 +1143,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right). // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled - // +---+-----+-----+--+--+--+--+-----+ - // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|xxxxx| - // +---+-----+-----+--+--+--+--+-----+ + // +---+-----+-----+--+--+--+--+--+----+ + // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|He|xxxx| + // +---+-----+-----+--+--+--+--+--+----+ // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits + // Hero-avatar status (He) - 12th bit auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); @@ -1152,7 +1158,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT); auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT); auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS); - + auto newHasPriority = oneAtBit16(bitItems, HAS_HERO_PRIORITY); + bool keyStateChanged = (_keyState != newKeyState); bool handStateChanged = (_handState != newHandState); bool faceStateChanged = (_headData->_isFaceTrackerConnected != newFaceTrackerConnected); @@ -1161,8 +1168,10 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement); bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement); bool collideWithOtherAvatarsChanged = (_collideWithOtherAvatars != newCollideWithOtherAvatars); + bool hasPriorityChanged = (getHasPriority() != newHasPriority); bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || - proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged; + proceduralEyeFaceMovementChanged || + proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged || hasPriorityChanged; _keyState = newKeyState; _handState = newHandState; @@ -1172,6 +1181,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement); _headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement); _collideWithOtherAvatars = newCollideWithOtherAvatars; + setHasPriorityWithoutTimestampReset(newHasPriority); sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags); @@ -1425,6 +1435,47 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { return numBytesRead; } +/**jsdoc + * The avatar mixer data comprises different types of data, with the data rates of each being tracked in kbps. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Rate NameDescription
"globalPosition"Incoming global position.
"localPosition"Incoming local position.
"avatarBoundingBox"Incoming avatar bounding box.
"avatarOrientation"Incoming avatar orientation.
"avatarScale"Incoming avatar scale.
"lookAtPosition"Incoming look-at position.
"audioLoudness"Incoming audio loudness.
"sensorToWorkMatrix"Incoming sensor-to-world matrix.
"additionalFlags"Incoming additional avatar flags.
"parentInfo"Incoming parent information.
"faceTracker"Incoming face tracker data.
"jointData"Incoming joint data.
"jointDefaultPoseFlagsRate"Incoming joint default pose flags.
"farGrabJointRate"Incoming far grab joint.
"globalPositionOutbound"Outgoing global position.
"localPositionOutbound"Outgoing local position.
"avatarBoundingBoxOutbound"Outgoing avatar bounding box.
"avatarOrientationOutbound"Outgoing avatar orientation.
"avatarScaleOutbound"Outgoing avatar scale.
"lookAtPositionOutbound"Outgoing look-at position.
"audioLoudnessOutbound"Outgoing audio loudness.
"sensorToWorkMatrixOutbound"Outgoing sensor-to-world matrix.
"additionalFlagsOutbound"Outgoing additional avatar flags.
"parentInfoOutbound"Outgoing parent information.
"faceTrackerOutbound"Outgoing face tracker data.
"jointDataOutbound"Outgoing joint data.
"jointDefaultPoseFlagsOutbound"Outgoing joint default pose flags.
""When no rate name is specified, the total incoming data rate is provided.
+ * + * @typedef {string} AvatarDataRate + */ float AvatarData::getDataRate(const QString& rateName) const { if (rateName == "") { return _parseBufferRate.rate() / BYTES_PER_KILOBIT; @@ -1486,6 +1537,35 @@ float AvatarData::getDataRate(const QString& rateName) const { return 0.0f; } +/**jsdoc + * The avatar mixer data comprises different types of data updated at different rates, in Hz. + * + * + * + * + * + * + + * + * + * + * + * + * + * + * + * + * + * + * + * + + * + * + *
Rate NameDescription
"globalPosition"Global position.
"localPosition"Local position.
"avatarBoundingBox"Avatar bounding box.
"avatarOrientation"Avatar orientation.
"avatarScale"Avatar scale.
"lookAtPosition"Look-at position.
"audioLoudness"Audio loudness.
"sensorToWorkMatrix"Sensor-to-world matrix.
"additionalFlags"Additional avatar flags.
"parentInfo"Parent information.
"faceTracker"Face tracker data.
"jointData"Joint data.
"farGrabJointData"Far grab joint data.
""When no rate name is specified, the overall update rate is provided.
+ * + * @typedef {string} AvatarUpdateRate + */ float AvatarData::getUpdateRate(const QString& rateName) const { if (rateName == "") { return _parseBufferUpdateRate.rate(); @@ -1911,42 +1991,16 @@ QUrl AvatarData::getWireSafeSkeletonModelURL() const { } } -qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion) { - - qint64 bytesWritten = 0; - - if (traitType == AvatarTraits::SkeletonModelURL) { - - QByteArray encodedSkeletonURL = getWireSafeSkeletonModelURL().toEncoded(); - - if (encodedSkeletonURL.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack simple trait" << traitType << "of size" << encodedSkeletonURL.size() - << "bytes since it exceeds the maximum size" << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size(); - bytesWritten += destination.writePrimitive(encodedURLSize); - - bytesWritten += destination.write(encodedSkeletonURL); - } - - return bytesWritten; +QByteArray AvatarData::packSkeletonModelURL() const { + return getWireSafeSkeletonModelURL().toEncoded(); } +void AvatarData::unpackSkeletonModelURL(const QByteArray& data) { + auto skeletonModelURL = QUrl::fromEncoded(data); + setSkeletonModelURL(skeletonModelURL); +} -qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar entities and check for entity data for the given ID QByteArray entityBinaryData; _avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] { @@ -1955,104 +2009,48 @@ qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitTy } }); - if (entityBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << entityBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!entityBinaryData.isNull()) { - AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size(); - - bytesWritten += destination.writePrimitive(entityBinarySize); - bytesWritten += destination.write(entityBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return entityBinaryData; } - -qint64 AvatarData::packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar grabs and check for grab data for the given ID QByteArray grabBinaryData; - _avatarGrabsLock.withReadLock([this, &grabBinaryData, &traitInstanceID] { if (_avatarGrabData.contains(traitInstanceID)) { grabBinaryData = _avatarGrabData[traitInstanceID]; } }); - if (grabBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << grabBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!grabBinaryData.isNull()) { - AvatarTraits::TraitWireSize grabBinarySize = grabBinaryData.size(); - - bytesWritten += destination.writePrimitive(grabBinarySize); - bytesWritten += destination.write(grabBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return grabBinaryData; } -qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; +QByteArray AvatarData::packTrait(AvatarTraits::TraitType traitType) const { + QByteArray traitBinaryData; + // Call packer function + if (traitType == AvatarTraits::SkeletonModelURL) { + traitBinaryData = packSkeletonModelURL(); + } + + return traitBinaryData; +} + +QByteArray AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID) { + QByteArray traitBinaryData; + + // Call packer function if (traitType == AvatarTraits::AvatarEntity) { - bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packAvatarEntityTraitInstance(traitInstanceID); } else if (traitType == AvatarTraits::Grab) { - bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packGrabTraitInstance(traitInstanceID); } - return bytesWritten; -} - -void AvatarData::prepareResetTraitInstances() { - if (_clientTraitsHandler) { - _avatarEntitiesLock.withReadLock([this]{ - foreach (auto entityID, _packedAvatarEntityData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); - } - foreach (auto grabID, _avatarGrabData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); - } - }); - } + return traitBinaryData; } void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) { if (traitType == AvatarTraits::SkeletonModelURL) { - // get the URL from the binary data - auto skeletonModelURL = QUrl::fromEncoded(traitBinaryData); - setSkeletonModelURL(skeletonModelURL); + unpackSkeletonModelURL(traitBinaryData); } } @@ -2073,6 +2071,19 @@ void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, } } +void AvatarData::prepareResetTraitInstances() { + if (_clientTraitsHandler) { + _avatarEntitiesLock.withReadLock([this]{ + foreach (auto entityID, _packedAvatarEntityData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + } + foreach (auto grabID, _avatarGrabData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); + } + }); + } +} + QByteArray AvatarData::identityByteArray(bool setIsReplicated) const { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); @@ -2721,13 +2732,16 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const } /**jsdoc + * Information on an attachment worn by the avatar. * @typedef {object} AttachmentData - * @property {string} modelUrl - * @property {string} jointName - * @property {Vec3} translation - * @property {Vec3} rotation - * @property {number} scale - * @property {boolean} soft + * @property {string} modelUrl - The URL of the model file. Models can be FBX or OBJ format. + * @property {string} jointName - The offset to apply to the model relative to the joint position. + * @property {Vec3} translation - The offset from the joint that the attachment is positioned at. + * @property {Vec3} rotation - The rotation applied to the model relative to the joint orientation. + * @property {number} scale - The scale applied to the attachment model. + * @property {boolean} soft - If true and the model has a skeleton, the bones of the attached model's skeleton are + * rotated to fit the avatar's current pose. If true, the translation, rotation, and + * scale parameters are ignored. */ QVariant AttachmentData::toVariant() const { QVariantMap result; @@ -2933,6 +2947,10 @@ float AvatarData::_avatarSortCoefficientSize { 8.0f }; float AvatarData::_avatarSortCoefficientCenter { 0.25f }; float AvatarData::_avatarSortCoefficientAge { 1.0f }; +/**jsdoc + * An object with the UUIDs of avatar entities as keys and avatar entity properties objects as values. + * @typedef {Object.} AvatarEntityMap + */ QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) { QScriptValue obj = engine->newObject(); for (auto entityID : value.keys()) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 63396a59ac..1c4b0cfc53 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -100,6 +100,9 @@ const quint32 AVATAR_MOTION_SCRIPTABLE_BITS = // Procedural audio to mouth movement is enabled 8th bit // Procedural Blink is enabled 9th bit // Procedural Eyelid is enabled 10th bit +// Procedural PROCEDURAL_BLINK_FACE_MOVEMENT is enabled 11th bit +// Procedural Collide with other avatars is enabled 12th bit +// Procedural Has Hero Priority is enabled 13th bit const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits @@ -111,8 +114,26 @@ const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit +const int HAS_HERO_PRIORITY = 12; // 13th bit (be scared) - +/**jsdoc + *

The pointing state of the hands is specified by the following values: +

+ * + * + * + * + * + * + * + * + * + * + *
ValueDescription
0No hand is pointing.
1The left hand is pointing.
2The right hand is pointing.
4It is the index finger that is pointing.
+ *

The values for the hand states are added together to give the HandState value. For example, if the left + * hand's finger is pointing, the value is 1 + 4 == 5. + * @typedef {number} HandState + */ const char HAND_STATE_NULL = 0; const char LEFT_HAND_POINTING_FLAG = 1; const char RIGHT_HAND_POINTING_FLAG = 2; @@ -411,7 +432,55 @@ class ClientTraitsHandler; class AvatarData : public QObject, public SpatiallyNestable { Q_OBJECT - // The following properties have JSDoc in MyAvatar.h and ScriptableAvatar.h + // IMPORTANT: The JSDoc for the following properties should be copied to MyAvatar.h and ScriptableAvatar.h. + /* + * @property {Vec3} position - The position of the avatar. + * @property {number} scale=1.0 - The scale of the avatar. The value can be set to anything between 0.005 and + * 1000.0. When the scale value is fetched, it may temporarily be further limited by the domain's settings. + * @property {number} density - The density of the avatar in kg/m3. The density is used to work out its mass in + * the application of physics. Read-only. + * @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} bodyYaw - The left or right rotation about an axis running from the head to the feet of the avatar. + * Yaw is sometimes called "heading". + * @property {number} bodyPitch - The rotation about an axis running from shoulder to shoulder of the avatar. Pitch is + * sometimes called "elevation". + * @property {number} bodyRoll - The rotation about an axis running from the chest to the back of the avatar. Roll is + * sometimes called "bank". + * @property {Quat} orientation - The orientation of the avatar. + * @property {Quat} headOrientation - The orientation of the avatar's head. + * @property {number} headPitch - The rotation about an axis running from ear to ear of the avatar's head. Pitch is + * sometimes called "elevation". + * @property {number} headYaw - The rotation left or right about an axis running from the base to the crown of the avatar's + * head. Yaw is sometimes called "heading". + * @property {number} headRoll - The rotation about an axis running from the nose to the back of the avatar's head. Roll is + * sometimes called "bank". + * @property {Vec3} velocity - The current velocity of the avatar. + * @property {Vec3} angularVelocity - The current angular velocity of the avatar. + * @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 {string} displayName - The avatar's display name. + * @property {string} sessionDisplayName - displayName's sanitized and default version defined by the avatar + * mixer rather than Interface clients. The result is unique among all avatars present in the domain at the time. + * @property {boolean} lookAtSnappingEnabled=true - true 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 lookAtSnappingEnabled == true. + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
+ * Deprecated: Use avatar entities instead. + * @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. + * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. + * @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. Read-only. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the + * avatar. Read-only. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. Read-only. + * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's + * size in the virtual world. Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. + */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) Q_PROPERTY(float density READ getDensity) @@ -450,6 +519,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: virtual QString getName() const override { return QString("Avatar:") + _displayName; } @@ -565,18 +636,18 @@ public: virtual bool getHasAudioEnabledFaceMovement() const { return false; } /**jsdoc - * Returns the minimum scale allowed for this avatar in the current domain. + * Gets the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. - * @function MyAvatar.getDomainMinScale - * @returns {number} minimum scale allowed for this avatar in the current domain. + * @function Avatar.getDomainMinScale + * @returns {number} The minimum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMinScale() const; /**jsdoc - * Returns the maximum scale allowed for this avatar in the current domain. + * Gets the maximum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. - * @function MyAvatar.getDomainMaxScale - * @returns {number} maximum scale allowed for this avatar in the current domain. + * @function Avatar.getDomainMaxScale + * @returns {number} The maximum scale allowed for this avatar in the current domain. */ Q_INVOKABLE float getDomainMaxScale() const; @@ -589,18 +660,18 @@ public: virtual bool canMeasureEyeHeight() const { return false; } /**jsdoc - * Provides read only access to the current eye height of the avatar. + * Gets the current eye height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. - * @function MyAvatar.getEyeHeight - * @returns {number} Eye height of avatar in meters. + * @function Avatar.getEyeHeight + * @returns {number} The eye height of the avatar. */ Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } /**jsdoc - * Provides read only access to the current height of the avatar. + * Gets the current height of the avatar. * This height is only an estimate and might be incorrect for avatars that are missing standard joints. - * @function MyAvatar.getHeight - * @returns {number} Height of avatar in meters. + * @function Avatar.getHeight + * @returns {number} The height of the avatar. */ Q_INVOKABLE virtual float getHeight() const; @@ -610,36 +681,43 @@ public: void setDomainMaximumHeight(float domainMaximumHeight); /**jsdoc - * @function MyAvatar.setHandState - * @param {string} state + * Sets the pointing state of the hands to control where the laser emanates from. If the right index finger is pointing, the + * laser emanates from the tip of that finger, otherwise it emanates from the palm. + * @function Avatar.setHandState + * @param {HandState} state - The pointing state of the hand. */ Q_INVOKABLE void setHandState(char s) { _handState = s; } /**jsdoc - * @function MyAvatar.getHandState - * @returns {string} + * Gets the pointing state of the hands to control where the laser emanates from. If the right index finger is pointing, the + * laser emanates from the tip of that finger, otherwise it emanates from the palm. + * @function Avatar.getHandState + * @returns {HandState} The pointing state of the hand. */ Q_INVOKABLE char getHandState() const { return _handState; } const QVector& getRawJointData() const { return _jointData; } /**jsdoc - * @function MyAvatar.setRawJointData - * @param {JointData[]} data + * Sets joint translations and rotations from raw joint data. + * @function Avatar.setRawJointData + * @param {JointData[]} data - The raw joint data. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setRawJointData(QVector data); /**jsdoc - * Set a specific joint's rotation and position relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's rotation and position relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointData + * @function Avatar.setJointData * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. * @example Set your avatar to it's default T-pose for a while.
* Avatar in T-pose * // Set all joint translations and rotations to defaults. @@ -654,91 +732,98 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation); /**jsdoc - * Set a specific joint's rotation relative to its parent. + * Sets a specific joint's rotation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotation + * @function Avatar.setJointRotation * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. */ Q_INVOKABLE virtual void setJointRotation(int index, const glm::quat& rotation); /**jsdoc - * Set a specific joint's translation relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's translation relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointTranslation + * @function Avatar.setJointTranslation * @param {number} index - The index of the joint. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual void setJointTranslation(int index, const glm::vec3& translation); /**jsdoc - * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default + * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly faster than the function variation that specifies the joint name.

- * @function MyAvatar.clearJointData + * @function Avatar.clearJointData * @param {number} index - The index of the joint. */ Q_INVOKABLE virtual void clearJointData(int index); /**jsdoc - * @function MyAvatar.isJointDataValid - * @param {number} index - * @returns {boolean} + * Checks that the data for a joint are valid. + * @function Avatar.isJointDataValid + * @param {number} index - The index of the joint. + * @returns {boolean} true if the joint data are valid, false if not. */ Q_INVOKABLE bool isJointDataValid(int index) const; /**jsdoc - * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. - * @function MyAvatar.getJointRotation + * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see + * Avatar Standards. + * @function Avatar.getJointRotation * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. */ Q_INVOKABLE virtual glm::quat getJointRotation(int index) const; /**jsdoc - * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. - * @function MyAvatar.getJointTranslation + * Gets the translation of a joint relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

+ * @function Avatar.getJointTranslation * @param {number} index - The index of the joint. - * @returns {Vec3} The translation of the joint relative to its parent. + * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual glm::vec3 getJointTranslation(int index) const; /**jsdoc - * Set a specific joint's rotation and position relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's rotation and position relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointData + * @function Avatar.setJointData * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ Q_INVOKABLE virtual void setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation); /**jsdoc - * Set a specific joint's rotation relative to its parent. + * Sets a specific joint's rotation relative to its parent. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotation + * @function Avatar.setJointRotation * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @example Set your avatar to its default T-pose then rotate its right arm.
@@ -759,104 +844,125 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); /**jsdoc - * Set a specific joint's translation relative to its parent. - *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * Sets a specific joint's translation relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointTranslation + * @function Avatar.setJointTranslation * @param {string} name - The name of the joint. - * @param {Vec3} translation - The translation of the joint relative to its parent. + * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. * @example Stretch your avatar's neck. Depending on the avatar you are using, you will either see a gap between * the head and body or you will see the neck stretched.
* Avatar with neck stretched * // Stretch your avatar's neck. - * MyAvatar.setJointTranslation("Neck", { x: 0, y: 25, z: 0 }); + * MyAvatar.setJointTranslation("Neck", Vec3.multiply(2, MyAvatar.getJointTranslation("Neck"))); * * // Restore your avatar's neck after 5s. * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointTranslation(const QString& name, const glm::vec3& translation); /**jsdoc - * Clear joint translations and rotations set by script for a specific joint. This restores all motion from the default + * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default * animation system including inverse kinematics for that joint. *

Note: This is slightly slower than the function variation that specifies the joint index.

- * @function MyAvatar.clearJointData + * @function Avatar.clearJointData * @param {string} name - The name of the joint. * @example Offset and restore the position of your avatar's head. - * // Move your avatar's head up by 25cm from where it should be. - * MyAvatar.setJointTranslation("Neck", { x: 0, y: 0.25, z: 0 }); + * // Stretch your avatar's neck. + * MyAvatar.setJointTranslation("Neck", Vec3.multiply(2, MyAvatar.getJointTranslation("Neck"))); * - * // Restore your avatar's head to its default position after 5s. + * // Restore your avatar's neck after 5s. * Script.setTimeout(function () { * MyAvatar.clearJointData("Neck"); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void clearJointData(const QString& name); /**jsdoc - * @function MyAvatar.isJointDataValid - * @param {string} name - * @returns {boolean} + * Checks if the data for a joint are valid. + * @function Avatar.isJointDataValid + * @param {string} name - The name of the joint. + * @returns {boolean} true if the joint data are valid, false if not. */ Q_INVOKABLE virtual bool isJointDataValid(const QString& name) const; /**jsdoc - * Get the rotation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. - * @function MyAvatar.getJointRotation + * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see + * Avatar Standards. + * @function Avatar.getJointRotation * @param {string} name - The name of the joint. * @returns {Quat} The rotation of the joint relative to its parent. * @example Report the rotation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; /**jsdoc - * Get the translation of a joint relative to its parent. For information on the joint hierarchy used, see - * Avatar Standards. - * @function MyAvatar.getJointTranslation + * Gets the translation of a joint relative to its parent, in model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

For information on the joint hierarchy used, see + * Avatar Standards.

+ * @function Avatar.getJointTranslation * @param {number} name - The name of the joint. - * @returns {Vec3} The translation of the joint relative to its parent. + * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. * @example Report the translation of your avatar's hips joint. * print(JSON.stringify(MyAvatar.getJointRotation("Hips"))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual glm::vec3 getJointTranslation(const QString& name) const; /**jsdoc - * Get the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. - * @function MyAvatar.getJointRotations + * Gets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * @function Avatar.getJointRotations * @returns {Quat[]} The rotations of all joints relative to each's parent. The values are in the same order as the array - * returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @example Report the rotations of all your avatar's joints. * print(JSON.stringify(MyAvatar.getJointRotations())); + * + * // Note: If using from the Avatar API, replace all "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QVector getJointRotations() const; /**jsdoc - * @function MyAvatar.getJointTranslations - * @returns {Vec3[]} + * Gets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in + * model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ * @function Avatar.getJointTranslations + * @returns {Vec3[]} The translations of all joints relative to each's parent, in model coordinates. The values are in the + * same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the + * Avatar API. */ Q_INVOKABLE virtual QVector getJointTranslations() const; /**jsdoc - * Set the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. + * Sets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

- * @function MyAvatar.setJointRotations + * @function Avatar.setJointRotations * @param {Quat[]} jointRotations - The rotations for all joints in the avatar. The values are in the same order as the - * array returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. + * array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. * @example Set your avatar to its default T-pose then rotate its right arm.
* Avatar in T-pose * // Set all joint translations and rotations to defaults. @@ -880,19 +986,31 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); /**jsdoc - * @function MyAvatar.setJointTranslations - * @param {Vec3[]} translations + * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in + * model coordinates. + *

Warning: These coordinates are not necessarily in meters.

+ *

Setting joint data completely overrides/replaces all motion from the default animation system including inverse + * kinematics, but just for the specified joint. So for example, if you were to procedurally manipulate the finger joints, + * the avatar's hand and head would still do inverse kinematics properly. However, as soon as you start to manipulate + * joints in the inverse kinematics chain, the inverse kinematics might not function as you expect. For example, if you set + * the rotation of the elbow, the hand inverse kinematics position won't end up in the right place.

+ * @function Avatar.setJointTranslations + * @param {Vec3[]} translations - The translations for all joints in the avatar, in model coordinates. The values are in + * the same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the + * Avatar API. */ Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); /**jsdoc - * Clear all joint translations and rotations that have been set by script. This restores all motion from the default + * Clears all joint translations and rotations that have been set by script. This restores all motion from the default * animation system including inverse kinematics for all joints. - * @function MyAvatar.clearJointsData + * @function Avatar.clearJointsData * @example Set your avatar to it's default T-pose for a while. * // Set all joint translations and rotations to defaults. * var i, length, rotation, translation; @@ -906,49 +1024,69 @@ public: * Script.setTimeout(function () { * MyAvatar.clearJointsData(); * }, 5000); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void clearJointsData(); /**jsdoc - * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by - * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. - * @function MyAvatar.getJointIndex + * Gets the joint index for a named joint. The joint index value is the position of the joint in the array returned by + * {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the Avatar API. + * @function Avatar.getJointIndex * @param {string} name - The name of the joint. - * @returns {number} The index of the joint. + * @returns {number} The index of the joint if valid, otherwise -1. * @example Report the index of your avatar's left arm joint. - * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm")); + * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm"))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const; /**jsdoc - * Get the names of all the joints in the current avatar. - * @function MyAvatar.getJointNames + * Gets the names of all the joints in the current avatar. + * @function Avatar.getJointNames * @returns {string[]} The joint names. * @example Report the names of all the joints in your current avatar. * print(JSON.stringify(MyAvatar.getJointNames())); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QStringList getJointNames() const; /**jsdoc - * @function MyAvatar.setBlendshape - * @param {string} name - * @param {number} value + * Sets the value of a blendshape to animate your avatar's face. To enable other users to see the resulting animation of + * your avatar's face, use {@link Avatar.setForceFaceTrackerConnected} or {@link MyAvatar.setForceFaceTrackerConnected}. + * @function Avatar.setBlendshape + * @param {string} name - The name of the blendshape, per the + * {@link https://docs.highfidelity.com/create/avatars/avatar-standards.html#blendshapes Avatar Standards}. + * @param {number} value - A value between 0.0 and 1.0. + * @example Open your avatar's mouth wide. + * MyAvatar.setForceFaceTrackerConnected(true); + * MyAvatar.setBlendshape("JawOpen", 1.0); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE void setBlendshape(QString name, float val) { _headData->setBlendshape(name, val); } /**jsdoc - * @function MyAvatar.getAttachmentsVariant - * @returns {object} + * Gets information about the models currently attached to your avatar. + * @function Avatar.getAttachmentsVariant + * @returns {AttachmentData[]} Information about all models attached to your avatar. + * @deprecated Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual QVariantList getAttachmentsVariant() const; /**jsdoc - * @function MyAvatar.setAttachmentsVariant - * @param {object} variant + * Sets all models currently attached to your avatar. For example, if you retrieve attachment data using + * {@link MyAvatar.getAttachmentsVariant} or {@link Avatar.getAttachmentsVariant}, make changes to it, and then want to + * update your avatar's attachments per the changed data. + * @function Avatar.setAttachmentsVariant + * @param {AttachmentData[]} variant - The attachment data defining the models to have attached to your avatar. + * @deprecated Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant); @@ -956,22 +1094,28 @@ public: virtual void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload); /**jsdoc - * @function MyAvatar.updateAvatarEntity - * @param {Uuid} entityID - * @param {string} entityData + * @function Avatar.updateAvatarEntity + * @param {Uuid} entityID - The entity ID. + * @param {Array.} entityData - Entity data. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); /**jsdoc - * @function MyAvatar.clearAvatarEntity - * @param {Uuid} entityID + * @function Avatar.clearAvatarEntity + * @param {Uuid} entityID - The entity ID. + * @param {boolean} [requiresRemovalFromTree=true] - Requires removal from tree. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); /**jsdoc - * @function MyAvatar.setForceFaceTrackerConnected - * @param {boolean} connected + * Enables blendshapes set using {@link Avatar.setBlendshape} or {@link MyAvatar.setBlendshape} to be transmitted to other + * users so that they can see the animation of your avatar's face. + * @function Avatar.setForceFaceTrackerConnected + * @param {boolean} connected - true to enable blendshape changes to be transmitted to other users, + * false to disable. */ Q_INVOKABLE void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } @@ -993,18 +1137,16 @@ public: // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged); - qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - qint64 packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - - void prepareResetTraitInstances(); + QByteArray packTrait(AvatarTraits::TraitType traitType) const; + QByteArray packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData); void processTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData); void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); + void prepareResetTraitInstances(); + QByteArray identityByteArray(bool setIsReplicated = false) const; QUrl getWireSafeSkeletonModelURL() const; @@ -1022,24 +1164,28 @@ public: } /**jsdoc - * Get information about all models currently attached to your avatar. - * @function MyAvatar.getAttachmentData + * Gets information about the models currently attached to your avatar. + * @function Avatar.getAttachmentData * @returns {AttachmentData[]} Information about all models attached to your avatar. + * @deprecated Use avatar entities instead. * @example Report the URLs of all current attachments. * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { - * print (attachments[i].modelURL); + * print(attachments[i].modelURL); * } + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual QVector getAttachmentData() const; /**jsdoc - * Set all models currently attached to your avatar. For example, if you retrieve attachment data using + * Sets all models currently attached to your avatar. For example, if you retrieve attachment data using * {@link MyAvatar.getAttachmentData} or {@link Avatar.getAttachmentData}, make changes to it, and then want to update your avatar's attachments per the * changed data. You can also remove all attachments by using setting attachmentData to null. - * @function MyAvatar.setAttachmentData - * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use + * @function Avatar.setAttachmentData + * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use * null to remove all attachments. + * @deprecated Use avatar entities instead. * @example Remove a hat attachment if your avatar is wearing it. * var hatURL = "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx"; * var attachments = MyAvatar.getAttachmentData(); @@ -1051,15 +1197,17 @@ public: * break; * } * } + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void setAttachmentData(const QVector& attachmentData); /**jsdoc - * Attach a model to your avatar. For example, you can give your avatar a hat to wear, a guitar to hold, or a surfboard to + * Attaches a model to your avatar. For example, you can give your avatar a hat to wear, a guitar to hold, or a surfboard to * stand on. *

Note: Attached models are models only; they are not entities and can not be manipulated using the {@link Entities} API. * Nor can you use this function to attach an entity (such as a sphere or a box) to your avatar.

- * @function MyAvatar.attach + * @function Avatar.attach * @param {string} modelURL - The URL of the model to attach. Models can be .FBX or .OBJ format. * @param {string} [jointName=""] - The name of the avatar joint (see {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}) to attach the model * to. @@ -1067,12 +1215,14 @@ public: * @param {Quat} [rotation=Quat.IDENTITY] - The rotation to apply to the model relative to the joint orientation. * @param {number} [scale=1.0] - The scale to apply to the model. * @param {boolean} [isSoft=false] - If the model has a skeleton, set this to true so that the bones of the - * attached model's skeleton are be rotated to fit the avatar's current pose. isSoft is used, for example, + * attached model's skeleton are rotated to fit the avatar's current pose. isSoft is used, for example, * to have clothing that moves with the avatar.
* If true, the translation, rotation, and scale parameters are * ignored. - * @param {boolean} [allowDuplicates=false] - * @param {boolean} [useSaved=true] + * @param {boolean} [allowDuplicates=false] - If true then more than one copy of any particular model may be + * attached to the same joint; if false then the same model cannot be attached to the same joint. + * @param {boolean} [useSaved=true] - Not used. + * @deprecated Use avatar entities instead. * @example Attach a cowboy hat to your avatar's head. * var attachment = { * modelURL: "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx", @@ -1089,6 +1239,8 @@ public: * attachment.rotation, * attachment.scale, * attachment.isSoft); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ Q_INVOKABLE virtual void attach(const QString& modelURL, const QString& jointName = QString(), const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), @@ -1096,20 +1248,22 @@ public: bool allowDuplicates = false, bool useSaved = true); /**jsdoc - * Detach the most recently attached instance of a particular model from either a specific joint or any joint. - * @function MyAvatar.detachOne + * Detaches the most recently attached instance of a particular model from either a specific joint or any joint. + * @function Avatar.detachOne * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the most * recently attached model is removed from which ever joint it was attached to. + * @deprecated Use avatar entities instead. */ Q_INVOKABLE virtual void detachOne(const QString& modelURL, const QString& jointName = QString()); /**jsdoc - * Detach all instances of a particular model from either a specific joint or all joints. - * @function MyAvatar.detachAll + * Detaches all instances of a particular model from either a specific joint or all joints. + * @function Avatar.detachAll * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the model is * detached from all joints. + * @deprecated Use avatar entities instead. */ Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString()); @@ -1121,6 +1275,18 @@ public: int getAverageBytesReceivedPerSecond() const; int getReceiveRate() const; + // An Avatar can be set Priority from the AvatarMixer side. + bool getHasPriority() const { return _hasPriority; } + // regular setHasPriority does a check of state changed and if true reset 'additionalFlagsChanged' timestamp + void setHasPriority(bool hasPriority) { + if (_hasPriority != hasPriority) { + _additionalFlagsChanged = usecTimestampNow(); + _hasPriority = hasPriority; + } + } + // In some cases, we want to assign the hasPRiority flag without reseting timestamp + void setHasPriorityWithoutTimestampReset(bool hasPriority) { _hasPriority = hasPriority; } + const glm::vec3& getTargetVelocity() const { return _targetVelocity; } void clearRecordingBasis(); @@ -1136,14 +1302,12 @@ public: AABox getDefaultBubbleBox() const; /**jsdoc - * @function MyAvatar.getAvatarEntityData - * @returns {object} + * @comment Documented in derived classes' JSDoc because implementations are different. */ Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const; /**jsdoc - * @function MyAvatar.setAvatarEntityData - * @param {object} avatarEntityData + * @comment Documented in derived classes' JSDoc because implementations are different. */ Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); @@ -1151,45 +1315,69 @@ public: AvatarEntityIDs getAndClearRecentlyRemovedIDs(); /**jsdoc - * @function MyAvatar.getSensorToWorldMatrix - * @returns {Mat4} + * Gets the transform from the user's real world to the avatar's size, orientation, and position in the virtual world. + * @function Avatar.getSensorToWorldMatrix + * @returns {Mat4} The scale, rotation, and translation transform from the user's real world to the avatar's size, + * orientation, and position in the virtual world. + * @example Report the sensor to world matrix. + * var sensorToWorldMatrix = MyAvatar.getSensorToWorldMatrix(); + * print("Sensor to woprld matrix: " + JSON.stringify(sensorToWorldMatrix)); + * print("Rotation: " + JSON.stringify(Mat4.extractRotation(sensorToWorldMatrix))); + * print("Translation: " + JSON.stringify(Mat4.extractTranslation(sensorToWorldMatrix))); + * print("Scale: " + JSON.stringify(Mat4.extractScale(sensorToWorldMatrix))); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; /**jsdoc - * @function MyAvatar.getSensorToWorldScale - * @returns {number} + * Gets the scale that transforms dimensions in the user's real world to the avatar's size in the virtual world. + * @function Avatar.getSensorToWorldScale + * @returns {number} The scale that transforms dimensions in the user's real world to the avatar's size in the virtual + * world. */ // thread safe Q_INVOKABLE float getSensorToWorldScale() const; /**jsdoc - * @function MyAvatar.getControllerLeftHandMatrix - * @returns {Mat4} + * Gets the rotation and translation of the left hand controller relative to the avatar. + * @function Avatar.getControllerLeftHandMatrix + * @returns {Mat4} The rotation and translation of the left hand controller relative to the avatar. + * @example Report the left hand controller matrix. + * var leftHandMatrix = MyAvatar.getControllerLeftHandMatrix(); + * print("Controller left hand matrix: " + JSON.stringify(leftHandMatrix)); + * print("Rotation: " + JSON.stringify(Mat4.extractRotation(leftHandMatrix))); + * print("Translation: " + JSON.stringify(Mat4.extractTranslation(leftHandMatrix))); + * print("Scale: " + JSON.stringify(Mat4.extractScale(leftHandMatrix))); // Always 1,1,1. + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; /**jsdoc - * @function MyAvatar.getControllerRightHandMatrix - * @returns {Mat4} + * Gets the rotation and translation of the right hand controller relative to the avatar. + * @function Avatar.getControllerRightHandMatrix + * @returns {Mat4} The rotation and translation of the right hand controller relative to the avatar. */ // thread safe Q_INVOKABLE glm::mat4 getControllerRightHandMatrix() const; /**jsdoc - * @function MyAvatar.getDataRate - * @param {string} [rateName=""] - * @returns {number} + * Gets the amount of avatar mixer data being generated by the avatar. + * @function Avatar.getDataRate + * @param {AvatarDataRate} [rateName=""] - The type of avatar mixer data to get the data rate of. + * @returns {number} The data rate in kbps. */ Q_INVOKABLE float getDataRate(const QString& rateName = QString("")) const; /**jsdoc - * @function MyAvatar.getUpdateRate - * @param {string} [rateName=""] - * @returns {number} + * Gets the update rate of avatar mixer data being generated by the avatar. + * @function Avatar.getUpdateRate + * @param {AvatarUpdateRate} [rateName=""] - The type of avatar mixer data to get the update rate of. + * @returns {number} The update rate in Hz. */ Q_INVOKABLE float getUpdateRate(const QString& rateName = QString("")) const; @@ -1235,52 +1423,92 @@ public: signals: /**jsdoc - * @function MyAvatar.displayNameChanged + * Triggered when the avatar's displayName property value changes. + * @function Avatar.displayNameChanged * @returns {Signal} + * @example Report when your avatar display name changes. + * MyAvatar.displayNameChanged.connect(function () { + * print("Avatar display name changed to: " + MyAvatar.displayName); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void displayNameChanged(); /**jsdoc - * @function MyAvatar.sessionDisplayNameChanged + * Triggered when the avatar's sessionDisplayName property value changes. + * @function Avatar.sessionDisplayNameChanged * @returns {Signal} + * @example Report when your avatar's session display name changes. + * MyAvatar.sessionDisplayNameChanged.connect(function () { + * print("Avatar session display name changed to: " + MyAvatar.sessionDisplayName); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void sessionDisplayNameChanged(); /**jsdoc - * @function MyAvatar.skeletonModelURLChanged + * Triggered when the avatar's model (i.e., skeletonModelURL property value) is changed. + * @function Avatar.skeletonModelURLChanged * @returns {Signal} + * @example Report when your avatar's skeleton model changes. + * MyAvatar.skeletonModelURLChanged.connect(function () { + * print("Skeleton model changed to: " + MyAvatar.skeletonModelURL); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void skeletonModelURLChanged(); /**jsdoc - * @function MyAvatar.lookAtSnappingChanged - * @param {boolean} enabled + * Triggered when the avatar's lookAtSnappingEnabled property value changes. + * @function Avatar.lookAtSnappingChanged + * @param {boolean} enabled - true if look-at snapping is enabled, false if not. * @returns {Signal} + * @example Report when your look-at snapping setting changes. + * MyAvatar.lookAtSnappingChanged.connect(function () { + * print("Avatar look-at snapping changed to: " + MyAvatar.lookAtSnappingEnabled); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void lookAtSnappingChanged(bool enabled); /**jsdoc - * @function MyAvatar.sessionUUIDChanged + * Triggered when the avatar's sessionUUID property value changes. + * @function Avatar.sessionUUIDChanged * @returns {Signal} + * @example Report when your avatar's session UUID changes. + * MyAvatar.sessionUUIDChanged.connect(function () { + * print("Avatar session UUID changed to: " + MyAvatar.sessionUUID); + * }); + * + * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ void sessionUUIDChanged(); public slots: /**jsdoc - * @function MyAvatar.sendAvatarDataPacket - * @param {boolean} [sendAll=false] + * @function Avatar.sendAvatarDataPacket + * @param {boolean} [sendAll=false] - Send all. + * @returns {number} + * @deprecated This function is deprecated and will be removed. */ virtual int sendAvatarDataPacket(bool sendAll = false); /**jsdoc - * @function MyAvatar.sendIdentityPacket + * @function Avatar.sendIdentityPacket + * @returns {number} + * @deprecated This function is deprecated and will be removed. */ int sendIdentityPacket(); /**jsdoc - * @function MyAvatar.setSessionUUID - * @param {Uuid} sessionUUID + * @function Avatar.setSessionUUID + * @param {Uuid} sessionUUID - Session UUID. + * @deprecated This function is deprecated and will be removed. */ virtual void setSessionUUID(const QUuid& sessionUUID) { if (sessionUUID != getID()) { @@ -1293,44 +1521,61 @@ public slots: } } + /**jsdoc - * @function MyAvatar.getAbsoluteJointRotationInObjectFrame - * @param {number} index - * @returns {Quat} + * Gets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

+ * @function Avatar.getAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @returns {Quat} Quat.IDENTITY. */ virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; /**jsdoc - * @function MyAvatar.getAbsoluteJointTranslationInObjectFrame - * @param {number} index - * @returns {Vec3} + * Gets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

+ * @function Avatar.getAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @returns {Vec3} Vec3.ZERO. */ virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; /**jsdoc - * @function MyAvatar.setAbsoluteJointRotationInObjectFrame - * @param {number} index - * @param {Quat} rotation - * @returns {boolean} + * Sets the rotation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

+ * @function Avatar.setAbsoluteJointRotationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. + * @returns {boolean} false. */ virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } /**jsdoc - * @function MyAvatar.setAbsoluteJointTranslationInObjectFrame - * @param {number} index - * @param {Vec3} translation - * @returns {boolean} + * Sets the translation of a joint relative to the avatar. + *

Warning: Not able to be used in the Avatar API.

+ * @function Avatar.setAbsoluteJointTranslationInObjectFrame + * @param {number} index - The index of the joint. Not used. + * @param {Vec3} translation - The translation of the joint relative to the avatar. Not used. + * @returns {boolean} false. */ virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } /**jsdoc - * @function MyAvatar.getTargetScale - * @returns {number} + * Gets the target scale of the avatar without any restrictions on permissible values imposed by the domain. In contrast, the + * scale property's value may be limited by the domain's settings. + * @function Avatar.getTargetScale + * @returns {number} The target scale of the avatar. + * @example Compare the target and current avatar scales. + * print("Current avatar scale: " + MyAvatar.scale); + * print("Target avatar scale: " + MyAvatar.getTargetScale()); + * + * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ float getTargetScale() const { return _targetScale; } // why is this a slot? /**jsdoc - * @function MyAvatar.resetLastSent + * @function Avatar.resetLastSent + * @deprecated This function is deprecated and will be removed. */ void resetLastSent() { _lastToByteArray = 0; } @@ -1352,13 +1597,13 @@ protected: bool hasParent() const { return !getParentID().isNull(); } bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; } - qint64 packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); - qint64 packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); + QByteArray packSkeletonModelURL() const; + QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + void unpackSkeletonModelURL(const QByteArray& data); + + // isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master" // Audio Mixer that the replicated avatar is connected to. bool _isReplicated{ false }; @@ -1462,6 +1707,7 @@ protected: glm::vec3 _globalBoundingBoxOffset; AABox _defaultBubbleBox; + AABox _fitBoundingBox; mutable ReadWriteLockable _avatarEntitiesLock; AvatarEntityIDs _avatarEntityRemoved; // recently removed AvatarEntity ids @@ -1498,6 +1744,7 @@ protected: bool _isNewAvatar { true }; bool _isClientAvatar { false }; bool _collideWithOtherAvatars { true }; + bool _hasPriority{ false }; // null unless MyAvatar or ScriptableAvatar sending traits data to mixer std::unique_ptr _clientTraitsHandler; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index c16d65506a..3abd352778 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -439,7 +439,6 @@ void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason remo } auto removedAvatar = _avatarHash.take(sessionUUID); - if (removedAvatar) { removedAvatars.push_back(removedAvatar); } diff --git a/libraries/avatars/src/AvatarTraits.cpp b/libraries/avatars/src/AvatarTraits.cpp new file mode 100644 index 0000000000..724f30e2f3 --- /dev/null +++ b/libraries/avatars/src/AvatarTraits.cpp @@ -0,0 +1,135 @@ +// +// AvatarTraits.cpp +// libraries/avatars/src +// +// Created by Clement Brisset on 3/19/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AvatarTraits.h" + +#include + +#include "AvatarData.h" + +namespace AvatarTraits { + + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion) { + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive(traitType); + if (traitVersion > DEFAULT_TRAIT_VERSION) { + bytesWritten += destination.writePrimitive(traitVersion); + } + bytesWritten += destination.write(instanceID.toRfc4122()); + bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); + return bytesWritten; + } +}; diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 4516572e42..13d64ec225 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -14,20 +14,35 @@ #include #include +#include #include #include +class ExtendedIODevice; +class AvatarData; + namespace AvatarTraits { enum TraitType : int8_t { + // Null trait NullTrait = -1, - SkeletonModelURL, + + // Simple traits + SkeletonModelURL = 0, + + // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, Grab, + + // Traits count TotalTraitTypes }; + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; + const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; + const int NUM_TRAITS = (int)TotalTraitTypes; + using TraitInstanceID = QUuid; inline bool isSimpleTrait(TraitType traitType) { @@ -46,22 +61,19 @@ namespace AvatarTraits { const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0; const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX; - inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, - TraitVersion traitVersion = NULL_TRAIT_VERSION) { - qint64 bytesWritten = 0; + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar); + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar); - bytesWritten += destination.writePrimitive(traitType); + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar); + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar); - if (traitVersion > DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion = NULL_TRAIT_VERSION); - bytesWritten += destination.write(instanceID.toRfc4122()); - - bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); - - return bytesWritten; - } }; #endif // hifi_AvatarTraits_h diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index bcbe5308c7..f6bd66e89a 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -106,9 +106,10 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); if (initialSend || *simpleIt == Updated) { - if (traitType == AvatarTraits::SkeletonModelURL) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar); + + if (traitType == AvatarTraits::SkeletonModelURL) { // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; } @@ -124,7 +125,9 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { || instanceIDValuePair.value == Updated) { // this is a changed trait we need to send or we haven't send out trait information yet // ask the owning avatar to pack it - bytesWritten += _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList); + bytesWritten += AvatarTraits::packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, + *traitsPacketList, *_owningAvatar); + } else if (!initialSend && instanceIDValuePair.value == Deleted) { // pack delete for this trait instance bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id, @@ -162,11 +165,11 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer m // override the skeleton URL but do not mark the trait as having changed // so that we don't unecessarily send a new trait packet to the mixer with the overriden URL - auto encodedSkeletonURL = QUrl::fromEncoded(message->readWithoutCopy(traitBinarySize)); auto hasChangesBefore = _hasChangedTraits; - _owningAvatar->setSkeletonModelURL(encodedSkeletonURL); + auto traitBinaryData = message->readWithoutCopy(traitBinarySize); + _owningAvatar->processTrait(traitType, traitBinaryData); // setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to // avoid unnecessarily sending the overriden skeleton model URL back to the mixer diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index a716a40ad8..18717c8ca3 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -343,6 +343,14 @@ glm::mat4 ScriptAvatarData::getControllerRightHandMatrix() const { // END // +bool ScriptAvatarData::getHasPriority() const { + if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { + return sharedAvatarData->getHasPriority(); + } else { + return false; + } +} + glm::quat ScriptAvatarData::getAbsoluteJointRotationInObjectFrame(int index) const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { return sharedAvatarData->getAbsoluteJointRotationInObjectFrame(index); diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 91bac61728..01f7ff360a 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -68,6 +68,8 @@ class ScriptAvatarData : public QObject { Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: ScriptAvatarData(AvatarSharedPointer avatarData); @@ -133,6 +135,8 @@ public: glm::mat4 getControllerLeftHandMatrix() const; glm::mat4 getControllerRightHandMatrix() const; + bool getHasPriority() const; + signals: void displayNameChanged(); void sessionDisplayNameChanged(); diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index cce76f152f..73618427f6 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -1,8 +1,6 @@ set(TARGET_NAME baking) setup_hifi_library(Concurrent) -link_hifi_libraries(shared graphics networking ktx image fbx) +link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx model-baker task) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) - -target_draco() diff --git a/libraries/baking/src/Baker.h b/libraries/baking/src/Baker.h index c1b2ddf959..611f992c96 100644 --- a/libraries/baking/src/Baker.h +++ b/libraries/baking/src/Baker.h @@ -52,7 +52,7 @@ protected: void handleErrors(const QStringList& errors); // List of baked output files. For instance, for an FBX this would - // include the .fbx and all of its texture files. + // include the .fbx, a .fst pointing to the fbx, and all of the fbx texture files. std::vector _outputFiles; QStringList _errorList; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index afaca1dd62..2189e7bdc3 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -33,29 +33,19 @@ #include "ModelBakingLoggingCategory.h" #include "TextureBaker.h" -#ifdef HIFI_DUMP_FBX -#include "FBXToJSON.h" -#endif - -void FBXBaker::bake() { - qDebug() << "FBXBaker" << _modelURL << "bake starting"; - - // setup the output folder for the results of this bake - setupOutputFolder(); - - if (shouldStop()) { - return; +FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputModelURL, inputTextureThreadGetter, 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)); + QUrl newInputModelURL = inputModelURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath); + _modelURL = newInputModelURL; } - - connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy); - - // make a local copy of the FBX file - loadSourceFBX(); } -void FBXBaker::bakeSourceCopy() { - // load the scene from the FBX file - importScene(); +void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { + _hfmModel = hfmModel; if (shouldStop()) { return; @@ -68,222 +58,100 @@ void FBXBaker::bakeSourceCopy() { return; } - rewriteAndBakeSceneModels(); + rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists); +} - if (shouldStop()) { +void FBXBaker::replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { + // Compress mesh information and store in dracoMeshNode + FBXNode dracoMeshNode; + bool success = buildDracoMeshNode(dracoMeshNode, dracoMeshBytes, dracoMaterialList); + + if (!success) { return; - } - - // check if we're already done with textures (in case we had none to re-write) - checkIfTexturesFinished(); -} - -void FBXBaker::setupOutputFolder() { - // make sure there isn't already an output directory using the same name - if (QDir(_bakedOutputDir).exists()) { - qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; } else { - qCDebug(model_baking) << "Creating FBX output folder" << _bakedOutputDir; + meshNode.children.push_back(dracoMeshNode); - // attempt to make the output folder - if (!QDir().mkpath(_bakedOutputDir)) { - handleError("Failed to create FBX output folder " + _bakedOutputDir); - return; - } - // attempt to make the output folder - if (!QDir().mkpath(_originalOutputDir)) { - handleError("Failed to create FBX output folder " + _originalOutputDir); - return; - } - } -} + static const std::vector nodeNamesToDelete { + // Node data that is packed into the draco mesh + "Vertices", + "PolygonVertexIndex", + "LayerElementNormal", + "LayerElementColor", + "LayerElementUV", + "LayerElementMaterial", + "LayerElementTexture", -void FBXBaker::loadSourceFBX() { - // check if the FBX is local or first needs to be downloaded - if (_modelURL.isLocalFile()) { - // load up the local file - QFile localFBX { _modelURL.toLocalFile() }; - - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; - - if (!localFBX.exists()) { - //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); - handleError("Could not find " + _modelURL.toString()); - return; - } - - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localFBX.copy(_originalModelFilePath); - - // emit our signal to start the import of the FBX source copy - emit sourceCopyReadyToLoad(); - } else { - // remote file, kick off a download - auto& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkRequest networkRequest; - - // setup the request to follow re-directs and always hit the network - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - - networkRequest.setUrl(_modelURL); - - qCDebug(model_baking) << "Downloading" << _modelURL; - auto networkReply = networkAccessManager.get(networkRequest); - - connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); - } -} - -void FBXBaker::handleFBXNetworkReply() { - auto requestReply = qobject_cast(sender()); - - if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded" << _modelURL; - - // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); - - qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << copyOfOriginal.fileName(); - - if (!copyOfOriginal.open(QIODevice::WriteOnly)) { - // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); - return; - } - if (copyOfOriginal.write(requestReply->readAll()) == -1) { - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); - return; - } - - // close that file now that we are done writing to it - copyOfOriginal.close(); - - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - // emit our signal to start the import of the FBX source copy - emit sourceCopyReadyToLoad(); - } else { - // add an error to our list stating that the FBX could not be downloaded - handleError("Failed to download " + _modelURL.toString()); - } -} - -void FBXBaker::importScene() { - qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists(); - - QFile fbxFile(_originalModelFilePath); - if (!fbxFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); - return; - } - - FBXSerializer fbxSerializer; - - qCDebug(model_baking) << "Parsing" << _modelURL; - _rootNode = fbxSerializer._rootNode = fbxSerializer.parseFBX(&fbxFile); - -#ifdef HIFI_DUMP_FBX - { - FBXToJSON fbxToJSON; - fbxToJSON << _rootNode; - QFileInfo modelFile(_originalModelFilePath); - QString outFilename(_bakedOutputDir + "/" + modelFile.completeBaseName() + "_FBX.json"); - QFile jsonFile(outFilename); - if (jsonFile.open(QIODevice::WriteOnly)) { - jsonFile.write(fbxToJSON.str().c_str(), fbxToJSON.str().length()); - jsonFile.close(); - } - } -#endif - - _hfmModel = fbxSerializer.extractHFMModel({}, _modelURL.toString()); - _textureContentMap = fbxSerializer._textureContent; -} - -void FBXBaker::rewriteAndBakeSceneModels() { - unsigned int meshIndex = 0; - bool hasDeformers { false }; - for (FBXNode& rootChild : _rootNode.children) { - if (rootChild.name == "Objects") { - for (FBXNode& objectChild : rootChild.children) { - if (objectChild.name == "Deformer") { - hasDeformers = true; - break; - } + // Node data that we don't support + "Edges", + "LayerElementTangent", + "LayerElementBinormal", + "LayerElementSmoothing" + }; + auto& children = meshNode.children; + auto it = children.begin(); + while (it != children.end()) { + auto begin = nodeNamesToDelete.begin(); + auto end = nodeNamesToDelete.end(); + if (find(begin, end, it->name) != end) { + it = children.erase(it); + } else { + ++it; } } - if (hasDeformers) { - break; - } } +} + +void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { + std::vector meshIndexToRuntimeOrder; + auto meshCount = (int)meshes.size(); + meshIndexToRuntimeOrder.resize(meshCount); + for (int i = 0; i < meshCount; i++) { + meshIndexToRuntimeOrder[meshes[i].meshIndex] = i; + } + + // The meshIndex represents the order in which the meshes are loaded from the FBX file + // We replicate this order by iterating over the meshes in the same way that FBXSerializer does + int meshIndex = 0; for (FBXNode& rootChild : _rootNode.children) { if (rootChild.name == "Objects") { - for (FBXNode& objectChild : rootChild.children) { - if (objectChild.name == "Geometry") { - - // TODO Pull this out of _hfmModel instead so we don't have to reprocess it - auto extractedMesh = FBXSerializer::extractMesh(objectChild, meshIndex, false); - - // Callback to get MaterialID - GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) { - return extractedMesh.partMaterialTextures[partIndex].first; - }; - - // Compress mesh information and store in dracoMeshNode - FBXNode dracoMeshNode; - bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback); - - // if bake fails - return, if there were errors and continue, if there were warnings. - if (!success) { - if (hasErrors()) { - return; - } else if (hasWarnings()) { - continue; - } - } else { - objectChild.children.push_back(dracoMeshNode); - - static const std::vector nodeNamesToDelete { - // Node data that is packed into the draco mesh - "Vertices", - "PolygonVertexIndex", - "LayerElementNormal", - "LayerElementColor", - "LayerElementUV", - "LayerElementMaterial", - "LayerElementTexture", - - // Node data that we don't support - "Edges", - "LayerElementTangent", - "LayerElementBinormal", - "LayerElementSmoothing" - }; - auto& children = objectChild.children; - auto it = children.begin(); - while (it != children.end()) { - auto begin = nodeNamesToDelete.begin(); - auto end = nodeNamesToDelete.end(); - if (find(begin, end, it->name) != end) { - it = children.erase(it); - } else { - ++it; + for (FBXNode& object : rootChild.children) { + if (object.name == "Geometry") { + if (object.properties.at(2) == "Mesh") { + int meshNum = meshIndexToRuntimeOrder[meshIndex]; + replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + meshIndex++; + } + } 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 + static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); + static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation"); + static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling"); + for (int i = 0; i < modelChild.children.size(); i++) { + const auto& prop = modelChild.children[i]; + const auto& propertyName = prop.properties.at(0); + if (propertyName == GEOMETRIC_TRANSLATION || + propertyName == GEOMETRIC_ROTATION || + propertyName == GEOMETRIC_SCALING) { + modelChild.children.removeAt(i); + --i; + } } + } else if (modelChild.name == "Vertices") { + // This model is also a mesh + int meshNum = meshIndexToRuntimeOrder[meshIndex]; + replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + meshIndex++; } } - } // Geometry Object + } - } // foreach root child + if (hasErrors()) { + return; + } + } } } } diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 2af51b2190..59ef5e349d 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -31,31 +31,18 @@ using TextureBakerThreadGetter = std::function; class FBXBaker : public ModelBaker { Q_OBJECT public: - using ModelBaker::ModelBaker; + FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); -public slots: - virtual void bake() override; - -signals: - void sourceCopyReadyToLoad(); - -private slots: - void bakeSourceCopy(); - void handleFBXNetworkReply(); +protected: + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void setupOutputFolder(); - - void loadSourceFBX(); - - void importScene(); - void embedTextureMetaData(); - void rewriteAndBakeSceneModels(); + void rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists); void rewriteAndBakeSceneTextures(); + void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); - HFMModel* _hfmModel; - QHash _textureNameMatchCount; - QHash _remappedTexturePaths; + hfm::Model::Pointer _hfmModel; bool _pendingErrorEmission { false }; }; diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index b19336f4ca..96d7247a82 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -11,9 +11,11 @@ #include "JSBaker.h" -#include +#include -#include "Baker.h" +#include +#include +#include const int ASCII_CHARACTERS_UPPER_LIMIT = 126; @@ -21,25 +23,79 @@ JSBaker::JSBaker(const QUrl& jsURL, const QString& bakedOutputDir) : _jsURL(jsURL), _bakedOutputDir(bakedOutputDir) { - } void JSBaker::bake() { qCDebug(js_baking) << "JS Baker " << _jsURL << "bake starting"; - // Import file to start baking - QFile jsFile(_jsURL.toLocalFile()); - if (!jsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - handleError("Error opening " + _jsURL.fileName() + " for reading"); - return; - } + // once our script is loaded, kick off a the processing + connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript); + if (_originalScript.isEmpty()) { + // first load the script (either locally or remotely) + loadScript(); + } else { + // we already have a script passed to us, use that + processScript(); + } +} + +void JSBaker::loadScript() { + // check if the script is local or first needs to be downloaded + if (_jsURL.isLocalFile()) { + // load up the local file + QFile localScript(_jsURL.toLocalFile()); + if (!localScript.open(QIODevice::ReadOnly | QIODevice::Text)) { + handleError("Error opening " + _jsURL.fileName() + " for reading"); + return; + } + + _originalScript = localScript.readAll(); + + emit originalScriptLoaded(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_jsURL); + + qCDebug(js_baking) << "Downloading" << _jsURL; + + // kickoff the download, wait for slot to tell us it is done + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &JSBaker::handleScriptNetworkReply); + } +} + +void JSBaker::handleScriptNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(js_baking) << "Downloaded script" << _jsURL; + + // store the original script so it can be passed along for the bake + _originalScript = requestReply->readAll(); + + emit originalScriptLoaded(); + } else { + // add an error to our list stating that this script could not be downloaded + handleError("Error downloading " + _jsURL.toString() + " - " + requestReply->errorString()); + } +} + +void JSBaker::processScript() { // Read file into an array - QByteArray inputJS = jsFile.readAll(); QByteArray outputJS; // Call baking on inputJS and store result in outputJS - bool success = bakeJS(inputJS, outputJS); + bool success = bakeJS(_originalScript, outputJS); if (!success) { qCDebug(js_baking) << "Bake Failed"; handleError("Unterminated multi-line comment"); diff --git a/libraries/baking/src/JSBaker.h b/libraries/baking/src/JSBaker.h index a7c3e62174..7eda85fa6d 100644 --- a/libraries/baking/src/JSBaker.h +++ b/libraries/baking/src/JSBaker.h @@ -25,11 +25,24 @@ public: JSBaker(const QUrl& jsURL, const QString& bakedOutputDir); static bool bakeJS(const QByteArray& inputFile, QByteArray& outputFile); + QString getJSPath() const { return _jsURL.toDisplayString(); } + QString getBakedJSFilePath() const { return _bakedJSFilePath; } + public slots: virtual void bake() override; +signals: + void originalScriptLoaded(); + +private slots: + void processScript(); + private: + void loadScript(); + void handleScriptNetworkReply(); + QUrl _jsURL; + QByteArray _originalScript; QString _bakedOutputDir; QString _bakedJSFilePath; diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp new file mode 100644 index 0000000000..dd1ba55e54 --- /dev/null +++ b/libraries/baking/src/MaterialBaker.cpp @@ -0,0 +1,247 @@ +// +// MaterialBaker.cpp +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "MaterialBaker.h" + +#include + +#include "QJsonObject" +#include "QJsonDocument" + +#include "MaterialBakingLoggingCategory.h" + +#include +#include + +#include + +std::function MaterialBaker::_getNextOvenWorkerThreadOperator; + +static int materialNum = 0; + +namespace std { + template <> + struct hash { + size_t operator()(const graphics::Material::MapChannel& a) const { + return std::hash()((size_t)a); + } + }; +}; + +MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) : + _materialData(materialData), + _isURL(isURL), + _bakedOutputDir(bakedOutputDir), + _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)), + _destinationPath(destinationPath) +{ +} + +void MaterialBaker::bake() { + qDebug(material_baking) << "Material Baker" << _materialData << "bake starting"; + + // once our script is loaded, kick off a the processing + connect(this, &MaterialBaker::originalMaterialLoaded, this, &MaterialBaker::processMaterial); + + if (!_materialResource) { + // first load the material (either locally or remotely) + loadMaterial(); + } else { + // we already have a material passed to us, use that + if (_materialResource->isLoaded()) { + processMaterial(); + } else { + connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + } + } +} + +void MaterialBaker::loadMaterial() { + if (!_isURL) { + qCDebug(material_baking) << "Loading local material" << _materialData; + + _materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource()); + // TODO: add baseURL to allow these to reference relative files next to them + _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(_materialData.toUtf8()), QUrl()); + } else { + qCDebug(material_baking) << "Downloading material" << _materialData; + _materialResource = MaterialCache::instance().getMaterial(_materialData); + } + + if (_materialResource) { + if (_materialResource->isLoaded()) { + emit originalMaterialLoaded(); + } else { + connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + } + } else { + handleError("Error loading " + _materialData); + } +} + +void MaterialBaker::processMaterial() { + if (!_materialResource || _materialResource->parsedMaterials.networkMaterials.size() == 0) { + handleError("Error processing " + _materialData); + return; + } + + if (QDir(_textureOutputDir).exists()) { + qWarning() << "Output path" << _textureOutputDir << "already exists. Continuing."; + } else { + qCDebug(material_baking) << "Creating materialTextures output folder" << _textureOutputDir; + if (!QDir().mkpath(_textureOutputDir)) { + handleError("Failed to create materialTextures output folder " + _textureOutputDir); + } + } + + 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(); + + QUrl url = texture->getUrl(); + QString cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + auto idx = cleanURL.lastIndexOf('.'); + auto extension = idx >= 0 ? url.toDisplayString().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 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 textureKey(textureURL, it->second); + if (!_textureBakers.contains(textureKey)) { + auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), it->second); + + QSharedPointer textureBaker { + new TextureBaker(textureURL, it->second, _textureOutputDir, "", baseTextureFileName), + &TextureBaker::deleteLater + }; + textureBaker->setMapChannel(mapChannel); + connect(textureBaker.data(), &TextureBaker::finished, this, &MaterialBaker::handleFinishedTextureBaker); + _textureBakers.insert(textureKey, textureBaker); + textureBaker->moveToThread(_getNextOvenWorkerThreadOperator ? _getNextOvenWorkerThreadOperator() : thread()); + QMetaObject::invokeMethod(textureBaker.data(), "bake"); + } + _materialsNeedingRewrite.insert(textureKey, networkMaterial.second); + } else { + qCDebug(material_baking) << "Texture extension not supported: " << extension; + } + } + } + } + } + + if (_textureBakers.empty()) { + outputMaterial(); + } +} + +void MaterialBaker::handleFinishedTextureBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + QPair textureKey = { baker->getTextureURL(), baker->getTextureType() }; + if (!baker->hasErrors()) { + // this TextureBaker is done and everything went according to plan + qCDebug(material_baking) << "Re-writing texture references to" << baker->getTextureURL(); + + auto newURL = QUrl(_textureOutputDir).resolved(baker->getMetaTextureFileName()); + auto relativeURL = QDir(_bakedOutputDir).relativeFilePath(newURL.toString()); + + // Replace the old texture URLs + for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) { + networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(_destinationPath.resolved(relativeURL)); + } + } else { + // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the texture to our warnings + _warningList << baker->getWarnings(); + } + + _materialsNeedingRewrite.remove(textureKey); + _textureBakers.remove(textureKey); + + if (_textureBakers.empty()) { + outputMaterial(); + } + } else { + handleWarning("Unidentified baker finished and signaled to material baker to handle texture. Material: " + _materialData); + } +} + +void MaterialBaker::outputMaterial() { + if (_materialResource) { + QJsonObject json; + if (_materialResource->parsedMaterials.networkMaterials.size() == 1) { + auto networkMaterial = _materialResource->parsedMaterials.networkMaterials.begin(); + auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial->second); + QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); + json.insert("materials", QJsonDocument::fromVariant(materialVariant).object()); + } else { + QJsonArray materialArray; + for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { + auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial.second); + QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); + materialArray.append(QJsonDocument::fromVariant(materialVariant).object()); + } + json.insert("materials", materialArray); + } + + QByteArray outputMaterial = QJsonDocument(json).toJson(QJsonDocument::Compact); + if (_isURL) { + auto fileName = QUrl(_materialData).fileName(); + auto baseName = fileName.left(fileName.lastIndexOf('.')); + auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION; + + _bakedMaterialData = _bakedOutputDir + "/" + bakedFilename; + + QFile bakedFile; + bakedFile.setFileName(_bakedMaterialData); + if (!bakedFile.open(QIODevice::WriteOnly)) { + handleError("Error opening " + _bakedMaterialData + " for writing"); + return; + } + + bakedFile.write(outputMaterial); + + // Export successful + _outputFiles.push_back(_bakedMaterialData); + qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData; + } else { + _bakedMaterialData = QString(outputMaterial); + qCDebug(material_baking) << "Converted" << _materialData << "to" << _bakedMaterialData; + } + } + + // emit signal to indicate the material baking is finished + emit finished(); +} diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h new file mode 100644 index 0000000000..41ce31380e --- /dev/null +++ b/libraries/baking/src/MaterialBaker.h @@ -0,0 +1,67 @@ +// +// MaterialBaker.h +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MaterialBaker_h +#define hifi_MaterialBaker_h + +#include "Baker.h" + +#include "TextureBaker.h" +#include "baking/TextureFileNamer.h" + +#include + +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); + + QString getMaterialData() const { return _materialData; } + bool isURL() const { return _isURL; } + QString getBakedMaterialData() const { return _bakedMaterialData; } + + static void setNextOvenWorkerThreadOperator(std::function getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; } + +public slots: + virtual void bake() override; + +signals: + void originalMaterialLoaded(); + +private slots: + void processMaterial(); + void outputMaterial(); + void handleFinishedTextureBaker(); + +private: + void loadMaterial(); + + QString _materialData; + bool _isURL; + + NetworkMaterialResourcePointer _materialResource; + + QHash, QSharedPointer> _textureBakers; + QMultiHash, std::shared_ptr> _materialsNeedingRewrite; + + QString _bakedOutputDir; + QString _textureOutputDir; + QString _bakedMaterialData; + QUrl _destinationPath; + + QScriptEngine _scriptEngine; + static std::function _getNextOvenWorkerThreadOperator; + TextureFileNamer _textureFileNamer; +}; + +#endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/MaterialBakingLoggingCategory.cpp b/libraries/baking/src/MaterialBakingLoggingCategory.cpp new file mode 100644 index 0000000000..75c0e6319c --- /dev/null +++ b/libraries/baking/src/MaterialBakingLoggingCategory.cpp @@ -0,0 +1,14 @@ +// +// MaterialBakingLoggingCategory.cpp +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "MaterialBakingLoggingCategory.h" + +Q_LOGGING_CATEGORY(material_baking, "hifi.material-baking"); diff --git a/libraries/baking/src/MaterialBakingLoggingCategory.h b/libraries/baking/src/MaterialBakingLoggingCategory.h new file mode 100644 index 0000000000..768bd9d769 --- /dev/null +++ b/libraries/baking/src/MaterialBakingLoggingCategory.h @@ -0,0 +1,19 @@ +// +// MaterialBakingLoggingCategory.h +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MaterialBakingLoggingCategory_h +#define hifi_MaterialBakingLoggingCategory_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(material_baking) + +#endif // hifi_MaterialBakingLoggingCategory_h diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 34f302b501..9568a81578 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -12,8 +12,17 @@ #include "ModelBaker.h" #include +#include + +#include +#include +#include + +#include +#include #include +#include #ifdef _WIN32 #pragma warning( push ) @@ -31,37 +40,275 @@ #pragma warning( pop ) #endif +#include "baking/BakerLibrary.h" + ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory) : + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : _modelURL(inputModelURL), _bakedOutputDir(bakedOutputDirectory), _originalOutputDir(originalOutputDirectory), - _textureThreadGetter(inputTextureThreadGetter) + _textureThreadGetter(inputTextureThreadGetter), + _hasBeenBaked(hasBeenBaked) { - auto tempDir = PathUtils::generateTemporaryDir(); + auto bakedFilename = _modelURL.fileName(); + if (!hasBeenBaked) { + bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.')); + bakedFilename += BAKED_FBX_EXTENSION; + } + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; +} - if (tempDir.isEmpty()) { - handleError("Failed to create a temporary directory."); +void ModelBaker::setOutputURLSuffix(const QUrl& outputURLSuffix) { + _outputURLSuffix = outputURLSuffix; +} + +void ModelBaker::setMappingURL(const QUrl& mappingURL) { + _mappingURL = mappingURL; +} + +void ModelBaker::setMapping(const hifi::VariantHash& mapping) { + _mapping = mapping; +} + +QUrl ModelBaker::getFullOutputMappingURL() const { + QUrl appendedURL = _outputMappingURL; + appendedURL.setFragment(_outputURLSuffix.fragment()); + appendedURL.setQuery(_outputURLSuffix.query()); + appendedURL.setUserInfo(_outputURLSuffix.userInfo()); + return appendedURL; +} + +void ModelBaker::bake() { + qDebug() << "ModelBaker" << _modelURL << "bake starting"; + + // Setup the output folders for the results of this bake + initializeOutputDirs(); + + if (shouldStop()) { return; } - _modelTempDir = tempDir; - - _originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName()); - qDebug() << "Made temporary dir " << _modelTempDir; - qDebug() << "Origin file path: " << _originalModelFilePath; + connect(this, &ModelBaker::modelLoaded, this, &ModelBaker::bakeSourceCopy); + // make a local copy of the model + saveSourceModel(); } -ModelBaker::~ModelBaker() { - if (_modelTempDir.exists()) { - if (!_modelTempDir.remove(_originalModelFilePath)) { - qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath; +void ModelBaker::initializeOutputDirs() { + // Attempt to make the output folders + // Warn if there is an output directory using the same name, unless we know a parent FST baker created them already + + if (QDir(_bakedOutputDir).exists()) { + if (_mappingURL.isEmpty()) { + qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; } - if (!_modelTempDir.rmdir(".")) { - qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir; + } else { + qCDebug(model_baking) << "Creating baked output folder" << _bakedOutputDir; + if (!QDir().mkpath(_bakedOutputDir)) { + handleError("Failed to create baked output folder " + _bakedOutputDir); + return; } } + + QDir originalOutputDir { _originalOutputDir }; + if (originalOutputDir.exists()) { + if (_mappingURL.isEmpty()) { + qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; + } + } else { + qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir; + if (!QDir().mkpath(_originalOutputDir)) { + handleError("Failed to create original output folder " + _originalOutputDir); + return; + } + } + + if (originalOutputDir.isReadable()) { + // The output directory is available. Use that to write/read the original model file + _originalOutputModelPath = originalOutputDir.filePath(_modelURL.fileName()); + } else { + handleError("Unable to write to original output folder " + _originalOutputDir); + } +} + +void ModelBaker::saveSourceModel() { + // check if the FBX is local or first needs to be downloaded + if (_modelURL.isLocalFile()) { + // load up the local file + QFile localModelURL { _modelURL.toLocalFile() }; + + qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalOutputModelPath; + + if (!localModelURL.exists()) { + //QMessageBox::warning(this, "Could not find " + _modelURL.toString(), ""); + handleError("Could not find " + _modelURL.toString()); + return; + } + + localModelURL.copy(_originalOutputModelPath); + + // emit our signal to start the import of the model source copy + emit modelLoaded(); + } else { + // remote file, kick off a download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + + networkRequest.setUrl(_modelURL); + + qCDebug(model_baking) << "Downloading" << _modelURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &ModelBaker::handleModelNetworkReply); + } +} + +void ModelBaker::handleModelNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _modelURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(_originalOutputModelPath); + + qDebug(model_baking) << "Writing copy of original model file to" << _originalOutputModelPath << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly)) { + // add an error to the error list for this model stating that a duplicate of the original model could not be made + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalOutputModelPath + ")"); + return; + } + if (copyOfOriginal.write(requestReply->readAll()) == -1) { + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + // emit our signal to start the import of the model source copy + emit modelLoaded(); + } else { + // add an error to our list stating that the model could not be downloaded + handleError("Failed to download " + _modelURL.toString()); + } +} + +void ModelBaker::bakeSourceCopy() { + QFile modelFile(_originalOutputModelPath); + if (!modelFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalOutputModelPath + " for reading"); + return; + } + hifi::ByteArray modelData = modelFile.readAll(); + + hfm::Model::Pointer bakedModel; + std::vector dracoMeshes; + std::vector> dracoMaterialLists; // Material order for per-mesh material lookup used by dracoMeshes + + { + auto serializer = DependencyManager::get()->getSerializerForMediaType(modelData, _modelURL, ""); + if (!serializer) { + handleError("Could not recognize file type of model file " + _originalOutputModelPath); + return; + } + hifi::VariantHash serializerMapping = _mapping; + serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library + serializerMapping["deduplicateIndices"] = true; // Draco compression also deduplicates, but we might as well shave it off to save on some earlier processing (currently FBXSerializer only) + hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); + + // Temporarily support copying the pre-parsed node from FBXSerializer, for better performance in FBXBaker + // TODO: Pure HFM baking + std::shared_ptr fbxSerializer = std::dynamic_pointer_cast(serializer); + if (fbxSerializer) { + qCDebug(model_baking) << "Parsing" << _modelURL; + _rootNode = fbxSerializer->_rootNode; + } + + baker::Baker baker(loadedModel, serializerMapping, _mappingURL); + auto config = baker.getConfiguration(); + // Enable compressed draco mesh generation + config->getJobConfig("BuildDracoMesh")->setEnabled(true); + // Do not permit potentially lossy modification of joint data meant for runtime + ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; + // The resources parsed from this job will not be used for now + // TODO: Proper full baking of all materials for a model + config->getJobConfig("ParseMaterialMapping")->setEnabled(false); + + // Begin hfm baking + baker.run(); + + bakedModel = 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& 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); + + if (shouldStop()) { + return; + } + + // Output FST file, copying over input mappings if available + QString outputFSTFilename = !_mappingURL.isEmpty() ? _mappingURL.fileName() : _modelURL.fileName(); + auto extensionStart = outputFSTFilename.indexOf("."); + if (extensionStart != -1) { + outputFSTFilename.resize(extensionStart); + } + outputFSTFilename += ".baked.fst"; + QString outputFSTURL = _bakedOutputDir + "/" + outputFSTFilename; + + 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] = "."; + hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); + + QFile fstOutputFile { outputFSTURL }; + if (!fstOutputFile.open(QIODevice::WriteOnly)) { + handleError("Failed to open file '" + outputFSTURL + "' for writing"); + return; + } + if (fstOutputFile.write(fstOut) == -1) { + handleError("Failed to write to file '" + outputFSTURL + "'"); + return; + } + _outputFiles.push_back(outputFSTURL); + _outputMappingURL = outputFSTURL; + + // check if we're already done with textures (in case we had none to re-write) + checkIfTexturesFinished(); } void ModelBaker::abort() { @@ -74,176 +321,36 @@ void ModelBaker::abort() { } } -bool ModelBaker::compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { - if (mesh.wasCompressed) { - handleError("Cannot re-bake a file that contains compressed mesh"); +bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { + if (dracoMeshBytes.isEmpty()) { + handleError("Failed to finalize the baking of a draco Geometry node"); return false; } - Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size()); - Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); - Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); - - int64_t numTriangles{ 0 }; - for (auto& part : mesh.parts) { - if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) { - handleWarning("Found a mesh part with invalid index data, skipping"); - continue; - } - numTriangles += part.quadTrianglesIndices.size() / 3; - numTriangles += part.triangleIndices.size() / 3; - } - - if (numTriangles == 0) { - return false; - } - - draco::TriangleSoupMeshBuilder meshBuilder; - - meshBuilder.Start(numTriangles); - - bool hasNormals{ mesh.normals.size() > 0 }; - bool hasColors{ mesh.colors.size() > 0 }; - bool hasTexCoords{ mesh.texCoords.size() > 0 }; - bool hasTexCoords1{ mesh.texCoords1.size() > 0 }; - bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true; - bool needsOriginalIndices{ hasDeformers }; - - int normalsAttributeID { -1 }; - int colorsAttributeID { -1 }; - int texCoordsAttributeID { -1 }; - int texCoords1AttributeID { -1 }; - int faceMaterialAttributeID { -1 }; - int originalIndexAttributeID { -1 }; - - const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, - 3, draco::DT_FLOAT32); - if (needsOriginalIndices) { - originalIndexAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, - 1, draco::DT_INT32); - } - - if (hasNormals) { - normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, - 3, draco::DT_FLOAT32); - } - if (hasColors) { - colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, - 3, draco::DT_FLOAT32); - } - if (hasTexCoords) { - texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, - 2, draco::DT_FLOAT32); - } - if (hasTexCoords1) { - texCoords1AttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, - 2, draco::DT_FLOAT32); - } - if (hasPerFaceMaterials) { - faceMaterialAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, - 1, draco::DT_UINT16); - } - - auto partIndex = 0; - draco::FaceIndex face; - uint16_t materialID; - - for (auto& part : mesh.parts) { - materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex; - - auto addFace = [&](QVector& indices, int index, draco::FaceIndex face) { - int32_t idx0 = indices[index]; - int32_t idx1 = indices[index + 1]; - int32_t idx2 = indices[index + 2]; - - if (hasPerFaceMaterials) { - meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); - } - - meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, - &mesh.vertices[idx0], &mesh.vertices[idx1], - &mesh.vertices[idx2]); - - if (needsOriginalIndices) { - meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, - &mesh.originalIndices[idx0], - &mesh.originalIndices[idx1], - &mesh.originalIndices[idx2]); - } - if (hasNormals) { - meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, - &mesh.normals[idx0], &mesh.normals[idx1], - &mesh.normals[idx2]); - } - if (hasColors) { - meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, - &mesh.colors[idx0], &mesh.colors[idx1], - &mesh.colors[idx2]); - } - if (hasTexCoords) { - meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, - &mesh.texCoords[idx0], &mesh.texCoords[idx1], - &mesh.texCoords[idx2]); - } - if (hasTexCoords1) { - meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, - &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], - &mesh.texCoords1[idx2]); - } - }; - - for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { - addFace(part.quadTrianglesIndices, i, face++); - } - - for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { - addFace(part.triangleIndices, i, face++); - } - - partIndex++; - } - - auto dracoMesh = meshBuilder.Finalize(); - - if (!dracoMesh) { - handleWarning("Failed to finalize the baking of a draco Geometry node"); - return false; - } - - // we need to modify unique attribute IDs for custom attributes - // so the attributes are easily retrievable on the other side - if (hasPerFaceMaterials) { - dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); - } - - if (hasTexCoords1) { - dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); - } - - if (needsOriginalIndices) { - dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); - } - - draco::Encoder encoder; - - encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); - encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); - encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); - encoder.SetSpeedOptions(0, 5); - - draco::EncoderBuffer buffer; - encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); - FBXNode dracoNode; dracoNode.name = "DracoMesh"; - auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size())); - dracoNode.properties.append(value); + dracoNode.properties.append(QVariant::fromValue(dracoMeshBytes)); + // Additional draco mesh node information + { + FBXNode fbxVersionNode; + fbxVersionNode.name = "FBXDracoMeshVersion"; + fbxVersionNode.properties.append(FBX_DRACO_MESH_VERSION); + dracoNode.children.append(fbxVersionNode); + + FBXNode dracoVersionNode; + dracoVersionNode.name = "DracoMeshVersion"; + dracoVersionNode.properties.append(DRACO_MESH_VERSION); + dracoNode.children.append(dracoVersionNode); + + FBXNode materialListNode; + materialListNode.name = "MaterialList"; + for (const hifi::ByteArray& materialID : dracoMaterialList) { + materialListNode.properties.append(materialID); + } + dracoNode.children.append(materialListNode); + } dracoMeshNode = dracoNode; - // Mesh compression successful return true return true; } @@ -274,45 +381,42 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture if (!modelTextureFileInfo.filePath().isEmpty()) { textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit()); } - auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull()); + auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull()); - QString baseTextureFileName; - if (_remappedTexturePaths.contains(urlToTexture)) { - baseTextureFileName = _remappedTexturePaths[urlToTexture]; - } else { + 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 - baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo); - _remappedTexturePaths[urlToTexture] = baseTextureFileName; - } + QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); - qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName - << "to" << baseTextureFileName; + QString bakedTextureFilePath { + _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX + }; - QString bakedTextureFilePath { - _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX - }; + textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - - if (!_bakingTextures.contains(urlToTexture)) { _outputFiles.push_back(bakedTextureFilePath); // bake this texture asynchronously - bakeTexture(urlToTexture, textureType, _bakedOutputDir, baseTextureFileName, textureContent); + 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 QUrl& textureURL, image::TextureUsage::Type textureType, - const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { - +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 bakingTexture{ - new TextureBaker(textureURL, textureType, outputDir, "../", bakedFilename, textureContent), + new TextureBaker(textureKey.first, textureKey.second, outputDir, "../", bakedFilename, textureContent), &TextureBaker::deleteLater }; @@ -321,7 +425,7 @@ void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type t connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture); // keep a shared pointer to the baking texture - _bakingTextures.insert(textureURL, bakingTexture); + _bakingTextures.insert(textureKey, bakingTexture); // start baking the texture on one of our available worker threads bakingTexture->moveToThread(_textureThreadGetter()); @@ -373,7 +477,7 @@ void ModelBaker::handleBakedTexture() { // now that this texture has been baked and handled, we can remove that TextureBaker from our hash - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); checkIfTexturesFinished(); } else { @@ -384,7 +488,7 @@ void ModelBaker::handleBakedTexture() { _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()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); // abort any other ongoing texture bakes since we know we'll end up failing for (auto& bakingTexture : _bakingTextures) { @@ -397,7 +501,7 @@ void ModelBaker::handleBakedTexture() { // 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()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); checkIfTexturesFinished(); } @@ -411,7 +515,7 @@ void ModelBaker::handleAbortedTexture() { qDebug() << "Texture aborted: " << bakedTexture->getTextureURL(); if (bakedTexture) { - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); } // since a texture we were baking aborted, our status is also aborted @@ -425,14 +529,11 @@ void ModelBaker::handleAbortedTexture() { checkIfTexturesFinished(); } -QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) { +QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded) { QUrl urlToTexture; - // use QFileInfo to easily split up the existing texture filename into its components - auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); - if (isEmbedded) { - urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath(); + urlToTexture = _modelURL.toString() + "/" + textureFileInfo.filePath(); } else { if (textureFileInfo.exists() && textureFileInfo.isFile()) { // set the texture URL to the local texture that we have confirmed exists @@ -442,14 +543,14 @@ QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativ // this is a relative file path which will require different handling // depending on the location of the original model - if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + 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(apparentRelativePath.absoluteFilePath()); + 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(apparentRelativePath.fileName()); + urlToTexture = _modelURL.resolved(textureFileInfo.fileName()); } } } @@ -494,25 +595,6 @@ void ModelBaker::checkIfTexturesFinished() { } } -QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo) { - // first make sure we have a unique base name for this texture - // in case another texture referenced by this model has the same base name - auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; - - QString baseTextureFileName{ textureFileInfo.completeBaseName() }; - - if (nameMatches > 0) { - // there are already nameMatches texture with this name - // append - and that number to our baked texture file name so that it is unique - baseTextureFileName += "-" + QString::number(nameMatches); - } - - // increment the number of name matches - ++nameMatches; - - return baseTextureFileName; -} - void ModelBaker::setWasAborted(bool wasAborted) { if (wasAborted != _wasAborted.load()) { Baker::setWasAborted(wasAborted); @@ -588,31 +670,25 @@ void ModelBaker::embedTextureMetaData() { } void ModelBaker::exportScene() { - // save the relative path to this FBX inside our passed output folder - auto fileName = _modelURL.fileName(); - auto baseName = fileName.left(fileName.lastIndexOf('.')); - auto bakedFilename = baseName + BAKED_FBX_EXTENSION; - - _bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename; - auto fbxData = FBXWriter::encodeFBX(_rootNode); - QFile bakedFile(_bakedModelFilePath); + QString bakedModelURL = _bakedModelURL.toString(); + QFile bakedFile(bakedModelURL); if (!bakedFile.open(QIODevice::WriteOnly)) { - handleError("Error opening " + _bakedModelFilePath + " for writing"); + handleError("Error opening " + bakedModelURL + " for writing"); return; } bakedFile.write(fbxData); - _outputFiles.push_back(_bakedModelFilePath); + _outputFiles.push_back(bakedModelURL); #ifdef HIFI_DUMP_FBX { FBXToJSON fbxToJSON; fbxToJSON << _rootNode; - QFileInfo modelFile(_bakedModelFilePath); + QFileInfo modelFile(_bakedModelURL.toString()); QString outFilename(modelFile.dir().absolutePath() + "/" + modelFile.completeBaseName() + "_FBX.json"); QFile jsonFile(outFilename); if (jsonFile.open(QIODevice::WriteOnly)) { @@ -622,5 +698,5 @@ void ModelBaker::exportScene() { } #endif - qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath; + qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << bakedModelURL; } diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 14a182f622..d9a559392f 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -19,6 +19,7 @@ #include "Baker.h" #include "TextureBaker.h" +#include "baking/TextureFileNamer.h" #include "ModelBakingLoggingCategory.h" @@ -30,57 +31,84 @@ using TextureBakerThreadGetter = std::function; using GetMaterialIDCallback = std::function ; -static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; +static const QString FST_EXTENSION { ".fst" }; +static const QString BAKED_FST_EXTENSION { ".baked.fst" }; +static const QString FBX_EXTENSION { ".fbx" }; +static const QString BAKED_FBX_EXTENSION { ".baked.fbx" }; +static const QString OBJ_EXTENSION { ".obj" }; +static const QString GLTF_EXTENSION { ".gltf" }; class ModelBaker : public Baker { Q_OBJECT public: - ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); - virtual ~ModelBaker(); + using TextureKey = QPair; - bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); + ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + + void setOutputURLSuffix(const QUrl& urlSuffix); + void setMappingURL(const QUrl& mappingURL); + void setMapping(const hifi::VariantHash& mapping); + + void initializeOutputDirs(); + + bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; QUrl getModelURL() const { return _modelURL; } - QString getBakedModelFilePath() const { return _bakedModelFilePath; } + virtual QUrl getFullOutputMappingURL() const; + QUrl getBakedModelURL() const { return _bakedModelURL; } + +signals: + void modelLoaded(); public slots: + virtual void bake() override; virtual void abort() override; protected: + void saveSourceModel(); + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) = 0; void checkIfTexturesFinished(); void texturesFinished(); void embedTextureMetaData(); void exportScene(); - + FBXNode _rootNode; QHash _textureContentMap; QUrl _modelURL; + QUrl _outputURLSuffix; + QUrl _mappingURL; + hifi::VariantHash _mapping; QString _bakedOutputDir; QString _originalOutputDir; - QString _bakedModelFilePath; - QDir _modelTempDir; - QString _originalModelFilePath; + TextureBakerThreadGetter _textureThreadGetter; + QString _originalOutputModelPath; + QString _outputMappingURL; + QUrl _bakedModelURL; + +protected slots: + void handleModelNetworkReply(); + virtual void bakeSourceCopy(); private slots: void handleBakedTexture(); void handleAbortedTexture(); private: - QString createBaseTextureFileName(const QFileInfo & textureFileInfo); - QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); - void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, - const QString & bakedFilename, const QByteArray & textureContent); + 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); - - TextureBakerThreadGetter _textureThreadGetter; - QMultiHash> _bakingTextures; + + QMultiHash> _bakingTextures; QHash _textureNameMatchCount; - QHash _remappedTexturePaths; - bool _pendingErrorEmission{ false }; + bool _pendingErrorEmission { false }; + + bool _hasBeenBaked { false }; + + TextureFileNamer _textureFileNamer; }; #endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 5a1239f88f..70bdeb2071 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -35,157 +35,51 @@ const QByteArray CONNECTIONS_NODE_PROPERTY = "OO"; const QByteArray CONNECTIONS_NODE_PROPERTY_1 = "OP"; const QByteArray MESH = "Mesh"; -void OBJBaker::bake() { - qDebug() << "OBJBaker" << _modelURL << "bake starting"; - - // trigger bakeOBJ once OBJ is loaded - connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ); - - // make a local copy of the OBJ - loadOBJ(); -} - -void OBJBaker::loadOBJ() { - if (!QDir().mkpath(_bakedOutputDir)) { - handleError("Failed to create baked OBJ output folder " + _bakedOutputDir); - return; - } - - if (!QDir().mkpath(_originalOutputDir)) { - handleError("Failed to create original OBJ output folder " + _originalOutputDir); - return; - } - - // check if the OBJ is local or it needs to be downloaded - if (_modelURL.isLocalFile()) { - // loading the local OBJ - QFile localOBJ { _modelURL.toLocalFile() }; - - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; - - if (!localOBJ.exists()) { - handleError("Could not find " + _modelURL.toString()); - return; - } - - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localOBJ.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localOBJ.copy(_originalModelFilePath); - - // local OBJ is loaded emit signal to trigger its baking - emit OBJLoaded(); - } else { - // OBJ is remote, start download - auto& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkRequest networkRequest; - - // setup the request to follow re-directs and always hit the network - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - networkRequest.setUrl(_modelURL); - - qCDebug(model_baking) << "Downloading" << _modelURL; - auto networkReply = networkAccessManager.get(networkRequest); - - connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply); - } -} - -void OBJBaker::handleOBJNetworkReply() { - auto requestReply = qobject_cast(sender()); - - if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded" << _modelURL; - - // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); - - qDebug(model_baking) << "Writing copy of original obj to" << _originalModelFilePath << copyOfOriginal.fileName(); - - if (!copyOfOriginal.open(QIODevice::WriteOnly)) { - // add an error to the error list for this obj stating that a duplicate of the original obj could not be made - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); - return; - } - if (copyOfOriginal.write(requestReply->readAll()) == -1) { - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); - return; - } - - // close that file now that we are done writing to it - copyOfOriginal.close(); - - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - // remote OBJ is loaded emit signal to trigger its baking - emit OBJLoaded(); - } else { - // add an error to our list stating that the OBJ could not be downloaded - handleError("Failed to download " + _modelURL.toString()); - } -} - -void OBJBaker::bakeOBJ() { - // Read the OBJ file - QFile objFile(_originalModelFilePath); - if (!objFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); - return; - } - - QByteArray objData = objFile.readAll(); - - OBJSerializer serializer; - QVariantHash mapping; - mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library - auto geometry = serializer.read(objData, mapping, _modelURL); - +void OBJBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { // Write OBJ Data as FBX tree nodes - createFBXNodeTree(_rootNode, *geometry); - - checkIfTexturesFinished(); + createFBXNodeTree(_rootNode, hfmModel, dracoMeshes[0]); } -void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh) { + // Make all generated nodes children of rootNode + rootNode.children = { FBXNode(), FBXNode(), FBXNode() }; + FBXNode& globalSettingsNode = rootNode.children[0]; + FBXNode& objectNode = rootNode.children[1]; + FBXNode& connectionsNode = rootNode.children[2]; + // Generating FBX Header Node FBXNode headerNode; headerNode.name = FBX_HEADER_EXTENSION; // Generating global settings node // Required for Unit Scale Factor - FBXNode globalSettingsNode; globalSettingsNode.name = GLOBAL_SETTINGS_NODE_NAME; // Setting the tree hierarchy: GlobalSettings -> Properties70 -> P -> Properties - FBXNode properties70Node; - properties70Node.name = PROPERTIES70_NODE_NAME; - - FBXNode pNode; { - pNode.name = P_NODE_NAME; - pNode.properties.append({ - "UnitScaleFactor", "double", "Number", "", - UNIT_SCALE_FACTOR - }); + globalSettingsNode.children.push_back(FBXNode()); + FBXNode& properties70Node = globalSettingsNode.children.back(); + properties70Node.name = PROPERTIES70_NODE_NAME; + + FBXNode pNode; + { + pNode.name = P_NODE_NAME; + pNode.properties.append({ + "UnitScaleFactor", "double", "Number", "", + UNIT_SCALE_FACTOR + }); + } + properties70Node.children = { pNode }; + } - properties70Node.children = { pNode }; - globalSettingsNode.children = { properties70Node }; - // Generating Object node - FBXNode objectNode; objectNode.name = OBJECTS_NODE_NAME; + objectNode.children = { FBXNode(), FBXNode() }; + FBXNode& geometryNode = objectNode.children[0]; + FBXNode& modelNode = objectNode.children[1]; - // Generating Object node's child - Geometry node - FBXNode geometryNode; + // Generating Object node's child - Geometry node geometryNode.name = GEOMETRY_NODE_NAME; NodeID geometryID; { @@ -196,15 +90,8 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { MESH }; } - - // Compress the mesh information and store in dracoNode - bool hasDeformers = false; // No concept of deformers for an OBJ - FBXNode dracoNode; - compressMesh(hfmModel.meshes[0], hasDeformers, dracoNode); - geometryNode.children.append(dracoNode); - + // Generating Object node's child - Model node - FBXNode modelNode; modelNode.name = MODEL_NODE_NAME; NodeID modelID; { @@ -212,16 +99,14 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { modelNode.properties = { modelID, MODEL_NODE_NAME, MESH }; } - objectNode.children = { geometryNode, modelNode }; - // Generating Objects node's child - Material node - auto& meshParts = hfmModel.meshes[0].parts; + auto& meshParts = hfmModel->meshes[0].parts; for (auto& meshPart : meshParts) { FBXNode materialNode; materialNode.name = MATERIAL_NODE_NAME; - if (hfmModel.materials.size() == 1) { + if (hfmModel->materials.size() == 1) { // case when no material information is provided, OBJSerializer considers it as a single default material - for (auto& materialID : hfmModel.materials.keys()) { + for (auto& materialID : hfmModel->materials.keys()) { setMaterialNodeProperties(materialNode, materialID, hfmModel); } } else { @@ -231,12 +116,28 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { objectNode.children.append(materialNode); } + // Store the draco node containing the compressed mesh information, along with the per-meshPart material IDs the draco node references + // Because we redefine the material IDs when initializing the material nodes above, we pass that in for the material list + // The nth mesh part gets the nth material + if (!dracoMesh.isEmpty()) { + std::vector newMaterialList; + newMaterialList.reserve(_materialIDs.size()); + for (auto materialID : _materialIDs) { + newMaterialList.push_back(hifi::ByteArray(std::to_string((int)materialID).c_str())); + } + FBXNode dracoNode; + buildDracoMeshNode(dracoNode, dracoMesh, newMaterialList); + geometryNode.children.append(dracoNode); + } else { + 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]; + HFMMaterial currentMaterial = hfmModel->materials[material]; if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { auto textureID = nextNodeID(); _mapTextureMaterial.emplace_back(textureID, i); @@ -281,14 +182,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { } // Generating Connections node - FBXNode connectionsNode; connectionsNode.name = CONNECTIONS_NODE_NAME; - // connect Geometry to Model - FBXNode cNode; - cNode.name = C_NODE_NAME; - cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID }; - connectionsNode.children = { cNode }; + // connect Geometry to Model + { + FBXNode cNode; + cNode.name = C_NODE_NAME; + cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID }; + connectionsNode.children.push_back(cNode); + } // connect all materials to model for (auto& materialID : _materialIDs) { @@ -320,18 +222,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { }; connectionsNode.children.append(cDiffuseNode); } - - // Make all generated nodes children of rootNode - rootNode.children = { globalSettingsNode, objectNode, connectionsNode }; } // Set properties for material nodes -void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel) { +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel) { auto materialID = nextNodeID(); _materialIDs.push_back(materialID); materialNode.properties = { materialID, material, MESH }; - HFMMaterial currentMaterial = hfmModel.materials[material]; + HFMMaterial currentMaterial = hfmModel->materials[material]; // Setting the hierarchy: Material -> Properties70 -> P -> Properties FBXNode properties70Node; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 5aaae49d4a..d1eced5452 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -27,20 +27,12 @@ class OBJBaker : public ModelBaker { public: using ModelBaker::ModelBaker; -public slots: - virtual void bake() override; - -signals: - void OBJLoaded(); - -private slots: - void bakeOBJ(); - void handleOBJNetworkReply(); +protected: + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void loadOBJ(); - void createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel); + void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 6407ce1846..d097b4765b 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -47,6 +47,14 @@ TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type tex auto originalFilename = textureURL.fileName(); _baseFilename = originalFilename.left(originalFilename.lastIndexOf('.')); } + + auto textureFilename = _textureURL.fileName(); + QString originalExtension; + int extensionStart = textureFilename.indexOf("."); + if (extensionStart != -1) { + originalExtension = textureFilename.mid(extensionStart); + } + _originalCopyFilePath = _outputDirectory.absoluteFilePath(_baseFilename + originalExtension); } void TextureBaker::bake() { @@ -128,7 +136,9 @@ void TextureBaker::processTexture() { TextureMeta meta; - auto originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName()); + QString originalCopyFilePath = _originalCopyFilePath.toString(); + + // Copy the original file into the baked output directory if it doesn't exist yet { QFile file { originalCopyFilePath }; if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) { @@ -138,9 +148,10 @@ void TextureBaker::processTexture() { // IMPORTANT: _originalTexture is empty past this point _originalTexture.clear(); _outputFiles.push_back(originalCopyFilePath); - meta.original = _metaTexturePathPrefix + _textureURL.fileName(); + meta.original = _metaTexturePathPrefix + _originalCopyFilePath.fileName(); } + // Load the copy of the original file from the baked output directory. New images will be created using the original as the source data. auto buffer = std::static_pointer_cast(std::make_shared(originalCopyFilePath)); if (!buffer->open(QIODevice::ReadOnly)) { handleError("Could not open original file at " + originalCopyFilePath); diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index c8c4fb73b8..4fc9680653 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -22,6 +22,8 @@ #include "Baker.h" +#include + extern const QString BAKED_TEXTURE_KTX_EXT; extern const QString BAKED_META_TEXTURE_SUFFIX; @@ -37,12 +39,18 @@ public: QUrl getTextureURL() const { return _textureURL; } + QString getBaseFilename() const { return _baseFilename; } + QString getMetaTextureFileName() const { return _metaTextureFileName; } virtual void setWasAborted(bool wasAborted) override; static void setCompressionEnabled(bool enabled) { _compressionEnabled = enabled; } + void setMapChannel(graphics::Material::MapChannel mapChannel) { _mapChannel = mapChannel; } + graphics::Material::MapChannel getMapChannel() const { return _mapChannel; } + image::TextureUsage::Type getTextureType() const { return _textureType; } + public slots: virtual void bake() override; virtual void abort() override; @@ -60,11 +68,14 @@ private: QUrl _textureURL; QByteArray _originalTexture; image::TextureUsage::Type _textureType; + graphics::Material::MapChannel _mapChannel; + bool _mapChannelSet { false }; QString _baseFilename; QDir _outputDirectory; QString _metaTextureFileName; QString _metaTexturePathPrefix; + QUrl _originalCopyFilePath; std::atomic _abortProcessing { false }; diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp new file mode 100644 index 0000000000..f9445bd432 --- /dev/null +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -0,0 +1,82 @@ +// +// BakerLibrary.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/02/14. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "BakerLibrary.h" + +#include "FSTBaker.h" +#include "../FBXBaker.h" +#include "../OBJBaker.h" + +// Check if the file pointed to by this URL is a bakeable model, by comparing extensions +QUrl getBakeableModelURL(const QUrl& url) { + static const std::vector extensionsToBake = { + FST_EXTENSION, + BAKED_FST_EXTENSION, + FBX_EXTENSION, + BAKED_FBX_EXTENSION, + OBJ_EXTENSION, + GLTF_EXTENSION + }; + + QString filename = url.fileName(); + for (auto& extension : extensionsToBake) { + if (filename.endsWith(extension, Qt::CaseInsensitive)) { + return url; + } + } + + qWarning() << "Unknown model type: " << url.fileName(); + return QUrl(); +} + +bool isModelBaked(const QUrl& bakeableModelURL) { + auto modelString = bakeableModelURL.toString(); + auto beforeModelExtension = modelString; + beforeModelExtension.resize(modelString.lastIndexOf('.')); + return beforeModelExtension.endsWith(".baked"); +} + +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, 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 + auto baseName = filename.left(filename.lastIndexOf('.')).left(filename.lastIndexOf(".baked")); + auto subDirName = "/" + baseName; + int i = 1; + while (QDir(contentOutputPath + subDirName).exists()) { + subDirName = "/" + baseName + "-" + QString::number(i++); + } + + QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked"; + QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; + + return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); +} + +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) { + auto filename = bakeableModelURL.fileName(); + + std::unique_ptr baker; + + if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); + } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); + } else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + //} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { + //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + } else { + qDebug() << "Could not create ModelBaker for url" << bakeableModelURL; + } + + return baker; +} diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h new file mode 100644 index 0000000000..a646c8d36a --- /dev/null +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -0,0 +1,31 @@ +// +// ModelBaker.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/02/14. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BakerLibrary_h +#define hifi_BakerLibrary_h + +#include + +#include "../ModelBaker.h" + +// Returns either the given model URL if valid, or an empty URL +QUrl getBakeableModelURL(const QUrl& url); + +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 getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); + +// Similar to getModelBaker, but gives control over where the output folders will be +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory); + +#endif // hifi_BakerLibrary_h diff --git a/libraries/baking/src/baking/FSTBaker.cpp b/libraries/baking/src/baking/FSTBaker.cpp new file mode 100644 index 0000000000..acf3bfe1c7 --- /dev/null +++ b/libraries/baking/src/baking/FSTBaker.cpp @@ -0,0 +1,128 @@ +// +// FSTBaker.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/06. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "FSTBaker.h" + +#include +#include + +#include "BakerLibrary.h" + +#include + +FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputMappingURL, inputTextureThreadGetter, 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)); + QUrl newInputMappingURL = inputMappingURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath); + _modelURL = newInputMappingURL; + } + _mappingURL = _modelURL; + + { + // Unused, but defined for consistency + auto bakedFilename = _modelURL.fileName(); + bakedFilename.replace(FST_EXTENSION, BAKED_FST_EXTENSION); + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; + } +} + +QUrl FSTBaker::getFullOutputMappingURL() const { + if (_modelBaker) { + return _modelBaker->getFullOutputMappingURL(); + } + return QUrl(); +} + +void FSTBaker::bakeSourceCopy() { + if (shouldStop()) { + return; + } + + QFile fstFile(_originalOutputModelPath); + if (!fstFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalOutputModelPath + " for reading"); + return; + } + + hifi::ByteArray fstData = fstFile.readAll(); + _mapping = FSTReader::readMapping(fstData); + + auto filenameField = _mapping[FILENAME_FIELD].toString(); + if (filenameField.isEmpty()) { + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be found"); + return; + } + auto modelURL = _mappingURL.adjusted(QUrl::RemoveFilename).resolved(filenameField); + auto bakeableModelURL = getBakeableModelURL(modelURL); + if (bakeableModelURL.isEmpty()) { + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be resolved to a valid bakeable model url"); + return; + } + + auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir); + _modelBaker = std::unique_ptr(dynamic_cast(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"); + return; + } + if (dynamic_cast(_modelBaker.get())) { + // Could be interesting, but for now let's just prevent infinite FST loops in the most straightforward way possible + handleError("The FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported."); + return; + } + _modelBaker->setMappingURL(_mappingURL); + _modelBaker->setMapping(_mapping); + // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + _modelBaker->setOutputURLSuffix(modelURL); + + connect(_modelBaker.get(), &ModelBaker::aborted, this, &FSTBaker::handleModelBakerAborted); + connect(_modelBaker.get(), &ModelBaker::finished, this, &FSTBaker::handleModelBakerFinished); + + // FSTBaker can't do much while waiting for the ModelBaker to finish, so start the bake on this thread. + _modelBaker->bake(); +} + +void FSTBaker::handleModelBakerEnded() { + for (auto& warning : _modelBaker->getWarnings()) { + _warningList.push_back(warning); + } + for (auto& error : _modelBaker->getErrors()) { + _errorList.push_back(error); + } + + // Get the output files, including but not limited to the FST file and the baked model file + for (auto& outputFile : _modelBaker->getOutputFiles()) { + _outputFiles.push_back(outputFile); + } + +} + +void FSTBaker::handleModelBakerAborted() { + handleModelBakerEnded(); + if (!wasAborted()) { + setWasAborted(true); + } +} + +void FSTBaker::handleModelBakerFinished() { + handleModelBakerEnded(); + setIsFinished(true); +} + +void FSTBaker::abort() { + ModelBaker::abort(); + if (_modelBaker) { + _modelBaker->abort(); + } +} diff --git a/libraries/baking/src/baking/FSTBaker.h b/libraries/baking/src/baking/FSTBaker.h new file mode 100644 index 0000000000..85c7c93a37 --- /dev/null +++ b/libraries/baking/src/baking/FSTBaker.h @@ -0,0 +1,45 @@ +// +// FSTBaker.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/06. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_FSTBaker_h +#define hifi_FSTBaker_h + +#include "../ModelBaker.h" + +class FSTBaker : public ModelBaker { + Q_OBJECT + +public: + FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + + virtual QUrl getFullOutputMappingURL() const override; + +signals: + void fstLoaded(); + +public slots: + virtual void abort() override; + +protected: + std::unique_ptr _modelBaker; + +protected slots: + virtual void bakeSourceCopy() override; + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override {}; + void handleModelBakerAborted(); + void handleModelBakerFinished(); + +private: + void handleModelBakerEnded(); +}; + +#endif // hifi_FSTBaker_h diff --git a/libraries/baking/src/baking/TextureFileNamer.cpp b/libraries/baking/src/baking/TextureFileNamer.cpp new file mode 100644 index 0000000000..612d89e604 --- /dev/null +++ b/libraries/baking/src/baking/TextureFileNamer.cpp @@ -0,0 +1,34 @@ +// +// TextureFileNamer.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/14. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TextureFileNamer.h" + +QString TextureFileNamer::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) { + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); + + QString baseTextureFileName{ textureFileInfo.baseName() + addMapChannel }; + + // first make sure we have a unique base name for this texture + // in case another texture referenced by this model has the same base name + auto& nameMatches = _textureNameMatchCount[baseTextureFileName]; + + if (nameMatches > 0) { + // there are already nameMatches texture with this name + // append - and that number to our baked texture file name so that it is unique + baseTextureFileName += "-" + QString::number(nameMatches); + } + + // increment the number of name matches + ++nameMatches; + + return baseTextureFileName; +} diff --git a/libraries/baking/src/baking/TextureFileNamer.h b/libraries/baking/src/baking/TextureFileNamer.h new file mode 100644 index 0000000000..9049588ef1 --- /dev/null +++ b/libraries/baking/src/baking/TextureFileNamer.h @@ -0,0 +1,30 @@ +// +// TextureFileNamer.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/14. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TextureFileNamer_h +#define hifi_TextureFileNamer_h + +#include +#include + +#include + +class TextureFileNamer { +public: + TextureFileNamer() {} + + QString createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType); + +protected: + QHash _textureNameMatchCount; +}; + +#endif // hifi_TextureFileNamer_h diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 5a396231b6..57be2f788b 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -180,6 +180,7 @@ namespace controller { * third person, to full screen mirror, then back to first person and repeat. * ContextMenunumbernumberShow / hide the tablet. * ToggleMutenumbernumberToggle the microphone mute. + * TogglePushToTalknumbernumberToggle push to talk. * ToggleOverlaynumbernumberToggle the display of overlays. * SprintnumbernumberSet avatar sprint mode. * ReticleClicknumbernumberSet mouse-pressed. @@ -245,6 +246,8 @@ namespace controller { * ContextMenu instead. * TOGGLE_MUTEnumbernumberDeprecated: Use * ToggleMute instead. + * TOGGLE_PUSHTOTALKnumbernumberDeprecated: Use + * TogglePushToTalk instead. * SPRINTnumbernumberDeprecated: Use * Sprint instead. * LONGITUDINAL_BACKWARDnumbernumberDeprecated: Use @@ -411,6 +414,7 @@ namespace controller { makeButtonPair(Action::ACTION2, "SecondaryAction"), makeButtonPair(Action::CONTEXT_MENU, "ContextMenu"), makeButtonPair(Action::TOGGLE_MUTE, "ToggleMute"), + makeButtonPair(Action::TOGGLE_PUSHTOTALK, "TogglePushToTalk"), makeButtonPair(Action::CYCLE_CAMERA, "CycleCamera"), makeButtonPair(Action::TOGGLE_OVERLAY, "ToggleOverlay"), makeButtonPair(Action::SPRINT, "Sprint"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index a12a3d60a9..3e99d8d147 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -60,6 +60,7 @@ enum class Action { CONTEXT_MENU, TOGGLE_MUTE, + TOGGLE_PUSHTOTALK, CYCLE_CAMERA, TOGGLE_OVERLAY, diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 143c7fa377..6cfff7bc41 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -221,17 +221,15 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); if (scene) { - render::Transaction transaction; for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; const EntityItemPointer& entityItem = renderer->getEntity(); if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { - renderer->removeFromScene(scene, transaction); + fadeOutRenderable(renderer); } else { savedEntities[entry.first] = entry.second; } } - scene->enqueueTransaction(transaction); } _renderablesToUpdate = savedEntities; @@ -258,12 +256,10 @@ void EntityTreeRenderer::clear() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); if (scene) { - render::Transaction transaction; for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; - renderer->removeFromScene(scene, transaction); + fadeOutRenderable(renderer); } - scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; } @@ -1016,10 +1012,7 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities - // here's where we remove the entity payload from the scene - render::Transaction transaction; - renderable->removeFromScene(scene, transaction); - scene->enqueueTransaction(transaction); + fadeOutRenderable(renderable); } void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) { @@ -1057,13 +1050,26 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool } } +void EntityTreeRenderer::fadeOutRenderable(const EntityRendererPointer& renderable) { + render::Transaction transaction; + auto scene = _viewState->getMain3DScene(); + + transaction.transitionFinishedOperator(renderable->getRenderItemID(), [scene, renderable]() { + render::Transaction transaction; + renderable->removeFromScene(scene, transaction); + scene->enqueueTransaction(transaction); + }); + + scene->enqueueTransaction(transaction); +} + void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entity, const Collision& collision) { assert((bool)entity); auto renderable = renderableForEntity(entity); - if (!renderable) { - return; + if (!renderable) { + return; } - + SharedSoundPointer collisionSound = renderable->getCollisionSound(); if (!collisionSound) { return; @@ -1105,7 +1111,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entit options.volume = volume; options.pitch = 1.0f / stretchFactor; - AudioInjector::playSoundAndDelete(collisionSound, options); + DependencyManager::get()->playSound(collisionSound, options, true); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a257951ba8..cee91ad1c7 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -16,7 +16,7 @@ #include #include -#include +#include #include // for RayToEntityIntersectionResult #include #include @@ -93,6 +93,8 @@ public: /// reloads the entity scripts, calling unload and preload void reloadEntityScripts(); + void fadeOutRenderable(const EntityRendererPointer& renderable); + // event handles which may generate entity related events QUuid mousePressEvent(QMouseEvent* event); void mouseReleaseEvent(QMouseEvent* event); @@ -255,6 +257,7 @@ private: std::unordered_map _renderablesToUpdate; std::unordered_map _entitiesInScene; std::unordered_map _entitiesToAdd; + // For Scene.shouldRenderEntities QList _entityIDsLastInScene; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index a6826da91b..3a56521702 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -148,7 +148,7 @@ EntityRenderer::EntityRenderer(const EntityItemPointer& entity) : _created(entit }); } -EntityRenderer::~EntityRenderer() { } +EntityRenderer::~EntityRenderer() {} // // Smart payload proxy members, implementing the payload interface @@ -421,6 +421,7 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa if (fading) { _isFading = Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + _prevIsTransparent = transparent; updateModelTransformAndBound(); @@ -493,4 +494,4 @@ glm::vec4 EntityRenderer::calculatePulseColor(const glm::vec4& color, const Puls } return result; -} \ No newline at end of file +} diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index e9a6035e3d..39f9ad091e 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -40,6 +40,7 @@ public: virtual bool wantsKeyboardFocus() const { return false; } virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } + virtual void emitScriptEvent(const QVariant& message) {} const EntityItemPointer& getEntity() const { return _entity; } const ItemID& getRenderItemID() const { return _renderItemID; } diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 2eb877b0e1..b474fb2266 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -169,7 +169,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (urlChanged && !usingMaterialData) { _networkMaterial = MaterialCache::instance().getMaterial(_materialURL); - auto onMaterialRequestFinished = [&, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) { + auto onMaterialRequestFinished = [this, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) { if (success) { deleteMaterial(oldParentID, oldParentMaterialName); _texturesLoaded = false; @@ -186,7 +186,11 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (_networkMaterial->isLoaded()) { onMaterialRequestFinished(!_networkMaterial->isFailed()); } else { - connect(_networkMaterial.data(), &Resource::finished, this, onMaterialRequestFinished); + connect(_networkMaterial.data(), &Resource::finished, this, [this, onMaterialRequestFinished](bool success) { + withWriteLock([&] { + onMaterialRequestFinished(success); + }); + }); } } } else if (materialDataChanged && usingMaterialData) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 03c50008a0..f921f6eca6 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -181,9 +181,11 @@ void RenderableModelEntityItem::updateModelBounds() { updateRenderItems = true; } - if (model->getScaleToFitDimensions() != getScaledDimensions() || - model->getRegistrationPoint() != getRegistrationPoint() || - !model->getIsScaledToFit()) { + bool overridingModelTransform = model->isOverridingModelTransformAndOffset(); + if (!overridingModelTransform && + (model->getScaleToFitDimensions() != getScaledDimensions() || + model->getRegistrationPoint() != getRegistrationPoint() || + !model->getIsScaledToFit())) { // The machinery for updateModelBounds will give existing models the opportunity to fix their // translation/rotation/scale/registration. The first two are straightforward, but the latter two // have guards to make sure they don't happen after they've already been set. Here we reset those guards. @@ -305,10 +307,6 @@ void RenderableModelEntityItem::setShapeType(ShapeType type) { } void RenderableModelEntityItem::setCompoundShapeURL(const QString& url) { - // because the caching system only allows one Geometry per url, and because this url might also be used - // as a visual model, we need to change this url in some way. We add a "collision-hull" query-arg so it - // will end up in a different hash-key in ResourceCache. TODO: It would be better to use the same URL and - // parse it twice. auto currentCompoundShapeURL = getCompoundShapeURL(); ModelEntityItem::setCompoundShapeURL(url); if (getCompoundShapeURL() != currentCompoundShapeURL || !getModel()) { @@ -1032,7 +1030,7 @@ void RenderableModelEntityItem::copyAnimationJointDataToModel() { }); if (changed) { - locationChanged(false, true); + locationChanged(true, true); } } diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index c139fbf320..d517ecd026 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -1,4 +1,4 @@ -// +// // RenderableParticleEffectEntityItem.cpp // interface/src // @@ -9,12 +9,12 @@ // #include "RenderableParticleEffectEntityItem.h" - #include #include #include +#include using namespace render; using namespace render::entities; @@ -79,6 +79,14 @@ bool ParticleEffectEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedE return true; } + if (_shapeType != entity->getShapeType()) { + return true; + } + + if (_compoundShapeURL != entity->getCompoundShapeURL()) { + return true; + } + return false; } @@ -87,10 +95,10 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi if (!newParticleProperties.valid()) { qCWarning(entitiesrenderer) << "Bad particle properties"; } - - if (resultWithReadLock([&]{ return _particleProperties != newParticleProperties; })) { + + if (resultWithReadLock([&] { return _particleProperties != newParticleProperties; })) { _timeUntilNextEmit = 0; - withWriteLock([&]{ + withWriteLock([&] { _particleProperties = newParticleProperties; if (!_prevEmitterShouldTrailInitialized) { _prevEmitterShouldTrailInitialized = true; @@ -101,13 +109,20 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi withWriteLock([&] { _pulseProperties = entity->getPulseProperties(); + _shapeType = entity->getShapeType(); + QString compoundShapeURL = entity->getCompoundShapeURL(); + if (_compoundShapeURL != compoundShapeURL) { + _compoundShapeURL = compoundShapeURL; + _hasComputedTriangles = false; + fetchGeometryResource(); + } }); _emitting = entity->getIsEmitting(); - bool textureEmpty = resultWithReadLock([&]{ return _particleProperties.textures.isEmpty(); }); + bool textureEmpty = resultWithReadLock([&] { return _particleProperties.textures.isEmpty(); }); if (textureEmpty) { if (_networkTexture) { - withWriteLock([&] { + withWriteLock([&] { _networkTexture.reset(); }); } @@ -116,11 +131,11 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi entity->setVisuallyReady(true); }); } else { - bool textureNeedsUpdate = resultWithReadLock([&]{ + bool textureNeedsUpdate = resultWithReadLock([&] { return !_networkTexture || _networkTexture->getURL() != QUrl(_particleProperties.textures); }); if (textureNeedsUpdate) { - withWriteLock([&] { + withWriteLock([&] { _networkTexture = DependencyManager::get()->getTexture(_particleProperties.textures); }); } @@ -144,7 +159,7 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { // Fill in Uniforms structure ParticleUniforms particleUniforms; - withReadLock([&]{ + withReadLock([&] { particleUniforms.radius.start = _particleProperties.radius.range.start; particleUniforms.radius.middle = _particleProperties.radius.gradient.target; particleUniforms.radius.finish = _particleProperties.radius.range.finish; @@ -181,9 +196,32 @@ Item::Bound ParticleEffectEntityRenderer::getBound() { return _bound; } -static const size_t VERTEX_PER_PARTICLE = 4; +// FIXME: these methods assume uniform emitDimensions, need to importance sample based on dimensions +float importanceSample2DDimension(float startDim) { + float dimension = 1.0f; + if (startDim < 1.0f) { + float innerDimensionSquared = startDim * startDim; + float outerDimensionSquared = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 2); + float randDimensionSquared = randFloatInRange(innerDimensionSquared, outerDimensionSquared); + dimension = std::sqrt(randDimensionSquared); + } + return dimension; +} -ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties) { +float importanceSample3DDimension(float startDim) { + float dimension = 1.0f; + if (startDim < 1.0f) { + float innerDimensionCubed = startDim * startDim * startDim; + float outerDimensionCubed = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 3); + float randDimensionCubed = randFloatInRange(innerDimensionCubed, outerDimensionCubed); + dimension = std::cbrt(randDimensionCubed); + } + return dimension; +} + +ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, + const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource, + const TriangleInfo& triangleInfo) { CpuParticle particle; const auto& accelerationSpread = particleProperties.emission.acceleration.spread; @@ -221,33 +259,130 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa float azimuth; if (azimuthFinish >= azimuthStart) { - azimuth = azimuthStart + (azimuthFinish - azimuthStart) * randFloat(); + azimuth = azimuthStart + (azimuthFinish - azimuthStart) * randFloat(); } else { azimuth = azimuthStart + (TWO_PI + azimuthFinish - azimuthStart) * randFloat(); } + // TODO: azimuth and elevation are only used for ellipsoids/circles, but could be used for other shapes too if (emitDimensions == Vectors::ZERO) { // Point emitDirection = glm::quat(glm::vec3(PI_OVER_TWO - elevation, 0.0f, azimuth)) * Vectors::UNIT_Z; } else { - // Ellipsoid - float radiusScale = 1.0f; - if (emitRadiusStart < 1.0f) { - float randRadius = - emitRadiusStart + randFloatInRange(0.0f, particle::MAXIMUM_EMIT_RADIUS_START - emitRadiusStart); - radiusScale = 1.0f - std::pow(1.0f - randRadius, 3.0f); + glm::vec3 emitPosition; + switch (shapeType) { + case SHAPE_TYPE_BOX: { + glm::vec3 dim = importanceSample3DDimension(emitRadiusStart) * 0.5f * emitDimensions; + + int side = randIntInRange(0, 5); + int axis = side % 3; + float direction = side > 2 ? 1.0f : -1.0f; + + emitDirection[axis] = direction; + emitPosition[axis] = direction * dim[axis]; + axis = (axis + 1) % 3; + emitPosition[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); + axis = (axis + 1) % 3; + emitPosition[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); + break; + } + + case SHAPE_TYPE_CYLINDER_X: + case SHAPE_TYPE_CYLINDER_Y: + case SHAPE_TYPE_CYLINDER_Z: { + glm::vec3 radii = importanceSample2DDimension(emitRadiusStart) * 0.5f * emitDimensions; + int axis = shapeType - SHAPE_TYPE_CYLINDER_X; + + emitPosition[axis] = emitDimensions[axis] * randFloatInRange(-0.5f, 0.5f); + emitDirection[axis] = 0.0f; + axis = (axis + 1) % 3; + emitPosition[axis] = radii[axis] * glm::cos(azimuth); + emitDirection[axis] = radii[axis] > 0.0f ? emitPosition[axis] / (radii[axis] * radii[axis]) : 0.0f; + axis = (axis + 1) % 3; + emitPosition[axis] = radii[axis] * glm::sin(azimuth); + emitDirection[axis] = radii[axis] > 0.0f ? emitPosition[axis] / (radii[axis] * radii[axis]) : 0.0f; + emitDirection = glm::normalize(emitDirection); + break; + } + + case SHAPE_TYPE_CIRCLE: { + glm::vec2 radii = importanceSample2DDimension(emitRadiusStart) * 0.5f * glm::vec2(emitDimensions.x, emitDimensions.z); + float x = radii.x * glm::cos(azimuth); + float z = radii.y * glm::sin(azimuth); + emitPosition = glm::vec3(x, 0.0f, z); + emitDirection = Vectors::UP; + break; + } + case SHAPE_TYPE_PLANE: { + glm::vec2 dim = importanceSample2DDimension(emitRadiusStart) * 0.5f * glm::vec2(emitDimensions.x, emitDimensions.z); + + int side = randIntInRange(0, 3); + int axis = side % 2; + float direction = side > 1 ? 1.0f : -1.0f; + + glm::vec2 pos; + pos[axis] = direction * dim[axis]; + axis = (axis + 1) % 2; + pos[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); + + emitPosition = glm::vec3(pos.x, 0.0f, pos.y); + emitDirection = Vectors::UP; + break; + } + + case SHAPE_TYPE_COMPOUND: { + // if we get here we know that geometryResource is loaded + + size_t index = randFloat() * triangleInfo.totalSamples; + Triangle triangle; + for (size_t i = 0; i < triangleInfo.samplesPerTriangle.size(); i++) { + size_t numSamples = triangleInfo.samplesPerTriangle[i]; + if (index < numSamples) { + triangle = triangleInfo.triangles[i]; + break; + } + index -= numSamples; + } + + float edgeLength1 = glm::length(triangle.v1 - triangle.v0); + float edgeLength2 = glm::length(triangle.v2 - triangle.v1); + float edgeLength3 = glm::length(triangle.v0 - triangle.v2); + + float perimeter = edgeLength1 + edgeLength2 + edgeLength3; + float fraction1 = randFloatInRange(0.0f, 1.0f); + float fractionEdge1 = glm::min(fraction1 * perimeter / edgeLength1, 1.0f); + float fraction2 = fraction1 - edgeLength1 / perimeter; + float fractionEdge2 = glm::clamp(fraction2 * perimeter / edgeLength2, 0.0f, 1.0f); + float fraction3 = fraction2 - edgeLength2 / perimeter; + float fractionEdge3 = glm::clamp(fraction3 * perimeter / edgeLength3, 0.0f, 1.0f); + + float dim = importanceSample2DDimension(emitRadiusStart); + triangle = triangle * (glm::scale(emitDimensions) * triangleInfo.transform); + glm::vec3 center = (triangle.v0 + triangle.v1 + triangle.v2) / 3.0f; + glm::vec3 v0 = (dim * (triangle.v0 - center)) + center; + glm::vec3 v1 = (dim * (triangle.v1 - center)) + center; + glm::vec3 v2 = (dim * (triangle.v2 - center)) + center; + + emitPosition = glm::mix(v0, glm::mix(v1, glm::mix(v2, v0, fractionEdge3), fractionEdge2), fractionEdge1); + emitDirection = triangle.getNormal(); + break; + } + + case SHAPE_TYPE_SPHERE: + case SHAPE_TYPE_ELLIPSOID: + default: { + glm::vec3 radii = importanceSample3DDimension(emitRadiusStart) * 0.5f * emitDimensions; + float x = radii.x * glm::cos(elevation) * glm::cos(azimuth); + float y = radii.y * glm::cos(elevation) * glm::sin(azimuth); + float z = radii.z * glm::sin(elevation); + emitPosition = glm::vec3(x, y, z); + emitDirection = glm::normalize(glm::vec3(radii.x > 0.0f ? x / (radii.x * radii.x) : 0.0f, + radii.y > 0.0f ? y / (radii.y * radii.y) : 0.0f, + radii.z > 0.0f ? z / (radii.z * radii.z) : 0.0f)); + break; + } } - glm::vec3 radii = radiusScale * 0.5f * emitDimensions; - float x = radii.x * glm::cos(elevation) * glm::cos(azimuth); - float y = radii.y * glm::cos(elevation) * glm::sin(azimuth); - float z = radii.z * glm::sin(elevation); - glm::vec3 emitPosition = glm::vec3(x, y, z); - emitDirection = glm::normalize(glm::vec3( - radii.x > 0.0f ? x / (radii.x * radii.x) : 0.0f, - radii.y > 0.0f ? y / (radii.y * radii.y) : 0.0f, - radii.z > 0.0f ? z / (radii.z * radii.z) : 0.0f - )); particle.relativePosition += emitOrientation * emitPosition; } } @@ -267,20 +402,28 @@ void ParticleEffectEntityRenderer::stepSimulation() { const auto now = usecTimestampNow(); const auto interval = std::min(USECS_PER_SECOND / 60, now - _lastSimulated); _lastSimulated = now; - + particle::Properties particleProperties; - withReadLock([&]{ + ShapeType shapeType; + GeometryResource::Pointer geometryResource; + withReadLock([&] { particleProperties = _particleProperties; + shapeType = _shapeType; + geometryResource = _geometryResource; }); const auto& modelTransform = getModelTransform(); - if (_emitting && particleProperties.emitting()) { + if (_emitting && particleProperties.emitting() && + (shapeType != SHAPE_TYPE_COMPOUND || (geometryResource && geometryResource->isLoaded()))) { uint64_t emitInterval = particleProperties.emitIntervalUsecs(); if (emitInterval > 0 && interval >= _timeUntilNextEmit) { auto timeRemaining = interval; while (timeRemaining > _timeUntilNextEmit) { + if (_shapeType == SHAPE_TYPE_COMPOUND && !_hasComputedTriangles) { + computeTriangles(geometryResource->getHFMModel()); + } // emit particle - _cpuParticles.push_back(createParticle(now, modelTransform, particleProperties)); + _cpuParticles.push_back(createParticle(now, modelTransform, particleProperties, shapeType, geometryResource, _triangleInfo)); _timeUntilNextEmit = emitInterval; if (emitInterval < timeRemaining) { timeRemaining -= emitInterval; @@ -297,7 +440,7 @@ void ParticleEffectEntityRenderer::stepSimulation() { } const float deltaTime = (float)interval / (float)USECS_PER_SECOND; - // update the particles + // update the particles for (auto& particle : _cpuParticles) { if (_prevEmitterShouldTrail != particleProperties.emission.shouldTrail) { if (_prevEmitterShouldTrail) { @@ -313,7 +456,7 @@ void ParticleEffectEntityRenderer::stepSimulation() { static GpuParticles gpuParticles; gpuParticles.clear(); gpuParticles.reserve(_cpuParticles.size()); // Reserve space - std::transform(_cpuParticles.begin(), _cpuParticles.end(), std::back_inserter(gpuParticles), [&particleProperties, &modelTransform](const CpuParticle& particle) { + std::transform(_cpuParticles.begin(), _cpuParticles.end(), std::back_inserter(gpuParticles), [&particleProperties, &modelTransform] (const CpuParticle& particle) { glm::vec3 position = particle.relativePosition + (particleProperties.emission.shouldTrail ? particle.basePosition : modelTransform.getTranslation()); return GpuParticle(position, glm::vec2(particle.lifetime, particle.seed)); }); @@ -356,5 +499,131 @@ void ParticleEffectEntityRenderer::doRender(RenderArgs* args) { batch.setInputBuffer(0, _particleBuffer, 0, sizeof(GpuParticle)); auto numParticles = _particleBuffer->getSize() / sizeof(GpuParticle); + static const size_t VERTEX_PER_PARTICLE = 4; batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); } + +void ParticleEffectEntityRenderer::fetchGeometryResource() { + QUrl hullURL(_compoundShapeURL); + if (hullURL.isEmpty()) { + _geometryResource.reset(); + } else { + _geometryResource = DependencyManager::get()->getCollisionGeometryResource(hullURL); + } +} + +// FIXME: this is very similar to Model::calculateTriangleSets +void ParticleEffectEntityRenderer::computeTriangles(const hfm::Model& hfmModel) { + PROFILE_RANGE(render, __FUNCTION__); + + int numberOfMeshes = hfmModel.meshes.size(); + + _hasComputedTriangles = true; + _triangleInfo.triangles.clear(); + _triangleInfo.samplesPerTriangle.clear(); + + std::vector areas; + float minArea = FLT_MAX; + AABox bounds; + + for (int i = 0; i < numberOfMeshes; i++) { + const HFMMesh& mesh = hfmModel.meshes.at(i); + + const int numberOfParts = mesh.parts.size(); + for (int j = 0; j < numberOfParts; j++) { + const HFMMeshPart& part = mesh.parts.at(j); + + const int INDICES_PER_TRIANGLE = 3; + const int INDICES_PER_QUAD = 4; + const int TRIANGLES_PER_QUAD = 2; + + // tell our triangleSet how many triangles to expect. + int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD; + int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE; + int totalTriangles = (numberOfQuads * TRIANGLES_PER_QUAD) + numberOfTris; + _triangleInfo.triangles.reserve(_triangleInfo.triangles.size() + totalTriangles); + areas.reserve(areas.size() + totalTriangles); + + auto meshTransform = hfmModel.offset * mesh.modelTransform; + + if (part.quadIndices.size() > 0) { + int vIndex = 0; + for (int q = 0; q < numberOfQuads; q++) { + int i0 = part.quadIndices[vIndex++]; + int i1 = part.quadIndices[vIndex++]; + int i2 = part.quadIndices[vIndex++]; + int i3 = part.quadIndices[vIndex++]; + + // track the model space version... these points will be transformed by the FST's offset, + // which includes the scaling, rotation, and translation specified by the FST/FBX, + // this can't change at runtime, so we can safely store these in our TriangleSet + glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 v3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + + Triangle tri1 = { v0, v1, v3 }; + Triangle tri2 = { v1, v2, v3 }; + _triangleInfo.triangles.push_back(tri1); + _triangleInfo.triangles.push_back(tri2); + + float area1 = tri1.getArea(); + areas.push_back(area1); + if (area1 > EPSILON) { + minArea = std::min(minArea, area1); + } + + float area2 = tri2.getArea(); + areas.push_back(area2); + if (area2 > EPSILON) { + minArea = std::min(minArea, area2); + } + + bounds += v0; + bounds += v1; + bounds += v2; + bounds += v3; + } + } + + if (part.triangleIndices.size() > 0) { + int vIndex = 0; + for (int t = 0; t < numberOfTris; t++) { + int i0 = part.triangleIndices[vIndex++]; + int i1 = part.triangleIndices[vIndex++]; + int i2 = part.triangleIndices[vIndex++]; + + // track the model space version... these points will be transformed by the FST's offset, + // which includes the scaling, rotation, and translation specified by the FST/FBX, + // this can't change at runtime, so we can safely store these in our TriangleSet + glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + + Triangle tri = { v0, v1, v2 }; + _triangleInfo.triangles.push_back(tri); + + float area = tri.getArea(); + areas.push_back(area); + if (area > EPSILON) { + minArea = std::min(minArea, area); + } + + bounds += v0; + bounds += v1; + bounds += v2; + } + } + } + } + + _triangleInfo.totalSamples = 0; + for (auto& area : areas) { + size_t numSamples = area / minArea; + _triangleInfo.samplesPerTriangle.push_back(numSamples); + _triangleInfo.totalSamples += numSamples; + } + + glm::vec3 scale = bounds.getScale(); + _triangleInfo.transform = glm::scale(1.0f / scale) * glm::translate(-bounds.calcCenter()); +} \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 853d5cac29..d13c966e96 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -81,7 +81,18 @@ private: glm::vec2 spare; }; - static CpuParticle createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties); + void computeTriangles(const hfm::Model& hfmModel); + bool _hasComputedTriangles{ false }; + struct TriangleInfo { + std::vector triangles; + std::vector samplesPerTriangle; + size_t totalSamples; + glm::mat4 transform; + } _triangleInfo; + + static CpuParticle createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, + const ShapeType& shapeType, const GeometryResource::Pointer& geometryResource, + const TriangleInfo& triangleInfo); void stepSimulation(); particle::Properties _particleProperties; @@ -90,11 +101,16 @@ private: CpuParticles _cpuParticles; bool _emitting { false }; uint64_t _timeUntilNextEmit { 0 }; - BufferPointer _particleBuffer{ std::make_shared() }; + BufferPointer _particleBuffer { std::make_shared() }; BufferView _uniformBuffer; quint64 _lastSimulated { 0 }; PulsePropertyGroup _pulseProperties; + ShapeType _shapeType; + QString _compoundShapeURL; + + void fetchGeometryResource(); + GeometryResource::Pointer _geometryResource; NetworkTexturePointer _networkTexture; ScenePointer _scene; diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 7050393221..2430643ce2 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -46,12 +46,7 @@ PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) void PolyLineEntityRenderer::buildPipeline() { // FIXME: opaque pipeline - gpu::ShaderPointer program; - if (DISABLE_DEFERRED) { - program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke_forward); - } else { - program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke); - } + gpu::ShaderPointer program = gpu::Shader::createProgram(DISABLE_DEFERRED ? shader::entities_renderer::program::paintStroke_forward : shader::entities_renderer::program::paintStroke); { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -100,19 +95,18 @@ bool PolyLineEntityRenderer::needsRenderUpdate() const { } bool PolyLineEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { - return ( - entity->pointsChanged() || - entity->widthsChanged() || - entity->normalsChanged() || - entity->texturesChanged() || - entity->colorsChanged() || - _isUVModeStretch != entity->getIsUVModeStretch() || - _glow != entity->getGlow() || - _faceCamera != entity->getFaceCamera() - ); + if (entity->pointsChanged() || entity->widthsChanged() || entity->normalsChanged() || entity->texturesChanged() || entity->colorsChanged()) { + return true; + } + + if (_isUVModeStretch != entity->getIsUVModeStretch() || _glow != entity->getGlow() || _faceCamera != entity->getFaceCamera()) { + return true; + } + + return Parent::needsRenderUpdateFromTypedEntity(entity); } -void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { +void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { auto pointsChanged = entity->pointsChanged(); auto widthsChanged = entity->widthsChanged(); auto normalsChanged = entity->normalsChanged(); @@ -124,10 +118,6 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo entity->resetPolyLineChanged(); - // Transform - updateModelTransformAndBound(); - _renderTransform = getModelTransform(); - // Textures if (entity->texturesChanged()) { entity->resetTexturesChanged(); @@ -136,7 +126,9 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (!textures.isEmpty()) { entityTextures = QUrl(textures); } - _texture = DependencyManager::get()->getTexture(entityTextures); + withWriteLock([&] { + _texture = DependencyManager::get()->getTexture(entityTextures); + }); _textureAspectRatio = 1.0f; _textureLoaded = false; } @@ -150,11 +142,13 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo // Data bool faceCameraChanged = faceCamera != _faceCamera; - if (faceCameraChanged || glow != _glow) { - _faceCamera = faceCamera; - _glow = glow; - updateData(); - } + withWriteLock([&] { + if (faceCameraChanged || glow != _glow) { + _faceCamera = faceCamera; + _glow = glow; + updateData(); + } + }); // Geometry if (pointsChanged) { @@ -170,10 +164,23 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo _colors = entity->getStrokeColors(); _color = toGlm(entity->getColor()); } - if (_isUVModeStretch != isUVModeStretch || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged) { - _isUVModeStretch = isUVModeStretch; - updateGeometry(); - } + + bool uvModeStretchChanged = _isUVModeStretch != isUVModeStretch; + _isUVModeStretch = isUVModeStretch; + + bool geometryChanged = uvModeStretchChanged || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged || faceCameraChanged; + + void* key = (void*)this; + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, geometryChanged] () { + withWriteLock([&] { + updateModelTransformAndBound(); + _renderTransform = getModelTransform(); + + if (geometryChanged) { + updateGeometry(); + } + }); + }); } void PolyLineEntityRenderer::updateGeometry() { @@ -272,22 +279,32 @@ void PolyLineEntityRenderer::updateData() { } void PolyLineEntityRenderer::doRender(RenderArgs* args) { - if (_numVertices < 2) { - return; - } - PerformanceTimer perfTimer("RenderablePolyLineEntityItem::render"); Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; - if (!_pipeline || !_glowPipeline) { + size_t numVertices; + Transform transform; + gpu::TexturePointer texture; + withReadLock([&] { + numVertices = _numVertices; + transform = _renderTransform; + texture = _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get()->getWhiteTexture(); + + batch.setResourceBuffer(0, _polylineGeometryBuffer); + batch.setUniformBuffer(0, _polylineDataBuffer); + }); + + if (numVertices < 2) { + return; + } + + if (!_pipeline) { buildPipeline(); } batch.setPipeline(_glow ? _glowPipeline : _pipeline); - batch.setModelTransform(_renderTransform); - batch.setResourceTexture(0, _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get()->getWhiteTexture()); - batch.setResourceBuffer(0, _polylineGeometryBuffer); - batch.setUniformBuffer(0, _polylineDataBuffer); - batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * _numVertices), 0); + batch.setModelTransform(transform); + batch.setResourceTexture(0, texture); + batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * numVertices), 0); } diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index b8a3ad3b3e..3815b57671 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -31,7 +31,7 @@ public: protected: virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; - virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; + virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; virtual ItemKey getKey() override; virtual ShapeKey getShapeKey() override; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 20837070d8..d859d4b739 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -19,8 +19,6 @@ #include "RenderPipelines.h" -#include - //#define SHAPE_ENTITY_USE_FADE_EFFECT #ifdef SHAPE_ENTITY_USE_FADE_EFFECT #include @@ -251,10 +249,14 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { graphics::MultiMaterial materials; auto geometryCache = DependencyManager::get(); GeometryCache::Shape geometryShape; + PrimitiveMode primitiveMode; + RenderLayer renderLayer; bool proceduralRender = false; glm::vec4 outColor; withReadLock([&] { geometryShape = geometryCache->getShapeForEntityShape(_shape); + primitiveMode = _primitiveMode; + renderLayer = _renderLayer; batch.setModelTransform(_renderTransform); // use a transform with scale, rotation, registration point and translation materials = _materials["0"]; auto& schema = materials.getSchemaBuffer().get(); @@ -263,13 +265,13 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { if (_procedural.isReady()) { outColor = _procedural.getColor(outColor); outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f; - _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f)); + _procedural.prepare(batch, _position, _dimensions, _orientation, _created, ProceduralProgramKey(outColor.a < 1.0f)); proceduralRender = true; } }); if (proceduralRender) { - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { geometryCache->renderWireShape(batch, geometryShape, outColor); } else { geometryCache->renderShape(batch, geometryShape, outColor); @@ -278,12 +280,12 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; render::ShapePipelinePointer pipeline; - if (_renderLayer == RenderLayer::WORLD && !DISABLE_DEFERRED) { + if (renderLayer == RenderLayer::WORLD) { pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); } else { pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); } - if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); } else { geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 107847826c..81f367a956 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -19,7 +19,7 @@ #include "GLMHelpers.h" -#include +#include "DeferredLightingEffect.h" using namespace render; using namespace render::entities; @@ -162,17 +162,19 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::vec4 backgroundColor; Transform modelTransform; glm::vec3 dimensions; - bool forwardRendered; + BillboardMode billboardMode; + bool layered; withReadLock([&] { modelTransform = _renderTransform; dimensions = _dimensions; + billboardMode = _billboardMode; float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; textColor = glm::vec4(_textColor, fadeRatio * _textAlpha); textColor = EntityRenderer::calculatePulseColor(textColor, _pulseProperties, _created); backgroundColor = glm::vec4(_backgroundColor, fadeRatio * _backgroundAlpha); backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created); - forwardRendered = _renderLayer != RenderLayer::WORLD || DISABLE_DEFERRED; + layered = _renderLayer != RenderLayer::WORLD; }); // Render background @@ -184,15 +186,20 @@ void TextEntityRenderer::doRender(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; + // FIXME: we need to find a better way of rendering text so we don't have to do this + if (layered) { + DependencyManager::get()->setupKeyLightBatch(args, batch); + } + auto transformToTopLeft = modelTransform; - transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); + transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), billboardMode, args->getViewFrustum().getPosition())); transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed if (backgroundColor.a > 0.0f) { batch.setModelTransform(transformToTopLeft); auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(batch, false, backgroundColor.a < 1.0f, false, false, false, true, forwardRendered); + geometryCache->bindSimpleProgram(batch, false, backgroundColor.a < 1.0f, false, false, false, true, layered); geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID); } @@ -203,7 +210,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { batch.setModelTransform(transformToTopLeft); glm::vec2 bounds = glm::vec2(dimensions.x - (_leftMargin + _rightMargin), dimensions.y - (_topMargin + _bottomMargin)); - _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, forwardRendered); + _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, layered); } } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 0345898b62..7118774d30 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -106,7 +106,7 @@ private: static std::function&, bool&, std::vector&)> _releaseWebSurfaceOperator; public slots: - void emitScriptEvent(const QVariant& scriptMessage); + void emitScriptEvent(const QVariant& scriptMessage) override; signals: void scriptEventReceived(const QVariant& message); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 631148c27a..8a7fa3f8e7 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -33,7 +33,7 @@ using namespace render::entities; ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { - _background->setSkybox(std::make_shared()); + _background->setSkybox(std::make_shared(entity->getCreated())); } void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index af0e34303b..0491bdedae 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -39,9 +39,7 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte } } -void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer entityTree, - EntityItemID entityItemID, - const EntityItemProperties& properties) { +void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer entityTree, EntityItemID entityItemID) { assert(_myAvatar); if (!entityTree) { qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage null entityTree."; @@ -54,11 +52,6 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer enti } entity->setLastBroadcast(usecTimestampNow()); - // serialize ALL properties in an "AvatarEntity" packet - // rather than just the ones being edited. - EntityItemProperties entityProperties = entity->getProperties(); - entityProperties.merge(properties); - OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE); EncodeBitstreamParams params; EntityTreeElementExtraEncodeDataPointer extra { nullptr }; @@ -82,7 +75,7 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit with no myAvatar"; } else if (properties.getOwningAvatarID() == _myAvatar->getID()) { // this is an avatar-based entity --> update our avatar-data rather than sending to the entity-server - queueEditAvatarEntityMessage(entityTree, entityItemID, properties); + queueEditAvatarEntityMessage(entityTree, entityItemID); } else { qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit for another avatar"; } diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 99a5202986..3cc2f016f0 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -50,8 +50,8 @@ public slots: void processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode); private: - void queueEditAvatarEntityMessage(EntityTreePointer entityTree, - EntityItemID entityItemID, const EntityItemProperties& properties); + friend class MyAvatar; + void queueEditAvatarEntityMessage(EntityTreePointer entityTree, EntityItemID entityItemID); private: std::mutex _mutex; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index b6b36704c8..8a50c39da9 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -789,8 +789,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef auto lastEdited = lastEditedFromBufferAdjusted; bool otherOverwrites = overwriteLocalData && !weOwnSimulation; - auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { - if (stillHasGrabActions()) { + // calculate hasGrab once outside the lambda rather than calling it every time inside + bool hasGrab = stillHasGrabAction(); + auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { + if (hasGrab) { return false; } bool simulationChanged = lastEdited > updatedTimestamp; @@ -957,12 +959,18 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // by doing this parsing here... but it's not likely going to fully recover the content. // - if (overwriteLocalData && (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { + if (overwriteLocalData && + !hasGrab && + (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { // NOTE: This code is attempting to "repair" the old data we just got from the server to make it more // closely match where the entities should be if they'd stepped forward in time to "now". The server // is sending us data with a known "last simulated" time. That time is likely in the past, and therefore // this "new" data is actually slightly out of date. We calculate the time we need to skip forward and // use our simulation helper routine to get a best estimate of where the entity should be. + // + // NOTE: We don't want to do this in the hasGrab case because grabs "know best" + // (e.g. grabs will prevent drift between distributed physics simulations). + // float skipTimeForward = (float)(now - lastSimulatedFromBufferAdjusted) / (float)(USECS_PER_SECOND); // we want to extrapolate the motion forward to compensate for packet travel time, but @@ -1426,7 +1434,7 @@ void EntityItem::getTransformAndVelocityProperties(EntityItemProperties& propert void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { uint8_t newPriority = glm::max(priority, _scriptSimulationPriority); - if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasGrabActions()) { + if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasMyGrabAction()) { newPriority = SCRIPT_GRAB_SIMULATION_PRIORITY; } if (newPriority != _scriptSimulationPriority) { @@ -1439,7 +1447,7 @@ void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { void EntityItem::clearScriptSimulationPriority() { // DO NOT markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_PRIORITY) here, because this // is only ever called from the code that actually handles the dirty flags, and it knows best. - _scriptSimulationPriority = stillHasGrabActions() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; + _scriptSimulationPriority = stillHasMyGrabAction() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; } void EntityItem::setPendingOwnershipPriority(uint8_t priority) { @@ -1690,10 +1698,7 @@ AACube EntityItem::getQueryAACube(bool& success) const { } bool EntityItem::shouldPuffQueryAACube() const { - bool hasGrabs = _grabsLock.resultWithReadLock([&] { - return _grabs.count() > 0; - }); - return hasActions() || isChildOfMyAvatar() || isMovingRelativeToParent() || hasGrabs; + return hasActions() || isChildOfMyAvatar() || isMovingRelativeToParent(); } // TODO: get rid of all users of this function... @@ -1888,7 +1893,8 @@ void EntityItem::setScaledDimensions(const glm::vec3& value) { void EntityItem::setUnscaledDimensions(const glm::vec3& value) { glm::vec3 newDimensions = glm::max(value, glm::vec3(ENTITY_ITEM_MIN_DIMENSION)); - if (getUnscaledDimensions() != newDimensions) { + const float MIN_SCALE_CHANGE_SQUARED = 1.0e-6f; + if (glm::length2(getUnscaledDimensions() - newDimensions) > MIN_SCALE_CHANGE_SQUARED) { withWriteLock([&] { _unscaledDimensions = newDimensions; }); @@ -1923,25 +1929,19 @@ void EntityItem::setRotation(glm::quat rotation) { void EntityItem::setVelocity(const glm::vec3& value) { glm::vec3 velocity = getLocalVelocity(); if (velocity != value) { - if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { - if (velocity != Vectors::ZERO) { - setLocalVelocity(Vectors::ZERO); - } - } else { - float speed = glm::length(value); - if (!glm::isnan(speed)) { - const float MIN_LINEAR_SPEED = 0.001f; - const float MAX_LINEAR_SPEED = 270.0f; // 3m per step at 90Hz - if (speed < MIN_LINEAR_SPEED) { - velocity = ENTITY_ITEM_ZERO_VEC3; - } else if (speed > MAX_LINEAR_SPEED) { - velocity = (MAX_LINEAR_SPEED / speed) * value; - } else { - velocity = value; - } - setLocalVelocity(velocity); - _flags |= Simulation::DIRTY_LINEAR_VELOCITY; + float speed = glm::length(value); + if (!glm::isnan(speed)) { + const float MIN_LINEAR_SPEED = 0.001f; + const float MAX_LINEAR_SPEED = 270.0f; // 3m per step at 90Hz + if (speed < MIN_LINEAR_SPEED) { + velocity = ENTITY_ITEM_ZERO_VEC3; + } else if (speed > MAX_LINEAR_SPEED) { + velocity = (MAX_LINEAR_SPEED / speed) * value; + } else { + velocity = value; } + setLocalVelocity(velocity); + _flags |= Simulation::DIRTY_LINEAR_VELOCITY; } } } @@ -1959,19 +1959,15 @@ void EntityItem::setDamping(float value) { void EntityItem::setGravity(const glm::vec3& value) { withWriteLock([&] { if (_gravity != value) { - if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { - _gravity = Vectors::ZERO; - } else { - float magnitude = glm::length(value); - if (!glm::isnan(magnitude)) { - const float MAX_ACCELERATION_OF_GRAVITY = 10.0f * 9.8f; // 10g - if (magnitude > MAX_ACCELERATION_OF_GRAVITY) { - _gravity = (MAX_ACCELERATION_OF_GRAVITY / magnitude) * value; - } else { - _gravity = value; - } - _flags |= Simulation::DIRTY_LINEAR_VELOCITY; + float magnitude = glm::length(value); + if (!glm::isnan(magnitude)) { + const float MAX_ACCELERATION_OF_GRAVITY = 10.0f * 9.8f; // 10g + if (magnitude > MAX_ACCELERATION_OF_GRAVITY) { + _gravity = (MAX_ACCELERATION_OF_GRAVITY / magnitude) * value; + } else { + _gravity = value; } + _flags |= Simulation::DIRTY_LINEAR_VELOCITY; } } }); @@ -1980,23 +1976,19 @@ void EntityItem::setGravity(const glm::vec3& value) { void EntityItem::setAngularVelocity(const glm::vec3& value) { glm::vec3 angularVelocity = getLocalAngularVelocity(); if (angularVelocity != value) { - if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { - setLocalAngularVelocity(Vectors::ZERO); - } else { - float speed = glm::length(value); - if (!glm::isnan(speed)) { - const float MIN_ANGULAR_SPEED = 0.0002f; - const float MAX_ANGULAR_SPEED = 9.0f * TWO_PI; // 1/10 rotation per step at 90Hz - if (speed < MIN_ANGULAR_SPEED) { - angularVelocity = ENTITY_ITEM_ZERO_VEC3; - } else if (speed > MAX_ANGULAR_SPEED) { - angularVelocity = (MAX_ANGULAR_SPEED / speed) * value; - } else { - angularVelocity = value; - } - setLocalAngularVelocity(angularVelocity); - _flags |= Simulation::DIRTY_ANGULAR_VELOCITY; + float speed = glm::length(value); + if (!glm::isnan(speed)) { + const float MIN_ANGULAR_SPEED = 0.0002f; + const float MAX_ANGULAR_SPEED = 9.0f * TWO_PI; // 1/10 rotation per step at 90Hz + if (speed < MIN_ANGULAR_SPEED) { + angularVelocity = ENTITY_ITEM_ZERO_VEC3; + } else if (speed > MAX_ANGULAR_SPEED) { + angularVelocity = (MAX_ANGULAR_SPEED / speed) * value; + } else { + angularVelocity = value; } + setLocalAngularVelocity(angularVelocity); + _flags |= Simulation::DIRTY_ANGULAR_VELOCITY; } } } @@ -2092,7 +2084,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int32_t& group, int32_t& mask } else { if (getDynamic()) { group = BULLET_COLLISION_GROUP_DYNAMIC; - } else if (isMovingRelativeToParent() || hasActions()) { + } else if (hasActions() || isMovingRelativeToParent()) { group = BULLET_COLLISION_GROUP_KINEMATIC; } else { group = BULLET_COLLISION_GROUP_STATIC; @@ -2200,7 +2192,7 @@ void EntityItem::enableNoBootstrap() { } void EntityItem::disableNoBootstrap() { - if (!stillHasGrabActions()) { + if (!stillHasMyGrabAction()) { _flags &= ~Simulation::SPECIAL_FLAGS_NO_BOOTSTRAPPING; _flags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar @@ -2286,7 +2278,13 @@ bool EntityItem::removeAction(EntitySimulationPointer simulation, const QUuid& a return success; } -bool EntityItem::stillHasGrabActions() const { +bool EntityItem::stillHasGrabAction() const { + return !_grabActions.empty(); +} + +// retutrns 'true' if there exists an action that returns 'true' for EntityActionInterface::isMine() +// (e.g. the action belongs to the MyAvatar instance) +bool EntityItem::stillHasMyGrabAction() const { QList holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD); QList::const_iterator i = holdActions.begin(); while (i != holdActions.end()) { @@ -2714,20 +2712,6 @@ void EntityItem::setLastEdited(quint64 lastEdited) { }); } -quint64 EntityItem::getLastBroadcast() const { - quint64 result; - withReadLock([&] { - result = _lastBroadcast; - }); - return result; -} - -void EntityItem::setLastBroadcast(quint64 lastBroadcast) { - withWriteLock([&] { - _lastBroadcast = lastBroadcast; - }); -} - void EntityItem::markAsChangedOnServer() { withWriteLock([&] { _changedOnServer = usecTimestampNow(); @@ -3071,30 +3055,18 @@ bool EntityItem::getCollisionless() const { } uint16_t EntityItem::getCollisionMask() const { - uint16_t result; - withReadLock([&] { - result = _collisionMask; - }); - return result; + return _collisionMask; } bool EntityItem::getDynamic() const { if (SHAPE_TYPE_STATIC_MESH == getShapeType()) { return false; } - bool result; - withReadLock([&] { - result = _dynamic; - }); - return result; + return _dynamic; } bool EntityItem::getLocked() const { - bool result; - withReadLock([&] { - result = _locked; - }); - return result; + return _locked; } void EntityItem::setLocked(bool value) { @@ -3166,7 +3138,6 @@ uint32_t EntityItem::getDirtyFlags() const { return result; } - void EntityItem::markDirtyFlags(uint32_t mask) { withWriteLock([&] { mask &= Simulation::DIRTY_FLAGS; @@ -3493,6 +3464,9 @@ void EntityItem::addGrab(GrabPointer grab) { simulation->addDynamic(action); markDirtyFlags(Simulation::DIRTY_MOTION_TYPE); simulation->changeEntity(getThisPointer()); + + // don't forget to set isMine() for locally-created grabs + action->setIsMine(grab->getOwnerID() == Physics::getSessionUUID()); } } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a9a8baa413..c57fd16a2e 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -124,8 +124,8 @@ public: { return (float)(usecTimestampNow() - getLastEdited()) / (float)USECS_PER_SECOND; } /// Last time we sent out an edit packet for this entity - quint64 getLastBroadcast() const; - void setLastBroadcast(quint64 lastBroadcast); + quint64 getLastBroadcast() const { return _lastBroadcast; } + void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; } void markAsChangedOnServer(); quint64 getLastChangedOnServer() const; @@ -448,7 +448,7 @@ public: bool clearActions(EntitySimulationPointer simulation); void setDynamicData(QByteArray dynamicData); const QByteArray getDynamicData() const; - bool hasActions() const { return !_objectActions.empty(); } + bool hasActions() const { return !_objectActions.empty() || !_grabActions.empty(); } QList getActionIDs() const { return _objectActions.keys(); } QVariantMap getActionArguments(const QUuid& actionID) const; void deserializeActions(); @@ -511,8 +511,6 @@ public: virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } - virtual void emitScriptEvent(const QVariant& message) {} - QUuid getLastEditedBy() const { return _lastEditedBy; } void setLastEditedBy(QUuid value) { _lastEditedBy = value; } @@ -564,6 +562,8 @@ public: static void setPrimaryViewFrustumPositionOperator(std::function getPrimaryViewFrustumPositionOperator) { _getPrimaryViewFrustumPositionOperator = getPrimaryViewFrustumPositionOperator; } static glm::vec3 getPrimaryViewFrustumPosition() { return _getPrimaryViewFrustumPositionOperator(); } + bool stillHasMyGrabAction() const; + signals: void requestRenderUpdate(); void spaceUpdate(std::pair data); @@ -576,7 +576,7 @@ protected: void setSimulated(bool simulated) { _simulated = simulated; } const QByteArray getDynamicDataInternal() const; - bool stillHasGrabActions() const; + bool stillHasGrabAction() const; void setDynamicDataInternal(QByteArray dynamicData); virtual void dimensionsChanged() override; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 3efedf02ec..5958af66dd 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -127,6 +127,7 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); addShapeType(SHAPE_TYPE_STATIC_MESH); addShapeType(SHAPE_TYPE_ELLIPSOID); + addShapeType(SHAPE_TYPE_CIRCLE); } QHash stringToMaterialMappingModeLookup; @@ -1114,23 +1115,28 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * default, particles emit along the entity's local z-axis, and azimuthStart and azimuthFinish * are relative to the entity's local x-axis. The default value is a rotation of -90 degrees about the local x-axis, i.e., * the particles emit vertically. - * @property {Vec3} emitDimensions=0,0,0 - The dimensions of the ellipsoid from which particles are emitted. - * @property {number} emitRadiusStart=1 - The starting radius within the ellipsoid at which particles start being emitted; - * range 0.01.0 for the ellipsoid center to the ellipsoid surface, respectively. - * Particles are emitted from the portion of the ellipsoid that lies between emitRadiusStart and the - * ellipsoid's surface. + * @property {Vec3} emitDimensions=0,0,0 - The dimensions of the shape from which particles are emitted. The shape is specified with + * shapeType. + * @property {number} emitRadiusStart=1 - The starting radius within the shape at which particles start being emitted; + * range 0.01.0 for the center to the surface, respectively. + * Particles are emitted from the portion of the shape that lies between emitRadiusStart and the + * shape's surface. * @property {number} polarStart=0 - The angle in radians from the entity's local z-axis at which particles start being emitted - * within the ellipsoid; range 0Math.PI. Particles are emitted from the portion of the - * ellipsoid that lies between polarStart and polarFinish. + * within the shape; range 0Math.PI. Particles are emitted from the portion of the + * shape that lies between polarStart and polarFinish. Only used if shapeType is + * ellipsoid or sphere. * @property {number} polarFinish=0 - The angle in radians from the entity's local z-axis at which particles stop being emitted - * within the ellipsoid; range 0Math.PI. Particles are emitted from the portion of the - * ellipsoid that lies between polarStart and polarFinish. + * within the shape; range 0Math.PI. Particles are emitted from the portion of the + * shape that lies between polarStart and polarFinish. Only used if shapeType is + * ellipsoid or sphere. * @property {number} azimuthStart=-Math.PI - The angle in radians from the entity's local x-axis about the entity's local * z-axis at which particles start being emitted; range -Math.PIMath.PI. Particles are - * emitted from the portion of the ellipsoid that lies between azimuthStart and azimuthFinish. + * emitted from the portion of the shape that lies between azimuthStart and azimuthFinish. + * Only used if shapeType is ellipsoid, sphere, or circle. * @property {number} azimuthFinish=Math.PI - The angle in radians from the entity's local x-axis about the entity's local * z-axis at which particles stop being emitted; range -Math.PIMath.PI. Particles are - * emitted from the portion of the ellipsoid that lies between azimuthStart and azimuthFinish. + * emitted from the portion of the shape that lies between azimuthStart and azimuthFinish. + * Only used if shapeType is ellipsoid, sphere, or circle.. * * @property {string} textures="" - The URL of a JPG or PNG image file to display for each particle. If you want transparency, * use PNG format. @@ -1170,7 +1176,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * up in the world. If true, they will point towards the entity's up vector, based on its orientation. * @property {Entities.Pulse} pulse - The pulse-related properties. Deprecated. * - * @property {ShapeType} shapeType="none" - Currently not used. Read-only. + * @property {ShapeType} shapeType="ellipsoid" - The shape of the collision hull used if collisions are enabled. + * @property {string} compoundShapeURL="" - The model file to use for the compound shape if shapeType is + * "compound". * * @example Create a ball of green smoke. * particles = Entities.addEntity({ @@ -1658,6 +1666,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool // Particles only if (_type == EntityTypes::ParticleEffect) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL); COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha); _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); @@ -3104,6 +3113,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy if (properties.getType() == EntityTypes::ParticleEffect) { APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)(properties.getShapeType())); + APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, properties.getCompoundShapeURL()); APPEND_ENTITY_PROPERTY(PROP_COLOR, properties.getColor()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, properties.getAlpha()); _staticPulse.setProperties(properties); @@ -3584,6 +3594,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int if (properties.getType() == EntityTypes::ParticleEffect) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE_TYPE, ShapeType, setShapeType); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLOR, u8vec3Color, setColor); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ALPHA, float, setAlpha); properties.getPulse().decodeFromEditPacket(propertyFlags, dataAt, processedBytes); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 22cd26eac6..f8c45b792a 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1101,13 +1101,13 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer void EntityScriptingInterface::onAddingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit addingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(EntityItemID, entity->getEntityItemID())); } } void EntityScriptingInterface::onDeletingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit deletingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(EntityItemID, entity->getEntityItemID())); } } @@ -1646,11 +1646,9 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); - EntityItemProperties properties; - EntityItemPointer entity; bool doTransmit = false; - _entityTree->withWriteLock([this, &entity, entityID, myNodeID, &doTransmit, actor, &properties] { + _entityTree->withWriteLock([this, &entity, entityID, myNodeID, &doTransmit, actor] { EntitySimulationPointer simulation = _entityTree->getSimulation(); entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { @@ -1669,16 +1667,12 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, doTransmit = actor(simulation, entity); _entityTree->entityChanged(entity); - if (doTransmit) { - properties.setEntityHostType(entity->getEntityHostType()); - properties.setOwningAvatarID(entity->getOwningAvatarID()); - } }); // transmit the change if (doTransmit) { - _entityTree->withReadLock([&] { - properties = entity->getProperties(); + EntityItemProperties properties = _entityTree->resultWithReadLock([&] { + return entity->getProperties(); }); properties.setActionDataDirty(); @@ -2202,14 +2196,7 @@ bool EntityScriptingInterface::wantsHandControllerPointerEvents(const QUuid& id) } void EntityScriptingInterface::emitScriptEvent(const EntityItemID& entityID, const QVariant& message) { - if (_entityTree) { - _entityTree->withReadLock([&] { - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (entity) { - entity->emitScriptEvent(message); - } - }); - } + EntityTree::emitScriptEvent(entityID, message); } // TODO move this someplace that makes more sense... diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 0cf9070b08..f6aedac3fc 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -1529,7 +1529,6 @@ public slots: * @function Entities.emitScriptEvent * @param {Uuid} entityID - The ID of the {@link Entities.EntityType|Web} entity. * @param {string} message - The message to send. - * @todo This function is currently not implemented. */ Q_INVOKABLE void emitScriptEvent(const EntityItemID& entityID, const QVariant& message); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index d64c8870eb..1ccf3fcfa2 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "EntitySimulation.h" #include "VariantMapToScriptValue.h" @@ -286,27 +287,7 @@ void EntityTree::postAddEntity(EntityItemPointer entity) { assert(entity); if (getIsServer()) { - QString certID(entity->getCertificateID()); - EntityItemID entityItemID = entity->getEntityItemID(); - EntityItemID existingEntityItemID; - - { - QWriteLocker locker(&_entityCertificateIDMapLock); - existingEntityItemID = _entityCertificateIDMap.value(certID); - if (!certID.isEmpty()) { - _entityCertificateIDMap.insert(certID, entityItemID); - qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; - } - } - - // Delete an already-existing entity from the tree if it has the same - // CertificateID as the entity we're trying to add. - if (!existingEntityItemID.isNull() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { - qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" - << existingEntityItemID << ". Deleting existing entity."; - deleteEntity(existingEntityItemID, true); - return; - } + addCertifiedEntityOnServer(entity); } // check to see if we need to simulate this entity.. @@ -764,13 +745,7 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) theEntity->die(); if (getIsServer()) { - { - QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); - QString certID = theEntity->getCertificateID(); - if (theEntity->getEntityItemID() == _entityCertificateIDMap.value(certID)) { - _entityCertificateIDMap.remove(certID); - } - } + removeCertifiedEntityOnServer(theEntity); // set up the deleted entities ID QWriteLocker recentlyDeletedEntitiesLocker(&_recentlyDeletedEntitiesLock); @@ -1421,11 +1396,123 @@ bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) { return false; } +void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { + QString certID(entity->getCertificateID()); + EntityItemID existingEntityItemID; + if (!certID.isEmpty()) { + EntityItemID entityItemID = entity->getEntityItemID(); + QWriteLocker locker(&_entityCertificateIDMapLock); + QList& entityList = _entityCertificateIDMap[certID]; // inserts it if needed. + if (!entityList.isEmpty() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) { + existingEntityItemID = entityList.first(); // we will only care about the first, if any, below. + entityList.removeOne(existingEntityItemID); + } + entityList << entityItemID; // adds to list within hash because entityList is a reference. + qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID << "total" << entityList.size() << "entities."; + } + // Delete an already-existing entity from the tree if it has the same + // CertificateID as the entity we're trying to add. + if (!existingEntityItemID.isNull()) { + qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" + << existingEntityItemID << ". Deleting existing entity."; + withWriteLock([&] { + deleteEntity(existingEntityItemID, true); + }); + } +} + +void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) { + QString certID = entity->getCertificateID(); + if (!certID.isEmpty()) { + QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); + QList& entityList = _entityCertificateIDMap[certID]; + entityList.removeOne(entity->getEntityItemID()); + if (entityList.isEmpty()) { + // hmmm, do we to make it be a hash instead of a list, so that this is faster if you stamp out 1000 of a domainUnlimited? + _entityCertificateIDMap.remove(certID); + } + } +} + +void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove) { + QReadLocker locker(&_entityCertificateIDMapLock); + QHashIterator> i(_entityCertificateIDMap); + qCDebug(entities) << _entityCertificateIDMap.size() << "certificates present."; + while (i.hasNext()) { + i.next(); + const auto& certificateID = i.key(); + const auto& entityIDs = i.value(); + if (entityIDs.isEmpty()) { + continue; + } + + // Examine each cert: + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); + requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); + QJsonObject request; + request["certificate_id"] = certificateID; + networkRequest.setUrl(requestURL); + + QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + + connect(networkReply, &QNetworkReply::finished, this, [this, entityIDs, networkReply, minimumAgeToRemove, &certificateID] { + + QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); + jsonObject = jsonObject["data"].toObject(); + bool failure = networkReply->error() != QNetworkReply::NoError; + auto failureReason = networkReply->error(); + networkReply->deleteLater(); + if (failure) { + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << failureReason + << "; NOT deleting cert" << certificateID << "More info:" << jsonObject; + return; + } + QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); + if (jsonObject["domain_id"].toString() == thisDomainID) { + // Entity belongs here. Nothing to do. + return; + } + // Entity does not belong here: + QList retained; + for (int i = 0; i < entityIDs.size(); i++) { + EntityItemID entityID = entityIDs.at(i); + EntityItemPointer entity = findEntityByEntityItemID(entityID); + if (!entity) { + qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID; + continue; + } + if (entity->getAge() <= minimumAgeToRemove) { + qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID; + retained << entityID; + continue; + } + qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() + << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; + withWriteLock([&] { + deleteEntity(entityID, true); + }); + } + { + QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); + if (retained.isEmpty()) { + qCDebug(entities) << "Removed" << certificateID; + _entityCertificateIDMap.remove(certificateID); + } else { + qCDebug(entities) << "Retained" << retained.size() << "young entities for" << certificateID; + _entityCertificateIDMap[certificateID] = retained; + } + } + }); + } +} + void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) { QTimer* _challengeOwnershipTimeoutTimer = new QTimer(this); - connect(this, &EntityTree::killChallengeOwnershipTimeoutTimer, this, [=](const QString& certID) { - QReadLocker locker(&_entityCertificateIDMapLock); - EntityItemID id = _entityCertificateIDMap.value(certID); + connect(this, &EntityTree::killChallengeOwnershipTimeoutTimer, this, [=](const EntityItemID& id) { if (entityItemID == id && _challengeOwnershipTimeoutTimer) { _challengeOwnershipTimeoutTimer->stop(); _challengeOwnershipTimeoutTimer->deleteLater(); @@ -1445,26 +1532,21 @@ void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) _challengeOwnershipTimeoutTimer->start(5000); } -QByteArray EntityTree::computeNonce(const QString& certID, const QString ownerKey) { +QByteArray EntityTree::computeNonce(const EntityItemID& entityID, const QString ownerKey) { QUuid nonce = QUuid::createUuid(); //random, 5-hex value, separated by "-" QByteArray nonceBytes = nonce.toByteArray(); - QWriteLocker locker(&_certNonceMapLock); - _certNonceMap.insert(certID, QPair(nonce, ownerKey)); + QWriteLocker locker(&_entityNonceMapLock); + _entityNonceMap.insert(entityID, QPair(nonce, ownerKey)); return nonceBytes; } -bool EntityTree::verifyNonce(const QString& certID, const QString& nonce, EntityItemID& id) { - { - QReadLocker certIdMapLocker(&_entityCertificateIDMapLock); - id = _entityCertificateIDMap.value(certID); - } - +bool EntityTree::verifyNonce(const EntityItemID& entityID, const QString& nonce) { QString actualNonce, key; { - QWriteLocker locker(&_certNonceMapLock); - QPair sent = _certNonceMap.take(certID); + QWriteLocker locker(&_entityNonceMapLock); + QPair sent = _entityNonceMap.take(entityID); actualNonce = sent.first.toString(); key = sent.second; } @@ -1474,9 +1556,9 @@ bool EntityTree::verifyNonce(const QString& certID, const QString& nonce, Entity bool verificationSuccess = EntityItemProperties::verifySignature(annotatedKey.toUtf8(), hashedActualNonce, QByteArray::fromBase64(nonce.toUtf8())); if (verificationSuccess) { - qCDebug(entities) << "Ownership challenge for Cert ID" << certID << "succeeded."; + qCDebug(entities) << "Ownership challenge for Entity ID" << entityID << "succeeded."; } else { - qCDebug(entities) << "Ownership challenge for Cert ID" << certID << "failed. Actual nonce:" << actualNonce << + qCDebug(entities) << "Ownership challenge for Entity ID" << entityID << "failed. Actual nonce:" << actualNonce << "\nHashed actual nonce (digest):" << hashedActualNonce << "\nSent nonce (signature)" << nonce << "\nKey" << key; } @@ -1484,42 +1566,42 @@ bool EntityTree::verifyNonce(const QString& certID, const QString& nonce, Entity } void EntityTree::processChallengeOwnershipRequestPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; int nodeToChallengeByteArraySize; - message.readPrimitive(&certIDByteArraySize); + message.readPrimitive(&idByteArraySize); message.readPrimitive(&textByteArraySize); message.readPrimitive(&nodeToChallengeByteArraySize); - QByteArray certID(message.read(certIDByteArraySize)); + QByteArray id(message.read(idByteArraySize)); QByteArray text(message.read(textByteArraySize)); QByteArray nodeToChallenge(message.read(nodeToChallengeByteArraySize)); - sendChallengeOwnershipRequestPacket(certID, text, nodeToChallenge, sourceNode); + sendChallengeOwnershipRequestPacket(id, text, nodeToChallenge, sourceNode); } void EntityTree::processChallengeOwnershipReplyPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { auto nodeList = DependencyManager::get(); - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; int challengingNodeUUIDByteArraySize; - message.readPrimitive(&certIDByteArraySize); + message.readPrimitive(&idByteArraySize); message.readPrimitive(&textByteArraySize); message.readPrimitive(&challengingNodeUUIDByteArraySize); - QByteArray certID(message.read(certIDByteArraySize)); + QByteArray id(message.read(idByteArraySize)); QByteArray text(message.read(textByteArraySize)); QUuid challengingNode = QUuid::fromRfc4122(message.read(challengingNodeUUIDByteArraySize)); auto challengeOwnershipReplyPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, - certIDByteArraySize + text.length() + 2 * sizeof(int), + idByteArraySize + text.length() + 2 * sizeof(int), true); - challengeOwnershipReplyPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipReplyPacket->writePrimitive(idByteArraySize); challengeOwnershipReplyPacket->writePrimitive(text.length()); - challengeOwnershipReplyPacket->write(certID); + challengeOwnershipReplyPacket->write(id); challengeOwnershipReplyPacket->write(text); nodeList->sendPacket(std::move(challengeOwnershipReplyPacket), *(nodeList->nodeWithUUID(challengingNode))); @@ -1529,7 +1611,7 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri // 1. Obtain a nonce auto nodeList = DependencyManager::get(); - QByteArray text = computeNonce(certID, ownerKey); + QByteArray text = computeNonce(entityItemID, ownerKey); if (text == "") { qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. Deleting entity..."; @@ -1539,14 +1621,14 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri } else { qCDebug(entities) << "Challenging ownership of Cert ID" << certID; // 2. Send the nonce to the rezzing avatar's node - QByteArray certIDByteArray = certID.toUtf8(); - int certIDByteArraySize = certIDByteArray.size(); + QByteArray idByteArray = entityItemID.toByteArray(); + int idByteArraySize = idByteArray.size(); auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership, - certIDByteArraySize + text.length() + 2 * sizeof(int), + idByteArraySize + text.length() + 2 * sizeof(int), true); - challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(idByteArraySize); challengeOwnershipPacket->writePrimitive(text.length()); - challengeOwnershipPacket->write(certIDByteArray); + challengeOwnershipPacket->write(idByteArray); challengeOwnershipPacket->write(text); nodeList->sendPacket(std::move(challengeOwnershipPacket), *senderNode); @@ -1560,24 +1642,24 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri } } -void EntityTree::sendChallengeOwnershipRequestPacket(const QByteArray& certID, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode) { +void EntityTree::sendChallengeOwnershipRequestPacket(const QByteArray& id, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode) { auto nodeList = DependencyManager::get(); // In this case, Client A is challenging Client B. Client A is inspecting a certified entity that it wants // to make sure belongs to Avatar B. QByteArray senderNodeUUID = senderNode->getUUID().toRfc4122(); - int certIDByteArraySize = certID.length(); + int idByteArraySize = id.length(); int TextByteArraySize = text.length(); int senderNodeUUIDSize = senderNodeUUID.length(); auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnershipRequest, - certIDByteArraySize + TextByteArraySize + senderNodeUUIDSize + 3 * sizeof(int), + idByteArraySize + TextByteArraySize + senderNodeUUIDSize + 3 * sizeof(int), true); - challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(idByteArraySize); challengeOwnershipPacket->writePrimitive(TextByteArraySize); challengeOwnershipPacket->writePrimitive(senderNodeUUIDSize); - challengeOwnershipPacket->write(certID); + challengeOwnershipPacket->write(id); challengeOwnershipPacket->write(text); challengeOwnershipPacket->write(senderNodeUUID); @@ -1636,22 +1718,21 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt } void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { - int certIDByteArraySize; + int idByteArraySize; int textByteArraySize; - message.readPrimitive(&certIDByteArraySize); + message.readPrimitive(&idByteArraySize); message.readPrimitive(&textByteArraySize); - QString certID(message.read(certIDByteArraySize)); + EntityItemID id(message.read(idByteArraySize)); QString text(message.read(textByteArraySize)); - emit killChallengeOwnershipTimeoutTimer(certID); + emit killChallengeOwnershipTimeoutTimer(id); - EntityItemID id; - if (!verifyNonce(certID, text, id)) { - if (!id.isNull()) { + if (!verifyNonce(id, text)) { + withWriteLock([&] { deleteEntity(id, true); - } + }); } } @@ -2978,6 +3059,7 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const { std::function EntityTree::_getEntityObjectOperator = nullptr; std::function EntityTree::_textSizeOperator = nullptr; std::function EntityTree::_areEntityClicksCapturedOperator = nullptr; +std::function EntityTree::_emitScriptEventOperator = nullptr; QObject* EntityTree::getEntityObject(const QUuid& id) { if (_getEntityObjectOperator) { @@ -3000,6 +3082,12 @@ bool EntityTree::areEntityClicksCaptured() { return false; } +void EntityTree::emitScriptEvent(const QUuid& id, const QVariant& message) { + if (_emitScriptEventOperator) { + _emitScriptEventOperator(id, message); + } +} + void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, MovingEntitiesOperator& moveOperator, bool force, bool tellServer) { // if the queryBox has changed, tell the entity-server diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 39b3dc57c7..c80517c82b 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -157,11 +157,6 @@ public: return _recentlyDeletedEntityItemIDs; } - QHash getEntityCertificateIDMap() const { - QReadLocker locker(&_entityCertificateIDMapLock); - return _entityCertificateIDMap; - } - void forgetEntitiesDeletedBefore(quint64 sinceTime); int processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode); @@ -252,8 +247,8 @@ public: static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; - QByteArray computeNonce(const QString& certID, const QString ownerKey); - bool verifyNonce(const QString& certID, const QString& nonce, EntityItemID& id); + QByteArray computeNonce(const EntityItemID& entityID, const QString ownerKey); + bool verifyNonce(const EntityItemID& entityID, const QString& nonce); QUuid getMyAvatarSessionUUID() { return _myAvatar ? _myAvatar->getSessionUUID() : QUuid(); } void setMyAvatar(std::shared_ptr myAvatar) { _myAvatar = myAvatar; } @@ -272,10 +267,14 @@ public: static void setEntityClicksCapturedOperator(std::function areEntityClicksCapturedOperator) { _areEntityClicksCapturedOperator = areEntityClicksCapturedOperator; } static bool areEntityClicksCaptured(); + static void setEmitScriptEventOperator(std::function emitScriptEventOperator) { _emitScriptEventOperator = emitScriptEventOperator; } + static void emitScriptEvent(const QUuid& id, const QVariant& message); + std::map getNamedPaths() const { return _namedPaths; } void updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, bool force, bool tellServer); + void startDynamicDomainVerificationOnServer(float minimumAgeToRemove); signals: void deletingEntity(const EntityItemID& entityID); @@ -287,7 +286,7 @@ signals: void entityServerScriptChanging(const EntityItemID& entityItemID, const bool reload); void newCollisionSoundURL(const QUrl& url, const EntityItemID& entityID); void clearingEntities(); - void killChallengeOwnershipTimeoutTimer(const QString& certID); + void killChallengeOwnershipTimeoutTimer(const EntityItemID& certID); protected: @@ -324,10 +323,10 @@ protected: QHash _entityMap; mutable QReadWriteLock _entityCertificateIDMapLock; - QHash _entityCertificateIDMap; + QHash> _entityCertificateIDMap; - mutable QReadWriteLock _certNonceMapLock; - QHash> _certNonceMap; + mutable QReadWriteLock _entityNonceMapLock; + QHash> _entityNonceMap; EntitySimulationPointer _simulation; @@ -374,8 +373,10 @@ protected: Q_INVOKABLE void startChallengeOwnershipTimer(const EntityItemID& entityItemID); private: + void addCertifiedEntityOnServer(EntityItemPointer entity); + void removeCertifiedEntityOnServer(EntityItemPointer entity); void sendChallengeOwnershipPacket(const QString& certID, const QString& ownerKey, const EntityItemID& entityItemID, const SharedNodePointer& senderNode); - void sendChallengeOwnershipRequestPacket(const QByteArray& certID, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode); + void sendChallengeOwnershipRequestPacket(const QByteArray& id, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode); void validatePop(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode); std::shared_ptr _myAvatar{ nullptr }; @@ -383,6 +384,7 @@ private: static std::function _getEntityObjectOperator; static std::function _textSizeOperator; static std::function _areEntityClicksCapturedOperator; + static std::function _emitScriptEventOperator; std::vector _staleProxies; diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 86ca6065bb..098183299f 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -49,8 +49,6 @@ class LineEntityItem : public EntityItem { QVector getLinePoints() const; - virtual ShapeType getShapeType() const override { return SHAPE_TYPE_NONE; } - // never have a ray intersection pick a LineEntityItem. virtual bool supportsDetailedIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 08468617ba..d532fefe7e 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -175,7 +175,7 @@ protected: QString _textures; - ShapeType _shapeType = SHAPE_TYPE_NONE; + ShapeType _shapeType { SHAPE_TYPE_NONE }; private: uint64_t _lastAnimated{ 0 }; diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index b916ecc3de..12119f1466 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -410,6 +410,7 @@ EntityItemProperties ParticleEffectEntityItem::getProperties(const EntityPropert EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(shapeType, getShapeType); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(compoundShapeURL, getCompoundShapeURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); withReadLock([&] { @@ -464,6 +465,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert bool somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); withWriteLock([&] { @@ -540,6 +542,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch const unsigned char* dataAt = data; READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); + READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY(PROP_COLOR, u8vec3Color, setColor); READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); withWriteLock([&] { @@ -598,6 +601,7 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_SHAPE_TYPE; + requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_COLOR; requestedProperties += PROP_ALPHA; requestedProperties += _pulseProperties.getEntityProperties(params); @@ -656,6 +660,7 @@ void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); + APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); withReadLock([&] { @@ -718,11 +723,42 @@ void ParticleEffectEntityItem::debugDump() const { } void ParticleEffectEntityItem::setShapeType(ShapeType type) { + switch (type) { + case SHAPE_TYPE_NONE: + case SHAPE_TYPE_CAPSULE_X: + case SHAPE_TYPE_CAPSULE_Y: + case SHAPE_TYPE_CAPSULE_Z: + case SHAPE_TYPE_HULL: + case SHAPE_TYPE_SIMPLE_HULL: + case SHAPE_TYPE_SIMPLE_COMPOUND: + case SHAPE_TYPE_STATIC_MESH: + // these types are unsupported for ParticleEffectEntity + type = particle::DEFAULT_SHAPE_TYPE; + break; + default: + break; + } + withWriteLock([&] { - if (type != _shapeType) { - _shapeType = type; - _flags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + _shapeType = type; + }); +} + +ShapeType ParticleEffectEntityItem::getShapeType() const { + return resultWithReadLock([&] { + return _shapeType; + }); +} + +void ParticleEffectEntityItem::setCompoundShapeURL(const QString& compoundShapeURL) { + withWriteLock([&] { + _compoundShapeURL = compoundShapeURL; + }); +} + +QString ParticleEffectEntityItem::getCompoundShapeURL() const { + return resultWithReadLock([&] { + return _compoundShapeURL; }); } diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 0755d7868b..52f229201e 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -79,6 +79,7 @@ namespace particle { static const QString DEFAULT_TEXTURES = ""; static const bool DEFAULT_EMITTER_SHOULD_TRAIL = false; static const bool DEFAULT_ROTATE_WITH_ENTITY = false; + static const ShapeType DEFAULT_SHAPE_TYPE = ShapeType::SHAPE_TYPE_ELLIPSOID; template struct Range { @@ -255,7 +256,10 @@ public: float getAlphaSpread() const { return _particleProperties.alpha.gradient.spread; } void setShapeType(ShapeType type) override; - virtual ShapeType getShapeType() const override { return _shapeType; } + virtual ShapeType getShapeType() const override; + + QString getCompoundShapeURL() const; + virtual void setCompoundShapeURL(const QString& url); virtual void debugDump() const override; @@ -349,7 +353,8 @@ protected: PulsePropertyGroup _pulseProperties; bool _isEmitting { true }; - ShapeType _shapeType { SHAPE_TYPE_NONE }; + ShapeType _shapeType{ particle::DEFAULT_SHAPE_TYPE }; + QString _compoundShapeURL { "" }; }; #endif // hifi_ParticleEffectEntityItem_h diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index 363a7f39d1..fc590e06a4 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -112,7 +112,7 @@ protected: //! This is SHAPE_TYPE_ELLIPSOID rather than SHAPE_TYPE_NONE to maintain //! prior functionality where new or unsupported shapes are treated as //! ellipsoids. - ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_ELLIPSOID }; + ShapeType _collisionShapeType { ShapeType::SHAPE_TYPE_ELLIPSOID }; }; #endif // hifi_ShapeEntityItem_h diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 98b18869fc..0771d9ad54 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -13,7 +13,6 @@ #include #include -#include #include @@ -353,7 +352,7 @@ bool ZoneEntityItem::contains(const glm::vec3& point) const { Extents meshExtents = hfmModel.getMeshExtents(); glm::vec3 meshExtentsDiagonal = meshExtents.maximum - meshExtents.minimum; - glm::vec3 offset = -meshExtents.minimum- (meshExtentsDiagonal * getRegistrationPoint()); + glm::vec3 offset = -meshExtents.minimum - (meshExtentsDiagonal * getRegistrationPoint()); glm::vec3 scale(getScaledDimensions() / meshExtentsDiagonal); glm::mat4 hfmToEntityMatrix = glm::scale(scale) * glm::translate(offset); @@ -463,9 +462,6 @@ void ZoneEntityItem::fetchCollisionGeometryResource() { if (hullURL.isEmpty()) { _shapeResource.reset(); } else { - QUrlQuery queryArgs(hullURL); - queryArgs.addQueryItem("collision-hull", ""); - hullURL.setQuery(queryArgs); _shapeResource = DependencyManager::get()->getCollisionGeometryResource(hullURL); } } diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index df6ce50fd6..69e3227135 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -133,7 +133,7 @@ protected: KeyLightPropertyGroup _keyLightProperties; AmbientLightPropertyGroup _ambientLightProperties; - ShapeType _shapeType = DEFAULT_SHAPE_TYPE; + ShapeType _shapeType { DEFAULT_SHAPE_TYPE }; QString _compoundShapeURL; // The following 3 values are the defaults for zone creation diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 8ad419c7ec..362ae93e99 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -13,27 +13,26 @@ #define hifi_FBX_h_ #include -#include #include #include #include +#include + // See comment in FBXSerializer::parseFBX(). static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23; -static const QByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary "); -static const QByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); +static const hifi::ByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary "); +static const hifi::ByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); static const quint32 FBX_VERSION_2015 = 7400; static const quint32 FBX_VERSION_2016 = 7500; -static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; -static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; -static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; -static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2; - static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0; static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1; +// The version of the FBX node containing the draco mesh. See also: DRACO_MESH_VERSION in HFM.h +static const int FBX_DRACO_MESH_VERSION = 2; + class FBXNode; using FBXNodeList = QList; @@ -41,7 +40,7 @@ using FBXNodeList = QList; /// A node within an FBX document. class FBXNode { public: - QByteArray name; + hifi::ByteArray name; QVariantList properties; FBXNodeList children; }; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 5246242a1e..aed313f54e 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -178,7 +178,7 @@ public: void printNode(const FBXNode& node, int indentLevel) { int indentLength = 2; - QByteArray spaces(indentLevel * indentLength, ' '); + hifi::ByteArray spaces(indentLevel * indentLength, ' '); QDebug nodeDebug = qDebug(modelformat); nodeDebug.nospace() << spaces.data() << node.name.data() << ": "; @@ -308,7 +308,7 @@ public: }; bool checkMaterialsHaveTextures(const QHash& materials, - const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { + const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { if (textureFilenames.contains(childID)) { @@ -375,7 +375,7 @@ HFMLight extractLight(const FBXNode& object) { return light; } -QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { +hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { // in order to match the behaviour when loading models from remote URLs // we assume that all external textures are right beside the loaded model // ignoring any relative paths or absolute paths inside of models @@ -383,8 +383,10 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QString& url) { +HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; + bool deduplicateIndices = mapping["deduplicateIndices"].toBool(); + QMap meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; @@ -406,11 +408,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr std::map lights; - QVariantHash blendshapeMappings = mapping.value("bs").toHash(); + hifi::VariantHash blendshapeMappings = mapping.value("bs").toHash(); - QMultiHash blendshapeIndices; + QMultiHash blendshapeIndices; for (int i = 0;; i++) { - QByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; + hifi::ByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; if (blendshapeName.isEmpty()) { break; } @@ -441,6 +443,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; + int fbxVersionNumber = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -455,7 +458,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (subobject.name == "Properties70") { foreach (const FBXNode& subsubobject, subobject.children) { - static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName")); + static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { hfmModel.applicationName = subsubobject.properties.at(4).toString(); @@ -463,6 +466,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } + } else if (object.name == "FBXVersion") { + fbxVersionNumber = object.properties.at(0).toInt(); } } } else if (child.name == "GlobalSettings") { @@ -472,9 +477,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr int index = 4; foreach (const FBXNode& subobject, object.children) { if (subobject.name == propertyName) { - static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor"); - static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor"); - static const QVariant UP_AXIS = QByteArray("UpAxis"); + static const QVariant UNIT_SCALE_FACTOR = hifi::ByteArray("UnitScaleFactor"); + static const QVariant AMBIENT_COLOR = hifi::ByteArray("AmbientColor"); + static const QVariant UP_AXIS = hifi::ByteArray("UpAxis"); const auto& subpropName = subobject.properties.at(0); if (subpropName == UNIT_SCALE_FACTOR) { unitScaleFactor = subobject.properties.at(index).toFloat(); @@ -499,7 +504,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr foreach (const FBXNode& object, child.children) { if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { - meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); + meshes.insert(getID(object.properties), extractMesh(object, meshIndex, deduplicateIndices)); } else { // object.properties.at(2) == "Shape" ExtractedBlendshape extracted = { getID(object.properties), extractBlendshape(object) }; blendshapes.append(extracted); @@ -540,7 +545,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QVector blendshapes; foreach (const FBXNode& subobject, object.children) { bool properties = false; - QByteArray propertyName; + hifi::ByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; @@ -553,27 +558,27 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr index = 4; } if (properties) { - static const QVariant ROTATION_ORDER = QByteArray("RotationOrder"); - static const QVariant GEOMETRIC_TRANSLATION = QByteArray("GeometricTranslation"); - static const QVariant GEOMETRIC_ROTATION = QByteArray("GeometricRotation"); - static const QVariant GEOMETRIC_SCALING = QByteArray("GeometricScaling"); - static const QVariant LCL_TRANSLATION = QByteArray("Lcl Translation"); - static const QVariant LCL_ROTATION = QByteArray("Lcl Rotation"); - static const QVariant LCL_SCALING = QByteArray("Lcl Scaling"); - static const QVariant ROTATION_MAX = QByteArray("RotationMax"); - static const QVariant ROTATION_MAX_X = QByteArray("RotationMaxX"); - static const QVariant ROTATION_MAX_Y = QByteArray("RotationMaxY"); - static const QVariant ROTATION_MAX_Z = QByteArray("RotationMaxZ"); - static const QVariant ROTATION_MIN = QByteArray("RotationMin"); - static const QVariant ROTATION_MIN_X = QByteArray("RotationMinX"); - static const QVariant ROTATION_MIN_Y = QByteArray("RotationMinY"); - static const QVariant ROTATION_MIN_Z = QByteArray("RotationMinZ"); - static const QVariant ROTATION_OFFSET = QByteArray("RotationOffset"); - static const QVariant ROTATION_PIVOT = QByteArray("RotationPivot"); - static const QVariant SCALING_OFFSET = QByteArray("ScalingOffset"); - static const QVariant SCALING_PIVOT = QByteArray("ScalingPivot"); - static const QVariant PRE_ROTATION = QByteArray("PreRotation"); - static const QVariant POST_ROTATION = QByteArray("PostRotation"); + static const QVariant ROTATION_ORDER = hifi::ByteArray("RotationOrder"); + static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); + static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation"); + static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling"); + static const QVariant LCL_TRANSLATION = hifi::ByteArray("Lcl Translation"); + static const QVariant LCL_ROTATION = hifi::ByteArray("Lcl Rotation"); + static const QVariant LCL_SCALING = hifi::ByteArray("Lcl Scaling"); + static const QVariant ROTATION_MAX = hifi::ByteArray("RotationMax"); + static const QVariant ROTATION_MAX_X = hifi::ByteArray("RotationMaxX"); + static const QVariant ROTATION_MAX_Y = hifi::ByteArray("RotationMaxY"); + static const QVariant ROTATION_MAX_Z = hifi::ByteArray("RotationMaxZ"); + static const QVariant ROTATION_MIN = hifi::ByteArray("RotationMin"); + static const QVariant ROTATION_MIN_X = hifi::ByteArray("RotationMinX"); + static const QVariant ROTATION_MIN_Y = hifi::ByteArray("RotationMinY"); + static const QVariant ROTATION_MIN_Z = hifi::ByteArray("RotationMinZ"); + static const QVariant ROTATION_OFFSET = hifi::ByteArray("RotationOffset"); + static const QVariant ROTATION_PIVOT = hifi::ByteArray("RotationPivot"); + static const QVariant SCALING_OFFSET = hifi::ByteArray("ScalingOffset"); + static const QVariant SCALING_PIVOT = hifi::ByteArray("ScalingPivot"); + static const QVariant PRE_ROTATION = hifi::ByteArray("PreRotation"); + static const QVariant POST_ROTATION = hifi::ByteArray("PostRotation"); foreach(const FBXNode& property, subobject.children) { const auto& childProperty = property.properties.at(0); if (property.name == propertyName) { @@ -643,10 +648,10 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } - } else if (subobject.name == "Vertices") { + } else if (subobject.name == "Vertices" || subobject.name == "DracoMesh") { // it's a mesh as well as a model mesh = &meshes[getID(object.properties)]; - *mesh = extractMesh(object, meshIndex); + *mesh = extractMesh(object, meshIndex, deduplicateIndices); } else if (subobject.name == "Shape") { ExtractedBlendshape blendshape = { subobject.properties.at(0).toString(), @@ -713,8 +718,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr const int MODEL_UV_SCALING_MIN_SIZE = 2; const int CROPPING_MIN_SIZE = 4; if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) { - QByteArray filename = subobject.properties.at(0).toByteArray(); - QByteArray filepath = filename.replace('\\', '/'); + hifi::ByteArray filename = subobject.properties.at(0).toByteArray(); + hifi::ByteArray filepath = filename.replace('\\', '/'); filename = fileOnUrl(filepath, url); _textureFilepaths.insert(getID(object.properties), filepath); _textureFilenames.insert(getID(object.properties), filename); @@ -743,17 +748,17 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr subobject.properties.at(2).value(), subobject.properties.at(3).value())); } else if (subobject.name == "Properties70") { - QByteArray propertyName; + hifi::ByteArray propertyName; int index; propertyName = "P"; index = 4; foreach (const FBXNode& property, subobject.children) { - static const QVariant UV_SET = QByteArray("UVSet"); - static const QVariant CURRENT_TEXTURE_BLEND_MODE = QByteArray("CurrentTextureBlendMode"); - static const QVariant USE_MATERIAL = QByteArray("UseMaterial"); - static const QVariant TRANSLATION = QByteArray("Translation"); - static const QVariant ROTATION = QByteArray("Rotation"); - static const QVariant SCALING = QByteArray("Scaling"); + static const QVariant UV_SET = hifi::ByteArray("UVSet"); + static const QVariant CURRENT_TEXTURE_BLEND_MODE = hifi::ByteArray("CurrentTextureBlendMode"); + static const QVariant USE_MATERIAL = hifi::ByteArray("UseMaterial"); + static const QVariant TRANSLATION = hifi::ByteArray("Translation"); + static const QVariant ROTATION = hifi::ByteArray("Rotation"); + static const QVariant SCALING = hifi::ByteArray("Scaling"); if (property.name == propertyName) { QString v = property.properties.at(0).toString(); if (property.properties.at(0) == UV_SET) { @@ -807,8 +812,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr _textureParams.insert(getID(object.properties), tex); } } else if (object.name == "Video") { - QByteArray filepath; - QByteArray content; + hifi::ByteArray filepath; + hifi::ByteArray content; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { filepath = subobject.properties.at(0).toByteArray(); @@ -828,7 +833,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr foreach (const FBXNode& subobject, object.children) { bool properties = false; - QByteArray propertyName; + hifi::ByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; @@ -845,31 +850,31 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (properties) { std::vector unknowns; - static const QVariant DIFFUSE_COLOR = QByteArray("DiffuseColor"); - static const QVariant DIFFUSE_FACTOR = QByteArray("DiffuseFactor"); - static const QVariant DIFFUSE = QByteArray("Diffuse"); - static const QVariant SPECULAR_COLOR = QByteArray("SpecularColor"); - static const QVariant SPECULAR_FACTOR = QByteArray("SpecularFactor"); - static const QVariant SPECULAR = QByteArray("Specular"); - static const QVariant EMISSIVE_COLOR = QByteArray("EmissiveColor"); - static const QVariant EMISSIVE_FACTOR = QByteArray("EmissiveFactor"); - static const QVariant EMISSIVE = QByteArray("Emissive"); - static const QVariant AMBIENT_FACTOR = QByteArray("AmbientFactor"); - static const QVariant SHININESS = QByteArray("Shininess"); - static const QVariant OPACITY = QByteArray("Opacity"); - static const QVariant MAYA_USE_NORMAL_MAP = QByteArray("Maya|use_normal_map"); - static const QVariant MAYA_BASE_COLOR = QByteArray("Maya|base_color"); - static const QVariant MAYA_USE_COLOR_MAP = QByteArray("Maya|use_color_map"); - static const QVariant MAYA_ROUGHNESS = QByteArray("Maya|roughness"); - static const QVariant MAYA_USE_ROUGHNESS_MAP = QByteArray("Maya|use_roughness_map"); - static const QVariant MAYA_METALLIC = QByteArray("Maya|metallic"); - static const QVariant MAYA_USE_METALLIC_MAP = QByteArray("Maya|use_metallic_map"); - static const QVariant MAYA_EMISSIVE = QByteArray("Maya|emissive"); - static const QVariant MAYA_EMISSIVE_INTENSITY = QByteArray("Maya|emissive_intensity"); - static const QVariant MAYA_USE_EMISSIVE_MAP = QByteArray("Maya|use_emissive_map"); - static const QVariant MAYA_USE_AO_MAP = QByteArray("Maya|use_ao_map"); - static const QVariant MAYA_UV_SCALE = QByteArray("Maya|uv_scale"); - static const QVariant MAYA_UV_OFFSET = QByteArray("Maya|uv_offset"); + static const QVariant DIFFUSE_COLOR = hifi::ByteArray("DiffuseColor"); + static const QVariant DIFFUSE_FACTOR = hifi::ByteArray("DiffuseFactor"); + static const QVariant DIFFUSE = hifi::ByteArray("Diffuse"); + static const QVariant SPECULAR_COLOR = hifi::ByteArray("SpecularColor"); + static const QVariant SPECULAR_FACTOR = hifi::ByteArray("SpecularFactor"); + static const QVariant SPECULAR = hifi::ByteArray("Specular"); + static const QVariant EMISSIVE_COLOR = hifi::ByteArray("EmissiveColor"); + static const QVariant EMISSIVE_FACTOR = hifi::ByteArray("EmissiveFactor"); + static const QVariant EMISSIVE = hifi::ByteArray("Emissive"); + static const QVariant AMBIENT_FACTOR = hifi::ByteArray("AmbientFactor"); + static const QVariant SHININESS = hifi::ByteArray("Shininess"); + static const QVariant OPACITY = hifi::ByteArray("Opacity"); + static const QVariant MAYA_USE_NORMAL_MAP = hifi::ByteArray("Maya|use_normal_map"); + static const QVariant MAYA_BASE_COLOR = hifi::ByteArray("Maya|base_color"); + static const QVariant MAYA_USE_COLOR_MAP = hifi::ByteArray("Maya|use_color_map"); + static const QVariant MAYA_ROUGHNESS = hifi::ByteArray("Maya|roughness"); + static const QVariant MAYA_USE_ROUGHNESS_MAP = hifi::ByteArray("Maya|use_roughness_map"); + static const QVariant MAYA_METALLIC = hifi::ByteArray("Maya|metallic"); + static const QVariant MAYA_USE_METALLIC_MAP = hifi::ByteArray("Maya|use_metallic_map"); + static const QVariant MAYA_EMISSIVE = hifi::ByteArray("Maya|emissive"); + static const QVariant MAYA_EMISSIVE_INTENSITY = hifi::ByteArray("Maya|emissive_intensity"); + static const QVariant MAYA_USE_EMISSIVE_MAP = hifi::ByteArray("Maya|use_emissive_map"); + static const QVariant MAYA_USE_AO_MAP = hifi::ByteArray("Maya|use_ao_map"); + static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale"); + static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset"); static const int MAYA_UV_OFFSET_PROPERTY_LENGTH = 6; static const int MAYA_UV_SCALE_PROPERTY_LENGTH = 6; @@ -1043,10 +1048,14 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr cluster.transformLink = createMat4(values); } } - clusters.insert(getID(object.properties), cluster); + + // skip empty clusters + if (cluster.indices.size() > 0 && cluster.weights.size() > 0) { + clusters.insert(getID(object.properties), cluster); + } } else if (object.properties.last() == "BlendShapeChannel") { - QByteArray name = object.properties.at(1).toByteArray(); + hifi::ByteArray name = object.properties.at(1).toByteArray(); name = name.left(name.indexOf('\0')); if (!blendshapeIndices.contains(name)) { @@ -1083,8 +1092,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } } else if (child.name == "Connections") { - static const QVariant OO = QByteArray("OO"); - static const QVariant OP = QByteArray("OP"); + static const QVariant OO = hifi::ByteArray("OO"); + static const QVariant OP = hifi::ByteArray("OP"); foreach (const FBXNode& connection, child.children) { if (connection.name == "C" || connection.name == "Connect") { if (connection.properties.at(0) == OO) { @@ -1103,7 +1112,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (connection.properties.at(0) == OP) { int counter = 0; - QByteArray type = connection.properties.at(3).toByteArray().toLower(); + hifi::ByteArray type = connection.properties.at(3).toByteArray().toLower(); if (type.contains("DiffuseFactor")) { diffuseFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if ((type.contains("diffuse") && !type.contains("tex_global_diffuse"))) { @@ -1155,8 +1164,14 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr counter++; } } - _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); - _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") { + // don't assign the new parent + qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + } else { + _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + } } } } @@ -1305,8 +1320,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.bindTransformFoundInCluster = false; - hfmModel.joints.append(joint); - QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); AnimationCurve yRotCurve = animationCurves.value(yComponents.value(rotationID)); @@ -1329,7 +1342,13 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()), yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()), zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size())); + if ((fbxVersionNumber < 7500) && (i == 0)) { + joint.translation = hfmModel.animationFrames[i].translations[jointIndex]; + joint.rotation = hfmModel.animationFrames[i].rotations[jointIndex]; + } + } + hfmModel.joints.append(joint); } // NOTE: shapeVertices are in joint-frame @@ -1400,9 +1419,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr // look for textures, material properties // allocate the Part material library + // NOTE: extracted.partMaterialTextures is empty for FBX_DRACO_MESH_VERSION >= 2. In that case, the mesh part's materialID string is already defined. int materialIndex = 0; int textureIndex = 0; - bool generateTangents = false; QList children = _connectionChildMap.values(modelID); for (int i = children.size() - 1; i >= 0; i--) { @@ -1415,12 +1434,10 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (extracted.partMaterialTextures.at(j).first == materialIndex) { HFMMeshPart& part = extracted.mesh.parts[j]; part.materialID = material.materialID; - generateTangents |= material.needTangentSpace(); } } materialIndex++; - } else if (_textureFilenames.contains(childID)) { // NOTE (Sabrina 2019/01/11): getTextures now takes in the materialID as a second parameter, because FBX material nodes can sometimes have uv transform information (ex: "Maya|uv_scale") // I'm leaving the second parameter blank right now as this code may never be used. @@ -1482,8 +1499,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } - // if we don't have a skinned joint, parent to the model itself - if (extracted.mesh.clusters.isEmpty()) { + // the last cluster is the root cluster + { HFMCluster cluster; cluster.jointIndex = modelIDs.indexOf(modelID); if (cluster.jointIndex == -1) { @@ -1494,13 +1511,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } // whether we're skinned depends on how many clusters are attached - const HFMCluster& firstHFMCluster = extracted.mesh.clusters.at(0); - glm::mat4 inverseModelTransform = glm::inverse(modelTransform); if (clusterIDs.size() > 1) { // this is a multi-mesh joint const int WEIGHTS_PER_VERTEX = 4; int numClusterIndices = extracted.mesh.vertices.size() * WEIGHTS_PER_VERTEX; - extracted.mesh.clusterIndices.fill(0, numClusterIndices); + extracted.mesh.clusterIndices.fill(extracted.mesh.clusters.size() - 1, numClusterIndices); QVector weightAccumulators; weightAccumulators.fill(0.0f, numClusterIndices); @@ -1510,19 +1525,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr const HFMCluster& hfmCluster = extracted.mesh.clusters.at(i); int jointIndex = hfmCluster.jointIndex; HFMJoint& joint = hfmModel.joints[jointIndex]; - glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; - glm::vec3 boneEnd = extractTranslation(transformJointToMesh); - glm::vec3 boneBegin = boneEnd; - glm::vec3 boneDirection; - float boneLength = 0.0f; - if (joint.parentIndex != -1) { - boneBegin = extractTranslation(inverseModelTransform * hfmModel.joints[joint.parentIndex].bindTransform); - boneDirection = boneEnd - boneBegin; - boneLength = glm::length(boneDirection); - if (boneLength > EPSILON) { - boneDirection /= boneLength; - } - } glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; ShapeVertices& points = shapeVertices.at(jointIndex); @@ -1535,6 +1537,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr int newIndex = it.value(); // remember vertices with at least 1/4 weight + // FIXME: vertices with no weightpainting won't get recorded here const float EXPANSION_WEIGHT_THRESHOLD = 0.25f; if (weight >= EXPANSION_WEIGHT_THRESHOLD) { // transform to joint-frame and save for later @@ -1575,20 +1578,24 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr int j = i * WEIGHTS_PER_VERTEX; // normalize weights into uint16_t - float totalWeight = weightAccumulators[j]; - for (int k = j + 1; k < j + WEIGHTS_PER_VERTEX; ++k) { + float totalWeight = 0.0f; + for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { totalWeight += weightAccumulators[k]; } + + const float ALMOST_HALF = 0.499f; if (totalWeight > 0.0f) { - const float ALMOST_HALF = 0.499f; float weightScalingFactor = (float)(UINT16_MAX) / totalWeight; for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { extracted.mesh.clusterWeights[k] = (uint16_t)(weightScalingFactor * weightAccumulators[k] + ALMOST_HALF); } + } else { + extracted.mesh.clusterWeights[j] = (uint16_t)((float)(UINT16_MAX) + ALMOST_HALF); } } } else { - // this is a single-mesh joint + // this is a single-joint mesh + const HFMCluster& firstHFMCluster = extracted.mesh.clusters.at(0); int jointIndex = firstHFMCluster.jointIndex; HFMJoint& joint = hfmModel.joints[jointIndex]; @@ -1700,11 +1707,13 @@ std::unique_ptr FBXSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer FBXSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { - QBuffer buffer(const_cast(&data)); +HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { + QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); _rootNode = parseFBX(&buffer); + // FBXSerializer's mapping parameter supports the bool "deduplicateIndices," which is passed into FBXSerializer::extractMesh as "deduplicate" + return HFMModel::Pointer(extractHFMModel(mapping, url.toString())); } diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index 379b1ac743..7d41f98444 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -15,9 +15,6 @@ #include #include #include -#include -#include -#include #include #include @@ -25,6 +22,7 @@ #include #include +#include #include "FBX.h" #include @@ -114,25 +112,25 @@ public: HFMModel* _hfmModel; /// Reads HFMModel from the supplied model and mapping data. /// \exception QString if an error occurs in parsing - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); - HFMModel* extractHFMModel(const QVariantHash& mapping, const QString& url); + HFMModel* extractHFMModel(const hifi::VariantHash& mapping, const QString& url); - static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); + static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate); QHash meshes; HFMTexture getTexture(const QString& textureID, const QString& materialID); QHash _textureNames; // Hashes the original RelativeFilename of textures - QHash _textureFilepaths; + QHash _textureFilepaths; // Hashes the place to look for textures, in case they are not inlined - QHash _textureFilenames; + QHash _textureFilenames; // Hashes texture content by filepath, in case they are inlined - QHash _textureContent; + QHash _textureContent; QHash _textureParams; diff --git a/libraries/fbx/src/FBXSerializer_Material.cpp b/libraries/fbx/src/FBXSerializer_Material.cpp index b47329e483..8b170eba1b 100644 --- a/libraries/fbx/src/FBXSerializer_Material.cpp +++ b/libraries/fbx/src/FBXSerializer_Material.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include #include @@ -29,7 +28,7 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& materialID) { HFMTexture texture; - const QByteArray& filepath = _textureFilepaths.value(textureID); + const hifi::ByteArray& filepath = _textureFilepaths.value(textureID); texture.content = _textureContent.value(filepath); if (texture.content.isEmpty()) { // the content is not inlined diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index fd1f80425b..c34b4678c7 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -13,12 +13,20 @@ #pragma warning( push ) #pragma warning( disable : 4267 ) #endif +// gcc and clang +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#endif #include #ifdef _WIN32 #pragma warning( pop ) #endif +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif #include #include @@ -190,8 +198,8 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me bool isMaterialPerPolygon = false; - static const QVariant BY_VERTICE = QByteArray("ByVertice"); - static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect"); + static const QVariant BY_VERTICE = hifi::ByteArray("ByVertice"); + static const QVariant INDEX_TO_DIRECT = hifi::ByteArray("IndexToDirect"); bool isDracoMesh = false; @@ -321,7 +329,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me } } } else if (child.name == "LayerElementMaterial") { - static const QVariant BY_POLYGON = QByteArray("ByPolygon"); + static const QVariant BY_POLYGON = hifi::ByteArray("ByPolygon"); foreach (const FBXNode& subdata, child.children) { if (subdata.name == "Materials") { materials = getIntVector(subdata); @@ -345,10 +353,26 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me isDracoMesh = true; data.extracted.mesh.wasCompressed = true; + // Check for additional metadata + unsigned int dracoMeshNodeVersion = 1; + std::vector dracoMaterialList; + for (const auto& dracoChild : child.children) { + if (dracoChild.name == "FBXDracoMeshVersion") { + if (!dracoChild.children.isEmpty()) { + dracoMeshNodeVersion = dracoChild.properties[0].toUInt(); + } + } else if (dracoChild.name == "MaterialList") { + dracoMaterialList.reserve(dracoChild.properties.size()); + for (const auto& materialID : dracoChild.properties) { + dracoMaterialList.push_back(materialID.toString()); + } + } + } + // load the draco mesh from the FBX and create a draco::Mesh draco::Decoder decoder; draco::DecoderBuffer decodedBuffer; - QByteArray dracoArray = child.properties.at(0).value(); + hifi::ByteArray dracoArray = child.properties.at(0).value(); decodedBuffer.Init(dracoArray.data(), dracoArray.size()); std::unique_ptr dracoMesh(new draco::Mesh()); @@ -462,8 +486,20 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me // grab or setup the HFMMeshPart for the part this face belongs to int& partIndexPlusOne = materialTextureParts[materialTexture]; if (partIndexPlusOne == 0) { - data.extracted.partMaterialTextures.append(materialTexture); data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); + HFMMeshPart& part = data.extracted.mesh.parts.back(); + + // Figure out what material this part is + if (dracoMeshNodeVersion >= 2) { + // Define the materialID now + if (dracoMaterialList.size() - 1 <= materialID) { + part.materialID = dracoMaterialList[materialID]; + } + } else { + // Define the materialID later, based on the order of first appearance of the materials in the _connectionChildMap + data.extracted.partMaterialTextures.append(materialTexture); + } + partIndexPlusOne = data.extracted.mesh.parts.size(); } diff --git a/libraries/fbx/src/FBXSerializer_Node.cpp b/libraries/fbx/src/FBXSerializer_Node.cpp index c982dfc7cb..f9ef84c6f2 100644 --- a/libraries/fbx/src/FBXSerializer_Node.cpp +++ b/libraries/fbx/src/FBXSerializer_Node.cpp @@ -48,10 +48,10 @@ QVariant readBinaryArray(QDataStream& in, int& position) { QVector values; if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) { values.resize(arrayLength); - QByteArray arrayData; + hifi::ByteArray arrayData; if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length - QByteArray compressed(sizeof(quint32) + compressedLength, 0); + hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; @@ -73,11 +73,11 @@ QVariant readBinaryArray(QDataStream& in, int& position) { values.reserve(arrayLength); if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length - QByteArray compressed(sizeof(quint32) + compressedLength, 0); + hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; - QByteArray uncompressed = qUncompress(compressed); + hifi::ByteArray uncompressed = qUncompress(compressed); if (uncompressed.isEmpty()) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } @@ -234,7 +234,7 @@ public: }; int nextToken(); - const QByteArray& getDatum() const { return _datum; } + const hifi::ByteArray& getDatum() const { return _datum; } void pushBackToken(int token) { _pushedBackToken = token; } void ungetChar(char ch) { _device->ungetChar(ch); } @@ -242,7 +242,7 @@ public: private: QIODevice* _device; - QByteArray _datum; + hifi::ByteArray _datum; int _pushedBackToken; }; @@ -325,7 +325,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) { expectingDatum = true; } else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) { - QByteArray datum = tokenizer.getDatum(); + hifi::ByteArray datum = tokenizer.getDatum(); if ((token = tokenizer.nextToken()) == ':') { tokenizer.ungetChar(':'); tokenizer.pushBackToken(Tokenizer::DATUM_TOKEN); diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index ad952c4ed7..fade0fa5bc 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -15,6 +15,8 @@ #include #include +static const unsigned int FST_VERSION = 1; +static const QString FST_VERSION_FIELD = "version"; static const QString NAME_FIELD = "name"; static const QString TYPE_FIELD = "type"; static const QString FILENAME_FIELD = "filename"; diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 940ba69bdd..1699722215 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -125,18 +125,18 @@ bool GLTFSerializer::getObjectArrayVal(const QJsonObject& object, const QString& return _defined; } -QByteArray GLTFSerializer::setGLBChunks(const QByteArray& data) { +hifi::ByteArray GLTFSerializer::setGLBChunks(const hifi::ByteArray& data) { int byte = 4; int jsonStart = data.indexOf("JSON", Qt::CaseSensitive); int binStart = data.indexOf("BIN", Qt::CaseSensitive); int jsonLength, binLength; - QByteArray jsonLengthChunk, binLengthChunk; + hifi::ByteArray jsonLengthChunk, binLengthChunk; jsonLengthChunk = data.mid(jsonStart - byte, byte); QDataStream tempJsonLen(jsonLengthChunk); tempJsonLen.setByteOrder(QDataStream::LittleEndian); tempJsonLen >> jsonLength; - QByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength); + hifi::ByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength); if (binStart != -1) { binLengthChunk = data.mid(binStart - byte, byte); @@ -567,10 +567,10 @@ bool GLTFSerializer::addTexture(const QJsonObject& object) { return true; } -bool GLTFSerializer::parseGLTF(const QByteArray& data) { +bool GLTFSerializer::parseGLTF(const hifi::ByteArray& data) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - QByteArray jsonChunk = data; + hifi::ByteArray jsonChunk = data; if (_url.toString().endsWith("glb") && data.indexOf("glTF") == 0 && data.contains("JSON")) { jsonChunk = setGLBChunks(data); @@ -711,19 +711,19 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { node.matrix[12], node.matrix[13], node.matrix[14], node.matrix[15]); } else { - if (node.defined["rotation"] && node.rotation.size() == 4) { - //quat(x,y,z,w) to quat(w,x,y,z) - glm::quat rotquat = glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]); - tmat = glm::mat4_cast(rotquat) * tmat; - } - if (node.defined["scale"] && node.scale.size() == 3) { glm::vec3 scale = glm::vec3(node.scale[0], node.scale[1], node.scale[2]); glm::mat4 s = glm::mat4(1.0); s = glm::scale(s, scale); tmat = s * tmat; } - + + if (node.defined["rotation"] && node.rotation.size() == 4) { + //quat(x,y,z,w) to quat(w,x,y,z) + glm::quat rotquat = glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]); + tmat = glm::mat4_cast(rotquat) * tmat; + } + if (node.defined["translation"] && node.translation.size() == 3) { glm::vec3 trans = glm::vec3(node.translation[0], node.translation[1], node.translation[2]); glm::mat4 t = glm::mat4(1.0); @@ -734,15 +734,54 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { return tmat; } -bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { +void GLTFSerializer::getSkinInverseBindMatrices(std::vector>& inverseBindMatrixValues) { + for (auto &skin : _file.skins) { + GLTFAccessor& indicesAccessor = _file.accessors[skin.inverseBindMatrices]; + GLTFBufferView& indicesBufferview = _file.bufferviews[indicesAccessor.bufferView]; + GLTFBuffer& indicesBuffer = _file.buffers[indicesBufferview.buffer]; + int accBoffset = indicesAccessor.defined["byteOffset"] ? indicesAccessor.byteOffset : 0; + QVector matrices; + addArrayOfType(indicesBuffer.blob, + indicesBufferview.byteOffset + accBoffset, + indicesAccessor.count, + matrices, + indicesAccessor.type, + indicesAccessor.componentType); + inverseBindMatrixValues.push_back(matrices.toStdVector()); + } +} + +void GLTFSerializer::getNodeQueueByDepthFirstChildren(std::vector& children, int stride, std::vector& result) { + int startingIndex = 0; + int finalIndex = (int)children.size(); + if (stride == -1) { + startingIndex = (int)children.size() - 1; + finalIndex = -1; + } + for (int index = startingIndex; index != finalIndex; index += stride) { + int c = children[index]; + result.push_back(c); + std::vector nested = _file.nodes[c].children.toStdVector(); + if (nested.size() != 0) { + std::sort(nested.begin(), nested.end()); + for (int r : nested) { + if (result.end() == std::find(result.begin(), result.end(), r)) { + getNodeQueueByDepthFirstChildren(nested, stride, result); + } + } + } + } +} + + +bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { + int numNodes = _file.nodes.size(); //Build dependencies - QVector> nodeDependencies(_file.nodes.size()); + QVector> nodeDependencies(numNodes); int nodecount = 0; - bool hasChildren = false; foreach(auto &node, _file.nodes) { //nodes_transforms.push_back(getModelTransform(node)); - hasChildren |= !node.children.isEmpty(); foreach(int child, node.children) nodeDependencies[child].push_back(nodecount); nodecount++; } @@ -764,26 +803,98 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { nodecount++; } - - HFMJoint joint; - joint.isSkeletonJoint = true; - joint.bindTransformFoundInCluster = false; - joint.distanceToParent = 0; - joint.parentIndex = -1; - hfmModel.joints.resize(_file.nodes.size()); - hfmModel.jointIndices["x"] = _file.nodes.size(); - int jointInd = 0; - for (auto& node : _file.nodes) { - int size = node.transforms.size(); - if (hasChildren) { size--; } - joint.preTransform = glm::mat4(1); - for (int i = 0; i < size; i++) { - joint.preTransform = node.transforms[i] * joint.preTransform; - } - joint.name = node.name; - hfmModel.joints[jointInd] = joint; - jointInd++; + + + // initialize order in which nodes will be parsed + std::vector nodeQueue; + nodeQueue.reserve(numNodes); + int rootNode = 0; + int finalNode = numNodes; + if (!_file.scenes[_file.scene].nodes.contains(0)) { + rootNode = numNodes - 1; + finalNode = -1; } + bool rootAtStartOfList = rootNode < finalNode; + int nodeListStride = 1; + if (!rootAtStartOfList) { nodeListStride = -1; } + + QVector initialSceneNodes = _file.scenes[_file.scene].nodes; + std::sort(initialSceneNodes.begin(), initialSceneNodes.end()); + int sceneRootNode = 0; + int sceneFinalNode = initialSceneNodes.size(); + if (!rootAtStartOfList) { + sceneRootNode = initialSceneNodes.size() - 1; + sceneFinalNode = -1; + } + for (int index = sceneRootNode; index != sceneFinalNode; index += nodeListStride) { + int i = initialSceneNodes[index]; + nodeQueue.push_back(i); + std::vector children = _file.nodes[i].children.toStdVector(); + std::sort(children.begin(), children.end()); + getNodeQueueByDepthFirstChildren(children, nodeListStride, nodeQueue); + } + + // Build joints + HFMJoint joint; + joint.distanceToParent = 0; + hfmModel.jointIndices["x"] = numNodes; + hfmModel.hasSkeletonJoints = false; + + for (int nodeIndex : nodeQueue) { + auto& node = _file.nodes[nodeIndex]; + + joint.parentIndex = -1; + if (!_file.scenes[_file.scene].nodes.contains(nodeIndex)) { + joint.parentIndex = std::distance(nodeQueue.begin(), std::find(nodeQueue.begin(), nodeQueue.end(), nodeDependencies[nodeIndex][0])); + } + joint.transform = node.transforms.first(); + joint.translation = extractTranslation(joint.transform); + joint.rotation = glmExtractRotation(joint.transform); + glm::vec3 scale = extractScale(joint.transform); + joint.postTransform = glm::scale(glm::mat4(), scale); + + joint.name = node.name; + joint.isSkeletonJoint = false; + hfmModel.joints.push_back(joint); + } + + + // Build skeleton + std::vector jointInverseBindTransforms; + jointInverseBindTransforms.resize(numNodes); + if (!_file.skins.isEmpty()) { + std::vector> inverseBindValues; + getSkinInverseBindMatrices(inverseBindValues); + + int jointIndex = finalNode; + while (jointIndex != rootNode) { + rootAtStartOfList ? jointIndex-- : jointIndex++; + int jOffset = nodeQueue[jointIndex]; + auto joint = hfmModel.joints[jointIndex]; + + hfmModel.hasSkeletonJoints = true; + for (int s = 0; s < _file.skins.size(); s++) { + auto skin = _file.skins[s]; + joint.isSkeletonJoint = skin.joints.contains(jOffset); + + if (joint.isSkeletonJoint) { + std::vector value = inverseBindValues[s]; + int matrixCount = 16 * skin.joints.indexOf(jOffset); + jointInverseBindTransforms[jointIndex] = + glm::mat4(value[matrixCount], value[matrixCount + 1], value[matrixCount + 2], value[matrixCount + 3], + value[matrixCount + 4], value[matrixCount + 5], value[matrixCount + 6], value[matrixCount + 7], + value[matrixCount + 8], value[matrixCount + 9], value[matrixCount + 10], value[matrixCount + 11], + value[matrixCount + 12], value[matrixCount + 13], value[matrixCount + 14], value[matrixCount + 15]); + } else { + jointInverseBindTransforms[jointIndex] = glm::mat4(); + } + glm::vec3 bindTranslation = extractTranslation(hfmModel.offset * glm::inverse(jointInverseBindTransforms[jointIndex])); + hfmModel.bindExtents.addPoint(bindTranslation); + } + hfmModel.joints[jointIndex] = joint; + } + } + //Build materials QVector materialIDs; @@ -803,23 +914,39 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { } - - nodecount = 0; // Build meshes - foreach(auto &node, _file.nodes) { + nodecount = 0; + for (int nodeIndex = rootNode; nodeIndex != finalNode; nodeIndex += nodeListStride) { + auto& node = _file.nodes[nodeIndex]; if (node.defined["mesh"]) { qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { hfmModel.meshes.append(HFMMesh()); HFMMesh& mesh = hfmModel.meshes[hfmModel.meshes.size() - 1]; - HFMCluster cluster; - cluster.jointIndex = nodecount; - cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1); - mesh.clusters.append(cluster); + if (!hfmModel.hasSkeletonJoints) { + HFMCluster cluster; + cluster.jointIndex = nodecount; + cluster.inverseBindMatrix = glm::mat4(); + cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix); + mesh.clusters.append(cluster); + } else { + for (int j = rootNode; j != finalNode; j += nodeListStride) { + HFMCluster cluster; + cluster.jointIndex = j; + cluster.inverseBindMatrix = jointInverseBindTransforms[j]; + cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix); + mesh.clusters.append(cluster); + } + } + HFMCluster root; + root.jointIndex = rootNode; + if (root.jointIndex == -1) { + root.jointIndex = 0; + } + root.inverseBindMatrix = jointInverseBindTransforms[root.jointIndex]; + root.inverseBindTransform = Transform(root.inverseBindMatrix); + mesh.clusters.append(root); HFMMeshPart part = HFMMeshPart(); @@ -848,6 +975,8 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { } QList keys = primitive.attributes.values.keys(); + QVector clusterJoints; + QVector clusterWeights; foreach(auto &key, keys) { int accessorIdx = primitive.attributes.values[key]; @@ -949,8 +1078,69 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { for (int n = 0; n < texcoords.size(); n = n + 2) { mesh.texCoords1.push_back(glm::vec2(texcoords[n], texcoords[n + 1])); } + } else if (key == "JOINTS_0") { + QVector joints; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + joints, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF JOINTS_0 data for model " << _url; + continue; + } + for (int n = 0; n < joints.size(); n++) { + clusterJoints.push_back(joints[n]); + } + } else if (key == "WEIGHTS_0") { + QVector weights; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + weights, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF WEIGHTS_0 data for model " << _url; + continue; + } + for (int n = 0; n < weights.size(); n++) { + clusterWeights.push_back(weights[n]); + } + } + } + + // adapted from FBXSerializer.cpp + if (hfmModel.hasSkeletonJoints) { + int numClusterIndices = clusterJoints.size(); + const int WEIGHTS_PER_VERTEX = 4; + const float ALMOST_HALF = 0.499f; + int numVertices = mesh.vertices.size(); + mesh.clusterIndices.fill(mesh.clusters.size() - 1, numClusterIndices); + mesh.clusterWeights.fill(0, numClusterIndices); + + for (int c = 0; c < clusterJoints.size(); c++) { + mesh.clusterIndices[c] = _file.skins[node.skin].joints[clusterJoints[c]]; } + // normalize and compress to 16-bits + for (int i = 0; i < numVertices; ++i) { + int j = i * WEIGHTS_PER_VERTEX; + + float totalWeight = 0.0f; + for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { + totalWeight += clusterWeights[k]; + } + if (totalWeight > 0.0f) { + float weightScalingFactor = (float)(UINT16_MAX) / totalWeight; + for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { + mesh.clusterWeights[k] = (uint16_t)(weightScalingFactor * clusterWeights[k] + ALMOST_HALF); + } + } else { + mesh.clusterWeights[j] = (uint16_t)((float)(UINT16_MAX) + ALMOST_HALF); + } + } } if (primitive.defined["material"]) { @@ -959,8 +1149,8 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { mesh.parts.push_back(part); // populate the texture coordinates if they don't exist - if (mesh.texCoords.size() == 0) { - for (int i = 0; i < part.triangleIndices.size(); i++) mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); + if (mesh.texCoords.size() == 0 && !hfmModel.hasSkeletonJoints) { + for (int i = 0; i < part.triangleIndices.size(); i++) { mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); } } mesh.meshExtents.reset(); foreach(const glm::vec3& vertex, mesh.vertices) { @@ -994,15 +1184,15 @@ std::unique_ptr GLTFSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { +HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { _url = url; // Normalize url for local files - QUrl normalizeUrl = DependencyManager::get()->normalizeURL(_url); + hifi::URL normalizeUrl = DependencyManager::get()->normalizeURL(_url); if (normalizeUrl.scheme().isEmpty() || (normalizeUrl.scheme() == "file")) { QString localFileName = PathUtils::expandToLocalDataAbsolutePath(normalizeUrl).toLocalFile(); - _url = QUrl(QFileInfo(localFileName).absoluteFilePath()); + _url = hifi::URL(QFileInfo(localFileName).absoluteFilePath()); } if (parseGLTF(data)) { @@ -1020,15 +1210,15 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas return nullptr; } -bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { +bool GLTFSerializer::readBinary(const QString& url, hifi::ByteArray& outdata) { bool success; if (url.contains("data:application/octet-stream;base64,")) { outdata = requestEmbeddedData(url); success = !outdata.isEmpty(); } else { - QUrl binaryUrl = _url.resolved(url); - std::tie(success, outdata) = requestData(binaryUrl); + hifi::URL binaryUrl = _url.resolved(url); + std::tie(success, outdata) = requestData(binaryUrl); } return success; @@ -1038,16 +1228,16 @@ bool GLTFSerializer::doesResourceExist(const QString& url) { if (_url.isEmpty()) { return false; } - QUrl candidateUrl = _url.resolved(url); + hifi::URL candidateUrl = _url.resolved(url); return DependencyManager::get()->resourceExists(candidateUrl); } -std::tuple GLTFSerializer::requestData(QUrl& url) { +std::tuple GLTFSerializer::requestData(hifi::URL& url) { auto request = DependencyManager::get()->createResourceRequest( nullptr, url, true, -1, "GLTFSerializer::requestData"); if (!request) { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } QEventLoop loop; @@ -1058,17 +1248,17 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { if (request->getResult() == ResourceRequest::Success) { return std::make_tuple(true, request->getData()); } else { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } } -QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { +hifi::ByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { QString binaryUrl = url.split(",")[1]; - return binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); + return binaryUrl.isEmpty() ? hifi::ByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); } -QNetworkReply* GLTFSerializer::request(QUrl& url, bool isTest) { +QNetworkReply* GLTFSerializer::request(hifi::URL& url, bool isTest) { if (!qApp) { return nullptr; } @@ -1099,8 +1289,8 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { if (texture.defined["source"]) { QString url = _file.images[texture.source].uri; - QString fname = QUrl(url).fileName(); - QUrl textureUrl = _url.resolved(url); + QString fname = hifi::URL(url).fileName(); + hifi::URL textureUrl = _url.resolved(url); qCDebug(modelformat) << "fname: " << fname; fbxtex.name = fname; fbxtex.filename = textureUrl.toEncoded(); @@ -1188,7 +1378,7 @@ void GLTFSerializer::setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& mat } template -bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count, +bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType) { QDataStream blobstream(bin); @@ -1245,7 +1435,7 @@ bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count, return true; } template -bool GLTFSerializer::addArrayOfType(const QByteArray& bin, int byteOffset, int count, +bool GLTFSerializer::addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType) { switch (componentType) { diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index a361e09fa6..d9c477bd99 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -214,7 +214,7 @@ struct GLTFBufferView { struct GLTFBuffer { int byteLength; //required QString uri; - QByteArray blob; + hifi::ByteArray blob; QMap defined; void dump() { if (defined["byteLength"]) { @@ -705,16 +705,18 @@ public: MediaType getMediaType() const override; std::unique_ptr getFactory() const override; - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; private: GLTFFile _file; - QUrl _url; - QByteArray _glbBinary; + hifi::URL _url; + hifi::ByteArray _glbBinary; glm::mat4 getModelTransform(const GLTFNode& node); + void getSkinInverseBindMatrices(std::vector>& inverseBindMatrixValues); + void getNodeQueueByDepthFirstChildren(std::vector& children, int stride, std::vector& result); - bool buildGeometry(HFMModel& hfmModel, const QUrl& url); - bool parseGLTF(const QByteArray& data); + bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); + bool parseGLTF(const hifi::ByteArray& data); bool getStringVal(const QJsonObject& object, const QString& fieldname, QString& value, QMap& defined); @@ -733,7 +735,7 @@ private: bool getObjectArrayVal(const QJsonObject& object, const QString& fieldname, QJsonArray& objects, QMap& defined); - QByteArray setGLBChunks(const QByteArray& data); + hifi::ByteArray setGLBChunks(const hifi::ByteArray& data); int getMaterialAlphaMode(const QString& type); int getAccessorType(const QString& type); @@ -760,24 +762,24 @@ private: bool addSkin(const QJsonObject& object); bool addTexture(const QJsonObject& object); - bool readBinary(const QString& url, QByteArray& outdata); + bool readBinary(const QString& url, hifi::ByteArray& outdata); template - bool readArray(const QByteArray& bin, int byteOffset, int count, + bool readArray(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType); template - bool addArrayOfType(const QByteArray& bin, int byteOffset, int count, + bool addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType); void retriangulate(const QVector& in_indices, const QVector& in_vertices, const QVector& in_normals, QVector& out_indices, QVector& out_vertices, QVector& out_normals); - std::tuple requestData(QUrl& url); - QByteArray requestEmbeddedData(const QString& url); + std::tuple requestData(hifi::URL& url); + hifi::ByteArray requestEmbeddedData(const QString& url); - QNetworkReply* request(QUrl& url, bool isTest); + QNetworkReply* request(hifi::URL& url, bool isTest); bool doesResourceExist(const QString& url); diff --git a/libraries/fbx/src/OBJSerializer.cpp b/libraries/fbx/src/OBJSerializer.cpp index 91d3fc7cc0..c2e9c08463 100644 --- a/libraries/fbx/src/OBJSerializer.cpp +++ b/libraries/fbx/src/OBJSerializer.cpp @@ -54,7 +54,7 @@ T& checked_at(QVector& vector, int i) { OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } -const QByteArray OBJTokenizer::getLineAsDatum() { +const hifi::ByteArray OBJTokenizer::getLineAsDatum() { return _device->readLine().trimmed(); } @@ -117,7 +117,7 @@ bool OBJTokenizer::isNextTokenFloat() { if (nextToken() != OBJTokenizer::DATUM_TOKEN) { return false; } - QByteArray token = getDatum(); + hifi::ByteArray token = getDatum(); pushBackToken(OBJTokenizer::DATUM_TOKEN); bool ok; token.toFloat(&ok); @@ -182,7 +182,7 @@ void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID) { // OBJFace // NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just // pairing it with the vertices vector for consistency. -bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { +bool OBJFace::add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { bool ok; int index = vertexIndex.toInt(&ok); if (!ok) { @@ -238,11 +238,11 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } } -bool OBJSerializer::isValidTexture(const QByteArray &filename) { +bool OBJSerializer::isValidTexture(const hifi::ByteArray &filename) { if (_url.isEmpty()) { return false; } - QUrl candidateUrl = _url.resolved(QUrl(filename)); + hifi::URL candidateUrl = _url.resolved(hifi::URL(filename)); return DependencyManager::get()->resourceExists(candidateUrl); } @@ -278,7 +278,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { #endif return; } - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); if (token == "newmtl") { if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { return; @@ -328,8 +328,8 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { } else if (token == "Ks") { currentMaterial.specularColor = tokenizer.getVec3(); } else if ((token == "map_Kd") || (token == "map_Ke") || (token == "map_Ks") || (token == "map_bump") || (token == "bump") || (token == "map_d")) { - const QByteArray textureLine = tokenizer.getLineAsDatum(); - QByteArray filename; + const hifi::ByteArray textureLine = tokenizer.getLineAsDatum(); + hifi::ByteArray filename; OBJMaterialTextureOptions textureOptions; parseTextureLine(textureLine, filename, textureOptions); if (filename.endsWith(".tga")) { @@ -354,7 +354,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { } } -void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions) { +void OBJSerializer::parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions) { // Texture options reference http://paulbourke.net/dataformats/mtl/ // and https://wikivisually.com/wiki/Material_Template_Library @@ -442,12 +442,12 @@ void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray& } } -std::tuple requestData(QUrl& url) { +std::tuple requestData(hifi::URL& url) { auto request = DependencyManager::get()->createResourceRequest( nullptr, url, true, -1, "(OBJSerializer) requestData"); if (!request) { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } QEventLoop loop; @@ -458,12 +458,12 @@ std::tuple requestData(QUrl& url) { if (request->getResult() == ResourceRequest::Success) { return std::make_tuple(true, request->getData()); } else { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } } -QNetworkReply* request(QUrl& url, bool isTest) { +QNetworkReply* request(hifi::URL& url, bool isTest) { if (!qApp) { return nullptr; } @@ -488,7 +488,7 @@ QNetworkReply* request(QUrl& url, bool isTest) { } -bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, +bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts) { FaceGroup faces; HFMMesh& mesh = hfmModel.meshes[0]; @@ -522,7 +522,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m result = false; break; } - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); //qCDebug(modelformat) << token; // we don't support separate objects in the same file, so treat "o" the same as "g". if (token == "g" || token == "o") { @@ -535,7 +535,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { break; } - QByteArray groupName = tokenizer.getDatum(); + hifi::ByteArray groupName = tokenizer.getDatum(); currentGroup = groupName; if (!combineParts) { currentMaterialName = QString("part-") + QString::number(_partCounter++); @@ -544,7 +544,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m if (tokenizer.nextToken(true) != OBJTokenizer::DATUM_TOKEN) { break; } - QByteArray libraryName = tokenizer.getDatum(); + hifi::ByteArray libraryName = tokenizer.getDatum(); librariesSeen[libraryName] = true; // We'll read it later only if we actually need it. } else if (token == "usemtl") { @@ -598,14 +598,14 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m // vertex-index // vertex-index/texture-index // vertex-index/texture-index/surface-normal-index - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); auto firstChar = token[0]; // Tokenizer treats line endings as whitespace. Non-digit and non-negative sign indicates done; if (!isdigit(firstChar) && firstChar != '-') { tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); break; } - QList parts = token.split('/'); + QList parts = token.split('/'); assert(parts.count() >= 1); assert(parts.count() <= 3); // If indices are negative relative indices then adjust them to absolute indices based on current vector sizes @@ -626,7 +626,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m } } } - const QByteArray noData {}; + const hifi::ByteArray noData {}; face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices, vertexColors); face.groupName = currentGroup; @@ -661,9 +661,9 @@ std::unique_ptr OBJSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { +HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - QBuffer buffer { const_cast(&data) }; + QBuffer buffer { const_cast(&data) }; buffer.open(QIODevice::ReadOnly); auto hfmModelPtr = std::make_shared(); @@ -849,11 +849,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail QString basename = filename.remove(extIndex + 1, sizeof("obj")); preDefinedMaterial.diffuseColor = glm::vec3(1.0f); - QVector extensions = { "jpg", "jpeg", "png", "tga" }; - QByteArray base = basename.toUtf8(), textName = ""; + QVector extensions = { "jpg", "jpeg", "png", "tga" }; + hifi::ByteArray base = basename.toUtf8(), textName = ""; qCDebug(modelformat) << "OBJSerializer looking for default texture"; for (int i = 0; i < extensions.count(); i++) { - QByteArray candidateString = base + extensions[i]; + hifi::ByteArray candidateString = base + extensions[i]; if (isValidTexture(candidateString)) { textName = candidateString; break; @@ -871,11 +871,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash if (needsMaterialLibrary) { foreach (QString libraryName, librariesSeen.keys()) { // Throw away any path part of libraryName, and merge against original url. - QUrl libraryUrl = _url.resolved(QUrl(libraryName).fileName()); + hifi::URL libraryUrl = _url.resolved(hifi::URL(libraryName).fileName()); qCDebug(modelformat) << "OBJSerializer material library" << libraryName; bool success; - QByteArray data; - std::tie(success, data) = requestData(libraryUrl); + hifi::ByteArray data; + std::tie(success, data) = requestData(libraryUrl); if (success) { QBuffer buffer { &data }; buffer.open(QIODevice::ReadOnly); diff --git a/libraries/fbx/src/OBJSerializer.h b/libraries/fbx/src/OBJSerializer.h index c4f8025e66..6fdd95e2c3 100644 --- a/libraries/fbx/src/OBJSerializer.h +++ b/libraries/fbx/src/OBJSerializer.h @@ -25,9 +25,9 @@ public: COMMENT_TOKEN = 0x101 }; int nextToken(bool allowSpaceChar = false); - const QByteArray& getDatum() const { return _datum; } + const hifi::ByteArray& getDatum() const { return _datum; } bool isNextTokenFloat(); - const QByteArray getLineAsDatum(); // some "filenames" have spaces in them + const hifi::ByteArray getLineAsDatum(); // some "filenames" have spaces in them void skipLine() { _device->readLine(); } void pushBackToken(int token) { _pushedBackToken = token; } void ungetChar(char ch) { _device->ungetChar(ch); } @@ -39,7 +39,7 @@ public: private: QIODevice* _device; - QByteArray _datum; + hifi::ByteArray _datum; int _pushedBackToken; QString _comment; }; @@ -52,7 +52,7 @@ public: QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use. QString materialName; // Add one more set of vertex data. Answers true if successful - bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, + bool add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. // Even though HFMMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. @@ -75,11 +75,11 @@ public: glm::vec3 diffuseColor; glm::vec3 specularColor; glm::vec3 emissiveColor; - QByteArray diffuseTextureFilename; - QByteArray specularTextureFilename; - QByteArray emissiveTextureFilename; - QByteArray bumpTextureFilename; - QByteArray opacityTextureFilename; + hifi::ByteArray diffuseTextureFilename; + hifi::ByteArray specularTextureFilename; + hifi::ByteArray emissiveTextureFilename; + hifi::ByteArray bumpTextureFilename; + hifi::ByteArray opacityTextureFilename; OBJMaterialTextureOptions bumpTextureOptions; int illuminationModel; @@ -103,17 +103,17 @@ public: QString currentMaterialName; QHash materials; - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; private: - QUrl _url; + hifi::URL _url; - QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, + QHash librariesSeen; + bool parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); - void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions); - bool isValidTexture(const QByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format. + void parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions); + bool isValidTexture(const hifi::ByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format. int _partCounter { 0 }; }; diff --git a/libraries/gpu/src/gpu/FrameReader.cpp b/libraries/gpu/src/gpu/FrameReader.cpp index 2fe143ee90..812f4db7cc 100644 --- a/libraries/gpu/src/gpu/FrameReader.cpp +++ b/libraries/gpu/src/gpu/FrameReader.cpp @@ -40,7 +40,7 @@ public: auto lastSlash = filename.rfind('/'); result = filename.substr(0, lastSlash + 1); } else { - std::string result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString(); + result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString(); if (*result.rbegin() != '/') { result += '/'; } diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h index 747788aef8..d2d330167d 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -96,6 +96,8 @@ namespace scriptable { bool defaultFallthrough; std::unordered_map propertyFallthroughs; // not actually exposed to script + + graphics::MaterialKey key { 0 }; }; /**jsdoc diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 848f9d42ac..3bd4af601c 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -363,24 +363,87 @@ namespace scriptable { obj.setProperty("name", material.name); obj.setProperty("model", material.model); - const QScriptValue FALLTHROUGH("fallthrough"); - obj.setProperty("opacity", material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT) ? FALLTHROUGH : material.opacity); - obj.setProperty("roughness", material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT) ? FALLTHROUGH : material.roughness); - obj.setProperty("metallic", material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT) ? FALLTHROUGH : material.metallic); - obj.setProperty("scattering", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT) ? FALLTHROUGH : material.scattering); - obj.setProperty("unlit", material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT) ? FALLTHROUGH : material.unlit); - obj.setProperty("emissive", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.emissive)); - obj.setProperty("albedo", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.albedo)); + bool hasPropertyFallthroughs = !material.propertyFallthroughs.empty(); - obj.setProperty("emissiveMap", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT) ? FALLTHROUGH : material.emissiveMap); - obj.setProperty("albedoMap", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT) ? FALLTHROUGH : material.albedoMap); - obj.setProperty("opacityMap", material.opacityMap); - obj.setProperty("occlusionMap", material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT) ? FALLTHROUGH : material.occlusionMap); - obj.setProperty("lightmapMap", material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT) ? FALLTHROUGH : material.lightmapMap); - obj.setProperty("scatteringMap", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT) ? FALLTHROUGH : material.scatteringMap); + const QScriptValue FALLTHROUGH("fallthrough"); + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) { + obj.setProperty("opacity", FALLTHROUGH); + } else if (material.key.isTranslucentFactor()) { + obj.setProperty("opacity", material.opacity); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) { + obj.setProperty("roughness", FALLTHROUGH); + } else if (material.key.isGlossy()) { + obj.setProperty("roughness", material.roughness); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) { + obj.setProperty("metallic", FALLTHROUGH); + } else if (material.key.isMetallic()) { + obj.setProperty("metallic", material.metallic); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) { + obj.setProperty("scattering", FALLTHROUGH); + } else if (material.key.isScattering()) { + obj.setProperty("scattering", material.scattering); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) { + obj.setProperty("unlit", FALLTHROUGH); + } else if (material.key.isUnlit()) { + obj.setProperty("unlit", material.unlit); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) { + obj.setProperty("emissive", FALLTHROUGH); + } else if (material.key.isEmissive()) { + obj.setProperty("emissive", vec3ColorToScriptValue(engine, material.emissive)); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) { + obj.setProperty("albedo", FALLTHROUGH); + } else if (material.key.isAlbedo()) { + obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo)); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) { + obj.setProperty("emissiveMap", FALLTHROUGH); + } else if (!material.emissiveMap.isEmpty()) { + obj.setProperty("emissiveMap", material.emissiveMap); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) { + obj.setProperty("albedoMap", FALLTHROUGH); + } else if (!material.albedoMap.isEmpty()) { + obj.setProperty("albedoMap", material.albedoMap); + } + + if (!material.opacityMap.isEmpty()) { + obj.setProperty("opacityMap", material.opacityMap); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) { + obj.setProperty("occlusionMap", FALLTHROUGH); + } else if (!material.occlusionMap.isEmpty()) { + obj.setProperty("occlusionMap", material.occlusionMap); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) { + obj.setProperty("lightmapMap", FALLTHROUGH); + } else if (!material.lightmapMap.isEmpty()) { + obj.setProperty("lightmapMap", material.lightmapMap); + } + + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) { + obj.setProperty("scatteringMap", FALLTHROUGH); + } else if (!material.scatteringMap.isEmpty()) { + obj.setProperty("scatteringMap", material.scatteringMap); + } // Only set one of each of these - if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { obj.setProperty("metallicMap", FALLTHROUGH); } else if (!material.metallicMap.isEmpty()) { obj.setProperty("metallicMap", material.metallicMap); @@ -388,7 +451,7 @@ namespace scriptable { obj.setProperty("specularMap", material.specularMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) { obj.setProperty("roughnessMap", FALLTHROUGH); } else if (!material.roughnessMap.isEmpty()) { obj.setProperty("roughnessMap", material.roughnessMap); @@ -396,7 +459,7 @@ namespace scriptable { obj.setProperty("glossMap", material.glossMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) { obj.setProperty("normalMap", FALLTHROUGH); } else if (!material.normalMap.isEmpty()) { obj.setProperty("normalMap", material.normalMap); @@ -405,16 +468,16 @@ namespace scriptable { } // These need to be implemented, but set the fallthrough for now - if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { obj.setProperty("texCoordTransform0", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { obj.setProperty("texCoordTransform1", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { obj.setProperty("lightmapParams", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) { obj.setProperty("materialParams", FALLTHROUGH); } diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h index a72c3be14b..267ba01041 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h @@ -103,6 +103,10 @@ private: }; +namespace scriptable { + QScriptValue scriptableMaterialToScriptValue(QScriptEngine* engine, const scriptable::ScriptableMaterial &material); +}; + Q_DECLARE_METATYPE(glm::uint32) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(NestableType) diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp index 4ff751782c..fdd06ffa64 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -45,75 +45,80 @@ scriptable::ScriptableMaterial& scriptable::ScriptableMaterial::operator=(const defaultFallthrough = material.defaultFallthrough; propertyFallthroughs = material.propertyFallthroughs; + key = material.key; + return *this; } -scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) : - name(material->getName().c_str()), - model(material->getModel().c_str()), - opacity(material->getOpacity()), - roughness(material->getRoughness()), - metallic(material->getMetallic()), - scattering(material->getScattering()), - unlit(material->isUnlit()), - emissive(material->getEmissive()), - albedo(material->getAlbedo()), - defaultFallthrough(material->getDefaultFallthrough()), - propertyFallthroughs(material->getPropertyFallthroughs()) -{ - auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP); - if (map && map->getTextureSource()) { - emissiveMap = map->getTextureSource()->getUrl().toString(); - } +scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) { + if (material) { + name = material->getName().c_str(); + model = material->getModel().c_str(); + opacity = material->getOpacity(); + roughness = material->getRoughness(); + metallic = material->getMetallic(); + scattering = material->getScattering(); + unlit = material->isUnlit(); + emissive = material->getEmissive(); + albedo = material->getAlbedo(); + defaultFallthrough = material->getDefaultFallthrough(); + propertyFallthroughs = material->getPropertyFallthroughs(); + key = material->getKey(); - map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP); - if (map && map->getTextureSource()) { - albedoMap = map->getTextureSource()->getUrl().toString(); - if (map->useAlphaChannel()) { - opacityMap = albedoMap; + auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP); + if (map && map->getTextureSource()) { + emissiveMap = map->getTextureSource()->getUrl().toString(); } - } - map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) { - metallicMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) { - specularMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP); + if (map && map->getTextureSource()) { + albedoMap = map->getTextureSource()->getUrl().toString(); + if (map->useAlphaChannel()) { + opacityMap = albedoMap; + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) { - roughnessMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) { - glossMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) { + metallicMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) { + specularMap = map->getTextureSource()->getUrl().toString(); + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) { - normalMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) { - bumpMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) { + roughnessMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) { + glossMap = map->getTextureSource()->getUrl().toString(); + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP); - if (map && map->getTextureSource()) { - occlusionMap = map->getTextureSource()->getUrl().toString(); - } + map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) { + normalMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) { + bumpMap = map->getTextureSource()->getUrl().toString(); + } + } - map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP); - if (map && map->getTextureSource()) { - lightmapMap = map->getTextureSource()->getUrl().toString(); - } + map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP); + if (map && map->getTextureSource()) { + occlusionMap = map->getTextureSource()->getUrl().toString(); + } - map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP); - if (map && map->getTextureSource()) { - scatteringMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP); + if (map && map->getTextureSource()) { + lightmapMap = map->getTextureSource()->getUrl().toString(); + } + + map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP); + if (map && map->getTextureSource()) { + scatteringMap = map->getTextureSource()->getUrl().toString(); + } } } diff --git a/libraries/graphics/src/graphics/MaterialTextures.slh b/libraries/graphics/src/graphics/MaterialTextures.slh index 1cbee33238..c725aae9bb 100644 --- a/libraries/graphics/src/graphics/MaterialTextures.slh +++ b/libraries/graphics/src/graphics/MaterialTextures.slh @@ -235,7 +235,7 @@ vec3 fetchLightmapMap(vec2 uv) { <@endfunc@> <@func discardInvisible(opacity)@> { - if (<$opacity$> < 1.e-6) { + if (<$opacity$> <= 0.0) { discard; } } diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 4f44595eaa..22b089328f 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -53,6 +53,14 @@ using ColorType = glm::vec3; const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; +// 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; + +static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; +static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; +static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; +static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2; + // High Fidelity Model namespace namespace hfm { diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 6aa09c4d0f..2488b15fcd 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -205,7 +205,11 @@ QImage processRawImageData(QIODevice& content, const std::string& filename) { // Help the QImage loader by extracting the image file format from the url filename ext. // Some tga are not created properly without it. auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - content.open(QIODevice::ReadOnly); + if (!content.isReadable()) { + content.open(QIODevice::ReadOnly); + } else { + content.reset(); + } if (filenameExtension == "tga") { QImage image = image::readTGA(content); diff --git a/libraries/model-baker/CMakeLists.txt b/libraries/model-baker/CMakeLists.txt index 22c240b487..6c0f220340 100644 --- a/libraries/model-baker/CMakeLists.txt +++ b/libraries/model-baker/CMakeLists.txt @@ -4,4 +4,6 @@ setup_hifi_library() link_hifi_libraries(shared shaders task gpu graphics hfm material-networking) include_hifi_library_headers(networking) include_hifi_library_headers(image) -include_hifi_library_headers(ktx) \ No newline at end of file +include_hifi_library_headers(ktx) + +target_draco() diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index f55cacf0f2..536255a841 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -11,15 +11,15 @@ #include "Baker.h" -#include - #include "BakerTypes.h" +#include "ModelMath.h" #include "BuildGraphicsMeshTask.h" #include "CalculateMeshNormalsTask.h" #include "CalculateMeshTangentsTask.h" #include "CalculateBlendshapeNormalsTask.h" #include "CalculateBlendshapeTangentsTask.h" #include "PrepareJointsTask.h" +#include "BuildDracoMeshTask.h" #include "ParseFlowDataTask.h" namespace baker { @@ -60,12 +60,12 @@ namespace baker { blendshapesPerMeshOut = blendshapesPerMeshIn; for (int i = 0; i < (int)blendshapesPerMeshOut.size(); i++) { - const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i]; - const auto& tangentsPerBlendshape = tangentsPerBlendshapePerMesh[i]; + const auto& normalsPerBlendshape = safeGet(normalsPerBlendshapePerMesh, i); + const auto& tangentsPerBlendshape = safeGet(tangentsPerBlendshapePerMesh, i); auto& blendshapesOut = blendshapesPerMeshOut[i]; for (int j = 0; j < (int)blendshapesOut.size(); j++) { - const auto& normals = normalsPerBlendshape[j]; - const auto& tangents = tangentsPerBlendshape[j]; + const auto& normals = safeGet(normalsPerBlendshape, j); + const auto& tangents = safeGet(tangentsPerBlendshape, j); auto& blendshape = blendshapesOut[j]; blendshape.normals = QVector::fromStdVector(normals); blendshape.tangents = QVector::fromStdVector(tangents); @@ -91,10 +91,10 @@ namespace baker { auto meshesOut = meshesIn; for (int i = 0; i < numMeshes; i++) { auto& meshOut = meshesOut[i]; - meshOut._mesh = graphicsMeshesIn[i]; - meshOut.normals = QVector::fromStdVector(normalsPerMeshIn[i]); - meshOut.tangents = QVector::fromStdVector(tangentsPerMeshIn[i]); - meshOut.blendshapes = QVector::fromStdVector(blendshapesPerMeshIn[i]); + meshOut._mesh = safeGet(graphicsMeshesIn, i); + meshOut.normals = QVector::fromStdVector(safeGet(normalsPerMeshIn, i)); + meshOut.tangents = QVector::fromStdVector(safeGet(tangentsPerMeshIn, i)); + meshOut.blendshapes = QVector::fromStdVector(safeGet(blendshapesPerMeshIn, i)); } output = meshesOut; } @@ -119,12 +119,13 @@ namespace baker { class BakerEngineBuilder { public: - using Input = VaryingSet2; - using Output = VaryingSet2; + using Input = VaryingSet3; + using Output = VaryingSet4, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); const auto& mapping = input.getN(1); + const auto& materialMappingBaseURL = input.getN(2); // Split up the inputs from hfm::Model const auto modelPartsIn = model.addJob("GetModelParts", hfmModelIn); @@ -157,7 +158,18 @@ namespace baker { const auto jointIndices = jointInfoOut.getN(2); // Parse material mapping - const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); + const auto parseMaterialMappingInputs = ParseMaterialMappingTask::Input(mapping, materialMappingBaseURL).asVarying(); + const auto materialMapping = model.addJob("ParseMaterialMapping", parseMaterialMappingInputs); + + // Build Draco meshes + // NOTE: This task is disabled by default and must be enabled through configuration + // TODO: Tangent support (Needs changes to FBXSerializer_Mesh as well) + // NOTE: Due to an unresolved linker error, BuildDracoMeshTask is not functional on Android + // TODO: Figure out why BuildDracoMeshTask.cpp won't link with draco on Android + const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); + const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); + const auto dracoMeshes = buildDracoMeshOutputs.getN(0); + const auto materialList = buildDracoMeshOutputs.getN(1); // Parse flow data const auto flowData = model.addJob("ParseFlowData", mapping); @@ -170,20 +182,38 @@ namespace baker { const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, flowData).asVarying(); const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); - output = Output(hfmModelOut, materialMapping); + output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList); } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { _engine->feedInput(0, hfmModel); _engine->feedInput(1, mapping); + _engine->feedInput(2, materialMappingBaseURL); + } + + std::shared_ptr Baker::getConfiguration() { + return _engine->getConfiguration(); } void Baker::run() { _engine->run(); - hfmModel = _engine->getOutput().get().get0(); - materialMapping = _engine->getOutput().get().get1(); } + hfm::Model::Pointer Baker::getHFMModel() const { + return _engine->getOutput().get().get0(); + } + + MaterialMapping Baker::getMaterialMapping() const { + return _engine->getOutput().get().get1(); + } + + const std::vector& Baker::getDracoMeshes() const { + return _engine->getOutput().get().get2(); + } + + std::vector> Baker::getDracoMaterialLists() const { + return _engine->getOutput().get().get3(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 856b5f0142..6f74cb646e 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -12,8 +12,7 @@ #ifndef hifi_baker_Baker_h #define hifi_baker_Baker_h -#include - +#include #include #include "Engine.h" @@ -24,18 +23,22 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping); + Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL); + + std::shared_ptr getConfiguration(); void run(); // Outputs, available after run() is called - hfm::Model::Pointer hfmModel; - MaterialMapping materialMapping; + hfm::Model::Pointer getHFMModel() const; + MaterialMapping getMaterialMapping() const; + const std::vector& getDracoMeshes() const; + // This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials) + std::vector> getDracoMaterialLists() const; protected: EnginePointer _engine; }; - }; #endif //hifi_baker_Baker_h diff --git a/libraries/model-baker/src/model-baker/BakerTypes.h b/libraries/model-baker/src/model-baker/BakerTypes.h index 8b80b0bde4..3d16afab2e 100644 --- a/libraries/model-baker/src/model-baker/BakerTypes.h +++ b/libraries/model-baker/src/model-baker/BakerTypes.h @@ -36,7 +36,6 @@ namespace baker { using TangentsPerBlendshape = std::vector>; using MeshIndicesToModelNames = QHash; - using GeometryMappingPair = std::pair; }; #endif // hifi_BakerTypes_h diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp new file mode 100644 index 0000000000..2e378965de --- /dev/null +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -0,0 +1,251 @@ +// +// BuildDracoMeshTask.cpp +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/02/20. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "BuildDracoMeshTask.h" + +// Fix build warnings due to draco headers not casting size_t +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4267 ) +#endif +// gcc and clang +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#endif + + +#ifndef Q_OS_ANDROID +#include +#include +#endif + +#ifdef _WIN32 +#pragma warning( pop ) +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#include "ModelBakerLogging.h" +#include "ModelMath.h" + +#ifndef Q_OS_ANDROID +std::vector createMaterialList(const hfm::Mesh& mesh) { + std::vector materialList; + for (const auto& meshPart : mesh.parts) { + auto materialID = QVariant(meshPart.materialID).toByteArray(); + const auto materialIt = std::find(materialList.cbegin(), materialList.cend(), materialID); + if (materialIt == materialList.cend()) { + materialList.push_back(materialID); + } + } + return materialList; +} + +std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { + Q_ASSERT(normals.size() == 0 || normals.size() == mesh.vertices.size()); + Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); + Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); + + int64_t numTriangles{ 0 }; + for (auto& part : mesh.parts) { + int extraQuadTriangleIndices = part.quadTrianglesIndices.size() % 3; + int extraTriangleIndices = part.triangleIndices.size() % 3; + if (extraQuadTriangleIndices != 0 || extraTriangleIndices != 0) { + qCWarning(model_baker) << "Found a mesh part with indices not divisible by three. Some indices will be discarded during Draco mesh creation."; + } + numTriangles += (part.quadTrianglesIndices.size() - extraQuadTriangleIndices) / 3; + numTriangles += (part.triangleIndices.size() - extraTriangleIndices) / 3; + } + + if (numTriangles == 0) { + return std::unique_ptr(); + } + + draco::TriangleSoupMeshBuilder meshBuilder; + + meshBuilder.Start(numTriangles); + + bool hasNormals{ normals.size() > 0 }; + bool hasColors{ mesh.colors.size() > 0 }; + bool hasTexCoords{ mesh.texCoords.size() > 0 }; + bool hasTexCoords1{ mesh.texCoords1.size() > 0 }; + bool hasPerFaceMaterials{ mesh.parts.size() > 1 }; + bool needsOriginalIndices{ (!mesh.clusterIndices.empty() || !mesh.blendshapes.empty()) && mesh.originalIndices.size() > 0 }; + + int normalsAttributeID { -1 }; + int colorsAttributeID { -1 }; + int texCoordsAttributeID { -1 }; + int texCoords1AttributeID { -1 }; + int faceMaterialAttributeID { -1 }; + int originalIndexAttributeID { -1 }; + + const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, + 3, draco::DT_FLOAT32); + if (needsOriginalIndices) { + originalIndexAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, + 1, draco::DT_INT32); + } + + if (hasNormals) { + normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, + 3, draco::DT_FLOAT32); + } + if (hasColors) { + colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, + 3, draco::DT_FLOAT32); + } + if (hasTexCoords) { + texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, + 2, draco::DT_FLOAT32); + } + if (hasTexCoords1) { + texCoords1AttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, + 2, draco::DT_FLOAT32); + } + if (hasPerFaceMaterials) { + faceMaterialAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, + 1, draco::DT_UINT16); + } + + auto partIndex = 0; + draco::FaceIndex face; + uint16_t materialID; + + for (auto& part : mesh.parts) { + auto materialIt = std::find(materialList.cbegin(), materialList.cend(), QVariant(part.materialID).toByteArray()); + materialID = (uint16_t)(materialIt - materialList.cbegin()); + + auto addFace = [&](const QVector& indices, int index, draco::FaceIndex face) { + int32_t idx0 = indices[index]; + int32_t idx1 = indices[index + 1]; + int32_t idx2 = indices[index + 2]; + + if (hasPerFaceMaterials) { + meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); + } + + meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, + &mesh.vertices[idx0], &mesh.vertices[idx1], + &mesh.vertices[idx2]); + + if (needsOriginalIndices) { + meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, + &mesh.originalIndices[idx0], + &mesh.originalIndices[idx1], + &mesh.originalIndices[idx2]); + } + if (hasNormals) { + meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, + &normals[idx0], &normals[idx1], + &normals[idx2]); + } + if (hasColors) { + meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, + &mesh.colors[idx0], &mesh.colors[idx1], + &mesh.colors[idx2]); + } + if (hasTexCoords) { + meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, + &mesh.texCoords[idx0], &mesh.texCoords[idx1], + &mesh.texCoords[idx2]); + } + if (hasTexCoords1) { + meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, + &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], + &mesh.texCoords1[idx2]); + } + }; + + for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { + addFace(part.quadTrianglesIndices, i, face++); + } + + for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { + addFace(part.triangleIndices, i, face++); + } + + partIndex++; + } + + auto dracoMesh = meshBuilder.Finalize(); + + if (!dracoMesh) { + qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node"; + return std::unique_ptr(); + } + + // we need to modify unique attribute IDs for custom attributes + // so the attributes are easily retrievable on the other side + if (hasPerFaceMaterials) { + dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); + } + + if (hasTexCoords1) { + dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); + } + + if (needsOriginalIndices) { + dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); + } + + return dracoMesh; +} +#endif // not Q_OS_ANDROID + +void BuildDracoMeshTask::configure(const Config& config) { + _encodeSpeed = config.encodeSpeed; + _decodeSpeed = config.decodeSpeed; +} + +void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { +#ifdef Q_OS_ANDROID + qCWarning(model_baker) << "BuildDracoMesh is disabled on Android. Output meshes will be empty."; +#else + const auto& meshes = input.get0(); + const auto& normalsPerMesh = input.get1(); + const auto& tangentsPerMesh = input.get2(); + auto& dracoBytesPerMesh = output.edit0(); + auto& materialLists = output.edit1(); + + dracoBytesPerMesh.reserve(meshes.size()); + materialLists.reserve(meshes.size()); + for (size_t i = 0; i < meshes.size(); i++) { + const auto& mesh = meshes[i]; + const auto& normals = baker::safeGet(normalsPerMesh, i); + const auto& tangents = baker::safeGet(tangentsPerMesh, i); + dracoBytesPerMesh.emplace_back(); + auto& dracoBytes = dracoBytesPerMesh.back(); + materialLists.push_back(createMaterialList(mesh)); + const auto& materialList = materialLists.back(); + + auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList); + + if (dracoMesh) { + draco::Encoder encoder; + + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); + encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); + encoder.SetSpeedOptions(_encodeSpeed, _decodeSpeed); + + draco::EncoderBuffer buffer; + encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); + + dracoBytes = hifi::ByteArray(buffer.data(), (int)buffer.size()); + } + } +#endif // not Q_OS_ANDROID +} diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h new file mode 100644 index 0000000000..0e33be3c41 --- /dev/null +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -0,0 +1,48 @@ +// +// BuildDracoMeshTask.h +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/02/20. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BuildDracoMeshTask_h +#define hifi_BuildDracoMeshTask_h + +#include +#include + +#include "Engine.h" +#include "BakerTypes.h" + +// BuildDracoMeshTask is disabled by default +class BuildDracoMeshConfig : public baker::JobConfig { + Q_OBJECT + Q_PROPERTY(int encodeSpeed MEMBER encodeSpeed) + Q_PROPERTY(int decodeSpeed MEMBER decodeSpeed) +public: + BuildDracoMeshConfig() : baker::JobConfig(false) {} + + int encodeSpeed { 0 }; + int decodeSpeed { 5 }; +}; + +class BuildDracoMeshTask { +public: + using Config = BuildDracoMeshConfig; + using Input = baker::VaryingSet3, baker::NormalsPerMesh, baker::TangentsPerMesh>; + using Output = baker::VaryingSet2, std::vector>>; + using JobModel = baker::Job::ModelIO; + + void configure(const Config& config); + void run(const baker::BakeContextPointer& context, const Input& input, Output& output); + +protected: + int _encodeSpeed { 0 }; + int _decodeSpeed { 5 }; +}; + +#endif // hifi_BuildDracoMeshTask_h diff --git a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp index c41431f940..2467da7656 100644 --- a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp @@ -15,6 +15,7 @@ #include #include "ModelBakerLogging.h" +#include "ModelMath.h" using vec2h = glm::tvec2; @@ -385,7 +386,7 @@ void BuildGraphicsMeshTask::run(const baker::BakeContextPointer& context, const auto& graphicsMesh = graphicsMeshes[i]; // Try to create the graphics::Mesh - buildGraphicsMesh(meshes[i], graphicsMesh, normalsPerMesh[i], tangentsPerMesh[i]); + buildGraphicsMesh(meshes[i], graphicsMesh, baker::safeGet(normalsPerMesh, i), baker::safeGet(tangentsPerMesh, i)); // Choose a name for the mesh if (graphicsMesh) { diff --git a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp index 04e05f0378..ba8fd94f09 100644 --- a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp @@ -24,7 +24,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte tangentsPerBlendshapePerMeshOut.reserve(normalsPerBlendshapePerMesh.size()); for (size_t i = 0; i < blendshapesPerMesh.size(); i++) { - const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i]; + const auto& normalsPerBlendshape = baker::safeGet(normalsPerBlendshapePerMesh, i); const auto& blendshapes = blendshapesPerMesh[i]; const auto& mesh = meshes[i]; tangentsPerBlendshapePerMeshOut.emplace_back(); @@ -43,7 +43,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte for (size_t j = 0; j < blendshapes.size(); j++) { const auto& blendshape = blendshapes[j]; const auto& tangentsIn = blendshape.tangents; - const auto& normals = normalsPerBlendshape[j]; + const auto& normals = baker::safeGet(normalsPerBlendshape, j); tangentsPerBlendshapeOut.emplace_back(); auto& tangentsOut = tangentsPerBlendshapeOut[tangentsPerBlendshapeOut.size()-1]; diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp index 6e12ec546d..d2144a0e30 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp @@ -34,7 +34,7 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co for (int i = 0; i < (int)meshes.size(); i++) { const auto& mesh = meshes[i]; const auto& tangentsIn = mesh.tangents; - const auto& normals = normalsPerMesh[i]; + const auto& normals = baker::safeGet(normalsPerMesh, i); tangentsPerMeshOut.emplace_back(); auto& tangentsOut = tangentsPerMeshOut[tangentsPerMeshOut.size()-1]; diff --git a/libraries/model-baker/src/model-baker/ModelMath.h b/libraries/model-baker/src/model-baker/ModelMath.h index 2a909e6eed..38bb3e1b3d 100644 --- a/libraries/model-baker/src/model-baker/ModelMath.h +++ b/libraries/model-baker/src/model-baker/ModelMath.h @@ -14,6 +14,17 @@ #include "BakerTypes.h" namespace baker { + template + const T& safeGet(const std::vector& data, size_t i) { + static T t; + + if (data.size() > i) { + return data[i]; + } else { + return t; + } + } + // Returns a reference to the normal at the specified index, or nullptr if it cannot be accessed using NormalAccessor = std::function; diff --git a/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp b/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp index 10991ecbe6..48466ebe07 100644 --- a/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp @@ -8,12 +8,11 @@ #include "ParseFlowDataTask.h" -void ParseFlowDataTask::run(const baker::BakeContextPointer& context, const Input& mappingPair, Output& output) { +void ParseFlowDataTask::run(const baker::BakeContextPointer& context, const Input& mapping, Output& output) { FlowData flowData; static const QString FLOW_PHYSICS_FIELD = "flowPhysicsData"; static const QString FLOW_COLLISIONS_FIELD = "flowCollisionsData"; - auto mapping = mappingPair.second; - for (auto mappingIter = mapping.begin(); mappingIter != mapping.end(); mappingIter++) { + for (auto mappingIter = mapping.cbegin(); mappingIter != mapping.cend(); mappingIter++) { if (mappingIter.key() == FLOW_PHYSICS_FIELD || mappingIter.key() == FLOW_COLLISIONS_FIELD) { QByteArray data = mappingIter.value().toByteArray(); QJsonObject dataObject = QJsonDocument::fromJson(data).object(); diff --git a/libraries/model-baker/src/model-baker/ParseFlowDataTask.h b/libraries/model-baker/src/model-baker/ParseFlowDataTask.h index 65b8f7654b..deabb63f79 100644 --- a/libraries/model-baker/src/model-baker/ParseFlowDataTask.h +++ b/libraries/model-baker/src/model-baker/ParseFlowDataTask.h @@ -12,11 +12,13 @@ #include #include "Engine.h" +#include + #include "BakerTypes.h" class ParseFlowDataTask { public: - using Input = baker::GeometryMappingPair; + using Input = hifi::VariantHash; using Output = FlowData; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index 0a1964d8cd..acb2bdc1c5 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -11,8 +11,8 @@ #include "ModelBakerLogging.h" void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { - const auto& url = input.first; - const auto& mapping = input.second; + const auto& mapping = input.get0(); + const auto& url = input.get1(); MaterialMapping materialMapping; auto mappingIter = mapping.find("materialMap"); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h index 5f5eff327d..7c94661b28 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -13,6 +13,8 @@ #include +#include + #include "Engine.h" #include "BakerTypes.h" @@ -20,7 +22,7 @@ class ParseMaterialMappingTask { public: - using Input = baker::GeometryMappingPair; + using Input = baker::VaryingSet2; using Output = MaterialMapping; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index b8bcdb386e..6bf25ff769 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -13,7 +13,7 @@ #include "ModelBakerLogging.h" -QMap getJointNameMapping(const QVariantHash& mapping) { +QMap getJointNameMapping(const hifi::VariantHash& mapping) { static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; QMap hfmToHifiJointNameMap; if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { @@ -26,7 +26,7 @@ QMap getJointNameMapping(const QVariantHash& mapping) { return hfmToHifiJointNameMap; } -QMap getJointRotationOffsets(const QVariantHash& mapping) { +QMap getJointRotationOffsets(const hifi::VariantHash& mapping) { QMap jointRotationOffsets; static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2"; @@ -56,68 +56,76 @@ QMap getJointRotationOffsets(const QVariantHash& mapping) { return jointRotationOffsets; } +void PrepareJointsTask::configure(const Config& config) { + _passthrough = config.passthrough; +} + void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& jointsIn = input.get0(); - const auto& mapping = input.get1(); auto& jointsOut = output.edit0(); - auto& jointRotationOffsets = output.edit1(); - auto& jointIndices = output.edit2(); - bool newJointRot = false; - static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2"; - QVariantHash fstHashMap = mapping.second; - if (fstHashMap.contains(JOINT_ROTATION_OFFSET2_FIELD)) { - newJointRot = true; + if (_passthrough) { + jointsOut = jointsIn; } else { - newJointRot = false; - } + const auto& mapping = input.get1(); + auto& jointRotationOffsets = output.edit1(); + auto& jointIndices = output.edit2(); - // Get joint renames - auto jointNameMapping = getJointNameMapping(mapping.second); - // Apply joint metadata from FST file mappings - for (const auto& jointIn : jointsIn) { - jointsOut.push_back(jointIn); - auto& jointOut = jointsOut.back(); + bool newJointRot = false; + static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2"; + QVariantHash fstHashMap = mapping; + if (fstHashMap.contains(JOINT_ROTATION_OFFSET2_FIELD)) { + newJointRot = true; + } else { + newJointRot = false; + } - if (!newJointRot) { - auto jointNameMapKey = jointNameMapping.key(jointIn.name); - if (jointNameMapping.contains(jointNameMapKey)) { - jointOut.name = jointNameMapKey; + // Get joint renames + auto jointNameMapping = getJointNameMapping(mapping); + // Apply joint metadata from FST file mappings + for (const auto& jointIn : jointsIn) { + jointsOut.push_back(jointIn); + auto& jointOut = jointsOut.back(); + + if (!newJointRot) { + auto jointNameMapKey = jointNameMapping.key(jointIn.name); + if (jointNameMapping.contains(jointNameMapKey)) { + jointOut.name = jointNameMapKey; + } + } + jointIndices.insert(jointOut.name, (int)jointsOut.size()); + } + + // Get joint rotation offsets from FST file mappings + auto offsets = getJointRotationOffsets(mapping); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + int jointIndex = jointIndices.value(jointName) - 1; + if (jointIndex >= 0) { + glm::quat rotationOffset = itr.value(); + jointRotationOffsets.insert(jointIndex, rotationOffset); + qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; } } - jointIndices.insert(jointOut.name, (int)jointsOut.size()); - } - // Get joint rotation offsets from FST file mappings - auto offsets = getJointRotationOffsets(mapping.second); - for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { - QString jointName = itr.key(); - int jointIndex = jointIndices.value(jointName) - 1; - if (jointIndex >= 0) { - glm::quat rotationOffset = itr.value(); - jointRotationOffsets.insert(jointIndex, rotationOffset); - qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; - } - } - - if (newJointRot) { - for (const auto& jointIn : jointsIn) { - - auto jointNameMapKey = jointNameMapping.key(jointIn.name); - int mappedIndex = jointIndices.value(jointIn.name); - if (jointNameMapping.contains(jointNameMapKey)) { - - // delete and replace with hifi name - jointIndices.remove(jointIn.name); - jointIndices.insert(jointNameMapKey, mappedIndex); - } else { - - // nothing mapped to this fbx joint name - if (jointNameMapping.contains(jointIn.name)) { - // but the name is in the list of hifi names is mapped to a different joint - int extraIndex = jointIndices.value(jointIn.name); - jointIndices.remove(jointIn.name); - jointIndices.insert("", extraIndex); + if (newJointRot) { + for (auto& jointOut : jointsOut) { + auto jointNameMapKey = jointNameMapping.key(jointOut.name); + int mappedIndex = jointIndices.value(jointOut.name); + if (jointNameMapping.contains(jointNameMapKey)) { + // delete and replace with hifi name + jointIndices.remove(jointOut.name); + jointOut.name = jointNameMapKey; + jointIndices.insert(jointOut.name, mappedIndex); + } else { + // nothing mapped to this fbx joint name + if (jointNameMapping.contains(jointOut.name)) { + // but the name is in the list of hifi names is mapped to a different joint + int extraIndex = jointIndices.value(jointOut.name); + jointIndices.remove(jointOut.name); + jointOut.name = ""; + jointIndices.insert(jointOut.name, extraIndex); + } } } } diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index b18acdfceb..802dbb3826 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -12,20 +12,32 @@ #ifndef hifi_PrepareJointsTask_h #define hifi_PrepareJointsTask_h -#include - +#include #include #include "Engine.h" #include "BakerTypes.h" +// The property "passthrough", when enabled, will let the input joints flow to the output unmodified, unlike the disabled property, which discards the data +class PrepareJointsConfig : public baker::JobConfig { + Q_OBJECT + Q_PROPERTY(bool passthrough MEMBER passthrough) +public: + bool passthrough { false }; +}; + class PrepareJointsTask { public: - using Input = baker::VaryingSet2, baker::GeometryMappingPair /*mapping*/>; + using Config = PrepareJointsConfig; + using Input = baker::VaryingSet2, hifi::VariantHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; - using JobModel = baker::Job::ModelIO; + using JobModel = baker::Job::ModelIO; + void configure(const Config& config); void run(const baker::BakeContextPointer& context, const Input& input, Output& output); + +protected: + bool _passthrough { false }; }; #endif // hifi_PrepareJointsTask_h \ No newline at end of file diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index a48f96eb1b..4cf7609ee9 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -120,19 +120,21 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { if (filename.isNull()) { finishedLoading(false); } else { - QUrl url = _url.resolved(filename); + const QString baseURL = _mapping.value("baseURL").toString(); + const QUrl base = _effectiveBaseURL.resolved(baseURL); + QUrl url = base.resolved(filename); QString texdir = _mapping.value(TEXDIR_FIELD).toString(); if (!texdir.isNull()) { if (!texdir.endsWith('/')) { texdir += '/'; } - _textureBaseUrl = resolveTextureBaseUrl(url, _url.resolved(texdir)); + _textureBaseUrl = resolveTextureBaseUrl(url, base.resolved(texdir)); } else { _textureBaseUrl = url.resolved(QUrl(".")); } - auto scripts = FSTReader::getScripts(_url, _mapping); + auto scripts = FSTReader::getScripts(base, _mapping); if (scripts.size() > 0) { _mapping.remove(SCRIPT_FIELD); for (auto &scriptPath : scripts) { @@ -145,7 +147,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { if (animGraphVariant.isValid()) { QUrl fstUrl(animGraphVariant.toString()); if (fstUrl.isValid()) { - _animGraphOverrideUrl = _url.resolved(fstUrl); + _animGraphOverrideUrl = base.resolved(fstUrl); } else { _animGraphOverrideUrl = QUrl(); } @@ -154,7 +156,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { } auto modelCache = DependencyManager::get(); - GeometryExtra extra { GeometryMappingPair(_url, _mapping), _textureBaseUrl, false }; + GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseUrl, false }; // Get the raw GeometryResource _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); @@ -249,6 +251,7 @@ void GeometryReader::run() { HFMModel::Pointer hfmModel; QVariantHash serializerMapping = _mapping.second; serializerMapping["combineParts"] = _combineParts; + serializerMapping["deduplicateIndices"] = true; if (_url.path().toLower().endsWith(".gz")) { QByteArray uncompressedData; @@ -339,12 +342,12 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) { // Do processing on the model - baker::Baker modelBaker(hfmModel, mapping); + baker::Baker modelBaker(hfmModel, mapping.second, mapping.first); modelBaker.run(); // Assume ownership of the processed HFMModel - _hfmModel = modelBaker.hfmModel; - _materialMapping = modelBaker.materialMapping; + _hfmModel = modelBaker.getHFMModel(); + _materialMapping = modelBaker.getMaterialMapping(); // Copy materials QHash materialIDAtlas; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 4647c50496..226433e388 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -536,7 +536,7 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas QByteArray postData; postData.append("grant_type=password&"); - postData.append("username=" + login + "&"); + postData.append("username=" + QUrl::toPercentEncoding(login) + "&"); postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index f4221e3d49..517daf8ce5 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -834,6 +834,7 @@ bool AddressManager::setDomainInfo(const QUrl& domainURL, LookupTrigger trigger) } _domainURL = domainURL; + _shareablePlaceName.clear(); // clear any current place information _rootPlaceID = QUuid(); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a9dbc12b09..18a180ad79 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -677,6 +677,9 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t // If there is a new node with the same socket, this is a reconnection, kill the old node removeOldNode(findNodeWithAddr(publicSocket)); removeOldNode(findNodeWithAddr(localSocket)); + // If there is an old Connection to the new node's address kill it + _nodeSocket.cleanupConnection(publicSocket); + _nodeSocket.cleanupConnection(localSocket); auto it = _connectionIDs.find(uuid); if (it == _connectionIDs.end()) { diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index e6eb6087b0..0021a594bc 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -1016,6 +1016,14 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // also send them the current ignore radius state. sendIgnoreRadiusStateToNode(newNode); + + // also send the current avatar and injector gains + if (_avatarGain != 0.0f) { + setAvatarGain(QUuid(), _avatarGain); + } + if (_injectorGain != 0.0f) { + setInjectorGain(_injectorGain); + } } if (newNode->getType() == NodeType::AvatarMixer) { // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, @@ -1062,13 +1070,17 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { if (nodeID.isNull()) { qCDebug(networking) << "Sending Set MASTER Avatar Gain packet with Gain:" << gain; - } else { - qCDebug(networking) << "Sending Set Avatar Gain packet with UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; - } - sendPacket(std::move(setAvatarGainPacket), *audioMixer); - QWriteLocker lock{ &_avatarGainMapLock }; - _avatarGainMap[nodeID] = gain; + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + _avatarGain = gain; + + } else { + qCDebug(networking) << "Sending Set Avatar Gain packet with UUID:" << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; + + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + QWriteLocker lock{ &_avatarGainMapLock }; + _avatarGainMap[nodeID] = gain; + } } else { qWarning() << "Couldn't find audio mixer to send set gain request"; @@ -1079,14 +1091,41 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { } float NodeList::getAvatarGain(const QUuid& nodeID) { - QReadLocker lock{ &_avatarGainMapLock }; - auto it = _avatarGainMap.find(nodeID); - if (it != _avatarGainMap.cend()) { - return it->second; + if (nodeID.isNull()) { + return _avatarGain; + } else { + QReadLocker lock{ &_avatarGainMapLock }; + auto it = _avatarGainMap.find(nodeID); + if (it != _avatarGainMap.cend()) { + return it->second; + } } return 0.0f; } +void NodeList::setInjectorGain(float gain) { + auto audioMixer = soloNodeOfType(NodeType::AudioMixer); + if (audioMixer) { + // setup the packet + auto setInjectorGainPacket = NLPacket::create(PacketType::InjectorGainSet, sizeof(float), true); + + // We need to convert the gain in dB (from the script) to an amplitude before packing it. + setInjectorGainPacket->writePrimitive(packFloatGainToByte(fastExp2f(gain / 6.02059991f))); + + qCDebug(networking) << "Sending Set Injector Gain packet with Gain:" << gain; + + sendPacket(std::move(setInjectorGainPacket), *audioMixer); + _injectorGain = gain; + + } else { + qWarning() << "Couldn't find audio mixer to send set gain request"; + } +} + +float NodeList::getInjectorGain() { + return _injectorGain; +} + void NodeList::kickNodeBySessionID(const QUuid& nodeID) { // send a request to domain-server to kick the node with the given session ID // the domain-server will handle the persistence of the kick (via username or IP) diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index e135bc937d..f871560fba 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -83,6 +83,8 @@ public: bool isPersonalMutingNode(const QUuid& nodeID) const; void setAvatarGain(const QUuid& nodeID, float gain); float getAvatarGain(const QUuid& nodeID); + void setInjectorGain(float gain); + float getInjectorGain(); void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); @@ -181,6 +183,9 @@ private: mutable QReadWriteLock _avatarGainMapLock; tbb::concurrent_unordered_map _avatarGainMap; + std::atomic _avatarGain { 0.0f }; // in dB + std::atomic _injectorGain { 0.0f }; // in dB + void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); #if defined(Q_OS_ANDROID) Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index c63170de75..2f47ef5e00 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -71,12 +71,12 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b doLogAction("makeUserConnection", payload); } -void UserActivityLoggerScriptingInterface::bubbleToggled(bool newValue) { - doLogAction(newValue ? "bubbleOn" : "bubbleOff"); +void UserActivityLoggerScriptingInterface::privacyShieldToggled(bool newValue) { + doLogAction(newValue ? "privacyShieldOn" : "privacyShieldOff"); } -void UserActivityLoggerScriptingInterface::bubbleActivated() { - doLogAction("bubbleActivated"); +void UserActivityLoggerScriptingInterface::privacyShieldActivated() { + doLogAction("privacyShieldActivated"); } void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) { diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index 71d411056d..1cda1235e9 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -30,8 +30,8 @@ public: Q_INVOKABLE void palAction(QString action, QString target); Q_INVOKABLE void palOpened(float secondsOpen); Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details = ""); - Q_INVOKABLE void bubbleToggled(bool newValue); - Q_INVOKABLE void bubbleActivated(); + Q_INVOKABLE void privacyShieldToggled(bool newValue); + Q_INVOKABLE void privacyShieldActivated(); Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{}); Q_INVOKABLE void commercePurchaseSuccess(QString marketplaceID, QString contentCreator, int cost, bool firstPurchaseOfThisItem); Q_INVOKABLE void commercePurchaseFailure(QString marketplaceID, QString contentCreator, int cost, bool firstPurchaseOfThisItem, QString errorDetails); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 7f20f881da..e527e660b3 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -38,10 +38,10 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(EntityQueryPacketVersion::ConicalFrustums); case PacketType::AvatarIdentity: case PacketType::AvatarData: - return static_cast(AvatarMixerPacketVersion::SendMaxTranslationDimension); + return static_cast(AvatarMixerPacketVersion::FBXJointOrderChange); case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::SendMaxTranslationDimension); + return static_cast(AvatarMixerPacketVersion::FBXJointOrderChange); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); // ICE packets diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 0ec7c40ca4..08abd40849 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, SetAvatarTraits, - UNUSED_PACKET_TYPE, + InjectorGainSet, AssignmentClientStatus, NoisyMute, AvatarIdentity, @@ -266,6 +266,8 @@ enum class EntityVersion : PacketVersion { ModelScale, ReOrderParentIDProperties, CertificateTypeProperty, + DisableWebMedia, + ParticleShapeType, // Add new versions above here NUM_PACKET_TYPE, @@ -327,7 +329,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { CollisionFlag, AvatarTraitsAck, FasterAvatarEntities, - SendMaxTranslationDimension + SendMaxTranslationDimension, + FBXJointOrderChange }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/octree/src/OctreeEntitiesFileParser.cpp b/libraries/octree/src/OctreeEntitiesFileParser.cpp index 2f03eda286..e82201adfd 100644 --- a/libraries/octree/src/OctreeEntitiesFileParser.cpp +++ b/libraries/octree/src/OctreeEntitiesFileParser.cpp @@ -97,7 +97,7 @@ bool OctreeEntitiesFileParser::parseEntities(QVariantMap& parsedEntities) { _errorString = "Duplicate Id entries"; return false; } - + gotId = true; if (nextToken() != '"') { _errorString = "Invalid Id value"; return false; @@ -107,14 +107,21 @@ bool OctreeEntitiesFileParser::parseEntities(QVariantMap& parsedEntities) { _errorString = "Invalid Id string"; return false; } - QUuid idValue = QUuid::fromString(QLatin1String(idString.c_str()) ); - if (idValue.isNull()) { - _errorString = "Id value invalid UUID string: " + idString; - return false; - } - parsedEntities["Id"] = idValue; - gotId = true; + // some older archives may have a null string id, so + // return success without setting parsedEntities, + // which will result in a new uuid for the restored + // archive. (not parsing and using isNull as parsing + // results in null if there is a corrupt string) + + if (idString != "{00000000-0000-0000-0000-000000000000}") { + QUuid idValue = QUuid::fromString(QLatin1String(idString.c_str()) ); + if (idValue.isNull()) { + _errorString = "Id value invalid UUID string: " + idString; + return false; + } + parsedEntities["Id"] = idValue; + } } else if (key == "Version") { if (gotVersion) { _errorString = "Duplicate Version entries"; diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 32ee72ea1c..20ba3cde60 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -82,11 +82,11 @@ void OctreePersistThread::start() { } if (data.readOctreeDataInfoFromData(_cachedJSONData)) { - qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; packet->writePrimitive(true); auto id = data.id.toRfc4122(); packet->write(id); - packet->writePrimitive(data.version); + packet->writePrimitive(data.dataVersion); } else { _cachedJSONData.clear(); qCWarning(octree) << "No octree data found"; @@ -144,8 +144,8 @@ void OctreePersistThread::handleOctreeDataFileReply(QSharedPointersetOctreeVersionInfo(data.id, data.version); + qDebug() << "Setting entity version info to: " << data.id << data.dataVersion; + _tree->setOctreeVersionInfo(data.id, data.dataVersion); } bool persistentFileRead; diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.cpp b/libraries/oculusMobile/src/ovr/Framebuffer.cpp index 0f59eef614..a1dc1841de 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.cpp +++ b/libraries/oculusMobile/src/ovr/Framebuffer.cpp @@ -7,59 +7,44 @@ // #include "Framebuffer.h" +#include + #include -#include #include #include #include +#include "Helpers.h" + using namespace ovr; void Framebuffer::updateLayer(int eye, ovrLayerProjection2& layer, const ovrMatrix4f* projectionMatrix ) const { auto& layerTexture = layer.Textures[eye]; - layerTexture.ColorSwapChain = _swapChain; - layerTexture.SwapChainIndex = _index; + layerTexture.ColorSwapChain = _swapChainInfos[eye].swapChain; + layerTexture.SwapChainIndex = _swapChainInfos[eye].index; if (projectionMatrix) { layerTexture.TexCoordsFromTanAngles = ovrMatrix4f_TanAngleMatrixFromProjection( projectionMatrix ); } layerTexture.TextureRect = { 0, 0, 1, 1 }; } +void Framebuffer::SwapChainInfo::destroy() { + if (swapChain != nullptr) { + vrapi_DestroyTextureSwapChain(swapChain); + swapChain = nullptr; + } + index = -1; + length = -1; +} + void Framebuffer::create(const glm::uvec2& size) { _size = size; - _index = 0; - _validTexture = false; - - // Depth renderbuffer - /* glGenRenderbuffers(1, &_depth); - glBindRenderbuffer(GL_RENDERBUFFER, _depth); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, _size.x, _size.y); - glBindRenderbuffer(GL_RENDERBUFFER, 0); -*/ - // Framebuffer - glGenFramebuffers(1, &_fbo); - // glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - // glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depth); - // glBindFramebuffer(GL_FRAMEBUFFER, 0); - - _swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, _size.x, _size.y, 1, 3); - - _length = vrapi_GetTextureSwapChainLength(_swapChain); - if (!_length) { - __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); - return; - } - - for (int i = 0; i < _length; ++i) { - GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, i); - glBindTexture(GL_TEXTURE_2D, chainTexId); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].create(size); + }); glBindTexture(GL_TEXTURE_2D, 0); + glGenFramebuffers(1, &_fbo); } void Framebuffer::destroy() { @@ -67,28 +52,82 @@ void Framebuffer::destroy() { glDeleteFramebuffers(1, &_fbo); _fbo = 0; } - if (0 != _depth) { - glDeleteRenderbuffers(1, &_depth); - _depth = 0; - } - if (_swapChain != nullptr) { - vrapi_DestroyTextureSwapChain(_swapChain); - _swapChain = nullptr; - } - _index = -1; - _length = -1; + + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].destroy(); + }); } void Framebuffer::advance() { - _index = (_index + 1) % _length; - _validTexture = false; + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].advance(); + }); } -void Framebuffer::bind() { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - if (!_validTexture) { - GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, _index); - glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, chainTexId, 0); - _validTexture = true; +void Framebuffer::bind(GLenum target) { + glBindFramebuffer(target, _fbo); + _swapChainInfos[0].bind(target, GL_COLOR_ATTACHMENT0); + _swapChainInfos[1].bind(target, GL_COLOR_ATTACHMENT1); +} + +void Framebuffer::invalidate(GLenum target) { + static const std::array INVALIDATE_ATTACHMENTS {{ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }}; + glInvalidateFramebuffer(target, static_cast(INVALIDATE_ATTACHMENTS.size()), INVALIDATE_ATTACHMENTS.data()); +} + + +void Framebuffer::drawBuffers(ovrEye eye) const { + static const std::array, 3> EYE_DRAW_BUFFERS { { + {GL_COLOR_ATTACHMENT0, GL_NONE}, + {GL_NONE, GL_COLOR_ATTACHMENT1}, + {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1} + } }; + + switch(eye) { + case VRAPI_EYE_LEFT: + case VRAPI_EYE_RIGHT: + case VRAPI_EYE_COUNT: { + const auto& eyeDrawBuffers = EYE_DRAW_BUFFERS[eye]; + glDrawBuffers(static_cast(eyeDrawBuffers.size()), eyeDrawBuffers.data()); + } + break; + + default: + throw std::runtime_error("Invalid eye for drawBuffers"); + } +} + +void Framebuffer::SwapChainInfo::create(const glm::uvec2 &size) { + index = 0; + validTexture = false; + // GL_SRGB8_ALPHA8 and GL_RGBA8 appear to behave the same here. The only thing that changes the + // output gamma behavior is VRAPI_MODE_FLAG_FRONT_BUFFER_SRGB passed to vrapi_EnterVrMode + swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_SRGB8_ALPHA8, size.x, size.y, 1, 3); + length = vrapi_GetTextureSwapChainLength(swapChain); + if (!length) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); + throw std::runtime_error("Unable to create Oculus texture swap chain"); + } + + for (int i = 0; i < length; ++i) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(swapChain, i); + glBindTexture(GL_TEXTURE_2D, chainTexId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } +} + +void Framebuffer::SwapChainInfo::advance() { + index = (index + 1) % length; + validTexture = false; +} + +void Framebuffer::SwapChainInfo::bind(uint32_t target, uint32_t attachment) { + if (!validTexture) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(swapChain, index); + glFramebufferTexture(target, attachment, chainTexId, 0); + validTexture = true; } } diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.h b/libraries/oculusMobile/src/ovr/Framebuffer.h index 5127574462..4600d91534 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.h +++ b/libraries/oculusMobile/src/ovr/Framebuffer.h @@ -9,6 +9,7 @@ #include #include +#include #include @@ -20,15 +21,28 @@ public: void create(const glm::uvec2& size); void advance(); void destroy(); - void bind(); + void bind(GLenum target = GL_DRAW_FRAMEBUFFER); + void invalidate(GLenum target = GL_DRAW_FRAMEBUFFER); + void drawBuffers(ovrEye eye) const; - uint32_t _depth { 0 }; + const glm::uvec2& size() const { return _size; } + +private: uint32_t _fbo{ 0 }; - int _length{ -1 }; - int _index{ -1 }; - bool _validTexture{ false }; glm::uvec2 _size; - ovrTextureSwapChain* _swapChain{ nullptr }; + struct SwapChainInfo { + int length{ -1 }; + int index{ -1 }; + bool validTexture{ false }; + ovrTextureSwapChain* swapChain{ nullptr }; + + void create(const glm::uvec2& size); + void destroy(); + void advance(); + void bind(GLenum target, GLenum attachment); + }; + + SwapChainInfo _swapChainInfos[VRAPI_FRAME_LAYER_EYE_MAX]; }; } // namespace ovr \ No newline at end of file diff --git a/libraries/oculusMobile/src/ovr/GLContext.cpp b/libraries/oculusMobile/src/ovr/GLContext.cpp index 449ba67084..8d81df42e5 100644 --- a/libraries/oculusMobile/src/ovr/GLContext.cpp +++ b/libraries/oculusMobile/src/ovr/GLContext.cpp @@ -13,10 +13,7 @@ #include #include - -#if !defined(EGL_OPENGL_ES3_BIT_KHR) -#define EGL_OPENGL_ES3_BIT_KHR 0x0040 -#endif +#include using namespace ovr; @@ -129,7 +126,7 @@ void GLContext::doneCurrent() { eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } -bool GLContext::create(EGLDisplay display, EGLContext shareContext) { +bool GLContext::create(EGLDisplay display, EGLContext shareContext, bool noError) { this->display = display; auto config = findConfig(display); @@ -139,7 +136,9 @@ bool GLContext::create(EGLDisplay display, EGLContext shareContext) { return false; } - EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, + noError ? EGL_CONTEXT_OPENGL_NO_ERROR_KHR : EGL_NONE, EGL_TRUE, + EGL_NONE }; context = eglCreateContext(display, config, shareContext, contextAttribs); if (context == EGL_NO_CONTEXT) { diff --git a/libraries/oculusMobile/src/ovr/GLContext.h b/libraries/oculusMobile/src/ovr/GLContext.h index 04f96e8d47..5ccbf20c42 100644 --- a/libraries/oculusMobile/src/ovr/GLContext.h +++ b/libraries/oculusMobile/src/ovr/GLContext.h @@ -25,7 +25,7 @@ struct GLContext { static EGLConfig findConfig(EGLDisplay display); bool makeCurrent(); void doneCurrent(); - bool create(EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY), EGLContext shareContext = EGL_NO_CONTEXT); + bool create(EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY), EGLContext shareContext = EGL_NO_CONTEXT, bool noError = false); void destroy(); operator bool() const { return context != EGL_NO_CONTEXT; } static void initModule(); diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index 3fe3901517..a7b0f9f8ee 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -9,39 +9,190 @@ #include #include +#include +#include #include +#include +#include #include #include #include //#include + #include "GLContext.h" #include "Helpers.h" #include "Framebuffer.h" +static AAssetManager* ASSET_MANAGER = nullptr; +#define USE_BLIT_PRESENT 0 + +#if !USE_BLIT_PRESENT + + + +static std::string getTextAsset(const char* assetPath) { + if (!ASSET_MANAGER || !assetPath) { + return nullptr; + } + AAsset* asset = AAssetManager_open(ASSET_MANAGER, assetPath, AASSET_MODE_BUFFER); + if (!asset) { + return {}; + } + + auto length = AAsset_getLength(asset); + if (0 == length) { + AAsset_close(asset); + return {}; + } + + auto buffer = AAsset_getBuffer(asset); + if (!buffer) { + AAsset_close(asset); + return {}; + } + + std::string result { static_cast(buffer), static_cast(length) }; + AAsset_close(asset); + return result; +} + +static std::string getShaderInfoLog(GLuint glshader) { + std::string result; + GLint infoLength = 0; + glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength); + if (infoLength > 0) { + char* temp = new char[infoLength]; + glGetShaderInfoLog(glshader, infoLength, NULL, temp); + result = std::string(temp); + delete[] temp; + } + return result; +} + +static GLuint buildShader(GLenum shaderDomain, const char* shader) { + GLuint glshader = glCreateShader(shaderDomain); + if (!glshader) { + throw std::runtime_error("Bad shader"); + } + + glShaderSource(glshader, 1, &shader, NULL); + glCompileShader(glshader); + + GLint compiled = 0; + glGetShaderiv(glshader, GL_COMPILE_STATUS, &compiled); + + // if compilation fails + if (!compiled) { + std::string compileError = getShaderInfoLog(glshader); + glDeleteShader(glshader); + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "Shader compile error: %s", compileError.c_str()); + return 0; + } + + return glshader; +} + +static std::string getProgramInfoLog(GLuint glprogram) { + std::string result; + GLint infoLength = 0; + glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength); + if (infoLength > 0) { + char* temp = new char[infoLength]; + glGetProgramInfoLog(glprogram, infoLength, NULL, temp); + result = std::string(temp); + delete[] temp; + } + return result; +} + +static GLuint buildProgram(const char* vertex, const char* fragment) { + // A brand new program: + GLuint glprogram { 0 }, glvertex { 0 }, glfragment { 0 }; + + try { + glprogram = glCreateProgram(); + if (0 == glprogram) { + throw std::runtime_error("Failed to create program, is GL context current?"); + } + + glvertex = buildShader(GL_VERTEX_SHADER, vertex); + if (0 == glvertex) { + throw std::runtime_error("Failed to create or compile vertex shader"); + } + glAttachShader(glprogram, glvertex); + + glfragment = buildShader(GL_FRAGMENT_SHADER, fragment); + if (0 == glfragment) { + throw std::runtime_error("Failed to create or compile fragment shader"); + } + glAttachShader(glprogram, glfragment); + + GLint linked { 0 }; + glLinkProgram(glprogram); + glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); + + if (!linked) { + std::string linkErrorLog = getProgramInfoLog(glprogram); + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "Program link error: %s", linkErrorLog.c_str()); + throw std::runtime_error("Failed to link program, is the interface between the fragment and vertex shaders correct?"); + } + + + } catch(const std::runtime_error& error) { + if (0 != glprogram) { + glDeleteProgram(glprogram); + glprogram = 0; + } + } + + if (0 != glvertex) { + glDeleteShader(glvertex); + } + + if (0 != glfragment) { + glDeleteShader(glfragment); + } + + if (0 == glprogram) { + throw std::runtime_error("Failed to build program"); + } + + return glprogram; +} + +#endif using namespace ovr; static thread_local bool isRenderThread { false }; struct VrSurface : public TaskQueue { - using HandlerTask = VrHandler::HandlerTask; + using HandlerTask = ovr::VrHandler::HandlerTask; JavaVM* vm{nullptr}; jobject oculusActivity{ nullptr }; ANativeWindow* nativeWindow{ nullptr }; - VrHandler* handler{nullptr}; + ovr::VrHandler* handler{nullptr}; ovrMobile* session{nullptr}; bool resumed { false }; - GLContext vrglContext; - Framebuffer eyeFbos[2]; - uint32_t readFbo{0}; + ovr::GLContext vrglContext; + ovr::Framebuffer eyesFbo; + +#if USE_BLIT_PRESENT + GLuint readFbo { 0 }; +#else + GLuint renderProgram { 0 }; + GLuint renderVao { 0 }; +#endif std::atomic presentIndex{1}; double displayTime{0}; + // Not currently set by anything + bool noErrorContext { false }; static constexpr float EYE_BUFFER_SCALE = 1.0f; @@ -71,9 +222,19 @@ struct VrSurface : public TaskQueue { EGLContext currentContext = eglGetCurrentContext(); EGLDisplay currentDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); - vrglContext.create(currentDisplay, currentContext); + vrglContext.create(currentDisplay, currentContext, noErrorContext); vrglContext.makeCurrent(); +#if USE_BLIT_PRESENT + glGenFramebuffers(1, &readFbo); +#else + glGenVertexArrays(1, &renderVao); + const char* vertex = nullptr; + auto vertexShader = getTextAsset("shaders/present.vert"); + auto fragmentShader = getTextAsset("shaders/present.frag"); + renderProgram = buildProgram(vertexShader.c_str(), fragmentShader.c_str()); +#endif + glm::uvec2 eyeTargetSize; withEnv([&](JNIEnv* env){ ovrJava java{ vm, env, oculusActivity }; @@ -83,22 +244,22 @@ struct VrSurface : public TaskQueue { }; }); __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "QQQ Eye Size %d, %d", eyeTargetSize.x, eyeTargetSize.y); - ovr::for_each_eye([&](ovrEye eye) { - eyeFbos[eye].create(eyeTargetSize); - }); - glGenFramebuffers(1, &readFbo); + eyesFbo.create(eyeTargetSize); vrglContext.doneCurrent(); } void shutdown() { + noErrorContext = false; + // Destroy the context? } - void setHandler(VrHandler *newHandler) { + void setHandler(VrHandler *newHandler, bool noError) { withLock([&] { isRenderThread = newHandler != nullptr; if (handler != newHandler) { shutdown(); handler = newHandler; + noErrorContext = noError; init(); if (handler) { updateVrMode(); @@ -142,7 +303,6 @@ struct VrSurface : public TaskQueue { __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "vrapi_LeaveVrMode"); vrapi_SetClockLevels(session, 1, 1); vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_OFF); - vrapi_SetDisplayRefreshRate(session, 60); vrapi_LeaveVrMode(session); session = nullptr; @@ -153,16 +313,20 @@ struct VrSurface : public TaskQueue { ovrJava java{ vm, env, oculusActivity }; ovrModeParms modeParms = vrapi_DefaultModeParms(&java); modeParms.Flags |= VRAPI_MODE_FLAG_NATIVE_WINDOW; + modeParms.Flags |= VRAPI_MODE_FLAG_FRONT_BUFFER_SRGB; + if (noErrorContext) { + modeParms.Flags |= VRAPI_MODE_FLAG_CREATE_CONTEXT_NO_ERROR; + } modeParms.Display = (unsigned long long) vrglContext.display; modeParms.ShareContext = (unsigned long long) vrglContext.context; modeParms.WindowSurface = (unsigned long long) nativeWindow; session = vrapi_EnterVrMode(&modeParms); - ovrPosef trackingTransform = vrapi_GetTrackingTransform( session, VRAPI_TRACKING_TRANSFORM_SYSTEM_CENTER_EYE_LEVEL); - vrapi_SetTrackingTransform( session, trackingTransform ); - vrapi_SetPerfThread(session, VRAPI_PERF_THREAD_TYPE_RENDERER, pthread_self()); + vrapi_SetTrackingSpace( session, VRAPI_TRACKING_SPACE_LOCAL); + vrapi_SetPerfThread(session, VRAPI_PERF_THREAD_TYPE_RENDERER, gettid()); vrapi_SetClockLevels(session, 2, 4); vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_DYNAMIC); - vrapi_SetDisplayRefreshRate(session, 72); + // Generates a warning on the quest: "vrapi_SetDisplayRefreshRate: Dynamic Display Refresh Rate not supported" + // vrapi_SetDisplayRefreshRate(session, 72); }); } } @@ -171,38 +335,51 @@ struct VrSurface : public TaskQueue { void presentFrame(uint32_t sourceTexture, const glm::uvec2 &sourceSize, const ovrTracking2& tracking) { ovrLayerProjection2 layer = vrapi_DefaultLayerProjection2(); layer.HeadPose = tracking.HeadPose; + + eyesFbo.bind(); if (sourceTexture) { + eyesFbo.invalidate(); +#if USE_BLIT_PRESENT glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo); glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, sourceTexture, 0); - GLenum framebufferStatus = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); - if (GL_FRAMEBUFFER_COMPLETE != framebufferStatus) { - __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "incomplete framebuffer"); - } - } - GLenum invalidateAttachment = GL_COLOR_ATTACHMENT0; - - ovr::for_each_eye([&](ovrEye eye) { - const auto &eyeTracking = tracking.Eye[eye]; - auto &eyeFbo = eyeFbos[eye]; - const auto &destSize = eyeFbo._size; - eyeFbo.bind(); - glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, 1, &invalidateAttachment); - if (sourceTexture) { + const auto &destSize = eyesFbo.size(); + ovr::for_each_eye([&](ovrEye eye) { auto sourceWidth = sourceSize.x / 2; auto sourceX = (eye == VRAPI_EYE_LEFT) ? 0 : sourceWidth; + // Each eye blit uses a different draw buffer + eyesFbo.drawBuffers(eye); glBlitFramebuffer( sourceX, 0, sourceX + sourceWidth, sourceSize.y, 0, 0, destSize.x, destSize.y, GL_COLOR_BUFFER_BIT, GL_NEAREST); - } - eyeFbo.updateLayer(eye, layer, &eyeTracking.ProjectionMatrix); - eyeFbo.advance(); - }); - if (sourceTexture) { - glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, &invalidateAttachment); + }); + static const std::array READ_INVALIDATE_ATTACHMENTS {{ GL_COLOR_ATTACHMENT0 }}; + glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)READ_INVALIDATE_ATTACHMENTS.size(), READ_INVALIDATE_ATTACHMENTS.data()); glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0); +#else + eyesFbo.drawBuffers(VRAPI_EYE_COUNT); + const auto &destSize = eyesFbo.size(); + glViewport(0, 0, destSize.x, destSize.y); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, sourceTexture); + glBindVertexArray(renderVao); + glUseProgram(renderProgram); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glUseProgram(0); + glBindVertexArray(0); +#endif + } else { + eyesFbo.drawBuffers(VRAPI_EYE_COUNT); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); } - glFlush(); + + ovr::for_each_eye([&](ovrEye eye) { + const auto &eyeTracking = tracking.Eye[eye]; + eyesFbo.updateLayer(eye, layer, &eyeTracking.ProjectionMatrix); + }); + + eyesFbo.advance(); ovrLayerHeader2 *layerHeader = &layer.Header; ovrSubmitFrameDescription2 frameDesc = {}; @@ -227,8 +404,8 @@ bool VrHandler::vrActive() const { return SURFACE.session != nullptr; } -void VrHandler::setHandler(VrHandler* handler) { - SURFACE.setHandler(handler); +void VrHandler::setHandler(VrHandler* handler, bool noError) { + SURFACE.setHandler(handler, noError); } void VrHandler::pollTask() { @@ -314,8 +491,9 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { return JNI_VERSION_1_6; } -JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnCreate(JNIEnv* env, jobject obj) { +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject assetManager) { __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); + ASSET_MANAGER = AAssetManager_fromJava(env, assetManager); SURFACE.onCreate(env, obj); } diff --git a/libraries/oculusMobile/src/ovr/VrHandler.h b/libraries/oculusMobile/src/ovr/VrHandler.h index 3c36ee8626..46e99f7476 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.h +++ b/libraries/oculusMobile/src/ovr/VrHandler.h @@ -21,7 +21,7 @@ public: using HandlerTask = std::function; using OvrMobileTask = std::function; using OvrJavaTask = std::function; - static void setHandler(VrHandler* handler); + static void setHandler(VrHandler* handler, bool noError = false); static bool withOvrMobile(const OvrMobileTask& task); protected: diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index ce9cb20c21..6abe5c3899 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -203,11 +203,6 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { } assert(entityTreeIsLocked()); - if (_entity->getShapeType() == SHAPE_TYPE_STATIC_MESH - || (_body && _body->getCollisionShape()->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)) { - return MOTION_TYPE_STATIC; - } - if (_entity->getLocked()) { if (_entity->isMoving()) { return MOTION_TYPE_KINEMATIC; @@ -222,9 +217,8 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { } return MOTION_TYPE_DYNAMIC; } - if (_entity->isMovingRelativeToParent() || - _entity->hasActions() || - _entity->hasGrabs() || + if (_entity->hasActions() || + _entity->isMovingRelativeToParent() || _entity->hasAncestorOfType(NestableType::Avatar)) { return MOTION_TYPE_KINEMATIC; } diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index ff8c270371..9940da0b9a 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -225,13 +225,15 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const uint64_t& created, const ProceduralProgramKey key) { std::lock_guard lock(_mutex); _entityDimensions = size; _entityPosition = position; _entityOrientation = glm::mat3_cast(orientation); + _entityCreated = created; if (!_shaderPath.isEmpty()) { - auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); + auto lastModified = (uint64_t)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { QFile file(_shaderPath); file.open(QIODevice::ReadOnly); @@ -278,7 +280,10 @@ void Procedural::prepare(gpu::Batch& batch, _proceduralPipelines[key] = gpu::Pipeline::create(program, key.isTransparent() ? _transparentState : _opaqueState); - _start = usecTimestampNow(); + _lastCompile = usecTimestampNow(); + if (_firstCompile == 0) { + _firstCompile = _lastCompile; + } _frameCount = 0; recompiledShader = true; } @@ -371,7 +376,11 @@ void Procedural::setupUniforms() { _uniforms.push_back([=](gpu::Batch& batch) { _standardInputs.position = vec4(_entityPosition, 1.0f); // Minimize floating point error by doing an integer division to milliseconds, before the floating point division to seconds - _standardInputs.time = (float)((usecTimestampNow() - _start) / USECS_PER_MSEC) / MSECS_PER_SECOND; + auto now = usecTimestampNow(); + _standardInputs.timeSinceLastCompile = (float)((now - _lastCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceFirstCompile = (float)((now - _firstCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceEntityCreation = (float)((now - _entityCreated) / USECS_PER_MSEC) / MSECS_PER_SECOND; + // Date { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 8477e69afc..956cef368f 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -82,10 +82,11 @@ public: bool isReady() const; bool isEnabled() const { return _enabled; } - void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const ProceduralProgramKey key = ProceduralProgramKey()); + void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const uint64_t& created, const ProceduralProgramKey key = ProceduralProgramKey()); glm::vec4 getColor(const glm::vec4& entityColor) const; - quint64 getFadeStartTime() const { return _fadeStartTime; } + uint64_t getFadeStartTime() const { return _fadeStartTime; } bool isFading() const { return _doesFade && _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } void setDoesFade(bool doesFade) { _doesFade = doesFade; } @@ -106,9 +107,10 @@ protected: vec4 date; vec4 position; vec4 scale; - float time; + float timeSinceLastCompile; + float timeSinceFirstCompile; + float timeSinceEntityCreation; int frameCount; - vec2 _spare1; vec4 resolution[4]; mat4 orientation; }; @@ -116,9 +118,10 @@ protected: static_assert(0 == offsetof(StandardInputs, date), "ProceduralOffsets"); static_assert(16 == offsetof(StandardInputs, position), "ProceduralOffsets"); static_assert(32 == offsetof(StandardInputs, scale), "ProceduralOffsets"); - static_assert(48 == offsetof(StandardInputs, time), "ProceduralOffsets"); - static_assert(52 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); - static_assert(56 == offsetof(StandardInputs, _spare1), "ProceduralOffsets"); + static_assert(48 == offsetof(StandardInputs, timeSinceLastCompile), "ProceduralOffsets"); + static_assert(52 == offsetof(StandardInputs, timeSinceFirstCompile), "ProceduralOffsets"); + static_assert(56 == offsetof(StandardInputs, timeSinceEntityCreation), "ProceduralOffsets"); + static_assert(60 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); static_assert(64 == offsetof(StandardInputs, resolution), "ProceduralOffsets"); static_assert(128 == offsetof(StandardInputs, orientation), "ProceduralOffsets"); @@ -126,13 +129,14 @@ protected: ProceduralData _data; bool _enabled { false }; - uint64_t _start { 0 }; + uint64_t _lastCompile { 0 }; + uint64_t _firstCompile { 0 }; int32_t _frameCount { 0 }; // Rendering object descriptions, from userData QString _shaderSource; QString _shaderPath; - quint64 _shaderModified { 0 }; + uint64_t _shaderModified { 0 }; NetworkShaderPointer _networkShader; bool _shaderDirty { true }; bool _uniformsDirty { true }; @@ -152,11 +156,12 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; + uint64_t _entityCreated; private: void setupUniforms(); - mutable quint64 _fadeStartTime { 0 }; + mutable uint64_t _fadeStartTime { 0 }; mutable bool _hasStartedFade { false }; mutable bool _isFading { false }; bool _doesFade { true }; diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slh b/libraries/procedural/src/procedural/ProceduralCommon.slh index bd894a9e92..6e73534440 100644 --- a/libraries/procedural/src/procedural/ProceduralCommon.slh +++ b/libraries/procedural/src/procedural/ProceduralCommon.slh @@ -36,9 +36,11 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { // Offset 48 float globalTime; // Offset 52 - int frameCount; + float localCreatedTime; // Offset 56 - vec2 _spare1; + float entityTime; + // Offset 60 + int frameCount; // Offset 64, acts as vec4[4] for alignment purposes vec3 channelResolution[4]; // Offset 128, acts as vec4[3] for alignment purposes @@ -52,6 +54,8 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { #define iWorldPosition standardInputs.worldPosition #define iWorldScale standardInputs.worldScale #define iGlobalTime standardInputs.globalTime +#define iLocalCreatedTime standardInputs.localCreatedTime +#define iEntityTime standardInputs.entityTime #define iFrameCount standardInputs.frameCount #define iChannelResolution standardInputs.channelResolution #define iWorldOrientation standardInputs.worldOrientation diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 6eb6d531e1..53df1532dc 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -17,7 +17,7 @@ #include #include -ProceduralSkybox::ProceduralSkybox() : graphics::Skybox() { +ProceduralSkybox::ProceduralSkybox(uint64_t created) : graphics::Skybox(), _created(created) { _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource(); _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox); // Adjust the pipeline state for background using the stencil test @@ -59,7 +59,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setModelTransform(Transform()); // only for Mac auto& procedural = skybox._procedural; - procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat()); + procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(), skybox.getCreated()); skybox.prepare(batch); batch.draw(gpu::TRIANGLE_STRIP, 4); } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 5db1078a5f..a1d7ea8fa7 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -19,7 +19,7 @@ class ProceduralSkybox: public graphics::Skybox { public: - ProceduralSkybox(); + ProceduralSkybox(uint64_t created = 0); void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); } @@ -29,8 +29,11 @@ public: void render(gpu::Batch& batch, const ViewFrustum& frustum) const override; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); + uint64_t getCreated() const { return _created; } + protected: mutable Procedural _procedural; + uint64_t _created; }; typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index cfb78d6bbc..7cad337dd5 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -178,7 +178,6 @@ void CauterizedModel::updateClusterMatrices() { } } } - computeMeshPartLocalBounds(); // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); @@ -245,7 +244,7 @@ void CauterizedModel::updateRenderItems() { Transform renderTransform = modelTransform; if (useDualQuaternionSkinning) { - if (meshState.clusterDualQuaternions.size() == 1) { + if (meshState.clusterDualQuaternions.size() == 1 || meshState.clusterDualQuaternions.size() == 2) { const auto& dq = meshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), dq.getScale(), @@ -253,7 +252,7 @@ void CauterizedModel::updateRenderItems() { renderTransform = modelTransform.worldTransform(transform); } } else { - if (meshState.clusterMatrices.size() == 1) { + if (meshState.clusterMatrices.size() == 1 || meshState.clusterMatrices.size() == 2) { renderTransform = modelTransform.worldTransform(Transform(meshState.clusterMatrices[0])); } } @@ -261,7 +260,7 @@ void CauterizedModel::updateRenderItems() { renderTransform = modelTransform; if (useDualQuaternionSkinning) { - if (cauterizedMeshState.clusterDualQuaternions.size() == 1) { + if (cauterizedMeshState.clusterDualQuaternions.size() == 1 || cauterizedMeshState.clusterDualQuaternions.size() == 2) { const auto& dq = cauterizedMeshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), dq.getScale(), @@ -269,7 +268,7 @@ void CauterizedModel::updateRenderItems() { renderTransform = modelTransform.worldTransform(Transform(transform)); } } else { - if (cauterizedMeshState.clusterMatrices.size() == 1) { + if (cauterizedMeshState.clusterMatrices.size() == 1 || cauterizedMeshState.clusterMatrices.size() == 2) { renderTransform = modelTransform.worldTransform(Transform(cauterizedMeshState.clusterMatrices[0])); } } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index ea32c5ecb3..fc9310a520 100644 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -29,7 +29,7 @@ float evalOpaqueFinalAlpha(float alpha, float mapAlpha) { <@include LightingModel.slh@> void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 emissive, float occlusion, float scattering) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } @@ -42,7 +42,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 lightmap) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } @@ -54,7 +54,7 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float r } void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } _fragColor0 = vec4(color, packUnlit()); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index e322dc9d2b..2e762a0107 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -836,14 +836,6 @@ render::ShapePipelinePointer GeometryCache::getFadingShapePipeline(bool textured ); } -render::ShapePipelinePointer GeometryCache::getOpaqueShapePipeline(bool isFading) { - return isFading ? _simpleOpaqueFadePipeline : _simpleOpaquePipeline; -} - -render::ShapePipelinePointer GeometryCache::getTransparentShapePipeline(bool isFading) { - return isFading ? _simpleTransparentFadePipeline : _simpleTransparentPipeline; -} - void GeometryCache::renderShape(gpu::Batch& batch, Shape shape) { batch.setInputFormat(getSolidStreamFormat()); _shapes[shape].draw(batch); @@ -1029,7 +1021,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); auto pointCount = points.size(); auto colorCount = colors.size(); int compactColor = 0; @@ -1107,7 +1099,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); auto pointCount = points.size(); auto colorCount = colors.size(); for (auto i = 0; i < pointCount; i++) { @@ -1195,7 +1187,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); for (int i = 0; i < points.size(); i++) { glm::vec3 point = points[i]; glm::vec2 texCoord = texCoords[i]; @@ -2018,77 +2010,6 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm batch.draw(gpu::LINES, 2, 0); } - -void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, float glowIntensity, float glowWidth, int id) { - - // Disable glow lines on OSX -#ifndef Q_OS_WIN - glowIntensity = 0.0f; -#endif - - if (glowIntensity <= 0.0f) { - if (color.a >= 1.0f) { - bindSimpleProgram(batch, false, false, false, true, true); - } else { - bindSimpleProgram(batch, false, true, false, true, true); - } - renderLine(batch, p1, p2, color, id); - return; - } - - // Compile the shaders - static std::once_flag once; - std::call_once(once, [&] { - auto state = std::make_shared(); - auto program = gpu::Shader::createProgram(shader::render_utils::program::glowLine); - state->setCullMode(gpu::State::CULL_NONE); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - PrepareStencil::testMask(*state); - _glowLinePipeline = gpu::Pipeline::create(program, state); - }); - - batch.setPipeline(_glowLinePipeline); - - Vec3Pair key(p1, p2); - bool registered = (id != UNKNOWN_ID); - BatchItemDetails& details = _registeredLine3DVBOs[id]; - - // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed - if (registered && details.isCreated) { - Vec3Pair& lastKey = _lastRegisteredLine3D[id]; - if (lastKey != key) { - details.clear(); - _lastRegisteredLine3D[id] = key; - } - } - - const int NUM_VERTICES = 4; - if (!details.isCreated) { - details.isCreated = true; - details.uniformBuffer = std::make_shared(); - - struct LineData { - vec4 p1; - vec4 p2; - vec4 color; - float width; - }; - - LineData lineData { vec4(p1, 1.0f), vec4(p2, 1.0f), color, glowWidth }; - details.uniformBuffer->resize(sizeof(LineData)); - details.uniformBuffer->setSubData(0, lineData); - } - - // The shader requires no vertices, only uniforms. - batch.setUniformBuffer(0, details.uniformBuffer); - batch.draw(gpu::TRIANGLE_STRIP, NUM_VERTICES, 0); -} - void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) { static std::once_flag once; std::call_once(once, [&]() { @@ -2282,8 +2203,7 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp _unlitShader = _forwardUnlitShader; } else { _simpleShader = gpu::Shader::createProgram(simple_textured); - // Use the forward pipeline for both here, otherwise transparents will be unlit - _transparentShader = gpu::Shader::createProgram(forward_simple_textured_transparent); + _transparentShader = gpu::Shader::createProgram(simple_transparent_textured); _unlitShader = gpu::Shader::createProgram(simple_textured_unlit); } }); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 5c4cc67adf..4ff061786a 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -185,12 +185,6 @@ public: render::ShapePipelinePointer getTransparentShapePipeline() { assert(_simpleTransparentPipeline != nullptr); return _simpleTransparentPipeline; } render::ShapePipelinePointer getForwardOpaqueShapePipeline() { assert(_forwardSimpleOpaquePipeline != nullptr); return _forwardSimpleOpaquePipeline; } render::ShapePipelinePointer getForwardTransparentShapePipeline() { assert(_forwardSimpleTransparentPipeline != nullptr); return _forwardSimpleTransparentPipeline; } - render::ShapePipelinePointer getOpaqueFadeShapePipeline() { assert(_simpleOpaqueFadePipeline != nullptr); return _simpleOpaqueFadePipeline; } - render::ShapePipelinePointer getTransparentFadeShapePipeline() { assert(_simpleTransparentFadePipeline != nullptr); return _simpleTransparentFadePipeline; } - render::ShapePipelinePointer getOpaqueShapePipeline(bool isFading); - render::ShapePipelinePointer getTransparentShapePipeline(bool isFading); - render::ShapePipelinePointer getWireShapePipeline() { assert(_simpleWirePipeline != nullptr); return GeometryCache::_simpleWirePipeline; } - // Static (instanced) geometry void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); @@ -317,12 +311,6 @@ public: void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color1, const glm::vec4& color2, int id); - void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, float glowIntensity, float glowWidth, int id); - - void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color, int id) - { renderGlowLine(batch, p1, p2, color, 1.0f, 0.05f, id); } - void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, int id) { renderDashedLine(batch, start, end, color, 0.05f, 0.025f, id); } @@ -385,6 +373,7 @@ public: const ShapeData * getShapeData(Shape shape) const; graphics::MeshPointer meshFromShape(Shape geometryShape, glm::vec3 color); + private: GeometryCache(); @@ -483,7 +472,6 @@ private: static render::ShapePipelinePointer _simpleOpaqueFadePipeline; static render::ShapePipelinePointer _simpleTransparentFadePipeline; static render::ShapePipelinePointer _simpleWirePipeline; - gpu::PipelinePointer _glowLinePipeline; static QHash _simplePrograms; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index a8d3e504f1..89a9c7cf47 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -247,7 +247,7 @@ void Model::updateRenderItems() { Transform renderTransform = modelTransform; if (useDualQuaternionSkinning) { - if (meshState.clusterDualQuaternions.size() == 1) { + if (meshState.clusterDualQuaternions.size() == 1 || meshState.clusterDualQuaternions.size() == 2) { const auto& dq = meshState.clusterDualQuaternions[0]; Transform transform(dq.getRotation(), dq.getScale(), @@ -255,7 +255,7 @@ void Model::updateRenderItems() { renderTransform = modelTransform.worldTransform(Transform(transform)); } } else { - if (meshState.clusterMatrices.size() == 1) { + if (meshState.clusterMatrices.size() == 1 || meshState.clusterMatrices.size() == 2) { renderTransform = modelTransform.worldTransform(Transform(meshState.clusterMatrices[0])); } } @@ -1346,19 +1346,14 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - render::Transaction transaction; - auto meshStates = _meshStates; - for (auto renderItem : _modelMeshRenderItemIDs) { - transaction.updateItem(renderItem, [this, meshStates](ModelMeshPartPayload& data) { - const Model::MeshState& state = meshStates.at(data._meshIndex); - if (_useDualQuaternionSkinning) { - data.computeAdjustedLocalBound(state.clusterDualQuaternions); - } else { - data.computeAdjustedLocalBound(state.clusterMatrices); - } - }); + for (auto& part : _modelMeshRenderItems) { + const Model::MeshState& state = _meshStates.at(part->_meshIndex); + if (_useDualQuaternionSkinning) { + part->computeAdjustedLocalBound(state.clusterDualQuaternions); + } else { + part->computeAdjustedLocalBound(state.clusterMatrices); + } } - AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); } // virtual @@ -1391,7 +1386,6 @@ void Model::updateClusterMatrices() { } } } - computeMeshPartLocalBounds(); // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); @@ -1648,7 +1642,7 @@ using packBlendshapeOffsetTo = void(glm::uvec4& packed, const BlendshapeOffsetUn void packBlendshapeOffsetTo_Pos_F32_3xSN10_Nor_3xSN10_Tan_3xSN10(glm::uvec4& packed, const BlendshapeOffsetUnpacked& unpacked) { float len = glm::compMax(glm::abs(unpacked.positionOffset)); glm::vec3 normalizedPos(unpacked.positionOffset); - if (len > 1.0f) { + if (len > 0.0f) { normalizedPos /= len; } else { len = 1.0f; diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 385e384efe..b1a62625b2 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -95,7 +95,11 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs& // Setup lighting model for all items; batch.setUniformBuffer(ru::Buffer::LightModel, lightingModel->getParametersBuffer()); - renderShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + if (_opaquePass) { + renderStateSortShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + } else { + renderShapes(renderContext, _shapePlumber, inItems, _maxDrawn); + } args->_batch = nullptr; }); } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 089d267711..ea2b05a6fa 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -216,8 +216,8 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawHazeDeferred", drawHazeInputs); // Render transparent objects forward in LightingBuffer - const auto transparentsInputs = DrawDeferred::Inputs(transparents, hazeFrame, lightFrame, lightingModel, lightClusters, shadowFrame, jitter).asVarying(); - task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); + const auto transparentsInputs = RenderTransparentDeferred::Inputs(transparents, hazeFrame, lightFrame, lightingModel, lightClusters, shadowFrame, jitter).asVarying(); + task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); const auto outlineRangeTimer = task.addJob("BeginHighlightRangeTimer", "Highlight"); @@ -436,7 +436,7 @@ void RenderDeferredTaskDebug::build(JobModel& task, const render::Varying& input } -void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs) { +void RenderTransparentDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); @@ -453,7 +453,7 @@ void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& RenderArgs* args = renderContext->args; - gpu::doInBatch("DrawDeferred::run", args->_context, [&](gpu::Batch& batch) { + gpu::doInBatch("RenderTransparentDeferred::run", args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; // Setup camera, projection and viewport for all items diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 0a188ec3a6..3eb1153928 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -19,7 +19,7 @@ #include "LightClusters.h" #include "RenderShadowTask.h" -class DrawDeferredConfig : public render::Job::Config { +class RenderTransparentDeferredConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY newStats) Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) @@ -41,13 +41,13 @@ protected: int _numDrawn{ 0 }; }; -class DrawDeferred { +class RenderTransparentDeferred { public: using Inputs = render::VaryingSet7; - using Config = DrawDeferredConfig; - using JobModel = render::Job::ModelI; + using Config = RenderTransparentDeferredConfig; + using JobModel = render::Job::ModelI; - DrawDeferred(render::ShapePlumberPointer shapePlumber) + RenderTransparentDeferred(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} void configure(const Config& config) { _maxDrawn = config.maxDrawn; } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 73692b41c2..0bc117bdb9 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -98,7 +98,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw opaques forward const auto opaqueInputs = DrawForward::Inputs(opaques, lightingModel).asVarying(); - task.addJob("DrawOpaques", opaqueInputs, shapePlumber); + task.addJob("DrawOpaques", opaqueInputs, shapePlumber, true); // Similar to light stage, background stage has been filled by several potential render items and resolved for the frame in this job const auto backgroundInputs = DrawBackgroundStage::Inputs(lightingModel, backgroundFrame).asVarying(); @@ -106,7 +106,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw transparent objects forward const auto transparentInputs = DrawForward::Inputs(transparents, lightingModel).asVarying(); - task.addJob("DrawTransparents", transparentInputs, shapePlumber); + task.addJob("DrawTransparents", transparentInputs, shapePlumber, false); // Layered const auto nullJitter = Varying(glm::vec2(0.0f, 0.0f)); @@ -261,7 +261,11 @@ void DrawForward::run(const RenderContextPointer& renderContext, const Inputs& i args->_globalShapeKey = globalKey._flags.to_ulong(); // Render items - renderStateSortShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + if (_opaquePass) { + renderStateSortShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + } else { + renderShapes(renderContext, _shapePlumber, inItems, -1, globalKey); + } args->_batch = nullptr; args->_globalShapeKey = 0; diff --git a/libraries/render-utils/src/RenderForwardTask.h b/libraries/render-utils/src/RenderForwardTask.h index 85b51ad5fa..40d004ddb2 100755 --- a/libraries/render-utils/src/RenderForwardTask.h +++ b/libraries/render-utils/src/RenderForwardTask.h @@ -76,12 +76,13 @@ public: using Inputs = render::VaryingSet2; using JobModel = render::Job::ModelI; - DrawForward(const render::ShapePlumberPointer& shapePlumber) : _shapePlumber(shapePlumber) {} + DrawForward(const render::ShapePlumberPointer& shapePlumber, bool opaquePass) : _shapePlumber(shapePlumber), _opaquePass(opaquePass) {} void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); private: render::ShapePlumberPointer _shapePlumber; + bool _opaquePass; }; #endif // hifi_RenderForwardTask_h diff --git a/libraries/render-utils/src/TextRenderer3D.cpp b/libraries/render-utils/src/TextRenderer3D.cpp index 93edc4217d..8ef0dc0d73 100644 --- a/libraries/render-utils/src/TextRenderer3D.cpp +++ b/libraries/render-utils/src/TextRenderer3D.cpp @@ -67,11 +67,11 @@ float TextRenderer3D::getFontSize() const { } void TextRenderer3D::draw(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4& color, - const glm::vec2& bounds, bool forwardRendered) { + const glm::vec2& bounds, bool layered) { // The font does all the OpenGL work if (_font) { _color = color; - _font->drawString(batch, _drawInfo, str, _color, _effectType, { x, y }, bounds, forwardRendered); + _font->drawString(batch, _drawInfo, str, _color, _effectType, { x, y }, bounds, layered); } } diff --git a/libraries/render-utils/src/TextRenderer3D.h b/libraries/render-utils/src/TextRenderer3D.h index b6475ab0ed..6c91411e1d 100644 --- a/libraries/render-utils/src/TextRenderer3D.h +++ b/libraries/render-utils/src/TextRenderer3D.h @@ -39,7 +39,7 @@ public: float getFontSize() const; // Pixel size void draw(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4& color = glm::vec4(1.0f), - const glm::vec2& bounds = glm::vec2(-1.0f), bool forwardRendered = false); + const glm::vec2& bounds = glm::vec2(-1.0f), bool layered = false); private: TextRenderer3D(const char* family, float pointSize, int weight = -1, bool italic = false, diff --git a/libraries/render-utils/src/forward_sdf_text3D.slf b/libraries/render-utils/src/forward_sdf_text3D.slf new file mode 100644 index 0000000000..09b10c0c42 --- /dev/null +++ b/libraries/render-utils/src/forward_sdf_text3D.slf @@ -0,0 +1,57 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// sdf_text3D_transparent.frag +// fragment shader +// +// Created by Bradley Austin Davis on 2015-02-04 +// Based on fragment shader code from +// https://github.com/paulhoux/Cinder-Samples/blob/master/TextRendering/include/text/Text.cpp +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +<@include DefaultMaterials.slh@> + +<@include ForwardGlobalLight.slh@> +<$declareEvalSkyboxGlobalColor()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +<@include render-utils/ShaderConstants.h@> + +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> + +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; +layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; +#define _texCoord0 _texCoord01.xy +#define _texCoord1 _texCoord01.zw + +layout(location=0) out vec4 _fragColor0; + +void main() { + float a = evalSDFSuperSampled(_texCoord0); + + float alpha = a * _color.a; + if (alpha <= 0.0) { + discard; + } + + TransformCamera cam = getTransformCamera(); + vec3 fragPosition = _positionES.xyz; + + _fragColor0 = vec4(evalSkyboxGlobalColor( + cam._viewInverse, + 1.0, + DEFAULT_OCCLUSION, + fragPosition, + normalize(_normalWS), + _color.rgb, + DEFAULT_FRESNEL, + DEFAULT_METALLIC, + DEFAULT_ROUGHNESS), + 1.0); +} \ No newline at end of file diff --git a/libraries/render-utils/src/forward_simple_textured.slf b/libraries/render-utils/src/forward_simple_textured.slf index ca31550b40..373ab13d1a 100644 --- a/libraries/render-utils/src/forward_simple_textured.slf +++ b/libraries/render-utils/src/forward_simple_textured.slf @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include gpu/Color.slh@> <@include DefaultMaterials.slh@> <@include ForwardGlobalLight.slh@> @@ -21,10 +22,8 @@ <@include render-utils/ShaderConstants.h@> -// the albedo texture LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; @@ -36,7 +35,11 @@ layout(location=0) out vec4 _fragColor0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0); - float colorAlpha = _color.a * texel.a; + texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); + vec3 albedo = _color.xyz * texel.xyz; + float metallic = DEFAULT_METALLIC; + + vec3 fresnel = getFresnelF0(metallic, albedo); TransformCamera cam = getTransformCamera(); vec3 fragPosition = _positionES.xyz; @@ -47,9 +50,9 @@ void main(void) { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - _color.rgb * texel.rgb, - DEFAULT_FRESNEL, - DEFAULT_METALLIC, + albedo, + fresnel, + metallic, DEFAULT_ROUGHNESS), 1.0); } \ No newline at end of file diff --git a/libraries/render-utils/src/forward_simple_textured_transparent.slf b/libraries/render-utils/src/forward_simple_textured_transparent.slf index 11d51bbd78..1b5047507b 100644 --- a/libraries/render-utils/src/forward_simple_textured_transparent.slf +++ b/libraries/render-utils/src/forward_simple_textured_transparent.slf @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include gpu/Color.slh@> <@include DefaultMaterials.slh@> <@include ForwardGlobalLight.slh@> @@ -21,22 +22,25 @@ <@include render-utils/ShaderConstants.h@> -// the albedo texture LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw -layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=0) out vec4 _fragColor0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0); - float colorAlpha = _color.a * texel.a; + texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); + vec3 albedo = _color.xyz * texel.xyz; + float alpha = _color.a * texel.a; + float metallic = DEFAULT_METALLIC; + + vec3 fresnel = getFresnelF0(metallic, albedo); TransformCamera cam = getTransformCamera(); vec3 fragPosition = _positionES.xyz; @@ -47,10 +51,10 @@ void main(void) { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - _color.rgb * texel.rgb, - DEFAULT_FRESNEL, - DEFAULT_METALLIC, + albedo, + fresnel, + metallic, DEFAULT_EMISSIVE, - DEFAULT_ROUGHNESS, colorAlpha), - colorAlpha); + DEFAULT_ROUGHNESS, alpha), + alpha); } \ No newline at end of file diff --git a/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp b/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp new file mode 100644 index 0000000000..3eea3a0da0 --- /dev/null +++ b/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp @@ -0,0 +1 @@ +VERTEX sdf_text3D diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf index b070fc44cf..91c73e9eec 100644 --- a/libraries/render-utils/src/sdf_text3D.slf +++ b/libraries/render-utils/src/sdf_text3D.slf @@ -13,54 +13,22 @@ <@include DeferredBufferWrite.slh@> <@include render-utils/ShaderConstants.h@> -LAYOUT(binding=0) uniform sampler2D Font; +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> -struct TextParams { - vec4 color; - vec4 outline; -}; - -LAYOUT(binding=0) uniform textParamsBuffer { - TextParams params; -}; - -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw -#define TAA_TEXTURE_LOD_BIAS -3.0 - -const float interiorCutoff = 0.8; -const float outlineExpansion = 0.2; -const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); - -float evalSDF(vec2 texCoord) { - // retrieve signed distance - float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; - sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); - - // Rely on TAA for anti-aliasing - return step(0.5, sdf); -} - void main() { - vec2 dxTexCoord = dFdx(_texCoord0) * 0.5 * taaBias; - vec2 dyTexCoord = dFdy(_texCoord0) * 0.5 * taaBias; - - // Perform 4x supersampling for anisotropic filtering - float a; - a = evalSDF(_texCoord0); - a += evalSDF(_texCoord0 + dxTexCoord); - a += evalSDF(_texCoord0 + dyTexCoord); - a += evalSDF(_texCoord0 + dxTexCoord + dyTexCoord); - a *= 0.25; + float a = evalSDFSuperSampled(_texCoord0); packDeferredFragment( normalize(_normalWS), - a * params.color.a, - params.color.rgb, + a, + _color.rgb, DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, diff --git a/libraries/render-utils/src/sdf_text3D.slh b/libraries/render-utils/src/sdf_text3D.slh new file mode 100644 index 0000000000..3297596efd --- /dev/null +++ b/libraries/render-utils/src/sdf_text3D.slh @@ -0,0 +1,63 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> + +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gondelman on 3/15/19 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +!> +<@if not SDF_TEXT3D_SLH@> +<@def SDF_TEXT3D_SLH@> + +LAYOUT(binding=0) uniform sampler2D Font; + +struct TextParams { + vec4 color; + vec4 outline; +}; + +LAYOUT(binding=0) uniform textParamsBuffer { + TextParams params; +}; + +<@func declareEvalSDFSuperSampled()@> + +#define TAA_TEXTURE_LOD_BIAS -3.0 + +const float interiorCutoff = 0.8; +const float outlineExpansion = 0.2; +const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); + +float evalSDF(vec2 texCoord) { + // retrieve signed distance + float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; + sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); + + // Rely on TAA for anti-aliasing + return step(0.5, sdf); +} + +float evalSDFSuperSampled(vec2 texCoord) { + vec2 dxTexCoord = dFdx(texCoord) * 0.5 * taaBias; + vec2 dyTexCoord = dFdy(texCoord) * 0.5 * taaBias; + + // Perform 4x supersampling for anisotropic filtering + float a; + a = evalSDF(texCoord); + a += evalSDF(texCoord + dxTexCoord); + a += evalSDF(texCoord + dyTexCoord); + a += evalSDF(texCoord + dxTexCoord + dyTexCoord); + a *= 0.25; + + return a; +} + +<@endfunc@> + +<@endif@> + diff --git a/libraries/render-utils/src/sdf_text3D.slv b/libraries/render-utils/src/sdf_text3D.slv index 5f4df86d56..274e09e6ad 100644 --- a/libraries/render-utils/src/sdf_text3D.slv +++ b/libraries/render-utils/src/sdf_text3D.slv @@ -11,18 +11,23 @@ // <@include gpu/Inputs.slh@> -<@include gpu/Transform.slh@> +<@include gpu/Color.slh@> <@include render-utils/ShaderConstants.h@> +<@include gpu/Transform.slh@> <$declareStandardTransform()$> +<@include sdf_text3D.slh@> + // the interpolated normal -layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; -layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; layout(location=RENDER_UTILS_ATTR_POSITION_ES) out vec4 _positionES; +layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; void main() { _texCoord01.xy = inTexCoord0.xy; + _color = color_sRGBAToLinear(params.color); // standard transform TransformCamera cam = getTransformCamera(); diff --git a/libraries/render-utils/src/sdf_text3D_transparent.slf b/libraries/render-utils/src/sdf_text3D_transparent.slf index 311c849915..c4a80091de 100644 --- a/libraries/render-utils/src/sdf_text3D_transparent.slf +++ b/libraries/render-utils/src/sdf_text3D_transparent.slf @@ -20,53 +20,22 @@ <@include render-utils/ShaderConstants.h@> -LAYOUT(binding=0) uniform sampler2D Font; - -struct TextParams { - vec4 color; - vec4 outline; -}; - -LAYOUT(binding=0) uniform textParamsBuffer { - TextParams params; -}; +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw layout(location=0) out vec4 _fragColor0; -#define TAA_TEXTURE_LOD_BIAS -3.0 - -const float interiorCutoff = 0.8; -const float outlineExpansion = 0.2; -const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); - -float evalSDF(vec2 texCoord) { - // retrieve signed distance - float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; - sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); - - // Rely on TAA for anti-aliasing - return step(0.5, sdf); -} - void main() { - vec2 dxTexCoord = dFdx(_texCoord0) * 0.5 * taaBias; - vec2 dyTexCoord = dFdy(_texCoord0) * 0.5 * taaBias; + float a = evalSDFSuperSampled(_texCoord0); - // Perform 4x supersampling for anisotropic filtering - float a; - a = evalSDF(_texCoord0); - a += evalSDF(_texCoord0 + dxTexCoord); - a += evalSDF(_texCoord0 + dyTexCoord); - a += evalSDF(_texCoord0 + dxTexCoord + dyTexCoord); - a *= 0.25; - - float alpha = a * params.color.a; + float alpha = a * _color.a; if (alpha <= 0.0) { discard; } @@ -80,7 +49,7 @@ void main() { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - params.color.rgb, + _color.rgb, DEFAULT_FRESNEL, DEFAULT_METALLIC, DEFAULT_EMISSIVE, diff --git a/libraries/render-utils/src/simple.slv b/libraries/render-utils/src/simple.slv index 0dd4e55f26..460ed53281 100644 --- a/libraries/render-utils/src/simple.slv +++ b/libraries/render-utils/src/simple.slv @@ -19,7 +19,6 @@ <@include render-utils/ShaderConstants.h@> -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_NORMAL_MS) out vec3 _normalMS; layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color; diff --git a/libraries/render-utils/src/simple_opaque_web_browser.slf b/libraries/render-utils/src/simple_opaque_web_browser.slf index 36b0c825ad..df789ee22b 100644 --- a/libraries/render-utils/src/simple_opaque_web_browser.slf +++ b/libraries/render-utils/src/simple_opaque_web_browser.slf @@ -28,7 +28,7 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord1 _texCoord01.zw void main(void) { - vec4 texel = texture(originalTexture, _texCoord0.st); + vec4 texel = texture(originalTexture, _texCoord0); texel = color_sRGBAToLinear(texel); packDeferredFragmentUnlit(normalize(_normalWS), 1.0, _color.rgb * texel.rgb); } diff --git a/libraries/render-utils/src/simple_transparent_textured.slf b/libraries/render-utils/src/simple_transparent_textured.slf index bd29ff2ec9..9f8a88c7c2 100644 --- a/libraries/render-utils/src/simple_transparent_textured.slf +++ b/libraries/render-utils/src/simple_transparent_textured.slf @@ -11,31 +11,50 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include DefaultMaterials.slh@> <@include gpu/Color.slh@> -<@include DeferredBufferWrite.slh@> - <@include render-utils/ShaderConstants.h@> -// the albedo texture +<@include DeferredGlobalLight.slh@> +<$declareEvalGlobalLightingAlphaBlendedWithHaze()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw +layout(location=0) out vec4 _fragColor0; + void main(void) { vec4 texel = texture(originalTexture, _texCoord0); texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); - texel.rgb *= _color.rgb; - texel.a *= abs(_color.a); + vec3 albedo = _color.xyz * texel.xyz; + float alpha = _color.a * texel.a; + float metallic = DEFAULT_METALLIC; - packDeferredFragmentTranslucent( + vec3 fresnel = getFresnelF0(metallic, albedo); + + TransformCamera cam = getTransformCamera(); + vec3 fragPosition = _positionES.xyz; + + _fragColor0 = vec4(evalGlobalLightingAlphaBlendedWithHaze( + cam._viewInverse, + 1.0, + DEFAULT_OCCLUSION, + fragPosition, normalize(_normalWS), - texel.a, - texel.rgb, - DEFAULT_ROUGHNESS); + albedo, + fresnel, + metallic, + DEFAULT_EMISSIVE, + DEFAULT_ROUGHNESS, alpha), + alpha); } \ No newline at end of file diff --git a/libraries/render-utils/src/simple_transparent_web_browser.slf b/libraries/render-utils/src/simple_transparent_web_browser.slf index 1d5aad0914..599fd3d87f 100644 --- a/libraries/render-utils/src/simple_transparent_web_browser.slf +++ b/libraries/render-utils/src/simple_transparent_web_browser.slf @@ -28,11 +28,11 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord1 _texCoord01.zw void main(void) { - vec4 texel = texture(originalTexture, _texCoord0.st); + vec4 texel = texture(originalTexture, _texCoord0); texel = color_sRGBAToLinear(texel); packDeferredFragmentTranslucent( normalize(_normalWS), - _color.a, + _color.a * texel.a, _color.rgb * texel.rgb, DEFAULT_ROUGHNESS); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index e0e99da020..364e24c5ac 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -13,6 +13,8 @@ #include "FontFamilies.h" #include "../StencilMaskPass.h" +#include "DisableDeferred.h" + static std::mutex fontMutex; struct TextureVertex { @@ -221,25 +223,43 @@ void Font::setupGPU() { // Setup render pipeline { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D); - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMaskDrawShape(*state); - _pipeline = gpu::Pipeline::create(program, state); + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::forward_sdf_text3D); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShape(*state); + _layeredPipeline = gpu::Pipeline::create(program, state); + } - gpu::ShaderPointer programTransparent = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D_transparent); - auto transparentState = std::make_shared(); - transparentState->setCullMode(gpu::State::CULL_BACK); - transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); - transparentState->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*transparentState); - _transparentPipeline = gpu::Pipeline::create(programTransparent, transparentState); + if (DISABLE_DEFERRED) { + _pipeline = _layeredPipeline; + } else { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShape(*state); + _pipeline = gpu::Pipeline::create(program, state); + } + + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D_transparent); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); + _transparentPipeline = gpu::Pipeline::create(program, state); + } } // Sanity checks @@ -343,7 +363,7 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm } void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const QString& str, const glm::vec4& color, - EffectType effectType, const glm::vec2& origin, const glm::vec2& bounds, bool forwardRendered) { + EffectType effectType, const glm::vec2& origin, const glm::vec2& bounds, bool layered) { if (str == "") { return; } @@ -370,7 +390,7 @@ void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const QString } // need the gamma corrected color here - batch.setPipeline(forwardRendered || (color.a < 1.0f) ? _transparentPipeline : _pipeline); + batch.setPipeline(color.a < 1.0f ? _transparentPipeline : (layered ? _layeredPipeline : _pipeline)); batch.setInputFormat(_format); batch.setInputBuffer(0, drawInfo.verticesBuffer, 0, _format->getChannels().at(0)._stride); batch.setResourceTexture(render_utils::slot::texture::TextFont, _texture); diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 26cc4e46c3..28af5bac43 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -46,7 +46,7 @@ public: // Render string to batch void drawString(gpu::Batch& batch, DrawInfo& drawInfo, const QString& str, const glm::vec4& color, EffectType effectType, - const glm::vec2& origin, const glm::vec2& bound, bool forwardRendered); + const glm::vec2& origin, const glm::vec2& bound, bool layered); static Pointer load(const QString& family); @@ -81,6 +81,7 @@ private: // gpu structures gpu::PipelinePointer _pipeline; + gpu::PipelinePointer _layeredPipeline; gpu::PipelinePointer _transparentPipeline; gpu::TexturePointer _texture; gpu::Stream::FormatPointer _format; diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 1850261c99..16d1d49d9f 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -47,6 +47,10 @@ void Transaction::queryTransitionOnItem(ItemID id, TransitionQueryFunc func) { _queriedTransitions.emplace_back(id, func); } +void Transaction::transitionFinishedOperator(ItemID id, TransitionFinishedFunc func) { + _transitionFinishedOperators.emplace_back(id, func); +} + void Transaction::updateItem(ItemID id, const UpdateFunctorPointer& functor) { _updatedItems.emplace_back(id, functor); } @@ -75,6 +79,7 @@ void Transaction::reserve(const std::vector& transactionContainer) size_t addedTransitionsCount = 0; size_t queriedTransitionsCount = 0; size_t reAppliedTransitionsCount = 0; + size_t transitionFinishedOperatorsCount = 0; size_t highlightResetsCount = 0; size_t highlightRemovesCount = 0; size_t highlightQueriesCount = 0; @@ -85,6 +90,7 @@ void Transaction::reserve(const std::vector& transactionContainer) updatedItemsCount += transaction._updatedItems.size(); resetSelectionsCount += transaction._resetSelections.size(); addedTransitionsCount += transaction._addedTransitions.size(); + transitionFinishedOperatorsCount += transaction._transitionFinishedOperators.size(); queriedTransitionsCount += transaction._queriedTransitions.size(); reAppliedTransitionsCount += transaction._reAppliedTransitions.size(); highlightResetsCount += transaction._highlightResets.size(); @@ -99,6 +105,7 @@ void Transaction::reserve(const std::vector& transactionContainer) _addedTransitions.reserve(addedTransitionsCount); _queriedTransitions.reserve(queriedTransitionsCount); _reAppliedTransitions.reserve(reAppliedTransitionsCount); + _transitionFinishedOperators.reserve(transitionFinishedOperatorsCount); _highlightResets.reserve(highlightResetsCount); _highlightRemoves.reserve(highlightRemovesCount); _highlightQueries.reserve(highlightQueriesCount); @@ -142,6 +149,7 @@ void Transaction::merge(Transaction&& transaction) { moveElements(_resetSelections, transaction._resetSelections); moveElements(_addedTransitions, transaction._addedTransitions); moveElements(_queriedTransitions, transaction._queriedTransitions); + moveElements(_transitionFinishedOperators, transaction._transitionFinishedOperators); moveElements(_reAppliedTransitions, transaction._reAppliedTransitions); moveElements(_highlightResets, transaction._highlightResets); moveElements(_highlightRemoves, transaction._highlightRemoves); @@ -156,6 +164,7 @@ void Transaction::merge(const Transaction& transaction) { copyElements(_addedTransitions, transaction._addedTransitions); copyElements(_queriedTransitions, transaction._queriedTransitions); copyElements(_reAppliedTransitions, transaction._reAppliedTransitions); + copyElements(_transitionFinishedOperators, transaction._transitionFinishedOperators); copyElements(_highlightResets, transaction._highlightResets); copyElements(_highlightRemoves, transaction._highlightRemoves); copyElements(_highlightQueries, transaction._highlightQueries); @@ -168,6 +177,7 @@ void Transaction::clear() { _resetSelections.clear(); _addedTransitions.clear(); _queriedTransitions.clear(); + _transitionFinishedOperators.clear(); _reAppliedTransitions.clear(); _highlightResets.clear(); _highlightRemoves.clear(); @@ -271,6 +281,7 @@ void Scene::processTransactionFrame(const Transaction& transaction) { transitionItems(transaction._addedTransitions); reApplyTransitions(transaction._reAppliedTransitions); queryTransitionItems(transaction._queriedTransitions); + resetTransitionFinishedOperator(transaction._transitionFinishedOperators); // Update the numItemsAtomic counter AFTER the pending changes went through _numAllocatedItems.exchange(maxID); @@ -394,7 +405,7 @@ void Scene::transitionItems(const Transaction::TransitionAdds& transactions) { // Only remove if: // transitioning to something other than none or we're transitioning to none from ELEMENT_LEAVE_DOMAIN or USER_LEAVE_DOMAIN const auto& oldTransitionType = transitionStage->getTransition(transitionId).eventType; - if (transitionType != Transition::NONE || !(oldTransitionType == Transition::ELEMENT_LEAVE_DOMAIN || oldTransitionType == Transition::USER_LEAVE_DOMAIN)) { + if (transitionType != oldTransitionType) { resetItemTransition(itemId); } } @@ -440,6 +451,23 @@ void Scene::queryTransitionItems(const Transaction::TransitionQueries& transacti } } +void Scene::resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& operators) { + for (auto& finishedOperator : operators) { + auto itemId = std::get<0>(finishedOperator); + const auto& item = _items[itemId]; + auto func = std::get<1>(finishedOperator); + + if (item.exist() && func != nullptr) { + TransitionStage::Index transitionId = item.getTransitionId(); + if (!TransitionStage::isIndexInvalid(transitionId)) { + _transitionFinishedOperatorMap[transitionId].emplace_back(func); + } else if (func) { + func(); + } + } + } +} + void Scene::resetHighlights(const Transaction::HighlightResets& transactions) { auto outlineStage = getStage(HighlightStage::getName()); if (outlineStage) { @@ -526,9 +554,18 @@ void Scene::setItemTransition(ItemID itemId, Index transitionId) { void Scene::resetItemTransition(ItemID itemId) { auto& item = _items[itemId]; - if (!render::TransitionStage::isIndexInvalid(item.getTransitionId())) { + TransitionStage::Index transitionId = item.getTransitionId(); + if (!render::TransitionStage::isIndexInvalid(transitionId)) { auto transitionStage = getStage(TransitionStage::getName()); - transitionStage->removeTransition(item.getTransitionId()); + + auto finishedOperators = _transitionFinishedOperatorMap[transitionId]; + for (auto finishedOperator : finishedOperators) { + if (finishedOperator) { + finishedOperator(); + } + } + _transitionFinishedOperatorMap.erase(transitionId); + transitionStage->removeTransition(transitionId); setItemTransition(itemId, render::TransitionStage::INVALID_INDEX); } } @@ -587,4 +624,4 @@ void Scene::resetStage(const Stage::Name& name, const StagePointer& stage) { } else { (*found).second = stage; } -} \ No newline at end of file +} diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index f00c74775d..08fbf33bcd 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -32,12 +32,15 @@ class Scene; // These changes must be expressed through the corresponding command from the Transaction // THe Transaction is then queued on the Scene so all the pending transactions can be consolidated and processed at the time // of updating the scene before it s rendered. -// +// + + class Transaction { friend class Scene; public: typedef std::function TransitionQueryFunc; + typedef std::function TransitionFinishedFunc; typedef std::function SelectionHighlightQueryFunc; Transaction() {} @@ -52,6 +55,7 @@ public: void removeTransitionFromItem(ItemID id); void reApplyTransitionToItem(ItemID id); void queryTransitionOnItem(ItemID id, TransitionQueryFunc func); + void transitionFinishedOperator(ItemID id, TransitionFinishedFunc func); template void updateItem(ItemID id, std::function func) { updateItem(id, std::make_shared>(func)); @@ -84,6 +88,7 @@ protected: using Update = std::tuple; using TransitionAdd = std::tuple; using TransitionQuery = std::tuple; + using TransitionFinishedOperator = std::tuple; using TransitionReApply = ItemID; using SelectionReset = Selection; using HighlightReset = std::tuple; @@ -95,6 +100,7 @@ protected: using Updates = std::vector; using TransitionAdds = std::vector; using TransitionQueries = std::vector; + using TransitionFinishedOperators = std::vector; using TransitionReApplies = std::vector; using SelectionResets = std::vector; using HighlightResets = std::vector; @@ -107,6 +113,7 @@ protected: TransitionAdds _addedTransitions; TransitionQueries _queriedTransitions; TransitionReApplies _reAppliedTransitions; + TransitionFinishedOperators _transitionFinishedOperators; SelectionResets _resetSelections; HighlightResets _highlightResets; HighlightRemoves _highlightRemoves; @@ -208,6 +215,7 @@ protected: ItemIDSet _masterNonspatialSet; void resetItems(const Transaction::Resets& transactions); + void resetTransitionFinishedOperator(const Transaction::TransitionFinishedOperators& transactions); void removeItems(const Transaction::Removes& transactions); void updateItems(const Transaction::Updates& transactions); void transitionItems(const Transaction::TransitionAdds& transactions); @@ -223,6 +231,8 @@ protected: mutable std::mutex _selectionsMutex; // mutable so it can be used in the thread safe getSelection const method SelectionMap _selections; + std::unordered_map> _transitionFinishedOperatorMap; + void resetSelections(const Transaction::SelectionResets& transactions); // More actions coming to selections soon: // void removeFromSelection(const Selection& selection); diff --git a/libraries/render/src/render/Transition.h b/libraries/render/src/render/Transition.h index 30bda8aa2a..eca41e9d6c 100644 --- a/libraries/render/src/render/Transition.h +++ b/libraries/render/src/render/Transition.h @@ -50,4 +50,4 @@ namespace render { typedef std::vector TransitionTypes; } -#endif // hifi_render_Transition_h \ No newline at end of file +#endif // hifi_render_Transition_h diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 65d71e46e6..a55cac292f 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -46,16 +46,6 @@ ScriptAudioInjector* AudioScriptingInterface::playSystemSound(SharedSoundPointer } ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions) { - if (QThread::currentThread() != thread()) { - ScriptAudioInjector* injector = NULL; - - BLOCKING_INVOKE_METHOD(this, "playSound", - Q_RETURN_ARG(ScriptAudioInjector*, injector), - Q_ARG(SharedSoundPointer, sound), - Q_ARG(const AudioInjectorOptions&, injectorOptions)); - return injector; - } - if (sound) { // stereo option isn't set from script, this comes from sound metadata or filename AudioInjectorOptions optionsCopy = injectorOptions; @@ -63,7 +53,7 @@ ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound optionsCopy.ambisonic = sound->isAmbisonic(); optionsCopy.localOnly = optionsCopy.localOnly || sound->isAmbisonic(); // force localOnly when Ambisonic - auto injector = AudioInjector::playSound(sound, optionsCopy); + auto injector = DependencyManager::get()->playSound(sound, optionsCopy); if (!injector) { return nullptr; } diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 7ad77b9b24..0cdc70e79c 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -23,6 +23,7 @@ /**jsdoc * @namespace Mat4 + * @variation 0 * * @hifi-interface * @hifi-client-entity @@ -38,7 +39,7 @@ class Mat4 : public QObject, protected QScriptable { public slots: /**jsdoc - * @function Mat4.multiply + * @function Mat4(0).multiply * @param {Mat4} m1 * @param {Mat4} m2 * @returns {Mat4} @@ -47,7 +48,7 @@ public slots: /**jsdoc - * @function Mat4.createFromRotAndTrans + * @function Mat4(0).createFromRotAndTrans * @param {Quat} rot * @param {Vec3} trans * @returns {Mat4} @@ -55,7 +56,7 @@ public slots: glm::mat4 createFromRotAndTrans(const glm::quat& rot, const glm::vec3& trans) const; /**jsdoc - * @function Mat4.createFromScaleRotAndTrans + * @function Mat4(0).createFromScaleRotAndTrans * @param {Vec3} scale * @param {Quat} rot * @param {Vec3} trans @@ -64,7 +65,7 @@ public slots: glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans) const; /**jsdoc - * @function Mat4.createFromColumns + * @function Mat4(0).createFromColumns * @param {Vec4} col0 * @param {Vec4} col1 * @param {Vec4} col2 @@ -74,7 +75,7 @@ public slots: glm::mat4 createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; /**jsdoc - * @function Mat4.createFromArray + * @function Mat4(0).createFromArray * @param {number[]} numbers * @returns {Mat4} */ @@ -82,21 +83,21 @@ public slots: /**jsdoc - * @function Mat4.extractTranslation + * @function Mat4(0).extractTranslation * @param {Mat4} m * @returns {Vec3} */ glm::vec3 extractTranslation(const glm::mat4& m) const; /**jsdoc - * @function Mat4.extractRotation + * @function Mat4(0).extractRotation * @param {Mat4} m * @returns {Vec3} */ glm::quat extractRotation(const glm::mat4& m) const; /**jsdoc - * @function Mat4.extractScale + * @function Mat4(0).extractScale * @param {Mat4} m * @returns {Vec3} */ @@ -104,7 +105,7 @@ public slots: /**jsdoc - * @function Mat4.transformPoint + * @function Mat4(0).transformPoint * @param {Mat4} m * @param {Vec3} point * @returns {Vec3} @@ -112,7 +113,7 @@ public slots: glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& point) const; /**jsdoc - * @function Mat4.transformVector + * @function Mat4(0).transformVector * @param {Mat4} m * @param {Vec3} vector * @returns {Vec3} @@ -121,7 +122,7 @@ public slots: /**jsdoc - * @function Mat4.inverse + * @function Mat4(0).inverse * @param {Mat4} m * @returns {Mat4} */ @@ -129,7 +130,7 @@ public slots: /**jsdoc - * @function Mat4.getFront + * @function Mat4(0).getFront * @param {Mat4} m * @returns {Vec3} */ @@ -137,28 +138,28 @@ public slots: glm::vec3 getFront(const glm::mat4& m) const { return getForward(m); } /**jsdoc - * @function Mat4.getForward + * @function Mat4(0).getForward * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getForward(const glm::mat4& m) const; /**jsdoc - * @function Mat4.getRight + * @function Mat4(0).getRight * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getRight(const glm::mat4& m) const; /**jsdoc - * @function Mat4.getUp + * @function Mat4(0).getUp * @param {Mat4} m * @returns {Vec3} */ glm::vec3 getUp(const glm::mat4& m) const; /**jsdoc - * @function Mat4.print + * @function Mat4(0).print * @param {string} label * @param {Mat4} m * @param {boolean} [transpose=false] diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index 8b51377bff..267ac3339d 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -19,10 +19,6 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c return QScriptValue(QScriptValue::NullValue); } - // when the script goes down we want to cleanup the injector - QObject::connect(engine, &QScriptEngine::destroyed, in, &ScriptAudioInjector::stopInjectorImmediately, - Qt::DirectConnection); - return engine->newQObject(in, QScriptEngine::ScriptOwnership); } @@ -37,13 +33,5 @@ ScriptAudioInjector::ScriptAudioInjector(const AudioInjectorPointer& injector) : } ScriptAudioInjector::~ScriptAudioInjector() { - if (!_injector.isNull()) { - // we've been asked to delete after finishing, trigger a queued deleteLater here - _injector->triggerDeleteAfterFinish(); - } -} - -void ScriptAudioInjector::stopInjectorImmediately() { - qCDebug(scriptengine) << "ScriptAudioInjector::stopInjectorImmediately called to stop audio injector immediately."; - _injector->stop(); -} + DependencyManager::get()->stop(_injector); +} \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptAudioInjector.h b/libraries/script-engine/src/ScriptAudioInjector.h index c7fb2f8a9a..d77291b92c 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.h +++ b/libraries/script-engine/src/ScriptAudioInjector.h @@ -14,7 +14,7 @@ #include -#include +#include /**jsdoc * Plays — "injects" — the content of an audio file. Used in the {@link Audio} API. @@ -48,7 +48,7 @@ public slots: * Stop current playback, if any, and start playing from the beginning. * @function AudioInjector.restart */ - void restart() { _injector->restart(); } + void restart() { DependencyManager::get()->restart(_injector); } /**jsdoc * Stop audio playback. @@ -68,28 +68,28 @@ public slots: * injector.stop(); * }, 2000); */ - void stop() { _injector->stop(); } + void stop() { DependencyManager::get()->stop(_injector); } /**jsdoc * Get the current configuration of the audio injector. * @function AudioInjector.getOptions * @returns {AudioInjector.AudioInjectorOptions} Configuration of how the injector plays the audio. */ - const AudioInjectorOptions& getOptions() const { return _injector->getOptions(); } + AudioInjectorOptions getOptions() const { return DependencyManager::get()->getOptions(_injector); } /**jsdoc * Configure how the injector plays the audio. * @function AudioInjector.setOptions * @param {AudioInjector.AudioInjectorOptions} options - Configuration of how the injector plays the audio. */ - void setOptions(const AudioInjectorOptions& options) { _injector->setOptions(options); } + void setOptions(const AudioInjectorOptions& options) { DependencyManager::get()->setOptions(_injector, options); } /**jsdoc * Get the loudness of the most recent frame of audio played. * @function AudioInjector.getLoudness * @returns {number} The loudness of the most recent frame of audio played, range 0.01.0. */ - float getLoudness() const { return _injector->getLoudness(); } + float getLoudness() const { return DependencyManager::get()->getLoudness(_injector); } /**jsdoc * Get whether or not the audio is currently playing. @@ -110,7 +110,7 @@ public slots: * print("Sound is playing: " + injector.isPlaying()); * }, 2000); */ - bool isPlaying() const { return _injector->isPlaying(); } + bool isPlaying() const { return DependencyManager::get()->isPlaying(_injector); } signals: @@ -134,13 +134,6 @@ signals: */ void finished(); -protected slots: - - /**jsdoc - * Stop audio playback. (Synonym of {@link AudioInjector.stop|stop}.) - * @function AudioInjector.stopInjectorImmediately - */ - void stopInjectorImmediately(); private: AudioInjectorPointer _injector; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 825017b1fe..a4fd2540d4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -260,12 +260,7 @@ bool ScriptEngine::isDebugMode() const { #endif } -ScriptEngine::~ScriptEngine() { - QSharedPointer scriptEngines(_scriptEngines); - if (scriptEngines) { - scriptEngines->removeScriptEngine(qSharedPointerCast(sharedFromThis())); - } -} +ScriptEngine::~ScriptEngine() {} void ScriptEngine::disconnectNonEssentialSignals() { disconnect(); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 48fb4f0b83..3549578ed5 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -573,11 +573,12 @@ public slots: /**jsdoc * @function Script.callAnimationStateHandler - * @param {function} callback - * @param {object} parameters - * @param {string[]} names - * @param {boolean} useNames - * @param {object} resultHandler + * @param {function} callback - Callback. + * @param {object} parameters - Parameters. + * @param {string[]} names - Names. + * @param {boolean} useNames - Use names. + * @param {function} resultHandler - Result handler. + * @deprecated This function is deprecated and will be removed. */ void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler); diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 3963ad5593..25c330e3fe 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -591,6 +591,8 @@ void ScriptEngines::onScriptFinished(const QString& rawScriptURL, ScriptEnginePo } } + removeScriptEngine(engine); + if (removed && !_isReloading) { // Update settings with removed script saveScripts(); diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index fef11c12e9..9beb52f20a 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -52,8 +52,15 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { } void UsersScriptingInterface::kick(const QUuid& nodeID) { - // ask the NodeList to kick the user with the given session ID - DependencyManager::get()->kickNodeBySessionID(nodeID); + + if (_kickConfirmationOperator) { + bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); + if (getCanKick() && !waitingForKickResponse) { + _kickConfirmationOperator(nodeID); + } + } else { + DependencyManager::get()->kickNodeBySessionID(nodeID); + } } void UsersScriptingInterface::mute(const QUuid& nodeID) { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 57de205066..f8ca974b8b 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -15,6 +15,7 @@ #define hifi_UsersScriptingInterface_h #include +#include /**jsdoc * @namespace Users @@ -38,6 +39,12 @@ class UsersScriptingInterface : public QObject, public Dependency { public: UsersScriptingInterface(); + void setKickConfirmationOperator(std::function kickConfirmationOperator) { + _kickConfirmationOperator = kickConfirmationOperator; + } + + bool getWaitForKickResponse() { return _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); } + void setWaitForKickResponse(bool waitForKickResponse) { _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = waitForKickResponse; }); } public slots: @@ -195,6 +202,11 @@ signals: private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); + + std::function _kickConfirmationOperator; + + ReadWriteLockable _kickResponseLock; + bool _waitingForKickResponse { false }; }; diff --git a/libraries/shared/src/BillboardMode.h b/libraries/shared/src/BillboardMode.h index 050f939941..700127aff1 100644 --- a/libraries/shared/src/BillboardMode.h +++ b/libraries/shared/src/BillboardMode.h @@ -18,9 +18,11 @@ * ValueDescription * * - * noneThe entity will not be billboarded. - * yawThe entity will yaw, but not pitch, to face the camera. Its actual rotation will be ignored. - * fullThe entity will be billboarded to face the camera. Its actual rotation will be ignored. + * "none"The entity will not be billboarded. + * "yaw"The entity will yaw, but not pitch, to face the camera. Its actual rotation will be + * ignored. + * "full"The entity will be billboarded to face the camera. Its actual rotation will be + * ignored. * * * @typedef {string} BillboardMode diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index 3f2f7cd7fb..b6fca03403 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -358,6 +358,12 @@ glm::vec3 Triangle::getNormal() const { return glm::normalize(glm::cross(edge1, edge2)); } +float Triangle::getArea() const { + glm::vec3 edge1 = v1 - v0; + glm::vec3 edge2 = v2 - v0; + return 0.5f * glm::length(glm::cross(edge1, edge2)); +} + Triangle Triangle::operator*(const glm::mat4& transform) const { return { glm::vec3(transform * glm::vec4(v0, 1.0f)), diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index 8ec75f71bd..04c54fc32e 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -125,6 +125,7 @@ public: glm::vec3 v1; glm::vec3 v2; glm::vec3 getNormal() const; + float getArea() const; Triangle operator*(const glm::mat4& transform) const; }; diff --git a/libraries/shared/src/NestableTransformNode.h b/libraries/shared/src/NestableTransformNode.h index a584bcd308..f70d158c91 100644 --- a/libraries/shared/src/NestableTransformNode.h +++ b/libraries/shared/src/NestableTransformNode.h @@ -20,8 +20,10 @@ public: _jointIndex(jointIndex) { auto nestablePointer = _spatiallyNestable.lock(); if (nestablePointer) { - glm::vec3 nestableDimensions = getActualScale(nestablePointer); - _baseScale = glm::max(glm::vec3(0.001f), nestableDimensions); + if (nestablePointer->getNestableType() != NestableType::Avatar) { + glm::vec3 nestableDimensions = getActualScale(nestablePointer); + _baseScale = glm::max(glm::vec3(0.001f), nestableDimensions); + } } } diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 47549b639a..597e537d8d 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -1069,13 +1069,14 @@ void pickRayFromScriptValue(const QScriptValue& object, PickRay& pickRay) { } /**jsdoc + * Details of a collision between avatars and entities. * @typedef {object} Collision * @property {ContactEventType} type - The contact type of the collision event. - * @property {Uuid} idA - The ID of one of the entities in the collision. - * @property {Uuid} idB - The ID of the other of the entities in the collision. - * @property {Vec3} penetration - The amount of penetration between the two entities. + * @property {Uuid} idA - The ID of one of the avatars or entities in the collision. + * @property {Uuid} idB - The ID of the other of the avatars or entities in the collision. + * @property {Vec3} penetration - The amount of penetration between the two items. * @property {Vec3} contactPoint - The point of contact. - * @property {Vec3} velocityChange - The change in relative velocity of the two entities, in m/s. + * @property {Vec3} velocityChange - The change in relative velocity of the two items, in m/s. */ QScriptValue collisionToScriptValue(QScriptEngine* engine, const Collision& collision) { QScriptValue obj = engine->newObject(); @@ -1147,19 +1148,21 @@ AnimationDetails::AnimationDetails(QString role, QUrl url, float fps, float prio } /**jsdoc + * The details of an animation that is playing. * @typedef {object} Avatar.AnimationDetails - * @property {string} role - * @property {string} url - * @property {number} fps - * @property {number} priority - * @property {boolean} loop - * @property {boolean} hold - * @property {boolean} startAutomatically - * @property {number} firstFrame - * @property {number} lastFrame - * @property {boolean} running - * @property {number} currentFrame - * @property {boolean} allowTranslation + * @property {string} role - Not used. + * @property {string} url - The URL to the animation file. Animation files need to be in .FBX format but only need to contain +* the avatar skeleton and animation data. + * @property {number} fps - The frames per second(FPS) rate for the animation playback. 30 FPS is normal speed. + * @property {number} priority - Not used. + * @property {boolean} loop - true if the animation should loop, false if it shouldn't. + * @property {boolean} hold - Not used. + * @property {number} firstFrame - The frame the animation should start at. + * @property {number} lastFrame - The frame the animation should stop at. + * @property {boolean} running - Not used. + * @property {number} currentFrame - The current frame being played. + * @property {boolean} startAutomatically - Not used. + * @property {boolean} allowTranslation - Not used. */ QScriptValue animationDetailsToScriptValue(QScriptEngine* engine, const AnimationDetails& details) { QScriptValue obj = engine->newObject(); diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 1edb303455..ea2c5b8354 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -43,6 +43,27 @@ Q_DECLARE_METATYPE(std::function); void registerMetaTypes(QScriptEngine* engine); // Mat4 +/**jsdoc + * A 4 x 4 matrix, typically containing a scale, rotation, and translation transform. See also the {@link Mat4(0)|Mat4} object. + * + * @typedef {object} Mat4 + * @property {number} r0c0 - Row 0, column 0 value. + * @property {number} r1c0 - Row 1, column 0 value. + * @property {number} r2c0 - Row 2, column 0 value. + * @property {number} r3c0 - Row 3, column 0 value. + * @property {number} r0c1 - Row 0, column 1 value. + * @property {number} r1c1 - Row 1, column 1 value. + * @property {number} r2c1 - Row 2, column 1 value. + * @property {number} r3c1 - Row 3, column 1 value. + * @property {number} r0c2 - Row 0, column 2 value. + * @property {number} r1c2 - Row 1, column 2 value. + * @property {number} r2c2 - Row 2, column 2 value. + * @property {number} r3c2 - Row 3, column 2 value. + * @property {number} r0c3 - Row 0, column 3 value. + * @property {number} r1c3 - Row 1, column 3 value. + * @property {number} r2c3 - Row 2, column 3 value. + * @property {number} r3c3 - Row 3, column 3 value. + */ QScriptValue mat4toScriptValue(QScriptEngine* engine, const glm::mat4& mat4); void mat4FromScriptValue(const QScriptValue& object, glm::mat4& mat4); diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index 31e6228bb9..0132e58d18 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -138,7 +138,7 @@ public slots: * var pickRay = Camera.computePickRay(event.x, event.y); * var intersection = Entities.findRayIntersection(pickRay); * if (intersection.intersects) { - * print ("You clicked on entity " + intersection.entityID); + * print("You clicked on entity " + intersection.entityID); * } * } * diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 137cffde94..2f2d38fe2a 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -240,6 +240,12 @@ class MessageBoxListener : public ModalDialogListener { return static_cast(_result.toInt()); } +protected slots: + virtual void onDestroyed() override { + ModalDialogListener::onDestroyed(); + onSelected(QMessageBox::NoButton); + } + private slots: void onSelected(int button) { _result = button; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 46dbdbdf13..6abbc486d0 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -34,6 +34,9 @@ class ModalDialogListener : public QObject { Q_OBJECT friend class OffscreenUi; +public: + QQuickItem* getDialogItem() { return _dialog; }; + protected: ModalDialogListener(QQuickItem* dialog); virtual ~ModalDialogListener(); @@ -43,7 +46,7 @@ signals: void response(const QVariant& value); protected slots: - void onDestroyed(); + virtual void onDestroyed(); protected: QQuickItem* _dialog; diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 7a1c37af33..b8895b6714 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -23,7 +23,7 @@ #include "ToolbarScriptingInterface.h" #include "Logging.h" -#include +#include #include "SettingHandle.h" @@ -212,7 +212,7 @@ void TabletScriptingInterface::playSound(TabletAudioEvents aEvent) { options.localOnly = true; options.positionSet = false; // system sound - AudioInjectorPointer injector = AudioInjector::playSoundAndDelete(sound, options); + DependencyManager::get()->playSound(sound, options, true); } } @@ -368,6 +368,7 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { if (toolbarMode) { #if !defined(DISABLE_QML) + closeDialog(); // create new desktop window auto tabletRootWindow = new TabletRootWindow(); tabletRootWindow->initQml(QVariantMap()); diff --git a/libraries/ui/src/ui/types/SoundEffect.cpp b/libraries/ui/src/ui/types/SoundEffect.cpp index dc2328b33e..38114ecef1 100644 --- a/libraries/ui/src/ui/types/SoundEffect.cpp +++ b/libraries/ui/src/ui/types/SoundEffect.cpp @@ -2,12 +2,10 @@ #include "SoundEffect.h" #include -#include SoundEffect::~SoundEffect() { if (_injector) { - // stop will cause the AudioInjector to delete itself. - _injector->stop(); + DependencyManager::get()->stop(_injector); } } @@ -28,15 +26,14 @@ void SoundEffect::setVolume(float volume) { _volume = volume; } -void SoundEffect::play(QVariant position) { +void SoundEffect::play(const QVariant& position) { AudioInjectorOptions options; options.position = vec3FromVariant(position); options.localOnly = true; options.volume = _volume; if (_injector) { - _injector->setOptions(options); - _injector->restart(); + DependencyManager::get()->setOptionsAndRestart(_injector, options); } else { - _injector = AudioInjector::playSound(_sound, options); + _injector = DependencyManager::get()->playSound(_sound, options); } } diff --git a/libraries/ui/src/ui/types/SoundEffect.h b/libraries/ui/src/ui/types/SoundEffect.h index a7e29d86f9..cb8a5cd67f 100644 --- a/libraries/ui/src/ui/types/SoundEffect.h +++ b/libraries/ui/src/ui/types/SoundEffect.h @@ -13,9 +13,7 @@ #include #include - -class AudioInjector; -using AudioInjectorPointer = QSharedPointer; +#include // SoundEffect object, exposed to qml only, not interface JavaScript. // This is used to play spatial sound effects on tablets/web entities from within QML. @@ -34,7 +32,7 @@ public: float getVolume() const; void setVolume(float volume); - Q_INVOKABLE void play(QVariant position); + Q_INVOKABLE void play(const QVariant& position); protected: QUrl _url; float _volume { 1.0f }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bd7e79dffc..e392680df9 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js" + "system/miniTablet.js", + "system/audioMuteOverlay.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/developer/createAvatarInputsBarEntity.js b/scripts/developer/createAvatarInputsBarEntity.js new file mode 100644 index 0000000000..deb0cfdd89 --- /dev/null +++ b/scripts/developer/createAvatarInputsBarEntity.js @@ -0,0 +1,138 @@ +"use strict"; + +(function(){ + var AppUi = Script.require("appUi"); + + var ui; + + var onCreateAvatarInputsBarEntity = false; + var micBarEntity = null; + var bubbleIconEntity = null; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var AVATAR_INPUTS_EDIT_QML_SOURCE = "hifi/EditAvatarInputsBar.qml"; + + // DPI + var ENTITY_DPI = 60.0; + // QML NATURAL DIMENSIONS + var MIC_BAR_DIMENSIONS = Vec3.multiply(30.0 / ENTITY_DPI, {x: 0.036, y: 0.048, z: 0.3}); + var BUBBLE_ICON_DIMENSIONS = Vec3.multiply(30.0 / ENTITY_DPI, {x: 0.036, y: 0.036, z: 0.3}); + // ENTITY NAMES + var MIC_BAR_NAME = "AvatarInputsMicBarEntity"; + var BUBBLE_ICON_NAME = "AvatarInputsBubbleIconEntity"; + // CONSTANTS + var LOCAL_POSITION_X_OFFSET = -0.2; + var LOCAL_POSITION_Y_OFFSET = -0.125; + var LOCAL_POSITION_Z_OFFSET = -0.5; + + function fromQml(message) { + if (message.method === "reposition") { + var micBarLocalPosition = Entities.getEntityProperties(micBarEntity).localPosition; + var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; + var newMicBarLocalPosition, newBubbleIconLocalPosition; + if (message.x !== undefined) { + newMicBarLocalPosition = { x: -((MIC_BAR_DIMENSIONS.x) / 2) + message.x, y: micBarLocalPosition.y, z: micBarLocalPosition.z }; + newBubbleIconLocalPosition = { x: ((MIC_BAR_DIMENSIONS.x) * 1.2 / 2) + message.x, y: bubbleIconLocalPosition.y, z: bubbleIconLocalPosition.z }; + } else if (message.y !== undefined) { + newMicBarLocalPosition = { x: micBarLocalPosition.x, y: message.y, z: micBarLocalPosition.z }; + newBubbleIconLocalPosition = { x: bubbleIconLocalPosition.x, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + message.y), z: bubbleIconLocalPosition.z }; + } else if (message.z !== undefined) { + newMicBarLocalPosition = { x: micBarLocalPosition.x, y: micBarLocalPosition.y, z: message.z }; + newBubbleIconLocalPosition = { x: bubbleIconLocalPosition.x, y: bubbleIconLocalPosition.y, z: message.z }; + } + var micBarProps = { + localPosition: newMicBarLocalPosition + }; + var bubbleIconProps = { + localPosition: newBubbleIconLocalPosition + }; + + Entities.editEntity(micBarEntity, micBarProps); + Entities.editEntity(bubbleIconEntity, bubbleIconProps); + } else if (message.method === "setVisible") { + if (message.visible !== undefined) { + var props = { + visible: message.visible + }; + Entities.editEntity(micBarEntity, props); + Entities.editEntity(bubbleIconEntity, props); + } + } else if (message.method === "print") { + // prints the local position into the hifi log. + var micBarLocalPosition = Entities.getEntityProperties(micBarEntity).localPosition; + var bubbleIconLocalPosition = Entities.getEntityProperties(bubbleIconEntity).localPosition; + console.log("mic bar local position is at " + JSON.stringify(micBarLocalPosition)); + console.log("bubble icon local position is at " + JSON.stringify(bubbleIconLocalPosition)); + } + }; + + function createEntities() { + if (micBarEntity != null && bubbleIconEntity != null) { + return; + } + // POSITIONS + var micBarLocalPosition = {x: (-(MIC_BAR_DIMENSIONS.x / 2)) + LOCAL_POSITION_X_OFFSET, y: LOCAL_POSITION_Y_OFFSET, z: LOCAL_POSITION_Z_OFFSET}; + var bubbleIconLocalPosition = {x: (MIC_BAR_DIMENSIONS.x * 1.2 / 2) + LOCAL_POSITION_X_OFFSET, y: ((MIC_BAR_DIMENSIONS.y - BUBBLE_ICON_DIMENSIONS.y) / 2 + LOCAL_POSITION_Y_OFFSET), z: LOCAL_POSITION_Z_OFFSET}; + var props = { + type: "Web", + name: MIC_BAR_NAME, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: micBarLocalPosition, + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, micBarLocalPosition)), + sourceUrl: Script.resourcesPath() + "qml/hifi/audio/MicBarApplication.qml", + // cutoff alpha for detecting transparency + alpha: 0.98, + dimensions: MIC_BAR_DIMENSIONS, + dpi: ENTITY_DPI, + drawInFront: true, + userData: { + grabbable: false + }, + }; + micBarEntity = Entities.addEntity(props, "local"); + var props = { + type: "Web", + name: BUBBLE_ICON_NAME, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + localPosition: bubbleIconLocalPosition, + localRotation: Quat.cancelOutRollAndPitch(Quat.lookAtSimple(Camera.orientation, bubbleIconLocalPosition)), + sourceUrl: Script.resourcesPath() + "qml/BubbleIcon.qml", + // cutoff alpha for detecting transparency + alpha: 0.98, + dimensions: BUBBLE_ICON_DIMENSIONS, + dpi: ENTITY_DPI, + drawInFront: true, + userData: { + grabbable: false + }, + }; + bubbleIconEntity = Entities.addEntity(props, "local"); + tablet.loadQMLSource(AVATAR_INPUTS_EDIT_QML_SOURCE); + }; + function cleanup() { + if (micBarEntity) { + Entities.deleteEntity(micBarEntity); + } + if (bubbleIconEntity) { + Entities.deleteEntity(bubbleIconEntity); + } + }; + + function setup() { + ui = new AppUi({ + buttonName: "AVBAR", + home: Script.resourcesPath() + "qml/hifi/EditAvatarInputsBar.qml", + onMessage: fromQml, + onOpened: createEntities, + // onClosed: cleanup, + normalButton: "icons/tablet-icons/edit-i.svg", + activeButton: "icons/tablet-icons/edit-a.svg", + }); + }; + + setup(); + + Script.scriptEnding.connect(cleanup); + +}()); diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index 3e8e0b1008..9771348377 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -353,10 +353,11 @@ function AppUi(properties) { // Close if necessary, clean up any remaining handlers, and remove the button. GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); GlobalServices.findableByChanged.disconnect(restartNotificationPoll); + that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.isOpen) { that.close(); + that.onScreenChanged("", ""); } - that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.button) { if (that.onClicked) { that.button.clicked.disconnect(that.onClicked); diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index 7201cdecad..73c7504089 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -227,6 +227,14 @@ "speedSpread": { "tooltip": "The spread in speeds at which particles are emitted at, resulting in a variety of speeds." }, + "particleShapeType": { + "tooltip": "The shape of the surface from which to emit particles.", + "jsPropertyName": "shapeType" + }, + "particleCompoundShapeURL": { + "tooltip": "The model file to use for the particle emitter if Shape Type is \"Use Compound Shape URL\".", + "jsPropertyName": "compoundShapeURL" + }, "emitDimensions": { "tooltip": "The outer limit radius in dimensions that the particles can be emitted from." }, diff --git a/scripts/system/audio.js b/scripts/system/audio.js index ee82c0c6ea..a161b40ffd 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -26,9 +26,15 @@ var UNMUTE_ICONS = { icon: "icons/tablet-icons/mic-unmute-i.svg", activeIcon: "icons/tablet-icons/mic-unmute-a.svg" }; +var PTT_ICONS = { + icon: "icons/tablet-icons/mic-ptt-i.svg", + activeIcon: "icons/tablet-icons/mic-ptt-a.svg" +}; function onMuteToggled() { - if (Audio.muted) { + if (Audio.pushToTalk) { + button.editProperties(PTT_ICONS); + } else if (Audio.muted) { button.editProperties(MUTE_ICONS); } else { button.editProperties(UNMUTE_ICONS); @@ -57,8 +63,8 @@ function onScreenChanged(type, url) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ - icon: Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, - activeIcon: Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, + icon: Audio.pushToTalk ? PTT_ICONS.icon : Audio.muted ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, + activeIcon: Audio.pushToTalk ? PTT_ICONS.activeIcon : Audio.muted ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, text: TABLET_BUTTON_NAME, sortOrder: 1 }); @@ -68,6 +74,8 @@ onMuteToggled(); button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); Audio.mutedChanged.connect(onMuteToggled); +Audio.pushToTalkChanged.connect(onMuteToggled); +HMD.displayModeChanged.connect(onMuteToggled); Script.scriptEnding.connect(function () { if (onAudioScreen) { @@ -76,6 +84,8 @@ Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); Audio.mutedChanged.disconnect(onMuteToggled); + Audio.pushToTalkChanged.disconnect(onMuteToggled); + HMD.displayModeChanged.disconnect(onMuteToggled); tablet.removeButton(button); }); diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index c597f75bca..9acc5ab123 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -1,104 +1,130 @@ -"use strict"; -/* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ -/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // audioMuteOverlay.js // // client script that creates an overlay to provide mute feedback // // Created by Triplelexx on 17/03/09 +// Reworked by Seth Alves on 2019-2-17 // Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function () { // BEGIN LOCAL_SCOPE - var utilsPath = Script.resolvePath('../developer/libraries/utils.js'); - Script.include(utilsPath); +"use strict"; - var TWEEN_SPEED = 0.025; - var MIX_AMOUNT = 0.25; +/* global Audio, Script, Overlays, Quat, MyAvatar, HMD */ - var overlayPosition = Vec3.ZERO; - var tweenPosition = 0; - var startColor = { - red: 170, - green: 170, - blue: 170 - }; - var endColor = { - red: 255, - green: 0, - blue: 0 - }; - var overlayID; +(function() { // BEGIN LOCAL_SCOPE - Script.update.connect(update); - Script.scriptEnding.connect(cleanup); + var lastShortTermInputLoudness = 0.0; + var lastLongTermInputLoudness = 0.0; + var sampleRate = 8.0; // Hz - function update(dt) { - if (!Audio.muted) { - if (hasOverlay()) { - deleteOverlay(); - } - } else if (!hasOverlay()) { - createOverlay(); - } else { - updateOverlay(); + var shortTermAttackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack + var shortTermReleaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release + + var longTermAttackTC = Math.exp(-1.0 / (sampleRate * 5.0)); // 5 second attack + var longTermReleaseTC = Math.exp(-1.0 / (sampleRate * 10.0)); // 10 seconds release + + var activationThreshold = 0.05; // how much louder short-term needs to be than long-term to trigger warning + + var holdReset = 2.0 * sampleRate; // 2 seconds hold + var holdCount = 0; + var warningOverlayID = null; + var pollInterval = null; + var warningText = "Muted"; + + function showWarning() { + if (warningOverlayID) { + return; + } + + if (HMD.active) { + warningOverlayID = Overlays.addOverlay("text3d", { + name: "Muted-Warning", + localPosition: { x: 0.0, y: -0.45, z: -1.0 }, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), + text: warningText, + textAlpha: 1, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + lineHeight: 0.042, + dimensions: { x: 0.11, y: 0.05 }, + visible: true, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") + }); } } - function getOffsetPosition() { - return Vec3.sum(Camera.position, Quat.getFront(Camera.orientation)); - } - - function createOverlay() { - overlayPosition = getOffsetPosition(); - overlayID = Overlays.addOverlay("sphere", { - position: overlayPosition, - rotation: Camera.orientation, - alpha: 0.9, - dimensions: 0.1, - solid: true, - ignoreRayIntersection: true - }); - } - - function hasOverlay() { - return Overlays.getProperty(overlayID, "position") !== undefined; - } - - function updateOverlay() { - // increase by TWEEN_SPEED until completion - if (tweenPosition < 1) { - tweenPosition += TWEEN_SPEED; - } else { - // after tween completion reset to zero and flip values to ping pong - tweenPosition = 0; - for (var component in startColor) { - var storedColor = startColor[component]; - startColor[component] = endColor[component]; - endColor[component] = storedColor; - } + function hideWarning() { + if (!warningOverlayID) { + return; } - // mix previous position with new and mix colors - overlayPosition = Vec3.mix(overlayPosition, getOffsetPosition(), MIX_AMOUNT); - Overlays.editOverlay(overlayID, { - color: colorMix(startColor, endColor, easeIn(tweenPosition)), - position: overlayPosition, - rotation: Camera.orientation - }); + Overlays.deleteOverlay(warningOverlayID); + warningOverlayID = null; } - function deleteOverlay() { - Overlays.deleteOverlay(overlayID); + function startPoll() { + if (pollInterval) { + return; + } + pollInterval = Script.setInterval(function() { + var shortTermInputLoudness = Audio.inputLevel; + var longTermInputLoudness = shortTermInputLoudness; + + var shortTc = (shortTermInputLoudness > lastShortTermInputLoudness) ? shortTermAttackTC : shortTermReleaseTC; + var longTc = (longTermInputLoudness > lastLongTermInputLoudness) ? longTermAttackTC : longTermReleaseTC; + + shortTermInputLoudness += shortTc * (lastShortTermInputLoudness - shortTermInputLoudness); + longTermInputLoudness += longTc * (lastLongTermInputLoudness - longTermInputLoudness); + + lastShortTermInputLoudness = shortTermInputLoudness; + lastLongTermInputLoudness = longTermInputLoudness; + + if (shortTermInputLoudness > lastLongTermInputLoudness + activationThreshold) { + holdCount = holdReset; + } else { + holdCount = Math.max(holdCount - 1, 0); + } + + if (holdCount > 0) { + showWarning(); + } else { + hideWarning(); + } + }, 1000.0 / sampleRate); + } + + function stopPoll() { + if (!pollInterval) { + return; + } + Script.clearInterval(pollInterval); + pollInterval = null; + hideWarning(); + } + + function startOrStopPoll() { + if (Audio.warnWhenMuted && Audio.muted) { + startPoll(); + } else { + stopPoll(); + } } function cleanup() { - deleteOverlay(); - Audio.muted.disconnect(onMuteToggled); - Script.update.disconnect(update); + stopPoll(); } -}()); // END LOCAL_SCOPE \ No newline at end of file + + Script.scriptEnding.connect(cleanup); + + startOrStopPoll(); + Audio.mutedChanged.connect(startOrStopPoll); + Audio.warnWhenMutedChanged.connect(startOrStopPoll); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index fb61b914a3..6439d30023 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -1,6 +1,8 @@ "use strict"; /*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/*global Tablet, Script, Entities, MyAvatar, Camera, Quat, HMD, Account, UserActivityLogger, Messages, print, + AvatarBookmarks, ContextOverlay, AddressManager +*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // avatarapp.js @@ -14,7 +16,6 @@ (function() { // BEGIN LOCAL_SCOPE -var request = Script.require('request').request; var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); @@ -22,7 +23,6 @@ Script.include("/~/system/libraries/controllers.js"); var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; -var ENTRY_VERSION = "version"; function executeLater(callback) { Script.setTimeout(callback, 300); @@ -44,7 +44,7 @@ function getMyAvatarWearables() { } var localRotation = entity.properties.localRotation; - entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation) + entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation); wearablesArray.push(entity); } @@ -52,7 +52,7 @@ function getMyAvatarWearables() { } function getMyAvatar() { - var avatar = {} + var avatar = {}; avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables(); @@ -68,7 +68,7 @@ function getMyAvatarSettings() { collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), - } + }; } function updateAvatarWearables(avatar, callback, wearablesOverride) { @@ -76,7 +76,8 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { var wearables = wearablesOverride ? wearablesOverride : getMyAvatarWearables(); avatar[ENTRY_AVATAR_ENTITIES] = wearables; - sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}) + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); if(callback) callback(); @@ -101,7 +102,7 @@ var adjustWearables = { this.opened = value; } } -} +}; var currentAvatarWearablesBackup = null; var currentAvatar = null; @@ -112,7 +113,7 @@ function onTargetScaleChanged() { if(currentAvatar.scale !== MyAvatar.getAvatarScale()) { currentAvatar.scale = MyAvatar.getAvatarScale(); if(notifyScaleChanged) { - sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}) + sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}); } } } @@ -126,7 +127,7 @@ function onSkeletonModelURLChanged() { function onDominantHandChanged(dominantHand) { if(currentAvatarSettings.dominantHand !== dominantHand) { currentAvatarSettings.dominantHand = dominantHand; - sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}) + sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}); } } @@ -140,37 +141,37 @@ function onHmdAvatarAlignmentTypeChanged(type) { function onCollisionsEnabledChanged(enabled) { if(currentAvatarSettings.collisionsEnabled !== enabled) { currentAvatarSettings.collisionsEnabled = enabled; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}); } } function onOtherAvatarsCollisionsEnabledChanged(enabled) { if (currentAvatarSettings.otherAvatarsCollisionsEnabled !== enabled) { currentAvatarSettings.otherAvatarsCollisionsEnabled = enabled; - sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }) + sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }); } } function onNewCollisionSoundUrl(url) { if(currentAvatarSettings.collisionSoundUrl !== url) { currentAvatarSettings.collisionSoundUrl = url; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}); } } function onAnimGraphUrlChanged(url) { if (currentAvatarSettings.animGraphUrl !== url) { currentAvatarSettings.animGraphUrl = url; - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }); if (currentAvatarSettings.animGraphOverrideUrl !== MyAvatar.getAnimGraphOverrideUrl()) { currentAvatarSettings.animGraphOverrideUrl = MyAvatar.getAnimGraphOverrideUrl(); - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', 'value': currentAvatarSettings.animGraphOverrideUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', + 'value': currentAvatarSettings.animGraphOverrideUrl }); } } } -var selectedAvatarEntityGrabbable = false; var selectedAvatarEntityID = null; var grabbedAvatarEntityChangeNotifier = null; @@ -178,6 +179,33 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); +function getWearablesFrozen() { + var wearablesFrozen = true; + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + if (isGrabbable(wearable.id)) { + wearablesFrozen = false; + } + }); + + return wearablesFrozen; +} + +function freezeWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, false); + }); +} + +function unfreezeWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, true); + }); +} + + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. switch (message.method) { case 'getAvatars': @@ -201,7 +229,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } } - sendToQml(message) + sendToQml(message); break; case 'selectAvatar': Entities.addingWearable.disconnect(onAddingWearable); @@ -209,6 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -228,11 +257,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See message.properties.localRotationAngles = Quat.safeEulerAngles(message.properties.localRotation); } - sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}) + sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}); break; case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); + unfreezeWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); @@ -305,11 +335,11 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See var currentAvatarURL = MyAvatar.getFullAvatarURLFromPreferences(); if(currentAvatarURL !== message.avatarURL) { MyAvatar.useFullAvatarURL(message.avatarURL); - sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}) + sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}); } break; case 'navigate': - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system") + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if(message.url.indexOf('app://') === 0) { if (message.url === 'app://marketplace') { tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -345,7 +375,17 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); - settings = getMyAvatarSettings(); + currentAvatarSettings = getMyAvatarSettings(); + break; + case 'toggleWearablesFrozen': + var wearablesFrozen = getWearablesFrozen(); + wearablesFrozen = !wearablesFrozen; + if (wearablesFrozen) { + freezeWearables(); + } else { + unfreezeWearables(); + } + sendToQml({'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : wearablesFrozen}); break; default: print('Unrecognized message from AvatarApp.qml'); @@ -366,9 +406,11 @@ function isGrabbable(entityID) { } function setGrabbable(entityID, grabbable) { - var properties = Entities.getEntityProperties(entityID, ['avatarEntity']); - if (properties.avatarEntity) { - Entities.editEntity(entityID, { grab: { grabbable: grabbable }}); + var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']); + if (properties.avatarEntity && properties.grab.grabbable != grabbable) { + var editProps = { grab: { grabbable: grabbable }}; + Entities.editEntity(entityID, editProps); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } @@ -378,18 +420,7 @@ function ensureWearableSelected(entityID) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; } - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, selectedAvatarEntityGrabbable); - } - selectedAvatarEntityID = entityID; - selectedAvatarEntityGrabbable = isGrabbable(entityID); - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, true); - } - return true; } @@ -398,7 +429,7 @@ function ensureWearableSelected(entityID) { function isEntityBeingWorn(entityID) { return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID; -}; +} function onSelectedEntity(entityID, pointerEvent) { if(selectedAvatarEntityID !== entityID && isEntityBeingWorn(entityID)) @@ -413,12 +444,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function handleWearableMessages(channel, message, sender) { @@ -435,30 +468,35 @@ function handleWearableMessages(channel, message, sender) { } var entityID = parsedMessage.grabbedEntity; + + var updateWearable = function() { + // for some reasons Entities.getEntityProperties returns more than was asked.. + var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; + var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); + var properties = {}; + + propertyNames.forEach(function(propertyName) { + properties[propertyName] = entityProperties[propertyName]; + }); + + properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, + 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); + + }; + if(parsedMessage.action === 'grab') { if(selectedAvatarEntityID !== entityID) { ensureWearableSelected(entityID); sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); } - grabbedAvatarEntityChangeNotifier = Script.setInterval(function() { - // for some reasons Entities.getEntityProperties returns more than was asked.. - var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; - var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); - var properties = {} - - propertyNames.forEach(function(propertyName) { - properties[propertyName] = entityProperties[propertyName]; - }) - - properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); - sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}) - - }, 1000); + grabbedAvatarEntityChangeNotifier = Script.setInterval(updateWearable, 1000); } else if(parsedMessage.action === 'release') { if(grabbedAvatarEntityChangeNotifier !== null) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; + updateWearable(); } } } @@ -481,8 +519,8 @@ function onBookmarkDeleted(bookmarkName) { function onBookmarkAdded(bookmarkName) { var bookmark = AvatarBookmarks.getBookmark(bookmarkName); bookmark.avatarEntites.forEach(function(avatarEntity) { - avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) - }) + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation); + }); sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': bookmark }); } @@ -601,14 +639,8 @@ function onTabletScreenChanged(type, url) { onAvatarAppScreen = onAvatarAppScreenNow; if(onAvatarAppScreenNow) { - var message = { - 'method' : 'initialize', - 'data' : { - 'jointNames' : MyAvatar.getJointNames() - } - }; - - sendToQml(message) + sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } diff --git a/scripts/system/away.js b/scripts/system/away.js index 2af43b2055..e45041139a 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -171,7 +171,7 @@ function goAway(fromStartup) { if (!previousBubbleState) { Users.toggleIgnoreRadius(); } - UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled()); + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); UserActivityLogger.toggledAway(true); MyAvatar.isAway = true; } @@ -186,7 +186,7 @@ function goActive() { if (Users.getIgnoreRadiusEnabled() !== previousBubbleState) { Users.toggleIgnoreRadius(); - UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled()); + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); } if (!Window.hasFocus()) { diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 6ca624872e..eca3b3dcd4 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -90,7 +90,7 @@ // Called from the C++ scripting interface to show the bubble overlay function enteredIgnoreRadius() { createOverlays(); - UserActivityLogger.bubbleActivated(); + UserActivityLogger.privacyShieldActivated(); } // Used to set the state of the bubble HUD button @@ -160,7 +160,7 @@ function onBubbleToggled(enabled, doNotLog) { writeButtonProperties(enabled); if (doNotLog !== true) { - UserActivityLogger.bubbleToggled(enabled); + UserActivityLogger.privacyShieldToggled(enabled); } if (enabled) { createOverlays(); @@ -174,7 +174,7 @@ } // Setup the bubble button - var buttonName = "BUBBLE"; + var buttonName = "SHIELD"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/bubble-i.svg", diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 17ff918243..86806fd8b4 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -377,6 +377,9 @@ function deleteSendMoneyParticleEffect() { } function onUsernameChanged() { + if (ui.checkIsOpen()) { + ui.open(WALLET_QML_SOURCE); + } } var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 28c3e2a299..0a9fa4dce1 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -12,7 +12,7 @@ LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, PointerManager, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, - PointerManager, print, Selection, DISPATCHER_HOVERING_LIST, DISPATCHER_HOVERING_STYLE, Keyboard + PointerManager, print, Keyboard */ controllerDispatcherPlugins = {}; @@ -53,11 +53,13 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.blacklist = []; this.pointerManager = new PointerManager(); this.grabSphereOverlays = [null, null]; + this.targetIDs = {}; // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name // is stored as the value, rather than false. this.activitySlots = { + head: false, leftHand: false, rightHand: false, rightHandTrigger: false, @@ -224,8 +226,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (tabletIndex !== -1 && closebyOverlays.indexOf(HMD.tabletID) === -1) { nearbyOverlays.splice(tabletIndex, 1); } - if (miniTabletIndex !== -1 - && ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { + if (miniTabletIndex !== -1 && + ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { nearbyOverlays.splice(miniTabletIndex, 1); } } @@ -335,6 +337,23 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }); } + // also make sure we have the properties from the current module's target + for (var tIDRunningPluginName in _this.runningPluginNames) { + if (_this.runningPluginNames.hasOwnProperty(tIDRunningPluginName)) { + var targetIDs = _this.targetIDs[tIDRunningPluginName]; + if (targetIDs) { + for (var k = 0; k < targetIDs.length; k++) { + var targetID = targetIDs[k]; + if (!nearbyEntityPropertiesByID[targetID]) { + var targetProps = Entities.getEntityProperties(targetID, DISPATCHER_PROPERTIES); + targetProps.id = targetID; + nearbyEntityPropertiesByID[targetID] = targetProps; + } + } + } + } + } + // bundle up all the data about the current situation var controllerData = { triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], @@ -401,10 +420,23 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.beginProfileRange("dispatch.run." + runningPluginName); } var runningness = plugin.run(controllerData, deltaTime); + + if (DEBUG) { + if (JSON.stringify(_this.targetIDs[runningPluginName]) != JSON.stringify(runningness.targets)) { + print("controllerDispatcher targetIDs[" + runningPluginName + "] = " + + JSON.stringify(runningness.targets)); + } + } + + _this.targetIDs[runningPluginName] = runningness.targets; if (!runningness.active) { // plugin is finished running, for now. remove it from the list // of running plugins and mark its activity-slots as "not in use" delete _this.runningPluginNames[runningPluginName]; + delete _this.targetIDs[runningPluginName]; + if (DEBUG) { + print("controllerDispatcher deleted targetIDs[" + runningPluginName + "]"); + } _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); if (DEBUG) { @@ -526,8 +558,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } if (action === "tablet") { - var tabletIDs = message.blacklist - ? [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] : []; + var tabletIDs = message.blacklist ? + [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] : + []; if (message.hand === LEFT_HAND) { _this.leftBlacklistTabletIDs = tabletIDs; _this.setLeftBlacklist(); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index b1c1bc7765..54b56ff271 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -7,10 +7,10 @@ /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex, - enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther, Messages, makeDispatcherModuleParameters, + enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, - entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData, - entityIsEquippable + entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData, + entityIsEquippable, HMD */ Script.include("/~/system/libraries/Xform.js"); @@ -183,8 +183,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var TRIGGER_OFF_VALUE = 0.1; var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab var BUMPER_ON_VALUE = 0.5; + var ATTACHPOINT_MAX_DISTANCE = 3.0; - var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; + // var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; var UNEQUIP_KEY = "u"; @@ -200,7 +201,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa indicatorOffset: props.grab.equippableIndicatorOffset }; } else { - return null + return null; } } @@ -231,6 +232,12 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; var joints = avatarSettingsData[hotspot.key]; if (joints) { + // make sure they are reasonable + if (joints[jointName] && joints[jointName][0] && + Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) { + print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0])); + return undefined; + } return joints[jointName]; } } @@ -456,6 +463,8 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa }; this.startEquipEntity = function (controllerData) { + var _this = this; + this.dropGestureReset(); this.clearEquipHaptics(); Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); @@ -498,17 +507,23 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa if (entityIsCloneable(grabbedProperties)) { var cloneID = this.cloneHotspot(grabbedProperties, controllerData); this.targetEntityID = cloneID; - Entities.editEntity(this.targetEntityID, reparentProps); controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties; isClone = true; - } else if (!grabbedProperties.locked) { - Entities.editEntity(this.targetEntityID, reparentProps); - } else { + } else if (grabbedProperties.locked) { this.grabbedHotspot = null; this.targetEntityID = null; return; } + + // HACK -- when + // https://highfidelity.fogbugz.com/f/cases/21767/entity-edits-shortly-after-an-add-often-fail + // is resolved, this can just be an editEntity rather than a setTimeout. + this.editDelayTimeout = Script.setTimeout(function () { + _this.editDelayTimeout = null; + Entities.editEntity(_this.targetEntityID, reparentProps); + }, 100); + // we don't want to send startEquip message until the trigger is released. otherwise, // guns etc will fire right as they are equipped. this.shouldSendStart = true; @@ -519,7 +534,6 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); - var _this = this; var grabEquipCheck = function() { var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(_this.targetEntityID, "startEquip", args); @@ -532,6 +546,12 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa }; this.endEquipEntity = function () { + + if (this.editDelayTimeout) { + Script.clearTimeout(this.editDelayTimeout); + this.editDelayTimeout = null; + } + this.storeAttachPointInSettings(); Entities.editEntity(this.targetEntityID, { parentID: Uuid.NULL, @@ -595,7 +615,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa equipHotspotBuddy.update(deltaTime, timestamp, controllerData); // if the potentialHotspot is cloneable, clone it and return it - // if the potentialHotspot os not cloneable and locked return null + // if the potentialHotspot is not cloneable and locked return null if (potentialEquipHotspot && (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || this.messageGrabEntity)) { @@ -603,7 +623,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.targetEntityID = this.grabbedHotspot.entityID; this.startEquipEntity(controllerData); this.equipedWithSecondary = this.secondarySmoothedSqueezed(); - return makeRunningValues(true, [potentialEquipHotspot.entityID], []); + return makeRunningValues(true, [this.targetEntityID], []); } else { return makeRunningValues(false, [], []); } diff --git a/scripts/system/controllers/controllerModules/pushToTalk.js b/scripts/system/controllers/controllerModules/pushToTalk.js new file mode 100644 index 0000000000..11335ba2f5 --- /dev/null +++ b/scripts/system/controllers/controllerModules/pushToTalk.js @@ -0,0 +1,64 @@ +"use strict"; + +// Created by Jason C. Najera on 3/7/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Handles Push-to-Talk functionality for HMD mode. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + function PushToTalkHandler() { + var _this = this; + this.active = false; + + this.shouldTalk = function (controllerData) { + // Set up test against controllerData here... + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; + return (gripVal) ? true : false; + }; + + this.shouldStopTalking = function (controllerData) { + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; + return (gripVal) ? false : true; + }; + + this.isReady = function (controllerData, deltaTime) { + if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) { + Audio.pushingToTalk = true; + return makeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) { + Audio.pushingToTalk = false; + print("Stop pushing to talk."); + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [], []); + }; + + this.parameters = makeDispatcherModuleParameters( + 950, + ["head"], + [], + 100); + } + + var pushToTalk = new PushToTalkHandler(); + enableDispatcherModule("PushToTalk", pushToTalk); + + function cleanup() { + disableDispatcherModule("PushToTalk"); + }; + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 726e075fcc..ca7d041792 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -33,7 +33,8 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearGrabHyperLinkEntity.js", "controllerModules/nearTabletHighlight.js", "controllerModules/nearGrabEntity.js", - "controllerModules/farGrabEntity.js" + "controllerModules/farGrabEntity.js", + "controllerModules/pushToTalk.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 2c3785217c..7c5af27b82 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -282,6 +282,28 @@ function checkEditPermissionsAndUpdate() { } } +// Copies the properties in `b` into `a`. `a` will be modified. +function copyProperties(a, b) { + for (var key in b) { + a[key] = b[key]; + } + return a; +} + +const DEFAULT_DYNAMIC_PROPERTIES = { + dynamic: true, + damping: 0.39347, + angularDamping: 0.39347, + gravity: { x: 0, y: -9.8, z: 0 }, +}; + +const DEFAULT_NON_DYNAMIC_PROPERTIES = { + dynamic: false, + damping: 0, + angularDamping: 0, + gravity: { x: 0, y: 0, z: 0 }, +}; + const DEFAULT_ENTITY_PROPERTIES = { All: { description: "", @@ -299,26 +321,14 @@ const DEFAULT_ENTITY_PROPERTIES = { y: 0, z: 0 }, - damping: 0, angularVelocity: { x: 0, y: 0, z: 0 }, - angularDamping: 0, restitution: 0.5, friction: 0.5, density: 1000, - gravity: { - x: 0, - y: 0, - z: 0 - }, - acceleration: { - x: 0, - y: 0, - z: 0 - }, dynamic: false, }, Shape: { @@ -484,11 +494,6 @@ var toolBar = (function () { dialogWindow = null, tablet = null; - function applyProperties(originalProperties, newProperties) { - for (var key in newProperties) { - originalProperties[key] = newProperties[key]; - } - } function createNewEntity(requestedProperties) { var dimensions = requestedProperties.dimensions ? requestedProperties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); @@ -496,17 +501,23 @@ var toolBar = (function () { var properties = {}; - applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All); + copyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All); var type = requestedProperties.type; if (type === "Box" || type === "Sphere") { - applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape); + copyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape); } else { - applyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]); + copyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]); } // We apply the requested properties first so that they take priority over any default properties. - applyProperties(properties, requestedProperties); + copyProperties(properties, requestedProperties); + + if (properties.dynamic) { + copyProperties(properties, DEFAULT_DYNAMIC_PROPERTIES); + } else { + copyProperties(properties, DEFAULT_NON_DYNAMIC_PROPERTIES); + } if (position !== null && position !== undefined) { @@ -675,7 +686,6 @@ var toolBar = (function () { grabbable: result.grabbable }, dynamic: dynamic, - gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } }); } } @@ -2286,14 +2296,15 @@ var PropertiesTool = function (opts) { }) }; - function updateSelections(selectionUpdated) { + function updateSelections(selectionUpdated, caller) { if (blockPropertyUpdates) { return; } var data = { type: 'update', - spaceMode: selectionDisplay.getSpaceMode() + spaceMode: selectionDisplay.getSpaceMode(), + isPropertiesToolUpdate: caller === this, }; if (selectionUpdated) { @@ -2339,7 +2350,7 @@ var PropertiesTool = function (opts) { emitScriptEvent(data); } - selectionManager.addEventListener(updateSelections); + selectionManager.addEventListener(updateSelections, this); var onWebEventReceived = function(data) { @@ -2523,7 +2534,7 @@ var PropertiesTool = function (opts) { createToolsWindow.webEventReceived.addListener(this, onWebEventReceived); - webView.webEventReceived.connect(onWebEventReceived); + webView.webEventReceived.connect(this, onWebEventReceived); return that; }; diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 8482591771..b15c4e6703 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -459,7 +459,7 @@ function loaded() { isRenameFieldBeingMoved = true; document.body.appendChild(elRenameInput); // keep the focus - elRenameInput.select(); + elRenameInput.focus(); } } @@ -475,7 +475,7 @@ function loaded() { elCell.innerHTML = ""; elCell.appendChild(elRenameInput); // keep the focus - elRenameInput.select(); + elRenameInput.focus(); isRenameFieldBeingMoved = false; } diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 863168d7fd..f450ff2036 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -807,6 +807,21 @@ const GROUPS = [ decimals: 2, propertyID: "speedSpread", }, + { + label: "Shape Type", + type: "dropdown", + options: { "box": "Box", "ellipsoid": "Ellipsoid", + "cylinder-y": "Cylinder", "circle": "Circle", "plane": "Plane", + "compound": "Use Compound Shape URL" }, + propertyID: "particleShapeType", + propertyName: "shapeType", + }, + { + label: "Compound Shape URL", + type: "string", + propertyID: "particleCompoundShapeURL", + propertyName: "compoundShapeURL", + }, { label: "Emit Dimensions", type: "vec3", @@ -3325,6 +3340,13 @@ function loaded() { } let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; + + if (!data.isPropertiesToolUpdate && !hasSelectedEntityChanged && document.hasFocus()) { + // in case the selection has not changed and we still have focus on the properties page, + // we will ignore the event. + return; + } + let doSelectElement = !hasSelectedEntityChanged; // the event bridge and json parsing handle our avatar id string differently. diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js new file mode 100644 index 0000000000..56075a514e --- /dev/null +++ b/scripts/system/html/js/marketplacesInject.js @@ -0,0 +1,379 @@ +/* global $, window, MutationObserver */ + +// +// marketplacesInject.js +// +// Created by David Rowe on 12 Nov 2016. +// Copyright 2016 High Fidelity, Inc. +// +// Injected into marketplace Web pages. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { + // Event bridge messages. + var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; + var CLARA_IO_STATUS = "CLARA.IO STATUS"; + var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; + var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; + var GOTO_DIRECTORY = "GOTO_DIRECTORY"; + var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; + var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; + var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; + var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; + + var canWriteAssets = false; + var xmlHttpRequest = null; + var isPreparing = false; // Explicitly track download request status. + + var marketplaceBaseURL = "https://highfidelity.com"; + var messagesWaiting = false; + + function injectCommonCode(isDirectoryPage) { + // Supporting styles from marketplaces.css. + // Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain. + $("head").append( + '' + ); + + // Supporting styles from edit-style.css. + // Font family, size, and position adjusted because Raleway-Bold cannot be used cross-domain. + $("head").append( + '' + ); + + // Footer. + var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace?"); + $("body").append( + '
' + + (!isInitialHiFiPage ? '' : '') + + (isInitialHiFiPage ? '🛈 Get items from Clara.io!' : '') + + (!isDirectoryPage ? '' : '') + + (isDirectoryPage ? '🛈 Select a marketplace to explore.' : '') + + '
' + ); + + // Footer actions. + $("#back-button").on("click", function () { + if (document.referrer !== "") { + window.history.back(); + } else { + var params = { type: GOTO_MARKETPLACE }; + var itemIdMatch = location.search.match(/itemId=([^&]*)/); + if (itemIdMatch && itemIdMatch.length === 2) { + params.itemId = itemIdMatch[1]; + } + EventBridge.emitWebEvent(JSON.stringify(params)); + } + }); + $("#all-markets").on("click", function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_DIRECTORY + })); + }); + } + + function injectDirectoryCode() { + + // Remove e-mail hyperlink. + var letUsKnow = $("#letUsKnow"); + letUsKnow.replaceWith(letUsKnow.html()); + + // Add button links. + + $('#exploreClaraMarketplace').on('click', function () { + window.location = "https://clara.io/library?gameCheck=true&public=true"; + }); + $('#exploreHifiMarketplace').on('click', function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_MARKETPLACE + })); + }); + } + + function updateClaraCode() { + // Have to repeatedly update Clara page because its content can change dynamically without location.href changing. + + // Clara library page. + if (location.href.indexOf("clara.io/library") !== -1) { + // Make entries navigate to "Image" view instead of default "Real Time" view. + var elements = $("a.thumbnail"); + for (var i = 0, length = elements.length; i < length; i++) { + var value = elements[i].getAttribute("href"); + if (value.slice(-6) !== "/image") { + elements[i].setAttribute("href", value + "/image"); + } + } + } + + // Clara item page. + if (location.href.indexOf("clara.io/view/") !== -1) { + // Make site navigation links retain gameCheck etc. parameters. + var element = $("a[href^=\'/library\']")[0]; + var parameters = "?gameCheck=true&public=true"; + var href = element.getAttribute("href"); + if (href.slice(-parameters.length) !== parameters) { + element.setAttribute("href", href + parameters); + } + + // Remove unwanted buttons and replace download options with a single "Download to High Fidelity" button. + var buttons = $("a.embed-button").parent("div"); + var downloadFBX; + if (buttons.find("div.btn-group").length > 0) { + buttons.children(".btn-primary, .btn-group , .embed-button").each(function () { this.remove(); }); + if ($("#hifi-download-container").length === 0) { // Button hasn't been moved already. + downloadFBX = $(' Download to High Fidelity'); + buttons.prepend(downloadFBX); + downloadFBX[0].addEventListener("click", startAutoDownload); + } + } + + // Move the "Download to High Fidelity" button to be more visible on tablet. + if ($("#hifi-download-container").length === 0 && window.innerWidth < 700) { + var downloadContainer = $('
'); + $(".top-title .col-sm-4").append(downloadContainer); + downloadContainer.append(downloadFBX); + } + } + } + + // Automatic download to High Fidelity. + function startAutoDownload() { + // One file request at a time. + if (isPreparing) { + console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); + return; + } + + // User must be able to write to Asset Server. + if (!canWriteAssets) { + console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); + EventBridge.emitWebEvent(JSON.stringify({ + type: WARN_USER_NO_PERMISSIONS + })); + return; + } + + // User must be logged in. + var loginButton = $("#topnav a[href='/signup']"); + if (loginButton.length > 0) { + loginButton[0].click(); + return; + } + + // Obtain zip file to download for requested asset. + // Reference: https://clara.io/learn/sdk/api/export + + //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; + // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to + // be successful in generating zip files. + var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; + + var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; + var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); + + xmlHttpRequest = new XMLHttpRequest(); + var responseTextIndex = 0; + var zipFileURL = ""; + + xmlHttpRequest.onreadystatechange = function () { + // Messages are appended to responseText; process the new ones. + var message = this.responseText.slice(responseTextIndex); + var statusMessage = ""; + + if (isPreparing) { // Ignore messages in flight after finished/cancelled. + var lines = message.split(/[\n\r]+/); + + for (var i = 0, length = lines.length; i < length; i++) { + if (lines[i].slice(0, 5) === "data:") { + // Parse line. + var data; + try { + data = JSON.parse(lines[i].slice(5)); + } + catch (e) { + data = {}; + } + + // Extract zip file URL. + if (data.hasOwnProperty("files") && data.files.length > 0) { + zipFileURL = data.files[0].url; + } + } + } + + if (statusMessage !== "") { + // Update the UI with the most recent status message. + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } + } + + responseTextIndex = this.responseText.length; + }; + + // Note: onprogress doesn't have computable total length so can't use it to determine % complete. + + xmlHttpRequest.onload = function () { + var statusMessage = ""; + + if (!isPreparing) { + return; + } + + isPreparing = false; + + var HTTP_OK = 200; + if (this.status !== HTTP_OK) { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } else if (zipFileURL.slice(-4) !== ".zip") { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: (statusMessage + ": " + zipFileURL) + })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_DOWNLOAD + })); + } + + xmlHttpRequest = null; + } + + isPreparing = true; + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: "Initiating download" + })); + + xmlHttpRequest.open("POST", url, true); + xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); + xmlHttpRequest.send(); + } + + function injectClaraCode() { + + // Make space for marketplaces footer in Clara pages. + $("head").append( + '' + ); + + // Condense space. + $("head").append( + '' + ); + + // Move "Download to High Fidelity" button. + $("head").append( + '' + ); + + // Update code injected per page displayed. + var updateClaraCodeInterval = undefined; + updateClaraCode(); + updateClaraCodeInterval = setInterval(function () { + updateClaraCode(); + }, 1000); + + window.addEventListener("unload", function () { + clearInterval(updateClaraCodeInterval); + updateClaraCodeInterval = undefined; + }); + + EventBridge.emitWebEvent(JSON.stringify({ + type: QUERY_CAN_WRITE_ASSETS + })); + } + + function cancelClaraDownload() { + isPreparing = false; + + if (xmlHttpRequest) { + xmlHttpRequest.abort(); + xmlHttpRequest = null; + console.log("Clara.io FBX: File download cancelled"); + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_CANCELLED_DOWNLOAD + })); + } + } + + function injectCode() { + var DIRECTORY = 0; + var HIFI = 1; + var CLARA = 2; + var HIFI_ITEM_PAGE = 3; + var pageType = DIRECTORY; + + if (location.href.indexOf(marketplaceBaseURL + "/") !== -1) { pageType = HIFI; } + if (location.href.indexOf("clara.io/") !== -1) { pageType = CLARA; } + if (location.href.indexOf(marketplaceBaseURL + "/marketplace/items/") !== -1) { pageType = HIFI_ITEM_PAGE; } + + injectCommonCode(pageType === DIRECTORY); + switch (pageType) { + case DIRECTORY: + injectDirectoryCode(); + break; + case CLARA: + injectClaraCode(); + break; + } + } + + function onLoad() { + EventBridge.scriptEventReceived.connect(function (message) { + message = JSON.parse(message); + if (message.type === CAN_WRITE_ASSETS) { + canWriteAssets = message.canWriteAssets; + } else if (message.type === CLARA_IO_CANCEL_DOWNLOAD) { + cancelClaraDownload(); + } else if (message.type === "marketplaces") { + if (message.action === "commerceSetting") { + marketplaceBaseURL = message.data.metaverseServerURL; + if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { + marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); + } + messagesWaiting = message.data.messagesWaiting; + injectCode(); + } + } + }); + + // Request commerce setting + // Code is injected into the webpage after the setting comes back. + EventBridge.emitWebEvent(JSON.stringify({ + type: "REQUEST_SETTING" + })); + } + + // Load / unload. + window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). + window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed +}()); diff --git a/scripts/system/libraries/cloneEntityUtils.js b/scripts/system/libraries/cloneEntityUtils.js index e0f4aba84a..f789e19cd8 100644 --- a/scripts/system/libraries/cloneEntityUtils.js +++ b/scripts/system/libraries/cloneEntityUtils.js @@ -5,8 +5,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global entityIsCloneable:true, getGrabbableData:true, cloneEntity:true, propsAreCloneDynamic:true, Script, - propsAreCloneDynamic:true, Entities*/ +/* global entityIsCloneable:true, cloneEntity:true, propsAreCloneDynamic:true, Script, + propsAreCloneDynamic:true, Entities, Uuid */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -47,13 +47,10 @@ propsAreCloneDynamic = function(props) { }; cloneEntity = function(props) { - var entityToClone = props.id; - var props = Entities.getEntityProperties(entityToClone, ['certificateID', 'certificateType']) - var certificateID = props.certificateID; - // ensure entity is cloneable and does not have a certificate ID, whereas cloneable limits - // will now be handled by the server where the entity add will fail if limit reached - if (entityIsCloneable(props) && (!!certificateID || props.certificateType.indexOf('domainUnlimited') >= 0)) { - var cloneID = Entities.cloneEntity(entityToClone); + var entityIDToClone = props.id; + if (entityIsCloneable(props) && + (Uuid.isNull(props.certificateID) || props.certificateType.indexOf('domainUnlimited') >= 0)) { + var cloneID = Entities.cloneEntity(entityIDToClone); return cloneID; } return null; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 385ed954b0..51645e5502 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -156,7 +156,9 @@ DISPATCHER_PROPERTIES = [ "grab.equippableIndicatorOffset", "userData", "avatarEntity", - "owningAvatarID" + "owningAvatarID", + "certificateID", + "certificateType" ]; // priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step @@ -339,8 +341,6 @@ entityIsGrabbable = function (eigProps) { var grabbable = getGrabbableData(eigProps).grabbable; if (!grabbable || eigProps.locked || - isAnothersAvatarEntity(eigProps) || - isAnothersChildEntity(eigProps) || FORBIDDEN_GRAB_TYPES.indexOf(eigProps.type) >= 0) { return false; } diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 269283ea6d..064dafec06 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -128,8 +128,11 @@ SelectionManager = (function() { } }; - that.addEventListener = function(func) { - listeners.push(func); + that.addEventListener = function(func, thisContext) { + listeners.push({ + callback: func, + thisContext: thisContext + }); }; that.hasSelection = function() { @@ -572,7 +575,7 @@ SelectionManager = (function() { for (var j = 0; j < listeners.length; j++) { try { - listeners[j](selectionUpdated === true, caller); + listeners[j].callback.call(listeners[j].thisContext, selectionUpdated === true, caller); } catch (e) { print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e)); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index e059081741..38287e3af3 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -615,11 +615,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openMarketplace(message.itemId, message.itemEdition); break; case 'passphrasePopup_cancelClicked': - case 'needsLogIn_cancelClicked': // Should/must NOT check for wallet setup. openMarketplace(); break; - case 'needsLogIn_loginClicked': + case 'marketplace_loginClicked': openLoginWindow(); break; case 'disableHmdPreview': diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js index 449921514c..91c8b1edcf 100644 --- a/scripts/system/miniTablet.js +++ b/scripts/system/miniTablet.js @@ -1048,6 +1048,7 @@ // Track grabbed state and item. switch (message.action) { case "grab": + case "equip": grabbingHand = HAND_NAMES.indexOf(message.joint); grabbedItem = message.grabbedEntity; break; @@ -1056,7 +1057,7 @@ grabbedItem = null; break; default: - error("Unexpected grab message!"); + error("Unexpected grab message: " + JSON.stringify(message)); return; } @@ -1144,4 +1145,4 @@ setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}()); diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 1d6b4dada3..7fdb863a83 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -203,7 +203,7 @@ // Notification plane positions noticeY = -sensorScaleFactor * (y * NOTIFICATION_3D_SCALE + 0.5 * noticeHeight); notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: 0.5 * sensorScaleFactor * (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH), y: noticeY, z: 0.001 }; + buttonPosition = { x: sensorScaleFactor * (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH), y: noticeY, z: 0.001 }; // Rotate plane notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, @@ -241,7 +241,7 @@ noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - notice.size = { x: noticeWidth, y: noticeHeight }; + notice.size = { x: noticeWidth * sensorScaleFactor, y: noticeHeight * sensorScaleFactor }; positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); @@ -249,8 +249,8 @@ notice.parentJointIndex = -2; if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE * sensorScaleFactor; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE * sensorScaleFactor; notice.bottomMargin = 0; notice.rightMargin = 0; notice.lineHeight = 10.0 * (fontSize * sensorScaleFactor / 12.0) * NOTIFICATION_3D_SCALE; @@ -267,14 +267,15 @@ button.isFacingAvatar = false; button.parentID = MyAvatar.sessionUUID; button.parentJointIndex = -2; + button.visible = false; buttons.push((Overlays.addOverlay("image3d", button))); overlay3DDetails.push({ notificationOrientation: positions.notificationOrientation, notificationPosition: positions.notificationPosition, buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight + width: noticeWidth * sensorScaleFactor, + height: noticeHeight * sensorScaleFactor }); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 0c4338b31d..4cf6ef1c34 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -284,7 +284,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See print("Error: unable to remove connection", connectionUserName, error || response.status); return; } - sendToQml({ method: 'refreshConnections' }); + sendToQml({ method: 'connectionRemoved', params: connectionUserName }); }); break; diff --git a/tools/dissectors/1-hfudt.lua b/tools/dissectors/1-hfudt.lua index de99c1ce3c..8179276dbb 100644 --- a/tools/dissectors/1-hfudt.lua +++ b/tools/dissectors/1-hfudt.lua @@ -152,7 +152,9 @@ local packet_types = { [97] = "OctreeDataPersist", [98] = "EntityClone", [99] = "EntityQueryInitialResultsComplete", - [100] = "BulkAvatarTraits" + [100] = "BulkAvatarTraits", + [101] = "AudioSoloRequest", + [102] = "BulkAvatarTraitsAck" } local unsourced_packet_types = { @@ -301,55 +303,53 @@ function p_hfudt.dissector(buf, pinfo, tree) -- check if we have part of a message that we need to re-assemble -- before it can be dissected - if obfuscation_bits == 0 then - if message_bit == 1 and message_position ~= 0 then - if fragments[message_number] == nil then - fragments[message_number] = {} - end - - if fragments[message_number][message_part_number] == nil then - fragments[message_number][message_part_number] = {} - end - - -- set the properties for this fragment - fragments[message_number][message_part_number] = { - payload = buf(i):bytes() - } - - -- if this is the last part, set our maximum part number - if message_position == 1 then - fragments[message_number].last_part_number = message_part_number - end - - -- if we have the last part - -- enumerate our parts for this message and see if everything is present - if fragments[message_number].last_part_number ~= nil then - local i = 0 - local has_all = true - - local finalMessage = ByteArray.new() - local message_complete = true - - while i <= fragments[message_number].last_part_number do - if fragments[message_number][i] ~= nil then - finalMessage = finalMessage .. fragments[message_number][i].payload - else - -- missing this part, have to break until we have it - message_complete = false - end - - i = i + 1 - end - - if message_complete then - debug("Message " .. message_number .. " is " .. finalMessage:len()) - payload_to_dissect = ByteArray.tvb(finalMessage, message_number) - end - end - - else - payload_to_dissect = buf(i):tvb() + if message_bit == 1 and message_position ~= 0 then + if fragments[message_number] == nil then + fragments[message_number] = {} end + + if fragments[message_number][message_part_number] == nil then + fragments[message_number][message_part_number] = {} + end + + -- set the properties for this fragment + fragments[message_number][message_part_number] = { + payload = buf(i):bytes() + } + + -- if this is the last part, set our maximum part number + if message_position == 1 then + fragments[message_number].last_part_number = message_part_number + end + + -- if we have the last part + -- enumerate our parts for this message and see if everything is present + if fragments[message_number].last_part_number ~= nil then + local i = 0 + local has_all = true + + local finalMessage = ByteArray.new() + local message_complete = true + + while i <= fragments[message_number].last_part_number do + if fragments[message_number][i] ~= nil then + finalMessage = finalMessage .. fragments[message_number][i].payload + else + -- missing this part, have to break until we have it + message_complete = false + end + + i = i + 1 + end + + if message_complete then + debug("Message " .. message_number .. " is " .. finalMessage:len()) + payload_to_dissect = ByteArray.tvb(finalMessage, message_number) + end + end + + else + payload_to_dissect = buf(i):tvb() end if payload_to_dissect ~= nil then diff --git a/tools/jsdoc/README.md b/tools/jsdoc/README.md index f9864a21e4..f3dda84291 100644 --- a/tools/jsdoc/README.md +++ b/tools/jsdoc/README.md @@ -15,6 +15,13 @@ To generate html documentation for the High Fidelity JavaScript API: The out folder should contain index.html. +If you get a "JavaScript heap out of memory" error when running the `jsdoc` command you need to increase the amount of memory +available to it. For example, to increase the memory available to 2GB on Windows: +* `where jsdoc` to find the `jsdoc.cmd` file. +* Edit the `jsdoc.cmd` file to add `--max-old-space-size=2048` after the `node` and/or `node.exe` commands. + +Reference: https://medium.com/@vuongtran/how-to-solve-process-out-of-memory-in-node-js-5f0de8f8464c + To generate the grav automation files, run node gravPrep.js after you have made a JSdoc output folder. This will create files that are needed for hifi-grav and hifi-grav-content repos diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl index b9a0e0ca86..c5fdefc7d8 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl @@ -13,8 +13,8 @@ var self = this; - - + + diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index b4350ddbdb..5ec94b46aa 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -107,6 +107,9 @@ exports.handlers = { if (e.doclet.hifiClientEntity) { rows.push("Client Entity Scripts"); } + if (e.doclet.hifiAvatar) { + rows.push("Avatar Scripts"); + } if (e.doclet.hifiServerEntity) { rows.push("Server Entity Scripts"); } @@ -121,23 +124,14 @@ exports.handlers = { e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn; } } + + if (e.doclet.kind === "function" && e.doclet.returns && e.doclet.returns[0].type + && e.doclet.returns[0].type.names[0] === "Signal") { + e.doclet.kind = "signal"; + } } }; -// Functions for adding @signal custom tag -/** @private */ -function setDocletKindToTitle(doclet, tag) { - doclet.addTag( 'kind', tag.title ); -} - -function setDocletNameToValue(doclet, tag) { - if (tag.value && tag.value.description) { // as in a long tag - doclet.addTag('name', tag.value.description); - } else if (tag.text) { // or a short tag - doclet.addTag('name', tag.text); - } -} - // Define custom hifi tags here exports.defineTags = function (dictionary) { @@ -155,6 +149,13 @@ exports.defineTags = function (dictionary) { } }); + // @hifi-avatar-script + dictionary.defineTag("hifi-avatar", { + onTagged: function (doclet, tag) { + doclet.hifiAvatar = true; + } + }); + // @hifi-client-entity dictionary.defineTag("hifi-client-entity", { onTagged: function (doclet, tag) { @@ -168,14 +169,5 @@ exports.defineTags = function (dictionary) { doclet.hifiServerEntity = true; } }); - - // @signal - dictionary.defineTag("signal", { - mustHaveValue: true, - onTagged: function(doclet, tag) { - setDocletKindToTitle(doclet, tag); - setDocletNameToValue(doclet, tag); - } - }); -}; \ No newline at end of file +}; diff --git a/tools/nitpick/compiledResources/.placeholder b/tools/nitpick/compiledResources/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc deleted file mode 100644 index 15f51ed7f4..0000000000 Binary files a/tools/nitpick/compiledResources/resources.rcc and /dev/null differ diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index a098d17917..19697d51dc 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -8,6 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "AWSInterface.h" +#include "common.h" #include #include @@ -29,7 +30,9 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user ) { _workingDirectory = workingDirectory; @@ -53,6 +56,9 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, _urlLineEdit = urlLineEdit; _urlLineEdit->setEnabled(false); + _branch = branch; + _user = user; + QString zipFilenameWithoutExtension = zipFilename.split('.')[0]; extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension); @@ -202,13 +208,21 @@ void AWSInterface::writeTitle(QTextStream& stream, const QStringList& originalNa stream << "run on " << hostName << "\n"; - int numberOfFailures = originalNamesFailures.length(); - int numberOfSuccesses = originalNamesSuccesses.length(); + stream << "

"; + stream << "nitpick " << nitpickVersion; + stream << ", tests from GitHub: " << _user << "/" << _branch; + stream << "

"; - stream << "

" << QString::number(numberOfFailures) << " failed, out of a total of " << QString::number(numberOfSuccesses) << " tests

\n"; + _numberOfFailures = originalNamesFailures.length(); + _numberOfSuccesses = originalNamesSuccesses.length(); + + stream << "

" << QString::number(_numberOfFailures) << " failed, out of a total of " << QString::number(_numberOfFailures + _numberOfSuccesses) << " tests

\n"; stream << "\t" << "\t" << "\n"; - stream << "\t" << "\t" << "

The following tests failed:

"; + + if (_numberOfFailures > 0) { + stream << "\t" << "\t" << "

The following tests failed:

"; + } } void AWSInterface::writeTable(QTextStream& stream, const QStringList& originalNamesFailures, const QStringList& originalNamesSuccesses) { @@ -289,7 +303,10 @@ void AWSInterface::writeTable(QTextStream& stream, const QStringList& originalNa closeTable(stream); stream << "\t" << "\t" << "\n"; - stream << "\t" << "\t" << "

The following tests passed:

"; + + if (_numberOfSuccesses > 0) { + stream << "\t" << "\t" << "

The following tests passed:

"; + } // Now do the same for passes folderNames.clear(); diff --git a/tools/nitpick/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h index 77d500fa7c..a2e4e36c37 100644 --- a/tools/nitpick/src/AWSInterface.h +++ b/tools/nitpick/src/AWSInterface.h @@ -31,7 +31,10 @@ public: QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit); + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ); void extractTestFailuresFromZippedFolder(const QString& folderName); void createHTMLFile(); @@ -70,9 +73,13 @@ private: QString AWS_BUCKET{ "hifi-qa" }; QLineEdit* _urlLineEdit; - + QString _user; + QString _branch; QString _comparisonImageFilename; + + int _numberOfFailures; + int _numberOfSuccesses; }; #endif // hifi_AWSInterface_h \ No newline at end of file diff --git a/tools/nitpick/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp index 7e3e6eaf63..b35c5d639d 100644 --- a/tools/nitpick/src/ImageComparer.cpp +++ b/tools/nitpick/src/ImageComparer.cpp @@ -43,6 +43,8 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec int windowCounter{ 0 }; double ssim{ 0.0 }; + double worstTileValue{ 1.0 }; + double min { 1.0 }; double max { -1.0 }; @@ -108,6 +110,10 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec if (value < min) min = value; if (value > max) max = value; + if (value < worstTileValue) { + worstTileValue = value; + } + ++windowCounter; y += WIN_SIZE; @@ -122,12 +128,17 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec _ssimResults.min = min; _ssimResults.max = max; _ssimResults.ssim = ssim / windowCounter; + _ssimResults.worstTileValue = worstTileValue; }; double ImageComparer::getSSIMValue() { return _ssimResults.ssim; } +double ImageComparer::getWorstTileValue() { + return _ssimResults.worstTileValue; +} + SSIMResults ImageComparer::getSSIMResults() { return _ssimResults; } diff --git a/tools/nitpick/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h index fc14dab94d..a18e432a01 100644 --- a/tools/nitpick/src/ImageComparer.h +++ b/tools/nitpick/src/ImageComparer.h @@ -18,7 +18,9 @@ class ImageComparer { public: void compareImages(const QImage& resultImage, const QImage& expectedImage); + double getSSIMValue(); + double getWorstTileValue(); SSIMResults getSSIMResults(); diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp index fd5df0dd4e..2a7aca9f2e 100644 --- a/tools/nitpick/src/MismatchWindow.cpp +++ b/tools/nitpick/src/MismatchWindow.cpp @@ -61,7 +61,7 @@ QPixmap MismatchWindow::computeDiffPixmap(const QImage& expectedImage, const QIm } void MismatchWindow::setTestResult(const TestResult& testResult) { - errorLabel->setText("Similarity: " + QString::number(testResult._error)); + errorLabel->setText("Similarity: " + QString::number(testResult._errorGlobal) + " (worst tile: " + QString::number(testResult._errorLocal) + ")"); imagePath->setText("Path to test: " + testResult._pathname); diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..02ed120350 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - " + nitpickVersion); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); @@ -266,7 +266,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() { } void Nitpick::on_createWebPagePushbutton_clicked() { - _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit); + _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit, _ui.branchLineEdit->text(), _ui.userLineEdit->text()); } void Nitpick::about() { diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..1e5b15360f 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -83,6 +83,7 @@ int TestCreator::compareImageLists() { QImage expectedImage(_expectedImagesFullFilenames[i]); double similarityIndex; // in [-1.0 .. 1.0], where 1.0 means images are identical + double worstTileValue; // in [-1.0 .. 1.0], where 1.0 means images are identical bool isInteractiveMode = (!_isRunningFromCommandLine && _checkBoxInteractiveMode->isChecked() && !_isRunningInAutomaticTestRun); @@ -90,13 +91,16 @@ int TestCreator::compareImageLists() { if (isInteractiveMode && (resultImage.width() != expectedImage.width() || resultImage.height() != expectedImage.height())) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); similarityIndex = -100.0; + worstTileValue = 0.0; } else { _imageComparer.compareImages(resultImage, expectedImage); similarityIndex = _imageComparer.getSSIMValue(); + worstTileValue = _imageComparer.getWorstTileValue(); } TestResult testResult = TestResult{ - (float)similarityIndex, + similarityIndex, + worstTileValue, _expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of result image @@ -105,10 +109,9 @@ int TestCreator::compareImageLists() { _mismatchWindow.setTestResult(testResult); - if (similarityIndex < THRESHOLD) { - ++numberOfFailures; - + if (similarityIndex < THRESHOLD_GLOBAL || worstTileValue < THRESHOLD_LOCAL) { if (!isInteractiveMode) { + ++numberOfFailures; appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); } else { _mismatchWindow.exec(); @@ -117,6 +120,7 @@ int TestCreator::compareImageLists() { case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: + ++numberOfFailures; appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); break; case USER_RESPONSE_ABORT: @@ -198,7 +202,8 @@ void TestCreator::appendTestResultsToFile(const TestResult& testResult, const QP stream << "TestCreator in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' stream << "Expected image was " << testResult._expectedImageFilename << endl; stream << "Actual image was " << testResult._actualImageFilename << endl; - stream << "Similarity index was " << testResult._error << endl; + stream << "Similarity index was " << testResult._errorGlobal << endl; + stream << "Worst tile was " << testResult._errorLocal << endl; descriptionFile.close(); @@ -819,6 +824,10 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti // If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant if (directories.length() == 0) { + QString testRecursivePathname = directory + "/" + TEST_RECURSIVE_FILENAME; + if (QFile::exists(testRecursivePathname)) { + QFile::remove(testRecursivePathname); + } return; } @@ -851,10 +860,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; @@ -1112,7 +1118,10 @@ void TestCreator::createWebPage( QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ) { QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, "Zipped TestCreator Results (TestResults--*.zip)"); @@ -1136,6 +1145,8 @@ void TestCreator::createWebPage( updateAWSCheckBox, diffImageRadioButton, ssimImageRadionButton, - urlLineEdit + urlLineEdit, + branch, + user ); } \ No newline at end of file diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index 7cd38b42d4..e708529ba4 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -107,7 +107,10 @@ public: QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit); + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ); private: QProgressBar* _progressBar; @@ -121,7 +124,8 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD{ 0.9999 }; + const double THRESHOLD_GLOBAL{ 0.9995 }; + const double THRESHOLD_LOCAL { 0.6 }; QDir _imageDirectory; diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 4d0d18ef3d..53a74da82f 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -9,6 +9,7 @@ // #include "TestRunnerMobile.h" +#include #include #include #include @@ -43,7 +44,7 @@ TestRunnerMobile::TestRunnerMobile( _installAPKPushbutton = installAPKPushbutton; _runInterfacePushbutton = runInterfacePushbutton; - folderLineEdit->setText("/sdcard/DCIM/TEST"); + folderLineEdit->setText("/sdcard/snapshots"); modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; modelNames["SM_N960U1"] = "Samsung Note 9 unlocked"; @@ -60,6 +61,7 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { setWorkingFolder(_workingFolderLabel); _connectDeviceButton->setEnabled(true); + _downloadAPKPushbutton->setEnabled(true); } void TestRunnerMobile::connectDevice() { @@ -68,13 +70,25 @@ void TestRunnerMobile::connectDevice() { _adbInterface = new AdbInterface(); } + // Get list of devices QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; appendLog(command); system(command.toStdString().c_str()); if (!QFile::exists(devicesFullFilename)) { - QMessageBox::critical(0, "Internal error", "devicesFullFilename not found"); + QMessageBox::critical(0, "Internal error", "devices.txt not found"); + exit (-1); + } + + // Get device IP address + QString ifconfigFullFilename{ _workingFolder + "/ifconfig.txt" }; + command = _adbInterface->getAdbCommand() + " shell ifconfig > " + ifconfigFullFilename; + appendLog(command); + system(command.toStdString().c_str()); + + if (!QFile::exists(ifconfigFullFilename)) { + QMessageBox::critical(0, "Internal error", "ifconfig.txt not found"); exit (-1); } @@ -85,6 +99,8 @@ void TestRunnerMobile::connectDevice() { QString line2 = devicesFile.readLine(); const QString DEVICE{ "device" }; + const QString MODEL{ "model" }; + if (line2.contains("unauthorized")) { QMessageBox::critical(0, "Unauthorized device detected", "Please allow USB debugging on device"); } else if (line2.contains(DEVICE)) { @@ -97,13 +113,20 @@ void TestRunnerMobile::connectDevice() { QStringList tokens = line2.split(QRegExp("[\r\n\t ]+")); QString deviceID = tokens[0]; - QString modelID = tokens[3].split(':')[1]; - QString modelName = "UNKNOWN"; - if (modelNames.count(modelID) == 1) { - modelName = modelNames[modelID]; + // Find the model entry + _modelName = "UNKNOWN"; + for (int i = 0; i < tokens.size(); ++i) { + if (tokens[i].contains(MODEL)) { + QString modelID = tokens[i].split(':')[1]; + + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; + } + break; + } } - _detectedDeviceLabel->setText(modelName + " [" + deviceID + "]"); + _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]"); _pullFolderButton->setEnabled(true); _folderLineEdit->setEnabled(true); _downloadAPKPushbutton->setEnabled(true); @@ -154,8 +177,6 @@ void TestRunnerMobile::downloadComplete() { } else { _statusLabel->setText("Installer download complete"); } - - _installAPKPushbutton->setEnabled(true); } void TestRunnerMobile::installAPK() { @@ -164,22 +185,16 @@ void TestRunnerMobile::installAPK() { _adbInterface = new AdbInterface(); } - if (_installerFilename.isNull()) { - QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, - "Available APKs (*.apk)" - ); + QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, + "Available APKs (*.apk)" + ); - if (installerPathname.isNull()) { - return; - } - - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; + if (installerPathname.isNull()) { + return; } _statusLabel->setText("Installing"); - QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); @@ -198,14 +213,28 @@ void TestRunnerMobile::runInterface() { ? QString("https://raw.githubusercontent.com/") + nitpick->getSelectedUser() + "/hifi_tests/" + nitpick->getSelectedBranch() + "/tests/testRecursive.js" : _scriptURL->text(); + // Quest and Android have different commands to run interface + QString startCommand; + if (_modelName == "Quest") { + startCommand = "io.highfidelity.questInterface/.PermissionsChecker"; + } else { + startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; + } + + QString serverIP { getServerIP() }; + if (serverIP == NETWORK_NOT_FOUND) { + _runInterfacePushbutton->setEnabled(false); + return; + } + QString command = _adbInterface->getAdbCommand() + - " shell am start -n io.highfidelity.hifiinterface/.PermissionChecker" + - " --es args \\\"" + - " --url file:///~/serverless/tutorial.json" + - " --no-updater" + - " --no-login-suggestion" + - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation /sdcard/snapshots" + + " shell am start -n " + startCommand + + " --es args \\\"" + + " --url hifi://" + serverIP + "/0,0,0" + " --no-updater" + + " --no-login-suggestion" + + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation /sdcard/snapshots" + "\\\""; appendLog(command); @@ -227,3 +256,85 @@ void TestRunnerMobile::pullFolder() { _statusLabel->setText("Pull complete"); #endif } + +QString TestRunnerMobile::getServerIP() { + // Get device IP (ifconfig.txt was created when connecting) + QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" }; + if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'ifconfig.txt'"); + exit(-1); + } + + QTextStream stream(&ifconfigFile); + QString line = ifconfigFile.readLine(); + while (!line.isNull()) { + // The device IP is in the line following the "wlan0" line + if (line.left(6) == "wlan0 ") { + break; + } + line = ifconfigFile.readLine(); + } + + // The following line looks like this "inet addr:192.168.0.15 Bcast:192.168.0.255 Mask:255.255.255.0" + // Extract the address and mask + line = ifconfigFile.readLine(); + QStringList lineParts = line.split(':'); + if (lineParts.size() < 4) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "IP address line not in expected format: " + line + "(check that device WIFI is on)"); + + return NETWORK_NOT_FOUND; + } + + qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]); + qint64 deviceMask = convertToBinary(lineParts[3].split(' ')[0]); + qint64 deviceSubnet = deviceMask & deviceIP; + + // The device needs to be on the same subnet as the server + // To find which of our IPs is the server - choose the 1st that is on the same subnet + // If more than one found then report an error + + QString serverIP; + + QList interfaces = QNetworkInterface::allInterfaces(); + for (int i = 0; i < interfaces.count(); i++) { + QList entries = interfaces.at(i).addressEntries(); + for (int j = 0; j < entries.count(); j++) { + if (entries.at(j).ip().protocol() == QAbstractSocket::IPv4Protocol) { + qint64 hostIP = convertToBinary(entries.at(j).ip().toString()); + qint64 hostMask = convertToBinary(entries.at(j).netmask().toString()); + qint64 hostSubnet = hostMask & hostIP; + + if (hostSubnet == deviceSubnet) { + if (!serverIP.isNull()) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Cannot identify server IP (multiple interfaces on device submask)"); + return QString("CANNOT IDENTIFY SERVER IP"); + } else { + union { + uint32_t ip; + uint8_t bytes[4]; + } u; + u.ip = hostIP; + + serverIP = QString::number(u.bytes[3]) + '.' + QString::number(u.bytes[2]) + '.' + QString::number(u.bytes[1]) + '.' + QString::number(u.bytes[0]); + } + } + } + } + } + + ifconfigFile.close(); + + return serverIP; +} + +qint64 TestRunnerMobile::convertToBinary(const QString& str) { + QString binary; + foreach (const QString& s, str.split(".")) { + binary += QString::number(s.toInt(), 2).rightJustified(8, '0'); + } + + return binary.toLongLong(NULL, 2); +} diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index f7b16da6f8..09f847785b 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -52,6 +52,9 @@ public: void pullFolder(); + QString getServerIP(); + qint64 convertToBinary (const QString& str); + private: QPushButton* _connectDeviceButton; QPushButton* _pullFolderButton; @@ -75,5 +78,9 @@ private: std::map modelNames; AdbInterface* _adbInterface; + + QString _modelName; + + QString NETWORK_NOT_FOUND{ "NETWORK NOT FOUND"}; }; #endif diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index eb228ff2b3..51f1f85113 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -18,7 +18,9 @@ public: int width; int height; std::vector results; + double ssim; + double worstTileValue; // Used for scaling double min; @@ -27,15 +29,17 @@ public: class TestResult { public: - TestResult(float error, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : - _error(error), + TestResult(double errorGlobal, double errorLocal, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : + _errorGlobal(errorGlobal), + _errorLocal(errorLocal), _pathname(pathname), _expectedImageFilename(expectedImageFilename), _actualImageFilename(actualImageFilename), _ssimResults(ssimResults) {} - double _error; + double _errorGlobal; + double _errorLocal; QString _pathname; QString _expectedImageFilename; @@ -56,4 +60,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; +const QString nitpickVersion{ "v3.1.5" }; #endif // hifi_common_h \ No newline at end of file diff --git a/tools/nitpick/ui/MismatchWindow.ui b/tools/nitpick/ui/MismatchWindow.ui index 8a174989d4..fa3e21957f 100644 --- a/tools/nitpick/ui/MismatchWindow.ui +++ b/tools/nitpick/ui/MismatchWindow.ui @@ -45,7 +45,7 @@ - 540 + 900 480 800 450 @@ -78,7 +78,7 @@ 60 630 - 480 + 540 28 @@ -145,7 +145,7 @@ - Abort current test + Abort evaluation diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index a0f368863d..e1d7699f22 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -46,7 +46,7 @@ - 5 + 0 @@ -620,7 +620,7 @@ <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> - usePreviousInstallation + Use Previous Installation false @@ -895,7 +895,7 @@ <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> - usePreviousInstallation + Use Previous Installation false diff --git a/tools/noramlizeFrame.py b/tools/noramlizeFrame.py deleted file mode 100644 index f1012f6428..0000000000 --- a/tools/noramlizeFrame.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import json -import shutil -import sys - -def scriptRelative(*paths): - scriptdir = os.path.dirname(os.path.realpath(sys.argv[0])) - result = os.path.join(scriptdir, *paths) - result = os.path.realpath(result) - result = os.path.normcase(result) - return result - - - -class FrameProcessor: - def __init__(self, filename): - self.filename = filename - dir, name = os.path.split(self.filename) - self.dir = dir - self.ktxDir = os.path.join(self.dir, 'ktx') - os.makedirs(self.ktxDir, exist_ok=True) - self.resDir = scriptRelative("../interface/resources") - - if (name.endswith(".json")): - self.name = name[0:-5] - else: - self.name = name - self.filename = self.filename + '.json' - - with open(self.filename, 'r') as f: - self.json = json.load(f) - - - def processKtx(self, texture): - if texture is None: return - if not 'ktxFile' in texture: return - sourceKtx = texture['ktxFile'] - if sourceKtx.startswith(':'): - sourceKtx = sourceKtx[1:] - while sourceKtx.startswith('/'): - sourceKtx = sourceKtx[1:] - sourceKtx = os.path.join(self.resDir, sourceKtx) - sourceKtxDir, sourceKtxName = os.path.split(sourceKtx) - destKtx = os.path.join(self.ktxDir, sourceKtxName) - if not os.path.isfile(destKtx): - shutil.copy(sourceKtx, destKtx) - newValue = 'ktx/' + sourceKtxName - texture['ktxFile'] = newValue - - - def process(self): - for texture in self.json['textures']: - self.processKtx(texture) - - with open(self.filename, 'w') as f: - json.dump(self.json, f, indent=2) - -fp = FrameProcessor("D:/Frames/20190114_1629.json") -fp.process() - - -#C:\Users\bdavi\git\hifi\interface\resources\meshes \ No newline at end of file diff --git a/tools/normalizeFrame.py b/tools/normalizeFrame.py index 36b2038d5c..892609bbf0 100644 --- a/tools/normalizeFrame.py +++ b/tools/normalizeFrame.py @@ -36,7 +36,10 @@ class FrameProcessor: if not 'ktxFile' in texture: return sourceKtx = texture['ktxFile'] if sourceKtx.startswith(':'): - sourceKtx = os.path.join(self.resDir, sourceKtx[3:]) + sourceKtx = sourceKtx[1:] + while sourceKtx.startswith('/'): + sourceKtx = sourceKtx[1:] + sourceKtx = os.path.join(self.resDir, sourceKtx) sourceKtxDir, sourceKtxName = os.path.split(sourceKtx) destKtx = os.path.join(self.ktxDir, sourceKtxName) if not os.path.isfile(destKtx): @@ -52,7 +55,7 @@ class FrameProcessor: with open(self.filename, 'w') as f: json.dump(self.json, f, indent=2) -fp = FrameProcessor("D:/Frames/20190110_1635.json") +fp = FrameProcessor("D:/Frames/20190112_1647.json") fp.process() diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 022c9769fe..c9b1aca1d4 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(networking shared image gpu ktx fbx hfm baking graphics) +link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking model-baker task) setup_memory_debugger() diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index ff672d13bf..2946db650c 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -20,9 +20,10 @@ #include "OvenCLIApplication.h" #include "ModelBakingLoggingCategory.h" -#include "FBXBaker.h" +#include "baking/BakerLibrary.h" #include "JSBaker.h" #include "TextureBaker.h" +#include "MaterialBaker.h" BakerCLI::BakerCLI(OvenCLIApplication* parent) : QObject(parent) { @@ -37,59 +38,68 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& qDebug() << "Baking file type: " << type; - static const QString MODEL_EXTENSION { "fbx" }; + static const QString MODEL_EXTENSION { "model" }; + static const QString FBX_EXTENSION { "fbx" }; // legacy + static const QString MATERIAL_EXTENSION { "material" }; static const QString SCRIPT_EXTENSION { "js" }; - // check what kind of baker we should be creating - bool isFBX = type == MODEL_EXTENSION; - bool isScript = type == SCRIPT_EXTENSION; - - // If the type doesn't match the above, we assume we have a texture, and the type specified is the - // texture usage type (albedo, cubemap, normals, etc.) - auto url = inputUrl.toDisplayString(); - auto idx = url.lastIndexOf('.'); - auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; - bool isSupportedImage = QImageReader::supportedImageFormats().contains(extension.toLatin1()); - _outputPath = outputPath; // create our appropiate baker - if (isFBX) { - _baker = std::unique_ptr { - new FBXBaker(inputUrl, - []() -> QThread* { return Oven::instance().getNextWorkerThread(); }, - outputPath) - }; - _baker->moveToThread(Oven::instance().getNextWorkerThread()); - } else if (isScript) { + if (type == MODEL_EXTENSION || type == FBX_EXTENSION) { + QUrl bakeableModelURL = getBakeableModelURL(inputUrl); + if (!bakeableModelURL.isEmpty()) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + _baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputPath); + if (_baker) { + _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } + } + } else if (type == SCRIPT_EXTENSION) { _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); - } else if (isSupportedImage) { - static const std::unordered_map STRING_TO_TEXTURE_USAGE_TYPE_MAP { - { "default", image::TextureUsage::DEFAULT_TEXTURE }, - { "strict", image::TextureUsage::STRICT_TEXTURE }, - { "albedo", image::TextureUsage::ALBEDO_TEXTURE }, - { "normal", image::TextureUsage::NORMAL_TEXTURE }, - { "bump", image::TextureUsage::BUMP_TEXTURE }, - { "specular", image::TextureUsage::SPECULAR_TEXTURE }, - { "metallic", image::TextureUsage::METALLIC_TEXTURE }, - { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, - { "gloss", image::TextureUsage::GLOSS_TEXTURE }, - { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, - { "cube", image::TextureUsage::CUBE_TEXTURE }, - { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, - { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, - { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, - }; - - auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type); - if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) { - qCDebug(model_baking) << "Unknown texture usage type:" << type; - QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); - } - _baker = std::unique_ptr { new TextureBaker(inputUrl, it->second, outputPath) }; + } else if (type == MATERIAL_EXTENSION) { + _baker = std::unique_ptr { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath, QUrl(outputPath)) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { + // If the type doesn't match the above, we assume we have a texture, and the type specified is the + // texture usage type (albedo, cubemap, normals, etc.) + auto url = inputUrl.toDisplayString(); + auto idx = url.lastIndexOf('.'); + auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + static const std::unordered_map STRING_TO_TEXTURE_USAGE_TYPE_MAP { + { "default", image::TextureUsage::DEFAULT_TEXTURE }, + { "strict", image::TextureUsage::STRICT_TEXTURE }, + { "albedo", image::TextureUsage::ALBEDO_TEXTURE }, + { "normal", image::TextureUsage::NORMAL_TEXTURE }, + { "bump", image::TextureUsage::BUMP_TEXTURE }, + { "specular", image::TextureUsage::SPECULAR_TEXTURE }, + { "metallic", image::TextureUsage::METALLIC_TEXTURE }, + { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, + { "gloss", image::TextureUsage::GLOSS_TEXTURE }, + { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, + { "cube", image::TextureUsage::CUBE_TEXTURE }, + { "skybox", image::TextureUsage::CUBE_TEXTURE }, + { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, + { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, + { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, + }; + + auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type); + if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) { + qCDebug(model_baking) << "Unknown texture usage type:" << type; + QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); + } + _baker = std::unique_ptr { new TextureBaker(inputUrl, it->second, outputPath) }; + _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } + } + + if (!_baker) { qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl; QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); return; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 0a75c72f9a..05745aad24 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -20,8 +20,7 @@ #include "Gzip.h" #include "Oven.h" -#include "FBXBaker.h" -#include "OBJBaker.h" +#include "baking/BakerLibrary.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, const QString& baseOutputPath, const QUrl& destinationPath, @@ -146,11 +145,192 @@ void DomainBaker::loadLocalFile() { } } -const QString ENTITY_MODEL_URL_KEY = "modelURL"; -const QString ENTITY_SKYBOX_KEY = "skybox"; -const QString ENTITY_SKYBOX_URL_KEY = "url"; -const QString ENTITY_KEYLIGHT_KEY = "keyLight"; -const QString ENTITY_KEYLIGHT_AMBIENT_URL_KEY = "ambientURL"; +void DomainBaker::addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) { + // grab a QUrl for the model URL + QUrl bakeableModelURL = getBakeableModelURL(url); + if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) { + // setup a ModelBaker for this URL, as long as we don't already have one + bool haveBaker = _modelBakers.contains(bakeableModelURL); + if (!haveBaker) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &Baker::deleteLater); + if (baker) { + // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + // Note: The ModelBaker currently doesn't store this in the FST because the equal signs mess up FST parsing. + // There is a small chance this could break a server workflow relying on the old behavior. + // Url suffix is still propagated to the baked URL if the input URL is an FST. + // Url suffix has always been stripped from the URL when loading the original model file to be baked. + baker->setOutputURLSuffix(url); + + // make sure our handler is called when the baker is done + connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _modelBakers.insert(bakeableModelURL, baker); + haveBaker = true; + + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(baker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + } + + if (haveBaker) { + // add this QJsonValueRef to our multi hash so that we can easily re-write + // the model URL to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(bakeableModelURL, { property, jsonRef }); + } + } +} + +void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef) { + QString cleanURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + auto idx = cleanURL.lastIndexOf('.'); + auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + // grab a clean version of the URL without a query or fragment + QUrl textureURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a texture baker for this URL, as long as we aren't baking a texture already + if (!_textureBakers.contains(textureURL)) { + + // setup a baker for this texture + QSharedPointer textureBaker { + new TextureBaker(textureURL, type, _contentOutputPath), + &TextureBaker::deleteLater + }; + + // make sure our handler is called when the texture baker is done + connect(textureBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedTextureBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _textureBakers.insert(textureURL, textureBaker); + + // move the baker to a worker thread and kickoff the bake + textureBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(textureBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(textureURL, { property, jsonRef }); + } else { + qDebug() << "Texture extension not supported: " << extension; + } +} + +void DomainBaker::addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) { + // grab a clean version of the URL without a query or fragment + QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a script baker for this URL, as long as we aren't baking a script already + if (!_scriptBakers.contains(scriptURL)) { + + // setup a baker for this script + QSharedPointer scriptBaker { + new JSBaker(scriptURL, _contentOutputPath), + &JSBaker::deleteLater + }; + + // make sure our handler is called when the script baker is done + connect(scriptBaker.data(), &JSBaker::finished, this, &DomainBaker::handleFinishedScriptBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _scriptBakers.insert(scriptURL, scriptBaker); + + // move the baker to a worker thread and kickoff the bake + scriptBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(scriptBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the script URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef }); +} + +void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef) { + // grab a clean version of the URL without a query or fragment + QString materialData; + if (isURL) { + materialData = QUrl(data).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + } else { + materialData = data; + } + + // setup a material baker for this URL, as long as we aren't baking a material already + if (!_materialBakers.contains(materialData)) { + + // setup a baker for this material + QSharedPointer materialBaker { + new MaterialBaker(data, isURL, _contentOutputPath, _destinationPath), + &MaterialBaker::deleteLater + }; + + // make sure our handler is called when the material baker is done + connect(materialBaker.data(), &MaterialBaker::finished, this, &DomainBaker::handleFinishedMaterialBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _materialBakers.insert(materialData, materialBaker); + + // move the baker to a worker thread and kickoff the bake + materialBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(materialBaker.data(), "bake"); + + // keep track of the total number of baking entities + ++_totalNumberOfSubBakes; + } + + // add this QJsonValueRef to our multi hash so that it can re-write the material URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(materialData, { property, jsonRef }); +} + +// All the Entity Properties that can be baked +// *************************************************************************************** + +const QString TYPE_KEY = "type"; + +// Models +const QString MODEL_URL_KEY = "modelURL"; +const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; +const QString GRAB_KEY = "grab"; +const QString EQUIPPABLE_INDICATOR_URL_KEY = "equippableIndicatorURL"; +const QString ANIMATION_KEY = "animation"; +const QString ANIMATION_URL_KEY = "url"; + +// Textures +const QString TEXTURES_KEY = "textures"; +const QString IMAGE_URL_KEY = "imageURL"; +const QString X_TEXTURE_URL_KEY = "xTextureURL"; +const QString Y_TEXTURE_URL_KEY = "yTextureURL"; +const QString Z_TEXTURE_URL_KEY = "zTextureURL"; +const QString AMBIENT_LIGHT_KEY = "ambientLight"; +const QString AMBIENT_URL_KEY = "ambientURL"; +const QString SKYBOX_KEY = "skybox"; +const QString SKYBOX_URL_KEY = "url"; + +// Scripts +const QString SCRIPT_KEY = "script"; +const QString SERVER_SCRIPTS_KEY = "serverScripts"; + +// Materials +const QString MATERIAL_URL_KEY = "materialURL"; +const QString MATERIAL_DATA_KEY = "materialData"; + +// *************************************************************************************** void DomainBaker::enumerateEntities() { qDebug() << "Enumerating" << _entities.size() << "entities from domain"; @@ -160,109 +340,80 @@ void DomainBaker::enumerateEntities() { if (it->isObject()) { auto entity = it->toObject(); - // check if this is an entity with a model URL or is a skybox texture - if (entity.contains(ENTITY_MODEL_URL_KEY)) { - // grab a QUrl for the model URL - QUrl modelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; - - // check if the file pointed to by this URL is a bakeable model, by comparing extensions - auto modelFileName = modelURL.fileName(); - - static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; - static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; - static const QString BAKED_MODEL_EXTENSION = ".baked.fbx"; - - bool isBakedModel = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive); - bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); - bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); - bool isBakeable = isBakeableFBX || isBakeableOBJ; - - if (isBakeable || (_shouldRebakeOriginals && isBakedModel)) { - - if (isBakedModel) { - // grab a URL to the original, that we assume is stored a directory up, in the "original" folder - // with just the fbx extension - qDebug() << "Re-baking original for" << modelURL; - - auto originalFileName = modelFileName; - originalFileName.replace(".baked", ""); - modelURL = modelURL.resolved("../original/" + originalFileName); - - qDebug() << "Original must be present at" << modelURL; - } else { - // grab a clean version of the URL without a query or fragment - modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - } - - // setup a ModelBaker for this URL, as long as we don't already have one - if (!_modelBakers.contains(modelURL)) { - auto filename = modelURL.fileName(); - auto baseName = filename.left(filename.lastIndexOf('.')); - auto subDirName = "/" + baseName; - int i = 1; - while (QDir(_contentOutputPath + subDirName).exists()) { - subDirName = "/" + baseName + "-" + QString::number(i++); - } - - QSharedPointer baker; - if (isBakeableFBX) { - baker = { - new FBXBaker(modelURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), - &FBXBaker::deleteLater - }; - } else { - baker = { - new OBJBaker(modelURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), - &OBJBaker::deleteLater - }; - } - - // make sure our handler is called when the baker is done - connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); - - // insert it into our bakers hash so we hold a strong pointer to it - _modelBakers.insert(modelURL, baker); - - // move the baker to the baker thread - // and kickoff the bake - baker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(baker.data(), "bake"); - - // keep track of the total number of baking entities - ++_totalNumberOfSubBakes; - } - - // add this QJsonValueRef to our multi hash so that we can easily re-write - // the model URL to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(modelURL, *it); + // Models + if (entity.contains(MODEL_URL_KEY)) { + addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); + } + if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { + // TODO: Support collision model baking + // Do not combine mesh parts, otherwise the collision behavior will be different + // combineParts is currently only used by OBJBaker (mesh-combining functionality ought to be moved to the asset engine at some point), and is also used by OBJBaker to determine if the material library should be loaded (should be separate flag) + // TODO: this could be optimized so that we don't do the full baking pass for collision shapes, + // but we have to handle the case where it's also used as a modelURL somewhere + //addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); + } + if (entity.contains(ANIMATION_KEY)) { + auto animationObject = entity[ANIMATION_KEY].toObject(); + if (animationObject.contains(ANIMATION_URL_KEY)) { + addModelBaker(ANIMATION_KEY + "." + ANIMATION_URL_KEY, animationObject[ANIMATION_URL_KEY].toString(), *it); } - } else { -// // We check now to see if we have either a texture for a skybox or a keylight, or both. -// if (entity.contains(ENTITY_SKYBOX_KEY)) { -// auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); -// if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { -// // we have a URL to a skybox, grab it -// QUrl skyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() }; -// -// // setup a bake of the skybox -// bakeSkybox(skyboxURL, *it); -// } -// } -// -// if (entity.contains(ENTITY_KEYLIGHT_KEY)) { -// auto keyLightObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); -// if (keyLightObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { -// // we have a URL to a skybox, grab it -// QUrl skyboxURL { keyLightObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY].toString() }; -// -// // setup a bake of the skybox -// bakeSkybox(skyboxURL, *it); -// } -// } + } + if (entity.contains(GRAB_KEY)) { + auto grabObject = entity[GRAB_KEY].toObject(); + if (grabObject.contains(EQUIPPABLE_INDICATOR_URL_KEY)) { + addModelBaker(GRAB_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it); + } + } + + // Textures + if (entity.contains(TEXTURES_KEY)) { + if (entity.contains(TYPE_KEY)) { + QString type = entity[TYPE_KEY].toString(); + // TODO: handle textures for model entities + if (type == "ParticleEffect" || type == "PolyLine") { + addTextureBaker(TEXTURES_KEY, entity[TEXTURES_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + } + } + if (entity.contains(IMAGE_URL_KEY)) { + addTextureBaker(IMAGE_URL_KEY, entity[IMAGE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(X_TEXTURE_URL_KEY)) { + addTextureBaker(X_TEXTURE_URL_KEY, entity[X_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(Y_TEXTURE_URL_KEY)) { + addTextureBaker(Y_TEXTURE_URL_KEY, entity[Y_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(Z_TEXTURE_URL_KEY)) { + addTextureBaker(Z_TEXTURE_URL_KEY, entity[Z_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(AMBIENT_LIGHT_KEY)) { + auto ambientLight = entity[AMBIENT_LIGHT_KEY].toObject(); + if (ambientLight.contains(AMBIENT_URL_KEY)) { + addTextureBaker(AMBIENT_LIGHT_KEY + "." + AMBIENT_URL_KEY, ambientLight[AMBIENT_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + } + } + if (entity.contains(SKYBOX_KEY)) { + auto skybox = entity[SKYBOX_KEY].toObject(); + if (skybox.contains(SKYBOX_URL_KEY)) { + addTextureBaker(SKYBOX_KEY + "." + SKYBOX_URL_KEY, skybox[SKYBOX_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + } + } + + // Scripts + if (entity.contains(SCRIPT_KEY)) { + addScriptBaker(SCRIPT_KEY, entity[SCRIPT_KEY].toString(), *it); + } + if (entity.contains(SERVER_SCRIPTS_KEY)) { + // TODO: serverScripts can be multiple scripts, need to handle that + } + + // Materials + if (entity.contains(MATERIAL_URL_KEY)) { + addMaterialBaker(MATERIAL_URL_KEY, entity[MATERIAL_URL_KEY].toString(), true, *it); + } + if (entity.contains(MATERIAL_DATA_KEY)) { + addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_DATA_KEY].toString(), false, *it); } } } @@ -271,112 +422,56 @@ void DomainBaker::enumerateEntities() { emit bakeProgress(0, _totalNumberOfSubBakes); } -void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { - - auto skyboxFileName = skyboxURL.fileName(); - - static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { - ".jpg", ".png", ".gif", ".bmp", ".pbm", ".pgm", ".ppm", ".xbm", ".xpm", ".svg" - }; - auto completeLowerExtension = skyboxFileName.mid(skyboxFileName.indexOf('.')).toLower(); - - if (BAKEABLE_SKYBOX_EXTENSIONS.contains(completeLowerExtension)) { - // grab a clean version of the URL without a query or fragment - skyboxURL = skyboxURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - - // setup a texture baker for this URL, as long as we aren't baking a skybox already - if (!_skyboxBakers.contains(skyboxURL)) { - // setup a baker for this skybox - - QSharedPointer skyboxBaker { - new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath), - &TextureBaker::deleteLater - }; - - // make sure our handler is called when the skybox baker is done - connect(skyboxBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedSkyboxBaker); - - // insert it into our bakers hash so we hold a strong pointer to it - _skyboxBakers.insert(skyboxURL, skyboxBaker); - - // move the baker to a worker thread and kickoff the bake - skyboxBaker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(skyboxBaker.data(), "bake"); - - // keep track of the total number of baking entities - ++_totalNumberOfSubBakes; - } - - // add this QJsonValueRef to our multi hash so that it can re-write the skybox URL - // to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(skyboxURL, entity); - } -} - void DomainBaker::handleFinishedModelBaker() { auto baker = qobject_cast(sender()); if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this ModelBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getModelURL(); - // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // setup a new URL using the prefix we were passed + auto relativeMappingFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getFullOutputMappingURL().toString()); + if (relativeMappingFilePath.startsWith("/")) { + relativeMappingFilePath = relativeMappingFilePath.right(relativeMappingFilePath.length() - 1); + } + + QUrl newURL = _destinationPath.resolved(relativeMappingFilePath); + + // enumerate the QJsonRef values for the URL of this model from our multi hash of // entity objects needing a URL re-write - for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getModelURL())) { - + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getModelURL())) { + QString property = propertyEntityPair.first; // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL - auto entity = entityValue.toObject(); + auto entity = propertyEntityPair.second.toObject(); - // grab the old URL - QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); - // setup a new URL using the prefix we were passed - auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); - if (relativeFBXFilePath.startsWith("/")) { - relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + // set the new URL as the value in our temp QJsonObject + // The fragment, query, and user info from the original model URL should now be present on the filename in the FST file + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; } - QUrl newModelURL = _destinationPath.resolved(relativeFBXFilePath); - // copy the fragment and query, and user info from the old model URL - newModelURL.setQuery(oldModelURL.query()); - newModelURL.setFragment(oldModelURL.fragment()); - newModelURL.setUserInfo(oldModelURL.userInfo()); - - // set the new model URL as the value in our temp QJsonObject - entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString(); - - // check if the entity also had an animation at the same URL - // in which case it should be replaced with our baked model URL too - const QString ENTITY_ANIMATION_KEY = "animation"; - const QString ENTITIY_ANIMATION_URL_KEY = "url"; - - if (entity.contains(ENTITY_ANIMATION_KEY)) { - auto animationObject = entity[ENTITY_ANIMATION_KEY].toObject(); - - if (animationObject.contains(ENTITIY_ANIMATION_URL_KEY)) { - // grab the old animation URL - QUrl oldAnimationURL { animationObject[ENTITIY_ANIMATION_URL_KEY].toString() }; - - // check if its stripped down version matches our stripped down model URL - if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveQuery | QUrl::RemoveFragment)) { - // the animation URL matched the old model URL, so make the animation URL point to the baked FBX - // with its original query and fragment - auto newAnimationURL = _destinationPath.resolved(relativeFBXFilePath); - newAnimationURL.setQuery(oldAnimationURL.query()); - newAnimationURL.setFragment(oldAnimationURL.fragment()); - newAnimationURL.setUserInfo(oldAnimationURL.userInfo()); - - animationObject[ENTITIY_ANIMATION_URL_KEY] = newAnimationURL.toString(); - - // replace the animation object in the entity object - entity[ENTITY_ANIMATION_KEY] = animationObject; - } - } - } - // replace our temp object with the value referenced by our QJsonValueRef - entityValue = entity; + propertyEntityPair.second = entity; } } else { // this model failed to bake - this doesn't fail the entire bake but we need to add @@ -398,48 +493,63 @@ void DomainBaker::handleFinishedModelBaker() { } } -void DomainBaker::handleFinishedSkyboxBaker() { +void DomainBaker::handleFinishedTextureBaker() { auto baker = qobject_cast(sender()); if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this TextureBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getTextureURL(); - // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // setup a new URL using the prefix we were passed + auto relativeTextureFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getMetaTextureFileName()); + if (relativeTextureFilePath.startsWith("/")) { + relativeTextureFilePath = relativeTextureFilePath.right(relativeTextureFilePath.length() - 1); + } + auto newURL = _destinationPath.resolved(relativeTextureFilePath); + + // enumerate the QJsonRef values for the URL of this texture from our multi hash of // entity objects needing a URL re-write - for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + QString property = propertyEntityPair.first; // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL - auto entity = entityValue.toObject(); + auto entity = propertyEntityPair.second.toObject(); - if (entity.contains(ENTITY_SKYBOX_KEY)) { - auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); - if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) { - if (rewriteSkyboxURL(skyboxObject[ENTITY_SKYBOX_URL_KEY], baker)) { - // we re-wrote the URL, replace the skybox object referenced by the entity object - entity[ENTITY_SKYBOX_KEY] = skyboxObject; - } - } - } + // copy the fragment and query, and user info from the old texture URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); - if (entity.contains(ENTITY_KEYLIGHT_KEY)) { - auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); - if (ambientObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) { - if (rewriteSkyboxURL(ambientObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY], baker)) { - // we re-wrote the URL, replace the ambient object referenced by the entity object - entity[ENTITY_KEYLIGHT_KEY] = ambientObject; - } - } + // copy the fragment and query, and user info from the old texture URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; } // replace our temp object with the value referenced by our QJsonValueRef - entityValue = entity; + propertyEntityPair.second = entity; } } else { - // this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from - // the model to our warnings + // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the texture to our warnings _warningList << baker->getWarnings(); } @@ -447,33 +557,177 @@ void DomainBaker::handleFinishedSkyboxBaker() { _entitiesNeedingRewrite.remove(baker->getTextureURL()); // drop our shared pointer to this baker so that it gets cleaned up - _skyboxBakers.remove(baker->getTextureURL()); + _textureBakers.remove(baker->getTextureURL()); - // emit progress to tell listeners how many models we have baked - emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + // emit progress to tell listeners how many textures we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); - // check if this was the last model we needed to re-write and if we are done now - checkIfRewritingComplete(); + // check if this was the last texture we needed to re-write and if we are done now + checkIfRewritingComplete(); } } -bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) { - // grab the old skybox URL - QUrl oldSkyboxURL { urlValue.toString() }; +void DomainBaker::handleFinishedScriptBaker() { + auto baker = qobject_cast(sender()); - if (oldSkyboxURL.matches(baker->getTextureURL(), QUrl::RemoveQuery | QUrl::RemoveFragment)) { - // change the URL to point to the baked texture with its original query and fragment + if (baker) { + if (!baker->hasErrors()) { + // this JSBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getJSPath(); - auto newSkyboxURL = _destinationPath.resolved(baker->getMetaTextureFileName()); - newSkyboxURL.setQuery(oldSkyboxURL.query()); - newSkyboxURL.setFragment(oldSkyboxURL.fragment()); - newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); + // setup a new URL using the prefix we were passed + auto relativeScriptFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedJSFilePath()); + if (relativeScriptFilePath.startsWith("/")) { + relativeScriptFilePath = relativeScriptFilePath.right(relativeScriptFilePath.length() - 1); + } + auto newURL = _destinationPath.resolved(relativeScriptFilePath); - urlValue = newSkyboxURL.toString(); + // enumerate the QJsonRef values for the URL of this script from our multi hash of + // entity objects needing a URL re-write + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getJSPath())) { + QString property = propertyEntityPair.first; + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = propertyEntityPair.second.toObject(); - return true; - } else { - return false; + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); + + // copy the fragment and query, and user info from the old script URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old script URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; + } + + // replace our temp object with the value referenced by our QJsonValueRef + propertyEntityPair.second = entity; + } + } else { + // this script failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the script to our warnings + _warningList << baker->getErrors(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getJSPath()); + + // drop our shared pointer to this baker so that it gets cleaned up + _scriptBakers.remove(baker->getJSPath()); + + // emit progress to tell listeners how many scripts we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last script we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + +void DomainBaker::handleFinishedMaterialBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this MaterialBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getMaterialData(); + + QString newDataOrURL; + if (baker->isURL()) { + // setup a new URL using the prefix we were passed + auto relativeMaterialFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedMaterialData()); + if (relativeMaterialFilePath.startsWith("/")) { + relativeMaterialFilePath = relativeMaterialFilePath.right(relativeMaterialFilePath.length() - 1); + } + newDataOrURL = _destinationPath.resolved(relativeMaterialFilePath).toDisplayString(); + } else { + newDataOrURL = baker->getBakedMaterialData(); + } + + // enumerate the QJsonRef values for the URL of this material from our multi hash of + // entity objects needing a URL re-write + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getMaterialData())) { + QString property = propertyEntityPair.first; + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = propertyEntityPair.second.toObject(); + + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); + + // copy the fragment and query, and user info from the old material data + if (baker->isURL()) { + QUrl newURL = newDataOrURL; + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + entity[property] = newDataOrURL; + } + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old material data + if (baker->isURL()) { + QUrl newURL = newDataOrURL; + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; + } else { + oldObject[propertySplit[1]] = newDataOrURL; + entity[propertySplit[0]] = oldObject; + } + } + + // replace our temp object with the value referenced by our QJsonValueRef + propertyEntityPair.second = entity; + } + } 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->getErrors(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getMaterialData()); + + // drop our shared pointer to this baker so that it gets cleaned up + _materialBakers.remove(baker->getMaterialData()); + + // emit progress to tell listeners how many materials we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last material we needed to re-write and if we are done now + checkIfRewritingComplete(); } } @@ -524,6 +778,5 @@ void DomainBaker::writeNewEntitiesFile() { return; } - qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; + qDebug() << "Exported baked entities file to" << bakedEntitiesFilePath; } - diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index e0286a51ff..81f5c345cd 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -17,9 +17,10 @@ #include #include -#include "Baker.h" -#include "FBXBaker.h" +#include "ModelBaker.h" #include "TextureBaker.h" +#include "JSBaker.h" +#include "MaterialBaker.h" class DomainBaker : public Baker { Q_OBJECT @@ -29,7 +30,7 @@ public: // That means you must pass a usable running QThread when constructing a domain baker. DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName, const QString& baseOutputPath, const QUrl& destinationPath, - bool shouldRebakeOriginals = false); + bool shouldRebakeOriginals); signals: void allModelsFinished(); @@ -38,7 +39,9 @@ signals: private slots: virtual void bake() override; void handleFinishedModelBaker(); - void handleFinishedSkyboxBaker(); + void handleFinishedTextureBaker(); + void handleFinishedScriptBaker(); + void handleFinishedMaterialBaker(); private: void setupOutputFolder(); @@ -47,9 +50,6 @@ private: void checkIfRewritingComplete(); void writeNewEntitiesFile(); - void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity); - bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker); - QUrl _localEntitiesFileURL; QString _domainName; QString _baseOutputPath; @@ -62,14 +62,21 @@ private: QJsonArray _entities; QHash> _modelBakers; - QHash> _skyboxBakers; + QHash> _textureBakers; + QHash> _scriptBakers; + QHash> _materialBakers; - QMultiHash _entitiesNeedingRewrite; + QMultiHash> _entitiesNeedingRewrite; int _totalNumberOfSubBakes { 0 }; int _completedSubBakes { 0 }; bool _shouldRebakeOriginals { false }; + + void addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef); + void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef); + void addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef); + void addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef); }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index af98376034..0a5a989cbf 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -20,6 +20,13 @@ #include #include #include +#include +#include +#include +#include +#include + +#include "MaterialBaker.h" Oven* Oven::_staticInstance { nullptr }; @@ -33,6 +40,18 @@ Oven::Oven() { DependencyManager::set(); DependencyManager::set(false); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + MaterialBaker::setNextOvenWorkerThreadOperator([] { + return Oven::instance().getNextWorkerThread(); + }); + + { + auto modelFormatRegistry = DependencyManager::set(); + modelFormatRegistry->addFormat(FBXSerializer()); + modelFormatRegistry->addFormat(OBJSerializer()); + } } Oven::~Oven() { @@ -63,6 +82,10 @@ void Oven::setupWorkerThreads(int numWorkerThreads) { } QThread* Oven::getNextWorkerThread() { + // FIXME: we assign these threads when we make the bakers, but if certain bakers finish quickly, we could end up + // in a situation where threads have finished and others have tons of work queued. Instead of assigning them at initialization, + // we should build a queue of bakers, and when threads finish, they can take the next available baker. + // Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use. // We can't use QThreadPool because we want to put QObjects with signals/slots on these threads. // So instead we setup our own list of threads, up to one less than the ideal thread count diff --git a/tools/oven/src/OvenCLIApplication.cpp b/tools/oven/src/OvenCLIApplication.cpp index c405c5f4a0..b4a011291d 100644 --- a/tools/oven/src/OvenCLIApplication.cpp +++ b/tools/oven/src/OvenCLIApplication.cpp @@ -33,7 +33,7 @@ OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) : parser.addOptions({ { CLI_INPUT_PARAMETER, "Path to file that you would like to bake.", "input" }, { CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" }, - { CLI_TYPE_PARAMETER, "Type of asset.", "type" }, + { CLI_TYPE_PARAMETER, "Type of asset. [model|material|js]", "type" }, { CLI_DISABLE_TEXTURE_COMPRESSION_PARAMETER, "Disable texture compression." } }); diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 9fa586871e..79ab733b0c 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -26,8 +26,7 @@ #include "../Oven.h" #include "../OvenGUIApplication.h" #include "OvenMainWindow.h" -#include "FBXBaker.h" -#include "OBJBaker.h" +#include "baking/BakerLibrary.h" static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory"; @@ -117,7 +116,7 @@ void ModelBakeWidget::chooseFileButtonClicked() { startDir = QDir::homePath(); } - auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj)"); + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj *.gltf *.fst)"); if (!selectedFiles.isEmpty()) { // set the contents of the model file text box to be the path to the selected file @@ -166,80 +165,47 @@ void ModelBakeWidget::bakeButtonClicked() { return; } + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + if (!outputDirectory.exists()) { + QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); + return; + } + // split the list from the model line edit to see how many models we need to bake auto fileURLStrings = _modelLineEdit->text().split(','); foreach (QString fileURLString, fileURLStrings) { // construct a URL from the path in the model file text box - QUrl modelToBakeURL(fileURLString); + QUrl modelToBakeURL = QUrl::fromUserInput(fileURLString); - // if the URL doesn't have a scheme, assume it is a local file - if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") { - qDebug() << modelToBakeURL.toString(); - qDebug() << modelToBakeURL.scheme(); - modelToBakeURL = QUrl::fromLocalFile(fileURLString); - qDebug() << "New url: " << modelToBakeURL; + QUrl bakeableModelURL = getBakeableModelURL(modelToBakeURL); + if (!bakeableModelURL.isEmpty()) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + + std::unique_ptr baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputDirectory.path()); + if (baker) { + // everything seems to be in place, kick off a bake for this model now + + // move the baker to the FBX baker thread + baker->moveToThread(Oven::instance().getNextWorkerThread()); + + // invoke the bake method on the baker thread + QMetaObject::invokeMethod(baker.get(), "bake"); + + // make sure we hear about the results of this baker when it is done + connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow(); + auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory); + + // keep a unique_ptr to this baker + // and remember the row that represents it in the results table + _bakers.emplace_back(std::move(baker), resultsRow); + } } - - auto modelName = modelToBakeURL.fileName().left(modelToBakeURL.fileName().lastIndexOf('.')); - - // make sure we have a valid output directory - QDir outputDirectory(_outputDirLineEdit->text()); - QString subFolderName = modelName + "/"; - - // output in a sub-folder with the name of the fbx, potentially suffixed by a number to make it unique - int iteration = 0; - - while (outputDirectory.exists(subFolderName)) { - subFolderName = modelName + "-" + QString::number(++iteration) + "/"; - } - - outputDirectory.mkpath(subFolderName); - - if (!outputDirectory.exists()) { - QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); - return; - } - - outputDirectory.cd(subFolderName); - - QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked"); - QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original"); - - bakedOutputDirectory.mkdir("."); - originalOutputDirectory.mkdir("."); - - std::unique_ptr baker; - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - // everything seems to be in place, kick off a bake for this model now - if (modelToBakeURL.fileName().endsWith(".fbx")) { - baker.reset(new FBXBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), - originalOutputDirectory.absolutePath())); - } else if (modelToBakeURL.fileName().endsWith(".obj")) { - baker.reset(new OBJBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), - originalOutputDirectory.absolutePath())); - } else { - qWarning() << "Unknown model type: " << modelToBakeURL.fileName(); - continue; - } - - // move the baker to the FBX baker thread - baker->moveToThread(Oven::instance().getNextWorkerThread()); - - // invoke the bake method on the baker thread - QMetaObject::invokeMethod(baker.get(), "bake"); - - // make sure we hear about the results of this baker when it is done - connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker); - - // add a pending row to the results window to show that this bake is in process - auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow(); - auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory); - - // keep a unique_ptr to this baker - // and remember the row that represents it in the results table - _bakers.emplace_back(std::move(baker), resultsRow); } } diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs similarity index 59% rename from tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs rename to tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index c25a962824..1070449080 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -6,22 +6,29 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -using UnityEngine; using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; using System; -using System.IO; using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.3"; + static readonly string AVATAR_EXPORTER_VERSION = "0.4.1"; - static readonly float HIPS_GROUND_MIN_Y = 0.01f; + static readonly float HIPS_MIN_Y_PERCENT_OF_HEIGHT = 0.03f; + static readonly float BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT = -0.15f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly string EMPTY_WARNING_TEXT = "None"; static readonly string TEXTURES_DIRECTORY = "textures"; static readonly string DEFAULT_MATERIAL_NAME = "No Name"; + static readonly string HEIGHT_REFERENCE_PREFAB = "Assets/Editor/AvatarExporter/HeightReference.prefab"; + static readonly Vector3 PREVIEW_CAMERA_PIVOT = new Vector3(0.0f, 1.755f, 0.0f); + static readonly Vector3 PREVIEW_CAMERA_DIRECTION = new Vector3(0.0f, 0.0f, -1.0f); // TODO: use regex static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { @@ -50,6 +57,8 @@ class AvatarExporter : MonoBehaviour { "2018.1.0f2", "2017.4.18f1", "2017.4.17f1", + "2017.4.16f1", + "2017.4.15f1", }; static readonly Dictionary HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary { @@ -223,7 +232,8 @@ class AvatarExporter : MonoBehaviour { HeadMapped, HeadDescendantOfChest, EyesMapped, - HipsNotOnGround, + HipsNotAtBottom, + ExtentsNotBelowGround, HipsSpineChestNotCoincident, TotalBoneCountUnderLimit, AvatarRuleEnd, @@ -239,18 +249,26 @@ class AvatarExporter : MonoBehaviour { class UserBoneInformation { public string humanName; // bone name in Humanoid if it is mapped, otherwise "" public string parentName; // parent user bone name + public BoneTreeNode boneTreeNode; // node within the user bone tree public int mappingCount; // number of times this bone is mapped in Humanoid public Vector3 position; // absolute position public Quaternion rotation; // absolute rotation - public BoneTreeNode boneTreeNode; public UserBoneInformation() { humanName = ""; parentName = ""; + boneTreeNode = new BoneTreeNode(); mappingCount = 0; position = new Vector3(); rotation = new Quaternion(); - boneTreeNode = new BoneTreeNode(); + } + public UserBoneInformation(string parent, BoneTreeNode treeNode, Vector3 pos) { + humanName = ""; + parentName = parent; + boneTreeNode = treeNode; + mappingCount = 0; + position = pos; + rotation = new Quaternion(); } public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); } @@ -258,11 +276,13 @@ class AvatarExporter : MonoBehaviour { class BoneTreeNode { public string boneName; + public string parentName; public List children = new List(); public BoneTreeNode() {} - public BoneTreeNode(string name) { + public BoneTreeNode(string name, string parent) { boneName = name; + parentName = parent; } } @@ -298,18 +318,17 @@ class AvatarExporter : MonoBehaviour { if (!string.IsNullOrEmpty(occlusionMap)) { json += "\"occlusionMap\": \"" + occlusionMap + "\", "; } - json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "] "; + json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "]"; if (!string.IsNullOrEmpty(emissiveMap)) { - json += "\", emissiveMap\": \"" + emissiveMap + "\""; + json += ", \"emissiveMap\": \"" + emissiveMap + "\""; } - json += "} }"; + json += " } }"; return json; } } static string assetPath = ""; - static string assetName = ""; - + static string assetName = ""; static ModelImporter modelImporter; static HumanDescription humanDescription; @@ -317,12 +336,22 @@ class AvatarExporter : MonoBehaviour { static Dictionary humanoidToUserBoneMappings = new Dictionary(); static BoneTreeNode userBoneTree = new BoneTreeNode(); static Dictionary failedAvatarRules = new Dictionary(); + static string warnings = ""; static Dictionary textureDependencies = new Dictionary(); static Dictionary materialMappings = new Dictionary(); static Dictionary materialDatas = new Dictionary(); - static List materialAlternateStandardShader = new List(); - static Dictionary materialUnsupportedShader = new Dictionary(); + static List alternateStandardShaderMaterials = new List(); + static List unsupportedShaderMaterials = new List(); + + static SceneSetup[] previousSceneSetup; + static Vector3 previousScenePivot = Vector3.zero; + static Quaternion previousSceneRotation = Quaternion.identity; + static float previousSceneSize = 0.0f; + static bool previousSceneOrthographic = false; + static UnityEngine.Object avatarResource; + static GameObject avatarPreviewObject; + static GameObject heightReferenceObject; [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { @@ -339,8 +368,8 @@ class AvatarExporter : MonoBehaviour { EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok"); } - static void ExportSelectedAvatar(bool updateAvatar) { - // ensure everything is saved to file before exporting + static void ExportSelectedAvatar(bool updateExistingAvatar) { + // ensure everything is saved to file before doing anything AssetDatabase.SaveAssets(); string[] guids = Selection.assetGUIDs; @@ -364,6 +393,11 @@ class AvatarExporter : MonoBehaviour { " the Rig section of it's Inspector window.", "Ok"); return; } + + avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); + humanDescription = modelImporter.humanDescription; + + string textureWarnings = SetTextureDependencies(); // if the rig is optimized we should de-optimize it during the export process bool shouldDeoptimizeGameObjects = modelImporter.optimizeGameObjects; @@ -371,28 +405,23 @@ class AvatarExporter : MonoBehaviour { modelImporter.optimizeGameObjects = false; modelImporter.SaveAndReimport(); } - - humanDescription = modelImporter.humanDescription; - string textureWarnings = SetTextureDependencies(); + SetBoneAndMaterialInformation(); + if (shouldDeoptimizeGameObjects) { + // switch back to optimized game object in case it was originally optimized + modelImporter.optimizeGameObjects = true; + modelImporter.SaveAndReimport(); + } + // check if we should be substituting a bone for a missing UpperChest mapping AdjustUpperChestMapping(); // format resulting avatar rule failure strings // consider export-blocking avatar rules to be errors and show them in an error dialog, // and also include any other avatar rule failures plus texture warnings as warnings in the dialog - if (shouldDeoptimizeGameObjects) { - // switch back to optimized game object in case it was originally optimized - modelImporter.optimizeGameObjects = true; - modelImporter.SaveAndReimport(); - } - - // format resulting bone rule failure strings - // consider export-blocking bone rules to be errors and show them in an error dialog, - // and also include any other bone rule failures plus texture warnings as warnings in the dialog string boneErrors = ""; - string warnings = ""; + warnings = ""; foreach (var failedAvatarRule in failedAvatarRules) { if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0) { boneErrors += failedAvatarRule.Value + "\n\n"; @@ -400,15 +429,16 @@ class AvatarExporter : MonoBehaviour { warnings += failedAvatarRule.Value + "\n\n"; } } - foreach (string materialName in materialAlternateStandardShader) { - warnings += "The material " + materialName + " is not using the recommended variation of the Standard shader. " + - "We recommend you change it to Standard (Roughness setup) shader for improved performance.\n\n"; - } - foreach (var material in materialUnsupportedShader) { - warnings += "The material " + material.Key + " is using an unsupported shader " + material.Value + - ". Please change it to a Standard shader type.\n\n"; - } + + // add material and texture warnings after bone-related warnings + AddMaterialWarnings(); warnings += textureWarnings; + + // remove trailing newlines at the end of the warnings + if (!string.IsNullOrEmpty(warnings)) { + warnings = warnings.Substring(0, warnings.LastIndexOf("\n\n")); + } + if (!string.IsNullOrEmpty(boneErrors)) { // if there are both errors and warnings then warnings will be displayed with errors in the error dialog if (!string.IsNullOrEmpty(warnings)) { @@ -421,150 +451,157 @@ class AvatarExporter : MonoBehaviour { return; } + // since there are no errors we can now open the preview scene in place of the user's scene + if (!OpenPreviewScene()) { + return; + } + + // show None instead of blank warnings if there are no warnings in the export windows + if (string.IsNullOrEmpty(warnings)) { + warnings = EMPTY_WARNING_TEXT; + } + string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string hifiFolder = documentsFolder + "\\High Fidelity Projects"; - if (updateAvatar) { // Update Existing Avatar menu option - bool copyModelToExport = false; + if (updateExistingAvatar) { // Update Existing Avatar menu option + // open update existing project popup window including project to update, scale, and warnings + // default the initial file chooser location to HiFi projects folder in user documents folder + ExportProjectWindow window = ScriptableObject.CreateInstance(); string initialPath = Directory.Exists(hifiFolder) ? hifiFolder : documentsFolder; - - // open file explorer defaulting to hifi projects folder in user documents to select target fst to update - string exportFstPath = EditorUtility.OpenFilePanel("Select .fst to update", initialPath, "fst"); - if (exportFstPath.Length == 0) { // file selection cancelled - return; - } - exportFstPath = exportFstPath.Replace('/', '\\'); - - // lookup the project name field from the fst file to update - string projectName = ""; - try { - string[] lines = File.ReadAllLines(exportFstPath); - foreach (string line in lines) { - int separatorIndex = line.IndexOf("="); - if (separatorIndex >= 0) { - string key = line.Substring(0, separatorIndex).Trim(); - if (key == "name") { - projectName = line.Substring(separatorIndex + 1).Trim(); - break; - } - } - } - } catch { - EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath + - ". Please check the file and try again.", "Ok"); - return; - } - - string exportModelPath = Path.GetDirectoryName(exportFstPath) + "\\" + assetName + ".fbx"; - if (File.Exists(exportModelPath)) { - // if the fbx in Unity Assets is newer than the fbx in the target export - // folder or vice-versa then ask to replace the older fbx with the newer fbx - DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); - DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); - if (assetModelWriteTime > targetModelWriteTime) { - int option = EditorUtility.DisplayDialogComplex("Error", "The " + assetName + - ".fbx model in the Unity Assets folder is newer than the " + exportModelPath + - " model.\n\nDo you want to replace the older .fbx with the newer .fbx?", - "Yes", "No", "Cancel"); - if (option == 2) { // Cancel - return; - } - copyModelToExport = option == 0; // Yes - } else if (assetModelWriteTime < targetModelWriteTime) { - int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + - " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + - "\n\nDo you want to replace the older .fbx with the newer .fbx and re-import it?", - "Yes", "No" , "Cancel"); - if (option == 2) { // Cancel - return; - } else if (option == 0) { // Yes - copy model to Unity project - // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it - try { - File.Copy(exportModelPath, assetPath, true); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath + - ". Please check the location and try again.", "Ok"); - return; - } - AssetDatabase.ImportAsset(assetPath); - - // set model to Humanoid animation type and force another refresh on it to process Humanoid - modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; - modelImporter.animationType = ModelImporterAnimationType.Human; - EditorUtility.SetDirty(modelImporter); - modelImporter.SaveAndReimport(); - - // redo parent names, joint mappings, and user bone positions due to the fbx change - // as well as re-check the avatar rules for failures - humanDescription = modelImporter.humanDescription; - SetBoneAndMaterialInformation(); - } - } - } else { - // if no matching fbx exists in the target export folder then ask to copy fbx over - int option = EditorUtility.DisplayDialogComplex("Error", "There is no existing " + exportModelPath + - " model.\n\nDo you want to copy over the " + assetName + - ".fbx model from the Unity Assets folder?", "Yes", "No", "Cancel"); - if (option == 2) { // Cancel - return; - } - copyModelToExport = option == 0; // Yes - } - - // copy asset fbx over deleting any existing fbx if we agreed to overwrite it - if (copyModelToExport) { - try { - File.Copy(assetPath, exportModelPath, true); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + assetPath + " to " + exportModelPath + - ". Please check the location and try again.", "Ok"); - return; - } - } - - // delete existing fst file since we will write a new file - // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file - try { - File.Delete(exportFstPath); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath + - ". Please check the file and try again.", "Ok"); - return; - } - - // write out a new fst file in place of the old file - if (!WriteFST(exportFstPath, projectName)) { - return; - } - - // copy any external texture files to the project's texture directory that are considered dependencies of the model - string texturesDirectory = GetTextureDirectory(exportFstPath); - if (!CopyExternalTextures(texturesDirectory)) { - return; - } - - // display success dialog with any avatar rule warnings - string successDialog = "Avatar successfully updated!"; - if (!string.IsNullOrEmpty(warnings)) { - successDialog += "\n\nWarnings:\n" + warnings; - } - EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); + window.Init(initialPath, warnings, updateExistingAvatar, avatarPreviewObject, OnUpdateExistingProject, OnExportWindowClose); } else { // Export New Avatar menu option // create High Fidelity Projects folder in user documents folder if it doesn't exist if (!Directory.Exists(hifiFolder)) { Directory.CreateDirectory(hifiFolder); } - if (string.IsNullOrEmpty(warnings)) { - warnings = EMPTY_WARNING_TEXT; - } - - // open a popup window to enter new export project name and project location + // open export new project popup window including project name, project location, scale, and warnings + // default the initial project location path to the High Fidelity Projects folder above ExportProjectWindow window = ScriptableObject.CreateInstance(); - window.Init(hifiFolder, warnings, OnExportProjectWindowClose); + window.Init(hifiFolder, warnings, updateExistingAvatar, avatarPreviewObject, OnExportNewProject, OnExportWindowClose); } } - static void OnExportProjectWindowClose(string projectDirectory, string projectName, string warnings) { + static void OnUpdateExistingProject(string exportFstPath, string projectName, float scale) { + bool copyModelToExport = false; + + // lookup the project name field from the fst file to update + projectName = ""; + try { + string[] lines = File.ReadAllLines(exportFstPath); + foreach (string line in lines) { + int separatorIndex = line.IndexOf("="); + if (separatorIndex >= 0) { + string key = line.Substring(0, separatorIndex).Trim(); + if (key == "name") { + projectName = line.Substring(separatorIndex + 1).Trim(); + break; + } + } + } + } catch { + EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + string exportModelPath = Path.GetDirectoryName(exportFstPath) + "\\" + assetName + ".fbx"; + if (File.Exists(exportModelPath)) { + // if the fbx in Unity Assets is newer than the fbx in the target export + // folder or vice-versa then ask to replace the older fbx with the newer fbx + DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); + DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); + if (assetModelWriteTime > targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + assetName + + ".fbx model in the Unity Assets folder is newer than the " + exportModelPath + + " model.\n\nDo you want to replace the older .fbx with the newer .fbx?", + "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; // Yes + } else if (assetModelWriteTime < targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + + " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + + "\n\nDo you want to replace the older .fbx with the newer .fbx and re-import it?", + "Yes", "No" , "Cancel"); + if (option == 2) { // Cancel + return; + } else if (option == 0) { // Yes - copy model to Unity project + // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it + try { + File.Copy(exportModelPath, assetPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath + + ". Please check the location and try again.", "Ok"); + return; + } + AssetDatabase.ImportAsset(assetPath); + + // set model to Humanoid animation type and force another refresh on it to process Humanoid + modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; + modelImporter.animationType = ModelImporterAnimationType.Human; + EditorUtility.SetDirty(modelImporter); + modelImporter.SaveAndReimport(); + + // redo parent names, joint mappings, and user bone positions due to the fbx change + // as well as re-check the avatar rules for failures + humanDescription = modelImporter.humanDescription; + SetBoneAndMaterialInformation(); + } + } + } else { + // if no matching fbx exists in the target export folder then ask to copy fbx over + int option = EditorUtility.DisplayDialogComplex("Error", "There is no existing " + exportModelPath + + " model.\n\nDo you want to copy over the " + assetName + + ".fbx model from the Unity Assets folder?", "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; // Yes + } + + // copy asset fbx over deleting any existing fbx if we agreed to overwrite it + if (copyModelToExport) { + try { + File.Copy(assetPath, exportModelPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + assetPath + " to " + exportModelPath + + ". Please check the location and try again.", "Ok"); + return; + } + } + + // delete existing fst file since we will write a new file + // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file + try { + File.Delete(exportFstPath); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + // write out a new fst file in place of the old file + if (!WriteFST(exportFstPath, projectName, scale)) { + return; + } + + // copy any external texture files to the project's texture directory that are considered dependencies of the model + string texturesDirectory = GetTextureDirectory(exportFstPath); + if (!CopyExternalTextures(texturesDirectory)) { + return; + } + + // display success dialog with any avatar rule warnings + string successDialog = "Avatar successfully updated!"; + if (!string.IsNullOrEmpty(warnings)) { + successDialog += "\n\nWarnings:\n" + warnings; + } + EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); + } + + static void OnExportNewProject(string projectDirectory, string projectName, float scale) { // copy the fbx from the Unity Assets folder to the project directory string exportModelPath = projectDirectory + assetName + ".fbx"; File.Copy(assetPath, exportModelPath); @@ -577,7 +614,7 @@ class AvatarExporter : MonoBehaviour { // write out the avatar.fst file to the project directory string exportFstPath = projectDirectory + "avatar.fst"; - if (!WriteFST(exportFstPath, projectName)) { + if (!WriteFST(exportFstPath, projectName, scale)) { return; } @@ -592,16 +629,27 @@ class AvatarExporter : MonoBehaviour { if (warnings != EMPTY_WARNING_TEXT) { successDialog += "Warnings:\n" + warnings; } - successDialog += "Note: If you are using any external textures with your model, " + + successDialog += "\n\nNote: If you are using any external textures with your model, " + "please ensure those textures are copied to " + texturesDirectory; EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } + + static void OnExportWindowClose() { + // close the preview avatar scene and go back to user's previous scene when export project windows close + ClosePreviewScene(); + } - static bool WriteFST(string exportFstPath, string projectName) { + // The High Fidelity FBX Serializer omits the colon based prefixes. This will make the jointnames compatible. + static string removeTypeFromJointname(string jointName) { + return jointName.Substring(jointName.IndexOf(':') + 1); + } + + static bool WriteFST(string exportFstPath, string projectName, float scale) { // write out core fields to top of fst file try { - File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + - assetName + ".fbx\n" + "texdir = textures\n"); + File.WriteAllText(exportFstPath, "exporterVersion = " + AVATAR_EXPORTER_VERSION + "\nname = " + projectName + + "\ntype = body+head\nscale = " + scale + "\nfilename = " + assetName + + ".fbx\n" + "texdir = textures\n"); } catch { EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + ". Please check the location and try again.", "Ok"); @@ -612,7 +660,7 @@ class AvatarExporter : MonoBehaviour { foreach (var userBoneInfo in userBoneInfos) { if (userBoneInfo.Value.HasHumanMapping()) { string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.Value.humanName]; - File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + userBoneInfo.Key + "\n"); + File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + removeTypeFromJointname(userBoneInfo.Key) + "\n"); } } @@ -653,7 +701,7 @@ class AvatarExporter : MonoBehaviour { // swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w); - File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + userBoneName + " = (" + jointOffset.x + ", " + + File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + removeTypeFromJointname(userBoneName) + " = (" + jointOffset.x + ", " + jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n"); } @@ -661,11 +709,10 @@ class AvatarExporter : MonoBehaviour { if (materialDatas.Count > 0) { string materialJson = "{ "; foreach (var materialData in materialDatas) { - // if this is the only material in the mapping and it is the default name No Name mapped to No Name, + // if this is the only material in the mapping and it is mapped to default material name No Name, // then the avatar has no embedded materials and this material should be applied to all meshes string materialName = materialData.Key; - if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME && - materialMappings[materialName] == DEFAULT_MATERIAL_NAME) { + if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME) { materialJson += "\"all\": "; } else { materialJson += "\"mat::" + materialName + "\": "; @@ -690,17 +737,18 @@ class AvatarExporter : MonoBehaviour { userBoneTree = new BoneTreeNode(); materialDatas.Clear(); - materialAlternateStandardShader.Clear(); - materialUnsupportedShader.Clear(); - + alternateStandardShaderMaterials.Clear(); + unsupportedShaderMaterials.Clear(); + SetMaterialMappings(); - - // instantiate a game object of the user avatar to traverse the bone tree to gather - // bone parents and positions as well as build a bone tree, then destroy it - UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); - GameObject assetGameObject = (GameObject)Instantiate(avatarResource); - TraverseUserBoneTree(assetGameObject.transform); - DestroyImmediate(assetGameObject); + + // instantiate a game object of the user avatar to traverse the bone tree to gather + // bone parents and positions as well as build a bone tree, then destroy it + GameObject avatarGameObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity); + TraverseUserBoneTree(avatarGameObject.transform, userBoneTree); + Bounds bounds = AvatarUtilities.GetAvatarBounds(avatarGameObject); + float height = AvatarUtilities.GetAvatarHeight(avatarGameObject); + DestroyImmediate(avatarGameObject); // iterate over Humanoid bones and update user bone info to increase human mapping counts for each bone // as well as set their Humanoid name and build a Humanoid to user bone mapping @@ -719,10 +767,10 @@ class AvatarExporter : MonoBehaviour { } // generate the list of avatar rule failure strings for any avatar rules that are not satisfied by this avatar - SetFailedAvatarRules(); + SetFailedAvatarRules(bounds, height); } - static void TraverseUserBoneTree(Transform modelBone) { + static void TraverseUserBoneTree(Transform modelBone, BoneTreeNode boneTreeNode) { GameObject gameObject = modelBone.gameObject; // check if this transform is a node containing mesh, light, or camera instead of a bone @@ -732,37 +780,56 @@ class AvatarExporter : MonoBehaviour { bool light = gameObject.GetComponent() != null; bool camera = gameObject.GetComponent() != null; - // if this is a mesh and the model is using external materials then store its material data to be exported - if (mesh && modelImporter.materialLocation == ModelImporterMaterialLocation.External) { + // if this is a mesh then store its material data to be exported if the material is mapped to an fbx material name + if (mesh) { Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials; StoreMaterialData(materials); + + // ensure branches within the transform hierarchy that contain meshes are removed from the user bone tree + Transform ancestorBone = modelBone; + string previousBoneName = ""; + // find the name of the root child bone that this mesh is underneath + while (ancestorBone != null) { + if (ancestorBone.parent == null) { + break; + } + previousBoneName = ancestorBone.name; + ancestorBone = ancestorBone.parent; + } + // remove the bone tree node from root's children for the root child bone that has mesh children + if (!string.IsNullOrEmpty(previousBoneName)) { + foreach (BoneTreeNode rootChild in userBoneTree.children) { + if (rootChild.boneName == previousBoneName) { + userBoneTree.children.Remove(rootChild); + break; + } + } + } } else if (!light && !camera) { // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name - UserBoneInformation userBoneInfo = new UserBoneInformation(); - userBoneInfo.position = modelBone.position; // bone's absolute position - string boneName = modelBone.name; if (modelBone.parent == null) { // if no parent then this is actual root bone node of the user avatar, so consider it's parent as "root" boneName = GetRootBoneName(); // ensure we use the root bone name from the skeleton list for consistency - userBoneTree = new BoneTreeNode(boneName); // initialize root of tree - userBoneInfo.parentName = "root"; - userBoneInfo.boneTreeNode = userBoneTree; + boneTreeNode.boneName = boneName; + boneTreeNode.parentName = "root"; } else { // otherwise add this bone node as a child to it's parent's children list // if its a child of the root bone, use the root bone name from the skeleton list as the parent for consistency string parentName = modelBone.parent.parent == null ? GetRootBoneName() : modelBone.parent.name; - BoneTreeNode boneTreeNode = new BoneTreeNode(boneName); - userBoneInfos[parentName].boneTreeNode.children.Add(boneTreeNode); - userBoneInfo.parentName = parentName; + BoneTreeNode node = new BoneTreeNode(boneName, parentName); + boneTreeNode.children.Add(node); + boneTreeNode = node; } + Vector3 bonePosition = modelBone.position; // bone's absolute position in avatar space + UserBoneInformation userBoneInfo = new UserBoneInformation(boneTreeNode.parentName, boneTreeNode, bonePosition); userBoneInfos.Add(boneName, userBoneInfo); } // recurse over transform node's children for (int i = 0; i < modelBone.childCount; ++i) { - TraverseUserBoneTree(modelBone.GetChild(i)); + TraverseUserBoneTree(modelBone.GetChild(i), boneTreeNode); } } @@ -806,7 +873,7 @@ class AvatarExporter : MonoBehaviour { return ""; } - static void SetFailedAvatarRules() { + static void SetFailedAvatarRules(Bounds avatarBounds, float avatarHeight) { failedAvatarRules.Clear(); string hipsUserBone = ""; @@ -871,18 +938,29 @@ class AvatarExporter : MonoBehaviour { break; case AvatarRule.ChestMapped: if (!humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { - // check to see if there is a child of Spine that we can suggest to be mapped to Chest - string spineChild = ""; + // check to see if there is an unmapped child of Spine that we can suggest to be mapped to Chest + string chestMappingCandidate = ""; if (!string.IsNullOrEmpty(spineUserBone)) { BoneTreeNode spineTreeNode = userBoneInfos[spineUserBone].boneTreeNode; - if (spineTreeNode.children.Count == 1) { - spineChild = spineTreeNode.children[0].boneName; + foreach (BoneTreeNode spineChildTreeNode in spineTreeNode.children) { + string spineChildBone = spineChildTreeNode.boneName; + if (userBoneInfos[spineChildBone].HasHumanMapping()) { + continue; + } + // a suitable candidate for Chest should have Neck/Head or Shoulder mappings in its descendants + if (IsHumanBoneInHierarchy(spineChildTreeNode, "Neck") || + IsHumanBoneInHierarchy(spineChildTreeNode, "Head") || + IsHumanBoneInHierarchy(spineChildTreeNode, "LeftShoulder") || + IsHumanBoneInHierarchy(spineChildTreeNode, "RightShoulder")) { + chestMappingCandidate = spineChildBone; + break; + } } } failedAvatarRules.Add(avatarRule, "There is no Chest bone mapped in Humanoid for the selected avatar."); // if the only found child of Spine is not yet mapped then add it as a suggestion for Chest mapping - if (!string.IsNullOrEmpty(spineChild) && !userBoneInfos[spineChild].HasHumanMapping()) { - failedAvatarRules[avatarRule] += " It is suggested that you map bone " + spineChild + + if (!string.IsNullOrEmpty(chestMappingCandidate)) { + failedAvatarRules[avatarRule] += " It is suggested that you map bone " + chestMappingCandidate + " to Chest in Humanoid."; } } @@ -915,15 +993,34 @@ class AvatarExporter : MonoBehaviour { } } break; - case AvatarRule.HipsNotOnGround: - // ensure the absolute Y position for the bone mapped to Hips (if its mapped) is at least HIPS_GROUND_MIN_Y + case AvatarRule.HipsNotAtBottom: + // ensure that Hips is not below a proportional percentage of the avatar's height in avatar space if (!string.IsNullOrEmpty(hipsUserBone)) { UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone]; hipsPosition = hipsBoneInfo.position; - if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + - ") should not be at ground level."); + + // find the lowest y position of the bones + float minBoneYPosition = float.MaxValue; + foreach (var userBoneInfo in userBoneInfos) { + Vector3 position = userBoneInfo.Value.position; + if (position.y < minBoneYPosition) { + minBoneYPosition = position.y; + } } + + // check that Hips is within a percentage of avatar's height from the lowest Y point of the avatar + float bottomYRange = HIPS_MIN_Y_PERCENT_OF_HEIGHT * avatarHeight; + if (Mathf.Abs(hipsPosition.y - minBoneYPosition) < bottomYRange) { + failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + + ") should not be at the bottom of the selected avatar."); + } + } + break; + case AvatarRule.ExtentsNotBelowGround: + // ensure the minimum Y extent of the model's bounds is not below a proportional threshold of avatar's height + float belowGroundThreshold = BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT * avatarHeight; + if (avatarBounds.min.y < belowGroundThreshold) { + failedAvatarRules.Add(avatarRule, "The bottom extents of the selected avatar go below ground level."); } break; case AvatarRule.HipsSpineChestNotCoincident: @@ -955,17 +1052,35 @@ class AvatarExporter : MonoBehaviour { } } + static bool IsHumanBoneInHierarchy(BoneTreeNode boneTreeNode, string humanBoneName) { + UserBoneInformation userBoneInfo; + if (userBoneInfos.TryGetValue(boneTreeNode.boneName, out userBoneInfo) && userBoneInfo.humanName == humanBoneName) { + // this bone matches the human bone name being searched for + return true; + } + + // recursively check downward through children bones for target human bone + foreach (BoneTreeNode childNode in boneTreeNode.children) { + if (IsHumanBoneInHierarchy(childNode, humanBoneName)) { + return true; + } + } + + return false; + } + static string CheckHumanBoneMappingRule(AvatarRule avatarRule, string humanBoneName) { string userBoneName = ""; // avatar rule fails if bone is not mapped in Humanoid if (!humanoidToUserBoneMappings.TryGetValue(humanBoneName, out userBoneName)) { - failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + " bone mapped in Humanoid for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + + " bone mapped in Humanoid for the selected avatar."); } return userBoneName; } - static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string userBoneName, string descendantOfHumanName) { - if (string.IsNullOrEmpty(userBoneName)) { + static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string descendantUserBoneName, string descendantOfHumanName) { + if (string.IsNullOrEmpty(descendantUserBoneName)) { return; } @@ -974,27 +1089,26 @@ class AvatarExporter : MonoBehaviour { return; } - string userBone = userBoneName; - string ancestorUserBone = ""; - UserBoneInformation userBoneInfo = new UserBoneInformation(); + string userBoneName = descendantUserBoneName; + UserBoneInformation userBoneInfo = userBoneInfos[userBoneName]; + string descendantHumanName = userBoneInfo.humanName; // iterate upward from user bone through user bone info parent names until root // is reached or the ancestor bone name matches the target descendant of name - while (ancestorUserBone != "root") { - if (userBoneInfos.TryGetValue(userBone, out userBoneInfo)) { - ancestorUserBone = userBoneInfo.parentName; - if (ancestorUserBone == descendantOfUserBoneName) { - return; - } - userBone = ancestorUserBone; + while (userBoneName != "root") { + if (userBoneName == descendantOfUserBoneName) { + return; + } + if (userBoneInfos.TryGetValue(userBoneName, out userBoneInfo)) { + userBoneName = userBoneInfo.parentName; } else { break; } } // avatar rule fails if no ancestor of given user bone matched the descendant of name (no early return) - failedAvatarRules.Add(avatarRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + - ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + - descendantOfUserBoneName + ")."); + failedAvatarRules.Add(avatarRule, "The bone mapped to " + descendantHumanName + " in Humanoid (" + + descendantUserBoneName + ") is not a descendant of the bone mapped to " + + descendantOfHumanName + " in Humanoid (" + descendantOfUserBoneName + ")."); } static void CheckAsymmetricalMappingRule(AvatarRule avatarRule, string[] mappingSuffixes, string appendage) { @@ -1070,13 +1184,18 @@ class AvatarExporter : MonoBehaviour { string materialName = material.name; string shaderName = material.shader.name; - // don't store any material data for unsupported shader types - if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) { - if (!materialUnsupportedShader.ContainsKey(materialName)) { - materialUnsupportedShader.Add(materialName, shaderName); - } + // if this material isn't mapped externally then ignore it + if (!materialMappings.ContainsValue(materialName)) { continue; } + + // don't store any material data for unsupported shader types + if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) { + if (!unsupportedShaderMaterials.Contains(materialName)) { + unsupportedShaderMaterials.Add(materialName); + } + continue; + } MaterialData materialData = new MaterialData(); materialData.albedo = material.GetColor("_Color"); @@ -1100,26 +1219,26 @@ class AvatarExporter : MonoBehaviour { // for non-roughness Standard shaders give a warning that is not the recommended Standard shader, // and invert smoothness for roughness if (shaderName == STANDARD_SHADER || shaderName == STANDARD_SPECULAR_SHADER) { - if (!materialAlternateStandardShader.Contains(materialName)) { - materialAlternateStandardShader.Add(materialName); + if (!alternateStandardShaderMaterials.Contains(materialName)) { + alternateStandardShaderMaterials.Add(materialName); } materialData.roughness = 1.0f - materialData.roughness; } - - // remap the material name from the Unity material name to the fbx material name that it overrides - if (materialMappings.ContainsKey(materialName)) { - materialName = materialMappings[materialName]; - } - if (!materialDatas.ContainsKey(materialName)) { - materialDatas.Add(materialName, materialData); + + // store the material data under each fbx material name that it overrides from the material mapping + foreach (var materialMapping in materialMappings) { + string fbxMaterialName = materialMapping.Key; + string unityMaterialName = materialMapping.Value; + if (unityMaterialName == materialName && !materialDatas.ContainsKey(fbxMaterialName)) { + materialDatas.Add(fbxMaterialName, materialData); + } } } } static string GetMaterialTexture(Material material, string textureProperty) { // ensure the texture property name exists in this material and return its texture directory path if so - string[] textureNames = material.GetTexturePropertyNames(); - if (Array.IndexOf(textureNames, textureProperty) >= 0) { + if (material.HasProperty(textureProperty)) { Texture texture = material.GetTexture(textureProperty); if (texture) { foreach (var textureDependency in textureDependencies) { @@ -1136,20 +1255,113 @@ class AvatarExporter : MonoBehaviour { static void SetMaterialMappings() { materialMappings.Clear(); - // store the mappings from fbx material name to the Unity material name overriding it using external fbx mapping + // store the mappings from fbx material name to the Unity Material name that overrides it using external fbx mapping var objectMap = modelImporter.GetExternalObjectMap(); foreach (var mapping in objectMap) { var material = mapping.Value as UnityEngine.Material; if (material != null) { - materialMappings.Add(material.name, mapping.Key.name); + materialMappings.Add(mapping.Key.name, material.name); } } } + + static void AddMaterialWarnings() { + string alternateStandardShaders = ""; + string unsupportedShaders = ""; + // combine all material names for each material warning into a comma-separated string + foreach (string materialName in alternateStandardShaderMaterials) { + if (!string.IsNullOrEmpty(alternateStandardShaders)) { + alternateStandardShaders += ", "; + } + alternateStandardShaders += materialName; + } + foreach (string materialName in unsupportedShaderMaterials) { + if (!string.IsNullOrEmpty(unsupportedShaders)) { + unsupportedShaders += ", "; + } + unsupportedShaders += materialName; + } + if (alternateStandardShaderMaterials.Count > 1) { + warnings += "The materials " + alternateStandardShaders + " are not using the " + + "recommended variation of the Standard shader. We recommend you change " + + "them to Standard (Roughness setup) shader for improved performance.\n\n"; + } else if (alternateStandardShaderMaterials.Count == 1) { + warnings += "The material " + alternateStandardShaders + " is not using the " + + "recommended variation of the Standard shader. We recommend you change " + + "it to Standard (Roughness setup) shader for improved performance.\n\n"; + } + if (unsupportedShaderMaterials.Count > 1) { + warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " + + "We recommend you change them to a Standard shader type.\n\n"; + } else if (unsupportedShaderMaterials.Count == 1) { + warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " + + "We recommend you change it to a Standard shader type.\n\n"; + } + } + + static bool OpenPreviewScene() { + // store the current scene setup to restore when closing the preview scene + previousSceneSetup = EditorSceneManager.GetSceneManagerSetup(); + + // if the user is currently in the Humanoid Avatar Configuration then inform them to close it first + if (EditorSceneManager.GetActiveScene().name == "Avatar Configuration" && previousSceneSetup.Length == 0) { + EditorUtility.DisplayDialog("Error", "Please exit the Avatar Configuration before exporting.", "Ok"); + return false; + } + + // see if the user wants to save their current scene before opening preview avatar scene in place of user's scene + if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { + return false; + } + + // open a new empty scene in place of the user's scene + EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); + + // instantiate a game object to preview the avatar and a game object for the height reference prefab at 0, 0, 0 + UnityEngine.Object heightReferenceResource = AssetDatabase.LoadAssetAtPath(HEIGHT_REFERENCE_PREFAB, typeof(UnityEngine.Object)); + avatarPreviewObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity); + heightReferenceObject = (GameObject)Instantiate(heightReferenceResource, Vector3.zero, Quaternion.identity); + + // store the camera pivot and rotation from the user's last scene to be restored later + // replace the camera pivot and rotation to point at the preview avatar object in the -Z direction (facing front of it) + var sceneView = SceneView.lastActiveSceneView; + if (sceneView != null) { + previousScenePivot = sceneView.pivot; + previousSceneRotation = sceneView.rotation; + previousSceneSize = sceneView.size; + previousSceneOrthographic = sceneView.orthographic; + sceneView.pivot = PREVIEW_CAMERA_PIVOT; + sceneView.rotation = Quaternion.LookRotation(PREVIEW_CAMERA_DIRECTION); + sceneView.orthographic = true; + sceneView.size = 5.0f; + } + + return true; + } + + static void ClosePreviewScene() { + // destroy the avatar and height reference game objects closing the scene + DestroyImmediate(avatarPreviewObject); + DestroyImmediate(heightReferenceObject); + + // restore to the previous scene setup that the user had open before exporting + EditorSceneManager.RestoreSceneManagerSetup(previousSceneSetup); + + // restore the camera pivot and rotation to the user's previous scene settings + var sceneView = SceneView.lastActiveSceneView; + if (sceneView != null) { + sceneView.pivot = previousScenePivot; + sceneView.rotation = previousSceneRotation; + sceneView.size = previousSceneSize; + sceneView.orthographic = previousSceneOrthographic; + } + } } class ExportProjectWindow : EditorWindow { const int WINDOW_WIDTH = 500; - const int WINDOW_HEIGHT = 460; + const int EXPORT_NEW_WINDOW_HEIGHT = 520; + const int UPDATE_EXISTING_WINDOW_HEIGHT = 465; const int BUTTON_FONT_SIZE = 16; const int LABEL_FONT_SIZE = 16; const int TEXT_FIELD_FONT_SIZE = 14; @@ -1157,28 +1369,65 @@ class ExportProjectWindow : EditorWindow { const int ERROR_FONT_SIZE = 12; const int WARNING_SCROLL_HEIGHT = 170; const string EMPTY_ERROR_TEXT = "None\n"; - + const int SLIDER_WIDTH = 340; + const int SCALE_TEXT_WIDTH = 60; + const float MIN_SCALE_SLIDER = 0.0f; + const float MAX_SCALE_SLIDER = 2.0f; + const int SLIDER_SCALE_EXPONENT = 10; + const float ACTUAL_SCALE_OFFSET = 1.0f; + const float MAXIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.DEFAULT_AVATAR_HEIGHT * 1.5f; + const float MINIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.DEFAULT_AVATAR_HEIGHT * 0.25f; + const float SLIDER_DIFFERENCE_REMOVE_TEXT = 0.01f; + readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f); + readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f); + + GameObject avatarPreviewObject; + bool updateExistingAvatar = false; string projectName = ""; string projectLocation = ""; + string initialProjectLocation = ""; string projectDirectory = ""; string errorText = EMPTY_ERROR_TEXT; - string warningText = ""; + string warningText = "\n"; Vector2 warningScrollPosition = new Vector2(0, 0); + string scaleWarningText = ""; + float sliderScale = 0.30103f; + float originalSliderScale; - public delegate void OnCloseDelegate(string projectDirectory, string projectName, string warnings); + public delegate void OnExportDelegate(string projectDirectory, string projectName, float scale); + OnExportDelegate onExportCallback; + + public delegate void OnCloseDelegate(); OnCloseDelegate onCloseCallback; - public void Init(string initialPath, string warnings, OnCloseDelegate closeCallback) { - minSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT); - maxSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT); - titleContent.text = "Export New Avatar"; - projectLocation = initialPath; + public void Init(string initialPath, string warnings, bool updateExisting, GameObject avatarObject, + OnExportDelegate exportCallback, OnCloseDelegate closeCallback) { + updateExistingAvatar = updateExisting; + float windowHeight = updateExistingAvatar ? UPDATE_EXISTING_WINDOW_HEIGHT : EXPORT_NEW_WINDOW_HEIGHT; + minSize = new Vector2(WINDOW_WIDTH, windowHeight); + maxSize = new Vector2(WINDOW_WIDTH, windowHeight); + avatarPreviewObject = avatarObject; + titleContent.text = updateExistingAvatar ? "Update Existing Avatar" : "Export New Avatar"; + initialProjectLocation = initialPath; + projectLocation = updateExistingAvatar ? "" : initialProjectLocation; warningText = warnings; + onExportCallback = exportCallback; onCloseCallback = closeCallback; + ShowUtility(); + + // if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height + float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject); + if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) { + float newScale = AvatarUtilities.DEFAULT_AVATAR_HEIGHT / height; + SetAvatarScale(newScale); + scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range."; + } + + originalSliderScale = sliderScale; } - void OnGUI() { + void OnGUI() { // define UI styles for all GUI elements to be created GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); buttonStyle.fontSize = BUTTON_FONT_SIZE; @@ -1192,35 +1441,82 @@ class ExportProjectWindow : EditorWindow { errorStyle.normal.textColor = Color.red; errorStyle.wordWrap = true; GUIStyle warningStyle = new GUIStyle(errorStyle); - warningStyle.normal.textColor = Color.yellow; + warningStyle.normal.textColor = COLOR_YELLOW; + GUIStyle sliderStyle = new GUIStyle(GUI.skin.horizontalSlider); + sliderStyle.fixedWidth = SLIDER_WIDTH; + GUIStyle sliderThumbStyle = new GUIStyle(GUI.skin.horizontalSliderThumb); + + // set the background for the window to a darker gray + Texture2D backgroundTexture = new Texture2D(1, 1, TextureFormat.RGBA32, false); + backgroundTexture.SetPixel(0, 0, COLOR_BACKGROUND); + backgroundTexture.Apply(); + GUI.DrawTexture(new Rect(0, 0, maxSize.x, maxSize.y), backgroundTexture, ScaleMode.StretchToFill); GUILayout.Space(10); - // Project name label and input text field - GUILayout.Label("Export project name:", labelStyle); - projectName = GUILayout.TextField(projectName, textStyle); + if (updateExistingAvatar) { + // Project file to update label and input text field + GUILayout.Label("Project file to update:", labelStyle); + projectLocation = GUILayout.TextField(projectLocation, textStyle); + } else { + // Project name label and input text field + GUILayout.Label("Export project name:", labelStyle); + projectName = GUILayout.TextField(projectName, textStyle); + + GUILayout.Space(10); + + // Project location label and input text field + GUILayout.Label("Export project location:", labelStyle); + projectLocation = GUILayout.TextField(projectLocation, textStyle); + } - GUILayout.Space(10); - - // Project location label and input text field - GUILayout.Label("Export project location:", labelStyle); - projectLocation = GUILayout.TextField(projectLocation, textStyle); - - // Browse button to open folder explorer that starts at project location path and then updates project location + // Browse button to open file/folder explorer and set project location if (GUILayout.Button("Browse", buttonStyle)) { - string result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); - if (result.Length > 0) { // folder selection not cancelled + string result = ""; + if (updateExistingAvatar) { + // open file explorer starting at hifi projects folder in user documents and select target fst to update + string initialPath = string.IsNullOrEmpty(projectLocation) ? initialProjectLocation : projectLocation; + result = EditorUtility.OpenFilePanel("Select .fst to update", initialPath, "fst"); + } else { + // open folder explorer starting at project location path and select folder to create project folder in + result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); + } + if (!string.IsNullOrEmpty(result)) { // file/folder selection not cancelled projectLocation = result.Replace('/', '\\'); } } - // Red error label text to display any file-related errors + // warning if scale is above/below recommended range or if scale was auto-adjusted initially + GUILayout.Label(scaleWarningText, warningStyle); + + // from left to right show scale label, scale slider itself, and scale value input with % value + // slider value itself is from 0.0 to 2.0, and actual scale is an exponent of it with an offset of 1 + // displayed scale is the actual scale value with 2 decimal places, and changing the displayed + // scale via keyboard does the inverse calculation to get the slider value via logarithm + GUILayout.BeginHorizontal(); + GUILayout.Label("Scale:", labelStyle); + sliderScale = GUILayout.HorizontalSlider(sliderScale, MIN_SCALE_SLIDER, MAX_SCALE_SLIDER, sliderStyle, sliderThumbStyle); + float actualScale = (Mathf.Pow(SLIDER_SCALE_EXPONENT, sliderScale) - ACTUAL_SCALE_OFFSET); + GUIStyle scaleInputStyle = new GUIStyle(textStyle); + scaleInputStyle.fixedWidth = SCALE_TEXT_WIDTH; + actualScale *= 100.0f; // convert to 100-based percentage for display purposes + string actualScaleStr = GUILayout.TextField(String.Format("{0:0.00}", actualScale), scaleInputStyle); + actualScaleStr = Regex.Replace(actualScaleStr, @"[^0-9.]", ""); + actualScale = float.Parse(actualScaleStr); + actualScale /= 100.0f; // convert back to 1.0-based percentage + SetAvatarScale(actualScale); + GUILayout.Label("%", labelStyle); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + // red error label text to display any file-related errors GUILayout.Label("Error:", errorStyle); GUILayout.Label(errorText, errorStyle); GUILayout.Space(10); - // Yellow warning label text to display scrollable list of any bone-related warnings + // yellow warning label text to display scrollable list of any bone-related warnings GUILayout.Label("Warnings:", warningStyle); warningScrollPosition = GUILayout.BeginScrollView(warningScrollPosition, GUILayout.Width(WINDOW_WIDTH), GUILayout.Height(WARNING_SCROLL_HEIGHT)); @@ -1229,23 +1525,23 @@ class ExportProjectWindow : EditorWindow { GUILayout.Space(10); - // Export button which will verify project folder can actually be created + // export button will verify target project folder can actually be created (or target fst file is valid) // before closing popup window and calling back to initiate the export bool export = false; if (GUILayout.Button("Export", buttonStyle)) { export = true; if (!CheckForErrors(true)) { Close(); - onCloseCallback(projectDirectory, projectName, warningText); + onExportCallback(updateExistingAvatar ? projectLocation : projectDirectory, projectName, actualScale); } } - // Cancel button just closes the popup window without callback + // cancel button closes the popup window triggering the close callback to close the preview scene if (GUILayout.Button("Cancel", buttonStyle)) { Close(); } - // When either text field changes check for any errors if we didn't just check errors from clicking Export above + // When a text field changes check for any errors if we didn't just check errors from clicking Export above if (GUI.changed && !export) { CheckForErrors(false); } @@ -1253,40 +1549,111 @@ class ExportProjectWindow : EditorWindow { bool CheckForErrors(bool exporting) { errorText = EMPTY_ERROR_TEXT; // default to None if no errors found - projectDirectory = projectLocation + "\\" + projectName + "\\"; - if (projectName.Length > 0) { - // new project must have a unique folder name since the folder will be created for it - if (Directory.Exists(projectDirectory)) { - errorText = "A folder with the name " + projectName + - " already exists at that location.\nPlease choose a different project name or location."; + if (updateExistingAvatar) { + // if any text is set in the project file to update field verify that the file actually exists + if (projectLocation.Length > 0) { + if (!File.Exists(projectLocation)) { + errorText = "Please select a valid project file to update.\n"; + return true; + } + } else if (exporting) { + errorText = "Please select a project file to update.\n"; return true; } - } - if (projectLocation.Length > 0) { - // before clicking Export we can verify that the project location at least starts with a drive - if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { - errorText = "Project location is invalid. Please choose a different project location.\n"; - return true; + } else { + projectDirectory = projectLocation + "\\" + projectName + "\\"; + if (projectName.Length > 0) { + // new project must have a unique folder name since the folder will be created for it + if (Directory.Exists(projectDirectory)) { + errorText = "A folder with the name " + projectName + + " already exists at that location.\nPlease choose a different project name or location."; + return true; + } } - } - if (exporting) { - // when exporting, project name and location must both be defined, and project location must - // be valid and accessible (we attempt to create the project folder at this time to verify this) - if (projectName.Length == 0) { - errorText = "Please define a project name.\n"; - return true; - } else if (projectLocation.Length == 0) { - errorText = "Please define a project location.\n"; - return true; - } else { - try { - Directory.CreateDirectory(projectDirectory); - } catch { + if (projectLocation.Length > 0) { + // before clicking Export we can verify that the project location at least starts with a drive + if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { errorText = "Project location is invalid. Please choose a different project location.\n"; return true; } } - } + if (exporting) { + // when exporting, project name and location must both be defined, and project location must + // be valid and accessible (we attempt to create the project folder at this time to verify this) + if (projectName.Length == 0) { + errorText = "Please define a project name.\n"; + return true; + } else if (projectLocation.Length == 0) { + errorText = "Please define a project location.\n"; + return true; + } else { + try { + Directory.CreateDirectory(projectDirectory); + } catch { + errorText = "Project location is invalid. Please choose a different project location.\n"; + return true; + } + } + } + } + return false; } + + void UpdateScaleWarning() { + // called on any scale changes + float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject); + if (height < MINIMUM_RECOMMENDED_HEIGHT) { + scaleWarningText = "The height of the avatar is below the recommended minimum."; + } else if (height > MAXIMUM_RECOMMENDED_HEIGHT) { + scaleWarningText = "The height of the avatar is above the recommended maximum."; + } else if (Mathf.Abs(originalSliderScale - sliderScale) > SLIDER_DIFFERENCE_REMOVE_TEXT) { + // once moving slider beyond a small threshold, remove the automatically scaled text + scaleWarningText = ""; + } + } + + void SetAvatarScale(float actualScale) { + // set the new scale uniformly on the preview avatar's transform to show the resulting avatar size + avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale); + + // adjust slider scale value to match the new actual scale value + sliderScale = GetSliderScaleFromActualScale(actualScale); + + UpdateScaleWarning(); + } + + float GetSliderScaleFromActualScale(float actualScale) { + // since actual scale is an exponent of slider scale with an offset, do the logarithm operation to convert it back + return Mathf.Log(actualScale + ACTUAL_SCALE_OFFSET, SLIDER_SCALE_EXPONENT); + } + + void OnDestroy() { + onCloseCallback(); + } +} + +class AvatarUtilities { + public const float DEFAULT_AVATAR_HEIGHT = 1.755f; + + public static Bounds GetAvatarBounds(GameObject avatarObject) { + Bounds bounds = new Bounds(); + if (avatarObject != null) { + var meshRenderers = avatarObject.GetComponentsInChildren(); + var skinnedMeshRenderers = avatarObject.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + foreach (var renderer in skinnedMeshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + } + return bounds; + } + + public static float GetAvatarHeight(GameObject avatarObject) { + // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers + Bounds avatarBounds = GetAvatarBounds(avatarObject); + return avatarBounds.max.y - avatarBounds.min.y; + } } diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat new file mode 100644 index 0000000000..69421ca8e2 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Average + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.53309965, g: 0.8773585, b: 0.27727836, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat new file mode 100644 index 0000000000..4c63832593 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Floor + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab new file mode 100644 index 0000000000..4464617387 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab @@ -0,0 +1,1393 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1001 &100100000 +Prefab: + m_ObjectHideFlags: 1 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 0} + m_Modifications: [] + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 0} + m_RootGameObject: {fileID: 1663253797283788} + m_IsPrefabAsset: 1 +--- !u!1 &1046656866020106 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224386929081752724} + - component: {fileID: 222160789105267064} + - component: {fileID: 114930405832365464} + m_Layer: 5 + m_Name: TwoAndHalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1098451480288840 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4735851023856772} + - component: {fileID: 33008877752475126} + - component: {fileID: 23983268565997994} + m_Layer: 0 + m_Name: HalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1107359137501064 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224352215517075892} + - component: {fileID: 222924084127982026} + - component: {fileID: 114523909969846714} + m_Layer: 5 + m_Name: TwoMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1108041172082256 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224494569551489322} + - component: {fileID: 223961774962398002} + - component: {fileID: 114011556853048752} + - component: {fileID: 114521005238033952} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1165326825168616 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224593141416602104} + - component: {fileID: 222331762946337184} + - component: {fileID: 114101794169638918} + m_Layer: 5 + m_Name: OneAndHalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1182485492886750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4302978871272126} + - component: {fileID: 33686989621546016} + - component: {fileID: 23982106336197490} + m_Layer: 0 + m_Name: TwoAndHalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1365616260555366 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224613908675679132} + - component: {fileID: 222421911825862480} + - component: {fileID: 114276838631099888} + m_Layer: 5 + m_Name: OneMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1398639835840810 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4460037940915778} + - component: {fileID: 33999849812690240} + - component: {fileID: 23416265009837404} + m_Layer: 0 + m_Name: Floor + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1534720920953066 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4413776654278098} + - component: {fileID: 33291071156168694} + - component: {fileID: 23550720950256080} + m_Layer: 0 + m_Name: Average + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1594624973687270 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4908828994703896} + - component: {fileID: 33726300519449444} + - component: {fileID: 23824769923661608} + m_Layer: 0 + m_Name: Tall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1663253797283788 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4466308008297536} + m_Layer: 0 + m_Name: HeightReference + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1684603522306818 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4359301733271006} + - component: {fileID: 33170278100239952} + - component: {fileID: 23463284742561382} + m_Layer: 0 + m_Name: TwoMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1758516477546936 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224093314116541246} + - component: {fileID: 222104353024021134} + - component: {fileID: 114198955202599194} + m_Layer: 5 + m_Name: HalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1843086377652878 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4967607462495426} + - component: {fileID: 33458427168817864} + - component: {fileID: 23807848267690204} + m_Layer: 0 + m_Name: Short + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1845490813592506 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4990347338131576} + - component: {fileID: 108630196659418708} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1883639722740524 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4177433262325602} + - component: {fileID: 33418961761515394} + - component: {fileID: 23536779434871182} + m_Layer: 0 + m_Name: TooShort + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1885741171197356 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4718462335765420} + - component: {fileID: 33030310456480364} + - component: {fileID: 23105277758912132} + m_Layer: 0 + m_Name: TooTall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1919147340747728 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4440944676647488} + - component: {fileID: 33820823812379558} + - component: {fileID: 23886085173153614} + m_Layer: 0 + m_Name: OneAndHalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1985295559338180 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4498194399146796} + - component: {fileID: 33041053251399642} + - component: {fileID: 23936786851965954} + m_Layer: 0 + m_Name: OneMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4177433262325602 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.219375, z: -1} + m_LocalScale: {x: 200, y: 0.43875, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4302978871272126 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 2.5, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 12 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4359301733271006 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 2, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 11 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4413776654278098 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 1.535625, z: -1} + m_LocalScale: {x: 200, y: 1.19375, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4440944676647488 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 1.5, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 10 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4460037940915778 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: -50, z: -0.5} + m_LocalScale: {x: 200, y: 100, z: 2} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4466308008297536 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1663253797283788} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 4990347338131576} + - {fileID: 4460037940915778} + - {fileID: 4177433262325602} + - {fileID: 4967607462495426} + - {fileID: 4413776654278098} + - {fileID: 4908828994703896} + - {fileID: 4718462335765420} + - {fileID: 224494569551489322} + - {fileID: 4735851023856772} + - {fileID: 4498194399146796} + - {fileID: 4440944676647488} + - {fileID: 4359301733271006} + - {fileID: 4302978871272126} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4498194399146796 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 1, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 9 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4718462335765420 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 502.6325, z: -1} + m_LocalScale: {x: 200, y: 1000, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4735851023856772 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0.5, z: -0.94} + m_LocalScale: {x: 200, y: 1, z: 0.01} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 8 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4908828994703896 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 2.3825, z: -1} + m_LocalScale: {x: 200, y: 0.5, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4967607462495426 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.68875, z: -1} + m_LocalScale: {x: 200, y: 0.5, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4990347338131576 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1845490813592506} + m_LocalRotation: {x: -0.11086535, y: -0.8745676, z: 0.40781754, w: -0.23775047} + m_LocalPosition: {x: 0, y: 3, z: 77.17} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 50.000004, y: -210.41699, z: 0} +--- !u!23 &23105277758912132 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d07e04b46b88ae54e9f418c8645f1580, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23416265009837404 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 320b570da434d374985fe89d653ae75b, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23463284742561382 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23536779434871182 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d07e04b46b88ae54e9f418c8645f1580, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23550720950256080 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 722779087c41d074eb632820263fc661, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23807848267690204 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 1cd16d030e4890a4cab22d897ccfc8d8, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23824769923661608 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 1cd16d030e4890a4cab22d897ccfc8d8, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23886085173153614 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23936786851965954 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23982106336197490 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23983268565997994 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!33 &33008877752475126 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33030310456480364 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33041053251399642 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33170278100239952 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33291071156168694 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33418961761515394 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33458427168817864 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33686989621546016 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33726300519449444 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33820823812379558 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33999849812690240 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!108 &108630196659418708 +Light: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1845490813592506} + m_Enabled: 1 + serializedVersion: 8 + m_Type: 1 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 2 + m_Range: 10 + m_SpotAngle: 30 + m_CookieSize: 10 + m_Shadows: + m_Type: 0 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_Lightmapping: 1 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!114 &114011556853048752 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!114 &114101794169638918 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '1.5m + +' +--- !u!114 &114198955202599194 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 0.5m +--- !u!114 &114276838631099888 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '1.0m + +' +--- !u!114 &114521005238033952 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &114523909969846714 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '2.0m + +' +--- !u!114 &114930405832365464 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '2.5m + +' +--- !u!222 &222104353024021134 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_CullTransparentMesh: 0 +--- !u!222 &222160789105267064 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_CullTransparentMesh: 0 +--- !u!222 &222331762946337184 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_CullTransparentMesh: 0 +--- !u!222 &222421911825862480 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_CullTransparentMesh: 0 +--- !u!222 &222924084127982026 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_CullTransparentMesh: 0 +--- !u!223 &223961774962398002 +Canvas: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 2 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &224093314116541246 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_LocalRotation: {x: 0, y: 1, z: 0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.001, y: 0.001, z: 0.001} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 0.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224352215517075892 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 2.05} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224386929081752724 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 2.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224494569551489322 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 224093314116541246} + - {fileID: 224613908675679132} + - {fileID: 224593141416602104} + - {fileID: 224352215517075892} + - {fileID: 224386929081752724} + m_Father: {fileID: 4466308008297536} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 1000, y: 1000} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224593141416602104 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 1.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224613908675679132 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 1.05} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat new file mode 100644 index 0000000000..2f9a048c63 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Line + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0, g: 0, b: 0, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat new file mode 100644 index 0000000000..5543fef85e --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: ShortOrTall + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.91758025, g: 0.9622642, b: 0.28595585, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat new file mode 100644 index 0000000000..4851a64056 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: TooShortOrTall + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.9056604, g: 0.19223925, b: 0.19223925, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 402719b497..0da0fc0d9d 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,23 +1,24 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.3 +Version 0.4.1 -Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. +Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter. To create a new avatar project: 1. Import your .fbx avatar model into your Unity project's Assets by either dragging and dropping the file into the Assets window or by using Assets menu > Import New Assets. 2. Select the .fbx avatar that you imported in step 1 in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply. 3. With the .fbx avatar still selected in the Assets window, choose High Fidelity menu > Export New Avatar. 4. Select a name for your avatar project (this will be used to create a directory with that name), as well as the target location for your project folder. -5. Once it is exported, your project directory will open in File Explorer. +5. If necessary, adjust the scale for your avatar so that it's height is within the recommended range. +6. Once it is exported, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer. To update an existing avatar project: -1. Select the existing .fbx avatar in the Assets window that you would like to re-export. -2. Choose High Fidelity menu > Update Existing Avatar and browse to the .fst file you would like to update. +1. Select the existing .fbx avatar in the Assets window that you would like to re-export and choose High Fidelity menu > Update Existing Avatar +2. Select the .fst project file that you wish to update. 3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your selected avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file before performing the update. -4. Once it is updated, your project directory will open in File Explorer. +4. Once it is updated, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer. * WARNING * If you are using any external textures as part of your .fbx model, be sure they are copied into the textures folder that is created in the project folder after exporting a new avatar. -For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension +For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension \ No newline at end of file diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index f7385e3831..2ce40a8a8f 100644 Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 9401da4314..a5ad5bc891 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -42,12 +42,14 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { return false; } try { - QByteArray fbxContents = fbx.readAll(); + hifi::ByteArray fbxContents = fbx.readAll(); HFMModel::Pointer hfmModel; + hifi::VariantHash mapping; + mapping["deduplicateIndices"] = true; if (filename.toLower().endsWith(".obj")) { - hfmModel = OBJSerializer().read(fbxContents, QVariantHash(), filename); + hfmModel = OBJSerializer().read(fbxContents, mapping, filename); } else if (filename.toLower().endsWith(".fbx")) { - hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), filename); + hfmModel = FBXSerializer().read(fbxContents, mapping, filename); } else { qWarning() << "file has unknown extension" << filename; return false;