OpenGL программирование/Современный OpenGL учебное пособие 02

Материал из Викиучебника — открытых книг для открытого мира

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

Управление шейдером[править]

Загрузка шейдеров[править]

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

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

/**
 * Всё хранимое содержимое файла в памяти, используется для передачи в шейдеры
 * Исходный код OpenGL
 */
/*Проблема:
  * Мы должны закрыть входной файл перед возвращением NULL; Но это приведет к большому количеству повторов
  * Вы Могли бы решить это с помощью goto, либо посредством злоупотребления переключатель switch/for/while + break или путем создания еще большего беспорядока
  * Лучшее решение: пусть пользователь обрабатывать файл: char* file_read(const FILE* input)
*/
char* file_read(const char* filename)
{
  FILE* input = fopen(filename, "rb");
  if(input == NULL) return NULL;
  
  if(fseek(input, 0, SEEK_END) == -1) return NULL;
  long size = ftell(input);
  if(size == -1) return NULL;
  if(fseek(input, 0, SEEK_SET) == -1) return NULL;
  	
  /*if using c-compiler: dont cast malloc's return value*/
  char *content = (char*) malloc( (size_t) size +1  ); 
  if(content == NULL) return NULL;
  
  fread(content, 1, (size_t)size, input);
  if(ferror(input)) {
    free(content);
    return NULL;
  }

  fclose(input);
  content[size] = '\0';
  return content;
}

Отладка шейдеров[править]

В настоящее время, если есть ошибка в наших шейдерах, то программа просто останавливается без объяснения того, что за ошибка произошла в этом частностом случае. Мы можем получить больше информации от OpenGL с использованием Infolog:

/**
 * Отобразим ошибки компиляции, полученные от компилятора шейдеров OpenGL
 */
void print_log(GLuint object)
{
  GLint log_length = 0;
  if (glIsShader(object))
    glGetShaderiv(object, GL_INFO_LOG_LENGTH, &log_length);
  else if (glIsProgram(object))
    glGetProgramiv(object, GL_INFO_LOG_LENGTH, &log_length);
  else {
    fprintf(stderr, "printlog: Not a shader or a program\n");
    return;
  }

  char* log = (char*)malloc(log_length);

  if (glIsShader(object))
    glGetShaderInfoLog(object, log_length, NULL, log);
  else if (glIsProgram(object))
    glGetProgramInfoLog(object, log_length, NULL, log);

  fprintf(stderr, "%s", log);
  free(log);
}

Абстрактные различия между OpenGL и GLES2[править]

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

  • GLSL #version отличаются
  • GLES2 требует точности,это не совместимо с OpenGL 2.1.

#version нуждаются в указании самыми первыми строками в некоторых GLSL компиляторах (например, на PowerVR SGX540), поэтому мы не можем использовать #ifdef директивы. Вместо этого, мы будем писать первыми строками C++ код связанный с версиями:

  const GLchar* sources[2] = {
#ifdef GL_ES_VERSION_2_0
    "#version 100\n"
    // Note: OpenGL ES automatically defines this:
    // #define GL_ES
#else
    "#version 120\n",
#endif
    source };
  glShaderSource(res, 2, sources, NULL);

Так как мы используем одну и ту же версию GLSL во всех наших учебных пособиях, это наиболее простое решение. Мы охватим #ifdef и прочие уточнения в следующем разделе.

Многоразовая функция создания шейдеров[править]

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

/**
 *  Компиляция шейдеров из файла 'имя_файла', с обработкой ошибок
 */
GLuint create_shader(const char* filename, GLenum type)
{
  const GLchar* source = file_read(filename);
  if (source == NULL) {
    fprintf(stderr, "Error opening %s: ", filename); perror("");
    return 0;
  }
  GLuint res = glCreateShader(type);
  const GLchar* sources[2] = {
#ifdef GL_ES_VERSION_2_0
    "#version 100\n"
    "#define GLES2\n",
#else
    "#version 120\n",
#endif
    source };
  glShaderSource(res, 2, sources, NULL);
  free((void*)source);

  glCompileShader(res);
  GLint compile_ok = GL_FALSE;
  glGetShaderiv(res, GL_COMPILE_STATUS, &compile_ok);
  if (compile_ok == GL_FALSE) {
    fprintf(stderr, "%s:", filename);
    print_log(res);
    glDeleteShader(res);
    return 0;
  }

  return res;
}

Теперь мы можем легко составить шейдер:

  GLuint vs, fs;
  if ((vs = create_shader("triangle.v.glsl", GL_VERTEX_SHADER))   == 0) return 0;
  if ((fs = create_shader("triangle.f.glsl", GL_FRAGMENT_SHADER)) == 0) return 0;

а также показать ошибки:

  if (!link_ok) {
    fprintf(stderr, "glLinkProgram:");
    print_log(program);
  }

Поместите новые функции в отдельном файле[править]

Мы размещаем эти новые функции в shader_utils.cpp. Note that we intend to write as few of these functions as possible: the OpenGL Wikibook's goal is to understand how OpenGL works, not how to use a toolkit that we develop. Давайте создадим файл заголовка shader_utils.h:

#ifndef _CREATE_SHADER_H
#define _CREATE_SHADER_H
#include <GL/glew.h>
char* file_read(const char* filename);
void print_log(GLuint object);
GLuint create_shader(const char* filename, GLenum type);
#endif

Ссылка на новый файл в triangle.cpp:

#include "shader_utils.h"

И в Makefile:

triangle: shader_utils.o

Использование объектов вершинного буфера (VBO - Vertex Buffer Objects)[править]

Хорошей практикой является хранение вершин напрямую в памяти видеокарты, используя Vertex Buffer Object (VBO)

В добавок, поддержка "массивов с клиентской стороны" была официально прекращена с OpenGL 3.0, отсутствует в WebGL и работает медленней. С этого момента давайте использовать VBO, даже если этот метод покажется вам сложнее. Важно понимать оба подхода к созданию графики, потому что они могут встретиться вам в других OpenGL программах.

Разделим задачу на 2 этапа:

  • Создать VBO с нашими вершинами
  • Связать VBO перед вызовом glDrawArray

Создадим глобальную переменную для хранения указателя на VBO (ниже #include):

GLuint vbo_triangle;

Перемещаем определение triangle_vertices в init_resources, создаем один (1) вершинный буфер и выбираем его как активный буфер:

  GLfloat triangle_vertices[] = {
     0.0,  0.8,
    -0.8, -0.8,
     0.8, -0.8,
  };
  glGenBuffers(1, &vbo_triangle);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);

Теперь в буфер можно вложить информацию о наших вершинах. Мы указываем как организованы наши данные и как часто они будут использоваться. GL_STATIC_DRAW обозначает, что запись в него будет происходить нечасто, и GPU должен хранить копию буфера в памяти видеокарты. Всегда можно перезаписать VBO. Но если данные изменяются каждый кадр или чаще, то следует использовать GL_DYNAMIC_DRAW или GL_STREAM_DRAW.

  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_vertices), triangle_vertices, GL_STATIC_DRAW);

В любое время можно установить значение активного буфера в 0 (сбросить буфер):

  glBindBuffer(GL_ARRAY_BUFFER, 0);

Не забывайте сбрасывать буфер, чтобы не возникло непредвиденных ошибок.

Теперь мы слегка адаптируем код в onDisplay. Вызываем glBindBuffer и изменяем последние два параметра в glVertexAttribPointer:

  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glEnableVertexAttribArray(attribute_coord2d);
  /* Описываем массив для OpenGL (Он не сможет угадать его вид самостоятельно) */
  glVertexAttribPointer(
    attribute_coord2d, // атрибут
    2,                 // кол-во элементов на каждую вершину. В данном случае - (x, y) 
    GL_FLOAT,          // тип каждого элемента
    GL_FALSE,          // взять данные без изменения
    0,                 // нет дополнительных данных между вершинами
    0                  // смещение первого элемента
  );

Не забудьте освободить ресурсы перед выходом из программы:

void free_resources()
{
  glDeleteProgram(program);
  glDeleteBuffers(1, &vbo_triangle);
}

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

Проверка версии OpenGL[править]

Некоторые видеокарты не поддерживают OpenGL 2. На них ваша программа вылетит или сцена будет отображаться не полностью. Это можно проверить в GLEW (разумеется после вызова glewInit()):

  if (!GLEW_VERSION_2_0) {
    fprintf(stderr, "Ошибка: ваша видеокарта не поддерживает OpenGL 2.0\n");
    return 1;
  }

Прошу заметить, что некоторые программы из этих уроков могут работать с ранними 2.0 картами, как Intel 945GM с минимальной поддержкой шейдеров, но официально поддерживающий лишь OpenGL 1.4.

Альтернатива GLEW[править]

Вы можете встретить эти заголовки в других программах:

#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>

Если вы не желаете загружать библиотеки OpenGL и ваши заголовки "достаточно современные", то можете использовать это вместо GLEW. Наши тесты показали, что у некоторых пользователей Windows могут быть неактуальные заголовки, в которых отсутствует GL_VERTEX_SHADER. Мы в уроках будем использовать GLEW (кроме того, мы будем готовы к загрузке расширений).

Также рекомендуем сравнить GLEW и GLEE в APIs, Libraries and acronyms

Один пользователь сообщил, что он использует этот способ вместо GLEW на Intel 945GM GPU, чтобы частично получить поддержку OpenGL 2.0 для простых уроков. GLEW самостоятельно может обеспечить частичную поддержку с помощью glewExperimental = GL_TRUE; перед вызовом glutInit.

Включение прозрачности[править]

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

Сперва, инициализируем прозрачность в GLUT.

glutInitDisplayMode(GLUT_RGBA|GLUT_ALPHA|GLUT_DOUBLE|GLUT_DEPTH);

Теперь необходимо включить прозрачность в OpenGL (изначально она выключена). Добавьте этот код в onDisplay():

// Включение прозрачности
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
The rendered triangle, partially transparent

Осталось только модифицировать фрагментный шейдер для поддержки прозрачности:

  gl_FragColor[0] = gl_FragCoord.x/640.0;
  gl_FragColor[1] = gl_FragCoord.y/480.0;
  gl_FragColor[2] = 0.5;
  if (mod(gl_FragCoord.y, 30.0) > 15)
    gl_FragColor[3] = 1;
  else
    gl_FragColor[3] = 0.4;

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

Шаблон:OpenGL Programming BottomNav