Files
phy-vr360/docs/superpowers/specs/2026-06-24-toi-uu-phy-vr360-design.md
tuyenio 0e8af80f07 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>
2026-06-24 15:39:27 +07:00

108 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).