using System.Collections; using System.Collections.Generic; using System; using UnityEngine; namespace DigitalOpus.MB.Core { public class MB_TextureArrays { internal class TexturePropertyData { public bool[] doMips; public int[] numMipMaps; public TextureFormat[] formats; public Vector2[] sizes; } internal static bool[] DetermineWhichPropertiesHaveTextures(MB_AtlasesAndRects[] resultAtlasesAndRectSlices) { bool[] hasTexForProperty = new bool[resultAtlasesAndRectSlices[0].texPropertyNames.Length]; for (int i = 0; i < hasTexForProperty.Length; i++) { hasTexForProperty[i] = false; } int numSlices = resultAtlasesAndRectSlices.Length; for (int sliceIdx = 0; sliceIdx < numSlices; sliceIdx++) { MB_AtlasesAndRects slice = resultAtlasesAndRectSlices[sliceIdx]; Debug.Assert(slice.texPropertyNames.Length == hasTexForProperty.Length); for (int propIdx = 0; propIdx < hasTexForProperty.Length; propIdx++) { if (slice.atlases[propIdx] != null) { hasTexForProperty[propIdx] = true; } } } return hasTexForProperty; } /// /// Creates one texture array per texture property. /// /// internal static Texture2DArray[] CreateTextureArraysForResultMaterial(TexturePropertyData texPropertyData, MB_AtlasesAndRects[] resultAtlasesAndRectSlices, bool[] hasTexForProperty, MB3_TextureCombiner combiner, MB2_LogLevel LOG_LEVEL) { Debug.Assert(texPropertyData.sizes.Length == hasTexForProperty.Length); // ASSUMPTION all slices in the same format and the same size, alpha channel and mipMapCount string[] texPropertyNames = resultAtlasesAndRectSlices[0].texPropertyNames; Debug.Assert(texPropertyNames.Length == hasTexForProperty.Length); Texture2DArray[] texArrays = new Texture2DArray[texPropertyNames.Length]; // Each texture property (_MainTex, _Bump, ...) becomes a Texture2DArray for (int propIdx = 0; propIdx < texPropertyNames.Length; propIdx++) { if (!hasTexForProperty[propIdx]) continue; int numSlices = resultAtlasesAndRectSlices.Length; int w = (int)texPropertyData.sizes[propIdx].x; int h = (int)texPropertyData.sizes[propIdx].y; int numMips = (int)texPropertyData.numMipMaps[propIdx]; TextureFormat format = texPropertyData.formats[propIdx]; bool doMipMaps = texPropertyData.doMips[propIdx]; Texture2DArray texArray = new Texture2DArray(w, h, numSlices, format, doMipMaps); if (LOG_LEVEL >= MB2_LogLevel.info) Debug.LogFormat("Creating Texture2DArray for property: {0} w: {1} h: {2} format: {3} doMips: {4}", texPropertyNames[propIdx], w, h, format, doMipMaps); for (int sliceIdx = 0; sliceIdx < numSlices; sliceIdx++) { Debug.Assert(resultAtlasesAndRectSlices[sliceIdx].atlases.Length == texPropertyNames.Length); Debug.Assert(resultAtlasesAndRectSlices[sliceIdx].texPropertyNames[propIdx] == texPropertyNames[propIdx]); Texture2D srcTex = resultAtlasesAndRectSlices[sliceIdx].atlases[propIdx]; if (LOG_LEVEL >= MB2_LogLevel.debug) Debug.LogFormat("Slice: {0} texture: {1}", sliceIdx, srcTex); bool isCopy = false; if (srcTex == null) { // Slices might not have all textures create a dummy if needed. srcTex = combiner._createTemporaryTexture(texPropertyNames[propIdx], w, h, format, doMipMaps); } Debug.Assert(srcTex.width == texArray.width, "Source texture is not the same width as the texture array " + srcTex); Debug.Assert(srcTex.height == texArray.height, "Source texture is not the same height as the texture array " + srcTex); Debug.Assert(srcTex.mipmapCount == numMips, "Source texture does have not the same number of mips as the texture array: " + srcTex + " numMipsTex: " + srcTex.mipmapCount + " numMipsTexArray: " + numMips + " texDims: " + srcTex.width + "x" + srcTex.height); Debug.Assert(srcTex.format == format, "Formats should have been converted before this. Texture: " + srcTex + "Source: " + srcTex.format + " Targ: " + format); for (int mipIdx = 0; mipIdx < numMips; mipIdx++) { Graphics.CopyTexture(srcTex, 0, mipIdx, texArray, sliceIdx, mipIdx); } if (isCopy) MB_Utility.Destroy(srcTex); } texArray.Apply(); texArrays[propIdx] = texArray; } return texArrays; } internal static bool ConvertTexturesToReadableFormat(TexturePropertyData texturePropertyData, MB_AtlasesAndRects[] resultAtlasesAndRectSlices, bool[] hasTexForProperty, List textureShaderProperties, MB3_TextureCombiner combiner, MB2_LogLevel logLevel, List createdTemporaryTextureAssets, MB2_EditorMethodsInterface textureEditorMethods) { for (int propIdx = 0; propIdx < hasTexForProperty.Length; propIdx++) { if (!hasTexForProperty[propIdx]) continue; TextureFormat format = texturePropertyData.formats[propIdx]; if (!textureEditorMethods.TextureImporterFormatExistsForTextureFormat(format)) { Debug.LogError("Could not find target importer format matching " + format); return false; } int numSlices = resultAtlasesAndRectSlices.Length; int targetWidth = (int)texturePropertyData.sizes[propIdx].x; int targetHeight = (int)texturePropertyData.sizes[propIdx].y; for (int sliceIdx = 0; sliceIdx < numSlices; sliceIdx++) { Texture2D sliceTex = resultAtlasesAndRectSlices[sliceIdx].atlases[propIdx]; Debug.Assert(sliceTex != null, "sliceIdx " + sliceIdx + " " + propIdx); if (sliceTex != null) { if (!MBVersion.IsTextureReadable(sliceTex)) { textureEditorMethods.SetReadWriteFlag(sliceTex, true, true); } bool isAsset = textureEditorMethods.IsAnAsset(sliceTex); if (logLevel >= MB2_LogLevel.trace) Debug.Log("Considering format of texture: " + sliceTex + " format:" + sliceTex.format); if ((sliceTex.width != targetWidth || sliceTex.height != targetHeight) || (!isAsset && sliceTex.format != format)) { // Do this the horrible hard way. It is only possible to resize textures in TrueColor formats, // And only possible to switch formats using the Texture importer. // Create a resized temporary texture asset in ARGB32 format. Then set its texture format and reimport resultAtlasesAndRectSlices[sliceIdx].atlases[propIdx] = textureEditorMethods.CreateTemporaryAssetCopy(sliceTex, targetWidth, targetHeight, format, logLevel); createdTemporaryTextureAssets.Add(resultAtlasesAndRectSlices[sliceIdx].atlases[propIdx]); } else if (sliceTex.format != format) { textureEditorMethods.AddTextureFormat(sliceTex, format, textureShaderProperties[propIdx].isNormalMap); } } else { } if (resultAtlasesAndRectSlices[sliceIdx].atlases[propIdx].format != format) { Debug.LogError("Could not convert texture to format " + format + ". This can happen if the target build platform in build settings does not support textures in this format." + " It may be necessary to switch the build platform in order to build texture arrays in this format."); return false; } } } return true; } /// /// Third coord is num mipmaps. /// internal static void FindBestSizeAndMipCountAndFormatForTextureArrays(List texPropertyNames, int maxAtlasSize, MB_TextureArrayFormatSet targetFormatSet, MB_AtlasesAndRects[] resultAtlasesAndRectSlices, TexturePropertyData texturePropertyData) { texturePropertyData.sizes = new Vector2[texPropertyNames.Count]; texturePropertyData.doMips = new bool[texPropertyNames.Count]; texturePropertyData.numMipMaps = new int[texPropertyNames.Count]; texturePropertyData.formats = new TextureFormat[texPropertyNames.Count]; for (int propIdx = 0; propIdx < texPropertyNames.Count; propIdx++) { int numSlices = resultAtlasesAndRectSlices.Length; texturePropertyData.sizes[propIdx] = new Vector3(16, 16, 1); bool hasMips = false; int mipCount = 1; for (int sliceIdx = 0; sliceIdx < numSlices; sliceIdx++) { Debug.Assert(resultAtlasesAndRectSlices[sliceIdx].atlases.Length == texPropertyNames.Count); Debug.Assert(resultAtlasesAndRectSlices[sliceIdx].texPropertyNames[propIdx] == texPropertyNames[propIdx].name); Texture2D sliceTex = resultAtlasesAndRectSlices[sliceIdx].atlases[propIdx]; if (sliceTex != null) { if (sliceTex.mipmapCount > 1) hasMips = true; mipCount = Mathf.Max(mipCount, sliceTex.mipmapCount); texturePropertyData.sizes[propIdx].x = Mathf.Min(Mathf.Max(texturePropertyData.sizes[propIdx].x, sliceTex.width), maxAtlasSize); texturePropertyData.sizes[propIdx].y = Mathf.Min(Mathf.Max(texturePropertyData.sizes[propIdx].y, sliceTex.height), maxAtlasSize); //texturePropertyData.sizes[propIdx].z = Mathf.Max(texturePropertyData.sizes[propIdx].z, sliceTex.mipmapCount); texturePropertyData.formats[propIdx] = targetFormatSet.GetFormatForProperty(texPropertyNames[propIdx].name); } } int numberMipsForMaxAtlasSize = Mathf.CeilToInt(Mathf.Log(maxAtlasSize, 2)) + 1; texturePropertyData.numMipMaps[propIdx] = Mathf.Min(numberMipsForMaxAtlasSize, mipCount); texturePropertyData.doMips[propIdx] = hasMips; } } internal static IEnumerator _CreateAtlasesCoroutineSingleResultMaterial(int resMatIdx, MB_TextureArrayResultMaterial bakedMatsAndSlicesResMat, MB_MultiMaterialTexArray resMatConfig, List objsToMesh, MB3_TextureCombiner combiner, MB_TextureArrayFormatSet[] textureArrayOutputFormats, MB_MultiMaterialTexArray[] resultMaterialsTexArray, List customShaderProperties, ProgressUpdateDelegate progressInfo, MB3_TextureCombiner.CreateAtlasesCoroutineResult coroutineResult, bool saveAtlasesAsAssets = false, MB2_EditorMethodsInterface editorMethods = null, float maxTimePerFrame = .01f) { MB2_LogLevel LOG_LEVEL = combiner.LOG_LEVEL; if (LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Baking atlases for result material " + resMatIdx + " num slices:" + resMatConfig.slices.Count); // Each result material can be one set of slices per textureProperty. Each slice can be an atlas. // Create atlases for each slice. List generatedTemporaryAtlases = new List(); { combiner.saveAtlasesAsAssets = false; // Don't want generated atlas slices to be assets List slicesConfig = resMatConfig.slices; for (int sliceIdx = 0; sliceIdx < slicesConfig.Count; sliceIdx++) { Material resMatToPass = null; List srcMatAndObjPairs = slicesConfig[sliceIdx].sourceMaterials; if (LOG_LEVEL >= MB2_LogLevel.trace) Debug.Log(" Baking atlases for result material:" + resMatIdx + " slice:" + sliceIdx); resMatToPass = resMatConfig.combinedMaterial; combiner.fixOutOfBoundsUVs = slicesConfig[sliceIdx].considerMeshUVs; MB3_TextureCombiner.CombineTexturesIntoAtlasesCoroutineResult coroutineResult2 = new MB3_TextureCombiner.CombineTexturesIntoAtlasesCoroutineResult(); MB_AtlasesAndRects sliceAtlasesAndRectOutput = bakedMatsAndSlicesResMat.slices[sliceIdx]; List usedMats = new List(); slicesConfig[sliceIdx].GetAllUsedMaterials(usedMats); yield return combiner.CombineTexturesIntoAtlasesCoroutine(progressInfo, sliceAtlasesAndRectOutput, resMatToPass, slicesConfig[sliceIdx].GetAllUsedRenderers(objsToMesh), usedMats, editorMethods, coroutineResult2, maxTimePerFrame, onlyPackRects: false, splitAtlasWhenPackingIfTooBig: false); coroutineResult.success = coroutineResult2.success; if (!coroutineResult.success) { coroutineResult.isFinished = true; yield break; } // Track which slices are new generated texture instances. Atlases could be original texture assets (one tex per atlas) or temporary texture instances in memory that will need to be destroyed. { for (int texPropIdx = 0; texPropIdx < sliceAtlasesAndRectOutput.atlases.Length; texPropIdx++) { Texture2D atlas = sliceAtlasesAndRectOutput.atlases[texPropIdx]; if (atlas != null) { bool atlasWasASourceTexture = false; for (int srcMatIdx = 0; srcMatIdx < srcMatAndObjPairs.Count; srcMatIdx++) { Material srcMat = srcMatAndObjPairs[srcMatIdx].sourceMaterial; if (srcMat.HasProperty(sliceAtlasesAndRectOutput.texPropertyNames[texPropIdx]) && srcMat.GetTexture(sliceAtlasesAndRectOutput.texPropertyNames[texPropIdx]) == atlas) { atlasWasASourceTexture = true; break; } } if (!atlasWasASourceTexture) { generatedTemporaryAtlases.Add(new MB3_TextureCombiner.TemporaryTexture(sliceAtlasesAndRectOutput.texPropertyNames[texPropIdx], atlas)); } } } } // end visit slices Debug.Assert(combiner._getNumTemporaryTextures() == 0, "Combiner should have no temporary textures."); } combiner.saveAtlasesAsAssets = saveAtlasesAsAssets; // Restore original setting. } // Generated atlas textures are temporary for texture arrays. They exist only in memory. Need to be cleaned up after we create slices. for (int i = 0; i < generatedTemporaryAtlases.Count; i++) { combiner.AddTemporaryTexture(generatedTemporaryAtlases[i]); } List texPropertyNames = new List(); MB3_TextureCombinerPipeline._CollectPropertyNames(texPropertyNames, customShaderProperties, resMatConfig.combinedMaterial, LOG_LEVEL); // The slices are built from different source-material-lists. Each slice can have different sets of texture properties missing (nulls). // Build a master list of texture properties. bool[] hasTexForProperty = MB_TextureArrays.DetermineWhichPropertiesHaveTextures(bakedMatsAndSlicesResMat.slices); List temporaryTextureAssets = new List(); try { MB_MultiMaterialTexArray resMaterial = resMatConfig; Dictionary resTexArraysByProperty = new Dictionary(); { // Initialize so I don't need to check if properties exist later. for (int propIdx = 0; propIdx < texPropertyNames.Count; propIdx++) { if (hasTexForProperty[propIdx]) resTexArraysByProperty[texPropertyNames[propIdx].name] = new MB_TexArrayForProperty(texPropertyNames[propIdx].name, new MB_TextureArrayReference[textureArrayOutputFormats.Length]); } } MB3_TextureCombinerNonTextureProperties textureBlender = null; textureBlender = new MB3_TextureCombinerNonTextureProperties(LOG_LEVEL, combiner.considerNonTextureProperties); textureBlender.LoadTextureBlendersIfNeeded(resMatConfig.combinedMaterial); textureBlender.AdjustNonTextureProperties(resMatConfig.combinedMaterial, texPropertyNames, editorMethods); // Vist each TextureFormatSet for (int texFormatSetIdx = 0; texFormatSetIdx < textureArrayOutputFormats.Length; texFormatSetIdx++) { MB_TextureArrayFormatSet textureArrayFormatSet = textureArrayOutputFormats[texFormatSetIdx]; editorMethods.Clear(); MB_TextureArrays.TexturePropertyData texPropertyData = new MB_TextureArrays.TexturePropertyData(); MB_TextureArrays.FindBestSizeAndMipCountAndFormatForTextureArrays(texPropertyNames, combiner.maxAtlasSize, textureArrayFormatSet, bakedMatsAndSlicesResMat.slices, texPropertyData); // Create textures we might need if they don't exist. { for (int propIdx = 0; propIdx < hasTexForProperty.Length; propIdx++) { if (hasTexForProperty[propIdx]) { TextureFormat format = texPropertyData.formats[propIdx]; int numSlices = bakedMatsAndSlicesResMat.slices.Length; int targetWidth = (int)texPropertyData.sizes[propIdx].x; int targetHeight = (int)texPropertyData.sizes[propIdx].y; for (int sliceIdx = 0; sliceIdx < numSlices; sliceIdx++) { if (bakedMatsAndSlicesResMat.slices[sliceIdx].atlases[propIdx] == null) { Texture2D sliceTex = new Texture2D(targetWidth, targetHeight, format, texPropertyData.doMips[propIdx]); Color col = textureBlender.GetColorForTemporaryTexture(resMatConfig.slices[sliceIdx].sourceMaterials[0].sourceMaterial, texPropertyNames[propIdx]); MB_Utility.setSolidColor(sliceTex, col); bakedMatsAndSlicesResMat.slices[sliceIdx].atlases[propIdx] = editorMethods.CreateTemporaryAssetCopy(sliceTex, targetWidth, targetHeight, format, LOG_LEVEL); temporaryTextureAssets.Add(bakedMatsAndSlicesResMat.slices[sliceIdx].atlases[propIdx]); MB_Utility.Destroy(sliceTex); } } } } } if (LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Converting source textures to readable formats."); if (MB_TextureArrays.ConvertTexturesToReadableFormat(texPropertyData, bakedMatsAndSlicesResMat.slices, hasTexForProperty, texPropertyNames, combiner, LOG_LEVEL, temporaryTextureAssets, editorMethods)) { // We now have a set of slices (one per textureProperty). Build these into Texture2DArray's. if (LOG_LEVEL >= MB2_LogLevel.debug) Debug.Log("Creating texture arrays"); if (LOG_LEVEL >= MB2_LogLevel.info) Debug.Log("THERE MAY BE ERRORS IN THE CONSOLE ABOUT 'Rebuilding mipmaps ... not supported'. THESE ARE PROBABLY FALSE POSITIVES AND CAN BE IGNORED."); Texture2DArray[] textureArrays = MB_TextureArrays.CreateTextureArraysForResultMaterial(texPropertyData, bakedMatsAndSlicesResMat.slices, hasTexForProperty, combiner, LOG_LEVEL); // Now have texture arrays for a result material, for all props. Save it. for (int propIdx = 0; propIdx < textureArrays.Length; propIdx++) { if (hasTexForProperty[propIdx]) { MB_TextureArrayReference texRef = new MB_TextureArrayReference(textureArrayFormatSet.name, textureArrays[propIdx]); resTexArraysByProperty[texPropertyNames[propIdx].name].formats[texFormatSetIdx] = texRef; if (saveAtlasesAsAssets) { editorMethods.SaveTextureArrayToAssetDatabase(textureArrays[propIdx], textureArrayFormatSet.GetFormatForProperty(texPropertyNames[propIdx].name), bakedMatsAndSlicesResMat.slices[0].texPropertyNames[propIdx], propIdx, resMaterial.combinedMaterial); } } } } } // end vist format set resMaterial.textureProperties = new List(); foreach (MB_TexArrayForProperty val in resTexArraysByProperty.Values) { resMaterial.textureProperties.Add(val); } } catch (Exception e) { Debug.LogError(e.Message + "\n" + e.StackTrace.ToString()); coroutineResult.isFinished = true; coroutineResult.success = false; } finally { editorMethods.RestoreReadFlagsAndFormats(progressInfo); combiner._destroyAllTemporaryTextures(); for (int i = 0; i < temporaryTextureAssets.Count; i++) { editorMethods.DestroyAsset(temporaryTextureAssets[i]); } temporaryTextureAssets.Clear(); } } } }