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.
- eagle_engine-0.1.0/LICENSE +21 -0
- eagle_engine-0.1.0/MANIFEST.in +14 -0
- eagle_engine-0.1.0/PKG-INFO +88 -0
- eagle_engine-0.1.0/README.md +54 -0
- eagle_engine-0.1.0/eagle/__init__.py +6 -0
- eagle_engine-0.1.0/eagle/assets/logo.png +0 -0
- eagle_engine-0.1.0/eagle/assets/shaders/basic.frag +32 -0
- eagle_engine-0.1.0/eagle/assets/shaders/basic.vert +22 -0
- eagle_engine-0.1.0/eagle/assets/shaders/unlit.frag +7 -0
- eagle_engine-0.1.0/eagle/assets/shaders/unlit.vert +8 -0
- eagle_engine-0.1.0/eagle/core/__init__.py +3 -0
- eagle_engine-0.1.0/eagle/core/engine.py +255 -0
- eagle_engine-0.1.0/eagle/core/splash.py +88 -0
- eagle_engine-0.1.0/eagle/gfx/__init__.py +10 -0
- eagle_engine-0.1.0/eagle/gfx/camera.py +41 -0
- eagle_engine-0.1.0/eagle/gfx/material.py +11 -0
- eagle_engine-0.1.0/eagle/gfx/mesh.py +83 -0
- eagle_engine-0.1.0/eagle/gfx/model.py +46 -0
- eagle_engine-0.1.0/eagle/gfx/orbit_camera.py +105 -0
- eagle_engine-0.1.0/eagle/gfx/shader.py +76 -0
- eagle_engine-0.1.0/eagle/gfx/texture.py +49 -0
- eagle_engine-0.1.0/eagle/loaders/__init__.py +5 -0
- eagle_engine-0.1.0/eagle/loaders/mtl.py +51 -0
- eagle_engine-0.1.0/eagle/loaders/obj.py +151 -0
- eagle_engine-0.1.0/eagle/loaders/stl.py +55 -0
- eagle_engine-0.1.0/eagle/math/__init__.py +5 -0
- eagle_engine-0.1.0/eagle/math/mat.py +122 -0
- eagle_engine-0.1.0/eagle/math/transform.py +37 -0
- eagle_engine-0.1.0/eagle/math/vec.py +132 -0
- eagle_engine-0.1.0/eagle/primitives/__init__.py +12 -0
- eagle_engine-0.1.0/eagle/primitives/axes.py +17 -0
- eagle_engine-0.1.0/eagle/primitives/cone.py +55 -0
- eagle_engine-0.1.0/eagle/primitives/cube.py +29 -0
- eagle_engine-0.1.0/eagle/primitives/cylinder.py +46 -0
- eagle_engine-0.1.0/eagle/primitives/grid.py +22 -0
- eagle_engine-0.1.0/eagle/primitives/icosphere.py +50 -0
- eagle_engine-0.1.0/eagle/primitives/plane.py +19 -0
- eagle_engine-0.1.0/eagle/primitives/sphere.py +29 -0
- eagle_engine-0.1.0/eagle/primitives/torus.py +32 -0
- eagle_engine-0.1.0/eagle/scene/__init__.py +4 -0
- eagle_engine-0.1.0/eagle/scene/object.py +43 -0
- eagle_engine-0.1.0/eagle/scene/scene.py +40 -0
- eagle_engine-0.1.0/eagle_engine.egg-info/PKG-INFO +88 -0
- eagle_engine-0.1.0/eagle_engine.egg-info/SOURCES.txt +47 -0
- eagle_engine-0.1.0/eagle_engine.egg-info/dependency_links.txt +1 -0
- eagle_engine-0.1.0/eagle_engine.egg-info/requires.txt +9 -0
- eagle_engine-0.1.0/eagle_engine.egg-info/top_level.txt +1 -0
- eagle_engine-0.1.0/pyproject.toml +50 -0
- 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
|
|
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,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
|