62 PointSpriteWindow(
const std::string &data_root,
int width,
int height,
bool fullscreen,
bool enable_vsync)
68 reset_tuxes(DEFAULT_TUXES);
69 last_update_time = SDL_GetTicks();
73 if (
device != VK_NULL_HANDLE) {
76 point_batch.cleanup();
79 void event(SDL_Event &e)
override {
80 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
82 }
else if (e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER) && !e.key.repeat) {
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);
93 point_batch.resize(
this);
97 if (!ensure_resources()) {
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;
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);
115 bool ensure_resources() {
116 if (point_batch.
loaded()) {
125 data_root +
"/tux.png",
126 data_root +
"/pointsprite.vert.spv",
127 data_root +
"/pointsprite.frag.spv",
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();
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]);
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]);
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];
159 space_was_down =
false;
160 population_growth_elapsed = 0.0f;
164 if (!space_was_down) {
165 add_tuxes(TUXES_PER_SPACE_PRESS);
166 population_growth_elapsed = 0.0f;
167 space_was_down =
true;
170 if (active_tuxes >= tuxes.size()) {
174 population_growth_elapsed += delta_time;
175 while (population_growth_elapsed >= POPULATION_GROWTH_INTERVAL && active_tuxes < tuxes.size()) {
177 add_tuxes(TUXES_PER_SPACE_PRESS);
181 void reset_size_scale() {
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;
194 void randomize_tux(TuxSprite &tux)
const {
197 tux.velocity = glm::vec2(std::cos(angle), std::sin(angle)) * speed;
199 tux.size =
random_float(MIN_SIZE, MAX_SIZE) * size_scale;
200 tux.frame =
random_float(0.0f,
static_cast<float>(FRAME_COUNT));
208 void place_tux_without_overlap(
size_t index) {
210 const float aspect = extent.height > 0U ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 16.0f / 9.0f;
212 auto &tux = tuxes[index];
213 const float radius = collision_radius(tux);
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)) {
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;
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) {
243 void update_tuxes(
float delta_time) {
245 const float aspect = extent.height > 0U ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 16.0f / 9.0f;
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));
257 bounce_from_bounds(tux, half_width, WORLD_HALF_HEIGHT);
260 resolve_collisions();
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]);
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);
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);
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]);
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) {
303 if (distance < 0.0001f) {
305 delta = glm::vec2(std::cos(angle), std::sin(angle));
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);
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) {
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;
325 const glm::vec2 tangent(-normal.y, normal.x);
327 a.velocity += tangent * deflect;
328 b.velocity -= tangent * deflect;
329 limit_speed(a.velocity);
330 limit_speed(b.velocity);
333 void limit_speed(glm::vec2 &velocity)
const {
334 const float speed = glm::length(velocity);
336 velocity = glm::normalize(velocity) * 0.65f;
337 }
else if (speed > 2.2f) {
338 velocity = glm::normalize(velocity) * 2.2f;
342 [[nodiscard]]
float collision_radius(
const TuxSprite &tux)
const {
344 const float height = extent.height > 0U ?
static_cast<float>(extent.height) : 720.0f;
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;
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;
359 [[nodiscard]] glm::mat4 make_mvp()
const {
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;
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;