Merge branch 'master' of github.com:highfidelity/hifi into 20466-addPlatormInfo

This commit is contained in:
NissimHadar 2018-12-30 20:24:16 -08:00
commit 4b4ef4412a
17 changed files with 815 additions and 248 deletions

1
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,9 +14,9 @@
<@include DefaultMaterials.slh@>
<@include ForwardGlobalLight.slh@>
<@include gpu/Transform.slh@>
<$declareEvalSkyboxGlobalColor()$>
<@include gpu/Transform.slh@>
<$declareStandardCameraTransform()$>
// the interpolated normal

View file

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

View file

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

View file

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

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

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 71e72751b2810fc4993ff53291c430b6
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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