Files

348 lines
16 KiB
C#

#if UNITY_EDITOR
using System.IO;
using System.Linq;
using HotUpdate.Gameplay;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
/// <summary>
/// Studio mesh + Mixamo TokenRig + Kimodo idle/walk.
/// unity command run_script --file AgentScripts/SetupCampGuideStudio.cs --entry SetupCampGuideStudio.Run
/// </summary>
public static class SetupCampGuideStudio
{
const string Root = "Assets/Bundles/Characters/Human/CampGuideStudio";
const string Actor = "CampGuideStudio";
const float Height = 1.55f;
public static string ClipInfo()
{
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>($"{Root}/Clips/Walk.anim");
var bindings = AnimationUtility.GetCurveBindings(clip);
var leg = bindings.FirstOrDefault(b => b.path.Contains("LeftUpLeg") && b.propertyName.EndsWith("m_LocalRotation.x"));
var curve = string.IsNullOrEmpty(leg.path) ? null : AnimationUtility.GetEditorCurve(clip, leg);
var rig = AssetDatabase.LoadAssetAtPath<GameObject>($"{Root}/{Actor}_Rigged.prefab");
var animator = rig.GetComponentInChildren<Animator>(true);
var bone = rig.GetComponentsInChildren<Transform>(true).FirstOrDefault(t => t.name.Contains("LeftUpLeg"));
string bonePath = bone == null || animator == null
? "missing"
: AnimationUtility.CalculateTransformPath(bone, animator.transform);
var ranges = new System.Text.StringBuilder();
foreach (var binding in bindings.Where(b => b.path.EndsWith("LeftUpLeg")))
{
var keys = AnimationUtility.GetEditorCurve(clip, binding).keys;
float lo = keys.Min(k => k.value), hi = keys.Max(k => k.value);
ranges.Append($"{binding.propertyName}:{lo:0.000}..{hi:0.000} ");
}
return $"curves={bindings.Length} len={clip.length:0.00} bonePath={bonePath} {ranges}";
}
public static string Dump()
{
AssetDatabase.Refresh();
var rig = AssetDatabase.LoadAssetAtPath<GameObject>($"{Root}/Rigged/{Actor}_Rigged.fbx");
if (rig == null) return "missing";
return string.Join(" | ", rig.GetComponentsInChildren<Transform>(true).Select(t => t.name).Take(40));
}
public static string Run()
{
AssetDatabase.Refresh();
var fbx = $"{Root}/Rigged/{Actor}_Rigged.fbx";
if (!File.Exists(fbx)) throw new FileNotFoundException("missing rig", fbx);
Configure(fbx, true, null);
Avatar avatar = null;
foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(fbx))
if (obj is Avatar found) { avatar = found; break; }
if (avatar == null) throw new System.InvalidOperationException("Mixamo avatar missing");
var rig = AssetDatabase.LoadAssetAtPath<GameObject>(fbx);
var bones = rig.GetComponentsInChildren<Transform>(true).Count(t => t.name.StartsWith("mixamorig:"));
if (bones < 30 || bones > 65) throw new System.InvalidOperationException("mixamorig count " + bones);
foreach (var clip in new[] { "Idle", "Walk" })
{
var anim = $"{Root}/Anims/{Actor}_{clip}.fbx";
Configure(anim, false, avatar);
var other = AssetDatabase.LoadAssetAtPath<GameObject>(anim);
var actual = other.GetComponentsInChildren<Transform>(true).Count(t => t.name.StartsWith("mixamorig:"));
if (actual != bones) throw new System.InvalidOperationException(clip + " skeleton " + actual + " != " + bones);
}
var mat = EnsureMaterial();
var controller = EnsureController();
var prefab = EnsurePrefab(fbx, controller, mat);
AssetDatabase.SaveAssets();
return $"bones={bones} prefab={prefab}";
}
static void Configure(string path, bool character, Avatar source)
{
var importer = (ModelImporter)AssetImporter.GetAtPath(path);
importer.animationType = ModelImporterAnimationType.Generic;
importer.avatarSetup = character ? ModelImporterAvatarSetup.CreateFromThisModel : ModelImporterAvatarSetup.CopyFromOther;
if (!character && source != null) importer.sourceAvatar = source;
importer.importAnimation = !character;
importer.materialImportMode = ModelImporterMaterialImportMode.None;
importer.globalScale = 1f;
importer.useFileScale = false;
importer.bakeAxisConversion = true;
importer.SaveAndReimport();
}
static Material EnsureMaterial()
{
var basePath = $"{Root}/{Actor}_base.png";
var mixPath = $"{Root}/{Actor}_mix.png";
foreach (var path in new[] { basePath, mixPath })
{
var tex = (TextureImporter)AssetImporter.GetAtPath(path);
tex.textureType = TextureImporterType.Default;
tex.sRGBTexture = !path.EndsWith("_mix.png");
tex.mipmapEnabled = true;
tex.SaveAndReimport();
}
var shader = Shader.Find("Test/XPbr");
if (shader == null) throw new System.InvalidOperationException("Test/XPbr missing");
var matPath = $"{Root}/{Actor}.mat";
var mat = AssetDatabase.LoadAssetAtPath<Material>(matPath) ?? new Material(shader);
mat.shader = shader;
var albedo = AssetDatabase.LoadAssetAtPath<Texture2D>(basePath);
var mix = AssetDatabase.LoadAssetAtPath<Texture2D>(mixPath);
if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", albedo);
if (mat.HasProperty("_MixTex")) mat.SetTexture("_MixTex", mix);
if (mat.HasProperty("_BaseMap")) mat.SetTexture("_BaseMap", albedo);
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.white);
if (mat.HasProperty("_Metallic")) mat.SetFloat("_Metallic", 1f);
if (mat.HasProperty("_Roughness")) mat.SetFloat("_Roughness", 1f);
if (mat.HasProperty("_AO")) mat.SetFloat("_AO", 1f);
if (mat.HasProperty("_AlphaTest")) mat.SetFloat("_AlphaTest", 0f);
mat.DisableKeyword("_ALPHATEST_ON");
if (AssetDatabase.LoadAssetAtPath<Material>(matPath) == null) AssetDatabase.CreateAsset(mat, matPath);
else EditorUtility.SetDirty(mat);
return AssetDatabase.LoadAssetAtPath<Material>(matPath);
}
static AnimatorController EnsureController()
{
var path = $"{Root}/{Actor}_Animator.controller";
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(path)
?? AnimatorController.CreateAnimatorControllerAtPath(path);
var machine = controller.layers[0].stateMachine;
foreach (var state in machine.states.ToArray()) machine.RemoveState(state.state);
foreach (var parameter in controller.parameters.ToArray()) controller.RemoveParameter(parameter);
controller.AddParameter("Motion", AnimatorControllerParameterType.Int);
var clipDir = $"{Root}/Clips";
Directory.CreateDirectory(Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Assets/Bundles/Characters/Human/CampGuideStudio/Clips"));
var idleClip = CopyClip("Idle", clipDir);
var walkClip = CopyClip("Walk", clipDir);
var idle = machine.AddState("Idle");
idle.motion = idleClip;
machine.defaultState = idle;
var walk = machine.AddState("Walk");
walk.motion = walkClip;
var toWalk = idle.AddTransition(walk);
toWalk.hasExitTime = false;
toWalk.duration = 0.1f;
toWalk.AddCondition(AnimatorConditionMode.Equals, 1, "Motion");
var toIdle = walk.AddTransition(idle);
toIdle.hasExitTime = false;
toIdle.duration = 0.1f;
toIdle.AddCondition(AnimatorConditionMode.Equals, 0, "Motion");
EditorUtility.SetDirty(controller);
return controller;
}
static AnimationClip CopyClip(string name, string clipDir)
{
var src = AssetDatabase.LoadAllAssetsAtPath($"{Root}/Anims/{Actor}_{name}.fbx")
.OfType<AnimationClip>().First(c => !c.name.StartsWith("__preview__"));
var copy = Object.Instantiate(src);
copy.name = name;
var settings = AnimationUtility.GetAnimationClipSettings(copy);
settings.loopTime = true;
AnimationUtility.SetAnimationClipSettings(copy, settings);
var dest = $"{clipDir}/{name}.anim";
var existing = AssetDatabase.LoadAssetAtPath<AnimationClip>(dest);
if (existing == null) AssetDatabase.CreateAsset(copy, dest);
else { EditorUtility.CopySerialized(copy, existing); Object.DestroyImmediate(copy); }
return AssetDatabase.LoadAssetAtPath<AnimationClip>(dest);
}
static string EnsurePrefab(string fbx, RuntimeAnimatorController controller, Material material)
{
var modelRoot = AssetDatabase.LoadAssetAtPath<GameObject>(fbx);
var instance = new GameObject(Actor + "_Rigged");
var model = (GameObject)PrefabUtility.InstantiatePrefab(modelRoot, instance.transform);
model.name = "Model";
var bounds = BindBounds(model);
float scale = Height / bounds.size.y;
model.transform.localScale = Vector3.one * scale;
model.transform.localPosition = -new Vector3(bounds.center.x, bounds.min.y, bounds.center.z) * scale;
foreach (var renderer in model.GetComponentsInChildren<Renderer>(true)) renderer.sharedMaterial = material;
var animator = model.GetComponent<Animator>() ?? model.AddComponent<Animator>();
animator.runtimeAnimatorController = controller;
animator.applyRootMotion = false;
animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
if (instance.GetComponent<ExplorerLocomotion>() == null) instance.AddComponent<ExplorerLocomotion>();
SetLayer(instance.transform);
var path = $"{Root}/{Actor}_Rigged.prefab";
PrefabUtility.SaveAsPrefabAsset(instance, path);
Object.DestroyImmediate(instance);
return path;
}
static Bounds BindBounds(GameObject model)
{
Bounds bounds = default;
bool has = false;
foreach (var skin in model.GetComponentsInChildren<SkinnedMeshRenderer>())
{
var mesh = new Mesh();
skin.BakeMesh(mesh);
foreach (var vertex in mesh.vertices)
{
var point = model.transform.InverseTransformPoint(skin.transform.TransformPoint(vertex));
if (!has) { bounds = new Bounds(point, Vector3.zero); has = true; }
else bounds.Encapsulate(point);
}
Object.DestroyImmediate(mesh);
}
if (!has) throw new System.InvalidOperationException("no skinned mesh");
return bounds;
}
public static string Fit()
{
var path = $"{Root}/{Actor}_Rigged.prefab";
var root = PrefabUtility.LoadPrefabContents(path);
try
{
var model = root.transform.Find("Model");
var anim = model.GetComponent<Animator>();
anim.Play("Idle", 0, 0f);
anim.Update(0f);
var before = Measure(model.gameObject);
float factor = Height / Mathf.Max(0.01f, before.max - before.min);
model.localScale *= factor;
anim.Update(0f);
var after = Measure(model.gameObject);
model.localPosition -= new Vector3(0f, after.min, 0f);
PrefabUtility.SaveAsPrefabAsset(root, path);
return $"before={before.min:0.000}..{before.max:0.000} scale={model.localScale.y:0.000}";
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
static (float min, float max) Measure(GameObject model)
{
float min = float.MaxValue, max = float.MinValue;
foreach (var skin in model.GetComponentsInChildren<SkinnedMeshRenderer>())
{
var mesh = new Mesh();
skin.BakeMesh(mesh);
foreach (var vertex in mesh.vertices)
{
float y = skin.transform.TransformPoint(vertex).y;
if (y < min) min = y;
if (y > max) max = y;
}
Object.DestroyImmediate(mesh);
}
return (min, max);
}
public static string Review()
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>($"{Root}/{Actor}_Rigged.prefab");
var stage = new GameObject("CampGuideStudioReview");
var rt = new RenderTexture(480, 720, 16, RenderTextureFormat.ARGB32);
var tex = new Texture2D(480, 720, TextureFormat.RGB24, false);
var dir = Path.GetFullPath(Path.Combine(Application.dataPath, "../../Tools/output/campguide_studio/review"));
Directory.CreateDirectory(dir);
try
{
var actor = (GameObject)PrefabUtility.InstantiatePrefab(prefab, stage.transform);
var lightGo = new GameObject("Light");
lightGo.transform.SetParent(stage.transform, false);
var light = lightGo.AddComponent<Light>();
light.type = LightType.Directional;
light.intensity = 1.2f;
light.transform.rotation = Quaternion.Euler(40f, -30f, 0f);
var camGo = new GameObject("Camera");
camGo.transform.SetParent(stage.transform, false);
var camera = camGo.AddComponent<Camera>();
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = new Color(0.85f, 0.85f, 0.85f, 1f);
camera.orthographic = true;
camera.orthographicSize = 0.95f;
camera.nearClipPlane = 0.05f;
camera.farClipPlane = 20f;
camera.targetTexture = rt;
var anim = actor.GetComponentInChildren<Animator>();
anim.applyRootMotion = false;
var model = actor.transform.Find("Model").gameObject;
var skin = actor.GetComponentInChildren<SkinnedMeshRenderer>();
var notes = new System.Text.StringBuilder();
foreach (var shot in new[] { "Idle", "Walk" })
{
var sampled = AssetDatabase.LoadAssetAtPath<AnimationClip>($"{Root}/Clips/{shot}.anim");
sampled.SampleAnimation(model, shot == "Walk" ? 0.5f : 0.4f);
var mesh = new Mesh();
skin.BakeMesh(mesh);
var posed = new GameObject("Posed");
posed.transform.SetParent(skin.transform, false);
posed.AddComponent<MeshFilter>().sharedMesh = mesh;
var posedRenderer = posed.AddComponent<MeshRenderer>();
posedRenderer.sharedMaterial = skin.sharedMaterial;
skin.enabled = false;
float minY = float.MaxValue, maxY = float.MinValue;
foreach (var v in mesh.vertices)
{
var p = skin.transform.TransformPoint(v);
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
}
notes.Append($"{shot} y={minY:0.000}..{maxY:0.000} scale={actor.transform.Find("Model").localScale.y:0.000} bounds={skin.bounds.size.y:0.000} ");
foreach (var view in new[] { "front", "side" })
{
camera.transform.position = view == "front" ? new Vector3(0f, 0.8f, -2.4f) : new Vector3(2.4f, 0.8f, 0f);
camera.transform.LookAt(new Vector3(0f, 0.75f, 0f));
camera.Render();
RenderTexture.active = rt;
tex.ReadPixels(new Rect(0, 0, 480, 720), 0, 0);
tex.Apply();
RenderTexture.active = null;
File.WriteAllBytes(Path.Combine(dir, shot.ToLowerInvariant() + "_" + view + ".png"), tex.EncodeToPNG());
}
skin.enabled = true;
Object.DestroyImmediate(posed);
Object.DestroyImmediate(mesh);
}
camera.targetTexture = null;
return notes + dir;
}
finally
{
RenderTexture.active = null;
cameraCleanup(rt, tex, stage);
}
}
static void cameraCleanup(RenderTexture rt, Texture2D tex, GameObject stage)
{
Object.DestroyImmediate(tex);
rt.Release();
Object.DestroyImmediate(rt);
Object.DestroyImmediate(stage);
}
static void SetLayer(Transform transform)
{
transform.gameObject.layer = 31;
foreach (Transform child in transform) SetLayer(child);
}
}
#endif