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) :
mxvk::
VK_Window(text, width, height, fullscreen,
MXVK_VALIDATION, enable_vsync) {
145 current_path = path.empty() ? std::string(sprite3d_example_ASSET_DIR) : path;
146 if (current_path ==
".") {
147 current_path = sprite3d_example_ASSET_DIR;
152 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> star_surface(create_star_surface(), SDL_DestroySurface);
153 if (star_surface ==
nullptr) {
157 if (stars_sprite ==
nullptr) {
160 stars_sprite->setDepthTestEnabled(
true);
161 stars_sprite->setDepthWriteEnabled(
false);
162 stars_sprite->setAlphaDiscardThreshold(0.01f);
164 const std::string image_path = current_path +
"/data/saucer.png";
165 std::unique_ptr<SDL_Surface,
decltype(&SDL_DestroySurface)> saucer_surface(load_transparent_saucer(image_path), SDL_DestroySurface);
168 if (sprite ==
nullptr) {
172 sprite->setDepthTestEnabled(
true);
173 sprite->setDepthWriteEnabled(
false);
174 sprite->setAlphaDiscardThreshold(0.01f);
181 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
182 mouse_dragging =
true;
183 last_mouse_x =
static_cast<int>(e.button.x);
184 last_mouse_y =
static_cast<int>(e.button.y);
186 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
187 mouse_dragging =
false;
189 if (e.type == SDL_EVENT_MOUSE_MOTION && mouse_dragging) {
190 const int mouse_x =
static_cast<int>(e.motion.x);
191 const int mouse_y =
static_cast<int>(e.motion.y);
192 const int delta_x = mouse_x - last_mouse_x;
193 const int delta_y = mouse_y - last_mouse_y;
195 yaw_degrees +=
static_cast<float>(delta_x) * mouse_sensitivity;
196 pitch_degrees -=
static_cast<float>(delta_y) * mouse_sensitivity;
197 pitch_degrees = std::clamp(pitch_degrees, -85.0f, 85.0f);
199 last_mouse_x = mouse_x;
200 last_mouse_y = mouse_y;
202 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
203 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y :
static_cast<float>(e.wheel.integer_y);
204 applyWheelZoom(delta);
206 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
214 if (stars_sprite !=
nullptr) {
215 stars_sprite->resize(
this);
217 if (sprite !=
nullptr) {
218 sprite->resize(
this);
223 if (sprite ==
nullptr) {
227 const auto now = std::chrono::steady_clock::now();
228 const float elapsed_seconds = std::chrono::duration<float>(now - start_time).count();
230 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
232 const float yaw_radians = glm::radians(yaw_degrees);
233 const float pitch_radians = glm::radians(pitch_degrees);
234 const glm::vec3 camera_position(camera_distance * std::cos(pitch_radians) * std::sin(yaw_radians), camera_distance * std::sin(pitch_radians), camera_distance * std::cos(pitch_radians) * std::cos(yaw_radians));
235 glm::mat4 view = glm::lookAt(camera_position, glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
236 glm::mat4 proj = glm::perspective(glm::radians(48.0f), aspect, 0.1f, 100.0f);
239 updateStars(elapsed_seconds);
241 if (stars_sprite !=
nullptr) {
242 stars_sprite->updateCamera(imageIndex, view, proj);
244 const float twinkle = 0.78f + 0.22f * std::sin(elapsed_seconds * star.twinkle);
245 const float star_size = star.size * twinkle * (star.is_constellation ? 1.25f : 1.0f);
246 const float alpha = std::clamp(((6.5f - star.magnitude) / 6.5f) * twinkle, 0.0f, 1.0f);
247 const glm::vec3 color = starColor(star.temperature);
248 stars_sprite->drawSprite(glm::vec3(star.x, star.y, star.z), glm::vec2(star_size, star_size), glm::vec4(color, alpha));
250 stars_sprite->render(cmd, imageIndex);
251 stars_sprite->clearQueue();
254 sprite->updateCamera(imageIndex, view, proj);
256 for (
const SaucerFlight &saucer : saucers) {
257 const float orbit_angle = saucer.orbit_phase + elapsed_seconds * saucer.orbit_speed;
258 const float pulse = 1.0f + std::sin(elapsed_seconds * saucer.pulse_speed + saucer.orbit_phase) * saucer.pulse_amount;
259 const float bob = std::sin(elapsed_seconds * saucer.bob_speed + saucer.orbit_phase) * saucer.bob_amplitude;
260 const float roll = orbit_angle * 0.85f + elapsed_seconds * saucer.roll_speed;
262 const glm::vec3 position(saucer.orbit_center.x + std::cos(orbit_angle) * saucer.orbit_radius, saucer.orbit_center.y + bob, saucer.orbit_center.z + std::sin(orbit_angle) * saucer.orbit_radius * 0.62f);
264 const glm::vec2 size(saucer.base_size * pulse, saucer.base_size * pulse * 0.72f);
265 sprite->drawSprite(position, size, saucer.tint, roll);
268 sprite->render(cmd, imageIndex);
269 sprite->clearQueue();
274 static const std::array<SaucerFlight, 5> base_saucers{{
275 {{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}},
276 {{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}},
277 {{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}},
278 {{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}},
279 {{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}},
282 saucers.reserve(base_saucers.size() * 4U);
283 for (
int ring = 0; ring < 4; ++ring) {
284 const float ring_offset =
static_cast<float>(ring) * 0.28f;
285 const float ring_speed_scale = 1.0f +
static_cast<float>(ring) * 0.06f;
286 const float ring_phase_offset =
static_cast<float>(ring) * 0.9f;
287 for (std::size_t i = 0; i < base_saucers.size(); ++i) {
288 SaucerFlight flight = base_saucers[i];
289 flight.orbit_center.x += (
static_cast<float>(i) - 2.0f) * 0.12f;
290 flight.orbit_center.y += (
static_cast<float>(ring) - 1.5f) * 0.06f;
291 flight.orbit_radius += ring_offset +
static_cast<float>(i) * 0.05f;
292 flight.orbit_speed *= ring_speed_scale * 1.35f;
293 flight.orbit_phase += ring_phase_offset +
static_cast<float>(i) * 0.4f;
294 flight.bob_amplitude *= 1.0f +
static_cast<float>(ring) * 0.08f;
295 flight.base_size *= 1.0f +
static_cast<float>(ring) * 0.05f;
296 flight.pulse_amount *= 1.0f +
static_cast<float>(ring) * 0.03f;
297 flight.pulse_speed *= (1.0f +
static_cast<float>(ring) * 0.05f) * 1.15f;
298 flight.roll_speed *= 1.25f;
299 saucers.push_back(flight);
304 void initStars(
int count) {
305 stars.reserve(
static_cast<std::size_t
>(count));
306 for (
int i = 0; i < count; ++i) {
307 stars.push_back(makeStar());
311 void updateStars([[maybe_unused]]
float elapsed_seconds) {
312 constexpr float max_radius = 120.0f;
313 constexpr float min_radius = 20.0f;
314 constexpr float dt = 1.0f / 60.0f;
316 for (StarParticle &star : stars) {
317 star.x += star.vx * dt;
318 star.y += star.vy * dt;
319 star.z += star.vz * dt;
321 const float radius_squared = (star.x * star.x) + (star.y * star.y) + (star.z * star.z);
322 if (radius_squared < (min_radius * min_radius) || radius_squared > (max_radius * max_radius)) {
328 StarParticle makeStar()
const {
330 constexpr float max_radius = 120.0f;
331 constexpr float min_radius = 20.0f;
335 const float radius =
random_float(min_radius, max_radius);
337 star.x = radius * std::sin(phi) * std::cos(theta);
338 star.y = radius * std::sin(phi) * std::sin(theta);
339 star.z = radius * std::cos(phi);
346 if (rarity < 0.05f) {
348 }
else if (rarity < 0.3f) {
354 if (star.magnitude < 3.0f) {
361 star.size = std::clamp(0.52f - star.magnitude * 0.045f, 0.10f, 0.45f);
362 star.is_constellation = (star.magnitude < 3.0f) && (
random_float(0.0f, 1.0f) < 0.3f);
366 static glm::vec3 starColor(
float temperature) {
371 if (temperature < 3700.0f) {
373 g = temperature / 3700.0f * 0.6f;
375 }
else if (temperature < 5200.0f) {
377 g = 0.6f + (temperature - 3700.0f) / 1500.0f * 0.4f;
378 b = (temperature - 3700.0f) / 1500.0f * 0.3f;
379 }
else if (temperature < 6000.0f) {
382 b = (temperature - 5200.0f) / 800.0f * 0.7f;
383 }
else if (temperature < 7500.0f) {
386 b = 0.7f + (temperature - 6000.0f) / 1500.0f * 0.3f;
388 r = 0.7f - (temperature - 7500.0f) / 10000.0f * 0.4f;
389 g = 0.8f + (temperature - 7500.0f) / 10000.0f * 0.2f;
393 return glm::vec3(r, g, b);
396 void applyWheelZoom(
float wheel_y) {
397 if (wheel_y == 0.0f) {
401 const float zoom_step = 1.15f;
402 const float zoom_factor = std::pow(zoom_step, -wheel_y);
403 camera_distance = std::clamp(camera_distance * zoom_factor, min_camera_distance, max_camera_distance);
406 std::string current_path =
".";
407 mxvk::VK_Sprite3D *stars_sprite =
nullptr;
408 mxvk::VK_Sprite3D *sprite =
nullptr;
409 std::vector<StarParticle> stars;
410 std::vector<SaucerFlight> saucers;
411 std::chrono::steady_clock::time_point start_time{std::chrono::steady_clock::now()};
412 float camera_distance = 10.0f;
413 float min_camera_distance = 3.0f;
414 float max_camera_distance = 20.0f;
415 bool mouse_dragging =
false;
416 int last_mouse_x = 0;
417 int last_mouse_y = 0;
418 float yaw_degrees = 0.0f;
419 float pitch_degrees = 10.0f;
420 float mouse_sensitivity = 0.20f;
424int main(
int argc,
char **argv) {
430 std::cerr << std::format(
"mxvk: Exception: {}\n", e.
text());
433 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).