본문 바로가기
-----ETC-----/C++ 게임 개발 시리즈

[C++ 게임 개발 시리즈] Day 23: 3D 모델링과 텍스처링

by cogito21_cpp 2024. 8. 1.
반응형

3D 모델링과 텍스처링

3D 모델링과 텍스처링은 게임에서 현실감 있는 그래픽을 표현하기 위해 필수적인 기술입니다. 3D 모델링은 객체의 형태를 정의하고, 텍스처링은 객체의 표면에 이미지를 입히는 작업입니다. 오늘은 간단한 3D 모델을 로드하고, 텍스처를 적용하는 방법을 학습하겠습니다.

3D 모델 파일 형식

3D 모델 파일 형식은 다양한 종류가 있습니다. 오늘은 간단하고 널리 사용되는 Wavefront OBJ 파일 형식을 사용하겠습니다. OBJ 파일은 3D 모델의 정점, 면, 텍스처 좌표 등을 텍스트 형식으로 저장합니다.

OBJ 파일 로딩

OBJ 파일을 로드하기 위해 간단한 로더를 작성합니다. 이 로더는 정점, 텍스처 좌표, 면 정보를 읽어들여 OpenGL에서 사용할 수 있는 형태로 변환합니다.

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <string>

struct Vertex {
    float x, y, z;
};

struct TexCoord {
    float u, v;
};

struct Face {
    unsigned int vertexIndex[3];
    unsigned int texCoordIndex[3];
};

class OBJLoader {
public:
    bool loadOBJ(const std::string& path, std::vector<Vertex>& outVertices, std::vector<TexCoord>& outTexCoords, std::vector<Face>& outFaces) {
        std::ifstream file(path);
        if (!file.is_open()) {
            std::cerr << "Failed to open OBJ file: " << path << std::endl;
            return false;
        }

        std::vector<Vertex> tempVertices;
        std::vector<TexCoord> tempTexCoords;
        std::vector<Face> tempFaces;

        std::string line;
        while (std::getline(file, line)) {
            std::istringstream lineStream(line);
            std::string type;
            lineStream >> type;

            if (type == "v") {
                Vertex vertex;
                lineStream >> vertex.x >> vertex.y >> vertex.z;
                tempVertices.push_back(vertex);
            } else if (type == "vt") {
                TexCoord texCoord;
                lineStream >> texCoord.u >> texCoord.v;
                tempTexCoords.push_back(texCoord);
            } else if (type == "f") {
                Face face;
                for (int i = 0; i < 3; ++i) {
                    std::string vertexData;
                    lineStream >> vertexData;
                    std::replace(vertexData.begin(), vertexData.end(), '/', ' ');
                    std::istringstream vertexStream(vertexData);
                    vertexStream >> face.vertexIndex[i] >> face.texCoordIndex[i];
                }
                tempFaces.push_back(face);
            }
        }

        outVertices = tempVertices;
        outTexCoords = tempTexCoords;
        outFaces = tempFaces;

        return true;
    }
};

텍스처 로딩

텍스처 이미지를 로드하고 OpenGL에서 사용할 수 있도록 설정합니다. SOIL(Simplified OpenGL Image Library) 라이브러리를 사용하여 텍스처를 로드할 수 있습니다.

SOIL 설치 및 설정

  1. SOIL 설치:
    • SOIL GitHub 페이지에서 SOIL을 다운로드하거나 패키지 관리자를 통해 설치합니다.
  2. SOIL을 사용하여 텍스처 로딩:
#include <SOIL/SOIL.h>

GLuint loadTexture(const std::string& path) {
    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);
    int width, height;
    unsigned char* image = SOIL_load_image(path.c_str(), &width, &height, 0, SOIL_LOAD_RGB);
    if (image == nullptr) {
        std::cerr << "Failed to load texture: " << path << std::endl;
        return 0;
    }
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    glGenerateMipmap(GL_TEXTURE_2D);
    SOIL_free_image_data(image);
    glBindTexture(GL_TEXTURE_2D, 0);
    return textureID;
}

3D 모델 렌더링

OBJ 파일과 텍스처를 로드한 후, OpenGL을 사용하여 3D 모델을 렌더링합니다.

전체 코드

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <string>
#include <SOIL/SOIL.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// 정점 셰이더 소스 코드
const char* vertexShaderSource = R"glsl(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}
)glsl";

// 프래그먼트 셰이더 소스 코드
const char* fragmentShaderSource = R"glsl(
#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D texture1;

void main()
{
    FragColor = texture(texture1, TexCoord);
}
)glsl";

// 셰이더 컴파일 함수
GLuint compileShader(GLenum type, const char* source) {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &source, NULL);
    glCompileShader(shader);

    GLint success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetShaderInfoLog(shader, 512, NULL, infoLog);
        std::cerr << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    return shader;
}

// 셰이더 프로그램 생성 함수
GLuint createShaderProgram() {
    GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
    GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);

    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    GLint success;
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cerr << "ERROR::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return shaderProgram;
}

// OBJ 로더 클래스
class OBJLoader {
public:
    bool loadOBJ(const std::string& path, std::vector<Vertex>& outVertices, std::vector<TexCoord>& outTexCoords, std::vector<Face>& outFaces) {
        std::ifstream file(path);
        if (!file.is_open()) {
            std::cerr << "Failed to open OBJ file: " << path << std::endl;
            return false;
        }

        std::vector<Vertex> tempVertices;
        std::vector<TexCoord> tempTexCoords;
        std::vector<Face> tempFaces;

        std::string line;
        while (std::getline(file, line)) {
            std::istringstream lineStream(line);
            std::string type;
            lineStream >> type;

            if (type == "v") {
                Vertex vertex;
                lineStream >> vertex.x >> vertex.y >> vertex.z;
                tempVertices.push_back(vertex);
            } else if (type == "vt") {
                TexCoord texCoord;
                lineStream >> texCoord.u >> texCoord.v;
                tempTexCoords.push_back(texCoord);
            } else if (type == "f") {
                Face face;
                for (int i = 0; i < 3; ++i) {
                    std::string vertexData;
                    lineStream >> vertexData;
                    std::replace(vertexData.begin(), vertexData.end(), '/', ' ');
                    std::istringstream vertexStream(vertexData);
                    vertexStream >> face.vertexIndex[i] >> face.texCoordIndex[i];
                }
                tempFaces.push_back(face);
            }
        }

        outVertices = tempVertices;
        outTexCoords = tempTexCoords;
        outFaces = tempFaces;

        return true;
    }
};

// 텍스처 로딩 함수
GLuint loadTexture(const std::string& path) {
    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);


    int width, height;
    unsigned char* image = SOIL_load_image(path.c_str(), &width, &height, 0, SOIL_LOAD_RGB);
    if (image == nullptr) {
        std::cerr << "Failed to load texture: " << path << std::endl;
        return 0;
    }
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    glGenerateMipmap(GL_TEXTURE_2D);
    SOIL_free_image_data(image);
    glBindTexture(GL_TEXTURE_2D, 0);
    return textureID;
}

int main() {
    // GLFW 초기화
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return -1;
    }

    // OpenGL 버전 설정 (3.3)
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // 창 생성
    GLFWwindow* window = glfwCreateWindow(800, 600, "3D Modeling and Texturing", NULL, NULL);
    if (!window) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }

    // OpenGL 컨텍스트 설정
    glfwMakeContextCurrent(window);

    // GLEW 초기화
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cerr << "Failed to initialize GLEW" << std::endl;
        return -1;
    }

    // 셰이더 프로그램 생성
    GLuint shaderProgram = createShaderProgram();

    // OBJ 파일 로드
    std::vector<Vertex> vertices;
    std::vector<TexCoord> texCoords;
    std::vector<Face> faces;
    OBJLoader objLoader;
    if (!objLoader.loadOBJ("path/to/your/model.obj", vertices, texCoords, faces)) {
        return -1;
    }

    // 텍스처 로드
    GLuint texture = loadTexture("path/to/your/texture.png");

    // 정점 데이터 설정
    std::vector<float> vertexData;
    for (const auto& face : faces) {
        for (int i = 0; i < 3; ++i) {
            Vertex vertex = vertices[face.vertexIndex[i] - 1];
            TexCoord texCoord = texCoords[face.texCoordIndex[i] - 1];
            vertexData.push_back(vertex.x);
            vertexData.push_back(vertex.y);
            vertexData.push_back(vertex.z);
            vertexData.push_back(texCoord.u);
            vertexData.push_back(texCoord.v);
        }
    }

    GLuint VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // OpenGL 상태 설정
    glEnable(GL_DEPTH_TEST);

    // 메인 루프
    while (!glfwWindowShouldClose(window)) {
        // 입력 처리
        if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
            glfwSetWindowShouldClose(window, true);
        }

        // 화면 지우기
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // 셰이더 프로그램 사용
        glUseProgram(shaderProgram);

        // 변환 행렬 설정
        glm::mat4 model = glm::mat4(1.0f);
        glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -3.0f));
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);

        GLint modelLoc = glGetUniformLocation(shaderProgram, "model");
        GLint viewLoc = glGetUniformLocation(shaderProgram, "view");
        GLint projectionLoc = glGetUniformLocation(shaderProgram, "projection");

        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

        // 텍스처 바인딩
        glBindTexture(GL_TEXTURE_2D, texture);

        // 객체 그리기
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, vertexData.size() / 5);
        glBindVertexArray(0);

        // 버퍼 교환
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 자원 해제
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    glfwTerminate();
    return 0;
}

결론

오늘은 3D 모델링과 텍스처링의 기본 개념을 학습하고, OBJ 파일과 텍스처 이미지를 로드하여 OpenGL을 사용해 3D 모델을 렌더링하는 방법을 배웠습니다. 이를 통해 3D 객체에 텍스처를 적용하여 더욱 현실감 있는 그래픽을 구현할 수 있습니다. 질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "Day 24: 3D 게임 프로젝트 시작 (1)"에 대해 학습하겠습니다.

반응형