diff --git a/.gitignore b/.gitignore index 09b58d71ef..bbb79ad6a9 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,4 @@ tools/jsdoc/package-lock.json tools/unity-avatar-exporter/Library tools/unity-avatar-exporter/Packages tools/unity-avatar-exporter/ProjectSettings +tools/unity-avatar-exporter/Temp diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5e41530d93..1c02ba88ad 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -170,6 +170,7 @@ #include "scripting/Audio.h" #include "networking/CloseEventSender.h" #include "scripting/TestScriptingInterface.h" +#include "scripting/PlatformInfoScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/ClipboardScriptingInterface.h" #include "scripting/DesktopScriptingInterface.h" @@ -6691,6 +6692,7 @@ void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); _overlayConductor.centerUI(); + getActiveDisplayPlugin()->resetSensors(); getMyAvatar()->reset(true, andReload); QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); } @@ -6994,6 +6996,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); } + scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine @@ -8929,6 +8932,10 @@ void Application::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } +QString Application::getGraphicsCardType() { + return GPUIdent::getInstance()->getName(); +} + #if defined(Q_OS_ANDROID) void Application::beforeEnterBackground() { auto nodeList = DependencyManager::get(); diff --git a/interface/src/Application.h b/interface/src/Application.h index fd45a594b5..a41c9f39f2 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -120,7 +120,7 @@ class Application : public QApplication, public: // virtual functions required for PluginContainer virtual ui::Menu* getPrimaryMenu() override; - virtual void requestReset() override { resetSensors(true); } + virtual void requestReset() override { resetSensors(false); } virtual void showDisplayPluginsTools(bool show) override; virtual GLWidget* getPrimaryWidget() override; virtual MainWindow* getPrimaryWindow() override; @@ -459,6 +459,8 @@ public slots: void changeViewAsNeeded(float boomLength); + QString Application::getGraphicsCardType(); + private slots: void onDesktopRootItemCreated(QQuickItem* qmlContext); void onDesktopRootContextCreated(QQmlContext* qmlContext); @@ -787,6 +789,5 @@ private: bool _showTrackedObjects { false }; bool _prevShowTrackedObjects { false }; - }; #endif // hifi_Application_h diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp new file mode 100644 index 0000000000..7f5ee3e08a --- /dev/null +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -0,0 +1,77 @@ +// +// Created by Nissim Hadar on 2018/12/28 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "PlatformInfoScriptingInterface.h" +#include "Application.h" + +#include + +#ifdef Q_OS_WIN +#include +#endif + +PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() { + static PlatformInfoScriptingInterface sharedInstance; + return &sharedInstance; +} + +QString PlatformInfoScriptingInterface::getOperatingSystemType() { +#ifdef Q_OS_WIN + return "WINDOWS"; +#elif defined Q_OS_MAC + return "MACOS"; +#else + return "UNKNOWN"; +#endif +} + +QString PlatformInfoScriptingInterface::getCPUBrand() { +#ifdef Q_OS_WIN + int CPUInfo[4] = { -1 }; + unsigned nExIds, i = 0; + char CPUBrandString[0x40]; + // Get the information associated with each extended ID. + __cpuid(CPUInfo, 0x80000000); + nExIds = CPUInfo[0]; + + for (i = 0x80000000; i <= nExIds; ++i) { + __cpuid(CPUInfo, i); + // Interpret CPU brand string + if (i == 0x80000002) { + memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo)); + } else if (i == 0x80000003) { + memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo)); + } else if (i == 0x80000004) { + memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo)); + } + } + + return CPUBrandString; +#else + return "NOT IMPLEMENTED"; +#endif +} + +unsigned int PlatformInfoScriptingInterface::getNumLogicalCores() { + + return std::thread::hardware_concurrency(); +} + +int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() { +#ifdef Q_OS_WIN + MEMORYSTATUSEX statex; + statex.dwLength = sizeof (statex); + GlobalMemoryStatusEx(&statex); + return statex.ullTotalPhys / 1024 / 1024; +#else + return -1; +#endif +} + +QString PlatformInfoScriptingInterface::getGraphicsCardType() { + return qApp->getGraphicsCardType(); +} \ No newline at end of file diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h new file mode 100644 index 0000000000..903658cc20 --- /dev/null +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -0,0 +1,58 @@ +// +// Created by Nissim Hadar on 2018/12/28 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_PlatformInfoScriptingInterface_h +#define hifi_PlatformInfoScriptingInterface_h + +#include + +class QScriptValue; + +class PlatformInfoScriptingInterface : public QObject { + Q_OBJECT + +public slots: + static PlatformInfoScriptingInterface* getInstance(); + + /**jsdoc + * Returns the Operating Sytem type + * @function Test.getOperatingSystemType + * @returns {string} "WINDOWS", "MACOS" or "UNKNOWN" + */ + QString getOperatingSystemType(); + + /**jsdoc + * Returns the CPU brand + *function PlatformInfo.getCPUBrand() + * @returns {string} brand of CPU + */ + QString getCPUBrand(); + + /**jsdoc + * Returns the number of logical CPU cores + *function PlatformInfo.getNumLogicalCores() + * @returns {int} number of logical CPU cores + */ + unsigned int getNumLogicalCores(); + + /**jsdoc + * Returns the total system memory in megabyte + *function PlatformInfo.getTotalSystemMemory() + * @returns {int} size of memory in megabytes + */ + int getTotalSystemMemoryMB(); + + /**jsdoc + * Returns the graphics card type + * @function Test.getGraphicsCardType + * @returns {string} graphics card type + */ + QString getGraphicsCardType(); +}; + +#endif // hifi_PlatformInfoScriptingInterface_h diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index a705b61cd3..1b5b8bccaf 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -38,18 +38,10 @@ ShapeEntityRenderer::ShapeEntityRenderer(const EntityItemPointer& entity) : Pare // FIXME: Setup proper uniform slots and use correct pipelines for forward rendering _procedural._opaqueFragmentSource = gpu::Shader::Source::get(shader::render_utils::fragment::simple); _procedural._transparentFragmentSource = gpu::Shader::Source::get(shader::render_utils::fragment::simple_transparent); - _procedural._opaqueState->setCullMode(gpu::State::CULL_NONE); - _procedural._opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); + + // TODO: move into Procedural.cpp PrepareStencil::testMaskDrawShape(*_procedural._opaqueState); - _procedural._opaqueState->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); - _procedural._transparentState->setCullMode(gpu::State::CULL_BACK); - _procedural._transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); PrepareStencil::testMask(*_procedural._transparentState); - _procedural._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); } bool ShapeEntityRenderer::needsRenderUpdate() const { @@ -212,7 +204,10 @@ ShapeKey ShapeEntityRenderer::getShapeKey() { return builder.build(); } else { ShapeKey::Builder builder; - if (_procedural.isReady()) { + bool proceduralReady = resultWithReadLock([&] { + return _procedural.isReady(); + }); + if (proceduralReady) { builder.withOwnPipeline(); } if (isTransparent()) { @@ -242,7 +237,7 @@ 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, outColor); + _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f)); proceduralRender = true; } } diff --git a/libraries/gpu/src/gpu/Pipeline.h b/libraries/gpu/src/gpu/Pipeline.h index 28f7fe106e..b46226182a 100755 --- a/libraries/gpu/src/gpu/Pipeline.h +++ b/libraries/gpu/src/gpu/Pipeline.h @@ -38,8 +38,6 @@ protected: StatePointer _state; Pipeline(); - Pipeline(const Pipeline& pipeline); // deep copy of the sysmem shader - Pipeline& operator=(const Pipeline& pipeline); // deep copy of the sysmem texture }; typedef Pipeline::Pointer PipelinePointer; diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 7095732d53..05cbde374d 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -39,8 +39,10 @@ static const std::string PROCEDURAL_BLOCK = "//PROCEDURAL_BLOCK"; static const std::string PROCEDURAL_VERSION = "//PROCEDURAL_VERSION"; bool operator==(const ProceduralData& a, const ProceduralData& b) { - return ((a.version == b.version) && (a.shaderUrl == b.shaderUrl) && (a.uniforms == b.uniforms) && - (a.channels == b.channels)); + return ((a.version == b.version) && + (a.shaderUrl == b.shaderUrl) && + (a.uniforms == b.uniforms) && + (a.channels == b.channels)); } QJsonValue ProceduralData::getProceduralData(const QString& proceduralJson) { @@ -57,9 +59,9 @@ QJsonValue ProceduralData::getProceduralData(const QString& proceduralJson) { return doc.object()[PROCEDURAL_USER_DATA_KEY]; } -ProceduralData ProceduralData::parse(const QString& userDataJson) { +ProceduralData ProceduralData::parse(const QString& proceduralData) { ProceduralData result; - result.parse(getProceduralData(userDataJson).toObject()); + result.parse(getProceduralData(proceduralData).toObject()); return result; } @@ -73,7 +75,7 @@ void ProceduralData::parse(const QJsonObject& proceduralData) { if (versionJson.isDouble()) { version = (uint8_t)(floor(versionJson.toDouble())); // invalid version - if (!(version == 1 || version == 2)) { + if (!(version == 1 || version == 2 || version == 3 || version == 4)) { return; } } else { @@ -102,20 +104,27 @@ void ProceduralData::parse(const QJsonObject& proceduralData) { //} Procedural::Procedural() { - _transparentState->setCullMode(gpu::State::CULL_NONE); + _opaqueState->setCullMode(gpu::State::CULL_BACK); + _opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); + _opaqueState->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); + + _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, + _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); _standardInputsBuffer = std::make_shared(sizeof(StandardInputs), nullptr); } void Procedural::setProceduralData(const ProceduralData& proceduralData) { + std::lock_guard lock(_mutex); if (proceduralData == _data) { return; } - _dirty = true; _enabled = false; if (proceduralData.version != _data.version) { @@ -124,6 +133,10 @@ void Procedural::setProceduralData(const ProceduralData& proceduralData) { } if (proceduralData.uniforms != _data.uniforms) { + // If the uniform keys changed, we need to recreate the whole shader to handle the reflection + if (proceduralData.uniforms.keys() != _data.uniforms.keys()) { + _shaderDirty = true; + } _data.uniforms = proceduralData.uniforms; _uniformsDirty = true; } @@ -147,16 +160,14 @@ void Procedural::setProceduralData(const ProceduralData& proceduralData) { if (proceduralData.shaderUrl != _data.shaderUrl) { _data.shaderUrl = proceduralData.shaderUrl; - _shaderDirty = true; const auto& shaderUrl = _data.shaderUrl; + + _shaderDirty = true; _networkShader.reset(); _shaderPath.clear(); + _shaderSource.clear(); - if (shaderUrl.isEmpty()) { - return; - } - - if (!shaderUrl.isValid()) { + if (shaderUrl.isEmpty() || !shaderUrl.isValid()) { return; } @@ -180,6 +191,8 @@ bool Procedural::isReady() const { return false; #endif + std::lock_guard lock(_mutex); + if (!_enabled) { return false; } @@ -209,10 +222,11 @@ bool Procedural::isReady() const { } void Procedural::prepare(gpu::Batch& batch, - const glm::vec3& position, - const glm::vec3& size, - const glm::quat& orientation, - const glm::vec4& color) { + const glm::vec3& position, + const glm::vec3& size, + const glm::quat& orientation, + const ProceduralProgramKey key) { + std::lock_guard lock(_mutex); _entityDimensions = size; _entityPosition = position; _entityOrientation = glm::mat3_cast(orientation); @@ -225,62 +239,56 @@ void Procedural::prepare(gpu::Batch& batch, _shaderDirty = true; _shaderModified = lastModified; } - } else if (_networkShader && _networkShader->isLoaded()) { + } else if (_shaderSource.isEmpty() && _networkShader && _networkShader->isLoaded()) { _shaderSource = _networkShader->_source; + _shaderDirty = true; } - if (!_opaquePipeline || !_transparentPipeline || _shaderDirty) { + if (_shaderDirty) { + _proceduralPipelines.clear(); + } + + auto pipeline = _proceduralPipelines.find(key); + bool recompiledShader = false; + if (pipeline == _proceduralPipelines.end()) { if (!_vertexShader) { _vertexShader = gpu::Shader::createVertex(_vertexSource); } + gpu::Shader::Source& fragmentSource = (key.isTransparent() && _transparentFragmentSource.valid()) ? _transparentFragmentSource : _opaqueFragmentSource; + // Build the fragment shader - _opaqueFragmentSource.replacements.clear(); - if (_data.version == 1) { - _opaqueFragmentSource.replacements[PROCEDURAL_VERSION] = "#define PROCEDURAL_V1 1"; - } else if (_data.version == 2) { - _opaqueFragmentSource.replacements[PROCEDURAL_VERSION] = "#define PROCEDURAL_V2 1"; - } - _opaqueFragmentSource.replacements[PROCEDURAL_BLOCK] = _shaderSource.toStdString(); - _transparentFragmentSource.replacements = _opaqueFragmentSource.replacements; + fragmentSource.replacements.clear(); + fragmentSource.replacements[PROCEDURAL_VERSION] = "#define PROCEDURAL_V" + std::to_string(_data.version); + fragmentSource.replacements[PROCEDURAL_BLOCK] = _shaderSource.toStdString(); // Set any userdata specified uniforms int customSlot = procedural::slot::uniform::Custom; for (const auto& key : _data.uniforms.keys()) { std::string uniformName = key.toLocal8Bit().data(); - _opaqueFragmentSource.reflection.uniforms[uniformName] = customSlot; - _transparentFragmentSource.reflection.uniforms[uniformName] = customSlot; + fragmentSource.reflection.uniforms[uniformName] = customSlot; ++customSlot; } // Leave this here for debugging - // qCDebug(procedural) << "FragmentShader:\n" << fragmentShaderSource.c_str(); + //qCDebug(proceduralLog) << "FragmentShader:\n" << fragmentSource.getSource(shader::Dialect::glsl450, shader::Variant::Mono).c_str(); + + gpu::ShaderPointer fragmentShader = gpu::Shader::createPixel(fragmentSource); + gpu::ShaderPointer program = gpu::Shader::createProgram(_vertexShader, fragmentShader); + + _proceduralPipelines[key] = gpu::Pipeline::create(program, key.isTransparent() ? _transparentState : _opaqueState); - // TODO: THis is a simple fix, we need a cleaner way to provide the "hosting" program for procedural custom shaders to be defined together with the required bindings. - _opaqueFragmentShader = gpu::Shader::createPixel(_opaqueFragmentSource); - _opaqueShader = gpu::Shader::createProgram(_vertexShader, _opaqueFragmentShader); - _opaquePipeline = gpu::Pipeline::create(_opaqueShader, _opaqueState); - if (_transparentFragmentSource.valid()) { - _transparentFragmentShader = gpu::Shader::createPixel(_transparentFragmentSource); - _transparentShader = gpu::Shader::createProgram(_vertexShader, _transparentFragmentShader); - _transparentPipeline = gpu::Pipeline::create(_transparentShader, _transparentState); - } else { - _transparentFragmentShader = _opaqueFragmentShader; - _transparentShader = _opaqueShader; - _transparentPipeline = _opaquePipeline; - } _start = usecTimestampNow(); _frameCount = 0; + recompiledShader = true; } - bool transparent = color.a < 1.0f; - batch.setPipeline(transparent ? _transparentPipeline : _opaquePipeline); + batch.setPipeline(recompiledShader ? _proceduralPipelines[key] : pipeline->second); - if (_shaderDirty || _uniformsDirty || _prevTransparent != transparent) { - setupUniforms(transparent); + if (_shaderDirty || _uniformsDirty) { + setupUniforms(); } - _prevTransparent = transparent; _shaderDirty = _uniformsDirty = false; for (auto lambda : _uniforms) { @@ -290,8 +298,7 @@ void Procedural::prepare(gpu::Batch& batch, static gpu::Sampler sampler; static std::once_flag once; std::call_once(once, [&] { - gpu::Sampler::Desc desc; - desc._filter = gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR; + sampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); }); for (size_t i = 0; i < MAX_PROCEDURAL_TEXTURE_CHANNELS; ++i) { @@ -301,19 +308,17 @@ void Procedural::prepare(gpu::Batch& batch, gpuTexture->setSampler(sampler); gpuTexture->setAutoGenerateMips(true); } - batch.setResourceTexture((gpu::uint32)i, gpuTexture); + batch.setResourceTexture((gpu::uint32)(procedural::slot::texture::Channel0 + i), gpuTexture); } } } -void Procedural::setupUniforms(bool transparent) { +void Procedural::setupUniforms() { _uniforms.clear(); - auto customUniformCount = _data.uniforms.keys().size(); // Set any userdata specified uniforms - for (int i = 0; i < customUniformCount; ++i) { - int slot = procedural::slot::uniform::Custom + i; - QString key = _data.uniforms.keys().at(i); + int slot = procedural::slot::uniform::Custom; + for (const auto& key : _data.uniforms.keys()) { std::string uniformName = key.toLocal8Bit().data(); QJsonValue value = _data.uniforms[key]; if (value.isDouble()) { @@ -360,6 +365,7 @@ void Procedural::setupUniforms(bool transparent) { } } } + slot++; } _uniforms.push_back([=](gpu::Batch& batch) { @@ -398,7 +404,7 @@ void Procedural::setupUniforms(bool transparent) { }); } -glm::vec4 Procedural::getColor(const glm::vec4& entityColor) { +glm::vec4 Procedural::getColor(const glm::vec4& entityColor) const { if (_data.version == 1) { return glm::vec4(1); } diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 781ac25249..3e10678ba7 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -7,8 +7,6 @@ // #pragma once -#ifndef hifi_RenderableProcedrualItem_h -#define hifi_RenderableProcedrualItem_h #include @@ -32,7 +30,6 @@ const size_t MAX_PROCEDURAL_TEXTURE_CHANNELS{ 4 }; struct ProceduralData { static QJsonValue getProceduralData(const QString& proceduralJson); static ProceduralData parse(const QString& userDataJson); - // This should only be called from the render thread, as it shares data with Procedural::prepare void parse(const QJsonObject&); // Rendering object descriptions, from userData @@ -42,10 +39,40 @@ struct ProceduralData { QJsonArray channels; }; +class ProceduralProgramKey { +public: + enum FlagBit { + IS_TRANSPARENT = 0, + NUM_FLAGS + }; + + typedef std::bitset Flags; + + Flags _flags; + + bool isTransparent() const { return _flags[IS_TRANSPARENT]; } + + ProceduralProgramKey(bool transparent = false) { + if (transparent) { + _flags.set(IS_TRANSPARENT); + } + } +}; +namespace std { + template <> + struct hash { + size_t operator()(const ProceduralProgramKey& key) const { + return std::hash>()(key._flags); + } + }; +} +inline bool operator==(const ProceduralProgramKey& a, const ProceduralProgramKey& b) { + return a._flags == b._flags; +} +inline bool operator!=(const ProceduralProgramKey& a, const ProceduralProgramKey& b) { + return a._flags != b._flags; +} -// WARNING with threaded rendering it is the RESPONSIBILITY OF THE CALLER to ensure that -// calls to `setProceduralData` happen on the main thread and that calls to `ready` and `prepare` -// are treated atomically, and that they cannot happen concurrently with calls to `setProceduralData` // FIXME better encapsulation // FIXME better mechanism for extending to things rendered using shaders other than simple.slv struct Procedural { @@ -55,10 +82,9 @@ 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 glm::vec4& color = glm::vec4(1)); - const gpu::ShaderPointer& getOpaqueShader() const { return _opaqueShader; } + void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const ProceduralProgramKey key = ProceduralProgramKey()); - glm::vec4 getColor(const glm::vec4& entityColor); + glm::vec4 getColor(const glm::vec4& entityColor) const; quint64 getFadeStartTime() const { return _fadeStartTime; } bool isFading() const { return _doesFade && _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } @@ -108,22 +134,19 @@ protected: QString _shaderPath; quint64 _shaderModified { 0 }; NetworkShaderPointer _networkShader; - bool _dirty { false }; bool _shaderDirty { true }; bool _uniformsDirty { true }; // Rendering objects UniformLambdas _uniforms; NetworkTexturePointer _channels[MAX_PROCEDURAL_TEXTURE_CHANNELS]; - gpu::PipelinePointer _opaquePipeline; - gpu::PipelinePointer _transparentPipeline; + + std::unordered_map _proceduralPipelines; + + gpu::ShaderPointer _vertexShader; + StandardInputs _standardInputs; gpu::BufferPointer _standardInputsBuffer; - gpu::ShaderPointer _vertexShader; - gpu::ShaderPointer _opaqueFragmentShader; - gpu::ShaderPointer _transparentFragmentShader; - gpu::ShaderPointer _opaqueShader; - gpu::ShaderPointer _transparentShader; // Entity metadata glm::vec3 _entityDimensions; @@ -131,14 +154,11 @@ protected: glm::mat3 _entityOrientation; private: - // This should only be called from the render thread, as it shares data with Procedural::prepare - void setupUniforms(bool transparent); + void setupUniforms(); mutable quint64 _fadeStartTime { 0 }; mutable bool _hasStartedFade { false }; mutable bool _isFading { false }; bool _doesFade { true }; - bool _prevTransparent { false }; + mutable std::mutex _mutex; }; - -#endif diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slh b/libraries/procedural/src/procedural/ProceduralCommon.slh index d515a79e22..bd894a9e92 100644 --- a/libraries/procedural/src/procedural/ProceduralCommon.slh +++ b/libraries/procedural/src/procedural/ProceduralCommon.slh @@ -8,11 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include gpu/Transform.slh@> <@include gpu/Noise.slh@> <@include procedural/ShaderConstants.h@> - -<$declareStandardCameraTransform()$> LAYOUT(binding=PROCEDURAL_TEXTURE_CHANNEL0) uniform sampler2D iChannel0; LAYOUT(binding=PROCEDURAL_TEXTURE_CHANNEL1) uniform sampler2D iChannel1; @@ -59,6 +56,32 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { #define iChannelResolution standardInputs.channelResolution #define iWorldOrientation standardInputs.worldOrientation +struct ProceduralFragment { + vec3 normal; + vec3 diffuse; + vec3 specular; + vec3 emissive; + float alpha; + float roughness; + float metallic; + float occlusion; + float scattering; +}; + +// Same as ProceduralFragment but with position +struct ProceduralFragmentWithPosition { + vec3 position; + vec3 normal; + vec3 diffuse; + vec3 specular; + vec3 emissive; + float alpha; + float roughness; + float metallic; + float occlusion; + float scattering; +}; + // Unimplemented uniforms // Resolution doesn't make sense in the VR context const vec3 iResolution = vec3(1.0); @@ -69,8 +92,6 @@ const float iSampleRate = 1.0; // No support for video input const vec4 iChannelTime = vec4(0.0); -#define PROCEDURAL 1 - //PROCEDURAL_VERSION // hack comment for extra whitespace diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index ea5be23eb8..211f6ca0a2 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -35,7 +35,6 @@ bool ProceduralSkybox::empty() { void ProceduralSkybox::clear() { // Parse and prepare a procedural with no shaders to release textures parse(QString()); - _procedural.isReady(); Skybox::clear(); } diff --git a/libraries/render-utils/src/forward_simple.slf b/libraries/render-utils/src/forward_simple.slf index 9c86f9dff1..677c369033 100644 --- a/libraries/render-utils/src/forward_simple.slf +++ b/libraries/render-utils/src/forward_simple.slf @@ -14,9 +14,9 @@ <@include DefaultMaterials.slh@> <@include ForwardGlobalLight.slh@> -<@include gpu/Transform.slh@> - <$declareEvalSkyboxGlobalColor()$> + +<@include gpu/Transform.slh@> <$declareStandardCameraTransform()$> // the interpolated normal diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index d2ddaa8e55..582549ade1 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -14,6 +14,9 @@ <@include DeferredBufferWrite.slh@> +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + <@include render-utils/ShaderConstants.h@> // the interpolated normal @@ -45,25 +48,76 @@ float getProceduralColors(inout vec3 diffuse, inout vec3 specular, inout float s return 1.0; } +float getProceduralFragment(inout ProceduralFragment proceduralData) { + return 1.0; +} + +float getProceduralFragmentWithPosition(inout ProceduralFragmentWithPosition proceduralData) { + return 1.0; +} + //PROCEDURAL_BLOCK_END #line 2030 void main(void) { vec3 normal = normalize(_normalWS.xyz); vec3 diffuse = _color.rgb; - vec3 specular = DEFAULT_SPECULAR; - float shininess = DEFAULT_SHININESS; + float roughness = DEFAULT_ROUGHNESS; + float metallic = DEFAULT_METALLIC; + vec3 emissive = DEFAULT_EMISSIVE; + float occlusion = DEFAULT_OCCLUSION; + float scattering = DEFAULT_SCATTERING; + float emissiveAmount = 0.0; -#ifdef PROCEDURAL - -#ifdef PROCEDURAL_V1 +#if defined(PROCEDURAL_V1) diffuse = getProceduralColor().rgb; - // Procedural Shaders are expected to be Gamma corrected so let's bring back the RGB in linear space for the rest of the pipeline - //diffuse = pow(diffuse, vec3(2.2)); emissiveAmount = 1.0; -#else + emissive = vec3(1.0); +#elif defined(PROCEDURAL_V2) + vec3 specular = DEFAULT_SPECULAR; + float shininess = DEFAULT_SHININESS; emissiveAmount = getProceduralColors(diffuse, specular, shininess); + roughness = max(0.0, 1.0 - shininess / 128.0); + metallic = length(specular); + emissive = vec3(clamp(emissiveAmount, 0.0, 1.0)); +#elif defined(PROCEDURAL_V3) || defined(PROCEDURAL_V4) +#if defined(PROCEDURAL_V3) + ProceduralFragment proceduralData = ProceduralFragment( +#else + TransformCamera cam = getTransformCamera(); + vec4 position = cam._viewInverse * _positionES; + ProceduralFragmentWithPosition proceduralData = ProceduralFragmentWithPosition( + position.xyz, +#endif + normal, + vec3(0.0), + DEFAULT_SPECULAR, + DEFAULT_EMISSIVE, + 1.0, + DEFAULT_ROUGHNESS, + DEFAULT_METALLIC, + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING + ); + +#if defined(PROCEDURAL_V3) + emissiveAmount = getProceduralFragment(proceduralData); +#else + emissiveAmount = getProceduralFragmentWithPosition(proceduralData); +#endif + normal = proceduralData.normal; + diffuse = proceduralData.diffuse; + roughness = proceduralData.roughness; + metallic = proceduralData.metallic; + emissive = proceduralData.emissive; + occlusion = proceduralData.occlusion; + scattering = proceduralData.scattering; + +#if defined(PROCEDURAL_V4) + position = vec4(proceduralData.position, 1.0); + vec4 posClip = cam._projection * (cam._view * position); + gl_FragDepth = 0.5 * (posClip.z / posClip.w + 1.0); #endif #endif @@ -73,18 +127,18 @@ void main(void) { normal, 1.0, diffuse, - max(0.0, 1.0 - shininess / 128.0), - DEFAULT_METALLIC, - vec3(clamp(emissiveAmount, 0.0, 1.0))); + roughness, + metallic, + emissive); } else { packDeferredFragment( normal, 1.0, diffuse, - max(0.0, 1.0 - shininess / 128.0), - length(specular), - DEFAULT_EMISSIVE, - DEFAULT_OCCLUSION, - DEFAULT_SCATTERING); + roughness, + metallic, + emissive, + occlusion, + scattering); } } diff --git a/libraries/render-utils/src/simple_transparent.slf b/libraries/render-utils/src/simple_transparent.slf index 0e29ed7470..ea444d6113 100644 --- a/libraries/render-utils/src/simple_transparent.slf +++ b/libraries/render-utils/src/simple_transparent.slf @@ -16,6 +16,9 @@ <@include DeferredGlobalLight.slh@> <$declareEvalGlobalLightingAlphaBlendedWithHaze()$> +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + <@include render-utils/ShaderConstants.h@> // the interpolated normal @@ -50,46 +53,100 @@ float getProceduralColors(inout vec3 diffuse, inout vec3 specular, inout float s return 1.0; } +float getProceduralFragment(inout ProceduralFragment proceduralData) { + return 1.0; +} + +float getProceduralFragmentWithPosition(inout ProceduralFragmentWithPosition proceduralData) { + return 1.0; +} + //PROCEDURAL_BLOCK_END #line 2030 void main(void) { vec3 normal = normalize(_normalWS.xyz); vec3 diffuse = _color.rgb; - vec3 specular = DEFAULT_SPECULAR; - float shininess = DEFAULT_SHININESS; + float alpha = _color.a; + float occlusion = DEFAULT_OCCLUSION; + vec3 fresnel = DEFAULT_FRESNEL; + float metallic = DEFAULT_METALLIC; + vec3 emissive = DEFAULT_EMISSIVE; + float roughness = DEFAULT_ROUGHNESS; + float emissiveAmount = 0.0; - -#ifdef PROCEDURAL + + TransformCamera cam = getTransformCamera(); + vec3 posEye = _positionES.xyz; #ifdef PROCEDURAL_V1 diffuse = getProceduralColor().rgb; - // Procedural Shaders are expected to be Gamma corrected so let's bring back the RGB in linear space for the rest of the pipeline - //diffuse = pow(diffuse, vec3(2.2)); emissiveAmount = 1.0; -#else + emissive = vec3(1.0); +#elif defined(PROCEDURAL_V2) + vec3 specular = DEFAULT_SPECULAR; + float shininess = DEFAULT_SHININESS; emissiveAmount = getProceduralColors(diffuse, specular, shininess); + roughness = max(0.0, 1.0 - shininess / 128.0); + metallic = length(specular); + emissive = vec3(clamp(emissiveAmount, 0.0, 1.0)); +#elif defined(PROCEDURAL_V3) || defined(PROCEDURAL_V4) +#if defined(PROCEDURAL_V3) + ProceduralFragment proceduralData = { +#else + vec4 position = cam._viewInverse * _positionES; + ProceduralFragmentWithPosition proceduralData = { + position.xyz, +#endif + normal, + vec3(0.0), + DEFAULT_SPECULAR, + DEFAULT_EMISSIVE, + 1.0, + DEFAULT_ROUGHNESS, + DEFAULT_METALLIC, + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING + }; + +#if defined(PROCEDURAL_V3) + emissiveAmount = getProceduralFragment(proceduralData); +#else + emissiveAmount = getProceduralFragmentWithPosition(proceduralData); +#endif + occlusion = proceduralData.occlusion; + normal = proceduralData.normal; + diffuse = proceduralData.diffuse; + fresnel = proceduralData.specular; + metallic = proceduralData.metallic; + emissive = proceduralData.emissive; + roughness = proceduralData.roughness; + alpha = proceduralData.alpha; + +#if defined(PROCEDURAL_V4) + position = vec4(proceduralData.position, 1.0); + vec4 posEye4 = cam._view * position; + posEye = posEye4.xyz; + vec4 posClip = cam._projection * posEye4; + gl_FragDepth = 0.5 * (posClip.z / posClip.w + 1.0); #endif #endif - TransformCamera cam = getTransformCamera(); - vec3 fragPosition = _positionES.xyz; - if (emissiveAmount > 0.0) { - _fragColor0 = vec4(diffuse, _color.a); + _fragColor0 = vec4(diffuse, alpha); } else { _fragColor0 = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, - DEFAULT_OCCLUSION, - fragPosition, + occlusion, + posEye, normal, diffuse, - DEFAULT_FRESNEL, - length(specular), - DEFAULT_EMISSIVE, - max(0.0, 1.0 - shininess / 128.0), _color.a), - _color.a); + fresnel, + metallic, + emissive, + roughness, alpha), + alpha); } } diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 60b5e0e643..18916267f0 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -12,8 +12,8 @@ using System; using System.IO; using System.Collections.Generic; -public class AvatarExporter : MonoBehaviour { - public static Dictionary UNITY_TO_HIFI_JOINT_NAME = new Dictionary { +class AvatarExporter : MonoBehaviour { + static readonly Dictionary HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary { {"Chest", "Spine1"}, {"Head", "Head"}, {"Hips", "Hips"}, @@ -70,138 +70,531 @@ public class AvatarExporter : MonoBehaviour { {"UpperChest", "Spine2"}, }; - public static string exportedPath = String.Empty; + static readonly Dictionary referenceAbsoluteRotations = new Dictionary { + {"Head", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)}, + {"Hips", new Quaternion(-3.043941e-10f, -1.573706e-7f, 5.112975e-6f, 1f)}, + {"LeftHandIndex3", new Quaternion(-0.5086057f, 0.4908088f, -0.4912299f, -0.5090388f)}, + {"LeftHandIndex2", new Quaternion(-0.4934928f, 0.5062312f, -0.5064303f, -0.4936835f)}, + {"LeftHandIndex1", new Quaternion(-0.4986293f, 0.5017503f, -0.5013659f, -0.4982448f)}, + {"LeftHandPinky3", new Quaternion(-0.490056f, 0.5143053f, -0.5095307f, -0.4855038f)}, + {"LeftHandPinky2", new Quaternion(-0.5083722f, 0.4954255f, -0.4915887f, -0.5044324f)}, + {"LeftHandPinky1", new Quaternion(-0.5062528f, 0.497324f, -0.4937346f, -0.5025966f)}, + {"LeftHandMiddle3", new Quaternion(-0.4871885f, 0.5123404f, -0.5125002f, -0.4873383f)}, + {"LeftHandMiddle2", new Quaternion(-0.5171652f, 0.4827828f, -0.4822642f, -0.5166069f)}, + {"LeftHandMiddle1", new Quaternion(-0.4955998f, 0.5041052f, -0.5043675f, -0.4958555f)}, + {"LeftHandRing3", new Quaternion(-0.4936301f, 0.5097645f, -0.5061787f, -0.4901562f)}, + {"LeftHandRing2", new Quaternion(-0.5089865f, 0.4943658f, -0.4909532f, -0.5054707f)}, + {"LeftHandRing1", new Quaternion(-0.5020972f, 0.5005084f, -0.4979034f, -0.4994819f)}, + {"LeftHandThumb3", new Quaternion(-0.7228092f, 0.2988393f, -0.4472938f, -0.4337862f)}, + {"LeftHandThumb2", new Quaternion(-0.7554525f, 0.2018595f, -0.3871402f, -0.4885356f)}, + {"LeftHandThumb1", new Quaternion(-0.7276843f, 0.2878546f, -0.439926f, -0.4405459f)}, + {"LeftEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)}, + {"LeftFoot", new Quaternion(0.009215056f, 0.3612514f, 0.9323555f, -0.01121602f)}, + {"LeftHand", new Quaternion(-0.4797408f, 0.5195366f, -0.5279632f, -0.4703038f)}, + {"LeftForeArm", new Quaternion(-0.4594738f, 0.4594729f, -0.5374805f, -0.5374788f)}, + {"LeftLeg", new Quaternion(-0.0005380471f, -0.03154583f, 0.9994993f, 0.002378627f)}, + {"LeftShoulder", new Quaternion(-0.3840606f, 0.525857f, -0.5957767f, -0.47013f)}, + {"LeftToeBase", new Quaternion(-0.0002536641f, 0.7113448f, 0.7027079f, -0.01379319f)}, + {"LeftArm", new Quaternion(-0.4591927f, 0.4591916f, -0.5377204f, -0.5377193f)}, + {"LeftUpLeg", new Quaternion(-0.0006682819f, 0.0006864658f, 0.9999968f, -0.002333928f)}, + {"Neck", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)}, + {"RightHandIndex3", new Quaternion(0.5083892f, 0.4911618f, -0.4914584f, 0.5086939f)}, + {"RightHandIndex2", new Quaternion(0.4931984f, 0.5065879f, -0.5067145f, 0.4933202f)}, + {"RightHandIndex1", new Quaternion(0.4991491f, 0.5012957f, -0.5008481f, 0.4987026f)}, + {"RightHandPinky3", new Quaternion(0.4890696f, 0.5154139f, -0.5104482f, 0.4843578f)}, + {"RightHandPinky2", new Quaternion(0.5084175f, 0.495413f, -0.4915423f, 0.5044444f)}, + {"RightHandPinky1", new Quaternion(0.5069782f, 0.4965974f, -0.4930001f, 0.5033045f)}, + {"RightHandMiddle3", new Quaternion(0.4867662f, 0.5129694f, -0.5128888f, 0.4866894f)}, + {"RightHandMiddle2", new Quaternion(0.5167004f, 0.4833596f, -0.4827653f, 0.5160643f)}, + {"RightHandMiddle1", new Quaternion(0.4965845f, 0.5031784f, -0.5033959f, 0.4967981f)}, + {"RightHandRing3", new Quaternion(0.4933217f, 0.5102056f, -0.5064691f, 0.4897075f)}, + {"RightHandRing2", new Quaternion(0.5085972f, 0.494844f, -0.4913519f, 0.505007f)}, + {"RightHandRing1", new Quaternion(0.502959f, 0.4996676f, -0.4970418f, 0.5003144f)}, + {"RightHandThumb3", new Quaternion(0.7221864f, 0.3001843f, -0.4482129f, 0.4329457f)}, + {"RightHandThumb2", new Quaternion(0.755621f, 0.20102f, -0.386691f, 0.4889769f)}, + {"RightHandThumb1", new Quaternion(0.7277303f, 0.2876409f, -0.4398623f, 0.4406733f)}, + {"RightEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)}, + {"RightFoot", new Quaternion(-0.009482829f, 0.3612484f, 0.9323512f, 0.01144584f)}, + {"RightHand", new Quaternion(0.4797273f, 0.5195542f, -0.5279628f, 0.4702987f)}, + {"RightForeArm", new Quaternion(0.4594217f, 0.4594215f, -0.5375242f, 0.5375237f)}, + {"RightLeg", new Quaternion(0.0005446263f, -0.03177159f, 0.9994922f, -0.002395923f)}, + {"RightShoulder", new Quaternion(0.3841222f, 0.5257177f, -0.5957286f, 0.4702966f)}, + {"RightToeBase", new Quaternion(0.0001034f, 0.7113398f, 0.7027067f, 0.01411251f)}, + {"RightArm", new Quaternion(0.4591419f, 0.4591423f, -0.537763f, 0.5377624f)}, + {"RightUpLeg", new Quaternion(0.0006750703f, 0.0008973633f, 0.9999966f, 0.002352045f)}, + {"Spine", new Quaternion(-0.05427956f, 1.508558e-7f, -2.775203e-6f, 0.9985258f)}, + {"Spine1", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)}, + {"Spine2", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)}, + }; + + static Dictionary userBoneToHumanoidMappings = new Dictionary(); + static Dictionary userParentNames = new Dictionary(); + static Dictionary userAbsoluteRotations = new Dictionary(); + + static string assetPath = ""; + static string assetName = ""; + static HumanDescription humanDescription; [MenuItem("High Fidelity/Export New Avatar")] - public static void ExportNewAvatar() { + static void ExportNewAvatar() { ExportSelectedAvatar(false); } - - [MenuItem("High Fidelity/Export New Avatar", true)] - private static bool ExportNewAvatarValidator() { - // only enable Export New Avatar option if we have an asset selected - string[] guids = Selection.assetGUIDs; - return guids.Length > 0; - } - - [MenuItem("High Fidelity/Update Avatar")] - public static void UpdateAvatar() { + + [MenuItem("High Fidelity/Update Existing Avatar")] + static void UpdateAvatar() { ExportSelectedAvatar(true); } - [MenuItem("High Fidelity/Update Avatar", true)] - private static bool UpdateAvatarValidation() { - // only enable Update Avatar option if the selected avatar is the last one that was exported - if (exportedPath != String.Empty) { - string[] guids = Selection.assetGUIDs; - if (guids.Length > 0) { - string selectedAssetPath = AssetDatabase.GUIDToAssetPath(guids[0]); - string selectedAsset = Path.GetFileNameWithoutExtension(selectedAssetPath); - string exportedAsset = Path.GetFileNameWithoutExtension(exportedPath); - return exportedAsset == selectedAsset; + static void ExportSelectedAvatar(bool updateAvatar) { + string[] guids = Selection.assetGUIDs; + if (guids.Length != 1) { + if (guids.Length == 0) { + EditorUtility.DisplayDialog("Error", "Please select an asset to export.", "Ok"); + } else { + EditorUtility.DisplayDialog("Error", "Please select a single asset to export.", "Ok"); } - } - return false; - } - - public static void ExportSelectedAvatar(bool usePreviousPath) { - string assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); - if (assetPath.LastIndexOf(".fbx") == -1) { - EditorUtility.DisplayDialog("Error", "Please select an fbx avatar to export", "Ok"); return; } - ModelImporter importer = ModelImporter.GetAtPath(assetPath) as ModelImporter; - if (importer == null) { - EditorUtility.DisplayDialog("Error", "Please select a model", "Ok"); + assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); + assetName = Path.GetFileNameWithoutExtension(assetPath); + ModelImporter modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; + if (Path.GetExtension(assetPath).ToLower() != ".fbx" || modelImporter == null) { + EditorUtility.DisplayDialog("Error", "Please select an .fbx model asset to export.", "Ok"); return; } - if (importer.animationType != ModelImporterAnimationType.Human) { - EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid", "Ok"); + if (modelImporter.animationType != ModelImporterAnimationType.Human) { + EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid in the Rig section of it's Inspector window.", "Ok"); return; } - - // store joint mappings only for joints that exist in hifi and verify missing joints - HumanDescription humanDescription = importer.humanDescription; - HumanBone[] boneMap = humanDescription.human; - Dictionary jointMappings = new Dictionary(); - foreach (HumanBone bone in boneMap) { - string humanBone = bone.humanName; - string hifiJointName; - if (UNITY_TO_HIFI_JOINT_NAME.TryGetValue(humanBone, out hifiJointName)) { - jointMappings.Add(hifiJointName, bone.boneName); - } - } - if (!jointMappings.ContainsKey("Hips")) { - EditorUtility.DisplayDialog("Error", "There is no Hips bone in selected avatar", "Ok"); + + humanDescription = modelImporter.humanDescription; + if (!SetJointMappingsAndParentNames()) { return; } - if (!jointMappings.ContainsKey("Spine")) { - EditorUtility.DisplayDialog("Error", "There is no Spine bone in selected avatar", "Ok"); - return; - } - if (!jointMappings.ContainsKey("Spine1")) { - EditorUtility.DisplayDialog("Error", "There is no Chest bone in selected avatar", "Ok"); - return; - } - if (!jointMappings.ContainsKey("Spine2")) { - // if there is no UpperChest (Spine2) bone then we remap Chest (Spine1) to Spine2 in hifi and skip Spine1 - jointMappings["Spine2"] = jointMappings["Spine1"]; - jointMappings.Remove("Spine1"); - } - - // open folder explorer defaulting to user documents folder to select target path if exporting new avatar, - // otherwise use previously exported path if updating avatar - string directoryPath; - string assetName = Path.GetFileNameWithoutExtension(assetPath); - if (!usePreviousPath || exportedPath == String.Empty) { - string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); - if (!SelectExportFolder(assetName, documentsFolder, out directoryPath)) { - return; - } - } else { - directoryPath = Path.GetDirectoryName(exportedPath) + "/"; - } - Directory.CreateDirectory(directoryPath); - - // delete any existing fst since we agreed to overwrite it - string fstPath = directoryPath + assetName + ".fst"; - if (File.Exists(fstPath)) { - File.Delete(fstPath); - } - - // write out core fields to top of fst file - File.WriteAllText(fstPath, "name = " + assetName + "\ntype = body+head\nscale = 1\nfilename = " + - assetName + ".fbx\n" + "texdir = textures\n"); - - // write out joint mappings to fst file - foreach (var jointMapping in jointMappings) { - File.AppendAllText(fstPath, "jointMap = " + jointMapping.Key + " = " + jointMapping.Value + "\n"); - } - // delete any existing fbx since we agreed to overwrite it, and copy fbx over - string targetAssetPath = directoryPath + assetName + ".fbx"; - if (File.Exists(targetAssetPath)) { - File.Delete(targetAssetPath); + 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; + 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 + File.Copy(exportModelPath, assetPath, true); + 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(); + humanDescription = modelImporter.humanDescription; + + // redo joint mappings and parent names due to the fbx change + SetJointMappingsAndParentNames(); + } + } + } 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 + WriteFST(exportFstPath, projectName); + } 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); + } + + // open a popup window to enter new export project name and project location + ExportProjectWindow window = ScriptableObject.CreateInstance(); + window.Init(hifiFolder, OnExportProjectWindowClose); } - File.Copy(assetPath, targetAssetPath); - - exportedPath = targetAssetPath; } - public static bool SelectExportFolder(string assetName, string initialPath, out string directoryPath) { - string selectedPath = EditorUtility.OpenFolderPanel("Select export location", initialPath, ""); - if (selectedPath.Length == 0) { // folder selection cancelled - directoryPath = ""; + static void OnExportProjectWindowClose(string projectDirectory, string projectName) { + // copy the fbx from the Unity Assets folder to the project directory + string exportModelPath = projectDirectory + assetName + ".fbx"; + File.Copy(assetPath, exportModelPath); + + // create empty Textures and Scripts folders in the project directory + string texturesDirectory = projectDirectory + "\\textures"; + string scriptsDirectory = projectDirectory + "\\scripts"; + Directory.CreateDirectory(texturesDirectory); + Directory.CreateDirectory(scriptsDirectory); + + // write out the avatar.fst file to the project directory + string exportFstPath = projectDirectory + "avatar.fst"; + WriteFST(exportFstPath, projectName); + + // remove any double slashes in texture directory path and warn user to copy external textures over + texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); + EditorUtility.DisplayDialog("Warning", "If you are using any external textures with your model, " + + "please copy those textures to " + texturesDirectory, "Ok"); + } + + static bool SetJointMappingsAndParentNames() { + userParentNames.Clear(); + userBoneToHumanoidMappings.Clear(); + + // instantiate a game object of the user avatar to save out bone parents then destroy it + UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); + GameObject assetGameObject = (GameObject)Instantiate(avatarResource); + SetParentNames(assetGameObject.transform, userParentNames); + DestroyImmediate(assetGameObject); + + // store joint mappings only for joints that exist in hifi and verify missing required joints + HumanBone[] boneMap = humanDescription.human; + string chestUserBone = ""; + string neckUserBone = ""; + foreach (HumanBone bone in boneMap) { + string humanName = bone.humanName; + string boneName = bone.boneName; + string hifiJointName; + if (HUMANOID_TO_HIFI_JOINT_NAME.TryGetValue(humanName, out hifiJointName)) { + userBoneToHumanoidMappings.Add(boneName, humanName); + if (humanName == "Chest") { + chestUserBone = boneName; + } else if (humanName == "Neck") { + neckUserBone = boneName; + } + } + + } + if (!userBoneToHumanoidMappings.ContainsValue("Hips")) { + EditorUtility.DisplayDialog("Error", "There is no Hips bone in selected avatar", "Ok"); return false; } - directoryPath = selectedPath + "/" + assetName + "/"; - if (Directory.Exists(directoryPath)) { - bool overwrite = EditorUtility.DisplayDialog("Error", "Directory " + assetName + - " already exists here, would you like to overwrite it?", "Yes", "No"); - if (!overwrite) { - SelectExportFolder(assetName, selectedPath, out directoryPath); + if (!userBoneToHumanoidMappings.ContainsValue("Spine")) { + EditorUtility.DisplayDialog("Error", "There is no Spine bone in selected avatar", "Ok"); + return false; + } + if (!userBoneToHumanoidMappings.ContainsValue("Chest")) { + // check to see if there is a child of Spine that could be mapped to Chest + string spineChild = ""; + foreach (var parentRelation in userParentNames) { + string humanName; + if (userBoneToHumanoidMappings.TryGetValue(parentRelation.Value, out humanName) && humanName == "Spine") { + if (spineChild == "") { + spineChild = parentRelation.Key; + } else { + // found more than one Spine child so we can't choose one to remap + spineChild = ""; + break; + } + } + } + if (spineChild != "" && !userBoneToHumanoidMappings.ContainsKey(spineChild)) { + // use child of Spine as Chest + userBoneToHumanoidMappings.Add(spineChild, "Chest"); + chestUserBone = spineChild; + } else { + EditorUtility.DisplayDialog("Error", "There is no Chest bone in selected avatar", "Ok"); + return false; } } + if (!userBoneToHumanoidMappings.ContainsValue("UpperChest")) { + //if parent of Neck is not Chest then map the parent to UpperChest + if (neckUserBone != "") { + string neckParentUserBone, neckParentHuman; + userParentNames.TryGetValue(neckUserBone, out neckParentUserBone); + userBoneToHumanoidMappings.TryGetValue(neckParentUserBone, out neckParentHuman); + if (neckParentHuman != "Chest" && !userBoneToHumanoidMappings.ContainsKey(neckParentUserBone)) { + userBoneToHumanoidMappings.Add(neckParentUserBone, "UpperChest"); + } + } + // if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest + if (!userBoneToHumanoidMappings.ContainsValue("UpperChest") && chestUserBone != "") { + userBoneToHumanoidMappings[chestUserBone] = "UpperChest"; + } + } + return true; } + + static void WriteFST(string exportFstPath, string projectName) { + userAbsoluteRotations.Clear(); + + // 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"); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + + ". Please check the location and try again.", "Ok"); + return; + } + + // write out joint mappings to fst file + foreach (var jointMapping in userBoneToHumanoidMappings) { + string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[jointMapping.Value]; + File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + jointMapping.Key + "\n"); + } + + // calculate and write out joint rotation offsets to fst file + SkeletonBone[] skeletonMap = humanDescription.skeleton; + foreach (SkeletonBone userBone in skeletonMap) { + string userBoneName = userBone.name; + Quaternion userBoneRotation = userBone.rotation; + + string parentName; + userParentNames.TryGetValue(userBoneName, out parentName); + if (parentName == "root") { + // if the parent is root then use bone's rotation + userAbsoluteRotations.Add(userBoneName, userBoneRotation); + } else { + // otherwise multiply bone's rotation by parent bone's absolute rotation + userAbsoluteRotations.Add(userBoneName, userAbsoluteRotations[parentName] * userBoneRotation); + } + + // generate joint rotation offsets for both humanoid-mapped bones as well as extra unmapped bones in user avatar + Quaternion jointOffset = new Quaternion(); + string humanName, outputJointName = ""; + if (userBoneToHumanoidMappings.TryGetValue(userBoneName, out humanName)) { + outputJointName = HUMANOID_TO_HIFI_JOINT_NAME[humanName]; + Quaternion rotation = referenceAbsoluteRotations[outputJointName]; + jointOffset = Quaternion.Inverse(userAbsoluteRotations[userBoneName]) * rotation; + } else if (userAbsoluteRotations.ContainsKey(userBoneName)) { + outputJointName = userBoneName; + string lastRequiredParent = FindLastRequiredParentBone(userBoneName); + if (lastRequiredParent == "root") { + jointOffset = Quaternion.Inverse(userAbsoluteRotations[userBoneName]); + } else { + // take the previous offset and multiply it by the current local when we have an extra joint + string lastRequiredParentHifiName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneToHumanoidMappings[lastRequiredParent]]; + Quaternion lastRequiredParentRotation = referenceAbsoluteRotations[lastRequiredParentHifiName]; + jointOffset = Quaternion.Inverse(userAbsoluteRotations[userBoneName]) * lastRequiredParentRotation; + } + } + + // swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst + if (outputJointName != "") { + jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w); + File.AppendAllText(exportFstPath, "jointRotationOffset = " + outputJointName + " = (" + jointOffset.x + ", " + + jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n"); + } + } + + // open File Explorer to the project directory once finished + System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); + } + + static void SetParentNames(Transform modelBone, Dictionary parentNames) { + for (int i = 0; i < modelBone.childCount; i++) { + SetParentNames(modelBone.GetChild(i), parentNames); + } + if (modelBone.parent != null) { + parentNames.Add(modelBone.name, modelBone.parent.name); + } else { + parentNames.Add(modelBone.name, "root"); + } + } + + static string FindLastRequiredParentBone(string currentBone) { + string result = currentBone; + while (result != "root" && !userBoneToHumanoidMappings.ContainsKey(result)) { + result = userParentNames[result]; + } + return result; + } +} + +class ExportProjectWindow : EditorWindow { + const int MIN_WIDTH = 450; + const int MIN_HEIGHT = 250; + const int BUTTON_FONT_SIZE = 16; + const int LABEL_FONT_SIZE = 16; + const int TEXT_FIELD_FONT_SIZE = 14; + const int ERROR_FONT_SIZE = 12; + + string projectName = ""; + string projectLocation = ""; + string projectDirectory = ""; + string errorLabel = "\n"; + + public delegate void OnCloseDelegate(string projectDirectory, string projectName); + OnCloseDelegate onCloseCallback; + + public void Init(string initialPath, OnCloseDelegate closeCallback) { + minSize = new Vector2(MIN_WIDTH, MIN_HEIGHT); + titleContent.text = "Export New Avatar"; + projectLocation = initialPath; + onCloseCallback = closeCallback; + ShowUtility(); + } + + void OnGUI() { + // define UI styles for all GUI elements to be created + GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); + buttonStyle.fontSize = BUTTON_FONT_SIZE; + GUIStyle labelStyle = new GUIStyle(GUI.skin.label); + labelStyle.fontSize = LABEL_FONT_SIZE; + GUIStyle textStyle = new GUIStyle(GUI.skin.textField); + textStyle.fontSize = TEXT_FIELD_FONT_SIZE; + GUIStyle errorStyle = new GUIStyle(GUI.skin.label); + errorStyle.fontSize = ERROR_FONT_SIZE; + errorStyle.normal.textColor = Color.red; + + GUILayout.Space(10); + + // 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); + + // Browse button to open folder explorer that starts at project location path and then updates project location + if (GUILayout.Button("Browse", buttonStyle)) { + string result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); + if (result.Length > 0) { // folder selection not cancelled + projectLocation = result.Replace('/', '\\'); + } + } + + // Red error label text to display any issues under text fields and Browse button + GUILayout.Label(errorLabel, errorStyle); + + GUILayout.Space(20); + + // Export button which will verify project folder can actually be created + // 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); + } + } + + // Cancel button just closes the popup window without callback + 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 + if (GUI.changed && !export) { + CheckForErrors(false); + } + } + + bool CheckForErrors(bool exporting) { + errorLabel = "\n"; // default to no error + 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)) { + errorLabel = "A folder with the name " + projectName + + " already exists at that location.\nPlease choose a different project name or location."; + 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] != ':') { + errorLabel = "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) { + errorLabel = "Please define a project name.\n"; + return true; + } else if (projectLocation.Length == 0) { + errorLabel = "Please define a project location.\n"; + return true; + } else { + try { + Directory.CreateDirectory(projectDirectory); + } catch { + errorLabel = "Project location is invalid. Please choose a different project location.\n"; + return true; + } + } + } + return false; + } } diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt new file mode 100644 index 0000000000..034ec23982 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -0,0 +1,15 @@ +To create a new avatar project: +1. Import your .fbx avatar model into Unity Assets (drag and drop file into Assets window or use Assets menu > Import New Assets). +2. Select the .fbx avatar that you imported 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, select 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. + +To update an existing avatar project: +1. Select the existing .fbx avatar in the Assets window that you would like to re-export. +2. Select High Fidelity menu > Update Existing Avatar and choose the .fst file you would like to update. +3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file. +4. Once it is updated, 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. diff --git a/tools/unity-avatar-exporter/Assets/README.txt.meta b/tools/unity-avatar-exporter/Assets/README.txt.meta new file mode 100644 index 0000000000..d8bc5b9b66 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/README.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 71e72751b2810fc4993ff53291c430b6 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index bb25cb4072..f333aecb12 100644 Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ diff --git a/tools/unity-avatar-exporter/packager.bat b/tools/unity-avatar-exporter/packager.bat index 99932f1ead..55b59a9db6 100644 --- a/tools/unity-avatar-exporter/packager.bat +++ b/tools/unity-avatar-exporter/packager.bat @@ -1 +1 @@ -"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets/Editor" "avatarExporter.unitypackage" +"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets" "avatarExporter.unitypackage"