Skip to content

Commit f23db78

Browse files
author
Tommy Hinks
committed
merge
2 parents 84c0add + 6933543 commit f23db78

21 files changed

+916
-13838
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "external/Catch2"]
2+
path = external/Catch2
3+
url = https://github.com/catchorg/Catch2.git

.vscode/c_cpp_properties.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"configurations": [
3+
{
4+
"name": "Win32",
5+
"includePath": [
6+
"${workspaceFolder}/**"
7+
],
8+
"defines": [
9+
"_DEBUG",
10+
"UNICODE",
11+
"_UNICODE"
12+
],
13+
"windowsSdkVersion": "10.0.17763.0",
14+
"compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Tools/MSVC/14.20.27508/bin/Hostx64/x64/cl.exe",
15+
"cStandard": "c11",
16+
"cppStandard": "c++17",
17+
"intelliSenseMode": "msvc-x64",
18+
"configurationProvider": "vector-of-bool.cmake-tools",
19+
"compileCommands": "${workspaceFolder}/build/compile_commands.json"
20+
}
21+
],
22+
"version": 4
23+
}

.vscode/settings.json

+68
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,72 @@
33
"cmake.ctestArgs": [
44
"--verbose"
55
],
6+
"files.associations": {
7+
"algorithm": "cpp",
8+
"array": "cpp",
9+
"cctype": "cpp",
10+
"chrono": "cpp",
11+
"cmath": "cpp",
12+
"cstddef": "cpp",
13+
"cstdint": "cpp",
14+
"cstdio": "cpp",
15+
"cstdlib": "cpp",
16+
"cstring": "cpp",
17+
"ctime": "cpp",
18+
"cwchar": "cpp",
19+
"deque": "cpp",
20+
"exception": "cpp",
21+
"fstream": "cpp",
22+
"functional": "cpp",
23+
"initializer_list": "cpp",
24+
"iomanip": "cpp",
25+
"ios": "cpp",
26+
"iosfwd": "cpp",
27+
"iostream": "cpp",
28+
"istream": "cpp",
29+
"iterator": "cpp",
30+
"limits": "cpp",
31+
"list": "cpp",
32+
"locale": "cpp",
33+
"map": "cpp",
34+
"memory": "cpp",
35+
"new": "cpp",
36+
"optional": "cpp",
37+
"ostream": "cpp",
38+
"random": "cpp",
39+
"ratio": "cpp",
40+
"regex": "cpp",
41+
"set": "cpp",
42+
"sstream": "cpp",
43+
"stack": "cpp",
44+
"stdexcept": "cpp",
45+
"streambuf": "cpp",
46+
"string": "cpp",
47+
"string_view": "cpp",
48+
"system_error": "cpp",
49+
"tuple": "cpp",
50+
"type_traits": "cpp",
51+
"typeinfo": "cpp",
52+
"unordered_map": "cpp",
53+
"utility": "cpp",
54+
"variant": "cpp",
55+
"vector": "cpp",
56+
"xfacet": "cpp",
57+
"xhash": "cpp",
58+
"xiosbase": "cpp",
59+
"xlocale": "cpp",
60+
"xlocbuf": "cpp",
61+
"xlocinfo": "cpp",
62+
"xlocmes": "cpp",
63+
"xlocmon": "cpp",
64+
"xlocnum": "cpp",
65+
"xloctime": "cpp",
66+
"xmemory": "cpp",
67+
"xmemory0": "cpp",
68+
"xstddef": "cpp",
69+
"xstring": "cpp",
70+
"xtr1common": "cpp",
71+
"xtree": "cpp",
72+
"xutility": "cpp"
73+
},
674
}

CMakeLists.txt

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ cmake_minimum_required(VERSION 3.1)
66
project(obj_io)
77

88
set(header_files
9-
${CMAKE_CURRENT_SOURCE_DIR}/include/thinks/obj_io/obj_io.h
9+
${CMAKE_CURRENT_SOURCE_DIR}/include/thinks/obj_io/obj_io.h
1010
)
1111
add_library(thinks_obj_io INTERFACE)
12+
add_library(thinks::obj_io ALIAS thinks_obj_io)
1213
target_sources(thinks_obj_io INTERFACE ${header_files})
1314
target_include_directories(thinks_obj_io INTERFACE include)
1415

1516
if($<LOWER_CASE:${CMAKE_CURRENT_SOURCE_DIR}> STREQUAL
1617
$<LOWER_CASE:${CMAKE_SOURCE_DIR}>)
1718
message(STATUS "obj-io: enable testing")
1819
enable_testing()
20+
add_subdirectory(external/Catch2)
1921
add_subdirectory(test)
20-
add_subdirectory(examples)
22+
add_subdirectory(examples)
2123
endif()

README.md

+241
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<<<<<<< HEAD
12
# OBJ-IO
23
This repository contains a [single-file](https://github.com/thinks/obj-io/blob/master/include/thinks/obj_io/obj_io.h), header-only, no-dependencies C++ implementation of the [OBJ file format](https://en.wikipedia.org/wiki/Wavefront_.obj_file). All code in this repository is released under the [MIT license](https://en.wikipedia.org/wiki/MIT_License), as per the included [license file](https://github.com/thinks/obj-io/blob/master/LICENSE). The code herein has not been optimized for speed, but rather for generality, readability, and robustness.
34

@@ -261,3 +262,243 @@ For more detailed test output locate the test executable (_thinks_obj_io_test.ex
261262
## Future Work
262263
* _Improved read performance_ - The current implementation is rather naive in that it reads only a single line at a time. Additionally, many operations are done using `std::string` operations, which is not ideal performance-wise.
263264
* _Optional validation_ - It would be nice to have optional mechanisms to perform validation such as checking that face indices are within the range of the other attributes. However, this has recieved low priority since it can easily be done by the user before/after reading/writing.
265+
=======
266+
# OBJ-IO
267+
This repository contains a [single-file](https://github.com/thinks/obj-io/blob/master/include/thinks/obj_io/obj_io.h), header-only, no-dependencies C++ implementation of the [OBJ file format](https://en.wikipedia.org/wiki/Wavefront_.obj_file). All code in this repository is released under the [MIT license](https://en.wikipedia.org/wiki/MIT_License), as per the included [license file](https://github.com/thinks/obj-io/blob/master/LICENSE). The code herein has not been optimized for speed, but rather for generality, readability, and robustness.
268+
269+
Those in need of the provided tools are likely to have written simple functions for writing (and/or reading) OBJ files at some point. Such utilities, however, were probably hard-coded for the mesh class at hand and not easily generalizable. Even though the OBJ format is embarrasingly simple there are a few pit-falls (did you forget that OBJ files use one-based indexing?). The goal here is to use the type system available in C++ to make it so that writing erroneous code becomes exceedingly difficult. The price to pay for this is learning yet another API, which in this case translates to a handful of functions, as shown in the examples below.
270+
271+
## The OBJ File Format
272+
The [OBJ file format](https://en.wikipedia.org/wiki/Wavefront_.obj_file) is ubiquitous in the field of computer graphics. While it is arguably not the most efficient way to store meshes on disk, its simplicity has made it widely supported. The OBJ file format is extremely useful for debugging and for transferring meshes between different software packages.
273+
274+
It should be noted that we currently only support _geometric vertices_ (i.e. positions), _texture coordinates_, _normals_, and _face elements_. These are the most fundamental properties of meshes and should cover the vast majority of use cases.
275+
276+
## Examples
277+
Mesh representations vary wildly across different frameworks. It seems fairly likely that most frameworks have their own representation. Because of this, our distribution provides methods for reading and writing OBJ files assuming no knowledge of a mesh class. Instead, our methods rely on callbacks to extract the required information. As such, our methods act more as middle-ware than an out-of-the-box solution. While this approach requires some additional work for users, it provides great flexibility and arguably makes this distribution more usable in the long run. Before showing a simple example it should be noted that several complete examples are available in the [examples](https://github.com/thinks/obj-io/tree/master/examples) folder.
278+
279+
A simple example illustrates how to read and write a mesh using our method. Let's assume we have the following simple mesh class.
280+
```cpp
281+
struct Vec2 {
282+
float x;
283+
float y;
284+
};
285+
286+
struct Vec3 {
287+
float x;
288+
float y;
289+
float z;
290+
};
291+
292+
struct Vertex {
293+
Vec3 position;
294+
Vec2 tex_coord;
295+
Vec3 normal;
296+
};
297+
298+
struct Mesh {
299+
std::vector<Vertex> vertices;
300+
std::vector<std::uint16_t> indices;
301+
};
302+
```
303+
This type of layout is common since it fits nicely with several widely used APIs for uploading a mesh to the GPU for rendering. Now, let's assume that we have an OBJ file from which we want to populate a mesh. A simple implementation could be as follows.
304+
```cpp
305+
//#include relevant std headers.
306+
307+
#include "thinks/obj_io/obj_io.h"
308+
309+
Mesh ReadMesh(const std::string& filename) {
310+
auto mesh = Mesh{};
311+
312+
// We cannot assume the order in which callbacks are invoked,
313+
// so we need to keep track of which vertex to add properties to.
314+
// The first encountered position gets added to the first vertex, etc.
315+
auto pos_count = std::uint32_t{0};
316+
auto tex_count = std::uint32_t{0};
317+
auto nml_count = std::uint32_t{0};
318+
319+
// Positions.
320+
// Wrap a lambda expression and set expectations on position data.
321+
// In this case we expect each position to be 3 floating point values.
322+
auto add_position = thinks::MakeObjAddFunc<thinks::ObjPosition<float, 3>>(
323+
[&mesh, &pos_count](const auto& pos) {
324+
// Check if we need a new vertex.
325+
if (mesh.vertices.size() <= pos_count) {
326+
mesh.vertices.push_back(Vertex{});
327+
}
328+
329+
// Write the position property of current vertex and
330+
// set position index to next vertex. Values are translated
331+
// from OBJ representation to our vector class.
332+
mesh.vertices[pos_count++].position =
333+
Vec3{pos.values[0], pos.values[1], pos.values[2]};
334+
});
335+
336+
// Faces.
337+
// We expect each face in the OBJ file to be a triangle, i.e. have three
338+
// indices. Also, we expect each index to have only one value.
339+
using ObjFaceType = thinks::ObjTriangleFace<thinks::ObjIndex<std::uint16_t>>;
340+
auto add_face =
341+
thinks::MakeObjAddFunc<ObjFaceType>([&mesh](const auto& face) {
342+
// Add triangle indices into the linear storage of our mesh class.
343+
mesh.indices.push_back(face.values[0].value);
344+
mesh.indices.push_back(face.values[1].value);
345+
mesh.indices.push_back(face.values[2].value);
346+
});
347+
348+
// Texture coordinates [optional].
349+
auto add_tex_coord = thinks::MakeObjAddFunc<
350+
thinks::ObjTexCoord<float, 2>>([&mesh, &tex_count](const auto& tex) {
351+
if (mesh.vertices.size() <= tex_count) {
352+
mesh.vertices.push_back(Vertex{});
353+
}
354+
mesh.vertices[tex_count++].tex_coord = Vec2{tex.values[0], tex.values[1]};
355+
});
356+
357+
// Normals [optional].
358+
// Note: Normals must always have 3 components.
359+
auto add_normal = thinks::MakeObjAddFunc<thinks::ObjNormal<float>>(
360+
[&mesh, &nml_count](const auto& nml) {
361+
if (mesh.vertices.size() <= nml_count) {
362+
mesh.vertices.push_back(Vertex{});
363+
}
364+
mesh.vertices[nml_count++].normal =
365+
Vec3{nml.values[0], nml.values[1], nml.values[2]};
366+
});
367+
368+
// Open the OBJ file and populate the mesh while parsing it.
369+
auto ifs = std::ifstream(filename);
370+
assert(ifs);
371+
const auto result =
372+
thinks::ReadObj(ifs, add_position, add_face, add_tex_coord, add_normal);
373+
ifs.close();
374+
375+
// Some sanity checks.
376+
assert(result.position_count == result.tex_coord_count &&
377+
result.position_count == result.normal_count &&
378+
"incomplete vertices in file");
379+
assert(result.position_count == mesh.vertices.size() && "bad position count");
380+
assert(result.tex_coord_count == mesh.vertices.size() &&
381+
"bad tex_coord count");
382+
assert(result.normal_count == mesh.vertices.size() && "bad normal count");
383+
assert(pos_count == tex_count && pos_count == nml_count &&
384+
"all vertices must be completely initialized");
385+
386+
return mesh;
387+
}
388+
389+
```
390+
A nice feature of reading a mesh this way is that we avoid memory spikes. The mesh data is never duplicated, as it might be if the `Read` method were to build its own internal representation of the mesh. Also, note that the `Read` method has no knowledge of the `Mesh` class itself, it simply calls the provided lambdas while parsing the OBJ file. Writing a mesh is done in a similar fashion.
391+
```cpp
392+
//#include relevant std headers.
393+
394+
#include "thinks/obj_io/obj_io.h"
395+
396+
void WriteMesh(const std::string& filename, const Mesh& mesh) {
397+
const auto vtx_iend = std::end(mesh.vertices);
398+
399+
// Mappers have two responsibilities:
400+
// (1) - Iterating over a certain attribute of the mesh (e.g. positions).
401+
// (2) - Translating from users types to OBJ types (e.g. Vec3 ->
402+
// Position<float, 3>)
403+
404+
// Positions.
405+
auto pos_vtx_iter = std::begin(mesh.vertices);
406+
auto pos_mapper = [&pos_vtx_iter, vtx_iend]() {
407+
using ObjPositionType = thinks::ObjPosition<float, 3>;
408+
409+
if (pos_vtx_iter == vtx_iend) {
410+
// End indicates that no further calls should be made to this mapper,
411+
// in this case because the captured iterator has reached the end
412+
// of the vector.
413+
return thinks::ObjEnd<ObjPositionType>();
414+
}
415+
416+
// Map indicates that additional positions may be available after this one.
417+
const auto pos = (*pos_vtx_iter++).position;
418+
return thinks::ObjMap(ObjPositionType(pos.x, pos.y, pos.z));
419+
};
420+
421+
// Faces.
422+
auto idx_iter = std::begin(mesh.indices);
423+
const auto idx_iend = std::end(mesh.indices);
424+
auto face_mapper = [&idx_iter, idx_iend]() {
425+
using ObjIndexType = thinks::ObjIndex<uint16_t>;
426+
using ObjFaceType = thinks::ObjTriangleFace<ObjIndexType>;
427+
428+
// Check that there are 3 more indices (trailing indices handled below).
429+
if (std::distance(idx_iter, idx_iend) < 3) {
430+
return thinks::ObjEnd<ObjFaceType>();
431+
}
432+
433+
// Create a face from the mesh indices.
434+
const auto idx0 = ObjIndexType(*idx_iter++);
435+
const auto idx1 = ObjIndexType(*idx_iter++);
436+
const auto idx2 = ObjIndexType(*idx_iter++);
437+
return thinks::ObjMap(ObjFaceType(idx0, idx1, idx2));
438+
};
439+
440+
// Texture coordinates [optional].
441+
auto tex_vtx_iter = std::begin(mesh.vertices);
442+
auto tex_mapper = [&tex_vtx_iter, vtx_iend]() {
443+
using ObjTexCoordType = thinks::ObjTexCoord<float, 2>;
444+
445+
if (tex_vtx_iter == vtx_iend) {
446+
return thinks::ObjEnd<ObjTexCoordType>();
447+
}
448+
449+
const auto tex = (*tex_vtx_iter++).tex_coord;
450+
return thinks::ObjMap(ObjTexCoordType(tex.x, tex.y));
451+
};
452+
453+
// Normals [optional].
454+
auto nml_vtx_iter = std::begin(mesh.vertices);
455+
auto nml_mapper = [&nml_vtx_iter, vtx_iend]() {
456+
using ObjNormalType = thinks::ObjNormal<float>;
457+
458+
if (nml_vtx_iter == vtx_iend) {
459+
return thinks::ObjEnd<ObjNormalType>();
460+
}
461+
462+
const auto nml = (*nml_vtx_iter++).normal;
463+
return thinks::ObjMap(ObjNormalType(nml.x, nml.y, nml.z));
464+
};
465+
466+
// Open the OBJ file and pass in the mappers, which will be called
467+
// internally to write the contents of the mesh to the file.
468+
auto ofs = std::ofstream(filename);
469+
assert(ofs);
470+
const auto result =
471+
thinks::WriteObj(ofs, pos_mapper, face_mapper, tex_mapper, nml_mapper);
472+
ofs.close();
473+
474+
// Some sanity checks.
475+
assert(result.position_count == mesh.vertices.size() && "bad position count");
476+
assert(result.tex_coord_count == mesh.vertices.size() &&
477+
"bad position count");
478+
assert(result.normal_count == mesh.vertices.size() && "bad normal count");
479+
assert(result.face_count == mesh.indices.size() / 3 && "bad face count");
480+
assert(idx_iter == idx_iend && "trailing indices");
481+
}
482+
```
483+
Again, the `Write` method has no direct knowledge of the `Mesh` class. The relevant information is provided through the lambdas that are passed in. Complete code examples using the above methods can be found in the [examples](https://github.com/thinks/obj-io/tree/master/examples) folder. More advanced mesh I/O utilities built on top of the provided framework can be found in the [test/read_write_utils.h](https://github.com/thinks/obj-io/blob/master/test/read_write_utils.h) file.
484+
485+
## Tests
486+
The tests for this distribution are written in the [Catch2](https://github.com/catchorg/Catch2) framework, which is included as a submodule of this repository. Cloning recursively to initialize submodules is not required when using the functionality in this package, only to run the tests.
487+
488+
Running the tests is simple. In a terminal do the following (and similar for `Debug`):
489+
```bash
490+
$ cd d:
491+
$ git clone --recursive git@github.com:/thinks/obj-io.git D:/obj-io
492+
$ mkdir build-obj-io
493+
$ cd build-obj-io
494+
$ cmake ../obj-io
495+
$ cmake --build . --config Release
496+
$ ctest . -C Release
497+
```
498+
For more detailed test output locate the test executable (_thinks_obj_io_test.exe_) in the build tree and run it directly.
499+
500+
501+
## Future Work
502+
* _Improved read performance_ - The current implementation is rather naive in that it reads only a single line at a time. Additionally, many operations are done using `std::string` operations, which is not ideal performance-wise.
503+
* _Optional validation_ - It would be nice to have optional mechanisms to perform validation such as checking that face indices are within the range of the other attributes. However, this has recieved low priority since it can easily be done by the user before/after reading/writing.
504+
>>>>>>> 6933543a6c3e42d03289254b77c9e108def8a46f

0 commit comments

Comments
 (0)