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