MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
starfield.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstdlib>
10#include <format>
11#include <iostream>
12#include <random>
13#include <string>
14#include <vector>
15
16#include <glm/ext/matrix_clip_space.hpp>
17#include <glm/ext/matrix_transform.hpp>
18#include <glm/glm.hpp>
19
20namespace {
21
30
31 struct Particle {
32 float x = 0.0f;
33 float y = 0.0f;
34 float z = 0.0f;
35 float vx = 0.0f;
36 float vy = 0.0f;
37 float vz = 0.0f;
38 float life = 0.0f;
39 float twinkle = 0.0f;
40 float twinkle_phase = 0.0f;
42 int layer = 0;
44 float pulse_speed = 0.0f;
45 float base_size = 0.0f;
46 };
47
48 struct LayerConfig {
49 float z_min = 0.0f;
50 float z_max = 0.0f;
51 float speed_multiplier = 0.0f;
52 float size_multiplier = 0.0f;
53 int count = 0;
54 };
55
56 constexpr int NUM_PARTICLES = 50000;
57 constexpr int NUM_LAYERS = 3;
58 constexpr int WARP_TRAIL_SEGMENTS = 5;
59
60 constexpr std::array<LayerConfig, NUM_LAYERS> LAYERS{{
61 {-12.0f, -6.0f, 0.2f, 0.6f, 28000},
62 {-6.0f, -3.0f, 0.5f, 1.0f, 14000},
63 {-3.0f, -1.0f, 1.0f, 1.6f, 8000},
64 }};
65
66 [[nodiscard]] float random_float(float min, float max) {
67 static std::random_device rd;
68 static std::default_random_engine engine(rd());
69 std::uniform_real_distribution<float> dist(min, max);
70 return dist(engine);
71 }
72
73 [[nodiscard]] StarType random_star_type() {
74 const float r = random_float(0.0f, 1.0f);
75 if (r < 0.50f) {
76 return StarType::NORMAL;
77 }
78 if (r < 0.65f) {
79 return StarType::BLUE;
80 }
81 if (r < 0.75f) {
82 return StarType::YELLOW;
83 }
84 if (r < 0.85f) {
85 return StarType::ORANGE;
86 }
87 if (r < 0.92f) {
88 return StarType::RED;
89 }
90 return StarType::BRIGHT;
91 }
92
93 [[nodiscard]] glm::vec4 star_color(StarType type, float brightness) {
94 switch (type) {
95 case StarType::BLUE:
96 return glm::vec4(0.6f * brightness, 0.8f * brightness, 1.0f * brightness, 1.0f);
98 return glm::vec4(1.0f * brightness, 0.7f * brightness, 0.3f * brightness, 1.0f);
99 case StarType::RED:
100 return glm::vec4(1.0f * brightness, 0.4f * brightness, 0.4f * brightness, 1.0f);
101 case StarType::YELLOW:
102 return glm::vec4(1.0f * brightness, 1.0f * brightness, 0.6f * brightness, 1.0f);
103 case StarType::BRIGHT:
104 return glm::vec4(1.0f * brightness, 1.0f * brightness, 1.0f * brightness, 1.0f);
105 case StarType::NORMAL:
106 default:
107 return glm::vec4(0.9f * brightness, 0.9f * brightness, 1.0f * brightness, 1.0f);
108 }
109 }
110
111} // namespace
112
113namespace example {
114
116 public:
117 StarfieldWindow(const std::string &data_root, int width, int height, bool fullscreen, bool enable_vsync)
118 : mxvk::VK_Window("MXVK Starfield", width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
119 data_root(data_root),
120 particles(NUM_PARTICLES),
121 vertices(NUM_PARTICLES * WARP_TRAIL_SEGMENTS) {
122 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
123 init_particles();
124 last_update_time = SDL_GetTicks();
125 }
126
127 ~StarfieldWindow() override {
128 if (device != VK_NULL_HANDLE) {
129 vkDeviceWaitIdle(device);
130 }
131 point_batch.cleanup();
132 }
133
134 void event(SDL_Event &e) override {
135 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
136 exit();
137 } else if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
138 boost_requested = true;
139 } else if (e.type == SDL_EVENT_KEY_UP && e.key.key == SDLK_SPACE) {
140 boost_requested = false;
141 }
142 }
143
144 void onSwapchainRecreated() override {
145 point_batch.resize(this);
146 }
147
148 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override {
149 if (!ensure_starfield_resources()) {
150 return;
151 }
152
153 const Uint32 current_time = SDL_GetTicks();
154 float delta_time = static_cast<float>(current_time - last_update_time) / 1000.0f;
155 last_update_time = current_time;
156 if (delta_time > 0.1f) {
157 delta_time = 0.1f;
158 }
159 global_time += delta_time;
160 update_boost(delta_time);
161
162 const size_t active_vertices = update_particles(delta_time);
163 point_batch.upload_vertices(vertices.data(), active_vertices);
164 point_batch.update_mvp(image_index, make_mvp());
165 point_batch.render(cmd, image_index);
166 }
167
168 private:
169 bool ensure_starfield_resources() {
170 if (point_batch.loaded()) {
171 return true;
172 }
173 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || getSwapchainImageCount() == 0U || getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
174 return false;
175 }
176
177 point_batch.load(
178 this,
179 data_root + "/star.png",
180 data_root + "/starfield.vert.spv",
181 data_root + "/starfield.frag.spv",
182 vertices.size());
183 point_batch.set_additive_blending(true);
184 point_batch.set_depth_test_enabled(false);
185 point_batch.set_depth_write_enabled(false);
186 last_update_time = SDL_GetTicks();
187 return true;
188 }
189
190 void init_particles() {
191 int particle_index = 0;
192 for (int layer = 0; layer < NUM_LAYERS; ++layer) {
193 for (int i = 0; i < LAYERS[layer].count && particle_index < NUM_PARTICLES; ++i, ++particle_index) {
194 init_particle(particles[static_cast<size_t>(particle_index)], layer);
195 particles[static_cast<size_t>(particle_index)].z = random_float(LAYERS[layer].z_min, 0.0f);
196 }
197 }
198 }
199
200 void init_particle(Particle &particle, int layer) {
201 const auto &cfg = LAYERS[static_cast<size_t>(layer)];
202 particle.layer = layer;
203 particle.x = random_float(-4.0f, 4.0f);
204 particle.y = random_float(-4.0f, 4.0f);
205 particle.z = random_float(cfg.z_min, cfg.z_max);
206 particle.vx = random_float(-0.01f, 0.01f) * cfg.speed_multiplier;
207 particle.vy = random_float(-0.01f, 0.01f) * cfg.speed_multiplier;
208 particle.vz = random_float(0.15f, 0.35f) * cfg.speed_multiplier;
209 particle.life = random_float(0.7f, 1.0f);
210 particle.twinkle = random_float(2.0f, 8.0f);
211 particle.twinkle_phase = random_float(0.0f, 6.28f);
212 particle.type = random_star_type();
213 particle.texture_index = random_float(0.0f, 1.0f) > 0.5f ? 1 : 0;
214 particle.pulse_speed = random_float(0.5f, 3.0f);
215
216 const float type_multiplier = particle.type == StarType::BRIGHT ? 2.0f : 1.0f;
217 particle.base_size = random_float(12.0f, 28.0f) * cfg.size_multiplier * type_multiplier;
218 }
219
220 void update_boost(float delta_time) {
221 const float target = boost_requested ? 1.0f : 0.0f;
222 const float rate = boost_requested ? 4.0f : 2.5f;
223 if (boost_amount < target) {
224 boost_amount = std::min(target, boost_amount + delta_time * rate);
225 } else if (boost_amount > target) {
226 boost_amount = std::max(target, boost_amount - delta_time * rate);
227 }
228 }
229
230 [[nodiscard]] size_t update_particles(float delta_time) {
231 const float speed_multiplier = 1.0f + boost_amount * 10.0f;
232 const float forward_boost = boost_amount * 2.85f;
233 const float brightness_boost = 1.0f + boost_amount * 1.15f;
234 const float size_boost = 1.0f + boost_amount * 0.85f;
235 const float warp_trail_amount = glm::smoothstep(0.05f, 1.0f, boost_amount);
236 size_t vertex_count = 0;
237
238 for (int i = 0; i < NUM_PARTICLES; ++i) {
239 auto &particle = particles[static_cast<size_t>(i)];
240 particle.x += particle.vx * delta_time;
241 particle.y += particle.vy * delta_time;
242 particle.z += (particle.vz * speed_multiplier + forward_boost) * delta_time;
243
244 if (particle.z > 0.0f) {
245 init_particle(particle, particle.layer);
246 }
247
248 if (particle.x > 4.0f) {
249 particle.x = -4.0f;
250 }
251 if (particle.x < -4.0f) {
252 particle.x = 4.0f;
253 }
254 if (particle.y > 4.0f) {
255 particle.y = -4.0f;
256 }
257 if (particle.y < -4.0f) {
258 particle.y = 4.0f;
259 }
260
261 const float twinkle1 = 0.5f * (1.0f + std::sin(global_time * particle.twinkle + particle.twinkle_phase));
262 const float twinkle2 = 0.3f * (1.0f + std::sin(global_time * particle.pulse_speed * 2.0f + particle.twinkle_phase * 1.5f));
263 const float twinkle_factor = 0.5f + 0.3f * twinkle1 + 0.2f * twinkle2;
264
265 const auto &cfg = LAYERS[static_cast<size_t>(particle.layer)];
266 float depth_factor = 1.0f - (particle.z / cfg.z_min);
267 depth_factor = glm::clamp(depth_factor, 0.3f, 1.0f);
268
269 const float brightness = particle.life * twinkle_factor * depth_factor * brightness_boost;
270 const glm::vec4 color = star_color(particle.type, brightness);
271 const float size_pulse = 1.0f + 0.2f * std::sin(global_time * particle.pulse_speed + particle.twinkle_phase);
272 const float size = particle.base_size * size_pulse * depth_factor * size_boost;
273 const float alpha = particle.life * glm::clamp(depth_factor + 0.2f, 0.0f, 1.0f);
274
275 const float radial_distance = std::sqrt(particle.x * particle.x + particle.y * particle.y);
276 const float radial_factor = radial_distance > 0.0001f ? 1.0f / radial_distance : 0.0f;
277 const float direction_x = particle.x * radial_factor;
278 const float direction_y = particle.y * radial_factor;
279 const float warp_spread = 1.0f + warp_trail_amount * (0.35f + depth_factor * 1.8f);
280 const float display_x = particle.x * warp_spread;
281 const float display_y = particle.y * warp_spread;
282
283 write_vertex(vertex_count++, display_x, display_y, particle.z, size, color, alpha);
284
285 if (warp_trail_amount > 0.0f) {
286 const int trail_segments = 1 + static_cast<int>(warp_trail_amount * static_cast<float>(WARP_TRAIL_SEGMENTS - 1));
287 const float trail_spacing = warp_trail_amount * (0.05f + depth_factor * 0.18f);
288 for (int trail = 1; trail < trail_segments; ++trail) {
289 const float trail_step = static_cast<float>(trail) / static_cast<float>(WARP_TRAIL_SEGMENTS - 1);
290 const float trail_alpha = alpha * warp_trail_amount * (1.0f - trail_step) * 0.55f;
291 const float trail_size = size * (1.0f + warp_trail_amount * (0.9f - trail_step * 0.45f));
292 const float trail_x = display_x - direction_x * trail_spacing * static_cast<float>(trail);
293 const float trail_y = display_y - direction_y * trail_spacing * static_cast<float>(trail);
294 const float trail_z = particle.z - warp_trail_amount * 0.02f * static_cast<float>(trail);
295 write_vertex(vertex_count++, trail_x, trail_y, trail_z, trail_size, color, trail_alpha);
296 }
297 }
298 }
299
300 return vertex_count;
301 }
302
303 void write_vertex(size_t index, float x, float y, float z, float size, const glm::vec4 &color, float alpha) {
304 auto &vertex = vertices[index];
305 vertex.position[0] = x;
306 vertex.position[1] = y;
307 vertex.position[2] = z;
308 vertex.size = size;
309 vertex.color[0] = color.r;
310 vertex.color[1] = color.g;
311 vertex.color[2] = color.b;
312 vertex.color[3] = alpha;
313 }
314
315 [[nodiscard]] glm::mat4 make_mvp() const {
316 const VkExtent2D extent = getSwapchainExtent();
317 const float aspect = extent.height > 0U ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
318 glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
319 projection[1][1] *= -1.0f;
320
321 const glm::vec3 camera_pos(
322 camera_zoom * std::sin(glm::radians(camera_rotation)),
323 0.0f,
324 camera_zoom * std::cos(glm::radians(camera_rotation)));
325 const glm::mat4 view = glm::lookAt(camera_pos, glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
326 return projection * view;
327 }
328
329 std::string data_root{};
330 std::vector<Particle> particles{};
331 std::vector<mxvk::PointSpriteVertex> vertices{};
332 mxvk::VK_PointSpriteBatch point_batch{};
333 float camera_zoom = 0.09f;
334 float camera_rotation = 356.0f;
335 float global_time = 0.0f;
336 Uint32 last_update_time = 0;
337 bool boost_requested = false;
338 float boost_amount = 0.0f;
339 };
340
341} // namespace example
342
343int main(int argc, char **argv) {
344 try {
345 const Arguments args = proc_args(argc, argv);
346 const std::string root = args.path.empty() ? std::string(STARFIELD_ASSET_DIR) : args.path;
347 example::StarfieldWindow window(root + "/data", args.width, args.height, args.fullscreen, args.enable_vsync);
348 window.loop();
349 } catch (mxvk::Exception &e) {
350 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
351 return EXIT_FAILURE;
352 } catch (ArgException<std::string> &e) {
353 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
354 return EXIT_FAILURE;
355 }
356
357 return EXIT_SUCCESS;
358}
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
void event(SDL_Event &e) override
Handle one SDL event.
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
StarfieldWindow(const std::string &data_root, int width, int height, bool fullscreen, bool enable_vsync)
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
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
VkDevice device
Definition mxvk.hpp:485
size_t getSwapchainImageCount() const noexcept
Get the number of swapchain images currently allocated.
Definition mxvk.hpp:192
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
VkCommandPool command_pool
Definition mxvk.hpp:504
VK_Window()=default
Construct an empty window object.
VkFormat getSwapchainFormat() const noexcept
Get the swapchain color format.
Definition mxvk.hpp:183
int main(void)
Definition main.cpp:7
#define MXVK_VALIDATION
Definition mxvk.hpp:27
Reusable point-sprite renderer for particle and starfield effects.
constexpr std::array< LayerConfig, NUM_LAYERS > LAYERS
Definition starfield.cpp:60
glm::vec4 star_color(StarType type, float brightness)
Definition starfield.cpp:93
float random_float(float min, float max)
Definition starfield.cpp:66
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
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
float y
Definition space.cpp:76
float vx
Definition space.cpp:77
float vy
Definition space.cpp:77
float x
Definition space.cpp:76