Skip to content

Yocto/ModelIO: Serialization for Obj, Ply and Pbrt models

Yocto/ModelIO is a collection of utilities for loading and saving scenes and meshes in Ply, Obj, Stl and Pbrt formats. Yocto/ModelIO is implemented in yocto_modelio.h and yocto_modelio.cpp.

Ply models

The Ply file format is a generic file format used to serialize meshes and point clouds. To use this library is helpful to understand the basic of the Ply file format for example from the Ply Wikipedia page.

Yocto/ModelIO represents Ply data with the ply_model struct. The internal representation matches the structure of a Ply file and can be accessed directly if desired. The Ply model is defined as an array of Ply elements, which in turn are defined as arrays of Ply properties. Ply properties can contain most C data types. All elements and properties are owned by the main ply_model. Yocto/ModelIO provides several functions to read and write Ply data whose use is preferred over direct data access.

Use ok = load_ply(filename, ply, error) to load Ply files and ok = save_ply(filename, ply, error) to save them.
Both loading and saving take a filename, a reference to a model, and an error string, and returns a boolean that indicated whether the operation is successful.

auto ply = ply_model{};              // ply model
auto error = string{};               // error string
if (!load_ply(filename, ply, error)) // load ply
  handle_error(error); 
if (!save_ply(filename, ply, error)) // save ply
  handle_error(error); 

Ply reading

Yocto/ModelIO defines several functions to make it easy to extract data from Ply files. Since Ply properties cane be stored with many different C types, the convenience functions convert the various underlying representations to the requested one.

Use has_property(ply,element,property) to check whether the model has a property named property in an element named element. Use get_property(ply,element,property) to get that property.

Use get_value(ply, element, property, values) to get the property values and stored them in the array values. The function returns whether or not the property was present. Since Ply properties are often stored together, e.g. xyz coordinates, Yocto/ModelIO supports queries that take arrays of properties and returns values packed in vecXf. Use get_values(ply, element, properties, values) to read arrays of properties.

For list properties, Yocto/ModelIO supports reading properties as arrays of arrays of dynamic size with get_lists(ply,element,property,lists). A faster, but harder to use, method is to get lists sizes and values as separate arrays, where list values are packed together to avoid small memory allocations. Use get_list_sizes(ply, element, property, sizes) for sizes and get_list_values(ply, element, property, values) for values.

auto ply = ply_model{};                 // ply model buffer
auto error = string{};                  // error buffer
load_ply(filename, ply, error);         // load ply

auto radius = vector<float>{};                   // property buffer
if(!get_value(ply, "vertex", "radius", radius))  // read property
  print_error("missing radius");                 // error if missing
auto positions = vector<vec3f>{};                // properties buffer
if(!get_values(ply, "vertex", {"x","y","z"}, positions)) // read properties
  print_error("missing positions");              // error if missing

auto faces = vector<vector<int>>{};              // list property buffer
if(!get_lists(ply, "face", "indices", faces))    // read lists
  print_error("missing faces");                  // error if missing

auto faces_sizes = vector<int>{};                // list property sizes
auto faces_values = vector<int>{};               // list property values
if(!get_list_sizes(ply, "face", "indices", faces_sizes)) // read lists sizes
  print_error("missing faces");                  // error if missing
if(!get_list_values(ply, "face", "indices", faces_values)) // read lists values
  print_error("missing faces");                  // error if missing

Yocto/Shape defines convenience functions to read the most used properties of meshes, using standard element and property names. For vertex properties, use get_positions(ply, positions), get_normals(ply, normals), get_texcoords(ply, texcoords, flipv), get_colors(ply, colors), and get_radius(ply, radius) to read positions, normals, texcoords, colors and radius if present. Texture coordinates can be optionally flipped vertically. For shape elements, use get_points(ply, points), get_lines(ply, lines), get_triangles(ply, triangles), and get_quads(ply, quads), to read points, lines, triangles and quads. Note that since Ply support arbitrary polygons and polylines, these functions tesselate the Ply polygons into the desired element type, for now using a simple fan-like algorithm that works only for convex elements. Use has_quads(ply) to check whether the Ply data has quads, or use get_faces(ply, triangles, quads) to triangles or quads.

auto ply = ply_model{};                 // ply model buffer
auto error = string{};                  // error buffer
load_ply(filename, ply, error);         // load ply

auto positions = vector<vec3f>{};       // vertex properties buffers
auto normals   = vector<vec3f>{};
auto texcoords = vector<vec2f>{};
auto colors    = vector<vec3f>{};
auto radius    = vector<float>{};
get_positions(ply, positions);          // read vertex props.
get_normals(ply, normals);
get_texcoords(ply, texcoords, false);   // last params, flips y is desired
get_colors(ply, colors);
get_radius(ply, radius);

auto points    = vector<vec4i>{};       // shape elements buffers
auto lines     = vector<vec4i>{};
auto triangles = vector<vec4i>{};
auto quads     = vector<vec4i>{};
get_points(ply, points);
get_lines(ply, lines);
if(has_quads(ply)) get_quads(ply, quads);
else get_triangles(ply, triangles);

Ply writing

Yocto/ModelIO defines several functions to make it easy to fill Ply data to save to file. Since Ply properties cane be stored with many different C types, the convenience functions maintain the same data type of the data passed in without performing any conversion.

Use add_value(ply, element, property, values) to add the property values stored them in the array values. The function returns whether or not the property was successfully added. Since Ply properties are often stored together, e.g. xyz coordinates, Yocto/ModelIO supports adding arrays of properties whose values are packed in vecXf. Use add_values(ply, element, properties, values) to add arrays of properties.

For list properties, Yocto/ModelIO supports adding list properties as arrays of arrays of dynamic size with add_lists(ply,element,property,lists). A faster, but harder to use, method is to add lists sizes and values as separate arrays, where list values are packed together to avoid small memory allocations. Use add_lists(ply, element, property, sizes, values). Finally, Yocto/ModelIO supports adding lists of fixed lengths, where the parameters are packed into vecXi, with add_lists(ply,element,property,values).

auto ply = ply_model{};               // ply model buffer

auto radius = vector<float>{...};                // property buffer
if(!add_value(ply, "vertex", "radius", radius))  // add property
  print_error("error in radius");                // error if missing
auto positions = vector<vec3f>{...};             // properties buffer
if(!add_values(ply, "vertex", {"x","y","z"}, positions)) // add properties
  print_error("missing positions");              // error if missing

auto faces = vector<vector<int>>{...};           // list property buffer
if(!add_lists(ply, "face", "indices", faces))    // add lists
  print_error("missing faces");                  // error if missing

auto faces_sizes = vector<int>{...};             // list property sizes
auto faces_values = vector<int>{...};            // list property values
if(!add_lists(ply, "face", "indices", faces_sizes, face_values)) // add lists
  print_error("missing faces");                  // error if missing

auto triangles = vector<vec3i>{...};             // fixed length list property
if(!add_lists(ply, "face", "indices", triangles))// add lists
  print_error("missing faces");                  // error if missing

auto error = string{};            // error buffer
save_ply(filename, ply, error);   // save ply

Yocto/ModelIO defines convenience functions to add the most used properties of meshes, using standard element and property names. For vertex properties, use add_positions(ply, positions), add_normals(ply, normals), add_texcoords(ply, texcoords, flipv), add_colors(ply, colors), and add_radius(ply, radius) to read positions, normals, texcoords, colors and radius if present. Texture coordinates can be optionally flipped vertically. For shape elements, use add_points(ply, points), add_lines(ply, lines), add_triangles(ply, triangles), and add_quads(ply, quads), to add points, lines, triangles and quads. Use add_faces(ply, faces) to add arbitrary polygonal faces.

auto ply = ply_model{};                 // ply model buffer

auto positions = vector<vec3f>{...};    // vertex properties buffers
auto normals   = vector<vec3f>{...};
auto texcoords = vector<vec2f>{...};
auto colors    = vector<vec3f>{...};
auto radius    = vector<float>{...};
add_positions(ply, positions);          // add vertex props.
add_normals(ply, normals);
add_texcoords(ply, texcoords, false);   // last params, flips y is desired
add_colors(ply, colors);
add_radius(ply, radius);

auto points    = vector<vec4i>{...};    // shape elements buffers
auto lines     = vector<vec4i>{...};
auto triangles = vector<vec4i>{...};
auto quads     = vector<vec4i>{...};
add_points(ply, points);                // add shape elements
add_lines(ply, lines);
add_triangles(ply, triangles);
add_quads(ply, quads);

auto error = string{};                  // error buffer
save_ply(filename, ply, error);         // save ply

Obj models

The Obj file format is a file format used to serialize meshes and materials. To use this library is helpful to understand the basic of the Obj file format for example from the Obj Wikipedia page. Obj files come in pairs, .obj for shapes and .mtl for materials.

Yocto/ModelIO represents Obj data with the obj_model struct. Obj models are defined as collections of shapes and materials. Obj shapes use a face-varying representation that has vertex positions, normals and texture coordinates, with their their own topology. In a shape, each face is tagged with a material used for that face. Yocto/ModelIO provides direct access to these tagged shapes by inspecting the obj_shape properties.

In Yocto/Obj, materials are represented by the obj_material struct. Each material is a collection of values and textures that specify the material lobes, like emission, diffuse, specular, reflection, etc. Each value has a corresponding texture stored as an index to the texture array.

Yocto/ModelIO defines two extensions to the Obj file format. Yocto/ModelIO adds another file, namely .obx, that stores cameras and environment maps, respectively as obj_camera and obj_environment. T his addition makes the extended Obj format capable of storing full scenes.

The Obj model is defined as an array of objects of the types defined above. Obj objects are pointers owned by the main obj_model. Objects properties can be read and written directly from the model data, and are documented in the header file for now. For shapes, Yocto/ModelIO provides several functions to read and write Obj shapes, with a simpler interface than accessing data directly.

auto obj = obj_model{...};                // obj model buffer
for(auto shape : obj.shapes)              // access shapes
  print_info(shape.name);                 // access shape properties
for(auto material : obj.material)         // access materials
  print_info(material.diffuse);           // access material properties
for(auto material : obj.material)         // access materials
  print_info(material.diffuse_tex);       // access material textures
for(auto camera : obj.cameras)            // access cameras [extension]
  print_info(camera.frame);               // access camera properties
for(auto environment : obj.environments)  // access environments [extension]
  print_info(environment.emission);       // access environment properties

Use ok = load_obj(filename, obj, error) to load Obj files and ok = save_obj(filename, obj, error) to save them. Both loading and saving take a filename, a reference to a model, and an error string, and returns a boolean that indicated whether the operation is successful.

Obj is a face-varying file format, while most applications handle only indexed meshes. The loading function takes an optional that specify whether to load as face-varying or convert to indexed meshes, which is the default.

auto obj = obj_model{};              // obj model
auto error = string{};               // error string
if (!load_obj(filename, obj, error)) // load obj
  handle_error(error); 
if (!save_obj(filename, obj, error)) // save obj
  handle_error(error); 

It is common in graphics to use Obj file to store single meshes. Yocto/Obj supports this modality by providing specialized loading and saving functions that take references to shapes as parameters. Use ok = load_obj(filename, shape, error) to load Obj shapes and ok = save_obj(filename, shape, error) to save them. The loading function takes an optional that specify whether to load as face-varying or convert to indexed meshes, which is the default.

auto oshape = obj_shape{};              // obj shape
auto error = string{};                  // error string
if (!load_obj(filename, oshape, error)) // load obj shape
  handle_error(error); 
if (!save_obj(filename, oshape, error)) // save obj shape
  handle_error(error); 

Obj reading

Obj is a face-varying format and that geometry representation is maintained in obj_shape. Yocto/ModelIO provides easier accessed to Obj shape data, both as indexed meshes and as face-varying meshes.

Use get_positions(shape, positions), get_normals(shape, normals), get_texcoords(shape, texcoords, flipv), to read positions, normals, texcoords, if present. Texture coordinates can be optionally flipped vertically. For shape elements, use get_points(shape, points, materials), get_lines(shape, lines, materials), get_triangles(shape, triangles, materials), and get_quads(shape, quads, materials), to read points, lines, triangles and quads. Note that since Obj support arbitrary polygons and polylines, these functions tesselate the Obj polygons into the desired element type, for now using a simple fan-like algorithm that works only for convex elements. Use has_quads(shape) to check whether the Obj data has quads, or use get_faces(shape, triangles, quads, materials) to triangles or quads.

In some cases, it may be desireable to extract the shape elements corresponding to a single material, for example for use in renderers that support a single shader per shape. To filtering elements by material index use get_points(shape, material, points), get_lines(shape, material, lines), get_triangles(shape, material, triangles), and get_quads(shape, material, quads). that are overrides of the previous get_<element>(...) functions, but differ in that they take a material is as input, instead of returning materials tags.

auto obj = obj_model{};                // obj model buffer
auto error = string{};                 // error buffer
load_obj(filename, obj, error);        // load obj
auto& shape = obj.shapes.front();      // get shape

auto positions = vector<vec3f>{};      // vertex properties
get_positions(shape, positions);
auto normals   = vector<vec3f>{};
get_normals(shape, normals);
auto texcoords = vector<vec2f>{};
get_texcoords(shape, texcoords);
auto triangles = vector<vec3i>{};      // element data
auto quads     = vector<vec4i>{};
auto materials = vector<int>{};        // per-face material ids
if(has_quads(shape)) {
  get_triangles(shape, triangles, materials); // read as triangles
} else {
  get_quads(shape, quads, materials);   // read as quads
}

auto material_id = 0;                     // material id to extract to
if(has_quads(shape)) {
  get_triangles(shape, material_id, triangles); // read as triangles
} else {
  get_quads(shape, material_id, quads);   // read as quads
}

Yocto/ModelIO supports also reading Obj shapes as face-varying quads with get_fvquads(...). In this case, the model should be loaded as face-varying.

auto obj =  obj_model{};                // obj model buffer
auto error = string{};                  // error buffer
load_obj(filename, obj, error, true);   // load obj as face-varying
auto& shape = obj.shapes.front();       // get shape

auto positions = vector<vec3f>{};       // vertex properties
get_positions(shape, positions);
auto normals   = vector<vec3f>{};
get_normals(shape, normals);
auto texcoords = vector<vec2f>{};
get_texcoords(shape, texcoords);
auto quadspos  = vector<vec4i>{};       // face-varying element data
auto quadsnorm = vector<vec4i>{};
auto quadsuv   = vector<vec4i>{};
auto materials = vector<int>{};         // per-face material ids
get_fvquads(shape,                      // read as face-varying quads
  quadspos, quadsnorm, quadsuv, materias);

Obj writing

To save an Obj, create a scene and add objects to it by manipulating the objects' arrays directly. For all objects, set the objects' properties directly.

For shapes, Yocto/ModelIO defines convenience functions that take either indexed mesh or face-varying meshes as input and create the appropriate Obj shape elements. Use add_positions(shape, positions), add_normals(shape, normals) and add_texcoords(shape, texcoords, flipv) to add vertex properties to a shape. Use add_triangles(shape, triangles, material, has_normals, has_texcoords), add_quads(shape, quads, material, has_normals, has_texcoords), add_lines(shape, lines, material, has_normals, has_texcoords), add_points(shape, points, material, has_normals, has_texcoords) to add triangles, quads, lines or points to shapes respectively. Material data is only represented as tags and can be left empty if only one material is used. To set material names use set_materials(shape, materials). To add face-varying shapes, use add_fvquads(...).

auto obj =  obj_model{};                 // obj model

auto camera = obj_camera{};              // init camera
camera.name = "camera";                  // set camera name
camera.frame = identity3x4f;             // set camera properties
obj.cameras.push_back(camera);           // add camera
auto environment = add_environment(obj); // init environment
environment.name = "environment";        // set environment name
environment.emission = {1,1,1};          // set environment properties
obj.environments.push_back(environment); // add environment
auto material = add_material(obj);       // init material
material.name = "material";              // set material name
material.diffuse = {1,0,0};              // set material properties
obj.materials.push_back(material);       // add material

auto triangles = vector<vec3i>{...};     // element data
auto positions = vector<vec3f>{...};     // vertex properties
auto normals   = vector<vec3f>{...};
auto texcoords = vector<vec2f>{...};

auto shape = obj_shape{};                // init shape
shape.name = "shape";                    // set shape name
add_triangles(shape, triangles, 0);      // set shape geometry
add_positions(shape, positions);
add_normals(shape, normals);
add_texcoords(shape, texcoords);

auto error = string{};                  // error buffer
save_obj(filename, obj, error);         // save obj

And similarly for face-varying shapes.

auto obj = obj_model{};                 // obj model buffer

auto quadspos  = vector<vec4i>{...};    // face-varying element data
auto quadsnorm = vector<vec4i>{...};
auto quadsuv   = vector<vec4i>{...};
auto positions = vector<vec3f>{...};    // vertex properties
auto normals   = vector<vec3f>{...};
auto texcoords = vector<vec2f>....{};

auto shape = obj_shape{};               // add shape
shape.name = "shape";                   // set shape name
add_fvquads(shape, quadspos,            // set shape geometry
  quadsnorm, quadstexcoord, 0);
add_positions(shape, positions);
add_normals(shape, normals);
add_texcoords(shape, texcoords);

auto error = string{};                  // error buffer
save_obj(filename, obj, error);         // save obj

Stl models

The Stl file format is a file format used to serialize meshes. To use this library is helpful to understand the basic of the Stl file format for example from the Stl Wikipedia page.

Yocto/ModelIO represents Stl data with the stl_model struct. Stl models are defined as collections of shapes, stored in stl_shape. Shapes are defined by an array of vertex positions, an array of triangle indices, and an array of triangle normals.

Use ok = load_stl(filename, stl, error) to load Stl files and ok = save_stl(filename, stl, error) to save them.

auto stl = stl_model{};              // stl model
auto error = string{};               // error string
if (!load_stl(filename, stl, error)) // load stl
  handle_error(error); 
if (!save_stl(filename, stl, error)) // save stl
  handle_error(error); 

Stl reading

You can access Stl data directly from the shapes, without any further complexity.

auto stl = stl_model{};                // stl model buffer
load_obj(filename, stl);               // load stl
auto& shape = stl.shapes.front();      // get shape

auto positions = shape.positions;      // vertex properties
auto triangles = shape.triangles;      // element data

Stl writing

Similarly, you can create an Stl model by acting directly on the data.

auto stl = stl_model{};                // stl model buffer
auto shape = stl_shape{};              // create shape
shape.positions = vector<vec3f>{...};
shape.triangles = vector<vec3i>{...};
stl.shapes.push_back(shape);           // add shape
load_obj(filename, stl);               // save stl

Pbrt models

The Pbrt file format is a scene representation suitable for realistic rendering and implemented by the Pbrt renderer. To use this library is helpful to understand the basic of the Pbrt file format for example from the Pbrt format documentatioon.

The Pbrt file format is an extensible file format for a plugin-based system. Representing the format directly allows for best fidelity but pushed the burden of interpreting standard plugins to the use. Yocto/ModelIO takes a different approach and translates camera, shapes, materials, textures and lights from Pbrt plugins to a common representation that presents users a simpler and more uniform scene representation.

Yocto/ModelIO represents Pbrt data with the pbrt_scene struct. Pbrt models are defined as collections of cameras, instanced shapes, materials, texture and environments. Pbrt cameras are translate into a thin-len approximations. Pbrt materials are translated to a material representation similar to the Disney BSDF. Pbrt textures are either interpreted ion the fly or defined by a image file. Pbrt area lights are translated to either emissive materials and environments.

The Pbrt model is defined as an array of objects of the types defined above. Pbrt objects are pointers owned by the main pbrt_scene. Objects properties can be read and written directly from the model data, and are documented in the header file for now. Yocto/ModelIO does not currently provide functions to read and write Pbrt shapes with a simpler interface than accessing data directly.

In general, Pbrt support is still experimental even if the library can parse most Pbrt files. The objects properties documentations are for now stored in the header file.

auto pbrt = new pbrt_scene{...};            // obj model buffer
for(auto shape : pbrt.shapes)              // access shapes
  print_info(shape.name);                  // access shape properties
for(auto material : pbrt.material)         // access materials
  print_info(material.diffuse);            // access material properties
for(auto material : pbrt.material)         // access materials
  print_info(material.color_tex);          // access material textures
for(auto camera : pbrt.cameras)            // access cameras [extension]
  print_info(camera.frame);                // access camera properties
for(auto environment : pbrt.environments)  // access environments [extension]
  print_info(environment.emission);        // access environment properties

Use ok = load_pbrt(filename, pbrt, error) to load Pbrt files and ok = save_pbrt(filename, pbrt, error) to save them.
Both loading and saving take a filename, a reference to a model, and an error string, and returns a boolean that indicated whether the operation is successful.

auto pbrt = pbrt_model{};              // pbrt model
auto error = string{};                 // error string
if (!load_pbrt(filename, pbrt, error)) // load pbrt
  handle_error(error); 
if (!save_pbrt(filename, pbrt, error)) // save pbrt
  handle_error(error);