// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.IO; using UnityEditor; using UnityEngine; namespace Microsoft.MixedReality.GraphicsTools.Editor { /// /// To improve parity with the Unity Standard shader per pixel metallic, smoothness, emissive, and occlusion values can /// all be controlled via channel packing. The TextureCombinerWindow is used to generate channel map textures without /// having to combine textures by hand. /// /// This window can be automatically filled out by selecting a Unity Standard shader and clicking "Autopopulate from Standard Material." /// /// Or, you can manually specify a texture (or constant value) per red, green, blue, or alpha channel. The texture /// combination is GPU accelerated and does not require the input texture to be CPU accessible. /// public class TextureCombinerWindow : EditorWindow { private enum Channel { Red = 0, Green = 1, Blue = 2, Alpha = 3, RGBAverage = 4 } private const float defaultUniformValue = -0.01f; private Texture2D metallicMap; private Channel metallicMapChannel = Channel.Red; private float metallicUniform = defaultUniformValue; private Texture2D occlusionMap; private Channel occlusionMapChannel = Channel.Green; private float occlusionUniform = defaultUniformValue; private Texture2D emissionMap; private Channel emissionMapChannel = Channel.RGBAverage; private float emissionUniform = defaultUniformValue; private Texture2D smoothnessMap; private Channel smoothnessMapChannel = Channel.Alpha; private float smoothnessUniform = defaultUniformValue; private Material standardMaterial; private TextureFile.Format textureFormat = TextureFile.Format.TGA; private const string StandardShaderName = "Standard"; private const string StandardRoughnessShaderName = "Standard (Roughness setup)"; private const string StandardSpecularShaderName = "Standard (Specular setup)"; [MenuItem("Window/Graphics Tools/Texture Combiner")] private static void ShowWindow() { TextureCombinerWindow window = GetWindow(); window.titleContent = new GUIContent("Texture Combiner"); window.minSize = new Vector2(380.0f, 700.0f); window.Show(); } private void OnGUI() { GUILayout.Label("Import", EditorStyles.boldLabel); GUI.enabled = metallicUniform < 0.0f; metallicMap = (Texture2D)EditorGUILayout.ObjectField("Metallic Map", metallicMap, typeof(Texture2D), false); metallicMapChannel = (Channel)EditorGUILayout.EnumPopup("Input Channel", metallicMapChannel); GUI.enabled = true; metallicUniform = EditorGUILayout.Slider(new GUIContent("Metallic Uniform"), metallicUniform, defaultUniformValue, 1.0f); GUILayout.Box("Output Channel: Red", EditorStyles.helpBox, System.Array.Empty()); EditorGUILayout.Separator(); GUI.enabled = occlusionUniform < 0.0f; occlusionMap = (Texture2D)EditorGUILayout.ObjectField("Occlusion Map", occlusionMap, typeof(Texture2D), false); occlusionMapChannel = (Channel)EditorGUILayout.EnumPopup("Input Channel", occlusionMapChannel); GUI.enabled = true; occlusionUniform = EditorGUILayout.Slider(new GUIContent("Occlusion Uniform"), occlusionUniform, defaultUniformValue, 1.0f); GUILayout.Box("Output Channel: Green", EditorStyles.helpBox, System.Array.Empty()); EditorGUILayout.Separator(); GUI.enabled = emissionUniform < 0.0f; emissionMap = (Texture2D)EditorGUILayout.ObjectField("Emission Map", emissionMap, typeof(Texture2D), false); emissionMapChannel = (Channel)EditorGUILayout.EnumPopup("Input Channel", emissionMapChannel); GUI.enabled = true; emissionUniform = EditorGUILayout.Slider(new GUIContent("Emission Uniform"), emissionUniform, defaultUniformValue, 1.0f); GUILayout.Box("Output Channel: Blue", EditorStyles.helpBox, System.Array.Empty()); EditorGUILayout.Separator(); GUI.enabled = smoothnessUniform < 0.0f; smoothnessMap = (Texture2D)EditorGUILayout.ObjectField("Smoothness Map", smoothnessMap, typeof(Texture2D), false); smoothnessMapChannel = (Channel)EditorGUILayout.EnumPopup("Input Channel", smoothnessMapChannel); GUI.enabled = true; smoothnessUniform = EditorGUILayout.Slider(new GUIContent("Smoothness Uniform"), smoothnessUniform, defaultUniformValue, 1.0f); GUILayout.Box("Output Channel: Alpha", EditorStyles.helpBox, System.Array.Empty()); EditorGUILayout.Separator(); standardMaterial = (Material)EditorGUILayout.ObjectField("Standard Material", standardMaterial, typeof(Material), false); GUI.enabled = standardMaterial != null && IsUnityStandardMaterial(standardMaterial); if (GUILayout.Button("Auto-populate from Standard Material")) { Autopopulate(); } GUI.enabled = CanSave(); EditorGUILayout.Separator(); GUILayout.Label("Export", EditorStyles.boldLabel); textureFormat = (TextureFile.Format)EditorGUILayout.EnumPopup("Texture Format", textureFormat); if (GUILayout.Button("Save Channel Map")) { Save(); } GUILayout.Box("Metallic (Red), Occlusion (Green), Emission (Blue), Smoothness (Alpha)", EditorStyles.helpBox, System.Array.Empty()); } private void Autopopulate() { metallicUniform = defaultUniformValue; occlusionUniform = defaultUniformValue; emissionUniform = defaultUniformValue; smoothnessUniform = defaultUniformValue; occlusionMap = (Texture2D)standardMaterial.GetTexture("_OcclusionMap"); occlusionMapChannel = occlusionMap != null ? Channel.Green : occlusionMapChannel; emissionMap = (Texture2D)standardMaterial.GetTexture("_EmissionMap"); emissionMapChannel = emissionMap != null ? Channel.RGBAverage : emissionMapChannel; if (standardMaterial.shader.name == StandardShaderName) { metallicMap = (Texture2D)standardMaterial.GetTexture("_MetallicGlossMap"); metallicMapChannel = metallicMap != null ? Channel.Red : metallicMapChannel; smoothnessMap = ((int)standardMaterial.GetFloat("_SmoothnessTextureChannel") == 0) ? metallicMap : (Texture2D)standardMaterial.GetTexture("_MainTex"); smoothnessMapChannel = smoothnessMap != null ? Channel.Alpha : smoothnessMapChannel; } else if (standardMaterial.shader.name == StandardRoughnessShaderName) { metallicMap = (Texture2D)standardMaterial.GetTexture("_MetallicGlossMap"); metallicMapChannel = metallicMap != null ? Channel.Red : metallicMapChannel; smoothnessMap = (Texture2D)standardMaterial.GetTexture("_SpecGlossMap"); smoothnessMapChannel = smoothnessMap != null ? Channel.Red : smoothnessMapChannel; } else { smoothnessMap = ((int)standardMaterial.GetFloat("_SmoothnessTextureChannel") == 0) ? (Texture2D)standardMaterial.GetTexture("_SpecGlossMap") : (Texture2D)standardMaterial.GetTexture("_MainTex"); smoothnessMapChannel = smoothnessMap != null ? Channel.Alpha : smoothnessMapChannel; } } private void Save() { int width; int height; Texture[] textures = new Texture[] { metallicMap, occlusionMap, emissionMap, smoothnessMap }; CalculateChannelMapSize(textures, out width, out height); Texture2D channelMap = new Texture2D(width, height); RenderTexture renderTexture = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); // Use the GPU to pack the various texture maps into a single texture. Material channelPacker = new Material(Shader.Find("Hidden/Graphics Tools/Texture Combiner")); channelPacker.SetTexture("_MetallicMap", metallicMap); channelPacker.SetInt("_MetallicMapChannel", (int)metallicMapChannel); channelPacker.SetFloat("_MetallicUniform", metallicUniform); channelPacker.SetTexture("_OcclusionMap", occlusionMap); channelPacker.SetInt("_OcclusionMapChannel", (int)occlusionMapChannel); channelPacker.SetFloat("_OcclusionUniform", occlusionUniform); channelPacker.SetTexture("_EmissionMap", emissionMap); channelPacker.SetInt("_EmissionMapChannel", (int)emissionMapChannel); channelPacker.SetFloat("_EmissionUniform", emissionUniform); channelPacker.SetTexture("_SmoothnessMap", smoothnessMap); channelPacker.SetInt("_SmoothnessMapChannel", (int)smoothnessMapChannel); channelPacker.SetFloat("_SmoothnessUniform", smoothnessUniform); Graphics.Blit(null, renderTexture, channelPacker); DestroyImmediate(channelPacker); // Save the last render texture to a texture. RenderTexture previous = RenderTexture.active; RenderTexture.active = renderTexture; channelMap.ReadPixels(new Rect(0.0f, 0.0f, width, height), 0, 0); channelMap.Apply(); RenderTexture.active = previous; RenderTexture.ReleaseTemporary(renderTexture); // Save the texture to disk. string filename = string.Format("{0}{1}.{2}", GetChannelMapName(textures), "_Channel", TextureFile.GetExtension(textureFormat)); string path = EditorUtility.SaveFilePanel("Save Channel Map", "", filename, TextureFile.GetExtension(textureFormat)); if (path.Length != 0) { byte[] textureData = TextureFile.Encode(channelMap, textureFormat); if (textureData != null) { File.WriteAllBytes(path, textureData); Debug.LogFormat("Saved channel map to: {0}", path); AssetDatabase.Refresh(); } } } private bool CanSave() { return metallicMap != null || occlusionMap != null || emissionMap != null || smoothnessMap != null || metallicUniform >= 0.0f || occlusionUniform >= 0.0f || emissionUniform >= 0.0f || smoothnessUniform >= 0.0f; } private static bool IsUnityStandardMaterial(Material material) { if (material != null) { if (material.shader.name == StandardShaderName || material.shader.name == StandardRoughnessShaderName || material.shader.name == StandardSpecularShaderName) { return true; } } return false; } private static string GetChannelMapName(Texture[] textures) { // Use the first named texture as the channel map name. foreach (Texture texture in textures) { if (texture != null && !string.IsNullOrEmpty(texture.name)) { return texture.name; } } return string.Empty; } private static void CalculateChannelMapSize(Texture[] textures, out int width, out int height) { width = 4; height = 4; // Find the max extents of all texture maps. foreach (Texture texture in textures) { width = texture != null ? Mathf.Max(texture.width, width) : width; height = texture != null ? Mathf.Max(texture.height, height) : height; } } } }