move image loading to assets.zig

This commit is contained in:
Rokas Puzonas 2025-12-30 17:01:24 +02:00
parent 9f3c41f991
commit b73ea021f7
10 changed files with 193 additions and 157 deletions

View File

@ -29,9 +29,6 @@ pub fn build(b: *std.Build) !void {
exe_mod.linkLibrary(tracy_dependency.artifact("tracy")); exe_mod.linkLibrary(tracy_dependency.artifact("tracy"));
exe_mod.addImport("tracy", tracy_dependency.module("tracy")); exe_mod.addImport("tracy", tracy_dependency.module("tracy"));
const stb_dependency = b.dependency("stb", .{});
exe_mod.addIncludePath(stb_dependency.path("."));
const sokol_c_dependency = b.dependency("sokol_c", .{}); const sokol_c_dependency = b.dependency("sokol_c", .{});
exe_mod.addIncludePath(sokol_c_dependency.path("util")); exe_mod.addIncludePath(sokol_c_dependency.path("util"));
@ -41,6 +38,9 @@ pub fn build(b: *std.Build) !void {
const tiled_dependency = b.dependency("tiled", .{}); const tiled_dependency = b.dependency("tiled", .{});
exe_mod.addImport("tiled", tiled_dependency.module("tiled")); exe_mod.addImport("tiled", tiled_dependency.module("tiled"));
const stb_image_dependency = b.dependency("stb_image", .{});
exe_mod.addImport("stb_image", stb_image_dependency.module("stb_image"));
const sokol_dependency = b.dependency("sokol", .{ const sokol_dependency = b.dependency("sokol", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
@ -66,11 +66,6 @@ pub fn build(b: *std.Build) !void {
.flags = cflags.items .flags = cflags.items
}); });
exe_mod.addCSourceFile(.{
.file = b.path("src/libs/stb_image.c"),
.flags = &.{}
});
if (has_imgui) { if (has_imgui) {
if (b.lazyDependency("cimgui", .{ if (b.lazyDependency("cimgui", .{
.target = target, .target = target,

View File

@ -17,10 +17,6 @@
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669", .url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
.hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx", .hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
}, },
.stb = .{
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
},
.sokol_c = .{ .sokol_c = .{
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492", .url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN", .hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
@ -31,6 +27,9 @@
}, },
.tiled = .{ .tiled = .{
.path = "./libs/tiled" .path = "./libs/tiled"
},
.stb_image = .{
.path = "./libs/stb_image"
} }
}, },
.paths = .{ .paths = .{

37
libs/stb_image/build.zig Normal file
View File

@ -0,0 +1,37 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("stb_image", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig"),
});
const stb_dependency = b.dependency("stb", .{});
mod.addIncludePath(stb_dependency.path("."));
mod.addCSourceFile(.{
.file = b.path("src/stb_image_impl.c"),
.flags = &.{}
});
const lib = b.addLibrary(.{
.name = "stb_image",
.root_module = mod
});
b.installArtifact(lib);
{
const tests = b.addTest(.{
.root_module = mod
});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_tests.step);
}
}

View File

@ -0,0 +1,17 @@
.{
.name = .stb_image,
.version = "0.0.0",
.fingerprint = 0xe5d3607840482046, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{
.stb = .{
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@ -0,0 +1,30 @@
const std = @import("std");
const c = @cImport({
@cInclude("stb_image.h");
});
const STBImage = @This();
rgba8_pixels: [*c]u8,
width: u32,
height: u32,
pub fn load(png_data: []const u8) !STBImage {
var width: c_int = undefined;
var height: c_int = undefined;
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
if (pixels == null) {
return error.InvalidPng;
}
return STBImage{
.rgba8_pixels = pixels,
.width = @intCast(width),
.height = @intCast(height)
};
}
pub fn deinit(self: *const STBImage) void {
c.stbi_image_free(self.rgba8_pixels);
}

View File

@ -1,6 +1,10 @@
const std = @import("std"); const std = @import("std");
const Font = @import("./engine/font.zig"); const Engine = @import("./engine/root.zig");
const Gfx = @import("./engine/graphics.zig"); const Gfx = Engine.Graphics;
const Font = Gfx.Font;
const Math = @import("./engine/math.zig");
const Vec2 = Math.Vec2;
const Assets = @This(); const Assets = @This();
@ -12,17 +16,48 @@ pub const FontVariant = enum {
const Map = std.EnumArray(FontVariant, Font.Id); const Map = std.EnumArray(FontVariant, Font.Id);
}; };
font_map: FontVariant.Map = .initFill(.invalid), pub const ImageId = enum {
tilemap
};
pub const TileId = enum {
player,
open_door,
locked_door
};
font_map: FontVariant.Map,
tile_coords: std.EnumArray(TileId, Vec2),
tilemap: Gfx.Image,
tile_size: Vec2,
tilemap_size: Vec2,
tilemap_view: Gfx.View,
pub fn init() !Assets { pub fn init() !Assets {
const font_map: FontVariant.Map = .init(.{ const tilemap_data = try Gfx.STBImage.load(@embedFile("./assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
.regular = try Gfx.fonts.add("regular", @embedFile("./assets/roboto-font/Roboto-Regular.ttf")), defer tilemap_data.deinit();
.italic = try Gfx.fonts.add("italic", @embedFile("./assets/roboto-font/Roboto-Italic.ttf")),
.bold = try Gfx.fonts.add("bold", @embedFile("./assets/roboto-font/Roboto-Bold.ttf")), const tilemap = try Gfx.makeImageWithMipMaps(&.{
tilemap_data
}); });
const tilemap_view = try Gfx.makeView(tilemap);
return Assets{ return Assets{
.font_map = font_map .font_map = .init(.{
.regular = try Gfx.fonts.add("regular", @embedFile("./assets/roboto-font/Roboto-Regular.ttf")),
.italic = try Gfx.fonts.add("italic", @embedFile("./assets/roboto-font/Roboto-Italic.ttf")),
.bold = try Gfx.fonts.add("bold", @embedFile("./assets/roboto-font/Roboto-Bold.ttf")),
}),
.tile_coords = .init(.{
.player = .init(4, 0),
.locked_door = .init(7, 2),
.open_door = .init(5, 2)
}),
.tilemap = tilemap,
.tilemap_view = tilemap_view,
.tile_size = .init(8, 8),
.tilemap_size = .init(@floatFromInt(tilemap_data.width), @floatFromInt(tilemap_data.height))
}; };
} }

View File

@ -10,12 +10,15 @@ const imgui = @import("imgui.zig");
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
pub const STBImage = @import("stb_image");
pub const Image = sg.Image;
pub const View = sg.View;
const c = @cImport({ const c = @cImport({
@cInclude("sokol/sokol_gfx.h"); @cInclude("sokol/sokol_gfx.h");
@cInclude("sokol/sokol_gl.h"); @cInclude("sokol/sokol_gl.h");
@cInclude("fontstash.h"); @cInclude("fontstash.h");
@cInclude("sokol_fontstash.h"); @cInclude("sokol_fontstash.h");
@cInclude("stb_image.h");
}); });
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@ -64,16 +67,6 @@ const DrawFrame = struct {
} }
}; };
pub const ImageId = enum {
tilemap
};
pub const TileId = enum {
player,
open_door,
locked_door
};
pub const Borders = struct { pub const Borders = struct {
size: f32 = 0, size: f32 = 0,
left: Vec4 = rgba(0, 0, 0, 0), left: Vec4 = rgba(0, 0, 0, 0),
@ -114,16 +107,9 @@ pub var circle_quality: f32 = 6;
pub var draw_frame: DrawFrame = undefined; pub var draw_frame: DrawFrame = undefined;
var main_pipeline: sgl.Pipeline = .{}; var main_pipeline: sgl.Pipeline = .{};
var image_map: std.EnumArray(ImageId, sg.Image) = .initFill(.{});
var image_view_map: std.EnumArray(ImageId, sg.View) = .initFill(.{});
var linear_sampler: sg.Sampler = .{}; var linear_sampler: sg.Sampler = .{};
var nearest_sampler: sg.Sampler = .{}; var nearest_sampler: sg.Sampler = .{};
var tile_coords: std.EnumArray(TileId, Vec2) = .initUndefined();
const tile_size: Vec2 = .init(8, 8);
var tilemap_size: Vec2 = .init(0, 0);
pub var fonts: Font.Context = undefined; pub var fonts: Font.Context = undefined;
const Options = struct { const Options = struct {
@ -135,63 +121,29 @@ inline fn structCast(T: type, value: anytype) T {
return @as(*T, @ptrFromInt(@intFromPtr(&value))).*; return @as(*T, @ptrFromInt(@intFromPtr(&value))).*;
} }
const ImageData = struct { pub fn makeImageWithMipMaps(image_datas: []const STBImage) !sg.Image {
rgba8_pixels: [*c]u8, assert(image_datas.len > 0);
width: u32,
height: u32,
fn load(png_data: []const u8) !ImageData { var data: sg.ImageData = .{};
var width: c_int = undefined; var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels);
var height: c_int = undefined;
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
if (pixels == null) {
return error.InvalidPng;
}
return ImageData{
.rgba8_pixels = pixels,
.width = @intCast(width),
.height = @intCast(height)
};
}
fn deinit(self: *const ImageData) void {
c.stbi_image_free(self.rgba8_pixels);
}
};
fn makeImageWithMipMaps(image_datas: []const ImageData) !sg.Image {
var mip_levels_buffer = [_]sg.Range{.{}} ** 16;
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&mip_levels_buffer);
var image_width: c_int = -1;
var image_height: c_int = -1;
for (image_datas) |mipmap_image| { for (image_datas) |mipmap_image| {
if (image_height == -1) {
image_width = @intCast(mipmap_image.width);
image_height = @intCast(mipmap_image.height);
}
try mip_levels.appendBounded(.{ try mip_levels.appendBounded(.{
.ptr = mipmap_image.rgba8_pixels, .ptr = mipmap_image.rgba8_pixels,
.size = mipmap_image.width * mipmap_image.height * 4 .size = mipmap_image.width * mipmap_image.height * 4
}); });
} }
assert(image_width > 0);
assert(image_height > 0);
// TODO: Should error be checked? // TODO: Should error be checked?
const image = sg.makeImage(.{ const image = sg.makeImage(.{
.width = image_width, .width = @intCast(image_datas[0].width),
.height = image_height, .height = @intCast(image_datas[0].height),
.pixel_format = .RGBA8, .pixel_format = .RGBA8,
.usage = .{ .usage = .{
.immutable = true .immutable = true
}, },
.num_mipmaps = @intCast(mip_levels.items.len), .num_mipmaps = @intCast(mip_levels.items.len),
.data = sg.ImageData{ .mip_levels = mip_levels_buffer }, .data = data
}); });
if (image.id == sg.invalid_id) { if (image.id == sg.invalid_id) {
return error.InvalidImage; return error.InvalidImage;
@ -200,25 +152,14 @@ fn makeImageWithMipMaps(image_datas: []const ImageData) !sg.Image {
return image; return image;
} }
fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image { pub fn makeView(image: sg.Image) !sg.View {
var stbi_images_buffer: [16]ImageData = undefined; const image_view = sg.makeView(.{
var stbi_images: std.ArrayListUnmanaged(ImageData) = .initBuffer(&stbi_images_buffer); .texture = .{ .image = image }
defer { });
for (stbi_images.items) |image| { if (image_view.id == sg.invalid_id) {
image.deinit(); return error.InvalidView;
}
} }
return image_view;
if (image_datas.len > stbi_images.capacity) {
return error.OutOfMemory;
}
for (image_datas) |image_data| {
const mipmap_image = try ImageData.load(image_data);
stbi_images.appendAssumeCapacity(mipmap_image);
}
return try makeImageWithMipMaps(stbi_images.items);
} }
pub fn init(options: Options) !void { pub fn init(options: Options) !void {
@ -253,6 +194,7 @@ pub fn init(options: Options) !void {
imgui.setup(options.allocator, .{ imgui.setup(options.allocator, .{
.logger = structCast(simgui.Logger, options.logger), .logger = structCast(simgui.Logger, options.logger),
.no_default_font = true,
// TODO: Figure out a way to make imgui play nicely with UI // TODO: Figure out a way to make imgui play nicely with UI
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used. // Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
@ -260,37 +202,11 @@ pub fn init(options: Options) !void {
.disable_set_mouse_cursor = true .disable_set_mouse_cursor = true
}); });
// TODO: Move this font loading to assets
imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16); imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16);
fonts = try Font.Context.init(512); fonts = try Font.Context.init(512);
const tilemap = try ImageData.load(@embedFile("../assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
defer tilemap.deinit();
image_map = .init(.{
.tilemap = try makeImageWithMipMaps(&.{
tilemap
})
});
tilemap_size = Vec2.init(@floatFromInt(tilemap.width), @floatFromInt(tilemap.height));
tile_coords = .init(.{
.player = .init(4, 0),
.locked_door = .init(7, 2),
.open_door = .init(5, 2)
});
var image_iter = image_map.iterator();
while (image_iter.next()) |entry| {
const image_view = sg.makeView(.{
.texture = .{ .image = entry.value.* }
});
assert(image_view.id != sg.invalid_id);
image_view_map.set(entry.key, image_view);
}
linear_sampler = sg.makeSampler(.{ linear_sampler = sg.makeSampler(.{
.min_filter = .LINEAR, .min_filter = .LINEAR,
.mag_filter = .LINEAR, .mag_filter = .LINEAR,
@ -362,22 +278,21 @@ pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void {
); );
} }
pub fn drawTile(tile_coord: Vec2, pos: Vec2, size: Vec2, tint: Vec4) void { pub fn drawTexturedRectangle(
pos: Vec2,
size: Vec2,
color: Vec4,
view: sg.View,
view_quad: Rect,
) void {
sgl.enableTexture(); sgl.enableTexture();
defer sgl.disableTexture(); defer sgl.disableTexture();
sgl.texture( sgl.texture(
image_view_map.get(.tilemap), view,
nearest_sampler nearest_sampler
); );
var tile_quad = Rect.init(
tile_coord.x,
tile_coord.y,
1,
1
).multiply(tile_size).divide(tilemap_size);
const top_left = pos; const top_left = pos;
const top_right = pos.add(.{ .x = size.x, .y = 0 }); const top_right = pos.add(.{ .x = size.x, .y = 0 });
const bottom_right = pos.add(size); const bottom_right = pos.add(size);
@ -386,42 +301,33 @@ pub fn drawTile(tile_coord: Vec2, pos: Vec2, size: Vec2, tint: Vec4) void {
sgl.beginQuads(); sgl.beginQuads();
defer sgl.end(); defer sgl.end();
sgl.c4f(tint.x, tint.y, tint.z, tint.w); sgl.c4f(color.x, color.y, color.z, color.w);
sgl.v2fT2f( sgl.v2fT2f(
top_left.x, top_left.x,
top_left.y, top_left.y,
tile_quad.left(), view_quad.left(),
tile_quad.top(), view_quad.top(),
); );
sgl.v2fT2f( sgl.v2fT2f(
top_right.x, top_right.x,
top_right.y, top_right.y,
tile_quad.right(), view_quad.right(),
tile_quad.top(), view_quad.top(),
); );
sgl.v2fT2f( sgl.v2fT2f(
bottom_right.x, bottom_right.x,
bottom_right.y, bottom_right.y,
tile_quad.right(), view_quad.right(),
tile_quad.bottom(), view_quad.bottom(),
); );
sgl.v2fT2f( sgl.v2fT2f(
bottom_left.x, bottom_left.x,
bottom_left.y, bottom_left.y,
tile_quad.left(), view_quad.left(),
tile_quad.bottom(), view_quad.bottom(),
); );
} }
pub fn drawTileById(tile_id: TileId, pos: Vec2, size: Vec2, tint: Vec4) void {
const tile_coord = tile_coords.get(tile_id);
drawTile(tile_coord, pos, size, tint);
}
pub fn getTileCoords(tile_id: TileId) Vec2 {
return tile_coords.get(tile_id);
}
pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void { pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
// TODO: Don't use line segments // TODO: Don't use line segments
drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width); drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);

View File

@ -4,6 +4,8 @@ const Engine = @import("./engine/root.zig");
const Vec2 = Engine.Math.Vec2; const Vec2 = Engine.Math.Vec2;
const Gfx = Engine.Graphics; const Gfx = Engine.Graphics;
const Assets = @import("./assets.zig");
const Entity = @This(); const Entity = @This();
pub const List = GenerationalArrayList(Entity); pub const List = GenerationalArrayList(Entity);
@ -26,5 +28,5 @@ locked: bool = false,
render_tile: ?union(enum) { render_tile: ?union(enum) {
position: Vec2, position: Vec2,
id: Gfx.TileId id: Assets.TileId
} = null } = null

View File

@ -7,6 +7,7 @@ const imgui = Engine.imgui;
const Gfx = Engine.Graphics; const Gfx = Engine.Graphics;
const Vec2 = Engine.Math.Vec2; const Vec2 = Engine.Math.Vec2;
const Vec4 = Engine.Math.Vec4; const Vec4 = Engine.Math.Vec4;
const Rect = Engine.Math.Rect;
const rgb = Engine.Math.rgb; const rgb = Engine.Math.rgb;
const rgb_hex = Engine.Math.rgb_hex; const rgb_hex = Engine.Math.rgb_hex;
@ -341,22 +342,21 @@ pub fn getInput(self: *Game, frame: Engine.Frame) Input {
} }
fn drawEntity(self: *Game, entity: *Entity) void { fn drawEntity(self: *Game, entity: *Entity) void {
_ = self; // autofix
if (entity.render_tile) |render_tile| { if (entity.render_tile) |render_tile| {
var tile_coord = switch (render_tile) { var tile_coord = switch (render_tile) {
.id => |tile_id| Gfx.getTileCoords(tile_id), .id => |tile_id| self.assets.tile_coords.get(tile_id),
.position => |position| position .position => |position| position
}; };
if (entity.type == .door) { if (entity.type == .door) {
if (entity.locked) { if (entity.locked) {
tile_coord = Gfx.getTileCoords(.locked_door); tile_coord = self.assets.tile_coords.get(.locked_door);
} else { } else {
tile_coord = Gfx.getTileCoords(.open_door); tile_coord = self.assets.tile_coords.get(.open_door);
} }
} }
Gfx.drawTile(tile_coord, entity.position, .init(1,1), rgb(255, 255, 255)); self.drawTile(tile_coord, entity.position);
} }
} }
@ -573,6 +573,21 @@ pub fn tick(self: *Game, frame: Engine.Frame) !void {
} }
} }
fn drawTile(self: *Game, tile_coord: Vec2, pos: Vec2) void {
Gfx.drawTexturedRectangle(
pos,
.init(1, 1),
Gfx.white,
self.assets.tilemap_view,
Rect.init(
tile_coord.x,
tile_coord.y,
1,
1
).multiply(self.assets.tile_size).divide(self.assets.tilemap_size)
);
}
pub fn debug(self: *Game) !void { pub fn debug(self: *Game) !void {
if (!imgui.beginWindow(.{ if (!imgui.beginWindow(.{
.name = "Debug", .name = "Debug",