import React, { useEffect, useRef, useState, useCallback } from 'react'; import * as THREE from 'three'; import {type DistortionArea, SpriteEffectArea} from '@/types'; import { ThreeScene } from '@/engine/ThreeScene'; import { ShaderManager } from '@/engine/ShaderManager'; import { AnimationLoop } from '@/engine/AnimationLoop'; import { SpriteEffectManager } from '@/engine/SpriteEffectManager'; import { useAnimationFrame } from '@/hooks/useAnimationFrame'; import { useMouseInteraction } from '@/hooks/useMouseInteraction'; import { SHADER_CONFIG } from '@/utils/constants'; import { MouseInteractionConfig } from '@/types/interaction'; /** * ImageDistortion 컴포넌트 Props */ export interface ImageDistortionProps { /** 이미지 소스 URL */ imageSrc: string; /** 왜곡 영역 배열 */ areas: DistortionArea[]; /** 버텍스 셰이더 경로 (선택사항) */ vertexShaderPath?: string; /** 프래그먼트 셰이더 경로 (선택사항) */ fragmentShaderPath?: string; /** 컨테이너 스타일 */ style?: React.CSSProperties; /** 컨테이너 클래스명 */ className?: string; /** 마우스 인터랙션 설정 */ mouseInteraction?: MouseInteractionConfig; /** 독립 스프라이트 이펙트 영역 (왜곡 영역과 분리) */ spriteEffectAreas?: SpriteEffectArea[]; } /** * GPU 가속 이미지 왜곡 컴포넌트 * Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다. */ export const ImageDistortion: React.FC = ({ imageSrc, areas, vertexShaderPath, fragmentShaderPath, style, className, mouseInteraction, spriteEffectAreas = [], }) => { const containerRef = useRef(null); const sceneRef = useRef(null); const shaderManagerRef = useRef(new ShaderManager()); const textureRef = useRef(null); const spriteManagerRef = useRef(null); const currentAreasRef = useRef(areas); const [isReady, setIsReady] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); const [currentAreas, setCurrentAreas] = useState(areas); // 마우스 인터랙션 훅 const mouseInteractionHook = useMouseInteraction( containerRef, mouseInteraction || { enabled: false, physics: { stiffness: 100, damping: 10, mass: 1, influenceRadius: 0.2, maxStrength: 1.0, }, } ); // 영역 변경 시 상태 업데이트 useEffect(() => { setCurrentAreas(areas); }, [areas]); // currentAreasRef 동기화 useEffect(() => { currentAreasRef.current = currentAreas; }, [currentAreas]); // 스프라이트 이펙트 매니저 초기화 useEffect(() => { if (!sceneRef.current || !isReady) return; const manager = new SpriteEffectManager(); manager.attachToScene(sceneRef.current.getScene()); spriteManagerRef.current = manager; return () => { manager.dispose(); spriteManagerRef.current = null; }; }, [isReady]); // 이펙트 영역 변경 또는 매니저 준비 시 스프라이트 이펙트 동기화 useEffect(() => { spriteManagerRef.current?.syncEffects(spriteEffectAreas); }, [spriteEffectAreas, isReady]); // 마우스 인터랙션 설정 변경 시 업데이트 useEffect(() => { if (mouseInteraction) { mouseInteractionHook.updateConfig(mouseInteraction); } }, [mouseInteraction, mouseInteractionHook]); // Three.js 씬 초기화 useEffect(() => { console.log('[ImageDistortion] useEffect 실행, containerRef.current:', containerRef.current); if (!containerRef.current) { console.warn('[ImageDistortion] containerRef.current가 null입니다. 컴포넌트가 제대로 마운트되지 않았습니다.'); return; } console.log('[ImageDistortion] v1.5.1 초기화 시작'); const scene = new ThreeScene(containerRef.current); sceneRef.current = scene; // 셰이더 로드 const vertPath = vertexShaderPath || '/shaders/distortion.vert.glsl'; const fragPath = fragmentShaderPath || '/shaders/distortion.frag.glsl'; console.log('[ImageDistortion] 셰이더 로드 시도:', { vertPath, fragPath }); shaderManagerRef.current .loadShaders(vertPath, fragPath) .then(({ vertex, fragment }) => { console.log('[ImageDistortion] 셰이더 로드 성공'); scene.setShaderMaterial(vertex, fragment); setIsReady(true); }) .catch((error) => { console.error('[ImageDistortion] 셰이더 로드 실패:', error); }); return () => { scene.dispose(); if (textureRef.current) { textureRef.current.dispose(); } }; }, [vertexShaderPath, fragmentShaderPath]); // 이미지 텍스처 로드 useEffect(() => { if (!imageSrc || !isReady) { console.log('[ImageDistortion] 이미지 로드 스킵:', { imageSrc, isReady }); return; } console.log('[ImageDistortion] 이미지 로드 시작:', imageSrc); setImageLoaded(false); const loader = new THREE.TextureLoader(); loader.load( imageSrc, (texture) => { console.log('[ImageDistortion] 이미지 로드 성공!', { width: texture.image.width, height: texture.image.height }); textureRef.current = texture; setImageLoaded(true); if (sceneRef.current) { sceneRef.current.updateUniforms({ u_texture: { value: texture }, }); sceneRef.current.render(); console.log('[ImageDistortion] 텍스처 업데이트 및 렌더링 완료'); } }, (progress) => { console.log('[ImageDistortion] 이미지 로딩 중...', Math.round((progress.loaded / progress.total) * 100) + '%' ); }, (error) => { console.error('[ImageDistortion] 이미지 로드 실패:', error); setImageLoaded(false); } ); return () => { if (textureRef.current) { textureRef.current.dispose(); textureRef.current = null; } }; }, [imageSrc, isReady]); // 셰이더 유니폼 업데이트 useEffect(() => { if (!sceneRef.current || !isReady) return; // 현재 해상도 가져오기 const resolution = sceneRef.current.getResolution(); // 포인트 배열 생성 // UI는 좌상단 (0,0), WebGL은 좌하단 (0,0)이므로 y 좌표를 반전 const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2); currentAreas.forEach((area, areaIndex) => { area.basePoints.forEach((point, pointIndex) => { const index = (areaIndex * 4 + pointIndex) * 2; points[index] = point.x; points[index + 1] = 1.0 - point.y; // y 좌표 반전 }); }); // 드래그 벡터 배열 생성 // dragVector도 y 좌표계를 맞춰야 하므로 y를 반전 const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2); currentAreas.forEach((area, index) => { const baseIndex = index * 2; dragVectors[baseIndex] = area.dragVector.x; dragVectors[baseIndex + 1] = -area.dragVector.y; // y 방향 반전 }); // 강도 배열 생성 const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS); currentAreas.forEach((area, index) => { strengths[index] = area.distortionStrength; }); // 렌즈 효과 배열 생성 const lensEffects = new Float32Array(SHADER_CONFIG.MAX_LENS_EFFECTS); currentAreas.forEach((area, index) => { lensEffects[index] = area.lensEffect?.strength ?? 0; }); sceneRef.current.updateUniforms({ u_numAreas: { value: currentAreas.length }, u_points: { value: points }, u_dragVectors: { value: dragVectors }, u_distortionStrengths: { value: strengths }, u_lensEffects: { value: lensEffects }, }); sceneRef.current.render(); }, [currentAreas, isReady]); // 애니메이션 루프 const animationCallback = useCallback((deltaTime: number) => { if (!isReady) return; setCurrentAreas((prevAreas) => { // 현재 인터랙션 중인 영역 인덱스 가져오기 const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || new Set(); // 1. 자동 애니메이션 업데이트 let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime); updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas); // 인터랙션 중인 영역만 dragVector를 0으로 설정 if (interactingIndices.size > 0) { updatedAreas = updatedAreas.map((area, index) => { if (interactingIndices.has(index)) { return { ...area, dragVector: { x: 0, y: 0 } }; } return area; }); } // 2. 마우스 인터랙션 적용 (기존 dragVector에 스프링 변위 추가) if (mouseInteraction?.enabled) { updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime); } return updatedAreas; }); // 스프라이트 이펙트 업데이트 (왜곡 영역과 독립적) if (spriteManagerRef.current) { const mouseState = mouseInteractionHook.getMouseState(); const resolution = sceneRef.current?.getResolution() ?? { x: 1, y: 1 }; spriteManagerRef.current.update( spriteEffectAreas, deltaTime, { position: mouseState.position ?? null, isDragging: mouseState.isDragging, }, resolution, ); // 스프라이트 메쉬 변경 후 렌더링 필요 sceneRef.current?.render(); } }, [isReady, mouseInteraction, mouseInteractionHook, spriteEffectAreas]); // 애니메이션 루프 실행 useAnimationFrame(animationCallback, true); return (
{!imageLoaded && (
이미지 로딩 중...
)}
); };