603 lines
14 KiB
C++
Executable File
603 lines
14 KiB
C++
Executable File
#include "assets/file_parsers/obj_loader.hpp"
|
|
|
|
#include <charconv>
|
|
#include <fstream>
|
|
#include <array>
|
|
|
|
#include "assets/components/mesh_vertex_components.hpp"
|
|
|
|
#include "util/logger.hpp"
|
|
#include "util/for_each.hpp"
|
|
#include "util/line_parser.hpp"
|
|
|
|
namespace assets::obj_loader_error
|
|
{
|
|
struct category : std::error_category
|
|
{
|
|
[[nodiscard]] const char* name() const noexcept override {
|
|
return "obj_loader";
|
|
}
|
|
|
|
[[nodiscard]] std::string message(int ev) const override
|
|
{
|
|
switch (static_cast<codes>(ev))
|
|
{
|
|
using enum codes;
|
|
case cannot_open_file:
|
|
return "Cannot open given obj file.";
|
|
case malformed_vertex:
|
|
return "File contains malformed 'v' statement.";
|
|
case malformed_texture_coordinate:
|
|
return "File contains malformed 'vt' statement.";
|
|
case malformed_normal:
|
|
return "File contains malformed 'vn' statement.";
|
|
case malformed_face:
|
|
return "File contains malformed 'f' statement.";
|
|
case face_index_out_of_range:
|
|
return "Face index out of range.";
|
|
case unknown_line_begin:
|
|
return "Unknown obj line begin.";
|
|
case use_material_without_material_library:
|
|
return "'usemtl' statement before material library loaded.";
|
|
case unknown_material_name:
|
|
return "No matching material name found in material library.";
|
|
default:
|
|
using namespace std::string_literals;
|
|
return "unrecognized error ("s + std::to_string(ev) + ")";
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace mesh_loader_error
|
|
|
|
inline std::error_category& obj_loader_error_category()
|
|
{
|
|
static assets::obj_loader_error::category category;
|
|
return category;
|
|
}
|
|
|
|
namespace assets::obj_loader_error
|
|
{
|
|
inline std::error_code make_error_code(codes e)
|
|
{
|
|
return { static_cast<int>(e), obj_loader_error_category() };
|
|
}
|
|
} // namespace mesh_loader_error
|
|
|
|
|
|
|
|
assets::obj_loader::parser_context::parser_context(
|
|
const texture_id_lookup& texture_id_lookup,
|
|
material_id_lookup& material_id_lookup,
|
|
material_store& material_store,
|
|
store_type& store
|
|
) :
|
|
m_texture_id_lookup{ &texture_id_lookup },
|
|
m_material_id_lookup{ &material_id_lookup },
|
|
m_material_store{ &material_store },
|
|
m_store{ &store }
|
|
{
|
|
constexpr auto expected_vertex_count = 8192;
|
|
m_buffer.positions().reserve(expected_vertex_count);
|
|
m_buffer.normals().reserve(expected_vertex_count);
|
|
m_buffer.colors().reserve(expected_vertex_count);
|
|
m_buffer.reflectances().reserve(expected_vertex_count);
|
|
m_buffer.tex_coords().reserve(expected_vertex_count);
|
|
m_buffer.triangles().reserve(2 * expected_vertex_count);
|
|
|
|
m_read_buffer.positions().reserve(expected_vertex_count);
|
|
m_read_buffer.normals().reserve(expected_vertex_count);
|
|
m_read_buffer.colors().reserve(expected_vertex_count);
|
|
m_read_buffer.reflectances().reserve(expected_vertex_count);
|
|
m_read_buffer.tex_coords().reserve(expected_vertex_count);
|
|
m_read_buffer.triangles().reserve(2 * expected_vertex_count);
|
|
}
|
|
|
|
void assets::obj_loader::parser_context::reset()
|
|
{
|
|
m_buffer.clear();
|
|
m_read_buffer.clear();
|
|
vertex_ids.clear();
|
|
}
|
|
|
|
void assets::obj_loader::parser_context::operator()(lookup_type::const_pointer entry) noexcept
|
|
{
|
|
using obj_loader_error::codes;
|
|
using obj_loader_error::make_error_code;
|
|
namespace fs = std::filesystem;
|
|
|
|
reset();
|
|
|
|
auto path_buffer = fs::path{};
|
|
const auto base_dir = fs::canonical(fs::path(filename).parent_path());
|
|
|
|
// Buffers for storing the vertex component definitions.
|
|
auto& position_buffer = m_read_buffer.positions();
|
|
auto& normal_buffer = m_read_buffer.normals();
|
|
auto& tex_coord_buffer = m_read_buffer.tex_coords();
|
|
|
|
auto& positions = m_buffer.positions();
|
|
auto& normals = m_buffer.normals();
|
|
auto& tex_coords = m_buffer.tex_coords();
|
|
auto& triangles = m_buffer.triangles();
|
|
|
|
const auto& [ filename, id ] = *entry;
|
|
|
|
auto in = std::ifstream{ filename };
|
|
if (not in.is_open())
|
|
{
|
|
ztu::logger::warn("Cannot open obj file %.", filename);
|
|
return;
|
|
}
|
|
|
|
|
|
const auto push_mesh = [&](const bool clear_read_buffer = false)
|
|
{
|
|
if (not triangles.empty())
|
|
{
|
|
ztu::logger::debug("Parsed % positions.", positions.size());
|
|
ztu::logger::debug("Parsed % normals.", normals.size());
|
|
ztu::logger::debug("Parsed % tex_coords.", tex_coords.size());
|
|
ztu::logger::debug("Parsed % triangles.", triangles.size());
|
|
|
|
// Copy buffer into store and keep capacity.
|
|
m_store->insert(id, m_buffer);
|
|
}
|
|
|
|
if (clear_read_buffer)
|
|
{
|
|
m_read_buffer.clear();
|
|
}
|
|
|
|
m_buffer.clear();
|
|
vertex_ids.clear();
|
|
};
|
|
|
|
const auto find_or_push_vertex = [&](const z3d::index_triangle& vertex) -> z3d::vertex_index
|
|
{
|
|
auto indexed_vid = indexed_vertex_type{
|
|
.vertex = vertex,
|
|
.buffer_index = static_cast<z3d::vertex_index>(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 index is out of range, push default constructed value.
|
|
// Not ideal, but better than out of range indices.
|
|
|
|
auto& position = positions.emplace_back();
|
|
if (position_index < position_buffer.size())
|
|
{
|
|
position = position_buffer[position_index];
|
|
}
|
|
|
|
auto& normal = normals.emplace_back();
|
|
if (normal_index < normal_buffer.size())
|
|
{
|
|
normal = normal_buffer[normal_index];
|
|
}
|
|
|
|
auto& tex_coord = tex_coords.emplace_back();
|
|
if (tex_coord_index < tex_coord_buffer.size())
|
|
{
|
|
tex_coord = tex_coord_buffer[tex_coord_index];
|
|
}
|
|
}
|
|
|
|
return id_it->buffer_index;
|
|
};
|
|
|
|
const material_library_data* curr_material_library{};
|
|
|
|
const auto ec = ztu::parse_lines<codes>(
|
|
in,
|
|
pedantic,
|
|
make_line_parser("v ", ztu::is_repeating, [&](const auto& param)
|
|
{
|
|
mesh_vertex_components::position position;
|
|
if (parse_numeric_vector(param, position) != std::errc{}) [[unlikely]]
|
|
{
|
|
return codes::malformed_vertex;
|
|
}
|
|
|
|
position_buffer.push_back(position);
|
|
|
|
return codes::ok;
|
|
}),
|
|
make_line_parser("vt ", ztu::is_repeating, [&](const auto& param)
|
|
{
|
|
mesh_vertex_components::tex_coord coord;
|
|
if (parse_numeric_vector(param, coord) != std::errc{}) [[unlikely]]
|
|
{
|
|
return codes::malformed_texture_coordinate;
|
|
}
|
|
|
|
tex_coord_buffer.push_back(coord);
|
|
|
|
return codes::ok;
|
|
}),
|
|
make_line_parser("vn ", ztu::is_repeating, [&](const auto& param)
|
|
{
|
|
mesh_vertex_components::normal normal;
|
|
if (parse_numeric_vector(param, normal) != std::errc{}) [[unlikely]]
|
|
{
|
|
return codes::malformed_normal;
|
|
}
|
|
|
|
normal_buffer.push_back(normal);
|
|
|
|
return codes::ok;
|
|
}),
|
|
make_line_parser("o ", ztu::is_not_repeating, [&](const auto&)
|
|
{
|
|
push_mesh(); // Name is currently ignored
|
|
return codes::ok;
|
|
}),
|
|
make_line_parser("f ", ztu::is_repeating, [&](const auto& param)
|
|
{
|
|
const auto begin = param.begin().base();
|
|
const auto end = param.end().base();
|
|
|
|
auto vertex = z3d::index_triangle{};
|
|
|
|
z3d::vertex_index 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::malformed_face;
|
|
}
|
|
|
|
--component_index; // Convert to zero based index.
|
|
it = ptr;
|
|
|
|
if (it == end or *it != '/')
|
|
{
|
|
break;
|
|
}
|
|
|
|
++it;
|
|
}
|
|
|
|
++vertex_count;
|
|
|
|
if (it != end and *it != ' ') [[unlikely]]
|
|
{
|
|
return codes::malformed_face;
|
|
}
|
|
|
|
const auto curr_index = find_or_push_vertex(vertex);
|
|
|
|
if (vertex_count >= 3)
|
|
{
|
|
triangles.emplace_back() = {
|
|
first_index,
|
|
prev_index,
|
|
curr_index
|
|
};
|
|
}
|
|
else if (vertex_count == 1)
|
|
{
|
|
first_index = curr_index;
|
|
}
|
|
|
|
prev_index = curr_index;
|
|
}
|
|
|
|
return codes::ok;
|
|
}),
|
|
make_line_parser("usemtl ", ztu::is_not_repeating, [&](const auto& param)
|
|
{
|
|
push_mesh(false);
|
|
|
|
if (not curr_material_library) [[unlikely]]
|
|
{
|
|
return codes::use_material_without_material_library;
|
|
}
|
|
|
|
const auto material_id_it = curr_material_library->find(param);
|
|
|
|
if (material_id_it == curr_material_library->end()) [[unlikely]]
|
|
{
|
|
return codes::unknown_material_name;
|
|
}
|
|
|
|
m_buffer.material() = material_id_it->second;
|
|
|
|
return codes::ok;
|
|
}),
|
|
make_line_parser("mtllib ", ztu::is_not_repeating, [&](const auto& param)
|
|
{
|
|
path_buffer.assign(param);
|
|
|
|
if (path_buffer.is_relative())
|
|
{
|
|
path_buffer = base_dir;
|
|
path_buffer /= param;
|
|
}
|
|
|
|
const auto material_library_id_it = m_id_lookups->material_libraries.find(path_buffer);
|
|
|
|
if (material_library_id_it != m_id_lookups->material_libraries.end()) [[likely]]
|
|
{
|
|
const auto material_library_id = material_library_id_it->second;
|
|
|
|
const auto [ it, found ] = m_stores->material_libraries.find(material_library_id);
|
|
if (found)
|
|
{
|
|
curr_material_library = &(it->second);
|
|
}
|
|
else
|
|
{
|
|
// TODO ALARM!!!
|
|
}
|
|
}
|
|
else [[unlikely]]
|
|
{
|
|
ztu::logger::warn(
|
|
"Could not find a matching material library with path '%'. Proceeding with default material.",
|
|
param
|
|
);
|
|
curr_material_library = nullptr;
|
|
}
|
|
})
|
|
);
|
|
|
|
if (ec != codes::ok)
|
|
{
|
|
const auto e = make_error_code(ec);
|
|
ztu::logger::error("Error while parsing obj file %: %", filename, e.message());
|
|
}
|
|
|
|
push_mesh();
|
|
}
|
|
|
|
void assets::obj_loader::find_materials(
|
|
std::span<char> buffer,
|
|
std::filesystem::path& path_buffer,
|
|
const std::filesystem::path& base_directory,
|
|
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 filename = str.substr(filename_begin, length);
|
|
|
|
path_buffer.assign(filename);
|
|
|
|
if (path_buffer.is_relative())
|
|
{
|
|
path_buffer = base_directory;
|
|
path_buffer /= filename;
|
|
}
|
|
|
|
material_filenames.push_back(path_buffer.c_str());
|
|
|
|
pos = filename_end;
|
|
}
|
|
else // String match exceeds buffer.
|
|
{
|
|
if (pos == 0) [[unlikely]]
|
|
{
|
|
ztu::logger::error("Ignoring string match, as it exceeds buffer size of % characters.", buffer.size());
|
|
leftover = 0;
|
|
}
|
|
else
|
|
{
|
|
leftover = str.size() - pos;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} while (not in.eof());
|
|
}
|
|
|
|
|
|
std::error_code assets::obj_loader::prefetch(
|
|
const file_dir_list& paths,
|
|
prefetch_queue& queue
|
|
) {
|
|
namespace fs = std::filesystem;
|
|
using assets::obj_loader_error::codes;
|
|
using assets::obj_loader_error::make_error_code;
|
|
|
|
auto buffer = std::vector<char>(8 * 1024, '\0');
|
|
|
|
auto in = std::ifstream{};
|
|
|
|
auto path_buffer = fs::path{};
|
|
auto filename_buffer = fs::path{};
|
|
|
|
const auto process_file = [&]()
|
|
{
|
|
in.open(filename_buffer);
|
|
if (not in.is_open()) {
|
|
ztu::logger::error("Could not open .obj file '%'", filename_buffer);
|
|
return;
|
|
}
|
|
|
|
filename_buffer.remove_filename();
|
|
|
|
find_materials(buffer, path_buffer, filename_buffer, in, queue.mtl_queue.files);
|
|
|
|
in.close();
|
|
};
|
|
|
|
|
|
for (const auto file : paths.files)
|
|
{
|
|
filename_buffer.assign(file);
|
|
process_file();
|
|
}
|
|
|
|
for (const auto directory : paths.directories)
|
|
{
|
|
for (const auto& file : fs::directory_iterator{ directory }) {
|
|
|
|
filename_buffer.assign(file.path());
|
|
|
|
// Avoid heap allocation of .extension()
|
|
if (not std::string_view(filename_buffer.c_str()).ends_with(".obj"))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
process_file();
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::error_code assets::obj_loader::load(
|
|
dynamic_mesh_buffer& buffer,
|
|
const file_dir_list& paths,
|
|
prefetch_lookup& id_lookup,
|
|
dynamic_shader_source_store& store,
|
|
bool pedantic
|
|
) {
|
|
namespace fs = std::filesystem;
|
|
|
|
auto position_buffer = buffer.positions();
|
|
auto normal_buffer = buffer.normals();
|
|
auto tex_coord_buffer = buffer.tex_coords();
|
|
|
|
auto read_buffer = dynamic_mesh_buffer{};
|
|
auto path_buffer = fs::path{};
|
|
auto vertex_ids = std::set<indexed_vertex_type>{};
|
|
auto in = std::ifstream{};
|
|
|
|
auto filename_buffer = fs::path{};
|
|
|
|
const auto process_file = [&]()
|
|
{
|
|
in.open(filename_buffer);
|
|
if (not in.is_open()) {
|
|
ztu::logger::error("Could not open .obj file '%'", filename_buffer);
|
|
return;
|
|
}
|
|
|
|
filename_buffer.remove_filename();
|
|
|
|
// parse file
|
|
const auto error = parse_file(
|
|
read_buffer,
|
|
buffer,
|
|
path_buffer,
|
|
filename_buffer,
|
|
vertex_ids,
|
|
in,
|
|
id_lookup,
|
|
store,
|
|
pedantic
|
|
);
|
|
|
|
if (error)
|
|
{
|
|
ztu::logger::error(
|
|
"Error occurred while parsing .obj file: [%] %",
|
|
error.category().name(),
|
|
error.message()
|
|
);
|
|
}
|
|
|
|
in.close();
|
|
};
|
|
|
|
|
|
for (const auto file : paths.files)
|
|
{
|
|
filename_buffer.assign(file);
|
|
process_file();
|
|
}
|
|
|
|
for (const auto directory : paths.directories)
|
|
{
|
|
for (const auto& file : fs::directory_iterator{ directory }) {
|
|
|
|
filename_buffer.assign(file.path());
|
|
|
|
// Avoid heap allocation of .extension()
|
|
if (not std::string_view(filename_buffer.c_str()).ends_with(".obj"))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
process_file();
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
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();
|
|
const auto 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 {};
|
|
};
|