MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
example.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
4#include "mxvk/mxvk_png.hpp"
5
6#include <algorithm>
7#include <array>
8#include <chrono>
9#include <cmath>
10#include <cstdint>
11#include <cstdlib>
12#include <format>
13#include <iostream>
14#include <memory>
15#include <random>
16#include <string>
17#include <vector>
18
19#include <glm/ext/matrix_clip_space.hpp>
20#include <glm/ext/matrix_transform.hpp>
21#include <glm/glm.hpp>
22
23namespace example {
24 namespace {
25 constexpr float k_pi = 3.14159265358979323846f;
26
27 SDL_Surface *load_transparent_saucer(const std::string &png_path) {
28 SDL_Surface *loaded_surface = mxvk::LoadPNG(png_path.c_str());
29 if (loaded_surface == nullptr) {
30 throw mxvk::Exception("Failed to load sprite3D image: " + png_path);
31 }
32
33 SDL_Surface *rgba_surface = SDL_ConvertSurface(loaded_surface, SDL_PIXELFORMAT_RGBA32);
34 SDL_DestroySurface(loaded_surface);
35 if (rgba_surface == nullptr) {
36 throw mxvk::Exception(std::format("Failed to convert sprite3D image to RGBA: {}", png_path));
37 }
38
39 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(rgba_surface->format);
40 if (format_details == nullptr) {
41 SDL_DestroySurface(rgba_surface);
42 throw mxvk::Exception("Failed to query pixel format details for: " + png_path);
43 }
44
45 if (!SDL_LockSurface(rgba_surface)) {
46 SDL_DestroySurface(rgba_surface);
47 throw mxvk::Exception("Failed to lock sprite3D surface: " + png_path);
48 }
49
50 auto *pixels = static_cast<std::uint32_t *>(rgba_surface->pixels);
51 const int pixel_count = rgba_surface->w * rgba_surface->h;
52 for (int i = 0; i < pixel_count; ++i) {
53 std::uint8_t r = 0;
54 std::uint8_t g = 0;
55 std::uint8_t b = 0;
56 std::uint8_t a = 0;
57 SDL_GetRGBA(pixels[i], format_details, nullptr, &r, &g, &b, &a);
58
59 if ((r | g | b) == 0) {
60 a = 0;
61 } else {
62 a = 255;
63 }
64
65 pixels[i] = SDL_MapRGBA(format_details, nullptr, r, g, b, a);
66 }
67
68 SDL_UnlockSurface(rgba_surface);
69 return rgba_surface;
70 }
71
72 SDL_Surface *create_star_surface() {
73 SDL_Surface *surface = SDL_CreateSurface(40, 40, SDL_PIXELFORMAT_RGBA32);
74 if (surface == nullptr) {
75 return nullptr;
76 }
77
78 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(surface->format);
79 if (format_details == nullptr) {
80 SDL_DestroySurface(surface);
81 return nullptr;
82 }
83
84 if (!SDL_LockSurface(surface)) {
85 SDL_DestroySurface(surface);
86 return nullptr;
87 }
88
89 auto *pixels = static_cast<std::uint32_t *>(surface->pixels);
90 for (int y = 0; y < surface->h; ++y) {
91 for (int x = 0; x < surface->w; ++x) {
92 const float dx = (static_cast<float>(x) + 0.5f) - (static_cast<float>(surface->w) * 0.5f);
93 const float dy = (static_cast<float>(y) + 0.5f) - (static_cast<float>(surface->h) * 0.5f);
94 const float dist = std::sqrt((dx * dx) + (dy * dy)) / (static_cast<float>(surface->w) * 0.5f);
95 const float core = std::clamp(1.0f - dist * 1.9f, 0.0f, 1.0f);
96 const float glow = std::clamp(1.0f - dist * 3.8f, 0.0f, 1.0f);
97 const std::uint8_t alpha = static_cast<std::uint8_t>(std::lround((core * core * 255.0f) + (glow * 80.0f)));
98 const std::uint8_t brightness = static_cast<std::uint8_t>(std::lround((core * 255.0f) + (glow * 150.0f)));
99 pixels[y * surface->w + x] = SDL_MapRGBA(format_details, nullptr, brightness, brightness, brightness, alpha);
100 }
101 }
102
103 SDL_UnlockSurface(surface);
104 return surface;
105 }
106
120
122 float x = 0.0f;
123 float y = 0.0f;
124 float z = 0.0f;
125 float vx = 0.0f;
126 float vy = 0.0f;
127 float vz = 0.0f;
128 float magnitude = 0.0f;
129 float temperature = 0.0f;
130 float twinkle = 0.0f;
131 float size = 0.0f;
132 bool is_constellation = false;
133 };
134
135 float random_float(float min_value, float max_value) {
136 static thread_local std::default_random_engine engine{std::random_device{}()};
137 std::uniform_real_distribution<float> dist(min_value, max_value);
138 return dist(engine);
139 }
140 } // namespace
141
142 class ExampleWindow : public mxvk::VK_Window {
143 public:
144 ExampleWindow(const std::string &path, const std::string &text, int width, int height, bool fullscreen, bool enable_vsync)
145 : mxvk::VK_Window(text, width, height, fullscreen, MXVK_VALIDATION, enable_vsync) {
146 current_path = path.empty() ? std::string(sprite3d_example_ASSET_DIR) : path;
147 if (current_path == ".") {
148 current_path = sprite3d_example_ASSET_DIR;
149 }
150
151 setClearColor(0.02f, 0.03f, 0.06f, 1.0f);
152
153 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> star_surface(create_star_surface(), SDL_DestroySurface);
154 if (star_surface == nullptr) {
155 throw mxvk::Exception("Failed to create starfield sprite texture");
156 }
157 stars_sprite = createSprite3D(star_surface.get());
158 if (stars_sprite == nullptr) {
159 throw mxvk::Exception("Failed to create starfield sprite batch");
160 }
161 stars_sprite->setDepthTestEnabled(true);
162 stars_sprite->setDepthWriteEnabled(false);
163 stars_sprite->setAlphaDiscardThreshold(0.01f);
164
165 const std::string image_path = current_path + "/data/saucer.png";
166 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> saucer_surface(load_transparent_saucer(image_path), SDL_DestroySurface);
167 sprite = createSprite3D(saucer_surface.get());
168
169 if (sprite == nullptr) {
170 throw mxvk::Exception("Failed to create sprite3D example sprite");
171 }
172
173 sprite->setDepthTestEnabled(true);
174 sprite->setDepthWriteEnabled(false);
175 sprite->setAlphaDiscardThreshold(0.01f);
176
177 initStars(25000);
178 initSaucers();
179 }
180
181 void event(SDL_Event &e) override {
182 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
183 mouse_dragging = true;
184 last_mouse_x = static_cast<int>(e.button.x);
185 last_mouse_y = static_cast<int>(e.button.y);
186 }
187 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
188 mouse_dragging = false;
189 }
190 if (e.type == SDL_EVENT_MOUSE_MOTION && mouse_dragging) {
191 const int mouse_x = static_cast<int>(e.motion.x);
192 const int mouse_y = static_cast<int>(e.motion.y);
193 const int delta_x = mouse_x - last_mouse_x;
194 const int delta_y = mouse_y - last_mouse_y;
195
196 yaw_degrees += static_cast<float>(delta_x) * mouse_sensitivity;
197 pitch_degrees -= static_cast<float>(delta_y) * mouse_sensitivity;
198 pitch_degrees = std::clamp(pitch_degrees, -85.0f, 85.0f);
199
200 last_mouse_x = mouse_x;
201 last_mouse_y = mouse_y;
202 }
203 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
204 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y : static_cast<float>(e.wheel.integer_y);
205 applyWheelZoom(delta);
206 }
207 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
208 exit();
209 }
210 }
211
212 void proc() override {}
213
214 void onSwapchainRecreated() override {
215 if (stars_sprite != nullptr) {
216 stars_sprite->resize(this);
217 }
218 if (sprite != nullptr) {
219 sprite->resize(this);
220 }
221 }
222
223 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
224 if (sprite == nullptr) {
225 return;
226 }
227
228 const auto now = std::chrono::steady_clock::now();
229 const float elapsed_seconds = std::chrono::duration<float>(now - start_time).count();
230 const VkExtent2D extent = getSwapchainExtent();
231 const float aspect = (extent.height > 0U)
232 ? static_cast<float>(extent.width) / static_cast<float>(extent.height)
233 : 1.0f;
234
235 const float yaw_radians = glm::radians(yaw_degrees);
236 const float pitch_radians = glm::radians(pitch_degrees);
237 const glm::vec3 camera_position(
238 camera_distance * std::cos(pitch_radians) * std::sin(yaw_radians),
239 camera_distance * std::sin(pitch_radians),
240 camera_distance * std::cos(pitch_radians) * std::cos(yaw_radians));
241 glm::mat4 view = glm::lookAt(camera_position, glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
242 glm::mat4 proj = glm::perspective(glm::radians(48.0f), aspect, 0.1f, 100.0f);
243 proj[1][1] *= -1.0f;
244
245 updateStars(elapsed_seconds);
246
247 if (stars_sprite != nullptr) {
248 stars_sprite->updateCamera(imageIndex, view, proj);
249 for (const StarParticle &star : stars) {
250 const float twinkle = 0.78f + 0.22f * std::sin(elapsed_seconds * star.twinkle);
251 const float star_size = star.size * twinkle * (star.is_constellation ? 1.25f : 1.0f);
252 const float alpha = std::clamp(((6.5f - star.magnitude) / 6.5f) * twinkle, 0.0f, 1.0f);
253 const glm::vec3 color = starColor(star.temperature);
254 stars_sprite->drawSprite(glm::vec3(star.x, star.y, star.z),
255 glm::vec2(star_size, star_size),
256 glm::vec4(color, alpha));
257 }
258 stars_sprite->render(cmd, imageIndex);
259 stars_sprite->clearQueue();
260 }
261
262 sprite->updateCamera(imageIndex, view, proj);
263
264 for (const SaucerFlight &saucer : saucers) {
265 const float orbit_angle = saucer.orbit_phase + elapsed_seconds * saucer.orbit_speed;
266 const float pulse = 1.0f + std::sin(elapsed_seconds * saucer.pulse_speed + saucer.orbit_phase) * saucer.pulse_amount;
267 const float bob = std::sin(elapsed_seconds * saucer.bob_speed + saucer.orbit_phase) * saucer.bob_amplitude;
268 const float roll = orbit_angle * 0.85f + elapsed_seconds * saucer.roll_speed;
269
270 const glm::vec3 position(
271 saucer.orbit_center.x + std::cos(orbit_angle) * saucer.orbit_radius,
272 saucer.orbit_center.y + bob,
273 saucer.orbit_center.z + std::sin(orbit_angle) * saucer.orbit_radius * 0.62f);
274
275 const glm::vec2 size(saucer.base_size * pulse, saucer.base_size * pulse * 0.72f);
276 sprite->drawSprite(position, size, saucer.tint, roll);
277 }
278
279 sprite->render(cmd, imageIndex);
280 sprite->clearQueue();
281 }
282
283 private:
284 void initSaucers() {
285 static const std::array<SaucerFlight, 5> base_saucers{{
286 {{0.0f, 0.0f, 0.0f}, 1.8f, 1.0f, 0.0f, 0.24f, 2.2f, 0.50f, 0.22f, 2.0f, 0.5f, {1.0f, 1.0f, 1.0f, 1.0f}},
287 {{0.0f, 0.18f, 0.0f}, 2.35f, 0.82f, 1.3f, 0.30f, 1.5f, 0.42f, 0.18f, 1.6f, -0.7f, {1.0f, 0.95f, 0.88f, 1.0f}},
288 {{0.0f, -0.16f, 0.0f}, 2.8f, 0.67f, 2.6f, 0.34f, 1.9f, 0.35f, 0.24f, 1.2f, 0.85f, {0.88f, 0.96f, 1.0f, 1.0f}},
289 {{0.0f, 0.08f, 0.0f}, 3.25f, 0.54f, 4.0f, 0.28f, 1.2f, 0.30f, 0.30f, 0.9f, 0.55f, {1.0f, 0.90f, 0.96f, 1.0f}},
290 {{0.0f, 0.0f, 0.0f}, 3.8f, 0.43f, 5.2f, 0.40f, 1.0f, 0.27f, 0.34f, 0.7f, 0.45f, {0.92f, 1.0f, 0.92f, 1.0f}},
291 }};
292
293 saucers.reserve(base_saucers.size() * 4U);
294 for (int ring = 0; ring < 4; ++ring) {
295 const float ring_offset = static_cast<float>(ring) * 0.28f;
296 const float ring_speed_scale = 1.0f + static_cast<float>(ring) * 0.06f;
297 const float ring_phase_offset = static_cast<float>(ring) * 0.9f;
298 for (std::size_t i = 0; i < base_saucers.size(); ++i) {
299 SaucerFlight flight = base_saucers[i];
300 flight.orbit_center.x += (static_cast<float>(i) - 2.0f) * 0.12f;
301 flight.orbit_center.y += (static_cast<float>(ring) - 1.5f) * 0.06f;
302 flight.orbit_radius += ring_offset + static_cast<float>(i) * 0.05f;
303 flight.orbit_speed *= ring_speed_scale * 1.35f;
304 flight.orbit_phase += ring_phase_offset + static_cast<float>(i) * 0.4f;
305 flight.bob_amplitude *= 1.0f + static_cast<float>(ring) * 0.08f;
306 flight.base_size *= 1.0f + static_cast<float>(ring) * 0.05f;
307 flight.pulse_amount *= 1.0f + static_cast<float>(ring) * 0.03f;
308 flight.pulse_speed *= (1.0f + static_cast<float>(ring) * 0.05f) * 1.15f;
309 flight.roll_speed *= 1.25f;
310 saucers.push_back(flight);
311 }
312 }
313 }
314
315 void initStars(int count) {
316 stars.reserve(static_cast<std::size_t>(count));
317 for (int i = 0; i < count; ++i) {
318 stars.push_back(makeStar());
319 }
320 }
321
322 void updateStars([[maybe_unused]] float elapsed_seconds) {
323 constexpr float max_radius = 120.0f;
324 constexpr float min_radius = 20.0f;
325 constexpr float dt = 1.0f / 60.0f;
326
327 for (StarParticle &star : stars) {
328 star.x += star.vx * dt;
329 star.y += star.vy * dt;
330 star.z += star.vz * dt;
331
332 const float radius_squared = (star.x * star.x) + (star.y * star.y) + (star.z * star.z);
333 if (radius_squared < (min_radius * min_radius) || radius_squared > (max_radius * max_radius)) {
334 star = makeStar();
335 }
336 }
337 }
338
339 StarParticle makeStar() const {
340 StarParticle star{};
341 constexpr float max_radius = 120.0f;
342 constexpr float min_radius = 20.0f;
343
344 const float theta = random_float(0.0f, 2.0f * k_pi);
345 const float phi = std::acos(random_float(-1.0f, 1.0f));
346 const float radius = random_float(min_radius, max_radius);
347
348 star.x = radius * std::sin(phi) * std::cos(theta);
349 star.y = radius * std::sin(phi) * std::sin(theta);
350 star.z = radius * std::cos(phi);
351
352 star.vx = random_float(-0.04f, 0.04f);
353 star.vy = random_float(-0.04f, 0.04f);
354 star.vz = random_float(-0.04f, 0.04f);
355
356 const float rarity = random_float(0.0f, 1.0f);
357 if (rarity < 0.05f) {
358 star.magnitude = random_float(-1.0f, 2.0f);
359 } else if (rarity < 0.3f) {
360 star.magnitude = random_float(2.0f, 4.0f);
361 } else {
362 star.magnitude = random_float(4.0f, 6.5f);
363 }
364
365 if (star.magnitude < 3.0f) {
366 star.temperature = random_float(4000.0f, 8000.0f);
367 } else {
368 star.temperature = random_float(2500.0f, 6000.0f);
369 }
370
371 star.twinkle = random_float(0.5f, 3.0f);
372 star.size = std::clamp(0.52f - star.magnitude * 0.045f, 0.10f, 0.45f);
373 star.is_constellation = (star.magnitude < 3.0f) && (random_float(0.0f, 1.0f) < 0.3f);
374 return star;
375 }
376
377 static glm::vec3 starColor(float temperature) {
378 float r = 1.0f;
379 float g = 1.0f;
380 float b = 1.0f;
381
382 if (temperature < 3700.0f) {
383 r = 1.0f;
384 g = temperature / 3700.0f * 0.6f;
385 b = 0.0f;
386 } else if (temperature < 5200.0f) {
387 r = 1.0f;
388 g = 0.6f + (temperature - 3700.0f) / 1500.0f * 0.4f;
389 b = (temperature - 3700.0f) / 1500.0f * 0.3f;
390 } else if (temperature < 6000.0f) {
391 r = 1.0f;
392 g = 1.0f;
393 b = (temperature - 5200.0f) / 800.0f * 0.7f;
394 } else if (temperature < 7500.0f) {
395 r = 1.0f;
396 g = 1.0f;
397 b = 0.7f + (temperature - 6000.0f) / 1500.0f * 0.3f;
398 } else {
399 r = 0.7f - (temperature - 7500.0f) / 10000.0f * 0.4f;
400 g = 0.8f + (temperature - 7500.0f) / 10000.0f * 0.2f;
401 b = 1.0f;
402 }
403
404 return glm::vec3(r, g, b);
405 }
406
407 void applyWheelZoom(float wheel_y) {
408 if (wheel_y == 0.0f) {
409 return;
410 }
411
412 const float zoom_step = 1.15f;
413 const float zoom_factor = std::pow(zoom_step, -wheel_y);
414 camera_distance = std::clamp(camera_distance * zoom_factor, min_camera_distance, max_camera_distance);
415 }
416
417 std::string current_path = ".";
418 mxvk::VK_Sprite3D *stars_sprite = nullptr;
419 mxvk::VK_Sprite3D *sprite = nullptr;
420 std::vector<StarParticle> stars;
421 std::vector<SaucerFlight> saucers;
422 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
423 float camera_distance = 10.0f;
424 float min_camera_distance = 3.0f;
425 float max_camera_distance = 20.0f;
426 bool mouse_dragging = false;
427 int last_mouse_x = 0;
428 int last_mouse_y = 0;
429 float yaw_degrees = 0.0f;
430 float pitch_degrees = 10.0f;
431 float mouse_sensitivity = 0.20f;
432 };
433} // namespace example
434
435int main(int argc, char **argv) {
436 try {
437 const Arguments args = proc_args(argc, argv);
438 example::ExampleWindow window(args.path, "VK_Sprite3D Example", args.width, args.height, args.fullscreen, args.enable_vsync);
439 window.loop();
440 } catch (mxvk::Exception &e) {
441 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
442 return EXIT_FAILURE;
443 } catch (ArgException<std::string> &e) {
444 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
445 return EXIT_FAILURE;
446 }
447
448 return EXIT_SUCCESS;
449}
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
ExampleWindow(const std::string &path, const std::string &text, int width, int height, bool fullscreen, bool enable_vsync)
Definition example.cpp:144
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition example.cpp:223
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition example.cpp:214
void event(SDL_Event &e) override
Handle one SDL event.
Definition example.cpp:181
void proc() override
Execute one processing/update step.
Definition example.cpp:212
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
VK_Sprite3D * createSprite3D(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a world-space billboard sprite from a PNG file.
Definition mxvk.cpp:3578
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.
int main(void)
Definition main.cpp:7
#define MXVK_VALIDATION
Definition mxvk.hpp:27
PNG image loading and saving utilities via SDL3.
SDL_Surface * load_transparent_saucer(const std::string &png_path)
Definition example.cpp:27
float random_float(float min_value, float max_value)
Definition example.cpp:135
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:103
float random_float(float min_value, float max_value)
Generates a uniformly distributed floating-point value.
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:730
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
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:733
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:735
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732