Skip to content
dmitrycelloPublic

About

Yet Another GL++ library

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

2,013 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

00-readme-1.png

Yet Another GL++ library

INSTALLATION →
USAGE →
EXAMPLES →

YAGL++ is yet another attempt to develop a C++ gear for the OpenGL API, merging its assets into the C++ objects. There are plenty of similar projects on the GitHub. So, why another one? The present library aims to fill the gap as a thoughtful, transparent and complete library, which includes every single API asset documented by Khronoss reference pages. Primarily, YAGL++ was designed as a resource to help Learn OpenGL, but after a while it turned out to be quite efficient. It takes care of the routine work, allowing to develop a code in less complicated manner, and without substantial overhead. The C++ features make the OpenGL programming more intuitive, clear and stable. For example, using the overloaded functions is much more convenient than to recall the every exact API name, such as glUniform3fv. Instead, it is possible to just call Uniform::Set class method with the appropriate parameters.

The library is fully interchangeable with the original API assets, it consists only of the header files, and requires the C++ 11 compiler or later, so the basic knowledge of C++ programming language is required to reading through the following section. The library uses standard library functions in its code, thus being cross-platform. However, it was initially designed to be used with MS Visual Studio under OS Windows. Therefore, the following features may be unavailable under different environment:

  • Implementation of the SAL concept (Microsoft Source Code Annotation Language). It makes the usage of the library less error-prone. The SAL macros are silently ignored in non-MSVC environment;
  • Using the Class Proprieties. They are unavailable in non-MSVC environment;
  • Accessing the Windows Resources of RCDATA type. The appropriate ...FromResource functions are hidden in non-MSVC environment. Nonetheless, it is still possible to load resources with ...FromMemory functions, while adding them via header files created with xxd utility.

The code of the library can be viewed in the repository's include directory. To properly install and use the library, follow the instructions in the INSTALLATION and USAGE documents. The library is extremely easy to start working with, it requires only one #include... statement for each compilation unit, while the #define... statement should be used only once per application:

#define YAGLPP_IMPLEMENTATION
#include <yaglpp/glpp.h>

The library works with OpenGL context version 2.0 to 3.3, the higher versions are yet to come, and GLFW version 3.4+. It is also integated with the STB image library as one of its direct dependencies. The Assimp and GLM libraries are both written in C++, so they can be included as is, without any workaround. It is possible to select the required OpenGL context version by altering the YAGLPP_CONTEXT_VERSION_MAJOR and YAGLPP_CONTEXT_VERSION_MINOR main switches (default 3.3) in the glpp.h header file. The used context version affects the build, making available only the supported API assets. Every call to the API function in the library is provided with the appropriate error checking in Debug build. On the contrary, under the Release build, the library attempts to impliment the inline calls minimizing the overhead. Here is the average YAGL++ member function implementation under the Debug build:

void Uniform::Set(GLsizei count, _In_reads_(count) const glm::vec3* value)
{
	glUniform3fv(get_location(), count, (GLfloat*)value);
	YAGLPP_GLAD_ERROR;
}

Under Release build it is exposed as:

inline void Uniform::Set(GLsizei count, _In_reads_(count) const glm::vec3* value)
{
	glUniform3fv(m_Location, count, (GLfloat*)value);
}

Naming concept

The YAGL++ library assets are defined within gl:: and glfw:: namespaces for GLAD and GLFW APIs respectively. The two helper classes, DataStore and StbImage, reside in the global namespace. You may bypass the gl:: and glfw:: prefixes with using namespace directives, but it is advised to keep them while managing large projects. For the sake of transparency, the original API names are mostly preserved. The names in YAGL++ library are obtained by stripping gl, glfw or stbi prefixes of the original API names, and by applying the Pascal-case rule for the rest, e.g., glDrawRangeElements in YAGL++ becomes gl::DrawRangeElements. The names in snake case are used in private assignments, and names starting with m_ followed by Pascal-case stand for a private class member data. All symbols starting with YAGLPP_ or yaglpp_ are for library's internal use only.

Many of the class names are based on the OpenGL targets, e.g., Texture2D class name is based on GL_TEXTURE_2D target. But some changes could be applied in rare conflicting cases. For example, the GL_TEXTURE_BUFFER target is being used by both buffer and texture objects, therefore the TextureBuffer class is a buffer, while the BufferTexture is a texture. Nonetheless, the class type will always use the same target. As the API functions are grouped around the classes, the API constants are grouped around the enum classes. This approach makes the library less error-prone, since the usage of a wrong enumerator type or invalid value simply won't compile. The class member names are stripped of their object names since they are redundant, e.g., the original API function glBindTexture used with GL_TEXTURE_2D target in YAGL++ becomes gl::Texture2D::Bind class method. Some of the API functions such as glDisable, glEnable, glGet, glIsEnabled, and glPixelStore are used with their constant names. For example:

glDisable(GL_SAMPLE_COVERAGE)
glEnable(GL_SCISSOR_TEST)
glGetIntegerv(GL_COLOR_CLEAR_VALUE, color)
glIsEnabled(GL_POLYGON_SMOOTH)
glPixelStoref(GL_UNPACK_ROW_LENGTH, length)
glfwWindowHint(GLFW_VISIBLE, visible)

in YAGL++ become:

gl::DisableSampleCoverage()
gl::EnableScissorTest()
gl::GetColorClearValue(color)
gl::IsPolygonSmooth()
gl::SetUnpackRowLength(length)
glfw::SetVisible(visible)

IntelliSense

Every class, function or enum member in the library is provided with a comment shown in Visual Studio by the IntelliSense with the description, parameter list and return value. This is a nice opportunity to briefly recap what the function does while typing the code, instead of recoursing to the manual time after time. All information in such comments is taken from Khronos website. If the description starts with the number in parentheses, it means either the minimal supported OpenGL context version (as floating point number), or the number of overloaded function (as integer number). The missing context version number assumes OpenGL 2.0 support. For example, the description of a function starting with (3.0) (3) means the support of OpenGL version 3.0 and above, plus it specifies that this is the third overloaded function. The description of the GLFW object function may start with (M) or (S) symbols meaning that the function can be called only from the main or secondary threads.

Important

The provided comments are brief, and if the function looks unfamiliar, it is recommended to consult its official API documentation.

The main switches

The symbols defined after #ifndef YAGLPP_CONFIG statement at the beginning of glpp.h file are the main switches, they affect the library setup. All switches except the last two may be commented to unset the flag. The last YAGLPP_CONTEXT_VERSION_MAJOR and YAGLPP_CONTEXT_VERSION_MINOR switches must be set to a value:

  • Commenting the YAGLPP_ASSIMP or YAGLPP_GLFW switches will exclude the corresponding library from the build;
  • Switches YAGLPP_COCOA_CHDIR_RESOURCES, YAGLPP_COCOA_MENUBAR and YAGLPP_JOYSTICK_HAT_BUTTONS are the GLFW hints set before the initialization, they are on by default;
  • Commenting the YAGLPP_CLASS_PROPERTIES switch will exlude the class properties from the library, they are unavailable while being in non-MSVC environment regardless of the flag;
  • Commenting the YAGLPP_GLM_HEADERS switch will exclude GLM library headers, this allows to include only specific GLM headers, while slightly reducing the compile time;
  • Commenting the YAGLPP_GLOBAL_ALLOCATOR switch will prevent the use of the library's new, new[], delete and delete[] operators, the library's allocator allows detecting memory leak;
  • Commenting the YAGLPP_NO_AFX_LAYOUT switch will transform the YAGL++ into the AFX-alike environment, without the main function. Instead, the code must contain the global variable of a class derived from glfw::Thread. Otherwise, classical layout with the main function is expected;
  • Commenting the YAGLPP_NO_GLFW_LEGACY switch allows to build YAGL++ with GLFW v3.3.10, the latest version supporting Windows XP. In this case, the recent GLFW features become unavailable;
  • The YAGLPP_STRICT_CONTEXT switch forces the YAGL++ to load the OpenGL verion set by the YAGLPP_CONTEXT_VERSION_MAJOR and YAGLPP_CONTEXT_VERSION_MINOR switches with Core Profile, but if the version is 3.1 or below, it will keep the default Compate Profile. Commenting this switch will load the highest available version with the default profile, ensuring that the loading will never fail. It is possible to eximine the loaded version by using IsContextVersion(major,minor) macro, or by appropriate window attributes. The loading algorithm could be overwritten by using glfw::ContextSetting stucture while creating window object;
  • Finally, the YAGLPP_CONTEXT_VERSION_MAJOR and YAGLPP_CONTEXT_VERSION_MINOR switches specify the OpenGL context version the library provides. They also limit the scope of the library, making available only the supported assets. Accepted combination of these values are: 2/0, 2/1, 3/0, 3/1, 3/2 and 3/3 to represent the supported versions 2.0 to 3.3 respectively;

Tip

It is possible to override the main swithes, placing them inside a project. To do that, first define a symbol YAGLPP_CONFIG, then all the above switches, prior to include the main glpp.h header file.

GLAD classes

The classes within the gl:: namespace are counterparts of the GLAD API. Every GLAD class have a copy constructor, allowing to use it in an assignment statement, as a function parameter, or as a return value. Every class has a unique data member: either the unsigned 4-byte integer in object classes, or the signed 4-byte integer in location classes. The object class contains the OpenGL object id, while the location class carries the index value retrieved from the linked program object. Both classes have no destructor, since the single data value does not require cleanup. The Sync class object contains single pointer, and its OpenGL object must be explicitly deleted at some point in the code, so it has no destructor as well. The UniformBlock class is considered as location class, but it does have a destructor, since several its data members require cleanup. The GLAD class object by design cares only about its lifetime. If it goes out of scope, it only cleans up itself, without deleting its real OpenGL object, which can still be present, and retrieved via various functions. This policy allows to prevent the cases when C++ object unintentionally deletes an OpenGL object while being assigned, copied or ceasing to exist. However, when necessary, every object class or Sync class can be instantiated via Auto template, deleting its OpenGL object when it goes out of scope:

gl::Auto<gl::Texture2D> texture;
    // cleaned up when out of scope

The object classes are derived from Objects, their default constructor creates an empty object, allowing the class to exist before OpenGL initialization. The object is considered empty, if its data member is set to zero, in this case its IsObject member function returns false. All derived object classes have the same data size as their parent classes, thus allowing to easily combine different types of objects within a stucture or another class. They use various templates making the library more compact, so the IntelliSense may often show the class name as ClassName_1, ClassName_2 etc. referring to the same class. The location classes have no base class, their default constructor also creates an empty object, which is when the location equals to -1, or invalid location value. The location value does not require a cleanup, since its lifetime rely on the program object. Some class constructors create, bind, and in some cases initialize their OpenGL objects. Here are some examples with the YAGL++ code and its API equivalent quasi-code written as comments:

gl::ArrayBuffer vbo(data, usage);
    // glGenBuffers(1, &vbo.id);
    // glBindBuffer(GL_ARRAY_BUFFER, vbo.id);
    // glBufferData(GL_ARRAY_BUFFER, data.GetSize(), data.Get(), usage);

gl::Sampler sampler = gl::Sampler::GetBinding();
    // glGetIntegerv(GL_SAMPLER_BINDING, &sampler.id);

GLboolean b1 = sampler.IsSampler();
    // b1 = glIsSampler(sampler.id);

GLboolean b2 = sampler.IsBinding();
    // glGetIntegerv(GL_SAMPLER_BINDING, &id);
    // b2 = (sampler.id == id);

gl::VertexAttrib attr(program, "aPos");
    // attr.location = glGetAttribLocation(program, "aPos");

GLAD tree

The following figure shows the GLAD class hierarchy chart. The Objects is the base class for almost every GLAD class, it is the only one having the red frame, meaning that it is an abstract class and cannot be created. Every class derived from Objects on the right side of the figure, controls the OpenGL Buffer, Framebuffer, Query, Renderbuffer, Sampler, Texture, and Vertex Array objects, using their particular API target, and therefore could be called a target objects. The Program and Shader objects use different mechanism: while the target objects should be binded to its target in order to operate, the shader object must be compiled before being attached to a program, where as the program object must be linked after the compiled shader objects are attached.

The first five classes directly derived from Objects, are subtype classes, they can be directly created, but without much of the functionality. They can be used as the temporary assets before being casted to one of its child class, when the resulting type is unknown. However, the precaution should be taken, once OpenGL object is initialized as particular type, using it as another type may result in OpenGL error. The dynamic allocation using operators new[] and delete[] for all classes derived from Objects is not allowed. The array of object classes should be generated with Alloc and deleted with Dealloc static member funtions. Another option is to use GenMap and DeleteMap, which sets up and clears the user-defined stucture of classes derived from the same subtype (e.g., ArrayBuffer, UniformBuffer, etc). Since the shader object cannot be created without knowing its type, its Alloc function returns an array of empty Shaders classes.

00-readme-2

The Uniform and VertexAttrib classes have no destructors, they operate the location index value of a Uniform and a Vertex Attribute respectively, which remains valid as long as their Program object remains linked. They have a default constructor creating an empty object, as well as a copy constructor copying the location value. The GetLocation method aquires the location index using a program object and the uniform or attribute name. If the location index is known, it could be set directly with SetLocation method. The appropriate constructors exist as well. To find out if the location value was set successfully, IsUniform and IsVertexAttrib methods should be used.

The Sync class operates the OpenGL Synchronization object, which is the pointer to an opaque API object. It must be created at the specific point of a program with Fence method, therefore its methods ClientWait, GetStatus and Wait require the valid object.

The UniformBlock class allows to operate many uniform variables at once, it works closely with Uniform Buffer Object. The class data has a uniform block index, which is aquired with GetIndex or directly set with SetIndex methods, or by the appropriate constructor. These methods retrieve as well the program id, number of uniforms, data block size and the array of uniform block indices. This allows to use the saved data in next operations without the need to save it outside of a class. The offset map is a special class feature allowing much easier uniform block data exchange. It is basically a user-defined stucture of pointers of particular types mirroring the uniform block types specified in GLSL shader. The OffsetMap method calculates all uniform offsets in the specified memory block, and sets the absolute addresses within the uniform buffer data to the offset map's pointers, allowing to interchange uniform values directly through these pointers.

GLFW classes

The classes in the glfw:: namespace are counterparts of the GLFW API. They all have the default constructor creating an empty class object, but unlike the GLAD objects, GLFW objects do not require binding, they must be explicitly created or selected via their methods. Consisting of multiple data members, these cumbersome objects are less suitable to be combined within a stucture or another class, and therefore they are not supplied with a copy constructor or any duplicate function. According to the GLFW documentation, the most of GLFW API functions must only be called from the main thread. Despite the GLFW library itself does not raise an error, the YAGL++ library will always assert the proper calling of these functions only in Debug mode, since otherwise their behaviour on the different platforms is not guaranteed. Starting from the version 3.4, YAGL++ use GLFW custom allocator, this allows customized memory managment via the general library macros.

The Cursor class operates a GLFW Cursor object. It has only createStandardCursor method and two overloaded createCursor methods to explicitly create the GLFW cursor object from different sources. The appropriate constructors exist as well. The destroyCursor method is called from the class destructor, if the object was not cleared before.

The Joystick class controls a GLFW Joystick input functionality, which exist from the GLFW initialization, and terefore its lifetime could not be controlled. Instead, the joystick device could be selected via appropriate constructor, two overloaded selectJoystick or selectJoystickGUID methods, and deselected via class destructor or deselectJoystick method. It is not possible to have multiple Joystick classes referencing the same joystick device, the class previously selecting that device will be automatically deselected when another class picks it up. The gamepad methods work only for the devices supporting the Gamepad input, to find out if this is the case, call joystickIsGamepad method. The Joystick class supports event handling, the single onDisconnect virtual function notifies the class object when associated physical device is disconnected, before that object is being voided. It is also possible to catch the joystick device connected event by setting JoystickConnectedCallback procedure via setJoystickConnectedCallback global function or static method.

Tip

In order to use the joystick disconnected event handler, it is necessary to derive a class from glfw::Joystick, and write over its onDisconnect virtual function.

The Monitor class is somewhat similar to Joystick class, it controls GLFW Monitor object used to set the OpenGL application into fullscreen mode, it exist from the GLFW initialization. The monitor device could be selected via appropriate constructor or three overloaded selectMonitor methods, and deselected via class destructor or deselectMonitor method. As for Joystick class, it is not possible to have multiple Monitor classes referencing the same monitor device, so the same mechanism is applied here. As Joystick class, Monitor class has onDisconnect virtual function. To catch monitor connected event, set MonitorConnectedCallback procedure via setMonitorkConnectedCallback global function or static method.

The Thread class is not a part of GLFW library, it is added to glfw:: namespace exclusively to handle the OS Windows Threads and thread syncronization. Its default constructor sets up an empty object, except that the main application Thread object exists since the GLFW initialization, it can be retreived through getMainThread global function or a static method. If the YAGL++ library is set into an AFX-alike mode, this function returns a pointer to a global application variable. Every other class object can start a new application thread. While the block message flag is unset (default), the Thread class allows to set up inter-thread communication, as well as to handle thread specific parameters, such as thread status, priority etc. The main working loop can be overwritten using the onRun virtual function. The other virtual functions onInit, onInput, onRender, onMessage, onIdle, and onExit are called from the main working loop, notifying the class object at every corresponding thread stage. It is recommended to use these function names if the onRun function is overwritten. This will prevent the unnecessary growth of the class virtual table in the derived class. While the YAGL++ library is not in AFX-alike mode, the getMainThread function returns the pointer to glfw::Thread base class. Despite its virtual functions could not be overwritten, it is still possible to handle its posted messages within the window rendering loop by calling dispatchMessage method:

glfw::Message msg;
while (glfw::dispatchMessage(&msg))
{
    // msg structure contains message data
}

The Window class operates a GLFW Window object. This is the largest library object, and the most of the GLFW API functions are merged around it. The window object is created via appropriate constructor or four overloaded createWindow methods, and deleted via class destructor or destroyWindow method. The createWindow method has an optional setting parameter, which is the pointer to ContextSetting structure, this allows to set all GLFW window hints at once. After window creation, it is important to call the makeContextCurrent method, to make the OpenGL context current on the calling thread. The associated thread object is saved in Window class data, and could be later retreived via getContextThread method. The very first call of makeContextCurrent method also initialize the OpenGL, it is very important to made it on the main calling thread. A context must only be made current on a single thread at a time and each thread can have only a single current context at a time. The makeContextNonCurrent method dissociates the window from the calling thread. The Window object has 17 virtual functions to handle the GLFW window original events. The library automatically transfers the event to the secondary thread, if a window operates outside of the main thread context. If the window block transfer flag is unset (default), the event handler is called twice: in main application thread, and then in the window host thread. The class saves the associated window data, such as the cursor position, it is possible to retreive this data from any thread via inline methods. These values are updated on background at an appropriate event, or direct API call:

double x = glWindow.getCursorPosX(); // Value since last cursor position event
glWindow.getCursorPos();             // Updates class X and Y values via API call
double y = glWindow.getCursorPosY(); // Value since the actual API call above

Some of the class members allows the API transfer feature, which is the possibility to perform the API call of the secondary thread by sending the appropriate message to the main thread. This requires the use of dispatchMessage and translateMessage functions within the main rendering loop. After the API transfer is complete, the main thread is sending a onWindowTransfer notification back to the sender window:

glfw::Message msg;
while (glfw::dispatchMessage(&msg))
{
    if (!glfw::translateMessage(&msg))
    {
        // Custom messages handling
    }
}

Helper classes

The YAGL++ library classes in global namespace are not a part of GLAD or GLFW API, they are included to simplify the I/O data handling. The DataStore class is managing the client memory block, the StbImage class is designed to manage the image data. Both helper classes create its data dynamically, have the size of a pointer, and therefore can be easily included to another class. They have an object duplicate ability using reference count, the default and copy constructors building an empty or duplicate object. There are two global helper functions: fileExists checks if the file with given path exists using stat structure, and freeMemory deallocates the memory block by the library defined de-allocator.

The DataStore class controls the lifetime of an allocated memory block. It could be used while working with the Buffer object classes, with UniformBlock class, or in any custom implementation. The createDataStore and deleteDataStore methods create and destroy the memory block. The getData and getSubData methods are used to get the pointer to a whole or a part of memory block. The optional length parameter is required to check the available data, and it is ignored in Release mode. The loadData overloaded member functions allocate the memory and load data from a binary resource of RCDATA type, from a file, or from another DataStore object. At the same time, loadSubData loads a part from only another DataStore object. The appropriate constructors exist as well. The writeData and writeSubData methods can save the memory block or its part into a file. The copySubData method replaces a part of destination memory block with a part of source memory block.

The StbImage class is a C++ interface of stb_image library, the lightweight and the most popular among the OpenGL programmers. StbImage class directly works with the Cursor, Texture and Window object classes, it supports several internal pixel formats, it can resize the image, and it does write into a file. The stb_image library allows an error callback function, so any error will be printed out only in Debug mode. After personal correspondance with its creator, Sean Barrett, the class has been rewritten to better fit the library's maintenance. According to the stb_image developers, the gif multi-layered loader is now depricated, and should come out one day. Nevertheless, the StbImage class will try to maintain this functionality as long as possible. The class handles three (3) ways of data input: binary resource, physical file and I/O callbacks. The I/O callbacks data input uses the StbCallbacks stucture, it allows to read from arbitrary sources, like packaged files etc. The stb_image library has internal conversion functions allowing to load an image in any supported pixel formats. The number of channels can be from 1 to 4, and each channel can be 8-bit unsigned integer, 16-bit unsigned integer or 32-bit float. The StbFormat enum class selects the desired pixel format, its Default value allows to load the original format without any conversion.

The image could be loaded with overloaded load, or loadGif methods for generic or multi-layered gif images. The loaded image parameters could be then retreived with inline getWidth, getHeight, getChannels, getDepth, and getFormat methods, where as the getFormat method returns both channels and depth as the StbFormat enum class value. The image source information could be retreived with static overloaded info methods, while using the StbInfo structure. The image parameters could be then retrieved directly from stucture's data, and via its channels, depth, and format members, which are equivalent to corresponding StbImage class members.

The static overloaded is16bit, and isHdr methods allow to find out, if the image source has 16-bit unsigned integer or 32-bit float pixel format. There are also various static members, setting up the global class parameters, e.g. setFlipVerticallyOnLoad. The createImage method allocates memory for the required dimensions and pixel format, and the deleteImage method unloads any previously loaded image, returning the class object into an empty state. The copyImage, copyRegion, and copySprite methods create memory block and copy from the source object an entire image, an image region, or the indexed image sprite region. The last option could be usefull while extacting multiple equally sized pictires, it calculates the required image location using the index value. The image could be resized with overloaded resizeImage and resizeRegion methods, and written into a file with writeBmp, writeHdr, writeJpg, writePng, and writeTga methods. The writeHdr requires the 32-bit float pixel data, otherwise it should be 8-bit unsigned integer.

Note

The isImage method of the StbImage class only returns true, if the memory block for an image was allocated. This is the case for load, loadGif, and create methods. The info methods do not allocate a memory block, therefore the isImage returns false.

↑ TOP

About

Yet Another GL++ library

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages