ПрограммированиеСтатьиГрафика

OpenGL для школьников

Внимание! Этот документ ещё не опубликован.

Автор:

OpenGL для школьников. Часть 1


Copyright © 2013 by Volodymyr Samokhatko Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3; with the Invariant Sections being "Введение" and all sub-sections, with the Front-Cover Texts being "Original Author: Volodymyr Samokhatko", and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". All scripts in this tutorial are covered by the GNU General Public License. The scripts are free source; you can redistribute them and/or modify them under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License, or (at your option) any later version. These scripts are distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License within this tutorial, under the section entitled "GNU General Public License"; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

Введение

Цель статьи - дать возможность внезапно научится писать графические приложения с использованием OpenGL.

Предполагается, что читатель немного знаком с каким-то императивным языком программирования, в котором есть классы (С++, Java, C#, Delphi, Python, VB...).

Работа будет вестись на языке JavaScript, но, на самом деле, это не имеет особого значения. Мне доводилось пользоваться OpenGL в Delphi, Java и С++, и, могу сказать, что разница несущественна. Родным языком для себя считаю C++, так что код будет написан, как бы, из расчета на C++.

Приложение будет запускаться в web-браузере, но соединения с интернетом для этого не потребуется. Для написания и отладки нужен, соответственно, только браузер, который поддерживает WebGL. Такой, как Mozilla Firefox, например.

Подготовка


Архив с текстом и исходниками здесь: http://ubuntuone.com/1UyR2G2sYapf1cCnpyFsKY

Чтобы отлаживать приложение в Firefox, нужно установить в нем дополнение Firebug (https://addons.mozilla.org/ru/firefox/addon/firebug/). После установки, в главном меню браузера под пунктом "Инструменты" появляется раздел "Веб-разработка->Firebug". Здесь нам интересен Debugger, которым можно дебажить JavaScript, а также полезна Веб-консоль, куда выводятся ошибки (переключатель "JS") и логи (переключатель "Logging").

В логи можно писать из приложения так:

console.log("log message!");

Шаблон

Приложение является Web-страницей. Это HTML файл, с одним элементом "canvas" (поверхностью для рисования) и одним скриптом JavaScript.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>

<head>
<title>Learning WebGL</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">

<script type="text/javascript" src="main.js"></script>

</head>

<body onload="webGLStart();">
    <canvas id="lesson-canvas" style="border: none;" width="320" height="240"></canvas>
</body>

</html>
index.html

На ней нам интересна строчка:

<script type="text/javascript" src="main.js"></script>
index.html

которая прикрепляет наш скриптовый файл main.js к странице.

А также, еще одна:

<body onload="webGLStart();">
index.html

которая говорит, что когда страница загрузится, то будет запущена JavaScript-функция webGLStart из main.js:

function webGLStart() {
    var canvas = document.getElementById("lesson-canvas");
    initGL(canvas);
    initShaders();
    initBuffers();

    drawScene();        
}
main.js

из HTML будет получен объект canvas - область рисования и передан в функцию initGL.

Код функции initGL означает следующее: все, что мы будем рисовать через объект "gl", будет выводится на наш элемент canvas:

var gl;
function initGL(canvas) {
    gl = canvas.getContext("experimental-webgl");
    if (gl == null) {
        alert("Could not initialize WebGL");
        return;
    }

    gl.viewportWidth = canvas.width;
    gl.viewportHeight = canvas.height;
}
main.js

Функции загрузки шейдеров (initShaders) и генерирования модели (initBuffers) оставим пока пустыми:

function initShaders() {
}

function initBuffers() {
}
main.js

А в функции рисования (drawScene) сделаем три вещи: установим размер области рисования по размерам элемента canvas, установим цвет заливки (4 компоненты RGBA: красный Red, зеленый Green, синий Blue, непрозрачность Alpha) и зальем всю область этим цветом:

function drawScene() {
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0.9, 0.8, 0.7, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
}
main.js

Значения цветов задаются в пределах от 0 до 1.

Все функции, которые вызываются через объект "gl" - это функции WebGL. Описания можно смотреть на сайте: http://www.opengl.org/sdk/docs/man/. Все функции без описаний, разбитые на категории доступны в форме Quick Reference Card: http://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf.

Можно теперь открыть в браузере файл index.html. Если появилось окошко с надписью "Could not initialize WebGL" (оно из нашей функции initGL: alert("Could not initialize WebGL")), то это означает, что WebGL не активирован или не поддерживается в браузере. А, возможно, что существуют вообще какие-то проблемы с драйверами или видеокартой.

Должен получится прямоугольник, залитый цветом, который передали функции gl.clearColor. Размеры его - 320*240, как было ранее заявлено в HTML файле:

<canvas id="lesson-canvas" style="border: none;" width="320" height="240"></canvas>
index.html

Рисование геометрии

Чтобы что-то нарисовать, нужно разобраться в том, что делает графический конвейер:
GraphicsPipeline | OpenGL для школьников
На вход ему нужно подать Vertex Buffer - список координат вершин и указать, что из них надо собирать треугольники. После этого, драйвер для каждой вершины запустит шейдер вершин (Vertex Shader), который мы ему дадим. Он пишется на GLSL и предназначен для того, чтобы двигать вершины туда, куда нам нужно. Потом будут собраны треугольники и определены пиксели на экране, которые принадлежат этим треугольникам. Для каждого пикселя драйвер запустит фрагментный шейдер (Fragment Shader), который тоже нужно задать. Пишется на GLSL и предназначен для того, чтобы красить пиксели в нужный цвет.

Напишем вершинный шейдер, который на вход принимает двухмерную позицию вершины (in_Pos - это x, y) и выдает её без изменений (gl_Position - это x, y, 0, 1):

attribute vec2 in_Pos;

void main(void) {
    gl_Position = vec4(in_Pos, 0.0, 1.0);
}

  • четвертая координата в gl_Position - это коэффициент, на который OpenGL делит все координаты. Вершинный шейдер должен возвращать четырехмерный вектор.
  • Вот как OpenGL сопоставляет координаты из gl_Position и положение в области рисования:
    ScreenCoords | OpenGL для школьников
    Центр координат расположен посредине canvas. Ось x направлена вправо, ось y - вверх.

    Если хоть одна из трех x, y, z координат вершины меньше -1.0 или больше 1.0, то вершина будет за пределами canvas. Следует заметить, что деление на четвертый коэффициент из gl_Position происходит до этого.

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

    precision mediump float;
    
    void main(void) {
        gl_FragColor = vec4(0.6, 0.7, 0.9, 1.0);
    }

    Формат цвета здесь тоже RGBA. В самой первой строчке идет задание точности float-ов, поскольку комитет по стандартизации решил, что в этой версии фрагментных шейдеров программист должен указывать точность сам.

    Мы убедились, что GLSL шейдеры - это обычные строки текста, которые нужны для работы OpenGL. Разместим их внутри HTML страницы, чтобы загружать их в нашей JavaScript функции initShaders:

    <script id="vertex-shader" type="text/plain">
        attribute vec2 in_Pos;
    
        void main(void) {
            gl_Position = vec4(in_Pos, 0.0, 1.0);
        }
    </script>
    
    <script id="fragment-shader" type="text/plain">
        precision mediump float;
    
        void main(void) {
            gl_FragColor = vec4(0.6, 0.7, 0.9, 1.0);
        }
    </script>
    index.html

    Для доставания этого GLSL кода из HTML тегов, будет использоваться самописная функция getTextFromElement, которая на вход принимает id элемента (в данном случае "vertex-shader" или "fragment-shader"), и возвращает внутренний текст.

    Наша функция загрузки шейдера будет принимать его текст (параметр str) и его тип (параметр type), создавать "шейдерный объект" (переменная shader), прикреплять к нему текст шейдера (функция gl.shaderSource), компилировать (функция gl.compileShader), проверять ошибки компиляции (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) и возвращать шейдерный объект:

    function makeShader(str, type) {
        var shader = gl.createShader(type);
        
        gl.shaderSource(shader, str);
        gl.compileShader(shader);
    
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            alert(gl.getShaderInfoLog(shader));
            return null;
        }
    
        return shader;
    }
    main.js

    Эти шейдерные объекты нужно будет собрать в один "программный объект". Итак, в функции initShaders создадутся два шейдерных объекта vertexShader и fragmentShader со скомпилированными шейдерами. Эти объекты прикрепятся функцией gl.attachShader к новому программному объекту shaderProgram. Потом произойдет компоновка программного объекта - gl.linkProgram(shaderProgram) и проверка на ошибки.

    var shaderProgram;
    var vertexPositionAttribute;
    
    function initShaders() {
        var vertexShader = makeShader(getTextFromElement("vertex-shader"), gl.VERTEX_SHADER);
        var fragmentShader = makeShader(getTextFromElement("fragment-shader"), gl.FRAGMENT_SHADER);
    
        shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);
    
        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
            alert("Could not initialize shaders");
        }
    
        vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "in_Pos");
        gl.enableVertexAttribArray(vertexPositionAttribute);
    }
    main.js

    Последние два вызова заботятся о том, чтобы на вход вершинному шейдеру можно было подать Vertex Buffer - список координат вершин. gl.getAttribLocation служит для получения идентификатора входного атрибута in_Pos, который мы объявили в шейдере. gl.enableVertexAttribArray сообщает драйверу, что он действительно будет использоваться для передачи данных.

    Создаваться и заполняться буфер будет в функции initBuffers:

    var squareVertexPositionBuffer;
    var itemSize, numItems;
    
    function initBuffers() {
        squareVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        vertices = [
             1.0, 1.0,
            -1.0, 1.0,
             1.0, -1.0,
            -1.0, -1.0,
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        itemSize = 2;
        numItems = 4;
    }
    main.js

    gl.bindBuffer служит для выбора буффера squareVertexPositionBuffer, чтобы функция gl.bufferData записала в него массив вершин vertices. В переменные itemSize и numItems запомним размерность (2) и количество вершин (4).

  • vertices - массив. В JavaScript массивы можно определять так:
  • myArray = [element1, element2, element3];

    Дополним функцию рисования drawScene.

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

    Дальше gl.bindBuffer активирует squareVertexPositionBuffer (буффер с координатами вершин) для того, чтобы присоединить его к входному атрибуту in_Pos (идентификатор атрибута in_Pos - это vertexPositionAttribute, который мы получили ранее в initShaders) с помощью функции gl.vertexAttribPointer.

    function drawScene(canvas) {
        gl.viewport(0, 0, canvas.width, canvas.height);
        gl.clearColor(0.9, 0.8, 0.7, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
    
        gl.useProgram(shaderProgram);
    
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        gl.vertexAttribPointer(vertexPositionAttribute, itemSize, gl.FLOAT, false, 0, 0);
    
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, numItems);
    }
    main.js

    Под конец, gl.drawArrays будет рисовать всё, что поступает через in_Pos: начиная с нулевого поступившего и в общей сложности numItems штук (numItems == 4). И из этого будет делаться полоска треугольников gl.TRIANGLE_STRIP. То есть, наши четыре двухмерные вершины пойдут на вход вершинному шейдеру, а из четырех вершин, которые мы из шейдера выйдадим, будут сложены треугольники.

    Запускаем - видим прямоугольник на весь canvas.

    Вершинный шейдер создан для того, чтобы организованно двигать вершины. То есть, можно начинать развлекаться:
    * я буду использовать далее в статье diff-запись: строчки, которые удаляются помечены знаком "-", а те, которые добавляются - знаком "+" (те, которые не меняются, никак не помечены). В заголовках с символами @@ указаны номера и количества строчек, а иногда и имя функции, в которой они находятся. Эти заголовки не являются частью программы, а используются только в статье.

    @@ -9,7 +9,7 @@
        attribute vec2 in_Pos;
     
        void main() {
    -        gl_Position = vec4(in_Pos, 0.0, 1.0);
    +        gl_Position = vec4(in_Pos * 0.5, 0.0, 1.0);
        }
    index.html

    То есть заменим "gl_Position = vec4(in_Pos, 0.0, 1.0);" на "gl_Position = vec4(in_Pos * 0.5, 0.0, 1.0);" в вершинном шейдере. Это умножает на 0.5 обе входные координаты всех четырех вершин (уменьшая в 2 раза прямоугольник).

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

    Масштабирование

    Заведем переменную, которую можно задавать из скрипта. Их называют "юниформами" (и почему-то не в женском роде). Завернем её сразу в структуру:

    @@ -8,8 +8,13 @@
     <script id="vertex-shader" type="text/plain">
      attribute vec2 in_Pos;
     
    +  uniform struct nav_s
    +  {
    +    float zoom;
    +  } nav;
    +
      void main() {
    -    gl_Position = vec4(in_Pos, 0.0, 1.0);
    +    gl_Position = vec4(in_Pos * nav.zoom, 0.0, 1.0);
      }
     </script>
    index.html

    uniform - значит общая для всех вершин, struct - значит структура.

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

    В JavaScript cоздадим класс Uniforms, отвечающий за связь с nav.zoom.

    В конструкторе "Uniforms()", функция gl.getUniformLocation будет извлекать из шейдерной программы идентификатор юниформа nav.zoom:

        function Uniforms() {
            this.zoomId = gl.getUniformLocation(shaderProgram, "nav.zoom");
        }

    Используя полученный в конструкторе идентификатор юниформа, метод обновления refresh будет принимать объект n и передавать в шейдер его поле zoom:

        Uniforms.prototype.refresh = function(n) {
            gl.uniform1f(this.zoomId, n.zoom);
        }

    gl.uniform1f называется так потому, что 1f - это 1 float.

    Создание экземпляра объекта:

        uniforms = new Uniforms();

    Добавим все это в функцию initShaders:

    @@ -54,6 +54,7 @@
     
     var shaderProgram;
     var vertexPositionAttribute;
    +var uniforms;
     
     function initShaders() {
        var vertexShader = makeShader(getTextFromElement("vertex-shader"), gl.VERTEX_SHADER);
    @@ -70,6 +71,16 @@ function initShaders() {
     
        vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "in_Pos");
        gl.enableVertexAttribArray(vertexPositionAttribute);
    +
    +   function Uniforms() {
    +       this.zoomId = gl.getUniformLocation(shaderProgram, "nav.zoom");
    +   }
    +
    +   Uniforms.prototype.refresh = function(n) {
    +       gl.uniform1f(this.zoomId, n.zoom);
    +   }
    +
    +   uniforms = new Uniforms();
     }
    main.js

    Для применения в отображении, нужно обновлять значение перед прорисовкой:

    -function drawScene(canvas) {
    +function drawScene(canvas, n) {
        gl.viewport(0, 0, canvas.width, canvas.height);
        gl.clearColor(0.9, 0.8, 0.7, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
    @@ -99,6 +110,8 @@ function drawScene(canvas) {
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        gl.vertexAttribPointer(vertexPositionAttribute, itemSize, gl.FLOAT, false, 0, 0);
     
    +   uniforms.refresh(n);
    +
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, numItems);
     }
    main.js

    И передать какое-то значение:

    @@ -108,6 +121,10 @@ function webGLStart() {
        initShaders();
        initBuffers();
     
    -    drawScene(canvas);       
    +    var navigation = {
    +        zoom: 1.0
    +    }
    +
    +    drawScene(canvas, navigation);
     }
    main.js

    Теперь можно задавать масштаб из JavaScript, меняя этот 1.0 на что-то меньшее.

    Страницы: 1 2 Следующая »

    #2D графика и изометрия, #графика, #OpenGL, #основы

    1 мая 2013

    Комментарии [4]