From d063825269485e8ea09e937fd6e39dd2a17c41ca Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 6 Mar 2019 12:17:19 -0800 Subject: [PATCH] export unity material data to materialMap in fst --- .../Assets/Editor/AvatarExporter.cs | 430 +++++++++++++----- tools/unity-avatar-exporter/Assets/README.txt | 2 +- .../avatarExporter.unitypackage | Bin 13667 -> 15949 bytes 3 files changed, 317 insertions(+), 115 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 7b90145223..40994c8f46 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -14,12 +14,14 @@ using System.Collections.Generic; 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.2"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.1"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly string EMPTY_WARNING_TEXT = "None"; + static readonly string TEXTURES_DIRECTORY = "textures"; + static readonly string DEFAULT_MATERIAL_NAME = "No Name"; // TODO: use regex static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { @@ -195,8 +197,17 @@ class AvatarExporter : MonoBehaviour { " Thumb Intermediate", " Thumb Proximal", }; + + static readonly string STANDARD_SHADER = "Standard"; + static readonly string STANDARD_ROUGHNESS_SHADER = "Standard (Roughness setup)"; + static readonly string STANDARD_SPECULAR_SHADER = "Standard (Specular setup)"; + static readonly string[] SUPPORTED_SHADERS = new string[] { + STANDARD_SHADER, + STANDARD_ROUGHNESS_SHADER, + STANDARD_SPECULAR_SHADER, + }; - enum BoneRule { + enum AvatarRule { RecommendedUnityVersion, SingleRoot, NoDuplicateMapping, @@ -215,14 +226,14 @@ class AvatarExporter : MonoBehaviour { HipsNotOnGround, HipsSpineChestNotCoincident, TotalBoneCountUnderLimit, - BoneRuleEnd, + AvatarRuleEnd, }; // rules that are treated as errors and prevent exporting, otherwise rules will show as warnings - static readonly BoneRule[] EXPORT_BLOCKING_BONE_RULES = new BoneRule[] { - BoneRule.HipsMapped, - BoneRule.SpineMapped, - BoneRule.ChestMapped, - BoneRule.HeadMapped, + static readonly AvatarRule[] EXPORT_BLOCKING_AVATAR_RULES = new AvatarRule[] { + AvatarRule.HipsMapped, + AvatarRule.SpineMapped, + AvatarRule.ChestMapped, + AvatarRule.HeadMapped, }; class UserBoneInformation { @@ -255,15 +266,67 @@ class AvatarExporter : MonoBehaviour { } } - static Dictionary userBoneInfos = new Dictionary(); - static Dictionary humanoidToUserBoneMappings = new Dictionary(); - static BoneTreeNode userBoneTree = new BoneTreeNode(); - static Dictionary failedBoneRules = new Dictionary(); + class MaterialData { + public Color albedo; + public string albedoMap; + public double metallic; + public string metallicMap; + public double roughness; + public string roughnessMap; + public string normalMap; + public string heightMap; + public string occlusionMap; + public Color emissive; + public string emissiveMap; + + public string getJSON() { + string json = "{ \"materialVersion\": 1, \"materials\": { "; + json += "\"albedo\": [" + albedo.r + ", " + albedo.g + ", " + albedo.b + "], "; + if (!string.IsNullOrEmpty(albedoMap)) { + json += "\"albedoMap\": \"" + albedoMap + "\", "; + } + json += "\"metallic\": " + metallic + ", "; + if (!string.IsNullOrEmpty(metallicMap)) { + json += "\"metallicMap\": \"" + metallicMap + "\", "; + } + json += "\"roughness\": " + roughness + ", "; + if (!string.IsNullOrEmpty(roughnessMap)) { + json += "\"roughnessMap\": \"" + roughnessMap + "\", "; + } + if (!string.IsNullOrEmpty(normalMap)) { + json += "\"normalMap\": \"" + normalMap + "\", "; + } + if (!string.IsNullOrEmpty(heightMap)) { + json += "\"heightMap\": \"" + heightMap + "\", "; + } + if (!string.IsNullOrEmpty(occlusionMap)) { + json += "\"occlusionMap\": \"" + occlusionMap + "\", "; + } + json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "] "; + if (!string.IsNullOrEmpty(emissiveMap)) { + json += "\", emissiveMap\": \"" + emissiveMap + "\""; + } + json += "} }"; + return json; + } + } static string assetPath = ""; static string assetName = ""; + + static ModelImporter modelImporter; static HumanDescription humanDescription; - static Dictionary dependencyTextures = new Dictionary(); + static Dictionary userBoneInfos = new Dictionary(); + static Dictionary humanoidToUserBoneMappings = new Dictionary(); + static BoneTreeNode userBoneTree = new BoneTreeNode(); + static Dictionary failedAvatarRules = new Dictionary(); + + static Dictionary textureDependencies = new Dictionary(); + static Dictionary materialMappings = new Dictionary(); + static Dictionary materialDatas = new Dictionary(); + static List materialAlternateStandardShader = new List(); + static Dictionary materialUnsupportedShader = new Dictionary(); + static List normalMapAndHeightMapNotBoth = new List(); [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { @@ -280,7 +343,10 @@ class AvatarExporter : MonoBehaviour { EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok"); } - static void ExportSelectedAvatar(bool updateAvatar) { + static void ExportSelectedAvatar(bool updateAvatar) { + // ensure everything is saved to file before exporting + AssetDatabase.SaveAssets(); + string[] guids = Selection.assetGUIDs; if (guids.Length != 1) { if (guids.Length == 0) { @@ -292,7 +358,7 @@ class AvatarExporter : MonoBehaviour { } assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); assetName = Path.GetFileNameWithoutExtension(assetPath); - ModelImporter modelImporter = ModelImporter.GetAtPath(assetPath) as 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; @@ -302,26 +368,37 @@ class AvatarExporter : MonoBehaviour { " the Rig section of it's Inspector window.", "Ok"); return; } - + humanDescription = modelImporter.humanDescription; - SetUserBoneInformation(); string textureWarnings = SetTextureDependencies(); + SetBoneAndMaterialInformation(); // check if we should be substituting a bone for a missing UpperChest mapping AdjustUpperChestMapping(); - // 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 + // 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 string boneErrors = ""; string warnings = ""; - foreach (var failedBoneRule in failedBoneRules) { - if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) { - boneErrors += failedBoneRule.Value + "\n\n"; + foreach (var failedAvatarRule in failedAvatarRules) { + if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0) { + boneErrors += failedAvatarRule.Value + "\n\n"; } else { - warnings += failedBoneRule.Value + "\n\n"; + 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"; + } + foreach (string materialName in normalMapAndHeightMapNotBoth) { + warnings += "The material " + materialName + " has both a normal map and a height map assigned but can only use 1.\n\n"; + } warnings += textureWarnings; if (!string.IsNullOrEmpty(boneErrors)) { // if there are both errors and warnings then warnings will be displayed with errors in the error dialog @@ -408,9 +485,9 @@ class AvatarExporter : MonoBehaviour { modelImporter.SaveAndReimport(); // redo parent names, joint mappings, and user bone positions due to the fbx change - // as well as re-check the bone rules for failures + // as well as re-check the avatar rules for failures humanDescription = modelImporter.humanDescription; - SetUserBoneInformation(); + SetBoneAndMaterialInformation(); } } } else { @@ -456,7 +533,7 @@ class AvatarExporter : MonoBehaviour { return; } - // display success dialog with any bone rule warnings + // display success dialog with any avatar rule warnings string successDialog = "Avatar successfully updated!"; if (!string.IsNullOrEmpty(warnings)) { successDialog += "\n\nWarnings:\n" + warnings; @@ -575,6 +652,27 @@ class AvatarExporter : MonoBehaviour { jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n"); } } + + // if there is any material data to save then write out all materials in JSON material format to the materialMap field + if (materialDatas.Count > 0) { + string materialJson = "{ "; + foreach (var materialData in materialDatas) { + // if this is the only material in the mapping and it is the default name No Name mapped to No Name, + // then the avatar has no embedded materials and this material should be applied to all meshes + string materialName = materialData.Key; + if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME && + materialMappings[materialName] == DEFAULT_MATERIAL_NAME) { + materialJson += "\"all\": "; + } else { + materialJson += "\"[mat::" + materialName + "]\": "; + } + materialJson += materialData.Value.getJSON(); + materialJson += ", "; + } + materialJson = materialJson.Substring(0, materialJson.LastIndexOf(", ")); + materialJson += " }"; + File.AppendAllText(exportFstPath, "materialMap = " + materialJson); + } // open File Explorer to the project directory once finished System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); @@ -582,11 +680,18 @@ class AvatarExporter : MonoBehaviour { return true; } - static void SetUserBoneInformation() { + static void SetBoneAndMaterialInformation() { userBoneInfos.Clear(); humanoidToUserBoneMappings.Clear(); userBoneTree = new BoneTreeNode(); + materialDatas.Clear(); + materialAlternateStandardShader.Clear(); + materialUnsupportedShader.Clear(); + normalMapAndHeightMapNotBoth.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)); @@ -610,20 +715,26 @@ class AvatarExporter : MonoBehaviour { } } - // generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar - SetFailedBoneRules(); + // generate the list of avatar rule failure strings for any avatar rules that are not satisfied by this avatar + SetFailedAvatarRules(); } static void TraverseUserBoneTree(Transform modelBone) { GameObject gameObject = modelBone.gameObject; // check if this transform is a node containing mesh, light, or camera instead of a bone - bool mesh = gameObject.GetComponent() != null || gameObject.GetComponent() != null; + MeshRenderer meshRenderer = gameObject.GetComponent(); + SkinnedMeshRenderer skinnedMeshRenderer = gameObject.GetComponent(); + bool mesh = meshRenderer != null || skinnedMeshRenderer != null; bool light = gameObject.GetComponent() != null; bool camera = gameObject.GetComponent() != null; - // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name - if (!mesh && !light && !camera) { + // 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) { + Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials; + StoreMaterialData(materials); + } else if (!light && !camera) { + // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name UserBoneInformation userBoneInfo = new UserBoneInformation(); userBoneInfo.position = modelBone.position; // bone's absolute position @@ -682,8 +793,8 @@ class AvatarExporter : MonoBehaviour { } } - static void SetFailedBoneRules() { - failedBoneRules.Clear(); + static void SetFailedAvatarRules() { + failedAvatarRules.Clear(); string hipsUserBone = ""; string spineUserBone = ""; @@ -692,60 +803,60 @@ class AvatarExporter : MonoBehaviour { Vector3 hipsPosition = new Vector3(); - // iterate over all bone rules in order and add any rules that fail - // to the failed bone rules map with appropriate error or warning text - for (BoneRule boneRule = 0; boneRule < BoneRule.BoneRuleEnd; ++boneRule) { - switch (boneRule) { - case BoneRule.RecommendedUnityVersion: + // iterate over all avatar rules in order and add any rules that fail + // to the failed avatar rules map with appropriate error or warning text + for (AvatarRule avatarRule = 0; avatarRule < AvatarRule.AvatarRuleEnd; ++avatarRule) { + switch (avatarRule) { + case AvatarRule.RecommendedUnityVersion: if (Array.IndexOf(RECOMMENDED_UNITY_VERSIONS, Application.unityVersion) == -1) { - failedBoneRules.Add(boneRule, "The current version of Unity is not one of the recommended Unity " + - "versions. If you are using a version of Unity later than 2018.2.12f1, " + - "it is recommended to apply Enforce T-Pose under the Pose dropdown " + - "in Humanoid configuration."); + failedAvatarRules.Add(avatarRule, "The current version of Unity is not one of the recommended Unity " + + "versions. If you are using a version of Unity later than 2018.2.12f1, " + + "it is recommended to apply Enforce T-Pose under the Pose dropdown " + + "in Humanoid configuration."); } break; - case BoneRule.SingleRoot: - // bone rule fails if the root bone node has more than one child bone + case AvatarRule.SingleRoot: + // avatar rule fails if the root bone node has more than one child bone if (userBoneTree.children.Count > 1) { - failedBoneRules.Add(boneRule, "There is more than one bone at the top level of the selected avatar's " + - "bone hierarchy. Please ensure all bones for Humanoid mappings are " + - "under the same bone hierarchy."); + failedAvatarRules.Add(avatarRule, "There is more than one bone at the top level of the selected avatar's " + + "bone hierarchy. Please ensure all bones for Humanoid mappings are " + + "under the same bone hierarchy."); } break; - case BoneRule.NoDuplicateMapping: - // bone rule fails if any user bone is mapped to more than one Humanoid bone + case AvatarRule.NoDuplicateMapping: + // avatar rule fails if any user bone is mapped to more than one Humanoid bone foreach (var userBoneInfo in userBoneInfos) { string boneName = userBoneInfo.Key; int mappingCount = userBoneInfo.Value.mappingCount; if (mappingCount > 1) { string text = "The " + boneName + " bone is mapped to more than one bone in Humanoid."; - if (failedBoneRules.ContainsKey(boneRule)) { - failedBoneRules[boneRule] += "\n" + text; + if (failedAvatarRules.ContainsKey(avatarRule)) { + failedAvatarRules[avatarRule] += "\n" + text; } else { - failedBoneRules.Add(boneRule, text); + failedAvatarRules.Add(avatarRule, text); } } } break; - case BoneRule.NoAsymmetricalLegMapping: - CheckAsymmetricalMappingRule(boneRule, LEG_MAPPING_SUFFIXES, "leg"); + case AvatarRule.NoAsymmetricalLegMapping: + CheckAsymmetricalMappingRule(avatarRule, LEG_MAPPING_SUFFIXES, "leg"); break; - case BoneRule.NoAsymmetricalArmMapping: - CheckAsymmetricalMappingRule(boneRule, ARM_MAPPING_SUFFIXES, "arm"); + case AvatarRule.NoAsymmetricalArmMapping: + CheckAsymmetricalMappingRule(avatarRule, ARM_MAPPING_SUFFIXES, "arm"); break; - case BoneRule.NoAsymmetricalHandMapping: - CheckAsymmetricalMappingRule(boneRule, HAND_MAPPING_SUFFIXES, "hand"); + case AvatarRule.NoAsymmetricalHandMapping: + CheckAsymmetricalMappingRule(avatarRule, HAND_MAPPING_SUFFIXES, "hand"); break; - case BoneRule.HipsMapped: - hipsUserBone = CheckHumanBoneMappingRule(boneRule, "Hips"); + case AvatarRule.HipsMapped: + hipsUserBone = CheckHumanBoneMappingRule(avatarRule, "Hips"); break; - case BoneRule.SpineMapped: - spineUserBone = CheckHumanBoneMappingRule(boneRule, "Spine"); + case AvatarRule.SpineMapped: + spineUserBone = CheckHumanBoneMappingRule(avatarRule, "Spine"); break; - case BoneRule.SpineDescendantOfHips: - CheckUserBoneDescendantOfHumanRule(boneRule, spineUserBone, "Hips"); + case AvatarRule.SpineDescendantOfHips: + CheckUserBoneDescendantOfHumanRule(avatarRule, spineUserBone, "Hips"); break; - case BoneRule.ChestMapped: + case AvatarRule.ChestMapped: if (!humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { // check to see if there is a child of Spine that we can suggest to be mapped to Chest string spineChild = ""; @@ -755,54 +866,54 @@ class AvatarExporter : MonoBehaviour { spineChild = spineTreeNode.children[0].boneName; } } - failedBoneRules.Add(boneRule, "There is no Chest bone mapped in Humanoid for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no Chest bone mapped in Humanoid for the selected avatar."); // if the only found child of Spine is not yet mapped then add it as a suggestion for Chest mapping if (!string.IsNullOrEmpty(spineChild) && !userBoneInfos[spineChild].HasHumanMapping()) { - failedBoneRules[boneRule] += " It is suggested that you map bone " + spineChild + - " to Chest in Humanoid."; + failedAvatarRules[avatarRule] += " It is suggested that you map bone " + spineChild + + " to Chest in Humanoid."; } } break; - case BoneRule.ChestDescendantOfSpine: - CheckUserBoneDescendantOfHumanRule(boneRule, chestUserBone, "Spine"); + case AvatarRule.ChestDescendantOfSpine: + CheckUserBoneDescendantOfHumanRule(avatarRule, chestUserBone, "Spine"); break; - case BoneRule.NeckMapped: - CheckHumanBoneMappingRule(boneRule, "Neck"); + case AvatarRule.NeckMapped: + CheckHumanBoneMappingRule(avatarRule, "Neck"); break; - case BoneRule.HeadMapped: - headUserBone = CheckHumanBoneMappingRule(boneRule, "Head"); + case AvatarRule.HeadMapped: + headUserBone = CheckHumanBoneMappingRule(avatarRule, "Head"); break; - case BoneRule.HeadDescendantOfChest: - CheckUserBoneDescendantOfHumanRule(boneRule, headUserBone, "Chest"); + case AvatarRule.HeadDescendantOfChest: + CheckUserBoneDescendantOfHumanRule(avatarRule, headUserBone, "Chest"); break; - case BoneRule.EyesMapped: + case AvatarRule.EyesMapped: bool leftEyeMapped = humanoidToUserBoneMappings.ContainsKey("LeftEye"); bool rightEyeMapped = humanoidToUserBoneMappings.ContainsKey("RightEye"); if (!leftEyeMapped || !rightEyeMapped) { if (leftEyeMapped && !rightEyeMapped) { - failedBoneRules.Add(boneRule, "There is no RightEye bone mapped in Humanoid " + - "for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no RightEye bone mapped in Humanoid " + + "for the selected avatar."); } else if (!leftEyeMapped && rightEyeMapped) { - failedBoneRules.Add(boneRule, "There is no LeftEye bone mapped in Humanoid " + - "for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no LeftEye bone mapped in Humanoid " + + "for the selected avatar."); } else { - failedBoneRules.Add(boneRule, "There is no LeftEye or RightEye bone mapped in Humanoid " + - "for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no LeftEye or RightEye bone mapped in Humanoid " + + "for the selected avatar."); } } break; - case BoneRule.HipsNotOnGround: + case AvatarRule.HipsNotOnGround: // ensure the absolute Y position for the bone mapped to Hips (if its mapped) is at least HIPS_GROUND_MIN_Y if (!string.IsNullOrEmpty(hipsUserBone)) { UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone]; hipsPosition = hipsBoneInfo.position; if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - failedBoneRules.Add(boneRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + - ") should not be at ground level."); + failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + + ") should not be at ground level."); } } break; - case BoneRule.HipsSpineChestNotCoincident: + case AvatarRule.HipsSpineChestNotCoincident: // ensure the bones mapped to Hips, Spine, and Chest are all not in the same position, // check Hips to Spine and Spine to Chest lengths are within HIPS_SPINE_CHEST_MIN_SEPARATION if (!string.IsNullOrEmpty(spineUserBone) && !string.IsNullOrEmpty(chestUserBone) && @@ -813,34 +924,34 @@ class AvatarExporter : MonoBehaviour { Vector3 spineToChest = spineBoneInfo.position - chestBoneInfo.position; if (hipsToSpine.magnitude < HIPS_SPINE_CHEST_MIN_SEPARATION && spineToChest.magnitude < HIPS_SPINE_CHEST_MIN_SEPARATION) { - failedBoneRules.Add(boneRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + - "), the bone mapped to Spine in Humanoid (" + spineUserBone + - "), and the bone mapped to Chest in Humanoid (" + chestUserBone + - ") should not be coincidental."); + failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + + "), the bone mapped to Spine in Humanoid (" + spineUserBone + + "), and the bone mapped to Chest in Humanoid (" + chestUserBone + + ") should not be coincidental."); } } break; - case BoneRule.TotalBoneCountUnderLimit: + case AvatarRule.TotalBoneCountUnderLimit: int userBoneCount = userBoneInfos.Count; if (userBoneCount > MAXIMUM_USER_BONE_COUNT) { - failedBoneRules.Add(boneRule, "The total number of bones in the avatar (" + userBoneCount + - ") exceeds the maximum bone limit (" + MAXIMUM_USER_BONE_COUNT + ")."); + failedAvatarRules.Add(avatarRule, "The total number of bones in the avatar (" + userBoneCount + + ") exceeds the maximum bone limit (" + MAXIMUM_USER_BONE_COUNT + ")."); } break; } } } - static string CheckHumanBoneMappingRule(BoneRule boneRule, string humanBoneName) { + static string CheckHumanBoneMappingRule(AvatarRule avatarRule, string humanBoneName) { string userBoneName = ""; - // bone rule fails if bone is not mapped in Humanoid + // avatar rule fails if bone is not mapped in Humanoid if (!humanoidToUserBoneMappings.TryGetValue(humanBoneName, out userBoneName)) { - failedBoneRules.Add(boneRule, "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; } - static void CheckUserBoneDescendantOfHumanRule(BoneRule boneRule, string userBoneName, string descendantOfHumanName) { + static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string userBoneName, string descendantOfHumanName) { if (string.IsNullOrEmpty(userBoneName)) { return; } @@ -867,13 +978,13 @@ class AvatarExporter : MonoBehaviour { } } - // bone rule fails if no ancestor of given user bone matched the descendant of name (no early return) - failedBoneRules.Add(boneRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + - ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + - descendantOfUserBoneName + ")."); + // avatar rule fails if no ancestor of given user bone matched the descendant of name (no early return) + failedAvatarRules.Add(avatarRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + + ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + + descendantOfUserBoneName + ")."); } - static void CheckAsymmetricalMappingRule(BoneRule boneRule, string[] mappingSuffixes, string appendage) { + static void CheckAsymmetricalMappingRule(AvatarRule avatarRule, string[] mappingSuffixes, string appendage) { int leftCount = 0; int rightCount = 0; // add Left/Right to each mapping suffix to make Humanoid mapping names, @@ -888,23 +999,23 @@ class AvatarExporter : MonoBehaviour { ++rightCount; } } - // bone rule fails if number of left appendage mappings doesn't match number of right appendage mappings + // avatar rule fails if number of left appendage mappings doesn't match number of right appendage mappings if (leftCount != rightCount) { - failedBoneRules.Add(boneRule, "The number of bones mapped in Humanoid for the left " + appendage + " (" + - leftCount + ") does not match the number of bones mapped in Humanoid for the right " + - appendage + " (" + rightCount + ")."); + failedAvatarRules.Add(avatarRule, "The number of bones mapped in Humanoid for the left " + appendage + " (" + + leftCount + ") does not match the number of bones mapped in Humanoid for the right " + + appendage + " (" + rightCount + ")."); } } static string GetTextureDirectory(string basePath) { - string textureDirectory = Path.GetDirectoryName(basePath) + "\\textures"; + string textureDirectory = Path.GetDirectoryName(basePath) + "\\" + TEXTURES_DIRECTORY; textureDirectory = textureDirectory.Replace("\\\\", "\\"); return textureDirectory; } static string SetTextureDependencies() { string textureWarnings = ""; - dependencyTextures.Clear(); + textureDependencies.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 @@ -913,11 +1024,11 @@ class AvatarExporter : MonoBehaviour { UnityEngine.Object textureObject = AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(Texture2D)); if (textureObject != null) { string textureName = Path.GetFileName(dependencyPath); - if (dependencyTextures.ContainsKey(textureName)) { + if (textureDependencies.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); + textureDependencies.Add(textureName, dependencyPath); } } } @@ -927,7 +1038,7 @@ class AvatarExporter : MonoBehaviour { 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) { + foreach (var texture in textureDependencies) { string targetPath = texturesDirectory + "\\" + texture.Key; try { File.Copy(texture.Value, targetPath, true); @@ -939,6 +1050,97 @@ class AvatarExporter : MonoBehaviour { } return true; } + + static void StoreMaterialData(Material[] materials) { + // store each material's info in the materialDatas list to be written out later to the FST if it is a supported shader + foreach (Material material in materials) { + string materialName = material.name; + string shaderName = material.shader.name; + + Debug.Log("material1 " + materialName); + + // 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); + } + continue; + } + + MaterialData materialData = new MaterialData(); + materialData.albedo = material.GetColor("_Color"); + materialData.albedoMap = GetMaterialTexture(material, "_MainTex"); + materialData.roughness = material.GetFloat("_Glossiness"); + materialData.roughnessMap = GetMaterialTexture(material, "_SpecGlossMap"); + materialData.normalMap = GetMaterialTexture(material, "_BumpMap"); + materialData.heightMap = GetMaterialTexture(material, "_ParallaxMap"); + materialData.occlusionMap = GetMaterialTexture(material, "_OcclusionMap"); + materialData.emissive = material.GetColor("_EmissionColor"); + materialData.emissiveMap = GetMaterialTexture(material, "_EmissionMap"); + + // for specular setups we will treat the metallic value as the average of the specular RGB intensities + if (shaderName == STANDARD_SPECULAR_SHADER) { + Color specular = material.GetColor("_SpecColor"); + materialData.metallic = (specular.r + specular.g + specular.b) / 3.0f; + } else { + materialData.metallic = material.GetFloat("_Metallic"); + materialData.metallicMap = GetMaterialTexture(material, "_MetallicGlossMap"); + } + + // 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); + } + materialData.roughness = 1.0f - materialData.roughness; + } + + // materials can not have both a normal map and a height map set + if (!string.IsNullOrEmpty(materialData.normalMap) && !string.IsNullOrEmpty(materialData.heightMap) && !normalMapAndHeightMapNotBoth.Contains(materialName)) { + normalMapAndHeightMapNotBoth.Add(materialName); + } + + // remap the material name from the Unity material name to the fbx material name that it overrides + if (materialMappings.ContainsKey(materialName)) { + materialName = materialMappings[materialName]; + } + if (!materialDatas.ContainsKey(materialName)) { + materialDatas.Add(materialName, materialData); + } + } + } + + static string GetMaterialTexture(Material material, string textureProperty) { + // ensure the texture property name exists in this material and return its texture directory path if so + string[] textureNames = material.GetTexturePropertyNames(); + if (Array.IndexOf(textureNames, textureProperty) >= 0) { + Texture texture = material.GetTexture(textureProperty); + if (texture) { + foreach (var textureDependency in textureDependencies) { + string textureFile = textureDependency.Key; + if (Path.GetFileNameWithoutExtension(textureFile) == texture.name) { + return TEXTURES_DIRECTORY + "/" + textureFile; + } + } + } + } + return ""; + } + + static void SetMaterialMappings() { + materialMappings.Clear(); + + // store the mappings from fbx material name to the Unity material name overriding 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); + //Debug.Log("materialMapping " + material.name + " " + mapping.Key.name); + } + } + } } class ExportProjectWindow : EditorWindow { diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index c84cec2978..2aa1e5f02b 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.2 +Version 0.3.1 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 95c000e7c61b7623e0da0a1f9974e0744375014e..ab39e1ed8f7d25ceb210c8ee59897c88aae20abb 100644 GIT binary patch literal 15949 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 B&65BC