Tak jak obiecałem w poprzedniej części, tym razem zajmniemy się ładowanie świata oraz podstawowymi mechanizmami poruszania się. Znowu najłatwiej będzie to wszystko wyjaśnić od końca.
function webGLStart() { var canvas = document.getElementById("webgl_canvas"); initGL(canvas); initShaders(); initTexture(); loadWorld(); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); document.onkeydown = handleKeyDown; document.onkeyup = handleKeyUp; tick(); }
Pojawia się nowa funckja odpowiedzialna za ładowanie świata. Zajrzyjmy do jej wnętrza.
function loadWorld() { var request = new XMLHttpRequest(); request.open("GET", "world.txt"); request.onreadystatechange = function() { if (request.readyState == 4) { handleLoadedWorld(request.responseText); } } request.send(); }
Kod ten może Ci się wydawać znajomy. I rzeczywiście jakby się chwilkę zastanowić to jest bardzo podobny do ładowania tekstury. Przy pomocy HTTP GET ładujemy plik world.txt. Po kompletnym załadowaniu pliku wywołujemy funkcję handleLoadedWorld
. Po czym przy pomocy metody send
wysyłamy zapytanie XMLHttpRequest
aby pobrać zawartość pliku. Przejdźmy zatem do handleLoadedWorld
która jest zdefiniowana zaraz powyżej funkcji przed chwilą omówionej.
var worldVertexPositionBuffer = null; var worldVertexTextureCoordBuffer = null; function handleLoadedWorld(data) {
Zadaniem funkcji jest przeparsować zawartość załadowanego pliku i użycie jej do stworzenia dwóch buforów takiego typu jak z poprzednich lekcjach. Format pliku wejściowego, którego używamy w tym przykładzie jest bardzo prosty. Zawiera listę trójkątów, każdy opisany przez 3 wierchołki, Każdy wierzchołek zdefiniowany jest w jednej linii, która zawiera 5 wartości - współrzędne XYZ oraz koordynaty tekstury S i T.
Bardzo fajny format? Otóż nie, jest straszny. Nie ma możliwości zdefiniowania wektorów, normalnych, czy różnych tekstur dla trójkątów. W rzeczywistości korzysta się z innych formatów (np. JSON). Jednak na potrzeby tego przykładu to wystarczy. Tym bardziej że jest łatwy do parsowania. Oto sposób w jaki to się odbywa.
var lines = data.split("\n"); var vertexCount = 0; var vertexPositions = []; var vertexTextureCoords = []; for (var i in lines) { var vals = lines[i].replace(/^\s+/, "").split(/\s+/); if (vals.length == 5 && vals[0] != "//") { // It is a line describing a vertex; get X, Y and Z first vertexPositions.push(parseFloat(vals[0])); vertexPositions.push(parseFloat(vals[1])); vertexPositions.push(parseFloat(vals[2])); // And then the texture coords vertexTextureCoords.push(parseFloat(vals[3])); vertexTextureCoords.push(parseFloat(vals[4])); vertexCount += 1; } }
Kolejna porcja kodu powinna być znajoma...
worldVertexPositionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexPositionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPositions), gl.STATIC_DRAW); worldVertexPositionBuffer.itemSize = 3; worldVertexPositionBuffer.numItems = vertexCount; worldVertexTextureCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexTextureCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexTextureCoords), gl.STATIC_DRAW); worldVertexTextureCoordBuffer.itemSize = 2; worldVertexTextureCoordBuffer.numItems = vertexCount;
... tworzy ona dwa bufory na podstawie danych które załadowaliśmy.
Przejdźmy do następnej części kodu - drawScene
. Najpierw sprawdzamy czy bufory zostały poprawnie załadowane. Jeśli nie to program się wysypie.
function drawScene() { gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); if (worldVertexTextureCoordBuffer == null || worldVertexPositionBuffer == null) { return; }
Następnie nasze standardowe ustawienia perspektywy:
mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix); mat4.identity(mvMatrix);
Następnym zagadnieniem jest kamera. Naszym zadaniem będzie umożliwienie użytkownikowi poruszania się po scenie. Jak wiele rzeczy w WebGL tak i obsługa kamery nie jest wspierana. Aczkolwiek symulacja tego jest dosyć prosta. Wyobraźmy sobie że mamy kamerę i jesteśmy z nią w pewnym punkcie X,Y,Z. Teraz możemy ruszać kamerą w górę i w dół wokół osi X (pitch) lub rozglądać się o pewien kąt w lewo lub prawo wokół osi Y (yaw). Tak naprawdę to kamera się w ogóle nie rusza. Zawsze jest w punkcie (0,0,0) a my mówimy tylko WebGL aby dostosował scene (eye space) w zależności od ruchu kamery. Zatem jeśli będziemy chcieli pójść do przodu to tak naprawdę nigdzie się nie ruszymy tylko scena się przemieści do tyłu. Istnieją jeszcze inne sposoby pozycjonowania kamery i poznamy je w kolejnych lekcjach. Teraz kod dla naszego sposobu:
mat4.rotate(mvMatrix, degToRad(-pitch), [1, 0, 0]); mat4.rotate(mvMatrix, degToRad(-yaw), [0, 1, 0]); mat4.translate(mvMatrix, [-xPos, -yPos, -zPos]);
Tak jest to jest to! Jedyne co musimy zrobić to wyrysować teraz scenę. Oto kod:
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, mudTexture); gl.uniform1i(shaderProgram.samplerUniform, 0); gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexTextureCoordBuffer); gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, worldVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, worldVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); setMatrixUniforms(); gl.drawArrays(gl.TRIANGLES, 0, worldVertexPositionBuffer.numItems); }
W ten sposób omówiliśmy już większość kodu. Teraz zajmiemy się sterowaniem oraz ruchem, który ma wyglądać tak jakbyśmy biegli po scenie. Przechwytujemy klawisze wciśnięte przez użytkownika i na tej podstawie określamy tempo poruszania się. Wygląda to tak:
var pitch = 0; var pitchRate = 0; var yaw = 0; var yawRate = 0; var xPos = 0; var yPos = 0.4; var zPos = 0; var speed = 0; function handleKeys() { if (currentlyPressedKeys[33]) { // Page Up pitchRate = 0.1; } else if (currentlyPressedKeys[34]) { // Page Down pitchRate = -0.1; } else { pitchRate = 0; } if (currentlyPressedKeys[37] || currentlyPressedKeys[65]) { // Left cursor key or A yawRate = 0.1; } else if (currentlyPressedKeys[39] || currentlyPressedKeys[68]) { // Right cursor key or D yawRate = -0.1; } else { yawRate = 0; } if (currentlyPressedKeys[38] || currentlyPressedKeys[87]) { // Up cursor key or W speed = 0.003; } else if (currentlyPressedKeys[40] || currentlyPressedKeys[83]) { // Down cursor key speed = -0.003; } else { speed = 0; } }
Ustawiane wartości to jednostka/ms. Np dla obrotu jest to 0.1 stopnia na milisekundę, czyli 100 stopni na sekundę lub inaczej 3.6 sekundy na pełen obrót. Skoro wiemy już jak ustawiane są wartości przejdźmy do animacji. Oto kod:
var lastTime = 0; var joggingAngle = 0; function animate() { var timeNow = new Date().getTime(); if (lastTime != 0) { var elapsed = timeNow - lastTime;
Większość z tego kodu jest nam znana. Mierzymy ile milisekund minęło od ostatniego wywołania funkcji. Nowością jest wprowadzenie zmiennej joggingAngle
. Aby uzyskać efekt biegu będziemy się poruszać w górę i w dół po osi Y zgodnie z wartościami sinusa przyjmując, że miejsca zerowe funkci są na wysokości naszej głowy. Nasza nowa zmienna przechowuje aktualną naszą pozycję. Oczywiście wszystko jest liczone tylko gdy się poruszamy.
if (speed != 0) { xPos -= Math.sin(degToRad(yaw)) * speed * elapsed; zPos -= Math.cos(degToRad(yaw)) * speed * elapsed; joggingAngle += elapsed * 0.6; // 0.6 "fiddle factor" -- makes it feel more realistic :-) yPos = Math.sin(degToRad(joggingAngle)) / 20 + 0.4 }
Jak widzimy wszystkie nasze współrzędne są wyliczane na podstawie funkcji trygonometrycznych. Natępnie uaktualniamy parametry ywa oraz pitch:
yaw += yawRate * elapsed; pitch += pitchRate * elapsed;
I na sam koniec zapisujemy aktualny czas:
} lastTime = timeNow; }
I to wszystko. Teraz możesz zacząć się poruszać po naszym nowo stworzonym świecie