суббота, 28 августа 2010 г.

WebGL Урок 2 - Добавление цвета



Добро пожаловать в мой второй WebGL урок! На этот раз мы собираемся посмотреть, как добавить цвета на сцену. Он основан на уроке №3 из NeHe OpenGL учебника.
Вот как урок выглядит в браузере, который поддерживает WebGL:


Нажмите здесь, что бы посмотреть WebGL версию, если у вас есть браузер, который поддерживает ее; если у вас нет WebGL браузера, - вам поможет руководство по установке.

Подробнее о том, как все это работает:

На заметку: Эти занятия ориентированы на людей обладающих хорошими знаниями в области программирования, но не имеющих реального опыта в 3D-графике; целью ставим себе поскорее ввести в курс дела и прояснить, что происходит в коде, с тем, что бы Вы смогли начать делать собственные 3D веб-страницы как можно быстрее. Если вы еще не читали первый урок, то вам следует сделать это перед чтением этого - здесь я буду только объяснить различия между кодом предыдущего урока и новым кодом.

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

Вы можете получить код для этого примера через "View Source" браузера, еще можно скачать код с GitHub, этот и другие уроки расположены в этом репозитарии. В любом случае, когда получите код, открывайте его в вашем любимом текстовом редакторе и посмотрите.

Большая часть должна выглядеть так же как в первом уроке. Пробежимся по коду:
Определяем вершинный и пиксельный шейдеры, используя HTML <script> теги типа "x-shader/x-vertex" и "x-shader/x-fragment"
Инициализируем WebGL контекст в initGL
Загружаем шейдеры в объект программы WebGL с помощью getShader и initShaders .
Определяем model-view матрицу mvMatrix наряду с функциями loadIdentity , multMatrix , mvTranslate для манипулирования матрицей.
Определяем матрицу проекции pMatrix и функцию perspective для манипулирования этой матрицей.
Определяем setMatrixUniforms для перемещения model-view матрицы и матрицы проекции из JavaScript в WebGL с тем, чтобы шейдеры могли их увидеть.
Загружаем буферы, содержащие информацию об объектах в сцене с использованием initBuffers
Рисуем эту сцену, в drawScene.
Определим функцию webGLStart для установки первоочередных настроек
Наконец, мы добавляем минимально HTML’а, необходимого для отображения всего этого.

Единственное, что изменилось в этом коде с первого урока, это шейдеры, а так же функции initBuffers и drawScene. Для того чтобы объяснить, в чем заключаются изменения, вам нужно немного узнать о WebGL рендеринг конвейере. Вот схема:
Диаграмма показывает, в очень упрощенной форме, как данные, передаваемые функциям JavaScript в drawScene, превращаются в пиксели, отображаемые в WebGL canvas на экране. Она только показывает шаги, необходимые для объяснения этого урока, и мы увидим на более подробные версии в дальнейших уроках.

На самом высоком уровне, этот процесс работает следующим образом: каждый раз при вызове функции, такой как drawArrays, WebGL обрабатывает данные, которые вы ранее загрузили в виде атрибутов (например, буферы мы использовали для вершин в уроке №1) и uniform переменных (который мы использовали для матриц model-view и проекции), и передает их в вершинный шейдер.

Происходит это путем вызова вершинного шейдера по одному разу для каждой вершины, каждый раз с атрибутами соответствующими этой вершине; uniform переменные также передаются, но, как следует из их названия, они не меняются от вызова к вызову. Вершинный шейдер обрабатывает эти данные - в уроке 1, он применил матрицы проекции и model-view так, что вершины все будут в перспективе и перемещены в соответствии с нашим текущим состоянием model-view - и помещает результаты в, так называемые, varying переменные. Результатом его работы может быть ряд varying переменных, в частности одна из них является обязательной, gl_Position , которая содержит координаты вершины после того как шейдер закончил возиться с ней.

После того как вершинный шейдер отработает, WebGL производит манипуляции необходимые для преобразования 3D-изображения из этих varying переменных в 2D изображение, а затем он вызывает пиксельный шейдер по одному разу для каждого пикселя изображения. Конечно, это означает, что он вызовет пиксельный шейдер для тех пикселей, в которых нет вершины - то есть, пикселей которые находятся между вершинами фигуры. Пространство между вершинами заполняется пикселями с помощью процесса линейной интерполяции - для позиций вершин, которые составляют наш треугольник, этот процесс "заполняет" точками пространство между вершинами, чтобы сделать треугольник видимым. Целью пиксельного шейдера является возвращение цвета для каждого из этих интерполированных точек, и он делает это в varying переменной gl_FragColor .

После того как пиксельный шейдер отработает, его результаты немного размываются WebGL (опять же, мы рассмотрим это в дальнейшем), и они помещаются в кадровый буфер, который в конечном счете является тем, что отображается на экране.

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

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

Удобно то, что это дает нам градиенты цветов автоматически. Все varying переменные установленные вершинным шейдером линейно интерполируются при создании пикселей между вершинами, не только на позиции. Линейная интерполяция цвета между вершинами дает плавный градиент, вы можете это увидеть в треугольнике на картинке выше.

Давайте посмотрим на код, мы будем работать, изменяя 1 урок. Во-первых, вершинный шейдер. В нем изменений довольно много, вот новый код:

attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec4 vColor;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
}

В коде создаются два атрибута - значения, которые варьируются от вершины к вершине - называются aVertexPosition и aVertexColor, две uniform переменные называются uMVMatrix и uPMatrix и одна varying переменная vColor .

В теле шейдера, мы рассчитываем gl_Position (который косвенно определен в качестве varying переменной для каждого вершинного шейдера) точно так же, как мы это делали в уроке 1, а цвет мы просто передаем из атрибута в varying переменную.

После того как вершинный шейдер отработал для каждой вершины, с помощью интерполяции созданы пиксели, и они передаются в пиксельный шейдер:

#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main(void) {
gl_FragColor = vColor;
}

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

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

var shaderProgram;
function initShaders() {
var fragmentShader = getShader(gl, "shader-fs");
var vertexShader = getShader(gl, "shader-vs");

shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);

if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}

gl.useProgram(shaderProgram);

shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}

Этот код, получающий атрибуты (который мы постарались объяснить в первом уроке) должен теперь быть совершенно понятным: мы получаем ссылки на атрибуты, которые хотим передать в вершинный шейдер для каждой вершины. В первом уроке мы просто сделали ссылку на атрибут позиции вершины. Теперь, мы к тому создаем ссылку на атрибут цвета.

Остальные изменения в этом уроке находятся в функции initBuffers, которой теперь необходимо создать буферы для двух атрибутов, и в drawScene, которая должна передать оба атрибута в WebGL.
Посмотрим на initBuffers сперва, мы определяем новые глобальные переменные для хранения буферов цветов для треугольника и квадрата:

var triangleVertexPositionBuffer;
var triangleVertexColorBuffer;
var squareVertexPositionBuffer;
var squareVertexColorBuffer;

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

function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
var vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;

triangleVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
var colors = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
triangleVertexColorBuffer.itemSize = 4;
triangleVertexColorBuffer.numItems = 3;

Таким образом, значения, которые мы обеспечиваем для цвета находятся в списке, один набор значений для всех вершин, как для позиций вершин. Однако, есть одно интересное отличие между этими двумя массивами буферов: в то время как позиции вершин задаются в виде трех цифр для каждой вершины, X, Y и Z координаты, их цвета задаются в виде 4 элементов - красный, зеленый, синий и альфа. Альф является мерой прозрачности (0 прозрачный, 1 полностью непрозрачный) и будет полезна в следующих уроках. Это изменение в количестве элементов для каждой вершины в буфере требует изменения, связанного с ним itemSize.

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

squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;

squareVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
colors = []
for (var i=0; i <>
colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
}
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
squareVertexColorBuffer.itemSize = 4;
squareVertexColorBuffer.numItems = 4;

Теперь у нас есть все данные для наших объектов в виде 4 буферов, поэтому далее мы изменим drawScene, что бы он использовал наши новые данные. Новый код отмечен красным цветом:

function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
loadIdentity();

mvTranslate([-1.5, 0.0, -7.0])

gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

mvTranslate([3.0, 0.0, 0.0])
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

setMatrixUniforms();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
}

Следующее изменение... подождите, а больше изменений нет! Это было все, что необходимо, что бы добавить цвет на нашу WebGL сцену, и, надеюсь, вы теперь знакомы с основами шейдеров и каким образом данные передаются между ними.

С этим уроком все - я надеюсь, было легче, чем с первым уроком! Если у вас есть какие-либо вопросы, комментарии или корректировки, пожалуйста, оставьте комментарий ниже.


Благодарности: разобраться, что происходит в конвейере рендеринга стало гораздо легче, с помощью книги OpenGL ES 2.0 Programming Guide, которую Джим Пик рекомендовал в его WebGL блоге. Как всегда, я благодарен NeHe за его OpenGL учебник, за скрипты для этого урока.

4 комментария: