//---------------------------------------------- // MeshBaker // Copyright © 2011-2012 Ian Deane //---------------------------------------------- using UnityEngine; using System.Collections; using System.IO; using System; using System.Collections.Specialized; using System.Collections.Generic; using System.Text.RegularExpressions; using DigitalOpus.MB.Core; using UnityEditor; namespace DigitalOpus.MB.MBEditor { public class MB_TextureBakerConfigureTextureArrays { private class Slice { public List atlasRects; public AtlasPackingResult packingResult; public int numAtlasRects; } private static GUIContent gc_TextureArrayOutputFormats = new GUIContent( "Texture Array Output Formats", "Texture Arrays do not have a 'TextureImporter' that lets you change the TextureArray format." + "You can provide a list of formats to be generated here. You will probably have one set of formats per platform."); public static void DrawTextureArrayConfiguration(MB3_TextureBaker momm, SerializedObject textureBaker, MB3_TextureBakerEditorInternal editorInternal) { EditorGUILayout.BeginVertical(editorInternal.editorStyles.multipleMaterialBackgroundStyle); EditorGUILayout.LabelField("Texture Array Slice Configuration", EditorStyles.boldLabel); float oldLabelWidth = EditorGUIUtility.labelWidth; if (GUILayout.Button(MB3_TextureBakerEditorInternal.configAtlasTextureSlicesFromObjsContent)) { ConfigureTextureArraysFromObjsToCombine(momm, editorInternal.resultMaterialsTexArray, textureBaker); } if (GUILayout.Button("Report texture sizes")) { Debug.Log(ReportTextureSizesAndFormats(momm)); } if (editorInternal.textureArrayOutputFormats.arraySize == 0) { EditorGUILayout.HelpBox("You need at least one output format.", MessageType.Error); } EditorGUILayout.PropertyField(editorInternal.textureArrayOutputFormats, gc_TextureArrayOutputFormats, true); EditorGUILayout.BeginHorizontal(); editorInternal.resultMaterialsFoldout = EditorGUILayout.Foldout(editorInternal.resultMaterialsFoldout, MB3_TextureBakerEditorInternal.textureArrayCombinedMaterialFoldoutGUIContent); if (GUILayout.Button(MB3_TextureBakerEditorInternal.insertContent, EditorStyles.miniButtonLeft, MB3_TextureBakerEditorInternal.buttonWidth)) { if (editorInternal.resultMaterialsTexArray.arraySize == 0) { momm.resultMaterialsTexArray = new MB_MultiMaterialTexArray[1]; } else { int idx = editorInternal.resultMaterialsTexArray.arraySize - 1; editorInternal.resultMaterialsTexArray.InsertArrayElementAtIndex(idx); } } if (GUILayout.Button(MB3_TextureBakerEditorInternal.deleteContent, EditorStyles.miniButtonRight, MB3_TextureBakerEditorInternal.buttonWidth)) { editorInternal.resultMaterialsTexArray.DeleteArrayElementAtIndex(editorInternal.resultMaterialsTexArray.arraySize - 1); } EditorGUILayout.EndHorizontal(); if (editorInternal.resultMaterialsFoldout) { for (int i = 0; i < editorInternal.resultMaterialsTexArray.arraySize; i++) { EditorGUILayout.Separator(); if (i % 2 == 1) { EditorGUILayout.BeginVertical(editorInternal.editorStyles.multipleMaterialBackgroundStyle); } else { EditorGUILayout.BeginVertical(editorInternal.editorStyles.multipleMaterialBackgroundStyleDarker); } string s = ""; if (i < momm.resultMaterialsTexArray.Length && momm.resultMaterialsTexArray[i] != null && momm.resultMaterialsTexArray[i].combinedMaterial != null) s = momm.resultMaterialsTexArray[i].combinedMaterial.shader.ToString(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("---------- submesh:" + i + " " + s, EditorStyles.boldLabel); if (GUILayout.Button(MB3_TextureBakerEditorInternal.deleteContent, EditorStyles.miniButtonRight, MB3_TextureBakerEditorInternal.buttonWidth)) { editorInternal.resultMaterialsTexArray.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); if (i < editorInternal.resultMaterialsTexArray.arraySize) { EditorGUILayout.Separator(); SerializedProperty resMat = editorInternal.resultMaterialsTexArray.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(resMat.FindPropertyRelative("combinedMaterial")); SerializedProperty slices = resMat.FindPropertyRelative("slices"); EditorGUILayout.PropertyField(slices, true); } EditorGUILayout.EndVertical(); } } EditorGUILayout.EndVertical(); } public static string ReportTextureSizesAndFormats(MB3_TextureBaker mom) { if (mom.resultType != MB2_TextureBakeResults.ResultType.textureArray) { Debug.LogError("Result Type must be Texture Array."); return ""; } for (int resMatIdx = 0; resMatIdx < mom.resultMaterialsTexArray.Length; resMatIdx++) { MB_MultiMaterialTexArray resMatTexArray = mom.resultMaterialsTexArray[resMatIdx]; if (resMatTexArray.combinedMaterial == null) { Debug.LogError("Result Material " + resMatIdx + " is null"); return ""; } } System.Text.StringBuilder sb = new System.Text.StringBuilder(); // Visit each result material for (int resMatIdx = 0; resMatIdx < mom.resultMaterialsTexArray.Length; resMatIdx++) { MB_MultiMaterialTexArray resMatTexArray = mom.resultMaterialsTexArray[resMatIdx]; // Do an atlas pack in order to collect all the textures needed by the result material // And group these by material texture property. MB3_TextureCombiner combiner = mom.CreateAndConfigureTextureCombiner(); combiner.saveAtlasesAsAssets = false; combiner.fixOutOfBoundsUVs = false; List packingResults = new List(); Material tempMat = new Material(resMatTexArray.combinedMaterial.shader); List allSourceMaterials = new List(); for (int sliceIdx = 0; sliceIdx < resMatTexArray.slices.Count; sliceIdx++) { List srcMats = new List(); resMatTexArray.slices[sliceIdx].GetAllUsedMaterials(srcMats); for (int srcMatIdx = 0; srcMatIdx < srcMats.Count; srcMatIdx++) { if (srcMats[srcMatIdx] != null && !allSourceMaterials.Contains(srcMats[srcMatIdx])) { allSourceMaterials.Add(srcMats[srcMatIdx]); } } } MB_AtlasesAndRects atlasesAndRects = new MB_AtlasesAndRects(); combiner.CombineTexturesIntoAtlases(null, atlasesAndRects, tempMat, mom.GetObjectsToCombine(), allSourceMaterials, null, packingResults, onlyPackRects:true, splitAtlasWhenPackingIfTooBig:false); // Now vist the packing results and collect all the textures Debug.Assert(packingResults.Count == 1); for (int texPropIdx = 0; texPropIdx < atlasesAndRects.texPropertyNames.Length; texPropIdx++) { string propertyName = atlasesAndRects.texPropertyNames[texPropIdx]; sb.AppendLine(String.Format("Prop: {0}", propertyName)); List matsData = (List)packingResults[0].data; HashSet visitedMats = new HashSet(); for (int matAndGoIdx = 0; matAndGoIdx < matsData.Count; matAndGoIdx++) { Material mat = matsData[matAndGoIdx].material; if (visitedMats.Contains(mat)) continue; visitedMats.Add(mat); if (mat.HasProperty(propertyName)) { Texture tex = mat.GetTexture(propertyName); if (tex != null) { string texFormatString = "UnknownFormat"; string texWrapMode = "UnknownClampMode"; if (tex is Texture2D) { texFormatString = ((Texture2D)tex).format.ToString(); texWrapMode = ((Texture2D)tex).wrapMode.ToString(); } sb.AppendLine(String.Format(" {0} x {1} format:{2} wrapMode:{3} {4}", tex.width.ToString().PadLeft(6,' '), tex.height.ToString().PadRight(6,' '), texFormatString.PadRight(20,' '), texWrapMode.PadRight(12,' '), tex.name)); } } } } } return sb.ToString(); } public static void ConfigureTextureArraysFromObjsToCombine(MB3_TextureBaker mom, SerializedProperty resultMaterialsTexArrays, SerializedObject textureBaker) { if (mom.GetObjectsToCombine().Count == 0) { Debug.LogError("You need to add some objects to combine before building the texture array result materials."); return; } if (resultMaterialsTexArrays.arraySize > 0) { Debug.LogError("You already have some texture array result materials configured. You must remove these before doing this operation."); return; } if (mom.textureBakeResults == null) { Debug.LogError("Texture Bake Result asset must be set before using this operation."); return; } //validate that the objects to be combined are valid for (int i = 0; i < mom.GetObjectsToCombine().Count; i++) { GameObject go = mom.GetObjectsToCombine()[i]; if (go == null) { Debug.LogError("Null object in list of objects to combine at position " + i); return; } if (MB_Utility.GetMesh(go) == null) { Debug.LogError("Could not get mesh for object in list of objects to combine at position " + i); return; } Renderer r = go.GetComponent(); if (r == null || (!(r is MeshRenderer) && !(r is SkinnedMeshRenderer))) { Debug.LogError("GameObject at position " + i + " in list of objects to combine did not have a renderer"); return; } if (r.sharedMaterial == null) { Debug.LogError("GameObject at position " + i + " in list of objects to combine has a null material"); return; } } //Will sort into "result material" // slices Dictionary> shader2ResultMat_map = new Dictionary>(); // first pass split by shader and analyse meshes. List objsToCombine = mom.GetObjectsToCombine(); for (int meshIdx = 0; meshIdx < objsToCombine.Count; meshIdx++) { GameObject srcGo = objsToCombine[meshIdx]; Mesh mesh = MB_Utility.GetMesh(srcGo); Renderer r = MB_Utility.GetRenderer(srcGo); if (mom.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log("1st Pass 'Split By Shader' Processing Mesh: "+ mesh +" Num submeshes: " + r.sharedMaterials.Length); for (int submeshIdx = 0; submeshIdx < r.sharedMaterials.Length; submeshIdx++) { if (r.sharedMaterials[submeshIdx] == null) continue; MB3_TextureBakerEditorInternal.MultiMatSubmeshInfo newKey = new MB3_TextureBakerEditorInternal.MultiMatSubmeshInfo(r.sharedMaterials[submeshIdx].shader, r.sharedMaterials[submeshIdx]); // Initially we fill the list of srcMaterials with garbage MB_MaterialAndUVRects. Will get proper ones when we atlas pack. MB_MaterialAndUVRect submeshMaterial = new MB_MaterialAndUVRect( r.sharedMaterials[submeshIdx], new Rect(0, 0, 0, 0), // garbage value false, //garbage value new Rect(0, 0, 0, 0), // garbage value new Rect(0, 0, 0, 0), // garbage value new Rect(0, 0, 1, 1), // garbage value MB_TextureTilingTreatment.unknown, // garbage value r.name); submeshMaterial.objectsThatUse = new List(); submeshMaterial.objectsThatUse.Add(r.gameObject); if (!shader2ResultMat_map.ContainsKey(newKey)) { // this is a new shader create a new result material Slice srcMaterials = new Slice { atlasRects = new List(), numAtlasRects = 1, }; srcMaterials.atlasRects.Add(submeshMaterial); List binsOfMatsThatUseShader = new List(); binsOfMatsThatUseShader.Add(srcMaterials); if (mom.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" Adding Source Material: " + submeshMaterial.material); shader2ResultMat_map.Add(newKey, binsOfMatsThatUseShader); } else { // there is a result material that uses this shader. Add this source material Slice srcMaterials = shader2ResultMat_map[newKey][0]; // There should only be one list of source materials if (srcMaterials.atlasRects.Find(x => x.material == submeshMaterial.material) == null) { if (mom.LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" Adding Source Material: " + submeshMaterial.material); srcMaterials.atlasRects.Add(submeshMaterial); } } } } int resMatCount = 0; foreach (MB3_TextureBakerEditorInternal.MultiMatSubmeshInfo resultMat in shader2ResultMat_map.Keys) { // at this point there there should be only one slice with all the source materials resMatCount++; // For each result material, all source materials are in the first slice. // We will now split these using a texture packer. Each "atlas" generated by the packer will be a slice. { // All source materials should be in the first slice at this point. List slices = shader2ResultMat_map[resultMat]; List newSlices = new List(); Slice firstSlice = slices[0]; List allMatsThatUserShader = new List(); List objsThatUseFirstSlice = new List(); for (int i = 0; i < firstSlice.atlasRects.Count; i++) { allMatsThatUserShader.Add(firstSlice.atlasRects[i].material); if (!objsThatUseFirstSlice.Contains(firstSlice.atlasRects[i].objectsThatUse[0])) { objsThatUseFirstSlice.Add(firstSlice.atlasRects[i].objectsThatUse[0]); } } MB3_TextureCombiner combiner = mom.CreateAndConfigureTextureCombiner(); combiner.packingAlgorithm = MB2_PackingAlgorithmEnum.MeshBakerTexturePacker; combiner.saveAtlasesAsAssets = false; combiner.fixOutOfBoundsUVs = true; combiner.doMergeDistinctMaterialTexturesThatWouldExceedAtlasSize = true; List packingResults = new List(); Material tempMat = new Material(resultMat.shader); if (mom.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("======== 2nd pass. Use atlas packer to split the first slice into multiple if it exceeds atlas size. "); combiner.CombineTexturesIntoAtlases(null, null, tempMat, mom.GetObjectsToCombine(), allMatsThatUserShader, null, packingResults, onlyPackRects:true, splitAtlasWhenPackingIfTooBig:true); if (mom.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("======== Completed packing with texture packer. numPackingResults: " + packingResults.Count); newSlices.Clear(); // The texture packing just split the atlas into multiple atlases. Each atlas will become a "slice". for (int newSliceIdx = 0; newSliceIdx < packingResults.Count; newSliceIdx++) { List sourceMats = new List(); List packedMatRects = (List) packingResults[newSliceIdx].data; HashSet distinctAtlasRects = new HashSet(); for (int packedMatRectIdx = 0; packedMatRectIdx < packedMatRects.Count; packedMatRectIdx++) { MB_MaterialAndUVRect muvr = packedMatRects[packedMatRectIdx]; distinctAtlasRects.Add(muvr.atlasRect); { Rect encapsulatingRect = muvr.GetEncapsulatingRect(); Vector2 sizeInAtlas_px = new Vector2( packingResults[newSliceIdx].atlasX * encapsulatingRect.width, packingResults[newSliceIdx].atlasY * encapsulatingRect.height); } sourceMats.Add(muvr); } Slice slice = new Slice() { atlasRects = sourceMats, packingResult = packingResults[newSliceIdx], numAtlasRects = distinctAtlasRects.Count, }; newSlices.Add(slice); } // Replace first slice with split version. if (mom.LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("First slice exceeded atlas size splitting it into " + newSlices.Count + " slices"); slices.RemoveAt(0); for (int i = 0; i < newSlices.Count; i++) { slices.Insert(i, newSlices[i]); } } } // build the texture array result materials if (shader2ResultMat_map.Count == 0) Debug.LogError("Found no materials in list of objects to combine"); mom.resultMaterialsTexArray = new MB_MultiMaterialTexArray[shader2ResultMat_map.Count]; int k = 0; foreach (MB3_TextureBakerEditorInternal.MultiMatSubmeshInfo resMatKey in shader2ResultMat_map.Keys) { List srcSlices = shader2ResultMat_map[resMatKey]; MB_MultiMaterialTexArray mm = mom.resultMaterialsTexArray[k] = new MB_MultiMaterialTexArray(); for (int sliceIdx = 0; sliceIdx < srcSlices.Count; sliceIdx++) { Slice slice = srcSlices[sliceIdx]; MB_TexArraySlice resSlice = new MB_TexArraySlice(); List usedMats = new List(); for (int srcMatIdx = 0; srcMatIdx < slice.atlasRects.Count; srcMatIdx++) { MB_MaterialAndUVRect matAndUVRect = slice.atlasRects[srcMatIdx]; List objsThatUse = matAndUVRect.objectsThatUse; for (int objsThatUseIdx = 0; objsThatUseIdx < objsThatUse.Count; objsThatUseIdx++) { GameObject obj = objsThatUse[objsThatUseIdx]; if (!resSlice.ContainsMaterialAndMesh(slice.atlasRects[srcMatIdx].material, MB_Utility.GetMesh(obj))) { resSlice.sourceMaterials.Add( new MB_TexArraySliceRendererMatPair() { renderer = obj, sourceMaterial = slice.atlasRects[srcMatIdx].material } ); } } } { // Should we use considerUVs bool doConsiderUVs = false; // If there is more than one atlas rectangle in a slice then use considerUVs if (slice.numAtlasRects > 1) { doConsiderUVs = true; } else { // There is only one source material, could be: // - lots of tiling (don't want consider UVs) // - We are extracting a small part of a large atlas (want considerUVs) if (slice.packingResult.atlasX >= mom.maxAtlasSize || slice.packingResult.atlasY >= mom.maxAtlasSize) { doConsiderUVs = false; // lots of tiling } else { doConsiderUVs = true; // extracting a small part of an atlas } } resSlice.considerMeshUVs = doConsiderUVs; } mm.slices.Add(resSlice); } // Enforce integrity. If a material appears in more than one slice then all those slices must be considerUVs=true { // collect all distinct materials HashSet distinctMats = new HashSet(); Dictionary mat2sliceCount = new Dictionary(); for (int sliceIdx = 0; sliceIdx < mm.slices.Count; sliceIdx++) { for (int sliceMatIdx = 0; sliceMatIdx < mm.slices[sliceIdx].sourceMaterials.Count; sliceMatIdx++) { Material mat = mm.slices[sliceIdx].sourceMaterials[sliceMatIdx].sourceMaterial; distinctMats.Add(mat); mat2sliceCount[mat] = 0; } } // Count the number of slices that use each material. foreach (Material mat in distinctMats) { for (int sliceIdx = 0; sliceIdx < mm.slices.Count; sliceIdx++) { if (mm.slices[sliceIdx].ContainsMaterial(mat)) { mat2sliceCount[mat] = mat2sliceCount[mat] + 1; } } } // Check that considerUVs is true for any materials that appear more than once foreach (Material mat in distinctMats) { if (mat2sliceCount[mat] > 1) { for (int sliceIdx = 0; sliceIdx < mm.slices.Count; sliceIdx++) { if (mm.slices[sliceIdx].ContainsMaterial(mat)) { if (mom.LOG_LEVEL >= MB2_LogLevel.debug && mm.slices[sliceIdx].considerMeshUVs) Debug.Log("There was a material " + mat + " that was used by more than one slice and considerUVs was false. sliceIdx:" + sliceIdx); mm.slices[sliceIdx].considerMeshUVs = true; } } } } } // Cleanup. remove "Renderer"s from source materials that do not use considerUVs and delete extra { // put any slices with consider UVs first List newSlices = new List(); for (int sliceIdx = 0; sliceIdx < mm.slices.Count; sliceIdx++) { if (mm.slices[sliceIdx].considerMeshUVs == true) { newSlices.Add(mm.slices[sliceIdx]); } } // for any slices without considerUVs, remove "renderer" and truncate for (int sliceIdx = 0; sliceIdx < mm.slices.Count; sliceIdx++) { MB_TexArraySlice slice = mm.slices[sliceIdx]; if (slice.considerMeshUVs == false) { newSlices.Add(slice); HashSet distinctMats = slice.GetDistinctMaterials(); slice.sourceMaterials.Clear(); foreach (Material mat in distinctMats) { slice.sourceMaterials.Add(new MB_TexArraySliceRendererMatPair() {sourceMaterial = mat }); } } } mm.slices = newSlices; } string pth = AssetDatabase.GetAssetPath(mom.textureBakeResults); string baseName = Path.GetFileNameWithoutExtension(pth); string folderPath = pth.Substring(0, pth.Length - baseName.Length - 6); string matName = folderPath + baseName + "-mat" + k + ".mat"; Material existingAsset = AssetDatabase.LoadAssetAtPath(matName); if (!existingAsset) { Material newMat = new Material(Shader.Find("Standard")); // Don't try to configure the material we need the user to pick a shader that has TextureArrays AssetDatabase.CreateAsset(newMat, matName); } mm.combinedMaterial = (Material)AssetDatabase.LoadAssetAtPath(matName, typeof(Material)); k++; } MBVersionEditor.UpdateIfDirtyOrScript(textureBaker); textureBaker.Update(); } } }