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