Рендеринг травы

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

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

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

Поэтому на практике группу травинок объединяют в один блок с соответствующей текстурой (см. рис. 1).

sample of grass texture

Рис 1. Пример текстуры травы.

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

merged grass textures

Рис 2. Объединение нескольких текстур в одну.

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

Существует два способа подбора таких прямоугольников.

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

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

Using billboards for rendering grass

Рис 3. Использование billboard-ов для моделирования травы.

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

example of polygon grouping

Рис 4. Пример группировки прямоугольников.

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

Однако использование первого подхода приводит к слишком однородно выглядящей траве.

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

Далее мы рассмотрим сначала ряд общих для обоих подходов моментов, а затем особенности их реализации.

Alpha blending

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

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

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

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

Кроме того, можно по-прежнему использовать эффект "растворения" травы с увеличением расстояния - для этого достаточно просто уменьшать пороговое значения с ростом расстояния до камеры.

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

Такой подход позволяет выводить все используемые для представления травы прямоугольники одним разом (при помощи дисплейных списков и вершинных массивов).

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

noise texture

Рис 5. Пример шумовой текстуры.

Инициализация

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

Простейшим способом построения такого распределения является так называемый "jitter of regular grid" на всей площади вводится сетка с заданным шагом (обеспечивающим необходимую плотность), после чего все вершины этой сетки подвергаются случайному смещению при помощи равномерно- распределенной случайной величины (см. рис. 6).

Random jitter of regular grid

Рис 6. Random jitter of regular grid.

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

block examples

Рис 7. Варианты построения блоков из трех прямоугольников, вид сверху.

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

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

uniform vec3 up;
uniform vec3 right;

void main(void)
{
    vec4 vertex      = gl_Vertex + vec4 ( (gl_MultiTexCoord0.x - 0.5) * right +
                                          (gl_MultiTexCoord0.y - 0.5) * up, 0.0 );
    float d          = distance ( vertex.xyz, eyePos );
    float alphaScale = 0.9 / ( 1 + d ) + 0.1;

    gl_Color.a  *= alphaScale;
    gl_Position  = gl_ModelViewProjectionMatrix * vertex;
}

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

Таким образом мы приходим к следующей структуре для представления вершин при использовании второго подхода:

struct GrassVertex
{
    Vector3D pos;
    Vector2D texCoord;
    Vector3D org;
};

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

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

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

Оба эти массива размещаются в памяти GPU и вывод всего массива травы осуществляется при помощи всего лишь нескольких команд OpenGL.

На следующем листинге приводится исходный код для инициализации рендеринга травы при помощи billboard-ов.

Vector3D vertex [N1][N2];              // vertices of a heightmap

struct GrassVertex
{
    Vector3D pos;
    Vector3D tex;                      // z component is stiffness
};

struct GrassObject                     // single billboard
{
    GrassVertex vertex [4];
};

list<GrassObject *> grass;

inline float rnd ()
{
    return (float) rand () / (float) RAND_MAX;
}

inline float rnd ( float x1, float x2 )
{
    return x1 + (x2 - x1) * rnd ();
}

inline float heightFunc ( float x, float y )
{
    return 5 * ( 1.0 + noise.noise ( 0.09375268*Vector3D ( x, y, 0.12387 ) ) );
}

void initGrass ( float averageDist )
{
    float texStart = 0.0;
    int   line     = 0;

    for ( float x0 = -0.5*N1; x0 < 0.5*N1; x0 += averageDist, line++ )
        for ( float y0 = -0.5*N2; y0 < 0.5*N2; y0 += averageDist )
        {
            float x = x0 + ( rnd () - 0.5 ) * averageDist;
            float y = y0 + ( rnd () - 0.5 ) * averageDist;

            if ( line & 1 )
                x += 0.5 * averageDist;

            Vector3D      point  = Vector3D ( x, y, heightFunc ( x, y ) + 0.5 );
            GrassObject * object = new GrassObject;

            texStart = 0.25*(rand () % 2);

            object -> vertex [0].pos = point;
            object -> vertex [0].tex = Vector3D ( texStart,        0, 0 );
            object -> vertex [1].pos = point;
            object -> vertex [1].tex = Vector3D ( texStart + 0.25, 0, 1 );
            object -> vertex [2].pos = point;
            object -> vertex [2].tex = Vector3D ( texStart + 0.25, 1, 1 );
            object -> vertex [3].pos = point;
            object -> vertex [3].tex = Vector3D ( texStart,        1, 0 );

            grass.push_back ( object );
        }

    printf ( "Created %d grass objects\n", grass.size () );
}

void drawGrass ()
{
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, noiseMap );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, grassMap );
    glEnable           ( GL_TEXTURE_2D );
    glEnable           ( GL_ALPHA_TEST );
    glAlphaFunc        ( GL_GEQUAL, 0.5 );

    program.bind ();

    for ( list<GrassObject *> :: iterator it = grass.begin (); it != grass.end (); ++it )
    {
        GrassObject * object = *it;

        glBegin ( GL_QUADS );

        for ( int k = 0; k < 4; k++ )
        {
            glTexCoord3fv ( object -> vertex [k].tex );
            glVertex3fv   ( object -> vertex [k].pos );
        }

        glEnd ();
    }

    program.unbind ();

    glDisable  ( GL_ALPHA_TEST );
}

Следующие два листинга содержат используемые вершинную и фрагментную программы.

//
// Grass vertex shader for billboard approach
//

uniform float time;
uniform	vec3  eyePos;
uniform vec3  up;
uniform vec3  right;

void main(void)
{
    vec4  vertex     = gl_Vertex + 0.7*vec4 ( (gl_MultiTexCoord0.z - 0.5)*right +
                                              (gl_MultiTexCoord0.y - 0.5)*up, 0.0 );
    float d          = distance ( vertex.xyz, eyePos );
    float alphaScale = 0.9 / ( 1 + d ) + 0.1;
    vec3  p          = vec3 ( gl_ModelViewMatrix * vertex );

    gl_Position     = gl_ModelViewProjectionMatrix * vertex;
    gl_TexCoord [0] = vec4 ( gl_MultiTexCoord0.x, (1.0 - gl_MultiTexCoord0.y),
                             gl_MultiTexCoord0.zw );
    gl_Color.a     *= alphaScale;

    if ( gl_TexCoord [0].y < 0.5 )
    {
        float angle1 = time + 0.5077 * gl_Vertex.x - 0.421 * gl_Vertex.z;
        float angle2 = 1.31415 * time + 0.3 * gl_Vertex.y - 0.6 * gl_Vertex.z;

        gl_Position += 0.05*vec4 ( sin ( angle1 ), cos ( angle2 ), 0, 0 );
    }
}

//
// Grass fragment shader for billboard approach
//

uniform sampler2D grassMap;
uniform sampler2D noiseMap;

void main (void)
{
    vec4  grassColor = texture2D ( grassMap, gl_TexCoord [0].xy );
    float noiseValue = texture2D ( noiseMap, gl_TexCoord [0].xy );

    gl_FragColor = grassColor * vec4 ( vec3 ( 2.0 ), 2.0*noiseValue*gl_Color.a );
}

grass rendered using billboards

Рис 8. Изображение травы получаемое при использовании billboard-ов.

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

Vector3D vertex [N1][N2];              // vertices of a heightmap

struct GrassVertex
{
    Vector3D pos;
    Vector3D tex;                      // z component is stiffness
    Vector3D refPoint;
};

struct GrassObject
{
    Vector3D    point;                 // ref point
    GrassVertex vertex [4*3];
};

list<GrassObject *> grass;

void buildGrassBase ( float start, float r, float delta, Vector3D * tri, Vector3D * gr )
{
    for ( int i = 0; i < 3; i++ )
    {
        tri [i].x = r * cos ( start + i * M_PI * 2.0 / 3.0 );
        tri [i].y = r * sin ( start + i * M_PI * 2.0 / 3.0 );
        tri [i].z = 0;
    }
                                       // create grass base
    gr [0] = tri [0] - Vector3D ( 0, delta, 0 );
    gr [1] = tri [1] - Vector3D ( 0, delta, 0 );
    gr [2] = tri [1] + Vector3D ( delta, 0, 0 );
    gr [3] = tri [2] + Vector3D ( delta, 0, 0 );
    gr [4] = tri [2] + Vector3D ( 0, delta, 0 );
    gr [5] = tri [0] + Vector3D ( 0, delta, 0 );
}

void initGrass ( float averageDist )
{
                                       // now fill with grass objects
    Vector3D tri [3];                  // triangle base (with respect to O)
    Vector3D gr  [6];                  // grass base
    float    r        = 1.0;
    float    delta    = 0.3*r;
    float    upScale  = 1.0;
    float    texStart = 0.0;
    Vector3D up ( 0, 0, r );
    int      count = 0;
    int      line  = 0;

    for ( float x0 = -0.5*N1; x0 < 0.5*N1; x0 += averageDist, line++ )
        for ( float y0 = -0.5*N2; y0 < 0.5*N2; y0 += averageDist )
        {
            float x = x0 + ( rnd () - 0.5 ) * averageDist*1.2;
            float y = y0 + ( rnd () - 0.5 ) * averageDist*1.2;

            if ( line & 1 )
                x += 0.5 * averageDist;

            if ( ((count++) % 17) == 0 )
                buildGrassBase ( rnd (), r, delta, tri, gr );

            Vector3D      point  = Vector3D ( x, y, heightFunc ( x, y ) );
            GrassObject * object = new GrassObject;

            up.x     = upScale*(rnd () - 0.5);
            up.y     = upScale*(rnd () - 0.5);

            object -> point = point;
            object -> vertex [0].pos = point + gr [0];
            object -> vertex [0].tex = Vector3D ( 0, 0, 0 );
            object -> vertex [1].pos = point + gr [1];
            object -> vertex [1].tex = Vector3D ( texStart + 0.25, 0, 0 );
            object -> vertex [2].pos = point + gr [1] + up;
            object -> vertex [2].tex = Vector3D ( texStart + 0.25, 1, 1 );
            object -> vertex [3].pos = point + gr [0] + up;
            object -> vertex [3].tex = Vector3D ( 0, 1, 1 );

            up.x     = upScale*(rnd () - 0.5);
            up.y     = upScale*(rnd () - 0.5);

            object -> vertex [4].pos = point + gr [2];
            object -> vertex [4].tex = Vector3D ( 0, 0, 0 );
            object -> vertex [5].pos = point + gr [3];
            object -> vertex [5].tex = Vector3D ( texStart + 0.25, 0, 0 );
            object -> vertex [6].pos = point + gr [3] + up;
            object -> vertex [6].tex = Vector3D ( texStart + 0.25, 1, 1 );
            object -> vertex [7].pos = point + gr [2] + up;
            object -> vertex [7].tex = Vector3D ( 0, 1, 1 );

            up.x     = upScale*(rnd () - 0.5);
            up.y     = upScale*(rnd () - 0.5);

            object -> vertex [8].pos  = point + gr [3];
            object -> vertex [8].tex  = Vector3D ( 0, 0, 0 );
            object -> vertex [9].pos  = point + gr [4];
            object -> vertex [9].tex  = Vector3D ( texStart + 0.25, 0, 0 );
            object -> vertex [10].pos = point + gr [4] + up;
            object -> vertex [10].tex = Vector3D ( texStart + 0.25, 1, 1 );
            object -> vertex [11].pos = point + gr [3] + up;
            object -> vertex [11].tex = Vector3D ( 0, 1, 1 );

            grass.push_back ( object );
        }

    printf ( "Created %d grass objects\n", grass.size () );
}

void drawGrass ()
{
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, noiseMap );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, grassMap );
    glEnable           ( GL_TEXTURE_2D );
    glEnable           ( GL_ALPHA_TEST );
    glAlphaFunc        ( GL_GEQUAL, 0.5 );

    program.bind ();

    for ( list<GrassObject *> :: iterator it = grass.begin (); it != grass.end (); ++it )
    {
        GrassObject * object = *it;

        glBegin            ( GL_QUADS );
        glMultiTexCoord3fv ( GL_TEXTURE1_ARB, object -> point );

        for ( int k = 0; k < 12; k++ )
        {
            glTexCoord3fv ( object -> vertex [k].tex );
            glVertex3fv   ( object -> vertex [k].pos );
        }

        glEnd ();
    }

    program.unbind ();

    glDisable  ( GL_ALPHA_TEST );
}

Ниже приводятся соответствующие вершинная и фрагментная программа.

//
// Grass vertex shader
//

uniform float time;
uniform vec3  eyePos;

void main(void)
{
    float d          = distance ( gl_Vertex.xyz, eyePos );
    float alphaScale = 0.9 / ( 1 + d ) + 0.1;
    vec3  p          = vec3 ( gl_ModelViewMatrix * gl_Vertex );

    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = vec4 ( gl_MultiTexCoord0.x, (1.0 - gl_MultiTexCoord0.y),
                             gl_MultiTexCoord0.zw );
    gl_TexCoord [1] = gl_MultiTexCoord1;    // ref point
    gl_Color.a     *= alphaScale;

    if ( gl_TexCoord [0].y < 0.5 )
    {
        float angle1 = time + 0.3 * gl_TexCoord [1].x - 0.221 * gl_TexCoord [1].z;
        float angle2 = 1.31415 * time + 0.3 * gl_TexCoord [1].y -
                       gl_TexCoord [1].z * gl_TexCoord [1].x;

        gl_Position += 0.05*vec4 ( sin ( angle1 ), cos ( angle2 ), 0, 0 );
    }
}

//
// Grass fragment shader
//

uniform sampler2D grassMap;
uniform sampler2D noiseMap;

void main (void)
{
    vec4  grassColor = texture2D ( grassMap, gl_TexCoord [0].xy );
    float noiseValue = texture2D ( noiseMap, gl_TexCoord [0].xy );

    gl_FragColor = grassColor * vec4 ( vec3 ( 2.0 ), 2.0*noiseValue*gl_Color.a );
}

grass rendered with polygon blocks

Рис 9. Изображение травы для второго подхода.

Анимация травы

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

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

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

uncorrelated vertex steps leading to artifacts

Рис 10. Несогласованные смещения вершин могут приводить к заметным артефактам.

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

Тогда функция анимации (смещения вершин) травы задается как непрерывная функция и времени и данного параметра (f(x,y,z,t)).

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

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

Оптимизация

Хотя вывод всех граней, используемых для моделирования травы, осуществляется при помощи всего лишь нескольких команд OpenGL, данный способ все равно далек от оптимального. Причина этого заключается в том, что лишь небольшая часть всех выводимых граней (примерно 1/6) попадает в область видимости камеры. Все остальные грани будут просто отброшены (но уже после их обработки вершинной программой).

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

Существует однако довольно простой прием, позволяющий избавится от обоих этих недостатков.

Разобьем весь ландшафт (точнее его проекцию на плоскость Oxy) на набор одинаковых квадратных блоков (см. рис. 11).

regular scene subdivision

Рис 11. Разбиение всей сцены на одинаковые блоки.

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

Таким образом, у каждого блока будет своя пара - вершинный и индексный массивы - для рендеринга травы.

Использование подобного разбиения ландшафта на блоки позволяет эффективно реализовать отсечение по пирамиде видимости (frustum culling). Пример такого отсечение с упорядочиванием блоков в порядке front-to-back можно найти в главе 13 книги "Расширения OpenGL".

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

struct GrassVertex
{
    Vector3D pos;
    Vector3D tex;
    Vector3D refPoint;
};

struct GrassObject
{
    Vector3D    point;                 // ref point
    GrassVertex vertex [4*3];
};

class Cell                             // basic bulding block for landscape
{
protected:
    list <GrassObject *> grass;
    unsigned             vertexId;
    unsigned             indexId;
    BoundingBox          bounds;
    int                  x, y;
    int                  xSize, ySize;

public:
    Cell  ( int theX, int theY, int dx, int dy );
    ~Cell ();

    const BoundingBox& getBounds () const
	{
        return bounds;
    }

    void initObjects ( const list<GrassObject *>&& objects );
    void draw        ();
};

Cell :: Cell ( int theX, int theY, int dx, int dy ) : x ( theX ), y ( theY ),
                                                      xSize ( dx ), ySize ( dy )
{
    vertexId = 0;
    indexId  = 0;
}

Cell :: ~Cell ()
{
    if ( vertexId != 0 )
        glDeleteBuffersARB ( 1, &vertexId );

    if ( indexId != 0 )
        glDeleteBuffersARB ( 1, &indexId  );
}

void Cell :: initObjects ( const list<GrassObject *>&objects )
{
                                       // collect all grass objects within this cell
    for ( list <GrassObject *> :: const_iterator it = objects.begin (); it != objects.end (); ++it )
    {
        GrassObject * object = *it;

        for ( int i = 0; i < 12; i++ )
            if ( object -> vertex [i].pos.x >= x && object -> vertex [i].pos.x < x + xSize &&
                 object -> vertex [i].pos.y >= y && object -> vertex [i].pos.y < y + ySize )
            {
                grass.push_front ( object );

                break;
            }
    }

    if ( grass.empty () )
        return;
                                       // update bounds with list and prepare vertex buffer
    int           numVertices = grass.size () * 12;
    GrassVertex * vertices    = new GrassVertex [numVertices];
    int           count       = 0;

    for ( list <GrassObject *> :: iterator gi = grass.begin (); gi != grass.end (); ++gi )
    {
       GrassObject * object = *gi;

        for ( int i = 0; i < 12; i++ )
        {
            bounds.addVertex ( object -> vertex [i].pos );
            vertices [count++] = object -> vertex [i];
        }
    }
                                       // create vertex buffer
    glGenBuffersARB ( 1, &vertexId );
    glBindBufferARB ( GL_ARRAY_BUFFER_ARB, vertexId );
    glBufferDataARB ( GL_ARRAY_BUFFER_ARB, numVertices * sizeof ( GrassVertex ), vertices,
                      GL_STREAM_DRAW_ARB );
    glBindBufferARB ( GL_ARRAY_BUFFER_ARB, 0 );

    delete vertices;
}

void Cell :: draw ()
{
    static GrassVertex _v;
    static int         vertexStride = sizeof ( GrassVertex );

    static int offsets [] =
    {
        ((int)&_v.pos)      - ((int)&_v),   //  position
        ((int)&_v.tex)      - ((int)&_v),   // texture coordinates
        ((int)&_v.refPoint) - ((int)&_v),   // reference point (as texCoord[1])
    };
                                       // save state
    glPushClientAttrib  ( GL_CLIENT_VERTEX_ARRAY_BIT );

                                       // setup vertex buffer
    glBindBufferARB     ( GL_ARRAY_BUFFER_ARB, vertexId );

    glEnableClientState      ( GL_VERTEX_ARRAY );
    glVertexPointer          ( 3, GL_FLOAT, vertexStride, (void *) offsets [0] );

    glClientActiveTextureARB ( GL_TEXTURE0_ARB );
    glEnableClientState      ( GL_TEXTURE_COORD_ARRAY );
    glTexCoordPointer        ( 3, GL_FLOAT, vertexStride, (void *) offsets [1] );

    glClientActiveTextureARB ( GL_TEXTURE1_ARB );
    glEnableClientState      ( GL_TEXTURE_COORD_ARRAY );
    glTexCoordPointer        ( 3, GL_FLOAT, vertexStride, (void *) offsets [2] );

    glDrawArrays ( GL_QUADS, 0, grass.size () * 12 );

                                       // unbind array buffer
    glBindBufferARB   ( GL_ARRAY_BUFFER_ARB,  0 );
    glPopClientAttrib ();
}

#define CELL_SIZE   16
#define NC1         (N1/CELL_SIZE)
#define NC2         (N2/CELL_SIZE)

list<GrassObject *> grass;
Cell              * cells [NC1][NC2];

void buildGrassBase ( float start, float r, float delta, Vector3D * tri, Vector3D * gr )
{
    for ( int i = 0; i < 3; i++ )
    {
        tri [i].x = r * cos ( start + i * M_PI * 2.0 / 3.0 );
        tri [i].y = r * sin ( start + i * M_PI * 2.0 / 3.0 );
        tri [i].z = 0;
    }
                                       // create grass base
    gr [0] = tri [0] - Vector3D ( 0, delta, 0 );
    gr [1] = tri [1] - Vector3D ( 0, delta, 0 );
    gr [2] = tri [1] + Vector3D ( delta, 0, 0 );
    gr [3] = tri [2] + Vector3D ( delta, 0, 0 );
    gr [4] = tri [2] + Vector3D ( 0, delta, 0 );
    gr [5] = tri [0] + Vector3D ( 0, delta, 0 );
}

void initGrass ( float averageDist )
{
                                       // now fill with grass objects
    Vector3D	tri [3];               // triangle base (with respect to O)
    Vector3D	gr  [6];               // grass base
    float		r        = 1.0;
    float		delta    = 0.3*r;
    float		upScale  = 1.0;
    float		texStart = 0.0;
    Vector3D 	up ( 0, 0, r );
    int			count = 0;
    int			line  = 0;

    for ( float x0 = -0.5*N1; x0 < 0.5*N1; x0 += averageDist, line++ )
        for ( float y0 = -0.5*N2; y0 < 0.5*N2; y0 += averageDist )
        {
            float x = x0 + ( rnd () - 0.5 ) * averageDist*1.2;
            float y = y0 + ( rnd () - 0.5 ) * averageDist*1.2;

            if ( line & 1 )
                x += 0.5 * averageDist;

            if ( ((count++) % 17) == 0 )
                buildGrassBase ( rnd (), r, delta, tri, gr );

            Vector3D      point  = Vector3D ( x, y, heightFunc ( x, y ) );
            GrassObject * object = new GrassObject;

            up.x     = upScale*(rnd () - 0.5);
            up.y     = upScale*(rnd () - 0.5);

            object -> point = point;
            object -> vertex [0].pos = point + gr [0];
            object -> vertex [0].tex = Vector3D ( 0, 0, 0 );
            object -> vertex [1].pos = point + gr [1];
            object -> vertex [1].tex = Vector3D ( texStart + 0.25, 0, 0 );
            object -> vertex [2].pos = point + gr [1] + up;
            object -> vertex [2].tex = Vector3D ( texStart + 0.25, 1, 1 );
            object -> vertex [3].pos = point + gr [0] + up;
            object -> vertex [3].tex = Vector3D ( 0, 1, 1 );

            up.x     = upScale*(rnd () - 0.5);
            up.y     = upScale*(rnd () - 0.5);

            object -> vertex [4].pos = point + gr [2];
            object -> vertex [4].tex = Vector3D ( 0, 0, 0 );
            object -> vertex [5].pos = point + gr [3];
            object -> vertex [5].tex = Vector3D ( texStart + 0.25, 0, 0 );
            object -> vertex [6].pos = point + gr [3] + up;
            object -> vertex [6].tex = Vector3D ( texStart + 0.25, 1, 1 );
            object -> vertex [7].pos = point + gr [2] + up;
            object -> vertex [7].tex = Vector3D ( 0, 1, 1 );

            up.x     = upScale*(rnd () - 0.5);
            up.y     = upScale*(rnd () - 0.5);

            object -> vertex [8].pos  = point + gr [3];
            object -> vertex [8].tex  = Vector3D ( 0, 0, 0 );
            object -> vertex [9].pos  = point + gr [4];
            object -> vertex [9].tex  = Vector3D ( texStart + 0.25, 0, 0 );
            object -> vertex [10].pos = point + gr [4] + up;
            object -> vertex [10].tex = Vector3D ( texStart + 0.25, 1, 1 );
            object -> vertex [11].pos = point + gr [3] + up;
            object -> vertex [11].tex = Vector3D ( 0, 1, 1 );

            for ( int i = 0; i < 12; i++ )
                object -> vertex [i].refPoint = point;

            grass.push_back ( object );
        }

    printf ( "Created %d grass objects\n", grass.size () );

                                       // now start bulding cells
    printf ( "Initializing cells\n" );

    for ( int i = 0; i < NC1; i++ )
        for ( int j = 0; j < NC2; j++ )
        {
            cells [i][j] = new Cell ( i * CELL_SIZE - N1/2, j * CELL_SIZE - N2/2,
                                      CELL_SIZE, CELL_SIZE );
            cells [i][j] -> initObjects ( grass );
        }

    printf ( "Cells: done\n" );
}

void drawGrass ()
{
    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, noiseMap );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, grassMap );
    glEnable           ( GL_TEXTURE_2D );
    glEnable           ( GL_ALPHA_TEST );
    glAlphaFunc        ( GL_GEQUAL, 0.5 );

    Frustum frustum;

    program.bind ();

    for ( int i = 0; i < NC1; i++ )
        for ( int j = 0; j < NC2; j++ )
            if ( frustum.boxInFrustum ( cells [i][j] -> getBounds () ) )
                cells [i][j] -> draw ();

    program.unbind ();

    glDisable  ( GL_ALPHA_TEST );
}

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

Если ранее предполагалось, что в момент инициализации происходит полная инициализация ВСЕХ блоков, то для больших сцен будет очень удобно отказаться от этого предположения.

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

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

initialized blocks for given camera pos

Рис 12. Инициализированные блоки для данного положения камеры.

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

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

Самым простым способом борьбы с этим является использование возможности инициализации генератора псевдослучайных чисел посредством функции srand. Тогда, если в самом начале с каждым блоком связать свое seed-значение для инициализации, то достаточно будет перед использованием генератора псевдо-случайных чисел, проинициализировать его при помощи seed-значения данного блока.

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

Развитие

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

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

Еще одним способом повышения разнообразия является случайные вариации размера создаваемых прямоугольников (отклонение на 10-15% от базового размера).

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

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

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

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

Valid HTML 4.01 Transitional

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