Rendering Objects in Godot That Don't Exist
Published • Last Updated • 3 minutes
Rendering Objects in Godot That Don’t Exist
My fascination with rendering meshes without nodes first started with Unity (GameObjects), and I’ve been chasing that high ever since I switched to Godot. Turns out it works in Godot in a very similar way to Unity: the game engine takes a mesh and batch renders it using an array of transforms and a material.
Rendering a Single Mesh
In this example, we export two variables, a mesh and a material, to be modified on the script attached to a root node. These objects can be either a built-in object (BoxMesh or StandardMaterial3D) or an asset already in your project.
The script stores a reference to a RID (resource ID) object, which will be used to render the mesh and material. We then create that object using RenderingServer.instance_create and attach the mesh, material, and default transform to it.
This will render a single cube in your scene. If you can’t see it, you might need to reposition your camera to see an object sitting at the origin.
extends Node3D
@export var mesh: Mesh
@export var material: Material
var instance: RID
func _ready() -> void:
instance = RenderingServer.instance_create()
RenderingServer.instance_set_base(instance, mesh)
RenderingServer.instance_set_scenario(instance, get_world_3d().scenario)
RenderingServer.instance_geometry_set_material_override(instance, material)
RenderingServer.instance_set_transform(instance, Transform3D.IDENTITY)
func _exit_tree() -> void:
if instance.is_valid():
RenderingServer.free_rid(instance)

Rendering Multiple Meshes
This example gets a bit more complicated as we create and store multiple instances of the object used to render meshes, as well as a corresponding transform, so we can position objects in different locations as well as update those positions in the _process loop.
The _ready method contains logic for generating a large number of mesh renderers in a grid and adds them to the instances variable.
The _process method updates the transforms of the mesh renderers to rotate them on each game tick, updating each instance with the newly modified transform.
extends Node3D
@export var mesh: Mesh
@export var material: Material
var instances: Dictionary[RID, Transform3D]
var size: int = 100
var local_scale: Vector3 = Vector3.ONE / 2
func _ready() -> void:
var scenario := get_world_3d().scenario
for x: int in range(-size, size):
for y: int in range(-size, size):
var instance := RenderingServer.instance_create()
RenderingServer.instance_set_base(instance, mesh)
RenderingServer.instance_set_scenario(instance, scenario)
RenderingServer.instance_geometry_set_material_override(instance, material)
var local_transform := Transform3D(
Basis.IDENTITY.scaled(local_scale), Vector3(x, y, 0)
)
RenderingServer.instance_set_transform(instance, local_transform)
instances[instance] = local_transform
func _process(delta: float) -> void:
var rotation_basis := Basis(Vector3.UP, 1.0 * delta)
for instance: RID in instances.keys():
instances[instance].basis = rotation_basis * instances[instance].basis
RenderingServer.instance_set_transform(instance, instances[instance])
func _exit_tree() -> void:
for instance: RID in instances.keys():
if instance.is_valid():
RenderingServer.free_rid(instance)

Rendering Multiple Meshes in Batches
This is where it gets fun. Up until now, we were creating an object to render a single mesh and needing to keep a reference to it. That works for a small number of meshes, but if you need hundreds or thousands, you probably want to batch them. That's where MultiMesh and MultiMeshInstance3D come into play.
Things are set up in a similar way to the previous example, but in this case, we are only storing transforms and not a mesh renderer for each object.
On _ready we are creating a MultiMesh object and attaching the mesh to it. Then we set the material on the MultiMeshInstance3D node (what the script needs to be attached to). And just like before, we create a grid of transforms and store them in an array. For each item in the array, we update the corresponding item in the MultiMesh with that transform.
The _process method updates the transforms of the mesh renderers to rotate them on each game tick, updating each mesh item with the newly modified transform.
extends MultiMeshInstance3D
@export var mesh: Mesh
@export var material: Material
var transforms: Array[Transform3D] = []
var current_rotation: float = 0.0
var size: int = 100
var total_instances: int = (size * 2) * (size * 2)
func _ready() -> void:
multimesh = MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.mesh = mesh
multimesh.instance_count = total_instances
transforms.resize(total_instances)
material_override = material
var index: int = 0
var local_scale: Vector3 = Vector3.ONE / 2
for x: int in range(-size, size):
for y: int in range(-size, size):
transforms[index] = Transform3D(
Basis.IDENTITY.scaled(local_scale),
Vector3(x, y, 0)
)
multimesh.set_instance_transform(index, transforms[index])
index += 1
func _process(delta: float) -> void:
current_rotation += 1.0 * delta
var rotation_basis := Basis(Vector3.UP, current_rotation)
for i: int in range(multimesh.instance_count):
var rotated_transform := transforms[i]
rotated_transform.basis = rotation_basis * rotated_transform.basis
multimesh.set_instance_transform(i, rotated_transform)

(Yep, it's the same photo as above because it renders the exact same objects in the same positions.)
Final Thoughts
So far, I've really enjoyed working with Godot in this way. Sure, I still create 3D nodes in the "normal" way, but if I know that I need to render a large number of meshes and I need to squeeze as much performance out of my game as possible, I'll look into rendering them using one of these workflows.
If you want to give this code a try without needing to set up your own project, the GitHub repo https://github.com/neogeek/Rendering-Objects-in-Godot-That-Dont-Exist contains all of this code and demo scenes.