diff --git a/src/app/components/antiquity/AntiquityViewer.tsx b/src/app/components/antiquity/AntiquityViewer.tsx index 09f9a85..5217143 100644 --- a/src/app/components/antiquity/AntiquityViewer.tsx +++ b/src/app/components/antiquity/AntiquityViewer.tsx @@ -5,6 +5,8 @@ import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js"; +import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js"; +import { createRenderGate } from "@/app/lib/render-gate"; import { Rotate3d, Pause, Play, Maximize2 } from "lucide-react"; type Status = "loading" | "ready" | "error"; @@ -35,7 +37,7 @@ export function AntiquityViewer({ src }: { src: string }) { let disposed = false; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" }); - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.08; renderer.domElement.style.cssText = "width:100%;height:100%;display:block;cursor:grab;"; @@ -70,6 +72,18 @@ export function AntiquityViewer({ src }: { src: string }) { }); controlsRef.current = controls; + let needsRender = true; + let active = true; + const requestRender = () => { + needsRender = true; + }; + controls.addEventListener("change", requestRender); + + const stopGate = createRenderGate(mount, (isActive) => { + active = isActive; + if (isActive) requestRender(); + }); + const resize = () => { const w = mount.clientWidth; const h = mount.clientHeight; @@ -77,12 +91,14 @@ export function AntiquityViewer({ src }: { src: string }) { renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix(); + needsRender = true; }; const observer = new ResizeObserver(resize); observer.observe(mount); resize(); const loader = new GLTFLoader(); + loader.setMeshoptDecoder(MeshoptDecoder); loader.load( src, (gltf) => { @@ -108,6 +124,7 @@ export function AntiquityViewer({ src }: { src: string }) { homeRef.current = { pos: camera.position.clone(), target: controls.target.clone() }; setStatus("ready"); + requestRender(); }, (event) => { if (event.total) setProgress(Math.round((event.loaded / event.total) * 100)); @@ -118,9 +135,14 @@ export function AntiquityViewer({ src }: { src: string }) { ); const tick = () => { - controls.update(); - renderer.render(scene, camera); raf = requestAnimationFrame(tick); + if (!active) return; + // controls.update() trả về true khi camera còn chuyển động (damping/autoRotate) + const moving = controls.update(); + if (needsRender || moving || controls.autoRotate) { + renderer.render(scene, camera); + needsRender = false; + } }; tick(); @@ -128,6 +150,8 @@ export function AntiquityViewer({ src }: { src: string }) { disposed = true; cancelAnimationFrame(raf); observer.disconnect(); + stopGate(); + controls.removeEventListener("change", requestRender); controls.dispose(); scene.traverse((object) => { const mesh = object as THREE.Mesh;