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) {
64 reset_tuxes(DEFAULT_TUXES);
65 last_update_time = SDL_GetTicks();
69 if (
device != VK_NULL_HANDLE) {
72 point_batch.cleanup();
75 void event(SDL_Event &e)
override {
76 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
78 }
else if (e.type == SDL_EVENT_KEY_DOWN && (e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER) && !e.key.repeat) {
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);
91 if (!ensure_resources()) {
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;
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);
109 bool ensure_resources() {
110 if (point_batch.
loaded()) {
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();
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]);
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]);
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];
148 space_was_down =
false;
149 population_growth_elapsed = 0.0f;
153 if (!space_was_down) {
154 add_tuxes(TUXES_PER_SPACE_PRESS);
155 population_growth_elapsed = 0.0f;
156 space_was_down =
true;
159 if (active_tuxes >= tuxes.size()) {
163 population_growth_elapsed += delta_time;
164 while (population_growth_elapsed >= POPULATION_GROWTH_INTERVAL && active_tuxes < tuxes.size()) {
166 add_tuxes(TUXES_PER_SPACE_PRESS);
170 void reset_size_scale() { size_scale = 1.0f; }
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;
181 void randomize_tux(TuxSprite &tux)
const {
184 tux.velocity = glm::vec2(std::cos(angle), std::sin(angle)) * speed;
186 tux.size =
random_float(MIN_SIZE, MAX_SIZE) * size_scale;
187 tux.frame =
random_float(0.0f,
static_cast<float>(FRAME_COUNT));
195 void place_tux_without_overlap(
size_t index) {
197 const float aspect = extent.height > 0U ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 16.0f / 9.0f;
199 auto &tux = tuxes[index];
200 const float radius = collision_radius(tux);
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)) {
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;
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) {
230 void update_tuxes(
float delta_time) {
232 const float aspect = extent.height > 0U ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 16.0f / 9.0f;
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));
244 bounce_from_bounds(tux, half_width, WORLD_HALF_HEIGHT);
247 resolve_collisions();
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]);
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);
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);
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]);
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) {
290 if (distance < 0.0001f) {
292 delta = glm::vec2(std::cos(angle), std::sin(angle));
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);
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) {
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;
312 const glm::vec2 tangent(-normal.y, normal.x);
314 a.velocity += tangent * deflect;
315 b.velocity -= tangent * deflect;
316 limit_speed(a.velocity);
317 limit_speed(b.velocity);
320 void limit_speed(glm::vec2 &velocity)
const {
321 const float speed = glm::length(velocity);
323 velocity = glm::normalize(velocity) * 0.65f;
324 }
else if (speed > 2.2f) {
325 velocity = glm::normalize(velocity) * 2.2f;
329 [[nodiscard]]
float collision_radius(
const TuxSprite &tux)
const {
331 const float height = extent.height > 0U ?
static_cast<float>(extent.height) : 720.0f;
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;
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;
346 [[nodiscard]] glm::mat4 make_mvp()
const {
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;
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;