Curso Professional

Post on 11-Jan-2016

52 views 3 download

description

Curso Professional. Lucas Euzébio Machado. Tópicos do Curso. C++, Algoritmos e Estruturas de Dados (36) Computação Gráfica (36) Programação Distribuída (6) Física (6) Inteligência Artificial (6) Fundamentos de Programação de Jogos (6) Projeto Final (24). - PowerPoint PPT Presentation

Transcript of Curso Professional

Curso Professional

Lucas Euzébio Machado

Tópicos do Curso

C++, Algoritmos e Estruturas de Dados (36) Computação Gráfica (36) Programação Distribuída (6) Física (6) Inteligência Artificial (6) Fundamentos de Programação de Jogos (6) Projeto Final (24)

Tópicos para C++, Algoritmos e Estruturas de Dados

Variáveis, Ponteiros, Referência, Alocação e Liberação de Memória Testes de condições Loops Funções Módulos, Compilação e Link-Edição Orientação a Objetos Classes e Structs Encapsulamento, Herança e Polimorfismo Construtores e Destrutores Exceções Funções e Variáveis estáticas Recursão Lista Encadeada, Fila e Pilha Árvore Binária Grafo Hashtable Templates Operator Overloading Lendo e Gravando Arquivos Multithreading

Variáveis

Ponteiros

Ponteiros são variáveis que armazenam endereços de memória.

Variáveis de referência

Referências servem como um segundo nome para um objeto. Elas permitem acesso indireto de forma semelhante a ponteiros.

Alocação de Memória

Para alocar memória da heap:

Liberando memória

Vetores

Vetores são agregações de elementos de um mesmo tipo que ocupam a memória de forma sequencial e adjacente.

Vetores

Uma variável vetor armazena na realidade o endereço de memória do primeiro elemento do vetor.

Vetores

Vetores podem ser multidimensionais

Testes de Condições

Testes de Condições

Loops

Instruções usada para causar repetições de trechos de código

Loops

Exercício

Crie um programa que gera um buffer de inteiros de 10 posições. Cujos valores sofrem um decremento de 10 até 1.

Adicione um trecho de código que busca pelo numero 4 no vetor e salva o endereço da posição do vetor em um ponteiro.

Exercício

Crie um programa que crie uma matriz 4x4 identidade usando um buffer bidimensional.

Funções

Funções são subprogramas que realizam operações definidas por usuários.

Ferramenta fundamental para redução de complexidade e reuso de código.

Em qualquer projeto, funções serão utilizadas sem que se conheça suas implementações internas.

Funções

Exercício

Crie uma função que adiciona dois vetores de 4 elementos e retorna o resultado em um terceiro vetor

Módulos, Compilação e Link-Edição

Módulos permitem a divisão de um sistema em diferentes partes, cada uma responsável por uma tarefa específica.

Um módulo para nós será definido como um .h e um .cpp.

O arquivo .h possui declarações. O arquivo .cpp possui implementações.

Exemplo de um modulo

Vector3.h

Vector3.cpp

Usando um módulo

Usando Módulos

Ao usar include é incluído todo o arquivo de texto na linha chamada.

Compilação

Ao compilar um arquivo de implementação .cpp é gerado um arquivo .obj com código binário e tabelas com referências externas ainda não resolvidas e símbolos públicos.

Link-Edição

A link edição resolve referências externas e junta todo o código em um só código binário gerando assim o arquivo executável.

Exercício

Implemente um módulo Vector4.h e Vector4.cpp que possui as funções de adição, subtração e multiplicação de vetores de 4 elementos e salvando o resultado em um outro vetor.

Crie um arquivo .cpp com a main e inclua o header do Vector4.h. Compile ambos arquivos .cpp e gere o executável.

Orientação a Objeto

Um objeto é uma entidade(podendo se basear em algo concreto ou abstrato) que possui um comportamento ou representa algo.

Você interage com um objeto através de uma interface formada por funções. Isso permite uma interação bem comportada.

A divisão de um sistema em objetos permite uma enorme redução em complexidade e aumento em reusabilidade.

Exemplo de um sistema dividido em objetos

Classes

Objetos são representados em software através de classes

Encapsulamento

Apenas os membros públicos de uma função podem ser acessados por outros objetos.

Encapsulamento

Fundamental para redução de complexidade. Você chama um método de uma classe e um

problema é resolvido para você. Não há necessidade de saber como o problema foi resolvido.

Permite interação comportada. Como você controla o acesso às variáveis através de funções não é possível partes externas do programa agirem de forma errada com suas variáveis.

Exercício

Implemente a classe AIManager. Essa classe possui duas funções. AddObject e Think. A função AddObject recebe uma string com um nome, uma posição x e uma posição y. A função Think mostra na tela os objetos adicionados.

Herança

Certos objetos compartilham características porém possuem particularidades suficientes de forma que precisam de novos objetos para serem apropriadamente representados.

Herança

Herança

O objeto Person e o objeto Stone são ambos objetos de cena que podem ser desenhados na tela. Então ambos podem pertencer à mesma classe de objetos SceneObject.

Se por exemplo um SceneObject possuir uma posição 3d. Tanto Person quanto Stone possuirão essa variável de posição pois a posição é uma variável que existe em todo objeto da classe SceneObject.

Herança

Exercício

Reimplemente o exercício anterior fazendo o AIManager ter funções para adicionar Dragon e Orc que são classes derivadas de AIEntity. A classe AIEntity possui membros públicos para armazenar um nome e uma posição x e y.

A classe Dragon tem uma string com o tipo de baforada do dragão e a classe Orc possui um inteiro que indica a arma sendo usada.

Implemente a função Think para tratar dos tipos específicos e desenhar suas partes específicas.

Polimorfismo

Quando um conjunto de classes deriva de uma mesma classe base, todo o conjunto compartilha uma interface.

É possível escrever partes de código levando em consideração apenas classes base de determinados objetos.

Isso é vantajoso para aumentar o reúso no código.

Polimorfismo

Polimorfismo

Exercício

Reimplemente o AIManager para receber objetos AIEntity. Crie uma função virtual pura em AIEntity chamada Think que imprime os dados da entidade. Faça a classe Orc e Dragon implementarem a função Think.

Mude o AIManager para fazer uso das funções Think dos objetos.

Construtores e Destrutores

Construtores são métodos de uma classe chamada para “construir” a classe. Todo objeto criado tem seu construtor chamado.

Construtores e Destrutores

É possível definir diferentes tipos de construtores

Construtores e Destrutores

Destrutores fazem o trabalho inverso dos construtores. Eles fazem a destruição do objeto, liberando recursos adquiridos como memória alocada, arquivos abertos, etc.

Construtores e Destrutores

Em classes base use destrutores virtuais. Ao deletar um objeto usando um ponteiro base o comportamento é indefinido caso o destrutor da classe base não seja virtual.

Exceções

Eventos anormais são normais. Programas executam normalmente até que

algum problema ocorra como falta de memória, acesso à uma área de memória ilegal, etc etc etc etc etc etc etc etc etc etc.....

É possível testar esses problemas ou exceções usando ifs ou fazendo uso de ferramentas de C++ criadas específicamente para tratar de exceções.

Exceções

Ao detectar uma anomalia, você gera uma exceção através da operação throw.

Exceções

Um exceção é uma classe

Exceções

Para capturar uma exceção deve-se usar o operador try e catch.

Exercício

Implemente uma classe chamada Buffer que cria um buffer de ints. A classe possui um construtor que recebe o número de elementos, e um destrutor que destroi os elementos.

A classe possui também um função Add que recebe um vetor de ints pra adicionar ao buffer e uma função Remove que recebe n e remove n elementos de buffer.

Jogue exceções caso sejam adicionados mais elementos do que o Buffer suporta ou sejam removidos mais elementos do que existem no buffer.

Funções e Variáveis Estáticas

Apesar de todo o perigo, às vezes acesso à informações globais é necessário e até simplifica designs.

Todos os objetos com um membro estático compartilham a mesma instância desse membro. Ou seja, se um objeto altera a variável estática, isso será visível por todos os outros objetos da mesma classe.

Declarando membros estáticos

Usando membros estáticos

Usando membros estáticos

Variáveis estáticas e constantes

Exercício

Implemente uma classe que possui internamente uma variável estática ponteiro do tipo da própria classe.

Inicialize essa variável para NULL Implemente uma função estática da classe chamada

GetInstance que retorna a variável quando o ponteiro é diferente de NULL, se o ponteiro for NULL é alocada memória para a variável e seu endereço é armazenado no ponteiro.

Recursão

A recursão é uma forma de solucionar certos problemas que envolve fazer uma função chamar a si mesma para resolver o problema.

Exemplo

Exercício

Implemente uma função recursiva que busque em um vetor de inteiros ordenado crescentemente por um determinado valor.

A função deve retornar o índice do valor ou , -1 caso o valor não seja encontrado.

Busca Binária

Complexidade de Algoritmos

Complexidade não é de difícil de entender ... é de difícil de computar.

É nossa missão encontrar os algoritmos mais eficientes para resolver algum problema.

Os algoritmos mais eficientes para resolver algum problema são chamados de algoritmos ótimos.

Complexidade da Busca Linear

No pior caso, é necessário realizar n testes para achar (ou não) um valor.

É usada uma notação O(n) para representar o algoritmo busca linear.

O(n) ~= se você colocar n elementos como input, o custo da função é uma função linear.

Complexidade da Busca Binária

Teste 1 = n elementos Teste 2 = n / 2 elementos Teste 3 = n / 4 elementos Teste final quando você tem 1 elemento. Dado n elementos, quantos testes são feitos? Temos que achar i quando n/2^i = 1. n/2^i = 1 -> n = 2^i log n = log 2^i Log n = i Complexidade = O(log n) Busca binária é melhor que busca linear.

Lista Encadeada, Fila e Pilha

As vezes, não é possível saber o número máximo de elementos que serão adicionados à uma classe.

Quando isso acontece é necessário criar uma estrutura capaz de crescer de acordo com a necessidade.

Lista Encadeada, Fila e Pilha são estruturas de dados fundamentais usadas em diversos algoritmos.

Lista Encadeada

A lista encadeada é uma lista criada dinamicamente que se liga através de ponteiros.

Lista Encadeada - implementação

Uma classe consiste na lista em si. A classe da lista usa outra classe que

representa os nós da lista. Cada nó armazena os dados e o ponteiro para o próximo nó da lista.

O último nó aponta para NULL. A classe da lista aponta para o primeiro

elemento da lista.

Exercício

Implemente uma lista encadeada cujos nós armazenam posições x,y e z em floats e possuem também um id inteiro.

Crie um método na classe para adicionar elementos no final da lista.

Crie método na classe para buscar um nó dado um inteiro, o método deve retornar as posições x, y e z.

Crie um método para imprimir toda a lista.

Lista Encadeada

Lista Encadeada

Fila

Implementada de maneira semelhante à lista encadeada, porém elementos só podem ser inseridos no final e removidos na frente.

Funciona em forma FIFO (First In First Out). Imagine uma fila de cinema!

Pilha

Também implementada de maneira parecida com a lista encadeada porém sua operação é LIFO (Last In First Out).

A pilha mantém um ponteiro para o seu topo. Quando se deseja adicionar um elemento realiza-se um push adicionando um elemento ao topo da lista, quando se deseja remover um elemento, faz-se um pop, removendo o elemento do topo.

Exercício

Implemente uma pilha. A pilha possui um ponteiro chamado top que aponta para o topo da pilha.

Crie o método push que adiciona um elemento ao topo da pilha.

Crie o método pop que remove o elemento do topo da pilha.

Crie o método print para imprimir a pilha.

Árvore Binária de Busca

Estrutura de dados semelhante à uma arvore invertida onde cada nó possui até 2 filhos.

Cada nó possui um id, um ponteiro para o filho esquerdo e um ponteiro para o filho direito.

Cada nó possui a seguinte característica, o filho a esquerda possui um id menor que o id do nó e o filho a direita possui um id maior que o id do nó.

Árvore Binária de Busca

A busca por um valor possui um custo O(h). Se a árvore estiver desbalanceada isso pode

representar O(n) Se a árvore estiver balanceada isso pode

representar O(log n) Uma árvore balanceada é uma árvore em que para

todo nó, suas subárvores à esquerda e à direita diferem no máximo em 1 na altura. Considere uma subárvore vazia como de altura -1.

Folha é um nó sem filhos.

Árvore Binária de Busca

Através de uma árvore balanceada é possível achar uma árvore perfeita de mesma altura.

Árvore perfeita é uma árvore em que todas as folhas possuem a mesma altura e todos os nós possuem 2 ou 0 filhos.

Número de elementos da árvore completa = Somatório(2^i, i varia de 0 até a altura).

n = 1 * (2^(h+1) – 1) / 2 – 1 (somatório de progressão geométrica) n = 2^(h+1) – 1 n + 1 = 2^(h+1) log(n+1) = h + 1 log(n+1) – 1 = h Logo a altura de uma árvore balanceada é um logaritmo de n ... O que

é muito bom!!!!

Exercício

Implemente uma árvore binária onde os nós possuem um id inteiro e armazenam um valor inteiro.

Implemente o método de busca por um id. Implemente o método de busca pelo maior

elemento. Implemente o método de adição de elementos. Implemente o método que desenha na tela os

valores dos nós de maneira ordenada crescentemente por id.

Grafo

Grafos são uma estrutura de dados onde um nó pode apontar para um número qualquer de outros nós.

Grafos podem ser cíclicos se possuirem referências que façam um ciclo (como um filho apontando para um pai).

Caso não existam referências cíclicas, o grafo é definido acíclico e se assemelha à uma árvore n-ária, onde cada nó possui n filhos.

Grafos Acíclicos – Uma Implementação

Cada grafo possui um ponteiro para o pai e uma estrutura de dados(lista encadeada, vetor) que armazena os ponteiros para os filhos.

Se assemelha a uma árvore n-ária.

Exercício

Implemente um grafo acíclico que armazena seus filhos em uma lista encadeada.

Para isso implemente uma classe GraphNode que possui os métodos AddChild e Print. Coloque os GraphNodes possuindo um inteiro internamente.

AddChild adiciona um filho ao nó. Print imprime os nós do grafo. Construa um grafo e o imprima.

Hash Table

É uma estrutura de dados que permite acessos em O(1) .... Ou seja ... Muito rápidos.

Para adicionar um elemento, é utilizada uma chave que identifica o elemento.

Através dessa chave, é calculada a posição do elemento (essa posição geralmente é um índice de um vetor).

A função que pega essa chave e calcula uma posição é chamada de função de hash.

Hash Table - Exemplo

Para armazenar objetos é usado um vetor onde cada elemento desse vetor é um ponteiro para um objeto.

A chave é um inteiro. A posição é calculada achando o resto da divisão da

chave pelo tamanho do vetor de ponteiros. ( index = chave % tam_vec; )

Quando uma chave gerar uma posição que já contém um objeto, outra estrutura de dados é utilizada para salvar os objetos (lista encadeada, arvore binária de busca, outra lista encadeada).

Exercício

Implemente uma hash table com as mesmas características do exemplo anterior. Para salvar objetos na mesma posição do vetor de ponteiros utilize uma lista encadeada.

Crie uma função de inserção que recebe a chave e o objeto e o salva na hash table.

Crie uma função de search que procura pelo objeto dada uma chave.

Template

Templates são como receitas para criar classes que recebem um ou mais tipos como parâmetros.

São úteis para reaproveitar algoritmos em diferentes tipos de dados.

Template

Template

Toda vez que um template é utilizado para um determinado tipo, é criado o código da classe para fazer uso daquele tipo.

Então no exemplo anterior, são criadas 2 classes com a mesma lógica porém só com os tipos int e float diferentes.

Como as classes são geradas em tempo de compilação, é necessário saber a implementação de todos os métodos de cada template. Se um template define seus métodos em um .cpp, outras classes não serão capazes de ver essa implementação e criar as novas classes (você provavelmente terá problemas de link-edição). Por esse motivo, geralmente os templates são criados completamente no .h (incluindo a implementação das funções).

Exercício

Crie uma lista encadeada que consegue armazenar qualquer tipo usando um template.

Operator Overloading

Operadores como +, - , [] , == , etc, podem ter seu comportamento redefinido nas classes.

Isso facilita a programação usando determinados tipos (ex: vetores 3d, strings).

Operator Overloading - Exemplo

Exercício

Implemente uma classe String. Faça overload do operador = que recebe um vetor

de caracteres e armazena na string. Faça overload do operador += que recebe um vetor

de caracteres e adiciona os caracteres ao final da string.

Faça overload do operador == que recebe um ponteiro para caracteres constantes (const char*)

Lendo e Gravando Arquivos

Para ler ou gravar arquivos é necessário criar uma variável ponteiro para FILE. É preciso incluir o stdio.h.

Para abrir um arquivo, usa-se a função fopen da seguinte forma– FILE* fopen(path do arquivo, tipo de abertura)

Lendo e Gravando Arquivos

O tipo de abertura de fopen pode definir diferentes formas de abertura do arquivo:– “r” : abre o arquivo para leitura.– “w”: abre o arquivo para escrita. Se o arquivo já

existir, seu conteúdo é destruído.– “a”: abre o arquivo permitindo a escrita ao final do

arquivo.

Lendo e Gravando Arquivos

Para a escrita de arquivos, usa-se a função fwrite que possui o seguinte protótipo:– size_t fwrite(const void* buffer, size_t size, size_t

count, FILE* stream)

Lendo e Gravando Arquivos

Para ler dados de arquivos, usa-se a função fread:– size_t fread(void* buffer, size_t size, size_t count,

FILE* stream)

Lendo e Gravando Arquivos

Para fechar arquivos usa-se fclose– int fclose(FILE* stream)

Lendo e Gravando Arquivos

Exercício

Crie um programa que salve as seguintes informações do jogador:

Estágio atual Nome do personagem Nível do Personagem Posição x, y e z do personagem E depois crie um outro programa que lê

esses dados e mostra na tela!

Multithreading

Multithreading permite a criação de trechos de código que executam paralelamente.

Se houver um único processador na máquina, as threads concorrem pelo processador.

O sistema operacional dá um time slice para cada thread. Ao término do time slice, o SO interrompe a execução da thread e coloca outra thread para processar.

Multithreading

Isso é útil em máquinas com múltiplos processadores por permitir processamento paralelo.

É útil também quando existem operações que podem bloquear e não é desejável que o sistema bloqueie por essas operações (ex: bloqueio enquanto uma leitura de um arquivo grande é realizada).

Multithreading

Para criar uma thread, utilizar a função _beginthread (incluir o process.h):– uintptr_t _beginthread(função da thread, tamanho

da pilha, ponteiro para a lista de parâmetros)

Multithreading

Multithreading

Variáveis que são acessadas por diferentes threads correm o risco de se tornarem corrompidas.

Imagine um programa onde existe uma lista de jogadores. Uma thread está adicionando um jogador na lista e outra thread está removendo outro jogador da lista. Pode acontecer da lista acabar com um dos jogadores apontando para lixo e até coisas piores.

Por isso é necessário usar funções que permite acesso à regiões críticas.

Multithreading

Para proteger regiões acessadas por múltiplas threads utilize CRITICAL_SECTION (inclua windows.h)

Use a API InitializeCriticalSection, EnterCriticalSection, LeaveCriticalSection e DeleteCriticalSection para usar regiões críticas.

Multithreading

Exercício

Crie uma aplicação com 2 threads adicionais. 1 thread incrementa um contador e o imprime na tela 1 thread decrementa esse mesmo contador e o

imprime na tela. Cada thread repete esse processo 50 vezes. A main thread fica em espera ocupada esperando as

threads terminarem. No final ela imprime THE END. Faça testes usando critical sections e sem usar

critical sections.

Tópicos para Computação Gráfica

Aplicações Win32 Pipeline gráfico do Direct3D e Dispositivo Direct3D Buffer de Vértices e Índices Effect Files, Vertex Shaders e Pixel Shaders com Cg Renderizando triângulos Matrizes e Transformações Sistemas de Coordenadas e o Processo de Visualização Renderizando triângulos com perspectiva Renderizando triângulos texturizados Produto Escalar Iluminação Renderizando Objetos com Iluminação por pixel Exportando e carregando modelos de arquivos .X (e renderizando!) Animação usando interpolação entre Key Frames Alpha Blending Volumes de Sombras Bump Mapping Grafos de Cena Culling em cenários usando Octrees

Programando aplicações Win32

Para criar uma aplicação Win32 é preciso incluir o windows.h

O processamento em aplicações Win32 começa na WinMain.

Programando aplicações Win32

hinstance é um handle. Um handle é simplesmente um número que identifica alguma coisa. hinstance é um identificador da aplicação. Esse parâmetro é necessário para alguma funções.

hprevinstance é um parâmetro não mais utilizado e é definido como 0.

cmdline é a linha de comando usada para rodar o programa.

cmdshow indica como o programa deveria ser inicialmente mostrado.

Programando aplicações Win32

Para criar a janela da aplicação é preciso instanciar e preencher a struct WNDCLASS que possui a seguinte cara.

Programando aplicações Win32

Programando aplicações Win32

style: define propriedades da janela. CS_VREDRAW fazem com que a janela seja redesenhada caso haja uma mudança na altura da janela.

lpfnWndProc: recebe a função que trata os eventos enviados para a janela de sua aplicação.

cbClsExtra: número de bytes extras para alocar para a windows class struct. cbWndExtra: número de bytes extras além das instância da janela. hInstance: handle da aplicação. hIcon: handle do ícone usado. hCursor: handle do cursor usado. hbhBackground: handle do pincel usado para pintar o background da janela. lpszMenuName: um nome que identifica o recurso de menu usado na

aplicação. lpszClassName: especifica o nome dessa classe de janela.

Programando aplicações Win32

Após definir as características da classe de janela, é preciso registrá-la.

Programando aplicações Win32

Após o registro da classe de janela, é preciso criar a janela e mostrá-la.

Programando aplicações Win32

Após criar a janela é necessário entrar no loop de captura e envio de mensagens. Nesse loop também são colocadas funções para executar o jogo.

Programando aplicações Win32

A última parte que falta é a parte da função de tratamento de eventos

Exercício

Junte todo o código mostrado e crie uma aplicação win32.

Implemente a aplicação para mostrar a posição x e y do mouse quando o botão esquerdo é pressionado.

Quando a tecla pra cima é pressionada mostrar também.

Dispositivo Direct3D

API Gráfica do DirectX Permite acesso à placas de aceleração

gráfica de maneira única e independente do dispositivo.

Pipeline Gráfico do Direct3D

Pipeline Gráfico do Direct3D

Vertex Data: Dados geométricos dos objetos 3D do cenário. Por exemplo, as posições dos vértices do objeto 3D.

Fixed Function Pipeline: Uma sequência de processamento fixa do Vertex Data. As alterações nessa sequência de processamento são pouco flexíveis.

Programable Pipeline: Se usado, permite a implementação de um programa que processa os vértices. Permite muita mais flexibilidade de processamento dos vértices.

Pipeline Gráfico do Direct3D

Clipping: Esse estágio recorta todas as partes dos triângulos que estão para fora da tela.

Pipeline Gráfico do Direct3D

Back Face Culling: Elimina os triângulos que estão de “costas para a câmera”. Esse estágio elimina triângulos não visíveis em objetos fechados.

Pipeline Gráfico do Direct3D

Rasterization: Nesse estágio, os triângulos são desenhados.

Pipeline Gráfico do Direct3D

Pixel Shader: Se usado, permite ao programador criar um processamento que executa a cada pixel desenhado. Permite grande flexilidade sobre a forma como os pixels são renderizados.

Texture Sampler: Esse estágio, se usado, captura amostras das textura para serem usadas no Pixel Shader.

Pipeline Gráfico do Direct3D

Alpha Test: Esse estágio permite configurar a aceitação de um pixel de acordo com o seu alpha.

Depth Test: Esse estágio permite configurar a aceitação de um pixel de acordo com a sua profundidade.

Stencil Test: Esse estágio permite configurar a aceitação de um pixel de acordo com o Stencil Buffer.

Pipeline Gráfico do Direct3D

Fogging: Esse estágio permite configurar fumaça na cena a medida que os objetos se distanciam.

Alpha Blending: Esse estágio permite que o pixel sendo renderizado seja combinado com o pixel já desenhado no buffer de cor que será apresentado na tela.

Pipeline Gráfico do Direct3D

A geometria dos modelos e cenários passa pelo pipeline do Direct3D e ao final é gerada uma imagem 2D do objeto 3D, que é mostrada na tela.

Integração de Sistemas

Sistemas de Coordenadas

Direct3D usa o sistema de coordenadas da mão esquerda.

O objeto Direct3D

É o primeiro objeto a ser criado e o último a ser liberado.

O dispositivo Direct3D

Objeto principal de renderização. É com esse objeto que passamos a

geometria de modelos para a placa gráfica.

Criando um dispositivo Direct3D

Preencher D3DPRESENT_PARAMETERS

Criando um dispositivo Direct3D

BackBufferWidth, BackBufferHeight: Largura e altura do Backbuffer. BackBufferFormat: Formato do backbuffer. BackBufferCount: Número de Backbuffers. MultiSampleType e MultiSampleQuality: Parâmetros usados para antialiasing. SwapEffect: Maneira como o backbuffer vira o front buffer. hDeviceWindow: handle da janela Windowed: Se a aplicação vai rodar em janela ou full screen. EnableAutoDepthStencil: Permissão se o Direct3D pode gerenciar o seu buffer

de depth e stencil. AutoDepthStencilFormat: O formato do buffer de depth e stencil. Flags: ... FullScreen_RefreshRateInHz: A taxa com que o monitor é atualizado. PresentationInterval: A taxa com que o backbuffer pode ser apresentado na

tela.

Criando um dispositivo Direct3D

Criar o dispositivo usando a função CreateDevice do objeto Direct3D.

Criando um dispositivo Direct3D

Adapter: número que identifica a placa gráfica usada.

DeviceType: O tipo de dispositivo a ser usado (Ex. HAL ou Ref).

hFocusWindow: handle para a janela BehaviorFlags: flags pPresentationParameters: a estrutura

PRESENT_PARAMETERS preenchida anteriormente.

ppReturnedDeviceInterface: retorna o dispositivo Direct3D criado.

Criando um dispositivo

Exercício

Crie um dispositivo Direct3D em 800x600 fullscreen.

Experimente com outros tamanhos. Crie um dispositivo para versão Windowed.

Para isso coloque Windowed = TRUE, e o fullscreen refresh rate = 0.

Cores

As cores no Direct3D são definidas geralmente pela combinação de vermelho, verde e azul.

Um componente chamado alpha também é usado eventualmente e representa o grau de transparência da cor.

Cores

Em Direct3D, cores de vértices são geralmente definidas como números hexadecimais com o seguinte formato:

– 0xalpharedgreenblue 0x00ff0000 = vermelho puro 0x0000ff00 = verde puro 0x000000ff = azul puro 0x00000000 = preto 0x00ffffff = branco

Limpando Buffers

Toda vez que se deseja renderizar um frame, é necessário limpar os buffers de cor e profundidade.

Caso o buffer de cor não seja limpo, partes das cenas de renderizações passadas serão mostradas na tela, já que os backbuffers são reutilizados. Caso um jogo no entando sempre desenha na tela toda, não é necessário limpar esse buffer.

O buffer de profundidade é usado para desenhar corretamente os objetos 3D com relação à distância para a câmera. Basicamente esse buffer garante que objetos 3D que estão na frente de outros objetos, são renderizados corretamente na frente desses objetos. Esse buffer sempre precisa ser limpo a cada nova renderização.

Limpando Buffers

Para limpar os buffers de cor e profundidade é necessário usar a função Clear do dispositivo Direct3D.

Limpando Buffers

Count: Número de retângulos em pRects. pRects: Buffer de retângulos que especificam as

regiões que irão sofrer limpeza. Flags: flags que especificam os buffers que serão

limpos. Color: cor usada para limpar o buffer de cor. Z: valor de profundidade usado para limpar o buffer

de profundidade. Stencil: valor de stencil usado para limpar o buffer

de stencil.

Limpando Buffers

Apresentando Back Buffers

Toda a renderização por default é feita no backbuffer.

Após a renderização é necessário apresentar o backbuffer, fazendo seus dados serem copiados para o front buffer, que é o buffer sendo mostrado na tela.

Apresentando Back Buffers

Para apresentar o back buffer deve ser usada a função Present do dispositivo Direct3D.

Apresentando Back Buffers

pSourceRect: Um retângulo que define a parte que será apresentada do Back Buffer.

pDestRect: Um retângulo que define o local onde será apresentado o back buffer.

hDestWindowOverride: handle da janela onde será apresentado o back buffer.

pDirtyRegion: Define a região que precisa ser atualizada.

Apresentando Back Buffers

Exercício

Crie uma aplicação que limpa o buffer de cor para a cor vermelha (0x00ff0000).

Objetos 3D

Um objeto 3D pode possuir diversas informações que são usadas na sua renderização.

Objetos 3D

Objetos 3D

Objetos 3D podem usar texturas(imagens) para dar detalhe à seus triângulos.

Coordenadas de textura podem ser usadas para aplicar imagens à um objeto 3D.

Objetos 3D

Normais também podem ser usadas para cálculos de iluminação por exemplo.

Objetos 3D (Resumo)

Para desenhar objetos 3D nós podemos usar:– Posições dos vértices– Índices das faces– Coordenadas de textura– Normais dos vértices

Buffer de Vértices e Índices

Objetos 3D são renderizados passando sua informação geométrica para a placa gráfica.

A informação geométrica dos objetos é armazenada em VertexBuffers e IndexBuffers.

Os vértices dos objetos podem possuir variações em seu conteúdo.

Nos VertexBuffers ficam armazenadas informações tipo posição x,y,z, coordenadas de textura u, v, normal, etc.

Criando um Vertex Buffer

Para isso é necessário usar a função CreateVertexBuffer do dispositivo Direct3D.

Criando um Vertex Buffer

Length: tamanho do vertex buffer em bytes. Usage: Flags que indicam usos especiais. FVF: Flags que indicam o conteúdo do vertex buffer. Pool: Memória onde esse buffer ficará armazenado

(Memória de Vídeo, AGP ou RAM normal). ppVertexBuffer: Retorna o vertex buffer. pSharedHandle: colocar NULL. (bacalhau M$)

Criando um Vertex Buffer

Carregando Dados no Vertex Buffer

Para carregar dados em um vertex buffer deve-se usar a função Lock do vertex buffer.

Após carregar os dados, deve-se usar a função Unlock.

Carregando dados no Vertex Buffer

OffsetToLock: Distância do início do vertex buffer onde o carregamento começará.

SizeToLock: Tamanho da região no vertex buffer onde os dados serão carregados.

ppbData: Ponteiro retornado que aponta para o conteúdo do vertex buffer.

Flags: ...

Carregando dados no Vertex Buffer

Vertex Buffers

Vertex buffers podem armazenar diferentes informações sobre um vértice. Ex:– x,y,z– Coordenadas de textura u, v– Normal– Cor do vértice

Exercício

Crie um vertex buffer e carregue dados x,y,z de um triângulo.

Leia esses dados em outro buffer usando o mesmo vertex buffer.

O processo de renderização de primitivas

Processo de renderização de primitivas

As informações de renderização (x,y,z, u, v, ...) do objeto são armazenadas em vertex buffers.

Esses buffers são colocados em uso através da função SetStreamSource do dispositivo Direct3D.

A função DrawPrimitive faz as informações dos buffers serem carregadas juntando a informação completa de um vértice.

Quem define como os vertex buffers são combinados formando um vértice é a declaração do vértice ...

Colocando vertex buffers em uso

Para colocar em uso um vertex buffer, deve-se usar a função SetStreamSource do dispositivo Direct3D.

Colocando vertex buffers em uso

StreamNumber: define o número do vertex buffer, começa em 0.

pStreamData: o vertex buffer OffsetInBytes: a distância em bytes no buffer

onde começa a informação dos vértices. Stride: A distância entre cada componente

do vertex buffer.

Colocando vertex buffers em uso

Exercício

Coloque o vertex buffer criado no exercício anterior para ser usado como o vertex buffer 0.

Crie um vertex buffer para armazenar uma cor para cada um dos vértices do triângulo. Use D3DCOLOR.

Colocar esse vertex buffer com as cores como o vertex buffer 1.

Declarações de Vértices

Os vertex buffers podem conter diferentes informações sobre o vértice.

A declaração do vértice informa ao Direct3D como ele deve processar os vertex buffers usados para adquirir a informação de um vértice.

Declarações de vértices dizem coisas do tipo: “O vertex buffer 0 tem a informação x,y,z do objeto, já o vertex buffer 1 tem a informação u,v do objeto”.

Criando uma declaração de vértices

Uma declaração de vértices é feita através da criação de um buffer onde cada elemento é um D3DVERTEXELEMENT9.

Criando uma declaração de vértices

Stream: o número do vertex buffer. Offset: a distância no vertex buffer onde começa a

informação. Type: O tipo de dado armazenado no buffer. Method: Define como o tesselator vai operar na

informação do vertex buffer. Usage: Define o objetivo do uso do vertex buffer. UsageIndex: Dado adicional ao objetivo do vertex

buffer.

Criando uma declaração de vértices

Criando uma declaração de vértices

Após criar o buffer de D3DVERTEXELEMENT9, uma declaração é criada chamando a função CreateVertexDeclaration do dispositivo Direct3D.

Criando uma declaração de vértices

Usando uma declaração de vértices

Para usar uma declaração de vértices usa-se a função SetVertexDeclaration do dispositivo Direct3D.

Exercício

Crie a vertex declaration para usar os buffers do exercício anterior.

Effect Files

Effect files são arquivos que estabelecem uma configuração do pipeline gráfico para renderizar alguma coisa.

Eles armazenam o vertex e o pixel shader.

Composição de um effect file.

Vertex Shader

É responsável pelo processamento dos vértices. É um programa que executa para cada vértice

passado para o pipeline. Recebe como parâmetro as informações dadas

pelos vertex buffers. O pipeline espera obrigatoriamente pela posição do

vértice. Opcionalmente, é possível enviar outras informações como coordenadas de textura e cores. Essas informações serão interpoladas por toda a face do triângulo sendo desenhado.

Vertex Shader

Ao final do processamento do Vertex Shader, o vértice é passado para o estágio de clipping.

O vértice estará na tela, se estiver entre as coordenadas (-1, -1, 0, 1) e (1, 1, 1, 1).

Se o vértice estiver fora do volume mostrado na imagem, ele fará com que o triângulo a que ele pertence seja clipado.

Vertex Shader

Vertex Shader

Dados de input:– POSITION: define que a variável receberá a informação de

posição dos vertex buffers.– TEXCOORD0: define que a variável receberá a informação

de coordenadas de textura 0 do vertex buffers.

Dados de output:– POSITION: parâmetro de retorno obrigatório. Recebe uma

posição no espaço de clip homogêneo.– TEXCOORD0: recebe uma coordenada de textura que

será interpolada linearmente pela face do triângulo.

Pixel Shader

O parâmetro de retorno obrigatório do pixel shader é a cor final do pixel.

Pixel Shader

Parâmetros de input:– TEXCOORD0: coordenada de textura

interpolada.[

Parâmetros de output:– COLOR: a cor final do pixel. (no exemplo

estamos colocando a cor branca)

Usando Effect files no Direct3D

Para criar um efeito no Direct3D usa-se a função D3DXCreateEffectFromFile.

Usando Effect files no Direct3D

pDevice: o dispositivo Direct3D pSrcFile: path do effect file pDefine: permite a definição de macros do arquivo de efeitos. pInclude: permite a definição de includes no arquivo de efeitos. Flags: ... pPool: Usado para permitir o uso compartilhado de parâmetros

em múltiplos efeitos. ppEffect: retorna o objeto efeito. ppCompilationErrors: retorna uma lista de erros de compilação

do efeito.

Usando Effect files no Direct3D

Renderizando Triângulos

Para renderizar triângulos é preciso usar a função DrawPrimitive do dispositivo Direct3D.

Renderizando Triângulos

PrimitiveType: Define o tipo de primitiva para renderizar. (lista de ponto, de linhas, de triangulos, etc ...)

StartVertex: índice do primeiro vértice a ser carregado para renderização.

PrimitiveCount: número de primitivas a serem renderizadas.

Renderizando Triângulos

Antes de qualquer DrawPrimitive deve ser chamada a função BeginScene do dispositivo Direct3D.

Depois de todas as chamadas de DrawPrimitive deve ser chamada a EndScene do dispositivo Direct3D.

Resumo Básico de Renderização de Objetos 3D

Definir os vertex buffers usados pelo objeto 3d.

Definir a declaração de vértice do objeto 3d. Com isso o Direct3D consegue entender a informação que existe dentro dos buffers.

Renderizar o objeto 3d com a função DrawPrimitive.

Renderizando triângulos

Exercício

Use os vertex buffers e o vertex declaration dos exercícios anteriores e renderize um triângulo.

Para isso é necessário alterar o vertex shader para receber um float4 color : COLOR0 e retornar também um COLOR0.

Altere também o pixel shader para receber esse COLOR0 e colocá-lo na tela.

Implemente um código que mexe o triângulo em x e y baseado nas teclas recebidas.

Matrizes e Transformações

Matrizes são uma parte fundamental na computação gráfica.

Elas permitem:– Posicionar objetos 3D no mundo.– Posicionar câmeras no mundo.– Projetar o cenário 3D em um plano permitindo a

visualização em monitores.

O que são matrizes?

São um conjunto de elementos organizados em linhas e colunas

Operações Básicas

Multiplicação Não é Comutativa

Vetores

Podem ser representados por matrizes 1xN ou Nx1.

Consistem em uma entidade matemática que possui direção e tamanho.

Representado por uma linha com uma ponta de flecha.

Transformações

Matrizes são usadas para realizar transformações em vetores.

Transformações

São operações realizadas em pontos que permitem até alterar o sistema de coordenadas do ponto. (Ex: um ponto 3D vira um ponto 2D).

Transformações

Transformações Simples

Transformações Simples

Matriz Identidade

Matriz de Rotação 2D

Matriz de Escala Uniforme

Como Implementar Translação?

Coordenadas Homogêneas

Coordenadas homogêneas permitem que translações sejam realizadas com multiplicações de matrizes.

As coordenadas homogêneas de um ponto de n dimensões possuem n+1 dimensões.

A coordenada adicional é um escalar que multiplica todas as outras coordenadas.

(x,y,z) = (wx, wy, wz, w)

Coordenadas Homogêneas

Representando um ponto 2D em coordenadas homogêneas:– (x, y) = (x, y, 1) ou (2x, 2y, 2)

Para achar um ponto 2D representado em coordenadas homogêneas:– (2x, 2y, 2) = (2x/2, 2y/2, 2/2) = (x, y, 1)

Adicionando Translação

Para realizar a translação, basta usar o escalar com valor 1.

Sistemas de Coordenadas e Processo de Visualização

O pipeline gráfico pega a geometria dos objetos em 3D e cria uma imagem em 2D a partir dela.

Para gerar uma imagem 2D de um cenário 3D visto de um determinado ângulo é necessário fazer com que os vértices de um objeto passem por vários sistemas de coordenadas.

Sistemas de coordenadas e processo de visualização

Sistema de coordenadas locais ou do objeto ou do modelo

Quando um objeto está sendo modelado, seus vértices em 3D são especificados com relação a alguma origem. Geralmente essa origem fica em alguma região próxima ao objeto ou no centro do objeto.

Sistema de coordenadas do mundo

Os vértices do objeto no espaço precisam ser transformados para que o objeto possa ser posicionado no mundo. Essa transformação é quem permite posicionar um objeto em cima de uma mesa, ou dois orcs lado a lado. Isso se dá através da matriz de mundo.

Após a transformação os vértices estão no espaço do mundo. Todas as transformações são feitas baseadas em uma mesma origem de mundo, por isso elas permitem posicionar os objetos no mundo.

A transformação que transforma uma posição do espaço do objeto para o espaço do mundo é chamada de transformação de mundo.

Sistema de coordenadas de olho ou visão

Após transformar os vértices para o espaço de mundo, eles devem ser transformado para o espaço do olho. Isso se dá com a matriz de visão.

O espaço do olho transforma o mundo de acordo com a posição e orientação da câmera. Ao final da transformação a câmera está na posição 0,0,0 olhando para +z. O mundo é transformado de maneira que a câmera continue a ver o que ela deveria ver em sua posição original. Na imagem, é mostrado como a câmera é posicionada no OpenGL. Em Direct3D a câmera aponta para +z.

Não existe um objeto câmera que é transformado. A posição e orientação da câmera são utilizadas apenas para transformar o mundo de maneira que a câmera fique na posição 0,0,0 e olhando para +z.

Essa transformação faz esse reposicionamento do mundo para permitir que a próxima etapa de visualização aconteça de maneira rápida e eficiente.

Sistema de coordenadas de clipping homogêneo

Após os vértices serem reposicionados levando em consideração a câmera, é hora de projetá-los na tela.

Os vértices são transformados pela matriz de projeção. Após a transformação eles estão num espaço chamado

espaço de clipping homogêneo que permite o pipeline gráfico executar o algoritmo de clipping que remove as partes das primitivas que não são visíveis.

Sistema de coordenadas normalizadas de dispositivo

Após o clipping os vértices têm seu x,y e z divididos por w, o que os deixa no espaço normalizado de dispositivo.

Sistemas de coordenadas da tela

A última transformação é a transformação de viewport. Essa transformação é quem coloca os vértices na posição correta na tela e permite que os triângulos sejam rasterizados.

Transformações principais

As principais transformações que nós trabalharemos serão as transformações de mundo, de visão e de projeção.

Matrizes de Mundo

Essas matrizes variam dependendo da forma como se deseja posicionar os objetos no mundo.

Matrizes de Visão

Essa matriz que coloca a posição da câmera no processamento gráfico.

Matrizes de Projeção

A matriz de projeção vai definir a forma como os vértices serão projetados.

Renderizando triângulos com perspectiva

Após as matrizes de mundo, visão e projeção serem definidas, é preciso multiplicá-las na ordem World * View * Proj e depois passar a matriz resultando para o vertex shader.

No vertex shader, a matriz tem que ser usada para multiplicar as posições dos vértices.

Renderizando triângulos com perspectiva

Renderizando triângulos com perspectiva

Exercício

Renderize 3 triângulos, um atrás do outro. Utilize 1 único vertex buffer para os 3 triângulos. (Dica: Use uma matriz de mundo para cada triângulo).

Posicione a câmera em um ângulo que permita ver os 3 triângulos

Trate o input do teclado e faça a câmera se mover.

Exercício

Anime os 3 triângulos da seguinte forma:– 1 triângulo rotaciona no eixo y fixo na posição 0,

0, 0– 1 triângulo rotaciona no eixo y fixo na posição 10,

0, 0.– 1 triângulo rotaciona no eixo y na posição 20, 0, 0

Renderizando triângulos com textura

Texturas são um dos principais elementos que dão realismo à um cenário 3D.

Texturas são imagens que são colocadas nos modelos 3d que dão detalhes aos modelos.

Renderizando Triângulos com texturas

Para criar texturas, nós usaremos a função D3DXCreateTextureFromFile.

Renderizando Triângulos com texturas

Na hora de renderizar, usamos a função SetTexture do efeito.

Renderizando triângulos com textura

Temos que definir as coordenadas de textura, carregar o vertex buffer e definir o vertex declaration.

Renderizando triângulos com textura

Exercício

Coloque os triângulos do exercício anterior usando texturas.

Use texturas diferentes para cada triângulo.

Produto Escalar

•Vamos achar o tamanho de b, usando os lados a e c e o ângulo entre a e c?•a^2 = h^2 + c1^2 .... b^2 = h^2 + c2^2 ... h^2 = a^2 – c1^2•b^2 = a^2 – c1^2 + c2^2 .... c = c1 + c2•b^2 = a^2 – c1^2 + (c-c1)^2•b^2 = a^2 – c1^2 + c^2 – 2cc1 + c1^2 ... cos theta = c1/a•b^2 = a^2 + c^2 – 2cc1 ... b^2 = a^2 + c^2 – 2ca cos theta

Lei dos Cosenos

A fórmula achada é a lei dos cosenos. Ela permite achar o tamanho de b usando os

lados a e c e o ângulo entre eles. b^2 = a^2 + c^2 – 2*a*c*cos(ab)

Antes uma informação ...

|a| = tamanho do vetor a |a| = raiz(ax^2 + ay^2 + az^2)

Usando vetores

|b-a|^2 = |a|^2 + |b|^2 – 2*|a|*|b|*cos (ab) |b-a|^2 - |a|^2 - |b|^2 = -2*|a|*|b|*cos(ab)(bx-ax)^2 + (by-ay)^2 + (bz-az)^2 – ax^2 – ay^2 – az^2 – bx^2 – by^2 – bz^2bx^2 – 2bxax + ax2 + by^2 – 2byay + ay^2 + bz^2 – 2bzaz + az^2 - ...-2bxax - 2byay – 2bzaz = -2*|a|*|b|*cos(ab)axbx + ayby + azbz = |a|*|b|*cos(ab)

Produto escalar: a * b = axbx + ayby + azbz. Ou seja, o produto escalar entre 2 vetores calcula uma informação de ângulo entre os vetores. Se os 2 vetores tiverem tamanho 1, o produto escalar dá o coseno entre os vetores.

Projetando um vetor em outro

O que acontece quando um dos vetores possui tamanho 1?

a * b = |a| * cos (ab) Nesse caso a * b dá o tamanho do vetor a

projetado no vetor b. Então para achar o vetor projetado de a em

b basta achar o a * b e multiplicar pelo vetor b .... a * b = t .... proj_a = multiplicar(b, t)

Exercício

Crie uma classe Vector3d com x,y e z como floats. Crie a função Length que acha o tamanho do vetor Crie a função Normalize que normaliza esse vetor

(Basta dividir os componentes pelo tamanho do vetor)

Crie a função Dot que acha o produto escalar entre 2 vetores

Ache o vetor projetado de um vetor em outro normalizado.

Produto Vetorial

Outra ferramenta extremamente importante para programadores gráficos é o produto vetorial.

O produto vetorial entre dois vetores não paralelos é outro vetor perpendicular aos dois vetores.

O produto vetorial é escrito como:– a x b = c

Onde a, b e c são vetores e c é perpendicular aos vetores a e b.

Como calcular o produto vetorial?

Abaixo a * b significa o produto escalar entre os vetores a e b.

a * (a x b) = 0 b * (a x b) = 0 Ou seja: axcx + aycy + azcz = 0 bxcx + bycy + bzcz = 0

Calculando o produto vetorial

Multiplicando a primeira linha por bx/ax:– bxcx + bxaycy/ax + bxazcz/ax = 0

Subtraindo a linha acima da segunda linha:– bxcx + bycy + bzcz – bxcx – bxaycy/ax –

bxazcz/ax = 0– bycy + bzcz = bxaycy/ax + bxazcz/ax– axbycy + axbzcz = bxaycy + bxazcz– axbycy – bxaycy = bxazcz – axbzcz– (axby – bxay)cy = (bxaz – axbz)cz

Calculando o produto vetorial

Multiplicando a primeira linha por by/ay:– byaxcx/ay + bycy + byazcz/ay = 0

Subtraindo da segunda linha:– bxcx + bycy + bzcz – byaxcx/ay – bycy –

byazcz/ay = 0– bxcx + bzcz = byaxcx/ay + byazcz/ay– aybxcx + aybzcz = byaxcx + byazcz– aybxcx – byaxcx = byazcz – aybzcz– (aybx – byax)cx = (byaz – aybz)cz

Logo...

(axby – bxay)cy = (bxaz – axbz)cz (aybx – byax)cx = (byaz – aybz)cz Vamos alterar a segunda linha: -1*(axby – bxay)cx = (byaz – aybz)cz (axby – bxay)cx = (aybz – byaz)cz

Logo .... (parte 2)

(axby – bxay)cy = (bxaz – axbz)cz (axby – bxay)cx = (aybz – byaz)cz Queremos encontrar cx, cy e cz que são nossas

incógnitas. Que valores cx, cy e cz possuem? Ele podem possuir infinitos valores ... Porém se você

selecionar um valor qualquer ... Coisas ruins podem acontecer ... como divisão por zero ... ouch.

Logo .... (parte 3)

(axby – bxay)cy = (bxaz – axbz)cz (axby – bxay)cx = (aybz – byaz)cz E se selecionarmos cz = axby – bxay? Nesse caso:

– cy = bxaz – axbz– cx = aybz – byaz

Para achar as incógnitas não precisa de nenhuma divisão ... o que é bom.

Portanto, o produto vetorial entre vetores a x b:– a x b = c = (aybz – byaz, bxaz – axbz, axby – bxay)

Exercício

Adicione o calculo vetorial no último exercício e faça testes

Iluminação

Iluminação permite a geração de cenas mais interessantes e realistas.

Existem diversas formas de calcular a iluminação de uma cena. Nós utilizaremos um modelo de iluminação semelhante ao Direct3D e OpenGL, porém usaremos o cálculo de iluminação por pixel.

Iluminação

A cor de um pixel é calculada da seguinte forma:

Cor = Contribuição Emissiva + Contribuição Ambiente + Contribuição Diffusa + Contribuição Especular.

Contribuição Emissiva

Essa contribuição representa a luz emitida pelo próprio objeto.

Contribuição Ambiente

Consiste no quanto o objeto reflete a iluminação espalhada pelo ambiente.

A contribuição ambiente é calculada da seguinte forma:– Material Ambiente * Iluminação Ambiente– Material Ambiente representa o quanto da

iluminação ambiente o objeto reflete.

Contribuição Difusa

Essa contribuição representa o quanto o objeto reflete para todos os lados a iluminação direta das luzes.

A contribuição difusa é calculada da seguinte forma:– Max((N * L),0) * Material Diffuso * Iluminação Diffusa– N = Normal normalizada do ponto em que está sendo

calculada a iluminação.– L = Vetor para luz normalizado.– Material Diffuso representa o quanto da iluminação diffusa

o objeto reflete.

Contribuição Especular

A contribuição especular representa o quanto de luz direta é refletida na direção principal de reflexão de uma luz.

A contribuição especular é calculada da seguinte forma:

– Max((H * N),0)^shininess * Material Especular * Iluminação Especular

– H = vetor da luz + vetor do olho – shininess define o quão brilhoso é o objeto.– Material Especular define o quanto o objeto reflete a

iluminação especular.

Renderizando objetos com iluminação por pixel

Renderizando objetos com iluminação por pixel

Transformando Normais

Para transformar normais é necessário multiplicá-las pela inversa da transposta da matriz de mundo.

Ou seja: n’ = n * (world^t)^-1

Compreendendo Transformações de Normais*

Primeiro: Estudar equação do plano (ver parte do ppt de física)

[nx ny nz –nP0] * [px py pz 1]^t = 0 q * p^t = 0 q * M * (p * R)^t = 0 q * M * R^t * p^t = 0 M * R^t = Identidade M = (R^t)^-1

Exercício

Implemente o exemplo anterior usando iluminação por pixel.

Exportando, Carregando e Renderizando modelos .X

Criar objetos 3D no código é relativamente extraordinariamente complexo de se fazer quando algo sofisticado é desejado.

A maneira correta de se criar cenários e objetos 3d é modelando-os em programas como o 3D Studio Max.

O problema é que o 3D Studio Max pode exportar em qualquer formato desejado (Se você implementar plugins).

Logo precisamos definir um formato com o qual trabalharemos.

Arquivo .X (O modelo, não a série)

Arquivos .X são interessantes porque a micro$oft já se deu o trabalho de implementar um carregador.

Nós precisamos no entanto achar um plugin para o 3ds que exporte arquivos .X. (Procure no google por Panda DirectX Exporter).

Nós iremos carregar o modelo usando a API do DirectX e depois extrairemos a informação geométrica, e com isso, iremos renderizar na tela modelos realmente interessantes...

Carregando Arquivos .X (Para pipeline fixo)

Renderizando Malhas DirectX (Usando pipeline fixo)

Exercício

Exporte um modelo no 3D Studio Max e carregue-o e renderize-o usando pipeline fixo.

(Para casa) Exporte um arquivo .ASE e tente escrever um parser dele.

Preparando malhas para uso em shaders

Preparando malhas para uso em shaders

Após carregar a malha, nós criamos uma nova malha com um formato compatível com o nosso vertex shader.

Nesse formato apenas um vertex buffer deve ser usado. (tentei com mais de 1 e não deu).

Renderizando a malha com shaders

Exercício

Renderize a malha usando shaders.

Animação utilizando interpolação entre keyframes

Nesse tipo de animação, um personagem é modelado em diferentes posições (como as imagens de desenhos animados) e para renderizar uma posição intermediária entre 2 key frames é feita uma interpolação linear entre 2 frames:

– Frame1.pos * (1-t) + Frame2.pos * t– t define a posição intermediária da animação. 0 representa

o frame 1 e 1 representa o frame 2. Um t = 0.5 representa uma posição no meio do caminho entre os frames 1 e 2.

Animação utilizando interpolação linear entre keyframes

Para conseguirmos fazer a interpolação de maneira rápida (na GPU) precisamos enviar as posições dos dois keyframes para o vertex shader interpolar.

Para isso usaremos coordenadas de texturas.

Animação usando interpolação entre keyframes

Animação usando interpolação entre keyframes

Animação usando interpolação entre keyframes

Animação usando interpolação entre keyframes

Exercício

Implemente um exemplo que anima um triângulo.

Alpha Blending

Usado para renderizar objetos com transparência.

Quando o alpha blending está habilitado, a cor final de um pixel após renderizar uma primitiva é calculada da seguinte maneira:

Final Color = (New Color * SourceBlendFactor) + (Pixel Color * DestBlendFactor)

Alpha Blending

O resultado do pixel shader é uma cor. Essa cor possui os componentes rgba. Nós trabalhos com os componentes rgb até agora para dar

a cor aos objetos. O componente alpha permite definir um nível de

transparência quando alpha blending está habilitado. Geralmente 1 representará um pixel totalmente opaco, e 0

um pixel totalmente transparente. Para definir o componente alpha da cor gerada pelo pixel

shader use:

Alpha Blending

Para habilitar alpha blending:

Alpha Blending

Para definir os fatores de blending:

Exercício

Renderize o triângulo da cena de forma transparente

(enigma) Se o triângulo transparente for renderizado antes do chão e do objeto .x ele terá sua cor calculada com o fundo preto, como fazer a cena aparecer transparente atrás do triângulo?

(dica) Utilize um único float para definir o alpha do triângulo.

Volumes de sombras

Sombras são muito importantes para posicionar objetos em um ambiente 3d. Através das sombras é possível saber a relação espacial entre os objetos.

A técnica de volume de sombras gera sombras através da criação de um volume de sombras gerado através da malha do objeto e o stencil buffer.

Volume de sombras

O volume de sombra é um objeto 3d que representa a região em sombra de outro objeto.

Stencil Buffer

É necessário usar o stencil buffer para a técnica de volumes de sombra.

O stencil buffer é um buffer que permite um controle à nível de pixel sobre o que é renderizado na tela.

Quando o stencil buffer está habilitado, um pixel só é renderizado quando ele passa na seguinte operação:

(StencilRef & StencilMask) CompFunc (StencilBufferValue & StencilMask)

Todos esses elementos são customizáveis e o StencilBufferValue é o valor do stencil buffer.

O Algoritmo

Limpar o Stencil Buffer para 0 Gerar o volume de sombras através da malha do objeto e da

posição da luz. Renderizar o volume de sombra sem back face culling e sem

z-writing. Por toda a região de todas as faces de frente do volume de

sombra, adicionar 1 ao stencil buffer. Por toda a região de todas as faces de trás do volume de

sombras, remover 1 do stencil buffer. Se um determinado pixel falhar em z, não alterar o stencil

buffer. Renderizar um Quad ocupando toda a tela, naquelas regiões

do stencil que forem diferentes de 0, escurecer.

O Algoritmo

Limpar o Stencil Buffer para 0

O Algoritmo

Gerar o volume de sombras através da malha e da posição da luz.

Para isso, usaremos uma malha especial.

O Algoritmo

Durante a criação do vertex buffer do volume de sombras para cada aresta do objeto definir triângulos da seguinte forma:

(triangulos da aresta v1v2) = {v1,~v2, ~v1, v1, v2, ~v2} .

~v1 é igual a v1 só que com o w = 0. ~v2 tem a mesma relação com v2.

O Algoritmo

Durante a renderização do volume de sombras, todos os vértices com w = 0 são afastados da luz uma certa distância e depois transformados.

Os vértices com w != 0 são transformados normalmente.

O Algoritmo

Para renderizar o volume sem back face culling e sem z-writing.

O Algoritmo

Para permitir que o stencil buffer faça uma operação com as faces da frente e outra operação com as faces de trás.

O Algoritmo

Para definir a adição no stencil buffer para todas as faces de frente e subtração para todas as faces de trás.

O Algoritmo

Use alpha blending para renderizar volumes transparentes!

O Algoritmo

Nesse momento o stencil buffer só possui valores diferentes de zero nas regiões de sombra. Para renderizar apenas nessas regiões:

Exercício

Coloque o triângulo renderizando com um volume de sombras.

Bump Mapping

Bump mapping é uma técnica que aumenta o realismo das cenas ao dar a aparência de mais detalhe em malhas geométricas.

Para implementar a técnica, deve ser usada uma textura especial que define como é a superfície de um determinado triângulo de uma malha.

Essa informação de superfície é usada na hora de calcular iluminação, e com isso o objeto parece ter muito mais detalhe do que ele possui.

Bump Mapping

Bump Mapping

A textura de bump mapping que nós utilizaremos armazenará as normais da superfície dos triângulos.

Quando nós calcularmos a iluminação por pixel, nós usaremos a normal adquirida pelo bump map e com isso daremos riqueza de detalhe à superfície.

Um problemão no entanto

As normais da textura estão num espaço próprio delas. Chamado de espaço tangente.

Ou nós convertemos a normal para o espaço de mundo, ou nós convertemos os outros vetores para o espaço tangente.

Vamos converter todos os vetores para o espaço tangente.

Matriz de mudança de base

A matriz de mudança de base é quem vai converter do espaço do mundo (onde a gente tem calculado a iluminação até agora) para o espaço tangente.

Ela é composta por 3 componentes:– Vetor normal: A normal do triângulo.– Vetor tangente: É um vetor novo, que deve vir em cada

vértice, e deve ser tangente à face do triângulo. O vetor tangente é perpendicular à normal.

– Vetor Binormal: É calculado usando um produto vetorial entre a normal e a tangente.

Todos esses vetores são normalizados.

Matriz de mudança de base

A cara da matriz é a seguinte:– (tx, nx, bx)– (ty, ny, by)– (tz, nz, bz)

Os vetores (da luz, do olho etc) são multiplicados por essa matriz da seguinte forma:

– (tx, nx, bx)– (ty, ny, by)– (lx, ly, lz) * (tz, nz, bz)

O exemplo acima multiplicou o vetor da luz no espaço de mundo pela matriz.

O resultado é o vetor da luz no espaço tangente que nós podemos usar para calcular a iluminação normalmente.

Mas porque essa matriz faz isso?

Matriz de mudança de base

O vetor resulta da multiplicação anterior é composto por 3 operações:– x = dot(L,T)– y = dot(L,N)– z = dot(L,B)

Lembra que ao fazer o dot de 2 vetores sendo que 1 é normalizado, você acha a projeção do outro vetor no vetor normalizado? (T, N e B são normalizados...)

dot(L,T) calcula a projeção de L no vetor tangente. dot(L,N) calcula a projeção de L no vetor normal. dot(L,B) calcula a projeção de L no vetor binormal. Essas projeções geram um vetor nos eixos x, y e z se a tangente

representar o eixo x, a normal representar o eixo y e a binormal representar o eixo z.

Matriz de mudança de base

Digamos que as normais são feitas da seguinte maneira:– O mapa está deitado no eixo x,z – As normais sem inclinação apontam para o eixo y.– Esse é o espaço tangente.

Os vetores Tangente, Normal e Binormal representam os vetores x,y e z porém no espaço de mundo.

Ao projetar L no vetor tangente, você acha um valor que representa o tamanho de L no vetor tangente. Esse tamanho é o mesmo no eixo x. Logo ao projetar L em T, você acha L em x.

Repetindo, T,N e B TEM QUE ESTAR NORMALIZADOS.

Vértices

Além da posição, da normal, e da coordenada de textura, você agora precisa adicionar a tangente e, caso desejado, mais uma coordenada de textura para o bump map.

Vertex Shader

Pixel Shader

Exercício

Implementar um triângulo com bump mapping.

Experimente gerar proceduralmente um mapa normal com algum formato.

Grafos de Cena

Grafos de cena são estruturas de dados muito importantes que são utilizadas para diferentes propósitos.

O propósito mais importantes do grafo de cena é hierarquia de transformações.

Um grafo de cena geralmente tem o formato de uma árvore n-ária.

Grafo de Cena

Por exemplo, digamos que você queira colocar uma arma no braço de um guerreiro.

Para colocar o guerreiro no ambiente 3d, ele é transformado por sua matriz de mundo.

Para colocar a arma no mundo, primeiro a arma é tranformada para a posição do guerreiro, e depois é transformada para a posição da mão do guerreiro.

Grafo de Cena

Grafo de Cena

Grafo de Cena

Node: é a classe que permite implementar a hierarquia de transformações. Possui uma transformação como membro e uma lista de filhos.

Mesh: é uma classe que implementa a renderização de algum objeto.

Grafo de Cena

No exemplo da arma, poderíamos ter 2 malhas, uma representando o torso do guerreiro e outra representando a arma.

A transformação da arma poderia ser uma translação +5 em x.

Após definir a transformação de mundo do guerreiro, é renderizada a malha do guerreiro. Depois essa transformação é usada para calcular a transformação de mundo da arma.

Culling em cenários usando octrees

Os cenários dos jogos de hoje são complexos e detalhados. Logo, são pesados para renderizar.

Porque enviar todo o cenário para a placa gráfica a cada frame, se na maioria das vezes você só consegue ver parte do cenário?

A octree é uma estrutura de dados que permite reduzir o custo de renderização pois com ela é possível cálcular as regiões do cenário que são visíveis.

A octree é uma árvore onde cada nó possui 8 filhos.

Culling usando octrees

Equação do plano

A equação de um plano é definida geralmente de duas formas: (P – P0) * n = 0. Nesse caso P é um ponto qualquer, P0 é um

ponto no plano e n é a normal normalizada do plano. Se P – P0 (que gera um vetor) dot n resultar em 0 é porque o ponto está no plano. Essa equação após distribuir os dots vira:

– P * n – P0 * n = 0 Ax + By + Cz + D = 0. Nesse caso A, B, C e D definem o plano.

Se você colocar um ponto (x,y,z) nessa fórmula e ela der zero é porque o ponto está no plano. A,B,C correspondem aos componentes da normal e D = - P0 * n.

Calculando a distância de um ponto à um plano

O que representa P * n e P0 * n na equação P * n – P0 * n ?

Calculando a distância de um ponto à um plano

P0 * n = | P0 | * cos (theta) | P0 | * cos (theta) = tamanho de P0 projetado na normal =

distância do plano à origem. P * n = | P | * cos (theta) | P | * cos (theta) = tamanho de P projetado na normal. P * n – P0 * n = Distância de P ao plano. Logo se essa distância for 0, o ponto está no plano. Se essa distância for positiva, o ponto está no lado do plano

em que aponta a normal. Se essa distância for negativa, o ponto está no lado oposto ao

qual a normal aponta.

Calculando a interseção de uma AABB com um plano

A idéia do algoritmo é calcular na AABB um mínimo e um máximo baseado na posição do plano e a seguir verificar as distâncias dos pontos min e max com relação ao plano.

Calculando a interseção de uma AABB com um plano

Verificando se uma AABB está dentro de um Frustum

Calculando um frustum

Calculando um frustum

Criando a Octree

Criando a octree

Criando a Octree

Criando a octree

Renderizando com a octree

Adicionando objetos à octree

Exercício

Implemente um código que calcula uma AABB em um objeto e só renderiza esse objeto se sua AABB estiver interceptando o frustum.

Implemente uma octree e adicione esse objeto à octree. Renderize usando a octree.

Quaternions

Quaternions são ferramentas importantes para realizar rotações.

Com multiplicação de quaternions você pode realizar rotações.

Realizar uma interpolação entre duas rotações é “relativamente” simples com quaternions.

Quaternions

Para entender quaternions, precisamos primeiro aprender sobre números complexos.

Com o conjunto dos números reais dá pra representar uma quantidade considerável de números.

Nós no entanto não conseguimos representar a raiz de -4. Porque nenhum número vezes ele mesmo dá um número negativo.

Números complexos

Números complexos possuem uma parte real e uma parte imaginária como escrito abaixo:

a + bi a = parte real. bi = parte imaginária. Então os números abaixo são todos números

complexos: 4 + 2i 1 + 10i 5 4i

Números complexos

A parte imaginária possui uma peculiaridade: i^2 = -1 Ou seja: 2i * 2i = 4i^2 = -4 Com isso nós podemos encontrar a raiz de

números negativos: raiz de -4 = raiz de 4 * i^2 = 2i raiz de -1 = raiz de i^2 = i

Quaternions

Quaternions são números hiper-complexos (sério!).

Eles possuem 3 partes imaginárias e são escritos como:

a + bi + cj + dk Então o número abaixo é um quaternion: 2 + 3i + 4j + 5k

Quaternions

Assim como números complexos: i^2 = j^2 = k^2 = -1 Porém números complexos possuem algumas definições

adicionais (aqui começa a loucura). i * j = k j * i = -k j * k = i k * j = -i k * i = j i * k = -j i * j * k = (i * j) * k = i * (j * k) = -1

Multiplicando dois quaternions

q0*q1 = (w0 + x0*i + y0*j + z0*k) * (w1 + x1*i + y1*j + z1*k).

Vamos aplicar multiplicação distributiva: w0*w1 + w0*x1*i + w0*y1*j + w0*z1*k + x0*i*w1 +

x0*i*x1*i + x0*i*y1*j + x0*i*z1*k + y0*j*w1 + y0*j*x1*i + y0*j*y1*j + y0*j*z1*k + z0*k*w1 + z0*k*x1*i + z0*k*y1*j + z0*k*z1*k

(w0*w1 – x0*x1 – y0*y1 – z0*z1) + w0*(x1*i + y1*j + z1*k) + w1*(x0*i + y0*j + z0*k) + x0*y1*k - x0*z1*j – y0*x1*k + y0*z1*i + z0*x1*j - z0*y1*i

Definições para facilitar a vida:

Um quaternion: q0 = (w0 + x0*i + y0*j + z0*k) = (w0 + v0)

Ou seja: v0 = (x0*i + y0*j + z0*k) Outro quaternion: q1 = (w1 + x1*i + y1*j + z1*k) =

(w1 + v1) Produto escalar: v0 * v1 = (x0*x1 + y0*y1 + z0*z1) Produto vetorial: v0 x v1 = (y0*z1*i – z0*y1*i +

z0*x1*j – x0*z1*j + x0*y1*k – y0*x1*k) Escala: a * v0 = (a*x0*i + a*y0*j + a*z0*k)

Usando nossas definições

q0*q1 = (w0*w1 – x0*x1 – y0*y1 – z0*z1) + w0*(x1*i + y1*j + z1*k) + w1*(x0*i + y0*j + z0*k) + x0*y1*k - x0*z1*j – y0*x1*k + y0*z1*i + z0*x1*j - z0*y1*i

Torna-se: q0*q1 = w0*w1 – v0*v1 + w0*v1 + w1*v0 +

v0 x v1

O conjugado de um quaternion

O conjugado de um quaternion é definido como:– q0 = (w0 + v0) – quaternion normal– q0* = (w0 – v0) – conjugado do quaternion

Rotacionando vetores

Crie 1 quaternion com w = 0 e com os componentes x,y e z do próprio vetor que você quer rotacionar:

– (x, y, z) -> (0, x, y, z) = v Crie outro quaternion onde w = cos(theta/2), o x,y,z

é o eixo de rotação normalizado e escalado por sin(theta/2):

– q = (cos(theta/2) + sin(theta/2)d) A multiplicação qvq* gera um quaternion cujos

elementos x,y,z são o vetor 3D rotacionado pelo eixo d do quaternion q por theta graus.

Rotacionando vetores

Ou seja, digamos que queremos rotacionar o vetor (1, 0, 0) no sentido anti-horário do eixo (0, 0, 1). Queremos rotacionar 90 graus gerando o vetor (0, 1, 0).

Vamos criar dois quaternions v e q: v = (0, 1, 0, 0) q = (cos(45) + sin(45)(0, 0, 1)) = (cos(45), 0, 0,

sin(45)) Logo: qvq* = (0, 0, 1, 0) = Rotacionamos o vetor!

Porque isso funciona?

Para entender porque quaternions funcionam precisamos primeiro aprender como criar uma matriz que rotaciona um vetor dado um eixo qualquer.

Rotacionando vetores pelo eixo z

Para rotacionar um vetor em xy usando o eixo z usamos a matriz:

(cos(theta) sin(theta) 0) (-sin(theta) cos(theta) 0) (vx vy vz) * (0 0 1) Como podemos fazer para rotacionar um

vetor por um eixo qualquer?

Matriz de Mudança de Base

Assim como no bump mapping vamos criar uma matriz que projeta o vetor em 3 outros vetores normalizados que formam uma base orthonormal.

Um desses vetores base é o eixo de rotação. Os outros dois vetores podem ser dois vetores quaisquer ortogonais entre si.

O Algoritmo

Criar a matriz de vetores base sendo que o vetor base que representa o eixo de rotação precisa ser a última coluna gerando o valor em z. A matriz abaixo mostra os vetores base a, b e rot, todos normalizados e rot é o eixo de rotação.

(ax bx rotx) (ay by roty) (vx vy vz) * (az bz rotz) = (v*a v*b v*rot) Com essas projeções é possível achar o vetor em nosso espaço x,y,z padrão. Sendo que

o eixo de rotação passa a representar o eixo z. Depois de multiplicar o vetor pela matriz acima, nós o multiplicamos pela matriz de

rotação no eixo z mostrada anteriormente. Com isso o vetor rotaciona no eixo z que representa o eixo de rotação rot.

Depois de rotacionar nosso vetor no eixos x,y,z padrão, devemos calcular como fica esse vetor rotacionado usando como vetores base os vetores a, b e rot. Para achar isso podemos calcular na mão, multiplicando os vetores a, b e rot pelos valores achados em x, y e z após a rotação:

– vfinal = vx * a + vy * b + vz * rot Ou podemos multiplicar o vetor rotacionado pela matriz inversa da matriz formada por a,b

e rot.

O Algoritmo (Resumidamente)

V = vetor que queremos rotacionar RV = vetor rotacionado MB = Matriz criada a partir de A, B e Rot RotZ = Matriz que rotaciona pelo eixo Z MB^t = Matriz transposta da MB. Matriz

Transposta = Inversa para rotações. O Algoritmo aplica a seguinte fórmula: RV = V * MB * RotZ * MB^t

Código

Código sem problemas de cosseno

Analisando a matriz de rotação

Vamos assumir que:– cos(theta) = c– sin(theta) = s

R = (ax bx rotx) (c s 0) (ax ay az) (ay by roty) (-s c 0) (bx by bz) (az bz rotz) (0 0 1) (rotx roty rotz)

R = (a^t b^t rot^t) (c s 0) (a) (-s c 0) (b) (0 0 1) (rot) R = (a^t b^t rot^t) (c*a+s*b) (-s*a+c*b) (rot) R = a^t(c*a+s*b) + b^t(-s*a+c*b) + rot^t*rot R = c*(a^t*a + b^t*b) + s*(a^t*b – b^t*a) + rot^t*rot

Algumas fórmulas

v = (a*v)*a + (b*v)*b + (rot*v)*rot = x0*a + x1*b + x2*rot

rot x v = rot x (x0*a + x1*b + x2*rot) = x0*(rot x a) + x1*(rot x b) + x2*(rot x rot)

rot x v = x0*(rot x a) + x1*(rot x b) = x0*b – x1*a rot x v = x0*b – x1*a = (a*v)*b - (b*v)*a rot x v = (v*a^t)*b – (v*b^t)*a rot x v = v*(a^t*b – b^t*a)

Algumas Fórmulas

v * I = v v * I = (a*v)a + (b*v)b + (rot*v)rot v * I = (v*a^t)a + (v*b^t)b + (v*rot^t)rot v * I = v*(a^t*a + b^t*b + rot^t*rot) I = a^t*a + b^t*b + rot^t*rot Voltando a nossa fórmula original: R = c*(a^t*a + b^t*b) + s*(a^t*b – b^t*a) + rot^t*rot R = c*(I – rot^t*rot) + s*(a^t*b – b^t*a) + rot^t*rot R = cI – c*rot^*rot + s*(a^t*b – b^t*a) + rot^t*rot R = cI + s*(a^t*b – b^t*a) + (1 – c)rot^t*rot

Algumas fórmulas

rot x (rot x v) = rot x (x0*b – x1*a) = x0*(rot x b) – x1*(rot x a) = -x0*a –x1*b

v = (a*v)*a + (b*v)*b + (rot*v)*rot = x0*a + x1*b + x2*rot -x*a – x1*b = x2*rot – v = (rot*v)*rot - v rot x (rot x v) = (rot*v)*rot – v = v*(rot^t*rot) – v rot x (rot x v) + v = v*(rot^t*rot) Voltando à formula: R = cI + s*(a^t*b – b^t*a) + (1 – c)rot^t*rot Se a gente multiplicar um vetor por essa matriz: vR = cv*I + sv*(a^t*b – b^t*a) + (1 – c)*v*(rot^t*rot) vR = cv*I + s*(rot x v) + (1 – c)*(rot x (rot x v) + v) vR = cv + s*(rot x v) + (1 – c)*(rot x (rot x v)) + (1 – c)v vR = cv + s*(rot x v) + (1 – c)*(rot x (rot x v)) + v – cv vR = v + sin(theta)*(rot x v) + (1 – cos(theta))*(rot x (rot x v))

Ou Seja

Quando você multiplica um vetor pela matriz RV abaixo:

RV = V * MB * RotZ * MB^t Você está fazendo na realidade o seguinte

cálculo: vR = v + sin(theta)*(rot x v) + (1 – cos(theta))*(rot x

(rot x v))

Código de rotação mais rápido

Voltando aos quaternions...

Foi dito que:– dado um vetor v = (x, y, z) armazenado em um

quaternion q0 = (0 + v)– Um eixo de rotação normalizado r = (rx, ry, rz)

armazenado em um quaternion da seguinte forma: q1 = (cos(theta/2) + sin(theta/2)*r)

– A multiplicação: q1q0q1* gera um quaternion cujos componente x, y, z consistem no vetor v rotacionado pelo eixo r por theta graus.

Vamos analisar essa multiplicação

Vamos considerar:– s = sin(theta/2)– c = cos(theta/2)

Lembrando:– q0*q1 = w0*w1 – v0*v1 + w0*v1 + w1*v0 + v0 x v1

q1q0q1* = (c + s*d)(0 + v)(c – s*d) q1q0q1* = (-s*d*v + c*v + s*d x v)(c – s*d) q1q0q1* = [-s*c*d*v – (c*v + s*d x v)*(-s*d)] + (-s*d*v)*(-s*d) + c*(c*v +

s*d x v) + (c*v + s*d x v) x (-s*d) -s*c*d*v + s*c*d*v + s^2*(d x v)*d + s^2*(d*v)*d + c^2*v + s*c*(d x v) -

s*c*(v x d) - s^2*(d x v) x d s^2*(d*v)*d + c^2*v + s*c*(d x v) + s*c*(d x v) - s^2*(d x v) x d s^2*(d*v)*d + c^2*v + 2*s*c*(d x v) - s^2*(d x v) x d s^2*(d*v)*d + c^2*v + 2*s*c*(d x v) + s^2*d x (d x v)

Continuando a análise

s^2*(d*v)*d + c^2*v + 2*s*c*(d x v) + s^2*d x (d x v) (1 – s^2)*v + 2*s*c*(d x v) + s^2*[(d*v)*d + d x (d x

v)] v – s^2*v + 2*s*c*(d x v) + s^2[(d*v)*d + d x (d x v)] v + 2*s*c*(d x v) + s^2[(d*v)*d – v + d x (d x v)] Porém: d x (d x v) = (d*v)*d – (d*d)*v = (d*v)*d – v.

Logo: q1q0q1* = v + 2*s*c*(d x v) + 2*s^2*d x (d x v)

Continuando

2*s*c = 2*sen(theta/2)*cos(theta/2) = ? Para resolver essa fórmula vamos provar a

seguinte fórmula: sin(a + b) = sin(a)cos(b) + sin(b)cos(a)

Prova da identidade trigonométrica

RPQ = pi/2 – RQP = pi/2 – (pi/2 – RQO) = RQO = a OP = 1 PQ = sin(b) OQ = cos(b) AQ/OQ = sin(a). AQ = sin(a)cos(b). AQ = RB. PR/PQ = cos(a). PR = cos(a)sin(b) sin(a + b) = PB = RB + PR = sin(a)cos(b) + cos(a)sin(b) Logo: sin(theta) = sin(theta/2)cos(theta/2) + cos(theta/2)sin(theta/2) sin(theta) = 2sin(theta/2)cos(theta/2)

Outra Prova

OP = 1 PQ = sin(b) OQ = cos(b) OA/OQ = cos(a). OA = cos(a)cos(b). RQ/PQ = sin(a). RQ = sin(a)sin(b). RQ = BA. cos(a + b) = OB = OA – BA = cos(a)cos(b) – sin(a)sin(b) Logo cos(theta) = cos(theta/2)cos(theta/2) – sin(theta/2)sin(theta/2) cos(theta) = cos^2(theta/2) – sin^2(theta/2) E Ainda: cos(theta) = (1 – sin^2(theta/2)) – sin^2(theta/2) cos(theta) = 1 – 2*sin^2(theta/2) 1 – cos(theta) = 2*sin^2(theta/2)

Voltando à fórmula do quaternion

q1q0q1* = v + 2*s*c*(d x v) + 2*s^2*d x (d x v) q1q0q1*= v + sin(theta)*(d x v) + (1 – cos(theta))*d x

(d x v) Comparando à fórmula de rotação por um eixo

qualquer: vR = v + sin(theta)*(rot x v) + (1 – cos(theta))*(rot x (rot x v)) Podemos ver então que a multiplicação de quaternions

q1q0q1* resulta na mesma equação da rotação de vetores por um eixo qualquer mostrada anteriormente.

Exercício

Rotacione um vetor usando multiplicação de quaternions.

Tópicos para Programação Distribuída

Fundamentos de Distribuição Técnicas de Distribuição para Jogos

Modelos de Distribuição

Cliente / Servidor

Modelos de Distribuição

Peer to Peer

Cliente / Servidor

Capaz de manter milhares de jogadores no mesmo jogo (Utilizado em Massively Multiplayer Games).

Há necessidade de manter um servidor (Custos podem ser elevados).

Peer to Peer (Ponto a Ponto)

Armazena (geralmente) menos jogadores por instância de jogo.

Não requer servidor de grande poder computacional.

TCP – Transmission Control Protocol

Protocolo complexo que implementa a comunicação como um fluxo de bytes que trafega entre dois pontos (Circuito Virtual).

Garante a chegada das mensagens As mensagens chegam de forma ordenada Algoritmos para redução do número de

mensagens Comunicação 1 x 1

TCP - Conexão

Há necessidade de estabelecer conexão

Função connect() da API dos sockets.

TCP – Envio de Mensagens

Garantia de chegada

TCP – Término da conexão

Ocorre quando a função closesocket() é chamada.

UDP – User Datagram Protocol

Protocolo simples que implementa comunicação como um envio de mensagens de forma atômica.

Não há garantia de chegada Mensagens podem chegar desordenada ou

duplicadas Permite realização de broadcasts ou multicast

(comunicação 1 x N) Não há necessidade de se conectar.

Sockets

API criada para abstrair a conexão entre computadores em diferentes sistemas operacionais.

Um socket é formado pela união IP + Porta, que define unicamente uma parte de uma conexão.

Função socket()

Função que cria um socket. SOCKET socket(int family, int type, int protocol) family: Define a família de protocolos utilizados.

AF_INET = Protocolos convencionais da internet. type: Define o tipo de socket. SOCK_STREAM ou

SOCK_DGRAM. protocol: Define o tipo de protocolo dentro da família.

IPPROTO_TCP = TCP, IPPROTO_UDP = UDP.

Função socket()

Exemplo:

SOCKET s = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP).

Função connect()

Estabelece conexão int connect(SOCKET socket, const struct

sockaddr *serveraddr, int addrlen) socket: O socket do cliente serveraddr: O endereço (IP+Porta) do

servidor. addrlen: O tamanho da estrutura serveraddr.

Estrutura sockaddr_in

Estrutura passada para a função connect.

struct sockaddr_in {   

short sin_family;    //AF_INET

unsigned short sin_port;   

struct   in_addr sin_addr;   

char sin_zero[8];

}; Define o endereço de um socket remoto

Estrutura in_addr

Função connect()

SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

sockaddr_in addr = {0};addr.sin_family = AF_INET;addr.sin_port = htons(SERVER_PORT);addr.sin_addr.s_addr = inet_addr(“127.0.0.1”);connect(s, (const sockaddr*)&addr,

sizeof(addr));

Função bind()

Função usada pelo servidor para definir um socket local

int bind(SOCKET s, const sockaddr *localaddr, int addrlen);

s: o socket do servidor localaddr: o endereço local addrlen: o tamanho do endereço

Função bind()

SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

sockaddr_in addr = {0};addr.sin_family = AF_INET;addr.sin_port = htons(LIST_PORT);addr.sin_addr.s_addr = htonl(INADDR_ANY);bind(server, (const serveraddr*)&addr,

sizeof(addr));

Função listen()

Define um socket como passivo. Informa o S.O. que deve aceitar conexões direcionadas à esse socket.

int listen(SOCKET s, int backlog); s: o socket do servidor backlog: O número de conexões que podem

ficar pendentes para esse socket.

Função listen()

//continuação do código da bind()

listen(server, 8);

Função accept()

Chamada por um servidor TCP para retornar a próxima conexão completa do ínicio da fila de conexões completas.

SOCKET accept(SOCKET s, struct sockaddr * addr, int * addrlen);

s: o socket do servidor addr: o endereço do novo cliente addrlen: o tamanho do endereço Essa função retorna o socket do novo cliente e

bloqueia quando não há conexões terminadas.

Função accept()

while(true) {

addrlen = sizeof(addr);

cli_socket = accept(server, (...)&addr, &addrlen);

ProcessClientSocket(cli_socket);

}

Função closesocket()

Libera os recursos alocados ao socket. int closesocket(SOCKET s);

Função send()

função que envia bytes usando um socket conectado.

int send(SOCKET s, const char * buffer, int len, int flags);

s: o socket conectado por onde enviar os dados buffer: os bytes a serem enviados len: o número de bytes a serem enviados flags: Flags

Função send()

//continuando o código da connect

char msg[]= “coéeee!”;

send(s, msg, strlen(msg)+1, 0);

Função recv()

Função usada para receber dados de uma conexão.

int recv(SOCKET s, char * buffer, int len, int flags);

Função recv()

//continuando o codigo da send()

char reply[64];

recv(s, reply, 64, 0);

cout << reply;

Função sendto()

Usada com sockets UDP não conectados int sendto(SOCKET s, const char *buf, int

len, int flags, const struct *destaddr, int addrlen);

Função recvfrom()

Função usada em servidores UDP sem conexões.

int recvfrom(SOCKET s, char * buffer, int len, int flags, sockaddr *addr, int *addrlen);

Para usar a API de sockets no windows

Para inicializar a DLL do sockets use:

Para finalizar a DLL:

Inclua a lib: ws2_32.lib Header file: winsock2.h

Exercício

Implementar uma aplicação cliente / servidor usando TCP onde o cliente envia uma string para o servidor e o servidor escreve na tela aquilo que recebeu. O servidor só aceita 1 cliente de cada vez.

Exercício

Implementar uma aplicação cliente / servidor semelhante ao exercício 1 porém usando UDP.

Livros Importantes

TCP/IP Illustrated, Vol 1 Unix Network Programming, Vol 1

(Ambos do W. Richard Stevens!)

Técnicas de Distribuição para Jogos

Multithreading (Revisão)

Conhecimento fundamental (Xbox 360). Thread = Parte de um programa que executa

concorrentemente ou paralelamente a outras partes do programa. Comunicação entre partes é feita através de variáveis compartilhadas.

Um game = Thread Principal (Main) + Outras Threads

Multithreading

Múltiplas threads parecem executar paralelamente mesmo em um único processador devido ao time slicing.

Multithreading (Exemplo)

Criando uma thread

uintptr_t _beginthread( void( *start_address )( void * ), unsigned stack_size, void *arglist ); start_address: endereço de uma função que inicia a

execução da thread. stack_size: tamanho da pilha da nova thread. arglist: ponteiro para parâmetro passado para

thread.

Criando uma thread

int A = 0;

void Add(void * data){

A++;}

void main(){

_beginthread(Add, 0, NULL);

//tente comentar essa linhaSleep(1000);

cout << A;

char c;

cin >> c;}

Case Study: Project Gotham Racing

Core Thread Software threads

00 Update, physics, rendering, UI

1 Audio update, networking

10 Crowd update, texture decompression

1 Texture decompression

20 XAudio

1

Exercício

Evolua o exercício 1 de forma que o servidor se torne capaz de processar múltiplos clientes simultaneamente.

Utilize 1 thread por cliente.

Técnicas usando TCP, UDP e Sockets

Entendendo TCP

O Algoritmo de Naggle e o Delayed Ack.

Desligando o algoritmoBOOL b = TRUE;

setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, (char*)&b, sizeof(bool));

Usando sockets UDP conectados

sendto() em alguns sistemas operacionais realiza conexão, envio e desconexão.

Permite receber mensagens ICMP.

Enviando múltiplos buffers em Sockets

WSABUF MsgBuf[2];

MsgBuf[0].buf = &Cabecalho;

MsgBuf[0].len = sizeof(Cabecalho);

MsgBuf[1].buf = MsgChat;

MsgBuf[1].len = strlen(MsgChat);

WSASend(Socket, MsgBuf, 2, &BytesSent, Flags, NULL, NULL);

Implementando Garantia de Chegada

Como calcular o RTO (Retransmission Timeout Timer) ?– Levar em consideração o tempo estimado de

round trip time (SRTT) e a variação estimada dos tempos de round trip time (VARRTT).

Implementando Garantia de Chegada

Calculando RTT estimado (SRTT):– delta = RTT – SRTT– SRTT = SRTT + (delta / RTTINF)

Calculando variação de RTT (VARRTT):– VARRTT = VARRTT + ((| delta | - VARRTT) /

VARRTTINF) Calculando RTO:

– RTO = SRTT + VARRTT * VARINC

Exercício (Para casa)

Implemente um protocol de reliable UDP.

Técnicas para Ambientes Virtuais Distribuídos

Dead-Reckoning

O conceito de Dead-Reckoning Forma normalmente usada: pos = pos0 + (vel * dir) * deltat

Com Dead-Reckoning baseado a objetivos há a possibilidade de redução ainda maior no número de mensagens trocadas. – Ex: “Desejo ir para o destino x,y”.

Sincronização baseada em tempo de comando

Alterando velocidades é possível fazer uma sincronização visual.

T 0s T 1s

T 2s

Eliminando Informações Desnecessárias

Geralmente, não há necessidade de enviar informações sobre objetos não perceptíveis.

Balanceamento

Para conseguir processar um MMORPG é necessário quebrar o processamento entre diversos servidores.

Outras técnicas

Nível de detalhe Agregação de Mensagens

Exemplo de Servidor MMORPG

Construindo a Rede de servidores

A ferramenta permite dividir responsabilidades entre os servidores

Diagrama de Classes

Outras características

IOCP Culling usando Grid Balanceamento usando retângulos Reliable UDP + Last Update Sincronização por Tempo de Comando Dead Reckoning baseado em objetivos

Resultados

Exercício

Implemente uma demo onde existem 2 retângulos e cada retângulo sobre movimentação de um cliente diferente. Ao mover um retângulo, o outro cliente deve ver a movimentação.

Implemente dead reckoning para reduzir o número de mensagens trocadas.

Tópicos para Física

Teste de Interseção Raio – Plano Teste de Interseção Raio - Triângulo Teste de Interseção Esfera – Esfera Teste de Interseção Elipsóide - Elipsóide Teste de Interseção AABB – AABB Detecção de Colisão em cenário estático usando elipsóides Resposta à Colisão Picking Otimizando a colisão com Binary Space Partition Trees. Dinâmica Linear Dinâmica Rotacional

Interseção Raio - Plano

Um plano pode ser expressado através da fórmula:– N dot (P – P0) = 0

Onde N é a normal do plano, P é um ponto qualquer e P0 é um ponto no plano. Qualquer P colocado na fórmula que dê zero como resultado está no plano.

Interseção Raio - Plano

Um raio pode ser expressado através da seguinte fórmula:– O + tD

Onde O é a origem do raio, D é a direção normalizada e t é um valor que permite achar qualquer ponto na extensão do raio.

Interseção Raio - Plano

Devemos achar um t que gere um ponto que quando utilizado na fórmula do plano resulte em zero. Ou seja, devemos achar um t que resulte em um ponto no plano.

t vai indicar a distância percorrida da origem ao local de interseção.

Interseção Raio - Plano

O + tD = P N dot (P – P0) = 0 N dot (O + tD – P0) = 0 N*O + tN*D – N*P0 = 0 tN*D = N*P0 – N*O t = (N*P0 – N*O) / N*D t = N*(P0 – O) / N*D Quando N*D for igual a zero, o raio é paralelo ao

plano.

Interseção Raio - Plano

Interseção Raio - Triângulo

Ache a interseção do raio com o plano do triângulo.

Ache uma componente na normal diferente de zero, projete em um plano do eixo alinhado de acordo com a componente isso transforma nosso problema em 2D.

Use os coeficientes angulares entre o ponto e os vértices para verificar se o ponto está dentro do triângulo.

Interseção Raio - Triângulo

Para achar a relação entre o coeficiente angular do ponto e uma aresta, testa-se a seguinte condição:

(v2y–v1y) / (v2x–v1x) < (py–v1y) / (px–v1x) (v2y-v1y)*(px-v1x) < (py-v1y)*(v2x-v1x) 0 < (py-v1y)*(v2x-v1x) - (v2y-v1y)*(px-v1x)

Interseção Raio - Triângulo

Interseção Raio - Triângulo

Interseção Esfera - Esfera

Um esfera é representada computacionalmente através de uma posição C que define o centro da esfera e um tamanho r que define o raio da esfera.

Duas esferas estão colidindo se a distância entre os centros for menor que a soma dos raios das esferas, ou seja:

Está colidindo se:– | C1 – C2 | < r1 + r2

Onde C1 e C2 são os centros das esferas 1 e 2 e r1 e r2 são os raios das esferas 1 e 2.

Interseção Esfera - Esfera

Interseção Elipsóide - Elipsóide

O que diabos é um elipsoíde? Elipsóide é como uma esfera que possui um

raio de tamanho diferente para cada eixo.

Interseção Elipsóide - Elipsóide

A interseção de elipsóides é muito parecida com a de esferas.

Dois elipsóides estão colidindo se:– | C1 – C2 | < | R1 | + | R2 |

Onde C1 e C2 são os centros dos elipsóides. R1 é o vetor que vai de C1 até a superfície da elipsóide 1 na direção de C2. R2 é similar porém indo na direção de C2 para C1.

Interseção Elipsóide - Elipsóide

O tamanho do raio de uma elipsóide varia dependendo da direção, logo como achar o tamanho de R1 e R2?

Interseção Elipsóide - Elipsóide

Ou seja, após achar a direção, normalizar e depois multiplicar pelos tamanhos dos raios nos eixos x, y e z.

Interseção Elipsóide - Elipsóide

Interseção AABB - AABB

AABB significa Axis Aligned Bounding Box, ou seja, representa uma caixa com os alinhada aos eixos x,y e z.

Interseção AABB - AABB

AABBs são representadas por um ponto mínimo e um ponto máximo.

Duas AABBs estão colidindo se em nenhum momento algum dos testes triviais de rejeição retornam sucesso.

Interseção AABB - AABB

Exercício

Implemente uma aplicação que renderiza dois cubos.

Coloque duas AABBs envolvendo cada um dos cubos.

Faça um dos cubos se moverem com o uso de setas.

Implemente a detecção de colisão entre os cubos e impeça que os cubos entrem um no outro.

Detecção de Colisão em cenário estático usando Elipsóides

Como podemos colidir elipsóides com triângulos?

Colisão com cenários estáticos

Para detectar colisão: Lance um raio do centro da elipsóide na

direção do triângulo usando a normal negativa do triângulo como direção.

Colisão com cenário estáticos

Calcule o tamanho do raio nessa direção usada.

Se o raio acerta o triângulo e sua distância para o plano é menor que o raio da elipsóide na direção usada, a elipsóide colide com o triângulo.

Colisão com cenários estáticos

Resposta à colisão

Após detectar a colisão temos que tratar a resposta à colisão.

Uma resposta normalmente dada é empurrar o objeto para fora da geometria em que ele colidiu.

Como vamos empurrar a elipsóide para fora do triângulo?

Resposta a colisão

Nós empurramos o centro da elipsóide para fora na direção da normal do plano.

A distância empurrada = Tamanho raio – Distância centro-plano.

Resposta à colisão

Exercício

Implemente uma aplicação onde no cenário há um triângulo.

Coloque um elipsóide como BV da câmera. Faça a câmera se mover e colidir com o

triângulo.

Picking

Algo bastante utilizado em games é a seleção de algum objeto 3d com o uso do mouse.

Para conseguirmos descobrir o objeto selecionado, precisamos lançar um raio no espaço de mundo.

Para descobrirmos esse raio precisamos saber onde no mundo se encontra a posição selecionada na tela.

Com essa informação podemos lançar o raio partindo da câmera e passando por essa posição 3D no mundo.

Picking

Para achar a posição no espaço 3D usaremos as informações para a criação do frustum de visão.

Para achar a metade da altura da tela em espaço de mundo você calcula(usando os valores da imagem como exemplo):

tg(15) = x / near tg(15) * near = x; Para achar a metade da largura basta fazer: half_width = x * aspect; aspect é a divisão da largura da tela pela altura. Na prática, o valor 15 da figura será FOVy / 2 (FOVy é o

mesmo valor passado na função de criação de matriz de perspectiva).

Picking

Binary Space Partition Trees

BSPs são árvores binárias muito parecidas com as árvores binárias de busca. (Na realidade, a árvore binária de busca pode ser vista como uma BSP de 1 dimensão).

BSPs são úteis para acelerar a detecção de colisão.

Binary Space Partition Trees

Uma BSP é uma árvore onde cada nó representa um plano. Esse plano corta o espaço em 2. Os planos dos filhos a direita e esquerda cortam os espaços resultantes da direita e da esquerda.

Binary Space Partition Trees

Para detectar a colisão de um objeto com outros:

Adicione todos os objetos à BSP. Os objetos terminam em listas nas folhas da árvore.

Detectar colisão de cada objeto com todos os outros objetos da mesma folha.

Exercício (Para Casa)

Implementar uma BSP. Adicionar objetos usando AABBs. Implemente a detecção de colisão de cada

objeto com os outros da mesma folha.

Dinâmica Linear

Como podemos mover objetos linearmente de maneira fisicamente realista?

Aplicamos a segunda lei de Newton: – F = M * A

Dessa forma: – F / M = A

Onde F é força, M é massa e A é aceleração. F e A são vetores, M é um escalar.

O tempo e a posição

Aceleração = variação de velocidade / variação de tempo

Velocidade = variação de posição / variação de tempo

Posição = área da função da velocidade em uma faixa de tempo

Velocidade = área da função da aceleração em uma faixa de tempo

Um problema complicado

Dada uma aceleração, temos que achar a variação da velocidade e depois a variação da posição.

O problema é que isso envolve uma integração de uma função (e nós não sabemos a função previamente ).

Método de Euler

Consiste no método mais simples para achar as áreas.

Nele você adquire o valor no início do intervalo e considera ele constante durante todo o intervalo.

Método de Euler

Para adquirir a posição:– Aceleração = força / massa– Posição = posição + velocidade * variação de

tempo– Velocidade = velocidade + aceleração * variação

de tempo

Implementando o método de Euler

Dinâmica Rotacional

Nós aprendemos a mexer linearmente um objeto. Mas e quanto a rotação?

Torque é o equivalente da força para o movimento rotacional.

Para achar o torque de um corpo rígido, ache o produto vetorial entre o vetor que vai do centro de massa do objeto até o local onde está sendo aplicada uma força e o vetor da força.

Descobrindo a rotação

Torque = Cross(R, F) Aceleração Angular = Torque / Inércia Rotação = Rotação + Velocidade Angular *

variação de tempo. Velocidade Angular = Velocidade Angular +

Aceleração Angular * variação de tempo.

Descobrindo a rotação

Exercício

Implemente um demo que renderiza uma caixa.

Implemente física linear aplicando uma força constante na caixa.

Implemente física rotacional causando a caixa rotacional dependendo do lugar onde se aplica a força.

Tópicos para Inteligência Artificial

Máquinas de Estados Embedding Lua na sua aplicação Path Finding

Máquinas de Estado

Máquinas de Estado são uma das principais técnicas de inteligência artificial usadas desde o primórdio dos tempos (~1980).

Máquinas de Estado são máquinas abstratas compostas por estados e eventos que geram transições nesses estados.

Elas são úteis pois permitem a criação de seres que possuem comportamento dependendo do estado e que variam esse comportamento dependendo de eventos que ocorrem no jogo.

Máquinas de Estado

Um soldado em um jogo pode ter a seguinte máquina de estado:

Maquinas de estado

Maquina de estado

Máquinas de Estado Paralelas

Quando uma determinada entidade exige um comportamento sofisticado sua máquina de estado tende a ficar complexa demais.

A solução pra isso é fazer uma mesma entidade usar múltiplas máquinas paralelas para resolver diferentes problemas.

Por exemplo: Digamos que aquele inimigo do exemplo anterior passe a usar uma metralhadora e 2 máquinas de estado, uma para movimentação e outra para atacar.

Maquina de estado de movimentação

Maquina de estado de combate

Máquinas de estado paralelas

Implemente da mesma forma que a máquina de estado normal, porém use 1 variável de estado por máquina e 1 switch por máquina.

Sincronizando máquinas de estado

É interessante às vezes fazer com que máquinas de estado façam interações.

Half-life fez isso com seus soldados que se coordenavam ao combater o player.

Para fazer essa comunicação basta usar variáveis compartilhadas como variáveis estáticas.

Sincronizando máquinas de estado

Digamos que nós queremos que um inimigo ao avistar o player avise aos outros inimigos da localização do player e todos os outros inimigos a seguir se movem para a região avisada.

Máquina de estado de movimentação

Exercício

Implemente um demo com 2 caixas. Uma caixa é o inimigo e se move pela tela seguindo por 4 posições pré-definidas. A outra é controlada pelo jogador.

Quando a caixa do jogador se aproxima à uma certa distância. O inimigo começa a se aproximar até que ele chega à uma distância e pára. Se o jogador se afastar depois do inimigo, o inimigo volta a caminhar no seu caminho pré-definido.

Implemente esse demo usando uma máquina de estados.

Embedding Lua na sua aplicação

Scripting é uma adição fundamental à qualquer engine.

Scripting fornece uma ambiente sandbox para programadores de gameplay e de inteligência artificial onde eles podem criar lógica e conteúdo sem se preocupar em quebrar nada interno da engine. Ou seja, Scripting fornece permite que um trabalho puramente criativo seja realizado.

Scripting também é necessário se você deseja que seus jogos sejam “modable”.

Embedding Lua

Embedding Lua

A primeira linha cria o objeto de estado lua que é usado em todas as funções.

A função dofile é quem executa scripts. No final lua_close deve ser chamado para

liberar recursos.

Escrevendo e lendo valores

Escrevendo e lendo valores

Script em lua:

Resultado: x = 7 e y = 9

Escrevendo e lendo valores

Lua utiliza de uma pilha para trocar valores com o código em C++.

A função setglobal define o nome de uma variável e a pushnumber seu valor.

A função getglobal coloca uma variável no topo da pilha que pode ser capturada com a função tonumber que recebe o estado lua e o índice que se deseja usar para pegar um valor.

Adicionando funções à Lua

Scripts são úteis para customização de comportamentos, implementação de lógica de jogo e AI.

Porém scripts são mais lentos que código compilado em C++, portanto para certas computações custosas é valido implementá-las em C++ e adicionar a função à Lua.

Adicionando funções a Lua

Adicionando funções a Lua

Adicionando funções a lua

A função register registra uma função em lua.

Dentro da função faz-se uso da pilha normalmente para acessar valores.

Para retornar valores usa-se push e o valor retornado define o numero de valores a serem retornados (É possível retornar mais de um valor).

Exercício

Reimplemente o exemplo anterior executando a máquina de estados em Lua ao invés de executá-la em C++.

Adicione à lua a função de distância implementada em C++.

Path Finding

Um problema muito comum em diversos jogos é a necessidade de calcular o caminho para se mover de um ponto a outro do mapa.

Para resolver esse problema nós usaremos o algoritmo A*.

A*

O algoritmo A* é um algoritmo que implementa uma busca heurística.

Uma busca heurística leva em consideração alguma informação antes de tomar uma decisão.

No nosso caso, o A* levará em consideração informações geométricas para tomar decisões.

Min-Heap

O algoritmo do A* precisa de uma estrutura de dados que implemente uma fila de prioridade.

Uma fila de prioridade implementa uma função para retornar o elemento mais prioritário de forma eficiente.

Uma forma de implementar uma fila de prioridade é usar a estrutura Min-Heap.

Min-Heap

Uma Min-Heap é semelhante à uma árvore binária que possui a seguinte regra:– Cada nó é menor que seus filhos.

A Min-Heap pode ser implementada em um vetor.

PriorityQueue

PriorityQueue

PriorityQueue

PriorityQueue

PriorityQueue

A*

A prioridade de um nó é calculada usando 2 informações.

– A distância percorrida até o nó = g.– A distância do nó atual até o nó destino = h.

A prioridade é então calculada:– f = g + h

Essas prioridades fazem o algoritmo selecionar os nós que se aproximam mais rapidamente do nó destino.

A*

A*

Exercício

Implemente um PathFinder usando A* que recebe uma matriz de inteiros onde 1 é lugar passável e 0 é parede, e a partir dessa matriz é criado uma estrutura de dados onde cada nó possui norte, sul, leste e oeste.

Faça o PathFinder calcular o caminho entre duas posições.

Imprima as posições numa console application.

Fundamentos de Programação de Jogos

Arquitetura de Tecnologias de Jogos

Arquitetura de Tecnologia de Jogos

Jogos 3D possuem a vantagem de poderem compartilhar muita tecnologia.

Isso aumenta a recompensa em implementar uma engine de jogos.

Engines de jogos podem possuir designs completamente diferentes, logo o design mostrado aqui não deve ser considerado o design supremo e fundamental de engines.

Implementando um Jogo

Uma forma interessante de dividir os diferentes momentos de um jogo é implementando-o como uma máquina de estados.

Cada estado é implementado em uma classe diferente.

Implementando um Jogo

Loop Principal

Uma forma, para jogos single player é a seguinte:– Renderiza a cena (Sem dar Present)– Adquire e Processa Input do Player– Processa AI– Processamento física, testa colisão e resposta– Processamentos extras, específicos do estado

atual– Present

Game Engine Todo List (Single Player)

1 Level Loader que carrega o arquivo com a configuração do level. Esse arquivo aponta para outros arquivos que deve ser carregados para a execução do level.

1 engine 3d para carregar e renderizar o cenário com efeitos especiais de maneira rápida.

1 engine de física/colisão com estruturas otimizadas para detecção de colisão. Esse engine move objetos e detecta colisão e repassa o tratamento da resposta para o game.

1 engine de input para aquisição do input. 1 engine de AI que carrega dados em um cenário específicos

para AI e processa as entidades AI do game. Todos são Singletons.

Level Loader

Tem como missão carregar o arquivo que representa o level.

Esse arquivo possui os nomes de todos os outros arquivos que serão usados no level. Ex: O arquivo da octree do cenário. O arquivo que armazena os waypoints do cenário. O arquivo que armazena a estrutura usada para processar a colisão. Os modelos, os scripts.

O level loader repassa todos esses nomes para as respectivas engines carregarem.

Engine 3D

Responsável pela parte gráfica do jogo. Carrega o cenário e os modelos 3D. Pode

carregar uma estrutura especial como uma Octree. Carrega também a parte gráfica dos elementos do cenário.

Renderiza o cenário 3D e os modelos usando técnicas de aceleração e efeitos especiais.

Engine 3D

Engine 3D (Extras)

É interessante criar plugins para 3DS Max para exportar cenário e modelos.

É importantíssimo implementar um level editor onde as fases são feitas a la wysiwyg onde você define locais onde ficam as entidades, triggers e scripts especiais. Você também configura características gráficas específicas da engine e vê em tempo real as alterações.

Engine de Física / Colisão

Carrega a estrutura de colisão e a parte física dos elementos da cena. Pode carregar por exemplo uma BSP.

Processa a física das entidades e usa técnicas de aceleração para os testes de colisão. Delega a resposta de colisão ao game. Isso pode ser feito com ponteiros para função ou funções virtuais.

Engine de Física / Colisão

Engine de Input

Wrapper de funções que fazem polling de dispositivos.

Engine de AI

Carrega a estrutura de dados necessária para executar a AI. Como exemplo, carregar os caminhos possíveis pela AI. Carrega a parte de AI das entidades da cena.

Processa movimentação e inteligência usando C++ e delegando para Lua partes do processamento.

Engine de AI

Problema

O AI Engine decide mover um NPC para alguma região. Ele então requisita a movimentação para o Engine de Física. O Engine de física o move normalmente. Como o engine gráfico recebe essa alteração?

Uma solução é fazer uso de IDs. Um elemento é identificado nas 3 engines através do uso de um mesmo ID. Logo se o engine de AI decidir mover um personagem, ele identifica o personagem através de seu ID. O engine gráfico antes da renderização requisita todos os elementos que se moveram para atualizar suas posições.

Elementos e IDs

Exercício Final

Implementar um Jogo 3D