Files
voxel-game/src/assets/Textures.zig

310 lines
10 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Module for loading persistent textures.
const Textures = @This();
const std = @import("std");
const media = @import("media");
const Atom = @import("../engine/Atom.zig").Atom;
const Engine = @import("../engine/Engine.zig");
const Texture = @import("../engine/Texture.zig");
pub const Id = enum(u16) {
// VOLATILE Synchronize explicit values with `init` implementation.
/// 1×1 texture with usage `.base_color`, whose texel is read as `vec4(1)`.
empty_base_color = 0,
/// 1×1 texture with usage `.emissive`, whose texel is read as `vec4(1)`.
empty_emissive = 1,
/// 1×1 texture with usage `.normal`, whose texel is read as
/// `vec4(0, 0, 1, 1)`.
empty_normal = 2,
/// 1×1 texture with usage `,occlusion_roughness_metallic`, whose texel is
/// read as `vec4(1)`.
empty_occlusion_roughness_metallic = 3,
_,
/// Cast an integer into an ID. This can produce an invalid ID.
pub fn fromInt(value: u16) Id {
return @enumFromInt(value);
}
/// Cast an index into an ID. This can produce an invalid ID. The caller
/// asserts that the index is not greater than the max ID value.
pub fn fromIndex(index: usize) Id {
std.debug.assert(index < max_textures);
return @enumFromInt(@as(u16, @intCast(index)));
}
/// Cast an index into an ID. This can produce an invalid ID. Returns an
/// error if the index is greater than the max ID value.
pub fn fromIndexSafe(index: usize) error{Overflow}!Id {
if (index >= max_textures) return error.Overflow;
return @enumFromInt(@as(u16, @intCast(index)));
}
/// Cast an ID into an integer.
pub fn toInt(self: Id) u16 {
return @intFromEnum(self);
}
};
pub const Key = struct {
/// Atom representing the filename of the texture.
filename: Atom,
/// Desired usage of the texture.
usage: Texture.Usage,
};
/// Maps a key value to a texture ID.
map: std.AutoHashMapUnmanaged(Key, Id),
/// Stores all `Texture` structs and maps a texture ID to a `Texture` struct.
array: std.ArrayList(Texture),
/// 4096 textures of usage `.base_color` and 16×16 dimensions should take 4 MiB
/// in VRAM.
pub const max_textures = 4096;
pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures {
var map: std.AutoHashMapUnmanaged(Key, Id) = .empty;
errdefer map.deinit(allocator);
try map.ensureTotalCapacity(allocator, max_textures);
var array: std.ArrayList(Texture) = try .initCapacity(allocator, max_textures);
errdefer {
for (array.items) |*texture| {
texture.deinit(engine);
}
array.deinit(allocator);
}
// VOLATILE Synchronize with explicit values on top of `Id` type.
const empty_base_color_texture = try Texture.init(engine, .{
.width = 1,
.height = 1,
.usage = .base_color,
.target_queue = .graphics,
.name = "@Empty",
});
array.appendAssumeCapacity(empty_base_color_texture);
const empty_emissive_texture = try Texture.init(engine, .{
.width = 1,
.height = 1,
.usage = .emissive,
.target_queue = .graphics,
.name = "@Empty",
});
array.appendAssumeCapacity(empty_emissive_texture);
const empty_normal_texture = try Texture.init(engine, .{
.width = 1,
.height = 1,
.usage = .normal,
.target_queue = .graphics,
.name = "@Empty",
});
array.appendAssumeCapacity(empty_normal_texture);
const empty_occlusuion_roughness_metallic_texture = try Texture.init(engine, .{
.width = 1,
.height = 1,
.usage = .occlusion_roughness_metallic,
.target_queue = .graphics,
.name = "@Empty",
});
array.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture);
try empty_base_color_texture.writeSamples(u8, engine, &.{ 255, 255, 255, 255 });
try empty_emissive_texture.writeSamples(f16, engine, &.{ 1.0, 1.0, 1.0, 1.0 });
try empty_normal_texture.writeSamples(i8, engine, &.{ 0, 0, 127, 127 });
try empty_occlusuion_roughness_metallic_texture.writeSamples(u8, engine, &.{ 255, 255, 255, 255 });
return .{
.map = map,
.array = array,
};
}
pub fn deinit(self: *Textures, engine: *Engine, allocator: std.mem.Allocator) void {
std.log.scoped(.deinit).debug("Deinitializing {*} with {*} and Allocator{{{*},{*}}}", .{ self, engine, allocator.ptr, allocator.vtable });
for (self.array.items) |*texture| {
texture.deinit(engine);
}
self.array.deinit(allocator);
self.map.deinit(allocator);
self.* = undefined;
}
/// Get the ID of a texture given its filename (as a string) and usage. Returns
/// `null` if such texture hasn't been loaded. When the filename is `null`,
/// returns an empty texture ID appropriate for given usage.
pub fn get(self: *const Textures, maybe_filename: ?[]const u8, usage: Texture.Usage) ?Id {
if (maybe_filename) |filename| {
return self.map.get(.{
// If the atom doesn't exist, then the texture cannot possibly exist.
.filename = .fromStringIfExists(filename) orelse return null,
.usage = usage,
});
} else {
return emptyTextureForUsage(usage);
}
}
/// Get the ID of a texture given its filename (as an atom) and usage. Returns
/// `null` if such texture hasn't been loaded. When the filename is `.empty`,
/// returns an empty texture ID appropriate for given usage.
pub fn getAtom(self: *const Textures, filename: Atom, usage: Texture.Usage) ?Id {
if (filename != .empty) {
return self.map.get(.{
.filename = filename,
.usage = usage,
});
} else {
return emptyTextureForUsage(usage);
}
}
/// Get the ID of a texture given its filename (as a string) and usage. Returns
/// either an existing texture ID or loads a new texture and assigns a new ID,
/// if necessary. Will not return any error if the texture already exists. When
/// the filename is `null`, returns an empty texture ID appropriate for given
/// usage.
pub fn getOrLoad(
self: *Textures,
engine: *Engine,
stbi: *media.stbi,
maybe_filename: ?[]const u8,
usage: Texture.Usage,
io: std.Io,
) !Id {
if (maybe_filename) |filename| {
const key: Key = .{
// If the texture already exists, then the atom must exist and the
// following line will not return any error.
.filename = try .fromString(filename, io),
.usage = usage,
};
// We don't use `getOrPutAssumeCapacity` method, because we might already be
// at full capacity, in which case we should return `error.OutOfTextures`.
if (self.map.get(key)) |id| {
return id;
} else {
const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) {
error.Overflow => return error.OutOfTextures,
};
const texture = try loadTexture(engine, stbi, filename, usage, io);
self.map.putAssumeCapacityNoClobber(key, id);
self.array.appendAssumeCapacity(texture);
return id;
}
} else {
return emptyTextureForUsage(usage);
}
}
/// Get the ID of a texture given its filename (as an atom) and usage. Returns
/// either an existing texture ID or loads a new texture and assigns a new ID,
/// if necessary. Will not return any error if the texture already exists. When
/// the filename is `.empty`, returns an empty texture ID appropriate for given
/// usage.
pub fn getOrLoadAtom(
self: *Textures,
engine: *Engine,
stbi: *media.stbi,
filename: Atom,
usage: Texture.Usage,
io: std.Io,
) !Id {
if (filename != .empty) {
const key: Key = .{
.filename = filename,
.usage = usage,
};
// We don't use `getOrPutAssumeCapacity` method, because we might already be
// at full capacity, in which case we should return `error.OutOfTextures`.
if (self.map.get(key)) |id| {
return id;
} else {
const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) {
error.Overflow => return error.OutOfTextures,
};
const texture = try loadTexture(engine, stbi, filename.toString(), usage, io);
self.map.putAssumeCapacityNoClobber(key, id);
self.array.appendAssumeCapacity(texture);
return id;
}
} else {
return emptyTextureForUsage(usage);
}
}
fn loadTexture(
engine: *Engine,
stbi: *media.stbi,
filename: []const u8,
usage: Texture.Usage,
io: std.Io,
) !Texture {
std.log.debug("Loading texture \"{s}\" as {s}...", .{ filename, @tagName(usage) });
const cwd = std.Io.Dir.cwd();
var dir = try cwd.openDir(io, "assets/textures", .{});
defer dir.close(io);
var file = try dir.openFile(io, filename, .{});
defer file.close(io);
// The textures are expected to be small; a standard block base color as a
// PNG takes well below 1 kiB.
var buf: [4096]u8 = undefined;
var reader = file.reader(io, &buf);
const img = try stbi.loadDynamicIo(&reader.interface);
defer stbi.freeDynamic(img);
const data = img.data[0 .. img.width * img.height];
if (usage == .normal) {
for (data) |*pixel| {
pixel.r = pixel.r +% 128;
pixel.g = pixel.g +% 128;
pixel.b = pixel.b +% 128;
pixel.a = pixel.a +% 128;
}
}
var texture = try Texture.init(engine, .{
.width = img.width,
.height = img.height,
.usage = usage,
.target_queue = .graphics,
.name = filename,
});
errdefer texture.deinit(engine);
try texture.writeRaw(engine, @ptrCast(data));
return texture;
}
fn emptyTextureForUsage(usage: Texture.Usage) !Id {
return switch (usage) {
.base_color => .empty_base_color,
.normal => .empty_normal,
.occlusion_roughness_metallic => .empty_occlusion_roughness_metallic,
.emissive => .empty_emissive,
.depth => error.InvalidUsage,
};
}