#if UNITY_EDITOR using System.IO; using System.Linq; using HotUpdate.Gameplay; using UnityEditor; using UnityEditor.Animations; using UnityEngine; /// /// Studio mesh + Mixamo TokenRig + Kimodo idle/walk. /// unity command run_script --file AgentScripts/SetupCampGuideStudio.cs --entry SetupCampGuideStudio.Run /// 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($"{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($"{Root}/{Actor}_Rigged.prefab"); var animator = rig.GetComponentInChildren(true); var bone = rig.GetComponentsInChildren(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($"{Root}/Rigged/{Actor}_Rigged.fbx"); if (rig == null) return "missing"; return string.Join(" | ", rig.GetComponentsInChildren(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(fbx); var bones = rig.GetComponentsInChildren(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(anim); var actual = other.GetComponentsInChildren(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(matPath) ?? new Material(shader); mat.shader = shader; var albedo = AssetDatabase.LoadAssetAtPath(basePath); var mix = AssetDatabase.LoadAssetAtPath(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(matPath) == null) AssetDatabase.CreateAsset(mat, matPath); else EditorUtility.SetDirty(mat); return AssetDatabase.LoadAssetAtPath(matPath); } static AnimatorController EnsureController() { var path = $"{Root}/{Actor}_Animator.controller"; var controller = AssetDatabase.LoadAssetAtPath(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().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(dest); if (existing == null) AssetDatabase.CreateAsset(copy, dest); else { EditorUtility.CopySerialized(copy, existing); Object.DestroyImmediate(copy); } return AssetDatabase.LoadAssetAtPath(dest); } static string EnsurePrefab(string fbx, RuntimeAnimatorController controller, Material material) { var modelRoot = AssetDatabase.LoadAssetAtPath(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(true)) renderer.sharedMaterial = material; var animator = model.GetComponent() ?? model.AddComponent(); animator.runtimeAnimatorController = controller; animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; if (instance.GetComponent() == null) instance.AddComponent(); 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()) { 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(); 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()) { 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($"{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.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.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(); anim.applyRootMotion = false; var model = actor.transform.Find("Model").gameObject; var skin = actor.GetComponentInChildren(); var notes = new System.Text.StringBuilder(); foreach (var shot in new[] { "Idle", "Walk" }) { var sampled = AssetDatabase.LoadAssetAtPath($"{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().sharedMesh = mesh; var posedRenderer = posed.AddComponent(); 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