4#if defined(MXVK_USE_EIGEN_MATH)
45 using SurfacePtr = std::unique_ptr<SDL_Surface, SurfaceDeleter>;
59 {-1.0f, -1.0f, -1.0f, 1.0f},
60 {1.0f, -1.0f, -1.0f, 1.0f},
61 {1.0f, 1.0f, -1.0f, 1.0f},
62 {-1.0f, 1.0f, -1.0f, 1.0f},
63 {-1.0f, -1.0f, 1.0f, 1.0f},
64 {1.0f, -1.0f, 1.0f, 1.0f},
65 {1.0f, 1.0f, 1.0f, 1.0f},
66 {-1.0f, 1.0f, 1.0f, 1.0f},
85 [[nodiscard]]
static Mesh sphere(
int latitude_segments,
int longitude_segments) {
88 for (
int latitude = 0; latitude <= latitude_segments; ++latitude) {
89 const float phi =
static_cast<float>(latitude) *
mxvk::PI /
static_cast<float>(latitude_segments);
90 const float ring_radius = std::sin(phi);
91 const float y = std::cos(phi);
92 for (
int longitude = 0; longitude <= longitude_segments; ++longitude) {
93 const float theta =
static_cast<float>(longitude) * 2.0f *
mxvk::PI /
static_cast<float>(longitude_segments);
95 ring_radius * std::cos(theta),
97 ring_radius * std::sin(theta),
102 const std::size_t row_size =
static_cast<std::size_t
>(longitude_segments + 1);
103 for (
int latitude = 0; latitude < latitude_segments; ++latitude) {
104 for (
int longitude = 0; longitude < longitude_segments; ++longitude) {
105 const std::size_t first =
106 static_cast<std::size_t
>(latitude) * row_size +
107 static_cast<std::size_t
>(longitude);
108 const std::size_t second = first + row_size;
109 mesh.
triangles.push_back({{first, second, first + 1}});
110 mesh.
triangles.push_back({{first + 1, second, second + 1}});
128 : frame_width(
width),
130 depth_buffer(static_cast<std::size_t>(
width) * static_cast<std::size_t>(
height)) {
131 frame_surface.reset(SDL_CreateSurface(
width,
height, SDL_PIXELFORMAT_RGBA32));
132 if (frame_surface ==
nullptr) {
133 throw mxvk::Exception(std::format(
"3dmath_pong: failed to create framebuffer: {}", SDL_GetError()));
135 frame_format = SDL_GetPixelFormatDetails(frame_surface->format);
136 if (frame_format ==
nullptr) {
137 throw mxvk::Exception(std::format(
"3dmath_pong: failed to query framebuffer format: {}", SDL_GetError()));
139 camera_rotation.BuildXYZ(17.0f, -8.0f, 0.0f);
143 return frame_surface.get();
155 std::ranges::fill(depth_buffer, std::numeric_limits<float>::infinity());
156 for (
int y = 0; y < frame_height; ++y) {
157 const float fraction =
static_cast<float>(y) /
static_cast<float>(frame_height - 1);
158 const int red =
static_cast<int>(4.0f + fraction * 5.0f);
159 const int green =
static_cast<int>(10.0f + fraction * 12.0f);
160 const int blue =
static_cast<int>(24.0f + fraction * 20.0f);
162 for (
int x = 0; x < frame_width; ++x) {
163 put_pixel(x, y, color);
172 std::vector<mxvk::vec4D> camera_vertices(instance.
mesh.
vertices.size());
173 std::vector<mxvk::vec4D> projected_vertices(instance.
mesh.
vertices.size());
174 for (std::size_t index = 0; index < instance.
mesh.
vertices.size(); ++index) {
181 transformed = object_rotation.
MulVec(transformed);
183 transformed = camera_rotation.MulVec(transformed);
185 camera_vertices[index] = transformed;
186 projected_vertices[index] = project(transformed);
189 const mxvk::vec4D light_direction = normalized({-0.35f, -0.65f, -1.0f, 0.0f});
197 const mxvk::vec4D center = (a + b + c) * (1.0f / 3.0f);
198 const mxvk::vec4D view_direction(-center.
x, -center.
y, -center.
z, 0.0f);
199 if (normal.
DotProduct(view_direction) <= 0.0f) {
203 normal = normal * -1.0f;
206 const float diffuse = std::max(0.0f, normal.
DotProduct(light_direction));
207 const float intensity = std::clamp(0.32f + diffuse * 0.68f, 0.0f, 1.0f);
209 projected_vertices[triangle.
indices[0]],
210 projected_vertices[triangle.
indices[1]],
211 projected_vertices[triangle.
indices[2]],
217 static constexpr std::array<std::uint8_t, 10> SEGMENTS = {
229 const std::uint8_t segments = SEGMENTS[
static_cast<std::size_t
>(std::clamp(digit, 0, 9))];
230 const auto horizontal = [
this, scale, color](
int left,
int top) {
231 fill_rectangle(left + scale, top, scale * 3, scale, color);
233 const auto vertical = [
this, scale, color](
int left,
int top) {
234 fill_rectangle(left, top + scale, scale, scale * 3, color);
236 if ((segments & 0b1000000U) != 0U)
238 if ((segments & 0b0100000U) != 0U)
239 vertical(x + scale * 4, y);
240 if ((segments & 0b0010000U) != 0U)
241 vertical(x + scale * 4, y + scale * 4);
242 if ((segments & 0b0001000U) != 0U)
243 horizontal(x, y + scale * 7);
244 if ((segments & 0b0000100U) != 0U)
245 vertical(x, y + scale * 4);
246 if ((segments & 0b0000010U) != 0U)
248 if ((segments & 0b0000001U) != 0U)
249 horizontal(x, y + scale * 3);
253 fill_rectangle(frame_width - 17, 8, 3, 12,
mxvk::MXVK_RGB(255, 220, 92));
254 fill_rectangle(frame_width - 10, 8, 3, 12,
mxvk::MXVK_RGB(255, 220, 92));
259 const SDL_PixelFormatDetails *frame_format =
nullptr;
261 int frame_height = 0;
262 std::vector<float> depth_buffer;
270 [[nodiscard]] std::uint32_t map_color(
mxvk::MXCOLOR color)
const {
281 if (x < 0 || y < 0 || x >= frame_width || y >= frame_height) {
285 static_cast<std::uint8_t *
>(frame_surface->pixels) +
286 static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(frame_surface->pitch);
287 *(
reinterpret_cast<std::uint32_t *
>(row) + x) = map_color(color);
291 for (
int row = 0; row <
height; ++row) {
292 for (
int column = 0; column <
width; ++column) {
293 put_pixel(x + column, y + row, color);
298 [[nodiscard]] mxvk::vec4D project(
const mxvk::vec4D &point)
const {
299 const float scale =
static_cast<float>(std::min(frame_width, frame_height)) * 1.62f;
300 const float z = std::max(point.
z, 0.001f);
302 static_cast<float>(frame_width) * 0.5f + point.
x / z * scale,
303 static_cast<float>(frame_height) * 0.54f - point.
y / z * scale,
309 void rasterize_triangle(
const mxvk::vec4D &a,
const mxvk::vec4D &b,
const mxvk::vec4D &c,
mxvk::MXCOLOR color) {
310 const auto edge = [](
const mxvk::vec4D &first,
const mxvk::vec4D &second,
float x,
float y) {
311 return (x - first.
x) * (second.y - first.
y) - (y - first.
y) * (second.x - first.
x);
313 const float area = edge(b, c, a.
x, a.
y);
318 const int min_x = std::clamp(
static_cast<int>(std::floor(std::min({a.
x, b.
x, c.
x}))), 0, frame_width - 1);
319 const int max_x = std::clamp(
static_cast<int>(std::ceil(std::max({a.
x, b.
x, c.
x}))), 0, frame_width - 1);
320 const int min_y = std::clamp(
static_cast<int>(std::floor(std::min({a.
y, b.
y, c.
y}))), 0, frame_height - 1);
321 const int max_y = std::clamp(
static_cast<int>(std::ceil(std::max({a.
y, b.
y, c.
y}))), 0, frame_height - 1);
322 for (
int y = min_y; y <= max_y; ++y) {
323 for (
int x = min_x; x <= max_x; ++x) {
324 const float sample_x =
static_cast<float>(x) + 0.5f;
325 const float sample_y =
static_cast<float>(y) + 0.5f;
326 const float weight_a = edge(b, c, sample_x, sample_y) / area;
327 const float weight_b = edge(c, a, sample_x, sample_y) / area;
328 const float weight_c = edge(a, b, sample_x, sample_y) / area;
329 if (weight_a < 0.0f || weight_b < 0.0f || weight_c < 0.0f) {
332 const float reciprocal_depth = weight_a / a.
z + weight_b / b.
z + weight_c / c.
z;
336 const float depth = 1.0f / reciprocal_depth;
337 const std::size_t pixel_index =
338 static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(frame_width) +
339 static_cast<std::size_t
>(x);
340 if (depth >= depth_buffer[pixel_index]) {
343 depth_buffer[pixel_index] = depth;
344 put_pixel(x, y, color);
353 : random_engine(std::random_device{}()) {
363 reset_ball(random_direction());
371 player_y = std::clamp(position, paddle_minimum_y(), paddle_maximum_y());
383 const float target = ball_position.y;
384 const float difference = target - computer_y;
385 const float computer_movement = std::clamp(difference, -AI_SPEED * delta_seconds, AI_SPEED * delta_seconds);
386 computer_y = std::clamp(computer_y + computer_movement, paddle_minimum_y(), paddle_maximum_y());
388 ball_position += ball_velocity * delta_seconds;
391 ball_velocity.y = -std::abs(ball_velocity.y);
394 ball_velocity.y = std::abs(ball_velocity.y);
397 collide_with_paddle(-
PADDLE_X, player_y, 1.0f);
398 collide_with_paddle(
PADDLE_X, computer_y, -1.0f);
412 [[nodiscard]]
int left_score()
const {
return player_score; }
417 static constexpr float AI_SPEED = 1.65f;
418 float player_y = 0.0f;
419 float computer_y = 0.0f;
420 mxvk::vec4D ball_position{0.0f, 0.0f, -0.18f, 1.0f};
421 mxvk::vec4D ball_velocity{1.8f, 0.35f, 0.0f, 0.0f};
422 int player_score = 0;
423 int computer_score = 0;
425 std::mt19937 random_engine;
427 [[nodiscard]]
static float paddle_minimum_y() {
431 [[nodiscard]]
static float paddle_maximum_y() {
435 [[nodiscard]]
float random_direction() {
436 std::uniform_int_distribution<int> distribution(0, 1);
437 return distribution(random_engine) == 0 ? -1.0f : 1.0f;
440 void reset_ball(
float horizontal_direction) {
441 std::uniform_real_distribution<float> vertical_distribution(-0.62f, 0.62f);
442 ball_position = {0.0f, 0.0f, -0.18f, 1.0f};
443 ball_velocity = {horizontal_direction, vertical_distribution(random_engine), 0.0f, 0.0f};
444 ball_velocity.Normalize();
445 ball_velocity = ball_velocity * 1.8f;
448 void collide_with_paddle(
float paddle_x,
float paddle_y,
float outgoing_direction) {
449 if (ball_velocity.x * outgoing_direction >= 0.0f) {
452 const bool horizontal_overlap =
454 const bool vertical_overlap =
456 if (!horizontal_overlap || !vertical_overlap) {
461 const float offset = (ball_position.y - paddle_y) / PADDLE_HALF_HEIGHT;
462 const float current_speed = std::min(3.5f, ball_velocity.Length() * 1.045f);
463 ball_velocity.x = outgoing_direction;
464 ball_velocity.y += offset * 0.72f;
465 ball_velocity.z = 0.0f;
466 ball_velocity.Normalize();
467 ball_velocity = ball_velocity * current_speed;
477 renderer(framebuffer.width, framebuffer.height),
478 cube_mesh(Mesh::cube()),
479 ball_mesh(Mesh::sphere(8, 12)) {
485 if (
event.type == SDL_EVENT_KEY_DOWN && !
event.key.repeat) {
486 switch (
event.key.key) {
500 if (
event.type == SDL_EVENT_MOUSE_MOTION) {
501 const float normalized =
502 1.0f - 2.0f *
event.motion.y /
static_cast<float>(std::max(1, output_height));
503 game.set_player_position(normalized * COURT_HALF_HEIGHT);
513 frame_sprite->updateTexture(renderer.surface());
514 frame_sprite->drawSpriteRect(0, 0, output_width, output_height);
518 SoftwareRenderer renderer;
523 std::chrono::steady_clock::time_point previous_frame_time = std::chrono::steady_clock::now();
524 int output_width = WINDOW_WIDTH;
525 int output_height = WINDOW_HEIGHT;
527 void ensure_sprite() {
528 if (frame_sprite !=
nullptr) {
536 const auto now = std::chrono::steady_clock::now();
537 const float delta_seconds = std::min(
538 std::chrono::duration<float>(now - previous_frame_time).count(),
540 previous_frame_time = now;
542 const bool *keyboard = SDL_GetKeyboardState(
nullptr);
543 if (keyboard !=
nullptr) {
544 float movement = 0.0f;
545 if (keyboard[SDL_SCANCODE_W] || keyboard[SDL_SCANCODE_UP]) {
546 movement += 2.7f * delta_seconds;
548 if (keyboard[SDL_SCANCODE_S] || keyboard[SDL_SCANCODE_DOWN]) {
549 movement -= 2.7f * delta_seconds;
551 game.move_player(movement);
553 game.update(delta_seconds);
557 renderer.begin_frame();
558 draw_cuboid({0.0f, 0.0f, 0.28f, 1.0f}, {2.72f, 1.52f, 0.10f, 0.0f},
mxvk::MXVK_RGB(12, 38, 65));
559 draw_cuboid({0.0f,
COURT_HALF_HEIGHT + 0.07f, 0.08f, 1.0f}, {2.72f, 0.07f, 0.12f, 0.0f},
mxvk::MXVK_RGB(45, 198, 255));
560 draw_cuboid({0.0f, -
COURT_HALF_HEIGHT - 0.07f, 0.08f, 1.0f}, {2.72f, 0.07f, 0.12f, 0.0f},
mxvk::MXVK_RGB(45, 198, 255));
562 for (
int dash = -4; dash <= 4; ++dash) {
564 {0.0f,
static_cast<float>(dash) * 0.31f, 0.11f, 1.0f},
565 {0.025f, 0.09f, 0.025f, 0.0f},
570 {-
PADDLE_X, game.player_position(), -0.02f, 1.0f},
574 {
PADDLE_X, game.computer_position(), -0.02f, 1.0f},
578 const float rotation =
static_cast<float>(SDL_GetTicks()) * 0.18f;
583 {rotation, rotation * 0.7f, 0.0f, 0.0f},
587 const int score_scale = std::max(2, std::min(renderer.width(), renderer.height()) / 80);
588 const int score_y = score_scale * 3;
590 renderer.width() / 2 - score_scale * 10,
592 game.left_score() % 10,
596 renderer.width() / 2 + score_scale * 4,
598 game.right_score() % 10,
601 if (game.is_paused()) {
602 renderer.draw_pause_indicator();
606 void draw_cuboid(
const mxvk::vec4D &position,
const mxvk::vec4D &scale,
mxvk::MXCOLOR color) {
611 {0.0f, 0.0f, 0.0f, 0.0f},
618int main(
int argc,
char **argv) {
627 std::cerr << std::format(
"mxvk: Exception: {}\n", exception.
text());
630 std::cerr << std::format(
"mxvk: Argument Exception: {}\n", exception.
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.
void set_player_position(float position)
float player_position() const
void update(float delta_seconds)
void move_player(float movement)
float computer_position() const
const mxvk::vec4D & ball() const
SoftwareRenderer(int width, int height)
void draw_pause_indicator()
SDL_Surface * surface() const
void draw_mesh(const MeshInstance &instance)
void draw_digit(int x, int y, int digit, int scale, mxvk::MXCOLOR color)
void operator()(SDL_Surface *surface) const
Math3DPongWindow(bool fullscreen, bool enable_vsync, const FramebufferDimensions &framebuffer)
void event(SDL_Event &event) override
Handle one SDL event.
void proc() override
Execute one processing/update step.
Four-by-four homogeneous transform matrix.
void BuildXYZ(float theta_x, float theta_y, float theta_z)
Build an XYZ Euler rotation matrix from angles in degrees.
vec4D MulVec(const vec4D &in) const
Transform a homogeneous 4D vector by this matrix.
void setTextureFilter(VkFilter filter)
Select the hardware filter used when scaling this sprite.
Main Vulkan window wrapper for MXVK.
void loop()
Run the main event/render loop.
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
VkExtent2D swapchain_extent
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.
Four-dimensional float vector used for homogeneous 3D coordinates.
void Normalize()
Normalize the 3D components in place and reset W to 1.
constexpr float DotProduct(const vec4D &v) const
Compute the 3D dot product, ignoring the W component.
void Build(const vec4D &to)
Replace this vector with the direction from this point to to.
Math, geometry, rasterization, and simple software 3D pipeline helpers for MXVK examples.
std::unique_ptr< SDL_Surface, SurfaceDeleter > SurfacePtr
constexpr float BALL_RADIUS
constexpr float COURT_HALF_HEIGHT
constexpr float CAMERA_DISTANCE
constexpr int WINDOW_HEIGHT
constexpr int DEFAULT_FRAME_WIDTH
constexpr float PADDLE_HALF_WIDTH
constexpr int DEFAULT_FRAME_HEIGHT
constexpr int WINDOW_WIDTH
constexpr float COURT_HALF_WIDTH
constexpr float PADDLE_HALF_HEIGHT
Utilities for loading and saving PNG images.
constexpr std::uint8_t color_r(MXCOLOR color)
Extract the red component from a packed ARGB color.
std::uint32_t MXCOLOR
Packed 32-bit color in ARGB byte order.
void BuildTables()
Rebuild the sine and cosine lookup tables.
MXCOLOR shade_color(MXCOLOR color, float intensity)
Scale the RGB channels of a color while preserving alpha.
constexpr std::uint8_t color_g(MXCOLOR color)
Extract the green component from a packed ARGB color.
constexpr MXCOLOR MXVK_RGB(int r, int g, int b)
Build an opaque ARGB color from red, green, and blue components.
constexpr std::uint8_t color_a(MXCOLOR color)
Extract the alpha component from a packed ARGB color.
constexpr float EPSILON
Default tolerance used for floating-point singularity and zero-length checks.
constexpr std::uint8_t color_b(MXCOLOR color)
Extract the blue component from a packed ARGB color.
constexpr float PI
Mathematical constant pi as a single-precision value.
Plain data structure returned by proc_args() with all common libmx2 CLI options.
FramebufferDimensions framebuffer
Software framebuffer size requested by --framebuffer.
bool framebufferSpecified
Whether --framebuffer was provided.
bool fullscreen
Whether fullscreen mode was requested.
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Parsed software framebuffer dimensions.
std::vector< Triangle > triangles
static Mesh sphere(int latitude_segments, int longitude_segments)
std::vector< mxvk::vec4D > vertices
std::array< std::size_t, 3 > indices