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,395 @@
1
+ """Интеграция с PyBullet как опциональный физический движок"""
2
+ import numpy as np
3
+ from typing import Optional, Dict, List, Tuple
4
+ import sys
5
+
6
+
7
+ class BulletPhysicsBackend:
8
+ """Бэкенд физики на PyBullet"""
9
+
10
+ _instance = None
11
+ _available = False
12
+
13
+ def __new__(cls):
14
+ if cls._instance is None:
15
+ cls._instance = super().__new__(cls)
16
+ return cls._instance
17
+
18
+ def __init__(self):
19
+ if hasattr(self, '_initialized'):
20
+ return
21
+
22
+ self._initialized = True
23
+ self._pybullet = None
24
+ self._client_id = -1
25
+ self._bodies: Dict[str, int] = {}
26
+ self._collision_shapes: Dict[str, int] = {}
27
+ self._ghost_objects: Dict[str, int] = {}
28
+
29
+ # Пробуем импортировать PyBullet
30
+ try:
31
+ import pybullet as p
32
+ self._pybullet = p
33
+ BulletPhysicsBackend._available = True
34
+ print("[BulletBackend] PyBullet available")
35
+ except ImportError:
36
+ print("[BulletBackend] PyBullet not installed. Use: pip install aether-engine[physics]")
37
+ print("[BulletBackend] Falling back to built-in physics")
38
+
39
+ @property
40
+ def available(self) -> bool:
41
+ return self._available and self._pybullet is not None
42
+
43
+ def initialize(self, gravity: Tuple[float, float, float] = (0, -9.81, 0)):
44
+ """Инициализирует физический мир PyBullet"""
45
+ if not self.available:
46
+ return False
47
+
48
+ if self._client_id >= 0:
49
+ return True
50
+
51
+ try:
52
+ self._client_id = self._pybullet.connect(self._pybullet.DIRECT)
53
+ self._pybullet.setGravity(*gravity)
54
+ self._pybullet.setPhysicsEngineParameter(
55
+ fixedTimeStep=1.0/60.0,
56
+ numSolverIterations=10,
57
+ numSubSteps=1
58
+ )
59
+ print(f"[BulletBackend] Initialized (client: {self._client_id})")
60
+ return True
61
+ except Exception as e:
62
+ print(f"[BulletBackend] Failed to initialize: {e}")
63
+ return False
64
+
65
+ def create_rigid_body(self,
66
+ entity_id: str,
67
+ mass: float = 1.0,
68
+ shape_type: str = "box",
69
+ dimensions: Tuple[float, float, float] = (1, 1, 1),
70
+ position: Tuple[float, float, float] = (0, 0, 0),
71
+ rotation: Tuple[float, float, float, float] = (0, 0, 0, 1),
72
+ is_kinematic: bool = False,
73
+ friction: float = 0.5,
74
+ restitution: float = 0.3) -> Optional[int]:
75
+ """Создаёт твёрдое тело"""
76
+ if not self.available:
77
+ return None
78
+
79
+ try:
80
+ # Создаём collision shape
81
+ shape_key = f"{shape_type}_{dimensions}"
82
+
83
+ if shape_key not in self._collision_shapes:
84
+ if shape_type == "box":
85
+ shape = self._pybullet.createCollisionShape(
86
+ self._pybullet.GEOM_BOX,
87
+ halfExtents=[d/2 for d in dimensions]
88
+ )
89
+ elif shape_type == "sphere":
90
+ shape = self._pybullet.createCollisionShape(
91
+ self._pybullet.GEOM_SPHERE,
92
+ radius=dimensions[0]
93
+ )
94
+ elif shape_type == "capsule":
95
+ shape = self._pybullet.createCollisionShape(
96
+ self._pybullet.GEOM_CAPSULE,
97
+ radius=dimensions[0],
98
+ height=dimensions[1]
99
+ )
100
+ elif shape_type == "cylinder":
101
+ shape = self._pybullet.createCollisionShape(
102
+ self._pybullet.GEOM_CYLINDER,
103
+ radius=dimensions[0],
104
+ height=dimensions[1]
105
+ )
106
+ elif shape_type == "mesh":
107
+ # Для mesh коллайдеров
108
+ vertices = dimensions # Передаются как массив вершин
109
+ shape = self._pybullet.createCollisionShape(
110
+ self._pybullet.GEOM_MESH,
111
+ vertices=vertices
112
+ )
113
+ else:
114
+ shape = self._pybullet.createCollisionShape(
115
+ self._pybullet.GEOM_BOX,
116
+ halfExtents=[0.5, 0.5, 0.5]
117
+ )
118
+
119
+ self._collision_shapes[shape_key] = shape
120
+
121
+ # Создаём тело
122
+ body_id = self._pybullet.createMultiBody(
123
+ baseMass=0 if is_kinematic else mass,
124
+ baseCollisionShapeIndex=self._collision_shapes[shape_key],
125
+ basePosition=position,
126
+ baseOrientation=rotation,
127
+ )
128
+
129
+ # Настройка физических свойств
130
+ self._pybullet.changeDynamics(
131
+ body_id, -1,
132
+ lateralFriction=friction,
133
+ restitution=restitution,
134
+ linearDamping=0.04,
135
+ angularDamping=0.04,
136
+ )
137
+
138
+ if is_kinematic:
139
+ self._pybullet.changeDynamics(body_id, -1, mass=0)
140
+
141
+ self._bodies[entity_id] = body_id
142
+ return body_id
143
+
144
+ except Exception as e:
145
+ print(f"[BulletBackend] Failed to create body: {e}")
146
+ return None
147
+
148
+ def remove_body(self, entity_id: str):
149
+ """Удаляет твёрдое тело"""
150
+ if entity_id in self._bodies:
151
+ try:
152
+ self._pybullet.removeBody(self._bodies[entity_id])
153
+ del self._bodies[entity_id]
154
+ except Exception as e:
155
+ print(f"[BulletBackend] Failed to remove body: {e}")
156
+
157
+ def step(self, dt: float):
158
+ """Шаг симуляции"""
159
+ if not self.available or self._client_id < 0:
160
+ return
161
+
162
+ try:
163
+ self._pybullet.stepSimulation()
164
+ except Exception as e:
165
+ print(f"[BulletBackend] Step failed: {e}")
166
+
167
+ def get_body_position(self, entity_id: str) -> Optional[np.ndarray]:
168
+ """Получает позицию тела"""
169
+ if entity_id not in self._bodies:
170
+ return None
171
+
172
+ try:
173
+ pos, _ = self._pybullet.getBasePositionAndOrientation(
174
+ self._bodies[entity_id]
175
+ )
176
+ return np.array(pos, dtype=np.float32)
177
+ except Exception:
178
+ return None
179
+
180
+ def get_body_rotation(self, entity_id: str) -> Optional[np.ndarray]:
181
+ """Получает поворот тела"""
182
+ if entity_id not in self._bodies:
183
+ return None
184
+
185
+ try:
186
+ _, rot = self._pybullet.getBasePositionAndOrientation(
187
+ self._bodies[entity_id]
188
+ )
189
+ return np.array(rot, dtype=np.float32)
190
+ except Exception:
191
+ return None
192
+
193
+ def set_body_position(self, entity_id: str, position: np.ndarray):
194
+ """Устанавливает позицию тела"""
195
+ if entity_id not in self._bodies:
196
+ return
197
+
198
+ try:
199
+ _, rot = self._pybullet.getBasePositionAndOrientation(
200
+ self._bodies[entity_id]
201
+ )
202
+ self._pybullet.resetBasePositionAndOrientation(
203
+ self._bodies[entity_id],
204
+ position.tolist(),
205
+ rot
206
+ )
207
+ except Exception as e:
208
+ print(f"[BulletBackend] Failed to set position: {e}")
209
+
210
+ def apply_force(self, entity_id: str, force: np.ndarray,
211
+ position: Optional[np.ndarray] = None):
212
+ """Применяет силу к телу"""
213
+ if entity_id not in self._bodies:
214
+ return
215
+
216
+ try:
217
+ if position is not None:
218
+ self._pybullet.applyForce(
219
+ self._bodies[entity_id], -1,
220
+ force.tolist(),
221
+ position.tolist(),
222
+ self._pybullet.WORLD_FRAME
223
+ )
224
+ else:
225
+ # Применяем в центре масс
226
+ self._pybullet.applyExternalForce(
227
+ self._bodies[entity_id], -1,
228
+ force.tolist(),
229
+ [0, 0, 0],
230
+ self._pybullet.WORLD_FRAME
231
+ )
232
+ except Exception as e:
233
+ print(f"[BulletBackend] Failed to apply force: {e}")
234
+
235
+ def apply_impulse(self, entity_id: str, impulse: np.ndarray):
236
+ """Применяет импульс"""
237
+ if entity_id not in self._bodies:
238
+ return
239
+
240
+ try:
241
+ self._pybullet.applyExternalForce(
242
+ self._bodies[entity_id], -1,
243
+ impulse.tolist(),
244
+ [0, 0, 0],
245
+ self._pybullet.WORLD_FRAME
246
+ )
247
+ except Exception as e:
248
+ print(f"[BulletBackend] Failed to apply impulse: {e}")
249
+
250
+ def raycast(self, from_pos: np.ndarray, to_pos: np.ndarray) -> List[dict]:
251
+ """Пуск луча"""
252
+ if not self.available:
253
+ return []
254
+
255
+ try:
256
+ results = self._pybullet.rayTestBatch(
257
+ [from_pos.tolist()],
258
+ [to_pos.tolist()]
259
+ )
260
+
261
+ hits = []
262
+ for result in results:
263
+ if result[0] >= 0: # Есть попадание
264
+ hits.append({
265
+ 'body_id': result[0],
266
+ 'link_index': result[1],
267
+ 'hit_position': np.array(result[3]),
268
+ 'hit_normal': np.array(result[4]),
269
+ 'hit_distance': result[2],
270
+ })
271
+
272
+ return hits
273
+ except Exception:
274
+ return []
275
+
276
+ def create_constraint(self,
277
+ entity_a: str, entity_b: str,
278
+ constraint_type: str = "point",
279
+ pivot_a: Tuple[float, float, float] = (0, 0, 0),
280
+ pivot_b: Tuple[float, float, float] = (0, 0, 0)) -> Optional[int]:
281
+ """Создаёт ограничение (joint) между телами"""
282
+ if not self.available:
283
+ return None
284
+
285
+ if entity_a not in self._bodies or entity_b not in self._bodies:
286
+ return None
287
+
288
+ try:
289
+ body_a = self._bodies[entity_a]
290
+ body_b = self._bodies[entity_b]
291
+
292
+ if constraint_type == "point":
293
+ constraint = self._pybullet.createConstraint(
294
+ body_a, -1, body_b, -1,
295
+ self._pybullet.JOINT_POINT2POINT,
296
+ [0, 0, 0],
297
+ pivot_a, pivot_b
298
+ )
299
+ elif constraint_type == "hinge":
300
+ constraint = self._pybullet.createConstraint(
301
+ body_a, -1, body_b, -1,
302
+ self._pybullet.JOINT_HINGE,
303
+ [0, 0, 0],
304
+ pivot_a, pivot_b
305
+ )
306
+ elif constraint_type == "fixed":
307
+ constraint = self._pybullet.createConstraint(
308
+ body_a, -1, body_b, -1,
309
+ self._pybullet.JOINT_FIXED,
310
+ [0, 0, 0],
311
+ pivot_a, pivot_b
312
+ )
313
+ else:
314
+ return None
315
+
316
+ return constraint
317
+
318
+ except Exception as e:
319
+ print(f"[BulletBackend] Failed to create constraint: {e}")
320
+ return None
321
+
322
+ def create_ghost_object(self,
323
+ entity_id: str,
324
+ shape_type: str = "box",
325
+ dimensions: Tuple[float, float, float] = (1, 1, 1),
326
+ position: Tuple[float, float, float] = (0, 0, 0)):
327
+ """Создаёт ghost object (триггер)"""
328
+ # Ghost objects не поддерживаются напрямую в PyBullet,
329
+ # используем body с нулевой массой и без коллизий
330
+ return self.create_rigid_body(
331
+ entity_id, mass=0, shape_type=shape_type,
332
+ dimensions=dimensions, position=position,
333
+ is_kinematic=True
334
+ )
335
+
336
+ def get_overlapping_objects(self, entity_id: str) -> List[str]:
337
+ """Получает список объектов, пересекающихся с данным"""
338
+ if entity_id not in self._bodies:
339
+ return []
340
+
341
+ try:
342
+ # Получаем AABB тела
343
+ aabb_min, aabb_max = self._pybullet.getAABB(
344
+ self._bodies[entity_id]
345
+ )
346
+
347
+ # Ищем все тела в этом AABB
348
+ overlapping = []
349
+ for other_id, body_id in self._bodies.items():
350
+ if other_id != entity_id:
351
+ other_aabb_min, other_aabb_max = self._pybullet.getAABB(body_id)
352
+
353
+ # Проверка пересечения AABB
354
+ if (aabb_min[0] <= other_aabb_max[0] and aabb_max[0] >= other_aabb_min[0] and
355
+ aabb_min[1] <= other_aabb_max[1] and aabb_max[1] >= other_aabb_min[1] and
356
+ aabb_min[2] <= other_aabb_max[2] and aabb_max[2] >= other_aabb_min[2]):
357
+ overlapping.append(other_id)
358
+
359
+ return overlapping
360
+ except Exception:
361
+ return []
362
+
363
+ def set_gravity(self, gravity: Tuple[float, float, float]):
364
+ """Устанавливает гравитацию"""
365
+ if self.available and self._client_id >= 0:
366
+ self._pybullet.setGravity(*gravity)
367
+
368
+ def cleanup(self):
369
+ """Очищает физический мир"""
370
+ if self.available and self._client_id >= 0:
371
+ try:
372
+ # Удаляем все тела
373
+ for body_id in self._bodies.values():
374
+ self._pybullet.removeBody(body_id)
375
+
376
+ self._bodies.clear()
377
+ self._collision_shapes.clear()
378
+
379
+ self._pybullet.disconnect()
380
+ self._client_id = -1
381
+
382
+ print("[BulletBackend] Cleanup complete")
383
+ except Exception:
384
+ pass
385
+
386
+
387
+ # Удобная функция для проверки доступности
388
+ def is_bullet_available() -> bool:
389
+ """Проверяет, установлен ли PyBullet"""
390
+ return BulletPhysicsBackend._available
391
+
392
+
393
+ def get_physics_backend() -> BulletPhysicsBackend:
394
+ """Возвращает синглтон физического бэкенда"""
395
+ return BulletPhysicsBackend()
@@ -0,0 +1,22 @@
1
+ """Коллайдеры"""
2
+ import numpy as np
3
+
4
+
5
+ class Collider:
6
+ def __init__(self, is_trigger=False):
7
+ self.is_trigger = is_trigger
8
+ self.enabled = True
9
+
10
+
11
+ class BoxCollider(Collider):
12
+ def __init__(self, size=(1,1,1), center=(0,0,0), is_trigger=False):
13
+ super().__init__(is_trigger)
14
+ self.size = np.array(size, dtype=np.float32)
15
+ self.center = np.array(center, dtype=np.float32)
16
+
17
+
18
+ class SphereCollider(Collider):
19
+ def __init__(self, radius=0.5, center=(0,0,0), is_trigger=False):
20
+ super().__init__(is_trigger)
21
+ self.radius = radius
22
+ self.center = np.array(center, dtype=np.float32)