docs: spec tối ưu hiệu năng & sửa bản đồ phy-vr360

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
tuyenio
2026-06-24 15:39:27 +07:00
parent 464ce1e402
commit 0e8af80f07
@@ -0,0 +1,107 @@
# Thiết kế: Tối ưu hiệu năng & sửa bản đồ — phy-vr360
- **Ngày:** 2026-06-24
- **Phạm vi:** Dự án tour VR360 Chùa Thái Lạc (`phy-vr360`)
- **Mục tiêu:** (1) Bản đồ trong panel "Vị trí" hiển thị cân đối, đầy đủ, không bị khuất. (2) Loại bỏ hiện tượng đơ/giật/lag, load mượt và nhanh hơn.
## Bối cảnh & chẩn đoán
Đo thực tế trong repo:
- **Panorama gốc quá nặng:** mỗi ảnh `public/images/chua-thai-lac/*.jpg` rộng ~10.00011.800px (ví dụ 10364×5182, 11774×5887), dung lượng 1224MB, **tổng 394MB** (26 ảnh, đang được git theo dõi). Đây là ảnh xuất thẳng từ iPhone, quá khổ cho web.
- Một ảnh 11774×5887 khi giải nén thành texture chiếm **~280MB RAM/GPU** (RGBA). Đây là nguyên nhân chính gây lag, tốn bộ nhớ, load chậm.
- **Preload ồ ạt:** `preloadSceneAndHotspots` / `preloadTourScene` tải trước texture của *tất cả* cảnh kề → chuyển cảnh có thể nạp hàng chục MB cùng lúc → giật.
- **Render liên tục:** `requestAnimationFrame` vẽ mỗi frame kể cả khi đứng yên; renderer bật `preserveDrawingBuffer: true` (đã xác nhận **không** có tính năng chụp ảnh/`toDataURL` nào dùng tới → bỏ được); `pixelRatio` cap 1.8.
- **Bản đồ overview** (panel "Vị trí"): các điểm dưới cùng (01/02/03) bị cắt ở mép khung; bố cục không căn giữa/cân đối. Hiện dùng `compressOverviewY` + clamp cứng, không tự vừa khung.
- **Tài sản phụ:** `modal-1.png` (2.6MB), `modal-2.png` (2.5MB) nặng không cần thiết. Hai model `antiquity/*.glb` (28MB + 42MB) nặng nhưng thuộc tính năng cổ vật riêng → **ngoài phạm vi** (Phase 2).
`sharp` đã có sẵn trong `node_modules`.
## Quyết định đã chốt
| Hạng mục | Lựa chọn |
|---|---|
| Mức nén panorama | Rộng **6144px**, **JPEG q80** (mozjpeg, progressive) — cân bằng nét/nhẹ (~30MB tổng) |
| Sửa bản đồ | **Auto-fit** (co cả bộ điểm vào khung, lề an toàn) |
| Tối ưu engine | **Gói đầy đủ** (render on-demand, bỏ preserveDrawingBuffer, hạ pixelRatio, preload thông minh, blur-up LQIP) |
| Bản gốc ảnh | Backup sang `image-originals/` (gitignored) trước khi ghi đè |
| GLB cổ vật | Ngoài phạm vi (Phase 2) |
## Phần A — Pipeline nén ảnh
**File mới:** `scripts/optimize-images.mjs` (chạy bằng `node scripts/optimize-images.mjs`).
**Panorama** (`public/images/chua-thai-lac/*.jpg`):
1. Với mỗi ảnh: nếu `image-originals/chua-thai-lac/<tên>` **chưa** tồn tại → di chuyển ảnh hiện tại vào đó (backup). Luôn đọc **nguồn từ backup** để nén lại an toàn, không nén chồng (idempotent).
2. `sharp(src).rotate()` (áp dụng EXIF orientation rồi xoá metadata) → `.resize({ width: 6144, withoutEnlargement: true })``.jpeg({ quality: 80, mozjpeg: true, progressive: true })` → ghi đè file đích trong `public/images/chua-thai-lac/`.
3. In log: tên, kích thước trước/sau, % giảm.
**Modal PNG** (`public/modal/modal-1.png`, `modal-2.png`):
- Backup sang `image-originals/modal/` rồi nén lại bằng sharp giữ alpha: `.png({ compressionLevel: 9, palette: true, quality: 80 })`. Mục tiêu < 500KB/ảnh. `background-1.png` (252KB) giữ nguyên.
**Blur-up / LQIP** (phục vụ Phần C-5):
- Với mỗi panorama, tạo bản thu nhỏ rộng ~32px, JPEG q40, xuất base64 data-URI.
- Ghi tất cả vào `src/app/tours/chua-thai-lac.lqip.ts` dưới dạng `export const lqip: Record<string, string>` khoá theo đường dẫn ảnh (vd `"/images/chua-thai-lac/01.jpg"`). Tổng ~40KB inline.
**`.gitignore`:** thêm dòng `/image-originals`.
**Sau khi chạy:** kiểm tra mắt vài cảnh, rồi commit ảnh đã nén + `lqip.ts`.
## Phần B — Bản đồ auto-fit (`MiniMap` trong `VirtualTour.tsx`)
Chỉ áp dụng cho **overview mode** (panel "Vị trí"); radar mode trong tour giữ nguyên.
- Tính bao (bounds) của toàn bộ điểm có `mapPosition`: `minX, maxX, minY, maxY` (tính một lần, từ `scenes`).
- Đặt **lề** `MARGIN = 8` (đơn vị toạ độ 0100).
- Hàm map cho overview:
```
fittedX = MARGIN + (x - minX) / (maxX - minX) * (100 - 2*MARGIN)
fittedY = MARGIN + (y - minY) / (maxY - minY) * (100 - 2*MARGIN)
```
- Thay thế `compressOverviewY` + clamp cứng hiện tại trong nhánh `isOverview` của `getMapPosition`. Giữ `clampMapPoint` làm lớp an toàn cuối.
- Áp dụng đồng nhất cho cả node (div) và đường nối (SVG line) vì cả hai đều qua `getMapPosition`.
Kết quả: 23 điểm + nhãn luôn nằm trọn trong khung, căn giữa, cân đối, không bị cắt.
## Phần C — Gói tối ưu engine (`VirtualTour.tsx`)
1. **Render theo nhu cầu (on-demand):**
- Thêm ref `needsRenderRef` + hàm `requestRender()` đặt cờ.
- Vòng `animate()` vẫn chạy qua RAF nhưng **chỉ gọi `renderer.render()` + `updateHotspots()` khi** cờ bật hoặc đang ở trạng thái động (transition, autoRotate, VR, đang kéo controls).
- Kích hoạt `requestRender()` ở: sự kiện `controls 'change'`, resize, đổi FOV, chuyển cảnh, bật/tắt autoRotate/VR, thay đổi orientation thiết bị.
- Mục tiêu: khi đứng yên, GPU gần như không tải.
2. **Bỏ `preserveDrawingBuffer: true`** trong cấu hình `WebGLRenderer` (đã xác nhận không có tính năng nào phụ thuộc).
3. **Hạ pixelRatio:** cap `Math.min(devicePixelRatio, 1.8)` → `1.5` trong renderer của tour.
4. **Preload thông minh:**
- Chỉ preload texture của các cảnh **kề trực tiếp** với cảnh hiện tại (đã đúng), nhưng **lùi xuống lúc rảnh** bằng `requestIdleCallback` (fallback `setTimeout`) thay vì gọi ngay khi vào cảnh.
- Cảnh hiện tại vẫn tải ưu tiên ngay.
5. **Blur-up (LQIP):**
- Khi bắt đầu tải một cảnh, nạp ngay texture từ data-URI trong `lqip` (Phần A) làm ảnh nền mờ tức thì trên sphere.
- Khi texture full tải xong → thay vào, bỏ blur (có thể fade nhẹ qua cơ chế transition material sẵn có).
- Nếu cảnh không có LQIP → giữ hành vi loading hiện tại.
## Phần D — `next.config` / khác
- Không bắt buộc đổi cho texture THREE (tải trực tiếp). Welcome cards dùng `next/image` trên ảnh đã nén → tự tối ưu thêm, không cần sửa.
- Không thêm cấu hình thừa.
## Rủi ro & giảm thiểu
- **Render on-demand** là thay đổi rủi ro nhất (dễ "đứng hình" nếu quên `requestRender` ở một tương tác). Giảm thiểu: liệt kê đủ điểm kích hoạt ở trên; test kỹ kéo/zoom/chuyển cảnh/VR/auto-rotate.
- **Nén ảnh không hoàn nguyên** nếu chạy chồng. Giảm thiểu: luôn nén từ `image-originals/` backup.
- **Chất lượng khi zoom sâu:** 6144px/q80 đủ cho FOV tối thiểu 36; nếu thấy thiếu nét có thể nâng q hoặc width sau (chạy lại từ backup).
## Tiêu chí hoàn thành
- Tổng ảnh panorama giảm từ 394MB xuống ~30MB; mỗi ảnh ~11.5MB.
- Panel "Vị trí": đủ 23 điểm hiển thị, căn giữa, không điểm/nhãn nào bị cắt.
- Tour: chuyển cảnh mượt, có ảnh mờ tạm khi tải; khi đứng yên không vẽ thừa; không còn giật rõ rệt.
- `tsc --noEmit` sạch; tour chạy đúng (kéo/zoom/chuyển cảnh/mini-map/VR/auto-rotate).
## Ngoài phạm vi (Phase 2 — nếu muốn)
- Nén `antiquity/chuong-1.glb` (28MB) và `chuong-2.glb` (42MB) bằng Draco/meshopt (cần `gltf-transform`).