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
aether/ui/button.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""UI Button и интерактивные элементы"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from OpenGL.GL import *
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
from aether.ui.text import TextRenderer, Font
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UIElement:
|
|
9
|
+
"""Базовый класс UI элемента"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, x: float = 0, y: float = 0,
|
|
12
|
+
width: float = 200, height: float = 50):
|
|
13
|
+
self.x = x
|
|
14
|
+
self.y = y
|
|
15
|
+
self.width = width
|
|
16
|
+
self.height = height
|
|
17
|
+
self.enabled = True
|
|
18
|
+
self.visible = True
|
|
19
|
+
self.parent = None
|
|
20
|
+
self.children = []
|
|
21
|
+
|
|
22
|
+
# Состояние
|
|
23
|
+
self.is_hovered = False
|
|
24
|
+
self.is_pressed = False
|
|
25
|
+
self.is_focused = False
|
|
26
|
+
|
|
27
|
+
def update(self, dt: float, mouse_x: float, mouse_y: float,
|
|
28
|
+
mouse_pressed: bool, mouse_just_pressed: bool):
|
|
29
|
+
"""Обновляет состояние элемента"""
|
|
30
|
+
if not self.enabled or not self.visible:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# Проверка наведения
|
|
34
|
+
self.is_hovered = self._point_inside(mouse_x, mouse_y)
|
|
35
|
+
|
|
36
|
+
# Обновляем детей
|
|
37
|
+
for child in self.children:
|
|
38
|
+
child.update(dt, mouse_x, mouse_y, mouse_pressed, mouse_just_pressed)
|
|
39
|
+
|
|
40
|
+
def render(self):
|
|
41
|
+
"""Рендерит элемент (переопределяется в наследниках)"""
|
|
42
|
+
if not self.visible:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
for child in self.children:
|
|
46
|
+
child.render()
|
|
47
|
+
|
|
48
|
+
def _point_inside(self, px: float, py: float) -> bool:
|
|
49
|
+
"""Проверяет, находится ли точка внутри элемента"""
|
|
50
|
+
return (self.x <= px <= self.x + self.width and
|
|
51
|
+
self.y <= py <= self.y + self.height)
|
|
52
|
+
|
|
53
|
+
def get_absolute_position(self) -> Tuple[float, float]:
|
|
54
|
+
"""Получает абсолютную позицию с учётом родителей"""
|
|
55
|
+
x, y = self.x, self.y
|
|
56
|
+
if self.parent:
|
|
57
|
+
px, py = self.parent.get_absolute_position()
|
|
58
|
+
x += px
|
|
59
|
+
y += py
|
|
60
|
+
return (x, y)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Button(UIElement):
|
|
64
|
+
"""Кнопка"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, text: str = "Button", x: float = 0, y: float = 0,
|
|
67
|
+
width: float = 200, height: float = 50,
|
|
68
|
+
callback: Callable = None):
|
|
69
|
+
super().__init__(x, y, width, height)
|
|
70
|
+
self.text = text
|
|
71
|
+
self.callback = callback
|
|
72
|
+
|
|
73
|
+
# Визуальные настройки
|
|
74
|
+
self.normal_color = (0.2, 0.3, 0.5, 1.0)
|
|
75
|
+
self.hover_color = (0.3, 0.4, 0.6, 1.0)
|
|
76
|
+
self.press_color = (0.1, 0.2, 0.4, 1.0)
|
|
77
|
+
self.text_color = (1.0, 1.0, 1.0, 1.0)
|
|
78
|
+
self.border_radius = 8.0
|
|
79
|
+
self.font_size = 20
|
|
80
|
+
|
|
81
|
+
self._was_pressed = False
|
|
82
|
+
|
|
83
|
+
def update(self, dt: float, mouse_x: float, mouse_y: float,
|
|
84
|
+
mouse_pressed: bool, mouse_just_pressed: bool):
|
|
85
|
+
super().update(dt, mouse_x, mouse_y, mouse_pressed, mouse_just_pressed)
|
|
86
|
+
|
|
87
|
+
if not self.enabled:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Обработка нажатия
|
|
91
|
+
if self.is_hovered:
|
|
92
|
+
if mouse_just_pressed:
|
|
93
|
+
self.is_pressed = True
|
|
94
|
+
self._was_pressed = True
|
|
95
|
+
elif mouse_pressed and self._was_pressed:
|
|
96
|
+
self.is_pressed = True
|
|
97
|
+
else:
|
|
98
|
+
if self._was_pressed and not mouse_pressed:
|
|
99
|
+
# Клик!
|
|
100
|
+
if self.callback:
|
|
101
|
+
self.callback()
|
|
102
|
+
self.is_pressed = False
|
|
103
|
+
self._was_pressed = False
|
|
104
|
+
else:
|
|
105
|
+
self.is_pressed = False
|
|
106
|
+
self._was_pressed = False
|
|
107
|
+
|
|
108
|
+
def render(self):
|
|
109
|
+
if not self.visible:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Определяем цвет
|
|
113
|
+
if self.is_pressed:
|
|
114
|
+
color = self.press_color
|
|
115
|
+
elif self.is_hovered:
|
|
116
|
+
color = self.hover_color
|
|
117
|
+
else:
|
|
118
|
+
color = self.normal_color
|
|
119
|
+
|
|
120
|
+
# Рисуем фон
|
|
121
|
+
self._draw_rounded_rect(self.x, self.y, self.width, self.height,
|
|
122
|
+
self.border_radius, color)
|
|
123
|
+
|
|
124
|
+
# Рендерим текст
|
|
125
|
+
text_renderer = TextRenderer()
|
|
126
|
+
if text_renderer.default_font:
|
|
127
|
+
text_width, text_height = text_renderer.measure_text(
|
|
128
|
+
self.text, self.font_size / 48.0
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
text_x = self.x + (self.width - text_width) / 2
|
|
132
|
+
text_y = self.y + (self.height - text_height) / 2
|
|
133
|
+
|
|
134
|
+
text_renderer.render_text(
|
|
135
|
+
self.text, text_x, text_y,
|
|
136
|
+
self.font_size / 48.0, self.text_color
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
super().render()
|
|
140
|
+
|
|
141
|
+
def _draw_rounded_rect(self, x, y, w, h, r, color):
|
|
142
|
+
"""Рисует закруглённый прямоугольник"""
|
|
143
|
+
glEnable(GL_BLEND)
|
|
144
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
145
|
+
|
|
146
|
+
# Простой прямоугольник (закругления можно добавить через шейдер)
|
|
147
|
+
glColor4f(*color)
|
|
148
|
+
glBegin(GL_QUADS)
|
|
149
|
+
glVertex2f(x, y)
|
|
150
|
+
glVertex2f(x + w, y)
|
|
151
|
+
glVertex2f(x + w, y + h)
|
|
152
|
+
glVertex2f(x, y + h)
|
|
153
|
+
glEnd()
|
|
154
|
+
|
|
155
|
+
glDisable(GL_BLEND)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Panel(UIElement):
|
|
159
|
+
"""Панель (контейнер)"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, x: float = 0, y: float = 0,
|
|
162
|
+
width: float = 400, height: float = 300):
|
|
163
|
+
super().__init__(x, y, width, height)
|
|
164
|
+
self.background_color = (0.1, 0.1, 0.15, 0.9)
|
|
165
|
+
self.border_color = (0.3, 0.3, 0.4, 1.0)
|
|
166
|
+
self.border_width = 2.0
|
|
167
|
+
self.padding = 10.0
|
|
168
|
+
self.title = ""
|
|
169
|
+
|
|
170
|
+
def render(self):
|
|
171
|
+
if not self.visible:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Фон
|
|
175
|
+
glEnable(GL_BLEND)
|
|
176
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
177
|
+
|
|
178
|
+
glColor4f(*self.background_color)
|
|
179
|
+
glBegin(GL_QUADS)
|
|
180
|
+
glVertex2f(self.x, self.y)
|
|
181
|
+
glVertex2f(self.x + self.width, self.y)
|
|
182
|
+
glVertex2f(self.x + self.width, self.y + self.height)
|
|
183
|
+
glVertex2f(self.x, self.y + self.height)
|
|
184
|
+
glEnd()
|
|
185
|
+
|
|
186
|
+
# Граница
|
|
187
|
+
glColor4f(*self.border_color)
|
|
188
|
+
glLineWidth(self.border_width)
|
|
189
|
+
glBegin(GL_LINE_LOOP)
|
|
190
|
+
glVertex2f(self.x, self.y)
|
|
191
|
+
glVertex2f(self.x + self.width, self.y)
|
|
192
|
+
glVertex2f(self.x + self.width, self.y + self.height)
|
|
193
|
+
glVertex2f(self.x, self.y + self.height)
|
|
194
|
+
glEnd()
|
|
195
|
+
glLineWidth(1.0)
|
|
196
|
+
|
|
197
|
+
glDisable(GL_BLEND)
|
|
198
|
+
|
|
199
|
+
# Заголовок
|
|
200
|
+
if self.title:
|
|
201
|
+
text_renderer = TextRenderer()
|
|
202
|
+
if text_renderer.default_font:
|
|
203
|
+
text_renderer.render_text(
|
|
204
|
+
self.title,
|
|
205
|
+
self.x + self.padding,
|
|
206
|
+
self.y + self.height - 30,
|
|
207
|
+
0.5, (1.0, 1.0, 1.0, 1.0)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
super().render()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Label(UIElement):
|
|
214
|
+
"""Текстовая метка"""
|
|
215
|
+
|
|
216
|
+
def __init__(self, text: str = "", x: float = 0, y: float = 0,
|
|
217
|
+
font_size: float = 20):
|
|
218
|
+
super().__init__(x, y, 0, 0)
|
|
219
|
+
self.text = text
|
|
220
|
+
self.font_size = font_size
|
|
221
|
+
self.color = (1.0, 1.0, 1.0, 1.0)
|
|
222
|
+
|
|
223
|
+
def render(self):
|
|
224
|
+
if not self.visible or not self.text:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
text_renderer = TextRenderer()
|
|
228
|
+
if text_renderer.default_font:
|
|
229
|
+
w, h = text_renderer.render_text(
|
|
230
|
+
self.text, self.x, self.y,
|
|
231
|
+
self.font_size / 48.0, self.color
|
|
232
|
+
)
|
|
233
|
+
self.width = w
|
|
234
|
+
self.height = h
|
|
235
|
+
|
|
236
|
+
super().render()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Canvas:
|
|
240
|
+
"""Корневой Canvas для UI"""
|
|
241
|
+
|
|
242
|
+
def __init__(self, screen_width: int = 1920, screen_height: int = 1080):
|
|
243
|
+
self.elements = []
|
|
244
|
+
self.screen_width = screen_width
|
|
245
|
+
self.screen_height = screen_height
|
|
246
|
+
|
|
247
|
+
# Настройка ортографической проекции для UI
|
|
248
|
+
self._setup_projection()
|
|
249
|
+
|
|
250
|
+
def _setup_projection(self):
|
|
251
|
+
"""Настройка проекции для UI"""
|
|
252
|
+
glMatrixMode(GL_PROJECTION)
|
|
253
|
+
glLoadIdentity()
|
|
254
|
+
glOrtho(0, self.screen_width, 0, self.screen_height, -1, 1)
|
|
255
|
+
glMatrixMode(GL_MODELVIEW)
|
|
256
|
+
glLoadIdentity()
|
|
257
|
+
|
|
258
|
+
def add(self, element: UIElement):
|
|
259
|
+
"""Добавляет элемент"""
|
|
260
|
+
self.elements.append(element)
|
|
261
|
+
|
|
262
|
+
def remove(self, element: UIElement):
|
|
263
|
+
"""Удаляет элемент"""
|
|
264
|
+
if element in self.elements:
|
|
265
|
+
self.elements.remove(element)
|
|
266
|
+
|
|
267
|
+
def update(self, dt: float, mouse_x: float, mouse_y: float,
|
|
268
|
+
mouse_pressed: bool, mouse_just_pressed: bool):
|
|
269
|
+
"""Обновляет все элементы"""
|
|
270
|
+
for element in self.elements:
|
|
271
|
+
element.update(dt, mouse_x, mouse_y, mouse_pressed, mouse_just_pressed)
|
|
272
|
+
|
|
273
|
+
def render(self):
|
|
274
|
+
"""Рендерит все элементы"""
|
|
275
|
+
# Сохраняем состояние
|
|
276
|
+
glPushAttrib(GL_ALL_ATTRIB_BITS)
|
|
277
|
+
glMatrixMode(GL_PROJECTION)
|
|
278
|
+
glPushMatrix()
|
|
279
|
+
glMatrixMode(GL_MODELVIEW)
|
|
280
|
+
glPushMatrix()
|
|
281
|
+
|
|
282
|
+
# Устанавливаем ортографическую проекцию
|
|
283
|
+
self._setup_projection()
|
|
284
|
+
|
|
285
|
+
# Отключаем тест глубины для UI
|
|
286
|
+
glDisable(GL_DEPTH_TEST)
|
|
287
|
+
|
|
288
|
+
for element in self.elements:
|
|
289
|
+
element.render()
|
|
290
|
+
|
|
291
|
+
# Восстанавливаем состояние
|
|
292
|
+
glEnable(GL_DEPTH_TEST)
|
|
293
|
+
glMatrixMode(GL_PROJECTION)
|
|
294
|
+
glPopMatrix()
|
|
295
|
+
glMatrixMode(GL_MODELVIEW)
|
|
296
|
+
glPopMatrix()
|
|
297
|
+
glPopAttrib()
|
aether/ui/canvas.py
ADDED
aether/ui/text.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Рендеринг текста с использованием FreeType"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from OpenGL.GL import *
|
|
4
|
+
import os
|
|
5
|
+
from typing import Dict, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Character:
|
|
9
|
+
"""Глиф символа"""
|
|
10
|
+
__slots__ = ('texture_id', 'size', 'bearing', 'advance')
|
|
11
|
+
|
|
12
|
+
def __init__(self, texture_id: int, size: Tuple[int, int],
|
|
13
|
+
bearing: Tuple[int, int], advance: int):
|
|
14
|
+
self.texture_id = texture_id
|
|
15
|
+
self.size = size
|
|
16
|
+
self.bearing = bearing
|
|
17
|
+
self.advance = advance
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Font:
|
|
21
|
+
"""Шрифт"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, path: str, size: int = 48):
|
|
24
|
+
self.characters: Dict[str, Character] = {}
|
|
25
|
+
self.size = size
|
|
26
|
+
self.line_height = 0
|
|
27
|
+
|
|
28
|
+
self._load_font(path, size)
|
|
29
|
+
|
|
30
|
+
def _load_font(self, path: str, size: int):
|
|
31
|
+
"""Загружает шрифт"""
|
|
32
|
+
try:
|
|
33
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
34
|
+
|
|
35
|
+
font = ImageFont.truetype(path, size)
|
|
36
|
+
|
|
37
|
+
# Загружаем ASCII символы
|
|
38
|
+
for c in range(32, 127):
|
|
39
|
+
char = chr(c)
|
|
40
|
+
self._load_character(char, font)
|
|
41
|
+
|
|
42
|
+
# Добавляем кириллицу
|
|
43
|
+
for c in range(1040, 1104):
|
|
44
|
+
char = chr(c)
|
|
45
|
+
self._load_character(char, font)
|
|
46
|
+
|
|
47
|
+
# Дополнительные символы
|
|
48
|
+
for char in "—«»…№":
|
|
49
|
+
self._load_character(char, font)
|
|
50
|
+
|
|
51
|
+
# Высота строки
|
|
52
|
+
self.line_height = size + 10
|
|
53
|
+
|
|
54
|
+
print(f"[Font] Loaded: {path} ({size}px, {len(self.characters)} chars)")
|
|
55
|
+
|
|
56
|
+
except ImportError:
|
|
57
|
+
print("[Font] PIL not installed, using default font")
|
|
58
|
+
self._create_default_font()
|
|
59
|
+
|
|
60
|
+
def _load_character(self, char: str, font):
|
|
61
|
+
"""Загружает один символ"""
|
|
62
|
+
from PIL import Image
|
|
63
|
+
|
|
64
|
+
# Получаем размеры символа
|
|
65
|
+
bbox = font.getbbox(char)
|
|
66
|
+
width = bbox[2] - bbox[0]
|
|
67
|
+
height = bbox[3] - bbox[1]
|
|
68
|
+
|
|
69
|
+
if width <= 0 or height <= 0:
|
|
70
|
+
width = self.size // 2
|
|
71
|
+
height = self.size
|
|
72
|
+
|
|
73
|
+
# Рендерим символ
|
|
74
|
+
image = Image.new('RGBA', (width + 4, height + 4), (0, 0, 0, 0))
|
|
75
|
+
draw = ImageDraw.Draw(image)
|
|
76
|
+
draw.text((2 - bbox[0], 2 - bbox[1]), char, font=font, fill=(255, 255, 255, 255))
|
|
77
|
+
|
|
78
|
+
# Создаём OpenGL текстуру
|
|
79
|
+
texture_id = glGenTextures(1)
|
|
80
|
+
glBindTexture(GL_TEXTURE_2D, texture_id)
|
|
81
|
+
|
|
82
|
+
image_data = np.array(image, dtype=np.uint8)
|
|
83
|
+
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height,
|
|
84
|
+
0, GL_RGBA, GL_UNSIGNED_BYTE, image_data)
|
|
85
|
+
|
|
86
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
|
87
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
|
88
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
|
89
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
|
90
|
+
|
|
91
|
+
# Сохраняем информацию
|
|
92
|
+
self.characters[char] = Character(
|
|
93
|
+
texture_id,
|
|
94
|
+
(width + 4, height + 4),
|
|
95
|
+
(bbox[0], -bbox[3]),
|
|
96
|
+
width + 4
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _create_default_font(self):
|
|
100
|
+
"""Создаёт простой встроенный шрифт"""
|
|
101
|
+
for c in range(32, 127):
|
|
102
|
+
char = chr(c)
|
|
103
|
+
texture_id = glGenTextures(1)
|
|
104
|
+
|
|
105
|
+
# Создаём простой символ 16x16
|
|
106
|
+
image = np.zeros((16, 16, 4), dtype=np.uint8)
|
|
107
|
+
image[:, :, 3] = 255
|
|
108
|
+
|
|
109
|
+
glBindTexture(GL_TEXTURE_2D, texture_id)
|
|
110
|
+
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16,
|
|
111
|
+
0, GL_RGBA, GL_UNSIGNED_BYTE, image)
|
|
112
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
|
|
113
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
|
|
114
|
+
|
|
115
|
+
self.characters[char] = Character(texture_id, (16, 16), (0, -16), 8)
|
|
116
|
+
|
|
117
|
+
self.line_height = 16
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TextRenderer:
|
|
121
|
+
"""Рендерер текста"""
|
|
122
|
+
|
|
123
|
+
_instance = None
|
|
124
|
+
|
|
125
|
+
def __new__(cls):
|
|
126
|
+
if cls._instance is None:
|
|
127
|
+
cls._instance = super().__new__(cls)
|
|
128
|
+
cls._instance._initialize()
|
|
129
|
+
return cls._instance
|
|
130
|
+
|
|
131
|
+
def _initialize(self):
|
|
132
|
+
"""Инициализация рендерера"""
|
|
133
|
+
# VAO и VBO для текста
|
|
134
|
+
self.vao = glGenVertexArrays(1)
|
|
135
|
+
self.vbo = glGenBuffers(1)
|
|
136
|
+
|
|
137
|
+
glBindVertexArray(self.vao)
|
|
138
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
139
|
+
|
|
140
|
+
# Выделяем память (6 вершин * 4 floats на глиф * максимум 256 символов)
|
|
141
|
+
glBufferData(GL_ARRAY_BUFFER, 256 * 6 * 4 * 4, None, GL_DYNAMIC_DRAW)
|
|
142
|
+
|
|
143
|
+
# Позиция (vec2) и UV (vec2)
|
|
144
|
+
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * 4, ctypes.c_void_p(0))
|
|
145
|
+
glEnableVertexAttribArray(0)
|
|
146
|
+
|
|
147
|
+
glBindVertexArray(0)
|
|
148
|
+
|
|
149
|
+
# Шейдер текста
|
|
150
|
+
self._setup_shader()
|
|
151
|
+
|
|
152
|
+
# Текущий шрифт
|
|
153
|
+
self.default_font = None
|
|
154
|
+
|
|
155
|
+
print("[TextRenderer] Initialized")
|
|
156
|
+
|
|
157
|
+
def _setup_shader(self):
|
|
158
|
+
"""Настройка шейдера для текста"""
|
|
159
|
+
vertex_shader = """
|
|
160
|
+
#version 460 core
|
|
161
|
+
layout(location = 0) in vec4 vertex; // xy: pos, zw: uv
|
|
162
|
+
|
|
163
|
+
out vec2 TexCoords;
|
|
164
|
+
|
|
165
|
+
uniform mat4 projection;
|
|
166
|
+
|
|
167
|
+
void main() {
|
|
168
|
+
gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
|
|
169
|
+
TexCoords = vertex.zw;
|
|
170
|
+
}
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
fragment_shader = """
|
|
174
|
+
#version 460 core
|
|
175
|
+
in vec2 TexCoords;
|
|
176
|
+
out vec4 color;
|
|
177
|
+
|
|
178
|
+
uniform sampler2D text;
|
|
179
|
+
uniform vec4 textColor;
|
|
180
|
+
|
|
181
|
+
void main() {
|
|
182
|
+
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
|
|
183
|
+
color = textColor * sampled;
|
|
184
|
+
}
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
from aether.graphics.shader import Shader
|
|
188
|
+
self.shader = Shader(vertex_shader, fragment_shader)
|
|
189
|
+
|
|
190
|
+
def set_default_font(self, font_path: str, size: int = 48):
|
|
191
|
+
"""Устанавливает шрифт по умолчанию"""
|
|
192
|
+
self.default_font = Font(font_path, size)
|
|
193
|
+
|
|
194
|
+
def render_text(self, text: str, x: float, y: float,
|
|
195
|
+
scale: float = 1.0, color: tuple = (1.0, 1.0, 1.0, 1.0),
|
|
196
|
+
font: Font = None, screen_width: int = 1920,
|
|
197
|
+
screen_height: int = 1080) -> Tuple[float, float]:
|
|
198
|
+
"""Рендерит текст и возвращает (width, height)"""
|
|
199
|
+
if not text:
|
|
200
|
+
return (0, 0)
|
|
201
|
+
|
|
202
|
+
if font is None:
|
|
203
|
+
font = self.default_font
|
|
204
|
+
|
|
205
|
+
if font is None:
|
|
206
|
+
return (0, 0)
|
|
207
|
+
|
|
208
|
+
# Ортографическая проекция
|
|
209
|
+
projection = np.array([
|
|
210
|
+
[2.0 / screen_width, 0, 0, -1],
|
|
211
|
+
[0, 2.0 / screen_height, 0, -1],
|
|
212
|
+
[0, 0, -1, 0],
|
|
213
|
+
[0, 0, 0, 1]
|
|
214
|
+
], dtype=np.float32)
|
|
215
|
+
|
|
216
|
+
self.shader.use()
|
|
217
|
+
self.shader.set_mat4("projection", projection)
|
|
218
|
+
self.shader.set_vec4("textColor", color)
|
|
219
|
+
|
|
220
|
+
glActiveTexture(GL_TEXTURE0)
|
|
221
|
+
glBindVertexArray(self.vao)
|
|
222
|
+
|
|
223
|
+
total_width = 0
|
|
224
|
+
total_height = 0
|
|
225
|
+
|
|
226
|
+
for char in text:
|
|
227
|
+
if char not in font.characters:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
ch = font.characters[char]
|
|
231
|
+
|
|
232
|
+
xpos = x + ch.bearing[0] * scale
|
|
233
|
+
ypos = y - (ch.size[1] - ch.bearing[1]) * scale
|
|
234
|
+
|
|
235
|
+
w = ch.size[0] * scale
|
|
236
|
+
h = ch.size[1] * scale
|
|
237
|
+
|
|
238
|
+
# 6 вершин на символ (2 треугольника)
|
|
239
|
+
vertices = np.array([
|
|
240
|
+
xpos, ypos + h, 0.0, 0.0,
|
|
241
|
+
xpos, ypos, 0.0, 1.0,
|
|
242
|
+
xpos + w, ypos, 1.0, 1.0,
|
|
243
|
+
|
|
244
|
+
xpos, ypos + h, 0.0, 0.0,
|
|
245
|
+
xpos + w, ypos, 1.0, 1.0,
|
|
246
|
+
xpos + w, ypos + h, 1.0, 0.0,
|
|
247
|
+
], dtype=np.float32)
|
|
248
|
+
|
|
249
|
+
# Рендерим глиф
|
|
250
|
+
glBindTexture(GL_TEXTURE_2D, ch.texture_id)
|
|
251
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
252
|
+
glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)
|
|
253
|
+
glDrawArrays(GL_TRIANGLES, 0, 6)
|
|
254
|
+
|
|
255
|
+
# Сдвигаем позицию для следующего символа
|
|
256
|
+
x += (ch.advance >> 6) * scale
|
|
257
|
+
|
|
258
|
+
total_width += (ch.advance >> 6) * scale
|
|
259
|
+
total_height = max(total_height, ch.size[1] * scale)
|
|
260
|
+
|
|
261
|
+
glBindVertexArray(0)
|
|
262
|
+
glBindTexture(GL_TEXTURE_2D, 0)
|
|
263
|
+
|
|
264
|
+
return (total_width, total_height)
|
|
265
|
+
|
|
266
|
+
def measure_text(self, text: str, scale: float = 1.0, font: Font = None) -> Tuple[float, float]:
|
|
267
|
+
"""Измеряет размер текста без рендеринга"""
|
|
268
|
+
if not text or font is None:
|
|
269
|
+
return (0, 0)
|
|
270
|
+
|
|
271
|
+
if font is None:
|
|
272
|
+
font = self.default_font
|
|
273
|
+
|
|
274
|
+
if font is None:
|
|
275
|
+
return (0, 0)
|
|
276
|
+
|
|
277
|
+
width = 0
|
|
278
|
+
height = 0
|
|
279
|
+
|
|
280
|
+
for char in text:
|
|
281
|
+
if char in font.characters:
|
|
282
|
+
ch = font.characters[char]
|
|
283
|
+
width += (ch.advance >> 6) * scale
|
|
284
|
+
height = max(height, ch.size[1] * scale)
|
|
285
|
+
|
|
286
|
+
return (width, height)
|
aether/utils/__init__.py
ADDED
|
File without changes
|
aether/utils/logger.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Логирование"""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_logger(name="Aether", level=logging.INFO):
|
|
6
|
+
logger = logging.getLogger(name)
|
|
7
|
+
logger.setLevel(level)
|
|
8
|
+
|
|
9
|
+
handler = logging.StreamHandler()
|
|
10
|
+
handler.setFormatter(logging.Formatter(
|
|
11
|
+
'[%(asctime)s] [%(levelname)s] %(message)s',
|
|
12
|
+
datefmt='%H:%M:%S'
|
|
13
|
+
))
|
|
14
|
+
logger.addHandler(handler)
|
|
15
|
+
|
|
16
|
+
return logger
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Математические утилиты"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def lerp(a: float, b: float, t: float) -> float:
|
|
6
|
+
return a + (b - a) * t
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def clamp(value: float, min_val: float, max_val: float) -> float:
|
|
10
|
+
return max(min_val, min(max_val, value))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize(v: np.ndarray) -> np.ndarray:
|
|
14
|
+
norm = np.linalg.norm(v)
|
|
15
|
+
return v / norm if norm > 0 else v
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def look_at(eye: np.ndarray, target: np.ndarray, up: np.ndarray) -> np.ndarray:
|
|
19
|
+
"""Создаёт look-at матрицу"""
|
|
20
|
+
f = target - eye
|
|
21
|
+
f = f / np.linalg.norm(f)
|
|
22
|
+
s = np.cross(f, up)
|
|
23
|
+
s = s / np.linalg.norm(s)
|
|
24
|
+
u = np.cross(s, f)
|
|
25
|
+
|
|
26
|
+
m = np.identity(4, dtype=np.float32)
|
|
27
|
+
m[0, :3] = s
|
|
28
|
+
m[1, :3] = u
|
|
29
|
+
m[2, :3] = -f
|
|
30
|
+
m[3, :3] = eye
|
|
31
|
+
return np.linalg.inv(m)
|