- vertex에 color를 지정하는 방법만드로 그럴싸한 3D물체를 표현하기에는 너무 많은 수의 vertex가 필요하다.
- Texture : 저렴한 비용인 이미지를 붙여넣는 방법으로 고품질의 랜더링 결과를 생성할 수 있음
- Texture Wrapping
- [0,1] 정규화된 범위를 벗어난 택스쳐 좌표값을 처리하는 옵션
- Texture Filtering
- 텍스쳐로 사용할 이미지의 크기가 화면보다 크거나 작을경우 처리하는 옵션- GL_NEAREST : 텍스쳐 좌표값에 가까운 픽셀값을 사용
- GL_LINEAR : 텍스쳐 좌표값 주변 4개의 픽셀값을 보간하여 사용
GL_NEAREST와 GL_LINEAR 비교
- OpenGL에서의 텍스쳐 사용 과정
- OpenGL texture object생성 및 바인딩
- warpping, filtering option 설정
- 이미지 데이터를 GPU메모리로 복사 (이미지를 사용하기 위해서는 GPU메모리상에 있어야 한다.)
- shader 프로그램이 바인딩 되었을때 사용하고자 하는 texture를 uniform형태의 프로그램에 전달
STB : Sean Barrett이라는 인디게임 제작자가 만든 라이브러리
single-file public domain library
- header file 하나에 라이브러리가 제공하고자 하는 모든 기능이 구현되어 있음
- 빌드가 매우 간편하다stb_image
- jpg, png, tga, bmp, psd, gif, hdr, pic 포멧을 지원하는 이미지 로딩 라이브러리
# 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)
#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__
#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에 저장한다.
#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 추가후 컴파일, 실행 해준다.
uint32_t m_texture;
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 작성하기
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);
#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;
}
#version 330 core
in vec4 vertexColor;
in vec2 texCoord;
out vec4 fragColor;
uniform sampler2D tex;
void main() {
fragColor = texture(tex, texCoord);
}
ShaderPtr vertShader = Shader::CreateFromFile("./shader/texture.vs", GL_VERTEX_SHADER);
ShaderPtr fragShader = Shader::CreateFromFile("./shader/texture.fs", GL_FRAGMENT_SHADER);
#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* : 소유원과 상관 없이 인스턴스에 접근
#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());
}
...
TextureUPtr m_texture;
...
...
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());
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);
...
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;
}
...
...
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로 설정
auto image = Image::Create(512, 512);
image->SetCheckImage(16, 16);
생성된 화면을 축소 하다보면 예기치 못한 무늬가 생긴다.
- 이러한 현상이 발생하는 이유는?
- 화면에 그리는 픽셀보다 텍스쳐 픽셀의 영역이 커지면 linear filter로도 충분, 문제가 생기지 않는다.
- 화면에 그리는 픽셀이 여러 텍스쳐 픽셀을 포함하게되면 문제가 생긴다. = 화면이 가지고 있는 한 픽셀에 > 표시해야할 텍스쳐 픽셀이 여러개가 있으면 생기는 문제이다.이러한 문제를 해결하기 위해 Mipmap기법이 필요하다.
화면 픽셀이 여러 택스쳐 픽셀을 포함하게 될 경우를 위해서 작은 사이즈의 이미지를 미리 준비하는 기법
- 가장 큰 이미지를 기본 레벨 0으로 한다.
- 가로세로 크기를 절반씩 줄인 이미지를 미리 계산하여 레벨을 1씩 증가시키며 저장한다.
레벨 0 512 * 512 Image | 레벨 1 256 * 256 Image | 레벨 2 128 * 128 Image | ...
- 원본 이미지 저장을 위해 필요 메모리보다 1/3만큼 더 필요하다.
- 변경된 함수
- 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); //추가된 함수
}
- GL_NEAREST_MIPMAP_NEAREST : 가장 적합한 레벨의 텍스쳐를 선택한뒤 가장 가까운 픽셀을 선택한다.
- GL_LINEAR_MIPMAP_LINEAR : 적합한 두 레벨의 텍스쳐에서 그 사이의 값을 보간한다. (주로 사용)
* OpneGL에서 동시에 사용가능한 텍스쳐는 32장이다.
TextureUPtr m_texture2;
// ... 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에 텍스처 슬롯 인덱스를 입력
- 두개의 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; }