# 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.000–11.800px (ví dụ 10364×5182, 11774×5887), dung lượng 12–24MB, **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ạ độ 0–100).
- 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 ~1–1.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`).
