mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 05:17:02 +02:00
Merge pull request #15270 from dback2/avatarExporterWarningFixes
Case 20964, 21758: Avatar Exporter v0.4.1 - bone rule fixes
This commit is contained in:
commit
8de75604cf
3 changed files with 158 additions and 72 deletions
|
@ -17,9 +17,10 @@ using System.Text.RegularExpressions;
|
||||||
|
|
||||||
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.4.0";
|
static readonly string AVATAR_EXPORTER_VERSION = "0.4.1";
|
||||||
|
|
||||||
static readonly float HIPS_GROUND_MIN_Y = 0.01f;
|
static readonly float HIPS_MIN_Y_PERCENT_OF_HEIGHT = 0.03f;
|
||||||
|
static readonly float BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT = -0.15f;
|
||||||
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";
|
||||||
|
@ -231,7 +232,8 @@ class AvatarExporter : MonoBehaviour {
|
||||||
HeadMapped,
|
HeadMapped,
|
||||||
HeadDescendantOfChest,
|
HeadDescendantOfChest,
|
||||||
EyesMapped,
|
EyesMapped,
|
||||||
HipsNotOnGround,
|
HipsNotAtBottom,
|
||||||
|
ExtentsNotBelowGround,
|
||||||
HipsSpineChestNotCoincident,
|
HipsSpineChestNotCoincident,
|
||||||
TotalBoneCountUnderLimit,
|
TotalBoneCountUnderLimit,
|
||||||
AvatarRuleEnd,
|
AvatarRuleEnd,
|
||||||
|
@ -247,18 +249,26 @@ class AvatarExporter : MonoBehaviour {
|
||||||
class UserBoneInformation {
|
class UserBoneInformation {
|
||||||
public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
|
public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
|
||||||
public string parentName; // parent user bone name
|
public string parentName; // parent user bone name
|
||||||
|
public BoneTreeNode boneTreeNode; // node within the user bone tree
|
||||||
public int mappingCount; // number of times this bone is mapped in Humanoid
|
public int mappingCount; // number of times this bone is mapped in Humanoid
|
||||||
public Vector3 position; // absolute position
|
public Vector3 position; // absolute position
|
||||||
public Quaternion rotation; // absolute rotation
|
public Quaternion rotation; // absolute rotation
|
||||||
public BoneTreeNode boneTreeNode;
|
|
||||||
|
|
||||||
public UserBoneInformation() {
|
public UserBoneInformation() {
|
||||||
humanName = "";
|
humanName = "";
|
||||||
parentName = "";
|
parentName = "";
|
||||||
|
boneTreeNode = new BoneTreeNode();
|
||||||
mappingCount = 0;
|
mappingCount = 0;
|
||||||
position = new Vector3();
|
position = new Vector3();
|
||||||
rotation = new Quaternion();
|
rotation = new Quaternion();
|
||||||
boneTreeNode = new BoneTreeNode();
|
}
|
||||||
|
public UserBoneInformation(string parent, BoneTreeNode treeNode, Vector3 pos) {
|
||||||
|
humanName = "";
|
||||||
|
parentName = parent;
|
||||||
|
boneTreeNode = treeNode;
|
||||||
|
mappingCount = 0;
|
||||||
|
position = pos;
|
||||||
|
rotation = new Quaternion();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); }
|
public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); }
|
||||||
|
@ -266,11 +276,13 @@ class AvatarExporter : MonoBehaviour {
|
||||||
|
|
||||||
class BoneTreeNode {
|
class BoneTreeNode {
|
||||||
public string boneName;
|
public string boneName;
|
||||||
|
public string parentName;
|
||||||
public List<BoneTreeNode> children = new List<BoneTreeNode>();
|
public List<BoneTreeNode> children = new List<BoneTreeNode>();
|
||||||
|
|
||||||
public BoneTreeNode() {}
|
public BoneTreeNode() {}
|
||||||
public BoneTreeNode(string name) {
|
public BoneTreeNode(string name, string parent) {
|
||||||
boneName = name;
|
boneName = name;
|
||||||
|
parentName = parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -732,9 +744,11 @@ class AvatarExporter : MonoBehaviour {
|
||||||
|
|
||||||
// instantiate a game object of the user avatar to traverse the bone tree to gather
|
// 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
|
// bone parents and positions as well as build a bone tree, then destroy it
|
||||||
GameObject assetGameObject = (GameObject)Instantiate(avatarResource);
|
GameObject avatarGameObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity);
|
||||||
TraverseUserBoneTree(assetGameObject.transform);
|
TraverseUserBoneTree(avatarGameObject.transform, userBoneTree);
|
||||||
DestroyImmediate(assetGameObject);
|
Bounds bounds = AvatarUtilities.GetAvatarBounds(avatarGameObject);
|
||||||
|
float height = AvatarUtilities.GetAvatarHeight(avatarGameObject);
|
||||||
|
DestroyImmediate(avatarGameObject);
|
||||||
|
|
||||||
// iterate over Humanoid bones and update user bone info to increase human mapping counts for each bone
|
// 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
|
// as well as set their Humanoid name and build a Humanoid to user bone mapping
|
||||||
|
@ -753,10 +767,10 @@ class AvatarExporter : MonoBehaviour {
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate the list of avatar rule failure strings for any avatar rules that are not satisfied by this avatar
|
// generate the list of avatar rule failure strings for any avatar rules that are not satisfied by this avatar
|
||||||
SetFailedAvatarRules();
|
SetFailedAvatarRules(bounds, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void TraverseUserBoneTree(Transform modelBone) {
|
static void TraverseUserBoneTree(Transform modelBone, BoneTreeNode boneTreeNode) {
|
||||||
GameObject gameObject = modelBone.gameObject;
|
GameObject gameObject = modelBone.gameObject;
|
||||||
|
|
||||||
// check if this transform is a node containing mesh, light, or camera instead of a bone
|
// check if this transform is a node containing mesh, light, or camera instead of a bone
|
||||||
|
@ -770,33 +784,52 @@ class AvatarExporter : MonoBehaviour {
|
||||||
if (mesh) {
|
if (mesh) {
|
||||||
Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials;
|
Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials;
|
||||||
StoreMaterialData(materials);
|
StoreMaterialData(materials);
|
||||||
|
|
||||||
|
// ensure branches within the transform hierarchy that contain meshes are removed from the user bone tree
|
||||||
|
Transform ancestorBone = modelBone;
|
||||||
|
string previousBoneName = "";
|
||||||
|
// find the name of the root child bone that this mesh is underneath
|
||||||
|
while (ancestorBone != null) {
|
||||||
|
if (ancestorBone.parent == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
previousBoneName = ancestorBone.name;
|
||||||
|
ancestorBone = ancestorBone.parent;
|
||||||
|
}
|
||||||
|
// remove the bone tree node from root's children for the root child bone that has mesh children
|
||||||
|
if (!string.IsNullOrEmpty(previousBoneName)) {
|
||||||
|
foreach (BoneTreeNode rootChild in userBoneTree.children) {
|
||||||
|
if (rootChild.boneName == previousBoneName) {
|
||||||
|
userBoneTree.children.Remove(rootChild);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (!light && !camera) {
|
} 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
|
// 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
|
|
||||||
|
|
||||||
string boneName = modelBone.name;
|
string boneName = modelBone.name;
|
||||||
if (modelBone.parent == null) {
|
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"
|
// if no parent then this is actual root bone node of the user avatar, so consider it's parent as "root"
|
||||||
boneName = GetRootBoneName(); // ensure we use the root bone name from the skeleton list for consistency
|
boneName = GetRootBoneName(); // ensure we use the root bone name from the skeleton list for consistency
|
||||||
userBoneTree = new BoneTreeNode(boneName); // initialize root of tree
|
boneTreeNode.boneName = boneName;
|
||||||
userBoneInfo.parentName = "root";
|
boneTreeNode.parentName = "root";
|
||||||
userBoneInfo.boneTreeNode = userBoneTree;
|
|
||||||
} else {
|
} else {
|
||||||
// otherwise add this bone node as a child to it's parent's children list
|
// otherwise add this bone node as a child to it's parent's children list
|
||||||
// if its a child of the root bone, use the root bone name from the skeleton list as the parent for consistency
|
// if its a child of the root bone, use the root bone name from the skeleton list as the parent for consistency
|
||||||
string parentName = modelBone.parent.parent == null ? GetRootBoneName() : modelBone.parent.name;
|
string parentName = modelBone.parent.parent == null ? GetRootBoneName() : modelBone.parent.name;
|
||||||
BoneTreeNode boneTreeNode = new BoneTreeNode(boneName);
|
BoneTreeNode node = new BoneTreeNode(boneName, parentName);
|
||||||
userBoneInfos[parentName].boneTreeNode.children.Add(boneTreeNode);
|
boneTreeNode.children.Add(node);
|
||||||
userBoneInfo.parentName = parentName;
|
boneTreeNode = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vector3 bonePosition = modelBone.position; // bone's absolute position in avatar space
|
||||||
|
UserBoneInformation userBoneInfo = new UserBoneInformation(boneTreeNode.parentName, boneTreeNode, bonePosition);
|
||||||
userBoneInfos.Add(boneName, userBoneInfo);
|
userBoneInfos.Add(boneName, userBoneInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// recurse over transform node's children
|
// recurse over transform node's children
|
||||||
for (int i = 0; i < modelBone.childCount; ++i) {
|
for (int i = 0; i < modelBone.childCount; ++i) {
|
||||||
TraverseUserBoneTree(modelBone.GetChild(i));
|
TraverseUserBoneTree(modelBone.GetChild(i), boneTreeNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -840,7 +873,7 @@ class AvatarExporter : MonoBehaviour {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SetFailedAvatarRules() {
|
static void SetFailedAvatarRules(Bounds avatarBounds, float avatarHeight) {
|
||||||
failedAvatarRules.Clear();
|
failedAvatarRules.Clear();
|
||||||
|
|
||||||
string hipsUserBone = "";
|
string hipsUserBone = "";
|
||||||
|
@ -905,18 +938,29 @@ class AvatarExporter : MonoBehaviour {
|
||||||
break;
|
break;
|
||||||
case AvatarRule.ChestMapped:
|
case AvatarRule.ChestMapped:
|
||||||
if (!humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) {
|
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
|
// check to see if there is an unmapped child of Spine that we can suggest to be mapped to Chest
|
||||||
string spineChild = "";
|
string chestMappingCandidate = "";
|
||||||
if (!string.IsNullOrEmpty(spineUserBone)) {
|
if (!string.IsNullOrEmpty(spineUserBone)) {
|
||||||
BoneTreeNode spineTreeNode = userBoneInfos[spineUserBone].boneTreeNode;
|
BoneTreeNode spineTreeNode = userBoneInfos[spineUserBone].boneTreeNode;
|
||||||
if (spineTreeNode.children.Count == 1) {
|
foreach (BoneTreeNode spineChildTreeNode in spineTreeNode.children) {
|
||||||
spineChild = spineTreeNode.children[0].boneName;
|
string spineChildBone = spineChildTreeNode.boneName;
|
||||||
|
if (userBoneInfos[spineChildBone].HasHumanMapping()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// a suitable candidate for Chest should have Neck/Head or Shoulder mappings in its descendants
|
||||||
|
if (IsHumanBoneInHierarchy(spineChildTreeNode, "Neck") ||
|
||||||
|
IsHumanBoneInHierarchy(spineChildTreeNode, "Head") ||
|
||||||
|
IsHumanBoneInHierarchy(spineChildTreeNode, "LeftShoulder") ||
|
||||||
|
IsHumanBoneInHierarchy(spineChildTreeNode, "RightShoulder")) {
|
||||||
|
chestMappingCandidate = spineChildBone;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
failedAvatarRules.Add(avatarRule, "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 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()) {
|
if (!string.IsNullOrEmpty(chestMappingCandidate)) {
|
||||||
failedAvatarRules[avatarRule] += " It is suggested that you map bone " + spineChild +
|
failedAvatarRules[avatarRule] += " It is suggested that you map bone " + chestMappingCandidate +
|
||||||
" to Chest in Humanoid.";
|
" to Chest in Humanoid.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -949,15 +993,34 @@ class AvatarExporter : MonoBehaviour {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AvatarRule.HipsNotOnGround:
|
case AvatarRule.HipsNotAtBottom:
|
||||||
// ensure the absolute Y position for the bone mapped to Hips (if its mapped) is at least HIPS_GROUND_MIN_Y
|
// ensure that Hips is not below a proportional percentage of the avatar's height in avatar space
|
||||||
if (!string.IsNullOrEmpty(hipsUserBone)) {
|
if (!string.IsNullOrEmpty(hipsUserBone)) {
|
||||||
UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone];
|
UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone];
|
||||||
hipsPosition = hipsBoneInfo.position;
|
hipsPosition = hipsBoneInfo.position;
|
||||||
if (hipsPosition.y < HIPS_GROUND_MIN_Y) {
|
|
||||||
failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone +
|
// find the lowest y position of the bones
|
||||||
") should not be at ground level.");
|
float minBoneYPosition = float.MaxValue;
|
||||||
|
foreach (var userBoneInfo in userBoneInfos) {
|
||||||
|
Vector3 position = userBoneInfo.Value.position;
|
||||||
|
if (position.y < minBoneYPosition) {
|
||||||
|
minBoneYPosition = position.y;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check that Hips is within a percentage of avatar's height from the lowest Y point of the avatar
|
||||||
|
float bottomYRange = HIPS_MIN_Y_PERCENT_OF_HEIGHT * avatarHeight;
|
||||||
|
if (Mathf.Abs(hipsPosition.y - minBoneYPosition) < bottomYRange) {
|
||||||
|
failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone +
|
||||||
|
") should not be at the bottom of the selected avatar.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AvatarRule.ExtentsNotBelowGround:
|
||||||
|
// ensure the minimum Y extent of the model's bounds is not below a proportional threshold of avatar's height
|
||||||
|
float belowGroundThreshold = BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT * avatarHeight;
|
||||||
|
if (avatarBounds.min.y < belowGroundThreshold) {
|
||||||
|
failedAvatarRules.Add(avatarRule, "The bottom extents of the selected avatar go below ground level.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AvatarRule.HipsSpineChestNotCoincident:
|
case AvatarRule.HipsSpineChestNotCoincident:
|
||||||
|
@ -989,6 +1052,23 @@ class AvatarExporter : MonoBehaviour {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool IsHumanBoneInHierarchy(BoneTreeNode boneTreeNode, string humanBoneName) {
|
||||||
|
UserBoneInformation userBoneInfo;
|
||||||
|
if (userBoneInfos.TryGetValue(boneTreeNode.boneName, out userBoneInfo) && userBoneInfo.humanName == humanBoneName) {
|
||||||
|
// this bone matches the human bone name being searched for
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively check downward through children bones for target human bone
|
||||||
|
foreach (BoneTreeNode childNode in boneTreeNode.children) {
|
||||||
|
if (IsHumanBoneInHierarchy(childNode, humanBoneName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static string CheckHumanBoneMappingRule(AvatarRule avatarRule, string humanBoneName) {
|
static string CheckHumanBoneMappingRule(AvatarRule avatarRule, string humanBoneName) {
|
||||||
string userBoneName = "";
|
string userBoneName = "";
|
||||||
// avatar rule fails if bone is not mapped in Humanoid
|
// avatar rule fails if bone is not mapped in Humanoid
|
||||||
|
@ -999,8 +1079,8 @@ class AvatarExporter : MonoBehaviour {
|
||||||
return userBoneName;
|
return userBoneName;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string userBoneName, string descendantOfHumanName) {
|
static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string descendantUserBoneName, string descendantOfHumanName) {
|
||||||
if (string.IsNullOrEmpty(userBoneName)) {
|
if (string.IsNullOrEmpty(descendantUserBoneName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1009,27 +1089,26 @@ class AvatarExporter : MonoBehaviour {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string userBone = userBoneName;
|
string userBoneName = descendantUserBoneName;
|
||||||
string ancestorUserBone = "";
|
UserBoneInformation userBoneInfo = userBoneInfos[userBoneName];
|
||||||
UserBoneInformation userBoneInfo = new UserBoneInformation();
|
string descendantHumanName = userBoneInfo.humanName;
|
||||||
// iterate upward from user bone through user bone info parent names until root
|
// 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
|
// is reached or the ancestor bone name matches the target descendant of name
|
||||||
while (ancestorUserBone != "root") {
|
while (userBoneName != "root") {
|
||||||
if (userBoneInfos.TryGetValue(userBone, out userBoneInfo)) {
|
if (userBoneName == descendantOfUserBoneName) {
|
||||||
ancestorUserBone = userBoneInfo.parentName;
|
return;
|
||||||
if (ancestorUserBone == descendantOfUserBoneName) {
|
}
|
||||||
return;
|
if (userBoneInfos.TryGetValue(userBoneName, out userBoneInfo)) {
|
||||||
}
|
userBoneName = userBoneInfo.parentName;
|
||||||
userBone = ancestorUserBone;
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// avatar rule fails if no ancestor of given user bone matched the descendant of name (no early return)
|
// 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 +
|
failedAvatarRules.Add(avatarRule, "The bone mapped to " + descendantHumanName + " in Humanoid (" +
|
||||||
") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" +
|
descendantUserBoneName + ") is not a descendant of the bone mapped to " +
|
||||||
descendantOfUserBoneName + ").");
|
descendantOfHumanName + " in Humanoid (" + descendantOfUserBoneName + ").");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CheckAsymmetricalMappingRule(AvatarRule avatarRule, string[] mappingSuffixes, string appendage) {
|
static void CheckAsymmetricalMappingRule(AvatarRule avatarRule, string[] mappingSuffixes, string appendage) {
|
||||||
|
@ -1296,9 +1375,8 @@ class ExportProjectWindow : EditorWindow {
|
||||||
const float MAX_SCALE_SLIDER = 2.0f;
|
const float MAX_SCALE_SLIDER = 2.0f;
|
||||||
const int SLIDER_SCALE_EXPONENT = 10;
|
const int SLIDER_SCALE_EXPONENT = 10;
|
||||||
const float ACTUAL_SCALE_OFFSET = 1.0f;
|
const float ACTUAL_SCALE_OFFSET = 1.0f;
|
||||||
const float DEFAULT_AVATAR_HEIGHT = 1.755f;
|
const float MAXIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.DEFAULT_AVATAR_HEIGHT * 1.5f;
|
||||||
const float MAXIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f;
|
const float MINIMUM_RECOMMENDED_HEIGHT = AvatarUtilities.DEFAULT_AVATAR_HEIGHT * 0.25f;
|
||||||
const float MINIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f;
|
|
||||||
const float SLIDER_DIFFERENCE_REMOVE_TEXT = 0.01f;
|
const float SLIDER_DIFFERENCE_REMOVE_TEXT = 0.01f;
|
||||||
readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f);
|
readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f);
|
||||||
readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f);
|
readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f);
|
||||||
|
@ -1339,9 +1417,9 @@ class ExportProjectWindow : EditorWindow {
|
||||||
ShowUtility();
|
ShowUtility();
|
||||||
|
|
||||||
// if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height
|
// if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height
|
||||||
float height = GetAvatarHeight();
|
float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject);
|
||||||
if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) {
|
if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) {
|
||||||
float newScale = DEFAULT_AVATAR_HEIGHT / height;
|
float newScale = AvatarUtilities.DEFAULT_AVATAR_HEIGHT / height;
|
||||||
SetAvatarScale(newScale);
|
SetAvatarScale(newScale);
|
||||||
scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range.";
|
scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range.";
|
||||||
}
|
}
|
||||||
|
@ -1524,7 +1602,7 @@ class ExportProjectWindow : EditorWindow {
|
||||||
|
|
||||||
void UpdateScaleWarning() {
|
void UpdateScaleWarning() {
|
||||||
// called on any scale changes
|
// called on any scale changes
|
||||||
float height = GetAvatarHeight();
|
float height = AvatarUtilities.GetAvatarHeight(avatarPreviewObject);
|
||||||
if (height < MINIMUM_RECOMMENDED_HEIGHT) {
|
if (height < MINIMUM_RECOMMENDED_HEIGHT) {
|
||||||
scaleWarningText = "The height of the avatar is below the recommended minimum.";
|
scaleWarningText = "The height of the avatar is below the recommended minimum.";
|
||||||
} else if (height > MAXIMUM_RECOMMENDED_HEIGHT) {
|
} else if (height > MAXIMUM_RECOMMENDED_HEIGHT) {
|
||||||
|
@ -1535,23 +1613,6 @@ class ExportProjectWindow : EditorWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float GetAvatarHeight() {
|
|
||||||
// height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers
|
|
||||||
if (avatarPreviewObject != null) {
|
|
||||||
Bounds bounds = new Bounds();
|
|
||||||
var meshRenderers = avatarPreviewObject.GetComponentsInChildren<MeshRenderer>();
|
|
||||||
var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren<SkinnedMeshRenderer>();
|
|
||||||
foreach (var renderer in meshRenderers) {
|
|
||||||
bounds.Encapsulate(renderer.bounds);
|
|
||||||
}
|
|
||||||
foreach (var renderer in skinnedMeshRenderers) {
|
|
||||||
bounds.Encapsulate(renderer.bounds);
|
|
||||||
}
|
|
||||||
return bounds.max.y;
|
|
||||||
}
|
|
||||||
return 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetAvatarScale(float actualScale) {
|
void SetAvatarScale(float actualScale) {
|
||||||
// set the new scale uniformly on the preview avatar's transform to show the resulting avatar size
|
// set the new scale uniformly on the preview avatar's transform to show the resulting avatar size
|
||||||
avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale);
|
avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale);
|
||||||
|
@ -1571,3 +1632,28 @@ class ExportProjectWindow : EditorWindow {
|
||||||
onCloseCallback();
|
onCloseCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AvatarUtilities {
|
||||||
|
public const float DEFAULT_AVATAR_HEIGHT = 1.755f;
|
||||||
|
|
||||||
|
public static Bounds GetAvatarBounds(GameObject avatarObject) {
|
||||||
|
Bounds bounds = new Bounds();
|
||||||
|
if (avatarObject != null) {
|
||||||
|
var meshRenderers = avatarObject.GetComponentsInChildren<MeshRenderer>();
|
||||||
|
var skinnedMeshRenderers = avatarObject.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||||
|
foreach (var renderer in meshRenderers) {
|
||||||
|
bounds.Encapsulate(renderer.bounds);
|
||||||
|
}
|
||||||
|
foreach (var renderer in skinnedMeshRenderers) {
|
||||||
|
bounds.Encapsulate(renderer.bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float GetAvatarHeight(GameObject avatarObject) {
|
||||||
|
// height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers
|
||||||
|
Bounds avatarBounds = GetAvatarBounds(avatarObject);
|
||||||
|
return avatarBounds.max.y - avatarBounds.min.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
High Fidelity, Inc.
|
High Fidelity, Inc.
|
||||||
Avatar Exporter
|
Avatar Exporter
|
||||||
Version 0.4.0
|
Version 0.4.1
|
||||||
|
|
||||||
Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter.
|
Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter.
|
||||||
|
|
||||||
|
|
Binary file not shown.
Loading…
Reference in a new issue