Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

Guia de iniciantes para programação de shaders gráficos: parte 2

Scroll to top
Read Time: 12 min
This post is part of a series called A Beginner's Guide to Coding Graphics Shaders.
A Beginner's Guide to Coding Graphics Shaders
A Beginner's Guide to Coding Graphics Shaders: Part 3

() translation by (you can also view the original English article)

ShaderToy, a ferramenta que usamos no tutorial anterior desta série, é ótimo para testes rápidos e experiências, mas é bastante limitado. Você não pode controlar quais dados são enviados para o shader, entre outras coisas. Ter o seu próprio ambiente onde você pode executar os shaders significa que você pode fazer todos os tipos de efeitos, e você pode aplicá-los em seus próprios projetos!

Nós vamos usar Three.js como nossa estrutura para executar shaders no navegador. WebGL é a API de Javascript que nos permitirá processar os shaders. Usar o Three.js simplesmente facilitará esse trabalho.

Se você não está interessado em JavaScript ou na plataforma web, não se preocupe: nós não focaremos especificamente em renderização web (embora caso queira aprender mais sobre o framework, confira este tutorial). Criar shaders no navegador é a maneira mais rápida de começar, mas tornar-se confortável com esse processo permitirá que você facilmente configure e use shaders em qualquer plataforma que quiser.

A instalação

Esta seção irá guiá-lo sobre como criar shaders localmente. Você pode acompanhar sem precisar baixar nada com esse CodePen já criado:

Você pode copiar e editar isto no CodePen.

Olá Three.js!

Three.js é um framework JavaScript que cuida de um monte de código clichê para WebGL que vamos precisar para processar nossos shaders. A maneira mais fácil de começar é usar uma versão hospedada em um CDN.

Aqui está um arquivo HTML contendo uma cena Threejs básica.

Tente salvar esse arquivo no disco, em seguida, abra-o no navegador. Você deve ver uma tela preta. Isso não é muito excitante, então vamos tentar acrescentar um cubo, para certificar-se de que tudo está funcionando.

Para criar um cubo, precisamos definir sua geometria e seu material e em seguida, adicioná-lo à cena. Adicione este trecho de código onde diz Add your code here:

1
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
2
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00} );//We make it green

3
var cube = new THREE.Mesh( geometry, material );
4
//Add it to the screen

5
scene.add( cube );
6
cube.position.z = -3;//Shift the cube back so we can see it

Não iremos entrar em muitos detalhes neste código, já que estamos mais interessados na parte do shader. Mas se tudo correu bem, você deve ver um cubo verde no centro da tela:

já que estamos aqui, vamos fazê-lo girar. A função render executa cada quadro. Podemos acessar a rotação do cubo através de cube.rotation.x (ou .y ou .z). Tente adicionar isso. Sua função de renderização ficará assim:

1
function render() {
2
  cube.rotation.y += 0.02;
3
	
4
	requestAnimationFrame( render );
5
	renderer.render( scene, camera );
6
}

Desafio: você consegue girar ao longo de um eixo diferente? E em dois eixos ao mesmo tempo?

Agora que está tudo configurado, vamos adicionar alguns shaders!

Adicionando Shaders

Neste ponto, podemos começar a pensar sobre o processo de implementação de shaders. É provável que você se encontre em uma situação semelhante, independentemente da plataforma que você planeja usar shaders: tudo já está configurado, e você tem coisas sendo desenhadas na tela, mas como você acessa a GPU?

Passo 1: carregar o código GLSL

Estamos usando JavaScript para construir esta cena. Em outras situações você pode estar usando C++, Lua ou qualquer outra linguagem. Shaders, independentemente, são escritos em uma linguagem de shader especial. A linguagem de shaders do OpenGL é o GLSL (OpenGL Shading Language). Já que estamos usando WebGL, que é baseado em OpenGL, GLSL é o que usaremos.

Então como e onde escrever nosso código GLSL? A regra geral é que você deseja carregar seu código GLSL em uma string. Você pode então enviá-la para ser analisada e executada pela GPU.

Em JavaScript, você pode fazer isso simplesmente colocando todo o seu código dentro de uma variável da seguinte forma:

1
var shaderCode = "All your shader code here;"

Isso funciona, mas como no JavaScript não tem um jeito fácil para criar várias strings com várias linhas, isso não é muito conveniente para nós. A maioria das pessoas tendem a escrever o código do shader em um arquivo de texto e dar-lhe uma extensão .glsl ou .frag (abreviação para fragmento de shader) e, em seguida, basta carregar o arquivo.

Isto funciona, mas vamos escrever nosso código dentro de uma tag <script> e carregá-lo no Javascript, assim podemos manter tudo em um único arquivo, o que é conveniente para este tutorial.

Crie uma nova tag <script> dentro do HTML parecido com esta:

1
<script id="fragShader" type="shader-code">
2
3
</script>

Adicione o ID fragShader para que possa ser acessado mais tarde. O tipo shader-code é na verdade um tipo de script falso que não existe. (Pode colocar qualquer nome lá que vai funcionar). A razão pela qual fazemos isto é para que o código não seja executado e não apareça no HTML.

Agora vamos colocar um shader básico que retorna apenas branco.

1
<script id="fragShader" type="shader-code">
2
void main() {
3
    gl_FragColor = vec4(1.0,1.0,1.0,1.0);
4
}
5
</script>

(Os componentes de vec4 neste caso correspondem ao valor rgba, conforme explicado no tutorial anterior.)

Finalmente, nós temos que carregar este código. Podemos fazer isto com uma simples linha de JavaScript que localiza o elemento HTML e extrai o texto interno:

1
var shaderCode = document.getElementById("fragShader").innerHTML;

Isto deve ir abaixo do código do cubo.

Lembre-se: apenas o que é carregado como uma sequência de caracteres será analisado como código GLSL válido (ou seja, void main () {...}. O resto é apenas coisa do HTML.)

Você pode copiar e editar isto no CodePen.

Passo 2: aplicando o shader

O método para aplicar o shader pode ser diferente dependendo da plataforma que você está usando e como ela interage com a GPU. Mas isso nunca é um passo complicado, e uma rápida pesquisa no Google nos mostra como criar um objeto e aplicar shaders com Three.js.

Precisamos criar um material especial e atribuir o nosso shader. Vamos criar um plano com o nosso shader (mas poderíamos muito bem usar o cubo). Isso é tudo que precisamos fazer:

1
//Create an object to apply the shaders to

2
var material = new THREE.ShaderMaterial({fragmentShader:shaderCode})
3
var geometry = new THREE.PlaneGeometry( 10, 10 );
4
var sprite = new THREE.Mesh( geometry,material );
5
scene.add( sprite );
6
sprite.position.z = -1;//Move it back so we can see it

Agora, você deve estar vendo uma tela branca:

Você pode copiar e editar isto no CodePen.


Se você alterar o código no shader para alguma outra cor e atualizar, você deve ver a nova cor!

Desafio: você consegue mudar parte da tela para a cor vermelha e a outra para azul? (Se você empacar, o próximo passo deve te dar uma dica!)

Passo 3: envio de dados

Neste ponto, podemos fazer o que quisermos com nosso shader, mas não há muito que possamos fazer. Só temos embutido a posição do pixel gl_FragCoord para usar, e se você lembrar, não está normalizado. Precisamos ter pelo menos as dimensões da tela.

Para enviar dados ao nosso shader, precisamos enviá-lo com o que chamamos de variável uniforme. Para isso, criamos um objeto chamado uniforms e adicionamos a ele nossas variáveis. Aqui está a sintaxe para enviar a resolução:

1
var uniforms = {};
2
uniforms.resolution = {type:'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)};
Cada variável uniforme deve ter um tipo e um valor. Neste caso, é um vetor de duas dimensões com largura e altura da janela como coordenadas. A tabela abaixo (retirada da documentação do Three.js) mostra todos os tipos de dados que você pode enviar e seus identificadores:
Strings tipo uniforme Tipo GLSL Tipo Javascript
'i', '1i'
int
Number
'f', '1f' float
Number
'v2'
vec2
THREE.Vector2
'v3'
vec3
THREE.Vector3
'c' vec3
THREE.Color
'v4' vec4
THREE.Vector4
'm3' mat3
THREE.Matrix3
'm4' mat4
THREE.Matrix4
't' sampler2D
THREE.Texture
't' samplerCube
THREE.CubeTexture
Para realmente enviá-lo para o shader, modifique o instanciador ShaderMaterial para incluí-lo, desta forma:
1
var material = new THREE.ShaderMaterial({uniforms:uniforms,fragmentShader:shaderCode})

Ainda não terminamos! Agora nosso shader está recebendo essa variável, precisamos fazer algo com ela. Vamos criar um gradiente da mesma forma que fizemos no tutorial anterior: normalizando nossa coordenada e usá-la para criar o nosso valor de cor.

Modifique seu o código do shader para que fique assim:

1
uniform vec2 resolution;//Uniform variables must be declared here first

2
void main() {
3
    //Now we can normalize our coordinate

4
	vec2 pos = gl_FragCoord.xy / resolution.xy;
5
    //And create a gradient!

6
    gl_FragColor = vec4(1.0,pos.x,pos.y,1.0);
7
}

E você deve ver um gradiente agradável!

Você pode copiar e editar isto no CodePen.

Se você está um pouco confuso sobre como conseguimos criar um gradiente tão agradável com apenas duas linhas de código no shader, confira a primeira parte desta série de tutoriais para se aprofundar na lógica por trás disso.

Desafio: você consegue dividir a tela em 4 seções iguais com cores diferentes? Algo parecido com isto:

Passo 4: atualizando dados

É legal ser capaz de enviar dados para nosso shader, mas e se precisarmos atualizá-la? Por exemplo, se você abre o exemplo anterior em uma nova aba e, em seguida, redimensiona a janela, o gradiente não é atualizado, porque ainda está usando as dimensões da tela inicial.

Para atualizar suas variáveis, geralmente você teria apenas que reenviar a variável uniforme e ele irá atualizar. Com o Three.js, no entanto, é preciso atualizar o objeto uniforms em nossa função render — não há necessidade de reenviá-la ao shader.

Aqui está como a nossa função de renderização ficará depois dessa mudança:

1
function render() {
2
	cube.rotation.y += 0.02;
3
	uniforms.resolution.value.x = window.innerWidth;
4
	uniforms.resolution.value.y = window.innerHeight;
5
6
	requestAnimationFrame( render );
7
	renderer.render( scene, camera );
8
}

Se você abrir um novo CodePen e redimensionar a janela, você verá as cores mudando (embora o tamanho do visor inicial permaneça o mesmo). É mais fácil ver isso observando as cores em cada canto para verificar que não mudam.

Nota: enviar dados para a GPU assim geralmente custa performance. Enviar um bocado de variáveis por quadro é ok, mas a taxa de quadros pode realmente cair se você enviar centenas por quadro. Pode não parecer um cenário realista, mas se você tem algumas centenas de objetos na tela, por exemplo, e todos precisam ter iluminação aplicada a eles, todas com diferentes propriedades, então as coisas rapidamente podem ficar fora de controle. Vamos aprender mais sobre como otimizar nossos shaders em artigos futuros!

Desafio: você consegue fazer as cores mudarem ao longo do tempo? (Se você empacar, veja como fizemos na primeira parte desta série de tutoriais.)

Passo 5: lidando com texturas

Independentemente de como você carrega suas texturas, ou em que formato, você vai enviá-las ao seu shader da mesma forma em todas as plataformas, como variáveis uniformes.

Uma nota rápida sobre como carregar arquivos em JavaScript: você pode carregar imagens de uma URL externa sem muita dificuldade (que é o que vamos fazer aqui), mas se você quiser carregar uma imagem localmente, você vai ter problemas de permissão, porque o JavaScript não pode e não deve, normalmente, acessar arquivos em seu sistema. A maneira mais fácil de contornar esse problema é iniciar um servidor local em Python, que é talvez a solução mais simples.

Three.js fornece-nos uma função útil para carregar uma imagem como uma textura:

1
THREE.ImageUtils.crossOrigin = '';//Allows us to load an external image

2
var tex = THREE.ImageUtils.loadTexture( "https://tutsplus.github.io/Beginners-Guide-to-Shaders/Part2/SIPI_Jelly_Beans.jpg" );

A primeira linha só precisa ser definida uma vez. Você pode colocar uma URL para uma imagem lá.

Em seguida, temos que adicionar nossa textura ao objeto uniforms.

1
uniforms.texture = {type:'t',value:tex};

Finalmente, queremos declarar o nossa variável uniforme no código do shader e renderizar da mesma forma que fizemos no tutorial anterior, com a função texture2D:

1
uniform vec2 resolution;
2
uniform sampler2D texture;
3
void main() {
4
	vec2 pos = gl_FragCoord.xy / resolution.xy;
5
    gl_FragColor = texture2D(texture,pos);
6
}

E você deve ver algumas jujubas saborosas, espalhadas por nossa tela:

Você pode copiar e editar isto no CodePen.

(Esta imagem é uma imagem de teste padrão no campo da computação gráfica, retirado do Instituto de Sinal e Processamento de Imagem da Universidade da California (daí as iniciais IPI). Parece bastante apropriado usá-la como nossa imagem de teste enquanto aprendemos sobre shaders gráficos!)

Desafio: você consegue fazer a textura mudar de colorida para tons de cinza ao longo do tempo? (Novamente, se você empacar, nós fizemos isso na primeira parte desta série.)

Bônus: aplicação de shaders em outros objetos

Não há nada especial no plano que criamos. Nós poderíamos ter aplicado tudo isso no nosso cubo. Na verdade, só precisamos mudar a linha referente a geometria, de:

1
var geometry = new THREE.PlaneGeometry( 10, 10 );

para:

1
var geometry = new THREE.BoxGeometry( 1, 1, 1 );

Voa lá, jujubas em um cubo:

Você pode copiar e editar isto no CodePen.

Agora você pode estar pensando, "espera aí, isso não parece uma projeção adequada de uma textura em um cubo!" E você está certo. Se olharmos no início do nosso shader, vamos ver que tudo o que fizemos foi "mapear todos os pixels da imagem na tela". O fato de que é um cubo só significa que os pixels fora dele estão sendo descartados.

Se você quiser aplicá-la de modo que pareça estar desenhada fisicamente sobre o cubo, isso envolveria a criação de um motor 3D (o que parece um pouco bobo, considerando que já estamos usando um motor 3D e podemos pedir para desenhar a textura em cada lado individualmente). Esta série de tutorial é mais sobre o uso de shaders para fazer coisas que não conseguiríamos fazer sem, então nós não vamos nos aprofundar em detalhes como esse. A Udacity tem um ótimo curso sobre os fundamentos de gráficos 3D, se você quiser saber mais!

Próximos passos

Neste ponto, você deve ser capaz de fazer tudo o que fizemos no ShaderToy, exceto que agora você tem a liberdade de usar qualquer textura que quiser, e espero que também em qualquer plataforma que quiser.

Com essa liberdade, podemos agora fazer algo como configurar um sistema de iluminação, com sombras e detalhes realistas. Isto é o que a próxima parte incidirá sobre, bem como dicas e técnicas para otimização de shaders!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.