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,436 @@
|
|
|
1
|
+
"""Мир физики с обнаружением коллизий и разрешением"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
from aether.physics.collider import Collider, BoxCollider, SphereCollider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CollisionInfo:
|
|
8
|
+
"""Информация о столкновении"""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.collided = False
|
|
12
|
+
self.point = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
|
13
|
+
self.normal = np.array([0.0, 1.0, 0.0], dtype=np.float32)
|
|
14
|
+
self.depth = 0.0
|
|
15
|
+
self.entity_a = None
|
|
16
|
+
self.entity_b = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PhysicsWorld:
|
|
20
|
+
"""Мир физики"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, gravity=(0, -9.81, 0)):
|
|
23
|
+
self.gravity = np.array(gravity, dtype=np.float32)
|
|
24
|
+
self.bodies = []
|
|
25
|
+
self.colliders = []
|
|
26
|
+
self.collision_pairs = []
|
|
27
|
+
|
|
28
|
+
# Настройки
|
|
29
|
+
self.solver_iterations = 10
|
|
30
|
+
self.enable_sleeping = True
|
|
31
|
+
self.sleep_threshold = 0.01
|
|
32
|
+
self.sleep_time_threshold = 1.0
|
|
33
|
+
|
|
34
|
+
# События
|
|
35
|
+
self.on_collision_enter = []
|
|
36
|
+
self.on_collision_exit = []
|
|
37
|
+
self.on_trigger_enter = []
|
|
38
|
+
|
|
39
|
+
print(f"[Physics] World created (gravity: {self.gravity})")
|
|
40
|
+
|
|
41
|
+
def add_rigidbody(self, body):
|
|
42
|
+
"""Добавляет твёрдое тело в симуляцию"""
|
|
43
|
+
self.bodies.append(body)
|
|
44
|
+
if hasattr(body, 'entity') and body.entity:
|
|
45
|
+
collider = body.entity.get_component("Collider") or \
|
|
46
|
+
body.entity.get_component("BoxCollider") or \
|
|
47
|
+
body.entity.get_component("SphereCollider")
|
|
48
|
+
if collider:
|
|
49
|
+
self.colliders.append((body, collider))
|
|
50
|
+
|
|
51
|
+
def remove_rigidbody(self, body):
|
|
52
|
+
"""Удаляет твёрдое тело из симуляции"""
|
|
53
|
+
if body in self.bodies:
|
|
54
|
+
self.bodies.remove(body)
|
|
55
|
+
self.colliders = [(b, c) for b, c in self.colliders if b != body]
|
|
56
|
+
|
|
57
|
+
def step(self, dt: float):
|
|
58
|
+
"""Шаг симуляции"""
|
|
59
|
+
# 1. Применяем силы
|
|
60
|
+
self._apply_forces(dt)
|
|
61
|
+
|
|
62
|
+
# 2. Обновляем скорости
|
|
63
|
+
self._integrate_velocities(dt)
|
|
64
|
+
|
|
65
|
+
# 3. Обнаружение коллизий
|
|
66
|
+
collisions = self._detect_collisions()
|
|
67
|
+
|
|
68
|
+
# 4. Разрешение коллизий
|
|
69
|
+
for _ in range(self.solver_iterations):
|
|
70
|
+
self._resolve_collisions(collisions, dt)
|
|
71
|
+
|
|
72
|
+
# 5. Обновляем позиции
|
|
73
|
+
self._integrate_positions(dt)
|
|
74
|
+
|
|
75
|
+
# 6. Проверка на сон
|
|
76
|
+
if self.enable_sleeping:
|
|
77
|
+
self._check_sleeping(dt)
|
|
78
|
+
|
|
79
|
+
def _apply_forces(self, dt: float):
|
|
80
|
+
"""Применяет силы к телам"""
|
|
81
|
+
for body in self.bodies:
|
|
82
|
+
if body.use_gravity and not body.is_kinematic:
|
|
83
|
+
body.velocity += self.gravity * dt
|
|
84
|
+
|
|
85
|
+
def _integrate_velocities(self, dt: float):
|
|
86
|
+
"""Интегрирует скорости"""
|
|
87
|
+
for body in self.bodies:
|
|
88
|
+
if not body.is_kinematic:
|
|
89
|
+
# Применяем drag
|
|
90
|
+
body.velocity *= (1.0 - body.drag * dt)
|
|
91
|
+
body.angular_velocity *= (1.0 - body.angular_drag * dt)
|
|
92
|
+
|
|
93
|
+
def _integrate_positions(self, dt: float):
|
|
94
|
+
"""Интегрирует позиции"""
|
|
95
|
+
for body in self.bodies:
|
|
96
|
+
if not body.is_kinematic and not body.is_sleeping:
|
|
97
|
+
transform = body.entity.get_component("Transform") if hasattr(body, 'entity') and body.entity else None
|
|
98
|
+
if transform:
|
|
99
|
+
transform.position += body.velocity * dt
|
|
100
|
+
transform.rotation += body.angular_velocity * dt
|
|
101
|
+
transform._matrix_dirty = True
|
|
102
|
+
|
|
103
|
+
def _detect_collisions(self) -> List[CollisionInfo]:
|
|
104
|
+
"""Обнаруживает все коллизии"""
|
|
105
|
+
collisions = []
|
|
106
|
+
|
|
107
|
+
for i in range(len(self.colliders)):
|
|
108
|
+
for j in range(i + 1, len(self.colliders)):
|
|
109
|
+
body_a, collider_a = self.colliders[i]
|
|
110
|
+
body_b, collider_b = self.colliders[j]
|
|
111
|
+
|
|
112
|
+
# Пропускаем спящие и кинематические пары
|
|
113
|
+
if (body_a.is_sleeping and body_b.is_sleeping) or \
|
|
114
|
+
(body_a.is_kinematic and body_b.is_kinematic):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
collision = self._check_collision(collider_a, collider_b)
|
|
118
|
+
if collision:
|
|
119
|
+
collision.entity_a = body_a.entity if hasattr(body_a, 'entity') else None
|
|
120
|
+
collision.entity_b = body_b.entity if hasattr(body_b, 'entity') else None
|
|
121
|
+
collisions.append(collision)
|
|
122
|
+
|
|
123
|
+
return collisions
|
|
124
|
+
|
|
125
|
+
def _check_collision(self, collider_a: Collider, collider_b: Collider) -> Optional[CollisionInfo]:
|
|
126
|
+
"""Проверяет столкновение между двумя коллайдерами"""
|
|
127
|
+
|
|
128
|
+
# Box vs Box
|
|
129
|
+
if isinstance(collider_a, BoxCollider) and isinstance(collider_b, BoxCollider):
|
|
130
|
+
return self._box_vs_box(collider_a, collider_b)
|
|
131
|
+
|
|
132
|
+
# Sphere vs Sphere
|
|
133
|
+
if isinstance(collider_a, SphereCollider) and isinstance(collider_b, SphereCollider):
|
|
134
|
+
return self._sphere_vs_sphere(collider_a, collider_b)
|
|
135
|
+
|
|
136
|
+
# Box vs Sphere
|
|
137
|
+
if isinstance(collider_a, BoxCollider) and isinstance(collider_b, SphereCollider):
|
|
138
|
+
return self._box_vs_sphere(collider_a, collider_b)
|
|
139
|
+
|
|
140
|
+
if isinstance(collider_a, SphereCollider) and isinstance(collider_b, BoxCollider):
|
|
141
|
+
collision = self._box_vs_sphere(collider_b, collider_a)
|
|
142
|
+
if collision:
|
|
143
|
+
collision.normal = -collision.normal
|
|
144
|
+
return collision
|
|
145
|
+
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def _box_vs_box(self, a: BoxCollider, b: BoxCollider) -> Optional[CollisionInfo]:
|
|
149
|
+
"""AABB vs AABB коллизия"""
|
|
150
|
+
# Получаем позиции из трансформов
|
|
151
|
+
pos_a = self._get_collider_position(a)
|
|
152
|
+
pos_b = self._get_collider_position(b)
|
|
153
|
+
|
|
154
|
+
if pos_a is None or pos_b is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
# AABB проверка
|
|
158
|
+
min_a = pos_a + a.center - a.size / 2
|
|
159
|
+
max_a = pos_a + a.center + a.size / 2
|
|
160
|
+
min_b = pos_b + b.center - b.size / 2
|
|
161
|
+
max_b = pos_b + b.center + b.size / 2
|
|
162
|
+
|
|
163
|
+
# Проверка пересечения
|
|
164
|
+
overlap_x = min(max_a[0], max_b[0]) - max(min_a[0], min_b[0])
|
|
165
|
+
overlap_y = min(max_a[1], max_b[1]) - max(min_a[1], min_b[1])
|
|
166
|
+
overlap_z = min(max_a[2], max_b[2]) - max(min_a[2], min_b[2])
|
|
167
|
+
|
|
168
|
+
if overlap_x <= 0 or overlap_y <= 0 or overlap_z <= 0:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Определяем нормаль и глубину
|
|
172
|
+
collision = CollisionInfo()
|
|
173
|
+
collision.collided = True
|
|
174
|
+
|
|
175
|
+
if overlap_x < overlap_y and overlap_x < overlap_z:
|
|
176
|
+
collision.depth = overlap_x
|
|
177
|
+
collision.normal = np.array([1, 0, 0] if (pos_a[0] < pos_b[0]) else [-1, 0, 0], dtype=np.float32)
|
|
178
|
+
elif overlap_y < overlap_z:
|
|
179
|
+
collision.depth = overlap_y
|
|
180
|
+
collision.normal = np.array([0, 1, 0] if (pos_a[1] < pos_b[1]) else [0, -1, 0], dtype=np.float32)
|
|
181
|
+
else:
|
|
182
|
+
collision.depth = overlap_z
|
|
183
|
+
collision.normal = np.array([0, 0, 1] if (pos_a[2] < pos_b[2]) else [0, 0, -1], dtype=np.float32)
|
|
184
|
+
|
|
185
|
+
# Точка контакта (центр пересечения)
|
|
186
|
+
collision.point = (pos_a + pos_b) / 2
|
|
187
|
+
|
|
188
|
+
return collision
|
|
189
|
+
|
|
190
|
+
def _sphere_vs_sphere(self, a: SphereCollider, b: SphereCollider) -> Optional[CollisionInfo]:
|
|
191
|
+
"""Sphere vs Sphere коллизия"""
|
|
192
|
+
pos_a = self._get_collider_position(a)
|
|
193
|
+
pos_b = self._get_collider_position(b)
|
|
194
|
+
|
|
195
|
+
if pos_a is None or pos_b is None:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
center_a = pos_a + a.center
|
|
199
|
+
center_b = pos_b + b.center
|
|
200
|
+
|
|
201
|
+
distance_vec = center_b - center_a
|
|
202
|
+
distance = np.linalg.norm(distance_vec)
|
|
203
|
+
min_distance = a.radius + b.radius
|
|
204
|
+
|
|
205
|
+
if distance >= min_distance:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
collision = CollisionInfo()
|
|
209
|
+
collision.collided = True
|
|
210
|
+
collision.depth = min_distance - distance
|
|
211
|
+
collision.normal = distance_vec / distance if distance > 0 else np.array([0, 1, 0], dtype=np.float32)
|
|
212
|
+
collision.point = center_a + collision.normal * a.radius
|
|
213
|
+
|
|
214
|
+
return collision
|
|
215
|
+
|
|
216
|
+
def _box_vs_sphere(self, box: BoxCollider, sphere: SphereCollider) -> Optional[CollisionInfo]:
|
|
217
|
+
"""Box vs Sphere коллизия"""
|
|
218
|
+
pos_box = self._get_collider_position(box)
|
|
219
|
+
pos_sphere = self._get_collider_position(sphere)
|
|
220
|
+
|
|
221
|
+
if pos_box is None or pos_sphere is None:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
# Находим ближайшую точку на AABB к центру сферы
|
|
225
|
+
box_min = pos_box + box.center - box.size / 2
|
|
226
|
+
box_max = pos_box + box.center + box.size / 2
|
|
227
|
+
sphere_center = pos_sphere + sphere.center
|
|
228
|
+
|
|
229
|
+
closest_point = np.clip(sphere_center, box_min, box_max)
|
|
230
|
+
distance_vec = sphere_center - closest_point
|
|
231
|
+
distance = np.linalg.norm(distance_vec)
|
|
232
|
+
|
|
233
|
+
if distance >= sphere.radius:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
collision = CollisionInfo()
|
|
237
|
+
collision.collided = True
|
|
238
|
+
collision.depth = sphere.radius - distance
|
|
239
|
+
|
|
240
|
+
if distance > 0:
|
|
241
|
+
collision.normal = distance_vec / distance
|
|
242
|
+
else:
|
|
243
|
+
# Сфера внутри бокса
|
|
244
|
+
collision.normal = np.array([0, 1, 0], dtype=np.float32)
|
|
245
|
+
collision.depth = sphere.radius + min(
|
|
246
|
+
closest_point[0] - box_min[0],
|
|
247
|
+
box_max[0] - closest_point[0],
|
|
248
|
+
closest_point[1] - box_min[1],
|
|
249
|
+
box_max[1] - closest_point[1],
|
|
250
|
+
closest_point[2] - box_min[2],
|
|
251
|
+
box_max[2] - closest_point[2]
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
collision.point = closest_point
|
|
255
|
+
|
|
256
|
+
return collision
|
|
257
|
+
|
|
258
|
+
def _resolve_collisions(self, collisions: List[CollisionInfo], dt: float):
|
|
259
|
+
"""Разрешает коллизии"""
|
|
260
|
+
for collision in collisions:
|
|
261
|
+
if not collision.entity_a or not collision.entity_b:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
body_a = self._get_body_from_entity(collision.entity_a)
|
|
265
|
+
body_b = self._get_body_from_entity(collision.entity_b)
|
|
266
|
+
|
|
267
|
+
if not body_a or not body_b:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
# Для триггеров только уведомляем
|
|
271
|
+
collider_a = collision.entity_a.get_component("Collider") or \
|
|
272
|
+
collision.entity_a.get_component("BoxCollider") or \
|
|
273
|
+
collision.entity_a.get_component("SphereCollider")
|
|
274
|
+
collider_b = collision.entity_b.get_component("Collider") or \
|
|
275
|
+
collision.entity_b.get_component("BoxCollider") or \
|
|
276
|
+
collision.entity_b.get_component("SphereCollider")
|
|
277
|
+
|
|
278
|
+
if (collider_a and collider_a.is_trigger) or \
|
|
279
|
+
(collider_b and collider_b.is_trigger):
|
|
280
|
+
# Вызываем событие триггера
|
|
281
|
+
for callback in self.on_trigger_enter:
|
|
282
|
+
callback(collision)
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
# Вычисляем общую инверсную массу
|
|
286
|
+
inv_mass_a = 0 if body_a.is_kinematic else 1.0 / body_a.mass
|
|
287
|
+
inv_mass_b = 0 if body_b.is_kinematic else 1.0 / body_b.mass
|
|
288
|
+
total_inv_mass = inv_mass_a + inv_mass_b
|
|
289
|
+
|
|
290
|
+
if total_inv_mass <= 0:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
# Разделение
|
|
294
|
+
separation = collision.normal * (collision.depth / total_inv_mass)
|
|
295
|
+
|
|
296
|
+
transform_a = collision.entity_a.get_component("Transform")
|
|
297
|
+
transform_b = collision.entity_b.get_component("Transform")
|
|
298
|
+
|
|
299
|
+
if transform_a and not body_a.is_kinematic:
|
|
300
|
+
transform_a.position -= separation * inv_mass_a
|
|
301
|
+
transform_a._matrix_dirty = True
|
|
302
|
+
|
|
303
|
+
if transform_b and not body_b.is_kinematic:
|
|
304
|
+
transform_b.position += separation * inv_mass_b
|
|
305
|
+
transform_b._matrix_dirty = True
|
|
306
|
+
|
|
307
|
+
# Обновляем скорости (простое отражение с коэффициентом)
|
|
308
|
+
restitution = 0.3 # Коэффициент упругости
|
|
309
|
+
|
|
310
|
+
relative_velocity = body_b.velocity - body_a.velocity
|
|
311
|
+
velocity_along_normal = np.dot(relative_velocity, collision.normal)
|
|
312
|
+
|
|
313
|
+
if velocity_along_normal > 0:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
impulse = collision.normal * (-(1 + restitution) * velocity_along_normal / total_inv_mass)
|
|
317
|
+
|
|
318
|
+
if not body_a.is_kinematic:
|
|
319
|
+
body_a.velocity -= impulse * inv_mass_a
|
|
320
|
+
if not body_b.is_kinematic:
|
|
321
|
+
body_b.velocity += impulse * inv_mass_b
|
|
322
|
+
|
|
323
|
+
# Трение
|
|
324
|
+
tangent = relative_velocity - collision.normal * velocity_along_normal
|
|
325
|
+
tangent_norm = np.linalg.norm(tangent)
|
|
326
|
+
|
|
327
|
+
if tangent_norm > 0.001:
|
|
328
|
+
tangent /= tangent_norm
|
|
329
|
+
friction_coefficient = 0.5
|
|
330
|
+
friction_impulse = tangent * (-friction_coefficient * np.linalg.norm(impulse))
|
|
331
|
+
|
|
332
|
+
if not body_a.is_kinematic:
|
|
333
|
+
body_a.velocity -= friction_impulse * inv_mass_a
|
|
334
|
+
if not body_b.is_kinematic:
|
|
335
|
+
body_b.velocity += friction_impulse * inv_mass_b
|
|
336
|
+
|
|
337
|
+
# Вызываем событие коллизии
|
|
338
|
+
for callback in self.on_collision_enter:
|
|
339
|
+
callback(collision)
|
|
340
|
+
|
|
341
|
+
def _get_collider_position(self, collider: Collider):
|
|
342
|
+
"""Получает мировую позицию коллайдера из трансформа"""
|
|
343
|
+
if hasattr(collider, 'entity') and collider.entity:
|
|
344
|
+
transform = collider.entity.get_component("Transform")
|
|
345
|
+
if transform:
|
|
346
|
+
return transform.position.copy()
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def _get_body_from_entity(self, entity):
|
|
350
|
+
"""Находит RigidbodyComponent у entity"""
|
|
351
|
+
return entity.get_component("Rigidbody")
|
|
352
|
+
|
|
353
|
+
def _check_sleeping(self, dt: float):
|
|
354
|
+
"""Проверяет, можно ли усыпить тело"""
|
|
355
|
+
for body in self.bodies:
|
|
356
|
+
if body.is_kinematic:
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
speed = np.linalg.norm(body.velocity)
|
|
360
|
+
|
|
361
|
+
if speed < self.sleep_threshold:
|
|
362
|
+
body._sleep_timer += dt
|
|
363
|
+
if body._sleep_timer >= self.sleep_time_threshold:
|
|
364
|
+
body.is_sleeping = True
|
|
365
|
+
else:
|
|
366
|
+
body._sleep_timer = 0
|
|
367
|
+
body.is_sleeping = False
|
|
368
|
+
|
|
369
|
+
def raycast(self, origin: np.ndarray, direction: np.ndarray, max_distance=100.0):
|
|
370
|
+
"""Пуск луча и поиск пересечений"""
|
|
371
|
+
ray_dir = direction / np.linalg.norm(direction)
|
|
372
|
+
closest_hit = None
|
|
373
|
+
closest_distance = max_distance
|
|
374
|
+
|
|
375
|
+
for body, collider in self.colliders:
|
|
376
|
+
pos = self._get_collider_position(collider)
|
|
377
|
+
if pos is None:
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
hit_distance = None
|
|
381
|
+
|
|
382
|
+
if isinstance(collider, BoxCollider):
|
|
383
|
+
hit_distance = self._ray_vs_aabb(origin, ray_dir, pos, collider)
|
|
384
|
+
elif isinstance(collider, SphereCollider):
|
|
385
|
+
hit_distance = self._ray_vs_sphere(origin, ray_dir, pos, collider)
|
|
386
|
+
|
|
387
|
+
if hit_distance and hit_distance < closest_distance:
|
|
388
|
+
closest_distance = hit_distance
|
|
389
|
+
from aether.physics.raycast import RaycastHit
|
|
390
|
+
closest_hit = RaycastHit()
|
|
391
|
+
closest_hit.hit = True
|
|
392
|
+
closest_hit.distance = hit_distance
|
|
393
|
+
closest_hit.point = origin + ray_dir * hit_distance
|
|
394
|
+
if hasattr(body, 'entity'):
|
|
395
|
+
closest_hit.entity = body.entity
|
|
396
|
+
|
|
397
|
+
return closest_hit
|
|
398
|
+
|
|
399
|
+
def _ray_vs_aabb(self, origin, direction, box_pos, collider: BoxCollider):
|
|
400
|
+
"""Луч vs AABB"""
|
|
401
|
+
box_min = box_pos + collider.center - collider.size / 2
|
|
402
|
+
box_max = box_pos + collider.center + collider.size / 2
|
|
403
|
+
|
|
404
|
+
t_min = (box_min - origin) / (direction + 1e-10)
|
|
405
|
+
t_max = (box_max - origin) / (direction + 1e-10)
|
|
406
|
+
|
|
407
|
+
t1 = np.minimum(t_min, t_max)
|
|
408
|
+
t2 = np.maximum(t_min, t_max)
|
|
409
|
+
|
|
410
|
+
t_near = np.max(t1)
|
|
411
|
+
t_far = np.min(t2)
|
|
412
|
+
|
|
413
|
+
if t_near <= t_far and t_far >= 0:
|
|
414
|
+
return t_near if t_near >= 0 else t_far
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
def _ray_vs_sphere(self, origin, direction, sphere_pos, collider: SphereCollider):
|
|
418
|
+
"""Луч vs Sphere"""
|
|
419
|
+
center = sphere_pos + collider.center
|
|
420
|
+
oc = origin - center
|
|
421
|
+
|
|
422
|
+
a = np.dot(direction, direction)
|
|
423
|
+
b = 2.0 * np.dot(oc, direction)
|
|
424
|
+
c = np.dot(oc, oc) - collider.radius ** 2
|
|
425
|
+
|
|
426
|
+
discriminant = b * b - 4 * a * c
|
|
427
|
+
|
|
428
|
+
if discriminant < 0:
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
t = (-b - np.sqrt(discriminant)) / (2 * a)
|
|
432
|
+
|
|
433
|
+
if t >= 0:
|
|
434
|
+
return t
|
|
435
|
+
|
|
436
|
+
return None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Raycasting"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Ray:
|
|
6
|
+
def __init__(self, origin, direction):
|
|
7
|
+
self.origin = np.array(origin, dtype=np.float32)
|
|
8
|
+
self.direction = np.array(direction, dtype=np.float32)
|
|
9
|
+
self.direction = self.direction / np.linalg.norm(self.direction)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RaycastHit:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.hit = False
|
|
15
|
+
self.point = None
|
|
16
|
+
self.normal = None
|
|
17
|
+
self.distance = 0.0
|
|
18
|
+
self.entity = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def raycast(ray: Ray, colliders, max_distance=100.0) -> RaycastHit:
|
|
22
|
+
"""Простой raycast против AABB"""
|
|
23
|
+
hit = RaycastHit()
|
|
24
|
+
# TODO: реализовать
|
|
25
|
+
return hit
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""База данных ассетов с GUID"""
|
|
2
|
+
import uuid
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AssetInfo:
|
|
9
|
+
"""Информация об ассете"""
|
|
10
|
+
def __init__(self, path: str, asset_type: str):
|
|
11
|
+
self.guid = str(uuid.uuid4())
|
|
12
|
+
self.path = Path(path)
|
|
13
|
+
self.asset_type = asset_type
|
|
14
|
+
self.dependencies = []
|
|
15
|
+
self.metadata = {}
|
|
16
|
+
|
|
17
|
+
def to_dict(self) -> dict:
|
|
18
|
+
return {
|
|
19
|
+
'guid': self.guid,
|
|
20
|
+
'path': str(self.path),
|
|
21
|
+
'type': self.asset_type,
|
|
22
|
+
'dependencies': self.dependencies,
|
|
23
|
+
'metadata': self.metadata
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_dict(cls, data: dict) -> 'AssetInfo':
|
|
28
|
+
asset = cls(data['path'], data['type'])
|
|
29
|
+
asset.guid = data['guid']
|
|
30
|
+
asset.dependencies = data.get('dependencies', [])
|
|
31
|
+
asset.metadata = data.get('metadata', {})
|
|
32
|
+
return asset
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AssetDatabase:
|
|
36
|
+
"""База данных всех ассетов проекта"""
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self._assets: Dict[str, AssetInfo] = {}
|
|
40
|
+
self._guid_index: Dict[str, AssetInfo] = {}
|
|
41
|
+
|
|
42
|
+
def register_asset(self, path: str, asset_type: str) -> AssetInfo:
|
|
43
|
+
"""Регистрирует ассет"""
|
|
44
|
+
asset = AssetInfo(path, asset_type)
|
|
45
|
+
self._assets[str(asset.path)] = asset
|
|
46
|
+
self._guid_index[asset.guid] = asset
|
|
47
|
+
return asset
|
|
48
|
+
|
|
49
|
+
def get_by_path(self, path: str) -> Optional[AssetInfo]:
|
|
50
|
+
return self._assets.get(path)
|
|
51
|
+
|
|
52
|
+
def get_by_guid(self, guid: str) -> Optional[AssetInfo]:
|
|
53
|
+
return self._guid_index.get(guid)
|
|
54
|
+
|
|
55
|
+
def save(self, path: str):
|
|
56
|
+
"""Сохраняет базу данных"""
|
|
57
|
+
data = {
|
|
58
|
+
'version': '1.0',
|
|
59
|
+
'assets': [a.to_dict() for a in self._assets.values()]
|
|
60
|
+
}
|
|
61
|
+
with open(path, 'w') as f:
|
|
62
|
+
json.dump(data, f, indent=2)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def load(cls, path: str) -> 'AssetDatabase':
|
|
66
|
+
"""Загружает базу данных"""
|
|
67
|
+
with open(path, 'r') as f:
|
|
68
|
+
data = json.load(f)
|
|
69
|
+
|
|
70
|
+
db = cls()
|
|
71
|
+
for asset_data in data.get('assets', []):
|
|
72
|
+
asset = AssetInfo.from_dict(asset_data)
|
|
73
|
+
db._assets[str(asset.path)] = asset
|
|
74
|
+
db._guid_index[asset.guid] = asset
|
|
75
|
+
|
|
76
|
+
return db
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Менеджер ресурсов с асинхронной загрузкой и кэшированием"""
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Dict, Any, Callable, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from queue import Queue
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Resource:
|
|
10
|
+
"""Базовый класс ресурса"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, path: str):
|
|
13
|
+
self.path = Path(path)
|
|
14
|
+
self.loaded = False
|
|
15
|
+
self.reference_count = 0
|
|
16
|
+
|
|
17
|
+
def load(self):
|
|
18
|
+
"""Загружает ресурс"""
|
|
19
|
+
self.loaded = True
|
|
20
|
+
|
|
21
|
+
def unload(self):
|
|
22
|
+
"""Выгружает ресурс"""
|
|
23
|
+
self.loaded = False
|
|
24
|
+
|
|
25
|
+
def add_reference(self):
|
|
26
|
+
self.reference_count += 1
|
|
27
|
+
|
|
28
|
+
def remove_reference(self):
|
|
29
|
+
self.reference_count = max(0, self.reference_count - 1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ResourceManager:
|
|
33
|
+
"""Менеджер ресурсов с кэшированием"""
|
|
34
|
+
|
|
35
|
+
_instance = None
|
|
36
|
+
|
|
37
|
+
def __new__(cls):
|
|
38
|
+
if cls._instance is None:
|
|
39
|
+
cls._instance = super().__new__(cls)
|
|
40
|
+
cls._instance._initialize()
|
|
41
|
+
return cls._instance
|
|
42
|
+
|
|
43
|
+
def _initialize(self):
|
|
44
|
+
"""Инициализация менеджера"""
|
|
45
|
+
self._cache: Dict[str, Resource] = {}
|
|
46
|
+
self._loaders: Dict[str, Callable] = {}
|
|
47
|
+
self._load_queue = Queue()
|
|
48
|
+
self._async_loading = False
|
|
49
|
+
self._loader_thread = None
|
|
50
|
+
print("[ResourceManager] Initialized")
|
|
51
|
+
|
|
52
|
+
def register_loader(self, extension: str, loader: Callable):
|
|
53
|
+
"""Регистрирует загрузчик для расширения"""
|
|
54
|
+
self._loaders[extension.lower()] = loader
|
|
55
|
+
print(f"[ResourceManager] Registered loader for .{extension}")
|
|
56
|
+
|
|
57
|
+
def load(self, path: str) -> Optional[Resource]:
|
|
58
|
+
"""Загружает ресурс (синхронно)"""
|
|
59
|
+
key = str(Path(path).resolve())
|
|
60
|
+
|
|
61
|
+
# Проверяем кэш
|
|
62
|
+
if key in self._cache:
|
|
63
|
+
resource = self._cache[key]
|
|
64
|
+
resource.add_reference()
|
|
65
|
+
return resource
|
|
66
|
+
|
|
67
|
+
# Определяем расширение
|
|
68
|
+
ext = Path(path).suffix.lower()
|
|
69
|
+
if ext not in self._loaders:
|
|
70
|
+
print(f"[ResourceManager] No loader for {ext}")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# Загружаем
|
|
74
|
+
try:
|
|
75
|
+
resource = self._loaders[ext](path)
|
|
76
|
+
if resource:
|
|
77
|
+
resource.load()
|
|
78
|
+
resource.add_reference()
|
|
79
|
+
self._cache[key] = resource
|
|
80
|
+
print(f"[ResourceManager] Loaded: {path}")
|
|
81
|
+
return resource
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f"[ResourceManager] Failed to load {path}: {e}")
|
|
84
|
+
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def load_async(self, path: str, callback: Callable = None):
|
|
88
|
+
"""Загружает ресурс асинхронно"""
|
|
89
|
+
self._load_queue.put((path, callback))
|
|
90
|
+
|
|
91
|
+
if not self._async_loading:
|
|
92
|
+
self._start_async_loader()
|
|
93
|
+
|
|
94
|
+
def _start_async_loader(self):
|
|
95
|
+
"""Запускает поток асинхронной загрузки"""
|
|
96
|
+
self._async_loading = True
|
|
97
|
+
self._loader_thread = threading.Thread(target=self._async_loader_loop, daemon=True)
|
|
98
|
+
self._loader_thread.start()
|
|
99
|
+
|
|
100
|
+
def _async_loader_loop(self):
|
|
101
|
+
"""Цикл асинхронной загрузки"""
|
|
102
|
+
while self._async_loading:
|
|
103
|
+
try:
|
|
104
|
+
if not self._load_queue.empty():
|
|
105
|
+
path, callback = self._load_queue.get()
|
|
106
|
+
resource = self.load(path)
|
|
107
|
+
if callback:
|
|
108
|
+
callback(resource)
|
|
109
|
+
else:
|
|
110
|
+
time.sleep(0.01)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"[ResourceManager] Async load error: {e}")
|
|
113
|
+
|
|
114
|
+
def unload(self, path: str):
|
|
115
|
+
"""Выгружает ресурс"""
|
|
116
|
+
key = str(Path(path).resolve())
|
|
117
|
+
if key in self._cache:
|
|
118
|
+
resource = self._cache[key]
|
|
119
|
+
resource.remove_reference()
|
|
120
|
+
if resource.reference_count <= 0:
|
|
121
|
+
resource.unload()
|
|
122
|
+
del self._cache[key]
|
|
123
|
+
print(f"[ResourceManager] Unloaded: {path}")
|
|
124
|
+
|
|
125
|
+
def get(self, path: str) -> Optional[Resource]:
|
|
126
|
+
"""Получает ресурс из кэша"""
|
|
127
|
+
key = str(Path(path).resolve())
|
|
128
|
+
return self._cache.get(key)
|
|
129
|
+
|
|
130
|
+
def clear_cache(self):
|
|
131
|
+
"""Очищает весь кэш"""
|
|
132
|
+
for resource in self._cache.values():
|
|
133
|
+
resource.unload()
|
|
134
|
+
self._cache.clear()
|
|
135
|
+
print("[ResourceManager] Cache cleared")
|
|
136
|
+
|
|
137
|
+
def get_stats(self) -> dict:
|
|
138
|
+
"""Возвращает статистику"""
|
|
139
|
+
total_memory = sum(
|
|
140
|
+
getattr(r, 'memory_usage', 0)
|
|
141
|
+
for r in self._cache.values()
|
|
142
|
+
)
|
|
143
|
+
return {
|
|
144
|
+
'cached_resources': len(self._cache),
|
|
145
|
+
'total_memory_mb': total_memory / (1024 * 1024),
|
|
146
|
+
'queue_size': self._load_queue.qsize()
|
|
147
|
+
}
|