avatar exporter 0.3.4/0.3.5 changes to master

This commit is contained in:
David Back 2019-03-14 15:03:33 -07:00
parent 58146063db
commit f9f2b6f8ac
9 changed files with 2296 additions and 257 deletions

View file

@ -6,15 +6,18 @@
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using System;
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
class AvatarExporter : MonoBehaviour {
// update version number for every PR that changes this file, also set updated version in README file
static readonly string AVATAR_EXPORTER_VERSION = "0.3.3";
static readonly string AVATAR_EXPORTER_VERSION = "0.3.5";
static readonly float HIPS_GROUND_MIN_Y = 0.01f;
static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f;
@ -22,6 +25,9 @@ class AvatarExporter : MonoBehaviour {
static readonly string EMPTY_WARNING_TEXT = "None";
static readonly string TEXTURES_DIRECTORY = "textures";
static readonly string DEFAULT_MATERIAL_NAME = "No Name";
static readonly string HEIGHT_REFERENCE_PREFAB = "Assets/Editor/AvatarExporter/HeightReference.prefab";
static readonly Vector3 PREVIEW_CAMERA_PIVOT = new Vector3(0.0f, 1.755f, 0.0f);
static readonly Vector3 PREVIEW_CAMERA_DIRECTION = new Vector3(0.0f, 0.0f, -1.0f);
// TODO: use regex
static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] {
@ -300,7 +306,7 @@ class AvatarExporter : MonoBehaviour {
}
json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "]";
if (!string.IsNullOrEmpty(emissiveMap)) {
json += "\", emissiveMap\": \"" + emissiveMap + "\"";
json += ", \"emissiveMap\": \"" + emissiveMap + "\"";
}
json += " } }";
return json;
@ -309,7 +315,6 @@ class AvatarExporter : MonoBehaviour {
static string assetPath = "";
static string assetName = "";
static ModelImporter modelImporter;
static HumanDescription humanDescription;
@ -317,12 +322,23 @@ class AvatarExporter : MonoBehaviour {
static Dictionary<string, string> humanoidToUserBoneMappings = new Dictionary<string, string>();
static BoneTreeNode userBoneTree = new BoneTreeNode();
static Dictionary<AvatarRule, string> failedAvatarRules = new Dictionary<AvatarRule, string>();
static string warnings = "";
static Dictionary<string, string> textureDependencies = new Dictionary<string, string>();
static Dictionary<string, string> materialMappings = new Dictionary<string, string>();
static Dictionary<string, MaterialData> materialDatas = new Dictionary<string, MaterialData>();
static List<string> materialAlternateStandardShader = new List<string>();
static Dictionary<string, string> materialUnsupportedShader = new Dictionary<string, string>();
static List<string> alternateStandardShaderMaterials = new List<string>();
static List<string> unsupportedShaderMaterials = new List<string>();
static Scene previewScene;
static string previousScene = "";
static Vector3 previousScenePivot = Vector3.zero;
static Quaternion previousSceneRotation = Quaternion.identity;
static float previousSceneSize = 0.0f;
static bool previousSceneOrthographic = false;
static UnityEngine.Object avatarResource;
static GameObject avatarPreviewObject;
static GameObject heightReferenceObject;
[MenuItem("High Fidelity/Export New Avatar")]
static void ExportNewAvatar() {
@ -339,8 +355,8 @@ class AvatarExporter : MonoBehaviour {
EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok");
}
static void ExportSelectedAvatar(bool updateAvatar) {
// ensure everything is saved to file before exporting
static void ExportSelectedAvatar(bool updateExistingAvatar) {
// ensure everything is saved to file before doing anything
AssetDatabase.SaveAssets();
string[] guids = Selection.assetGUIDs;
@ -365,6 +381,11 @@ class AvatarExporter : MonoBehaviour {
return;
}
avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object));
humanDescription = modelImporter.humanDescription;
string textureWarnings = SetTextureDependencies();
// if the rig is optimized we should de-optimize it during the export process
bool shouldDeoptimizeGameObjects = modelImporter.optimizeGameObjects;
if (shouldDeoptimizeGameObjects) {
@ -372,27 +393,22 @@ class AvatarExporter : MonoBehaviour {
modelImporter.SaveAndReimport();
}
humanDescription = modelImporter.humanDescription;
string textureWarnings = SetTextureDependencies();
SetBoneAndMaterialInformation();
if (shouldDeoptimizeGameObjects) {
// switch back to optimized game object in case it was originally optimized
modelImporter.optimizeGameObjects = true;
modelImporter.SaveAndReimport();
}
// check if we should be substituting a bone for a missing UpperChest mapping
AdjustUpperChestMapping();
// format resulting avatar rule failure strings
// consider export-blocking avatar rules to be errors and show them in an error dialog,
// and also include any other avatar rule failures plus texture warnings as warnings in the dialog
if (shouldDeoptimizeGameObjects) {
// switch back to optimized game object in case it was originally optimized
modelImporter.optimizeGameObjects = true;
modelImporter.SaveAndReimport();
}
// format resulting bone rule failure strings
// consider export-blocking bone rules to be errors and show them in an error dialog,
// and also include any other bone rule failures plus texture warnings as warnings in the dialog
string boneErrors = "";
string warnings = "";
warnings = "";
foreach (var failedAvatarRule in failedAvatarRules) {
if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0) {
boneErrors += failedAvatarRule.Value + "\n\n";
@ -400,15 +416,16 @@ class AvatarExporter : MonoBehaviour {
warnings += failedAvatarRule.Value + "\n\n";
}
}
foreach (string materialName in materialAlternateStandardShader) {
warnings += "The material " + materialName + " is not using the recommended variation of the Standard shader. " +
"We recommend you change it to Standard (Roughness setup) shader for improved performance.\n\n";
}
foreach (var material in materialUnsupportedShader) {
warnings += "The material " + material.Key + " is using an unsupported shader " + material.Value +
". Please change it to a Standard shader type.\n\n";
}
// add material and texture warnings after bone-related warnings
AddMaterialWarnings();
warnings += textureWarnings;
// remove trailing newlines at the end of the warnings
if (!string.IsNullOrEmpty(warnings)) {
warnings = warnings.Substring(0, warnings.LastIndexOf("\n\n"));
}
if (!string.IsNullOrEmpty(boneErrors)) {
// if there are both errors and warnings then warnings will be displayed with errors in the error dialog
if (!string.IsNullOrEmpty(warnings)) {
@ -421,21 +438,42 @@ class AvatarExporter : MonoBehaviour {
return;
}
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
// since there are no errors we can now open the preview scene in place of the user's scene
if (!OpenPreviewScene()) {
return;
}
exportFstPath = exportFstPath.Replace('/', '\\');
// show None instead of blank warnings if there are no warnings in the export windows
if (string.IsNullOrEmpty(warnings)) {
warnings = EMPTY_WARNING_TEXT;
}
string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
string hifiFolder = documentsFolder + "\\High Fidelity Projects";
if (updateExistingAvatar) { // Update Existing Avatar menu option
// open update existing project popup window including project to update, scale, and warnings
// default the initial file chooser location to HiFi projects folder in user documents folder
ExportProjectWindow window = ScriptableObject.CreateInstance<ExportProjectWindow>();
string initialPath = Directory.Exists(hifiFolder) ? hifiFolder : documentsFolder;
window.Init(initialPath, warnings, updateExistingAvatar, avatarPreviewObject, OnUpdateExistingProject, OnExportWindowClose);
} else { // Export New Avatar menu option
// create High Fidelity Projects folder in user documents folder if it doesn't exist
if (!Directory.Exists(hifiFolder)) {
Directory.CreateDirectory(hifiFolder);
}
// open export new project popup window including project name, project location, scale, and warnings
// default the initial project location path to the High Fidelity Projects folder above
ExportProjectWindow window = ScriptableObject.CreateInstance<ExportProjectWindow>();
window.Init(hifiFolder, warnings, updateExistingAvatar, avatarPreviewObject, OnExportNewProject, OnExportWindowClose);
}
}
static void OnUpdateExistingProject(string exportFstPath, string projectName, float scale) {
bool copyModelToExport = false;
// lookup the project name field from the fst file to update
string projectName = "";
projectName = "";
try {
string[] lines = File.ReadAllLines(exportFstPath);
foreach (string line in lines) {
@ -532,7 +570,7 @@ class AvatarExporter : MonoBehaviour {
}
// write out a new fst file in place of the old file
if (!WriteFST(exportFstPath, projectName)) {
if (!WriteFST(exportFstPath, projectName, scale)) {
return;
}
@ -548,23 +586,9 @@ class AvatarExporter : MonoBehaviour {
successDialog += "\n\nWarnings:\n" + warnings;
}
EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
} else { // Export New Avatar menu option
// create High Fidelity Projects folder in user documents folder if it doesn't exist
if (!Directory.Exists(hifiFolder)) {
Directory.CreateDirectory(hifiFolder);
}
if (string.IsNullOrEmpty(warnings)) {
warnings = EMPTY_WARNING_TEXT;
}
// open a popup window to enter new export project name and project location
ExportProjectWindow window = ScriptableObject.CreateInstance<ExportProjectWindow>();
window.Init(hifiFolder, warnings, OnExportProjectWindowClose);
}
}
static void OnExportProjectWindowClose(string projectDirectory, string projectName, string warnings) {
static void OnExportNewProject(string projectDirectory, string projectName, float scale) {
// copy the fbx from the Unity Assets folder to the project directory
string exportModelPath = projectDirectory + assetName + ".fbx";
File.Copy(assetPath, exportModelPath);
@ -577,7 +601,7 @@ class AvatarExporter : MonoBehaviour {
// write out the avatar.fst file to the project directory
string exportFstPath = projectDirectory + "avatar.fst";
if (!WriteFST(exportFstPath, projectName)) {
if (!WriteFST(exportFstPath, projectName, scale)) {
return;
}
@ -592,16 +616,27 @@ class AvatarExporter : MonoBehaviour {
if (warnings != EMPTY_WARNING_TEXT) {
successDialog += "Warnings:\n" + warnings;
}
successDialog += "Note: If you are using any external textures with your model, " +
successDialog += "\n\nNote: If you are using any external textures with your model, " +
"please ensure those textures are copied to " + texturesDirectory;
EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
}
static bool WriteFST(string exportFstPath, string projectName) {
static void OnExportWindowClose() {
// close the preview avatar scene and go back to user's previous scene when export project windows close
ClosePreviewScene();
}
// The High Fidelity FBX Serializer omits the colon based prefixes. This will make the jointnames compatible.
static string removeTypeFromJointname(string jointName) {
return jointName.Substring(jointName.IndexOf(':') + 1);
}
static bool WriteFST(string exportFstPath, string projectName, float scale) {
// write out core fields to top of fst file
try {
File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " +
assetName + ".fbx\n" + "texdir = textures\n");
File.WriteAllText(exportFstPath, "exporterVersion = " + AVATAR_EXPORTER_VERSION + "\nname = " + projectName +
"\ntype = body+head\nscale = " + scale + "\nfilename = " + assetName +
".fbx\n" + "texdir = textures\n");
} catch {
EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath +
". Please check the location and try again.", "Ok");
@ -612,7 +647,7 @@ class AvatarExporter : MonoBehaviour {
foreach (var userBoneInfo in userBoneInfos) {
if (userBoneInfo.Value.HasHumanMapping()) {
string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.Value.humanName];
File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + userBoneInfo.Key + "\n");
File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + removeTypeFromJointname(userBoneInfo.Key) + "\n");
}
}
@ -653,7 +688,7 @@ class AvatarExporter : MonoBehaviour {
// swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst
jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + userBoneName + " = (" + jointOffset.x + ", " +
File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + removeTypeFromJointname(userBoneName) + " = (" + jointOffset.x + ", " +
jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n");
}
@ -690,14 +725,13 @@ class AvatarExporter : MonoBehaviour {
userBoneTree = new BoneTreeNode();
materialDatas.Clear();
materialAlternateStandardShader.Clear();
materialUnsupportedShader.Clear();
alternateStandardShaderMaterials.Clear();
unsupportedShaderMaterials.Clear();
SetMaterialMappings();
// instantiate a game object of the user avatar to traverse the bone tree to gather
// bone parents and positions as well as build a bone tree, then destroy it
UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object));
GameObject assetGameObject = (GameObject)Instantiate(avatarResource);
TraverseUserBoneTree(assetGameObject.transform);
DestroyImmediate(assetGameObject);
@ -732,8 +766,8 @@ class AvatarExporter : MonoBehaviour {
bool light = gameObject.GetComponent<Light>() != null;
bool camera = gameObject.GetComponent<Camera>() != null;
// if this is a mesh and the model is using external materials then store its material data to be exported
if (mesh && modelImporter.materialLocation == ModelImporterMaterialLocation.External) {
// if this is a mesh then store its material data to be exported if the material is mapped to an fbx material name
if (mesh) {
Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials;
StoreMaterialData(materials);
} else if (!light && !camera) {
@ -959,7 +993,8 @@ class AvatarExporter : MonoBehaviour {
string userBoneName = "";
// avatar rule fails if bone is not mapped in Humanoid
if (!humanoidToUserBoneMappings.TryGetValue(humanBoneName, out userBoneName)) {
failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + " bone mapped in Humanoid for the selected avatar.");
failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName +
" bone mapped in Humanoid for the selected avatar.");
}
return userBoneName;
}
@ -1072,8 +1107,8 @@ class AvatarExporter : MonoBehaviour {
// don't store any material data for unsupported shader types
if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) {
if (!materialUnsupportedShader.ContainsKey(materialName)) {
materialUnsupportedShader.Add(materialName, shaderName);
if (!unsupportedShaderMaterials.Contains(materialName)) {
unsupportedShaderMaterials.Add(materialName);
}
continue;
}
@ -1100,18 +1135,19 @@ class AvatarExporter : MonoBehaviour {
// for non-roughness Standard shaders give a warning that is not the recommended Standard shader,
// and invert smoothness for roughness
if (shaderName == STANDARD_SHADER || shaderName == STANDARD_SPECULAR_SHADER) {
if (!materialAlternateStandardShader.Contains(materialName)) {
materialAlternateStandardShader.Add(materialName);
if (!alternateStandardShaderMaterials.Contains(materialName)) {
alternateStandardShaderMaterials.Add(materialName);
}
materialData.roughness = 1.0f - materialData.roughness;
}
// remap the material name from the Unity material name to the fbx material name that it overrides
if (materialMappings.ContainsKey(materialName)) {
materialName = materialMappings[materialName];
// store the material data under each fbx material name that it overrides from the material mapping
foreach (var materialMapping in materialMappings) {
string fbxMaterialName = materialMapping.Key;
string unityMaterialName = materialMapping.Value;
if (unityMaterialName == materialName && !materialDatas.ContainsKey(fbxMaterialName)) {
materialDatas.Add(fbxMaterialName, materialData);
}
if (!materialDatas.ContainsKey(materialName)) {
materialDatas.Add(materialName, materialData);
}
}
}
@ -1136,20 +1172,110 @@ class AvatarExporter : MonoBehaviour {
static void SetMaterialMappings() {
materialMappings.Clear();
// store the mappings from fbx material name to the Unity material name overriding it using external fbx mapping
// store the mappings from fbx material name to the Unity Material name that overrides it using external fbx mapping
var objectMap = modelImporter.GetExternalObjectMap();
foreach (var mapping in objectMap) {
var material = mapping.Value as UnityEngine.Material;
if (material != null) {
materialMappings.Add(material.name, mapping.Key.name);
materialMappings.Add(mapping.Key.name, material.name);
}
}
}
static void AddMaterialWarnings() {
string alternateStandardShaders = "";
string unsupportedShaders = "";
// combine all material names for each material warning into a comma-separated string
foreach (string materialName in alternateStandardShaderMaterials) {
if (!string.IsNullOrEmpty(alternateStandardShaders)) {
alternateStandardShaders += ", ";
}
alternateStandardShaders += materialName;
}
foreach (string materialName in unsupportedShaderMaterials) {
if (!string.IsNullOrEmpty(unsupportedShaders)) {
unsupportedShaders += ", ";
}
unsupportedShaders += materialName;
}
if (alternateStandardShaderMaterials.Count > 1) {
warnings += "The materials " + alternateStandardShaders + " are not using the " +
"recommended variation of the Standard shader. We recommend you change " +
"them to Standard (Roughness setup) shader for improved performance.\n\n";
} else if (alternateStandardShaderMaterials.Count == 1) {
warnings += "The material " + alternateStandardShaders + " is not using the " +
"recommended variation of the Standard shader. We recommend you change " +
"it to Standard (Roughness setup) shader for improved performance.\n\n";
}
if (unsupportedShaderMaterials.Count > 1) {
warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " +
"Please change them to a Standard shader type.\n\n";
} else if (unsupportedShaderMaterials.Count == 1) {
warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " +
"Please change it to a Standard shader type.\n\n";
}
}
static bool OpenPreviewScene() {
// see if the user wants to save their current scene before opening preview avatar scene in place of user's scene
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
return false;
}
// store the user's current scene to re-open when done and open a new default scene in place of the user's scene
previousScene = EditorSceneManager.GetActiveScene().path;
previewScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
// instantiate a game object to preview the avatar and a game object for the height reference prefab at 0, 0, 0
UnityEngine.Object heightReferenceResource = AssetDatabase.LoadAssetAtPath(HEIGHT_REFERENCE_PREFAB, typeof(UnityEngine.Object));
avatarPreviewObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity);
heightReferenceObject = (GameObject)Instantiate(heightReferenceResource, Vector3.zero, Quaternion.identity);
// store the camera pivot and rotation from the user's last scene to be restored later
// replace the camera pivot and rotation to point at the preview avatar object in the -Z direction (facing front of it)
var sceneView = SceneView.lastActiveSceneView;
if (sceneView != null) {
previousScenePivot = sceneView.pivot;
previousSceneRotation = sceneView.rotation;
previousSceneSize = sceneView.size;
previousSceneOrthographic = sceneView.orthographic;
sceneView.pivot = PREVIEW_CAMERA_PIVOT;
sceneView.rotation = Quaternion.LookRotation(PREVIEW_CAMERA_DIRECTION);
sceneView.orthographic = true;
sceneView.size = 5.0f;
}
return true;
}
static void ClosePreviewScene() {
// destroy the avatar and height reference game objects closing the scene
DestroyImmediate(avatarPreviewObject);
DestroyImmediate(heightReferenceObject);
// re-open the scene the user had open before switching to the preview scene
if (!string.IsNullOrEmpty(previousScene)) {
EditorSceneManager.OpenScene(previousScene);
}
// close the preview scene and flag it to be removed
EditorSceneManager.CloseScene(previewScene, true);
// restore the camera pivot and rotation to the user's previous scene settings
var sceneView = SceneView.lastActiveSceneView;
if (sceneView != null) {
sceneView.pivot = previousScenePivot;
sceneView.rotation = previousSceneRotation;
sceneView.size = previousSceneSize;
sceneView.orthographic = previousSceneOrthographic;
}
}
}
class ExportProjectWindow : EditorWindow {
const int WINDOW_WIDTH = 500;
const int WINDOW_HEIGHT = 460;
const int EXPORT_NEW_WINDOW_HEIGHT = 520;
const int UPDATE_EXISTING_WINDOW_HEIGHT = 465;
const int BUTTON_FONT_SIZE = 16;
const int LABEL_FONT_SIZE = 16;
const int TEXT_FIELD_FONT_SIZE = 14;
@ -1157,25 +1283,59 @@ class ExportProjectWindow : EditorWindow {
const int ERROR_FONT_SIZE = 12;
const int WARNING_SCROLL_HEIGHT = 170;
const string EMPTY_ERROR_TEXT = "None\n";
const int SLIDER_WIDTH = 340;
const int SCALE_TEXT_WIDTH = 60;
const float MIN_SCALE_SLIDER = 0.0f;
const float MAX_SCALE_SLIDER = 2.0f;
const int SLIDER_SCALE_EXPONENT = 10;
const float ACTUAL_SCALE_OFFSET = 1.0f;
const float DEFAULT_AVATAR_HEIGHT = 1.755f;
const float MAXIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f;
const float MINIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f;
readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f);
readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f);
GameObject avatarPreviewObject;
bool updateExistingAvatar = false;
string projectName = "";
string projectLocation = "";
string initialProjectLocation = "";
string projectDirectory = "";
string errorText = EMPTY_ERROR_TEXT;
string warningText = "";
string warningText = "\n";
Vector2 warningScrollPosition = new Vector2(0, 0);
string scaleWarningText = "";
float sliderScale = 0.30103f;
public delegate void OnCloseDelegate(string projectDirectory, string projectName, string warnings);
public delegate void OnExportDelegate(string projectDirectory, string projectName, float scale);
OnExportDelegate onExportCallback;
public delegate void OnCloseDelegate();
OnCloseDelegate onCloseCallback;
public void Init(string initialPath, string warnings, OnCloseDelegate closeCallback) {
minSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT);
maxSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT);
titleContent.text = "Export New Avatar";
projectLocation = initialPath;
public void Init(string initialPath, string warnings, bool updateExisting, GameObject avatarObject,
OnExportDelegate exportCallback, OnCloseDelegate closeCallback) {
updateExistingAvatar = updateExisting;
float windowHeight = updateExistingAvatar ? UPDATE_EXISTING_WINDOW_HEIGHT : EXPORT_NEW_WINDOW_HEIGHT;
minSize = new Vector2(WINDOW_WIDTH, windowHeight);
maxSize = new Vector2(WINDOW_WIDTH, windowHeight);
avatarPreviewObject = avatarObject;
titleContent.text = updateExistingAvatar ? "Update Existing Avatar" : "Export New Avatar";
initialProjectLocation = initialPath;
projectLocation = updateExistingAvatar ? "" : initialProjectLocation;
warningText = warnings;
onExportCallback = exportCallback;
onCloseCallback = closeCallback;
ShowUtility();
// if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height
float height = GetAvatarHeight();
if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) {
float newScale = DEFAULT_AVATAR_HEIGHT / height;
SetAvatarScale(newScale);
scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range.";
}
}
void OnGUI() {
@ -1192,10 +1352,24 @@ class ExportProjectWindow : EditorWindow {
errorStyle.normal.textColor = Color.red;
errorStyle.wordWrap = true;
GUIStyle warningStyle = new GUIStyle(errorStyle);
warningStyle.normal.textColor = Color.yellow;
warningStyle.normal.textColor = COLOR_YELLOW;
GUIStyle sliderStyle = new GUIStyle(GUI.skin.horizontalSlider);
sliderStyle.fixedWidth = SLIDER_WIDTH;
GUIStyle sliderThumbStyle = new GUIStyle(GUI.skin.horizontalSliderThumb);
// set the background for the window to a darker gray
Texture2D backgroundTexture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
backgroundTexture.SetPixel(0, 0, COLOR_BACKGROUND);
backgroundTexture.Apply();
GUI.DrawTexture(new Rect(0, 0, maxSize.x, maxSize.y), backgroundTexture, ScaleMode.StretchToFill);
GUILayout.Space(10);
if (updateExistingAvatar) {
// Project file to update label and input text field
GUILayout.Label("Project file to update:", labelStyle);
projectLocation = GUILayout.TextField(projectLocation, textStyle);
} else {
// Project name label and input text field
GUILayout.Label("Export project name:", labelStyle);
projectName = GUILayout.TextField(projectName, textStyle);
@ -1205,22 +1379,55 @@ class ExportProjectWindow : EditorWindow {
// Project location label and input text field
GUILayout.Label("Export project location:", labelStyle);
projectLocation = GUILayout.TextField(projectLocation, textStyle);
}
// Browse button to open folder explorer that starts at project location path and then updates project location
// Browse button to open file/folder explorer and set project location
if (GUILayout.Button("Browse", buttonStyle)) {
string result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, "");
if (result.Length > 0) { // folder selection not cancelled
string result = "";
if (updateExistingAvatar) {
// open file explorer starting at hifi projects folder in user documents and select target fst to update
string initialPath = string.IsNullOrEmpty(projectLocation) ? initialProjectLocation : projectLocation;
result = EditorUtility.OpenFilePanel("Select .fst to update", initialPath, "fst");
} else {
// open folder explorer starting at project location path and select folder to create project folder in
result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, "");
}
if (!string.IsNullOrEmpty(result)) { // file/folder selection not cancelled
projectLocation = result.Replace('/', '\\');
}
}
// Red error label text to display any file-related errors
// warning if scale is above/below recommended range or if scale was auto-adjusted initially
GUILayout.Label(scaleWarningText, warningStyle);
// from left to right show scale label, scale slider itself, and scale value input with % value
// slider value itself is from 0.0 to 2.0, and actual scale is an exponent of it with an offset of 1
// displayed scale is the actual scale value with 2 decimal places, and changing the displayed
// scale via keyboard does the inverse calculation to get the slider value via logarithm
GUILayout.BeginHorizontal();
GUILayout.Label("Scale:", labelStyle);
sliderScale = GUILayout.HorizontalSlider(sliderScale, MIN_SCALE_SLIDER, MAX_SCALE_SLIDER, sliderStyle, sliderThumbStyle);
float actualScale = (Mathf.Pow(SLIDER_SCALE_EXPONENT, sliderScale) - ACTUAL_SCALE_OFFSET);
GUIStyle scaleInputStyle = new GUIStyle(textStyle);
scaleInputStyle.fixedWidth = SCALE_TEXT_WIDTH;
actualScale *= 100.0f; // convert to 100-based percentage for display purposes
string actualScaleStr = GUILayout.TextField(String.Format("{0:0.00}", actualScale), scaleInputStyle);
actualScaleStr = Regex.Replace(actualScaleStr, @"[^0-9.]", "");
actualScale = float.Parse(actualScaleStr);
actualScale /= 100.0f; // convert back to 1.0-based percentage
SetAvatarScale(actualScale);
GUILayout.Label("%", labelStyle);
GUILayout.EndHorizontal();
GUILayout.Space(15);
// red error label text to display any file-related errors
GUILayout.Label("Error:", errorStyle);
GUILayout.Label(errorText, errorStyle);
GUILayout.Space(10);
// Yellow warning label text to display scrollable list of any bone-related warnings
// yellow warning label text to display scrollable list of any bone-related warnings
GUILayout.Label("Warnings:", warningStyle);
warningScrollPosition = GUILayout.BeginScrollView(warningScrollPosition, GUILayout.Width(WINDOW_WIDTH),
GUILayout.Height(WARNING_SCROLL_HEIGHT));
@ -1229,30 +1436,43 @@ class ExportProjectWindow : EditorWindow {
GUILayout.Space(10);
// Export button which will verify project folder can actually be created
// export button will verify target project folder can actually be created (or target fst file is valid)
// before closing popup window and calling back to initiate the export
bool export = false;
if (GUILayout.Button("Export", buttonStyle)) {
export = true;
if (!CheckForErrors(true)) {
Close();
onCloseCallback(projectDirectory, projectName, warningText);
onExportCallback(updateExistingAvatar ? projectLocation : projectDirectory, projectName, actualScale);
}
}
// Cancel button just closes the popup window without callback
// cancel button closes the popup window triggering the close callback to close the preview scene
if (GUILayout.Button("Cancel", buttonStyle)) {
Close();
}
// When either text field changes check for any errors if we didn't just check errors from clicking Export above
// when any value changes check for any errors and update scale warning if we are not exporting
if (GUI.changed && !export) {
CheckForErrors(false);
UpdateScaleWarning();
}
}
bool CheckForErrors(bool exporting) {
errorText = EMPTY_ERROR_TEXT; // default to None if no errors found
if (updateExistingAvatar) {
// if any text is set in the project file to update field verify that the file actually exists
if (projectLocation.Length > 0) {
if (!File.Exists(projectLocation)) {
errorText = "Please select a valid project file to update.\n";
return true;
}
} else if (exporting) {
errorText = "Please select a project file to update.\n";
return true;
}
} else {
projectDirectory = projectLocation + "\\" + projectName + "\\";
if (projectName.Length > 0) {
// new project must have a unique folder name since the folder will be created for it
@ -1287,6 +1507,51 @@ class ExportProjectWindow : EditorWindow {
}
}
}
}
return false;
}
void UpdateScaleWarning() {
// called on any input changes
float height = GetAvatarHeight();
if (height < MINIMUM_RECOMMENDED_HEIGHT) {
scaleWarningText = "The height of the avatar is below the recommended minimum.";
} else if (height > MAXIMUM_RECOMMENDED_HEIGHT) {
scaleWarningText = "The height of the avatar is above the recommended maximum.";
} else {
scaleWarningText = "";
}
}
float GetAvatarHeight() {
// height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers
Bounds bounds = new Bounds();
var meshRenderers = avatarPreviewObject.GetComponentsInChildren<MeshRenderer>();
var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var renderer in meshRenderers) {
bounds.Encapsulate(renderer.bounds);
}
foreach (var renderer in skinnedMeshRenderers) {
bounds.Encapsulate(renderer.bounds);
}
return bounds.max.y;
}
void SetAvatarScale(float actualScale) {
// set the new scale uniformly on the preview avatar's transform to show the resulting avatar size
avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale);
// adjust slider scale value to match the new actual scale value
sliderScale = GetSliderScaleFromActualScale(actualScale);
}
float GetSliderScaleFromActualScale(float actualScale) {
// since actual scale is an exponent of slider scale with an offset, do the logarithm operation to convert it back
return Mathf.Log(actualScale + ACTUAL_SCALE_OFFSET, SLIDER_SCALE_EXPONENT);
}
void OnDestroy() {
onCloseCallback();
}
}

View file

@ -0,0 +1,76 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Average
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0.53309965, g: 0.8773585, b: 0.27727836, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}

View file

@ -0,0 +1,76 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Floor
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,76 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Line
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0, g: 0, b: 0, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}

View file

@ -0,0 +1,76 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: ShortOrTall
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0.91758025, g: 0.9622642, b: 0.28595585, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}

View file

@ -0,0 +1,76 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: TooShortOrTall
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0.9056604, g: 0.19223925, b: 0.19223925, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}

View file

@ -1,6 +1,6 @@
High Fidelity, Inc.
Avatar Exporter
Version 0.3.3
Version 0.3.5
Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter.
@ -9,13 +9,14 @@ To create a new avatar project:
2. Select the .fbx avatar that you imported in step 1 in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply.
3. With the .fbx avatar still selected in the Assets window, choose High Fidelity menu > Export New Avatar.
4. Select a name for your avatar project (this will be used to create a directory with that name), as well as the target location for your project folder.
5. Once it is exported, your project directory will open in File Explorer.
5. If necessary, adjust the scale for your avatar so that it's height is within the recommended range.
6. Once it is exported, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer.
To update an existing avatar project:
1. Select the existing .fbx avatar in the Assets window that you would like to re-export.
2. Choose High Fidelity menu > Update Existing Avatar and browse to the .fst file you would like to update.
1. Select the existing .fbx avatar in the Assets window that you would like to re-export and choose High Fidelity menu > Update Existing Avatar
2. Select the .fst project file that you wish to update.
3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your selected avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file before performing the update.
4. Once it is updated, your project directory will open in File Explorer.
4. Once it is updated, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer.
* WARNING *
If you are using any external textures as part of your .fbx model, be sure they are copied into the textures folder that is created in the project folder after exporting a new avatar.