You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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;
}

关键部分解释

  1. 跳过规则处理

    • 我们定义的skip规则会自动跳过所有空白字符(空格、制表符、换行),以及所有#开头的注释行,完全符合你的第5、6条需求。
  2. 与STL容器的直接绑定

    • 利用Boost.Phoenix的push_back动作,每个解析出来的数值会直接存入对应的STL向量,无需手动处理中间存储,代码简洁且高效。
    • 比如position_rule中,每个qi::double_解析的结果都会通过phoenix::push_back添加到positions向量,最终positions会以[x1,y1,z1,x2,y2,z2,...]的形式存储所有顶点坐标。
  3. 面记录的解析

    • 面记录的规则(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条需求。
  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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 07:27:38