Compare commits

...

10 Commits

Author SHA1 Message Date
duck
92ca641f89 Small fixes 2025-09-01 18:04:16 +05:00
duck
d317694056 Better scrolling, cubemap, yakuza setup 2025-08-31 08:03:52 +05:00
duck
39db8ed3a1 Decks
The code is getting real messy...
2025-08-31 07:08:29 +05:00
duck
1d3f89e5f5 Fixes and adjustments 2025-08-31 05:36:51 +05:00
duck
6f933449a1 Iterative texture loading 2025-08-31 05:36:51 +05:00
duck
3f1c0aa2f8 Async asset loading 2025-08-31 05:36:51 +05:00
duck
9fdd997be6 Dock 2025-08-31 05:36:46 +05:00
duck
1ef69d1545 Scrolling and better hand stacking 2025-08-29 05:26:35 +05:00
duck
855194acea Initial commit for tabletop
We'll branch off of main game for a cool quick side project idea for a moment
2025-08-29 01:59:33 +05:00
duck
f3d2eff20e Sway and matrices 2025-08-24 18:54:49 +05:00
22 changed files with 1444 additions and 536 deletions

View File

@@ -11,42 +11,93 @@ pub fn build(b: *Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const exe = stepBuildMain(b, target, optimize); const sdl_module, const sdl_step = stepSdlModule(b, target, optimize);
b.installArtifact(exe);
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); 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); b.getInstallStep().dependOn(copy_data);
const run = b.addRunArtifact(exe); const run = b.addRunArtifact(offline_exe);
run.step.dependOn(b.getInstallStep()); 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)); run.setCwd(b.path(std.fs.path.relative(b.allocator, b.build_root.path.?, b.exe_dir) catch unreachable));
if (b.args) |args| { if (b.args) |args| {
run.addArgs(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); run_step.dependOn(&run.step);
const check_step = b.step("check", "Check for build errors"); const check_step = b.step("check", "Check for build errors (offline build only)");
check_step.dependOn(&exe.step); 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, b: *Build,
target: Build.ResolvedTarget, target: Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode, optimize: std.builtin.OptimizeMode,
) *Build.Step.Compile { ) *Build.Step.Compile {
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "spacefarer", .name = "tabletop_server",
.root_source_file = b.path("src/main.zig"), .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, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
const sdl_module, const sdl_step = stepSdlModule(b, target, optimize);
exe.root_module.addImport("sdl", sdl_module); exe.root_module.addImport("sdl", sdl_module);
exe.step.dependOn(sdl_step); exe.step.dependOn(sdl_step);
@@ -58,13 +109,12 @@ fn stepBuildMain(
fn stepBuildSdlTranslator( fn stepBuildSdlTranslator(
b: *Build, b: *Build,
target: Build.ResolvedTarget, target: Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) *Build.Step.Compile { ) *Build.Step.Compile {
const sdl_translator = b.addExecutable(.{ const sdl_translator = b.addExecutable(.{
.name = "sdl_header_translator", .name = "sdl_header_translator",
.root_source_file = b.path("utils/sdl_translator.zig"), .root_source_file = b.path("utils/sdl_translator.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = .Debug,
}); });
sdl_translator.linkSystemLibrary("SDL3"); sdl_translator.linkSystemLibrary("SDL3");
return sdl_translator; return sdl_translator;
@@ -73,9 +123,8 @@ fn stepBuildSdlTranslator(
fn stepTranslateSdl( fn stepTranslateSdl(
b: *Build, b: *Build,
target: Build.ResolvedTarget, target: Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) struct { *Build.Step, Build.LazyPath } { ) 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 translate = b.addRunArtifact(sdl_translator);
const sdl_rename = translate.addOutputFileArg("sdl_rename.h"); const sdl_rename = translate.addOutputFileArg("sdl_rename.h");
return .{ return .{
@@ -97,7 +146,7 @@ fn stepSdlModule(
}); });
sdl_module.linkSystemLibrary("SDL3", .{}); 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()); sdl_module.addIncludePath(sdl_rename.dirname());
return .{ return .{

View File

@@ -1,8 +1,8 @@
.{ .{
.name = .spacefarer, .name = .tabletop,
.version = "0.0.0", .version = "0.0.0",
.fingerprint = 0x946ddccb5911fb15, .fingerprint = 0x9467f2a6727b4ca5,
.minimum_zig_version = "0.15.0", .minimum_zig_version = "0.14.0",
.dependencies = .{}, .dependencies = .{},
.paths = .{ .paths = .{
"build.zig", "build.zig",

BIN
data/cubemap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

BIN
data/hand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -7,4 +7,7 @@ layout(set = 2, binding = 0) uniform sampler2D texture_sampler;
void main() { void main() {
fragColor = texture(texture_sampler, inUV); fragColor = texture(texture_sampler, inUV);
if (fragColor.a < 0.5) {
discard;
}
} }

View File

@@ -13,5 +13,6 @@ layout(set = 1, binding = 1) uniform Object{
void main() { void main() {
gl_Position = vec4(inCoord, 1.0) * object.transform * camera.transform; gl_Position = vec4(inCoord, 1.0) * object.transform * camera.transform;
gl_ClipDistance[0] = gl_Position.z;
outUV = inUV; outUV = inUV;
} }

BIN
data/yakuza.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,104 +1,292 @@
const std = @import("std"); const std = @import("std");
const sdl = @import("sdl");
const err = @import("error.zig"); const err = @import("error.zig");
const c = @import("c.zig");
const comp = @import("components.zig");
const Game = @import("game.zig"); const Game = @import("game.zig");
const Graphics = @import("graphics.zig"); const FileLoader = @import("assets/file.zig");
const TextureLoader = @import("assets/texture.zig");
const Assets = @This(); const Assets = @This();
const Storage = comp.Storage(Asset, .{}); pub const Texture = AssetContainer(TextureLoader);
var storage: Storage = undefined; const WORKERS_MAX = 4;
var next_worker_update: usize = 0;
var workers: [WORKERS_MAX]WorkerState = undefined;
const WorkerState = struct {
running: bool = false,
thread: ?std.Thread = null,
};
const AssetMap = std.HashMapUnmanaged(AssetId, *AssetCell, AssetContext, 80);
var asset_map_mutex: std.Thread.Mutex = .{};
var asset_map: AssetMap = undefined;
const RequestBoard = std.ArrayListUnmanaged(*AssetCell);
var request_board_mutex: std.Thread.Mutex = .{};
var request_board: RequestBoard = undefined;
var request_board_counter: usize = 0;
const FreeBoard = std.ArrayListUnmanaged(*AssetCell);
var free_board_mutex: std.Thread.Mutex = .{};
var free_board: FreeBoard = undefined;
const AssetId = struct {
type: AssetType,
path: []const u8,
};
const AssetContext = struct {
pub fn hash(self: @This(), key: AssetId) u64 {
_ = self;
var hasher = std.hash.Wyhash.init(@intFromEnum(key.type));
hasher.update(key.path);
return hasher.final();
}
pub fn eql(self: @This(), a: AssetId, b: AssetId) bool {
_ = self;
return a.type == b.type and std.mem.eql(u8, a.path, b.path);
}
};
pub const LoadError = error{
DependencyError,
ParsingError,
SdlError,
FileTooBig,
} || std.mem.Allocator.Error || std.fs.File.OpenError || std.fs.File.ReadError;
pub const AssetType = enum { pub const AssetType = enum {
file,
texture, texture,
pub fn getType(comptime self: @This()) type {
return switch (self) {
.file => FileLoader,
.texture => TextureLoader,
}; };
pub const Texture = struct { }
handle: Storage.Key,
}; };
const Asset = struct { const AssetState = union(enum) {
not_loaded,
loaded,
fail: LoadError,
};
pub const AssetCell = struct {
mutex: std.Thread.Mutex,
type: AssetType,
data: *void,
path: []const u8, path: []const u8,
data: union(AssetType) { loader: *const fn (*AssetCell, std.mem.Allocator) LoadError!void,
texture: AssetTexture, unloader: *const fn (*AssetCell, std.mem.Allocator) void,
state: AssetState,
counter: usize,
fn load(self: *AssetCell, alloc: std.mem.Allocator) void {
self.loader(self, alloc) catch |e| {
self.state = .{ .fail = e };
return;
};
self.state = .loaded;
}
fn unload(self: *AssetCell, alloc: std.mem.Allocator) void {
self.unloader(self, alloc);
}
};
pub fn AssetContainer(comptime T: type) type {
return struct {
data_pointer: ?*T = null,
asset_pointer: *AssetCell,
last_state: AssetState = .not_loaded,
pub fn get(self: *@This()) ?*T {
switch (self.last_state) {
.loaded => {
@branchHint(.likely);
return self.data_pointer;
}, },
.fail => {
return null;
},
.not_loaded => {
if (self.asset_pointer.mutex.tryLock()) {
defer self.asset_pointer.mutex.unlock();
self.last_state = self.asset_pointer.state;
}
if (self.last_state == .loaded) {
self.data_pointer = @alignCast(@ptrCast(self.asset_pointer.data));
return self.data_pointer;
} else return null;
},
}
}
/// To be used by worker threads to request other assets
pub fn getSync(self: *@This()) !*T {
sw: switch (self.last_state) {
.loaded => {
return self.data_pointer.?;
},
.fail => |e| {
return e;
},
.not_loaded => {
// TODO: Do something else while the asset is locked?
self.asset_pointer.mutex.lock();
defer self.asset_pointer.mutex.unlock();
self.last_state = self.asset_pointer.state;
if (self.last_state == .not_loaded) {
self.asset_pointer.load(Game.alloc);
self.last_state = self.asset_pointer.state;
}
if (self.last_state == .loaded) {
self.data_pointer = @alignCast(@ptrCast(self.asset_pointer.data));
}
continue :sw self.last_state;
},
}
}
}; };
pub const AssetTexture = struct { }
texture: *sdl.GPUTexture,
sampler: *sdl.GPUSampler,
};
pub fn init() void { pub fn init() void {
Assets.storage = Storage.init(); Assets.next_worker_update = 0;
Assets.workers = .{WorkerState{}} ** WORKERS_MAX;
Assets.asset_map_mutex = .{};
Assets.asset_map = AssetMap.empty;
Assets.request_board_mutex = .{};
Assets.request_board = RequestBoard.empty;
Assets.request_board_counter = 0;
Assets.free_board_mutex = .{};
Assets.free_board = FreeBoard.empty;
} }
pub fn deinit() void { pub fn deinit() void {
var iter = Assets.storage.iter(); for (&Assets.workers) |*worker| {
if (worker.thread == null) continue;
worker.thread.?.join();
}
var iter = Assets.asset_map.valueIterator();
while (iter.next()) |asset| { while (iter.next()) |asset| {
Assets.freeAsset(asset); std.debug.assert(asset.*.counter == 0);
if (asset.*.state == .loaded)
asset.*.unload(Game.alloc);
Game.alloc.destroy(asset.*);
} }
Assets.storage.deinit(); Assets.asset_map.clearAndFree(Game.alloc);
Assets.request_board.clearAndFree(Game.alloc);
Assets.free_board.clearAndFree(Game.alloc);
} }
pub fn load(comptime asset_type: AssetType, path: []const u8) typeFromAssetType(asset_type) { pub fn update() void {
switch (asset_type) { const worker = &Assets.workers[Assets.next_worker_update];
.texture => { if (!@atomicLoad(bool, &worker.running, .acquire) and worker.thread != null) {
const data = loadFile(Game.alloc, path) catch |e| err.file(e, path); worker.thread.?.join();
var x: i32 = undefined; worker.thread = null;
var y: i32 = undefined;
var z: i32 = undefined;
const image = c.stbi_load_from_memory(@ptrCast(data), @intCast(data.len), &x, &y, &z, 4);
Game.alloc.free(data);
if (image == null) err.stbi();
const image_slice = image[0..@intCast(x * y * z)];
const texture, const sampler = Graphics.loadTexture(@intCast(x), @intCast(y), image_slice);
c.stbi_image_free(image);
return .{ .handle = Assets.storage.add(.{
.path = path,
.data = .{ .texture = .{
.texture = texture,
.sampler = sampler,
} },
}) };
},
} }
} if (worker.thread == null and @atomicLoad(usize, &Assets.request_board_counter, .monotonic) > 4 * Assets.next_worker_update) {
pub fn free(asset: anytype) void { worker.running = true;
if (Assets.storage.free(asset.handle)) |stored| { worker.thread = std.Thread.spawn(.{}, loaderLoop, .{Assets.next_worker_update}) catch err.oom();
freeAsset(stored);
}
}
pub fn freeAsset(asset: *Asset) void {
switch (asset.data) {
.texture => {
Graphics.unloadTexture(asset.data.texture.texture, asset.data.texture.sampler);
},
}
}
pub fn get(asset: anytype) ?assetTypeFromType(@TypeOf(asset)) {
if (Assets.storage.get(asset.handle)) |stored| {
switch (@TypeOf(asset)) {
Texture => {
return stored.data.texture;
},
else => @compileError("Cannot get asset of type " ++ @typeName(@TypeOf(asset))),
}
}
unreachable;
} }
fn loadFile(alloc: std.mem.Allocator, path: []const u8) ![]u8 { Assets.next_worker_update += 1;
const file = try std.fs.cwd().openFile(path, .{}); if (Assets.next_worker_update >= WORKERS_MAX) {
defer file.close(); Assets.next_worker_update = 0;
return file.readToEndAlloc(alloc, std.math.maxInt(i32));
} }
fn typeFromAssetType(comptime asset_type: AssetType) type {
return switch (asset_type) { Assets.free_board_mutex.lock();
.texture => Texture, defer Assets.free_board_mutex.unlock();
if (Assets.free_board.items.len == 0) return;
// TODO: Delegate freeing to worker threads?
Assets.asset_map_mutex.lock();
defer Assets.asset_map_mutex.unlock();
while (Assets.free_board.pop()) |request| {
if (@atomicLoad(usize, &request.counter, .monotonic) == 0) {
if (!Assets.asset_map.remove(.{ .type = request.type, .path = request.path })) continue;
if (request.state == .loaded)
request.unload(Game.alloc);
Game.alloc.destroy(request);
}
}
}
pub fn load(comptime asset_type: AssetType, path: []const u8) AssetContainer(asset_type.getType()) {
const asset = mapAsset(asset_type, path);
{
Assets.request_board_mutex.lock();
Assets.request_board.append(Game.alloc, asset) catch err.oom();
_ = @atomicRmw(usize, &Assets.request_board_counter, .Add, 1, .monotonic);
Assets.request_board_mutex.unlock();
}
return .{ .asset_pointer = asset };
}
pub fn free(asset: anytype) void {
const prev = @atomicRmw(usize, &asset.asset_pointer.counter, .Sub, 1, .monotonic);
if (prev == 1) {
Assets.free_board_mutex.lock();
Assets.free_board.append(Game.alloc, asset.asset_pointer) catch err.oom();
Assets.free_board_mutex.unlock();
}
}
fn loaderLoop(worker_id: usize) void {
var processed: usize = 0;
defer @atomicStore(bool, &Assets.workers[worker_id].running, false, .release);
while (true) {
const asset = blk: {
Assets.request_board_mutex.lock();
defer Assets.request_board_mutex.unlock();
const request = Assets.request_board.pop() orelse return;
_ = @atomicRmw(usize, &Assets.request_board_counter, .Sub, 1, .monotonic);
break :blk request;
}; };
defer processed += 1;
asset.mutex.lock();
if (asset.state == .not_loaded)
asset.load(Game.alloc);
asset.mutex.unlock();
} }
fn assetTypeFromType(comptime T: type) type { }
return switch (T) {
Texture => AssetTexture, fn mapAsset(comptime asset_type: AssetType, path: []const u8) *AssetCell {
else => unreachable, Assets.asset_map_mutex.lock();
defer Assets.asset_map_mutex.unlock();
const res = Assets.asset_map.getOrPut(Game.alloc, .{ .type = asset_type, .path = path }) catch err.oom();
if (!res.found_existing) {
res.value_ptr.* = Game.alloc.create(AssetCell) catch err.oom();
res.value_ptr.*.* = .{
.mutex = .{},
.type = asset_type,
.data = undefined,
.path = path,
.loader = Assets.makeLoader(asset_type.getType(), asset_type.getType().load),
.unloader = Assets.makeUnloader(asset_type.getType(), asset_type.getType().unload),
.state = .not_loaded,
.counter = 1,
}; };
} else _ = @atomicRmw(usize, &res.value_ptr.*.counter, .Add, 1, .monotonic);
return res.value_ptr.*;
}
fn makeLoader(comptime T: type, comptime func: *const fn ([]const u8, std.mem.Allocator) LoadError!T) *const fn (*AssetCell, std.mem.Allocator) LoadError!void {
const Container = struct {
pub fn loader(cell: *AssetCell, alloc: std.mem.Allocator) LoadError!void {
const mem = try alloc.create(T);
errdefer alloc.destroy(mem);
mem.* = try func(cell.path, alloc);
cell.data = @ptrCast(mem);
}
};
return Container.loader;
}
fn makeUnloader(comptime T: type, comptime func: *const fn (T, std.mem.Allocator) void) *const fn (*AssetCell, std.mem.Allocator) void {
const Container = struct {
pub fn unloader(cell: *AssetCell, alloc: std.mem.Allocator) void {
func(@as(*T, @alignCast(@ptrCast(cell.data))).*, alloc);
alloc.destroy(@as(*T, @alignCast(@ptrCast(cell.data))));
}
};
return Container.unloader;
} }

14
src/assets/file.zig Normal file
View File

@@ -0,0 +1,14 @@
const std = @import("std");
const Assets = @import("../assets.zig");
bytes: []u8,
pub fn load(path: []const u8, alloc: std.mem.Allocator) Assets.LoadError!@This() {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
return .{ .bytes = try file.readToEndAlloc(alloc, std.math.maxInt(i32)) };
}
pub fn unload(self: @This(), alloc: std.mem.Allocator) void {
alloc.free(self.bytes);
}

107
src/assets/texture.zig Normal file
View File

@@ -0,0 +1,107 @@
const std = @import("std");
const sdl = @import("sdl");
const err = @import("../error.zig");
const c = @import("../c.zig");
const Assets = @import("../assets.zig");
const Graphics = @import("../graphics.zig");
texture: *sdl.GPUTexture,
sampler: *sdl.GPUSampler,
pub fn load(path: []const u8, alloc: std.mem.Allocator) Assets.LoadError!@This() {
_ = alloc;
var file = Assets.load(.file, path);
defer Assets.free(file);
const data = (file.getSync() catch return error.DependencyError).bytes;
var width: u32 = undefined;
var height: u32 = undefined;
var channels: u32 = undefined;
const image = c.stbi_load_from_memory(
@ptrCast(data),
@intCast(data.len),
@ptrCast(&width),
@ptrCast(&height),
@ptrCast(&channels),
4,
);
if (image == null) return error.ParsingError;
defer c.stbi_image_free(image);
const image_slice = image[0..@intCast(width * height * channels)];
if (width > 8192 or height > 8192) return error.FileTooBig;
const target_format = sdl.GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
const bytes_per_pixel = 4;
const mip_level = if(std.math.isPowerOfTwo(width) and width == height) @as(u32, Graphics.MIP_LEVEL) else @as(u32, 1);
const texture = Graphics.createTexture(
width,
height,
target_format,
sdl.GPU_TEXTUREUSAGE_SAMPLER | sdl.GPU_TEXTUREUSAGE_COLOR_TARGET,
mip_level,
);
errdefer Graphics.freeTexture(texture);
const transfer_buffer_capacity = Graphics.TRANSFER_BUFFER_DEFAULT_CAPACITY;
const transfer_buffer = sdl.CreateGPUTransferBuffer(Graphics.device, &.{
.size = transfer_buffer_capacity,
.usage = sdl.GPU_TRANSFERBUFFERUSAGE_UPLOAD,
}) orelse return error.SdlError;
defer sdl.ReleaseGPUTransferBuffer(Graphics.device, transfer_buffer);
var rows_uploaded: u32 = 0;
while (rows_uploaded < height) {
const rows_to_upload = @min(height - rows_uploaded, transfer_buffer_capacity / width / bytes_per_pixel);
if (rows_to_upload == 0) return error.FileTooBig;
const command_buffer = sdl.AcquireGPUCommandBuffer(Graphics.device) orelse return error.SdlError;
{
errdefer _ = sdl.CancelGPUCommandBuffer(command_buffer);
const copy_pass = sdl.BeginGPUCopyPass(command_buffer) orelse return error.SdlError;
defer sdl.EndGPUCopyPass(copy_pass);
const map: [*]u8 = @ptrCast(sdl.MapGPUTransferBuffer(Graphics.device, transfer_buffer, false) orelse err.sdl());
@memcpy(map, image_slice[(rows_uploaded * width * bytes_per_pixel)..((rows_uploaded + rows_to_upload) * width * bytes_per_pixel)]);
sdl.UnmapGPUTransferBuffer(Graphics.device, transfer_buffer);
sdl.UploadToGPUTexture(copy_pass, &sdl.GPUTextureTransferInfo{
.offset = 0,
.pixels_per_row = width,
.rows_per_layer = rows_to_upload,
.transfer_buffer = transfer_buffer,
}, &sdl.GPUTextureRegion{
.texture = texture,
.mip_level = 0,
.layer = 0,
.x = 0,
.y = rows_uploaded,
.z = 0,
.w = width,
.h = rows_to_upload,
.d = 1,
}, false);
}
rows_uploaded += rows_to_upload;
if (rows_uploaded == height and mip_level > 1) {
sdl.GenerateMipmapsForGPUTexture(command_buffer, texture);
}
const fence = sdl.SubmitGPUCommandBufferAndAcquireFence(command_buffer) orelse return error.SdlError;
defer sdl.ReleaseGPUFence(Graphics.device, fence);
if (!sdl.WaitForGPUFences(Graphics.device, true, &fence, 1)) return error.SdlError;
}
const sampler = Graphics.createSampler(mip_level);
return .{
.texture = texture,
.sampler = sampler,
};
}
pub fn unload(self: @This(), alloc: std.mem.Allocator) void {
_ = alloc;
Graphics.freeTexture(self.texture);
Graphics.freeSampler(self.sampler);
}

View File

@@ -1,118 +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,
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 {
const transform = Graphics.Transform{
.position = .{
@floatFromInt(self.position[0]),
@floatFromInt(self.position[1]),
0.5,
},
};
Graphics.drawMesh(World.cube_mesh, World.texture, transform);
if (!self.player) return;
Graphics.camera.transform.position = math.lerpTimeLn(
Graphics.camera.transform.position,
transform.position + @Vector(3, f32){ 0.0, -2.0, 5.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,
));
}

View File

@@ -27,7 +27,16 @@ pub fn init(game_alloc: std.mem.Allocator) void {
Game.running = false; Game.running = false;
Game.time = Time{ .now = 0, .delta = 0 }; Game.time = Time{ .now = 0, .delta = 0 };
Game.keyboard = .{}; 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(); Graphics.create();
Assets.init(); Assets.init();
World.initDebug(); World.initDebug();
@@ -46,11 +55,14 @@ pub fn run() void {
} else err.sdl(); } else err.sdl();
Game.processEvents(); Game.processEvents();
World.updateReal(Game.time.delta); Game.mouse.x_norm = (Game.mouse.x_screen / @as(f32, @floatFromInt(Graphics.window_width))) * 2 - 1;
Game.mouse.y_norm = (Game.mouse.y_screen / @as(f32, @floatFromInt(Graphics.window_height))) * -2 + 1;
World.update(Game.time.delta);
if (Game.beginDraw()) { if (Game.beginDraw()) {
World.draw(Game.time.delta); World.draw();
Game.endDraw(); Game.endDraw();
} }
Assets.update();
} }
} }
@@ -66,6 +78,7 @@ fn processEvents() void {
Game.mouse.dx = 0.0; Game.mouse.dx = 0.0;
Game.mouse.dy = 0.0; Game.mouse.dy = 0.0;
Game.keyboard.keys.reset(); Game.keyboard.keys.reset();
Game.mouse.reset();
sdl.PumpEvents(); sdl.PumpEvents();
while (true) { while (true) {
@@ -77,14 +90,10 @@ fn processEvents() void {
sdl.EVENT_QUIT => { sdl.EVENT_QUIT => {
Game.running = false; Game.running = false;
}, },
sdl.EVENT_WINDOW_RESIZED => {
if (event.window.windowID != Graphics.windowId()) continue;
Graphics.resize(@intCast(event.window.data1), @intCast(event.window.data2));
},
sdl.EVENT_MOUSE_MOTION => { sdl.EVENT_MOUSE_MOTION => {
if (event.motion.windowID != Graphics.windowId()) continue; if (event.motion.windowID != Graphics.windowId()) continue;
Game.mouse.x = event.motion.x; Game.mouse.x_screen = event.motion.x;
Game.mouse.y = event.motion.y; Game.mouse.y_screen = event.motion.y;
Game.mouse.dx += event.motion.xrel; Game.mouse.dx += event.motion.xrel;
Game.mouse.dy += event.motion.yrel; Game.mouse.dy += event.motion.yrel;
}, },
@@ -104,6 +113,9 @@ fn processEvents() void {
if (event.button.windowID != Graphics.windowId()) continue; if (event.button.windowID != Graphics.windowId()) continue;
Game.mouse.buttons.release(event.button.button); Game.mouse.buttons.release(event.button.button);
}, },
sdl.EVENT_MOUSE_WHEEL => {
Game.mouse.wheel += event.wheel.integer_y;
},
else => {}, else => {},
} }
} }

View File

@@ -12,12 +12,12 @@ pub const Mesh = struct {
vertex_count: usize, vertex_count: usize,
}; };
var window: *sdl.Window = undefined; pub var window: *sdl.Window = undefined;
var renderer: *sdl.Renderer = undefined; pub var device: *sdl.GPUDevice = undefined;
var device: *sdl.GPUDevice = undefined;
/// Only available while drawing /// Only available while drawing
var command_buffer: ?*sdl.GPUCommandBuffer = null; var command_buffer: ?*sdl.GPUCommandBuffer = null;
var render_pass: ?*sdl.GPURenderPass = null; var render_pass: ?*sdl.GPURenderPass = null;
var render_target: ?*sdl.GPUTexture = null;
var shader_vert: *sdl.GPUShader = undefined; var shader_vert: *sdl.GPUShader = undefined;
var shader_frag: *sdl.GPUShader = undefined; var shader_frag: *sdl.GPUShader = undefined;
@@ -30,37 +30,37 @@ var transfer_buffer: *sdl.GPUTransferBuffer = undefined;
var transfer_buffer_capacity: usize = undefined; var transfer_buffer_capacity: usize = undefined;
var depth_texture: *sdl.GPUTexture = 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 pipeline: *sdl.GPUGraphicsPipeline = undefined;
var window_size: [2]u32 = undefined; pub var window_width: u32 = undefined;
pub var window_height: u32 = undefined;
var fsaa_scale: u32 = 4;
var fsaa_level: u32 = 3;
pub var camera: Camera = undefined; pub var camera: Camera = undefined;
var to_resize: ?[2]u32 = null;
const VERTEX_BUFFER_DEFAULT_CAPACITY = 1024; const VERTEX_BUFFER_DEFAULT_CAPACITY = 1024;
const VERTEX_BUFFER_GROWTH_MULTIPLIER = 2; const VERTEX_BUFFER_GROWTH_MULTIPLIER = 2;
const TRANSFER_BUFFER_DEFAULT_CAPACITY = 4096;
const BYTES_PER_VERTEX = 5 * 4; const BYTES_PER_VERTEX = 5 * 4;
const DEPTH_FORMAT = sdl.GPU_TEXTUREFORMAT_D32_FLOAT;
pub const TRANSFER_BUFFER_DEFAULT_CAPACITY = 256 * 1024;
pub const MIP_LEVEL = 4;
const Graphics = @This(); const Graphics = @This();
pub fn create() void { pub fn create() void {
// Init // Init
if (!sdl.Init(sdl.INIT_VIDEO | sdl.INIT_EVENTS)) err.sdl(); 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 // Window and Renderer
if (!sdl.CreateWindowAndRenderer( Graphics.window = sdl.CreateWindow(
"", "",
1600, 1600,
900, 900,
sdl.WINDOW_VULKAN | sdl.WINDOW_RESIZABLE, sdl.WINDOW_VULKAN | sdl.WINDOW_RESIZABLE,
@ptrCast(&Graphics.window), ) orelse err.sdl();
@ptrCast(&Graphics.renderer),
)) err.sdl();
Graphics.window_size = .{ 1600, 900 };
if (!sdl.SetRenderVSync(renderer, sdl.RENDERER_VSYNC_ADAPTIVE)) err.sdl();
// Device // Device
Graphics.device = sdl.CreateGPUDevice( Graphics.device = sdl.CreateGPUDevice(
@@ -109,12 +109,22 @@ pub fn create() void {
if (target_format == sdl.GPU_TEXTUREFORMAT_INVALID) err.sdl(); if (target_format == sdl.GPU_TEXTUREFORMAT_INVALID) err.sdl();
// TODO: Clean // TODO: Clean
var window_width: c_int = 1; if (!sdl.GetWindowSizeInPixels(Graphics.window, @ptrCast(&Graphics.window_width), @ptrCast(&Graphics.window_height))) err.sdl();
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.depth_texture = createTexture(
Graphics.msaa_resolve = createTexture(@intCast(window_width), @intCast(window_height), target_format); @as(u32, @intCast(Graphics.window_width)) * Graphics.fsaa_scale,
@as(u32, @intCast(Graphics.window_height)) * Graphics.fsaa_scale,
DEPTH_FORMAT,
sdl.GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET,
1,
);
Graphics.fsaa_target = createTexture(
@as(u32, @intCast(Graphics.window_width)) * Graphics.fsaa_scale,
@as(u32, @intCast(Graphics.window_height)) * Graphics.fsaa_scale,
target_format,
sdl.GPU_TEXTUREUSAGE_COLOR_TARGET | sdl.GPU_TEXTUREUSAGE_SAMPLER,
fsaa_level,
);
Graphics.pipeline = sdl.CreateGPUGraphicsPipeline(Graphics.device, &.{ Graphics.pipeline = sdl.CreateGPUGraphicsPipeline(Graphics.device, &.{
.vertex_shader = Graphics.shader_vert, .vertex_shader = Graphics.shader_vert,
@@ -144,12 +154,9 @@ pub fn create() void {
}, },
.primitive_type = sdl.GPU_PRIMITIVETYPE_TRIANGLELIST, .primitive_type = sdl.GPU_PRIMITIVETYPE_TRIANGLELIST,
.rasterizer_state = presets.RASTERIZER_CULL, .rasterizer_state = presets.RASTERIZER_CULL,
.multisample_state = .{
.sample_count = sdl.GPU_SAMPLECOUNT_4,
},
.depth_stencil_state = presets.DEPTH_ENABLED, .depth_stencil_state = presets.DEPTH_ENABLED,
.target_info = .{ .target_info = .{
.depth_stencil_format = sdl.GPU_TEXTUREFORMAT_D16_UNORM, .depth_stencil_format = DEPTH_FORMAT,
.color_target_descriptions = &sdl.GPUColorTargetDescription{ .color_target_descriptions = &sdl.GPUColorTargetDescription{
.format = target_format, .format = target_format,
.blend_state = presets.BLEND_NORMAL, .blend_state = presets.BLEND_NORMAL,
@@ -161,20 +168,20 @@ pub fn create() void {
Graphics.camera = Camera{ Graphics.camera = Camera{
.transform = .{}, .transform = .{},
.near = 1.0, .near = 1.0 / 16.0,
.far = 1024.0, .far = 128.0,
.lens = 1.5, .lens = 1.5,
.aspect = 16.0 / 9.0, .aspect = 16.0 / 9.0,
.matrix = undefined,
}; };
} }
pub fn destroy() void { pub fn destroy() void {
sdl.ReleaseWindowFromGPUDevice(Graphics.device, Graphics.window); sdl.ReleaseWindowFromGPUDevice(Graphics.device, Graphics.window);
sdl.DestroyRenderer(Graphics.renderer);
sdl.DestroyWindow(Graphics.window); sdl.DestroyWindow(Graphics.window);
sdl.ReleaseGPUGraphicsPipeline(Graphics.device, Graphics.pipeline); 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.ReleaseGPUTexture(Graphics.device, Graphics.depth_texture);
sdl.ReleaseGPUBuffer(Graphics.device, Graphics.vertex_buffer); sdl.ReleaseGPUBuffer(Graphics.device, Graphics.vertex_buffer);
sdl.ReleaseGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer); sdl.ReleaseGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer);
@@ -189,67 +196,6 @@ pub fn destroy() void {
sdl.DestroyGPUDevice(Graphics.device); sdl.DestroyGPUDevice(Graphics.device);
} }
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 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());
@memcpy(map, texture_bytes);
sdl.UnmapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer);
sdl.UploadToGPUTexture(copy_pass, &sdl.GPUTextureTransferInfo{
.offset = 0,
.pixels_per_row = width,
.rows_per_layer = height,
.transfer_buffer = Graphics.transfer_buffer,
}, &sdl.GPUTextureRegion{
.texture = texture,
.mip_level = 0,
.layer = 0,
.x = 0,
.y = 0,
.z = 0,
.w = width,
.h = height,
.d = 1,
}, false);
}
if (!sdl.SubmitGPUCommandBuffer(temp_command_buffer)) err.sdl();
const sampler = sdl.CreateGPUSampler(Graphics.device, &sdl.GPUSamplerCreateInfo{
.address_mode_u = sdl.GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
.address_mode_v = sdl.GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
.address_mode_w = sdl.GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
.mag_filter = sdl.GPU_FILTER_NEAREST,
.min_filter = sdl.GPU_FILTER_LINEAR,
}) orelse err.sdl();
return .{
texture,
sampler,
};
}
pub fn unloadTexture(texture: *sdl.GPUTexture, sampler: *sdl.GPUSampler) void {
sdl.ReleaseGPUSampler(Graphics.device, sampler);
sdl.ReleaseGPUTexture(Graphics.device, texture);
}
pub fn loadMesh(mesh_bytes: []const u8) Mesh { pub fn loadMesh(mesh_bytes: []const u8) Mesh {
std.debug.assert(mesh_bytes.len < Graphics.transfer_buffer_capacity); std.debug.assert(mesh_bytes.len < Graphics.transfer_buffer_capacity);
@@ -261,7 +207,7 @@ pub fn loadMesh(mesh_bytes: []const u8) Mesh {
Graphics.growVertexBuffer(Graphics.vertex_buffer_capacity * size_mult); 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); @memcpy(@as([*]u8, @ptrCast(map)), mesh_bytes);
sdl.UnmapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer); sdl.UnmapGPUTransferBuffer(Graphics.device, Graphics.transfer_buffer);
@@ -346,29 +292,27 @@ fn growVertexBuffer(new_size: usize) void {
/// Otherwise `command_buffer` and `render_pass` are both set /// Otherwise `command_buffer` and `render_pass` are both set
pub fn beginDraw() bool { pub fn beginDraw() bool {
Graphics.command_buffer = sdl.AcquireGPUCommandBuffer(Graphics.device) orelse err.sdl(); Graphics.command_buffer = sdl.AcquireGPUCommandBuffer(Graphics.device) orelse err.sdl();
if (Graphics.to_resize) |new_size| {
Graphics.resetTextures(new_size[0], new_size[1]);
Graphics.camera.aspect = @as(f32, @floatFromInt(new_size[0])) / @as(f32, @floatFromInt(new_size[1]));
Graphics.window_size = new_size;
Graphics.to_resize = null;
}
var render_target: ?*sdl.GPUTexture = null;
var width: u32 = 0; var width: u32 = 0;
var height: u32 = 0; var height: u32 = 0;
if (!sdl.WaitAndAcquireGPUSwapchainTexture(Graphics.command_buffer, Graphics.window, &render_target, &width, &height)) err.sdl(); if (!sdl.WaitAndAcquireGPUSwapchainTexture(Graphics.command_buffer, Graphics.window, &Graphics.render_target, &width, &height)) err.sdl();
// Hidden // Window is probably hidden
if (render_target == null) return false; if (Graphics.render_target == null or width == 0 or height == 0) return false;
Graphics.render_pass = sdl.BeginGPURenderPass(Graphics.command_buffer, &.{ if (width != Graphics.window_width or height != Graphics.window_height) {
Graphics.resetTextures(width, height);
Graphics.camera.aspect = @as(f32, @floatFromInt(width)) / @as(f32, @floatFromInt(height));
Graphics.window_width = width;
Graphics.window_height = height;
}
Graphics.render_pass = sdl.BeginGPURenderPass(Graphics.command_buffer.?, &.{
.clear_color = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0 }, .clear_color = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0 },
.cycle = false, .cycle = false,
.load_op = sdl.GPU_LOADOP_CLEAR, .load_op = sdl.GPU_LOADOP_CLEAR,
.store_op = sdl.GPU_STOREOP_RESOLVE, .store_op = sdl.GPU_STOREOP_STORE,
// .store_op = sdl.GPU_STOREOP_STORE,
.resolve_texture = render_target,
.mip_level = 0, .mip_level = 0,
.texture = Graphics.msaa_resolve, .texture = Graphics.fsaa_target,
}, 1, &.{ }, 1, &.{
.clear_depth = 1.0, .clear_depth = 1.0,
.load_op = sdl.GPU_LOADOP_CLEAR, .load_op = sdl.GPU_LOADOP_CLEAR,
@@ -380,14 +324,39 @@ pub fn beginDraw() bool {
sdl.BindGPUGraphicsPipeline(Graphics.render_pass, Graphics.pipeline); sdl.BindGPUGraphicsPipeline(Graphics.render_pass, Graphics.pipeline);
sdl.BindGPUVertexBuffers(Graphics.render_pass, 0, &.{ .offset = 0, .buffer = Graphics.vertex_buffer }, 1); 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; return true;
} }
pub fn drawMesh(mesh: Mesh, texture: Assets.Texture, transform: Transform) void { pub fn clearDepth() void {
sdl.EndGPURenderPass(Graphics.render_pass.?);
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_LOAD,
.store_op = sdl.GPU_STOREOP_STORE,
.mip_level = 0,
.texture = Graphics.fsaa_target,
}, 1, &.{
.clear_depth = 1.0,
.load_op = sdl.GPU_LOADOP_CLEAR,
.store_op = sdl.GPU_STOREOP_DONT_CARE,
.stencil_load_op = sdl.GPU_STOREOP_DONT_CARE,
.stencil_store_op = sdl.GPU_STOREOP_DONT_CARE,
.texture = Graphics.depth_texture,
}) orelse err.sdl();
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);
}
pub fn drawMesh(mesh: Mesh, texture: *Assets.Texture, transform: Transform) void {
if (Graphics.render_pass == null) return; if (Graphics.render_pass == null) return;
const asset_texture = Assets.get(texture) orelse return; const asset_texture = texture.get() orelse return;
sdl.PushGPUVertexUniformData(Graphics.command_buffer, 1, &transform.matrix(), 16 * 4); sdl.PushGPUVertexUniformData(Graphics.command_buffer, 1, &transform.matrix(), 16 * 4);
sdl.BindGPUFragmentSamplers(Graphics.render_pass, 0, &sdl.GPUTextureSamplerBinding{ sdl.BindGPUFragmentSamplers(Graphics.render_pass, 0, &sdl.GPUTextureSamplerBinding{
@@ -402,6 +371,23 @@ pub fn endDraw() void {
defer Graphics.render_pass = null; defer Graphics.render_pass = null;
if (Graphics.render_pass) |pass| { if (Graphics.render_pass) |pass| {
sdl.EndGPURenderPass(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_width,
.h = Graphics.window_height,
.mip_level = fsaa_level - 1,
},
.destination = .{
.texture = Graphics.render_target,
.w = Graphics.window_width,
.h = Graphics.window_height,
},
.load_op = sdl.GPU_LOADOP_DONT_CARE,
.filter = sdl.GPU_FILTER_NEAREST,
});
} }
if (!sdl.SubmitGPUCommandBuffer(Graphics.command_buffer)) err.sdl(); if (!sdl.SubmitGPUCommandBuffer(Graphics.command_buffer)) err.sdl();
} }
@@ -410,7 +396,7 @@ fn loadShader(path: []const u8, info: sdl.GPUShaderCreateInfo) *sdl.GPUShader {
const file = std.fs.cwd().openFile(path, .{}) catch |e| err.file(e, path); const file = std.fs.cwd().openFile(path, .{}) catch |e| err.file(e, path);
defer file.close(); 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); defer std.heap.c_allocator.free(code);
var updated_info = info; var updated_info = info;
@@ -419,44 +405,75 @@ fn loadShader(path: []const u8, info: sdl.GPUShaderCreateInfo) *sdl.GPUShader {
return sdl.CreateGPUShader(device, &updated_info) orelse err.sdl(); return sdl.CreateGPUShader(device, &updated_info) orelse err.sdl();
} }
fn createDepthTexture(width: u32, height: u32) *sdl.GPUTexture { pub fn createTexture(width: u32, height: u32, format: c_uint, usage: c_uint, mip_level: 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 {
return sdl.CreateGPUTexture(device, &.{ return sdl.CreateGPUTexture(device, &.{
.format = format, .format = format,
.layer_count_or_depth = 1, .layer_count_or_depth = 1,
.width = width, .width = width,
.height = height, .height = height,
.num_levels = 1, .num_levels = mip_level,
.sample_count = sdl.GPU_SAMPLECOUNT_4, .sample_count = sdl.GPU_SAMPLECOUNT_1,
.usage = sdl.GPU_TEXTUREUSAGE_COLOR_TARGET, .usage = usage,
}) orelse err.sdl(); }) orelse err.sdl();
} }
pub fn freeTexture(texture: *sdl.GPUTexture) void {
sdl.ReleaseGPUTexture(Graphics.device, texture);
}
pub fn createSampler(mip_level: u32) *sdl.GPUSampler {
return sdl.CreateGPUSampler(Graphics.device, &sdl.GPUSamplerCreateInfo{
.address_mode_u = sdl.GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
.address_mode_v = sdl.GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
.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 = @floatFromInt(mip_level - 1),
.mip_lod_bias = -0.5,
}) orelse err.sdl();
}
pub fn freeSampler(sampler: *sdl.GPUSampler) void {
sdl.ReleaseGPUSampler(Graphics.device, sampler);
}
fn resetTextures(width: u32, height: u32) void { fn resetTextures(width: u32, height: u32) void {
sdl.ReleaseGPUTexture(Graphics.device, Graphics.depth_texture); 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); const target_format = sdl.GetGPUSwapchainTextureFormat(Graphics.device, Graphics.window);
sdl.ReleaseGPUTexture(Graphics.device, Graphics.msaa_resolve); sdl.ReleaseGPUTexture(Graphics.device, Graphics.fsaa_target);
Graphics.msaa_resolve = createTexture(width, height, target_format); Graphics.fsaa_target = createTexture(
} width * Graphics.fsaa_scale,
height * Graphics.fsaa_scale,
pub fn resize(width: u32, height: u32) void { target_format,
Graphics.to_resize = .{ width, height }; sdl.GPU_TEXTUREUSAGE_COLOR_TARGET | sdl.GPU_TEXTUREUSAGE_SAMPLER,
fsaa_level,
);
} }
pub fn windowId() sdl.WindowID { pub fn windowId() sdl.WindowID {
return sdl.GetWindowID(Graphics.window); return sdl.GetWindowID(Graphics.window);
} }
pub fn generatePlane(x0: f32, y0: f32, x1: f32, y1: f32, w: f32, h: f32) [30]f32 {
const hw = w * 0.5;
const hh = h * 0.5;
return .{
-hw, -hh, 0, x0, y1,
hw, hh, 0, x1, y0,
-hw, hh, 0, x0, y0,
hw, hh, 0, x1, y0,
-hw, -hh, 0, x0, y1,
hw, -hh, 0, x1, y1,
};
}

View File

@@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const sdl = @import("sdl"); const sdl = @import("sdl");
const math = @import("../math.zig");
const Transform = @import("transform.zig"); const Transform = @import("transform.zig");
const Camera = @This(); const Camera = @This();
@@ -11,7 +12,11 @@ far: f32,
/// width = height * aspect /// width = height * aspect
aspect: f32, 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 xx = 1.0 / (camera.lens * camera.aspect);
const yy = 1.0 / camera.lens; const yy = 1.0 / camera.lens;
const fnmod = 1.0 / (camera.far - camera.near); const fnmod = 1.0 / (camera.far - camera.near);
@@ -23,5 +28,81 @@ pub fn matrix(camera: Camera) @Vector(16, f32) {
0, 0, -zz, wz, 0, 0, -zz, wz,
0, 0, -1, 0, 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, width: f32, height: f32) bool {
@setFloatMode(.optimized);
const matrix = Transform.multiplyMatrix(camera.matrix, quad_transform.matrix());
const hw = width * 0.5;
const hh = height * 0.5;
const pi: [4]@Vector(2, f32) = .{
.{ -hw, -hh },
.{ -hw, hh },
.{ hw, hh },
.{ hw, -hh },
};
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);
} }

View File

@@ -1,25 +1,25 @@
const std = @import("std"); const std = @import("std");
const math = @import("../math.zig");
const Transform = @This(); const Transform = @This();
pub const TMatrix = @Vector(16, f32); pub const TMatrix = @Vector(16, f32);
pub const Position = @Vector(3, f32); pub const Position = @Vector(3, f32);
pub const Rotation = @Vector(4, f32); pub const Rotation = @Vector(4, f32);
pub const Scale = @Vector(3, f32); pub const Scale = f32;
position: Position = @splat(0.0), position: Position = @splat(0.0),
rotation: Rotation = .{ 1.0, 0.0, 0.0, 0.0 }, rotation: Rotation = .{ 1.0, 0.0, 0.0, 0.0 },
scale: Scale = @splat(1.0), scale: Scale = 1.0,
pub fn matrix(transform: Transform) TMatrix { pub fn matrix(transform: Transform) TMatrix {
@setFloatMode(.optimized); @setFloatMode(.optimized);
const r = rotationMatrix(transform.rotation); const r = rotationMatrix(transform.rotation) * @as(@Vector(9, f32), @splat(transform.scale));
const sx, const sy, const sz = transform.scale;
return .{ return .{
sx * r[0], sy * r[1], sz * r[2], transform.position[0], r[0], r[1], r[2], transform.position[0],
sx * r[3], sy * r[4], sz * r[5], transform.position[1], r[3], r[4], r[5], transform.position[1],
sx * r[6], sy * r[7], sz * r[8], transform.position[2], r[6], r[7], r[8], transform.position[2],
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0,
}; };
} }
@@ -27,24 +27,12 @@ pub fn matrix(transform: Transform) TMatrix {
pub fn inverseMatrix(transform: Transform) TMatrix { pub fn inverseMatrix(transform: Transform) TMatrix {
@setFloatMode(.optimized); @setFloatMode(.optimized);
const r = rotationMatrix(flipRotation(transform.rotation)); const r = rotationMatrix(flipRotation(transform.rotation)) * @as(@Vector(9, f32), @splat(1.0 / transform.scale));
const tx, const ty, const tz = transform.position; const tx, const ty, const tz = transform.position;
const sx = 1.0 / transform.scale[0];
const sy = 1.0 / transform.scale[1];
const sz = 1.0 / transform.scale[2];
const r0 = r[0] * sx;
const r1 = r[1] * sx;
const r2 = r[2] * sx;
const r3 = r[3] * sy;
const r4 = r[4] * sy;
const r5 = r[5] * sy;
const r6 = r[6] * sz;
const r7 = r[7] * sz;
const r8 = r[8] * sz;
return .{ return .{
r0, r1, r2, -(r0 * tx + r1 * ty + r2 * tz), r[0], r[1], r[2], -(r[0] * tx + r[1] * ty + r[2] * tz),
r3, r4, r5, -(r3 * tx + r4 * ty + r5 * tz), r[3], r[4], r[5], -(r[3] * tx + r[4] * ty + r[5] * tz),
r6, r7, r8, -(r6 * tx + r7 * ty + r8 * tz), r[6], r[7], r[8], -(r[6] * tx + r[7] * ty + r[8] * tz),
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0,
}; };
} }
@@ -94,6 +82,7 @@ pub fn rotateVector(vector: Position, rotation: Rotation) Position {
pub fn rotate(transform: *Transform, rotation: Rotation) void { pub fn rotate(transform: *Transform, rotation: Rotation) void {
@setFloatMode(.optimized); @setFloatMode(.optimized);
// Also rotate `Position` around the origin?
transform.rotation = normalizeRotation(combineRotations(transform.rotation, rotation)); transform.rotation = normalizeRotation(combineRotations(transform.rotation, rotation));
} }
@@ -148,6 +137,8 @@ pub fn normalizeRotation(r: Rotation) Rotation {
} }
} }
/// a: Child `Rotation`
/// b: Parent `Rotation`
pub fn combineRotations(a: Rotation, b: Rotation) Rotation { pub fn combineRotations(a: Rotation, b: Rotation) Rotation {
@setFloatMode(.optimized); @setFloatMode(.optimized);
@@ -230,3 +221,31 @@ pub fn multiplyMatrix(a: TMatrix, b: TMatrix) TMatrix {
} }
return output; return output;
} }
/// a: Child `Transform`
/// b: Parent `Transform`
pub fn combineTransforms(a: Transform, b: Transform) Transform {
@setFloatMode(.optimized);
return Transform{
.position = rotateVector(a.position, b.rotation) * @as(Position, @splat(b.scale)) + b.position,
.rotation = combineRotations(a.rotation, b.rotation),
.scale = a.scale * b.scale,
};
}
pub fn lerpTransformTimeLn(a: Transform, b: Transform, t: f32, lnf: f32) Transform {
return lerpTransform(b, a, @exp(lnf * t));
}
pub fn lerpTransform(a: Transform, b: Transform, f: f32) Transform {
@setFloatMode(.optimized);
return Transform{
.position = math.lerp(a.position, b.position, f),
.rotation = math.slerp(a.rotation, b.rotation, f),
.scale = math.lerp(a.scale, b.scale, f),
};
}
pub const ZERO = Transform{};

View File

@@ -36,15 +36,21 @@ pub inline fn slerpTime(a: anytype, b: anytype, t: f32, comptime f: f32) @TypeOf
pub fn slerpTimeLn(a: anytype, b: anytype, t: f32, lnf: f32) @TypeOf(a, b) { pub fn slerpTimeLn(a: anytype, b: anytype, t: f32, lnf: f32) @TypeOf(a, b) {
@setFloatMode(.optimized); @setFloatMode(.optimized);
return slerp(b, a, @exp(lnf * t));
}
pub fn slerp(a: anytype, b: anytype, f: f32) @TypeOf(a, b) {
@setFloatMode(.optimized);
const cos = @reduce(.Add, a * b); const cos = @reduce(.Add, a * b);
if (cos > 0.999) { if (cos > 0.999) {
return lerpTimeLn(a, b, t, lnf); return lerp(a, b, f);
} }
const angle = std.math.acos(cos); const angle = std.math.acos(cos);
const a_angle_factor = @exp(lnf * t); const a_angle_factor = 1 - f;
const b_angle_factor = 1.0 - a_angle_factor; const b_angle_factor = f;
const rev_angle_sin = 1.0 / std.math.sin(angle); const rev_angle_sin = 1.0 / std.math.sin(angle);
const a_sin = std.math.sin(a_angle_factor * angle); const a_sin = std.math.sin(a_angle_factor * angle);
@@ -106,3 +112,73 @@ pub fn dotInt(a: anytype, b: anytype) (@typeInfo(@TypeOf(a)).vector.child) {
return @reduce(.Add, a * b); return @reduce(.Add, a * b);
} }
pub fn Sway(comptime T: type) type {
const STABILIZATION = -1;
return packed struct {
value: T = 0,
velocity: T = 0,
frequency: T,
amplitude: T,
const Self = @This();
pub fn update(self: *Self, delta: f32) void {
@setFloatMode(.optimized);
const dist = delta * -2 * std.math.pi * self.frequency;
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) {
self.value = 0.001;
self.velocity = 0;
len = 0.001;
}
const new_value = self.value * cos - self.velocity * sin;
const new_velocity = self.value * sin + self.velocity * cos;
const target_len = lerpTimeLn(len, self.amplitude, delta, STABILIZATION);
const mult = target_len / len;
self.value = new_value * mult;
self.velocity = new_velocity * mult;
}
};
}
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;
}
pub fn norm(vector: anytype) @TypeOf(vector) {
const len = length(vector);
if (len < 1.01 and len > 0.99) return vector;
if (len < 1e-10) {
var output = @as(@TypeOf(vector), @splat(0));
output[@typeInfo(@TypeOf(vector)).vector.len - 1] = 1;
return output;
}
return vector * @as(@TypeOf(vector), @splat(1.0 / len));
}

View File

@@ -3,7 +3,15 @@ const key_store = @import("data/keystore.zig");
buttons: key_store.KeyStore(@TypeOf(sdl.BUTTON_LEFT), 4, 0) = .{}, buttons: key_store.KeyStore(@TypeOf(sdl.BUTTON_LEFT), 4, 0) = .{},
x: f32 = 0, x_screen: f32 = 0,
y: f32 = 0, y_screen: f32 = 0,
x_norm: f32 = 0,
y_norm: f32 = 0,
dx: f32 = 0, dx: f32 = 0,
dy: f32 = 0, dy: f32 = 0,
wheel: i32 = 0,
pub fn reset(mouse: *@This()) void {
mouse.buttons.reset();
mouse.wheel = 0;
}

5
src/offline.zig Normal file
View File

@@ -0,0 +1,5 @@
const client = @import("client.zig");
pub fn main() void {
client.main();
}

5
src/server.zig Normal file
View File

@@ -0,0 +1,5 @@
const std = @import("std");
pub fn main() void {
std.debug.print("Server started!\n", .{});
}

View File

@@ -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 };
}

View File

@@ -1,140 +1,630 @@
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 Graphics = @import("graphics.zig");
const Assets = @import("assets.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; const Id = u32;
var next_stop: Time = undefined; const Order = i32;
var entities: comp.Storage(Entity, .{}) = undefined;
pub var plane_mesh: Graphics.Mesh = undefined; pub var object_map: std.AutoHashMapUnmanaged(Id, usize) = .{};
pub var objects: std.ArrayListUnmanaged(Object) = .{};
pub var hand_mesh: Graphics.Mesh = undefined;
pub var cube_mesh: Graphics.Mesh = undefined; pub var cube_mesh: Graphics.Mesh = undefined;
pub var texture: Assets.Texture = undefined; pub var table_mesh: Graphics.Mesh = undefined;
pub var cubemap_mesh: Graphics.Mesh = undefined;
pub var table_texture: Assets.Texture = undefined;
pub var hand_texture: Assets.Texture = undefined;
pub var cubemap_texture: Assets.Texture = undefined;
pub var camera_position: @Vector(2, f32) = @splat(0);
pub var hand_transform: Graphics.Transform = .{};
pub var dock_transform: Graphics.Transform = .{};
pub var zoom: i32 = 0;
pub var hover: ?Id = null;
pub var panning = false;
pub var hand_objects: u32 = 0;
pub var hand_scale: f32 = 0;
pub var dock_objects: u32 = 0;
pub var dock_last_width: f32 = 0;
pub var dock_focused: bool = false;
pub var dock_spacing: f32 = 0;
pub var min_order: Order = undefined;
pub var max_order: Order = undefined;
const DOCK_TILT = 0.03;
const DOCK_TILT_SIN = std.math.sin(DOCK_TILT);
const DOCK_TILT_COS = std.math.cos(DOCK_TILT);
const Object = struct {
type: Type,
transform: Graphics.Transform = .{},
target_transform: Graphics.Transform = .{},
width: f32,
height: f32,
mesh: Graphics.Mesh,
texture: Assets.Texture,
order: Order,
id: Id,
index: u32,
z: u32 = 0,
parent: Parent = .none,
parent_index: u32 = 0,
child_last_id: u32 = 0,
influence: f32 = 0,
const Type = enum {
card,
deck,
};
const Parent = union(enum) {
none,
hand,
dock,
deck: Id,
};
pub fn reparent(self: *@This(), new_parent: Parent) void {
self.transform = self.drawingTransform();
self.influence = 0;
self.parent = new_parent;
}
pub fn drawingTransform(self: @This()) Graphics.Transform {
const transform = self.transform;
const parent_transform = switch (self.parent) {
.hand => World.hand_transform,
.dock => World.dock_transform,
.deck => |deck| if (World.getObject(deck)) |object| object.drawingTransform() else Graphics.Transform{},
.none => return transform,
};
return Graphics.Transform.combineTransforms(
transform,
Graphics.Transform.lerpTransform(
.{},
parent_transform,
self.influence,
),
);
}
};
const World = @This(); const World = @This();
pub fn initDebug() void { pub fn initDebug() void {
entities = comp.Storage(Entity, .{}).init(); for (0..70) |i| {
_ = entities.add(.{ World.objects.append(Game.alloc, .{
.position = .{ 0, 0 }, .type = .card,
.player = true, .width = 0.5,
}); .height = 0.5,
_ = entities.add(.{ .mesh = Graphics.loadMesh(@ptrCast(&Graphics.generatePlane(
.position = .{ 2, 0 }, @as(f32, @floatFromInt(9 + i / 10)) / 16.0,
.enemy = true, @as(f32, @floatFromInt(0 + i % 10)) / 16.0,
.controller = .{ @as(f32, @floatFromInt(10 + i / 10)) / 16.0,
.move_units = 0.25, @as(f32, @floatFromInt(1 + i % 10)) / 16.0,
}, 0.5,
}); 0.5,
_ = entities.add(.{ ))),
.position = .{ 3, 0 }, .texture = Assets.load(.texture, "data/yakuza.png"),
.enemy = true, .order = @intCast(i),
.controller = .{ .id = @intCast(i),
.move_units = 0.25, .index = @intCast(i),
}, .parent = .{ .deck = if (i < 60) @as(Id, 70) else @as(Id, 71) },
}); }) catch err.oom();
time = Time.ZERO; 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.objects.append(Game.alloc, .{
World.texture = Assets.load(.texture, "data/wawa.png"); .target_transform = .{ .position = .{ -3, 0, 0 } },
.type = .deck,
.width = 1,
.height = 1,
.mesh = Graphics.loadMesh(@ptrCast(&Graphics.generatePlane(
0.0 / 8.0,
4.0 / 8.0,
1.0 / 8.0,
5.0 / 8.0,
1,
1,
))),
.texture = Assets.load(.texture, "data/yakuza.png"),
.order = 70,
.id = 70,
.index = 70,
}) catch err.oom();
World.object_map.put(Game.alloc, 60, 60) catch err.oom();
World.objects.append(Game.alloc, .{
.target_transform = .{ .position = .{ 3, 0, 0 } },
.type = .deck,
.width = 1,
.height = 1,
.mesh = Graphics.loadMesh(@ptrCast(&Graphics.generatePlane(
0.0 / 8.0,
4.0 / 8.0,
1.0 / 8.0,
5.0 / 8.0,
1,
1,
))),
.texture = Assets.load(.texture, "data/yakuza.png"),
.order = 71,
.id = 71,
.index = 71,
}) catch err.oom();
World.object_map.put(Game.alloc, 71, 71) catch err.oom();
World.table_mesh = Graphics.loadMesh(@ptrCast(&Graphics.generatePlane(0, 0, 0.5, 0.5, 8, 8)));
World.hand_mesh = Graphics.loadMesh(@ptrCast(&PLANE_MESH_DATA));
World.cubemap_mesh = Graphics.loadMesh(@ptrCast(&CUBEMAP_MESH_DATA));
World.table_texture = Assets.load(.texture, "data/yakuza.png");
World.hand_texture = Assets.load(.texture, "data/hand.png");
World.cubemap_texture = Assets.load(.texture, "data/cubemap.png");
World.camera_position = @splat(0);
World.hand_transform = .{};
World.hand_scale = 0.5;
World.dock_transform = .{
.position = .{ 0, 0, 4 },
};
World.dock_spacing = 0.2;
World.zoom = 0;
World.panning = false;
World.dock_focused = false;
World.min_order = 0;
World.max_order = 71;
} }
pub fn deinit() void { pub fn deinit() void {
Graphics.unloadMesh(World.plane_mesh); Graphics.unloadMesh(World.table_mesh);
Graphics.unloadMesh(World.cube_mesh); Graphics.unloadMesh(World.hand_mesh);
Assets.free(World.texture); Graphics.unloadMesh(World.cubemap_mesh);
World.entities.deinit(); Assets.free(World.table_texture);
Assets.free(World.hand_texture);
Assets.free(World.cubemap_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 { pub fn update(delta: f32) void {
const update_until = World.time.plus(Time.durationFromUnits(delta)); World.updateCamera(delta);
while (!World.time.past(update_until)) { {
const current = Time.earliest(World.next_stop, update_until); World.dock_transform = Graphics.Transform.lerpTransformTimeLn(
defer World.time = current; World.dock_transform,
Graphics.Transform.combineTransforms(.{ .position = .{
0,
-1,
-1 / Graphics.camera.lens,
} }, Graphics.camera.transform),
delta,
-128,
);
}
{
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){ 0, 0, 0.2 },
delta,
-24,
);
}
var iter = World.entities.iter(); World.updateOrder();
while (iter.next()) |entity| {
entity.update(); World.hover = null;
World.hand_objects = 0;
World.dock_objects = 0;
for (World.objects.items) |*object| {
updateHover(object);
}
for (World.objects.items) |*object| {
updateObject(object, delta);
}
World.updateControls();
}
pub fn updateControls() void {
if (Game.keyboard.keys.is_pressed(sdl.SDL_SCANCODE_LSHIFT)) {
World.scroll(Game.mouse.wheel);
} else {
World.zoom = std.math.clamp(World.zoom + Game.mouse.wheel, -4, 8);
}
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();
}
if (Game.mouse.y_norm <= -0.8) {
World.dock_focused = true;
}
if (Game.mouse.y_norm >= -0.6) {
World.dock_focused = false;
}
}
pub fn scroll(delta: i32) void {
if (World.getHover()) |hover_object| {
if (hover_object.type == .deck) {
if (delta > 0) {
var left_to_put = delta;
var i = World.objects.items.len - 1;
while (left_to_put > 0) {
const object = &World.objects.items[i];
if (object.parent != .hand) {
if (i == 0) break;
i -= 1;
continue;
}
World.bringToTop(object);
object.reparent(.{ .deck = hover_object.id });
if (i == 0) break;
i -= 1;
left_to_put -= 1;
}
}
if (delta < 0) {
var left_to_take = -delta;
var i = World.objects.items.len - 1;
while (left_to_take > 0) {
const object = &World.objects.items[i];
if (object.parent != .deck or object.parent.deck != hover_object.id) {
if (i == 0) break;
i -= 1;
continue;
}
World.bringToTop(object);
object.reparent(.hand);
if (i == 0) break;
i -= 1;
left_to_take -= 1;
}
}
return;
}
}
if (delta > 0 and World.hand_objects > 0) {
var left_to_scroll = @rem(delta, @as(i32, @intCast(World.hand_objects)));
var i = World.objects.items.len - 1;
while (left_to_scroll > 0) : (i -= 1) {
const object = &World.objects.items[i];
if (object.parent != .hand) continue;
World.bringToBottom(object);
left_to_scroll -= 1;
}
}
if (delta < 0 and World.hand_objects > 0) {
var left_to_scroll = @rem(-delta, @as(i32, @intCast(World.hand_objects)));
var i: usize = 0;
while (left_to_scroll > 0) : (i += 1) {
const object = &World.objects.items[i];
if (object.parent != .hand) continue;
World.bringToTop(object);
left_to_scroll -= 1;
} }
} }
} }
pub fn draw(delta: f32) void { pub fn tryPick() bool {
Graphics.drawMesh(World.plane_mesh, World.texture, .{ .scale = @splat(5) }); var object = World.getHover() orelse return false;
var iter = World.entities.iter(); switch (object.type) {
while (iter.next()) |entity| { .card => {},
entity.draw(delta); .deck => {
if (!Game.keyboard.keys.is_pressed(sdl.SDL_SCANCODE_LSHIFT)) {
for (World.objects.items) |*child| {
if (child.parent == .deck and child.parent.deck == object.id and child.id == object.child_last_id) {
object = child;
}
}
}
},
}
World.panning = false;
object.reparent(.hand);
World.bringToTop(object);
return true;
}
pub fn tryRelease() bool {
const object = blk: {
var i = World.objects.items.len - 1;
while (true) {
const object = &World.objects.items[i];
if (object.parent == .hand) {
break :blk object;
}
if (i > 0)
i -= 1
else
return false;
}
};
object.target_transform.position = World.hand_transform.position;
World.bringToTop(object);
if (object.type == .card and !Game.keyboard.keys.is_pressed(sdl.SDL_SCANCODE_LSHIFT)) {
if (World.getHover()) |hover_object| {
if (hover_object.type == .deck) {
object.reparent(.{ .deck = hover_object.id });
return true;
}
}
}
if (World.dock_focused) {
object.reparent(.dock);
return true;
} else {
object.reparent(.none);
return true;
} }
} }
pub fn requestUpdate(at: Time) void { pub fn updateHover(object: *Object) void {
World.next_stop = Time.earliest(at, World.next_stop); switch (object.parent) {
.deck => |id| {
if (World.getObject(id)) |deck| {
deck.child_last_id = object.id;
}
},
.none => {
if (!World.dock_focused and Graphics.camera.mouse_in_quad(.{ Game.mouse.x_norm, Game.mouse.y_norm }, object.drawingTransform(), object.width, object.height)) {
if (World.hover == null or World.getHover().?.z < object.z) {
World.hover = object.id;
}
}
},
.hand => {
object.parent_index = World.hand_objects;
World.hand_objects += 1;
},
.dock => {
object.parent_index = World.dock_objects;
World.dock_last_width = object.width * object.target_transform.scale;
World.dock_objects += 1;
if (World.dock_focused and Graphics.camera.mouse_in_quad(.{ Game.mouse.x_norm, Game.mouse.y_norm }, object.transform.combineTransforms(World.dock_transform), object.width, object.height)) {
if (World.hover == null or World.getObject(World.hover.?).?.z < object.z) {
World.hover = object.id;
}
}
},
}
} }
pub fn entityAt(position: @Vector(2, i32)) ?*Entity { pub fn updateObject(object: *Object, delta: f32) void {
var iter = World.entities.iter(); switch (object.parent) {
while (iter.next()) |entity| { .none => {
if (@reduce(.And, entity.position == position)) object.target_transform.position[2] = @as(f32, 0.001) * @as(f32, @floatFromInt(object.index + 1));
return entity; object.target_transform.scale = if (World.hover == object.id) @as(f32, 1.1) else @as(f32, 1);
},
.hand => {
var target_position = @as(@Vector(3, f32), @splat(0));
var target_scale: f32 = 1.0;
target_position[2] -= 0.001;
const hand_order = hand_objects - object.parent_index - 1;
switch (hand_order) {
0 => {},
else => |i| {
target_position[0] += World.hand_scale * if (i & 2 == 0) @as(f32, 1) else @as(f32, 1.5);
target_position[1] += World.hand_scale * if ((i - 1) & 2 == 0) @as(f32, -0.25) else @as(f32, -0.75);
target_position[2] -= @as(f32, @floatFromInt((hand_order - 1) / 4)) * 0.001;
target_scale = 0.5;
},
}
object.target_transform.position = target_position;
object.target_transform.scale = target_scale;
},
.dock => {
var topleft_x = -World.dock_last_width * 0.5 * DOCK_TILT_COS + World.dock_spacing * (@as(f32, @floatFromInt(object.parent_index)) - @as(f32, @floatFromInt(World.dock_objects - 1)) * 0.5);
const total_w = @as(f32, @floatFromInt(World.dock_objects - 1)) * World.dock_spacing + World.dock_last_width * DOCK_TILT_COS;
if (total_w > Graphics.camera.aspect * 2) {
topleft_x += math.lerp(0, Graphics.camera.aspect - total_w * 0.5, Game.mouse.x_norm);
}
const hit = World.hover == object.id;
const topleft_y = if (World.dock_focused) if (hit) @as(f32, 0.5) else @as(f32, 0.3) else @as(f32, 0.2);
object.target_transform.position = .{
topleft_x + object.width * 0.5 * object.target_transform.scale * DOCK_TILT_COS,
topleft_y - object.height * 0.5 * object.target_transform.scale,
if (hit) @as(f32, 0.02) else -object.width * 0.5 * DOCK_TILT_SIN,
};
object.target_transform.rotation = if (hit)
Graphics.Transform.ZERO.rotation
else
Graphics.Transform.rotationByAxis(.{ 0, 1, 0 }, DOCK_TILT);
},
.deck => {
object.target_transform.position = .{ 0, 0, @as(f32, 0.001) };
object.target_transform.scale = if (World.hover == object.id) @as(f32, 1.1) else @as(f32, 1);
},
}
object.z = switch (object.parent) {
.deck => |deck_id| if (World.getObject(deck_id)) |deck| deck.z + object.index else object.index,
.none, .hand, .dock => object.index,
};
if (object.parent != .none) {
object.influence = math.lerpTimeLn(
object.influence,
1.0,
delta,
-24,
);
}
object.transform = Graphics.Transform.lerpTransformTimeLn(
object.transform,
object.target_transform,
delta,
-24,
);
}
pub fn draw() void {
Graphics.drawMesh(World.table_mesh, &World.table_texture, .{});
for (World.objects.items) |*object| {
sw: switch (object.parent) {
.none, .hand => {
Graphics.drawMesh(object.mesh, &object.texture, object.drawingTransform());
},
.dock => {},
.deck => |id| {
if (World.getObject(id)) |deck| {
if (deck.child_last_id != object.id) continue;
}
continue :sw .none;
},
}
}
Graphics.drawMesh(
World.hand_mesh,
&World.hand_texture,
Graphics.Transform.combineTransforms(
.{
.position = .{ World.hand_scale * 0.5, -World.hand_scale * 0.5, 0 },
.scale = World.hand_scale,
},
World.hand_transform,
),
);
Graphics.drawMesh(World.cubemap_mesh, &World.cubemap_texture, .{
.scale = Graphics.camera.far,
.position = Graphics.camera.transform.position,
});
Graphics.clearDepth();
for (World.objects.items) |*object| {
if (object.parent == .dock)
Graphics.drawMesh(object.mesh, &object.texture, object.drawingTransform());
}
}
pub fn updateCamera(delta: f32) void {
const zoom_factor = std.math.exp(@as(f32, @floatFromInt(zoom)) * @log(2.0) * -0.5);
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.window_width)) * -15;
World.camera_position[1] += zoom_factor * Game.mouse.dy / @as(f32, @floatFromInt(Graphics.window_height)) * 15;
}
}
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 getHover() ?*Object {
if (World.hover) |id| {
return World.getObject(id);
} }
return null; return null;
} }
pub fn isFree(position: @Vector(2, i32)) bool { fn getObject(id: Id) ?*Object {
return World.entityAt(position) == null; const index = World.object_map.get(id) orelse return null;
if (index >= World.objects.items.len) return null;
return &World.objects.items[index];
} }
pub fn getPlayer() ?*Entity { fn bringToTop(object: *Object) void {
var iter = World.entities.iter(); World.max_order += 1;
while (iter.next()) |entity| { object.order = World.max_order;
if (entity.player)
return entity;
} }
return null; fn bringToBottom(object: *Object) void {
World.min_order -= 1;
object.order = World.min_order;
} }
const CUBE_MESH_DATA = [_]f32{ fn updateOrder() void {
-0.5, 0.5, -0.5, 0.0, 0.0, std.sort.block(Object, World.objects.items, {}, objectOrderLessThan);
0.5, 0.5, -0.5, 0.0, 0.0, World.object_map.clearRetainingCapacity();
-0.5, -0.5, -0.5, 0.0, 0.0, for (0.., World.objects.items) |i, *object| {
0.5, -0.5, -0.5, 0.0, 0.0, object.index = @intCast(i);
-0.5, -0.5, -0.5, 0.0, 0.0, World.object_map.putAssumeCapacityNoClobber(object.id, i);
0.5, 0.5, -0.5, 0.0, 0.0, }
}
0.5, 0.5, -0.5, 0.0, 0.0, fn objectOrderLessThan(ctx: void, lhs: Object, rhs: Object) bool {
0.5, 0.5, 0.5, 0.0, 0.0, _ = ctx;
0.5, -0.5, -0.5, 0.0, 0.0, return lhs.order < rhs.order;
0.5, -0.5, 0.5, 0.0, 0.0, }
0.5, -0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 0.5, 0.0, 0.0, const T1 = 1.0 / 3.0;
const T2 = 2.0 / 3.0;
const CUBEMAP_MESH_DATA = [_]f32{
-0.5, 0.5, -0.5, T2, 0,
-0.5, -0.5, -0.5, T2, 0.5,
0.5, 0.5, -0.5, 1, 0,
0.5, -0.5, -0.5, 1, 0.5,
0.5, 0.5, -0.5, 1, 0,
-0.5, -0.5, -0.5, T2, 0.5,
0.5, 0.5, -0.5, 0, 1,
0.5, -0.5, -0.5, T1, 1,
0.5, 0.5, 0.5, 0, 0.5,
0.5, -0.5, 0.5, T1, 0.5,
0.5, 0.5, 0.5, 0, 0.5,
0.5, -0.5, -0.5, T1, 1,
0.5, 0.5, 0.5, 1.0, 0.0, 0.5, 0.5, 0.5, 1.0, 0.0,
-0.5, 0.5, 0.5, 0.0, 0.0,
0.5, -0.5, 0.5, 1.0, 1.0, 0.5, -0.5, 0.5, 1.0, 1.0,
-0.5, 0.5, 0.5, 0.0, 0.0,
-0.5, -0.5, 0.5, 0.0, 1.0, -0.5, -0.5, 0.5, 0.0, 1.0,
-0.5, 0.5, 0.5, 0.0, 0.0,
0.5, -0.5, 0.5, 1.0, 1.0, 0.5, -0.5, 0.5, 1.0, 1.0,
-0.5, 0.5, 0.5, 0.0, 0.0,
-0.5, 0.5, 0.5, 0.0, 0.0, -0.5, 0.5, 0.5, T1, 0,
-0.5, 0.5, -0.5, 0.0, 0.0, -0.5, -0.5, 0.5, 0, 0,
-0.5, -0.5, 0.5, 0.0, 0.0, -0.5, 0.5, -0.5, T1, 0.5,
-0.5, -0.5, -0.5, 0.0, 0.0, -0.5, -0.5, -0.5, 0, 0.5,
-0.5, -0.5, 0.5, 0.0, 0.0, -0.5, 0.5, -0.5, T1, 0.5,
-0.5, 0.5, -0.5, 0.0, 0.0, -0.5, -0.5, 0.5, 0, 0,
-0.5, 0.5, 0.5, 0.0, 0.0, -0.5, 0.5, 0.5, T1, 0.5,
0.5, 0.5, 0.5, 0.0, 0.0, -0.5, 0.5, -0.5, T1, 1,
-0.5, 0.5, -0.5, 0.0, 0.0, 0.5, 0.5, 0.5, T2, 0.5,
0.5, 0.5, -0.5, 0.0, 0.0, 0.5, 0.5, -0.5, T2, 1,
-0.5, 0.5, -0.5, 0.0, 0.0, 0.5, 0.5, 0.5, T2, 0.5,
0.5, 0.5, 0.5, 0.0, 0.0, -0.5, 0.5, -0.5, T1, 1,
-0.5, -0.5, -0.5, 0.0, 0.0, -0.5, -0.5, -0.5, T2, 0.5,
0.5, -0.5, -0.5, 0.0, 0.0, -0.5, -0.5, 0.5, T2, 0,
-0.5, -0.5, 0.5, 0.0, 0.0, 0.5, -0.5, -0.5, T1, 0.5,
0.5, -0.5, 0.5, 0.0, 0.0, 0.5, -0.5, 0.5, T1, 0,
-0.5, -0.5, 0.5, 0.0, 0.0, 0.5, -0.5, -0.5, T1, 0.5,
0.5, -0.5, -0.5, 0.0, 0.0, -0.5, -0.5, 0.5, T2, 0,
}; };
const PLANE_MESH_DATA = [_]f32{ const PLANE_MESH_DATA = [_]f32{
-0.5, -0.5, 0, 0.0, 1.0, -0.5, -0.5, 0, 0.0, 1.0,
@@ -144,9 +634,11 @@ const PLANE_MESH_DATA = [_]f32{
-0.5, -0.5, 0, 0.0, 1.0, -0.5, -0.5, 0, 0.0, 1.0,
0.5, -0.5, 0, 1.0, 1.0, 0.5, -0.5, 0, 1.0, 1.0,
}; };
const TEXTURE_DATA = [_]u8{ const PLANE_MESH_DATA_HALF = [_]f32{
255, 64, 64, 255, -0.25, -0.25, 0, 0.0, 1.0,
64, 255, 64, 255, 0.25, 0.25, 0, 1.0, 0.0,
64, 64, 255, 255, -0.25, 0.25, 0, 0.0, 0.0,
64, 64, 64, 255, 0.25, 0.25, 0, 1.0, 0.0,
-0.25, -0.25, 0, 0.0, 1.0,
0.25, -0.25, 0, 1.0, 1.0,
}; };