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