No meu tutorial
“Criando Shaders”, observei principalmente os shaders de fragmento, que são suficientes para implementar quaisquer efeitos e exemplos 2D no
ShaderToy . Mas há toda uma categoria de técnicas que requerem o uso de shaders de vértice. Neste tutorial, falarei sobre a criação de um shader de água estilizado para desenhos animados e apresentarei os shaders de vértice. Também falarei sobre o buffer de profundidade e como usá-lo para obter mais informações sobre a cena e criar linhas de espuma do mar.
Aqui está como será o efeito final. Uma demonstração interativa pode ser vista
aqui .
Este efeito consiste nos seguintes elementos:
- Uma malha translúcida de água com polígonos subdivididos e vértices deslocados para criar ondas.
- Linhas de água estáticas na superfície.
- Flutuabilidade simulada do barco.
- Linhas dinâmicas de espuma em torno dos limites dos objetos na água.
- Pós-processamento para criar distorção de tudo debaixo d'água.
Nesse sentido, gosto do fato de que ele aborda muitos conceitos diferentes de computação gráfica, por isso nos permitirá usar as idéias dos tutoriais anteriores, bem como desenvolver técnicas que possam ser aplicadas em novos efeitos.
Neste tutorial, usarei o
PlayCanvas , simplesmente porque é um IDE da web gratuito e conveniente, mas tudo pode ser aplicado a qualquer outro ambiente WebGL sem problemas. No final do artigo, a versão do código fonte do Three.js será apresentada. Assumiremos que você já conhece bem os shaders de fragmentos e a interface PlayCanvas. Você pode atualizar seu conhecimento sobre shaders
aqui e se familiarizar com o PlayCanvas
aqui .
Configuração do ambiente
O objetivo desta seção é configurar nosso projeto PlayCanvas e inserir nele vários objetos ambientais que a água influenciará.
Se você não possui uma conta PlayCanvas,
registre-a e crie um novo
projeto em branco . Por padrão, você deve ter alguns objetos na cena, uma câmera e uma fonte de luz.
Inserir modelos
Um ótimo recurso para encontrar modelos 3D para a web é o projeto Google
Poly . Peguei o
modelo de barco de lá. Depois de baixar e descompactar o arquivo, você encontrará arquivos
.obj
e
.png
nele.
- Arraste os dois arquivos para a janela Assets do projeto PlayCanvas.
- Selecione o material gerado automaticamente e selecione o arquivo
.png
como seu mapa difuso.
Agora você pode arrastar
Tugboat.json para a cena e excluir os objetos Box e Plane. Se o barco parecer muito pequeno, você poderá aumentar sua escala (defino o valor para 50).
Da mesma forma, você pode adicionar outros modelos à cena.
Câmera em órbita
Para configurar a câmera voando em órbita, copiaremos o script
deste exemplo do PlayCanvas . Siga o link e clique no
Editor para abrir o projeto.
- Copie o conteúdo de
mouse-input.js
e orbit-camera.js
deste projeto tutorial em arquivos com os mesmos nomes do seu projeto. - Adicione um componente Script à câmera.
- Anexe dois scripts à câmera.
Dica: para organizar o projeto, você pode criar pastas na janela Ativos. Coloquei esses dois scripts de câmera na pasta Scripts / Camera /, meu modelo em Models / e o material na pasta Materials /.
Agora, quando você inicia o jogo (o botão de lançamento na parte superior direita da janela da cena), você deve ver um barco que você pode inspecionar com uma câmera, movendo-o em órbita com o mouse.
Divisão de polígonos de superfície da água
O objetivo desta seção é criar uma malha subdividida que será usada como superfície da água.
Para criar uma superfície da água, adaptamos parte do código do
tutorial de geração de relevo . Crie um novo
Water.js
script
Water.js
. Abra este script para edição e crie uma nova função
GeneratePlaneMesh
que será parecida com esta:
Water.prototype.GeneratePlaneMesh = function(options){
Agora podemos chamá-lo na função de
initialize
:
Water.prototype.initialize = function() { this.GeneratePlaneMesh({subdivisions:100, width:10, height:10}); };
Agora, quando você iniciar o jogo, verá apenas uma superfície plana. Mas isso não é apenas uma superfície plana, é uma malha composta por milhares de picos. Como exercício, tente verificar isso sozinho (esse é um bom motivo para estudar o código que acabou de copiar).
Problema 1: altere a coordenada Y de cada vértice por um valor aleatório para que o plano se pareça com a figura abaixo.
As ondas
O objetivo desta seção é designar a superfície da água do seu próprio material e criar ondas animadas.
Para obter os efeitos de que precisamos, você precisa configurar seu próprio material. A maioria dos mecanismos 3D possui um conjunto de shaders predefinidos para renderizar objetos e uma maneira de redefini-los. Aqui está um
bom link sobre como fazer isso no PlayCanvas.
Acessório Shader
Vamos criar uma nova função
CreateWaterMaterial
que
CreateWaterMaterial
novo material com um shader alterado e o retorna:
Water.prototype.CreateWaterMaterial = function(){
Essa função pega o código de sombreamento de vértice e fragmento dos atributos de script Então, vamos defini-los na parte superior do arquivo (após a linha
pc.createScript
):
Water.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Water.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' });
Agora podemos criar esses arquivos de sombreador e anexá-lo ao nosso script. Retorne ao editor e crie dois arquivos de sombreador:
Water.frag e
Water.vert . Anexe esses sombreadores ao script, como mostra a figura abaixo.
Se os novos atributos não forem exibidos no editor, clique no botão
Analisar para atualizar o script.
Agora cole esse shader básico no
Water.frag :
void main(void) { vec4 color = vec4(0.0,0.0,1.0,0.5); gl_FragColor = color; }
E este está em
Water.vert :
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0); }
Por fim, retorne ao
Water.js para usar nosso novo material em vez do material padrão. Ou seja, em vez de:
var material = new pc.StandardMaterial();
inserir:
var material = this.CreateWaterMaterial();
Agora, depois de iniciar o jogo, o avião deve estar azul.
Reinicialização a quente
Por enquanto, apenas configuramos espaços em branco para o nosso novo material. Antes de começar a escrever efeitos reais, desejo configurar o recarregamento automático de código.
Depois de descomentar a função de
swap
em qualquer arquivo de script (por exemplo, em Water.js), ativaremos o recarregamento a quente. Mais tarde, veremos como usar isso para manter o estado, mesmo ao atualizar o código em tempo real. Mas, por enquanto, queremos apenas reaplicar os shaders depois de fazer as alterações. Antes de executar no WebGL, os shaders são compilados; portanto, para isso, precisamos recriar nosso material.
Verificaremos se o conteúdo do nosso código de sombreador mudou e, se houver, criaremos o material novamente. Primeiro, salve os shaders atuais na
inicialização :
E, na
atualização , verificamos se alguma alteração ocorreu:
Agora, para garantir que isso funcione, inicie o jogo e altere a cor do avião no
Water.frag para um azul mais agradável. Após salvar o arquivo, ele deve ser atualizado mesmo sem uma reinicialização e reinicie! Aqui está a cor que eu escolhi:
vec4 color = vec4(0.0,0.7,1.0,0.5);
Vertex Shaders
Para criar ondas, devemos mover cada vértice da nossa malha em cada quadro. Parece que será muito ineficiente, mas cada vértice de cada modelo já está transformado em cada quadro renderizado. É isso que o shader de vértice faz.
Se percebermos um shader de fragmento como uma função executada para cada pixel, obtém sua posição e retorna a cor, um
shader de vértice é uma função que roda para cada pixel, obtém sua posição e retorna
sua posição .
Um sombreador de vértice, por padrão, obtém uma
posição no mundo do modelo e retorna sua
posição na tela . Nossa cena 3D é definida nas coordenadas x, ye z, mas como o monitor é um plano bidimensional plano, projetamos um mundo 3D em uma tela 2D. Matrizes do tipo, projeção e modelo estão envolvidas nessa projeção; portanto, não vamos considerá-lo neste tutorial. Mas se você quiser entender o que exatamente acontece em cada estágio, aqui está um
guia muito bom .
Ou seja, esta linha:
gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0);
recebe
aPosition
como uma posição no mundo 3D de um vértice específico e o converte em
gl_Position
, ou seja, na posição final na tela 2D. O prefixo "a" em aPosition indica que esse valor é um
atributo . Não esqueça que a variável
uniforme é um valor que podemos definir na CPU e passá-lo ao shader. Mantém o mesmo valor para todos os pixels / vértices. Por outro lado, o valor do atributo é obtido da
matriz da CPU especificada. Um sombreador de vértice é chamado para cada valor dessa matriz de atributos.
Você pode ver que esses atributos estão configurados na definição de sombreador que definimos no Water.js:
var shaderDefinition = { attributes: { aPosition: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0, }, vshader: vertexShader, fshader: fragmentShader };
O PlayCanvas se encarrega de configurar e transmitir uma matriz de posições de vértices para
aPosition
ao passar essa enumeração, mas, no caso geral, podemos passar qualquer matriz de dados para o sombreador de vértices.
Movimento de vértice
Suponha que queremos compactar o plano inteiro multiplicando todos os valores de
x
por 0,5. Precisamos alterar
aPosition
ou
gl_Position
?
Vamos tentar uma
aPosition
primeiro. Não podemos alterar o atributo diretamente, mas podemos criar uma cópia:
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition; pos.x *= 0.5; gl_Position = matrix_viewProjection * matrix_model * vec4(pos, 1.0); }
Agora o avião deve se parecer mais com um retângulo. E não há nada de estranho nisso. Mas o que acontece se tentarmos mudar a
gl_Position
?
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition;
Até você começar a mover a câmera, ela pode ter a mesma aparência. Alteramos as coordenadas do espaço da tela, ou seja, a imagem depende de
como a vemos .
Assim, podemos mover os vértices e, ao mesmo tempo, é importante distinguir entre trabalho nos espaços do mundo e da tela.
Tarefa 2: você pode mover toda a superfície do plano várias unidades para cima (ao longo do eixo Y) no sombreador de vértices sem distorcer sua forma?
Tarefa 3: Eu disse que gl_Position é bidimensional, mas gl_Position.z também existe. Você pode verificar se esse valor afeta algo e, em caso afirmativo, para que é usado?
Adicionando tempo
A última coisa que precisamos antes de começar a criar ondas em movimento é uma variável uniforme que pode ser usada com o tempo. Declare uniforme no shader de vértice:
uniform float uTime;
Agora, para passá-lo ao shader,
vamos voltar ao
Water.js e definir a variável time na inicialização:
Water.prototype.initialize = function() { this.time = 0;
Agora, para transferir a variável para o shader, usamos
material.setParameter
. Primeiro, configuramos o valor inicial no final da função
CreateWaterMaterial
:
Agora, na função de
update
, podemos executar um incremento de tempo e acessar o material usando o link criado para isso:
this.time += 0.1; this.material.setParameter('uTime',this.time);
Finalmente, na função de troca, copiamos o valor do horário antigo para que, mesmo após alterar o código, ele continue aumentando sem redefinir para 0.
Water.prototype.swap = function(old) { this.time = old.time; };
Agora está tudo pronto. Execute o jogo para garantir que não haja erros. Agora vamos mover nosso avião usando a função de tempo em
Water.vert
:
pos.y += cos(uTime)
E o nosso avião deve começar a subir e descer! Como agora temos uma função de troca, também podemos atualizar o Water.js sem precisar reiniciar. Para garantir que isso funcione, tente alterar o incremento de tempo.
Tarefa 4: você pode mover os vértices para que se pareçam com as ondas na figura abaixo?
Deixe-me dizer-lhe que examinei em detalhes o tópico de várias maneiras de criar ondas
aqui . O artigo está relacionado ao 2D, mas os cálculos matemáticos são aplicáveis ao nosso caso. Se você só quer ver a solução,
aqui está a essência .
Translucência
O objetivo desta seção é criar uma superfície translúcida da água.
Você pode perceber que a cor retornada ao Water.frag tem um valor de canal alfa de 0,5, mas a superfície ainda permanece opaca. Em muitos casos, a transparência ainda se torna um problema não resolvido na computação gráfica. Uma maneira de baixo custo para resolvê-lo é usar a mistura.
Geralmente, antes de desenhar um pixel, ele verifica o valor no
buffer de profundidade e o compara com seu próprio valor de profundidade (sua posição no eixo Z) para determinar se deve ou não redesenhar o pixel atual da tela. É isso que permite renderizar a cena corretamente sem precisar classificar os objetos de volta para a frente.
Ao misturar, em vez de simplesmente rejeitar o pixel ou substituir, podemos combinar a cor do pixel já renderizado (destino) com o pixel que vamos desenhar (a fonte). Uma lista de todas as funções de mixagem disponíveis no WebGL pode ser encontrada
aqui .
Para que o canal alfa funcione de acordo com nossas expectativas, queremos que a cor combinada do resultado seja uma fonte multiplicada por um canal alfa mais um pixel de destino multiplicado por um menos alfa. Em outras palavras, se alfa = 0,4, a cor final deve ter um valor:
finalColor = source * 0.4 + destination * 0.6;
No PlayCanvas, esta é a operação que o
pc.BLEND_NORMAL executa .
Para habilitá-lo, basta definir a propriedade do material dentro de
CreateWaterMaterial
:
material.blendType = pc.BLEND_NORMAL;
Se você iniciar o jogo agora, a água ficará translúcida! No entanto, ainda é imperfeito. O problema surge quando a superfície translúcida é sobreposta a si mesma, como mostrado abaixo.
Podemos eliminá-lo usando
alfa para cobertura , uma técnica de multisampling para transparência, em vez de mesclar:
Mas está disponível apenas no WebGL 2. No restante do tutorial, por uma questão de simplicidade, usarei a mistura.
Resumir
Montamos o ambiente e criamos uma superfície translúcida da água com ondas animadas do vertex shader. Na segunda parte do tutorial, consideraremos a flutuabilidade dos objetos, adicionaremos linhas à superfície da água e criaremos linhas de espuma ao longo dos limites dos objetos que se cruzam com a superfície.
Na terceira (última) parte, consideraremos a aplicação do efeito pós-processamento de distorções subaquáticas e consideraremos idéias para melhorias adicionais.
Código fonte
O projeto concluído do PlayCanvas pode ser encontrado
aqui . Nosso repositório também possui uma
porta de projeto em Three.js .