mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-06-21 14:21:24 +02:00
Merge pull request #14703 from dback2/avatarExporterImprovements
Avatar Exporter v0.1 - add bone rules and versioning
This commit is contained in:
commit
a8f95c8916
3 changed files with 611 additions and 197 deletions
|
@ -13,6 +13,42 @@ using System.IO;
|
||||||
using System.Collections.Generic;
|
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
|
||||||
|
static readonly string AVATAR_EXPORTER_VERSION = "0.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[] RECOMMENDED_UNITY_VERSIONS = new string[] {
|
||||||
|
"2018.2.12f1",
|
||||||
|
"2018.2.11f1",
|
||||||
|
"2018.2.10f1",
|
||||||
|
"2018.2.9f1",
|
||||||
|
"2018.2.8f1",
|
||||||
|
"2018.2.7f1",
|
||||||
|
"2018.2.6f1",
|
||||||
|
"2018.2.5f1",
|
||||||
|
"2018.2.4f1",
|
||||||
|
"2018.2.3f1",
|
||||||
|
"2018.2.2f1",
|
||||||
|
"2018.2.1f1",
|
||||||
|
"2018.2.0f2",
|
||||||
|
"2018.1.9f2",
|
||||||
|
"2018.1.8f1",
|
||||||
|
"2018.1.7f1",
|
||||||
|
"2018.1.6f1",
|
||||||
|
"2018.1.5f1",
|
||||||
|
"2018.1.4f1",
|
||||||
|
"2018.1.3f1",
|
||||||
|
"2018.1.2f1",
|
||||||
|
"2018.1.1f1",
|
||||||
|
"2018.1.0f2",
|
||||||
|
"2017.4.18f1",
|
||||||
|
"2017.4.17f1",
|
||||||
|
};
|
||||||
|
|
||||||
static readonly Dictionary<string, string> HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary<string, string> {
|
static readonly Dictionary<string, string> HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary<string, string> {
|
||||||
{"Chest", "Spine1"},
|
{"Chest", "Spine1"},
|
||||||
{"Head", "Head"},
|
{"Head", "Head"},
|
||||||
|
@ -70,71 +106,164 @@ class AvatarExporter : MonoBehaviour {
|
||||||
{"UpperChest", "Spine2"},
|
{"UpperChest", "Spine2"},
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly Dictionary<string, Quaternion> referenceAbsoluteRotations = new Dictionary<string, Quaternion> {
|
// absolute reference rotations for each Humanoid bone using Artemis fbx in Unity 2018.2.12f1
|
||||||
|
static readonly Dictionary<string, Quaternion> REFERENCE_ROTATIONS = new Dictionary<string, Quaternion> {
|
||||||
|
{"Chest", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)},
|
||||||
{"Head", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
|
{"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)},
|
{"Hips", new Quaternion(-3.043941e-10f, -1.573706e-7f, 5.112975e-6f, 1f)},
|
||||||
{"LeftHandIndex3", new Quaternion(-0.5086057f, 0.4908088f, -0.4912299f, -0.5090388f)},
|
{"Left Index Distal", new Quaternion(-0.5086057f, 0.4908088f, -0.4912299f, -0.5090388f)},
|
||||||
{"LeftHandIndex2", new Quaternion(-0.4934928f, 0.5062312f, -0.5064303f, -0.4936835f)},
|
{"Left Index Intermediate", new Quaternion(-0.4934928f, 0.5062312f, -0.5064303f, -0.4936835f)},
|
||||||
{"LeftHandIndex1", new Quaternion(-0.4986293f, 0.5017503f, -0.5013659f, -0.4982448f)},
|
{"Left Index Proximal", new Quaternion(-0.4986293f, 0.5017503f, -0.5013659f, -0.4982448f)},
|
||||||
{"LeftHandPinky3", new Quaternion(-0.490056f, 0.5143053f, -0.5095307f, -0.4855038f)},
|
{"Left Little Distal", new Quaternion(-0.490056f, 0.5143053f, -0.5095307f, -0.4855038f)},
|
||||||
{"LeftHandPinky2", new Quaternion(-0.5083722f, 0.4954255f, -0.4915887f, -0.5044324f)},
|
{"Left Little Intermediate", new Quaternion(-0.5083722f, 0.4954255f, -0.4915887f, -0.5044324f)},
|
||||||
{"LeftHandPinky1", new Quaternion(-0.5062528f, 0.497324f, -0.4937346f, -0.5025966f)},
|
{"Left Little Proximal", new Quaternion(-0.5062528f, 0.497324f, -0.4937346f, -0.5025966f)},
|
||||||
{"LeftHandMiddle3", new Quaternion(-0.4871885f, 0.5123404f, -0.5125002f, -0.4873383f)},
|
{"Left Middle Distal", new Quaternion(-0.4871885f, 0.5123404f, -0.5125002f, -0.4873383f)},
|
||||||
{"LeftHandMiddle2", new Quaternion(-0.5171652f, 0.4827828f, -0.4822642f, -0.5166069f)},
|
{"Left Middle Intermediate", new Quaternion(-0.5171652f, 0.4827828f, -0.4822642f, -0.5166069f)},
|
||||||
{"LeftHandMiddle1", new Quaternion(-0.4955998f, 0.5041052f, -0.5043675f, -0.4958555f)},
|
{"Left Middle Proximal", new Quaternion(-0.4955998f, 0.5041052f, -0.5043675f, -0.4958555f)},
|
||||||
{"LeftHandRing3", new Quaternion(-0.4936301f, 0.5097645f, -0.5061787f, -0.4901562f)},
|
{"Left Ring Distal", new Quaternion(-0.4936301f, 0.5097645f, -0.5061787f, -0.4901562f)},
|
||||||
{"LeftHandRing2", new Quaternion(-0.5089865f, 0.4943658f, -0.4909532f, -0.5054707f)},
|
{"Left Ring Intermediate", new Quaternion(-0.5089865f, 0.4943658f, -0.4909532f, -0.5054707f)},
|
||||||
{"LeftHandRing1", new Quaternion(-0.5020972f, 0.5005084f, -0.4979034f, -0.4994819f)},
|
{"Left Ring Proximal", new Quaternion(-0.5020972f, 0.5005084f, -0.4979034f, -0.4994819f)},
|
||||||
{"LeftHandThumb3", new Quaternion(-0.6617184f, 0.2884935f, -0.3604706f, -0.5907297f)},
|
{"Left Thumb Distal", new Quaternion(-0.6617184f, 0.2884935f, -0.3604706f, -0.5907297f)},
|
||||||
{"LeftHandThumb2", new Quaternion(-0.6935627f, 0.1995147f, -0.2805665f, -0.6328092f)},
|
{"Left Thumb Intermediate", new Quaternion(-0.6935627f, 0.1995147f, -0.2805665f, -0.6328092f)},
|
||||||
{"LeftHandThumb1", new Quaternion(-0.6663674f, 0.278572f, -0.3507071f, -0.5961183f)},
|
{"Left Thumb Proximal", new Quaternion(-0.6663674f, 0.278572f, -0.3507071f, -0.5961183f)},
|
||||||
{"LeftEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
|
{"LeftEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
|
||||||
{"LeftFoot", new Quaternion(0.009215056f, 0.3612514f, 0.9323555f, -0.01121602f)},
|
{"LeftFoot", new Quaternion(0.009215056f, 0.3612514f, 0.9323555f, -0.01121602f)},
|
||||||
{"LeftHand", new Quaternion(-0.4797408f, 0.5195366f, -0.5279632f, -0.4703038f)},
|
{"LeftHand", new Quaternion(-0.4797408f, 0.5195366f, -0.5279632f, -0.4703038f)},
|
||||||
{"LeftForeArm", new Quaternion(-0.4594738f, 0.4594729f, -0.5374805f, -0.5374788f)},
|
{"LeftLowerArm", new Quaternion(-0.4594738f, 0.4594729f, -0.5374805f, -0.5374788f)},
|
||||||
{"LeftLeg", new Quaternion(-0.0005380471f, -0.03154583f, 0.9994993f, 0.002378627f)},
|
{"LeftLowerLeg", new Quaternion(-0.0005380471f, -0.03154583f, 0.9994993f, 0.002378627f)},
|
||||||
{"LeftShoulder", new Quaternion(-0.3840606f, 0.525857f, -0.5957767f, -0.47013f)},
|
{"LeftShoulder", new Quaternion(-0.3840606f, 0.525857f, -0.5957767f, -0.47013f)},
|
||||||
{"LeftToeBase", new Quaternion(-0.0002536641f, 0.7113448f, 0.7027079f, -0.01379319f)},
|
{"LeftToes", new Quaternion(-0.0002536641f, 0.7113448f, 0.7027079f, -0.01379319f)},
|
||||||
{"LeftArm", new Quaternion(-0.4591927f, 0.4591916f, -0.5377204f, -0.5377193f)},
|
{"LeftUpperArm", new Quaternion(-0.4591927f, 0.4591916f, -0.5377204f, -0.5377193f)},
|
||||||
{"LeftUpLeg", new Quaternion(-0.0006682819f, 0.0006864658f, 0.9999968f, -0.002333928f)},
|
{"LeftUpperLeg", new Quaternion(-0.0006682819f, 0.0006864658f, 0.9999968f, -0.002333928f)},
|
||||||
{"Neck", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
|
{"Neck", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
|
||||||
{"RightHandIndex3", new Quaternion(0.5083892f, 0.4911618f, -0.4914584f, 0.5086939f)},
|
{"Right Index Distal", new Quaternion(0.5083892f, 0.4911618f, -0.4914584f, 0.5086939f)},
|
||||||
{"RightHandIndex2", new Quaternion(0.4931984f, 0.5065879f, -0.5067145f, 0.4933202f)},
|
{"Right Index Intermediate", new Quaternion(0.4931984f, 0.5065879f, -0.5067145f, 0.4933202f)},
|
||||||
{"RightHandIndex1", new Quaternion(0.4991491f, 0.5012957f, -0.5008481f, 0.4987026f)},
|
{"Right Index Proximal", new Quaternion(0.4991491f, 0.5012957f, -0.5008481f, 0.4987026f)},
|
||||||
{"RightHandPinky3", new Quaternion(0.4890696f, 0.5154139f, -0.5104482f, 0.4843578f)},
|
{"Right Little Distal", new Quaternion(0.4890696f, 0.5154139f, -0.5104482f, 0.4843578f)},
|
||||||
{"RightHandPinky2", new Quaternion(0.5084175f, 0.495413f, -0.4915423f, 0.5044444f)},
|
{"Right Little Intermediate", new Quaternion(0.5084175f, 0.495413f, -0.4915423f, 0.5044444f)},
|
||||||
{"RightHandPinky1", new Quaternion(0.5069782f, 0.4965974f, -0.4930001f, 0.5033045f)},
|
{"Right Little Proximal", new Quaternion(0.5069782f, 0.4965974f, -0.4930001f, 0.5033045f)},
|
||||||
{"RightHandMiddle3", new Quaternion(0.4867662f, 0.5129694f, -0.5128888f, 0.4866894f)},
|
{"Right Middle Distal", new Quaternion(0.4867662f, 0.5129694f, -0.5128888f, 0.4866894f)},
|
||||||
{"RightHandMiddle2", new Quaternion(0.5167004f, 0.4833596f, -0.4827653f, 0.5160643f)},
|
{"Right Middle Intermediate", new Quaternion(0.5167004f, 0.4833596f, -0.4827653f, 0.5160643f)},
|
||||||
{"RightHandMiddle1", new Quaternion(0.4965845f, 0.5031784f, -0.5033959f, 0.4967981f)},
|
{"Right Middle Proximal", new Quaternion(0.4965845f, 0.5031784f, -0.5033959f, 0.4967981f)},
|
||||||
{"RightHandRing3", new Quaternion(0.4933217f, 0.5102056f, -0.5064691f, 0.4897075f)},
|
{"Right Ring Distal", new Quaternion(0.4933217f, 0.5102056f, -0.5064691f, 0.4897075f)},
|
||||||
{"RightHandRing2", new Quaternion(0.5085972f, 0.494844f, -0.4913519f, 0.505007f)},
|
{"Right Ring Intermediate", new Quaternion(0.5085972f, 0.494844f, -0.4913519f, 0.505007f)},
|
||||||
{"RightHandRing1", new Quaternion(0.502959f, 0.4996676f, -0.4970418f, 0.5003144f)},
|
{"Right Ring Proximal", new Quaternion(0.502959f, 0.4996676f, -0.4970418f, 0.5003144f)},
|
||||||
{"RightHandThumb3", new Quaternion(0.6611374f, 0.2896575f, -0.3616535f, 0.5900872f)},
|
{"Right Thumb Distal", new Quaternion(0.6611374f, 0.2896575f, -0.3616535f, 0.5900872f)},
|
||||||
{"RightHandThumb2", new Quaternion(0.6937408f, 0.1986776f, -0.279922f, 0.6331626f)},
|
{"Right Thumb Intermediate", new Quaternion(0.6937408f, 0.1986776f, -0.279922f, 0.6331626f)},
|
||||||
{"RightHandThumb1", new Quaternion(0.6664271f, 0.2783172f, -0.3505667f, 0.596253f)},
|
{"Right Thumb Proximal", new Quaternion(0.6664271f, 0.2783172f, -0.3505667f, 0.596253f)},
|
||||||
{"RightEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
|
{"RightEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
|
||||||
{"RightFoot", new Quaternion(-0.009482829f, 0.3612484f, 0.9323512f, 0.01144584f)},
|
{"RightFoot", new Quaternion(-0.009482829f, 0.3612484f, 0.9323512f, 0.01144584f)},
|
||||||
{"RightHand", new Quaternion(0.4797273f, 0.5195542f, -0.5279628f, 0.4702987f)},
|
{"RightHand", new Quaternion(0.4797273f, 0.5195542f, -0.5279628f, 0.4702987f)},
|
||||||
{"RightForeArm", new Quaternion(0.4594217f, 0.4594215f, -0.5375242f, 0.5375237f)},
|
{"RightLowerArm", new Quaternion(0.4594217f, 0.4594215f, -0.5375242f, 0.5375237f)},
|
||||||
{"RightLeg", new Quaternion(0.0005446263f, -0.03177159f, 0.9994922f, -0.002395923f)},
|
{"RightLowerLeg", new Quaternion(0.0005446263f, -0.03177159f, 0.9994922f, -0.002395923f)},
|
||||||
{"RightShoulder", new Quaternion(0.3841222f, 0.5257177f, -0.5957286f, 0.4702966f)},
|
{"RightShoulder", new Quaternion(0.3841222f, 0.5257177f, -0.5957286f, 0.4702966f)},
|
||||||
{"RightToeBase", new Quaternion(0.0001034f, 0.7113398f, 0.7027067f, 0.01411251f)},
|
{"RightToes", new Quaternion(0.0001034f, 0.7113398f, 0.7027067f, 0.01411251f)},
|
||||||
{"RightArm", new Quaternion(0.4591419f, 0.4591423f, -0.537763f, 0.5377624f)},
|
{"RightUpperArm", new Quaternion(0.4591419f, 0.4591423f, -0.537763f, 0.5377624f)},
|
||||||
{"RightUpLeg", new Quaternion(0.0006750703f, 0.0008973633f, 0.9999966f, 0.002352045f)},
|
{"RightUpperLeg", new Quaternion(0.0006750703f, 0.0008973633f, 0.9999966f, 0.002352045f)},
|
||||||
{"Spine", new Quaternion(-0.05427956f, 1.508558e-7f, -2.775203e-6f, 0.9985258f)},
|
{"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)},
|
{"UpperChest", 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>();
|
// Humanoid mapping name suffixes for each set of appendages
|
||||||
static Dictionary<string, string> userParentNames = new Dictionary<string, string>();
|
static readonly string[] LEG_MAPPING_SUFFIXES = new string[] {
|
||||||
static Dictionary<string, Quaternion> userAbsoluteRotations = new Dictionary<string, Quaternion>();
|
"UpperLeg",
|
||||||
|
"LowerLeg",
|
||||||
|
"Foot",
|
||||||
|
"Toes",
|
||||||
|
};
|
||||||
|
static readonly string[] ARM_MAPPING_SUFFIXES = new string[] {
|
||||||
|
"Shoulder",
|
||||||
|
"UpperArm",
|
||||||
|
"LowerArm",
|
||||||
|
"Hand",
|
||||||
|
};
|
||||||
|
static readonly string[] HAND_MAPPING_SUFFIXES = new string[] {
|
||||||
|
" Index Distal",
|
||||||
|
" Index Intermediate",
|
||||||
|
" Index Proximal",
|
||||||
|
" Little Distal",
|
||||||
|
" Little Intermediate",
|
||||||
|
" Little Proximal",
|
||||||
|
" Middle Distal",
|
||||||
|
" Middle Intermediate",
|
||||||
|
" Middle Proximal",
|
||||||
|
" Ring Distal",
|
||||||
|
" Ring Intermediate",
|
||||||
|
" Ring Proximal",
|
||||||
|
" Thumb Distal",
|
||||||
|
" Thumb Intermediate",
|
||||||
|
" Thumb Proximal",
|
||||||
|
};
|
||||||
|
|
||||||
|
enum BoneRule {
|
||||||
|
RecommendedUnityVersion,
|
||||||
|
SingleRoot,
|
||||||
|
NoDuplicateMapping,
|
||||||
|
NoAsymmetricalLegMapping,
|
||||||
|
NoAsymmetricalArmMapping,
|
||||||
|
NoAsymmetricalHandMapping,
|
||||||
|
HipsMapped,
|
||||||
|
SpineMapped,
|
||||||
|
SpineDescendantOfHips,
|
||||||
|
ChestMapped,
|
||||||
|
ChestDescendantOfSpine,
|
||||||
|
NeckMapped,
|
||||||
|
HeadMapped,
|
||||||
|
HeadDescendantOfChest,
|
||||||
|
EyesMapped,
|
||||||
|
HipsNotOnGround,
|
||||||
|
HipsSpineChestNotCoincident,
|
||||||
|
TotalBoneCountUnderLimit,
|
||||||
|
BoneRuleEnd,
|
||||||
|
};
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
class UserBoneInformation {
|
||||||
|
public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
|
||||||
|
public string parentName; // parent user bone name
|
||||||
|
public int mappingCount; // number of times this bone is mapped in Humanoid
|
||||||
|
public Vector3 position; // absolute position
|
||||||
|
public Quaternion rotation; // absolute rotation
|
||||||
|
public BoneTreeNode boneTreeNode;
|
||||||
|
|
||||||
|
public UserBoneInformation() {
|
||||||
|
humanName = "";
|
||||||
|
parentName = "";
|
||||||
|
mappingCount = 0;
|
||||||
|
position = new Vector3();
|
||||||
|
rotation = new Quaternion();
|
||||||
|
boneTreeNode = new BoneTreeNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class BoneTreeNode {
|
||||||
|
public string boneName;
|
||||||
|
public List<BoneTreeNode> children = new List<BoneTreeNode>();
|
||||||
|
|
||||||
|
public BoneTreeNode() {}
|
||||||
|
public BoneTreeNode(string name) {
|
||||||
|
boneName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, UserBoneInformation> userBoneInfos = new Dictionary<string, UserBoneInformation>();
|
||||||
|
static Dictionary<string, string> humanoidToUserBoneMappings = new Dictionary<string, string>();
|
||||||
|
static BoneTreeNode userBoneTree = new BoneTreeNode();
|
||||||
|
static Dictionary<BoneRule, string> failedBoneRules = new Dictionary<BoneRule, string>();
|
||||||
|
|
||||||
static string assetPath = "";
|
static string assetPath = "";
|
||||||
static string assetName = "";
|
static string assetName = "";
|
||||||
static HumanDescription humanDescription;
|
static HumanDescription humanDescription;
|
||||||
|
|
||||||
|
|
||||||
[MenuItem("High Fidelity/Export New Avatar")]
|
[MenuItem("High Fidelity/Export New Avatar")]
|
||||||
static void ExportNewAvatar() {
|
static void ExportNewAvatar() {
|
||||||
ExportSelectedAvatar(false);
|
ExportSelectedAvatar(false);
|
||||||
|
@ -145,6 +274,11 @@ class AvatarExporter : MonoBehaviour {
|
||||||
ExportSelectedAvatar(true);
|
ExportSelectedAvatar(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MenuItem("High Fidelity/About")]
|
||||||
|
static void About() {
|
||||||
|
EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok");
|
||||||
|
}
|
||||||
|
|
||||||
static void ExportSelectedAvatar(bool updateAvatar) {
|
static void ExportSelectedAvatar(bool updateAvatar) {
|
||||||
string[] guids = Selection.assetGUIDs;
|
string[] guids = Selection.assetGUIDs;
|
||||||
if (guids.Length != 1) {
|
if (guids.Length != 1) {
|
||||||
|
@ -163,15 +297,59 @@ class AvatarExporter : MonoBehaviour {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (modelImporter.animationType != ModelImporterAnimationType.Human) {
|
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");
|
EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid in " +
|
||||||
|
" the Rig section of it's Inspector window.", "Ok");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
humanDescription = modelImporter.humanDescription;
|
humanDescription = modelImporter.humanDescription;
|
||||||
if (!SetJointMappingsAndParentNames()) {
|
SetUserBoneInformation();
|
||||||
|
|
||||||
|
// 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 as warnings in the dialog
|
||||||
|
string boneErrors = "";
|
||||||
|
string boneWarnings = "";
|
||||||
|
foreach (var failedBoneRule in failedBoneRules) {
|
||||||
|
if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) {
|
||||||
|
boneErrors += failedBoneRule.Value + "\n\n";
|
||||||
|
} else {
|
||||||
|
boneWarnings += failedBoneRule.Value + "\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(boneWarnings)) {
|
||||||
|
boneErrors = "Errors:\n\n" + boneErrors;
|
||||||
|
boneErrors += "Warnings:\n\n" + boneWarnings;
|
||||||
|
}
|
||||||
|
// remove ending newlines from the last rule failure string that was added above
|
||||||
|
boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n"));
|
||||||
|
EditorUtility.DisplayDialog("Error", boneErrors, "Ok");
|
||||||
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
|
||||||
|
@ -237,10 +415,11 @@ class AvatarExporter : MonoBehaviour {
|
||||||
modelImporter.animationType = ModelImporterAnimationType.Human;
|
modelImporter.animationType = ModelImporterAnimationType.Human;
|
||||||
EditorUtility.SetDirty(modelImporter);
|
EditorUtility.SetDirty(modelImporter);
|
||||||
modelImporter.SaveAndReimport();
|
modelImporter.SaveAndReimport();
|
||||||
humanDescription = modelImporter.humanDescription;
|
|
||||||
|
|
||||||
// redo joint mappings and parent names due to the fbx change
|
// redo parent names, joint mappings, and user bone positions due to the fbx change
|
||||||
SetJointMappingsAndParentNames();
|
// as well as re-check the bone rules for failures
|
||||||
|
humanDescription = modelImporter.humanDescription;
|
||||||
|
SetUserBoneInformation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -277,19 +456,30 @@ 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);
|
WriteFST(exportFstPath, projectName);
|
||||||
|
|
||||||
|
// display success dialog with any bone rule warnings
|
||||||
|
string successDialog = "Avatar successfully updated!";
|
||||||
|
if (!string.IsNullOrEmpty(boneWarnings)) {
|
||||||
|
successDialog += "\n\nWarnings:\n" + boneWarnings;
|
||||||
|
}
|
||||||
|
EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
|
||||||
} else { // Export New Avatar menu option
|
} else { // Export New Avatar menu option
|
||||||
// create High Fidelity Projects folder in user documents folder if it doesn't exist
|
// create High Fidelity Projects folder in user documents folder if it doesn't exist
|
||||||
if (!Directory.Exists(hifiFolder)) {
|
if (!Directory.Exists(hifiFolder)) {
|
||||||
Directory.CreateDirectory(hifiFolder);
|
Directory.CreateDirectory(hifiFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(boneWarnings)) {
|
||||||
|
boneWarnings = 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, OnExportProjectWindowClose);
|
window.Init(hifiFolder, boneWarnings, OnExportProjectWindowClose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void OnExportProjectWindowClose(string projectDirectory, string projectName) {
|
static void OnExportProjectWindowClose(string projectDirectory, string projectName, string warnings) {
|
||||||
// copy the fbx from the Unity Assets folder to the project directory
|
// copy the fbx from the Unity Assets folder to the project directory
|
||||||
string exportModelPath = projectDirectory + assetName + ".fbx";
|
string exportModelPath = projectDirectory + assetName + ".fbx";
|
||||||
File.Copy(assetPath, exportModelPath);
|
File.Copy(assetPath, exportModelPath);
|
||||||
|
@ -304,94 +494,19 @@ class AvatarExporter : MonoBehaviour {
|
||||||
string exportFstPath = projectDirectory + "avatar.fst";
|
string exportFstPath = projectDirectory + "avatar.fst";
|
||||||
WriteFST(exportFstPath, projectName);
|
WriteFST(exportFstPath, projectName);
|
||||||
|
|
||||||
// remove any double slashes in texture directory path and warn user to copy external textures over
|
// 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
|
||||||
texturesDirectory = texturesDirectory.Replace("\\\\", "\\");
|
texturesDirectory = texturesDirectory.Replace("\\\\", "\\");
|
||||||
EditorUtility.DisplayDialog("Warning", "If you are using any external textures with your model, " +
|
string successDialog = "Avatar successfully exported!\n\n";
|
||||||
"please copy those textures to " + texturesDirectory, "Ok");
|
if (warnings != EMPTY_WARNING_TEXT) {
|
||||||
|
successDialog += "Warnings:\n" + warnings;
|
||||||
}
|
}
|
||||||
|
successDialog += "Note: If you are using any external textures with your model, " +
|
||||||
static bool SetJointMappingsAndParentNames() {
|
"please copy those textures to " + texturesDirectory;
|
||||||
userParentNames.Clear();
|
EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
|
||||||
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;
|
|
||||||
}
|
|
||||||
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) {
|
static void WriteFST(string exportFstPath, string projectName) {
|
||||||
userAbsoluteRotations.Clear();
|
|
||||||
|
|
||||||
// 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 = " +
|
||||||
|
@ -403,49 +518,53 @@ class AvatarExporter : MonoBehaviour {
|
||||||
}
|
}
|
||||||
|
|
||||||
// write out joint mappings to fst file
|
// write out joint mappings to fst file
|
||||||
foreach (var jointMapping in userBoneToHumanoidMappings) {
|
foreach (var userBoneInfo in userBoneInfos) {
|
||||||
string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[jointMapping.Value];
|
if (userBoneInfo.Value.HasHumanMapping()) {
|
||||||
File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + jointMapping.Key + "\n");
|
string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.Value.humanName];
|
||||||
|
File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + userBoneInfo.Key + "\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate and write out joint rotation offsets to fst file
|
// calculate and write out joint rotation offsets to fst file
|
||||||
SkeletonBone[] skeletonMap = humanDescription.skeleton;
|
SkeletonBone[] skeletonMap = humanDescription.skeleton;
|
||||||
foreach (SkeletonBone userBone in skeletonMap) {
|
foreach (SkeletonBone userBone in skeletonMap) {
|
||||||
string userBoneName = userBone.name;
|
string userBoneName = userBone.name;
|
||||||
Quaternion userBoneRotation = userBone.rotation;
|
UserBoneInformation userBoneInfo;
|
||||||
|
if (!userBoneInfos.TryGetValue(userBoneName, out userBoneInfo)) {
|
||||||
string parentName;
|
continue;
|
||||||
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 userBoneRotation = userBone.rotation;
|
||||||
Quaternion jointOffset = new Quaternion();
|
string parentName = userBoneInfo.parentName;
|
||||||
string humanName, outputJointName = "";
|
if (parentName == "root") {
|
||||||
if (userBoneToHumanoidMappings.TryGetValue(userBoneName, out humanName)) {
|
// if the parent is root then use bone's rotation
|
||||||
outputJointName = HUMANOID_TO_HIFI_JOINT_NAME[humanName];
|
userBoneInfo.rotation = userBoneRotation;
|
||||||
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 {
|
} else {
|
||||||
|
// otherwise multiply bone's rotation by parent bone's absolute rotation
|
||||||
|
userBoneInfo.rotation = userBoneInfos[parentName].rotation * userBoneRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate joint rotation offsets for both humanoid-mapped bones as well as extra unmapped bones
|
||||||
|
Quaternion jointOffset = new Quaternion();
|
||||||
|
string outputJointName = "";
|
||||||
|
if (userBoneInfo.HasHumanMapping()) {
|
||||||
|
outputJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.humanName];
|
||||||
|
Quaternion rotation = REFERENCE_ROTATIONS[userBoneInfo.humanName];
|
||||||
|
jointOffset = Quaternion.Inverse(userBoneInfo.rotation) * rotation;
|
||||||
|
} else {
|
||||||
|
outputJointName = userBoneName;
|
||||||
|
jointOffset = Quaternion.Inverse(userBoneInfo.rotation);
|
||||||
|
string lastRequiredParent = FindLastRequiredAncestorBone(userBoneName);
|
||||||
|
if (lastRequiredParent != "root") {
|
||||||
// take the previous offset and multiply it by the current local when we have an extra joint
|
// 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]];
|
string lastRequiredParentHumanName = userBoneInfos[lastRequiredParent].humanName;
|
||||||
Quaternion lastRequiredParentRotation = referenceAbsoluteRotations[lastRequiredParentHifiName];
|
Quaternion lastRequiredParentRotation = REFERENCE_ROTATIONS[lastRequiredParentHumanName];
|
||||||
jointOffset = Quaternion.Inverse(userAbsoluteRotations[userBoneName]) * lastRequiredParentRotation;
|
jointOffset *= lastRequiredParentRotation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst
|
// swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst
|
||||||
if (outputJointName != "") {
|
if (!string.IsNullOrEmpty(outputJointName)) {
|
||||||
jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
|
jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
|
||||||
File.AppendAllText(exportFstPath, "jointRotationOffset = " + outputJointName + " = (" + jointOffset.x + ", " +
|
File.AppendAllText(exportFstPath, "jointRotationOffset = " + outputJointName + " = (" + jointOffset.x + ", " +
|
||||||
jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n");
|
jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n");
|
||||||
|
@ -456,47 +575,325 @@ class AvatarExporter : MonoBehaviour {
|
||||||
System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath);
|
System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SetParentNames(Transform modelBone, Dictionary<string, string> parentNames) {
|
static void SetUserBoneInformation() {
|
||||||
for (int i = 0; i < modelBone.childCount; i++) {
|
userBoneInfos.Clear();
|
||||||
SetParentNames(modelBone.GetChild(i), parentNames);
|
humanoidToUserBoneMappings.Clear();
|
||||||
|
userBoneTree = new BoneTreeNode();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// iterate over Humanoid bones and update user bone info to increase human mapping counts for each bone
|
||||||
|
// as well as set their Humanoid name and build a Humanoid to user bone mapping
|
||||||
|
HumanBone[] boneMap = humanDescription.human;
|
||||||
|
foreach (HumanBone bone in boneMap) {
|
||||||
|
string humanName = bone.humanName;
|
||||||
|
string userBoneName = bone.boneName;
|
||||||
|
string hifiJointName;
|
||||||
|
if (userBoneInfos.ContainsKey(userBoneName)) {
|
||||||
|
++userBoneInfos[userBoneName].mappingCount;
|
||||||
|
if (HUMANOID_TO_HIFI_JOINT_NAME.TryGetValue(humanName, out hifiJointName)) {
|
||||||
|
userBoneInfos[userBoneName].humanName = humanName;
|
||||||
|
humanoidToUserBoneMappings.Add(humanName, userBoneName);
|
||||||
}
|
}
|
||||||
if (modelBone.parent != null) {
|
|
||||||
parentNames.Add(modelBone.name, modelBone.parent.name);
|
|
||||||
} else {
|
|
||||||
parentNames.Add(modelBone.name, "root");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static string FindLastRequiredParentBone(string currentBone) {
|
// generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar
|
||||||
|
SetFailedBoneRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<MeshRenderer>() != null || gameObject.GetComponent<SkinnedMeshRenderer>() != null;
|
||||||
|
bool light = gameObject.GetComponent<Light>() != null;
|
||||||
|
bool camera = gameObject.GetComponent<Camera>() != 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) {
|
||||||
|
UserBoneInformation userBoneInfo = new UserBoneInformation();
|
||||||
|
userBoneInfo.position = modelBone.position; // bone's absolute position
|
||||||
|
|
||||||
|
string boneName = modelBone.name;
|
||||||
|
if (modelBone.parent == null) {
|
||||||
|
// if no parent then this is actual root bone node of the user avatar, so consider it's parent as "root"
|
||||||
|
userBoneTree = new BoneTreeNode(boneName); // initialize root of tree
|
||||||
|
userBoneInfo.parentName = "root";
|
||||||
|
userBoneInfo.boneTreeNode = userBoneTree;
|
||||||
|
} else {
|
||||||
|
// otherwise add this bone node as a child to it's parent's children list
|
||||||
|
string parentName = modelBone.parent.name;
|
||||||
|
BoneTreeNode boneTreeNode = new BoneTreeNode(boneName);
|
||||||
|
userBoneInfos[parentName].boneTreeNode.children.Add(boneTreeNode);
|
||||||
|
userBoneInfo.parentName = parentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
userBoneInfos.Add(boneName, userBoneInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// recurse over transform node's children
|
||||||
|
for (int i = 0; i < modelBone.childCount; ++i) {
|
||||||
|
TraverseUserBoneTree(modelBone.GetChild(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string FindLastRequiredAncestorBone(string currentBone) {
|
||||||
string result = currentBone;
|
string result = currentBone;
|
||||||
while (result != "root" && !userBoneToHumanoidMappings.ContainsKey(result)) {
|
// iterating upward through user bone info parent names, find the first ancestor bone that is mapped in Humanoid
|
||||||
result = userParentNames[result];
|
while (result != "root" && userBoneInfos.ContainsKey(result) && !userBoneInfos[result].HasHumanMapping()) {
|
||||||
|
result = userBoneInfos[result].parentName;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void SetFailedBoneRules() {
|
||||||
|
failedBoneRules.Clear();
|
||||||
|
|
||||||
|
string hipsUserBone = "";
|
||||||
|
string spineUserBone = "";
|
||||||
|
string chestUserBone = "";
|
||||||
|
string headUserBone = "";
|
||||||
|
|
||||||
|
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:
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BoneRule.SingleRoot:
|
||||||
|
// bone 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.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BoneRule.NoDuplicateMapping:
|
||||||
|
// bone 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;
|
||||||
|
} else {
|
||||||
|
failedBoneRules.Add(boneRule, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BoneRule.NoAsymmetricalLegMapping:
|
||||||
|
CheckAsymmetricalMappingRule(boneRule, LEG_MAPPING_SUFFIXES, "leg");
|
||||||
|
break;
|
||||||
|
case BoneRule.NoAsymmetricalArmMapping:
|
||||||
|
CheckAsymmetricalMappingRule(boneRule, ARM_MAPPING_SUFFIXES, "arm");
|
||||||
|
break;
|
||||||
|
case BoneRule.NoAsymmetricalHandMapping:
|
||||||
|
CheckAsymmetricalMappingRule(boneRule, HAND_MAPPING_SUFFIXES, "hand");
|
||||||
|
break;
|
||||||
|
case BoneRule.HipsMapped:
|
||||||
|
hipsUserBone = CheckHumanBoneMappingRule(boneRule, "Hips");
|
||||||
|
break;
|
||||||
|
case BoneRule.SpineMapped:
|
||||||
|
spineUserBone = CheckHumanBoneMappingRule(boneRule, "Spine");
|
||||||
|
break;
|
||||||
|
case BoneRule.SpineDescendantOfHips:
|
||||||
|
CheckUserBoneDescendantOfHumanRule(boneRule, spineUserBone, "Hips");
|
||||||
|
break;
|
||||||
|
case BoneRule.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 = "";
|
||||||
|
if (!string.IsNullOrEmpty(spineUserBone)) {
|
||||||
|
BoneTreeNode spineTreeNode = userBoneInfos[spineUserBone].boneTreeNode;
|
||||||
|
if (spineTreeNode.children.Count == 1) {
|
||||||
|
spineChild = spineTreeNode.children[0].boneName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failedBoneRules.Add(boneRule, "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.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BoneRule.ChestDescendantOfSpine:
|
||||||
|
CheckUserBoneDescendantOfHumanRule(boneRule, chestUserBone, "Spine");
|
||||||
|
break;
|
||||||
|
case BoneRule.NeckMapped:
|
||||||
|
CheckHumanBoneMappingRule(boneRule, "Neck");
|
||||||
|
break;
|
||||||
|
case BoneRule.HeadMapped:
|
||||||
|
headUserBone = CheckHumanBoneMappingRule(boneRule, "Head");
|
||||||
|
break;
|
||||||
|
case BoneRule.HeadDescendantOfChest:
|
||||||
|
CheckUserBoneDescendantOfHumanRule(boneRule, headUserBone, "Chest");
|
||||||
|
break;
|
||||||
|
case BoneRule.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.");
|
||||||
|
} else if (!leftEyeMapped && rightEyeMapped) {
|
||||||
|
failedBoneRules.Add(boneRule, "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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BoneRule.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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BoneRule.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) &&
|
||||||
|
!string.IsNullOrEmpty(hipsUserBone)) {
|
||||||
|
UserBoneInformation spineBoneInfo = userBoneInfos[spineUserBone];
|
||||||
|
UserBoneInformation chestBoneInfo = userBoneInfos[chestUserBone];
|
||||||
|
Vector3 hipsToSpine = hipsPosition - spineBoneInfo.position;
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BoneRule.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 + ").");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string CheckHumanBoneMappingRule(BoneRule boneRule, string humanBoneName) {
|
||||||
|
string userBoneName = "";
|
||||||
|
// bone 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.");
|
||||||
|
}
|
||||||
|
return userBoneName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CheckUserBoneDescendantOfHumanRule(BoneRule boneRule, string userBoneName, string descendantOfHumanName) {
|
||||||
|
if (string.IsNullOrEmpty(userBoneName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string descendantOfUserBoneName = "";
|
||||||
|
if (!humanoidToUserBoneMappings.TryGetValue(descendantOfHumanName, out descendantOfUserBoneName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string userBone = userBoneName;
|
||||||
|
string ancestorUserBone = "";
|
||||||
|
UserBoneInformation userBoneInfo = new UserBoneInformation();
|
||||||
|
// iterate upward from user bone through user bone info parent names until root
|
||||||
|
// is reached or the ancestor bone name matches the target descendant of name
|
||||||
|
while (ancestorUserBone != "root") {
|
||||||
|
if (userBoneInfos.TryGetValue(userBone, out userBoneInfo)) {
|
||||||
|
ancestorUserBone = userBoneInfo.parentName;
|
||||||
|
if (ancestorUserBone == descendantOfUserBoneName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
userBone = ancestorUserBone;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 + ").");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CheckAsymmetricalMappingRule(BoneRule boneRule, string[] mappingSuffixes, string appendage) {
|
||||||
|
int leftCount = 0;
|
||||||
|
int rightCount = 0;
|
||||||
|
// add Left/Right to each mapping suffix to make Humanoid mapping names,
|
||||||
|
// and count the number of bones mapped in Humanoid on each side
|
||||||
|
foreach (string mappingSuffix in mappingSuffixes) {
|
||||||
|
string leftMapping = "Left" + mappingSuffix;
|
||||||
|
string rightMapping = "Right" + mappingSuffix;
|
||||||
|
if (humanoidToUserBoneMappings.ContainsKey(leftMapping)) {
|
||||||
|
++leftCount;
|
||||||
|
}
|
||||||
|
if (humanoidToUserBoneMappings.ContainsKey(rightMapping)) {
|
||||||
|
++rightCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// bone 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 + ").");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExportProjectWindow : EditorWindow {
|
class ExportProjectWindow : EditorWindow {
|
||||||
const int MIN_WIDTH = 450;
|
const int WINDOW_WIDTH = 500;
|
||||||
const int MIN_HEIGHT = 250;
|
const int WINDOW_HEIGHT = 460;
|
||||||
const int BUTTON_FONT_SIZE = 16;
|
const int BUTTON_FONT_SIZE = 16;
|
||||||
const int LABEL_FONT_SIZE = 16;
|
const int LABEL_FONT_SIZE = 16;
|
||||||
const int TEXT_FIELD_FONT_SIZE = 14;
|
const int TEXT_FIELD_FONT_SIZE = 14;
|
||||||
const int TEXT_FIELD_HEIGHT = 20;
|
const int TEXT_FIELD_HEIGHT = 20;
|
||||||
const int ERROR_FONT_SIZE = 12;
|
const int ERROR_FONT_SIZE = 12;
|
||||||
|
const int WARNING_SCROLL_HEIGHT = 170;
|
||||||
|
const string EMPTY_ERROR_TEXT = "None\n";
|
||||||
|
|
||||||
string projectName = "";
|
string projectName = "";
|
||||||
string projectLocation = "";
|
string projectLocation = "";
|
||||||
string projectDirectory = "";
|
string projectDirectory = "";
|
||||||
string errorLabel = "\n";
|
string errorText = EMPTY_ERROR_TEXT;
|
||||||
|
string warningText = "";
|
||||||
|
Vector2 warningScrollPosition = new Vector2(0, 0);
|
||||||
|
|
||||||
public delegate void OnCloseDelegate(string projectDirectory, string projectName);
|
public delegate void OnCloseDelegate(string projectDirectory, string projectName, string warnings);
|
||||||
OnCloseDelegate onCloseCallback;
|
OnCloseDelegate onCloseCallback;
|
||||||
|
|
||||||
public void Init(string initialPath, OnCloseDelegate closeCallback) {
|
public void Init(string initialPath, string warnings, OnCloseDelegate closeCallback) {
|
||||||
minSize = new Vector2(MIN_WIDTH, MIN_HEIGHT);
|
minSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||||
|
maxSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||||
titleContent.text = "Export New Avatar";
|
titleContent.text = "Export New Avatar";
|
||||||
projectLocation = initialPath;
|
projectLocation = initialPath;
|
||||||
|
warningText = warnings;
|
||||||
onCloseCallback = closeCallback;
|
onCloseCallback = closeCallback;
|
||||||
ShowUtility();
|
ShowUtility();
|
||||||
}
|
}
|
||||||
|
@ -513,6 +910,9 @@ class ExportProjectWindow : EditorWindow {
|
||||||
GUIStyle errorStyle = new GUIStyle(GUI.skin.label);
|
GUIStyle errorStyle = new GUIStyle(GUI.skin.label);
|
||||||
errorStyle.fontSize = ERROR_FONT_SIZE;
|
errorStyle.fontSize = ERROR_FONT_SIZE;
|
||||||
errorStyle.normal.textColor = Color.red;
|
errorStyle.normal.textColor = Color.red;
|
||||||
|
errorStyle.wordWrap = true;
|
||||||
|
GUIStyle warningStyle = new GUIStyle(errorStyle);
|
||||||
|
warningStyle.normal.textColor = Color.yellow;
|
||||||
|
|
||||||
GUILayout.Space(10);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
|
@ -534,10 +934,20 @@ class ExportProjectWindow : EditorWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Red error label text to display any issues under text fields and Browse button
|
// Red error label text to display any file-related errors
|
||||||
GUILayout.Label(errorLabel, errorStyle);
|
GUILayout.Label("Error:", errorStyle);
|
||||||
|
GUILayout.Label(errorText, errorStyle);
|
||||||
|
|
||||||
GUILayout.Space(20);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
GUILayout.Label(warningText, warningStyle);
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
|
||||||
|
GUILayout.Space(10);
|
||||||
|
|
||||||
// Export button which will verify project folder can actually be created
|
// Export button which will verify project folder can actually be created
|
||||||
// before closing popup window and calling back to initiate the export
|
// before closing popup window and calling back to initiate the export
|
||||||
|
@ -546,7 +956,7 @@ class ExportProjectWindow : EditorWindow {
|
||||||
export = true;
|
export = true;
|
||||||
if (!CheckForErrors(true)) {
|
if (!CheckForErrors(true)) {
|
||||||
Close();
|
Close();
|
||||||
onCloseCallback(projectDirectory, projectName);
|
onCloseCallback(projectDirectory, projectName, warningText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,12 +972,12 @@ class ExportProjectWindow : EditorWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CheckForErrors(bool exporting) {
|
bool CheckForErrors(bool exporting) {
|
||||||
errorLabel = "\n"; // default to no error
|
errorText = EMPTY_ERROR_TEXT; // default to None if no errors found
|
||||||
projectDirectory = projectLocation + "\\" + projectName + "\\";
|
projectDirectory = projectLocation + "\\" + projectName + "\\";
|
||||||
if (projectName.Length > 0) {
|
if (projectName.Length > 0) {
|
||||||
// new project must have a unique folder name since the folder will be created for it
|
// new project must have a unique folder name since the folder will be created for it
|
||||||
if (Directory.Exists(projectDirectory)) {
|
if (Directory.Exists(projectDirectory)) {
|
||||||
errorLabel = "A folder with the name " + projectName +
|
errorText = "A folder with the name " + projectName +
|
||||||
" already exists at that location.\nPlease choose a different project name or location.";
|
" already exists at that location.\nPlease choose a different project name or location.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -575,7 +985,7 @@ class ExportProjectWindow : EditorWindow {
|
||||||
if (projectLocation.Length > 0) {
|
if (projectLocation.Length > 0) {
|
||||||
// before clicking Export we can verify that the project location at least starts with a drive
|
// 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] != ':') {
|
if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') {
|
||||||
errorLabel = "Project location is invalid. Please choose a different project location.\n";
|
errorText = "Project location is invalid. Please choose a different project location.\n";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -583,16 +993,16 @@ class ExportProjectWindow : EditorWindow {
|
||||||
// when exporting, project name and location must both be defined, and project location must
|
// 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)
|
// be valid and accessible (we attempt to create the project folder at this time to verify this)
|
||||||
if (projectName.Length == 0) {
|
if (projectName.Length == 0) {
|
||||||
errorLabel = "Please define a project name.\n";
|
errorText = "Please define a project name.\n";
|
||||||
return true;
|
return true;
|
||||||
} else if (projectLocation.Length == 0) {
|
} else if (projectLocation.Length == 0) {
|
||||||
errorLabel = "Please define a project location.\n";
|
errorText = "Please define a project location.\n";
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
Directory.CreateDirectory(projectDirectory);
|
Directory.CreateDirectory(projectDirectory);
|
||||||
} catch {
|
} catch {
|
||||||
errorLabel = "Project location is invalid. Please choose a different project location.\n";
|
errorText = "Project location is invalid. Please choose a different project location.\n";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
High Fidelity, Inc.
|
||||||
|
Avatar Exporter
|
||||||
|
Version 0.1
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
To create a new avatar project:
|
To create a new avatar project:
|
||||||
|
|
Binary file not shown.
Loading…
Reference in a new issue