diff --git a/World/birds.gd b/World/birds.gd new file mode 100644 index 0000000..7d7760f --- /dev/null +++ b/World/birds.gd @@ -0,0 +1,116 @@ +@tool +extends Node3D +# Киберпанк-птицы кружат вокруг цели. Один MultiMesh, взмахи крыльев считает шейдер. + +@export var target_path: NodePath + +var target: Node3D +@export var count := 12 +@export var min_radius := 55.0 +@export var max_radius := 100.0 +@export var min_height := 15.0 +@export var max_height := 150.0 +@export var speed_range := Vector2(0.15, 0.35) # рад/с вокруг цели +@export var bird_scale := 1.6 +@export var seed_value := 7 + +const SHADER := """ +shader_type spatial; +render_mode cull_disabled, unshaded, fog_disabled; +uniform float flap_speed = 9.0; +uniform float flap_amp = 0.9; +varying vec3 col; +void vertex() { + float ph = INSTANCE_CUSTOM.x; + // COLOR.r — вес крыла (0 у тела, 1 на кончике) + VERTEX.y += sin(TIME * flap_speed * (0.8 + INSTANCE_CUSTOM.y * 0.4) + ph) * flap_amp * COLOR.r; + VERTEX.z += COLOR.r * 0.25 * cos(TIME * flap_speed + ph); + col = INSTANCE_CUSTOM.z > 0.5 ? vec3(1.0, 0.1, 0.8) : vec3(0.0, 0.95, 1.0); +} +void fragment() { + // Светлее к кончикам крыльев — HDR для свечения + ALBEDO = col * (5.0 + 3.0 * COLOR.r); +} +""" + +var _mm: MultiMesh +var _orbit: Array[Dictionary] = [] + +func _ready() -> void: + target = get_node_or_null(target_path) as Node3D + _build() + +func _build() -> void: + var rng := RandomNumberGenerator.new() + rng.seed = seed_value + + var mat := ShaderMaterial.new() + var sh := Shader.new() + sh.code = SHADER + mat.shader = sh + + _mm = MultiMesh.new() + _mm.transform_format = MultiMesh.TRANSFORM_3D + _mm.use_custom_data = true + _mm.mesh = _make_mesh() + _mm.mesh.surface_set_material(0, mat) + _mm.instance_count = count + for i in count: + _orbit.append({ + "r": rng.randf_range(min_radius, max_radius), + "h": rng.randf_range(min_height, max_height), + "w": rng.randf_range(speed_range.x, speed_range.y) * (1.0 if rng.randf() > 0.3 else -1.0), + "a": rng.randf() * TAU, + "bob": rng.randf() * TAU, + }) + _mm.set_instance_custom_data(i, Color(rng.randf() * TAU, rng.randf(), 1.0 if rng.randf() < 0.3 else 0.0, 0.0)) + + var mmi := MultiMeshInstance3D.new() + mmi.name = "Flock" + mmi.multimesh = _mm + mmi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + mmi.custom_aabb = AABB(Vector3(-400, -50, -400), Vector3(800, 400, 800)) + mmi.top_level = true # координаты глобальные + add_child(mmi) + _update(0.0) + +# Угловатая птица: тело-ромб, хвост и два крыла-«клинка» (вес крыла в цвете вершины) +func _make_mesh() -> ArrayMesh: + var st := SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + var tris := [ + # [вершина, вес крыла] + # тело (носом в -Z) + [Vector3(0, 0, -1.2), 0.0], [Vector3(-0.25, 0, 0.3), 0.0], [Vector3(0.25, 0, 0.3), 0.0], + [Vector3(0, 0.2, 0.0), 0.0], [Vector3(-0.25, 0, 0.3), 0.0], [Vector3(0, 0, -1.2), 0.0], + [Vector3(0, 0.2, 0.0), 0.0], [Vector3(0, 0, -1.2), 0.0], [Vector3(0.25, 0, 0.3), 0.0], + # хвост + [Vector3(-0.2, 0, 0.3), 0.0], [Vector3(0, 0, 1.4), 0.0], [Vector3(0.2, 0, 0.3), 0.0], + # левое крыло + [Vector3(-0.2, 0, -0.5), 0.0], [Vector3(-2.6, 0, 0.5), 1.0], [Vector3(-0.2, 0, 0.4), 0.0], + # правое крыло + [Vector3(0.2, 0, -0.5), 0.0], [Vector3(0.2, 0, 0.4), 0.0], [Vector3(2.6, 0, 0.5), 1.0], + ] + for t in tris: + st.set_color(Color(t[1], 0, 0)) + st.add_vertex(t[0]) + return st.commit() + +func _process(delta: float) -> void: + _update(delta) + +func _update(delta: float) -> void: + if _mm == null or target == null: + return + var c := target.global_position + var t := Time.get_ticks_msec() * 0.001 + for i in _orbit.size(): + var o: Dictionary = _orbit[i] + o["a"] += o["w"] * delta + var a: float = o["a"] + var pos := c + Vector3(cos(a) * o["r"], o["h"] + sin(t * 0.7 + o["bob"]) * 3.0, sin(a) * o["r"]) + # Летим по касательной, лёгкий крен в сторону поворота + var dir := Vector3(-sin(a), 0, cos(a)) * signf(o["w"]) + var b := Basis.looking_at(dir, Vector3.UP) + b = b.rotated(dir, -0.35 * signf(o["w"])).scaled(Vector3.ONE * bird_scale) + _mm.set_instance_transform(i, Transform3D(b, pos)) diff --git a/World/birds.gd.uid b/World/birds.gd.uid new file mode 100644 index 0000000..b776fc4 --- /dev/null +++ b/World/birds.gd.uid @@ -0,0 +1 @@ +uid://j5ultotjrstk diff --git a/World/forest.gd b/World/forest.gd new file mode 100644 index 0000000..678127c --- /dev/null +++ b/World/forest.gd @@ -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 diff --git a/World/forest.gd.uid b/World/forest.gd.uid new file mode 100644 index 0000000..f2684de --- /dev/null +++ b/World/forest.gd.uid @@ -0,0 +1 @@ +uid://cx16mgkh4bldx diff --git a/World/levitate.gd b/World/levitate.gd new file mode 100644 index 0000000..5bad1bc --- /dev/null +++ b/World/levitate.gd @@ -0,0 +1,19 @@ +@tool +extends Node3D +# Плавное парение вверх-вниз вокруг стартовой высоты. + +@export var amplitude := 4.0 +@export var period := 7.0 + +var _base_y: float +var _t := 0.0 + +func _ready() -> void: + _base_y = position.y + +func _process(delta: float) -> void: + _t += delta + if Engine.is_editor_hint(): + position.y = _base_y # в редакторе не двигаем, чтобы не сохранить смещение + return + position.y = _base_y + sin(_t * TAU / period) * amplitude diff --git a/World/levitate.gd.uid b/World/levitate.gd.uid new file mode 100644 index 0000000..55183a7 --- /dev/null +++ b/World/levitate.gd.uid @@ -0,0 +1 @@ +uid://chtck5eaaddrw diff --git a/main.tscn b/main.tscn index 058f1a8..767c0a0 100644 --- a/main.tscn +++ b/main.tscn @@ -8,7 +8,10 @@ [ext_resource type="PackedScene" uid="uid://dr2udwp3pu8g1" path="res://models/house_02/house_02-col.glb" id="6_5vw27"] [ext_resource type="AudioStream" uid="uid://ddsynauqeoppw" path="res://World/tron_01.ogg" id="7_272bh"] [ext_resource type="AudioStream" uid="uid://280pvcvxt2n2" path="res://models/Bochi-Rock/Guitar.ogg" id="7_dg77c"] -[ext_resource type="PackedScene" uid="uid://cqfe5g5ospxic" path="res://models/Tree_01/Tree_01.glb" id="8_5vw27"] +[ext_resource type="PackedScene" path="res://models/Tree_01/Tree_01_low.glb" id="13_treelow"] +[ext_resource type="Script" path="res://World/forest.gd" id="12_forest"] +[ext_resource type="Script" path="res://World/birds.gd" id="14_birds"] +[ext_resource type="Script" path="res://World/levitate.gd" id="15_levitate"] [ext_resource type="PackedScene" uid="uid://evml2s8rmerw" path="res://models/obelisk_01/Obelisk.glb" id="9_kek77"] [ext_resource type="Script" uid="uid://b3ckck8y0rby6" path="res://models/Bochi-Rock/rock-path.gd" id="10_4c57u"] [ext_resource type="PackedScene" uid="uid://cowp33ejxcfcl" path="res://models/Bochi-Rock/Rock.glb" id="11_efxa6"] @@ -59,7 +62,10 @@ transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -12. transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14.864338, -0.0029354095, 18.549324) [node name="Obelisk" parent="Architecture" unique_id=94486605 instance=ExtResource("9_kek77")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 78.88046, -2.6785774, 38.156742) +transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 78.88046, 25.0, 38.156742) +script = ExtResource("15_levitate") +amplitude = 5.0 +period = 8.0 [node name="rock" type="Path3D" parent="Architecture" unique_id=1419147983] curve = SubResource("Curve3D_dg77c") @@ -102,18 +108,17 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.63628197, -0.002937317, 0. metadata/_edit_lock_ = true [node name="Nature" type="Node3D" parent="." unique_id=950189180] +script = ExtResource("12_forest") +tree_scene = ExtResource("13_treelow") -[node name="Tree_01" parent="Nature" unique_id=1867241468 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13.709265, 0.24313244, -13.908665) - -[node name="Tree_01" parent="Nature/Tree_01" unique_id=179570336 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13.335356, 0.24313053, 59.155888) - -[node name="Tree_01" parent="Nature/Tree_01/Tree_01" unique_id=230889573 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39.854347, 0.2431305, 12.051704) - -[node name="Tree_02" parent="Nature/Tree_01/Tree_01" unique_id=1966462925 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -43.224022, 0.2431305, -92.65268) +[node name="Birds" type="Node3D" parent="." unique_id=770000001] +script = ExtResource("14_birds") +target_path = NodePath("../Architecture/Tower_01") +min_radius = 18.0 +max_radius = 32.0 +min_height = 6.0 +max_height = 55.0 +bird_scale = 0.9 [node name="CharacterBody3D" parent="." unique_id=365865796 instance=ExtResource("2_h2yge")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.0830812, 2.6370785, 0.44329834) diff --git a/models/Tree_01/Tree_01_low.glb b/models/Tree_01/Tree_01_low.glb new file mode 100644 index 0000000..f04d7db Binary files /dev/null and b/models/Tree_01/Tree_01_low.glb differ diff --git a/models/Tree_01/Tree_01_low.glb.import b/models/Tree_01/Tree_01_low.glb.import new file mode 100644 index 0000000..8f0a85a --- /dev/null +++ b/models/Tree_01/Tree_01_low.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b6d4btx7xn51j" +path="res://.godot/imported/Tree_01_low.glb-80917d9d74dea44cbc99751337404631.scn" + +[deps] + +source_file="res://models/Tree_01/Tree_01_low.glb" +dest_files=["res://.godot/imported/Tree_01_low.glb-80917d9d74dea44cbc99751337404631.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1