#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 {}; }; void find_materials( std::span buffer, std::ifstream& in, ztu::string_list& material_filenames ) { static constexpr auto keyword = std::string_view{ "\nmtllib " }; const auto buffer_view = std::string_view(buffer); // Add linebreak to simplify line begin search buffer.front() = '\n'; auto leftover = std::size_t{ 1 }; do { // Keep some old characters to continue matching interrupted sequence std::copy(buffer.end() - leftover, buffer.end(), buffer.begin()); in.read(buffer.data() + leftover, buffer.size() - leftover); const auto str = buffer_view.substr(0, leftover + in.gcount()); leftover = keyword.size(); auto pos = std::string_view::size_type{}; while ((pos = str.find(keyword, pos)) != std::string_view::npos) { const auto filename_begin = pos + keyword.size(); const auto filename_end = str.find('\n', filename_begin); if (filename_end != std::string_view::npos) { const auto length = filename_end - filename_begin; const auto material_filename = str.substr(filename_begin, length); // TODO get base dir from param figure out how to avoid heap if (material_filename.is_relative()) { material_filename = directory / material_filename; } material_filenames.push_back(material_filename); pos = filename_end; } else // string exceeds the buffer { if (pos == 0) [[unlikely]] { std::cout << "Ignoring string match, as it exceeds buffer size." << std::endl; leftover = 0; } else { leftover = str.size() - pos; } break; } } } while (not in.eof()); } std::error_code obj_loader::prefetch( const file_dir_list& paths, prefetch_queue& queue ) { namespace fs = std::filesystem; using obj_loader_error::codes; using obj_loader_error::make_error_code; auto buffer = std::vector(8 * 1024, '\0'); auto in = std::ifstream{}; const auto parse_file = [&](const char* filename) { in.open(filename); if (not in.is_open()) { ztu::logger::error("Could not open .obj file '%'", filename); return; } find_materials(buffer, in, queue.mtl_queue.files); in.close(); }; for (const auto file : paths.files) { // `file` is null-terminates by list. parse_file(file.data()); } for (const auto directory : paths.directories) { for (const auto& file : fs::directory_iterator{ directory }) { const auto& file_path = std::string_view{ file.path().c_str() }; // TODO remove heap allocation if (not file_path.ends_with(".obj")) { continue; } // Null terminated by fs::path. parse_file(file_path.data()); } } 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 {}; } [[nodiscard]] static std::error_code load( components::mesh_vertex::flags enabled_components, dynamic_mesh_buffer& buffer, const file_dir_list& paths, prefetch_lookup& id_lookup, dynamic_data_store& store, bool pedantic = false ) { auto in = std::ifstream{}; std::set vertex_ids; const auto parse_file = [&](const char* filename) { in.open(filename); if (not in.is_open()) { ztu::logger::error("Could not open .obj file '%'", filename); return; } vertex_ids.clear(); in.close(); }; for (const auto file : paths.files) { // `file` is null-terminates by list. parse_file(file.data()); } for (const auto directory : paths.directories) { for (const auto& file : fs::directory_iterator{ directory }) { const auto& file_path = std::string_view{ file.path().c_str() }; // TODO remove heap allocation if (not file_path.ends_with(".obj")) { continue; } // Null terminated by fs::path. parse_file(file_path.data()); } } } // TODO refactor so there is a function like parse_normals etc. std::error_code obj_loader::load( components::mesh_vertex::flags enabled_components, dynamic_mesh_buffer& buffer, const char* filename, prefetch_lookup& id_lookup, dynamic_data_store& store, 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; // Buffers // TODO find out if this is still relevant auto position_buffer = buffer.positions(); auto normal_buffer = buffer.normals(); auto tex_coord_buffer = buffer.tex_coords(); std::unordered_map material_name_lookup; std::string material_name; const auto push_mesh = [&](const bool clear_buffers = false) { if (not buffer.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)) { buffer.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 buffer.positions().empty()) { buffer.components() |= components::mesh_vertex::flags::position; } if (not buffer.normals().empty()) { buffer.components() |= components::mesh_vertex::flags::normal; } if (not buffer.tex_coords().empty()) { buffer.components() |= components::mesh_vertex::flags::tex_coord; } ztu::logger::debug("Pushing obj mesh with % triangles.", buffer.triangles().size()); store.meshes.add(buffer); } if (clear_buffers) { position_buffer.clear(); normal_buffer.clear(); tex_coord_buffer.clear(); } buffer.clear(); 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(buffer.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()) { buffer.positions().emplace_back(position_buffer[position_index]); } if (normal_index < normal_buffer.size()) { buffer.normals().emplace_back(normal_buffer[normal_index]); } if (tex_coord_index < tex_coord_buffer.size()) { buffer.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() ); } }) ); if (ec != codes::ok) { return make_error_code(ec); } push_mesh(); return {}; }