aether-engine 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. aether/__init__.py +54 -0
  2. aether/audio/__init__.py +0 -0
  3. aether/audio/audio_manager.py +211 -0
  4. aether/audio/music.py +6 -0
  5. aether/audio/sound.py +6 -0
  6. aether/core/__init__.py +211 -0
  7. aether/core/achievement_system.py +304 -0
  8. aether/core/application.py +19 -0
  9. aether/core/config.py +75 -0
  10. aether/core/console.py +366 -0
  11. aether/core/engine.py +50 -0
  12. aether/core/event_bus.py +24 -0
  13. aether/core/input.py +58 -0
  14. aether/core/plugin_system.py +93 -0
  15. aether/core/save_system.py +409 -0
  16. aether/core/time_manager.py +25 -0
  17. aether/core/window.py +57 -0
  18. aether/graphics/__init__.py +0 -0
  19. aether/graphics/camera.py +69 -0
  20. aether/graphics/framebuffer.py +119 -0
  21. aether/graphics/light.py +32 -0
  22. aether/graphics/lod_system.py +170 -0
  23. aether/graphics/material.py +25 -0
  24. aether/graphics/mesh.py +83 -0
  25. aether/graphics/mesh_factory.py +67 -0
  26. aether/graphics/particle_system.py +268 -0
  27. aether/graphics/post_process.py +279 -0
  28. aether/graphics/renderer.py +135 -0
  29. aether/graphics/shader.py +61 -0
  30. aether/graphics/shader_library.py +157 -0
  31. aether/graphics/sky_atmosphere.py +309 -0
  32. aether/graphics/terrain.py +408 -0
  33. aether/graphics/texture.py +34 -0
  34. aether/graphics/water.py +432 -0
  35. aether/physics/__init__.py +0 -0
  36. aether/physics/bullet_backend.py +395 -0
  37. aether/physics/collider.py +22 -0
  38. aether/physics/physics_world.py +436 -0
  39. aether/physics/raycast.py +25 -0
  40. aether/physics/rigidbody.py +7 -0
  41. aether/resources/__init__.py +0 -0
  42. aether/resources/asset_database.py +76 -0
  43. aether/resources/resource_manager.py +147 -0
  44. aether/resources/serialization.py +114 -0
  45. aether/scene/__init__.py +0 -0
  46. aether/scene/component.py +368 -0
  47. aether/scene/entity.py +46 -0
  48. aether/scene/scene.py +33 -0
  49. aether/scene/scene_graph.py +252 -0
  50. aether/scene/scene_manager.py +191 -0
  51. aether/ui/__init__.py +0 -0
  52. aether/ui/button.py +297 -0
  53. aether/ui/canvas.py +11 -0
  54. aether/ui/text.py +286 -0
  55. aether/utils/__init__.py +0 -0
  56. aether/utils/logger.py +16 -0
  57. aether/utils/math_utils.py +31 -0
  58. aether/utils/object_pool.py +137 -0
  59. aether/utils/profiler.py +187 -0
  60. aether_engine-1.0.0.dist-info/METADATA +71 -0
  61. aether_engine-1.0.0.dist-info/RECORD +64 -0
  62. aether_engine-1.0.0.dist-info/WHEEL +5 -0
  63. aether_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
  64. aether_engine-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,32 @@
1
+ """Источники света"""
2
+ import numpy as np
3
+
4
+
5
+ class DirectionalLight:
6
+ def __init__(self, direction=(0, -1, 0), color=(1,1,1), intensity=1.0):
7
+ self.direction = np.array(direction, dtype=np.float32)
8
+ self.color = np.array(color, dtype=np.float32)
9
+ self.intensity = intensity
10
+ self.cast_shadows = True
11
+
12
+
13
+ class PointLight:
14
+ def __init__(self, position=(0, 2, 0), color=(1,1,1), intensity=1.0, range=10.0):
15
+ self.position = np.array(position, dtype=np.float32)
16
+ self.color = np.array(color, dtype=np.float32)
17
+ self.intensity = intensity
18
+ self.range = range
19
+ self.cast_shadows = False
20
+
21
+
22
+ class SpotLight:
23
+ def __init__(self, position=(0, 5, 0), direction=(0, -1, 0),
24
+ color=(1,1,1), intensity=1.0, range=15.0,
25
+ inner_angle=12.5, outer_angle=17.5):
26
+ self.position = np.array(position, dtype=np.float32)
27
+ self.direction = np.array(direction, dtype=np.float32)
28
+ self.color = np.array(color, dtype=np.float32)
29
+ self.intensity = intensity
30
+ self.range = range
31
+ self.inner_angle = inner_angle
32
+ self.outer_angle = outer_angle
@@ -0,0 +1,170 @@
1
+ """Система уровней детализации (LOD)"""
2
+ import numpy as np
3
+ from typing import List, Tuple, Optional, Dict
4
+ from aether.graphics.mesh import Mesh
5
+
6
+
7
+ class LODLevel:
8
+ """Уровень детализации"""
9
+
10
+ def __init__(self, mesh: Mesh, distance: float, name: str = ""):
11
+ self.mesh = mesh
12
+ self.distance = distance # Расстояние, на котором активируется
13
+ self.name = name or f"LOD_{distance}"
14
+ self.screen_coverage = 1.0 # Процент покрытия экрана
15
+
16
+
17
+ class LODGroup:
18
+ """Группа LOD для одного объекта"""
19
+
20
+ def __init__(self):
21
+ self.levels: List[LODLevel] = []
22
+ self._current_level = -1
23
+ self._transition_progress = 0.0
24
+ self._transition_speed = 2.0
25
+ self._fade_enabled = True
26
+
27
+ def add_level(self, mesh: Mesh, distance: float, name: str = ""):
28
+ """Добавляет уровень детализации"""
29
+ level = LODLevel(mesh, distance, name)
30
+ self.levels.append(level)
31
+ # Сортируем по расстоянию (ближние первыми)
32
+ self.levels.sort(key=lambda x: x.distance)
33
+
34
+ def get_mesh_for_distance(self, distance: float) -> Mesh:
35
+ """Возвращает подходящий меш для расстояния"""
36
+ if not self.levels:
37
+ return None
38
+
39
+ selected_level = self.levels[0]
40
+
41
+ for level in self.levels:
42
+ if distance <= level.distance:
43
+ selected_level = level
44
+ break
45
+ selected_level = level
46
+
47
+ return selected_level.mesh
48
+
49
+ def update(self, camera_position: np.ndarray,
50
+ object_position: np.ndarray,
51
+ object_bounds: float = 1.0,
52
+ fov: float = 60.0,
53
+ screen_height: int = 1080) -> Tuple[Mesh, float]:
54
+ """
55
+ Обновляет LOD и возвращает (меш, прогресс перехода)
56
+ """
57
+ if not self.levels:
58
+ return None, 0.0
59
+
60
+ distance = np.linalg.norm(object_position - camera_position)
61
+
62
+ # Вычисляем screen coverage
63
+ screen_coverage = self._calculate_screen_coverage(
64
+ distance, object_bounds, fov, screen_height
65
+ )
66
+
67
+ # Выбираем уровень
68
+ new_level_idx = 0
69
+ for i, level in enumerate(self.levels):
70
+ if screen_coverage > level.screen_coverage or distance <= level.distance:
71
+ new_level_idx = i
72
+ break
73
+
74
+ # Плавный переход
75
+ if self._fade_enabled and new_level_idx != self._current_level:
76
+ if self._current_level >= 0:
77
+ self._transition_progress += self._transition_speed * 0.016
78
+ if self._transition_progress >= 1.0:
79
+ self._current_level = new_level_idx
80
+ self._transition_progress = 0.0
81
+ else:
82
+ self._current_level = new_level_idx
83
+ self._transition_progress = 0.0
84
+
85
+ current_mesh = self.levels[self._current_level].mesh
86
+ return current_mesh, self._transition_progress
87
+
88
+ def _calculate_screen_coverage(self, distance: float,
89
+ object_size: float,
90
+ fov: float,
91
+ screen_height: int) -> float:
92
+ """Вычисляет процент покрытия экрана объектом"""
93
+ if distance <= 0:
94
+ return 1.0
95
+
96
+ # Размер объекта в мировых единицах на экране
97
+ fov_rad = np.radians(fov)
98
+ screen_world_size = 2.0 * distance * np.tan(fov_rad / 2.0)
99
+ screen_coverage = object_size / screen_world_size
100
+
101
+ return np.clip(screen_coverage, 0.0, 1.0)
102
+
103
+
104
+ class LODManager:
105
+ """Менеджер LOD для всей сцены"""
106
+
107
+ _instance = None
108
+
109
+ def __new__(cls):
110
+ if cls._instance is None:
111
+ cls._instance = super().__new__(cls)
112
+ cls._instance._initialize()
113
+ return cls._instance
114
+
115
+ def _initialize(self):
116
+ """Инициализация менеджера"""
117
+ self._lod_groups: Dict[str, LODGroup] = {}
118
+ self._enabled = True
119
+ self._global_lod_bias = 1.0 # Множитель расстояния (1.0 = нормально)
120
+
121
+ print("[LODManager] Initialized")
122
+
123
+ def create_lod_group(self, entity_id: str, mesh_distances: List[Tuple[Mesh, float]]) -> LODGroup:
124
+ """
125
+ Создаёт LOD группу.
126
+ mesh_distances: список (меш, расстояние)
127
+ """
128
+ group = LODGroup()
129
+
130
+ for mesh, distance in mesh_distances:
131
+ group.add_level(mesh, distance * self._global_lod_bias)
132
+
133
+ self._lod_groups[entity_id] = group
134
+ return group
135
+
136
+ def get_lod_mesh(self, entity_id: str,
137
+ camera_position: np.ndarray,
138
+ object_position: np.ndarray,
139
+ object_bounds: float = 1.0) -> Optional[Mesh]:
140
+ """Получает подходящий LOD меш для объекта"""
141
+ if not self._enabled:
142
+ return None
143
+
144
+ group = self._lod_groups.get(entity_id)
145
+ if not group:
146
+ return None
147
+
148
+ distance = np.linalg.norm(object_position - camera_position)
149
+ return group.get_mesh_for_distance(distance * self._global_lod_bias)
150
+
151
+ def set_global_lod_bias(self, bias: float):
152
+ """Устанавливает глобальное смещение LOD"""
153
+ self._global_lod_bias = max(0.1, min(3.0, bias))
154
+
155
+ def remove_lod_group(self, entity_id: str):
156
+ """Удаляет LOD группу"""
157
+ if entity_id in self._lod_groups:
158
+ del self._lod_groups[entity_id]
159
+
160
+ def clear(self):
161
+ """Очищает все LOD группы"""
162
+ self._lod_groups.clear()
163
+
164
+ @property
165
+ def enabled(self) -> bool:
166
+ return self._enabled
167
+
168
+ @enabled.setter
169
+ def enabled(self, value: bool):
170
+ self._enabled = value
@@ -0,0 +1,25 @@
1
+ """PBR-материал"""
2
+ import numpy as np
3
+ from aether.graphics.texture import Texture
4
+
5
+
6
+ class Material:
7
+ def __init__(self, name="Material"):
8
+ self.name = name
9
+ self.albedo = np.array([1.0, 1.0, 1.0], dtype=np.float32)
10
+ self.metallic = 0.0
11
+ self.roughness = 0.5
12
+ self.ao = 1.0
13
+
14
+ self.albedo_map: Texture = None
15
+ self.normal_map: Texture = None
16
+ self.metallic_map: Texture = None
17
+ self.roughness_map: Texture = None
18
+
19
+ def apply(self, shader):
20
+ """Применяет материал к шейдеру"""
21
+ shader.set_vec3("material.albedo", self.albedo)
22
+ shader.set_float("material.metallic", self.metallic)
23
+ shader.set_float("material.roughness", self.roughness)
24
+ shader.set_float("material.ao", self.ao)
25
+ # Текстуры будут привязаны позже
@@ -0,0 +1,83 @@
1
+ """Геометрия: VAO, VBO, EBO"""
2
+ import numpy as np
3
+ from OpenGL.GL import *
4
+ import ctypes
5
+
6
+
7
+ class Mesh:
8
+ def __init__(self, vertices: np.ndarray, indices: np.ndarray,
9
+ normals: np.ndarray = None, uvs: np.ndarray = None):
10
+ self.vertex_count = len(vertices)
11
+ self.index_count = len(indices)
12
+
13
+ # Автоматические нормали
14
+ if normals is None:
15
+ normals = self._generate_normals(vertices, indices)
16
+
17
+ # VAO
18
+ self.vao = glGenVertexArrays(1)
19
+ glBindVertexArray(self.vao)
20
+
21
+ # VBO
22
+ self.vbo = glGenBuffers(1)
23
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
24
+
25
+ # Упаковываем данные: позиции + нормали + UV
26
+ data = []
27
+ for i, v in enumerate(vertices):
28
+ data.extend(v)
29
+ data.extend(normals[i] if i < len(normals) else [0, 1, 0])
30
+ if uvs is not None and i < len(uvs):
31
+ data.extend(uvs[i])
32
+ else:
33
+ data.extend([0.0, 0.0])
34
+
35
+ data = np.array(data, dtype=np.float32)
36
+ glBufferData(GL_ARRAY_BUFFER, data.nbytes, data, GL_STATIC_DRAW)
37
+
38
+ stride = (3 + 3 + 2) * 4 # pos + normal + uv
39
+ # Позиции (location = 0)
40
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(0))
41
+ glEnableVertexAttribArray(0)
42
+ # Нормали (location = 1)
43
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(12))
44
+ glEnableVertexAttribArray(1)
45
+ # UV (location = 2)
46
+ glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(24))
47
+ glEnableVertexAttribArray(2)
48
+
49
+ # EBO
50
+ self.ebo = glGenBuffers(1)
51
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.ebo)
52
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)
53
+
54
+ glBindVertexArray(0)
55
+
56
+ def _generate_normals(self, vertices, indices):
57
+ """Вычисляет нормали для каждого треугольника"""
58
+ normals = np.zeros_like(vertices)
59
+ for i in range(0, len(indices), 3):
60
+ i0, i1, i2 = indices[i], indices[i+1], indices[i+2]
61
+ v0, v1, v2 = vertices[i0], vertices[i1], vertices[i2]
62
+ edge1 = v1 - v0
63
+ edge2 = v2 - v0
64
+ normal = np.cross(edge1, edge2)
65
+ normals[i0] += normal
66
+ normals[i1] += normal
67
+ normals[i2] += normal
68
+ # Нормализуем
69
+ for i in range(len(normals)):
70
+ n = np.linalg.norm(normals[i])
71
+ if n > 0:
72
+ normals[i] /= n
73
+ return normals
74
+
75
+ def draw(self):
76
+ glBindVertexArray(self.vao)
77
+ glDrawElements(GL_TRIANGLES, self.index_count, GL_UNSIGNED_INT, None)
78
+ glBindVertexArray(0)
79
+
80
+ def cleanup(self):
81
+ glDeleteVertexArrays(1, [self.vao])
82
+ glDeleteBuffers(1, [self.vbo])
83
+ glDeleteBuffers(1, [self.ebo])
@@ -0,0 +1,67 @@
1
+ """Фабрика примитивов"""
2
+ import numpy as np
3
+ from aether.graphics.mesh import Mesh
4
+
5
+
6
+ class MeshFactory:
7
+ @staticmethod
8
+ def create_cube(size: float = 1.0) -> Mesh:
9
+ s = size / 2
10
+ vertices = np.array([
11
+ # Front face
12
+ [-s, -s, s], [ s, -s, s], [ s, s, s], [-s, s, s],
13
+ # Back face
14
+ [ s, -s, -s], [-s, -s, -s], [-s, s, -s], [ s, s, -s],
15
+ # Right face
16
+ [ s, -s, s], [ s, -s, -s], [ s, s, -s], [ s, s, s],
17
+ # Left face
18
+ [-s, -s, -s], [-s, -s, s], [-s, s, s], [-s, s, -s],
19
+ # Top face
20
+ [-s, s, s], [ s, s, s], [ s, s, -s], [-s, s, -s],
21
+ # Bottom face
22
+ [-s, -s, -s], [ s, -s, -s], [ s, -s, s], [-s, -s, s],
23
+ ], dtype=np.float32)
24
+
25
+ indices = np.array([
26
+ 0,1,2, 2,3,0, 4,5,6, 6,7,4,
27
+ 8,9,10, 10,11,8, 12,13,14, 14,15,12,
28
+ 16,17,18, 18,19,16, 20,21,22, 22,23,20,
29
+ ], dtype=np.uint32)
30
+
31
+ return Mesh(vertices, indices)
32
+
33
+ @staticmethod
34
+ def create_sphere(radius: float = 1.0, segments: int = 32) -> Mesh:
35
+ vertices = []
36
+ indices = []
37
+
38
+ for y in range(segments + 1):
39
+ for x in range(segments + 1):
40
+ x_seg = x / segments
41
+ y_seg = y / segments
42
+ x_pos = radius * np.cos(x_seg * 2 * np.pi) * np.sin(y_seg * np.pi)
43
+ y_pos = radius * np.cos(y_seg * np.pi)
44
+ z_pos = radius * np.sin(x_seg * 2 * np.pi) * np.sin(y_seg * np.pi)
45
+ vertices.append([x_pos, y_pos, z_pos])
46
+
47
+ vertices = np.array(vertices, dtype=np.float32)
48
+
49
+ for y in range(segments):
50
+ for x in range(segments):
51
+ i0 = y * (segments + 1) + x
52
+ i1 = i0 + 1
53
+ i2 = i0 + segments + 1
54
+ i3 = i2 + 1
55
+ indices.extend([i0, i2, i1, i1, i2, i3])
56
+
57
+ indices = np.array(indices, dtype=np.uint32)
58
+ return Mesh(vertices, indices)
59
+
60
+ @staticmethod
61
+ def create_plane(size: float = 10.0) -> Mesh:
62
+ s = size / 2
63
+ vertices = np.array([
64
+ [-s, 0, -s], [ s, 0, -s], [ s, 0, s], [-s, 0, s],
65
+ ], dtype=np.float32)
66
+ indices = np.array([0, 2, 1, 0, 3, 2], dtype=np.uint32)
67
+ return Mesh(vertices, indices)
@@ -0,0 +1,268 @@
1
+ """Система частиц"""
2
+ import numpy as np
3
+ from OpenGL.GL import *
4
+ import random
5
+ from typing import List
6
+ import time
7
+
8
+
9
+ class Particle:
10
+ """Одна частица"""
11
+
12
+ def __init__(self):
13
+ self.position = np.array([0.0, 0.0, 0.0], dtype=np.float32)
14
+ self.velocity = np.array([0.0, 0.0, 0.0], dtype=np.float32)
15
+ self.color = np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32)
16
+ self.size = 1.0
17
+ self.life = 1.0
18
+ self.max_life = 1.0
19
+ self.alive = False
20
+
21
+ def reset(self, position, velocity, color, size, life):
22
+ """Сбрасывает частицу"""
23
+ self.position = np.array(position, dtype=np.float32)
24
+ self.velocity = np.array(velocity, dtype=np.float32)
25
+ self.color = np.array(color, dtype=np.float32)
26
+ self.size = size
27
+ self.life = life
28
+ self.max_life = life
29
+ self.alive = True
30
+
31
+
32
+ class ParticleEmitter:
33
+ """Эмиттер частиц"""
34
+
35
+ def __init__(self, max_particles=1000):
36
+ self.max_particles = max_particles
37
+ self.particles: List[Particle] = []
38
+ self._pool = []
39
+
40
+ # Настройки эмиссии
41
+ self.emission_rate = 100 # частиц в секунду
42
+ self._emission_accumulator = 0.0
43
+
44
+ self.position = np.array([0.0, 0.0, 0.0], dtype=np.float32)
45
+ self.velocity = np.array([0.0, 1.0, 0.0], dtype=np.float32)
46
+ self.velocity_variation = np.array([0.5, 0.5, 0.5], dtype=np.float32)
47
+
48
+ self.start_color = np.array([1.0, 0.5, 0.0, 1.0], dtype=np.float32)
49
+ self.end_color = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float32)
50
+
51
+ self.start_size = 0.5
52
+ self.end_size = 0.0
53
+
54
+ self.min_life = 0.5
55
+ self.max_life = 2.0
56
+
57
+ self.looping = True
58
+ self.playing = True
59
+
60
+ # Силы, влияющие на частицы
61
+ self.gravity = np.array([0.0, -2.0, 0.0], dtype=np.float32)
62
+ self.wind = np.array([0.0, 0.0, 0.0], dtype=np.float32)
63
+
64
+ # Предсоздаём пул частиц
65
+ for _ in range(max_particles):
66
+ self._pool.append(Particle())
67
+
68
+ print(f"[ParticleEmitter] Created with {max_particles} max particles")
69
+
70
+ def emit(self, count: int):
71
+ """Выпускает частицы"""
72
+ for _ in range(count):
73
+ particle = self._get_dead_particle()
74
+ if particle is None:
75
+ break
76
+
77
+ # Случайные вариации
78
+ vel = self.velocity.copy()
79
+ vel += np.array([
80
+ random.uniform(-1, 1) * self.velocity_variation[0],
81
+ random.uniform(-1, 1) * self.velocity_variation[1],
82
+ random.uniform(-1, 1) * self.velocity_variation[2]
83
+ ], dtype=np.float32)
84
+
85
+ life = random.uniform(self.min_life, self.max_life)
86
+
87
+ particle.reset(
88
+ self.position.copy(),
89
+ vel,
90
+ self.start_color.copy(),
91
+ self.start_size,
92
+ life
93
+ )
94
+
95
+ self.particles.append(particle)
96
+
97
+ def update(self, dt: float):
98
+ """Обновляет все частицы"""
99
+ if not self.playing:
100
+ return
101
+
102
+ # Эмиссия
103
+ if self.looping:
104
+ self._emission_accumulator += self.emission_rate * dt
105
+ emit_count = int(self._emission_accumulator)
106
+ if emit_count > 0:
107
+ self._emission_accumulator -= emit_count
108
+ self.emit(emit_count)
109
+
110
+ # Обновление существующих частиц
111
+ for particle in self.particles[:]:
112
+ particle.life -= dt
113
+
114
+ if particle.life <= 0:
115
+ particle.alive = False
116
+ self.particles.remove(particle)
117
+ continue
118
+
119
+ # Применяем силы
120
+ particle.velocity += (self.gravity + self.wind) * dt
121
+
122
+ # Обновляем позицию
123
+ particle.position += particle.velocity * dt
124
+
125
+ # Интерполяция цвета
126
+ t = 1.0 - (particle.life / particle.max_life)
127
+ particle.color = self.start_color * (1 - t) + self.end_color * t
128
+
129
+ # Интерполяция размера
130
+ particle.size = self.start_size * (1 - t) + self.end_size * t
131
+
132
+ def _get_dead_particle(self) -> Particle:
133
+ """Получает мёртвую частицу из пула"""
134
+ for particle in self._pool:
135
+ if not particle.alive:
136
+ return particle
137
+ return None
138
+
139
+ def stop(self):
140
+ """Останавливает эмиссию"""
141
+ self.playing = False
142
+
143
+ def play(self):
144
+ """Запускает эмиссию"""
145
+ self.playing = True
146
+
147
+ def clear(self):
148
+ """Очищает все частицы"""
149
+ for particle in self.particles:
150
+ particle.alive = False
151
+ self.particles.clear()
152
+
153
+ def get_particle_data(self) -> tuple:
154
+ """Возвращает данные для рендеринга"""
155
+ positions = []
156
+ colors = []
157
+ sizes = []
158
+
159
+ for particle in self.particles:
160
+ positions.extend(particle.position)
161
+ colors.extend(particle.color)
162
+ sizes.append(particle.size)
163
+
164
+ return (
165
+ np.array(positions, dtype=np.float32),
166
+ np.array(colors, dtype=np.float32),
167
+ np.array(sizes, dtype=np.float32)
168
+ )
169
+
170
+
171
+ class ParticleRenderer:
172
+ """Рендерер для системы частиц"""
173
+
174
+ def __init__(self):
175
+ self.vao = glGenVertexArrays(1)
176
+ self.vbo = glGenBuffers(1)
177
+
178
+ # Простой шейдер для частиц
179
+ self._setup_shader()
180
+
181
+ print("[ParticleRenderer] Initialized")
182
+
183
+ def _setup_shader(self):
184
+ """Настройка шейдера частиц"""
185
+ vertex_shader = """
186
+ #version 460 core
187
+ layout(location = 0) in vec3 aPos;
188
+ layout(location = 1) in vec4 aColor;
189
+ layout(location = 2) in float aSize;
190
+
191
+ out vec4 vertexColor;
192
+
193
+ uniform mat4 view;
194
+ uniform mat4 projection;
195
+
196
+ void main() {
197
+ vec4 viewPos = view * vec4(aPos, 1.0);
198
+ gl_Position = projection * viewPos;
199
+ gl_PointSize = aSize * (300.0 / -viewPos.z); // Размер с учётом перспективы
200
+ vertexColor = aColor;
201
+ }
202
+ """
203
+
204
+ fragment_shader = """
205
+ #version 460 core
206
+ in vec4 vertexColor;
207
+ out vec4 FragColor;
208
+
209
+ void main() {
210
+ // Круглая частица
211
+ vec2 coord = gl_PointCoord - vec2(0.5);
212
+ float dist = length(coord);
213
+ if (dist > 0.5) discard;
214
+
215
+ // Мягкий край
216
+ float alpha = 1.0 - smoothstep(0.4, 0.5, dist);
217
+
218
+ FragColor = vec4(vertexColor.rgb, vertexColor.a * alpha);
219
+ }
220
+ """
221
+
222
+ from aether.graphics.shader import Shader
223
+ self.shader = Shader(vertex_shader, fragment_shader)
224
+
225
+ def render(self, emitter: ParticleEmitter, view: np.ndarray, projection: np.ndarray):
226
+ """Рендерит частицы"""
227
+ if not emitter.particles:
228
+ return
229
+
230
+ positions, colors, sizes = emitter.get_particle_data()
231
+
232
+ # Обновляем буфер
233
+ glBindVertexArray(self.vao)
234
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
235
+
236
+ # Собираем все данные в один массив
237
+ data = []
238
+ for i in range(len(emitter.particles)):
239
+ data.extend(positions[i*3:(i+1)*3])
240
+ data.extend(colors[i*4:(i+1)*4])
241
+ data.append(sizes[i])
242
+
243
+ data = np.array(data, dtype=np.float32)
244
+ glBufferData(GL_ARRAY_BUFFER, data.nbytes, data, GL_DYNAMIC_DRAW)
245
+
246
+ stride = (3 + 4 + 1) * 4 # pos + color + size
247
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(0))
248
+ glEnableVertexAttribArray(0)
249
+ glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(12))
250
+ glEnableVertexAttribArray(1)
251
+ glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(28))
252
+ glEnableVertexAttribArray(2)
253
+
254
+ # Рендеринг
255
+ self.shader.use()
256
+ self.shader.set_mat4("view", view)
257
+ self.shader.set_mat4("projection", projection)
258
+
259
+ glEnable(GL_BLEND)
260
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
261
+ glDepthMask(GL_FALSE)
262
+
263
+ glDrawArrays(GL_POINTS, 0, len(emitter.particles))
264
+
265
+ glDepthMask(GL_TRUE)
266
+ glDisable(GL_BLEND)
267
+
268
+ glBindVertexArray(0)