commit 314ae8b57392bfdca75afb07bb5fe9a659cd02d0 Author: Maciej Samborski Date: Thu Sep 4 09:08:36 2025 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b673f5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.zig-cache/ +zig-out/ + +libs/libopenwindow.so diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..00cab81 --- /dev/null +++ b/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe_mod.addIncludePath(.{ .cwd_relative = "./deps/"}); + + const exe = b.addExecutable(.{ + .name = "game", + .root_module = exe_mod, + }); + + exe.root_module.addCMacro("GL_GLEXT_PROTOTYPES", "1"); + + exe.linkLibC(); + exe.linkSystemLibrary("OpenGl"); + exe.linkSystemLibrary("X11"); + exe.linkSystemLibrary("GLX"); + exe.linkSystemLibrary("gallium"); + exe.addLibraryPath(.{ .cwd_relative = "./libs/" }); + exe.linkSystemLibrary("openwindow"); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..c2a7da7 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,86 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .game, + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0x232b318ceacc9a92, // Changing this has security and trust implications. + + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.14.1", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/deps/openwindow.h b/deps/openwindow.h new file mode 100644 index 0000000..9e4181b --- /dev/null +++ b/deps/openwindow.h @@ -0,0 +1,227 @@ +#ifndef _OPEN_WINDOW_H_ +#define _OPEN_WINDOW_H_ + +#define WINDOW_DEFAULT_FPS 60 + +#define WINDOW_KEY_COUNT 256 + +#define WINDOW_MOUSE_BUTTON_COUNT 5 +#define WINDOW_MOUSE_BUTTON1 (Button1) +#define WINDOW_MOUSE_BUTTON2 (Button2) +#define WINDOW_MOUSE_BUTTON3 (Button3) +#define WINDOW_MOUSE_BUTTON4 (Button4) +#define WINDOW_MOUSE_BUTTON5 (Button5) + +#include +#include +#include +#include +#include + +int gladLoadGL(void); + +#include "GL/gl.h" + +#ifndef _WIN32 + +#define Window X11Window +#include +#include +#include +#include +#undef Window + +typedef struct { + GLXFBConfig config; + GLXContext context; +} GLX; + +typedef struct { + Display * display; + X11Window window; + Colormap cmap; + long eventMask; +} X11; + +typedef struct { + struct { + char current[WINDOW_KEY_COUNT]; + char last[WINDOW_KEY_COUNT]; + } keyboard; + struct { + bool current[WINDOW_MOUSE_BUTTON_COUNT]; + bool last[WINDOW_MOUSE_BUTTON_COUNT]; + + int x; + int y; + } mouse; +} WindowIOState; + +typedef struct { + struct timespec * t1; + struct timespec * t2; + double dt; +} WindowDeltaTime; + +typedef struct Window Window; + +typedef struct Window { + WindowIOState io; + GLX glx; + X11 x; + WindowDeltaTime dt; + uint32_t fps; + int width; + int height; +} Window; + +#define WINDOW_KEY_Q ("q") +#define WINDOW_KEY_W ("w") +#define WINDOW_KEY_E ("e") +#define WINDOW_KEY_R ("r") +#define WINDOW_KEY_T ("t") +#define WINDOW_KEY_Y ("y") +#define WINDOW_KEY_U ("u") +#define WINDOW_KEY_I ("i") +#define WINDOW_KEY_O ("o") +#define WINDOW_KEY_P ("p") +#define WINDOW_KEY_A ("a") +#define WINDOW_KEY_S ("s") +#define WINDOW_KEY_D ("d") +#define WINDOW_KEY_F ("f") +#define WINDOW_KEY_G ("g") +#define WINDOW_KEY_H ("h") +#define WINDOW_KEY_J ("j") +#define WINDOW_KEY_K ("k") +#define WINDOW_KEY_L ("l") +#define WINDOW_KEY_Z ("z") +#define WINDOW_KEY_X ("x") +#define WINDOW_KEY_C ("c") +#define WINDOW_KEY_V ("v") +#define WINDOW_KEY_B ("b") +#define WINDOW_KEY_N ("n") +#define WINDOW_KEY_M ("m") + +#define WINDOW_KEY_1 ("1") +#define WINDOW_KEY_2 ("2") +#define WINDOW_KEY_3 ("3") +#define WINDOW_KEY_4 ("4") +#define WINDOW_KEY_5 ("5") +#define WINDOW_KEY_6 ("6") +#define WINDOW_KEY_7 ("7") +#define WINDOW_KEY_8 ("8") +#define WINDOW_KEY_9 ("9") +#define WINDOW_KEY_0 ("0") + +#define WINDOW_KEY_ESC ("Escape") +#define WINDOW_KEY_SPACE ("space") + +bool windowKeyPressed(Window * window, char * key); +bool windowKeyReleased(Window * window, char * key); +bool windowKeyHeld(Window * window, char * key); + +bool windowKeyShift(Window * window); +bool windowKeyCtrl(Window * window); +bool windowKeyAlt(Window * window); + +bool windowMousePressed(Window * window, int button); +bool windowMouseHeld(Window * window, int button); +bool windowMouseReleased(Window * window, int button); + +#else + +#include + +typedef struct { + HINSTANCE instance; + HDC display; + HWND window; + HGLRC context; +} WGL; + +typedef struct { + BYTE current[WINDOW_KEY_COUNT]; + BYTE last[WINDOW_KEY_COUNT]; +} WindowIOState; + +typedef struct { + LARGE_INTEGER t1; + LARGE_INTEGER t2; + LARGE_INTEGER freq; + double dt; +} WindowDeltaTime; + +typedef struct Window Window; + +typedef struct Window { + WindowIOState io; + WGL wgl; + WindowDeltaTime dt; + uint32_t fps; + int width; + int height; +} Window; + +#define WINDOW_KEY_Q ('Q') +#define WINDOW_KEY_W ('W') +#define WINDOW_KEY_E ('E') +#define WINDOW_KEY_R ('R') +#define WINDOW_KEY_T ('T') +#define WINDOW_KEY_Y ('Y') +#define WINDOW_KEY_U ('U') +#define WINDOW_KEY_I ('I') +#define WINDOW_KEY_O ('O') +#define WINDOW_KEY_P ('P') +#define WINDOW_KEY_A ('A') +#define WINDOW_KEY_S ('S') +#define WINDOW_KEY_D ('D') +#define WINDOW_KEY_F ('F') +#define WINDOW_KEY_G ('G') +#define WINDOW_KEY_H ('H') +#define WINDOW_KEY_J ('J') +#define WINDOW_KEY_K ('K') +#define WINDOW_KEY_L ('L') +#define WINDOW_KEY_Z ('Z') +#define WINDOW_KEY_X ('X') +#define WINDOW_KEY_C ('C') +#define WINDOW_KEY_V ('V') +#define WINDOW_KEY_B ('B') +#define WINDOW_KEY_N ('N') +#define WINDOW_KEY_M ('M') + +#define WINDOW_KEY_1 ('1') +#define WINDOW_KEY_2 ('2') +#define WINDOW_KEY_3 ('3') +#define WINDOW_KEY_4 ('4') +#define WINDOW_KEY_5 ('5') +#define WINDOW_KEY_6 ('6') +#define WINDOW_KEY_7 ('7') +#define WINDOW_KEY_8 ('8') +#define WINDOW_KEY_9 ('9') +#define WINDOW_KEY_0 ('0') + +#define WINDOW_KEY_ESC (VK_ESCAPE) +#define WINDOW_KEY_SPACE (VK_SPACE) + +bool windowKeyPressed(Window * window, int key); +bool windowKeyReleased(Window * window, int key); +bool windowKeyHeld(Window * window, int key); + +bool windowKeyShift(Window * window); +bool windowKeyCtrl(Window * window); +bool windowKeyAlt(Window * window); + +#endif // _WIN32 + +Window * openWindow(const char * name, size_t width, size_t height); +void windowSetFps(Window * window, uint32_t fps); +double windowGetDeltaTime(Window * window); +void windowGetSize(Window * window, int * width, int * height); +void windowGetMousePosition(Window * window, int * x, int * y); +void windowDraw(Window * window); +void windowHandleEvents(Window * window); +void closeWindow(Window * window); + +int getOpenGLProcs(void); + +#endif // _OPEN_WINDOW_H_ diff --git a/libs/LIBS.md b/libs/LIBS.md new file mode 100644 index 0000000..8b16bfc --- /dev/null +++ b/libs/LIBS.md @@ -0,0 +1,3 @@ +# Libs + +1. [openwindow](https://gitea.hskl.org/MaciejSamborski/openwindow) diff --git a/src/camera.zig b/src/camera.zig new file mode 100644 index 0000000..bb28734 --- /dev/null +++ b/src/camera.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const math = @import("math.zig"); + +const Camera = @This(); + +position: math.Vec2, +scale: f32, + +pub fn init(position: math.Vec2) Camera { + return .{ + .position = position, + .scale = 1.0, + }; +} diff --git a/src/common.zig b/src/common.zig new file mode 100644 index 0000000..c240660 --- /dev/null +++ b/src/common.zig @@ -0,0 +1,15 @@ +pub const Color = struct { + r: f32, + g: f32, + b: f32, + a: f32, + + pub fn fromHex(hex: u32) Color { + return Color{ + .r = @as(f32, @floatFromInt(((hex >> (8 * 3)) & 0xFF))) / @as(f32, @floatFromInt(0xFF)), + .g = @as(f32, @floatFromInt(((hex >> (8 * 2)) & 0xFF))) / @as(f32, @floatFromInt(0xFF)), + .b = @as(f32, @floatFromInt(((hex >> (8 * 1)) & 0xFF))) / @as(f32, @floatFromInt(0xFF)), + .a = @as(f32, @floatFromInt(((hex >> (8 * 0)) & 0xFF))) / @as(f32, @floatFromInt(0xFF)), + }; + } +}; diff --git a/src/draw.zig b/src/draw.zig new file mode 100644 index 0000000..0d9fc6b --- /dev/null +++ b/src/draw.zig @@ -0,0 +1,360 @@ +const std = @import("std"); +const common = @import("common.zig"); +const math = @import("math.zig"); + +const gl = @cImport(@cInclude("GL/gl.h")); + +const Draw = @This(); +const Renderer = @import("renderer.zig"); + +renderer: *Renderer, + +allocator: std.mem.Allocator, +drawables: struct { + general: struct { + triangles: Drawables(gl.GL_TRIANGLES), + triangleFan: Drawables(gl.GL_TRIANGLE_FAN), + }, + instanced: struct { + triangles: DrawablesInstanced(gl.GL_TRIANGLES), + triangleFan: DrawablesInstanced(gl.GL_TRIANGLE_FAN), + }, +}, + +attribs: struct { + general: std.ArrayList(Attribute.General), + instanced: std.ArrayList(Attribute.Instanced), +}, + +pub fn init(renderer: *Renderer) !Draw { + return Draw{ + .renderer = renderer, + .allocator = renderer.allocator, + .drawables = .{ + .general = .{ + .triangles = .init(renderer.allocator), + .triangleFan = .init(renderer.allocator), + }, + .instanced = .{ + .triangles = .init(renderer.allocator), + .triangleFan = .init(renderer.allocator), + }, + }, + .attribs = .{ + .general = .init(renderer.allocator), + .instanced = .init(renderer.allocator), + }, + }; +} + +pub const Attribute = union(enum) { + instanced: Instanced, + general: General, + + pub const AttributeOptions = struct { size: gl.GLint, type: gl.GLenum, normalized: gl.GLboolean }; + + const AttributeData = struct { + name: []const u8, + loc: gl.GLuint, + size: gl.GLint, + type: gl.GLenum, + normalized: gl.GLboolean, + offset: usize, + }; + + pub const Instanced = struct { + data: AttributeData, + + pub var offset: usize = 0; + + fn new(draw: *Draw, name: []const u8, options: AttributeOptions) Instanced { + offset += @intCast(options.size); + + return .{ + .data = .{ + .name = name, + .loc = @intCast(gl.glGetAttribLocation(draw.renderer.program, @ptrCast(name))), + .size = options.size, + .type = options.type, + .normalized = options.normalized, + .offset = offset - @as(usize, @intCast(options.size)), + }, + }; + } + }; + + pub const General = struct { + data: AttributeData, + + pub var offset: usize = 0; + + fn new(draw: *Draw, name: []const u8, options: AttributeOptions) General { + offset += @intCast(options.size); + + return .{ + .data = .{ + .name = name, + .loc = @intCast(gl.glGetAttribLocation(draw.renderer.program, @ptrCast(name))), + .size = options.size, + .type = options.type, + .normalized = options.normalized, + .offset = offset - @as(usize, @intCast(options.size)), + }, + }; + } + }; +}; + +pub fn addAttrib(draw: *Draw, name: []const u8, options: Attribute.AttributeOptions, instanced: bool) !void { + if (instanced) + try draw.attribs.instanced.append(.new(draw, name, options)) + else + try draw.attribs.general.append( .new(draw, name, options)) + ; +} + +pub fn getAttrib(draw: *Draw, name: []const u8) ?*Attribute { + for (draw.attribs.items) |attr| { + if (std.mem.eql(u8, name, attr.name)) { + return &attr; + } + } + + return null; +} + +pub const Uniform = struct { + loc: gl.GLuint, +}; + +pub fn getUniform(draw: *Draw, name: []const u8) Uniform { + return .{ + .loc = @intCast(gl.glGetUniformLocation(draw.renderer.program, @ptrCast(name))), + }; +} + +pub fn Drawables(mode: gl.GLenum) type { + return struct { + data: switch (mode) { + gl.GL_TRIANGLES => std.ArrayList(f32), + gl.GL_TRIANGLE_FAN => std.ArrayList(std.ArrayList(f32)), + else => unreachable, + }, + drawMode: gl.GLenum = mode, + + fn init(allocator: std.mem.Allocator) @This() { + return .{ + .data = .init(allocator), + }; + } + }; +} + +pub fn DrawablesInstanced(mode: gl.GLenum) type { + return struct { + data: std.ArrayList(std.ArrayList(f32)), + instanceData: std.ArrayList(std.ArrayList(f32)), + drawMode: gl.GLenum = mode, + + fn init(allocator: std.mem.Allocator) @This() { + return .{ + .data = .init(allocator), + .instanceData = .init(allocator), + }; + } + }; +} + +pub fn drawRectangle(draw: *Draw, corners: [4]@Vector(2, f32), depth: f32, color: common.Color) !void { + try draw.drawables.general.triangles.data.appendSlice(&.{ + corners[3][0], corners[3][1], depth, color.r, color.g, color.b, color.a, + corners[0][0], corners[0][1], depth, color.r, color.g, color.b, color.a, + corners[1][0], corners[1][1], depth, color.r, color.g, color.b, color.a, + + corners[3][0], corners[3][1], depth, color.r, color.g, color.b, color.a, + corners[1][0], corners[1][1], depth, color.r, color.g, color.b, color.a, + corners[2][0], corners[2][1], depth, color.r, color.g, color.b, color.a, + }); +} + +pub fn drawRectangleInstanced(draw: *Draw, corners: [4]@Vector(2, f32), offsets: []const @Vector(2, f32), depth: f32, color: common.Color) !void { + var data = std.ArrayList(f32).init(draw.allocator); + var instanceData = std.ArrayList(f32).init(draw.allocator); + + try data.appendSlice(&.{ + corners[3][0], corners[3][1], depth, color.r, color.g, color.b, color.a, + corners[0][0], corners[0][1], depth, color.r, color.g, color.b, color.a, + corners[1][0], corners[1][1], depth, color.r, color.g, color.b, color.a, + + corners[3][0], corners[3][1], depth, color.r, color.g, color.b, color.a, + corners[1][0], corners[1][1], depth, color.r, color.g, color.b, color.a, + corners[2][0], corners[2][1], depth, color.r, color.g, color.b, color.a, + }); + + for (offsets) |offset| { + try instanceData.appendSlice(&.{ + offset[0], offset[1], depth + }); + } + + try draw.drawables.instanced.triangles.data.append(data); + try draw.drawables.instanced.triangles.instanceData.append(instanceData); +} + +pub fn drawTriangle(draw: *Draw, corners: [3]@Vector(2, f32), depth: f32, color: common.Color) !void { + try draw.drawables.general.triangles.data.appendSlice(&.{ + corners[0][0], corners[0][1], depth, color.r, color.g, color.b, color.a, + corners[1][0], corners[1][1], depth, color.r, color.g, color.b, color.a, + corners[2][0], corners[2][1], depth, color.r, color.g, color.b, color.a, + }); +} + +pub fn drawTriangleInstanced(draw: *Draw, corners: [3]@Vector(2, f32), offsets: []const @Vector(2, f32), depth: f32, color: common.Color) !void { + var data = std.ArrayList(f32).init(draw.allocator); + var instanceData = std.ArrayList(f32).init(draw.allocator); + + try data.appendSlice(&.{ + corners[0][0], corners[0][1], depth, color.r, color.g, color.b, color.a, + corners[1][0], corners[1][1], depth, color.r, color.g, color.b, color.a, + corners[2][0], corners[2][1], depth, color.r, color.g, color.b, color.a, + }); + + for (offsets) |offset| { + try instanceData.appendSlice(&.{ + offset[0], offset[1], depth + }); + } + + try draw.drawables.instanced.triangles.data.append(data); + try draw.drawables.instanced.triangles.instanceData.append(instanceData); +} + +pub const CirclePrecission = 64; + +pub fn drawCircle( + draw: *Draw, + origin: @Vector(2, f32), + depth: f32, + radius: f32, + arc: f32, + wound: enum { clockwise, counterclockwise }, + color: common.Color +) !void { + var data: std.ArrayList(f32) = .init(draw.allocator); + + try data.appendSlice(&.{ + origin[0], origin[1], depth, color.r, color.g, color.b, color.a, + }); + + const r = if (wound == .clockwise) radius else -radius; + + for (0..@intFromFloat(std.math.round((CirclePrecission + 1) * arc))) |i| { + var v = math.Vec2{ 0, r }; + + math.rotateVector2(&v, 2.0 * std.math.pi / @as(f32, @floatFromInt(CirclePrecission)) * @as(f32, @floatFromInt(i))); + + v += origin; + + try data.appendSlice(&.{ + v[0], v[1], depth, color.r, color.g, color.b, color.a, + }); + } + + try draw.drawables.general.triangleFan.data.append(data); +} + +pub fn drawCircleInstanced( + draw: *Draw, + origin: @Vector(2, f32), + offsets: []const @Vector(2, f32), + depth: f32, + radius: f32, + arc: f32, + wound: enum { clockwise, counterclockwise }, + color: common.Color +) !void { + var data = std.ArrayList(f32).init(draw.allocator); + var instanceData = std.ArrayList(f32).init(draw.allocator); + + try data.appendSlice(&.{ + origin[0], origin[1], depth, color.r, color.g, color.b, color.a, + }); + + for (0..@intFromFloat(std.math.round((CirclePrecission + 1) * arc))) |i| { + var v = math.Vec2{ 0, radius }; + math.rotateVector2(&v, 2.0 * std.math.pi / @as(f32, @floatFromInt(CirclePrecission)) * @as(f32, @floatFromInt(i))); + + if (wound == .counterclockwise) v *= @splat(-1); + + v += origin; + + try data.appendSlice(&.{ + v[0], v[1], depth, color.r, color.g, color.b, color.a, + }); + } + + for (offsets) |offset| { + try instanceData.appendSlice(&.{ + offset[0], offset[1], depth + }); + } + + try draw.drawables.instanced.triangleFan.data.append(data); + try draw.drawables.instanced.triangleFan.instanceData.append(instanceData); +} + +pub fn drawLine(draw: *Draw, points: [2]@Vector(2, f32), width: f32, depth: f32, color: common.Color) !void { + const dx = points[1][0] - points[0][0]; + const dy = points[1][1] - points[0][1]; + + var angle = std.math.atan2(dy, dx); + if (angle < 0) angle += std.math.pi * 2; + const inverse = @mod((angle + std.math.pi), (std.math.pi * 2)); + + const ps1 = math.findLinePoints(points[0], width, angle); + const ps2 = math.findLinePoints(points[1], width, inverse); + + try draw.drawables.general.triangles.data.appendSlice(&.{ + ps1.p1[0], ps1.p1[1], depth, color.r, color.g, color.b, color.a, + ps1.p2[0], ps1.p2[1], depth, color.r, color.g, color.b, color.a, + ps2.p1[0], ps2.p1[1], depth, color.r, color.g, color.b, color.a, + + ps2.p1[0], ps2.p1[1], depth, color.r, color.g, color.b, color.a, + ps2.p2[0], ps2.p2[1], depth, color.r, color.g, color.b, color.a, + ps1.p1[0], ps1.p1[1], depth, color.r, color.g, color.b, color.a, + }); +} + +pub fn drawLineInstanced(draw: *Draw, points: [2]@Vector(2, f32), offsets: []const @Vector(2, f32), width: f32, depth: f32, color: common.Color) !void { + var data = std.ArrayList(f32).init(draw.allocator); + var instanceData = std.ArrayList(f32).init(draw.allocator); + + const dx = points[1][0] - points[0][0]; + const dy = points[1][1] - points[0][1]; + + var angle = std.math.atan2(dy, dx); + if (angle < 0) angle += std.math.pi * 2; + const inverse = @mod((angle + std.math.pi), (std.math.pi * 2)); + + const ps1 = math.findLinePoints(points[0], width, angle); + const ps2 = math.findLinePoints(points[1], width, inverse); + + try data.appendSlice(&.{ + ps1.p1[0], ps1.p1[1], depth, color.r, color.g, color.b, color.a, + ps1.p2[0], ps1.p2[1], depth, color.r, color.g, color.b, color.a, + ps2.p1[0], ps2.p1[1], depth, color.r, color.g, color.b, color.a, + + ps2.p1[0], ps2.p1[1], depth, color.r, color.g, color.b, color.a, + ps2.p2[0], ps2.p2[1], depth, color.r, color.g, color.b, color.a, + ps1.p1[0], ps1.p1[1], depth, color.r, color.g, color.b, color.a, + }); + + for (offsets) |offset| { + try instanceData.appendSlice(&.{ + offset[0], offset[1], depth + }); + } + + try draw.drawables.instanced.triangles.data.append(data); + try draw.drawables.instanced.triangles.instanceData.append(instanceData); +} diff --git a/src/game.zig b/src/game.zig new file mode 100644 index 0000000..7d5a6c4 --- /dev/null +++ b/src/game.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +const Renderer = @import("renderer.zig"); +const Camera = @import("camera.zig"); +const Draw = @import("draw.zig"); +const Grid = @import("grid.zig"); + +const Game = @This(); + +const World = struct { + grid: Grid, + size: @Vector(2, usize), + + fn init(allocator: std.mem.Allocator, width: usize, height: usize) !World { + return World{ + .grid = try Grid.init(allocator, width, height), + .size = .{ width, height }, + }; + } +}; + +const Player = struct { + position: PlayerPosition = .{ 0, 0 }, + + const PlayerPosition = @Vector(2, f32); + const default: Player = .{}; +}; + +camera: Camera, +draw: Draw, + +world: World, +player: Player, + +pub fn init(renderer: *Renderer, width: u32, height: u32) !Game { + return .{ + .camera = .init(.{ @as(f32, @floatFromInt(renderer.width.*))/2.0, @as(f32, @floatFromInt(renderer.height.*))/2.0 }), + .draw = try .init(renderer), + .world = try World.init(renderer.allocator, width, height), + .player = .default, + }; +} diff --git a/src/grid.zig b/src/grid.zig new file mode 100644 index 0000000..660c085 --- /dev/null +++ b/src/grid.zig @@ -0,0 +1,59 @@ +const std = @import("std"); +const common = @import("common.zig"); + +const Grid = @This(); + +pub const Cell = struct { + const Face = struct { + color: common.Color, + }; + + topFace: Face = .{ + .color = common.Color.fromHex(0xFF0000FF), + }, + leftFace: Face = .{ + .color = common.Color.fromHex(0x00FF00FF), + }, + rightFace: Face = .{ + .color = common.Color.fromHex(0x0000FFFF), + }, +}; + +pub const CellMapKey = struct { + x: usize, + y: usize, +}; + +pub fn coord(x: usize, y: usize) CellMapKey { + return .{ + .x = x, + .y = y, + }; +} + +const CellMap = std.AutoHashMap(CellMapKey, Cell); + +map: CellMap, + +pub fn init(allocator: std.mem.Allocator, width: usize, height: usize) !Grid { + var map: CellMap = CellMap.init(allocator); + try map.ensureTotalCapacity(@intCast(width * height)); + + for (0..width) |w| { + for (0..height) |h| { + try map.put(coord(w, h), Cell{}); + } + } + + return .{ + .map = map, + }; +} + +pub fn get(grid: *Grid, key: CellMapKey) ?Cell { + return grid.map.get(key); +} + +pub fn put(grid: *Grid, key: CellMapKey, cell: Cell) !void { + try grid.map.put(key, cell); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..7d6220e --- /dev/null +++ b/src/main.zig @@ -0,0 +1,148 @@ +const std = @import("std"); +const gl = @cImport(@cInclude("GL/gl.h")); +const ow = @cImport(@cInclude("openwindow.h")); + +const Renderer = @import("renderer.zig"); +const Game = @import("game.zig"); +const Grid = @import("grid.zig"); +const Draw = @import("draw.zig"); + +const Transform = @import("transform.zig"); + +const math = @import("math.zig"); +const common = @import("common.zig"); + +pub fn main() !void { + const window: *ow.Window = ow.openWindow("game", 1920, 1080); + defer ow.closeWindow(window); + + var aa = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer aa.deinit(); + const allocator = aa.allocator(); + + var renderer = try Renderer.init(allocator, @ptrCast(&window.width), @ptrCast(&window.height)); + + var g = try Game.init(&renderer, 4, 4); + + var d = try Draw.init(&renderer); + try addCommonAttribs(&d); + + try d.drawLineInstanced( + .{ .{ 0, 1004 }, .{ 0, -4 }}, + &.{ + .{ 0, 0 }, + .{ 100, 0 }, + .{ 200, 0 }, + .{ 300, 0 }, + .{ 400, 0 }, + .{ 500, 0 }, + .{ 600, 0 }, + .{ 700, 0 }, + .{ 800, 0 }, + .{ 900, 0 }, + .{ 1000, 0 }, + }, + 4, + 0, + common.Color.fromHex(0x000000FF) + ); + + try d.drawLineInstanced( + .{ .{ 1004, 0 }, .{ -4, 0 }}, + &.{ + .{ 0, 0 }, + .{ 0, 100 }, + .{ 0, 200 }, + .{ 0, 300 }, + .{ 0, 400 }, + .{ 0, 500 }, + .{ 0, 600 }, + .{ 0, 700 }, + .{ 0, 800 }, + .{ 0, 900 }, + .{ 0, 1000 }, + }, + 4, + 0, + common.Color.fromHex(0x000000FF) + ); + + try d.drawRectangle(.{ + .{ 0, 0 }, + .{ 100, 0 }, + .{ 100, 100 }, + .{ 0, 100 }, + }, 0, common.Color.fromHex(0xFF0000FF), + ); + + try d.drawCircle(.{ 50, 50 }, + 0.01, + 50, + 1.0, + .clockwise, + common.Color.fromHex(0x00FF00FF) + ); + + var globalTransform = Transform.init(); + + while (!ow.windowKeyPressed(window, @ptrCast(@constCast(ow.WINDOW_KEY_ESC)))) { + ow.windowHandleEvents(window); + defer ow.windowDraw(window); + + handleInput(window, &g); + + const ortho = math.Mat4x4.orthographic(0.0, @floatFromInt(window.width), 0.0, @floatFromInt(window.height), -1.0, 1.0); + globalTransform.set(.orthographic, ortho); + globalTransform.set(.translation, math.Mat4x4.translation(g.camera.position)); + globalTransform.set(.scale, math.Mat4x4.scale(g.camera.scale)); + gl.glUniformMatrix4fv(@intCast(d.getUniform("uTransform").loc), 1, gl.GL_FALSE, &globalTransform.get().data[0][0]); + try renderer.renderDraw(&d); + } +} + +fn addCommonAttribs(d: *Draw) !void { + + // General + + try d.addAttrib("aPos", .{ + .size = 3, + .type = gl.GL_FLOAT, + .normalized = gl.GL_FALSE, + }, false); + try d.addAttrib("aColor", .{ + .size = 4, + .type = gl.GL_FLOAT, + .normalized = gl.GL_FALSE, + }, false); + + // Instanced + + try d.addAttrib("aInstanceOffset", .{ + .size = 3, + .type = gl.GL_FLOAT, + .normalized = gl.GL_FALSE, + }, true); +} + +fn handleInput(window: *ow.Window, game: *Game) void { + const dt: f32 = @floatCast(ow.windowGetDeltaTime(window)); + const speed = 300; + + if (ow.windowKeyHeld(window, @ptrCast(@constCast(ow.WINDOW_KEY_W)))) + game.camera.position[1] -= speed*dt; + + if (ow.windowKeyHeld(window, @ptrCast(@constCast(ow.WINDOW_KEY_S)))) + game.camera.position[1] += speed*dt; + + if (ow.windowKeyHeld(window, @ptrCast(@constCast(ow.WINDOW_KEY_A)))) + game.camera.position[0] += speed*dt; + + if (ow.windowKeyHeld(window, @ptrCast(@constCast(ow.WINDOW_KEY_D)))) + game.camera.position[0] -= speed*dt; + + if (ow.windowKeyHeld(window, @ptrCast(@constCast(ow.WINDOW_KEY_Q)))) + game.camera.scale += 2*dt; + + if (ow.windowKeyHeld(window, @ptrCast(@constCast(ow.WINDOW_KEY_E)))) + game.camera.scale -= 2*dt; +} diff --git a/src/math.zig b/src/math.zig new file mode 100644 index 0000000..47b2b19 --- /dev/null +++ b/src/math.zig @@ -0,0 +1,107 @@ +const std = @import("std"); + +pub const Mat4x4 = struct { + const Self = @This(); + + data: [4][4]f32, + + pub fn init(data: [16]f32) Mat4x4 { + return .{ + .data = .{ + data[0..4].*, + data[4..8].*, + data[8..12].*, + data[12..16].*, + }, + }; + } + + pub fn mult(matrix: *Self, other: Mat4x4) void { + var result: [4][4]f32 = undefined; + + for (&result, 0..) |*col, j| { + for (col, 0..) |*elem, i| { + elem.* = matrix.data[0][i] * other.data[j][0] + + matrix.data[1][i] * other.data[j][1] + + matrix.data[2][i] * other.data[j][2] + + matrix.data[3][i] * other.data[j][3]; + } + } + + matrix.data = result; + } + + pub fn mults(matrix: *Self, scalar: f32) void { + for (&matrix.data) |*col| { + for (col) |*row| { + row.* *= scalar; + } + } + } + + pub fn transform(matrix: *const Self, point: *Vec2) void { + const x = point[0]; + const y = point[1]; + + point[0] = matrix.data[0][0]*x + matrix.data[1][0]*y + matrix.data[2][0]*0.0 + matrix.data[3][0]*1.0; + point[1] = matrix.data[0][1]*x + matrix.data[1][1]*y + matrix.data[2][1]*0.0 + matrix.data[3][1]*1.0; + } + + pub const zero: Mat4x4 = .init(@splat(0)); + pub const id: Mat4x4 = .init(.{ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + }); + + pub fn orthographic(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) Mat4x4 { + return .init(.{ + 2 / (right - left), 0, 0, 0, + 0, 2 / (top - bottom), 0, 0, + 0, 0, -2 / (far - near), 0, + -((right + left)/(right - left)), -((top + bottom)/(top - bottom)), -((far + near)/(far - near)), 1 + }); + } + + pub fn translation(offset: Vec2) Mat4x4 { + return .init(.{ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + offset[0], offset[1], 0.0, 1.0, + }); + } + + pub fn scale(scalar: f32) Mat4x4 { + return .init(.{ + scalar, 0.0, 0.0, 0.0, + 0.0, scalar, 0.0, 0.0, + 0.0, 0.0, scalar, 0.0, + 0.0, 0.0, 0.0, 1.0, + }); + } +}; + +pub const Vec2 = @Vector(2, f32); + +pub fn rotateVector2(vec: *Vec2, angle: f32) void { + const x: f32 = vec[0]; + const y: f32 = vec[1]; + vec[0] = x * std.math.cos(angle) - y * std.math.sin(angle); + vec[1] = x * std.math.sin(angle) + y * std.math.cos(angle); +} + +pub fn vector2FromRotation(angle: f32) Vec2 { + return .{ + std.math.cos(angle), + std.math.sin(angle), + }; +} + +pub fn findLinePoints(point: Vec2, width: f32, angle: f32) struct { p1: Vec2, p2: Vec2 } { + const p1 = Vec2 { point[0] - width * std.math.sin(angle), point[1] + width * std.math.cos(angle) }; + const p2 = Vec2 { point[0] + width * std.math.sin(angle), point[1] - width * std.math.cos(angle) }; + + return .{ .p1 = p1, .p2 = p2 }; +} diff --git a/src/renderer.zig b/src/renderer.zig new file mode 100644 index 0000000..ecfa981 --- /dev/null +++ b/src/renderer.zig @@ -0,0 +1,225 @@ +const std = @import("std"); +const Game = @import("game.zig"); +const Grid = @import("grid.zig"); +const Draw = @import("draw.zig"); +const gl = @cImport(@cInclude("GL/gl.h")); + +const Renderer = @This(); + +const ShaderCompileError = error{ + COMPILATION_FAILED, +}; + +const opengl = struct { + const shaderKind = enum { + VERTEX, + FRAGMENT, + }; + + fn createShader(allocator: std.mem.Allocator, source: [:0]const u8, kind: shaderKind) !gl.GLuint { + const stype: c_uint = switch (kind) { + .VERTEX => gl.GL_VERTEX_SHADER, + .FRAGMENT => gl.GL_FRAGMENT_SHADER, + }; + + const shader = gl.glCreateShader(stype); + gl.glShaderSource(shader, 1, @ptrCast(&source.ptr), null); + gl.glCompileShader(shader); + + var len: c_int = 0; + var info: []u8 = undefined; + + gl.glGetShaderiv(shader, gl.GL_INFO_LOG_LENGTH, &len); + + if (len > 0) { + info = try allocator.alloc(u8, @intCast(len)); + gl.glGetShaderInfoLog(shader, len, &len, @ptrCast(info)); + std.debug.print("{s}\n", .{info}); + allocator.free(info); + + return ShaderCompileError.COMPILATION_FAILED; + } + + return shader; + } + + fn createProgram(allocator: std.mem.Allocator, vertex: [:0]const u8, fragment: [:0]const u8) !gl.GLuint { + const program = gl.glCreateProgram(); + + const vertexShader = try createShader(allocator, vertex, .VERTEX); + const fragmentShader = try createShader(allocator, fragment, .FRAGMENT); + + gl.glAttachShader(program, vertexShader); + gl.glAttachShader(program, fragmentShader); + + gl.glLinkProgram(program); + + return program; + } + + fn init(allocator: std.mem.Allocator) !struct { program: c_uint, vao: c_uint, vbo: c_uint, vboi: c_uint } { + const program = try opengl.createProgram(allocator, vss, fss); + + var vao: gl.GLuint = 0; + gl.glCreateVertexArrays(1, @ptrCast(&vao)); + + var vbo: gl.GLuint = 0; + gl.glCreateBuffers(1, @ptrCast(&vbo)); + + var vboi: gl.GLuint = 0; + gl.glCreateBuffers(1, @ptrCast(&vboi)); + + gl.glBindVertexArray(vao); + + gl.glUseProgram(program); + + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA); + gl.glEnable(gl.GL_BLEND); + + gl.glEnable(gl.GL_DEPTH_TEST); + gl.glDepthFunc(gl.GL_LESS); + + // TODO: enable + //gl.glEnable(gl.GL_CULL_FACE); + //gl.glCullFace(gl.GL_BACK); + + return .{ + .program = program, + .vao = vao, + .vbo = vbo, + .vboi = vboi + }; + } +}; + +const vss = @embedFile("shaders/main.vs"); +const fss = @embedFile("shaders/main.fs"); + +allocator: std.mem.Allocator, + +program: gl.GLuint = 0, +vao: gl.GLuint = 0, +vbo: gl.GLuint = 0, +vboi: gl.GLuint = 0, +width: *u32 = undefined, +height: *u32 = undefined, + +pub fn init(allocator: std.mem.Allocator, width: *u32, height: *u32) !Renderer { + // destructuring no worky :( + const initState = try opengl.init(allocator); + + return .{ + .allocator = allocator, + .program = initState.program, + .vao = initState.vao, + .vbo = initState.vbo, + .vboi = initState.vboi, + .width = width, + .height = height, + }; +} + +//pub fn renderGame(renderer: *const Renderer, allocator: std.mem.Allocator, game: *Game) !void { +// _ = game; +// +// var data = std.ArrayList(f32).init(allocator); +// +// try data.appendSlice(&[_]f32{ -0.8, -0.8, 1.0, 0.0, 0.0, -0.3, -0.8, 0.0, 1.0, 0.0, -0.8, -0.3, 0.0, 0.0, 1.0 }); +// +// var acc: usize = 0; +// for (renderer.attribs.items) |attrib| { +// acc += @intCast(attrib.size); +// } +// +// gl.glBindBuffer(gl.GL_ARRAY_BUFFER, renderer.vbo); +// gl.glBufferData(gl.GL_ARRAY_BUFFER, @as(gl.GLsizeiptr, @intCast(data.items.len)) * @sizeOf(f32), @ptrCast(data.items.ptr), gl.GL_STATIC_DRAW); +// +// for (renderer.attribs.items) |attrib| { +// gl.glEnableVertexAttribArray(attrib.loc); +// gl.glVertexAttribPointer(attrib.loc, attrib.size, attrib.type, attrib.normalized, @intCast(renderer.stride * @sizeOf(f32)), @ptrFromInt(attrib.offset)); +// } +// +// gl.glClearColor(0.8, 0.8, 0.8, 1.0); +// gl.glClear(gl.GL_COLOR_BUFFER_BIT); +// +// gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3); +//} + +pub fn renderDraw(renderer: *Renderer, draw: *Draw) !void { + gl.glViewport(0, 0, @intCast(renderer.width.*), @intCast(renderer.height.*)); + gl.glClearColor(1.0, 1.0, 1.0, 1.0); + gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT); + + gl.glUniform1i(@intCast(draw.getUniform("uInstanced").loc), 0); + + inline for (@typeInfo(@TypeOf(draw.drawables.general)).@"struct".fields) |field| { + const drawables = @field(draw.drawables.general, field.name); + renderer.render(draw, drawables); + } + + gl.glUniform1i(@intCast(draw.getUniform("uInstanced").loc), 1); + + inline for (@typeInfo(@TypeOf(draw.drawables.instanced)).@"struct".fields) |field| { + const drawables = @field(draw.drawables.instanced, field.name); + renderer.renderInstanced(draw, drawables); + } +} + +fn render(renderer: *Renderer, draw: *Draw, drawables: anytype) void { + if (drawables.data.items.len == 0) return; + + switch (@TypeOf(drawables)) { + Draw.Drawables(gl.GL_TRIANGLES) => { + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, renderer.vbo); + gl.glBufferData(gl.GL_ARRAY_BUFFER, @as(gl.GLsizeiptr, @intCast(drawables.data.items.len)) * @sizeOf(f32), @ptrCast(drawables.data.items.ptr), gl.GL_STATIC_DRAW); + + for (draw.attribs.general.items) |attrib| { + gl.glEnableVertexAttribArray(attrib.data.loc); + gl.glVertexAttribPointer(attrib.data.loc, attrib.data.size, attrib.data.type, attrib.data.normalized, @intCast(Draw.Attribute.General.offset * @sizeOf(f32)), @ptrFromInt(attrib.data.offset * @sizeOf(f32))); + } + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, @intCast(drawables.data.items.len / Draw.Attribute.General.offset)); + }, + Draw.Drawables(gl.GL_TRIANGLE_FAN) => { + for (drawables.data.items) |data| { + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, renderer.vbo); + gl.glBufferData(gl.GL_ARRAY_BUFFER, @as(gl.GLsizeiptr, @intCast(data.items.len)) * @sizeOf(f32), @ptrCast(data.items.ptr), gl.GL_STATIC_DRAW); + + for (draw.attribs.general.items) |attrib| { + gl.glEnableVertexAttribArray(attrib.data.loc); + gl.glVertexAttribPointer(attrib.data.loc, attrib.data.size, attrib.data.type, attrib.data.normalized, @intCast(Draw.Attribute.General.offset * @sizeOf(f32)), @ptrFromInt(attrib.data.offset * @sizeOf(f32))); + } + + gl.glDrawArrays(gl.GL_TRIANGLE_FAN, 0, @intCast(data.items.len / Draw.Attribute.General.offset)); + } + }, + else => unreachable, + } +} + +fn renderInstanced(renderer: *Renderer, draw: *Draw, drawables: anytype) void { + if (drawables.data.items.len == 0) return; + + for (drawables.data.items, drawables.instanceData.items) |data, instanceData| { + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, renderer.vbo); + gl.glBufferData(gl.GL_ARRAY_BUFFER, @as(gl.GLsizeiptr, @intCast(data.items.len)) * @sizeOf(f32), @ptrCast(data.items.ptr), gl.GL_STATIC_DRAW); + + for (draw.attribs.general.items) |attrib| { + gl.glEnableVertexAttribArray(attrib.data.loc); + gl.glVertexAttribPointer(attrib.data.loc, attrib.data.size, attrib.data.type, attrib.data.normalized, @intCast(Draw.Attribute.General.offset * @sizeOf(f32)), @ptrFromInt(attrib.data.offset * @sizeOf(f32))); + } + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, renderer.vboi); + gl.glBufferData(gl.GL_ARRAY_BUFFER, @as(gl.GLsizeiptr, @intCast(instanceData.items.len)) * @sizeOf(f32), @ptrCast(instanceData.items.ptr), gl.GL_STATIC_DRAW); + + for (draw.attribs.instanced.items) |attrib| { + gl.glEnableVertexAttribArray(attrib.data.loc); + gl.glVertexAttribPointer(attrib.data.loc, attrib.data.size, attrib.data.type, attrib.data.normalized, @intCast(Draw.Attribute.Instanced.offset * @sizeOf(f32)), @ptrFromInt(attrib.data.offset * @sizeOf(f32))); + gl.glVertexAttribDivisor(renderer.vao, 1); + } + + const mode = if (@TypeOf(drawables) == Draw.DrawablesInstanced(gl.GL_TRIANGLES)) gl.GL_TRIANGLES else gl.GL_TRIANGLE_FAN; + + gl.glDrawArraysInstanced(mode, 0, @intCast(data.items.len / Draw.Attribute.General.offset), @intCast(instanceData.items.len / Draw.Attribute.Instanced.offset)); + } +} diff --git a/src/shaders/main.fs b/src/shaders/main.fs new file mode 100644 index 0000000..02ee85c --- /dev/null +++ b/src/shaders/main.fs @@ -0,0 +1,8 @@ +#version 330 + +in vec4 vColor; +out vec4 outColor; + +void main() { + outColor = vColor; +} diff --git a/src/shaders/main.vs b/src/shaders/main.vs new file mode 100644 index 0000000..e4fb419 --- /dev/null +++ b/src/shaders/main.vs @@ -0,0 +1,18 @@ +#version 330 + +uniform bool uInstanced; +uniform mat4 uTransform; + +in vec3 aPos; +in vec3 aInstanceOffset; +in vec4 aColor; +out vec4 vColor; + +void main() { + if (uInstanced) { + gl_Position = uTransform * vec4(aPos.xyz + aInstanceOffset.xyz, 1.0); + } else { + gl_Position = uTransform * vec4(aPos.xyz, 1.0); + } + vColor = aColor; +} diff --git a/src/transform.zig b/src/transform.zig new file mode 100644 index 0000000..8839f4f --- /dev/null +++ b/src/transform.zig @@ -0,0 +1,51 @@ +const Transform = @This(); + +const math = @import("math.zig"); + +base: math.Mat4x4, +translation: math.Mat4x4, +scale: math.Mat4x4, + +pub fn init() Transform { + return .{ + .base = .id, + .translation = .id, + .scale = .id, + }; +} + +const TransformType = enum { + orthographic, + translation, + scale, +}; + +pub fn set(transform: *Transform, tag: TransformType, transformation: math.Mat4x4) void { + switch (tag) { + .orthographic => transform.base = transformation, + .translation => transform.translation = transformation, + .scale => transform.scale = transformation, + } +} + +pub fn add(transform: *Transform, tag: TransformType, transformation: math.Mat4x4) void { + switch (tag) { + .orthographic => @compileError("you shouldn't add to orthographic transformation, use `.set` instead!"), + .translation => transform.translation.mult(transformation), + .scale => transform.scale.mult(transformation), + } +} + +pub fn get(transform: *Transform) math.Mat4x4 { + const width: f32 = 2.0 / transform.base.data[0][0]; + const height: f32 = 2.0 / transform.base.data[1][1]; + + var result = transform.base; + result.mult(math.Mat4x4.translation(math.Vec2 { width/2.0, height/2.0 })); + result.mult(transform.scale); + result.mult(math.Mat4x4.translation(math.Vec2 { -width/2.0, -height/2.0 })); + + result.mult(transform.translation); + + return result; +}