engine/tools/asset_compiler.zig
2024-02-15 16:38:16 +04:00

217 lines
7.1 KiB
Zig

const std = @import("std");
const formats = @import("formats");
const asset_manifest = @import("asset_manifest");
const Vector2 = formats.Vector2;
const Vector3 = formats.Vector3;
const c = @cImport({
@cInclude("assimp/cimport.h");
@cInclude("assimp/scene.h");
@cInclude("assimp/mesh.h");
@cInclude("assimp/postprocess.h");
@cInclude("stb_image.h");
});
const basisu = @import("mach-basisu");
const ASSET_MAX_BYTES = 1024 * 1024 * 1024;
const AssetType = enum {
Mesh,
Shader,
ShaderProgram,
Texture,
};
pub fn resolveAssetTypeByExtension(path: []const u8) ?AssetType {
if (std.mem.endsWith(u8, path, ".obj")) {
return .Mesh;
}
if (std.mem.endsWith(u8, path, ".prog")) {
return .ShaderProgram;
}
if (std.mem.endsWith(u8, path, ".glsl")) {
return .Shader;
}
if (std.mem.endsWith(u8, path, ".png")) {
return .Texture;
}
return null;
}
pub fn main() !void {
const allocator = std.heap.c_allocator;
const argv = std.os.argv;
if (argv.len < 3) {
std.log.err("usage assetc <basedir> <input> <output>\n", .{});
return error.MissingArgs;
}
const input = argv[argv.len - 2];
const output = std.mem.span(argv[argv.len - 1]);
const asset_type = resolveAssetTypeByExtension(std.mem.span(input)) orelse return error.UnknownAssetType;
switch (asset_type) {
.Mesh => try processMesh(allocator, input, output),
.ShaderProgram => try processShaderProgram(allocator, std.mem.span(input), output),
.Texture => try processTexture(allocator, input, output),
else => return error.CantProcessAssetType,
}
}
fn processMesh(allocator: std.mem.Allocator, input: [*:0]const u8, output: []const u8) !void {
const maybe_scene: ?*const c.aiScene = @ptrCast(c.aiImportFile(
input,
c.aiProcess_CalcTangentSpace | c.aiProcess_Triangulate | c.aiProcess_JoinIdenticalVertices | c.aiProcess_SortByPType | c.aiProcess_GenNormals,
));
if (maybe_scene == null) {
std.log.err("assimp import error: {s}\n", .{c.aiGetErrorString()});
return error.ImportFailed;
}
const scene = maybe_scene.?;
defer c.aiReleaseImport(scene);
if (scene.mNumMeshes == 0) return error.NoMeshes;
if (scene.mNumMeshes > 1) return error.TooManyMeshes;
const mesh: *c.aiMesh = @ptrCast(scene.mMeshes[0]);
if (mesh.mNormals == null) return error.MissingNormals;
if (mesh.mTextureCoords[0] == null) return error.MissingUVs;
if (mesh.mNumUVComponents[0] != 2) return error.WrongUVComponents;
var vertices = try allocator.alloc(Vector3, @intCast(mesh.mNumVertices));
var normals = try allocator.alloc(Vector3, @intCast(mesh.mNumVertices));
var uvs = try allocator.alloc(Vector2, @intCast(mesh.mNumVertices));
var indices = try allocator.alloc(formats.Index, @intCast(mesh.mNumFaces * 3)); // triangles
for (0..mesh.mNumVertices) |i| {
vertices[i] = .{
.x = mesh.mVertices[i].x,
.y = mesh.mVertices[i].y,
.z = mesh.mVertices[i].z,
};
normals[i] = .{
.x = mesh.mNormals[i].x,
.y = mesh.mNormals[i].y,
.z = mesh.mNormals[i].z,
};
uvs[i] = .{
.x = mesh.mTextureCoords[0][i].x,
.y = mesh.mTextureCoords[0][i].y,
};
}
for (0..mesh.mNumFaces) |i| {
std.debug.assert(mesh.mFaces[i].mNumIndices == 3);
for (0..3) |j| {
const index = mesh.mFaces[i].mIndices[j];
if (index > std.math.maxInt(formats.Index)) {
std.log.err("indices out of range for index format: {}\n", .{index});
return error.TimeToIncreaseIndexSize;
}
indices[i * 3 + j] = @intCast(index);
}
}
const out_mesh = formats.Mesh{
.vertices = vertices,
.normals = normals,
.uvs = uvs,
.indices = indices,
};
const out_file = try std.fs.createFileAbsolute(output, .{});
defer out_file.close();
var buf_writer = std.io.bufferedWriter(out_file.writer());
try formats.writeMesh(
buf_writer.writer(),
out_mesh,
formats.native_endian, // TODO: use target endiannes
);
try buf_writer.flush();
}
fn processShaderProgram(allocator: std.mem.Allocator, absolute_input: []const u8, output: []const u8) !void {
var cwd_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const cwd_path = try std.os.getcwd(&cwd_buf);
const input = try std.fs.path.relative(allocator, cwd_path, absolute_input);
defer allocator.free(input);
const input_dir = std.fs.path.dirname(input).?;
var file_contents: []u8 = undefined;
{
const input_file = try std.fs.cwd().openFile(input, .{});
defer input_file.close();
file_contents = try input_file.readToEndAlloc(allocator, ASSET_MAX_BYTES);
}
defer allocator.free(file_contents);
const ShaderProgram = struct {
shader: []const u8,
vertex: bool,
fragment: bool,
};
const program = try std.json.parseFromSlice(ShaderProgram, allocator, file_contents, .{});
defer program.deinit();
const shader_path = try std.fs.path.resolve(allocator, &.{ input_dir, program.value.shader });
const shader_asset_id = asset_manifest.getAssetByPath(shader_path);
if (shader_asset_id == 0) {
std.log.debug("{s}\n", .{shader_path});
return error.InvalidShaderPath;
}
const out_file = try std.fs.createFileAbsolute(output, .{});
defer out_file.close();
var buf_writer = std.io.bufferedWriter(out_file.writer());
try formats.writeShaderProgram(buf_writer.writer(), shader_asset_id, program.value.vertex, program.value.fragment, formats.native_endian);
try buf_writer.flush();
}
fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: []const u8) !void {
_ = allocator; // autofix
var x: c_int = undefined;
var y: c_int = undefined;
var comps: c_int = undefined;
c.stbi_set_flip_vertically_on_load(1);
const FORCED_COMPONENTS = 3; // force rgb
const data_c = @as(?[*]u8, @ptrCast(c.stbi_load(input, &x, &y, &comps, FORCED_COMPONENTS))) orelse return error.ImageLoadError;
defer c.stbi_image_free(data_c);
const data = data_c[0 .. @as(usize, @intCast(x)) * @as(usize, @intCast(y)) * FORCED_COMPONENTS];
basisu.init_encoder();
var params = basisu.CompressorParams.init(@intCast(try std.Thread.getCpuCount()));
defer params.deinit();
const img = params.getImageSource(0);
img.fill(data, @intCast(x), @intCast(y), @intCast(FORCED_COMPONENTS));
// TODO: configure per-texture somehow
params.setQualityLevel(64);
params.setBasisFormat(basisu.BasisTextureFormat.uastc4x4);
params.setColorSpace(basisu.ColorSpace.srgb);
params.setGenerateMipMaps(true);
var compressor = try basisu.Compressor.init(params);
defer compressor.deinit();
try compressor.process();
const out_file = try std.fs.createFileAbsolute(output, .{});
defer out_file.close();
var buf_writer = std.io.bufferedWriter(out_file.writer());
try buf_writer.writer().writeAll(compressor.output());
try buf_writer.flush();
}