Solved!
Turns out it wasn’t as trivial as I first thought. And the above example is flawed by assuming that all static mesh renderer offsets don’t change when baking the various lightmaps - which only works for simple scenes. However in a more complicated scene where enabling/disabling static mesh objects (like lights for “bright” and “dark” lightmaps) occurs then the renderers offsets may change making lightmaps not always line up correctly.
I have created this solution which works pretty well (based on Laurenth’s example) and should even transfer lightprobes! However it differs from hers by taking the currently loaded lightmap and copies its textures to a specified Resources folder. It also creates a .Json file in that folder which contains all of the renderer offsets and lightprobe coefficients, as well as references to the textures. The data in the .Json file is used to correctly align the lightmap to the corresponding mesh renderers - all on demand!
There is no limit to the number of lightmaps you can create for a single scene. But I recommend using a good naming convention for the Resources folder names.
NOTE: Static Batching - I found that if I enable Project Settings>Player>Static Batching then the lightmap takes on different offsets at runtime causing misalignment. I have disabled static batching in my project, If I figure out how to compensate for the batch offsets I will update the example with a fix.
To use this tool:
-
Place script somewhere in Assets and place the editor script in the Editor folder.
-
Attach ChangeLightmap script to an empty gameobject in the scene.
-
Add and bake some lightning in the scene (daytime lighting).
-
Change the “resourceFolder” directory name field to something unique (eg. LightMapData_Bright).
-
Press “Save” button to create a Resources folder and all the data files.
-
Adjust your lights a little and bake scene lightning again (nighttime lighting).
-
Change “resourceFolder” name field to another unique directory name(eg. LightMapData_Dark).
-
Press “Save” button again to create the second folder and lightmap data files.
-
Finally you may type any of those unique directory names in the name field then press “Load” button to switch between lightmaps!!
Lightmap Loader/Saver Tool:

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using System;
using System.IO;
using System.Collections.Generic;
public class ChangeLightmap : MonoBehaviour
{
private string jsonFileName = "mapconfig.txt"; // Name of the json data file.
[SerializeField] private string m_resourceFolder = "LightMapData_1";
public string resourceFolder {get { return m_resourceFolder; }}
private string absoluteName;
[System.Serializable]
private class SphericalHarmonics
{
public float[] coefficients = new float[27];
}
[System.Serializable]
private class RendererInfo
{
public Renderer renderer;
public int lightmapIndex;
public Vector4 lightmapOffsetScale;
}
[System.Serializable]
private class LightingScenarioData
{
public RendererInfo[] rendererInfos;
public Texture2D[] lightmaps;
public Texture2D[] lightmapsDir;
public Texture2D[] lightmapsShadow;
public LightmapsMode lightmapsMode;
public SphericalHarmonics[] lightProbes;
}
[SerializeField]
private LightingScenarioData lightingScenariosData;
[SerializeField] private bool loadOnAwake = false; // Load the selected lighmap when this script wakes up (aka when game starts).
//TODO : enable logs only when verbose enabled
[SerializeField] private bool verbose = false;
public string GetResourcesDirectory (string dir)
{
return Application.dataPath + "/Resources/"+dir+"/"; // The directory where the lightmap data resides.
}
public bool CheckResourcesDirectoryExists (string dir)
{
return Directory.Exists (GetResourcesDirectory(dir));
}
private void CreateResourcesDirectory(string dir) {
if (!CheckResourcesDirectoryExists (m_resourceFolder))
{
Directory.CreateDirectory (GetResourcesDirectory(dir));
}
}
public void Load (string folderName)
{
m_resourceFolder = folderName;
Load ();
}
public void Load ()
{
lightingScenariosData = LoadJsonData ();
var newLightmaps = new LightmapData[lightingScenariosData.lightmaps.Length];
for (int i = 0; i < newLightmaps.Length; i++)
{
newLightmaps[i] = new LightmapData();
newLightmaps[i].lightmapLight = Resources.Load<Texture2D>(m_resourceFolder+"/" + lightingScenariosData.lightmaps[i].name);
if (lightingScenariosData.lightmapsMode != LightmapsMode.NonDirectional)
{
newLightmaps [i].lightmapDir = Resources.Load<Texture2D>(m_resourceFolder+"/" + lightingScenariosData.lightmapsDir[i].name);
if (lightingScenariosData.lightmapsShadow [i] != null) { // If the textuer existed and was set in the data file.
newLightmaps [i].shadowMask = Resources.Load<Texture2D> (m_resourceFolder + "/" + lightingScenariosData.lightmapsShadow [i].name);
}
}
}
LoadLightProbes();
ApplyRendererInfo(lightingScenariosData.rendererInfos);
LightmapSettings.lightmaps = newLightmaps;
}
private void LoadLightProbes()
{
var sphericalHarmonicsArray = new SphericalHarmonicsL2[lightingScenariosData.lightProbes.Length];
for (int i = 0; i < lightingScenariosData.lightProbes.Length; i++)
{
var sphericalHarmonics = new SphericalHarmonicsL2();
// j is coefficient
for (int j = 0; j < 3; j++)
{
//k is channel ( r g b )
for (int k = 0; k < 9; k++)
{
sphericalHarmonics[j, k] = lightingScenariosData.lightProbes[i].coefficients[j * 9 + k];
}
}
sphericalHarmonicsArray[i] = sphericalHarmonics;
}
try
{
LightmapSettings.lightProbes.bakedProbes = sphericalHarmonicsArray;
}
catch { Debug.LogWarning("Warning, error when trying to load lightprobes for scenario "); }
}
private void ApplyRendererInfo (RendererInfo[] infos)
{
try
{
for (int i = 0; i < infos.Length; i++)
{
var info = infos[i];
info.renderer.lightmapIndex = infos[i].lightmapIndex;
if (!info.renderer.isPartOfStaticBatch)
{
info.renderer.lightmapScaleOffset = infos[i].lightmapOffsetScale;
}
if (info.renderer.isPartOfStaticBatch && verbose == true)
{
Debug.Log("Object " + info.renderer.gameObject.name + " is part of static batch, skipping lightmap offset and scale.");
}
}
}
catch (Exception e)
{
Debug.LogError("Error in ApplyRendererInfo:" + e.GetType().ToString());
}
}
public static ChangeLightmap instance;
private void Awake ()
{
if (instance != null)
{ // Singleton pattern.
Destroy(gameObject);
}else{
instance = this;
}
if (loadOnAwake) {
Load ();
}
}
private void WriteJsonFile (string path, string json)
{
absoluteName = path + jsonFileName;
File.WriteAllText (absoluteName, json); // Write all the data to the file.
}
private string GetJsonFile (string f)
{
if (!File.Exists(f))
{
return "";
}
return File.ReadAllText (f); // Write all the data to the file.
}
private LightingScenarioData LoadJsonData ()
{
absoluteName = GetResourcesDirectory(m_resourceFolder) + jsonFileName;
string json = GetJsonFile (absoluteName);
return lightingScenariosData = JsonUtility.FromJson<LightingScenarioData> (json);
}
public void GenerateLightmapInfoStore ()
{
lightingScenariosData = new LightingScenarioData ();
var newRendererInfos = new List<RendererInfo>();
var newLightmapsTextures = new List<Texture2D>();
var newLightmapsTexturesDir = new List<Texture2D>();
var newLightmapsTexturesShadow = new List<Texture2D>();
var newLightmapsMode = new LightmapsMode();
var newSphericalHarmonicsList = new List<SphericalHarmonics>();
newLightmapsMode = LightmapSettings.lightmapsMode;
var renderers = FindObjectsOfType(typeof(MeshRenderer));
Debug.Log("stored info for "+renderers.Length+" meshrenderers");
foreach (MeshRenderer renderer in renderers)
{
if (renderer.lightmapIndex != -1)
{
RendererInfo info = new RendererInfo();
info.renderer = renderer;
info.lightmapOffsetScale = renderer.lightmapScaleOffset;
Texture2D lightmaplight = LightmapSettings.lightmaps[renderer.lightmapIndex].lightmapLight;
info.lightmapIndex = newLightmapsTextures.IndexOf(lightmaplight);
if (info.lightmapIndex == -1)
{
info.lightmapIndex = newLightmapsTextures.Count;
newLightmapsTextures.Add(lightmaplight);
}
if (newLightmapsMode != LightmapsMode.NonDirectional)
{
//first directional lighting
Texture2D lightmapdir = LightmapSettings.lightmaps[renderer.lightmapIndex].lightmapDir;
info.lightmapIndex = newLightmapsTexturesDir.IndexOf(lightmapdir);
if (info.lightmapIndex == -1)
{
info.lightmapIndex = newLightmapsTexturesDir.Count;
newLightmapsTexturesDir.Add(lightmapdir);
}
//now the shadowmask
Texture2D lightmapshadow = LightmapSettings.lightmaps[renderer.lightmapIndex].shadowMask;
info.lightmapIndex = newLightmapsTexturesShadow.IndexOf(lightmapshadow);
if (info.lightmapIndex == -1)
{
info.lightmapIndex = newLightmapsTexturesShadow.Count;
newLightmapsTexturesShadow.Add(lightmapshadow);
}
}
newRendererInfos.Add(info);
}
}
lightingScenariosData.lightmapsMode = newLightmapsMode;
lightingScenariosData.lightmaps = newLightmapsTextures.ToArray();
if (newLightmapsMode != LightmapsMode.NonDirectional)
{
lightingScenariosData.lightmapsDir = newLightmapsTexturesDir.ToArray();
lightingScenariosData.lightmapsShadow = newLightmapsTexturesShadow.ToArray();
}
lightingScenariosData.rendererInfos = newRendererInfos.ToArray();
var scene_LightProbes = new SphericalHarmonicsL2[LightmapSettings.lightProbes.bakedProbes.Length];
scene_LightProbes = LightmapSettings.lightProbes.bakedProbes;
for (int i = 0; i < scene_LightProbes.Length; i++)
{
var SHCoeff = new SphericalHarmonics();
// j is coefficient
for (int j = 0; j < 3; j++)
{
//k is channel ( r g b )
for (int k = 0; k < 9; k++)
{
SHCoeff.coefficients[j*9+k] = scene_LightProbes[i][j, k];
}
}
newSphericalHarmonicsList.Add(SHCoeff);
}
lightingScenariosData.lightProbes = newSphericalHarmonicsList.ToArray ();
// write the files and map config data.
CreateResourcesDirectory (m_resourceFolder);
string resourcesDir = GetResourcesDirectory(m_resourceFolder);
CopyTextureToResources (resourcesDir, lightingScenariosData.lightmaps);
CopyTextureToResources (resourcesDir, lightingScenariosData.lightmapsDir);
CopyTextureToResources (resourcesDir, lightingScenariosData.lightmapsShadow);
string json = JsonUtility.ToJson(lightingScenariosData);
WriteJsonFile (resourcesDir, json);
}
private void CopyTextureToResources (string toPath, Texture2D[] textures)
{
for (int i = 0; i < textures.Length; i++)
{
Texture2D texture = textures [i];
if (texture != null) // Maybe the optional shadowmask didn't exist?
{
FileUtil.ReplaceFile (
AssetDatabase.GetAssetPath (texture),
toPath + Path.GetFileName (AssetDatabase.GetAssetPath (texture))
);
AssetDatabase.Refresh (); // Refresh so the newTexture file can be found and loaded.
Texture2D newTexture = Resources.Load<Texture2D> (m_resourceFolder + "/" + texture.name); // Load the new texture as an object.
CopyTextureImporterProperties (textures [i], newTexture); // Ensure new texture takes on same properties as origional texture.
AssetDatabase.ImportAsset( AssetDatabase.GetAssetPath( newTexture ) ); // Re-import texture file so it will be successfully compressed to desired format.
EditorUtility.CompressTexture (newTexture, textures[i].format, TextureCompressionQuality.Best); // Now compress the texture.
textures [i] = newTexture; // Set the new texture as the reference in the Json file.
}
}
}
private void CopyTextureImporterProperties (Texture2D fromTexture, Texture2D toTexture)
{
TextureImporter fromTextureImporter = GetTextureImporter (fromTexture);
TextureImporter toTextureImporter = GetTextureImporter (toTexture);
toTextureImporter.wrapMode = fromTextureImporter.wrapMode;
toTextureImporter.anisoLevel = fromTextureImporter.anisoLevel;
toTextureImporter.sRGBTexture = fromTextureImporter.sRGBTexture;
toTextureImporter.textureType = fromTextureImporter.textureType;
toTextureImporter.textureCompression = fromTextureImporter.textureCompression;
}
private TextureImporter GetTextureImporter (Texture2D texture)
{
string newTexturePath = AssetDatabase.GetAssetPath (texture);
TextureImporter importer = AssetImporter.GetAtPath (newTexturePath) as TextureImporter;
return importer;
}
}
Editor script (presents the Load and Save buttons for the inspector):
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(ChangeLightmap))]
public class ChangeLightmapInspector : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI ();
ChangeLightmap lightmapData = (ChangeLightmap)target;
if (GUILayout.Button("Load"))
{
lightmapData.Load ();
}
if (GUILayout.Button("Save"))
{
if (lightmapData.CheckResourcesDirectoryExists (lightmapData.resourceFolder)) {
if (!EditorUtility.DisplayDialog ("Overwrite Lightmap Resources?", "Lighmap Resources folder with name: \"" + lightmapData.resourceFolder + "\" already exists.\n\nPress OK to overwrite existing lightmap data.", "OK", "Cancel")) {
return;
}
} else {
if (!EditorUtility.DisplayDialog ("Create Lightmap Resources?", "Create new lighmap Resources folder: \"" + lightmapData.resourceFolder + "?", "OK", "Cancel")) {
return;
}
}
lightmapData.GenerateLightmapInfoStore ();
}
}
}
If you find these scripts useful - please give this post a like.