eagle-engine 0.1.0__tar.gz

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 (49) hide show
  1. eagle_engine-0.1.0/LICENSE +21 -0
  2. eagle_engine-0.1.0/MANIFEST.in +14 -0
  3. eagle_engine-0.1.0/PKG-INFO +88 -0
  4. eagle_engine-0.1.0/README.md +54 -0
  5. eagle_engine-0.1.0/eagle/__init__.py +6 -0
  6. eagle_engine-0.1.0/eagle/assets/logo.png +0 -0
  7. eagle_engine-0.1.0/eagle/assets/shaders/basic.frag +32 -0
  8. eagle_engine-0.1.0/eagle/assets/shaders/basic.vert +22 -0
  9. eagle_engine-0.1.0/eagle/assets/shaders/unlit.frag +7 -0
  10. eagle_engine-0.1.0/eagle/assets/shaders/unlit.vert +8 -0
  11. eagle_engine-0.1.0/eagle/core/__init__.py +3 -0
  12. eagle_engine-0.1.0/eagle/core/engine.py +255 -0
  13. eagle_engine-0.1.0/eagle/core/splash.py +88 -0
  14. eagle_engine-0.1.0/eagle/gfx/__init__.py +10 -0
  15. eagle_engine-0.1.0/eagle/gfx/camera.py +41 -0
  16. eagle_engine-0.1.0/eagle/gfx/material.py +11 -0
  17. eagle_engine-0.1.0/eagle/gfx/mesh.py +83 -0
  18. eagle_engine-0.1.0/eagle/gfx/model.py +46 -0
  19. eagle_engine-0.1.0/eagle/gfx/orbit_camera.py +105 -0
  20. eagle_engine-0.1.0/eagle/gfx/shader.py +76 -0
  21. eagle_engine-0.1.0/eagle/gfx/texture.py +49 -0
  22. eagle_engine-0.1.0/eagle/loaders/__init__.py +5 -0
  23. eagle_engine-0.1.0/eagle/loaders/mtl.py +51 -0
  24. eagle_engine-0.1.0/eagle/loaders/obj.py +151 -0
  25. eagle_engine-0.1.0/eagle/loaders/stl.py +55 -0
  26. eagle_engine-0.1.0/eagle/math/__init__.py +5 -0
  27. eagle_engine-0.1.0/eagle/math/mat.py +122 -0
  28. eagle_engine-0.1.0/eagle/math/transform.py +37 -0
  29. eagle_engine-0.1.0/eagle/math/vec.py +132 -0
  30. eagle_engine-0.1.0/eagle/primitives/__init__.py +12 -0
  31. eagle_engine-0.1.0/eagle/primitives/axes.py +17 -0
  32. eagle_engine-0.1.0/eagle/primitives/cone.py +55 -0
  33. eagle_engine-0.1.0/eagle/primitives/cube.py +29 -0
  34. eagle_engine-0.1.0/eagle/primitives/cylinder.py +46 -0
  35. eagle_engine-0.1.0/eagle/primitives/grid.py +22 -0
  36. eagle_engine-0.1.0/eagle/primitives/icosphere.py +50 -0
  37. eagle_engine-0.1.0/eagle/primitives/plane.py +19 -0
  38. eagle_engine-0.1.0/eagle/primitives/sphere.py +29 -0
  39. eagle_engine-0.1.0/eagle/primitives/torus.py +32 -0
  40. eagle_engine-0.1.0/eagle/scene/__init__.py +4 -0
  41. eagle_engine-0.1.0/eagle/scene/object.py +43 -0
  42. eagle_engine-0.1.0/eagle/scene/scene.py +40 -0
  43. eagle_engine-0.1.0/eagle_engine.egg-info/PKG-INFO +88 -0
  44. eagle_engine-0.1.0/eagle_engine.egg-info/SOURCES.txt +47 -0
  45. eagle_engine-0.1.0/eagle_engine.egg-info/dependency_links.txt +1 -0
  46. eagle_engine-0.1.0/eagle_engine.egg-info/requires.txt +9 -0
  47. eagle_engine-0.1.0/eagle_engine.egg-info/top_level.txt +1 -0
  48. eagle_engine-0.1.0/pyproject.toml +50 -0
  49. eagle_engine-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilya Titov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+
5
+ # Только нужное из eagle/assets
6
+ recursive-include eagle/assets *.vert *.frag *.png
7
+
8
+ # Явно исключить мусор
9
+ global-exclude __pycache__
10
+ global-exclude *.py[co]
11
+ global-exclude .pytest_cache
12
+ global-exclude .ruff_cache
13
+ global-exclude *.egg-info
14
+ global-exclude .DS_Store
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: eagle-engine
3
+ Version: 0.1.0
4
+ Summary: 3D-движок как расширение PyGame: GPU-рендеринг через ModernGL
5
+ Author: Eagle Engine contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/your-username/eagle-engine
8
+ Project-URL: Repository, https://github.com/your-username/eagle-engine
9
+ Project-URL: Issues, https://github.com/your-username/eagle-engine/issues
10
+ Keywords: pygame,3d,engine,opengl,moderngl,gamedev
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Games/Entertainment
21
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pygame-ce>=2.4
26
+ Requires-Dist: moderngl>=5.8
27
+ Requires-Dist: numpy>=1.24
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7; extra == "dev"
30
+ Requires-Dist: ruff>=0.4; extra == "dev"
31
+ Requires-Dist: build>=1.0; extra == "dev"
32
+ Requires-Dist: twine>=5.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Eagle Engine
36
+
37
+ 3D-extension for PyGame with GPU-rendering by ModernGL (OpenGL 3.3 Core).
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install eagle-engine
43
+ ```
44
+ Quickstart
45
+
46
+ ```python
47
+ import pygame
48
+ from eagle.core import Engine
49
+ from eagle.primitives import Cube
50
+ from eagle.gfx import Camera
51
+
52
+ engine = Engine(800, 600, "Eagle")
53
+ camera = Camera(position=(0, 0, 3))
54
+ cube = Cube()
55
+
56
+ @engine.on_render_3d
57
+ def render():
58
+ engine.draw(cube, camera)
59
+
60
+ @engine.on_render_2d
61
+ def hud():
62
+ font = pygame.font.SysFont("Arial", 20)
63
+ engine.surface.blit(font.render("Hello Eagle", True, "white"), (16, 16))
64
+
65
+ engine.run()
66
+ ```
67
+
68
+ ## Contents
69
+
70
+ * SDL2 window + OpenGL 3.3 Core + MSAA ×4
71
+
72
+ * Math (Vec3/Mat4/Transform) on numpy
73
+
74
+ * Blinn-Phong-lighting
75
+
76
+ * 9 primitives: Cube, Sphere, Icosphere, Plane, Grid, Cylinder, Cone, Torus, Axes
77
+
78
+ * Loaders of .obj (+.mtl, textures) and .stl (ASCII + binary)
79
+
80
+ * SceneObject / Scene with transfomation hierarhy
81
+
82
+ * 2D-overlay by pygame.Surface → GL-texture
83
+
84
+ * Camera with orbital controls (mouse + mouse wheel)
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,54 @@
1
+ # Eagle Engine
2
+
3
+ 3D-extension for PyGame with GPU-rendering by ModernGL (OpenGL 3.3 Core).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install eagle-engine
9
+ ```
10
+ Quickstart
11
+
12
+ ```python
13
+ import pygame
14
+ from eagle.core import Engine
15
+ from eagle.primitives import Cube
16
+ from eagle.gfx import Camera
17
+
18
+ engine = Engine(800, 600, "Eagle")
19
+ camera = Camera(position=(0, 0, 3))
20
+ cube = Cube()
21
+
22
+ @engine.on_render_3d
23
+ def render():
24
+ engine.draw(cube, camera)
25
+
26
+ @engine.on_render_2d
27
+ def hud():
28
+ font = pygame.font.SysFont("Arial", 20)
29
+ engine.surface.blit(font.render("Hello Eagle", True, "white"), (16, 16))
30
+
31
+ engine.run()
32
+ ```
33
+
34
+ ## Contents
35
+
36
+ * SDL2 window + OpenGL 3.3 Core + MSAA ×4
37
+
38
+ * Math (Vec3/Mat4/Transform) on numpy
39
+
40
+ * Blinn-Phong-lighting
41
+
42
+ * 9 primitives: Cube, Sphere, Icosphere, Plane, Grid, Cylinder, Cone, Torus, Axes
43
+
44
+ * Loaders of .obj (+.mtl, textures) and .stl (ASCII + binary)
45
+
46
+ * SceneObject / Scene with transfomation hierarhy
47
+
48
+ * 2D-overlay by pygame.Surface → GL-texture
49
+
50
+ * Camera with orbital controls (mouse + mouse wheel)
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,6 @@
1
+ """Eagle Engine — 3D-расширение для PyGame с GPU-рендерингом."""
2
+ __version__ = "0.1.0"
3
+
4
+ from eagle.core import Engine # noqa: F401
5
+
6
+ __all__ = ["Engine", "__version__"]
Binary file
@@ -0,0 +1,32 @@
1
+ #version 330 core
2
+
3
+ in vec3 v_world_pos;
4
+ in vec3 v_normal;
5
+ in vec2 v_uv;
6
+
7
+ uniform vec3 u_camera_pos;
8
+ uniform vec3 u_light_dir;
9
+ uniform vec3 u_light_color;
10
+ uniform vec3 u_ambient;
11
+ uniform vec3 u_color;
12
+ uniform float u_shininess;
13
+ uniform float u_specular;
14
+ uniform int u_use_texture;
15
+ uniform sampler2D u_texture;
16
+
17
+ out vec4 frag_color;
18
+
19
+ void main() {
20
+ vec3 albedo = u_color;
21
+ if (u_use_texture == 1) {
22
+ albedo *= texture(u_texture, v_uv).rgb;
23
+ }
24
+ vec3 N = normalize(v_normal);
25
+ vec3 L = normalize(-u_light_dir);
26
+ vec3 V = normalize(u_camera_pos - v_world_pos);
27
+ vec3 H = normalize(L + V);
28
+ float diff = max(dot(N, L), 0.0);
29
+ float spec = pow(max(dot(N, H), 0.0), u_shininess) * u_specular;
30
+ vec3 color = albedo * (u_ambient + u_light_color * diff) + u_light_color * spec;
31
+ frag_color = vec4(color, 1.0);
32
+ }
@@ -0,0 +1,22 @@
1
+ #version 330 core
2
+
3
+ in vec3 in_position;
4
+ in vec3 in_normal;
5
+ in vec2 in_uv;
6
+
7
+ uniform mat4 u_model;
8
+ uniform mat4 u_view;
9
+ uniform mat4 u_proj;
10
+ uniform mat3 u_normal_mat;
11
+
12
+ out vec3 v_world_pos;
13
+ out vec3 v_normal;
14
+ out vec2 v_uv;
15
+
16
+ void main() {
17
+ vec4 world = u_model * vec4(in_position, 1.0);
18
+ v_world_pos = world.xyz;
19
+ v_normal = normalize(u_normal_mat * in_normal);
20
+ v_uv = in_uv;
21
+ gl_Position = u_proj * u_view * world;
22
+ }
@@ -0,0 +1,7 @@
1
+ #version 330 core
2
+ in vec2 v_uv;
3
+ uniform sampler2D u_texture;
4
+ out vec4 frag_color;
5
+ void main() {
6
+ frag_color = texture(u_texture, v_uv);
7
+ }
@@ -0,0 +1,8 @@
1
+ #version 330 core
2
+ in vec2 in_position;
3
+ in vec2 in_uv;
4
+ out vec2 v_uv;
5
+ void main() {
6
+ v_uv = in_uv;
7
+ gl_Position = vec4(in_position, 0.0, 1.0);
8
+ }
@@ -0,0 +1,3 @@
1
+ from eagle.core.engine import Engine
2
+
3
+ __all__ = ["Engine"]
@@ -0,0 +1,255 @@
1
+ """Engine — окно + GL-контекст + цикл + 2D-оверлей + splash."""
2
+ from __future__ import annotations
3
+ from typing import Callable
4
+
5
+ import numpy as np
6
+ import pygame
7
+ import moderngl
8
+
9
+ from eagle.gfx.shader import shader_from_package, Shader
10
+ from eagle.gfx.texture import Texture
11
+ from eagle.gfx.camera import Camera
12
+ from eagle.gfx.mesh import Mesh
13
+ from eagle.math.mat import Mat4
14
+ from eagle.core.splash import load_logo, make_icon, Splash
15
+
16
+
17
+ class CallbackList:
18
+ """Список подписчиков. Как декоратор — регистрирует, как dispatch — исполняет."""
19
+ __slots__ = ("_subs",)
20
+
21
+ def __init__(self):
22
+ self._subs: list[Callable] = []
23
+
24
+ def __call__(self, fn: Callable) -> Callable:
25
+ self._subs.append(fn)
26
+ return fn
27
+
28
+ def dispatch(self, *args) -> None:
29
+ for fn in self._subs:
30
+ fn(*args)
31
+
32
+ def __len__(self):
33
+ return len(self._subs)
34
+
35
+
36
+ class Engine:
37
+ def __init__(
38
+ self,
39
+ width: int = 800,
40
+ height: int = 600,
41
+ title: str = "Eagle Engine",
42
+ *,
43
+ vsync: bool = True,
44
+ target_fps: int | None = 60,
45
+ logo_path: str | None = None,
46
+ logo: pygame.Surface | None = None,
47
+ splash: bool = True,
48
+ ):
49
+ pygame.init()
50
+ pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MAJOR_VERSION, 3)
51
+ pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MINOR_VERSION, 3)
52
+ pygame.display.gl_set_attribute(pygame.GL_CONTEXT_PROFILE_MASK, pygame.GL_CONTEXT_PROFILE_CORE)
53
+ pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 24)
54
+ pygame.display.gl_set_attribute(pygame.GL_DOUBLEBUFFER, 1)
55
+ pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 4)
56
+
57
+ self.width = width
58
+ self.height = height
59
+ self.screen = pygame.display.set_mode(
60
+ (width, height),
61
+ pygame.OPENGL | pygame.DOUBLEBUF | pygame.RESIZABLE,
62
+ vsync=1 if vsync else 0,
63
+ )
64
+ pygame.display.set_caption(title)
65
+
66
+ # --- логотип и иконка ---
67
+ if logo is not None:
68
+ _logo_surf = logo.convert_alpha()
69
+ elif logo_path is not None:
70
+ _logo_surf = load_logo(logo_path)
71
+ else:
72
+ _logo_surf = load_logo() # встроенный eagle/assets/logo.png
73
+
74
+ pygame.display.set_icon(make_icon(_logo_surf, size=64))
75
+
76
+ self._splash = Splash(_logo_surf, width, height) if splash else None
77
+
78
+ # --- OpenGL / ModernGL ---
79
+ self.ctx = moderngl.create_context(require=330)
80
+ self.ctx.enable(moderngl.DEPTH_TEST | moderngl.CULL_FACE | moderngl.BLEND)
81
+ self.ctx.blend_func = moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA
82
+ self.ctx.gc_mode = "auto"
83
+
84
+ self.surface = pygame.Surface((width, height), pygame.SRCALPHA)
85
+
86
+ self._overlay_tex = Texture(self.surface, flip=False)
87
+ self._overlay_tex.build(self.ctx)
88
+ self._overlay_shader = shader_from_package("unlit.vert", "unlit.frag")
89
+ self._overlay_shader.build(self.ctx)
90
+ # CCW-порядок для TRIANGLE_STRIP, v=0 сверху, v=1 снизу.
91
+ quad = np.array([
92
+ -1, -1, 0, 1, # лево-низ
93
+ 1, -1, 1, 1, # право-низ
94
+ -1, 1, 0, 0, # лево-верх
95
+ 1, 1, 1, 0, # право-верх
96
+ ], dtype="f4")
97
+ self._overlay_vao = self.ctx.vertex_array(
98
+ self._overlay_shader.program,
99
+ [(self.ctx.buffer(quad.tobytes()), "2f 2f", "in_position", "in_uv")],
100
+ )
101
+
102
+ self._basic_shader: Shader = shader_from_package("basic.vert", "basic.frag")
103
+ self._basic_shader.build(self.ctx)
104
+
105
+ self.running = True
106
+ self.clock = pygame.time.Clock()
107
+ self.target_fps = target_fps
108
+ self._elapsed = 0.0
109
+ self._last_dt = 0.0
110
+
111
+ # --- колбэки (декораторы) ---
112
+ self.on_update = CallbackList()
113
+ self.on_render_3d = CallbackList()
114
+ self.on_render_2d = CallbackList()
115
+ self.on_event = CallbackList()
116
+
117
+ # --- освещение ---
118
+ self.light_dir = (0.3, -0.7, -0.6)
119
+ self.light_color = (1.0, 1.0, 1.0)
120
+ self.ambient = (0.25, 0.25, 0.28)
121
+
122
+ # ============================================================
123
+ # ЦИКЛ
124
+ # ============================================================
125
+ def tick(self) -> float:
126
+ dt = self.clock.tick(self.target_fps if self.target_fps else 0) / 1000.0
127
+ self._last_dt = dt
128
+ self._elapsed += dt
129
+
130
+ skip_splash = False
131
+ for ev in pygame.event.get():
132
+ if ev.type == pygame.QUIT:
133
+ self.running = False
134
+ elif ev.type == pygame.VIDEORESIZE:
135
+ self.width, self.height = ev.w, ev.h
136
+ self.surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
137
+ self.ctx.viewport = (0, 0, self.width, self.height)
138
+ self._overlay_tex.release()
139
+ self._overlay_tex = Texture(self.surface, flip=False)
140
+ self._overlay_tex.build(self.ctx)
141
+ elif ev.type in (pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN):
142
+ skip_splash = True
143
+ self.on_event.dispatch(ev)
144
+
145
+ if self._splash is not None and not self._splash.done:
146
+ self._splash.update(dt)
147
+ if skip_splash:
148
+ self._splash._t = self._splash.duration + self._splash.fade
149
+ self._splash._done = True
150
+ return dt
151
+
152
+ self.on_update.dispatch(dt)
153
+ return dt
154
+
155
+ def clear(self, color=(0.10, 0.10, 0.14, 1.0)):
156
+ self.ctx.clear(*color, depth=1.0)
157
+ self.ctx.viewport = (0, 0, self.width, self.height)
158
+
159
+ # ============================================================
160
+ # РИСОВАНИЕ
161
+ # ============================================================
162
+ def draw(self, obj, camera: Camera, *, model: Mat4 | None = None,
163
+ color=(0.85, 0.85, 0.9), shininess=32.0, specular=0.4, texture=None):
164
+ """obj — Mesh (голая геометрия) или Model (несколько частей с материалами)."""
165
+ from eagle.gfx.model import Model
166
+
167
+ if isinstance(obj, Model):
168
+ for part in obj.parts:
169
+ self._draw_mesh(part.mesh, camera, model=model,
170
+ color=part.color, shininess=part.shininess,
171
+ specular=part.specular, texture=part.texture)
172
+ else:
173
+ self._draw_mesh(obj, camera, model=model, color=color,
174
+ shininess=shininess, specular=specular, texture=texture)
175
+
176
+ def _draw_mesh(self, mesh, camera, *, model, color, shininess, specular, texture):
177
+ if model is None:
178
+ model = Mat4.identity()
179
+ camera.set_aspect(self.width / max(1, self.height))
180
+
181
+ sh = self._basic_shader
182
+ sh["u_model"] = model
183
+ sh["u_view"] = camera.view_matrix()
184
+ sh["u_proj"] = camera.projection_matrix()
185
+ nm = np.linalg.inv(model.array[:3, :3]).T.astype(np.float32)
186
+ sh.program["u_normal_mat"].write(nm.tobytes(order="C"))
187
+ sh["u_camera_pos"] = camera.position.xyz
188
+ sh["u_light_dir"] = self.light_dir
189
+ sh["u_light_color"] = self.light_color
190
+ sh["u_ambient"] = self.ambient
191
+ sh["u_color"] = color
192
+ sh["u_shininess"] = shininess
193
+ sh["u_specular"] = specular
194
+
195
+ if texture is not None:
196
+ if texture._tex is None:
197
+ texture.build(self.ctx)
198
+ texture.use(0)
199
+ sh["u_texture"] = 0
200
+ sh["u_use_texture"] = 1
201
+ else:
202
+ sh["u_use_texture"] = 0
203
+
204
+ sh.use()
205
+ if mesh._vao is None:
206
+ mesh.build(self.ctx, sh.program)
207
+ mesh.render()
208
+
209
+ def present(self):
210
+ # 2D-оверлей: без depth-test, без culling (это плоский квад лицом к камере)
211
+ self.ctx.disable(moderngl.DEPTH_TEST)
212
+ self.ctx.disable(moderngl.CULL_FACE)
213
+ self.ctx.enable(moderngl.BLEND)
214
+ self._overlay_tex.update_from_surface(self.surface)
215
+ self._overlay_shader.use()
216
+ self._overlay_tex.use(0)
217
+ self._overlay_shader["u_texture"] = 0
218
+ self._overlay_vao.render(moderngl.TRIANGLE_STRIP)
219
+ self.ctx.enable(moderngl.DEPTH_TEST)
220
+ self.ctx.enable(moderngl.CULL_FACE)
221
+ pygame.display.flip()
222
+
223
+ # ============================================================
224
+ # ГЛАВНЫЙ ЦИКЛ
225
+ # ============================================================
226
+ def run(self):
227
+ try:
228
+ while self.running:
229
+ self.tick()
230
+ self.clear()
231
+
232
+ if self._splash is not None and not self._splash.done:
233
+ self.surface.fill((0, 0, 0, 0))
234
+ self._splash.draw(self.surface)
235
+ self.present()
236
+ continue
237
+
238
+ self.on_render_3d.dispatch()
239
+ self.surface.fill((0, 0, 0, 0))
240
+ self.on_render_2d.dispatch()
241
+ self.present()
242
+ finally:
243
+ self.shutdown()
244
+
245
+ def shutdown(self):
246
+ self.running = False
247
+ self._overlay_tex.release()
248
+ self._overlay_shader.release()
249
+ self._overlay_vao.release()
250
+ self._basic_shader.release()
251
+ pygame.quit()
252
+
253
+ @property
254
+ def time(self):
255
+ return self._elapsed
@@ -0,0 +1,88 @@
1
+ """Splash-заставка с логотипом Eagle."""
2
+ from __future__ import annotations
3
+ import os
4
+ import pygame
5
+
6
+
7
+ def load_logo(path: str | None = None) -> pygame.Surface:
8
+ """Загружает логотип. Если path=None — встроенный eagle/assets/logo.png."""
9
+ if path is None:
10
+ here = os.path.dirname(os.path.abspath(__file__))
11
+ path = os.path.join(os.path.dirname(here), "assets", "logo.png")
12
+ if not os.path.exists(path):
13
+ # Fallback: пустая поверхность, чтобы не падать
14
+ surf = pygame.Surface((256, 256), pygame.SRCALPHA)
15
+ return surf
16
+ return pygame.image.load(path).convert_alpha()
17
+
18
+
19
+ def make_icon(logo: pygame.Surface, size: int = 64) -> pygame.Surface:
20
+ """Ресайз логотипа под иконку окна. Сохраняет пропорции, паддинг прозрачный."""
21
+ w, h = logo.get_size()
22
+ scale = size / max(w, h)
23
+ nw, nh = max(1, int(w * scale)), max(1, int(h * scale))
24
+ resized = pygame.transform.smoothscale(logo, (nw, nh))
25
+ icon = pygame.Surface((size, size), pygame.SRCALPHA)
26
+ icon.blit(resized, ((size - nw) // 2, (size - nh) // 2))
27
+ return icon
28
+
29
+
30
+ class Splash:
31
+ """Splash-заставка в окне. Показывается duration секунд, потом fade-out за fade секунд."""
32
+
33
+ def __init__(self, logo: pygame.Surface, engine_w: int, engine_h: int,
34
+ duration: float = 2.0, fade: float = 0.6):
35
+ self.duration = duration
36
+ self.fade = fade
37
+ self._t = 0.0
38
+ self._done = False
39
+
40
+ # Размер логотипа на экране: min(0.35 * размер окна)
41
+ target = int(min(engine_w, engine_h) * 0.35)
42
+ w, h = logo.get_size()
43
+ scale = target / max(w, h)
44
+ self._logo = pygame.transform.smoothscale(logo, (int(w * scale), int(h * scale)))
45
+ self._engine_w = engine_w
46
+ self._engine_h = engine_h
47
+
48
+ # Шрифт и текст подписи рендерим один раз — не пересоздаём каждый кадр
49
+ self._font = pygame.font.SysFont("Arial", 18, bold=True)
50
+ self._label = self._font.render("Eagle Engine", True, (240, 240, 240))
51
+
52
+ @property
53
+ def done(self) -> bool:
54
+ return self._done
55
+
56
+ def update(self, dt: float) -> None:
57
+ if self._done:
58
+ return
59
+ self._t += dt
60
+ if self._t >= self.duration + self.fade:
61
+ self._done = True
62
+
63
+ def draw(self, surface: pygame.Surface) -> None:
64
+ if self._done:
65
+ return
66
+
67
+ # Фон — тот же цвет, что у engine.clear
68
+ surface.fill((25, 25, 35, 255))
69
+
70
+ # Прозрачность: 255 → 0 при fade-out
71
+ alpha = 255
72
+ if self._t > self.duration:
73
+ alpha = int(255 * (1.0 - (self._t - self.duration) / self.fade))
74
+ alpha = max(0, min(255, alpha))
75
+
76
+ lw, lh = self._logo.get_size()
77
+ x = (self._engine_w - lw) // 2
78
+ y = (self._engine_h - lh) // 2
79
+
80
+ logo_copy = self._logo.copy()
81
+ logo_copy.set_alpha(alpha)
82
+ surface.blit(logo_copy, (x, y))
83
+
84
+ # Подпись
85
+ label = self._label.copy()
86
+ label.set_alpha(alpha)
87
+ tw = label.get_width()
88
+ surface.blit(label, ((self._engine_w - tw) // 2, y + lh + 20))
@@ -0,0 +1,10 @@
1
+ from eagle.gfx.shader import Shader, shader_from_files, shader_from_package
2
+ from eagle.gfx.mesh import Mesh
3
+ from eagle.gfx.model import Model, ModelPart
4
+ from eagle.gfx.camera import Camera
5
+ from eagle.gfx.orbit_camera import OrbitCamera
6
+ from eagle.gfx.texture import Texture
7
+ from eagle.gfx.material import Material
8
+
9
+ __all__ = ["Shader", "shader_from_files", "shader_from_package",
10
+ "Mesh", "Model", "ModelPart", "Camera", "OrbitCamera", "Texture", "Material"]
@@ -0,0 +1,41 @@
1
+ """Camera — перспектива / орто + look-at."""
2
+ from __future__ import annotations
3
+ from eagle.math.vec import Vec3
4
+ from eagle.math.mat import Mat4
5
+
6
+
7
+ def _as_vec3(v, default: Vec3) -> Vec3:
8
+ if v is None:
9
+ return default
10
+ if isinstance(v, Vec3):
11
+ return v
12
+ if isinstance(v, (tuple, list)):
13
+ return Vec3.from_array(v)
14
+ raise TypeError(f"Camera: cannot convert {type(v)} to Vec3")
15
+
16
+
17
+ class Camera:
18
+ def __init__(self, position=None, target=None, fov_deg=60.0, near=0.1, far=1000.0):
19
+ self.position = _as_vec3(position, Vec3(0.0, 0.0, 3.0))
20
+ self.target = _as_vec3(target, Vec3.zero())
21
+ self.up = Vec3.up()
22
+ self.fov_deg = fov_deg
23
+ self.near = near
24
+ self.far = far
25
+ self._aspect = 1.0
26
+ self._ortho = None
27
+
28
+ def set_aspect(self, aspect): self._aspect = aspect
29
+ def set_ortho(self, l, r, b, t, near=-1.0, far=1.0): self._ortho = (l, r, b, t, near, far)
30
+ def clear_ortho(self): self._ortho = None
31
+
32
+ def view_matrix(self) -> Mat4:
33
+ return Mat4.look_at(self.position, self.target, self.up)
34
+
35
+ def projection_matrix(self) -> Mat4:
36
+ if self._ortho is not None:
37
+ return Mat4.ortho(*self._ortho)
38
+ return Mat4.perspective(self.fov_deg, self._aspect, self.near, self.far)
39
+
40
+ def view_proj(self) -> Mat4:
41
+ return self.projection_matrix() @ self.view_matrix()
@@ -0,0 +1,11 @@
1
+ """Material — базовый Blinn-Phong."""
2
+ from __future__ import annotations
3
+ from eagle.gfx.texture import Texture
4
+
5
+
6
+ class Material:
7
+ def __init__(self, color=(0.8, 0.8, 0.85), shininess=32.0, specular=0.4, texture: Texture | None = None):
8
+ self.color = color
9
+ self.shininess = shininess
10
+ self.specular = specular
11
+ self.texture = texture