OpenGL을 사용하여 3D 모델을 로드하여 화면에 보여주고 마우스 드래그로 회전시켜보는 예제 코드입니다.
소프트웨어적으로 구현하니 너무 느려서 NVidia 그래픽카드 GPU를 사용하도록 했습니다.
2024. 1. 3 최초작성
실행결과입니다. 마우스 드래그로 모델을 좌우상하로 회전시킬수 있습니다. 하지만 아직 어색합니다. 그리고 모델 문제인지 구현한 코드 문제인지 약간 깨져 보입니다.
그래서 다음 링크에 있는 다른 모델을 테스트해봤습니다. 깨짐이 없어 고양이 모델의 문제로 보입니다. 좌우상하 이동이 고양이에 비해 어색합니다.
https://free3d.com/3d-model/tree-74556.html
Ubuntu 22.04에 설치된 miniconda를 사용한 파이썬 개발 환경에서 테스트를 진행했습니다.
Visual Studio Code와 Miniconda를 사용한 Python 개발 환경 만들기( Windows, Ubuntu, WSL2)
https://webnautes.tistory.com/1842
파이썬 가상환경을 생성하고 활성화합니다.
$ conda create -n opengl python=3.10
$ conda activate opengl
필요한 패키지를 설치합니다.
$ pip install PyOpenGL PyOpenGL-accelerate pygame
다음 패키지를 추가로 설치해야 합니다.
$ sudo apt-get install -y libgl1-mesa-dev libgl1-mesa-glx mesa-utils libgl1-mesa-dri
$ sudo apt-get install -y xvfb x11-xserver-utils
$ sudo apt-get install -y python3-opengl
아래 링크에서 고양이 모델을 다운로드합니다. 다른 모델을 다운로드 해도 됩니다.
https://free3d.com/3d-model/cat-v1--522281.html
코드에서 지원하는 모델인 확장자가 obj인 파일을 복사해둡니다.
테스트에 사용한 코드입니다.
import os os.environ['__NV_PRIME_RENDER_OFFLOAD'] = '1' os.environ['__GLX_VENDOR_LIBRARY_NAME'] = 'nvidia' import numpy as np from OpenGL.GL import * from OpenGL.GL import shaders from OpenGL.GLU import * import pygame # 버텍스 쉐이더 VERTEX_SHADER = """ #version 330 core layout(location = 0) in vec3 position; layout(location = 1) in vec3 normal; uniform mat4 model; uniform mat4 view; uniform mat4 projection; out vec3 FragPos; out vec3 Normal; void main() { FragPos = vec3(model * vec4(position, 1.0)); Normal = mat3(transpose(inverse(model))) * normal; gl_Position = projection * view * model * vec4(position, 1.0); } """ # 프래그먼트 쉐이더 수정 FRAGMENT_SHADER = """ #version 330 core in vec3 FragPos; in vec3 Normal; uniform vec3 lightPos; uniform vec3 viewPos; uniform vec3 lightColor; uniform vec3 objectColor; out vec4 FragColor; void main() { // 주변광 세기 증가 float ambientStrength = 0.4; vec3 ambient = ambientStrength * lightColor; // 분산광 (디퓨즈) vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor; // 반사광 (스페큘러) 추가 float specularStrength = 0.5; vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * lightColor; vec3 result = (ambient + diffuse + specular) * objectColor; FragColor = vec4(result, 1.0); } """ class ModernModelViewer: def __init__(self, model_path): pygame.init() # OpenGL 설정 pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1) pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 4) pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 24) pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, 8) self.display = pygame.display.set_mode((800, 600), pygame.OPENGL | pygame.DOUBLEBUF | pygame.HWSURFACE) pygame.display.set_caption("3D Model Viewer (Modern OpenGL)") # 모델 데이터 로드 self.vertices, self.faces, self.normals = self.load_obj(model_path) # 변환 행렬 초기화 self.model_matrix = np.identity(4, dtype=np.float32) self.view_matrix = np.identity(4, dtype=np.float32) self.projection_matrix = self.perspective(45, 800/600, 0.1, 100.0) # 카메라/뷰어 상태 self.camera_pos = np.array([0.0, 2.0, 5.0], dtype=np.float32) # z축 거리를 늘리고 y축으로 올립니다 self.rotation = np.array([30.0, 0.0, 0.0]) # x축으로 30도 회전하여 모델을 내려다보게 합니다 self.scale = 1.0 # 마우스 제어 관련 속성 추가 self.dragging = False self.prev_mouse_pos = None self.mouse_sensitivity = 0.8 # 쉐이더 프로그램 생성 self.shader = self.create_shader_program() # VAO, VBO 설정 self.setup_vertex_buffer() # FPS 제어 self.clock = pygame.time.Clock() self.target_fps = 144 def perspective(self, fovy, aspect, near, far): """원근 투영 행렬 생성""" f = 1.0 / np.tan(np.radians(fovy) / 2.0) return np.array([ [f/aspect, 0.0, 0.0, 0.0], [0.0, f, 0.0, 0.0], [0.0, 0.0, (far+near)/(near-far), 2*far*near/(near-far)], [0.0, 0.0, -1.0, 0.0] ], dtype=np.float32) def create_shader_program(self): """쉐이더 프로그램 컴파일 및 링크""" try: vertex_shader = shaders.compileShader(VERTEX_SHADER, GL_VERTEX_SHADER) print("Vertex shader compiled successfully") fragment_shader = shaders.compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER) print("Fragment shader compiled successfully") shader_program = shaders.compileProgram(vertex_shader, fragment_shader) print("Shader program linked successfully") # 쉐이더 프로그램 검증 glValidateProgram(shader_program) validation_status = glGetProgramiv(shader_program, GL_VALIDATE_STATUS) print(f"Shader program validation status: {validation_status}") if validation_status != GL_TRUE: print(glGetProgramInfoLog(shader_program)) return shader_program except Exception as e: print(f"Error creating shader program: {str(e)}") raise def load_obj(self, filename): """OBJ 파일 로드 및 법선 벡터 계산""" vertices = [] faces = [] normals = [] try: with open(filename, 'r') as f: for line in f: if line.startswith('v '): parts = line.split() if len(parts) >= 4: # v x y z 형식인지 확인 vertices.append([float(parts[1]), float(parts[2]), float(parts[3])]) elif line.startswith('f '): face = [] parts = line.split()[1:] # 첫 번째 'f' 제외 for part in parts: # 빈 줄이나 잘못된 형식 건너뛰기 if '/' in part: vertex_index = part.split('/')[0] if vertex_index: face.append(int(vertex_index) - 1) if len(face) >= 3: # 최소 3개의 유효한 인덱스가 있는지 확인 faces.append(face) if not vertices or not faces: raise ValueError("No valid vertices or faces found in the file") vertices = np.array(vertices, dtype=np.float32) faces = np.array(faces, dtype=np.int32) # 모델 중심을 원점으로 이동 center = (vertices.max(axis=0) + vertices.min(axis=0)) / 2 vertices -= center # 모델 크기를 1로 정규화 scale = np.max(np.abs(vertices)) if scale != 0: vertices /= scale # 버텍스 법선 계산 normals = np.zeros_like(vertices) for face in faces: if len(face) >= 3: # 안전 검사 추가 v1, v2, v3 = vertices[face[0]], vertices[face[1]], vertices[face[2]] normal = np.cross(v2 - v1, v3 - v1) norm = np.linalg.norm(normal) if norm > 1e-6: normal = normal / norm normals[face] += normal # 법선 벡터 정규화 norms = np.linalg.norm(normals, axis=1) norms[norms == 0] = 1 normals = normals / norms[:, np.newaxis] print(f"Loaded model with {len(vertices)} vertices and {len(faces)} faces") return vertices, faces, normals except Exception as e: print(f"Error loading OBJ file: {str(e)}") print(f"Current working directory: {os.getcwd()}") print(f"Trying to load file: {filename}") return np.array([]), np.array([]), np.array([]) def update_matrices(self): """변환 행렬 업데이트""" # 모델 행렬 model = np.identity(4, dtype=np.float32) # 회전 적용 rx = self.rotation_matrix(self.rotation[0], [1, 0, 0]) ry = self.rotation_matrix(self.rotation[1], [0, 1, 0]) rz = self.rotation_matrix(self.rotation[2], [0, 0, 1]) model = model.dot(rx).dot(ry).dot(rz) self.model_matrix = model # 뷰 행렬 target = np.array([0.0, 0.0, 0.0]) up = np.array([0.0, 1.0, 0.0]) self.view_matrix = self.look_at(self.camera_pos, target, up) def rotation_matrix(self, angle, axis): """회전 행렬 생성""" angle = np.radians(angle) axis = np.array(axis) / np.linalg.norm(axis) a = np.cos(angle / 2.0) b, c, d = -axis * np.sin(angle / 2.0) return np.array([ [a*a+b*b-c*c-d*d, 2*(b*c-a*d), 2*(b*d+a*c), 0], [2*(b*c+a*d), a*a+c*c-b*b-d*d, 2*(c*d-a*b), 0], [2*(b*d-a*c), 2*(c*d+a*b), a*a+d*d-b*b-c*c, 0], [0, 0, 0, 1] ], dtype=np.float32) def look_at(self, eye, target, up): """뷰 행렬 생성""" eye = np.array(eye, dtype=np.float32) target = np.array(target, dtype=np.float32) up = np.array(up, dtype=np.float32) # z axis = eye - target (normalized) z = eye - target z = z / np.linalg.norm(z) # x axis = up cross z (normalized) x = np.cross(up, z) if np.sum(x) == 0: # 'up' 벡터와 'z' 벡터가 평행한 경우 처리 x = np.array([1.0, 0.0, 0.0], dtype=np.float32) else: x = x / np.linalg.norm(x) # y axis = z cross x (normalized) y = np.cross(z, x) y = y / np.linalg.norm(y) translation = np.array([ [1, 0, 0, -eye[0]], [0, 1, 0, -eye[1]], [0, 0, 1, -eye[2]], [0, 0, 0, 1] ], dtype=np.float32) rotation = np.array([ [x[0], x[1], x[2], 0], [y[0], y[1], y[2], 0], [z[0], z[1], z[2], 0], [0, 0, 0, 1] ], dtype=np.float32) return rotation.dot(translation) def setup_vertex_buffer(self): """VAO와 VBO 설정""" try: self.vao = glGenVertexArrays(1) glBindVertexArray(self.vao) # 모든 버텍스 데이터를 하나의 배열로 준비 vertices = [] for face in self.faces: for vertex_id in face: vertex = self.vertices[vertex_id] normal = self.normals[vertex_id] vertices.extend([*vertex, *normal]) # [x,y,z, nx,ny,nz] vertices = np.array(vertices, dtype=np.float32) self.vertex_count = len(self.faces) * 3 # VBO 생성 및 데이터 업로드 self.vbo = glGenBuffers(1) glBindBuffer(GL_ARRAY_BUFFER, self.vbo) glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW) # vertex positions glEnableVertexAttribArray(0) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, None) # vertex normals glEnableVertexAttribArray(1) glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(12)) # 디버그 출력 print(f"Vertex count: {self.vertex_count}") print(f"Vertex data size: {vertices.nbytes} bytes") except Exception as e: print(f"Error in setup_vertex_buffer: {str(e)}") raise def render_model(self): try: glClearColor(0.1, 0.1, 0.1, 1.0) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glEnable(GL_DEPTH_TEST) glEnable(GL_MULTISAMPLE) glEnable(GL_LINE_SMOOTH) glEnable(GL_POLYGON_SMOOTH) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST) glUseProgram(self.shader) self.update_matrices() # 조명 설정 glUniform3f(glGetUniformLocation(self.shader, "lightPos"), 3.0, 5.0, 3.0) glUniform3f(glGetUniformLocation(self.shader, "viewPos"), *self.camera_pos) glUniform3f(glGetUniformLocation(self.shader, "lightColor"), 1.0, 1.0, 0.95) glUniform3f(glGetUniformLocation(self.shader, "objectColor"), 0.9, 0.9, 0.9) # 행렬 전송 glUniformMatrix4fv(glGetUniformLocation(self.shader, "model"), 1, GL_TRUE, self.model_matrix) glUniformMatrix4fv(glGetUniformLocation(self.shader, "view"), 1, GL_TRUE, self.view_matrix) glUniformMatrix4fv(glGetUniformLocation(self.shader, "projection"), 1, GL_TRUE, self.projection_matrix) # 모델 렌더링 glBindVertexArray(self.vao) glDrawArrays(GL_TRIANGLES, 0, self.vertex_count) pygame.display.flip() except Exception as e: print(f"Render error: {str(e)}") def run(self): """메인 렌더링 루프""" while True: try: # 이벤트 처리 for event in pygame.event.get(): if event.type == pygame.QUIT: return elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: return # 마우스 입력 처리 추가 elif event.type == pygame.MOUSEBUTTONDOWN: if event.button == 1: # 왼쪽 버튼 self.dragging = True self.prev_mouse_pos = pygame.mouse.get_pos() elif event.type == pygame.MOUSEBUTTONUP: if event.button == 1: # 왼쪽 버튼 self.dragging = False elif event.type == pygame.MOUSEMOTION: if self.dragging: x, y = pygame.mouse.get_pos() if self.prev_mouse_pos: dx = x - self.prev_mouse_pos[0] dy = y - self.prev_mouse_pos[1] self.rotation[2] -= dx * self.mouse_sensitivity # z축 회전으로 변경 self.rotation[0] -= dy * self.mouse_sensitivity # x축 유지 self.prev_mouse_pos = (x, y) self.render_model() self.clock.tick(self.target_fps) except Exception as e: print(f"Error during rendering: {str(e)}") break pygame.quit() # 리소스 정리 glDeleteVertexArrays(1, [self.vao]) glDeleteBuffers(1, [self.vbo]) glDeleteProgram(self.shader) if __name__ == "__main__": import sys import os if len(sys.argv) > 1: model_path = sys.argv[1] else: model_path = "12221_Cat_v1_l3.obj" # "Tree.obj" # 파일 존재 여부 확인 if not os.path.exists(model_path): print(f"Error: File '{model_path}' does not exist") sys.exit(1) try: viewer = ModernModelViewer(model_path) viewer.run() except Exception as e: print(f"Error: {str(e)}") |
'OpenGL' 카테고리의 다른 글
Modern OpenGL 강좌 - 텍스처( texture ) 매핑하는 방법 1/2 (0) | 2023.10.19 |
---|---|
Modern OpenGL 강좌 - 사각형 그리기(렌더링, Element Buffer Object) (0) | 2023.10.19 |
Modern OpenGL 강좌 - 삼각형 그리기( 렌더링, Vertex Array Object, Vertex Buffer Object) (0) | 2023.10.19 |
Modern OpenGL 강좌 - GLFW와 GLEW 라이브러리 기본 사용방법 (0) | 2023.10.19 |
Visual Studio 2023에 OpenGL 개발 환경 만들기 ( GLFW / GLEW ) (0) | 2023.10.19 |