Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Графический конвейер для современных GPU состоит из целого ряда шагов, реализуемых в виде шейдеров на GLSL (вершинный, фрагментный, геометрический, тесселляционные шейдеры).
При этом каждый такой шаг пишется как отдельный шейдер, независимо компилируется, а затем они все собираются в единый программный объект. На стадии сборки (линковки) проверяется, что входные данные одного шейдера соответствуют выходным данным шейдера с предыдущего шага.
Такой подход предоставляет ряд удобств - все стадии собраны вместе в один объект, осуществляется проверка их соответствия друг другу, все стадии внутри программы разделяют общий набор uniform-переменных.
Однако этому подходу присущи также и свои недостатки - уже нельзя свободно группировать шейдеры разных шагов конвейера (как это было возможно для расширений ARB_vertex_program и ARB_fragment_program). Например нельзя использовать один и тот же вершинный шейдер в двух разных программах с различными фрагментными шейдерами - мы будем вынуждены дублировать вершинный шейдер.
Расширение ARB_separare_shader_objects (вошедшее в состав OpenGL 4.1) вводит новый тип программного объекта - program pipeline. Данный объект является набором программ, реализующих отдельные стадии конвейера и может использоваться вместо традиционной программы. Этот объект легко собирается из программ, реализующих отдельные стадии конвейера.
Однако подобная возможность несет с себе ряд важных вопросов, которые должны быть корректно решены. Во-первых, в данном расширении за каждую стадию отвечает отдельный программный объект, состоящий из шейдера для всего одной стадии конвейера (также возможно создать pipeline object и из полноценной программы со всеми стадиями).
Понятно, что каждая составляющая его программа не может быть успешно слинкована и выбрана (bind) в рамках стандартной работа с шейдерами (так состоит всего из одной стадии). Поэтому в OpenGL был введен специальный тип программы - separate. Программы данного типа могут состоять всего лишь из одного шейдера, соответствующего одной стадии конвейера рендеринга, и именно такие программы и используются для создания pipeline-объектов.
Для создания таких программ расширение вводит новую функцию glCreateShaderProgramv:
GLuint glCreateShaderProgramv ( GLenum type, GLsizei count, const char ** strings );
По стандарту вызов этой функции эквивалентен следующему фрагменту кода.
const uint shader = glCreateShader( type ); if ( shader ) { glShaderSource ( shader, count, strings, NULL ); glCompileShader ( shader ); const uint program = glCreateProgram (); if ( program ) { int compiled = FALSE; glGetShaderiv ( shader, COMPILE_STATUS, &compiled ); glProgramParameteri ( program, PROGRAM_SEPARABLE, TRUE ); if ( compiled ) { glAttachShader ( program, shader ); glLinkProgram ( program ); glDetachShader ( program, shader ); } append-shader-info-log-to-program-info-log } glDeleteShader ( shader ); return program; } else return 0;
Следующим моментом, который должен быть решен, является способ задания значений uniform-переменных без явного выбора (bind) соответствующей программы. Для решения этой задачи был использован подход из расширения EXT_direct_state_access и введены следующие функции, позволяющие явно устанавливать значения uniform-переменных без их явного выбора (bind'а).
void glProgramUniform1i ( GLuint program, int location, int x );
void glProgramUniform2i ( GLuint program, int location, int x, int y );
void glProgramUniform3i ( GLuint program, int location, int x, int y, int z );
void glProgramUniform4i ( GLuint program, int location, int x, int y, int z, int w );
void glProgramUniform1ui ( GLuint program, int location, GLuint x );
void glProgramUniform2ui ( GLuint program, int location, GLuint x, GLuint y );
void glProgramUniform3ui ( GLuint program, int location, GLuint x, GLuint y, GLuint z );
void glProgramUniform4ui ( GLuint program, int location, GLuint x, GLuint y, GLuint z, GLuint w );
void glProgramUniform1f ( GLuint program, int location, float x );
void glProgramUniform2f ( GLuint program, int location, float x, float y );
void glProgramUniform3f ( GLuint program, int location, float x, float y, float z );
void glProgramUniform4f ( GLuint program, int location, float x, float y, float z, float w );
void glProgramUniform1d ( GLuint program, int location, double x );
void glProgramUniform2d ( GLuint program, int location, double x, double y );
void glProgramUniform3d ( GLuint program, int location, double x, double y, double z );
void glProgramUniform4d ( GLuint program, int location, double x, double y, double z, double w );
void glProgramUniform1iv ( GLuint program, int location, GLsizei count, const int * value );
void glProgramUniform2iv ( GLuint program, int location, GLsizei count, const int * value );
void glProgramUniform3iv ( GLuint program, int location, GLsizei count, const int * value );
void glProgramUniform4iv ( GLuint program, int location, GLsizei count, const int * value );
void glProgramUniform1uiv ( GLuint program, int location, GLsizei count, const GLuint * value );
void glProgramUniform2uiv ( GLuint program, int location, GLsizei count, const GLuint * value );
void glProgramUniform3uiv ( GLuint program, int location, GLsizei count, const GLuint * value );
void glProgramUniform4uiv ( GLuint program, int location, GLsizei count, const GLuint * value );
void glProgramUniform1fv ( GLuint program, int location, GLsizei count, const float * value );
void glProgramUniform2fv ( GLuint program, int location, GLsizei count, const float * value );
void glProgramUniform3fv ( GLuint program, int location, GLsizei count, const float * value );
void glProgramUniform4fv ( GLuint program, int location, GLsizei count, const float * value );
void glProgramUniform1dv ( GLuint program, int location, GLsizei count, const double * value );
void glProgramUniform2dv ( GLuint program, int location, GLsizei count, const double * value );
void glProgramUniform3dv ( GLuint program, int location, GLsizei count, const double * value );
void glProgramUniform4dv ( GLuint program, int location, GLsizei count, const double * value );
void glProgramUniformMatrix2fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix3fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix4fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix2dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix3dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix4dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix2x3fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix3x2fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix2x4fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix4x2fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix3x4fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix4x3fv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const float * value );
void glProgramUniformMatrix2x3dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix3x2dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix2x4dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix4x2dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix3x4dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
void glProgramUniformMatrix4x3dv ( GLuint program, int location, GLsizei count,
GLboolean transpose, const double * value );
Еще одним (и очень важным) моментом является взаимодействие между собой различных стадий, а точнее - передача данных от шейдера одной стадии к шейдеру следующей стадии. Теперь мы уже не можем на этапе линковки проверить соответствие входных данных одного шейдера выходным данным другого.
Для решения этой задачи были введены специальные правила соответствия. Обычные входные и выходные переменные должны совпадать по имени, типу и дополнительным описателям. Для блоков важно не только совпадение всех переменных по именам, типам и описателям, но и совпадение имен блоков и порядка переменных внутри блока.
Альтернативным вариантом для задания соответствия между входными переменными одного шейдера и выходными другого является использование в шейдера описателя location, сопоставляющего каждой переменной свое положение (номер). В этом случае соответствие имен (и типов) уже не важно. Ниже приводится пример такого описания как для входных, так и для выходных значений.
layout(location = 0) in vec4 pos;
layout(location = 1) in vec3 normal;
layout(location = 0) out vec3 n;
layout(location = 1) out vec3 v;
layout(location = 2) out vec3 l;
layout(location = 3) out vec3 h;
Работа с новым типом объекта - program pipeline - осуществляется аналогично работ6е с другими типами объектов в OpenGL. Каждый такой объект идентифицируется беззнаковым целым числом, отличным от нуля. Для выделения таких чисел (идентификаторов) и их освобождения служат функции glGenProgramPipelines и glDeleteProgramPipelines. Для проверки того, является ли заданное число идентификатором какого-либо объекта program pipeline служит функция glIsProgramPipeline.
void glGenProgramPipelines ( GLsizei n, GLuint * pipelines );
void glDeleteProgramPipelines ( GLsizei n, const GLuint * pipelines );
GLboolean glIsProgramPipeline ( GLuint pipeline );
Выбрать объект (и собственно создать его при первом выборе) можно при помощи команды glBindProgramPipeline.
void glBindProgramPipeline ( GLuint pipeline );
Данная команда создает соответствующий pipeline-объект, если он еще не был создан, и делает его текущим. Если в качестве идентификатора был передан ноль, то мы переходим в режим, когда нет установленного pipeline-объекта.
Сам объект состоит из набора программ, соответствующих различным типам шейдеров (GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER, GL_TESS_CONTROL_SHADER и GL_TESS_EVALUATION_SHADER), а также активной программы (GL_ACTIVE_PROGRAM).
Для того, чтобы подключить программу (возможно содержащую сразу несколько стадий конвейера) к заданному pipeline-объекту служит команда glUseProgramStages.
void glUseProgramStages ( GLuint pipeline, GLbitfield stages, GLuint program );
Здесь параметр pipeline является идентификатором соответствующего pipeline-объекта, параметр stages определяет стадии конвейера pipeline-объекта для которых с идентификатором program становится текущей. Параметр stages является набором битов, соответствующих различным стадиям конвейера. Для задания соответствующих стадий используются константы GL_VERTEX_SHADER_BIT, GL_FRAGMENT_SHADER_BIT, GL_GEOMETRY_SHADER_BIT, GL_TESS_CONTROL_SHADER_BIT и GL_TESS_EVALUATION_SHADER_BIT. Также для этого параметра можно использовать значения GL_ALL_SHADER_BITS, говорящее о том, что данная программа должны стать текущей для всех стадий конвейера.
Если программа, соответствующая идентификатору program, не имеет атрибута GL_PROGRAM_SEPARATE или же не была успешно слинкована, то возникает ошибка GL_INVALID_OPERATION и состояние pipeline-объекта не изменяется.
Также для pipeline-объекта можно задать активную программу (GL_ACTIVE_PROGRAM) при помощи следующей команды:
void glActiveShaderProgram ( GLuint pipeline, GLuint program );
Активная программа в pipeline-объекте - это та программа, к которой будут относиться традиционные команды для работы с uniform-переменными (т.е. использующие "привязку" программы вместо явного задания идентификатора программы). Однако гораздо удобнее (и правильнее) использовать для работы с uniform-переменными функции, вводимые данным расширением.
Поскольку при сборке pipeline-объекта из отдельных компонент могут возникать проблемы (например, отсутствие необходимых стадий или несоответствии по входным и выходным значениям), то для проверки pipeline-объекта можно использовать функцию glValidateProgramPipeline.
void glValidateProgramPipeline ( GLuint pipeline );
Данная команда проверяет соответствующий объект и по результатам проверки выставляет атрибут с именем GL_VALIDATE_STATUS.
Для доступа к свойствам (атрибутам) pipeline-объекта служит функция glGetProgramPipelineiv.
void glGetProgramPipelineiv ( GLuint pipeline, GLenum pname, int * params );
Параметр pname задает свойство (атрибут), значение которого будет возвращено через параметр params. В следующей таблице приведены допустимые значения для этого параметра.
Атрибут | Значение |
---|---|
GL_ACTIVE_PROGRAM | Идентификатор активной программы |
GL_VERTEX_SHADER | Идентификатор программы, содержащей используемый вершинный шейдер |
GL_FRAGMENT_SHADER | Идентификатор программы, содержащей используемый фрагментный шейдер |
GL_GEOMETRY_SHADER | Идентификатор программы, содержащей используемый геометрический шейдер |
GL_TESS_CONTROL_SHADER | Идентификатор программы, содержащей используемый tessellation control шейдер |
GL_TESS_EVALUATION_SHADER | Идентификатор программы, содержащей используемый tessellation evaluation шейдер |
GL_INFO_LOG_LENGTH | Размер лога (включая нулевой байт в конце) в байтах |
GL_VALIDATE_STATUS | Результат проверки объекта |
Для получения лога, соответствующего данному pipeline-объекту служит команда glGetProgramPipelineInfoLog
void glGetProgramPipelineInfoLog ( GLuint pipeline, GLsizei bufSize, GLsizei * length, char * infoLog );
Данная команда копирует содержимое лога в виде ASCIIZ-строки в буфер, задаваемый параметром infoLog. Размер буфера передается через параметр bufSize, он определяет максимальное число символов, включая завершающий нулевой байт, которое будет скопировано в буфер. Через параметр length возвращается число скопированных в буфер символов (не считая нулевой байт в конце).
Фактически данное расширение позволяет иметь набор программ, реализующих отдельные стадии конвейера, и из них собирать на ходу готовую программу, которая и будет использована при рендеринге. При этом каждая используемая программа загружается/компилируется/хранится всего один раз.
Для удобства работы с этим расширением "завернем" pipeline-объект в соответствующий класс С++. На следующем листинге приводится описание данного класса.
class Pipeline
{
GLuint id;
Pipeline ( const Pipeline& );
Pipeline& operator = ( const Pipeline& );
public:
Pipeline ();
~Pipeline ();
GLuint getId () const
{
return id;
}
bool isOk () const
{
return id != 0;
}
string getLog () const;
bool create ();
bool bind ();
bool unbind ();
bool setStage ( GLbitfield mask, Program * );
bool setActiveProgram ( Program * );
bool validate ();
static int active (); // id of active pipeline object
};
Ниже приводится реализация этого класса.
Pipeline :: Pipeline ()
{
id = 0;
}
Pipeline :: ~Pipeline ()
{
if ( id != 0 )
glDeleteProgramPipelines ( 1, &id );
}
bool Pipeline :: create ()
{
glGenProgramPipelines ( 1, &id );
return id != 0;
}
bool Pipeline :: bind ()
{
if ( id == 0 )
return false;
glBindProgramPipeline ( id );
return true;
}
bool Pipeline :: unbind ()
{
glBindProgramPipeline ( 0 );
return true;
}
bool Pipeline :: setStage ( GLbitfield mask, Program * program )
{
if ( id == 0 || program == NULL || !program -> isOk () )
return false;
glUseProgramStages ( id, mask, program -> getProgram () );
return true;
}
bool Pipeline :: setActiveProgram ( Program * program )
{
if ( id == 0 || program == NULL || !program -> isOk () )
return false;
glActiveShaderProgram ( id, program -> getProgram () );
return true;
}
bool Pipeline :: validate ()
{
GLint status;
glValidateProgramPipeline ( id );
glGetProgramPipelineiv ( id, GL_VALIDATE_STATUS, &status );
return status != 0;
}
string Pipeline :: getLog () const
{
int len;
string log;
glGetProgramPipelineiv ( id, GL_INFO_LOG_LENGTH, &len );
if ( len < 1 )
return "";
char * buf = (char *) malloc ( len + 1 );
glGetProgramPipelineInfoLog ( id, len+1, NULL, buf );
log = buf;
free ( buf );
return log;
}
int Pipeline :: active () // id of active pipeline object
{
int pipeline;
glGetIntegerv ( GL_PROGRAM_PIPELINE_BINDING, &pipeline );
return pipeline;
}
Для поддержки данного расширения также необходимо внести некоторые изменения в класс Program, а именно - поддержку работы с separate-программами.
bool Program :: loadSeparate ( GLenum type, Data * data )
{
if ( !data -> isOk () || data -> getLength () < 1 )
{
log += "Error loading separate shader\n";
return false;
}
// now create an array of zero-terminated strings
const char * body = (const char *) data -> getPtr ( 0 );
GLint len = data -> getLength ();
char * buf = (char *) malloc ( len + 1 );
memcpy ( buf, body, len );
buf [len] = '\0';
program = glCreateShaderProgramv ( type, 1, (const char **)&buf );
separate = true;
linkRequired = false;
ok = true;
loadProgramLog ( program );
free ( buf );
return program != 0;
}
bool Program :: loadSeparate ( GLenum type, const string& fileName )
{
Data * data = new Data ( fileName );
if ( data == NULL )
{
log += "Cannot open \"";
log += fileName;
log += "\"\n";
return false;
}
bool res = loadSeparate ( type, data );
delete data;
return res;
}
Рассмотрим теперь практическое применение данного расширения. Для этого введем два pipeline-объекта с общим вершинным шейдером и разными фрагментными.
Ниже приводится пример общего вершинного шейдера. В этом примере передача значений в фрагментный шейдер происходит через спецификатор location.
#version 410 core
uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 eye;
uniform vec3 light;
layout(location = 0) in vec4 pos;
layout(location = 1) in vec3 normal;
layout (location = 0) out vec3 n;
layout (location = 1) out vec3 v;
layout (location = 2) out vec3 l;
layout (location = 3) out vec3 h;
void main(void)
{
vec4 p = mv * vec4 ( pos.xyz, 1.0 );
gl_Position = proj * p;
n = normalize ( nm * normal );
v = normalize ( eye - p.xyz );
l = normalize ( light - p.xyz );
h = normalize ( l + v );
}
Данный шейдер будет использован сразу в двух моделях освещения - Блинна и Гуч (см. статью о моделях освещения). Ниже приводится соответствующий фрагментный шейдер для модели Блинна.
#version 410 core
layout (location = 0) in vec3 n;
layout (location = 1) in vec3 v;
layout (location = 2) in vec3 l;
layout (location = 3) in vec3 h;
out vec4 color;
void main(void)
{
const vec4 diffColor = vec4 ( 0.5, 0.0, 0.0, 1.0 );
const vec4 specColor = vec4 ( 0.7, 0.7, 0.0, 1.0 );
const float specPower = 30.0;
vec3 n2 = normalize ( n );
vec3 l2 = normalize ( l );
vec3 h2 = normalize ( h );
vec4 diff = diffColor * max ( dot ( n2, l2 ), 0.1 );
vec4 spec = specColor * pow ( max ( dot ( n2, h2 ), 0.0 ), specPower );
color = diff + spec;
}
Фрагментный шейдер для модели Гуч приведен на следующем листинге.
#version 410 core
layout (location = 0) in vec3 n;
layout (location = 1) in vec3 v;
layout (location = 2) in vec3 l;
layout (location = 3) in vec3 h;
out vec4 color;
void main(void)
{
const vec3 surfaceColor = vec3 ( 0.75, 0.75, 0.75 );
const vec3 warmColor = vec3 ( 0.6, 0.6, 0.0 );
const vec3 coolColor = vec3 ( 0.0, 0.0, 0.6 );
const float diffuseWarm = 0.45;
const float diffuseCool = 0.45;
vec3 n2 = normalize ( n );
vec3 l2 = normalize ( l );
vec3 v2 = normalize ( h );
vec3 r = normalize ( reflect ( -l2, n2 ) );
vec3 cool = min ( coolColor + diffuseCool * surfaceColor, 1.0 );
vec3 warm = min ( warmColor + diffuseWarm * surfaceColor, 1.0 );
vec3 final = mix ( cool, warm, max(0,dot(n2,l2)));
float spec = pow ( max ( dot ( r, v2 ), 0.0 ), 32.0 );
color = vec4 ( min ( final + spec, 1.0 ), 1.0 );
}
Ниже приводится фрагмент программы, создающей соответствующие pipeline-объекты.
Pipeline p1, p2; // create two pipelines
Program vp, fp1, fp2; // create one common vertex program and two fragment programs
. . .
p1.create ();
p1.bind ();
if ( !vp.loadSeparate ( GL_VERTEX_SHADER, "sep-vp.glsl" ) )
printf ( "Error loadSeparate vp\n" );
p1.setStage ( GL_VERTEX_SHADER_BIT, &vp );
if ( !fp1.loadSeparate ( GL_FRAGMENT_SHADER, "sep-fp1.glsl" ) )
printf ( "Error loadSeparate fp\n" );
p1.setStage ( GL_FRAGMENT_SHADER_BIT, &fp1 );
if ( !p1.validate () )
printf ( "Error validating: %s\n", p1.getLog ().c_str () );
p1.unbind ();
p2.create ();
p2.bind ();
p2.setStage ( GL_VERTEX_SHADER_BIT, &vp );
if ( !fp2.loadSeparate ( GL_FRAGMENT_SHADER, "sep-fp2.glsl" ) )
printf ( "Error loadSeparate fp\n" );
p2.setStage ( GL_FRAGMENT_SHADER_BIT, &fp2 );
if ( !p2.validate () )
printf ( "Error validating: %s\n", p2.getLog ().c_str () );
p2.unbind ();
Следующий листинг содержит функцию, отвечающую непосредственно за рендеринг.
void display ()
{
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
mat4 mv = mat4 :: rotateZ ( toRadians ( rot.z ) ) * mat4 :: rotateX ( toRadians ( rot.x ) ) * mat4 :: rotateY ( toRadians ( rot.y ) );
mat3 nm = normalMatrix ( mv );
glProgramUniformMatrix4fv ( vp.getProgram (), mvLoc, 1, GL_TRUE, mv.data () );
glProgramUniformMatrix3fv ( vp.getProgram (), nmLoc, 1, GL_TRUE, nm.data () );
if ( first )
p1.bind ();
else
p2.bind ();
vao.bind ();
glDrawElements ( GL_TRIANGLES, 3*mesh.numTris, GL_UNSIGNED_INT, NULL );
vao.unbind ();
if ( first )
p1.unbind ();
else
p2.unbind ();
glutSwapBuffers ();
}
Ниже приводятся вершинный и фрагментный листинги, не использующие спецификатора location, а полагающиеся на совпадение имен, типов и спецификаторов.
Вершинный шейдер:
#version 410 core
uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 eye;
uniform vec3 light;
layout(location = 0) in vec4 pos;
layout(location = 1) in vec3 normal;
out vec3 n;
out vec3 v;
out vec3 l;
out vec3 h;
void main(void)
{
vec4 p = mv * vec4 ( pos.xyz, 1.0 );
gl_Position = proj * p;
n = normalize ( nm * normal );
v = normalize ( eye - p.xyz );
l = normalize ( light - p.xyz );
h = normalize ( l + v );
}
Один из фрагментных шейдеров:
#version 410 core
in vec3 n;
in vec3 v;
in vec3 l;
in vec3 h;
out vec4 color;
void main(void)
{
const vec3 surfaceColor = vec3 ( 0.75, 0.75, 0.75 );
const vec3 warmColor = vec3 ( 0.6, 0.6, 0.0 );
const vec3 coolColor = vec3 ( 0.0, 0.0, 0.6 );
const float diffuseWarm = 0.45;
const float diffuseCool = 0.45;
vec3 n2 = normalize ( n );
vec3 l2 = normalize ( l );
vec3 v2 = normalize ( h );
vec3 r = normalize ( reflect ( -l2, n2 ) );
vec3 cool = min ( coolColor + diffuseCool * surfaceColor, 1.0 );
vec3 warm = min ( warmColor + diffuseWarm * surfaceColor, 1.0 );
vec3 final = mix ( cool, warm, max(0,dot(n2,l2)));
float spec = pow ( max ( dot ( r, v2 ), 0.0 ), 32.0 );
color = vec4 ( min ( final + spec, 1.0 ), 1.0 );
}
По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows и Linux.
Обратите внимание, что для сборки примеров нужна последняя версия фреймворка, доступная через svn при помощи команды
svn checkout http://steps-framework.googlecode.com/svn/trunk/ steps-framework-read-only