如何用Boost.Spirit解析混合数据类型的OBJ文件?
使用Boost.Spirit解析OBJ文件的高效实现方案
我经常用Boost.Spirit处理这类结构化文本解析,针对你的OBJ文件需求,最高效的方式是利用Spirit Qi的声明式规则定义,直接和STL容器对接,几乎不需要额外的中间数据结构。它不仅代码简洁,性能也能媲美手写的解析器,非常适合处理这类规则明确的文本格式。
完整实现代码
#include <iostream> #include <fstream> #include <vector> #include <cstdint> #include <boost/spirit/include/qi.hpp> #include <boost/spirit/include/phoenix.hpp> namespace qi = boost::spirit::qi; namespace phoenix = boost::phoenix; int main(int argc, char **argv) { std::vector<double> positions; std::vector<double> texcoords; std::vector<double> normals; std::vector<uint32_t> faces; // 检查输入参数 if (argc != 2) { std::cerr << "Usage: " << argv[0] << " <obj_file_path>" << std::endl; return 1; } // 读取文件内容 std::ifstream file(argv[1]); if (!file.is_open()) { std::cerr << "Failed to open file: " << argv[1] << std::endl; return 1; } file.unsetf(std::ios::skipws); std::string content{std::istream_iterator<char>(file), std::istream_iterator<char>()}; // 定义跳过规则:空白字符 + #开头的注释(整行) auto skip = qi::space | (qi::lit("#") >> *(qi::char_ - qi::eol) >> qi::eol); // 定义各个OBJ记录的解析规则 qi::rule<std::string::iterator, void(), decltype(skip)> position_rule, texcoord_rule, normal_rule, face_rule; // 解析v记录:三个double,依次存入positions position_rule = qi::lit("v") >> qi::double_[phoenix::push_back(phoenix::ref(positions), qi::_1)] >> qi::double_[phoenix::push_back(phoenix::ref(positions), qi::_1)] >> qi::double_[phoenix::push_back(phoenix::ref(positions), qi::_1)]; // 解析vt记录:两个double,依次存入texcoords texcoord_rule = qi::lit("vt") >> qi::double_[phoenix::push_back(phoenix::ref(texcoords), qi::_1)] >> qi::double_[phoenix::push_back(phoenix::ref(texcoords), qi::_1)]; // 解析vn记录:三个double,依次存入normals normal_rule = qi::lit("vn") >> qi::double_[phoenix::push_back(phoenix::ref(normals), qi::_1)] >> qi::double_[phoenix::push_back(phoenix::ref(normals), qi::_1)] >> qi::double_[phoenix::push_back(phoenix::ref(normals), qi::_1)]; // 解析f记录:每组三个uint用/分隔,多组用空格分隔,所有uint依次存入faces face_rule = qi::lit("f") >> (qi::uint_[phoenix::push_back(phoenix::ref(faces), qi::_1)] >> '/' >> qi::uint_[phoenix::push_back(phoenix::ref(faces), qi::_1)] >> '/' >> qi::uint_[phoenix::push_back(phoenix::ref(faces), qi::_1)]) % qi::space; // 主解析规则:重复解析任意数量的合法记录 qi::rule<std::string::iterator, void(), decltype(skip)> main_rule = *(position_rule | texcoord_rule | normal_rule | face_rule); // 执行解析 auto it = content.begin(); bool success = qi::phrase_parse(it, content.end(), main_rule, skip); // 检查是否完全解析 if (success && it == content.end()) { std::cout << "解析成功!" << std::endl; std::cout << "顶点数: " << positions.size() / 3 << std::endl; std::cout << "纹理坐标数: " << texcoords.size() / 2 << std::endl; std::cout << "法向量数: " << normals.size() / 3 << std::endl; std::cout << "面索引数: " << faces.size() << std::endl; } else { std::cerr << "解析失败,剩余未解析内容从这里开始: " << std::string(it, content.end()) << std::endl; return 1; } return 0; }
关键部分解释
跳过规则处理
- 我们定义的
skip规则会自动跳过所有空白字符(空格、制表符、换行),以及所有#开头的注释行,完全符合你的第5、6条需求。
- 我们定义的
与STL容器的直接绑定
- 利用Boost.Phoenix的
push_back动作,每个解析出来的数值会直接存入对应的STL向量,无需手动处理中间存储,代码简洁且高效。 - 比如
position_rule中,每个qi::double_解析的结果都会通过phoenix::push_back添加到positions向量,最终positions会以[x1,y1,z1,x2,y2,z2,...]的形式存储所有顶点坐标。
- 利用Boost.Phoenix的
面记录的解析
- 面记录的规则
(uint_ >> '/' >> uint_ >> '/' >> uint_) % space表示:重复解析多组“整数/整数/整数”的结构,组与组之间用空格分隔。 - 每组的三个整数会依次存入
faces向量,比如f 1/1/1 2/2/2 21/4/3会被解析成[1,1,1,2,2,2,21,4,3],完全匹配你的第4条需求。
- 面记录的规则
完整解析检查
- 解析完成后,我们会检查迭代器是否到达文件末尾,确保整个文件都被正确解析,没有残留的非法内容。
注意事项
- OBJ文件的索引是从1开始的,如果你的渲染系统需要0-based索引,可以在解析
uint_的时候添加一个减1的动作,比如把qi::uint_改成(qi::uint_ - 1),这样存入faces的就是0-based索引了。 - 代码使用了Boost.Phoenix来处理动作,这是Spirit的标准配套库,确保了解析逻辑和动作的无缝集成。
- 编译时需要链接Boost的spirit和phoenix模块,比如使用GCC的话,编译命令可以是:
g++ -std=c++17 obj_parser.cpp -o obj_parser -lboost_system -lboost_filesystem(根据你的Boost版本可能需要调整链接参数)。
内容的提问来源于stack exchange,提问作者Bobo Feugo
相关产品推荐
相关产品推荐

