MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
moon.cpp
Go to the documentation of this file.
1#include <cstdlib>
2
3#include <algorithm>
4#include <array>
5#include <chrono>
6#include <cmath>
7#include <cstdint>
8#include <format>
9#include <iostream>
10#include <memory>
11#include <random>
12#include <string>
13#include <vector>
14
15#include <SDL3/SDL.h>
16#include <glm/ext/matrix_clip_space.hpp>
17#include <glm/ext/matrix_transform.hpp>
18#include <glm/glm.hpp>
19
20#include "mxvk/argz.hpp"
21#include "mxvk/mxvk.hpp"
24#include "mxvk/mxvk_png.hpp"
25
26namespace example {
27
29 glm::vec3 surface_normal;
30 float scale;
32 };
33
34 struct StarParticle {
35 glm::vec3 position{0.0f};
36 float speed = 0.0f;
37 float size = 0.0f;
38 float brightness = 0.0f;
39 float twinkle_speed = 0.0f;
40 float twinkle_phase = 0.0f;
41 float drift = 0.0f;
42 glm::vec3 color{1.0f};
43 };
44
45 SDL_Surface *loadColorKeyedPNG(const std::string &path, std::uint8_t threshold = 12, std::uint8_t softness = 48) {
46 SDL_Surface *loaded_surface = mxvk::LoadPNG(path.c_str());
47 if (loaded_surface == nullptr) {
48 throw mxvk::Exception("Failed to load PNG: " + path);
49 }
50
51 SDL_Surface *surface = SDL_ConvertSurface(loaded_surface, SDL_PIXELFORMAT_RGBA32);
52 SDL_DestroySurface(loaded_surface);
53 if (surface == nullptr) {
54 throw mxvk::Exception("Failed to convert PNG to RGBA: " + path);
55 }
56
57 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(surface->format);
58 if (format_details == nullptr) {
59 SDL_DestroySurface(surface);
60 throw mxvk::Exception("Failed to query pixel format details for: " + path);
61 }
62
63 if (!SDL_LockSurface(surface)) {
64 SDL_DestroySurface(surface);
65 throw mxvk::Exception("Failed to lock PNG surface: " + path);
66 }
67
68 auto *pixels = static_cast<std::uint32_t *>(surface->pixels);
69 const int pixel_count = surface->w * surface->h;
70 for (int i = 0; i < pixel_count; ++i) {
71 std::uint8_t r = 0;
72 std::uint8_t g = 0;
73 std::uint8_t b = 0;
74 std::uint8_t a = 0;
75 SDL_GetRGBA(pixels[i], format_details, nullptr, &r, &g, &b, &a);
76
77 const int brightness = std::max({static_cast<int>(r), static_cast<int>(g), static_cast<int>(b)});
78 if (brightness <= threshold) {
79 a = 0;
80 } else if (brightness < static_cast<int>(threshold) + static_cast<int>(softness)) {
81 const float fade = static_cast<float>(brightness - threshold) / static_cast<float>(std::max<int>(1, softness));
82 a = static_cast<std::uint8_t>(std::clamp(static_cast<int>(std::lround(static_cast<float>(a) * fade)), 0, 255));
83 }
84
85 pixels[i] = SDL_MapRGBA(format_details, nullptr, r, g, b, a);
86 }
87
88 SDL_UnlockSurface(surface);
89 return surface;
90 }
91
92 class MoonWindow : public mxvk::VK_Window {
93 public:
94 MoonWindow(const std::string &filename,
95 const std::string &path,
96 const std::string &fragment_path,
97 const std::string &title,
98 int width,
99 int height,
100 bool fullscreen, bool enable_vsync)
101 : mxvk::VK_Window(title, width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
102 asset_root(path.empty() ? std::string(MOON_ASSET_DIR) : path) {
103 const std::string model_path = filename.empty() ? (asset_root + "/data/moon.obj") : filename;
104 const std::string texture_base_path = asset_root + "/data";
105 const std::string vert_path = asset_root + "/data/model.vert.spv";
106 const std::string frag_path = fragment_path.empty() ? (asset_root + "/data/model.frag.spv") : fragment_path;
107
108 model.load(this, model_path, "", texture_base_path, 1.0f);
109 model.setShaders(this, vert_path, frag_path);
110
111 const std::string pyramid_path = asset_root + "/data/pyramid.obj";
112 for (mxvk::VKAbstractModel &pyramid : pyramids) {
113 pyramid.load(this, pyramid_path, "", texture_base_path, 1.0f);
114 pyramid.setShaders(this, vert_path, frag_path);
115 }
116
117 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> star_surface(loadColorKeyedPNG(asset_root + "/data/star.png"), SDL_DestroySurface);
118 star_sprite = createSprite3D(star_surface.get());
119 if (star_sprite == nullptr) {
120 throw mxvk::Exception("Failed to create moon starfield sprite batch");
121 }
122 star_sprite->setDepthTestEnabled(true);
123 star_sprite->setDepthWriteEnabled(false);
124 star_sprite->setAlphaDiscardThreshold(0.01f);
125 initStars();
126 }
127
128 ~MoonWindow() override {
129 if (device != VK_NULL_HANDLE) {
130 vkDeviceWaitIdle(device);
131 }
132 model.cleanup(this);
133 for (mxvk::VKAbstractModel &pyramid : pyramids) {
134 pyramid.cleanup(this);
135 }
136 if (star_sprite != nullptr) {
137 star_sprite->cleanup();
138 }
139 }
140
141 void event(SDL_Event &e) override {
142 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
143 exit();
144 return;
145 }
146
147 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
148 if (!e.key.repeat) {
149 auto_spin_enabled = !auto_spin_enabled;
150 }
151 return;
152 }
153
154 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
155 mouse_dragging = true;
156 last_mouse_x = static_cast<int>(e.button.x);
157 last_mouse_y = static_cast<int>(e.button.y);
158 return;
159 }
160
161 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
162 mouse_dragging = false;
163 return;
164 }
165
166 if (e.type == SDL_EVENT_MOUSE_MOTION && mouse_dragging) {
167 const int x = static_cast<int>(e.motion.x);
168 const int y = static_cast<int>(e.motion.y);
169 const int delta_x = x - last_mouse_x;
170 const int delta_y = y - last_mouse_y;
171
172 yaw_degrees += static_cast<float>(delta_x) * mouse_sensitivity;
173 pitch_degrees += static_cast<float>(delta_y) * mouse_sensitivity;
174 pitch_degrees = std::clamp(pitch_degrees, -80.0f, 80.0f);
175
176 last_mouse_x = x;
177 last_mouse_y = y;
178 return;
179 }
180
181 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
182 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y : static_cast<float>(e.wheel.integer_y);
183 camera_distance -= delta * 0.45f;
184 camera_distance = std::clamp(camera_distance, 1.8f, 12.0f);
185 return;
186 }
187 }
188
189 void onSwapchainRecreated() override {
190 model.resize(this);
191 for (mxvk::VKAbstractModel &pyramid : pyramids) {
192 pyramid.resize(this);
193 }
194 if (star_sprite != nullptr) {
195 star_sprite->resize(this);
196 }
197 }
198
199 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
200 const auto now = std::chrono::steady_clock::now();
201 const float elapsed_seconds = std::chrono::duration<float>(now - start_time).count();
202 const float delta_seconds = std::chrono::duration<float>(now - last_frame_time).count();
203 last_frame_time = now;
204 if (auto_spin_enabled) {
205 auto_spin_radians += delta_seconds * auto_spin_speed;
206 }
207
208 const VkExtent2D extent = getSwapchainExtent();
209 const float aspect = (extent.height > 0U)
210 ? static_cast<float>(extent.width) / static_cast<float>(extent.height)
211 : 1.0f;
212
213 glm::mat4 moon_rotation = glm::rotate(glm::mat4(1.0f), glm::radians(pitch_degrees), glm::vec3(1.0f, 0.0f, 0.0f));
214 moon_rotation = glm::rotate(moon_rotation, glm::radians(yaw_degrees), glm::vec3(0.0f, 1.0f, 0.0f));
215 moon_rotation = glm::rotate(moon_rotation, auto_spin_radians, glm::vec3(0.0f, 1.0f, 0.0f));
216
218 ubo.model = moon_rotation;
219 ubo.model = glm::scale(ubo.model, glm::vec3(model.modelRenderScale()));
220 ubo.model = glm::translate(ubo.model, model.modelCenterOffset());
221 ubo.view = glm::lookAt(glm::vec3(0.0f, 0.1f, camera_distance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
222 ubo.proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
223 ubo.proj[1][1] *= -1.0f;
224 ubo.fx = glm::vec4(elapsed_seconds, 0.0f, 0.0f, 0.37f);
225
226 model.updateUBO(imageIndex, ubo);
227 model.render(cmd, imageIndex, false);
228
229 for (size_t i = 0; i < pyramids.size(); ++i) {
230 mxvk::UniformBufferObject pyramid_ubo = ubo;
231 pyramid_ubo.model = pyramidTransform(moon_rotation, pyramids[i], pyramid_placements[i]);
232 pyramids[i].updateUBO(imageIndex, pyramid_ubo);
233 pyramids[i].render(cmd, imageIndex, false);
234 }
235
236 if (star_sprite != nullptr) {
237 updateStars(elapsed_seconds);
238 star_sprite->updateCamera(imageIndex, ubo.view, ubo.proj);
239 for (const StarParticle &star : stars) {
240 const float twinkle = 0.72f + 0.28f * std::sin((elapsed_seconds * star.twinkle_speed) + star.twinkle_phase);
241 const float depth_fade = std::clamp((-star.position.z - 8.0f) / 42.0f, 0.2f, 1.0f);
242 const float alpha = std::clamp(star.brightness * twinkle * depth_fade, 0.08f, 0.95f);
243 const float size = star.size * 1.8f * (0.72f + (1.0f - depth_fade) * 0.38f) * twinkle;
244 star_sprite->drawSprite(star.position, glm::vec2(size), glm::vec4(star.color, alpha));
245 }
246 star_sprite->render(cmd, imageIndex);
247 star_sprite->clearQueue();
248 }
249 }
250
251 private:
252 void initStars() {
253 stars.reserve(STAR_COUNT);
254 for (size_t i = 0; i < STAR_COUNT; ++i) {
255 stars.push_back(makeStar(true));
256 }
257 }
258
259 void updateStars(float elapsed_seconds) {
260 const float delta_seconds = std::clamp(elapsed_seconds - last_star_update_seconds, 0.0f, 0.05f);
261 last_star_update_seconds = elapsed_seconds;
262
263 for (StarParticle &star : stars) {
264 star.position.z += star.speed * delta_seconds;
265 star.position.x += std::sin(elapsed_seconds * 0.26f + star.twinkle_phase) * star.drift * delta_seconds;
266 star.position.y += std::cos(elapsed_seconds * 0.19f + star.twinkle_phase) * star.drift * delta_seconds;
267 if (star.position.z > -6.0f) {
268 star = makeStar(false);
269 }
270 }
271 }
272
273 [[nodiscard]] StarParticle makeStar(bool randomize_depth) {
274 StarParticle star{};
275 star.position.x = randomFloat(-26.0f, 26.0f);
276 star.position.y = randomFloat(-16.0f, 16.0f);
277 star.position.z = randomize_depth ? randomFloat(-66.0f, -8.0f) : randomFloat(-66.0f, -52.0f);
278 star.speed = randomFloat(0.16f, 1.25f);
279 star.brightness = randomFloat(0.22f, 1.0f);
280 star.twinkle_speed = randomFloat(1.5f, 5.5f);
281 star.twinkle_phase = randomFloat(0.0f, 6.28318530718f);
282 star.drift = randomFloat(0.01f, 0.09f);
283
284 const float density = randomFloat(0.0f, 1.0f);
285 if (density < 0.58f) {
286 star.size = randomFloat(0.030f, 0.075f);
287 star.brightness *= randomFloat(0.55f, 0.85f);
288 } else if (density < 0.88f) {
289 star.size = randomFloat(0.075f, 0.150f);
290 star.brightness *= randomFloat(0.75f, 1.0f);
291 } else {
292 star.size = randomFloat(0.150f, 0.320f);
293 star.brightness *= randomFloat(0.95f, 1.20f);
294 }
295
296 const float tint = randomFloat(0.0f, 1.0f);
297 if (tint < 0.24f) {
298 star.color = glm::vec3(0.70f, 0.82f, 1.0f);
299 } else if (tint < 0.48f) {
300 star.color = glm::vec3(0.86f, 0.92f, 1.0f);
301 } else if (tint < 0.72f) {
302 star.color = glm::vec3(1.0f, 0.95f, 0.82f);
303 } else if (tint < 0.90f) {
304 star.color = glm::vec3(1.0f, 0.86f, 0.66f);
305 } else {
306 star.color = glm::vec3(1.0f, 1.0f, 1.0f);
307 }
308 return star;
309 }
310
311 [[nodiscard]] float randomFloat(float min_value, float max_value) {
312 std::uniform_real_distribution<float> dist(min_value, max_value);
313 return dist(random_engine);
314 }
315
316 [[nodiscard]] static glm::mat4 surfaceBasis(const glm::vec3 &surface_normal, float yaw_degrees) {
317 const glm::vec3 normal = glm::normalize(surface_normal);
318 const glm::vec3 reference = (std::abs(normal.y) > 0.92f) ? glm::vec3(1.0f, 0.0f, 0.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
319 glm::vec3 tangent = glm::normalize(glm::cross(reference, normal));
320 glm::vec3 bitangent = glm::normalize(glm::cross(normal, tangent));
321
322 const float yaw = glm::radians(yaw_degrees);
323 tangent = glm::normalize((std::cos(yaw) * tangent) + (std::sin(yaw) * bitangent));
324 bitangent = glm::normalize(glm::cross(normal, tangent));
325
326 glm::mat4 basis(1.0f);
327 basis[0] = glm::vec4(tangent, 0.0f);
328 basis[1] = glm::vec4(normal, 0.0f);
329 basis[2] = glm::vec4(bitangent, 0.0f);
330 return basis;
331 }
332
333 [[nodiscard]] static glm::mat4 pyramidTransform(const glm::mat4 &moon_rotation,
334 const mxvk::VKAbstractModel &pyramid,
335 const PyramidPlacement &placement) {
336 constexpr float MOON_RADIUS = 1.28f;
337 const glm::vec3 normal = glm::normalize(placement.surface_normal);
338 const float surface_offset = MOON_RADIUS + (placement.scale * 0.55f);
339
340 glm::mat4 transform = moon_rotation;
341 transform = glm::translate(transform, normal * surface_offset);
342 transform *= surfaceBasis(normal, placement.yaw_degrees);
343 transform = glm::scale(transform, glm::vec3(pyramid.modelRenderScale() * placement.scale));
344 transform = glm::translate(transform, pyramid.modelCenterOffset());
345 return transform;
346 }
347
348 std::string asset_root;
349 mxvk::VKAbstractModel model{};
350 std::array<mxvk::VKAbstractModel, 7> pyramids{};
351 mxvk::VK_Sprite3D *star_sprite = nullptr;
352 std::vector<StarParticle> stars{};
353 std::default_random_engine random_engine{1337U};
354 const std::array<PyramidPlacement, 7> pyramid_placements{{
355 {glm::vec3(0.18f, 0.98f, 0.08f), 0.055f, 12.0f},
356 {glm::vec3(-0.45f, 0.74f, 0.50f), 0.075f, 51.0f},
357 {glm::vec3(0.58f, 0.62f, 0.53f), 0.048f, 138.0f},
358 {glm::vec3(-0.76f, 0.43f, -0.20f), 0.062f, 204.0f},
359 {glm::vec3(0.70f, 0.22f, -0.58f), 0.085f, 296.0f},
360 {glm::vec3(-0.12f, -0.18f, 0.98f), 0.045f, 33.0f},
361 {glm::vec3(0.30f, -0.50f, -0.81f), 0.070f, 250.0f},
362 }};
363 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
364 bool mouse_dragging = false;
365 bool auto_spin_enabled = true;
366 int last_mouse_x = 0;
367 int last_mouse_y = 0;
368 float yaw_degrees = 0.0f;
369 float pitch_degrees = 8.0f;
370 float camera_distance = 4.3f;
371 float mouse_sensitivity = 0.35f;
372 float auto_spin_speed = 0.45f;
373 float auto_spin_radians = 0.0f;
374 float last_star_update_seconds = 0.0f;
375 std::chrono::steady_clock::time_point last_frame_time{std::chrono::steady_clock::now()};
376 static constexpr size_t STAR_COUNT = 1800;
377 };
378
379} // namespace example
380
381int main(int argc, char **argv) {
382 try {
383 const Arguments args = proc_args(argc, argv);
384 example::MoonWindow window(args.filename, args.path, args.fragmentPath, "MXVK Moon Example", args.width, args.height, args.fullscreen, args.enable_vsync);
385 window.loop();
386 } catch (mxvk::Exception &e) {
387 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
388 return EXIT_FAILURE;
389 } catch (ArgException<std::string> &e) {
390 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
391 return EXIT_FAILURE;
392 }
393
394 return EXIT_SUCCESS;
395}
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
MoonWindow(const std::string &filename, const std::string &path, const std::string &fragment_path, const std::string &title, int width, int height, bool fullscreen, bool enable_vsync)
Definition moon.cpp:94
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition moon.cpp:199
void event(SDL_Event &e) override
Handle one SDL event.
Definition moon.cpp:141
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition moon.cpp:189
~MoonWindow() override
Definition moon.cpp:128
std::string text() const
Convenience wrapper that owns mesh, textures, descriptors, and pipeline state.
float modelRenderScale() const
Access the computed render scale used for normalization.
glm::vec3 modelCenterOffset() const
Access the computed center offset used for normalization.
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
VkDevice device
Definition mxvk.hpp:485
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.
PNG image loading and saving utilities via SDL3.
SDL_Surface * loadColorKeyedPNG(const std::string &path, std::uint8_t threshold=12, std::uint8_t softness=48)
Definition moon.cpp:45
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
constexpr int STAR_COUNT
Definition space.cpp:27
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 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
glm::vec3 surface_normal
Definition moon.cpp:29
glm::vec3 color
Definition moon.cpp:42
glm::vec3 position
Definition moon.cpp:35
Default transform UBO payload for model shaders.