import * as THREE from 'three'; import type { ShaderUniforms } from '@/types'; /** * Three.js 씬 관리 클래스 */ export class ThreeScene { private scene: THREE.Scene; private camera: THREE.OrthographicCamera; private renderer: THREE.WebGLRenderer; private mesh: THREE.Mesh | null = null; private uniforms: ShaderUniforms; constructor(private container: HTMLElement) { // 씬 생성 this.scene = new THREE.Scene(); // 2D용 직교 카메라 설정 (카메라는 -z 방향, near=0 ~ far=1) this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); // 렌더러 설정 this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, }); this.renderer.setPixelRatio(window.devicePixelRatio); this.container.appendChild(this.renderer.domElement); // 유니폼 초기화 this.uniforms = { u_resolution: { value: new THREE.Vector2() }, u_texture: { value: null }, u_points: { value: new Float32Array(64) }, // 32포인트 × 2(x,y) u_numAreas: { value: 0 }, u_dragVectors: { value: new Float32Array(16) }, // 8벡터 × 2(x,y) u_distortionStrengths: { value: new Float32Array(8) }, u_lensEffects: { value: new Float32Array(8) }, }; this.handleResize(); window.addEventListener('resize', this.handleResize); } /** * 윈도우 리사이즈 핸들러 */ private handleResize = () => { const width = this.container.clientWidth; const height = this.container.clientHeight; this.renderer.setSize(width, height); this.uniforms.u_resolution.value.set(width, height); if (this.mesh) { this.render(); } }; /** * 셰이더 머티리얼 설정 * @param vertexShader 버텍스 셰이더 소스 * @param fragmentShader 프래그먼트 셰이더 소스 */ public setShaderMaterial(vertexShader: string, fragmentShader: string) { console.log('[ThreeScene] setShaderMaterial 호출됨'); console.log('[ThreeScene] vertexShader 길이:', vertexShader.length); console.log('[ThreeScene] fragmentShader 길이:', fragmentShader.length); const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: this.uniforms, vertexShader, fragmentShader, }); console.log('[ThreeScene] ShaderMaterial 생성됨'); // 셰이더 컴파일 에러 확인 const renderer = this.renderer; const testScene = new THREE.Scene(); const testMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material); testScene.add(testMesh); try { renderer.compile(testScene, this.camera); console.log('[ThreeScene] 셰이더 컴파일 성공!'); } catch (e) { console.error('[ThreeScene] 셰이더 컴파일 에러:', e); } if (this.mesh) { this.scene.remove(this.mesh); } this.mesh = new THREE.Mesh(geometry, material); this.mesh.renderOrder = 0; this.scene.add(this.mesh); console.log('[ThreeScene] mesh를 씬에 추가함'); } /** * Three.js 씬 객체 반환 */ public getScene(): THREE.Scene { return this.scene; } /** * 유니폼 값 업데이트 * @param updates 업데이트할 유니폼 값들 */ public updateUniforms(updates: Partial) { Object.keys(updates).forEach((key) => { const uniformKey = key as keyof ShaderUniforms; this.uniforms[uniformKey].value = updates[uniformKey]!.value; }); } /** * 씬 렌더링 */ public render() { this.renderer.render(this.scene, this.camera); } /** * 현재 해상도 가져오기 */ public getResolution(): { x: number; y: number } { return { x: this.uniforms.u_resolution.value.x, y: this.uniforms.u_resolution.value.y, }; } /** * 리소스 정리 */ public dispose() { window.removeEventListener('resize', this.handleResize); this.renderer.dispose(); if (this.mesh) { this.mesh.geometry.dispose(); (this.mesh.material as THREE.Material).dispose(); } if (this.container.contains(this.renderer.domElement)) { this.container.removeChild(this.renderer.domElement); } } }