/* * OrientationCalibrator * * 用途:调试 R02 (或任意 3D View 子物体) 的朝向 + 缩放 + 偏移, * 一键 apply 到 prefab 上持久化保存。 * * 默认目标:选中场景里的 GameObject (含 "View" 子物体),然后菜单 Tools/OctoberStudio/VAT/Orientation Calibrator */ using System.Linq; using UnityEditor; using UnityEngine; namespace OctoberStudio.EditorTools { public class OrientationCalibrator : EditorWindow { private GameObject _target; private Transform _view; private Vector3 _euler = new Vector3(0f, 180f, 0f); private float _uniformScale = 0.5f; private Vector3 _localPos = Vector3.zero; [MenuItem("Tools/OctoberStudio/VAT/Orientation Calibrator", priority = 10)] public static void ShowWindow() { var win = GetWindow("Orientation Calibrator"); win.AutoPickTarget(); } private void OnEnable() => AutoPickTarget(); private void OnSelectionChange() { AutoPickTarget(); Repaint(); } private void AutoPickTarget() { if (Selection.activeGameObject != null) { _target = Selection.activeGameObject; _view = _target.transform.Find("View"); if (_view != null) { _euler = _view.localEulerAngles; var s = _view.localScale; _uniformScale = (s.x + s.y + s.z) / 3f; _localPos = _view.localPosition; } } } private void OnGUI() { EditorGUILayout.LabelField("Orientation Calibrator", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "1) 在场景里选中 Wizard_R02 (或任意含 'View' 子物体的角色)\n" + "2) 点 Quick Face 按钮预览朝向\n" + "3) 调好后点 [Apply to Prefab] 持久化", MessageType.Info); _target = (GameObject)EditorGUILayout.ObjectField("Target", _target, typeof(GameObject), true); if (_target == null) return; _view = _target.transform.Find("View"); if (_view == null) { EditorGUILayout.HelpBox("Target 没有名为 'View' 的子物体", MessageType.Warning); return; } EditorGUILayout.Space(); EditorGUILayout.LabelField("Quick Face (相机看 +Z 方向)", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Face Camera\n(Y=180)")) Apply(new Vector3(0, 180, 0)); if (GUILayout.Button("Face Right\n(Y=270)")) Apply(new Vector3(0, 270, 0)); if (GUILayout.Button("Face Left\n(Y=90)")) Apply(new Vector3(0, 90, 0)); if (GUILayout.Button("Face Away\n(Y=0)")) Apply(new Vector3(0, 0, 0)); } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Lay Down +\n(X=-90)")) Apply(new Vector3(-90, 0, 0)); if (GUILayout.Button("Lay Down -\n(X=90)")) Apply(new Vector3(90, 0, 0)); if (GUILayout.Button("Tilt Up 15°")) Apply(_euler + new Vector3(-15, 0, 0)); if (GUILayout.Button("Tilt Down 15°")) Apply(_euler + new Vector3(15, 0, 0)); } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Rotate Y -45")) Apply(_euler + new Vector3(0, -45, 0)); if (GUILayout.Button("Rotate Y -15")) Apply(_euler + new Vector3(0, -15, 0)); if (GUILayout.Button("Rotate Y +15")) Apply(_euler + new Vector3(0, 15, 0)); if (GUILayout.Button("Rotate Y +45")) Apply(_euler + new Vector3(0, 45, 0)); } EditorGUILayout.Space(); EditorGUILayout.LabelField("Manual", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); _euler = EditorGUILayout.Vector3Field("Local Euler", _euler); _uniformScale = EditorGUILayout.Slider("Uniform Scale", _uniformScale, 0.05f, 5f); _localPos = EditorGUILayout.Vector3Field("Local Position", _localPos); if (EditorGUI.EndChangeCheck()) ApplyToScene(); EditorGUILayout.Space(); EditorGUILayout.LabelField("Pivot Helper (基于 MeshRenderer.bounds 自动校正)", EditorStyles.boldLabel); if (GUILayout.Button("Auto Center Pivot (基于 bounds 把模型对齐到原点)")) { AutoCenterPivot(); } EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(_view == null)) { if (GUILayout.Button("Apply to Prefab (持久化)", GUILayout.Height(36))) { ApplyToPrefab(); } } } private void Apply(Vector3 newEuler) { _euler = NormalizeEuler(newEuler); ApplyToScene(); } private static Vector3 NormalizeEuler(Vector3 e) { return new Vector3( Mathf.Repeat(e.x + 180f, 360f) - 180f, Mathf.Repeat(e.y + 180f, 360f) - 180f, Mathf.Repeat(e.z + 180f, 360f) - 180f ); } private void ApplyToScene() { if (_view == null) return; Undo.RecordObject(_view, "Calibrate View"); _view.localEulerAngles = _euler; _view.localScale = Vector3.one * _uniformScale; _view.localPosition = _localPos; EditorUtility.SetDirty(_view); SceneView.RepaintAll(); } private void AutoCenterPivot() { if (_view == null) return; var mr = _view.GetComponentInChildren(); if (mr == null) { Debug.LogWarning("No MeshRenderer under View"); return; } // bounds.center in world → 转 view local → 反向偏移 var centerWorld = mr.bounds.center; var centerLocalInView = _view.InverseTransformPoint(centerWorld); // 想让模型中心对齐到 View 的原点:localPos -= local-space offset (scaled) // 简化:直接把 mesh.localBounds.center 当偏移 (避免世界缩放影响) var mf = _view.GetComponentInChildren(); if (mf != null && mf.sharedMesh != null) { var meshCenter = mf.sharedMesh.bounds.center; // model-space _localPos = -Vector3.Scale(meshCenter, Vector3.one * _uniformScale); ApplyToScene(); Debug.Log($"[Calibrator] Auto-center: model bounds.center={meshCenter}, set View.localPos={_localPos}"); } } private void ApplyToPrefab() { if (_target == null) return; ApplyToScene(); // Try outermost prefab instance and apply overrides var inst = PrefabUtility.GetNearestPrefabInstanceRoot(_target); if (inst == null) { EditorUtility.DisplayDialog("Calibrator", "Target 不是 prefab 实例", "OK"); return; } // 1) Apply property modifications (rotation/scale/position) of the View transform back to its corresponding prefab asset var path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(inst); if (string.IsNullOrEmpty(path)) { EditorUtility.DisplayDialog("Calibrator", "找不到 prefab 资产路径", "OK"); return; } // Apply overrides on the entire instance try { PrefabUtility.ApplyPrefabInstance(inst, InteractionMode.AutomatedAction); AssetDatabase.SaveAssets(); Debug.Log($"[Calibrator] Applied overrides to prefab: {path}"); EditorUtility.DisplayDialog("Calibrator", $"已 Apply 到 prefab:\n{path}", "OK"); } catch (System.Exception e) { Debug.LogException(e); EditorUtility.DisplayDialog("Calibrator", $"Apply 失败: {e.Message}", "OK"); } } } }