@tool class_name TerrainGenerator extends MeshInstance3D @export var noise_setting: FastNoiseLite: set(value): if noise_setting and noise_setting.changed.is_connected(_on_settings_changed): noise_setting.changed.disconnect(_on_settings_changed) noise_setting = value if noise_setting: noise_setting.changed.connect(_on_settings_changed) _request_update() @export var material_setting: ShaderMaterial: set(value): material_setting = value if mesh: mesh.surface_set_material(0, material_setting) @export_group("Terrain Size") @export var terrain_size: Vector2 = Vector2(200, 200): set(value): terrain_size = value _request_update() @export_range(2, 512) var subdivision: int = 128: set(value): subdivision = value _request_update() @export var max_height: float = 50.0: set(value): max_height = value _request_update() @export_group("Island Mask") @export var island_mask: bool = true: set(value): island_mask = value _request_update() @export_range(0.0, 1.0) var mask_falloff_start: float = 0.5: set(value): mask_falloff_start = value _request_update() @export_range(0.1, 2.0) var mask_power: float = 1.5: set(value): mask_power = value _request_update() @export_group("Actions") @export var regenerate: bool = false: set(_value): generate_terrain() @export var bake_collision: bool = false: set(_value): _bake_collision() var _update_queued: bool = false func _ready() -> void: if noise_setting: if not noise_setting.changed.is_connected(_on_settings_changed): noise_setting.changed.connect(_on_settings_changed) generate_terrain() func _on_settings_changed() -> void: _request_update() func _request_update() -> void: if _update_queued: return _update_queued = true call_deferred("_do_update") func _do_update() -> void: _update_queued = false generate_terrain() func generate_terrain() -> void: if not noise_setting: return var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) var step_x: float = terrain_size.x / float(subdivision - 1) var step_z: float = terrain_size.y / float(subdivision - 1) var half_x: float = terrain_size.x / 2.0 var half_z: float = terrain_size.y / 2.0 var heights: Array[float] = [] heights.resize(subdivision * subdivision) for z in range(subdivision): for x in range(subdivision): var world_x: float = x * step_x - half_x var world_z: float = z * step_z - half_z var height: float = noise_setting.get_noise_2d(world_x, world_z) * max_height if island_mask: height *= _calculate_island_mask(world_x, world_z, half_x, half_z) heights[z * subdivision + x] = height for z in range(subdivision - 1): for x in range(subdivision - 1): var idx00: int = z * subdivision + x var idx10: int = z * subdivision + (x + 1) var idx01: int = (z + 1) * subdivision + x var idx11: int = (z + 1) * subdivision + (x + 1) var x0: float = x * step_x - half_x var x1: float = (x + 1) * step_x - half_x var z0: float = z * step_z - half_z var z1: float = (z + 1) * step_z - half_z var v00 := Vector3(x0, heights[idx00], z0) var v10 := Vector3(x1, heights[idx10], z0) var v01 := Vector3(x0, heights[idx01], z1) var v11 := Vector3(x1, heights[idx11], z1) # Triangle 1: v00, v01, v10 var normal1: Vector3 = (v01 - v00).cross(v10 - v00).normalized() st.set_normal(normal1) st.set_uv(Vector2(x0 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5)) st.add_vertex(v00) st.set_uv(Vector2(x0 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5)) st.add_vertex(v01) st.set_uv(Vector2(x1 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5)) st.add_vertex(v10) # Triangle 2: v10, v01, v11 var normal2: Vector3 = (v01 - v10).cross(v11 - v10).normalized() st.set_normal(normal2) st.set_uv(Vector2(x1 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5)) st.add_vertex(v10) st.set_uv(Vector2(x0 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5)) st.add_vertex(v01) st.set_uv(Vector2(x1 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5)) st.add_vertex(v11) mesh = st.commit() if material_setting: mesh.surface_set_material(0, material_setting) func _calculate_island_mask(x: float, z: float, half_x: float, half_z: float) -> float: var nx: float = x / half_x var nz: float = z / half_z var dist: float = sqrt(nx * nx + nz * nz) if dist < mask_falloff_start: return 1.0 elif dist > 1.0: return 0.0 else: var t: float = (dist - mask_falloff_start) / (1.0 - mask_falloff_start) return pow(1.0 - t, mask_power) func _bake_collision() -> void: if not mesh: push_warning("TerrainGenerator: No mesh to bake collision from") return var static_body: StaticBody3D var collision_shape: CollisionShape3D for child in get_children(): if child is StaticBody3D: static_body = child break if not static_body: static_body = StaticBody3D.new() static_body.name = "TerrainCollision" add_child(static_body) if Engine.is_editor_hint(): static_body.owner = get_tree().edited_scene_root for child in static_body.get_children(): if child is CollisionShape3D: collision_shape = child break if not collision_shape: collision_shape = CollisionShape3D.new() collision_shape.name = "CollisionShape" static_body.add_child(collision_shape) if Engine.is_editor_hint(): collision_shape.owner = get_tree().edited_scene_root collision_shape.shape = mesh.create_trimesh_shape() print("TerrainGenerator: Collision baked successfully")