MXVK Vulkan Framework 0.35.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) : mxvk::VK_Window(text, width, height, fullscreen, MXVK_VALIDATION, enable_vsync) {
145 current_path = path.empty() ? std::string(sprite3d_example_ASSET_DIR) : path;
146 if (current_path == ".") {
147 current_path = sprite3d_example_ASSET_DIR;
148 }
149
150 setClearColor(0.02f, 0.03f, 0.06f, 1.0f);
151
152 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> star_surface(create_star_surface(), SDL_DestroySurface);
153 if (star_surface == nullptr) {
154 throw mxvk::Exception("Failed to create starfield sprite texture");
155 }
156 stars_sprite = createSprite3D(star_surface.get());
157 if (stars_sprite == nullptr) {
158 throw mxvk::Exception("Failed to create starfield sprite batch");
159 }
160 stars_sprite->setDepthTestEnabled(true);
161 stars_sprite->setDepthWriteEnabled(false);
162 stars_sprite->setAlphaDiscardThreshold(0.01f);
163
164 const std::string image_path = current_path + "/data/saucer.png";
165 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> saucer_surface(load_transparent_saucer(image_path), SDL_DestroySurface);
166 sprite = createSprite3D(saucer_surface.get());
167
168 if (sprite == nullptr) {
169 throw mxvk::Exception("Failed to create sprite3D example sprite");
170 }
171
172 sprite->setDepthTestEnabled(true);
173 sprite->setDepthWriteEnabled(false);
174 sprite->setAlphaDiscardThreshold(0.01f);
175
176 initStars(25000);
177 initSaucers();
178 }
179
180 void event(SDL_Event &e) override {
181 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
182 mouse_dragging = true;
183 last_mouse_x = static_cast<int>(e.button.x);
184 last_mouse_y = static_cast<int>(e.button.y);
185 }
186 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
187 mouse_dragging = false;
188 }
189 if (e.type == SDL_EVENT_MOUSE_MOTION && mouse_dragging) {
190 const int mouse_x = static_cast<int>(e.motion.x);
191 const int mouse_y = static_cast<int>(e.motion.y);
192 const int delta_x = mouse_x - last_mouse_x;
193 const int delta_y = mouse_y - last_mouse_y;
194
195 yaw_degrees += static_cast<float>(delta_x) * mouse_sensitivity;
196 pitch_degrees -= static_cast<float>(delta_y) * mouse_sensitivity;
197 pitch_degrees = std::clamp(pitch_degrees, -85.0f, 85.0f);
198
199 last_mouse_x = mouse_x;
200 last_mouse_y = mouse_y;
201 }
202 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
203 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y : static_cast<float>(e.wheel.integer_y);
204 applyWheelZoom(delta);
205 }
206 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
207 exit();
208 }
209 }
210
211 void proc() override {}
212
213 void onSwapchainRecreated() override {
214 if (stars_sprite != nullptr) {
215 stars_sprite->resize(this);
216 }
217 if (sprite != nullptr) {
218 sprite->resize(this);
219 }
220 }
221
222 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
223 if (sprite == nullptr) {
224 return;
225 }
226
227 const auto now = std::chrono::steady_clock::now();
228 const float elapsed_seconds = std::chrono::duration<float>(now - start_time).count();
229 const VkExtent2D extent = getSwapchainExtent();
230 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
231
232 const float yaw_radians = glm::radians(yaw_degrees);
233 const float pitch_radians = glm::radians(pitch_degrees);
234 const glm::vec3 camera_position(camera_distance * std::cos(pitch_radians) * std::sin(yaw_radians), camera_distance * std::sin(pitch_radians), camera_distance * std::cos(pitch_radians) * std::cos(yaw_radians));
235 glm::mat4 view = glm::lookAt(camera_position, glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
236 glm::mat4 proj = glm::perspective(glm::radians(48.0f), aspect, 0.1f, 100.0f);
237 proj[1][1] *= -1.0f;
238
239 updateStars(elapsed_seconds);
240
241 if (stars_sprite != nullptr) {
242 stars_sprite->updateCamera(imageIndex, view, proj);
243 for (const StarParticle &star : stars) {
244 const float twinkle = 0.78f + 0.22f * std::sin(elapsed_seconds * star.twinkle);
245 const float star_size = star.size * twinkle * (star.is_constellation ? 1.25f : 1.0f);
246 const float alpha = std::clamp(((6.5f - star.magnitude) / 6.5f) * twinkle, 0.0f, 1.0f);
247 const glm::vec3 color = starColor(star.temperature);
248 stars_sprite->drawSprite(glm::vec3(star.x, star.y, star.z), glm::vec2(star_size, star_size), glm::vec4(color, alpha));
249 }
250 stars_sprite->render(cmd, imageIndex);
251 stars_sprite->clearQueue();
252 }
253
254 sprite->updateCamera(imageIndex, view, proj);
255
256 for (const SaucerFlight &saucer : saucers) {
257 const float orbit_angle = saucer.orbit_phase + elapsed_seconds * saucer.orbit_speed;
258 const float pulse = 1.0f + std::sin(elapsed_seconds * saucer.pulse_speed + saucer.orbit_phase) * saucer.pulse_amount;
259 const float bob = std::sin(elapsed_seconds * saucer.bob_speed + saucer.orbit_phase) * saucer.bob_amplitude;
260 const float roll = orbit_angle * 0.85f + elapsed_seconds * saucer.roll_speed;
261
262 const glm::vec3 position(saucer.orbit_center.x + std::cos(orbit_angle) * saucer.orbit_radius, saucer.orbit_center.y + bob, saucer.orbit_center.z + std::sin(orbit_angle) * saucer.orbit_radius * 0.62f);
263
264 const glm::vec2 size(saucer.base_size * pulse, saucer.base_size * pulse * 0.72f);
265 sprite->drawSprite(position, size, saucer.tint, roll);
266 }
267
268 sprite->render(cmd, imageIndex);
269 sprite->clearQueue();
270 }
271
272 private:
273 void initSaucers() {
274 static const std::array<SaucerFlight, 5> base_saucers{{
275 {{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}},
276 {{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}},
277 {{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}},
278 {{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}},
279 {{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}},
280 }};
281
282 saucers.reserve(base_saucers.size() * 4U);
283 for (int ring = 0; ring < 4; ++ring) {
284 const float ring_offset = static_cast<float>(ring) * 0.28f;
285 const float ring_speed_scale = 1.0f + static_cast<float>(ring) * 0.06f;
286 const float ring_phase_offset = static_cast<float>(ring) * 0.9f;
287 for (std::size_t i = 0; i < base_saucers.size(); ++i) {
288 SaucerFlight flight = base_saucers[i];
289 flight.orbit_center.x += (static_cast<float>(i) - 2.0f) * 0.12f;
290 flight.orbit_center.y += (static_cast<float>(ring) - 1.5f) * 0.06f;
291 flight.orbit_radius += ring_offset + static_cast<float>(i) * 0.05f;
292 flight.orbit_speed *= ring_speed_scale * 1.35f;
293 flight.orbit_phase += ring_phase_offset + static_cast<float>(i) * 0.4f;
294 flight.bob_amplitude *= 1.0f + static_cast<float>(ring) * 0.08f;
295 flight.base_size *= 1.0f + static_cast<float>(ring) * 0.05f;
296 flight.pulse_amount *= 1.0f + static_cast<float>(ring) * 0.03f;
297 flight.pulse_speed *= (1.0f + static_cast<float>(ring) * 0.05f) * 1.15f;
298 flight.roll_speed *= 1.25f;
299 saucers.push_back(flight);
300 }
301 }
302 }
303
304 void initStars(int count) {
305 stars.reserve(static_cast<std::size_t>(count));
306 for (int i = 0; i < count; ++i) {
307 stars.push_back(makeStar());
308 }
309 }
310
311 void updateStars([[maybe_unused]] float elapsed_seconds) {
312 constexpr float max_radius = 120.0f;
313 constexpr float min_radius = 20.0f;
314 constexpr float dt = 1.0f / 60.0f;
315
316 for (StarParticle &star : stars) {
317 star.x += star.vx * dt;
318 star.y += star.vy * dt;
319 star.z += star.vz * dt;
320
321 const float radius_squared = (star.x * star.x) + (star.y * star.y) + (star.z * star.z);
322 if (radius_squared < (min_radius * min_radius) || radius_squared > (max_radius * max_radius)) {
323 star = makeStar();
324 }
325 }
326 }
327
328 StarParticle makeStar() const {
329 StarParticle star{};
330 constexpr float max_radius = 120.0f;
331 constexpr float min_radius = 20.0f;
332
333 const float theta = random_float(0.0f, 2.0f * k_pi);
334 const float phi = std::acos(random_float(-1.0f, 1.0f));
335 const float radius = random_float(min_radius, max_radius);
336
337 star.x = radius * std::sin(phi) * std::cos(theta);
338 star.y = radius * std::sin(phi) * std::sin(theta);
339 star.z = radius * std::cos(phi);
340
341 star.vx = random_float(-0.04f, 0.04f);
342 star.vy = random_float(-0.04f, 0.04f);
343 star.vz = random_float(-0.04f, 0.04f);
344
345 const float rarity = random_float(0.0f, 1.0f);
346 if (rarity < 0.05f) {
347 star.magnitude = random_float(-1.0f, 2.0f);
348 } else if (rarity < 0.3f) {
349 star.magnitude = random_float(2.0f, 4.0f);
350 } else {
351 star.magnitude = random_float(4.0f, 6.5f);
352 }
353
354 if (star.magnitude < 3.0f) {
355 star.temperature = random_float(4000.0f, 8000.0f);
356 } else {
357 star.temperature = random_float(2500.0f, 6000.0f);
358 }
359
360 star.twinkle = random_float(0.5f, 3.0f);
361 star.size = std::clamp(0.52f - star.magnitude * 0.045f, 0.10f, 0.45f);
362 star.is_constellation = (star.magnitude < 3.0f) && (random_float(0.0f, 1.0f) < 0.3f);
363 return star;
364 }
365
366 static glm::vec3 starColor(float temperature) {
367 float r = 1.0f;
368 float g = 1.0f;
369 float b = 1.0f;
370
371 if (temperature < 3700.0f) {
372 r = 1.0f;
373 g = temperature / 3700.0f * 0.6f;
374 b = 0.0f;
375 } else if (temperature < 5200.0f) {
376 r = 1.0f;
377 g = 0.6f + (temperature - 3700.0f) / 1500.0f * 0.4f;
378 b = (temperature - 3700.0f) / 1500.0f * 0.3f;
379 } else if (temperature < 6000.0f) {
380 r = 1.0f;
381 g = 1.0f;
382 b = (temperature - 5200.0f) / 800.0f * 0.7f;
383 } else if (temperature < 7500.0f) {
384 r = 1.0f;
385 g = 1.0f;
386 b = 0.7f + (temperature - 6000.0f) / 1500.0f * 0.3f;
387 } else {
388 r = 0.7f - (temperature - 7500.0f) / 10000.0f * 0.4f;
389 g = 0.8f + (temperature - 7500.0f) / 10000.0f * 0.2f;
390 b = 1.0f;
391 }
392
393 return glm::vec3(r, g, b);
394 }
395
396 void applyWheelZoom(float wheel_y) {
397 if (wheel_y == 0.0f) {
398 return;
399 }
400
401 const float zoom_step = 1.15f;
402 const float zoom_factor = std::pow(zoom_step, -wheel_y);
403 camera_distance = std::clamp(camera_distance * zoom_factor, min_camera_distance, max_camera_distance);
404 }
405
406 std::string current_path = ".";
407 mxvk::VK_Sprite3D *stars_sprite = nullptr;
408 mxvk::VK_Sprite3D *sprite = nullptr;
409 std::vector<StarParticle> stars;
410 std::vector<SaucerFlight> saucers;
411 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
412 float camera_distance = 10.0f;
413 float min_camera_distance = 3.0f;
414 float max_camera_distance = 20.0f;
415 bool mouse_dragging = false;
416 int last_mouse_x = 0;
417 int last_mouse_y = 0;
418 float yaw_degrees = 0.0f;
419 float pitch_degrees = 10.0f;
420 float mouse_sensitivity = 0.20f;
421 };
422} // namespace example
423
424int main(int argc, char **argv) {
425 try {
426 const Arguments args = proc_args(argc, argv);
427 example::ExampleWindow window(args.path, "VK_Sprite3D Example", args.width, args.height, args.fullscreen, args.enable_vsync);
428 window.loop();
429 } catch (mxvk::Exception &e) {
430 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
431 return EXIT_FAILURE;
432 } catch (ArgException<std::string> &e) {
433 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
434 return EXIT_FAILURE;
435 }
436
437 return EXIT_SUCCESS;
438}
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
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:222
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition example.cpp:213
void event(SDL_Event &e) override
Handle one SDL event.
Definition example.cpp:180
void proc() override
Execute one processing/update step.
Definition example.cpp:211
std::string text() const
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
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:4573
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VK_Window()=default
Construct an empty window object.
#define MXVK_VALIDATION
Definition mxvk.hpp:28
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
int main()
Definition main.py:165
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:89
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:718
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
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720