Unity中从3D模型资产中批量提取材质

news2024/11/17 3:31:29

如何使用
只需在“项目”窗口中创建一个名为“编辑器”的文件夹,然后在其中添加此脚本即可。然后,打开Window-Batch Extract Materials,配置参数并点击“ Extract! ”。

在Unity 2019.1+上,可以将默认材质重映射条件配置为自动检测模型资源中嵌入的重复材质并为它们提取单个材质,而不是将它们提取为重复材质实例。
在这里插入图片描述

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;

public class BatchExtractMaterials : EditorWindow
{
	private enum ExtractMode { Extract = 0, Remap = 1, Ignore = 2 };

	[System.Serializable]
	private class ExtractData
	{
		public GameObject model;

		public List<string> materialNames = new List<string>();
		public List<Material> originalMaterials = new List<Material>();
		public List<Material> remappedMaterials = new List<Material>();
		public List<ExtractMode> materialExtractModes = new List<ExtractMode>();

		public ExtractData() { }
		public ExtractData( GameObject model ) { this.model = model; }
	}

	private class RemapAllPopup : EditorWindow
	{
		private List<Material> remapFrom = new List<Material>( 2 );
		private Material remapTo;
		private bool skipIgnoredMaterials;

		private Vector2 scrollPos;

		private System.Action<List<Material>, Material, bool> onRemapConfirmed;

		public static void ShowAt( Rect buttonRect, Vector2 size, System.Action<List<Material>, Material, bool> onRemapConfirmed )
		{
			buttonRect.position = GUIUtility.GUIToScreenPoint( buttonRect.position );

			remapAllPopup = GetWindow<RemapAllPopup>( true );
			remapAllPopup.position = new Rect( buttonRect.position + new Vector2( ( buttonRect.width - size.x ) * 0.5f, buttonRect.height ), size );
			remapAllPopup.minSize = size;
			remapAllPopup.titleContent = new GUIContent( "Remap All..." );
			remapAllPopup.skipIgnoredMaterials = EditorPrefs.GetBool( "BEM_SkipIgnoredMats", true );
			remapAllPopup.onRemapConfirmed = onRemapConfirmed;
			remapAllPopup.scrollPos = Vector2.zero;
			remapAllPopup.Show();
		}

		public static void Hide()
		{
			if( remapAllPopup )
			{
				remapAllPopup.Close();
				remapAllPopup = null;
			}
		}

		private void OnDestroy()
		{
			remapAllPopup = null;
		}

		private void OnGUI()
		{
			if( !remapAllPopup )
			{
				Close();
				GUIUtility.ExitGUI();
			}

			Event ev = Event.current;

			EditorGUILayout.LabelField( "This will find all materials that point to 'Remap From' and remap them to 'Remap To'. If 'Remap From' is empty, all materials will be remapped to 'Remap To'.", EditorStyles.wordWrappedLabel );

			scrollPos = EditorGUILayout.BeginScrollView( scrollPos );

			GUILayout.BeginHorizontal();
			GUILayout.Label( "Remap From (drag & drop here)" );

			if( remapFrom.Count == 0 )
				remapFrom.Add( null );

			// Allow drag & dropping materials to array
			// Credit: https://answers.unity.com/answers/657877/view.html
			if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) )
			{
				DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
				if( ev.type == EventType.DragPerform )
				{
					DragAndDrop.AcceptDrag();

					Object[] draggedObjects = DragAndDrop.objectReferences;
					for( int i = 0; i < draggedObjects.Length; i++ )
					{
						Material material = draggedObjects[i] as Material;
						if( !material )
							continue;

						if( !remapFrom.Contains( material ) )
						{
							bool replacedNullElement = false;
							for( int j = 0; j < remapFrom.Count; j++ )
							{
								if( !remapFrom[j] )
								{
									remapFrom[j] = material;
									replacedNullElement = true;
									break;
								}
							}

							if( !replacedNullElement )
								remapFrom.Add( material );
						}
					}
				}

				ev.Use();
			}

			if( GUILayout.Button( "+", GL_WIDTH_25 ) )
				remapFrom.Insert( 0, null );

			GUILayout.EndHorizontal();

			for( int i = 0; i < remapFrom.Count; i++ )
			{
				GUILayout.BeginHorizontal();

				remapFrom[i] = EditorGUILayout.ObjectField( GUIContent.none, remapFrom[i], typeof( Material ), false ) as Material;

				if( GUILayout.Button( "+", GL_WIDTH_25 ) )
					remapFrom.Insert( i + 1, null );

				if( GUILayout.Button( "-", GL_WIDTH_25 ) )
				{
					// Lists with no elements look ugly, always keep a dummy null variable
					if( remapFrom.Count > 1 )
						remapFrom.RemoveAt( i-- );
					else
						remapFrom[0] = null;
				}

				GUILayout.EndHorizontal();
			}

			EditorGUILayout.EndScrollView();

			remapTo = EditorGUILayout.ObjectField( "Remap To", remapTo, typeof( Material ), false ) as Material;

			EditorGUI.BeginChangeCheck();
			skipIgnoredMaterials = EditorGUILayout.Toggle( "Skip Ignored Materials", skipIgnoredMaterials );
			if( EditorGUI.EndChangeCheck() )
				EditorPrefs.SetBool( "BEM_SkipIgnoredMats", skipIgnoredMaterials );

			EditorGUILayout.Space();

			GUILayout.BeginHorizontal();
			if( GUILayout.Button( "Cancel" ) )
				Close();
			if( GUILayout.Button( "Apply" ) )
			{
				if( remapTo && onRemapConfirmed != null )
				{
					bool remapFromIsFilled = false;
					for( int i = 0; i < remapFrom.Count; i++ )
					{
						if( remapFrom[i] )
						{
							remapFromIsFilled = true;
							break;
						}
					}

					onRemapConfirmed( remapFromIsFilled ? remapFrom : null, remapTo, skipIgnoredMaterials );
				}

				Close();
			}
			GUILayout.EndHorizontal();

			GUILayout.Space( 5f );
		}
	}

	private const string HELP_TEXT =
		"- Extract: material will be extracted to the destination folder\n" +
		"- Remap: material will be remapped to an existing material asset" +
#if UNITY_2019_1_OR_NEWER
		" (when Remap is the default value, then it means that a material that satisfies 'Default Material Remap Conditions' was found)" +
#endif
		". If Remap points to an embedded material, then that embedded material will first be extracted\n" +
		"- Ignore: material's current value will stay intact (when Ignore is the default value, either the material couldn't be found " +
		"or it was already extracted)";

	private static readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f );
	private readonly GUILayoutOption GL_WIDTH_75 = GUILayout.Width( 75f );
	private readonly GUILayoutOption GL_MIN_WIDTH_50 = GUILayout.MinWidth( 50f );

	private string materialsFolder = "Assets/Materials";
	private List<ExtractData> modelData = new List<ExtractData>( 16 );

#if UNITY_2019_1_OR_NEWER
	private bool remappedMaterialNamesMustMatch = false;
	private bool remappedMaterialPropertiesMustMatch = true;
	private bool dontRemapExtractedMaterials = true;
	private bool dontRemapMaterialsAcrossDifferentModels = false;
#endif

	private bool inModelSelectionPhase = true;

	private Rect remapAllButtonRect;
	private static RemapAllPopup remapAllPopup;

	private Vector2 scrollPos;

	[MenuItem( "Window/Batch Extract Materials" )]
	private static void Init()
	{
		BatchExtractMaterials window = GetWindow<BatchExtractMaterials>();
		window.titleContent = new GUIContent( "Extract Materials" );
		window.minSize = new Vector2( 300f, 120f );
		window.Show();
	}

	private void OnDestroy()
	{
		// Close RemapAllPopup with this window
		RemapAllPopup.Hide();
	}

	private void OnFocus()
	{
		// Don't let RemapAllPopup be obstructed by this window
		// We are using delayCall because otherwise clicking an ObjectField in this window doesn't highlight that material in the Project window
		EditorApplication.delayCall += () =>
		{
			if( remapAllPopup )
				remapAllPopup.Focus();
		};
	}

	private void OnGUI()
	{
		scrollPos = EditorGUILayout.BeginScrollView( scrollPos );

		GUI.enabled = inModelSelectionPhase;
		DrawDestinationPathField();
		DrawModelsToProcessList();
		DrawMaterialRemapConditionsField();
		GUI.enabled = true;

		bool modelsToProcessListIsFilled = modelData.Find( ( data ) => data.model ) != null;

		if( inModelSelectionPhase )
		{
			GUI.enabled = modelsToProcessListIsFilled && !string.IsNullOrEmpty( materialsFolder ) && materialsFolder.StartsWith( "Assets" );
			if( GUILayout.Button( "Next" ) )
			{
				inModelSelectionPhase = false;
				CalculateRemappedMaterials();

				GUIUtility.ExitGUI();
			}
		}
		else
		{
			DrawMaterialRemapList();

			GUILayout.BeginHorizontal();

			if( GUILayout.Button( "Back" ) )
			{
				inModelSelectionPhase = true;
				RemapAllPopup.Hide();

				GUIUtility.ExitGUI();
			}

			Color c = GUI.backgroundColor;
			GUI.backgroundColor = Color.green;

			GUI.enabled = modelsToProcessListIsFilled;
			if( GUILayout.Button( "Extract!" ) )
			{
				inModelSelectionPhase = true;
				RemapAllPopup.Hide();

				ExtractMaterials();
				GUIUtility.ExitGUI();
			}

			GUI.backgroundColor = c;
			GUILayout.EndHorizontal();
		}

		GUI.enabled = true;

		EditorGUILayout.Space();
		EditorGUILayout.EndScrollView();
	}

	private void DrawDestinationPathField()
	{
		Event ev = Event.current;

		GUILayout.BeginHorizontal();

		materialsFolder = EditorGUILayout.TextField( "Extract Materials To", materialsFolder );

		// Allow drag & dropping a folder to the text field
		// Credit: https://answers.unity.com/answers/657877/view.html
		if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) )
		{
			DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
			if( ev.type == EventType.DragPerform )
			{
				DragAndDrop.AcceptDrag();

				string[] draggedFiles = DragAndDrop.paths;
				for( int i = 0; i < draggedFiles.Length; i++ )
				{
					if( !string.IsNullOrEmpty( draggedFiles[i] ) && AssetDatabase.IsValidFolder( draggedFiles[i] ) )
					{
						materialsFolder = draggedFiles[i];
						break;
					}
				}
			}

			ev.Use();
		}

		if( GUILayout.Button( "o", GL_WIDTH_25 ) )
		{
			string selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "Assets", "" );
			if( !string.IsNullOrEmpty( selectedPath ) )
			{
				selectedPath = selectedPath.Replace( '\\', '/' ) + "/";

				int relativePathIndex = selectedPath.IndexOf( "/Assets/" ) + 1;
				if( relativePathIndex > 0 )
					materialsFolder = selectedPath.Substring( relativePathIndex, selectedPath.Length - relativePathIndex - 1 );
			}

			GUIUtility.keyboardControl = 0; // Remove focus from active text field
		}

		GUILayout.EndHorizontal();
		EditorGUILayout.Space();
	}

	private void DrawModelsToProcessList()
	{
		Event ev = Event.current;

		GUILayout.BeginHorizontal();
		GUILayout.Label( "Models To Process (drag & drop here)" );

		if( modelData.Count == 0 )
			modelData.Add( new ExtractData() );

		// Allow drag & dropping models to array
		// Credit: https://answers.unity.com/answers/657877/view.html
		if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) )
		{
			DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
			if( ev.type == EventType.DragPerform )
			{
				DragAndDrop.AcceptDrag();

				Object[] draggedObjects = DragAndDrop.objectReferences;
				for( int i = 0; i < draggedObjects.Length; i++ )
				{
					if( !( draggedObjects[i] as GameObject ) || PrefabUtility.GetPrefabAssetType( draggedObjects[i] ) != PrefabAssetType.Model )
						continue;

					bool modelAlreadyExists = false;
					for( int j = 0; j < modelData.Count; j++ )
					{
						if( modelData[j].model == draggedObjects[i] )
						{
							modelAlreadyExists = true;
							break;
						}
					}

					if( !modelAlreadyExists )
					{
						bool replacedNullElement = false;
						for( int j = 0; j < modelData.Count; j++ )
						{
							if( !modelData[j].model )
							{
								modelData[j] = new ExtractData( draggedObjects[i] as GameObject );
								replacedNullElement = true;
								break;
							}
						}

						if( !replacedNullElement )
							modelData.Add( new ExtractData( draggedObjects[i] as GameObject ) );
					}
				}
			}

			ev.Use();
		}

		if( GUILayout.Button( "+", GL_WIDTH_25 ) )
			modelData.Insert( 0, new ExtractData() );

		GUILayout.EndHorizontal();

		for( int i = 0; i < modelData.Count; i++ )
		{
			ExtractData element = modelData[i];

			GUI.changed = false;
			GUILayout.BeginHorizontal();

			GameObject prevObject = element.model;
			GameObject newObject = EditorGUILayout.ObjectField( GUIContent.none, prevObject, typeof( GameObject ), false ) as GameObject;
			if( newObject && PrefabUtility.GetPrefabAssetType( newObject ) != PrefabAssetType.Model )
				newObject = prevObject;

			modelData[i].model = newObject;

			if( GUILayout.Button( "+", GL_WIDTH_25 ) )
				modelData.Insert( i + 1, new ExtractData() );

			if( GUILayout.Button( "-", GL_WIDTH_25 ) )
			{
				// Lists with no elements look ugly, always keep a dummy null variable
				if( modelData.Count > 1 )
					modelData.RemoveAt( i-- );
				else
					modelData[0] = new ExtractData();
			}

			GUILayout.EndHorizontal();
		}

		EditorGUILayout.Space();
	}

	private void DrawMaterialRemapConditionsField()
	{
#if UNITY_2019_1_OR_NEWER
		EditorGUILayout.LabelField( "Default Material Remap Conditions" );
		EditorGUI.indentLevel++;

		remappedMaterialNamesMustMatch = EditorGUILayout.ToggleLeft( "Material names must match", remappedMaterialNamesMustMatch );
		remappedMaterialPropertiesMustMatch = EditorGUILayout.ToggleLeft( "Material properties must match", remappedMaterialPropertiesMustMatch );
		dontRemapExtractedMaterials = EditorGUILayout.ToggleLeft( "Don't remap already extracted materials", dontRemapExtractedMaterials );
		dontRemapMaterialsAcrossDifferentModels = EditorGUILayout.ToggleLeft( "Don't remap Model A's materials to Model B (i.e. different models won't share the same materials)", dontRemapMaterialsAcrossDifferentModels );

		EditorGUI.indentLevel--;
		EditorGUILayout.Space();
#endif
	}

	private void DrawMaterialRemapList()
	{
		EditorGUILayout.HelpBox( HELP_TEXT, MessageType.Info );

		GUILayout.BeginHorizontal();

		if( GUILayout.Button( "Extract All" ) )
		{
			for( int i = 0; i < modelData.Count; i++ )
			{
				for( int j = 0; j < modelData[i].materialExtractModes.Count; j++ )
					modelData[i].materialExtractModes[j] = ExtractMode.Extract;
			}
		}

		if( GUILayout.Button( "Remap All..." ) )
		{
			RemapAllPopup.ShowAt( remapAllButtonRect, new Vector2( 325f, 250f ), ( List<Material> remapFrom, Material remapTo, bool skipIgnoredMaterials ) =>
			{
				for( int i = 0; i < modelData.Count; i++ )
				{
					ExtractData data = modelData[i];
					for( int j = 0; j < data.remappedMaterials.Count; j++ )
					{
						switch( data.materialExtractModes[j] )
						{
							case ExtractMode.Extract:
							{
								if( remapFrom == null || ( data.originalMaterials[j] && remapFrom.Contains( data.originalMaterials[j] ) ) )
								{
									data.materialExtractModes[j] = ExtractMode.Remap;
									data.remappedMaterials[j] = remapTo;
								}

								break;
							}
							case ExtractMode.Remap:
							{
								if( remapFrom == null || ( data.remappedMaterials[j] && remapFrom.Contains( data.remappedMaterials[j] ) ) )
									data.remappedMaterials[j] = remapTo;

								break;
							}
							case ExtractMode.Ignore:
							{
								if( !skipIgnoredMaterials && ( remapFrom == null || ( data.originalMaterials[j] && remapFrom.Contains( data.originalMaterials[j] ) ) ) )
								{
									data.materialExtractModes[j] = ExtractMode.Remap;
									data.remappedMaterials[j] = remapTo;
								}

								break;
							}
						}
					}
				}

				Repaint();
			} );
		}

		if( Event.current.type == EventType.Repaint )
			remapAllButtonRect = GUILayoutUtility.GetLastRect();

		if( GUILayout.Button( "Ignore All" ) )
		{
			for( int i = 0; i < modelData.Count; i++ )
			{
				for( int j = 0; j < modelData[i].materialExtractModes.Count; j++ )
					modelData[i].materialExtractModes[j] = ExtractMode.Ignore;
			}
		}

		GUILayout.EndHorizontal();

		EditorGUILayout.Space();

		for( int i = 0; i < modelData.Count; i++ )
		{
			ExtractData data = modelData[i];
			if( !data.model )
				continue;

			GUI.enabled = false;
			EditorGUILayout.ObjectField( GUIContent.none, data.model, typeof( GameObject ), false );
			GUI.enabled = true;

			if( data.originalMaterials.Count == 0 )
				EditorGUILayout.LabelField( "This model has no materials..." );

			for( int j = 0; j < data.originalMaterials.Count; j++ )
			{
				GUILayout.BeginHorizontal();

				EditorGUILayout.PrefixLabel( data.materialNames[j] );

				data.materialExtractModes[j] = (ExtractMode) EditorGUILayout.EnumPopup( GUIContent.none, data.materialExtractModes[j], GL_WIDTH_75 );
				if( data.materialExtractModes[j] == ExtractMode.Remap )
				{
					EditorGUI.BeginChangeCheck();
					data.remappedMaterials[j] = EditorGUILayout.ObjectField( GUIContent.none, data.remappedMaterials[j], typeof( Material ), false, GL_MIN_WIDTH_50 ) as Material;
					if( EditorGUI.EndChangeCheck() && ( !data.remappedMaterials[j] || data.remappedMaterials[j] == data.originalMaterials[j] ) )
						data.materialExtractModes[j] = ExtractMode.Ignore;
				}
				else
				{
					GUI.enabled = false;
					EditorGUILayout.ObjectField( GUIContent.none, data.originalMaterials[j], typeof( Material ), false, GL_MIN_WIDTH_50 );
					GUI.enabled = true;
				}

				GUILayout.EndHorizontal();
			}

			EditorGUILayout.Space();
		}
	}

	private void CalculateRemappedMaterials()
	{
#if UNITY_2019_1_OR_NEWER
		// Key: Material CRC (material.ComputeCRC)
		// Value: All materials sharing that CRC
		Dictionary<int, HashSet<Material>> duplicateMaterialsLookup = new Dictionary<int, HashSet<Material>>( modelData.Count * 8 );

		// Add all existing materials at materialsFolder to the lookup table
		if( !dontRemapMaterialsAcrossDifferentModels && Directory.Exists( materialsFolder ) )
		{
			string[] existingMaterialPaths = Directory.GetFiles( materialsFolder, "*.mat", SearchOption.TopDirectoryOnly );
			for( int i = 0; i < existingMaterialPaths.Length; i++ )
			{
				Material material = AssetDatabase.LoadMainAssetAtPath( existingMaterialPaths[i] ) as Material;
				if( material )
					GetMaterialsWithCRC( duplicateMaterialsLookup, material ).Add( material );
			}
		}
#endif

		for( int i = 0; i < modelData.Count; i++ )
		{
			ExtractData data = modelData[i];
			if( !data.model )
			{
				modelData.RemoveAt( i-- );
				continue;
			}

			string modelPath = AssetDatabase.GetAssetPath( data.model );
			ModelImporter modelImporter = AssetImporter.GetAtPath( modelPath ) as ModelImporter;
			if( !modelImporter )
			{
				Debug.LogWarning( "Couldn't get ModelImporter from asset: " + AssetDatabase.GetAssetPath( data.model ), data.model );
				modelData.RemoveAt( i-- );
				continue;
			}

			// Reset previously assigned values to this entry (if any)
			data = modelData[i] = new ExtractData( data.model );

			Object[] embeddedAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath( modelPath );
			List<Material> embeddedMaterials = new List<Material>( embeddedAssets.Length );
			for( int j = 0; j < embeddedAssets.Length; j++ )
			{
				Material embeddedMaterial = embeddedAssets[j] as Material;
				if( embeddedMaterial )
					embeddedMaterials.Add( embeddedMaterial );
			}

			// Get the model's current material remapping
			// Credit: https://forum.unity.com/threads/batch-change-all-fbx-default-materials-help.626341/#post-6530939
			using( SerializedObject so = new SerializedObject( modelImporter ) )
			{
				SerializedProperty materials = so.FindProperty( "m_Materials" );
				SerializedProperty externalObjects = so.FindProperty( "m_ExternalObjects" );

				for( int materialIndex = 0; materialIndex < materials.arraySize; materialIndex++ )
				{
					SerializedProperty id = materials.GetArrayElementAtIndex( materialIndex );
					string name = id.FindPropertyRelative( "name" ).stringValue;
					string type = id.FindPropertyRelative( "type" ).stringValue;

					Material material = null;
					for( int externalObjectIndex = 0; externalObjectIndex < externalObjects.arraySize; externalObjectIndex++ )
					{
						SerializedProperty pair = externalObjects.GetArrayElementAtIndex( externalObjectIndex );
						string externalName = pair.FindPropertyRelative( "first.name" ).stringValue;
						string externalType = pair.FindPropertyRelative( "first.type" ).stringValue;

						if( externalType == type && externalName == name && ( pair = pair.FindPropertyRelative( "second" ) ) != null )
						{
							material = pair.objectReferenceValue as Material;
							break;
						}
					}

					if( !material )
						material = embeddedMaterials.Find( ( m ) => m.name == name );

					data.materialNames.Add( name );
					data.originalMaterials.Add( material );

					if( !material )
					{
						data.materialExtractModes.Add( ExtractMode.Ignore );
						data.remappedMaterials.Add( null );
					}
					else
					{
						bool materialAlreadyExtracted = AssetDatabase.IsMainAsset( material );
#if UNITY_2019_1_OR_NEWER
						HashSet<Material> duplicateMaterials = GetMaterialsWithCRC( duplicateMaterialsLookup, material );
						Material remappedMaterial = null;

						// - Material was already extracted: remap the material only if 'dontRemapExtractedMaterials' is false
						// - 'dontRemapMaterialsAcrossDifferentModels' is true: only remap with a material from the same model
						// - 'remappedMaterialPropertiesMustMatch' is true: only remap with a material whose properties match
						//   the current material's properties
						// - Only 'remappedMaterialNamesMustMatch' is true: remap with a material with the same name; properties
						//   of the two materials may not match
						if( !materialAlreadyExtracted || !dontRemapExtractedMaterials )
						{
							if( remappedMaterialPropertiesMustMatch )
							{
								foreach( Material _material in duplicateMaterials )
								{
									if( _material.name == name || ( !remappedMaterial && !remappedMaterialNamesMustMatch ) )
										remappedMaterial = _material;
								}
							}
							else if( remappedMaterialNamesMustMatch )
								remappedMaterial = GetMaterialWithName( duplicateMaterialsLookup, name );
						}

						if( remappedMaterial && remappedMaterial != material )
						{
							data.materialExtractModes.Add( ExtractMode.Remap );
							data.remappedMaterials.Add( remappedMaterial );
						}
						else
#endif
						{
							data.materialExtractModes.Add( materialAlreadyExtracted ? ExtractMode.Ignore : ExtractMode.Extract );
							data.remappedMaterials.Add( null );

#if UNITY_2019_1_OR_NEWER
							duplicateMaterials.Add( material );
#endif
						}
					}
				}
			}

#if UNITY_2019_1_OR_NEWER
			if( dontRemapMaterialsAcrossDifferentModels )
				duplicateMaterialsLookup.Clear();
#endif
		}
	}

#if UNITY_2019_1_OR_NEWER
	private HashSet<Material> GetMaterialsWithCRC( Dictionary<int, HashSet<Material>> lookup, Material material )
	{
		int crcHash = material.ComputeCRC();
		HashSet<Material> result;
		if( !lookup.TryGetValue( crcHash, out result ) )
			lookup[crcHash] = result = new HashSet<Material>();

		return result;
	}

	private Material GetMaterialWithName( Dictionary<int, HashSet<Material>> lookup, string name )
	{
		foreach( HashSet<Material> allMaterials in lookup.Values )
		{
			foreach( Material material in allMaterials )
			{
				if( material.name == name )
					return material;
			}
		}

		return null;
	}
#endif

	private void ExtractMaterials()
	{
		if( materialsFolder.EndsWith( "/" ) )
			materialsFolder = materialsFolder.Substring( 0, materialsFolder.Length - 1 );

		if( !Directory.Exists( materialsFolder ) )
		{
			Directory.CreateDirectory( materialsFolder );
			AssetDatabase.ImportAsset( materialsFolder, ImportAssetOptions.ForceUpdate );
		}

		List<AssetImporter> dirtyModelImporters = new List<AssetImporter>( modelData.Count );
		Dictionary<Material, Material> extractedMaterials = new Dictionary<Material, Material>( modelData.Count * 8 );

		for( int i = 0; i < modelData.Count; i++ )
		{
			ExtractData data = modelData[i];
			if( !data.model )
				continue;

			AssetImporter modelImporter = AssetImporter.GetAtPath( AssetDatabase.GetAssetPath( data.model ) );

			// Remap/extract the model's materials
			// Credit: https://forum.unity.com/threads/batch-change-all-fbx-default-materials-help.626341/#post-6530939
			using( SerializedObject so = new SerializedObject( modelImporter ) )
			{
				SerializedProperty materials = so.FindProperty( "m_Materials" );
				SerializedProperty externalObjects = so.FindProperty( "m_ExternalObjects" );

				for( int materialIndex = 0; materialIndex < materials.arraySize; materialIndex++ )
				{
					SerializedProperty id = materials.GetArrayElementAtIndex( materialIndex );
					string name = id.FindPropertyRelative( "name" ).stringValue;
					string type = id.FindPropertyRelative( "type" ).stringValue;

					// j: index of the target material in data's lists
					int j = ( materialIndex < data.materialNames.Count && data.materialNames[materialIndex] == name ) ? materialIndex : data.materialNames.IndexOf( name );
					if( j < 0 )
					{
						// This can only occur if user reimports the model with more materials when 'inModelSelectionPhase' is false
						Debug.LogWarning( data.model.name + "." + name + " material has no matching data, skipped", data.model );
						continue;
					}

					Material targetMaterial = null;
					switch( data.materialExtractModes[j] )
					{
						case ExtractMode.Extract:
						{
							if( data.originalMaterials[j] && !AssetDatabase.IsMainAsset( data.originalMaterials[j] ) )
								targetMaterial = data.originalMaterials[j];
							else
								Debug.LogWarning( data.model.name + "." + name + " isn't extracted because either the material doesn't exist or it is already extracted", data.model );

							break;
						}
						case ExtractMode.Remap:
						{
							if( data.remappedMaterials[j] && ( data.originalMaterials[j] != data.remappedMaterials[j] || !AssetDatabase.IsMainAsset( data.remappedMaterials[j] ) ) )
								targetMaterial = data.remappedMaterials[j];
							else
								Debug.LogWarning( data.model.name + "." + name + " isn't remapped because either the material doesn't exist or it is already extracted", data.model );

							break;
						}
					}

					if( !targetMaterial )
						continue;
					else if( !AssetDatabase.IsMainAsset( targetMaterial ) )
					{
						Material extractedMaterial;
						if( !extractedMaterials.TryGetValue( targetMaterial, out extractedMaterial ) )
						{
							extractedMaterials[targetMaterial] = extractedMaterial = new Material( targetMaterial );
							AssetDatabase.CreateAsset( extractedMaterial, AssetDatabase.GenerateUniqueAssetPath( materialsFolder + "/" + targetMaterial.name + ".mat" ) );
						}

						targetMaterial = extractedMaterial;
					}

					SerializedProperty materialProperty = null;
					for( int externalObjectIndex = 0; externalObjectIndex < externalObjects.arraySize; externalObjectIndex++ )
					{
						SerializedProperty pair = externalObjects.GetArrayElementAtIndex( externalObjectIndex );
						string externalName = pair.FindPropertyRelative( "first.name" ).stringValue;
						string externalType = pair.FindPropertyRelative( "first.type" ).stringValue;

						if( externalType == type && externalName == name )
						{
							materialProperty = pair.FindPropertyRelative( "second" );
							break;
						}
					}

					if( materialProperty == null )
					{
						SerializedProperty currentSerializedProperty = externalObjects.GetArrayElementAtIndex( externalObjects.arraySize++ );
						currentSerializedProperty.FindPropertyRelative( "first.name" ).stringValue = name;
						currentSerializedProperty.FindPropertyRelative( "first.type" ).stringValue = type;
						currentSerializedProperty.FindPropertyRelative( "first.assembly" ).stringValue = id.FindPropertyRelative( "assembly" ).stringValue;
						currentSerializedProperty.FindPropertyRelative( "second" ).objectReferenceValue = targetMaterial;
					}
					else
						materialProperty.objectReferenceValue = targetMaterial;
				}

				if( so.hasModifiedProperties )
				{
					dirtyModelImporters.Add( modelImporter );
					so.ApplyModifiedPropertiesWithoutUndo();
				}
			}
		}

		for( int i = 0; i < dirtyModelImporters.Count; i++ )
			dirtyModelImporters[i].SaveAndReimport();
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1130046.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot 分布式验证码登录方案

前言 为了防止验证系统被暴力破解&#xff0c;很多系统都增加了验证码效验&#xff0c;比较常见的就是图片二维码&#xff0c;业内比较安全的是短信验证码&#xff0c;当然还有一些拼图验证码&#xff0c;加入人工智能的二维码等等&#xff0c;我们今天的主题就是前后端分离的…

【C++】STL容器——【深浅拷贝】与【写时拷贝】对比详解(拷贝构造)(10)

前言 大家好吖&#xff0c;欢迎来到 YY 滴C系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 目录 一.深浅拷贝浅拷贝&#xff1a;深拷贝&#xff1a; 二.写时拷贝 一.深浅拷贝 (默认拷贝构造运用 引用 防止死递归的后遗症&#…

基于C/C++的UG二次开发流程

文章目录 基于C/C的UG二次开发流程1 环境搭建1.1 新建工程1.2 项目属性设置1.3 添加入口函数并生成dll文件1.4 执行程序1.5 ufsta入口1.5.1 创建程序部署目录结构1.5.2 创建菜单文件1.5.3 设置系统环境变量1.5.4 制作对话框1.5.5 创建代码1.5.6 部署和执行 基于C/C的UG二次开发…

嚼一嚼Halcon中的3D手眼标定

文章目录 一、问题概述1、何为手眼标定&#xff1f;2、手眼标定的2种形式1&#xff09;眼在手上&#xff08;eye in hand&#xff09;&#xff1a;即相机固定在机械臂末端2&#xff09;眼在手外&#xff08;eye to hand&#xff09;&#xff1a;即相机固定在机械臂以外的地方 3…

【JVM】类的生命周期

【JVM】类的生命周期 文章目录 【JVM】类的生命周期1. 生命周期概述2. 加载阶段3. 连接阶段3.1 验证3.2 准备3.3 解析 4. 初始化阶段4.1 触发初始化的方式4.2 clinit不存在的情况4.3 多个类的初始化 5. 总结 1. 生命周期概述 类的生命周期分为5/7个阶段&#xff1a; 加载(Loa…

MSQL系列(八) Mysql实战-SQL存储引擎

Mysql实战-SQL存储引擎 前面我们讲解了索引的存储结构&#xff0c;BTree的索引结构&#xff0c;我们一般都知道Mysql的存储引擎有两种&#xff0c;MyISAM和InnoDB,今天我们来详细讲解下Mysql的存储引擎 文章目录 Mysql实战-SQL存储引擎1.存储引擎2.MyISAM的特点3. InnoDB的特…

面向小白vim使用手册

撤回上一步 按 "u" 注释多段 1&#xff1a;选择开始注释的地方&#xff0c;在命令模式下按 ctrv进入可视化模式。 2&#xff1a;鼠标向下滑动&#xff0c;选中要注释的段落&#xff1a;3:进入大写锁定&#xff0c;按"i"键进入插入模式&#xff1a; 4:输…

vue3 + ant design vue项目svg格式的background-image使用配置

直接使用的时候会报错: 图片解析不出来还是svg源文件代码 项目启动报错 vue3使用的时候普通配置会出现this.getOptions is not function错误&#xff08; Webpack 版本升级引起的&#xff0c;在较新的 Webpack 版本中&#xff0c;this.getOptions 方法已被移除。&#xff09;…

小知识(6) el-table表格选中行和回显行(vue3)

el-table表格选中行和回显行 官方文档说明 https://element-plus.org/zh-CN/component/table.html#table-%E6%96%B9%E6%B3%95 环境&#xff1a;vue3element-plus 选中行selection <el-table ref"baseTableRef" row-key"id" border :selection"tr…

支持多校 微信课表小程序源码 排课小程序源码 支持导入课表 情侣课表 背景设置

练手Lab课程表小程序源码是一个基于thinkphp系统进行开发的前后端分离系统。 源码功能介绍 1、情侣功能 2、情侣间留言 3、情侣间互相设置课程表背景 4、自己日、周课程表背景设置 5、教务系统课程表导入 6、导入别人分享的课表 7、导入别人分享的单课 8、多校支持 9…

25装饰器2

有一个比较绕的地方&#xff0c;但是我相信我能理解透彻这个里面的逻辑还有原理 def check(func):print(登录验证)def inner():func()return innerdef fss():print(发说说)fss check(fss) fss()这里的fss check(fss)还有后面的fss()什么意思&#xff1f;怎么理解呢&#xff…

Qt之彻底解决QSpinBox限定范围无效的问题

QSpinBox有个比较啃爹的问题,不管取值范围设置为多少,都能一直输入0,如下图所示: 当取值范围包含负数时,负号后也可以一直输入0,如下图所示: 还有就是当取值范围设置为10以上时,比如10~100,却可以输入1~9 虽然上述非法输入最终都未生效,当QSpinBox失去焦点时会显示为…

【iOS逆向与安全】某音App直播间自动发666 和 懒人自动看视频

1.目标 由于看直播的时候主播叫我发 666&#xff0c;支持他&#xff0c;我肯定支持他呀&#xff0c;就一直发&#xff0c;可是后来发现太浪费时间了&#xff0c;能不能做一个直播间自动发 666 呢&#xff1f;于是就花了几分钟做了一个。 2.操作环境 越狱iPhone一台 frida m…

如何使用 JMeter 进行 HTTPS 请求测试?

本文将介绍如何使用 JMeter 测试 HTTPS 请求&#xff0c;并提供相关的技巧和注意事项。 在进行性能测试时&#xff0c;很多网站都采用了 HTTPS 协议。当我们测试 HTTPS 请求&#xff0c;如果服务端开启了双向认证&#xff0c;则需要客户端发送请求时带上证书。本文介绍如何在 …

机器学习-模型评估与选择

文章目录 评估方法留出法交叉验证自助法 性能的衡量回归问题分类问题查准率、查全率与F1ROC与AUC 在机器学习中&#xff0c;我们通常面临两个主要问题&#xff1a;欠拟合和过拟合。欠拟合指模型无法在训练数据上获得足够低的误差&#xff0c;通常是因为模型太简单&#xff0c;无…

从另外一个进程中读取数据

从另外一个进程中读取数据&#xff0c;其实就注入线程&#xff0c;寻址&#xff0c;解析内存&#xff0c;处理数据。例如这个就是从另外一个正在运行的进程中&#xff0c;读取数据并保存。实时性还可以。

C/C++新冠疫情死亡率 2020年9月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C新冠疫情死亡率 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C新冠疫情死亡率 2020年9月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 2020年全世界爆发了新冠疫情&#xff0c;请…

openEuler 22.03 LTS 安装 Docker CE 和 Dcoker Compose

openEuler 使用 DNF 安装 Docker CE 1024&#xff0c;节日快乐&#xff01;回归正题&#xff0c;DNF 安装 DockerOS 系统环境准备安装 docker-ce 步骤1、更新系统2、安装必要的软件包3、添加 Docker CE 存储库4、更新索引缓存并安装 Docker CE5、启动 Docker 服务6、查看 Docke…

当前JavaEE初阶的阶段知识总结

当前JavaEE初阶的阶段知识总结 多线程 文件IO 文件系统操作 ~~ File类. 文件内容操作 ~~ 读文件,写文件. IO 流对象. 流(Stream),形象的比喻,读取文件,就像水流一样,读写文件的时候,和水流类似,读100字节,可以一次读1个字节,100次完成;也可以一次读10个字节,10次完成…… 在…

用户案例 | 珍岛集团基于 Apache DolphinScheduler 打造智能营销云平台

珍岛集团致力于打造全球领先的智能营销云平台&#xff0c;在国内率先推出的Marketingforce&#xff08;营销力&#xff09;平台&#xff0c;专注于人工智能、大数据、云计算在数字营销及企业数字化智能化领域的创新与实践&#xff0c;面向全球企业提供营销力软件及服务&#xf…