Рендеринг в вершинный буфер (render-to-vertex-buffer) в OpenGL

Как известно, передача данных от CPU к GPU является довольно дорогостоящей операцией и поэтому желательно количество подобных передач минимизировать.

Однако во многих случаях возникает необходимость в постоянной передаче больших вершинных массивов GPU. Подобная ситуация возникает в случаях, когда имеет место слишком сложный закон преобразования данных в вершинах (который или вообще нельзя реализовать через вершинный шейдер или это очень дорого с точки зрения быстродействия) или же когда происходит изменение топологии объекта (изменяется количество вершин, организация их в примитивы и т.п.).

В таких случаях было бы крайне удобно иметь возможность построения таких вершинных массивов непосредственно на GPU. Тогда отпала бы необходимость в дорогостоящей пересылке данных от CPU к GPU и все операции с вершинами велись бы целиком на GPU и в пределах GPU.

К сожалению GPU (не будем рассматривать SM4) могут строить сами только текстуры - для этого достаточно использовать расширение EXT_framebuffer_object, позволяющее осуществлять рендеринг прямо в текстуру. В результате этого мы можем построить прямо на GPU текстуру любого размера и формата (поддерживаемого данным GPU, естественно).

Однако если внимательно посмотреть, то становится видно, что текстура отличается от вершинного массива исключительно способом использования - и текстура и вершинный массив представляет собой массив векторов (1-4-мерных) заданного типа (в том числе и float).

Поэтому если добавить возможность просто копировать текстуру (или ее часть) в вершинный массив (а это будет операция копирования внутри памяти GPU, что происходит с очень большой скоростью), то мы как раз и получаем то, что нам надо.

Именно такую возможность и предоставляют расширения EXT_pixel_buffer_object и ARB_pixel_buffer_object.

Данные расширения (а отличия между ними заключается только в окончаниях констант - EXT или ARB) позволяют в команде glBindBufferARB в качестве параметра target задать величину GL_PIXEL_PACK_BUFFER_EXT (GL_PIXEL_PACK_BUFFER_ARB). Тогда команды glReadPixels и glGetTexImage будут осуществлять чтение данных из текстуры (текущего буфера) прямо в вершинный массив, трактуя передаваемый им адрес просто как смещение внутри вершинного массива (т.е. если в качестве адреса передать NULL, то копирование данных будет осуществляться прямо с начала данного вершинного массива).

В результате можно при помощи специального фрагментного шейдера осуществить построение текстуры (через рендеринг в текстуру, желательно при этом использовать текстуры с floating-point-компонентами), а затем просто скопировать получившиеся данные прямо в вершинный массив.

Подобный прием и называется рендерингом в вершинный массив (render-to-vertex-buffer).

Построение ландшафта по карте высот

В качестве первого примера рассмотрим преобразования текстуры, содержащей карту высот, в массив вершин, где каждому текселу текстуры будет соответствовать вершина, z-координата которой будет пропорциональна яркости данного пиксела.

При этом строящаяся фрагментным шейдером текстура будет содержать массив вершин ландшафта, задаваемого исходной картой высот. Для построения этого ландшафта необходимо скопировать данные из текстуры в соответствующий вершинный массив и осуществить его рендеринг.

В результате этого прямо на GPU на каждом кадре строится массив из 2562 вершин и не покидая пределов GPU данный массив вершин сразу же выводится.

Ниже приводится фрагмент кода, осуществляющего построение массива вершин и его последующий рендеринг (полный код доступен по ссылке в конце статьи).

void	buildVertices ()
{
                                                    // load source image map
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, srcMap );

    program.bind  ();                               // perform rendering to FBO texture
    buffer.bind   ();
    startOrtho    ( TEX_SIZE, TEX_SIZE );
    drawQuad      ( TEX_SIZE, TEX_SIZE );
    endOrtho      ();
    program.unbind ();

                                                    // copy vertex data from texture to vertex buffer
    vertexBuffer -> bind   ( GL_PIXEL_PACK_BUFFER_ARB );
    glReadPixels           ( 0, 0, TEX_SIZE, TEX_SIZE, GL_RGBA, GL_FLOAT, NULL );
    buffer.unbind          ();
    vertexBuffer -> unbind ();
}

void display ()
{
    buildVertices ();

    glClear      ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    glMatrixMode ( GL_MODELVIEW );
    glPushMatrix ();

    float scale = 1.0f;

    glRotatef    ( rot.x, 1, 0, 0 );
    glRotatef    ( rot.y, 0, 1, 0 );
    glRotatef    ( rot.z, 0, 0, 1 );
    glScalef     ( scale, scale, 2 );
    glTranslatef ( -5, -5, -1 );

    glColor3f ( 1, 1, 1 );

    glPushClientAttrib  ( GL_CLIENT_VERTEX_ARRAY_BIT );

    glEnableClientState  ( GL_VERTEX_ARRAY );
    vertexBuffer -> bind ( GL_ARRAY_BUFFER );
    glVertexPointer      ( 4, GL_FLOAT, 0, NULL );

    indexBuffer  -> bind ( GL_ELEMENT_ARRAY_BUFFER_ARB );
    glIndexPointer       ( GL_UNSIGNED_INT, 0, 0 );

    if ( wireframe )
        glPolygonMode ( GL_FRONT_AND_BACK, GL_LINE );

    drawProgram.bind   ();
    glDrawElements     ( GL_TRIANGLES, (TEX_SIZE-1)*(TEX_SIZE-1)*6, GL_UNSIGNED_INT, 0 );
    drawProgram.unbind ();

    glPolygonMode     ( GL_FRONT_AND_BACK, GL_FILL );  // restore polygon mode
    glPopClientAttrib ();
    glPopMatrix       ();
    glutSwapBuffers   ();
}

Для удобства работы помимо классов FrameBuffer и GlslProgram здесь используется также класс VertexBuffer, описание и реализация которого приводятся ниже.

class VertexBuffer
{
    GLuint  id;
    GLenum  target;
    bool    ok;

public:
    VertexBuffer  ();
    ~VertexBuffer ();

    void    bind   ( GLenum theTarget );
    void    unbind ();

    void    setData    ( unsigned size, const void * ptr, GLenum usage );
    void    setSubData ( unsigned offs, unsigned size, const void * ptr );

    void  * map   ( GLenum access );
    bool    unmap ();

    static bool isSupported ();
};

VertexBuffer :: VertexBuffer ()
{
    glGenBuffersARB ( 1, &id );

    ok     = ( glIsBufferARB ( id ) == GL_TRUE );
    target = 0;
}

VertexBuffer :: ~VertexBuffer ()
{
    glDeleteBuffersARB ( 1, &id );
}

void VertexBuffer :: bind ( GLenum theTarget )
{
    glBindBufferARB ( target = theTarget, id );
}

void VertexBuffer :: unbind ()
{
    glBindBufferARB ( target, 0 );
}

void VertexBuffer :: setData ( unsigned size, const void * ptr, GLenum usage )
{
    glBufferDataARB ( target, size, ptr, usage );
}

void VertexBuffer :: setSubData ( unsigned offs, unsigned size, const void * ptr )
{
    glBufferSubDataARB ( target, offs, size, ptr );
}

void  * VertexBuffer :: map ( GLenum access )
{
    return glMapBufferARB ( target, access );
}

bool VertexBuffer :: unmap ()
{
    return glUnmapBufferARB ( target ) == GL_TRUE;
}

bool VertexBuffer :: isSupported ()
{
    return isExtensionSupported ( "GL_ARB_vertex_buffer_object" );
}

На следующем рисунке приводится пример получаемого изображения (цвет использован для обозначения высоты точки, чтобы сделать рельеф более заметным).

Рис 1. Ландшафт, построенный по карте высот.

Ниже приводятся фрагментные шейдеры для рендеринга в текстуру и рендеринга получившегося вершинного массива.

//
// Fragment shader for computing vertex positions for simple R2VB demo
//

uniform sampler2D srcMap;

void main ()
{
    vec3  clr = texture2D ( srcMap, gl_TexCoord [0].xy ).xyz;
    float lum = dot ( clr, vec3 ( 0.3, 0.59, 0.11 ) );

    gl_FragColor = vec4 ( 10.0*gl_TexCoord [0].xy, lum, 1.0 );
}

//
// Fragment shader for drawing for simple R2VB demo
//

void main ()
{
    const vec4 c1 = vec4 ( 0.0, 0.5, 0.0, 1.0 );
    const vec4 c2 = vec4 ( 1.0, 0.0, 0.0, 1.0 );

    float h = gl_TexCoord [0].z;

    gl_FragColor = mix ( c1, c2, h );
}

Рендеринг анимированной поверхности воды

В качестве следующего примера на использование рендеринга в текстуру рассмотрим рендеринг анимированной воды. Вместо традиционного использования bump mapping'а мы будем на ходу строить анимированные массивы вершин и нормалей, используя в качестве анимирующей функции турбулентность (turbulence).

При этом очень удобно воспользоваться возможностью рендеринга сразу в две текстуры (MRT, Multiple Render Targets) - в одну текстуру будут выводится координаты вершин, а в другую - вектора нормали в этих вершинах.

Плюсом использования рендеринга в вершинный буфер в данном случае является возможность полноценной работы с текстурами при вычислении вершин (далеко не все GPU поддерживают обращение к текстурам из вершинного шейдера, и даже в случае его поддержки, как правило нормальные режимы фильтрации недоступны).

На следующем листинге приводится фрагментный шейдер, используемый для построения вершин и нормалей в них.

//
// Fragment shader for computing vertex positions & normals for R2VB water rendering demo
//

uniform sampler3D noiseMap;
uniform float     time;
uniform float     waveScale;

vec3 turbulence ( const in vec3 p, const in float freqScale )
{
    float sum = 0.0;
    vec3  t   = vec3 ( 0.0 );
    float f   = 1.0;

    for ( int i = 0; i <= 3; i++ )
    {
        t   += abs ( 2.0 * texture3D ( noiseMap, f * p.xyz ).rgb - vec3 ( 1.0 ) ) / f;
        sum += 1.0 / f;
        f   *= freqScale;
    }

                                        // remap from [0,sum] to [-1,1]
    return 2.0 * t / sum - vec3 ( 1.0 );
}

void main ()
{                                       // deltas
    const vec3 dx = vec3 ( 0.01, 0.0,  0.0 );
    const vec3 dy = vec3 ( 0.0,  0.01, 0.0 );

                                        // compute height and delta height (in dx and dy)
    vec3 tex    = vec3       ( gl_TexCoord [0].xy, 0.1*time );
    vec3 turb   = turbulence ( tex,      2.0 );
    vec3 turbX  = turbulence ( tex + dx, 2.0 );
    vec3 turbY  = turbulence ( tex + dy, 2.0 );
    vec3 n      = vec3       ( turbX.x - turb.x, turbY.x - turb.x, 1.0 );

    gl_FragData [0] = vec4 ( 20.0*gl_TexCoord [0].xy, waveScale * turb.x, 1.0 );
    gl_FragData [1] = vec4 ( normalize ( n ), 1.0 );
}

Для рендеринга поверхности воды используется следующий фрагментный шейдер.

//
// Fragment shader for drawing water for R2VB water demo
//

uniform sampler3D   noiseMap;
uniform samplerCube reflectionMap;
uniform float       time;
varying vec3        n;
varying vec3        v;

void	main ()
{
    const vec4 c1 = vec4 ( 0.0, 0.4, 0.3, 1.0 );
    const vec4 c2 = vec4 ( 0.0, 0.3, 0.7, 1.0 );

    vec3  nn = normalize ( n );
    vec3  vn = normalize ( v );
    vec3  r  = reflect   ( vn, nn );
    float ca = dot       ( vn, nn );

                        // Do a lookup into the environment map.
    vec4 envColor   = textureCube ( reflectionMap, r.zyx );
    vec4 waterColor = mix         ( c1, c2, ca );

                        // calc fresnels term.  This allows a view dependant
                        // blend of reflection/refraction
    float fresnel = clamp ( ca, 0.1, 0.9 );

    gl_FragColor = mix ( waterColor, envColor, fresnel );
}

Ниже приводится фрагмент кода, служащий для построения массивов вершин и нормалей, а также для вывода получившейся поверхности воды.

void	buildVertices ()
{
    GLenum buffers [] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT };

                                            // load source image map
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_3D, noiseMap );

    program.bind   ();                      // perform rendering to FBO texture
    buffer.bind    ();
    glDrawBuffers  ( 2, buffers );
    startOrtho     ( TEX_SIZE, TEX_SIZE );
    drawQuad       ( TEX_SIZE, TEX_SIZE );
    endOrtho       ();
    program.unbind ();

                                            // copy vertex data from texture to vertex buffer
    glReadBuffer           ( GL_COLOR_ATTACHMENT0_EXT );
    vertexBuffer -> bind   ( GL_PIXEL_PACK_BUFFER_ARB );
    glReadPixels           ( 0, 0, TEX_SIZE, TEX_SIZE, GL_RGBA, GL_FLOAT, NULL );
    vertexBuffer -> unbind ();

    glReadBuffer           ( GL_COLOR_ATTACHMENT1_EXT );
    normalBuffer -> bind   ( GL_PIXEL_PACK_BUFFER_ARB );
    glReadPixels           ( 0, 0, TEX_SIZE, TEX_SIZE, GL_RGBA, GL_FLOAT, NULL );
    buffer.unbind          ();
    normalBuffer -> unbind ();

}

void display ()
{
    buildVertices ();

    glClear      ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    glMatrixMode ( GL_MODELVIEW );
    glPushMatrix ();

    float scale = 1.0f;

    glRotatef    ( rot.x, 1, 0, 0 );
    glRotatef    ( rot.y, 0, 1, 0 );
    glRotatef    ( rot.z, 0, 0, 1 );
    glScalef     ( scale, scale, 2 );
    glTranslatef ( -10, -10, -1 );

    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_3D, noiseMap );

    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_CUBE_MAP, reflectionMap );

    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glColor3f          ( 1, 1, 1 );

    glPushClientAttrib   ( GL_CLIENT_VERTEX_ARRAY_BIT );

    glEnableClientState  ( GL_VERTEX_ARRAY );
    vertexBuffer -> bind ( GL_ARRAY_BUFFER_ARB );
    glVertexPointer      ( 4, GL_FLOAT, 0, NULL );

    glEnableClientState  ( GL_TEXTURE_COORD_ARRAY );
    normalBuffer -> bind ( GL_ARRAY_BUFFER_ARB );
    glTexCoordPointer    ( 4, GL_FLOAT, 0, NULL );

    indexBuffer  -> bind ( GL_ELEMENT_ARRAY_BUFFER_ARB );
    glIndexPointer       ( GL_UNSIGNED_INT, 0, 0 );

    if ( wireframe )
        glPolygonMode ( GL_FRONT_AND_BACK, GL_LINE );

    drawProgram.bind   ();
    glDrawElements     ( GL_TRIANGLES, (TEX_SIZE-1)*(TEX_SIZE-1)*6, GL_UNSIGNED_INT, 0 );
    drawProgram.unbind ();

    glPolygonMode     ( GL_FRONT_AND_BACK, GL_FILL );  // restore polygon mode
    glPopClientAttrib ();
    glPopMatrix       ();
    glutSwapBuffers   ();
}

На следующем рисунке приведено получающееся изображение воды.

water rendered via render-to-vertex-buffer

Рис 2. Поверхность воды.

Скелетная анимация

В качестве следующего примера рассмотрим реализацию скелетной анимации моделей из игры DooM III при помощи рендеринга в вершинный буфер.

Хотя в простейших случаях скелетную анимацию можно реализовать и при помощи вершинных программ, подобная реализация оказывается эффективной только при очень небольшом числе костей (joints) на вершину (1-2).

Это связано с тем, что для каждой кости необходимо передать довольно большое количество данных. Пока все эти данные помещаются в стандартные вершинные атрибуты OpenGL все хорошо.

Однако при количестве костей на вершину 5 и больше (а в моделях из DooM III используется до 10 костей на вершину) передаваемые для каждой вершины данные уже не помещаются в стандартные атрибуты OpenGL и приходится использовать вершинные атрибуты общего вида (glVertexAttrib*).

Их количество так же ограничено и, что более важно, обычно OpenGL работает со стандартными вершинными атрибутами более эффективно (смотря статью с сайта developer.nvidia.com о pseudoinstancing в OpenGL).

При большом количестве вершин использование вершинной программы для скелетной анимации также приводит к большой нагрузке на вершинные процессоры, в то время как обычно фрагментные процессоры более оптимизированы (и их больше).

Поэтому использование рендеринга в вершинный буфер может оказаться гораздо удобнее - все данные о вершинах можно разместить в текстурах, для расчета положения вершин используются фрагментные процессоры и количество вершин в модели уже не оказывает слишком сильного влияния на быстродействие (в части скелетной анимации конечно).

Сначала рассмотрим какие именно текстуры нам нужны и какие данные будут в них размещаться.

Прежде всего нам нужны текстур, которая будет хранить информацию обо всех костях скелета (jointsMap). Для каждой кости скелета нам необходимо запомнить ориентацию (кватернион, т.е. четырехмерный вектор), положение (трехмерный вектор) и индекс (номер) родительской кости.

Легко видно, что, что при использовании RGBA-текстур с floating-point компонентами нам достаточно двух текселей на кость.

Поскольку костей в скелете обычно бывает немного, то проще всего использовать текстуру типа GL_TEXTURE_RECTANGLE_ARB (как и для всех остальных текстур, используемых для расчета скелетной анимации) размером 2*numJoints на 1 текселей.

При этом содержимое этой текстуры будет изменяться для каждого кадра анимации.

Также нам понадобятся две текстуры для работы с вершинами. Первая из них (weightsMap) будет для каждого веса хранить смещение (pos), вклад данного веса (weight) и индекс соответствующей кости (jointIndex).

Тут нам также достаточно двух текселов (в формате RGBA) на один вес:

В качестве формата данной текстуры также удобно использовать GL_TEXTURE_RECTANGLE_ARB, но поскольку количество весов может быть довольно большим, то лучше заранее задать достаточно большое значение ширины текстуры (например, 512), а высоту текстуры вычислять на ходу, исходя из общего числа весов в модели.

Наконец нам нужна текстура (vertexWeightsMap), которая для каждой вершины будет хранить два значения - номер первого веса (weightIndex) и количество подряд идущих весов (weightCount).

Тут нам достаточно одного тексела на вершину.

Обратите внимание, что большинство моделей из DooM III на самом деле состоят из нескольких подмоделей (обычно это связано с необходимостью использования различных шейдеров для разных частей модели).

Для нашей реализации будет очень удобно по всем подмоделям построить общий набор данных (т.е. текстур), и за один проход анимировать все подмодели сразу. В принципе подобный подход можно развить и далее, анимируя за один проход сразу большую группу моделей.

bool createJointsMap ()
{
    glGenTextures   ( 1, &jointsMap );
    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, jointsMap );
    glPixelStorei   ( GL_UNPACK_ALIGNMENT, 1 );

    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

    glTexImage2D    ( GL_TEXTURE_RECTANGLE_ARB, 0, mapFormat, 2*numJoints, 1, 0, GL_RGBA, GL_FLOAT, NULL );
    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, 0 );

    return true;
}

bool loadJointsMap ()
{
    if ( jointsBuf == NULL )
        jointsBuf = (float *) calloc ( 1, numJoints * 8 * sizeof ( float ) );

    if ( jointsBuf == NULL )
        return false;

                                                          // now remap joints array to buffer
    for ( int i = 0; i < numJoints; i++ )
    {
        jointsBuf [i*8    ] = joints [i].pos.x;
        jointsBuf [i*8 + 1] = joints [i].pos.y;
        jointsBuf [i*8 + 2] = joints [i].pos.z;
        jointsBuf [i*8 + 3] = joints [i].parentIndex;
        jointsBuf [i*8 + 4] = joints [i].orient.x;
        jointsBuf [i*8 + 5] = joints [i].orient.y;
        jointsBuf [i*8 + 6] = joints [i].orient.z;
        jointsBuf [i*8 + 7] = joints [i].orient.w;
    }

    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, jointsMap );
    glTexImage2D    ( GL_TEXTURE_RECTANGLE_ARB, 0, mapFormat, 2*numJoints, 1, 0, GL_RGBA, GL_FLOAT, jointsBuf );
    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, 0 );

    return true;
}

//
// Build using model vertexWeightsMap and weightsMap
//

bool buildWeightAndVertexMaps ()
{
    int weightsCount = 0;
    int vertexCount  = 0;
    int i;

    for ( i = 0; i < model.numMeshes; i++ )
    {
        Md5Mesh& mesh = model.meshes [i];

                                       // fix vertex ## in tris list
        for ( int j = 0; j < mesh.numTris; j++ )
            for ( int k = 0; k < 3; k++ )
                mesh.tris [j].index [k] += vertexCount;

        for ( int l = 0; l < mesh.numVertices; l++ )
            mesh.vertices [l].weightIndex += weightsCount;

        weightsCount += mesh.numWeights;
        vertexCount  += mesh.numVertices;
    }
                                       // we need weightsCount*2 texels in weightsMap
    int width         = 512;           // use this width for all our fixed maps
    int weightsHeight = (weightsCount * 2 + width - 1 ) / width;
    int vertexHeight  = (vertexCount + width - 1) / width;
    int count         = 0;

                                       // now prepare data to initialize maps
    float * weightBuf = (float *) calloc ( 4*4, width * weightsHeight );

    vertexMapHeight = vertexHeight;

    for ( i = 0; i < model.numMeshes; i++ )
    {
        Md5Mesh& mesh = model.meshes [i];

        mesh.weightsStart = count;

        for ( int j = 0; j < mesh.numWeights; j++ )
        {
            weightBuf [count++] = mesh.weights [j].pos.x;
            weightBuf [count++] = mesh.weights [j].pos.y;
            weightBuf [count++] = mesh.weights [j].pos.z;
            weightBuf [count++] = mesh.weights [j].weight;
            weightBuf [count++] = mesh.weights [j].jointIndex;
            weightBuf [count++] = 0;
            weightBuf [count++] = 0;
            weightBuf [count++] = 0;
        }
    }

    glEnable        ( GL_TEXTURE_RECTANGLE_ARB );
    glGenTextures   ( 1, &weightsMap );
    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, weightsMap );
    glPixelStorei   ( GL_UNPACK_ALIGNMENT, 1 );

    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S,     GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T,     GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

    glTexImage2D    ( GL_TEXTURE_RECTANGLE_ARB, 0, mapFormat, width, weightsHeight, 0, GL_RGBA,
                      GL_FLOAT, weightBuf );

    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, 0 );

    free ( weightBuf );

    float * vertexBuf = (float *) calloc ( 4*4, width * vertexHeight );

    for ( i = count = 0; i < model.numMeshes; i++ )
    {
        Md5Mesh& mesh = model.meshes [i];

        mesh.vertexStart = count;

        for ( int j = 0; j < mesh.numVertices; j++ )
        {
            vertexBuf [count++] = mesh.vertices [j].weightIndex;
            vertexBuf [count++] = mesh.vertices [j].weightCount;
            vertexBuf [count++] = 0;
            vertexBuf [count++] = 0;
        }
    }

    glGenTextures   ( 1, &vertexWeightsMap );
    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, vertexWeightsMap );
    glPixelStorei   ( GL_UNPACK_ALIGNMENT, 1 );

    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S,     GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T,     GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

    glTexImage2D    ( GL_TEXTURE_RECTANGLE_ARB, 0, mapFormat, width, vertexHeight, 0, GL_RGBA,
                      GL_FLOAT, vertexBuf );

    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, 0 );

    free ( vertexBuf );

    return true;
}

Далее приводится фрагментный шейдер, осуществляющий скелетную анимацию. Поскольку GLSL не имеет встроенной поддержки кватернионов, то две базовые операции над ними - перемножение двух кватернионов и поворот вектора при помощи кватерниона - вынесены в отдельные функции.

//
// Fragment shader for computing vertex positions for skeletal animation
//

uniform	sampler2DRect	vertDataMap;
uniform	sampler2DRect	weightsMap;
uniform	sampler2DRect	jointsMap;

//
// Quaternion multiplication
//

vec4 quatMul ( in vec4 q1, in vec4 q2 )
{
    vec3  im = q1.w * q2.xyz + q1.xyz * q2.w + cross ( q1.xyz, q2.xyz );
    vec4  dt = q1 * q2;
    float re = dot ( dt, vec4 ( -1.0, -1.0, -1.0, 1.0 ) );

    return vec4 ( im, re );
}

//
// vector rotation via quaternion
//

vec4 quatRotate ( in vec3 p, in vec4 q )
{
    vec4 temp = quatMul ( q, vec4 ( p, 0.0 ) );

    return quatMul ( temp, vec4 ( -q.x, -q.y, -q.z, q.w ) );
}

vec3 boneTransf ( in vec4 bonePosParent, in vec4 boneOrient, vec3 pos )
{
    return bonePosParent.xyz + quatRotate ( pos, boneOrient ).xyz;
}

//
// Get couple of vec4 values from textureRect
//

void readVec8 ( in sampler2DRect s, in int index, out vec4 v1, out vec4 v2 )
{
    int index2 = 2 * index;
    int y      = int ( index2 / 512 );
    int x      = index2 - 512 * y;
    int y2     = y;
    int x2     = x + 1;

    if ( x2 >= 512 )
    {
        y2 ++;
        x2 -= 512;
    }

    v1 = texture2DRect ( s, vec2 ( x,  y  ) );
    v2 = texture2DRect ( s, vec2 ( x2, y2 ) );
}

void main ()
{
    vec2 weights     = texture2DRect ( vertDataMap, gl_TexCoord [0].xy ).xy;
    int  startWeight = int ( weights.x );
    int  weightCount = int ( weights.y );
    vec3 pos         = vec3 ( 0.0 );
    vec4 pw, jz;                                 // (pos.xyz, weight), (jointIndex, 0, 0, 0)
    vec4 pp, orient;                             // (pos.xyz, parent), (orient.xyzw)

    for ( int i = 0; i < weightCount; i++ )
    {
        int index = startWeight + i;

        readVec8 ( weightsMap, startWeight + i, pw, jz );
        readVec8 ( jointsMap, int ( jz.x ), pp, orient );

        pos += boneTransf ( pp, orient, pw.xyz ) * pw.w;
    }

    gl_FragColor = vec4 ( pos.yxz, 1 );
}

Весь исходный код можно скачать по этой ссылке. Также можно скачать откомпилированные версии для M$ Windows, Linux и Mac OS X.

Используются технологии uCoz