MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
4#include "mxvk/mxvk_cv.hpp"
6#include <algorithm>
7#include <array>
8#include <chrono>
9#include <cmath>
10#include <cstdlib>
11#include <filesystem>
12#include <format>
13#include <glm/ext/matrix_clip_space.hpp>
14#include <glm/ext/matrix_transform.hpp>
15#include <glm/glm.hpp>
16#include <iostream>
17#include <opencv2/imgproc.hpp>
18#include <opencv2/videoio.hpp>
19#include <string>
20
21#ifndef opencv_model_ASSET_DIR
22#define opencv_model_ASSET_DIR "."
23#endif
24
25#ifndef opencv_model_SHADER_DIR
26#define opencv_model_SHADER_DIR "."
27#endif
28
29namespace example {
30
32 public:
33 OpenCVModelWindow(const Arguments &args, const std::string &title)
34 : mxvk::VK_Window(title, args.width, args.height, args.fullscreen, MXVK_VALIDATION, args.enable_vsync),
35 assetRoot((args.path.empty() || args.path == ".") ? std::string(opencv_model_ASSET_DIR) : args.path),
36 shaderRoot(args.shaderPath.empty() ? assetRoot + "/data" : args.shaderPath),
37 cameraIndex(args.camera_index),
38 fallbackWidth(args.width),
39 fallbackHeight(args.height) {
40 try {
41 modelPath = resolveModelPath(args.filename, assetRoot);
42 if (modelPath.empty()) {
43 throw mxvk::Exception("opencv_model: args.filename must point to a model file (.obj/.mxmod/.mxmod.z)");
44 }
45 setFont(assetRoot + "/data/font.ttf", 20);
46
47 if (!capture.open(cameraIndex)) {
48 throw mxvk::Exception(std::format("opencv_model: failed to open camera index {}", cameraIndex));
49 }
50
51 capture.set(cv::CAP_PROP_FRAME_WIDTH, static_cast<double>(fallbackWidth));
52 capture.set(cv::CAP_PROP_FRAME_HEIGHT, static_cast<double>(fallbackHeight));
53 fps = configureCameraFps();
54
55 fallbackWidth = static_cast<int>(capture.get(cv::CAP_PROP_FRAME_WIDTH));
56 fallbackHeight = static_cast<int>(capture.get(cv::CAP_PROP_FRAME_HEIGHT));
57
58 std::cout << "opencv_model: model='" << modelPath << "' capture="
59 << fallbackWidth << "x" << fallbackHeight << " @ " << fps << " fps\n";
60
61 const std::string vertPath = shaderRoot + "/model.vert.spv";
62 const std::string fragPath = shaderRoot + "/model.frag.spv";
63 model.load(this, modelPath, "", "", 1.0f);
64 model.setShaders(this, vertPath, fragPath);
65
66 if (!capture.readToModelTexture(model, true)) {
67 std::cerr << "opencv_model: failed to upload initial camera frame\n";
68 }
69 } catch (...) {
70 capture.close();
71 if (device != VK_NULL_HANDLE) {
72 vkDeviceWaitIdle(device);
73 model.cleanup(this);
74 }
75 throw;
76 }
77 }
78
79 ~OpenCVModelWindow() override {
80 if (device != VK_NULL_HANDLE) {
81 vkDeviceWaitIdle(device);
82 }
83 model.cleanup(this);
84 release();
85 capture.close();
86 }
87
88 void event(SDL_Event &e) override {
89 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
90 exit();
91 return;
92 }
93
94 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
95 mouseDragging = true;
96 lastMouseX = static_cast<int>(e.button.x);
97 lastMouseY = static_cast<int>(e.button.y);
98 return;
99 }
100
101 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
102 mouseDragging = false;
103 return;
104 }
105
106 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseDragging) {
107 const int x = static_cast<int>(e.motion.x);
108 const int y = static_cast<int>(e.motion.y);
109 const int deltaX = x - lastMouseX;
110 const int deltaY = y - lastMouseY;
111
112 mouseYawDegrees += static_cast<float>(deltaX) * mouseSensitivity;
113 mousePitchDegrees += static_cast<float>(deltaY) * mouseSensitivity;
114 mousePitchDegrees = std::clamp(mousePitchDegrees, -80.0f, 80.0f);
115
116 lastMouseX = x;
117 lastMouseY = y;
118 return;
119 }
120
121 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
122 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y : static_cast<float>(e.wheel.integer_y);
123 cameraDistance -= delta * 0.45f;
124 cameraDistance = std::clamp(cameraDistance, 1.8f, 12.0f);
125 return;
126 }
127 }
128
129 void onSwapchainRecreated() override {
130 model.resize(this);
131 }
132
133 void proc() override {
134 const auto now = std::chrono::steady_clock::now();
135 const float deltaSeconds = std::clamp(
136 std::chrono::duration<float>(now - lastUpdateTime).count(),
137 0.0f,
138 0.1f);
139 lastUpdateTime = now;
140 updateRotationFromKeyboard(deltaSeconds);
141
142 if (!capture.readToModelTexture(model, true)) {
143 capture.close();
144 if (!capture.open(cameraIndex)) {
145 return;
146 }
147 capture.set(cv::CAP_PROP_FRAME_WIDTH, static_cast<double>(fallbackWidth));
148 capture.set(cv::CAP_PROP_FRAME_HEIGHT, static_cast<double>(fallbackHeight));
149 fps = configureCameraFps();
150 if (!capture.readToModelTexture(model, true)) {
151 return;
152 }
153 }
154 updateFpsOverlay();
155 }
156
157 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
158 const VkExtent2D extent = getSwapchainExtent();
159 const float elapsedSeconds = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime).count();
160
161 const float aspect = (extent.height > 0U)
162 ? static_cast<float>(extent.width) / static_cast<float>(extent.height)
163 : 1.0f;
164
166 ubo.model = glm::mat4(1.0f);
167 ubo.model = glm::rotate(ubo.model, glm::radians(mousePitchDegrees), glm::vec3(1.0f, 0.0f, 0.0f));
168 ubo.model = glm::rotate(ubo.model, glm::radians(mouseYawDegrees), glm::vec3(0.0f, 1.0f, 0.0f));
169 ubo.model = glm::rotate(ubo.model, pitchRadians, glm::vec3(1.0f, 0.0f, 0.0f));
170 ubo.model = glm::rotate(ubo.model, yawRadians + autoSpinRadians, glm::vec3(0.0f, 1.0f, 0.0f));
171 ubo.model = glm::scale(ubo.model, glm::vec3(model.modelRenderScale()));
172 ubo.model = glm::translate(ubo.model, model.modelCenterOffset());
173 ubo.view = glm::lookAt(glm::vec3(0.0f, 0.15f, cameraDistance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
174 ubo.proj = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 100.0f);
175 ubo.proj[1][1] *= -1.0f;
176 ubo.fx = glm::vec4(elapsedSeconds, 0.32f, 0.18f + 0.12f * std::sin(elapsedSeconds * 1.2f), 0.0f);
177
178 model.updateUBO(imageIndex, ubo);
179 model.render(cmd, imageIndex, false);
180 }
181
182 private:
183 [[nodiscard]] double configureCameraFps() {
184 static constexpr std::array<double, 3> fpsChoices = {60.0, 30.0, 24.0};
185
186 for (const double requestedFps : fpsChoices) {
187 capture.set(cv::CAP_PROP_FPS, requestedFps);
188 const double reportedFps = capture.get(cv::CAP_PROP_FPS);
189 if (reportedFps > 0.0 && reportedFps + 0.5 >= requestedFps) {
190 return reportedFps;
191 }
192 }
193
194 capture.set(cv::CAP_PROP_FPS, fpsChoices.back());
195 const double reportedFps = capture.get(cv::CAP_PROP_FPS);
196 return (reportedFps > 0.0) ? reportedFps : fpsChoices.back();
197 }
198
199 [[nodiscard]] static std::string resolveModelPath(const std::string &filename, const std::string &assetRoot) {
200 if (filename.empty()) {
201 return {};
202 }
203
204 namespace fs = std::filesystem;
205 const fs::path requested(filename);
206 std::error_code ec{};
207 if (fs::exists(requested, ec)) {
208 return fs::weakly_canonical(requested, ec).string();
209 }
210 ec.clear();
211
212 if (requested.is_absolute()) {
213 return filename;
214 }
215
216 const fs::path assetPath(assetRoot);
217 const fs::path sourceRoot = assetPath.parent_path().parent_path();
218 const std::array<fs::path, 3> candidates = {
219 assetPath / requested,
220 sourceRoot / requested,
221 sourceRoot / "models" / requested.filename(),
222 };
223
224 for (const fs::path &candidate : candidates) {
225 if (fs::exists(candidate, ec)) {
226 return fs::weakly_canonical(candidate, ec).string();
227 }
228 ec.clear();
229 }
230
231 return filename;
232 }
233
234 [[nodiscard]] static float wrapAngle(float angleRadians) {
235 constexpr float TWO_PI = 6.28318530718f;
236 float wrapped = std::fmod(angleRadians, TWO_PI);
237 if (wrapped < 0.0f) {
238 wrapped += TWO_PI;
239 }
240 return wrapped;
241 }
242
243 void updateRotationFromKeyboard(float deltaSeconds) {
244 const bool *keys = SDL_GetKeyboardState(nullptr);
245 if (keys == nullptr) {
246 return;
247 }
248
249 constexpr float MANUAL_SPEED = glm::radians(120.0f);
250 constexpr float AUTO_SPIN_SPEED = 0.55f;
251
252 bool usingArrowKeys = false;
253 if (keys[SDL_SCANCODE_LEFT]) {
254 yawRadians -= MANUAL_SPEED * deltaSeconds;
255 usingArrowKeys = true;
256 }
257 if (keys[SDL_SCANCODE_RIGHT]) {
258 yawRadians += MANUAL_SPEED * deltaSeconds;
259 usingArrowKeys = true;
260 }
261 if (keys[SDL_SCANCODE_UP]) {
262 pitchRadians -= MANUAL_SPEED * deltaSeconds;
263 usingArrowKeys = true;
264 }
265 if (keys[SDL_SCANCODE_DOWN]) {
266 pitchRadians += MANUAL_SPEED * deltaSeconds;
267 usingArrowKeys = true;
268 }
269
270 if (!usingArrowKeys) {
271 autoSpinRadians += AUTO_SPIN_SPEED * deltaSeconds;
272 }
273
274 yawRadians = wrapAngle(yawRadians);
275 pitchRadians = wrapAngle(pitchRadians);
276 autoSpinRadians = wrapAngle(autoSpinRadians);
277 }
278
279 void updateFpsOverlay() {
280 ++fpsFrameCount;
281 const auto now = std::chrono::steady_clock::now();
282 const double elapsed = std::chrono::duration<double>(now - fpsSampleTime).count();
283 if (elapsed >= 0.25) {
284 fps = static_cast<double>(fpsFrameCount) / elapsed;
285 fpsFrameCount = 0;
286 fpsSampleTime = now;
287 fpsText = std::format("FPS: {:.1f}", fps);
288 }
289
290 printText(fpsText, 15, 15, SDL_Color{255, 255, 255, 255});
291 }
292
293 std::string assetRoot{};
294 std::string shaderRoot{};
295 std::string modelPath{};
296 int cameraIndex = 0;
297 int fallbackWidth = 1280;
298 int fallbackHeight = 720;
299 float yawRadians = 0.0f;
300 float pitchRadians = 0.0f;
301 float autoSpinRadians = 0.0f;
302 bool mouseDragging = false;
303 int lastMouseX = 0;
304 int lastMouseY = 0;
305 float mouseYawDegrees = 0.0f;
306 float mousePitchDegrees = 0.0f;
307 float cameraDistance = 4.2f;
308 float mouseSensitivity = 0.35f;
309 double fps = 0.0;
310 uint32_t fpsFrameCount = 0;
311 std::chrono::steady_clock::time_point fpsSampleTime{std::chrono::steady_clock::now()};
312 std::string fpsText = "FPS: --";
313 mxvk::VK_Capture capture{};
314 mxvk::VKAbstractModel model{};
315 std::chrono::steady_clock::time_point lastUpdateTime{std::chrono::steady_clock::now()};
316 std::chrono::steady_clock::time_point startTime{std::chrono::steady_clock::now()};
317 };
318
319} // namespace example
320
321int main(int argc, char **argv) {
322 try {
323 const Arguments args = proc_args(argc, argv);
324 example::OpenCVModelWindow window(args, "OpenCV Model Example");
325 window.loop();
326 } catch (mxvk::Exception &e) {
327 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
328 return EXIT_FAILURE;
329 } catch (ArgException<std::string> &e) {
330 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
331 return EXIT_FAILURE;
332 }
333 return EXIT_SUCCESS;
334}
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
void proc() override
Execute one processing/update step.
Definition main.cpp:133
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition main.cpp:157
OpenCVModelWindow(const Arguments &args, const std::string &title)
Definition main.cpp:33
void event(SDL_Event &e) override
Handle one SDL event.
Definition main.cpp:88
~OpenCVModelWindow() override
Definition main.cpp:79
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition main.cpp:129
std::string text() const
double get(unsigned int option)
Query a VideoCapture property.
Definition mxvk_cv.cpp:485
void set(unsigned int option, double value)
Set a VideoCapture property.
Definition mxvk_cv.cpp:481
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 exit()
Request loop termination.
Definition mxvk.cpp:1126
VK_Window()=default
Construct an empty window object.
void release()
Release Vulkan and SDL resources.
Definition mxvk.cpp:218
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
int main(void)
Definition main.cpp:7
#define opencv_model_ASSET_DIR
Definition main.cpp:22
#define MXVK_VALIDATION
Definition mxvk.hpp:27
High-level model wrapper integrated with MXVK dynamic rendering.
OpenCV video-capture integration for the Vulkan backend.
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
std::string filename
Optional input filename (--filename).
Definition argz.hpp:738
Default transform UBO payload for model shaders.