9.5 Normais como cores

Nesta seção, implementaremos a segunda versão do visualizador de modelos 3D apresentado originalmente na seção 8.4.

As seguintes funcionalidade serão incorporadas:

  • Cálculo de normais de vértices, usando o procedimento descrito no final da seção 6.3;
  • Um novo shader que visualiza os vetores normais através de cores;
  • Uma caixa de combinação (combo box) para selecionar entre o shader do projeto anterior e o novo shader.
  • Um botão “Load 3D Model” para carregar arquivos OBJ durante a execução.

O resultado ficará como a seguir:

Configuração inicial

Faça uma cópia do projeto viewer1 da seção 8.4 e renomeie-o para viewer2.

  • Dentro de abcg/examples/viewer2/assets, crie os arquivos vazios normal.frag e normal.vert. Esses serão os arquivos do novo shader de visualização de vetores normais como cores.

  • Baixe o arquivo imfilebrowser.h do repositório AirGuanZ/imgui-filebrowser e salve-o em abcg/examples/viewer2. Esse arquivo contém a implementação do controle “imgui-filebrowser,” que é uma caixa de diálogo de seleção de arquivos usando a interface da ImGui.

Como os demais arquivos utilizados são os mesmos do projeto anterior, vamos nos concentrar apenas nas partes que serão modificadas.

model.hpp

Modifique a estrutura Vertex para que cada vértice tenha um atributo adicional de vetor normal glm::vec3 normal:

struct Vertex {
  glm::vec3 position{};
  glm::vec3 normal{};

  bool operator==(const Vertex& other) const noexcept {
    static const auto epsilon{std::numeric_limits<float>::epsilon()};
    return glm::all(glm::epsilonEqual(position, other.position, epsilon)) &&
           glm::all(glm::epsilonEqual(normal, other.normal, epsilon));
  }
};
Observação

Na implementação do operador de igualdade de Vertex, estamos agora utilizando um método mais preciso de determinar se dois vetores são iguais.

Em vez de fazer position == other.position e normal == other.normal, verificamos se cada elemento de cada tupla está a uma distância que corresponde a um epsilon de um float (diferença entre 1.0f e o próximo valor representado por um float).

Essa é uma forma mais robusta de comparar dois vértices, pois vértices equivalentes podem ter coordenadas ligeiramente diferentes por conta de erros de conversão de ponto flutuante durante a leitura do arquivo OBJ.

Dentro da classe Model, incluiremos as seguintes definições:

  bool m_hasNormals{false};

  void computeNormals();

Durante a leitura do arquivo OBJ, se o modelo já vier com vetores normais calculados, m_hasNormals será true. Caso contrário, será false e então chamaremos Model::computeNormals para calcular as normais.

model.cpp

Como modificamos a estrutura Vertex em model.hpp, precisamos modificar também a especialização de std::hash para gerar um valor de hashing que leve em conta tanto a posição do vértice quanto o vetor normal. Afinal, dois vértices na mesma posição espacial são vértices diferentes caso tenham vetores normais diferentes:

// Explicit specialization of std::hash for Vertex
namespace std {
template <>
struct hash<Vertex> {
  size_t operator()(Vertex const& vertex) const noexcept {
    const std::size_t h1{std::hash<glm::vec3>()(vertex.position)};
    const std::size_t h2{std::hash<glm::vec3>()(vertex.normal)};
    return h1 ^ h2;
  }
};
}  // namespace std

O valor de hashing é calculado como h1 ^ h2, onde ^ é o operador “ou exclusivo” bit a bit. Essa é uma forma simples de misturar dois valores para obter um valor de hashing.

Calculando as normais de vértices

O cálculo dos vetores normais dos vértices é feito em Model::computeNormals:

void Model::computeNormals() {
  // Clear previous vertex normals
  for (auto& vertex : m_vertices) {
    vertex.normal = glm::zero<glm::vec3>();
  }

  // Compute face normals
  for (const auto offset : iter::range<int>(0, m_indices.size(), 3)) {
    // Get face vertices
    Vertex& a{m_vertices.at(m_indices.at(offset + 0))};
    Vertex& b{m_vertices.at(m_indices.at(offset + 1))};
    Vertex& c{m_vertices.at(m_indices.at(offset + 2))};

    // Compute normal
    const auto edge1{b.position - a.position};
    const auto edge2{c.position - b.position};
    const glm::vec3 normal{glm::cross(edge1, edge2)};

    // Accumulate on vertices
    a.normal += normal;
    b.normal += normal;
    c.normal += normal;
  }

  // Normalize
  for (auto& vertex : m_vertices) {
    vertex.normal = glm::normalize(vertex.normal);
  }

  m_hasNormals = true;
}

Esta função é chamada logo após o carregamento do modelo, isto é, quando m_vertices e m_indices já contêm a geometria indexada do modelo, mas antes da criação do VBO/EBO.

Antes de calcular os vetores normais, todos os vetores normais em m_vertices são definidos como \((0,0,0)\):

  // Clear previous vertex normals
  for (auto& vertex : m_vertices) {
    vertex.normal = glm::zero<glm::vec3>();
  }

Para cada triângulo \(\triangle ABC\) da malha, o vetor normal é calculado como

\[\mathbf{n}=(B-A) \times (C-B),\]

    // Compute normal
    const auto edge1{b.position - a.position};
    const auto edge2{c.position - b.position};
    const glm::vec3 normal{glm::cross(edge1, edge2)};

O resultado é acumulado nos vértices:

    // Accumulate on vertices
    a.normal += normal;
    b.normal += normal;
    c.normal += normal;

Lembre-se que, como estamos usando geometria indexada, um mesmo vértice pode ser compartilhado por vários triângulos. Então, ao final do laço que itera todos os triângulos, o atributo normal de cada vértice será a soma dos vetores normais dos triângulos que usam tal vértice. Por exemplo, se um vértice é compartilhado por 5 triângulos, então seu atributo normal será a soma dos vetores normais desses 5 triângulos.

Para finalizar, basta normalizar o atributo normal de cada vértice. O resultado será um vetor unitário que corresponde à média dos vetores normais dos triângulos adjacentes:

  // Normalize
  for (auto& vertex : m_vertices) {
    vertex.normal = glm::normalize(vertex.normal);
  }

Leitura do arquivo OBJ

A função Model::loadObj é modificada para ler vetores normais caso estejam presentes (linhas 116 a 126 do código abaixo). Se os vetores normais não são encontrados, chamamos Model::computeNormals (linhas 148 a 150):

void Model::loadObj(std::string_view path, bool standardize) {
  tinyobj::ObjReader reader;

  if (!reader.ParseFromFile(path.data())) {
    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()};

  m_vertices.clear();
  m_indices.clear();

  m_hasNormals = 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 vertex{};
      vertex.position = {vx, vy, vz};
      vertex.normal = {nx, ny, nz};

      // 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]);
    }
  }

  if (standardize) {
    this->standardize();
  }

  if (!m_hasNormals) {
    computeNormals();
  }

  createBuffers();
}

Mapeamento do VBO com as normais

Uma vez que cada vértice tem agora dois atributos (posição e vetor normal), precisamos configurar como o VBO será mapeado para os atributos de entrada do vertex shader que chamaremos de inPosition e inNormal. Isso é feito em Model::setupVAO:

void Model::setupVAO(GLuint program) {
  // Release previous VAO
  abcg::glDeleteVertexArrays(1, &m_VAO);

  // Create VAO
  abcg::glGenVertexArrays(1, &m_VAO);
  abcg::glBindVertexArray(m_VAO);

  // Bind EBO and VBO
  abcg::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);

  // Bind vertex attributes
  const GLint positionAttribute{
      abcg::glGetAttribLocation(program, "inPosition")};
  if (positionAttribute >= 0) {
    abcg::glEnableVertexAttribArray(positionAttribute);
    abcg::glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE,
                                sizeof(Vertex), nullptr);
  }

  const GLint normalAttribute{abcg::glGetAttribLocation(program, "inNormal")};
  if (normalAttribute >= 0) {
    abcg::glEnableVertexAttribArray(normalAttribute);
    GLsizei offset{sizeof(glm::vec3)};
    abcg::glVertexAttribPointer(normalAttribute, 3, GL_FLOAT, GL_FALSE,
                                sizeof(Vertex),
                                reinterpret_cast<void*>(offset));
  }

  // End of binding
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
  abcg::glBindVertexArray(0);
}

Nosso VBO usa dados intercalados no formato

\[\left[ [x\;\; y\;\; z]_1\;\; [n_x\;\; n_y\;\; n_z]_1\;\; [x\;\; y\;\; z]_2\;\; [n_x\;\; n_y\;\; n_z]_2\;\; \cdots\;\; [x\;\; y\;\; z]_m\;\; [n_x\;\; n_y\;\; n_z]_m \right],\]

onde \([x\;\; y\;\; z]_i\) e \([n_x\;\; n_y\;\; n_z]_i\) são a posição e vetor normal do \(i\)-ésimo vértice do arranjo.

Logo, o mapeamento para inNormal precisa usar um deslocamento (offset) de sizeof(glm::vec3), que é o que fazemos nas linhas 191 a 194.

openglwindow.hpp

Na versão anterior deste visualizador (projeto viewer1) só era possível usar um único programa de shader, identificado por m_program. Em particular, esse programa de shader correspondia ao par de shaders depth.vert e depth.frag. Nesta aplicação, o usuário poderá escolher entre dois programas de shaders. Para permitir isso, a variável m_program definida na classe OpenGLWindow será substituída por um conjunto de variáveis:

std::vector<const char*> m_shaderNames{"normal", "depth"};
std::vector<GLuint> m_programs;
int m_currentProgramIndex{-1};

onde

  • m_shaderNames é um arranjo de nomes dos pares de shaders contidos no subdiretório assets. Neste projeto usaremos os shaders normal e depth. Vamos supor que cada nome corresponde a dois arquivos, um com extensão .vert (vertex shader) e outro com extensão .frag (fragment shader).

  • m_programs é um arranjo de identificadores dos programas de shader compilados, um para cada elemento de m_shaderNames;

  • m_currentProgramIndex é um índice para m_programs que indica qual é o programa atualmente selecionado pelo usuário.

    Sempre que um novo programa for selecionado usando a caixa de combinação da ImGui, Model::SetupVAO será chamada para o novo programa, pois o VAO é modificado de acordo com os shaders.

    Por padrão, o valor de m_currentProgramIndex é -1. Na primeira chamada a OpenGLWindow::paintUI, esse valor é modificado para 0, que é o índice padrão da caixa de combinação de seleção de shaders. Como isso equivale a uma mudança de programa, a função Model::SetupVAO é chamada (poderíamos chamar Model::SetupVAO em OpenGLWindow::initializeGL, mas assim evitamos duplicação de código).

openglwindow.cpp

No início do arquivo precisamos incluir alguns cabeçalhos a mais:

#include <glm/gtc/matrix_inverse.hpp>
#include "imfilebrowser.h"

initializeGL

Em OpenGLWindow::initializeGL, compilamos e ligamos todos os shaders mencionados em m_shaderNames, supondo que o arquivo .vert tem o mesmo nome do arquivo .frag:

void OpenGLWindow::initializeGL() {
  abcg::glClearColor(0, 0, 0, 1);
  abcg::glEnable(GL_DEPTH_TEST);

  // Create programs
  for (const auto& name : m_shaderNames) {
    const auto program{createProgramFromFile(getAssetsPath() + name + ".vert",
                                             getAssetsPath() + name + ".frag")};
    m_programs.push_back(program);
  }

  // Load model
  m_model.loadObj(getAssetsPath() + "bunny.obj");
  m_trianglesToDraw = m_model.getNumTriangles();
}

Observe que continuamos carregando bunny.obj como modelo 3D inicial. A função Model::loadObj será chamada novamente sempre que o usuário selecionar um novo arquivo usando o botão “Load 3D Model” que definiremos mais adiante em OpenGLWindow::paintUI.

paintGL

A definição de OpenGLWindow::paintGL ficará assim:

void OpenGLWindow::paintGL() {
  update();

  abcg::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  abcg::glViewport(0, 0, m_viewportWidth, m_viewportHeight);

  // Use currently selected program
  const auto program{m_programs.at(m_currentProgramIndex)};
  abcg::glUseProgram(program);

  // Get location of uniform variables
  const GLint viewMatrixLoc{abcg::glGetUniformLocation(program, "viewMatrix")};
  const GLint projMatrixLoc{abcg::glGetUniformLocation(program, "projMatrix")};
  const GLint modelMatrixLoc{
      abcg::glGetUniformLocation(program, "modelMatrix")};
  const GLint normalMatrixLoc{
      abcg::glGetUniformLocation(program, "normalMatrix")};

  // 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]);

  // Set uniform variables of the current object
  abcg::glUniformMatrix4fv(modelMatrixLoc, 1, GL_FALSE, &m_modelMatrix[0][0]);

  const auto modelViewMatrix{glm::mat3(m_viewMatrix * m_modelMatrix)};
  const glm::mat3 normalMatrix{glm::inverseTranspose(modelViewMatrix)};
  abcg::glUniformMatrix3fv(normalMatrixLoc, 1, GL_FALSE, &normalMatrix[0][0]);

  m_model.render(m_trianglesToDraw);

  abcg::glUseProgram(0);
}

Observe que, além de enviar para o shader as matrizes \(4 \times 4\) de visão (viewMatrix), projeção (projMatrix) e modelo (modelMatrix), também enviamos uma matriz \(3 \times 3\) chamada de normalMatrix.

Nas linhas 73 a 75, a matriz normalMatrix é calculada como a transposta da inversa de \(M_{\textrm{view}}M_{\textrm{model}}\), isto é:

\[ M_{\textrm{normal}}=\left((M_{\textrm{view}}M_{\textrm{model}})^{-1}\right)^{T}. \]

\(M_{\textrm{normal}}\) é a matriz que transforma um vetor normal do espaço do mundo para um vetor normal do espaço da câmera. Existe um motivo especial para usar essa matriz no lugar de \(M_{\textrm{view}}M_{\textrm{model}}\) para transformar vetores normais. Isso será explicado logo mais no final desta seção.

paintUI

No início de OpenGLWindow::paintUI, inicializamos o objeto que define a caixa de diálogo do elemento de interface “imgui-filebrowser”:

  static ImGui::FileBrowser fileDialog;
  fileDialog.SetTitle("Load 3D Model");
  fileDialog.SetTypeFilters({".obj"});
  fileDialog.SetWindowSize(m_viewportWidth * 0.8f, m_viewportHeight * 0.8f);
  fileDialog.SetPwd(getAssetsPath());

Com essa configuração, o navegador de arquivos mostrará arquivos com extensão .obj no subdiretório assets, e a caixa de diálogo ocupará 80% do tamanho do viewport.

A caixa de seleção de shaders é implementada com o seguinte trecho de código:

    // Shader combo box
    {
      static std::size_t currentIndex{};

      ImGui::PushItemWidth(120);
      if (ImGui::BeginCombo("Shader", m_shaderNames.at(currentIndex))) {
        for (const auto index : iter::range(m_shaderNames.size())) {
          const bool isSelected{currentIndex == index};
          if (ImGui::Selectable(m_shaderNames.at(index), isSelected))
            currentIndex = index;
          if (isSelected) ImGui::SetItemDefaultFocus();
        }
        ImGui::EndCombo();
      }
      ImGui::PopItemWidth();

      // Set up VAO if shader program has changed
      if (static_cast<int>(currentIndex) != m_currentProgramIndex) {
        m_currentProgramIndex = static_cast<int>(currentIndex);
        m_model.setupVAO(m_programs.at(m_currentProgramIndex));
      }
    }

Veja que usamos os nomes de m_shaderNames como elementos da caixa de combinação. Observe também que a função Model::setupVAO é chamada sempre que um novo shader é selecionado (linhas 196 a 200).

O botão “Load 3D Model” é criado com o código a seguir:

    if (ImGui::Button("Load 3D Model...", ImVec2(-1, -1))) {
      fileDialog.Open();
    }

Quando o botão é pressionado, chamamos fileDialog.Open para abrir a caixa de diálogo de seleção de arquivos OBJ.

No fim de OpenGLWindow::paintUI, colocamos o código responsável pela renderização da caixa de diálogo e pela leitura do novo modelo 3D.

  fileDialog.Display();

  if (fileDialog.HasSelected()) {
    // Load model
    m_model.loadObj(fileDialog.GetSelected().string());
    m_model.setupVAO(m_programs.at(m_currentProgramIndex));
    m_trianglesToDraw = m_model.getNumTriangles();
    fileDialog.ClearSelected();
  }

Se algum arquivo foi selecionado na caixa de diálogo (linha 212), chamamos Model::loadObj para carregar o arquivo, e então Model::setupVAO para configurar o VAO. Por fim, atualizamos a variável m_trianglesToDraw, utilizada para controlar o número de triângulos processados por glDrawElements.

Isso é tudo em relação às mudanças do código em C++. Vamos agora à definição dos shaders normal.vert e normal.frag usando GLSL.

normal.frag

Vamos começar pelo conteúdo de normal.frag, que é bem simples:

#version 410

in vec4 fragColor;
out vec4 outColor;

void main() { outColor = fragColor; }

A cor de entrada é simplesmente copiada para a cor de saída, como já fizemos em vários outros projetos. Assim, se cada vértice do triângulo tiver uma cor diferente, fragColor será uma cor interpolada linearmente a partir dos vértices. O resultado será um gradiente de cor.

normal.vert

Este shader converte as coordenadas do vetor normal de vértice em uma cor RGB.

Em muitos casos, é mais fácil visualizar a direção de vetores normais através de cores do que através do desenho de setas que saem dos vértices. Se o modelo tiver muitos vértices, as setas cobrirão todo o objeto e não conseguiremos distinguir um vetor de outro. Isso é ainda mais importante se quisermos observar os vetores normais calculados para cada fragmento.

As coordenadas \((x, y, z)\) de um vetor unitário estão no intervalo \([-1,1]\). Uma cor RGB tem componentes \((r, g, b)\) no intervalo \([0,1]\). Logo, a conversão das coordenadas em cores é um simples mapeamento linear de \([-1,1]\) para \([0,1]\):

\[ r = \frac{x+1}{2}, \qquad g = \frac{y+1}{2}, \qquad b = \frac{z+1}{2}. \]

Assim, se o vetor normal tiver coordenadas \((1,0,0)\) (direção do eixo \(x\) positivo), o resultado será um tom próximo ao vermelho \((1, 0.5, 0.5)\). Se o vetor normal tiver coordenadas \((0,1,0)\) (direção de \(y\) positivo), o resultado será um tom próximo ao verde \((0.5, 1, 0.5)\). Se tiver coordenadas \((0,0,1)\) (direção de \(z\) positivo), terá um tom próximo ao azul \((0.5, 0.5, 1)\). Essa convenção de cores é a mesma que temos utilizado nas ilustrações dos eixos principais em todas as figuras. A figura 9.20 mostra as cores correspondentes para as direções \(\pm x\), \(\pm y\) e \(\pm z\).

Cores correspondentes para as direções positivas e negativas dos eixos principais.

Figura 9.20: Cores correspondentes para as direções positivas e negativas dos eixos principais.

O código ficará como a seguir:

#version 410

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform mat3 normalMatrix;

out vec4 fragColor;

void main() {
  mat4 MVP = projMatrix * viewMatrix * modelMatrix;

  gl_Position = MVP * vec4(inPosition, 1.0);

  vec3 N = inNormal;  // Object space
  // vec3 N = normalMatrix * inNormal; // Eye space

  // Convert from [-1,1] to [0,1]
  fragColor = vec4((N + 1.0) / 2.0, 1.0);
}

Temos dois atributos de entrada: inPosition (linha 3) e inNormal (linha 4), que correspondem à posição do vértice e seu vetor normal unitário. Vamos supor que ambos estão no espaço do objeto.

Temos apenas um atributo de saída (linha 11), que é a cor que iremos calcular com base no vetor normal.

Na linha 14, criamos uma matriz MVP que é a composição das matrizes de modelo, visão e projeção.

Na linha 16, multiplicamos MVP pela posição do vértice, de modo a converter a posição em coordenadas do espaço do objeto para coordenadas do espaço de recorte. O resultado é atribuído a gl_Position.

Na linha 18, criamos um vetor N que é uma cópia de inNormal.

A conversão de XYZ para RGBA é feita na linha 22 (a componente A é sempre 1).

Dica

Da forma como está, fragColor é a cor que representa um vetor normal unitário no espaço do objeto.

Experimente comentar a linha 18 e, no lugar, usar o código que está comentado na linha 19. Isso fará com que fragColor represente um vetor normal unitário no espaço da câmera.

Tente identificar visualmente a diferença entre vetores normais no espaço do objeto e no espaço da câmera. Há alguma cor que aparece para N em um espaço e não aparece para N em outro espaço? Por quê?

Convertendo normais para o espaço da câmera

Se usarmos a linha 19 no lugar da linha 18, N será transformado por normalMatrix para converter o vetor normal do espaço do objeto para o espaço da câmera. Em muitos casos, isso é o mesmo que fazer

vec4 N = viewMatrix * modelMatrix * vec4(inNormal, 0);

O problema é que transformar um vetor normal pela matriz de modelo e visão nem sempre resulta em um vetor normal à superfície. Esse é o caso quando a matriz de modelo (ou de visão) contém uma escala não uniforme. Veja, na figura 9.21, como uma escala não uniforme faz com que a maioria dos vetores normais não sejam mais perpendiculares às faces (que nesse caso são lados) do objeto.

A escala não uniforme pode alterar o ângulo entre o vetor normal e um vetor tangente à superfície.

Figura 9.21: A escala não uniforme pode alterar o ângulo entre o vetor normal e um vetor tangente à superfície.

Suponha que os vetores \(\mathbf{n}\) e \(\mathbf{t}\) da figura 9.21 sejam matrizes coluna

\[\mathbf{n}=\begin{bmatrix}n_x\\n_y\\n_z\end{bmatrix},\qquad \mathbf{t}=\begin{bmatrix}t_x\\t_y\\t_z\end{bmatrix}.\]

Os vetores são perpendiculares. Logo,

\[\mathbf{n} \cdot \mathbf{t} = 0.\]

Também podemos escrever na notação de multiplicação entre matrizes:

\[ \mathbf{n}^T\mathbf{t} = 0. \]

Seja \(\mathbf{M}\) a matriz modelo-visão:

\[ \mathbf{M}=\mathbf{M}_{\textrm{view}}\mathbf{M}_{\textrm{model}}. \] Já sabemos que nem sempre \(\mathbf{M}\mathbf{n} \cdot \mathbf{M}\mathbf{t}=0\). Acabamos de ver um contraexemplo na figura 9.21. Entretanto, suponha que existe uma matriz \(\mathbf{W}\) tal que

\[(\mathbf{W}\mathbf{n}) \cdot (\mathbf{M}\mathbf{t}) = 0.\] Podemos reescrever a expressão como

\[ \begin{align} (\mathbf{W}\mathbf{n})^T(\mathbf{M}\mathbf{t}) = 0,\\ (\mathbf{n}^T\mathbf{W}^T)(\mathbf{M}\mathbf{t}) = 0,\\ \mathbf{n}^T(\mathbf{W}^T\mathbf{M})\mathbf{t} = 0.\\ \end{align} \] Nesta última expressão, observe que, se o termo entre parênteses resultar em uma matriz identidade, isto é, se

\[(\mathbf{W}^T\mathbf{M})=\mathbf{I},\]

então

\[\mathbf{n}^T\mathbf{t} = 0,\]

que é o que declaramos no início (os vetores são perpendiculares). Podemos isolar \(\mathbf{W}\) para obter a forma final da matriz que devemos usar para transformar o vetor normal:

\[ \begin{align} \mathbf{W}^T\mathbf{M}&=\mathbf{I},\\ \mathbf{W}^T&=\mathbf{M}^{-1},\\ \mathbf{W}&=(\mathbf{M}^{-1})^T.\\ \end{align} \] Isso mostra que a matriz que devemos utilizar para converter um vetor normal do espaço do objeto para o espaço da câmera é a transposta da inversa da matriz modelo-visão:

\[ \mathbf{M}_{\textrm{normal}}=(\mathbf{M_{\textrm{modelview}}}^{-1})^T. \]

Baixe o código completo do projeto usando este link.