@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))