LinkedIn YouTube Facebook
Szukaj

Wstecz
SoM / SBC

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).

Ponieważ kod odpowiedzialny za animację 3D jest wykonywany przez przeglądarkę po stronie komputera użytkownika, należy upewnić się, że wybrana przez nas przeglądarka internetowa wspiera API WebGL v1.

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

contact@lukasz-skalski.com

Łukasz Skalski - absolwent Politechniki Gdańskiej, miłośnik FLOSS, autor książki "Linux. Podstawy i aplikacje dla systemów embedded" oraz szeregu artykułów dotyczących programowania systemów wbudowanych. Zawodowo związany z firmą Samsung. Wszystkie wolne chwile poświęca projektowaniu i programowaniu urządzeń wyposażonych w mikroprocesory 8-/16- i 32-bitowe. Szczególnym zainteresowaniem obejmuje tematykę systemu Linux w aplikacjach na urządzenia embedded.