Serwer WWW z elementami grafiki 3D – praktyczne wykorzystanie pakietów Node.js oraz Three.js w systemach wbudowanych (część 2)
Node.js – rozbudowa interfejsu o proste elementy grafiki 3D (Three.js)
Na obecnym etapie realizacji projektu przygotowane zostały już wszystkie elementy składowe. W poprzedniej części artykułu omówiono back-end w postaci serwera WWW ze skryptu main.js, którego zadaniem był odczyt danych z procesu potomnego gyro-i2c oraz przesłanie danych za pomocą socket.io do warstwy front-end – interfejsu graficznego w postaci strony index.html. W ostatnim podrozdziale artykułu rozbudujemy interfejs graficzny aplikacji o prostą grafikę 3D w postaci sześciennej kostki, odwzorowującej ruch podłączonego modułu żyroskopu.
Do realizacji operacji graficznych wykorzystamy bibliotekę Three.js, która to natomiast korzysta z API WebGL – oficjalnego rozszerzenia możliwości języka JavaScript o interfejs grafiki 3D. Bezpośrednie wykorzystanie interfejsu WebGL jest dość uciążliwe, choćby ze względu na dużą liczbę operacji niskiego poziomu, jakie spoczywają na programiście – definicja wierzchołków, buforów, macierzy transformacji, operacje związane z wyświetlaniem sceny, obsługa shaderów, oświetlenia, modeli, kamer i wiele innych. W bibliotece Three.js scena budowana jest z obiektów (sama scena jest również obiektem w którym umieszczamy inne obiekty). Do podstawowych obiektów możemy zaliczyć: figury geometryczne (biblioteka posiada zdefiniowane kilka gotowych do użycia obiektów takich jak sfera czy sześcian), materiały przypisywane do figur geometrycznym (określające m.in. ich kolor i fizykę odbijania światła), źródła światła oraz obserwatora sceny (czyli „kamerę”, która obserwuje scenę w określonym położeniu).
Rozbudowę aplikacji rozpoczynamy od pobrania kodu biblioteki Three.js (plik three.min.js) do katalogu w którym umieszczono skrypt main.js oraz stronę index.html (pełny kod źródłowy skryptu main.js oraz strony index.html został przedstawiony na listingu 6 i listingu 9 w pierwszej części artykułu):
wget http://threejs.org/build/three.min.js
Edycję pliku index.html rozpoczynamy od zdefiniowania w sekcji head „płótna” canvas (o wymiarach 500x500px oraz identyfikatorze mycanvas), w którym będzie renderowana docelowa animacja:
<canvas id="mycanvas" width="500" height="500"></canvas>
Następnie w sekcji head dołączamy bibliotekę Three.js:
<script src='three.min.js'></script>
W dalszej części skryptu definiujemy zmienne w których będziemy przechowywać wyniki pomiarów w osi x, y, z oraz informacje o tworzonej scenie i dołączonych do niej obiektach:
var camera, scene, renderer; var geometry, material, mesh; var x, y, z;
Następnie implementujemy funkcję init(), której zadaniem jest zbudowanie sceny z określonych obiektów – listing 5.
function init() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera (70, 500/500, 0.01, 10); camera.position.z = 0.5; geometry = new THREE.BoxGeometry (0.2, 0.2, 0.2); material = new THREE.MeshNormalMaterial(); mesh = new THREE.Mesh (geometry, material); scene.add (mesh); renderer = new THREE.WebGLRenderer ({ canvas: mycanvas}); renderer.setSize (500, 500); document.body.appendChild (renderer.domElement);
Listing 5. Budowanie sceny z wykorzystaniem biblioteki Three.js
W pierwszej linii kodu funkcji init() tworzymy scenę, do której będziemy dołączali kolejno definiowane obiekty (kamerę, figurę geometryczną oraz materiał dla tej figury):
scene = new THREE.Scene();
W następnym kroku tworzymy obiekt kamery określając kąt jej widzenia (70 stopni), proporcje kadru, zakresy widzenia: bliski i daleki, a także jej umiejscowienie:
camera = new THREE.PerspectiveCamera (70, 500/500, 0.01, 10); camera.position.z = 0.5;
Korzystając ze zdefiniowanych w bibliotece Three.js kształtów, tworzymy obiekt reprezentujący sześcian (BoxGeometry):
geometry = new THREE.BoxGeometry (0.2, 0.2, 0.2);
oraz obiekt stanowiący „materiał” z jakiego wykony jest nasz sześcian (decyduje on m.in. o kolorze obiektu i sposobie rozpraszania światła) – wykorzystamy tutaj predefiniowany materiał MeshNormalMaterial:
material = new THREE.MeshNormalMaterial();
Z połączenia figury z materiałem możemy utworzyć obiekt klasy Mesh, który dodajemy do tworzonej sceny:
mesh = new THREE.Mesh (geometry, material); scene.add (mesh);
W ostatnich liniach funkcji init(), określamy rozmiar i identyfikator powierzchni (mycanvas) na której będzie renderowana animacja:
renderer = new THREE.WebGLRenderer ({ canvas: mycanvas}); renderer.setSize (500, 500); document.body.appendChild (renderer.domElement);
Na listingu 6 przedstawiono kod funkcji animate(), której zadaniem jest wykonanie obrotu obiektu, zgodnie z kątem obrotu zapisanym w zmiennych x, y, z:
function animate() { requestAnimationFrame (animate); mesh.rotation.x = THREE.Math.degToRad(x); mesh.rotation.y = THREE.Math.degToRad(y); mesh.rotation.z = THREE.Math.degToRad(z); renderer.render (scene, camera);
Listing 6. Kod funkcji wykonującej obrót obiektu
Pełna zawartość pliku index.html została przedstawiona na listingu 7.
<!DOCTYPE html> <html> <head> <canvas id="mycanvas" width="500" height="500"></canvas> <style> table, th, td { border: 1px solid black; } th, td { border: 1px solid black; padding: 15px; } </style> <script src='/socket.io/socket.io.js'></script> <script src='three.min.js'></script> <script> var camera, scene, renderer; var geometry, material, mesh; var x, y, z; function init() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera (70, 500/500, 0.01, 10); camera.position.z = 0.5; geometry = new THREE.BoxGeometry (0.2, 0.2, 0.2); material = new THREE.MeshNormalMaterial(); mesh = new THREE.Mesh (geometry, material); scene.add (mesh); renderer = new THREE.WebGLRenderer ({ canvas: mycanvas}); renderer.setSize (500, 500); document.body.appendChild (renderer.domElement); } function animate() { requestAnimationFrame (animate); mesh.rotation.x = THREE.Math.degToRad(x); mesh.rotation.y = THREE.Math.degToRad(y); mesh.rotation.z = THREE.Math.degToRad(z); renderer.render (scene, camera); } init(); animate(); var socket = io(); socket.on ('xyz', function (data) { var arr = data.message.split(" "); x = arr[0]; y = arr[1]; z = arr[2]; document.getElementById("x_val").innerHTML = x; document.getElementById("y_val").innerHTML = y; document.getElementById("z_val").innerHTML = z; }); </script> </head> <body> <h1>Gyroscope I2C</h1> <table> <tr> <th>X [deg]</th> <td><p id="x_val">---</p></td> </tr> <tr> <th>Y [deg]</th> <td><p id="y_val">---</p></td> </tr> <tr> <th>Z [deg]</th> <td><p id="z_val">---</p></td> </tr> </table> </body> </html>
Listing 7. Pełny kod źródłowy strony index.html po dodaniu elementów grafiki 3D
Niewielkiej modyfikacji wymaga również sam kod serwera main.js. Dotychczas serwer na żądnie klienta udostępniał wyłącznie plik index.html. W aktualnie formie, przy ładowaniu strony głównej, klient zażąda również pliku three.min.js – serwer powinien to żądanie obsłużyć i dostarczyć klientowi wymaganą bibliotekę – listing 8.
var url = require('url'); var server = http.createServer (function handler (request, response) { var pathname = url.parse(request.url).pathname; console.log("Request for " + pathname + " received."); response.writeHead (200, {'Content-Type': 'text/html'}); if(pathname == "/") { var index = fs.readFileSync (__dirname + '/index.html'); response.write (index); } else if (pathname == "/three.min.js") { var script = fs.readFileSync (__dirname + '/three.min.js'); response.write (script); } response.end(); }); }
Po skopiowaniu do katalogu /tmp skompilowanej wersji kodu gyro-i2c z listingu 4, oraz ponownym skryptu main.js:
nodejs main.js
uruchomiony na komputerze jednopłytkowym serwer WWW prezentuje dane pomiarowe w postaci animowanej kostki 3D, jak przedstawiono to na Rysunku 4.
Rys. 4. Prezentacja danych odczytanych z żyroskopu w postaci animacji 3D
Łukasz Skalski