MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
point.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 constexpr int DEFAULT_TUXES = 8;
23 constexpr int MAX_TUXES = 1000;
24 constexpr int TUXES_PER_SPACE_PRESS = 25;
25 constexpr int FRAME_COUNT = 16;
26 constexpr float WORLD_HALF_HEIGHT = 4.5f;
27 constexpr float MIN_SIZE = 306.0f;
28 constexpr float MAX_SIZE = 450.0f;
29 constexpr float SPRITE_COLLISION_SCALE = 0.44f;
30 constexpr float SIZE_SCALE_STEP = 1.12f;
31 constexpr float MIN_SIZE_SCALE = 0.6f;
32 constexpr float MAX_SIZE_SCALE = 2.0f;
33 constexpr float POPULATION_GROWTH_INTERVAL = 0.18f;
34 constexpr int INITIAL_PLACEMENT_ATTEMPTS = 200;
35
36 struct TuxSprite {
37 glm::vec2 position{};
38 glm::vec2 velocity{};
39 float z = 0.0f;
40 float size = 0.0f;
41 float frame = 0.0f;
42 float frames_per_second = 0.0f;
43 float wobble_phase = 0.0f;
44 float wobble_speed = 0.0f;
45 float wobble_amount = 0.0f;
46 float tint = 1.0f;
47 };
48
49 [[nodiscard]] float random_float(float min, float max) {
50 static std::random_device rd;
51 static std::default_random_engine engine(rd());
52 std::uniform_real_distribution<float> dist(min, max);
53 return dist(engine);
54 }
55
56} // namespace
57
58namespace example {
59
61 public:
62 PointSpriteWindow(const std::string &data_root, int width, int height, bool fullscreen, bool enable_vsync)
63 : mxvk::VK_Window("MXVK Point Sprite Tux", width, height, fullscreen, MXVK_VALIDATION, enable_vsync),
64 data_root(data_root),
65 tuxes(MAX_TUXES),
66 vertices(MAX_TUXES) {
67 setClearColor(0.0f, 0.0f, 0.0f, 0.0f);
68 reset_tuxes(DEFAULT_TUXES);
69 last_update_time = SDL_GetTicks();
70 }
71
72 ~PointSpriteWindow() override {
73 if (device != VK_NULL_HANDLE) {
74 vkDeviceWaitIdle(device);
75 }
76 point_batch.cleanup();
77 }
78
79 void event(SDL_Event &e) override {
80 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
81 exit();
82 } else if (e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER) && !e.key.repeat) {
83 reset_size_scale();
84 reset_tuxes(DEFAULT_TUXES);
85 } else if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_PAGEUP && !e.key.repeat) {
86 scale_tuxes(SIZE_SCALE_STEP);
87 } else if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_PAGEDOWN && !e.key.repeat) {
88 scale_tuxes(1.0f / SIZE_SCALE_STEP);
89 }
90 }
91
92 void onSwapchainRecreated() override {
93 point_batch.resize(this);
94 }
95
96 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override {
97 if (!ensure_resources()) {
98 return;
99 }
100
101 const Uint32 current_time = SDL_GetTicks();
102 float delta_time = static_cast<float>(current_time - last_update_time) / 1000.0f;
103 last_update_time = current_time;
104 delta_time = std::min(delta_time, 0.1f);
105 global_time += delta_time;
106
107 update_population_growth(delta_time);
108 update_tuxes(delta_time);
109 point_batch.upload_vertices(vertices.data(), active_tuxes);
110 point_batch.update_mvp(image_index, make_mvp());
111 point_batch.render(cmd, image_index);
112 }
113
114 private:
115 bool ensure_resources() {
116 if (point_batch.loaded()) {
117 return true;
118 }
119 if (device == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE || getSwapchainImageCount() == 0U || getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
120 return false;
121 }
122
123 point_batch.load(
124 this,
125 data_root + "/tux.png",
126 data_root + "/pointsprite.vert.spv",
127 data_root + "/pointsprite.frag.spv",
128 vertices.size());
129 point_batch.set_additive_blending(false);
130 point_batch.set_depth_test_enabled(false);
131 point_batch.set_depth_write_enabled(false);
132 last_update_time = SDL_GetTicks();
133 return true;
134 }
135
136 void reset_tuxes(size_t count) {
137 active_tuxes = std::min(count, tuxes.size());
138 for (size_t i = 0; i < active_tuxes; ++i) {
139 randomize_tux(tuxes[i]);
140 place_tux_without_overlap(i);
141 write_vertex(vertices[i], tuxes[i]);
142 }
143 }
144
145 void add_tuxes(size_t count) {
146 const size_t old_count = active_tuxes;
147 active_tuxes = std::min(active_tuxes + count, tuxes.size());
148 for (size_t i = old_count; i < active_tuxes; ++i) {
149 randomize_tux(tuxes[i]);
150 place_tux_without_overlap(i);
151 write_vertex(vertices[i], tuxes[i]);
152 }
153 }
154
155 void update_population_growth(float delta_time) {
156 const bool *keys = SDL_GetKeyboardState(nullptr);
157 const bool space_down = keys != nullptr && keys[SDL_SCANCODE_SPACE];
158 if (!space_down) {
159 space_was_down = false;
160 population_growth_elapsed = 0.0f;
161 return;
162 }
163
164 if (!space_was_down) {
165 add_tuxes(TUXES_PER_SPACE_PRESS);
166 population_growth_elapsed = 0.0f;
167 space_was_down = true;
168 }
169
170 if (active_tuxes >= tuxes.size()) {
171 return;
172 }
173
174 population_growth_elapsed += delta_time;
175 while (population_growth_elapsed >= POPULATION_GROWTH_INTERVAL && active_tuxes < tuxes.size()) {
176 population_growth_elapsed -= POPULATION_GROWTH_INTERVAL;
177 add_tuxes(TUXES_PER_SPACE_PRESS);
178 }
179 }
180
181 void reset_size_scale() {
182 size_scale = 1.0f;
183 }
184
185 void scale_tuxes(float factor) {
186 const float previous_scale = size_scale;
187 size_scale = std::clamp(size_scale * factor, MIN_SIZE_SCALE, MAX_SIZE_SCALE);
188 const float applied_factor = size_scale / previous_scale;
189 for (size_t i = 0; i < active_tuxes; ++i) {
190 tuxes[i].size *= applied_factor;
191 }
192 }
193
194 void randomize_tux(TuxSprite &tux) const {
195 const float angle = random_float(0.0f, 6.28318f);
196 const float speed = random_float(0.75f, 1.85f);
197 tux.velocity = glm::vec2(std::cos(angle), std::sin(angle)) * speed;
198 tux.z = random_float(-0.35f, 0.35f);
199 tux.size = random_float(MIN_SIZE, MAX_SIZE) * size_scale;
200 tux.frame = random_float(0.0f, static_cast<float>(FRAME_COUNT));
201 tux.frames_per_second = random_float(7.0f, 14.0f);
202 tux.wobble_phase = random_float(0.0f, 6.28318f);
203 tux.wobble_speed = random_float(1.4f, 3.4f);
204 tux.wobble_amount = random_float(0.08f, 0.18f);
205 tux.tint = random_float(0.82f, 1.0f);
206 }
207
208 void place_tux_without_overlap(size_t index) {
209 const VkExtent2D extent = getSwapchainExtent();
210 const float aspect = extent.height > 0U ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 16.0f / 9.0f;
211 const float half_width = WORLD_HALF_HEIGHT * aspect;
212 auto &tux = tuxes[index];
213 const float radius = collision_radius(tux);
214
215 for (int attempt = 0; attempt < INITIAL_PLACEMENT_ATTEMPTS; ++attempt) {
216 tux.position.x = random_float(-half_width + radius, half_width - radius);
217 tux.position.y = random_float(-WORLD_HALF_HEIGHT + radius, WORLD_HALF_HEIGHT - radius);
218 if (!overlaps_existing_tux(index)) {
219 return;
220 }
221 }
222
223 const float columns = std::ceil(std::sqrt(static_cast<float>(active_tuxes) * aspect));
224 const float rows = std::ceil(static_cast<float>(active_tuxes) / columns);
225 const float column = std::fmod(static_cast<float>(index), columns);
226 const float row = std::floor(static_cast<float>(index) / columns);
227 tux.position.x = -half_width + ((column + 0.5f) / columns) * half_width * 2.0f;
228 tux.position.y = -WORLD_HALF_HEIGHT + ((row + 0.5f) / rows) * WORLD_HALF_HEIGHT * 2.0f;
229 }
230
231 [[nodiscard]] bool overlaps_existing_tux(size_t index) const {
232 const auto &tux = tuxes[index];
233 const float radius = collision_radius(tux);
234 for (size_t i = 0; i < index; ++i) {
235 const float min_distance = radius + collision_radius(tuxes[i]);
236 if (glm::length(tux.position - tuxes[i].position) < min_distance) {
237 return true;
238 }
239 }
240 return false;
241 }
242
243 void update_tuxes(float delta_time) {
244 const VkExtent2D extent = getSwapchainExtent();
245 const float aspect = extent.height > 0U ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 16.0f / 9.0f;
246 const float half_width = WORLD_HALF_HEIGHT * aspect;
247
248 for (size_t i = 0; i < active_tuxes; ++i) {
249 auto &tux = tuxes[i];
250 tux.position += tux.velocity * delta_time;
251 tux.position.y += std::sin(global_time * tux.wobble_speed + tux.wobble_phase) * tux.wobble_amount * delta_time;
252 tux.frame += tux.frames_per_second * delta_time;
253 if (tux.frame >= static_cast<float>(FRAME_COUNT)) {
254 tux.frame = std::fmod(tux.frame, static_cast<float>(FRAME_COUNT));
255 }
256
257 bounce_from_bounds(tux, half_width, WORLD_HALF_HEIGHT);
258 }
259
260 resolve_collisions();
261
262 for (size_t i = 0; i < active_tuxes; ++i) {
263 bounce_from_bounds(tuxes[i], half_width, WORLD_HALF_HEIGHT);
264 write_vertex(vertices[i], tuxes[i]);
265 }
266 }
267
268 void bounce_from_bounds(TuxSprite &tux, float half_width, float half_height) const {
269 const float radius = collision_radius(tux);
270 if (tux.position.x < -half_width + radius) {
271 tux.position.x = -half_width + radius;
272 tux.velocity.x = std::abs(tux.velocity.x);
273 } else if (tux.position.x > half_width - radius) {
274 tux.position.x = half_width - radius;
275 tux.velocity.x = -std::abs(tux.velocity.x);
276 }
277
278 if (tux.position.y < -half_height + radius) {
279 tux.position.y = -half_height + radius;
280 tux.velocity.y = std::abs(tux.velocity.y);
281 } else if (tux.position.y > half_height - radius) {
282 tux.position.y = half_height - radius;
283 tux.velocity.y = -std::abs(tux.velocity.y);
284 }
285 }
286
287 void resolve_collisions() {
288 for (size_t i = 0; i < active_tuxes; ++i) {
289 for (size_t j = i + 1; j < active_tuxes; ++j) {
290 resolve_collision(tuxes[i], tuxes[j]);
291 }
292 }
293 }
294
295 void resolve_collision(TuxSprite &a, TuxSprite &b) const {
296 glm::vec2 delta = b.position - a.position;
297 float distance = glm::length(delta);
298 const float min_distance = collision_radius(a) + collision_radius(b);
299 if (distance >= min_distance) {
300 return;
301 }
302
303 if (distance < 0.0001f) {
304 const float angle = random_float(0.0f, 6.28318f);
305 delta = glm::vec2(std::cos(angle), std::sin(angle));
306 distance = 1.0f;
307 }
308
309 const glm::vec2 normal = delta / distance;
310 const float overlap = min_distance - distance;
311 a.position -= normal * (overlap * 0.5f);
312 b.position += normal * (overlap * 0.5f);
313
314 const glm::vec2 relative_velocity = b.velocity - a.velocity;
315 const float velocity_along_normal = glm::dot(relative_velocity, normal);
316 if (velocity_along_normal > 0.0f) {
317 return;
318 }
319
320 constexpr float restitution = 0.95f;
321 const float impulse = -(1.0f + restitution) * velocity_along_normal * 0.5f;
322 a.velocity -= impulse * normal;
323 b.velocity += impulse * normal;
324
325 const glm::vec2 tangent(-normal.y, normal.x);
326 const float deflect = random_float(-0.22f, 0.22f);
327 a.velocity += tangent * deflect;
328 b.velocity -= tangent * deflect;
329 limit_speed(a.velocity);
330 limit_speed(b.velocity);
331 }
332
333 void limit_speed(glm::vec2 &velocity) const {
334 const float speed = glm::length(velocity);
335 if (speed < 0.65f) {
336 velocity = glm::normalize(velocity) * 0.65f;
337 } else if (speed > 2.2f) {
338 velocity = glm::normalize(velocity) * 2.2f;
339 }
340 }
341
342 [[nodiscard]] float collision_radius(const TuxSprite &tux) const {
343 const VkExtent2D extent = getSwapchainExtent();
344 const float height = extent.height > 0U ? static_cast<float>(extent.height) : 720.0f;
345 return (tux.size * SPRITE_COLLISION_SCALE / height) * WORLD_HALF_HEIGHT * 2.0f;
346 }
347
348 void write_vertex(mxvk::PointSpriteVertex &vertex, const TuxSprite &tux) const {
349 vertex.position[0] = tux.position.x;
350 vertex.position[1] = tux.position.y;
351 vertex.position[2] = tux.z;
352 vertex.size = tux.size;
353 vertex.color[0] = std::floor(tux.frame) / static_cast<float>(FRAME_COUNT - 1);
354 vertex.color[1] = tux.velocity.x < 0.0f ? 1.0f : 0.0f;
355 vertex.color[2] = tux.tint;
356 vertex.color[3] = 1.0f;
357 }
358
359 [[nodiscard]] glm::mat4 make_mvp() const {
360 const VkExtent2D extent = getSwapchainExtent();
361 const float aspect = extent.height > 0U ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 16.0f / 9.0f;
362 glm::mat4 projection = glm::ortho(-WORLD_HALF_HEIGHT * aspect, WORLD_HALF_HEIGHT * aspect, -WORLD_HALF_HEIGHT, WORLD_HALF_HEIGHT, -1.0f, 1.0f);
363 projection[1][1] *= -1.0f;
364 return projection;
365 }
366
367 std::string data_root{};
368 std::vector<TuxSprite> tuxes{};
369 std::vector<mxvk::PointSpriteVertex> vertices{};
370 mxvk::VK_PointSpriteBatch point_batch{};
371 size_t active_tuxes = 0;
372 float global_time = 0.0f;
373 float population_growth_elapsed = 0.0f;
374 float size_scale = 1.0f;
375 Uint32 last_update_time = 0;
376 bool space_was_down = false;
377 };
378
379} // namespace example
380
381int main(int argc, char **argv) {
382 try {
383 const Arguments args = proc_args(argc, argv);
384 const std::string root = args.path.empty() ? std::string(POINTSPRITE_ASSET_DIR) : args.path;
385 example::PointSpriteWindow window(root + "/data", args.width, args.height, args.fullscreen, args.enable_vsync);
386 window.loop();
387 } catch (mxvk::Exception &e) {
388 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
389 return EXIT_FAILURE;
390 } catch (ArgException<std::string> &e) {
391 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
392 return EXIT_FAILURE;
393 }
394
395 return EXIT_SUCCESS;
396}
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.
Definition point.cpp:79
PointSpriteWindow(const std::string &data_root, int width, int height, bool fullscreen, bool enable_vsync)
Definition point.cpp:62
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
Definition point.cpp:96
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition point.cpp:92
~PointSpriteWindow() override
Definition point.cpp:72
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 float MAX_SIZE_SCALE
Definition point.cpp:32
constexpr int INITIAL_PLACEMENT_ATTEMPTS
Definition point.cpp:34
constexpr float SIZE_SCALE_STEP
Definition point.cpp:30
constexpr float MIN_SIZE
Definition point.cpp:27
constexpr float SPRITE_COLLISION_SCALE
Definition point.cpp:29
constexpr float POPULATION_GROWTH_INTERVAL
Definition point.cpp:33
constexpr int TUXES_PER_SPACE_PRESS
Definition point.cpp:24
constexpr float MAX_SIZE
Definition point.cpp:28
float random_float(float min, float max)
Definition point.cpp:49
constexpr float WORLD_HALF_HEIGHT
Definition point.cpp:26
constexpr float MIN_SIZE_SCALE
Definition point.cpp:31
constexpr int DEFAULT_TUXES
Definition point.cpp:22
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 color[4]
RGBA tint color passed to shader location 2.
float size
Rasterized point size in pixels, passed to shader location 1.
float position[3]
Vertex position passed to shader location 0.