Лес на MultiMesh, киберпанк-птицы у башни, левитация обелиска
- Деревья: упрощённая модель, MultiMesh, коллизия стволов - Птицы кружат вокруг Tower_01 (шейдерные крылья, свечение) - Обелиск увеличен, поднят и плавно левитирует Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+127
@@ -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
|
||||
Reference in New Issue
Block a user