diff --git a/build.zig b/build.zig index 642cbf1..8a850ad 100644 --- a/build.zig +++ b/build.zig @@ -11,42 +11,93 @@ pub fn build(b: *Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const exe = stepBuildMain(b, target, optimize); - b.installArtifact(exe); + const sdl_module, const sdl_step = stepSdlModule(b, target, optimize); + + const client_exe = stepBuildClient(b, target, optimize, sdl_module, sdl_step); + const client_install = b.addInstallArtifact(client_exe, .{}); + + const server_exe = stepBuildServer(b, target, optimize); + const server_install = b.addInstallArtifact(server_exe, .{}); + + const offline_exe = stepBuildOffline(b, target, optimize, sdl_module, sdl_step); + const offline_install = b.addInstallArtifact(offline_exe, .{}); const copy_data = stepCopyData(b, target, optimize); + + b.getInstallStep().dependOn(&client_install.step); + b.getInstallStep().dependOn(&server_install.step); + b.getInstallStep().dependOn(&offline_install.step); b.getInstallStep().dependOn(copy_data); - const run = b.addRunArtifact(exe); - run.step.dependOn(b.getInstallStep()); + const run = b.addRunArtifact(offline_exe); + run.step.dependOn(&offline_install.step); + run.step.dependOn(copy_data); - // Why is this not the default behavoir? + // Why is this not the default behavior? run.setCwd(b.path(std.fs.path.relative(b.allocator, b.build_root.path.?, b.exe_dir) catch unreachable)); if (b.args) |args| { run.addArgs(args); } - const run_step = b.step("run", "Run the app"); + const run_step = b.step("run", "Build and Run tabletop in offline mode"); run_step.dependOn(&run.step); - const check_step = b.step("check", "Check for build errors"); - check_step.dependOn(&exe.step); + const check_step = b.step("check", "Check for build errors (offline build only)"); + check_step.dependOn(&offline_exe.step); } -fn stepBuildMain( +fn stepBuildClient( + b: *Build, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + sdl_module: *Build.Module, + sdl_step: *Build.Step, +) *Build.Step.Compile { + const exe = b.addExecutable(.{ + .name = "tabletop_client", + .root_source_file = b.path("src/client.zig"), + .target = target, + .optimize = optimize, + }); + + exe.root_module.addImport("sdl", sdl_module); + exe.step.dependOn(sdl_step); + + exe.addIncludePath(b.path("lib/clibs")); + + return exe; +} + +fn stepBuildServer( b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) *Build.Step.Compile { const exe = b.addExecutable(.{ - .name = "spacefarer", - .root_source_file = b.path("src/main.zig"), + .name = "tabletop_server", + .root_source_file = b.path("src/server.zig"), + .target = target, + .optimize = optimize, + }); + + return exe; +} + +fn stepBuildOffline( + b: *Build, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + sdl_module: *Build.Module, + sdl_step: *Build.Step, +) *Build.Step.Compile { + const exe = b.addExecutable(.{ + .name = "tabletop", + .root_source_file = b.path("src/offline.zig"), .target = target, .optimize = optimize, }); - const sdl_module, const sdl_step = stepSdlModule(b, target, optimize); exe.root_module.addImport("sdl", sdl_module); exe.step.dependOn(sdl_step); @@ -58,13 +109,12 @@ fn stepBuildMain( fn stepBuildSdlTranslator( b: *Build, target: Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, ) *Build.Step.Compile { const sdl_translator = b.addExecutable(.{ .name = "sdl_header_translator", .root_source_file = b.path("utils/sdl_translator.zig"), .target = target, - .optimize = optimize, + .optimize = .Debug, }); sdl_translator.linkSystemLibrary("SDL3"); return sdl_translator; @@ -73,9 +123,8 @@ fn stepBuildSdlTranslator( fn stepTranslateSdl( b: *Build, target: Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, ) struct { *Build.Step, Build.LazyPath } { - const sdl_translator = stepBuildSdlTranslator(b, target, optimize); + const sdl_translator = stepBuildSdlTranslator(b, target); const translate = b.addRunArtifact(sdl_translator); const sdl_rename = translate.addOutputFileArg("sdl_rename.h"); return .{ @@ -97,7 +146,7 @@ fn stepSdlModule( }); sdl_module.linkSystemLibrary("SDL3", .{}); - const translate_step, const sdl_rename = stepTranslateSdl(b, target, optimize); + const translate_step, const sdl_rename = stepTranslateSdl(b, target); sdl_module.addIncludePath(sdl_rename.dirname()); return .{ diff --git a/build.zig.zon b/build.zig.zon index a480fc1..4c61daa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,8 +1,8 @@ .{ - .name = .spacefarer, + .name = .tabletop, .version = "0.0.0", - .fingerprint = 0x946ddccb5911fb15, - .minimum_zig_version = "0.15.0", + .fingerprint = 0x9467f2a6727b4ca5, + .minimum_zig_version = "0.14.0", .dependencies = .{}, .paths = .{ "build.zig", diff --git a/data/hand.png b/data/hand.png new file mode 100644 index 0000000..d9e507e Binary files /dev/null and b/data/hand.png differ diff --git a/data/shaders/basic.frag b/data/shaders/basic.frag index ec61ee5..99b85b5 100644 --- a/data/shaders/basic.frag +++ b/data/shaders/basic.frag @@ -7,4 +7,7 @@ layout(set = 2, binding = 0) uniform sampler2D texture_sampler; void main() { fragColor = texture(texture_sampler, inUV); + if (fragColor.a < 0.5) { + discard; + } } diff --git a/data/shaders/basic.vert b/data/shaders/basic.vert index 00fe354..3797185 100644 --- a/data/shaders/basic.vert +++ b/data/shaders/basic.vert @@ -13,5 +13,6 @@ layout(set = 1, binding = 1) uniform Object{ void main() { gl_Position = vec4(inCoord, 1.0) * object.transform * camera.transform; + gl_ClipDistance[0] = gl_Position.z; outUV = inUV; } diff --git a/data/yakuza.png b/data/yakuza.png new file mode 100644 index 0000000..7ecec14 Binary files /dev/null and b/data/yakuza.png differ diff --git a/src/main.zig b/src/client.zig similarity index 100% rename from src/main.zig rename to src/client.zig diff --git a/src/entity.zig b/src/entity.zig index 073d339..e69de29 100644 --- a/src/entity.zig +++ b/src/entity.zig @@ -1,123 +0,0 @@ -const std = @import("std"); -const sdl = @import("sdl"); -const Game = @import("game.zig"); -const Graphics = @import("graphics.zig"); -const Time = @import("time.zig"); -const World = @import("world.zig"); -const math = @import("math.zig"); - -position: @Vector(2, i32), -player: bool = false, -enemy: bool = false, -controller: Controller = .{}, -next_update: Time = Time.ZERO, -sway_x: math.Sway(f32) = .{ .amplitude = 1, .frequency = 0.11 }, -sway_y: math.Sway(f32) = .{ .amplitude = 1, .frequency = 0.13 }, - -const Controller = struct { - const Action = union(enum) { - move: @Vector(2, i32), - }; - wanted_action: ?Action = null, - move_units: f32 = 0.125, -}; - -const Self = @This(); -pub fn update(self: *Self) void { - if (!World.time.past(self.next_update)) return; - - if (self.player) self.updatePlayer(); - if (self.enemy) self.updateEnemy(); - self.updateController(); -} - -pub fn updatePlayer(self: *Self) void { - var delta: @Vector(2, i32) = .{ 0, 0 }; - if (Game.keyboard.keys.is_pressed(sdl.SCANCODE_UP)) { - delta[1] += 1; - } - if (Game.keyboard.keys.is_pressed(sdl.SCANCODE_DOWN)) { - delta[1] -= 1; - } - if (Game.keyboard.keys.is_pressed(sdl.SCANCODE_RIGHT)) { - delta[0] += 1; - } - if (Game.keyboard.keys.is_pressed(sdl.SCANCODE_LEFT)) { - delta[0] -= 1; - } - if (@reduce(.Or, delta != @Vector(2, i32){ 0, 0 })) - self.controller.wanted_action = .{ .move = delta } - else - self.controller.wanted_action = null; -} - -fn updateEnemy(self: *Self) void { - if (World.getPlayer()) |player| { - var delta = player.position - self.position; - if (@reduce(.And, @abs(delta) <= @Vector(2, i64){ 1, 1 })) { - self.controller.wanted_action = null; - } else { - delta[0] = @max(-1, @min(1, delta[0])); - delta[1] = @max(-1, @min(1, delta[1])); - self.controller.wanted_action = .{ .move = delta }; - } - } -} - -fn updateController(self: *Self) void { - if (self.controller.wanted_action) |action| { - switch (action) { - .move => |delta| { - const target = self.position + delta; - if (World.isFree(target)) { - self.next_update = World.time.offset(self.controller.move_units * math.lengthInt(delta)); - self.position[0] += delta[0]; - self.position[1] += delta[1]; - } - }, - } - } -} - -pub fn draw(self: *Self, delta: f32) void { - self.sway_x.update(delta); - self.sway_y.update(delta); - const transform = Graphics.Transform{ - .rotation = Graphics.Transform.rotationByAxis(.{ self.sway_x.value, self.sway_y.value, 0 }, 0.05), - .position = .{ - @floatFromInt(self.position[0]), - @floatFromInt(self.position[1]), - 0.5, - }, - }; - Graphics.drawMesh(World.plane_mesh, World.texture, transform.matrix()); - - if (!self.player) return; - - Graphics.camera.transform.position = math.lerpTimeLn( - Graphics.camera.transform.position, - transform.position + @Vector(3, f32){ 0.0, -1.0, 4.0 }, - delta, - -25, - ); - - const ORIGIN_DIR = @Vector(3, f32){ 0.0, 0.0, -1.0 }; - const INIT_ROTATION = Graphics.Transform.rotationByAxis(.{ 1.0, 0.0, 0.0 }, std.math.pi * 0.5); - - const ROTATED_DIR = Graphics.Transform.rotateVector(ORIGIN_DIR, INIT_ROTATION); - - const target_rotation = Graphics.Transform.combineRotations( - INIT_ROTATION, - Graphics.Transform.rotationToward( - ROTATED_DIR, - transform.position - Graphics.camera.transform.position, - .{ .normalize_to = true }, - ), - ); - Graphics.camera.transform.rotation = Graphics.Transform.normalizeRotation(math.slerpTimeLn( - Graphics.camera.transform.rotation, - target_rotation, - delta, - -2, - )); -} diff --git a/src/game.zig b/src/game.zig index 9cae0f6..0b084a0 100644 --- a/src/game.zig +++ b/src/game.zig @@ -27,7 +27,16 @@ pub fn init(game_alloc: std.mem.Allocator) void { Game.running = false; Game.time = Time{ .now = 0, .delta = 0 }; Game.keyboard = .{}; - Game.mouse = .{ .x = 0, .y = 0, .dx = 0, .dy = 0 }; + Game.mouse = .{ + .buttons = .{}, + .x_screen = 0, + .y_screen = 0, + .x_norm = 0, + .y_norm = 0, + .dx = 0, + .dy = 0, + .wheel = 0, + }; Graphics.create(); Assets.init(); World.initDebug(); @@ -46,9 +55,11 @@ pub fn run() void { } else err.sdl(); Game.processEvents(); - World.updateReal(Game.time.delta); + Game.mouse.x_norm = (Game.mouse.x_screen / @as(f32, @floatFromInt(Graphics.getWidth()))) * 2 - 1; + Game.mouse.y_norm = (Game.mouse.y_screen / @as(f32, @floatFromInt(Graphics.getHeight()))) * -2 + 1; + World.update(Game.time.delta); if (Game.beginDraw()) { - World.draw(Game.time.delta); + World.draw(); Game.endDraw(); } } @@ -66,6 +77,7 @@ fn processEvents() void { Game.mouse.dx = 0.0; Game.mouse.dy = 0.0; Game.keyboard.keys.reset(); + Game.mouse.reset(); sdl.PumpEvents(); while (true) { @@ -83,8 +95,8 @@ fn processEvents() void { }, sdl.EVENT_MOUSE_MOTION => { if (event.motion.windowID != Graphics.windowId()) continue; - Game.mouse.x = event.motion.x; - Game.mouse.y = event.motion.y; + Game.mouse.x_screen = event.motion.x; + Game.mouse.y_screen = event.motion.y; Game.mouse.dx += event.motion.xrel; Game.mouse.dy += event.motion.yrel; }, @@ -104,6 +116,9 @@ fn processEvents() void { if (event.button.windowID != Graphics.windowId()) continue; Game.mouse.buttons.release(event.button.button); }, + sdl.EVENT_MOUSE_WHEEL => { + Game.mouse.wheel += event.wheel.integer_y; + }, else => {}, } } diff --git a/src/graphics.zig b/src/graphics.zig index 141712c..b1b9602 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -13,11 +13,11 @@ pub const Mesh = struct { }; var window: *sdl.Window = undefined; -var renderer: *sdl.Renderer = undefined; var device: *sdl.GPUDevice = undefined; /// Only available while drawing var command_buffer: ?*sdl.GPUCommandBuffer = null; var render_pass: ?*sdl.GPURenderPass = null; +var render_target: ?*sdl.GPUTexture = null; var shader_vert: *sdl.GPUShader = undefined; var shader_frag: *sdl.GPUShader = undefined; @@ -30,10 +30,12 @@ var transfer_buffer: *sdl.GPUTransferBuffer = undefined; var transfer_buffer_capacity: usize = undefined; var depth_texture: *sdl.GPUTexture = undefined; -var msaa_resolve: *sdl.GPUTexture = undefined; +var fsaa_target: *sdl.GPUTexture = undefined; var pipeline: *sdl.GPUGraphicsPipeline = undefined; var window_size: [2]u32 = undefined; +var fsaa_scale: u32 = 4; +var fsaa_level: u32 = 3; pub var camera: Camera = undefined; @@ -41,27 +43,27 @@ var to_resize: ?[2]u32 = null; const VERTEX_BUFFER_DEFAULT_CAPACITY = 1024; const VERTEX_BUFFER_GROWTH_MULTIPLIER = 2; -const TRANSFER_BUFFER_DEFAULT_CAPACITY = 4096; +const TRANSFER_BUFFER_DEFAULT_CAPACITY = 4096 * 1024; const BYTES_PER_VERTEX = 5 * 4; +const DEPTH_FORMAT = sdl.GPU_TEXTUREFORMAT_D32_FLOAT; +const MIP_LEVEL = 4; const Graphics = @This(); pub fn create() void { // Init if (!sdl.Init(sdl.INIT_VIDEO | sdl.INIT_EVENTS)) err.sdl(); + if (!sdl.SetHint(sdl.HINT_LOGGING, "*=info")) err.sdl(); + if (!sdl.SetHint(sdl.HINT_GPU_DRIVER, "vulkan")) err.sdl(); // Window and Renderer - if (!sdl.CreateWindowAndRenderer( + Graphics.window = sdl.CreateWindow( "", 1600, 900, sdl.WINDOW_VULKAN | sdl.WINDOW_RESIZABLE, - @ptrCast(&Graphics.window), - @ptrCast(&Graphics.renderer), - )) err.sdl(); + ) orelse err.sdl(); Graphics.window_size = .{ 1600, 900 }; - if (!sdl.SetRenderVSync(renderer, sdl.RENDERER_VSYNC_ADAPTIVE)) err.sdl(); - // Device Graphics.device = sdl.CreateGPUDevice( sdl.GPU_SHADERFORMAT_SPIRV, @@ -113,8 +115,20 @@ pub fn create() void { var window_height: c_int = 1; if (!sdl.GetWindowSizeInPixels(Graphics.window, &window_width, &window_height)) err.sdl(); - Graphics.depth_texture = createDepthTexture(@intCast(window_width), @intCast(window_height)); - Graphics.msaa_resolve = createTexture(@intCast(window_width), @intCast(window_height), target_format); + Graphics.depth_texture = createTexture( + @as(u32, @intCast(window_width)) * Graphics.fsaa_scale, + @as(u32, @intCast(window_height)) * Graphics.fsaa_scale, + DEPTH_FORMAT, + sdl.GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, + 1, + ); + Graphics.fsaa_target = createTexture( + @as(u32, @intCast(window_width)) * Graphics.fsaa_scale, + @as(u32, @intCast(window_height)) * Graphics.fsaa_scale, + target_format, + sdl.GPU_TEXTUREUSAGE_COLOR_TARGET | sdl.GPU_TEXTUREUSAGE_SAMPLER, + fsaa_level, + ); Graphics.pipeline = sdl.CreateGPUGraphicsPipeline(Graphics.device, &.{ .vertex_shader = Graphics.shader_vert, @@ -144,12 +158,9 @@ pub fn create() void { }, .primitive_type = sdl.GPU_PRIMITIVETYPE_TRIANGLELIST, .rasterizer_state = presets.RASTERIZER_CULL, - .multisample_state = .{ - .sample_count = sdl.GPU_SAMPLECOUNT_4, - }, .depth_stencil_state = presets.DEPTH_ENABLED, .target_info = .{ - .depth_stencil_format = sdl.GPU_TEXTUREFORMAT_D16_UNORM, + .depth_stencil_format = DEPTH_FORMAT, .color_target_descriptions = &sdl.GPUColorTargetDescription{ .format = target_format, .blend_state = presets.BLEND_NORMAL, @@ -161,20 +172,20 @@ pub fn create() void { Graphics.camera = Camera{ .transform = .{}, - .near = 1.0, - .far = 1024.0, + .near = 1.0 / 16.0, + .far = 128.0, .lens = 1.5, .aspect = 16.0 / 9.0, + .matrix = undefined, }; } pub fn destroy() void { sdl.ReleaseWindowFromGPUDevice(Graphics.device, Graphics.window); - sdl.DestroyRenderer(Graphics.renderer); sdl.DestroyWindow(Graphics.window); sdl.ReleaseGPUGraphicsPipeline(Graphics.device, Graphics.pipeline); - sdl.ReleaseGPUTexture(Graphics.device, Graphics.msaa_resolve); + sdl.ReleaseGPUTexture(Graphics.device, Graphics.fsaa_target); sdl.ReleaseGPUTexture(Graphics.device, Graphics.depth_texture); sdl.ReleaseGPUBuffer(Graphics.device, Graphics.vertex_buffer); sdl.ReleaseGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer); @@ -190,25 +201,22 @@ pub fn destroy() void { } pub fn loadTexture(width: u32, height: u32, texture_bytes: []const u8) struct { *sdl.GPUTexture, *sdl.GPUSampler } { - // const target_format = sdl.SDL_GetGPUSwapchainTextureFormat(Graphics.device, Graphics.window); const target_format = sdl.GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; - const texture = sdl.CreateGPUTexture(Graphics.device, &sdl.GPUTextureCreateInfo{ - .format = target_format, - .layer_count_or_depth = 1, - .width = width, - .height = height, - .num_levels = 1, - .sample_count = sdl.GPU_SAMPLECOUNT_1, - .usage = sdl.GPU_TEXTUREUSAGE_SAMPLER, - }) orelse err.sdl(); + const texture = Graphics.createTexture( + width, + height, + target_format, + sdl.GPU_TEXTUREUSAGE_SAMPLER | sdl.GPU_TEXTUREUSAGE_COLOR_TARGET, + MIP_LEVEL, + ); const temp_command_buffer = sdl.AcquireGPUCommandBuffer(Graphics.device) orelse err.sdl(); { const copy_pass = sdl.BeginGPUCopyPass(temp_command_buffer) orelse err.sdl(); defer sdl.EndGPUCopyPass(copy_pass); - const map: [*]u8 = @ptrCast(sdl.MapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer, false) orelse err.sdl()); + const map: [*]u8 = @ptrCast(sdl.MapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer, true) orelse err.sdl()); @memcpy(map, texture_bytes); sdl.UnmapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer); @@ -229,6 +237,7 @@ pub fn loadTexture(width: u32, height: u32, texture_bytes: []const u8) struct { .d = 1, }, false); } + sdl.GenerateMipmapsForGPUTexture(temp_command_buffer, texture); if (!sdl.SubmitGPUCommandBuffer(temp_command_buffer)) err.sdl(); const sampler = sdl.CreateGPUSampler(Graphics.device, &sdl.GPUSamplerCreateInfo{ @@ -237,6 +246,10 @@ pub fn loadTexture(width: u32, height: u32, texture_bytes: []const u8) struct { .address_mode_w = sdl.GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE, .mag_filter = sdl.GPU_FILTER_NEAREST, .min_filter = sdl.GPU_FILTER_LINEAR, + .mipmap_mode = sdl.GPU_SAMPLERMIPMAPMODE_LINEAR, + .min_lod = 0, + .max_lod = 16, + .mip_lod_bias = -2, }) orelse err.sdl(); return .{ @@ -261,7 +274,7 @@ pub fn loadMesh(mesh_bytes: []const u8) Mesh { Graphics.growVertexBuffer(Graphics.vertex_buffer_capacity * size_mult); } - const map = sdl.MapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer, false) orelse err.sdl(); + const map = sdl.MapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer, true) orelse err.sdl(); @memcpy(@as([*]u8, @ptrCast(map)), mesh_bytes); sdl.UnmapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer); @@ -353,22 +366,19 @@ pub fn beginDraw() bool { Graphics.to_resize = null; } - var render_target: ?*sdl.GPUTexture = null; var width: u32 = 0; var height: u32 = 0; - if (!sdl.WaitAndAcquireGPUSwapchainTexture(Graphics.command_buffer, Graphics.window, &render_target, &width, &height)) err.sdl(); - // Hidden - if (render_target == null) return false; + if (!sdl.WaitAndAcquireGPUSwapchainTexture(Graphics.command_buffer, Graphics.window, &Graphics.render_target, &width, &height)) err.sdl(); + // Window is probably hidden + if (Graphics.render_target == null) return false; Graphics.render_pass = sdl.BeginGPURenderPass(Graphics.command_buffer, &.{ .clear_color = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0 }, .cycle = false, .load_op = sdl.GPU_LOADOP_CLEAR, - .store_op = sdl.GPU_STOREOP_RESOLVE, - // .store_op = sdl.GPU_STOREOP_STORE, - .resolve_texture = render_target, + .store_op = sdl.GPU_STOREOP_STORE, .mip_level = 0, - .texture = Graphics.msaa_resolve, + .texture = Graphics.fsaa_target, }, 1, &.{ .clear_depth = 1.0, .load_op = sdl.GPU_LOADOP_CLEAR, @@ -380,7 +390,8 @@ pub fn beginDraw() bool { sdl.BindGPUGraphicsPipeline(Graphics.render_pass, Graphics.pipeline); sdl.BindGPUVertexBuffers(Graphics.render_pass, 0, &.{ .offset = 0, .buffer = Graphics.vertex_buffer }, 1); - sdl.PushGPUVertexUniformData(Graphics.command_buffer, 0, &Graphics.camera.matrix(), 16 * 4); + Graphics.camera.computeMatrix(); + sdl.PushGPUVertexUniformData(Graphics.command_buffer, 0, &Graphics.camera.matrix, 16 * 4); return true; } @@ -402,6 +413,23 @@ pub fn endDraw() void { defer Graphics.render_pass = null; if (Graphics.render_pass) |pass| { sdl.EndGPURenderPass(pass); + + if (Graphics.fsaa_level > 1) sdl.GenerateMipmapsForGPUTexture(Graphics.command_buffer, Graphics.fsaa_target); + sdl.BlitGPUTexture(Graphics.command_buffer, &.{ + .source = .{ + .texture = Graphics.fsaa_target, + .w = Graphics.window_size[0], + .h = Graphics.window_size[1], + .mip_level = fsaa_level - 1, + }, + .destination = .{ + .texture = Graphics.render_target, + .w = Graphics.window_size[0], + .h = Graphics.window_size[1], + }, + .load_op = sdl.GPU_LOADOP_DONT_CARE, + .filter = sdl.GPU_FILTER_NEAREST, + }); } if (!sdl.SubmitGPUCommandBuffer(Graphics.command_buffer)) err.sdl(); } @@ -410,7 +438,7 @@ fn loadShader(path: []const u8, info: sdl.GPUShaderCreateInfo) *sdl.GPUShader { const file = std.fs.cwd().openFile(path, .{}) catch |e| err.file(e, path); defer file.close(); - const code = file.readToEndAllocOptions(std.heap.c_allocator, std.math.maxInt(usize), null, .@"1", 0) catch |e| err.file(e, path); + const code = file.readToEndAllocOptions(std.heap.c_allocator, std.math.maxInt(usize), null, 1, 0) catch |e| err.file(e, path); defer std.heap.c_allocator.free(code); var updated_info = info; @@ -419,38 +447,38 @@ fn loadShader(path: []const u8, info: sdl.GPUShaderCreateInfo) *sdl.GPUShader { return sdl.CreateGPUShader(device, &updated_info) orelse err.sdl(); } -fn createDepthTexture(width: u32, height: u32) *sdl.GPUTexture { - return sdl.CreateGPUTexture(device, &.{ - .format = sdl.GPU_TEXTUREFORMAT_D16_UNORM, - .layer_count_or_depth = 1, - .width = width, - .height = height, - .num_levels = 1, - .sample_count = sdl.GPU_SAMPLECOUNT_4, - .usage = sdl.GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, - }) orelse err.sdl(); -} - -fn createTexture(width: u32, height: u32, format: c_uint) *sdl.GPUTexture { +fn createTexture(width: u32, height: u32, format: c_uint, usage: c_uint, mip_level: u32) *sdl.GPUTexture { return sdl.CreateGPUTexture(device, &.{ .format = format, .layer_count_or_depth = 1, .width = width, .height = height, - .num_levels = 1, - .sample_count = sdl.GPU_SAMPLECOUNT_4, - .usage = sdl.GPU_TEXTUREUSAGE_COLOR_TARGET, + .num_levels = mip_level, + .sample_count = sdl.GPU_SAMPLECOUNT_1, + .usage = usage, }) orelse err.sdl(); } fn resetTextures(width: u32, height: u32) void { sdl.ReleaseGPUTexture(Graphics.device, Graphics.depth_texture); - Graphics.depth_texture = createDepthTexture(width, height); + Graphics.depth_texture = createTexture( + width * Graphics.fsaa_scale, + height * Graphics.fsaa_scale, + DEPTH_FORMAT, + sdl.GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, + 1, + ); const target_format = sdl.SDL_GetGPUSwapchainTextureFormat(Graphics.device, Graphics.window); - sdl.ReleaseGPUTexture(Graphics.device, Graphics.msaa_resolve); - Graphics.msaa_resolve = createTexture(width, height, target_format); + sdl.ReleaseGPUTexture(Graphics.device, Graphics.fsaa_target); + Graphics.fsaa_target = createTexture( + width * Graphics.fsaa_scale, + height * Graphics.fsaa_scale, + target_format, + sdl.GPU_TEXTUREUSAGE_COLOR_TARGET | sdl.GPU_TEXTUREUSAGE_SAMPLER, + fsaa_level, + ); } pub fn resize(width: u32, height: u32) void { @@ -460,3 +488,21 @@ pub fn resize(width: u32, height: u32) void { pub fn windowId() sdl.WindowID { return sdl.GetWindowID(Graphics.window); } + +pub fn getWidth() u32 { + return @max(1, Graphics.window_size[0]); +} +pub fn getHeight() u32 { + return @max(1, Graphics.window_size[1]); +} + +pub fn generatePlane(x0: f32, y0: f32, x1: f32, y1: f32) [30]f32 { + return .{ + -0.5, -0.5, 0, x0, y1, + 0.5, 0.5, 0, x1, y0, + -0.5, 0.5, 0, x0, y0, + 0.5, 0.5, 0, x1, y0, + -0.5, -0.5, 0, x0, y1, + 0.5, -0.5, 0, x1, y1, + }; +} diff --git a/src/graphics/camera.zig b/src/graphics/camera.zig index 20502fc..4b37b4b 100644 --- a/src/graphics/camera.zig +++ b/src/graphics/camera.zig @@ -1,5 +1,6 @@ const std = @import("std"); const sdl = @import("sdl"); +const math = @import("../math.zig"); const Transform = @import("transform.zig"); const Camera = @This(); @@ -11,7 +12,11 @@ far: f32, /// width = height * aspect aspect: f32, -pub fn matrix(camera: Camera) @Vector(16, f32) { +matrix: Transform.TMatrix, + +pub fn computeMatrix(camera: *Camera) void { + @setFloatMode(.optimized); + const xx = 1.0 / (camera.lens * camera.aspect); const yy = 1.0 / camera.lens; const fnmod = 1.0 / (camera.far - camera.near); @@ -23,5 +28,79 @@ pub fn matrix(camera: Camera) @Vector(16, f32) { 0, 0, -zz, wz, 0, 0, -1, 0, }; - return Transform.multiplyMatrix(projection, camera.transform.inverseMatrix()); + camera.matrix = Transform.multiplyMatrix(projection, camera.transform.inverseMatrix()); +} + +pub fn to_screen(camera: Camera, position: Transform.Position) @Vector(2, f32) { + @setFloatMode(.optimized); + + var x: f32 = camera.matrix[3]; + var y: f32 = camera.matrix[7]; + var w: f32 = camera.matrix[15]; + + for (0..3) |i| { + x += camera.matrix[i] * position[i]; + } + for (0..3) |i| { + y += camera.matrix[i + 4] * position[i]; + } + for (0..3) |i| { + w += camera.matrix[i + 12] * position[i]; + } + @setRuntimeSafety(false); + const wmod = 1 / w; + return .{ x * wmod, y * wmod }; +} + +pub fn mouse_in_quad(camera: Camera, mouse: @Vector(2, f32), quad_transform: Transform) bool { + @setFloatMode(.optimized); + + const matrix = Transform.multiplyMatrix(camera.matrix, quad_transform.matrix()); + + const pi: [4]@Vector(2, f32) = .{ + .{ -0.5, -0.5 }, + .{ -0.5, 0.5 }, + .{ 0.5, 0.5 }, + .{ 0.5, -0.5 }, + }; + var po: [4]@Vector(2, f32) = undefined; + for (0..4) |i| { + const x = matrix[0] * pi[i][0] + matrix[1] * pi[i][1] + matrix[3]; + const y = matrix[4] * pi[i][0] + matrix[5] * pi[i][1] + matrix[7]; + const w = matrix[12] * pi[i][0] + matrix[13] * pi[i][1] + matrix[15]; + @setRuntimeSafety(false); + po[i] = .{ x / w, y / w }; + } + inline for (0..4) |i| { + const a = po[i]; + const b = po[(i + 1) % 4]; + const c = mouse; + if ((c[0] - a[0]) * (b[1] - a[1]) - (c[1] - a[1]) * (b[0] - a[0]) < 0.0) { + return false; + } + } + + return true; +} + +pub fn raycast(camera: Camera, mouse: @Vector(2, f32), plane: @Vector(4, f32)) @Vector(3, f32) { + const matrix = camera.transform.matrix(); + + const local = @Vector(3, f32){ + mouse[0] * camera.lens * camera.aspect, + mouse[1] * camera.lens, + -1, + }; + var global = @Vector(3, f32){ + matrix[3], + matrix[7], + matrix[11], + }; + for (0..3) |i| { + for (0..3) |j| { + global[i] += local[j] * matrix[4 * i + j]; + } + } + + return math.raycast(camera.transform.position, global, plane); } diff --git a/src/math.zig b/src/math.zig index 07ec24b..731f22a 100644 --- a/src/math.zig +++ b/src/math.zig @@ -123,7 +123,7 @@ pub fn Sway(comptime T: type) type { const sin = std.math.sin(dist); const cos = std.math.cos(dist); var len = length(@Vector(2, T){ self.value, self.velocity }); - if(len < 0.001) { + if (len < 0.001) { self.value = 0.001; self.velocity = 0; len = 0.001; @@ -137,3 +137,31 @@ pub fn Sway(comptime T: type) type { } }; } + +pub fn raycast( + origin: @Vector(3, f32), + target: @Vector(3, f32), + plane: @Vector(4, f32), +) @Vector(3, f32) { + @setFloatMode(.optimized); + + const offset = target - origin; + const plane_dir = @Vector(3, f32){ plane[0], plane[1], plane[2] }; + const dist = plane[3]; + const dist_mod = dist / dot(plane_dir, plane_dir); + const num = dot(plane_dir, plane_dir * @as(@Vector(3, f32), @splat(dist_mod)) - origin); + var den = dot(offset, plane_dir); + if (@abs(den) < 0.0001) { + den = 0.0001; + } + + return origin + offset * @as(@Vector(3, f32), @splat(num / den)); +} + +pub fn limit(vector: anytype, value: f32) @TypeOf(vector) { + const max = @reduce(.Max, vector); + if (max > value) + return vector * @as(@TypeOf(vector), @splat(value / max)) + else + return vector; +} diff --git a/src/mouse.zig b/src/mouse.zig index ce870a2..3734c99 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -3,7 +3,15 @@ const key_store = @import("data/keystore.zig"); buttons: key_store.KeyStore(@TypeOf(sdl.BUTTON_LEFT), 4, 0) = .{}, -x: f32 = 0, -y: f32 = 0, +x_screen: f32 = 0, +y_screen: f32 = 0, +x_norm: f32 = 0, +y_norm: f32 = 0, dx: f32 = 0, dy: f32 = 0, +wheel: i32 = 0, + +pub fn reset(mouse: *@This()) void { + mouse.buttons.reset(); + mouse.wheel = 0; +} diff --git a/src/offline.zig b/src/offline.zig new file mode 100644 index 0000000..26a2566 --- /dev/null +++ b/src/offline.zig @@ -0,0 +1,5 @@ +const client = @import("client.zig"); + +pub fn main() void { + client.main(); +} diff --git a/src/server.zig b/src/server.zig new file mode 100644 index 0000000..3995b10 --- /dev/null +++ b/src/server.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +pub fn main() void { + std.debug.print("Server started!\n", .{}); +} diff --git a/src/time.zig b/src/time.zig deleted file mode 100644 index ff505d5..0000000 --- a/src/time.zig +++ /dev/null @@ -1,51 +0,0 @@ -const TimeType = u64; -const TIME_UNIT: TimeType = 1 << 32; -const TIME_MULT = 1.0 / @as(f32, @floatFromInt(TIME_UNIT)); -const Time = @This(); - -pub const ZERO = Time{ .clock = 0 }; - -clock: TimeType, - -pub fn tick(self: *Time, units: f32) void { - self.clock += durationFromUnits(units); -} - -pub fn past(self: *Time, goal: Time) bool { - return self.clock >= goal.clock; -} - -pub fn offset(self: Time, units: f32) Time { - return Time{ - .clock = self.clock + durationFromUnits(units), - }; -} - -pub fn unitsSince(self: *Time, from: Time) f32 { - if (from.clock > self.clock) return 0; - return @as(f32, @floatFromInt(self.clock - from.clock)) * TIME_MULT; -} - -pub fn progress(self: *Time, from: Time, to: Time) f32 { - if (from.clock > to.clock) return 1.0; - if (self.clock > to.clock) return 1.0; - - const duration = to.clock - from.clock; - return @as(f32, @floatFromInt(self.clock - from.clock)) / @as(f32, @floatFromInt(duration)); -} - -pub fn unitsFromDuration(duration: TimeType) f32 { - return @as(f32, @floatFromInt(duration)) * TIME_MULT; -} - -pub fn durationFromUnits(units: f32) TimeType { - return @intFromFloat(@as(f32, @floatFromInt(TIME_UNIT)) * units); -} - -pub fn earliest(a: Time, b: Time) Time { - return .{ .clock = @min(a.clock, b.clock) }; -} - -pub fn plus(time: Time, ticks: TimeType) Time { - return .{ .clock = time.clock + ticks }; -} diff --git a/src/world.zig b/src/world.zig index d7b394f..35eecf6 100644 --- a/src/world.zig +++ b/src/world.zig @@ -1,82 +1,261 @@ +const std = @import("std"); +const sdl = @import("sdl"); +const math = @import("math.zig"); +const err = @import("error.zig"); +const Game = @import("game.zig"); const Graphics = @import("graphics.zig"); const Assets = @import("assets.zig"); -const Entity = @import("entity.zig"); -const Time = @import("time.zig"); -const comp = @import("components.zig"); -pub var time: Time = undefined; -var next_stop: Time = undefined; -pub var entities: comp.Storage(Entity, .{}) = undefined; +const Id = u32; +const Order = i32; +pub var object_map: std.AutoHashMapUnmanaged(Id, usize) = .{}; +pub var objects: std.ArrayListUnmanaged(Object) = .{}; pub var plane_mesh: Graphics.Mesh = undefined; pub var cube_mesh: Graphics.Mesh = undefined; +pub var table_mesh: Graphics.Mesh = undefined; pub var texture: Assets.Texture = undefined; +pub var hand_texture: Assets.Texture = undefined; +pub var camera_position: @Vector(2, f32) = @splat(0); +pub var hover: ?Id = null; +pub var hand_transform: Graphics.Transform = .{}; +pub var panning = false; +pub var zoom: i32 = 0; +pub var hand_objects: u32 = 0; +pub var min_order: Order = undefined; +pub var max_order: Order = undefined; + +const Object = struct { + transform: Graphics.Transform = .{}, + scale: Graphics.Transform.Scale, + mesh: Graphics.Mesh, + texture: Assets.Texture, + order: Order, + id: Id, + parent: enum { + none, + hand, + } = .none, + hand_index: u32 = 0, + parent_infl: f32 = 0, +}; const World = @This(); pub fn initDebug() void { - entities = comp.Storage(Entity, .{}).init(); - _ = entities.add(.{ - .position = .{ 0, 0 }, - .player = true, - }); - time = Time.ZERO; + for (0..10) |i| { + (World.objects.addOne(Game.alloc) catch err.oom()).* = .{ + .scale = @splat(0.5), + .mesh = Graphics.loadMesh(@ptrCast(&Graphics.generatePlane( + 15.0 / 16.0, + @as(f32, @floatFromInt(i)) / 16.0, + 16.0 / 16.0, + @as(f32, @floatFromInt(i + 1)) / 16.0, + ))), + .texture = Assets.load(.texture, "data/yakuza.png"), + .order = @intCast(i), + .id = @intCast(i), + }; + World.object_map.put(Game.alloc, @intCast(i), i) catch err.oom(); + } World.plane_mesh = Graphics.loadMesh(@ptrCast(&PLANE_MESH_DATA)); World.cube_mesh = Graphics.loadMesh(@ptrCast(&CUBE_MESH_DATA)); - World.texture = Assets.load(.texture, "data/wawa.png"); + World.table_mesh = Graphics.loadMesh(@ptrCast(&Graphics.generatePlane(0, 0, 0.5, 0.5))); + World.texture = Assets.load(.texture, "data/yakuza.png"); + World.hand_texture = Assets.load(.texture, "data/hand.png"); + World.camera_position = @splat(0); + World.hover = null; + World.hand_transform = .{ + .scale = @splat(0.5), + }; + World.panning = false; + World.zoom = 0; + World.min_order = 0; + World.max_order = 9; } pub fn deinit() void { Graphics.unloadMesh(World.plane_mesh); Graphics.unloadMesh(World.cube_mesh); + Graphics.unloadMesh(World.table_mesh); Assets.free(World.texture); - World.entities.deinit(); + Assets.free(World.hand_texture); + for (World.objects.items) |*object| { + Assets.free(object.texture); + Graphics.unloadMesh(object.mesh); + } + World.objects.clearAndFree(Game.alloc); + World.object_map.clearAndFree(Game.alloc); } -pub fn updateReal(delta: f32) void { - const update_until = World.time.plus(Time.durationFromUnits(delta)); - while (!World.time.past(update_until)) { - const current = Time.earliest(World.next_stop, update_until); - defer World.time = current; +pub fn update(delta: f32) void { + const hand_target = Graphics.camera.raycast(.{ Game.mouse.x_norm, Game.mouse.y_norm }, .{ 0, 0, 1, 0 }); + World.hand_transform.position = math.lerpTimeLn( + World.hand_transform.position, + hand_target + @Vector(3, f32){ World.hand_transform.scale[0] * 0.5, -World.hand_transform.scale[1] * 0.5, 0.25 }, + delta, + -16, + ); + World.hover = null; + World.hand_objects = 0; + for (World.objects.items) |*object| { + updateHover(object); + } + for (World.objects.items) |*object| { + updateObject(object, delta); + } + if (Game.mouse.buttons.is_just_pressed(sdl.BUTTON_LEFT)) { + World.panning = !World.tryPick(); + } + if (Game.mouse.buttons.is_just_pressed(sdl.BUTTON_RIGHT)) { + _ = World.tryRelease(); + } + World.updateCamera(delta); +} - var iter = World.entities.iter(); - while (iter.next()) |entity| { - entity.update(); +pub fn tryPick() bool { + if (World.hover) |hover_id| { + World.panning = false; + World.getObject(hover_id).?.parent = .hand; + return true; + } else return false; +} +pub fn tryRelease() bool { + var last: ?*Object = null; + for (World.objects.items) |*object| { + if (object.parent != .hand) continue; + last = object; + } + if (last) |object| { + object.parent = .none; + return true; + } + return false; +} + +pub fn updateHover(object: *Object) void { + if (object.parent == .hand) { + object.hand_index = World.hand_objects; + World.hand_objects += 1; + return; + } + if (Graphics.camera.mouse_in_quad(.{ Game.mouse.x_norm, Game.mouse.y_norm }, object.transform)) { + if (World.hover == null or World.getObject(World.hover.?).?.transform.position[2] < object.transform.position[2]) { + World.hover = object.id; } } } -pub fn draw(delta: f32) void { - Graphics.drawMesh(World.plane_mesh, World.texture, Graphics.Transform.matrix(.{ .scale = @splat(5) })); - var iter = World.entities.iter(); - while (iter.next()) |entity| { - entity.draw(delta); +pub fn updateObject(object: *Object, delta: f32) void { + switch (object.parent) { + .none => { + object.transform.position[2] = math.lerpTimeLn( + object.transform.position[2], + if (World.hover == object.id) @as(f32, 0.125) else @as(f32, 0.0625), + delta, + -8, + ); + object.transform.scale = math.lerpTimeLn( + object.transform.scale, + if (World.hover == object.id) object.scale * @as(@Vector(3, f32), @splat(1.25)) else object.scale, + delta, + -4, + ); + }, + .hand => { + var target_position = World.hand_transform.position; + var target_scale = object.scale; + target_position[2] *= 0.5; + const hand_order = hand_objects - object.hand_index - 1; + switch (hand_order) { + 0 => { + target_position[0] -= World.hand_transform.scale[0] * 0.5; + target_position[1] += World.hand_transform.scale[1] * 0.5; + }, + else => |i| { + target_position[0] += World.hand_transform.scale[0] * if ((i - 1) & 1 == 0) @as(f32, 0.5) else @as(f32, 1); + target_position[1] += World.hand_transform.scale[1] * if ((i - 1) & 2 == 0) @as(f32, 0.25) else @as(f32, -0.25); + target_position[2] -= @as(f32, @floatFromInt((hand_order - 1) / 4)) * 0.01; + target_scale = math.limit(target_scale, World.hand_transform.scale[1] * 0.5); + }, + } + object.transform.position = math.lerpTimeLn( + object.transform.position, + target_position, + delta, + -16, + ); + object.transform.scale = math.lerpTimeLn( + object.transform.scale, + target_scale, + delta, + -4, + ); + }, } } -pub fn requestUpdate(at: Time) void { - World.next_stop = Time.earliest(at, World.next_stop); -} - -pub fn entityAt(position: @Vector(2, i32)) ?*Entity { - var iter = World.entities.iter(); - while (iter.next()) |entity| { - if (@reduce(.And, entity.position == position)) - return entity; +pub fn draw() void { + Graphics.drawMesh(World.table_mesh, World.texture, Graphics.Transform.matrix(.{ .scale = @splat(8) })); + for (World.objects.items) |*object| { + Graphics.drawMesh(object.mesh, object.texture, object.transform.matrix()); } - return null; + Graphics.drawMesh(World.plane_mesh, World.hand_texture, World.hand_transform.matrix()); } -pub fn isFree(position: @Vector(2, i32)) bool { - return World.entityAt(position) == null; -} +pub fn updateCamera(delta: f32) void { + World.zoom = std.math.clamp(World.zoom + Game.mouse.wheel, -4, 8); + const zoom_factor = std.math.exp(@as(f32, @floatFromInt(zoom)) * @log(2.0) * -0.5); -pub fn getPlayer() ?*Entity { - var iter = World.entities.iter(); - while (iter.next()) |entity| { - if (entity.player) - return entity; + if (Game.mouse.buttons.is_pressed(sdl.BUTTON_LEFT)) { + if (World.panning) { + World.camera_position[0] += zoom_factor * Game.mouse.dx / @as(f32, @floatFromInt(Graphics.getWidth())) * -15; + World.camera_position[1] += zoom_factor * Game.mouse.dy / @as(f32, @floatFromInt(Graphics.getWidth())) * 15; + } } - return null; + + const offset = @Vector(3, f32){ 0.0, -1.0 * zoom_factor, 4.0 * zoom_factor }; + const target_position = @Vector(3, f32){ World.camera_position[0], World.camera_position[1], 0.0 }; + Graphics.camera.transform.position = math.lerpTimeLn( + Graphics.camera.transform.position, + target_position + offset, + delta, + -32, + ); + + const ORIGIN_DIR = @Vector(3, f32){ 0.0, 0.0, -1.0 }; + const INIT_ROTATION = Graphics.Transform.rotationByAxis(.{ 1.0, 0.0, 0.0 }, std.math.pi * 0.5); + + const ROTATED_DIR = Graphics.Transform.rotateVector(ORIGIN_DIR, INIT_ROTATION); + + const target_rotation = Graphics.Transform.combineRotations( + INIT_ROTATION, + Graphics.Transform.rotationToward( + ROTATED_DIR, + math.lerp(-offset, target_position - Graphics.camera.transform.position, 0.125), + .{ .normalize_to = true }, + ), + ); + Graphics.camera.transform.rotation = Graphics.Transform.normalizeRotation(math.slerpTimeLn( + Graphics.camera.transform.rotation, + target_rotation, + delta, + -16, + )); +} + +fn getObject(id: Id) ?*Object { + const index = World.object_map.get(id) orelse return null; + if (index >= World.objects.items.len) return null; + return &World.objects.items[index]; +} + +fn bringToTop(object: *Object) void { + World.max_order += 1; + object.order = World.max_order; +} +fn bringToBottom(object: *Object) void { + World.min_order -= 1; + object.order = World.min_order; } const CUBE_MESH_DATA = [_]f32{ @@ -130,9 +309,3 @@ const PLANE_MESH_DATA = [_]f32{ -0.5, -0.5, 0, 0.0, 1.0, 0.5, -0.5, 0, 1.0, 1.0, }; -const TEXTURE_DATA = [_]u8{ - 255, 64, 64, 255, - 64, 255, 64, 255, - 64, 64, 255, 255, - 64, 64, 64, 255, -};