WebGL туториал 2 (Цветная плоскость в 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); //....Теперь, наш квадрат раскрашен и мы вплотную подошли к наложению текстур. Полный код примера тут.
А вот и живой пример:
Комментарии
Отправить комментарий