#include "assets/data_loaders/obj_loader.hpp" #include #include #include #include "assets/components/mesh_vertex_components.hpp" #include "assets/dynamic_data_loaders/dynamic_material_loader.hpp" #include "util/logger.hpp" #include "util/for_each.hpp" #include "util/uix.hpp" #include #include "util/line_parser.hpp" namespace obj_loader_error { struct category : std::error_category { [[nodiscard]] const char* name() const noexcept override { return "connector"; } [[nodiscard]] std::string message(int ev) const override { switch (static_cast(ev)) { using enum codes; case obj_cannot_open_file: return "Cannot open given obj file."; case obj_malformed_vertex: return "File contains malformed 'v' statement."; case obj_malformed_texture_coordinate: return "File contains malformed 'vt' statement."; case obj_malformed_normal: return "File contains malformed 'vn' statement."; case obj_malformed_face: return "File contains malformed 'f' statement."; case obj_face_index_out_of_range: return "Face index out of range."; case obj_unknown_line_begin: return "Unknown obj line begin."; default: using namespace std::string_literals; return "unrecognized error ("s + std::to_string(ev) + ")"; } } }; } // namespace mesh_loader_error inline std::error_category& connector_error_category() { static obj_loader_error::category category; return category; } namespace obj_loader_error { inline std::error_code make_error_code(codes e) { return { static_cast(e), connector_error_category() }; } } // namespace mesh_loader_error using vertex_type = std::array; struct indexed_vertex_type { vertex_type vertex; ztu::u32 buffer_index; friend auto operator<=>(const indexed_vertex_type& a, const indexed_vertex_type& b) { return a.vertex <=> b.vertex; } bool operator==(const indexed_vertex_type& other) const noexcept { return other.vertex == vertex; } }; // TODO add compile time selection and unrolling template std::errc parse_numeric_vector(std::string_view param, std::array& values) { auto it = param.begin(), end = param.end(); for (auto& value : values) { if (it >= end) { return std::errc::invalid_argument; } const auto [ptr, ec] = std::from_chars(it, end, value); if (ec != std::errc{}) { return ec; } it = ptr + 1; // skip space in between components } return {}; }; std::error_code obj_loader::load_directory( dynamic_data_loader_ctx& ctx, dynamic_mesh_store& store, components::mesh_vertex::flags enabled_components, const std::filesystem::path& path, const bool pedantic ) { namespace fs = std::filesystem; if (not fs::exists(path)) { return make_error_code(std::errc::no_such_file_or_directory); } for (const auto& file : fs::directory_iterator{ path }) { const auto& file_path = file.path(); if (file_path.extension() != ".obj") { continue; } if (const auto e = load( ctx, store, enabled_components, path, pedantic )) { ztu::logger::error( "Error while loading obj file '%': [%] %", file_path, e.category().name(), e.message() ); } } return {}; } // TODO refactor so there is a function like parse_normals etc. std::error_code obj_loader::load( dynamic_data_loader_ctx& ctx, dynamic_mesh_store& store, components::mesh_vertex::flags enabled_components, const std::filesystem::path& filename, const bool pedantic ) { using obj_loader_error::codes; using obj_loader_error::make_error_code; auto in = std::ifstream{ filename }; if (not in.is_open()) { return make_error_code(codes::obj_cannot_open_file); } namespace fs = std::filesystem; const auto directory = fs::path(filename).parent_path(); // Each vertex of a face can represent a unique combination of vertex-/texture-/normal-coordinates. // But some combinations may occur more than once, for example on every corner of a cube 3 triangles will // reference the exact same corner vertex. // To get the best rendering performance and lowest final memory footprint these duplicates // need to be removed. So this std::set lookup is used to identify the aforementioned duplicates // and only push unique combinations to the buffers. std::set vertex_ids; auto mesh = dynamic_mesh_data{}; // Buffers auto position_buffer = mesh.positions(); auto normal_buffer = mesh.normals(); auto tex_coord_buffer = mesh.tex_coords(); std::unordered_map material_name_lookup; constexpr auto mtl_loader_id = *ctx.material_loader.find_loader_static("mtl"); mtl_loader& material_loader = ctx.material_loader.get_loader(); material_loader.clear_name_lookup(); std::string material_name; const auto push_mesh = [&](const bool clear_buffers = false) { if (not mesh.positions().empty()) { // Copy buffers instead of moving to keep capacity for further parsing // and have the final buffers be shrunk to size. if (not material_name.empty()) { if (const auto id = material_loader.find_id(material_name)) { mesh.material_id() = *id; } else { ztu::logger::warn( "Could not find material '%'.", material_name ); } } ztu::logger::debug("Parsed % positions.", mesh.positions().size()); ztu::logger::debug("Parsed % normals.", mesh.normals().size()); ztu::logger::debug("Parsed % tex_coords.", mesh.tex_coords().size()); if (not mesh.positions().empty()) { mesh.components() |= components::mesh_vertex::flags::position; } if (not mesh.normals().empty()) { mesh.components() |= components::mesh_vertex::flags::normal; } if (not mesh.tex_coords().empty()) { mesh.components() |= components::mesh_vertex::flags::tex_coord; } ztu::logger::debug("Pushing obj mesh with % triangles.", mesh.triangles().size()); store.add(std::move(mesh)); } if (clear_buffers) { position_buffer.clear(); normal_buffer.clear(); tex_coord_buffer.clear(); } mesh = dynamic_mesh_data{}; vertex_ids.clear(); material_name.clear(); }; const auto find_or_push_vertex = [&](const vertex_type& vertex) -> ztu::u32 { auto indexed_vid = indexed_vertex_type{ .vertex = vertex, .buffer_index = static_cast(mesh.positions().size()) }; // Search through sorted lookup to check if index combination is unique const auto [ id_it, unique ] = vertex_ids.insert(indexed_vid); if (unique) { const auto& [ position_index, tex_coord_index, normal_index ] = vertex; if (position_index < position_buffer.size()) { mesh.positions().emplace_back(position_buffer[position_index]); } if (normal_index < normal_buffer.size()) { mesh.normals().emplace_back(normal_buffer[normal_index]); } if (tex_coord_index < tex_coord_buffer.size()) { mesh.tex_coords().emplace_back(tex_coord_buffer[tex_coord_index]); } } return id_it->buffer_index; }; using flags = components::mesh_vertex::flags; const auto component_disabled = [&](const flags component) { return (enabled_components & component) == flags::none; }; const auto positions_disabled = component_disabled(flags::position); const auto normals_disabled = component_disabled(flags::normal); const auto tex_coords_disabled = component_disabled(flags::tex_coord); const auto ec = ztu::parse_lines( in, pedantic, ztu::make_line_parser("v ", ztu::is_repeating, [&](const auto& param) { if (positions_disabled) return codes::ok; components::mesh_vertex::position position; if (parse_numeric_vector(param, position) != std::errc{}) [[unlikely]] { return codes::obj_malformed_vertex; } position_buffer.push_back(position); return codes::ok; }), ztu::make_line_parser("vt ", ztu::is_repeating, [&](const auto& param) { if (tex_coords_disabled) return codes::ok; components::mesh_vertex::tex_coord coord; if (parse_numeric_vector(param, coord) != std::errc{}) [[unlikely]] { return codes::obj_malformed_texture_coordinate; } tex_coord_buffer.push_back(coord); return codes::ok; }), ztu::make_line_parser("vn ", ztu::is_repeating, [&](const auto& param) { if (normals_disabled) return codes::ok; components::mesh_vertex::normal normal; if (parse_numeric_vector(param, normal) != std::errc{}) [[unlikely]] { return codes::obj_malformed_normal; } normal_buffer.push_back(normal); return codes::ok; }), ztu::make_line_parser("o ", ztu::is_not_repeating, [&](const auto&) { push_mesh(); // Name is currently ignored return codes::ok; }), ztu::make_line_parser("f ", ztu::is_repeating, [&](const auto& param) { const auto begin = param.begin().base(); const auto end = param.end().base(); auto vertex = vertex_type{}; ztu::u32 first_index{}, prev_index{}; auto vertex_count = std::size_t{}; for (auto it = begin; it <= end; ++it) { for (auto& component_index : vertex) { if (it != end and *it == '/') { ++it; continue; } const auto [ptr, ec] = std::from_chars(it, end, component_index); if (ec != std::errc()) [[unlikely]] { // Discard whole face if one index is malformed return codes::obj_malformed_face; } --component_index; // Indices start at one it = ptr; if (it == end or *it != '/') { break; } ++it; } ++vertex_count; if (it != end and *it != ' ') [[unlikely]] { return codes::obj_malformed_face; } const auto curr_index = find_or_push_vertex(vertex); if (vertex_count >= 3) { auto& triangle = mesh.triangles().emplace_back(); triangle[0] = first_index; triangle[1] = prev_index; triangle[2] = curr_index; } else if (vertex_count == 1) { first_index = curr_index; } prev_index = curr_index; } return codes::ok; }), ztu::make_line_parser("usemtl ", ztu::is_not_repeating, [&](const auto& param) { push_mesh(false); material_name = param; return codes::ok; }), ztu::make_line_parser("mtllib ", ztu::is_not_repeating, [&](const auto& param) { auto material_filename = fs::path(param); if (material_filename.is_relative()) { material_filename = directory / material_filename; } if (const auto error = ctx.material_loader.read( ctx, mtl_loader_id, material_filename, pedantic )) { ztu::logger::warn( "Error occurred while loading mtl files '%': [%] %", material_filename, error.category().name(), error.message() ); } }) ); material_loader.clear_name_lookup(); if (ec != codes::ok) { return make_error_code(ec); } push_mesh(); return {}; }