Как я могу улучшить свой метод записи файлов, чтобы уменьшить размер файла объектов Wavefront? - PullRequest
1 голос
/ 26 марта 2020

Я пытаюсь записать вокселизацию модели в объектный файл Wavefront .

Мой метод прост и работает в разумные сроки. Проблема в том, что он производит файлы OBJ, которые имеют смехотворный размер. Я пытался загрузить файл размером 1 ГБ в 3D Viewer на очень респектабельном компьютере с SSD, но в некоторых случаях задержка составляла несколько секунд при попытке переместить камеру, в других она вообще отказывалась что-либо делать и эффективно блокируется.

Что я сделал до сих пор:

  • Я не выписываю никаких граней, которые являются внутренними для модели, то есть грани между двумя вокселями, которые будут записаны в файл. Нет никакого смысла, поскольку никто не может их видеть.
  • Поскольку OBJ не имеет широко поддерживаемого двоичного формата (насколько я знаю), я обнаружил, что мог бы сэкономить некоторое пространство, обрезая конечные нули от позиции вершин в файле.

Очевидное сохранение пространства, я не знаю, как это сделать:

  • Не выписывать дублирующиеся вершины. Всего в файле примерно в 8 раз больше вершин, чем должно быть. Однако исправить это чрезвычайно сложно, поскольку объекты в объектных файлах Wavefront используют не для каждого объекта, а глобальные вершины. Записывая все 8 вершин каждый раз, я всегда знаю, какие 8 вершин составляют следующий воксель. Если я не выписываю все 8, как мне отслеживать, какое место в глобальном списке я могу найти эти 8 (если вообще).

Сложнее, но потенциально полезно экономия большого пространства:

  • Если бы я мог работать более абстрактно, мог бы быть способ объединить воксели в меньшее количество объектов или объединить грани, которые l ie вдоль одной плоскости, в большую лица. IE, если у двух вокселей активна передняя грань, превратите их в один прямоугольник большего размера, в два раза больше.

Поскольку это необходимо, вот некоторый код, который примерно показывает, что происходит. Это не тот код, который на самом деле используется. Я не могу опубликовать это, и он опирается на многие пользовательские типы и имеет много кода для обработки крайних случаев или дополнительной функциональности, поэтому в любом случае это будет сложным и длинным.

Единственная вещь, которая важна для этого вопроса, - это мой метод - выбрать воксель, выписать все 8 вершин, а затем записать, какая из 6 сторон не соседствует с активным вокселем. Вам просто придется поверить мне, что он работает, хотя он и производит большие файлы.

Мой вопрос заключается в том, какой метод или подход я могу использовать для дальнейшего уменьшения размера. Как я могу, например, не записывать дублирующиеся вершины?

Допущения:

  • Point - это просто массив размера 3 с геттерами например .x ()
  • Vector3D - это трехмерная оболочка вокруг std::vector с .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));
    }
}

Приложение:

В то время как я в конечном итоге сделал что-то вроде what @ Тау предположил, что есть одно ключевое отличие - оператор сравнения.

Для точек, представленных 3 числами с плавающей запятой, < и == недостаточно. Даже при использовании допусков в обоих случаях он не работает согласованно и имеет расхождения между режимом отладки и выпуска.

У меня есть новый метод, который я опубликую здесь, когда смогу, хотя даже он не на 100% надежен.

Ответы [ 2 ]

1 голос
/ 26 марта 2020

Если вы определяете пользовательский компаратор следующим образом:

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 в файл, затем грани.

Оптимальное объединение граней вокселей действительно несколько сложнее, чем это, но если вы хотите иметь go, проверить это .

В качестве альтернативы, если вы обрабатываете всего несколько мешей в целом, вы можете избавить себя от хлопот, оптимизируя ваш код и используйте бесплатную MeshLab , которая может удалять дубликаты вершин, объединять грани и экспортировать в различные (более эффективные) форматы всего несколькими щелчками мыши.

Кстати: хранение ваших вокселей в список эффективен только в том случае, если они действительно редки; вместо этого использование bool[][][] будет более эффективным в большинстве случаев и действительно просто облегчит ваши алгоритмы (например, для поиска соседей).

0 голосов
/ 26 марта 2020

Очевидная экономия места, я не знаю, как это сделать:

  • Не выписывать дублирующиеся вершины. Всего в файле примерно в 8 раз больше вершин, чем должно быть. Однако исправить это чрезвычайно сложно, поскольку объекты в объектных файлах Wavefront используют не объект, а глобальные вершины. Записывая все 8 вершин каждый раз, я всегда знаю, какие 8 вершин составляют следующий воксель. Если я не выписываю все 8, как мне отслеживать, какое место в глобальном списке я могу найти эти 8 (если вообще).

Это то, что я однажды сделал в прошлом, чтобы подготовить сетки (из загруженной геометрии) для буферов OpenGL. Для этого я намеревался удалить дубликаты из вершин, поскольку буфер индекса уже был запланирован.

Это то, что я сделал: вставьте все вершины в std::set, что позволит удалить дубликаты.

Для чтобы уменьшить потребление памяти, я использовал std::set с индексным типом (например, size_t или unsigned) с пользовательским предикатом, который выполняет сравнение по индексированным координатам.

Пользовательский предикат 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; };

it необходимо перегрузить оператор 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, чтобы различать guish и сортировать векторные значения по различным компонентам.

Чтобы продемонстрировать это, я использую tetrix sponge :

Tetrix Sponge (on mathworld.wolfram.com)

Легко построить с переменным числом треугольников (в зависимости от уровней подразделений) и очень хорошо напоминает ИМХО предположения, которые я сделал относительно данных OP:

  • значительное количество общих вершин
  • небольшое количество различных нормалей.

Полный пример кода 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 координат против 10 координат
  • 48 нормалей против 4 нормалей.

А вот как это выглядит:

snapshot of test1.uncompressed.obj in RF::SGEdit²

(я не мог видеть никакой визуальной разницы с 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 с + 1,51 с = 1,63 с
  • против. запись без сжатия: 6,2 с

snapshot of test8.compressed.obj in RF::SGEdit²

...