На главную Назад Вперёд

Программирование графики, введение

В этой и нескольких следующих главах речь пойдёт об использовании технологии OpenGL для визуализации двухмерных и трёхмерных рисунков при помощи видеокарты. Изложение материала ведётся одновременно для языков C++ и Javascript.

Немного об OpenGL

OpenGL – стандартный программный интерфейс для работы с видеокартой. Появился в начале 90х годов, с тех пор постоянно обновляется и усовершенствуется. Тем не менее, на настоящий момент наиболее актуальной версией стандарта является OpenGL 2.0, датированная 2004 годом. Несмотря на глубокое техническое и моральное устаревание, это – единственная версия OpenGL, массово поддерживаемая браузерами и мобильными устройствами.

К тому же, для учебных целей версия OpenGL 2.0 почти идеальна, поскольку среди всех версий стандарта она является наиболее просто устроенной.

Инициализация OpenGL на C++

Для работы с 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 в браузере

Для того, чтобы воспользоваться функциональностью 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 и их именование

В скелете были использованы две функции 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);
}

Сборка вершиннофрагментной программы

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

Процедура компиляции вершинной или фрагментной программы следующая:

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

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

Сообщение об ошибках сборки можно получить функцией getProgramInfoLog.

После успешной сборки программы нужно применить к ней функцию useProgram.

Сборка ВФП (C++)

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

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 в браузере не позволяет общаться с файловой системой пользователя, текст вершинной и фрагментной программ приходится брать откуда-то ещё. Самый простой способ – просто вставить их в код. Для этого удобно использовать шаблонные текстовые литералы.

Входные данные для вершинной программы

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

Всё, теперь можно рисовать треугольники функцией drawArrays.

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

Входные данные на C++

К счастью, здесь всё максимально просто.

Буфер вершин заполняется так:

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