Custom mipmap gen, output linear textures from assetc
This commit is contained in:
parent
03c1d181e1
commit
d3370cc559
@ -350,7 +350,7 @@ fn loadTextureErr(self: *AssetManager, id: AssetId) !*const LoadedTexture {
|
||||
const path = asset_manifest.getPath(id);
|
||||
const data = try self.loadFile(self.frame_arena, path, TEXTURE_MAX_BYTES);
|
||||
|
||||
const texture = try formats.Texture.fromBuffer(data.bytes);
|
||||
const texture = try formats.Texture.fromBuffer(self.frame_arena, data.bytes);
|
||||
|
||||
var name: gl.GLuint = 0;
|
||||
gl.createTextures(gl.TEXTURE_2D, 1, &name);
|
||||
@ -361,24 +361,25 @@ fn loadTextureErr(self: *AssetManager, id: AssetId) !*const LoadedTexture {
|
||||
|
||||
gl.textureStorage2D(
|
||||
name,
|
||||
@intCast(texture.header.mip_levels),
|
||||
gl.COMPRESSED_SRGB_ALPHA_BPTC_UNORM,
|
||||
@intCast(texture.mipLevels()),
|
||||
gl.COMPRESSED_RGBA_BPTC_UNORM,
|
||||
@intCast(texture.header.width),
|
||||
@intCast(texture.header.height),
|
||||
);
|
||||
checkGLError();
|
||||
|
||||
for (0..texture.header.mip_levels) |mip_level| {
|
||||
for (0..texture.mipLevels()) |mip_level| {
|
||||
const desc = texture.getMipDesc(mip_level);
|
||||
gl.compressedTextureSubImage2D(
|
||||
name,
|
||||
@intCast(mip_level),
|
||||
0,
|
||||
0,
|
||||
@intCast(texture.header.width),
|
||||
@intCast(texture.header.height),
|
||||
gl.COMPRESSED_SRGB_ALPHA_BPTC_UNORM,
|
||||
@intCast(texture.data.len),
|
||||
@ptrCast(texture.data.ptr),
|
||||
@intCast(desc.width),
|
||||
@intCast(desc.height),
|
||||
gl.COMPRESSED_RGBA_BPTC_UNORM,
|
||||
@intCast(texture.data[mip_level].len),
|
||||
@ptrCast(texture.data[mip_level].ptr),
|
||||
);
|
||||
checkGLError();
|
||||
}
|
||||
|
@ -163,8 +163,7 @@ pub const Texture = struct {
|
||||
pub const Format = enum(u32) {
|
||||
bc5, // uncorrelated 2 channel, used for normal maps
|
||||
bc6, // f16 for hdr textures
|
||||
bc7, // normal rgba textures, assumed linear colors
|
||||
bc7_srgb, // normal rgba textures, assumed srgb
|
||||
bc7, // normal rgba textures, linear colors
|
||||
};
|
||||
|
||||
pub const MAGIC = [_]u8{ 'T', 'X', 'F', 'M' };
|
||||
@ -172,33 +171,67 @@ pub const Texture = struct {
|
||||
pub const Header = extern struct {
|
||||
magic: [4]u8 = MAGIC,
|
||||
format: Format,
|
||||
mip_levels: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
size: u32,
|
||||
mip_count: u32,
|
||||
};
|
||||
|
||||
header: Header,
|
||||
data: []u8,
|
||||
data: []const []const u8,
|
||||
|
||||
pub fn fromBuffer(buf: []u8) !Texture {
|
||||
pub inline fn mipLevels(self: *const Texture) usize {
|
||||
return self.data.len;
|
||||
}
|
||||
|
||||
pub fn getMipDesc(self: *const Texture, mip_level: usize) MipDesc {
|
||||
const divisor = std.math.powi(u32, 2, @intCast(mip_level)) catch unreachable;
|
||||
return MipDesc{
|
||||
.width = self.header.width / divisor,
|
||||
.height = self.header.height / divisor,
|
||||
};
|
||||
}
|
||||
|
||||
pub const MipDesc = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
};
|
||||
|
||||
// TODO: avoid allocation here
|
||||
pub fn fromBuffer(allocator: std.mem.Allocator, buf: []u8) !Texture {
|
||||
const header: *align(1) Header = @ptrCast(buf.ptr);
|
||||
|
||||
if (!std.mem.eql(u8, &header.magic, &MAGIC)) {
|
||||
return error.MagicMatch;
|
||||
}
|
||||
|
||||
const data = try allocator.alloc([]u8, @intCast(header.mip_count));
|
||||
|
||||
var mip_level: usize = 0;
|
||||
var mip_data = buf[@sizeOf(Header)..];
|
||||
|
||||
while (mip_data.len > 4) {
|
||||
const mip_len = std.mem.readInt(u32, mip_data[0..4], native_endian);
|
||||
const mip_slice = mip_data[4..@intCast(4 + mip_len)];
|
||||
data[mip_level] = mip_slice;
|
||||
mip_data = mip_data[4 + mip_slice.len ..];
|
||||
mip_level += 1;
|
||||
}
|
||||
|
||||
return Texture{
|
||||
.header = header.*,
|
||||
.data = buf[@sizeOf(Header) .. @sizeOf(Header) + header.size],
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: this doesn't respect endiannes at all
|
||||
pub fn writeTexture(writer: anytype, value: Texture) !void {
|
||||
pub fn writeTexture(writer: anytype, value: Texture, endian: std.builtin.Endian) !void {
|
||||
try writer.writeStruct(value.header);
|
||||
try writer.writeAll(value.data);
|
||||
|
||||
for (value.data) |mip_img| {
|
||||
try writer.writeInt(u32, @intCast(mip_img.len), endian);
|
||||
try writer.writeAll(mip_img);
|
||||
}
|
||||
}
|
||||
|
||||
test "texture write/parse" {
|
||||
|
@ -192,9 +192,16 @@ fn processShaderProgram(allocator: std.mem.Allocator, absolute_input: []const u8
|
||||
try formats.writeShaderProgram(buf_writer.writer(), shader_asset_id, program.value.vertex, program.value.fragment, formats.native_endian);
|
||||
try buf_writer.flush();
|
||||
}
|
||||
const MipLevel = struct {
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: []u8,
|
||||
out_data: []const u8 = &.{},
|
||||
};
|
||||
|
||||
fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: []const u8, hdr: bool) !void {
|
||||
_ = hdr; // autofix
|
||||
const input_srgb = true;
|
||||
var width_int: c_int = undefined;
|
||||
var height_int: c_int = undefined;
|
||||
var comps: c_int = undefined;
|
||||
@ -210,51 +217,195 @@ fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: []
|
||||
const width: usize = @intCast(width_int);
|
||||
const height: usize = @intCast(height_int);
|
||||
|
||||
const data = data_c[0 .. width * height * FORCED_COMPONENTS];
|
||||
|
||||
// TODO: support textures not divisible by 4
|
||||
if (width % 4 != 0 or height % 4 != 0) {
|
||||
std.log.debug("Image size: {}X{}\n", .{ width, height });
|
||||
return error.ImageSizeShouldBeDivisibleBy4;
|
||||
}
|
||||
|
||||
const blocks_x: usize = width / 4;
|
||||
const blocks_y: usize = height / 4;
|
||||
|
||||
const rgba_surf = c.rgba_surface{
|
||||
.ptr = data_c,
|
||||
.width = @intCast(width),
|
||||
.height = @intCast(height),
|
||||
.stride = width_int * FORCED_COMPONENTS,
|
||||
};
|
||||
var settings: c.bc7_enc_settings = undefined;
|
||||
|
||||
if (input_srgb) {
|
||||
convertSrgb(data);
|
||||
}
|
||||
|
||||
if (comps == 3) {
|
||||
c.GetProfile_ultrafast(&settings);
|
||||
} else if (comps == 4) {
|
||||
premultiplyAlpha(data);
|
||||
c.GetProfile_alpha_ultrafast(&settings);
|
||||
} else {
|
||||
std.log.debug("Channel count: {}\n", .{comps});
|
||||
return error.UnsupportedChannelCount;
|
||||
}
|
||||
|
||||
const mip_levels_to_gen = 1 + @as(
|
||||
u32,
|
||||
@intFromFloat(@log2(@as(f32, @floatFromInt(@max(width, height))))),
|
||||
);
|
||||
var actual_mip_count: usize = 1;
|
||||
|
||||
var mip_pyramid = std.ArrayList(MipLevel).init(allocator);
|
||||
try mip_pyramid.append(MipLevel{
|
||||
.data = data,
|
||||
.width = width,
|
||||
.height = height,
|
||||
});
|
||||
|
||||
for (1..mip_levels_to_gen) |mip_level| {
|
||||
const divisor = std.math.powi(usize, 2, mip_level) catch unreachable;
|
||||
const mip_width = width / divisor;
|
||||
const mip_height = height / divisor;
|
||||
|
||||
if (mip_width % 4 != 0 or mip_height % 4 != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
try mip_pyramid.append(
|
||||
MipLevel{
|
||||
.width = mip_width,
|
||||
.height = mip_height,
|
||||
.data = try allocator.alloc(u8, mip_width * mip_height * FORCED_COMPONENTS),
|
||||
},
|
||||
);
|
||||
actual_mip_count += 1;
|
||||
}
|
||||
std.log.debug("mip count {}\n", .{actual_mip_count});
|
||||
|
||||
for (0..actual_mip_count) |mip_level| {
|
||||
const mip_data = &mip_pyramid.items[mip_level];
|
||||
if (mip_level > 0) {
|
||||
downsampleImage2X(&mip_pyramid.items[mip_level - 1], mip_data);
|
||||
}
|
||||
|
||||
const blocks_x: usize = mip_data.width / 4;
|
||||
const blocks_y: usize = mip_data.height / 4;
|
||||
|
||||
const out_data = try allocator.alloc(u8, blocks_x * blocks_y * 16);
|
||||
|
||||
const rgba_surf = c.rgba_surface{
|
||||
.width = @intCast(mip_data.width),
|
||||
.height = @intCast(mip_data.height),
|
||||
.stride = @intCast(mip_data.width * FORCED_COMPONENTS),
|
||||
.ptr = mip_data.data.ptr,
|
||||
};
|
||||
|
||||
c.CompressBlocksBC7(&rgba_surf, out_data.ptr, &settings);
|
||||
|
||||
mip_data.out_data = out_data;
|
||||
}
|
||||
|
||||
const out_data = try allocator.alloc([]const u8, actual_mip_count);
|
||||
for (0..actual_mip_count) |mip_level| {
|
||||
out_data[mip_level] = mip_pyramid.items[mip_level].out_data;
|
||||
}
|
||||
|
||||
const texture = formats.Texture{
|
||||
.header = .{
|
||||
.format = .bc7_srgb,
|
||||
.format = .bc7,
|
||||
.width = @intCast(width),
|
||||
.height = @intCast(height),
|
||||
.mip_levels = 1,
|
||||
.size = @intCast(out_data.len),
|
||||
.mip_count = @intCast(actual_mip_count),
|
||||
},
|
||||
.data = out_data,
|
||||
};
|
||||
|
||||
const out_file = try std.fs.createFileAbsolute(output, .{});
|
||||
defer out_file.close();
|
||||
var buf_writer = std.io.bufferedWriter(out_file.writer());
|
||||
|
||||
try formats.writeTexture(buf_writer.writer(), texture);
|
||||
try formats.writeTexture(buf_writer.writer(), texture, formats.native_endian);
|
||||
try buf_writer.flush();
|
||||
}
|
||||
|
||||
const gamma = 2.2;
|
||||
const srgb_to_linear: [256]u8 = blk: {
|
||||
@setEvalBranchQuota(10000);
|
||||
var result: [256]u8 = undefined;
|
||||
for (0..256) |i| {
|
||||
var f: f32 = @floatFromInt(i);
|
||||
f /= 255.0;
|
||||
result[i] = @intFromFloat(std.math.pow(f32, f, gamma) * 255.0);
|
||||
}
|
||||
break :blk result;
|
||||
};
|
||||
|
||||
fn convertSrgb(img: []u8) void {
|
||||
@setRuntimeSafety(false);
|
||||
|
||||
for (0..img.len / 4) |i| {
|
||||
const pixel = img[i * 4 .. i * 4 + 4];
|
||||
|
||||
pixel[0] = srgb_to_linear[pixel[0]];
|
||||
pixel[1] = srgb_to_linear[pixel[1]];
|
||||
pixel[2] = srgb_to_linear[pixel[2]];
|
||||
}
|
||||
}
|
||||
|
||||
fn premultiplyAlpha(img: []u8) void {
|
||||
for (0..img.len / 4) |i| {
|
||||
const pixel = img[i * 4 .. i * 4 + 4];
|
||||
|
||||
const r = @as(f32, @floatFromInt(pixel[0])) / 255.0;
|
||||
const g = @as(f32, @floatFromInt(pixel[1])) / 255.0;
|
||||
const b = @as(f32, @floatFromInt(pixel[2])) / 255.0;
|
||||
const a = @as(f32, @floatFromInt(pixel[3])) / 255.0;
|
||||
|
||||
pixel[0] = @intFromFloat(r * a * 255.0);
|
||||
pixel[1] = @intFromFloat(g * a * 255.0);
|
||||
pixel[2] = @intFromFloat(b * a * 255.0);
|
||||
}
|
||||
}
|
||||
|
||||
inline fn vecPow(x: @Vector(4, f32), y: f32) @Vector(4, f32) {
|
||||
return @exp(@log(x) * @as(@Vector(4, f32), @splat(y)));
|
||||
}
|
||||
|
||||
fn downsampleImage2X(src: *const MipLevel, dst: *const MipLevel) void {
|
||||
const srcStride = src.width * 4;
|
||||
const dstStride = dst.width * 4;
|
||||
for (0..dst.height) |y| {
|
||||
for (0..dst.width) |x| {
|
||||
const x0 = x * 2;
|
||||
const y0 = y * 2;
|
||||
var result = @Vector(4, f32){ 0, 0, 0, 0 };
|
||||
|
||||
for (0..2) |y1| {
|
||||
for (0..2) |x1| {
|
||||
const srcX = x0 + x1;
|
||||
const srcY = y0 + y1;
|
||||
|
||||
result += loadColorVec(src.data[srcY * srcStride + srcX * 4 ..]);
|
||||
}
|
||||
}
|
||||
|
||||
result /= @splat(4);
|
||||
storeColorVec(dst.data[y * dstStride + x * 4 ..], result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fn loadColorVec(pixel: []const u8) @Vector(4, f32) {
|
||||
@setRuntimeSafety(false);
|
||||
std.debug.assert(pixel.len >= 4);
|
||||
|
||||
return @Vector(4, f32){
|
||||
@as(f32, @floatFromInt(pixel[0])),
|
||||
@as(f32, @floatFromInt(pixel[1])),
|
||||
@as(f32, @floatFromInt(pixel[2])),
|
||||
@as(f32, @floatFromInt(pixel[3])),
|
||||
} / @as(@Vector(4, f32), @splat(255.0));
|
||||
}
|
||||
|
||||
inline fn storeColorVec(pixel: []u8, vec: @Vector(4, f32)) void {
|
||||
@setRuntimeSafety(false);
|
||||
std.debug.assert(pixel.len >= 4);
|
||||
|
||||
const out = vec * @as(@Vector(4, f32), @splat(255.0));
|
||||
|
||||
pixel[0] = @intFromFloat(out[0]);
|
||||
pixel[1] = @intFromFloat(out[1]);
|
||||
pixel[2] = @intFromFloat(out[2]);
|
||||
pixel[3] = @intFromFloat(out[3]);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user