117 StarfieldWindow(
const std::string &data_root,
int width,
int height,
bool fullscreen,
bool enable_vsync)
119 data_root(data_root),
120 particles(NUM_PARTICLES),
121 vertices(NUM_PARTICLES * WARP_TRAIL_SEGMENTS) {
124 last_update_time = SDL_GetTicks();
128 if (
device != VK_NULL_HANDLE) {
131 point_batch.cleanup();
135 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
137 }
else if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
138 boost_requested =
true;
139 }
else if (e.type == SDL_EVENT_KEY_UP && e.key.key == SDLK_SPACE) {
140 boost_requested =
false;
145 point_batch.resize(
this);
149 if (!ensure_starfield_resources()) {
153 const Uint32 current_time = SDL_GetTicks();
154 float delta_time =
static_cast<float>(current_time - last_update_time) / 1000.0f;
155 last_update_time = current_time;
156 if (delta_time > 0.1f) {
159 global_time += delta_time;
160 update_boost(delta_time);
162 const size_t active_vertices = update_particles(delta_time);
163 point_batch.upload_vertices(vertices.data(), active_vertices);
164 point_batch.update_mvp(image_index, make_mvp());
165 point_batch.render(cmd, image_index);
169 bool ensure_starfield_resources() {
170 if (point_batch.
loaded()) {
179 data_root +
"/star.png",
180 data_root +
"/starfield.vert.spv",
181 data_root +
"/starfield.frag.spv",
183 point_batch.set_additive_blending(
true);
184 point_batch.set_depth_test_enabled(
false);
185 point_batch.set_depth_write_enabled(
false);
186 last_update_time = SDL_GetTicks();
190 void init_particles() {
191 int particle_index = 0;
192 for (
int layer = 0; layer <
NUM_LAYERS; ++layer) {
193 for (
int i = 0; i <
LAYERS[layer].count && particle_index <
NUM_PARTICLES; ++i, ++particle_index) {
194 init_particle(particles[
static_cast<size_t>(particle_index)], layer);
195 particles[
static_cast<size_t>(particle_index)].z =
random_float(LAYERS[layer].z_min, 0.0f);
200 void init_particle(Particle &particle,
int layer) {
201 const auto &cfg =
LAYERS[
static_cast<size_t>(layer)];
202 particle.layer = layer;
208 particle.vz =
random_float(0.15f, 0.35f) * cfg.speed_multiplier;
213 particle.texture_index =
random_float(0.0f, 1.0f) > 0.5f ? 1 : 0;
216 const float type_multiplier = particle.type ==
StarType::BRIGHT ? 2.0f : 1.0f;
217 particle.base_size =
random_float(12.0f, 28.0f) * cfg.size_multiplier * type_multiplier;
220 void update_boost(
float delta_time) {
221 const float target = boost_requested ? 1.0f : 0.0f;
222 const float rate = boost_requested ? 4.0f : 2.5f;
223 if (boost_amount < target) {
224 boost_amount = std::min(target, boost_amount + delta_time * rate);
225 }
else if (boost_amount > target) {
226 boost_amount = std::max(target, boost_amount - delta_time * rate);
230 [[nodiscard]]
size_t update_particles(
float delta_time) {
231 const float speed_multiplier = 1.0f + boost_amount * 10.0f;
232 const float forward_boost = boost_amount * 2.85f;
233 const float brightness_boost = 1.0f + boost_amount * 1.15f;
234 const float size_boost = 1.0f + boost_amount * 0.85f;
235 const float warp_trail_amount = glm::smoothstep(0.05f, 1.0f, boost_amount);
236 size_t vertex_count = 0;
239 auto &particle = particles[
static_cast<size_t>(i)];
240 particle.x += particle.vx * delta_time;
241 particle.y += particle.vy * delta_time;
242 particle.z += (particle.vz * speed_multiplier + forward_boost) * delta_time;
244 if (particle.z > 0.0f) {
245 init_particle(particle, particle.layer);
248 if (particle.x > 4.0f) {
251 if (particle.x < -4.0f) {
254 if (particle.y > 4.0f) {
257 if (particle.y < -4.0f) {
261 const float twinkle1 = 0.5f * (1.0f + std::sin(global_time * particle.twinkle + particle.twinkle_phase));
262 const float twinkle2 = 0.3f * (1.0f + std::sin(global_time * particle.pulse_speed * 2.0f + particle.twinkle_phase * 1.5f));
263 const float twinkle_factor = 0.5f + 0.3f * twinkle1 + 0.2f * twinkle2;
265 const auto &cfg =
LAYERS[
static_cast<size_t>(particle.layer)];
266 float depth_factor = 1.0f - (particle.z / cfg.z_min);
267 depth_factor = glm::clamp(depth_factor, 0.3f, 1.0f);
269 const float brightness = particle.life * twinkle_factor * depth_factor * brightness_boost;
270 const glm::vec4 color =
star_color(particle.type, brightness);
271 const float size_pulse = 1.0f + 0.2f * std::sin(global_time * particle.pulse_speed + particle.twinkle_phase);
272 const float size = particle.base_size * size_pulse * depth_factor * size_boost;
273 const float alpha = particle.life * glm::clamp(depth_factor + 0.2f, 0.0f, 1.0f);
275 const float radial_distance = std::sqrt(particle.x * particle.x + particle.y * particle.y);
276 const float radial_factor = radial_distance > 0.0001f ? 1.0f / radial_distance : 0.0f;
277 const float direction_x = particle.x * radial_factor;
278 const float direction_y = particle.y * radial_factor;
279 const float warp_spread = 1.0f + warp_trail_amount * (0.35f + depth_factor * 1.8f);
280 const float display_x = particle.x * warp_spread;
281 const float display_y = particle.y * warp_spread;
283 write_vertex(vertex_count++, display_x, display_y, particle.z, size, color, alpha);
285 if (warp_trail_amount > 0.0f) {
286 const int trail_segments = 1 +
static_cast<int>(warp_trail_amount *
static_cast<float>(
WARP_TRAIL_SEGMENTS - 1));
287 const float trail_spacing = warp_trail_amount * (0.05f + depth_factor * 0.18f);
288 for (
int trail = 1; trail < trail_segments; ++trail) {
289 const float trail_step =
static_cast<float>(trail) /
static_cast<float>(WARP_TRAIL_SEGMENTS - 1);
290 const float trail_alpha = alpha * warp_trail_amount * (1.0f - trail_step) * 0.55f;
291 const float trail_size = size * (1.0f + warp_trail_amount * (0.9f - trail_step * 0.45f));
292 const float trail_x = display_x - direction_x * trail_spacing *
static_cast<float>(trail);
293 const float trail_y = display_y - direction_y * trail_spacing *
static_cast<float>(trail);
294 const float trail_z = particle.z - warp_trail_amount * 0.02f *
static_cast<float>(trail);
295 write_vertex(vertex_count++, trail_x, trail_y, trail_z, trail_size, color, trail_alpha);
303 void write_vertex(
size_t index,
float x,
float y,
float z,
float size,
const glm::vec4 &color,
float alpha) {
304 auto &vertex = vertices[index];
305 vertex.position[0] = x;
306 vertex.position[1] = y;
307 vertex.position[2] = z;
309 vertex.color[0] = color.r;
310 vertex.color[1] = color.g;
311 vertex.color[2] = color.b;
312 vertex.color[3] = alpha;
315 [[nodiscard]] glm::mat4 make_mvp()
const {
317 const float aspect = extent.height > 0U ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
318 glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
319 projection[1][1] *= -1.0f;
321 const glm::vec3 camera_pos(
322 camera_zoom * std::sin(glm::radians(camera_rotation)),
324 camera_zoom * std::cos(glm::radians(camera_rotation)));
325 const glm::mat4 view = glm::lookAt(camera_pos, glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
326 return projection * view;
329 std::string data_root{};
330 std::vector<Particle> particles{};
331 std::vector<mxvk::PointSpriteVertex> vertices{};
332 mxvk::VK_PointSpriteBatch point_batch{};
333 float camera_zoom = 0.09f;
334 float camera_rotation = 356.0f;
335 float global_time = 0.0f;
336 Uint32 last_update_time = 0;
337 bool boost_requested =
false;
338 float boost_amount = 0.0f;