Files
phy-vr360/docs/superpowers/specs/2026-06-24-toi-uu-phy-vr360-design.md
T
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

7.6 KiB
Raw Blame History

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).