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 <cstdlib>
2
3#include <algorithm>
4#include <chrono>
5#include <cmath>
6#include <cstring>
7#include <format>
8#include <iostream>
9#include <memory>
10#include <string>
11#include <vector>
12
13#include <SDL3/SDL.h>
14
15#include <glm/ext/matrix_clip_space.hpp>
16#include <glm/ext/matrix_transform.hpp>
17#include <glm/glm.hpp>
18
19#include "mxvk/argz.hpp"
20#include "mxvk/mxvk.hpp"
23
24#include "rain.hpp"
25
26namespace example {
27
29 public:
30 ModelWindow(const std::string filename,
31 bool usingDefaultModel,
32 const std::string &path,
33 const std::string &resource,
34 const std::string &resource_path,
35 const std::string &fragmentShaderPath,
36 const std::string &texture_path,
37 const std::string &title,
38 int width,
39 int height,
40 bool fullscreen,
41 bool enable_vsync,
42 bool binaryTextureMode,
43 int fontSize,
44 const std::string &fontPath,
45 const std::string &color)
46 : mxvk::VK_Window(title, width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
47 assetRoot(path.empty() ? std::string(MODEL_EXAMPLE_ASSET_DIR) : path),
48 binaryTextureMode(binaryTextureMode) {
49 const std::string modelPath = filename;
50 const std::string textureManifestPath = resource.empty() && usingDefaultModel ? assetRoot + "/data/texture_manifest.txt" : resource;
51 const bool useDefaultTextureBase = resource_path.empty() && (usingDefaultModel || !resource.empty());
52 const std::string textureBasePath = resource_path.empty() ? (useDefaultTextureBase ? assetRoot + "/data" : "") : resource_path;
53 const std::string vertPath = assetRoot + "/data/model.vert.spv";
54 const std::string fragPath = fragmentShaderPath.empty() ? (assetRoot + "/data/model.frag.spv") : fragmentShaderPath;
55 if (!texture_path.empty())
56 background = createSprite(texture_path, "", "");
57 model.load(this, modelPath, textureManifestPath, textureBasePath, 1.0f);
58 model.setShaders(this, vertPath, fragPath);
59 model.setBackfaceCulling(!binaryTextureMode);
60 if (binaryTextureMode) {
61 matrix::RainConfig rainConfig = matrix::make_matrix_rain_config(assetRoot, false);
62 rainConfig.surface_width = 2048;
63 rainConfig.surface_height = 2048;
64 rainConfig.font_size = std::max(1, fontSize);
65 if (!fontPath.empty()) {
66 rainConfig.font_path = fontPath;
67 }
68 rainConfig.color = color;
69 binaryMatrixTexture = std::make_unique<matrix::Rain>(std::move(rainConfig));
70 }
71 }
72
73 ~ModelWindow() override {
74 if (device != VK_NULL_HANDLE) {
75 vkDeviceWaitIdle(device);
76 }
77 model.cleanup(this);
78 }
79
80 void event(SDL_Event &e) override {
81 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
82 exit();
83 return;
84 }
85
86 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
87 if (e.key.repeat) {
88 return;
89 }
90 autoSpinEnabled = !autoSpinEnabled;
91 return;
92 }
93
94 if (e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER)) {
95 if (e.key.repeat) {
96 return;
97 }
98 skyboxMode = !skyboxMode;
99 model.setBackfaceCulling(!(binaryTextureMode || skyboxMode));
100 if (skyboxMode) {
101 resetSkyboxCamera();
102 }
103 return;
104 }
105
106 if (e.type == SDL_EVENT_KEY_DOWN || e.type == SDL_EVENT_KEY_UP) {
107 const bool pressed = (e.type == SDL_EVENT_KEY_DOWN);
108 if (e.key.key == SDLK_W) {
109 skyboxLookUpKey = pressed;
110 } else if (e.key.key == SDLK_S) {
111 skyboxLookDownKey = pressed;
112 } else if (e.key.key == SDLK_A) {
113 skyboxLookLeftKey = pressed;
114 } else if (e.key.key == SDLK_D) {
115 skyboxLookRightKey = pressed;
116 } else if (e.key.key == SDLK_UP) {
117 zoomInKey = pressed;
118 } else if (e.key.key == SDLK_DOWN) {
119 zoomOutKey = pressed;
120 }
121 }
122
123 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
124 mouseDragging = true;
125 lastMouseX = static_cast<int>(e.button.x);
126 lastMouseY = static_cast<int>(e.button.y);
127 return;
128 }
129
130 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
131 mouseDragging = false;
132 return;
133 }
134
135 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseDragging) {
136 const int x = static_cast<int>(e.motion.x);
137 const int y = static_cast<int>(e.motion.y);
138 const int deltaX = x - lastMouseX;
139 const int deltaY = y - lastMouseY;
140
141 if (skyboxMode) {
142 skyboxYawDegrees += static_cast<float>(deltaX) * mouseSensitivity;
143 skyboxPitchDegrees += static_cast<float>(deltaY) * mouseSensitivity;
144 skyboxPitchDegrees = std::fmod(skyboxPitchDegrees, 360.0f);
145 } else {
146 yawDegrees += static_cast<float>(deltaX) * mouseSensitivity;
147 pitchDegrees += static_cast<float>(deltaY) * mouseSensitivity;
148 pitchDegrees = std::clamp(pitchDegrees, -80.0f, 80.0f);
149 }
150
151 lastMouseX = x;
152 lastMouseY = y;
153 return;
154 }
155
156 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
157 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y : static_cast<float>(e.wheel.integer_y);
158 if (skyboxMode) {
159 const float zoomSpeed = 0.45f;
160 skyboxCameraPosition += skyboxForwardVector() * (delta * zoomSpeed);
161
162 const float maxSkyboxDistance = 12.0f;
163 const float skyboxDistance = glm::length(skyboxCameraPosition);
164 if (skyboxDistance > maxSkyboxDistance) {
165 skyboxCameraPosition = glm::normalize(skyboxCameraPosition) * maxSkyboxDistance;
166 }
167 } else {
168 adjustZoomTarget(delta);
169 }
170 return;
171 }
172 }
173
174 void onSwapchainRecreated() override {
175 model.resize(this);
176 }
177
178 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
179 const auto now = std::chrono::steady_clock::now();
180 const float deltaSeconds = std::chrono::duration<float>(now - lastFrameTime).count();
181 const float elapsedSeconds = std::chrono::duration<float>(now - startTime).count();
182 lastFrameTime = now;
183 if (autoSpinEnabled) {
184 autoSpinRadians += deltaSeconds * autoSpinSpeed;
185 }
186
187 if (skyboxMode) {
188 updateSkyboxCamera(deltaSeconds);
189 } else {
190 updateZoom(deltaSeconds);
191 }
192
193 const VkExtent2D extent = getSwapchainExtent();
194
195 const float aspect = (extent.height > 0U)
196 ? static_cast<float>(extent.width) / static_cast<float>(extent.height)
197 : 1.0f;
198
200 if (skyboxMode) {
201 const glm::vec3 front = skyboxForwardVector();
202 const glm::vec3 target = skyboxCameraPosition + front;
203 ubo.model = glm::scale(glm::mat4(1.0f), glm::vec3(model.modelRenderScale() * skyboxScaleMultiplier));
204 ubo.model = glm::translate(ubo.model, model.modelCenterOffset());
205 ubo.view = glm::lookAt(skyboxCameraPosition, target, skyboxUpVector());
206 ubo.proj = glm::perspective(glm::radians(70.0f), aspect, 0.02f, 100.0f);
207 } else {
208 ubo.model = glm::rotate(glm::mat4(1.0f), glm::radians(pitchDegrees), glm::vec3(1.0f, 0.0f, 0.0f));
209 ubo.model = glm::rotate(ubo.model, glm::radians(yawDegrees) + autoSpinRadians, glm::vec3(0.0f, 1.0f, 0.0f));
210 ubo.model = glm::scale(ubo.model, glm::vec3(model.modelRenderScale()));
211 ubo.model = glm::translate(ubo.model, model.modelCenterOffset());
212 ubo.view = glm::lookAt(glm::vec3(0.0f, 0.0f, cameraDistance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
213 ubo.proj = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 100.0f);
214 }
215 ubo.proj[1][1] *= -1.0f;
216 ubo.fx = glm::vec4(elapsedSeconds, 0.0f, 0.0f, 0.37f);
217
218 if (background) {
219 background->drawSpriteRect(0, 0, static_cast<int>(swapchain_extent.width), static_cast<int>(swapchain_extent.height));
220 renderStandaloneSprite(*background, cmd);
221 background->clearQueue();
222 }
223
224 model.updateUBO(imageIndex, ubo);
225 if (binaryTextureMode && binaryMatrixTexture != nullptr) {
226 binaryMatrixTexture->update(deltaSeconds);
227 const int textureWidth = binaryMatrixTexture->width();
228 const int textureHeight = binaryMatrixTexture->height();
229 const int texturePitch = binaryMatrixTexture->pitch();
230 const void *texturePixels = flippedMatrixTexturePixels(textureWidth, textureHeight, texturePitch);
231 [[maybe_unused]] const bool textureUpdated = model.updatePrimaryTexture(
232 texturePixels, textureWidth, textureHeight, textureWidth * 4);
233 }
234 model.render(cmd, imageIndex, false);
235 }
236
237 private:
238 std::string assetRoot;
239 mxvk::VKAbstractModel model{};
240 mxvk::VK_Sprite *background = nullptr;
241 bool binaryTextureMode = false;
242 std::unique_ptr<matrix::Rain> binaryMatrixTexture{};
243 bool mouseDragging = false;
244 bool skyboxMode = false;
245 bool skyboxLookUpKey = false;
246 bool skyboxLookDownKey = false;
247 bool skyboxLookLeftKey = false;
248 bool skyboxLookRightKey = false;
249 bool autoSpinEnabled = true;
250 int lastMouseX = 0;
251 int lastMouseY = 0;
252 float yawDegrees = 0.0f;
253 float pitchDegrees = 12.0f;
254 float cameraDistance = 4.2f;
255 float targetCameraDistance = 4.2f;
256 glm::vec3 skyboxCameraPosition{0.0f, 0.0f, 0.0f};
257 float skyboxYawDegrees = 180.0f;
258 float skyboxPitchDegrees = 0.0f;
259 float skyboxScaleMultiplier = 1.6f;
260 float mouseSensitivity = 0.35f;
261 float autoSpinSpeed = 0.65f;
262 float autoSpinRadians = 0.0f;
263 std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
264 std::chrono::steady_clock::time_point lastFrameTime = std::chrono::steady_clock::now();
265 std::vector<Uint8> flippedMatrixTexture;
266 bool zoomInKey = false;
267 bool zoomOutKey = false;
268
269 void adjustZoomTarget(float wheelDelta) {
270 if (wheelDelta == 0.0f) {
271 return;
272 }
273
274 constexpr float zoomStep = 1.12f;
275 targetCameraDistance *= std::pow(zoomStep, -wheelDelta);
276 targetCameraDistance = std::clamp(targetCameraDistance, 1.8f, 12.0f);
277 }
278
279 void updateZoom(float deltaSeconds) {
280 const float keyDelta = (zoomInKey ? 1.0f : 0.0f) - (zoomOutKey ? 1.0f : 0.0f);
281 if (keyDelta != 0.0f) {
282 constexpr float keyZoomStep = 1.09f;
283 targetCameraDistance *= std::pow(keyZoomStep, -keyDelta * deltaSeconds * 60.0f);
284 targetCameraDistance = std::clamp(targetCameraDistance, 1.8f, 12.0f);
285 }
286
287 constexpr float zoomResponsiveness = 14.0f;
288 const float smoothing = 1.0f - std::exp(-zoomResponsiveness * deltaSeconds);
289 cameraDistance += (targetCameraDistance - cameraDistance) * smoothing;
290 }
291
292 [[nodiscard]] const void *flippedMatrixTexturePixels(int width, int height, int pitch) {
293 const auto *pixels = static_cast<const Uint8 *>(binaryMatrixTexture != nullptr ? binaryMatrixTexture->pixels() : nullptr);
294 if (pixels == nullptr || width <= 0 || height <= 0 || pitch <= 0) {
295 return nullptr;
296 }
297
298 const int rowBytes = width * 4;
299 flippedMatrixTexture.resize(static_cast<std::size_t>(rowBytes * height));
300 for (int y = 0; y < height; ++y) {
301 const Uint8 *src = pixels + static_cast<std::size_t>(height - 1 - y) * static_cast<std::size_t>(pitch);
302 Uint8 *dst = flippedMatrixTexture.data() + static_cast<std::size_t>(y) * static_cast<std::size_t>(rowBytes);
303 std::memcpy(dst, src, static_cast<std::size_t>(rowBytes));
304 }
305 return flippedMatrixTexture.data();
306 }
307
308 void resetSkyboxCamera() {
309 skyboxCameraPosition = glm::vec3(0.0f);
310 skyboxYawDegrees = 180.0f;
311 skyboxPitchDegrees = 0.0f;
312 }
313
314 [[nodiscard]] glm::vec3 skyboxUpVector() const {
315 const float yawRadians = glm::radians(skyboxYawDegrees);
316 const float upPitchRadians = glm::radians(skyboxPitchDegrees + 90.0f);
317 return glm::normalize(glm::vec3{
318 std::sin(yawRadians) * std::cos(upPitchRadians),
319 std::sin(upPitchRadians),
320 -std::cos(yawRadians) * std::cos(upPitchRadians)});
321 }
322
323 [[nodiscard]] glm::vec3 skyboxForwardVector() const {
324 const float yawRadians = glm::radians(skyboxYawDegrees);
325 const float pitchRadians = glm::radians(skyboxPitchDegrees);
326 return glm::normalize(glm::vec3{
327 std::sin(yawRadians) * std::cos(pitchRadians),
328 std::sin(pitchRadians),
329 -std::cos(yawRadians) * std::cos(pitchRadians)});
330 }
331
332 [[nodiscard]] glm::vec3 skyboxRightVector() const {
333 return glm::normalize(glm::cross(skyboxForwardVector(), skyboxUpVector()));
334 }
335
336 void updateSkyboxCamera(float deltaSeconds) {
337 const float lookSpeed = 85.0f;
338 if (skyboxLookLeftKey) {
339 skyboxYawDegrees -= lookSpeed * deltaSeconds;
340 }
341 if (skyboxLookRightKey) {
342 skyboxYawDegrees += lookSpeed * deltaSeconds;
343 }
344 if (skyboxLookUpKey) {
345 skyboxPitchDegrees += lookSpeed * deltaSeconds;
346 }
347 if (skyboxLookDownKey) {
348 skyboxPitchDegrees -= lookSpeed * deltaSeconds;
349 }
350 skyboxPitchDegrees = std::fmod(skyboxPitchDegrees, 360.0f);
351 if (skyboxPitchDegrees < 0.0f) {
352 skyboxPitchDegrees += 360.0f;
353 }
354
355 const float keyZoomDelta = (zoomInKey ? 1.0f : 0.0f) - (zoomOutKey ? 1.0f : 0.0f);
356 if (keyZoomDelta != 0.0f) {
357 constexpr float zoomSpeed = 2.25f;
358 skyboxCameraPosition += skyboxForwardVector() * (keyZoomDelta * zoomSpeed * deltaSeconds);
359 }
360 }
361 };
362
363} // namespace example
364
365int main(int argc, char **argv) {
366 try {
367 const Arguments args = proc_args(argc, argv);
368 const bool usingDefaultModel = args.filename.empty();
369 std::string filename = args.filename;
370 if (args.filename.empty()) {
371 filename = args.path + "/data/pyramid.obj";
372 }
374 filename,
375 usingDefaultModel,
376 args.path,
377 args.resource,
378 args.resource_path,
379 args.fragmentPath,
380 args.texture,
381 "MXVK Model Example",
382 args.width,
383 args.height,
384 args.fullscreen,
385 args.enable_vsync,
386 args.binary,
387 args.font_size,
388 args.font_path,
389 args.color);
390 window.loop();
391 } catch (mxvk::Exception &e) {
392 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
393 return EXIT_FAILURE;
394 } catch (ArgException<std::string> &e) {
395 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
396 return EXIT_FAILURE;
397 }
398
399 return EXIT_SUCCESS;
400}
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
ModelWindow(const std::string filename, bool usingDefaultModel, const std::string &path, const std::string &resource, const std::string &resource_path, const std::string &fragmentShaderPath, const std::string &texture_path, const std::string &title, int width, int height, bool fullscreen, bool enable_vsync, bool binaryTextureMode, int fontSize, const std::string &fontPath, const std::string &color)
Definition main.cpp:30
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition main.cpp:178
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition main.cpp:174
void event(SDL_Event &e) override
Handle one SDL event.
Definition main.cpp:80
~ModelWindow() override
Definition main.cpp:73
std::string text() const
Convenience wrapper that owns mesh, textures, descriptors, and pipeline state.
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
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:3477
VkExtent2D swapchain_extent
Definition mxvk.hpp:495
void renderStandaloneSprite(VK_Sprite &sprite, VkCommandBuffer cmd)
Render one standalone sprite using the window's shared sprite pipeline.
Definition mxvk.cpp:1142
void exit()
Request loop termination.
Definition mxvk.cpp:1126
VK_Window()=default
Construct an empty window object.
int main(void)
Definition main.cpp:7
#define MXVK_VALIDATION
Definition mxvk.hpp:27
High-level model wrapper integrated with MXVK dynamic rendering.
RainConfig make_matrix_rain_config(const std::string &asset_root, bool binary_glyph_mode)
Definition rain.cpp:157
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 font_path
Optional font file path (--font-path).
Definition argz.hpp:762
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:736
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:750
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 color
Optional rain RGB tint (--color).
Definition argz.hpp:763
std::string resource_path
Resource path.
Definition argz.hpp:771
std::string filename
Optional input filename (--filename).
Definition argz.hpp:738
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:735
std::string fragmentPath
Optional fragment shader SPV path (--fragment).
Definition argz.hpp:766
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732
std::string resource
Resource file.
Definition argz.hpp:770
int font_size
Matrix rain font size in pixels (--font-size).
Definition argz.hpp:761
bool binary
Use binary glyphs only (--binary).
Definition argz.hpp:748
std::string color
Definition rain.hpp:19
std::string font_path
Definition rain.hpp:18
Default transform UBO payload for model shaders.