1
0

initial parser and compiler

This commit is contained in:
2025-07-19 17:30:42 +03:00
parent 27bd44a82b
commit 58a84ade0d
9 changed files with 717 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build/
.vscode/

8
CMakeLists.txt Normal file
View File

@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.16)
project(wavefront_parser LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
file(GLOB SRC_FILES src/*.cpp)
add_executable(wavefront_parser ${SRC_FILES})

15
src/helper.hpp Normal file
View File

@@ -0,0 +1,15 @@
#ifndef APP_HELPER_H
#define APP_HELPER_H
#include <array>
#include <functional>
#include <variant>
template<std::size_t N, typename T>
constexpr std::array<T, N> array_fill(const T& value) {
std::array<T, N> array{};
array.fill(value);
return array;
}
#endif // APP_HELPER_H

139
src/main.cpp Normal file
View File

@@ -0,0 +1,139 @@
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <initializer_list>
#include <iostream>
#include <istream>
#include <limits>
#include <optional>
#include <set>
#include <tuple>
#include <variant>
#include <vector>
#include "wavefront.hpp"
#include "output.hpp"
namespace fs = std::filesystem;
void printUsage() {
std::cout
<< "Usage: parse-wavefront [options] <input.obj>" << std::endl
<< "Options:" << std::endl
<< " --filter-object / -O <name> Filter objects by name (can be used multiple times)" << std::endl
<< " --filter-group / -G <name> Filter groups by name (can be used multiple times)" << std::endl
<< " --output-float-set / -f <file> Output float set to file" << std::endl
<< " --output-position / -p <file> Output positions to file" << std::endl
<< " --output-vertex / -v <file> Output vertices to file" << std::endl
<< " --output-mesh / -m <file> Output mesh to file" << std::endl
<< " --output-normal-file / -n <file> Output normals to file" << std::endl
<< " --output-texcoord-file / -t <file> Output texture coordinates to file" << std::endl
<< " -- Stop processing flags" << std::endl
;
std::cout
<< " <input.obj> Input Wavefront OBJ file" << std::endl
<< "Examples:" << std::endl
<< " parse-wavefront --float64 --output-float-set output.fset --output-position output.pos --filter-object Car,Wheel --filter-group Metal -- model.obj" << std::endl
;
}
[[noreturn]]
void exitWithError(const std::string& message) {
std::cerr << "Error: " << message << std::endl;
std::exit(1);
}
int main(int argc, char *argv[]) {
std::vector<std::string> selected_objects, selected_groups;
bool process_flags = true;
bool has_input_file = false;
bool has_output_float_set_file = false;
bool has_output_position_file = false;
bool has_output_mesh_file = false;
bool has_output_vertex_file = false;
std::string input_file_path;
for (decltype(argc) i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (!process_flags) {
if (has_input_file) {
std::cerr << "Unexpected argument after the input file: " << arg << std::endl;
printUsage();
return 1;
}
has_input_file = true;
input_file_path = arg;
continue;
}
if (arg == "--filter-object" || arg == "-O") {
if (i + 1 < argc) {
selected_groups.push_back(argv[++i]);
}
} else if (arg == "--filter-group" || arg == "-G") {
if (i + 1 < argc) {
selected_groups.push_back(argv[++i]);
}
} else if (arg == "--") {
process_flags = false;
} else if (arg.starts_with("-")) {
std::cerr << "Unknown option: " << arg << std::endl;
printUsage();
return 1;
} else {
input_file_path = arg;
has_input_file = true;
}
}
if (!has_input_file) {
std::cerr << "No input file specified." << std::endl;
printUsage();
return 1;
}
// if (!has_output_float_set_file) {
// std::cerr << "No output float set file specified." << std::endl;
// printUsage();
// return 1;
// }
// if (!has_output_vertex_file) {
// std::cerr << "No output vertex file specified." << std::endl;
// printUsage();
// return 1;
// }
// if (!has_output_mesh_file) {
// std::cerr << "No output mesh file specified." << std::endl;
// printUsage();
// return 1;
// }
// if (!has_output_position_file) {
// std::cerr << "No output position file specified." << std::endl;
// printUsage();
// return 1;
// }
WaveFront::Settings wavefront_settings(
selected_objects.empty() ? std::nullopt : std::make_optional(selected_objects),
selected_groups.empty() ? std::nullopt : std::make_optional(selected_groups)
);
WaveFront wavefront_parser(wavefront_settings);
try {
if (input_file_path == "-") {
wavefront_parser.parse(std::cin);
} else {
std::ifstream input_file(input_file_path);
if (!input_file) {
std::cerr << "Failed to open input file: " << input_file_path << std::endl;
return 1;
}
wavefront_parser.parse(input_file);
}
} catch (const WaveFront::parse_error& e) {
std::cerr << input_file_path << ":" << e.line_number() << ": " << e.what() << std::endl;
return 1;
}
Output output;
output.compile(wavefront_parser);
return 0;
}

70
src/numset.hpp Normal file
View File

@@ -0,0 +1,70 @@
#ifndef APP_NUMSET_H
#define APP_NUMSET_H
#include <cmath>
#include <functional>
#include <limits>
#include <set>
template <typename T>
struct float_is_equal {
bool operator()(T a, T b) const {
static const auto epsilon = std::numeric_limits<T>::epsilon();
static const auto min_value = std::numeric_limits<T>::min();
static const auto max_value = std::numeric_limits<T>::max();
// Nothing is equal to NaN (not even NaN)
if (std::isnan(a) || std::isnan(b)) {
return false;
}
// If both are exactly equal, they are equal, no further checks.
if (a == b) {
return true;
}
auto difference = std::abs(a - b);
auto finite_amplitude = std::min(std::abs(a) + std::abs(b), max_value);
auto min_difference = std::max(min_value, finite_amplitude * epsilon);
return difference < min_difference;
}
};
template <typename T>
struct float_compare_less {
bool operator()(T a, T b) const {
static const auto is_equal_function = float_is_equal<T>();
if (!is_equal_function(a, b)) {
return a < b;
}
return false;
}
};
template <typename T>
struct less_with_nan_first_and_nearly_equal {
bool operator()(T a, T b) const {
static const auto epsilon = std::numeric_limits<T>::epsilon();
static const auto min_value = std::numeric_limits<T>::min();
static const auto max_value = std::numeric_limits<T>::max();
if (std::isnan(a)) {
return !std::isnan(b);
}
if (std::isnan(b)) {
return false;
}
if (a == b) {
return false;
}
auto difference = std::abs(a - b);
auto finite_amplitude = std::min(std::abs(a) + std::abs(b), max_value);
auto min_difference = std::max(min_value, finite_amplitude * epsilon);
if (difference < min_difference) {
return false;
}
return a < b;
}
};
template<typename T>
using numset_t = std::set<T, less_with_nan_first_and_nearly_equal<T>>;
#endif // APP_NUMSET_H

93
src/output.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include <algorithm>
#include <cassert>
#include <functional>
#include <limits>
#include <sstream>
#include "output.hpp"
#include "helper.hpp"
#include "numset.hpp"
Output::Output() {
float_data_.push_back(std::numeric_limits<float>::quiet_NaN());
vector_2d_data_.push_back(array_fill<2, std::uint32_t>(0));
vector_3d_data_.push_back(array_fill<3, std::uint32_t>(0));
triangle_data_.push_back(array_fill<3, uint32_t>(0));
}
void Output::compile(const WaveFront &input) {
compile_float_data(input);
}
static const std::array<std::string, 3> attribute_name = {
"position",
"texture",
"normal"
};
template<std::size_t N>
void ensure_dims(std::size_t num_dimensions, std::uint32_t attribute_index) {
if (num_dimensions != 3) {
std::stringstream message;
message << "Unexpected unaligned " << attribute_name[attribute_index] << " size: expected exactly " << N << " dimensions, got " << num_dimensions;
throw std::runtime_error(message.str());
}
}
void Output::compile_float_data(const WaveFront &input) {
using get_data_reference_func_t = std::function<const std::vector<std::array<float, 3>> &()>;
static const std::array<get_data_reference_func_t, 3> get_data_reference{{
[&]() -> const std::vector<std::array<float, 3>> & {
return input.obj_v();
},
[&]() -> const std::vector<std::array<float, 3>> & {
return input.obj_vt();
},
[&]() -> const std::vector<std::array<float, 3>> & {
return input.obj_vn();
},
}};
std::set<float, less_with_nan_first_and_nearly_equal<float>> float_set;
std::size_t texture_size = 0;
using check_attribute_dimensions_func_t = std::function<void(std::uint32_t, std::uint32_t)>;
std::array<check_attribute_dimensions_func_t, 3> check_attribute_dimensions{{
ensure_dims<3>,
[&](std::uint32_t num_dimensions, std::uint32_t attribute_index) {
texture_size = num_dimensions;
check_attribute_dimensions[1] = [&](std::uint32_t num_dimensions, std::uint32_t attribute_index) {
if (texture_size != num_dimensions) {
std::stringstream message;
message << "Unexpected unaligned texture size: expected exactly " << texture_size << " dimensions, got " << num_dimensions;
throw std::runtime_error(message.str());
}
};
},
ensure_dims<3>,
}};
for (const auto &triangle : input.obj_f()) {
for (const auto &vertex : triangle) {
for (std::uint32_t attribute_index = 0; attribute_index < 3; ++attribute_index) {
const auto &attribute_reference = vertex[attribute_index];
if (attribute_reference == 0) {
continue;
}
const auto &container = get_data_reference[attribute_index]();
assert(attribute_reference < container.size());
std::uint32_t dim_count = 0;
for (const auto &value : container.at(attribute_reference)) {
if (std::isnan(value)) {
break;
}
++dim_count;
float_set.insert(value);
}
assert(dim_count > 0);
check_attribute_dimensions[attribute_index](dim_count, attribute_index);
}
}
}
float_data_.insert(float_data_.end(), float_set.begin(), float_set.end());
int j = 0;
}

32
src/output.hpp Normal file
View File

@@ -0,0 +1,32 @@
#ifndef APP_MODEL_HPP
#define APP_MODEL_HPP
#include <cstdint>
#include <variant>
#include <vector>
#include "wavefront.hpp"
class Output {
std::vector<float> float_data_;
std::vector<std::array<std::uint32_t, 2>> vector_2d_data_;
std::vector<std::array<std::uint32_t, 3>> vector_3d_data_;
std::variant<
uint32_t,
std::array<uint32_t, 2>,
std::array<uint32_t, 3>
> vertex_data_;
std::vector<std::array<uint32_t, 3>> triangle_data_;
public:
Output();
~Output() = default;
private:
void compile_float_data(const WaveFront &input);
public:
void compile(const WaveFront &input);
};
#endif // APP_MODEL_HPP

278
src/wavefront.cpp Normal file
View File

@@ -0,0 +1,278 @@
#include "wavefront.hpp"
#include "helper.hpp"
#include <algorithm>
#include <limits>
WaveFront::Settings::Settings(
std::optional<std::vector<std::string>> selected_objects,
std::optional<std::vector<std::string>> selected_groups
) :
selected_objects_(selected_objects),
selected_groups_(selected_groups)
{
}
WaveFront::parse_error::parse_error(std::size_t line_number, const std::string& message) :
std::runtime_error(message),
line_number_(line_number)
{
}
std::size_t WaveFront::parse_error::line_number() const noexcept {
return line_number_;
}
const std::map<std::string, WaveFront::_parse_line_t> WaveFront::_parse_line_{
{ "v", &WaveFront::parse_v },
{ "vt", &WaveFront::parse_vt },
{ "vn", &WaveFront::parse_vn },
{ "f", &WaveFront::parse_f },
{ "o", &WaveFront::parse_o },
{ "g", &WaveFront::parse_g },
};
WaveFront::WaveFront(
const Settings& settings
) :
settings_(settings),
num_attributes_(0),
num_texture_coordinates_(0),
current_line_(0)
{
obj_v_.push_back(array_fill<3>(std::numeric_limits<float>::quiet_NaN()));
obj_vt_.push_back(array_fill<3>(std::numeric_limits<float>::quiet_NaN()));
obj_vn_.push_back(array_fill<3>(std::numeric_limits<float>::quiet_NaN()));
obj_f_.push_back(array_fill<3>(array_fill<3>(uint32_t(0))));
}
void WaveFront::parse(std::istream &input_stream) {
std::string line;
while(std::getline(input_stream, line)) {
++current_line_;
if (line.empty() || line[0] == '#') {
continue; // Skip empty lines and comments
}
std::istringstream line_parser(line);
std::string token;
std::getline(line_parser, token, ' ');
if (token.empty()) {
continue;
}
auto token_parser_iterator = _parse_line_.find(token);
if (token_parser_iterator != _parse_line_.end()) {
(this->*(token_parser_iterator->second))(line_parser);
}
}
}
void WaveFront::parse_o(std::istringstream &line_parser) {
std::string object_name;
std::getline(line_parser, object_name);
if (object_name.empty()) {
current_object_.reset();
} else {
current_object_ = object_name;
}
}
void WaveFront::parse_g(std::istringstream &line_parser) {
std::string group_name;
std::getline(line_parser, group_name);
if (group_name.empty()) {
current_group_.reset();
} else {
current_group_ = group_name;
}
}
void WaveFront::parse_v(std::istringstream &line_parser) {
std::array<float, 3> data = array_fill<3>(std::numeric_limits<float>::quiet_NaN());
std::string element;
std::size_t element_count = 0;
while (std::getline(line_parser, element, ' ')) {
if (element_count >= 3) {
throw parse_error(current_line_, "Type \"v\" with more than 3 components is not supported.");
}
if (element.empty()) {
break;
}
try {
data[element_count++] = std::stof(element);
} catch (const std::invalid_argument&) {
throw parse_error(current_line_, "Invalid float value in vertex definition.");
} catch (const std::out_of_range&) {
throw parse_error(current_line_, "Float value out of range in vertex definition.");
}
}
if (element_count < 3) {
throw parse_error(current_line_, "Type \"v\" must have exactly 3 components.");
}
obj_v_.push_back(data);
}
void WaveFront::parse_vn(std::istringstream &line_parser) {
std::array<float, 3> data = array_fill<3>(std::numeric_limits<float>::quiet_NaN());
std::string element;
std::size_t element_count = 0;
while (std::getline(line_parser, element, ' ')) {
if (element_count >= 3) {
throw parse_error(current_line_, "Type \"vn\" with more than 3 components is not supported.");
}
if (element.empty()) {
break;
}
try {
data[element_count++] = std::stof(element);
} catch (const std::invalid_argument&) {
throw parse_error(current_line_, "Invalid float value in vertex normal definition.");
} catch (const std::out_of_range&) {
throw parse_error(current_line_, "Float value out of range in vertex normal definition.");
}
}
if (element_count < 3) {
throw parse_error(current_line_, "Type \"vn\" with have exactly 3 components.");
}
obj_vn_.push_back(data);
}
void WaveFront::parse_vt(std::istringstream &line_parser) {
std::array<float, 3> data = array_fill<3>(std::numeric_limits<float>::quiet_NaN());
std::string element;
std::size_t element_count = 0;
while (std::getline(line_parser, element, ' ')) {
if (element_count >= 3) {
throw parse_error(current_line_, "Type \"vt\" with more than 3 components is not supported.");
};
if (element.empty()) {
break;
}
try {
data[element_count++] = std::stof(element);
} catch (const std::invalid_argument&) {
throw parse_error(current_line_, "Invalid float value in texture coordinate definition.");
} catch (const std::out_of_range&) {
throw parse_error(current_line_, "Float value out of range in texture coordinate definition.");
}
}
if (element_count < 1) {
throw parse_error(current_line_, "Type \"vt\" must have at least 1 component.");
}
obj_vt_.push_back(data);
}
void WaveFront::parse_f(std::istringstream &line_parser) {
if (settings_.selected_objects_.has_value()) {
if (!current_object_.has_value()) {
return;
}
auto &selected_objects = settings_.selected_objects_.value();
if (std::find(selected_objects.begin(), selected_objects.end(), current_object_.value()) == selected_objects.end()) {
return;
}
}
if (settings_.selected_groups_) {
if (!current_group_.has_value()) {
return;
}
auto selected_begin = settings_.selected_groups_->begin();
auto selected_end = settings_.selected_groups_->end();
auto current_value = current_group_.value();
if (std::find(selected_begin, selected_end, current_value) == selected_end) {
return;
}
}
std::size_t vertex_count = 0;
std::array<std::array<uint32_t, 3>, 3> triangle;
std::string token;
while (std::getline(line_parser, token, ' ')) {
if (token.empty()) {
break;
}
auto vertex_indices = parse_vertex(token);
if (vertex_count < 3) {
triangle[vertex_count] = vertex_indices;
} else {
obj_f_.push_back(triangle);
triangle[0] = triangle[1];
triangle[1] = triangle[2];
triangle[2] = vertex_indices;
}
++vertex_count;
}
}
std::array<uint32_t, 3> WaveFront::parse_vertex(std::string &token) {
std::istringstream token_parser(token);
std::array<uint32_t, 3> vertex_result{0, 0, 0};
std::string vertex_token;
std::size_t attribute_count = 0;
static const std::array<std::vector<std::array<float, 3>>, 3> data_reference = {obj_v_, obj_vt_, obj_vn_};
while (std::getline(token_parser, vertex_token, '/')) {
if (attribute_count >= 3) {
throw parse_error(current_line_, "Face vertex with more than 3 components is not supported.");
}
int64_t attribute_value;
if (vertex_token.empty()) {
attribute_value = 0;
} else {
try {
attribute_value = std::stoll(vertex_token);
} catch (const std::invalid_argument&) {
std::stringstream message;
message << "Invalid face vertex index: " << vertex_token;
throw parse_error(current_line_, message.str());
} catch (const std::out_of_range&) {
std::stringstream message;
message << "Face vertex index out of range: " << vertex_token;
throw parse_error(current_line_, message.str());
}
}
if (attribute_value < 0) {
attribute_value = data_reference[attribute_count].size() + attribute_value;
if (attribute_value < 1 || attribute_value > data_reference[attribute_count].size()) {
std::stringstream message;
message << "Face vertex index out of range: " << vertex_token;
throw parse_error(current_line_, message.str());
}
} else if (attribute_value >= static_cast<int64_t>(std::numeric_limits<uint32_t>::max())) {
std::stringstream message;
message << "Face vertex index out of range: " << vertex_token;
throw parse_error(current_line_, message.str());
}
vertex_result[attribute_count] = attribute_value;
++attribute_count;
}
return vertex_result;
}
const std::vector<std::array<float, 3>> &WaveFront::obj_v() const noexcept {
return obj_v_;
}
const std::vector<std::array<float, 3>> &WaveFront::obj_vt() const noexcept {
return obj_vt_;
}
const std::vector<std::array<float, 3>> &WaveFront::obj_vn() const noexcept {
return obj_vn_;
}
const std::vector<std::array<std::array<std::uint32_t, 3>, 3>> &WaveFront::obj_f() const noexcept {
return obj_f_;
}

80
src/wavefront.hpp Normal file
View File

@@ -0,0 +1,80 @@
#ifndef APP_WAVEFRONT_HPP
#define APP_WAVEFRONT_HPP
#include <array>
#include <cstdint>
#include <map>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
class WaveFront;
class WaveFront {
public:
enum class AttributeIndex : uint32_t {
POSITION = 0,
TEXTURE = 1,
NORMAL = 2
};
public:
class Settings {
friend class WaveFront;
private:
std::optional<std::vector<std::string>> selected_objects_;
std::optional<std::vector<std::string>> selected_groups_;
public:
Settings() = default;
Settings(
std::optional<std::vector<std::string>> selected_objects,
std::optional<std::vector<std::string>> selected_groups
);
Settings(const Settings&) = default;
Settings(Settings&&) = default;
~Settings() = default;
};
class parse_error : public std::runtime_error {
public:
parse_error(std::size_t line_number, const std::string& message);
std::size_t line_number() const noexcept;
private:
std::size_t line_number_;
};
private:
using _parse_line_t = void (WaveFront::*)(std::istringstream&);
Settings settings_;
std::vector<std::array<float, 3>> obj_v_, obj_vt_, obj_vn_;
std::vector<std::array<std::array<std::uint32_t, 3>, 3>> obj_f_;
std::size_t num_attributes_, num_texture_coordinates_, current_line_;
std::optional<std::string> current_object_, current_group_;
static const std::map<std::string, _parse_line_t> _parse_line_;
private:
void parse_o(std::istringstream &line_parser);
void parse_g(std::istringstream &line_parser);
void parse_v(std::istringstream &line_parser);
void parse_vn(std::istringstream &line_parser);
void parse_vt(std::istringstream &line_parser);
void parse_f(std::istringstream &line_parser);
std::array<uint32_t, 3> parse_vertex(std::string &line_parser);
public:
const std::vector<std::array<float, 3>> &obj_v() const noexcept;
const std::vector<std::array<float, 3>> &obj_vt() const noexcept;
const std::vector<std::array<float, 3>> &obj_vn() const noexcept;
const std::vector<std::array<std::array<std::uint32_t, 3>, 3>> &obj_f() const noexcept;
public:
explicit WaveFront(const Settings &settings = Settings(std::nullopt, std::nullopt));
~WaveFront() = default;
void parse(std::istream &input_string);
};
#endif // APP_WAVEFRONT_HPP