Files
2022-07-04 13:15:35 +03:00

463 lines
15 KiB
C#

using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
#if UNITY_5_5_OR_NEWER
using UnityEngine.AI;
#endif
public class AdjustPivot : EditorWindow
{
private const string GENERATED_COLLIDER_NAME = "__GeneratedCollider";
private const string GENERATED_NAVMESH_OBSTACLE_NAME = "__GeneratedNavMeshObstacle";
private const string GENERATED_EMPTY_PARENT_NAME = "__GeneratedParent";
private const string UNDO_CREATE_PIVOT_REFERENCE = "Create Pivot Reference";
private const string UNDO_ADJUST_PIVOT = "Move Pivot";
private const string UNDO_SAVE_MODEL_AS = "Save Model As";
private bool createColliderObjectOnPivotChange = false;
private bool createNavMeshObstacleObjectOnPivotChange = false;
private readonly GUILayoutOption headerHeight = GUILayout.Height( 25 );
private GUIStyle buttonStyle;
private GUIStyle headerStyle;
private Vector3 selectionPrevPos;
private Vector3 selectionPrevRot;
private Vector2 scrollPos = Vector2.zero;
[MenuItem( "Window/Adjust Pivot" )]
private static void Init()
{
AdjustPivot window = GetWindow<AdjustPivot>();
window.titleContent = new GUIContent( "Adjust Pivot" );
window.minSize = new Vector2( 330f, 200f );
window.Show();
}
private void OnEnable()
{
GetPrefs();
Selection.selectionChanged += Repaint;
EditorApplication.update += OnUpdate;
}
private void OnDisable()
{
Selection.selectionChanged -= Repaint;
EditorApplication.update -= OnUpdate;
}
private void OnUpdate()
{
Transform selection = Selection.activeTransform;
if( !IsNull( selection ) )
{
Vector3 pos = selection.localPosition;
Vector3 rot = selection.localEulerAngles;
if( pos != selectionPrevPos || rot != selectionPrevRot )
{
Repaint();
selectionPrevPos = pos;
selectionPrevRot = rot;
}
}
}
private void OnGUI()
{
if( buttonStyle == null )
{
buttonStyle = new GUIStyle( GUI.skin.button ) { richText = true, wordWrap = true, padding = new RectOffset( 7, 7, 7, 7 ) };
headerStyle = new GUIStyle( GUI.skin.box ) { alignment = TextAnchor.MiddleCenter };
}
scrollPos = EditorGUILayout.BeginScrollView( scrollPos );
GUILayout.Box( "ADJUST PIVOT", headerStyle, GUILayout.ExpandWidth( true ), headerHeight );
Transform selection = Selection.activeTransform;
if( !IsNull( selection ) )
{
if( !IsNull( selection.parent ) )
{
#if UNITY_2018_3_OR_NEWER
if( UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage() == null )
{
#endif
if( selection.localPosition != Vector3.zero || selection.localEulerAngles != Vector3.zero )
{
if( GUILayout.Button( string.Concat( "Move <b>", selection.parent.name, "</b>'s pivot here" ), buttonStyle ) )
SetParentPivot( selection );
if( selection.localEulerAngles != Vector3.zero )
{
Vector3 parentScale = selection.parent.localScale;
if( Mathf.Approximately( parentScale.x, parentScale.y ) && Mathf.Approximately( parentScale.x, parentScale.z ) )
EditorGUILayout.HelpBox( string.Concat( "Pivot will also be rotated to match ", selection.name, "'s rotation." ), MessageType.None );
else
EditorGUILayout.HelpBox( string.Concat( "Edge case! 1) ", selection.parent.name, " has non-uniform scale and 2) ", selection.name, " is rotated.\n\nTo change the pivot correctly in this scenario, ", selection.parent.name, " will be parented to an empty GameObject with the same non-uniform scale value." ), MessageType.Warning );
}
}
else
{
GUI.enabled = false;
GUILayout.Button( "Selected object is at pivot position", buttonStyle );
GUI.enabled = true;
}
#if UNITY_2018_3_OR_NEWER
}
else
{
GUI.enabled = false;
GUILayout.Button( "Modifying prefabs directly is not allowed, create an instance in the scene instead!", buttonStyle );
GUI.enabled = true;
}
#endif
}
else
{
GUI.enabled = false;
GUILayout.Button( "Selected object has no parent", buttonStyle );
GUI.enabled = true;
}
}
else
{
GUI.enabled = false;
GUILayout.Button( "Nothing is selected", buttonStyle );
GUI.enabled = true;
}
GUILayout.Space( 15f );
GUILayout.Box( "MESH UTILITY", headerStyle, GUILayout.ExpandWidth( true ), headerHeight );
EditorGUILayout.HelpBox( "If an object has a MeshFilter, changing its pivot will modify the mesh. That modified mesh must be saved before it can be applied to prefab.", MessageType.None );
if( !IsNull( selection ) )
{
MeshFilter meshFilter = selection.GetComponent<MeshFilter>();
if( !IsNull( meshFilter ) && !IsNull( meshFilter.sharedMesh ) )
{
if( GUILayout.Button( string.Concat( "Save <b>", selection.name, "</b>'s mesh as Asset (Recommended)" ), buttonStyle ) )
SaveMesh( meshFilter, true );
GUILayout.Space( 5f );
if( GUILayout.Button( string.Concat( "Save <b>", selection.name, "</b>'s mesh as OBJ" ), buttonStyle ) )
SaveMesh( meshFilter, false );
}
else
{
GUI.enabled = false;
GUILayout.Button( "Selected object has no mesh", buttonStyle );
GUI.enabled = true;
}
}
else
{
GUI.enabled = false;
GUILayout.Button( "Nothing is selected", buttonStyle );
GUI.enabled = true;
}
GUILayout.Space( 15f );
GUILayout.Box( "SETTINGS", headerStyle, GUILayout.ExpandWidth( true ), headerHeight );
EditorGUI.BeginChangeCheck();
createColliderObjectOnPivotChange = EditorGUILayout.ToggleLeft( "Create Child Collider Object On Pivot Change", createColliderObjectOnPivotChange );
EditorGUILayout.HelpBox( "Note that original collider(s) (if exists) will not be destroyed automatically.", MessageType.None );
if( EditorGUI.EndChangeCheck() )
EditorPrefs.SetBool( "AdjustPivotCreateColliders", createColliderObjectOnPivotChange );
GUILayout.Space( 10f );
EditorGUI.BeginChangeCheck();
createNavMeshObstacleObjectOnPivotChange = EditorGUILayout.ToggleLeft( "Create Child NavMesh Obstacle Object On Pivot Change", createNavMeshObstacleObjectOnPivotChange );
EditorGUILayout.HelpBox( "Note that original NavMesh Obstacle (if exists) will not be destroyed automatically.", MessageType.None );
if( EditorGUI.EndChangeCheck() )
EditorPrefs.SetBool( "AdjustPivotCreateNavMeshObstacle", createNavMeshObstacleObjectOnPivotChange );
GUILayout.Space( 10f );
EditorGUILayout.EndScrollView();
}
private void GetPrefs()
{
createColliderObjectOnPivotChange = EditorPrefs.GetBool( "AdjustPivotCreateColliders", false );
createNavMeshObstacleObjectOnPivotChange = EditorPrefs.GetBool( "AdjustPivotCreateNavMeshObstacle", false );
}
private void SetParentPivot( Transform pivot )
{
Transform pivotParent = pivot.parent;
if( IsPrefab( pivotParent ) )
{
Debug.LogWarning( "Modifying prefabs directly is not allowed, create an instance in the scene instead!" );
return;
}
if( pivot.localPosition == Vector3.zero && pivot.localEulerAngles == Vector3.zero )
{
Debug.LogWarning( "Pivot hasn't changed!" );
return;
}
if( pivot.localEulerAngles != Vector3.zero )
{
Vector3 parentScale = pivotParent.localScale;
if( !Mathf.Approximately( parentScale.x, parentScale.y ) || !Mathf.Approximately( parentScale.x, parentScale.z ) )
{
// This is an edge case (object has non-uniform scale and pivot is rotated). We must create an empty parent GameObject in this scenario
GameObject emptyParentObject = new GameObject( GENERATED_EMPTY_PARENT_NAME );
if( !IsNull( pivotParent.parent ) )
emptyParentObject.transform.SetParent( pivotParent.parent, false );
else
SceneManager.MoveGameObjectToScene( emptyParentObject, pivotParent.gameObject.scene );
emptyParentObject.transform.localPosition = pivotParent.localPosition;
emptyParentObject.transform.localRotation = pivotParent.localRotation;
emptyParentObject.transform.localScale = pivotParent.localScale;
Undo.RegisterCreatedObjectUndo( emptyParentObject, UNDO_ADJUST_PIVOT );
Undo.SetTransformParent( pivotParent, emptyParentObject.transform, UNDO_ADJUST_PIVOT );
// Automatically expand the newly created empty parent GameObject in Hierarchy
EditorGUIUtility.PingObject( pivot.gameObject );
}
}
MeshFilter meshFilter = pivotParent.GetComponent<MeshFilter>();
Mesh originalMesh = null;
if( !IsNull( meshFilter ) && !IsNull( meshFilter.sharedMesh ) )
{
Undo.RecordObject( meshFilter, UNDO_ADJUST_PIVOT );
originalMesh = meshFilter.sharedMesh;
Mesh mesh = Instantiate( meshFilter.sharedMesh );
meshFilter.sharedMesh = mesh;
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Vector4[] tangents = mesh.tangents;
if( pivot.localPosition != Vector3.zero )
{
Vector3 deltaPosition = -pivot.localPosition;
for( int i = 0; i < vertices.Length; i++ )
vertices[i] += deltaPosition;
}
if( pivot.localEulerAngles != Vector3.zero )
{
Quaternion deltaRotation = Quaternion.Inverse( pivot.localRotation );
for( int i = 0; i < vertices.Length; i++ )
{
vertices[i] = deltaRotation * vertices[i];
normals[i] = deltaRotation * normals[i];
Vector3 tangentDir = deltaRotation * tangents[i];
tangents[i] = new Vector4( tangentDir.x, tangentDir.y, tangentDir.z, tangents[i].w );
}
}
mesh.vertices = vertices;
mesh.normals = normals;
mesh.tangents = tangents;
mesh.RecalculateBounds();
}
GetPrefs();
Collider[] colliders = pivotParent.GetComponents<Collider>();
foreach( Collider collider in colliders )
{
MeshCollider meshCollider = collider as MeshCollider;
if( !IsNull( meshCollider ) && !IsNull( originalMesh ) && meshCollider.sharedMesh == originalMesh )
{
Undo.RecordObject( meshCollider, UNDO_ADJUST_PIVOT );
meshCollider.sharedMesh = meshFilter.sharedMesh;
}
}
if( createColliderObjectOnPivotChange && IsNull( pivotParent.Find( GENERATED_COLLIDER_NAME ) ) )
{
GameObject colliderObj = null;
foreach( Collider collider in colliders )
{
if( IsNull( collider ) )
continue;
MeshCollider meshCollider = collider as MeshCollider;
if( IsNull( meshCollider ) || meshCollider.sharedMesh != meshFilter.sharedMesh )
{
if( colliderObj == null )
{
colliderObj = new GameObject( GENERATED_COLLIDER_NAME );
colliderObj.transform.SetParent( pivotParent, false );
}
EditorUtility.CopySerialized( collider, colliderObj.AddComponent( collider.GetType() ) );
}
}
if( colliderObj != null )
Undo.RegisterCreatedObjectUndo( colliderObj, UNDO_ADJUST_PIVOT );
}
if( createNavMeshObstacleObjectOnPivotChange && IsNull( pivotParent.Find( GENERATED_NAVMESH_OBSTACLE_NAME ) ) )
{
NavMeshObstacle obstacle = pivotParent.GetComponent<NavMeshObstacle>();
if( !IsNull( obstacle ) )
{
GameObject obstacleObj = new GameObject( GENERATED_NAVMESH_OBSTACLE_NAME );
obstacleObj.transform.SetParent( pivotParent, false );
EditorUtility.CopySerialized( obstacle, obstacleObj.AddComponent( obstacle.GetType() ) );
Undo.RegisterCreatedObjectUndo( obstacleObj, UNDO_ADJUST_PIVOT );
}
}
Transform[] children = new Transform[pivotParent.childCount];
Vector3[] childrenPositions = new Vector3[children.Length];
Quaternion[] childrenRotations = new Quaternion[children.Length];
for( int i = children.Length - 1; i >= 0; i-- )
{
children[i] = pivotParent.GetChild( i );
childrenPositions[i] = children[i].position;
childrenRotations[i] = children[i].rotation;
Undo.RecordObject( children[i], UNDO_ADJUST_PIVOT );
}
Undo.RecordObject( pivotParent, UNDO_ADJUST_PIVOT );
pivotParent.position = pivot.position;
pivotParent.rotation = pivot.rotation;
for( int i = 0; i < children.Length; i++ )
{
children[i].position = childrenPositions[i];
children[i].rotation = childrenRotations[i];
}
pivot.localPosition = Vector3.zero;
pivot.localRotation = Quaternion.identity;
}
private void SaveMesh( MeshFilter meshFilter, bool saveAsAsset )
{
if( IsPrefab( meshFilter ) )
{
Debug.LogWarning( "Modifying prefabs directly is not allowed, create an instance in the scene instead!" );
return;
}
string savedMeshName = meshFilter.sharedMesh.name;
while( savedMeshName.EndsWith( "(Clone)" ) )
savedMeshName = savedMeshName.Substring( 0, savedMeshName.Length - 7 );
string savePath = EditorUtility.SaveFilePanelInProject( "Save As", savedMeshName, saveAsAsset ? "asset" : "obj", string.Empty );
if( string.IsNullOrEmpty( savePath ) )
return;
Mesh originalMesh = meshFilter.sharedMesh;
Mesh savedMesh = saveAsAsset ? SaveMeshAsAsset( meshFilter, savePath ) : SaveMeshAsOBJ( meshFilter, savePath );
if( meshFilter.sharedMesh != savedMesh )
{
Undo.RecordObject( meshFilter, UNDO_SAVE_MODEL_AS );
meshFilter.sharedMesh = savedMesh;
}
MeshCollider[] meshColliders = meshFilter.GetComponents<MeshCollider>();
foreach( MeshCollider meshCollider in meshColliders )
{
if( !IsNull( meshCollider ) && meshCollider.sharedMesh == originalMesh && meshCollider.sharedMesh != savedMesh )
{
Undo.RecordObject( meshCollider, UNDO_SAVE_MODEL_AS );
meshCollider.sharedMesh = savedMesh;
}
}
}
private Mesh SaveMeshAsAsset( MeshFilter meshFilter, string savePath )
{
Mesh mesh = meshFilter.sharedMesh;
if( !string.IsNullOrEmpty( AssetDatabase.GetAssetPath( mesh ) ) ) // If mesh is an asset, clone it
mesh = Instantiate( mesh );
AssetDatabase.CreateAsset( mesh, savePath );
AssetDatabase.SaveAssets();
return mesh;
}
//Credit: http://wiki.unity3d.com/index.php?title=ObjExporter
private Mesh SaveMeshAsOBJ( MeshFilter meshFilter, string savePath )
{
Mesh mesh = meshFilter.sharedMesh;
Renderer renderer = meshFilter.GetComponent<Renderer>();
Material[] mats = !IsNull( renderer ) ? renderer.sharedMaterials : null;
StringBuilder meshString = new StringBuilder();
meshString.Append( "g " ).Append( Path.GetFileNameWithoutExtension( savePath ) ).Append( "\n" );
foreach( Vector3 v in mesh.vertices )
meshString.Append( string.Format( CultureInfo.InvariantCulture, "v {0} {1} {2}\n", -v.x, v.y, v.z ) );
meshString.Append( "\n" );
foreach( Vector3 v in mesh.normals )
meshString.Append( string.Format( CultureInfo.InvariantCulture, "vn {0} {1} {2}\n", -v.x, v.y, v.z ) );
meshString.Append( "\n" );
foreach( Vector3 v in mesh.uv )
meshString.Append( string.Format( CultureInfo.InvariantCulture, "vt {0} {1}\n", v.x, v.y ) );
for( int material = 0; material < mesh.subMeshCount; material++ )
{
meshString.Append( "\n" );
if( mats != null && mats.Length > material )
{
meshString.Append( "usemtl " ).Append( mats[material].name ).Append( "\n" );
meshString.Append( "usemap " ).Append( mats[material].name ).Append( "\n" );
}
int[] triangles = mesh.GetTriangles( material );
for( int i = 0; i < triangles.Length; i += 3 )
{
meshString.Append( string.Format( "f {1}/{1}/{1} {0}/{0}/{0} {2}/{2}/{2}\n",
triangles[i] + 1, triangles[i + 1] + 1, triangles[i + 2] + 1 ) );
}
}
File.WriteAllText( savePath, meshString.ToString() );
AssetDatabase.ImportAsset( savePath, ImportAssetOptions.ForceUpdate );
return AssetDatabase.LoadAssetAtPath<Mesh>( savePath );
}
private bool IsPrefab( Object obj )
{
return AssetDatabase.Contains( obj );
}
private bool IsNull( Object obj )
{
return obj == null || obj.Equals( null );
}
}