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