W tej części tutoriala zajmiemy się światłem na teksturowanych obiektach. Posłużymy się przykładem sfery z nałożoną teksturą powierzchni księżyca dodając do niej odpowiednie oświetlenie kierunkowe oraz możliwość obracania za pomocą myszy.
Na wstępie musimy zmodyfikować kod funkcji webGLStart()
, tak aby możliwa była obsługa event'ów myszy:
function webGLStart() { canvas = document.getElementById("webgl_canvas"); initGL(canvas); initShaders(); initBuffers(); initTexture(); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); /*Fragment odpowiedzialny za obsługę myszy*/ canvas.onmousedown = handleMouseDown; document.onmouseup = handleMouseUp; document.onmousemove = handleMouseMove; /**/ tick(); }
Przejdźmy teraz do funkcji tick()
, która dla naszego przykładu ustawia kolejną klatkę animacji i wywołuje rysowanie sceny funkjąc drawScene()
:
function tick() { requestAnimFrame(tick); drawScene(); }
Kolejne zmiany zostały wprowadzone w kodzie funkcji drawScene()
. Zaczynamy od wyczyszczenia elementu canvas
i kodu odpowiedzialnego za perspektywę, aby dalej zrobić dokładnie to samo co w tutorialu 7 z ustawieniami światła:
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); var lighting = document.getElementById("lighting").checked; gl.uniform1i(shaderProgram.useLightingUniform, lighting); if (lighting) { gl.uniform3f( shaderProgram.ambientColorUniform, parseFloat(document.getElementById("ambientR").value), parseFloat(document.getElementById("ambientG").value), parseFloat(document.getElementById("ambientB").value) ); var lightingDirection = [ parseFloat(document.getElementById("lightDirectionX").value), parseFloat(document.getElementById("lightDirectionY").value), parseFloat(document.getElementById("lightDirectionZ").value) ]; var adjustedLD = vec3.create(); vec3.normalize(lightingDirection, adjustedLD); vec3.scale(adjustedLD, -1); gl.uniform3fv(shaderProgram.lightingDirectionUniform, adjustedLD); gl.uniform3f( shaderProgram.directionalColorUniform, parseFloat(document.getElementById("directionalR").value), parseFloat(document.getElementById("directionalG").value), parseFloat(document.getElementById("directionalB").value) ); }
W dalszej kolejności należy przejść na pozycję odpowiednią dla wyrysowania naszego księżyca:
mat4.identity(mvMatrix); mat4.translate(mvMatrix, [0, 0, -6]);
Aktualny stan księżyca (jego rotację) będziemy przechowywać w macierzy, która na starcie ma być macierzą jednostkową (stan bez rotacji). Natomiast w trakcie manipulowania obiektem przez użytkownika ma się zmieniać tak, aby odzwierciedlać żądane przekształcenia.
Dlatego też przed wyrysowaniem obiektu konieczne jest dostosowanie macieży obrotu (moonRotationMatrix
) do naszej macierzy widoku modelu (mvMatrix
). Jest to możliwe dzięki użyciu funkcji mat4.multiply()
:
mat4.multiply(mvMatrix, moonRotationMatrix);
Po wykonaniu tych operacji nie pozostaje nic innego jak wyrysowanie samego obiektu księżyca, które przebiega dość standardowo. W pierwszej kolejności ustawiamy teksturę i używamy kodu, który pojawił się już wielokrotnie w poprzednich częściach tutoriala:
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, moonTexture); gl.uniform1i(shaderProgram.samplerUniform, 0); gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, moonVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexTextureCoordBuffer); gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, moonVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexNormalBuffer); gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, moonVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, moonVertexIndexBuffer); setMatrixUniforms(); gl.drawElements(gl.TRIANGLES, moonVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); }
Chyba wszystko jasne, tylko jakie wartości mają przyjmować zmienne odpowiedzialne za Vertex Buffers, aby poprawnie wyrysować sferę? Typowo, mamy zdefiniowaną funkcję initBuffers()
.
Przed jej definicją znajduje się kilka globalnych zmiennych, po jednej dla każdego bufora. W samym ciele funkcji musimy na wstępie zdecydować jakie wartości długości i szerokości geograficznej chcemy użyć, a także wybrać promień naszej sfery:
var moonVertexPositionBuffer; var moonVertexNormalBuffer; var moonVertexTextureCoordBuffer; var moonVertexIndexBuffer; function initBuffers() { var latitudeBands = 30; var longitudeBands = 30; var radius = 2;
Dlaczego posługujemy się tutaj szerokością i długością geograficzną? Otóż, służą one do wyrysowania zestawu trójkątów, które są przybliżeniem sfery. Istnieje bardzo wiele sposobów wykonania tej operacji. My w tym tutorialu posłużymy się sposobem zaprezentowanym [ tutaj ].
Każdy wie (lub chociaż powinien wiedzieć) co oznacza szerokość i długość geograficzna i w jaki sposób wyznaczane przez nie linie dzielą sferę. Punkty w których się one przecinają posłużą nam jako pozycje wierzchołków naszych trójkątów, gdyż każdy kwadrat wyznaczony przez te linie możemy podzielić na dwa trójkąty.
Iterując po wszystkich segmentach powstałych z 'posiekania' sfery poprzez linie szerokości i długości geograficznej, wyliczane są wierzchołki, które następnie ładowane są do listy vertexPositionData
:
var vertexPositionData = []; var normalData = []; var textureCoordData = []; for (var latNumber = 0; latNumber <= latitudeBands; latNumber++) { var theta = latNumber * Math.PI / latitudeBands; var sinTheta = Math.sin(theta); var cosTheta = Math.cos(theta); for (var longNumber = 0; longNumber <= longitudeBands; longNumber++) { var phi = longNumber * 2 * Math.PI / longitudeBands; var sinPhi = Math.sin(phi); var cosPhi = Math.cos(phi); var x = cosPhi * sinTheta; var y = cosTheta; var z = sinPhi * sinTheta; var u = 1 - (longNumber / longitudeBands); var v = 1 - (latNumber / latitudeBands); normalData.push(x); normalData.push(y); normalData.push(z); textureCoordData.push(u); textureCoordData.push(v); vertexPositionData.push(radius * x); vertexPositionData.push(radius * y); vertexPositionData.push(radius * z); } }
Mając już wygenerowaną listę wierzchołków, musimy je jeszcze połączyć z listą indeksów. Każdy indeks jest reprezentowany jako sekwencja 6 wartości, każda określająca kwadrat wyrażony jako para trójkątów.
var indexData = []; for (var latNumber = 0; latNumber < latitudeBands; latNumber++) { for (var longNumber = 0; longNumber < longitudeBands; longNumber++) { var first = (latNumber * (longitudeBands + 1)) + longNumber; var second = first + longitudeBands + 1; indexData.push(first); indexData.push(second); indexData.push(first + 1); indexData.push(second); indexData.push(second + 1); indexData.push(first + 1); } }
Tym samym mamy przygotowaną funkcję initBuffers()
. Ostatniem elementem, który musimy wykonać to funkcje obsługujące eventy muszy i ich powiązanie z macierzą obrotu księżyca moonRotationMatrix
:
var mouseDown = false; var lastMouseX = null; var lastMouseY = null; var moonRotationMatrix = mat4.create(); mat4.identity(moonRotationMatrix); function handleMouseDown(event) { mouseDown = true; lastMouseX = event.clientX; lastMouseY = event.clientY; } function handleMouseUp(event) { mouseDown = false; } function handleMouseMove(event) { if (!mouseDown) { return; } var newX = event.clientX; var newY = event.clientY; var deltaX = newX - lastMouseX; var newRotationMatrix = mat4.create(); mat4.identity(newRotationMatrix); mat4.rotate(newRotationMatrix, degToRad(deltaX / 10), [0, 1, 0]); var deltaY = newY - lastMouseY; mat4.rotate(newRotationMatrix, degToRad(deltaY / 10), [1, 0, 0]); mat4.multiply(newRotationMatrix, moonRotationMatrix, moonRotationMatrix); lastMouseX = newX lastMouseY = newY; }
Kierunek: | X: | Y: | Z: |
Kolor: | R: | G: | B: |
Kolor: | R: | G: | B: |