start rewriting mesh code in zig

This commit is contained in:
catangent 2024-12-16 23:19:26 +00:00
parent 5a76cdcd13
commit 6f1eb16a30
14 changed files with 47357 additions and 40 deletions

3
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "raylib"] [submodule "raylib"]
path = raylib path = raylib
url = git@github.com:raysan5/raylib.git url = git@github.com:raysan5/raylib.git
[submodule "zig-gamedev"]
path = zig-gamedev
url = https://github.com/zig-gamedev/zig-gamedev.git

View file

@ -4,6 +4,18 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const rl = raylib: {
const rl = b.dependency("raylib", .{
.target = target,
.optimize = optimize,
});
break :raylib rl;
};
const znoise = b.dependency("znoise", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "voxel_test", .name = "voxel_test",
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
@ -11,19 +23,14 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
const rl = b.dependency("raylib", .{
.target = target,
.optimize = optimize,
});
exe.linkLibrary(rl.artifact("raylib")); exe.linkLibrary(rl.artifact("raylib"));
exe.root_module.addImport("znoise", znoise.module("root"));
exe.linkLibrary(znoise.artifact("FastNoiseLite"));
b.installArtifact(exe); b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe); const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep()); run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| { if (b.args) |args| {
run_cmd.addArgs(args); run_cmd.addArgs(args);
} }
@ -38,6 +45,8 @@ pub fn build(b: *std.Build) void {
}); });
exe_unit_tests.linkLibrary(rl.artifact("raylib")); exe_unit_tests.linkLibrary(rl.artifact("raylib"));
exe_unit_tests.root_module.addImport("znoise", znoise.module("root"));
exe_unit_tests.linkLibrary(znoise.artifact("FastNoiseLite"));
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

View file

@ -4,9 +4,8 @@
.version = "0.0.0", .version = "0.0.0",
.dependencies = .{ .dependencies = .{
.raylib = .{ .znoise = .{ .path = "zig-gamedev/libs/znoise" },
.path = "raylib", .raylib = .{ .path = "raylib" },
},
}, },
.paths = .{ .paths = .{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

40465
rmodels.tmpzig Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,10 @@
const std = @import("std"); const std = @import("std");
const rh = @import("util/raylib_helper.zig");
const raylib = rh.raylib; const raylib_helper = @import("lib_helpers/raylib_helper.zig");
const v3 = rh.v3; const raylib = raylib_helper.raylib;
const v3 = raylib_helper.v3;
const znoise = @import("znoise");
const chunks = @import("world/chunk.zig"); const chunks = @import("world/chunk.zig");
const TILE_TEXTURE_RESOLUTION = 16; const TILE_TEXTURE_RESOLUTION = 16;
@ -14,7 +17,7 @@ pub fn drawCameraPosition(camera: raylib.Camera3D, x: i32, y: i32) !void {
const slice = try std.fmt.bufPrintZ( const slice = try std.fmt.bufPrintZ(
&buf, &buf,
"position: {d} {d} {d}", "position: {d:.2} {d:.2} {d:.2}",
.{ camera.position.x, camera.position.y, camera.position.z }, .{ camera.position.x, camera.position.y, camera.position.z },
); );
@ -43,7 +46,8 @@ pub fn main() !void {
defer raylib.CloseWindow(); defer raylib.CloseWindow();
raylib.DisableCursor(); raylib.DisableCursor();
raylib.SetTargetFPS(60); raylib.SetWindowState(raylib.FLAG_VSYNC_HINT);
raylib.SetWindowState(raylib.FLAG_FULLSCREEN_MODE);
var camera = raylib.Camera3D{ var camera = raylib.Camera3D{
.position = raylib.Vector3{ .x = 0, .y = 0, .z = 0 }, .position = raylib.Vector3{ .x = 0, .y = 0, .z = 0 },
@ -67,14 +71,18 @@ pub fn main() !void {
var chunk = try chunks.Chunk.init(a7r); var chunk = try chunks.Chunk.init(a7r);
defer chunk.deinit(); defer chunk.deinit();
const height_generator = znoise.FnlGenerator{ .seed = 413445 };
const tile_type_generator = znoise.FnlGenerator{ .seed = 4435, .frequency = 0.1 };
for (0..32) |raw_x| for (0..32) |raw_y| for (0..32) |raw_z| { for (0..32) |raw_x| for (0..32) |raw_y| for (0..32) |raw_z| {
const x: u5 = @intCast(raw_x); const x: u5 = @intCast(raw_x);
const y: u5 = @intCast(raw_y); const y: u5 = @intCast(raw_y);
const z: u5 = @intCast(raw_z); const z: u5 = @intCast(raw_z);
const xt: i32 = @as(i32, @intCast(x)) - 16; const xf: f32 = @floatFromInt(raw_x);
const yt: i32 = @as(i32, @intCast(y)) - 16; const yf: f32 = @floatFromInt(raw_y);
const zt: i32 = @as(i32, @intCast(z)) - 16; const zf: f32 = @floatFromInt(raw_z);
if (xt * xt + yt * yt + zt * zt < 15 * 15) chunk.setTile(x, y, z, if (2 * xt > yt - 30) 1 else 2); const height: f32 = (height_generator.noise2(xf, zf) + 1) * 16;
const tile_type: u32 = if (tile_type_generator.noise3(xf, yf, zf) > 0) 1 else 2;
if (height >= yf) chunk.setTile(x, y, z, tile_type);
}; };
if (benchmark_chunk_meshing) { if (benchmark_chunk_meshing) {

View file

@ -0,0 +1,177 @@
const std = @import("std");
const raylib_helper = @import("../lib_helpers/raylib_helper.zig");
const raylib = raylib_helper.raylib;
const MAX_VBOS = 4;
const VERTICES_VBO_ID = 0;
const TEXCOORDS_VBO_ID = 1;
const TEXCOORDS2_VBO_ID = 2;
const NORMALS_VBO_ID = 3;
const ChunkMesh = packed struct {
vertexCount: i32,
triangleCount: i32,
vertices: [*]f32, // vertex position, XYZ, 3 vars per vertex
texcoords: [*]f32, // vertex texture coorinates, UV, 2 vars per vertex
texcoords2: [*]f32, // vertex second texture coorinates, UV, 2 vars per vertex
normals: [*]f32, // vertex normals, XYZ, 3 vars per vertex
vaoId: u32,
vboId: [*]u32,
};
pub fn UploadChunkMesh(mesh: ChunkMesh, dynamic: bool) void {
if (mesh.vaoId > 0) {
raylib.TraceLog(raylib.LOG_WARNING, "VAO: [ID %i] Trying to re-load an already loaded chunk mesh", mesh.vaoId);
return;
}
mesh.vboId = @ptrCast(@alignCast(raylib.MemAlloc(@sizeOf(u32) * MAX_VBOS)));
mesh.vaoId = 0;
mesh.vboId[VERTICES_VBO_ID] = 0;
mesh.vboId[TEXCOORDS_VBO_ID] = 0;
mesh.vboId[TEXCOORDS2_VBO_ID] = 0;
mesh.vboId[NORMALS_VBO_ID] = 0;
mesh.vaoId = raylib.rlLoadVertexArray();
raylib.rlEnableVertexArray(mesh.vaoId);
mesh.vboId[VERTICES_VBO_ID] = raylib.rlLoadVertexBuffer(mesh.vertices, mesh.vertexCount * 3 * @sizeOf(f32), dynamic);
raylib.rlSetVertexAttribute(VERTICES_VBO_ID, 3, raylib.RL_FLOAT, false, 0, 0);
raylib.rlEnableVertexAttribute(VERTICES_VBO_ID);
mesh.vboId[TEXCOORDS_VBO_ID] = raylib.rlLoadVertexBuffer(mesh.texcoords, mesh.vertexCount * 2 * @sizeOf(f32), dynamic);
raylib.rlSetVertexAttribute(TEXCOORDS_VBO_ID, 2, raylib.RL_FLOAT, false, 0, 0);
raylib.rlEnableVertexAttribute(TEXCOORDS_VBO_ID);
mesh.vboId[TEXCOORDS2_VBO_ID] = raylib.rlLoadVertexBuffer(mesh.texcoords, mesh.vertexCount * 2 * @sizeOf(f32), dynamic);
raylib.rlSetVertexAttribute(TEXCOORDS2_VBO_ID, 2, raylib.RL_FLOAT, false, 0, 0);
raylib.rlEnableVertexAttribute(TEXCOORDS2_VBO_ID);
mesh.vboId[NORMALS_VBO_ID] = raylib.rlLoadVertexBuffer(mesh.vertices, mesh.vertexCount * 3 * @sizeOf(f32), dynamic);
raylib.rlSetVertexAttribute(NORMALS_VBO_ID, 3, raylib.RL_FLOAT, false, 0, 0);
raylib.rlEnableVertexAttribute(NORMALS_VBO_ID);
if (mesh.vaoId > 0) {
raylib.TraceLog(raylib.LOG_INFO, "VAO: [ID %i] Chunk mesh uploaded successfully to VRAM (GPU)", mesh.vaoId);
} else {
raylib.TraceLog(raylib.LOG_INFO, "VBO: Chunk mesh uploaded successfully to VRAM (GPU)");
}
raylib.rlDisableVertexArray();
}
pub fn UpdateMeshBuffer(mesh: ChunkMesh, index: i32, data: ?*const anyopaque, dataSize: i32, offset: i32) void {
raylib.rlUpdateVertexBuffer(mesh.vboId[index], data, dataSize, offset);
}
pub export fn UnloadMesh(mesh: ChunkMesh) void {
raylib.rlUnloadVertexArray(mesh.vaoId);
if (mesh.vboId != null) {
for (0..MAX_VBOS) |i| {
raylib.rlUnloadVertexBuffer(mesh.vboId[i]);
}
}
raylib.MemFree(mesh.vboId);
raylib.MemFree(mesh.vertices);
raylib.MemFree(mesh.texcoords);
raylib.MemFree(mesh.texcoords2);
raylib.MemFree(mesh.normals);
}
pub fn DrawMesh(mesh: ChunkMesh, material: raylib.Material, transform: raylib.Matrix) void {
raylib.rlEnableShader(material.shader.id);
if (material.shader.locs[raylib.SHADER_LOC_COLOR_DIFFUSE] != -1) {
const values: [4]f32 = [4]f32{
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_DIFFUSE].color.r)) / 255.0,
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_DIFFUSE].color.g)) / 255.0,
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_DIFFUSE].color.b)) / 255.0,
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_DIFFUSE].color.a)) / 255.0,
};
raylib.rlSetUniform(material.shader.locs[raylib.SHADER_LOC_COLOR_DIFFUSE], values, raylib.SHADER_UNIFORM_VEC4, 1);
}
if (material.shader.locs[raylib.SHADER_LOC_COLOR_SPECULAR] != -1) {
const values: [4]f32 = [4]f32{
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_SPECULAR].color.r)) / 255.0,
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_SPECULAR].color.g)) / 255.0,
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_SPECULAR].color.b)) / 255.0,
@as(u32, @floatFromInt(material.maps[raylib.MATERIAL_MAP_SPECULAR].color.a)) / 255.0,
};
raylib.rlSetUniform(material.shader.locs[raylib.SHADER_LOC_COLOR_SPECULAR], values, raylib.SHADER_UNIFORM_VEC4, 1);
}
var matModel: raylib.Matrix = raylib.MatrixIdentity();
const matView: raylib.Matrix = raylib.rlGetMatrixModelview();
var matModelView: raylib.Matrix = raylib.MatrixIdentity();
const matProjection: raylib.Matrix = raylib.rlGetMatrixProjection();
if (material.shader.locs[raylib.SHADER_LOC_MATRIX_VIEW] != -1) raylib.rlSetUniformMatrix(material.shader.locs[raylib.SHADER_LOC_MATRIX_VIEW], matView);
if (material.shader.locs[raylib.SHADER_LOC_MATRIX_PROJECTION] != -1) raylib.rlSetUniformMatrix(material.shader.locs[raylib.SHADER_LOC_MATRIX_PROJECTION], matProjection);
matModel = raylib.MatrixMultiply(transform, raylib.rlGetMatrixTransform());
if (material.shader.locs[raylib.SHADER_LOC_MATRIX_MODEL] != -1) raylib.rlSetUniformMatrix(material.shader.locs[raylib.SHADER_LOC_MATRIX_MODEL], matModel);
matModelView = raylib.MatrixMultiply(matModel, matView);
if (material.shader.locs[raylib.SHADER_LOC_MATRIX_NORMAL] != -1) raylib.rlSetUniformMatrix(material.shader.locs[raylib.SHADER_LOC_MATRIX_NORMAL], raylib.MatrixTranspose(raylib.MatrixInvert(matModel)));
for (0..raylib.MAX_MATERIAL_MAPS) |i| {
if (material.maps[i].texture.id > 0) {
raylib.rlActiveTextureSlot(i);
if (i == raylib.MATERIAL_MAP_IRRADIANCE or i == raylib.MATERIAL_MAP_PREFILTER or i == raylib.MATERIAL_MAP_CUBEMAP) {
raylib.rlEnableTextureCubemap(material.maps[i].texture.id);
} else {
raylib.rlEnableTexture(material.maps[i].texture.id);
}
raylib.rlSetUniform(material.shader.locs[raylib.SHADER_LOC_MAP_DIFFUSE + i], &i, raylib.SHADER_UNIFORM_INT, 1);
}
}
if (!raylib.rlEnableVertexArray(mesh.vaoId)) {
raylib.rlEnableVertexBuffer(mesh.vboId[0]);
raylib.rlSetVertexAttribute(material.shader.locs[VERTICES_VBO_ID], 3, raylib.RL_FLOAT, 0, 0, 0);
raylib.rlEnableVertexAttribute(material.shader.locs[VERTICES_VBO_ID]);
raylib.rlEnableVertexBuffer(mesh.vboId[1]);
raylib.rlSetVertexAttribute(material.shader.locs[TEXCOORDS_VBO_ID], 2, raylib.RL_FLOAT, 0, 0, 0);
raylib.rlEnableVertexAttribute(material.shader.locs[TEXCOORDS_VBO_ID]);
raylib.rlEnableVertexBuffer(mesh.vboId[2]);
raylib.rlSetVertexAttribute(material.shader.locs[TEXCOORDS2_VBO_ID], 2, raylib.RL_FLOAT, 0, 0, 0);
raylib.rlEnableVertexAttribute(material.shader.locs[TEXCOORDS2_VBO_ID]);
raylib.rlEnableVertexBuffer(mesh.vboId[3]);
raylib.rlSetVertexAttribute(material.shader.locs[NORMALS_VBO_ID], 3, raylib.RL_FLOAT, 0, 0, 0);
raylib.rlEnableVertexAttribute(material.shader.locs[NORMALS_VBO_ID]);
}
var eyeCount = 1;
if (raylib.rlIsStereoRenderEnabled()) {
eyeCount = 2;
}
for (0..eyeCount) |eye| {
var matModelViewProjection: raylib.Matrix = raylib.MatrixIdentity();
if (eyeCount == 1) {
matModelViewProjection = raylib.MatrixMultiply(matModelView, matProjection);
} else {
raylib.rlViewport(@divTrunc(eye * raylib.rlGetFramebufferWidth(), 2), 0, @divTrunc(raylib.rlGetFramebufferWidth(), 2), raylib.rlGetFramebufferHeight());
matModelViewProjection = raylib.MatrixMultiply(raylib.MatrixMultiply(matModelView, raylib.rlGetMatrixViewOffsetStereo(eye)), raylib.rlGetMatrixProjectionStereo(eye));
}
raylib.rlSetUniformMatrix(material.shader.locs[raylib.SHADER_LOC_MATRIX_MVP], matModelViewProjection);
raylib.rlDrawVertexArray(0, mesh.vertexCount);
}
for (0..raylib.MAX_MATERIAL_MAPS) |i| {
if (material.maps[i].texture.id > 0) {
raylib.rlActiveTextureSlot(i);
if (((i == raylib.MATERIAL_MAP_IRRADIANCE) or (i == raylib.MATERIAL_MAP_PREFILTER)) or (i == raylib.MATERIAL_MAP_CUBEMAP)) {
raylib.rlDisableTextureCubemap();
} else {
raylib.rlDisableTexture();
}
}
}
raylib.rlDisableVertexArray();
raylib.rlDisableVertexBuffer();
raylib.rlDisableVertexBufferElement();
raylib.rlDisableShader();
raylib.rlSetMatrixModelview(matView);
raylib.rlSetMatrixProjection(matProjection);
}

6648
src/rendering/chunk_models.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
const std = @import("std"); const std = @import("std");
const rh = @import("../util/raylib_helper.zig"); const raylib_helper = @import("../lib_helpers/raylib_helper.zig");
const raylib = rh.raylib; const raylib = raylib_helper.raylib;
const v3 = rh.v3; const v3 = raylib_helper.v3;
const A7r = std.mem.Allocator; const A7r = std.mem.Allocator;
const RawQuad = struct { const RawQuad = struct {
tile: i32, tile: u32,
top_left: raylib.Vector3, top_left: raylib.Vector3,
top_right: raylib.Vector3, top_right: raylib.Vector3,
bottom_right: raylib.Vector3, bottom_right: raylib.Vector3,
@ -16,13 +16,13 @@ const RawQuad = struct {
}; };
pub const Chunk = struct { pub const Chunk = struct {
tiles: []i32, tiles: []u32,
a7r: A7r, a7r: A7r,
pub fn init(a7r: A7r) !Chunk { pub fn init(a7r: A7r) !Chunk {
const self = Chunk{ const self = Chunk{
.a7r = a7r, .a7r = a7r,
.tiles = try a7r.alloc(i32, 32 * 32 * 32), .tiles = try a7r.alloc(u32, 32 * 32 * 32),
}; };
@memset(self.tiles, 0); @memset(self.tiles, 0);
return self; return self;
@ -32,19 +32,19 @@ pub const Chunk = struct {
self.a7r.free(self.tiles); self.a7r.free(self.tiles);
} }
pub fn getTile(self: Chunk, x: u5, y: u5, z: u5) i32 { pub fn getTile(self: Chunk, x: u5, y: u5, z: u5) u32 {
return self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)]; return self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)];
} }
pub fn setTile(self: Chunk, x: u5, y: u5, z: u5, tile: i32) void { pub fn setTile(self: Chunk, x: u5, y: u5, z: u5, tile: u32) void {
self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)] = tile; self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)] = tile;
} }
fn getTileRaw(self: Chunk, x: u5, y: u5, z: u5) i32 { fn getTileRaw(self: Chunk, x: u5, y: u5, z: u5) u32 {
return self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)]; return self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)];
} }
inline fn getTileRawShifted(self: Chunk, x: u5, y: u5, z: u5, comptime d: comptime_int) i32 { inline fn getTileRawShifted(self: Chunk, x: u5, y: u5, z: u5, comptime d: comptime_int) u32 {
if (d % 3 == 0) { if (d % 3 == 0) {
return self.getTileRaw(x, y, z); return self.getTileRaw(x, y, z);
} else if (d % 3 == 1) { } else if (d % 3 == 1) {
@ -61,12 +61,12 @@ pub const Chunk = struct {
inline for (0..3) |d| { inline for (0..3) |d| {
for (0..32) |raw_x| { for (0..32) |raw_x| {
const x: u5 = @intCast(raw_x); const x: u5 = @intCast(raw_x);
var positive_tile_surfaces: [32][32]i32 = .{.{0} ** 32} ** 32; var positive_tile_surfaces: [32][32]u32 = .{.{0} ** 32} ** 32;
var negative_tile_surfaces: [32][32]i32 = .{.{0} ** 32} ** 32; var negative_tile_surfaces: [32][32]u32 = .{.{0} ** 32} ** 32;
for (0..32) |raw_y| for (0..32) |raw_z| { for (0..32) |raw_y| for (0..32) |raw_z| {
const y: u5 = @intCast(raw_y); const y: u5 = @intCast(raw_y);
const z: u5 = @intCast(raw_z); const z: u5 = @intCast(raw_z);
const tile: i32 = chunk.getTileRawShifted(x, y, z, d); const tile: u32 = chunk.getTileRawShifted(x, y, z, d);
if (tile == 0) continue; if (tile == 0) continue;
if (x == 31 or chunk.getTileRawShifted(x + 1, y, z, d) == 0) positive_tile_surfaces[y][z] = tile; if (x == 31 or chunk.getTileRawShifted(x + 1, y, z, d) == 0) positive_tile_surfaces[y][z] = tile;
if (x == 0 or chunk.getTileRawShifted(x - 1, y, z, d) == 0) negative_tile_surfaces[y][z] = tile; if (x == 0 or chunk.getTileRawShifted(x - 1, y, z, d) == 0) negative_tile_surfaces[y][z] = tile;
@ -154,7 +154,7 @@ pub const Chunk = struct {
for (raw_quads.items, 0..) |raw_quad, i| { for (raw_quads.items, 0..) |raw_quad, i| {
if (raw_quad.tile <= 0) continue; if (raw_quad.tile <= 0) continue;
const tile = @as(u32, @intCast(raw_quad.tile)); const tile = raw_quad.tile;
for (0..6) |j| { for (0..6) |j| {
normals[18 * i + 3 * j + 0] = raw_quad.normal.x; normals[18 * i + 3 * j + 0] = raw_quad.normal.x;
@ -224,13 +224,7 @@ pub const Chunk = struct {
.texcoords = texcoords, .texcoords = texcoords,
.texcoords2 = texcoords2, .texcoords2 = texcoords2,
.normals = normals, .normals = normals,
.tangents = null,
.colors = null,
.indices = null,
.animVertices = null,
.animNormals = null,
.boneIds = null,
.boneWeights = null,
.vaoId = 0, .vaoId = 0,
.vboId = null, .vboId = null,
}; };

13
todo.md Normal file
View file

@ -0,0 +1,13 @@
# current tasks
yoink implementation of mesh into meshes.zig so i could modify it to add ambient occlusion vars
implement ambient occlusion either
- via passing extra info to shader (can use 132 bits to indicate what tiles around the quad are obsuring light. need to pass as an extra variable)
implement animated textures by either
- making literally everything animated
- somehow passing extra info to shader
limit vertical camera rotations
update libraries like raylib
# future tasks
implement chunk meshing cache to reduce delay on block placement
investigate binary/SIMD meshing for performance

1
zig-gamedev Submodule

@ -0,0 +1 @@
Subproject commit d96ecc993bcfc4461f44d9432589dc9273952a78