В этой и нескольких следующих главах речь пойдёт об использовании технологии OpenGL для визуализации двухмерных и трёхмерных рисунков при помощи видеокарты. Изложение материала ведётся одновременно для языков C++ и Javascript.
OpenGL – стандартный программный интерфейс для работы с видеокартой. Появился в начале 90х годов, с тех пор постоянно обновляется и усовершенствуется. Тем не менее, на настоящий момент наиболее актуальной версией стандарта является OpenGL 2.0, датированная 2004 годом. Несмотря на глубокое техническое и моральное устаревание, это – единственная версия OpenGL, массово поддерживаемая браузерами и мобильными устройствами.
К тому же, для учебных целей версия OpenGL 2.0 почти идеальна, поскольку среди всех версий стандарта она является наиболее просто устроенной.
Для работы с OpenGL требуется как минимум библиотека для работы с окнами и устройствами ввода. Наиболее часто используются GLFW и SDL. Для примеров мы будем использовать SDL.
Максимально упрощённый скелет приложения, использующего OpenGL, выглядит так:
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <GL/glew.h>
#include <SDL2/SDL.h>
SDL_Window * WND = nullptr;
void initGraphics() {
SDL_Init(SDL_INIT_VIDEO);
WND = SDL_CreateWindow("Hello",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
600, 600, // размеры окна
SDL_WINDOW_OPENGL);
SDL_GL_CreateContext(WND);
glewInit();
}
void renderFrame() {
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
SDL_GL_SwapWindow(WND); // переносит нарисованную картинку на окно
}
bool mainLoop() {
SDL_Event e;
if (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) { return false; }
}
renderFrame();
return true;
}
int main() {
initGraphics();
while ( mainLoop() ) {}
SDL_Quit();
}
Чтобы его скомпилировать, нужно скачать (или установить) библиотеки SDL2 и GLEW и указать при сборке приложения правильные пути к ним. В предположении POSIX-совместимого окружения с установленными библиотеками строка вызова компилятора может выглядеть как-то так:
g++ -o foobar foobar.cpp -lSDL2 -lGLEW -lGL -std=c++11
Для того, чтобы воспользоваться функциональностью OpenGL в браузере, требуется получить от canvas
-элемента так называемый webgl-контекст. Делается это следующим образом:
function initWebGL() {
let canvas = document.getElementById(/*идентификатор*/)
canvas.width = 600
canvas.height = 600
return canvas.getContext("webgl")
}
Соответственно, скелет приложения может выглядеть так:
window.onload = main
function main() {
GL = initWebGL()
renderFrame()
}
function renderFrame() {
GL.clearColor(0,0,0,1)
GL.clear(GL.COLOR_BUFFER_BIT)
}
function initWebGL() { ... }
В скелете были использованы две функции OpenGL: clear
и clearColor
(мы их будем именовать именно в таком стиле, как в Javascript). Первая из этих функций закрашивает все пиксели изображения одним и тем же цветом (кроме цвета она позволяет единообразно заполнять ещё некоторые свойства пикселей, которые мы обсудим позже). Вторая из них служит для настройки цвета, используемого clear
.
Вообще говоря, для OpenGL такой режим работы достаточно типичен: сначала вызываются несколько конфигурационных функций, а затем итоговая функция, производящая некоторое «сложное» действие на основе заданной конфигурации.
Основной режим работы видеокарты – рисование треугольников. Треугольник задаётся тремя вершинами, положение каждой из которых вычисляется при помощи т.н. вершинной программы (или, как её ещё называют, вершинного шейдера).
Цвет каждого из пикселей треугольника вычисляется при помощи фрагментной программы (также можно встретить термины фрагментный шейдер или пиксельный шейдер).
Шейдеры пишутся на языке GLSL – специальном диалекте языка C. Различные особенности GLSL мы будем подчёркивать по мере усложнения рассматриваемых примеров.
Начнём с тривиальных вершинной и фрагментной программ.
// вершинная программа
attribute vec4 pos;
void main() {
gl_Position = pos;
}
Глобальные переменные с модификатором attribute
– входные данные для вершинной программы, задающиеся индивидуально для каждой вершины. В глобальную переменную gl_Position
вершинная программа должна записать четырёхмерный вектор – вычисленное положение вершины. Назначение каждой из четырёх координат мы обсудим немного позднее.
// фрагментная программа
void main() {
gl_FragColor = vec4(1,1,1,1);
}
Для того, чтобы использовать вершинную и фрагментную программы, нужно их скомпилировать, а затем собрать из них объединённую вершиннофрагментную программу (далее ВФП).
Процедура компиляции вершинной или фрагментной программы следующая:
createShader
, на вход которой подаётся тип программы (VERTEX_SHADER
или FRAGMENT_SHADER
)shaderSource
compileShader
В тексте компилируемой программы могут быть ошибки. Сообщение компилятора об этих ошибках можно получить функцией getShaderInfoLog
. В языках C/C++ полная процедура получения сообщения компилятора довольно громоздкая (как будет видно дальше), в других языках она обычно короче.
После того, как вершинная и фрагментная программы скомпилированы, их можно собрать в единую ВФП следующим образом:
createProgram
attachShader
linkProgram
Сообщение об ошибках сборки можно получить функцией getProgramInfoLog
.
После успешной сборки программы нужно применить к ней функцию useProgram
.
Скомпилировать вершинную или фрагментную программу, записанную в файл, можно следующим образом:
GLuint compileShader(char const * filename, GLenum type) {
std::ifstream in(filename);
if (not in) {
std::cerr << "Couldn't find " << filename << std::endl;
return 0;
}
std::stringstream ss;
ss << in.rdbuf();
std::string contents = ss.str();
char const *source = contents.c_str();
auto shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
GLint infoLogLength;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
std::vector<char> infoLog(infoLogLength);
glGetShaderInfoLog(shader, infoLogLength, nullptr, infoLog.data());
std::cerr << "Couldn't compile " << filename << std::endl;
std::cerr << infoLog.data() << std::endl;
return 0;
}
return shader;
}
GLuint compileVertexShader(char const * filename) {
return compileShader(filename, GL_VERTEX_SHADER);
}
GLuint compileFragmentShader(char const * filename) {
return compileShader(filename, GL_FRAGMENT_SHADER);
}
Собрать из вершинной и фрагментной программ ВФП можно так:
GLuint linkProgram(GLuint vs, GLuint fs) {
auto program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status != GL_TRUE) {
GLint infoLogLength;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
std::vector<char> infoLog(infoLogLength);
glGetProgramInfoLog(program, infoLogLength, nullptr, infoLog.data());
std::cerr << "Couldn't link program " << std::endl;
std::cerr << infoLog.data() << std::endl;
return 0;
}
return program;
}
На Javascript код выглядит чуть проще:
function compileShader(source, type) {
let shader = GL.createShader(type)
GL.shaderSource(shader, source)
GL.compileShader(shader)
console.log(GL.getShaderInfoLog(shader))
if (!GL.getShaderParameter(shader, GL.COMPILE_STATUS)) { return null }
return shader
}
function compileVertexShader(source) {
return compileShader(source, GL.VERTEX_SHADER)
}
function compileFragmentShader(source) {
return compileShader(source, GL.FRAGMENT_SHADER)
}
function linkProgram(vs, fs) {
let program = GL.createProgram()
GL.attachShader(program, vs)
GL.attachShader(program, fs)
GL.linkProgram(program)
console.log(GL.getProgramInfoLog(program))
if (!GL.getProgramParameter(program, GL.LINK_STATUS)) { return null }
return program
}
Поскольку Javascript в браузере не позволяет общаться с файловой системой пользователя, текст вершинной и фрагментной программ приходится брать откуда-то ещё. Самый простой способ – просто вставить их в код. Для этого удобно использовать шаблонные текстовые литералы.
Настройка входных данных для вершинной программы на первый взгляд довольно трудоёмка. Она состоит из следующих этапов:
createBuffer
(glGenBuffers
в C/C++)ARRAY_BUFFER
при помощи функции bindBuffer
bufferData
(которая выделяет память и после этого копирует туда данные)getAttribLocation
vertexAttribPointer
(очень странное название, но что поделать…)enableVertexAttribArray
(не менее странное название…)Всё, теперь можно рисовать треугольники функцией drawArrays
.
Далее мы опишем некоторые особенности вышеупомянутых функций.
bufferData
– подсказка видеокарте о том, для каких целей будет использоваться буфер; в принципе, можно использовать любое значение, но рекомендуется всё же выставлять его правильноvertexAttribPointer
имеет значение только для целочисленных типов данных; если он истиннен, то целые числа преобразуются в долю, которую они составляют от максимально возможного значения соответствующего целочисленного типаvertexAttribPointer
довольно странный: он отвечает за промежуток между соседними значениями, но при этом он должен быть равен либо нулю (если соседние значения расположены друг за другом), либо разности между началами соседних значений (при этом, например, если одно значение занимает 4 байта, то 0 и 4 означают одно и то же!)К счастью, здесь всё максимально просто.
Буфер вершин заполняется так:
GLuint buffer; glGenBuffers(1,&buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
std::vector<float> vertices {-0.5, 0, 0, 1,
0.5, 0.5, 0, 1,
0.5, -0.5, 0, 1};
glBufferData(GL_ARRAY_BUFFER, vertices.size()*sizeof(float), vertices.data(), GL_STATIC_DRAW);
auto pos = glGetAttribLocation(program, "pos");
glVertexAttribPointer(pos, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(pos);
Нарисовать всё это можно командой
glDrawArrays(GL_TRIANGLES, 0, 3);
Второй и третий аргументы – положение начальной вершины в буфере вершин и количество вершин соответственно.
Всё производится ровно так же, как и в C++.
Инициализация:
let buffer = GL.createBuffer();
GL.bindBuffer(GL.ARRAY_BUFFER, buffer);
let vertices = [-0.5, 0, 0, 1,
0.5, 0.5, 0, 1,
0.5, -0.5, 0, 1];
GL.bufferData(GL.ARRAY_BUFFER, new Float32Array(vertices), GL.STATIC_DRAW);
let pos = GL.getAttribLocation(program, "pos");
GL.vertexAttribPointer(pos, 4, GL.FLOAT, false, 0, 0);
GL.enableVertexAttribArray(pos);
Рисование:
GL.drawArrays(GL.TRIANGLES, 0, 3)
@ 2016 arbrk1, all rights reversed