mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 09:08:47 +02:00
Merge pull request #576 from AnotherFoxGuy/unity-exporter-improvements
✨ "Update Existing Avatar" now properly updates the fst file
This commit is contained in:
commit
d882843b01
5 changed files with 487 additions and 219 deletions
|
@ -15,12 +15,13 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Overte;
|
||||||
|
|
||||||
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.6.0";
|
public static readonly string AVATAR_EXPORTER_VERSION = "2023.08";
|
||||||
|
|
||||||
static readonly float HIPS_MIN_Y_PERCENT_OF_HEIGHT = 0.03f;
|
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 BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT = -0.15f;
|
||||||
|
@ -186,9 +187,11 @@ class AvatarExporter : MonoBehaviour
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly string STANDARD_SHADER = "Standard";
|
static readonly string STANDARD_SHADER = "Standard";
|
||||||
|
static readonly string STANDARD_ROUGHNESS_SHADER = "Autodesk Interactive"; // "Standard (Roughness setup)" Has been renamed in unity 2018.03
|
||||||
static readonly string STANDARD_SPECULAR_SHADER = "Standard (Specular setup)";
|
static readonly string STANDARD_SPECULAR_SHADER = "Standard (Specular setup)";
|
||||||
static readonly string[] SUPPORTED_SHADERS = new string[] {
|
static readonly string[] SUPPORTED_SHADERS = new string[] {
|
||||||
STANDARD_SHADER,
|
STANDARD_SHADER,
|
||||||
|
STANDARD_ROUGHNESS_SHADER,
|
||||||
STANDARD_SPECULAR_SHADER,
|
STANDARD_SPECULAR_SHADER,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -223,106 +226,13 @@ class AvatarExporter : MonoBehaviour
|
||||||
AvatarRule.HeadMapped,
|
AvatarRule.HeadMapped,
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserBoneInformation
|
|
||||||
{
|
|
||||||
public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
|
|
||||||
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 Vector3 position; // absolute position
|
|
||||||
public Quaternion rotation; // absolute rotation
|
|
||||||
|
|
||||||
public UserBoneInformation()
|
|
||||||
{
|
|
||||||
humanName = "";
|
|
||||||
parentName = "";
|
|
||||||
boneTreeNode = new BoneTreeNode();
|
|
||||||
mappingCount = 0;
|
|
||||||
position = new Vector3();
|
|
||||||
rotation = new Quaternion();
|
|
||||||
}
|
|
||||||
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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoneTreeNode
|
|
||||||
{
|
|
||||||
public string boneName;
|
|
||||||
public string parentName;
|
|
||||||
public List<BoneTreeNode> children = new List<BoneTreeNode>();
|
|
||||||
|
|
||||||
public BoneTreeNode() { }
|
|
||||||
public BoneTreeNode(string name, string parent)
|
|
||||||
{
|
|
||||||
boneName = name;
|
|
||||||
parentName = parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 occlusionMap;
|
|
||||||
public Color emissive;
|
|
||||||
public string emissiveMap;
|
|
||||||
|
|
||||||
public string getJSON()
|
|
||||||
{
|
|
||||||
string json = "{ \"materialVersion\": 1, \"materials\": { ";
|
|
||||||
|
|
||||||
//Albedo
|
|
||||||
json += $"\"albedo\": [{albedo.r.F()}, {albedo.g.F()}, {albedo.b.F()}], ";
|
|
||||||
if (!string.IsNullOrEmpty(albedoMap))
|
|
||||||
json += $"\"albedoMap\": \"{albedoMap}\", ";
|
|
||||||
|
|
||||||
//Metallic
|
|
||||||
json += $"\"metallic\": {metallic.F()}, ";
|
|
||||||
if (!string.IsNullOrEmpty(metallicMap))
|
|
||||||
json += $"\"metallicMap\": \"{metallicMap}\", ";
|
|
||||||
|
|
||||||
//Roughness
|
|
||||||
json += $"\"roughness\": {roughness.F()}, ";
|
|
||||||
if (!string.IsNullOrEmpty(roughnessMap))
|
|
||||||
json += $"\"roughnessMap\": \"{roughnessMap}\", ";
|
|
||||||
|
|
||||||
//Normal
|
|
||||||
if (!string.IsNullOrEmpty(normalMap))
|
|
||||||
json += $"\"normalMap\": \"{normalMap}\", ";
|
|
||||||
|
|
||||||
//Occlusion
|
|
||||||
if (!string.IsNullOrEmpty(occlusionMap))
|
|
||||||
json += $"\"occlusionMap\": \"{occlusionMap}\", ";
|
|
||||||
|
|
||||||
//Emissive
|
|
||||||
json += $"\"emissive\": [{emissive.r.F()}, {emissive.g.F()}, {emissive.b.F()}]";
|
|
||||||
if (!string.IsNullOrEmpty(emissiveMap))
|
|
||||||
json += $", \"emissiveMap\": \"{emissiveMap}\"";
|
|
||||||
|
|
||||||
json += " } }";
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static string assetPath = "";
|
static string assetPath = "";
|
||||||
static string assetName = "";
|
static string assetName = "";
|
||||||
static ModelImporter modelImporter;
|
static ModelImporter modelImporter;
|
||||||
static HumanDescription humanDescription;
|
static HumanDescription humanDescription;
|
||||||
|
|
||||||
|
static FST currentFst;
|
||||||
|
|
||||||
static Dictionary<string, UserBoneInformation> userBoneInfos = new Dictionary<string, UserBoneInformation>();
|
static Dictionary<string, UserBoneInformation> userBoneInfos = new Dictionary<string, UserBoneInformation>();
|
||||||
static Dictionary<string, string> humanoidToUserBoneMappings = new Dictionary<string, string>();
|
static Dictionary<string, string> humanoidToUserBoneMappings = new Dictionary<string, string>();
|
||||||
static BoneTreeNode userBoneTree = new BoneTreeNode();
|
static BoneTreeNode userBoneTree = new BoneTreeNode();
|
||||||
|
@ -359,7 +269,8 @@ class AvatarExporter : MonoBehaviour
|
||||||
[MenuItem("Overte/About")]
|
[MenuItem("Overte/About")]
|
||||||
static void About()
|
static void About()
|
||||||
{
|
{
|
||||||
EditorUtility.DisplayDialog("About", "Avatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION + "\nCopyright 2022 to 2023 Overte e.V.\nCopyright 2018 High Fidelity, Inc.", "Ok");
|
EditorUtility.DisplayDialog("About",
|
||||||
|
$"Avatar Exporter\nVersion {AVATAR_EXPORTER_VERSION}\nCopyright 2022 to 2023 Overte e.V.\nCopyright 2018 High Fidelity, Inc.", "Ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ExportSelectedAvatar(bool updateExistingAvatar)
|
static void ExportSelectedAvatar(bool updateExistingAvatar)
|
||||||
|
@ -427,14 +338,10 @@ class AvatarExporter : MonoBehaviour
|
||||||
warnings = "";
|
warnings = "";
|
||||||
foreach (var failedAvatarRule in failedAvatarRules)
|
foreach (var failedAvatarRule in failedAvatarRules)
|
||||||
{
|
{
|
||||||
if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0)
|
if (EXPORT_BLOCKING_AVATAR_RULES.Contains(failedAvatarRule.Key))
|
||||||
{
|
|
||||||
boneErrors += failedAvatarRule.Value + "\n\n";
|
boneErrors += failedAvatarRule.Value + "\n\n";
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
warnings += failedAvatarRule.Value + "\n\n";
|
warnings += failedAvatarRule.Value + "\n\n";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add material and texture warnings after bone-related warnings
|
// add material and texture warnings after bone-related warnings
|
||||||
|
@ -443,9 +350,7 @@ class AvatarExporter : MonoBehaviour
|
||||||
|
|
||||||
// remove trailing newlines at the end of the warnings
|
// remove trailing newlines at the end of the warnings
|
||||||
if (!string.IsNullOrEmpty(warnings))
|
if (!string.IsNullOrEmpty(warnings))
|
||||||
{
|
warnings = warnings.Trim();
|
||||||
warnings = warnings.Substring(0, warnings.LastIndexOf("\n\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(boneErrors))
|
if (!string.IsNullOrEmpty(boneErrors))
|
||||||
{
|
{
|
||||||
|
@ -456,7 +361,7 @@ class AvatarExporter : MonoBehaviour
|
||||||
boneErrors += "Warnings:\n\n" + warnings;
|
boneErrors += "Warnings:\n\n" + warnings;
|
||||||
}
|
}
|
||||||
// remove ending newlines from the last rule failure string that was added above
|
// remove ending newlines from the last rule failure string that was added above
|
||||||
boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n"));
|
boneErrors = boneErrors.Trim();
|
||||||
EditorUtility.DisplayDialog("Error", boneErrors, "Ok");
|
EditorUtility.DisplayDialog("Error", boneErrors, "Ok");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -502,33 +407,25 @@ class AvatarExporter : MonoBehaviour
|
||||||
{
|
{
|
||||||
bool copyModelToExport = false;
|
bool copyModelToExport = false;
|
||||||
|
|
||||||
// lookup the project name field from the fst file to update
|
|
||||||
projectName = "";
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string[] lines = File.ReadAllLines(exportFstPath);
|
currentFst = new FST();
|
||||||
foreach (string line in lines)
|
// load the old file first
|
||||||
|
if(!currentFst.LoadFile(exportFstPath))
|
||||||
{
|
{
|
||||||
int separatorIndex = line.IndexOf("=");
|
EditorUtility.DisplayDialog("Error",
|
||||||
if (separatorIndex >= 0)
|
$"Failed to read from existing file {exportFstPath}. Please check the file and try again.", "Ok");
|
||||||
{
|
return;
|
||||||
string key = line.Substring(0, separatorIndex).Trim();
|
|
||||||
if (key == "name")
|
|
||||||
{
|
|
||||||
projectName = line.Substring(separatorIndex + 1).Trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath +
|
EditorUtility.DisplayDialog("Error",
|
||||||
". Please check the file and try again.", "Ok");
|
$"Failed to read from existing file {exportFstPath}. Please check the file and try again.", "Ok");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string exportModelPath = Path.GetDirectoryName(exportFstPath) + "/" + assetName + ".fbx";
|
string exportModelPath = $"{Path.GetDirectoryName(exportFstPath)}/{assetName}.fbx";
|
||||||
if (File.Exists(exportModelPath))
|
if (File.Exists(exportModelPath))
|
||||||
{
|
{
|
||||||
// if the fbx in Unity Assets is newer than the fbx in the target export
|
// if the fbx in Unity Assets is newer than the fbx in the target export
|
||||||
|
@ -613,21 +510,10 @@ class AvatarExporter : MonoBehaviour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete existing fst file since we will write a new file
|
currentFst.scale = scale;
|
||||||
// TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(exportFstPath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath +
|
|
||||||
". Please check the file and try again.", "Ok");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out a new fst file in place of the old file
|
// write out a new fst file in place of the old file
|
||||||
if (!WriteFST(exportFstPath, projectName, scale))
|
if (!WriteFST(exportFstPath))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -662,7 +548,14 @@ class AvatarExporter : MonoBehaviour
|
||||||
|
|
||||||
// write out the avatar.fst file to the project directory
|
// write out the avatar.fst file to the project directory
|
||||||
string exportFstPath = projectDirectory + "avatar.fst";
|
string exportFstPath = projectDirectory + "avatar.fst";
|
||||||
if (!WriteFST(exportFstPath, projectName, scale))
|
|
||||||
|
currentFst = new FST
|
||||||
|
{
|
||||||
|
name = projectName,
|
||||||
|
scale = scale,
|
||||||
|
filename = $"{assetName}.fbx"
|
||||||
|
};
|
||||||
|
if (!WriteFST(exportFstPath))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -692,39 +585,24 @@ class AvatarExporter : MonoBehaviour
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Overte FBX Serializer omits the colon based prefixes. This will make the jointnames compatible.
|
// The Overte FBX Serializer omits the colon based prefixes. This will make the jointnames compatible.
|
||||||
static string removeTypeFromJointname(string jointName)
|
static string removeTypeFromJointname(string jointName) => jointName.Substring(jointName.IndexOf(':') + 1);
|
||||||
{
|
|
||||||
return jointName.Substring(jointName.IndexOf(':') + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool WriteFST(string exportFstPath, string projectName, float scale)
|
static bool WriteFST(string exportFstPath)
|
||||||
{
|
{
|
||||||
// write out core fields to top of fst file
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.WriteAllText(exportFstPath,
|
|
||||||
$"exporterVersion = {AVATAR_EXPORTER_VERSION}\n" +
|
|
||||||
$"name = {projectName}\n" +
|
|
||||||
"type = body+head\n" +
|
|
||||||
$"scale = {scale.F()}\n" +
|
|
||||||
$"filename = {assetName}.fbx\n" +
|
|
||||||
"texdir = textures\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath +
|
|
||||||
". Please check the location and try again.", "Ok");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out joint mappings to fst file
|
// write out joint mappings to fst file
|
||||||
foreach (var userBoneInfo in userBoneInfos)
|
foreach (var userBoneInfo in userBoneInfos)
|
||||||
{
|
{
|
||||||
if (userBoneInfo.Value.HasHumanMapping())
|
if (userBoneInfo.Value.HasHumanMapping())
|
||||||
{
|
{
|
||||||
string overteJointName = HUMANOID_TO_OVERTE_JOINT_NAME[userBoneInfo.Value.humanName];
|
var jointName = HUMANOID_TO_OVERTE_JOINT_NAME[userBoneInfo.Value.humanName];
|
||||||
File.AppendAllText(exportFstPath, $"jointMap = {overteJointName} = {removeTypeFromJointname(userBoneInfo.Key)}\n");
|
var userJointName = removeTypeFromJointname(userBoneInfo.Key);
|
||||||
|
// Skip joints with the same name
|
||||||
|
if(jointName == userJointName)
|
||||||
|
continue;
|
||||||
|
if (!currentFst.jointMapList.Exists(x => x.From == jointName))
|
||||||
|
currentFst.jointMapList.Add(new JointMap(jointName, userJointName));
|
||||||
|
else
|
||||||
|
currentFst.jointMapList.Find(x => x.From == jointName).To = userJointName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -772,11 +650,15 @@ class AvatarExporter : MonoBehaviour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swap from left-handed (Unity) to right-handed (Overte) coordinates and write out joint rotation offset to fst
|
var norBName = removeTypeFromJointname(userBoneName);
|
||||||
jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
|
if (!currentFst.jointRotationList.Exists(x => x.BoneName == norBName))
|
||||||
File.AppendAllText(exportFstPath,
|
// swap from left-handed (Unity) to right-handed (Overte) coordinates and write out joint rotation offset to fst
|
||||||
$"jointRotationOffset2 = {removeTypeFromJointname(userBoneName)} = ({jointOffset.x.F()}, {jointOffset.y.F()}, {jointOffset.z.F()}, {jointOffset.w.F()})\n"
|
currentFst.jointRotationList.Add(
|
||||||
);
|
new JointRotationOffset2(norBName, -jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
currentFst.jointRotationList.Find(x => x.BoneName == norBName).offset =
|
||||||
|
new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is any material data to save then write out all materials in JSON material format to the materialMap field
|
// if there is any material data to save then write out all materials in JSON material format to the materialMap field
|
||||||
|
@ -788,11 +670,13 @@ class AvatarExporter : MonoBehaviour
|
||||||
// if this is the only material in the mapping and it is mapped to default material name No Name,
|
// if this is the only material in the mapping and it is mapped to default material name No Name,
|
||||||
// then the avatar has no embedded materials and this material should be applied to all meshes
|
// then the avatar has no embedded materials and this material should be applied to all meshes
|
||||||
string matName = (materialMappings.Count == 1 && materialData.Key == DEFAULT_MATERIAL_NAME) ? "all" : $"mat::{materialData.Key}";
|
string matName = (materialMappings.Count == 1 && materialData.Key == DEFAULT_MATERIAL_NAME) ? "all" : $"mat::{materialData.Key}";
|
||||||
matData.Add($"\"{matName}\": {materialData.Value.getJSON()}");
|
matData.Add($"\"{matName}\": {materialData.Value}");
|
||||||
}
|
}
|
||||||
File.AppendAllText(exportFstPath, $"materialMap = {{{string.Join(",", matData)}}}");
|
currentFst.materialMap = $"{{{string.Join(",", matData)}}}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var res = currentFst.ExportFile(exportFstPath);
|
||||||
|
|
||||||
EditorPrefs.SetString("OV_LAST_PROJECT_PATH", exportFstPath);
|
EditorPrefs.SetString("OV_LAST_PROJECT_PATH", exportFstPath);
|
||||||
|
|
||||||
/*if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows)
|
/*if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows)
|
||||||
|
@ -801,7 +685,7 @@ class AvatarExporter : MonoBehaviour
|
||||||
System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath);
|
System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath);
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
return true;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SetBoneAndMaterialInformation()
|
static void SetBoneAndMaterialInformation()
|
||||||
|
@ -831,11 +715,10 @@ class AvatarExporter : MonoBehaviour
|
||||||
{
|
{
|
||||||
string humanName = bone.humanName;
|
string humanName = bone.humanName;
|
||||||
string userBoneName = bone.boneName;
|
string userBoneName = bone.boneName;
|
||||||
string overteJointName;
|
|
||||||
if (userBoneInfos.ContainsKey(userBoneName))
|
if (userBoneInfos.ContainsKey(userBoneName))
|
||||||
{
|
{
|
||||||
++userBoneInfos[userBoneName].mappingCount;
|
++userBoneInfos[userBoneName].mappingCount;
|
||||||
if (HUMANOID_TO_OVERTE_JOINT_NAME.TryGetValue(humanName, out overteJointName))
|
if (HUMANOID_TO_OVERTE_JOINT_NAME.ContainsKey(humanName))
|
||||||
{
|
{
|
||||||
userBoneInfos[userBoneName].humanName = humanName;
|
userBoneInfos[userBoneName].humanName = humanName;
|
||||||
humanoidToUserBoneMappings.Add(humanName, userBoneName);
|
humanoidToUserBoneMappings.Add(humanName, userBoneName);
|
||||||
|
@ -1350,8 +1233,9 @@ class AvatarExporter : MonoBehaviour
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't store any material data for unsupported shader types
|
// don't store any material data for unsupported shader types
|
||||||
if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1)
|
if (!SUPPORTED_SHADERS.Contains(shaderName))
|
||||||
{
|
{
|
||||||
|
Debug.LogWarning($"Unsuported shader {shaderName} in mat {materialName}");
|
||||||
if (!unsupportedShaderMaterials.Contains(materialName))
|
if (!unsupportedShaderMaterials.Contains(materialName))
|
||||||
{
|
{
|
||||||
unsupportedShaderMaterials.Add(materialName);
|
unsupportedShaderMaterials.Add(materialName);
|
||||||
|
@ -1444,46 +1328,22 @@ class AvatarExporter : MonoBehaviour
|
||||||
|
|
||||||
static void AddMaterialWarnings()
|
static void AddMaterialWarnings()
|
||||||
{
|
{
|
||||||
string alternateStandardShaders = "";
|
if (alternateStandardShaderMaterials.Count != 0)
|
||||||
string unsupportedShaders = "";
|
|
||||||
// combine all material names for each material warning into a comma-separated string
|
|
||||||
foreach (string materialName in alternateStandardShaderMaterials)
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(alternateStandardShaders))
|
string alternateStandardShaders = string.Join(", ", alternateStandardShaderMaterials);
|
||||||
{
|
warnings += alternateStandardShaderMaterials.Count == 1
|
||||||
alternateStandardShaders += ", ";
|
? $"The material {alternateStandardShaders} is not using the recommended variation of the Standard shader."
|
||||||
}
|
: $"The materials {alternateStandardShaders} are not using the recommended variation of the Standard shader."
|
||||||
alternateStandardShaders += materialName;
|
+ " We recommend you change them to \"Autodesk Interactive\" shader for improved performance.\n\n";
|
||||||
}
|
}
|
||||||
foreach (string materialName in unsupportedShaderMaterials)
|
|
||||||
|
if (unsupportedShaderMaterials.Count != 0)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(unsupportedShaders))
|
string unsupportedShaders = string.Join(", ", unsupportedShaderMaterials);
|
||||||
{
|
warnings += unsupportedShaderMaterials.Count == 1
|
||||||
unsupportedShaders += ", ";
|
? $"The material {unsupportedShaders} is using an unsupported shader."
|
||||||
}
|
: $"The materials {unsupportedShaders} are using an unsupported shader."
|
||||||
unsupportedShaders += materialName;
|
+ " We recommend you change it to the \"Autodesk Interactive\" shader\n\n";
|
||||||
}
|
|
||||||
if (alternateStandardShaderMaterials.Count > 1)
|
|
||||||
{
|
|
||||||
warnings += "The materials " + alternateStandardShaders + " are not using the " +
|
|
||||||
"recommended variation of the Standard shader. We recommend you change " +
|
|
||||||
"them to Standard (Roughness setup) shader for improved performance.\n\n";
|
|
||||||
}
|
|
||||||
else if (alternateStandardShaderMaterials.Count == 1)
|
|
||||||
{
|
|
||||||
warnings += "The material " + alternateStandardShaders + " is not using the " +
|
|
||||||
"recommended variation of the Standard shader. We recommend you change " +
|
|
||||||
"it to Standard (Roughness setup) shader for improved performance.\n\n";
|
|
||||||
}
|
|
||||||
if (unsupportedShaderMaterials.Count > 1)
|
|
||||||
{
|
|
||||||
warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " +
|
|
||||||
"We recommend you change them to a Standard shader type.\n\n";
|
|
||||||
}
|
|
||||||
else if (unsupportedShaderMaterials.Count == 1)
|
|
||||||
{
|
|
||||||
warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " +
|
|
||||||
"We recommend you change it to a Standard shader type.\n\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1917,11 +1777,6 @@ class AvatarUtilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ConverterExtensions
|
|
||||||
{
|
|
||||||
//Helper function to convert floats to string without commas
|
|
||||||
public static string F(this float x) => x.ToString("G", CultureInfo.InvariantCulture);
|
|
||||||
public static string F(this double x) => x.ToString("G", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
402
tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs
Normal file
402
tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs
Normal file
|
@ -0,0 +1,402 @@
|
||||||
|
// FST.cs
|
||||||
|
//
|
||||||
|
// Created by Edgar on 24-8-2023
|
||||||
|
// Copyright 2023 Overte e.V.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Overte
|
||||||
|
{
|
||||||
|
class JointMap
|
||||||
|
{
|
||||||
|
public string From;
|
||||||
|
public string To;
|
||||||
|
|
||||||
|
private Regex parseRx = new Regex(@"^(?<From>[\w]*)\s*=\s*(?<To>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public JointMap(string RawInput)
|
||||||
|
{
|
||||||
|
var parsed = parseRx.Matches(RawInput)[0];
|
||||||
|
From = parsed.Groups["From"].Value.Trim();
|
||||||
|
To = parsed.Groups["To"].Value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointMap(string f, string t)
|
||||||
|
{
|
||||||
|
From = f; To = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"jointMap = {From} = {To}";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Joint
|
||||||
|
{
|
||||||
|
public string From;
|
||||||
|
public string To;
|
||||||
|
|
||||||
|
private Regex parseRx = new Regex(@"^(?<From>[\w]*)\s*=\s*(?<To>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public Joint(string RawInput)
|
||||||
|
{
|
||||||
|
var parsed = parseRx.Matches(RawInput)[0];
|
||||||
|
From = parsed.Groups["From"].Value.Trim();
|
||||||
|
To = parsed.Groups["To"].Value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Joint(string f, string t)
|
||||||
|
{
|
||||||
|
From = f; To = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"joint = {From} = {To}";
|
||||||
|
}
|
||||||
|
|
||||||
|
class JointRotationOffset2
|
||||||
|
{
|
||||||
|
public string BoneName;
|
||||||
|
public Quaternion offset;
|
||||||
|
|
||||||
|
private Regex parseRx = new Regex(@"(?<BoneName>.*)\s*=\s*\(\s*(?<X>.*)\s*,\s*(?<Y>.*)\s*,\s*(?<Z>.*)\s*,\s*(?<W>.*)\s*\)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public JointRotationOffset2(string value)
|
||||||
|
{
|
||||||
|
var parsed = parseRx.Matches(value)[0];
|
||||||
|
BoneName = parsed.Groups["BoneName"].Value.Trim();
|
||||||
|
offset = new Quaternion
|
||||||
|
{
|
||||||
|
x = float.Parse(parsed.Groups["X"].Value, CultureInfo.InvariantCulture),
|
||||||
|
y = float.Parse(parsed.Groups["Y"].Value, CultureInfo.InvariantCulture),
|
||||||
|
z = float.Parse(parsed.Groups["Z"].Value, CultureInfo.InvariantCulture),
|
||||||
|
w = float.Parse(parsed.Groups["W"].Value, CultureInfo.InvariantCulture)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointRotationOffset2(string boneName, float x, float y, float z, float w)
|
||||||
|
{
|
||||||
|
BoneName = boneName;
|
||||||
|
offset = new Quaternion(x, y, z, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"jointRotationOffset2 = {BoneName} = ({offset.x.F()}, {offset.y.F()}, {offset.z.F()}, {offset.w.F()})";
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemapBlendShape
|
||||||
|
{
|
||||||
|
public string From;
|
||||||
|
public string To;
|
||||||
|
public float Multiplier;
|
||||||
|
|
||||||
|
private Regex parseRx = new Regex(@"(?<From>.*)\s*=\s*(?<To>.*)\s*=\s*(?<Multiplier>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public RemapBlendShape(string rawData)
|
||||||
|
{
|
||||||
|
var parsed = parseRx.Matches(rawData)[0];
|
||||||
|
From = parsed.Groups["From"].Value.Trim();
|
||||||
|
To = parsed.Groups["To"].Value.Trim();
|
||||||
|
Multiplier = float.Parse(parsed.Groups["Multiplier"].Value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"bs = {From} = {To} = {Multiplier.F()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
class JointIndex
|
||||||
|
{
|
||||||
|
public string BoneName;
|
||||||
|
public int Index;
|
||||||
|
|
||||||
|
private Regex parseRx = new Regex(@"^(?<BoneName>.*)\s*=\s*(?<Index>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public JointIndex(string rawData)
|
||||||
|
{
|
||||||
|
var parsed = parseRx.Matches(rawData)[0];
|
||||||
|
BoneName = parsed.Groups["BoneName"].Value.Trim();
|
||||||
|
Index = int.Parse(parsed.Groups["Index"].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"jointIndex = {BoneName} = {Index}";
|
||||||
|
}
|
||||||
|
|
||||||
|
class FST
|
||||||
|
{
|
||||||
|
public readonly string exporterVersion = AvatarExporter.AVATAR_EXPORTER_VERSION;
|
||||||
|
public string name;
|
||||||
|
public string type = "body+head";
|
||||||
|
public float scale = 1.0f;
|
||||||
|
public string filename;
|
||||||
|
public string texdir = "textures";
|
||||||
|
public string materialMap;
|
||||||
|
public string script;
|
||||||
|
|
||||||
|
public List<RemapBlendShape> remapBlendShapeList = new List<RemapBlendShape>();
|
||||||
|
|
||||||
|
public List<Joint> jointList = new List<Joint>();
|
||||||
|
public List<JointMap> jointMapList = new List<JointMap>();
|
||||||
|
public List<JointRotationOffset2> jointRotationList = new List<JointRotationOffset2>();
|
||||||
|
public List<JointIndex> jointIndexList = new List<JointIndex>();
|
||||||
|
public List<string> freeJointList = new List<string>();
|
||||||
|
|
||||||
|
private Regex parseRx = new Regex(@"^(?<Key>[\w]*)\s*=\s*(?<Value>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public string flowPhysicsData;
|
||||||
|
public string flowCollisionsData;
|
||||||
|
|
||||||
|
public string lod;
|
||||||
|
public string joint;
|
||||||
|
|
||||||
|
List<string> fstContent = new List<string>();
|
||||||
|
|
||||||
|
public bool ExportFile(string fstPath)
|
||||||
|
{
|
||||||
|
fstContent = new List<string>
|
||||||
|
{
|
||||||
|
$"exporterVersion = {exporterVersion}",
|
||||||
|
$"name = {name}",
|
||||||
|
$"type = {type}",
|
||||||
|
$"scale = {scale.F()}",
|
||||||
|
$"filename = {filename}",
|
||||||
|
$"texdir = {texdir}"
|
||||||
|
};
|
||||||
|
AddIfNotNull(remapBlendShapeList);
|
||||||
|
AddIfNotNull(jointMapList);
|
||||||
|
AddIfNotNull(jointIndexList);
|
||||||
|
AddIfNotNull(jointRotationList);
|
||||||
|
AddIfNotNull("freeJoint", freeJointList);
|
||||||
|
|
||||||
|
AddIfNotNull(nameof(materialMap), materialMap);
|
||||||
|
AddIfNotNull(nameof(flowPhysicsData), flowPhysicsData);
|
||||||
|
AddIfNotNull(nameof(flowCollisionsData), flowCollisionsData);
|
||||||
|
AddIfNotNull(nameof(lod), lod);
|
||||||
|
AddIfNotNull(nameof(joint), joint);
|
||||||
|
AddIfNotNull(nameof(script), script);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.IO.File.WriteAllLines(fstPath, fstContent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("Error", "Failed to write file " + fstPath +
|
||||||
|
". Please check the location and try again.", "Ok");
|
||||||
|
Debug.LogException(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddIfNotNull<T>(string keyname, List<T> list)
|
||||||
|
{
|
||||||
|
if (list.Count != 0)
|
||||||
|
list.ForEach(x => fstContent.Add($"{keyname} = {x}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddIfNotNull<T>(List<T> list)
|
||||||
|
{
|
||||||
|
if (list.Count != 0)
|
||||||
|
fstContent.Add(string.Join("\n", list));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddIfNotNull(string keyname, string valname)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(valname))
|
||||||
|
fstContent.Add($"{keyname} = {valname}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool LoadFile(string fstPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rawFst = System.IO.File.ReadAllLines(fstPath);
|
||||||
|
|
||||||
|
foreach (var l in rawFst)
|
||||||
|
{
|
||||||
|
if (!parseRx.IsMatch(l)) continue;
|
||||||
|
var match = parseRx.Matches(l)[0];
|
||||||
|
ParseLine(match.Groups["Key"].Value.Trim(), match.Groups["Value"].Value.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("Error", "Failed to read file " + fstPath +
|
||||||
|
". Please check the location and try again.", "Ok");
|
||||||
|
Debug.LogException(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseLine(string key, string value)
|
||||||
|
{
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "exporterVersion":
|
||||||
|
//Just ingnore the old exporterVersion
|
||||||
|
break;
|
||||||
|
case "name":
|
||||||
|
name = value;
|
||||||
|
break;
|
||||||
|
case "type":
|
||||||
|
type = value;
|
||||||
|
break;
|
||||||
|
case "scale":
|
||||||
|
scale = float.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
break;
|
||||||
|
case "filename":
|
||||||
|
filename = value;
|
||||||
|
break;
|
||||||
|
case "texdir":
|
||||||
|
texdir = value;
|
||||||
|
break;
|
||||||
|
case "materialMap":
|
||||||
|
// The materialMap will be generated by unity, no need to parse it
|
||||||
|
// TODO:Parse it when changed to importing instead of updating
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "joint":
|
||||||
|
jointList.Add(new Joint(value));
|
||||||
|
break;
|
||||||
|
case "jointMap":
|
||||||
|
jointMapList.Add(new JointMap(value));
|
||||||
|
break;
|
||||||
|
case "jointRotationOffset":
|
||||||
|
// Old version, does not seem to be used
|
||||||
|
break;
|
||||||
|
case "jointRotationOffset2":
|
||||||
|
jointRotationList.Add(new JointRotationOffset2(value));
|
||||||
|
break;
|
||||||
|
case "jointIndex":
|
||||||
|
jointIndexList.Add(new JointIndex(value));
|
||||||
|
break;
|
||||||
|
case "freeJoint":
|
||||||
|
freeJointList.Add(value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bs":
|
||||||
|
remapBlendShapeList.Add(new RemapBlendShape(value));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Debug.LogError($"Unknown key \"{key}\"\nPlease report this issue on the issue tracker");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyValuePair<string, string> ParseKVPair(Regex rx, string sinput)
|
||||||
|
{
|
||||||
|
var match = rx.Matches(sinput)[0];
|
||||||
|
return new KeyValuePair<string, string>(match.Groups["Key"].Value.Trim(), match.Groups["Value"].Value.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserBoneInformation
|
||||||
|
{
|
||||||
|
public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
|
||||||
|
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 Vector3 position; // absolute position
|
||||||
|
public Quaternion rotation; // absolute rotation
|
||||||
|
|
||||||
|
public UserBoneInformation()
|
||||||
|
{
|
||||||
|
humanName = "";
|
||||||
|
parentName = "";
|
||||||
|
boneTreeNode = new BoneTreeNode();
|
||||||
|
mappingCount = 0;
|
||||||
|
position = new Vector3();
|
||||||
|
rotation = new Quaternion();
|
||||||
|
}
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class BoneTreeNode
|
||||||
|
{
|
||||||
|
public string boneName;
|
||||||
|
public string parentName;
|
||||||
|
public List<BoneTreeNode> children = new List<BoneTreeNode>();
|
||||||
|
|
||||||
|
public BoneTreeNode() { }
|
||||||
|
public BoneTreeNode(string name, string parent)
|
||||||
|
{
|
||||||
|
boneName = name;
|
||||||
|
parentName = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 occlusionMap;
|
||||||
|
public Color emissive;
|
||||||
|
public string emissiveMap;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
string json = "{ \"materialVersion\": 1, \"materials\": { ";
|
||||||
|
|
||||||
|
//Albedo
|
||||||
|
json += $"\"albedo\": [{albedo.r.F()}, {albedo.g.F()}, {albedo.b.F()}], ";
|
||||||
|
if (!string.IsNullOrEmpty(albedoMap))
|
||||||
|
json += $"\"albedoMap\": \"{albedoMap}\", ";
|
||||||
|
|
||||||
|
//Metallic
|
||||||
|
json += $"\"metallic\": {metallic.F()}, ";
|
||||||
|
if (!string.IsNullOrEmpty(metallicMap))
|
||||||
|
json += $"\"metallicMap\": \"{metallicMap}\", ";
|
||||||
|
|
||||||
|
//Roughness
|
||||||
|
json += $"\"roughness\": {roughness.F()}, ";
|
||||||
|
if (!string.IsNullOrEmpty(roughnessMap))
|
||||||
|
json += $"\"roughnessMap\": \"{roughnessMap}\", ";
|
||||||
|
|
||||||
|
//Normal
|
||||||
|
if (!string.IsNullOrEmpty(normalMap))
|
||||||
|
json += $"\"normalMap\": \"{normalMap}\", ";
|
||||||
|
|
||||||
|
//Occlusion
|
||||||
|
if (!string.IsNullOrEmpty(occlusionMap))
|
||||||
|
json += $"\"occlusionMap\": \"{occlusionMap}\", ";
|
||||||
|
|
||||||
|
//Emissive
|
||||||
|
json += $"\"emissive\": [{emissive.r.F()}, {emissive.g.F()}, {emissive.b.F()}]";
|
||||||
|
if (!string.IsNullOrEmpty(emissiveMap))
|
||||||
|
json += $", \"emissiveMap\": \"{emissiveMap}\"";
|
||||||
|
|
||||||
|
json += " } }";
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ConverterExtensions
|
||||||
|
{
|
||||||
|
//Helper function to convert floats to string without commas
|
||||||
|
public static string F(this float x) => x.ToString("G", CultureInfo.InvariantCulture);
|
||||||
|
public static string F(this double x) => x.ToString("G", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2472151ab437fca478d4c48d8d010c49
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -1,5 +1,5 @@
|
||||||
Avatar Exporter
|
Avatar Exporter
|
||||||
Version 0.6.0
|
Version 2023.08
|
||||||
Copyright 2018 High Fidelity, Inc.
|
Copyright 2018 High Fidelity, Inc.
|
||||||
Copyright 2022 Overte e.V.
|
Copyright 2022 Overte e.V.
|
||||||
|
|
||||||
|
|
Binary file not shown.
Loading…
Reference in a new issue