Is It Possible to Combine Meshes and Bake Lighting in One Pass?

Hi everyone,
I’m working on a project in Unity (version 2021.3.2f1, Built-in Render Pipeline) and want to optimize my scene by combining multiple static meshes into a single mesh. At the same time, I’d also like to bake the lighting for these combined objects in one unified process if possible.

My goal is to avoid doing two separate steps—combining the meshes first and then rebaking (or vice versa). Ideally, I want a workflow or tool that can both merge the meshes (for reduced draw calls) and also generate correct lightmaps (or maintain existing lightmaps) in a single pass.

I’ve considered using “Mesh Baker” or writing a custom script, but I’m not sure if there’s an established best practice or recommended approach for simultaneous mesh combining and lightmap baking. Has anyone successfully done this? Do you have any tips, tutorials, or documentation references?

Additionally, if simultaneous baking isn’t practical, what’s the standard pipeline? Should I combine the meshes first and then bake the lighting, or bake the lighting and adjust the UVs afterwards? Any guidance would be greatly appreciated. Thanks in advance!

There is a mesh combiner in the asset store already.
Suppose you could put that into an editor window script,
with a list of objects to combine in the layout.
Then just have that list execute on run. (See below)

As for baking at runtime, the consensus is that Unity has
no scriptable runtime light baking. Some suggest UE4, but
that is a stretch, and likely also no.

So I asked brave a.i to help me generate a runtime shader
that outputs a light bake to folder.


Here is a sample script for your mesh combining…
(Not tested)

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.IO;

public class MeshCombiner : EditorWindow
{
    // List of objects to combine
    public List<GameObject> objectsToCombine = new List<GameObject>();

    // Output folder path
    public string outputFolderPath = "";

    // Button to combine meshes
    [MenuItem("Window/Mesh Combiner")]
    public static void ShowWindow()
    {
        GetWindow<MeshCombiner>("Mesh Combiner");
    }

    private void OnGUI()
    {
        // Objects to combine list
        EditorGUILayout.LabelField("Objects to Combine:");
        objectsToCombine = (List<GameObject>)EditorGUILayout.PropertyField(objectsToCombine, true);

        // Output folder path
        EditorGUILayout.LabelField("Output Folder:");
        outputFolderPath = EditorGUILayout.TextField(outputFolderPath);

        // Button to select output folder
        if (GUILayout.Button("Select Output Folder"))
        {
            outputFolderPath = EditorUtility.OpenFolderPanel("Select Output Folder", outputFolderPath, "");
        }

        // Button to combine meshes
        if (GUILayout.Button("Combine Meshes"))
        {
            CombineMeshes();
        }
    }

    private void CombineMeshes()
    {
        // Check if output folder path is valid
        if (string.IsNullOrEmpty(outputFolderPath))
        {
            Debug.LogError("Output folder path is invalid.");
            return;
        }

        // Create output folder if it doesn't exist
        if (!Directory.Exists(outputFolderPath))
        {
            Directory.CreateDirectory(outputFolderPath);
        }

        // Combine meshes
        Mesh combinedMesh = new Mesh();
        foreach (GameObject obj in objectsToCombine)
        {
            MeshFilter meshFilter = obj.GetComponent<MeshFilter>();
            if (meshFilter != null)
            {
                Mesh mesh = meshFilter.mesh;
                combinedMesh.CombineMeshes(new Mesh[] { mesh });
            }
        }

        // Save combined mesh to file
        string filePath = Path.Combine(outputFolderPath, "CombinedMesh.obj");
        MeshExporter.ExportMesh(combinedMesh, filePath);

        Debug.Log("Meshes combined and saved to " + filePath);
    }
}

// Helper class to export mesh to file
public static class MeshExporter
{
    public static void ExportMesh(Mesh mesh, string filePath)
    {
        using (StreamWriter writer = new StreamWriter(filePath))
        {
            // Write header
            writer.WriteLine("# OBJ file generated by MeshCombiner");

            // Write vertices
            foreach (Vector3 vertex in mesh.vertices)
            {
                writer.WriteLine("v " + vertex.x + " " + vertex.y + " " + vertex.z);
            }

            // Write triangles
            int[] triangles = mesh.triangles;
            for (int i = 0; i < triangles.Length; i += 3)
            {
                writer.WriteLine("f " + (triangles[i] + 1) + " " + (triangles[i + 1] + 1) + " " + (triangles[i + 2] + 1));
            }
        }
    }
}

Here is a sample script of a shader that bakes lighting at runtime to an output folder

Shader "Custom/LightBakeShader" {
    Properties {
        _MainTex ("Albedo (RGBA)", 2D) = "white" {}
        _BakeFolder ("Bake Folder", String) = "Assets/LightBake/"
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // Calculate lighting
                fixed4 col = tex2D(_MainTex, i.normal.xy * _MainTex_ST.xy + _MainTex_ST.zw);
                float3 lightDir = _WorldSpaceLightPos0.xyz;
                float3 lightColor = _LightColor0.rgb;
                float NdotL = max(0, dot(i.normal, lightDir));
                col.rgb *= lightColor * NdotL;

                // Bake lighting information to texture
                #ifdef UNITY_EDITOR
                string bakeFolder = _BakeFolder;
                string fileName = "LightBake_" + UnityEditor.AssetDatabase.GetAssetPath(UnityEditor.Selection.activeObject).Split('/').Last() + ".png";
                string filePath = bakeFolder + fileName;
                #endif

                // Output to screen
                return col;
            }
            ENDCG
        }

        // Custom pass to bake lighting information to texture
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // Get the current frame's render texture
                Texture2D renderTexture = (Texture2D)UnityEditor.RenderTexture.active;

                // Bake lighting information to texture
                #ifdef UNITY_EDITOR
                string bakeFolder = _BakeFolder;
                string fileName = "LightBake_" + UnityEditor.AssetDatabase.GetAssetPath(UnityEditor.Selection.activeObject).Split('/').Last() + ".png";
                string filePath = bakeFolder + fileName;
                System.IO.File.WriteAllBytes(filePath, renderTexture.EncodeToPNG());
                #endif

                // Output black color
                return fixed4(0, 0, 0, 1);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Hope that helps.
Good luck with your project!