Unity动画TA:在导入fbx时直接分解出Mesh文件及自动生成 ...
感觉很多项目喜欢这么做……剥离fbx里的所有资源然后把fbx删掉。于是写了一个有待完善的个人代码块。目前没有经过复杂测试,可能有很多bug。
依赖一些规则,如SkinnedMesh一定要和Root同级。
可能有很多写法幼稚的部分,如果大佬们实在忍不住,请评论区或私信留下优化方式。
to-do: 天色晚了,没有把材质处理保存进去,接下来更新了再说。
【只想参考代码的同学请直接翻到最后】
<hr/>更新于2021.11.17:
新增支持导出材质,如果导入了同名材质不会覆盖原有的材质(毕竟原有的材质可能是被调好的)
一个fbx中的每个SkinnedMesh会被视为一个装备,单独导出为一个Prefab。
新增支持导出静态网格。如果所有的Mesh都是静态网格,则只保存成一个Prefab。
新增拖动导入后,工程里完全看不见fbx文件,只有被分解后的mesh、material和perfab,不劳有强迫症的同学手动删除fbx矣。
测试1:导入单个骨骼网格体
声明:Mannequin模型来源于虚幻官方。
工具导入骨骼网格体
https://www.zhihu.com/video/1444453808764481536
fbx全程不出现,直接成了Prefab。
测试2:导入有多个submesh的静态网格体的FBX
dcc里随便拖四个box,然后导出fbx文件。
工具导入静态网格体
https://www.zhihu.com/video/1444455676902412288
于是成功导入四个mesh资源,外加一个保持原来形状的Prefab。
测试3:导入带有多个带蒙皮的子模型的FBX
其实就是看有项目组喜欢一套外装分开导出,于是目前生成多个预制体,每个蒙皮网格生成一个。对于不喜欢这个生成预制体方式的同学来说,让蒙皮子模型保持为一个prefab的代码会简单得多,目前建议自己两分钟写完 0.0
使用刚才测试静态网格的四个box,每个都加上蒙皮,再导出一次就成了含有多个带蒙皮的子模型的fbx文件。
导入多个蒙皮子网格
https://www.zhihu.com/video/1444459825879379968
to-do:
动画TA的练手工具现在就还剩下导入动画不支持了,下次再说吧……
<hr/>更新于2021.11.19:
新增导入时分解出动画片段的功能。具体代码可以在下边翻,或者看下一篇具体解释。
加了两个用于调试的没啥用的宏,修改了删fbx的位置。
https://zhuanlan.zhihu.com/p/435022514
<hr/>
#define USE_IMPORTOR
#define USE_NO_FBX
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
namespace ZxtTool
{
#if(USE_IMPORTOR)
public class CharacterFbxImporter : AssetPostprocessor
{
void OnpreprocessModel() {
ModelImporter modelImporter = assetImporter as ModelImporter;
modelImporter.clipAnimations = modelImporter.defaultClipAnimations;
ModelImporterClipAnimation[] clip_animations = modelImporter.clipAnimations;
for (int i = 0; i < clip_animations.Length; i++) {
//to-do:目前什么都没做,但肯定要做点什么,比如设置根运动类型之类的
}
modelImporter.animationType =clip_animations.Length > 0 ? ModelImporterAnimationType.Generic: ModelImporterAnimationType.None;
modelImporter.SaveAndReimport();
}
void OnPostprocessModel(GameObject g)
{
ModelImporter modelImporter = assetImporter as ModelImporter;
if (modelImporter == null) return;
string dir = Path.GetDirectoryName(assetPath);
#if (USE_NO_FBX)
if (g) {
//得到骨骼(?)
SkinnedMeshRenderer[] smrs = g.GetComponentsInChildren<SkinnedMeshRenderer>();
MeshRenderer[] mrs = g.GetComponentsInChildren<MeshRenderer>();
if (smrs.Length > 0)
{
Mesh[] meshes = new Mesh;
for (int i = 0; i < smrs.Length; i++)
{
GameObject skeleton = SkeletonTool.FormSkeleton(g);
GameObject skinObj = new GameObject();
SkinnedMeshRenderer smr = skinObj.AddComponent<SkinnedMeshRenderer>();
//to-do:在资源极端不规范的情况下,有可能出现local scale和lossy scale相差很大的情况,比如Root存在非等比缩放或者蒙皮在Root下边的层级,这种情况没考虑到
skinObj.transform.parent = skeleton.transform;
meshes = smrs.sharedMesh;
string mesh_path = Path.Combine(dir, meshes.name + &#34;.asset&#34;);
AssetDatabase.CreateAsset(meshes, mesh_path);
Material[] mats = smrs.sharedMaterials;
string[] mat_paths = new string;
for (int j = 0; j < mats.Length; j++)
{
mat_paths = Path.Combine(dir, mats.name + &#34;.mat&#34;);
string full_path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, mat_paths);
if (File.Exists(full_path))
{
//不替换已经存在的同名资源
continue;
}
AssetDatabase.CreateAsset(mats, mat_paths);
}
Mesh mesh = AssetDatabase.LoadAssetAtPath<Mesh>(mesh_path);
//清空mat,虽然也许没必要
mats = new Material;
for (int j = 0; j < mats.Length; j++)
{
mats = AssetDatabase.LoadAssetAtPath<Material>(mat_paths);
}
GameObject skinObjOriginal = smrs.gameObject;
skinObj.transform.position = skinObjOriginal.transform.position;
skinObj.transform.rotation = skinObjOriginal.transform.rotation;
skinObj.transform.localScale = skinObjOriginal.transform.localScale;
skinObj.name = skinObjOriginal.name;
smr.sharedMesh = mesh;
smr.bones = SkeletonTool.MatchBonesByName(smrs.bones, skeleton);
smr.rootBone = SkeletonTool.MatchBoneByName(smrs.rootBone, skeleton);
smr.sharedMaterials = mats;
string prefab_path = Path.Combine(dir, meshes.name + &#34;.prefab&#34;);
//prefab_path = AssetDatabase.GenerateUniqueAssetPath(prefab_path);
PrefabUtility.SaveAsPrefabAssetAndConnect(skeleton, prefab_path, InteractionMode.AutomatedAction);
Object.DestroyImmediate(skeleton);
}
//AssetDatabase.DeleteAsset(assetPath);
//meta已经被删除,但还是会弹一个“存在meta file”的警告
//AssetDatabase.DeleteAsset(Path.Combine(assetPath, &#34;.meta&#34;));
AssetDatabase.Refresh();
}
else if (mrs.Length > 0) {
Mesh[] meshes = new Mesh;
string[] mesh_paths = new string;
for (int i = 0; i < mrs.Length; i++) {
MeshFilter mesh_filter = mrs.GetComponent<MeshFilter>();
Mesh mesh = mesh_filter.sharedMesh;
mesh_paths = Path.Combine(dir, mesh.name + &#34;.asset&#34;);
Material[] mats = mrs.sharedMaterials;
string[] mat_paths = new string;
for (int j = 0; j < mats.Length; j++) {
string name = mats.name;
mat_paths = Path.Combine(dir, name + &#34;.mat&#34;);
string full_path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, mat_paths);
if (!File.Exists(full_path))
{
//不替换已经存在的同名资源
AssetDatabase.CreateAsset(mats, mat_paths);
}
}
for (int j = 0; j < mats.Length; j++) {
mats = AssetDatabase.LoadAssetAtPath<Material>(mat_paths);
}
AssetDatabase.CreateAsset(mesh, mesh_paths);
meshes = AssetDatabase.LoadAssetAtPath<Mesh>(mesh_paths);
mesh_filter.sharedMesh = meshes;
mrs.sharedMaterials = mats;
}
string prefab_path = Path.Combine(dir, g.name + &#34;.prefab&#34;);
PrefabUtility.SaveAsPrefabAssetAndConnect(g, prefab_path, InteractionMode.AutomatedAction);
//AssetDatabase.DeleteAsset(assetPath);
//meta已经被删除,但还是会弹一个“存在meta file”的警告
//AssetDatabase.DeleteAsset(Path.Combine(assetPath, &#34;.meta&#34;));
AssetDatabase.Refresh();
}
}
#endif
}
public static void OnPostprocessAllAssets(string[] importedAsset, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
bool processed_flag = false;
foreach (string asset_path in importedAsset) {
string dir = Path.GetDirectoryName(asset_path);
Object main_asset = AssetDatabase.LoadMainAssetAtPath(asset_path);
if (main_asset.GetType() == typeof(GameObject) && Path.GetExtension(asset_path).ToLower() == &#34;.fbx&#34;) {
Object[] assets = AssetDatabase.LoadAllAssetsAtPath(asset_path);
List<Object> animation_clip_list = new List<Object>();
foreach (Object asset in assets)
{
if (asset.GetType() == typeof(AnimationClip))
{
//这种preview的clip完全没法拷贝序列化
if (!asset.name.StartsWith(&#34;__preview__&#34;)){
animation_clip_list.Add(asset);
}
}
}
foreach (AnimationClip animation_clip in animation_clip_list)
{
Object new_animation_clip = new AnimationClip();
//Debug.LogFormat(&#34;{0},{1}&#34;, animation_clip.GetType(), new_animation_clip.GetType());
EditorUtility.CopySerialized(animation_clip, new_animation_clip);
//感谢虚幻引擎官方提供了测试模型
if (new_animation_clip.name == &#34;Unreal Take&#34; || new_animation_clip.name == &#34;&#34;)
{
new_animation_clip.name = Path.GetFileNameWithoutExtension(asset_path);
}
string animation_path = Path.Combine(dir, new_animation_clip.name + &#34;.anim&#34;);
AssetDatabase.CreateAsset(new_animation_clip, animation_path);
}
AssetDatabase.DeleteAsset(asset_path);
processed_flag = true;
}
}
if (processed_flag) {
AssetDatabase.Refresh();
}
}
}
#endif
public static class SkeletonTool
{
public static GameObject FormSkeleton(GameObject g) {
//剔除所有的SkinnedMeshRenderer,剩下的看作都是骨骼
if (g.GetComponent<SkinnedMeshRenderer>() == null)
{
GameObject skeleton_bone = new GameObject(g.name);
skeleton_bone.transform.position = g.transform.position;
skeleton_bone.transform.rotation = g.transform.rotation;
skeleton_bone.transform.localScale = g.transform.localScale;
for (int i = 0; i < g.transform.childCount; i++) {
GameObject childBone = FormSkeleton(g.transform.GetChild(i).gameObject);
if (childBone) {
childBone.transform.parent = skeleton_bone.transform;
}
}
return skeleton_bone;
}
return null;
}
public static Transform[] MatchBonesByName(Transform[] sourceBones, GameObject targetSkeleton) {
Transform[] match = new Transform;
Transform[] target_transforms = targetSkeleton.GetComponentsInChildren<Transform>();
for (int i = 0; i < sourceBones.Length; i++) {
for (int j = 0; j < target_transforms.Length; j++) {
if(sourceBones.name == target_transforms.name){
match = target_transforms;
break;
}
}
}
return match;
}
public static Transform MatchBoneByName(Transform sourceBone, GameObject targetSkeleton) {
Transform[] target_transforms = targetSkeleton.GetComponentsInChildren<Transform>();
foreach (Transform target_bone in target_transforms) {
if (target_bone.name == sourceBone.name) {
return target_bone;
}
}
return null;
}
}
} 搞不懂这样做的意义是什么?要知道modelimport可是包含很多设置项的但是,我也曾经这样干过,给fbx增加第二第三套uv,在unity里编辑后另存为mesh,主要给gpuskin用 设置项的问题我最倾向的解决方式是,在部分文件夹下的Editor文件夹下存一个config文件,选文件夹的时候加给右键菜单弹出对话框编辑设置项,真正可用的importer在导入时逐层向上查找最近的那个config。这样一边解决手动导入工作量大容易出错的问题,另一边可以加一些例如“自动分配文件夹里已有的Avatar”“根据名字决定是否提取y方向rootmotion”之类的trick型设置……
不过这里懒得加了,再加就跑题了 0.0
我本人其实不喜欢把fbx拆出来,留着fbx方便美术迭代挺香的,而且导出得够规范就能规避大部分的垃圾asset容量……留给control freak或者真优化大神用吧就是。万一技术负责人拍脑袋决定了“全给我拆了”,我能回答Yes we can就不错
页:
[1]