An in-development project I’m a part of necessitates dynamic manipulation of lightmap textures. Currently this is being handled by using CustomRenderTextures and setting the unity_Lightmap shader property to them on individual renderers. This however is unwieldy to use, and complicates operations such as raycasting into the lightmap.
I managed to throw together a (very dangerous) prototype that can forcefully store any texture type in the lightmaps array, and with the exception of erroring or crashing when the engine tries to clean up the texture instance it actually works perfectly fine (the textures even preview correctly in the lightmap inspector!).
using System;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using UnityEngine;
using Object = UnityEngine.Object;
class ConvertLightmapsToRenderTextures
{
[StructLayout(LayoutKind.Sequential)]
class UnityObject
{
public unsafe NativeUnityObject* CachedPtr;
// Partial version of the internal C++ Unity object layout.
// Due to being partial, this is only safe to use by reference!
public unsafe struct NativeUnityObject
{
static readonly BitVector32.Section s_MemLabelIdentifier = BitVector32.CreateSection(1 << 11);
static readonly BitVector32.Section s_TemporaryFlags = BitVector32.CreateSection(1 << 0, s_MemLabelIdentifier);
static readonly BitVector32.Section s_HideFlags = BitVector32.CreateSection(1 << 6, s_TemporaryFlags);
static readonly BitVector32.Section s_IsPersistent = BitVector32.CreateSection(1 << 0, s_HideFlags);
static readonly BitVector32.Section s_CachedTypeIndex = BitVector32.CreateSection(1 << 10, s_IsPersistent);
public IntPtr* MethodTable;
public int InstanceID;
public BitVector32 BitFields;
public RuntimeTypeIndex CachedTypeIndex
{
get => (RuntimeTypeIndex)BitFields[s_CachedTypeIndex];
set => BitFields[s_CachedTypeIndex] = (int)value;
}
}
}
enum RuntimeTypeIndex : uint
{
WebCamTexture = 239,
RenderTexture = 244,
CustomRenderTexture = 245,
SparseTexture = 246,
Texture2D = 247,
Cubemap = 248,
Texture2DArray = 249,
Texture3D = 250,
}
static unsafe void ChangeTypeIndex(Object obj, RuntimeTypeIndex index)
{
Unsafe.As<UnityObject>(obj).CachedPtr->CachedTypeIndex = index;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Execute()
{
var lightmaps = LightmapSettings.lightmaps;
for (int i = 0; i < lightmaps.Length; i++)
{
// Create a RenderTexture clone of the lightmap, as an example.
var lightmap = lightmaps[i].lightmapColor;
var renderTexture = new RenderTexture(lightmap.width, lightmap.height, 0);
Graphics.Blit(lightmap, renderTexture);
// Trick engine into thinking that the RenderTexture is a Texture2D.
ChangeTypeIndex(renderTexture, RuntimeTypeIndex.Texture2D);
lightmaps[i].lightmapColor = Unsafe.As<Texture2D>(renderTexture);
}
// Ensure original type indices are restored before the game quits, this is presumably
// because the engine calls the Texture2D cleanup function on each lightmap despite
// them not actually being Texture2D objects. It's also possible that this is the result
// of C++ virtual call optimization if we assume the engine stores them as Texture2D fields.
Application.quitting += () =>
{
for (int i = 0; i < lightmaps.Length; i++)
{
var lightmap = lightmaps[i].lightmapColor;
// If this lightmap is not actually a Texture2D, then
// its type index must be restored to prevent a crash.
switch (lightmap.GetType())
{
case Type type when type == typeof(WebCamTexture):
ChangeTypeIndex(lightmap, RuntimeTypeIndex.WebCamTexture);
break;
case Type type when type == typeof(RenderTexture):
ChangeTypeIndex(lightmap, RuntimeTypeIndex.RenderTexture);
break;
case Type type when type == typeof(CustomRenderTexture):
ChangeTypeIndex(lightmap, RuntimeTypeIndex.CustomRenderTexture);
break;
case Type type when type == typeof(SparseTexture):
ChangeTypeIndex(lightmap, RuntimeTypeIndex.SparseTexture);
break;
case Type type when type == typeof(Cubemap):
ChangeTypeIndex(lightmap, RuntimeTypeIndex.Cubemap);
break;
case Type type when type == typeof(Texture2DArray):
ChangeTypeIndex(lightmap, RuntimeTypeIndex.Texture2DArray);
break;
case Type type when type == typeof(Texture3D):
ChangeTypeIndex(lightmap, RuntimeTypeIndex.Texture3D);
break;
}
}
};
LightmapSettings.lightmaps = lightmaps;
}
}
(I’m almost hoping that the sheer hackiness of this workaround will convince somebody at Unity to take a look at implementing this properly in the engine )
The problem is that I absolutely do not want to rely on a hack like this, as it’s reliant on implementation details within the engine (and would break if, for example, the internal layout of the Object class changed).
Would it be possible for this functionality to be added to the engine within the Unity 2021 (or even 2020) release cycle? Perhaps via the addition of APIs along the lines of:
public sealed partial class LightmapData
{
public Texture polymorphicColor { get; set; }
public Texture polymorphicDir { get; set; }
public Texture polymorphicShadowMask { get; set; }
}
The Texture2D property variants could either throw or return null if another texture kind is used.