MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
model.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2## @file model.py
3## @brief Interactive Python MXVK model loader and renderer.
4## @details Loads an OBJ or MXMOD asset, builds NumPy transform matrices, and
5## renders the model with mouse-controlled camera interaction. The resource
6## path selects the shader and texture data directory used by the example.
7##
8## @section model_run Running the example
9## @code{.sh}
10## python3 python-examples/model/model.py --resolution 1280x720
11## python3 python-examples/model/model.py --input models/pyramid.obj --enable-vsync
12## @endcode
13## @section model_controls Controls
14## - Drag with the left mouse button: orbit the camera.
15## - Mouse wheel: zoom.
16## - Escape: exit.
17## @section model_lifecycle Lifecycle
18## @c ModelWindow creates and renders @c AbstractModel through the Python
19## binding, then calls model cleanup before releasing the Vulkan window.
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 = "MXVK Model Loader"
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, 1.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
132 self.model = mxvk.AbstractModel()
133 self.model.load(self, str(model_path), str(material_path) if material_path.is_file() else "", str(model_path.parent), 1.0)
134 self.model.set_shaders(self, str(model_vertex_shader), str(model_fragment_shader))
135
136 except Exception:
137 self.close()
138 raise
139
140 def close(self):
141 if (self._closed):
142 return
143
144 self._closed = True
145
146 try:
147 self.wait_idle()
148
149 finally:
150 try:
151 if (self.model is not None):
152 self.model.cleanup(self)
153 self.model = None
154 finally:
155 self.release()
156
157 def event(self, event):
158 if (event.key_down):
159 if (event.key == mxvk.KEY_ESCAPE):
160 self.request_exit()
161 return
162
163 if (event.mouse_button_down and event.button == mxvk.MOUSE_BUTTON_LEFT):
164 self.mouse_dragging = True
165 self.last_mouse_x = int(event.x)
166 self.last_mouse_y = int(event.y)
167 return
168
169 if (event.mouse_button_up and event.button == mxvk.MOUSE_BUTTON_LEFT):
170 self.mouse_dragging = False
171 return
172
173 if (event.mouse_motion and self.mouse_dragging):
174 x = int(event.x)
175 y = int(event.y)
176 delta_x = x - self.last_mouse_x
177 delta_y = y - self.last_mouse_y
178 self.yaw_degrees += float(delta_x) * self.mouse_sensitivity
179 self.pitch_degrees += float(delta_y) * self.mouse_sensitivity
180 self.pitch_degrees = max(-55.0, min(55.0, self.pitch_degrees))
181 self.last_mouse_x = x
182 self.last_mouse_y = y
183 return
184
185 if (event.mouse_wheel):
186 if (event.wheel_y != 0.0):
187 delta = float(event.wheel_y)
188 else:
189 delta = float(event.wheel_ticks_y)
190
191 self.camera_distance -= delta * 0.35
192 self.camera_distance = max(2.0, min(10.0, self.camera_distance))
193 return
194
196 if (self.model is not None):
197 self.model.resize(self)
198
199 def proc(self):
200 width, _ = self.swapchain_extent
201
202 if (width <= 0):
203 width = self.fallback_width
204
205 dimensions = self.get_text_dimensions(self.TITLE_TEXT)
206
207 if (dimensions is not None):
208 text_width, _ = dimensions
209 text_x = max(0, (int(width) - int(text_width)) // 2)
210 self.print_text(self.TITLE_TEXT, text_x, 24, mxvk.Color(255, 255, 255, 255))
211
212 def on_record_custom_rendering(self, command_buffer, image_index: int):
213 if (self.model is None):
214 return
215
216 now = time.monotonic()
217 delta_seconds = now - self.last_frame_time
218
219 self.elapsed_seconds = now - self.start_time
220 self.last_frame_time = now
221
222 if (self.auto_spin_enabled):
223 self.auto_spin_radians += delta_seconds * 0.55
224
225 width, height = self.swapchain_extent
226
227 if (height > 0):
228 aspect = float(width) / float(height)
229 else:
230 aspect = 1.0
231
232 yaw_radians = math.radians(self.yaw_degrees)
233 pitch_radians = math.radians(self.pitch_degrees)
234 camera_x = self.camera_distance * math.cos(pitch_radians) * math.sin(yaw_radians)
235 camera_y = 0.35 + self.camera_distance * math.sin(pitch_radians)
236 camera_z = self.camera_distance * math.cos(pitch_radians) * math.cos(yaw_radians)
237 camera_position = (camera_x, camera_y, camera_z)
238 view_matrix = look_at_matrix(camera_position, (0.0, 0.35, 0.0), (0.0, 1.0, 0.0))
239 projection_matrix = perspective_matrix(math.radians(48.0), aspect, 0.1, 100.0)
240 center_x, center_y, center_z = self.model.center_offset
241 render_scale = float(self.model.render_scale) * 1.18
242 model_matrix = rotation_y_matrix(self.auto_spin_radians)
243 model_matrix = model_matrix @ scale_matrix(render_scale, render_scale, render_scale)
244 model_matrix = model_matrix @ translation_matrix(center_x, center_y, center_z)
245 uniforms = mxvk.ModelUniforms()
246 uniforms.model = matrix_values(model_matrix)
247 uniforms.view = matrix_values(view_matrix)
248 uniforms.projection = matrix_values(projection_matrix)
249 uniforms.effects = [
250 self.elapsed_seconds,
251 0.0,
252 0.0,
253 0.42,
254 ]
255 self.model.update_uniforms(image_index, uniforms)
256 self.model.render(command_buffer, image_index, False)
257
258def parse_resolution(value: str) -> tuple[int, int]:
259 try:
260 width_text, height_text = value.lower().split("x", 1)
261 width = int(width_text)
262 height = int(height_text)
263
264 except ValueError as exception:
265 raise argparse.ArgumentTypeError("resolution must use WIDTHxHEIGHT, for example 1280x720") from exception
266
267 if (width <= 0 or height <= 0):
268 raise argparse.ArgumentTypeError("resolution width and height must be greater than zero")
269 return width, height
270
272 parser = argparse.ArgumentParser(description="MXVK textured Penguin Python example")
273 parser.add_argument("filename", nargs="?", default="", help="optional OBJ or MXMOD model path")
274 parser.add_argument("-i", "--input", dest="input_filename", default="", help="optional OBJ or MXMOD model path")
275 parser.add_argument("-p", "--path", default=str(EXAMPLE_DIR), help="resource root containing the data directory")
276 parser.add_argument("-r", "--resolution", type=parse_resolution, default=(1280, 720), metavar="WIDTHxHEIGHT", help="window resolution (default: 1280x720)")
277 parser.add_argument("-f", "--fullscreen", action="store_true", help="start fullscreen")
278 parser.add_argument("--enable-vsync", action="store_true", help="enable vertical synchronization")
279 return parser.parse_args()
280
281def main() -> int:
282 args = parse_args()
283 width, height = args.resolution
284 if (args.input_filename):
285 filename = args.input_filename
286 else:
287 filename = args.filename
288 window = None
289 try:
290 window = ModelWindow(filename, args.path, "MXVK Model Test", width, height, args.fullscreen, args.enable_vsync)
291 window.loop()
292
293 except mxvk.MXVKError as exception:
294 print(f"mxvk: Exception: {exception}", file=sys.stderr)
295 return 1
296 finally:
297 if (window is not None):
298 window.close()
299 return 0
300
301if (__name__ == "__main__"):
302 raise SystemExit(main())
on_swapchain_recreated(self)
Definition model.py:195
on_record_custom_rendering(self, command_buffer, int image_index)
Definition model.py:212
float camera_distance
Definition model.py:108
bool auto_spin_enabled
Definition model.py:103
event(self, event)
Definition model.py:157
float elapsed_seconds
Definition model.py:111
float yaw_degrees
Definition model.py:106
bool mouse_dragging
Definition model.py:102
__init__(self, str filename, str path, str title, int width, int height, bool fullscreen, bool enable_vsync)
Definition model.py:93
float auto_spin_radians
Definition model.py:110
float mouse_sensitivity
Definition model.py:109
float pitch_degrees
Definition model.py:107
str data_directory
Definition model.py:98
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
Definition main.py:1
list[float] matrix_values(np.ndarray matrix)
Definition model.py:33
np.ndarray identity_matrix()
Definition model.py:36
int main()
Definition model.py:281
np.ndarray normalize_vector(np.ndarray vector)
Definition model.py:50
np.ndarray translation_matrix(float x, float y, float z)
Definition model.py:47
np.ndarray look_at_matrix(tuple[float, float, float] eye, tuple[float, float, float] center, tuple[float, float, float] up)
Definition model.py:57
np.ndarray scale_matrix(float x, float y, float z)
Definition model.py:44
np.ndarray rotation_y_matrix(float angle)
Definition model.py:39
parse_args()
Definition model.py:271
np.ndarray perspective_matrix(float fov_radians, float aspect, float near_plane, float far_plane)
Definition model.py:79
tuple[int, int] parse_resolution(str value)
Definition model.py:258