MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
penguin.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2## @file penguin.py
3## @brief Python MXVK textured penguin model demonstration.
4## @details Shows NumPy transform generation, model loading, and interactive
5## camera controls in a Python @c VK_Window application. An optional OBJ or
6## MXMOD input replaces the bundled asset while retaining the example shaders.
7##
8## @section penguin_run Running the example
9## @code{.sh}
10## python3 python-examples/penguin/penguin.py --resolution 1280x720
11## python3 python-examples/penguin/penguin.py --input penguin.mxmod --enable-vsync
12## @endcode
13## @section penguin_controls Controls
14## - Drag with the left mouse button: orbit the camera.
15## - Mouse wheel: zoom.
16## - Escape: exit.
17## @section penguin_lifecycle Lifecycle
18## @c PenguinWindow owns the native model handle and cleans it up before calling
19## @c release(), avoiding Vulkan-resource teardown after the parent device dies.
20
21from __future__ import annotations
22
23import argparse
24import math
25import sys
26import time
27from pathlib import Path
28import numpy as np
29import mxvk_ext as mxvk
30
31EXAMPLE_DIR = Path(__file__).resolve().parent
32
33def matrix_values(matrix: np.ndarray) -> list[float]:
34 return matrix.astype(np.float32, copy=False).reshape(16).tolist()
35
36def identity_matrix() -> np.ndarray:
37 return np.eye(4, dtype=np.float32)
38
39def rotation_y_matrix(angle: float) -> np.ndarray:
40 cosine = math.cos(angle)
41 sine = math.sin(angle)
42 return np.array([[cosine, 0.0, sine, 0.0], [0.0, 1.0, 0.0, 0.0], [-sine, 0.0, cosine, 0.0], [0.0, 0.0, 0.0, 1.0]], dtype=np.float32)
43
44def scale_matrix(x: float, y: float, z: float) -> np.ndarray:
45 return np.array([[x, 0.0, 0.0, 0.0], [0.0, y, 0.0, 0.0], [0.0, 0.0, z, 0.0], [0.0, 0.0, 0.0, 1.0]], dtype=np.float32)
46
47def translation_matrix(x: float, y: float, z: float) -> np.ndarray:
48 return np.array([[1.0, 0.0, 0.0, x], [0.0, 1.0, 0.0, y], [0.0, 0.0, 1.0, z], [0.0, 0.0, 0.0, 1.0]], dtype=np.float32)
49
50def normalize_vector(vector: np.ndarray) -> np.ndarray:
51 length = float(np.linalg.norm(vector))
52 if (length <= 0.0):
53 return vector
54
55 return vector / length
56
57def look_at_matrix(eye: tuple[float, float, float], center: tuple[float, float, float], up: tuple[float, float, float]) -> np.ndarray:
58 eye_vector = np.array(eye, dtype=np.float32)
59 center_vector = np.array(center, dtype=np.float32)
60 up_vector = np.array(up, dtype=np.float32)
61 forward = normalize_vector(center_vector - eye_vector)
62 side = normalize_vector(np.cross(forward, up_vector))
63 camera_up = np.cross(side, forward)
64 matrix = identity_matrix()
65 matrix[0, 0] = side[0]
66 matrix[0, 1] = side[1]
67 matrix[0, 2] = side[2]
68 matrix[0, 3] = -float(np.dot(side, eye_vector))
69 matrix[1, 0] = camera_up[0]
70 matrix[1, 1] = camera_up[1]
71 matrix[1, 2] = camera_up[2]
72 matrix[1, 3] = -float(np.dot(camera_up, eye_vector))
73 matrix[2, 0] = -forward[0]
74 matrix[2, 1] = -forward[1]
75 matrix[2, 2] = -forward[2]
76 matrix[2, 3] = float(np.dot(forward, eye_vector))
77 return matrix
78
79def perspective_matrix(fov_radians: float, aspect: float, near_plane: float, far_plane: float) -> np.ndarray:
80 tangent = math.tan(fov_radians * 0.5)
81 matrix = np.zeros((4, 4), dtype=np.float32)
82 matrix[0, 0] = 1.0 / (aspect * tangent)
83 matrix[1, 1] = 1.0 / tangent
84 matrix[2, 2] = far_plane / (near_plane - far_plane)
85 matrix[2, 3] = (far_plane * near_plane) / (near_plane - far_plane)
86 matrix[3, 2] = -1.0
87 matrix[1, 1] *= -1.0
88 return matrix
89
91 TITLE_TEXT = "Python Penguin"
92
93 def __init__(self, filename: str, path: str, title: str, width: int, height: int, fullscreen: bool, enable_vsync: bool):
94 if (not path or path == "."):
95 self.asset_root = EXAMPLE_DIR
96 else:
97 self.asset_root = Path(path).expanduser().resolve()
98 self.data_directory = self.asset_root / "data"
99 mxvk.set_default_shader_directory(str(self.data_directory))
100 self.fallback_width = width
101 self.fallback_height = height
102 self.mouse_dragging = False
106 self.yaw_degrees = 0.0
107 self.pitch_degrees = 0.0
108 self.camera_distance = 5.35
112 self.start_time = time.monotonic()
114 self.model = None
115 self._closed = False
116 super().__init__(title, width, height, fullscreen, False, enable_vsync)
117
118 try:
119 self.set_clear_color(0.0, 0.0, 0.0, 0.0)
120 self.set_font(str(self.data_directory / "font.ttf"), 48)
121
122 if (filename):
123 model_path = Path(filename).expanduser()
124 material_path = model_path.with_suffix(".mtl")
125 else:
126 model_path = self.data_directory / "classic_tux.obj"
127 material_path = self.data_directory / "classic_tux.mtl"
128
129 model_vertex_shader = self.data_directory / "model.vert.spv"
130 model_fragment_shader = self.data_directory / "model.frag.spv"
131 gradient_fragment_shader = self.data_directory / "gradient_mix.frag.spv"
132
133 self.model = mxvk.AbstractModel()
134 self.model.load(self, str(model_path), str(material_path) if material_path.is_file() else "", str(model_path.parent), 1.0)
135 self.model.set_shaders(self, str(model_vertex_shader), str(model_fragment_shader))
136 self.attach_post_processing_shader(str(gradient_fragment_shader), 0.0, 0.0, 0.0, 0.0)
137 self.set_post_processing_shader_time_enabled(True)
138 self.set_post_processing_enabled(True)
139
140 except Exception:
141 self.close()
142 raise
143
144 def close(self):
145 if (self._closed):
146 return
147
148 self._closed = True
149
150 try:
151 self.wait_idle()
152 self.detach_post_processing_shader()
153
154 finally:
155 try:
156 if (self.model is not None):
157 self.model.cleanup(self)
158 self.model = None
159 finally:
160 self.release()
161
162 def event(self, event):
163 if (event.key_down):
164 if (event.key == mxvk.KEY_ESCAPE):
165 self.request_exit()
166 return
167
168 if (event.mouse_button_down and event.button == mxvk.MOUSE_BUTTON_LEFT):
169 self.mouse_dragging = True
170 self.last_mouse_x = int(event.x)
171 self.last_mouse_y = int(event.y)
172 return
173
174 if (event.mouse_button_up and event.button == mxvk.MOUSE_BUTTON_LEFT):
175 self.mouse_dragging = False
176 return
177
178 if (event.mouse_motion and self.mouse_dragging):
179 x = int(event.x)
180 y = int(event.y)
181 delta_x = x - self.last_mouse_x
182 delta_y = y - self.last_mouse_y
183 self.yaw_degrees += float(delta_x) * self.mouse_sensitivity
184 self.pitch_degrees += float(delta_y) * self.mouse_sensitivity
185 self.pitch_degrees = max(-55.0, min(55.0, self.pitch_degrees))
186 self.last_mouse_x = x
187 self.last_mouse_y = y
188 return
189
190 if (event.mouse_wheel):
191 if (event.wheel_y != 0.0):
192 delta = float(event.wheel_y)
193 else:
194 delta = float(event.wheel_ticks_y)
195
196 self.camera_distance -= delta * 0.35
197 self.camera_distance = max(2.0, min(10.0, self.camera_distance))
198 return
199
201 if (self.model is not None):
202 self.model.resize(self)
203
204 def proc(self):
205 width, _ = self.swapchain_extent
206
207 if (width <= 0):
208 width = self.fallback_width
209
210 dimensions = self.get_text_dimensions(self.TITLE_TEXT)
211
212 if (dimensions is not None):
213 text_width, _ = dimensions
214 text_x = max(0, (int(width) - int(text_width)) // 2)
215 self.print_text(self.TITLE_TEXT, text_x, 24, mxvk.Color(255, 255, 255, 255))
216
217 def on_record_custom_rendering(self, command_buffer, image_index: int):
218 if (self.model is None):
219 return
220
221 now = time.monotonic()
222 delta_seconds = now - self.last_frame_time
223
224 self.elapsed_seconds = now - self.start_time
225 self.last_frame_time = now
226
227 if (self.auto_spin_enabled):
228 self.auto_spin_radians += delta_seconds * 0.55
229
230 width, height = self.swapchain_extent
231
232 if (height > 0):
233 aspect = float(width) / float(height)
234 else:
235 aspect = 1.0
236
237 yaw_radians = math.radians(self.yaw_degrees)
238 pitch_radians = math.radians(self.pitch_degrees)
239 camera_x = self.camera_distance * math.cos(pitch_radians) * math.sin(yaw_radians)
240 camera_y = 0.35 + self.camera_distance * math.sin(pitch_radians)
241 camera_z = self.camera_distance * math.cos(pitch_radians) * math.cos(yaw_radians)
242 camera_position = (camera_x, camera_y, camera_z)
243 view_matrix = look_at_matrix(camera_position, (0.0, 0.35, 0.0), (0.0, 1.0, 0.0))
244 projection_matrix = perspective_matrix(math.radians(48.0), aspect, 0.1, 100.0)
245 center_x, center_y, center_z = self.model.center_offset
246 render_scale = float(self.model.render_scale) * 1.18
247 model_matrix = rotation_y_matrix(self.auto_spin_radians)
248 model_matrix = model_matrix @ scale_matrix(render_scale, render_scale, render_scale)
249 model_matrix = model_matrix @ translation_matrix(center_x, center_y, center_z)
250 uniforms = mxvk.ModelUniforms()
251 uniforms.model = matrix_values(model_matrix)
252 uniforms.view = matrix_values(view_matrix)
253 uniforms.projection = matrix_values(projection_matrix)
254 uniforms.effects = [
255 self.elapsed_seconds,
256 0.0,
257 0.0,
258 0.42,
259 ]
260 self.model.update_uniforms(image_index, uniforms)
261 self.model.render(command_buffer, image_index, False)
262
263def parse_resolution(value: str) -> tuple[int, int]:
264 try:
265 width_text, height_text = value.lower().split("x", 1)
266 width = int(width_text)
267 height = int(height_text)
268
269 except ValueError as exception:
270 raise argparse.ArgumentTypeError("resolution must use WIDTHxHEIGHT, for example 1280x720") from exception
271
272 if (width <= 0 or height <= 0):
273 raise argparse.ArgumentTypeError("resolution width and height must be greater than zero")
274 return width, height
275
277 parser = argparse.ArgumentParser(description="MXVK textured Penguin Python example")
278 parser.add_argument("filename", nargs="?", default="", help="optional OBJ or MXMOD model path")
279 parser.add_argument("-i", "--input", dest="input_filename", default="", help="optional OBJ or MXMOD model path")
280 parser.add_argument("-p", "--path", default=str(EXAMPLE_DIR), help="resource root containing the data directory")
281 parser.add_argument("-r", "--resolution", type=parse_resolution, default=(1280, 720), metavar="WIDTHxHEIGHT", help="window resolution (default: 1280x720)")
282 parser.add_argument("-f", "--fullscreen", action="store_true", help="start fullscreen")
283 parser.add_argument("--enable-vsync", action="store_true", help="enable vertical synchronization")
284 return parser.parse_args()
285
286def main() -> int:
287 args = parse_args()
288 width, height = args.resolution
289 if (args.input_filename):
290 filename = args.input_filename
291 else:
292 filename = args.filename
293 window = None
294 try:
295 window = PenguinWindow(filename, args.path, "MXVK Python Penguin", width, height, args.fullscreen, args.enable_vsync)
296 window.loop()
297
298 except mxvk.MXVKError as exception:
299 print(f"mxvk: Exception: {exception}", file=sys.stderr)
300 return 1
301 finally:
302 if (window is not None):
303 window.close()
304 return 0
305
306if (__name__ == "__main__"):
307 raise SystemExit(main())
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkExtent2D swapchain_extent
Definition mxvk.hpp:616
void release()
Release Vulkan and SDL resources.
Definition mxvk.cpp:247
virtual void render()
Render one frame.
Definition mxvk.cpp:824
virtual void proc()
Execute one processing/update step.
Definition mxvk.cpp:1278
on_record_custom_rendering(self, command_buffer, int image_index)
Definition penguin.py:217
on_swapchain_recreated(self)
Definition penguin.py:200
event(self, event)
Definition penguin.py:162
__init__(self, str filename, str path, str title, int width, int height, bool fullscreen, bool enable_vsync)
Definition penguin.py:93
Definition main.py:1
list[float] matrix_values(np.ndarray matrix)
Definition penguin.py:33
np.ndarray identity_matrix()
Definition penguin.py:36
int main()
Definition penguin.py:286
np.ndarray normalize_vector(np.ndarray vector)
Definition penguin.py:50
np.ndarray translation_matrix(float x, float y, float z)
Definition penguin.py:47
np.ndarray look_at_matrix(tuple[float, float, float] eye, tuple[float, float, float] center, tuple[float, float, float] up)
Definition penguin.py:57
np.ndarray scale_matrix(float x, float y, float z)
Definition penguin.py:44
np.ndarray rotation_y_matrix(float angle)
Definition penguin.py:39
parse_args()
Definition penguin.py:276
np.ndarray perspective_matrix(float fov_radians, float aspect, float near_plane, float far_plane)
Definition penguin.py:79
tuple[int, int] parse_resolution(str value)
Definition penguin.py:263