extends Node3D # Оформление хаба: пол с кольцами, колонны, потолочные светильники, окна с ночным городом, лавки, # платформа с вращающейся голограммой в центре, пылинки в воздухе. Размеры берутся из hub.gd. # Всё строится кодом и без тяжёлых ресурсов — хаб лежит в основном пакете игры. const BRASS := Color(0.85, 0.58, 0.26) const CYAN := Color(0.25, 0.85, 1.0) const DARK := Color(0.045, 0.05, 0.065) const FLOOR_SHADER := """ shader_type spatial; varying vec3 wpos; void vertex() { wpos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; } void fragment() { vec2 cell = wpos.xz / 2.0; vec2 g = abs(fract(cell - 0.5) - 0.5) / fwidth(cell); float line = 1.0 - min(min(g.x, g.y), 1.0); float r = length(wpos.xz); float ring = 1.0 - smoothstep(0.0, 0.09, min(abs(r - 7.5), abs(r - 12.5))); // дорожка света от платформы к порталам float lane = 1.0 - smoothstep(0.0, 0.07, abs(wpos.x)) ; lane *= step(wpos.z, -4.0) * 0.8; vec3 base = vec3(0.028, 0.032, 0.045); vec3 col = mix(base, vec3(0.08, 0.5, 0.65), line * 0.55); col = mix(col, vec3(0.9, 0.58, 0.24), clamp(ring + lane, 0.0, 1.0)); ALBEDO = col * 0.55; EMISSION = vec3(0.03, 0.25, 0.34) * line + vec3(0.5, 0.3, 0.1) * clamp(ring + lane, 0.0, 1.0); ROUGHNESS = 0.35; METALLIC = 0.4; } """ # Окно: ночной город — небо с зарёй у горизонта, силуэты домов со светящимися окнами const CITY_SHADER := """ shader_type spatial; render_mode unshaded; float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } void fragment() { float yb = 1.0 - UV.y; vec3 col = mix(vec3(0.015, 0.02, 0.06), vec3(0.42, 0.14, 0.30), pow(yb, 2.2) * 0.9); col += vec3(0.5, 0.25, 0.1) * smoothstep(0.35, 0.0, yb) * 0.6; for (int layer = 0; layer < 2; layer++) { float n = layer == 0 ? 14.0 : 24.0; float id = floor(UV.x * n + float(layer) * 7.0); float top = (layer == 0 ? 0.42 : 0.26) * (0.35 + 0.65 * h(vec2(id, float(layer) + 1.0))); if (yb < top) { vec3 body = layer == 0 ? vec3(0.02, 0.02, 0.035) : vec3(0.05, 0.03, 0.07); vec2 wc = vec2(UV.x * n * 3.0, yb * (layer == 0 ? 34.0 : 22.0)); vec2 wi = floor(wc); vec2 wf = fract(wc); float lit = step(0.62, h(wi + id)) * step(0.2, wf.x) * step(wf.x, 0.8) * step(0.25, wf.y) * step(wf.y, 0.75); vec3 wcol = h(wi + 3.0) > 0.55 ? vec3(1.0, 0.7, 0.3) : vec3(0.4, 0.8, 1.0); col = body + wcol * lit * (layer == 0 ? 0.9 : 0.5); break; } } ALBEDO = col; } """ var _spin: Array = [] # [узел, ось, скорость] var _pulse_mat: StandardMaterial3D var _t := 0.0 func build(width: float, depth: float, height: float) -> void: _build_pillars(width, depth, height) _build_ceiling(width, depth, height) _build_windows(width, depth, height) _build_benches(width, depth) _build_dais() _build_signs(width, depth, height) _build_dust(width, depth, height) func floor_material() -> ShaderMaterial: var m := ShaderMaterial.new() m.shader = Shader.new() m.shader.code = FLOOR_SHADER return m func _process(delta: float) -> void: _t += delta for s in _spin: (s[0] as Node3D).rotate(s[1], s[2] * delta) if _pulse_mat: _pulse_mat.emission_energy_multiplier = 1.6 + 0.5 * sin(_t * 1.6) # ---------- вспомогательное ---------- func _emissive(c: Color, energy := 2.0) -> StandardMaterial3D: var m := StandardMaterial3D.new() m.albedo_color = c.darkened(0.6) m.emission_enabled = true m.emission = c m.emission_energy_multiplier = energy return m func _matte(c: Color, metallic := 0.0, rough := 0.7) -> StandardMaterial3D: var m := StandardMaterial3D.new() m.albedo_color = c m.metallic = metallic m.roughness = rough return m func _box(size: Vector3, pos: Vector3, mat: Material, collide := false) -> MeshInstance3D: var mesh := BoxMesh.new() mesh.size = size mesh.material = mat var mi := MeshInstance3D.new() mi.mesh = mesh mi.position = pos mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(mi) if collide: var body := StaticBody3D.new() var cs := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = size cs.shape = shape body.add_child(cs) mi.add_child(body) return mi func _label(text: String, size: float, color: Color, pos: Vector3, yaw := 0.0) -> Label3D: var l := Label3D.new() l.text = text l.font_size = 64 l.pixel_size = size l.outline_size = 10 l.modulate = color l.shaded = false l.position = pos l.rotation.y = yaw add_child(l) return l # ---------- колонны ---------- func _build_pillars(width: float, depth: float, height: float) -> void: var steel := _matte(Color(0.07, 0.075, 0.09), 0.6, 0.4) var glow := _emissive(CYAN, 2.2) var brass := _emissive(BRASS, 1.4) var px := width * 0.5 - 8.0 for sx in [-1.0, 1.0]: for z in [-6.0, 2.0, 10.0]: var p := Vector3(sx * px, 0.0, z) _box(Vector3(1.1, height, 1.1), p + Vector3(0, height * 0.5, 0), steel, true) _box(Vector3(1.2, 0.18, 1.2), p + Vector3(0, 0.09, 0), brass) _box(Vector3(1.2, 0.18, 1.2), p + Vector3(0, height - 0.09, 0), brass) # светящаяся прорезь на каждой грани по высоте _box(Vector3(0.12, height - 1.6, 1.14), p + Vector3(0, height * 0.5, 0), glow) _box(Vector3(1.14, height - 1.6, 0.12), p + Vector3(0, height * 0.5, 0), glow) # ---------- потолок ---------- func _build_ceiling(width: float, depth: float, height: float) -> void: var strip := _emissive(Color(1.0, 0.86, 0.62), 2.4) var y := height - 0.08 for x in [-18.0, -9.0, 0.0, 9.0, 18.0]: _box(Vector3(0.35, 0.12, depth - 4.0), Vector3(x, y, 0), strip) for z in [-8.0, 0.0, 8.0]: _box(Vector3(width - 4.0, 0.1, 0.22), Vector3(0, y + 0.02, z), _emissive(BRASS, 1.2)) # большое кольцо над платформой var ring := TorusMesh.new() ring.inner_radius = 3.9 ring.outer_radius = 4.1 ring.material = _emissive(CYAN, 2.0) var mi := MeshInstance3D.new() mi.mesh = ring mi.position = Vector3(0, height - 0.5, 0) mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(mi) # мягкий свет зон: тёплый у порталов, холодный в центре for x in [-14.0, 14.0]: var l := OmniLight3D.new() l.position = Vector3(x, height - 1.0, 4.0) l.omni_range = 20.0 l.light_energy = 0.7 l.light_color = Color(1.0, 0.82, 0.6) l.shadow_enabled = false add_child(l) # ---------- окна в боковых стенах ---------- func _build_windows(width: float, depth: float, height: float) -> void: var city := ShaderMaterial.new() city.shader = Shader.new() city.shader.code = CITY_SHADER var frame := _matte(Color(0.05, 0.05, 0.06), 0.7, 0.35) var brass := _emissive(BRASS, 0.9) var hw := width * 0.5 var win := Vector2(8.5, 5.6) var y := 4.6 for side in [-1, 1]: var yaw := PI * 0.5 * float(-side) # лицом в комнату var x := float(side) * (hw - 0.04) for z in [-8.5, 0.5, 9.5]: var q := QuadMesh.new() q.size = win q.material = city var mi := MeshInstance3D.new() mi.mesh = q mi.position = Vector3(x, y, z) mi.rotation.y = yaw mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(mi) # рама и переплёты var inner_x := float(side) * (hw - 0.12) _box(Vector3(0.1, win.y, 0.18), Vector3(inner_x, y, z - win.x * 0.25), frame) _box(Vector3(0.1, win.y, 0.18), Vector3(inner_x, y, z + win.x * 0.25), frame) _box(Vector3(0.1, 0.18, win.x), Vector3(inner_x, y - 0.5, z), frame) for e in [-1.0, 1.0]: _box(Vector3(0.12, win.y + 0.5, 0.2), Vector3(inner_x, y, z + e * (win.x * 0.5 + 0.1)), frame) var wl := OmniLight3D.new() # отсвет города на полу и лавках wl.position = Vector3(float(side) * (hw - 3.0), y - 1.0, z) wl.light_color = Color(0.75, 0.45, 0.9) wl.light_energy = 1.1 wl.omni_range = 11.0 wl.shadow_enabled = false add_child(wl) _box(Vector3(0.12, 0.08, win.x + 0.5), Vector3(inner_x, y - win.y * 0.5 - 0.3, z), brass) _box(Vector3(0.12, 0.08, win.x + 0.5), Vector3(inner_x, y + win.y * 0.5 + 0.3, z), brass) # ---------- лавки ---------- func _build_benches(width: float, _depth: float) -> void: var seat := _matte(Color(0.16, 0.08, 0.06), 0.0, 0.55) var metal := _matte(Color(0.08, 0.08, 0.1), 0.8, 0.3) var x := width * 0.5 - 2.0 for side in [-1.0, 1.0]: for z in [-8.5, 0.5, 9.5]: var base := Vector3(side * x, 0.0, z) _box(Vector3(1.0, 0.45, 5.0), base + Vector3(0, 0.225, 0), seat, true) _box(Vector3(0.25, 0.9, 5.0), base + Vector3(side * 0.5, 0.7, 0), seat) _box(Vector3(0.95, 0.08, 4.8), base + Vector3(0, 0.02, 0), metal) _box(Vector3(0.05, 0.04, 4.8), base + Vector3(-side * 0.5, 0.3, 0), _emissive(BRASS, 1.0)) # ---------- центр: платформа и голограмма ---------- func _build_dais() -> void: var body := StaticBody3D.new() add_child(body) var cyl := CylinderMesh.new() cyl.top_radius = 3.6 cyl.bottom_radius = 3.7 cyl.height = 0.32 cyl.material = _matte(Color(0.06, 0.065, 0.08), 0.7, 0.3) var mi := MeshInstance3D.new() mi.mesh = cyl mi.position.y = 0.16 mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF body.add_child(mi) var cs := CollisionShape3D.new() var shape := CylinderShape3D.new() shape.radius = 3.65 shape.height = 0.32 cs.shape = shape cs.position.y = 0.16 body.add_child(cs) var rim := TorusMesh.new() rim.inner_radius = 3.45 rim.outer_radius = 3.6 rim.material = _emissive(BRASS, 1.8) var rim_mi := MeshInstance3D.new() rim_mi.mesh = rim rim_mi.position.y = 0.33 rim_mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(rim_mi) _pulse_mat = _emissive(CYAN, 2.0) var holo := Node3D.new() holo.position = Vector3(0, 2.6, 0) add_child(holo) var core := SphereMesh.new() core.radius = 0.5 core.height = 1.0 core.material = _pulse_mat var core_mi := MeshInstance3D.new() core_mi.mesh = core core_mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF holo.add_child(core_mi) var specs := [[1.1, CYAN, Vector3(1, 0, 0), 0.7], [1.5, BRASS, Vector3(0, 0, 1), -0.5], [1.9, CYAN, Vector3(0, 1, 0), 0.35]] for i in specs.size(): var t := TorusMesh.new() t.inner_radius = specs[i][0] - 0.035 t.outer_radius = specs[i][0] + 0.035 t.rings = 48 t.material = _emissive(specs[i][1], 2.2) var pivot := Node3D.new() holo.add_child(pivot) pivot.rotation = Vector3(0.4 * i, 0.9 * i, 0.0) var tm := MeshInstance3D.new() tm.mesh = t tm.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF pivot.add_child(tm) _spin.append([pivot, specs[i][2], specs[i][3]]) var light := OmniLight3D.new() light.position = Vector3(0, 2.6, 0) light.light_color = CYAN light.light_energy = 1.4 light.omni_range = 11.0 light.shadow_enabled = false add_child(light) var l := Label3D.new() l.text = "AMAN'S WORLD" l.billboard = BaseMaterial3D.BILLBOARD_ENABLED l.font_size = 64 l.pixel_size = 0.009 l.outline_size = 12 l.modulate = Color(1.0, 0.9, 0.7) l.shaded = false l.position = Vector3(0, 4.9, 0) add_child(l) # ---------- вывески ---------- func _build_signs(width: float, depth: float, height: float) -> void: var hd := depth * 0.5 # южная стена: «лифтовая» панель этажа _box(Vector3(10.0, 2.2, 0.15), Vector3(0, 5.0, hd - 0.08), _matte(Color(0.03, 0.03, 0.04), 0.5, 0.4)) _box(Vector3(10.3, 0.08, 0.2), Vector3(0, 6.15, hd - 0.1), _emissive(BRASS, 1.2)) _box(Vector3(10.3, 0.08, 0.2), Vector3(0, 3.85, hd - 0.1), _emissive(BRASS, 1.2)) _label("ЛОББИ", 0.02, Color(1.0, 0.82, 0.5), Vector3(0, 5.4, hd - 0.2), PI) _label("ЭТАЖ 0 · ВСЕ КАРТЫ ВНИЗУ ПО КОРИДОРУ", 0.008, Color(0.7, 0.9, 1.0), Vector3(0, 4.4, hd - 0.2), PI) # северная стена: фриз над порталами _box(Vector3(width - 6.0, 0.12, 0.3), Vector3(0, 8.9, -hd + 0.15), _emissive(BRASS, 1.3)) _box(Vector3(width - 6.0, 0.12, 0.3), Vector3(0, 8.0, -hd + 0.15), _emissive(BRASS, 1.3)) # ---------- пыль в лучах света ---------- func _build_dust(width: float, depth: float, height: float) -> void: var p := GPUParticles3D.new() p.amount = 140 p.lifetime = 14.0 p.preprocess = 14.0 p.visibility_aabb = AABB(Vector3(-width * 0.5, 0, -depth * 0.5), Vector3(width, height, depth)) var pm := ParticleProcessMaterial.new() pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX pm.emission_box_extents = Vector3(width * 0.5 - 1.0, height * 0.5 - 0.5, depth * 0.5 - 1.0) pm.gravity = Vector3.ZERO pm.direction = Vector3(0.2, 1.0, 0.1) pm.spread = 180.0 pm.initial_velocity_min = 0.05 pm.initial_velocity_max = 0.25 p.process_material = pm var q := QuadMesh.new() q.size = Vector2(0.05, 0.05) var m := StandardMaterial3D.new() m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED m.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA m.albedo_color = Color(1.0, 0.9, 0.7, 0.35) q.material = m p.draw_pass_1 = q p.position = Vector3(0, height * 0.5, 0) p.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(p)