Files
AIGame/AIClient/AgentScripts/BuildCharacterAnimationLab.cs
T

308 lines
23 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AIGame.EditorTools;
using AIGame.Review;
using Newtonsoft.Json;
using Unity.Collections;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public static class BuildCharacterAnimationLab
{
public const string Root = "Assets/Review/CharacterAnimationLab";
public const string ScenePath = Root + "/CharacterAnimationLab.unity";
const string Generated = Root + "/Generated";
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
static string DataPath => Path.Combine(Repo, "Tools/output/campguide_studio/rig_mixamo41/unity_review/review_data.json");
public class Bone { public string name; public int parent; public float[] rest; public float[][] bindpose; }
public class Variant { public string name; public float[][] vertices, normals, uv, auditEdges; public int[] triangles; public float[][][] weights; public int originalVertices, trianglesCount; }
public class Motion { public string name; public int fps; public float[][][] samples; }
public class Gait { public Dictionary<string,Motion> motions; }
public class Data { public Bone[] bones; public Variant[] variants; public Motion[] clips; public float translationScale,height; public float[][] translationOffsets,translationDeltaRotations,bindRotationCorrections; public int[] rotationParents; }
static Vector3 V(float[] x, int offset = 0) => new Vector3(x[offset], x[offset + 1], x[offset + 2]);
static Quaternion Q(float[] x) => new Quaternion(x[3], x[4], x[5], x[6]);
static void SetTRS(Transform t, float[] x) { t.localPosition = V(x); t.localRotation = Q(x); t.localScale = V(x, 7); }
static Matrix4x4 Matrix(float[][] rows)
{
var m = new Matrix4x4(); for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) m[r, c] = rows[r][c]; return m;
}
static void Folder(string path)
{
if (AssetDatabase.IsValidFolder(path)) return;
var parent = path.Substring(0, path.LastIndexOf('/')); Folder(parent);
AssetDatabase.CreateFolder(parent, path.Substring(path.LastIndexOf('/') + 1));
}
static Material Material(string name, Color color, string destination = Generated)
{
var path = destination + "/" + name + ".mat";
var material = AssetDatabase.LoadAssetAtPath<Material>(path);
if (!material) { material = new Material(Shader.Find("Universal Render Pipeline/Lit")); AssetDatabase.CreateAsset(material, path); }
material.SetColor("_BaseColor", color); material.SetFloat("_Smoothness", .22f); material.SetFloat("_Metallic", 0);
EditorUtility.SetDirty(material); return material;
}
static Mesh MakeMesh(Variant item, Bone[] bones, string destination = Generated)
{
string path = destination + "/" + item.name + ".asset";
var mesh = AssetDatabase.LoadAssetAtPath<Mesh>(path);
if (!mesh) { mesh = new Mesh { name = item.name }; AssetDatabase.CreateAsset(mesh, path); }
mesh.Clear(); mesh.indexFormat = IndexFormat.UInt32;
mesh.vertices = item.vertices.Select(x => V(x)).ToArray();
mesh.normals = item.normals.Select(x => V(x)).ToArray();
mesh.uv = item.uv.Select(x => new Vector2(x[0], x[1])).ToArray();
mesh.triangles = item.triangles; mesh.bindposes = bones.Select(b => Matrix(b.bindpose)).ToArray();
var counts = new NativeArray<byte>(item.weights.Select(x => (byte)x.Length).ToArray(), Allocator.Temp);
var flattened = new NativeArray<BoneWeight1>(item.weights.SelectMany(x => x).Select(x => new BoneWeight1 { boneIndex = (int)x[0], weight = x[1] }).ToArray(), Allocator.Temp);
try { mesh.SetBoneWeights(counts, flattened); } finally { counts.Dispose(); flattened.Dispose(); }
mesh.RecalculateBounds(); mesh.RecalculateTangents(); EditorUtility.SetDirty(mesh); return mesh;
}
static AnimationClip MakeClip(Motion motion, Bone[] bones, string destination = Generated)
{
string path = destination + "/" + motion.name + ".anim";
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
if (!clip) { clip = new AnimationClip(); AssetDatabase.CreateAsset(clip, path); }
clip.ClearCurves(); clip.name = motion.name; clip.frameRate = motion.fps; clip.legacy = false;
string[] paths = new string[bones.Length];
string[] properties = { "m_LocalPosition.x", "m_LocalPosition.y", "m_LocalPosition.z", "m_LocalRotation.x", "m_LocalRotation.y", "m_LocalRotation.z", "m_LocalRotation.w", "m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z" };
for (int b = 0; b < bones.Length; b++)
{
paths[b] = bones[b].parent < 0 ? bones[b].name : paths[bones[b].parent] + "/" + bones[b].name;
for (int channel = 0; channel < properties.Length; channel++)
{
var keys = new Keyframe[motion.samples.Length];
for (int f = 0; f < keys.Length; f++) keys[f] = new Keyframe(f / (float)motion.fps, motion.samples[f][b][channel]);
var curve = new AnimationCurve(keys);
for (int f = 0; f < keys.Length; f++) { AnimationUtility.SetKeyLeftTangentMode(curve, f, AnimationUtility.TangentMode.Linear); AnimationUtility.SetKeyRightTangentMode(curve, f, AnimationUtility.TangentMode.Linear); }
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve(paths[b], typeof(Transform), properties[channel]), curve);
}
}
clip.EnsureQuaternionContinuity();
var settings = AnimationUtility.GetAnimationClipSettings(clip); settings.loopTime = true; AnimationUtility.SetAnimationClipSettings(clip, settings);
EditorUtility.SetDirty(clip); return clip;
}
public static string Build()
{
if (EditorApplication.isPlaying) throw new InvalidOperationException("Build in Edit Mode.");
var data = JsonConvert.DeserializeObject<Data>(File.ReadAllText(DataPath));
if (data.bones.Length != 41 || data.variants.Length != 3) throw new InvalidDataException("Expected 3 variants and 41 bones.");
Folder(Generated);
var meshes = data.variants.Select(v => MakeMesh(v, data.bones)).ToArray();
var clips = data.clips.Select(c => MakeClip(c, data.bones)).ToArray();
var clay = Material("Clay", new Color(.68f, .73f, .79f));
string texturedPath = Generated + "/CharacterTextured.mat";
var textured = AssetDatabase.LoadAssetAtPath<Material>(texturedPath);
if (!textured)
{
textured = new Material(AssetDatabase.LoadAssetAtPath<Material>("Assets/Bundles/Characters/Human/CampGuideStudio/CampGuideStudio.mat"));
textured.name = "CharacterTextured"; AssetDatabase.CreateAsset(textured, texturedPath);
}
string uiPrefab = JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(Repo, "Doc/UIPrefabCreater/UI_CharacterAnimationLab.json"));
if (string.IsNullOrEmpty(uiPrefab)) throw new InvalidOperationException("Comparison UI JSON failed.");
var previous = SceneManager.GetActiveScene();
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
try
{
SceneManager.SetActiveScene(scene);
var root = new GameObject("CharacterAnimationLab");
var viewer = root.AddComponent<CharacterAnimationLab>(); viewer.clips = clips; viewer.texturedMaterial = textured; viewer.clayMaterial = clay;
viewer.travel = data.clips.Select(c => V(c.samples[c.samples.Length - 1][0]) - V(c.samples[0][0])).ToArray();
viewer.actors = new CharacterAnimationLab.Actor[3];
string[] labels = { "Mixamo 网站", "TokenRig 原始", "手部约束后" };
Color[] colors = { new Color(.49f,.78f,.85f), new Color(.94f,.72f,.43f), new Color(.62f,.82f,.51f) };
for (int i = 0; i < 3; i++)
{
var slot = new GameObject("Stage_" + data.variants[i].name); slot.transform.SetParent(root.transform, false); slot.transform.localPosition = new Vector3((i - 1) * .99f, 0, 0);
var rig = new GameObject("Rig"); rig.transform.SetParent(slot.transform, false); rig.transform.localScale = Vector3.one * (1.55f / 1.1348386f);
var bones = new Transform[data.bones.Length];
for (int b = 0; b < bones.Length; b++)
{
var go = new GameObject(data.bones[b].name); bones[b] = go.transform;
go.transform.SetParent(data.bones[b].parent < 0 ? rig.transform : bones[data.bones[b].parent], false); SetTRS(go.transform, data.bones[b].rest);
}
var meshGo = new GameObject("CharacterMesh"); meshGo.transform.SetParent(rig.transform, false);
var skin = meshGo.AddComponent<SkinnedMeshRenderer>(); skin.sharedMesh = meshes[i]; skin.bones = bones; skin.rootBone = bones[0]; skin.sharedMaterial = textured;
skin.quality = SkinQuality.Auto; skin.updateWhenOffscreen = true; skin.localBounds = new Bounds(new Vector3(0,.75f,0), new Vector3(3,3,3));
viewer.actors[i] = new CharacterAnimationLab.Actor { label = labels[i], rig = rig, hips = bones[0], rightHand = bones.First(b=>b.name=="mixamorig:RightHand"), renderer = skin };
var pedestal = GameObject.CreatePrimitive(PrimitiveType.Cylinder); pedestal.name = "Pedestal"; pedestal.transform.SetParent(slot.transform, false);
pedestal.transform.localPosition = new Vector3(0,-.02f,0); pedestal.transform.localScale = new Vector3(.98f,.01f,.98f);
pedestal.GetComponent<Renderer>().sharedMaterial = Material("Pedestal" + i, colors[i]); UnityEngine.Object.DestroyImmediate(pedestal.GetComponent<Collider>());
}
var floor = GameObject.CreatePrimitive(PrimitiveType.Cube); floor.name = "ReviewFloor"; floor.transform.SetParent(root.transform, false);
floor.transform.localPosition = new Vector3(0,-.09f,.15f); floor.transform.localScale = new Vector3(5.4f,.1f,3.2f);
floor.GetComponent<Renderer>().sharedMaterial = Material("Floor", new Color(.83f,.88f,.92f)); UnityEngine.Object.DestroyImmediate(floor.GetComponent<Collider>());
var background = new GameObject("BackgroundCamera").AddComponent<Camera>(); background.transform.SetParent(root.transform,false); background.depth=-10; background.cullingMask=0;
background.clearFlags=CameraClearFlags.SolidColor; background.backgroundColor=new Color(.75f,.83f,.89f);
var camera = new GameObject("ReviewCamera").AddComponent<Camera>(); camera.transform.SetParent(root.transform,false); camera.tag="MainCamera";
camera.orthographic=true; camera.rect=new Rect(0,.34f,1,.535f); camera.clearFlags=CameraClearFlags.SolidColor; camera.backgroundColor=background.backgroundColor; camera.allowHDR=false;
camera.nearClipPlane=.05f; camera.farClipPlane=30; camera.gameObject.AddComponent<AudioListener>(); viewer.reviewCamera=camera;
foreach (var setup in new[] { ("KeyLight",new Vector3(45,-35,0),1.1f), ("FillLight",new Vector3(35,145,0),.45f) })
{
var light=new GameObject(setup.Item1).AddComponent<Light>(); light.transform.SetParent(root.transform,false); light.type=LightType.Directional;
light.transform.rotation=Quaternion.Euler(setup.Item2); light.intensity=setup.Item3; light.shadows=setup.Item1=="KeyLight"?LightShadows.Soft:LightShadows.None;
}
RenderSettings.ambientMode=AmbientMode.Flat; RenderSettings.ambientLight=new Color(.65f,.69f,.73f); RenderSettings.fog=false;
var ui=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(uiPrefab),root.transform); viewer.ui=ui.transform;
ui.GetComponentInChildren<AnimationLabSeekBar>().viewer=viewer;
var events = new GameObject("EventSystem", typeof(EventSystem),typeof(StandaloneInputModule)); events.transform.SetParent(root.transform,false);
viewer.motion=0; viewer.time=10f/30; viewer.inPlace=true; viewer.Sample();
EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene,ScenePath);
PrefabUtility.SaveAsPrefabAsset(root,Generated+"/CharacterAnimationLab.prefab"); AssetDatabase.SaveAssets();
return JsonConvert.SerializeObject(new { scene=ScenePath, variants=3,bones=41,triangles=3000,clips=clips.Select(c=>new {c.name,c.length}), ui=uiPrefab });
}
finally
{
if (previous.IsValid() && previous.isLoaded) SceneManager.SetActiveScene(previous);
EditorSceneManager.CloseScene(scene,true);
}
}
public static string BuildLmGirl()
{
if (EditorApplication.isPlaying) throw new InvalidOperationException("Edit Mode required.");
const string targetRoot=Root+"/LmGirl";
const string generated=targetRoot+"/Generated";
if (SceneManager.GetSceneByPath(targetRoot+"/LmGirlAnimationLab.unity").isLoaded)
throw new InvalidOperationException("Open a different scene before rebuilding LmGirl; Unity cannot overwrite another loaded scene.");
Folder(generated);
var data=JsonConvert.DeserializeObject<Data>(File.ReadAllText(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/rig_mixamo41/unity_review_data.json")));
var meshes=data.variants.ToDictionary(v=>v.name,v=>MakeMesh(v,data.bones,generated));
var website=JsonConvert.DeserializeObject<Data>(File.ReadAllText(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/rig_mixamo41/website/unity_review_data.json")));
meshes["mixamo"]=MakeMesh(website.variants[0],website.bones,generated);
var gait=JsonConvert.DeserializeObject<Gait>(File.ReadAllText(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/rig_mixamo41/gait_correction.json")));
var walk41=MakeClip(gait.motions["tokenrig"],data.bones,generated);
var walk33=MakeClip(gait.motions["mixamo"],website.bones,generated);
foreach (string suffix in new[]{"base","mix"})
{
string path=generated+"/LmGirlTpose_"+suffix+".png";
File.Copy(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/xpbr/LmGirlTpose_"+suffix+".png"),path,true);
AssetDatabase.ImportAsset(path,ImportAssetOptions.ForceSynchronousImport);
var importer=(TextureImporter)AssetImporter.GetAtPath(path);
importer.sRGBTexture=suffix=="base";importer.maxTextureSize=1024;
importer.alphaIsTransparency=false;importer.SaveAndReimport();
}
string matPath=generated+"/LmGirlTextured.mat";
var material=AssetDatabase.LoadAssetAtPath<Material>(matPath);
if (!material)
{
material=new Material(AssetDatabase.LoadAssetAtPath<Material>(Generated+"/CharacterTextured.mat"));
AssetDatabase.CreateAsset(material,matPath);
}
material.SetTexture("_MainTex",AssetDatabase.LoadAssetAtPath<Texture2D>(generated+"/LmGirlTpose_base.png"));
material.SetTexture("_MixTex",AssetDatabase.LoadAssetAtPath<Texture2D>(generated+"/LmGirlTpose_mix.png"));
EditorUtility.SetDirty(material);
var uiPath=JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(Repo,"Doc/UIPrefabCreater/UI_LmGirlAnimationLab.json"));
if (string.IsNullOrEmpty(uiPath)) throw new InvalidOperationException("LmGirl UI conversion failed.");
var previous=SceneManager.GetActiveScene();
var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive);
try
{
SceneManager.SetActiveScene(scene);
var root=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Generated+"/CharacterAnimationLab.prefab"));
PrefabUtility.UnpackPrefabInstance(root,PrefabUnpackMode.Completely,InteractionMode.AutomatedAction);
root.name="LmGirlAnimationLab";
var viewer=root.GetComponent<CharacterAnimationLab>();
string[] variants={"mixamo","raw","post","local"};
string[] labels={"Mixamo 网站 · 33 骨","TokenRig 原始 · 41 骨","体素后处理 · 41 骨","局部约束 · 41 骨"};
var slots=viewer.actors.Select(a=>a.rig.transform.parent).ToList();
foreach(var actor in viewer.actors)UnityEngine.Object.DestroyImmediate(actor.rig);
slots.Add(UnityEngine.Object.Instantiate(slots[2],root.transform));
viewer.actors=new CharacterAnimationLab.Actor[4];
Color[] colors={new Color(.49f,.78f,.85f),new Color(.94f,.72f,.43f),new Color(.68f,.62f,.85f),new Color(.62f,.82f,.51f)};
Quaternion Rotation(float[] q)=>new Quaternion(q[0],q[1],q[2],q[3]);
for (int i=0;i<variants.Length;i++)
{
var actorData=i==0?website:data;
var slot=slots[i];slot.localPosition=new Vector3((i-1.5f)*.83f,0,0);
slot.name="Stage_LmGirl_"+variants[i];
slot.GetComponentInChildren<Renderer>().sharedMaterial=Material("LmGirlPedestal"+i,colors[i],generated);
var rig=new GameObject("Rig");rig.transform.SetParent(slot,false);rig.transform.localScale=Vector3.one*(1.55f/actorData.height);
var bones=new Transform[actorData.bones.Length];
for (int b=0;b<bones.Length;b++)
{
var go=new GameObject(actorData.bones[b].name);bones[b]=go.transform;
go.transform.SetParent(actorData.bones[b].parent<0?rig.transform:bones[actorData.bones[b].parent],false);
SetTRS(go.transform,actorData.bones[b].rest);
}
var meshGo=new GameObject("CharacterMesh");meshGo.transform.SetParent(rig.transform,false);
var skin=meshGo.AddComponent<SkinnedMeshRenderer>();skin.sharedMesh=meshes[variants[i]];skin.bones=bones;skin.rootBone=bones[0];
skin.sharedMaterial=material;skin.quality=SkinQuality.Auto;skin.updateWhenOffscreen=true;
skin.localBounds=new Bounds(new Vector3(0,.75f,0),new Vector3(3,3,3));
viewer.actors[i]=new CharacterAnimationLab.Actor{label=labels[i],rig=rig,hips=bones[0],
rightHand=bones.First(b=>b.name=="mixamorig:RightHand"),renderer=skin,
walkingClip=i==0?walk33:walk41,
translationScale=actorData.translationScale,translationOffsets=actorData.translationOffsets.Select(x=>V(x)).ToArray(),
translationDeltaRotations=actorData.translationDeltaRotations?.Select(Rotation).ToArray()??Array.Empty<Quaternion>(),
bindRotationCorrections=actorData.bindRotationCorrections?.Select(Rotation).ToArray()??Array.Empty<Quaternion>(),
rotationParents=actorData.rotationParents??Array.Empty<int>()};
}
UnityEngine.Object.DestroyImmediate(viewer.ui.gameObject);
var ui=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(uiPath),root.transform);
viewer.ui=ui.transform;ui.GetComponentInChildren<AnimationLabSeekBar>().viewer=viewer;
viewer.texturedMaterial=material;viewer.motion=0;viewer.time=10f/30;viewer.focus=-1;viewer.detail=0;viewer.yaw=0;viewer.textured=true;viewer.playing=true;
viewer.Sample();
RenderSettings.ambientMode=AmbientMode.Flat;RenderSettings.ambientLight=new Color(.65f,.69f,.73f);RenderSettings.fog=false;
if (!EditorSceneManager.SaveScene(scene,targetRoot+"/LmGirlAnimationLab.unity"))
throw new IOException("LmGirl scene was not saved.");
PrefabUtility.SaveAsPrefabAsset(root,generated+"/LmGirlAnimationLab.prefab");
// Keep the rejected generic-cleaning experiments inspectable as
// persistent prefabs without promoting them to the default lineup.
for(int b=0;b<data.bones.Length;b++)SetTRS(viewer.actors[1].renderer.bones[b],data.bones[b].rest);
for(int b=0;b<website.bones.Length;b++)SetTRS(viewer.actors[0].renderer.bones[b],website.bones[b].rest);
PrefabUtility.SaveAsPrefabAsset(viewer.actors[0].rig,generated+"/LmGirl_mixamo.prefab");
foreach (string variant in new[]{"raw","post","clean","clean_eps","local"})
{
var actor=viewer.actors[1];actor.renderer.sharedMesh=meshes[variant];
PrefabUtility.SaveAsPrefabAsset(actor.rig,generated+"/LmGirl_"+variant+".prefab");
}
AssetDatabase.SaveAssets();
return JsonConvert.SerializeObject(new{scene=targetRoot+"/LmGirlAnimationLab.unity",visibleVariants=4,websiteBones=website.bones.Length,tokenRigBones=data.bones.Length,triangles=4999,sourceClips=viewer.clips.Select(c=>AssetDatabase.GetAssetPath(c)),walkingClips=new[]{AssetDatabase.GetAssetPath(walk33),AssetDatabase.GetAssetPath(walk41)},translationScale=data.translationScale});
}
finally
{
if (previous.IsValid()&&previous.isLoaded)SceneManager.SetActiveScene(previous);
EditorSceneManager.CloseScene(scene,true);
}
}
public static string OpenLmGirl()
{
if (EditorSceneManager.GetSceneManagerSetup().Any(s=>SceneManager.GetSceneByPath(s.path).isDirty))throw new InvalidOperationException("Existing scene has unsaved changes.");
EditorSceneManager.OpenScene(Root+"/LmGirl/LmGirlAnimationLab.unity",OpenSceneMode.Single);
Selection.activeGameObject=UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>().gameObject;
return SceneManager.GetActiveScene().path;
}
public static string RefineLayout()
{
if (EditorApplication.isPlaying) throw new InvalidOperationException("Edit Mode required.");
var viewer = UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>();
if (!viewer || viewer.gameObject.scene.path != ScenePath) throw new InvalidOperationException("Open the comparison scene first.");
for (int i=0;i<viewer.actors.Length;i++)
{
var actor=viewer.actors[i];
actor.rig.transform.parent.localPosition=new Vector3((i-1)*.99f,0,0);
actor.rightHand=actor.renderer.bones.First(b=>b.name=="mixamorig:RightHand");
}
viewer.Sample();
EditorUtility.SetDirty(viewer);
EditorSceneManager.MarkSceneDirty(viewer.gameObject.scene);
EditorSceneManager.SaveScene(viewer.gameObject.scene);
PrefabUtility.SaveAsPrefabAsset(viewer.gameObject,Generated+"/CharacterAnimationLab.prefab");
AssetDatabase.SaveAssets();
return "Saved adaptive comparison framing, stage spacing and hand focus references.";
}
public static string Open()
{
if (EditorSceneManager.GetSceneManagerSetup().Any(s=>SceneManager.GetSceneByPath(s.path).isDirty))
throw new InvalidOperationException("Existing scene has unsaved changes; open comparison additively instead.");
EditorSceneManager.OpenScene(ScenePath,OpenSceneMode.Single);
Selection.activeGameObject=UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>().gameObject;
return ScenePath;
}
}
#endif