Merge pull request #14881 from dback2/avatarExporterTextures

Case 20613: Avatar Exporter v0.2 - copy external textures
This commit is contained in:
John Conklin II 2019-02-15 14:14:05 -08:00 committed by GitHub
commit c7a6902e93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 44 deletions

View file

@ -14,13 +14,14 @@ using System.Collections.Generic;
class AvatarExporter : MonoBehaviour { class AvatarExporter : MonoBehaviour {
// update version number for every PR that changes this file, also set updated version in README file // update version number for every PR that changes this file, also set updated version in README file
static readonly string AVATAR_EXPORTER_VERSION = "0.1"; static readonly string AVATAR_EXPORTER_VERSION = "0.2";
static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_GROUND_MIN_Y = 0.01f;
static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f;
static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly int MAXIMUM_USER_BONE_COUNT = 256;
static readonly string EMPTY_WARNING_TEXT = "None"; static readonly string EMPTY_WARNING_TEXT = "None";
// TODO: use regex
static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] {
"2018.2.12f1", "2018.2.12f1",
"2018.2.11f1", "2018.2.11f1",
@ -262,7 +263,7 @@ class AvatarExporter : MonoBehaviour {
static string assetPath = ""; static string assetPath = "";
static string assetName = ""; static string assetName = "";
static HumanDescription humanDescription; static HumanDescription humanDescription;
static Dictionary<string, string> dependencyTextures = new Dictionary<string, string>();
[MenuItem("High Fidelity/Export New Avatar")] [MenuItem("High Fidelity/Export New Avatar")]
static void ExportNewAvatar() { static void ExportNewAvatar() {
@ -304,24 +305,29 @@ class AvatarExporter : MonoBehaviour {
humanDescription = modelImporter.humanDescription; humanDescription = modelImporter.humanDescription;
SetUserBoneInformation(); SetUserBoneInformation();
string textureWarnings = SetTextureDependencies();
// check if we should be substituting a bone for a missing UpperChest mapping
AdjustUpperChestMapping();
// format resulting bone rule failure strings // format resulting bone rule failure strings
// consider export-blocking bone rules to be errors and show them in an error dialog, // consider export-blocking bone rules to be errors and show them in an error dialog,
// and also include any other bone rule failures as warnings in the dialog // and also include any other bone rule failures plus texture warnings as warnings in the dialog
string boneErrors = ""; string boneErrors = "";
string boneWarnings = ""; string warnings = "";
foreach (var failedBoneRule in failedBoneRules) { foreach (var failedBoneRule in failedBoneRules) {
if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) { if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) {
boneErrors += failedBoneRule.Value + "\n\n"; boneErrors += failedBoneRule.Value + "\n\n";
} else { } else {
boneWarnings += failedBoneRule.Value + "\n\n"; warnings += failedBoneRule.Value + "\n\n";
} }
} }
warnings += textureWarnings;
if (!string.IsNullOrEmpty(boneErrors)) { if (!string.IsNullOrEmpty(boneErrors)) {
// if there are both errors and warnings then warnings will be displayed with errors in the error dialog // if there are both errors and warnings then warnings will be displayed with errors in the error dialog
if (!string.IsNullOrEmpty(boneWarnings)) { if (!string.IsNullOrEmpty(warnings)) {
boneErrors = "Errors:\n\n" + boneErrors; boneErrors = "Errors:\n\n" + boneErrors;
boneErrors += "Warnings:\n\n" + boneWarnings; boneErrors += "Warnings:\n\n" + warnings;
} }
// remove ending newlines from the last rule failure string that was added above // remove ending newlines from the last rule failure string that was added above
boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n")); boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n"));
@ -329,27 +335,6 @@ class AvatarExporter : MonoBehaviour {
return; return;
} }
if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) {
// if parent of Neck is not Chest then map the parent to UpperChest
string neckUserBone;
if (humanoidToUserBoneMappings.TryGetValue("Neck", out neckUserBone)) {
UserBoneInformation neckParentBoneInfo;
string neckParentUserBone = userBoneInfos[neckUserBone].parentName;
if (userBoneInfos.TryGetValue(neckParentUserBone, out neckParentBoneInfo) && !neckParentBoneInfo.HasHumanMapping()) {
neckParentBoneInfo.humanName = "UpperChest";
humanoidToUserBoneMappings.Add("UpperChest", neckParentUserBone);
}
}
// if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest
string chestUserBone;
if (!humanoidToUserBoneMappings.ContainsKey("UpperChest") &&
humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) {
userBoneInfos[chestUserBone].humanName = "UpperChest";
humanoidToUserBoneMappings.Remove("Chest");
humanoidToUserBoneMappings.Add("UpperChest", chestUserBone);
}
}
string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
string hifiFolder = documentsFolder + "\\High Fidelity Projects"; string hifiFolder = documentsFolder + "\\High Fidelity Projects";
if (updateAvatar) { // Update Existing Avatar menu option if (updateAvatar) { // Update Existing Avatar menu option
@ -407,7 +392,13 @@ class AvatarExporter : MonoBehaviour {
return; return;
} else if (option == 0) { // Yes - copy model to Unity project } 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 // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it
File.Copy(exportModelPath, assetPath, true); try {
File.Copy(exportModelPath, assetPath, true);
} catch {
EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath +
". Please check the location and try again.", "Ok");
return;
}
AssetDatabase.ImportAsset(assetPath); AssetDatabase.ImportAsset(assetPath);
// set model to Humanoid animation type and force another refresh on it to process Humanoid // set model to Humanoid animation type and force another refresh on it to process Humanoid
@ -455,12 +446,20 @@ class AvatarExporter : MonoBehaviour {
} }
// write out a new fst file in place of the old file // write out a new fst file in place of the old file
WriteFST(exportFstPath, projectName); if (!WriteFST(exportFstPath, projectName)) {
return;
}
// copy any external texture files to the project's texture directory that are considered dependencies of the model
string texturesDirectory = GetTextureDirectory(exportFstPath);
if (!CopyExternalTextures(texturesDirectory)) {
return;
}
// display success dialog with any bone rule warnings // display success dialog with any bone rule warnings
string successDialog = "Avatar successfully updated!"; string successDialog = "Avatar successfully updated!";
if (!string.IsNullOrEmpty(boneWarnings)) { if (!string.IsNullOrEmpty(warnings)) {
successDialog += "\n\nWarnings:\n" + boneWarnings; successDialog += "\n\nWarnings:\n" + warnings;
} }
EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
} else { // Export New Avatar menu option } else { // Export New Avatar menu option
@ -469,13 +468,13 @@ class AvatarExporter : MonoBehaviour {
Directory.CreateDirectory(hifiFolder); Directory.CreateDirectory(hifiFolder);
} }
if (string.IsNullOrEmpty(boneWarnings)) { if (string.IsNullOrEmpty(warnings)) {
boneWarnings = EMPTY_WARNING_TEXT; warnings = EMPTY_WARNING_TEXT;
} }
// open a popup window to enter new export project name and project location // open a popup window to enter new export project name and project location
ExportProjectWindow window = ScriptableObject.CreateInstance<ExportProjectWindow>(); ExportProjectWindow window = ScriptableObject.CreateInstance<ExportProjectWindow>();
window.Init(hifiFolder, boneWarnings, OnExportProjectWindowClose); window.Init(hifiFolder, warnings, OnExportProjectWindowClose);
} }
} }
@ -485,28 +484,34 @@ class AvatarExporter : MonoBehaviour {
File.Copy(assetPath, exportModelPath); File.Copy(assetPath, exportModelPath);
// create empty Textures and Scripts folders in the project directory // create empty Textures and Scripts folders in the project directory
string texturesDirectory = projectDirectory + "\\textures"; string texturesDirectory = GetTextureDirectory(projectDirectory);
string scriptsDirectory = projectDirectory + "\\scripts"; string scriptsDirectory = projectDirectory + "\\scripts";
Directory.CreateDirectory(texturesDirectory); Directory.CreateDirectory(texturesDirectory);
Directory.CreateDirectory(scriptsDirectory); Directory.CreateDirectory(scriptsDirectory);
// write out the avatar.fst file to the project directory // write out the avatar.fst file to the project directory
string exportFstPath = projectDirectory + "avatar.fst"; string exportFstPath = projectDirectory + "avatar.fst";
WriteFST(exportFstPath, projectName); if (!WriteFST(exportFstPath, projectName)) {
return;
}
// copy any external texture files to the project's texture directory that are considered dependencies of the model
if (!CopyExternalTextures(texturesDirectory)) {
return;
}
// remove any double slashes in texture directory path, display success dialog with any // remove any double slashes in texture directory path, display success dialog with any
// bone warnings previously mentioned, and suggest user to copy external textures over // bone warnings previously mentioned, and suggest user to copy external textures over
texturesDirectory = texturesDirectory.Replace("\\\\", "\\");
string successDialog = "Avatar successfully exported!\n\n"; string successDialog = "Avatar successfully exported!\n\n";
if (warnings != EMPTY_WARNING_TEXT) { if (warnings != EMPTY_WARNING_TEXT) {
successDialog += "Warnings:\n" + warnings; successDialog += "Warnings:\n" + warnings;
} }
successDialog += "Note: If you are using any external textures with your model, " + successDialog += "Note: If you are using any external textures with your model, " +
"please copy those textures to " + texturesDirectory; "please ensure those textures are copied to " + texturesDirectory;
EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
} }
static void WriteFST(string exportFstPath, string projectName) { static bool WriteFST(string exportFstPath, string projectName) {
// write out core fields to top of fst file // write out core fields to top of fst file
try { try {
File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " +
@ -514,7 +519,7 @@ class AvatarExporter : MonoBehaviour {
} catch { } catch {
EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath +
". Please check the location and try again.", "Ok"); ". Please check the location and try again.", "Ok");
return; return false;
} }
// write out joint mappings to fst file // write out joint mappings to fst file
@ -573,6 +578,8 @@ class AvatarExporter : MonoBehaviour {
// open File Explorer to the project directory once finished // open File Explorer to the project directory once finished
System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath);
return true;
} }
static void SetUserBoneInformation() { static void SetUserBoneInformation() {
@ -652,6 +659,29 @@ class AvatarExporter : MonoBehaviour {
return result; return result;
} }
static void AdjustUpperChestMapping() {
if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) {
// if parent of Neck is not Chest then map the parent to UpperChest
string neckUserBone;
if (humanoidToUserBoneMappings.TryGetValue("Neck", out neckUserBone)) {
UserBoneInformation neckParentBoneInfo;
string neckParentUserBone = userBoneInfos[neckUserBone].parentName;
if (userBoneInfos.TryGetValue(neckParentUserBone, out neckParentBoneInfo) && !neckParentBoneInfo.HasHumanMapping()) {
neckParentBoneInfo.humanName = "UpperChest";
humanoidToUserBoneMappings.Add("UpperChest", neckParentUserBone);
}
}
// if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest
string chestUserBone;
if (!humanoidToUserBoneMappings.ContainsKey("UpperChest") &&
humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) {
userBoneInfos[chestUserBone].humanName = "UpperChest";
humanoidToUserBoneMappings.Remove("Chest");
humanoidToUserBoneMappings.Add("UpperChest", chestUserBone);
}
}
}
static void SetFailedBoneRules() { static void SetFailedBoneRules() {
failedBoneRules.Clear(); failedBoneRules.Clear();
@ -865,6 +895,50 @@ class AvatarExporter : MonoBehaviour {
appendage + " (" + rightCount + ")."); appendage + " (" + rightCount + ").");
} }
} }
static string GetTextureDirectory(string basePath) {
string textureDirectory = Path.GetDirectoryName(basePath) + "\\textures";
textureDirectory = textureDirectory.Replace("\\\\", "\\");
return textureDirectory;
}
static string SetTextureDependencies() {
string textureWarnings = "";
dependencyTextures.Clear();
// build the list of all local asset paths for textures that Unity considers dependencies of the model
// for any textures that have duplicate names, return a string of duplicate name warnings
string[] dependencies = AssetDatabase.GetDependencies(assetPath);
foreach (string dependencyPath in dependencies) {
UnityEngine.Object textureObject = AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(Texture2D));
if (textureObject != null) {
string textureName = Path.GetFileName(dependencyPath);
if (dependencyTextures.ContainsKey(textureName)) {
textureWarnings += "There is more than one texture with the name " + textureName +
" referenced in the selected avatar.\n\n";
} else {
dependencyTextures.Add(textureName, dependencyPath);
}
}
}
return textureWarnings;
}
static bool CopyExternalTextures(string texturesDirectory) {
// copy the found dependency textures from the local asset folder to the textures folder in the target export project
foreach (var texture in dependencyTextures) {
string targetPath = texturesDirectory + "\\" + texture.Key;
try {
File.Copy(texture.Value, targetPath, true);
} catch {
EditorUtility.DisplayDialog("Error", "Failed to copy texture file " + texture.Value + " to " + targetPath +
". Please check the location and try again.", "Ok");
return false;
}
}
return true;
}
} }
class ExportProjectWindow : EditorWindow { class ExportProjectWindow : EditorWindow {

View file

@ -1,6 +1,6 @@
High Fidelity, Inc. High Fidelity, Inc.
Avatar Exporter Avatar Exporter
Version 0.1 Version 0.2
Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter.