From 4a21ff6ef1559d116c3ca59cfb64bd920277a0d0 Mon Sep 17 00:00:00 2001 From: Ivory Date: Tue, 3 Sep 2024 13:13:28 -0400 Subject: [PATCH] noise, cameras, logging, idfk --- build.zig | 6 + open-simplex/include/OpenSimplex2F.h | 104 +++++++++ .../src/OpenSimplex2F.c | 8 +- src/Camera.zig | 67 ++++++ src/Color.zig | 67 +++++- src/Engine.zig | 32 ++- src/Entity.zig | 96 +++++++-- src/Noise.zig | 171 +++++++++++++++ src/Pawn.zig | 52 +++++ src/Scene.zig | 66 ++++-- src/Sprite.zig | 4 +- src/Tag.zig | 47 +++++ src/Terrain.zig | 199 ++++++++++++++++-- src/Texture.zig | 2 +- src/assets.zig | 68 +++++- src/geometry/Matrix4f.zig | 2 +- src/geometry/Rectf.zig | 4 +- src/geometry/Recti.zig | 22 +- src/geometry/Vec2f.zig | 14 +- src/geometry/Vec2i.zig | 27 ++- src/scenes.zig | 7 +- src/shaders.zig | 11 +- textures.png | Bin 36967 -> 47725 bytes 23 files changed, 969 insertions(+), 107 deletions(-) create mode 100644 open-simplex/include/OpenSimplex2F.h rename src/noise.c => open-simplex/src/OpenSimplex2F.c (99%) create mode 100644 src/Camera.zig create mode 100644 src/Noise.zig create mode 100644 src/Pawn.zig create mode 100644 src/Tag.zig diff --git a/build.zig b/build.zig index e0f3b7c..37c67b7 100644 --- a/build.zig +++ b/build.zig @@ -54,6 +54,12 @@ pub fn build(b: *std.Build) void { .file = b.path(glad_folder ++ "/src/glad.c") }); exe.addIncludePath(b.path(glad_folder ++ "/include")); + + exe.addCSourceFile(.{ + .file = b.path("open-simplex/src/OpenSimplex2F.c") + }); + exe.addIncludePath(b.path("open-simplex/include")); + exe.linkLibC(); exe.linkSystemLibrary("glfw"); exe.linkSystemLibrary("GL"); diff --git a/open-simplex/include/OpenSimplex2F.h b/open-simplex/include/OpenSimplex2F.h new file mode 100644 index 0000000..1ade4c9 --- /dev/null +++ b/open-simplex/include/OpenSimplex2F.h @@ -0,0 +1,104 @@ +#ifndef OpenSimplex2F_h__ +#define OpenSimplex2F_h__ + + + +/** + * K.jpg's OpenSimplex 2, faster variant + * + * - 2D is standard simplex implemented using a lookup table. + * - 3D is "Re-oriented 4-point BCC noise" which constructs a + * congruent BCC lattice in a much different way than usual. + * - 4D constructs the lattice as a union of five copies of its + * reciprocal. It successively finds the closest point on each. + * + * Multiple versions of each function are provided. See the + * documentation above each, for more info. + * + * Ported from Java to C by Stephen M. Cameron + * + */ + +#include + +struct OpenSimplex2F_context { + int16_t *perm; + struct Grad2 *permGrad2; + struct Grad3 *permGrad3; + struct Grad4 *permGrad4; +}; + +/* Allocate and initialize OpenSimplex2F context */ +int OpenSimplex2F(int64_t seed, struct OpenSimplex2F_context **ctx); + +/* Free OpenSimplex2F context */ +void OpenSimplex2F_free(struct OpenSimplex2F_context * ctx); + +/* Free singleton lattice point data */ +void OpenSimplex2F_shutdown(void); + +/** + * 2D Simplex noise, standard lattice orientation. + */ +double OpenSimplex2F_noise2(struct OpenSimplex2F_context *ctx, double x, double y); + +/** + * 2D Simplex noise, with Y pointing down the main diagonal. + * Might be better for a 2D sandbox style game, where Y is vertical. + * Probably slightly less optimal for heightmaps or continent maps. + */ +double OpenSimplex2F_noise2_XBeforeY(struct OpenSimplex2F_context *ctx, double x, double y); + +/** + * 3D Re-oriented 4-point BCC noise, classic orientation. + * Proper substitute for 3D Simplex in light of Forbidden Formulae. + * Use noise3_XYBeforeZ or noise3_XZBeforeY instead, wherever appropriate. + */ +double OpenSimplex2F_noise3_Classic(struct OpenSimplex2F_context *ctx, double x, double y, double z); + +/** + * 3D Re-oriented 4-point BCC noise, with better visual isotropy in (X, Y). + * Recommended for 3D terrain and time-varied animations. + * The Z coordinate should always be the "different" coordinate in your use case. + * If Y is vertical in world coordinates, call noise3_XYBeforeZ(x, z, Y) or use noise3_XZBeforeY. + * If Z is vertical in world coordinates, call noise3_XYBeforeZ(x, y, Z). + * For a time varied animation, call noise3_XYBeforeZ(x, y, T). + */ +double OpenSimplex2F_noise3_XYBeforeZ(struct OpenSimplex2F_context *ctx, double x, double y, double z); + +/** + * 3D Re-oriented 4-point BCC noise, with better visual isotropy in (X, Z). + * Recommended for 3D terrain and time-varied animations. + * The Y coordinate should always be the "different" coordinate in your use case. + * If Y is vertical in world coordinates, call noise3_XZBeforeY(x, Y, z). + * If Z is vertical in world coordinates, call noise3_XZBeforeY(x, Z, y) or use noise3_XYBeforeZ. + * For a time varied animation, call noise3_XZBeforeY(x, T, y) or use noise3_XYBeforeZ. + */ +double OpenSimplex2F_noise3_XZBeforeY(struct OpenSimplex2F_context *ctx, double x, double y, double z); + +/** + * 4D OpenSimplex2F noise, classic lattice orientation. + */ +double OpenSimplex2F_noise4_Classic(struct OpenSimplex2F_context *ctx, double x, double y, double z, double w); + +/** + * 4D OpenSimplex2F noise, with XY and ZW forming orthogonal triangular-based planes. + * Recommended for 3D terrain, where X and Y (or Z and W) are horizontal. + * Recommended for noise(x, y, sin(time), cos(time)) trick. + */ +double OpenSimplex2F_noise4_XYBeforeZW(struct OpenSimplex2F_context *ctx, double x, double y, double z, double w); + +/** + * 4D OpenSimplex2F noise, with XZ and YW forming orthogonal triangular-based planes. + * Recommended for 3D terrain, where X and Z (or Y and W) are horizontal. + */ +double OpenSimplex2F_noise4_XZBeforeYW(struct OpenSimplex2F_context *ctx, double x, double y, double z, double w); + +/** + * 4D OpenSimplex2F noise, with XYZ oriented like noise3_Classic, + * and W for an extra degree of freedom. W repeats eventually. + * Recommended for time-varied animations which texture a 3D object (W=time) + */ +double OpenSimplex2F_noise4_XYZBeforeW(struct OpenSimplex2F_context *ctx, double x, double y, double z, double w); +#endif + diff --git a/src/noise.c b/open-simplex/src/OpenSimplex2F.c similarity index 99% rename from src/noise.c rename to open-simplex/src/OpenSimplex2F.c index ca56e09..5a24abd 100644 --- a/src/noise.c +++ b/open-simplex/src/OpenSimplex2F.c @@ -137,13 +137,6 @@ static struct LatticePoint4D *new_LatticePoint4D(int xsv, int ysv, int zsv, int return this; } -struct OpenSimplex2F_context { - int16_t *perm; - struct Grad2 *permGrad2; - struct Grad3 *permGrad3; - struct Grad4 *permGrad4; -}; - #define ARRAYSIZE(x) (sizeof(x) / sizeof((x)[0])) static struct Grad2 GRADIENTS_2D[PSIZE]; @@ -918,3 +911,4 @@ double OpenSimplex2F_noise4_XYZBeforeW(struct OpenSimplex2F_context *ctx, double return noise4_Base(ctx, xs, ys, zs, ws); } + diff --git a/src/Camera.zig b/src/Camera.zig new file mode 100644 index 0000000..88c28df --- /dev/null +++ b/src/Camera.zig @@ -0,0 +1,67 @@ +const Camera = @This(); +pub const TAG = @import("Tag.zig").create(Camera); + +const std = @import("std"); +const Entity = @import("Entity.zig"); +const Scene = @import("Scene.zig"); +const Engine = @import("Engine.zig"); +const Sprite = @import("Sprite.zig"); +const Layer = @import("Layer.zig"); +const Color = @import("Color.zig"); +const Vec2i = @import("geometry/Vec2i.zig"); +const Vec2f = @import("geometry/Vec2f.zig"); +const Recti = @import("geometry/Recti.zig"); + +// focus: Vec2i = Vec2i.EAST.scale(100), +focus: Vec2f = Vec2f.ZERO, +tile_size: i32 = 16, +window_size_offset_x: i32 = 0, +window_size_offset_y: i32 = 0, + +pub fn create() !*Camera { + const self = Camera {}; + return try Scene.allocate(self); +} + +pub fn resize(self: *Camera, width: i32, height: i32) void { + std.debug.print("[Camera:resize] {d} x {d}\n", .{ width, height }); + self.window_size_offset_x = @divFloor(width, 2); + self.window_size_offset_y = @divFloor(height, 2); +} + +pub fn destroy(self: *const Camera) void { + Scene.deallocate(self); +} + +pub fn entity(self: *Camera) Entity { + return Entity.init(self, .{ + .tag = TAG + }); +} + +inline fn world_to_screen_vec2i(self: *const Camera, coords: Vec2i) Vec2i { + const tile_size_f: f32 = @floatFromInt(self.tile_size); + const focus_x_screen: i32 = @intFromFloat(self.focus.x * tile_size_f); + const focus_y_screen: i32 = @intFromFloat(self.focus.y * tile_size_f); + + return Vec2i.create( + self.tile_size * coords.x - focus_x_screen + self.window_size_offset_x, + self.tile_size * coords.y - focus_y_screen + self.window_size_offset_y + ); +} + +inline fn world_to_screen_recti(self: *const Camera, box: Recti) Recti { + return Recti.from_ab( + self.world_to_screen_vec2i(box.a), + self.world_to_screen_vec2i(box.b), + ); +} + +pub fn draw_sprite_i(self: *const Camera, sprite: *const Sprite, world_pos: Recti, layer: Layer, color: Color) void { + const screen_pos = self.world_to_screen_recti(world_pos); + sprite.draw(screen_pos, layer, color); +} + +pub fn set_focus(self: *Camera, f: Vec2f) void { + self.focus = f; +} diff --git a/src/Color.zig b/src/Color.zig index 225ec68..0d55cb3 100644 --- a/src/Color.zig +++ b/src/Color.zig @@ -1,4 +1,4 @@ - +const std = @import("std"); const Color = @This(); r: f32, @@ -6,6 +6,16 @@ g: f32, b: f32, a: f32, +pub fn blend(a: *const Color, b: *const Color, t: f32) Color { + const real_t = 1 - (if (t < 0.0) 0.0 else if (t > 1.0) 1.0 else t); + return Color.rgba( + a.r + (b.r - a.r) * real_t, + a.g + (b.g - a.g) * real_t, + a.b + (b.b - a.b) * real_t, + a.a + (b.a - a.a) * real_t + ); +} + pub fn rgba(r: f32, g: f32, b: f32, a: f32) Color { return .{ .r = r, @@ -15,6 +25,59 @@ pub fn rgba(r: f32, g: f32, b: f32, a: f32) Color { }; } +pub fn with_opacity(self: *const Color, a: f32) Color { + return .{ + .r = self.r, + .g = self.g, + .b = self.b, + .a = a + }; +} + +// r: 0 - 360 +// g: 0 - 1 +// b: 0 - 1 +// a: 0 - 1 +// no idea how it REALLY works, i dont know color theory but chatgpt does +pub fn hsla(h: f32, s: f32, l: f32, a: f32) Color { + const c = (1.0 - @abs(2.0 * l - 1.0)) * s; + const one = (h / 60.0); + const x = c * (1.0 - @abs(@rem(one, 2.0) - 1.0)); + const m = l - c / 2.0; + + var r: f32 = 0.0; + var g: f32 = 0.0; + var b: f32 = 0.0; + + if (h < 60.0) { + r = c; + g = x; + b = 0.0; + } else if (h < 120.0) { + r = x; + g = c; + b = 0.0; + } else if (h < 180.0) { + r = 0.0; + g = c; + b = x; + } else if (h < 240.0) { + r = 0.0; + g = x; + b = c; + } else if (h < 300.0) { + r = x; + g = 0.0; + b = c; + } else { + r = c; + g = 0.0; + b = x; + } + + return Color.rgba(r + m, g + m, b + m, a); +} + pub const WHITE = rgba(1, 1, 1, 1); pub const RED = rgba(1, 0, 0, 1); pub const ORANGE = rgba(1, 0.5, 0, 1); @@ -28,3 +91,5 @@ pub const BLUE = rgba(0, 0, 1, 1); pub const INDIGO = rgba(0.5, 0, 1, 1); pub const MAGENTA = rgba(1, 0, 1, 1); pub const HOT_PINK = rgba(1, 0, 0.5, 1); + +pub const BROWN = rgba(0.4, 0.25, 0.1, 1); diff --git a/src/Engine.zig b/src/Engine.zig index 2a38801..9129ef4 100644 --- a/src/Engine.zig +++ b/src/Engine.zig @@ -15,14 +15,16 @@ const c = @cImport({ @cInclude("glad/glad.h"); @cInclude("GLFW/glfw3.h"); }); -const HashMap = std.HashMap; +const AutoHashMap = std.AutoHashMap; +var store: AutoHashMap(*c.GLFWwindow, *Engine) = AutoHashMap(*c.GLFWwindow, *Engine).init(std.heap.page_allocator); // export const IGame = struct { // render: fn () void, // update: fn (f32) void, // }; current_scene: Scene, +projection: Matrix4f = undefined, pub fn create(initial_scene: Scene) Engine { return .{ @@ -35,9 +37,22 @@ fn errorCallback(err: c_int, desc_c: [*c]const u8) callconv(.C) void { std.log.err("glfw error {x:0>8}: {s}", .{ err, desc }); } +fn on_resize (window: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.C) void { + std.debug.print("[Engine:on_resize] {?}: {d} x {d}\n", .{ window, width, height }); + c.glViewport(0, 0, width, height); + const engine_instance = store.get(window.?).?; + engine_instance.set_matrices(width, height); + engine_instance.current_scene.resize(width, height); +} + +pub fn set_matrices(self: *Engine, width: i32, height: i32) void { + self.projection = Matrix4f.orthographic(0, @floatFromInt(width), @floatFromInt(height), 0, 0, 100); + shaders.set_projection_matrix(&self.projection); +} + // export fn run(game: *const IGame) !void { pub fn run(self: *Engine) !void { - + // in case of errors, set callback early! // for some reason this returns an error function as well?? _ = c.glfwSetErrorCallback(errorCallback); @@ -63,6 +78,8 @@ pub fn run(self: *Engine) !void { }; defer c.glfwDestroyWindow(window); + try store.put(window, self); + // load opengl into the window c.glfwMakeContextCurrent(window); if (c.gladLoadGL() != 1) { @@ -78,6 +95,8 @@ pub fn run(self: *Engine) !void { try assets.load(); + _ = c.glfwSetWindowSizeCallback(window, on_resize); + const clearBrightness: f32 = 0.09; c.glClearColor(clearBrightness, clearBrightness, clearBrightness, 1.0); @@ -92,16 +111,15 @@ pub fn run(self: *Engine) !void { var width: c_int = undefined; var height: c_int = undefined; c.glfwGetFramebufferSize(window, @ptrCast(&width), @ptrCast(&height)); - - const projection = Matrix4f.orthographic(0, @floatFromInt(width), @floatFromInt(height), 0, 0, 100); + std.debug.print("[Engine:run] window size {d} x {d}\n", .{ width, height }); + self.set_matrices(width, height); + try self.current_scene.start(); // run the main loop while (c.glfwWindowShouldClose(window) == 0) { - shaders.set_projection_matrix(&projection); c.glClear(c.GL_COLOR_BUFFER_BIT | c.GL_DEPTH_BUFFER_BIT); self.current_scene.draw(); - // game.render(); - // game.update(1.0); + assets.terrain[0][0].draw(Recti.from_xywh(-1000, -1000, 50000, 50000), Layer.FLOOR, Color.WHITE); c.glfwSwapBuffers(window); c.glfwPollEvents(); diff --git a/src/Entity.zig b/src/Entity.zig index 12ba754..89207a8 100644 --- a/src/Entity.zig +++ b/src/Entity.zig @@ -3,23 +3,29 @@ const Layer = @import("Layer.zig"); const Color = @import("Color.zig"); const Recti = @import("geometry/Recti.zig"); const std = @import("std"); +const Tag = @import("Tag.zig"); +const Scene = @import("Scene.zig"); const Entity = @This(); const EntityOptions = struct { - renders: bool = true, - updates: bool = true, - accepts_keyboard: bool = false, - accepts_mouse: bool = false, + tag: Tag = Tag.NONE, }; ptr: *anyopaque, -options: EntityOptions, -destroyFn: *const fn(*const anyopaque) void, -updateFn: *const fn(*anyopaque, f32) void, -drawFn: *const fn(*const anyopaque) void, +tag: Tag, +_destroy: *const fn(*const anyopaque) void, +_update: *const fn(*anyopaque, f32) void, +_draw: *const fn(*const anyopaque) void, +_draw_opacity: *const fn(*const anyopaque) void, +_start: *const fn(*anyopaque, *Scene) void, +_resize: *const fn(*anyopaque, i32, i32) void, pub fn init(ptr: anytype, options: EntityOptions) Entity { + std.debug.print("[Entity:init] creating entity {s} with tag \"{s}\"\n", .{ + @typeName(@typeInfo(@TypeOf(ptr)).Pointer.child), + options.tag.type + }); const MutablePointer = @TypeOf(ptr); // @compileLog("Compiling Entity type for " ++ @typeName(MutablePointer)); @@ -41,38 +47,86 @@ pub fn init(ptr: anytype, options: EntityOptions) Entity { // @compileLog("const pointer type: " ++ @typeName(ConstPointer)); const gen = struct { - pub fn destroyImpl(pointer: *const anyopaque) void { + pub fn _destroy(pointer: *const anyopaque) void { const self: ConstPointer = @ptrCast(@alignCast(pointer)); type_info.Pointer.child.destroy(self); } - pub fn updateImpl(pointer: *anyopaque, dt: f32) void { + pub fn _start(pointer: *anyopaque, scene: *Scene) void { const self: MutablePointer = @ptrCast(@alignCast(pointer)); - type_info.Pointer.child.update(self, dt); + if(@hasDecl(type_info.Pointer.child, "start")) { + type_info.Pointer.child.start(self, scene); + } } - pub fn drawImpl(pointer: *const anyopaque) void { + pub fn _resize(pointer: *anyopaque, width: i32, height: i32) void { + const self: MutablePointer = @ptrCast(@alignCast(pointer)); + if(@hasDecl(type_info.Pointer.child, "resize")) { + type_info.Pointer.child.resize(self, width, height); + } + } + pub fn _update(pointer: *anyopaque, dt: f32) void { + const self: MutablePointer = @ptrCast(@alignCast(pointer)); + if(@hasDecl(type_info.Pointer.child, "update")) { + type_info.Pointer.child.update(self, dt); + } + } + pub fn _draw(pointer: *const anyopaque) void { const self: ConstPointer = @ptrCast(@alignCast(pointer)); - type_info.Pointer.child.draw(self); + if(@hasDecl(type_info.Pointer.child, "draw")) { + type_info.Pointer.child.draw(self); + } + } + pub fn _draw_opacity(pointer: *const anyopaque) void { + const self: ConstPointer = @ptrCast(@alignCast(pointer)); + if(@hasDecl(type_info.Pointer.child, "draw_opacity")) { + type_info.Pointer.child.draw_opacity(self); + } } }; return .{ .ptr = ptr, - .options = options, - .destroyFn = gen.destroyImpl, - - .updateFn = gen.updateImpl, - .drawFn = gen.drawImpl, + .tag = options.tag, + ._destroy = gen._destroy, + ._start = gen._start, + ._resize = gen._resize, + ._update = gen._update, + ._draw = gen._draw, + ._draw_opacity = gen._draw_opacity, }; } pub fn update(self: *const Entity, dt: f32) void { - self.updateFn(self.ptr, dt); + self._update(self.ptr, dt); } pub fn draw(self: *const Entity) void { - self.drawFn(self.ptr); + self._draw(self.ptr); +} + +pub fn draw_opacity(self: *const Entity) void { + self._draw_opacity(self.ptr); } pub fn destroy(self: *const Entity) void { - self.destroyFn(self.ptr); + self._destroy(self.ptr); +} + +pub fn start(self: *const Entity, scene: *Scene) void { + self._start(self.ptr, scene); +} + +pub fn resize(self: *const Entity, width: i32, height: i32) void { + self._resize(self.ptr, width, height); +} + +pub fn allocate_array(comptime T: type, size: usize, default: T) ![]T { + const ptr = try std.heap.page_allocator.alloc(T, size); + for (0..size) |idx| { + ptr[idx] = default; + } + return ptr; +} + +pub fn free_array(ptr: anytype) void { + std.heap.page_allocator.free(ptr); } diff --git a/src/Noise.zig b/src/Noise.zig new file mode 100644 index 0000000..838d6bf --- /dev/null +++ b/src/Noise.zig @@ -0,0 +1,171 @@ +const c = @cImport({ + @cInclude("OpenSimplex2F.h"); +}); +const std = @import("std"); +const ArrayList = std.ArrayList; +const RealColor = @import("Color.zig"); + +const Noise = @This(); +const NoiseContext = c.OpenSimplex2F_context; +const NoiseContextPointer = ?*NoiseContext; + +octave_count: usize = 1, +scale: f32 = 1, +noise_context: NoiseContext = undefined, +noise_ptr: NoiseContextPointer = undefined, + +pub fn create(seed: i64, octaves: usize, scale: f32) Noise { + var self = Noise { + .octave_count = octaves, + .scale = scale, + }; + self.noise_context = NoiseContext {}; + self.noise_ptr = &self.noise_context; + _ = c.OpenSimplex2F(seed, @ptrCast(&self.noise_ptr)); + return self; +} + +pub fn get(self: *const Noise, x: f32, y: f32) f32 { + var total: f32 = 0; + var max: f32 = 0; + for (0..self.octave_count) |idx| { + // in a series / sum n = 1 -> inf + const n: f32 = @floatFromInt(idx + 1); + const amplitude: f32 = 1.0 / n; + // frequency is altered both by the scale of the while gen + // and the octave we're on. + const frequency: f32 = self.scale * n; + const sx = x * frequency + n; + const sy = y * frequency; + // -amplitude <= r <= amplitude + const r: f32 = @as(f32, @floatCast(c.OpenSimplex2F_noise2(self.noise_ptr, sx, sy))) * amplitude; + total += r; + max += amplitude; + } + const n: f32 = total / max; + return @min(@max((n + 1.0) / 2.0, 0), 1); +} + +pub fn destroy(self: *const Noise) void { + _ = c.OpenSimplex2F_free(self.noise_ptr); +} + +pub const Octave = struct { + frequency: f32, + amplitude: f32, +}; + +pub const Custom = struct { + octaves: ArrayList(Octave) = undefined, + context: NoiseContext = undefined, + context_ptr: NoiseContextPointer = undefined, + scale: f32 = 1, + + pub fn create(seed: i64, scale: f32, octaves: []const Octave) !Custom { + var self = Custom { + .scale = scale + }; + self.octaves = ArrayList(Octave).init(std.heap.page_allocator); + self.context = NoiseContext {}; + self.context_ptr = &self.context; + _ = c.OpenSimplex2F(seed, @ptrCast(&self.context_ptr)); + + for (octaves) |octave| { + try self.octaves.append(octave); + } + + return self; + } + + pub fn get(self: *const Custom, x: f32, y: f32) f32 { + var total: f32 = 0; + var max: f32 = 0; + for (self.octaves.items) |octave| { + // @compileLog(octave); + // octave + const sx = x * self.scale * octave.frequency; + const sy = y * self.scale * octave.frequency; + const r: f32 = @as(f32, @floatCast(c.OpenSimplex2F_noise2(self.context_ptr, sx, sy))) * octave.amplitude; + total += r; + max += octave.amplitude; + } + const n: f32 = total / max; + return @min(@max((n + 1.0) / 2.0, 0), 1); + } + + pub fn destroy(self: *const Custom) void { + _ = c.OpenSimplex2F_free(self.context_ptr); + self.octaves.deinit(); + } +}; + +pub const Color = struct { + noise_h: Noise.Custom, + noise_s: Noise.Custom, + noise_l: Noise.Custom, + range: ColorRange, + + const OCTAVES = [_]Octave{ + .{ .frequency = 1, .amplitude = 1.0 }, + .{ .frequency = 2, .amplitude = 0.5 }, + .{ .frequency = 4, .amplitude = 0.4 }, + .{ .frequency = 8, .amplitude = 0.3 }, + .{ .frequency = 16, .amplitude = 0.2 }, + .{ .frequency = 30, .amplitude = 1.0 }, + }; + + pub fn create(seed: i64, range: ColorRange) !Noise.Color { + return .{ + .noise_h = try Noise.Custom.create(seed + 0, 0.03, &OCTAVES), + .noise_s = try Noise.Custom.create(seed + 1, 0.03, &OCTAVES), + .noise_l = try Noise.Custom.create(seed + 2, 0.03, &OCTAVES), + .range = range, + }; + } + + pub fn get(self: *const Noise.Color, x: f32, y: f32) RealColor { + return RealColor.hsla( + self.noise_h.get(x, y) * (self.range.h[1] - self.range.h[0]) + self.range.h[0], + self.noise_s.get(x, y) * (self.range.s[1] - self.range.s[0]) + self.range.s[0], + self.noise_l.get(x, y) * (self.range.l[1] - self.range.l[0]) + self.range.l[0], + 1.0 + ); + } + + pub fn destroy(self: *const Noise.Color) void { + self.noise_h.destroy(); + self.noise_s.destroy(); + self.noise_l.destroy(); + } +}; + +pub const ColorRange = struct { + // min / max values + h: struct { f32, f32 }, + s: struct { f32, f32 }, + l: struct { f32, f32 }, + + pub const DIRT = Noise.ColorRange { + .h = .{ 30, 33 }, + .s = .{ 0.30, 0.45 }, + .l = .{ 0.12, 0.22 }, + }; + + pub const STONE = Noise.ColorRange { + .h = .{ 0, 360 }, + .s = .{ 0.00, 0.02 }, + .l = .{ 0.20, 0.30 }, + }; + + pub const GRASS = Noise.ColorRange { + .h = .{ 100, 110 }, + .s = .{ 0.72, 0.82 }, + .l = .{ 0.25, 0.40 }, + }; + + pub const GRASS_TUNDRA = Noise.ColorRange { + .h = .{ 115, 125 }, + .s = .{ 0.4, 0.5 }, + .l = .{ 0.2, 0.3 }, + }; +}; diff --git a/src/Pawn.zig b/src/Pawn.zig new file mode 100644 index 0000000..663d308 --- /dev/null +++ b/src/Pawn.zig @@ -0,0 +1,52 @@ +const Pawn = @This(); + +const std = @import("std"); +const Entity = @import("Entity.zig"); +const Scene = @import("Scene.zig"); +const Camera = @import("Camera.zig"); +const Terrain = @import("Terrain.zig"); +const Tag = @import("Tag.zig"); +const Vec2i = @import("geometry/Vec2i.zig"); +const assets = @import("assets.zig"); +const Layer = @import("Layer.zig"); +const Color = @import("Color.zig"); +var prng = std.rand.DefaultPrng.init(0); + +camera: *const Camera = undefined, +position: Vec2i = Vec2i.ZERO, + +pub fn random() !*Pawn { + var self: Pawn = Pawn {}; + self.position = Vec2i.create( + @intFromFloat(@floor(prng.random().float(f32) * @as(f32, @floatFromInt(Terrain.CHUNK_SIZE)))), + @intFromFloat(@floor(prng.random().float(f32) * @as(f32, @floatFromInt(Terrain.CHUNK_SIZE)))) + ); + return try Scene.allocate(self); +} + +pub fn create() !*Pawn { + const self: Pawn = Pawn {}; + return try Scene.allocate(self); +} + +pub fn start(self: *Pawn, scene: *const Scene) void { + self.camera = scene.get(Camera.TAG, Camera); +} + +pub fn destroy(self: *const Pawn) void { + Scene.deallocate(self); +} + +pub fn entity(self: *Pawn) Entity { + return Entity.init(self, .{}); +} + +// pub fn tile(self: *Pawn) Tile { +// return Tile.init(.{ +// .solid = true +// }); +// } + +pub fn draw(self: *const Pawn) void { + self.camera.draw_sprite_i(assets.pawn, self.position.to_unit_recti(), Layer.ENTITIES, Color.WHITE); +} diff --git a/src/Scene.zig b/src/Scene.zig index 6444789..ea56039 100644 --- a/src/Scene.zig +++ b/src/Scene.zig @@ -6,16 +6,19 @@ const Layer = @import("Layer.zig"); const Color = @import("Color.zig"); const Entity = @import("Entity.zig"); const Terrain = @import("Terrain.zig"); +const Tag = @import("Tag.zig"); const std = @import("std"); const ArrayList = std.ArrayList; +const AutoHashMap = std.AutoHashMap; -entities: ArrayList(Entity), +entities: ArrayList(Entity) = undefined, +tagged_entities: AutoHashMap(Tag.ID, *Entity) = undefined, pub fn create() Scene { - const e: ArrayList(Entity) = ArrayList(Entity).init(std.heap.page_allocator); - return .{ - .entities = e - }; + var self = Scene {}; + self.entities = ArrayList(Entity).init(std.heap.page_allocator); + self.tagged_entities = AutoHashMap(Tag.ID, *Entity).init(std.heap.page_allocator); + return self; } pub fn destroy(self: *Scene) void { @@ -26,11 +29,37 @@ pub fn destroy(self: *Scene) void { } pub fn start(self: *Scene) !void { - const terrain = try Terrain.create(); - try self.add(terrain); + std.debug.print("[Scene:start] Scene starting...\n", .{}); + std.debug.print("[Scene:start] entities: {}\n", .{ self.entities.items.len }); + std.debug.print("[Scene:start] tagged entities: {}\n", .{ self.tagged_entities.count() }); + for (self.entities.items) |entity| { + entity.start(self); + } } -// --- +pub fn get(self: *const Scene, tag: Tag, comptime T: type) *T { + const tag_id = tag.id; + const entity_ptr: *Entity = self.tagged_entities.get(tag_id) orelse @panic("Could not find entity"); + // @compileLog(entity_ptr); + const opaque_ptr = entity_ptr.ptr; + const real_ptr: *T = @ptrCast(@alignCast(opaque_ptr)); + return real_ptr; + // var iterator = self.tagged_entities.iterator(); + // while (iterator.next()) |entry| { + // const id_ptr: *Tag.ID = entry.key_ptr; + // const entity_ptr_ptr: **Entity = entry.value_ptr; + // const entity_ptr: *Entity = entity_ptr_ptr.*; + // + // const other_id = entity_ptr.tag.id; + // if (tag_id == other_id) { + // const opaque_ptr: *anyopaque = entity_ptr.ptr; + // const real_ptr: *T = @ptrCast(@alignCast(opaque_ptr)); + // // @compileLog(r); + // return real_ptr; + // } + // } + // @panic("Could not find entity"); +} pub fn add(self: *Scene, instance_ptr: anytype) !void { const instance_ptr_type = @TypeOf(instance_ptr); @@ -52,7 +81,14 @@ pub fn add(self: *Scene, instance_ptr: anytype) !void { if (!@hasDecl(instance_type, "entity")) @compileError("Pointer must be to a struct with fn entity() Entity"); - try self.entities.append(instance_ptr.entity()); + const entity: Entity = instance_ptr.entity(); + + const entity_ptr = try self.entities.addOne(); + entity_ptr.* = entity; + + if (entity_ptr.tag.id != Tag.NONE.id) { + try self.tagged_entities.put(entity.tag.id, entity_ptr); + } } pub fn update(self: *Scene, dt: f32) void { @@ -62,14 +98,18 @@ pub fn update(self: *Scene, dt: f32) void { } pub fn draw(self: *Scene) void { - for (self.entities.items) |entity| { entity.draw(); } + for (self.entities.items) |entity| { + entity.draw_opacity(); + } +} - assets.heart.draw(&Recti.from_xywh(120, 100, 32, 64), Layer.ENTITIES, Color.LIGHT_BLUE); - assets.heart.draw(&Recti.from_xywh(100, 100, 32, 64), Layer.FLOOR, Color.WHITE); - assets.heart.draw(&Recti.from_xywh(80, 100, 32, 64), Layer.ENTITIES, Color.INDIGO); +pub fn resize(self: *Scene, width: i32, height: i32) void { + for (self.entities.items) |entity| { + entity.resize(width, height); + } } // --- helpfer functions for entities themselves diff --git a/src/Sprite.zig b/src/Sprite.zig index 2432467..e8d196a 100644 --- a/src/Sprite.zig +++ b/src/Sprite.zig @@ -16,7 +16,7 @@ texture: *const Texture = undefined, pub fn create(tex: *const Texture, rect: Recti) Sprite { return .{ - .texture_area = rect.scalef(Vec2f.new( + .texture_area = rect.scalef(Vec2f.create( @floatFromInt(tex.width), @floatFromInt(tex.height) )), @@ -24,7 +24,7 @@ pub fn create(tex: *const Texture, rect: Recti) Sprite { }; } -pub fn draw(self: *const Sprite, screen_pos: *const Recti, layer: Layer, color: Color) void { +pub fn draw(self: *const Sprite, screen_pos: Recti, layer: Layer, color: Color) void { self.texture.bind(c.GL_TEXTURE0); c.glBegin(c.GL_QUADS); { diff --git a/src/Tag.zig b/src/Tag.zig new file mode 100644 index 0000000..68fa822 --- /dev/null +++ b/src/Tag.zig @@ -0,0 +1,47 @@ +const Tag = @This(); +const std = @import("std"); +pub const ID = *const anyopaque; + +id: ID, +type: []const u8, + +fn create_id_for_type(comptime T: type) ID { + // create a unique struct for each type this is called with. + const unique_struct = struct { + // this simply gives the struct a size, so the pointer is not + // optimized into oblivion. + var x: u8 = 0; + + // capture the type so zig knows not to make this + // struct the same for each call. if the type isnt captured + // zig will create one struct in memory for each unique call. + // ie: the function will be unique, but this struct wont be. + comptime { + _ = T; + } + }; + + // @compileLog(T); + // @compileLog(&unique_struct.x); + + // return the pointer to the only data within the struct, + // again so the compiler doesnt optimize this into non-existence. + return &unique_struct.x; +} + +pub const NONE = Tag { + .type = "None", + .id = create_id_for_type(struct {}), +}; + +pub fn create(comptime t: type) Tag { + // @compileLog(@typeName(t)); + return Tag { + .type = @typeName(t), + .id = create_id_for_type(t), + }; +} + +pub inline fn equals(self: *const Tag, other: Tag) bool { + return self.id == other.id; +} diff --git a/src/Terrain.zig b/src/Terrain.zig index ad97431..d20a17c 100644 --- a/src/Terrain.zig +++ b/src/Terrain.zig @@ -1,45 +1,206 @@ const Terrain = @This(); - const assets = @import("assets.zig"); const Layer = @import("Layer.zig"); const Color = @import("Color.zig"); const Recti = @import("geometry/Recti.zig"); +const Vec2f = @import("geometry/Vec2f.zig"); const Entity = @import("Entity.zig"); const Scene = @import("Scene.zig"); +const Camera = @import("Camera.zig"); const std = @import("std"); +const allocator = std.heap.page_allocator; var prng = std.rand.DefaultPrng.init(0); +const Noise = @import("Noise.zig"); -size: u16 = 24, +const TAG = @import("Tag.zig").create(Terrain); -pub fn entity(self: *Terrain) Entity { - return Entity.init(self, .{}); +pub const CHUNK_SIZE = 48; +const CHUNK_LENGTH = CHUNK_SIZE * CHUNK_SIZE; + +const tile_size: usize = 16; + +pub inline fn xy_to_index(x: i32, y: i32) i32 { + return x + y * CHUNK_SIZE; } -pub fn create() !*Terrain { - return try Scene.allocate(Terrain { - .size = 24, +pub inline fn index_to_xy(index: i32) struct { i32, i32 } { + const y: i32 = @divFloor(index, CHUNK_SIZE); + const x: i32 = @rem(index, CHUNK_SIZE); + return .{ x, y }; +} + +const GROWS = true; +const GROWTH_RATE: f32 = 0.5; + +pub const Tile = struct { + texture_idx: usize, + growth: f32, + stone_decoration: f32, + grass_color: Color, + dirt_color: Color, + stone_color: Color, + + pub const NULL = Tile { + .texture_idx = 0, + .growth = 0, + .stone_decoration = 0, + .grass_color = Color.WHITE, + .dirt_color = Color.WHITE, + .stone_color = Color.WHITE, + }; +}; + +tiles: []Tile = undefined, + +// the noise for deciding random textures +texture_noise: Noise, +// noise for deciding grass growth +growth_noise: Noise, +// noise for stone decoration placement +stone_decoration_noise: Noise.Custom, +// noise for colors +dirt_color_noise: Noise.Color, +grass_color_noise: Noise.Color, +stone_color_noise: Noise.Color, + + +camera: *Camera = undefined, + +pub fn entity(self: *Terrain) Entity { + return Entity.init(self, .{ + .tag = TAG, }); } +pub fn start(self: *Terrain, scene: *Scene) void { + self.camera = scene.get(Camera.TAG, Camera); + self.camera.set_focus(Vec2f.create(CHUNK_SIZE / 2, CHUNK_SIZE / 2)); +} + +fn contrast(n: f32, blend_strength: f32) f32 { + const pi_halves = std.math.pi / 2.0; + const two_piths = 2.0 / std.math.pi; + const true_blend = std.math.pow(f32, 1.2, blend_strength) - 1; + const numerator_qty = pi_halves * (2 * n - 1) * true_blend; + const numerator = two_piths * std.math.atan(numerator_qty); + const denominator = two_piths * std.math.atan(pi_halves * true_blend); + const unadjusted_total = numerator / denominator; + // const adjusted_total = (unadjusted_total + 1.0) / 2.0; + return (unadjusted_total + 1.0) / 2.0; +} + +fn generate_tiles(self: *Terrain) void { + for(0..CHUNK_LENGTH) |idx| { + const x, const y = index_to_xy(@intCast(idx)); + const fx: f32 = @floatFromInt(x); + const fy: f32 = @floatFromInt(y); + const texture_random = self.texture_noise.get(fx, fy); + self.tiles[idx] = Tile { + .texture_idx = @intFromFloat(@floor(texture_random * 4)), + .growth = contrast(self.growth_noise.get(fx, fy), 10), + .stone_decoration = self.stone_decoration_noise.get(fx, fy), + .dirt_color = self.dirt_color_noise.get(fx, fy), + .grass_color = self.grass_color_noise.get(fx, fy), + .stone_color = self.stone_color_noise.get(fx, fy), + }; + } +} + +pub fn create(seed: i64) !*Terrain { + var self: *Terrain = try Scene.allocate(Terrain { + .tiles = try Entity.allocate_array(Tile, CHUNK_LENGTH, Tile.NULL), + .growth_noise = Noise.create(seed, 10, 0.03), + .texture_noise = Noise.create(seed + 1, 1, 3), + .stone_decoration_noise = try Noise.Custom.create(seed + 2, 1, &.{ + .{ .frequency = 0.03, .amplitude = 1 }, + .{ .frequency = 1, .amplitude = 1 } + }), + .dirt_color_noise = try Noise.Color.create(seed + 3, Noise.ColorRange.DIRT), + .grass_color_noise = try Noise.Color.create(seed + 4, Noise.ColorRange.GRASS), + .stone_color_noise = try Noise.Color.create(seed + 5, Noise.ColorRange.STONE) + }); + + self.generate_tiles(); + return self; +} + pub fn destroy(self: *const Terrain) void { + self.texture_noise.destroy(); + self.growth_noise.destroy(); + self.stone_decoration_noise.destroy(); + + self.dirt_color_noise.destroy(); + self.grass_color_noise.destroy(); + self.stone_color_noise.destroy(); + + Entity.free_array(self.tiles); Scene.deallocate(self); } -pub fn update(self: *Terrain, _: f32) void { - if (prng.random().int(u8) == 34) { - self.size = self.size + 1; +fn chance(percent: f32) bool { + return prng.random().float(f32) < percent; +} + +fn grow(self: *Terrain, x: i32, y: i32, amt: f32) void { + if (x < 0 or x >= CHUNK_SIZE) return; + if (y < 0 or y >= CHUNK_SIZE) return; + const idx: usize = @intCast(xy_to_index(x, y)); + const tile = &self.tiles[idx]; + // @compileLog(tile); + tile.*.growth = @min(@max(tile.growth + amt, 0), 1); +} + +pub fn update(self: *Terrain, dt: f32) void { + if (GROWS) { + for (0..CHUNK_LENGTH) |idx| { + const x, const y = index_to_xy(@intCast(idx)); + const tile = self.tiles[idx]; + const should_grow = chance(dt * GROWTH_RATE) and chance(tile.growth); + if (should_grow) { + self.grow(x, y, 0.010); + + self.grow(x + 1, y, 0.008); + self.grow(x - 1, y, 0.008); + self.grow(x, y + 1, 0.008); + self.grow(x, y - 1, 0.008); + + self.grow(x + 1, y + 1, 0.004); + self.grow(x + 1, y - 1, 0.004); + self.grow(x - 1, y + 1, 0.004); + self.grow(x - 1, y - 1, 0.004); + } + } } } pub fn draw(self: *const Terrain) void { - for(0..self.size) |y| { - for(0..self.size) |x| { - const idx: usize = @intFromFloat(@floor(prng.random().float(f32) * 4)); - var grey = prng.random().float(f32); - grey /= 4.0; - grey += 3.0 / 4.0; - const rect = Recti.from_xywh(@intCast(100 + 16 * x), @intCast(100 + 16 * y), 16, 16); - assets.terrain[idx].draw(&rect, Layer.FLOOR, Color.rgba(grey, grey, grey, 1)); - } + for(0..CHUNK_LENGTH) |idx| { + const tile = self.tiles[idx]; + const x, const y = index_to_xy(@intCast(idx)); + const rect = Recti.from_xywh(x, y, 1, 1); + const sprite = assets.terrain[0][tile.texture_idx]; + self.camera.draw_sprite_i(sprite, rect, Layer.FLOOR, tile.grass_color); + // switch(tile.texture_idx) { + // 0 => self.camera.draw_sprite_i(sprite, rect, Layer.FLOOR, Color.WHITE), + // 1 => self.camera.draw_sprite_i(sprite, rect, Layer.FLOOR, Color.RED), + // 2 => self.camera.draw_sprite_i(sprite, rect, Layer.FLOOR, Color.GREEN), + // 3 => self.camera.draw_sprite_i(sprite, rect, Layer.FLOOR, Color.BLUE), + // else => {} + // } } } + +// pub fn draw_opacity(self: *const Terrain) void { +// for(0..CHUNK_LENGTH) |idx| { +// const tile = self.tiles[idx]; +// const x, const y = index_to_xy(@intCast(idx)); +// // const grassiness_max: f32 = @floatFromInt(assets.terrain.len + 1); +// // const grassiness: usize = @intFromFloat(@floor(tile.growth * grassiness_max)); +// // if (grassiness == 0) continue; +// // const grassiness_idx = assets.terrain.len - @min(4, grassiness); +// const rect = Recti.from_xywh(x, y, 1, 1); +// const sprite = assets.terrain[0][tile.texture_idx]; +// const color = tile.grass_color.with_opacity(tile.growth); +// self.camera.draw_sprite_i(sprite, rect, Layer.FLOOR, color); +// } +// } diff --git a/src/Texture.zig b/src/Texture.zig index fc2853c..c87f3af 100644 --- a/src/Texture.zig +++ b/src/Texture.zig @@ -59,7 +59,7 @@ pub fn create(path: []const u8) !Texture { c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_RGBA, @intCast(ihdr.width), @intCast(ihdr.height), 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, @ptrCast(out_buf)); c.glBindTexture(c.GL_TEXTURE_2D, 0); - std.debug.print("texture generated: ID={} Path={s}\n", .{ tex_id, path }); + std.debug.print("[Texture:create] texture generated: ID={} Path={s}\n", .{ tex_id, path }); return .{ .handle = @intCast(tex_id), diff --git a/src/assets.zig b/src/assets.zig index 54f9cad..25c79c9 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -6,27 +6,77 @@ const std = @import("std"); const Assets = struct { tree: Sprite, heart: Sprite, - terrain: [4]Sprite, + pawn: Sprite, + terrain: [4][4]Sprite, + rocks: [4]Sprite, }; var assets: Assets = undefined; var texture: Texture = undefined; +inline fn make8 (x: i32, y: i32, w: i32, h: i32) Sprite { + return Sprite.create(&texture, Recti.from_xywh(x * 8, y * 8, w * 8, h * 8)); +} + pub fn load() !void { + + // @compileLog(@TypeOf(terrain)); + texture = try Texture.create("textures.png"); assets = .{ - .tree = Sprite.create(&texture, Recti.from_xywh(8*27, 8*4, 8, 16)), - .heart = Sprite.create(&texture, Recti.from_xywh(8*27, 8*4, 8, 16)), - .terrain = [_]Sprite { - Sprite.create(&texture, Recti.from_xywh(8*3, 8*8, 8, 8)), - Sprite.create(&texture, Recti.from_xywh(8*3, 8*9, 8, 8)), - Sprite.create(&texture, Recti.from_xywh(8*4, 8*8, 8, 8)), - Sprite.create(&texture, Recti.from_xywh(8*4, 8*9, 8, 8)), + .tree = make8(27, 4, 1, 2), + .heart = make8(27, 4, 1, 2), + .pawn = make8(6, 11, 1, 1), + .terrain = [4][4]Sprite { + [4]Sprite { make8(18, 8, 1, 1), make8(19, 8, 1, 1), make8(18, 9, 1, 1), make8(19, 9, 1, 1) }, + [4]Sprite { make8(20, 8, 1, 1), make8(21, 8, 1, 1), make8(20, 9, 1, 1), make8(21, 9, 1, 1) }, + [4]Sprite { make8(18, 10, 1, 1), make8(19, 10, 1, 1), make8(18, 11, 1, 1), make8(19, 11, 1, 1) }, + [4]Sprite { make8(20, 10, 1, 1), make8(21, 10, 1, 1), make8(20, 11, 1, 1), make8(21, 11, 1, 1) }, + }, + .rocks = [4]Sprite { + make8(18, 12, 1, 1), make8(19, 12, 1, 1), make8(20, 12, 1, 1), make8(21, 12, 1, 1) } }; } +fn make_pointers(comptime array_ptr: anytype) + [@typeInfo(@typeInfo(@TypeOf(array_ptr)).Pointer.child).Array.len] + *const @typeInfo(@typeInfo(@TypeOf(array_ptr)).Pointer.child).Array.child +{ + const len = @typeInfo(@typeInfo(@TypeOf(array_ptr)).Pointer.child).Array.len; + const item_type = @typeInfo(@typeInfo(@TypeOf(array_ptr)).Pointer.child).Array.child; + + comptime var ptr_array: [len]*const item_type = undefined; + comptime for (0..len) |idx| { + ptr_array[idx] = &(array_ptr.*[idx]); + }; + + return ptr_array; +} + pub const tree: *const Sprite = &assets.tree; pub const heart: *const Sprite = &assets.heart; -pub const terrain:[]const Sprite = &assets.terrain; +pub const pawn: *const Sprite = &assets.pawn; +pub const terrain = [4]*const [4]*const Sprite { + &make_pointers(&assets.terrain[0]), + &make_pointers(&assets.terrain[1]), + &make_pointers(&assets.terrain[2]), + &make_pointers(&assets.terrain[3]), +}; +pub const rocks: *const Sprite = make_pointers(&assets.rocks); + + + + + + + + + + + + + + + diff --git a/src/geometry/Matrix4f.zig b/src/geometry/Matrix4f.zig index e153665..93dc03a 100644 --- a/src/geometry/Matrix4f.zig +++ b/src/geometry/Matrix4f.zig @@ -1,6 +1,6 @@ const Matrix4f = @This(); -values: [16]f32 = identity, +values: [16]f32, const SIZE: i8 = 4 * 4; const identity = from_values([16]f32 { diff --git a/src/geometry/Rectf.zig b/src/geometry/Rectf.zig index 49209a1..d4b884b 100644 --- a/src/geometry/Rectf.zig +++ b/src/geometry/Rectf.zig @@ -17,8 +17,8 @@ pub fn from_xywh(x: f32, y: f32, w: f32, h: f32) Rectf { .y = y, .w = w, .h = h, - .a = Vec2f.new(x, y), - .b = Vec2f.new(x + w, y + h), + .a = Vec2f.create(x, y), + .b = Vec2f.create(x + w, y + h), }; } diff --git a/src/geometry/Recti.zig b/src/geometry/Recti.zig index 77cd44d..03365a9 100644 --- a/src/geometry/Recti.zig +++ b/src/geometry/Recti.zig @@ -1,6 +1,7 @@ const Vec2i = @import("./Vec2i.zig"); const Vec2f = @import("./Vec2f.zig"); const Rectf = @import("./Rectf.zig"); +const std = @import("std"); const Recti = @This(); @@ -17,8 +18,25 @@ pub fn from_xywh(x: i32, y: i32, w: i32, h: i32) Recti { .y = y, .w = w, .h = h, - .a = Vec2i.new(x, y), - .b = Vec2i.new(x + w, y + h), + .a = Vec2i.create(x, y), + .b = Vec2i.create(x + w, y + h), + }; +} + +pub fn from_ab(a: Vec2i, b: Vec2i) Recti { + const tlx = @min(a.x, b.x); + const tly = @min(a.y, b.y); + const brx = @max(a.x, b.x); + const bry = @max(a.y, b.y); + const width = brx - tlx; + const height = bry - tly; + return .{ + .x = tlx, + .y = tly, + .w = width, + .h = height, + .a = Vec2i.create(tlx, tly), + .b = Vec2i.create(brx, bry), }; } diff --git a/src/geometry/Vec2f.zig b/src/geometry/Vec2f.zig index 1d7630a..b01db3d 100644 --- a/src/geometry/Vec2f.zig +++ b/src/geometry/Vec2f.zig @@ -4,16 +4,16 @@ const Vec2f = @This(); x: f32, y: f32, -pub fn new(x: f32, y: f32) Vec2f { +pub fn create(x: f32, y: f32) Vec2f { return .{ .x = x, .y = y, }; } -pub const ZERO = new(0, 0); -pub const ONE = new(1, 1); -pub const NORTH = new(0, -1); -pub const SOUTH = new(0, 1); -pub const EAST = new(1, 0); -pub const WEST = new(-1, 0); +pub const ZERO = create(0, 0); +pub const ONE = create(1, 1); +pub const NORTH = create(0, -1); +pub const SOUTH = create(0, 1); +pub const EAST = create(1, 0); +pub const WEST = create(-1, 0); diff --git a/src/geometry/Vec2i.zig b/src/geometry/Vec2i.zig index 8bb81aa..01a9355 100644 --- a/src/geometry/Vec2i.zig +++ b/src/geometry/Vec2i.zig @@ -1,18 +1,31 @@ const Vec2i = @This(); +const Recti = @import("Recti.zig"); + x: i32, y: i32, -pub fn new(x: i32, y: i32) Vec2i { +pub fn create(x: i32, y: i32) Vec2i { return .{ .x = x, .y = y, }; } -pub const ZERO = new(0, 0); -pub const ONE = new(1, 1); -pub const NORTH = new(0, -1); -pub const SOUTH = new(0, 1); -pub const EAST = new(1, 0); -pub const WEST = new(-1, 0); +pub fn scale(self: *const Vec2i, s: i32) Vec2i { + return .{ + .x = self.x * s, + .y = self.y * s + }; +} + +pub fn to_unit_recti(self: *const Vec2i) Recti { + return Recti.from_xywh(self.x, self.y, 1, 1); +} + +pub const ZERO = create(0, 0); +pub const ONE = create(1, 1); +pub const NORTH = create(0, -1); +pub const SOUTH = create(0, 1); +pub const EAST = create(1, 0); +pub const WEST = create(-1, 0); diff --git a/src/scenes.zig b/src/scenes.zig index c65b3c2..5cb5457 100644 --- a/src/scenes.zig +++ b/src/scenes.zig @@ -1,13 +1,16 @@ const Scene = @import("Scene.zig"); const Terrain = @import("Terrain.zig"); - +const Pawn = @import("Pawn.zig"); +const Camera = @import("Camera.zig"); pub fn game() !Scene { var scene = Scene.create(); // first try is for allocating more memory in entities // second try is for allocating terrain on the heap... - try scene.add(try Terrain.create()); + try scene.add(try Camera.create()); + try scene.add(try Terrain.create(123)); + try scene.add(try Pawn.random()); return scene; } diff --git a/src/shaders.zig b/src/shaders.zig index 3b016b9..e36211d 100644 --- a/src/shaders.zig +++ b/src/shaders.zig @@ -55,7 +55,6 @@ pub const POSITION_ATTRIBUTE_ID = 0; pub const COLOR_ATTRIBUTE_ID = 1; pub const TEXCOORD_ATTRIBUTE_ID = 2; - var flat: Shader = undefined; var active_shader: *const Shader = undefined; @@ -67,6 +66,7 @@ pub fn load() !void { } pub fn set_projection_matrix(matrix: *const Matrix4f) void { + // std.debug.print("[shaders:set_projection_matrix] {}: {}\n", .{ active_shader, matrix }); active_shader.set_projection_matrix(matrix); } @@ -78,7 +78,6 @@ const Shader = struct { const Error = error { CompilationFailed }; - // cache: HashMap<*const u8, i32> = undefined, fn enable(self: *const Shader) void { c.glUseProgram(self.program_handle); @@ -88,18 +87,18 @@ const Shader = struct { } fn compile(vert_shader_text: []const u8, frag_shader_text: []const u8) !Shader { - const vertex_shader: c.GLuint= c.glCreateShader(c.GL_VERTEX_SHADER); + const vertex_shader: c.GLuint = c.glCreateShader(c.GL_VERTEX_SHADER); { c.glShaderSource(vertex_shader, 1, @ptrCast(&vert_shader_text), null); c.glCompileShader(vertex_shader); var compile_status: c.GLint = undefined; c.glGetShaderiv(vertex_shader, c.GL_COMPILE_STATUS, &compile_status); if (compile_status != c.GL_TRUE) { - std.log.err("Vertex shader compilation failed!", .{}); + std.log.err("[shaders:compile] Vertex shader compilation failed!", .{}); var error_text: [1000]u8 = undefined; var len: i32 = undefined; c.glGetShaderInfoLog(vertex_shader, 1000, &len, &error_text); - std.debug.print("{s}\n", .{ error_text[0..@intCast(len)] }); + std.log.err("{s}\n", .{ error_text[0..@intCast(len)] }); return Shader.Error.CompilationFailed; } } @@ -111,7 +110,7 @@ const Shader = struct { var compile_status: c.GLint = undefined; c.glGetShaderiv(fragment_shader, c.GL_COMPILE_STATUS, &compile_status); if (compile_status != c.GL_TRUE) { - std.log.err("Fragment shader compilation failed!", .{}); + std.log.err("[shaders:compile] Fragment shader compilation failed!", .{}); var error_text: [1000]u8 = undefined; var len: i32 = undefined; c.glGetShaderInfoLog(fragment_shader, 1000, &len, &error_text); diff --git a/textures.png b/textures.png index 14ad769cf7f18e4acedcfad6bcd3ee77feac072c..0f2565f2abf712867b44a278d8f47f58fb5230e3 100644 GIT binary patch delta 26470 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 A#sB~S delta 13652 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