110 StarfieldWindow(
const std::string &data_root,
int width,
int height,
bool fullscreen,
bool enable_vsync) :
mxvk::
VK_Window(
"MXVK Starfield", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), data_root(data_root), particles(NUM_PARTICLES), vertices(NUM_PARTICLES * WARP_TRAIL_SEGMENTS) {
113 last_update_time = SDL_GetTicks();
117 if (
device != VK_NULL_HANDLE) {
120 point_batch.cleanup();
124 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
126 }
else if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
127 boost_requested =
true;
128 }
else if (e.type == SDL_EVENT_KEY_UP && e.key.key == SDLK_SPACE) {
129 boost_requested =
false;
136 if (!ensure_starfield_resources()) {
140 const Uint32 current_time = SDL_GetTicks();
141 float delta_time =
static_cast<float>(current_time - last_update_time) / 1000.0f;
142 last_update_time = current_time;
143 if (delta_time > 0.1f) {
146 global_time += delta_time;
147 update_boost(delta_time);
149 const size_t active_vertices = update_particles(delta_time);
150 point_batch.upload_vertices(vertices.data(), active_vertices);
151 point_batch.update_mvp(image_index, make_mvp());
152 point_batch.render(cmd, image_index);
156 bool ensure_starfield_resources() {
157 if (point_batch.
loaded()) {
164 point_batch.load(
this, data_root +
"/star.png", data_root +
"/starfield.vert.spv", data_root +
"/starfield.frag.spv", vertices.size());
165 point_batch.set_additive_blending(
true);
166 point_batch.set_depth_test_enabled(
false);
167 point_batch.set_depth_write_enabled(
false);
168 last_update_time = SDL_GetTicks();
172 void init_particles() {
173 int particle_index = 0;
174 for (
int layer = 0; layer <
NUM_LAYERS; ++layer) {
175 for (
int i = 0; i <
LAYERS[layer].count && particle_index <
NUM_PARTICLES; ++i, ++particle_index) {
176 init_particle(particles[
static_cast<size_t>(particle_index)], layer);
177 particles[
static_cast<size_t>(particle_index)].z =
random_float(LAYERS[layer].z_min, 0.0f);
182 void init_particle(Particle &particle,
int layer) {
183 const auto &cfg =
LAYERS[
static_cast<size_t>(layer)];
184 particle.layer = layer;
190 particle.vz =
random_float(0.15f, 0.35f) * cfg.speed_multiplier;
195 particle.texture_index =
random_float(0.0f, 1.0f) > 0.5f ? 1 : 0;
198 const float type_multiplier = particle.type ==
StarType::BRIGHT ? 2.0f : 1.0f;
199 particle.base_size =
random_float(12.0f, 28.0f) * cfg.size_multiplier * type_multiplier;
202 void update_boost(
float delta_time) {
203 const float target = boost_requested ? 1.0f : 0.0f;
204 const float rate = boost_requested ? 4.0f : 2.5f;
205 if (boost_amount < target) {
206 boost_amount = std::min(target, boost_amount + delta_time * rate);
207 }
else if (boost_amount > target) {
208 boost_amount = std::max(target, boost_amount - delta_time * rate);
212 [[nodiscard]]
size_t update_particles(
float delta_time) {
213 const float speed_multiplier = 1.0f + boost_amount * 10.0f;
214 const float forward_boost = boost_amount * 2.85f;
215 const float brightness_boost = 1.0f + boost_amount * 1.15f;
216 const float size_boost = 1.0f + boost_amount * 0.85f;
217 const float warp_trail_amount = glm::smoothstep(0.05f, 1.0f, boost_amount);
218 size_t vertex_count = 0;
221 auto &particle = particles[
static_cast<size_t>(i)];
222 particle.x += particle.vx * delta_time;
223 particle.y += particle.vy * delta_time;
224 particle.z += (particle.vz * speed_multiplier + forward_boost) * delta_time;
226 if (particle.z > 0.0f) {
227 init_particle(particle, particle.layer);
230 if (particle.x > 4.0f) {
233 if (particle.x < -4.0f) {
236 if (particle.y > 4.0f) {
239 if (particle.y < -4.0f) {
243 const float twinkle1 = 0.5f * (1.0f + std::sin(global_time * particle.twinkle + particle.twinkle_phase));
244 const float twinkle2 = 0.3f * (1.0f + std::sin(global_time * particle.pulse_speed * 2.0f + particle.twinkle_phase * 1.5f));
245 const float twinkle_factor = 0.5f + 0.3f * twinkle1 + 0.2f * twinkle2;
247 const auto &cfg =
LAYERS[
static_cast<size_t>(particle.layer)];
248 float depth_factor = 1.0f - (particle.z / cfg.z_min);
249 depth_factor = glm::clamp(depth_factor, 0.3f, 1.0f);
251 const float brightness = particle.life * twinkle_factor * depth_factor * brightness_boost;
252 const glm::vec4 color =
star_color(particle.type, brightness);
253 const float size_pulse = 1.0f + 0.2f * std::sin(global_time * particle.pulse_speed + particle.twinkle_phase);
254 const float size = particle.base_size * size_pulse * depth_factor * size_boost;
255 const float alpha = particle.life * glm::clamp(depth_factor + 0.2f, 0.0f, 1.0f);
257 const float radial_distance = std::sqrt(particle.x * particle.x + particle.y * particle.y);
258 const float radial_factor = radial_distance > 0.0001f ? 1.0f / radial_distance : 0.0f;
259 const float direction_x = particle.x * radial_factor;
260 const float direction_y = particle.y * radial_factor;
261 const float warp_spread = 1.0f + warp_trail_amount * (0.35f + depth_factor * 1.8f);
262 const float display_x = particle.x * warp_spread;
263 const float display_y = particle.y * warp_spread;
265 write_vertex(vertex_count++, display_x, display_y, particle.z, size, color, alpha);
267 if (warp_trail_amount > 0.0f) {
268 const int trail_segments = 1 +
static_cast<int>(warp_trail_amount *
static_cast<float>(
WARP_TRAIL_SEGMENTS - 1));
269 const float trail_spacing = warp_trail_amount * (0.05f + depth_factor * 0.18f);
270 for (
int trail = 1; trail < trail_segments; ++trail) {
271 const float trail_step =
static_cast<float>(trail) /
static_cast<float>(WARP_TRAIL_SEGMENTS - 1);
272 const float trail_alpha = alpha * warp_trail_amount * (1.0f - trail_step) * 0.55f;
273 const float trail_size = size * (1.0f + warp_trail_amount * (0.9f - trail_step * 0.45f));
274 const float trail_x = display_x - direction_x * trail_spacing *
static_cast<float>(trail);
275 const float trail_y = display_y - direction_y * trail_spacing *
static_cast<float>(trail);
276 const float trail_z = particle.z - warp_trail_amount * 0.02f *
static_cast<float>(trail);
277 write_vertex(vertex_count++, trail_x, trail_y, trail_z, trail_size, color, trail_alpha);
285 void write_vertex(
size_t index,
float x,
float y,
float z,
float size,
const glm::vec4 &color,
float alpha) {
286 auto &vertex = vertices[index];
287 vertex.position[0] = x;
288 vertex.position[1] = y;
289 vertex.position[2] = z;
291 vertex.color[0] = color.r;
292 vertex.color[1] = color.g;
293 vertex.color[2] = color.b;
294 vertex.color[3] = alpha;
297 [[nodiscard]] glm::mat4 make_mvp()
const {
299 const float aspect = extent.height > 0U ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
300 glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
301 projection[1][1] *= -1.0f;
303 const glm::vec3 camera_pos(camera_zoom * std::sin(glm::radians(camera_rotation)), 0.0f, camera_zoom * std::cos(glm::radians(camera_rotation)));
304 const glm::mat4 view = glm::lookAt(camera_pos, glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
305 return projection * view;
308 std::string data_root{};
309 std::vector<Particle> particles{};
310 std::vector<mxvk::PointSpriteVertex> vertices{};
311 mxvk::VK_PointSpriteBatch point_batch{};
312 float camera_zoom = 0.09f;
313 float camera_rotation = 356.0f;
314 float global_time = 0.0f;
315 Uint32 last_update_time = 0;
316 bool boost_requested =
false;
317 float boost_amount = 0.0f;