@tool #const HT_Logger = preload("./util/logger.gd") const HTerrainData = preload("./hterrain_data.gd") const SEAM_LEFT = 1 const SEAM_RIGHT = 2 const SEAM_BOTTOM = 4 const SEAM_TOP = 8 const SEAM_CONFIG_COUNT = 16 # [seams_mask][lod] var _mesh_cache := [] var _chunk_size_x := 16 var _chunk_size_y := 16 func configure(chunk_size_x: int, chunk_size_y: int, lod_count: int): assert(typeof(chunk_size_x) == TYPE_INT) assert(typeof(chunk_size_y) == TYPE_INT) assert(typeof(lod_count) == TYPE_INT) assert(chunk_size_x >= 2 or chunk_size_y >= 2) _mesh_cache.resize(SEAM_CONFIG_COUNT) if chunk_size_x == _chunk_size_x \ and chunk_size_y == _chunk_size_y and lod_count == len(_mesh_cache): return _chunk_size_x = chunk_size_x _chunk_size_y = chunk_size_y # TODO Will reduce the size of this cache, but need index buffer swap feature for seams in SEAM_CONFIG_COUNT: var slot := [] slot.resize(lod_count) _mesh_cache[seams] = slot for lod in lod_count: slot[lod] = make_flat_chunk(_chunk_size_x, _chunk_size_y, 1 << lod, seams) func get_chunk(lod: int, seams: int) -> Mesh: return _mesh_cache[seams][lod] as Mesh static func make_flat_chunk(quad_count_x: int, quad_count_y: int, stride: int, seams: int) -> Mesh: var positions = PackedVector3Array() positions.resize((quad_count_x + 1) * (quad_count_y + 1)) var i = 0 for y in quad_count_y + 1: for x in quad_count_x + 1: positions[i] = Vector3(x * stride, 0, y * stride) i += 1 var indices := make_indices(quad_count_x, quad_count_y, seams) var arrays := [] arrays.resize(Mesh.ARRAY_MAX); arrays[Mesh.ARRAY_VERTEX] = positions arrays[Mesh.ARRAY_INDEX] = indices var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) return mesh # size: chunk size in quads (there are N+1 vertices) # seams: Bitfield for which seams are present static func make_indices(chunk_size_x: int, chunk_size_y: int, seams: int) -> PackedInt32Array: var output_indices := PackedInt32Array() if seams != 0: # LOD seams can't be made properly on uneven chunk sizes assert(chunk_size_x % 2 == 0 and chunk_size_y % 2 == 0) var reg_origin_x := 0 var reg_origin_y := 0 var reg_size_x := chunk_size_x var reg_size_y := chunk_size_y var reg_hstride := 1 if seams & SEAM_LEFT: reg_origin_x += 1; reg_size_x -= 1; reg_hstride += 1 if seams & SEAM_BOTTOM: reg_origin_y += 1 reg_size_y -= 1 if seams & SEAM_RIGHT: reg_size_x -= 1 reg_hstride += 1 if seams & SEAM_TOP: reg_size_y -= 1 # Regular triangles var ii := reg_origin_x + reg_origin_y * (chunk_size_x + 1) for y in reg_size_y: for x in reg_size_x: var i00 := ii var i10 := ii + 1 var i01 := ii + chunk_size_x + 1 var i11 := i01 + 1 # 01---11 # | /| # | / | # |/ | # 00---10 # This flips the pattern to make the geometry orientation-free. # Not sure if it helps in any way though var flip = ((x + reg_origin_x) + (y + reg_origin_y) % 2) % 2 != 0 if flip: output_indices.push_back( i00 ) output_indices.push_back( i10 ) output_indices.push_back( i01 ) output_indices.push_back( i10 ) output_indices.push_back( i11 ) output_indices.push_back( i01 ) else: output_indices.push_back( i00 ) output_indices.push_back( i11 ) output_indices.push_back( i01 ) output_indices.push_back( i00 ) output_indices.push_back( i10 ) output_indices.push_back( i11 ) ii += 1 ii += reg_hstride # Left seam if seams & SEAM_LEFT: # 4 . 5 # |\ . # | \ . # | \. # (2)| 3 # | /. # | / . # |/ . # 0 . 1 var i := 0 var n := chunk_size_y / 2 for j in n: var i0 := i var i1 := i + 1 var i3 := i + chunk_size_x + 2 var i4 := i + 2 * (chunk_size_x + 1) var i5 := i4 + 1 output_indices.push_back( i0 ) output_indices.push_back( i3 ) output_indices.push_back( i4 ) if j != 0 or (seams & SEAM_BOTTOM) == 0: output_indices.push_back( i0 ) output_indices.push_back( i1 ) output_indices.push_back( i3 ) if j != n - 1 or (seams & SEAM_TOP) == 0: output_indices.push_back( i3 ) output_indices.push_back( i5 ) output_indices.push_back( i4 ) i = i4 if seams & SEAM_RIGHT: # 4 . 5 # . /| # . / | # ./ | # 2 |(3) # .\ | # . \ | # . \| # 0 . 1 var i := chunk_size_x - 1 var n := chunk_size_y / 2 for j in n: var i0 := i var i1 := i + 1 var i2 := i + chunk_size_x + 1 var i4 := i + 2 * (chunk_size_x + 1) var i5 := i4 + 1 output_indices.push_back( i1 ) output_indices.push_back( i5 ) output_indices.push_back( i2 ) if j != 0 or (seams & SEAM_BOTTOM) == 0: output_indices.push_back( i0 ) output_indices.push_back( i1 ) output_indices.push_back( i2 ) if j != n - 1 or (seams & SEAM_TOP) == 0: output_indices.push_back( i2 ) output_indices.push_back( i5 ) output_indices.push_back( i4 ) i = i4; if seams & SEAM_BOTTOM: # 3 . 4 . 5 # . / \ . # . / \ . # ./ \. # 0-------2 # (1) var i := 0; var n := chunk_size_x / 2; for j in n: var i0 := i var i2 := i + 2 var i3 := i + chunk_size_x + 1 var i4 := i3 + 1 var i5 := i4 + 1 output_indices.push_back( i0 ) output_indices.push_back( i2 ) output_indices.push_back( i4 ) if j != 0 or (seams & SEAM_LEFT) == 0: output_indices.push_back( i0 ) output_indices.push_back( i4 ) output_indices.push_back( i3 ) if j != n - 1 or (seams & SEAM_RIGHT) == 0: output_indices.push_back( i2 ) output_indices.push_back( i5 ) output_indices.push_back( i4 ) i = i2 if seams & SEAM_TOP: # (4) # 3-------5 # .\ /. # . \ / . # . \ / . # 0 . 1 . 2 var i := (chunk_size_y - 1) * (chunk_size_x + 1) var n := chunk_size_x / 2 for j in n: var i0 := i var i1 := i + 1 var i2 := i + 2 var i3 := i + chunk_size_x + 1 var i5 := i3 + 2 output_indices.push_back( i3 ) output_indices.push_back( i1 ) output_indices.push_back( i5 ) if j != 0 or (seams & SEAM_LEFT) == 0: output_indices.push_back( i0 ) output_indices.push_back( i1 ) output_indices.push_back( i3 ) if j != n - 1 or (seams & SEAM_RIGHT) == 0: output_indices.push_back( i1 ) output_indices.push_back( i2 ) output_indices.push_back( i5 ) i = i2 return output_indices static func get_mesh_size(width: int, height: int) -> Dictionary: return { "vertices": width * height, "triangles": (width - 1) * (height - 1) * 2 } # Makes a full mesh from a heightmap, without any LOD considerations. # Using this mesh for rendering is very expensive on large terrains. # Initially used as a workaround for Godot to use for navmesh generation. static func make_heightmap_mesh(heightmap: Image, stride: int, scale: Vector3, logger = null) -> Mesh: var size_x := heightmap.get_width() / stride var size_z := heightmap.get_height() / stride assert(size_x >= 2) assert(size_z >= 2) var positions := PackedVector3Array() positions.resize(size_x * size_z) var i := 0 if heightmap.get_format() == Image.FORMAT_RH or heightmap.get_format() == Image.FORMAT_RF: for mz in size_z: for mx in size_x: var x := mx * stride var z := mz * stride var y := heightmap.get_pixel(x, z).r positions[i] = Vector3(x, y, z) * scale i += 1 elif heightmap.get_format() == Image.FORMAT_RGB8: for mz in size_z: for mx in size_x: var x := mx * stride var z := mz * stride var c := heightmap.get_pixel(x, z) var y := HTerrainData.decode_height_from_rgb8_unorm(c) positions[i] = Vector3(x, y, z) * scale i += 1 else: logger.error("Unknown heightmap format!") return null var indices := make_indices(size_x - 1, size_z - 1, 0) var arrays := [] arrays.resize(Mesh.ARRAY_MAX); arrays[Mesh.ARRAY_VERTEX] = positions arrays[Mesh.ARRAY_INDEX] = indices if logger != null: logger.debug(str("Generated mesh has ", len(positions), " vertices and ", len(indices) / 3, " triangles")) var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) return mesh