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 para criar um recurso de textura. Por exemplo, para criar apenas uma textura, podemos fazer

glGenTextures(1, &textureID);

onde textureID é uma variável do tipo GLuint que será preenchida com o identificador do recurso de textura criado pelo OpenGL.

Em seguida, devemos ligar o recurso de textura a um “alvo de textura,” que é GL_TEXTURE_2D para texturas 2D:

glBindTexture(GL_TEXTURE_2D, textureID);

Neste momento, a textura ainda está vazia. Para definir seu conteúdo, devemos ter um mapa de bits que descreve o conteúdo de uma imagem. Podemos carregar o conteúdo do mapa de bits através da função glTexImage2D. Por exemplo, considere o código a seguir:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 800, 600, 
             0, 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).

Não é necessário entrarmos em detalhes sobre os diferentes parâmetros de glTexImage2D, pois usaremos a função abcg::opengl::loadTexture da ABCg que se encarrega de fazer todo o procedimento de criação de textura a partir de um arquivo de imagem. Essa função é definida em abcg/abcg_image.cpp e tem a seguinte assinatura:

[[nodiscard]] GLuint loadTexture(std::string_view path,
                                 bool generateMipmaps = true);

Internamente, essa função usa funções da SDL para converter os dados do arquivo de imagem em um mapa de bits, e então chama as funções do OpenGL para criar o identificador de textura. Assim, para criar uma textura a partir de um arquivo imagem.png, podemos fazer simplesmente:

textureID = abcg::opengl::loadTexture("imagem.png");

Por padrão, abcg::opengl::loadTexture 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 usarmos false como segundo argumento:

textureID = abcg::opengl::loadTexture("imagem.png", false);

Para destruir a textura e liberar seus recursos, devemos chamar manualmente glDeleteTextures. Por exemplo, o seguinte código libera a textura textureID criada com abcg::opengl::loadTexture.

glDeleteTextures(1, &textureID);

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::opengl::loadTexture(path);
}

Essa 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 retorna sem fazer nada (linha 78). Caso contrário, o recurso de textura anterior é liberado e abcg::opengl::loadTexture é 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 objeto31. 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 especifica o parâmetro de brilho especular (Ns) do material 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 houver. 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) {
  const auto 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::Exception{abcg::Exception::Runtime(
          fmt::format("Failed to load model {} ({})", path, reader.Error()))};
    }
    throw abcg::Exception{
        abcg::Exception::Runtime(fmt::format("Failed to load model {}", path))};
  }

  if (!reader.Warning().empty()) {
    fmt::print("Warning: {}\n", reader.Warning());
  }

  const auto& attrib{reader.GetAttrib()};
  const auto& shapes{reader.GetShapes()};
  const auto& 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 (const auto& shape : shapes) {
    // Loop over indices
    for (const auto offset : iter::range(shape.mesh.indices.size())) {
      // Access to vertex
      const tinyobj::index_t index{shape.mesh.indices.at(offset)};

      // Vertex position
      const int startIndex{3 * index.vertex_index};
      const float vx{attrib.vertices.at(startIndex + 0)};
      const float vy{attrib.vertices.at(startIndex + 1)};
      const float vz{attrib.vertices.at(startIndex + 2)};

      // Vertex normal
      float nx{};
      float ny{};
      float nz{};
      if (index.normal_index >= 0) {
        m_hasNormals = true;
        const int normalStartIndex{3 * index.normal_index};
        nx = attrib.normals.at(normalStartIndex + 0);
        ny = attrib.normals.at(normalStartIndex + 1);
        nz = attrib.normals.at(normalStartIndex + 2);
      }

      // Vertex texture coordinates
      float tu{};
      float tv{};
      if (index.texcoord_index >= 0) {
        m_hasTexCoords = true;
        const int texCoordsStartIndex{2 * index.texcoord_index};
        tu = attrib.texcoords.at(texCoordsStartIndex + 0);
        tv = attrib.texcoords.at(texCoordsStartIndex + 1);
      }

      Vertex vertex{};
      vertex.position = {vx, vy, vz};
      vertex.normal = {nx, ny, nz};
      vertex.texCoord = {tu, tv};

      // If hash doesn't contain this vertex
      if (hash.count(vertex) == 0) {
        // 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()) {
    const auto& mat{materials.at(0)};  // First material
    m_Ka = glm::vec4(mat.ambient[0], mat.ambient[1], mat.ambient[2], 1);
    m_Kd = glm::vec4(mat.diffuse[0], mat.diffuse[1], mat.diffuse[2], 1);
    m_Ks = glm::vec4(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) {
    this->standardize();
  }

  if (!m_hasNormals) {
    computeNormals();
  }

  createBuffers();
}

Note que, logo no início da função, definimos um objeto readerConfig (linha 87) que é utilizado como argumento de ObjReader::ParseFromFile da TinyObjLoader (linha 92) 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 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::Exception{abcg::Exception::Runtime(
          fmt::format("Failed to load model {} ({})", path, reader.Error()))};
    }
    throw abcg::Exception{
        abcg::Exception::Runtime(fmt::format("Failed to load model {}", path))};
  }

Nas linhas 170 a 186, 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()) {
    const auto& mat{materials.at(0)};  // First material
    m_Ka = glm::vec4(mat.ambient[0], mat.ambient[1], mat.ambient[2], 1);
    m_Kd = glm::vec4(mat.diffuse[0], mat.diffuse[1], mat.diffuse[2], 1);
    m_Ks = glm::vec4(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 143 a 151:

      // Vertex texture coordinates
      float tu{};
      float tv{};
      if (index.texcoord_index >= 0) {
        m_hasTexCoords = true;
        const int texCoordsStartIndex{2 * index.texcoord_index};
        tu = attrib.texcoords.at(texCoordsStartIndex + 0);
        tv = attrib.texcoords.at(texCoordsStartIndex + 1);
      }

Se o vértice contém coordenadas de textura (linha 146), o flag m_hasTexCoords é definido como true e as coordenadas são carregadas em tu e tv. Se o arquivo OBJ não tiver coordenadas de textura, tu e tv serão zero para todos os vértices.

A estrutura Vertex é criada com o novo atributo de coordenadas de textura:

      Vertex vertex{};
      vertex.position = {vx, vy, vz};
      vertex.normal = {nx, ny, nz};
      vertex.texCoord = {tu, tv};

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 hashing e carregamos seus dados no formato de um VBO de dados intercalados.

Renderizando

Em Model::render (chamado em OpenGLWindow::paintGL), 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);

  const auto numIndices{(numTriangles < 0) ? m_indices.size()
                                           : numTriangles * 3};

  abcg::glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(numIndices),
                       GL_UNSIGNED_INT, nullptr);

  abcg::glBindVertexArray(0);
}

Na linha 202, a função glActiveTexture é chamada para ativar a primeira “unidade de textura” (GL_TEXTURE0). O número de unidades ativas corresponde ao número de texturas diferentes que poderão ser utilizadas ao mesmo tempo na execução do programa de shader. Podemos ativar pelo menos 80 unidades de textura no OpenGL para desktop, e 32 no OpenGL ES (usado pelo WebGL), mas o número pode ser maior dependendo da implementação do driver. 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 205 a 211, 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.

Observação

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 410

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 de inPosition);
  • fragNObj (linha 19) é o vetor normal no espaço do objeto (isto é, uma cópia de inNormal).

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. Vamos analisá-lo parte por parte.

#version 410

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

    // Sample with x planar mapping
    vec2 texCoord1 = PlanarMappingX(fragPObj);
    vec4 color1 = BlinnPhong(fragN, fragL, fragV, texCoord1);

    // Sample with y planar mapping
    vec2 texCoord2 = PlanarMappingY(fragPObj);
    vec4 color2 = BlinnPhong(fragN, fragL, fragV, texCoord2);

    // Sample with z planar mapping
    vec2 texCoord3 = PlanarMappingZ(fragPObj);
    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 com glActiveTexture em Model::render. É através desse amostrador que acessamos 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, e 0 quando as coordenadas de textura não foram encontradas.
Importante

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 108 de OpenGLWindow::paintGL junto com a definição das outras variáveis uniformes:

  const GLint diffuseTexLoc{abcg::glGetUniformLocation(program, "diffuseTex")};
  const GLint mappingModeLoc{
      abcg::glGetUniformLocation(program, "mappingMode")};

  // Set uniform variables used by every scene object
  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 27).

Até a linha 41, o código é igual ao anterior.

A linha 43 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 46).

Também criamos uma cor ambiente map_Ka (linha 44) que multiplica as componentes de reflexão ambiente (linha 48). 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 66), 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

    // Sample with x planar mapping
    vec2 texCoord1 = PlanarMappingX(fragPObj);
    vec4 color1 = BlinnPhong(fragN, fragL, fragV, texCoord1);

    // Sample with y planar mapping
    vec2 texCoord2 = PlanarMappingY(fragPObj);
    vec4 color2 = BlinnPhong(fragN, fragL, fragV, texCoord2);

    // Sample with z planar mapping
    vec2 texCoord3 = PlanarMappingZ(fragPObj);
    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 86–102), como a textura é amostrada três vezes (linhas 88–98) e então combinada em uma média ponderada pelo valor absoluto das componentes do vetor normal (linhas 101–102).

Observação

No mapeamento triplanar, não seria necessário chamar BlinnPhong três vezes, pois 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.


  1. 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.↩︎