如何改进我的文件写入方法以减小 Wavefront 对象文件的大小?
How can I improve my file writing method to reduce Wavefront Object file size?
我正在尝试写出 voxelization of a model to a Wavefront Object File。
我的方法很简单,而且运行时间合理。问题是——它生成的 OBJ 文件大小过大。我试图将一个 1 GB 的文件加载到 3D Viewer 上一台非常受人尊敬的带有 SSD 的机器上,但在某些情况下,尝试移动相机时会延迟几秒钟,而在其他情况下,它根本拒绝做任何事情并有效地软锁定.
到目前为止我做了什么:
- 我不会写出模型内部的任何面孔 - 也就是说,两个体素之间的面孔都将被写入文件。没有意义,因为没人能看到它们。
- 因为 OBJ 没有广泛支持的二进制格式(据我所知),我发现我可以通过修剪文件中顶点位置的尾随零来节省一些 space。
明显的space-保存我不知道怎么做:
- 没有写出重复的顶点。总的来说,文件中的顶点比应有的多大约 8 倍。然而,解决这个问题非常棘手,因为 Wavefront 对象文件中的对象不使用每个对象,而是 global 顶点。通过每次写出所有 8 个顶点,我总是知道下一个体素由哪 8 个顶点组成。如果我没有写出所有 8 个,我如何跟踪全局列表中的哪个位置我可以找到这 8 个(如果有的话)。
更难,但可能有用的大 space-保存:
- 如果我可以更抽象地工作,可能会有一种方法可以将体素组合成更少的对象,或者将位于同一平面上的面组合成更大的面。 IE,如果两个体素的正面都处于活动状态,则将其变成一个大两倍的矩形。
因为这是必需的,所以这里有一些代码可以粗略地显示正在发生的事情。这不是实际使用的代码。我不能 post 那个,它依赖于许多用户定义的类型,并且有很多代码来处理边缘情况或额外的功能,所以不管怎样放在这里会很混乱和冗长。
对这个问题唯一重要的是我的方法——逐个体素,写出所有 8 个顶点,然后写出 6 个边中不与活动体素相邻的那个。尽管它确实会生成大文件,但您必须相信我它是有效的。
我的问题是我可以使用什么方法或方法来进一步减小尺寸。例如,我怎样才能不写出任何重复的顶点?
假设:
Point
只是一个大小为 3 的数组,带有像 .x() 这样的吸气剂
Vector3D
是围绕 std::vector
的 3D 包装器,具有 .at(x,y,z)
方法
- 哪些体素处于活动状态是任意的,不遵循某种模式,但在调用
writeObj
之前已知。可以快速获取体素是否在任何位置处于活动状态。
//Left, right, bottom, top, front, rear
static const std::vector<std::vector<uint8_t>> quads = {
{3, 0, 4, 7}, {1, 2, 6, 5}, {3, 2, 1, 0},
{4, 5, 6, 7}, {0, 1, 5, 4}, {2, 3, 7, 6}};
void writeOBJ(
std::string folder,
const std::string& filename,
const Vector3D<Voxel>& voxels,
const Point<unsigned> gridDim,
const Point<unsigned>& voxelCenterMinpoint,
const float voxelWidth)
{
unsigned numTris = 0;
std::ofstream filestream;
std::string filepath;
std::string extension;
ulong numVerticesWritten = 0;
// Make sure the folder ends with a '/'
if (folder.back() != '/')
{
folder.append("/");
}
filepath = folder + filename + ".obj";
filestream.open(filepath, std::ios::out);
// Remove the voxelization file if it already exists
std::remove(filepath.c_str());
Point<unsigned> voxelPos;
for (voxelPos[0] = 0; voxelPos[0] < gridDim.x(); voxelPos[0]++)
{
for (voxelPos[1] = 0; voxelPos[1] < gridDim.y(); voxelPos[1]++)
{
for (voxelPos[2] = 0; voxelPos[2] < gridDim.z(); voxelPos[2]++)
{
if (voxels.at(voxelPos))
{
writeVoxelToOBJ(
filestream, voxels, voxelPos, voxelCenterMinpoint, voxelWidth,
numVerticesWritten);
}
}
}
}
filestream.close();
}
void writeVoxelToOBJ(
std::ofstream& filestream,
const Vector3D<Voxel>& voxels,
const Point<unsigned>& voxelPos,
const Point<unsigned>& voxelCenterMinpoint,
const float voxelWidth,
ulong& numVerticesWritten)
{
std::vector<bool> neighborDrawable(6);
std::vector<Vecutils::Point<float>> corners(8);
unsigned numNeighborsDrawable = 0;
// Determine which neighbors are active and what the 8 corners of the
// voxel are
writeVoxelAux(
voxelPos, voxelCenterMinpoint, voxelWidth, neighborDrawable,
numNeighborsDrawable, corners);
// Normally, if all neighbors are active, there is no reason to write out this
// voxel. (All its faces are internal) If inverted, the opposite is true.
if (numNeighborsDrawable == 6)
{
return;
}
// Write out the vertices
for (const Vecutils::Point<float>& corner : corners)
{
std::string x = std::to_string(corner.x());
std::string y = std::to_string(corner.y());
std::string z = std::to_string(corner.z());
// Strip trailing zeros, they serve no prupose and bloat filesize
x.erase(x.find_last_not_of('0') + 1, std::string::npos);
y.erase(y.find_last_not_of('0') + 1, std::string::npos);
z.erase(z.find_last_not_of('0') + 1, std::string::npos);
filestream << "v " << x << " " << y << " " << z << "\n";
}
numVerticesWritten += 8;
// The 6 sides of the voxel
for (uint8_t i = 0; i < 6; i++)
{
// We only write them out if the neighbor in that direction
// is inactive
if (!neighborDrawable[i])
{
// The indices of the quad making up this face
const std::vector<uint8_t>& quad0 = quads[i];
ulong q0p0 = numVerticesWritten - 8 + quad0[0] + 1;
ulong q0p1 = numVerticesWritten - 8 + quad0[1] + 1;
ulong q0p2 = numVerticesWritten - 8 + quad0[2] + 1;
ulong q0p3 = numVerticesWritten - 8 + quad0[3] + 1;
// Wavefront object files are 1-indexed with regards to vertices
filestream << "f " << std::to_string(q0p0) << " "
<< std::to_string(q0p1) << " " << std::to_string(q0p2)
<< " " << std::to_string(q0p3) << "\n";
}
}
}
void writeVoxelAux(
const Point<unsigned>& voxelPos,
const Point<unsigned>& voxelCenterMinpoint,
const float voxelWidth,
std::vector<bool>& neighborsDrawable,
unsigned& numNeighborsDrawable,
std::vector<Point<float>>& corners)
{
// Which of the 6 immediate neighbors of the voxel are active?
for (ulong i = 0; i < 6; i++)
{
neighborsDrawable[i] = isNeighborDrawable(voxelPos.cast<int>() + off[i]);
numNeighborsDrawable += neighborsDrawable[i];
}
// Coordinates of the center of the voxel
Vecutils::Point<float> center =
voxelCenterMinpoint + (voxelPos.cast<float>() * voxelWidth);
// From this center, we can get the 8 corners of the triangle
for (ushort i = 0; i < 8; i++)
{
corners[i] = center + (crnoff[i] * (voxelWidth / 2));
}
}
附录:
虽然我最终按照@Tau 的建议做了一些事情,但有一个关键区别 - 比较运算符。
对于由 3 个浮点数表示的点,<
和 ==
是不够的。即使在两者上都使用公差,它也不能始终如一地工作,并且在我的调试和发布模式之间存在差异。
我有一个新方法,我会尽可能post这里,尽管它不是 100% 万无一失。
如果您像这样定义自定义比较器:
struct PointCompare
{
bool operator() (const Point<float>& lhs, const Point<float>& rhs) const
{
if (lhs.x() < rhs.x()) // x position is most significant (arbitrary)
return true;
else if (lhs.x() == rhs.x()) {
if (lhs.y() < rhs.y())
return true;
else if (lhs.y() == lhs.y())
return lhs.z() < rhs.z();
}
}
};
然后您可以制作从点到它们在向量中的索引的映射,并且每当您在面上使用顶点时,检查它是否已经存在:
std::vector<Point> vertices;
std::map<Point, unsigned, PointCompare> indices;
unsigned getVertexIndex(Point<float>& p) {
auto it = indices.find(p);
if (it != indices.end()) // known vertex
return it->second;
else { // new vertex, store in list
unsigned pos = vertices.size();
vertices.push_back(p);
indices[p] = pos;
return pos;
}
}
使用此计算所有面孔,然后将 vertices
写入文件,然后是面孔。
最佳组合体素面确实比这复杂一些,但如果您想尝试一下,check this out。
或者,如果您总体上只处理几个网格,您可能希望省去优化代码的麻烦,并使用免费的 MeshLab,它可以删除重复的顶点、合并面并导出到只需点击几下,即可获得多种(更高效的)格式。
顺便说一句:将体素存储在列表中只有当它们非常稀疏时才有效;在大多数情况下,使用 bool[][][]
会更有效,并且会真正简化您的算法(例如寻找邻居)。
The obvious space-save I don't know how to do:
- Not writing out duplicate vertices. In total, there are around 8x more vertices in the file than there should be. However, fixing this is extremely tricky because objects in Wavefront Object files do not use per-object, but global vertices. By writing out all 8 vertices each time, I always know which 8 vertices make up the next voxel. If I do not write out all 8, how do I keep track of which place in the global list I can find those 8 (if at all).
这是我过去为 OpenGL 缓冲区准备网格(未加载几何体)时所做的事情。为此,我打算从顶点中删除重复项,因为索引缓冲区已经计划好了。
这就是我所做的:将所有顶点插入 std::set
中,这将消除重复项。
为了降低内存消耗,我使用了带有索引类型(例如 size_t
或 unsigned
)的 std::set
和自定义谓词,该谓词对索引坐标进行比较。
自定义 less 谓词:
// functor for less predicate comparing indexed values
template <typename VALUE, typename INDEX>
struct LessValueT {
VALUE *values;
LessValueT(std::vector<VALUE> &values): values(values.data()) { }
bool operator()(INDEX i1, INDEX i2) const { return values[i1] < values[i2]; }
};
和带有此谓词的 std::set
:
// an index table (sorting indices by indexed values)
template <typename VALUE, typename INDEX>
using LookUpTableT = std::set<INDEX, LessValueT<VALUE, INDEX>>;
将以上内容与存储的坐标(或法线)一起使用,例如作为
template <typename VALUE>
struct Vec3T { VALUE x, y, z; };
有必要相应地重载 less 运算符,我以最天真的方式为这个示例做了:
template <typename VALUE>
bool operator<(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return vec1.x < vec2.x ? true : vec1.x > vec2.x ? false
: vec1.y < vec2.y ? true : vec1.y > vec2.y ? false
: vec1.z < vec2.z;
}
因此,不必考虑由此谓词产生的顺序的意义或意义。它必须符合 std::set
的要求,以区分和排序具有不同分量的向量值。
为了演示这一点,我使用 tetrix sponge:
使用不同数量的三角形很容易构建(取决于细分级别)并且类似于恕我直言,非常好我对 OPs 数据所做的假设:
- 大量共享顶点
- 少量不同的法线。
完整示例代码testCollectVtcs.cc
:
#include <cassert>
#include <cmath>
#include <chrono>
#include <fstream>
#include <functional>
#include <iostream>
#include <numeric>
#include <set>
#include <string>
#include <vector>
namespace Compress {
// functor for less predicate comparing indexed values
template <typename VALUE, typename INDEX>
struct LessValueT {
VALUE *values;
LessValueT(std::vector<VALUE> &values): values(values.data()) { }
bool operator()(INDEX i1, INDEX i2) const { return values[i1] < values[i2]; }
};
// an index table (sorting indices by indexed values)
template <typename VALUE, typename INDEX>
using LookUpTableT = std::set<INDEX, LessValueT<VALUE, INDEX>>;
} // namespace Compress
// the compress function - modifies the values vector
template <typename VALUE, typename INDEX = size_t>
std::vector<INDEX> compress(std::vector<VALUE> &values)
{
typedef Compress::LessValueT<VALUE, INDEX> LessValue;
typedef Compress::LookUpTableT<VALUE, INDEX> LookUpTable;
// collect indices and remove duplicate values
std::vector<INDEX> idcs; idcs.reserve(values.size());
LookUpTable lookUp((LessValue(values)));
INDEX iIn = 0, nOut = 0;
for (const INDEX n = values.size(); iIn < n; ++iIn) {
values[nOut] = values[iIn];
std::pair<LookUpTable::iterator, bool> ret = lookUp.insert(nOut);
if (ret.second) { // new index added?
++nOut; // remark value as stored
}
idcs.push_back(*ret.first); // store index
}
// discard all obsolete values
values.resize(nOut);
// done
return idcs;
}
// instrumentation to take times
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::microseconds USecs;
typedef decltype(std::chrono::duration_cast<USecs>(Clock::now() - Clock::now())) Time;
Time duration(const Clock::time_point &t0)
{
return std::chrono::duration_cast<USecs>(Clock::now() - t0);
}
Time stopWatch(std::function<void()> func)
{
const Clock::time_point t0 = Clock::now();
func();
return duration(t0);
}
// a minimal linear algebra tool set
template <typename VALUE>
struct Vec3T { VALUE x, y, z; };
template <typename VALUE>
Vec3T<VALUE> operator*(const Vec3T<VALUE> &vec, VALUE s) { return { vec.x * s, vec.y * s, vec.z * s }; }
template <typename VALUE>
Vec3T<VALUE> operator*(VALUE s, const Vec3T<VALUE> &vec) { return { s * vec.x, s * vec.y, s * vec.z }; }
template <typename VALUE>
Vec3T<VALUE> operator+(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return { vec1.x + vec2.x, vec1.y + vec2.y, vec1.z + vec2.z };
}
template <typename VALUE>
Vec3T<VALUE> operator-(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return { vec1.x - vec2.x, vec1.y - vec2.y, vec1.z - vec2.z };
}
template <typename VALUE>
VALUE length(const Vec3T<VALUE> &vec)
{
return std::sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
}
template <typename VALUE>
VALUE dot(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z;
}
template <typename VALUE>
Vec3T<VALUE> cross(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return {
vec1.y * vec2.z - vec1.z * vec2.y,
vec1.z * vec2.x - vec1.x * vec2.z,
vec1.x * vec2.y - vec1.y * vec2.x
};
}
template <typename VALUE>
Vec3T<VALUE> normalize(const Vec3T<VALUE> &vec) { return (VALUE)1 / length(vec) * vec; }
// build sample - a tetraeder sponge
template <typename VALUE>
using StoreTriFuncT = std::function<void(const Vec3T<VALUE>&, const Vec3T<VALUE>&, const Vec3T<VALUE>&)>;
namespace TetraSponge {
template <typename VALUE>
void makeTetrix(
const Vec3T<VALUE> &p0, const Vec3T<VALUE> &p1,
const Vec3T<VALUE> &p2, const Vec3T<VALUE> &p3,
StoreTriFuncT<VALUE> &storeTri)
{
storeTri(p0, p1, p2);
storeTri(p0, p2, p3);
storeTri(p0, p3, p1);
storeTri(p1, p3, p2);
}
template <typename VALUE>
void subDivide(
unsigned depth,
const Vec3T<VALUE> &p0, const Vec3T<VALUE> &p1,
const Vec3T<VALUE> &p2, const Vec3T<VALUE> &p3,
StoreTriFuncT<VALUE> &storeTri)
{
if (!depth) { // build the 4 triangles
makeTetrix(p0, p1, p2, p3, storeTri);
} else {
--depth;
auto middle = [](const Vec3T<VALUE> &p0, const Vec3T<VALUE> &p1)
{
return 0.5f * p0 + 0.5f * p1;
};
const Vec3T<VALUE> p01 = middle(p0, p1);
const Vec3T<VALUE> p02 = middle(p0, p2);
const Vec3T<VALUE> p03 = middle(p0, p3);
const Vec3T<VALUE> p12 = middle(p1, p2);
const Vec3T<VALUE> p13 = middle(p1, p3);
const Vec3T<VALUE> p23 = middle(p2, p3);
subDivide(depth, p0, p01, p02, p03, storeTri);
subDivide(depth, p01, p1, p12, p13, storeTri);
subDivide(depth, p02, p12, p2, p23, storeTri);
subDivide(depth, p03, p13, p23, p3, storeTri);
}
}
} // namespace TetraSponge
template <typename VALUE>
void makeTetraSponge(
unsigned depth, // recursion depth (values 0 ... 9 recommended)
StoreTriFuncT<VALUE> &storeTri)
{
TetraSponge::subDivide(depth,
{ -1, -1, -1 },
{ +1, +1, -1 },
{ +1, -1, +1 },
{ -1, +1, +1 },
storeTri);
}
// minimal obj file writer
template <typename VALUE, typename INDEX>
void writeObjFile(
std::ostream &out,
const std::vector<Vec3T<VALUE>> &coords, const std::vector<INDEX> &idcsCoords,
const std::vector<Vec3T<VALUE>> &normals, const std::vector<INDEX> &idcsNormals)
{
assert(idcsCoords.size() == idcsNormals.size());
out
<< "# Wavefront OBJ file\n"
<< "\n"
<< "# " << coords.size() << " coordinates\n";
for (const Vec3 &coord : coords) {
out << "v " << coord.x << " " << coord.y << " " << coord.z << '\n';
}
out
<< "# " << normals.size() << " normals\n";
for (const Vec3 &normal : normals) {
out << "vn " << normal.x << " " << normal.y << " " << normal.z << '\n';
}
out
<< "\n"
<< "g faces\n"
<< "# " << idcsCoords.size() / 3 << " triangles\n";
for (size_t i = 0, n = idcsCoords.size(); i < n; i += 3) {
out << "f "
<< idcsCoords[i + 0] + 1 << "//" << idcsNormals[i + 0] + 1 << ' '
<< idcsCoords[i + 1] + 1 << "//" << idcsNormals[i + 1] + 1 << ' '
<< idcsCoords[i + 2] + 1 << "//" << idcsNormals[i + 2] + 1 << '\n';
}
}
template <typename VALUE, typename INDEX = size_t>
void writeObjFile(
std::ostream &out,
const std::vector<Vec3T<VALUE>> &coords, const std::vector<Vec3T<VALUE>> &normals)
{
assert(coords.size() == normals.size());
std::vector<INDEX> idcsCoords(coords.size());
std::iota(idcsCoords.begin(), idcsCoords.end(), 0);
std::vector<INDEX> idcsNormals(normals.size());
std::iota(idcsNormals.begin(), idcsNormals.end(), 0);
writeObjFile(out, coords, idcsCoords, normals, idcsNormals);
}
// main program (experiment)
template <typename VALUE>
bool operator<(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return vec1.x < vec2.x ? true : vec1.x > vec2.x ? false
: vec1.y < vec2.y ? true : vec1.y > vec2.y ? false
: vec1.z < vec2.z;
}
using Vec3 = Vec3T<float>;
using StoreTriFunc = StoreTriFuncT<float>;
int main(int argc, char **argv)
{
// read command line options
if (argc <= 2) {
std::cerr
<< "Usage:\n"
<< "> testCollectVtcs DEPTH FILE\n";
return 1;
}
const unsigned depth = std::stoi(argv[1]);
const std::string file = argv[2];
std::cout << "Build sample...\n";
std::vector<Vec3> coords, normals;
{ const Time t = stopWatch([&]() {
StoreTriFunc storeTri = [&](const Vec3 &p0, const Vec3 &p1, const Vec3 &p2) {
coords.push_back(p0); coords.push_back(p1); coords.push_back(p2);
const Vec3 n = normalize(cross(p0 - p2, p1 - p2));
normals.push_back(n); normals.push_back(n); normals.push_back(n);
};
makeTetraSponge(depth, storeTri);
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout << "coords: " << coords.size() << ", normals: " << normals.size() << '\n';
const std::string fileUncompr = file + ".uncompressed.obj";
std::cout << "Write uncompressed OBJ file '" << fileUncompr << "'...\n";
{ const Time t = stopWatch([&]() {
std::ofstream fOut(fileUncompr.c_str(), std::ios::binary);
/* std::ios::binary -> force Unix line-endings on Windows
* to win some extra bytes
*/
writeObjFile(fOut, coords, normals);
fOut.close();
if (!fOut.good()) {
std::cerr << "Writing of '" << fileUncompr << "' failed!\n";
throw std::ios::failure("Failed to complete writing of file!");
}
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout << "Compress coordinates and normals...\n";
std::vector<size_t> idcsCoords, idcsNormals;
{ const Time t = stopWatch([&]() {
idcsCoords = compress(coords);
idcsNormals = compress(normals);
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout
<< "coords: " << coords.size() << ", normals: " << normals.size() << '\n'
<< "coord idcs: " << idcsCoords.size() << ", normals: " << normals.size() << '\n';
const std::string fileCompr = file + ".compressed.obj";
std::cout << "Write compressed OBJ file'" << fileCompr << "'...\n";
{ const Time t = stopWatch([&]() {
std::ofstream fOut(fileCompr.c_str(), std::ios::binary);
/* std::ios::binary -> force Unix line-endings on Windows
* to win some extra bytes
*/
writeObjFile(fOut, coords, idcsCoords, normals, idcsNormals);
fOut.close();
if (!fOut.good()) {
std::cerr << "Writing of '" << fileCompr << "' failed!\n";
throw std::ios::failure("Failed to complete writing of file!");
}
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout << "Done.\n";
}
第一次检查:
> testCollectVtcs
Usage:
> testCollectVtcs DEPTH FILE
> testCollectVtcs 1 test1
Build sample...
Done after 34 us.
coords: 48, normals: 48
Write uncompressed OBJ file 'test1.uncompressed.obj'...
Done after 1432 us.
Compress coordinates and normals...
Done after 12 us.
coords: 10, normals: 4
coord idcs: 48, normals: 4
Write compressed OBJ file'test1.compressed.obj'...
Done after 1033 us.
Done.
这产生了两个文件:
$ ls test1.*.obj
-rw-r--r-- 1 Scheff 1049089 553 Mar 26 11:46 test1.compressed.obj
-rw-r--r-- 1 Scheff 1049089 2214 Mar 26 11:46 test1.uncompressed.obj
$
$ cat test1.uncompressed.obj
# Wavefront OBJ file
# 48 coordinates
v -1 -1 -1
v 0 0 -1
v 0 -1 0
v -1 -1 -1
v 0 -1 0
v -1 0 0
v -1 -1 -1
v -1 0 0
v 0 0 -1
v 0 0 -1
v -1 0 0
v 0 -1 0
v 0 0 -1
v 1 1 -1
v 1 0 0
v 0 0 -1
v 1 0 0
v 0 1 0
v 0 0 -1
v 0 1 0
v 1 1 -1
v 1 1 -1
v 0 1 0
v 1 0 0
v 0 -1 0
v 1 0 0
v 1 -1 1
v 0 -1 0
v 1 -1 1
v 0 0 1
v 0 -1 0
v 0 0 1
v 1 0 0
v 1 0 0
v 0 0 1
v 1 -1 1
v -1 0 0
v 0 1 0
v 0 0 1
v -1 0 0
v 0 0 1
v -1 1 1
v -1 0 0
v -1 1 1
v 0 1 0
v 0 1 0
v -1 1 1
v 0 0 1
# 48 normals
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
g faces
# 16 triangles
f 1//1 2//2 3//3
f 4//4 5//5 6//6
f 7//7 8//8 9//9
f 10//10 11//11 12//12
f 13//13 14//14 15//15
f 16//16 17//17 18//18
f 19//19 20//20 21//21
f 22//22 23//23 24//24
f 25//25 26//26 27//27
f 28//28 29//29 30//30
f 31//31 32//32 33//33
f 34//34 35//35 36//36
f 37//37 38//38 39//39
f 40//40 41//41 42//42
f 43//43 44//44 45//45
f 46//46 47//47 48//48
$
$ cat test1.compressed.obj
# Wavefront OBJ file
# 10 coordinates
v -1 -1 -1
v 0 0 -1
v 0 -1 0
v -1 0 0
v 1 1 -1
v 1 0 0
v 0 1 0
v 1 -1 1
v 0 0 1
v -1 1 1
# 4 normals
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
g faces
# 16 triangles
f 1//1 2//1 3//1
f 1//2 3//2 4//2
f 1//3 4//3 2//3
f 2//4 4//4 3//4
f 2//1 5//1 6//1
f 2//2 6//2 7//2
f 2//3 7//3 5//3
f 5//4 7//4 6//4
f 3//1 6//1 8//1
f 3//2 8//2 9//2
f 3//3 9//3 6//3
f 6//4 9//4 8//4
f 4//1 7//1 9//1
f 4//2 9//2 10//2
f 4//3 10//3 7//3
f 7//4 10//4 9//4
$
所以,这就是结果
- 48 坐标 vs. 10 坐标
- 48 个法线对 4 个法线。
这是,看起来如何:
(我看不出与 test1.compressed.obj
有任何视觉差异。)
关于秒表时间,我不会太相信他们。为此,样本太小了。
因此,另一个具有更多几何形状(更多)的测试:
> testCollectVtcs 8 test8
Build sample...
Done after 40298 us.
coords: 786432, normals: 786432
Write uncompressed OBJ file 'test8.uncompressed.obj'...
Done after 6200571 us.
Compress coordinates and normals...
Done after 115817 us.
coords: 131074, normals: 4
coord idcs: 786432, normals: 4
Write compressed OBJ file'test8.compressed.obj'...
Done after 1513216 us.
Done.
>
两个文件:
$ ls -l test8.*.obj
-rw-r--r-- 1 ds32737 1049089 11540967 Mar 26 11:56 test8.compressed.obj
-rw-r--r-- 1 ds32737 1049089 57424470 Mar 26 11:56 test8.uncompressed.obj
$
总结一下:
- 11 兆字节与 56 兆字节。
- 压缩写入:0.12 s + 1.51 s = 1.63 s
- 对比写入未压缩的:6.2 s
我正在尝试写出 voxelization of a model to a Wavefront Object File。
我的方法很简单,而且运行时间合理。问题是——它生成的 OBJ 文件大小过大。我试图将一个 1 GB 的文件加载到 3D Viewer 上一台非常受人尊敬的带有 SSD 的机器上,但在某些情况下,尝试移动相机时会延迟几秒钟,而在其他情况下,它根本拒绝做任何事情并有效地软锁定.
到目前为止我做了什么:
- 我不会写出模型内部的任何面孔 - 也就是说,两个体素之间的面孔都将被写入文件。没有意义,因为没人能看到它们。
- 因为 OBJ 没有广泛支持的二进制格式(据我所知),我发现我可以通过修剪文件中顶点位置的尾随零来节省一些 space。
明显的space-保存我不知道怎么做:
- 没有写出重复的顶点。总的来说,文件中的顶点比应有的多大约 8 倍。然而,解决这个问题非常棘手,因为 Wavefront 对象文件中的对象不使用每个对象,而是 global 顶点。通过每次写出所有 8 个顶点,我总是知道下一个体素由哪 8 个顶点组成。如果我没有写出所有 8 个,我如何跟踪全局列表中的哪个位置我可以找到这 8 个(如果有的话)。
更难,但可能有用的大 space-保存:
- 如果我可以更抽象地工作,可能会有一种方法可以将体素组合成更少的对象,或者将位于同一平面上的面组合成更大的面。 IE,如果两个体素的正面都处于活动状态,则将其变成一个大两倍的矩形。
因为这是必需的,所以这里有一些代码可以粗略地显示正在发生的事情。这不是实际使用的代码。我不能 post 那个,它依赖于许多用户定义的类型,并且有很多代码来处理边缘情况或额外的功能,所以不管怎样放在这里会很混乱和冗长。
对这个问题唯一重要的是我的方法——逐个体素,写出所有 8 个顶点,然后写出 6 个边中不与活动体素相邻的那个。尽管它确实会生成大文件,但您必须相信我它是有效的。
我的问题是我可以使用什么方法或方法来进一步减小尺寸。例如,我怎样才能不写出任何重复的顶点?
假设:
Point
只是一个大小为 3 的数组,带有像 .x() 这样的吸气剂
Vector3D
是围绕std::vector
的 3D 包装器,具有.at(x,y,z)
方法- 哪些体素处于活动状态是任意的,不遵循某种模式,但在调用
writeObj
之前已知。可以快速获取体素是否在任何位置处于活动状态。
//Left, right, bottom, top, front, rear
static const std::vector<std::vector<uint8_t>> quads = {
{3, 0, 4, 7}, {1, 2, 6, 5}, {3, 2, 1, 0},
{4, 5, 6, 7}, {0, 1, 5, 4}, {2, 3, 7, 6}};
void writeOBJ(
std::string folder,
const std::string& filename,
const Vector3D<Voxel>& voxels,
const Point<unsigned> gridDim,
const Point<unsigned>& voxelCenterMinpoint,
const float voxelWidth)
{
unsigned numTris = 0;
std::ofstream filestream;
std::string filepath;
std::string extension;
ulong numVerticesWritten = 0;
// Make sure the folder ends with a '/'
if (folder.back() != '/')
{
folder.append("/");
}
filepath = folder + filename + ".obj";
filestream.open(filepath, std::ios::out);
// Remove the voxelization file if it already exists
std::remove(filepath.c_str());
Point<unsigned> voxelPos;
for (voxelPos[0] = 0; voxelPos[0] < gridDim.x(); voxelPos[0]++)
{
for (voxelPos[1] = 0; voxelPos[1] < gridDim.y(); voxelPos[1]++)
{
for (voxelPos[2] = 0; voxelPos[2] < gridDim.z(); voxelPos[2]++)
{
if (voxels.at(voxelPos))
{
writeVoxelToOBJ(
filestream, voxels, voxelPos, voxelCenterMinpoint, voxelWidth,
numVerticesWritten);
}
}
}
}
filestream.close();
}
void writeVoxelToOBJ(
std::ofstream& filestream,
const Vector3D<Voxel>& voxels,
const Point<unsigned>& voxelPos,
const Point<unsigned>& voxelCenterMinpoint,
const float voxelWidth,
ulong& numVerticesWritten)
{
std::vector<bool> neighborDrawable(6);
std::vector<Vecutils::Point<float>> corners(8);
unsigned numNeighborsDrawable = 0;
// Determine which neighbors are active and what the 8 corners of the
// voxel are
writeVoxelAux(
voxelPos, voxelCenterMinpoint, voxelWidth, neighborDrawable,
numNeighborsDrawable, corners);
// Normally, if all neighbors are active, there is no reason to write out this
// voxel. (All its faces are internal) If inverted, the opposite is true.
if (numNeighborsDrawable == 6)
{
return;
}
// Write out the vertices
for (const Vecutils::Point<float>& corner : corners)
{
std::string x = std::to_string(corner.x());
std::string y = std::to_string(corner.y());
std::string z = std::to_string(corner.z());
// Strip trailing zeros, they serve no prupose and bloat filesize
x.erase(x.find_last_not_of('0') + 1, std::string::npos);
y.erase(y.find_last_not_of('0') + 1, std::string::npos);
z.erase(z.find_last_not_of('0') + 1, std::string::npos);
filestream << "v " << x << " " << y << " " << z << "\n";
}
numVerticesWritten += 8;
// The 6 sides of the voxel
for (uint8_t i = 0; i < 6; i++)
{
// We only write them out if the neighbor in that direction
// is inactive
if (!neighborDrawable[i])
{
// The indices of the quad making up this face
const std::vector<uint8_t>& quad0 = quads[i];
ulong q0p0 = numVerticesWritten - 8 + quad0[0] + 1;
ulong q0p1 = numVerticesWritten - 8 + quad0[1] + 1;
ulong q0p2 = numVerticesWritten - 8 + quad0[2] + 1;
ulong q0p3 = numVerticesWritten - 8 + quad0[3] + 1;
// Wavefront object files are 1-indexed with regards to vertices
filestream << "f " << std::to_string(q0p0) << " "
<< std::to_string(q0p1) << " " << std::to_string(q0p2)
<< " " << std::to_string(q0p3) << "\n";
}
}
}
void writeVoxelAux(
const Point<unsigned>& voxelPos,
const Point<unsigned>& voxelCenterMinpoint,
const float voxelWidth,
std::vector<bool>& neighborsDrawable,
unsigned& numNeighborsDrawable,
std::vector<Point<float>>& corners)
{
// Which of the 6 immediate neighbors of the voxel are active?
for (ulong i = 0; i < 6; i++)
{
neighborsDrawable[i] = isNeighborDrawable(voxelPos.cast<int>() + off[i]);
numNeighborsDrawable += neighborsDrawable[i];
}
// Coordinates of the center of the voxel
Vecutils::Point<float> center =
voxelCenterMinpoint + (voxelPos.cast<float>() * voxelWidth);
// From this center, we can get the 8 corners of the triangle
for (ushort i = 0; i < 8; i++)
{
corners[i] = center + (crnoff[i] * (voxelWidth / 2));
}
}
附录:
虽然我最终按照@Tau 的建议做了一些事情,但有一个关键区别 - 比较运算符。
对于由 3 个浮点数表示的点,<
和 ==
是不够的。即使在两者上都使用公差,它也不能始终如一地工作,并且在我的调试和发布模式之间存在差异。
我有一个新方法,我会尽可能post这里,尽管它不是 100% 万无一失。
如果您像这样定义自定义比较器:
struct PointCompare
{
bool operator() (const Point<float>& lhs, const Point<float>& rhs) const
{
if (lhs.x() < rhs.x()) // x position is most significant (arbitrary)
return true;
else if (lhs.x() == rhs.x()) {
if (lhs.y() < rhs.y())
return true;
else if (lhs.y() == lhs.y())
return lhs.z() < rhs.z();
}
}
};
然后您可以制作从点到它们在向量中的索引的映射,并且每当您在面上使用顶点时,检查它是否已经存在:
std::vector<Point> vertices;
std::map<Point, unsigned, PointCompare> indices;
unsigned getVertexIndex(Point<float>& p) {
auto it = indices.find(p);
if (it != indices.end()) // known vertex
return it->second;
else { // new vertex, store in list
unsigned pos = vertices.size();
vertices.push_back(p);
indices[p] = pos;
return pos;
}
}
使用此计算所有面孔,然后将 vertices
写入文件,然后是面孔。
最佳组合体素面确实比这复杂一些,但如果您想尝试一下,check this out。
或者,如果您总体上只处理几个网格,您可能希望省去优化代码的麻烦,并使用免费的 MeshLab,它可以删除重复的顶点、合并面并导出到只需点击几下,即可获得多种(更高效的)格式。
顺便说一句:将体素存储在列表中只有当它们非常稀疏时才有效;在大多数情况下,使用 bool[][][]
会更有效,并且会真正简化您的算法(例如寻找邻居)。
The obvious space-save I don't know how to do:
- Not writing out duplicate vertices. In total, there are around 8x more vertices in the file than there should be. However, fixing this is extremely tricky because objects in Wavefront Object files do not use per-object, but global vertices. By writing out all 8 vertices each time, I always know which 8 vertices make up the next voxel. If I do not write out all 8, how do I keep track of which place in the global list I can find those 8 (if at all).
这是我过去为 OpenGL 缓冲区准备网格(未加载几何体)时所做的事情。为此,我打算从顶点中删除重复项,因为索引缓冲区已经计划好了。
这就是我所做的:将所有顶点插入 std::set
中,这将消除重复项。
为了降低内存消耗,我使用了带有索引类型(例如 size_t
或 unsigned
)的 std::set
和自定义谓词,该谓词对索引坐标进行比较。
自定义 less 谓词:
// functor for less predicate comparing indexed values
template <typename VALUE, typename INDEX>
struct LessValueT {
VALUE *values;
LessValueT(std::vector<VALUE> &values): values(values.data()) { }
bool operator()(INDEX i1, INDEX i2) const { return values[i1] < values[i2]; }
};
和带有此谓词的 std::set
:
// an index table (sorting indices by indexed values)
template <typename VALUE, typename INDEX>
using LookUpTableT = std::set<INDEX, LessValueT<VALUE, INDEX>>;
将以上内容与存储的坐标(或法线)一起使用,例如作为
template <typename VALUE>
struct Vec3T { VALUE x, y, z; };
有必要相应地重载 less 运算符,我以最天真的方式为这个示例做了:
template <typename VALUE>
bool operator<(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return vec1.x < vec2.x ? true : vec1.x > vec2.x ? false
: vec1.y < vec2.y ? true : vec1.y > vec2.y ? false
: vec1.z < vec2.z;
}
因此,不必考虑由此谓词产生的顺序的意义或意义。它必须符合 std::set
的要求,以区分和排序具有不同分量的向量值。
为了演示这一点,我使用 tetrix sponge:
使用不同数量的三角形很容易构建(取决于细分级别)并且类似于恕我直言,非常好我对 OPs 数据所做的假设:
- 大量共享顶点
- 少量不同的法线。
完整示例代码testCollectVtcs.cc
:
#include <cassert>
#include <cmath>
#include <chrono>
#include <fstream>
#include <functional>
#include <iostream>
#include <numeric>
#include <set>
#include <string>
#include <vector>
namespace Compress {
// functor for less predicate comparing indexed values
template <typename VALUE, typename INDEX>
struct LessValueT {
VALUE *values;
LessValueT(std::vector<VALUE> &values): values(values.data()) { }
bool operator()(INDEX i1, INDEX i2) const { return values[i1] < values[i2]; }
};
// an index table (sorting indices by indexed values)
template <typename VALUE, typename INDEX>
using LookUpTableT = std::set<INDEX, LessValueT<VALUE, INDEX>>;
} // namespace Compress
// the compress function - modifies the values vector
template <typename VALUE, typename INDEX = size_t>
std::vector<INDEX> compress(std::vector<VALUE> &values)
{
typedef Compress::LessValueT<VALUE, INDEX> LessValue;
typedef Compress::LookUpTableT<VALUE, INDEX> LookUpTable;
// collect indices and remove duplicate values
std::vector<INDEX> idcs; idcs.reserve(values.size());
LookUpTable lookUp((LessValue(values)));
INDEX iIn = 0, nOut = 0;
for (const INDEX n = values.size(); iIn < n; ++iIn) {
values[nOut] = values[iIn];
std::pair<LookUpTable::iterator, bool> ret = lookUp.insert(nOut);
if (ret.second) { // new index added?
++nOut; // remark value as stored
}
idcs.push_back(*ret.first); // store index
}
// discard all obsolete values
values.resize(nOut);
// done
return idcs;
}
// instrumentation to take times
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::microseconds USecs;
typedef decltype(std::chrono::duration_cast<USecs>(Clock::now() - Clock::now())) Time;
Time duration(const Clock::time_point &t0)
{
return std::chrono::duration_cast<USecs>(Clock::now() - t0);
}
Time stopWatch(std::function<void()> func)
{
const Clock::time_point t0 = Clock::now();
func();
return duration(t0);
}
// a minimal linear algebra tool set
template <typename VALUE>
struct Vec3T { VALUE x, y, z; };
template <typename VALUE>
Vec3T<VALUE> operator*(const Vec3T<VALUE> &vec, VALUE s) { return { vec.x * s, vec.y * s, vec.z * s }; }
template <typename VALUE>
Vec3T<VALUE> operator*(VALUE s, const Vec3T<VALUE> &vec) { return { s * vec.x, s * vec.y, s * vec.z }; }
template <typename VALUE>
Vec3T<VALUE> operator+(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return { vec1.x + vec2.x, vec1.y + vec2.y, vec1.z + vec2.z };
}
template <typename VALUE>
Vec3T<VALUE> operator-(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return { vec1.x - vec2.x, vec1.y - vec2.y, vec1.z - vec2.z };
}
template <typename VALUE>
VALUE length(const Vec3T<VALUE> &vec)
{
return std::sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
}
template <typename VALUE>
VALUE dot(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z;
}
template <typename VALUE>
Vec3T<VALUE> cross(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return {
vec1.y * vec2.z - vec1.z * vec2.y,
vec1.z * vec2.x - vec1.x * vec2.z,
vec1.x * vec2.y - vec1.y * vec2.x
};
}
template <typename VALUE>
Vec3T<VALUE> normalize(const Vec3T<VALUE> &vec) { return (VALUE)1 / length(vec) * vec; }
// build sample - a tetraeder sponge
template <typename VALUE>
using StoreTriFuncT = std::function<void(const Vec3T<VALUE>&, const Vec3T<VALUE>&, const Vec3T<VALUE>&)>;
namespace TetraSponge {
template <typename VALUE>
void makeTetrix(
const Vec3T<VALUE> &p0, const Vec3T<VALUE> &p1,
const Vec3T<VALUE> &p2, const Vec3T<VALUE> &p3,
StoreTriFuncT<VALUE> &storeTri)
{
storeTri(p0, p1, p2);
storeTri(p0, p2, p3);
storeTri(p0, p3, p1);
storeTri(p1, p3, p2);
}
template <typename VALUE>
void subDivide(
unsigned depth,
const Vec3T<VALUE> &p0, const Vec3T<VALUE> &p1,
const Vec3T<VALUE> &p2, const Vec3T<VALUE> &p3,
StoreTriFuncT<VALUE> &storeTri)
{
if (!depth) { // build the 4 triangles
makeTetrix(p0, p1, p2, p3, storeTri);
} else {
--depth;
auto middle = [](const Vec3T<VALUE> &p0, const Vec3T<VALUE> &p1)
{
return 0.5f * p0 + 0.5f * p1;
};
const Vec3T<VALUE> p01 = middle(p0, p1);
const Vec3T<VALUE> p02 = middle(p0, p2);
const Vec3T<VALUE> p03 = middle(p0, p3);
const Vec3T<VALUE> p12 = middle(p1, p2);
const Vec3T<VALUE> p13 = middle(p1, p3);
const Vec3T<VALUE> p23 = middle(p2, p3);
subDivide(depth, p0, p01, p02, p03, storeTri);
subDivide(depth, p01, p1, p12, p13, storeTri);
subDivide(depth, p02, p12, p2, p23, storeTri);
subDivide(depth, p03, p13, p23, p3, storeTri);
}
}
} // namespace TetraSponge
template <typename VALUE>
void makeTetraSponge(
unsigned depth, // recursion depth (values 0 ... 9 recommended)
StoreTriFuncT<VALUE> &storeTri)
{
TetraSponge::subDivide(depth,
{ -1, -1, -1 },
{ +1, +1, -1 },
{ +1, -1, +1 },
{ -1, +1, +1 },
storeTri);
}
// minimal obj file writer
template <typename VALUE, typename INDEX>
void writeObjFile(
std::ostream &out,
const std::vector<Vec3T<VALUE>> &coords, const std::vector<INDEX> &idcsCoords,
const std::vector<Vec3T<VALUE>> &normals, const std::vector<INDEX> &idcsNormals)
{
assert(idcsCoords.size() == idcsNormals.size());
out
<< "# Wavefront OBJ file\n"
<< "\n"
<< "# " << coords.size() << " coordinates\n";
for (const Vec3 &coord : coords) {
out << "v " << coord.x << " " << coord.y << " " << coord.z << '\n';
}
out
<< "# " << normals.size() << " normals\n";
for (const Vec3 &normal : normals) {
out << "vn " << normal.x << " " << normal.y << " " << normal.z << '\n';
}
out
<< "\n"
<< "g faces\n"
<< "# " << idcsCoords.size() / 3 << " triangles\n";
for (size_t i = 0, n = idcsCoords.size(); i < n; i += 3) {
out << "f "
<< idcsCoords[i + 0] + 1 << "//" << idcsNormals[i + 0] + 1 << ' '
<< idcsCoords[i + 1] + 1 << "//" << idcsNormals[i + 1] + 1 << ' '
<< idcsCoords[i + 2] + 1 << "//" << idcsNormals[i + 2] + 1 << '\n';
}
}
template <typename VALUE, typename INDEX = size_t>
void writeObjFile(
std::ostream &out,
const std::vector<Vec3T<VALUE>> &coords, const std::vector<Vec3T<VALUE>> &normals)
{
assert(coords.size() == normals.size());
std::vector<INDEX> idcsCoords(coords.size());
std::iota(idcsCoords.begin(), idcsCoords.end(), 0);
std::vector<INDEX> idcsNormals(normals.size());
std::iota(idcsNormals.begin(), idcsNormals.end(), 0);
writeObjFile(out, coords, idcsCoords, normals, idcsNormals);
}
// main program (experiment)
template <typename VALUE>
bool operator<(const Vec3T<VALUE> &vec1, const Vec3T<VALUE> &vec2)
{
return vec1.x < vec2.x ? true : vec1.x > vec2.x ? false
: vec1.y < vec2.y ? true : vec1.y > vec2.y ? false
: vec1.z < vec2.z;
}
using Vec3 = Vec3T<float>;
using StoreTriFunc = StoreTriFuncT<float>;
int main(int argc, char **argv)
{
// read command line options
if (argc <= 2) {
std::cerr
<< "Usage:\n"
<< "> testCollectVtcs DEPTH FILE\n";
return 1;
}
const unsigned depth = std::stoi(argv[1]);
const std::string file = argv[2];
std::cout << "Build sample...\n";
std::vector<Vec3> coords, normals;
{ const Time t = stopWatch([&]() {
StoreTriFunc storeTri = [&](const Vec3 &p0, const Vec3 &p1, const Vec3 &p2) {
coords.push_back(p0); coords.push_back(p1); coords.push_back(p2);
const Vec3 n = normalize(cross(p0 - p2, p1 - p2));
normals.push_back(n); normals.push_back(n); normals.push_back(n);
};
makeTetraSponge(depth, storeTri);
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout << "coords: " << coords.size() << ", normals: " << normals.size() << '\n';
const std::string fileUncompr = file + ".uncompressed.obj";
std::cout << "Write uncompressed OBJ file '" << fileUncompr << "'...\n";
{ const Time t = stopWatch([&]() {
std::ofstream fOut(fileUncompr.c_str(), std::ios::binary);
/* std::ios::binary -> force Unix line-endings on Windows
* to win some extra bytes
*/
writeObjFile(fOut, coords, normals);
fOut.close();
if (!fOut.good()) {
std::cerr << "Writing of '" << fileUncompr << "' failed!\n";
throw std::ios::failure("Failed to complete writing of file!");
}
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout << "Compress coordinates and normals...\n";
std::vector<size_t> idcsCoords, idcsNormals;
{ const Time t = stopWatch([&]() {
idcsCoords = compress(coords);
idcsNormals = compress(normals);
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout
<< "coords: " << coords.size() << ", normals: " << normals.size() << '\n'
<< "coord idcs: " << idcsCoords.size() << ", normals: " << normals.size() << '\n';
const std::string fileCompr = file + ".compressed.obj";
std::cout << "Write compressed OBJ file'" << fileCompr << "'...\n";
{ const Time t = stopWatch([&]() {
std::ofstream fOut(fileCompr.c_str(), std::ios::binary);
/* std::ios::binary -> force Unix line-endings on Windows
* to win some extra bytes
*/
writeObjFile(fOut, coords, idcsCoords, normals, idcsNormals);
fOut.close();
if (!fOut.good()) {
std::cerr << "Writing of '" << fileCompr << "' failed!\n";
throw std::ios::failure("Failed to complete writing of file!");
}
});
std::cout << "Done after " << t.count() << " us.\n";
}
std::cout << "Done.\n";
}
第一次检查:
> testCollectVtcs
Usage:
> testCollectVtcs DEPTH FILE
> testCollectVtcs 1 test1
Build sample...
Done after 34 us.
coords: 48, normals: 48
Write uncompressed OBJ file 'test1.uncompressed.obj'...
Done after 1432 us.
Compress coordinates and normals...
Done after 12 us.
coords: 10, normals: 4
coord idcs: 48, normals: 4
Write compressed OBJ file'test1.compressed.obj'...
Done after 1033 us.
Done.
这产生了两个文件:
$ ls test1.*.obj
-rw-r--r-- 1 Scheff 1049089 553 Mar 26 11:46 test1.compressed.obj
-rw-r--r-- 1 Scheff 1049089 2214 Mar 26 11:46 test1.uncompressed.obj
$
$ cat test1.uncompressed.obj
# Wavefront OBJ file
# 48 coordinates
v -1 -1 -1
v 0 0 -1
v 0 -1 0
v -1 -1 -1
v 0 -1 0
v -1 0 0
v -1 -1 -1
v -1 0 0
v 0 0 -1
v 0 0 -1
v -1 0 0
v 0 -1 0
v 0 0 -1
v 1 1 -1
v 1 0 0
v 0 0 -1
v 1 0 0
v 0 1 0
v 0 0 -1
v 0 1 0
v 1 1 -1
v 1 1 -1
v 0 1 0
v 1 0 0
v 0 -1 0
v 1 0 0
v 1 -1 1
v 0 -1 0
v 1 -1 1
v 0 0 1
v 0 -1 0
v 0 0 1
v 1 0 0
v 1 0 0
v 0 0 1
v 1 -1 1
v -1 0 0
v 0 1 0
v 0 0 1
v -1 0 0
v 0 0 1
v -1 1 1
v -1 0 0
v -1 1 1
v 0 1 0
v 0 1 0
v -1 1 1
v 0 0 1
# 48 normals
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
vn 0.57735 0.57735 0.57735
g faces
# 16 triangles
f 1//1 2//2 3//3
f 4//4 5//5 6//6
f 7//7 8//8 9//9
f 10//10 11//11 12//12
f 13//13 14//14 15//15
f 16//16 17//17 18//18
f 19//19 20//20 21//21
f 22//22 23//23 24//24
f 25//25 26//26 27//27
f 28//28 29//29 30//30
f 31//31 32//32 33//33
f 34//34 35//35 36//36
f 37//37 38//38 39//39
f 40//40 41//41 42//42
f 43//43 44//44 45//45
f 46//46 47//47 48//48
$
$ cat test1.compressed.obj
# Wavefront OBJ file
# 10 coordinates
v -1 -1 -1
v 0 0 -1
v 0 -1 0
v -1 0 0
v 1 1 -1
v 1 0 0
v 0 1 0
v 1 -1 1
v 0 0 1
v -1 1 1
# 4 normals
vn 0.57735 -0.57735 -0.57735
vn -0.57735 -0.57735 0.57735
vn -0.57735 0.57735 -0.57735
vn 0.57735 0.57735 0.57735
g faces
# 16 triangles
f 1//1 2//1 3//1
f 1//2 3//2 4//2
f 1//3 4//3 2//3
f 2//4 4//4 3//4
f 2//1 5//1 6//1
f 2//2 6//2 7//2
f 2//3 7//3 5//3
f 5//4 7//4 6//4
f 3//1 6//1 8//1
f 3//2 8//2 9//2
f 3//3 9//3 6//3
f 6//4 9//4 8//4
f 4//1 7//1 9//1
f 4//2 9//2 10//2
f 4//3 10//3 7//3
f 7//4 10//4 9//4
$
所以,这就是结果
- 48 坐标 vs. 10 坐标
- 48 个法线对 4 个法线。
这是,看起来如何:
(我看不出与 test1.compressed.obj
有任何视觉差异。)
关于秒表时间,我不会太相信他们。为此,样本太小了。
因此,另一个具有更多几何形状(更多)的测试:
> testCollectVtcs 8 test8
Build sample...
Done after 40298 us.
coords: 786432, normals: 786432
Write uncompressed OBJ file 'test8.uncompressed.obj'...
Done after 6200571 us.
Compress coordinates and normals...
Done after 115817 us.
coords: 131074, normals: 4
coord idcs: 786432, normals: 4
Write compressed OBJ file'test8.compressed.obj'...
Done after 1513216 us.
Done.
>
两个文件:
$ ls -l test8.*.obj
-rw-r--r-- 1 ds32737 1049089 11540967 Mar 26 11:56 test8.compressed.obj
-rw-r--r-- 1 ds32737 1049089 57424470 Mar 26 11:56 test8.uncompressed.obj
$
总结一下:
- 11 兆字节与 56 兆字节。
- 压缩写入:0.12 s + 1.51 s = 1.63 s
- 对比写入未压缩的:6.2 s