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{};

  friend bool operator==(Vertex const &, Vertex const &) = default;
};

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 hash 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
template <> struct std::hash<Vertex> {
  size_t operator()(Vertex const &vertex) const noexcept {
    auto const h1{std::hash<glm::vec3>()(vertex.position)};
    auto const h2{std::hash<glm::vec3>()(vertex.normal)};
    return abcg::hashCombine(h1, h2);
  }
};

Os valores de hash h1 e h2 são combinados com abcg::hashCombine, que usa um algoritmo de combinação de valores de hash inspirado na função boost::hash_combine da biblioteca Boost. Uma forma mais simples seria fazer h1^h2, onde ^ é o operador “ou exclusivo” bit a bit, mas isso pode resultar em muitas colisões, uma vez que h1^h2 == h2^h1, e h1^h2 == 0 se h1 == h2.

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::vec3(0.0f);
  }

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

    // Compute normal
    auto const edge1{b.position - a.position};
    auto const edge2{c.position - b.position};
    auto const 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;
}

Essa 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::vec3(0.0f);
  }

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

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

    // Compute normal
    auto const edge1{b.position - a.position};
    auto const edge2{c.position - b.position};
    auto const 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 107 a 115 do código abaixo). Se os vetores normais não forem encontrados, chamamos Model::computeNormals (linhas 135 a 137):

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

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

  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 (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)};

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

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

      Vertex const vertex{.position = position, .normal = normal};

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

  if (standardize) {
    Model::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
  auto const positionAttribute{
      abcg::glGetAttribLocation(program, "inPosition")};
  if (positionAttribute >= 0) {
    abcg::glEnableVertexAttribArray(positionAttribute);
    abcg::glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE,
                                sizeof(Vertex), nullptr);
  }

  auto const normalAttribute{abcg::glGetAttribLocation(program, "inNormal")};
  if (normalAttribute >= 0) {
    abcg::glEnableVertexAttribArray(normalAttribute);
    auto const offset{offsetof(Vertex, normal)};
    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 na linhas 177 usando a macro offsetof.

window.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 Window será substituída por um conjunto de variáveis:

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

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.

window.cpp

No início do arquivo precisamos incluir um cabeçalho a mais:

#include "imfilebrowser.h"

onCreate

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

void Window::onCreate() {
  auto const assetsPath{abcg::Application::getAssetsPath()};

  abcg::glClearColor(0, 0, 0, 1);
  abcg::glEnable(GL_DEPTH_TEST);

  // Create programs
  for (auto const &name : m_shaderNames) {
    auto const program{
        abcg::createOpenGLProgram({{.source = assetsPath + name + ".vert",
                                    .stage = abcg::ShaderStage::Vertex},
                                   {.source = assetsPath + name + ".frag",
                                    .stage = abcg::ShaderStage::Fragment}})};
    m_programs.push_back(program);
  }

  // Load model
  m_model.loadObj(assetsPath + "bunny.obj");
  m_model.setupVAO(m_programs.at(m_currentProgramIndex));

  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 Window::onPaintUI.

onPaint

A definição de Window::onPaint ficará assim:

void Window::onPaint() {
  abcg::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  abcg::glViewport(0, 0, m_viewportSize.x, m_viewportSize.y);

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

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

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

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

  auto const modelViewMatrix{glm::mat3(m_viewMatrix * m_modelMatrix)};
  auto const 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.

Na linha 81, 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.

onPaintUI

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

  auto const scaledWidth{gsl::narrow_cast<int>(m_viewportSize.x * 0.8f)};
  auto const scaledHeight{gsl::narrow_cast<int>(m_viewportSize.y * 0.8f)};

  // File browser for models
  static ImGui::FileBrowser fileDialogModel;
  fileDialogModel.SetTitle("Load 3D Model");
  fileDialogModel.SetTypeFilters({".obj"});
  fileDialogModel.SetWindowSize(scaledWidth, scaledHeight);
  
#if defined(__EMSCRIPTEN__)
  auto const assetsPath{abcg::Application::getAssetsPath()};  
  fileDialogModel.SetPwd(assetsPath);
#endif  

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.

Na janela da ImGui na parte superior direita incluiremos uma caixa de seleção de shaders. Para que a janela tenha espaço vertical suficiente para essa caixa de seleção, mudaremos o widgetSize, de (222, 90) para (222, 142):

    auto const widgetSize{ImVec2(222, 142)};

A caixa de seleção de shaders é implementada com o seguinte trecho de código, após a criação do combo box de seleção da projeção:

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

      ImGui::PushItemWidth(120);
      if (ImGui::BeginCombo("Shader", m_shaderNames.at(currentIndex))) {
        for (auto const index : iter::range(m_shaderNames.size())) {
          auto const 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 (gsl::narrow<int>(currentIndex) != m_currentProgramIndex) {
        m_currentProgramIndex = gsl::narrow<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.

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

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

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

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

  fileDialogModel.Display();

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

Se algum arquivo foi selecionado na caixa de diálogo (linha 228), 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.

onDestroy

Em Window::onDestroy, liberamos os programas de shader que foram criados:

void Window::onDestroy() {
  m_model.destroy();
  for (auto const &program : m_programs) {
    abcg::glDeleteProgram(program);
  }
}

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

depth.vert

Em depth.vert, removemos a variável uniforme de mudança de cor. Todos os objetos agora serão desenhados com tons de cinza:

#version 300 es

layout(location = 0) in vec3 inPosition;

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;

out vec4 fragColor;

void main() {
  vec4 posEyeSpace = viewMatrix * modelMatrix * vec4(inPosition, 1);

  float i = 1.0 - (-posEyeSpace.z / 3.0);
  fragColor = vec4(i, i, i, 1);

  gl_Position = projMatrix * posEyeSpace;
}

normal.frag

O conteúdo do fragment shader responsável pelo desenho das normais de vértices como cores é bem simples:

#version 300 es

precision mediump float;

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.21 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.21: Cores correspondentes para as direções positivas e negativas dos eixos principais.

O código ficará como a seguir:

#version 300 es

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.

Observe 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);

Entretanto, a transformação de 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.22, como uma escala não uniforme faz com que os 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.22: 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.22 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.22. 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.