A fast and easy-to-use library for parsing PLY files, in a single c++11 header and cpp file with no external dependencies, ready to drop into your project.
- Fast: loads all 8929 PLY files from the pbrt-v3-scenes repository in under 9 seconds total - an average parsing time of less than 1 millisecond per file!
- Small: just a single .h and .cpp file which you can copy into your project.
- Complete: parses ASCII, binary little-endian and binary big-endian versions of the file format (binary loading assumes you're running on a little-endian CPU).
- Cross-platform: works on Windows, macOS and Linux.
- Provides helper methods for getting standard vertex and face properties.
- Can triangulate polygons as they're loaded.
- Fast path for models where you know every face has the same fixed number of vertices
- MIT license
Note that miniply does not support writing PLY files, only reading them.
- Copy
miniply.h
andminiply.cpp
into your project. - Add
#include <miniply.h>
wherever necessary.
The CMake file that you see in this repo is purely for building the miniply-info
and miniply-perf
command line tools in the extra
folder; it isn't required if
you're just using the library in your own project.
The general usage model for this library is:
- Construct a
PLYReader
object. This will open the PLY file and read the header. - Call
reader.valid()
to check that the file was opened successfully. - Iterate over the elements using something like
for (; reader.has_element(); reader.next_element()) { ... }
, callingreader.element_is()
to check whether the current element is one that you want data from. - For any elements that you're interested in:
- Call
reader.load_element()
. - Use some combination of the
extract_columns()
,extract_list_column()
andextract_triangles()
methods to get the data you're interested in.
- Call
You can only iterate forwards over the elements in the file (at present). You cannot jump back to an earlier element.
You can skip forward to the next element simply by calling next_element()
without
having called load_element()
yet. This will be very efficient if the current element
is fixed-size. If the current element contains any list properties then we will have to
scan through all of the element data to find where it finishes and the next element
starts, which is not as efficient (although the library will do it's best!).
This function (taken directly from extra/miniply-info.cpp) shows how you can get the header from a .ply file:
bool print_ply_header(const char* filename)
{
miniply::PLYReader reader(filename);
if (!reader.valid()) {
fprintf(stderr, "Failed to open %s\n", filename);
return false;
}
printf("ply\n");
printf("format %s %d.%d\n", kFileTypes[int(reader.file_type())],
reader.version_major(), reader.version_minor());
for (uint32_t i = 0, endI = reader.num_elements(); i < endI; i++) {
const miniply::PLYElement* elem = reader.get_element(i);
printf("element %s %u\n", elem->name.c_str(), elem->count);
for (const miniply::PLYProperty& prop : elem->properties) {
if (prop.countType != miniply::PLYPropertyType::None) {
printf("property list %s %s %s\n", kPropertyTypes[uint32_t(prop.countType)],
kPropertyTypes[uint32_t(prop.type)], prop.name.c_str());
}
else {
printf("property %s %s\n", kPropertyTypes[uint32_t(prop.type)], prop.name.c_str());
}
}
}
printf("end_header\n");
return true;
}
Polygons in PLY files are ordinarily stored in a list property, meaning that each
polygon is a variable-length list of vertex indices. If the mesh representation in
your program is triangles-only, you will need to triangulate the faces. miniply
has built-in support for this:
Note that if you know in advance that your PLY file only contains triangles, there is a much faster way to load it. See below for details.
// Very basic triangle mesh struct, for example purposes
struct TriMesh {
// Per-vertex data
float* pos = nullptr; // has 3 * numVerts elements.
float* uv = nullptr; // if non-null, has 2 * numVerts elements.
uint32_t numVerts = 0;
// Per-index data
int* indices = nullptr;
uint32_t numIndices = 0; // number of indices = 3 times the number of triangles.
};
TriMesh* load_trimesh_from_ply(const char* filename)
{
miniply::PLYReader reader(filename);
if (!reader.valid()) {
return nullptr;
}
uint32_t indexes[3];
bool gotVerts = false, gotFaces = false;
TriMesh* trimesh = new TriMesh();
while (reader.has_element() && (!gotVerts || !gotFaces)) {
if (reader.element_is(miniply::kPLYVertexElement) && reader.load_element() && reader.find_pos(indexes)) {
trimesh->numVerts = reader.num_rows();
trimesh->pos = new float[trimesh->numVerts * 3];
reader.extract_properties(indexes, 3, miniply::PLYPropertyType::Float, trimesh->pos);
if (reader.find_texcoord(indexes)) {
trimesh->uv = new float[trimesh->numVerts * 2];
reader.extract_properties(indexes, 2, miniply::PLYPropertyType::Float, trimesh->uv);
}
gotVerts = true;
}
else if (reader.element_is(miniply::kPLYFaceElement) && reader.load_element() && reader.find_indices(indexes)) {
bool polys = reader.requires_triangulation(indexes[0]);
if (polys && !gotVerts) {
fprintf(stderr, "Error: need vertex positions to triangulate faces.\n");
break;
}
if (polys) {
trimesh->numIndices = reader.num_triangles(indexes[0]) * 3;
trimesh->indices = new int[trimesh->numIndices];
reader.extract_triangles(indexes[0], trimesh->pos, trimesh->numVerts, miniply::PLYPropertyType::Int, trimesh->indices);
}
else {
trimesh->numIndices = reader.num_rows() * 3;
trimesh->indices = new int[trimesh->numIndices];
reader.extract_list_property(indexes[0], miniply::PLYPropertyType::Int, trimesh->indices);
}
gotFaces = true;
}
if (gotVerts && gotFaces) {
break;
}
reader.next_element();
}
if (!gotVerts || !gotFaces) {
delete trimesh;
return nullptr;
}
return trimesh;
}
For a more complete example, see extra/miniply-perf.cpp
Loading the vertex indices for each face from a variable length list is a bit
wasteful if you know ahread of time that your PLY file only contains triangles.
With miniply
you can take advantage of this knowledge to get a massive
reduction in the loading time for the file.
The idea is to replace the single list property, which miniply has to treat as
variable-sized, with a set of fixed-size properties. There will be one
property corresponding to the item count for each list (which we will ignore
during loading, because we know it will always be three), followed by three
new properties (one for each list index). You do this by calling
convert_list_to_fixed_size()
on the face element at some point prior to
loading its data.
Doing this allows miniply
to use its far more efficient code path for loading
fixed-size elements instead. This can cut loading times by more than half!
// Note: using the same TriMesh class as the example above, omitting it here
// for the sake of brevity.
TriMesh* load_trimesh_from_triangles_only_ply(const char* filename)
{
miniply::PLYReader reader(filename);
if (!reader.valid()) {
return nullptr;
}
uint32_t faceIdxs[3];
miniply::PLYElement* faceElem = reader.get_element(reader.find_element(miniply::kPLYFaceElement));
if (faceElem == nullptr) {
return nullptr;
}
faceElem->convert_list_to_fixed_size(faceElem->find_property("vertex_indices"), 3, faceIdxs);
uint32_t indexes[3];
bool gotVerts = false, gotFaces = false;
TriMesh* trimesh = new TriMesh();
while (reader.has_element() && (!gotVerts || !gotFaces)) {
if (reader.element_is(miniply::kPLYVertexElement) && reader.load_element() && reader.find_pos(indexes)) {
// This section is the same as the example above, not repeating it here.
}
else if (!gotFaces && reader.element_is(miniply::kPLYFaceElement) && reader.load_element()) {
trimesh->numIndices = reader.num_rows() * 3;
trimesh->indices = new int[trimesh->numIndices];
reader.extract_properties(faceIdxs, 3, miniply::PLYPropertyType::Int, trimesh->indices);
gotFaces = true;
}
if (gotVerts && gotFaces) {
break;
}
reader.next_element();
}
if (!gotVerts || !gotFaces) {
delete trimesh;
return nullptr;
}
return trimesh;
}
To recap, the differences from the previous example are:
- We're calling
faceElem->convert_list_to_fixed_size()
up front. - In the section which processes the face element, we're calling
reader.extract_properties()
to get the index data instead ofreader.extract_triangles()
orreader.extrat_list_property()
.
I originally wrote the code that became miniply as part of minipbrt. I looked at several existing PLY parsing libraries first, but (a) I really wanted to keep minipbrt dependency free; and (b) I already had a lot of parsing code lying around just begging to be reused. In the end, the code I wrote for minipbrt seemed complete enough to be worth publishing as a standalone library too. Since then I've refined the API, improved performance and reduced memory usage. I hope you like the result. :-)
The ply-parsing-perf repo has a detailed performance comparison between miniply and a number of other ply parsing libraries.
Overall miniply
is between 2 and 8 times faster than all the other parsers I've
tested, for that workload (creating a simple poly mesh from each ply file).
See also Maciej Halber's ply_io_benchmark (thanks to Dimitri Diakopoulos for the pointer!) for more performance comparisons.
If miniply doesn't meet your needs, perhaps one of these other great PLY parsing libraries will?
In particular these all support writing as well as reading, whereas
miniply
only supports reading.
GitHub issues: https://github.com/vilya/miniply/issues
If you're using miniply and find it useful, drop me an email - I'd love to know about it!