@tool extends Node3D # Лес одним MultiMesh: одна отрисовка на все деревья, удобно для веба. @export var tree_scene: PackedScene: set(v): tree_scene = v _queue_rebuild() @export var count := 60: set(v): count = v _queue_rebuild() @export var area_radius := 220.0 # Радиус леса вокруг центра @export var spawn_clear_radius := 12.0 # Свободная зона вокруг старта @export var building_clear_radius := 14.0 # Свободная зона вокруг построек @export var path_clear_radius := 7.0 # Свободная зона вокруг пути камня @export var scale_range := Vector2(1.3, 2.0) @export var seed_value := 1: set(v): seed_value = v _queue_rebuild() @export var visible_distance := 160.0 # Дальше — деревья не рисуются @export var trunk_radius := 1.4 # Коллизия ствола (до масштабирования) @export var trunk_height := 9.0 @export var cast_shadows := false # Тени от 150 деревьев тяжелы для веба func _ready() -> void: _rebuild() func _queue_rebuild() -> void: if is_node_ready(): _rebuild.call_deferred() func _rebuild() -> void: if tree_scene == null: return for n in ["Trees", "TrunkColliders"]: var old := get_node_or_null(n) if old: remove_child(old) old.queue_free() var inst := tree_scene.instantiate() var mi := _find_mesh(inst) if mi == null: push_error("forest: в tree_scene нет MeshInstance3D") return var mesh := mi.mesh var base := mi.transform # смещение меша внутри glb var keep_out := _collect_keep_out() var rng := RandomNumberGenerator.new() rng.seed = seed_value var xforms: Array[Transform3D] = [] var tries := 0 while xforms.size() < count and tries < count * 30: tries += 1 var p := Vector2.from_angle(rng.randf() * TAU) * sqrt(rng.randf()) * area_radius if p.length() < spawn_clear_radius or _blocked(p, keep_out): continue var s := rng.randf_range(scale_range.x, scale_range.y) var b := Basis(Vector3.UP, rng.randf() * TAU).scaled(Vector3(s, s * rng.randf_range(0.9, 1.15), s)) xforms.append(Transform3D(b, Vector3(p.x, 0.0, p.y)) * base) var mm := MultiMesh.new() mm.transform_format = MultiMesh.TRANSFORM_3D mm.mesh = mesh mm.instance_count = xforms.size() for i in xforms.size(): mm.set_instance_transform(i, xforms[i]) var mmi := MultiMeshInstance3D.new() mmi.name = "Trees" mmi.multimesh = mm mmi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF mmi.visibility_range_end = visible_distance mmi.visibility_range_end_margin = 20.0 mmi.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED mmi.custom_aabb = AABB(Vector3(-area_radius - 20, -5, -area_radius - 20), Vector3(area_radius * 2 + 40, 60, area_radius * 2 + 40)) add_child(mmi) inst.free() if not Engine.is_editor_hint(): _add_colliders(xforms) # Мультимеш коллизий не даёт — ставим по цилиндру на ствол каждого дерева func _add_colliders(xforms: Array[Transform3D]) -> void: var body := StaticBody3D.new() body.name = "TrunkColliders" add_child(body) for t in xforms: var s := t.basis.get_scale() var shape := CylinderShape3D.new() shape.radius = trunk_radius * s.x shape.height = trunk_height * s.y var cs := CollisionShape3D.new() cs.shape = shape cs.position = Vector3(t.origin.x, t.origin.y + shape.height * 0.5, t.origin.z) body.add_child(cs) func _find_mesh(n: Node) -> MeshInstance3D: if n is MeshInstance3D: return n for c in n.get_children(): var r := _find_mesh(c) if r: return r return null # Круги (x, z, радиус), куда деревья ставить нельзя func _collect_keep_out() -> Array[Vector3]: var out: Array[Vector3] = [] var arch := get_node_or_null("../Architecture") if arch: for c in arch.get_children(): if c is Path3D: for pt in c.curve.get_baked_points(): var g: Vector3 = c.to_global(pt) out.append(Vector3(g.x, g.z, path_clear_radius)) elif c is Node3D: out.append(Vector3(c.global_position.x, c.global_position.z, building_clear_radius)) return out func _blocked(p: Vector2, keep_out: Array[Vector3]) -> bool: for k in keep_out: if p.distance_squared_to(Vector2(k.x, k.y)) < k.z * k.z: return true return false