10.4 Texturização na prática
Nesta seção, daremos continuidade ao projeto do visualizador de modelos 3D apresentado na seção 9.6.
Esta será a versão 4 do visualizador (viewer4
) e terá um shader que usa uma textura para modificar as propriedades de reflexão difusa (\(\kappa_d\)) e ambiente (\(\kappa_a\)) do material utilizado no modelo de reflexão de Blinn–Phong.
Se o objeto lido do arquivo OBJ já vier com coordenadas de textura definidas em seus vértices (mapeamento UV unwrap), o visualizador usará essas coordenadas para amostrar a textura. Entretanto, também poderemos selecionar, através da interface da ImGui, um mapeamento pré-definido: triplanar, cilíndrico ou esférico.
Nesta nova versão, o botão “Load 3D Model” será transformado em um item do menu “File”. Há também uma opção de menu para carregar uma textura como um arquivo de imagem no formato PNG ou JPEG.
O resultado ficará como a seguir:
Como o código dessa versão contém apenas mudanças incrementais em relação ao anterior, nosso foco será apenas nessas mudanças.
Baixe o código completo deste link.
Carregando texturas
Para carregar uma textura no OpenGL, primeiro devemos chamar a função glGenTextures
que cria um ou mais recursos de textura. Por exemplo, para criar apenas uma textura, podemos fazer
(1, &textureID); glGenTextures
onde textureID
é uma variável do tipo GLuint
que será preenchida com o identificador do recurso de textura criado pelo OpenGL.
Em seguida, ligamos o recurso de textura a um “alvo de textura”, que é GL_TEXTURE_2D
para texturas 2D:
(GL_TEXTURE_2D, textureID); glBindTexture
Neste momento, a textura ainda está vazia. Para definir seu conteúdo, devemos ter um mapa de bits. Podemos carregar o conteúdo do mapa de bits através da função glTexImage2D
. Por exemplo, considere o código a seguir:
(GL_TEXTURE_2D, 0, GL_RGBA, 800, 600,
glTexImage2D0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexImage2D
copia o mapa de bits contido no ponteiro pixels
, supondo que o mapa é um arranjo de \(800 \times 600\) pixels. A função considera que cada pixel é uma tupla de valores RGBA (GL_RGBA
), e que cada componente de cor é um byte sem sinal (GL_UNSIGNED_BYTE
).
Na ABCg podemos usar a função auxiliar abcg::loadOpenGLTexture
. Essa função recebe uma estrutura abcg::OpenGLTextureCreateInfo
com informações sobre a criação da textura, tal como o nome de um arquivo de imagem. O valor de retorno é o identificar da textura criada. Internamente, a função usa funções da SDL para carregar o mapa de bits, e em seguida chama as funções do OpenGL para criar o recurso de textura. Assim, para criar uma textura a partir de um arquivo imagem.png
, podemos fazer simplesmente:
= abcg::loadOpenGLTexture({.path = "imagem.png"}); textureID
Por padrão, abcg::loadOpenGLTexture
cria também o mipmap da textura e usa o filtro de minificação GL_LINEAR_MIPMAP_LINEAR
. Se quisermos que o mipmap não seja gerado, basta mudar o valor de OpenGLTextureCreateInfo::generateMipmaps
para false
:
= abcg::opengl::loadTexture({.path = "imagem.png", .generateMipmaps = false}); textureID
Para destruir a textura e liberar seus recursos, devemos chamar manualmente glDeleteTextures
. Por exemplo, o seguinte código libera a textura textureID
criada com glGenTextures
ou abcg::loadOpenGLTexture
.
(1, &textureID); glDeleteTextures
Nessa nova versão do visualizador, a classe Model
implementa a função membro Model::loadDiffuseTexture
, que carrega um arquivo de imagem e gera um identificador de recurso de textura difusa na variável m_diffuseTexture
. A função é definida como a seguir (em model.cpp
):
void Model::loadDiffuseTexture(std::string_view path) {
if (!std::filesystem::exists(path))
return;
abcg::glDeleteTextures(1, &m_diffuseTexture);
m_diffuseTexture = abcg::loadOpenGLTexture({.path = path});
}
A função recebe em path
o caminho contendo o nome do arquivo de imagem PNG ou JPEG. Se o arquivo não existir, a função simplesmente retorna (linha 72). Caso contrário, o recurso de textura anterior é liberado, e abcg::loadOpenGLTexture
é chamada para criar a nova textura.
Carregando modelos com textura
Um arquivo OBJ pode vir acompanhado de um arquivo .mtl
opcional que contém a descrição das propriedades dos materiais de cada objeto32. Por exemplo, o arquivo roman_lamp.obj
(lâmpada romana da figura 10.11) vem acompanhado do arquivo roman_lamp.mtl
que tem o seguinte conteúdo:
newmtl roman_lamp
Ns 25.0000
Ni 1.5000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.2000 0.2000 0.2000
Kd 1.0000 1.0000 1.0000
Ks 0.6000 0.6000 0.6000
Ke 0.0000 0.0000 0.0000
map_Ka maps/roman_lamp_diffuse.jpg
map_Kd maps/roman_lamp_diffuse.jpg
map_bump maps/roman_lamp_normal.jpg bump maps/roman_lamp_normal.jpg
Entre outras coisas, o arquivo contém o valor do expoente de brilho especular (Ns
) e as propriedades de reflexão ambiente (Ka
), difusa (Kd
) e especular (Ks
). Além disso, o arquivo contém o nome do mapa de textura que deve ser utilizado para modificar a reflexão ambiente (map_Ka
) e reflexão difusa (map_Kd
). Há outras propriedades, mas elas não serão utilizadas no momento.
Nossa implementação de Model::loadObj
, que utiliza funções da TinyObjLoader
, carrega automaticamente a textura difusa, se ela existir. A definição completa dessa versão atualizada de Model::loadObj
é mostrada seguir (arquivo model.cpp
).
void Model::loadObj(std::string_view path, bool standardize) {
auto const basePath{std::filesystem::path{path}.parent_path().string() + "/"};
tinyobj::ObjReaderConfig readerConfig;
readerConfig.mtl_search_path = basePath; // Path to material files
tinyobj::ObjReader reader;
if (!reader.ParseFromFile(path.data(), readerConfig)) {
if (!reader.Error().empty()) {
throw abcg::RuntimeError(
fmt::format("Failed to load model {} ({})", path, reader.Error()));
}
throw abcg::RuntimeError(fmt::format("Failed to load model {}", path));
}
if (!reader.Warning().empty()) {
fmt::print("Warning: {}\n", reader.Warning());
}
auto const &attrib{reader.GetAttrib()};
auto const &shapes{reader.GetShapes()};
auto const &materials{reader.GetMaterials()};
m_vertices.clear();
m_indices.clear();
m_hasNormals = false;
m_hasTexCoords = false;
// A key:value map with key=Vertex and value=index
std::unordered_map<Vertex, GLuint> hash{};
// Loop over shapes
for (auto const &shape : shapes) {
// Loop over indices
for (auto const offset : iter::range(shape.mesh.indices.size())) {
// Access to vertex
auto const index{shape.mesh.indices.at(offset)};
// Position
auto const startIndex{3 * index.vertex_index};
glm::vec3 position{attrib.vertices.at(startIndex + 0),
attrib.vertices.at(startIndex + 1),
attrib.vertices.at(startIndex + 2)};
// Normal
glm::vec3 normal{};
if (index.normal_index >= 0) {
m_hasNormals = true;
auto const normalStartIndex{3 * index.normal_index};
normal = {attrib.normals.at(normalStartIndex + 0),
attrib.normals.at(normalStartIndex + 1),
attrib.normals.at(normalStartIndex + 2)};
}
// Texture coordinates
glm::vec2 texCoord{};
if (index.texcoord_index >= 0) {
m_hasTexCoords = true;
auto const texCoordsStartIndex{2 * index.texcoord_index};
texCoord = {attrib.texcoords.at(texCoordsStartIndex + 0),
attrib.texcoords.at(texCoordsStartIndex + 1)};
}
Vertex const vertex{
.position = position, .normal = normal, .texCoord = texCoord};
// If hash doesn't contain this vertex
if (!hash.contains(vertex)) {
// Add this index (size of m_vertices)
hash[vertex] = m_vertices.size();
// Add this vertex
m_vertices.push_back(vertex);
}
m_indices.push_back(hash[vertex]);
}
}
// Use properties of first material, if available
if (!materials.empty()) {
auto const &mat{materials.at(0)}; // First material
m_Ka = {mat.ambient[0], mat.ambient[1], mat.ambient[2], 1};
m_Kd = {mat.diffuse[0], mat.diffuse[1], mat.diffuse[2], 1};
m_Ks = {mat.specular[0], mat.specular[1], mat.specular[2], 1};
m_shininess = mat.shininess;
if (!mat.diffuse_texname.empty())
loadDiffuseTexture(basePath + mat.diffuse_texname);
} else {
// Default values
m_Ka = {0.1f, 0.1f, 0.1f, 1.0f};
m_Kd = {0.7f, 0.7f, 0.7f, 1.0f};
m_Ks = {1.0f, 1.0f, 1.0f, 1.0f};
m_shininess = 25.0f;
}
if (standardize) {
Model::standardize();
}
if (!m_hasNormals) {
computeNormals();
}
createBuffers();
}
Note que, logo no início da função, definimos um objeto readerConfig
(linha 81) que é utilizado como argumento de ObjReader::ParseFromFile
da TinyObjLoader (linha 86) para informar o diretório onde estão os arquivos de materiais. Por padrão, esse caminho é o mesmo diretório onde está o arquivo OBJ:
void Model::loadObj(std::string_view path, bool standardize) {
auto const basePath{std::filesystem::path{path}.parent_path().string() + "/"};
tinyobj::ObjReaderConfig readerConfig;
readerConfig.mtl_search_path = basePath; // Path to material files
tinyobj::ObjReader reader;
if (!reader.ParseFromFile(path.data(), readerConfig)) {
if (!reader.Error().empty()) {
throw abcg::RuntimeError(
fmt::format("Failed to load model {} ({})", path, reader.Error()));
}
throw abcg::RuntimeError(fmt::format("Failed to load model {}", path));
}
if (!reader.Warning().empty()) {
fmt::print("Warning: {}\n", reader.Warning());
}
Nas linhas 158 a 174, as propriedades do primeiro material são utilizadas. Se não houver nenhum material, valores padrão são utilizados:
// Use properties of first material, if available
if (!materials.empty()) {
auto const &mat{materials.at(0)}; // First material
m_Ka = {mat.ambient[0], mat.ambient[1], mat.ambient[2], 1};
m_Kd = {mat.diffuse[0], mat.diffuse[1], mat.diffuse[2], 1};
m_Ks = {mat.specular[0], mat.specular[1], mat.specular[2], 1};
m_shininess = mat.shininess;
if (!mat.diffuse_texname.empty())
loadDiffuseTexture(basePath + mat.diffuse_texname);
} else {
// Default values
m_Ka = {0.1f, 0.1f, 0.1f, 1.0f};
m_Kd = {0.7f, 0.7f, 0.7f, 1.0f};
m_Ks = {1.0f, 1.0f, 1.0f, 1.0f};
m_shininess = 25.0f;
}
Durante a leitura dos atributos dos vértices, Model::loadObj
também verifica se a malha contém coordenadas de textura. Isso é feito nas linhas 134 a 141:
// Texture coordinates
glm::vec2 texCoord{};
if (index.texcoord_index >= 0) {
m_hasTexCoords = true;
auto const texCoordsStartIndex{2 * index.texcoord_index};
texCoord = {attrib.texcoords.at(texCoordsStartIndex + 0),
attrib.texcoords.at(texCoordsStartIndex + 1)};
}
Se o vértice contém coordenadas de textura (linha 136), o flag m_hasTexCoords
é definido como true
e as coordenadas são carregadas em texCoord
. Se o arquivo OBJ não tiver coordenadas de textura, texCoord
será (0, 0)
para todos os vértices.
A estrutura Vertex
é criada com o novo atributo de coordenadas de textura:
texCoord
é um novo atributo de Vertex
(um glm::vec2
), criado de forma semelhante ao modo como criamos o atributo normal
no projeto viewer2
(seção 9.5), isto é, usamos o atributo como chave de hash e carregamos seus dados no formato de um VBO de dados intercalados.
Renderizando
Em Model::render
(chamado em Window::onPaint
), incluímos a ativação da textura no pipeline. A definição completa ficará como a seguir:
void Model::render(int numTriangles) const {
abcg::glBindVertexArray(m_VAO);
abcg::glActiveTexture(GL_TEXTURE0);
abcg::glBindTexture(GL_TEXTURE_2D, m_diffuseTexture);
// Set minification and magnification parameters
abcg::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
abcg::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Set texture wrapping parameters
abcg::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
abcg::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
auto const numIndices{(numTriangles < 0) ? m_indices.size()
: numTriangles * 3};
abcg::glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_INT, nullptr);
abcg::glBindVertexArray(0);
}
Na linha 190, a função glActiveTexture
é chamada para ativar a primeira “unidade de textura” (GL_TEXTURE0
). O número de unidades de textura ativas corresponde ao número de texturas diferentes que podem ser utilizadas ao mesmo tempo em um programa de shader. Para cada estágio de shader (por exemplo, para o vertex shader, ou para o fragment shader) é possível acessar pelo menos 16 unidades de textura ao mesmo tempo. Esse número pode ser maior dependendo da implementação do driver. Se considerarmos todas os estágios de shaders disponíveis, então podemos ativar pelo menos 32 texturas ao mesmo tempo no OpenGL ES (16 para o vertex shader, mais 16 para o fragment shader), e 80 no OpenGL para desktop (16 para cada um dos 5 estágios de shaders). Como queremos manter as coisas simples, nosso visualizador por enquanto só utilizará uma unidade de textura. Nas próximas versões veremos como usar mais unidades.
Após a ativação da unidade de textura, a função glBindTexture
é chamada para ligar o identificar de textura à unidade recém ativada.
Nas linha 193 a 199, a função glTexParameteri
é chamada para configurar os filtros de textura e modos de empacotamento. Os valores aqui utilizados são os valores padrão do OpenGL.
Isso é tudo para configurar a texturização. O restante agora é feito nos shaders. O projeto viewer4
define dois novos shaders com suporte a texturas: texture.vert
e texture.frag
. Esses shaders são bem similares aos shaders do modelo de Blinn–Phong.
Para o conteúdo de assets
ficar mais organizado, a partir desta versão do visualizador, as texturas ficarão armazenadas em assets/maps
, e os shaders ficarão armazenados em assets/shaders
. Os arquivos .obj
continuam na pasta assets
, agora junto também com os arquivos .mtl
.
texture.vert
O código completo do shader é mostrado a seguir:
#version 300 es
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inTexCoord;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform mat3 normalMatrix;
uniform vec4 lightDirWorldSpace;
out vec3 fragV;
out vec3 fragL;
out vec3 fragN;
out vec2 fragTexCoord;
out vec3 fragPObj;
out vec3 fragNObj;
void main() {
vec3 P = (viewMatrix * modelMatrix * vec4(inPosition, 1.0)).xyz;
vec3 N = normalMatrix * inNormal;
vec3 L = -(viewMatrix * lightDirWorldSpace).xyz;
fragL = L;
fragV = -P;
fragN = N;
fragTexCoord = inTexCoord;
fragPObj = inPosition;
fragNObj = inNormal;
gl_Position = projMatrix * vec4(P, 1.0);
}
Esse código contém algumas poucas modificações em relação ao conteúdo de blinnphong.vert
.
Observe como agora temos o atributo de entrada inTexCoord
(linha 5) contendo as coordenadas de textura lidas do arquivo OBJ.
O vertex shader não faz nenhum processamento com as coordenadas de textura e simplesmente passa-as adiante para o fragment shader através do atributo de saída fragTexCoord
(linha 17).
O vertex shader também possui dois outros atributos adicionais de saída:
fragPObj
(linha 18) é a posição do vértice no espaço do objeto (isto é, uma cópia deinPosition
);fragNObj
(linha 19) é o vetor normal no espaço do objeto (isto é, uma cópia deinNormal
).
Esses atributos são utilizados para calcular, no fragment shader, as coordenadas de textura do mapeamento triplanar, cilíndrico ou esférico (o vetor normal só é utilizado no mapeamento triplanar). Se o mapeamento utilizado é aquele fornecido pelo arquivo OBJ, isto é, o mapeamento determinado pelas coordenada de textura de inTexCoord
, então os atributos fragPObj
e fragNObj
não são utilizados.
texture.frag
O código completo do shader é mostrado a seguir:
#version 300 es
precision mediump float;
in vec3 fragN;
in vec3 fragL;
in vec3 fragV;
in vec2 fragTexCoord;
in vec3 fragPObj;
in vec3 fragNObj;
// Light properties
uniform vec4 Ia, Id, Is;
// Material properties
uniform vec4 Ka, Kd, Ks;
uniform float shininess;
// Diffuse texture sampler
uniform sampler2D diffuseTex;
// Mapping mode
// 0: triplanar; 1: cylindrical; 2: spherical; 3: from mesh
uniform int mappingMode;
out vec4 outColor;
// Blinn-Phong reflection model
vec4 BlinnPhong(vec3 N, vec3 L, vec3 V, vec2 texCoord) {
N = normalize(N);
L = normalize(L);
// Compute lambertian term
float lambertian = max(dot(N, L), 0.0);
// Compute specular term
float specular = 0.0;
if (lambertian > 0.0) {
V = normalize(V);
vec3 H = normalize(L + V);
float angle = max(dot(H, N), 0.0);
specular = pow(angle, shininess);
}
vec4 map_Kd = texture(diffuseTex, texCoord);
vec4 map_Ka = map_Kd;
vec4 diffuseColor = map_Kd * Kd * Id * lambertian;
vec4 specularColor = Ks * Is * specular;
vec4 ambientColor = map_Ka * Ka * Ia;
return ambientColor + diffuseColor + specularColor;
}
// Planar mapping
vec2 PlanarMappingX(vec3 P) { return vec2(1.0 - P.z, P.y); }
vec2 PlanarMappingY(vec3 P) { return vec2(P.x, 1.0 - P.z); }
vec2 PlanarMappingZ(vec3 P) { return P.xy; }
#define PI 3.14159265358979323846
// Cylindrical mapping
vec2 CylindricalMapping(vec3 P) {
float longitude = atan(P.x, P.z);
float height = P.y;
float u = longitude / (2.0 * PI) + 0.5; // From [-pi, pi] to [0, 1]
float v = height - 0.5; // Base at y = -0.5
return vec2(u, v);
}
// Spherical mapping
vec2 SphericalMapping(vec3 P) {
float longitude = atan(P.x, P.z);
float latitude = asin(P.y / length(P));
float u = longitude / (2.0 * PI) + 0.5; // From [-pi, pi] to [0, 1]
float v = latitude / PI + 0.5; // From [-pi/2, pi/2] to [0, 1]
return vec2(u, v);
}
void main() {
vec4 color;
if (mappingMode == 0) {
// Triplanar mapping
// An offset to center the texture around the origin
vec3 offset = vec3(-0.5, -0.5, -0.5);
// Sample with x planar mapping
vec2 texCoord1 = PlanarMappingX(fragPObj + offset);
vec4 color1 = BlinnPhong(fragN, fragL, fragV, texCoord1);
// Sample with y planar mapping
vec2 texCoord2 = PlanarMappingY(fragPObj + offset);
vec4 color2 = BlinnPhong(fragN, fragL, fragV, texCoord2);
// Sample with z planar mapping
vec2 texCoord3 = PlanarMappingZ(fragPObj + offset);
vec4 color3 = BlinnPhong(fragN, fragL, fragV, texCoord3);
// Compute average based on normal
vec3 weight = abs(normalize(fragNObj));
color = color1 * weight.x + color2 * weight.y + color3 * weight.z;
} else {
vec2 texCoord;
if (mappingMode == 1) {
// Cylindrical mapping
texCoord = CylindricalMapping(fragPObj);
} else if (mappingMode == 2) {
// Spherical mapping
texCoord = SphericalMapping(fragPObj);
} else if (mappingMode == 3) {
// From mesh
texCoord = fragTexCoord;
}
color = BlinnPhong(fragN, fragL, fragV, texCoord);
}
if (gl_FrontFacing) {
outColor = color;
} else {
float i = (color.r + color.g + color.b) / 3.0;
outColor = vec4(i, 0, 0, 1.0);
}
}
A primeira mudança em relação ao shader blinnphong.frag
é o número de atributos de entrada, que agora inclui os atributos fragTexCoord
, fragPObj
e fragNObj
da saída do vertex shader:
in vec3 fragN;
in vec3 fragL;
in vec3 fragV;
in vec2 fragTexCoord;
in vec3 fragPObj;
in vec3 fragNObj;
Há também duas novas variáveis uniformes:
// Diffuse texture sampler
uniform sampler2D diffuseTex;
// Mapping mode
// 0: triplanar; 1: cylindrical; 2: spherical; 3: from mesh
uniform int mappingMode;
diffuseTex
é um amostrador de textura 2D (sampler2D
) que acessa a primeira unidade de textura (GL_TEXTURE0
) ativada comglActiveTexture
emModel::render
. É através desse amostrador que conseguiremos acessar os texels da textura difusa.mappingMode
é um inteiro que identifica o modo de mapeamento escolhido pelo usuário. Esse valor é determinado pelo índice da caixa de combinação “UV mapping” da ImGui. O padrão é3
para arquivos OBJ que possuem coordenadas de textura, e0
quando as coordenadas de textura não foram encontradas.
O pipeline associa diffuseTex
à unidade de textura GL_TEXTURE0
pois o valor dessa variável uniforme é definido como 0
no código em C++. Isso é feito na linha 106 de Window::onPaint
junto com a definição das outras variáveis uniformes:
auto const diffuseTexLoc{abcg::glGetUniformLocation(program, "diffuseTex")};
auto const mappingModeLoc{abcg::glGetUniformLocation(program, "mappingMode")};
// Set uniform variables that have the same value for every model
abcg::glUniformMatrix4fv(viewMatrixLoc, 1, GL_FALSE, &m_viewMatrix[0][0]);
abcg::glUniformMatrix4fv(projMatrixLoc, 1, GL_FALSE, &m_projMatrix[0][0]);
abcg::glUniform1i(diffuseTexLoc, 0);
abcg::glUniform1i(mappingModeLoc, m_mappingMode);
A definição da função BlinnPhong
é ligeiramente diferente daquela do shader blinnphong.frag
:
// Blinn-Phong reflection model
vec4 BlinnPhong(vec3 N, vec3 L, vec3 V, vec2 texCoord) {
N = normalize(N);
L = normalize(L);
// Compute lambertian term
float lambertian = max(dot(N, L), 0.0);
// Compute specular term
float specular = 0.0;
if (lambertian > 0.0) {
V = normalize(V);
vec3 H = normalize(L + V);
float angle = max(dot(H, N), 0.0);
specular = pow(angle, shininess);
}
vec4 map_Kd = texture(diffuseTex, texCoord);
vec4 map_Ka = map_Kd;
vec4 diffuseColor = map_Kd * Kd * Id * lambertian;
vec4 specularColor = Ks * Is * specular;
vec4 ambientColor = map_Ka * Ka * Ia;
return ambientColor + diffuseColor + specularColor;
}
Agora, a função tem o parâmetro adicional de coordenadas de textura texCoord
(linha 29).
Até a linha 43, o código é igual ao anterior.
A linha 45 contém o código utilizado para amostrar a textura difusa. A função texture
recebe como argumentos o amostrador de textura (diffuseTex
) e as coordenadas de textura (texCoord
). O resultado é a cor RGBA amostrada na posição dada, usando o modo de filtragem e modo de empacotamento definidos pelo código C++ antes da renderização.
Como estamos amostrando uma textura difusa, a cor da textura (map_Kd
) é multiplicada por Kd * Id * lambertian
para compor a componente difusa final (linha 48).
Também criamos uma cor ambiente map_Ka
(linha 46) que multiplica as componentes de reflexão ambiente (linha 50). Nesse caso, consideramos que map_Ka
é igual a map_Kd
, pois geralmente é esse o caso (a textura difusa é também a textura ambiente). Entretanto, é possível que um material defina uma textura diferente para a componente ambiente. Nesse caso teríamos de mudar o shader para incluir um outro amostrador específico para map_Ka
.
Além da função BlinnPhong
, o shader também define as funções de geração de coordenadas de textura usando mapeamento planar (PlanarMappingX
, PlanarMappingY
, PlanarMappingZ
), cilíndrico (CylindricalMapping
) e esférico (SphericalMapping
):
// Planar mapping
vec2 PlanarMappingX(vec3 P) { return vec2(1.0 - P.z, P.y); }
vec2 PlanarMappingY(vec3 P) { return vec2(P.x, 1.0 - P.z); }
vec2 PlanarMappingZ(vec3 P) { return P.xy; }
#define PI 3.14159265358979323846
// Cylindrical mapping
vec2 CylindricalMapping(vec3 P) {
float longitude = atan(P.x, P.z);
float height = P.y;
float u = longitude / (2.0 * PI) + 0.5; // From [-pi, pi] to [0, 1]
float v = height - 0.5; // Base at y = -0.5
return vec2(u, v);
}
// Spherical mapping
vec2 SphericalMapping(vec3 P) {
float longitude = atan(P.x, P.z);
float latitude = asin(P.y / length(P));
float u = longitude / (2.0 * PI) + 0.5; // From [-pi, pi] to [0, 1]
float v = latitude / PI + 0.5; // From [-pi/2, pi/2] to [0, 1]
return vec2(u, v);
}
Todas as funções recebem como parâmetro a posição do ponto (P
) e retornam as coordenadas de textura correspondentes ao mapeamento. O código reproduz as equações descritas na seção 10.1. A única exceção é o cálculo da componente \(v\) do mapeamento cilíndrico (linha 68), que aqui é deslocada para fazer com que o cilindro tenha base em \(-0.5\) em vez de \(0\). Assim, a textura fica centralizada verticalmente em objetos de raio unitário centralizados na origem, que é o nosso caso pois usamos a função Model::standardize
após a leitura do arquivo OBJ.
As funções de geração de coordenadas de textura são chamadas em main
de acordo com o valor de mappingMode
:
void main() {
vec4 color;
if (mappingMode == 0) {
// Triplanar mapping
// An offset to center the texture around the origin
vec3 offset = vec3(-0.5, -0.5, -0.5);
// Sample with x planar mapping
vec2 texCoord1 = PlanarMappingX(fragPObj + offset);
vec4 color1 = BlinnPhong(fragN, fragL, fragV, texCoord1);
// Sample with y planar mapping
vec2 texCoord2 = PlanarMappingY(fragPObj + offset);
vec4 color2 = BlinnPhong(fragN, fragL, fragV, texCoord2);
// Sample with z planar mapping
vec2 texCoord3 = PlanarMappingZ(fragPObj + offset);
vec4 color3 = BlinnPhong(fragN, fragL, fragV, texCoord3);
// Compute average based on normal
vec3 weight = abs(normalize(fragNObj));
color = color1 * weight.x + color2 * weight.y + color3 * weight.z;
} else {
vec2 texCoord;
if (mappingMode == 1) {
// Cylindrical mapping
texCoord = CylindricalMapping(fragPObj);
} else if (mappingMode == 2) {
// Spherical mapping
texCoord = SphericalMapping(fragPObj);
} else if (mappingMode == 3) {
// From mesh
texCoord = fragTexCoord;
}
color = BlinnPhong(fragN, fragL, fragV, texCoord);
}
if (gl_FrontFacing) {
outColor = color;
} else {
float i = (color.r + color.g + color.b) / 3.0;
outColor = vec4(i, 0, 0, 1.0);
}
}
Observe, no mapeamento triplanar (linhas 88–107), como a textura é amostrada três vezes (linhas 93–103) e as cores amostradas são combinadas em uma média ponderada pelo valor absoluto das componentes do vetor normal (linhas 105–107).
Observe que, no mapeamento triplanar, não seria necessário chamar BlinnPhong
três vezes. Afinal, a iluminação sem a textura é a mesma nas três chamadas.
O código ficaria mais eficiente se BlinnPhong
retornasse uma estrutura contendo as componentes ambiente, difusa e especular separadas. Poderíamos então criar uma outra função só para fazer a amostragem da textura difusa e compor a cor final.
Se mappingMode
é 3
, então nenhuma função de mapeamento é chamada e as coordenadas de textura utilizadas em BlinnPhong
são aquelas contidas em fragTexCoord
, pois essas são as coordenadas de textura interpoladas a partir das coordenadas definidas nos vértices.
Isso resume as modificações necessárias para habilitar a texturização. O restante do código contém modificações complementares relacionadas a conceitos que já foram abordados em projetos anteriores, como a mudança da interface da ImGui e a determinação de um ângulo e eixo de rotação inicial para o trackball virtual.
Nosso visualizador suporta apenas um objeto por arquivo OBJ e, portanto, suporta apenas um material. Por isso, todos os arquivos OBJ que utilizaremos devem ter apenas um objeto e um material.↩︎