
O WebGL já existe há muito tempo, muitos artigos foram escritos sobre shaders, há uma série de lições. Mas, na maioria das vezes, são muito complicados para o designer de layout. É ainda melhor dizer que eles cobrem grandes quantidades de informações que o desenvolvedor do game engine precisa e não o designer do layout. Eles começam imediatamente com a construção de uma cena complexa, uma câmera, luz ... Em um site comum, para criar um par de efeitos com fotos, todo esse conhecimento é redundante. Como resultado, as pessoas fazem estruturas arquitetônicas muito complexas e escrevem shaders longos e longos para ações muito simples em essência.
Tudo isso levou a uma introdução aos aspectos do trabalho com shaders que provavelmente são úteis ao designer de layout para criar vários efeitos 2D com fotos no site. Obviamente, ajustados pelo fato de que eles próprios são relativamente raramente usados no design de interface. Criaremos um modelo inicial em JS puro, sem bibliotecas de terceiros, e consideraremos as idéias de criar alguns efeitos populares baseados na troca de pixels, difíceis de fazer no SVG, mas ao mesmo tempo eles são facilmente implementados usando shaders.
Supõe-se que o leitor já esteja familiarizado com a canvas
, descreve o que é o WebGL e possui um conhecimento mínimo de matemática. Alguns pontos serão descritos de maneira simplista, não acadêmica, a fim de fornecer uma compreensão prática das tecnologias para trabalhar com elas, e não uma teoria completa de sua cozinha interna ou termos de aprendizado. Existem livros inteligentes para isso.
Deve-se notar imediatamente que os editores integrados ao artigo do CodePen têm a capacidade de influenciar o desempenho do que é feito neles. Portanto, antes de escrever um comentário, informando que algo está diminuindo a velocidade do seu macbook, verifique se o problema não vem deles.
Idéias principais
O que é um shader?
O que é um shader de fragmento? Este é essencialmente um pequeno programa. É executado para cada pixel na anvas
. Se tivermos uma canvas
tamanho 1000x500px, esse programa será executado 500.000 vezes, sempre recebendo como parâmetros de entrada as coordenadas do pixel para o qual está sendo executado no momento. Isso tudo acontece na GPU em uma variedade de threads paralelos. No processador central, esses cálculos levariam muito mais tempo.
Um sombreador de vértice também é um programa, mas não é executado para todos os pixels da canvas
, mas para cada vértice nas formas em que tudo é construído no espaço tridimensional. Também paralelo a todos os vértices. Assim, a entrada recebe as coordenadas do vértice, não o pixel.
Além disso, no contexto de nossa tarefa, ocorre o seguinte:
- Tomamos um conjunto de coordenadas dos vértices do retângulo, nos quais a fotografia será "desenhada".
- Um sombreador de vértice para cada vértice considera sua localização no espaço. Para nós, isso se resumirá a um caso especial - um avião paralelo à tela. Fotos em 3d não precisamos. A projeção subsequente no plano da tela não pode dizer nada.
- Além disso, para cada fragmento visível, e em nosso contexto para todos os fragmentos de pixel, é executado um sombreador de fragmento, que tira uma foto e as coordenadas atuais, conta alguma coisa e fornece cor para esse pixel específico.
- Se não houvesse lógica no shader de fragmento, o comportamento de tudo isso será semelhante ao
drawImage()
da canvas
. Mas então adicionamos essa lógica e obtemos muitas coisas interessantes.
Esta é uma descrição muito simplificada, mas deve ficar claro quem faz o quê.
Um pouco sobre sintaxe
Os shaders são escritos em GLSL - OpenGL Shading Language. Essa linguagem é muito parecida com C. Não faz sentido descrever toda a sintaxe e os métodos padrão aqui, mas você sempre pode usar a folha de dicas:
Cada sombreador tem uma função principal, com a qual sua execução começa. Parâmetros de entrada padrão para sombreadores e saída dos resultados de seu trabalho são implementados através de variáveis especiais com o prefixo gl_
. Eles são reservados com antecedência e estão disponíveis dentro desses mesmos shaders. Portanto, as coordenadas do vértice para o sombreador de vértice estão na variável gl_Position
, as coordenadas do fragmento (pixel) para o sombreador de fragmento estão em gl_FragCoord
, etc. Você sempre pode encontrar a lista completa de variáveis especiais disponíveis na mesma folha de dicas.
Os principais tipos de variáveis no GLSL são bastante despretensiosos - void
, bool
, int
, float
... Se você trabalhou com qualquer linguagem semelhante a C, já os viu. Existem outros tipos, em particular vetores de diferentes dimensões - vec2
, vec3
, vec4
. Nós os usaremos constantemente para coordenadas e cores. As variáveis que podemos criar são de três modificações importantes:
- Uniforme - dados globais em todos os sentidos. Passado de fora, o mesmo para todas as chamadas de shaders de vértice e fragmento.
- Atributo - Esses dados são transferidos com mais precisão e para cada chamada de sombreador pode ser diferente.
- Variação - Necessário para transferir dados de shaders de vértice para shaders de fragmentos.
É útil prefixar u / a / v para todas as variáveis nos shaders para facilitar a compreensão de quais dados vieram.
Acredito que vale a pena seguir para um exemplo prático para assistir imediatamente a tudo isso em ação e não carregar sua memória.
Modelo de início de cozinha
Vamos começar com JS. Como geralmente acontece quando se trabalha com canvas
, precisamos dela e do contexto. Para não carregar o código de amostra, criaremos variáveis globais:
const CANVAS = document.getElementById(IDs.canvas); const GL = canvas.getContext('webgl');
Pule o momento associado ao tamanho da canvas
e seu recálculo ao redimensionar a janela do navegador. Esse código está incluído nos exemplos e geralmente depende do restante do layout. Não faz sentido se concentrar nele. Vamos para as ações com o WebGL.
function createProgram() { const shaders = getShaders(); PROGRAM = GL.createProgram(); GL.attachShader(PROGRAM, shaders.vertex); GL.attachShader(PROGRAM, shaders.fragment); GL.linkProgram(PROGRAM); GL.useProgram(PROGRAM); }
Primeiro, compilamos os shaders (será um pouco menor), criamos um programa, adicionamos os dois shaders a ele e criamos um link. Neste ponto, a compatibilidade dos shaders é verificada. Lembre-se de variáveis variáveis que são passadas do vértice para o fragmento? - Em particular, seus conjuntos são verificados aqui para que mais tarde no processo não ocorra que algo não tenha sido transmitido ou transmitido, mas que não seja. Obviamente, essa verificação não revelará erros lógicos, acho que isso é compreensível.
As coordenadas dos vértices serão armazenadas em uma matriz de buffer especial e serão transmitidas em partes, um vértice, para cada chamada de sombreador. A seguir, descrevemos alguns detalhes para trabalhar com essas peças. Primeiramente, usaremos as coordenadas do vértice no sombreador através da a_position
atributo a_position
. Pode ser chamado de forma diferente, não importa. Nós obtemos sua localização (isso é algo como um ponteiro em C, mas não um ponteiro, mas um número de entidade que existe apenas dentro do programa).
const vertexPositionAttribute = GL.getAttribLocation(PROGRAM, 'a_position');
A seguir, indicamos que uma matriz com coordenadas será passada por essa variável (no próprio shader, já a perceberemos como um vetor). O WebGL descobrirá de forma independente quais coordenadas de quais pontos de nossas formas devem ser passadas para as quais o sombreador chama. Definimos apenas os parâmetros para a matriz vetorial que será transmitida: dimensão - 2 (transmitiremos as coordenadas (x,y)
), consiste em números e não é normalizado. Os últimos parâmetros não são interessantes para nós, deixamos zeros por padrão.
GL.enableVertexAttribArray(vertexPositionAttribute); GL.vertexAttribPointer(vertexPositionAttribute, 2, GL.FLOAT, false, 0, 0);
Agora crie o buffer em si com as coordenadas dos vértices do nosso plano, nos quais a foto será exibida. As coordenadas "2d" são mais claras, mas, para nossas tarefas, isso é a coisa mais importante.
function createPlane() { GL.bindBuffer(GL.ARRAY_BUFFER, GL.createBuffer()); GL.bufferData( GL.ARRAY_BUFFER, new Float32Array([ -1, -1, -1, 1, 1, -1, 1, 1 ]), GL.STATIC_DRAW ); }
Este quadrado será suficiente para todos os nossos exemplos. STATIC_DRAW
significa que o buffer é carregado uma vez e depois será reutilizado. Não enviaremos nada novamente.
Antes de passar para os próprios shaders, vejamos a compilação deles:
function getShaders() { return { vertex: compileShader( GL.VERTEX_SHADER, document.getElementById(IDs.shaders.vertex).textContent ), fragment: compileShader( GL.FRAGMENT_SHADER, document.getElementById(IDs.shaders.fragment).textContent ) }; } function compileShader(type, source) { const shader = GL.createShader(type); GL.shaderSource(shader, source); GL.compileShader(shader); return shader; }
Nós obtemos o código de sombreamento dos elementos da página, criamos um sombreador e o compilamos. Em teoria, você pode armazenar o código do sombreador em arquivos separados e carregá-lo durante a montagem como uma string no lugar certo, mas o CodePen não fornece essa oportunidade para exemplos. Muitas lições sugerem escrever código diretamente na linha em JS, mas o idioma não o transforma em um idioma conveniente. Embora, claro, tenha gosto e cor ...
Se ocorrer um erro durante a compilação, o script continuará sendo executado, mostrando alguns avisos no console que não fazem muito sentido. É útil examinar os logs após a compilação para não sobrecarregar o cérebro com o que não foi compilado lá:
console.log(GL.getShaderInfoLog(shader));
O WebGL fornece várias opções diferentes para rastrear problemas ao compilar sombreadores e criar um programa, mas, na prática, acontece que em tempo real não podemos consertar nada. Muitas vezes, seremos guiados pelo pensamento "caiu - depois caiu" e não carregaremos o código com várias verificações extras.
Vamos passar para os próprios shaders
Como teremos apenas um plano com o qual não faremos nada, basta um simples sombreador de vértices, o que faremos desde o início. Os principais esforços serão focados nos shaders de fragmentos e todos os exemplos subsequentes serão relevantes para eles.
Tente escrever código de sombreador com nomes de variáveis mais ou menos significativos. Na rede, você encontrará exemplos em que funções com matemática vigorosa para 200 linhas de texto contínuo serão reunidas a partir de variáveis de uma letra, mas apenas porque alguém o faz não significa que vale a pena repetir. Essa abordagem não é uma “especificidade do trabalho com GL”, é uma cópia e pasta banal de códigos-fonte do século passado, escritos por pessoas que, na juventude, tinham restrições no tamanho dos nomes de variáveis.
Primeiro, o vertex shader. Um vetor 2D com coordenadas (x,y)
será transferido para a variável de atributo a_position
, como dissemos. O sombreador deve retornar um vetor de quatro valores (x,y,z,w)
. Ele não moverá nada no espaço; portanto, no eixo z, simplesmente zeramos tudo e configuramos o valor de w para a unidade padrão. Se você está se perguntando por que existem quatro e não três coordenadas, use a pesquisa de rede para "coordenadas uniformes".
<script id='vertex-shader' type='x-shader/x-vertex'> precision mediump float; attribute vec2 a_position; void main() { gl_Position = vec4(position, 0, 1); } </script>
O resultado do trabalho é registrado em uma variável especial gl_Position
. Os sombreadores não têm uma return
no sentido pleno da palavra; eles anotam todos os resultados de seu trabalho em variáveis especialmente reservadas para esses fins.
Observe o trabalho de precisão para o tipo de dados flutuante. Para evitar alguns dos problemas em dispositivos móveis, a precisão deve ser pior que o highp e deve ser a mesma nos dois shaders. Isso é mostrado como um exemplo aqui, mas é uma boa prática em telefones desativar completamente essa beleza com shaders.
O sombreador de fragmento sempre retornará a mesma cor para começar. Nosso quadrado ocupará toda a canvas
; portanto, aqui definimos a cor de cada pixel:
<script id='fragment-shader' type='x-shader/x-fragment'> precision mediump float; #define GOLD vec4(1.0, 0.86, 0.6, 1.0) void main() { gl_FragColor = GOLD; } </script>
Você pode prestar atenção aos números que descrevem a cor. Isso é familiar a todos os tipógrafos RGBA, apenas normalizados. Os valores não são números inteiros de 0 a 255, mas fracionários de 0 a 1. A ordem é a mesma.
Não se esqueça de usar o pré-processador para todas as constantes mágicas em projetos reais - isso torna o código mais compreensível sem afetar o desempenho (a substituição, como em C, ocorre durante a compilação).
Vale ressaltar outro ponto sobre o pré-processador:
O uso de verificações constantes #ifdef GL_ES em várias lições é desprovido de significado prático. hoje em nosso navegador, simplesmente não existem outras opções contábeis.
Mas é hora de já olhar para o resultado:
O quadrado dourado indica que os shaders estão funcionando conforme o esperado. Faz sentido brincar um pouco com eles antes de começar a trabalhar com fotos.
Vetores de gradiente e transformação
Normalmente, os tutoriais WebGL começam desenhando gradientes. Isso faz pouco sentido prático, mas será útil observar alguns pontos.
void main() { gl_FragColor = vec4(gl_FragCoord.zxy / 500.0, 1.0); }
Neste exemplo, usamos as coordenadas do pixel atual como cor. Você verá isso frequentemente em exemplos na rede. Ambos são vetores. Portanto, ninguém se incomoda em misturar tudo de uma vez. Os evangelistas TypeScript devem ter um ataque aqui. Um ponto importante é como obtemos apenas parte das coordenadas do vetor. Propriedades .x
, .y
, .z
, .xy
, .zy
, .xyz
, .zyx
, .xyzw
, etc. em seqüências diferentes, você pode extrair os elementos de um vetor em uma determinada ordem na forma de outro vetor. Muito convenientemente implementado. Além disso, um vetor de dimensão superior pode ser criado a partir de um vetor de dimensão inferior, adicionando os valores ausentes, como fizemos.
Sempre indique explicitamente a parte fracionária dos números. Não há conversão automática int -> float aqui.
Uniformes e passagem do tempo
O próximo exemplo útil é o uso de uniformes. Esses são os dados mais comuns para todas as chamadas de sombreador. Nós obtemos sua localização da mesma maneira que para variáveis de atributo, por exemplo:
GL.getUniformLocation(PROGRAM, 'u_time')
Em seguida, podemos definir os valores antes de cada quadro. Assim como os vetores, existem muitos métodos semelhantes aqui, começando com a palavra uniform
, em seguida, vem a dimensão da variável (1 para números, 2, 3 ou 4 para vetores) e o tipo (f - float, i - int, v - vector) .
function draw(timeStamp) { GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_time'), timeStamp / 1000.0); GL.drawArrays(GL.TRIANGLE_STRIP, 0, 4); window.requestAnimationFrame(draw); }
De fato, nem sempre precisamos de 60fps nas interfaces. É bem possível adicionar uma lentidão ao requestAnimationFrame e reduzir a frequência dos quadros de redesenho.
Por exemplo, mudaremos a cor do preenchimento. Nos shaders, todas as funções matemáticas básicas estão disponíveis - sin
, cos
, tan
, asin
, acos
, atan
, pow
, exp
, log
, sqrt
, abs
e outras. Vamos usar dois deles.
uniform float u_time; void main() { gl_FragColor = vec4( abs(sin(u_time)), abs(sin(u_time * 3.0)), abs(sin(u_time * 5.0)), 1.0); }
O tempo em tais animações é um conceito relativo. Aqui, usamos os valores fornecidos pelo requestAnimationFrame
, mas podemos criar nosso próprio "tempo". A idéia é que, se alguns parâmetros são descritos em função do tempo, podemos mudar o tempo na direção oposta, desacelerar, acelerar ou retornar ao seu estado original. Isso pode ser muito útil.
Mas há exemplos abstratos suficientes, vamos continuar usando imagens.
Carregando uma imagem em uma textura
Para usar a imagem, precisamos criar uma textura, que será renderizada em nosso avião. Para começar, carregue a própria imagem:
function createTexture() { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => {
Após o carregamento, crie uma textura e indique que ele ficará abaixo do número 0. No WebGL, pode haver muitas texturas ao mesmo tempo e devemos indicar explicitamente a quais comandos subsequentes se relacionarão. Nos nossos exemplos, haverá apenas uma textura, mas ainda indicamos explicitamente que será zero.
const texture = GL.createTexture(); GL.activeTexture(GL.TEXTURE0); GL.bindTexture(GL.TEXTURE_2D, texture);
Resta adicionar uma imagem. Também dizemos imediatamente que ele precisa ser girado ao longo do eixo Y, porque no WebGL, o eixo está de cabeça para baixo:
GL.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, true); GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGB, GL.RGB, GL.UNSIGNED_BYTE, image);
Em teoria, a textura deve ser quadrada. Mais precisamente, eles devem ter um tamanho igual à potência de dois - 32px, 64px, 128px etc. Mas todos entendemos que ninguém processará fotos e elas terão proporções diferentes a cada vez. Isso causará erros mesmo se o tamanho da canvas
se encaixar perfeitamente na textura. Portanto, preenchemos todo o espaço até as bordas do plano com os pixels extremos da imagem. Esta é uma prática padrão, embora pareça um pouco de muleta.
GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR);
Resta transferir a textura para os shaders. Como esses dados são comuns a todos, usamos o modificador uniform
.
GL.uniform1i(GL.getUniformLocation(PROGRAM, 'u_texture'), 0);
Agora podemos usar as cores da textura no shader de fragmento. Mas também queremos que a imagem ocupe toda a canvas
. Se a imagem e a canvas
tiverem as mesmas proporções, essa tarefa se tornará trivial. Primeiro, transferimos o tamanho da canvas
para os shaders (isso deve ser feito toda vez que você altera o tamanho):
GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_canvas_size'), Math.max(CANVAS.height, CANVAS.width));
E divida as coordenadas nele:
uniform sampler2D u_texture; uniform float u_canvas_size; void main() { gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size); }
Neste ponto, você pode pausar e preparar o chá. Fizemos todo o trabalho preparatório e passamos a criar vários efeitos.
Efeitos
Ao criar vários efeitos, a intuição e a experimentação desempenham um papel importante. Geralmente, você pode substituir um algoritmo complexo por algo completamente simples e obter um resultado semelhante. O usuário final não notará a diferença, mas agilizamos o trabalho e simplificamos o suporte. O WebGL não fornece ferramentas adequadas para depuração de shaders, por isso é benéfico termos pequenos pedaços de código que podem caber na cabeça como um todo.
Menos código significa menos problemas. E é mais fácil de ler. Sempre verifique os shaders encontrados na rede para ações desnecessárias. Acontece que você pode remover metade do código e nada muda.
Vamos brincar um pouco com o shader. A maioria dos nossos efeitos será baseada no fato de retornarmos a cor não do pixel na textura que deveria estar neste local, mas em alguns dos vizinhos. É útil tentar adicionar às coordenadas o resultado de uma função padrão das coordenadas. O tempo também será útil - portanto, o resultado da execução será mais fácil de rastrear e, no final, ainda produziremos efeitos animados. Vamos tentar usar o seno:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y))
O resultado é estranho. Obviamente, tudo se move com muita amplitude. Divida tudo por algum número:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y) / 250.0)
Já está melhor. Agora está claro que tivemos um pouco de emoção. Em teoria, para aumentar cada onda, precisamos dividir o argumento seno - a coordenada. Vamos fazer isso:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y / 30.0) / 250.0)
Efeitos semelhantes são frequentemente acompanhados pela seleção de coeficientes. Isso é feito a olho nu. Como na culinária, no começo será difícil adivinhar, mas depois acontecerá por si só. O principal é entender pelo menos aproximadamente o que esse ou aquele coeficiente na fórmula resultante afeta. Depois que os coeficientes são selecionados, faz sentido colocá-los em macros (como foi o primeiro exemplo) e fornecer nomes significativos.
Espelho torto, bicicletas e experiências
Pensar é bom. Sim, existem algoritmos prontos para resolver alguns problemas que podemos apenas pegar e usar. , .
, " ", . O que fazer?
, , ? . , rand() - . , , , , . . . , . . . -, . . , , , . , "":
float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); }
, , , NVIDIA ATI . , .
, , :
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy) / 100.0)
:
gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy + vec2(sin(u_time))) / 250.0)
, , :
, . , , . — . Como fazer isso? . .
0 1, - . 5 — . , .
vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; gl_FragColor = texture2D(u_texture, texture_coord + rand(floor(texture_coord * 5.0) + vec2(sin(u_time))) / 100.0);
, - . - . , , . ?
, , , - . , . , .. -. , . . , , . .
sin
cos
, . . .
gl_FragColor = texture2D(u_texture, texture_coord + vec2( noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0));
. fract
. 1 1 — :
float noise(vec2 position) { vec2 block_position = floor(position); float top_left_value = rand(block_position); float top_right_value = rand(block_position + vec2(1.0, 0.0)); float bottom_left_value = rand(block_position + vec2(0.0, 1.0)); float bottom_right_value = rand(block_position + vec2(1.0, 1.0)); vec2 computed_value = fract(position);
. WebGL smoothstep
, :
vec2 computed_value = smoothstep(0.0, 1.0, fract(position))
, . , X :
return computed_value.x;
… , , ...
- , , ... .
y — , . ?
return length(computed_value);
.
. 0.5 — .
return mix(top_left_value, top_right_value, computed_value.x) + (bottom_left_value - top_left_value) * computed_value.y * (1.0 - computed_value.x) + (bottom_right_value - top_right_value) * computed_value.x * computed_value.y - 0.5;
:
, , , .
, , . - .
uniform-, . 0 1, 0 — , 1 — .
uniform float u_intensity;
:
gl_FragColor = texture2D(u_texture, texture_coord + vec2(noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0) * u_intensity);
, .
( 0 1), .
, , , . — requestAnimationFrame. , FPS.
, . uniform-.
document.addEventListener('mousemove', (e) => { let rect = CANVAS.getBoundingClientRect(); MOUSE_POSITION = [ e.clientX - rect.left, rect.height - (e.clientY - rect.top) ]; GL.uniform2fv(GL.getUniformLocation(PROGRAM, 'u_mouse_position'), MOUSE_POSITION); });
, . — , .
void main() { vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; vec2 direction = u_mouse_position / u_canvas_size - texture_coord; float dist = distance(gl_FragCoord.xy, u_mouse_position) / u_canvas_size; if (dist < 0.4) { gl_FragColor = texture2D(u_texture, texture_coord + u_intensity * direction * dist * 1.2 ); } else { gl_FragColor = texture2D(u_texture, texture_coord); } }
- . .
. , .
. Glitch- , SVG. . — . ? — , , , .
float random_value = rand(vec2(texture_coord.y, u_time)); if (random_value < 0.05) { gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 5.0, texture_coord.y)); } else { gl_FragColor = texture2D(u_texture, texture_coord); }
" ?" — , . .
. — , .
float random_value = rand(vec2(floor(texture_coord.y * 20.0), u_time));
. , :
gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 4.0, texture_coord.y)) + vec4(vec3(random_value), 1.0)
. — . , — .r
, .g
, .b
, .rg
, .rb
, .rgb
, .bgr
, ... .
:
float random_value = u_intensity * rand(vec2(floor(texture_coord.y * 20.0), u_time));
Qual é o resultado?
, , . , , — .