Wreszcie przechodzimy do czynów. W tym tutorialu narysujemy trójkąt oraz kwadrat. Mimo, że nie jest to może spektakularne samo w sobie, to jest dobrą podstawą do tego aby zrozumieć WebGl’a. Zrozumienie tego ułatwi Ci dalszą pracę.
Oto efekt jaki będziemy chcieli osiągnąć:
Jeśli chodzi o html'a to całe ciało może wyglądać tak. Jeden przycisk, który wywoła funkcję JavaScript oraz jeden obiekt canvas ,do którego ładowany będzie wynik działania skryptu. Cała resztę wykonuje kod napisany w JS.
<body> <button class="runButton" onclick="webGLStart();">Uruchom przykład</button> <canvas id="webgl_canvas" style="border: none;" width="500" height="500"></canvas> </body>
Przyjrzyjmy się teraz funkcji webGLStart()
, która jest wywoływana po wciśnięciu przysicku. Jej ciało wygląda następująco:
function webGLStart() { var canvas = document.getElementById("webgl_canvas"); initGL(canvas); initShaders(); initBuffers(); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); drawScene(); }
Jak widzimy w jej ciele odbywa się inicjalizacja WebGL. W pierwszej kolejności pobieramy obiekt canvas, do którego będziemy ładować wyniki naszej pracy i przekazujemy jako parametr do funkcji initGL. Ponadto inicjowane są cienie oraz bufor. Bufor to takie miejsce, w którym przechowywane będą szczegóły na temat naszego trójkąta oraz kwadratu.
Następnie odbywa się wstępna konfiguracja. Przy pomocy clearColor
ustawiamy tło na kolor czarny, a następnie włączamy tryb DEPTH_TESTING. Dzięki temu obiekty rysowane za innymi powinny zostać przykryte przez te, które są przed nimi. Obie funkcje są wywoływane na obiekcie gl. Jak on jest inicjalizowny zobaczymy później.
Ostatecznie wywołujemy funkcję drawScene, która przy pomocy bufora rysuje nasze figury.
Przejdźmy teraz do analizy funkcji initBuffers
.
var triangleVertexPositionBuffer; var squareVertexPositionBuffer; 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; 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; }
Przed deklaracją funkcji definiujemy dwie zmienne globalne, które będzą przechowywały bufory triangleVertexPositionBuffer
oraz squareVertexPositionBuffer
.
Następnie w lini nr. 5 tworzymy bufor dla trójkąta. Będzie on przechowywał informację o jego wierzchołkach. Zapisuje on sobie te informacje w pamięci na karcie graficznej i w odpowiednim momencie jak będziemy chcieli narysować obiekt to te dane zostaną odczytane.
Kolejna linia mówi WebGL, aby wszystkie poniższe czynności, które odbywają się na buforach zastosować do tego, który wyszczególniliśmy.
Teraz możemy określić pozycje wierzchołków w przestrzeni. W tym celu definiujemy jak w JavaScript listę. Każdy wiersz określa współrzędne X,Y,Z każdego wierzchołka. Zauważ, że są to punkty trójkąta równoramiennego o środku mieszczącym się w 0,0,0.
Zapis gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
tworzy nowy obiekt Float32Array
bazując na wcześniej stworzonej liście oraz mówi WebGL aby użył go do wypłnienia aktualnego buforu. Więcej na temat Float32Array
będzie w następnym rozdziale.
Ostatno rzeczą jaką robimy na buforze jest ustawienie dwóch właściwości. Nie są one wbudowane w WebGL'a, ale będą później przydatne. Przy ich pomocy mówimy, że ten 9-elementowy bufor reprezentuje trzy osobne elementy o rozmiarze równym trzy.
Analogicznie wykonujemy wzsystkie kroki w przypadku kwadratu.
Skoro już umieściliśmy informacje na karcie graficznej, sprawdźmy teraz jak je wykorzystać aby coś narysować. Odpowiedzialna jest za to funkcja drawScene
function drawScene() { gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix); mat4.identity(mvMatrix); mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]); gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); setMatrixUniforms(); gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems); mat4.translate(mvMatrix, [3.0, 0.0, 0.0]); gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); setMatrixUniforms(); gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems); }
Zaczynamy od ustalenia rozmiaru okna w którym wyświetlana będzie nasza praca oraz wyczyszczenia go.
Domyślnie WebGL obiekty oddalone rysuje takiej samej wielkości jak obiekty które są blisko. Dlatego konieczne jest ustawienie perspektywy co odbywa się w lini 5. Pierwszy parametr to kąt, drugi do stosunek szerokości do wysokości, trzeci oraz czwarty określają zasięg widzenia. Tajemnicza zmienna pMatrix
zostanie objaśniona później.
Linia 7 przypisuje ustawienia do zmiennej mvMatrix
.
W WebGL'u jeśli coś rysujesz to mówisz: "Narysuj coś w aktualnej pozycji z aktualną rotacją". Jeśli chcesz narysować wiele obiektów to wtedy na przykład chcesz powiedzieć coś w stylu: "Przesuń się o 20 jednostek obróc o 40 stopni i rysuj, następnie przejdź kolejne jednostek do przodu i znowu rysuj". Aktualna pozycja oraz rotacja przetrzymywane są w macierzy. Bez zbędnego zagłębiania się w działanie, aby przesunąć o 1.5 jednostki w lewo i 7 jednostek w stronę kamery należy wywołac funkcję z linii dziewiątej.
No i wreszcie zaczynamy coś rysować. Tak jak wcześniej używaliśmy funkcji gl.bindBuffer
aby wybrać bufor tak robimy to i teraz. Następnie mówimy, że wartości z bufora powinny zostać użyte do rysowania. Widzimy, że wcześniej użyta zmienna itemSize się przydała. Informuje WebGL'a, że każdy element w buforze jest 3 cyfrowy. No i na reszcie funkcja rysująca która mówi: "Weź to co Ci przekazałem wcześniej w buforze jako trójkąty zaczynając od zerowego elementu 4 razy.
Kwadrat rysujemy w analogiczny sposób. Pamiętać należy jednak aby dokonać przesunięcia. W przeciwnym wypadku figury będą nachodzić na siebie
Trochę już mamy za sobą. W ramach odpoczynku możesz skopiować cały kod i poeksperymentować trochę.
W tym momencie już możesz przejść do kolejnego rozdziału (Kolorowania figur), gdyż posiadasz już wystarczającą wiedzę. Jednak możesz też kontynuować czytanie i dowiedzieć się jak działają funkcje pomocnicze używane w kodzie. Nie jest to niezbędne, ale zrozumienie ich nie jest trudne, a na pewno dzięki temu będziesz pisał lepszy kod.
Dalej ze mną? Super. Zacznijmy od pierwszej pomocniczej funkcji initGL
:
var gl; function initGL(canvas) { try { gl = canvas.getContext("experimental-webgl"); gl.viewportWidth = canvas.width; gl.viewportHeight = canvas.height; } catch (e) { } if (!gl) { alert("Could not initialise WebGL, sorry :-("); } }
Jak widać nie wiele się dzieje. Tworzony jest obiekt gl który później jest używany w naszej aplikacji.
Następnie inicjalizowane są shadery. Po krótce są one odpowiedzialne za nadawanie kolorów, cieni i innych efektów scenie.
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.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); }
Jak widać przy pomocy funkcji getShader
pobiera fragment-shader oraz vertex-shader i przypisuje je do programu. Program jest kodem odpalonym na karcie graficznej. Następnie dołączamy shadery do programu.
Przejdźmy teraz do omówienia funkcji getShader:
function getShader(gl, id) { var shaderScript = document.getElementById(id); if (!shaderScript) { return null; } var str = ""; var k = shaderScript.firstChild; while (k) { if (k.nodeType == 3) str += k.textContent; k = k.nextSibling; } var shader; if (shaderScript.type == "x-shader/x-fragment") { shader = gl.createShader(gl.FRAGMENT_SHADER); } else if (shaderScript.type == "x-shader/x-vertex") { shader = gl.createShader(gl.VERTEX_SHADER); } else { return null; } gl.shaderSource(shader, str); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert(gl.getShaderInfoLog(shader)); return null; } return shader; }
Funkcja może wygląda strasznie, ale wcale taka nie jest. Szukamy elementu w dokumencie o zadanym id, ładujemy jego zawartość i tworzymy w zależności od tej zawartości fragment lub vertex shader (o tym więcej będzie później). I na koniec kompilujemy to do formy zrozumiałej dla WebGL'a.
Teraz przekonamy się jak wygląda kod który ładujemy:
<script id="shader-fs" type="x-shader/x-fragment"> precision mediump float; 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; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; void main(void) { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); } </script>
Pierwsze na co należy zwrócić uwagę, to fakt, że powyższy kod nie jest pisany w JavaScript tylko w specjalnym języku shadera GLSC. Shader-fs nie robi nic ciekawego. Jedynie określa dokładność liczb zmienno przecinkowych.
Ciekawsze rzeczy się dzieją w drugim shaderze. Vertex shader może zrobić wszystko z wierzchołkami. Definiuje on dwie zmienne uMVMatrix, uPMatrix
, które będą dostępne nawet z zewnątrz shadera (uniform variables). Następnie jest on wywoływany dla każdego wierzchołka. Bierze jego pozycję i mnoży przez macierz Model-View oraz macierz projekcji.
Uff. Tak to już koniec. Jak na pierwszy raz to trochę dużo ale mam nadzieję że dzięki temu wszystko dobrze zrozumiałeś i masz solidne podstawy na starcie:)