renderizador 3d para aplicaÇÕes grÁficas utilizando...

61
UNIVERSIDADE REGIONAL DE BLUMENAU CENTRO DE CIÊNCIAS EXATAS E NATURAIS CURSO DE CIÊNCIA DA COMPUTAÇÃO BACHARELADO RENDERIZADOR 3D PARA APLICAÇÕES GRÁFICAS UTILIZANDO VULKAN DANIEL STRECK BLUMENAU 2017

Upload: nguyenxuyen

Post on 05-Oct-2018

217 views

Category:

Documents


0 download

TRANSCRIPT

UNIVERSIDADE REGIONAL DE BLUMENAU

CENTRO DE CIÊNCIAS EXATAS E NATURAIS

CURSO DE CIÊNCIA DA COMPUTAÇÃO – BACHARELADO

RENDERIZADOR 3D PARA APLICAÇÕES GRÁFICAS

UTILIZANDO VULKAN

DANIEL STRECK

BLUMENAU

2017

DANIEL STRECK

RENDERIZADOR 3D PARA APLICAÇÕES GRÁFICAS

UTILIZANDO VULKAN

Trabalho de Conclusão de Curso apresentado

ao curso de graduação em Ciência da

Computação do Centro de Ciências Exatas e

Naturais da Universidade Regional de

Blumenau como requisito parcial para a

obtenção do grau de Bacharel em Ciência da

Computação.

Prof. Dalton Solano dos Reis, Mestre - Orientador

BLUMENAU

2017

RENDERIZADOR 3D PARA APLICAÇÕES GRÁFICAS

UTILIZANDO VULKAN

Por

DANIEL STRECK

Trabalho de Conclusão de Curso aprovado

para obtenção dos créditos na disciplina de

Trabalho de Conclusão de Curso II pela banca

examinadora formada por:

______________________________________________________

Presidente: Prof. Dalton Solano dos Reis, M. Sc. – Orientador, FURB

______________________________________________________

Membro: Prof. Mauro Marcelo Mattos, Doutor – FURB

______________________________________________________

Membro: Prof. Aurélio Faustino Hoppe, M.Sc. – FURB

Blumenau, 12 de dezembro de 2017

Dedico este trabalho à minha família, amigos e

namorada que me ajudaram na conclusão do

mesmo.

AGRADECIMENTOS

Aos meus professores, que me guiaram na busca por conhecimento.

À minha família, que sempre me apoiou e incentivou.

À minha namorada, por ter sido paciente e me incentivado.

E ao meu orientador, por ter acreditado na ideia proposta por este trabalho.

Você tem poder sobre sua mente – não eventos

exteriores. Perceba isso, e você encontrará

força.

Marco Aurélio

RESUMO

Neste trabalho é apresentada a implementação de uma biblioteca renderizadora 3D utilizando

as APIs Vulkan e OpenGL. O enfoque do trabalho está no estudo exploratório da API Vulkan

e seu funcionamento e da comparação de performance e de detalhes de implementação entre

as APIs gráficas. Desenvolveu-se uma biblioteca de renderização 3D com um módulo

utilizando Vulkan e outro OpenGL, ambos implementando uma mesma interface genérica

para garantir que os mesmos testes possam ser realizados de forma similar nas duas APIs. O

desenvolvimento da biblioteca é apresentado em detalhes, contendo informações referentes à

funcionalidade do Vulkan e OpenGL, com demonstrações do código implementado onde se

aplica. Para captação de métricas de performance, três cenários de testes utilizando cada API

foram desenvolvidos para efeito de comparação. Ao final conclui-se que a partir dos dados

obtidos nos testes desenvolvidos, a utilização da API Vulkan resultou em melhor desempenho

nos cenários empregados. Por fim são apresentadas sugestões de extensão deste trabalho.

Palavras chave: Computação gráfica. Vulkan. OpenGL.

ABSTRACT

This paper presents a 3D renderer implementation utilizing the OpenGL and Vulkan APIs.

The main focus of the paper is the exploratory study of the Vulkan API and the performance

and implementation details comparison between the two graphics APIs. A 3D rendering

library with a Vulkan and an OpenGL module was developed, both implementing the same

base generic interface to guarantee the same tests can be applied in a similar manner in both

APIs. The library’s development is shown in detail, containing information about Vulkan and

OpenGL’s implementation details, and where applicable, source code is provided. Three test

scenarios were developed to acquire performance metrics using each API to effectively

realize the comparison between the two. At the end, it can be concluded from the performance

tests that the Vulkan API is more performant than OpenGL on the presented scenarios. At last

extension topics for this work are proposed.

Key words: Computer graphics. Vulkan. OpenGL.

LISTA DE FIGURAS

Figura 1 – Estágios do pipeline gráfico .................................................................................... 15

Figura 2 – Captura de tela da aplicação ProtoStar.................................................................... 20

Figura 3 – Processo de criação da árvore de renderização (Render tree) ................................. 22

Figura 4 – Processo de renderização da Render tree................................................................ 23

Figura 5 – Captura de tela do Vulkan Tutorial ......................................................................... 25

Figura 6 – Diagrama de atividade (Vulkan) ............................................................................. 27

Figura 7 - Diagrama de classes (Vulkan) ................................................................................. 29

Figura 8 – Fluxo principal da biblioteca ................................................................................... 30

Figura 9 – Configuração do diretório de Include do Vulkan SDK ........................................... 45

Figura 10 – Configuração do diretório de Lib do Vulkan SDK ............................................... 46

Figura 11 – Estrutura do projeto no Visual Studio ................................................................... 47

Figura 12 – Captura de tela da cena 1 ...................................................................................... 51

Figura 13 – Captura de tela da cena 2 ...................................................................................... 51

Figura 14 – Captura de tela da cena 3 ...................................................................................... 52

Figura 15 – Gráfico de quadros por segundos .......................................................................... 53

Figura 16 – Gráfico com a relação de frame time (ms) entre as APIs ...................................... 54

Figura 17 – Gráfico com a relação de utilização de mémoria RAM entre as APIs.................. 55

Figura 18 – Gráfico com a relação de utilização de CPU entre as APIs .................................. 55

Figura 19 – Gráfico comparativo de escalabilidade de FPS entre as APIs .............................. 56

Figura 19 – Representação gráfica da arquiterua com Vulkan ................................................. 60

LISTA DE QUADROS

Quadro 1 – inicialização de instância Vulkan .......................................................................... 32

Quadro 2 – Busca de um dispositivo físico (RendererVk.cpp:259) ......................................... 33

Quadro 3 – Atribuição das queues e dispositivo lógico (RendererVk.cpp:283) ...................... 35

Quadro 4 – Configuração do swap chain (RendererVk.cpp:354) ............................................ 36

Quadro 5 – Configuração do render pass (RendererVk.cpp:501)............................................ 37

Quadro 6 – Configuração de um VkPipeline (RendererVk.cpp:632) ...................................... 38

Quadro 7 – Conversão de shader GLSL para SPIR-V (compile shaders.bat) .......................... 38

Quadro 8 – Configuração dos semáforos (RendererVk.cpp:632) ............................................ 39

Quadro 9 – Inicialização do GLFW para OpenGL (RendererGL.cpp:30) ............................... 40

Quadro 10 – Inicialização do OpenGL (RendererGL.cpp:52) ................................................. 40

Quadro 11 – Pseudocódigo de gravação dos command buffers ............................................... 43

Quadro 12 – Pseudocódigo de uma função de desenho com Vulkan ..................................... 44

Quadro 13 – Pseudocódigo de uma função de desenho com OpenGL ................................... 45

Quadro 14 – Definição classe Cena1 ...................................................................................... 48

Quadro 15 – Classe VulkanShader ........................................................................................... 49

Quadro 16 – Comparativo entre os trabalhos correlatos .......................................................... 50

LISTA DE TABELAS

Tabela 1 – Tempo (milissegundos) de um frame na CPU em relação à quantidade de objetos

............................................................................................................................... 24

Tabela 2 – Tempo (milissegundos) de um frame na GPU em relação à quantidade de objetos

............................................................................................................................... 24

LISTA DE ABREVIATURAS E SIGLAS

API – Application Programming Interface

CPU – Central Processing Unit

FPS – Frames Per Second

GLEW – OpenGL Extension Wrangler Library

GPU – Graphics Processing Unit

GLSL – GL Shading Language

OpenGL – Open Graphics Library

SDK – Software Development Kit

SPIR-V – Standard Portable Intermediate Representation – V

SUMÁRIO

1 INTRODUÇÃO .................................................................................................................. 13

1.1 OBJETIVOS ...................................................................................................................... 14

1.2 ESTRUTURA.................................................................................................................... 14

2 FUNDAMENTAÇÃO TEÓRICA .................................................................................... 15

2.1 FUNDAMENTAÇÃO EM COMPUTAÇÃO GRÁFICA ................................................ 15

2.2 OPENGL E OPENGL MATHEMATICS (GLM) ............................................................ 17

2.3 VULKAN .......................................................................................................................... 17

2.4 TRABALHOS CORRELATOS ........................................................................................ 19

2.4.1 UNREAL ENGINE ......................................................................................................... 19

2.4.2 Vulkan based render toolkit ............................................................................................ 21

2.4.3 Vulkan tutorial ................................................................................................................ 24

3 DESENVOLVIMENTO DA BIBLIOTECA ................................................................... 26

3.1 REQUISITOS .................................................................................................................... 26

3.2 ESPECIFICAÇÃO ............................................................................................................ 26

3.2.1 Diagrama de atividades ................................................................................................... 26

3.2.2 Diagrama de classes ........................................................................................................ 28

3.3 IMPLEMENTAÇÃO ........................................................................................................ 30

3.3.1 Técnicas e ferramentas utilizadas.................................................................................... 30

3.3.2 OPERACIONALIDADE DA IMPLEMENTAÇÃO ...................................................... 45

3.4 ANÁLISE DOS RESULTADOS ...................................................................................... 49

3.4.1 COMPARAÇÃO ENTRE OS TRABALHOS CORRELATOS .................................... 49

3.4.2 RESULTADOS DOS CENÁRIOS DE TESTES ........................................................... 50

3.4.3 Desempenho .................................................................................................................... 52

4 CONCLUSÕES .................................................................................................................. 57

4.1 EXTENSÕES .................................................................................................................... 58

REFERÊNCIAS ..................................................................................................................... 59

ANEXO A – REPRESENTAÇÃO GRÁFICA DA ARQUITETURA COM VULKAN . 60

13

1 INTRODUÇÃO

Pode-se afirmar que para criar a ilusão de imagem em movimento em vídeos ou

aplicações gráficas, a taxa de quadros para que uma aplicação possa ser denominada de

tempo-real começa a partir dos 15 quadros por segundo. Portanto, há necessidade de garantir

alta performance para que aplicações interativas se tornem agradáveis para os usuários

(AKENINE-MÖLLER; HAINES; HOFFMAN, 2008, p.1). Para atingir altos níveis de

performance, APIs são comumente empregadas para programação de aplicações que utilizam

o hardware com recursos de computação gráfica.

Uma das entidades que mantém tais APIs é o grupo Khronos, que tem como objetivo

criar e manter especificações abertas de APIs para computação paralela, computação gráfica,

mídias dinâmicas, visão computacional e processamento de sensores em uma gama variada de

plataformas (KHRONOS, 2016c). Algumas das especificações mantidas pelo grupo

começaram a evoluir de maneira separada, tais como o OpenGL (uma API gráfica utilizada

em plataformas desktop e consoles) e o OpenGL ES (OpenGL for Embedded Systems) para

mobile. Devido a isso, a iniciativa do OpenGL começou a se fragmentar e evoluir

independentemente, o que causou falta de conformidade e compatibilidade entre aplicações

do OpenGL ES e OpenGL (KHRONOS, 2016c).

Devido a esta fragmentação, desenvolvedores começaram a apontar outras

inconsistências e possíveis melhorias para a API. Isto resultou em uma iniciativa sem

precedentes a partir de desenvolvedores proeminentes das indústrias de desenvolvimento de

jogos digitais, tanto de hardware quanto software, para especificação do futuro do OpenGL,

que veio a se tornar o Vulkan (KHRONOS, 2016c).

Algumas das vantagens propostas pela especificação do Vulkan, em relação às APIs

anteriores, são a uniformidade entre plataformas, suporte ao envio de comandos assíncronos

da CPU para GPU e a disponibilização explicita de suas funções com pouco overhead. Estas

mudanças transferem maior responsabilidade para o desenvolvedor, para utilizá-las de forma

que atenda melhor cada aplicação em particular. Através dessas funções, possibilita-se que a

API se comporte de maneira extremamente previsível (SAMSUNG, 2016).

Diante do exposto, desenvolveu-se um estudo exploratório da API Vulkan, realizando

a implementação de uma biblioteca para renderização de cenas 3D e análise da performance

obtida em comparação com a API OpenGL.

14

1.1 OBJETIVOS

O objetivo deste trabalho é realizar um estudo exploratório da API Vulkan.

Os objetivos específicos são:

a) desenvolver uma biblioteca para renderização 3D utilizando Vulkan;

b) realizar uma comparação da biblioteca desenvolvida com Vulkan a uma análoga

desenvolvida com OpenGL;

c) analisar a performance atingida nos testes propostos.

1.2 ESTRUTURA

O trabalho está organizado em quatro capítulos. O primeiro capítulo contém a

introdução, os objetivos e a estrutura. No segundo capítulo, está presente a fundamentação

teórica necessária para a compreensão do objeto de estudo deste trabalho. O desenvolvimento

do trabalho é demonstrado no terceiro capítulo, onde é apresentado um diagrama de

atividades e o digrama de classe e são apresentadas as técnicas e detalhes de implementação.

O quarto capítulo contempla a conclusão do trabalho e sugestões para trabalhos futuros.

15

2 FUNDAMENTAÇÃO TEÓRICA

Este capítulo aborda assuntos relevantes para a compreensão do objeto de estudo deste

trabalho. A seção 2.1 trata de assuntos relacionados à fundamentação em Computação

Gráfica, como uma visão geral do pipeline gráfico moderno e conceitos como occlusion

culling e materiais. A seção 2.2 contempla a API OpenGL e a biblioteca OpenGL

Mathematics (GLM). Na seção 2.3 são apresentados conceitos sobre a API Vulkan, tais como

uma descrição geral sobre a mesma e suas características como command queues, SPIR-V,

validation layers e swap chain. Na seção 2.4 são apresentados os trabalhos correlatos.

2.1 FUNDAMENTAÇÃO EM COMPUTAÇÃO GRÁFICA

Para se gerar imagens através da Computação Gráfica, faz-se necessária a utilização de

dispositivos de hardware com recurso do pipeline gráfico, geralmente GPUs. Tais dispositivos

podem receber comandos para executar diversas operações computacionais, que podem ter

como resultado imagens rasterizadas ou simplesmente produtos computacionais.

Um pipeline gráfico de renderização tem como produto final uma imagem

bidimensional de forma rasterizada. Para tal, diversos estágios são empregados (Figura 1),

alguns são fixos e outros são programáveis ou configuráveis.

Figura 1 – Estágios do pipeline gráfico

Fonte: Akenine-Möller, Haines e Hoffman (2008).

O pipeline é ativado com um draw call, que, de acordo com Akenine-Möller, Haines e

Hoffman (2008, p.31), é uma chamada para a API gráfica para desenhar um grupo de

primitivas, causando a execução do pipeline gráfico. Após o comando de execução do

pipeline, o primeiro estágio a ser executado é o estágio de geometria. Ele recebe as primitivas

de renderização (pontos, linhas e triângulos) enviados a partir da aplicação e é responsável

pelas operações que ocorrem a cada vértice e polígono (AKENINE-MÖLLER; HAINES;

HOFFMAN, 2008). Durante este estágio, ocorrem algumas etapas que podem ser

16

consideradas estágios separados ou não, que dependendo da implementação do hardware,

podem ocorrer de forma paralela.

Primeiramente ocorre a etapa de transformações geométricas, na qual as primitivas

enviadas pela aplicação são transformadas para espaço global e espaço de câmera sintética

respectivamente. Em seguida, de acordo com Akenine-Möller, Haines e Hoffman (2008, p.17)

na etapa de vertex shading são executadas equações de shading que tem o propósito de definir

o aspecto visual de um objeto através de atributos disponíveis por vértice, como sua

localização no espaço universal, vetor normal, cores ou informações personalizadas. O

produto dessa etapa é posteriormente utilizado com entrada para o estágio de rasterização.

Após o shading, a próxima etapa é a de projeção, na qual o volume de visão da câmera

sintética é transformado em projeção ortográfica ou perspectiva. Em seguida ocorre a etapa de

clipping. Nesta etapa são descartados os objetos primitivos que não estão inteira ou

parcialmente dentro do volume de visão da câmera sintética, e portanto não precisam ser

desenhados. Os objetos que estão parcialmente dentro do volume de visão passam pela

operação de clipping, na qual novos vértices precisam ser determinados para as partes de

objetos que estão parcialmente no volume de visão. A última etapa do estágio de geometria é

o mapeamento para espaço de tela, na qual as primitivas, ainda representados em coordenadas

tridimensionais, são convertidos para espaço de tela em duas dimensões.

O próximo estágio do pipeline gráfico é o de rasterização, que tem por finalidade

converter os dados providos pelo estágio de geometria para pixels e determinar seus

respectivos valores de cor (AKENINE-MÖLLER; HAINES; HOFFMAN, 2008).

O estágio de rasterização é constituído também por diversas etapas que são: triangle

setup, uma etapa não-programável, na qual dados que posteriormente serão utilizados para

realizar o processo de triangle traversal, são gerados e interpolados a partir dos dados

providos pelo estágio de geometria. Após, no estágio de triangle traversal, os pixels que

possuem seu centro contido por um triângulo, são marcados e um fragment é criado para eles.

Em seguida ocorre o estágio de pixel shading. Neste estágio programável, que recebe

como entrada dos dados das etapas anteriores interpolados, são executadas equações de

shading nos fragments, que produzirão valores de cor para os pixels que serão encaminhados

para etapas posteriores do pipeline. Nesta etapa são aplicadas técnicas como mapeamento de

textura em um objeto gráfico.

A última etapa do estágio de rasterização é denominada merging e nela os fragmentos

providos pela etapa anterior são combinados com os pixels presentes no color buffer (um

array retangular de valores de cor dos pixels a serem desenhados na tela). Nesta etapa ocorre

17

também a resolução de visibilidade. Para tal, geralmente se emprega o Z-Buffer (ou depth

buffer), que é um array com as mesmas dimensões do color buffer e guarda o valor de

coordenada Z de pixel em relação à câmera sintética.

O conceito de material é comumente empregado em aplicações gráficas que, de acordo

com Akenine-Möller, Haines e Hoffman (2008, p.104) tem a seguinte definição: a aparência

de um objeto gráfico é representada por agregar materiais a modelos na cena. Cada material é

associado com conjunto de shaders, texturas e outras propriedades para simular a interação de

luz com objetos. Outra técnica muito empregada em renderizadores gráficos é o occlusion

culling, definido por Akenine-Möller, Haines e Hoffman (2008, p.671) como uma técnica de

otimização utilizada para não renderizar objetos em uma cena que estão obstruídos por outros.

2.2 OPENGL E OPENGL MATHEMATICS (GLM)

OpenGL é uma API para o desenvolvimento de aplicações gráficas introduzida

originalmente em 1992, tornando-se a API gráfica com especificação aberta mais utilizada

para uma alta gama de aplicações gráficas (KHRONOS, 2016b). Em sua concepção, o

OpenGL é agnóstico em relação à plataforma e linguagem de programação.

Em sua especificação, OpenGL segue um modelo de estados globais, no qual qualquer

objeto (frame buffer, handle de texturas) pode ser adquirido por uma aplicação em tempo de

execução (KHRONOS, 2016b). O que torna a API mais acessível para desenvolvedores, uma

vez que funções providas pela API, como transformações geométricas por exemplo, não

precisam ser necessariamente implementadas.

OpenGL Mathematics é uma biblioteca matemática em forma header de C++ para

softwares gráficos baseados no OpenGL Shading Language (GLSL) (G-TRUC CREATION,

2016). A biblioteca está inclusa no SDK do Vulkan. São fornecidas nela, funções matemáticas

referentes a quaternions, transformações geométricas, matemática vetorial, entre outros.

2.3 VULKAN

Vulkan é uma API para desenvolvimento de aplicações gráficas aceleradas por

hardware mantida pelo grupo Khronos. Sua concepção se deu pela necessidade de uma API

gráfica que atendesse melhor o ecossistema moderno do mercado de aplicações gráficas.

Especificado em conjunto com os líderes da indústria da computação gráfica, para ser o novo

padrão de API gráficas com especificação aberta (KHRONOS, 2016c).

Em sua concepção, o Vulkan é mais explícito do que seu antecessor (OpenGL). Nele,

o desenvolvedor possui maior responsabilidade na utilização dos recursos de hardware, ao

18

invés de haver certos comandos abstraídos nos drivers ou em comandos fixos disponíveis pela

API. Como resultado, o Vulkan pode ser utilizado de forma que se adapta melhor a aplicação,

possivelmente assim, rendendo maior desempenho (KHRONOS, 2016c).

Alguns benefícios da especificação do Vulkan são comentados de acordo com Khronos

(2016c): introdução do conceito de command queue – fila de command buffers para serem

enviados para o dispositivo físico (GPU). A criação das command queues pode ser feita de

forma paralela e assíncrona utilizando-se diversas threads, o que otimiza a utilização da CPU.

Outra vantagem proposta em relação ao OpenGL, é que especificação do Vulkan permite que

aplicações sejam desenvolvidas para múltiplas plataformas sem que haja necessidade de

alterações específicas para cada uma.

A SPIR-V é uma linguagem intermediária desenvolvida pelo grupo Khronos para

representação nativa de shaders gráficos e kernels computacionais. Permite que shaders

possam ser compilados para formato binário antes da execução, o que cria alguns benefícios

como: depuração antes do tempo de execução, maior otimização e proteção de propriedade

intelectual. Os programas (shaders) podem ser escritos utilizando a mesma linguagem

empregue no OpenGL, o GLSL, mas pode-se também fazer uso do SDK para desenvolver um

compilador para linguagens de terceiros, que ao final precisam estar no formato binário do

SPIR-V (KHRONOS, 2016a).

O conceito de validation layers foi introduzido no SDK do Vulkan para auxiliar no

processo de depuração e verificação de conformidade com a especificação no

desenvolvimento de aplicações que utilizam o mesmo (KHRONOS, 2016c). As validation

layers podem ser empregadas durante o desenvolvimento de uma aplicação para reportar a

utilização de forma incorreta da API Vulkan. Fornece também recursos como árvore de

chamadas de funções da API para análise e depuração. Uma das vantagens propostas por essa

ferramenta é que ela pode ser desabilitada completamente da aplicação quando ela estiver

com o desenvolvimento concluído. Removendo assim qualquer tipo de overhead que poderia

ser causado pela API realizando validações internas para verificar a conformidade e validade

das chamadas de função.

Para apresentar os resultados de renderização para uma superfície ou tela utilizando

Vulkan faz-se necessária a utilização do objeto swap chain (KHRONOS, 2016e). Este objeto,

fornecido através de uma extensão, é uma abstração de um array com imagens associadas a

uma tela pronta para apresentação. A aplicação renderiza uma imagem que resulta em um

objeto VkImage, o qual é adicionado à fila do swap chain para futuramente ser apresentada

19

pelo presentation engine, que é responsável por ordenar quais imagens podem ser adquiridas

pela aplicação.

Na API Vulkan existem dois tipos de recursos para representar dados arbitrários na

memória da GPU: VkBuffer e VkImage. VkBuffers são containers para dados de forma

linear que podem ser usados de diversas formas – estruturas de dados, arrays “crus” e até

informações sobre imagens. VkImages, em contrapartida, são estruturadas, possuem tipo e

possuem dados sobre formato. Podem ser multidimensionais para formar arrays, e suportam

operações avançadas de leitura e escrita de dados (SELLERS; KESSENICH, 2016).

2.4 TRABALHOS CORRELATOS

Serão introduzidos um produto, um trabalho acadêmico e uma ferramenta educacional

que implementam o objeto de estudo a ser explorado por este trabalho. O primeiro é o motor

para aplicações multimídia com foco em jogos digitais Unreal Engine (EPIC GAMES,

2016b). O segundo é um trabalho que realiza um estudo exploratório de um renderizador 3D

de alta performance, o Vulkan Based Render Toolkit (MAINUŠ, 2016). O terceiro é um guia

em formato de tutorial para utilização da API Vulkan (OVERVOORDE, 2016).

2.4.1 UNREAL ENGINE

A Unreal Engine é um conjunto de ferramentas para desenvolvimento de aplicações

multimídia com alta fidelidade gráfica e alta performance mantidas pela EPIC Games (EPIC

GAMES, 2016c). Foi criado em 1998 e foi originalmente apresentado ao público com o jogo

digital Unreal (BLESZINSKI, 2010) e a partir da versão 4, a utilização do motor se tornou

gratuita e com código aberto, sendo cobrados apenas 5% da receita a partir de 3 mil dólares

(EPIC GAMES, 2016c).

A EPIC Games faz parte do grupo Khronos e participou ativamente na especificação

da API Vulkan. Portanto, foi uma das primeiras engines a implementar o seu renderer

backend utilizando Vulkan (SAMSUNG, 2016). A implementação da API Vulkan foi

introduzida de forma experimental na versão 4.12 da Unreal Engine, permitindo assim a

implementação de aplicações multimídia com suporte à Vulkan (EPIC GAMES, 2016b).

Em conjunto com a Samsung, a Epic Games desenvolveu uma aplicação de

demonstração para o Samsung Galaxy S7 chamada ProtoStar (Figura 2), utilizando Vulkan,

com o objetivo de demonstrar o potencial da API em plataformas mobile. Para atingir altos

níveis de fidelidade gráfica, foram implementadas diversas técnicas de computação gráfica

como (EPIC GAMES, 2016a):

20

a) reflexos planares dinâmicos (reflexos de alta qualidade para objetos dinâmicos);

b) simulação de partículas na GPU;

c) Temporal Anti-Aliasing (TAA);

d) compressão de texturas de alta qualidade;

e) Chromatic aberration;

f) refração dinâmica de luz para plataformas mobile;

g) suporte a cenas com milhares de objetos dinâmicos.

Figura 2 – Captura de tela da aplicação ProtoStar

Fonte: Epic Games (2016c).

Um dos recursos disponíveis chama-se command buffers, que é um objeto utilizado

para gravar comandos que podem subsequentemente ser enviados para a fila de um

dispositivo para execução (KHRONOS, 2016c). Para manter a taxa de quadros aceitável no

dispositivo Samsung Galaxy S7, a equipe da Unreal Engine empregou algumas técnicas,

como a utilização de somente 3 grandes command buffers, sendo utilizado somente um por

frame e alternando-se entre eles utilizando a técnica de Round Robin.

Akenine-Möller, Haines e Hoffman (2008, p.711) define GPU instancing como o

conceito de desenhar um objeto várias vezes com somente um draw call (comando de

desenho). Foram apontadas também algumas das vantagens da reutilização dos command

buffers, como em um caso de otimização simples, o GPU instancing, e em um caso de uso

ótimo, a renderização estereoscópica para Realidade Virtual (RV) (ARM, 2016).

De acordo com Khronos Group (2016d), semáforos são utilizados para coordenar

operações internas com filas externas a uma fila de comandos. Outra técnica de sincronização

é feita com a utilização de fences. Objetos deste tipo podem ser utilizadas pelo dispositivo

21

hospedeiro para determinar a completude da execução de uma fila de comandos. Para realizar

a sincronização e ordenação do envio dos comandos para a GPU, se utilizou semáforos para

comandos na GPU, e fences para a CPU, principalmente para verificação se um comando

arbitrário já foi completado pela GPU. Em conclusão, os autores afirmam ter atingido níveis

de fidelidade gráfica e performance no mesmo nível de consoles de sétima geração

(SAMSUNG, 2016).

2.4.2 Vulkan based render toolkit

Este trabalho tem como objetivo realizar um estudo exploratório da API Vulkan e

demonstrar novas funções disponíveis na mesma. Para tal, o autor desenvolveu em C++ um

render toolkit e uma aplicação para análise de performance (MAINUŠ 2016).

O render toolkit é formado por um módulo que trata do ciclo de vida da aplicação e o

gerenciamento de eventos (denominado core) e um módulo para renderização. De acordo

com Mainuš (2016), o módulo de renderização cria o objeto compositor, que é responsável

por criar os render passes e por percorrer a árvore de objetos gráficos presentes no grafo de

cena de uma cena, para fazer uma triagem e determinar quais objetos atendem algumas

restrições especificas para serem ordenadas na árvore. A ordenação é feita levando em

consideração objetos com contextos de renderização similares, ou seja, com características

como material e malhas iguais. Este processo está ilustrado na Figura 3.

22

Figura 3 – Processo de criação da árvore de renderização (Render tree)

Fonte: Mainuš (2016).

Após o processo ilustrado na Figura 3 a render tree é delegada para o render

worker que, por sua vez, divide as tarefas de renderização de maneira assíncrona através de

diversas threads. Inicialmente cada render component tem os dados de inicialização

preparados. Em seguida, os atributos referentes a um render component são enviados para a

memória da GPU. Na próxima etapa as informações de malha referentes aos materiais que

serão utilizados pelos objetos, são atribuídas ao pipeline para poderem ser reutilizados caso

uma malha seja repetida (GPU instancing). Por último, dados referentes à cena são agregados

ao pipeline. Este processo está ilustrado na Figura 4 (MAINUŠ, 2016).

23

Figura 4 – Processo de renderização da Render tree

Fonte: Mainuš (2016).

Para verificação de funcionalidades, o autor criou uma aplicação utilizando o render

toolkit e realizou testes divididos em 5 níveis de otimização, sendo eles:

a) no primeiro nível não é realizado nenhum tipo de otimização;

b) no segundo nível os trabalhos de renderização foram paralelizados com 5 threads;

c) no terceiro nível adicionou-se a técnica de utilização de staging buffers;

d) no quarto nível incorpora-se ao segundo nível o recurso de memory pools para pré-

alocar e reutilizar os recursos;

e) o quinto nível implementa todas as otimizações propostas anteriormente, mas faz

com que ocorram o mínimo possível de trocas de estados do pipeline.

Nas Tabelas 1 e 2 pode-se observar respectivamente o tempo de cada frame na CPU e

GPU em relação à quantidade de objetos presentes na cena.

24

Tabela 1 – Tempo (milissegundos) de um frame na CPU em relação à quantidade de objetos

1k 2k 3k 4k 5k 6k 7k

Level 0 6.2 9.1 14.8 16.7

Level 1 4.9 6.2 11.6 13.3 13.1 16.7

Level 2 6.5 7.4 27.9 49.7 46.9 42.0 37.0

Level 3 6.7 7.6 8.2 13.2 16.3 18.0 19.9

Level 4 8.8 8.7 11.9 16.9 19.8 22.9 25.3 Fonte: Mainuš (2016).

Tabela 2 – Tempo (milissegundos) de um frame na GPU em relação à quantidade de objetos

1k 2k 3k 4k 5k 6k 7k

Level 0 48.3 61.0 68.1 66.3

Level 1 49.4 61.2 67.8 64.3 63.7 63.2

Level 2 5.7 7.7 9.7 10.5 11.7 13.1 14.0

Level 3 5.5 7.5 9.3 10.0 10.6 11.0 11.9

Level 4 5.2 7.0 8.6 9.4 10.0 10.8 11.4 Fonte: Mainuš (2016).

2.4.3 Vulkan tutorial

Este trabalho é uma ferramenta educacional em formato de tutorial. Foi elaborado por

Alexander Overvoorde em 2016 com o intuito de ensinar o básico de utilização da API

Vulkan (OVERVOORDE, 2016). O trabalho é disponibilizado através de sua homepage e em

formato de E-Book, nos quais encontra-se um guia com os passos necessários para utilização

da API Vulkan. Além da parte textual explicando o funcionamento da API, é fornecido

também o código fonte dos exemplos utilizados. O tutorial está dividido em capítulos, os

quais estão elencados a seguir:

a) introduction;

b) overview;

c) development environment;

d) drawing a triangle;

e) vertex buffers;

f) uniform buffers;

g) texture mapping;

h) depth buffering;

i) loading models.

No capítulo de introdução o autor introduz os objetivos do tutorial, a API Vulkan e o

público alvo para o qual o tutorial foi elaborado. No capítulo seguinte, denominado Overview,

o autor descreve as origens da API Vulkan e em seguida elenca os passos necessários para

renderização de uma imagem utilizando a API. Em seguida, no capítulo de Development

environment, o autor descreve as bibliotecas auxiliares utilizadas e como instalá-las, além da

25

configuração dos ambientes de desenvolvimento para utilização do código fonte provido. Em

sequência, no capítulo intitulado Drawing a triangle, o autor descreve todos os passos e

técnicas de implementação necessárias para desenhar um triangulo e mostrá-lo na tela em

Vulkan. No capítulo seguinte, Vertex buffers, é descrito como é possível generalizar dados

referentes aos vértices para desenho de objetos gráficos e como fazer o gerenciamento de

memória necessário para tal.

No capítulo Uniform buffers, o autor descreve como utilizar os resource descriptors

para enviar variáveis globais para os shaders. No capítulo seguinte intitulado Texture

mapping, o autor entra em detalhes de como utilizar a técnica de mapeamento de texturas em

malhas 3D. No penúltimo capítulo, intitulado Depth buffering, o autor aponta como introduzir

o elemento de profundidade no eixo Z do espaço cartesiano, na qual se faz uso do depth buffer

para guardar tais informações e criar impressão de terceira dimensão. Por último, no capítulo

Loading models, o autor introduz o carregamento de malhas 3D utilizando a biblioteca

Tinyobj, o qual está ilustrado na captura de tela da Figura 5. Após a última etapa do tutorial se

tem uma aplicação que renderiza uma malha 3D no formato OBJ com mapeamento de textura

e modelo de iluminação Blinn-Phong com Vulkan.

Figura 5 – Captura de tela do Vulkan Tutorial

Fonte: elaborado pelo autor.

26

3 DESENVOLVIMENTO DA BIBLIOTECA

Neste capítulo são demonstradas as etapas do desenvolvimento da biblioteca. Na seção

3.1 são apresentados os requisitos funcionais e não funcionais da biblioteca. A seção 3.2

demonstra a especificação da biblioteca. A seção 3.3 apresenta de forma explicativa a

implementação da biblioteca de renderização. Por fim, a seção 3.4 apresenta os resultados dos

testes.

3.1 REQUISITOS

A biblioteca desenvolvida deve permitir:

a) renderizar cenas compostas por diversos objetos gráficos em 3D (Requisito

Funcional - RF);

b) ser implementada utilizando o pipeline gráfico programável para aplicação de

shaders (RF);

c) utilizar a API Vulkan (Requisito Não Funcional - RNF);

d) ser implementada utilizando a linguagem C++ (RNF);

e) utilizar a biblioteca OpenGL Mathematics (GLM) para funções matemáticas

(RNF).

3.2 ESPECIFICAÇÃO

A biblioteca foi especificada utilizando-se da Unified Modeling Language (UML),

utilizando as ferramentas Enterprise Architect e Microsoft Visual Studio

Community 2015.

3.2.1 Diagrama de atividades

Na Figura 6 pode-se observar um diagrama de atividades ilustrando o processo de

renderização de uma cena, primeiramente com a inicialização e criação de contexto da API

Vulkan e objetos necessários para renderização que serão utilizados posteriormente.

27

Figura 6 – Diagrama de atividade (Vulkan)

Fonte: elaborado pelo autor.

Após a criação de uma cena e a confecção dos objetos gráficos com suas malhas e

texturas se inicia o loop principal da aplicação. Nele, primeiramente ocorre a atualização dos

UniformBuffer dos objetos gráficos, os quais contém informações sobre transformações

geométricas e posição da fonte de luz em espaço global, e a gravação de command buffers de

desenho. Em sequência a função drawFrame, a qual busca uma imagem válida para ser

28

desenhada e envia a fila de comandos é chamada e invoca o processo de renderização na

GPU. Este processo ocorre até que o evento para fechamento de janela seja recebido.

3.2.2 Diagrama de classes

Na Figura 7 está presente o diagrama de classe apresentando a modelagem do

renderizador utilizando Vulkan. A principal classe a ser notada é a RendererVk a qual

entende a classe abstrata Renderer e serve como principal gerenciador do processo de

renderização. A classe Scene tem como propósito conter os objetos gráficos (DrawableObj)

que serão renderizados na cena. Tais objetos gráficos na implementação com Vulkan são

representados pela classe VulkanDrawableObj os quais estendem a classe abstrata

DrawableObj. Os objetos do tipo VulkanDrawableObj possuem um ponteiro para as classes

VulkanMesh e VulkanMaterial as quais representam o aspecto visual do objeto. Objetos do

tipo VulkanMesh possuem uma lista dos vértices e índices dos mesmos que compõem uma

malha 3D. Já os objetos do tipo VulkanMaterial representam o conceito de material e por

sua vez, contém referência para objetos do tipo VulkanShader e VulkanTexture. A classe

VulkanDevice serve como abstração do dispositivo que será utilizado para renderização,

contendo o dispositivo lógico, físico e filas de comandos que o pertencem. A classe

VulkanSwapChain funciona como abstração do swap chain do Vulkan, contendo os objetos e

funções necessárias para manipulação da mesma. Além das classes apresentadas, outras

classes implementadas no trabalho serão apresentadas com maior detalhamento na seção 3.3.

29

Figura 7 - Diagrama de classes (Vulkan)

Fonte: elaborado pelo autor.

30

3.3 IMPLEMENTAÇÃO

Neste capítulo são mostradas as técnicas, ferramentas e a operacionalidade da

implementação. A seção 3.3.1 apresenta o detalhamento das ferramentas e as técnicas

utilizadas. A seção 3.3.2 demonstra o processo operacional da biblioteca.

3.3.1 Técnicas e ferramentas utilizadas

A IDE Microsoft Visual Studio Community 2015 foi utilizada para o desenvolvimento

da biblioteca, e a mesma foi desenvolvida na linguagem C++. Foram utilizadas as seguintes

tecnologias no desenvolvimento da biblioteca:

a) Vulkan SDK: SDK com o header fornecendo a implementação do Vulkan;

b) GLEW: para carregamento da API OpenGL;

c) GLFW: gerenciador de janelas multi-plataforma;

d) GLM: biblioteca para operações matemáticas;

e) Tinyobj: biblioteca para carregamento de malhas 3D no formato OBJ;

f) STP image: biblioteca para carregamento de imagens.

A seguir tem-se o detalhamento das técnicas utilizadas para implementação da

biblioteca de renderização. Na Figura 8 pode-se observar o fluxo principal de execução da

biblioteca.

Figura 8 – Fluxo principal da biblioteca

Fonte: elaborado pelo autor.

O detalhamento das técnicas de implementação está separado em seções e será

apresentado o código fonte. Nota-se que a ênfase por um maior detalhamento para os detalhes

de implementação foi dada para a API Vulkan, por este ser um trabalho exploratório dessa

API. A próxima seção descreve o processo de inicialização das APIs gráficas.

3.3.1.1 INICIALIZAÇÃO DAS APIS GRÁFICAS

Esta seção compreende a etapa de inicialização das APIs gráficas e seus respectivos

objetos que se fazem necessários para a utilização das mesmas. Primeiramente serão

apresentadas as etapas de inicialização da API Vulkan, e em sequência as da API OpenGL.

A seguir estão descritos os passos necessários para inicializar a API Vulkan e preparar

o contexto com os objetos necessários para realizar o processo de renderização de uma cena.

31

No ANEXO A é possível se ter uma visualização gráfica geral dos objetos que serão tratados

a seguir.

Para o desenvolvimento com Vulkan utilizou-se uma versão modificada e adaptada de

Overvoorde (2016). Os tipos de objetos que devem ser inicializados em ordem estão

elencados a seguir:

a) instância, physical device;

b) logical device, queue families;

c) window surface, swap chain;

d) image views, frame buffers;

e) render passes;

f) graphics pipeline;

g) command pool, command buffers;

h) presentation.

Para se utilizar a API Vulkan faz-se necessária a criação de um objeto vkInstance, o

qual possui como propósito servir de instância operante da API e separar o estado de outras

aplicações executando Vulkan. A partir dele é possível realizar operações subsequentes com a

biblioteca, como elencar os dispositivos com os quais a API pode se comunicar e verificar

quais extensions e layers estão disponíveis no dispositivo.

Em sua criação através da função vkCreateInstance (Quadro 1) deve ser

especificado um struct de configuração do tipo VkInstanceCreateInfo que descreve

informações sobre a instância a ser inicializada como quais extensions serão utilizadas, o

nome da aplicação e sua versão. Todas as funções de criação de objetos em Vulkan seguem

esse padrão, com structs de configuração e especificação do objeto a ser instanciado.

32

Quadro 1 – inicialização de instância Vulkan 1. void RendererVk::initVulkan() 2. { 3. if (enableValidationLayers && !VulkanHelper::checkValidationLayerSupport()) 4. throw std::runtime_error("validation layers error"); 5. 6. VkApplicationInfo applicationInfo = 7. { 8. VK_STRUCTURE_TYPE_APPLICATION_INFO, // VkStructureType sType 9. nullptr, // const void *pNext 10. "vkFurb", // const char *pApplicationName 11. VK_MAKE_VERSION(1, 0, 0), // uint32_t applicationVersion 12. "No Engine", // const char *pEngineName 13. VK_MAKE_VERSION(1, 0, 0), // uint32_t engineVersion 14. VK_API_VERSION_1_0 // uint32_t apiVersion 15. }; 16. VkInstanceCreateInfo createInfo = {}; 17. createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; 18. createInfo.pApplicationInfo = &applicationInfo; 19. auto extensions = getRequiredExtensions(); 20. createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); 21. createInfo.ppEnabledExtensionNames = extensions.data(); 22. // inicializar com ValidationLayers para receber mensagens de depuracao 23. if (enableValidationLayers) 24. { 25. createInfo.enabledLayerCount= static_cast<uint32_t>(validationLayers.size()); 26. createInfo.ppEnabledLayerNames = validationLayers.data(); 27. } 28. else 29. { 30. createInfo.enabledLayerCount = 0; 31. } 32. if (vkCreateInstance(&createInfo, nullptr, &_vkInstance) != VK_SUCCESS) 33. throw std::runtime_error("ERRO AO INICIALIZAR VkInstance"); 34. 35. if (enableValidationLayers) 36. setupDebugCallback(); 37. }

Fonte: elaborado pelo autor.

Após a inicialização de instância do Vulkan, a função vkEnumeratePhysicalDevices

pode ser utilizada para buscar dispositivos físicos (como GPUs) que são compatíveis com

Vulkan. Primeiramente chama-se a função com o terceiro argumento com valor nullptr,

para retornar o número de dispositivos físicos, então chama-se novamente a função com um

vetor para receber os objetos concretos. Após buscar os dispositivos físicos disponíveis que

suportam Vulkan, pode-se elencar quais dispositivos serão os mais adequados para a

utilização. Para tal, as funções vkGetPhysicalDeviceProperties e

vkGetPhysicalDeviceFeatures podem ser chamadas para verificação de aspectos como o

layout de memória do dispositivo ou se o mesmo suporta recursos como geometry shaders,

entre outros. Após escolhido o dispositivo, deve-se instanciar um (ou mais) objeto(s) do tipo

vkPhysicalDevice, o qual é responsável por gerenciar o acesso e a verificação de recursos

disponíveis através de dispositivos físicos. O processo descrito para adquirir um

33

vkPhysicalDevice pode ser visualizado no código C++ no Quadro 2, onde se abstraiu o

código de Overvoorde (2016) na função getPhysicalDevice.

Quadro 2 – Busca de um dispositivo físico (RendererVk.cpp:259) 1. bool RendererVk::getPhysicalDevice() 2. { 3. uint32_t deviceCount = 0; 4. vkEnumeratePhysicalDevices(_vkInstance, &deviceCount, nullptr); 5. 6. if (deviceCount == 0) 7. throw std::runtime_error("failed to find GPUs with Vulkan support!"); 8. 9. std::vector<VkPhysicalDevice> devices(deviceCount); 10. vkEnumeratePhysicalDevices(_vkInstance, &deviceCount, devices.data()); 11. 12. for (const auto& device : devices) { 13. if (isDeviceSuitable(device)) { 14. vkPhysicalDevice = device; 15. break; 16. } 17. } 18. 19. if (vkPhysicalDevice == VK_NULL_HANDLE) 20. throw std::runtime_error("failed to find a suitable GPU!"); 21. }

Fonte: adaptado de Overvoorde (2016).

Para exibir imagens em uma janela utilizando Vulkan, faz-se necessário utilizar

alguma de suas extensões Window System Integration (WSI), pois como o Vulkan é

agnóstico de plataforma, não suporta em seu núcleo a integração com janelas de sistemas

operacionais específicos. Então deve-se se obter um objeto do tipo VkSurfaceKHR que atua

como “superfície de renderização” na qual as imagens geradas irão ser apresentadas. Para tal

pode-se chamar a função vkCreateWin32SurfaceKHR do Vulkan que recebe como

argumento um struct de configuração, ou pode-se utilizar uma função

(glfwCreateWindowSurface) provida pelo gerenciador de janelas GLFW, a qual não necessita

de um struct de configuração e é multi-plataforma. A utilização de um VkSurfaceKHR é

opcional, pois pode-se utilizar a API Vulkan para renderizar imagens sem exibi-las em uma

janela.

A partir de um objeto do tipo VkPhysicalDevice, pode-se instanciar uma ou mais

instâncias de objetos do tipo VkDevice, conforme necessário de acordo com a aplicação

sendo desenvolvida, este processo pode ser visualizado no Quadro 3. Objetos desse tipo

atuam como dispositivo lógico, realizando a conexão entre a aplicação e o dispositivo sendo

utilizado.

A maioria dos recursos e objetos necessários para efetuar operações subsequentes com

a API necessitam como argumento em sua função de criação uma referência a um VkDevice.

34

Tais recursos incluem a criação de objetos como: VkQueues, VkImage, VkBuffer,

VkFramebuffer, VkRenderPass, VkPipeline, VkCommandBuffer, entre outros, os quais

serão introduzidos a seguir.

A comunicação entre o dispositivo físico e a aplicação se dá através de operações que

são gravadas em command buffers e enviadas para VkQueues de famílias específicas. Tais

famílias são denominadas queue families e são filas de comandos com finalidades específicas,

por exemplo, uma fila somente para renderização, outra somente para utilizar a GPU para

computação (sem renderização) ou ainda outra somente para a apresentação de imagens após

a renderização (present). A verificação de quais filas gráficas estão disponíveis no dispositivo

físico se dá através da função findQueueFamilies, como pode ser averiguado no Quadro 3,

onde se abstraiu o código de Overvoorde (2016) na função getLogicalDevice.

35

Quadro 3 – Atribuição das queues e dispositivo lógico (RendererVk.cpp:283) 1. bool RendererVk::getLogicalDevice() 2. { 3. QueueFamilyIndices indices =

VulkanHelper::findQueueFamilies(vkPhysicalDevice, _vkSurfaceKHR); 4. 5. std::vector<VkDeviceQueueCreateInfo> queueCreateInfos; 6. std::set<int> uniqueQueueFamilies = { indices.graphicsFamily,

indices.presentFamily }; 7. 8. float queuePriority = 1.0f; 9. for (int queueFamily : uniqueQueueFamilies) { 10. VkDeviceQueueCreateInfo queueCreateInfo = {}; 11. queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; 12. queueCreateInfo.queueFamilyIndex = queueFamily; 13. queueCreateInfo.queueCount = 1; 14. queueCreateInfo.pQueuePriorities = &queuePriority; 15. queueCreateInfos.push_back(queueCreateInfo); 16. } 17. 18. VkPhysicalDeviceFeatures deviceFeatures = {}; 19. deviceFeatures.samplerAnisotropy = VK_TRUE; 20. 21. VkDeviceCreateInfo createInfo = {}; 22. createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; 23. createInfo.queueCreateInfoCount =

static_cast<uint32_t>(queueCreateInfos.size()); 24. createInfo.pQueueCreateInfos = queueCreateInfos.data(); 25. createInfo.pEnabledFeatures = &deviceFeatures; 26. createInfo.enabledExtensionCount =

static_cast<uint32_t>(deviceExtensions.size()); 27. createInfo.ppEnabledExtensionNames = deviceExtensions.data(); 28. 29. if (vkCreateDevice(vkPhysicalDevice, &createInfo, nullptr, &vkDevice) !=

VK_SUCCESS) { 30. throw std::runtime_error("failed to create logical device!"); 31. } 32. 33. vkGetDeviceQueue(vkDevice, indices.graphicsFamily, 0, &graphicsQueue); 34. vkGetDeviceQueue(vkDevice, indices.presentFamily, 0, &presentQueue); 35. }

Fonte: adaptado de Overvoorde (2016).

A aquisição de imagens que servirão de container para imagens renderizadas e

posteriormente apresentação na tela, é feita através de um objeto do tipo swap chain. O

swap chain é constituído basicamente por uma fila de imagens (VkImage). Pode ser concebido

como contendo o front e back buffer (se estiver operando em modo double buffering), mas

não é necessariamente análogo pois contém mais informações.

Para efetuar a renderização deve-se buscar uma imagem no swap chain, renderizar

nela e depois devolvê-la para o swap chain e para a fila de imagens que podem ser adquiridas

para apresentação na tela. Não há necessidade de criar ou alocar memória para as imagens do

swap chain, pois é um dos poucos objetos em que o Vulkan realiza o gerenciamento de

memória automático implícito. Portanto, como a alocação de memória e feita pela API, ela

não fornece acesso direto às imagens (VkImage) contidas nela. Para acessar o conteúdo das

36

imagens faz-se necessária a criação de objetos VkImageView que atuam como uma forma de

“visão” para a imagem, com permissão somente de leitura, que serão posteriormente

utilizados nos frame buffer. A criação de tal objeto se dá pela especificação de um struct de

configuração contendo informações como a quantidade de imagens que serão utilizadas, a

superfície de renderização e suas qualidades e as filas de comandos que serão utilizadas

(Quadro 4).

Nota-se que em OpenGL o conceito de swap chain não existia e o gerenciamento de

frame buffers era feito de forma implícita pela API, e era necessário somente chamar a função

glSwapBuffers para trocar as imagens do front e back buffers.

Quadro 4 – Configuração do swap chain (RendererVk.cpp:354) 1. ... 2. VkSwapchainCreateInfoKHR createInfo = {}; 3. createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; 4. createInfo.surface = _vkSurfaceKHR; 5. createInfo.minImageCount = imageCount; 6. createInfo.imageFormat = surfaceFormat.format; 7. createInfo.imageColorSpace = surfaceFormat.colorSpace; 8. createInfo.imageExtent = extent; 9. createInfo.imageArrayLayers = 1; 10. createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; 11. 12. QueueFamilyIndices indices = VulkanHelper::findQueueFamilies(vkPhysicalDevice,

_vkSurfaceKHR); 13. uint32_t queueFamilyIndices[] = {indices.graphicsFamily, indices.presentFamily }; 14. 15. if (indices.graphicsFamily != indices.presentFamily) { 16. createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; 17. createInfo.queueFamilyIndexCount = 2; 18. createInfo.pQueueFamilyIndices = queueFamilyIndices; 19. } 20. else { 21. createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; 22. } 23. 24. createInfo.preTransform = swapChainSupport.capabilities.currentTransform; 25. createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; 26. createInfo.presentMode = presentMode; 27. createInfo.clipped = VK_TRUE; 28. createInfo.oldSwapchain = VK_NULL_HANDLE; 29. 30. if (vkCreateSwapchainKHR(vkDevice, &createInfo, nullptr, &_swapChain.swapChain) !=

VK_SUCCESS) 31. throw std::runtime_error("failed to create swap chain!"); 32. ...

Fonte: adaptado de Overvoorde (2016).

Outro objeto necessário para realizar uma operação de renderização em Vulkan, é o

VkRenderPass, o qual descreve um render pass. Um render pass em Vulkan consiste de

alguns attachments, imagens artefatos de renderização, como depth, color e stencil. Nele é

descrita a maneira que estas imagens serão utilizadas durante o processo de renderização. No

caso da biblioteca desenvolvida, existe somente um sub pass com os attachments de color e

depth, que definem a cor e profundidade de um fragment em uma imagem. Para rotinas

37

gráficas mais complexas pode-se utilizar de mais de um sub pass como por exemplo efetuar

um desfoque, em que se teria uma imagem da cena renderizada em um sub pass (como é feito

nesta biblioteca) e em outro se aplicaria o desfoque com o produto do render pass anterior,

combinando o resultado de acordo com uma função de blending.

Os attachments do render pass devem ser manipulados por objetos do tipo

VkFramebuffer, os quais contém as VkImageViews dos mesmos. Para cada imagem no swap

chain faz-se necessário um VkFramebuffer equivalente. O processo de especificação de um

render pass pode ser visualizado no código em C++ no Quadro 5, onde se abstraiu o código

de criação de render pass de Overvoorde (2016) para uma função da classe RendererVk.

Quadro 5 – Configuração do render pass (RendererVk.cpp:501) 1. ... 2. std::array<VkAttachmentDescription, 2> attachments = { colorAttachment,

depthAttachment }; 3. VkRenderPassCreateInfo renderPassInfo = {}; 4. renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; 5. renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size()); 6. renderPassInfo.pAttachments = attachments.data(); 7. renderPassInfo.subpassCount = 1; 8. renderPassInfo.pSubpasses = &subpass; 9. renderPassInfo.dependencyCount = 1; 10. renderPassInfo.pDependencies = &dependency; 11. 12. if (vkCreateRenderPass(vkDevice, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) 13. throw std::runtime_error("failed to create render pass!"); 14. ...

Fonte: adaptado de Overvoorde (2016).

A configuração da maneira na qual o pipeline gráfico será utilizado para renderização é

definida em outro objeto necessário no processo de renderização, o VkPipeline (Quadro 6).

Nota-se que o VkPipeline é criado e atribuído antes do processo de renderização ser iniciado,

portanto se algum aspecto (alteração de shader por exemplo) precisar ser alterado em tempo

de execução ele deve ser recriado. Somente alguns aspectos podem sofrer alteração sem

necessidade de reinicialização de um VkPipeline, como o tamanho de viewport e o line

width.

Os aspectos que devem ser definidos no VkPipeline são:

a) os shaders que serão utilizados;

b) o formato dos vértices que serão submetidos (se haverá descrição de informações

relativas a cor ou vetor normal, por exemplo);

c) a descrição da viewport (em que região do frame buffer a imagem será renderizada

(posição mínima, até posição máxima));

d) a descrição do rasterizer (descreve a forma que os vértices fornecidos serão

transformados em fragments que irão alimentar o estágio de fragment shader do

38

pipeline);

e) se haverá utilização de Multisample Anti-Aliasing;

f) o tipo de color blending que será utilizado;

g) o pipeline layout;

h) o render pass;

i) se haverá subpasses;

j) se sua criação vai ser baseada em um VkPipeline existente (base pipeline);

k) e se os fragments podem ser descartados no depth test.

Quadro 6 – Configuração de um VkPipeline (RendererVk.cpp:632) 1. ... 2. VkGraphicsPipelineCreateInfo pipelineInfo = {}; 3. pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; 4. pipelineInfo.stageCount = 2; 5. pipelineInfo.pStages = shaderStages; 6. pipelineInfo.pVertexInputState = &vertexInputInfo; 7. pipelineInfo.pInputAssemblyState = &inputAssembly; 8. pipelineInfo.pViewportState = &viewportState; 9. pipelineInfo.pRasterizationState = &rasterizer; 10. pipelineInfo.pMultisampleState = &multisampling; 11. pipelineInfo.pColorBlendState = &colorBlending; 12. pipelineInfo.layout = pipelineLayout; 13. pipelineInfo.renderPass = renderPass; 14. pipelineInfo.subpass = 0; 15. pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; 16. pipelineInfo.pDepthStencilState = &depthStencil; 17. ...

Fonte: adaptado de Overvoorde (2016).

Na utilização de shaders em Vulkan faz-se necessário compilar o código em GLSL

para o formato binários SPIR-V. O SDK fornecido pela LunarG possui um executável que

realiza esta conversão, o qual foi utilizado neste trabalho como se pode ver no Quadro 7.

Quadro 7 – Conversão de shader GLSL para SPIR-V (compile shaders.bat)

1. C:/VulkanSDK/1.0.42.1/Bin32/glslangValidator.exe -V shader_base.vert

2. C:/VulkanSDK/1.0.42.1/Bin32/glslangValidator.exe -V shader_base.frag

Fonte: elaborado pelo autor.

Além da compilação para formato binário do shaders, para poderem ser utilizados no

VkPipeline faz-se necessário criar objetos do tipo VkShaderModule, que atuam como a

representação em Vulkan de um shader. Porém, os VkShaderModule somente representam os

programas shader de forma separada e isolada do resto de pipeline, portanto, para poderem

ser utilizados deve-se criar objetos do tipo VkPipelineShaderStageCreateInfo para

especificar em qual estágio do pipeline gráfico o shader deve ser utilizado (vertex ou

fragment por exemplo) e o ponto de entrada do programa (geralmente “main”). Os

39

VkPipelineShaderStageCreateInfo devem ser atribuídos à propriedade “pStages” do

struct de configuração do VkPipeline, como pode ser visto no Quadro 6.

O envio de informações para os shaders é feito através da utilização de descriptors.

Através deles, os shaders conseguem acessar recursos arbitrários, como uniform buffers

contendo transformações geométricas no estágio de vertex ou texture samplers no estágio de

fragment, por exemplo. Os descriptors são representados por objetos do tipo

VkDescriptorSet, os quais são alocados e sua memória gerenciada por um objeto do tipo

VkDescriptorPool. Cada binding de um shader deve ser descrito em um objeto do tipo

VkDescriptorSetLayout.

Comandos em Vulkan não são chamadas de função como eram em OpenGL. Para

enviar comandos para a GPU em Vulkan faz-se necessária a utilização de command buffers.

Eles são constituídos por uma fila de comandos que serão enviados para uma GPU somente

quando a função de vkQueueSubmit for chamada. Command buffers são alocados a partir de

command pools, as quais gerenciam a memória dos mesmos.

A última etapa de inicialização antes de se poder renderizar algo, é instanciar

semáforos do pipeline para sincronização entre operações desencadeados por command

buffers. Semáforos em Vulkan são representados por objetos do tipo VkSemaphore, e na

implementação deste trabalho foram utilizados 2 semáforos: um que é liberado quando uma

imagem está disponível para ser buscada no swap chain, e outro para sinalizar quando o

processo de renderização foi concluído e uma imagem está pronta para ser apresentada. O

código de inicialização dos semáforos pode ser visto no Quadro 8, onde se abstraiu o código

de criação de semáforos de Overvoorde (2016) para uma função da classe RendererVk.

Quadro 8 – Configuração dos semáforos (RendererVk.cpp:632) 1. bool RendererVk::createSemaphores() 2. { 3. VkSemaphoreCreateInfo semaphore_create_info; 4. semaphore_create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; 5. semaphore_create_info.pNext = nullptr; 6. semaphore_create_info.flags = 0; 7. 8. if ((vkCreateSemaphore(vkDevice, &semaphore_create_info, nullptr,

&_imageAvailableSemaphore) != VK_SUCCESS) || 9. (vkCreateSemaphore(vkDevice, &semaphore_create_info, nullptr,

&_renderingFinishedSemaphore) != VK_SUCCESS)) { 10. std::cout << "Could not create semaphores!" << std::endl; 11. return false; 12. } 13. 14. return true; 15. }

Fonte: adaptado de Overvoorde (2016).

40

Após a realização de todos os passos até aqui citados e a inicialização de todos os

objetos necessários para utilização da API Vulkan, pode-se realizar o carregamento de cenas

para depois efetuar a renderização. Tais processos serão explicados nas seções 3.3.1.2 e

3.3.1.3.

A inicialização do OpenGL se faz com muito menos complexidades se comparada com

Vulkan. Primeiramente foi necessário inicializar o gerenciador de janelas GLFW definindo a

versão do OpenGL que será utilizada (neste trabalho versão 4.5) e definindo o contexto para a

janela criada (Quadro 9).

Quadro 9 – Inicialização do GLFW para OpenGL (RendererGL.cpp:30) 1. void RendererGL::initGLFW() 2. { 3. glfwInit(); 4. 5. glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); 6. glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5); 7. glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 8. glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 9. 10. glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); 11. 12. _glfwWindow = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL", nullptr, nullptr); 13. glfwMakeContextCurrent(_glfwWindow); 14. }

Fonte: elaborado pelo autor.

A inicialização do OpenGL em si é realizada através do GLEW chamando a função

glewInit para inicialização da API. Além disso fez-se necessário definir alguns estados da

API como a função de blending que será utilizada, habilitar o depth test e habilitar o culling

de faces (Quadro 10).

Quadro 10 – Inicialização do OpenGL (RendererGL.cpp:52) 1. void RendererGL::initOpenGL() 2. { 3. glEnable(GL_BLEND); 4. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 5. 6. glEnable(GL_DEPTH_TEST); 7. glDepthFunc(GL_LESS); 8. 9. glEnable(GL_CULL_FACE); 10. 11. glewExperimental = GL_TRUE; 12. glewInit(); 13. }

Fonte: elaborado pelo autor.

Após a realização dos passos citados pode-se iniciar o processo de carregamento de

cenas e posteriormente sua renderização em OpenGL. Na seção seguinte será demonstrado o

processo de carregamento de cenas.

41

3.3.1.2 CARREGAMENTO DE CENAS

Nesta seção será descrito como ocorre o carregamento de objetos que compõe as cenas

nesta biblioteca. Primeiramente serão apresentadas as etapas do carregamento em Vulkan e na

sequência em OpenGL. O processo de carregamento das cenas consiste basicamente de:

a) carregar as malhas dos objetos gráficos;

b) carregar as texturas e shaders que serão utilizadas nos materiais.

Após a inicialização dos objetos citados até o momento, a estrutura para renderizar

cenas em Vulkan está pronta. As malhas em Vulkan são representadas por objetos do tipo

VkBuffer contendo os dados referentes aos vértices especificados. Possuem também um

VkBuffer contendo os índices de tais vértices para renderização indexada. Para cada

VkBuffer utilizado precisa-se de um objeto do tipo VkDeviceMemory para manter-se os

dados dos buffers em memória acessível pela CPU.

Utilizou-se da biblioteca Tinyobj para carregar arquivos de malhas 3D no formato OBJ

para a biblioteca. Para tal a função LoadObj do Tinyobj é utilizada para ler o arquivo e

retornar listas com posição de vértices, vetor normal e coordenadas de textura (UV). Nota-se

que nas coordenadas de textura deve-se inverter o valor de y (ou V), pois em Vulkan o valor

de y em coordenadas de tela tem sinal inverso em relação ao OpenGL.

As texturas em Vulkan foram representadas por objetos do tipo VkImage, os quais

contêm as informações de como a imagem deve ser tratada pela API. Utilizou-se a biblioteca

STP image para realizar o carregamento de imagens, as quais são carregadas arrays de bytes

que irão popular um VkBuffer. Após o carregamento a criação de um VkBuffer deve-se

converter o VkBuffer em um VkImage, o que pode ser feito com o comando

vkCmdCopyBufferToImage. Após a criação do VkImage deve-se criar um objeto do tipo

VkImageView, pois a implementação de Vulkan não permite que VkImages sejam acessadas

diretamente. Para que texturas possam ser acessadas por shaders deve-se criar objetos

samplers para que as texturas sejam acessada através deles. Tais objetos controlam aspectos

como filtros e transformações nas imagens, como por exemplo, anisotropia e o modo de

repetição da imagem. A representação desse tipo de objeto em Vulkan faz-se com objetos do

tipo VkSampler os quais são criados com o comando vkCreateSampler, e como todos os

outros objetos em Vulkan, requer um struct de configuração para sua concepção.

A representação de malhas 3D em OpenGL é feita através de objetos Vertex Buffer

Object (VBO), Vertex Array Object (VAO) e Element Buffer Object (EBO). O objeto VBO é criado

a partir da lista vértices carregados utilizando a biblioteca Tinyobj e é utilizado para enviar os

42

dados referentes aos vértices de uma malha para a memória da GPU. O VAO é responsável por

descrever o estado referente a vértices ou outros atributos que servirão de input para o shader

utilizado para desenhar um objeto gráfico. No caso deste trabalho, descreve como acessar as

coordenadas de um vértice, as coordenadas de textura, as coordenadas de vetor normal e a

cor. Para realizar a renderização indexada faz-se a utilização de um objeto do tipo EBO.

Texturas na implementação com OpenGL também são carregadas utilizando a

biblioteca STP Image. A mesma carrega as imagens do disco para formato de array de bytes,

os quais são fornecidos para a função do OpenGL glTexImage2D a qual gera a textura na

memória de vídeo.

O carregamento de shaders é feito lendo os arquivos que contém seus programas e

enviando seu conteúdo para ser compilado pelo OpenGL através da função

glCompileShader.

3.3.1.3 RENDERIZAÇÃO

Nas seções a seguir serão discutidas as etapas necessárias para renderizar uma imagem

após a configuração de cena. Primeiramente serão mostradas as etapas e detalhes de

implementação em Vulkan e em sequência as do OpenGL.

O processo de renderização em Vulkan ocorre somente após a realização das etapas de

inicialização descritas nas seções anteriores. As etapas que são necessárias para renderização

de uma imagem estão listados a seguir:

a) gravação de command buffers;

b) adquirir uma imagem válida do swap chain;

c) enviar os command buffers posteriormente gravados para a fila gráfica;

d) mostrar a imagem na tela com a chamada de função vkQueuePresentKHR.

A primeira etapa para desenhar uma imagem na tela em Vulkan é gravar os command

buffers de desenho. Para cada command buffer utilizado (no caso deste trabalho são 3, pois a

técnica de triple-buffering está sendo empregada), deve-se inicializar a gravação através da

chamada função vkBeginCommandBuffer passando o command buffer alvo da gravação

como argumento. Em seguida deve-se atribuir o render pass elaborado anteriormente, para tal

chama-se a função vkCmdBeginRenderPass com um objeto um struct de configuração

especificando o VkRenderPass que será utilizado. Na sequência, para cada objeto gráfico a

ser renderizado na cena, deve-se realizar as seguintes operações:

43

a) atribuir o descriptor set correspondente aos atributos que serão enviados ao shader

que será utilizado para desenhar tal objeto;

b) atribuir o vertex buffer contendo os vértices da malha 3D;

c) atribuir o index buffer;

d) atribuir o VkPipeline;

e) enviar as push constants (opcional, no caso deste trabalho é utilizado para enviar o

uniform buffer para o shader);

f) executar o comando de desenho indexado (vkCmdDrawIndexed).

Após a determinação dos comandos para cada objeto, deve-se sinalizar que não haverá

mais comandos referentes ao render pass com a função vkCmdEndRenderPass. Em seguida,

para finalizar a gravação em um command buffer, chama-se a função vkEndCommandBuffer.

O processo de gravação de command buffers é demonstrado no pseudocódigo do Quadro 11.

Quadro 11 – Pseudocódigo de gravação dos command buffers 1. for (size_t i = 0; i < commandBuffers.size(); i++) 2. { 3. (...)

4. vkBeginCommandBuffer(commandBuffers[i], &beginInfo);

5. (...)

6. vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, (...));

7. for (const auto &obj : scene->getSceneGraph())

8. {

9. vkCmdBindDescriptorSets(commandBuffers[i], (...));

10. (...)

11. vkCmdBindVertexBuffers(commandBuffers[i], (...));

12. vkCmdBindIndexBuffer(commandBuffers[i], (...));

13. vkCmdBindPipeline(commandBuffers[i], (...));

14. vkCmdPushConstants(commandBuffers[i], (...));

15. vkCmdDrawIndexed(commandBuffers[i], (...));

16. }

17. vkCmdEndRenderPass(commandBuffers[i]);

18. vkEndCommandBuffer(commandBuffers[i]);

19. }

Fonte: elaborado pelo autor.

Com os command buffers devidamente gravados, pode-se iniciar os procedimentos

finais para renderizar uma imagem e apresentá-la na tela. Primeiramente, faz-se necessário

adquirir uma imagem válida para utilização do swap chain. Para tal, chama-se a função

vkAcquireNextImageKHR, a qual retorna o índice de uma imagem do swap chain que pode

ser utilizada para renderização. Em seguida, os comandos de desenho gravados anteriormente

são enviados para a fila gráfica da GPU com o comando vkQueueSubmit. Após todas as

etapas citadas até este ponto, pode-se chamar a função vkQueuePresentKHR com referência

para a fila de apresentação da GPU e struct de configuração contendo os semáforos de

renderização e o swap chain que será utilizado. Com estes passos executados, a imagem será

44

renderizada na tela. Os passos descritos anteriormente são demonstrados no pseudocódigo do

Quadro 12.

Quadro 12 – Pseudocódigo de uma função de desenho com Vulkan 1. void drawFrame() 2. { 3. // adquirir uma imagem no swapchain para poder desenhar

4. VkResult result = vkAcquireNextImageKHR((...));

5. (...)

6. // verificar se a imagem adquirida é valida

7. if (result == VK_ERROR_OUT_OF_DATE_KHR) {

8. recreateSwapChain();

9. }

10. (...)

11. // enviar os command buffers para a fila gráfica

12. vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);

13. (...)

14. // mostrar a imagem na tela

15. vkQueuePresentKHR(presentQueue, &presentInfo);

16. }

Fonte: elaborado pelo autor.

Após a realização dos passos citados anteriormente realizados, tem-se uma imagem

renderizada e seu conteúdo exibido em uma janela. Isso conclui a etapa de renderização em

Vulkan. Na sequência serão apresentados os passos para renderização em OpenGL.

O processo de renderização em OpenGL possui as seguintes etapas:

a) invocar o comando de clear screen para limpar os buffers;

b) desenhar na janela;

c) swap buffer, para alternar entre o front e back buffer.

Na etapa de clear screen, executa-se o comando glClear que limpa o conteúdo do

frame buffer ativo. Após esta etapa, para cada objeto a ser desenhado deve-se executar os

seguintes passos:

a) atribuir um shader ao estado do OpenGL através da chamada de função

glUseProgram;

b) enviar a matriz transformação para o shader através de chamada de função

glUniformMatrix4fv;

c) atribuir a textura ao estado do OpenGL com a função glBindTexture;

d) atribuir o VAO do objeto com a função glBindVertexArray;

e) desenhar o objeto com a função glDrawElements.

O último passo para que os objetos desenhados apareçam na janela, é chamar a função

glfwSwapBuffers do GLFW, para que frame buffer apropriado seja exibido. O pseudocódigo

do processo descrito anteriormente pode ser visualizado no Quadro 13.

45

Quadro 13 – Pseudocódigo de uma função de desenho com OpenGL 1. void drawFrame() 2. { 3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 4. 5. for (auto obj : scene->getSceneGraph()) 6. { 7. glUseProgram(this->Program); 8. 9. glUniformMatrix4fv((...)); 10. 11. glActiveTexture(GL_TEXTURE0); 12. (...) 13. glBindTexture(GL_TEXTURE_2D, textureId); 14. 15. glBindVertexArray(this->VAO); 16. // Draw mesh 17. glDrawElements(GL_TRIANGLES, this->indicesSize, GL_UNSIGNED_INT, 0); 18. (...) 19. } 20. glfwSwapBuffers(_glfwWindow); 21. }

Fonte: elaborado pelo autor.

3.3.2 OPERACIONALIDADE DA IMPLEMENTAÇÃO

Para utilização da API Vulkan, faz-se necessária a instalação de seu SDK

disponibilizado pela LunarG e uma placa de vídeo que suporte a API. Além disso, nas

propriedades do projeto no Visual Studio faz-se necessário apontar o caminho da pasta

Include do SDK do Vulkan nas configurações de C/C++ (Configuration Properties, C/C++,

General), como ilustrado na Figura 9. Deve-se também configurar o caminho da pasta Lib do

SDK do Vulkan nas configurações do Linker (Configuration Properties, Linker, General),

como ilustrado na Figura 10. As outras bibliotecas estão inclusas na pasta da solução do

projeto, portanto não precisam ser incluídas manualmente. Nota-se que a biblioteca foi

desenvolvida e testada somente na plataforma Microsoft Windows 10.

Figura 9 – Configuração do diretório de Include do Vulkan SDK

Fonte: elaborado pelo autor.

46

Figura 10 – Configuração do diretório de Lib do Vulkan SDK

Fonte: elaborado pelo autor.

Para efetuar o estudo comparativo entre as APIs, se desenvolveu uma biblioteca para

renderização de cenas com objetos compostos por malhas 3D simples, mapeamento de textura

e modelo de iluminação Blinn-Phong nos objetos.

A biblioteca foi separada em três projetos do Visual Studio, o primeiro sendo uma

biblioteca estática e interface base para as subsequentes implementações do renderizador para

cada API (commonBase). O segundo contendo a implementação que realiza a abstração da API

utilizando OpenGL (glFurb), outro contendo uma implementação análoga mas utilizando

Vulkan (vkFurb), como ilustrado na Figura 11.

47

Figura 11 – Estrutura do projeto no Visual Studio

Fonte: elaborado pelo autor.

No projeto commonBase se encontra a implementação genérica que serve como base

para implementação do renderizador em OpenGL e Vulkan respectivamente. As derivações

da classe abstrata Renderer atuam como gerenciador principal e ponto de entrada do

processo de renderização. Nelas ocorre a inicialização das APIs gráficas e do gerenciador de

janela. Além disso, também mantém-se nela uma referência para uma cena contendo os

objetos gráficos que serão renderizados pela biblioteca.

Uma cena é especificada na classe Scene a qual contém uma coleção de objetos do

tipo DrawableObj, uma Camera e um objeto do tipo Light. No Quadro 14 é possível verificar

a implementação da cena 1 como exemplo. A classe DrawableObj é uma classe abstrata da

qual as classes dos tipos OGLDrawableObj e VulkanDrawableObj devem implementar. As

classes desse tipo representam um objeto gráfico em uma cena e contém a seguintes

informações que definem seu aspecto visual: malha 3D, material e matriz de transformação.

Ainda nessa classe é definido o método update o qual realiza a atualização do uniform buffer

do objeto, que contém as transformações geométricas, matriz de projeção e view da câmera e

posição da luz que serão enviadas ao shader para serem consumidas posteriormente no estado

apropriado do pipeline gráfico.

48

Além da lista de objetos que serão renderizados, contém também um objeto do tipo

Camera, o qual contém as matrizes de projeção e view que definem o aspecto visual da cena.

O objeto Light contém somente a posição que a luz virtual vai estar situada em espaço

global.

Quadro 14 – Definição classe Cena1 1. #include "../VulkanHeader.h" 2. 3. class Cena1 : public Scene 4. { 5. public: 6. Cena1() 7. { 8. name = "Cena1"; 9. // criação dos assets que serão utilizdados 10. VulkanTexture *texture(new VulkanTexture("../textures/bronze.jpg")); 11. VulkanMaterial* material(new VulkanMaterial); 12. VulkanShader* shader(new VulkanShader("../shaders/vert.spv",

"../shaders/frag.spv")); 13. VulkanMesh* mesh(new VulkanMesh("../models/buddha.obj")); 14. // atribuir o shader ao material 15. material->setShader(shader); 16. // criação do objeto gráfico 17. DrawableObj* obj1(new VulkanDrawableObj); 18. // configuração do objeto gráifco 19. PtrDownCast(obj1)->material = material; 20. PtrDownCast(obj1)->getMaterial()->setTexture(texture); 21. shader->updateDescriptorSet(PtrDownCast(obj1)->uniformBuffer, texture); 22. PtrDownCast(obj1)->setMesh(mesh); 23. PtrDownCast(obj1)->setPosition(glm::vec3(0)); 24. PtrDownCast(obj1)->setScale(glm::vec3(1)); 25. // adicionar o objeto gráfico à cena 26. this->AddObject(obj1); 27. // configurar luz e camera da cena 28. this->light = Light(glm::vec4(0, 5, 10, 0)); 29. this->camera = new Camera(glm::vec3(0, 0.5f, 1), glm::vec3(0.0f, .1f, 0.0f),

glm::vec3(0.0f, 1.0f, 0.0f)); 30. } 31. };

Fonte: elaborado pelo autor.

As classes derivadas de Mesh representam uma malha 3D e contém informações

referentes aos vértices e índices que compões a mesma. Em sua implementação na API

Vulkan, além de conter uma lista de vértices e uma lista de índices, possui objetos dos tipos

VkBuffers e VkDeviceMemory para representar os vértices e índices na memória da GPU. Já

na implementação em OpenGL, existem os atributos VAO, VBO e EBO que são handles do

OpenGL para objetos do tipo Vertex Array Object, Vertex Buffer Object e Element Buffer

Object.

As classes OGLMaterial e VulkanMaterial representam o conceito de material de um

objeto gráfico. Na implementação deste trabalho sua implementação é constituída apenas de

uma referência para um objeto do tipo Shader e um objeto do tipo Texture.

49

Na implementação com Vulkan a classe VulkanShader representa um shader

programável com programas dos estágios de vertex e fragment sendo utilizados. Para

representação de um shader foi definido que ele deve conter um objeto do tipo VkPipeline e

seus descriptors, como demonstrado no Quadro 15.

Quadro 15 – Classe VulkanShader 1. ... 2. class VulkanShader 3. { 4. public: 5. VulkanShader(std::string vertPath, std::string fragPath); 6. ~VulkanShader(); 7. 8. void prepare(); 9. 10. void createDescriptorPool(); 11. void createDescriptorSet(); 12. void updateDescriptorSet(VkBuffer uniformBuffer,VulkanTexture* vulkanTexture); 13. void createDescriptorSetLayout(); 14. void recreateGraphicsPipeline(); 15. 16. VkDescriptorPool getDescriptorPool() { return descriptorPool; } 17. const VkDescriptorSet* getDescriptorSet() { return &descriptorSet; } 18. 19. VkDescriptorSet descriptorSet; 20. VkDescriptorPool descriptorPool; 21. VkDescriptorSetLayout descriptorSetLayout; 22. 23. VkPipeline graphicsPipeline; 24. };

Fonte: elaborado pelo autor.

3.4 ANÁLISE DOS RESULTADOS

Nesta seção serão apresentados os cenários de testes desenvolvidos e os resultados

estatísticos oriundos dos mesmos. Na próxima seção será apresentado um quadro comparativo

entre os trabalhos correlatos e na seção seguinte serão apresentados os resultados dos cenários

de teste.

3.4.1 COMPARAÇÃO ENTRE OS TRABALHOS CORRELATOS

Conforme análise apresentada (Quadro 14), todos os trabalhos correlatos foram

realizados utilizando a API Vulkan, mas apenas Epic Games (2016a) e Mainuš (2016)

implementam a técnica de reutilização de command buffers previamente construídos para

envio de comandos através da API para a GPU.

50

Quadro 16 – Comparativo entre os trabalhos correlatos

Características Epic Games

(2016a)

Mainuš (2016) Overvoorde

(2016)

VkFurb (2017)

suporte à Vulkan sim sim sim sim

reutilização de

command buffers

sim sim não não

suporte à materiais sim sim não sim

occlusion culling sim não não não

multi-threaded

command

submission

sim sim não não

Fonte: elaborado pelo autor.

Todos os trabalhos correlatos, com exceção de Overvoorde (2016), implementam o

conceito de material, o qual é implementado de forma básica no trabalho aqui apresentado. A

Unreal Engine implementa a técnica de otimização occlusion culling, mas Vulkan Based

Render Toolkit e Vulkan Tutorial não, o qual também não é implementado neste trabalho.

Observa-se também que Epic Games (2016a) e Mainuš (2016) utilizam técnicas de

programação concorrente para a criação de command buffers (Multi-threaded command

submission), enquanto Overvoorde (2016) e este trabalho não.

3.4.2 RESULTADOS DOS CENÁRIOS DE TESTES

Foram desenvolvidas três cenas de testes com configurações variáveis para verificação

e comparação de performance entre as APIs gráficas. As configurações das cenas de testes

estão listadas a seguir:

a) cena 1: 1 objeto, constituído por 543.652 vértices, 1.087.716 triângulos (Stanford

Happy Buddha), como ilustrado na Figura 12;

b) cena 2: 289 objetos iguais, constituídos por 35.947 vértices, 69.451 triângulos

(Stanford Bunny), como ilustrado na Figura 13;

c) cena 3: 120 objetos mistos; sendo 100 Stanford Bunny, 10 Blender Monkey (507

vértices, 968 triângulos), 10 Stanford Happy Buddha, como ilustrado na Figura 14.

51

Figura 12 – Captura de tela da cena 1

Fonte: elaborado pelo autor.

Figura 13 – Captura de tela da cena 2

Fonte: elaborado pelo autor.

52

Figura 14 – Captura de tela da cena 3

Fonte: elaborado pelo autor.

3.4.3 Desempenho

A comparação de performance foi feita utilizando as seguintes métricas de

performance:

a) quadros por segundo;

b) frame time (tempo em milissegundos entre o frame anterior e o atual);

c) consumo de memória;

d) utilização da CPU.

Para obtenção de dados estatísticos, se desenvolveu uma aplicação que executa a

biblioteca com as cenas de teste por 20 segundos, e calcula as médias das métricas

mencionadas anteriormente em relação ao tempo. As cenas de teste foram executadas em

computador de uso pessoal possuindo as seguintes configurações:

a) processador Intel I5 6600K com frequência de 4,4GHz por core;

b) placa de vídeo Nvidia Geforce GTX 1060 6GB;

c) 8 Gb de memória RAM DDR4 com frequência de 3400 MHz.

53

Nas próximas seções serão apresentados os resultados dos testes comparativos da

execução das cenas de testes.

3.4.3.1 Quadros por segundo

Na Figura 15 é apresentado um gráfico com a comparação entre, a média da

quantidade de quadros por segundos obtidos em cada cena e cada API gráfica. Na primeira

cena de testes a diferença no número de quadros por segundo em favor ao Vulkan foi de

aproximadamente 8%. Na segunda cena, Vulkan teve em média aproximadamente 19% a

mais de quadros por segundos. Na última cena, Vulkan apresentou em média

aproximadamente 10% quadros por segundo a mais que OpenGL. Pode-se inferir que

utilizando Vulkan obteve-se em média aproximadamente 12% a mais de renderização de

quadros por segundo.

Figura 15 – Gráfico de quadros por segundos

Fonte: elaborado pelo autor.

54

3.4.3.2 Frame time

Na Figura 16, é apresentado o gráfico de comparação entre a média dos frame times

em milissegundos capturados nas cenas entre as APIs gráficas. Na primeira cena de testes, o

tempo de renderização de um frame em média foi aproximadamente 13% mais lento. Na

segunda cena, Vulkan levou, em média, aproximadamente 21% menos tempo para renderizar

um frame. Na última cena, Vulkan renderizou um frame em média aproximadamente 23%

mais rápido que OpenGL. Conclui-se que o tempo para renderização de um frame em

OpenGL foi de aproximadamente 19% mais lento que com Vulkan.

Figura 16 – Gráfico com a relação de frame time (ms) entre as APIs

Fonte: elaborado pelo autor.

3.4.3.3 Consumo de memória

A Figura 17 ilustra o gráfico de comparação entre a utilização de memória RAM

média das cenas de teste para cada API gráfica. Na primeira cena de testes Vulkan consumiu

em média aproximadamente 53% de memória RAM a menos que OpenGL. Na segunda cena,

houve pouca diferença no consumo, sendo que Vulkan consumiu somente 2% de memória

RAM a menos que OpenGL. Na última cena, a diferença no consumo de memória RAM foi

de aproximadamente 11%. Pode-se concluir que em média, quando utilizando OpenGL,

houve um consumo aproximadamente 22% maior de memória RAM.

55

Figura 17 – Gráfico com a relação de utilização de mémoria RAM entre as APIs

Fonte: elaborado pelo autor.

3.4.3.4 Utilização da CPU

Na Figura 18 pode-se observar o gráfico comparativo de utilização da CPU (em

porcentagem) entre as cenas de teste e as APIs gráficas. É possível constatar que nas 3 cenas

de testes um padrão se repetiu. A API Vulkan utilizou aproximadamente 98% a menos poder

de processamento em relação API OpenGL.

Figura 18 – Gráfico com a relação de utilização de CPU entre as APIs

Fonte: elaborado pelo autor.

3.4.3.5 Escalabilidade de FPS entre APIs

Na Figura 19 pode-se visualizar um gráfico representando a escalabilidade de FPS

entre as APIs. Foram realizados 20 testes com o mesmo tipo de objeto. A cada teste se

56

adicionou mais 50 objetos na cena, totalizando no último teste, 1000 objetos. Pode-se

averiguar que nas configurações testadas, o comportamento das APIs em relação a FPS foi

similar.

Figura 19 – Gráfico comparativo de escalabilidade de FPS entre as APIs

Fonte: elaborado pelo autor.

57

4 CONCLUSÕES

A proposta do trabalho era implementar uma biblioteca de renderização 3D utilizando

as APIs Vulkan e OpenGL e realizar uma comparação estatística de performance entre as

duas, tal objetivo foi alcançado. Originalmente foram propostos dois requisitos funcionais a

mais (implementar as técnicas de occlusion culling e multi-threaded command submission) do

que foram atendidos, porém, ficou claro durante o desenvolvimento do trabalho que devido à

complexidade da API Vulkan, tais requisitos estavam fora do escopo para realização deste

primeiro estudo exploratório da API.

Nota-se que maior ênfase foi colocada na parte de implementação utilizando a API

Vulkan, pois um dos objetivos do trabalho era realizar um estudo exploratório da mesma

devido à natureza recente do objeto de estudo. Além disso, durante o desenvolvimento do

trabalho preocupou-se em deixar as interfaces das implementações com as duas APIs o mais

similar possível, para conseguir gerar dados estatísticos válidos para comparação. Através das

cenas de teste desenvolvidas, foi possível verificar que a versão da biblioteca com Vulkan

obteve um melhor desempenho em relação a implementação com OpenGL.

Nota-se que o desenvolvimento utilizando a API Vulkan apresentou grande dificuldade

para compreensão de seus vários objetos de etapas. A natureza da API é muito explicita, e ao

contrário do OpenGL, cada estágio do pipeline da GPU precisa ser explicitamente inicializado

e a forma que o mesmo será utilizado precisa ser explicitamente definida antes da execução

do processo de renderização. Portanto além de ser necessário possuir uma boa compreensão

dos elementos que compõe a API, faz-se necessário possuir conhecimento avançado em

computação gráfica e de arquitetura de placas de vídeo modernas para utilização da API

Vulkan.

Outra dificuldade apresentada foi com as dependências circulares em C++, onde duas

unidades de compilação precisam referenciar uma a outra. Por exemplo, em um caso em que

duas classes arbitrárias possuem referência uma a outra, e que se faça necessário incluir o

header de uma classe A antes de uma classe B e o header da classe B antes de A. Tal tarefa

prova-se temporalmente impossível. Para resolver tal problema utilizou-se da técnica de

forward declaration, na qual se declara a classe necessária antes da classe na qual ela será

referenciada porém sem a sua definição, a qual será inferida pelo compilador posteriormente.

58

4.1 EXTENSÕES

Para trabalhos futuros, são sugeridas as seguintes extensões:

a) utilizar programação concorrente (multi-threaded command submission) para

criação de command buffers em Vulkan;

b) implementar outras rotinas gráficas para realização de mais testes comparativos

(Occlusion Culling, shadow mapping, deferred rendering);

c) implementar uma biblioteca com Vulkan para as plataformas Android ou iOS;

d) implementar técnicas de otimização como ordenação de cena por material para

minimização de trocas de estado da API Vulkan;

e) implementar outros cenários de testes.

59

REFERÊNCIAS

AKENINE-MÖLLER, Tomas; HAINES, Eric; HOFFMAN, Naty. Real-Time Rendering. 3.

ed. Natick: A K Peters/CRC Press, 2008. 1045 p.

ARM. Vulkan on Mobile. San Francisco: Vulkan On Mobile, 2016. Color. Disponível em:

<http://malideveloper.arm.com/downloads/Presentations/GDC 2016/Sponsored/Vulkan on

Mobile with Unreal Engine 4 Case Study.pdf>. Acesso em: 10 set. 2016.

BLESZINSKI, Cliff. History of the Unreal Engine. 2010. Disponível em:

<http://www.ign.com/articles/2010/02/23/history-of-the-unreal-engine>. Acesso em: 02 set.

2016.

EPIC GAMES: Protostar: Pushing Mobile Graphics with UE4 & Vulkan API | Feature

Highlight | Unreal Engine. 2016a. Disponível em:

<https://www.youtube.com/watch?v=lIdNoSB69PI>. Acesso em: 10 set. 2016.

______. Vulkan: How to use Vulkan in Unreal Engine 4. 2016b. Disponível em:

<https://wiki.unrealengine.com/Vulkan>. Acesso em: 03 set. 2016.

______. What Is Unreal Engine 4? 2016c. Disponível em:

<https://www.unrealengine.com/what-is-unreal-engine-4>. Acesso em: 01 set. 2016.

G-TRUC CREATION. OpenGL Mathematics, 2016. Disponível em: <http://glm.g-

truc.net/0.9.7/index.html>. Acesso em: 04 set. 2016.

KHRONOS GROUP. An Introduction to SPIR-V, 2016a. Disponível em:

<https://www.khronos.org/registry/spir-v/papers/WhitePaper.pdf>. Acesso em: 01 nov. 2016.

______. OpenGL Overview, 2016b. Disponível em: <https://www.opengl.org/about/>.

Acesso em: 06 nov. 2016.

______. The Khronos Group Inc, 2016c. Disponível em: <https://www.khronos.org/>.

Acesso em: 12 set. 2016.

______. Vulkan 1.0.25 - A Specification, 2016d. Disponível em:

<https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html>. Acesso em: 01 set.

2016.

______. Vulkan 1.0.32 - A Specification (with KHR extensions), 2016e. Disponível em:

<https://www.khronos.org/registry/vulkan/specs/1.0-

wsi_extensions/xhtml/vkspec.html#_wsi_swapchain>. Acesso em: 05 nov. 2016.

MAINUŠ, Matěj. Vulkan based render toolkit. In: EXCEL@FIT, 2016, Brno. Excel@FIT

2016. Brno: Brno University Of Technology, 2016. p. 1 - 5. Disponível em:

<http://excel.fit.vutbr.cz/submissions/2016/040/40.pdf>. Acesso em: 01 set. 2016.

SAMSUNG (Org.). SDC 2016 Session: Developing Console Games with Vulkan and

Unreal. Disponível em: <https://www.youtube.com/watch?v=NyGsMr2tcks>. Acesso em: 01

set. 2016.

SELLERS, Graham; KESSENICH, John. Vulkan Programming Guide: The Official Guide

to Learning Vulkan. Boston: Addison-wesley Professional, 2016. 480 p.

SINGH, Parminder. Learning Vulkan: Discover how to build impressive 3D graphics with

the next-generation graphics API-Vulkan. Birmingham: Packt Publishing Ltd, 2016. 466 p.

OVERVOORDE, Alexander. Vulkan Tutorial. 2016. Disponível em: <https://vulkan-

tutorial.com/>. Acesso em: 01 ago. 2017.

60

ANEXO A – Representação gráfica da arquitetura com Vulkan

Na Figura 20 é possível observar um diagrama que ilustra os principais objetos e suas

relações na API Vulkan. Pode-se ter uma visão geral da arquitetura do Vulkan e de quais

objetos interagem, e suas dependências.

Figura 20 – Representação gráfica da arquiterua com Vulkan

Fonte: Singh (2016).