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