MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
viewer.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
6
7#include <SDL3/SDL.h>
8
9#include <algorithm>
10#include <array>
11#include <chrono>
12#include <cmath>
13#include <cstdlib>
14#include <filesystem>
15#include <format>
16#include <iostream>
17#include <memory>
18#include <string>
19
20#include <glm/ext/matrix_clip_space.hpp>
21#include <glm/ext/matrix_transform.hpp>
22#include <glm/glm.hpp>
23
24#ifndef VIEWER_ASSET_DIR
25#define VIEWER_ASSET_DIR "."
26#endif
27
28#ifndef VIEWER_SHADER_DIR
29#define VIEWER_SHADER_DIR "."
30#endif
31
32namespace viewer {
33
35 public:
36 explicit ModelViewerWindow(const Arguments &args) : mxvk::VK_Window("MXModel Viewer - [ Vulkan ]", args.width, args.height, args.fullscreen, MXVK_VALIDATION, args.enable_vsync), assetRoot((args.path.empty() || args.path == ".") ? std::string(VIEWER_ASSET_DIR) : args.path), shaderRoot(args.shaderPath.empty() ? assetRoot + "/data" : args.shaderPath), benchmarkEnabled(args.benchmark) {
37 setClearColor(0.3f, 0.3f, 0.3f, 1.0f);
38 setFont(resolveFontPath(), 18);
39
40 modelPath = resolveModelPath(args.filename.empty() ? defaultModelName : args.filename);
41 textureManifestPath = resolveOptionalPath(args.texture);
42 textureBasePath = args.resource_path.empty() ? resolveTextureBasePath(textureManifestPath) : resolveOptionalPath(args.resource_path);
43
44 std::cout << "viewer: model='" << modelPath << "'\n";
45 if (!textureManifestPath.empty()) {
46 std::cout << "viewer: texture manifest='" << textureManifestPath << "' base='" << textureBasePath << "'\n";
47 }
48
49 model.load(this, modelPath, textureManifestPath, textureBasePath, 1.0f);
50 model.setShaders(this, shaderRoot + "/model.vert.spv", shaderRoot + "/model.frag.spv");
51 const std::string modelFormat = std::filesystem::path(modelPath).extension() == ".obj" ? "OBJ" : "model";
52 benchmarkName = std::format("{} geometry draw (Vulkan backend, {} frames)", modelFormat, BENCHMARK_FRAME_COUNT);
53 }
54
55 ~ModelViewerWindow() override {
56 if (device != VK_NULL_HANDLE) {
57 vkDeviceWaitIdle(device);
58 }
59 model.cleanup(this);
60 }
61
62 void event(SDL_Event &e) override {
63 if (e.type == SDL_EVENT_KEY_DOWN) {
64 if (e.key.key == SDLK_ESCAPE) {
65 exit();
66 return;
67 }
68 if (e.key.repeat) {
69 return;
70 }
71
72 switch (e.key.key) {
73 case SDLK_W:
74 wireframe = !wireframe;
75 std::cout << "viewer: wireframe " << (wireframe ? "on" : "off") << '\n';
76 return;
77 case SDLK_P:
78 case SDLK_R:
79 autoRotate = !autoRotate;
80 std::cout << "viewer: auto-rotate " << (autoRotate ? "on" : "off") << '\n';
81 return;
82 case SDLK_H:
83 case SDLK_SPACE:
84 showHelp = !showHelp;
85 return;
86 case SDLK_HOME:
87 resetView();
88 return;
89 case SDLK_EQUALS:
90 case SDLK_PLUS:
91 adjustCameraDistance(-0.5f);
92 return;
93 case SDLK_MINUS:
94 adjustCameraDistance(0.5f);
95 return;
96 default:
97 break;
98 }
99 }
100
101 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
102 mouseDragging = true;
103 lastMouseX = static_cast<int>(e.button.x);
104 lastMouseY = static_cast<int>(e.button.y);
105 return;
106 }
107
108 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
109 mouseDragging = false;
110 return;
111 }
112
113 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseDragging) {
114 const int x = static_cast<int>(e.motion.x);
115 const int y = static_cast<int>(e.motion.y);
116 rotationYDegrees += static_cast<float>(x - lastMouseX) * mouseSensitivity;
117 rotationXDegrees += static_cast<float>(y - lastMouseY) * mouseSensitivity;
118 rotationXDegrees = std::clamp(rotationXDegrees, -89.0f, 89.0f);
119 lastMouseX = x;
120 lastMouseY = y;
121 return;
122 }
123
124 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
125 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y : static_cast<float>(e.wheel.integer_y);
126 adjustCameraDistance(-delta * 0.45f);
127 return;
128 }
129 }
130
131 void proc() override {
132 const bool *keys = SDL_GetKeyboardState(nullptr);
133 if (keys == nullptr) {
134 return;
135 }
136
137 const auto now = std::chrono::steady_clock::now();
138 const float deltaSeconds = std::clamp(std::chrono::duration<float>(now - lastUpdateTime).count(), 0.0f, 0.1f);
139 lastUpdateTime = now;
140
141 constexpr float ROTATE_SPEED = 120.0f;
142 if (keys[SDL_SCANCODE_LEFT]) {
143 rotationYDegrees -= ROTATE_SPEED * deltaSeconds;
144 }
145 if (keys[SDL_SCANCODE_RIGHT]) {
146 rotationYDegrees += ROTATE_SPEED * deltaSeconds;
147 }
148 if (keys[SDL_SCANCODE_UP]) {
149 rotationXDegrees -= ROTATE_SPEED * deltaSeconds;
150 }
151 if (keys[SDL_SCANCODE_DOWN]) {
152 rotationXDegrees += ROTATE_SPEED * deltaSeconds;
153 }
154 if (keys[SDL_SCANCODE_A]) {
155 adjustCameraDistance(-2.5f * deltaSeconds);
156 }
157 if (keys[SDL_SCANCODE_S]) {
158 adjustCameraDistance(2.5f * deltaSeconds);
159 }
160 rotationXDegrees = std::clamp(rotationXDegrees, -89.0f, 89.0f);
161
162 if (autoRotate) {
163 autoRotationRadians = wrapRadians(autoRotationRadians + 0.55f * deltaSeconds);
164 }
165
166 if (!benchmarkEnabled) {
167 updateOverlay();
168 }
169 }
170
171 void render() override {
172 if (benchmarkEnabled && benchmarkStopwatch == nullptr) {
173 benchmarkStopwatch = std::make_unique<StopWatch<HighResolutionClockPolicy>>(benchmarkName);
174 }
175
177
178 if (benchmarkStopwatch == nullptr) {
179 return;
180 }
181 ++benchmarkFrameCount;
182 if (benchmarkFrameCount == BENCHMARK_FRAME_COUNT) {
183 if (device != VK_NULL_HANDLE) {
184 vkDeviceWaitIdle(device);
185 }
186 benchmarkStopwatch->Stop();
187 benchmarkStopwatch.reset();
188 exit();
189 }
190 }
191
192 void onSwapchainRecreated() override { model.resize(this); }
193
194 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
195 const VkExtent2D extent = getSwapchainExtent();
196 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
197 const float elapsedSeconds = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime).count();
198
200 ubo.model = glm::rotate(glm::mat4(1.0f), glm::radians(rotationXDegrees), glm::vec3(1.0f, 0.0f, 0.0f));
201 ubo.model = glm::rotate(ubo.model, glm::radians(rotationYDegrees) + autoRotationRadians, glm::vec3(0.0f, 1.0f, 0.0f));
202 ubo.model = glm::scale(ubo.model, glm::vec3(model.modelRenderScale()));
203 ubo.model = glm::translate(ubo.model, model.modelCenterOffset());
204 ubo.view = glm::lookAt(glm::vec3(0.0f, 0.0f, cameraDistance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
205 ubo.proj = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 100.0f);
206 ubo.proj[1][1] *= -1.0f;
207 ubo.fx = glm::vec4(elapsedSeconds, wireframe ? 1.0f : 0.0f, 0.0f, 0.0f);
208
209 model.updateUBO(imageIndex, ubo);
210 model.render(cmd, imageIndex, wireframe);
211 }
212
213 private:
214 [[nodiscard]] std::string resolveFontPath() const {
215 const std::array<std::filesystem::path, 4> candidates = {
216 std::filesystem::path(assetRoot) / "data/default.ttf",
217 std::filesystem::path(assetRoot) / "data/font.ttf",
218 std::filesystem::path(VIEWER_ASSET_DIR) / "data/default.ttf",
219 std::filesystem::path(VIEWER_ASSET_DIR) / "data/font.ttf",
220 };
221
222 for (const std::filesystem::path &candidate : candidates) {
223 if (std::filesystem::exists(candidate)) {
224 return candidate.string();
225 }
226 }
227
228 return candidates.front().string();
229 }
230
231 [[nodiscard]] std::string resolveModelPath(const std::string &name) const {
232 namespace fs = std::filesystem;
233 const fs::path requested(name);
234 std::error_code ec{};
235
236 if (fs::exists(requested, ec)) {
237 return fs::weakly_canonical(requested, ec).string();
238 }
239 ec.clear();
240
241 const fs::path assetPath(assetRoot);
242 const fs::path sourceRoot = fs::path(VIEWER_SOURCE_DIR).parent_path().parent_path();
243 const fs::path runtimeRoot = fs::path(VIEWER_ASSET_DIR).parent_path().parent_path();
244 const fs::path fileName = requested.filename();
245 const fs::path withoutExtension = fileName.stem();
246
247 const fs::path candidates[] = {
248 assetPath / requested,
249 assetPath / "data" / requested,
250 sourceRoot / requested,
251 sourceRoot / "models" / requested,
252 sourceRoot / "models" / fileName,
253 sourceRoot / "models" / (withoutExtension.string() + ".mxmod.z"),
254 runtimeRoot / requested,
255 runtimeRoot / "models" / requested,
256 runtimeRoot / "models" / fileName,
257 runtimeRoot / "models" / (withoutExtension.string() + ".mxmod.z"),
258 };
259
260 for (const fs::path &candidate : candidates) {
261 if (fs::exists(candidate, ec)) {
262 return fs::weakly_canonical(candidate, ec).string();
263 }
264 ec.clear();
265 }
266
267 throw mxvk::Exception("viewer: failed to locate model: " + name);
268 }
269
270 [[nodiscard]] std::string resolveOptionalPath(const std::string &name) const {
271 if (name.empty()) {
272 return {};
273 }
274
275 namespace fs = std::filesystem;
276 const fs::path requested(name);
277 std::error_code ec{};
278 if (fs::exists(requested, ec)) {
279 return fs::weakly_canonical(requested, ec).string();
280 }
281 ec.clear();
282
283 const fs::path assetPath(assetRoot);
284 const fs::path sourceRoot = fs::path(VIEWER_SOURCE_DIR).parent_path().parent_path();
285 const fs::path candidates[] = {
286 assetPath / requested,
287 assetPath / "data" / requested,
288 sourceRoot / requested,
289 sourceRoot / "models" / requested,
290 };
291
292 for (const fs::path &candidate : candidates) {
293 if (fs::exists(candidate, ec)) {
294 return fs::weakly_canonical(candidate, ec).string();
295 }
296 ec.clear();
297 }
298
299 return name;
300 }
301
302 [[nodiscard]] static std::string resolveTextureBasePath(const std::string &manifestPath) {
303 if (manifestPath.empty()) {
304 return {};
305 }
306 return std::filesystem::path(manifestPath).parent_path().string();
307 }
308
309 void adjustCameraDistance(float delta) { cameraDistance = std::clamp(cameraDistance + delta, 0.4f, 100.0f); }
310
311 void resetView() {
312 rotationXDegrees = 0.0f;
313 rotationYDegrees = 0.0f;
314 autoRotationRadians = 0.0f;
315 cameraDistance = 5.0f;
316 }
317
318 [[nodiscard]] static float wrapRadians(float radians) {
319 constexpr float TWO_PI = 6.28318530718f;
320 float wrapped = std::fmod(radians, TWO_PI);
321 if (wrapped < 0.0f) {
322 wrapped += TWO_PI;
323 }
324 return wrapped;
325 }
326
327 void updateOverlay() {
328 ++frameCount;
329 const auto now = std::chrono::steady_clock::now();
330 const double elapsed = std::chrono::duration<double>(now - fpsSampleTime).count();
331 if (elapsed >= 0.25) {
332 fpsText = std::format("FPS: {:.1f}", static_cast<double>(frameCount) / elapsed);
333 frameCount = 0;
334 fpsSampleTime = now;
335 }
336
337 printText(fpsText, 14, 12, SDL_Color{235, 240, 255, 255});
338 printText(std::format("Model: {}", std::filesystem::path(modelPath).filename().string()), 14, 36, SDL_Color{210, 220, 235, 255});
339 printText(std::format("Mode: {} Auto: {} Distance: {:.1f}", wireframe ? "wire" : "fill", autoRotate ? "on" : "off", cameraDistance), 14, 60, SDL_Color{210, 220, 235, 255});
340
341 if (showHelp) {
342 printText("Drag/arrows rotate Wheel/+/-/A/S zoom W wire R/P auto-rotate H/Space help Home reset Esc quit", 14, static_cast<int>(getSwapchainExtent().height) - 30, SDL_Color{185, 198, 215, 255});
343 }
344 }
345
346 static constexpr const char *defaultModelName = "cube.mxmod.z";
347 static constexpr std::size_t BENCHMARK_FRAME_COUNT = 60 * 10;
348
349 std::string assetRoot{};
350 std::string shaderRoot{};
351 std::string modelPath{};
352 std::string textureManifestPath{};
353 std::string textureBasePath{};
354 mxvk::VKAbstractModel model{};
355 bool wireframe = false;
356 bool autoRotate = true;
357 bool showHelp = true;
358 bool mouseDragging = false;
359 int lastMouseX = 0;
360 int lastMouseY = 0;
361 float mouseSensitivity = 0.5f;
362 float rotationXDegrees = 0.0f;
363 float rotationYDegrees = 0.0f;
364 float autoRotationRadians = 0.0f;
365 float cameraDistance = 5.0f;
366 uint32_t frameCount = 0;
367 std::string fpsText = "FPS: --";
368 std::chrono::steady_clock::time_point fpsSampleTime{std::chrono::steady_clock::now()};
369 std::chrono::steady_clock::time_point lastUpdateTime{std::chrono::steady_clock::now()};
370 std::chrono::steady_clock::time_point startTime{std::chrono::steady_clock::now()};
371 bool benchmarkEnabled = false;
372 std::size_t benchmarkFrameCount = 0;
373 std::string benchmarkName;
374 std::unique_ptr<StopWatch<HighResolutionClockPolicy>> benchmarkStopwatch;
375 };
376
377} // namespace viewer
378
379int main(int argc, char **argv) {
380 std::cout.setf(std::ios::unitbuf);
381 std::cerr.setf(std::ios::unitbuf);
382 setvbuf(stdout, nullptr, _IONBF, 0);
383 setvbuf(stderr, nullptr, _IONBF, 0);
384
385 try {
386 Arguments args = proc_args(argc, argv);
387 if (args.benchmark && !args.resolutionSpecified) {
388 args.width = 320;
389 args.height = 180;
390 std::cout << "viewer: benchmark resolution defaults to 320x180; use -r or --resolution to override\n";
391 }
392 viewer::ModelViewerWindow window(args);
393 window.loop();
394 } catch (const mxvk::Exception &e) {
395 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
396 return EXIT_FAILURE;
397 } catch (const ArgException<std::string> &e) {
398 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
399 return EXIT_FAILURE;
400 } catch (const std::exception &e) {
401 std::cerr << std::format("viewer: Exception: {}\n", e.what());
402 return EXIT_FAILURE;
403 }
404 return EXIT_SUCCESS;
405}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
std::string text() const
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:222
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
VkDevice device
Definition mxvk.hpp:606
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3919
virtual void render()
Render one frame.
Definition mxvk.cpp:824
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition viewer.cpp:194
~ModelViewerWindow() override
Definition viewer.cpp:55
void event(SDL_Event &e) override
Handle one SDL event.
Definition viewer.cpp:62
ModelViewerWindow(const Arguments &args)
Definition viewer.cpp:36
void render() override
Render one frame.
Definition viewer.cpp:171
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition viewer.cpp:192
void proc() override
Execute one processing/update step.
Definition viewer.cpp:131
#define MXVK_VALIDATION
Definition mxvk.hpp:28
High-level model wrapper integrated with MXVK dynamic rendering.
int main()
Definition main.py:165
Definition model.py:1
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
bool resolutionSpecified
Whether -r/–resolution was provided.
Definition argz.hpp:722
std::string texture
Optional texture file path (--texture).
Definition argz.hpp:752
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string resource_path
Resource path.
Definition argz.hpp:759
std::string filename
Optional input filename (--filename).
Definition argz.hpp:726
bool benchmark
Enable application benchmark mode (--benchmark).
Definition argz.hpp:741
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720
Default transform UBO payload for model shaders.
#define VIEWER_ASSET_DIR
Definition viewer.cpp:25