I'm trying to sample 3d noise in a cylindrical fashion to create a game map that allows for seamless wrapping along the X-Axis. To do this, I've got variables to help calculate a cylinder:

var chunk_width: int = 70
var chunk_depth: int = 180
var mesh_resolution: int = 1
var num_segments: int = 5

func _ready():
	var pi = 3.14159265358979
	var circumference = chunk_width * num_segments * mesh_resolution
	var radius = circumference / (2 * pi)
	var angle_step = (360.0 / circumference) * (pi / 180.0)
	for segment in range(0, num_segments):
		var current_angle: float = 0.0
# get_noise_3d(x, radius * sin(current_angle), radius * cos(current_angle))
		for i in range(data.get_vertex_count()):
			var vertex = data.get_vertex(i)
			vertex.y = noise.get_noise_3d(vertex.x + (chunk_width * segment), vertex.z, 0) * 5
			data.set_vertex(i, vertex)
			current_angle += angle_step

I'm a bit lost, however, as to how the math works to get the position of the vertex in the get_noise_3d() function.

  • xyz replied to this.

    Lousifr You need a function that projects planar uv coordinates to a 3d cylinder:

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

    For simplicity, the above assumes that the input uv is in range (0-1) and that the cylinder is 1 unit high, has a radius of 1 unit, and base center at (0,0,0). For a bit of homework you can adapt the function to work with arbitrary cylinder radii, heights and center offsets, passed as optional input parameters.

      xyz I think you misunderstood the effect I'm trying to achieve. I don't actually want a cylinder in the game world, I want a flat map, splint into chunks and wrapped around the cameras x position.

      I need to calculate the coordinates of a point on a circle after a certain arc length, and use that as the x and y values passed into get_noise_3d(), for the z value, I can just use the z value of the vertex, as I don't want wrapping in the z axis.
      Here's the code I have thus far:

      class_name GameMap
      extends Node
      
      var chunk_width: int = 72
      var chunk_depth: int = 180
      var mesh_resolution: int = 1
      var num_chunks: int = 5
      
      var meshes: Array[MeshInstance3D]
      
      @export var noise: FastNoiseLite = FastNoiseLite.new()
      @export var camera_rig: Node3D = Node3D.new()
      
      func _ready():
      	var circumference = chunk_width * num_chunks * mesh_resolution
      	var radius = circumference / (2 * PI)
      	var chunk_step = rad_to_deg(chunk_width / radius) 
      	var angle_step = (360.0 / circumference) * (PI / 180.0)
      	for segment in range(0, num_chunks):
      		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://mat_grass.tres")
      
      		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(vertex.x + (chunk_width * segment), 0, 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_ON
      		add_child(meshes[segment])
      		meshes[segment].translate(Vector3(chunk_width * segment + (chunk_width / 2), 0, 0))
      
      func _process(delta):
      	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
      • xyz replied to this.

        Lousifr I think you misunderstood the effect I'm trying to achieve. I don't actually want a cylinder in the game world, I want a flat map, splint into chunks and wrapped around the cameras x position.

        I think I understood it correctly. You want to read 3d noise values around the surface of a cylinder and map these values as a heightmap onto the ground plane. The function I posted would facilitate exactly that. You pass your ground xz as the uv input and it returns the corresponding 3d vector on the surface of the cylinder. The returned vector is a 3d coordinate that should be used for sampling the noise. The value sampled from noise thus represents your heightmap displacement i.e. your vertex y offset from the ground.

        The problem boils down to standard cylindrical projection of uv coordinates.

          xyz So then is this where I plug my radius in?:

          func cylinder_uv_to_xyz(uv: Vector2, radius: float) -> Vector3:
          	return Vector3(radius, uv.y, 0.0).rotated(Vector3.UP, uv.x * PI * 2)
          		for i in range(data.get_vertex_count()):
          			var vertex = data.get_vertex(i)
          			var noise_value = cylinder_uv_to_xyz(Vector2(vertex.x, vertex.z), radius)
          			vertex.y = noise.get_noise_3d(noise_value.x, noise_value.y, noise_value.z) * 5
          			data.set_vertex(i, vertex)
          • xyz replied to this.

            Lousifr Radius is not really important. First make it work with the radius of 1. What's important is to normalize your xz vertex coordinates to 0-1 range or if you're mapping onto built in plane mesh, use actual uv coordinates instead.

              xyz I think the issue I'm having is that I'm generating the plane mesh in chunks, each beginning at the default position in world space, using that position in get_noise_3d(), committing to mesh, then translating the meshes to be in the correct position in terms of the map, but the noise function was sampled in the same location for each one. I will continue to work towards solving that issue, but I don't know if I am correct or not in my thinking.

              • xyz replied to this.

                Lousifr Then it's just a matter of translating and scaling the uv coordinate you pass to projection function, So instead of always using full 0-1 range for the u coordinate for a chunk, scale/translate it to only part of that range. That way, the chunks will tile around the cylinder and then wrap after wanted number of chunks. If you for example want to have 4 tiled chunks, the first chunk would use 0-.25 u range, the second one would use .25-.5 and so on.

                  xyz I got the following to work:

                  vertex.y = noise.get_noise_3d(radius * cos(deg_to_rad(((segment * chunk_width) + vertex.x) * angle_step)), radius * sin(deg_to_rad(((segment * chunk_width) + vertex.x) * angle_step)), vertex.z) * 5
                  • xyz replied to this.

                    Lousifr Not a very elegant way of doing it, but if it works for you - fine.

                    xyz How would I get it to use specific ranges for the u value? I've tried multiplying the scale in the x axis by 0.25, then adding to uv_translate using v, but to no avail.

                    	for v in md.get_vertex_count():
                    		var cyl_coord = cylinder_uv_to_xyz(md.get_vertex_uv(v) * (uv_scale) + (uv_translate))
                    		md.set_vertex(v, md.get_vertex(v) + Vector3(0.0, noise.get_noise_3dv(cyl_coord) * elevation_scale, 0.0))
                    • xyz replied to this.

                      Lousifr Cylinder's surface covers full 0-1 UV space. So if you want to tile N chunks around the cylinder circumference (in U direction), each chunk's U range needs to cover 1.0/N of total U range. For 4 chunks that's 0.25.

                      To properly position chunks, every successive chunk needs to be translated in U direction for additional 1.0/N (i.e. the U width of a chunk). So for the chunk number I, the total U translation should be I*(1.0/N).

                      V translation is irrelevant for proper positioning. You might want to tweak V scale though, simply to match the UV ratio of chunk's sampling range to its physical XZ ratio, so that sampled noise frequency appears same in both directions. The more chunks you tile around the cylinder, the more you'll need to scale the V coordinate to compensate for the narrowing of the U range per single chunk.

                        xyz This helps to understand UVs. Thanks for the help, I'm learning a lot.

                        • xyz replied to this.

                          Lousifr Yeah, the whole thing is much easier to understand through pictures. Now you just need to imagine this patch wrapping into a cylinder.

                            xyz So if I wanted to have a multiplier for the ground elevation that gets larger or smaller based on how high the ground is so as to allow for smooth transitions between sea level, level terrain (where the player will have its units), and mountains, should I use linear interpolation? Also, say I wanted to use a different noise function for generating mountains, how would I interpolate between the two based on the height of the mountain noise function? Or is this a better application for curves?

                            Just pointers on where I could learn more is enough.

                            • xyz replied to this.

                              Lousifr Well you can do whatever you wish with noises, For example, sample two different noises and use a third noise as an interpolation or mask parameter, or remap/ramp/threshold noise in any way. It boils down to getting creative and experiment with simple math to achieve wanted results. Don't know it there are tutorial specifically for that. Maybe take a look at this article to get some ideas. There is also this presentation that breaks down terrain generation techniques in Minecraft which may also give you some insights on how to manipulate noises into getting what you want.

                              It's conceptually no different from having multiple different noise filled layers in something like photoshop to blend between.

                                Megalomaniak It's conceptually no different from having multiple different noise filled layers in something like photoshop to blend between

                                Yeah and things like Levels, Curves or Gradient maps, which are, mathematically speaking, just various remapping functions.

                                I've got the map looking almost how I want it before I move onto the next step, to be returned to later. I'm curious how I can get the elevation in the ocean regions to follow a curve that lowers the terrain more the lower the elevation value. How do I use curves to do this, or is there a better way?

                                var cyl_coord = cylinder_uv_to_xyz(md.get_vertex_uv(v) * uv_scale * Vector2(1.0 / num_chunks, 1.0) + Vector2(i * (1.0 / num_chunks), 0.0))
                                var plains = plain_noise.get_noise_3dv(cyl_coord) * (plain_scale)
                                var mountains = mountain_noise.get_noise_3dv(cyl_coord) * (mountain_scale)
                                var elevation = plains + (mountains - plains) * (mountains * 0.7)
                                md.set_vertex(v, md.get_vertex(v) + Vector3(0.0, elevation, 0.0))
                                • xyz replied to this.

                                  Lousifr Normalize the elevation to 0-1 range and use it as an input to sample a curve. Again, it's good to implement realtime rebuild so you can see how changing the curve affects the final terrain.