Chunk LOD implementation help

Godot Version

4.6

Question

Here is a fast and furious Grid Terrain that I made based loosely on Henry’s work

instead of generating terrain I load up a 4k terrain texture and use that to displace the chunks …

unfortunately I messed up the setup and the chunks no longer load in place at different resolution … not sure why they just appear smaller.

Anyway I implemented an Occlusion system, so you need to check occlusion culling in the renderer settings and then the script should do the work …

Anyway if anyone can tell me how to get this modified script working as chunk LOD again that would be great, then your a better coder than I am after a days work. The FPS is comparable to other terrain addons… with Terrain3D I only get about 20 FPS with Sky3D, Terrabrush broke, the chunk LOD implementations, hterrain, SimpleTerrain, both ran really slow … so I suppose I am also sharing this … Occlusion …I get the min_height of a chunk and build a box that extends down to zero at that point …

As you can see from the screen shots the FPS is OK (like 50-77) when stood at the side of the hills … usually 40 ish, and low when viewing the entire thing.

script:

@tool
class_name STRIP
extends MeshInstance3D
#@export_tool_button("Rebuild mesh","Callable") var _rebuild_mesh_action = run_it


var hmap:Image 
var albedo_tex:Texture2D
var splat2:Texture2D

func load_image():
	hmap = LoadLargeHeightMap("res://assets/Heightmaps/ISLAND_4K/export.exr")
	

func LoadLargeHeightMap(filename:String)->Image:
	var height:Texture2D = load(filename)
	albedo_tex = load("res://assets/Heightmaps/ISLAND_4K/export_preview.png")
	splat2 = load("res://assets/Heightmaps/ISLAND_4K/color_grad.png")
	return height.get_image()
	
func get_altitude(pos:Vector3)->float:
	
	if hmap:
		if pos.x < 4096 and pos.z < 4096:
			return hmap.get_pixel(pos.x,pos.z).r* 500.0
		
		return hmap.get_pixel(0,0).r 
	else:
		print("error no heightmap")
		return 0.0
			
	
@export var _noise: FastNoiseLite

var resolution:int = 128
var occluders = []
var min_h = 100000	
func generate_mesh_data(rect: Rect2) -> Array:
	var grid_size = resolution + 1
	var step_size = rect.size.x / resolution
	
	# Step 1: Generate Base Height Data
	var height_data = PackedFloat32Array()
	height_data.resize(grid_size * grid_size)
	
	var rect2 = rect
	rect2.size.x +=1
	rect2.size.y +=1
	
	
	#var h_region = hmap.get_region(rect2)
	#var texture:Texture2D = ImageTexture.create_from_image(h_region)
	min_h = 100000	
	#texture.
	for z in grid_size:
		for x in grid_size:
			var pos_x = rect.position.x + (x * step_size)
			var pos_z = rect.position.y + (z * step_size)
			var h:float = get_altitude(Vector3(pos_x, 0, pos_z))
			if h < min_h:
				min_h = h
			height_data[x + z * grid_size] = h
	
	# Step 3: Build the Mesh Arrays
	var vertices = PackedVector3Array()
	var uvs = PackedVector2Array()
	var indices = PackedInt32Array()
	
	vertices.resize(grid_size * grid_size)
	uvs.resize(grid_size * grid_size)
	
	for z in grid_size:
		for x in grid_size:
			var i = x + z * grid_size
			var pos_x =  (x * step_size)
			var pos_z =  (z * step_size)
			
			vertices[i] = Vector3(pos_x, height_data[i], pos_z)
			uvs[i] = Vector2( (rect.position.x+float(x)) / (4096.0), (rect.position.y+float(z)) / (4096.0))

	# Indices logic
	for z in resolution:
		for x in resolution:
			var i = x + z * grid_size
			indices.append(i)
			indices.append(i + 1)
			indices.append(i + grid_size)
			indices.append(i + 1)
			indices.append(i + grid_size + 1)
			indices.append(i + grid_size)

	var arrays = []
	arrays.resize(Mesh.ARRAY_MAX)
	arrays[Mesh.ARRAY_VERTEX] = vertices
	arrays[Mesh.ARRAY_TEX_UV] = uvs
	arrays[Mesh.ARRAY_INDEX] = indices
	
	return arrays
	
func thread_generate_mesh(node: Rect2):
	# 1. Generate the raw data
	var mesh_data = generate_mesh_data(node)
	
	# 2. DO THE HEAVY LIFTING HERE (Off the main thread)
	var array_mesh = ArrayMesh.new()
	array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_data)
	
	var st = SurfaceTool.new()
	st.create_from(array_mesh, 0)
	st.generate_normals()
	var final_mesh = st.commit() # This is now ready for the GPU
	

	
	# Pass the completed mesh to the main thread
	#call_deferred("finalize_node", node, final_mesh, min_h)	
	finalize_node(node, final_mesh, min_h)

func finalize_node(node: Rect2, final_mesh: Mesh,h:float):
	#pending_nodes = max(0, pending_nodes - 1)
	
	# Create the instance - this is now very light
	var mesh_instance = MeshInstance3D.new()
	mesh_instance.mesh = final_mesh
	mesh_instance.material_override = load("uid://dlywemjv2a6fg")
	(mesh_instance.material_override as ShaderMaterial).set_shader_parameter("splat",albedo_tex)
	(mesh_instance.material_override as ShaderMaterial).set_shader_parameter("splat2",splat2)
	
	mesh_instance.set_meta("node", node)
	
	var occ = OccluderInstance3D.new()
	var box_occ = BoxOccluder3D.new()
	box_occ.size = Vector3(resolution, h, resolution)
	occ.occluder = box_occ
	
	
	#var box = MeshInstance3D.new()
	#box.mesh = BoxMesh.new()
	#(box.mesh as BoxMesh).size =  Vector3(resolution, h, resolution)
	#box.global_position = Vector3(node.position.x+resolution/2.0, h/2.0, node.position.y+resolution/2.0)	
	#
	
	add_child(mesh_instance)
	add_child(occ)
	occ.global_position = Vector3(node.position.x+resolution/2.0, h/2.0, node.position.y+resolution/2.0)	
	#add_child(box)
	mesh_instance.global_position = Vector3(node.position.x,0.0,node.position.y)


var first_time:bool = true	
#func _process(delta:float) -> void:
func _ready() -> void:
	if not Engine.is_editor_hint() and first_time:
		first_time = false
		load_image()
		for i in range(0,4096/resolution):
			for j in range(4096/resolution):
				var rect:Rect2
				rect.position = Vector2(float(i)*resolution,float(j)*resolution)
				rect.size = Vector2(resolution, resolution)
				thread_generate_mesh(rect)
			

Picture shows the debug occluders inside the terrain.

Got it sorted with import settings on Chunk LOD terrain engines, so im importing the occluders to the other scene. Terrain3D is the only one that reliably loads 8k terrains but this is just a 4k terrain so it would also do it easily. Terrain3D might not benefit from occlusion so much.