MXVK Vulkan Framework 0.35.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, const std::string &path, const std::string &fragment_path, const std::string &title, int width, int height, bool fullscreen, bool enable_vsync) : mxvk::VK_Window(title, width, height, fullscreen, MXVK_VALIDATION, enable_vsync), asset_root(path.empty() ? std::string(MOON_ASSET_DIR) : path) {
95 const std::string model_path = filename.empty() ? (asset_root + "/data/moon.obj") : filename;
96 const std::string texture_base_path = asset_root + "/data";
97 const std::string vert_path = asset_root + "/data/model.vert.spv";
98 const std::string frag_path = fragment_path.empty() ? (asset_root + "/data/model.frag.spv") : fragment_path;
99
100 model.load(this, model_path, "", texture_base_path, 1.0f);
101 model.setShaders(this, vert_path, frag_path);
102
103 const std::string pyramid_path = asset_root + "/data/pyramid.obj";
104 for (mxvk::VKAbstractModel &pyramid : pyramids) {
105 pyramid.load(this, pyramid_path, "", texture_base_path, 1.0f);
106 pyramid.setShaders(this, vert_path, frag_path);
107 }
108
109 std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> star_surface(loadColorKeyedPNG(asset_root + "/data/star.png"), SDL_DestroySurface);
110 star_sprite = createSprite3D(star_surface.get());
111 if (star_sprite == nullptr) {
112 throw mxvk::Exception("Failed to create moon starfield sprite batch");
113 }
114 star_sprite->setDepthTestEnabled(true);
115 star_sprite->setDepthWriteEnabled(false);
116 star_sprite->setAlphaDiscardThreshold(0.01f);
117 initStars();
118 }
119
120 ~MoonWindow() override {
121 if (device != VK_NULL_HANDLE) {
122 vkDeviceWaitIdle(device);
123 }
124 model.cleanup(this);
125 for (mxvk::VKAbstractModel &pyramid : pyramids) {
126 pyramid.cleanup(this);
127 }
128 if (star_sprite != nullptr) {
129 star_sprite->cleanup();
130 }
131 }
132
133 void event(SDL_Event &e) override {
134 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
135 exit();
136 return;
137 }
138
139 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
140 if (!e.key.repeat) {
141 auto_spin_enabled = !auto_spin_enabled;
142 }
143 return;
144 }
145
146 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
147 mouse_dragging = true;
148 last_mouse_x = static_cast<int>(e.button.x);
149 last_mouse_y = static_cast<int>(e.button.y);
150 return;
151 }
152
153 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
154 mouse_dragging = false;
155 return;
156 }
157
158 if (e.type == SDL_EVENT_MOUSE_MOTION && mouse_dragging) {
159 const int x = static_cast<int>(e.motion.x);
160 const int y = static_cast<int>(e.motion.y);
161 const int delta_x = x - last_mouse_x;
162 const int delta_y = y - last_mouse_y;
163
164 yaw_degrees += static_cast<float>(delta_x) * mouse_sensitivity;
165 pitch_degrees += static_cast<float>(delta_y) * mouse_sensitivity;
166 pitch_degrees = std::clamp(pitch_degrees, -80.0f, 80.0f);
167
168 last_mouse_x = x;
169 last_mouse_y = y;
170 return;
171 }
172
173 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
174 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y : static_cast<float>(e.wheel.integer_y);
175 camera_distance -= delta * 0.45f;
176 camera_distance = std::clamp(camera_distance, 1.8f, 12.0f);
177 return;
178 }
179 }
180
181 void onSwapchainRecreated() override {
182 model.resize(this);
183 for (mxvk::VKAbstractModel &pyramid : pyramids) {
184 pyramid.resize(this);
185 }
186 if (star_sprite != nullptr) {
187 star_sprite->resize(this);
188 }
189 }
190
191 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
192 const auto now = std::chrono::steady_clock::now();
193 const float elapsed_seconds = std::chrono::duration<float>(now - start_time).count();
194 const float delta_seconds = std::chrono::duration<float>(now - last_frame_time).count();
195 last_frame_time = now;
196 if (auto_spin_enabled) {
197 auto_spin_radians += delta_seconds * auto_spin_speed;
198 }
199
200 const VkExtent2D extent = getSwapchainExtent();
201 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
202
203 glm::mat4 moon_rotation = glm::rotate(glm::mat4(1.0f), glm::radians(pitch_degrees), glm::vec3(1.0f, 0.0f, 0.0f));
204 moon_rotation = glm::rotate(moon_rotation, glm::radians(yaw_degrees), glm::vec3(0.0f, 1.0f, 0.0f));
205 moon_rotation = glm::rotate(moon_rotation, auto_spin_radians, glm::vec3(0.0f, 1.0f, 0.0f));
206
208 ubo.model = moon_rotation;
209 ubo.model = glm::scale(ubo.model, glm::vec3(model.modelRenderScale()));
210 ubo.model = glm::translate(ubo.model, model.modelCenterOffset());
211 ubo.view = glm::lookAt(glm::vec3(0.0f, 0.1f, camera_distance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
212 ubo.proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
213 ubo.proj[1][1] *= -1.0f;
214 ubo.fx = glm::vec4(elapsed_seconds, 0.0f, 0.0f, 0.37f);
215
216 model.updateUBO(imageIndex, ubo);
217 model.render(cmd, imageIndex, false);
218
219 for (size_t i = 0; i < pyramids.size(); ++i) {
220 mxvk::UniformBufferObject pyramid_ubo = ubo;
221 pyramid_ubo.model = pyramidTransform(moon_rotation, pyramids[i], pyramid_placements[i]);
222 pyramids[i].updateUBO(imageIndex, pyramid_ubo);
223 pyramids[i].render(cmd, imageIndex, false);
224 }
225
226 if (star_sprite != nullptr) {
227 updateStars(elapsed_seconds);
228 star_sprite->updateCamera(imageIndex, ubo.view, ubo.proj);
229 for (const StarParticle &star : stars) {
230 const float twinkle = 0.72f + 0.28f * std::sin((elapsed_seconds * star.twinkle_speed) + star.twinkle_phase);
231 const float depth_fade = std::clamp((-star.position.z - 8.0f) / 42.0f, 0.2f, 1.0f);
232 const float alpha = std::clamp(star.brightness * twinkle * depth_fade, 0.08f, 0.95f);
233 const float size = star.size * 1.8f * (0.72f + (1.0f - depth_fade) * 0.38f) * twinkle;
234 star_sprite->drawSprite(star.position, glm::vec2(size), glm::vec4(star.color, alpha));
235 }
236 star_sprite->render(cmd, imageIndex);
237 star_sprite->clearQueue();
238 }
239 }
240
241 private:
242 void initStars() {
243 stars.reserve(STAR_COUNT);
244 for (size_t i = 0; i < STAR_COUNT; ++i) {
245 stars.push_back(makeStar(true));
246 }
247 }
248
249 void updateStars(float elapsed_seconds) {
250 const float delta_seconds = std::clamp(elapsed_seconds - last_star_update_seconds, 0.0f, 0.05f);
251 last_star_update_seconds = elapsed_seconds;
252
253 for (StarParticle &star : stars) {
254 star.position.z += star.speed * delta_seconds;
255 star.position.x += std::sin(elapsed_seconds * 0.26f + star.twinkle_phase) * star.drift * delta_seconds;
256 star.position.y += std::cos(elapsed_seconds * 0.19f + star.twinkle_phase) * star.drift * delta_seconds;
257 if (star.position.z > -6.0f) {
258 star = makeStar(false);
259 }
260 }
261 }
262
263 [[nodiscard]] StarParticle makeStar(bool randomize_depth) {
264 StarParticle star{};
265 star.position.x = randomFloat(-26.0f, 26.0f);
266 star.position.y = randomFloat(-16.0f, 16.0f);
267 star.position.z = randomize_depth ? randomFloat(-66.0f, -8.0f) : randomFloat(-66.0f, -52.0f);
268 star.speed = randomFloat(0.16f, 1.25f);
269 star.brightness = randomFloat(0.22f, 1.0f);
270 star.twinkle_speed = randomFloat(1.5f, 5.5f);
271 star.twinkle_phase = randomFloat(0.0f, 6.28318530718f);
272 star.drift = randomFloat(0.01f, 0.09f);
273
274 const float density = randomFloat(0.0f, 1.0f);
275 if (density < 0.58f) {
276 star.size = randomFloat(0.030f, 0.075f);
277 star.brightness *= randomFloat(0.55f, 0.85f);
278 } else if (density < 0.88f) {
279 star.size = randomFloat(0.075f, 0.150f);
280 star.brightness *= randomFloat(0.75f, 1.0f);
281 } else {
282 star.size = randomFloat(0.150f, 0.320f);
283 star.brightness *= randomFloat(0.95f, 1.20f);
284 }
285
286 const float tint = randomFloat(0.0f, 1.0f);
287 if (tint < 0.24f) {
288 star.color = glm::vec3(0.70f, 0.82f, 1.0f);
289 } else if (tint < 0.48f) {
290 star.color = glm::vec3(0.86f, 0.92f, 1.0f);
291 } else if (tint < 0.72f) {
292 star.color = glm::vec3(1.0f, 0.95f, 0.82f);
293 } else if (tint < 0.90f) {
294 star.color = glm::vec3(1.0f, 0.86f, 0.66f);
295 } else {
296 star.color = glm::vec3(1.0f, 1.0f, 1.0f);
297 }
298 return star;
299 }
300
301 [[nodiscard]] float randomFloat(float min_value, float max_value) {
302 std::uniform_real_distribution<float> dist(min_value, max_value);
303 return dist(random_engine);
304 }
305
306 [[nodiscard]] static glm::mat4 surfaceBasis(const glm::vec3 &surface_normal, float yaw_degrees) {
307 const glm::vec3 normal = glm::normalize(surface_normal);
308 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);
309 glm::vec3 tangent = glm::normalize(glm::cross(reference, normal));
310 glm::vec3 bitangent = glm::normalize(glm::cross(normal, tangent));
311
312 const float yaw = glm::radians(yaw_degrees);
313 tangent = glm::normalize((std::cos(yaw) * tangent) + (std::sin(yaw) * bitangent));
314 bitangent = glm::normalize(glm::cross(normal, tangent));
315
316 glm::mat4 basis(1.0f);
317 basis[0] = glm::vec4(tangent, 0.0f);
318 basis[1] = glm::vec4(normal, 0.0f);
319 basis[2] = glm::vec4(bitangent, 0.0f);
320 return basis;
321 }
322
323 [[nodiscard]] static glm::mat4 pyramidTransform(const glm::mat4 &moon_rotation, const mxvk::VKAbstractModel &pyramid, const PyramidPlacement &placement) {
324 constexpr float MOON_RADIUS = 1.28f;
325 const glm::vec3 normal = glm::normalize(placement.surface_normal);
326 const float surface_offset = MOON_RADIUS + (placement.scale * 0.55f);
327
328 glm::mat4 transform = moon_rotation;
329 transform = glm::translate(transform, normal * surface_offset);
330 transform *= surfaceBasis(normal, placement.yaw_degrees);
331 transform = glm::scale(transform, glm::vec3(pyramid.modelRenderScale() * placement.scale));
332 transform = glm::translate(transform, pyramid.modelCenterOffset());
333 return transform;
334 }
335
336 std::string asset_root;
337 mxvk::VKAbstractModel model{};
338 std::array<mxvk::VKAbstractModel, 7> pyramids{};
339 mxvk::VK_Sprite3D *star_sprite = nullptr;
340 std::vector<StarParticle> stars{};
341 std::default_random_engine random_engine{1337U};
342 const std::array<PyramidPlacement, 7> pyramid_placements{{
343 {glm::vec3(0.18f, 0.98f, 0.08f), 0.055f, 12.0f},
344 {glm::vec3(-0.45f, 0.74f, 0.50f), 0.075f, 51.0f},
345 {glm::vec3(0.58f, 0.62f, 0.53f), 0.048f, 138.0f},
346 {glm::vec3(-0.76f, 0.43f, -0.20f), 0.062f, 204.0f},
347 {glm::vec3(0.70f, 0.22f, -0.58f), 0.085f, 296.0f},
348 {glm::vec3(-0.12f, -0.18f, 0.98f), 0.045f, 33.0f},
349 {glm::vec3(0.30f, -0.50f, -0.81f), 0.070f, 250.0f},
350 }};
351 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
352 bool mouse_dragging = false;
353 bool auto_spin_enabled = true;
354 int last_mouse_x = 0;
355 int last_mouse_y = 0;
356 float yaw_degrees = 0.0f;
357 float pitch_degrees = 8.0f;
358 float camera_distance = 4.3f;
359 float mouse_sensitivity = 0.35f;
360 float auto_spin_speed = 0.45f;
361 float auto_spin_radians = 0.0f;
362 float last_star_update_seconds = 0.0f;
363 std::chrono::steady_clock::time_point last_frame_time{std::chrono::steady_clock::now()};
364 static constexpr size_t STAR_COUNT = 1800;
365 };
366
367} // namespace example
368
369int main(int argc, char **argv) {
370 try {
371 const Arguments args = proc_args(argc, argv);
372 example::MoonWindow window(args.filename, args.path, args.fragmentPath, "MXVK Moon Example", args.width, args.height, args.fullscreen, args.enable_vsync);
373 window.loop();
374 } catch (mxvk::Exception &e) {
375 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
376 return EXIT_FAILURE;
377 } catch (ArgException<std::string> &e) {
378 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
379 return EXIT_FAILURE;
380 }
381
382 return EXIT_SUCCESS;
383}
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
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:191
void event(SDL_Event &e) override
Handle one SDL event.
Definition moon.cpp:133
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition moon.cpp:181
~MoonWindow() override
Definition moon.cpp:120
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: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
VkDevice device
Definition mxvk.hpp:606
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.
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
int main()
Definition main.py:165
Definition model.py:1
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
constexpr int STAR_COUNT
Definition space.cpp:27
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 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
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.