使用 Boost.Spirit 解析具有混合数据类型的 OBJ 文件?
Parse OBJ file with mixed data-types using Boost.Spirit?
我有一个 OBJ 文件,如下所示:
# This file uses centimeters as units for non-parametric coordinates.
# first element
v -0.017050 -0.017928 0.005579
v -0.014504 -0.017928 0.010577
.
.
v -0.000000 -0.017928 0.017967
vt 0.397581 0.004762
vt 0.397544 0.034263
.
.
vt 0.397507 0.063764
vn -0.951057 0.000000 0.309016
vn -0.809017 0.000000 0.587785
.
.
vn -0.587785 0.000000 0.809017
f 1/1/1 2/2/2 21/4/3
f 21/4/3 2/2/2 22/3/4
.
.
f 3/5/5 4/7/7 23/6/6
# second element
v -0.014504 0.017928 -0.010499
v -0.017050 0.017928 -0.005501
.
.
v -0.000000 0.017928 0.000039
vt 0.063001 0.262615
vt 0.073837 0.268136
.
.
vt 0.089861 0.299584
vn 0.000000 1.000000 -0.000002
vn 0.000000 1.000000 -0.000002
.
.
vn 0.000000 1.000000 -0.000000
f 36/80/78 37/81/79 42/66/64
f 37/81/79 38/82/80 42/66/64
.
.
f 40/84/82 21/64/62 42/66/64
# third element
.
.
etc.
该文件包含四种不同类型的记录,每种记录都以 v
、vt
、vn
或 f
开头。
我想知道如何在以下情况下使用 boost-spirit 来解析这样的文件:
int main(int argc, char **argv) {
std::vector<double> positions;
std::vector<double> texcoords;
std::vector<double> normals;
std::vector<uint32_t> faces;
// 1- store each record begins with 'v' into 'positions'
// 2- store each record begins with 'vt' into 'texcoords'
// 3- store each record begins with 'vn' into 'normals'
// 4- store each record begins with 'f' into 'faces'. The integers are separated either with '/' or a blank-space (Each record with three groups, each group with three integers)
// 5- ignore each record begins with '#'
// 6- ignoring all empty lines
return 0;
}
使用 boost-spirit 最有效的方法是什么?
好的,所以我开始在 Spirit X3 中对此进行阐述,以进行更改。
AST
一如既往,要事第一。我认为我们可以对数据结构更精确一些以匹配数据本身。
这将使它更有用并且更不容易出错。
using Position = std::tuple<double, double, double>;
using Coords = std::tuple<double, double>;
using Normal = std::tuple<double, double, double>;
//using Face3 = std::array<uint32_t, 3>;
//using Face9 = std::array<Face3, 3>; // 9
using Face9 = std::vector<uint32_t>; // hmmm
struct Element {
std::vector<Position> positions;
std::vector<Coords> texcoords;
std::vector<Normal> normals;
std::vector<Face9> faces;
};
struct OBJ {
std::vector<Element> elements;
};
I'd have loved to use std::array
for fixed-length sequences (Face3/Face9) but coudln't readily make it work, so here I opted for the more flexible vector
like you did.
解析器
这是一个 1:1 映射:
// 1- store each record begins with 'v' into 'positions'
auto pos
= rule<struct _pos, Position> {"pos"}
= "v" >> double_ >> double_ >> double_;
// 2- store each record begins with 'vt' into 'texcoords'
auto tex
= rule<struct _tex, Coords> {"tex"}
= "vt" >> double_ >> double_;
// 3- store each record begins with 'vn' into 'normals'
auto normals
= rule<struct _norm, Normal> {"normal"}
= "vn" >> double_ >> double_ >> double_;
// 4- store each record begins with 'f' into 'faces'.
// The integers are separated either with '/' or a blank-space (Each
// record with three groups, each group with three integers)
auto face3
= rule<struct _face3, Face9> {"face3"}
= uint_ >> '/' >> uint_ >> '/' >> uint_;
auto faces
= rule<struct _faces, Face9> {"faces"}
= "f" >> repeat(3) [ face3 ];
我们不关心 comment/whitespace,因为我们将使用船长:
// 5- ignore each record begins with '#'
// 6- ignoring all empty lines
auto skipper = blank | '#' >> *(char_ - eol) >> (eol|eoi);
现在,让我们跳到最后阶段:主要解析器规则:
auto OBJ = *skip(skipper) [
*eol >> &pos [ new_element ] >>
lines_of(pos, &Element::positions) >>
lines_of(tex, &Element::texcoords) >>
lines_of(normals, &Element::normals) >>
lines_of(faces, &Element::faces)
];
如您所见,我再次假设了更多限制(因为示例文件中的注释表明出现了单独的元素,并且各部分按顺序出现)。
为了解析重复的行,我们有这个工厂:
auto lines_of = [](auto p, auto member) {
return *(p [ push(member) ] >> (eoi|+eol));
};
push
动作是按功能生成的:
auto push = [](auto member) {
return [member](auto& ctx) {
auto& data = get<OBJ>(ctx);
auto& v = std::invoke(member, data.elements.back());
v.push_back(std::move(_attr(ctx)));
};
};
We'll be getting the OBJ
object from the parser context
使用 OBJ 上下文调用解析器
main
代码如下所示:
#include <iostream>
#include <iomanip>
int main() {
std::ifstream ifs("input.txt");
boost::spirit::istream_iterator f(ifs >> std::noskipws), l;
OBJ data;
if (x3::parse(f, l, x3::with<OBJ>(data) [ Parser::OBJ ])) {
std::cout << "Yay, " << data.elements.size() << " elements\n";
for (auto& el : data.elements) {
dump(el);
}
} else {
std::cout << "Failed\n";
}
if (f!=l) {
std::cout << "Remaining unparsed: " << std::quoted(std::string(f,l)) << "\n";
}
}
就是这样。对于给定的输入,打印:
Yay, 2 elements
奖励:调试输出
如果你有 {fmt}
可用,使用 -DHAVE_FMT
编译以获得漂亮的输出:
#ifdef HAVE_FMT
#include <fmt/printf.h>
#include <fmt/ranges.h>
void dump(Element const& el) {
auto& [pos,tex,nrm,fac] = el;
fmt::print("positions: {}\n"
"texcoords: {}\n"
"normals: {}\n"
"faces: {}\n\n", pos, tex, nrm, fac);
}
#else
void dump(Element const&) { }
#endif
打印:
Yay, 2 elements
positions: {(-0.01705, -0.017928, 0.005579), (-0.014504, -0.017928, 0.010577), (-0.0, -0.017928, 0.017967)}
texcoords: {(0.397581, 0.004762), (0.397544, 0.034263), (0.397507, 0.063764)}
normals: {(-0.951057, 0.0, 0.309016), (-0.809017, 0.0, 0.587785), (-0.587785, 0.0, 0.809017)}
faces: {{1, 1, 1, 2, 2, 2, 21, 4, 3}, {21, 4, 3, 2, 2, 2, 22, 3, 4}, {3, 5, 5, 4, 7, 7, 23, 6, 6}}
positions: {(-0.014504, 0.017928, -0.010499), (-0.01705, 0.017928, -0.005501), (-0.0, 0.017928, 3.9e-05)}
texcoords: {(0.063001, 0.262615), (0.073837, 0.268136), (0.089861, 0.299584)}
normals: {(0.0, 1.0, -2e-06), (0.0, 1.0, -2e-06), (0.0, 1.0, -0.0)}
faces: {{36, 80, 78, 37, 81, 79, 42, 66, 64}, {37, 81, 79, 38, 82, 80, 42, 66, 64}, {40, 84, 82, 21, 64, 62, 42, 66, 64}}
完整列表
//#define HAVE_FMT
//#define BOOST_SPIRIT_X3_DEBUG
#include <boost/fusion/adapted/std_tuple.hpp>
#include <boost/fusion/adapted/std_array.hpp>
#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/include/support_istream_iterator.hpp>
#include <fstream>
using Position = std::tuple<double, double, double>;
using Coords = std::tuple<double, double>;
using Normal = std::tuple<double, double, double>;
//using Face3 = std::array<uint32_t, 3>;
//using Face9 = std::array<Face3, 3>; // 9
using Face9 = std::vector<uint32_t>; // hmmm
struct Element {
std::vector<Position> positions;
std::vector<Coords> texcoords;
std::vector<Normal> normals;
std::vector<Face9> faces;
};
struct OBJ {
std::vector<Element> elements;
};
namespace x3 = boost::spirit::x3;
namespace Parser {
using namespace x3;
auto new_element = [](auto& ctx) {
auto& data = get<OBJ>(ctx);
data.elements.emplace_back();
};
// 1- store each record begins with 'v' into 'positions'
auto pos
= rule<struct _pos, Position> {"pos"}
= "v" >> double_ >> double_ >> double_;
// 2- store each record begins with 'vt' into 'texcoords'
auto tex
= rule<struct _tex, Coords> {"tex"}
= "vt" >> double_ >> double_;
// 3- store each record begins with 'vn' into 'normals'
auto normals
= rule<struct _norm, Normal> {"normal"}
= "vn" >> double_ >> double_ >> double_;
// 4- store each record begins with 'f' into 'faces'.
// The integers are separated either with '/' or a blank-space (Each
// record with three groups, each group with three integers)
auto face3
= rule<struct _face3, Face9> {"face3"}
= uint_ >> '/' >> uint_ >> '/' >> uint_;
auto faces
= rule<struct _faces, Face9> {"faces"}
= "f" >> repeat(3) [ face3 ];
// 5- ignore each record begins with '#'
// 6- ignoring all empty lines
auto skipper = blank | '#' >> *(char_ - eol) >> (eol|eoi);
auto push = [](auto member) {
return [member](auto& ctx) {
auto& data = get<OBJ>(ctx);
auto& v = std::invoke(member, data.elements.back());
v.push_back(std::move(_attr(ctx)));
};
};
auto lines_of = [](auto p, auto member) {
return *(p [ push(member) ] >> (eoi|+eol));
};
auto OBJ = *skip(skipper) [
*eol >> &pos [ new_element ] >>
lines_of(pos, &Element::positions) >>
lines_of(tex, &Element::texcoords) >>
lines_of(normals, &Element::normals) >>
lines_of(faces, &Element::faces)
];
}
#ifdef HAVE_FMT
#include <fmt/printf.h>
#include <fmt/ranges.h>
void dump(Element const& el) {
auto& [pos,tex,nrm,fac] = el;
fmt::print("positions: {}\n"
"texcoords: {}\n"
"normals: {}\n"
"faces: {}\n\n", pos, tex, nrm, fac);
}
#else
void dump(Element const&) { }
#endif
#include <iostream>
#include <iomanip>
int main() {
std::ifstream ifs("input.txt");
boost::spirit::istream_iterator f(ifs >> std::noskipws), l;
OBJ data;
if (x3::parse(f, l, x3::with<OBJ>(data) [ Parser::OBJ ])) {
std::cout << "Yay, " << data.elements.size() << " elements\n";
for (auto& el : data.elements) {
dump(el);
}
} else {
std::cout << "Failed\n";
}
if (f!=l) {
std::cout << "Remaining unparsed: " << std::quoted(std::string(f,l)) << "\n";
}
}
我有一个 OBJ 文件,如下所示:
# This file uses centimeters as units for non-parametric coordinates.
# first element
v -0.017050 -0.017928 0.005579
v -0.014504 -0.017928 0.010577
.
.
v -0.000000 -0.017928 0.017967
vt 0.397581 0.004762
vt 0.397544 0.034263
.
.
vt 0.397507 0.063764
vn -0.951057 0.000000 0.309016
vn -0.809017 0.000000 0.587785
.
.
vn -0.587785 0.000000 0.809017
f 1/1/1 2/2/2 21/4/3
f 21/4/3 2/2/2 22/3/4
.
.
f 3/5/5 4/7/7 23/6/6
# second element
v -0.014504 0.017928 -0.010499
v -0.017050 0.017928 -0.005501
.
.
v -0.000000 0.017928 0.000039
vt 0.063001 0.262615
vt 0.073837 0.268136
.
.
vt 0.089861 0.299584
vn 0.000000 1.000000 -0.000002
vn 0.000000 1.000000 -0.000002
.
.
vn 0.000000 1.000000 -0.000000
f 36/80/78 37/81/79 42/66/64
f 37/81/79 38/82/80 42/66/64
.
.
f 40/84/82 21/64/62 42/66/64
# third element
.
.
etc.
该文件包含四种不同类型的记录,每种记录都以 v
、vt
、vn
或 f
开头。
我想知道如何在以下情况下使用 boost-spirit 来解析这样的文件:
int main(int argc, char **argv) {
std::vector<double> positions;
std::vector<double> texcoords;
std::vector<double> normals;
std::vector<uint32_t> faces;
// 1- store each record begins with 'v' into 'positions'
// 2- store each record begins with 'vt' into 'texcoords'
// 3- store each record begins with 'vn' into 'normals'
// 4- store each record begins with 'f' into 'faces'. The integers are separated either with '/' or a blank-space (Each record with three groups, each group with three integers)
// 5- ignore each record begins with '#'
// 6- ignoring all empty lines
return 0;
}
使用 boost-spirit 最有效的方法是什么?
好的,所以我开始在 Spirit X3 中对此进行阐述,以进行更改。
AST
一如既往,要事第一。我认为我们可以对数据结构更精确一些以匹配数据本身。
这将使它更有用并且更不容易出错。
using Position = std::tuple<double, double, double>;
using Coords = std::tuple<double, double>;
using Normal = std::tuple<double, double, double>;
//using Face3 = std::array<uint32_t, 3>;
//using Face9 = std::array<Face3, 3>; // 9
using Face9 = std::vector<uint32_t>; // hmmm
struct Element {
std::vector<Position> positions;
std::vector<Coords> texcoords;
std::vector<Normal> normals;
std::vector<Face9> faces;
};
struct OBJ {
std::vector<Element> elements;
};
I'd have loved to use
std::array
for fixed-length sequences (Face3/Face9) but coudln't readily make it work, so here I opted for the more flexiblevector
like you did.
解析器
这是一个 1:1 映射:
// 1- store each record begins with 'v' into 'positions'
auto pos
= rule<struct _pos, Position> {"pos"}
= "v" >> double_ >> double_ >> double_;
// 2- store each record begins with 'vt' into 'texcoords'
auto tex
= rule<struct _tex, Coords> {"tex"}
= "vt" >> double_ >> double_;
// 3- store each record begins with 'vn' into 'normals'
auto normals
= rule<struct _norm, Normal> {"normal"}
= "vn" >> double_ >> double_ >> double_;
// 4- store each record begins with 'f' into 'faces'.
// The integers are separated either with '/' or a blank-space (Each
// record with three groups, each group with three integers)
auto face3
= rule<struct _face3, Face9> {"face3"}
= uint_ >> '/' >> uint_ >> '/' >> uint_;
auto faces
= rule<struct _faces, Face9> {"faces"}
= "f" >> repeat(3) [ face3 ];
我们不关心 comment/whitespace,因为我们将使用船长:
// 5- ignore each record begins with '#'
// 6- ignoring all empty lines
auto skipper = blank | '#' >> *(char_ - eol) >> (eol|eoi);
现在,让我们跳到最后阶段:主要解析器规则:
auto OBJ = *skip(skipper) [
*eol >> &pos [ new_element ] >>
lines_of(pos, &Element::positions) >>
lines_of(tex, &Element::texcoords) >>
lines_of(normals, &Element::normals) >>
lines_of(faces, &Element::faces)
];
如您所见,我再次假设了更多限制(因为示例文件中的注释表明出现了单独的元素,并且各部分按顺序出现)。
为了解析重复的行,我们有这个工厂:
auto lines_of = [](auto p, auto member) {
return *(p [ push(member) ] >> (eoi|+eol));
};
push
动作是按功能生成的:
auto push = [](auto member) {
return [member](auto& ctx) {
auto& data = get<OBJ>(ctx);
auto& v = std::invoke(member, data.elements.back());
v.push_back(std::move(_attr(ctx)));
};
};
We'll be getting the
OBJ
object from the parser context
使用 OBJ 上下文调用解析器
main
代码如下所示:
#include <iostream>
#include <iomanip>
int main() {
std::ifstream ifs("input.txt");
boost::spirit::istream_iterator f(ifs >> std::noskipws), l;
OBJ data;
if (x3::parse(f, l, x3::with<OBJ>(data) [ Parser::OBJ ])) {
std::cout << "Yay, " << data.elements.size() << " elements\n";
for (auto& el : data.elements) {
dump(el);
}
} else {
std::cout << "Failed\n";
}
if (f!=l) {
std::cout << "Remaining unparsed: " << std::quoted(std::string(f,l)) << "\n";
}
}
就是这样。对于给定的输入,打印:
Yay, 2 elements
奖励:调试输出
如果你有 {fmt}
可用,使用 -DHAVE_FMT
编译以获得漂亮的输出:
#ifdef HAVE_FMT
#include <fmt/printf.h>
#include <fmt/ranges.h>
void dump(Element const& el) {
auto& [pos,tex,nrm,fac] = el;
fmt::print("positions: {}\n"
"texcoords: {}\n"
"normals: {}\n"
"faces: {}\n\n", pos, tex, nrm, fac);
}
#else
void dump(Element const&) { }
#endif
打印:
Yay, 2 elements
positions: {(-0.01705, -0.017928, 0.005579), (-0.014504, -0.017928, 0.010577), (-0.0, -0.017928, 0.017967)}
texcoords: {(0.397581, 0.004762), (0.397544, 0.034263), (0.397507, 0.063764)}
normals: {(-0.951057, 0.0, 0.309016), (-0.809017, 0.0, 0.587785), (-0.587785, 0.0, 0.809017)}
faces: {{1, 1, 1, 2, 2, 2, 21, 4, 3}, {21, 4, 3, 2, 2, 2, 22, 3, 4}, {3, 5, 5, 4, 7, 7, 23, 6, 6}}
positions: {(-0.014504, 0.017928, -0.010499), (-0.01705, 0.017928, -0.005501), (-0.0, 0.017928, 3.9e-05)}
texcoords: {(0.063001, 0.262615), (0.073837, 0.268136), (0.089861, 0.299584)}
normals: {(0.0, 1.0, -2e-06), (0.0, 1.0, -2e-06), (0.0, 1.0, -0.0)}
faces: {{36, 80, 78, 37, 81, 79, 42, 66, 64}, {37, 81, 79, 38, 82, 80, 42, 66, 64}, {40, 84, 82, 21, 64, 62, 42, 66, 64}}
完整列表
//#define HAVE_FMT
//#define BOOST_SPIRIT_X3_DEBUG
#include <boost/fusion/adapted/std_tuple.hpp>
#include <boost/fusion/adapted/std_array.hpp>
#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/include/support_istream_iterator.hpp>
#include <fstream>
using Position = std::tuple<double, double, double>;
using Coords = std::tuple<double, double>;
using Normal = std::tuple<double, double, double>;
//using Face3 = std::array<uint32_t, 3>;
//using Face9 = std::array<Face3, 3>; // 9
using Face9 = std::vector<uint32_t>; // hmmm
struct Element {
std::vector<Position> positions;
std::vector<Coords> texcoords;
std::vector<Normal> normals;
std::vector<Face9> faces;
};
struct OBJ {
std::vector<Element> elements;
};
namespace x3 = boost::spirit::x3;
namespace Parser {
using namespace x3;
auto new_element = [](auto& ctx) {
auto& data = get<OBJ>(ctx);
data.elements.emplace_back();
};
// 1- store each record begins with 'v' into 'positions'
auto pos
= rule<struct _pos, Position> {"pos"}
= "v" >> double_ >> double_ >> double_;
// 2- store each record begins with 'vt' into 'texcoords'
auto tex
= rule<struct _tex, Coords> {"tex"}
= "vt" >> double_ >> double_;
// 3- store each record begins with 'vn' into 'normals'
auto normals
= rule<struct _norm, Normal> {"normal"}
= "vn" >> double_ >> double_ >> double_;
// 4- store each record begins with 'f' into 'faces'.
// The integers are separated either with '/' or a blank-space (Each
// record with three groups, each group with three integers)
auto face3
= rule<struct _face3, Face9> {"face3"}
= uint_ >> '/' >> uint_ >> '/' >> uint_;
auto faces
= rule<struct _faces, Face9> {"faces"}
= "f" >> repeat(3) [ face3 ];
// 5- ignore each record begins with '#'
// 6- ignoring all empty lines
auto skipper = blank | '#' >> *(char_ - eol) >> (eol|eoi);
auto push = [](auto member) {
return [member](auto& ctx) {
auto& data = get<OBJ>(ctx);
auto& v = std::invoke(member, data.elements.back());
v.push_back(std::move(_attr(ctx)));
};
};
auto lines_of = [](auto p, auto member) {
return *(p [ push(member) ] >> (eoi|+eol));
};
auto OBJ = *skip(skipper) [
*eol >> &pos [ new_element ] >>
lines_of(pos, &Element::positions) >>
lines_of(tex, &Element::texcoords) >>
lines_of(normals, &Element::normals) >>
lines_of(faces, &Element::faces)
];
}
#ifdef HAVE_FMT
#include <fmt/printf.h>
#include <fmt/ranges.h>
void dump(Element const& el) {
auto& [pos,tex,nrm,fac] = el;
fmt::print("positions: {}\n"
"texcoords: {}\n"
"normals: {}\n"
"faces: {}\n\n", pos, tex, nrm, fac);
}
#else
void dump(Element const&) { }
#endif
#include <iostream>
#include <iomanip>
int main() {
std::ifstream ifs("input.txt");
boost::spirit::istream_iterator f(ifs >> std::noskipws), l;
OBJ data;
if (x3::parse(f, l, x3::with<OBJ>(data) [ Parser::OBJ ])) {
std::cout << "Yay, " << data.elements.size() << " elements\n";
for (auto& el : data.elements) {
dump(el);
}
} else {
std::cout << "Failed\n";
}
if (f!=l) {
std::cout << "Remaining unparsed: " << std::quoted(std::string(f,l)) << "\n";
}
}