WebGL туториал 3 (Текстурированный куб)

В двух предыдущих статьях мы разобрались со многими вещами. И вот на очереди текстуры. Статья должна быть простой, по сравнению с предыдущей. Мы добавим пару строчек в шейдер и одну функцию для загрузки текстуры, так же увеличим количество вершин объекта, что бы получился куб.







Введение

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


Загрузка текстуры

Начнем с загрузки текстуры, без неё дальнейшее повествование не будет иметь смысла. Специально для загрузки текстур напишем простую функцию. Обратите внимание, что загрузка картинки проходит с использованием экземпляра Image. В обработчике события загрузки картинки для экземпляра текстуры устанавливаются все необходимые настройки.

function initTexture(fileName, onReady) {
 var glTexture = gl.createTexture();
 glTexture.image = new Image();
 glTexture.image.onload = function () {
  gl.bindTexture(gl.TEXTURE_2D, glTexture);
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, glTexture.image);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
  gl.bindTexture(gl.TEXTURE_2D, null);
  onReady(glTexture);
 }
 glTexture.image.src = fileName;
}

Из всех настроек, внимание следует обратить на установку следующих параметров:
gl.TEXTURE_WRAP_S и gl.TEXTURE_WRAP_T - режим интерпретации S и T (U и V) координат текстуры.
Режимы:
REPEAT - повторение. Текстура при этом должна быть квадратной, а размеры сторон должны быть степенью двойки (64x64, 128x128, ...)
CLAMP_TO_EDGE - привязка к краям текстуры. Текстура при этом может быть любого размера
MIRRORED_REPEAT - работает так же как и REPEAT, но каждое повторение отражается  

Пока текстуры не загружены, отрисовку сцены мы не начинаем, а потому webGLStart теперь выглядит вот так:
function webGLStart() {
    var canvas = document.getElementById("main_canvas");
    gl = canvas.getContext("webgl");
    initShaders();
    initTexture("abuksigun.png", function (texture) { 
        glTexture = texture;
        drawScene();
        canvas.onmousemove = function onMouseMove(event) {
            a1 = event.clientX / 100;
            a2 = event.clientY / 100;
            drawScene();
        };
    });
}
 

Использование текстуры в коде шейдера

Текстура загружена, теперь нужно передать её в шейдер, делается это аналогично передаче матриц в код шейдера, при помощи переменных типа uniform. Единственное отличие от матрицы - тип данных будет sampler2D, вместо mat4.

В коде шейдера объявляем переменную:
//....
varying vec2 vTextureCoord; //transport variable

uniform sampler2D uTexture; //texture

void main(void) {
 gl_FragColor = vec4(texture2D(uTexture, vTextureCoord).rgb,1);
}

При инициализации шейдерных программ, получаем ссылку на текстуру:
shaderMemory["uTexture"] = gl.getUniformLocation(shaderProgram, "uTexture");

Наконец передаем загруженную текстуру в шейдер по полученному адресу:
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D,glTexture);
gl.uniform1i(shaderMemory["uTexture"],0);

А теперь давайте на секунду представим, что мы раскрашиваем не квадрат, а скажем игральную кость. Игральная кость будет состоять из 6 квадратов-граней, каждый из которых в свою очередь будет состоять из двух треугольников. Хранить текстуры для каждой грани в отдельных файлах и загружать их отдельно не рационально. Потому люди придумали размещать изображения для каждой плоскости (треугольника) на одной текстуре и хранить в одном файле. При таком раскладе придется указывать, какую часть картинки, на какой грани нужно рисовать. Для задания точек, описывающих области, соответствующие треугольникам, используются две координаты,  названные U и V, отсюда и название UV-координаты.
Хорошо. Есть текстура, есть шейдер, который задает цвет пикселей. Все что нужно, это задать данному пикселю соответствующий цвет. Таким образом, при вызове фрагментного шейдера нам нужно знать, из какой части изображения-текстуры, взять цвет. Для того, что бы вытащить из текстуры цвет одного пикселя, используется встроенная функция шейдера texture2D. Формат вызова такой:

vec4 texture2D(sampler2D s, vec2 coord [, float bias])
s - текстура
coord - UV координаты
bias - смещение
Текстура s уже есть в наличие, нужны только uv-координаты пикселя текстуры соответствующе текущей точки. Получить их можно интерполяцией крайних точек области Картинка ниже должна пояснить ситуацию. Так как изображение занимает всю области текстуры, uv координатами картинки для одной половины квадрата будут:  [1.0, 0.0], [0.0, 1.0],  [1.0, 1.0].

Хватит лирики, переходим к коду. Записываем UV координаты углов текстуры в массив и отправляем в шейдер. Что бы задать соответствие вершин плоскости и uv координат, мы просто кладем uv-координаты в массив, в том же порядке. Ну вы поняли, индекс uv-координаы в массиве, должен соответствовать индексу вершины в массиве вершин.

//Bind the buffer to work with it
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
var coords = new Float32Array([
     0.0, 0.0,//top left
     0.0, 1.0, //bottom left
     1.0, 0.0,//top right
     1.0, 1.0//bottom right
]);
//Move js array to the binded buffer
gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STATIC_DRAW);
var itemSize = 2; //three component
var numItems = coords.length / itemSize; //four points
//Say to GL about our vertex list
gl.vertexAttribPointer(shaderMemory["aTextureCoord"], itemSze, gl.FLOAT, false, 0, 0); 
 
Обратите внимание, UV-координаты  передаются в виде атрибутов, так же как и координаты вершин. Потому, нужно не забыть получить указатель на атрибут aTextureCoord при инициализации шейдеров.

shaderMemory["aTextureCoord"] = gl.getAttribLocation(shaderProgram, "aTextureCoord");
gl.enableVertexAttribArray(shaderMemory["aTextureCoord"]);

Куб, а не квадрат

Да-да, я обещал куб. Кубы модней квадратов, наверное потому что из них можно сделать клон Minecraft. К счастью, чтобы превратить квадрат в куб, достаточно увеличить кол-во вершин. Правда теперь мы будем записывать в массив координаты плоскостей целиком (по три вершины на треугольник). В общем, что бы получить куб, сначала заменим массив вершин:
var vertices = [
 1.000000, 1.000000, -1.000000,
 1.000000, -1.000000, -1.000000,
 -1.000000, 1.000000, -1.000000,
 1.000000, 0.999999, 1.000000,
 -1.000000, 1.000000, 1.000000,
 0.999999, -1.000001, 1.000000,
 1.000000, 1.000000, -1.000000,
 1.000000, 0.999999, 1.000000,
 1.000000, -1.000000, -1.000000,
 1.000000, -1.000000, -1.000000,
 0.999999, -1.000001, 1.000000,
 -1.000000, -1.000000, -1.000000,
 -1.000000, -1.000000, -1.000000,
 -1.000000, -1.000000, 1.000000,
 -1.000000, 1.000000, -1.000000,
 1.000000, 0.999999, 1.000000,
 1.000000, 1.000000, -1.000000,
 -1.000000, 1.000000, 1.000000,
 1.000000, -1.000000, -1.000000,
 -1.000000, -1.000000, -1.000000,
 -1.000000, 1.000000, -1.000000,
 -1.000000, 1.000000, 1.000000,
 -1.000000, -1.000000, 1.000000,
 0.999999, -1.000001, 1.000000,
 1.000000, 0.999999, 1.000000,
 0.999999, -1.000001, 1.000000,
 1.000000, -1.000000, -1.000000,
 0.999999, -1.000001, 1.000000,
 -1.000000, -1.000000, 1.000000,
 -1.000000, -1.000000, -1.000000,
 -1.000000, -1.000000, 1.000000,
 -1.000000, 1.000000, 1.000000,
 -1.000000, 1.000000, -1.000000,
 1.000000, 1.000000, -1.000000,
 -1.000000, 1.000000, -1.000000,
 -1.000000, 1.000000, 1.000000
];
А затем и массив UV-координат:
var coords = new Float32Array([
 0.662724, 0.037667,
 0.951821, 0.037667,
 0.662724, 0.326763,
 0.029829, 0.964545,
 0.029829, 0.675448,
 0.318926, 0.964545,
 0.649965, 0.675448,
 0.360869, 0.675448,
 0.649965, 0.386352,
 0.060939, 0.337249,
 0.060939, 0.048152,
 0.350035, 0.337248,
 0.649965, 0.675448,
 0.939061, 0.675448,
 0.649965, 0.964545,
 0.360869, 0.675448,
 0.649965, 0.675448,
 0.360869, 0.964545,
 0.951821, 0.037667,
 0.951821, 0.326763,
 0.662724, 0.326763,
 0.029829, 0.675448,
 0.318926, 0.675448,
 0.318926, 0.964545,
 0.360869, 0.675448,
 0.360869, 0.386352,
 0.649965, 0.386352,
 0.060939, 0.048152,
 0.350035, 0.048152,
 0.350035, 0.337248,
 0.939061, 0.675448,
 0.939061, 0.964544,
 0.649965, 0.964545,
 0.649965, 0.675448,
 0.649965, 0.964544,
 0.360869, 0.964545  
]);
Откуда я взял все эти цифры? Это долгая история, наполненная страданиями. Сначала я хотел просто разукрасить куб логотипом, но решил что это не даст читателю понять смысла UV развертки. Тогда, я решил сделать кусок земли из майнкрафта. Но, то что у меня получилось, мне не понравилось. Кроме того, пример получился не слишком наглядным. И тогда я решил закрасить все былым и сделать что-то вроде игральной кости. Так и получилось то, что получилось. Затем мне нужно было экспортировать то, что у меня получилось и записать в массивы, приведенные выше. Так как я не очень разбираюсь в форматах 3d графики, я выбрал единственный известный мне текстовый формат obj. После чтения статьи на википедии, я разобрался что к чему и вручную(!) скопировал вершины и UV координаты в js скрипт. Мне всегда тяжело давалась монотонная работа, но мне показалось это лучшим выбором. Я не хотел утяжелять статью разбором obj файла, кроме того - этот файл нужно загружать, а для этого нужен локальный сервер. Я уверен, не каждый читатель захочет заморачиваться с такими вещами.



Остался последний штрих, изменим режим рендеринга с gl.TRIANGLE_STRIP на gl.TRIANGLES.
gl.drawArrays(gl.TRIANGLES, 0, numItems);

Комментарии

Популярные сообщения из этого блога

Siege Up! Editor (beta)

STM32F4 и программный выход в DFU

Git и Yandex.Disk