Tidying up vertex-attribute handling for GL


#1

I've been looking at the JUCE GL demo.

I noticed that it is a mighty kerfuffle bridging vertex data between C/C++ and the GPU using OpenGL/GLES API (https://github.com/julianstorer/JUCE/blob/master/examples/Demo/Source/Demos/OpenGLDemo.cpp#L51)

I bounced off this problem a few years back when I wrote a basic GL engine using Obj-C (https://github.com/p-i-/Enjinn -- probably a couple of years before GLKit came out).

Out of curiosity I posted on Stack Overflow, asking whether anyone has come across a solution -- it seems reasonable that there is some C++11 way of automatically generating the boilerplate and hiding it from the consumer.

http://stackoverflow.com/questions/28775659/c-interface-for-managing-opengl-vertex-attributes

Someone has already posted a really good answer. Would it be of any interest if I look at incorporating something like this into JUCE's GL API?

π

PS This follows from a preliminary investigation into connecting a 3D engine into JUCE. I concluded that if the things I want are fairly basic, I may as well implement them straight from the JUCE GL API. Then I noticed that this corner of the API could be tidied up.

PPS As always, I'm on the IRC channel.


#2

Definitely very interesting! We're doing some GL work at ROLI so this kind of thing would probably make life easier, as well as being a nice addition to the library - I'll put in on our list of things to look into!


#3

I had a look at this and came up with a very tidy syntax:

You can just do:

using V = Vertex< float[3], int[2], double[4] >;

... and it will automatically set up the correct structure.

You could then do:

V quad[4]
{
    { {0f,0f,1f}, {127,0}, {1.,0.,0.,0.8} },
    :
}

cout << quad[0].get<1,1>(); // get 127
cout << quad[0].get<1>.u;   // same


quad[0].set<1,1>(126);

quad.setup_gl();

I've put my work up as an answer to the stack overflow question linked in the original post (relink:  http://stackoverflow.com/a/28816767/435129).  It contains a working live demo (convenience link: http://coliru.stacked-crooked.com/a/454dc9ba82274528 )

So far I've just been figuring out the mechanics of doing the work at compile time so that runtime access is fast.  Template metaprogramming is a right pain in the butt.

I will start to look at integrating it into JUCE tomorrow.


#4

I decided I don't like the metaprogramming approach.

Because it is intricate, difficult to understand, many moving parts to break. And if something breaks good luck fixing it.

Just because something can be accomplished using templates doesn't mean it should be.

Rather than provide a maximally concise interface at the expense of pushing complexity into the core, I think it's better to minimise overall complexity.

I'm looking at a macro-based solution that is a lot easier on the brain.

http://coliru.stacked-crooked.com/a/f7db9038abb189c0


#5

<deleted as obsolete>


#6

Here is working fork of the JUCE GL demo (the spinning teapot).

Just replace JUCE/examples/OpenGLAppExample/Source/MainComponent.cpp with: https://gist.github.com/p-i-/202c06f88f704b2ce96f

...and add a JUCE/examples/OpenGLAppExample/Source/AttribHelper.h containing: https://gist.github.com/p-i-/520f5e9cea09f345ca15

I've commented out the Attribute class in MainComponent.cpp, providing instead my own (Attr) class in AttribHelper.h.

Other changes to MainComponent.cpp are:

#include "AttribHelper.h"
    void createShaders()
    {
        vertexShader =...

        fragmentShader = ...

        ScopedPointer<OpenGLShaderProgram> newShader (new OpenGLShaderProgram (openGLContext));
        String statusText;

        if (newShader->addVertexShader (OpenGLHelpers::translateVertexShaderToV3 (vertexShader))
              && newShader->addFragmentShader (OpenGLHelpers::translateFragmentShaderToV3 (fragmentShader))
              && newShader->link())
        {
            shape = nullptr;
            attributes = nullptr;
            uniforms = nullptr;

            shader = newShader;
            shader->use();

            static const std::vector<AttrData> attr_data = {
                ATTR_DATA( Vertex, position , "position"        ),
                ATTR_DATA( Vertex, colour   , "sourceColour"    ),
                ATTR_DATA( Vertex, texCoord , "texureCoordIn"   )
            };

            shape      = new Shape (openGLContext);
            attributes = new Attr (openGLContext, *shader, attr_data); //Attributes (openGLContext, *shader);
            uniforms   = new Uniforms (openGLContext, *shader);

            statusText = "GLSL: v" + String (OpenGLShaderProgram::getLanguageVersion(), 2);
        }
        else
        {
            statusText = newShader->getLastError();
        }
    }

Then later...

    //ScopedPointer<Attributes> attributes;
    ScopedPointer<Attr> attributes;

That ATTR_DATA macro looks like this:

struct AttrData {
    const char* name;
    GLenum      gltype;
    uint32_t         dim;
    uint32_t         byteoffset;
    uint32_t         stride_bytes;
    OpenGLShaderProgram::Attribute*  juce_attr;
    //ScopedPointer<OpenGLShaderProgram::Attribute>  juce_attr;
};

#define ATTR_DATA( Vert, Attr, NameString ) \
    AttrData{ \
        /* name         */ NameString, \
        /* gltype       */ glconst4type< std::remove_extent< decltype(Vert::Attr) >::type >::value, \
        /* dim          */ std::extent< decltype(Vert::Attr) >::value, \
        /* byteoffset   */ offsetof(Vert, Attr), \
        /* stride_bytes */ sizeof(Vert), \
        /* juce_attr    */ nullptr \
    }

It looks as though JUCE currently only provides support for glVertexAttribPointer. However, there is also glVertexAttrib{I,L}Pointer.

That is to say that I don't think JUCE currently handles non-float vertex attributes.

My implementation provides handling for these, although I've commented it out for the time being.

π

PS Full listing for AttribHelper.h:


/*

AttribHelper.h
Created: 11 Mar 2015 10:56:40am
Author:  π

==============================================================================
*/

#ifndef ATTRIBHELPER_H_INCLUDED
#define ATTRIBHELPER_H_INCLUDED

#include “…/JuceLibraryCode/JuceHeader.h”

template <typename T>
struct glconst4type
{
static_assert( std::is_same<T, void>::value, “Invalid type!” );
static constexpr GLenum value = 0;
};

template <> struct glconst4type<unsigned char> {static constexpr GLenum value = GL_UNSIGNED_BYTE;};
template <> struct glconst4type<signed char> {static constexpr GLenum value = GL_BYTE;};
//template <> struct glconst4type<char> {static constexpr GLenum value = Utils::is_char_signed ? GL_BYTE : GL_UNSIGNED_BYTE;};
template <> struct glconst4type<unsigned short> {static constexpr GLenum value = GL_UNSIGNED_SHORT;};
template <> struct glconst4type<signed short> {static constexpr GLenum value = GL_SHORT;};
template <> struct glconst4type<unsigned int> {static constexpr GLenum value = GL_UNSIGNED_INT;};
template <> struct glconst4type<signed int> {static constexpr GLenum value = GL_INT;};
template <> struct glconst4type<float> {static constexpr GLenum value = GL_FLOAT;};
template <> struct glconst4type<double> {static constexpr GLenum value = GL_DOUBLE;};

struct AttrData {
const char* name;
GLenum gltype;
uint32_t dim;
uint32_t byteoffset;
uint32_t stride_bytes;
OpenGLShaderProgram::Attribute* juce_attr;
//ScopedPointer<OpenGLShaderProgram::Attribute> juce_attr;
};

#define ATTR_DATA( Vert, Attr, NameString )
AttrData{
/* name / NameString,
/
gltype / glconst4type< std::remove_extent< decltype(Vert::Attr) >::type >::value,
/
dim / std::extent< decltype(Vert::Attr) >::value,
/
byteoffset / offsetof(Vert, Attr),
/
stride_bytes / sizeof(Vert),
/
juce_attr */ nullptr
}

#define CX(x) std::cout << “’” << #x << "’: " << x << std::endl
#define COUT(x) std::cout << x << std::endl

struct Attr
{
const OpenGLContext& m_openGLContext;
const OpenGLShaderProgram& m_shader;
std::vector<AttrData> m_data;

Attr( OpenGLContext&amp; context, OpenGLShaderProgram&amp; shader, const std::vector&lt;AttrData&gt;&amp; data )
: m_openGLContext{context}, m_shader{shader}, m_data{data}
{
    CX( m_data.size() );
    for( int i=0;  i &lt; m_data.size();  i++ )
    {
        GLint location = m_openGLContext.extensions.glGetAttribLocation (m_shader.getProgramID(), m_data[i].name );

        if( location &gt; -1 )
            m_data[i].juce_attr = new OpenGLShaderProgram::Attribute (m_shader, m_data[i].name);
        else {
            COUT( "Attribute " &lt;&lt; m_data[i].name &lt;&lt; "not found in vertex-shader!" );
            jassert(false);
        }
    }
}

void enable()
{
    for( int i=0;  i &lt; m_data.size();  i++ )
    {
        const AttrData&amp; a = m_data[i];

// switch( a.gltype )
// {
// case glconst4type<float>::value :

            m_openGLContext.extensions.glVertexAttribPointer(
                (GLuint)            a.juce_attr-&gt;attributeID,
                (GLint)             a.dim,
                (GLenum)            a.gltype,
                (GLboolean)         GL_FALSE,       // normalized
                (GLsizei)           a.stride_bytes,
                (const GLvoid*)     (uintptr_t) a.byteoffset
                );

// break;

// case glconst4type<double>::value :
//
// m_openGLContext.extensions.glVertexAttribLPointer(
// (GLuint) i,
// (GLint) a.dim,
// (GLenum) a.gltype,
// (GLsizei) a.stride_bytes,
// (const GLvoid*) a.byteoffset
// );
// break;
//
// case 0 :
// static_assert( false, “Invalid type!” );
// break;
//
// default:
//
// m_openGLContext.extensions.glVertexAttribIPointer(
// (GLuint) i,
// (GLint) a.dim,
// (GLenum) a.gltype,
// (GLsizei) a.stride_bytes,
// (const GLvoid*) a.byteoffset
// );
// } // switch

        m_openGLContext.extensions.glEnableVertexAttribArray( a.juce_attr -&gt; attributeID );

    } // i

} // enable()

void disable()
{
    for( int i=0;  i &lt; m_data.size();  i++ )
        m_openGLContext.extensions.glDisableVertexAttribArray( m_data[i].juce_attr -&gt; attributeID );
}

};

#endif // ATTRIBHELPER_H_INCLUDED

 


#7

Here is another approach. The consumer can do:

VBO<Vertex>* pvbo;

pvbo = new VBO<Vertex>( openGLContext, *shader
                        , &Vertex::position , "position"
                        , &Vertex::colour   , "sourceColour"
                        , &Vertex::texCoord , "texureCoordIn"
                        );

The implementation would look like this (code tested and working):

template<typename V>
struct VBO
{
    static std::vector<AttrData>& attr_data() {
        static std::vector<AttrData> d;
        return d;
    }

    const OpenGLContext&        m_openGLContext;
    const OpenGLShaderProgram&  m_shader;

    template< typename ... P >
    VBO( OpenGLContext& context, OpenGLShaderProgram& shader, P... p )
    : m_openGLContext{context}, m_shader{shader}
    {
        add_attr( p... );
    }

    void add_attr() {};

    template< typename Tn, typename ... P > // Tn = e.g. int[3]
    void add_attr( Tn V::* m, const char* name, P... p )
    {
        using T = typename std::remove_extent< Tn >::type;

        AttrData d;

        d.name = name;
        d.gltype = glconst4type<T>::value;
        d.dim = std::extent<Tn>::value;
        d.byteoffset = (int) (uintptr_t) & ( ((V*)0) ->* m );
        d.stride_bytes = sizeof(V);
        d.juce_attr = nullptr;

        attr_data().push_back(d);

        add_attr( p... );
    }
};

It might make sense to create a more general VBO object. So that rather than just handling attributes, it also takes care of vertex buffers, index buffers, drawing. That's why I called it VBO.

However, the problem is every time you glom 2 GL commands together you lose potential flexibility and optimisation. For example, what if you need to draw a different number of vertices each frame. You might create static storage enough for worst case scenario, and each frame just load the first k verts onto the GPU. So maybe it is a bad plan to abstract too early.

So I'm going to actually try doing a few things with GL before looking at any kind of further abstraction.

π