Link

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 kaIa, kdId, ksIs 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 kaIa numa variável constante chamada ambientLightColor; kdId numa outra variável constante chamada diffuseLightColor; e ksIs 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 r 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:

Figura 19 – Modelo 3D com textura utilizando Phong shading


Anterior Próximo