├── .vscode ├── settings.json ├── c_cpp_properties.json ├── launch.json └── tasks.json ├── .gitignore ├── gamekit ├── deps │ ├── fontstash │ │ ├── src │ │ │ └── fontstash.c │ │ ├── build.zig │ │ └── fontstash.zig │ ├── stb │ │ ├── stb.zig │ │ ├── src │ │ │ └── stb_impl.c │ │ ├── build.zig │ │ ├── stb_rect_pack.zig │ │ ├── stb_image_write.zig │ │ └── stb_image.zig │ ├── imgui │ │ ├── Makefile │ │ ├── build.zig │ │ └── temporary_hacks.cpp │ └── sdl │ │ └── build.zig ├── assets │ ├── ProggyTiny.ttf │ ├── sprite_fs.glsl │ ├── sprite_vs.glsl │ ├── sprite_fs.metal │ └── sprite_vs.metal ├── imgui │ ├── assets │ │ ├── fa-regular-400.ttf │ │ └── fa-solid-900.ttf │ ├── implementation.zig │ ├── renderer.zig │ └── events.zig ├── utils │ ├── utils.zig │ ├── fs.zig │ ├── camera.zig │ └── fixed_list.zig ├── math │ ├── math.zig │ ├── rect.zig │ ├── quad.zig │ ├── mat32.zig │ └── color.zig ├── graphics │ ├── offscreen_pass.zig │ ├── multi_batcher.zig │ ├── mesh.zig │ ├── triangle_batcher.zig │ ├── texture.zig │ ├── shader.zig │ ├── batcher.zig │ └── fontbook.zig ├── gamekit.zig ├── gfx.zig ├── input_types.zig ├── window.zig ├── time.zig ├── input.zig └── draw.zig ├── examples ├── assets │ ├── textures │ │ ├── bee-1.png │ │ ├── bee-2.png │ │ ├── bee-3.png │ │ ├── bee-4.png │ │ ├── bee-5.png │ │ ├── bee-6.png │ │ ├── bee-7.png │ │ ├── bee-8.png │ │ ├── block.png │ │ ├── tree.png │ │ ├── container.png │ │ ├── mario_kart.png │ │ └── awesomeface.png │ ├── shaders │ │ ├── mrt_fs.glsl │ │ ├── sprite_fs.glsl │ │ ├── vert_sway_fs.glsl │ │ ├── sprite_vs.glsl │ │ ├── vert_sway_vs.glsl │ │ ├── mode7_fs.glsl │ │ └── shader_src.glsl │ └── shaders.zig ├── clear.zig ├── tri_batcher.zig ├── vert_sway.zig ├── mrt.zig ├── primitives.zig ├── clear_imgui.zig ├── meshes.zig ├── stencil.zig ├── offscreen.zig ├── batcher.zig └── mode7.zig ├── .gitmodules ├── LICENSE └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | imgui.ini 3 | .DS_Store 4 | zig-arm-cache 5 | -------------------------------------------------------------------------------- /gamekit/deps/fontstash/src/fontstash.c: -------------------------------------------------------------------------------- 1 | #define FONTSTASH_IMPLEMENTATION 2 | #include "fontstash.h" 3 | -------------------------------------------------------------------------------- /gamekit/assets/ProggyTiny.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/gamekit/assets/ProggyTiny.ttf -------------------------------------------------------------------------------- /examples/assets/textures/bee-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-1.png -------------------------------------------------------------------------------- /examples/assets/textures/bee-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-2.png -------------------------------------------------------------------------------- /examples/assets/textures/bee-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-3.png -------------------------------------------------------------------------------- /examples/assets/textures/bee-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-4.png -------------------------------------------------------------------------------- /examples/assets/textures/bee-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-5.png -------------------------------------------------------------------------------- /examples/assets/textures/bee-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-6.png -------------------------------------------------------------------------------- /examples/assets/textures/bee-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-7.png -------------------------------------------------------------------------------- /examples/assets/textures/bee-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/bee-8.png -------------------------------------------------------------------------------- /examples/assets/textures/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/block.png -------------------------------------------------------------------------------- /examples/assets/textures/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/tree.png -------------------------------------------------------------------------------- /examples/assets/textures/container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/container.png -------------------------------------------------------------------------------- /examples/assets/textures/mario_kart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/mario_kart.png -------------------------------------------------------------------------------- /gamekit/imgui/assets/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/gamekit/imgui/assets/fa-regular-400.ttf -------------------------------------------------------------------------------- /gamekit/imgui/assets/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/gamekit/imgui/assets/fa-solid-900.ttf -------------------------------------------------------------------------------- /examples/assets/textures/awesomeface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prime31/zig-gamekit/HEAD/examples/assets/textures/awesomeface.png -------------------------------------------------------------------------------- /gamekit/utils/utils.zig: -------------------------------------------------------------------------------- 1 | pub const FixedList = @import("fixed_list.zig").FixedList; 2 | pub const Camera = @import("camera.zig").Camera; 3 | pub const fs = @import("fs.zig"); 4 | -------------------------------------------------------------------------------- /gamekit/deps/stb/stb.zig: -------------------------------------------------------------------------------- 1 | pub usingnamespace @import("stb_image.zig"); 2 | pub usingnamespace @import("stb_image_write.zig"); 3 | pub usingnamespace @import("stb_rect_pack.zig"); 4 | -------------------------------------------------------------------------------- /gamekit/math/math.zig: -------------------------------------------------------------------------------- 1 | pub const Vec2 = @import("vec2.zig").Vec2; 2 | pub const Mat32 = @import("mat32.zig").Mat32; 3 | pub const Color = @import("color.zig").Color; 4 | pub const Quad = @import("quad.zig").Quad; 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "renderkit"] 2 | path = renderkit 3 | url=https://github.com/prime31/zig-renderkit 4 | [submodule "gamekit/deps/imgui/cimgui"] 5 | path = gamekit/deps/imgui/cimgui 6 | url = https://github.com/cimgui/cimgui.git 7 | -------------------------------------------------------------------------------- /gamekit/math/rect.zig: -------------------------------------------------------------------------------- 1 | pub const Rect = struct { 2 | x: f32, 3 | y: f32, 4 | w: f32, 5 | h: f32, 6 | }; 7 | 8 | pub const RectI = struct { 9 | x: i32, 10 | y: i32, 11 | w: i32, 12 | h: i32, 13 | }; 14 | -------------------------------------------------------------------------------- /gamekit/deps/stb/src/stb_impl.c: -------------------------------------------------------------------------------- 1 | #define STB_IMAGE_IMPLEMENTATION 2 | #define STBI_NO_STDIO 3 | #include "stb_image.h" 4 | 5 | #define STB_IMAGE_WRITE_IMPLEMENTATION 6 | #include "stb_image_write.h" 7 | 8 | #define STB_RECT_PACK_IMPLEMENTATION 9 | #include "stb_rect_pack.h" -------------------------------------------------------------------------------- /examples/assets/shaders/mrt_fs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform sampler2D main_tex; 4 | 5 | layout(location = 0) out vec4 frag_color_0; 6 | in vec2 uv_out; 7 | layout(location = 1) out vec4 frag_color_1; 8 | in vec4 color_out; 9 | 10 | void main() 11 | { 12 | frag_color_0 = texture(main_tex, uv_out); 13 | frag_color_1 = texture(main_tex, uv_out) * color_out; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/clear.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const Color = gk.math.Color; 4 | 5 | pub fn main() !void { 6 | try gk.run(.{ 7 | .init = init, 8 | .render = render, 9 | }); 10 | } 11 | 12 | fn init() !void {} 13 | 14 | fn render() !void { 15 | gk.gfx.beginPass(.{ .color = Color.lime }); 16 | gk.gfx.endPass(); 17 | } 18 | -------------------------------------------------------------------------------- /gamekit/assets/sprite_fs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform sampler2D main_tex; 4 | 5 | layout(location = 0) out vec4 frag_color; 6 | in vec2 uv_out; 7 | in vec4 color_out; 8 | 9 | vec4 effect(sampler2D tex, vec2 tex_coord, vec4 vert_color) 10 | { 11 | return texture(tex, tex_coord) * vert_color; 12 | } 13 | 14 | void main() 15 | { 16 | vec2 param = uv_out; 17 | vec4 param_1 = color_out; 18 | frag_color = effect(main_tex, param, param_1); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /examples/assets/shaders/sprite_fs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform sampler2D main_tex; 4 | 5 | layout(location = 0) out vec4 frag_color; 6 | in vec2 uv_out; 7 | in vec4 color_out; 8 | 9 | vec4 effect(sampler2D tex, vec2 tex_coord, vec4 vert_color) 10 | { 11 | return texture(tex, tex_coord) * vert_color; 12 | } 13 | 14 | void main() 15 | { 16 | vec2 param = uv_out; 17 | vec4 param_1 = color_out; 18 | frag_color = effect(main_tex, param, param_1); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /examples/assets/shaders/vert_sway_fs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform sampler2D main_tex; 4 | 5 | layout(location = 0) out vec4 frag_color; 6 | in vec2 uv_out; 7 | in vec4 color_out; 8 | 9 | vec4 effect(sampler2D tex, vec2 tex_coord, vec4 vert_color) 10 | { 11 | return texture(tex, tex_coord) * vert_color; 12 | } 13 | 14 | void main() 15 | { 16 | vec2 param = uv_out; 17 | vec4 param_1 = color_out; 18 | frag_color = effect(main_tex, param, param_1); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /examples/assets/shaders/sprite_vs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform vec4 VertexParams[2]; 4 | out vec2 uv_out; 5 | layout(location = 1) in vec2 uv_in; 6 | out vec4 color_out; 7 | layout(location = 2) in vec4 color_in; 8 | layout(location = 0) in vec2 pos_in; 9 | 10 | void main() 11 | { 12 | uv_out = uv_in; 13 | color_out = color_in; 14 | gl_Position = vec4(mat3x2(vec2(VertexParams[0].x, VertexParams[0].y), vec2(VertexParams[0].z, VertexParams[0].w), vec2(VertexParams[1].x, VertexParams[1].y)) * vec3(pos_in, 1.0), 0.0, 1.0); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /gamekit/assets/sprite_vs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform vec4 VertexParams[2]; 4 | 5 | layout(location = 0) in vec2 pos_in; 6 | layout(location = 1) in vec2 uv_in; 7 | layout(location = 2) in vec4 color_in; 8 | 9 | out vec2 uv_out; 10 | out vec4 color_out; 11 | 12 | void main() 13 | { 14 | uv_out = uv_in; 15 | color_out = color_in; 16 | gl_Position = vec4(mat3x2(vec2(VertexParams[0].x, VertexParams[0].y), vec2(VertexParams[0].z, VertexParams[0].w), vec2(VertexParams[1].x, VertexParams[1].y)) * vec3(pos_in, 1.0), 0.0, 1.0); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /examples/assets/shaders/vert_sway_vs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform vec4 VertexSwayParams[3]; 4 | out vec2 uv_out; 5 | layout(location = 1) in vec2 uv_in; 6 | out vec4 color_out; 7 | layout(location = 2) in vec4 color_in; 8 | layout(location = 0) in vec2 pos_in; 9 | 10 | void main() 11 | { 12 | uv_out = uv_in; 13 | color_out = color_in; 14 | gl_Position = vec4(mat3x2(vec2(VertexSwayParams[0].x, VertexSwayParams[0].y), vec2(VertexSwayParams[0].z, VertexSwayParams[0].w), vec2(VertexSwayParams[1].x, VertexSwayParams[1].y)) * vec3(pos_in.x + ((sin(VertexSwayParams[2].x) * 20.0) * (1.0 - uv_in.y)), pos_in.y, 1.0), 0.0, 1.0); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /examples/tri_batcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const math = gk.math; 4 | const Color = math.Color; 5 | 6 | var tri_batch: gk.gfx.TriangleBatcher = undefined; 7 | 8 | pub fn main() !void { 9 | try gk.run(.{ 10 | .init = init, 11 | .render = render, 12 | }); 13 | } 14 | 15 | fn init() !void { 16 | tri_batch = try gk.gfx.TriangleBatcher.init(std.heap.c_allocator, 100); 17 | } 18 | 19 | fn render() !void { 20 | gk.gfx.beginPass(.{}); 21 | 22 | tri_batch.begin(); 23 | tri_batch.drawTriangle(.{ .x = 50, .y = 50 }, .{ .x = 150, .y = 150 }, .{ .x = 0, .y = 150 }, Color.sky_blue); 24 | tri_batch.drawTriangle(.{ .x = 300, .y = 50 }, .{ .x = 350, .y = 150 }, .{ .x = 200, .y = 150 }, Color.lime); 25 | tri_batch.end(); 26 | 27 | gk.gfx.endPass(); 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/MetalKit.framework/Headers", 8 | "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Metal.framework/Headers", 9 | ], 10 | "defines": [], 11 | "macFrameworkPath": [ 12 | "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks" 13 | ], 14 | "compilerPath": "/usr/local/opt/llvm/bin/clang", 15 | "cStandard": "c17", 16 | "cppStandard": "c++14", 17 | "intelliSenseMode": "clang-x64" 18 | } 19 | ], 20 | "version": 4 21 | } -------------------------------------------------------------------------------- /gamekit/deps/stb/build.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const Builder = std.build.Builder; 4 | 5 | pub fn build(b: *std.build.Builder) !void { 6 | _ = b; 7 | } 8 | 9 | pub fn linkArtifact(b: *Builder, exe: *std.build.LibExeObjStep, target: std.zig.CrossTarget, comptime prefix_path: []const u8) void { 10 | _ = b; 11 | _ = target; 12 | exe.linkLibC(); 13 | exe.addIncludePath(prefix_path ++ "gamekit/deps/stb/src"); 14 | 15 | const lib_cflags = &[_][]const u8{"-std=c99"}; 16 | exe.addCSourceFile(prefix_path ++ "gamekit/deps/stb/src/stb_impl.c", lib_cflags); 17 | } 18 | 19 | pub fn getModule(b: *std.Build, comptime prefix_path: []const u8) *std.build.Module { 20 | if (prefix_path.len > 0 and !std.mem.endsWith(u8, prefix_path, "/")) @panic("prefix-path must end with '/' if it is not empty"); 21 | return b.createModule(.{ 22 | .source_file = .{ .path = prefix_path ++ "gamekit/deps/stb/stb.zig" }, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /gamekit/assets/sprite_fs.metal: -------------------------------------------------------------------------------- 1 | #pragma clang diagnostic ignored "-Wmissing-prototypes" 2 | 3 | #include 4 | #include 5 | 6 | using namespace metal; 7 | 8 | struct main0_out 9 | { 10 | float4 frag_color [[color(0)]]; 11 | }; 12 | 13 | struct main0_in 14 | { 15 | float2 uv_out [[user(locn0)]]; 16 | float4 color_out [[user(locn1)]]; 17 | }; 18 | 19 | #line 18 "" 20 | static inline __attribute__((always_inline)) 21 | float4 effect(thread const texture2d tex, thread const sampler texSmplr, thread const float2& tex_coord, thread const float4& vert_color) 22 | { 23 | #line 18 "" 24 | return tex.sample(texSmplr, tex_coord) * vert_color; 25 | } 26 | 27 | #line 15 "" 28 | fragment main0_out main0(main0_in in [[stage_in]], texture2d main_tex [[texture(0)]], sampler main_texSmplr [[sampler(0)]]) 29 | { 30 | main0_out out = {}; 31 | #line 15 "" 32 | float2 param = in.uv_out; 33 | float4 param_1 = in.color_out; 34 | out.frag_color = effect(main_tex, main_texSmplr, param, param_1); 35 | return out; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /gamekit/assets/sprite_vs.metal: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace metal; 5 | 6 | struct VertexParams 7 | { 8 | float4 transform_matrix[2]; 9 | }; 10 | 11 | struct main0_out 12 | { 13 | float2 uv_out [[user(locn0)]]; 14 | float4 color_out [[user(locn1)]]; 15 | float4 gl_Position [[position]]; 16 | }; 17 | 18 | struct main0_in 19 | { 20 | float2 pos_in [[attribute(0)]]; 21 | float2 uv_in [[attribute(1)]]; 22 | float4 color_in [[attribute(2)]]; 23 | }; 24 | 25 | #line 18 "" 26 | vertex main0_out main0(main0_in in [[stage_in]], constant VertexParams& _28 [[buffer(0)]]) 27 | { 28 | main0_out out = {}; 29 | #line 18 "" 30 | out.uv_out = in.uv_in; 31 | #line 19 "" 32 | out.color_out = in.color_in; 33 | #line 20 "" 34 | out.gl_Position = float4(float3x2(float2(_28.transform_matrix[0].x, _28.transform_matrix[0].y), float2(_28.transform_matrix[0].z, _28.transform_matrix[0].w), float2(_28.transform_matrix[1].x, _28.transform_matrix[1].y)) * float3(in.pos_in, 1.0), 0.0, 1.0); 35 | return out; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /gamekit/utils/fs.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// reads the contents of a file. Returned value is owned by the caller and must be freed! 4 | pub fn read(allocator: std.mem.Allocator, filename: []const u8) ![]u8 { 5 | const file = try std.fs.cwd().openFile(filename, .{}); 6 | defer file.close(); 7 | 8 | const file_size = try file.getEndPos(); 9 | var buffer = try allocator.alloc(u8, file_size); 10 | const bytes_read = try file.read(buffer[0..buffer.len]); 11 | _ = bytes_read; 12 | 13 | return buffer; 14 | } 15 | 16 | /// reads the contents of a file. Returned value is owned by the caller and must be freed! 17 | pub fn readZ(allocator: std.mem.Allocator, filename: []const u8) ![:0]u8 { 18 | const file = try std.fs.cwd().openFile(filename, .{}); 19 | defer file.close(); 20 | 21 | const file_size = try file.getEndPos(); 22 | var buffer = try allocator.alloc(u8, file_size + 1); 23 | const bytes_read = try file.read(buffer[0..file_size]); 24 | _ = bytes_read; 25 | buffer[file_size] = 0; 26 | 27 | return buffer[0..file_size :0]; 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 prime31 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gamekit/imgui/implementation.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const imgui = @import("imgui"); 3 | const sdl = @import("sdl"); 4 | const gk = @import("../gamekit.zig"); 5 | 6 | const Renderer = @import("renderer.zig").Renderer; 7 | const Events = @import("events.zig").Events; 8 | 9 | var state = struct { 10 | renderer: Renderer = undefined, 11 | events: Events = undefined, 12 | }{}; 13 | 14 | // public methods 15 | pub fn init(window: *sdl.SDL_Window, docking: bool, viewports: bool, icon_font: bool) void { 16 | _ = window; 17 | state.renderer = Renderer.init(docking, viewports, icon_font); 18 | state.events = Events.init(); 19 | } 20 | 21 | pub fn deinit() void { 22 | state.renderer.deinit(); 23 | state.events.deinit(); 24 | } 25 | 26 | pub fn newFrame() void { 27 | state.events.newFrame(gk.window.sdl_window); 28 | imgui.igNewFrame(); 29 | } 30 | 31 | pub fn render() void { 32 | state.renderer.render(); 33 | } 34 | 35 | /// returns true if the event is handled by imgui and should be ignored 36 | pub fn handleEvent(event: *sdl.SDL_Event) bool { 37 | return state.events.handleEvent(event); 38 | } 39 | -------------------------------------------------------------------------------- /gamekit/deps/fontstash/build.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const Builder = std.build.Builder; 4 | 5 | pub fn build(b: *std.build.Builder) !void { 6 | _ = b; 7 | } 8 | 9 | /// prefix_path is used to add package paths. It should be the the same path used to include this build file 10 | pub fn linkArtifact(b: *Builder, exe: *std.build.LibExeObjStep, target: std.zig.CrossTarget, comptime prefix_path: []const u8) void { 11 | _ = b; 12 | _ = target; 13 | if (prefix_path.len > 0 and !std.mem.endsWith(u8, prefix_path, "/")) @panic("prefix-path must end with '/' if it is not empty"); 14 | exe.linkLibC(); 15 | 16 | const lib_cflags = &[_][]const u8{"-O3"}; 17 | exe.addCSourceFile(prefix_path ++ "gamekit/deps/fontstash/src/fontstash.c", lib_cflags); 18 | } 19 | 20 | pub fn getModule(b: *std.Build, comptime prefix_path: []const u8) *std.build.Module { 21 | if (prefix_path.len > 0 and !std.mem.endsWith(u8, prefix_path, "/")) @panic("prefix-path must end with '/' if it is not empty"); 22 | return b.createModule(.{ 23 | .source_file = .{ .path = prefix_path ++ "gamekit/deps/fontstash/fontstash.zig" }, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /gamekit/utils/camera.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("../gamekit.zig"); 3 | const math = gk.math; 4 | 5 | pub const Camera = struct { 6 | pos: math.Vec2 = .{}, 7 | zoom: f32 = 1, 8 | 9 | pub fn init() Camera { 10 | return .{}; 11 | } 12 | 13 | pub fn transMat(self: Camera) math.Mat32 { 14 | const size = gk.window.size(); 15 | const half_w = @intToFloat(f32, size.w) * 0.5; 16 | const half_h = @intToFloat(f32, size.h) * 0.5; 17 | 18 | var transform = math.Mat32.identity; 19 | 20 | var tmp = math.Mat32.identity; 21 | tmp.translate(-self.pos.x, -self.pos.y); 22 | transform = tmp.mul(transform); 23 | 24 | tmp = math.Mat32.identity; 25 | tmp.scale(self.zoom, self.zoom); 26 | transform = tmp.mul(transform); 27 | 28 | tmp = math.Mat32.identity; 29 | tmp.translate(half_w, half_h); 30 | transform = tmp.mul(transform); 31 | 32 | return transform; 33 | } 34 | 35 | pub fn screenToWorld(self: Camera, pos: math.Vec2) math.Vec2 { 36 | var inv_trans_mat = self.transMat().invert(); 37 | return inv_trans_mat.transformVec2(.{ .x = pos.x, .y = @intToFloat(f32, gk.window.height()) - pos.y }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /examples/vert_sway.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const shaders = @import("assets/shaders.zig"); 3 | const gk = @import("gamekit"); 4 | const gfx = gk.gfx; 5 | const Color = gk.math.Color; 6 | 7 | var tex: gfx.Texture = undefined; 8 | var shader: gfx.Shader = undefined; 9 | var vs_params: shaders.VertexSwayParams = shaders.VertexSwayParams{ 10 | .time = 1, 11 | }; 12 | 13 | pub fn main() !void { 14 | try gk.run(.{ 15 | .init = init, 16 | .update = update, 17 | .render = render, 18 | .shutdown = shutdown, 19 | }); 20 | } 21 | 22 | fn init() !void { 23 | tex = try gfx.Texture.initFromFile(std.heap.c_allocator, "examples/assets/textures/tree.png", .linear); 24 | shader = try shaders.createVertSwayShader(); 25 | } 26 | 27 | fn shutdown() !void { 28 | tex.deinit(); 29 | shader.deinit(); 30 | } 31 | 32 | fn update() !void { 33 | std.mem.copy(f32, &vs_params.transform_matrix, &gfx.state.transform_mat.data); 34 | vs_params.time = @floatCast(f32, gk.time.toSeconds(gk.time.now())); 35 | } 36 | 37 | fn render() !void { 38 | gfx.beginPass(.{ .color = Color.blue, .shader = &shader }); 39 | shader.setVertUniform(shaders.VertexSwayParams, &vs_params); 40 | gfx.draw.tex(tex, .{ .x = 300, .y = 200 }); 41 | gfx.endPass(); 42 | } 43 | -------------------------------------------------------------------------------- /examples/mrt.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const shaders = @import("assets/shaders.zig"); 3 | const gk = @import("gamekit"); 4 | const math = gk.math; 5 | const gfx = gk.gfx; 6 | const draw = gfx.draw; 7 | 8 | var checker_tex: gfx.Texture = undefined; 9 | var pass: gfx.OffscreenPass = undefined; 10 | var shader: gfx.Shader = undefined; 11 | 12 | pub fn main() !void { 13 | try gk.run(.{ 14 | .window = .{ 15 | .resizable = false, 16 | }, 17 | .init = init, 18 | .render = render, 19 | .shutdown = shutdown, 20 | }); 21 | } 22 | 23 | fn init() !void { 24 | checker_tex = gfx.Texture.initCheckerTexture(); 25 | pass = gfx.OffscreenPass.initMrt(400, 300, 2); 26 | shader = shaders.createMrtShader() catch unreachable; 27 | } 28 | 29 | fn render() !void { 30 | // offscreen rendering 31 | gk.gfx.beginPass(.{ .color = math.Color.sky_blue, .shader = &shader, .pass = pass }); 32 | draw.texScale(checker_tex, .{ .x = 260, .y = 70 }, 12.5); 33 | draw.point(.{ .x = 20, .y = 20 }, 40, math.Color.yellow); 34 | gk.gfx.endPass(); 35 | 36 | // backbuffer rendering 37 | gk.gfx.beginPass(.{ .color = gk.math.Color.beige }); 38 | draw.tex(pass.color_texture, .{}); 39 | draw.tex(pass.color_texture2.?, .{ .x = 400, .y = 300 }); 40 | gk.gfx.endPass(); 41 | } 42 | 43 | fn shutdown() !void { 44 | checker_tex.deinit(); 45 | pass.deinit(); 46 | shader.deinit(); 47 | } 48 | -------------------------------------------------------------------------------- /examples/primitives.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const gfx = gk.gfx; 4 | const Color = gk.math.Color; 5 | const Vec2 = gk.math.Vec2; 6 | 7 | pub fn main() !void { 8 | try gk.run(.{ 9 | .init = init, 10 | .render = render, 11 | }); 12 | } 13 | 14 | fn init() !void {} 15 | 16 | fn render() !void { 17 | gfx.beginPass(.{}); 18 | // draw some text 19 | gfx.draw.text("The text rendering exists", 5, 20, null); 20 | 21 | gfx.draw.fontbook.setColor(Color.purple); 22 | gfx.draw.text("Purple text", 5, 40, null); 23 | gfx.draw.fontbook.setColor(Color.white); 24 | 25 | gfx.draw.fontbook.pushState(); 26 | gfx.draw.fontbook.setBlur(1); 27 | gfx.draw.fontbook.setColor(Color.blue); 28 | gfx.draw.text("I'm some blurry blue text", 250, 95, null); 29 | gfx.draw.fontbook.popState(); 30 | 31 | // render some primitives 32 | gfx.draw.line(Vec2.init(0, 0), Vec2.init(640, 480), 2, Color.blue); 33 | gfx.draw.point(Vec2.init(350, 350), 10, Color.sky_blue); 34 | gfx.draw.point(Vec2.init(380, 380), 15, Color.magenta); 35 | gfx.draw.rect(Vec2.init(387, 372), 40, 15, Color.dark_brown); 36 | gfx.draw.hollowRect(Vec2.init(430, 372), 40, 15, 2, Color.yellow); 37 | gfx.draw.circle(.{ .x = 400, .y = 300 }, 50, 3, 12, Color.orange); 38 | 39 | const poly = [_]Vec2{ .{ .x = 400, .y = 30 }, .{ .x = 420, .y = 10 }, .{ .x = 430, .y = 80 }, .{ .x = 410, .y = 60 }, .{ .x = 375, .y = 40 } }; 40 | gfx.draw.hollowPolygon(poly[0..], 2, Color.lime); 41 | gfx.endPass(); 42 | } 43 | -------------------------------------------------------------------------------- /gamekit/deps/stb/stb_rect_pack.zig: -------------------------------------------------------------------------------- 1 | pub const stbrp_context = extern struct { 2 | width: c_int, 3 | height: c_int, 4 | @"align": c_int, 5 | init_mode: c_int, 6 | heuristic: c_int, 7 | num_nodes: c_int, 8 | active_head: [*c]stbrp_node, 9 | free_head: [*c]stbrp_node, 10 | extra: [2]stbrp_node, 11 | }; 12 | 13 | pub const stbrp_node = extern struct { 14 | x: c_ushort, 15 | y: c_ushort, 16 | next: [*c]stbrp_node, 17 | }; 18 | 19 | pub const stbrp_rect = extern struct { 20 | id: c_int, 21 | w: c_ushort, 22 | h: c_ushort, 23 | x: c_ushort = 0, 24 | y: c_ushort = 0, 25 | was_packed: c_int = 0, 26 | }; 27 | 28 | pub extern fn stbrp_pack_rects(context: [*c]stbrp_context, rects: [*c]stbrp_rect, num_rects: c_int) c_int; 29 | pub extern fn stbrp_init_target(context: [*c]stbrp_context, width: c_int, height: c_int, nodes: [*c]stbrp_node, num_nodes: c_int) void; 30 | pub extern fn stbrp_setup_allow_out_of_mem(context: [*c]stbrp_context, allow_out_of_mem: c_int) void; 31 | pub extern fn stbrp_setup_heuristic(context: [*c]stbrp_context, heuristic: c_int) void; 32 | 33 | pub const STBRP_HEURISTIC_Skyline_default = @enumToInt(enum_unnamed_1.STBRP_HEURISTIC_Skyline_default); 34 | pub const STBRP_HEURISTIC_Skyline_BL_sortHeight = @enumToInt(enum_unnamed_1.STBRP_HEURISTIC_Skyline_BL_sortHeight); 35 | pub const STBRP_HEURISTIC_Skyline_BF_sortHeight = @enumToInt(enum_unnamed_1.STBRP_HEURISTIC_Skyline_BF_sortHeight); 36 | const enum_unnamed_1 = enum(c_int) { 37 | STBRP_HEURISTIC_Skyline_default = 0, 38 | STBRP_HEURISTIC_Skyline_BL_sortHeight = 0, 39 | STBRP_HEURISTIC_Skyline_BF_sortHeight = 1, 40 | _, 41 | }; 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb-mi", 9 | "request": "launch", 10 | "name": "LLDB", 11 | "target": "./test", 12 | "cwd": "${workspaceRoot}", 13 | "valuesFormatting": "parseText" 14 | }, 15 | { 16 | "type": "gdb", 17 | "request": "launch", 18 | "name": "Launch Program", 19 | "target": "./test", 20 | "cwd": "${workspaceRoot}", 21 | "valuesFormatting": "parseText" 22 | }, 23 | { 24 | "name": "Debug", 25 | "type": "gdb", 26 | "request": "launch", 27 | "target": "${workspaceFolder}/zig-cache/bin/run", 28 | "cwd": "${workspaceRoot}", 29 | "valuesFormatting": "parseText" 30 | }, 31 | { 32 | "name": "Debug Windows", 33 | "type": "cppvsdbg", 34 | "request": "launch", 35 | "program": "${workspaceFolder}/zig-cache/bin/run.exe", 36 | "args": [], 37 | "stopAtEntry": false, 38 | "cwd": "${workspaceFolder}/zig-cache/bin", 39 | "symbolSearchPath": "${workspaceFolder}/zig-cache/bin", 40 | "environment": [], 41 | "externalConsole": false, 42 | "logging": { 43 | "moduleLoad": false 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /gamekit/deps/stb/stb_image_write.zig: -------------------------------------------------------------------------------- 1 | pub extern var stbi_write_tga_with_rle: c_int; 2 | pub extern var stbi_write_png_compression_level: c_int; 3 | pub extern var stbi_write_force_png_filter: c_int; 4 | 5 | pub extern fn stbi_write_png(filename: [*c]const u8, w: c_int, h: c_int, comp: c_int, data: ?*const anyopaque, stride_in_bytes: c_int) c_int; 6 | pub extern fn stbi_write_bmp(filename: [*c]const u8, w: c_int, h: c_int, comp: c_int, data: ?*const anyopaque) c_int; 7 | pub extern fn stbi_write_tga(filename: [*c]const u8, w: c_int, h: c_int, comp: c_int, data: ?*const anyopaque) c_int; 8 | pub extern fn stbi_write_hdr(filename: [*c]const u8, w: c_int, h: c_int, comp: c_int, data: [*c]const f32) c_int; 9 | pub extern fn stbi_write_jpg(filename: [*c]const u8, x: c_int, y: c_int, comp: c_int, data: ?*const anyopaque, quality: c_int) c_int; 10 | 11 | pub const stbi_write_func = fn (?*anyopaque, ?*anyopaque, c_int) callconv(.C) void; 12 | pub extern fn stbi_write_png_to_func(func: ?stbi_write_func, context: ?*anyopaque, w: c_int, h: c_int, comp: c_int, data: ?*const anyopaque, stride_in_bytes: c_int) c_int; 13 | pub extern fn stbi_write_bmp_to_func(func: ?stbi_write_func, context: ?*anyopaque, w: c_int, h: c_int, comp: c_int, data: ?*const anyopaque) c_int; 14 | pub extern fn stbi_write_tga_to_func(func: ?stbi_write_func, context: ?*anyopaque, w: c_int, h: c_int, comp: c_int, data: ?*const anyopaque) c_int; 15 | pub extern fn stbi_write_hdr_to_func(func: ?stbi_write_func, context: ?*anyopaque, w: c_int, h: c_int, comp: c_int, data: [*c]const f32) c_int; 16 | pub extern fn stbi_write_jpg_to_func(func: ?stbi_write_func, context: ?*anyopaque, x: c_int, y: c_int, comp: c_int, data: ?*const anyopaque, quality: c_int) c_int; 17 | pub extern fn stbi_flip_vertically_on_write(flip_boolean: c_int) void; 18 | -------------------------------------------------------------------------------- /examples/assets/shaders/mode7_fs.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform vec4 Mode7Params[3]; 4 | uniform sampler2D main_tex; 5 | uniform sampler2D map_tex; 6 | 7 | layout(location = 0) out vec4 frag_color; 8 | in vec2 uv_out; 9 | in vec4 color_out; 10 | 11 | vec4 effect(sampler2D tex, vec2 tex_coord, vec4 vert_color) 12 | { 13 | vec2 _86 = vec2((0.5 - tex_coord.x) * Mode7Params[1].x, (Mode7Params[1].z - tex_coord.y) * (Mode7Params[1].x / Mode7Params[1].y)) * mat2(vec2(Mode7Params[2].x, Mode7Params[2].z), vec2(Mode7Params[2].y, Mode7Params[2].w)); 14 | float _100 = ((_86.x / tex_coord.y) + Mode7Params[0].z) / Mode7Params[0].x; 15 | float _113 = ((_86.y / tex_coord.y) + Mode7Params[0].w) / Mode7Params[0].y; 16 | bool _119 = Mode7Params[1].w == 0.0; 17 | bool _146; 18 | if (_119) 19 | { 20 | bool _124 = _100 < 0.0; 21 | bool _131; 22 | if (!_124) 23 | { 24 | _131 = _100 > 1.0; 25 | } 26 | else 27 | { 28 | _131 = _124; 29 | } 30 | bool _138; 31 | if (!_131) 32 | { 33 | _138 = _113 < 0.0; 34 | } 35 | else 36 | { 37 | _138 = _131; 38 | } 39 | bool _145; 40 | if (!_138) 41 | { 42 | _145 = _113 > 1.0; 43 | } 44 | else 45 | { 46 | _145 = _138; 47 | } 48 | _146 = _145; 49 | } 50 | else 51 | { 52 | _146 = _119; 53 | } 54 | if (_146) 55 | { 56 | return vec4(0.0); 57 | } 58 | else 59 | { 60 | return texture(map_tex, mod(vec2(_100, _113), vec2(1.0))) * vert_color; 61 | } 62 | } 63 | 64 | void main() 65 | { 66 | vec2 param = uv_out; 67 | vec4 param_1 = color_out; 68 | frag_color = effect(main_tex, param, param_1); 69 | } 70 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig GameKit 2 | Companion repo and example implementation for [zig-renderkit](https://github.com/prime31/zig-renderkit). `GameKit` provides an example implementation of a game framework built on top of `RenderKit`. It includes the core render loop, window (via SDL), input, Dear ImGui and timing support. You can use it as a base to make a 2D game as-is or create your own 2D framework based on it. 3 | 4 | `GameKit` provides the following wrappers around `RenderKit`'s API showing how it can be abstracted away in a real world project: `Texture`, `Shader` and `OffscreenPass`. Building on top of those types, `GameKit` then provides `Mesh` and `DynamicMesh` which manage buffers and bindings for you. Finally, the high level types utilize `DynamicMesh` and cover pretty much all that any 2D game would require: `Batcher` (quad/sprite batch) and `TriangleBatcher`. 5 | 6 | Some basic utilities and a small math lib with just the types required for the renderer (`Vec2`, `Vec3`, `Color`, `3x2 Matrix`, `Quad`) are also included. 7 | 8 | 9 | ## Dependencies 10 | GameKit has just one external dependency: SDL. You can install SDL with the package manager of your choice. 11 | 12 | 13 | ### Usage 14 | - clone the repository recursively: `git clone --recursive https://github.com/prime31/zig-gamekit` 15 | - `zig build help` to see what examples are availble 16 | - `zig build EXAMPLE_NAME` to run an example 17 | 18 | 19 | ### Minimal GameKit Project File 20 | ```zig 21 | var texture: Texture = undefined; 22 | 23 | pub fn main() !void { 24 | try gamekit.run(.{ .init = init, .render = render }); 25 | } 26 | 27 | fn init() !void { 28 | texture = Texture.initFromFile(std.heap.HeapAllocator, "texture.png", .nearest) catch unreachable; 29 | } 30 | 31 | fn render() !void { 32 | gamekit.gfx.beginPass(.{ .color = Color.lime }); 33 | gamekit.gfx.draw.tex(texture, .{ .x = 50, .y = 50 }); 34 | gamekit.gfx.endPass(); 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /examples/assets/shaders.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const gfx = gk.gfx; 4 | const math = gk.math; 5 | const renderkit = gk.renderkit; 6 | 7 | pub const Mode7Shader = gfx.ShaderState(Mode7Params); 8 | 9 | pub fn createMode7Shader() Mode7Shader { 10 | const frag = @embedFile("shaders/mode7_fs.glsl"); 11 | return Mode7Shader.init(.{ .frag = frag, .onPostBind = Mode7Shader.onPostBind }); 12 | } 13 | 14 | pub fn createMrtShader() !gfx.Shader { 15 | const vert = @embedFile("shaders/sprite_vs.glsl"); 16 | const frag = @embedFile("shaders/mrt_fs.glsl"); 17 | return try gfx.Shader.initWithVertFrag(VertexParams, struct { pub const metadata = .{ .images = .{ "main_tex" } }; }, .{ .frag = frag, .vert = vert }); 18 | } 19 | 20 | pub fn createVertSwayShader() !gfx.Shader { 21 | const vert = @embedFile("shaders/vert_sway_vs.glsl"); 22 | const frag = @embedFile("shaders/sprite_fs.glsl"); 23 | return try gfx.Shader.initWithVertFrag(VertexSwayParams, struct { pub const metadata = .{ .images = .{ "main_tex" } }; }, .{ .frag = frag, .vert = vert }); 24 | } 25 | 26 | 27 | pub const VertexParams = extern struct { 28 | pub const metadata = .{ 29 | .uniforms = .{ .VertexParams = .{ .type = .float4, .array_count = 2 } }, 30 | }; 31 | 32 | transform_matrix: [8]f32 = [_]f32{0} ** 8, 33 | }; 34 | 35 | pub const VertexSwayParams = extern struct { 36 | pub const metadata = .{ 37 | .uniforms = .{ .VertexSwayParams = .{ .type = .float4, .array_count = 3 } }, 38 | }; 39 | 40 | transform_matrix: [8]f32 = [_]f32{0} ** 8, 41 | time: f32 = 0, 42 | _pad36_0_: [12]u8 = [_]u8{0} ** 12, 43 | }; 44 | 45 | pub const Mode7Params = extern struct { 46 | pub const metadata = .{ 47 | .images = .{ "main_tex", "map_tex" }, 48 | .uniforms = .{ .Mode7Params = .{ .type = .float4, .array_count = 3 } }, 49 | }; 50 | 51 | mapw: f32 = 0, 52 | maph: f32 = 0, 53 | x: f32 = 0, 54 | y: f32 = 0, 55 | zoom: f32 = 0, 56 | fov: f32 = 0, 57 | offset: f32 = 0, 58 | wrap: f32 = 0, 59 | x1: f32 = 0, 60 | x2: f32 = 0, 61 | y1: f32 = 0, 62 | y2: f32 = 0, 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /gamekit/deps/imgui/Makefile: -------------------------------------------------------------------------------- 1 | # You will need SDL2 (http://www.libsdl.org): 2 | # Linux: 3 | # apt-get install libsdl2-dev 4 | # Mac OS X: 5 | # brew install sdl2 6 | # MSYS2: 7 | # pacman -S mingw-w64-i686-SDL2 8 | 9 | TARGET := 10 | 11 | CP := cp 12 | CIMGUI := cimgui 13 | IMGUI := $(CIMGUI)/imgui 14 | $(shell mkdir -p build) 15 | 16 | CFLAGS := -I. 17 | CFLAGS += -DCIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 18 | CFLAGS += -DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1 19 | CFLAGS += -DIMGUI_IMPL_OPENGL_LOADER_GL3W 20 | CFLAGS += -DIMGUI_IMPL_API=extern\ \"C\" 21 | CFLAGS += `sdl2-config --cflags` 22 | 23 | ## Using OpenGL loader: gl3w [default] 24 | GL3W := $(CIMGUI)/imgui/examples/libs/gl3w 25 | CFLAGS += -I$(GL3W) 26 | 27 | TARGET += gl3w.o 28 | TARGET += imgui_impl_sdl.o 29 | TARGET += imgui_impl_opengl3.o 30 | 31 | ##--------------------------------------------------------------------- 32 | ## BUILD FLAGS PER PLATFORM 33 | ##--------------------------------------------------------------------- 34 | 35 | UNAME_S := $(shell uname -s) 36 | LIB_FOLDER := 37 | 38 | ifeq ($(UNAME_S), Linux) #LINUX 39 | CFLAGS += `sdl2-config --cflags` 40 | 41 | SED := sed -i 42 | TARGET += cimgui.so # not yet supported 43 | else ifeq ($(UNAME_S), Darwin) #APPLE 44 | CFLAGS += `sdl2-config --cflags` 45 | CFLAGS += -I/usr/local/include 46 | CFLAGS += -Icimgui/imgui/examples/libs/gl3w 47 | 48 | SED := sed -i '' 49 | endif 50 | 51 | ##--------------------------------------------------------------------- 52 | ## BUILD RULES 53 | ##--------------------------------------------------------------------- 54 | 55 | all: $(TARGET) 56 | @echo build complete 57 | 58 | gl3w.o: $(IMGUI)/examples/libs/gl3w/GL/gl3w.c 59 | $(CC) -fPIC -c -o build/$@ $^ -I$(GL3W) -fno-threadsafe-statics 60 | 61 | imgui_impl_sdl.o: $(IMGUI)/examples/imgui_impl_sdl.cpp 62 | $(CXX) -fPIC -c -o build/$@ $^ -I$(IMGUI) `sdl2-config --cflags` -DIMGUI_IMPL_API=extern\ \"C\" -fno-threadsafe-statics 63 | 64 | imgui_impl_opengl3.o: $(IMGUI)/examples/imgui_impl_opengl3.cpp 65 | $(CXX) -fPIC -c -o build/$@ $^ -I$(IMGUI) -I$(GL3W) $(CFLAGS) -DIMGUI_IMPL_API=extern\ \"C\" -fno-threadsafe-statics 66 | 67 | clean: 68 | $(RM) *.o *.so *.h $(TARGET) 69 | 70 | clobber: clean 71 | $(RM) -Rf build 72 | -------------------------------------------------------------------------------- /examples/clear_imgui.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const gfx = gk.gfx; 4 | const imgui = gk.imgui; 5 | 6 | pub const enable_imgui = true; 7 | 8 | var clear_color = gk.math.Color.aya; 9 | var camera: gk.utils.Camera = undefined; 10 | var tex: gfx.Texture = undefined; 11 | 12 | pub fn main() !void { 13 | try gk.run(.{ 14 | .init = init, 15 | .update = update, 16 | .render = render, 17 | }); 18 | } 19 | 20 | fn init() !void { 21 | camera = gk.utils.Camera.init(); 22 | } 23 | 24 | fn update() !void { 25 | imgui.igShowDemoWindow(null); 26 | 27 | if (gk.input.keyDown(.a)) { 28 | camera.pos.x += 100 * gk.time.dt(); 29 | } else if (gk.input.keyDown(.d)) { 30 | camera.pos.x -= 100 * gk.time.dt(); 31 | } 32 | if (gk.input.keyDown(.w)) { 33 | camera.pos.y -= 100 * gk.time.dt(); 34 | } else if (gk.input.keyDown(.s)) { 35 | camera.pos.y += 100 * gk.time.dt(); 36 | } 37 | } 38 | 39 | fn render() !void { 40 | gfx.beginPass(.{ .color = clear_color, .trans_mat = camera.transMat() }); 41 | 42 | imgui.igText("WASD moves camera " ++ imgui.icons.camera); 43 | 44 | var color = clear_color.asArray(); 45 | if (imgui.igColorEdit4("Clear Color", &color[0], imgui.ImGuiColorEditFlags_NoInputs)) { 46 | clear_color = gk.math.Color.fromRgba(color[0], color[1], color[2], color[3]); 47 | } 48 | 49 | var buf: [255]u8 = undefined; 50 | var str = try std.fmt.bufPrintZ(&buf, "Camera Pos: {d:.2}, {d:.2}", .{ camera.pos.x, camera.pos.y }); 51 | imgui.igText(str); 52 | 53 | var mouse = gk.input.mousePos(); 54 | var world = camera.screenToWorld(mouse); 55 | 56 | str = try std.fmt.bufPrintZ(&buf, "Mouse Pos: {d:.2}, {d:.2}", .{ mouse.x, mouse.y }); 57 | imgui.igText(str); 58 | 59 | str = try std.fmt.bufPrintZ(&buf, "World Pos: {d:.2}, {d:.2}", .{ world.x, world.y }); 60 | imgui.igText(str); 61 | 62 | if (imgui.ogButton("Camera Pos to 0,0")) camera.pos = .{}; 63 | if (imgui.ogButton("Camera Pos to screen center")) { 64 | const size = gk.window.size(); 65 | camera.pos = .{ .x = @intToFloat(f32, size.w) * 0.5, .y = @intToFloat(f32, size.h) * 0.5 }; 66 | } 67 | 68 | gfx.draw.point(.{}, 40, gk.math.Color.white); 69 | 70 | gfx.endPass(); 71 | } 72 | -------------------------------------------------------------------------------- /gamekit/deps/sdl/build.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const Builder = std.build.Builder; 4 | 5 | pub fn build(b: *std.build.Builder) !void { 6 | _ = b; 7 | } 8 | 9 | pub fn linkArtifact(b: *Builder, exe: *std.build.LibExeObjStep, target: std.zig.CrossTarget, comptime prefix_path: []const u8) void { 10 | _ = target; 11 | if (prefix_path.len > 0 and !std.mem.endsWith(u8, prefix_path, "/")) @panic("prefix-path must end with '/' if it is not empty"); 12 | exe.linkSystemLibrary("c"); 13 | exe.linkSystemLibrary("sdl2"); 14 | 15 | if (builtin.os.tag == .windows) { 16 | exe.linkSystemLibraryName("SDL2"); 17 | exe.linkSystemLibraryName("SDL2main"); 18 | 19 | // Windows include dirs for SDL2. This requires downloading SDL2 dev and extracting to c:\SDL2 20 | exe.addLibraryPath("c:\\SDL2\\lib\\x64"); 21 | 22 | // SDL2.dll needs to be copied to the zig-cache/bin folder 23 | // TODO: installFile doesnt seeem to work so manually copy the file over 24 | b.installFile("c:\\SDL2\\lib\\x64\\SDL2.dll", "bin\\SDL2.dll"); 25 | 26 | std.fs.cwd().makePath("zig-cache\\bin") catch unreachable; 27 | const src_dir = std.fs.cwd().openDir("c:\\SDL2\\lib\\x64", .{}) catch unreachable; 28 | src_dir.copyFile("SDL2.dll", std.fs.cwd(), "zig-cache\\bin\\SDL2.dll", .{}) catch unreachable; 29 | } else if (builtin.os.tag == .macos) { 30 | exe.linkSystemLibrary("iconv"); 31 | exe.linkFramework("AppKit"); 32 | exe.linkFramework("AudioToolbox"); 33 | exe.linkFramework("Carbon"); 34 | exe.linkFramework("Cocoa"); 35 | exe.linkFramework("CoreAudio"); 36 | exe.linkFramework("CoreFoundation"); 37 | exe.linkFramework("CoreGraphics"); 38 | exe.linkFramework("CoreHaptics"); 39 | exe.linkFramework("CoreVideo"); 40 | exe.linkFramework("ForceFeedback"); 41 | exe.linkFramework("GameController"); 42 | exe.linkFramework("IOKit"); 43 | exe.linkFramework("Metal"); 44 | } 45 | } 46 | 47 | pub fn getModule(b: *std.Build, comptime prefix_path: []const u8) *std.build.Module { 48 | if (prefix_path.len > 0 and !std.mem.endsWith(u8, prefix_path, "/")) @panic("prefix-path must end with '/' if it is not empty"); 49 | return b.createModule(.{ 50 | .source_file = .{ .path = prefix_path ++ "gamekit/deps/sdl/sdl.zig" }, 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /examples/meshes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const math = gk.math; 4 | const gfx = gk.gfx; 5 | 6 | var tex: gfx.Texture = undefined; 7 | var colored_tex: gfx.Texture = undefined; 8 | var mesh: gfx.Mesh = undefined; 9 | var dyn_mesh: gfx.DynamicMesh(u16, gfx.Vertex) = undefined; 10 | 11 | pub fn main() !void { 12 | try gk.run(.{ 13 | .init = init, 14 | .update = update, 15 | .render = render, 16 | }); 17 | } 18 | 19 | fn init() !void { 20 | tex = gfx.Texture.initCheckerTexture(); 21 | colored_tex = gfx.Texture.initSingleColor(0xFFFF0000); 22 | 23 | var vertices = [_]gfx.Vertex{ 24 | .{ .pos = .{ .x = 10, .y = 10 }, .uv = .{ .x = 0, .y = 0 } }, // tl 25 | .{ .pos = .{ .x = 100, .y = 10 }, .uv = .{ .x = 1, .y = 0 } }, // tr 26 | .{ .pos = .{ .x = 100, .y = 100 }, .uv = .{ .x = 1, .y = 1 } }, // br 27 | .{ .pos = .{ .x = 50, .y = 130 }, .uv = .{ .x = 0.5, .y = 1 } }, // bc 28 | .{ .pos = .{ .x = 10, .y = 100 }, .uv = .{ .x = 0, .y = 1 } }, // bl 29 | .{ .pos = .{ .x = 50, .y = 50 }, .uv = .{ .x = 0.5, .y = 0.5 } }, // c 30 | }; 31 | var indices = [_]u16{ 0, 5, 4, 5, 3, 4, 5, 2, 3, 5, 1, 2, 5, 0, 1 }; 32 | mesh = gfx.Mesh.init(u16, indices[0..], gfx.Vertex, vertices[0..]); 33 | 34 | var dyn_vertices = [_]gfx.Vertex{ 35 | .{ .pos = .{ .x = 10, .y = 10 }, .uv = .{ .x = 0, .y = 0 } }, // tl 36 | .{ .pos = .{ .x = 100, .y = 10 }, .uv = .{ .x = 1, .y = 0 } }, // tr 37 | .{ .pos = .{ .x = 100, .y = 100 }, .uv = .{ .x = 1, .y = 1 } }, // br 38 | .{ .pos = .{ .x = 10, .y = 100 }, .uv = .{ .x = 0, .y = 1 } }, // bl 39 | }; 40 | var dyn_indices = [_]u16{ 0, 1, 2, 2, 3, 0 }; 41 | dyn_mesh = try gfx.DynamicMesh(u16, gfx.Vertex).init(std.heap.c_allocator, vertices.len, &dyn_indices); 42 | for (dyn_vertices, 0..) |_, i| { 43 | dyn_vertices[i].pos.x += 200; 44 | dyn_vertices[i].pos.y += 200; 45 | dyn_mesh.verts[i] = dyn_vertices[i]; 46 | } 47 | } 48 | 49 | fn update() !void { 50 | for (dyn_mesh.verts) |*vert| { 51 | vert.pos.x += 0.01; 52 | vert.pos.y += 0.01; 53 | } 54 | dyn_mesh.updateAllVerts(); 55 | } 56 | 57 | fn render() !void { 58 | gk.gfx.beginPass(.{ .color = math.Color.beige }); 59 | 60 | mesh.bindImage(tex.img, 0); 61 | mesh.draw(); 62 | 63 | dyn_mesh.bindImage(colored_tex.img, 0); 64 | dyn_mesh.drawAllVerts(); 65 | 66 | gk.gfx.endPass(); 67 | } 68 | -------------------------------------------------------------------------------- /gamekit/math/quad.zig: -------------------------------------------------------------------------------- 1 | const Vec2 = @import("vec2.zig").Vec2; 2 | const Rect = @import("rect.zig").Rect; 3 | const RectI = @import("rect.zig").RectI; 4 | 5 | pub const Quad = struct { 6 | img_w: f32, 7 | img_h: f32, 8 | positions: [4]Vec2 = undefined, 9 | uvs: [4]Vec2 = undefined, 10 | 11 | pub fn init(x: f32, y: f32, width: f32, height: f32, img_w: f32, img_h: f32) Quad { 12 | var q = Quad{ 13 | .img_w = img_w, 14 | .img_h = img_h, 15 | }; 16 | q.setViewport(x, y, width, height); 17 | 18 | return q; 19 | } 20 | 21 | pub fn setViewportRect(self: *Quad, viewport: Rect) void { 22 | self.setViewport(viewport.x, viewport.y, viewport.w, viewport.h); 23 | } 24 | 25 | pub fn setViewportRectI(self: *Quad, viewport: RectI) void { 26 | self.setViewport(@intToFloat(f32, viewport.x), @intToFloat(f32, viewport.y), @intToFloat(f32, viewport.w), @intToFloat(f32, viewport.h)); 27 | } 28 | 29 | pub fn setViewport(self: *Quad, x: f32, y: f32, width: f32, height: f32) void { 30 | self.positions[0] = Vec2{ .x = 0, .y = 0 }; // bl 31 | self.positions[1] = Vec2{ .x = width, .y = 0 }; // br 32 | self.positions[2] = Vec2{ .x = width, .y = height }; // tr 33 | self.positions[3] = Vec2{ .x = 0, .y = height }; // tl 34 | 35 | // squeeze texcoords in by 128th of a pixel to avoid bleed 36 | const w_tol = (1.0 / self.img_w) / 128.0; 37 | const h_tol = (1.0 / self.img_h) / 128.0; 38 | 39 | const inv_w = 1.0 / self.img_w; 40 | const inv_h = 1.0 / self.img_h; 41 | 42 | self.uvs[0] = Vec2{ .x = x * inv_w + w_tol, .y = y * inv_h + h_tol }; 43 | self.uvs[1] = Vec2{ .x = (x + width) * inv_w - w_tol, .y = y * inv_h + h_tol }; 44 | self.uvs[2] = Vec2{ .x = (x + width) * inv_w - w_tol, .y = (y + height) * inv_h - h_tol }; 45 | self.uvs[3] = Vec2{ .x = x * inv_w + w_tol, .y = (y + height) * inv_h - h_tol }; 46 | } 47 | 48 | /// sets the Quad to be the full size of the texture 49 | pub fn setFill(self: *Quad, img_w: f32, img_h: f32) void { 50 | self.setImageDimensions(img_w, img_h); 51 | self.setViewport(0, 0, img_w, img_h); 52 | } 53 | 54 | pub fn setImageDimensions(self: *Quad, w: f32, h: f32) void { 55 | self.img_w = w; 56 | self.img_h = h; 57 | } 58 | }; 59 | 60 | test "quad tests" { 61 | var q1 = Quad.init(0, 0, 50, 50, 600, 400); 62 | q1.setImageDimensions(600, 400); 63 | q1.setViewportRect(.{ .x = 0, .y = 0, .w = 50, .h = 0 }); 64 | } 65 | -------------------------------------------------------------------------------- /examples/stencil.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gl = @import("renderkit").gl; 3 | const gk = @import("gamekit"); 4 | const math = gk.math; 5 | const gfx = gk.gfx; 6 | const draw = gfx.draw; 7 | 8 | const Thing = struct { 9 | dir: f32, 10 | pos: math.Vec2 = .{}, 11 | col: math.Color, 12 | }; 13 | 14 | var white_tex: gfx.Texture = undefined; 15 | var pass: gfx.OffscreenPass = undefined; 16 | var points: [2]Thing = [_]Thing{ 17 | .{ .dir = 1, .pos = .{ .x = 60, .y = 300 }, .col = math.Color.red }, 18 | .{ .dir = -1, .pos = .{ .x = 600, .y = 300 }, .col = math.Color.blue }, 19 | }; 20 | 21 | pub fn main() !void { 22 | try gk.run(.{ 23 | .init = init, 24 | .update = update, 25 | .render = render, 26 | }); 27 | } 28 | 29 | fn init() !void { 30 | white_tex = gfx.Texture.initSingleColor(0xFFFFFFFF); 31 | 32 | const size = gk.window.size(); 33 | pass = gfx.OffscreenPass.initWithStencil(size.w, size.h, .nearest, .clamp); 34 | } 35 | 36 | fn update() !void { 37 | const speed: f32 = 5; 38 | const size = gk.window.size(); 39 | for (&points) |*p| { 40 | p.pos.x += p.dir * speed; 41 | if (p.pos.x + 0.30 * gk.time.rawDeltaTime() > @intToFloat(f32, size.w)) p.dir *= -1; 42 | if (p.pos.x - 0.30 * gk.time.rawDeltaTime() < 0) p.dir *= -1; 43 | } 44 | } 45 | 46 | fn render() !void { 47 | // offscreen rendering. set stencil to write 48 | gfx.setRenderState(.{ .stencil = .{ 49 | .enabled = true, 50 | .write_mask = 0xFF, 51 | .compare_func = .always, 52 | .ref = 1, 53 | .read_mask = 0xFF, 54 | } }); 55 | gk.gfx.beginPass(.{ 56 | .color = math.Color.purple, 57 | .pass = pass, 58 | .clear_stencil = true, 59 | }); 60 | draw.point(points[0].pos, 160, points[0].col); 61 | gk.gfx.endPass(); 62 | 63 | // set stencil to read 64 | gfx.setRenderState(.{ 65 | .stencil = .{ 66 | .enabled = true, 67 | .write_mask = 0x00, // disable writing to stencil 68 | .compare_func = .equal, 69 | .ref = 1, 70 | .read_mask = 0xFF, 71 | }, 72 | }); 73 | gk.gfx.beginPass(.{ 74 | .clear_color = false, 75 | .clear_stencil = false, 76 | .pass = pass, 77 | }); 78 | draw.point(points[1].pos, 60, points[1].col); 79 | gk.gfx.endPass(); 80 | 81 | // backbuffer rendering, reset stencil 82 | gfx.setRenderState(.{}); 83 | gk.gfx.beginPass(.{}); 84 | draw.tex(pass.color_texture, .{}); 85 | gk.gfx.endPass(); 86 | } 87 | -------------------------------------------------------------------------------- /gamekit/utils/fixed_list.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// fixed size array wrapper that provides ArrayList-like semantics. Appending more items than fit in the 4 | /// list ignores the item and logs a warning. Use the FixedList.len field to get the actual number of items present. 5 | pub fn FixedList(comptime T: type, comptime len: usize) type { 6 | return struct { 7 | const Self = @This(); 8 | 9 | items: [len]T = undefined, 10 | len: usize = 0, 11 | 12 | pub const Iterator = struct { 13 | list: Self, 14 | index: usize = 0, 15 | 16 | pub fn next(self: *Iterator) ?T { 17 | if (self.index == self.list.len) return null; 18 | var next_item = self.list.items[self.index]; 19 | self.index += 1; 20 | return next_item; 21 | } 22 | }; 23 | 24 | pub fn init() Self { 25 | return Self{ .items = [_]T{0} ** len }; 26 | } 27 | 28 | pub fn initWithValue(comptime value: T) Self { 29 | return Self{ .items = [_]T{value} ** len }; 30 | } 31 | 32 | pub fn append(self: *Self, item: T) void { 33 | if (self.len == self.items.len) { 34 | std.log.warn("attemped to append to a full FixedList\n", .{}); 35 | return; 36 | } 37 | self.items[self.len] = item; 38 | self.len += 1; 39 | } 40 | 41 | pub fn clear(self: *Self) void { 42 | self.len = 0; 43 | } 44 | 45 | pub fn pop(self: *Self) T { 46 | var item = self.items[self.len - 1]; 47 | self.len -= 1; 48 | return item; 49 | } 50 | 51 | pub fn contains(self: Self, value: T) bool { 52 | return self.indexOf(value) != null; 53 | } 54 | 55 | pub fn indexOf(self: Self, value: T) ?usize { 56 | var i: usize = 0; 57 | while (i < self.len) : (i += 1) { 58 | if (self.items[i] == value) return i; 59 | } 60 | return null; 61 | } 62 | 63 | /// Removes the element at the specified index and returns it. The empty slot is filled from the end of the list. 64 | pub fn swapRemove(self: *Self, i: usize) T { 65 | if (self.len - 1 == i) return self.pop(); 66 | 67 | const old_item = self.items[i]; 68 | self.items[i] = self.pop(); 69 | return old_item; 70 | } 71 | 72 | pub fn iter(self: Self) Iterator { 73 | return Iterator{ .list = self }; 74 | } 75 | }; 76 | } 77 | 78 | test "fixed list" { 79 | var list = FixedList(u32, 4).init(); 80 | list.append(4); 81 | std.testing.expectEqual(list.len, 1); 82 | 83 | list.append(46); 84 | list.append(146); 85 | list.append(4456); 86 | std.testing.expectEqual(list.len, 4); 87 | 88 | _ = list.pop(); 89 | std.testing.expectEqual(list.len, 3); 90 | 91 | list.clear(); 92 | std.testing.expectEqual(list.len, 0); 93 | } 94 | -------------------------------------------------------------------------------- /gamekit/graphics/offscreen_pass.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const rk = @import("renderkit"); 3 | const gfx = @import("../gamekit.zig").gfx; 4 | 5 | pub const OffscreenPass = struct { 6 | pass: rk.Pass, 7 | color_texture: gfx.Texture, 8 | color_texture2: ?gfx.Texture = null, 9 | color_texture3: ?gfx.Texture = null, 10 | color_texture4: ?gfx.Texture = null, 11 | depth_stencil_texture: ?gfx.Texture = null, 12 | 13 | pub fn init(width: i32, height: i32) OffscreenPass { 14 | return initWithOptions(width, height, .nearest, .clamp); 15 | } 16 | 17 | pub fn initMrt(width: i32, height: i32, tex_cnt: usize) OffscreenPass { 18 | var pass = OffscreenPass{ 19 | .pass = undefined, 20 | .color_texture = undefined, 21 | }; 22 | 23 | pass.color_texture = gfx.Texture.initOffscreen(width, height, .nearest, .clamp); 24 | pass.color_texture2 = if (tex_cnt > 1) gfx.Texture.initOffscreen(width, height, .nearest, .clamp) else null; 25 | pass.color_texture3 = if (tex_cnt > 2) gfx.Texture.initOffscreen(width, height, .nearest, .clamp) else null; 26 | pass.color_texture4 = if (tex_cnt > 3) gfx.Texture.initOffscreen(width, height, .nearest, .clamp) else null; 27 | 28 | var desc = rk.PassDesc{ 29 | .color_img = pass.color_texture.img, 30 | .color_img2 = if (pass.color_texture2) |t| t.img else null, 31 | .color_img3 = if (pass.color_texture3) |t| t.img else null, 32 | .color_img4 = if (pass.color_texture4) |t| t.img else null, 33 | }; 34 | 35 | pass.pass = rk.createPass(desc); 36 | return pass; 37 | } 38 | 39 | pub fn initWithOptions(width: i32, height: i32, filter: rk.TextureFilter, wrap: rk.TextureWrap) OffscreenPass { 40 | const color_tex = gfx.Texture.initOffscreen(width, height, filter, wrap); 41 | 42 | const pass = rk.createPass(.{ 43 | .color_img = color_tex.img, 44 | }); 45 | return .{ .pass = pass, .color_texture = color_tex }; 46 | } 47 | 48 | pub fn initWithStencil(width: i32, height: i32, filter: rk.TextureFilter, wrap: rk.TextureWrap) OffscreenPass { 49 | const color_tex = gfx.Texture.initOffscreen(width, height, filter, wrap); 50 | const depth_stencil_img = gfx.Texture.initStencil(width, height, filter, wrap); 51 | 52 | const pass = rk.createPass(.{ 53 | .color_img = color_tex.img, 54 | .depth_stencil_img = depth_stencil_img.img, 55 | }); 56 | return .{ .pass = pass, .color_texture = color_tex, .depth_stencil_texture = depth_stencil_img }; 57 | } 58 | 59 | pub fn deinit(self: *const OffscreenPass) void { 60 | // Pass MUST be destroyed first! It relies on the Textures being present. 61 | rk.destroyPass(self.pass); 62 | self.color_texture.deinit(); 63 | if (self.color_texture2) |t| t.deinit(); 64 | if (self.color_texture3) |t| t.deinit(); 65 | if (self.color_texture4) |t| t.deinit(); 66 | if (self.depth_stencil_texture) |depth_stencil| depth_stencil.deinit(); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /gamekit/deps/imgui/build.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const Builder = std.build.Builder; 4 | 5 | var framework_dir: ?[]u8 = null; 6 | const build_impl_type: enum { exe, static_lib, object_files } = .static_lib; 7 | 8 | pub fn build(b: *std.build.Builder) !void { 9 | const exe = b.addStaticLibrary("JunkLib", null); 10 | linkArtifact(b, exe, b.standardTargetOptions(.{}), .static, ""); 11 | exe.install(); 12 | } 13 | 14 | /// prefix_path is used to add package paths. It should be the the same path used to include this build file 15 | pub fn linkArtifact(b: *Builder, exe: *std.build.LibExeObjStep, target: std.zig.CrossTarget, comptime prefix_path: []const u8) void { 16 | if (prefix_path.len > 0 and !std.mem.endsWith(u8, prefix_path, "/")) @panic("prefix-path must end with '/' if it is not empty"); 17 | 18 | exe.linkLibCpp(); 19 | 20 | if (target.isWindows()) { 21 | exe.linkSystemLibrary("user32"); 22 | exe.linkSystemLibrary("gdi32"); 23 | } else if (target.isDarwin()) { 24 | const frameworks_dir = macosFrameworksDir(b) catch unreachable; 25 | exe.addFrameworkPath(frameworks_dir); 26 | exe.linkFramework("Foundation"); 27 | exe.linkFramework("Cocoa"); 28 | exe.linkFramework("Quartz"); 29 | exe.linkFramework("QuartzCore"); 30 | exe.linkFramework("Metal"); 31 | exe.linkFramework("MetalKit"); 32 | exe.linkFramework("OpenGL"); 33 | exe.linkFramework("AudioToolbox"); 34 | exe.linkFramework("CoreAudio"); 35 | exe.linkSystemLibrary("c++"); 36 | } else { 37 | exe.linkLibC(); 38 | exe.linkSystemLibrary("c++"); 39 | } 40 | 41 | const base_path = prefix_path ++ "gamekit/deps/imgui/"; 42 | exe.addIncludePath(base_path ++ "cimgui/imgui"); 43 | exe.addIncludePath(base_path ++ "cimgui/imgui/examples"); 44 | 45 | const cpp_args = [_][]const u8{"-Wno-return-type-c-linkage"}; 46 | exe.addCSourceFile(base_path ++ "cimgui/imgui/imgui.cpp", &cpp_args); 47 | exe.addCSourceFile(base_path ++ "cimgui/imgui/imgui_demo.cpp", &cpp_args); 48 | exe.addCSourceFile(base_path ++ "cimgui/imgui/imgui_draw.cpp", &cpp_args); 49 | exe.addCSourceFile(base_path ++ "cimgui/imgui/imgui_widgets.cpp", &cpp_args); 50 | exe.addCSourceFile(base_path ++ "cimgui/cimgui.cpp", &cpp_args); 51 | exe.addCSourceFile(base_path ++ "temporary_hacks.cpp", &cpp_args); 52 | } 53 | 54 | /// helper function to get SDK path on Mac 55 | fn macosFrameworksDir(b: *Builder) ![]u8 { 56 | if (framework_dir) |dir| return dir; 57 | 58 | var str = b.exec(&[_][]const u8{ "xcrun", "--show-sdk-path" }); 59 | const strip_newline = std.mem.lastIndexOf(u8, str, "\n"); 60 | if (strip_newline) |index| { 61 | str = str[0..index]; 62 | } 63 | framework_dir = try std.mem.concat(b.allocator, u8, &[_][]const u8{ str, "/System/Library/Frameworks" }); 64 | return framework_dir.?; 65 | } 66 | 67 | pub fn getModule(b: *std.Build, comptime prefix_path: []const u8) *std.build.Module { 68 | if (prefix_path.len > 0 and !std.mem.endsWith(u8, prefix_path, "/")) @panic("prefix-path must end with '/' if it is not empty"); 69 | return b.createModule(.{ 70 | .source_file = .{ .path = prefix_path ++ "gamekit/deps/imgui/imgui.zig" }, 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /examples/assets/shaders/shader_src.glsl: -------------------------------------------------------------------------------- 1 | @vs sprite_vs 2 | uniform VertexParams { 3 | vec4 transform_matrix[2]; 4 | }; 5 | 6 | layout(location = 0) in vec2 pos_in; 7 | layout(location = 1) in vec2 uv_in; 8 | layout(location = 2) in vec4 color_in; 9 | 10 | out vec2 uv_out; 11 | out vec4 color_out; 12 | 13 | void main() { 14 | uv_out = uv_in; 15 | color_out = color_in; 16 | mat3x2 transMat = mat3x2(transform_matrix[0].x, transform_matrix[0].y, transform_matrix[0].z, transform_matrix[0].w, transform_matrix[1].x, transform_matrix[1].y); 17 | 18 | gl_Position = vec4(transMat * vec3(pos_in, 1), 0, 1); 19 | } 20 | @end 21 | 22 | 23 | @block sprite_fs_main 24 | uniform sampler2D main_tex; 25 | 26 | in vec2 uv_out; 27 | in vec4 color_out; 28 | out vec4 frag_color; 29 | 30 | vec4 effect(sampler2D tex, vec2 tex_coord, vec4 vert_color); 31 | 32 | void main() { 33 | frag_color = effect(main_tex, uv_out.st, color_out); 34 | } 35 | @end 36 | 37 | 38 | @fs sprite_fs 39 | @include_block sprite_fs_main 40 | vec4 effect(sampler2D tex, vec2 tex_coord, vec4 vert_color) { 41 | return texture(tex, tex_coord) * vert_color; 42 | } 43 | @end 44 | 45 | @program sprite sprite_vs sprite_fs 46 | 47 | 48 | @fs mode7_fs 49 | @include_block sprite_fs_main 50 | uniform Mode7Params { 51 | float mapw; 52 | float maph; 53 | float x; 54 | float y; 55 | float zoom; 56 | float fov; 57 | float offset; 58 | float wrap; 59 | float x1, x2, y1, y2; 60 | }; 61 | uniform sampler2D map_tex; 62 | 63 | vec4 effect(sampler2D tex, vec2 tex_coord, vec4 vert_color) { 64 | mat2 rotation = mat2(x1, y1, x2, y2); 65 | vec2 uv = vec2( 66 | (0.5 - tex_coord.x) * zoom, 67 | (offset - tex_coord.y) * (zoom / fov) 68 | ) * rotation; 69 | vec2 uv2 = vec2( 70 | (uv.x / tex_coord.y + x) / mapw, 71 | (uv.y / tex_coord.y + y) / maph 72 | ); 73 | 74 | if (wrap == 0 && (uv2.x < 0.0 || uv2.x > 1.0 || uv2.y < 0.0 || uv2.y > 1.0)) { 75 | return vec4(0.0, 0.0, 0.0, 0.0); 76 | } else { 77 | return (texture(map_tex, mod(uv2, 1.0) ) * vert_color); 78 | } 79 | } 80 | @end 81 | 82 | @program mode7 sprite_vs mode7_fs 83 | 84 | 85 | @fs mrt_fs 86 | uniform sampler2D main_tex; 87 | 88 | in vec2 uv_out; 89 | in vec4 color_out; 90 | 91 | layout(location = 0) out vec4 frag_color_0; 92 | layout(location = 1) out vec4 frag_color_1; 93 | 94 | void main() { 95 | frag_color_0 = texture(main_tex, uv_out.st); 96 | frag_color_1 = texture(main_tex, uv_out.st) * color_out; 97 | } 98 | @end 99 | 100 | @program mrt sprite_vs mrt_fs 101 | 102 | 103 | 104 | @vs vert_sway_vs 105 | uniform VertexSwayParams { 106 | vec4 transform_matrix[2]; 107 | float time; 108 | }; 109 | 110 | layout(location = 0) in vec2 pos_in; 111 | layout(location = 1) in vec2 uv_in; 112 | layout(location = 2) in vec4 color_in; 113 | 114 | out vec2 uv_out; 115 | out vec4 color_out; 116 | 117 | void main() { 118 | uv_out = uv_in; 119 | color_out = color_in; 120 | mat3x2 transMat = mat3x2(transform_matrix[0].x, transform_matrix[0].y, transform_matrix[0].z, transform_matrix[0].w, transform_matrix[1].x, transform_matrix[1].y); 121 | 122 | vec2 pos = pos_in; 123 | pos.x += (sin(time) * 20) * (1 - uv_in.y); 124 | gl_Position = vec4(transMat * vec3(pos, 1), 0, 1); 125 | } 126 | @end 127 | 128 | @program vert_sway vert_sway_vs sprite_fs 129 | 130 | 131 | #@include example_include_commented_out.glsl -------------------------------------------------------------------------------- /gamekit/gamekit.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sdl = @import("sdl"); 3 | const imgui_impl = @import("imgui/implementation.zig"); 4 | 5 | pub const imgui = @import("imgui"); 6 | pub const renderkit = @import("renderkit"); 7 | pub const utils = @import("utils/utils.zig"); 8 | pub const math = @import("math/math.zig"); 9 | 10 | const Gfx = @import("gfx.zig").Gfx; 11 | const Window = @import("window.zig").Window; 12 | const WindowConfig = @import("window.zig").WindowConfig; 13 | const Input = @import("input.zig").Input; 14 | const Time = @import("time.zig").Time; 15 | 16 | pub const Config = struct { 17 | init: fn () anyerror!void, 18 | update: ?fn () anyerror!void = null, 19 | render: fn () anyerror!void, 20 | shutdown: ?fn () anyerror!void = null, 21 | 22 | window: WindowConfig = WindowConfig{}, 23 | 24 | update_rate: f64 = 60, // desired fps 25 | imgui_icon_font: bool = true, 26 | imgui_viewports: bool = false, // whether imgui viewports should be enabled 27 | imgui_docking: bool = true, // whether imgui docking should be enabled 28 | }; 29 | 30 | // search path: root.build_options, root.enable_imgui, default to false 31 | pub const enable_imgui: bool = if (@hasDecl(@import("root"), "build_options")) 32 | blk: { 33 | break :blk @field(@import("root"), "build_options").enable_imgui; 34 | } else if (@hasDecl(@import("root"), "enable_imgui")) 35 | blk: { 36 | break :blk @field(@import("root"), "enable_imgui"); 37 | } else blk: { 38 | break :blk false; 39 | }; 40 | 41 | pub const gfx = @import("gfx.zig"); 42 | pub var window: Window = undefined; 43 | pub var time: Time = undefined; 44 | pub var input: Input = undefined; 45 | 46 | pub fn run(comptime config: Config) !void { 47 | window = try Window.init(config.window); 48 | 49 | renderkit.setup(.{ 50 | .gl_loader = sdl.SDL_GL_GetProcAddress, 51 | }, std.heap.c_allocator); 52 | 53 | gfx.init(); 54 | time = Time.init(config.update_rate); 55 | input = Input.init(window.scale()); 56 | 57 | if (enable_imgui) imgui_impl.init(window.sdl_window, config.imgui_docking, config.imgui_viewports, config.imgui_icon_font); 58 | 59 | try config.init(); 60 | 61 | while (!pollEvents()) { 62 | time.tick(); 63 | if (config.update) |update| try update(); 64 | try config.render(); 65 | 66 | if (enable_imgui) { 67 | gfx.beginPass(.{ .clear_color = false }); 68 | imgui_impl.render(); 69 | gfx.endPass(); 70 | _ = sdl.SDL_GL_MakeCurrent(window.sdl_window, window.gl_ctx); 71 | } 72 | 73 | sdl.SDL_GL_SwapWindow(window.sdl_window); 74 | gfx.commitFrame(); 75 | input.newFrame(); 76 | } 77 | 78 | if (enable_imgui) imgui_impl.deinit(); 79 | if (config.shutdown) |shutdown| try shutdown(); 80 | gfx.deinit(); 81 | renderkit.shutdown(); 82 | window.deinit(); 83 | sdl.SDL_Quit(); 84 | } 85 | 86 | fn pollEvents() bool { 87 | var event: sdl.SDL_Event = undefined; 88 | while (sdl.SDL_PollEvent(&event) != 0) { 89 | if (enable_imgui and imgui_impl.handleEvent(&event)) continue; 90 | 91 | switch (event.type) { 92 | sdl.SDL_QUIT => return true, 93 | sdl.SDL_WINDOWEVENT => { 94 | if (event.window.windowID == window.id) { 95 | if (event.window.event == sdl.SDL_WINDOWEVENT_CLOSE) return true; 96 | window.handleEvent(&event.window); 97 | } 98 | }, 99 | else => input.handleEvent(&event), 100 | } 101 | } 102 | 103 | // if ImGui is running we force a timer resync every frame. This ensures we get exactly one update call and one render call 104 | // each frame which prevents ImGui from flickering due to skipped/doubled update calls. 105 | if (enable_imgui) { 106 | imgui_impl.newFrame(); 107 | time.resync(); 108 | } 109 | 110 | return false; 111 | } 112 | -------------------------------------------------------------------------------- /gamekit/deps/imgui/temporary_hacks.cpp: -------------------------------------------------------------------------------- 1 | #include "cimgui/imgui/imgui.h" 2 | #include "cimgui/imgui/imgui_internal.h" 3 | #include "cimgui/cimgui.h" 4 | 5 | CIMGUI_API void _ogImDrawData_ScaleClipRects(ImDrawData* self, const float fb_scale) { 6 | ImVec2 fb_scale_vec; 7 | fb_scale_vec.x = fb_scale; 8 | fb_scale_vec.y = fb_scale; 9 | self->ScaleClipRects(fb_scale_vec); 10 | } 11 | 12 | // workaround for Windows not functioning with ImVec4s 13 | CIMGUI_API void _ogImage(ImTextureID user_texture_id, const ImVec2* size, const ImVec2* uv0, const ImVec2* uv1) { 14 | ImVec4 tint_col; 15 | tint_col.x = tint_col.y = tint_col.z = tint_col.w = 1; 16 | ImVec4 border_col; 17 | return ImGui::Image(user_texture_id, *size, *uv0, *uv1, tint_col, border_col); 18 | } 19 | 20 | CIMGUI_API bool _ogImageButton(ImTextureID user_texture_id, const ImVec2* size, const ImVec2* uv0, const ImVec2* uv1, int frame_padding) { 21 | ImVec4 tint_col; 22 | tint_col.x = tint_col.y = tint_col.z = tint_col.w = 1; 23 | ImVec4 border_col; 24 | return ImGui::ImageButton(user_texture_id, *size, *uv0, *uv1, frame_padding, border_col, tint_col); 25 | } 26 | 27 | CIMGUI_API void _ogColoredText(float r, float g, float b, const char* text) { 28 | ImVec4 tint_col; 29 | tint_col.x = r; 30 | tint_col.y = g; 31 | tint_col.z = b; 32 | tint_col.w = 1; 33 | ImGui::TextColored(tint_col, text); 34 | } 35 | 36 | // M1 needs this for some reason... 37 | CIMGUI_API bool _ogButton(const char* label, const float x, const float y) { 38 | ImVec2 size; 39 | size.x = x; 40 | size.y = y; 41 | return ImGui::Button(label, size); 42 | } 43 | 44 | CIMGUI_API void _ogDockBuilderSetNodeSize(ImGuiID node_id, const ImVec2* size) { 45 | return ImGui::DockBuilderSetNodeSize(node_id, *size); 46 | } 47 | 48 | CIMGUI_API void _ogSetNextWindowPos(const ImVec2* pos, ImGuiCond cond, const ImVec2* pivot) { 49 | return ImGui::SetNextWindowPos(*pos, cond, *pivot); 50 | } 51 | 52 | CIMGUI_API void _ogSetNextWindowSize(const ImVec2* size, ImGuiCond cond) { 53 | return ImGui::SetNextWindowSize(*size, cond); 54 | } 55 | 56 | CIMGUI_API void _ogPushStyleVarVec2(ImGuiStyleVar idx, const float x, const float y) { 57 | ImVec2 pos; 58 | pos.x = x; 59 | pos.y = y; 60 | return ImGui::PushStyleVar(idx, pos); 61 | } 62 | 63 | CIMGUI_API bool _ogInvisibleButton(const char* str_id, const float w, const float h, ImGuiButtonFlags flags) { 64 | ImVec2 size; 65 | size.x = w; 66 | size.y = h; 67 | return ImGui::InvisibleButton(str_id, size, flags); 68 | } 69 | 70 | CIMGUI_API bool _ogSelectableBool(const char* label, bool selected, ImGuiSelectableFlags flags, const float w, const float h) { 71 | ImVec2 size; 72 | size.x = w; 73 | size.y = h; 74 | return ImGui::Selectable(label, selected, flags, size); 75 | } 76 | 77 | CIMGUI_API void _ogDummy(const float w, const float h) { 78 | ImVec2 size; 79 | size.x = w; 80 | size.y = h; 81 | return ImGui::Dummy(size); 82 | } 83 | 84 | CIMGUI_API bool _ogBeginChildFrame(ImGuiID id, const float w, const float h, ImGuiWindowFlags flags) { 85 | ImVec2 size; 86 | size.x = w; 87 | size.y = h; 88 | return ImGui::BeginChildFrame(id, size, flags); 89 | } 90 | 91 | CIMGUI_API void _ogDockSpace(ImGuiID id, const float w, const float h, ImGuiDockNodeFlags flags, const ImGuiWindowClass* window_class) { 92 | ImVec2 size; 93 | size.x = w; 94 | size.y = h; 95 | return ImGui::DockSpace(id, size, flags, window_class); 96 | } 97 | 98 | CIMGUI_API void _ogImDrawList_AddQuad(ImDrawList* self, const ImVec2* p1, const ImVec2* p2, const ImVec2* p3, const ImVec2* p4, ImU32 col, float thickness) { 99 | return self->AddQuad(*p1, *p2, *p3, *p4, col, thickness); 100 | } 101 | 102 | CIMGUI_API void _ogImDrawList_AddQuadFilled(ImDrawList* self, const ImVec2* p1, const ImVec2* p2, const ImVec2* p3, const ImVec2* p4, ImU32 col) { 103 | return self->AddQuadFilled(*p1, *p2, *p3, *p4, col); 104 | } 105 | 106 | CIMGUI_API void _ogSetCursorScreenPos(const ImVec2* pos) { 107 | return ImGui::SetCursorScreenPos(*pos); 108 | } 109 | 110 | CIMGUI_API bool _ogListBoxHeaderVec2(const char* label, const ImVec2* size) { 111 | return ImGui::ListBoxHeader(label, *size); 112 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "options": { 6 | "env": { 7 | "ZIG_SYSTEM_LINKER_HACK": "1", 8 | "MTL_SHADER_VALIDATION": "1", 9 | "MTL_SHADER_VALIDATION_GLOBAL_MEMORY": "1", 10 | "MTL_SHADER_VALIDATION_TEXTURE_USAGE": "1", 11 | "MTL_DEBUG_LAYER": "1", 12 | "METAL_DEVICE_WRAPPER_TYPE": "1", 13 | }, 14 | }, 15 | "tasks": [ 16 | { 17 | "label": "Build Project", 18 | "type": "shell", 19 | "command": "zig build", 20 | "problemMatcher": [ 21 | "$gcc" 22 | ], 23 | }, 24 | { 25 | "label": "Build and Run Project", 26 | "type": "shell", 27 | "command": "zig build run", 28 | "problemMatcher": [ 29 | "$gcc" 30 | ], 31 | "group": { 32 | "kind": "build", 33 | "isDefault": true 34 | }, 35 | "presentation": { 36 | "clear": true 37 | } 38 | }, 39 | { 40 | "label": "Build and Run Project (x64 on arm)", 41 | "type": "shell", 42 | "command": "~/zig/zig-x64/zig build run", 43 | "problemMatcher": [ 44 | "$gcc" 45 | ], 46 | "group": "build", 47 | "presentation": { 48 | "clear": true 49 | } 50 | }, 51 | { 52 | "label": "Build and Run Project (release-fast)", 53 | "type": "shell", 54 | "command": "zig build run -Drelease-fast", 55 | "problemMatcher": [ 56 | "$gcc" 57 | ], 58 | "group": { 59 | "kind": "build", 60 | "isDefault": true 61 | }, 62 | "presentation": { 63 | "clear": true 64 | } 65 | }, 66 | { 67 | "label": "Build and Run Project (release-small)", 68 | "type": "shell", 69 | "command": "zig build run -Drelease-small", 70 | "problemMatcher": [ 71 | "$gcc" 72 | ], 73 | "group": { 74 | "kind": "build", 75 | "isDefault": true 76 | }, 77 | "presentation": { 78 | "clear": true 79 | } 80 | }, 81 | { 82 | "label": "Test Project", 83 | "type": "shell", 84 | "command": "zig build test", 85 | "problemMatcher": [ 86 | "$gcc" 87 | ], 88 | "group": "build", 89 | "presentation": { 90 | "clear": true, 91 | }, 92 | }, 93 | { 94 | "label": "Build and Run Tests in Current File", 95 | "type": "shell", 96 | "command": "zig test ${file}", 97 | "problemMatcher": [ 98 | "$gcc" 99 | ], 100 | "presentation": { 101 | "clear": true 102 | }, 103 | "group": "build", 104 | }, 105 | { 106 | "label": "Compile Shaders", 107 | "type": "shell", 108 | "command": "zig build compile-shaders", 109 | "problemMatcher": [ 110 | "$gcc" 111 | ], 112 | "group": "build", 113 | "presentation": { 114 | "clear": true 115 | } 116 | }, 117 | { 118 | "label": "apitrace run executable", 119 | "type": "shell", 120 | "command": "apitrace", 121 | "args": [ 122 | "trace", 123 | "--api", 124 | "gl", 125 | "${workspaceFolder}/zig-cache/bin/run" 126 | ], 127 | "problemMatcher": [ 128 | "$gcc" 129 | ], 130 | "presentation": { 131 | "clear": true 132 | }, 133 | "group": "build", 134 | }, 135 | ] 136 | } -------------------------------------------------------------------------------- /gamekit/deps/stb/stb_image.zig: -------------------------------------------------------------------------------- 1 | pub const STBI_default = @enumToInt(enum_unnamed_1.STBI_default); 2 | pub const STBI_grey = @enumToInt(enum_unnamed_1.STBI_grey); 3 | pub const STBI_grey_alpha = @enumToInt(enum_unnamed_1.STBI_grey_alpha); 4 | pub const STBI_rgb = @enumToInt(enum_unnamed_1.STBI_rgb); 5 | pub const STBI_rgb_alpha = @enumToInt(enum_unnamed_1.STBI_rgb_alpha); 6 | const enum_unnamed_1 = enum(c_int) { 7 | STBI_default = 0, 8 | STBI_grey = 1, 9 | STBI_grey_alpha = 2, 10 | STBI_rgb = 3, 11 | STBI_rgb_alpha = 4, 12 | _, 13 | }; 14 | 15 | pub const stbi_uc = u8; 16 | pub const stbi_us = c_ushort; 17 | const struct_unnamed_9 = extern struct { 18 | read: ?fn (?*anyopaque, [*c]u8, c_int) callconv(.C) c_int, 19 | skip: ?fn (?*anyopaque, c_int) callconv(.C) void, 20 | eof: ?fn (?*anyopaque) callconv(.C) c_int, 21 | }; 22 | pub const stbi_io_callbacks = struct_unnamed_9; 23 | pub extern fn stbi_load(filename: [*]const u8, x: [*c]c_int, y: [*c]c_int, channels_in_file: [*c]c_int, desired_channels: c_int) [*c]stbi_uc; 24 | pub extern fn stbi_load_from_memory(buffer: [*c]const stbi_uc, len: c_int, x: [*c]c_int, y: [*c]c_int, channels_in_file: [*c]c_int, desired_channels: c_int) [*c]stbi_uc; 25 | pub extern fn stbi_load_from_callbacks(clbk: [*c]const stbi_io_callbacks, user: ?*anyopaque, x: [*c]c_int, y: [*c]c_int, channels_in_file: [*c]c_int, desired_channels: c_int) [*c]stbi_uc; 26 | pub extern fn stbi_load_gif_from_memory(buffer: [*c]const stbi_uc, len: c_int, delays: [*c][*c]c_int, x: [*c]c_int, y: [*c]c_int, z: [*c]c_int, comp: [*c]c_int, req_comp: c_int) [*c]stbi_uc; 27 | pub extern fn stbi_load_16_from_memory(buffer: [*c]const stbi_uc, len: c_int, x: [*c]c_int, y: [*c]c_int, channels_in_file: [*c]c_int, desired_channels: c_int) [*c]stbi_us; 28 | pub extern fn stbi_load_16_from_callbacks(clbk: [*c]const stbi_io_callbacks, user: ?*anyopaque, x: [*c]c_int, y: [*c]c_int, channels_in_file: [*c]c_int, desired_channels: c_int) [*c]stbi_us; 29 | pub extern fn stbi_loadf_from_memory(buffer: [*c]const stbi_uc, len: c_int, x: [*c]c_int, y: [*c]c_int, channels_in_file: [*c]c_int, desired_channels: c_int) [*c]f32; 30 | pub extern fn stbi_loadf_from_callbacks(clbk: [*c]const stbi_io_callbacks, user: ?*anyopaque, x: [*c]c_int, y: [*c]c_int, channels_in_file: [*c]c_int, desired_channels: c_int) [*c]f32; 31 | pub extern fn stbi_hdr_to_ldr_gamma(gamma: f32) void; 32 | pub extern fn stbi_hdr_to_ldr_scale(scale: f32) void; 33 | pub extern fn stbi_ldr_to_hdr_gamma(gamma: f32) void; 34 | pub extern fn stbi_ldr_to_hdr_scale(scale: f32) void; 35 | pub extern fn stbi_is_hdr_from_callbacks(clbk: [*c]const stbi_io_callbacks, user: ?*anyopaque) c_int; 36 | pub extern fn stbi_is_hdr_from_memory(buffer: [*c]const stbi_uc, len: c_int) c_int; 37 | pub extern fn stbi_failure_reason() [*c]const u8; 38 | pub extern fn stbi_image_free(retval_from_stbi_load: ?*anyopaque) void; 39 | pub extern fn stbi_info_from_memory(buffer: [*c]const stbi_uc, len: c_int, x: [*c]c_int, y: [*c]c_int, comp: [*c]c_int) c_int; 40 | pub extern fn stbi_info_from_callbacks(clbk: [*c]const stbi_io_callbacks, user: ?*anyopaque, x: [*c]c_int, y: [*c]c_int, comp: [*c]c_int) c_int; 41 | pub extern fn stbi_is_16_bit_from_memory(buffer: [*c]const stbi_uc, len: c_int) c_int; 42 | pub extern fn stbi_is_16_bit_from_callbacks(clbk: [*c]const stbi_io_callbacks, user: ?*anyopaque) c_int; 43 | pub extern fn stbi_set_unpremultiply_on_load(flag_true_if_should_unpremultiply: c_int) void; 44 | pub extern fn stbi_convert_iphone_png_to_rgb(flag_true_if_should_convert: c_int) void; 45 | pub extern fn stbi_set_flip_vertically_on_load(flag_true_if_should_flip: c_int) void; 46 | pub extern fn stbi_set_flip_vertically_on_load_thread(flag_true_if_should_flip: c_int) void; 47 | pub extern fn stbi_zlib_decode_malloc_guesssize(buffer: [*c]const u8, len: c_int, initial_size: c_int, outlen: [*c]c_int) [*c]u8; 48 | pub extern fn stbi_zlib_decode_malloc_guesssize_headerflag(buffer: [*c]const u8, len: c_int, initial_size: c_int, outlen: [*c]c_int, parse_header: c_int) [*c]u8; 49 | pub extern fn stbi_zlib_decode_malloc(buffer: [*c]const u8, len: c_int, outlen: [*c]c_int) [*c]u8; 50 | pub extern fn stbi_zlib_decode_buffer(obuffer: [*c]u8, olen: c_int, ibuffer: [*c]const u8, ilen: c_int) c_int; 51 | pub extern fn stbi_zlib_decode_noheader_malloc(buffer: [*c]const u8, len: c_int, outlen: [*c]c_int) [*c]u8; 52 | pub extern fn stbi_zlib_decode_noheader_buffer(obuffer: [*c]u8, olen: c_int, ibuffer: [*c]const u8, ilen: c_int) c_int; 53 | -------------------------------------------------------------------------------- /gamekit/graphics/multi_batcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const rk = @import("renderkit"); 3 | const gk = @import("../gamekit.zig"); 4 | const math = gk.math; 5 | 6 | const IndexBuffer = rk.IndexBuffer; 7 | const VertexBuffer = rk.VertexBuffer; 8 | 9 | pub const MultiVertex = extern struct { 10 | pos: math.Vec2, 11 | uv: math.Vec2 = .{ .x = 0, .y = 0 }, 12 | col: u32 = 0xFFFFFFFF, 13 | tid: f32 = 0, 14 | }; 15 | 16 | pub const MultiBatcher = struct { 17 | mesh: gk.gfx.DynamicMesh(u16, MultiVertex), 18 | vert_index: usize = 0, // current index into the vertex array 19 | textures: [8]rk.Image = undefined, 20 | last_texture: usize = 0, 21 | 22 | pub fn init(allocator: std.mem.Allocator, max_sprites: usize) MultiBatcher { 23 | if (max_sprites * 6 > std.math.maxInt(u16)) @panic("max_sprites exceeds u16 index buffer size"); 24 | 25 | var indices = allocator.alloc(u16, max_sprites * 6) catch unreachable; 26 | var i: usize = 0; 27 | while (i < max_sprites) : (i += 1) { 28 | indices[i * 3 * 2 + 0] = @intCast(u16, i) * 4 + 0; 29 | indices[i * 3 * 2 + 1] = @intCast(u16, i) * 4 + 1; 30 | indices[i * 3 * 2 + 2] = @intCast(u16, i) * 4 + 2; 31 | indices[i * 3 * 2 + 3] = @intCast(u16, i) * 4 + 0; 32 | indices[i * 3 * 2 + 4] = @intCast(u16, i) * 4 + 2; 33 | indices[i * 3 * 2 + 5] = @intCast(u16, i) * 4 + 3; 34 | } 35 | 36 | return .{ 37 | .mesh = gk.gfx.DynamicMesh(u16, MultiVertex).init(allocator, max_sprites * 4, indices) catch unreachable, 38 | .textures = [_]rk.Image{0} ** 8, 39 | }; 40 | } 41 | 42 | pub fn deinit(self: *MultiBatcher) void { 43 | self.mesh.deinit(); 44 | } 45 | 46 | pub fn begin(self: *MultiBatcher) void { 47 | self.vert_index = 0; 48 | } 49 | 50 | pub fn end(self: *MultiBatcher) void { 51 | self.flush(); 52 | } 53 | 54 | pub fn flush(self: *MultiBatcher) void { 55 | if (self.vert_index == 0) return; 56 | 57 | // send data to gpu 58 | self.mesh.updateVertSlice(self.vert_index); 59 | 60 | // bind textures 61 | for (self.textures, 0..) |tid, slot| { 62 | if (slot == self.last_texture) break; 63 | self.mesh.bindImage(tid, @intCast(c_uint, slot)); 64 | } 65 | 66 | // draw 67 | const quads = @divExact(self.vert_index, 4); 68 | self.mesh.draw(0, @intCast(c_int, quads * 6)); 69 | 70 | // reset state 71 | for (self.textures, 0..) |*tid, slot| { 72 | if (slot == self.last_texture) break; 73 | self.mesh.bindImage(tid.*, @intCast(c_uint, slot)); 74 | tid.* = 0; 75 | } 76 | 77 | self.vert_index = 0; 78 | self.last_texture = 0; 79 | } 80 | 81 | inline fn submitTexture(self: *MultiBatcher, img: rk.Image) f32 { 82 | if (std.mem.indexOfScalar(rk.Image, &self.textures, img)) |index| return @intToFloat(f32, index); 83 | 84 | self.textures[self.last_texture] = img; 85 | self.last_texture += 1; 86 | return @intToFloat(f32, self.last_texture - 1); 87 | } 88 | 89 | pub fn drawTex(self: *MultiBatcher, pos: math.Vec2, col: u32, texture: gk.gfx.Texture) void { 90 | if (self.vert_index >= self.mesh.verts.len) { 91 | self.flush(); 92 | } 93 | 94 | const tid = self.submitTexture(texture.img); 95 | 96 | var verts = self.mesh.verts[self.vert_index .. self.vert_index + 4]; 97 | verts[0].pos = pos; // tl 98 | verts[0].uv = .{ .x = 0, .y = 0 }; 99 | verts[0].col = col; 100 | verts[0].tid = tid; 101 | 102 | verts[1].pos = .{ .x = pos.x + texture.width, .y = pos.y }; // tr 103 | verts[1].uv = .{ .x = 1, .y = 0 }; 104 | verts[1].col = col; 105 | verts[1].tid = tid; 106 | 107 | verts[2].pos = .{ .x = pos.x + texture.width, .y = pos.y + texture.height }; // br 108 | verts[2].uv = .{ .x = 1, .y = 1 }; 109 | verts[2].col = col; 110 | verts[2].tid = tid; 111 | 112 | verts[3].pos = .{ .x = pos.x, .y = pos.y + texture.height }; // bl 113 | verts[3].uv = .{ .x = 0, .y = 1 }; 114 | verts[3].col = col; 115 | verts[3].tid = tid; 116 | 117 | self.vert_index += 4; 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /gamekit/deps/fontstash/fontstash.zig: -------------------------------------------------------------------------------- 1 | pub const Context = extern struct { 2 | params: Params, 3 | itw: f32, 4 | ith: f32, 5 | tex_data: [*c]u8, 6 | // omitted rest of struct 7 | 8 | pub fn init(params: *Params) !*Context { 9 | return fonsCreateInternal(params) orelse error.FailedToCreateFONS; 10 | } 11 | 12 | pub fn deinit(self: *Context) void { 13 | fonsDeleteInternal(self); 14 | } 15 | }; 16 | 17 | pub const Params = extern struct { 18 | width: c_int = 256, 19 | height: c_int = 256, 20 | flags: Flags = .top_left, 21 | user_ptr: *anyopaque, 22 | renderCreate: ?*const fn (?*anyopaque, c_int, c_int) callconv(.C) c_int = null, 23 | renderResize: ?*const fn (?*anyopaque, c_int, c_int) callconv(.C) c_int = null, 24 | renderUpdate: ?*const fn (?*anyopaque, [*c]c_int, [*c]const u8) callconv(.C) c_int = null, 25 | }; 26 | 27 | pub const Flags = enum(u8) { 28 | top_left = 1, 29 | bottom_left = 2, 30 | }; 31 | 32 | pub const Align = enum(c_int) { 33 | // horizontal 34 | left = 1, // Default 35 | center = 2, 36 | right = 4, 37 | // vertical 38 | top = 8, 39 | middle = 16, 40 | bottom = 32, 41 | baseline = 64, 42 | default = 65, 43 | // combos 44 | left_middle = 17, 45 | center_middle = 18, 46 | right_middle = 20, 47 | top_left = 9, 48 | }; 49 | 50 | pub const ErrorCode = enum(c_int) { 51 | atlas_full = 1, 52 | scratch_full = 2, // Scratch memory used to render glyphs is full, requested size reported in 'val', you may need to bump up FONS_SCRATCH_BUF_SIZE. 53 | overflow = 3, // Calls to fonsPushState has created too large stack, if you need deep state stack bump up FONS_MAX_STATES. 54 | underflow = 4, // Trying to pop too many states fonsPopState(). 55 | }; 56 | 57 | pub const Quad = extern struct { 58 | x0: f32, 59 | y0: f32, 60 | s0: f32, 61 | t0: f32, 62 | x1: f32, 63 | y1: f32, 64 | s1: f32, 65 | t1: f32, 66 | }; 67 | 68 | pub const FONSfont = opaque {}; 69 | 70 | pub const TextIter = extern struct { 71 | x: f32, 72 | y: f32, 73 | nextx: f32, 74 | nexty: f32, 75 | scale: f32, 76 | spacing: f32, 77 | color: c_uint, 78 | codepoint: c_uint, 79 | isize: c_short, 80 | iblur: c_short, 81 | font: ?*FONSfont, 82 | prevGlyphIndex: c_int, 83 | str: [*c]const u8, 84 | next: [*c]const u8, 85 | end: [*c]const u8, 86 | utf8state: c_uint, 87 | }; 88 | 89 | pub const FONS_INVALID = -1; 90 | 91 | pub extern fn fonsCreateInternal(params: [*c]Params) ?*Context; 92 | pub extern fn fonsDeleteInternal(s: ?*Context) void; 93 | pub extern fn fonsSetErrorCallback(s: ?*Context, callback: ?fn (?*anyopaque, c_int, c_int) callconv(.C) void, uptr: ?*anyopaque) void; 94 | pub extern fn fonsGetAtlasSize(s: ?*Context, width: [*c]c_int, height: [*c]c_int) void; 95 | pub extern fn fonsExpandAtlas(s: ?*Context, width: c_int, height: c_int) c_int; 96 | pub extern fn fonsResetAtlas(stash: ?*Context, width: c_int, height: c_int) c_int; 97 | pub extern fn fonsAddFontMem(stash: ?*Context, name: [*c]const u8, data: [*c]const u8, dataSize: c_int, freeData: c_int) c_int; 98 | pub extern fn fonsGetFontByName(s: ?*Context, name: [*c]const u8) c_int; 99 | pub extern fn fonsAddFallbackFont(stash: ?*Context, base: c_int, fallback: c_int) c_int; 100 | pub extern fn fonsPushState(s: ?*Context) void; 101 | pub extern fn fonsPopState(s: ?*Context) void; 102 | pub extern fn fonsClearState(s: ?*Context) void; 103 | pub extern fn fonsSetSize(s: ?*Context, size: f32) void; 104 | pub extern fn fonsSetColor(s: ?*Context, color: c_uint) void; 105 | pub extern fn fonsSetSpacing(s: ?*Context, spacing: f32) void; 106 | pub extern fn fonsSetBlur(s: ?*Context, blur: f32) void; 107 | pub extern fn fonsSetAlign(s: ?*Context, alignment: Align) void; 108 | pub extern fn fonsSetFont(s: ?*Context, font: c_int) void; 109 | pub extern fn fonsTextBounds(s: ?*Context, x: f32, y: f32, string: [*c]const u8, end: [*c]const u8, bounds: [*c]f32) f32; 110 | pub extern fn fonsLineBounds(s: ?*Context, y: f32, miny: [*c]f32, maxy: [*c]f32) void; 111 | pub extern fn fonsVertMetrics(s: ?*Context, ascender: [*c]f32, descender: [*c]f32, lineh: [*c]f32) void; 112 | pub extern fn fonsTextIterInit(stash: ?*Context, iter: [*c]TextIter, x: f32, y: f32, str: [*c]const u8, len: c_int) c_int; 113 | pub extern fn fonsTextIterNext(stash: ?*Context, iter: [*c]TextIter, quad: [*c]Quad) c_int; 114 | pub extern fn fonsGetTextureData(stash: ?*Context, width: [*c]c_int, height: [*c]c_int) [*c]const u8; 115 | pub extern fn fonsValidateTexture(s: ?*Context, dirty: [*c]c_int) c_int; 116 | -------------------------------------------------------------------------------- /gamekit/gfx.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gamekit = @import("gamekit.zig"); 3 | const renderkit = @import("renderkit"); 4 | const math = gamekit.math; 5 | 6 | // high level wrapper objects that use the low-level backend api 7 | pub const Texture = @import("graphics/texture.zig").Texture; 8 | pub const OffscreenPass = @import("graphics/offscreen_pass.zig").OffscreenPass; 9 | pub const Shader = @import("graphics/shader.zig").Shader; 10 | pub const ShaderState = @import("graphics/shader.zig").ShaderState; 11 | 12 | // even higher level wrappers for 2D game dev 13 | pub const Mesh = @import("graphics/mesh.zig").Mesh; 14 | pub const DynamicMesh = @import("graphics/mesh.zig").DynamicMesh; 15 | 16 | pub const Batcher = @import("graphics/batcher.zig").Batcher; 17 | pub const MultiBatcher = @import("graphics/multi_batcher.zig").MultiBatcher; 18 | pub const TriangleBatcher = @import("graphics/triangle_batcher.zig").TriangleBatcher; 19 | 20 | pub const FontBook = @import("graphics/fontbook.zig").FontBook; 21 | 22 | pub const Vertex = extern struct { 23 | pos: math.Vec2 = .{ .x = 0, .y = 0 }, 24 | uv: math.Vec2 = .{ .x = 0, .y = 0 }, 25 | col: u32 = 0xFFFFFFFF, 26 | }; 27 | 28 | pub const PassConfig = struct { 29 | pub const ColorAttachmentAction = extern struct { 30 | clear: bool = true, 31 | color: math.Color = math.Color.aya, 32 | }; 33 | 34 | clear_color: bool = true, 35 | color: math.Color = math.Color.aya, 36 | mrt_colors: [3]ColorAttachmentAction = [_]ColorAttachmentAction{.{}} ** 3, 37 | clear_stencil: bool = false, 38 | stencil: u8 = 0, 39 | clear_depth: bool = false, 40 | depth: f64 = 0, 41 | 42 | trans_mat: ?math.Mat32 = null, 43 | shader: ?*Shader = null, 44 | pass: ?OffscreenPass = null, 45 | 46 | pub fn asClearCommand(self: PassConfig) renderkit.ClearCommand { 47 | var cmd = renderkit.ClearCommand{}; 48 | cmd.colors[0].clear = self.clear_color; 49 | cmd.colors[0].color = self.color.asArray(); 50 | 51 | for (self.mrt_colors, 0..) |mrt_color, i| { 52 | cmd.colors[i + 1] = .{ 53 | .clear = mrt_color.clear, 54 | .color = mrt_color.color.asArray(), 55 | }; 56 | } 57 | 58 | cmd.clear_stencil = self.clear_stencil; 59 | cmd.stencil = self.stencil; 60 | cmd.clear_depth = self.clear_depth; 61 | cmd.depth = self.depth; 62 | return cmd; 63 | } 64 | }; 65 | 66 | pub var state = struct { 67 | shader: Shader = undefined, 68 | transform_mat: math.Mat32 = math.Mat32.identity, 69 | }{}; 70 | 71 | pub fn init() void { 72 | state.shader = Shader.initDefaultSpriteShader() catch unreachable; 73 | draw.init(); 74 | } 75 | 76 | pub fn deinit() void { 77 | draw.deinit(); 78 | state.shader.deinit(); 79 | } 80 | 81 | pub fn setShader(shader: ?*Shader) void { 82 | const new_shader = shader orelse &state.shader; 83 | 84 | draw.batcher.flush(); 85 | new_shader.bind(); 86 | new_shader.setTransformMatrix(&state.transform_mat); 87 | } 88 | 89 | pub fn setRenderState(renderstate: renderkit.RenderState) void { 90 | draw.batcher.flush(); 91 | renderkit.setRenderState(renderstate); 92 | } 93 | 94 | pub fn beginPass(config: PassConfig) void { 95 | var proj_mat: math.Mat32 = math.Mat32.init(); 96 | var clear_command = config.asClearCommand(); 97 | draw.batcher.begin(); 98 | 99 | if (config.pass) |pass| { 100 | renderkit.beginPass(pass.pass, clear_command); 101 | // inverted for OpenGL offscreen passes 102 | proj_mat = math.Mat32.initOrthoInverted(pass.color_texture.width, pass.color_texture.height); 103 | } else { 104 | const size = gamekit.window.drawableSize(); 105 | renderkit.beginDefaultPass(clear_command, size.w, size.h); 106 | proj_mat = math.Mat32.initOrtho(@intToFloat(f32, size.w), @intToFloat(f32, size.h)); 107 | } 108 | 109 | // if we were given a transform matrix multiply it here 110 | if (config.trans_mat) |trans_mat| { 111 | proj_mat = proj_mat.mul(trans_mat); 112 | } 113 | 114 | state.transform_mat = proj_mat; 115 | 116 | // if we were given a Shader use it else set the default Shader 117 | setShader(config.shader); 118 | } 119 | 120 | pub fn endPass() void { 121 | setShader(null); 122 | draw.batcher.end(); 123 | renderkit.endPass(); 124 | } 125 | 126 | /// if we havent yet blitted to the screen do so now 127 | pub fn commitFrame() void { 128 | renderkit.commitFrame(); 129 | } 130 | 131 | // import all the drawing methods 132 | pub const draw = @import("draw.zig").draw; 133 | -------------------------------------------------------------------------------- /examples/offscreen.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const math = gk.math; 4 | const gfx = gk.gfx; 5 | const draw = gfx.draw; 6 | 7 | var rng = std.rand.DefaultPrng.init(0x12345678); 8 | 9 | pub fn range(comptime T: type, at_least: T, less_than: T) T { 10 | if (@typeInfo(T) == .Int) { 11 | return rng.random().intRangeLessThanBiased(T, at_least, less_than); 12 | } else if (@typeInfo(T) == .Float) { 13 | return at_least + rng.random().float(T) * (less_than - at_least); 14 | } 15 | unreachable; 16 | } 17 | 18 | pub fn randomColor() u32 { 19 | const r = 200 + range(u8, 0, 55); 20 | const g = 200 + range(u8, 0, 55); 21 | const b = 200 + range(u8, 0, 55); 22 | return (r) | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, 255) << 24); 23 | } 24 | 25 | const Thing = struct { 26 | texture: gfx.Texture, 27 | pos: math.Vec2, 28 | vel: math.Vec2, 29 | col: u32, 30 | 31 | pub fn init(tex: gfx.Texture) Thing { 32 | return .{ 33 | .texture = tex, 34 | .pos = .{ 35 | .x = range(f32, 0, 750), 36 | .y = range(f32, 0, 50), 37 | }, 38 | .vel = .{ 39 | .x = range(f32, 0, 0), 40 | .y = range(f32, 0, 50), 41 | }, 42 | .col = randomColor(), 43 | }; 44 | } 45 | }; 46 | 47 | var texture: gfx.Texture = undefined; 48 | var checker_tex: gfx.Texture = undefined; 49 | var white_tex: gfx.Texture = undefined; 50 | var things: []Thing = undefined; 51 | var pass: gfx.OffscreenPass = undefined; 52 | var rt_pos: math.Vec2 = .{}; 53 | var camera: gk.utils.Camera = undefined; 54 | 55 | pub fn main() !void { 56 | rng.seed(@intCast(u64, std.time.milliTimestamp())); 57 | try gk.run(.{ 58 | .init = init, 59 | .update = update, 60 | .render = render, 61 | }); 62 | } 63 | 64 | fn init() !void { 65 | camera = gk.utils.Camera.init(); 66 | const size = gk.window.size(); 67 | camera.pos = .{ .x = @intToFloat(f32, size.w) * 0.5, .y = @intToFloat(f32, size.h) * 0.5 }; 68 | 69 | texture = gfx.Texture.initFromFile(std.heap.c_allocator, "examples/assets/textures/bee-8.png", .nearest) catch unreachable; 70 | checker_tex = gfx.Texture.initCheckerTexture(); 71 | white_tex = gfx.Texture.initSingleColor(0xFFFFFFFF); 72 | things = makeThings(12, texture); 73 | pass = gfx.OffscreenPass.init(300, 200); 74 | } 75 | 76 | fn update() !void { 77 | for (things) |*thing| { 78 | thing.pos.x += thing.vel.x * gk.time.dt(); 79 | thing.pos.y += thing.vel.y * gk.time.dt(); 80 | } 81 | 82 | rt_pos.x += 0.05; 83 | rt_pos.y += 0.05; 84 | 85 | if (gk.input.keyDown(.a)) { 86 | camera.pos.x += 1 * gk.time.dt(); 87 | } else if (gk.input.keyDown(.d)) { 88 | camera.pos.x -= 1 * gk.time.dt(); 89 | } 90 | if (gk.input.keyDown(.w)) { 91 | camera.pos.y -= 1 * gk.time.dt(); 92 | } else if (gk.input.keyDown(.s)) { 93 | camera.pos.y += 1 * gk.time.dt(); 94 | } 95 | } 96 | 97 | fn render() !void { 98 | // offscreen rendering 99 | gk.gfx.beginPass(.{ .color = math.Color.purple, .pass = pass }); 100 | draw.tex(texture, .{ .x = 10 + range(f32, -5, 5) }); 101 | draw.tex(texture, .{ .x = 50 + range(f32, -5, 5) }); 102 | draw.tex(texture, .{ .x = 90 + range(f32, -5, 5) }); 103 | draw.tex(texture, .{ .x = 130 + range(f32, -5, 5) }); 104 | gk.gfx.endPass(); 105 | 106 | // backbuffer rendering 107 | gk.gfx.beginPass(.{ 108 | .color = math.Color{ .value = randomColor() }, 109 | .trans_mat = camera.transMat(), 110 | }); 111 | 112 | // render the offscreen texture to the backbuffer 113 | draw.tex(pass.color_texture, rt_pos); 114 | 115 | for (things) |thing| { 116 | draw.tex(thing.texture, thing.pos); 117 | } 118 | 119 | draw.texScale(checker_tex, .{ .x = 350, .y = 50 }, 12); 120 | draw.point(.{ .x = 400, .y = 300 }, 20, math.Color.orange); 121 | draw.texScale(checker_tex, .{ .x = 0, .y = 0 }, 12.5); // bl 122 | draw.texScale(checker_tex, .{ .x = 800 - 50, .y = 0 }, 12.5); // br 123 | draw.texScale(checker_tex, .{ .x = 800 - 50, .y = 600 - 50 }, 12.5); // tr 124 | draw.texScale(checker_tex, .{ .x = 0, .y = 600 - 50 }, 12.5); // tl 125 | 126 | gk.gfx.endPass(); 127 | } 128 | 129 | fn makeThings(n: usize, tex: gfx.Texture) []Thing { 130 | var the_things = std.heap.c_allocator.alloc(Thing, n) catch unreachable; 131 | 132 | for (the_things) |*thing| { 133 | thing.* = Thing.init(tex); 134 | } 135 | 136 | return the_things; 137 | } 138 | -------------------------------------------------------------------------------- /gamekit/graphics/mesh.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const renderkit = @import("renderkit"); 3 | 4 | pub const Mesh = struct { 5 | bindings: renderkit.BufferBindings, 6 | element_count: c_int, 7 | 8 | pub fn init(comptime IndexT: type, indices: []IndexT, comptime VertT: type, verts: []VertT) Mesh { 9 | var ibuffer = renderkit.createBuffer(IndexT, .{ 10 | .type = .index, 11 | .content = indices, 12 | }); 13 | var vbuffer = renderkit.createBuffer(VertT, .{ 14 | .content = verts, 15 | }); 16 | 17 | return .{ 18 | .bindings = renderkit.BufferBindings.init(ibuffer, &[_]renderkit.Buffer{vbuffer}), 19 | .element_count = @intCast(c_int, indices.len), 20 | }; 21 | } 22 | 23 | pub fn deinit(self: Mesh) void { 24 | renderkit.destroyBuffer(self.bindings.index_buffer); 25 | renderkit.destroyBuffer(self.bindings.vert_buffers[0]); 26 | } 27 | 28 | pub fn bindImage(self: *Mesh, image: renderkit.Image, slot: c_uint) void { 29 | self.bindings.bindImage(image, slot); 30 | } 31 | 32 | pub fn draw(self: Mesh) void { 33 | renderkit.applyBindings(self.bindings); 34 | renderkit.draw(0, self.element_count, 1); 35 | } 36 | }; 37 | 38 | /// Contains a dynamic vert buffer and a slice of verts 39 | pub fn DynamicMesh(comptime IndexT: type, comptime VertT: type) type { 40 | return struct { 41 | const Self = @This(); 42 | 43 | bindings: renderkit.BufferBindings, 44 | verts: []VertT, 45 | element_count: c_int, 46 | allocator: std.mem.Allocator, 47 | 48 | pub fn init(allocator: std.mem.Allocator, vertex_count: usize, indices: []IndexT) !Self { 49 | var ibuffer = if (IndexT == void) @as(renderkit.Buffer, 0) else renderkit.createBuffer(IndexT, .{ 50 | .type = .index, 51 | .content = indices, 52 | }); 53 | var vertex_buffer = renderkit.createBuffer(VertT, .{ 54 | .usage = .stream, 55 | .size = @intCast(c_long, vertex_count * @sizeOf(VertT)), 56 | }); 57 | 58 | return Self{ 59 | .bindings = renderkit.BufferBindings.init(ibuffer, &[_]renderkit.Buffer{vertex_buffer}), 60 | .verts = try allocator.alloc(VertT, vertex_count), 61 | .element_count = @intCast(c_int, indices.len), 62 | .allocator = allocator, 63 | }; 64 | } 65 | 66 | pub fn deinit(self: *Self) void { 67 | if (IndexT != void) 68 | renderkit.destroyBuffer(self.bindings.index_buffer); 69 | renderkit.destroyBuffer(self.bindings.vert_buffers[0]); 70 | self.allocator.free(self.verts); 71 | } 72 | 73 | pub fn updateAllVerts(self: *Self) void { 74 | renderkit.updateBuffer(VertT, self.bindings.vert_buffers[0], self.verts); 75 | // updateBuffer gives us a fresh buffer so make sure we reset our append offset 76 | self.bindings.vertex_buffer_offsets[0] = 0; 77 | } 78 | 79 | /// uploads to the GPU the slice up to num_verts 80 | pub fn updateVertSlice(self: *Self, num_verts: usize) void { 81 | std.debug.assert(num_verts <= self.verts.len); 82 | const vert_slice = self.verts[0..num_verts]; 83 | renderkit.updateBuffer(VertT, self.bindings.vert_buffers[0], vert_slice); 84 | } 85 | 86 | /// uploads to the GPU the slice from start with num_verts. Records the offset in the BufferBindings allowing you 87 | /// to interleave appendVertSlice and draw calls. When calling draw after appendVertSlice 88 | /// the base_element is reset to the start of the newly updated data so you would pass in 0 for base_element. 89 | pub fn appendVertSlice(self: *Self, start_index: usize, num_verts: usize) void { 90 | std.debug.assert(start_index + num_verts <= self.verts.len); 91 | const vert_slice = self.verts[start_index .. start_index + num_verts]; 92 | self.bindings.vertex_buffer_offsets[0] = renderkit.appendBuffer(VertT, self.bindings.vert_buffers[0], vert_slice); 93 | } 94 | 95 | pub fn bindImage(self: *Self, image: renderkit.Image, slot: c_uint) void { 96 | self.bindings.bindImage(image, slot); 97 | } 98 | 99 | pub fn draw(self: Self, base_element: c_int, element_count: c_int) void { 100 | renderkit.applyBindings(self.bindings); 101 | renderkit.draw(base_element, element_count, 1); 102 | } 103 | 104 | pub fn drawAllVerts(self: Self) void { 105 | self.draw(0, @intCast(c_int, self.element_count)); 106 | } 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /gamekit/graphics/triangle_batcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const renderkit = @import("renderkit"); 3 | const gk = @import("../gamekit.zig"); 4 | const math = gk.math; 5 | 6 | const Vertex = gk.gfx.Vertex; 7 | const DynamicMesh = gk.gfx.DynamicMesh; 8 | 9 | pub const TriangleBatcher = struct { 10 | mesh: DynamicMesh(void, Vertex), 11 | draw_calls: std.ArrayList(i32), 12 | white_tex: gk.gfx.Texture = undefined, 13 | 14 | begin_called: bool = false, 15 | frame: u32 = 0, // tracks when a batch is started in a new frame so that state can be reset 16 | vert_index: usize = 0, // current index into the vertex array 17 | vert_count: i32 = 0, // total verts that we have not yet rendered 18 | buffer_offset: i32 = 0, // offset into the vertex buffer of the first non-rendered vert 19 | 20 | fn createDynamicMesh(allocator: std.mem.Allocator, max_tris: u16) !DynamicMesh(void, Vertex) { 21 | return try DynamicMesh(void, Vertex).init(allocator, max_tris * 3, &[_]void{}); 22 | } 23 | 24 | pub fn init(allocator: std.mem.Allocator, max_tris: u16) !TriangleBatcher { 25 | var batcher = TriangleBatcher{ 26 | .mesh = try createDynamicMesh(allocator, max_tris * 3), 27 | .draw_calls = try std.ArrayList(i32).initCapacity(allocator, 10), 28 | }; 29 | errdefer batcher.deinit(); 30 | 31 | var pixels = [_]u32{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF }; 32 | batcher.white_tex = gk.gfx.Texture.initWithData(u32, 2, 2, pixels[0..]); 33 | 34 | return batcher; 35 | } 36 | 37 | pub fn deinit(self: TriangleBatcher) void { 38 | self.mesh.deinit(); 39 | self.draw_calls.deinit(); 40 | self.white_tex.deinit(); 41 | } 42 | 43 | pub fn begin(self: *TriangleBatcher) void { 44 | std.debug.assert(!self.begin_called); 45 | 46 | // reset all state for new frame 47 | if (self.frame != gk.time.frames()) { 48 | self.frame = gk.time.frames(); 49 | self.vert_index = 0; 50 | self.buffer_offset = 0; 51 | } 52 | self.begin_called = true; 53 | } 54 | 55 | /// call at the end of the frame when all drawing is complete. Flushes the batch and resets local state. 56 | pub fn end(self: *TriangleBatcher) void { 57 | std.debug.assert(self.begin_called); 58 | self.flush(); 59 | self.begin_called = false; 60 | } 61 | 62 | pub fn flush(self: *TriangleBatcher) void { 63 | if (self.vert_index == 0) return; 64 | 65 | self.mesh.updateVertSlice(self.vert_index); 66 | self.mesh.bindImage(self.white_tex.img, 0); 67 | 68 | // draw 69 | const tris = self.vert_index / 3; 70 | self.mesh.draw(0, @intCast(c_int, tris * 3)); 71 | 72 | self.vert_index = 0; 73 | } 74 | 75 | /// ensures the vert buffer has enough space 76 | fn ensureCapacity(self: *TriangleBatcher) !void { 77 | // if we run out of buffer we have to flush the batch and possibly discard the whole buffer 78 | if (self.vert_index + 3 > self.mesh.verts.len) { 79 | self.flush(); 80 | 81 | self.vert_index = 0; 82 | self.buffer_offset = 0; 83 | 84 | // with GL we can just orphan the buffer 85 | self.mesh.updateAllVerts(); 86 | self.mesh.bindings.vertex_buffer_offsets[0] = 0; 87 | } 88 | 89 | // start a new draw call if we dont already have one going 90 | if (self.draw_calls.items.len == 0) { 91 | try self.draw_calls.append(0); 92 | } 93 | } 94 | 95 | pub fn drawTriangle(self: *TriangleBatcher, pt1: math.Vec2, pt2: math.Vec2, pt3: math.Vec2, color: math.Color) void { 96 | self.ensureCapacity() catch |err| { 97 | std.log.warn("TriangleBatcher.draw failed to append a draw call with error: {}\n", .{err}); 98 | return; 99 | }; 100 | 101 | // copy the triangle positions, uvs and color into vertex array transforming them with the matrix after we do it 102 | self.mesh.verts[self.vert_index].pos = pt1; 103 | self.mesh.verts[self.vert_index].col = color.value; 104 | self.mesh.verts[self.vert_index + 1].pos = pt2; 105 | self.mesh.verts[self.vert_index + 1].col = color.value; 106 | self.mesh.verts[self.vert_index + 2].pos = pt3; 107 | self.mesh.verts[self.vert_index + 2].col = color.value; 108 | 109 | const mat = math.Mat32.identity; 110 | mat.transformVertexSlice(self.mesh.verts[self.vert_index .. self.vert_index + 3]); 111 | 112 | self.draw_calls.items[self.draw_calls.items.len - 1] += 3; 113 | self.vert_count += 3; 114 | self.vert_index += 3; 115 | } 116 | }; 117 | 118 | test "test triangle batcher" { 119 | var batcher = try TriangleBatcher.init(null, 10); 120 | _ = try batcher.ensureCapacity(null); 121 | batcher.flush(false); 122 | batcher.deinit(); 123 | } 124 | -------------------------------------------------------------------------------- /gamekit/graphics/texture.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const stb_image = @import("stb"); 3 | const gk = @import("../gamekit.zig"); 4 | const imgui = @import("imgui"); 5 | const renderkit = @import("renderkit"); 6 | const fs = gk.utils.fs; 7 | 8 | pub const Texture = struct { 9 | img: renderkit.Image, 10 | width: f32 = 0, 11 | height: f32 = 0, 12 | 13 | pub fn init(width: i32, height: i32) Texture { 14 | return initDynamic(width, height, .nearest, .clamp); 15 | } 16 | 17 | pub fn initDynamic(width: i32, height: i32, filter: renderkit.TextureFilter, wrap: renderkit.TextureWrap) Texture { 18 | const img = renderkit.createImage(.{ 19 | .width = width, 20 | .height = height, 21 | .usage = .dynamic, 22 | .min_filter = filter, 23 | .mag_filter = filter, 24 | .wrap_u = wrap, 25 | .wrap_v = wrap, 26 | .content = null, 27 | }); 28 | return .{ 29 | .img = img, 30 | .width = @intToFloat(f32, width), 31 | .height = @intToFloat(f32, height), 32 | }; 33 | } 34 | 35 | pub fn initFromFile(allocator: std.mem.Allocator, file: []const u8, filter: renderkit.TextureFilter) !Texture { 36 | const image_contents = try fs.read(allocator, file); 37 | defer allocator.free(image_contents); 38 | 39 | var w: c_int = undefined; 40 | var h: c_int = undefined; 41 | var channels: c_int = undefined; 42 | const load_res = stb_image.stbi_load_from_memory(image_contents.ptr, @intCast(c_int, image_contents.len), &w, &h, &channels, 4); 43 | if (load_res == null) return error.ImageLoadFailed; 44 | defer stb_image.stbi_image_free(load_res); 45 | 46 | return initWithDataOptions(u8, w, h, load_res[0..@intCast(usize, w * h * channels)], filter, .clamp); 47 | } 48 | 49 | pub fn initWithData(comptime T: type, width: i32, height: i32, pixels: []T) Texture { 50 | return initWithDataOptions(T, width, height, pixels, .nearest, .clamp); 51 | } 52 | 53 | pub fn initWithDataOptions(comptime T: type, width: i32, height: i32, pixels: []T, filter: renderkit.TextureFilter, wrap: renderkit.TextureWrap) Texture { 54 | const img = renderkit.createImage(.{ 55 | .width = width, 56 | .height = height, 57 | .min_filter = filter, 58 | .mag_filter = filter, 59 | .wrap_u = wrap, 60 | .wrap_v = wrap, 61 | .content = std.mem.sliceAsBytes(pixels).ptr, 62 | }); 63 | return .{ 64 | .img = img, 65 | .width = @intToFloat(f32, width), 66 | .height = @intToFloat(f32, height), 67 | }; 68 | } 69 | 70 | pub fn initCheckerTexture() Texture { 71 | var pixels = [_]u32{ 72 | 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 0xFF000000, 73 | 0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 74 | 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 0xFF000000, 75 | 0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 76 | }; 77 | 78 | return initWithData(u32, 4, 4, &pixels); 79 | } 80 | 81 | pub fn initSingleColor(color: u32) Texture { 82 | var pixels: [16]u32 = undefined; 83 | std.mem.set(u32, &pixels, color); 84 | return initWithData(u32, 4, 4, pixels[0..]); 85 | } 86 | 87 | pub fn initOffscreen(width: i32, height: i32, filter: renderkit.TextureFilter, wrap: renderkit.TextureWrap) Texture { 88 | const img = renderkit.createImage(.{ 89 | .render_target = true, 90 | .width = width, 91 | .height = height, 92 | .min_filter = filter, 93 | .mag_filter = filter, 94 | .wrap_u = wrap, 95 | .wrap_v = wrap, 96 | }); 97 | return .{ 98 | .img = img, 99 | .width = @intToFloat(f32, width), 100 | .height = @intToFloat(f32, height), 101 | }; 102 | } 103 | 104 | pub fn initStencil(width: i32, height: i32, filter: renderkit.TextureFilter, wrap: renderkit.TextureWrap) Texture { 105 | const img = renderkit.createImage(.{ 106 | .render_target = true, 107 | .pixel_format = .depth_stencil, 108 | .width = width, 109 | .height = height, 110 | .min_filter = filter, 111 | .mag_filter = filter, 112 | .wrap_u = wrap, 113 | .wrap_v = wrap, 114 | }); 115 | return .{ 116 | .img = img, 117 | .width = @intToFloat(f32, width), 118 | .height = @intToFloat(f32, height), 119 | }; 120 | } 121 | 122 | pub fn deinit(self: *const Texture) void { 123 | renderkit.destroyImage(self.img); 124 | } 125 | 126 | pub fn setData(self: *Texture, comptime T: type, data: []T) void { 127 | renderkit.updateImage(T, self.img, data); 128 | } 129 | 130 | pub fn imTextureID(self: Texture) imgui.ImTextureID { 131 | return @intToPtr(*anyopaque, self.img); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /gamekit/input_types.zig: -------------------------------------------------------------------------------- 1 | pub const Keys = enum(c_int) { 2 | unknown = 0, 3 | a = 4, 4 | b = 5, 5 | c = 6, 6 | d = 7, 7 | e = 8, 8 | f = 9, 9 | g = 10, 10 | h = 11, 11 | i = 12, 12 | j = 13, 13 | k = 14, 14 | l = 15, 15 | m = 16, 16 | n = 17, 17 | o = 18, 18 | p = 19, 19 | q = 20, 20 | r = 21, 21 | s = 22, 22 | t = 23, 23 | u = 24, 24 | v = 25, 25 | w = 26, 26 | x = 27, 27 | y = 28, 28 | z = 29, 29 | num_1 = 30, 30 | num_2 = 31, 31 | num_3 = 32, 32 | num_4 = 33, 33 | num_5 = 34, 34 | num_6 = 35, 35 | num_7 = 36, 36 | num_8 = 37, 37 | num_9 = 38, 38 | num_0 = 39, 39 | key_return = 40, 40 | escape = 41, 41 | backspace = 42, 42 | tab = 43, 43 | space = 44, 44 | minus = 45, 45 | equals = 46, 46 | leftbracket = 47, 47 | rightbracket = 48, 48 | backslash = 49, 49 | nonushash = 50, 50 | semicolon = 51, 51 | apostrophe = 52, 52 | grave = 53, 53 | comma = 54, 54 | period = 55, 55 | slash = 56, 56 | capslock = 57, 57 | f1 = 58, 58 | f2 = 59, 59 | f3 = 60, 60 | f4 = 61, 61 | f5 = 62, 62 | f6 = 63, 63 | f7 = 64, 64 | f8 = 65, 65 | f9 = 66, 66 | f10 = 67, 67 | f11 = 68, 68 | f12 = 69, 69 | printscreen = 70, 70 | scrolllock = 71, 71 | pause = 72, 72 | insert = 73, 73 | home = 74, 74 | pageup = 75, 75 | delete = 76, 76 | end = 77, 77 | pagedown = 78, 78 | right = 79, 79 | left = 80, 80 | down = 81, 81 | up = 82, 82 | numlockclear = 83, 83 | kp_divide = 84, 84 | kp_multiply = 85, 85 | kp_minus = 86, 86 | kp_plus = 87, 87 | kp_enter = 88, 88 | kp_1 = 89, 89 | kp_2 = 90, 90 | kp_3 = 91, 91 | kp_4 = 92, 92 | kp_5 = 93, 93 | kp_6 = 94, 94 | kp_7 = 95, 95 | kp_8 = 96, 96 | kp_9 = 97, 97 | kp_0 = 98, 98 | kp_period = 99, 99 | nonusbackslash = 100, 100 | application = 101, 101 | power = 102, 102 | kp_equals = 103, 103 | f13 = 104, 104 | f14 = 105, 105 | f15 = 106, 106 | f16 = 107, 107 | f17 = 108, 108 | f18 = 109, 109 | f19 = 110, 110 | f20 = 111, 111 | f21 = 112, 112 | f22 = 113, 113 | f23 = 114, 114 | f24 = 115, 115 | execute = 116, 116 | help = 117, 117 | menu = 118, 118 | select = 119, 119 | stop = 120, 120 | again = 121, 121 | undo = 122, 122 | cut = 123, 123 | copy = 124, 124 | paste = 125, 125 | find = 126, 126 | mute = 127, 127 | volumeup = 128, 128 | volumedown = 129, 129 | kp_comma = 133, 130 | kp_equalsas400 = 134, 131 | international1 = 135, 132 | international2 = 136, 133 | international3 = 137, 134 | international4 = 138, 135 | international5 = 139, 136 | international6 = 140, 137 | international7 = 141, 138 | international8 = 142, 139 | international9 = 143, 140 | lang1 = 144, 141 | lang2 = 145, 142 | lang3 = 146, 143 | lang4 = 147, 144 | lang5 = 148, 145 | lang6 = 149, 146 | lang7 = 150, 147 | lang8 = 151, 148 | lang9 = 152, 149 | alterase = 153, 150 | sysreq = 154, 151 | cancel = 155, 152 | clear = 156, 153 | prior = 157, 154 | return2 = 158, 155 | separator = 159, 156 | out = 160, 157 | oper = 161, 158 | clearagain = 162, 159 | crsel = 163, 160 | exsel = 164, 161 | kp_00 = 176, 162 | kp_000 = 177, 163 | thousandsseparator = 178, 164 | decimalseparator = 179, 165 | currencyunit = 180, 166 | currencysubunit = 181, 167 | kp_leftparen = 182, 168 | kp_rightparen = 183, 169 | kp_leftbrace = 184, 170 | kp_rightbrace = 185, 171 | kp_tab = 186, 172 | kp_backspace = 187, 173 | kp_a = 188, 174 | kp_b = 189, 175 | kp_c = 190, 176 | kp_d = 191, 177 | kp_e = 192, 178 | kp_f = 193, 179 | kp_xor = 194, 180 | kp_power = 195, 181 | kp_percent = 196, 182 | kp_less = 197, 183 | kp_greater = 198, 184 | kp_ampersand = 199, 185 | kp_dblampersand = 200, 186 | kp_verticalbar = 201, 187 | kp_dblverticalbar = 202, 188 | kp_colon = 203, 189 | kp_hash = 204, 190 | kp_space = 205, 191 | kp_at = 206, 192 | kp_exclam = 207, 193 | kp_memstore = 208, 194 | kp_memrecall = 209, 195 | kp_memclear = 210, 196 | kp_memadd = 211, 197 | kp_memsubtract = 212, 198 | kp_memmultiply = 213, 199 | kp_memdivide = 214, 200 | kp_plusminus = 215, 201 | kp_clear = 216, 202 | kp_clearentry = 217, 203 | kp_binary = 218, 204 | kp_octal = 219, 205 | kp_decimal = 220, 206 | kp_hexadecimal = 221, 207 | lctrl = 224, 208 | lshift = 225, 209 | lalt = 226, 210 | lgui = 227, 211 | rctrl = 228, 212 | rshift = 229, 213 | ralt = 230, 214 | rgui = 231, 215 | mode = 257, 216 | audionext = 258, 217 | audioprev = 259, 218 | audiostop = 260, 219 | audioplay = 261, 220 | audiomute = 262, 221 | mediaselect = 263, 222 | www = 264, 223 | mail = 265, 224 | calculator = 266, 225 | computer = 267, 226 | ac_search = 268, 227 | ac_home = 269, 228 | ac_back = 270, 229 | ac_forward = 271, 230 | ac_stop = 272, 231 | ac_refresh = 273, 232 | ac_bookmarks = 274, 233 | brightnessdown = 275, 234 | brightnessup = 276, 235 | displayswitch = 277, 236 | kbdillumtoggle = 278, 237 | kbdillumdown = 279, 238 | kbdillumup = 280, 239 | eject = 281, 240 | sleep = 282, 241 | app1 = 283, 242 | app2 = 284, 243 | audiorewind = 285, 244 | audiofastforward = 286, 245 | num_keys = 287, 246 | }; 247 | -------------------------------------------------------------------------------- /examples/batcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sdl = @import("sdl"); 3 | const gk = @import("gamekit"); 4 | const gfx = gk.gfx; 5 | const math = gk.math; 6 | 7 | var rng = std.rand.DefaultPrng.init(0x12345678); 8 | 9 | const total_textures: usize = 8; 10 | const max_sprites_per_batch: usize = 5000; 11 | const total_objects = 10000; 12 | const draws_per_tex_swap = 250; 13 | const use_multi_texture_batcher = false; 14 | 15 | const MultiFragUniform = struct { 16 | samplers: [8]c_int = undefined, 17 | }; 18 | 19 | pub fn range(comptime T: type, at_least: T, less_than: T) T { 20 | if (@typeInfo(T) == .Int) { 21 | return rng.random().intRangeLessThanBiased(T, at_least, less_than); 22 | } else if (@typeInfo(T) == .Float) { 23 | return at_least + rng.random().float(T) * (less_than - at_least); 24 | } 25 | unreachable; 26 | } 27 | 28 | pub fn randomColor() u32 { 29 | const r = range(u8, 0, 255); 30 | const g = range(u8, 0, 255); 31 | const b = range(u8, 0, 255); 32 | return (r) | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, 255) << 24); 33 | } 34 | 35 | const Thing = struct { 36 | texture: gfx.Texture, 37 | pos: math.Vec2, 38 | vel: math.Vec2, 39 | col: u32, 40 | 41 | pub fn init(tex: gfx.Texture) Thing { 42 | return .{ 43 | .texture = tex, 44 | .pos = .{ 45 | .x = range(f32, 0, 750), 46 | .y = range(f32, 0, 50), 47 | }, 48 | .vel = .{ 49 | .x = range(f32, -150, 150), 50 | .y = range(f32, 0, 250), 51 | }, 52 | .col = randomColor(), 53 | }; 54 | } 55 | }; 56 | 57 | var shader: ?gfx.Shader = undefined; 58 | var batcher: if (use_multi_texture_batcher) gfx.MultiBatcher else gfx.Batcher = undefined; 59 | var textures: []gfx.Texture = undefined; 60 | var things: []Thing = undefined; 61 | 62 | pub fn main() !void { 63 | rng.seed(@intCast(u64, std.time.milliTimestamp())); 64 | try gk.run(.{ .init = init, .update = update, .render = render, .shutdown = shutdown, .window = .{ .disable_vsync = true } }); 65 | } 66 | 67 | fn init() !void { 68 | if (use_multi_texture_batcher and gk.renderkit.current_renderer != .opengl) @panic("only OpenGL is implemented for MultiBatcher shader"); 69 | 70 | shader = if (use_multi_texture_batcher) 71 | try gfx.Shader.initWithFrag(MultiFragUniform, .{ 72 | .vert = @embedFile("assets/shaders/multi_batcher.gl.vs"), 73 | .frag = @embedFile("assets/shaders/multi_batcher.gl.fs"), 74 | }) 75 | else 76 | null; 77 | 78 | if (use_multi_texture_batcher) { 79 | var uniform = MultiFragUniform{}; 80 | for (uniform.samplers, 0..) |*val, i| val.* = @intCast(c_int, i); 81 | shader.?.bind(); 82 | shader.?.setVertUniform(MultiFragUniform, &uniform); 83 | } 84 | 85 | batcher = if (use_multi_texture_batcher) gfx.MultiBatcher.init(std.heap.c_allocator, max_sprites_per_batch) else gfx.Batcher.init(std.heap.c_allocator, max_sprites_per_batch); 86 | 87 | loadTextures(); 88 | makeThings(total_objects); 89 | } 90 | 91 | fn shutdown() !void { 92 | std.heap.c_allocator.free(things); 93 | defer { 94 | for (textures) |tex| tex.deinit(); 95 | std.heap.c_allocator.free(textures); 96 | } 97 | } 98 | 99 | fn update() !void { 100 | const size = gk.window.size(); 101 | const win_w = @intToFloat(f32, size.w); 102 | const win_h = @intToFloat(f32, size.h); 103 | 104 | if (@mod(gk.time.frames(), 500) == 0) std.debug.print("fps: {d}\n", .{gk.time.fps()}); 105 | 106 | for (things) |*thing| { 107 | thing.pos.x += thing.vel.x * gk.time.rawDeltaTime(); 108 | thing.pos.y += thing.vel.y * gk.time.rawDeltaTime(); 109 | 110 | if (thing.pos.x > win_w) { 111 | thing.vel.x *= -1; 112 | thing.pos.x = win_w; 113 | } 114 | if (thing.pos.x < 0) { 115 | thing.vel.x *= -1; 116 | thing.pos.x = 0; 117 | } 118 | if (thing.pos.y > win_h) { 119 | thing.vel.y *= -1; 120 | thing.pos.y = win_h; 121 | } 122 | if (thing.pos.y < 0) { 123 | thing.vel.y *= -1; 124 | thing.pos.y = 0; 125 | } 126 | } 127 | } 128 | 129 | fn render() !void { 130 | gfx.beginPass(.{ .color = math.Color.beige }); 131 | if (shader) |*shdr| gfx.setShader(shdr); 132 | batcher.begin(); 133 | 134 | for (things) |thing| { 135 | batcher.drawTex(thing.pos, thing.col, thing.texture); 136 | } 137 | 138 | batcher.end(); 139 | gfx.endPass(); 140 | } 141 | 142 | fn loadTextures() void { 143 | textures = std.heap.c_allocator.alloc(gfx.Texture, total_textures) catch unreachable; 144 | 145 | var buf: [512]u8 = undefined; 146 | for (textures, 0..) |_, i| { 147 | var name = std.fmt.bufPrintZ(&buf, "examples/assets/textures/bee-{}.png", .{i + 1}) catch unreachable; 148 | textures[i] = gfx.Texture.initFromFile(std.heap.c_allocator, name, .nearest) catch unreachable; 149 | } 150 | } 151 | 152 | fn makeThings(n: usize) void { 153 | things = std.heap.c_allocator.alloc(Thing, n) catch unreachable; 154 | 155 | var count: usize = 0; 156 | var tid = range(usize, 0, total_textures); 157 | 158 | for (things) |*thing| { 159 | count += 1; 160 | if (@mod(count, draws_per_tex_swap) == 0) { 161 | count = 0; 162 | tid = range(usize, 0, total_textures); 163 | } 164 | 165 | if (use_multi_texture_batcher) tid = range(usize, 0, total_textures); 166 | 167 | thing.* = Thing.init(textures[tid]); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /gamekit/window.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sdl = @import("sdl"); 3 | const renderkit = @import("renderkit"); 4 | 5 | pub const WindowConfig = struct { 6 | title: [:0]const u8 = "Zig GameKit", // the window title as UTF-8 encoded string 7 | width: i32 = 800, // the preferred width of the window / canvas 8 | height: i32 = 600, // the preferred height of the window / canvas 9 | resizable: bool = true, // whether the window should be allowed to be resized 10 | fullscreen: bool = false, // whether the window should be created in fullscreen mode 11 | high_dpi: bool = false, // whether the backbuffer is full-resolution on HighDPI displays 12 | disable_vsync: bool = false, // whether vsync should be disabled 13 | }; 14 | 15 | pub const WindowMode = enum(u32) { 16 | windowed = 0, 17 | full_screen = 1, 18 | desktop = 4097, 19 | }; 20 | 21 | pub const Window = struct { 22 | sdl_window: *sdl.SDL_Window = undefined, 23 | gl_ctx: sdl.SDL_GLContext = undefined, 24 | id: u32 = 0, 25 | focused: bool = true, 26 | 27 | pub fn init(config: WindowConfig) !Window { 28 | var window = Window{}; 29 | _ = sdl.SDL_InitSubSystem(sdl.SDL_INIT_VIDEO); 30 | 31 | var flags: c_int = 0; 32 | if (config.resizable) flags |= sdl.SDL_WINDOW_RESIZABLE; 33 | if (config.high_dpi) flags |= sdl.SDL_WINDOW_ALLOW_HIGHDPI; 34 | if (config.fullscreen) flags |= sdl.SDL_WINDOW_FULLSCREEN_DESKTOP; 35 | 36 | window.createOpenGlWindow(config, flags); 37 | 38 | _ = sdl.SDL_GL_SetSwapInterval(if (config.disable_vsync) 0 else 1); 39 | 40 | window.id = sdl.SDL_GetWindowID(window.sdl_window); 41 | return window; 42 | } 43 | 44 | pub fn deinit(self: Window) void { 45 | sdl.SDL_DestroyWindow(self.sdl_window); 46 | sdl.SDL_GL_DeleteContext(self.gl_ctx); 47 | } 48 | 49 | fn createOpenGlWindow(self: *Window, config: WindowConfig, flags: c_int) void { 50 | _ = sdl.SDL_GL_SetAttribute(.SDL_GL_CONTEXT_FLAGS, sdl.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); 51 | _ = sdl.SDL_GL_SetAttribute(.SDL_GL_CONTEXT_PROFILE_MASK, sdl.SDL_GL_CONTEXT_PROFILE_CORE); 52 | _ = sdl.SDL_GL_SetAttribute(.SDL_GL_CONTEXT_MAJOR_VERSION, 3); 53 | _ = sdl.SDL_GL_SetAttribute(.SDL_GL_CONTEXT_MINOR_VERSION, 3); 54 | 55 | _ = sdl.SDL_GL_SetAttribute(.SDL_GL_DOUBLEBUFFER, 1); 56 | _ = sdl.SDL_GL_SetAttribute(.SDL_GL_DEPTH_SIZE, 24); 57 | _ = sdl.SDL_GL_SetAttribute(.SDL_GL_STENCIL_SIZE, 8); 58 | 59 | var window_flags = flags | @enumToInt(sdl.SDL_WindowFlags.SDL_WINDOW_OPENGL); 60 | self.sdl_window = sdl.SDL_CreateWindow(config.title, sdl.SDL_WINDOWPOS_UNDEFINED, sdl.SDL_WINDOWPOS_UNDEFINED, config.width, config.height, @bitCast(u32, window_flags)) orelse { 61 | sdl.SDL_Log("Unable to create window: %s", sdl.SDL_GetError()); 62 | @panic("no window"); 63 | }; 64 | 65 | self.gl_ctx = sdl.SDL_GL_CreateContext(self.sdl_window); 66 | } 67 | 68 | pub fn handleEvent(self: *Window, event: *sdl.SDL_WindowEvent) void { 69 | switch (event.event) { 70 | sdl.SDL_WINDOWEVENT_SIZE_CHANGED => { 71 | std.log.warn("resize: {}x{}\n", .{ event.data1, event.data2 }); 72 | // TODO: make a resized event 73 | }, 74 | sdl.SDL_WINDOWEVENT_FOCUS_GAINED => self.focused = true, 75 | sdl.SDL_WINDOWEVENT_FOCUS_LOST => self.focused = false, 76 | else => {}, 77 | } 78 | } 79 | 80 | /// returns the drawable width / the window width. Used to scale mouse coords when the OS gives them to us in points. 81 | pub fn scale(self: Window) f32 { 82 | var wx = self.width(); 83 | const draw_size = self.drawableSize(); 84 | return @intToFloat(f32, draw_size.w) / @intToFloat(f32, wx); 85 | } 86 | 87 | pub fn width(self: Window) i32 { 88 | return self.size().w; 89 | } 90 | 91 | pub fn height(self: Window) i32 { 92 | return self.size().h; 93 | } 94 | 95 | pub fn drawableSize(self: Window) struct { w: c_int, h: c_int } { 96 | var w: c_int = 0; 97 | var h: c_int = 0; 98 | sdl.SDL_GL_GetDrawableSize(self.sdl_window, &w, &h); 99 | 100 | return .{ .w = w, .h = h }; 101 | } 102 | 103 | pub fn size(self: Window) struct { w: c_int, h: c_int } { 104 | var w: c_int = 0; 105 | var h: c_int = 0; 106 | sdl.SDL_GetWindowSize(self.sdl_window, &w, &h); 107 | return .{ .w = w, .h = h }; 108 | } 109 | 110 | pub fn setSize(self: Window, w: i32, h: i32) void { 111 | sdl.SDL_SetWindowSize(self.sdl_window, w, h); 112 | } 113 | 114 | pub fn position(self: Window) struct { x: c_int, y: c_int } { 115 | var x: c_int = 0; 116 | var y: c_int = 0; 117 | sdl.SDL_GetWindowPosition(self.sdl_window, x, y); 118 | return .{ .x = x, .y = y }; 119 | } 120 | 121 | pub fn setPosition(self: Window, x: i32, y: i32) struct { w: c_int, h: c_int } { 122 | sdl.SDL_SetWindowPosition(self.sdl_window, x, y); 123 | } 124 | 125 | pub fn setMode(self: Window, mode: WindowMode) void { 126 | sdl.SDL_SetWindowFullscreen(self.sdl_window, mode); 127 | } 128 | 129 | pub fn focused(self: Window) bool { 130 | return self.focused; 131 | } 132 | 133 | pub fn resizable(self: Window) bool { 134 | return (sdl.SDL_GetWindowFlags(self.sdl_window) & @intCast(u32, @enumToInt(sdl.SDL_WindowFlags.SDL_WINDOW_RESIZABLE))) != 0; 135 | } 136 | 137 | pub fn setResizable(self: Window, new_resizable: bool) void { 138 | sdl.SDL_SetWindowResizable(self.sdl_window, new_resizable); 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /gamekit/graphics/shader.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("../gamekit.zig"); 3 | const math = gk.math; 4 | const rk = gk.renderkit; 5 | const fs = gk.utils.fs; 6 | 7 | /// default params for the sprite shader. Translates the Mat32 into 2 arrays of f32 for the shader uniform slot. 8 | pub const VertexParams = extern struct { 9 | pub const metadata = .{ 10 | .uniforms = .{ .VertexParams = .{ .type = .float4, .array_count = 2 } }, 11 | .images = .{"main_tex"}, 12 | }; 13 | 14 | transform_matrix: [8]f32 = [_]f32{0} ** 8, 15 | 16 | pub fn init(mat: *math.Mat32) VertexParams { 17 | var params = VertexParams{}; 18 | std.mem.copy(f32, ¶ms.transform_matrix, &mat.data); 19 | return params; 20 | } 21 | }; 22 | 23 | fn defaultVertexShader() [:0]const u8 { 24 | return @embedFile("../assets/sprite_vs.glsl"); 25 | } 26 | 27 | fn defaultFragmentShader() [:0]const u8 { 28 | return @embedFile("../assets/sprite_fs.glsl"); 29 | } 30 | 31 | pub const Shader = struct { 32 | shader: rk.ShaderProgram, 33 | onPostBind: ?*const fn (*Shader) void, 34 | onSetTransformMatrix: ?*const fn (*math.Mat32) void, 35 | 36 | const Empty = struct {}; 37 | 38 | pub const ShaderOptions = struct { 39 | /// if vert and frag are file paths an Allocator is required. If they are the shader code then no Allocator should be provided 40 | allocator: ?std.mem.Allocator = null, 41 | 42 | /// optional vertex shader file path (without extension) or shader code. If null, the default sprite shader vertex shader is used 43 | vert: ?[:0]const u8 = null, 44 | 45 | /// required frag shader file path (without extension) or shader code. 46 | frag: [:0]const u8, 47 | 48 | /// optional function that will be called immediately after bind is called allowing you to auto-update uniforms 49 | onPostBind: ?*const fn (*Shader) void = null, 50 | 51 | /// optional function that lets you override the behavior when the transform matrix is set. This is used when there is a 52 | /// custom vertex shader and isnt necessary if the standard sprite vertex shader is used. Note that the shader is already 53 | /// bound when this is called if `gfx.setShader` is used so send your uniform immediately! 54 | onSetTransformMatrix: ?*const fn (*math.Mat32) void = null, 55 | }; 56 | 57 | pub fn initDefaultSpriteShader() !Shader { 58 | return initWithVertFrag(VertexParams, Empty, .{ .vert = defaultVertexShader(), .frag = defaultFragmentShader() }); 59 | } 60 | 61 | pub fn initWithFrag(comptime FragUniformT: type, options: ShaderOptions) !Shader { 62 | return initWithVertFrag(VertexParams, FragUniformT, options); 63 | } 64 | 65 | pub fn initWithVertFrag(comptime VertUniformT: type, comptime FragUniformT: type, options: ShaderOptions) !Shader { 66 | const vert = blk: { 67 | // if we were not provided a vert shader we substitute in the sprite shader 68 | if (options.vert) |vert| { 69 | // if we were provided an allocator that means this is a file 70 | if (options.allocator) |allocator| { 71 | const vert_path = try std.mem.concat(allocator, u8, &[_][]const u8{ vert, ".glsl\x00" }); 72 | defer allocator.free(vert_path); 73 | break :blk try fs.readZ(allocator, vert_path); 74 | } 75 | break :blk vert; 76 | } else { 77 | break :blk defaultVertexShader(); 78 | } 79 | }; 80 | const frag = blk: { 81 | if (options.allocator) |allocator| { 82 | const frag_path = try std.mem.concat(allocator, u8, &[_][]const u8{ options.frag, ".glsl" }); 83 | defer allocator.free(frag_path); 84 | break :blk try fs.readZ(allocator, frag_path); 85 | } 86 | break :blk options.frag; 87 | }; 88 | 89 | return Shader{ 90 | .shader = rk.createShaderProgram(VertUniformT, FragUniformT, .{ .vs = vert, .fs = frag }), 91 | .onPostBind = options.onPostBind, 92 | .onSetTransformMatrix = options.onSetTransformMatrix, 93 | }; 94 | } 95 | 96 | pub fn deinit(self: Shader) void { 97 | rk.destroyShaderProgram(self.shader); 98 | } 99 | 100 | pub fn bind(self: *Shader) void { 101 | rk.useShaderProgram(self.shader); 102 | if (self.onPostBind) |onPostBind| onPostBind(self); 103 | } 104 | 105 | pub fn setTransformMatrix(self: Shader, matrix: *math.Mat32) void { 106 | if (self.onSetTransformMatrix) |setMatrix| { 107 | setMatrix(matrix); 108 | } else { 109 | var params = VertexParams.init(matrix); 110 | self.setVertUniform(VertexParams, ¶ms); 111 | } 112 | } 113 | 114 | pub fn setVertUniform(self: Shader, comptime VertUniformT: type, value: *VertUniformT) void { 115 | rk.setShaderProgramUniformBlock(VertUniformT, self.shader, .vs, value); 116 | } 117 | 118 | pub fn setFragUniform(self: Shader, comptime FragUniformT: type, value: *FragUniformT) void { 119 | rk.setShaderProgramUniformBlock(FragUniformT, self.shader, .fs, value); 120 | } 121 | }; 122 | 123 | /// convenience object that binds a fragment uniform with a Shader. You can optionally wire up the onPostBind method 124 | /// to the Shader.onPostBind so that the FragUniformT object is automatically updated when the Shader is bound. 125 | pub fn ShaderState(comptime FragUniformT: type) type { 126 | return struct { 127 | shader: Shader, 128 | frag_uniform: FragUniformT = .{}, 129 | 130 | pub fn init(options: Shader.ShaderOptions) @This() { 131 | return .{ 132 | .shader = Shader.initWithFrag(FragUniformT, options) catch unreachable, 133 | }; 134 | } 135 | 136 | pub fn deinit(self: @This()) void { 137 | self.shader.deinit(); 138 | } 139 | 140 | pub fn onPostBind(shader: *Shader) void { 141 | const self = @fieldParentPtr(@This(), "shader", shader); 142 | shader.setFragUniform(FragUniformT, &self.frag_uniform); 143 | } 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /gamekit/graphics/batcher.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const renderkit = @import("renderkit"); 3 | const gk = @import("../gamekit.zig"); 4 | const math = gk.math; 5 | 6 | const IndexBuffer = renderkit.IndexBuffer; 7 | const VertexBuffer = renderkit.VertexBuffer; 8 | const Vertex = gk.gfx.Vertex; 9 | const Texture = gk.gfx.Texture; 10 | 11 | pub const Batcher = struct { 12 | mesh: gk.gfx.DynamicMesh(u16, Vertex), 13 | draw_calls: std.ArrayList(DrawCall), 14 | 15 | begin_called: bool = false, 16 | frame: u32 = 0, // tracks when a batch is started in a new frame so that state can be reset 17 | vert_index: usize = 0, // current index into the vertex array 18 | quad_count: usize = 0, // total quads that we have not yet rendered 19 | buffer_offset: i32 = 0, // offset into the vertex buffer of the first non-rendered vert 20 | 21 | const DrawCall = struct { 22 | image: renderkit.Image, 23 | quad_count: i32, 24 | }; 25 | 26 | fn createDynamicMesh(allocator: std.mem.Allocator, max_sprites: u16) !gk.gfx.DynamicMesh(u16, Vertex) { 27 | var indices = allocator.alloc(u16, max_sprites * 6) catch unreachable; 28 | defer allocator.free(indices); 29 | var i: usize = 0; 30 | while (i < max_sprites) : (i += 1) { 31 | indices[i * 3 * 2 + 0] = @intCast(u16, i) * 4 + 0; 32 | indices[i * 3 * 2 + 1] = @intCast(u16, i) * 4 + 1; 33 | indices[i * 3 * 2 + 2] = @intCast(u16, i) * 4 + 2; 34 | indices[i * 3 * 2 + 3] = @intCast(u16, i) * 4 + 0; 35 | indices[i * 3 * 2 + 4] = @intCast(u16, i) * 4 + 2; 36 | indices[i * 3 * 2 + 5] = @intCast(u16, i) * 4 + 3; 37 | } 38 | 39 | return try gk.gfx.DynamicMesh(u16, Vertex).init(allocator, max_sprites * 4, indices); 40 | } 41 | 42 | pub fn init(allocator: std.mem.Allocator, max_sprites: u16) Batcher { 43 | if (max_sprites * 6 > std.math.maxInt(u16)) @panic("max_sprites exceeds u16 index buffer size"); 44 | 45 | return .{ 46 | .mesh = createDynamicMesh(allocator, max_sprites) catch unreachable, 47 | .draw_calls = std.ArrayList(DrawCall).initCapacity(allocator, 10) catch unreachable, 48 | }; 49 | } 50 | 51 | pub fn deinit(self: *Batcher) void { 52 | self.mesh.deinit(); 53 | self.draw_calls.deinit(); 54 | } 55 | 56 | pub fn begin(self: *Batcher) void { 57 | std.debug.assert(!self.begin_called); 58 | 59 | // reset all state for new frame 60 | if (self.frame != gk.time.frames()) { 61 | self.frame = gk.time.frames(); 62 | self.vert_index = 0; 63 | self.buffer_offset = 0; 64 | } 65 | self.begin_called = true; 66 | } 67 | 68 | pub fn end(self: *Batcher) void { 69 | std.debug.assert(self.begin_called); 70 | self.flush(); 71 | self.begin_called = false; 72 | } 73 | 74 | /// should be called when any graphics state change will occur such as setting a new shader or RenderState 75 | pub fn flush(self: *Batcher) void { 76 | if (self.quad_count == 0) return; 77 | 78 | self.mesh.appendVertSlice(@intCast(usize, self.buffer_offset), @intCast(usize, self.quad_count * 4)); 79 | 80 | // run through all our accumulated draw calls 81 | var base_element: i32 = 0; 82 | for (self.draw_calls.items) |*draw_call| { 83 | self.mesh.bindImage(draw_call.image, 0); 84 | self.mesh.draw(base_element, draw_call.quad_count * 6); 85 | 86 | self.buffer_offset += draw_call.quad_count * 4; 87 | draw_call.image = renderkit.invalid_resource_id; 88 | base_element += draw_call.quad_count * 6; 89 | } 90 | 91 | self.quad_count = 0; 92 | self.draw_calls.items.len = 0; 93 | } 94 | 95 | /// ensures the vert buffer has enough space and manages the draw call command buffer when textures change 96 | fn ensureCapacity(self: *Batcher, texture: Texture) !void { 97 | // if we run out of buffer we have to flush the batch and possibly discard and resize the whole buffer 98 | if (self.vert_index + 4 > self.mesh.verts.len - 1) { 99 | self.flush(); 100 | 101 | self.vert_index = 0; 102 | self.buffer_offset = 0; 103 | 104 | // with GL we can just orphan the buffer 105 | self.mesh.updateAllVerts(); 106 | self.mesh.bindings.vertex_buffer_offsets[0] = 0; 107 | } 108 | 109 | // start a new draw call if we dont already have one going or whenever the texture changes 110 | if (self.draw_calls.items.len == 0 or self.draw_calls.items[self.draw_calls.items.len - 1].image != texture.img) { 111 | try self.draw_calls.append(.{ .image = texture.img, .quad_count = 0 }); 112 | } 113 | } 114 | 115 | pub fn drawTex(self: *Batcher, pos: math.Vec2, col: u32, texture: Texture) void { 116 | self.ensureCapacity(texture) catch |err| { 117 | std.log.warn("Batcher.draw failed to append a draw call with error: {}\n", .{err}); 118 | return; 119 | }; 120 | 121 | var verts = self.mesh.verts[self.vert_index .. self.vert_index + 4]; 122 | verts[0].pos = pos; // tl 123 | verts[0].uv = .{ .x = 0, .y = 0 }; 124 | verts[0].col = col; 125 | 126 | verts[1].pos = .{ .x = pos.x + texture.width, .y = pos.y }; // tr 127 | verts[1].uv = .{ .x = 1, .y = 0 }; 128 | verts[1].col = col; 129 | 130 | verts[2].pos = .{ .x = pos.x + texture.width, .y = pos.y + texture.height }; // br 131 | verts[2].uv = .{ .x = 1, .y = 1 }; 132 | verts[2].col = col; 133 | 134 | verts[3].pos = .{ .x = pos.x, .y = pos.y + texture.height }; // bl 135 | verts[3].uv = .{ .x = 0, .y = 1 }; 136 | verts[3].col = col; 137 | 138 | self.draw_calls.items[self.draw_calls.items.len - 1].quad_count += 1; 139 | self.quad_count += 1; 140 | self.vert_index += 4; 141 | } 142 | 143 | pub fn draw(self: *Batcher, texture: Texture, quad: math.Quad, mat: math.Mat32, color: math.Color) void { 144 | self.ensureCapacity(texture) catch |err| { 145 | std.log.warn("Batcher.draw failed to append a draw call with error: {}\n", .{err}); 146 | return; 147 | }; 148 | 149 | // copy the quad positions, uvs and color into vertex array transforming them with the matrix as we do it 150 | mat.transformQuad(self.mesh.verts[self.vert_index .. self.vert_index + 4], quad, color); 151 | 152 | self.draw_calls.items[self.draw_calls.items.len - 1].quad_count += 1; 153 | self.quad_count += 1; 154 | self.vert_index += 4; 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /gamekit/graphics/fontbook.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("../gamekit.zig"); 3 | const rk = @import("renderkit"); 4 | const fons = @import("fontstash"); 5 | 6 | pub const FontBook = struct { 7 | stash: *fons.Context, 8 | texture: ?gk.gfx.Texture, 9 | tex_filter: rk.TextureFilter, 10 | width: i32 = 0, 11 | height: i32 = 0, 12 | tex_dirty: bool = false, 13 | last_update: u32 = 0, 14 | allocator: std.mem.Allocator, 15 | 16 | pub const Align = fons.Align; 17 | 18 | pub fn init(allocator: std.mem.Allocator, width: i32, height: i32, filter: rk.TextureFilter) !*FontBook { 19 | var book = try allocator.create(FontBook); 20 | errdefer allocator.destroy(book); 21 | 22 | var params = fons.Params{ 23 | .width = width, 24 | .height = height, 25 | .flags = fons.Flags.top_left, 26 | .user_ptr = book, 27 | .renderCreate = renderCreate, 28 | .renderResize = renderResize, 29 | .renderUpdate = renderUpdate, 30 | }; 31 | 32 | book.texture = null; 33 | book.tex_filter = filter; 34 | book.width = width; 35 | book.height = height; 36 | book.allocator = allocator; 37 | book.stash = try fons.Context.init(¶ms); 38 | 39 | return book; 40 | } 41 | 42 | pub fn deinit(self: *FontBook) void { 43 | self.stash.deinit(); 44 | if (self.texture != null) self.texture.?.deinit(); 45 | self.allocator.destroy(self); 46 | } 47 | 48 | // add fonts 49 | pub fn addFont(self: *FontBook, file: [:0]const u8) c_int { 50 | const data = gk.utils.fs.read(self.allocator, file) catch unreachable; 51 | 52 | // we can let FONS free the data since we are using the c_allocator here 53 | return fons.fonsAddFontMem(self.stash, file, @ptrCast([*c]const u8, data), @intCast(i32, data.len), 1); 54 | } 55 | 56 | pub fn addFontMem(self: *FontBook, name: [:0]const u8, data: []const u8, free_data: bool) c_int { 57 | const free: c_int = if (free_data) 1 else 0; 58 | return fons.fonsAddFontMem(self.stash, name, @ptrCast([*c]const u8, data), @intCast(i32, data.len), free); 59 | } 60 | 61 | // state setting 62 | pub fn setAlign(self: *FontBook, alignment: Align) void { 63 | fons.fonsSetAlign(self.stash, alignment); 64 | } 65 | 66 | pub fn setSize(self: FontBook, size: f32) void { 67 | fons.fonsSetSize(self.stash, size); 68 | } 69 | 70 | pub fn setColor(self: FontBook, color: gk.math.Color) void { 71 | fons.fonsSetColor(self.stash, color.value); 72 | } 73 | 74 | pub fn setSpacing(self: FontBook, spacing: f32) void { 75 | fons.fonsSetSpacing(self.stash, spacing); 76 | } 77 | 78 | pub fn setBlur(self: FontBook, blur: f32) void { 79 | fons.fonsSetBlur(self.stash, blur); 80 | } 81 | 82 | // state handling 83 | pub fn pushState(self: *FontBook) void { 84 | fons.fonsPushState(self.stash); 85 | } 86 | 87 | pub fn popState(self: *FontBook) void { 88 | fons.fonsPopState(self.stash); 89 | } 90 | 91 | pub fn clearState(self: *FontBook) void { 92 | fons.fonsClearState(self.stash); 93 | } 94 | 95 | // measure text 96 | pub fn getTextBounds(self: *FontBook, x: f32, y: f32, str: []const u8) struct { width: f32, bounds: f32 } { 97 | var bounds: f32 = undefined; 98 | const width = fons.fonsTextBounds(self.stash, x, y, str.ptr, null, &bounds); 99 | return .{ .width = width, .bounds = bounds }; 100 | } 101 | 102 | pub fn getLineBounds(self: *FontBook, y: f32) struct { min_y: f32, max_y: f32 } { 103 | var min_y: f32 = undefined; 104 | var max_y: f32 = undefined; 105 | fons.fonsLineBounds(self.stash, y, &min_y, &max_y); 106 | return .{ .min_y = min_y, .max_y = max_y }; 107 | } 108 | 109 | pub fn getVertMetrics(self: *FontBook) struct { ascender: f32, descender: f32, line_h: f32 } { 110 | var ascender: f32 = undefined; 111 | var descender: f32 = undefined; 112 | var line_h: f32 = undefined; 113 | fons.fonsVertMetrics(self.stash, &ascender, &descender, &line_h); 114 | return .{ .ascender = ascender, .descender = descender, .line_h = line_h }; 115 | } 116 | 117 | // text iter 118 | pub fn getTextIterator(self: *FontBook, str: []const u8) fons.TextIter { 119 | var iter = std.mem.zeroes(fons.TextIter); 120 | const res = fons.fonsTextIterInit(self.stash, &iter, 0, 0, str.ptr, @intCast(c_int, str.len)); 121 | if (res == 0) std.log.warn("getTextIterator failed! Make sure you have added a font.\n", .{}); 122 | return iter; 123 | } 124 | 125 | pub fn textIterNext(self: *FontBook, iter: *fons.TextIter, quad: *fons.Quad) bool { 126 | return fons.fonsTextIterNext(self.stash, iter, quad) == 1; 127 | } 128 | 129 | pub fn getQuad(self: FontBook) fons.Quad { 130 | _ = self; 131 | return std.mem.zeroes(fons.Quad); 132 | } 133 | 134 | fn renderCreate(ctx: ?*anyopaque, width: c_int, height: c_int) callconv(.C) c_int { 135 | var self = @ptrCast(*FontBook, @alignCast(@alignOf(FontBook), ctx)); 136 | 137 | if (self.texture != null and (self.texture.?.width != @intToFloat(f32, width) or self.texture.?.height != @intToFloat(f32, height))) { 138 | self.texture.?.deinit(); 139 | self.texture = null; 140 | } 141 | 142 | if (self.texture == null) 143 | self.texture = gk.gfx.Texture.initDynamic(width, height, self.tex_filter, .clamp); 144 | 145 | self.width = width; 146 | self.height = height; 147 | 148 | return 1; 149 | } 150 | 151 | fn renderResize(ctx: ?*anyopaque, width: c_int, height: c_int) callconv(.C) c_int { 152 | return renderCreate(ctx, width, height); 153 | } 154 | 155 | fn renderUpdate(ctx: ?*anyopaque, rect: [*c]c_int, data: [*c]const u8) callconv(.C) c_int { 156 | // TODO: only update the rect that changed 157 | _ = rect; 158 | 159 | var self = @ptrCast(*FontBook, @alignCast(@alignOf(FontBook), ctx)); 160 | if (!self.tex_dirty or self.last_update == gk.time.frames()) { 161 | self.tex_dirty = true; 162 | return 0; 163 | } 164 | 165 | const tex_area = @intCast(usize, self.width * self.height); 166 | var pixels = self.allocator.alloc(u8, tex_area * 4) catch |err| { 167 | std.log.warn("failed to allocate texture data: {}\n", .{err}); 168 | return 0; 169 | }; 170 | defer self.allocator.free(pixels); 171 | const source = data[0..tex_area]; 172 | 173 | for (source, 0..) |alpha, i| { 174 | pixels[i * 4 + 0] = 255; 175 | pixels[i * 4 + 1] = 255; 176 | pixels[i * 4 + 2] = 255; 177 | pixels[i * 4 + 3] = alpha; 178 | } 179 | 180 | self.texture.?.setData(u8, pixels); 181 | self.tex_dirty = false; 182 | self.last_update = gk.time.frames(); 183 | return 1; 184 | } 185 | }; 186 | -------------------------------------------------------------------------------- /gamekit/imgui/renderer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const imgui = @import("imgui"); 3 | const gk = @import("../gamekit.zig"); 4 | const gfx = gk.gfx; 5 | const rk = @import("renderkit"); 6 | 7 | pub const Renderer = struct { 8 | font_texture: gfx.Texture, 9 | vert_buffer_size: c_long, 10 | index_buffer_size: c_long, 11 | bindings: rk.BufferBindings, 12 | 13 | const font_awesome_range: [3]imgui.ImWchar = [_]imgui.ImWchar{ imgui.icons.icon_range_min, imgui.icons.icon_range_max, 0 }; 14 | 15 | pub fn init(docking: bool, viewports: bool, icon_font: bool) Renderer { 16 | const max_verts = 16384; 17 | const index_buffer_size = @intCast(c_long, max_verts * 3 * @sizeOf(u16)); 18 | var ibuffer = rk.createBuffer(u16, .{ 19 | .type = .index, 20 | .usage = .stream, 21 | .size = index_buffer_size, 22 | }); 23 | const vert_buffer_size = @intCast(c_long, max_verts * @sizeOf(gfx.Vertex)); 24 | var vertex_buffer = rk.createBuffer(gfx.Vertex, .{ 25 | .usage = .stream, 26 | .size = vert_buffer_size, 27 | }); 28 | 29 | _ = imgui.igCreateContext(null); 30 | var io = imgui.igGetIO(); 31 | if (docking) io.ConfigFlags |= imgui.ImGuiConfigFlags_DockingEnable; 32 | if (viewports) io.ConfigFlags |= imgui.ImGuiConfigFlags_ViewportsEnable; 33 | io.ConfigDockingWithShift = true; 34 | 35 | imgui.igStyleColorsDark(imgui.igGetStyle()); 36 | imgui.igGetStyle().FrameRounding = 0; 37 | imgui.igGetStyle().WindowRounding = 0; 38 | 39 | _ = imgui.ImFontAtlas_AddFontDefault(io.Fonts, null); 40 | 41 | // add FontAwesome optionally 42 | if (icon_font) { 43 | var icons_config = imgui.ImFontConfig_ImFontConfig(); 44 | icons_config[0].MergeMode = true; 45 | icons_config[0].PixelSnapH = true; 46 | icons_config[0].FontDataOwnedByAtlas = false; 47 | 48 | var data = @embedFile("assets/" ++ imgui.icons.font_icon_filename_fas); 49 | _ = imgui.ImFontAtlas_AddFontFromMemoryTTF(io.Fonts, data, data.len, 13, icons_config, &font_awesome_range[0]); 50 | } 51 | 52 | var w: i32 = undefined; 53 | var h: i32 = undefined; 54 | var bytes_per_pixel: i32 = undefined; 55 | var pixels: [*c]u8 = undefined; 56 | imgui.ImFontAtlas_GetTexDataAsRGBA32(io.Fonts, &pixels, &w, &h, &bytes_per_pixel); 57 | 58 | const font_tex = gfx.Texture.initWithData(u8, w, h, pixels[0..@intCast(usize, w * h * bytes_per_pixel)]); 59 | imgui.ImFontAtlas_SetTexID(io.Fonts, font_tex.imTextureID()); 60 | 61 | return .{ 62 | .font_texture = font_tex, 63 | .vert_buffer_size = vert_buffer_size, 64 | .index_buffer_size = index_buffer_size, 65 | .bindings = rk.BufferBindings.init(ibuffer, &[_]rk.Buffer{vertex_buffer}), 66 | }; 67 | } 68 | 69 | pub fn deinit(self: Renderer) void { 70 | self.font_texture.deinit(); 71 | rk.destroyBuffer(self.bindings.index_buffer); 72 | rk.destroyBuffer(self.bindings.vert_buffers[0]); 73 | } 74 | 75 | pub fn render(self: *Renderer) void { 76 | imgui.igEndFrame(); 77 | imgui.igRender(); 78 | 79 | const io = imgui.igGetIO(); 80 | _ = io; 81 | const draw_data = imgui.igGetDrawData(); 82 | if (draw_data.TotalVtxCount == 0) return; 83 | 84 | self.updateBuffers(draw_data); 85 | self.bindings.index_buffer_offset = 0; 86 | 87 | var tex_id = imgui.igGetIO().Fonts.TexID; 88 | self.bindings.images[0] = @intCast(rk.Image, @ptrToInt(tex_id)); 89 | 90 | var fb_scale = draw_data.FramebufferScale; 91 | imgui.ogImDrawData_ScaleClipRects(draw_data, fb_scale); 92 | const width = @floatToInt(i32, draw_data.DisplaySize.x * fb_scale.x); 93 | const height = @floatToInt(i32, draw_data.DisplaySize.y * fb_scale.y); 94 | rk.viewport(0, 0, width, height); 95 | 96 | gfx.setShader(null); 97 | 98 | rk.setRenderState(.{ .scissor = true }); 99 | 100 | var vb_offset: u32 = 0; 101 | for (draw_data.CmdLists[0..@intCast(usize, draw_data.CmdListsCount)]) |list| { 102 | // append vertices and indices to buffers 103 | const indices = @ptrCast([*]u16, list.IdxBuffer.Data)[0..@intCast(usize, list.IdxBuffer.Size)]; 104 | self.bindings.index_buffer_offset = rk.appendBuffer(u16, self.bindings.index_buffer, indices); 105 | 106 | const verts = @ptrCast([*]gfx.Vertex, list.VtxBuffer.Data)[0..@intCast(usize, list.VtxBuffer.Size)]; 107 | vb_offset = rk.appendBuffer(gfx.Vertex, self.bindings.vert_buffers[0], verts); 108 | self.bindings.vertex_buffer_offsets[0] = vb_offset; 109 | 110 | rk.applyBindings(self.bindings); 111 | 112 | var base_element: c_int = 0; 113 | var vtx_offset: u32 = 0; 114 | for (list.CmdBuffer.Data[0..@intCast(usize, list.CmdBuffer.Size)]) |cmd| { 115 | if (cmd.UserCallback) |cb| { 116 | cb(list, &cmd); 117 | } else { 118 | // DisplayPos is 0,0 unless viewports is enabled 119 | const clip_x = @floatToInt(i32, (cmd.ClipRect.x - draw_data.DisplayPos.x) * fb_scale.x); 120 | const clip_y = @floatToInt(i32, (cmd.ClipRect.y - draw_data.DisplayPos.y) * fb_scale.y); 121 | const clip_w = @floatToInt(i32, (cmd.ClipRect.z - draw_data.DisplayPos.x) * fb_scale.x); 122 | const clip_h = @floatToInt(i32, (cmd.ClipRect.w - draw_data.DisplayPos.y) * fb_scale.y); 123 | 124 | rk.scissor(clip_x, clip_y, clip_w - clip_x, clip_h - clip_y); 125 | 126 | if (tex_id != cmd.TextureId or vtx_offset != cmd.VtxOffset) { 127 | tex_id = cmd.TextureId; 128 | self.bindings.images[0] = @intCast(rk.Image, @ptrToInt(tex_id)); 129 | 130 | vtx_offset = cmd.VtxOffset; 131 | self.bindings.vertex_buffer_offsets[0] = vb_offset + vtx_offset * @sizeOf(imgui.ImDrawVert); 132 | rk.applyBindings(self.bindings); 133 | } 134 | 135 | rk.draw(base_element, @intCast(c_int, cmd.ElemCount), 1); 136 | } 137 | base_element += @intCast(c_int, cmd.ElemCount); 138 | } // end for CmdBuffer.Data 139 | } 140 | 141 | // reset the scissor 142 | rk.scissor(0, 0, width, height); 143 | rk.setRenderState(.{}); 144 | } 145 | 146 | pub fn updateBuffers(self: *Renderer, draw_data: *imgui.ImDrawData) void { 147 | // Expand buffers if we need more room 148 | if (draw_data.TotalIdxCount > self.index_buffer_size) { 149 | rk.destroyBuffer(self.bindings.index_buffer); 150 | 151 | self.index_buffer_size = @floatToInt(c_long, @intToFloat(f32, draw_data.TotalIdxCount) * 1.5); 152 | var ibuffer = rk.createBuffer(u16, .{ 153 | .type = .index, 154 | .usage = .stream, 155 | .size = self.index_buffer_size, 156 | }); 157 | self.bindings.index_buffer = ibuffer; 158 | } 159 | 160 | if (draw_data.TotalVtxCount > self.vert_buffer_size) { 161 | rk.destroyBuffer(self.bindings.vert_buffers[0]); 162 | 163 | self.vert_buffer_size = @floatToInt(c_long, @intToFloat(f32, draw_data.TotalVtxCount) * 1.5); 164 | var vertex_buffer = rk.createBuffer(gfx.Vertex, .{ 165 | .usage = .stream, 166 | .size = self.vert_buffer_size, 167 | }); 168 | _ = vertex_buffer; 169 | } 170 | } 171 | }; 172 | -------------------------------------------------------------------------------- /gamekit/time.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sdl = @import("sdl"); 3 | 4 | const samples_for_avg = 5; 5 | 6 | pub const Time = struct { 7 | fps_frames: u32 = 0, 8 | prev_time: u32 = 0, 9 | curr_time: u32 = 0, 10 | fps_last_update: u32 = 0, 11 | frames_per_seconds: u32 = 0, 12 | frame_count: u32 = 1, 13 | timestep: Timestep = undefined, 14 | 15 | pub fn init(update_rate: f64) Time { 16 | return Time{ 17 | .timestep = Timestep.init(update_rate), 18 | }; 19 | } 20 | 21 | fn updateFps(self: *Time) void { 22 | self.frame_count += 1; 23 | self.fps_frames += 1; 24 | self.prev_time = self.curr_time; 25 | self.curr_time = sdl.SDL_GetTicks(); 26 | 27 | const time_since_last = self.curr_time - self.fps_last_update; 28 | if (self.curr_time > self.fps_last_update + 1000) { 29 | self.frames_per_seconds = self.fps_frames * 1000 / time_since_last; 30 | self.fps_last_update = self.curr_time; 31 | self.fps_frames = 0; 32 | } 33 | } 34 | 35 | pub fn tick(self: *Time) void { 36 | self.updateFps(); 37 | self.timestep.tick(); 38 | } 39 | 40 | pub fn sleep(self: Time, ms: u32) void { 41 | _ = self; 42 | sdl.SDL_Delay(ms); 43 | } 44 | 45 | pub fn frames(self: Time) u32 { 46 | return self.frame_count; 47 | } 48 | 49 | pub fn ticks(self: Time) u32 { 50 | _ = self; 51 | return sdl.SDL_GetTicks(); 52 | } 53 | 54 | pub fn seconds(self: Time) f32 { 55 | _ = self; 56 | return @intToFloat(f32, sdl.SDL_GetTicks()) / 1000; 57 | } 58 | 59 | pub fn fps(self: Time) u32 { 60 | return self.frames_per_seconds; 61 | } 62 | 63 | pub fn dt(self: Time) f32 { 64 | return self.timestep.fixed_deltatime; 65 | } 66 | 67 | pub fn rawDeltaTime(self: Time) f32 { 68 | return self.timestep.raw_deltatime; 69 | } 70 | 71 | pub fn now(self: Time) u64 { 72 | _ = self; 73 | return sdl.SDL_GetPerformanceCounter(); 74 | } 75 | 76 | /// returns the time in milliseconds since the last call 77 | pub fn laptime(self: Time, last_time: *f64) f64 { 78 | var tmp = last_time; 79 | // now 80 | const n = self.now(); 81 | 82 | const curr_dt: f64 = if (tmp.* != 0) { 83 | @intToFloat(f64, ((n - tmp.*) * 1000.0) / @intToFloat(f64, sdl.SDL_GetPerformanceFrequency())); 84 | } else 0; 85 | return curr_dt; 86 | } 87 | 88 | pub fn toSeconds(self: Time, perf_counter_time: u64) f64 { 89 | _ = self; 90 | return @intToFloat(f64, perf_counter_time) / @intToFloat(f64, sdl.SDL_GetPerformanceFrequency()); 91 | } 92 | 93 | pub fn toMs(self: Time, perf_counter_time: u64) f64 { 94 | _ = self; 95 | return @intToFloat(f64, perf_counter_time) * 1000 / @intToFloat(f64, sdl.SDL_GetPerformanceFrequency()); 96 | } 97 | 98 | /// forces a resync of the timing code. Useful after some slower operations such as level loads or window resizes 99 | pub fn resync(self: *Time) void { 100 | self.timestep.resync = true; 101 | self.timestep.prev_frame_time = sdl.SDL_GetPerformanceCounter() + @floatToInt(u64, self.timestep.fixed_deltatime); 102 | } 103 | 104 | // converted from Tyler Glaiel's: https://github.com/TylerGlaiel/FrameTimingControl/blob/master/frame_timer.cpp 105 | const Timestep = struct { 106 | // compute how many ticks one update should be 107 | fixed_deltatime: f32, 108 | desired_frametime: i32, 109 | raw_deltatime: f32 = 0, 110 | 111 | // these are to snap deltaTime to vsync values if it's close enough 112 | vsync_maxerror: u64, 113 | snap_frequencies: [5]i32 = undefined, 114 | prev_frame_time: u64, 115 | frame_accumulator: i64 = 0, 116 | resync: bool = false, 117 | // time_averager: utils.Ring_Buffer(u64, samples_for_avg), 118 | time_averager: [samples_for_avg]i32 = undefined, 119 | 120 | pub fn init(update_rate: f64) Timestep { 121 | var timestep = Timestep{ 122 | .fixed_deltatime = 1 / @floatCast(f32, update_rate), 123 | .desired_frametime = @floatToInt(i32, @intToFloat(f64, sdl.SDL_GetPerformanceFrequency()) / update_rate), 124 | .vsync_maxerror = sdl.SDL_GetPerformanceFrequency() / 5000, 125 | .prev_frame_time = sdl.SDL_GetPerformanceCounter(), 126 | }; 127 | 128 | // TODO: 129 | // utils.ring_buffer_fill(×tep.time_averager, timestep.desired_frametime); 130 | timestep.time_averager = [samples_for_avg]i32{ timestep.desired_frametime, timestep.desired_frametime, timestep.desired_frametime, timestep.desired_frametime, timestep.desired_frametime }; 131 | 132 | const time_60hz = @floatToInt(i32, @intToFloat(f64, sdl.SDL_GetPerformanceFrequency()) / 60); 133 | timestep.snap_frequencies[0] = time_60hz; // 60fps 134 | timestep.snap_frequencies[1] = time_60hz * 2; // 30fps 135 | timestep.snap_frequencies[2] = time_60hz * 3; // 20fps 136 | timestep.snap_frequencies[3] = time_60hz * 4; // 15fps 137 | timestep.snap_frequencies[4] = @divTrunc(time_60hz + 1, 2); // 120fps 138 | 139 | return timestep; 140 | } 141 | 142 | pub fn tick(self: *Timestep) void { 143 | // frame timer 144 | const current_frame_time = sdl.SDL_GetPerformanceCounter(); 145 | const delta_u32 = @truncate(u32, current_frame_time - self.prev_frame_time); 146 | var delta_time = @intCast(i32, delta_u32); 147 | self.prev_frame_time = current_frame_time; 148 | 149 | // handle unexpected timer anomalies (overflow, extra slow frames, etc) 150 | if (delta_time > self.desired_frametime * 8) delta_time = self.desired_frametime; 151 | if (delta_time < 0) delta_time = 0; 152 | 153 | // vsync time snapping 154 | for (self.snap_frequencies) |snap| { 155 | if (std.math.absCast(delta_time - snap) < self.vsync_maxerror) { 156 | delta_time = snap; 157 | break; 158 | } 159 | } 160 | 161 | // delta time averaging 162 | var dt_avg = delta_time; 163 | var i: usize = 0; 164 | while (i < samples_for_avg - 1) : (i += 1) { 165 | self.time_averager[i] = self.time_averager[i + 1]; 166 | dt_avg += self.time_averager[i]; 167 | } 168 | 169 | self.time_averager[samples_for_avg - 1] = delta_time; 170 | delta_time = @divTrunc(dt_avg, samples_for_avg); 171 | self.raw_deltatime = @intToFloat(f32, delta_u32) / @intToFloat(f32, sdl.SDL_GetPerformanceFrequency()); 172 | 173 | // add to the accumulator 174 | self.frame_accumulator += delta_time; 175 | 176 | // spiral of death protection 177 | if (self.frame_accumulator > self.desired_frametime * 8) self.resync = true; 178 | 179 | // TODO: why does vsync not work on x64 macs all of a sudden?!?! forced sleep. 180 | if (@import("builtin").os.tag == .macos and @import("builtin").target.cpu.arch == std.Target.Cpu.Arch.x86_64) { 181 | const elapsed = self.desired_frametime - delta_time; 182 | if (elapsed > 0) { 183 | const diff = @intToFloat(f32, elapsed) / @intToFloat(f32, sdl.SDL_GetPerformanceFrequency()); 184 | std.time.sleep(@floatToInt(u64, diff * 1000000000)); 185 | } 186 | } 187 | 188 | // TODO: should we zero out the frame_accumulator here? timer resync if requested so reset all state 189 | if (self.resync) { 190 | self.frame_accumulator = self.desired_frametime; 191 | delta_time = self.desired_frametime; 192 | self.resync = false; 193 | } 194 | } 195 | }; 196 | }; 197 | -------------------------------------------------------------------------------- /gamekit/input.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sdl = @import("sdl"); 3 | const gk = @import("gamekit.zig"); 4 | const math = gk.math; 5 | const input_types = @import("input_types.zig"); 6 | pub usingnamespace input_types; 7 | 8 | const FixedList = gk.utils.FixedList; 9 | 10 | const released: u3 = 1; // true only the frame the key is released 11 | const down: u3 = 2; // true the entire time the key is down 12 | const pressed: u3 = 3; // only true if down this frame and not down the previous frame 13 | 14 | pub const MouseButton = enum(usize) { 15 | left = 1, 16 | middle = 2, 17 | right = 3, 18 | }; 19 | 20 | pub const Input = struct { 21 | keys: [@intCast(usize, @enumToInt(input_types.Keys.num_keys))]u2 = [_]u2{0} ** @intCast(usize, @enumToInt(input_types.Keys.num_keys)), 22 | dirty_keys: FixedList(i32, 10), 23 | mouse_buttons: [4]u2 = [_]u2{0} ** 4, 24 | dirty_mouse_buttons: FixedList(u2, 3), 25 | mouse_wheel_y: i32 = 0, 26 | mouse_rel_x: i32 = 0, 27 | mouse_rel_y: i32 = 0, 28 | window_scale: i32 = 0, 29 | 30 | text_edit_buffer: [32]u8 = [_]u8{0} ** 32, 31 | text_input_buffer: [32]u8 = [_]u8{0} ** 32, 32 | text_edit: ?[]u8 = null, 33 | text_input: ?[]u8 = null, 34 | 35 | pub fn init(win_scale: f32) Input { 36 | return .{ 37 | .dirty_keys = FixedList(i32, 10).init(), 38 | .dirty_mouse_buttons = FixedList(u2, 3).init(), 39 | .window_scale = @floatToInt(i32, win_scale), 40 | }; 41 | } 42 | 43 | /// clears any released keys 44 | pub fn newFrame(self: *Input) void { 45 | if (self.dirty_keys.len > 0) { 46 | var iter = self.dirty_keys.iter(); 47 | while (iter.next()) |key| { 48 | const ukey = @intCast(usize, key); 49 | 50 | // guard against double key presses 51 | if (self.keys[ukey] > 0) 52 | self.keys[ukey] -= 1; 53 | } 54 | self.dirty_keys.clear(); 55 | } 56 | 57 | if (self.dirty_mouse_buttons.len > 0) { 58 | var iter = self.dirty_mouse_buttons.iter(); 59 | while (iter.next()) |button| { 60 | 61 | // guard against double mouse presses 62 | if (self.mouse_buttons[button] > 0) 63 | self.mouse_buttons[button] -= 1; 64 | } 65 | self.dirty_mouse_buttons.clear(); 66 | } 67 | 68 | self.mouse_wheel_y = 0; 69 | self.mouse_rel_x = 0; 70 | self.mouse_rel_y = 0; 71 | self.text_edit = null; 72 | self.text_input = null; 73 | } 74 | 75 | pub fn handleEvent(self: *Input, event: *sdl.SDL_Event) void { 76 | switch (event.type) { 77 | sdl.SDL_KEYDOWN, sdl.SDL_KEYUP => self.handleKeyboardEvent(&event.key), 78 | sdl.SDL_MOUSEBUTTONDOWN, sdl.SDL_MOUSEBUTTONUP => self.handleMouseEvent(&event.button), 79 | sdl.SDL_MOUSEWHEEL => self.mouse_wheel_y = event.wheel.y, 80 | sdl.SDL_MOUSEMOTION => { 81 | self.mouse_rel_x = event.motion.xrel; 82 | self.mouse_rel_y = event.motion.yrel; 83 | }, 84 | sdl.SDL_CONTROLLERAXISMOTION => std.log.warn("SDL_CONTROLLERAXISMOTION\n", .{}), 85 | sdl.SDL_CONTROLLERBUTTONDOWN, sdl.SDL_CONTROLLERBUTTONUP => std.log.warn("SDL_CONTROLLERBUTTONUP/DOWN\n", .{}), 86 | sdl.SDL_CONTROLLERDEVICEADDED, sdl.SDL_CONTROLLERDEVICEREMOVED => std.log.warn("SDL_CONTROLLERDEVICEADDED/REMOVED\n", .{}), 87 | sdl.SDL_CONTROLLERDEVICEREMAPPED => std.log.warn("SDL_CONTROLLERDEVICEREMAPPED\n", .{}), 88 | sdl.SDL_TEXTEDITING, sdl.SDL_TEXTINPUT => { 89 | self.text_input_buffer = event.text.text; 90 | const end = std.mem.indexOfScalar(u8, &self.text_input_buffer, 0).?; 91 | self.text_input = self.text_input_buffer[0..end]; 92 | }, 93 | else => {}, 94 | } 95 | } 96 | 97 | fn handleKeyboardEvent(self: *Input, evt: *sdl.SDL_KeyboardEvent) void { 98 | const scancode = @enumToInt(evt.keysym.scancode); 99 | self.dirty_keys.append(scancode); 100 | 101 | if (evt.state == 0) { 102 | self.keys[@intCast(usize, scancode)] = released; 103 | } else { 104 | self.keys[@intCast(usize, scancode)] = pressed; 105 | } 106 | 107 | // std.debug.warn("kb: {s}: {}\n", .{ sdl.SDL_GetKeyName(evt.keysym.sym), evt }); 108 | } 109 | 110 | fn handleMouseEvent(self: *Input, evt: *sdl.SDL_MouseButtonEvent) void { 111 | self.dirty_mouse_buttons.append(@intCast(u2, evt.button)); 112 | if (evt.state == 0) { 113 | self.mouse_buttons[@intCast(usize, evt.button)] = released; 114 | } else { 115 | self.mouse_buttons[@intCast(usize, evt.button)] = pressed; 116 | } 117 | 118 | // std.debug.warn("mouse: {}\n", .{evt}); 119 | } 120 | 121 | /// only true if down this frame and not down the previous frame 122 | pub fn keyPressed(self: Input, key: input_types.Keys) bool { 123 | return self.keys[@intCast(usize, @enumToInt(key))] == pressed; 124 | } 125 | 126 | /// true the entire time the key is down 127 | pub fn keyDown(self: Input, key: input_types.Keys) bool { 128 | return self.keys[@intCast(usize, @enumToInt(key))] > released; 129 | } 130 | 131 | /// true only the frame the key is released 132 | pub fn keyUp(self: Input, key: input_types.Keys) bool { 133 | return self.keys[@intCast(usize, @enumToInt(key))] == released; 134 | } 135 | 136 | /// slice is only valid for the current frame 137 | pub fn textEdit(self: Input) ?[]const u8 { 138 | return self.text_edit orelse null; 139 | } 140 | 141 | /// slice is only valid for the current frame 142 | pub fn textInput(self: Input) ?[]const u8 { 143 | return self.text_input orelse null; 144 | } 145 | 146 | /// only true if down this frame and not down the previous frame 147 | pub fn mousePressed(self: Input, button: MouseButton) bool { 148 | return self.mouse_buttons[@enumToInt(button)] == pressed; 149 | } 150 | 151 | /// true the entire time the button is down 152 | pub fn mouseDown(self: Input, button: MouseButton) bool { 153 | return self.mouse_buttons[@enumToInt(button)] > released; 154 | } 155 | 156 | /// true only the frame the button is released 157 | pub fn mouseUp(self: Input, button: MouseButton) bool { 158 | return self.mouse_buttons[@enumToInt(button)] == released; 159 | } 160 | 161 | pub fn mouseWheel(self: Input) i32 { 162 | return self.mouse_wheel_y; 163 | } 164 | 165 | pub fn mousePos(self: Input) math.Vec2 { 166 | var xc: c_int = undefined; 167 | var yc: c_int = undefined; 168 | _ = sdl.SDL_GetMouseState(&xc, &yc); 169 | return .{ .x = @intToFloat(f32, xc * self.window_scale), .y = @intToFloat(f32, yc * self.window_scale) }; 170 | } 171 | 172 | // gets the scaled mouse position based on the currently bound render texture scale and offset 173 | // as calcuated in OffscreenPass. scale should be scale and offset_n is the calculated x, y value. 174 | pub fn mousePosScaled(self: Input) math.Vec2 { 175 | const p = self.mousePos(); 176 | 177 | const xf = p.x - @intToFloat(f32, self.res_scaler.x); 178 | const yf = p.y - @intToFloat(f32, self.res_scaler.y); 179 | return .{ .x = xf / self.res_scaler.scale, .y = yf / self.res_scaler.scale }; 180 | } 181 | 182 | pub fn mousePosScaledVec(self: Input) math.Vec2 { 183 | var x: i32 = undefined; 184 | var y: i32 = undefined; 185 | self.mousePosScaled(&x, &y); 186 | return .{ .x = @intToFloat(f32, x), .y = @intToFloat(f32, y) }; 187 | } 188 | 189 | pub fn mouseRelMotion(self: Input, x: *i32, y: *i32) void { 190 | x.* = self.mouse_rel_x; 191 | y.* = self.mouse_rel_y; 192 | } 193 | }; 194 | 195 | test "test input" { 196 | var input = Input.init(1); 197 | _ = input.keyPressed(.a); 198 | _ = input.mousePressed(.left); 199 | _ = input.mouseWheel(); 200 | 201 | var x: i32 = undefined; 202 | var y: i32 = undefined; 203 | _ = input.mousePosScaled(&x, &y); 204 | 205 | _ = input.mousePosScaledVec(); 206 | input.mouseRelMotion(&x, &y); 207 | } 208 | -------------------------------------------------------------------------------- /gamekit/draw.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gfx = @import("gfx.zig"); 3 | const renderkit = @import("renderkit"); 4 | const gk = @import("gamekit.zig"); 5 | const math = gk.math; 6 | 7 | const Texture = gfx.Texture; 8 | 9 | pub const draw = struct { 10 | pub var batcher: gfx.Batcher = undefined; 11 | pub var fontbook: *gfx.FontBook = undefined; 12 | 13 | var quad: math.Quad = math.Quad.init(0, 0, 1, 1, 1, 1); 14 | var white_tex: Texture = undefined; 15 | 16 | pub fn init() void { 17 | white_tex = Texture.initSingleColor(0xFFFFFFFF); 18 | batcher = gfx.Batcher.init(std.heap.c_allocator, 1000); 19 | 20 | fontbook = gfx.FontBook.init(std.heap.c_allocator, 128, 128, .nearest) catch unreachable; 21 | _ = fontbook.addFontMem("ProggyTiny", @embedFile("assets/ProggyTiny.ttf"), false); 22 | fontbook.setSize(10); 23 | } 24 | 25 | pub fn deinit() void { 26 | batcher.deinit(); 27 | white_tex.deinit(); 28 | fontbook.deinit(); 29 | } 30 | 31 | /// binds a Texture to the Bindings in the Batchers DynamicMesh 32 | pub fn bindTexture(texture: Texture, slot: c_uint) void { 33 | batcher.mesh.bindImage(texture.img, slot); 34 | } 35 | 36 | /// unbinds a previously bound texture. All texture slots > 0 must be unbound manually! 37 | pub fn unbindTexture(slot: c_uint) void { 38 | batcher.mesh.bindImage(0, slot); 39 | } 40 | 41 | // Drawing 42 | pub fn tex(texture: Texture, position: math.Vec2) void { 43 | quad.setFill(texture.width, texture.height); 44 | 45 | var mat = math.Mat32.initTransform(.{ .x = position.x, .y = position.y }); 46 | batcher.draw(texture, quad, mat, math.Color.white); 47 | } 48 | 49 | pub fn texScale(texture: Texture, position: math.Vec2, scale: f32) void { 50 | quad.setFill(texture.width, texture.height); 51 | 52 | var mat = math.Mat32.initTransform(.{ .x = position.x, .y = position.y, .sx = scale, .sy = scale }); 53 | batcher.draw(texture, quad, mat, math.Color.white); 54 | } 55 | 56 | pub fn texScaleOrigin(texture: Texture, x: f32, y: f32, scale: f32, ox: f32, oy: f32) void { 57 | quad.setFill(texture.width, texture.height); 58 | 59 | var mat = math.Mat32.initTransform(.{ .x = x, .y = y, .sx = scale, .sy = scale, .ox = ox, .oy = oy }); 60 | batcher.draw(texture, quad, mat, math.Color.white); 61 | } 62 | 63 | pub fn texScaleOriginRotation(texture: Texture, x: f32, y: f32, scale: f32, ox: f32, oy: f32, angle: f32) void { 64 | quad.setFill(texture.width, texture.height); 65 | 66 | var mat = math.Mat32.initTransform(.{ .x = x, .y = y, .sx = scale, .sy = scale, .ox = ox, .oy = oy, .angle = angle }); 67 | batcher.draw(texture, quad, mat, math.Color.white); 68 | } 69 | 70 | pub fn texMatrix(texture: Texture, mat: math.Mat32) void { 71 | quad.setFill(texture.width, texture.height); 72 | batcher.draw(texture, quad, mat, math.Color.white); 73 | } 74 | 75 | pub fn texViewport(texture: Texture, viewport: math.RectI, transform: math.Mat32) void { 76 | quad.setImageDimensions(texture.width, texture.height); 77 | quad.setViewportRectI(viewport); 78 | batcher.draw(texture, quad, transform, math.Color.white); 79 | } 80 | 81 | pub fn text(str: []const u8, x: f32, y: f32, fb: ?*gfx.FontBook) void { 82 | var book = fb orelse fontbook; 83 | // TODO: dont hardcode scale as 4 84 | var matrix = math.Mat32.initTransform(.{ .x = x, .y = y, .sx = 2, .sy = 2 }); 85 | 86 | var fons_quad = book.getQuad(); 87 | var iter = book.getTextIterator(str); 88 | while (book.textIterNext(&iter, &fons_quad)) { 89 | quad.positions[0] = .{ .x = fons_quad.x0, .y = fons_quad.y0 }; 90 | quad.positions[1] = .{ .x = fons_quad.x1, .y = fons_quad.y0 }; 91 | quad.positions[2] = .{ .x = fons_quad.x1, .y = fons_quad.y1 }; 92 | quad.positions[3] = .{ .x = fons_quad.x0, .y = fons_quad.y1 }; 93 | 94 | quad.uvs[0] = .{ .x = fons_quad.s0, .y = fons_quad.t0 }; 95 | quad.uvs[1] = .{ .x = fons_quad.s1, .y = fons_quad.t0 }; 96 | quad.uvs[2] = .{ .x = fons_quad.s1, .y = fons_quad.t1 }; 97 | quad.uvs[3] = .{ .x = fons_quad.s0, .y = fons_quad.t1 }; 98 | 99 | batcher.draw(book.texture.?, quad, matrix, math.Color{ .value = iter.color }); 100 | } 101 | } 102 | 103 | pub fn textOptions(str: []const u8, fb: ?*gfx.FontBook, options: struct { x: f32, y: f32, rot: f32 = 0, sx: f32 = 1, sy: f32 = 1, alignment: gfx.FontBook.Align = .default, color: math.Color = math.Color.white }) void { 104 | var book = fb orelse fontbook; 105 | var matrix = math.Mat32.initTransform(.{ .x = options.x, .y = options.y, .angle = options.rot, .sx = options.sx, .sy = options.sy }); 106 | book.setAlign(options.alignment); 107 | 108 | var fons_quad = book.getQuad(); 109 | var iter = book.getTextIterator(str); 110 | while (book.textIterNext(&iter, &fons_quad)) { 111 | quad.positions[0] = .{ .x = fons_quad.x0, .y = fons_quad.y0 }; 112 | quad.positions[1] = .{ .x = fons_quad.x1, .y = fons_quad.y0 }; 113 | quad.positions[2] = .{ .x = fons_quad.x1, .y = fons_quad.y1 }; 114 | quad.positions[3] = .{ .x = fons_quad.x0, .y = fons_quad.y1 }; 115 | 116 | quad.uvs[0] = .{ .x = fons_quad.s0, .y = fons_quad.t0 }; 117 | quad.uvs[1] = .{ .x = fons_quad.s1, .y = fons_quad.t0 }; 118 | quad.uvs[2] = .{ .x = fons_quad.s1, .y = fons_quad.t1 }; 119 | quad.uvs[3] = .{ .x = fons_quad.s0, .y = fons_quad.t1 }; 120 | 121 | batcher.draw(book.texture.?, quad, matrix, options.color); 122 | } 123 | } 124 | 125 | pub fn point(position: math.Vec2, size: f32, color: math.Color) void { 126 | quad.setFill(size, size); 127 | 128 | const offset = if (size == 1) 0 else size * 0.5; 129 | var mat = math.Mat32.initTransform(.{ .x = position.x, .y = position.y, .ox = offset, .oy = offset }); 130 | batcher.draw(white_tex, quad, mat, color); 131 | } 132 | 133 | pub fn line(start: math.Vec2, end: math.Vec2, thickness: f32, color: math.Color) void { 134 | quad.setFill(1, 1); 135 | 136 | const angle = start.angleBetween(end); 137 | const length = start.distance(end); 138 | 139 | var mat = math.Mat32.initTransform(.{ .x = start.x, .y = start.y, .angle = angle, .sx = length, .sy = thickness }); 140 | batcher.draw(white_tex, quad, mat, color); 141 | } 142 | 143 | pub fn rect(position: math.Vec2, width: f32, height: f32, color: math.Color) void { 144 | quad.setFill(width, height); 145 | var mat = math.Mat32.initTransform(.{ .x = position.x, .y = position.y }); 146 | batcher.draw(white_tex, quad, mat, color); 147 | } 148 | 149 | pub fn hollowRect(position: math.Vec2, width: f32, height: f32, thickness: f32, color: math.Color) void { 150 | const tr = math.Vec2{ .x = position.x + width, .y = position.y }; 151 | const br = math.Vec2{ .x = position.x + width, .y = position.y + height }; 152 | const bl = math.Vec2{ .x = position.x, .y = position.y + height }; 153 | 154 | line(position, tr, thickness, color); 155 | line(tr, br, thickness, color); 156 | line(br, bl, thickness, color); 157 | line(bl, position, thickness, color); 158 | } 159 | 160 | pub fn circle(center: math.Vec2, radius: f32, thickness: f32, resolution: i32, color: math.Color) void { 161 | quad.setFill(white_tex.width, white_tex.height); 162 | 163 | var last = math.Vec2.init(1, 0).scale(radius); 164 | var last_p = last.orthogonal(); 165 | 166 | var i: usize = 0; 167 | while (i <= resolution) : (i += 1) { 168 | const at = math.Vec2.angleToVec(@intToFloat(f32, i) * std.math.pi * 0.5 / @intToFloat(f32, resolution), radius); 169 | const at_p = at.orthogonal(); 170 | 171 | line(center.addv(last), center.addv(at), thickness, color); 172 | line(center.subv(last), center.subv(at), thickness, color); 173 | line(center.addv(last_p), center.addv(at_p), thickness, color); 174 | line(center.subv(last_p), center.subv(at_p), thickness, color); 175 | 176 | last = at; 177 | last_p = at_p; 178 | } 179 | } 180 | 181 | pub fn hollowPolygon(verts: []const math.Vec2, thickness: f32, color: math.Color) void { 182 | var i: usize = 0; 183 | while (i < verts.len - 1) : (i += 1) { 184 | line(verts[i], verts[i + 1], thickness, color); 185 | } 186 | line(verts[verts.len - 1], verts[0], thickness, color); 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /gamekit/math/mat32.zig: -------------------------------------------------------------------------------- 1 | const Vec2 = @import("vec2.zig").Vec2; 2 | const Color = @import("color.zig").Color; 3 | const Quad = @import("quad.zig").Quad; 4 | const Vertex = @import("../gamekit.zig").gfx.Vertex; 5 | const std = @import("std"); 6 | const math = std.math; 7 | 8 | // 3 row, 2 col 2D matrix 9 | // 10 | // m[0] m[2] m[4] 11 | // m[1] m[3] m[5] 12 | // 13 | // 0: scaleX 2: sin 4: transX 14 | // 1: cos 3: scaleY 5: transY 15 | // 16 | pub const Mat32 = extern struct { 17 | data: [6]f32 = undefined, 18 | 19 | pub const TransformParams = struct { x: f32 = 0, y: f32 = 0, angle: f32 = 0, sx: f32 = 1, sy: f32 = 1, ox: f32 = 0, oy: f32 = 0 }; 20 | 21 | pub const identity = Mat32{ .data = .{ 1, 0, 0, 1, 0, 0 } }; 22 | 23 | pub fn format(self: Mat32, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 24 | _ = fmt; 25 | _ = options; 26 | return writer.print("{d:0.6}, {d:0.6}, {d:0.6}, {d:0.6}, {d:0.6}, {d:0.6}", .{ self.data[0], self.data[1], self.data[2], self.data[3], self.data[4], self.data[5] }); 27 | } 28 | 29 | pub fn init() Mat32 { 30 | return identity; 31 | } 32 | 33 | pub fn initTransform(vals: TransformParams) Mat32 { 34 | var mat = Mat32{}; 35 | mat.setTransform(vals); 36 | return mat; 37 | } 38 | 39 | pub fn initOrthoInverted(width: f32, height: f32) Mat32 { 40 | var result = Mat32{}; 41 | result.data[0] = 2 / width; 42 | result.data[3] = 2 / height; 43 | result.data[4] = -1; 44 | result.data[5] = -1; 45 | return result; 46 | } 47 | 48 | pub fn initOrtho(width: f32, height: f32) Mat32 { 49 | var result = Mat32{}; 50 | result.data[0] = 2 / width; 51 | result.data[3] = -2 / height; 52 | result.data[4] = -1; 53 | result.data[5] = 1; 54 | return result; 55 | } 56 | 57 | pub fn initOrthoOffCenter(width: f32, height: f32) Mat32 { 58 | const half_w = @ceil(width / 2); 59 | const half_h = @ceil(height / 2); 60 | 61 | var result = identity; 62 | result.data[0] = 2.0 / (half_w + half_w); 63 | result.data[3] = 2.0 / (-half_h - half_h); 64 | result.data[4] = (-half_w + half_w) / (-half_w - half_w); 65 | result.data[5] = (half_h - half_h) / (half_h + half_h); 66 | return result; 67 | } 68 | 69 | pub fn setTransform(self: *Mat32, vals: TransformParams) void { 70 | const c = math.cos(vals.angle); 71 | const s = math.sin(vals.angle); 72 | 73 | // matrix multiplication carried out on paper: 74 | // |1 x| |c -s | |sx | |1 -ox| 75 | // | 1 y| |s c | | sy | | 1 -oy| 76 | // move rotate scale origin 77 | self.data[0] = c * vals.sx; 78 | self.data[1] = s * vals.sx; 79 | self.data[2] = -s * vals.sy; 80 | self.data[3] = c * vals.sy; 81 | self.data[4] = vals.x - vals.ox * self.data[0] - vals.oy * self.data[2]; 82 | self.data[5] = vals.y - vals.ox * self.data[1] - vals.oy * self.data[3]; 83 | } 84 | 85 | pub fn invert(self: Mat32) Mat32 { 86 | var res = Mat32{}; 87 | var det = 1 / (self.data[0] * self.data[3] - self.data[1] * self.data[2]); 88 | 89 | res.data[0] = self.data[3] * det; 90 | res.data[1] = -self.data[1] * det; 91 | 92 | res.data[2] = -self.data[2] * det; 93 | res.data[3] = self.data[0] * det; 94 | 95 | res.data[4] = (self.data[5] * self.data[2] - self.data[4] * self.data[3]) * det; 96 | res.data[5] = -(self.data[5] * self.data[0] - self.data[4] * self.data[1]) * det; 97 | 98 | return res; 99 | } 100 | 101 | pub fn mul(self: Mat32, r: Mat32) Mat32 { 102 | var result = Mat32{}; 103 | result.data[0] = self.data[0] * r.data[0] + self.data[2] * r.data[1]; 104 | result.data[1] = self.data[1] * r.data[0] + self.data[3] * r.data[1]; 105 | result.data[2] = self.data[0] * r.data[2] + self.data[2] * r.data[3]; 106 | result.data[3] = self.data[1] * r.data[2] + self.data[3] * r.data[3]; 107 | result.data[4] = self.data[0] * r.data[4] + self.data[2] * r.data[5] + self.data[4]; 108 | result.data[5] = self.data[1] * r.data[4] + self.data[3] * r.data[5] + self.data[5]; 109 | return result; 110 | } 111 | 112 | pub fn translate(self: *Mat32, x: f32, y: f32) void { 113 | self.data[4] = self.data[0] * x + self.data[2] * y + self.data[4]; 114 | self.data[5] = self.data[1] * x + self.data[3] * y + self.data[5]; 115 | } 116 | 117 | pub fn rotate(self: *Mat32, rads: f32) void { 118 | const cos = math.cos(rads); 119 | const sin = math.sin(rads); 120 | 121 | const nm0 = self.data[0] * cos + self.data[2] * sin; 122 | const nm1 = self.data[1] * cos + self.data[3] * sin; 123 | 124 | self.data[2] = self.data[0] * -sin + self.data[2] * cos; 125 | self.data[3] = self.data[1] * -sin + self.data[3] * cos; 126 | self.data[0] = nm0; 127 | self.data[1] = nm1; 128 | } 129 | 130 | pub fn scale(self: *Mat32, x: f32, y: f32) void { 131 | self.data[0] *= x; 132 | self.data[1] *= x; 133 | self.data[2] *= y; 134 | self.data[3] *= y; 135 | } 136 | 137 | pub fn transformVec2(self: Mat32, pos: Vec2) Vec2 { 138 | return .{ 139 | .x = pos.x * self.data[0] + pos.y * self.data[2] + self.data[4], 140 | .y = pos.x * self.data[1] + pos.y * self.data[3] + self.data[5], 141 | }; 142 | } 143 | 144 | pub fn transformVec2Slice(self: Mat32, comptime T: type, dst: []T, src: []Vec2) void { 145 | for (src, 0..) |_, i| { 146 | const x = src[i].x * self.data[0] + src[i].y * self.data[2] + self.data[4]; 147 | const y = src[i].x * self.data[1] + src[i].y * self.data[3] + self.data[5]; 148 | dst[i].x = x; 149 | dst[i].y = y; 150 | } 151 | } 152 | 153 | /// transforms the positions in Quad and copies them to dst along with the uvs and color. This could be made generic 154 | /// if we have other common Vertex types 155 | pub fn transformQuad(self: Mat32, dst: []Vertex, quad: Quad, color: Color) void { 156 | for (dst, 0..) |*item, i| { 157 | item.*.pos.x = quad.positions[i].x * self.data[0] + quad.positions[i].y * self.data[2] + self.data[4]; 158 | item.*.pos.y = quad.positions[i].x * self.data[1] + quad.positions[i].y * self.data[3] + self.data[5]; 159 | item.*.uv = quad.uvs[i]; 160 | item.*.col = color.value; 161 | } 162 | } 163 | 164 | pub fn transformVertexSlice(self: Mat32, dst: []Vertex) void { 165 | for (dst, 0..) |_, i| { 166 | const x = dst[i].pos.x * self.data[0] + dst[i].pos.y * self.data[2] + self.data[4]; 167 | const y = dst[i].pos.x * self.data[1] + dst[i].pos.y * self.data[3] + self.data[5]; 168 | 169 | // we defer setting because src and dst are the same 170 | dst[i].pos.x = x; 171 | dst[i].pos.y = y; 172 | } 173 | } 174 | }; 175 | 176 | test "mat32 tests" { 177 | const i = Mat32.identity; 178 | _ = i; 179 | const mat1 = Mat32.initTransform(.{ .x = 10, .y = 10 }); 180 | var mat2 = Mat32{}; 181 | mat2.setTransform(.{ .x = 10, .y = 10 }); 182 | std.testing.expectEqual(mat2, mat1); 183 | 184 | var mat3 = Mat32.init(); 185 | mat3.setTransform(.{ .x = 10, .y = 10 }); 186 | std.testing.expectEqual(mat3, mat1); 187 | 188 | const mat4 = Mat32.initOrtho(640, 480); 189 | _ = mat4; 190 | const mat5 = Mat32.initOrthoOffCenter(640, 480); 191 | _ = mat5; 192 | 193 | var mat6 = Mat32.init(); 194 | mat6.translate(10, 20); 195 | std.testing.expectEqual(mat6.data[4], 10); 196 | } 197 | 198 | test "mat32 transform tests" { 199 | const i = Mat32.identity; 200 | const vec = Vec2.init(44, 55); 201 | const vec_t = i.transformVec2(vec); 202 | std.testing.expectEqual(vec, vec_t); 203 | 204 | var verts = [_]Vertex{ 205 | .{ .pos = .{ .x = 0.5, .y = 0.5 }, .uv = .{ .x = 1, .y = 1 }, .col = 0xFFFFFFFF }, 206 | .{ .pos = .{ .x = 0.5, .y = -0.5 }, .uv = .{ .x = 1, .y = 0 }, .col = 0x00FF0FFF }, 207 | .{ .pos = .{ .x = -0.5, .y = -0.5 }, .uv = .{ .x = 0, .y = 0 }, .col = 0xFF00FFFF }, 208 | .{ .pos = .{ .x = -0.5, .y = -0.5 }, .uv = .{ .x = 0, .y = 0 }, .col = 0xFF00FFFF }, 209 | .{ .pos = .{ .x = -0.5, .y = 0.5 }, .uv = .{ .x = 0, .y = 1 }, .col = 0x00FFFFFF }, 210 | .{ .pos = .{ .x = 0.5, .y = 0.5 }, .uv = .{ .x = 1, .y = 1 }, .col = 0xFFFFFFFF }, 211 | }; 212 | const quad = Quad.init(0, 0, 50, 50, 600, 400); 213 | _ = quad; 214 | // i.transformQuad(verts[0..], quad, Color.red); // triggers Zig bug 215 | 216 | i.transformVertexSlice(verts[0..]); 217 | } 218 | -------------------------------------------------------------------------------- /gamekit/math/color.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const Color = extern union { 4 | value: u32, 5 | comps: packed struct { 6 | r: u8, 7 | g: u8, 8 | b: u8, 9 | a: u8, 10 | }, 11 | 12 | /// parses a hex string color literal. 13 | /// allowed formats are: 14 | /// - `RGB` 15 | /// - `RGBA` 16 | /// - `#RGB` 17 | /// - `#RGBA` 18 | /// - `RRGGBB` 19 | /// - `#RRGGBB` 20 | /// - `RRGGBBAA` 21 | /// - `#RRGGBBAA` 22 | pub fn parse(comptime str: []const u8) !Color { 23 | switch (str.len) { 24 | // RGB 25 | 3 => { 26 | const r = try std.fmt.parseInt(u8, str[0..1], 16); 27 | const g = try std.fmt.parseInt(u8, str[1..2], 16); 28 | const b = try std.fmt.parseInt(u8, str[2..3], 16); 29 | 30 | return fromBytes( 31 | r | (r << 4), 32 | g | (g << 4), 33 | b | (b << 4), 34 | ); 35 | }, 36 | 37 | // #RGB, RGBA 38 | 4 => { 39 | if (str[0] == '#') 40 | return parse(str[1..]); 41 | 42 | const r = try std.fmt.parseInt(u8, str[0..1], 16); 43 | const g = try std.fmt.parseInt(u8, str[1..2], 16); 44 | const b = try std.fmt.parseInt(u8, str[2..3], 16); 45 | const a = try std.fmt.parseInt(u8, str[3..4], 16); 46 | 47 | // bit-expand the patters to a uniform range 48 | return fromBytes( 49 | r | (r << 4), 50 | g | (g << 4), 51 | b | (b << 4), 52 | a | (a << 4), 53 | ); 54 | }, 55 | 56 | // #RGBA 57 | 5 => return parse(str[1..]), 58 | 59 | // RRGGBB 60 | 6 => { 61 | const r = try std.fmt.parseInt(u8, str[0..2], 16); 62 | const g = try std.fmt.parseInt(u8, str[2..4], 16); 63 | const b = try std.fmt.parseInt(u8, str[4..6], 16); 64 | 65 | return fromBytes(r, g, b, 255); 66 | }, 67 | 68 | // #RRGGBB 69 | 7 => return parse(str[1..]), 70 | 71 | // RRGGBBAA 72 | 8 => { 73 | const r = try std.fmt.parseInt(u8, str[0..2], 16); 74 | const g = try std.fmt.parseInt(u8, str[2..4], 16); 75 | const b = try std.fmt.parseInt(u8, str[4..6], 16); 76 | const a = try std.fmt.parseInt(u8, str[6..8], 16); 77 | 78 | return fromBytes(r, g, b, a); 79 | }, 80 | 81 | // #RRGGBBAA 82 | 9 => return parse(str[1..]), 83 | 84 | else => return error.UnknownFormat, 85 | } 86 | } 87 | 88 | pub fn fromBytes(r: u8, g: u8, b: u8, a: u8) Color { 89 | return .{ .value = (r) | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24) }; 90 | } 91 | 92 | pub fn fromRgbBytes(r: u8, g: u8, b: u8) Color { 93 | return fromBytes(r, g, b, 255); 94 | } 95 | 96 | pub fn fromRgb(r: f32, g: f32, b: f32) Color { 97 | return fromBytes(@floatToInt(u8, @round(r * 255)), @floatToInt(u8, @round(g * 255)), @floatToInt(u8, @round(b * 255)), @as(u8, 255)); 98 | } 99 | 100 | pub fn fromRgba(r: f32, g: f32, b: f32, a: f32) Color { 101 | return fromBytes(@floatToInt(u8, @round(r * 255)), @floatToInt(u8, @round(g * 255)), @floatToInt(u8, @round(b * 255)), @floatToInt(u8, @round(a * 255))); 102 | } 103 | 104 | pub fn fromI32(r: i32, g: i32, b: i32, a: i32) Color { 105 | return fromBytes(@truncate(u8, @intCast(u32, r)), @truncate(u8, @intCast(u32, g)), @truncate(u8, @intCast(u32, b)), @truncate(u8, @intCast(u32, a))); 106 | } 107 | 108 | pub fn r_val(self: Color) u8 { 109 | return @truncate(u8, self.value); 110 | } 111 | 112 | pub fn g_val(self: Color) u8 { 113 | return @truncate(u8, self.value >> 8); 114 | } 115 | 116 | pub fn b_val(self: Color) u8 { 117 | return @truncate(u8, self.value >> 16); 118 | } 119 | 120 | pub fn a_val(self: Color) u8 { 121 | return @truncate(u8, self.value >> 24); 122 | } 123 | 124 | pub fn set_r(self: *Color, r: u8) void { 125 | self.value = (self.value & 0xffffff00) | r; 126 | } 127 | 128 | pub fn set_g(self: *Color, g: u8) void { 129 | self.value = (self.value & 0xffff00ff) | g; 130 | } 131 | 132 | pub fn set_b(self: *Color, b: u8) void { 133 | self.value = (self.value & 0xff00ffff) | b; 134 | } 135 | 136 | pub fn set_a(self: *Color, a: u8) void { 137 | self.value = (self.value & 0x00ffffff) | a; 138 | } 139 | 140 | pub fn asArray(self: Color) [4]f32 { 141 | return [_]f32{ 142 | @intToFloat(f32, self.comps.r) / 255, 143 | @intToFloat(f32, self.comps.g) / 255, 144 | @intToFloat(f32, self.comps.b) / 255, 145 | @intToFloat(f32, self.comps.a) / 255, 146 | }; 147 | } 148 | 149 | pub fn scale(self: Color, s: f32) Color { 150 | const r = @floatToInt(i32, @intToFloat(f32, self.r_val()) * s); 151 | const g = @floatToInt(i32, @intToFloat(f32, self.g_val()) * s); 152 | const b = @floatToInt(i32, @intToFloat(f32, self.b_val()) * s); 153 | const a = @floatToInt(i32, @intToFloat(f32, self.a_val()) * s); 154 | return fromI32(r, g, b, a); 155 | } 156 | 157 | pub const white = Color{ .value = 0xFFFFFFFF }; 158 | pub const black = Color{ .value = 0xFF000000 }; 159 | pub const transparent = Color{ .comps = .{ .r = 0, .g = 0, .b = 0, .a = 0 } }; 160 | pub const aya = Color{ .comps = .{ .r = 204, .g = 51, .b = 77, .a = 255 } }; 161 | pub const light_gray = Color{ .comps = .{ .r = 200, .g = 200, .b = 200, .a = 255 } }; 162 | pub const gray = Color{ .comps = .{ .r = 130, .g = 130, .b = 130, .a = 255 } }; 163 | pub const dark_gray = Color{ .comps = .{ .r = 80, .g = 80, .b = 80, .a = 255 } }; 164 | pub const yellow = Color{ .comps = .{ .r = 253, .g = 249, .b = 0, .a = 255 } }; 165 | pub const gold = Color{ .comps = .{ .r = 255, .g = 203, .b = 0, .a = 255 } }; 166 | pub const orange = Color{ .comps = .{ .r = 255, .g = 161, .b = 0, .a = 255 } }; 167 | pub const pink = Color{ .comps = .{ .r = 255, .g = 109, .b = 194, .a = 255 } }; 168 | pub const red = Color{ .comps = .{ .r = 230, .g = 41, .b = 55, .a = 255 } }; 169 | pub const maroon = Color{ .comps = .{ .r = 190, .g = 33, .b = 55, .a = 255 } }; 170 | pub const green = Color{ .comps = .{ .r = 0, .g = 228, .b = 48, .a = 255 } }; 171 | pub const lime = Color{ .comps = .{ .r = 0, .g = 158, .b = 47, .a = 255 } }; 172 | pub const dark_green = Color{ .comps = .{ .r = 0, .g = 117, .b = 44, .a = 255 } }; 173 | pub const sky_blue = Color{ .comps = .{ .r = 102, .g = 191, .b = 255, .a = 255 } }; 174 | pub const blue = Color{ .comps = .{ .r = 0, .g = 121, .b = 241, .a = 255 } }; 175 | pub const dark_blue = Color{ .comps = .{ .r = 0, .g = 82, .b = 172, .a = 255 } }; 176 | pub const purple = Color{ .comps = .{ .r = 200, .g = 122, .b = 255, .a = 255 } }; 177 | pub const voilet = Color{ .comps = .{ .r = 135, .g = 60, .b = 190, .a = 255 } }; 178 | pub const dark_purple = Color{ .comps = .{ .r = 112, .g = 31, .b = 126, .a = 255 } }; 179 | pub const beige = Color{ .comps = .{ .r = 211, .g = 176, .b = 131, .a = 255 } }; 180 | pub const brown = Color{ .comps = .{ .r = 127, .g = 106, .b = 79, .a = 255 } }; 181 | pub const dark_brown = Color{ .comps = .{ .r = 76, .g = 63, .b = 47, .a = 255 } }; 182 | pub const magenta = Color{ .comps = .{ .r = 255, .g = 0, .b = 255, .a = 255 } }; 183 | }; 184 | 185 | test "test color" { 186 | const ColorConverter = extern struct { r: u8, g: u8, b: u8, a: u8 }; 187 | 188 | const c = Color{ .value = @as(u32, 0xFF9900FF) }; 189 | _ = c; 190 | const cc = Color{ .value = @bitCast(u32, [4]u8{ 10, 45, 34, 255 }) }; 191 | const ccc = Color{ .value = @bitCast(u32, ColorConverter{ .r = 10, .g = 45, .b = 34, .a = 255 }) }; 192 | // const c = @bitCast(Color, @as(u32, 0xFF9900FF)); 193 | // const cc = @bitCast(Color, [4]u8{ 10, 45, 34, 255 }); 194 | // const ccc = @bitCast(Color, ColorConverter{ .r = 10, .g = 45, .b = 34, .a = 255 }); 195 | std.testing.expectEqual(cc.value, ccc.value); 196 | 197 | // const c2 = Color.fromBytes(10, 45, 34, 255); 198 | const c3 = Color.fromRgb(0.2, 0.4, 0.3); 199 | const c4 = Color.fromRgba(0.2, 0.4, 0.3, 1.0); 200 | std.testing.expectEqual(c3.value, c4.value); 201 | 202 | var c5 = Color.fromI32(10, 45, 34, 255); 203 | std.testing.expectEqual(c5.r_val(), 10); 204 | std.testing.expectEqual(c5.g_val(), 45); 205 | std.testing.expectEqual(c5.b_val(), 34); 206 | std.testing.expectEqual(c5.a_val(), 255); 207 | 208 | c5.set_r(100); 209 | std.testing.expectEqual(c5.r_val(), 100); 210 | 211 | const scaled = c5.scale(2); 212 | std.testing.expectEqual(scaled.r_val(), 200); 213 | } 214 | -------------------------------------------------------------------------------- /examples/mode7.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const gk = @import("gamekit"); 3 | const gfx = gk.gfx; 4 | const math = gk.math; 5 | const shaders = @import("assets/shaders.zig"); 6 | 7 | const Texture = gk.gfx.Texture; 8 | const Color = gk.math.Color; 9 | 10 | const Block = struct { 11 | tex: Texture, 12 | pos: math.Vec2, 13 | scale: f32, 14 | dist: f32, 15 | }; 16 | 17 | const Camera = struct { 18 | sw: f32, 19 | sh: f32, 20 | x: f32 = 0, 21 | y: f32 = 0, 22 | r: f32 = 0, 23 | z: f32 = 32, 24 | f: f32 = 1, 25 | o: f32 = 1, 26 | x1: f32 = 0, 27 | y1: f32 = 0, 28 | x2: f32 = 0, 29 | y2: f32 = 0, 30 | sprites: std.ArrayList(Block) = undefined, 31 | 32 | pub fn init(sw: f32, sh: f32) Camera { 33 | var cam = Camera{ .sw = sw, .sh = sh, .sprites = std.ArrayList(Block).init(std.heap.c_allocator) }; 34 | cam.setRotation(0); 35 | return cam; 36 | } 37 | 38 | pub fn deinit(self: Camera) void { 39 | self.sprites.deinit(); 40 | } 41 | 42 | pub fn setRotation(self: *Camera, rot: f32) void { 43 | self.r = rot; 44 | self.x1 = std.math.sin(rot); 45 | self.y1 = std.math.cos(rot); 46 | self.x2 = -std.math.cos(rot); 47 | self.y2 = std.math.sin(rot); 48 | } 49 | 50 | pub fn toWorld(self: Camera, pos: gk.math.Vec2) gk.math.Vec2 { 51 | const sx = (self.sw / 2 - pos.x) * self.z / (self.sw / self.sh); 52 | const sy = (self.o * self.sh - pos.y) * (self.z / self.f); 53 | 54 | const rot_x = sx * self.x1 + sy * self.y1; 55 | const rot_y = sx * self.x2 + sy * self.y2; 56 | 57 | return .{ .x = rot_x / pos.y + self.x, .y = rot_y / pos.y + self.y }; 58 | } 59 | 60 | pub fn toScreen(self: Camera, pos: gk.math.Vec2) struct { x: f32, y: f32, size: f32 } { 61 | const obj_x = -(self.x - pos.x) / self.z; 62 | const obj_y = (self.y - pos.y) / self.z; 63 | 64 | const space_x = (-obj_x * self.x1 - obj_y * self.y1); 65 | const space_y = (obj_x * self.x2 + obj_y * self.y2) * self.f; 66 | 67 | const distance = 1 - space_y; 68 | const screen_x = (space_x / distance) * self.o * self.sw + self.sw / 2; 69 | const screen_y = ((space_y + self.o - 1) / distance) * self.sh + self.sh; 70 | 71 | // Should be approximately one pixel on the plane 72 | const size = ((1 / distance) / self.z * self.o) * self.sw; 73 | 74 | return .{ .x = screen_x, .y = screen_y, .size = size }; 75 | } 76 | 77 | pub fn placeSprite(self: *Camera, tex: gk.gfx.Texture, pos: gk.math.Vec2, scale: f32) void { 78 | const dim = self.toScreen(pos); 79 | const sx2 = (dim.size * scale) / tex.width; 80 | 81 | if (sx2 < 0) return; 82 | 83 | _ = self.sprites.append(.{ 84 | .tex = tex, 85 | .pos = .{ .x = dim.x, .y = dim.y }, 86 | .scale = sx2, 87 | .dist = dim.size, 88 | }) catch unreachable; 89 | } 90 | 91 | pub fn renderSprites(self: *Camera) void { 92 | if (self.sprites.items.len > 0) { 93 | std.sort.sort(Block, self.sprites.items, {}, sort); 94 | } 95 | 96 | for (self.sprites.items) |sprite| { 97 | gfx.draw.texScaleOrigin(sprite.tex, sprite.pos.x, sprite.pos.y, sprite.scale, sprite.tex.width / 2, sprite.tex.height); 98 | } 99 | self.sprites.items.len = 0; 100 | } 101 | 102 | fn sort(ctx: void, a: Block, b: Block) bool { 103 | _ = ctx; 104 | return a.dist < b.dist; 105 | } 106 | }; 107 | 108 | var map: Texture = undefined; 109 | var block: Texture = undefined; 110 | var mode7_shader: shaders.Mode7Shader = undefined; 111 | var camera: Camera = undefined; 112 | var blocks: std.ArrayList(math.Vec2) = undefined; 113 | var wrap: f32 = 0; 114 | 115 | pub fn main() !void { 116 | try gk.run(.{ 117 | .init = init, 118 | .update = update, 119 | .render = render, 120 | .shutdown = shutdown, 121 | .window = .{ .resizable = false }, 122 | }); 123 | } 124 | 125 | fn init() !void { 126 | const drawable_size = gk.window.drawableSize(); 127 | camera = Camera.init(@intToFloat(f32, drawable_size.w), @intToFloat(f32, drawable_size.h)); 128 | 129 | map = Texture.initFromFile(std.heap.c_allocator, "examples/assets/textures/mario_kart.png", .nearest) catch unreachable; 130 | block = Texture.initFromFile(std.heap.c_allocator, "examples/assets/textures/block.png", .nearest) catch unreachable; 131 | 132 | mode7_shader = shaders.createMode7Shader(); 133 | 134 | blocks = std.ArrayList(math.Vec2).init(std.heap.c_allocator); 135 | _ = blocks.append(.{ .x = 0, .y = 0 }) catch unreachable; 136 | 137 | // uncomment for sorting stress test 138 | // var x: usize = 4; 139 | // while (x < 512) : (x += 12) { 140 | // var y: usize = 4; 141 | // while (y < 512) : (y += 12) { 142 | // _ = blocks.append(.{ .x = @intToFloat(f32, x), .y = @intToFloat(f32, y) }) catch unreachable; 143 | // } 144 | // } 145 | } 146 | 147 | fn shutdown() !void { 148 | map.deinit(); 149 | block.deinit(); 150 | mode7_shader.deinit(); 151 | blocks.deinit(); 152 | camera.deinit(); 153 | } 154 | 155 | fn update() !void { 156 | const move_speed = 140.0; 157 | if (gk.input.keyDown(.w)) { 158 | camera.x += std.math.cos(camera.r) * move_speed * gk.time.rawDeltaTime(); 159 | camera.y += std.math.sin(camera.r) * move_speed * gk.time.rawDeltaTime(); 160 | } else if (gk.input.keyDown(.s)) { 161 | camera.x = camera.x - std.math.cos(camera.r) * move_speed * gk.time.rawDeltaTime(); 162 | camera.y = camera.y - std.math.sin(camera.r) * move_speed * gk.time.rawDeltaTime(); 163 | } 164 | 165 | if (gk.input.keyDown(.a)) { 166 | camera.x += std.math.cos(camera.r - std.math.pi / 2.0) * move_speed * gk.time.rawDeltaTime(); 167 | camera.y += std.math.sin(camera.r - std.math.pi / 2.0) * move_speed * gk.time.rawDeltaTime(); 168 | } else if (gk.input.keyDown(.d)) { 169 | camera.x += std.math.cos(camera.r + std.math.pi / 2.0) * move_speed * gk.time.rawDeltaTime(); 170 | camera.y += std.math.sin(camera.r + std.math.pi / 2.0) * move_speed * gk.time.rawDeltaTime(); 171 | } 172 | 173 | if (gk.input.keyDown(.i)) { 174 | camera.f += gk.time.rawDeltaTime(); 175 | } else if (gk.input.keyDown(.o)) { 176 | camera.f -= gk.time.rawDeltaTime(); 177 | } 178 | 179 | if (gk.input.keyDown(.k)) { 180 | camera.o += gk.time.rawDeltaTime(); 181 | } else if (gk.input.keyDown(.l)) { 182 | camera.o -= gk.time.rawDeltaTime(); 183 | } 184 | 185 | if (gk.input.keyDown(.minus)) { 186 | camera.z += gk.time.rawDeltaTime() * 10; 187 | } else if (gk.input.keyDown(.equals)) { 188 | camera.z -= gk.time.rawDeltaTime() * 10; 189 | } 190 | 191 | if (gk.input.keyDown(.q)) { 192 | camera.setRotation(@mod(camera.r, std.math.tau) - gk.time.rawDeltaTime()); 193 | } else if (gk.input.keyDown(.e)) { 194 | camera.setRotation(@mod(camera.r, std.math.tau) + gk.time.rawDeltaTime()); 195 | } 196 | 197 | if (gk.input.mousePressed(.left)) { 198 | var pos = camera.toWorld(gk.input.mousePos()); 199 | _ = blocks.append(pos) catch unreachable; 200 | } 201 | 202 | if (gk.input.mousePressed(.right)) { 203 | wrap = if (wrap == 0) 1 else 0; 204 | } 205 | } 206 | 207 | fn render() !void { 208 | // bind our mode7 shader, draw the plane which will then unset the shader for regular sprite drawing 209 | updateMode7Uniforms(); 210 | gfx.beginPass(.{ .shader = &mode7_shader.shader }); 211 | drawPlane(); 212 | 213 | var pos = camera.toScreen(camera.toWorld(gk.input.mousePos())); 214 | gfx.draw.circle(.{ .x = pos.x, .y = pos.y }, pos.size, 2, 8, gk.math.Color.white); 215 | gfx.draw.texScaleOrigin(block, pos.x, pos.y, pos.size, block.width / 2, block.height); 216 | 217 | for (blocks.items) |b| camera.placeSprite(block, b, 8); 218 | camera.renderSprites(); 219 | 220 | gfx.draw.text("WASD to move", 5, 20, null); 221 | gfx.draw.text("i/o to change fov", 5, 40, null); 222 | gfx.draw.text("k/l to change offset", 5, 60, null); 223 | gfx.draw.text("-/= to change z pos", 5, 80, null); 224 | gfx.draw.text("q/e to rotate cam", 5, 100, null); 225 | gfx.draw.text("left click to place block", 5, 120, null); 226 | gfx.draw.text("right click to toggle wrap", 5, 140, null); 227 | 228 | gfx.endPass(); 229 | } 230 | 231 | fn updateMode7Uniforms() void { 232 | mode7_shader.frag_uniform.mapw = map.width; 233 | mode7_shader.frag_uniform.maph = map.height; 234 | mode7_shader.frag_uniform.x = camera.x; 235 | mode7_shader.frag_uniform.y = camera.y; 236 | mode7_shader.frag_uniform.zoom = camera.z; 237 | mode7_shader.frag_uniform.fov = camera.f; 238 | mode7_shader.frag_uniform.offset = camera.o; 239 | mode7_shader.frag_uniform.wrap = wrap; 240 | mode7_shader.frag_uniform.x1 = camera.x1; 241 | mode7_shader.frag_uniform.y1 = camera.y1; 242 | mode7_shader.frag_uniform.x2 = camera.x2; 243 | mode7_shader.frag_uniform.y2 = camera.y2; 244 | } 245 | 246 | fn drawPlane() void { 247 | // bind our map to the second texture slot and we need a full screen render for the shader so we just draw a full screen rect 248 | gfx.draw.bindTexture(map, 1); 249 | const drawable_size = gk.window.size(); 250 | gfx.draw.rect(.{}, @intToFloat(f32, drawable_size.w), @intToFloat(f32, drawable_size.h), math.Color.white); 251 | gfx.setShader(null); 252 | } 253 | -------------------------------------------------------------------------------- /gamekit/imgui/events.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const imgui = @import("imgui"); 3 | const sdl = @import("sdl"); 4 | const gk = @import("../gamekit.zig"); 5 | 6 | pub const Events = struct { 7 | mouse_cursors: [imgui.ImGuiMouseCursor_COUNT]?*sdl.SDL_Cursor = undefined, 8 | mouse_button_state: [4]bool = undefined, 9 | global_time: u64 = 0, 10 | 11 | var clipboard_text: [*c]u8 = null; 12 | 13 | pub fn init() Events { 14 | var io = imgui.igGetIO(); 15 | io.BackendFlags |= imgui.ImGuiBackendFlags_HasMouseCursors; 16 | io.BackendFlags |= imgui.ImGuiBackendFlags_HasSetMousePos; 17 | 18 | io.KeyMap[imgui.ImGuiKey_Tab] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_TAB); 19 | io.KeyMap[imgui.ImGuiKey_LeftArrow] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_LEFT); 20 | io.KeyMap[imgui.ImGuiKey_RightArrow] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_RIGHT); 21 | io.KeyMap[imgui.ImGuiKey_UpArrow] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_UP); 22 | io.KeyMap[imgui.ImGuiKey_DownArrow] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_DOWN); 23 | io.KeyMap[imgui.ImGuiKey_PageUp] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_PAGEUP); 24 | io.KeyMap[imgui.ImGuiKey_PageDown] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_PAGEDOWN); 25 | io.KeyMap[imgui.ImGuiKey_Home] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_HOME); 26 | io.KeyMap[imgui.ImGuiKey_End] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_END); 27 | io.KeyMap[imgui.ImGuiKey_Insert] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_INSERT); 28 | io.KeyMap[imgui.ImGuiKey_Delete] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_DELETE); 29 | io.KeyMap[imgui.ImGuiKey_Backspace] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_BACKSPACE); 30 | io.KeyMap[imgui.ImGuiKey_Space] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_SPACE); 31 | io.KeyMap[imgui.ImGuiKey_Enter] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_RETURN); 32 | io.KeyMap[imgui.ImGuiKey_Escape] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_ESCAPE); 33 | io.KeyMap[imgui.ImGuiKey_KeyPadEnter] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_RETURN2); 34 | io.KeyMap[imgui.ImGuiKey_A] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_A); 35 | io.KeyMap[imgui.ImGuiKey_C] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_C); 36 | io.KeyMap[imgui.ImGuiKey_V] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_V); 37 | io.KeyMap[imgui.ImGuiKey_X] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_X); 38 | io.KeyMap[imgui.ImGuiKey_Y] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_Y); 39 | io.KeyMap[imgui.ImGuiKey_Z] = @enumToInt(sdl.SDL_Scancode.SDL_SCANCODE_Z); 40 | 41 | io.SetClipboardTextFn = setClipboardTextFn; 42 | io.GetClipboardTextFn = getClipboardTextFn; 43 | 44 | var self = Events{}; 45 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_Arrow)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_ARROW); 46 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_TextInput)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_IBEAM); 47 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_ResizeAll)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEALL); 48 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_ResizeNS)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENS); 49 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_ResizeEW)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEWE); 50 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_ResizeNESW)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENESW); 51 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_ResizeNWSE)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENWSE); 52 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_Hand)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_HAND); 53 | self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_NotAllowed)] = sdl.SDL_CreateSystemCursor(sdl.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NO); 54 | 55 | // TODO: ImGui_ImplSDL2_UpdateMonitors 56 | 57 | // TODO: ImGui_ImplSDL2_InitPlatformInterface 58 | if ((io.ConfigFlags & imgui.ImGuiConfigFlags_ViewportsEnable) != 0 and (io.BackendFlags & imgui.ImGuiBackendFlags_PlatformHasViewports) != 0) { 59 | // var main_vp = imgui.igGetMainViewport(); 60 | // main_vp.PlatformHandle = window; 61 | //init_platform_interface(window); 62 | } 63 | 64 | return self; 65 | } 66 | 67 | pub fn deinit(self: Events) void { 68 | if (clipboard_text) |txt| sdl.SDL_free(txt); 69 | 70 | // Destroy SDL mouse cursors 71 | for (self.mouse_cursors) |cursor| { 72 | sdl.SDL_FreeCursor(cursor); 73 | } 74 | } 75 | 76 | fn getClipboardTextFn(ctx: ?*anyopaque) callconv(.C) [*c]const u8 { 77 | _ = ctx; 78 | if (clipboard_text) |txt| { 79 | sdl.SDL_free(txt); 80 | clipboard_text = null; 81 | } 82 | clipboard_text = sdl.SDL_GetClipboardText(); 83 | return clipboard_text; 84 | } 85 | 86 | fn setClipboardTextFn(ctx: ?*anyopaque, text: [*c]const u8) callconv(.C) void { 87 | _ = ctx; 88 | _ = sdl.SDL_SetClipboardText(text); 89 | } 90 | 91 | pub fn newFrame( 92 | self: *Events, 93 | window: *sdl.SDL_Window, 94 | ) void { 95 | var win_size = gk.window.size(); 96 | var drawable_size = gk.window.drawableSize(); 97 | 98 | const io = imgui.igGetIO(); 99 | io.DisplaySize = imgui.ImVec2{ .x = @intToFloat(f32, win_size.w), .y = @intToFloat(f32, win_size.h) }; 100 | 101 | if (win_size.w > 0 and win_size.h > 0) { 102 | io.DisplayFramebufferScale = imgui.ImVec2{ 103 | .x = @intToFloat(f32, drawable_size.w) / @intToFloat(f32, win_size.w), 104 | .y = @intToFloat(f32, drawable_size.h) / @intToFloat(f32, win_size.h), 105 | }; 106 | } 107 | 108 | const frequency = sdl.SDL_GetPerformanceFrequency(); 109 | const current_time = sdl.SDL_GetPerformanceCounter(); 110 | io.DeltaTime = if (self.global_time > 0) @floatCast(f32, (@intToFloat(f64, current_time - self.global_time)) / @intToFloat(f64, frequency)) else @as(f32, 1 / 60); 111 | self.global_time = current_time; 112 | 113 | // ImGui_ImplSDL2_UpdateMousePosAndButtons 114 | if (io.WantSetMousePos) { 115 | if ((io.ConfigFlags & imgui.ImGuiConfigFlags_ViewportsEnable) != 0) { 116 | _ = sdl.SDL_WarpMouseGlobal(@floatToInt(c_int, io.MousePos.x), @floatToInt(c_int, io.MousePos.y)); 117 | } else { 118 | _ = sdl.SDL_WarpMouseInWindow(window, @floatToInt(c_int, io.MousePos.x), @floatToInt(c_int, io.MousePos.y)); 119 | } 120 | } 121 | 122 | // Set Dear ImGui mouse pos from OS mouse pos + get buttons. (this is the common behavior) 123 | var mouse_x_local: c_int = undefined; 124 | var mouse_y_local: c_int = undefined; 125 | const mouse_buttons = sdl.SDL_GetMouseState(&mouse_x_local, &mouse_y_local); 126 | io.MouseDown[0] = self.mouse_button_state[0] or sdlButton(mouse_buttons, 1); 127 | io.MouseDown[1] = self.mouse_button_state[1] or sdlButton(mouse_buttons, 3); 128 | io.MouseDown[2] = self.mouse_button_state[2] or sdlButton(mouse_buttons, 2); 129 | 130 | self.mouse_button_state[0] = false; 131 | self.mouse_button_state[1] = false; 132 | self.mouse_button_state[2] = false; 133 | 134 | var mouse_x_global: c_int = undefined; 135 | var mouse_y_global: c_int = undefined; 136 | _ = sdl.SDL_GetGlobalMouseState(&mouse_x_global, &mouse_y_global); 137 | 138 | if (io.ConfigFlags & imgui.ImGuiConfigFlags_ViewportsEnable != 0) { 139 | std.log.warn("viewports not implemented\n", .{}); 140 | } else if (sdl.SDL_GetWindowFlags(window) | @intCast(u32, @enumToInt(sdl.SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS)) != 1) { 141 | var win_x: i32 = undefined; 142 | var win_y: i32 = undefined; 143 | sdl.SDL_GetWindowPosition(window, &win_x, &win_y); 144 | io.MousePos = imgui.ImVec2{ 145 | .x = @intToFloat(f32, mouse_x_global - win_x), 146 | .y = @intToFloat(f32, mouse_y_global - win_y), 147 | }; 148 | } 149 | 150 | // ImGui_ImplSDL2_UpdateMouseCursor 151 | if (io.ConfigFlags & imgui.ImGuiConfigFlags_NoMouseCursorChange == 0) { 152 | const cursor = imgui.igGetMouseCursor(); 153 | if (io.MouseDrawCursor or cursor == imgui.ImGuiMouseCursor_None) { 154 | _ = sdl.SDL_ShowCursor(sdl.SDL_FALSE); 155 | } else { 156 | sdl.SDL_SetCursor(self.mouse_cursors[@intCast(usize, cursor)] orelse self.mouse_cursors[@intCast(usize, imgui.ImGuiMouseCursor_Arrow)].?); 157 | _ = sdl.SDL_ShowCursor(sdl.SDL_TRUE); 158 | } 159 | } 160 | 161 | // TODO: ImGui_ImplSDL2_UpdateGamepads 162 | } 163 | 164 | // Mimics the SDL_BUTTON macro and does the button down check 165 | fn sdlButton(mouse_buttons: u32, comptime button: u32) bool { 166 | return mouse_buttons & (1 << (button - 1)) != 0; 167 | } 168 | 169 | pub fn handleEvent(self: *Events, event: *sdl.SDL_Event) bool { 170 | switch (event.type) { 171 | sdl.SDL_MOUSEWHEEL => { 172 | const io = imgui.igGetIO(); 173 | if (event.wheel.x > 0) io.MouseWheelH -= 1; 174 | if (event.wheel.x < 0) io.MouseWheelH += 1; 175 | if (event.wheel.y > 0) io.MouseWheel += 1; 176 | if (event.wheel.y < 0) io.MouseWheel -= 1; 177 | return io.WantCaptureMouse; 178 | }, 179 | sdl.SDL_MOUSEBUTTONDOWN => { 180 | const io = imgui.igGetIO(); 181 | if (event.button.button == 1) self.mouse_button_state[0] = true; 182 | if (event.button.button == 2) self.mouse_button_state[1] = true; 183 | if (event.button.button == 3) self.mouse_button_state[2] = true; 184 | return io.WantCaptureMouse; 185 | }, 186 | sdl.SDL_TEXTINPUT => { 187 | const io = imgui.igGetIO(); 188 | imgui.ImGuiIO_AddInputCharactersUTF8(io, &event.text.text[0]); 189 | return io.WantCaptureKeyboard; 190 | }, 191 | sdl.SDL_KEYDOWN, sdl.SDL_KEYUP => { 192 | const io = imgui.igGetIO(); 193 | const mod_state = @enumToInt(sdl.SDL_GetModState()); 194 | io.KeysDown[@intCast(usize, @enumToInt(event.key.keysym.scancode))] = event.type == sdl.SDL_KEYDOWN; 195 | io.KeyShift = (mod_state & @enumToInt(sdl.SDL_Keymod.KMOD_SHIFT)) != 0; 196 | io.KeyCtrl = (mod_state & @enumToInt(sdl.SDL_Keymod.KMOD_CTRL)) != 0; 197 | io.KeyAlt = (mod_state & @enumToInt(sdl.SDL_Keymod.KMOD_ALT)) != 0; 198 | if (@import("builtin").target.os.tag == .windows) io.KeySuper = false else io.KeySuper = (mod_state & @enumToInt(sdl.SDL_Keymod.KMOD_GUI)) != 0; 199 | return io.WantCaptureKeyboard; 200 | }, 201 | sdl.SDL_WINDOWEVENT => { 202 | // TODO: should this return true? 203 | const event_type = @intToEnum(sdl.SDL_WindowEventID, event.window.event); 204 | if (event_type == .SDL_WINDOWEVENT_CLOSE or event_type == .SDL_WINDOWEVENT_MOVED or event_type == .SDL_WINDOWEVENT_RESIZED) { 205 | if (imgui.igFindViewportByPlatformHandle(sdl.SDL_GetWindowFromID(event.window.windowID))) |viewport| { 206 | if (event_type == .SDL_WINDOWEVENT_CLOSE) viewport.PlatformRequestClose = true; 207 | if (event_type == .SDL_WINDOWEVENT_MOVED) viewport.PlatformRequestMove = true; 208 | if (event_type == .SDL_WINDOWEVENT_RESIZED) viewport.PlatformRequestResize = true; 209 | } 210 | } 211 | }, 212 | else => {}, 213 | } 214 | return false; 215 | } 216 | }; 217 | --------------------------------------------------------------------------------