mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-04-14 13:36:02 +02:00
Merge branch 'master' of github.com:highfidelity/hifi into 20466-addPlatormInfo
This commit is contained in:
commit
4b4ef4412a
17 changed files with 815 additions and 248 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -6692,6 +6692,7 @@ void Application::resetSensors(bool andReload) {
|
|||
DependencyManager::get<DdeFaceTracker>()->reset();
|
||||
DependencyManager::get<EyeTracker>()->reset();
|
||||
_overlayConductor.centerUI();
|
||||
getActiveDisplayPlugin()->resetSensors();
|
||||
getMyAvatar()->reset(true, andReload);
|
||||
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "reset", Qt::QueuedConnection);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<bool>([&] {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<gpu::Buffer>(sizeof(StandardInputs), nullptr);
|
||||
}
|
||||
|
||||
void Procedural::setProceduralData(const ProceduralData& proceduralData) {
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef hifi_RenderableProcedrualItem_h
|
||||
#define hifi_RenderableProcedrualItem_h
|
||||
|
||||
#include <atomic>
|
||||
|
||||
|
@ -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<NUM_FLAGS> 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<ProceduralProgramKey> {
|
||||
size_t operator()(const ProceduralProgramKey& key) const {
|
||||
return std::hash<std::bitset<ProceduralProgramKey::FlagBit::NUM_FLAGS>>()(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<ProceduralProgramKey, gpu::PipelinePointer> _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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
<@include DefaultMaterials.slh@>
|
||||
|
||||
<@include ForwardGlobalLight.slh@>
|
||||
<@include gpu/Transform.slh@>
|
||||
|
||||
<$declareEvalSkyboxGlobalColor()$>
|
||||
|
||||
<@include gpu/Transform.slh@>
|
||||
<$declareStandardCameraTransform()$>
|
||||
|
||||
// the interpolated normal
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ using System;
|
|||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class AvatarExporter : MonoBehaviour {
|
||||
public static Dictionary<string, string> UNITY_TO_HIFI_JOINT_NAME = new Dictionary<string, string> {
|
||||
class AvatarExporter : MonoBehaviour {
|
||||
static readonly Dictionary<string, string> HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary<string, string> {
|
||||
{"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<string, Quaternion> referenceAbsoluteRotations = new Dictionary<string, Quaternion> {
|
||||
{"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<string, string> userBoneToHumanoidMappings = new Dictionary<string, string>();
|
||||
static Dictionary<string, string> userParentNames = new Dictionary<string, string>();
|
||||
static Dictionary<string, Quaternion> userAbsoluteRotations = new Dictionary<string, Quaternion>();
|
||||
|
||||
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<string, string> jointMappings = new Dictionary<string, string>();
|
||||
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<ExportProjectWindow>();
|
||||
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<string, string> 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;
|
||||
}
|
||||
}
|
||||
|
|
15
tools/unity-avatar-exporter/Assets/README.txt
Normal file
15
tools/unity-avatar-exporter/Assets/README.txt
Normal file
|
@ -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.
|
7
tools/unity-avatar-exporter/Assets/README.txt.meta
Normal file
7
tools/unity-avatar-exporter/Assets/README.txt.meta
Normal file
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 71e72751b2810fc4993ff53291c430b6
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Binary file not shown.
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue