Files
Z3D/source/assets/data_loaders/obj_loader.cpp
2024-12-22 21:52:05 +01:00

610 lines
14 KiB
C++
Executable File

#include "assets/data_loaders/obj_loader.hpp"
#include <charconv>
#include <fstream>
#include <array>
#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 <set>
#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<codes>(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<int>(e), connector_error_category() };
}
} // namespace mesh_loader_error
using vertex_type = std::array<dynamic_mesh_buffer::index_type, 3>;
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<typename T, std::size_t Count>
std::errc parse_numeric_vector(std::string_view param, std::array<T, Count>& 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<char> 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<char>(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<indexed_vertex_type> 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<indexed_vertex_type> 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<std::string, ztu::u32> 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<ztu::u32>(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<codes>(
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 {};
}