diff --git a/reports/RelatorioFase4.tex b/reports/RelatorioFase4.tex index 7868e071..4507b366 100644 --- a/reports/RelatorioFase4.tex +++ b/reports/RelatorioFase4.tex @@ -81,7 +81,36 @@ \section{\emph{Generator}} \subsection{Formato \texttt{.3d}} -{\color{red} TODO - Humberto} +O formato \texttt{.3d} exportado pelo \texttt{generator} e utilizado pela \texttt{engine} é o +Wavefront OBJ \cite{wavefront-obj}. Apenas uma pequena fração das funcionalidades deste formato +são suportadas e, nesta fase, foram necessárias adições aos mecanismos de escrita e leitura de +ficheiros Wavefront OBJ para suportar coordenadas de textura e normais. Este formato é textual, onde +cada linha pode ser um comentário, uma posição, uma coordenada de textura, uma normal, ou uma face +triangular, como mostra o exemplo abaixo, na ordem apresentada: + +\begin{lstlisting} + +# Comment +v 0.5 0.5 1 +vt 0.3 0.3 +vn 0 1 0 +f 1/2/3 4/5/6 7/8/9 +\end{lstlisting} + +Quando uma linha começa com \texttt{v}, \texttt{vt} ou \texttt{vn}, devem seguir-se as coordenadas +de uma posição, de uma textura, ou de um vetor normal, respetivamente. Quando uma linha começa com +\texttt{f}, deve seguir-se uma face triangular, ou seja três pontos. O \texttt{generator} e a +\texttt{engine} suportam dois tipos de ponto: + +\begin{itemize} + \item Apenas um número, um índice de uma posição, ou seja, um elemento do tipo \texttt{v} + (começando a contar em 1); + \item Da forma \texttt{v/t/n}, onde estão presentes três índices, um para uma posição, um para + uma coordenada de textura, e outro para um vetor normal. +\end{itemize} + +Como o \emph{parser} de ficheiros Wavefront OBJ foi reimplementado na fase anterior com base em +expressões regulares, foi trivial adicionar o suporte para coordenadas de textura e vetores normais. \subsection{Plano Horizontal} @@ -109,7 +138,11 @@ \subsection{\emph{Torus}} \subsection{Outras Figuras} -{\color{red} TODO - Humberto} +Para as restantes figuras, devido à sua complexidade e ao pouco tempo disponível para a conclusão +desta fase do trabalho prático, não foram adicionadas nem coordenadas de textura nem normais. No +entanto, a \texttt{engine} ainda é capaz de importar estes modelos e de os desenhar com iluminação, +graças a um algoritmo de geração automática de normais implementado e descrito posteriormente neste +relatório. \subsection{Sistema Solar} @@ -119,7 +152,99 @@ \section{\emph{Engine}} \subsection{Geração Automática de Normais} -{\color{red} TODO - Humberto} +A \texttt{engine} é capaz de carregar modelos que não tenham informação sobre coordenadas de +texturas ou normais. Caso não haja informação sobre as coordenadas de textura de um modelo, é +possível desenhá-lo a uma cor sólida, mas é necessário que se tenha informação sobre as suas normais +para o iluminar corretamente. Como esta nem sempre está presente, foi implementado um algoritmo para +gerar normais de modelos automaticamente. + +Este algoritmo considera o modelo inteiro como um único \emph{smoothing group}, e calcula a normal +de cada vértice como a média das normais dos triângulos a que este pertence, pesada pela área dos +triângulos. Logo, é necessário, em primeiro lugar, uma forma de calcular a normal de um triângulo. +Para um triângulo $[ABC]$, esta pode ser calculada do seguinte modo: + +$$ +\hat{n}_{[ABC]} = \frac{ + \overrightarrow{AB} \times \overrightarrow{AC} +}{ + \lVert \overrightarrow{AB} \times \overrightarrow{AC} \rVert +} +$$ + +Depois, a área de cada triângulo, $A$, pode ser calculada pela fórmula de Heron: + +$$ +S = \frac{ + \lVert \overrightarrow{AB} \rVert + + \lVert \overrightarrow{AC} \rVert + + \lVert \overrightarrow{BC} \rVert +}{ + 2 +} +$$ + +$$ +A = \sqrt{ + S + \left ( S - \lVert \overrightarrow{AB} \rVert \right ) + \left ( S - \lVert \overrightarrow{AC} \rVert \right ) + \left ( S - \lVert \overrightarrow{BC} \rVert \right ) +} +$$ + +Logo, sendo $F$ o conjunto de faces triangulares nas quais um ponto está presente, o vetor normal +desse ponto é dado por: + +$$ +\hat{n} = \frac{ + \sum_{f \in F} {A_f \, \hat{n}_f} +}{ + \lVert \sum_{f \in F} {A_f \, \hat{n}_f} \rVert +} +$$ + +Em termos de implementação deste algoritmo, um dicionário é utilizado para armazenar associações +entre posições de pontos e pares normal-área. Iteram-se por todas as faces do modelo e, para +cada face, calcula-se a sua normal e a sua área. Depois, para cada ponto nessa face, adicionam-se +aos valores armazenados de normal e de área $A \, \hat{n}$ e $A$ respetivamente. Após iterar por +todas as faces, itera-se por todas as posições no dicionário, e define-se a normal de cada ponto +como o quociente entre a normal armazenada e a área total. Logicamente, este vetor deve ser +normalizado antes de adicionado ao modelo. + +\subsection{VBOs} + +Com a adição de coordenadas de textura e normais, foi necessário criar mais VBOs para as armazenar. +Agora, cada modelo passa a ter um VAO associado a três VBOs, como mostra a figura abaixo: + +\begin{figure}[H] + \centering + \includegraphics[width=\textwidth]{res/phase4/VAO.pdf} + \caption{Organização do VAO, dos VBOs, e do EBO de um modelo.} +\end{figure} + +Pode observar-se que, ao contrário do que acontece nos ficheiros \texttt{.3d}, não pode haver +vértices formados por posições, coordenadas de textura, e normais com diferentes índices. Para isso, +seria necessário mais do que um \emph{buffer} de índices, e isso não é suportado pelo OpenGL. Logo, +após carregar um modelo, é necessário converter o esquema de indexação de um ficheiro Wavefront OBJ +para o de um \emph{index buffer}. Para o fazer, um algoritmo simples pode ser utilizado: + +\begin{itemize} + \item Cria-se um \emph{index buffer} e três outros \emph{buffers}, para armazenamento das + posições, das coordenadas de textura, e das normais, inicialmente vazios; + + \item Armazena-se um dicionário que associa tuplos posição-c.textura-normal a índices (elementos + do \emph{index buffer}); + + \item Iteram-se por todos os vértices no modelo. Para cada vértice: + \begin{itemize} + \item Constrói-se o tuplo posição-c.textura-normal associado ao vértice, que se procura + no dicionário; + \item Caso seja encontrado, adiciona-se o índice encontrado ao \emph{index buffer}. + \item Caso contrário, coloca-se o tuplo no dicionário; adicionam-se a posição, a + coordenada de textura e a normal aos seus respetivos \emph{buffers}; e adiciona-se o + índice de um destes elementos ao \emph{index buffer}. + \end{itemize} +\end{itemize} \subsection{Adição ao \emph{Schema} XML} @@ -289,6 +414,9 @@ \section{Bibliografia} \renewcommand{\section}[2]{} \begin{thebibliography}{9} + \bibitem{wavefront-obj} + ``Wavefront OBJ File Format Summary.''{} FileFormat.Info. Accessed: May 14, 2025. [Online.] + Available: \url{https://www.fileformat.info/format/wavefrontobj/egff.htm} \bibitem{stb-image} ``stb.'' GitHub. Accessed: May 13, 2025. [Online.] Available: \url{https://github.com/nothings/stb} diff --git a/reports/res/phase4/VAO.drawio b/reports/res/phase4/VAO.drawio new file mode 100644 index 00000000..342467b0 --- /dev/null +++ b/reports/res/phase4/VAO.drawio @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/reports/res/phase4/VAO.pdf b/reports/res/phase4/VAO.pdf new file mode 100644 index 00000000..081051ad Binary files /dev/null and b/reports/res/phase4/VAO.pdf differ diff --git a/src/utils/WavefrontOBJ.cpp b/src/utils/WavefrontOBJ.cpp index 92c6c3d0..559eff88 100644 --- a/src/utils/WavefrontOBJ.cpp +++ b/src/utils/WavefrontOBJ.cpp @@ -267,20 +267,20 @@ void WavefrontOBJ::generateNormals() { const glm::vec4 p1 = this->positions[face.positions[1]]; const glm::vec4 p2 = this->positions[face.positions[2]]; - const glm::vec3 v0 = glm::vec3(p0) - glm::vec3(p1); - const glm::vec3 v1 = glm::vec3(p0) - glm::vec3(p2); - const glm::vec3 v2 = glm::vec3(p1) - glm::vec3(p2); + const glm::vec3 v0 = glm::vec3(p1) - glm::vec3(p0); + const glm::vec3 v1 = glm::vec3(p2) - glm::vec3(p0); + const glm::vec3 v2 = glm::vec3(p2) - glm::vec3(p1); // Calculate area of triangle via Heron's formula const float sideLength0 = glm::length(v0); const float sideLength1 = glm::length(v1); const float sideLength2 = glm::length(v2); const float semiPerimeter = 0.5f * (sideLength0 + sideLength1 + sideLength2); - const float area = semiPerimeter * (semiPerimeter - sideLength0) * - (semiPerimeter - sideLength1) * (semiPerimeter - sideLength2); + const float area = sqrtf(semiPerimeter * (semiPerimeter - sideLength0) * + (semiPerimeter - sideLength1) * (semiPerimeter - sideLength2)); // Calculate weighted average - const glm::vec3 normal = glm::cross(v0, v1) * area; + const glm::vec3 normal = glm::normalize(glm::cross(v0, v1)) * area; averageNormals[p0] += normal; averageWeights[p0] += area;