19#include <glm/ext/matrix_clip_space.hpp>
20#include <glm/ext/matrix_transform.hpp>
25 constexpr float k_pi = 3.14159265358979323846f;
28 SDL_Surface *loaded_surface =
mxvk::LoadPNG(png_path.c_str());
29 if (loaded_surface ==
nullptr) {
33 SDL_Surface *rgba_surface = SDL_ConvertSurface(loaded_surface, SDL_PIXELFORMAT_RGBA32);
34 SDL_DestroySurface(loaded_surface);
35 if (rgba_surface ==
nullptr) {
36 throw mxvk::Exception(std::format(
"Failed to convert sprite3D image to RGBA: {}", png_path));
39 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(rgba_surface->format);
40 if (format_details ==
nullptr) {
41 SDL_DestroySurface(rgba_surface);
42 throw mxvk::Exception(
"Failed to query pixel format details for: " + png_path);
45 if (!SDL_LockSurface(rgba_surface)) {
46 SDL_DestroySurface(rgba_surface);
50 auto *pixels =
static_cast<std::uint32_t *
>(rgba_surface->pixels);
51 const int pixel_count = rgba_surface->w * rgba_surface->h;
52 for (
int i = 0; i < pixel_count; ++i) {
57 SDL_GetRGBA(pixels[i], format_details,
nullptr, &r, &g, &b, &a);
59 if ((r | g | b) == 0) {
65 pixels[i] = SDL_MapRGBA(format_details,
nullptr, r, g, b, a);
68 SDL_UnlockSurface(rgba_surface);
73 SDL_Surface *
surface = SDL_CreateSurface(40, 40, SDL_PIXELFORMAT_RGBA32);
78 const SDL_PixelFormatDetails *format_details = SDL_GetPixelFormatDetails(
surface->format);
79 if (format_details ==
nullptr) {
84 if (!SDL_LockSurface(
surface)) {
89 auto *pixels =
static_cast<std::uint32_t *
>(
surface->pixels);
90 for (
int y = 0; y <
surface->h; ++y) {
91 for (
int x = 0; x <
surface->w; ++x) {
92 const float dx = (
static_cast<float>(x) + 0.5f) - (
static_cast<float>(
surface->w) * 0.5f);
93 const float dy = (
static_cast<float>(y) + 0.5f) - (
static_cast<float>(
surface->h) * 0.5f);
94 const float dist = std::sqrt((dx * dx) + (dy * dy)) / (
static_cast<float>(
surface->w) * 0.5f);
95 const float core = std::clamp(1.0f - dist * 1.9f, 0.0f, 1.0f);
96 const float glow = std::clamp(1.0f - dist * 3.8f, 0.0f, 1.0f);
97 const std::uint8_t alpha =
static_cast<std::uint8_t
>(std::lround((core * core * 255.0f) + (glow * 80.0f)));
98 const std::uint8_t brightness =
static_cast<std::uint8_t
>(std::lround((core * 255.0f) + (glow * 150.0f)));
99 pixels[y *
surface->w + x] = SDL_MapRGBA(format_details,
nullptr, brightness, brightness, brightness, alpha);
136 static thread_local std::default_random_engine engine{std::random_device{}()};
137 std::uniform_real_distribution<float> dist(min_value, max_value);
144 ExampleWindow(
const std::string &path,
const std::string &text,
int width,
int height,
bool fullscreen,
bool enable_vsync)
146 current_path = path.empty() ? std::string(sprite3d_example_ASSET_DIR) : path;
147 if (current_path ==
".") {
148 current_path = sprite3d_example_ASSET_DIR;
153 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> star_surface(create_star_surface(), SDL_DestroySurface);
154 if (star_surface ==
nullptr) {
158 if (stars_sprite ==
nullptr) {
161 stars_sprite->setDepthTestEnabled(
true);
162 stars_sprite->setDepthWriteEnabled(
false);
163 stars_sprite->setAlphaDiscardThreshold(0.01f);
165 const std::string image_path = current_path +
"/data/saucer.png";
166 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> saucer_surface(load_transparent_saucer(image_path), SDL_DestroySurface);
169 if (sprite ==
nullptr) {
173 sprite->setDepthTestEnabled(
true);
174 sprite->setDepthWriteEnabled(
false);
175 sprite->setAlphaDiscardThreshold(0.01f);
182 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
183 mouse_dragging =
true;
184 last_mouse_x =
static_cast<int>(e.button.x);
185 last_mouse_y =
static_cast<int>(e.button.y);
187 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
188 mouse_dragging =
false;
190 if (e.type == SDL_EVENT_MOUSE_MOTION && mouse_dragging) {
191 const int mouse_x =
static_cast<int>(e.motion.x);
192 const int mouse_y =
static_cast<int>(e.motion.y);
193 const int delta_x = mouse_x - last_mouse_x;
194 const int delta_y = mouse_y - last_mouse_y;
196 yaw_degrees +=
static_cast<float>(delta_x) * mouse_sensitivity;
197 pitch_degrees -=
static_cast<float>(delta_y) * mouse_sensitivity;
198 pitch_degrees = std::clamp(pitch_degrees, -85.0f, 85.0f);
200 last_mouse_x = mouse_x;
201 last_mouse_y = mouse_y;
203 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
204 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y :
static_cast<float>(e.wheel.integer_y);
205 applyWheelZoom(delta);
207 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
215 if (stars_sprite !=
nullptr) {
216 stars_sprite->resize(
this);
218 if (sprite !=
nullptr) {
219 sprite->resize(
this);
224 if (sprite ==
nullptr) {
228 const auto now = std::chrono::steady_clock::now();
229 const float elapsed_seconds = std::chrono::duration<float>(now - start_time).count();
231 const float aspect = (extent.height > 0U)
232 ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height)
235 const float yaw_radians = glm::radians(yaw_degrees);
236 const float pitch_radians = glm::radians(pitch_degrees);
237 const glm::vec3 camera_position(
238 camera_distance * std::cos(pitch_radians) * std::sin(yaw_radians),
239 camera_distance * std::sin(pitch_radians),
240 camera_distance * std::cos(pitch_radians) * std::cos(yaw_radians));
241 glm::mat4 view = glm::lookAt(camera_position, glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
242 glm::mat4 proj = glm::perspective(glm::radians(48.0f), aspect, 0.1f, 100.0f);
245 updateStars(elapsed_seconds);
247 if (stars_sprite !=
nullptr) {
248 stars_sprite->updateCamera(imageIndex, view, proj);
250 const float twinkle = 0.78f + 0.22f * std::sin(elapsed_seconds * star.twinkle);
251 const float star_size = star.size * twinkle * (star.is_constellation ? 1.25f : 1.0f);
252 const float alpha = std::clamp(((6.5f - star.magnitude) / 6.5f) * twinkle, 0.0f, 1.0f);
253 const glm::vec3 color = starColor(star.temperature);
254 stars_sprite->drawSprite(glm::vec3(star.x, star.y, star.z),
255 glm::vec2(star_size, star_size),
256 glm::vec4(color, alpha));
258 stars_sprite->render(cmd, imageIndex);
259 stars_sprite->clearQueue();
262 sprite->updateCamera(imageIndex, view, proj);
264 for (
const SaucerFlight &saucer : saucers) {
265 const float orbit_angle = saucer.orbit_phase + elapsed_seconds * saucer.orbit_speed;
266 const float pulse = 1.0f + std::sin(elapsed_seconds * saucer.pulse_speed + saucer.orbit_phase) * saucer.pulse_amount;
267 const float bob = std::sin(elapsed_seconds * saucer.bob_speed + saucer.orbit_phase) * saucer.bob_amplitude;
268 const float roll = orbit_angle * 0.85f + elapsed_seconds * saucer.roll_speed;
270 const glm::vec3 position(
271 saucer.orbit_center.x + std::cos(orbit_angle) * saucer.orbit_radius,
272 saucer.orbit_center.y + bob,
273 saucer.orbit_center.z + std::sin(orbit_angle) * saucer.orbit_radius * 0.62f);
275 const glm::vec2 size(saucer.base_size * pulse, saucer.base_size * pulse * 0.72f);
276 sprite->drawSprite(position, size, saucer.tint, roll);
279 sprite->render(cmd, imageIndex);
280 sprite->clearQueue();
285 static const std::array<SaucerFlight, 5> base_saucers{{
286 {{0.0f, 0.0f, 0.0f}, 1.8f, 1.0f, 0.0f, 0.24f, 2.2f, 0.50f, 0.22f, 2.0f, 0.5f, {1.0f, 1.0f, 1.0f, 1.0f}},
287 {{0.0f, 0.18f, 0.0f}, 2.35f, 0.82f, 1.3f, 0.30f, 1.5f, 0.42f, 0.18f, 1.6f, -0.7f, {1.0f, 0.95f, 0.88f, 1.0f}},
288 {{0.0f, -0.16f, 0.0f}, 2.8f, 0.67f, 2.6f, 0.34f, 1.9f, 0.35f, 0.24f, 1.2f, 0.85f, {0.88f, 0.96f, 1.0f, 1.0f}},
289 {{0.0f, 0.08f, 0.0f}, 3.25f, 0.54f, 4.0f, 0.28f, 1.2f, 0.30f, 0.30f, 0.9f, 0.55f, {1.0f, 0.90f, 0.96f, 1.0f}},
290 {{0.0f, 0.0f, 0.0f}, 3.8f, 0.43f, 5.2f, 0.40f, 1.0f, 0.27f, 0.34f, 0.7f, 0.45f, {0.92f, 1.0f, 0.92f, 1.0f}},
293 saucers.reserve(base_saucers.size() * 4U);
294 for (
int ring = 0; ring < 4; ++ring) {
295 const float ring_offset =
static_cast<float>(ring) * 0.28f;
296 const float ring_speed_scale = 1.0f +
static_cast<float>(ring) * 0.06f;
297 const float ring_phase_offset =
static_cast<float>(ring) * 0.9f;
298 for (std::size_t i = 0; i < base_saucers.size(); ++i) {
299 SaucerFlight flight = base_saucers[i];
300 flight.orbit_center.x += (
static_cast<float>(i) - 2.0f) * 0.12f;
301 flight.orbit_center.y += (
static_cast<float>(ring) - 1.5f) * 0.06f;
302 flight.orbit_radius += ring_offset +
static_cast<float>(i) * 0.05f;
303 flight.orbit_speed *= ring_speed_scale * 1.35f;
304 flight.orbit_phase += ring_phase_offset +
static_cast<float>(i) * 0.4f;
305 flight.bob_amplitude *= 1.0f +
static_cast<float>(ring) * 0.08f;
306 flight.base_size *= 1.0f +
static_cast<float>(ring) * 0.05f;
307 flight.pulse_amount *= 1.0f +
static_cast<float>(ring) * 0.03f;
308 flight.pulse_speed *= (1.0f +
static_cast<float>(ring) * 0.05f) * 1.15f;
309 flight.roll_speed *= 1.25f;
310 saucers.push_back(flight);
315 void initStars(
int count) {
316 stars.reserve(
static_cast<std::size_t
>(count));
317 for (
int i = 0; i < count; ++i) {
318 stars.push_back(makeStar());
322 void updateStars([[maybe_unused]]
float elapsed_seconds) {
323 constexpr float max_radius = 120.0f;
324 constexpr float min_radius = 20.0f;
325 constexpr float dt = 1.0f / 60.0f;
327 for (StarParticle &star : stars) {
328 star.x += star.vx * dt;
329 star.y += star.vy * dt;
330 star.z += star.vz * dt;
332 const float radius_squared = (star.x * star.x) + (star.y * star.y) + (star.z * star.z);
333 if (radius_squared < (min_radius * min_radius) || radius_squared > (max_radius * max_radius)) {
339 StarParticle makeStar()
const {
341 constexpr float max_radius = 120.0f;
342 constexpr float min_radius = 20.0f;
346 const float radius =
random_float(min_radius, max_radius);
348 star.x = radius * std::sin(phi) * std::cos(theta);
349 star.y = radius * std::sin(phi) * std::sin(theta);
350 star.z = radius * std::cos(phi);
357 if (rarity < 0.05f) {
359 }
else if (rarity < 0.3f) {
365 if (star.magnitude < 3.0f) {
372 star.size = std::clamp(0.52f - star.magnitude * 0.045f, 0.10f, 0.45f);
373 star.is_constellation = (star.magnitude < 3.0f) && (
random_float(0.0f, 1.0f) < 0.3f);
377 static glm::vec3 starColor(
float temperature) {
382 if (temperature < 3700.0f) {
384 g = temperature / 3700.0f * 0.6f;
386 }
else if (temperature < 5200.0f) {
388 g = 0.6f + (temperature - 3700.0f) / 1500.0f * 0.4f;
389 b = (temperature - 3700.0f) / 1500.0f * 0.3f;
390 }
else if (temperature < 6000.0f) {
393 b = (temperature - 5200.0f) / 800.0f * 0.7f;
394 }
else if (temperature < 7500.0f) {
397 b = 0.7f + (temperature - 6000.0f) / 1500.0f * 0.3f;
399 r = 0.7f - (temperature - 7500.0f) / 10000.0f * 0.4f;
400 g = 0.8f + (temperature - 7500.0f) / 10000.0f * 0.2f;
404 return glm::vec3(r, g, b);
407 void applyWheelZoom(
float wheel_y) {
408 if (wheel_y == 0.0f) {
412 const float zoom_step = 1.15f;
413 const float zoom_factor = std::pow(zoom_step, -wheel_y);
414 camera_distance = std::clamp(camera_distance * zoom_factor, min_camera_distance, max_camera_distance);
417 std::string current_path =
".";
418 mxvk::VK_Sprite3D *stars_sprite =
nullptr;
419 mxvk::VK_Sprite3D *sprite =
nullptr;
420 std::vector<StarParticle> stars;
421 std::vector<SaucerFlight> saucers;
422 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
423 float camera_distance = 10.0f;
424 float min_camera_distance = 3.0f;
425 float max_camera_distance = 20.0f;
426 bool mouse_dragging =
false;
427 int last_mouse_x = 0;
428 int last_mouse_y = 0;
429 float yaw_degrees = 0.0f;
430 float pitch_degrees = 10.0f;
431 float mouse_sensitivity = 0.20f;
435int main(
int argc,
char **argv) {
441 std::cerr << std::format(
"mxvk: Exception: {}\n", e.
text());
444 std::cerr << std::format(
"mxvk: Argument Exception: {}\n", e.
text());
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.
Exception thrown by Argz::proc() on unrecognised or malformed options.
ExampleWindow(const std::string &path, const std::string &text, int width, int height, bool fullscreen, bool enable_vsync)
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
void event(SDL_Event &e) override
Handle one SDL event.
void proc() override
Execute one processing/update step.
Main Vulkan window wrapper for MXVK.
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
void loop()
Run the main event/render loop.
VK_Sprite3D * createSprite3D(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a world-space billboard sprite from a PNG file.
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
void exit()
Request loop termination.
VK_Window()=default
Construct an empty window object.
PNG image loading and saving utilities via SDL3.
SDL_Surface * load_transparent_saucer(const std::string &png_path)
SDL_Surface * create_star_surface()
float random_float(float min_value, float max_value)
Utilities for loading and saving PNG images.
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
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.
bool fullscreen
Whether fullscreen mode was requested.
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
int height
Viewport height in pixels (default: 720).
std::string path
Asset root; proc_args() defaults it to the executable directory.
int width
Viewport width in pixels (default: 1280).