Introdução aos Shaders
Home XNA Shaders Introdução aos Shaders
Vertex Shader e Rasterizador
Pixel Shader, OutputMerger e finalização
Shaders no XNA
Este primeiro Post marca o início de uma série sobre shaders. Ao longo dos tutoriais pretendo explicar e implementar diversas técnicas e efeitos especiais como Bump Mapping e Phong Shading. A série não será limitada ao ensino de uma linguagem de programação, irei discutir com uma certa profundidade questões como arquitetura da GPU e técnicas de otimização usadas na computação gráfica.
Usarei XNA 4.0 (PC e XBOX) como framework e HLSL como linguagem de programação para a GPU, porém a discussão e os conceitos apresentados não são específicos destas tecnologias.
Neste primeiro tutorial pretendo mostrar todos os passos envolvidos na renderização de um modelo 3D com custom shaders sem usar as facilidades do XNA. Revelarei o que acontece por debaixo dos panos!!!
Um modelo é armazenado como uma lista de índices e uma lista de vértices conforme a figura a seguir: (em jogos normalmente usamos esta representação por ser compacta):
O modelo (quadrado a esquerda) pode ser descrito por meio de duas listas, a primeira contendo atributos de vértice (como Posições V e Texturas T) e a segunda lista contendo os índices (posição na lista dos vértices) que formarão cada triângulo do modelo (0,3,1 e 0,2,3).
Notar que vértice não significa posição, vértice contém posição e mais outros atributos, neste modelo (quadrado acima), cada vértice tem uma coordenada de textura e uma posição. Atributos comuns são: normais, cor, tangentes, índices de ossos e peso de ossos, sendo os dois últimos usados em Bone Animation.
Enviando Modelos para a GPU
No XNA e no DirectX, as informações de vértices são armazenadas em um container chamado VertexBuffer. Esta classe representa um elo entre a CPU e a GPU. Ao criar um vertexBuffer estamos na verdade criando uma espécie de proxy do lado da CPU para dados que serão armazenados na GPU. (É comum dizer "lado da CPU" e "lado da GPU", mas na verdade estamos querendo dizer, RAM da CPU e RAM da GPU).
Em um jogo, é bastante comum termos objetos (caixa, casa, parede ... ) com formatos de VertexBuffers distintos (por exemplo, um vértice de um sistema de partículas tem atributos como tempo de vida da partícula e um vértice de um modelo simples não tem este atributo), desta forma precisamos informar para a GPU o FORMATO correto do dado contido no VertexBuffer.
Para esta finalidade, existe uma estrutura chamada VertexDeclaration (XNA e Directx) que descreve cada um dos atributos dos vértices de um VertexBuffer.
Antes de criar o VertexDeclaration, devemos criar o nosso vértice de fato. Como exemplo, criarei um tipo de vértice chamado CUSTOMVertexPositionNormalTexture que conterá informações sobre Posição, Normal e Textura do modelo. O XNA oferece uma versão chamada VertexPositionNormalTexture praticamente idêntica a esta.
A definição do vértice é a seguinte:
struct CUSTOMVertexPositionNormalTexture
public Vector3 position; ///informacoes sobre posicao
public Vector3 normal;   ///informacoes sobre normal
public Vector2 texcoord; ///informacoes sobre coordenadas de textura
A seguir devemos criar o VertexDeclaration para este tipo de vértice. O construtor do VertexDeclaration recebe um array de VertexElement. Cada VertexElement descreve um dos atributos do vértice. A seguir temos a definição dos VertexElements para o vértice criado:
VertexElement positionDeclaration = new VertexElement(0,VertexElementFormat.Vector3,VertexElementUsage.Position,0);
VertexElement normalDeclaration = new VertexElement(sizeof(float) * 3,VertexElementFormat.Vector3,VertexElementUsage.Normal,0);
VertexElement textCoordDeclaration = new VertexElement(sizeof(float) * 6,VertexElementFormat.Vector2,VertexElementUsage.TextureCoordinate,0);
VertexElement[] vertexElements = new VertexElement[] {positionDeclaration,normalDeclaration,textCoordDeclaration};
VertexDeclaration vd = new VertexDeclaration(vertexElements);
Vamos por partes. O construtor do VertexElement recebe 4 parâmetros cuja descrição é a seguinte:
Offset: Distancia em bytes entre o começo do vértice e o atributo em questão
elementFormat: Tamanho do atributo em questão (XNA tem um enum que facilita as coisas, o DirectX não ... )
elementUsage: Uso pretendido do element, neste campo colocamos a semântica que será utilizada pelos Shaders para acessar este dado.
usageIndex: Índice utilizado para acessar o elemento no Shader (Falarei deles mais a frente)
Como exemplo descreverei a criação do VertexElement para o atributo coordenada de textura:
O primeiro parâmetro é o offset, neste caso usamos sizeof(float) * 6, pois existem 2 atributos Vector3 (Posicão e Normal) antes da coordenada de textuta (veja definição do CUSTOMVertexPositionNormalTexture) e cada Vector3 é constituído de 3 floats.
O segundo parâmetro diz qual o tamanho do dado usado (Vector2), o XNA fornece um Enum que ajuda a definir este parâmetro. Em DirectX, não existe esta facilidade, colocaríamos algo do tipo sizeof(float) * 2
O terceiro e o quarto parâmetro descrevem o uso que daremos a este elemento no shader. Esta parte será explicada mais a frente.
Em seguida os VertexElements são empacotados em um array e usados para construir o VertexDeclaration. Com ele em mãos, a GPU poderá "entender" os dados dos vértices que estão sendo enviados.
No DirectX e no XNA 3.1, o VertexBuffer e o VertexDeclaration são entidades completamente separadas, porém no XNA 4.0 o VertexDeclaration virou um membro do VertexBuffer (isto facilita um pouquinho na hora de desenhar um modelo).
Para criar o vertexBuffer usamos:
VertexBuffer vb = new VertexBuffer(GraphicsDevice, vertexDeclaration, NUMERO_DE_VERTICES, BufferUsage.None);
vb.SetData(ARRAY_DE_VERTICES, 0, NUMERO_DE_VERTICES);
ARRAY_DE_VERTICES é um array de CUSTOMVertexPositionNormalTexture contendo os vértices do modelo.
A linha vb.SetData(…) corresponde ao momento em que os vértices do modelo estão sendo enviados para a RAM da GPU. (Este processo não é síncrono, ou seja, a transferência não acontece necessariamente no momento que chamamos esta função).
Após configurar o VertexBuffer, devemos enviar as informações de índices para que a GPU consiga criar cada um dos triângulos do modelo. Para isto criamos um IndexBuffer.
short[] indices = new short[NUMERO_DE_INDICES];
///inicia o array de indices
IndexBuffer ib = new IndexBuffer(GraphicsDevice, IndexElementSize.SixteenBits, NUMERO_DE_INDICES, BufferUsage.None);
INDICES é um array de shorts contendo os índices do modelo. (para modelos com muitos vértices usamos int ao invés de short, neste caso deveríamos usar IndexElementSize.ThirtyTwoBits no construtor do IndexBuffer)
Pronto !!! Já temos uma descrição do modelo na RAM da GPU e agora?? Vamos ver como a GPU trata estes dados.
GPU Pipeline (DirectX 9c)
A GPU é um processador SIMD (Single instruction Multiple Data) cuja função (entre outras) é auxiliar no processo de renderização (ver também GPGPU). Para isto, a GPU usa uma arquitetura extremamente especializada baseada massivamente em pipeline (um pipeline de um processador Pentium 4 tem algo em torno de 10 estágios, uma GPU costuma ter algo próximo de 1000).
A figura abaixo mostra como que se da à troca de informações entre a CPU e a GPU, quando um modelo é desenhado:
VertexBuffer,IndexBuffer e VertexDeclaration: Dados geométricos do modelo
Render States: São parâmetros que determinam como que as etapas configuráveis da GPU funcionarão.
Shader: Código que comandará o funcionamento das etapas programáveis da GPU.
Texturas e Constantes: São parâmetros constantes que o shader atual acessará. Eles são de somente leitura.
Apenas como curiosidade, dêem uma olhada nesse pôster que mostra a pipeline (nem todos os estágios) do DirectX 9. No mínimo assustador! Abaixo segue uma descrição mais resumida do processo:
Input Assembler: Ler e interpretar os vértices e atributos de vértices do Vertex Buffer (por meio do VertexDeclaration e do IndexBuffer) e enviá-los para o VertexShader
[PROGRÁMAVEL] Vertex Shader: Executado uma vez para cada vértice de cada triângulo. Sua função principal é converter os vértices para espaço de projeção.
Rasterizer: Converte os triângulos (A GPU suporta diversas outras primitivas) em pixels e envia-os para o Pixel Shader. O rasterizador também realiza outras tarefas como clipping e interpolação dos atributos do vértice para cada pixel.
[PROGRÁMAVEL] Pixel Shader : Determina a cor final do pixel a ser escrito no framebuffer (num primeiro momento pode ser entendido como a tela do monitor), é executado uma vez para cada pixel rasterizado de cada primitiva.
Output Merger: Combina a saída do Pixel Shader com os valores do Render Target atual. Pode efetuar algumas operações como alpha blending e depth/stencil/alpha testings. (Neste tutorial usarei Render Target como um sinônimo para framebuffer, mas na verdade framebuffer é um tipo de Render Target)
12/04/2011 16:42:29 |189.59.222.xxx| Marcos Vasconcelos - Otimo artigo.
Otimo artigo, demorei para chegar ao final mas deu para ter uma visão melhor de como funciona.
Ainda preciso montar um ambiente XNA para testar exemplos como esse.
12/04/2011 19:38:40 | Jonathan Araujo
Cara muito bom o artigo, to comecando a ler sobre shader, só que mais sobre GLSL.
Como to estudando eu li e tive um monte de dúvidas, vamos a elas:
Pelo que eu vi sempre se usa float4 para poisção, em algum momento esse quarto float é usado?
Esse shader que você mostrou de exemplo na página 3 é pra fazer iluminação phong?
Como que funcionam os shaders qndo se usam imagens como em um bump mapping, shadow mapping, tesselation etc?
Ultima dúvida como o computador que não suporta o shader model 2 se comporta ao executar o programa? Existe alguma coisa como se não tiver suporte a shader desliga-los? Da pra na mesma técnica fazer vários shaders, por exemplo se o PC tiver suporte a shader 4 faça A se tiver suporte a shader 3 faça B e assim por diante...
Novamente parabens pelo artigo
12/04/2011 21:02:30 | Thiago Dias Pastor - Resposta
vamos as repostas
Sim, ele eh super importante, pois as matrizes de transformacao usadas (como a World, a View ...) sao 4x4 (nao da para multiplicar um vetor3 por uma matrix4x4).
Em computacao grafica normalmente representamos vetores e pontos como float4, os pontos tem o ultimo float como 1 e os vetores com o ultimo componente sendo 0.
Os vetores nao sao afetados por translacoes (um vetor nao tem uma posicao no espaco, somente direcao, sentido e intensidade), dessa forma este ultimo elemento sendo 0, anulara as translacoes das matrizes de tranformacao. Para uma discusao mais detalhada de uma olhada na minha monografia http://ploobsen gine.codeplex.com/documentation (Our Graduation Thesis)
Nao, este shader soh pega a textura do modelo e manda pra tela, sem nehum efeito nem iluminacao. Pretendo fazer um proximo tutorial sobre iluminacao Phong
Estas imagens entram como uma textura a mais no shader (da mesma maneira como eu declarei a textura difusa) Voce teria dois samplers e duas textures. No shader, para ler de cada textura, voce chamaria o text2D passando o sampler correto.
No inicio, na parte CPU, podemos identificar quais Shader Models a placa de video do usuario suporta. Para cada efeito, nosso shader diversas tecnicas (cada uma compilada para um shader model diferent) e escolheriamos em tempo real qual tecnica do shader usar . Isso eh bastante comum nos jogos.
13/04/2011 10:49:56 | Vinícius Godoy de Mendonça
Só um comentário. O sistema de coordenadas homogêneo (que explica essa quarta dimensão no vetor) será explicado no meu próximo artigo de matrizes, aqui mesmo no Ponto v.
Por hora, você só precisa saber que esse é um fator de escala e que os valores de x, y e z serão divididos por ele. Na prática, dificilmente define-se esse valor diretamente, mas ele será usado no cálculo do deslocamento (translation). Note que o artigo atual de matrizes só cobre rotação e escala, e não deslocamento, pois usa matrizes lineares de dimensão exatamente igual ao número de eixos ordenados.
13/04/2011 10:50:21 | Thiago Dias Pastor
Conforme mostrado em http://graphics.ucsd.edu/courses/cse167_w06/slides/CSE167_02.ppt Adoro este ppt !!! =P
De uma maneira intutiva.
No inicio o component w do float4 eh apenas um "diferenciador" entre pontos e vetores.
Um artificio matematico para fazer fazer com q os vetores nao sejam afetados pela translacao contida na matrix World (uma vez q nao faz sentido transladar um vetor).
A matrix World (em XNA/DirectX) contem na sua quarta linha a translacao, quando multiplicarmos essa matrix pelo ponto, essa linha sera multiplicada pelo ultimo componente W do ponto q eh 1 (logo a translacao sera considerada), no caso do vetor, essa linha(translacao) nao sera aplicada pois ela sera multiplicada por 0 (componente w do vetor). (eu costumo dizer q eh uma gambiarra matematica (como varias outras) para podemos adicionar um valor atraves de uma multiplicacao -- a translacao nada mais eh doque somar um vetor a um ponto, conseguimos fazer isso atraves de uma multiplicacao -- ponto pela Matrix World)
Enfim, coordenadas homogeneas sao um artificio para podermos combinar, atraves de multiplicacoes, as tranformacoes (de escala, rotacao e translacao) em uma unica matriz. Aqui ensina legal o conceito http://www.inf.pucrs.br/~pinho/CG/Aulas/Vis2d/Instan ciamento/Instanciamento.htm
Em comp grafica fazemos. Pegamos um Ponto3D/Vetor3D, transformamos em coordenadas homogeneas (colocando o 1 ou 0 no componente W) fazemos todas as transformacoes e convertemos para Ponto3D/Vetor3D de novo (dividindo o vetor/ponto pelo W dele)
Quando voce multiplica um ponto (em coordenadas homogeneas) pela View e Projection, a coordenada W deste ponto resultante nao eh mais 1. Para recuperar o ponto projetado de fato em coordenadas 3D, temos que dividir as coordenas x,y,z pelo w conforme o Vinicius explicou. resultaria em (x/w,y/w,z/w,1), sendo x/w e y/w a coodenada de tela homogenea o z/w a profundidade.
As pessoas costumam enxergar esse W como um fator de escala.
O valor 0 para um vetor pode ser entendido como que se um vetor fosse um ponto no infinito pois teriamos (x/0,y/0,z/0/,0/0 -> (inf,inf,inf,NAN) caso dividessemos ele pelo fator de escala.
A GPU usa este valor w (do ponto depois de transformado pela World,View e Projection) na rasterizacao (para fazer correcao de perspectiva). Eu implementei este algoritmo uma vez aqui http://softrender.codeplex.com /. Caso estejam interessados, de uma olhada no codigo para ver como os pontos e os vetores sao transformados pelas matrizes e como q o componente W eh usado.
13/04/2011 12:03:14 | Vinícius Godoy de Mendonça
Não existem operações sobre pontos (soma, por exemplo), por isso somos obrigados a recorrer a notação vetorial, e entende-los como deslocamentos em relação à origem.
Confusão pode ocorrer, pois você muitas vezes verá artigos e textos dizendo que no sistema homogêneo, vetores terminam com a coordenada 1:
http://en.wikipedia.org/wiki/Scaling_%28geometry%29
http://homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/BEARDSLEY/node 1.html
O que também é correto, pois são vetores que estão representados dentro de um determinado espaço em consideração.
Já a última coordenada zero, representa apenas um ponto no infinito, fora do seu plano de consideração. Ou seja, podemos entender apenas como uma direção, sem um ponto específico sendo apontado, portanto, não trata de uma posição geográfica específica (um ponto).
Então, a analogia do Thiago está correta. Para a computação gráfica, um vetor terminado em 0, representará tão somente um componente direcional. Não sofrerá translações e não mudará o local para onde ele aponta.
Um vetor com a quarta coordenada diferente de 0, do contrário, pode representar localidades específicas no espaço, ou seja, pontos. Por isso, sofrem alterações de posição durante a operação de translação.
Agora, a confusão pode ocorrer pois sempre estaremos de falando de vetores. Como expliquei no artigo de matemática, pontos são representados por eles. Por isso é bom tomar cuidado sobre como realizar a leitura, e diferenciar bem esse conceito.
Localidades = pontos = coordenada != 0 no final
Direção = Coordenada 0 no final.
12/04/2011 21:04:57 | Jonathan Araujo
No caso a textura ta no teu modelo .x ou fbx certo? Mas essa textura ja não está aplicada no modelo .x, .fbx?
Entendi teria q fazer meio via linha de código identificar qual é a versão do shader, algo como Se shaderVersion == 2 load Tech0 Se shaderVersion == 3 load Tech1 e assim por diante?
Pow brigadao acho que entendi o resto.
12/04/2011 21:31:42 | Thiago Dias Pastor
Quando a gente usa shader, nao existe o conteito de "textura ja está aplicada no modelo". O .x basico por exemplo, contem apenas informacoes geometricas do modelo (como vertices, tranformacoes, ossos, nome das texturas usadas .... ).
Nos que devemos usar as informacoes dos vertices (coordenadas de textura no caso) para mapear os texels da textura no modelo 3D.
Existe a possibilidade de colocarmos um parametro a mais no vertice (chamado de VertexColor). No vertex shader apenas redirecionaremos este valor para o proximo estagio e no pixel shader retornaremos ele para a tela. Nessa abordagem, de fato a textura esta aplicada no modelo (as cores da textura foram "colocadas" nos vertices), porem, entre outras coisas, o resultado eh muito ruim visualmente (as cores sao dadas por vertice e serao interpoladas para os pixels, se os triangulos formados forem grandes, perceberemos varias regioes com cores uniformes uniformes ... - teriamos uma sensacao parecida com a q temod qd rodamos um jogo em uma resolucao MUITO baixa ) isto nao eh mais usado.
13/04/2011 14:23:33 | Jonathan Araujo
Pow valew...
Com certeza vou dar uma lida na sua monografia
18/04/2011 19:57:55 |186.212.46.xxx| Michel braz - Artigo muito bom
Ola ... só queria parabenizar pelo artigo.
Gosto muito do assunto ...
ja fiz algumas coisas com shader porem com directx em c++...
mas é uma matéria muito interessante que acho que vale a pena ser compartilhado....
04/05/2011 14:13:58 |201.16.226.xxx| Marcelo Gamba - Excelente
Sou entusiasta no assunto, e achei bem interessante esse artigo para introduzir os leigos (assim como eu) no mundo dos Shaders. Resumindo um belo overview.
09/05/2011 14:57:36 |186.198.113.xxx| gokernel
Bem interessante o artigo, estou interessado em shader somente para DirectX pois infelizmente a placa gráfica do meu note não suporta shader com OpenGL.
09/05/2011 16:44:42 | Vinícius Godoy de Mendonça
Felizmente o XNA usa a mesma linguagem de shaders que o DirectX usa.
09/05/2011 18:49:10 |186.198.113.xxx| gokernel
Que bom né ? Então viva o Bill \o/.
04/07/2011 19:33:03 |187.115.115.xxx| Cleber - Referência
olá, gostaria de usar algumas informações desse artigo como referência para minha monografia. Se possível gostaria de saber o ano de publicação e se foi publicado em algum periódico. Obrigado
04/07/2011 20:55:01 | Vinícius Godoy de Mendonça
Nada impede você também de lançar esse site como referência bibliográfica. Há normas da ABNT para isso e, com a quantidade absurda de material de alta qualidade disponível na internet, tem sido uma prática da vez mais comum.
Isso nos ajudaria a valorizar o site. Pense na possibilidade, nem que seja lançando a monografia do Thiago junto.
05/07/2011 00:10:46 |187.115.115.xxx| cleber
todas as referências que estou pegando na internet estou colocando dísponível em "..........."
o site tem muito material mesmo, muito bom.
05/07/2011 00:39:25 | Vinícius Godoy de Mendonça
Obrigado. A nossa idéia é continuar crescendo sempre, com conteúdos de qualidade como este.
05/07/2011 15:07:52 | Thiago Dias Pastor
Eu sugiro voce colocar os dois como referencias (o site e a monografia). As abordagens foram bem diferentes e complementares.
05/07/2011 13:28:02 |187.115.115.xxx| cleber
farei isso, vou colocar um como revisão e outro como bibliografia, pois não preciso me aprofundar no assunto, são conceitos básicos de shader que irei utilizar... obrigado novamente...
04/07/2011 19:34:22 |187.115.115.xxx| Cleber
acredito que é de 2011, mas gostaria de confirmação... ótimo artigo, parabéns!!
04/07/2011 20:47:41 | Thiago Dias Pastor - ploobs.com.br
Esse artigo é uma versão mais mais informal de um capitulo da minha monografia http://www.codeplex.com/Project/Download/FileDownload.aspx?ProjectName =ploobsengine&DownloadId=217469
A monografia foi escrita no segundo semestre do ano passado e o artigo foi escrito em marco desse ano.
Qualquer coisa entre em contato contato@ploobs.com.br
18/05/2017 22:13:29 |168.181.51.xxx| WhataF*ck - Poderia ser melhor
O site de vocês poderia ser melhor, só tem coisas simples e acaba ai mesmo, não dão continuidade, tem muita coisa pela metade.
Assim fica assim.
Até parece que o site é apenas para status e currículo de vocês.
Melhorem os tutoriais
30/08/2017 16:37:58 |201.22.56.xxx| Vixixi - Concordo
Concordo com o comentário do Whataf*ck:
"O site de vocês poderia ser melhor, só tem coisas simples e acaba ai mesmo, não dão continuidade, tem muita coisa pela metade.