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.
- aether/__init__.py +54 -0
- aether/audio/__init__.py +0 -0
- aether/audio/audio_manager.py +211 -0
- aether/audio/music.py +6 -0
- aether/audio/sound.py +6 -0
- aether/core/__init__.py +211 -0
- aether/core/achievement_system.py +304 -0
- aether/core/application.py +19 -0
- aether/core/config.py +75 -0
- aether/core/console.py +366 -0
- aether/core/engine.py +50 -0
- aether/core/event_bus.py +24 -0
- aether/core/input.py +58 -0
- aether/core/plugin_system.py +93 -0
- aether/core/save_system.py +409 -0
- aether/core/time_manager.py +25 -0
- aether/core/window.py +57 -0
- aether/graphics/__init__.py +0 -0
- aether/graphics/camera.py +69 -0
- aether/graphics/framebuffer.py +119 -0
- aether/graphics/light.py +32 -0
- aether/graphics/lod_system.py +170 -0
- aether/graphics/material.py +25 -0
- aether/graphics/mesh.py +83 -0
- aether/graphics/mesh_factory.py +67 -0
- aether/graphics/particle_system.py +268 -0
- aether/graphics/post_process.py +279 -0
- aether/graphics/renderer.py +135 -0
- aether/graphics/shader.py +61 -0
- aether/graphics/shader_library.py +157 -0
- aether/graphics/sky_atmosphere.py +309 -0
- aether/graphics/terrain.py +408 -0
- aether/graphics/texture.py +34 -0
- aether/graphics/water.py +432 -0
- aether/physics/__init__.py +0 -0
- aether/physics/bullet_backend.py +395 -0
- aether/physics/collider.py +22 -0
- aether/physics/physics_world.py +436 -0
- aether/physics/raycast.py +25 -0
- aether/physics/rigidbody.py +7 -0
- aether/resources/__init__.py +0 -0
- aether/resources/asset_database.py +76 -0
- aether/resources/resource_manager.py +147 -0
- aether/resources/serialization.py +114 -0
- aether/scene/__init__.py +0 -0
- aether/scene/component.py +368 -0
- aether/scene/entity.py +46 -0
- aether/scene/scene.py +33 -0
- aether/scene/scene_graph.py +252 -0
- aether/scene/scene_manager.py +191 -0
- aether/ui/__init__.py +0 -0
- aether/ui/button.py +297 -0
- aether/ui/canvas.py +11 -0
- aether/ui/text.py +286 -0
- aether/utils/__init__.py +0 -0
- aether/utils/logger.py +16 -0
- aether/utils/math_utils.py +31 -0
- aether/utils/object_pool.py +137 -0
- aether/utils/profiler.py +187 -0
- aether_engine-1.0.0.dist-info/METADATA +71 -0
- aether_engine-1.0.0.dist-info/RECORD +64 -0
- aether_engine-1.0.0.dist-info/WHEEL +5 -0
- aether_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- aether_engine-1.0.0.dist-info/top_level.txt +1 -0
aether/graphics/light.py
ADDED
|
@@ -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
|
+
# Текстуры будут привязаны позже
|
aether/graphics/mesh.py
ADDED
|
@@ -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)
|