Is it possible for LightmapGI to bake light onto RenderingServer instances?

Godot Version

v4.7.stable.mono

Question

My foliage system creates mesh instances from the RenderingServer to make use of per-instance occlusion culling. They are not part of the scene tree and thus are not included in LightmapGI bakes.
However, my instances are effectively persistent as they are generated deterministically in EnterTree() and freed only at ExitTree(), so it would be unfortunate to have to use dynamic GI or return to batched multimeshinstance3ds.

I assume Meshinstances draw themselves with RenderingServer instances under the hood somewhere, and LightMapGI recognizes those, but I haven’t found any way to hijack that process. (It seems hard-coded to only scan for mesh nodes)

Is there a way to get LightMapGI to recognize RenderingServer Instances or should I settle for a compromise?

You can set instance’s lightmap using RenderingServer::instance_geometry_set_lightmap()

That function doesn’t seem to light the foliage and lightmap.lightdata.getusercount doesn’t report any new users; maybe I’m using it wrong?

My code is roughly:

Rid LightMapRid = lightmapGI.GetInstance();

Vector2I texSize = new Vector2I(
 lightmapGI.LightData.LightmapTextures[0].GetWidth(),
 lightmapGI.LightData.LightmapTextures[0].GetHeight());
}
for(int i=0;i<count;i++) {
 int pixelPortion = texSize.X/totalToMake;
 Vector2 size = new Vector2(pixelPortion, pixelPortion);
 Vector2 pos = new Vector2(i*pixelPortion, texSize.Y-pixelPortion);
 Rect2 lightMapPos = new Rect2(pos, size);
 //make instance rid
 RenderingServer.InstanceGeometrySetLightmap(instanceRid, LightMapRid, lightMapPos, 0);
}

(i also tried normalized texture coords)

no light shows on the mesh and the .exr baked file doesn’t have anything either.
I’m guessing the rect2 arg is the position I want in the lightmap texture and the int arg is the layer of the texture, but nothing I passed it worked.

You need to get the slice index from the baked data.

I don’t quite follow, it seems to me LightmapGIData is supposed to receive the slice index when I add a new user to it.

I tried changing the slice index, and it seems the lightmap is affecting the instances–I just couldn’t tell because the lowest slice index’s fill color was black. Changing the slice index to a higher slice where the fill color is lighter lights them all with that flat fill color. They don’t seem to bake themselves onto the texture, however.

Since your generation is deterministic, you’d need to bake to generated nodes and then transfer the baked data to corresponding nodeless geometry instances on the server.

However I looked at the LightmapGIData resource implementation. There are functions to retrieve the uv rectangle and slice index per user object, but they for some reason are not exposed to scripting. Unless you’re willing to manually parse the binary data stored in *.lmbake file or implement a gdextension - it looks like this is currently not possible to do from a script.

EDIT: There’s an undocumented hack that lets you do it. See @mrcdk’s example below.

The node to nodeless data transfer strategy seems promising.
It’s unfortunate the user functions aren’t exposed, I suppose I’ll go see how complex setting up a gdextension is tomorrow.
Thank you for your time.

You can. The script needs to be a @tool script, it needs to be attached to a Node3D or a child of a Node3D, and it must implement these 2 methods:

  • get_bake_meshes() that returns a flat array of interleaved Mesh/Transform3D like: [mesh1, transform1, mesh2, transform2,…]
  • get_bake_mesh_instance() that returns the RID of the mesh instance

Example:

@tool
extends Node3D

var RS = RenderingServer


var instances: Array[MeshInstance]


func _init() -> void:
	set_notify_transform(true)
	
	
func get_bake_meshes() -> Array:
	var ret = []
	for instance in instances:
		ret.push_back(instance.mesh)
		ret.push_back(global_transform * instance.transform)
	return ret


func get_bake_mesh_instance(idx: int) -> RID:
	var instance = instances.get(idx)
	if instance:
		return instance.rid
	else:
		return RID()


func _notification(what: int) -> void:
	if what == NOTIFICATION_ENTER_TREE:
		_update_visibility()
	elif what == NOTIFICATION_EXIT_TREE:
		pass
	elif what == NOTIFICATION_ENTER_WORLD:
		for instance in instances:
			RS.instance_set_scenario(instance.rid, get_world_3d().scenario)
			RS.instance_set_transform(instance.rid, global_transform * instance.transform)
	elif what == NOTIFICATION_EXIT_WORLD:
		for instance in instances:
			RS.instance_set_scenario(instance.rid, RID())
	elif what == NOTIFICATION_VISIBILITY_CHANGED:
		_update_visibility()
	elif what == NOTIFICATION_TRANSFORM_CHANGED:
		for instance in instances:
			RS.instance_set_transform(instance.rid, global_transform * instance.transform)
	elif what == NOTIFICATION_PREDELETE:
		_clear_mesh_instances()


func _get(property: StringName) -> Variant:
	if property == "_data":
		return get_bake_meshes()

	return null


func _set(property: StringName, value: Variant) -> bool:
	if property == "_data":
		_clear_mesh_instances()
		var arr = value as Array

		if not arr or not arr.size() % 2 == 0:
			return false

		for idx in range(0, arr.size(), 2):
			if not arr[idx] is Mesh or not arr[idx + 1] is Transform3D:
				continue

			var instance = _create_mesh_instance(arr[idx], arr[idx + 1])

			instances.push_back(instance)

		return true

	return false


func _get_property_list() -> Array[Dictionary]:
	var props: Array[Dictionary]
	props.push_back({
		"name": "_data",
		"type": TYPE_ARRAY,
		"usage": PROPERTY_USAGE_STORAGE
	})
	return props


func _update_visibility() -> void:
	for instance in instances:
		RS.instance_set_visible(instance.rid, is_visible_in_tree())


func _clear_mesh_instances() -> void:
	for instance in instances:
		RS.free_rid(instance.rid)

	instances.clear()

	notify_property_list_changed()


func _create_mesh_instance(mesh: Mesh, local_transform: Transform3D = Transform3D()) -> MeshInstance:
	var instance = MeshInstance.new()
	instance.mesh = mesh
	instance.transform = local_transform
	instance.rid = RS.instance_create()
	RS.instance_set_base(instance.rid, instance.mesh.get_rid())
	RS.instance_attach_object_instance_id(instance.rid, get_instance_id())

	if is_inside_tree():
		RS.instance_set_scenario(instance.rid, get_world_3d().scenario)
		RS.instance_set_transform(instance.rid, global_transform * instance.transform)

	return instance


func _generate() -> void:
	# generate MeshInstance here
	pass


class MeshInstance:
	var mesh: Mesh
	var transform: Transform3D
	var rid: RID

The order of the meshes needs to be the same when baking and when running.

It also has to be seen by the LightmapGI node so it needs to be a child or sibling of the node.

Also, the LightmapGIData data is marked as internal but you can still access and modify it with light_data.user_data It needs to be a flat array with interleaved: path to node NodePath/uv of the island in the lightmap textures Rect2/slice index in the lightmap textures int/instance sub index int

:eyes: Is this documented somewhere?

Nope. I’ve been working with lightmaps for a while and found them while reading the code.

Technically, the get_bake_meshes() and get_bake_mesh_instance() are “documented” in the GridMap node in a roundabout way.

The LightmapGIData.user_data is not documented anywhere as far as I can tell.

Thank you, this works great and was remarkably painless to implement.

It’s hacky as hell though.
Btw, you should mark @mrcdk’s post as the solution.