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,409 @@
|
|
|
1
|
+
"""Система сохранения и загрузки игрового прогресса"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import pickle
|
|
5
|
+
import base64
|
|
6
|
+
import zlib
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Dict, Any, List, Optional, Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SaveData:
|
|
14
|
+
"""Данные сохранения"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, slot_name: str = "auto"):
|
|
17
|
+
self.slot_name = slot_name
|
|
18
|
+
self.timestamp = datetime.now().isoformat()
|
|
19
|
+
self.version = "1.0"
|
|
20
|
+
self.playtime = 0.0 # Время игры в секундах
|
|
21
|
+
|
|
22
|
+
# Данные сцены
|
|
23
|
+
self.scene_name = ""
|
|
24
|
+
self.scene_data = {}
|
|
25
|
+
|
|
26
|
+
# Пользовательские данные
|
|
27
|
+
self.custom_data: Dict[str, Any] = {}
|
|
28
|
+
|
|
29
|
+
# Метаданные
|
|
30
|
+
self.metadata = {
|
|
31
|
+
'save_version': 1,
|
|
32
|
+
'game_version': '1.0',
|
|
33
|
+
'checksum': '',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def set_custom(self, key: str, value: Any):
|
|
37
|
+
"""Сохраняет пользовательские данные"""
|
|
38
|
+
self.custom_data[key] = value
|
|
39
|
+
|
|
40
|
+
def get_custom(self, key: str, default: Any = None) -> Any:
|
|
41
|
+
"""Получает пользовательские данные"""
|
|
42
|
+
return self.custom_data.get(key, default)
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
"""Сериализует в словарь"""
|
|
46
|
+
return {
|
|
47
|
+
'slot_name': self.slot_name,
|
|
48
|
+
'timestamp': self.timestamp,
|
|
49
|
+
'version': self.version,
|
|
50
|
+
'playtime': self.playtime,
|
|
51
|
+
'scene_name': self.scene_name,
|
|
52
|
+
'scene_data': self.scene_data,
|
|
53
|
+
'custom_data': self.custom_data,
|
|
54
|
+
'metadata': self.metadata,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, data: dict) -> 'SaveData':
|
|
59
|
+
"""Создаёт из словаря"""
|
|
60
|
+
save = cls(data.get('slot_name', 'unknown'))
|
|
61
|
+
save.timestamp = data.get('timestamp', '')
|
|
62
|
+
save.version = data.get('version', '1.0')
|
|
63
|
+
save.playtime = data.get('playtime', 0.0)
|
|
64
|
+
save.scene_name = data.get('scene_name', '')
|
|
65
|
+
save.scene_data = data.get('scene_data', {})
|
|
66
|
+
save.custom_data = data.get('custom_data', {})
|
|
67
|
+
save.metadata = data.get('metadata', {})
|
|
68
|
+
return save
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SaveSlot:
|
|
72
|
+
"""Слот сохранения"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, slot_id: int, name: str = ""):
|
|
75
|
+
self.id = slot_id
|
|
76
|
+
self.name = name or f"Save {slot_id + 1}"
|
|
77
|
+
self.save_data: Optional[SaveData] = None
|
|
78
|
+
self.empty = True
|
|
79
|
+
self.screenshot_path = ""
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict:
|
|
82
|
+
return {
|
|
83
|
+
'id': self.id,
|
|
84
|
+
'name': self.name,
|
|
85
|
+
'empty': self.empty,
|
|
86
|
+
'screenshot_path': self.screenshot_path,
|
|
87
|
+
'save_data': self.save_data.to_dict() if self.save_data else None,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SaveSystem:
|
|
92
|
+
"""Система сохранения и загрузки"""
|
|
93
|
+
|
|
94
|
+
_instance = None
|
|
95
|
+
|
|
96
|
+
def __new__(cls):
|
|
97
|
+
if cls._instance is None:
|
|
98
|
+
cls._instance = super().__new__(cls)
|
|
99
|
+
cls._instance._initialize()
|
|
100
|
+
return cls._instance
|
|
101
|
+
|
|
102
|
+
def _initialize(self):
|
|
103
|
+
"""Инициализация системы сохранения"""
|
|
104
|
+
self._save_dir = Path("./saves")
|
|
105
|
+
self._max_slots = 10
|
|
106
|
+
self._slots: List[SaveSlot] = []
|
|
107
|
+
self._auto_save_enabled = True
|
|
108
|
+
self._auto_save_interval = 300 # 5 минут
|
|
109
|
+
self._auto_save_timer = 0.0
|
|
110
|
+
self._compression_enabled = True
|
|
111
|
+
self._encryption_enabled = False
|
|
112
|
+
self._encryption_key = b"aether_engine_key_2024"
|
|
113
|
+
|
|
114
|
+
# Коллбэки
|
|
115
|
+
self.on_save: List[Callable] = []
|
|
116
|
+
self.on_load: List[Callable] = []
|
|
117
|
+
self.on_slot_created: List[Callable] = []
|
|
118
|
+
|
|
119
|
+
# Создаём директорию
|
|
120
|
+
os.makedirs(self._save_dir, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
# Загружаем информацию о слотах
|
|
123
|
+
self._load_slots_info()
|
|
124
|
+
|
|
125
|
+
print(f"[SaveSystem] Initialized ({self._max_slots} slots)")
|
|
126
|
+
|
|
127
|
+
def _load_slots_info(self):
|
|
128
|
+
"""Загружает информацию о слотах"""
|
|
129
|
+
slots_file = self._save_dir / "slots_info.json"
|
|
130
|
+
|
|
131
|
+
if slots_file.exists():
|
|
132
|
+
try:
|
|
133
|
+
with open(slots_file, 'r') as f:
|
|
134
|
+
data = json.load(f)
|
|
135
|
+
|
|
136
|
+
self._slots = []
|
|
137
|
+
for slot_data in data.get('slots', []):
|
|
138
|
+
slot = SaveSlot(slot_data['id'], slot_data['name'])
|
|
139
|
+
slot.empty = slot_data.get('empty', True)
|
|
140
|
+
slot.screenshot_path = slot_data.get('screenshot_path', '')
|
|
141
|
+
self._slots.append(slot)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"[SaveSystem] Failed to load slots info: {e}")
|
|
144
|
+
self._create_default_slots()
|
|
145
|
+
else:
|
|
146
|
+
self._create_default_slots()
|
|
147
|
+
|
|
148
|
+
def _create_default_slots(self):
|
|
149
|
+
"""Создаёт слоты по умолчанию"""
|
|
150
|
+
self._slots = []
|
|
151
|
+
for i in range(self._max_slots):
|
|
152
|
+
self._slots.append(SaveSlot(i))
|
|
153
|
+
self._save_slots_info()
|
|
154
|
+
|
|
155
|
+
def _save_slots_info(self):
|
|
156
|
+
"""Сохраняет информацию о слотах"""
|
|
157
|
+
slots_file = self._save_dir / "slots_info.json"
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
data = {
|
|
161
|
+
'slots': [slot.to_dict() for slot in self._slots]
|
|
162
|
+
}
|
|
163
|
+
with open(slots_file, 'w') as f:
|
|
164
|
+
json.dump(data, f, indent=2)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print(f"[SaveSystem] Failed to save slots info: {e}")
|
|
167
|
+
|
|
168
|
+
def save(self, slot_id: int, save_data: SaveData,
|
|
169
|
+
screenshot_data: bytes = None) -> bool:
|
|
170
|
+
"""
|
|
171
|
+
Сохраняет данные в слот.
|
|
172
|
+
Возвращает True при успехе.
|
|
173
|
+
"""
|
|
174
|
+
if slot_id < 0 or slot_id >= len(self._slots):
|
|
175
|
+
print(f"[SaveSystem] Invalid slot ID: {slot_id}")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
slot = self._slots[slot_id]
|
|
180
|
+
|
|
181
|
+
# Обновляем временную метку
|
|
182
|
+
save_data.timestamp = datetime.now().isoformat()
|
|
183
|
+
|
|
184
|
+
# Сериализуем
|
|
185
|
+
data = save_data.to_dict()
|
|
186
|
+
|
|
187
|
+
# Сжимаем (опционально)
|
|
188
|
+
if self._compression_enabled:
|
|
189
|
+
serialized = self._compress(json.dumps(data))
|
|
190
|
+
else:
|
|
191
|
+
serialized = json.dumps(data).encode()
|
|
192
|
+
|
|
193
|
+
# Шифруем (опционально)
|
|
194
|
+
if self._encryption_enabled:
|
|
195
|
+
serialized = self._encrypt(serialized)
|
|
196
|
+
|
|
197
|
+
# Сохраняем файл
|
|
198
|
+
save_file = self._save_dir / f"slot_{slot_id}.sav"
|
|
199
|
+
with open(save_file, 'wb') as f:
|
|
200
|
+
f.write(serialized)
|
|
201
|
+
|
|
202
|
+
# Сохраняем скриншот
|
|
203
|
+
if screenshot_data:
|
|
204
|
+
screenshot_file = self._save_dir / f"slot_{slot_id}.png"
|
|
205
|
+
with open(screenshot_file, 'wb') as f:
|
|
206
|
+
f.write(screenshot_data)
|
|
207
|
+
slot.screenshot_path = str(screenshot_file)
|
|
208
|
+
|
|
209
|
+
# Обновляем слот
|
|
210
|
+
slot.save_data = save_data
|
|
211
|
+
slot.empty = False
|
|
212
|
+
|
|
213
|
+
# Сохраняем метаданные
|
|
214
|
+
self._save_slots_info()
|
|
215
|
+
|
|
216
|
+
# Вызываем коллбэки
|
|
217
|
+
for callback in self.on_save:
|
|
218
|
+
callback(slot_id, save_data)
|
|
219
|
+
|
|
220
|
+
print(f"[SaveSystem] Saved to slot {slot_id}: {slot.name}")
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"[SaveSystem] Failed to save: {e}")
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def load(self, slot_id: int) -> Optional[SaveData]:
|
|
228
|
+
"""
|
|
229
|
+
Загружает данные из слота.
|
|
230
|
+
Возвращает SaveData или None при ошибке.
|
|
231
|
+
"""
|
|
232
|
+
if slot_id < 0 or slot_id >= len(self._slots):
|
|
233
|
+
print(f"[SaveSystem] Invalid slot ID: {slot_id}")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
save_file = self._save_dir / f"slot_{slot_id}.sav"
|
|
238
|
+
|
|
239
|
+
if not save_file.exists():
|
|
240
|
+
print(f"[SaveSystem] No save file in slot {slot_id}")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Читаем файл
|
|
244
|
+
with open(save_file, 'rb') as f:
|
|
245
|
+
serialized = f.read()
|
|
246
|
+
|
|
247
|
+
# Расшифровываем
|
|
248
|
+
if self._encryption_enabled:
|
|
249
|
+
serialized = self._decrypt(serialized)
|
|
250
|
+
|
|
251
|
+
# Распаковываем
|
|
252
|
+
if self._compression_enabled:
|
|
253
|
+
data_str = self._decompress(serialized)
|
|
254
|
+
else:
|
|
255
|
+
data_str = serialized.decode()
|
|
256
|
+
|
|
257
|
+
# Десериализуем
|
|
258
|
+
data = json.loads(data_str)
|
|
259
|
+
save_data = SaveData.from_dict(data)
|
|
260
|
+
|
|
261
|
+
# Обновляем слот
|
|
262
|
+
self._slots[slot_id].save_data = save_data
|
|
263
|
+
self._slots[slot_id].empty = False
|
|
264
|
+
|
|
265
|
+
# Вызываем коллбэки
|
|
266
|
+
for callback in self.on_load:
|
|
267
|
+
callback(slot_id, save_data)
|
|
268
|
+
|
|
269
|
+
print(f"[SaveSystem] Loaded from slot {slot_id}: {self._slots[slot_id].name}")
|
|
270
|
+
return save_data
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print(f"[SaveSystem] Failed to load: {e}")
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
def delete_save(self, slot_id: int) -> bool:
|
|
277
|
+
"""Удаляет сохранение из слота"""
|
|
278
|
+
if slot_id < 0 or slot_id >= len(self._slots):
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
# Удаляем файлы
|
|
283
|
+
save_file = self._save_dir / f"slot_{slot_id}.sav"
|
|
284
|
+
screenshot_file = self._save_dir / f"slot_{slot_id}.png"
|
|
285
|
+
|
|
286
|
+
if save_file.exists():
|
|
287
|
+
os.remove(save_file)
|
|
288
|
+
if screenshot_file.exists():
|
|
289
|
+
os.remove(screenshot_file)
|
|
290
|
+
|
|
291
|
+
# Очищаем слот
|
|
292
|
+
self._slots[slot_id].save_data = None
|
|
293
|
+
self._slots[slot_id].empty = True
|
|
294
|
+
self._slots[slot_id].screenshot_path = ""
|
|
295
|
+
|
|
296
|
+
self._save_slots_info()
|
|
297
|
+
|
|
298
|
+
print(f"[SaveSystem] Deleted save in slot {slot_id}")
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
print(f"[SaveSystem] Failed to delete save: {e}")
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def get_slot_info(self, slot_id: int) -> Optional[SaveSlot]:
|
|
306
|
+
"""Возвращает информацию о слоте"""
|
|
307
|
+
if slot_id < 0 or slot_id >= len(self._slots):
|
|
308
|
+
return None
|
|
309
|
+
return self._slots[slot_id]
|
|
310
|
+
|
|
311
|
+
def get_all_slots(self) -> List[SaveSlot]:
|
|
312
|
+
"""Возвращает все слоты"""
|
|
313
|
+
return self._slots.copy()
|
|
314
|
+
|
|
315
|
+
def get_all_saves(self) -> List[SaveData]:
|
|
316
|
+
"""Возвращает все сохранения"""
|
|
317
|
+
saves = []
|
|
318
|
+
for slot in self._slots:
|
|
319
|
+
if not slot.empty:
|
|
320
|
+
# Загружаем, если ещё не загружено
|
|
321
|
+
if slot.save_data is None:
|
|
322
|
+
slot.save_data = self.load(slot.id)
|
|
323
|
+
if slot.save_data:
|
|
324
|
+
saves.append(slot.save_data)
|
|
325
|
+
return saves
|
|
326
|
+
|
|
327
|
+
def quick_save(self, save_data: SaveData) -> bool:
|
|
328
|
+
"""Быстрое сохранение в первый свободный слот"""
|
|
329
|
+
for slot in self._slots:
|
|
330
|
+
if slot.empty:
|
|
331
|
+
return self.save(slot.id, save_data)
|
|
332
|
+
|
|
333
|
+
# Если нет свободных — перезаписываем последний
|
|
334
|
+
return self.save(self._max_slots - 1, save_data)
|
|
335
|
+
|
|
336
|
+
def quick_load(self) -> Optional[SaveData]:
|
|
337
|
+
"""Быстрая загрузка последнего сохранения"""
|
|
338
|
+
for slot in reversed(self._slots):
|
|
339
|
+
if not slot.empty:
|
|
340
|
+
return self.load(slot.id)
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
def auto_save(self, save_data: SaveData):
|
|
344
|
+
"""Автосохранение"""
|
|
345
|
+
if not self._auto_save_enabled:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
save_data.slot_name = "auto"
|
|
349
|
+
self.quick_save(save_data)
|
|
350
|
+
|
|
351
|
+
def update(self, dt: float):
|
|
352
|
+
"""Обновление таймера автосохранения"""
|
|
353
|
+
if self._auto_save_enabled:
|
|
354
|
+
self._auto_save_timer += dt
|
|
355
|
+
if self._auto_save_timer >= self._auto_save_interval:
|
|
356
|
+
self._auto_save_timer = 0.0
|
|
357
|
+
# Автосохранение будет вызвано извне
|
|
358
|
+
|
|
359
|
+
def export_save(self, slot_id: int, path: str) -> bool:
|
|
360
|
+
"""Экспортирует сохранение в файл"""
|
|
361
|
+
save_data = self.load(slot_id)
|
|
362
|
+
if not save_data:
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
with open(path, 'w') as f:
|
|
367
|
+
json.dump(save_data.to_dict(), f, indent=2)
|
|
368
|
+
return True
|
|
369
|
+
except Exception as e:
|
|
370
|
+
print(f"[SaveSystem] Failed to export: {e}")
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
def import_save(self, path: str) -> Optional[SaveData]:
|
|
374
|
+
"""Импортирует сохранение из файла"""
|
|
375
|
+
try:
|
|
376
|
+
with open(path, 'r') as f:
|
|
377
|
+
data = json.load(f)
|
|
378
|
+
|
|
379
|
+
save_data = SaveData.from_dict(data)
|
|
380
|
+
return save_data
|
|
381
|
+
except Exception as e:
|
|
382
|
+
print(f"[SaveSystem] Failed to import: {e}")
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
def _compress(self, data: str) -> bytes:
|
|
386
|
+
"""Сжимает данные"""
|
|
387
|
+
return zlib.compress(data.encode())
|
|
388
|
+
|
|
389
|
+
def _decompress(self, data: bytes) -> str:
|
|
390
|
+
"""Распаковывает данные"""
|
|
391
|
+
return zlib.decompress(data).decode()
|
|
392
|
+
|
|
393
|
+
def _encrypt(self, data: bytes) -> bytes:
|
|
394
|
+
"""Шифрует данные (простое XOR)"""
|
|
395
|
+
key = self._encryption_key
|
|
396
|
+
return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])
|
|
397
|
+
|
|
398
|
+
def _decrypt(self, data: bytes) -> bytes:
|
|
399
|
+
"""Расшифровывает данные"""
|
|
400
|
+
return self._encrypt(data) # XOR — симметричный
|
|
401
|
+
|
|
402
|
+
def set_encryption_key(self, key: bytes):
|
|
403
|
+
"""Устанавливает ключ шифрования"""
|
|
404
|
+
self._encryption_key = key
|
|
405
|
+
|
|
406
|
+
def cleanup(self):
|
|
407
|
+
"""Очистка"""
|
|
408
|
+
self._save_slots_info()
|
|
409
|
+
print("[SaveSystem] Cleanup complete")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Управление временем и FPS"""
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Time:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.delta_time = 0.0
|
|
8
|
+
self.total_time = 0.0
|
|
9
|
+
self.fps = 0.0
|
|
10
|
+
self._last_frame = time.perf_counter()
|
|
11
|
+
self._frame_count = 0
|
|
12
|
+
self._fps_timer = 0.0
|
|
13
|
+
|
|
14
|
+
def update(self):
|
|
15
|
+
current = time.perf_counter()
|
|
16
|
+
self.delta_time = current - self._last_frame
|
|
17
|
+
self._last_frame = current
|
|
18
|
+
self.total_time += self.delta_time
|
|
19
|
+
|
|
20
|
+
self._frame_count += 1
|
|
21
|
+
self._fps_timer += self.delta_time
|
|
22
|
+
if self._fps_timer >= 1.0:
|
|
23
|
+
self.fps = self._frame_count / self._fps_timer
|
|
24
|
+
self._frame_count = 0
|
|
25
|
+
self._fps_timer = 0.0
|
aether/core/window.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Окно и OpenGL-контекст"""
|
|
2
|
+
import glfw
|
|
3
|
+
from OpenGL.GL import *
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Window:
|
|
7
|
+
def __init__(self, width, height, title, fullscreen, msaa_samples):
|
|
8
|
+
if not glfw.init():
|
|
9
|
+
raise RuntimeError("Failed to initialize GLFW")
|
|
10
|
+
|
|
11
|
+
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
|
|
12
|
+
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 6)
|
|
13
|
+
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
|
|
14
|
+
glfw.window_hint(glfw.SAMPLES, msaa_samples)
|
|
15
|
+
|
|
16
|
+
monitor = glfw.get_primary_monitor() if fullscreen else None
|
|
17
|
+
self._handle = glfw.create_window(width, height, title, monitor, None)
|
|
18
|
+
|
|
19
|
+
if not self._handle:
|
|
20
|
+
glfw.terminate()
|
|
21
|
+
raise RuntimeError("Failed to create window")
|
|
22
|
+
|
|
23
|
+
glfw.make_context_current(self._handle)
|
|
24
|
+
glfw.swap_interval(1) # vsync
|
|
25
|
+
|
|
26
|
+
# OpenGL state
|
|
27
|
+
glEnable(GL_DEPTH_TEST)
|
|
28
|
+
glEnable(GL_MULTISAMPLE)
|
|
29
|
+
glEnable(GL_CULL_FACE)
|
|
30
|
+
glCullFace(GL_BACK)
|
|
31
|
+
|
|
32
|
+
# Viewport
|
|
33
|
+
self.width = width
|
|
34
|
+
self.height = height
|
|
35
|
+
glViewport(0, 0, width, height)
|
|
36
|
+
|
|
37
|
+
print(f"[Window] Created: {width}x{height} (OpenGL {glGetString(GL_VERSION).decode()})")
|
|
38
|
+
|
|
39
|
+
def should_close(self) -> bool:
|
|
40
|
+
return glfw.window_should_close(self._handle)
|
|
41
|
+
|
|
42
|
+
def swap_buffers(self):
|
|
43
|
+
glfw.swap_buffers(self._handle)
|
|
44
|
+
|
|
45
|
+
def poll_events(self):
|
|
46
|
+
glfw.poll_events()
|
|
47
|
+
|
|
48
|
+
def set_title(self, title: str):
|
|
49
|
+
glfw.set_window_title(self._handle, title)
|
|
50
|
+
|
|
51
|
+
def destroy(self):
|
|
52
|
+
glfw.destroy_window(self._handle)
|
|
53
|
+
glfw.terminate()
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def handle(self):
|
|
57
|
+
return self._handle
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Камера"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Camera:
|
|
6
|
+
def __init__(self, position=(0, 2, 10), yaw=-90, pitch=0, fov=60):
|
|
7
|
+
self.position = np.array(position, dtype=np.float32)
|
|
8
|
+
self.yaw = yaw
|
|
9
|
+
self.pitch = pitch
|
|
10
|
+
self.fov = fov
|
|
11
|
+
self.near = 0.1
|
|
12
|
+
self.far = 1000.0
|
|
13
|
+
self.speed = 5.0
|
|
14
|
+
self.sensitivity = 0.1
|
|
15
|
+
|
|
16
|
+
self._update_vectors()
|
|
17
|
+
|
|
18
|
+
def _update_vectors(self):
|
|
19
|
+
front = np.array([
|
|
20
|
+
np.cos(np.radians(self.yaw)) * np.cos(np.radians(self.pitch)),
|
|
21
|
+
np.sin(np.radians(self.pitch)),
|
|
22
|
+
np.sin(np.radians(self.yaw)) * np.cos(np.radians(self.pitch))
|
|
23
|
+
], dtype=np.float32)
|
|
24
|
+
self.front = front / np.linalg.norm(front)
|
|
25
|
+
self.right = np.cross(self.front, np.array([0, 1, 0], dtype=np.float32))
|
|
26
|
+
self.up = np.cross(self.right, self.front)
|
|
27
|
+
|
|
28
|
+
def get_view_matrix(self) -> np.ndarray:
|
|
29
|
+
target = self.position + self.front
|
|
30
|
+
f = target - self.position
|
|
31
|
+
f = f / np.linalg.norm(f)
|
|
32
|
+
s = np.cross(f, np.array([0, 1, 0], dtype=np.float32))
|
|
33
|
+
s = s / np.linalg.norm(s)
|
|
34
|
+
u = np.cross(s, f)
|
|
35
|
+
|
|
36
|
+
view = np.identity(4, dtype=np.float32)
|
|
37
|
+
view[0, :3] = s
|
|
38
|
+
view[1, :3] = u
|
|
39
|
+
view[2, :3] = -f
|
|
40
|
+
view[3, :3] = -np.array([np.dot(s, self.position), np.dot(u, self.position), np.dot(-f, self.position)])
|
|
41
|
+
return view
|
|
42
|
+
|
|
43
|
+
def get_projection_matrix(self, aspect_ratio: float) -> np.ndarray:
|
|
44
|
+
f = 1.0 / np.tan(np.radians(self.fov) / 2)
|
|
45
|
+
return np.array([
|
|
46
|
+
[f / aspect_ratio, 0, 0, 0],
|
|
47
|
+
[0, f, 0, 0],
|
|
48
|
+
[0, 0, (self.far + self.near) / (self.near - self.far),
|
|
49
|
+
(2 * self.far * self.near) / (self.near - self.far)],
|
|
50
|
+
[0, 0, -1, 0]
|
|
51
|
+
], dtype=np.float32)
|
|
52
|
+
|
|
53
|
+
def move_forward(self, dt):
|
|
54
|
+
self.position += self.front * self.speed * dt
|
|
55
|
+
|
|
56
|
+
def move_backward(self, dt):
|
|
57
|
+
self.position -= self.front * self.speed * dt
|
|
58
|
+
|
|
59
|
+
def move_left(self, dt):
|
|
60
|
+
self.position -= self.right * self.speed * dt
|
|
61
|
+
|
|
62
|
+
def move_right(self, dt):
|
|
63
|
+
self.position += self.right * self.speed * dt
|
|
64
|
+
|
|
65
|
+
def rotate(self, dx, dy):
|
|
66
|
+
self.yaw += dx * self.sensitivity
|
|
67
|
+
self.pitch -= dy * self.sensitivity
|
|
68
|
+
self.pitch = max(-89.0, min(89.0, self.pitch))
|
|
69
|
+
self._update_vectors()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Framebuffer Object для рендеринга в текстуру"""
|
|
2
|
+
from OpenGL.GL import *
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Framebuffer:
|
|
7
|
+
"""OpenGL Framebuffer Object"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, width: int, height: int,
|
|
10
|
+
color_attachments: int = 1,
|
|
11
|
+
depth_attachment: bool = True,
|
|
12
|
+
samples: int = 0):
|
|
13
|
+
self.width = width
|
|
14
|
+
self.height = height
|
|
15
|
+
self.samples = samples
|
|
16
|
+
|
|
17
|
+
self.fbo = glGenFramebuffers(1)
|
|
18
|
+
self.color_textures = []
|
|
19
|
+
self.depth_texture = None
|
|
20
|
+
self.depth_rbo = None
|
|
21
|
+
|
|
22
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.fbo)
|
|
23
|
+
|
|
24
|
+
# Создаём цветовые аттачменты
|
|
25
|
+
for i in range(color_attachments):
|
|
26
|
+
tex = self._create_texture(GL_COLOR_ATTACHMENT0 + i)
|
|
27
|
+
self.color_textures.append(tex)
|
|
28
|
+
|
|
29
|
+
# Буфер глубины
|
|
30
|
+
if depth_attachment:
|
|
31
|
+
if samples > 0:
|
|
32
|
+
self.depth_rbo = self._create_depth_rbo()
|
|
33
|
+
else:
|
|
34
|
+
self.depth_texture = self._create_depth_texture()
|
|
35
|
+
|
|
36
|
+
# Проверка
|
|
37
|
+
if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE:
|
|
38
|
+
raise RuntimeError("Framebuffer is not complete!")
|
|
39
|
+
|
|
40
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
41
|
+
|
|
42
|
+
def _create_texture(self, attachment) -> int:
|
|
43
|
+
"""Создаёт текстуру для цветового аттачмента"""
|
|
44
|
+
tex = glGenTextures(1)
|
|
45
|
+
glBindTexture(GL_TEXTURE_2D, tex)
|
|
46
|
+
|
|
47
|
+
if self.samples > 0:
|
|
48
|
+
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, self.samples,
|
|
49
|
+
GL_RGBA16F, self.width, self.height, GL_TRUE)
|
|
50
|
+
else:
|
|
51
|
+
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F,
|
|
52
|
+
self.width, self.height, 0, GL_RGBA, GL_FLOAT, None)
|
|
53
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
|
54
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
|
55
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
|
56
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
|
57
|
+
|
|
58
|
+
glFramebufferTexture2D(GL_FRAMEBUFFER, attachment,
|
|
59
|
+
GL_TEXTURE_2D_MULTISAMPLE if self.samples > 0 else GL_TEXTURE_2D,
|
|
60
|
+
tex, 0)
|
|
61
|
+
|
|
62
|
+
return tex
|
|
63
|
+
|
|
64
|
+
def _create_depth_texture(self) -> int:
|
|
65
|
+
"""Создаёт текстуру глубины"""
|
|
66
|
+
tex = glGenTextures(1)
|
|
67
|
+
glBindTexture(GL_TEXTURE_2D, tex)
|
|
68
|
+
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24,
|
|
69
|
+
self.width, self.height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, None)
|
|
70
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
|
|
71
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
|
|
72
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
|
73
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
|
74
|
+
|
|
75
|
+
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
|
|
76
|
+
GL_TEXTURE_2D, tex, 0)
|
|
77
|
+
|
|
78
|
+
return tex
|
|
79
|
+
|
|
80
|
+
def _create_depth_rbo(self) -> int:
|
|
81
|
+
"""Создаёт Renderbuffer для глубины (мультисэмплинг)"""
|
|
82
|
+
rbo = glGenRenderbuffers(1)
|
|
83
|
+
glBindRenderbuffer(GL_RENDERBUFFER, rbo)
|
|
84
|
+
glRenderbufferStorageMultisample(GL_RENDERBUFFER, self.samples,
|
|
85
|
+
GL_DEPTH_COMPONENT24,
|
|
86
|
+
self.width, self.height)
|
|
87
|
+
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
|
|
88
|
+
GL_RENDERBUFFER, rbo)
|
|
89
|
+
return rbo
|
|
90
|
+
|
|
91
|
+
def bind(self):
|
|
92
|
+
"""Привязывает Framebuffer"""
|
|
93
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.fbo)
|
|
94
|
+
glViewport(0, 0, self.width, self.height)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def unbind():
|
|
98
|
+
"""Отвязывает Framebuffer (возвращает рендеринг на экран)"""
|
|
99
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
100
|
+
|
|
101
|
+
def resize(self, width: int, height: int):
|
|
102
|
+
"""Изменяет размер Framebuffer"""
|
|
103
|
+
self.cleanup()
|
|
104
|
+
self.__init__(width, height)
|
|
105
|
+
|
|
106
|
+
def cleanup(self):
|
|
107
|
+
"""Очищает ресурсы"""
|
|
108
|
+
for tex in self.color_textures:
|
|
109
|
+
glDeleteTextures(1, [tex])
|
|
110
|
+
if self.depth_texture:
|
|
111
|
+
glDeleteTextures(1, [self.depth_texture])
|
|
112
|
+
if self.depth_rbo:
|
|
113
|
+
glDeleteRenderbuffers(1, [self.depth_rbo])
|
|
114
|
+
glDeleteFramebuffers(1, [self.fbo])
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def color_texture(self) -> int:
|
|
118
|
+
"""Возвращает первую цветовую текстуру"""
|
|
119
|
+
return self.color_textures[0] if self.color_textures else 0
|