4#if defined(MXVK_USE_EIGEN_MATH)
43 using SurfacePtr = std::unique_ptr<SDL_Surface, SurfaceDeleter>;
57 {-1.0f, -1.0f, -1.0f, 1.0f},
58 {1.0f, -1.0f, -1.0f, 1.0f},
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},
83 [[nodiscard]]
static Mesh sphere(
int latitude_segments,
int longitude_segments) {
86 for (
int latitude = 0; latitude <= latitude_segments; ++latitude) {
87 const float phi =
static_cast<float>(latitude) *
mxvk::PI /
static_cast<float>(latitude_segments);
88 const float ring_radius = std::sin(phi);
89 const float y = std::cos(phi);
90 for (
int longitude = 0; longitude <= longitude_segments; ++longitude) {
91 const float theta =
static_cast<float>(longitude) * 2.0f *
mxvk::PI /
static_cast<float>(longitude_segments);
92 mesh.
vertices.emplace_back(ring_radius * std::cos(theta), y, ring_radius * std::sin(theta), 1.0f);
96 const std::size_t row_size =
static_cast<std::size_t
>(longitude_segments + 1);
97 for (
int latitude = 0; latitude < latitude_segments; ++latitude) {
98 for (
int longitude = 0; longitude < longitude_segments; ++longitude) {
99 const std::size_t first =
static_cast<std::size_t
>(latitude) * row_size +
static_cast<std::size_t
>(longitude);
100 const std::size_t second = first + row_size;
101 mesh.
triangles.push_back({{first, second, first + 1}});
102 mesh.
triangles.push_back({{first + 1, second, second + 1}});
120 frame_surface.reset(SDL_CreateSurface(
width,
height, SDL_PIXELFORMAT_RGBA32));
121 if (frame_surface ==
nullptr) {
122 throw mxvk::Exception(std::format(
"3dmath_pong: failed to create framebuffer: {}", SDL_GetError()));
124 frame_format = SDL_GetPixelFormatDetails(frame_surface->format);
125 if (frame_format ==
nullptr) {
126 throw mxvk::Exception(std::format(
"3dmath_pong: failed to query framebuffer format: {}", SDL_GetError()));
128 camera_rotation.BuildXYZ(17.0f, -8.0f, 0.0f);
131 [[nodiscard]] SDL_Surface *
surface()
const {
return frame_surface.get(); }
133 [[nodiscard]]
int width()
const {
return frame_width; }
135 [[nodiscard]]
int height()
const {
return frame_height; }
138 std::ranges::fill(depth_buffer, std::numeric_limits<float>::infinity());
139 for (
int y = 0; y < frame_height; ++y) {
140 const float fraction =
static_cast<float>(y) /
static_cast<float>(frame_height - 1);
141 const int red =
static_cast<int>(4.0f + fraction * 5.0f);
142 const int green =
static_cast<int>(10.0f + fraction * 12.0f);
143 const int blue =
static_cast<int>(24.0f + fraction * 20.0f);
145 for (
int x = 0; x < frame_width; ++x) {
146 put_pixel(x, y, color);
155 std::vector<mxvk::vec4D> camera_vertices(instance.
mesh.
vertices.size());
156 std::vector<mxvk::vec4D> projected_vertices(instance.
mesh.
vertices.size());
157 for (std::size_t index = 0; index < instance.
mesh.
vertices.size(); ++index) {
160 transformed = object_rotation.
MulVec(transformed);
162 transformed = camera_rotation.MulVec(transformed);
164 camera_vertices[index] = transformed;
165 projected_vertices[index] = project(transformed);
168 const mxvk::vec4D light_direction = normalized({-0.35f, -0.65f, -1.0f, 0.0f});
176 const mxvk::vec4D center = (a + b + c) * (1.0f / 3.0f);
177 const mxvk::vec4D view_direction(-center.
x, -center.
y, -center.
z, 0.0f);
178 if (normal.
DotProduct(view_direction) <= 0.0f) {
182 normal = normal * -1.0f;
185 const float diffuse = std::max(0.0f, normal.
DotProduct(light_direction));
186 const float intensity = std::clamp(0.32f + diffuse * 0.68f, 0.0f, 1.0f);
192 static constexpr std::array<std::uint8_t, 10> SEGMENTS = {
204 const std::uint8_t segments = SEGMENTS[
static_cast<std::size_t
>(std::clamp(digit, 0, 9))];
205 const auto horizontal = [
this, scale, color](
int left,
int top) { fill_rectangle(left + scale, top, scale * 3, scale, color); };
206 const auto vertical = [
this, scale, color](
int left,
int top) { fill_rectangle(left, top + scale, scale, scale * 3, color); };
207 if ((segments & 0b1000000U) != 0U)
209 if ((segments & 0b0100000U) != 0U)
210 vertical(x + scale * 4, y);
211 if ((segments & 0b0010000U) != 0U)
212 vertical(x + scale * 4, y + scale * 4);
213 if ((segments & 0b0001000U) != 0U)
214 horizontal(x, y + scale * 7);
215 if ((segments & 0b0000100U) != 0U)
216 vertical(x, y + scale * 4);
217 if ((segments & 0b0000010U) != 0U)
219 if ((segments & 0b0000001U) != 0U)
220 horizontal(x, y + scale * 3);
224 fill_rectangle(frame_width - 17, 8, 3, 12,
mxvk::MXVK_RGB(255, 220, 92));
225 fill_rectangle(frame_width - 10, 8, 3, 12,
mxvk::MXVK_RGB(255, 220, 92));
230 const SDL_PixelFormatDetails *frame_format =
nullptr;
232 int frame_height = 0;
233 std::vector<float> depth_buffer;
244 if (x < 0 || y < 0 || x >= frame_width || y >= frame_height) {
247 auto *row =
static_cast<std::uint8_t *
>(frame_surface->pixels) +
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(frame_surface->pitch);
248 *(
reinterpret_cast<std::uint32_t *
>(row) + x) = map_color(color);
252 for (
int row = 0; row <
height; ++row) {
253 for (
int column = 0; column <
width; ++column) {
254 put_pixel(x + column, y + row, color);
259 [[nodiscard]] mxvk::vec4D project(
const mxvk::vec4D &point)
const {
260 const float scale =
static_cast<float>(std::min(frame_width, frame_height)) * 1.62f;
261 const float z = std::max(point.
z, 0.001f);
263 static_cast<float>(frame_width) * 0.5f + point.
x / z * scale,
264 static_cast<float>(frame_height) * 0.54f - point.
y / z * scale,
270 void rasterize_triangle(
const mxvk::vec4D &a,
const mxvk::vec4D &b,
const mxvk::vec4D &c,
mxvk::MXCOLOR color) {
271 const auto edge = [](
const mxvk::vec4D &first,
const mxvk::vec4D &second,
float x,
float y) {
return (x - first.
x) * (second.y - first.
y) - (y - first.
y) * (second.x - first.
x); };
272 const float area = edge(b, c, a.
x, a.
y);
277 const int min_x = std::clamp(
static_cast<int>(std::floor(std::min({a.
x, b.
x, c.
x}))), 0, frame_width - 1);
278 const int max_x = std::clamp(
static_cast<int>(std::ceil(std::max({a.
x, b.
x, c.
x}))), 0, frame_width - 1);
279 const int min_y = std::clamp(
static_cast<int>(std::floor(std::min({a.
y, b.
y, c.
y}))), 0, frame_height - 1);
280 const int max_y = std::clamp(
static_cast<int>(std::ceil(std::max({a.
y, b.
y, c.
y}))), 0, frame_height - 1);
281 for (
int y = min_y; y <= max_y; ++y) {
282 for (
int x = min_x; x <= max_x; ++x) {
283 const float sample_x =
static_cast<float>(x) + 0.5f;
284 const float sample_y =
static_cast<float>(y) + 0.5f;
285 const float weight_a = edge(b, c, sample_x, sample_y) / area;
286 const float weight_b = edge(c, a, sample_x, sample_y) / area;
287 const float weight_c = edge(a, b, sample_x, sample_y) / area;
288 if (weight_a < 0.0f || weight_b < 0.0f || weight_c < 0.0f) {
291 const float reciprocal_depth = weight_a / a.
z + weight_b / b.
z + weight_c / c.
z;
295 const float depth = 1.0f / reciprocal_depth;
296 const std::size_t pixel_index =
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(frame_width) +
static_cast<std::size_t
>(x);
297 if (depth >= depth_buffer[pixel_index]) {
300 depth_buffer[pixel_index] = depth;
301 put_pixel(x, y, color);
317 reset_ball(random_direction());
322 void set_player_position(
float position) { player_y = std::clamp(position, paddle_minimum_y(), paddle_maximum_y()); }
331 const float target = ball_position.y;
332 const float difference = target - computer_y;
333 const float computer_movement = std::clamp(difference, -AI_SPEED * delta_seconds, AI_SPEED * delta_seconds);
334 computer_y = std::clamp(computer_y + computer_movement, paddle_minimum_y(), paddle_maximum_y());
336 ball_position += ball_velocity * delta_seconds;
339 ball_velocity.y = -std::abs(ball_velocity.y);
342 ball_velocity.y = std::abs(ball_velocity.y);
345 collide_with_paddle(-
PADDLE_X, player_y, 1.0f);
346 collide_with_paddle(
PADDLE_X, computer_y, -1.0f);
360 [[nodiscard]]
int left_score()
const {
return player_score; }
365 static constexpr float AI_SPEED = 1.65f;
366 float player_y = 0.0f;
367 float computer_y = 0.0f;
368 mxvk::vec4D ball_position{0.0f, 0.0f, -0.18f, 1.0f};
369 mxvk::vec4D ball_velocity{1.8f, 0.35f, 0.0f, 0.0f};
370 int player_score = 0;
371 int computer_score = 0;
373 std::mt19937 random_engine;
379 [[nodiscard]]
float random_direction() {
380 std::uniform_int_distribution<int> distribution(0, 1);
381 return distribution(random_engine) == 0 ? -1.0f : 1.0f;
384 void reset_ball(
float horizontal_direction) {
385 std::uniform_real_distribution<float> vertical_distribution(-0.62f, 0.62f);
386 ball_position = {0.0f, 0.0f, -0.18f, 1.0f};
387 ball_velocity = {horizontal_direction, vertical_distribution(random_engine), 0.0f, 0.0f};
388 ball_velocity.Normalize();
389 ball_velocity = ball_velocity * 1.8f;
392 void collide_with_paddle(
float paddle_x,
float paddle_y,
float outgoing_direction) {
393 if (ball_velocity.x * outgoing_direction >= 0.0f) {
398 if (!horizontal_overlap || !vertical_overlap) {
403 const float offset = (ball_position.y - paddle_y) / PADDLE_HALF_HEIGHT;
404 const float current_speed = std::min(3.5f, ball_velocity.Length() * 1.045f);
405 ball_velocity.x = outgoing_direction;
406 ball_velocity.y += offset * 0.72f;
407 ball_velocity.z = 0.0f;
408 ball_velocity.Normalize();
409 ball_velocity = ball_velocity * current_speed;
417 Math3DPongWindow(
bool fullscreen,
bool enable_vsync,
const FramebufferDimensions &framebuffer) :
mxvk::
VK_Window(
"MXVK 3D Math Pong", WINDOW_WIDTH, WINDOW_HEIGHT, fullscreen,
MXVK_VALIDATION, enable_vsync), renderer(framebuffer.width, framebuffer.height), cube_mesh(Mesh::cube()), ball_mesh(Mesh::sphere(8, 12)) {
423 if (
event.type == SDL_EVENT_KEY_DOWN && !
event.key.repeat) {
424 switch (
event.key.key) {
438 if (
event.type == SDL_EVENT_MOUSE_MOTION) {
439 const float normalized = 1.0f - 2.0f *
event.motion.y /
static_cast<float>(std::max(1, output_height));
440 game.set_player_position(normalized * COURT_HALF_HEIGHT);
450 frame_sprite->updateTexture(renderer.surface());
451 frame_sprite->drawSpriteRect(0, 0, output_width, output_height);
455 SoftwareRenderer renderer;
460 std::chrono::steady_clock::time_point previous_frame_time = std::chrono::steady_clock::now();
461 int output_width = WINDOW_WIDTH;
462 int output_height = WINDOW_HEIGHT;
464 void ensure_sprite() {
465 if (frame_sprite !=
nullptr) {
473 const auto now = std::chrono::steady_clock::now();
474 const float delta_seconds = std::min(std::chrono::duration<float>(now - previous_frame_time).count(), 0.05f);
475 previous_frame_time = now;
477 const bool *keyboard = SDL_GetKeyboardState(
nullptr);
478 if (keyboard !=
nullptr) {
479 float movement = 0.0f;
480 if (keyboard[SDL_SCANCODE_W] || keyboard[SDL_SCANCODE_UP]) {
481 movement += 2.7f * delta_seconds;
483 if (keyboard[SDL_SCANCODE_S] || keyboard[SDL_SCANCODE_DOWN]) {
484 movement -= 2.7f * delta_seconds;
486 game.move_player(movement);
488 game.update(delta_seconds);
492 renderer.begin_frame();
493 draw_cuboid({0.0f, 0.0f, 0.28f, 1.0f}, {2.72f, 1.52f, 0.10f, 0.0f},
mxvk::MXVK_RGB(12, 38, 65));
494 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));
495 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));
497 for (
int dash = -4; dash <= 4; ++dash) {
498 draw_cuboid({0.0f,
static_cast<float>(dash) * 0.31f, 0.11f, 1.0f}, {0.025f, 0.09f, 0.025f, 0.0f},
mxvk::MXVK_RGB(112, 151, 180));
501 draw_cuboid({-
PADDLE_X, game.player_position(), -0.02f, 1.0f}, {
PADDLE_HALF_WIDTH,
PADDLE_HALF_HEIGHT, 0.18f, 0.0f},
mxvk::MXVK_RGB(30, 144, 255));
502 draw_cuboid({
PADDLE_X, game.computer_position(), -0.02f, 1.0f}, {
PADDLE_HALF_WIDTH,
PADDLE_HALF_HEIGHT, 0.18f, 0.0f},
mxvk::MXVK_RGB(255, 65, 112));
504 const float rotation =
static_cast<float>(SDL_GetTicks()) * 0.18f;
509 {rotation, rotation * 0.7f, 0.0f, 0.0f},
513 const int score_scale = std::max(2, std::min(renderer.width(), renderer.height()) / 80);
514 const int score_y = score_scale * 3;
515 renderer.draw_digit(renderer.width() / 2 - score_scale * 10, score_y, game.left_score() % 10, score_scale,
mxvk::MXVK_RGB(80, 183, 255));
516 renderer.draw_digit(renderer.width() / 2 + score_scale * 4, score_y, game.right_score() % 10, score_scale,
mxvk::MXVK_RGB(255, 91, 133));
517 if (game.is_paused()) {
518 renderer.draw_pause_indicator();
522 void draw_cuboid(
const mxvk::vec4D &position,
const mxvk::vec4D &scale,
mxvk::MXCOLOR color) {
527 {0.0f, 0.0f, 0.0f, 0.0f},
534int main(
int argc,
char **argv) {
541 std::cerr << std::format(
"mxvk: Exception: {}\n", exception.
text());
544 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