0e8af80f07
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
7.6 KiB
Markdown
108 lines
7.6 KiB
Markdown
# 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`).
|