Một sa bàn 3D sống động chạy ngay trên trình duyệt: tàu hơi nước, phố biển Tuy Hòa và Tháp Nghinh Phong. Mọi thứ được dựng bằng code.
Không dùng file model 3D, cũng không dùng file âm thanh. Địa hình, nhà cửa, con người, xe cộ, bầu trời và tiếng động đều sinh ra từ code.
Chạy thử · Các thế giới · Điều khiển · Kỹ thuật · Đóng góp
✨ Vì sao đáng xem
- 🗺️ Dựng từ bản đồ thật. Tuy Hòa hiện lên với độ cao SRTM, khoảng 900 con phố, khoảng 26 000 ngôi nhà theo dữ liệu Overture/OpenStreetMap, sông Đà Rằng, Núi Nhạn và đường sắt Bắc–Nam.
- 🏛️ Tháp Nghinh Phong. Hai tháp cột đá lục giác bên quảng trường hơn 7 000 m² nhìn ra biển. Ban đêm tháp lên đèn nhiều màu, có thêm lời thuyết minh bằng tiếng Việt và tiếng Anh.
- 🚶 Một thành phố biết sống. Có người đi bộ, du khách chụp ảnh, xe máy, xe đạp và ô tô chạy theo đèn tín hiệu, tàu thuyền ngoài khơi, chim bay và khinh khí cầu. Vào giờ giới nghiêm, phố đêm vắng dần.
- 🌗 Ngày, đêm và thời tiết. Đồng hồ chạy theo giờ Việt Nam thật hoặc tua nhanh (một ngày trong 4 phút). Thời tiết có mưa (người đi đường bật ô) và tuyết.
- 🎧 Âm thanh tổng hợp. Tiếng tàu xình xịch, còi, ray, mưa và chim đều tạo bằng Web Audio, không dùng file mp3 nào.
- ⚡ Nhẹ và nhanh. Cảnh được gộp draw call, dùng instancing,
SkinnedMesh cho nhân vật và biên dịch shader trước. Khoảng 270 draw call mỗi frame. Mức chất lượng tự điều chỉnh theo máy, kể cả điện thoại.
🌏 Ba thế giới
| Thế giới | Mở bằng | Có gì |
|---|
| NGHINH PHONG (trang chủ) | / | Khu 2 × 2 km quanh Tháp Nghinh Phong: phố thật, bãi biển, đại lộ có dải phân cách, vòng xuyến, đời sống phố về đêm |
| TUY HÒA | ?world=tuyhoa | Toàn thành phố từ bản đồ thật: bờ biển, sông Đà Rằng, Núi Nhạn với Tháp Nhạn, tàu con thoi Bắc–Nam |
| PYN | ?world=pyn | Thung lũng gốc: tàu hơi nước chạy vòng qua hầm và cầu, làng, cối xay gió, cừu, thuyền câu, người leo núi |
🚀 Chạy thử trong 30 giây
git clone https://github.com/nguyenhoangtin1404/pyn-world.git
cd pyn-world
npm install
npm run dev # http://localhost:5173 (thêm ?stats để xem draw call / ms mỗi frame)
npm run build # xuất ra dist/: site tĩnh, deploy lên GitHub Pages / Netlify / Vercel
npm run check # lint + kiểu + unit test + e2e (lần đầu: npx playwright install chromium)
Một vài tham số URL hữu ích: ?world=<id> mở thẳng một thế giới, ?lang=en (hoặc trang /en/) chuyển sang tiếng Anh, ?clock=fast cho đồng hồ chạy nhanh từ 9:00.
🎮 Điều khiển
| Phím | Tác dụng | Phím | Tác dụng |
|---|
1–0 | Camera: toàn cảnh, theo tàu, cây cầu, hành khách, lái tàu, theo người, chim, xe, du khách, khinh khí cầu | W A S D | Bay tự do (ở chế độ toàn cảnh) |
V | Bay tới công trình (Tháp Nghinh Phong) | I | Bật/tắt thuyết minh |
T / Shift+T | Đổi buổi trong ngày | C | Giờ thật ↔ tua nhanh |
Space | Tạm dừng | X | Dừng/chạy thời gian |
N | Thế giới tiếp theo | B | Cây cầu tiếp theo |
P | Mức pixel art | O | Viền mực |
H | Ẩn/hiện bảng | M | Tắt/bật tiếng |
Ở thung lũng PYN, các phím F G K J L đưa camera tới những góc nhỏ: đôi tình nhân, đàn cừu trên cầu, đỉnh núi, ông câu cá và tàu thủy.
🧠 Bên trong
WorldConfig ──► World ──► lõi (ray, địa hình, hầm, cầu, trời, thời tiết)
└──► features (ga, phố, nhà, xe, người, chim, tháp…) ──► systems: update / lateUpdate mỗi frame
- World = lõi + feature. Mỗi thế giới là một
WorldConfig trong src/worlds/. Feature bật/tắt bằng cfg.features, khai báo phụ thuộc bằng needs/after, và mỗi feature có luồng ngẫu nhiên riêng nên thêm một feature không làm xáo trộn feature khác.
- Render dùng chung.
StaticBatch cho vật đứng yên, Instancer cho cả đàn vật giống nhau, skinFigure gộp nhân vật có khớp thành một SkinnedMesh, ParticlePool cho khói và gợn nước. Sóng, dòng chảy, mưa và tuyết chạy trong shader.
- Tỉ lệ như sa bàn.
world.scale tách tỉ lệ bản đồ khỏi cỡ đồ vật, nhờ vậy người và xe ở Tuy Hòa (1/10) vẫn nhìn rõ.
- Kiểm thử nghiêm ngặt. Golden checksum tới từng đỉnh và từng cái cây, mô phỏng tua nhanh 300 giây, so ảnh từng điểm, kiểm tra rò rỉ GPU khi đổi thế giới, kèm axe a11y. Không có retry: test đỏ là lỗi thật.
Đọc thêm: CLAUDE.md (quy tắc kiến trúc), docs/real-worlds.md, docs/vehicles.md, docs/performance.md, docs/i18n.md.
| File | Vai trò |
|---|
src/boot.js | Entry của trang: có WebGL thì tải main.js, không thì thẻ ảnh + giới thiệu (app/fallback.js) |
src/main.js | App: renderer, camera, vòng lặp frame, dựng và đổi world |
src/app/ | Quanh App: loader.js (màn hình loading), keys.js (phím tắt), perf.js (mức chất lượng, tự hạ độ phân giải, bảng ?stats), fallback.js (không có WebGL / mất context), motion.js (prefers-reduced-motion) |
src/World.js | Một thế giới hoàn chỉnh dựng từ 1 WorldConfig: scene riêng (cả trời, đèn, sương mù), lõi (ray, địa hình, hầm, cầu, trời, thời tiết) + các feature trong config; vòng lặp chung cho mọi system (update / lateUpdate), dispose |
src/features/ | Những gì đặt vào một world, bật/tắt bằng cfg.features: station (ga chính), village (làng), halt (trạm dừng + khu phố), windmill, sheep, trees (cây/hoa/đá), clouds, train, fish, boats, balloons, villagers (dân làng/dân phố đi lại, đi tàu), birds, hikers (đường mòn + người leo núi). Dùng chung: platform.js (sân ga) |
src/services/ | Dịch vụ dùng chung trong một world, tạo lần đầu khi cần (world.service): lamps.js (đèn ban đêm), houses.js (nhà rỗng, cửa tự mở, khói ống khói), waterlife.js (gợn nước) |
src/features/villagers/ | Phần của dân làng: areas.js (lưới dẫn đường + chỗ đến ở mỗi trạm), riding.js (lên/xuống tàu), kids.js (trẻ em đi theo bố mẹ) |
src/types.d.ts | Hợp đồng giữa App, World và feature (WorldConfig, StopConfig, System, Frame, Feature…), dùng qua JSDoc; tsc kiểm tra các file có |
🤝 Đóng góp
- Chạy
npm run check trước khi push (CI chạy y như vậy).
- Commit và tiêu đề PR viết bằng tiếng Anh, theo Conventional Commits:
feat(world): …, fix(people): ….
- Cố ý thay đổi một thế giới thì chạy
npm run e2e -- --update-snapshots, xem diff golden rồi commit kèm.
- Ảnh trong README được chụp lại bằng
node tools/readme/shoot.mjs (cần npx vite --port 5233 đang chạy) và ghép bằng node tools/readme/compose.mjs.
🗺️ Dữ liệu bản đồ
World TUY HÒA và NGHINH PHONG dùng độ cao SRTM (NASA/USGS, public domain) và bản đồ từ
Overture Maps (đường sắt, đường phố, nhà, sông hồ) —
© OpenStreetMap contributors, cấp phép ODbL; nhà có thêm
nguồn mở khác qua Overture. Nguồn ghi trong sources của src/worlds/data/tuyhoa.json.
📜 Giấy phép
- Mã nguồn: GNU AGPL-3.0 trở lên. Chạy bản đã sửa trên mạng thì phải cho người dùng lấy được mã của bản đó
(site có liên kết "Mã nguồn" trong mục "Nâng cao" của bảng điều khiển). Cần dùng ngoài điều khoản này (ví dụ bản tùy biến đóng mã): liên hệ chủ repo.
- Dữ liệu bản đồ (
src/worlds/data/): ODbL 1.0, © OpenStreetMap contributors.
- Âm thanh và hình (
public/tour/, public/og.png): bảo lưu mọi quyền.
- Chi tiết và lưu ý về công trình thật: LICENSE-DATA.md.