Texture

대인공·2022년 8월 1일
0

OpenGL

목록 보기
4/8
post-thumbnail

Texture

- vertex에 color를 지정하는 방법만드로 그럴싸한 3D물체를 표현하기에는 너무 많은 수의 vertex가 필요하다.

  • Texture : 저렴한 비용인 이미지를 붙여넣는 방법으로 고품질의 랜더링 결과를 생성할 수 있음

Texture Coordinates

  • 텍스쳐 좌표
    - 각 정점 별로 이미지의 어느 지접을 입힐 것인지 별도의 좌표를 지정한다.
    - [0,1]사이로 Normalized된 좌표계
    - 항상 이미지의 좌하단이 원점(0,0)이다.

  • 텍스쳐가 삼각형에 입혀지는 과정
    - 텍스쳐 좌표가 정점의 위치와 함께 vertex attributes형태로 vertex shader에 입력된다.
    - Rasterization 과정을 거쳐서 각 픽셀별 텍스쳐 좌표값이 계산된다.
    - fragment shader에서 텍스쳐 좌표를 바탕으로 텍스쳐 이미지의 색상값을 가지고 온다.

Texture Coordinates Option

  • Texture Wrapping
    - [0,1] 정규화된 범위를 벗어난 택스쳐 좌표값을 처리하는 옵션
  • Texture Filtering
    - 텍스쳐로 사용할 이미지의 크기가 화면보다 크거나 작을경우 처리하는 옵션

- GL_NEAREST : 텍스쳐 좌표값에 가까운 픽셀값을 사용

- GL_LINEAR : 텍스쳐 좌표값 주변 4개의 픽셀값을 보간하여 사용

GL_NEAREST와 GL_LINEAR 비교

Texture In OpenGL

  • OpenGL에서의 텍스쳐 사용 과정
    - OpenGL texture object생성 및 바인딩
    - warpping, filtering option 설정
    - 이미지 데이터를 GPU메모리로 복사 (이미지를 사용하기 위해서는 GPU메모리상에 있어야 한다.)
    - shader 프로그램이 바인딩 되었을때 사용하고자 하는 texture를 uniform형태의 프로그램에 전달

1. 이미지 로딩

STB : Sean Barrett이라는 인디게임 제작자가 만든 라이브러리

  • single-file public domain library

    - header file 하나에 라이브러리가 제공하고자 하는 모든 기능이 구현되어 있음
    - 빌드가 매우 간편하다

  • stb_image

    - jpg, png, tga, bmp, psd, gif, hdr, pic 포멧을 지원하는 이미지 로딩 라이브러리

  • Dependency.cmake에 다음을 추가한다
# stb
ExternalProject_Add(
    dep_stb
    GIT_REPOSITORY "https://github.com/nothings/stb"
    GIT_TAG "master"
    GIT_SHALLOW 1
    UPDATE_COMMAND ""
    PATCH_COMMAND ""
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ""
    TEST_COMMAND ""
    INSTALL_COMMAND ${CMAKE_COMMAND} -E copy
        ${PROJECT_BINARY_DIR}/dep_stb-prefix/src/dep_stb/stb_image.h
        ${DEP_INSTALL_DIR}/include/stb/stb_image.h
    )
set(DEP_LIST ${DEP_LIST} dep_stb)
  • 이미지 로딩 , src / image.h 생성
#ifndef __IMAGE_H__
#define __IMAGE_H__

#include "common.h"

CLASS_PTR(Image)
class Image {
public:
    static ImageUPtr Load(const std::string& filepath);
    ~Image();

    const uint8_t* GetData() const { return m_data; }
    int GetWidth() const { return m_width; }
    int GetHeight() const { return m_height; }
    int GetChannelCount() const { return m_channelCount; }

private:
    Image() {};
    bool LoadWithStb(const std::string& filepath);
    int m_width { 0 };
    int m_height { 0 };
    int m_channelCount { 0 };
    uint8_t* m_data { nullptr };
};

#endif // __IMAGE_H__
  • 이미지 로딩 , src / image.cpp 생성
#include "image.h"
#include <stb/stb_image.h>

ImageUPtr Image::Load(const std::string& filepath) {
    auto image = ImageUPtr(new Image());
    if (!image->LoadWithStb(filepath))
        return nullptr;
    return std::move(image);
}

Image::~Image() {
    if (m_data) {
        stbi_image_free(m_data);
    }
}

bool Image::LoadWithStb(const std::string& filepath) {
    m_data = stbi_load(filepath.c_str(), &m_width, &m_height, &m_channelCount, 0);
    if (!m_data) {
        SPDLOG_ERROR("failed to load image: {}", filepath);
        return false;
    }
    return true;
}

사용할 이미지를 image / (image name).jpg에 저장한다.

  • 이미지 로딩 , src / context.cpp 작성
#include "image.h"

bool Context :: Init()
{
	...
    
    // return전에 붙여넣어 준다.
	auto image = Image::Load("./image/container.jpg");
	if (!image) 
  		return false;
	SPDLOG_INFO("image: {}x{}, {} channels", image->GetWidth(), image->GetHeight(), image->GetChannelCount());
    
    return true;
}

cMakeList.txt에 src / image.cpp , src / image.h 추가후 컴파일, 실행 해준다.

2. 텍스쳐 생성 및 적용

텍스쳐 생성

  • context클래스 내에 texture id를 저장하기 위한 변수 선언해준다 , src / context.h 작성
uint32_t m_texture; 
  • 텍스쳐 생성 및 설정( Context :: Init() ) , src / context.cpp 작성
bool Context :: Init()
{
	...
	//이미지 로딩 코드 작성 후 적용
    
	glGenTextures(1, &m_texture);
	glBindTexture(GL_TEXTURE_2D, m_texture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image->GetWidth(), image->GetHeight(), 0, 
                 GL_RGB, GL_UNSIGNED_BYTE, image->GetData());
    
    return true;
}

설명

  • glGenTextures(1, &texture id) : OpenGL texture object 생성
  • glBintTexture(target, texture id) : 사용하고자 하는 텍스처 바인딩
  • glTexParameteri(target, filter, Wrapping) : 텍스처 필터 / 래핑 방식 등 파라미터 설정
  • glTexImage2D(target, level, internalFormat, width, height, border, format, type, data) :
    - 바인딩된 텍스처의 크기 / 픽셀 포맷을 설정하고 GPU에 이미지 데이터를 복사
    - target: 대상이 될 바인딩 텍스처
    - level: 설정할 텍스처 레벨. 0레벨이 base.
    - internalFormat: 텍스처의 픽셀 포맷
    - width: 텍스처 / 이미지의 가로 크기
    - height: 텍스처 / 이미지의 세로 크기
    - border: 텍스처 외곽의 border 크기
    - format: 입력하는 이미지의 픽셀 포맷
    - type: 입력하는 이미지의 채널별 데이터 타입
    - data: 이미지 데이터가 기록된 메모리 주소
    • internalFormat : GL_RED, GL_RG, GL_RGBA8
    • 텍스쳐의 크기
      - 가로 / 세로의 크기가 2의 지수 형태일 때 GPU가 가증 효율적으로 처리할 수 있다.
      ( 256x256, 512x512 ... )
      - NPOT(Non-Power-Of-Two) Texture : 2의 지수 크기가 아닌 텍스쳐는 GPU의 스펙에 따라 지원을 안하는 경우도 있다.

텍스쳐 적용

  • 텍스쳐 적용을 위해 추가적으로 할일
    - vertex arrtibute에 텍스쳐 좌표를 추가하기
    - 텍스쳐를 읽어들여 픽셀값을 결정하는 shader 작성하기
  • vertex 정보 추가 [x, y, z, r, g, b, s, t] , src / context.cpp / Init() 수정
    ( s,t : Texture Coordinate)
float vertices[] = {
  0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
  0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
  -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
  -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
};

m_vertexLayout = VertexLayout::Create();
m_vertexBuffer = Buffer::CreateWithData(GL_ARRAY_BUFFER, GL_STATIC_DRAW, vertices, sizeof(float) * 32);

m_vertexLayout->SetAttrib(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, 0);
m_vertexLayout->SetAttrib(1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, sizeof(float) * 3);
m_vertexLayout->SetAttrib(2, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 8, sizeof(float) * 6);
  • shader / texture.vs 생성
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec4 vertexColor;
out vec2 texCoord;

void main() {
    gl_Position = vec4(aPos, 1.0);
    vertexColor = vec4(aColor, 1.0);
    texCoord = aTexCoord;
}
  • shader / texture.fs 생성
#version 330 core
in vec4 vertexColor;
in vec2 texCoord;
out vec4 fragColor;

uniform sampler2D tex;

void main() {
    fragColor = texture(tex, texCoord);
}
  • 새로 작성한 shader 컴파일 , src / context.cpp / Init() 수정
	ShaderPtr vertShader = Shader::CreateFromFile("./shader/texture.vs", GL_VERTEX_SHADER);
	ShaderPtr fragShader = Shader::CreateFromFile("./shader/texture.fs", GL_FRAGMENT_SHADER);

Refactoring

  • Texture클래스 선언 , src / texture.h 생성
#ifndef __TEXTURE_H__
#define __TEXTURE_H__

#include "image.h"

CLASS_PTR(Texture)
class Texture {
public:
    static TextureUPtr CreateFromImage(const Image* image);
    ~Texture();

    const uint32_t Get() const { return m_texture; }
    void Bind() const;
    void SetFilter(uint32_t minFilter, uint32_t magFilter) const;
    void SetWrap(uint32_t sWrap, uint32_t tWrap) const;
    
private:
    Texture() {}
    void CreateTexture();
    void SetTextureFromImage(const Image* image);

    uint32_t m_texture { 0 };
};

#endif // __TEXTURE_H__
  • ImagePtr이나 ImageUPtr이 아닌 Image*를 인자로 사용하는 이유
    - ImageUPtr : 이미지 인스턴스 소유권이 함수 안으로 넘어오게됨
    - ImagePtr : 이미지 인스턴스 소유권을 공유함
    - Image* : 소유원과 상관 없이 인스턴스에 접근
  • Texture클래스 구현 , src / texture.cpp 생성
#include "texture.h"

TextureUPtr Texture::CreateFromImage(const Image* image) 
{
    auto texture = TextureUPtr(new Texture());
    texture->CreateTexture();
    texture->SetTextureFromImage(image);
    return std::move(texture);
}

Texture::~Texture() 
{
    if (m_texture) {
        glDeleteTextures(1, &m_texture);
    }
}

void Texture::Bind() const 
{
    glBindTexture(GL_TEXTURE_2D, m_texture);
}

void Texture::SetFilter(uint32_t minFilter, uint32_t magFilter) const 
{
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
}

void Texture::SetWrap(uint32_t sWrap, uint32_t tWrap) const {
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, sWrap);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, tWrap);
}
    
void Texture::CreateTexture() {
    glGenTextures(1, &m_texture);
    // bind and set default filter and wrap option
    Bind();
    SetFilter(GL_LINEAR, GL_LINEAR);
    SetWrap(GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE);
}

void Texture::SetTextureFromImage(const Image* image) {
    GLenum format = GL_RGBA;
    switch (image->GetChannelCount()) 
    {
        default: break;
        case 1: format = GL_RED; break;
        case 2: format = GL_RG; break;
        case 3: format = GL_RGB; break;
    }
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->GetWidth(), image->GetHeight(), 0,
                 format, GL_UNSIGNED_BYTE, image->GetData());
}
  • 코드 수정 , src / context.cpp 작성
    ( 기존의 uint32_t을 TextureUPtr로 데이터 타입을 바꾸어 준다.)
    	...
    	TextureUPtr m_texture;
      	...
  • 코드 수정 , src / context.cpp / Init() 작성
  	...
  	auto image = Image::Load("./image/container.jpg"); // 파일 경로
	if (!image) 
  		return false;
	SPDLOG_INFO("image: {}x{}, {} channels",
  	image->GetWidth(), image->GetHeight(), image->GetChannelCount());
 
	m_texture = Texture::CreateFromImage(image.get()); //수정된 코드
    ...

기존의 코드

glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image->GetWidth(), image->GetHeight(), 0, 
             GL_RGB, GL_UNSIGNED_BYTE, image->GetData());
  • 빌드
    - cMakeList.txt에 src / texture.h, src / texture.cpp를 추가시킨후 컴파일, 실행한다.

Image 만들기

체커 이미지 만들기

  • 새 함수 선언 , src / image.h 작성
CLASS_PTR(Image)
class Image {
public:
    static ImageUPtr Load(const std::string& filepath);
    static ImageUPtr Create(int width, int height, int channelCount = 4);
    ~Image();
 
    const uint8_t* GetData() const { return m_data; }
    int GetWidth() const { return m_width; }
    int GetHeight() const { return m_height; }
    int GetChannelCount() const { return m_channelCount; }
 
    void SetCheckImage(int gridX, int gridY);
    
private:
    Image() {};
    bool LoadWithStb(const std::string& filepath);
    bool Allocate(int width, int height, int channelCount);
    int m_width { 0 };
    int m_height { 0 };
    int m_channelCount { 0 };
    uint8_t* m_data { nullptr };
};
  • 세개의 함수를 추가해준다.
    - static ImageUPtr Create(int width, int height, int channelCount = 4);
    - void SetCheckImage(int gridX, int gridY);
    - bool Allocate(int width, int height, int channelCount);
  • 이미지를 위한 메모리 할당 함수 구현 및 추가 , src / image.cpp 작성
 ...
 
ImageUPtr Image::Create(int width, int height, int channelCount) {
    auto image = ImageUPtr(new Image());
    if (!image->Allocate(width, height, channelCount))
        return nullptr;
    return std::move(image);
}

bool Image::Allocate(int width, int height, int channelCount) {
    m_width = width;
    m_height = height;
    m_channelCount = channelCount;
    m_data = (uint8_t*)malloc(m_width * m_height * m_channelCount);
    return m_data ? true : false;
}

...
  • 체커 이미지 설정 함수 구현 및 추가 , src / image.cpp 작성
...

void Image::SetCheckImage(int gridX, int gridY) {
    for (int j = 0; j < m_height; j++) {
        for (int i = 0; i < m_width; i++) {
            int pos = (j * m_width + i) * m_channelCount;
            bool even = ((i / gridX) + (j / gridY)) % 2 == 0;
            uint8_t value = even ? 255 : 0;
            for (int k = 0; k < m_channelCount; k++)
                m_data[pos + k] = value;
            if (m_channelCount > 3)
                m_data[3] = 255;
        }
    }
}  

...
  • 함수 설명
    - gridX, gridY 크기의 흑백 타일로 구성된 체커보드 이미지
    - 알파 채널은 항상 255로 설정
  • 만들어진 체커 이미지를 텍스쳐 설정에 사용 , src / context.cpp / Init()
auto image = Image::Create(512, 512);
image->SetCheckImage(16, 16);

생성된 화면을 축소 하다보면 예기치 못한 무늬가 생긴다.

  • 이러한 현상이 발생하는 이유는?
    - 화면에 그리는 픽셀보다 텍스쳐 픽셀의 영역이 커지면 linear filter로도 충분, 문제가 생기지 않는다.
    - 화면에 그리는 픽셀이 여러 텍스쳐 픽셀을 포함하게되면 문제가 생긴다. = 화면이 가지고 있는 한 픽셀에 > 표시해야할 텍스쳐 픽셀이 여러개가 있으면 생기는 문제이다.

    이러한 문제를 해결하기 위해 Mipmap기법이 필요하다.

Mipmap 기법

화면 픽셀이 여러 택스쳐 픽셀을 포함하게 될 경우를 위해서 작은 사이즈의 이미지를 미리 준비하는 기법

Mipmap

- 가장 큰 이미지를 기본 레벨 0으로 한다.
- 가로세로 크기를 절반씩 줄인 이미지를 미리 계산하여 레벨을 1씩 증가시키며 저장한다.

레벨 0 512 * 512 Image | 레벨 1 256 * 256 Image | 레벨 2 128 * 128 Image | ...

- 원본 이미지 저장을 위해 필요 메모리보다 1/3만큼 더 필요하다.

  • Texture 클래스의 구현 변경 , src / texture.cpp 작성
    • 변경된 함수
      - SetFilter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR); : 매개변수 GL_LINEAR_MIPMAP_LINEAR가 변경되었다.
      - glGenerateMipmap(GL_TEXTURE_2D); : 새로 추가되었다.
void Texture::CreateTexture()
{
    glGenTextures(1, &m_texture);
    // bind and set default filter and wrap option
    Bind();
    SetFilter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR); // 추가된 함수
    SetWrap(GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE);
}

void Texture::SetTextureFromImage(const Image* image) 
{
    GLenum format = GL_RGBA;
    switch (image->GetChannelCount()) 
    {
        default: break;
        case 1: format = GL_RED; break;
        case 2: format = GL_RG; break;
        case 3: format = GL_RGB; break;
    }
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
        image->GetWidth(), image->GetHeight(), 0,
        format, GL_UNSIGNED_BYTE,
        image->GetData());
 
    glGenerateMipmap(GL_TEXTURE_2D); //추가된 함수
}

Mipmap filter Option

- GL_NEAREST_MIPMAP_NEAREST : 가장 적합한 레벨의 텍스쳐를 선택한뒤 가장 가까운 픽셀을 선택한다.
- GL_LINEAR_MIPMAP_LINEAR : 적합한 두 레벨의 텍스쳐에서 그 사이의 값을 보간한다. (주로 사용)

여러장의 텍스쳐 사용하기

* OpneGL에서 동시에 사용가능한 텍스쳐는 32장이다.

  • Context클래스에 새로운 변수 선언 , src / context.h 작성
    	TextureUPtr m_texture2;
  • Context클래스에 새로운 텍스쳐 이미지 생성 , src / context.cpp 작성
// ... Context::Init()
	m_texture = Texture::CreateFromImage(image.get());
 
	auto image2 = Image::Load("./image/awesomeface.png"); // 새로운 이미지 파일 경로
	m_texture2 = Texture::CreateFromImage(image2.get());
    
    glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_texture->Get());
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, m_texture2->Get());

	m_program->Use();
	glUniform1i(glGetUniformLocation(m_program->Get(), "tex"), 0);
	glUniform1i(glGetUniformLocation(m_program->Get(), "tex2"), 1);

- 텍스쳐가 여러장인 경우 텍스쳐마다 슬롯이 매겨진다.

  • 텍스처를 shader program에 올바르게 제공하는 방법
    - glActiveTexture(textureSlot) : 현재 다루고자 하는 텍스처 슬롯을 선택
    - glBindTexture(textureType, textureId) : 현재 설정중인 텍스처 슬롯에 우리의 텍스처 오브젝트를 바인딩
    - glGetUniformLocation() : shader 내의 sampler2D uniform 핸들을 얻어옴
    - glUniform1i() : sampler2D uniform에 텍스처 슬롯 인덱스를 입력
  • Fragment shader 코드 수정 , src / texture.fs 작성
    • 두개의 sampler2D를 사용
    • 두 sampler2D로부터 얻어온 텍스쳐 컬러를 4:1비율로 블랜딩
#version 330 core
in vec4 vertexColor;
in vec2 texCoord;
out vec4 fragColor;

uniform sampler2D tex;
uniform sampler2D tex2;

void main() {
    fragColor = texture(tex, texCoord) * 0.8 + texture(tex2, texCoord) * 0.2;
}

위의 fragColor에 전달되는 비율로 두 이미지가 겹쳐 표현된다.

  • 보통의 이미지는 좌 상단을 원점으로 한다. 하지만 OpenGL은 좌하단을 원점으로 하기 때문에
    기존의 이미지를 사용하여 바로 로딩하면 이미지가 상하 반전이 되어 블랜딩된다.
    - 이미지의 상하를 반전시키는 함수인 ' stbi_set_flip_vertically_on_load(true); '를 추가시켜 문제를 해결할 수 있다.
bool Image::LoadWithStb(const std::string& filepath) 
{
    stbi_set_flip_vertically_on_load(true);
    m_data = stbi_load(filepath.c_str(), &m_width, &m_height, &m_channelCount, 0);
    if (!m_data) {
        SPDLOG_ERROR("failed to load image: {}", filepath);
        return false;
    }
    return true;
}
profile
이제 막 시작하는 유니티 클라이언트

0개의 댓글

관련 채용 정보