451 lines
11 KiB
C++
Executable File
451 lines
11 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_data::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 {};
|
|
};
|
|
|
|
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<indexed_vertex_type> 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<std::string, ztu::u32> material_name_lookup;
|
|
|
|
|
|
constexpr auto mtl_loader_id = *ctx.material_loader.find_loader_static("mtl");
|
|
mtl_loader& material_loader = ctx.material_loader.get_loader<mtl_loader_id>();
|
|
|
|
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<ztu::u32>(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<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()
|
|
);
|
|
}
|
|
})
|
|
);
|
|
|
|
material_loader.clear_name_lookup();
|
|
|
|
if (ec != codes::ok)
|
|
{
|
|
return make_error_code(ec);
|
|
}
|
|
|
|
push_mesh();
|
|
|
|
return {};
|
|
}
|