"""
3D 모델 렌더링 모듈
다각도 렌더링, 조명 조건 적용, 카메라 파라미터 조절 기능을 제공합니다.
"""

import numpy as np
import trimesh
from typing import List, Dict, Tuple, Optional
import cv2
from PIL import Image
import math
import os

# Docker 환경에서 렌더링을 위한 설정 (OSMesa 백엔드)
# 환경 변수는 한 번만 설정하고 순서를 고려
os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3'
os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330'
os.environ['OSMESA_HEADLESS'] = '1'
os.environ['OPEN3D_HEADLESS'] = '1'
os.environ['DISPLAY'] = ':99'
os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1'
os.environ['GALLIUM_DRIVER'] = 'llvmpipe'


class Renderer:
    """3D 모델 렌더링을 담당하는 클래스"""
    
    # 클래스 변수로 모듈 로드 상태 추적
    _modules_loaded = False
    _osmesa_configured = False
    
    def __init__(self, image_size: Tuple[int, int] = (512, 512)):
        """
        렌더러 초기화
        
        Args:
            image_size (Tuple[int, int]): 렌더링 이미지 크기 (width, height)
        """
        self.image_size = image_size
        self.width, self.height = image_size
        
        # OSMesa 설정 강화 (한 번만 실행)
        if not Renderer._modules_loaded:
            self._setup_osmesa()
            Renderer._modules_loaded = True
        
        # 기본 카메라 파라미터
        self.camera_distance = 2.0
        self.camera_elevation = 30.0
        self.camera_azimuth = 0.0
        
        # 조명 설정
        self.lighting_conditions = {
            'default': {'intensity': 1.0, 'direction': [1, 1, 1]},
            'bright': {'intensity': 1.5, 'direction': [1, 1, 1]},
            'dim': {'intensity': 0.5, 'direction': [1, 1, 1]},
            'side': {'intensity': 1.0, 'direction': [1, 0, 0]},
            'top': {'intensity': 1.0, 'direction': [0, 1, 0]},
            'front': {'intensity': 1.0, 'direction': [0, 0, 1]}
        }
    
    def _setup_osmesa(self):
        """OSMesa 렌더링 환경 설정 (개선된 버전)"""
        try:
            # OSMesa 설치 상태 확인
            self._verify_osmesa_installation()
            
            # OSMesa 관련 환경 변수 재설정 (강화된 설정)
            os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3'
            os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330'
            os.environ['OSMESA_HEADLESS'] = '1'
            os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1'
            os.environ['GALLIUM_DRIVER'] = 'llvmpipe'
            os.environ['LIBGL_ALWAYS_INDIRECT'] = '1'
            os.environ['MESA_LOADER_DRIVER_OVERRIDE'] = 'llvmpipe'
            
            # matplotlib 백엔드 설정 (GUI 없는 환경)
            try:
                import matplotlib
                matplotlib.use('Agg')
                print("matplotlib 백엔드를 Agg로 설정했습니다.")
            except ImportError:
                print("matplotlib을 찾을 수 없습니다.")
            
            # trimesh OSMesa 백엔드 설정
            try:
                import trimesh
                if hasattr(trimesh, 'rendering'):
                    import trimesh.rendering
                    print("trimesh 렌더링 모듈이 로드되었습니다.")
                else:
                    print("trimesh 렌더링 모듈을 찾을 수 없습니다.")
            except ImportError as ie:
                print(f"trimesh 렌더링 모듈 import 실패: {ie}")
            
            # Open3D 헤드리스 설정
            try:
                import open3d as o3d
                print(f"Open3D 버전: {o3d.__version__}")
            except ImportError:
                print("Open3D를 찾을 수 없습니다.")
                
        except Exception as e:
            print(f"OSMesa 설정 중 경고: {e}")
            # 설정 실패해도 계속 진행
    
    def _verify_osmesa_installation(self):
        """OSMesa 설치 상태 확인"""
        try:
            import subprocess
            import ctypes
            import glob
            
            print("OSMesa 설치 상태를 확인하는 중...")
            
            # 1. 시스템 라이브러리 확인
            osmesa_found = False
            for lib_path in ['/usr/lib/x86_64-linux-gnu/libOSMesa.so.8', 
                           '/usr/lib/x86_64-linux-gnu/libOSMesa.so.6',
                           '/usr/lib/x86_64-linux-gnu/libOSMesa.so']:
                if os.path.exists(lib_path):
                    try:
                        osmesa_lib = ctypes.CDLL(lib_path)
                        print(f"✓ OSMesa 라이브러리가 발견되었습니다: {lib_path}")
                        osmesa_found = True
                        break
                    except OSError:
                        continue
            
            if not osmesa_found:
                print("⚠ OSMesa 라이브러리를 찾을 수 없습니다.")
                # 라이브러리 경로 검색
                lib_paths = glob.glob('/usr/lib/x86_64-linux-gnu/libOSMesa*')
                if lib_paths:
                    print(f"발견된 OSMesa 라이브러리: {lib_paths}")
            
            # 2. OpenGL 라이브러리 확인
            try:
                gl_lib = ctypes.CDLL('libGL.so.1')
                print("✓ OpenGL 라이브러리가 발견되었습니다.")
            except OSError:
                print("⚠ OpenGL 라이브러리를 찾을 수 없습니다.")
            
            # 3. PyOpenGL 확인
            try:
                import OpenGL
                import OpenGL.GL
                print(f"✓ PyOpenGL 버전: {OpenGL.__version__}")
            except ImportError:
                print("⚠ PyOpenGL을 가져올 수 없습니다.")
            
            # 4. 환경 변수 확인
            platform = os.environ.get('PYOPENGL_PLATFORM', 'None')
            print(f"✓ PYOPENGL_PLATFORM: {platform}")
            
            # 5. LD_LIBRARY_PATH 확인
            ld_path = os.environ.get('LD_LIBRARY_PATH', 'None')
            print(f"✓ LD_LIBRARY_PATH: {ld_path}")
            
        except Exception as e:
            print(f"OSMesa 설치 확인 중 오류: {e}")
    
    def _ensure_osmesa_context(self):
        """OSMesa 컨텍스트가 올바르게 초기화되었는지 확인하고 설정"""
        # 이미 설정된 경우 중복 실행 방지
        if Renderer._osmesa_configured:
            return
            
        try:
            # OSMesa 관련 환경 변수 재확인
            if os.environ.get('PYOPENGL_PLATFORM') != 'osmesa':
                os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
                print("PYOPENGL_PLATFORM을 osmesa로 재설정했습니다.")
            
            # OSMesa 렌더링을 위한 추가 환경 변수 설정
            os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3'
            os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330'
            os.environ['OSMESA_HEADLESS'] = '1'
            os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1'
            os.environ['GALLIUM_DRIVER'] = 'llvmpipe'
            
            # trimesh의 OSMesa 렌더링 백엔드 확인 (한 번만)
            import trimesh
            if hasattr(trimesh, 'rendering'):
                # OSMesa 렌더러 사용 가능 여부 확인
                try:
                    # trimesh의 렌더링 모듈에서 OSMesa 사용 가능 여부 확인
                    import trimesh.rendering
                    print("trimesh 렌더링 모듈이 로드되었습니다.")
                except Exception as re:
                    print(f"trimesh 렌더링 모듈 로드 실패: {re}")
            
            Renderer._osmesa_configured = True
            
        except Exception as e:
            print(f"OSMesa 컨텍스트 설정 중 오류: {e}")
    
    def _initialize_osmesa_context(self):
        """OSMesa 컨텍스트를 명시적으로 초기화"""
        try:
            # PyOpenGL을 사용하여 OSMesa 컨텍스트 직접 초기화
            import OpenGL
            import OpenGL.GL
            import OpenGL.osmesa
            
            # OSMesa 컨텍스트 생성
            context = OpenGL.osmesa.OSMesaCreateContext(OpenGL.GL.GL_RGBA, None)
            if context is None:
                raise RuntimeError("OSMesa 컨텍스트 생성 실패")
            
            # 버퍼 생성
            buffer = (OpenGL.GL.GLubyte * (self.width * self.height * 4))()
            
            # 컨텍스트를 버퍼에 바인딩
            if not OpenGL.osmesa.OSMesaMakeCurrent(context, buffer, OpenGL.GL.GL_UNSIGNED_BYTE, self.width, self.height):
                raise RuntimeError("OSMesa 컨텍스트 바인딩 실패")
            
            # OpenGL 상태 초기화
            OpenGL.GL.glClearColor(0.0, 0.0, 0.0, 1.0)
            OpenGL.GL.glClear(OpenGL.GL.GL_COLOR_BUFFER_BIT | OpenGL.GL.GL_DEPTH_BUFFER_BIT)
            OpenGL.GL.glEnable(OpenGL.GL.GL_DEPTH_TEST)
            OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHTING)
            OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHT0)
            
            print("OSMesa 컨텍스트가 성공적으로 초기화되었습니다.")
            
        except Exception as e:
            print(f"OSMesa 컨텍스트 초기화 실패: {e}")
            # 실패해도 계속 진행 (trimesh가 자체적으로 처리할 수 있음)
    
    def _normalize_mesh(self, mesh: trimesh.Trimesh) -> trimesh.Trimesh:
        """메시를 적절한 크기로 정규화 (OSMesa 렌더링을 위해)"""
        try:
            # 메시 복사
            normalized_mesh = mesh.copy()
            
            # 바운딩 박스 계산
            bbox = normalized_mesh.bounds
            center = (bbox[0] + bbox[1]) / 2
            size = bbox[1] - bbox[0]
            max_size = np.max(size)
            
            # 메시가 너무 작거나 큰 경우 정규화
            if max_size < 0.1 or max_size > 10.0:
                # 중심을 원점으로 이동
                normalized_mesh.vertices = normalized_mesh.vertices - center
                
                # 적절한 크기로 스케일링 (대각선 길이가 2가 되도록)
                scale_factor = 2.0 / max_size
                normalized_mesh.vertices = normalized_mesh.vertices * scale_factor
                
                print(f"메시를 정규화했습니다. 스케일 팩터: {scale_factor}")
            
            return normalized_mesh
            
        except Exception as e:
            print(f"메시 정규화 실패: {e}")
            return mesh
    
    def _create_empty_image(self) -> np.ndarray:
        """빈 이미지 생성"""
        return np.zeros((self.height, self.width, 3), dtype=np.uint8)
    
    def _configure_trimesh_rendering(self):
        """trimesh 렌더링을 위한 설정 (디스플레이 연결 방지)"""
        # 이미 설정된 경우 중복 실행 방지
        if Renderer._osmesa_configured:
            return
            
        try:
            # trimesh가 디스플레이에 연결하지 않도록 강화된 환경 변수 설정
            os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
            os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3'
            os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330'
            os.environ['OSMESA_HEADLESS'] = '1'
            os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1'
            os.environ['GALLIUM_DRIVER'] = 'llvmpipe'
            os.environ['DISPLAY'] = ''  # 디스플레이 연결 방지
            os.environ['QT_QPA_PLATFORM'] = 'offscreen'  # Qt 오프스크린 모드
            os.environ['MPLBACKEND'] = 'Agg'  # matplotlib 백엔드 강제 설정
            
            # trimesh의 렌더링 모듈에서 OSMesa 사용 가능 여부 확인
            try:
                import trimesh
                import trimesh.rendering
                
                if hasattr(trimesh.rendering, 'SceneViewer'):
                    print("trimesh SceneViewer 사용 가능")
                
            except ImportError as ie:
                print(f"trimesh 렌더링 모듈 import 실패: {ie}")
            
            # PyOpenGL OSMesa 모듈 직접 초기화
            try:
                import OpenGL.osmesa
                import OpenGL.GL
                
                # OSMesa 라이브러리 로드 확인
                print("OSMesa 라이브러리 로드 성공")
                
            except ImportError as ie:
                print(f"OSMesa 모듈 import 실패: {ie}")
            
        except Exception as e:
            print(f"trimesh 렌더링 설정 실패: {e}")
    
    def _render_with_direct_osmesa(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray:
        """직접 OSMesa를 사용한 렌더링 (GLU 없이, 메모리 안전)"""
        context = None
        try:
            import OpenGL.GL as GL
            import OpenGL.osmesa as OSMesa
            import numpy as np
            
            # OSMesa 컨텍스트 생성
            context = OSMesa.OSMesaCreateContext(OSMesa.OSMESA_RGBA, None)
            if context is None:
                raise RuntimeError("OSMesa 컨텍스트 생성 실패")
            
            # 렌더링 버퍼 생성
            buffer = (GL.GLubyte * (self.width * self.height * 4))()
            
            # 컨텍스트를 버퍼에 바인딩
            if not OSMesa.OSMesaMakeCurrent(context, buffer, GL.GL_UNSIGNED_BYTE, self.width, self.height):
                raise RuntimeError("OSMesa 컨텍스트 바인딩 실패")
            
            # OpenGL 상태 설정
            GL.glClearColor(0.0, 0.0, 0.0, 1.0)
            GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
            GL.glEnable(GL.GL_DEPTH_TEST)
            GL.glEnable(GL.GL_LIGHTING)
            GL.glEnable(GL.GL_LIGHT0)
            
            # 뷰포트 설정
            GL.glViewport(0, 0, self.width, self.height)
            
            # 투영 행렬 설정 (GLU 없이 직접 계산)
            GL.glMatrixMode(GL.GL_PROJECTION)
            GL.glLoadIdentity()
            
            # gluPerspective 대신 직접 투영 행렬 계산
            fov = 45.0
            aspect = self.width / self.height
            near = 0.1
            far = 100.0
            
            f = 1.0 / np.tan(np.radians(fov) / 2.0)
            projection_matrix = np.array([
                [f/aspect, 0, 0, 0],
                [0, f, 0, 0],
                [0, 0, (far+near)/(near-far), (2*far*near)/(near-far)],
                [0, 0, -1, 0]
            ], dtype=np.float32)
            
            GL.glLoadMatrixf(projection_matrix.flatten())
            
            # 모델뷰 행렬 설정
            GL.glMatrixMode(GL.GL_MODELVIEW)
            GL.glLoadIdentity()
            
            # gluLookAt 대신 직접 뷰 행렬 계산
            target = np.array([0, 0, 0])
            up = np.array([0, 0, 1])
            
            forward = target - camera_pos
            forward = forward / np.linalg.norm(forward)
            
            right = np.cross(forward, up)
            if np.linalg.norm(right) < 1e-6:
                right = np.array([1, 0, 0])
            right = right / np.linalg.norm(right)
            
            up = np.cross(right, forward)
            
            view_matrix = np.array([
                [right[0], up[0], -forward[0], 0],
                [right[1], up[1], -forward[1], 0],
                [right[2], up[2], -forward[2], 0],
                [-np.dot(right, camera_pos), -np.dot(up, camera_pos), np.dot(forward, camera_pos), 1]
            ], dtype=np.float32)
            
            GL.glLoadMatrixf(view_matrix.flatten())
            
            # 메시 렌더링
            vertices = mesh.vertices
            faces = mesh.faces
            
            GL.glBegin(GL.GL_TRIANGLES)
            for face in faces:
                for vertex_idx in face:
                    vertex = vertices[vertex_idx]
                    GL.glVertex3f(vertex[0], vertex[1], vertex[2])
            GL.glEnd()
            
            # 버퍼에서 이미지 데이터 추출
            image_data = np.frombuffer(buffer, dtype=np.uint8)
            image_data = image_data.reshape((self.height, self.width, 4))
            
            # RGBA를 RGB로 변환
            image_rgb = image_data[:, :, :3]
            
            # 이미지 뒤집기 (OpenGL은 아래에서 위로 렌더링)
            image_rgb = np.flipud(image_rgb)
            
            return image_rgb
            
        except Exception as e:
            print(f"직접 OSMesa 렌더링 실패: {e}")
            raise
        finally:
            # 메모리 정리 (반드시 실행)
            if context is not None:
                try:
                    OSMesa.OSMesaDestroyContext(context)
                except Exception as cleanup_error:
                    print(f"OSMesa 컨텍스트 정리 중 오류: {cleanup_error}")
    
    
    def render_multiple_views(self, model: Dict, num_views: int = 36) -> List[np.ndarray]:
        """
        360도 다각도 렌더링을 수행합니다.
        
        Args:
            model (Dict): 3D 모델 정보
            num_views (int): 렌더링할 뷰의 개수
            
        Returns:
            List[np.ndarray]: 렌더링된 이미지 리스트
        """
        vertices = model['vertices']
        faces = model['faces']
        
        # trimesh 메시 객체 생성
        mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
        
        rendered_images = []
        
        for i in range(num_views):
            # 각도 계산 (0도부터 360도까지)
            angle = (360.0 / num_views) * i
            
            # 카메라 위치 계산
            camera_pos = self._calculate_camera_position(angle)
            
            # 렌더링 수행
            rendered_image = self._render_single_view(mesh, camera_pos)
            rendered_images.append(rendered_image)
        
        return rendered_images
    
    def apply_lighting_conditions(self, model: Dict, lighting: str = 'default') -> Dict:
        """
        다양한 조명 조건을 적용합니다.
        
        Args:
            model (Dict): 3D 모델 정보
            lighting (str): 조명 조건 ('default', 'bright', 'dim', 'side', 'top', 'front')
            
        Returns:
            Dict: 조명이 적용된 모델 정보
        """
        if lighting not in self.lighting_conditions:
            lighting = 'default'
        
        # 조명 정보를 모델에 추가
        model_with_lighting = model.copy()
        model_with_lighting['lighting'] = self.lighting_conditions[lighting]
        
        return model_with_lighting
    
    def adjust_camera_parameters(self, distance: float, elevation: float, azimuth: float = 0.0) -> Dict:
        """
        카메라 파라미터를 조절합니다.
        
        Args:
            distance (float): 카메라 거리
            elevation (float): 카메라 고도각 (도)
            azimuth (float): 카메라 방위각 (도)
            
        Returns:
            Dict: 카메라 파라미터 정보
        """
        self.camera_distance = distance
        self.camera_elevation = elevation
        self.camera_azimuth = azimuth
        
        camera_params = {
            'distance': distance,
            'elevation': elevation,
            'azimuth': azimuth,
            'position': self._calculate_camera_position(azimuth, elevation)
        }
        
        return camera_params
    
    def render_with_lighting(self, model: Dict, lighting: str = 'default', 
                           num_views: int = 8) -> List[np.ndarray]:
        """
        조명 조건을 적용하여 다각도 렌더링을 수행합니다.
        
        Args:
            model (Dict): 3D 모델 정보
            lighting (str): 조명 조건
            num_views (int): 렌더링할 뷰의 개수
            
        Returns:
            List[np.ndarray]: 렌더링된 이미지 리스트
        """
        # 조명 적용
        model_with_lighting = self.apply_lighting_conditions(model, lighting)
        
        # 다각도 렌더링
        rendered_images = self.render_multiple_views(model_with_lighting, num_views)
        
        return rendered_images
    
    def _render_single_view(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray:
        """
        trimesh OSMesa 기반 단일 뷰 렌더링 (수정된 버전)
        
        Args:
            mesh (trimesh.Trimesh): 렌더링할 메시
            camera_pos (np.ndarray): 카메라 위치
            
        Returns:
            np.ndarray: 렌더링된 이미지
        """
        try:
            # OSMesa 렌더링을 위한 추가 설정 (한 번만 실행)
            self._ensure_osmesa_context()
            self._configure_trimesh_rendering()
            
            # 메시 유효성 검사 및 정규화
            if not hasattr(mesh, 'vertices') or len(mesh.vertices) == 0:
                print("메시에 유효한 정점이 없습니다.")
                return self._create_empty_image()
            
            # 메시 정규화 (중요: OSMesa가 제대로 작동하려면 메시가 적절한 크기여야 함)
            mesh = self._normalize_mesh(mesh)
            
            # 직접 OSMesa 렌더링 시도
            try:
                return self._render_with_direct_osmesa(mesh, camera_pos)
            except Exception as e:
                print(f"OSMesa 렌더링 실패: {e}")
                # 간단한 정사영 렌더링으로 폴백
                return self._render_simple_fallback(mesh, camera_pos)
            
        except Exception as e:
            print(f"렌더링 실패: {e}")
            # 간단한 정사영 렌더링으로 폴백
            return self._render_simple_fallback(mesh, camera_pos)
    
    def _get_camera_transform_simple(self, camera_pos: np.ndarray) -> np.ndarray:
        """간단한 카메라 변환 행렬 생성 (trimesh용)"""
        # 카메라가 원점을 바라보도록 설정
        target = np.array([0, 0, 0])
        up = np.array([0, 0, 1])
        
        # 카메라 방향 벡터 계산
        forward = target - camera_pos
        forward = forward / np.linalg.norm(forward)
        
        # 오른쪽 벡터 계산
        right = np.cross(forward, up)
        if np.linalg.norm(right) < 1e-6:  # forward와 up이 평행한 경우
            right = np.array([1, 0, 0])
        right = right / np.linalg.norm(right)
        
        # 위쪽 벡터 재계산
        up = np.cross(right, forward)
        
        # 변환 행렬 생성
        transform = np.eye(4)
        transform[:3, 0] = right
        transform[:3, 1] = up
        transform[:3, 2] = -forward
        transform[:3, 3] = camera_pos
        
        return transform
    
    def _get_rotation_matrix(self, direction: np.ndarray) -> np.ndarray:
        """카메라 방향에 따른 회전 행렬 생성"""
        # 기본 up 벡터
        up = np.array([0, 0, 1])
        
        # right 벡터 계산
        right = np.cross(direction, up)
        if np.linalg.norm(right) < 1e-6:  # direction이 up과 평행한 경우
            right = np.array([1, 0, 0])
        right = right / np.linalg.norm(right)
        
        # up 벡터 재계산
        up = np.cross(right, direction)
        up = up / np.linalg.norm(up)
        
        # 회전 행렬 구성
        rotation = np.eye(3)
        rotation[:, 0] = right
        rotation[:, 1] = up
        rotation[:, 2] = -direction  # OpenGL 좌표계에 맞게 반전
        
        return rotation
    

    def _render_simple_projection(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray:
        """가장 안정적인 정사영 렌더링"""
        try:
            vertices = np.array(mesh.vertices)
            
            # 메시 정규화
            bbox = mesh.bounds
            center = (bbox[0] + bbox[1]) / 2
            size = bbox[1] - bbox[0]
            max_size = np.max(size)
            vertices_norm = (vertices - center) / max_size
            
            # 배경을 중간 회색으로 설정하여 엣지 검출 개선
            image = np.full((self.height, self.width, 3), 128, dtype=np.uint8)
            
            # 카메라 방향 정사영
            camera_direction = camera_pos / np.linalg.norm(camera_pos)
            projected = vertices_norm @ camera_direction
            
            # 2D 좌표 변환
            scale = min(self.width, self.height) / 2.4
            for i, (vertex, proj) in enumerate(zip(vertices_norm, projected)):
                if proj > 0:  # 카메라 앞쪽만
                    x = int((vertex[0] * scale) + self.width / 2)
                    y = int((vertex[1] * scale) + self.height / 2)
                    
                    if 0 <= x < self.width and 0 <= y < self.height:
                        # 거리에 따른 색상 계산 (더 명확한 대비)
                        intensity = max(50, min(255, int(255 * (1 - proj / 2))))
                        
                        # 점 대신 작은 원으로 그리기
                        cv2.circle(image, (x, y), 2, (intensity, intensity, intensity), -1)
                        
                        # 인접한 점들과 선으로 연결
                        if i > 0:
                            prev_vertex = vertices_norm[i-1]
                            prev_proj = projected[i-1]
                            if prev_proj > 0:
                                prev_x = int((prev_vertex[0] * scale) + self.width / 2)
                                prev_y = int((prev_vertex[1] * scale) + self.height / 2)
                                
                                if (0 <= prev_x < self.width and 0 <= prev_y < self.height and
                                    0 <= x < self.width and 0 <= y < self.height):
                                    cv2.line(image, (prev_x, prev_y), (x, y), (intensity, intensity, intensity), 1)
            
            return image
            
        except Exception as e:
            print(f"정사영 렌더링 실패: {e}")
            return np.zeros((self.height, self.width, 3), dtype=np.uint8)

    def _render_simple_fallback(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray:
        """렌더링 실패 시 사용할 간단한 대체 방법 (메모리 안전)"""
        try:
            # 메시의 바운딩 박스 계산
            bbox = mesh.bounds
            center = (bbox[0] + bbox[1]) / 2
            size = bbox[1] - bbox[0]
            max_size = np.max(size)
            
            # 최소 크기 보장
            if max_size < 1e-6:
                max_size = 1.0
            
            # 간단한 정사영 렌더링
            image = np.full((self.height, self.width, 3), 128, dtype=np.uint8)
            
            # 카메라 방향에 따른 정사영
            camera_distance = np.linalg.norm(camera_pos)
            if camera_distance < 1e-6:
                camera_distance = 1.0
            camera_direction = camera_pos / camera_distance
            
            # 정점들을 카메라 방향으로 정사영
            vertices = mesh.vertices - center
            projected = vertices @ camera_direction
            
            # 정사영된 점들을 이미지 좌표로 변환
            scale = min(self.width, self.height) / (max_size * 1.2)
            
            # 성능을 위해 샘플링 (너무 많은 정점이 있는 경우)
            num_vertices = len(vertices)
            if num_vertices > 10000:
                step = num_vertices // 10000
                vertices = vertices[::step]
                projected = projected[::step]
            
            for i, (vertex, proj) in enumerate(zip(vertices, projected)):
                if proj > 0:  # 카메라 앞쪽에 있는 점만
                    # 정사영 좌표 계산
                    x = int((vertex[0] * scale) + self.width / 2)
                    y = int((vertex[1] * scale) + self.height / 2)
                    
                    if 0 <= x < self.width and 0 <= y < self.height:
                        # 거리에 따른 색상 계산
                        intensity = max(0, min(255, int(255 * (1 - proj / max_size))))
                        image[y, x] = [intensity, intensity, intensity]
            
            return image
            
        except Exception as e:
            print(f"대체 렌더링도 실패: {e}")
            return np.zeros((self.height, self.width, 3), dtype=np.uint8)
    
    def _calculate_camera_position(self, azimuth: float, elevation: float = None) -> np.ndarray:
        """
        카메라 위치를 계산합니다.
        
        Args:
            azimuth (float): 방위각 (도)
            elevation (float): 고도각 (도)
            
        Returns:
            np.ndarray: 카메라 위치 [x, y, z]
        """
        if elevation is None:
            elevation = self.camera_elevation
        
        # 각도를 라디안으로 변환
        azimuth_rad = math.radians(azimuth)
        elevation_rad = math.radians(elevation)
        
        # 구면 좌표를 직교 좌표로 변환
        x = self.camera_distance * math.cos(elevation_rad) * math.cos(azimuth_rad)
        y = self.camera_distance * math.cos(elevation_rad) * math.sin(azimuth_rad)
        z = self.camera_distance * math.sin(elevation_rad)
        
        return np.array([x, y, z])
    
    
    def create_depth_map(self, model: Dict, camera_pos: np.ndarray) -> np.ndarray:
        """
        깊이 맵을 생성합니다.
        
        Args:
            model (Dict): 3D 모델 정보
            camera_pos (np.ndarray): 카메라 위치
            
        Returns:
            np.ndarray: 깊이 맵
        """
        vertices = model['vertices']
        faces = model['faces']
        
        # 메시를 점군으로 변환
        pointcloud = vertices
        
        # 카메라 방향에 따른 간단한 변환
        camera_distance = np.linalg.norm(camera_pos)
        camera_direction = camera_pos / camera_distance
        
        # 점들을 카메라 방향으로 정사영
        camera_points = pointcloud - camera_pos
        
        # 깊이 값 추출 (카메라로부터의 거리)
        depths = np.linalg.norm(camera_points, axis=1)
        
        # 깊이 맵 생성 (간단한 투영)
        depth_map = np.zeros((self.height, self.width))
        
        # 점들을 이미지 평면에 투영
        for i, point in enumerate(camera_points):
            if depths[i] > 0:  # 유효한 깊이를 가진 점만
                # 간단한 정사영
                x = int((point[0] * 100) + self.width / 2)
                y = int((point[1] * 100) + self.height / 2)
                
                if 0 <= x < self.width and 0 <= y < self.height:
                    depth_map[y, x] = depths[i]
        
        return depth_map
    
    def get_rendering_statistics(self, rendered_images: List[np.ndarray]) -> Dict:
        """
        렌더링 결과의 통계를 계산합니다.
        
        Args:
            rendered_images (List[np.ndarray]): 렌더링된 이미지 리스트
            
        Returns:
            Dict: 렌더링 통계 정보
        """
        if not rendered_images:
            return {}
        
        # 각 이미지의 통계 계산
        stats = {
            'num_images': len(rendered_images),
            'image_size': rendered_images[0].shape,
            'mean_brightness': [],
            'std_brightness': [],
            'mean_contrast': []
        }
        
        for img in rendered_images:
            if len(img.shape) == 3:
                gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
            else:
                gray = img
            
            stats['mean_brightness'].append(np.mean(gray))
            stats['std_brightness'].append(np.std(gray))
            stats['mean_contrast'].append(np.std(gray) / (np.mean(gray) + 1e-8))
        
        # 전체 통계 계산
        stats['avg_brightness'] = np.mean(stats['mean_brightness'])
        stats['avg_contrast'] = np.mean(stats['mean_contrast'])
        stats['brightness_std'] = np.std(stats['mean_brightness'])
        
        return stats
