I have a terrain generator that works fine with a map resolution (multiplicand of subdivisions) of 1, but when I increase it, the map becomes distorted and stops tiling correctly. Examples below.
Resolution of 1:

Resolution of 2:


I've spent time going through the code to make sure the equations are right, and it seems so to me, so I'm a bit lost as to what the issue is.

func _ready():
	var circumference = chunk_width * num_chunks * mesh_resolution
	var radius = circumference / (2 * PI)
	var angle_step = 360.0 / (chunk_width * num_chunks * mesh_resolution)
	
	for segment in range(0, num_chunks):
		var plane_mesh = generate_plane_mesh()

		var surface = SurfaceTool.new()
		var data = MeshDataTool.new()
		surface.create_from(plane_mesh, 0)

		var array_mesh = surface.commit()
		data.create_from_surface(array_mesh, 0)
		for i in range(data.get_vertex_count()):
			var vertex = data.get_vertex(i)
			vertex.y = noise.get_noise_3d(
				radius * cos(deg_to_rad(((segment * chunk_width * mesh_resolution) + vertex.x) * angle_step)),
				radius * sin(deg_to_rad(((segment * chunk_width * mesh_resolution) + vertex.x) * angle_step)),
				vertex.z
			) * 5
			data.set_vertex(i, vertex)

		array_mesh.clear_surfaces()

		data.commit_to_surface(array_mesh)
		surface.begin(Mesh.PRIMITIVE_TRIANGLES)
		surface.create_from(array_mesh, 0)
		surface.generate_normals()

		meshes.append(MeshInstance3D.new())
		meshes[segment].mesh = surface.commit()
		meshes[segment].create_trimesh_collision()
		meshes[segment].cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
		add_child(meshes[segment])
		meshes[segment].translate(Vector3(chunk_width * segment + (chunk_width / 2), 0, 0))
  • xyz replied to this.
    • Best Answerset by Lousifr

    Lousifr Well that looks like some other kind of problem with your code. Here's a compact demo to try:

    extends MeshInstance3D
    
    @export var elevation_scale = .5
    @export var uv_translate = Vector2(0.0, 0.0)
    @export var uv_scale = Vector2(1.0, 1.0)
    
    var md: MeshDataTool = MeshDataTool.new()
    var st: SurfaceTool = SurfaceTool.new()
    var plane: PlaneMesh = PlaneMesh.new()
    var noise: FastNoiseLite = FastNoiseLite.new()
    
    func _ready():
    	mesh = ArrayMesh.new()
    	noise.frequency = .2
    
    func _process(delta):
    	rebuild()
    
    func rebuild(subdiv: Vector2i = Vector2i(32, 32)):
    	# create plane array mesh with specified subdivs
    	plane.subdivide_depth = subdiv.x
    	plane.subdivide_width = subdiv.y
    	mesh.clear_surfaces()
    	mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, plane.get_mesh_arrays())
    	
    	# displace vertices
    	md.create_from_surface(mesh, 0)
    	for i in md.get_vertex_count():
    		var cyl_coord = cylinder_uv_to_xyz(md.get_vertex_uv(i) * uv_scale + uv_translate)
    		md.set_vertex(i, md.get_vertex(i) + Vector3(0.0, noise.get_noise_3dv(cyl_coord) * elevation_scale, 0.0))
    		
    	# rebuild normals
    	mesh.clear_surfaces()
    	md.commit_to_surface(mesh)
    	st.create_from(mesh, 0)
    	st.generate_normals()
    	mesh = st.commit()
    
    func cylinder_uv_to_xyz(uv: Vector2) -> Vector3:
    	return Vector3(1.0, uv.y * PI * 2, 0.0).rotated(Vector3.UP, uv.x * PI * 2)

    Lousifr Umm, I already answered this in your previous thread. Use proper cylindrical projection when sampling the noise texture. Employ a generalized cylindrical projection function that's not tied to mesh resolution (I posted it in the previous thread). That way you can control which chunk projects on which part of the cylinder to ensure tiling/wrapping, regardless of the mesh resolution.

      xyz

      func cylinder_uv_to_xyz(uv: Vector2) -> Vector3:
      	return Vector3(1.0, uv.y, 0.0).rotated(Vector3.UP, uv.x * PI * 2)

      Is this how I should use this function? I'm sorry for not understanding your solution, I'm trying my best. I've been reading the docs, and PlaneMesh doesn't have a UV property. Am I supposed to modify the MeshInstance3D after I commit the surface?

      for i in range(data.get_vertex_count()):
      	var vertex = data.get_vertex(i)
      	var pos = cylinder_uv_to_xyz(Vector2(vertex.x, vertex.z))
      	vertex.y = noise.get_noise_3d(
      		pos.x,
      		pos.y,
      		pos.z
      	) * 5
      	data.set_vertex(i, vertex)

      This isn't working, but is this close?

      var array_mesh = surface.commit()
      data.create_from_surface(array_mesh, 0)
      for v in range(data.get_vertex_count()):
      	var vertex = data.get_vertex(v)
      	var pos = cylinder_uv_to_xyz(data.get_vertex_uv(v))
      	vertex.y = noise.get_noise_3d(
      		pos.x,
      		pos.y,
      		pos.z
      	) * 5
      	data.set_vertex(v, vertex)
      • xyz replied to this.

        Lousifr PlaneMesh actually does generate proper UVs. You can access them via mesh data tool similar to how you access vertex positions: data.get_vertex_uv(i)

          xyz How would I get the sampling to change for each chunk? I'm not translating them until after I commit to mesh, so the values are being sampled at the same place for each chunk.

          Lousifr First, test the whole thing on a single chunk. Read the UVs via data tool, project them using that function to get 3d cylinder coordinates and use those coordinates as input to the noise function.

          Since PlaneMesh UVs will be in 0-1 range and my function maps 0-1 range U coordinate to the whole circumference of the cylinder and 0-1 range V coordinate to the whole height of the cylinder, your test chunk will be fully mapped to the whole cylinder (with radius=1 and height=1)

          Now you can manipulate the mapping by scaling and sliding UVs that are passed to the projection function. If you multiply U and V by some values, you'll scale the mapped area in corresponding directions. If you add some value to the U coordinate, you'll slide the area around the circumference and if you add a value to the V coordinate, you'll slide it up/down the height of the cylinder.

          To get some intuition about this. I suggest exporting two Vector2 properties. Multiply UV with one of them and add the other to the resulting UV. Play with these two properties at runtime and see how the noise scales/slides along the chunk. You'll need to rebuild the mesh each frame for this to work but it's not a big deal and it's totally worth it to get a nice realtime preview.

            xyz The sampling is hardly changing in the z-axis:

            I tried plugging the vertex.z position into the z component of the Vector3:

            return Vector3(10.0, uv.y, tex_z).rotated(Vector3.UP, uv.x * PI * 2)

            But that had odd results:

            • xyz replied to this.

              Lousifr Don't change the projection function. Use the one I posted until you get things working.

              Okay. How would I go about rebuilding the mesh every frame? Obviously I would need to handle this in _process. Is there a way I can this directly through the MeshInstance3D or do I have to create a new Surface and MeshData?
              Also, this is what you meant with the two Vector2's?

              var pos = cylinder_uv_to_xyz((data.get_vertex_uv(v) * uv_mult) + uv_add)
              • xyz replied to this.

                Lousifr Okay. How would I go about rebuilding the mesh every frame? Obviously I would need to handle this in _process. Is there a way I can this directly through the MeshInstance3D or do I have to create a new Surface and MeshData?

                Best to make a function, something like build_chunk_mesh() and call it from _process(). You can put it in the same script your current mesh building code resides.

                You can create mesh tool objects once at startup and then just reinitialize them when rebuilding. It's also ok if you create them anew on each rebuild. Doesn't really matter as this in only for preview.

                Also, create the noise object only once at startup. Note that you don't really need to bother with changing the size of the sampling cylinder. If you need denser noise, simply increase the frequency in the noise object.

                Lousifr Also, this is what you meant with the two Vector2's?

                Yep.

                  xyz I'm seeing what happens when I adjust the uv_add and uv_mult vectors, but the z values from the sampling are staying the same.

                  for v in range(data.get_vertex_count()):
                  	var vertex = data.get_vertex(v)
                  	var pos = cylinder_uv_to_xyz((data.get_vertex_uv(v) * uv_mult) + uv_add)
                  	vertex.y = noise.get_noise_3d(
                  		pos.x,
                  		pos.y,
                  		pos.z
                  	) * 5
                  	data.set_vertex(v, vertex)
                  func cylinder_uv_to_xyz(uv: Vector2) -> Vector3:
                  	return Vector3(10.0, uv.y, 0.0).rotated(Vector3.UP, uv.x * PI * 2)
                  • xyz replied to this.

                    Lousifr In cylinder_uv_to_xyz() there should be 1.0 instead of 10.0. Not sure how you ended up with that 10.0. This makes sampling cylinder of radius 10 and height of 1, effectively stretching the sampling in u direction more than 10 times in respect to sampling in v direction.

                      xyz That's not solving the issue with the z values in the noise function.

                      class_name GameMap
                      extends Node
                      
                      var chunk_width: int = 180
                      var chunk_depth: int = 180
                      var mesh_resolution: int = 1
                      var num_chunks: int = 1
                      
                      var meshes: Array[MeshInstance3D]
                      var surface = SurfaceTool.new()
                      var data = MeshDataTool.new()
                      var plane_mesh = PlaneMesh.new()
                      
                      @export var noise: FastNoiseLite = FastNoiseLite.new()
                      @export var camera_rig: Node3D = Node3D.new()
                      @export var uv_mult: Vector2 = Vector2.ZERO
                      @export var uv_add: Vector2 = Vector2.ZERO
                      
                      func _ready():
                      	var circumference = chunk_width * num_chunks * mesh_resolution
                      	var radius = circumference / (2 * PI)
                      	var angle_step = 360.0 / (chunk_width * num_chunks * mesh_resolution)
                      	
                      	for segment in range(0, num_chunks):
                      		build_chunk_mesh(segment)
                      
                      func _process(delta) -> void:
                      	build_chunk_mesh(0)
                      	
                      	var map_width = chunk_width * num_chunks
                      	for m in meshes:
                      		var widths_from_camera = (m.global_position.x - camera_rig.global_position.x) / map_width
                      		if (abs(widths_from_camera) <= 0.5):
                      			continue
                      
                      		if (widths_from_camera > 0):
                      			widths_from_camera += 0.5
                      		else:
                      			widths_from_camera -= 0.5
                      
                      		var widths_to_fix: int = widths_from_camera
                      		m.global_position.x -= widths_to_fix * map_width
                      
                      func generate_plane_mesh() -> PlaneMesh:
                      	var plane_mesh = PlaneMesh.new()
                      	plane_mesh.size = Vector2(chunk_width, chunk_depth)
                      	plane_mesh.subdivide_width = chunk_width * mesh_resolution
                      	plane_mesh.subdivide_depth = chunk_depth * mesh_resolution
                      	plane_mesh.material = preload("res://terrain_shader_material.tres")
                      	return plane_mesh
                      
                      func build_chunk_mesh(chunk: int) -> void:
                      	plane_mesh = generate_plane_mesh()
                      	surface.create_from(plane_mesh, 0)
                      
                      	var array_mesh = surface.commit()
                      	data.create_from_surface(array_mesh, 0)
                      	for v in range(data.get_vertex_count()):
                      		var vertex = data.get_vertex(v)
                      		var pos = cylinder_uv_to_xyz((data.get_vertex_uv(v) * uv_mult) + uv_add)
                      		vertex.y = noise.get_noise_3d(
                      			pos.x,
                      			pos.y,
                      			pos.z
                      		) * 50
                      		data.set_vertex(v, vertex)
                      
                      	array_mesh.clear_surfaces()
                      
                      	data.commit_to_surface(array_mesh)
                      	surface.begin(Mesh.PRIMITIVE_TRIANGLES)
                      	surface.create_from(array_mesh, 0)
                      	surface.generate_normals()
                      
                      	meshes.append(MeshInstance3D.new())
                      	meshes[chunk].mesh = surface.commit()
                      	meshes[chunk].create_trimesh_collision()
                      	meshes[chunk].cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
                      	add_child(meshes[chunk])
                      	meshes[chunk].translate(Vector3(chunk_width * chunk, 0, 0))
                      
                      func cylinder_uv_to_xyz(uv: Vector2) -> Vector3:
                      	return Vector3(1.0, uv.y, 0.0).rotated(Vector3.UP, uv.x * PI * 2)
                      • xyz replied to this.

                        Lousifr What happens if you try this:

                        return Vector3(1.0, uv.y * PI * 2, 0.0).rotated(Vector3.UP, uv.x * PI * 2)

                          xyz That produced results, but now the debugger is spamming these two errors until my computer freezes:

                          E 0:00:01:0047   GameMap.gd:28 @ _process(): Condition "!is_inside_tree()" is true. Returning: Transform3D()
                            <C++ Source>   scene/3d/node_3d.cpp:343 @ get_global_transform()
                            <Stack Trace>  GameMap.gd:28 @ _process()
                          
                          E 0:00:01:0080   GameMap.gd:75 @ build_chunk_mesh(): Can't add child '@MeshInstance3D@2' to 'GameMap', already has a parent 'GameMap'.
                            <C++ Error>    Condition "p_child->data.parent" is true.
                            <C++ Source>   scene/main/node.cpp:1411 @ add_child()
                            <Stack Trace>  GameMap.gd:75 @ build_chunk_mesh()
                                           GameMap.gd:24 @ _process()
                          • xyz replied to this.

                            Lousifr Well that looks like some other kind of problem with your code. Here's a compact demo to try:

                            extends MeshInstance3D
                            
                            @export var elevation_scale = .5
                            @export var uv_translate = Vector2(0.0, 0.0)
                            @export var uv_scale = Vector2(1.0, 1.0)
                            
                            var md: MeshDataTool = MeshDataTool.new()
                            var st: SurfaceTool = SurfaceTool.new()
                            var plane: PlaneMesh = PlaneMesh.new()
                            var noise: FastNoiseLite = FastNoiseLite.new()
                            
                            func _ready():
                            	mesh = ArrayMesh.new()
                            	noise.frequency = .2
                            
                            func _process(delta):
                            	rebuild()
                            
                            func rebuild(subdiv: Vector2i = Vector2i(32, 32)):
                            	# create plane array mesh with specified subdivs
                            	plane.subdivide_depth = subdiv.x
                            	plane.subdivide_width = subdiv.y
                            	mesh.clear_surfaces()
                            	mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, plane.get_mesh_arrays())
                            	
                            	# displace vertices
                            	md.create_from_surface(mesh, 0)
                            	for i in md.get_vertex_count():
                            		var cyl_coord = cylinder_uv_to_xyz(md.get_vertex_uv(i) * uv_scale + uv_translate)
                            		md.set_vertex(i, md.get_vertex(i) + Vector3(0.0, noise.get_noise_3dv(cyl_coord) * elevation_scale, 0.0))
                            		
                            	# rebuild normals
                            	mesh.clear_surfaces()
                            	md.commit_to_surface(mesh)
                            	st.create_from(mesh, 0)
                            	st.generate_normals()
                            	mesh = st.commit()
                            
                            func cylinder_uv_to_xyz(uv: Vector2) -> Vector3:
                            	return Vector3(1.0, uv.y * PI * 2, 0.0).rotated(Vector3.UP, uv.x * PI * 2)

                            Lousifr You're trying to add a new chunk node each frame. That's something you don't want to do and is likely causing the problem. Instead rebuild the mesh resource in the single existing chunk node.