MB3_BatchPrefabBakerEditor.cs 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  1. using UnityEngine;
  2. using UnityEditor;
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.IO;
  7. using DigitalOpus.MB.Core;
  8. using System.Text.RegularExpressions;
  9. namespace DigitalOpus.MB.MBEditor
  10. {
  11. [CustomEditor(typeof(MB3_BatchPrefabBaker))]
  12. public class MB3_BatchPrefabBakerEditor : Editor
  13. {
  14. public class UnityTransform
  15. {
  16. public Vector3 p;
  17. public Quaternion q;
  18. public Vector3 s;
  19. public Transform t;
  20. public UnityTransform(Transform t)
  21. {
  22. this.t = t;
  23. p = t.localPosition;
  24. q = t.localRotation;
  25. s = t.localScale;
  26. }
  27. }
  28. private enum TargetMeshTreatment
  29. {
  30. createNewMesh,
  31. replaceMesh,
  32. reuseMesh
  33. }
  34. private class ProcessedMeshInfo
  35. {
  36. public Material[] srcMaterials;
  37. public Material[] targMaterials;
  38. public Mesh targetMesh;
  39. }
  40. SerializedObject prefabBaker = null;
  41. GUIContent GUIContentLogLevelContent = new GUIContent("Log Level");
  42. GUIContent GUIContentBatchBakePrefabReplacePrefab = new GUIContent("Batch Bake Prefabs (Replace Prefab)",
  43. "This will clone the source prefab, replace the meshes in the clone with baked versions and replace the target prefab with the clone.\n\n" +
  44. "IF ANY CHANGES HAD BEEN MADE TO THE TARGET PREFAB, THOSE WILL BE LOST.");
  45. GUIContent GUIContentBatchBakePrefabOnlyMeshesAndMats = new GUIContent("Batch Bake Prefabs (Only Replace Meshes & Materials)",
  46. "This will attempt to match the meshes used by the target prefab to those used by the source prefab. For this to work" +
  47. " well, the source and target prefabs should have the same hierarchy. The meshes and materials in the target prefab will be updated to baked versions. " +
  48. " Modifications to the target prefab other than the meshes and materials will be preserved.\n\n" +
  49. "Check the console for errors after baking the prefabs.");
  50. SerializedProperty prefabRows, outputFolder, logLevel;
  51. Color buttonColor = new Color(.8f, .8f, 1f, 1f);
  52. [MenuItem("GameObject/Create Other/Mesh Baker/Batch Prefab Baker", false, 1000)]
  53. public static void CreateNewBatchPrefabBaker()
  54. {
  55. if (MB3_MeshCombiner.EVAL_VERSION)
  56. {
  57. Debug.LogError("The prefab baker is only available in the full version of MeshBaker.");
  58. return;
  59. }
  60. MB3_TextureBaker[] mbs = (MB3_TextureBaker[])Editor.FindObjectsOfType(typeof(MB3_TextureBaker));
  61. Regex regex = new Regex(@"\((\d+)\)$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
  62. int largest = 0;
  63. try
  64. {
  65. for (int i = 0; i < mbs.Length; i++)
  66. {
  67. Match match = regex.Match(mbs[i].name);
  68. if (match.Success)
  69. {
  70. int val = Convert.ToInt32(match.Groups[1].Value);
  71. if (val >= largest)
  72. largest = val + 1;
  73. }
  74. }
  75. }
  76. catch (Exception e)
  77. {
  78. if (e == null) e = null; //Do nothing supress compiler warning
  79. }
  80. GameObject nmb = new GameObject("BatchPrefabBaker (" + largest + ")");
  81. nmb.transform.position = Vector3.zero;
  82. nmb.AddComponent<MB3_BatchPrefabBaker>();
  83. nmb.AddComponent<MB3_TextureBaker>();
  84. nmb.AddComponent<MB3_MeshBaker>();
  85. }
  86. void OnEnable()
  87. {
  88. prefabBaker = new SerializedObject(target);
  89. prefabRows = prefabBaker.FindProperty("prefabRows");
  90. outputFolder = prefabBaker.FindProperty("outputPrefabFolder");
  91. logLevel = prefabBaker.FindProperty("LOG_LEVEL");
  92. }
  93. void OnDisable()
  94. {
  95. prefabBaker = null;
  96. }
  97. public override void OnInspectorGUI()
  98. {
  99. prefabBaker.Update();
  100. EditorGUILayout.HelpBox(
  101. "This tool speeds up the process of preparing prefabs " +
  102. " for static and dynamic batching. It creates duplicate prefab assets and meshes " +
  103. "that share a combined material. Source assets are not touched.\n\n" +
  104. "1) bake the textures to be used by prefabs using the MB3_TextureBaker attached to this game object\n" +
  105. "2) enter the number of prefabs to bake in the 'Prefab Rows Size' field\n" +
  106. "3) drag source prefab assets to the 'Source Prefab' slots. These should be project assets not scene objects. Renderers" +
  107. " do not need to be in the root of the prefab. There can be more than one" +
  108. " renderer in each prefab.\n" +
  109. "4) choose a folder where the result prefabs will be stored and click 'Create Empty Result Prefabs'\n" +
  110. "5) click 'Batch Bake Prefabs'\n" +
  111. "6) Check the console for messages and errors", MessageType.Info);
  112. EditorGUILayout.PropertyField(logLevel, GUIContentLogLevelContent);
  113. EditorGUILayout.PropertyField(prefabRows, true);
  114. EditorGUILayout.LabelField("Output Folder", EditorStyles.boldLabel);
  115. EditorGUILayout.LabelField(outputFolder.stringValue);
  116. if (GUILayout.Button("Browse For Output Folder"))
  117. {
  118. string path = EditorUtility.OpenFolderPanel("Browse For Output Folder", "", "");
  119. outputFolder.stringValue = path;
  120. }
  121. if (GUILayout.Button("Create Empty Result Prefabs"))
  122. {
  123. CreateEmptyOutputPrefabs();
  124. }
  125. Color oldColor = GUI.backgroundColor;
  126. GUI.backgroundColor = buttonColor;
  127. if (GUILayout.Button(GUIContentBatchBakePrefabReplacePrefab))
  128. {
  129. MB3_BatchPrefabBaker pb = (MB3_BatchPrefabBaker)target;
  130. BakePrefabs(pb, true);
  131. }
  132. if (GUILayout.Button(GUIContentBatchBakePrefabOnlyMeshesAndMats))
  133. {
  134. MB3_BatchPrefabBaker pb = (MB3_BatchPrefabBaker)target;
  135. BakePrefabs(pb, false);
  136. }
  137. GUI.backgroundColor = oldColor;
  138. if (GUILayout.Button("Poplate Prefab Rows From Texture Baker"))
  139. {
  140. PopulatePrefabRowsFromTextureBaker((MB3_BatchPrefabBaker)prefabBaker.targetObject);
  141. }
  142. if (GUILayout.Button("Open Replace Prefabs In Scene Window"))
  143. {
  144. MB3_BatchPrefabBaker pb = (MB3_BatchPrefabBaker)target;
  145. MB_ReplacePrefabsInSceneEditorWindow.ShowWindow(pb.prefabRows);
  146. }
  147. prefabBaker.ApplyModifiedProperties();
  148. prefabBaker.SetIsDifferentCacheDirty();
  149. }
  150. public void PopulatePrefabRowsFromTextureBaker(MB3_BatchPrefabBaker prefabBaker)
  151. {
  152. MB3_TextureBaker texBaker = prefabBaker.GetComponent<MB3_TextureBaker>();
  153. List<GameObject> newPrefabs = new List<GameObject>();
  154. List<GameObject> gos = texBaker.GetObjectsToCombine();
  155. for (int i = 0; i < gos.Count; i++)
  156. {
  157. GameObject go = (GameObject)PrefabUtility.FindPrefabRoot(gos[i]);
  158. UnityEngine.Object obj = MBVersionEditor.PrefabUtility_GetCorrespondingObjectFromSource(go);
  159. if (obj != null && obj is GameObject)
  160. {
  161. if (!newPrefabs.Contains((GameObject)obj)) newPrefabs.Add((GameObject)obj);
  162. }
  163. else
  164. {
  165. Debug.LogWarning(String.Format("Object {0} did not have a prefab", gos[i]));
  166. }
  167. }
  168. // Remove prefabs that are already in the list of batch prefab baker's prefabs.
  169. {
  170. List<GameObject> tmpNewPrefabs = new List<GameObject>();
  171. for (int i = 0; i < newPrefabs.Count; i++)
  172. {
  173. bool found = false;
  174. for (int j = 0; j < prefabBaker.prefabRows.Length; j++)
  175. {
  176. if (prefabBaker.prefabRows[j].sourcePrefab == newPrefabs[i])
  177. {
  178. found = true;
  179. break;
  180. }
  181. }
  182. if (!found)
  183. {
  184. tmpNewPrefabs.Add(newPrefabs[i]);
  185. }
  186. }
  187. newPrefabs = tmpNewPrefabs;
  188. }
  189. List<MB3_BatchPrefabBaker.MB3_PrefabBakerRow> newRows = new List<MB3_BatchPrefabBaker.MB3_PrefabBakerRow>();
  190. if (prefabBaker.prefabRows == null) prefabBaker.prefabRows = new MB3_BatchPrefabBaker.MB3_PrefabBakerRow[0];
  191. newRows.AddRange(prefabBaker.prefabRows);
  192. for (int i = 0; i < newPrefabs.Count; i++)
  193. {
  194. MB3_BatchPrefabBaker.MB3_PrefabBakerRow row = new MB3_BatchPrefabBaker.MB3_PrefabBakerRow();
  195. row.sourcePrefab = newPrefabs[i];
  196. newRows.Add(row);
  197. }
  198. Undo.RecordObject(prefabBaker, "Populate prefab rows");
  199. prefabBaker.prefabRows = newRows.ToArray();
  200. }
  201. public static void BakePrefabs(MB3_BatchPrefabBaker pb, bool doReplaceTargetPrefab)
  202. {
  203. if (pb.LOG_LEVEL >= MB2_LogLevel.info) Debug.Log("Batch baking prefabs");
  204. if (Application.isPlaying)
  205. {
  206. Debug.LogError("The BatchPrefabBaker cannot be run in play mode.");
  207. return;
  208. }
  209. MB3_MeshBaker mb = pb.GetComponent<MB3_MeshBaker>();
  210. if (mb == null)
  211. {
  212. Debug.LogError("Prefab baker needs to be attached to a Game Object with a MB3_MeshBaker component.");
  213. return;
  214. }
  215. if (mb.textureBakeResults == null)
  216. {
  217. Debug.LogError("Texture Bake Results is not set");
  218. return;
  219. }
  220. int numResultMats = mb.textureBakeResults.NumResultMaterials();
  221. for (int i = 0; i < numResultMats; i++)
  222. {
  223. if (mb.textureBakeResults.GetCombinedMaterialForSubmesh(i) == null)
  224. {
  225. Debug.LogError("The texture bake result had a null result material on submesh " + i + ". Try re-baking textures");
  226. return;
  227. }
  228. }
  229. if (mb.meshCombiner.outputOption != MB2_OutputOptions.bakeMeshAssetsInPlace)
  230. {
  231. mb.meshCombiner.outputOption = MB2_OutputOptions.bakeMeshAssetsInPlace;
  232. }
  233. MB2_TextureBakeResults tbr = mb.textureBakeResults;
  234. HashSet<Mesh> sourceMeshes = new HashSet<Mesh>();
  235. HashSet<Mesh> allResultMeshes = new HashSet<Mesh>();
  236. //validate prefabs
  237. for (int i = 0; i < pb.prefabRows.Length; i++)
  238. {
  239. if (pb.prefabRows[i] == null || pb.prefabRows[i].sourcePrefab == null)
  240. {
  241. Debug.LogError("Source Prefab on row " + i + " is not set.");
  242. return;
  243. }
  244. if (pb.prefabRows[i].resultPrefab == null)
  245. {
  246. Debug.LogError("Result Prefab on row " + i + " is not set.");
  247. return;
  248. }
  249. for (int j = i + 1; j < pb.prefabRows.Length; j++)
  250. {
  251. if (pb.prefabRows[i].sourcePrefab == pb.prefabRows[j].sourcePrefab)
  252. {
  253. Debug.LogError("Rows " + i + " and " + j + " contain the same source prefab");
  254. return;
  255. }
  256. }
  257. for (int j = 0; j < pb.prefabRows.Length; j++)
  258. {
  259. if (pb.prefabRows[i].sourcePrefab == pb.prefabRows[j].resultPrefab)
  260. {
  261. Debug.LogError("Row " + i + " source prefab is the same as row " + j + " result prefab");
  262. return;
  263. }
  264. }
  265. if (MBVersionEditor.GetPrefabType(pb.prefabRows[i].sourcePrefab) != MB_PrefabType.modelPrefabAsset &&
  266. MBVersionEditor.GetPrefabType(pb.prefabRows[i].sourcePrefab) != MB_PrefabType.prefabAsset)
  267. {
  268. Debug.LogError("Row " + i + " source prefab is not a prefab asset ");
  269. return;
  270. }
  271. if (MBVersionEditor.GetPrefabType(pb.prefabRows[i].resultPrefab) != MB_PrefabType.modelPrefabAsset &&
  272. MBVersionEditor.GetPrefabType(pb.prefabRows[i].resultPrefab) != MB_PrefabType.prefabAsset)
  273. {
  274. Debug.LogError("Row " + i + " result prefab is not a prefab asset");
  275. return;
  276. }
  277. GameObject so = (GameObject)Instantiate(pb.prefabRows[i].sourcePrefab);
  278. GameObject ro = (GameObject)Instantiate(pb.prefabRows[i].resultPrefab);
  279. Renderer[] rs = (Renderer[])so.GetComponentsInChildren<Renderer>(true);
  280. for (int j = 0; j < rs.Length; j++)
  281. {
  282. if (IsGoodToBake(rs[j], tbr))
  283. {
  284. sourceMeshes.Add(MB_Utility.GetMesh(rs[j].gameObject));
  285. }
  286. }
  287. rs = ro.GetComponentsInChildren<Renderer>(true);
  288. for (int j = 0; j < rs.Length; j++)
  289. {
  290. Renderer r = rs[j];
  291. if (r is MeshRenderer || r is SkinnedMeshRenderer)
  292. {
  293. Mesh m = MB_Utility.GetMesh(r.gameObject);
  294. if (m != null)
  295. {
  296. allResultMeshes.Add(m);
  297. }
  298. }
  299. }
  300. DestroyImmediate(so); //todo should cache these and have a proper cleanup at end
  301. DestroyImmediate(ro);
  302. }
  303. sourceMeshes.IntersectWith(allResultMeshes);
  304. HashSet<Mesh> sourceMeshesThatAreUsedByResult = sourceMeshes;
  305. if (sourceMeshesThatAreUsedByResult.Count > 0)
  306. {
  307. foreach (Mesh m in sourceMeshesThatAreUsedByResult)
  308. {
  309. Debug.LogWarning("Mesh " + m + " is used by both the source and result prefabs. New meshes will be created.");
  310. }
  311. //return;
  312. }
  313. List<UnityTransform> unityTransforms = new List<UnityTransform>();
  314. // Bake the meshes using the meshBaker component one prefab at a time
  315. for (int prefabIdx = 0; prefabIdx < pb.prefabRows.Length; prefabIdx++)
  316. {
  317. if (doReplaceTargetPrefab)
  318. {
  319. ProcessPrefabRowReplaceTargetPrefab(pb, pb.prefabRows[prefabIdx], tbr, unityTransforms, mb);
  320. }
  321. else
  322. {
  323. ProcessPrefabRowOnlyMeshesAndMaterials(pb, pb.prefabRows[prefabIdx], tbr, unityTransforms, mb);
  324. }
  325. }
  326. AssetDatabase.Refresh();
  327. mb.ClearMesh();
  328. }
  329. private static void ProcessPrefabRowOnlyMeshesAndMaterials(MB3_BatchPrefabBaker pb, MB3_BatchPrefabBaker.MB3_PrefabBakerRow pr, MB2_TextureBakeResults tbr, List<UnityTransform> unityTransforms, MB3_MeshBaker mb)
  330. {
  331. if (pb.LOG_LEVEL >= MB2_LogLevel.info) Debug.Log("==== Processing Source Prefab " + pr.sourcePrefab);
  332. GameObject srcPrefab = pr.sourcePrefab;
  333. GameObject targetPrefab = pr.resultPrefab;
  334. string targetPrefabName = AssetDatabase.GetAssetPath(targetPrefab);
  335. GameObject srcPrefabInstance = GameObject.Instantiate(srcPrefab);
  336. GameObject targPrefabInstance = GameObject.Instantiate(targetPrefab);
  337. Renderer[] rs = srcPrefabInstance.GetComponentsInChildren<Renderer>(true);
  338. if (rs.Length < 1)
  339. {
  340. Debug.LogWarning("Prefab " + pr.sourcePrefab + " does not have a renderer");
  341. DestroyImmediate(srcPrefabInstance);
  342. DestroyImmediate(targPrefabInstance);
  343. return;
  344. }
  345. Renderer[] sourceRenderers = srcPrefabInstance.GetComponentsInChildren<Renderer>(true);
  346. Dictionary<Mesh, List<ProcessedMeshInfo>> processedMeshesSrcToTargetMap = new Dictionary<Mesh, List<ProcessedMeshInfo>>();
  347. for (int i = 0; i < sourceRenderers.Length; i++)
  348. {
  349. if (!IsGoodToBake(sourceRenderers[i], tbr))
  350. {
  351. continue;
  352. }
  353. Mesh sourceMesh = MB_Utility.GetMesh(sourceRenderers[i].gameObject);
  354. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("== Visiting renderer: " + sourceRenderers[i]);
  355. // Try to find an existing mesh in the target that we can re-use
  356. Mesh targetMeshAsset = null;
  357. Transform tr = FindCorrespondingTransform(srcPrefabInstance.transform, sourceRenderers[i].transform, targPrefabInstance.transform);
  358. Renderer targetRenderer = null;
  359. if (tr != null)
  360. {
  361. Mesh targMesh = MB_Utility.GetMesh(tr.gameObject);
  362. // Only replace target meshes if they are part of the target prefab.
  363. if (AssetDatabase.GetAssetPath(targMesh) == AssetDatabase.GetAssetPath(targetPrefab))
  364. {
  365. targetRenderer = tr.GetComponent<Renderer>();
  366. if (sourceRenderers[i].GetType() == targetRenderer.GetType())
  367. {
  368. targetMeshAsset = MB_Utility.GetMesh(tr.gameObject);
  369. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log("Found correspoinding transform in target prefab: " + tr + " mesh: " + targetMeshAsset);
  370. }
  371. else
  372. {
  373. Debug.LogError("The renderer on the target prefab matching source prefab " + pr.sourcePrefab + " was not the same kind of renderer " + sourceRenderers[i].name);
  374. continue;
  375. }
  376. }
  377. }
  378. else
  379. {
  380. Debug.LogError("There was a renderer in source prefab " + pr.sourcePrefab + " that could not be a matched to a target renderer in the target prefab: " + sourceRenderers[i].name +
  381. " This can happen if the hierarchy in the source prefab is different from the hierarchy in the target prefab.");
  382. continue;
  383. }
  384. // Check that we haven't processed this mesh already.
  385. List<ProcessedMeshInfo> lpmi;
  386. if (processedMeshesSrcToTargetMap.TryGetValue(sourceMesh, out lpmi))
  387. {
  388. Material[] srcMats = MB_Utility.GetGOMaterials(sourceRenderers[i].gameObject);
  389. for (int j = 0; j < lpmi.Count; j++)
  390. {
  391. if (ComapreMaterials(srcMats, lpmi[j].srcMaterials))
  392. {
  393. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log("Found already processed mesh that uses the same mats");
  394. targetMeshAsset = lpmi[j].targetMesh;
  395. break;
  396. }
  397. }
  398. }
  399. Material[] sourceMaterials = MB_Utility.GetGOMaterials(sourceRenderers[i].gameObject);
  400. TargetMeshTreatment targetMeshTreatment = TargetMeshTreatment.createNewMesh;
  401. string newMeshName = sourceMesh.name;
  402. if (targetMeshAsset != null)
  403. {
  404. // check if this mesh has already been processed
  405. processedMeshesSrcToTargetMap.TryGetValue(sourceMesh, out lpmi);
  406. if (lpmi != null)
  407. {
  408. // check if this mesh uses the same materials as one of the processed meshs
  409. bool foundMatch = false;
  410. bool targetMeshHasBeenUsed = false;
  411. Material[] foundMatchMaterials = null;
  412. for (int j = 0; j < lpmi.Count; j++)
  413. {
  414. if (lpmi[j].targetMesh == targetMeshAsset)
  415. {
  416. targetMeshHasBeenUsed = true;
  417. }
  418. if (ComapreMaterials(sourceMaterials, lpmi[j].srcMaterials))
  419. {
  420. foundMatchMaterials = lpmi[j].targMaterials;
  421. foundMatch = true;
  422. break;
  423. }
  424. }
  425. if (foundMatch)
  426. {
  427. // If materials match then we can re-use this processed mesh don't process.
  428. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" we can re-use this processed mesh don't process. " + targetMeshAsset);
  429. targetMeshTreatment = TargetMeshTreatment.reuseMesh;
  430. MB_Utility.SetMesh(tr.gameObject, targetMeshAsset);
  431. SetMaterials(foundMatchMaterials, targetRenderer);
  432. continue;
  433. }
  434. else
  435. {
  436. if (targetMeshHasBeenUsed)
  437. {
  438. // we need a new target mesh with a safe different name
  439. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" we can't re-use this processed mesh create new with different name. " + targetMeshAsset);
  440. newMeshName = GetNameForNewMesh(AssetDatabase.GetAssetPath(targetPrefab), newMeshName);
  441. targetMeshTreatment = TargetMeshTreatment.createNewMesh;
  442. targetMeshAsset = null;
  443. }
  444. else
  445. {
  446. // is it safe to reuse the target mesh
  447. // we need a new target mesh with a safe different name
  448. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" we can replace this processed mesh. " + targetMeshAsset);
  449. targetMeshTreatment = TargetMeshTreatment.replaceMesh;
  450. }
  451. }
  452. }
  453. else
  454. {
  455. // source mesh has not been processed can reuse the target mesh
  456. targetMeshTreatment = TargetMeshTreatment.replaceMesh;
  457. }
  458. }
  459. if (targetMeshTreatment == TargetMeshTreatment.replaceMesh)
  460. {
  461. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Replace mesh " + targetMeshAsset);
  462. EditorUtility.CopySerialized(sourceMesh, targetMeshAsset);
  463. AssetDatabase.SaveAssets();
  464. AssetDatabase.ImportAsset(targetPrefabName);
  465. }
  466. else if (targetMeshTreatment == TargetMeshTreatment.createNewMesh)
  467. {
  468. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Create new mesh " + newMeshName);
  469. targetMeshAsset = GameObject.Instantiate<Mesh>(sourceMesh);
  470. targetMeshAsset.name = newMeshName;
  471. AssetDatabase.AddObjectToAsset(targetMeshAsset, targetPrefab);
  472. #if UNITY_2018_3_OR_NEWER
  473. PrefabUtility.SavePrefabAsset(targetPrefab);
  474. #endif
  475. Debug.Assert(targetMeshAsset != null);
  476. // need a new mesh
  477. }
  478. if (targetMeshTreatment == TargetMeshTreatment.createNewMesh || targetMeshTreatment == TargetMeshTreatment.replaceMesh)
  479. {
  480. if (ProcessMesh(sourceRenderers[i], targetMeshAsset, unityTransforms, mb))
  481. {
  482. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Done processing mesh " + targetMeshAsset + " verts " + targetMeshAsset.vertexCount);
  483. ProcessedMeshInfo pmi = new ProcessedMeshInfo();
  484. pmi.targetMesh = targetMeshAsset;
  485. pmi.srcMaterials = sourceMaterials;
  486. pmi.targMaterials = sourceRenderers[i].sharedMaterials;
  487. AddToDictionary(sourceMesh, pmi, processedMeshesSrcToTargetMap);
  488. }
  489. else
  490. {
  491. Debug.LogError("Error processing mesh " + targetMeshAsset);
  492. }
  493. }
  494. MB_Utility.SetMesh(tr.gameObject, targetMeshAsset);
  495. }
  496. // TODO replace this with MBVersionEditor.ReplacePrefab I tried to do this, but when I did,
  497. // ProcessedMeshInfo.targetMesh becomes null, not sure what is going on there.
  498. GameObject obj = (GameObject)AssetDatabase.LoadAssetAtPath(targetPrefabName, typeof(GameObject));
  499. PrefabUtility.ReplacePrefab(targPrefabInstance, obj, ReplacePrefabOptions.ReplaceNameBased);
  500. GameObject.DestroyImmediate(srcPrefabInstance);
  501. GameObject.DestroyImmediate(targPrefabInstance);
  502. // Destroy obsolete meshes
  503. UnityEngine.Object[] allAssets = AssetDatabase.LoadAllAssetsAtPath(targetPrefabName);
  504. HashSet<Mesh> usedByTarget = new HashSet<Mesh>();
  505. foreach (List<ProcessedMeshInfo> ll in processedMeshesSrcToTargetMap.Values)
  506. {
  507. for (int i = 0; i < ll.Count; i++)
  508. {
  509. usedByTarget.Add(ll[i].targetMesh);
  510. }
  511. }
  512. int numDestroyed = 0;
  513. for (int i = 0; i < allAssets.Length; i++)
  514. {
  515. if (allAssets[i] is Mesh)
  516. {
  517. if (!usedByTarget.Contains((Mesh)allAssets[i]) && AssetDatabase.GetAssetPath(allAssets[i]) == AssetDatabase.GetAssetPath(targetPrefab))
  518. {
  519. numDestroyed++;
  520. GameObject.DestroyImmediate(allAssets[i], true);
  521. }
  522. }
  523. }
  524. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Destroyed " + numDestroyed + " meshes");
  525. AssetDatabase.SaveAssets();
  526. //--------------------------
  527. }
  528. private static void ProcessPrefabRowReplaceTargetPrefab(MB3_BatchPrefabBaker pb, MB3_BatchPrefabBaker.MB3_PrefabBakerRow pr, MB2_TextureBakeResults tbr, List<UnityTransform> unityTransforms, MB3_MeshBaker mb)
  529. {
  530. if (pb.LOG_LEVEL >= MB2_LogLevel.info) Debug.Log("==== Processing Source Prefab " + pr.sourcePrefab);
  531. GameObject srcPrefab = pr.sourcePrefab;
  532. GameObject targetPrefab = pr.resultPrefab;
  533. string targetPrefabName = AssetDatabase.GetAssetPath(targetPrefab);
  534. GameObject prefabInstance = GameObject.Instantiate(srcPrefab);
  535. Renderer[] rs = prefabInstance.GetComponentsInChildren<Renderer>(true);
  536. if (rs.Length < 1)
  537. {
  538. Debug.Log("Prefab " + pr.sourcePrefab + " does not have a renderer. Not replacing prefab.");
  539. DestroyImmediate(prefabInstance);
  540. return;
  541. }
  542. Renderer[] sourceRenderers = prefabInstance.GetComponentsInChildren<Renderer>(true);
  543. Dictionary<Mesh, List<ProcessedMeshInfo>> processedMeshesSrcToTargetMap = new Dictionary<Mesh, List<ProcessedMeshInfo>>();
  544. for (int i = 0; i < sourceRenderers.Length; i++)
  545. {
  546. if (!IsGoodToBake(sourceRenderers[i], tbr))
  547. {
  548. continue;
  549. }
  550. Mesh sourceMesh = MB_Utility.GetMesh(sourceRenderers[i].gameObject);
  551. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("== Visiting source renderer: " + sourceRenderers[i]);
  552. // Try to find an existing mesh in the target that we can re-use
  553. Mesh targetMeshAsset = null;
  554. Transform tr = FindCorrespondingTransform(prefabInstance.transform, sourceRenderers[i].transform, targetPrefab.transform);
  555. if (tr != null)
  556. {
  557. Mesh targMesh = MB_Utility.GetMesh(tr.gameObject);
  558. // Only replace target meshes if they are part of the target prefab.
  559. if (AssetDatabase.GetAssetPath(targMesh) == AssetDatabase.GetAssetPath(targetPrefab))
  560. {
  561. targetMeshAsset = MB_Utility.GetMesh(tr.gameObject);
  562. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log("Found corresponding transform in target prefab: " + tr + " mesh: " + targetMeshAsset);
  563. }
  564. }
  565. // Check that we haven't processed this mesh already.
  566. List<ProcessedMeshInfo> lpmi;
  567. if (processedMeshesSrcToTargetMap.TryGetValue(sourceMesh, out lpmi))
  568. {
  569. Material[] srcMats = MB_Utility.GetGOMaterials(sourceRenderers[i].gameObject);
  570. for (int j = 0; j < lpmi.Count; j++)
  571. {
  572. if (ComapreMaterials(srcMats, lpmi[j].srcMaterials))
  573. {
  574. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log("Found already processed mesh that uses the same mats");
  575. targetMeshAsset = lpmi[j].targetMesh;
  576. break;
  577. }
  578. }
  579. }
  580. Material[] sourceMaterials = MB_Utility.GetGOMaterials(sourceRenderers[i].gameObject);
  581. TargetMeshTreatment targetMeshTreatment = TargetMeshTreatment.createNewMesh;
  582. string newMeshName = sourceMesh.name;
  583. if (targetMeshAsset != null)
  584. {
  585. // check if this mesh has already been processed
  586. processedMeshesSrcToTargetMap.TryGetValue(sourceMesh, out lpmi);
  587. if (lpmi != null)
  588. {
  589. // check if this mesh uses the same materials as one of the processed meshs
  590. bool foundMatch = false;
  591. bool targetMeshHasBeenUsed = false;
  592. Material[] foundMatchMaterials = null;
  593. for (int j = 0; j < lpmi.Count; j++)
  594. {
  595. if (lpmi[j].targetMesh == targetMeshAsset)
  596. {
  597. targetMeshHasBeenUsed = true;
  598. }
  599. if (ComapreMaterials(sourceMaterials, lpmi[j].srcMaterials))
  600. {
  601. foundMatchMaterials = lpmi[j].targMaterials;
  602. foundMatch = true;
  603. break;
  604. }
  605. }
  606. if (foundMatch)
  607. {
  608. // If materials match then we can re-use this processed mesh don't process.
  609. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" we can re-use this processed mesh don't process. " + targetMeshAsset);
  610. targetMeshTreatment = TargetMeshTreatment.reuseMesh;
  611. MB_Utility.SetMesh(sourceRenderers[i].gameObject, targetMeshAsset);
  612. SetMaterials(foundMatchMaterials, sourceRenderers[i]);
  613. continue;
  614. }
  615. else
  616. {
  617. if (targetMeshHasBeenUsed)
  618. {
  619. // we need a new target mesh with a safe different name
  620. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" we can't re-use this processed mesh create new with different name. " + targetMeshAsset);
  621. newMeshName = GetNameForNewMesh(AssetDatabase.GetAssetPath(targetPrefab), newMeshName);
  622. targetMeshTreatment = TargetMeshTreatment.createNewMesh;
  623. targetMeshAsset = null;
  624. }
  625. else
  626. {
  627. // is it safe to reuse the target mesh
  628. // we need a new target mesh with a safe different name
  629. if (pb.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" we can replace this processed mesh. " + targetMeshAsset);
  630. targetMeshTreatment = TargetMeshTreatment.replaceMesh;
  631. }
  632. }
  633. }
  634. else
  635. {
  636. // source mesh has not been processed can reuse the target mesh
  637. targetMeshTreatment = TargetMeshTreatment.replaceMesh;
  638. }
  639. }
  640. if (targetMeshTreatment == TargetMeshTreatment.replaceMesh)
  641. {
  642. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Replace mesh " + targetMeshAsset);
  643. EditorUtility.CopySerialized(sourceMesh, targetMeshAsset);
  644. AssetDatabase.SaveAssets();
  645. AssetDatabase.ImportAsset(targetPrefabName);
  646. }
  647. else if (targetMeshTreatment == TargetMeshTreatment.createNewMesh)
  648. {
  649. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Create new mesh " + newMeshName);
  650. targetMeshAsset = GameObject.Instantiate<Mesh>(sourceMesh);
  651. targetMeshAsset.name = newMeshName;
  652. AssetDatabase.AddObjectToAsset(targetMeshAsset, targetPrefab);
  653. #if UNITY_2018_3_OR_NEWER
  654. PrefabUtility.SavePrefabAsset(targetPrefab);
  655. #endif
  656. Debug.Assert(targetMeshAsset != null);
  657. // need a new mesh
  658. }
  659. if (targetMeshTreatment == TargetMeshTreatment.createNewMesh || targetMeshTreatment == TargetMeshTreatment.replaceMesh)
  660. {
  661. if (ProcessMesh(sourceRenderers[i], targetMeshAsset, unityTransforms, mb))
  662. {
  663. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Done processing mesh " + targetMeshAsset + " verts " + targetMeshAsset.vertexCount);
  664. ProcessedMeshInfo pmi = new ProcessedMeshInfo();
  665. pmi.targetMesh = targetMeshAsset;
  666. pmi.srcMaterials = sourceMaterials;
  667. pmi.targMaterials = sourceRenderers[i].sharedMaterials;
  668. AddToDictionary(sourceMesh, pmi, processedMeshesSrcToTargetMap);
  669. }
  670. else
  671. {
  672. Debug.LogError("Error processing mesh " + targetMeshAsset);
  673. }
  674. }
  675. MB_Utility.SetMesh(sourceRenderers[i].gameObject, targetMeshAsset);
  676. }
  677. // TODO replace this with MBVersionEditor.ReplacePrefab I tried to do this, but when I did,
  678. // ProcessedMeshInfo.targetMesh becomes null, not sure what is going on there.
  679. GameObject obj = (GameObject)AssetDatabase.LoadAssetAtPath(targetPrefabName, typeof(GameObject));
  680. PrefabUtility.ReplacePrefab(prefabInstance, obj, ReplacePrefabOptions.ReplaceNameBased);
  681. GameObject.DestroyImmediate(prefabInstance);
  682. // Destroy obsolete meshes
  683. UnityEngine.Object[] allAssets = AssetDatabase.LoadAllAssetsAtPath(targetPrefabName);
  684. HashSet<Mesh> usedByTarget = new HashSet<Mesh>();
  685. foreach (List<ProcessedMeshInfo> ll in processedMeshesSrcToTargetMap.Values)
  686. {
  687. for (int i = 0; i < ll.Count; i++)
  688. {
  689. usedByTarget.Add(ll[i].targetMesh);
  690. }
  691. }
  692. int numDestroyed = 0;
  693. for (int i = 0; i < allAssets.Length; i++)
  694. {
  695. if (allAssets[i] is Mesh)
  696. {
  697. if (!usedByTarget.Contains((Mesh)allAssets[i]) && AssetDatabase.GetAssetPath(allAssets[i]) == AssetDatabase.GetAssetPath(targetPrefab))
  698. {
  699. numDestroyed++;
  700. GameObject.DestroyImmediate(allAssets[i], true);
  701. }
  702. }
  703. }
  704. if (pb.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Destroyed " + numDestroyed + " meshes");
  705. AssetDatabase.SaveAssets();
  706. //--------------------------
  707. }
  708. private static string GetNameForNewMesh(string prefabPath, string baseName)
  709. {
  710. // get all Mesh assets in prefab
  711. UnityEngine.Object[] objs = AssetDatabase.LoadAllAssetsAtPath(prefabPath);
  712. string[] oldNames = new string[objs.Length]; // TODO get these
  713. for (int i = 0; i < oldNames.Length; i++)
  714. {
  715. oldNames[i] = objs[i].name;
  716. }
  717. bool isUnique = false;
  718. int idx = 0;
  719. string name = baseName;
  720. while (!isUnique)
  721. {
  722. bool wasAMatch = false;
  723. for (int i = 0; i < oldNames.Length; i++)
  724. {
  725. if (oldNames[i].Equals(name))
  726. {
  727. wasAMatch = true;
  728. break;
  729. }
  730. }
  731. if (wasAMatch)
  732. {
  733. idx++;
  734. name = baseName + idx;
  735. }
  736. else
  737. {
  738. isUnique = true;
  739. }
  740. }
  741. return name;
  742. }
  743. private static bool ComapreMaterials(Material[] a, Material[] b)
  744. {
  745. if (a.Length != b.Length) return false;
  746. for (int i = 0; i < a.Length; i++)
  747. {
  748. if (a[i] != b[i]) return false;
  749. }
  750. return true;
  751. }
  752. private static void AddToDictionary(Mesh sourceMesh, ProcessedMeshInfo pmi, Dictionary<Mesh, List<ProcessedMeshInfo>> dict)
  753. {
  754. List<ProcessedMeshInfo> lpmi;
  755. if (!dict.ContainsKey(sourceMesh))
  756. {
  757. lpmi = new List<ProcessedMeshInfo>();
  758. dict[sourceMesh] = lpmi;
  759. }
  760. else
  761. {
  762. lpmi = dict[sourceMesh];
  763. }
  764. lpmi.Add(pmi);
  765. }
  766. private static bool ProcessMesh(Renderer r, Mesh m, List<UnityTransform> unityTransforms, MB3_MeshBaker mb)
  767. {
  768. unityTransforms.Clear();
  769. // position rotation and scale are baked into combined mesh.
  770. // Remember all the transforms settings then
  771. // record transform values to root of hierarchy
  772. Transform t = r.transform;
  773. if (t != t.root)
  774. {
  775. do
  776. {
  777. unityTransforms.Add(new UnityTransform(t));
  778. t = t.parent;
  779. } while (t != null && t != t.root);
  780. }
  781. //add the root
  782. unityTransforms.Add(new UnityTransform(t.root));
  783. //position at identity
  784. for (int k = 0; k < unityTransforms.Count; k++)
  785. {
  786. unityTransforms[k].t.localPosition = Vector3.zero;
  787. unityTransforms[k].t.localRotation = Quaternion.identity;
  788. unityTransforms[k].t.localScale = Vector3.one;
  789. }
  790. //bake the mesh
  791. MB3_MeshCombinerSingle mc = (MB3_MeshCombinerSingle)mb.meshCombiner;
  792. if (!MB3_BakeInPlace.BakeOneMesh(mc, m, r.gameObject))
  793. {
  794. return false;
  795. }
  796. //replace the mesh
  797. if (r is MeshRenderer)
  798. {
  799. MeshFilter mf = r.gameObject.GetComponent<MeshFilter>();
  800. mf.sharedMesh = m;
  801. }
  802. else
  803. { //skinned mesh
  804. SkinnedMeshRenderer smr = r.gameObject.GetComponent<SkinnedMeshRenderer>();
  805. smr.sharedMesh = m;
  806. smr.bones = ((SkinnedMeshRenderer)mc.targetRenderer).bones;
  807. }
  808. if (mc.targetRenderer != null)
  809. {
  810. SetMaterials(mc.targetRenderer.sharedMaterials, r);
  811. }
  812. //restore the transforms
  813. for (int k = 0; k < unityTransforms.Count; k++)
  814. {
  815. unityTransforms[k].t.localPosition = unityTransforms[k].p;
  816. unityTransforms[k].t.localRotation = unityTransforms[k].q;
  817. unityTransforms[k].t.localScale = unityTransforms[k].s;
  818. }
  819. mc.SetMesh(null);
  820. return true;
  821. }
  822. private static void SetMaterials(Material[] sharedMaterials, Renderer r)
  823. {
  824. //First try to get the materials from the target renderer. This is because the mesh may have fewer submeshes than number of result materials if some of the submeshes had zero length tris.
  825. //If we have just baked then materials on the target renderer will be correct wheras materials on the textureBakeResult may not be correct.
  826. Material[] sharedMats = new Material[sharedMaterials.Length];
  827. for (int i = 0; i < sharedMats.Length; i++)
  828. {
  829. sharedMats[i] = sharedMaterials[i];
  830. }
  831. if (r is SkinnedMeshRenderer)
  832. {
  833. r.sharedMaterial = null;
  834. r.sharedMaterials = sharedMats;
  835. }
  836. else
  837. {
  838. r.sharedMaterial = null;
  839. r.sharedMaterials = sharedMats;
  840. }
  841. }
  842. private static bool IsGoodToBake(Renderer r, MB2_TextureBakeResults tbr)
  843. {
  844. if (r == null) return false;
  845. if (!(r is MeshRenderer) && !(r is SkinnedMeshRenderer))
  846. {
  847. return false;
  848. }
  849. Material[] mats = r.sharedMaterials;
  850. for (int i = 0; i < mats.Length; i++)
  851. {
  852. if (!tbr.ContainsMaterial(mats[i]))
  853. {
  854. Debug.LogWarning("Mesh on " + r + " uses a material " + mats[i] + " that is not in the list of materials. This mesh will not be baked. The original mesh and material will be used in the result prefab.");
  855. return false;
  856. }
  857. }
  858. if (MB_Utility.GetMesh(r.gameObject) == null)
  859. {
  860. return false;
  861. }
  862. return true;
  863. }
  864. internal static Transform FindCorrespondingTransform(Transform srcRoot, Transform srcChild,
  865. Transform targRoot)
  866. {
  867. if (srcRoot == srcChild) return targRoot;
  868. // Debug.Log ("start ============");
  869. //build the path to the root in the source prefab
  870. List<Transform> path_root2child = new List<Transform>();
  871. Transform t = srcChild;
  872. do
  873. {
  874. path_root2child.Insert(0, t);
  875. t = t.parent;
  876. } while (t != null && t != t.root && t != srcRoot);
  877. if (t == null)
  878. {
  879. Debug.LogError("scrChild was not child of srcRoot " + srcRoot + " " + srcChild);
  880. return null;
  881. }
  882. path_root2child.Insert(0, srcRoot);
  883. // Debug.Log ("path to root for " + srcChild + " " + path_root2child.Count);
  884. //try to find a matching path in the target prefab
  885. t = targRoot;
  886. for (int i = 1; i < path_root2child.Count; i++)
  887. {
  888. Transform tSrc = path_root2child[i - 1];
  889. //try to find child in same position with same name
  890. int srcIdx = TIndexOf(tSrc, path_root2child[i]);
  891. if (srcIdx < t.childCount && path_root2child[i].name.Equals(t.GetChild(srcIdx).name))
  892. {
  893. t = t.GetChild(srcIdx);
  894. // Debug.Log ("found child in same position with same name " + t);
  895. continue;
  896. }
  897. //try to find child with same name
  898. for (int j = 0; j < t.childCount; j++)
  899. {
  900. if (t.GetChild(j).name.Equals(path_root2child[i].name))
  901. {
  902. t = t.GetChild(j);
  903. // Debug.Log ("found child with same name " + t);
  904. continue;
  905. }
  906. }
  907. t = null;
  908. break;
  909. }
  910. // Debug.Log ("end =============== " + t);
  911. return t;
  912. }
  913. private static int TIndexOf(Transform p, Transform c)
  914. {
  915. for (int i = 0; i < p.childCount; i++)
  916. {
  917. if (c == p.GetChild(i))
  918. {
  919. return i;
  920. }
  921. }
  922. return -1;
  923. }
  924. public void CreateEmptyOutputPrefabs()
  925. {
  926. if (outputFolder.stringValue == null)
  927. {
  928. Debug.LogError("Output folder must be set");
  929. return;
  930. }
  931. if (outputFolder.stringValue.StartsWith(Application.dataPath))
  932. {
  933. string relativePath = "Assets" + outputFolder.stringValue.Substring(Application.dataPath.Length);
  934. string gid = AssetDatabase.AssetPathToGUID(relativePath);
  935. if (gid == null)
  936. {
  937. Debug.LogError("Output folder must be a folder in the Unity project Asset folder");
  938. return;
  939. }
  940. }
  941. else
  942. {
  943. Debug.LogError("Output folder must be a folder in the Unity project Asset folder");
  944. return;
  945. }
  946. int numCreated = 0;
  947. int numSkippedSrcNull = 0;
  948. int numSkippedAlreadyExisted = 0;
  949. MB3_BatchPrefabBaker prefabBaker = (MB3_BatchPrefabBaker)target;
  950. for (int i = 0; i < prefabBaker.prefabRows.Length; i++)
  951. {
  952. if (prefabBaker.prefabRows[i].sourcePrefab != null)
  953. {
  954. if (prefabBaker.prefabRows[i].resultPrefab == null)
  955. {
  956. string outName = outputFolder.stringValue + "/" + prefabBaker.prefabRows[i].sourcePrefab.name + ".prefab";
  957. outName = outName.Replace(Application.dataPath, "");
  958. outName = "Assets" + outName;
  959. GameObject go = new GameObject(prefabBaker.prefabRows[i].sourcePrefab.name);
  960. prefabBaker.prefabRows[i].resultPrefab = PrefabUtility.CreatePrefab(outName, go);
  961. DestroyImmediate(go);
  962. numCreated++;
  963. }
  964. else
  965. {
  966. numSkippedAlreadyExisted++;
  967. }
  968. }
  969. else
  970. {
  971. numSkippedSrcNull++;
  972. }
  973. }
  974. Debug.Log(String.Format("Created {0} prefabs. Skipped {1} because source prefab was null. Skipped {2} because the result prefab was already assigned", numCreated, numSkippedSrcNull, numSkippedAlreadyExisted));
  975. }
  976. }
  977. }