Load gltf with embedded albedo, metallic and roughness maps

This commit is contained in:
sergeypdev 2024-02-27 00:01:58 +04:00
parent 2067a133f8
commit d33d6b2454
12 changed files with 269 additions and 56 deletions

2
.gitattributes vendored
View File

@ -2,6 +2,8 @@
* text=auto eol=lf
# Git LFS hooks, add large file types to track here.
*.glb filter=lfs diff=lfs merge=lfs -text
*.gltf filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text

BIN
assets/amd_ryzen_9.glb (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -81,9 +81,9 @@ struct Material {
Material evalMaterial() {
Material result;
result.albedo = textureSize(albedo_map, 0) == ivec2(0) ? pow(color, vec3(2.2)) : texture(albedo_map, VertexOut.uv).rgb;
float fMetallic = textureSize(metallic_map, 0) == ivec2(0) ? metallic : texture(metallic_map, VertexOut.uv).r;
float fMetallic = textureSize(metallic_map, 0) == ivec2(0) ? metallic : texture(metallic_map, VertexOut.uv).b;
result.metallic = fMetallic > 0.5;
result.roughness = max(0.01, textureSize(roughness_map, 0) == ivec2(0) ? roughness : texture(roughness_map, VertexOut.uv).r);
result.roughness = max(0.01, textureSize(roughness_map, 0) == ivec2(0) ? roughness : texture(roughness_map, VertexOut.uv).g);
result.emission = textureSize(emission_map, 0) == ivec2(0) ? emission : texture(emission_map, VertexOut.uv).rgb;
return result;

View File

@ -20,11 +20,11 @@
.hash = "1220483cbb42231cb056f4ea6669894c68ccd560d3af5832d6e9c84c61844bc20b7d",
},
.@"zig-assimp" = .{
.url = "https://github.com/sergeypdev/zig-assimp/tarball/59c2f0202bf1e5110f3eb219669f3762e3db2768",
.hash = "122015247b178258ee2fd9f7fbd3f8025138e8e38b5cbecdc94262974da49bd1c225",
.url = "https://github.com/sergeypdev/zig-assimp/tarball/39380dcc231788b3b54f447540bd6fea296fab10",
.hash = "12209b95f2f5a85107ed38814143179f1e7516440704fb94004046d6f5b5ed3c8667",
},
.zalgebra = .{
.url = "git+https://github.com/sergeypdev/zalgebra.git#232ff76712dc7cc270b6c48cedc84617536f3a59",
.url = "https://github.com/sergeypdev/zalgebra/tarball/232ff76712dc7cc270b6c48cedc84617536f3a59",
.hash = "12206e29e5d0f012c694f413b21cb66238964fdaef0a29781e0bf3ff75ec08a2ed78",
},
},

View File

@ -137,6 +137,21 @@ pub fn resolveScene(self: *AssetManager, handle: Handle.Scene) *const formats.Sc
return &self.loadScene(handle.id).scene;
}
pub fn resolveMaterial(self: *AssetManager, handle: Handle.Material) *const formats.Material {
if (handle.id == 0) return &NullMaterial;
if (self.loaded_assets.getPtr(handle.id)) |asset| {
switch (asset.*) {
.material => |*material| {
return material;
},
else => unreachable,
}
}
return self.loadMaterial(handle.id);
}
// TODO: proper watching
pub fn watchChanges(self: *AssetManager) void {
var iter = self.loaded_assets.iterator();
@ -278,6 +293,8 @@ const NullScene = LoadedScene{
.scene = .{},
};
const NullMaterial = formats.Material{};
pub fn loadMesh(self: *AssetManager, id: AssetId) *const LoadedMesh {
return self.loadMeshErr(id) catch |err| {
std.log.err("Error: {} loading mesh at path: {s}", .{ err, asset_manifest.getPath(id) });
@ -474,12 +491,39 @@ fn loadSceneErr(self: *AssetManager, id: AssetId) !*const LoadedScene {
return &self.loaded_assets.getPtr(id).?.scene;
}
fn loadMaterial(self: *AssetManager, id: AssetId) *const formats.Material {
return self.loadMaterialErr(id) catch |err| {
std.log.err("Error: {} loading material at path {s}\n", .{ err, asset_manifest.getPath(id) });
return &NullMaterial;
};
}
fn loadMaterialErr(self: *AssetManager, id: AssetId) !*const formats.Material {
const path = asset_manifest.getPath(id);
const data = try self.loadFile(self.frame_arena, path, TEXTURE_MAX_BYTES);
const material = formats.Material.fromBuffer(data.bytes);
try self.loaded_assets.put(
self.allocator,
id,
.{
.material = material,
},
);
try self.modified_times.put(self.allocator, id, data.modified);
return &self.loaded_assets.getPtr(id).?.material;
}
const LoadedAsset = union(enum) {
shader: LoadedShader,
shaderProgram: LoadedShaderProgram,
mesh: LoadedMesh,
texture: LoadedTexture,
scene: LoadedScene,
material: formats.Material,
};
const LoadedShader = struct {
@ -666,6 +710,7 @@ fn unloadAssetWithDependees(self: *AssetManager, id: AssetId) void {
.scene => |*scene| {
self.allocator.free(scene.buf);
},
.material => {},
}
}
_ = self.loaded_assets.remove(id);

View File

@ -6,4 +6,5 @@ pub const Handle = struct {
pub const ShaderProgram = extern struct { id: AssetId = 0 };
pub const Mesh = extern struct { id: AssetId = 0 };
pub const Texture = extern struct { id: AssetId = 0 };
pub const Material = extern struct { id: AssetId = 0 };
};

View File

@ -163,6 +163,11 @@ fn writeVector3(writer: anytype, value: Vector3, endian: std.builtin.Endian) !vo
try writeFloat(writer, value.y, endian);
try writeFloat(writer, value.z, endian);
}
fn writeVec3(writer: anytype, value: Vec3, endian: std.builtin.Endian) !void {
try writeFloat(writer, value.x(), endian);
try writeFloat(writer, value.y(), endian);
try writeFloat(writer, value.z(), endian);
}
fn writeFloat(writer: anytype, value: f32, endian: std.builtin.Endian) !void {
const val: u32 = @bitCast(value);
@ -350,6 +355,7 @@ test "write and read scene" {
}
pub const Material = extern struct {
// TODO: rgba
albedo: Vec3 = Vec3.one(),
albedo_map: Handle.Texture = .{},
normal_map: Handle.Texture = .{},
@ -359,4 +365,15 @@ pub const Material = extern struct {
roughness_map: Handle.Texture = .{},
emission: f32 = 0,
emission_map: Handle.Texture = .{},
pub fn fromBuffer(buf: []const u8) Material {
const mat: *align(1) const Material = @ptrCast(buf);
return mat.*;
}
};
// TODO: doesn't respect endianness
pub fn writeMaterial(writer: anytype, value: Material) !void {
try writer.writeStruct(value);
}

View File

@ -224,9 +224,9 @@ export fn game_init(global_allocator: *std.mem.Allocator) void {
.transform = .{ .scale = Vec3.one().scale(2) },
.mesh = .{
.handle = a.Meshes.plane,
.material = .{
.normal_map = a.Textures.@"tile.norm",
},
// .material = .{
// .normal_map = a.Textures.@"tile.norm",
// },
},
});
@ -239,11 +239,11 @@ export fn game_init(global_allocator: *std.mem.Allocator) void {
.flags = .{ .mesh = true },
.mesh = .{
.handle = a.Meshes.bunny,
.material = .{
.albedo_map = a.Textures.bunny_tex1,
// .normal_map = a.Textures.@"tile.norm",
.roughness = @as(f32, @floatFromInt(i)) / 10.0,
},
// .material = .{
// .albedo_map = a.Textures.bunny_tex1,
// // .normal_map = a.Textures.@"tile.norm",
// .roughness = @as(f32, @floatFromInt(i)) / 10.0,
// },
},
});
}
@ -257,19 +257,22 @@ export fn game_init(global_allocator: *std.mem.Allocator) void {
.flags = .{ .mesh = true },
.mesh = .{
.handle = a.Meshes.bunny,
.material = .{
.albedo = Vec3.new(1.000, 0.766, 0.336),
// .albedo_map = a.Textures.bunny_tex1,
// .normal_map = a.Textures.@"tile.norm",
.roughness = @as(f32, @floatFromInt(i + 1)) / 10.0,
.metallic = 1.0,
},
// .material = .{
// .albedo = Vec3.new(1.000, 0.766, 0.336),
// // .albedo_map = a.Textures.bunny_tex1,
// // .normal_map = a.Textures.@"tile.norm",
// .roughness = @as(f32, @floatFromInt(i + 1)) / 10.0,
// .metallic = 1.0,
// },
},
});
}
}
// const test_scene_root = globals.g_mem.world.createScene(globals.g_assetman.resolveScene(a.Scenes.test_scene.scene));
const ryzen = globals.g_mem.world.createScene(globals.g_assetman.resolveScene(a.Scenes.amd_ryzen_9.scene));
const ent = globals.g_mem.world.getEntity(ryzen) orelse @panic("WTF");
ent.data.transform.pos = Vec3.new(0, 1, 0);
ent.data.transform.scale = Vec3.one().scale(0.2);
}
export fn game_update() bool {
@ -456,7 +459,7 @@ export fn game_update() bool {
if (ent.data.flags.mesh) {
gmem.render.draw(.{
.mesh = ent.data.mesh.handle,
.material = ent.data.mesh.material,
.material = gmem.assetman.resolveMaterial(ent.data.mesh.material).*,
.transform = ent.globalMatrix(&gmem.world).*,
});
} else if (ent.data.flags.point_light) {

View File

@ -5,6 +5,7 @@ pub const Meshes = manifest.Meshes;
pub const Shaders = manifest.Shaders;
pub const ShaderPrograms = manifest.ShaderPrograms;
pub const Textures = manifest.Textures;
pub const Materials = manifest.Materials;
pub fn getPath(asset_id: u64) []const u8 {
manifest.init();

View File

@ -77,7 +77,7 @@ pub const Entity = struct {
pub const Mesh = extern struct {
handle: AssetManager.Handle.Mesh = .{},
material: Material = .{},
material: AssetManager.Handle.Material = .{},
};
pub const PointLight = extern struct {
radius: f32 = std.math.floatEps(f32), // should never be 0 or bad things happen

View File

@ -69,8 +69,9 @@ pub fn main() !void {
const cwd_path = try std.os.getcwd(&cwd_buf);
const rel_input = try std.fs.path.relative(allocator, cwd_path, abs_input);
const rel_output = try std.fs.path.relative(allocator, cwd_path, output_dirname);
var output_dir = try std.fs.cwd().makeOpenPath(output_dirname, .{});
var output_dir = try std.fs.cwd().makeOpenPath(rel_output, .{});
defer output_dir.close();
const asset_type = resolveAssetTypeByExtension(abs_input) orelse return error.UnknownAssetType;
@ -78,7 +79,7 @@ pub fn main() !void {
var buf_asset_list_writer = std.io.bufferedWriter(std.io.getStdOut().writer());
const asset_list_writer = buf_asset_list_writer.writer();
std.log.debug("type: {s}, rel_input: {s}", .{ @tagName(asset_type), rel_input });
std.log.debug("type: {s}, rel_input: {s}, output_dir: {s}", .{ @tagName(asset_type), rel_input, rel_output });
switch (asset_type) {
.Scene => try processScene(allocator, rel_input, output_dir, asset_list_writer),
@ -134,16 +135,23 @@ fn createOutput(_type: AssetType, asset_path: AssetPath, output_dir: std.fs.Dir,
};
}
const AI_MATKEY_NAME = "?mat.name";
const AI_MATKEY_SHADING_MODEL = "$mat.shadingm";
const AI_MATKEY_BASE_COLOR = "$clr.base";
const AI_MATKEY_METALLIC_FACTOR = "$mat.metallicFactor";
const AI_MATKEY_ROUGHNESS_FACTOR = "$mat.roughnessFactor";
const AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE = c.aiTextureType_UNKNOWN;
/// This can output either a single mesh (for simple formats like obj)
/// or a scene + a bunch of sub assets (meshes, materials, textures, animations, etc.)
/// It all depends on the source asset.
fn processScene(allocator: std.mem.Allocator, input: []const u8, output_dir: std.fs.Dir, asset_list_writer: anytype) !void {
const input_z = try std.mem.concatWithSentinel(allocator, u8, &.{input}, 0);
const config: *c.aiPropertyStore = @as(?*c.aiPropertyStore, @ptrCast(c.aiCreatePropertyStore())) orelse return error.PropertyStore;
defer c.aiReleasePropertyStore(config);
// const config: *c.aiPropertyStore = @as(?*c.aiPropertyStore, @ptrCast(c.aiCreatePropertyStore())) orelse return error.PropertyStore;
// defer c.aiReleasePropertyStore(config);
// Remove point and line meshes
c.aiSetImportPropertyInteger(config, c.AI_CONFIG_PP_SBP_REMOVE, c.aiPrimitiveType_POINT | c.aiPrimitiveType_LINE);
// // Remove point and line meshes
// c.aiSetImportPropertyInteger(config, c.AI_CONFIG_PP_SBP_REMOVE, c.aiPrimitiveType_POINT | c.aiPrimitiveType_LINE);
const maybe_scene: ?*const c.aiScene = @ptrCast(c.aiImportFile(
input_z.ptr,
@ -154,7 +162,7 @@ fn processScene(allocator: std.mem.Allocator, input: []const u8, output_dir: std
return error.ImportFailed;
}
const scene = maybe_scene.?;
defer c.aiReleaseImport(scene);
// defer c.aiReleaseImport(scene);
if (scene.mNumMeshes == 0) return error.NoMeshes;
@ -168,20 +176,7 @@ fn processScene(allocator: std.mem.Allocator, input: []const u8, output_dir: std
} else {
const base_asset_path = AssetPath{ .simple = input };
const meshes: []*c.aiMesh = @ptrCast(scene.mMeshes[0..@intCast(scene.mNumMeshes)]);
var mesh_outputs = try allocator.alloc(AssetListEntry, meshes.len);
for (meshes, 0..) |mesh, i| {
const name = mesh.mName.data[0..mesh.mName.length];
std.log.debug("mesh name {s}\n", .{name});
var output = try createOutput(.Mesh, base_asset_path.subPath(try allocator.dupe(u8, name)), output_dir, asset_list_writer);
defer output.file.close();
mesh_outputs[i] = output.list_entry;
try processMesh(allocator, scene, mesh, output.file);
}
// Embedded textures
var texture_outputs = try allocator.alloc(AssetListEntry, @intCast(scene.mNumTextures));
if (scene.mTextures != null) {
const textures: []*c.aiTexture = @ptrCast(scene.mTextures[0..@intCast(scene.mNumTextures)]);
@ -200,12 +195,85 @@ fn processScene(allocator: std.mem.Allocator, input: []const u8, output_dir: std
var output = try createOutput(.Texture, base_asset_path.subPath(name), output_dir, asset_list_writer);
defer output.file.close();
try processTexture(allocator, name, @as([*]u8, @ptrCast(texture.pcData))[0..@intCast(texture.mWidth)], output.file);
texture_outputs[i] = output.list_entry;
try processTexture(allocator, name, @as([*]u8, @ptrCast(texture.pcData))[0..@intCast(texture.mWidth)], output.file);
}
}
// Materials
var material_outputs = try allocator.alloc(AssetListEntry, @intCast(scene.mNumMaterials));
if (scene.mMaterials != null) {
const materials: []*c.aiMaterial = @ptrCast(scene.mMaterials[0..@intCast(scene.mNumMaterials)]);
for (materials, 0..) |material, i| {
var str: c.aiString = .{};
tryAssimp(c.aiGetMaterialString(material, AI_MATKEY_NAME, 0, 0, &str)) catch {};
var name: []u8 = @alignCast(str.data[0..str.length]);
if (name.len == 0) {
name = try std.fmt.allocPrint(allocator, "material_{}", .{i + 1});
} else {
name = try allocator.dupe(u8, name);
}
var output = try createOutput(.Material, base_asset_path.subPath(name), output_dir, asset_list_writer);
defer output.file.close();
var buf_writer = std.io.bufferedWriter(output.file.writer());
material_outputs[i] = output.list_entry;
var mat_output = formats.Material{};
var base_color: c.aiColor4D = .{};
try tryAssimp(c.aiGetMaterialColor(material, AI_MATKEY_BASE_COLOR, 0, 0, &base_color));
// TODO: rgba
mat_output.albedo = Vec3.new(base_color.r, base_color.g, base_color.b);
if (c.aiGetMaterialTextureCount(material, c.aiTextureType_BASE_COLOR) > 0) {
const mat_texture = try getMaterialTexture(allocator, material, c.aiTextureType_BASE_COLOR, 0);
const entry = mat_texture.path.resolveAssetListEntry(texture_outputs);
mat_output.albedo_map.id = entry.getAssetId();
}
try tryAssimp(c.aiGetMaterialFloat(material, AI_MATKEY_METALLIC_FACTOR, 0, 0, &mat_output.metallic));
if (c.aiGetMaterialTextureCount(material, c.aiTextureType_METALNESS) > 0) {
const mat_texture = try getMaterialTexture(allocator, material, c.aiTextureType_METALNESS, 0);
const entry = mat_texture.path.resolveAssetListEntry(texture_outputs);
mat_output.metallic_map.id = entry.getAssetId();
}
try tryAssimp(c.aiGetMaterialFloat(material, AI_MATKEY_ROUGHNESS_FACTOR, 0, 0, &mat_output.roughness));
if (c.aiGetMaterialTextureCount(material, c.aiTextureType_DIFFUSE_ROUGHNESS) > 0) {
const mat_texture = try getMaterialTexture(allocator, material, c.aiTextureType_DIFFUSE_ROUGHNESS, 0);
const entry = mat_texture.path.resolveAssetListEntry(texture_outputs);
mat_output.roughness_map.id = entry.getAssetId();
}
try formats.writeMaterial(buf_writer.writer(), mat_output);
try buf_writer.flush();
}
}
const MeshEntry = struct { mesh: AssetListEntry, material: AssetListEntry };
const meshes: []*c.aiMesh = @ptrCast(scene.mMeshes[0..@intCast(scene.mNumMeshes)]);
var mesh_outputs = try allocator.alloc(MeshEntry, meshes.len);
for (meshes, 0..) |mesh, i| {
const name = mesh.mName.data[0..mesh.mName.length];
var output = try createOutput(.Mesh, base_asset_path.subPath(try allocator.dupe(u8, name)), output_dir, asset_list_writer);
defer output.file.close();
if (mesh.mMaterialIndex < 0 or @as(usize, @intCast(mesh.mMaterialIndex)) > material_outputs.len) {
return error.InvalidMaterialIndex;
}
mesh_outputs[i] = .{ .mesh = output.list_entry, .material = material_outputs[@intCast(mesh.mMaterialIndex)] };
try processMesh(allocator, scene, mesh, output.file);
}
if (scene.mRootNode == null) return;
var node_to_entity_idx = std.AutoHashMap(*c.aiNode, usize).init(allocator);
@ -253,7 +321,8 @@ fn processScene(allocator: std.mem.Allocator, input: []const u8, output_dir: std
const mesh_entry = mesh_outputs[mesh_indices[0]];
ent.flags.mesh = true;
ent.mesh.handle = .{ .id = mesh_entry.src_path.hash() };
ent.mesh.handle = .{ .id = mesh_entry.mesh.getAssetId() };
ent.mesh.material = .{ .id = mesh_entry.material.getAssetId() };
} else {
for (mesh_indices) |mesh_idx| {
const mesh_entry = mesh_outputs[@intCast(mesh_idx)];
@ -266,7 +335,8 @@ fn processScene(allocator: std.mem.Allocator, input: []const u8, output_dir: std
sub_ent.flags.mesh = true;
sub_ent.mesh = .{
.handle = .{ .id = mesh_entry.src_path.hash() },
.handle = .{ .id = mesh_entry.mesh.getAssetId() },
.material = .{ .id = mesh_entry.material.getAssetId() },
};
}
}
@ -289,6 +359,67 @@ fn processScene(allocator: std.mem.Allocator, input: []const u8, output_dir: std
}
}
const AssimpTextureRef = union(enum) {
external: []const u8,
embedded: usize,
pub fn fromString(str: []const u8) !AssimpTextureRef {
if (str.len == 0) return error.EmptyPath;
if (str[0] == '*') {
const idx = try std.fmt.parseInt(usize, str[1..], 10);
return .{ .embedded = idx };
}
return .{ .external = str };
}
pub fn resolveAssetListEntry(self: AssimpTextureRef, embedded: []const AssetListEntry) AssetListEntry {
switch (self) {
.embedded => |idx| {
return embedded[idx];
},
.external => |path| {
// TODO: resolve relative to current input file
return AssetListEntry{ .src_path = AssetPath.fromString(path), .type = .Texture };
},
}
}
};
const MaterialTexture = struct {
path: AssimpTextureRef = .{ .external = "" },
mapping: c.aiTextureMapping = 0,
uv_index: c_uint = 0,
blend: f32 = 0,
op: c.aiTextureOp = 0,
map_mode: [3]c.aiTextureMapMode = .{ 0, 0, 0 },
flags: c_uint = 0,
};
fn getMaterialTexture(allocator: std.mem.Allocator, material: *c.aiMaterial, _type: c.aiTextureType, index: c_uint) !MaterialTexture {
var path: c.aiString = undefined;
var result: MaterialTexture = .{};
try tryAssimp(c.aiGetMaterialTexture(
material,
_type,
index,
&path,
&result.mapping,
&result.uv_index,
&result.blend,
&result.op,
&result.map_mode,
&result.flags,
));
const path_str: []u8 = try allocator.dupe(u8, @alignCast(path.data[0..path.length]));
result.path = try AssimpTextureRef.fromString(path_str);
return result;
}
fn processMesh(allocator: std.mem.Allocator, scene: *const c.aiScene, mesh: *const c.aiMesh, out_file: std.fs.File) !void {
_ = scene; // autofix
if (mesh.mNormals == null) return error.MissingNormals;
@ -422,9 +553,9 @@ fn processTexture(allocator: std.mem.Allocator, input: []const u8, contents: []c
const sub_ext = std.fs.path.extension(std.fs.path.stem(input));
const format = if (std.mem.eql(u8, sub_ext, ".norm")) formats.Texture.Format.bc5 else formats.Texture.Format.bc7;
var width_int: c_int = undefined;
var height_int: c_int = undefined;
var comps: c_int = undefined;
var width_int: c_int = 0;
var height_int: c_int = 0;
var comps: c_int = 0;
c.stbi_set_flip_vertically_on_load(1);
const rgba_data_c = c.stbi_load_from_memory(contents.ptr, @intCast(contents.len), &width_int, &height_int, &comps, 4);
@ -711,9 +842,16 @@ inline fn storeColorVec4(pixel: []u8, vec: @Vector(4, f32)) void {
pixel[3] = @intFromFloat(out[3]);
}
fn changeExtensionAlloc(allocator: std.mem.Allocator, input: []const u8, new_ext: []const u8) ![]u8 {
const input_basename = std.fs.path.basename(input);
const ext = std.fs.path.extension(input_basename);
const name_without_ext = input_basename[0 .. input_basename.len - ext.len];
return try std.mem.concat(allocator, u8, &.{ name_without_ext, ".", new_ext });
fn tryAssimp(code: c.aiReturn) !void {
switch (code) {
c.aiReturn_SUCCESS => {},
c.aiReturn_FAILURE => {
std.log.err("getMaterialTexture: {s}\n", .{c.aiGetErrorString()});
return error.AssimpError;
},
c.aiReturn_OUTOFMEMORY => {
return error.OutOfMemory;
},
else => unreachable,
}
}

View File

@ -6,6 +6,7 @@ pub const AssetType = enum {
Shader,
ShaderProgram,
Texture,
Material,
pub fn pluralName(self: AssetType) []const u8 {
return switch (self) {
@ -14,6 +15,7 @@ pub const AssetType = enum {
.Shader => "Shaders",
.ShaderProgram => "ShaderPrograms",
.Texture => "Textures",
.Material => "Materials",
};
}
@ -24,6 +26,7 @@ pub const AssetType = enum {
.Shader => "glsl",
.ShaderProgram => "prog",
.Texture => "tex",
.Material => "mat",
};
}
};