194 lines
14 KiB
C#
194 lines
14 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 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
|