Skip to content

karolek471/objgl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 

Repository files navigation

OBJ GL Loader v2

Quite fast .OBJ loader written in C

How to use it

Put objgl2.c and objgl2.h files in Your project and include them.
To put it simply:

//!!!Buffersize must not be smaller than the line length!
//set it to at least 1K bytes, just to be safe
unsigned int bufferSize = 65536;
objgl2StreamInfo strinfo = objgl2_init_filestream("file/name.obj", bufferSize);
objgl2Data objd = objgl2_readobj(&strinfo);

//cleanup
objgl2_deletestream(&strinfo);
objgl2_deleteobj(&objd);

In comparison to the older version, the newer one is based on the idea of streaming - either from a file or a buffer. I'll talk about it in a second. The example above reads the .OBJ file from the file in ~65535 bytes portions but you don't need to read it from file, you can read the .OBJ data from a regular char buffer:

char *rawobj = your_filereading_function("file/name.obj");

objgl2StreamInfo strinfo = objgl2_init_bufferstream(rawobj);
objgl2Data objd = objgl2_readobj(&strinfo);

Here the stream is created from an already existing buffer.
If, for some reason, you don't want to have a default C FILE streaming implementation just #define OBJGL_FSTREAM_IMPL 0 before including the header file but in that case you'll need to implement own file streaming.

Features

  • For some reason the newer version is faster. Not much but a little bit faster!
  • Loads an ~80MB .obj file with 5,626,896 indices in around 2 sec on my machine (AMD Ryzen 5 2600 Six-Core Processor, no compiler optimizations, debug mode, streaming from file, buffer size - 65536 bytes)
  • Uses peak 432.96 MB of memory for loading the said file and 522 allocations/reallocations
  • Puts the data in an OpenGL-friendly way (interleaved vertex attributes, indices)
  • OpenGL-friendly, easy to use material system
  • Uses a hash table to find unique vertices
  • Made for indexed rendering (glDrawElements)
  • Triangulates the faces if needed
  • NEGATIVE INDICES!!! Yaaaay!
  • Smooth shading, flat shading, auto-smooth, it's not a problem, just remember to generate the normals to the file

    The file used for tests was vokselia_spawn.obj from https://casual-effects.com/data/

Not-so-much features

  • Does not support multiple objects in one file (at the moment, I'll fix it)
  • Uses "triangle fan" triangulation algorithm (glitches may appear if the face is not convex)
  • Does not generate the normals if not present in file
  • Does not care about smoothing groups, flat shading, smooth shading, if no normals are present in the file

Streams and buffers

As I said above, there are two ways of "feeding" the parser with a data - a file stream or a buffer stream. It's important to note that the buffer stream is not really a stream.

When to use file stream over the buffer stream? Sometimes you deal with a large file, hundreds of megabytes. Malloc might not be able to allocate that much memory or there are other reasons not to allocate that much at once, so the idea of streaming comes in handy. Instead of reading the entire file at once and storing the result in one big buffer the program reads n bytes of data from a file, puts it into a buffer and then the OBJ Loader processes that buffer. Once it gets to the end of the buffer it requests more data, and the stream reader gets more bytes from the file into the buffer.

Reading from an already existing buffer supplied by the programmer is a little bit different - here the "streaming" mechanism is just a wrapper and there's no streaming at all. The OBJ Loader requests the data from the streamer and it gives it an entire buffer at once, so there's no overhead of fetching small portions of data.

And what about the buffer size? I recommend you to set it as big as possible - the bigger the buffer the less fetching is done. The OBJ Loader also requires the streamer to return whole lines and if your buffer size is smaller than the length of the line - it won't work! During the tests 10 bytes were too small but 100 bytes did the job, however as I said, set the buffer size as big as possible, 10K, 65K would be optimal.

Implementing your own file streamer

If you think you can do better than me (yes, probably you can) or you just don't want to use the C way of reading files, you can make your own stream reader. The stream reading function pointer looks like that: uint_least32_t (*objgl2_streamreader_ptr)(objgl2StreamInfo*) and the declaration of the default file streamer is uint_least32_t objgl2_filestreamreader(objgl2StreamInfo* info). Only four requirements are:

  • The streamer must return the buffer length (buffer length is not necessarily equal to bufferLen)
  • The streamer must not read more bytes than the bufferLen
  • The streamer must read whole lines, thus it will most likely read fewer bytes than the bufferLen, because it cannot read more than that
  • The streamer must set the eof field to a non-zero value if the end of the file is reached (set the eof to true basicaly). My implementation does it this way: it reads as many bytes (= bufferLen) as it can with fread. It goes back from the end of the buffer until it sees the \n. If the buffer already ends with \n then the length does not change. It returns the length of the buffer (distance from the start of the buffer to the last \n). It's as simple as that. The buffer reader is even simple - it does nothing.

`objgl2_init_bufferstream` and `objgl2_init_filestream` are there just to facilitate the creation of the stream info struct. You'll need to initialize your stream info struct yourself or just change the `objgl2_streamreader_ptr function` after initializing the struct. The code speaks louder than my convoluted descriptions, so please look it up.

How do materials work?

Really simply - let's say we have a function drawIndices(float *vertexData, unsigned int *indices, unsigned int numIndices) and our model has 3 materials: First we read the OBJ.
And now let's draw the entire model: drawIndices(objdata.data, objdata.indices, objdata.numIndices) and here it is! Now let's just render the first material: drawIndices(objdata.data, objdata.materials[0].indices, objdata.materials[0].len) once again, here it is, however now we see only the parts with the first material.

Why is it like that?

It feels intuitive to me - if you're using an OpenGL OBJ loader, probably you need a data suitable for OpenGL, thus vertex parameter interleaving. Probably you want to use different shader for different materials, thus such material system and if you don't care about materials, you just don't care about them in code and just render the model in it's entirety.

Can I use it for...

No, you cannot. Use it however you want as long as it's compliant with the ObjGL2 Loader (modified MIT) license, so go ahead and read it's "rules". The best reward for me is someone actually using my code, not gotoshaming me for QUESTIONABLE coding style. I just want it to be fast. I always use this type of parsing whenever I need to parse a file, because it's quick, easy for me to understand, no string-splitting bloat, just you and code. Harmony.

Data structures

objgl2Data is a structure for holding the OBJ Data - indices, vertices etc.

  • float *data - pointer to an interleaved buffer of vertices (ex. pos texcoord normal pos texcoord normal pos texcoord normal)
  • uint_least32_t *indices - pointer to the indices buffer. Each index is at least 32 bit long unsigned integer
  • objgl2Material *materials - pointer to the materials
  • uint_least32_t numIndices - how many indices there are in the OBJ
  • uint_least32_t numVertices - how many vertices there are
  • uint_least32_t vertSize - one vertex' size in bytes, can be 12 (only positions), 20 (positions and texcoords), 24 (positions and normals) or 32 (all three attributes)
  • uint_least32_t numMaterials - how many materials there are
  • unsigned char hasNormals - (boolean) whether the normals are present in the vertex attributes
  • unsigned char hasTexCoords - (boolean) whether the texcoords are present in the vertex attributes
  • char *name - (might be deleted in the future) the OBJ name, taken from the first o name declaration in the file, it's not important

objgl2StreamInfo is a structure for holding the stream data

  • uint_least64_t fOffset - offset from the beginning of the file, used by fread for fetching the chunks of data
  • uint_least32_t bufferLen - the malloced size of a buffer, maximal amount of bytes to read from the file to the buffer at once
  • uint_least32_t buffOffset - offset from the beginning of the buffer, not used at the moment
  • objgl2_streamreader_ptr function - pointer to a streaming function, by default can be objgl2_filestreamreader or objgl2_bufferstreamreader
  • char* filename - null terminated file name and path, ex. /home/obj/vokselia_spawn.obj
  • char eof - End of the file flag. Needs to be set by the stream reader if the EOF is reached
  • char type - type of the stream, can be OBJGL_FSTREAM or OBJGL_BSTREAM. Used by objgl2_deletestream

objgl2Material is a structure for holding the material data

  • uint_least32_t *indices - pointer to the face indices with that material, it's some offset of objgl2Data's *indices, so the memory is shared
  • uint_least32_t len - how many indices there are in that material. Indices of a specific material are contiguous.
  • char *name - null terminated name of the character. It's malloced and gets freed on objgl2_deleteobj

About

C Wavefront OBJ loader for OpenGL

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages