用于管理 OpenGL 顶点属性的 C++ 接口

C++ interface for managing OpenGL vertex attributes

部分 OpenGL API 感觉非常 clumsy/awkward,尤其是设置顶点结构。

这是 JUCE 的一个例子,说明了这一点:https://github.com/julianstorer/JUCE/blob/master/examples/Demo/Source/Demos/OpenGLDemo.cpp#L51

您可以看到,如果要添加一个具有不同顶点结构的新着色器,您将不得不重写很多行非常相似的代码。

应该可以使用 C++11 构造来创建能够处理任意顶点结构的通用组件,从而消除所有这些样板文件。

但是怎么办呢?合理的界面应该是什么样的?

许多人会以不同的方式解决这个问题,但我猜只有少数可行的解决方案路径。因此,我认为这个问题不太开放...


到目前为止我的想法...

顶点可能是:

struct V {
    GLFloat position[3];
    GLInt somethingElse;
    GLFloat color[4];
}

即一些属性,其中每个属性都是一些 GL 原始类型的数组(长度可能为 1)(其中大约有十几个——浮点数加倍整数偶数矩阵...)。

也许可以使用 glGetActiveAttrib,它允许从着色器源中提取属性:1, 2

所以消费者的工作流程是:检查顶点着色器,检查属性,创建一个 C-struct 如上所述进行镜像,然后使用 glGetActiveAttrib 将每个属性连接到它在 C-结构。

这听起来可能很狡猾,也许存在对齐问题?

另一种方法可能是这样的:

V = VertGen< 
            "position"      , GLFloat , 3 , 
            "somethingElse" , GLInt   , 1 ,
            "color"         , GLFloat , 4 
           >

C++ 需要以某种方式循环遍历这些,使用某种参数包 car/cdr 类型递归。

这个其实看起来挺合理的,逼着消费者把shader的属性结构布局清楚。

但是消费者如何引用特定属性?

myvert["position"] = ...; // ?

这将是一个昂贵的字典查找;假设我们正在填充一个包含数千个顶点的网格。

是否有编译时字典查找之类的东西?那会很方便吗?

否则我们将不得不使用:

myvert[0] = ...; // meh, heading towards unreadable code

// or...
enum { pos, se, color };
myvert[pos] = ...; // this seems reasonable

这里唯一的问题是它要求消费者写两次东西:

V = VertGen< 
            "position"      , GLFloat , 3 , 
            "somethingElse" , GLInt   , 1 ,
            "color"         , GLFloat , 4 
           >
enum { pos, se, color };

那有什么办法吗?

其他 GL C++ 包装器

Are there any plans or existing projects to port OpenGL API to C++?

^ 另一个超级有用的问题已关闭:|为什么社区为什么要这样做? 怒吼

以下是我设法找到的候选人:

GL Toolkit
Unofficial OpenGL SDK
GL++
OOGL -- Object-oriented OpenGL
OGLplus

(四年前我用 ObjC https://github.com/p-i-/Enjinn 写了一个基本的 GLES2 引擎——它不属于这个列表,但我会 link 无论如何)。

我正在做类似的事情,您已经描述过,但我是从另一个角度解决这个问题:我为我正在加载的每种资产类型定义一个结构,因为这是数据的来源首先(因此您必须为每种资产类型 [.obj, .xbf, ...] 创建这样一个带有元数据的结构)。稍后,当决定用什么着色器显示对象时,可以很容易地从结构中获得缓冲区生成和 vao 设置(元数据,它是从属性名称到偏移量的映射)。

当然这不是 "super awesome beats everything" 解决方案,但它减少了此类结构的生成以及您要支持的文件格式的数量。蚂蚁这个数字(至少在我的情况下)比我 运行.

的着色器排列数小很多

更新:将此代码用于模型存储或属性绑定时要小心。模型必须正确对齐,但我的容器有 none。 好的,这是我的实现。

它是内联的&template-only,所以你应该把它放在header。

请记住,它期望顶点属性的位置在着色器中用 layout(location = ..) 指定并从 0 开始(然后是 1,2,3...)。


特点:

属性设置:

SetVertexAttribs<AttribLayout<Vec<float, 4>, Vec<int, 2>, vec<double, 3>>>();

// It enables attrib arrays (here: 0, 1 and 2), and then sets attrib pointers for them.
// It also selects correct glVertexAttrib{|I|L}Pointer function depending on type of each parametr.

Class Vec:

Vec<float, 4> a {1, 2, 3, 4}, b;
// First parametr can be any type. Second parametr is
// number of objects of that type in the vector.
// First parametr can be any default scalar type.
// Second parametr can only be 1, 2, 3 or 4.

a.x = 2; // you can use .x or .r - they have same meaning
a.y = 3; // you can use .y or .g - they have same meaning
a.z = 4; // you can use .z or .b - they have same meaning
a.w = 4; // you can use .w or .a - they have same meaning
a = a + a - a + -a; // Overloaded operators
a = {1, 2, 3, 4};  b = a;  // Overloaded assignment
a += {0, 1, 2, 3}; b += a;
a -= {0, 1, 0, 1}; b -= a;

Vec<float, 4>::type var; // here means `float var;`
// Vec<...>::type means type of a single element
std::cout << a.dim; // here prints 4  -- number of dimensions of a vector

Class AttribLayout:

这种类型的数组对于存储模型很有用。

AttribLayout<Vec<float, 4>, Vec<int, 2>, Vec<double, 3>>
    obj1, obj2({0,0,0,0},{1,2},{10,20,30}); // Constructors
// This class uses trick with char array - don't worry about padding, it does not have it.

std::cout << obj1.elements; // Here prints 3  -- number of vectors in layout
std::cout << obj1.byte_len; // Same as `sizeof obj1`

Vec<int, 2> var = obj1.get<1>(); // Returns vector with a specific number in a layout
double dvar = obj2.get<2,1>; // Here dvar == 20. First parametr means number of vector in a layout, second parametr means number of element in a vector.

obj1.set<1>({1,2}); // Parametrs mean same things as in `get()`.
obj1.set<2,0>(0.0); // I think you understood what `set()` does.

AttribLayout<Vec<float, 4>, Vec<int, 2>, vec<double, 3>>::type_at<1> var;
// Here means `Vec<int, 2> var;`

有用的功能:

type2glconst<int>::value // == GL_INT
// I think you understand what it's for.

type2glattribfunc<float> // same as glVertexAttribPointer
type2glattribfunc<double> // same as glVertexAttribLPointer
type2glattribfunc</*any integer type*/> // same as glVertexAttribIPointer


代码:

    template <typename T>
    struct type2glconst
    {
        static_assert(std::is_same<T, void>::value, "Invalid type!");
        static constexpr GLenum value = 0;
    };
    template <> struct type2glconst<unsigned char>  {static constexpr GLenum value = GL_UNSIGNED_BYTE;};
    template <> struct type2glconst<signed char>    {static constexpr GLenum value = GL_BYTE;};
    template <> struct type2glconst<char>           {static constexpr GLenum value = Utils::is_char_signed ? GL_BYTE : GL_UNSIGNED_BYTE;};
    template <> struct type2glconst<unsigned short> {static constexpr GLenum value = GL_UNSIGNED_SHORT;};
    template <> struct type2glconst<signed short>   {static constexpr GLenum value = GL_SHORT;};
    template <> struct type2glconst<unsigned int>   {static constexpr GLenum value = GL_UNSIGNED_INT;};
    template <> struct type2glconst<signed int>     {static constexpr GLenum value = GL_INT;};
    template <> struct type2glconst<float>          {static constexpr GLenum value = GL_FLOAT;};
    template <> struct type2glconst<double>         {static constexpr GLenum value = GL_DOUBLE;};

    template <typename T>
    inline void type2glattribfunc(GLuint index, GLint size, GLsizei stride, const GLvoid *pointer)
    {
        static_assert(type2glconst<T>::value, "Invalid type!");
        glVertexAttribIPointer(index, size, type2glconst<T>::value, stride, pointer);
    }
    template <>
    inline void type2glattribfunc<float>(GLuint index, GLint size, GLsizei stride, const GLvoid *pointer)
    {
        glVertexAttribPointer(index, size, type2glconst<float>::value, GL_FALSE, stride, pointer);
    }
    template <>
    inline void type2glattribfunc<double>(GLuint index, GLint size, GLsizei stride, const GLvoid *pointer)
    {
        glVertexAttribLPointer(index, size, type2glconst<double>::value, stride, pointer);
    }

    template <typename T, unsigned int D>
    struct Vec
    {
        static_assert((std::is_void<T>::value || type2glconst<T>::value) && D <= 4, "Invalid dimension for vector!");
        static constexpr int dim = 0;
        using type = void;
    };

    template <typename T>
    struct Vec<T, 1>
    {
        using type = T;
        static constexpr int dim = 1;
        union {T x, r;};
        Vec<T, 1> operator+(const Vec<T, 1> &o) const {return {x + o.x};}
        Vec<T, 1> &operator+=(const Vec<T, 1> &o) {x += o.x; return *this;}
        Vec<T, 1> operator-() const {return {-x};}
        Vec<T, 1> operator-(const Vec<T, 1> &o) const {return {x - o.x};}
        Vec<T, 1> &operator-=(const Vec<T, 1> &o) {x -= o.x; return *this;}
    };
    template <typename T>
    struct Vec<T, 2>
    {
        using type = T;
        static constexpr int dim = 2;
        union {T x, r;};
        union {T y, g;};
        Vec<T, 2> operator+(const Vec<T, 2> &o) const {return {x + o.x, y + o.y};}
        Vec<T, 2> &operator+=(const Vec<T, 2> &o) {x += o.x; y += o.y; return *this;}
        Vec<T, 2> operator-() const {return {-x, -y};}
        Vec<T, 2> operator-(const Vec<T, 2> &o) const {return {x - o.x, y - o.y};}
        Vec<T, 2> &operator-=(const Vec<T, 2> &o) {x -= o.x; y -= o.y; return *this;}
    };
    template <typename T>
    struct Vec<T, 3>
    {
        using type = T;
        static constexpr int dim = 3;
        union {T x, r;};
        union {T y, g;};
        union {T z, b;};
        Vec<T, 3> operator+(const Vec<T, 3> &o) const {return {x + o.x, y + o.y, z + o.z};}
        Vec<T, 3> &operator+=(const Vec<T, 3> &o) {x += o.x; y += o.y; z += o.z; return *this;}
        Vec<T, 3> operator-() const {return {-x, -y, -z};}
        Vec<T, 3> operator-(const Vec<T, 3> &o) const {return {x - o.x, y - o.y, z - o.z};}
        Vec<T, 3> &operator-=(const Vec<T, 3> &o) {x -= o.x; y -= o.y; z -= o.z; return *this;}
        Vec<T, 3> operator*(const Vec<T, 3> &o) const {return {y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x};}
        Vec<T, 3> &operator*=(const Vec<T, 3> &o) {*this = *this * o; return *this;}
    };
    template <typename T>
    struct Vec<T, 4>
    {
        using type = T;
        static constexpr int dim = 4;
        union {T x, r;};
        union {T y, g;};
        union {T z, b;};
        union {T w, a;};
        Vec<T, 4> operator+(const Vec<T, 4> &o) const {return {x + o.x, y + o.y, z + o.z, w + o.w};}
        Vec<T, 4> &operator+=(const Vec<T, 4> &o) {x += o.x; y += o.y; z += o.z; w += o.w; return *this;}
        Vec<T, 4> operator-() const {return {-x, -y, -z, -w};}
        Vec<T, 4> operator-(const Vec<T, 4> &o) const {return {x - o.x, y - o.y, z - o.z, w - o.w};}
        Vec<T, 4> &operator-=(const Vec<T, 4> &o) {x -= o.x; y -= o.y; z -= o.z; w -= o.w; return *this;}
    };

    template <typename T, typename ...P>
    struct AttribLayout_InternalValue
    {
        static_assert(!std::is_same<T, void>::value, "Void vector is used in layout!");
        using curtype = T;
    };

    template <typename T, typename ...P>
    struct AttribLayout_InternalContainer
    {
        static_assert(!std::is_same<T, void>::value, "Void vector is used in layout!");
        using curtype = T;
        using nexttype = typename std::conditional<(sizeof...(P) > 1), AttribLayout_InternalContainer<P...>, AttribLayout_InternalValue<P...>>::type;
    };

    template <typename ...P>
    class AttribLayout
    {
    protected:
        static_assert(sizeof...(P) > 0, "Zero-length attrib layout!");
        using cont_type = typename std::conditional<(sizeof...(P) > 1), AttribLayout_InternalContainer<P...>, AttribLayout_InternalValue<P...>>::type;
    public:
        static constexpr int elements = sizeof...(P);
    protected:
        template <unsigned int N, typename T> struct type_at_internal
        {
            using type = typename type_at_internal<(N - 1), typename T::nexttype>::type;
        };
        template <typename T> struct type_at_internal<0, T>
        {
            using type = T;
        };
        template <unsigned int N> using cont_type_at = typename type_at_internal<N, cont_type>::type;
    public:
        template <unsigned int N> using type_at = typename type_at_internal<N, cont_type>::type::curtype;

        template <unsigned int N, typename T> struct bytes_internal
        {
            static constexpr unsigned int var = bytes_internal<(N - 1), T>::var + (type_at<(N - 1)>::dim * sizeof(typename type_at<(N - 1)>::type));
        };
        template <typename T> struct bytes_internal<0, T>
        {
            static constexpr unsigned int var = 0;
        };

        static constexpr unsigned int byte_len = bytes_internal<(sizeof...(P)), void>::var;

        unsigned char bytearr[byte_len];

        template <unsigned int N> type_at<N> get()
        {
            static_assert(N >= 0 && N < sizeof...(P), "Element is out of range!");
            type_at<N> ret;
            std::memcpy(&ret, bytearr + bytes_internal<N, void>::var, sizeof (type_at<N>));
            return ret;
        }
        template <unsigned int N> void set(const type_at<N> &ref)
        {
            static_assert(N >= 0 && N < sizeof...(P), "Element is out of range!");
            std::memcpy(bytearr + bytes_internal<N, void>::var, &ref, sizeof (type_at<N>));
        }
        template <unsigned int N, unsigned int X> typename type_at<N>::type get()
        {
            static_assert(N >= 0 && N < sizeof...(P), "Element is out of range!");
            static_assert(X > 0 && X <= type_at<N>::dim, "Vector element is out of range!");
            typename type_at<N>::type ret;
            std::memcpy(&ret, bytearr + bytes_internal<N, void>::var + sizeof(typename type_at<N>::type) * (X - 1), sizeof (typename type_at<N>::type));
            return ret;
        }
        template <unsigned int N, unsigned int X> void set(typename type_at<N>::type obj)
        {
            static_assert(N >= 0 && N < sizeof...(P), "Element is out of range!");
            static_assert(X > 0 && X <= type_at<N>::dim, "Vector element is out of range!");
            std::memcpy(bytearr + bytes_internal<N, void>::var + sizeof(typename type_at<N>::type) * (X - 1), &obj, sizeof (typename type_at<N>::type));
        }
    protected:
        template <unsigned int N, unsigned int M> struct ctor_internal
        {
            static void func(void **ptr, unsigned char *arr)
            {
                std::memcpy(arr + bytes_internal<(M - N), void>::var, *ptr, sizeof (type_at<(M - N)>));
                ctor_internal<(N - 1), M>::func(ptr + 1, arr);
            }
        };
        template <unsigned int M> struct ctor_internal<0, M>
        {
            static void func(void **ptr, unsigned char *arr)
            {
                std::memcpy(arr + bytes_internal<M, void>::var, *ptr, sizeof (type_at<M>));
            }
        };
    public:
        AttribLayout()
        {
            static_assert(sizeof (decltype(*this)) == byte_len, "Crappy compiler have added padding to AttribLayout!");
            static_assert(alignof (decltype(*this)) == 1, "Crappy compiler have added alignment to AttribLayout!");
        }
        AttribLayout(P ...params)
        {
            void *ptrs[sizeof...(P)] {(void *)&params...};
            ctor_internal<(sizeof...(P) - 1), (sizeof...(P) - 1)>::func(ptrs, bytearr);
        }
    };

    namespace InternalStuff
    {
        template <class T, unsigned N> struct SetVertexAttribs_internal
        {
            static void func()
            {
                SetVertexAttribs_internal<T, (N - 1)>::func();
                glEnableVertexAttribArray(N);
                type2glattribfunc<typename T::template type_at<N>::type>(N, T::template type_at<N>::dim, T::byte_len, (void *)T::template bytes_internal<N, void>::var);
            }
        };
        template <class T> struct SetVertexAttribs_internal<T, 0>
        {
            static void func()
            {
                glEnableVertexAttribArray(0);
                type2glattribfunc<typename T::template type_at<0>::type>(0, T::template type_at<0>::dim, T::byte_len, (void *)T::template bytes_internal<0, void>::var);
            }
        };
    }

    template <class T>
    void SetVertexAttribs()
    {
        InternalStuff::SetVertexAttribs_internal<T, (T::elements - 1)>::func();
    }

我一直在浏览 HolyBlackCat 的精彩回答。我做了部分重写,主要是为了帮助我理解它是如何工作的。

我允许使用稍微更紧凑的接口语法:

using V = Attribute< float[3], int[1], double[4] >;

我还对 HBC 的回答做了一些小的格式化:github

coliru

// https://github.com/julianstorer/JUCE/blob/master/examples/Demo/Source/Demos/OpenGLDemo.cpp#L51

#include <string>
#include <iostream>
#include <typeinfo>
#include <cstring> // memcpy (!)
#include <array>

#define COUT(x) cout << #x << ":  " << x << endl

#define XCOUT(x) cout << #x << endl; x

using namespace std;

#define sc_uint   static constexpr  unsigned int

// from gltypes.h
typedef uint32_t GLbitfield;
typedef uint8_t  GLboolean;
typedef int8_t   GLbyte;
typedef float    GLclampf;
typedef uint32_t GLenum;
typedef float    GLfloat;
typedef int32_t  GLint;
typedef int16_t  GLshort;
typedef int32_t  GLsizei;
typedef uint8_t  GLubyte;
typedef uint32_t GLuint;
typedef uint16_t GLushort;
typedef void     GLvoid;

// from gl3.h
#define GL_BYTE                           0x1400
#define GL_UNSIGNED_BYTE                  0x1401
#define GL_SHORT                          0x1402
#define GL_UNSIGNED_SHORT                 0x1403
#define GL_INT                            0x1404
#define GL_UNSIGNED_INT                   0x1405
#define GL_FLOAT                          0x1406
#define GL_DOUBLE                         0x140A

#define GL_FALSE                          0
#define GL_TRUE                           1

#define GLAPI
#define APIENTRY

GLAPI void APIENTRY glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
GLAPI void APIENTRY glVertexAttribIPointer (GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
GLAPI void APIENTRY glVertexAttribLPointer (GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);



// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

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;};


template <typename T>
inline void generic_glVertexAttribPointer(GLuint index, GLint size, GLsizei stride, const GLvoid *pointer) {
    static_assert( glconst4type<T>::value,  "Invalid type!" );
    glVertexAttribIPointer( index, size, glconst4type<T>::value,                 stride, pointer );
}

template <>
inline void generic_glVertexAttribPointer<float>(GLuint index, GLint size, GLsizei stride, const GLvoid *pointer) {
    glVertexAttribPointer( index, size, glconst4type<float>::value, GL_FALSE,    stride, pointer );
}

template <>
inline void generic_glVertexAttribPointer<double>(GLuint index, GLint size, GLsizei stride, const GLvoid *pointer) {
    glVertexAttribLPointer( index, size, glconst4type<double>::value,            stride, pointer );
}


template <typename T, unsigned int D>
struct Vec {
    static_assert( (std::is_void<T>::value || glconst4type<T>::value)  &&  D <= 4,  "Invalid dimension for vector!" );
    static constexpr int dim = 0;
    using type = void;
};


template <typename T>
struct Vec<T, 1> {
    using type = T;
    static constexpr int dim = 1;
    T x;
    Vec<T, 1>  operator- ()                   const {return {-x};}
    Vec<T, 1>  operator+ (const Vec<T, 1> &o) const {return {x + o.x};}
    Vec<T, 1>  operator- (const Vec<T, 1> &o) const {return {x - o.x};}
    Vec<T, 1> &operator+=(const Vec<T, 1> &o)       {x += o.x; return *this;}
    Vec<T, 1> &operator-=(const Vec<T, 1> &o)       {x -= o.x; return *this;}
};

template <typename T>
struct Vec<T, 2> {
    using type = T;
    static constexpr int dim = 2;
    union {T x, u;};
    union {T y, v;};
    Vec<T, 2>  operator- ()                   const {return {-x, -y};}
    Vec<T, 2>  operator+ (const Vec<T, 2> &o) const {return {x + o.x, y + o.y};}
    Vec<T, 2>  operator- (const Vec<T, 2> &o) const {return {x - o.x, y - o.y};}
    Vec<T, 2> &operator+=(const Vec<T, 2> &o)       {x += o.x; y += o.y; return *this;}
    Vec<T, 2> &operator-=(const Vec<T, 2> &o)       {x -= o.x; y -= o.y; return *this;}
};

template <typename T>
struct Vec<T, 3> {
    using type = T;
    static constexpr int dim = 3;
    union {T x, r;};
    union {T y, g;};
    union {T z, b;};
    Vec<T, 3>  operator- ()                   const {return {-x, -y, -z};}
    Vec<T, 3>  operator+ (const Vec<T, 3> &o) const {return {x + o.x, y + o.y, z + o.z};}
    Vec<T, 3>  operator- (const Vec<T, 3> &o) const {return {x - o.x, y - o.y, z - o.z};}
    Vec<T, 3>  operator* (const Vec<T, 3> &o) const {return {y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x};}
    Vec<T, 3> &operator+=(const Vec<T, 3> &o)       {x += o.x; y += o.y; z += o.z; return *this;}
    Vec<T, 3> &operator-=(const Vec<T, 3> &o)       {x -= o.x; y -= o.y; z -= o.z; return *this;}
    Vec<T, 3> &operator*=(const Vec<T, 3> &o)       {*this = *this * o; return *this;}
};

template <typename T>
struct Vec<T, 4> {
    using type = T;
    static constexpr int dim = 4;
    union {T x, r;};
    union {T y, g;};
    union {T z, b;};
    union {T w, a;};
    Vec<T, 4>  operator- ()                   const {return {-x, -y, -z, -w};}
    Vec<T, 4>  operator+ (const Vec<T, 4> &o) const {return {x + o.x, y + o.y, z + o.z, w + o.w};}
    Vec<T, 4>  operator- (const Vec<T, 4> &o) const {return {x - o.x, y - o.y, z - o.z, w - o.w};}
    Vec<T, 4> &operator+=(const Vec<T, 4> &o)       {x += o.x; y += o.y; z += o.z; w += o.w; return *this;}
    Vec<T, 4> &operator-=(const Vec<T, 4> &o)       {x -= o.x; y -= o.y; z -= o.z; w -= o.w; return *this;}
};


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

template < int k, int offset_, typename... P >
struct Recursor;

template < int k, int offset_, typename Tn_, typename... P >
struct Recursor<k, offset_, Tn_, P...> 
{
public:
    // split Tn into Type[dim]
    using Tn = Tn_;
    using EltType = typename remove_extent<Tn>::type;

    sc_uint  dim        = extent<Tn>::value;
    sc_uint  offset     = offset_;
    sc_uint  attr_size  = sizeof(Tn);
    sc_uint  elt_size   = sizeof(EltType);

private:
    // remember at least one elt has been split off from P
    static_assert( 0 <= k  &&  k <= sizeof...(P),  "k out of bounds" ); 

    struct Terminator {
        using destination = void;
        sc_uint  bytecount = 0;
        static void setup_gl(int, int) { cout << "...setup_gl done!" << endl; }
    };

    using This =    Recursor< k  , offset          , Tn, P... >;
    using Next = typename conditional<
                    k == 0,
                    Terminator,
                    Recursor< k-1, offset+attr_size,     P... >
                    >::type;

public:
    // usage: e.g. 
    //      using Ri = Recursor<i, 0, W>::destination;
    //      cout << Ri::offset;
    using destination = typename conditional< k == 0, This, typename Next::destination >::type;

    // bytecount counts bytes of elements k thru N, so bytecount of k=0
    // will calculate total bytes
    sc_uint  bytecount = attr_size + Next::bytecount;

    // MUST call this like:
    //      Recursor<N-1, 0, W>::setup_gl(0); // N = number of attributes
    static void setup_gl(int total_bytes, int attribNumber) 
    {
        //glEnableVertexAttribArray( attribNumber );

        //generic_glVertexAttribPointer< EltType >(
        //    attribNumber,
        //    dim,
        //    size,
        //    offset
        //    );

        cout << "k: " << k << ":" 
                << dim 
                << typeid(EltType).name() << "(" 
                << "attr_size:" << attr_size << ", " 
                << "offset: "   << offset    << ")" << endl; 


        Next::setup_gl( total_bytes, attribNumber+1 );
    }
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

template< typename... P > 
struct Attribute
{
    sc_uint  N = sizeof...(P);

    // Extract data for the k-th parameter in pack P
    // (Always pass an initial set of 0)
    template< int k > 
    using R = Recursor< k, 0, P... >;

    // Raw storage
    sc_uint  total_bytes = R<N-1>::bytecount;
    uint8_t data[ total_bytes ];

    static void setup_gl() {
        R<N-1>::setup_gl(total_bytes, 0);
    }

    template< int k >  
    using A = typename R<k>::destination;

    template< int k >  using Tn      = typename A<k>::Tn;     // of form T[n], e.g. float[3]
    template< int k >  using EltType = typename A<k>::EltType;


    template <int k>
    using V = Vec< typename A<k>::EltType,  A<k>::dim >;

    // ATTRIBUTE 
    // Get
    template <int k>
    V<k> get() {
        static_assert( k>=0  &&  k<N,  "Attribute index out of range!" );
        V<k> ret;
        memcpy( &ret,  data + A<k>::offset,  A<k>::attr_size );
        return ret;
    }

    // Set
    template <int k>
    void set( void* src ) {
        cout << "set<" << k << ">( void* )" << endl << endl;
        static_assert( k>=0  &&  k<N,  "Attribute index out of range!" );
        memcpy( data + A<k>::offset,  src,  A<k>::attr_size );
    }

    template <int k> // need only this one!
    void set( V<k> const& ref ) { 
        cout << "set<" << k << ">( V<" << k << "> const& ref )" << endl; 
        set<k>( (void*)&ref ); 
    }

    template <int k>
    void set( Tn<k> const& ref ) { 
        cout << "set<" << k << ">( Tn<" << k << "> const& ref )" << endl; 
        set<k>( (void*)&ref ); 
    }

    // Attribute ELEMENT
    // Get
    template <int k, int e>
    EltType<k> const  get() {
        static_assert( k>=0 && k<N  &&  e>=0 && e < A<k>::dim,  "Attribute or Element index out of range!" );
        EltType<k> ret;
        memcpy( &ret,  data + A<k>::offset + e*A<k>::elt_size,  A<k>::elt_size );
        return ret;
    }

    // Set
    template <int k, int e>
    void set( const EltType<k>& ref ) {
        static_assert( k>=0 && k<N  &&  e>=0 && e < A<k>::dim,  "Attribute or Element index out of range!" );
        memcpy( data + A<k>::offset + e*A<k>::elt_size,  &ref,  A<k>::elt_size );
    } 

    template< int k, typename P0,  typename... Px >
    void set_all( P0 const&  p0,  Px const&  ... p )
    {
        set<k>( p0 );
        set_all<k+1>( p... );
    }

    template< int index >
    void set_all()
    { }

    Attribute() {}

    Attribute( P const& ... params ) {
        set_all<0>( params... );
    }
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

int main()
{
    if((1)){
        using V = Attribute< int[2] >;

        COUT( sizeof(V) );
        COUT( V::total_bytes );

        V::setup_gl();
        {
            V v;
            int a0[2]{1,2};
            v.set<0>( a0 );

            COUT(( v.get<0,1>() ));

            v.set<0>( {3,4} );
            v.set<0>( a0 );
        }

        {
            cout << "test vw" << endl;
            V v( {1,2} );
            V w( {{1,2}} );

            auto a0 = v.get<0>();
            a0.u += 10;
            COUT(a0.u);
            v.set<0>( a0 );
            v.set<0>( {7,8} );
            v.set<0,1>( 9 );        COUT(( v.get<0,1>() ));
            v.set<0,1>( {10} );     COUT(( v.get<0,1>() ));
        }
    }

    if((1)){
        cout << "using V = Attribute< float[3], int[1], double[4] >;" << endl;
        using V = Attribute< float[3], int[1], double[4] >;

        COUT( V::A<0>::dim );
        COUT( V::A<1>::dim );
        COUT( V::A<2>::dim );

        V v(   {1.f,2.f,3.f}, {4}, {5.,6.,7.,8.}   );   COUT(( v.get<2,2>() )); // 7
        v.set<2>( { 50., 60., 70., 80. } );             COUT(( v.get<2,2>() )); // 70
        v.set<2,2>( 100 );                              COUT(( v.get<2,2>() )); // 100
    }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -