Лес на MultiMesh, киберпанк-птицы у башни, левитация обелиска

- Деревья: упрощённая модель, MultiMesh, коллизия стволов
- Птицы кружат вокруг Tower_01 (шейдерные крылья, свечение)
- Обелиск увеличен, поднят и плавно левитирует

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 09:29:15 +05:00
co-authored by Claude Sonnet 5.5
parent 4c4d0403cf
commit d759cf6a54
9 changed files with 328 additions and 13 deletions
+127
View File
@@ -0,0 +1,127 @@
@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