@tool class_name TerrainControlLayers extends Resource ## Resource containing artist-controlled terrain layers for procedural generation. ## Includes biome maps, height profiles, masks, and slope limits. signal layers_changed @export var resolution: Vector2i = Vector2i(512, 512): set(value): resolution = value emit_changed() layers_changed.emit() @export_group("Biome Control") @export var biome_map: Texture2D: set(value): biome_map = value emit_changed() layers_changed.emit() @export var height_profile: Curve: set(value): if height_profile and height_profile.changed.is_connected(_on_curve_changed): height_profile.changed.disconnect(_on_curve_changed) height_profile = value if height_profile: height_profile.changed.connect(_on_curve_changed) emit_changed() layers_changed.emit() @export_group("Slope Control") @export var slope_limit_map: Texture2D: set(value): slope_limit_map = value emit_changed() layers_changed.emit() @export_range(0.0, 90.0) var default_slope_limit: float = 45.0: set(value): default_slope_limit = value emit_changed() layers_changed.emit() @export_group("Mask Layers") @export var mask_layers: Array[Texture2D] = []: set(value): mask_layers = value emit_changed() layers_changed.emit() @export var mask_names: Array[String] = []: set(value): mask_names = value emit_changed() layers_changed.emit() func _on_curve_changed() -> void: emit_changed() layers_changed.emit() func get_biome_value(uv: Vector2) -> Color: if not biome_map: return Color.WHITE var img := biome_map.get_image() if not img: return Color.WHITE var px := clampi(int(uv.x * img.get_width()), 0, img.get_width() - 1) var py := clampi(int(uv.y * img.get_height()), 0, img.get_height() - 1) return img.get_pixel(px, py) func get_slope_limit(uv: Vector2) -> float: if not slope_limit_map: return default_slope_limit var img := slope_limit_map.get_image() if not img: return default_slope_limit var px := clampi(int(uv.x * img.get_width()), 0, img.get_width() - 1) var py := clampi(int(uv.y * img.get_height()), 0, img.get_height() - 1) return img.get_pixel(px, py).r * 90.0 func get_height_profile_value(normalized_height: float) -> float: if not height_profile: return normalized_height return height_profile.sample(clampf(normalized_height, 0.0, 1.0)) func get_mask_value(mask_index: int, uv: Vector2) -> float: if mask_index < 0 or mask_index >= mask_layers.size(): return 0.0 var tex := mask_layers[mask_index] if not tex: return 0.0 var img := tex.get_image() if not img: return 0.0 var px := clampi(int(uv.x * img.get_width()), 0, img.get_width() - 1) var py := clampi(int(uv.y * img.get_height()), 0, img.get_height() - 1) return img.get_pixel(px, py).r func get_mask_name(mask_index: int) -> String: if mask_index < 0 or mask_index >= mask_names.size(): return "mask_%d" % mask_index return mask_names[mask_index]