Grid Based Mesh Instancing by obj Data on Godot 3D

We will create a obj file as a grid reference. After we’ll fill it with meshes.

Create grid obj file

  1. Create a plane
  2. Subdivide it
  3. Remove unwanted faces
  4. Split faces
  5. Save obj file

file -> export -> wavefront .obj

We dont need UV and normals

import obj file to godot

Read grid obj file

Inside of an obj file

If we open our .obj file we will see something like this:

# Blender 4.2.3 LTS
# www.blender.org
o Plane.001
v 5.000000 0.000000 3.000000
v 5.000000 0.000000 1.000000
v 3.000000 0.000000 3.000000
v -1.000000 0.000000 3.000000
...
v 4.500000 0.000000 1.500000
v 4.500000 0.000000 3.500000
v 4.500000 0.000000 3.500000
v 4.500000 0.000000 3.500000
s 0
f 742 254 255 353
f 739 256 257 371
f 736 312 310 619
f 732 315 13 111
f 729 316 302 394
  • v -> define vertex:
    v x_coordinate y_coordinate z_coordinate
  • f -> define face:
    f index V2_index V3_index V4_index

Code for read obj

pseudo code:

  1. Open obj file
  2. Read line
  3. if line starts with v, add to vertices_location
  4. if line starts with f, calculate middle point of quad and add to loc_points
  5. if line starts with something else or empty do not care
  6. Go to 2. step until reach the end of file
  7. Close obj file

real code:

var loc_points : PoolVector3Array  = []

# read grid points from obj file
func load_grid_data(file : String):
	loc_points = PoolVector3Array() 
	
	# We create array for store vertices
	var vertices_location : PoolVector3Array  = [Vector3()]
	
	# read obj file line by line
	var f = File.new()
	f.open(file, File.READ)	
	
	while not f.eof_reached(): 
		
		var line : String = f.get_line()
		if line.empty():
			continue
			
		match line[0]:
			'v': # lines with start v (vertex)
				var ap = line.substr(2).split_floats(' ') # delete prefix and split
#				print(ap)

				var vert_loc : Vector3 = Vector3(ap[0],ap[1],ap[2])
				vertices_location.push_back(vert_loc)
				
				pass
			'f': # lines with start f (face). 
				var ap = line.substr(2).split_floats(' ') # delete prefix and split
#				print(ap)
				
				# vertex indices of four side of the quad 
				var p0_index : int = ap[0]
				var p1_index : int = ap[1]
				var p2_index : int = ap[2]
				var p3_index : int = ap[3]
				
#				sum 4 points of the quad and divide to 4
#				for finding middle point of the quad (square)
				var point_loc : Vector3 = Vector3() 
				point_loc += vertices_location[p0_index]
				point_loc += vertices_location[p1_index]
				point_loc += vertices_location[p2_index]
				point_loc += vertices_location[p3_index]
				point_loc /= 4 #sum all points and divide to four 
				
				# push the result the global points array 
				loc_points.push_back(point_loc)
				
				pass
			_: # we dont care other lines so we can pass 
				pass
				
	f.close()
	
	return
	

Create other meshes and materials

We need some meshes for our grid. (a cube, a torus, or maybe a monkey …)

Load Meshes

pseudo code:

  1. calculate paths
  2. load and set mesh and material
  3. create multimesh
  4. set transforms of the each instance
  5. create and add multimeshinstance to scene

real code:

onready var MESH_PATH = "res://meshes/"
onready var MATERIAL_PATH = "res://meshes/materials/"
var MESH_PREFIX = "SM_"
var MESH_POSTFIX = ".obj"
var MATERIAL_PREFIX = "M_"
var MATERIAL_POSTFIX = ".tres"



func load_building(building_index : int, count :int , mesh_path : String, material_path : String):
	if (count>loc_points.size()):
		count = loc_points.size()
	
	# create the path string of mesh and material
	var mesh_file_path : String = mesh_path + MESH_PREFIX + String(building_index) + MESH_POSTFIX
	var material_file_path : String = material_path + MATERIAL_PREFIX + String(building_index) + MATERIAL_POSTFIX
	
	# load mesh and material
	var building : ArrayMesh = load(mesh_file_path)
	var building_material : SpatialMaterial = load(material_file_path)
	building.surface_set_material(0,building_material)
	
	# initialize multimesh
	var multi_mesh = MultiMesh.new()
	multi_mesh.transform_format = MultiMesh.TRANSFORM_3D
	multi_mesh.color_format = MultiMesh.COLOR_NONE
	multi_mesh.custom_data_format = MultiMesh.CUSTOM_DATA_NONE
	
	multi_mesh.instance_count = count
	multi_mesh.visible_instance_count = count
	multi_mesh.mesh = building
	
	
	# set locations of meshes
	for i in count:
		var rand_num : int = rng.randi() % loc_points.size()
		multi_mesh.set_instance_transform(i, Transform(Basis(), loc_points[rand_num]))
		loc_points.remove(rand_num) 
	
	
	# create and add multi mesh instance to current scene
	var multi_mesh_ins = MultiMeshInstance.new() 
	multi_mesh_ins.set_multimesh(multi_mesh)
	add_child(multi_mesh_ins)
	
	pass

Why MultiMesh

Its definition:

“MultiMesh provides low-level mesh instancing. Drawing thousands of MeshInstance3D nodes can be slow, since each object is submitted to the GPU then drawn individually.

MultiMesh is much faster as it can draw thousands of instances with a single draw call, resulting in less API overhead.

As a drawback, if the instances are too far away from each other, performance may be reduced as every single instance will always render (they are spatially indexed as one, for the whole object)…”

https://docs.godotengine.org/en/stable/classes/class_multimesh.html

All code

extends Spatial


onready var GRID_DATA_FILE = "res://data/grid example.obj"
onready var MESH_PATH = "res://meshes/"
onready var MATERIAL_PATH = "res://meshes/materials/"
var MESH_PREFIX = "SM_"
var MESH_POSTFIX = ".obj"
var MATERIAL_PREFIX = "M_"
var MATERIAL_POSTFIX = ".tres"


var loc_points : PoolVector3Array  = []
var rng = RandomNumberGenerator.new()



# read grid points from obj file
func load_grid_data(file : String):
	loc_points = PoolVector3Array() 
	
	# We create array for store vertices
	var vertices_location : PoolVector3Array  = [Vector3()]
	
	# read obj file line by line
	var f = File.new()
	f.open(file, File.READ)	
	
	while not f.eof_reached(): 
		
		var line : String = f.get_line()
		if line.empty():
			continue
			
		match line[0]:
			'v': # lines with start v (vertex)
				var ap = line.substr(2).split_floats(' ') # delete prefix and split
#				print(ap)

				var vert_loc : Vector3 = Vector3(ap[0],ap[1],ap[2])
				vertices_location.push_back(vert_loc)
				
				pass
			'f': # lines with start f (face). 
				var ap = line.substr(2).split_floats(' ') # delete prefix and split
#				print(ap)
				
				# vertex indices of four side of the quad 
				var p0_index : int = ap[0]
				var p1_index : int = ap[1]
				var p2_index : int = ap[2]
				var p3_index : int = ap[3]
				
#				sum 4 points of the quad and divide to 4
#				for finding middle point of the quad (square)
				var point_loc : Vector3 = Vector3() 
				point_loc += vertices_location[p0_index]
				point_loc += vertices_location[p1_index]
				point_loc += vertices_location[p2_index]
				point_loc += vertices_location[p3_index]
				point_loc /= 4 #sum all points and divide to four 
				
				# push the result the global points array 
				loc_points.push_back(point_loc)
				
				pass
			_: # we dont care other lines so we can pass 
				pass
				
	f.close()
	
	return
	


func load_building(building_index : int, count :int , mesh_path : String, material_path : String):
	if (count>loc_points.size()):
		count = loc_points.size()
	
	# create the path string of mesh and material
	var mesh_file_path : String = mesh_path + MESH_PREFIX + String(building_index) + MESH_POSTFIX
	var material_file_path : String = material_path + MATERIAL_PREFIX + String(building_index) + MATERIAL_POSTFIX
	
	# load mesh and material
	var building : ArrayMesh = load(mesh_file_path)
	var building_material : SpatialMaterial = load(material_file_path)
	building.surface_set_material(0,building_material)
	
	# initialize multimesh
	var multi_mesh = MultiMesh.new()
	multi_mesh.transform_format = MultiMesh.TRANSFORM_3D
	multi_mesh.color_format = MultiMesh.COLOR_NONE
	multi_mesh.custom_data_format = MultiMesh.CUSTOM_DATA_NONE
	
	multi_mesh.instance_count = count
	multi_mesh.visible_instance_count = count
	multi_mesh.mesh = building
	
	
	# set locations of meshes
	for i in count:
		var rand_num : int = rng.randi() % loc_points.size()
		multi_mesh.set_instance_transform(i, Transform(Basis(), loc_points[rand_num]))
		loc_points.remove(rand_num) 
	
	
	# create and add multi mesh instance to current scene
	var multi_mesh_ins = MultiMeshInstance.new() 
	multi_mesh_ins.set_multimesh(multi_mesh)
	add_child(multi_mesh_ins)
	
	pass


# Called when the node enters the scene tree for the first time.
func _ready():
	# load our grid data
	load_grid_data(GRID_DATA_FILE)
	
	# create buildings 
	for i in range(3):
		load_building(i,50,MESH_PATH,MATERIAL_PATH)
	
	pass

scene setup

just a spatial node and script. Nothing more (I add a camera for the see result but it is not neccessary)


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *