WebGL туториал 2 (Цветная плоскость в 3d)
В предыдущей части туториала я описывал простейший случай рисования в WebGL. Теперь мы немного усложним имеющийся код. Изменения коснутся шейдеров, мы научимся передавать в них произвольные данные. Наконец появится пару строчек на тему алгебры. Нам придется задействовать матричные преобразования, чтобы говорить о 3D в привычном понимании.Вкратце, в конце туториала мы получим квадрат раскрашенный градиентным кружочком, который будет поворачиваться вслед за курсором. Полный код примера тут.
Введение
Эта часть туториала будет сложнее предыдущей. Тут мне пришлось коснуться математки, к счастью не самого сложного её раздела. Но в целом, я разобрался с чем хотел и желаю читателю того же. Я сразу покажу результат в IFrame, возможно так чтиво будет интереснее:
Шейдеры
Первым делом, оформим наши шейдеры. На данный момент они хранятся в строках. Однако, удобнее выделить для них место среди скриптов. Мы можем получить доступ к содержимому тегов script через свойство innerHTML так же легко, как и к содержимому элементов на странице. Ниже показано, как будет выглядеть код оформленных шейдеров к концу статьи. В этом коде важно только оформление, а наполнение я объясню позже.<head>
//....
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float; //medium float precision
varying vec2 vCoord;
void main(void) {
gl_FragColor = vec4(1.0,1.0,1.0, 1.0);
}
</script>
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition; //current vertex position
uniform mat4 uMVMatrix; //move matrix
uniform mat4 uPMatrix; //perspective matrix
void main(void) {
//Calculate vertex position
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>
//....
<script type="text/javascript">
//....
//Find and compile shader
function getShader(id) {
var shaderScript = document.getElementById(id);
var shader;
if (shaderScript.type == "x-shader/x-fragment")
shader = gl.createShader(gl.FRAGMENT_SHADER);
if (shaderScript.type == "x-shader/x-vertex")
shader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shader, shaderScript.innerHTML);
gl.compileShader(shader);
console.log(gl.getShaderInfoLog(shader));
return shader;
}
//Link and install compiled shaders
function initShaders() {
//Create a shader program
var shaderProgram = gl.createProgram();
//Attach compiled shaders
gl.attachShader(shaderProgram, getShader("shader-vs"));
gl.attachShader(shaderProgram, getShader("shader-fs"));
//Link and install program to GPU
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
//Get and save variable addresses
shaderMemory["aVertexPosition"] = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
shaderMemory["uPMatrix"] = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderMemory["uMVMatrix"] = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
//....
</script>
В коде можно увидеть метод getShader, используемый для получения готового шейдера по id элемента script и почти не изменившуюся функцию инициализации шейдеров. К остальному вернемся после алгебры.
Матрицы
Второе изменение, как я написал выше - введение матричных преобразований. Нам потребуется определить 2 матрицы:- матрицу перспективы
- матрицу перемещения
Матрица перспективного преобразования строится с использованием 4х компонентов, смысл которых я постарался проиллюстрировать рисунком ниже.
Определившись со значениями всех компонентов составляем матрицу по шаблону:
//Perspective matrix var ar = gl.canvas.width / gl.canvas.height; var angle = Math.PI / 4; //45 deg var far = 100; var near = 1; var pMatrixSimple = new Float32Array( [1 / (ar * tan(angle / 2)), 0, 0, 0, 0,1 / tan(angle / 2),0,0, 0,0, (- near - far) / (near - far), (2 * near * far) / (near - far), 0,0,-1,0]);
Матрица есть, осталось только умножить координаты каждой точки на эту матрицу. Разумеется, для лучшей производительности мы будем делать это в коде шейдеров. Напомню, что теперь можно забыть про четвертую координату вершины, она всегда будет равна единице. Массив вершин теперь выглядит вот так:
var vertices = [ 1.0, 1.0, 0.0,//top left 1.0, -1.0, 0.0,//bottom left -1.0, 1.0, 0.0,//top right -1.0, -1.0, 0.0//bottom right ];
Теперь о матрице перемещения. Мы хотим чтобы наш квадрат крутился в след за курсором мыши и для этого должны менять координаты каждой вершины. Конкретнее, мы должны вращать вершины вокруг оси координат. В википедии довольно понятно разъяснено, как добиться этого при помощи матриц перемещения тут. Посмотрев статью, я выбрал 3 матрицы, которые помогут воплотить задумку в жизнь:
Матрица смещения по x,y,z:
Матрицы поворота вокруг осей x и y:
Матрицы есть, теперь нужно их перемножить, что я и сделал, воспользовавшись статьей тут. Обратите внимание, последовательность записи элементов матрицы в коде отличается от приведенной на картинках.
//Move matrix var x = 0; var y = 0; var z = -7; var aX = a2; var aY = a1; var mvMatrixSimple = new Float32Array( [cos(aY), 0, sin(aY), 0, -sin(aY) * sin(aX), cos(aX), cos(aY) * sin(aX), 0, -sin(aY) * cos(aX), -sin(aX), cos(aY) * cos(aX), 0, x, y, z, 1]);
Передача матриц в код шейдеров
На данном этапе у нас есть обе матрицы и нам остается скормить их шейдеру. Для передачи подобных данных в код шейдеров используются переменные типа uniform. Я решил, что тут уместно будет написать о типах префиксов переменных, используемых в коде шейдеров. Их всего четыре:- uniform - прямая передача параметров из кода скрипта
- attribute - передача в шейдер одного элемента массива, соответствующего номеру обрабатываемой вершины
- varying - интересная штука, использующаяся для передачи данных между шейдером вершин и шейдером фрагментов. Идея в том, что значение будет передано не на прямую а после интерполяции. Ниже я поясню, как это работает на примере цветного квадрата
- без префикса - обычные Сишные локальные и глобальные переменные

Из схемы выше понятно, что переменная uniform доступна из кода любого шейдера, где определена. Принцип её работы предельно интуитивен, положили данные из js - забрали данные в коде шейдера. Напомню, с переменной типа attribute мы уже сталкивались в прошлой части статьи и используем её для передачи координат вершин. Ну и с типом varying мы еще встретимся, когда будем разукрашивать квадрат.
Для того, чтобы начать использовать переменную типа uniform, придется проделать следующее:
1. Определяем uniform переменные в коде шейдера.
2. Получаем ссылку на эту переменную из кода js
3. Заливаем матрицу по выясненному адресу
По порядку:
Объявляем матрицы в коде шейдера вершин:
<script id="shader-fs" type="x-shader/x-fragment">
//....
uniform mat4 uMVMatrix; //move matrix
uniform mat4 uPMatrix; //perspective matrix
Получаем ссылки на эти матрицы и помещаем в список указателей:
function initShaders(){
//....
shaderMemory["uPMatrix"] = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderMemory["uMVMatrix"] = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
Заливаем матрицы по полученным адресам перед отрисовкой:
gl.uniformMatrix4fv(shaderMemory["uPMatrix"], false, pMatrixSimple);
gl.uniformMatrix4fv(shaderMemory["uMVMatrix"], false, mvMatrixSimple);
Теперь матрица на своем месте и видна из кода шейдера, можно смело выполнять матричные операции.
attribute vec3 aVertexPosition; //current vertex position
uniform mat4 uMVMatrix; //move matrix
uniform mat4 uPMatrix; //perspective matrix
void main(void) {
//Calculate vertex position
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
Вращение фигуры
Чтобы убедиться в том что мы сделали настоящее 3дэ, покрутим нашу плоскость, повесим обработчик на событие onmousemove. Красивее всего это можно сделать так:function webGLStart() {
var canvas = document.getElementById("main_canvas");
//...
canvas.onmousemove = function onMouseMove(event) {
a1 = event.clientX / 100;
a2 = event.clientY / 100;
drawScene();
};
};
Красим квадрат
После внесения всех модификаций выше, квадрат из прошлой части туториала должен начать крутиться вокруг осей OX и OY. Но, в начале урока я обещал добавить ему красок. Сделаем же это. В целом, план таков: в коде фрагментного шейдера мы получаем координаты текущего фрагмента, вычисляем расстояние до центра квадрата, и если оно укладывается в заданный диапазон, устанавливаем цвет пикселя, иначе оставляем его черным. Таким образом должна получиться окружность.Проделав все это, мы наконец разберемся с переменными типа varying и осознаем что в коде шейдеров можно написать много всего интересного.
И так, чтобы получить координаты фрагмента, введем переменную типа varying для передачи через него двух координат текущей вершины.
<script id="shader-vs" type="x-shader/x-vertex">
//....
varying vec2 vCoord;
void main(void) {
//Calculate vertex position
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vCoord = vec2((aVertexPosition.x + 1.0) / 2.0, (aVertexPosition.y + 1.0) / 2.0);
}
Для получения координат в коде фрагментного шейдера, переменная vCoord должна быть так же видна в нем:<script id="shader-fs" type="x-shader/x-fragment"> varying vec2 vCoord;А теперь, внимание "интерполяция". Из кода вершинного шейдера мы передали только координаты крайних вершин, но после предварительного расчета плоскостей, фрагментный шейдер получит промежуточные значения vCoord для каждого пикселя. Я проиллюстрировал это рисунком ниже:
Такой трюк позволяет нам многое, например таким образом можно передавать UV координаты, рисовать градиенты и возможно, еще что-то, о чем я пока не знаю.
И так, в коде фрагментного шейдера мы получаем координаты текущей точки, теперь нужно проверить расстояние до нее от центра квадрата.
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float; //medium float precision
varying vec2 vCoord;
void main(void) {
//Calculate fragment color
float x = vCoord.x - 0.5;
float y = vCoord.y - 0.5;
float d = sqrt(x * x + y * y);
if ((d > 0.4) && (d < 0.5))
gl_FragColor = vec4(0.0,vCoord, 1.0);
else
gl_FragColor = vec4(0.0,0.0,0.0, 1.0);
//....
Теперь, наш квадрат раскрашен и мы вплотную подошли к наложению текстур. Полный код примера тут.А вот и живой пример:





Комментарии
Отправить комментарий