Implementação do modelo de Phong e o Phong shading
Agora que abordamos o básico do modelo de reflexão de Phong e o Phong shading, podemos avançar para a implementação deles. Neste projeto iremos fazer uma série de simplificações para calcular o valor de iluminação para cada fragmento. A primeira delas é que teremos uma única fonte de luz que será definida apenas por sua posição na cena, não iremos considerar aqui a atenuação da luz devido a distância do objeto em relação a posição luz. A segunda simplificação é que vamos definir os valores de ,
,
e
como constantes no código do fragment shader.
Definindo a posição da luz
Começamos definindo a localização da nossa fonte de luz. Para armazenar essa informação em nosso programa, definimos um membro de classe em Renderer
com o seguinte valor:
QVector3D m_lightPosition = QVector3D(0.0, 1.0, 1.0);
Para simplificar, vamos passar a posição da fonte de luz através do UBO no shader. Para isso, modificamos o UBO no vertex shader para receber a posição da luz:
#version 450
layout(set = 0, binding = 1) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
vec3 lightPosition;
} ubo;
...
Em seguida, atualizamos o UBO no código do programa para incluir um vetor 3D contendo a posição da fonte de luz. Adicionamos um membro do tipo QVector3D
na estrutura UniformBufferObject
para isso:
struct UniformBufferObject {
QMatrix4x4 model;
QMatrix4x4 view;
QMatrix4x4 proj;
QVector3D lighPosition;
};
Agora podemos atualizar a função updateUniformBuffer
para passar a posição da luz para o UBO no vertex shader:
float ecLightPosition[] = {
m_lightPosition.x(),
m_lightPosition.y(),
m_lightPosition.z()
};
...
memcpy(data, ubo.model.constData(), 64);
memcpy(data + 64, ubo.view.constData(), 64);
memcpy(data + 128, ubo.proj.constData(), 64);
memcpy(data + 192, ecLightPosition, 3 * sizeof(float));
Normais de superfície
Como discutido anteriormente, para renderizar nosso modelo utilizando o modelo de reflexão de Phong, precisamos das normais da superfície. Para obter essa informação, modificamos a estrutura Vertex
para incluir um QVector3D
para o valor da normal:
struct Vertex {
QVector3D pos;
QVector3D color;
QVector2D texCoord;
QVector3D normal;
static VkVertexInputBindingDescription getBindingDescription() {
VkVertexInputBindingDescription bindingDescription = {};
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
bindingDescription.stride = sizeof(Vertex);
bindingDescription.binding = 0;
return bindingDescription;
}
static std::array<VkVertexInputAttributeDescription, 4> getAttributeDescriptions() {
std::array<VkVertexInputAttributeDescription, 4> attributeDescriptions = {};
attributeDescriptions[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, pos);
attributeDescriptions[1].binding = 0;
attributeDescriptions[1].location = 1;
attributeDescriptions[1].format =
VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[1].offset = offsetof(Vertex, color);
attributeDescriptions[2].binding = 0;
attributeDescriptions[2].location = 2;
attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT;
attributeDescriptions[2].offset =
offsetof(Vertex, texCoord);
attributeDescriptions[3].binding = 0;
attributeDescriptions[3].location = 3;
attributeDescriptions[3].format =
VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[3].offset = offsetof(Vertex, normal);
return attributeDescriptions;
}
};
Adicionamos também um VkVertexInputAttributeDescription
para que possamos usar o acesso a normais como entrada no vertex shader. Isso é necessário para poder passá-las ao fragment shader para o calculo da reflexão difusa.
Carregando normais do modelo
Agora precisamos atualizar nossa função readOBJFile
para carregar as normais do modelo 3D.
void Model::readOBJFile(QString const &filePath) {
...
for (const auto &shape : shapes) {
for (const auto &index : shape.mesh.indices) {
Vertex vertex = {};
size_t indexTemp;
...
indexTemp = index.normal_index * 3;
if (index.normal_index > -1) {
vertex.normal = {
attribs.normals[indexTemp + 0],
attribs.normals[indexTemp + 1],
attribs.normals[indexTemp + 2]
};
} else {
vertex.normal = {0.0f, 0.0f, 0.0f};
}
vertices.push_back(vertex);
}
}
Da mesma forma que atribs.vertices
contém os valores de posição dos vértices do modelo, attribs.normals
contém o valores das normais para cada vértice do modelo. Estamos assumindo aqui, que o modelo no arquivo OBJ contém os valores das normais para cada vértice do modelo.
Modificando o vertex shader
Em seguida, adicionamos um novo layout de local no vertex shader para receber o atributo da normal:
...
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;
layout(location = 3) in vec3 inNormal;
...
Agora que temos os valores das normais e a posição da fonte de luz, vamos implementar os cálculos necessários para obter o valor de luz para cada fragmento.
Primeiro precisamos passar o valor da normal do vertex shader para o fragment shader. Para isso, criamos uma nova variável de saída do tipo vec3
para enviar as informação da normal ao fragment shader. Mas antes de passarmos a normal para fragment shader, a normal precisa ser convertida no espaço do mundo. Diferentemente de como multiplicamos a posição local, a matriz do modelo para converter a normal no espaço do mundo precisa ser tratada de maneira diferente. A normal é multiplicada pela transposta inversa da matriz 3×3 superior do modelo e é armazenada na variável de saída fragNormal
:
..
layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec2 fragTexCoord;
layout(location = 2) out vec3 fragNormal;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model *
vec4(inPosition, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
fragNormal = mat3(inverse(transpose(ubo.model))) * inNormal;
}
Como vamos utilizar o produto ubo.model * vec4(inPosition, 1.0)
mais de uma vez, vamos definir uma variável auxiliar chamada worldPos
do tipo vec4
para armazenar esse valor:
gl_Position = ubo.proj * ubo.view * ubo.model *
vec4(inPosition, 1.0);
vec4 worldPos = ubo.model * vec4(inPosition, 1.0);
...
Em seguida, utilizamos esse valor para calcular a variável de saída para o fragment shader responsável por armazenar o vetor apontado para o observador e outro vetor apontando para a fonte de luz. Chamamos esses vetores, respectivamente, de fragViewVec
e fragLightVec
, ambos serão variáveis de saída do tipo vec3
:
layout(location = 3) out vec3 fragViewVec;
layout(location = 4) out vec3 fragLightVec;
...
void main() {
...
vec4 worldPos = ubo.model * vec4(inPosition, 1.0);
fragNormal = mat3(inverse(transpose(ubo.view * ubo.model))) *
inNormal;
fragViewVec = (ubo.view * worldPos).xyz;
fragLightVec = ubo.lightPosition - vec3(worldPos);
}
Isso é tudo para definir o vertex shader. O código completo do vertex shader é o seguinte:
#version 450
layout(set = 0, binding = 1) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
vec3 lightPosition;
} ubo;
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;
layout(location = 3) in vec3 inNormal;
layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec2 fragTexCoord;
layout(location = 2) out vec3 fragNormal;
layout(location = 3) out vec3 fragViewVec;
layout(location = 4) out vec3 fragLightVec;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model *
vec4(inPosition, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
vec4 worldPos = ubo.model * vec4(inPosition, 1.0);
fragNormal = mat3(inverse(transpose(ubo.model))) * inNormal;
fragViewVec = (ubo.view * worldPos).xyz;
fragLightVec = ubo.lightPosition - vec3(worldPos);
}
Modificando o fragment shader
Agora vamos atualizar o fragment shader. A primeira coisa que vamos fazer é definir as variáveis de entrada para a normal, o vetor apontando para a fonte de luz e o vetor apontando para o observador (visão):
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;
layout(location = 2) in vec3 fragNormal;
layout(location = 3) in vec3 fragViewVec;
layout(location = 4) in vec3 fragLightVec;
Em seguida definimos o valor de numa variável constante chamada
ambientLightColor
; numa outra variável constante chamada
diffuseLightColor
; e numa variável, também constante, chamada
specularLightColor
com o seguintes valores:
...
const vec3 ambientLightColor = vec3(0.1);
const vec3 diffuseLightColor = vec3(1.0);
const vec3 specularLightColor = vec3(1.0);
const float shininess = 16.0;
...
Também definimos o valor de na variável
shininess
.
Antes de usarmos os valores de fragNormal
, fragLightVec
e fragViewVec
, para o cálculo das reflexões do modelo de Phong, precisamos normalizá-los. Isso é feito através da função normalize
:
void main() {
vec3 n = normalize(fragNormal);
vec3 l = normalize(fragLightVec);
vec3 v = normalize(fragViewVec);
vec3 r = reflect(l, n);
...
}
Como mencionado anteriormente, para calcular utilizamos a função
reflect
.
Agora utilizamos a equação do modelo de reflexão de Phong para calcular o valor de cada componente de reflexão:
void main() {
...
vec3 ambient = ambientLightColor;
vec3 diffuse = diffuseLightColor * max(dot(n, l), 0.0);
vec3 specular = specularLightColor *
pow(max(dot(r, v), 0.0), shininess);
}
Por fim, utilizamos a soma das componentes de reflexão e multiplicamos isso pelo valor de cor dado pela textura:
void main() {
...
outColor = texture(texSampler, fragTexCoord) *
vec4(ambient + diffuse + specular, 1.0);
}
O código completo do fragmet shader é o seguinte:
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;
layout(location = 2) in vec3 fragNormal;
layout(location = 3) in vec3 fragViewVec;
layout(location = 4) in vec3 fragLightVec;
layout(set = 0, binding = 0) uniform sampler2D texSampler;
layout(location = 0) out vec4 outColor;
const vec3 ambientLightColor = vec3(0.1);
const vec3 diffuseLightColor = vec3(1.0);
const vec3 specularLightColor = vec3(1.0);
const float shininess = 16.0;
void main() {
vec3 n = normalize(fragNormal);
vec3 l = normalize(fragLightVec);
vec3 v = normalize(fragViewVec);
vec3 r = reflect(l, n);
vec3 ambient = ambientLightColor;
vec3 diffuse = diffuseLightColor * max(dot(n, l), 0.0);
vec3 specular = specularLightColor * pow(max(dot(r, v), 0.0), shininess);
outColor = texture(texSampler, fragTexCoord) *
vec4(ambient + diffuse + specular, 1.0);
}
Não podemos nos esquecer de recompilar os shaders.
Agora se executarmos o programa e carregar o modelo junto com a textura sugeridos para download, veremos o seguinte resultado:
