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