0
$\begingroup$

I have a set of models in Blender with materials that are mostly metallic. There is a hi-poly and a low-poly version of each model. I need to transfer these models over to Unity for the project I am working on. I plan to use a standard URP/Lit shader in Unity for these models.

I have unwrapped the low-poly models as a group so that they can each share the same material maps. I know how to bake the Diffuse and Normal maps for these models. I know I can bake out the metallic and roughness maps in greyscale using an emit node, but this would give me two RGB maps.

Per Unity's Reference Manual, Unity wants Metallic in the Red channel, and Smoothness (inverted roughness) in the Alpha channel. How do I get Blender to bake a map like this (without purchasing addons or third-party software)?

Given the popularity of Unity, I would expect there to be some good explanations on how to do this. I spent several hours trying to lookup how to create this kind of map, but I came up empty.

I could probably write a script in Unity to insert greyscale maps into individual channels on a new map (and optionally invert them), but I would prefer to use built in methods in either Blender or Unity.

Any help would be much appreciated.

$\endgroup$
0
$\begingroup$

I ended up writing a custom Unity script to convert maps generated in Blender. I will share the script below.

With regards to generating the individual maps. The Smoothness map is fairly straight forward. I generated a Roughness map, then inverted it using my script. The Metallic map is a little more of a hassle. Blender does not appear to have a bake option for it. The closest is a Glossy option, which seems to bake shadows into it. The only useful advise I found online was to copy the metallic values over to a field that you can bake out like the Roughness, then bake out the "Roughness" and call it the Metallic map. This worked for my needs. Just remember to revert the Blender file back to what it was before setting up for the metallic bake.

Here is the Unity script I wrote that will merge the individual maps into a single map and invert the maps that need inverting. Feel free to use/modify this code for your own needs:

public class TextureChannelPacker : EditorWindow
{
    private Texture2D redTexture,
                    greenTexture,
                    blueTexture,
                    alphaTexture;

    private bool redInvert,
                 greenInvert,
                 blueInvert,
                 alphaInvert;

    private channel redChannel = channel.Red,
                    greenChannel = channel.Green,
                    blueChannel = channel.Blue,
                    alphaChannel = channel.Alpha;


    private enum channel
    {
        Red,
        Green,
        Blue,
        Alpha,
        RGB
    }

    private GUILayoutOption channelLabelWidth, 
                            channelTextureWidth;

    private float labelWidth = 80f;
    

    [MenuItem("Edit/Texture Channel Packer")]
    [MenuItem("Custom Tools/Texture Channel Packer")]
    public static void ShowWindow()
    {
        EditorWindow.GetWindow(typeof(TextureChannelPacker));
    }

    void OnGUI()
    {
        UpdateLayoutWidth();

        CreateChannelHeaderRow();
        CreateChannelRow("Red", ref redTexture, ref redChannel, ref redInvert);
        CreateChannelRow("Green", ref greenTexture, ref greenChannel, ref greenInvert);
        CreateChannelRow("Blue", ref blueTexture, ref blueChannel, ref blueInvert);
        CreateChannelRow("Alpha", ref alphaTexture, ref alphaChannel, ref alphaInvert);

        CreateSaveButton();
    }

    private void SaveToFile()
    {
        var path = EditorUtility.SaveFilePanel(
            "Save Composite Texture as PNG",
            "",
            "New Composite Texture.png",
            "png");

        Texture2D texture = CreateNewTexture2D();

        CompositeTextures(ref texture);

        if (path.Length != 0)
        {
            var pngData = texture.EncodeToPNG();
            if (pngData != null)
                File.WriteAllBytes(path, pngData);
            else
                Debug.Log("Unable to save, no texture data to save.");
        }
    }

    private bool AreTexturesReadable()
    {
        bool result = true;
        string errorText = "Please set 'Read/Write Enabled' to 'true' on the following textures:";

        if (redTexture)
            if (!redTexture.isReadable)
            {
                errorText += "\n\t" + redTexture.name.ToString();
                result = false;
            }
        if (greenTexture)
            if (!greenTexture.isReadable)
            {
                errorText += "\n\t" + greenTexture.name.ToString();
                result = false;
            }
        if (blueTexture)
            if (!blueTexture.isReadable)
            {
                errorText += "\n\t" + blueTexture.name.ToString();
                result = false;
            }
        if (alphaTexture)
            if (!alphaTexture.isReadable)
            {
                errorText += "\n\t" + alphaTexture.name.ToString(); 
                result = false;
            }

        if (result) return true;

        Debug.Log(errorText);
        EditorUtility.DisplayDialog("Error", errorText, "Okay");

        return false;
    }

    private void CompositeTextures(ref Texture2D compositeTexture)
    {
        float u, v;

        for (int x = 0; x < compositeTexture.width; x++)
        {
            u = (float)x / (float)compositeTexture.width;
            
            for(int y = 0; y < compositeTexture.height; y++)
            {
                v = (float)y / (float)compositeTexture.height;

                Color pixelColor = new Color(GetColorForChannel(redTexture, redChannel, redInvert, u, v),
                                             GetColorForChannel(greenTexture, greenChannel, greenInvert, u, v),
                                             GetColorForChannel(blueTexture, blueChannel, blueInvert, u, v),
                                             GetColorForChannel(alphaTexture, alphaChannel, alphaInvert, u, v, true));

                compositeTexture.SetPixel(x, y, pixelColor);
            }

            EditorUtility.DisplayProgressBar("Compositing Texture...",
                                             "Progress: " + Mathf.Round(u * 1000) / 10 + "%",
                                             u);
        }

        compositeTexture.Apply();
        EditorUtility.ClearProgressBar();
    }

    private float GetColorForChannel(Texture2D texture, channel textureChannel, bool invert, float u, float v, bool isAlphaChannel = false)
    {
        if (!texture) 
        {
            if (isAlphaChannel)
                return 1f;
            else
                return 0f;
        }

        int x = Mathf.FloorToInt(texture.width * u),
            y = Mathf.FloorToInt(texture.height * v);

        Color color = texture.GetPixel(x, y);

        float value = 0f;

        switch(textureChannel)
        {
            case channel.Red:
                value = color.r;
                break;
            case channel.Green:
                value = color.g;
                break;
            case channel.Blue:
                value = color.b;
                break;
            case channel.Alpha:
                value = color.a;
                break;
            case channel.RGB:
                value = (color.r + color.g + color.b) / 3f; // average value of channels
                break;
        }

        if (invert) value = 1 - value;

        return Mathf.Clamp01(value);
    }

    private Texture2D CreateNewTexture2D()
    {
        Vector2Int size = new Vector2Int();

        if (redTexture != null)
        {
            if (size.x < redTexture.width) size.x = redTexture.width;
            if (size.y < redTexture.height) size.y = redTexture.height;
        }

        if (greenTexture != null)
        {
            if (size.x < greenTexture.width) size.x = greenTexture.width;
            if (size.y < greenTexture.height) size.y = greenTexture.height;
        }

        if (blueTexture != null)
        {
            if (size.x < blueTexture.width) size.x = blueTexture.width;
            if (size.y < blueTexture.height) size.y = blueTexture.height;
        }

        if (alphaTexture != null)
        {
            if (size.x < alphaTexture.width) size.x = alphaTexture.width;
            if (size.y < alphaTexture.height) size.y = alphaTexture.height;
        }

        return new Texture2D(size.x, size.y);

    }

    private bool WasATextureProvided()
    {
        bool result = redTexture ||
                      greenTexture ||
                      blueTexture ||
                      alphaTexture;

        if(!result)
        {
            Debug.LogWarning("Unable to Save. No input textures selected.");
            EditorUtility.DisplayDialog("Error", "Unable to Save. No input textures selected.", "Okay");
        }

        return result;
    }

    private void UpdateLayoutWidth()
    {
        float remainingWidth = EditorGUIUtility.currentViewWidth;

        remainingWidth -= labelWidth;
        channelLabelWidth = GUILayout.Width(labelWidth);

        remainingWidth /= 2f;
        channelTextureWidth = GUILayout.Width(remainingWidth);
    }

    private void CreateChannelHeaderRow()
    {
        EditorGUILayout.BeginHorizontal();

        EditorGUILayout.BeginVertical(channelLabelWidth);
        EditorGUILayout.LabelField("Channel", channelLabelWidth);
        EditorGUILayout.EndVertical();

        EditorGUILayout.BeginVertical(channelTextureWidth);
        EditorGUILayout.LabelField("Input Texture");
        EditorGUILayout.EndVertical();

        EditorGUILayout.BeginVertical();
        EditorGUILayout.LabelField("Settings");
        EditorGUILayout.EndVertical();

        EditorGUILayout.EndHorizontal();
    }

    private void CreateChannelRow(string label, ref Texture2D inputTexture, ref channel channel, ref bool invert)
    {
        
        EditorGUILayout.BeginHorizontal();

        EditorGUILayout.BeginVertical(channelLabelWidth);
        EditorGUILayout.LabelField(label, channelLabelWidth);
        EditorGUILayout.EndVertical();

        EditorGUILayout.BeginVertical(channelTextureWidth);
        inputTexture = (Texture2D)EditorGUILayout.ObjectField(inputTexture, typeof(Texture), false);
        EditorGUILayout.EndVertical();

        EditorGUILayout.BeginVertical();
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("Extract:", GUILayout.MaxWidth(60f));
        channel = (channel)EditorGUILayout.EnumPopup(channel);
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("Invert:", GUILayout.MaxWidth(60f));
        invert = EditorGUILayout.Toggle(invert);
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.EndVertical();
        EditorGUILayout.EndHorizontal();
    }

    private void CreateSaveButton()
    {
        EditorGUILayout.BeginHorizontal();
        GUILayout.FlexibleSpace();
        if (GUILayout.Button("Save to file"))
        {
            if (WasATextureProvided() && AreTexturesReadable()) 
                SaveToFile();
        }
        GUILayout.FlexibleSpace();
        EditorGUILayout.EndHorizontal();
    }
}
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.