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,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