---
title: "generic: ręcznie napisany silnik 3D w JavaScripcie, przeniesiony na ESP8266"
url: https://michalasobczak.pl/pl/posts/generic-3d-engine-in-javascript-ported-to-esp8266.html
author: "Michał Sobczak"
description: "Renderer 3D napisany od zera w 2013 roku na zwykłym canvasie HTML, z teksturowaniem, światłem, occlusion cullingiem i wykrywaniem kolizji, a jego matematyka projekcji przeniesiona później do Lua na mikrokontrolerze ESP8266. Wierzchołki na sekundę: tablet z Intel Atom kontra układ 80/160 MHz."
published: 2026-10-25T10:00:00.000Z
updated: 2026-10-25T10:00:00.000Z
language: pl
categories: ["Hobby"]
tags: ["3D", "Programming", "Hardware", "Smart Home & IoT"]
license: CC BY-NC-ND 4.0 (https://creativecommons.org/licenses/by-nc-nd/4.0/)
---
# generic: ręcznie napisany silnik 3D w JavaScripcie, przeniesiony na ESP8266

> **Zastrzeżenie:** opisane tu projekty są efektem mojej własnej, samodzielnej pracy, wykonanej bez użycia AI. Sam artykuł powstał z pomocą AI.

Większość moich eksperymentów graficznych zaczyna się od tego samego kawałka kodu. [generic](https://github.com/michalasobczak/retro-lab/tree/master/js3d-engine) (dziś katalog `js3d-engine` w moim repozytorium [retro-lab](https://github.com/michalasobczak/retro-lab)) to mały silnik 3D, który napisałem na początku 2013 roku dla przeglądarek na Androidzie i Windows Phone, bez WebGL i bez żadnej biblioteki 3D. Wszystko, od macierzy obrotu po mapowanie tekstur, to czysty JavaScript rysujący na canvasie 2D.

W 2018 roku uporządkowałem go, opublikowałem i użyłem jako implementacji referencyjnej dla portów na [Commodore 64](https://michalasobczak.pl/posts/commodore-64-restoration-and-3d-in-basic-c-and-assembly.html), [sektor rozruchowy x86](https://michalasobczak.pl/posts/x86-bootloader-2d-and-3d-graphics-in-nasm.html) oraz mikrokontroler [ESP8266](https://github.com/michalasobczak/retro-lab/tree/master/esp8266).

![Mapa z kafelków wyrenderowana przez silnik 3D generic w JavaScripcie](https://michalasobczak.pl/media/projects/generic-3d.png)

## Co robi silnik

| Moduł (`js/env/`) | Odpowiedzialność |
| --- | --- |
| `3d.js` | macierze obrotu, projekcja 3D → 2D, cache macierzy i funkcji trygonometrycznych |
| `camera.js` | pozycja kamery i obrót wokół punktu |
| `model.js` | wczytywanie modeli OBJ, ściany, transformacje |
| `map.js`, `mapSector.js` | świat zbudowany z kwadratowych kafelków |
| `occlusionCulling.js` | sortowanie modeli według odległości (algorytm malarza) |
| `lightSource.js` | cieniowanie ścian w zależności od odległości od kamery |
| `collisionDetector.js` | proste sprawdzanie kolizji |
| `elements/texture.js` | teksturowanie ścian z pliku BMP |

Wektory i macierze pochodzą z biblioteki Sylvester, wyszukiwanie ścieżek z PathFinding.js, a reszta to mój własny kod.

### Obrót z cache

Macierz obrotu jest iloczynem trzech macierzy wokół osi X, Y i Z. Jej obliczenie jest kosztowne, a kąt kamery zmienia się rzadko, więc wynik jest cache'owany według kąta, a funkcje trygonometryczne przechodzą przez własny cache (`getT`):

```javascript
getM = function(th) {
  var lookup = ""+th.x+th.y+th.z;
  var existingEntry = cacheM[lookup];
  if (existingEntry !== undefined) {
    return existingEntry;
  }
  var d1 = [
    1, 0,                0                  ,
    0, getT("cos",th.x), -1*getT("sin",th.x),
    0, getT("sin",th.x), getT("cos",th.x)   ];
  var d2 = [
    getT("cos",th.y),    0, getT("sin",th.y),
    0,                   1, 0               ,
    -1*getT("sin",th.y), 0, getT("cos",th.y)];
  var d3 = [
    getT("cos",th.z), -1*getT("sin",th.z), 0,
    getT("sin",th.z), getT("cos",th.z),    0,
    0,                0,                   1];
  var newEntry = matM(matM(d1,d2),d3);
  cacheM[lookup] = newEntry;
  return newEntry;
}
```

### Algorytm malarza i „światło”

Nie ma z-bufora. `OcclusionCulling` przechowuje listę modeli, w każdej klatce aktualizuje odległość każdego z nich od kamery i sortuje listę od najdalszego do najbliższego, więc bliższe ściany są po prostu malowane na dalszych.

Oświetlenie to sztuczka, a nie model fizyczny: kolor ściany jest rozjaśniany lub przyciemniany o procent zależny od odległości między kamerą a środkiem ściany. Wyniki są cache'owane dla każdego koloru i procentu, bo konwersja string ↔ hex w JavaScripcie była zaskakująco kosztowna na telefonie z 2013 roku:

```javascript
getFaceColor: function(faceCenter, requestedColor) {
  var distance = getDistance3(camera.position, faceCenter);
  return this.getColor(requestedColor, 20-(distance/15));
}
```

## Uruchomienie

Wystarczy dowolny serwer plików statycznych. Z Rubym:

```bash
ruby -run -ehttpd . -p8000
```

Następnie otwórz `http://localhost:8000/html/core.html` i poruszaj kamerą klawiszami `a`/`d` oraz `w`/`s`. Pierwszy kafelek mapy 10×10 jest teksturowany.

## Przeniesienie matematyki na ESP8266

ESP8266 to układ Wi-Fi z 32-bitowym rdzeniem Tensilica L106, 80 KB pamięci RAM na dane i zegarem 80 lub 160 MHz. Z firmware NodeMCU uruchamia Lua, więc wziąłem część `generic` odpowiedzialną za projekcję (macierze obrotu, kamera, dzielenie perspektywiczne) i przepisałem ją w Lua (`3d.lua`). Nie ma wyświetlacza: układ oblicza rzutowane wierzchołki, a wynik trafia na konsolę szeregową, co wystarcza do zmierzenia przepustowości.

Co warto wiedzieć, zanim zaczniesz:

-   Zbuduj firmware tylko z potrzebnymi modułami za pomocą usługi *NodeMCU custom builds*. Możesz wybrać wersję integer lub float; port używa liczb całkowitych ze stałym współczynnikiem skalowania, więc działa na obu.
-   Wgraj go za pomocą `nodemcu-flasher`: firmware pod `0x00000`, a `esp_init_data_default.bin` pod `0xfc000`, 115200 bodów. Pierwsze uruchomienie formatuje system plików.
-   Pliki Lua wgrywaj przez ESPlorer. Jeśli skrypt zablokuje układ, wyczyść pamięć flash i zacznij od nowa:

```bash
python esptool.py -p COM3 -b 115200 erase_flash
```

`init.lua` mierzy wolną pamięć sterty przed uruchomieniem i po nim:

```lua
print("START HEAP "..node.heap());
dofile("3d.lua")
print("FINISH HEAP "..node.heap());
```

Program startuje z 41 424 bajtami wolnej sterty i kończy z 24 792.

## Liczby

| Platforma | Przepustowość |
| --- | --- |
| JavaScript, Opera 53, tablet z Intel Atom Z3736F, teksturowany kafelek na ekranie | 5 000–7 000 wierzchołków/s |
| JavaScript, ten sam tablet, bez teksturowanego kafelka na ekranie | ponad 10 000 wierzchołków/s |
| Lua na ESP8266 przy 80 MHz | 300 wierzchołków/s |
| Lua na ESP8266 przy 160 MHz | 600 wierzchołków/s |

Na ESP8266 szybkość skaluje się dokładnie z zegarem, co oznacza, że praca jest ograniczona wyłącznie przez CPU: przy tej skali nie ma wąskiego gardła w pamięci ani we/wy. Na tablecie jeden teksturowany kafelek w polu widzenia obniża przepustowość o jedną trzecią lub więcej: teksturowanie na canvasie 2D, bez GPU, jest zdecydowanie najkosztowniejszą rzeczą, jaką robi silnik.

## Czego mnie to nauczyło

-   Mały silnik, który rozumiesz w całości, jest lepszym punktem odniesienia niż duży, którego nie rozumiesz. Przeniesienie `generic` na cztery bardzo różne maszyny było możliwe tylko dlatego, że cały mieści mi się w głowie.
-   Cache'owanie (macierzy, funkcji trygonometrycznych, kolorów) dało więcej niż jakakolwiek mikrooptymalizacja kodu rysującego.
-   Mierzenie wierzchołków na sekundę na każdej platformie dało mi jedną liczbę do porównania przeglądarki, mikrokontrolera i komputera 8-bitowego. Tego samego podejścia użyłem później z OpenCL w [Simple High Performance Computing](https://michalasobczak.pl/posts/simple-high-performance-computing.html).

Kod źródłowy: [retro-lab/js3d-engine](https://github.com/michalasobczak/retro-lab/tree/master/js3d-engine) i [retro-lab/esp8266](https://github.com/michalasobczak/retro-lab/tree/master/esp8266). Zobacz też [przegląd moich praktycznych projektów](https://michalasobczak.pl/posts/hands-on-projects-on-github.html).
