commit 4ab7f93ac68b29c6c98a7c0c7e1b96d31d8c5a38 Author: rhinemann Date: Sat Mar 28 22:36:32 2026 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f98224 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.zig-cache +**.bak +zig-out diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..7a69bfd --- /dev/null +++ b/build.zig @@ -0,0 +1,69 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "PCG_6", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{}, + }), + .use_llvm = true, + }); + + const zgl = b.dependency("zgl", .{ .target = target, .optimize = optimize }); + exe.root_module.addImport("zgl", zgl.module("zgl")); + + const zglfw = b.dependency("zglfw", .{ .target = target, .optimize = optimize }); + exe.root_module.addImport("zglfw", zglfw.module("root")); + exe.root_module.linkLibrary(zglfw.artifact("glfw")); + + const zmath = b.dependency("zmath", .{}); + exe.root_module.addImport("zmath", zmath.module("root")); + + const zmesh = b.dependency("zmesh", .{ .target = target, .optimize = optimize, .shape_use_32bit_indices = false }); + exe.root_module.addImport("zmesh", zmesh.module("root")); + + const zstbi = b.dependency("zstbi", .{}); + exe.root_module.addImport("zstbi", zstbi.module("root")); + + const zgui = b.dependency("zgui", .{ + .shared = false, + .backend = .glfw_opengl3, + .with_implot = false, + .with_gizmo = false, + .with_node_editor = false, + .with_te = false, + .with_freetype = false, + .with_knobs = false, + .disable_obsolete = true, + }); + exe.root_module.addImport("zgui", zgui.module("root")); + exe.root_module.linkLibrary(zgui.artifact("imgui")); + + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + const run_exe_tests = b.addRunArtifact(exe_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_exe_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..5390ef1 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,37 @@ +.{ + .name = .computer_graphics_lab, + .version = "0.0.0", + .fingerprint = 0x44a1624c4685a0df, // Changing this has security and trust implications. + .minimum_zig_version = "0.15.2", + .dependencies = .{ + .zmath = .{ + .url = "git+https://github.com/zig-gamedev/zmath.git#3a5955b2b72cd081563fbb084eff05bffd1e3fbb", + .hash = "zmath-0.11.0-dev-wjwivdMsAwD-xaLj76YHUq3t9JDH-X16xuMTmnDzqbu2", + }, + .zglfw = .{ + .url = "git+https://github.com/zig-gamedev/zglfw.git#0dd29d8073487c9fe1e45e6b729b3aac271d5a71", + .hash = "zglfw-0.10.0-dev-zgVDNIG4IQBWN_sfMD-xfC9bJS2hbBN2W7jNlDLovcdC", + }, + .zgl = .{ + .url = "git+https://github.com/ziglibs/zgl.git#6ee54c287f3c4f176bd05a92746828bb791a1ad0", + .hash = "zgl-1.1.0-p_NpAJNECgCzUGx0aF5KFsmD5VOwfw8LaOsN0fcoU2bi", + }, + .zmesh = .{ + .url = "git+https://github.com/zig-gamedev/zmesh.git#a9c23ba7440b8c03cbc2bec89a3285fe84cbb50f", + .hash = "zmesh-0.11.0-dev-oO3A5lKRCgCGK8Krro4Rj_F_MhO8LT487re5u_DNIzvl", + }, + .zstbi = .{ + .url = "git+https://github.com/zig-gamedev/zstbi#664305dd52640be15cbebd7cd73d1199679933e1", + .hash = "zstbi-0.11.0-dev-L0Ea_2yVBwA4uh-tHfgdhZcQaWRoZVYlNGs4Dl_WTeuR", + }, + .zgui = .{ + .url = "git+https://github.com/zig-gamedev/zgui.git#ce016156a8520c438e886cd6c0b605e10ee7af3d", + .hash = "zgui-0.6.0-dev--L6sZKkSbgCUBXLVfwJDPpMkETz7ll-mmQYQae-nMxjt", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/shaders/default.frag b/shaders/default.frag new file mode 100644 index 0000000..6f4db72 --- /dev/null +++ b/shaders/default.frag @@ -0,0 +1,44 @@ +#version 460 core +in vec2 v_texcoord; +in vec3 v_normal; +in vec3 v_position; + +layout (binding = 1, std140) uniform Light { + uniform vec4 light_color; + uniform vec3 light_pos; + uniform vec3 camera_pos; +}; + +uniform sampler2D u_texture; + +out vec4 f_color; + +vec4 pointLight() { + vec3 light_vector = light_pos - v_position; + float dist = length(light_vector); + float a = 1.0; + float b = 0.7; + float intensity = 1 / (a * dist * dist + b * dist + 1.0f); + + // ambient lighting + float ambient = 0.2; + + // diffuse lighting + float diffuse_light = 0.7; + vec3 normal = normalize(v_normal); + vec3 light_direction = normalize(light_vector); + float diffuse = diffuse_light * max(dot(normal, light_direction), 0); + + // specular lighting + float specular_light = 0.7; + vec3 view_direction = normalize(camera_pos - v_position); + vec3 reflection_direction = reflect(-light_direction, normal); + vec3 halfway_vec = normalize(view_direction + light_direction); + float specular = specular_light * pow(max(dot(normal, halfway_vec), 0), 100); + + return (texture(u_texture, v_texcoord) * (diffuse * intensity + ambient) + specular * intensity) * light_color; +} + +void main() { + f_color = pointLight(); +} diff --git a/shaders/default.vert b/shaders/default.vert new file mode 100644 index 0000000..634b843 --- /dev/null +++ b/shaders/default.vert @@ -0,0 +1,72 @@ +#version 460 core +struct PositionData { + float x; + float y; + float z; +}; + +struct UvData { + float u; + float v; +}; + +struct NormalData { + float x; + float y; + float z; +}; + +layout (binding = 0, std430) restrict readonly buffer _positions { + PositionData pos_data[]; +}; + +layout (binding = 1, std430) restrict readonly buffer _uvs { + UvData uv_data[]; +}; + +layout (binding = 2, std430) restrict readonly buffer _normals { + NormalData norm_data[]; +}; + +vec4 getPosition(int index) { + return vec4( + pos_data[index].x, + pos_data[index].y, + pos_data[index].z, + 1 + ); +} + +vec2 getUv(int index) { + return vec2( + uv_data[index].u, + uv_data[index].v + ); +} + +vec3 getNormal(int index) { + return vec3( + norm_data[index].x, + norm_data[index].y, + norm_data[index].z + ); +} + +uniform mat4 u_model_matrix; +uniform mat4 u_normal_matrix; + +layout (binding = 0, std140) uniform SharedMatrices { + mat4 projection; + mat4 cam_matrix; +}; + +out vec2 v_texcoord; +out vec3 v_normal; +out vec3 v_position; + +void main() { + gl_Position = projection * cam_matrix * u_model_matrix * getPosition(gl_VertexID); + v_texcoord = getUv(gl_VertexID); + v_normal = mat3(u_normal_matrix) * getNormal(gl_VertexID); + v_position = vec3(u_model_matrix * getPosition(gl_VertexID)); +} diff --git a/shaders/light.frag b/shaders/light.frag new file mode 100644 index 0000000..c38bd05 --- /dev/null +++ b/shaders/light.frag @@ -0,0 +1,13 @@ +#version 460 core + +layout (binding = 1, std140) uniform Light { + uniform vec4 light_color; + uniform vec3 light_pos; + uniform vec3 camera_pos; +}; + +out vec4 f_color; + +void main() { + f_color = light_color; +} \ No newline at end of file diff --git a/shaders/light.vert b/shaders/light.vert new file mode 100644 index 0000000..ed05db1 --- /dev/null +++ b/shaders/light.vert @@ -0,0 +1,19 @@ +#version 460 core + +layout (binding = 0, std140) uniform SharedMatrices { + mat4 projection; + mat4 cam_matrix; +}; + +layout (binding = 1, std140) uniform Light { + uniform vec4 light_color; + uniform vec3 light_pos; + uniform vec3 camera_pos; +}; + +uniform vec4 u_position; + +void main() { + gl_Position = projection * cam_matrix * u_position; + gl_PointSize = 20 / distance(u_position.xyz, camera_pos); +} \ No newline at end of file diff --git a/shaders/skybox.frag b/shaders/skybox.frag new file mode 100644 index 0000000..814d2a4 --- /dev/null +++ b/shaders/skybox.frag @@ -0,0 +1,10 @@ +#version 460 core +in vec3 v_texcoord; + +out vec4 f_color; + +uniform samplerCube cubemap_texture; + +void main() { + f_color = texture(cubemap_texture, v_texcoord); +} \ No newline at end of file diff --git a/shaders/skybox.vert b/shaders/skybox.vert new file mode 100644 index 0000000..ab8acde --- /dev/null +++ b/shaders/skybox.vert @@ -0,0 +1,35 @@ +#version 460 core +// layout (location = 0) in vec3 a_position; + +struct PositionData { + float x; + float y; + float z; +}; + +layout (binding = 0, std430) restrict readonly buffer _positions { + PositionData pos_data[]; +}; + +vec4 getPosition(int index) { + return vec4( + pos_data[index].x, + pos_data[index].y, + pos_data[index].z, + 1 + ); +} + +layout (binding = 0, std140) uniform SharedMatrices { + mat4 projection; + mat4 cam_matrix; +}; + +out vec3 v_texcoord; + +void main() { + vec4 a_position = getPosition(gl_VertexID); + vec4 WVP_position = projection * mat4(mat3(cam_matrix)) * a_position; + gl_Position = WVP_position.xyww; + v_texcoord = a_position.xyz; +} \ No newline at end of file diff --git a/src/cube.zig b/src/cube.zig new file mode 100644 index 0000000..4648116 --- /dev/null +++ b/src/cube.zig @@ -0,0 +1,27 @@ +const IndexType = @import("zmesh").Shape.IndexType; + +pub const positions = [_][3]f32{ + [3]f32{ -1.0, 1.0, -1.0 }, + [3]f32{ -1.0, -1.0, -1.0 }, + [3]f32{ 1.0, -1.0, -1.0 }, + [3]f32{ 1.0, 1.0, -1.0 }, + [3]f32{ -1.0, -1.0, 1.0 }, + [3]f32{ -1.0, 1.0, 1.0 }, + [3]f32{ 1.0, -1.0, 1.0 }, + [3]f32{ 1.0, 1.0, 1.0 }, +}; + +pub const indices = [_]IndexType{ + 0, 1, 2, + 2, 3, 0, + 4, 1, 0, + 0, 5, 4, + 2, 6, 7, + 7, 3, 2, + 4, 5, 7, + 7, 6, 4, + 0, 3, 7, + 7, 5, 0, + 1, 4, 2, + 2, 4, 6, +}; diff --git a/src/gl_helper/camera.zig b/src/gl_helper/camera.zig new file mode 100644 index 0000000..3e3a7b5 --- /dev/null +++ b/src/gl_helper/camera.zig @@ -0,0 +1,113 @@ +const std = @import("std"); +const approxEqAbs = std.math.approxEqAbs; +const floatEps = std.math.floatEps; +const degToRad = std.math.degreesToRadians; + +const glfw = @import("zglfw"); +const zm = @import("zmath"); + +const Shader = @import("shader.zig"); + +const Camera = @This(); + +width: f32, +height: f32, +position: zm.Vec, +orientation: zm.Vec = zm.f32x4(0, 0, -1, 1), +up: zm.Vec = zm.f32x4(0, 1, 0, 1), +view: zm.Mat, +projection: zm.Mat, + +initial_speed: f32, +sensitivity: f32 = 100.0, + +controller: struct { + forward: bool = false, + backward: bool = false, + left: bool = false, + right: bool = false, + up: bool = false, + down: bool = false, + accelerate: bool = false, +}, + +pub fn init(position: zm.Vec, width: f32, height: f32, initial_speed: f32) Camera { + return .{ + .width = width, + .height = height, + .position = position, + .initial_speed = initial_speed, + .controller = .{}, + .view = zm.identity(), + .projection = zm.identity(), + }; +} + +pub fn updateProjection(self: *Camera, near: f32, far: f32) void { + self.projection = zm.perspectiveFovRhGl(std.math.degreesToRadians(60), self.width / self.height, near, far); +} + +pub fn updateView(self: *Camera) void { + self.view = zm.lookAtRh(self.position, self.position + self.orientation, self.up); +} + +pub fn updateControls(self: *Camera, key: glfw.Key, action: glfw.Action) void { + const bool_action = @intFromEnum(action) != 0; + switch (key) { + .a => self.controller.left = bool_action, + .w => self.controller.forward = bool_action, + .s => self.controller.backward = bool_action, + .d => self.controller.right = bool_action, + .space => self.controller.up = bool_action, + .left_control => self.controller.down = bool_action, + .left_shift => self.controller.accelerate = bool_action, + else => {}, + } +} + +pub fn move(self: *Camera, delta_time: f32) void { + var current_speed = zm.f32x4s(self.initial_speed * delta_time); + if (self.controller.accelerate) { + current_speed = zm.f32x4s(self.initial_speed * delta_time * 2); + } + + if (self.controller.forward) { + self.position += self.orientation * current_speed; + } + if (self.controller.backward) { + self.position -= self.orientation * current_speed; + } + if (self.controller.left) { + self.position -= zm.normalize3(zm.cross3(self.orientation, self.up)) * current_speed; + } + if (self.controller.right) { + self.position += zm.normalize3(zm.cross3(self.orientation, zm.normalize3(self.up))) * current_speed; + } + if (self.controller.up) { + self.position += zm.normalize3(zm.cross3(zm.cross3(self.orientation, self.up), self.orientation)) * current_speed; + } + if (self.controller.down) { + self.position -= zm.normalize3(zm.cross3(zm.cross3(self.orientation, self.up), self.orientation)) * current_speed; + } +} + +pub fn updateAngle(self: *Camera, xpos: f32, ypos: f32) void { + // Normalizes and shifts the coordinates of the cursor such that they begin in the middle of the screen + // and then "transforms" them into degrees + const rot_x: f32 = self.sensitivity * (ypos - (self.height / 2)) / self.height; + const rot_y: f32 = self.sensitivity * (xpos - (self.width / 2)) / self.width; + + // Calculates upcoming vertical change in the Orientation + // Rotate the view vector by the horizontal angle around the vertical axis + const horizontal_axis_rotation = zm.quatFromNormAxisAngle(self.up, degToRad(-rot_y)); + self.orientation = zm.normalize3(zm.rotate(horizontal_axis_rotation, self.orientation)); + + // Rotate the view vector by the vertical angle around the horizontal axis + const u = zm.cross3(self.orientation, self.up); + const vertical_axis_rotation = zm.quatFromNormAxisAngle(u, degToRad(-rot_x)); + + const new_orientation = zm.normalize3(zm.rotate(vertical_axis_rotation, self.orientation)); + if (!approxEqAbs(f32, new_orientation[1], -1, 0.001)) { + self.orientation = new_orientation; + } +} diff --git a/src/gl_helper/color.zig b/src/gl_helper/color.zig new file mode 100644 index 0000000..07d98de --- /dev/null +++ b/src/gl_helper/color.zig @@ -0,0 +1,63 @@ +const zm = @import("zmath"); + +pub const Byte = struct { + // Custom raylib color palette for amazing visuals on WHITE background + pub const light_gray = [_]u8{ 200, 200, 200, 255 }; // Light Gray + pub const gray = [_]u8{ 130, 130, 130, 255 }; // Gray + pub const dark_gray = [_]u8{ 80, 80, 80, 255 }; // Dark Gray + pub const yellow = [_]u8{ 253, 249, 0, 255 }; // Yellow + pub const gold = [_]u8{ 255, 203, 0, 255 }; // Gold + pub const orange = [_]u8{ 255, 161, 0, 255 }; // Orange + pub const pink = [_]u8{ 255, 109, 194, 255 }; // Pink + pub const red = [_]u8{ 230, 41, 55, 255 }; // Red + pub const maroon = [_]u8{ 190, 33, 55, 255 }; // Maroon + pub const green = [_]u8{ 0, 228, 48, 255 }; // Green + pub const lime = [_]u8{ 0, 158, 47, 255 }; // Lime + pub const dark_green = [_]u8{ 0, 117, 44, 255 }; // Dark Green + pub const sky_blue = [_]u8{ 102, 191, 255, 255 }; // Sky Blue + pub const blue = [_]u8{ 0, 121, 241, 255 }; // Blue + pub const dark_blue = [_]u8{ 0, 82, 172, 255 }; // Dark Blue + pub const purple = [_]u8{ 200, 122, 255, 255 }; // Purple + pub const violet = [_]u8{ 135, 60, 190, 255 }; // Violet + pub const dark_purple = [_]u8{ 112, 31, 126, 255 }; // Dark Purple + pub const beige = [_]u8{ 211, 176, 131, 255 }; // Beige + pub const brown = [_]u8{ 127, 106, 79, 255 }; // Brown + pub const dark_brown = [_]u8{ 76, 63, 47, 255 }; // Dark Brown + + pub const white = [_]u8{ 255, 255, 255, 255 }; // White + pub const black = [_]u8{ 0, 0, 0, 255 }; // Black + pub const blank = [_]u8{ 0, 0, 0, 0 }; // Blank (Transparent) + pub const magenta = [_]u8{ 255, 0, 255, 255 }; // Magenta + pub const ray_white = [_]u8{ 245, 245, 245, 255 }; // Raylib whire (raylib logo) +}; + +pub const Float = struct { + // Custom raylib color palette for amazing visuals on WHITE background + pub const light_gray = zm.normalize4(zm.f32x4(200, 200, 200, 255)); // Light Gray + pub const gray = zm.normalize4(zm.f32x4(130, 130, 130, 255)); // Gray + pub const dark_gray = zm.normalize4(zm.f32x4(80, 80, 80, 255)); // Dark Gray + pub const yellow = zm.normalize4(zm.f32x4(253, 249, 0, 255)); // Yellow + pub const gold = zm.normalize4(zm.f32x4(255, 203, 0, 255)); // Gold + pub const orange = zm.normalize4(zm.f32x4(255, 161, 0, 255)); // Orange + pub const pink = zm.normalize4(zm.f32x4(255, 109, 194, 255)); // Pink + pub const red = zm.normalize4(zm.f32x4(230, 41, 55, 255)); // Red + pub const maroon = zm.normalize4(zm.f32x4(190, 33, 55, 255)); // Maroon + pub const green = zm.normalize4(zm.f32x4(0, 228, 48, 255)); // Green + pub const lime = zm.normalize4(zm.f32x4(0, 158, 47, 255)); // Lime + pub const dark_green = zm.normalize4(zm.f32x4(0, 117, 44, 255)); // Dark Green + pub const sky_blue = zm.normalize4(zm.f32x4(102, 191, 255, 255)); // Sky Blue + pub const blue = zm.normalize4(zm.f32x4(0, 121, 241, 255)); // Blue + pub const dark_blue = zm.normalize4(zm.f32x4(0, 82, 172, 255)); // Dark Blue + pub const purple = zm.normalize4(zm.f32x4(200, 122, 255, 255)); // Purple + pub const violet = zm.normalize4(zm.f32x4(135, 60, 190, 255)); // Violet + pub const dark_purple = zm.normalize4(zm.f32x4(112, 31, 126, 255)); // Dark Purple + pub const beige = zm.normalize4(zm.f32x4(211, 176, 131, 255)); // Beige + pub const brown = zm.normalize4(zm.f32x4(127, 106, 79, 255)); // Brown + pub const dark_brown = zm.normalize4(zm.f32x4(76, 63, 47, 255)); // Dark Brown + + pub const white = zm.normalize4(zm.f32x4(255, 255, 255, 255)); // White + pub const black = zm.normalize4(zm.f32x4(0, 0, 0, 255)); // Black + pub const blank = zm.normalize4(zm.f32x4(0, 0, 0, 0)); // Blank (Transparent) + pub const magenta = zm.normalize4(zm.f32x4(255, 0, 255, 255)); // Magenta + pub const ray_white = zm.normalize4(zm.f32x4(245, 245, 245, 255)); // Raylib whire (raylib logo) +}; diff --git a/src/gl_helper/mesh.zig b/src/gl_helper/mesh.zig new file mode 100644 index 0000000..6111d50 --- /dev/null +++ b/src/gl_helper/mesh.zig @@ -0,0 +1,125 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const zgl = @import("zgl"); +const Shape = @import("zmesh").Shape; + +const helper = @import("root.zig"); +const Camera = @import("camera.zig"); +const Shader = @import("shader.zig"); +const Texture = @import("texture.zig"); + +index_count: usize, +primitive_type: zgl.PrimitiveType, +textures: ?[]const Texture, + +position_data_buffer: zgl.Buffer, +uv_data_buffer: ?zgl.Buffer, +normal_data_buffer: ?zgl.Buffer, + +vao: zgl.VertexArray, + +pub fn initShape(shape: Shape, textures: ?[]const Texture, primitive_type: zgl.PrimitiveType) @This() { + return initVertices( + shape.positions, + shape.texcoords, + shape.normals, + shape.indices, + textures, + primitive_type, + ); +} + +pub fn initVertices( + positions: []const [3]f32, + maybe_texcoords: ?[]const [2]f32, + maybe_normals: ?[]const [3]f32, + indices: []const Shape.IndexType, + textures: ?[]const Texture, + primitive_type: zgl.PrimitiveType, +) @This() { + const vao = zgl.VertexArray.create(); + + const position_data_buffer = helper.initSsbo([3]f32, positions, .{}); + var uv_data_buffer: ?zgl.Buffer = null; + var normal_data_buffer: ?zgl.Buffer = null; + + if (maybe_texcoords) |texcoords| { + uv_data_buffer = helper.initSsbo([2]f32, texcoords, .{}); + } + + if (maybe_normals) |normals| { + normal_data_buffer = helper.initSsbo([3]f32, normals, .{}); + } + + const ebo = helper.initEbo(Shape.IndexType, indices); + defer ebo.delete(); + vao.elementBuffer(ebo); + + return .{ + .index_count = indices.len, + .textures = textures, + .primitive_type = primitive_type, + .vao = vao, + .position_data_buffer = position_data_buffer, + .uv_data_buffer = uv_data_buffer, + .normal_data_buffer = normal_data_buffer, + }; +} + +pub fn draw(self: @This(), shader: Shader) void { + drawInstanced(self, shader, 1); +} + +pub fn drawInstanced(self: @This(), shader: Shader, instance_count: u32) void { + shader.use(); + self.vao.bind(); + + if (self.textures) |textures| { + textures[0].bind(); + shader.updateUniformInt("u_texture", @intCast(textures[0].unit)); + } + + zgl.bindBufferBase(.shader_storage_buffer, 0, self.position_data_buffer); + + if (self.uv_data_buffer) |uv_buffer| { + zgl.bindBufferBase(.shader_storage_buffer, 1, uv_buffer); + } + + if (self.normal_data_buffer) |normal_buffer| { + zgl.bindBufferBase(.shader_storage_buffer, 2, normal_buffer); + } + + if (Shape.IndexType == u32) { + zgl.drawElementsInstanced(self.primitive_type, self.index_count, .unsigned_int, 0, instance_count); + } else { + zgl.drawElementsInstanced(self.primitive_type, self.index_count, .unsigned_short, 0, instance_count); + } + + if (self.textures) |textures| { + for (textures) |tex| { + zgl.bindTextureUnit(.invalid, tex.unit); + } + } + + zgl.bindBufferBase(.shader_storage_buffer, 0, .invalid); + zgl.bindBufferBase(.shader_storage_buffer, 1, .invalid); + zgl.bindBufferBase(.shader_storage_buffer, 2, .invalid); +} + +pub fn delete(self: @This()) void { + self.vao.delete(); + if (self.textures) |textures| { + for (textures) |tex| { + tex.delete(); + } + } + self.position_data_buffer.delete(); + if (self.uv_data_buffer) |uv_buffer| { + uv_buffer.delete(); + } + + if (self.normal_data_buffer) |normal_buffer| { + normal_buffer.delete(); + } +} diff --git a/src/gl_helper/model.zig b/src/gl_helper/model.zig new file mode 100644 index 0000000..01d8d98 --- /dev/null +++ b/src/gl_helper/model.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const ArrayList = std.ArrayList; + +const zm = @import("zmath"); + +const Mesh = @import("mesh.zig"); +const Shader = @import("shader.zig"); +const Tree = @import("../tree.zig").Tree; + +pub const JointData = struct { + mesh: Mesh, + translation: zm.Mat, + rotation: zm.Quat, +}; + +data: Tree(JointData), + +pub fn init( + data: Tree(JointData), +) @This() { + return .{ .data = data }; +} + +pub fn draw(self: *@This(), shader: Shader) void { + var iter = self.data.depthFirstIterator(); + while (iter.next()) |node| { + var rotation = node.val.rotation; + var current = node; + while (current.parent) |parent| { + rotation = zm.qmul(rotation, parent.val.rotation); + current = parent; + } + + const model_matrix = zm.mul(zm.quatToMat(rotation), node.val.translation); + const normal_matrix = zm.transpose(zm.inverse(model_matrix)); + shader.updateUniformMatrix("u_model_matrix", &.{model_matrix}); + shader.updateUniformMatrix("u_normal_matrix", &.{normal_matrix}); + node.val.mesh.draw(shader); + } +} + +pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + self.data.deinit(allocator); +} diff --git a/src/gl_helper/root.zig b/src/gl_helper/root.zig new file mode 100644 index 0000000..ad3faeb --- /dev/null +++ b/src/gl_helper/root.zig @@ -0,0 +1,53 @@ +const refAllDeclsRecursive = @import("std").testing.refAllDeclsRecursive; + +const zgl = @import("zgl"); +const zm = @import("zmath"); + +pub const Camera = @import("camera.zig"); +pub const Color = @import("color.zig"); +pub const Mesh = @import("mesh.zig"); +pub const Model = @import("model.zig"); +pub const Shader = @import("shader.zig"); +pub const Texture = @import("texture.zig"); + +pub fn initEbo(T: type, indices: []const T) zgl.Buffer { + var buffer = zgl.Buffer.create(); + buffer.storage(T, indices.len, indices.ptr, .{}); + return buffer; +} + +pub fn initVbo(T: type, items: []const T, flags: zgl.BufferStorageFlags) zgl.Buffer { + const buffer = zgl.Buffer.create(); + buffer.storage(T, items.len, items.ptr, flags); + return buffer; +} + +pub fn initUbo(T: type, shaders: []const Shader, name: [:0]const u8, binding: u32, size: u32) zgl.Buffer { + for (shaders) |shader| { + if (zgl.getUniformBlockIndex(shader.id, name)) |index| { + zgl.uniformBlockBinding(shader.id, index, binding); + } + } + var buffer = zgl.Buffer.create(); + buffer.storage(T, size, null, .{ .dynamic_storage = true }); + zgl.bindBufferRange(.uniform_buffer, binding, buffer, 0, size * @sizeOf(T)); + + return buffer; +} + +pub fn initSsbo(T: type, vertices: []const T, flags: zgl.BufferStorageFlags) zgl.Buffer { + var buffer = zgl.Buffer.create(); + buffer.storage(T, vertices.len, vertices.ptr, flags); + return buffer; +} + +pub fn linkAttribute(vao: zgl.VertexArray, vbo: zgl.Buffer, location: u32, binding_index: u32, size: u32, attrib_type: zgl.Type, normalised: bool, offset: usize, stride: usize) void { + vao.vertexBuffer(binding_index, vbo, offset, stride); + vao.enableVertexAttribute(location); + vao.attribFormat(location, size, attrib_type, normalised, offset); + vao.attribBinding(location, binding_index); +} + +comptime { + refAllDeclsRecursive(@This()); +} diff --git a/src/gl_helper/shader.zig b/src/gl_helper/shader.zig new file mode 100644 index 0000000..76b34be --- /dev/null +++ b/src/gl_helper/shader.zig @@ -0,0 +1,72 @@ +const std = @import("std"); + +// const gl = @import("zopengl").wrapper; +const zgl = @import("zgl"); +const zm = @import("zmath"); + +const Shader = @This(); + +id: zgl.Program, + +pub fn initSource(vertex_source: []const u8, fragment_source: []const u8) !Shader { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const vertex = zgl.Shader.create(.vertex); + vertex.source(1, &vertex_source); + vertex.compile(); + + const fragment = zgl.Shader.create(.fragment); + fragment.source(1, &fragment_source); + fragment.compile(); + + const program = zgl.Program.create(); + program.attach(vertex); + program.attach(fragment); + program.link(); + + return .{ .id = program }; +} + +pub fn initFile(vertex_path: []const u8, fragment_path: []const u8) !Shader { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const vertex_source = try readFromFile(arena.allocator(), vertex_path); + const fragment_source = try readFromFile(arena.allocator(), fragment_path); + return initSource(vertex_source, fragment_source); +} + +pub fn use(self: Shader) void { + self.id.use(); +} + +pub fn updateUniformMatrix(self: Shader, name: [:0]const u8, matrices: []const zm.Mat) void { + self.id.uniformMatrix4(self.id.uniformLocation(name), false, matrices); +} + +pub fn updateUniformVec4(self: Shader, name: [:0]const u8, vectors: []const zm.Vec) void { + if (self.id.uniformLocation(name)) |location| { + zgl.binding.programUniform4fv(@intFromEnum(self.id), @intCast(location), @intCast(vectors.len), @ptrCast(vectors)); + } +} + +pub fn updateUniformVec3(self: Shader, name: [:0]const u8, vectors: []const [3]f32) void { + if (self.id.uniformLocation(name)) |location| { + zgl.binding.programUniform3fv(@intFromEnum(self.id), @intCast(location), @intCast(vectors.len), @ptrCast(vectors)); + } + // zgl.uniform3fv(self.id.uniformLocation(name), vectors); +} + +pub fn updateUniformInt(self: Shader, name: [:0]const u8, value: i32) void { + self.id.uniform1i(self.id.uniformLocation(name), value); +} + +pub fn delete(self: Shader) void { + self.id.delete(); +} + +/// The caller is responsible for freeing the content. +fn readFromFile(allocator: std.mem.Allocator, file_path: []const u8) ![:0]u8 { + return std.fs.cwd().readFileAllocOptions(allocator, file_path, std.math.maxInt(usize), null, .@"1", 0); +} diff --git a/src/gl_helper/texture.zig b/src/gl_helper/texture.zig new file mode 100644 index 0000000..a371a80 --- /dev/null +++ b/src/gl_helper/texture.zig @@ -0,0 +1,108 @@ +const std = @import("std"); + +const zgl = @import("zgl"); + +const zstbi = @import("zstbi"); +const Image = zstbi.Image; + +const Texture = @This(); + +id: zgl.Texture, +target: zgl.TextureTarget, +unit: u32, + +pub fn init(unit: u32, target: zgl.TextureTarget) Texture { + const id = zgl.Texture.create(target); + + return .{ .id = id, .unit = unit, .target = target }; +} + +pub fn loadCubemapFromFile(self: Texture, pathnames: []const [:0]const u8) !void { + const info = Image.info(pathnames[0]); + var internal_formal: zgl.TextureInternalFormat = undefined; + var pixel_formal: zgl.PixelFormat = undefined; + internal_formal, pixel_formal = switch (info.num_components) { + 1 => .{ .r8, .red }, + 2 => .{ .rg8, .rg }, + 3 => .{ .rgb8, .rgb }, + 4 => .{ .rgba8, .rgba }, + else => unreachable, + }; + self.id.storage2D(1, internal_formal, info.width, info.height); + + for (pathnames, 0..) |pathname, i| { + var image = try Image.loadFromFile(pathname, info.num_components); + defer image.deinit(); + self.id.subImage3D( + 0, + 0, + 0, + i, + info.width, + info.height, + 1, + pixel_formal, + if (zstbi.is16bit(pathname)) .unsigned_short else .unsigned_byte, + image.data.ptr, + ); + } +} + +pub fn loadCubemap(self: Texture, images: []const [*]const u8, width: usize, height: usize, pixel_type: zgl.PixelType, channels: u32) void { + var internal_formal: zgl.TextureInternalFormat = undefined; + var pixel_formal: zgl.PixelFormat = undefined; + internal_formal, pixel_formal = switch (channels) { + 1 => .{ .r8, .red }, + 2 => .{ .rg8, .rg }, + 3 => .{ .rgb8, .rgb }, + 4 => .{ .rgba8, .rgba }, + else => unreachable, + }; + + self.id.storage2D(1, internal_formal, width, height); + for (0..6) |i| { + self.id.subImage3D(0, 0, 0, i, width, height, 1, pixel_formal, pixel_type, images[i]); + } +} + +pub fn loadFromFile(self: Texture, pathname: [:0]const u8) !void { + var image = try Image.loadFromFile(pathname, 0); + defer image.deinit(); + + load( + self, + image.data.ptr, + image.width, + image.height, + if (zstbi.is16bit(pathname)) .unsigned_short else .unsigned_byte, + image.num_components, + ); +} + +pub fn load(self: Texture, image: [*]const u8, width: usize, height: usize, pixel_type: zgl.PixelType, channels: u32) void { + var internal_formal: zgl.TextureInternalFormat = undefined; + var pixel_formal: zgl.PixelFormat = undefined; + internal_formal, pixel_formal = switch (channels) { + 1 => .{ .r8, .red }, + 2 => .{ .rg8, .rg }, + 3 => .{ .rgb8, .rgb }, + 4 => .{ .rgba8, .rgba }, + else => unreachable, + }; + + self.id.storage2D(1, internal_formal, width, height); + self.id.subImage2D(0, 0, 0, width, height, pixel_formal, pixel_type, image); + self.id.generateMipmap(); +} + +pub fn bind(self: Texture) void { + self.id.bindTo(self.unit); +} + +pub fn unbind(self: Texture) void { + zgl.bindTextureUnit(.invalid, self.unit); +} + +pub fn delete(self: Texture) void { + self.id.delete(); +} diff --git a/src/glfw_helper/callbacks.zig b/src/glfw_helper/callbacks.zig new file mode 100644 index 0000000..c768748 --- /dev/null +++ b/src/glfw_helper/callbacks.zig @@ -0,0 +1,49 @@ +const zgl = @import("zgl"); +const zgui = @import("zgui"); +const zm = @import("zmath"); + +const glfw = @import("zglfw"); +const Window = glfw.Window; + +const glfw_helper = @import("root.zig"); +const AppState = glfw_helper.AppState; + +pub fn cursorPosCallback(window: *Window, xpos: f64, ypos: f64) callconv(.c) void { + const app_state = window.getUserPointer(AppState).?; + + if (!app_state.render_gui) { + app_state.camera.updateAngle(@floatCast(xpos), @floatCast(ypos)); + glfw.setCursorPos(window, @as(f32, @floatFromInt(app_state.width)) / 2, @as(f32, @floatFromInt(app_state.height)) / 2); + } +} + +pub fn keyCallback(window: *Window, key: glfw.Key, _: c_int, action: glfw.Action, _: glfw.Mods) callconv(.c) void { + const app_state = window.getUserPointer(AppState).?; + + if (action == .press) { + switch (key) { + .tab => { + app_state.render_gui = !app_state.render_gui; + if (app_state.render_gui) { + window.setInputMode(.cursor, .normal) catch {}; + } else { + window.setInputMode(.cursor, .disabled) catch {}; + app_state.camera.updateAngle(@as(f32, @floatFromInt(app_state.width)) / 2, @as(f32, @floatFromInt(app_state.height)) / 2); + } + glfw.setCursorPos(window, @as(f32, @floatFromInt(app_state.width)) / 2, @as(f32, @floatFromInt(app_state.height)) / 2); + }, + .escape => window.setShouldClose(true), + else => {}, + } + } + + if (!app_state.render_gui) { + app_state.camera.updateControls(key, action); + } +} + +pub fn messageCallback(source: zgl.DebugSource, msg_type: zgl.DebugMessageType, id: usize, severity: zgl.DebugSeverity, message: []const u8) void { + const print = @import("std").debug.print; + + print("{t}, {t}, {t}, {d}: {s}\n", .{ source, msg_type, severity, id, message }); +} diff --git a/src/glfw_helper/root.zig b/src/glfw_helper/root.zig new file mode 100644 index 0000000..f98558d --- /dev/null +++ b/src/glfw_helper/root.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const refAllDeclsRecursive = std.testing.refAllDeclsRecursive; + +const zgl = @import("zgl"); +const zm = @import("zmath"); +const glfw = @import("zglfw"); +const Window = glfw.Window; + +const gl_helper = @import("../gl_helper/root.zig"); + +const callbacks = @import("callbacks.zig"); +const Scene = @import("scene.zig").Scene; + +pub const AppState = struct { + height: i32, + width: i32, + camera: gl_helper.Camera, + scene: Scene, + light_pos: zm.Vec, + render_gui: bool, + + pub fn init(height: i32, width: i32, scene: Scene, camera_pos: zm.Vec, light_pos: zm.Vec, initial_speed: f32) AppState { + return .{ + .height = height, + .width = width, + .scene = scene, + .camera = .init(camera_pos, @floatFromInt(width), @floatFromInt(height), initial_speed), + .light_pos = light_pos, + .render_gui = true, + }; + } +}; + +pub const GlfwContext = struct { + window: *Window, + + pub fn init(app_state: *AppState, title: [:0]const u8) !GlfwContext { + // glfw: initialize and configure + try glfw.init(); + + glfw.windowHint(.client_api, .opengl_api); + glfw.windowHint(.context_version_major, 4); + glfw.windowHint(.context_version_minor, 6); + glfw.windowHint(.opengl_profile, .opengl_core_profile); + glfw.windowHint(.opengl_forward_compat, true); + glfw.windowHint(.doublebuffer, true); + + glfw.windowHint(.focused, true); + glfw.windowHint(.resizable, false); + + // glfw window creation + const window = try Window.create(app_state.width, app_state.height, title, null, null); + glfw.makeContextCurrent(window); + glfw.swapInterval(1); + + if (glfw.rawMouseMotionSupported()) { + try window.setInputMode(.raw_mouse_motion, true); + } + + _ = window.setKeyCallback(callbacks.keyCallback); + _ = window.setCursorPosCallback(callbacks.cursorPosCallback); + + // zgl: load all OpenGL function pointers + try zgl.loadExtensions(@as(glfw.GlProc, undefined), getProcAddress); + + zgl.enable(.debug_output); + zgl.enable(.debug_output_synchronous); + zgl.debugMessageCallback({}, callbacks.messageCallback); + + window.setUserPointer(app_state); + + return .{ .window = window }; + } + + pub fn postRender(self: GlfwContext) void { + self.window.swapBuffers(); + glfw.pollEvents(); + } + + pub fn cleanUp(self: GlfwContext) void { + self.window.destroy(); + glfw.terminate(); + } +}; + +pub fn getTime() f64 { + return glfw.getTime(); +} + +fn getProcAddress(_: glfw.GlProc, proc: [:0]const u8) ?zgl.binding.FunctionPointer { + return @alignCast(glfw.getProcAddress(proc)); +} + +comptime { + refAllDeclsRecursive(@This()); +} diff --git a/src/glfw_helper/scene.zig b/src/glfw_helper/scene.zig new file mode 100644 index 0000000..9bba03f --- /dev/null +++ b/src/glfw_helper/scene.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +const glfw = @import("zglfw"); + +pub const Scene = enum(i32) { + robot, + fish, +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..36c50fe --- /dev/null +++ b/src/main.zig @@ -0,0 +1,370 @@ +const std = @import("std"); + +const glfw = @import("zglfw"); +const zgl = @import("zgl"); +const zgui = @import("zgui"); +const zm = @import("zmath"); +const Mat = zm.Mat; +const zmesh = @import("zmesh"); +const Shape = zmesh.Shape; +const zstbi = @import("zstbi"); + +const gl_helper = @import("gl_helper/root.zig"); +const Shader = gl_helper.Shader; +const Texture = gl_helper.Texture; +const Mesh = gl_helper.Mesh; +const Model = gl_helper.Model; +const ByteColor = gl_helper.Color.Byte; +const FloatColor = gl_helper.Color.Float; +const robot_texture_data = ByteColor.green; +const glfw_helper = @import("glfw_helper/root.zig"); +const GlfwContect = glfw_helper.GlfwContext; +const AppState = glfw_helper.AppState; +const Tree = @import("tree.zig").Tree; + +const floor_texture_data = + ByteColor.blue ++ ByteColor.white ++ + ByteColor.white ++ ByteColor.blue; + +var app_state = AppState.init( + 600, + 600, + .robot, + zm.f32x4(0, 1, 3, 1), + zm.f32x4(0, 1, 0.5, 1), + 1, +); + +const window_flags = zgui.WindowFlags{ + .no_move = true, + .no_resize = true, + .no_title_bar = true, + .no_nav_inputs = true, +}; + +pub fn main() !void { + var debug_allocator = std.heap.DebugAllocator(.{}).init; + defer _ = debug_allocator.deinit(); + const allocator = debug_allocator.allocator(); + + const context = try GlfwContect.init(&app_state, "Lab 6"); + + zmesh.init(allocator); + defer zmesh.deinit(); + + zstbi.init(allocator); + defer zstbi.deinit(); + + zgui.init(allocator); + defer zgui.deinit(); + + zgui.io.setIniFilename(null); + + zgui.backend.init(context.window); + defer zgui.backend.deinit(); + + const style = zgui.getStyle(); + style.setColorsBuiltin(.dark); + style.window_border_size = 0; + + // shaders + const model_shader = try Shader.initFile("shaders/default.vert", "shaders/default.frag"); + defer model_shader.delete(); + + const light_shader = try Shader.initFile("shaders/light.vert", "shaders/light.frag"); + defer light_shader.delete(); + + const skybox_shader = try Shader.initFile("shaders/skybox.vert", "shaders/skybox.frag"); + defer skybox_shader.delete(); + + // textures + const floor_texture: []const Texture = &.{ + Texture.init(0, .@"2d"), + }; + floor_texture[0].id.parameter(.mag_filter, .nearest); + floor_texture[0].id.parameter(.min_filter, .nearest); + floor_texture[0].load(&floor_texture_data, 2, 2, .unsigned_byte, 4); + + const robot_texture: []const Texture = &.{ + Texture.init(0, .@"2d"), + }; + robot_texture[0].id.parameter(.mag_filter, .nearest); + robot_texture[0].id.parameter(.min_filter, .nearest); + robot_texture[0].load(&robot_texture_data, 1, 1, .unsigned_byte, 4); + + // meshes + const floor = initFloor(floor_texture); + defer floor.delete(); + + var robot = try initRobot(allocator, robot_texture); + defer robot.deinit(allocator); + + // uniform data + var light_color = zm.f32x4(1, 1, 1, 1); + + light_shader.updateUniformVec4("u_position", &.{app_state.light_pos}); + model_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()}); + model_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()}); + + // uniform buffer objects + const matrix_ubo = gl_helper.initUbo( + zm.Mat, + &.{ model_shader, light_shader, skybox_shader }, + "SharedMatrices", + 0, + 2, + ); + defer matrix_ubo.delete(); + app_state.camera.updateProjection(0.1, 200); + matrix_ubo.subData(0, zm.Mat, &.{app_state.camera.projection}); + + const light_ubo = gl_helper.initUbo( + zm.Vec, + &.{ model_shader, light_shader }, + "Light", + 1, + 3, + ); + defer light_ubo.delete(); + light_ubo.subData(0, zm.Vec, &.{light_color}); + light_ubo.subData(@sizeOf(zm.Vec), zm.Vec, &.{app_state.light_pos}); + + // time handling + var previous_time = glfw_helper.getTime(); + var current_time: f64 = 0; + var delta_time: f64 = undefined; + // var frame_counter: u16 = 0; + + // OpenGL setup + const clear_color = FloatColor.sky_blue; + zgl.clearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]); + zgl.enable(.program_point_size); + zgl.enable(.depth_test); + zgl.enable(.cull_face); + + while (!context.window.shouldClose()) : (context.postRender()) { + // input + app_state.camera.move(@floatCast(delta_time)); + + // ubo updates + matrix_ubo.subData(0, Mat, &.{app_state.camera.projection}); + app_state.camera.updateView(); + matrix_ubo.subData(@sizeOf(Mat), Mat, &.{app_state.camera.view}); + + light_ubo.subData(@sizeOf(zm.Vec) * 2, zm.Vec, &.{app_state.camera.position}); + + // render + zgl.clear(.{ .color = true, .depth = true }); + + switch (app_state.scene) { + .robot => { + model_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()}); + model_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()}); + floor.draw(model_shader); + + // animateRobot(&frame_counter, robot); + robot.draw(model_shader); + }, + .fish => {}, + } + + light_shader.use(); + zgl.drawArrays(.points, 0, 1); + + // GUI + drawGui(light_ubo, light_shader, &light_color); + + // time + current_time = glfw_helper.getTime(); + delta_time = current_time - previous_time; + previous_time = current_time; + } +} + +fn initFloor(textures: ?[]const Texture) Mesh { + var shape = Shape.initPlane(1, 1); + var inverted_shape = Shape.initPlane(1, 1); + defer shape.deinit(); + defer inverted_shape.deinit(); + + inverted_shape.invert(0, 0); + shape.merge(inverted_shape); + + shape.translate(-0.5, -0.5, 0); + shape.rotate(std.math.degreesToRadians(-90), 1, 0, 0); + shape.scale(2, 1, 2); + if (shape.texcoords) |texcoords| { + for (texcoords) |*coord| { + coord[0] *= 6; + coord[1] *= 6; + } + } + + return Mesh.initShape(shape, textures, .triangles); +} + +fn initRobot(allocator: std.mem.Allocator, textures: ?[]const Texture) !Model { + // var meshes = try std.ArrayList(Mesh).initCapacity(allocator, 14); + // var translations = try std.ArrayList(Mat).initCapacity(allocator, 14); + + var shape = Shape.initCube(); + defer shape.deinit(); + + shape.unweld(); + shape.computeNormals(); + shape.translate(-0.5, -1, -0.5); + // shape.rotate(std.math.degreesToRadians(180), 1, 0, 0); + + // var foot = shape.clone(); + // defer foot.deinit(); + // foot.scale(0.1, 0.025, 0.15); + // meshes.appendNTimesAssumeCapacity(Mesh.initShape(foot, textures, .triangles), 2); + // translations.appendAssumeCapacity(zm.translation(-0.07, 0.025, 0)); + // translations.appendAssumeCapacity(zm.translation(0.07, 0.025, 0)); + + // var leg = shape.clone(); + // defer leg.deinit(); + // leg.scale(0.08, 0.2, 0.08); + // meshes.appendNTimesAssumeCapacity(Mesh.initShape(leg, textures, .triangles), 4); + // translations.appendAssumeCapacity(zm.translation(-0.07, 0.225, 0)); + // translations.appendAssumeCapacity(zm.translation(0.07, 0.225, 0)); + // translations.appendAssumeCapacity(zm.translation(-0.07, 0.425, 0)); + // translations.appendAssumeCapacity(zm.translation(0.07, 0.425, 0)); + + // var arm = shape.clone(); + // defer arm.deinit(); + // arm.scale(0.08, 0.2, 0.08); + // meshes.appendNTimesAssumeCapacity(Mesh.initShape(arm, textures, .triangles), 4); + // translations.appendAssumeCapacity(zm.translation(-0.3 / 2.0 - 0.04, 0.225 + 0.4, 0)); + // translations.appendAssumeCapacity(zm.translation(0.3 / 2.0 + 0.04, 0.225 + 0.4, 0)); + // translations.appendAssumeCapacity(zm.translation(-0.3 / 2.0 - 0.04, 0.425 + 0.4, 0)); + // translations.appendAssumeCapacity(zm.translation(0.3 / 2.0 + 0.04, 0.425 + 0.4, 0)); + + // var hand = shape.clone(); + // defer hand.deinit(); + // hand.scale(0.025, 0.08, 0.07); + // meshes.appendNTimesAssumeCapacity(Mesh.initShape(hand, textures, .triangles), 2); + // translations.appendAssumeCapacity(zm.translation(-0.3 / 2.0 - 0.04, 0.425, 0)); + // translations.appendAssumeCapacity(zm.translation(0.3 / 2.0 + 0.04, 0.425, 0)); + + var chest = shape.clone(); + defer chest.deinit(); + chest.scale(0.3, 0.4, 0.15); + // meshes.appendAssumeCapacity(Mesh.initShape(chest, textures, .triangles)); + // translations.appendAssumeCapacity(zm.translation(0, 0.425 + 0.4, 0)); + + var model_data = Tree(Model.JointData).init( + .{ + .mesh = Mesh.initShape(chest, textures, .triangles), + .translation = zm.translation(0, 0.425 + 0.4, 0), + .rotation = zm.qidentity(), + }, + ); + + var head = shape.clone(); + defer head.deinit(); + head.scale(0.1, 0.1, 0.1); + + try model_data.root.append(allocator, .{ + .mesh = Mesh.initShape(head, textures, .triangles), + .translation = zm.translation(0, 0.425 + 0.4 + 0.1, 0), + .rotation = zm.qidentity(), + }); + // meshes.appendAssumeCapacity(Mesh.initShape(head, textures, .triangles)); + // translations.appendAssumeCapacity(zm.translation(0, 0.425 + 0.4 + 0.1, 0)); + + return Model.init(model_data); +} + +// fn initRobot(allocator: std.mem.Allocator, textures: ?[]const Texture) Model { +// var shape = Shape.initCube(); +// defer shape.deinit(); + +// shape.unweld(); +// shape.computeNormals(); +// shape.translate(-0.5, -1, -0.5); + +// var chest = shape.clone(); +// defer chest.deinit(); +// chest.scale(0.3, 0.4, 0.15); + +// var model_data = Tree(Model.JointData).init(.{ +// .mesh = Mesh.initShape(chest, textures, .triangles), +// .translation = zm.translation(0, 0.425 + 0.4, 0), +// .rotation = zm.qidentity(), +// }); + +// var head = shape.clone(); +// defer head.deinit(); +// head.scale(0.1, 0.1, 0.1); + +// try model_data.root.append(allocator, .{ +// .mesh = Mesh.initShape(head, textures, .triangles), +// .translation = zm.translation(0, 0.425 + 0.4 + 0.1, 0), +// .rotation = zm.qidentity(), +// }); + +// return Model.init(model_data); +// } + +// fn animateRobot(frame_counter: *u16, robot: Model) void { +// if (frame_counter.* > robot.animation_frames) { +// robot.translations.items[2] = zm.mul(zm.rotationX(-std.math.degreesToRadians(1)), robot.translations.items[2]); +// robot.translations.items[3] = zm.mul(zm.rotationX(-std.math.degreesToRadians(1)), robot.translations.items[3]); +// } else { +// robot.translations.items[2] = zm.mul(zm.rotationX(std.math.degreesToRadians(1)), robot.translations.items[2]); +// robot.translations.items[3] = zm.mul(zm.rotationX(std.math.degreesToRadians(1)), robot.translations.items[3]); +// } + +// frame_counter.* += 1; + +// if (frame_counter.* > robot.animation_frames * 2) { +// frame_counter.* = 0; +// } +// } + +fn drawGui(light_ubo: zgl.Buffer, light_shader: gl_helper.Shader, light_color: *zm.Vec) void { + if (app_state.render_gui) { + zgui.backend.newFrame(@intCast(app_state.width), @intCast(app_state.height)); + zgui.setNextWindowPos(.{ .x = 0, .y = 0, .cond = .appearing }); + zgui.setNextWindowSize(.{ .w = @floatFromInt(app_state.width), .h = 0, .cond = .appearing }); + if (zgui.begin( + "##Window", + .{ .flags = window_flags }, + )) { + _ = zgui.radioButtonStatePtr("Robot", .{ .v_button = 0, .v = @ptrCast(&app_state.scene) }); + zgui.sameLine(.{}); + _ = zgui.radioButtonStatePtr("Fish", .{ .v_button = 1, .v = @ptrCast(&app_state.scene) }); + + if (zgui.sliderFloat3( + "Light position", + .{ + .v = @ptrCast(&app_state.light_pos), + .min = -2, + .max = 2, + .cfmt = "%.2f", + .flags = .{ .always_clamp = true }, + }, + )) { + light_ubo.subData(@sizeOf(zm.Vec), zm.Vec, &.{app_state.light_pos}); + light_shader.updateUniformVec4("u_position", &.{app_state.light_pos}); + } + + if (zgui.colorEdit3( + "Light Color", + .{ + .col = @ptrCast(light_color), + .flags = .{}, + }, + )) { + light_ubo.subData(0, zm.Vec, &.{light_color.*}); + } + } + zgui.end(); + zgui.backend.draw(); + } +} + +comptime { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/src/main.zig.bak b/src/main.zig.bak new file mode 100644 index 0000000..86c42e6 --- /dev/null +++ b/src/main.zig.bak @@ -0,0 +1,244 @@ +const std = @import("std"); + +const glfw = @import("zglfw"); +const zgl = @import("zgl"); + +const zm = @import("zmath"); +const Mat = zm.Mat; + +const zmesh = @import("zmesh"); +const Shape = zmesh.Shape; + +const glfw_helper = @import("glfw_helper/root.zig"); +const GlfwContect = glfw_helper.GlfwContext; +const AppState = glfw_helper.AppState; + +const gl_helper = @import("gl_helper/root.zig"); +const Shader = gl_helper.Shader; +const Texture = gl_helper.Texture; +const Mesh = gl_helper.Mesh; +const ByteColor = gl_helper.Color.Byte; +const FloatColor = gl_helper.Color.Float; + +pub const texture = + ByteColor.red ++ ByteColor.gold ++ + ByteColor.gold ++ ByteColor.red; + +pub const pyramid_texture = + ByteColor.violet ++ ByteColor.violet ++ ByteColor.violet ++ ByteColor.violet ++ + ByteColor.blue ++ ByteColor.blue ++ ByteColor.blue ++ ByteColor.blue ++ + ByteColor.green ++ ByteColor.green ++ ByteColor.green ++ ByteColor.green ++ + ByteColor.red ++ ByteColor.red ++ ByteColor.red ++ ByteColor.red; + +var app_state = AppState.init( + 600, + 600, + .diffuse, + zm.f32x4(0, 2, 3, 1), + 3, +); + +pub fn main() !void { + var debug_allocator = std.heap.DebugAllocator(.{}).init; + defer _ = debug_allocator.deinit(); + const allocator = debug_allocator.allocator(); + + const context = try GlfwContect.init(&app_state, "ZGL Test"); + + zmesh.init(allocator); + defer zmesh.deinit(); + + // shaders + var model_shader = try Shader.initFile("shaders/default.vert", "shaders/default.frag"); + defer model_shader.delete(); + + var instanced_shader = try Shader.initFile("shaders/instanced.vert", "shaders/default.frag"); + defer instanced_shader.delete(); + + var diffuse_shader = try Shader.initFile("shaders/default.vert", "shaders/diffuse.frag"); + defer diffuse_shader.delete(); + + var specular_shader = try Shader.initFile("shaders/default.vert", "shaders/specular.frag"); + defer specular_shader.delete(); + + var light_shader = try Shader.initFile("shaders/light.vert", "shaders/light.frag"); + defer light_shader.delete(); + + // textures + const textures: []const Texture = &.{Texture.init(0, .@"2d")}; + textures[0].load(&texture, 2, 2, .rgba8, .rgba, .unsigned_byte); + + const p_textures: []const Texture = &.{Texture.init(0, .@"2d")}; + p_textures[0].load(&pyramid_texture, 4, 4, .rgba8, .rgba, .unsigned_byte); + + // meshes + var floor_shape = Shape.initPlane(1, 1); + defer floor_shape.deinit(); + floor_shape.translate(-0.5, -0.5, 0); + floor_shape.rotate(std.math.degreesToRadians(-90), 1, 0, 0); + floor_shape.scale(6, 1, 6); + if (floor_shape.texcoords) |texcoords| { + for (texcoords) |*coord| { + coord[0] *= 6; + coord[1] *= 6; + } + } + const floor = Mesh.initShape(floor_shape, textures, .triangles); + defer floor.delete(); + + var pyramid_shape = Shape.initCone(4, 1); + defer pyramid_shape.deinit(); + pyramid_shape.rotate(std.math.degreesToRadians(-90), 1, 0, 0); + pyramid_shape.scale(1, 1.5, 1); + for (pyramid_shape.texcoords.?) |*texcoord| { + texcoord[0] *= 3; + } + const pyramid = Mesh.initShape(pyramid_shape, p_textures, .triangles); + defer pyramid.delete(); + + var cube_shape = Shape.initCube(); + defer cube_shape.deinit(); + cube_shape.computeNormals(); + cube_shape.translate(-3, 0, -3); + const cube = Mesh.initShape(cube_shape, textures, .triangles); + defer cube.delete(); + + // instancing uniforms + var cube_transformations: [27]Mat = undefined; + var cube_normals: [27]Mat = undefined; + for (0..3) |i| { + for (0..3) |j| { + for (0..3) |k| { + cube_transformations[i + 3 * (j + k * 3)] = zm.translation( + @as(f32, @floatFromInt(i)) * 2.5, + @as(f32, @floatFromInt(j)) * 2.5, + @as(f32, @floatFromInt(k)) * 2.5, + ); + cube_normals[i + 3 * (j + k * 3)] = zm.transpose(zm.inverse(cube_transformations[i + 3 * (j + k * 3)])); + } + } + } + + // uniform data + const light_color = zm.f32x4s(1); + var pyramid_rotation = zm.quatFromRollPitchYaw(0, std.math.degreesToRadians(45), 0); + var light_pos = zm.f32x4(0, 2, 0, 1); + + // uniform buffer objects + const matrix_ubo = gl_helper.initUbo(zm.Mat, &.{ + model_shader, + instanced_shader, + diffuse_shader, + specular_shader, + light_shader, + }, "SharedMatrices", 0, 1); + defer matrix_ubo.delete(); + + const light_ubo = gl_helper.initUbo(f32, &.{ + model_shader, + instanced_shader, + diffuse_shader, + specular_shader, + }, "Light", 1, 12); + defer light_ubo.delete(); + light_ubo.subData(0, zm.Vec, &.{light_color}); + + // const uniforms + diffuse_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()}); + diffuse_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()}); + specular_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()}); + specular_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()}); + instanced_shader.updateUniformMatrix("u_model_matrix[0]", &cube_transformations); + instanced_shader.updateUniformMatrix("u_normal_matrix[0]", &cube_normals); + light_shader.updateUniformVec4("light_color", &.{light_color}); + + // time handling + var previous_time = glfw_helper.getTime(); + var current_time: f64 = 0; + var delta_time: f64 = undefined; + + // OpenGL setup + const clear_color = FloatColor.sky_blue; + zgl.clearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]); + zgl.enable(.program_point_size); + zgl.enable(.depth_test); + zgl.enable(.cull_face); + + while (!context.window.shouldClose()) : (context.postRender()) { + // input + app_state.camera.move(@floatCast(delta_time)); + moveLight(&light_pos, @floatCast(delta_time)); + + // ubo updates + app_state.camera.updateMatrix(60, 0.1, 200); + matrix_ubo.subData(0, Mat, &.{app_state.camera.matrix}); + + light_ubo.subData(@sizeOf(f32) * 4, zm.Vec, &.{light_pos}); + light_ubo.subData(@sizeOf(f32) * 4 * 2, zm.Vec, &.{app_state.camera.position}); + + // render + zgl.clear(.{ .color = true, .depth = true }); + + switch (app_state.scene) { + .pyramid => { + model_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()}); + model_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()}); + floor.draw(model_shader); + + pyramid_rotation = zm.qmul(pyramid_rotation, zm.quatFromRollPitchYaw(0, @floatCast(-1 * delta_time), 0)); + const pyramid_matrix = zm.quatToMat(pyramid_rotation); + model_shader.updateUniformMatrix("u_model_matrix", &.{pyramid_matrix}); + model_shader.updateUniformMatrix("u_normal_matrix", &.{zm.transpose(zm.inverse(pyramid_matrix))}); + pyramid.draw(model_shader); + }, + .cubes => { + model_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()}); + model_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()}); + floor.draw(model_shader); + + cube.drawInstanced(instanced_shader, 27); + }, + .diffuse => { + floor.draw(diffuse_shader); + }, + .specular => { + floor.draw(specular_shader); + }, + } + + light_shader.use(); + light_shader.updateUniformVec4("u_position", &.{light_pos}); + light_shader.updateUniformVec3("camera_pos", &.{zm.vecToArr3(app_state.camera.position)}); + zgl.drawArrays(.points, 0, 1); + + // time + current_time = glfw_helper.getTime(); + delta_time = current_time - previous_time; + previous_time = current_time; + } +} + +fn moveLight(light_pos: *zm.Vec, delta_time: f32) void { + if (app_state.light_controls.forward) { + light_pos[2] -= 3 * delta_time; + } + if (app_state.light_controls.left) { + light_pos[0] -= 3 * delta_time; + } + if (app_state.light_controls.backward) { + light_pos[2] += 3 * delta_time; + } + if (app_state.light_controls.right) { + light_pos[0] += 3 * delta_time; + } + if (app_state.light_controls.up) { + light_pos[1] += 3 * delta_time; + } + if (app_state.light_controls.down) { + light_pos[1] -= 3 * delta_time; + } +} + +comptime { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/src/tree.zig b/src/tree.zig new file mode 100644 index 0000000..21a31f2 --- /dev/null +++ b/src/tree.zig @@ -0,0 +1,150 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub fn Tree(comptime T: type) type { + return struct { + // const Self = @This(); + + root: Node, + + const Children = std.ArrayList(Node); + const Node = struct { + val: T, + parent: ?*Node, + index: usize, + children: std.ArrayList(Node), + + pub fn init(val: T, parent: ?*Node, index: usize) Node { + return .{ + .val = val, + .children = .empty, + .parent = parent, + .index = index, + }; + } + + pub fn append(self: *Node, allocator: Allocator, val: T) !void { + const child = Node.init(val, self, self.children.items.len); + try self.children.append(allocator, child); + } + }; + + pub fn init(val: T) @This() { + return .{ .root = .init(val, null, 0) }; + } + + pub const DepthFirstIterator = struct { + const State = enum { + GoDeeper, + GoBroader, + }; + tree: *Tree(T), + current: ?*Node, + state: State, + + pub fn init(tree: *Tree(T)) DepthFirstIterator { + return DepthFirstIterator{ + .tree = tree, + .current = &tree.root, + .state = State.GoDeeper, + }; + } + + pub fn next(it: *DepthFirstIterator) ?*Node { + // State machine + while (it.current) |current| { + switch (it.state) { + State.GoDeeper => { + // Follow child node until deepest possible level + if (current.children.items.len > 0) { + it.current = ¤t.children.items[0]; + } else { + it.state = State.GoBroader; + return current; + } + }, + State.GoBroader => { + if (current.parent) |parent| { + if (parent.children.items.len - 1 > (current.index)) { + it.current = &parent.children.items[current.index + 1]; + it.state = .GoDeeper; + } else { + it.current = current.parent; + return current.parent; + } + } else { + return null; + } + }, + } + } + return null; + } + + pub fn reset(it: *DepthFirstIterator) void { + it.current = it.tree.root; + } + }; + + pub fn depthFirstIterator(tree: *@This()) DepthFirstIterator { + return DepthFirstIterator.init(tree); + } + + pub fn deinit(self: *@This(), allocator: Allocator) void { + var iterator = self.depthFirstIterator(); + while (iterator.next()) |node| { + node.children.deinit(allocator); + } + } + }; +} + +pub fn main() !void { + var debug_allocator = std.heap.DebugAllocator(.{}).init; + defer _ = debug_allocator.deinit(); + const allocator = debug_allocator.allocator(); + + const UTree = Tree(u16); + + var tree = UTree.init(10); + defer tree.deinit(allocator); + + try tree.root.append(allocator, 20); + try tree.root.append(allocator, 500); + + var iterator = tree.depthFirstIterator(); + std.debug.print("\n", .{}); + while (iterator.next()) |node| { + var accumulator: u32 = node.val; + var current = node; + while (current.parent) |parent| { + accumulator *= parent.val; + current = parent; + } + std.debug.print("Val: {} Acc: {}\n", .{ node.val, accumulator }); + } +} + +test "Tree" { + const allocator = std.testing.allocator; + + const UTree = Tree(u16); + + var tree = UTree.init(10); + defer tree.deinit(allocator); + + try tree.root.append(allocator, 20); + try tree.root.append(allocator, 500); + + var iterator = tree.depthFirstIterator(); + std.debug.print("\n", .{}); + while (iterator.next()) |node| { + var accumulator: u32 = node.val; + var current = node; + while (current.parent) |parent| { + accumulator *= parent.val; + current = parent; + } + std.debug.print("Val: {} Acc: {}\n", .{ node.val, accumulator }); + } +} diff --git a/src/tree.zig.bak b/src/tree.zig.bak new file mode 100644 index 0000000..b0ab39d --- /dev/null +++ b/src/tree.zig.bak @@ -0,0 +1,319 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const testing = std.testing; +const assert = std.debug.assert; +const FixedBufferAllocator = std.heap.FixedBufferAllocator; + +/// Generic k-ary tree represented as a "left-child right-sibling" binary tree. +pub fn Tree(comptime T: type) type { + return struct { + const Self = @This(); + root: Node, + + /// Node inside the tree. + pub const Node = struct { + value: T, + parent: ?*Node, + leftmost_child: ?*Node, + right_sibling: ?*Node, + + fn init(value: T) Node { + return Node{ + .value = value, + .parent = null, + .leftmost_child = null, + .right_sibling = null, + }; + } + }; + + /// Initialize a tree. + /// + /// Arguments: + /// value: Value (aka weight, key, etc.) of the root node. + /// + /// Returns: + /// A tree containing one node with specified value. + pub fn init(value: T) Self { + return Self{ + .root = Node.init(value), + }; + } + + /// Allocate a new node. Caller owns returned Node and must free with `destroyNode`. + /// + /// Arguments: + /// allocator: Dynamic memory allocator. + /// + /// Returns: + /// A pointer to the new node. + /// + /// Errors: + /// If a new node cannot be allocated. + pub fn allocateNode(tree: *Self, allocator: *Allocator) !*Node { + _ = tree; // autofix + return allocator.create(Node); + } + + /// Deallocate a node. Node must have been allocated with `allocator`. + /// + /// Arguments: + /// node: Pointer to the node to deallocate. + /// allocator: Dynamic memory allocator. + pub fn destroyNode(tree: *Self, node: *Node, allocator: *Allocator) void { + assert(tree.containsNode(node)); + allocator.destroy(node); + } + + /// Allocate and initialize a node and its value. + /// + /// Arguments: + /// value: Value (aka weight, key, etc.) of newly created node. + /// allocator: Dynamic memory allocator. + /// + /// Returns: + /// A pointer to the new node. + /// + /// Errors: + /// If a new node cannot be allocated. + pub fn createNode(tree: *Self, value: T, allocator: *Allocator) !*Node { + const node = try tree.allocateNode(allocator); + node.* = Node.init(value); + return node; + } + + /// Insert a node at the specified position inside the tree. + /// + /// Arguments: + /// node: Pointer to the node to insert. + /// parent: Pointer to node which the newly created node will be a child of. + /// + /// Returns: + /// A pointer to the new node. + pub fn insert(tree: *Self, node: *Node, parent: *Node) void { + _ = tree; // autofix + node.parent = parent; + node.right_sibling = parent.leftmost_child; + parent.leftmost_child = node; + } + + /// Add another tree at the specified position inside this tree. + /// + /// Arguments: + /// other: Pointer to the tree to insert. + /// parent: Pointer to node which the newly created node will be a parent of. + pub fn graft(tree: *Self, other: *Self, parent: *Node) void { + _ = tree; // autofix + _ = other; // autofix + _ = parent; // autofix + } + + /// Remove (detach) a "branch" from the tree (remove node and all its descendants). + /// Does nothing when applied to root node. + /// + /// Arguments: + /// node: Pointer to node to be removed. + pub fn prune(tree: *Self, node: *Node) void { + assert(tree.containsNode(node)); + if (node.parent) |parent| { + var ptr = &parent.leftmost_child; + while (ptr.*) |sibling| : (ptr = &sibling.right_sibling) { + if (sibling == node) { + ptr.* = node.right_sibling; + break; + } + } + node.right_sibling = null; + node.parent = null; + } + } + + /// Remove a node preserving all its children, which take its place inside the tree. + /// Does nothing when applied to root node. + /// + /// Arguments: + /// node: Pointer to node to be removed. + pub fn remove(tree: *Self, node: *Node) void { + assert(tree.containsNode(node)); + if (node.parent) |parent| { + var ptr = &parent.leftmost_child; + while (ptr.*) |sibling| : (ptr = &sibling.right_sibling) { + if (sibling == node) break; + } + ptr.* = node.leftmost_child; + while (ptr.*) |old_child| : (ptr = &old_child.right_sibling) { + old_child.parent = parent; + } + ptr.* = node.right_sibling; + node.parent = null; + node.leftmost_child = null; + node.right_sibling = null; + } + } + + /// Iterator that performs a depth-first post-order traversal of the tree. + /// It is non-recursive and uses constant memory (no allocator needed). + pub const DepthFirstIterator = struct { + const State = enum { + GoDeeper, + GoBroader, + }; + tree: *Self, + current: ?*Node, + state: State, + + // NB: + // If not children_done: + // Go as deep as possible + // Yield node + // If can move right: + // children_done = false; + // Move right + // Else: + // children_done = true; + // Move up + + pub fn init(tree: *Self) DepthFirstIterator { + return DepthFirstIterator{ + .tree = tree, + .current = &tree.root, + .state = State.GoDeeper, + }; + } + + pub fn next(it: *DepthFirstIterator) ?*Node { + // State machine + while (it.current) |current| { + switch (it.state) { + State.GoDeeper => { + // Follow child node until deepest possible level + if (current.leftmost_child) |child| { + it.current = child; + } else { + it.state = State.GoBroader; + return current; + } + }, + State.GoBroader => { + if (current.right_sibling) |sibling| { + it.current = sibling; + it.state = State.GoDeeper; + } else { + it.current = current.parent; + return current.parent; + } + }, + } + } + return null; + } + + pub fn reset(it: *DepthFirstIterator) void { + it.current = it.tree.root; + } + }; + + /// Get a depth-first iterator over the nodes of this tree. + /// + /// Returns: + /// An iterator struct (one containing `next` and `reset` member functions). + pub fn depthFirstIterator(tree: *Self) DepthFirstIterator { + return DepthFirstIterator.init(tree); + } + + /// Check if a node is contained in this tree. + /// + /// Arguments: + /// target: Pointer to node to be searched for. + /// + /// Returns: + /// A bool telling whether it has been found. + pub fn containsNode(tree: *Self, target: *Node) bool { + var iter = tree.depthFirstIterator(); + while (iter.next()) |node| { + if (node == target) { + return true; + } + } + return false; + } + }; +} + +test "tree node insertion" { + var tree = Tree(u32).init(1); + + const allocator = std.debug.global_allocator; + //var buffer: [5000]u8 = undefined; + // const allocator = &FixedBufferAllocator.init(buffer[0..]).allocator; + + const two = try tree.createNode(2, allocator); + const three = try tree.createNode(3, allocator); + const four = try tree.createNode(4, allocator); + const five = try tree.createNode(5, allocator); + const six = try tree.createNode(6, allocator); + const fortytwo = try tree.createNode(42, allocator); + defer { + tree.destroyNode(two, allocator); + tree.destroyNode(three, allocator); + tree.destroyNode(four, allocator); + tree.destroyNode(five, allocator); + tree.destroyNode(six, allocator); + allocator.destroy(fortytwo); + } + + tree.insert(two, &tree.root); + tree.insert(three, &tree.root); + tree.insert(four, &tree.root); + tree.insert(five, two); + tree.insert(six, two); + + testing.expect(tree.root.value == 1); + testing.expect(two.parent == &tree.root); + testing.expect(three.parent == &tree.root); + testing.expect(tree.containsNode(four)); + testing.expect(five.parent == two); + testing.expect(!tree.containsNode(fortytwo)); + + var iter = tree.depthFirstIterator(); + while (iter.next()) |node| { + std.debug.warn("{} ", node.value); + } +} + +test "tree node removal" { + var tree = Tree(u32).init(1); + + const allocator = std.debug.global_allocator; + + const two = try tree.createNode(2, allocator); + const three = try tree.createNode(3, allocator); + const four = try tree.createNode(4, allocator); + const five = try tree.createNode(5, allocator); + const six = try tree.createNode(6, allocator); + defer { + tree.destroyNode(three, allocator); + tree.destroyNode(four, allocator); + allocator.destroy(two); + allocator.destroy(five); + allocator.destroy(six); + } + + tree.insert(two, &tree.root); + tree.insert(three, &tree.root); + tree.insert(four, &tree.root); + tree.insert(five, two); + tree.insert(six, two); + + tree.prune(two); + + testing.expect(tree.containsNode(three)); + testing.expect(tree.containsNode(four)); + testing.expect(!tree.containsNode(two)); + testing.expect(!tree.containsNode(five)); + testing.expect(!tree.containsNode(six)); + var iter = tree.depthFirstIterator(); + while (iter.next()) |node| { + std.debug.warn("{} ", node.value); + } +} diff --git a/textures/earth.jpg b/textures/earth.jpg new file mode 100644 index 0000000..2e753ce Binary files /dev/null and b/textures/earth.jpg differ diff --git a/textures/planks.png b/textures/planks.png new file mode 100644 index 0000000..4eeb835 Binary files /dev/null and b/textures/planks.png differ diff --git a/textures/planks_spec.png b/textures/planks_spec.png new file mode 100644 index 0000000..e8649a9 Binary files /dev/null and b/textures/planks_spec.png differ diff --git a/textures/skybox/back.jpg b/textures/skybox/back.jpg new file mode 100644 index 0000000..470a679 Binary files /dev/null and b/textures/skybox/back.jpg differ diff --git a/textures/skybox/bottom.jpg b/textures/skybox/bottom.jpg new file mode 100644 index 0000000..893f394 Binary files /dev/null and b/textures/skybox/bottom.jpg differ diff --git a/textures/skybox/front.jpg b/textures/skybox/front.jpg new file mode 100644 index 0000000..4e17b77 Binary files /dev/null and b/textures/skybox/front.jpg differ diff --git a/textures/skybox/left.jpg b/textures/skybox/left.jpg new file mode 100644 index 0000000..5750b91 Binary files /dev/null and b/textures/skybox/left.jpg differ diff --git a/textures/skybox/right.jpg b/textures/skybox/right.jpg new file mode 100644 index 0000000..8963037 Binary files /dev/null and b/textures/skybox/right.jpg differ diff --git a/textures/skybox/top.jpg b/textures/skybox/top.jpg new file mode 100644 index 0000000..4db3c2a Binary files /dev/null and b/textures/skybox/top.jpg differ diff --git a/textures/textures/plank_flooring_04_ao_1k.jpg b/textures/textures/plank_flooring_04_ao_1k.jpg new file mode 100644 index 0000000..db71746 Binary files /dev/null and b/textures/textures/plank_flooring_04_ao_1k.jpg differ diff --git a/textures/textures/plank_flooring_04_arm_1k.jpg b/textures/textures/plank_flooring_04_arm_1k.jpg new file mode 100644 index 0000000..3f4c129 Binary files /dev/null and b/textures/textures/plank_flooring_04_arm_1k.jpg differ diff --git a/textures/textures/plank_flooring_04_diff_1k.jpg b/textures/textures/plank_flooring_04_diff_1k.jpg new file mode 100644 index 0000000..3698a88 Binary files /dev/null and b/textures/textures/plank_flooring_04_diff_1k.jpg differ diff --git a/textures/textures/plank_flooring_04_disp_1k.jpg b/textures/textures/plank_flooring_04_disp_1k.jpg new file mode 100644 index 0000000..442f090 Binary files /dev/null and b/textures/textures/plank_flooring_04_disp_1k.jpg differ diff --git a/textures/textures/plank_flooring_04_nor_gl_1k.jpg b/textures/textures/plank_flooring_04_nor_gl_1k.jpg new file mode 100644 index 0000000..5545b5c Binary files /dev/null and b/textures/textures/plank_flooring_04_nor_gl_1k.jpg differ diff --git a/textures/textures/plank_flooring_04_rough_1k.jpg b/textures/textures/plank_flooring_04_rough_1k.jpg new file mode 100644 index 0000000..dd16f85 Binary files /dev/null and b/textures/textures/plank_flooring_04_rough_1k.jpg differ