Jogo 3D Shaders para Iniciantes: Efeitos

[ A primeira parte ]

Depois de lidar com o básico, nesta parte do artigo, implementamos efeitos como contornos de objetos, bloom, SSAO, blur, profundidade de campo, pixelização e outros.

Esboços



Criar contornos em torno da geometria da cena dá ao jogo uma aparência única que se assemelha a quadrinhos ou desenhos animados.

Material difuso


O sombreador de contorno precisa de uma textura de entrada para reconhecer e colorir as bordas. Os candidatos a essa textura de entrada podem ser cores difusas de materiais, cores de texturas difusas, vértice normal ou mesmo cores de mapas normais.

uniform struct { vec4 diffuse ; } p3d_Material; out vec4 fragColor; void main() { vec3 diffuseColor = p3d_Material.diffuse.rgb; fragColor = vec4(diffuseColor, 1); } 

Esse é um pequeno shader de fragmento que transforma a cor difusa de um material de geometria em uma textura de buffer de quadro. Essa textura de cor difusa do buffer do quadro será a textura de entrada para o sombreador de caminho.


Essa é a textura da cor difusa do material do buffer de quadros, que exibe as cores que definimos no Blender. O sombreador de contorno reconhecerá as bordas da cena e as colorirá.

Note-se que a cor difusa dos materiais não funcionará se certas partes da cena não tiverem sua própria cor difusa do material.

Criando arestas



Criar arestas é semelhante ao uso de filtros de reconhecimento de arestas no GIMP .

Todos os cálculos para esta técnica de sombreamento são realizados em um sombreador de fragmento. Para criar contornos para o sombreador de vértice, basta passar quatro vértices da malha retangular para a saída para caber na tela.

 // ... uniform sampler2D materialDiffuseTexture; // ... vec2 texSize = textureSize(materialDiffuseTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy; // ... 

Antes de começar a reconhecer as bordas, é necessário preparar a textura recebida com a qual trabalharemos. Como a textura possui um tamanho de tela, podemos calcular as coordenadas UV, conhecendo as coordenadas do fragmento e o tamanho da textura recebida.

  // ... int separation = 1; // ... 

separation pode ser personalizada para se adequar ao seu gosto. Quanto maior a separação, mais grossas as bordas ou linhas.

  // ... float threshold = 0; // ... vec4 mx = vec4(0); vec4 mn = vec4(1); int x = -1; int y = -1; for (int i = 0; i < 9; ++i) { vec4 color = texture ( materialDiffuseTexture , (texCoord + (vec2(x, y) * separation)) / texSize ); mx = max(color, mx); mn = min(color, mn); x += 1; if (x >= 2) { x = -1; y += 1; } } float alpha = ((mx.r + mx.g + mx.b) / 3) - ((mn.r + mn.g + mn.b) / 3); if (alpha > threshold) { alpha = 1; } // ... 


A técnica de reconhecimento de bordas encontra alterações nas cores da textura recebida. Focalizando o fragmento atual, ele usa a janela de fragmentos 3x3 para encontrar as cores mais brilhantes e mais escuras das nove amostras. Então ela subtrai do brilho de uma cor o brilho de outra, obtendo a diferença.

  // ... vec3 lineRgb = vec3(0.012, 0.014, 0.022); // ... vec4 lineColor = vec4(lineRgb, alpha); // ... fragColor = lineColor; // ... 

Essa diferença é usada no canal alfa da cor de saída. Se não houver diferença, a aresta ou a linha não será desenhada. Se houver uma diferença, a aresta é desenhada.

  // ... float threshold = 0; // ... if (alpha > threshold) { alpha = 1; } // ... 

Tente experimentar o valor limite. Agora é zero. Qualquer valor diferente de zero se torna uma aresta; esse limite pode ser alterado. Isso é especialmente útil para texturas de entrada mais ruidosas com pequenas diferenças. No caso de uma textura barulhenta, geralmente é necessário criar contornos apenas para grandes diferenças.

Código fonte



Nevoeiro



O nevoeiro (ou névoa, como é chamado no Blender) adiciona neblina atmosférica à cena, criando misteriosas partes salientes e amolecidas. As partes salientes aparecem quando alguma geometria cai subitamente na pirâmide de visibilidade da câmera.

 // ... uniform struct p3d_FogParameters { vec4 color ; float start ; float end ; } p3d_Fog; // ... 

O Panda3D possui uma estrutura de dados conveniente que contém todos os parâmetros de neblina, mas você pode transferi-los para o shader manualmente.

  // ... float fogIntensity = clamp ( ( p3d_Fog.end - vertexPosition.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; // ... 

No exemplo de código, um modelo linear é usado para calcular o brilho da neblina ao se afastar da câmera. Em vez disso, você pode usar o modelo exponencial. O brilho do nevoeiro é zero antes ou no início do nevoeiro. Quando a posição do vértice se aproxima do final do nevoeiro, fogIntensity aproxima da unidade. Para todos os vértices após o final do nevoeiro, o fogIntensity limitado a 1 a partir de cima.

  // ... fragColor = mix ( outputColor , p3d_Fog.color , fogIntensity ); // ... 

Com base no brilho do nevoeiro, misturamos a cor do nevoeiro com a cor de saída. À medida que o fogIntensity aproxima da unidade, haverá cada vez menos outputColor e mais e mais cores de nevoeiro. Quando o fogIntensity atinge a unidade, apenas a cor do nevoeiro permanece.

Nevoeiro nos contornos




 // ... uniform sampler2D positionTexture; // ... vec4 position = texture(positionTexture, texCoord / texSize); float fogIntensity = clamp ( ( p3d_Fog.end - position.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; vec4 lineWithFogColor = mix ( lineColor , p3d_Fog.color , fogIntensity ); fragColor = vec4(lineWithFogColor.rgb, alpha); // ... 

O Path Shader aplica neblina às cores das bordas para uma imagem mais holística. Se ele não fizesse isso, a geometria dos contornos seria obscurecida pelo nevoeiro, o que pareceria estranho. No entanto, ele ainda cria contornos nas arestas mais externas da geometria do palco com o moinho, porque as arestas vão além da geometria - para onde não há posições de vértices.

positionTexture é uma textura de buffer de quadro que contém as posições dos vértices do espaço da vista. Você aprenderá sobre isso quando implementarmos o shader SSAO.

Código fonte



Bloom



Adicionar bloom à cena pode criar uma ilusão convincente do modelo de iluminação. Objetos emissores de luz se tornam mais convincentes e os reflexos da luz recebem uma quantidade adicional de brilho.

  //... float separation = 3; int samples = 15; float threshold = 0.5; float amount = 1; // ... 

Você pode personalizar essas configurações ao seu gosto. A separação aumenta o tamanho do desfoque. Amostras determina a força do desfoque. O limite determina o que será e o que não será afetado por esse efeito. Quantidade controla a quantidade de saída de bloom.

  // ... int size = samples; int size2 = size * size; int x = 0; int y = 0; // ... float value = 0; vec4 result = vec4(0); vec4 color = vec4(0); // ... for (int i = 0; i < size2; ++i) { // ... } // ... 

Essa técnica começa passando samples tamanho de janela para samples centralizadas em relação ao fragmento atual. Parece uma janela usada para criar caminhos.

  // ... color = texture ( bloomTexture , ( gl_FragCoord.xy + vec2(x * separation, y * separation) ) / texSize ); value = ((0.3 * color.r) + (0.59 * color.g) + (0.11 * color.b)); if (value < threshold) { color = vec4(0); } result += color; // ... 

Esse código obtém a cor da textura recebida e transforma os valores de vermelho, verde e azul em um valor em escala de cinza. Se o valor em escala de cinza for menor que o limite, ele descartará essa cor, tornando-a preta.

Passando por todas as amostras dentro da janela, ele acumula todos os seus valores no result .

  // ... result = result / size2; // ... 

Depois de concluir a coleta de amostras, ele divide a soma das amostras de cores pelo número de amostras coletadas. O resultado é a cor do meio do próprio fragmento e de seus vizinhos. Ao fazer isso para cada fragmento, obtemos uma imagem borrada. Esse tipo de desfoque é chamado de desfoque de caixa.


Aqui você vê o processo de execução do algoritmo bloom.

Código fonte



Oclusão ambiental do espaço da tela (SSAO)



O SSAO é um desses efeitos que você não sabe que existem, mas assim que você sabe que não pode mais viver sem eles. Ele pode transformar uma cena medíocre em uma cena incrível! Em cenas estáticas, a oclusão do ambiente pode ser inserida na textura, mas para cenas mais dinâmicas precisamos de um sombreador. O SSAO é uma das técnicas de sombreamento mais sofisticadas, mas depois que você descobrir isso, você se tornará um sombreador principal.

Observe que o termo “espaço na tela” no título não está totalmente correto, porque nem todos os cálculos são realizados no espaço na tela.

Dados recebidos


O shader SSAO precisará da seguinte entrada.

  • Vetores de posições de vértices no espaço de visualização.
  • Vetores normais para os vértices no espaço de visualização.
  • Vetores de amostra no espaço tangente.
  • Vetores de ruído no espaço tangente.
  • A matriz de projeção na lente da câmera.

Posição



Não é necessário armazenar posições de vértice na textura do buffer do quadro. Podemos recriá-los a partir do buffer de profundidade da câmera . Estou escrevendo um guia para iniciantes, portanto, não usaremos essa otimização e começaremos os negócios imediatamente. Na sua implementação, você pode facilmente usar o buffer de profundidade.

 PT(Texture) depthTexture = new Texture("depthTexture"); depthTexture->set_format(Texture::Format::F_depth_component32); PT(GraphicsOutput) depthBuffer = graphicsOutput->make_texture_buffer("depthBuffer", 0, 0, depthTexture); depthBuffer->set_clear_color(LVecBase4f(0, 0, 0, 0)); NodePath depthCameraNP = window->make_camera(); DCAST(Camera, depthCameraNP.node())->set_lens(window->get_camera(0)->get_lens()); PT(DisplayRegion) depthBufferRegion = depthBuffer->make_display_region(0, 1, 0, 1); depthBufferRegion->set_camera(depthCameraNP); 

Se você decidir usar o buffer de profundidade, eis como você pode configurá-lo no Panda3D.

 in vec4 vertexPosition; out vec4 fragColor; void main() { fragColor = vertexPosition; } 

Aqui está um sombreador simples para renderizar as posições dos vértices no espaço de visualização em uma textura de buffer de quadro. Uma tarefa mais difícil é ajustar a textura do buffer de quadros para que os componentes do vetor de fragmento obtidos por ele não sejam limitados ao intervalo [0, 1] e que cada um tenha uma precisão suficientemente alta (um número suficientemente grande de bits). Por exemplo, se algum tipo de posição de vértice interpolado for <-139.444444566, 0.00000034343, 2.5> , não será possível salvá-lo na textura como <0.0, 0.0, 1.0> .

  // ... FrameBufferProperties fbp = FrameBufferProperties::get_default(); // ... fbp.set_rgba_bits(32, 32, 32, 32); fbp.set_rgb_color(true); fbp.set_float_color(true); // ... 

Aqui está um código de exemplo que prepara uma textura de buffer de quadro para armazenar posições de vértice. Ele precisa de 32 bits para vermelho, verde, azul e alfa, então desabilita a restrição de valores pelo intervalo [0, 1] . A chamada para set_rgba_bits(32, 32, 32, 32) define o volume de bits e desativa a restrição.

  glTexImage2D ( GL_TEXTURE_2D , 0 , GL_RGB32F , 1200 , 900 , 0 , GL_RGB , GL_FLOAT , nullptr ); 

Aqui está uma ligação semelhante no OpenGL. GL_RGB32F define os bits e desativa a restrição.

Se o buffer de cores tiver uma vírgula fixa, os componentes dos valores inicial e final, bem como os índices de mistura, antes de calcular a equação de mistura, serão limitados a [0, 1] ou [-1, 1], respectivamente, para os buffers de cores normalizados e com sinalização normalizados e assinados. Se o buffer de cores tiver um ponto flutuante, a restrição não será atendida.

Fonte


Aqui você vê as posições dos vértices; o eixo y está para cima.

Lembre-se que o Panda3D define o eixo z como um vetor apontando para cima, enquanto no OpenGL o eixo y olha para cima. O shader de posição exibe as posições dos vértices com um z para cima, porque no Panda3D
o parâmetro gl-coordinate-system default está configurado.

Normal



Para a orientação correta das amostras obtidas no shader SSAO, precisamos dos normais para os vértices. O código de amostra gera vários vetores de amostragem distribuídos no hemisfério, mas você pode usar a esfera e resolver completamente o problema da necessidade de normais.

 in vec3 vertexNormal; out vec4 fragColor; void main() { vec3 normal = normalize(vertexNormal); fragColor = vec4(normal, 1); } 

Como o shader de posição, o shader normal é muito simples. Lembre-se de normalizar os normais para os vértices e lembre-se de que eles estão no espaço de visualização.


Os normais para os vértices são mostrados aqui; o eixo y está para cima.

Lembre-se de que o Panda3D considera o eixo z como o vetor ascendente e o OpenGL como o eixo y. O sombreador normal exibe as posições dos vértices com o eixo z apontando para cima, porque o gl-coordinate-system default configurado no Panda3D.

Amostras


Para determinar o valor da oclusão do ambiente para qualquer fragmento, precisamos amostrar a área circundante.

  // ... for (int i = 0; i < numberOfSamples; ++i) { LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) ).normalized(); float rand = randomFloats(generator); sample[0] *= rand; sample[1] *= rand; sample[2] *= rand; float scale = (float) i / (float) numberOfSamples; scale = lerp(0.1, 1.0, scale * scale); sample[0] *= scale; sample[1] *= scale; sample[2] *= scale; ssaoSamples.push_back(sample); } // ... 

O código de amostra gera 64 amostras aleatórias distribuídas em um hemisfério. Essas ssaoSamples serão passadas para o shader SSAO.

  LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 ).normalized(); 

Se você deseja distribuir suas amostras por uma esfera, altere o intervalo do componente aleatório z para que ele mude de menos um para um.

O barulho


  // ... for (int i = 0; i < 16; ++i) { LVecBase3f noise = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , 0.0 ); ssaoNoise.push_back(noise); } // ... 

Para cobrir bem a área amostrada, precisamos gerar vetores de ruído. Esses vetores de ruído podem girar amostras ao redor da parte superior da superfície.

Oclusão ambiental



O SSAO realiza sua tarefa amostrando o espaço de visualização ao redor do fragmento. Quanto mais amostras abaixo da superfície, mais escura a cor do fragmento. Essas amostras estão localizadas no fragmento e indicam na direção geral do normal ao vértice. Cada amostra é usada para procurar uma posição na textura da posição do buffer do quadro. A posição retornada é comparada com a amostra. Se a amostra estiver mais distante da câmera do que a posição, a amostra em direção ao fragmento será ocluída.


Aqui você vê o espaço acima da superfície amostrada para oclusão.

  // ... float radius = 1.1; float bias = 0.026; float lowerRange = -2; float upperRange = 2; // ... 

Como algumas outras técnicas, o shader SSAO possui vários parâmetros de controle que podem ser alterados para obter a aparência desejada. o viés é adicionado à distância da amostra para a câmera. Este parâmetro pode ser usado para combater manchas. raio aumenta ou diminui a área de cobertura do espaço da amostra. lowerRange e upperRange alteram o intervalo padrão da métrica do fator de [0, 1] para qualquer valor selecionado. Ao aumentar o alcance, você pode aumentar o contraste.

  // ... vec4 position = texture(positionTexture, texCoord); vec3 normal = texture(normalTexture, texCoord).xyz; int noiseX = int(gl_FragCoord.x - 0.5) % 4; int noiseY = int(gl_FragCoord.y - 0.5) % 4; vec3 random = noise[noiseX + (noiseY * 4)]; // ... 

Obtemos a posição, vetor normal e aleatório para uso posterior. Lembre-se de que no exemplo de código, 16 vetores aleatórios foram criados. Um vetor aleatório é selecionado com base na posição da tela dos fragmentos atuais.

  // ... vec3 tangent = normalize(random - normal * dot(random, normal)); vec3 binormal = cross(normal, tangent); mat3 tbn = mat3(tangent, binormal, normal); // ... 

Usando um vetor aleatório e um vetor normal, coletamos a matriz da tangente, binormal e normal. Precisamos dessa matriz para transformar os vetores de amostra do espaço tangente para o espaço de pesquisa.

  // ... float occlusion = NUM_SAMPLES; for (int i = 0; i < NUM_SAMPLES; ++i) { // ... } // ... 

Tendo uma matriz, o shader pode fazer um loop através de todas as amostras no loop, subtraindo o número de amostras fechadas.

  // ... vec3 sample = tbn * samples[i]; sample = position.xyz + sample * radius; // ... 

Usando a matriz, coloque a amostra ao lado da posição do vértice / fragmento e dimensione-a pelo raio.

  // ... vec4 offset = vec4(sample, 1.0); offset = lensProjection * offset; offset.xyz /= offset.w; offset.xyz = offset.xyz * 0.5 + 0.5; // ... 

Usando a posição da amostra no espaço de visualização, nós a transformamos do espaço de visualização para o espaço de recorte e depois para o espaço UV.

 -1 * 0.5 + 0.5 = 0 1 * 0.5 + 0.5 = 1 

Não se esqueça de que os componentes do espaço de recorte estão no intervalo de menos um a um e as coordenadas UV estão no intervalo de zero a um. Para converter as coordenadas do espaço de recorte em coordenadas UV, multiplique-as por um segundo e adicione um segundo.

  // ... vec4 offsetPosition = texture(positionTexture, offset.xy); float occluded = 0; if (sample.y + bias <= offsetPosition.y) { occluded = 0; } else { occluded = 1; } // ... 

Usando as coordenadas de deslocamento UV obtidas projetando a amostra 3D na textura de posição 2D, encontramos o vetor de posição correspondente. Isso nos leva do espaço de visualização para o espaço de recorte para o espaço UV e depois volta para o espaço de visualização. O sombreador executa esse loop para determinar se existe alguma geometria atrás da amostra, no local da amostra ou na frente da amostra. Se a amostra estiver localizada na frente ou em alguma geometria, ela simplesmente não será levada em consideração em relação ao fragmento sobreposto. Se a amostra estiver atrás de alguma geometria, ela será levada em consideração em relação ao fragmento sobreposto.

  // ... float intensity = smoothstep ( 0.0 , 1.0 , radius / abs(position.y - offsetPosition.y) ); occluded *= intensity; occlusion -= occluded; // ... 

Agora, adicione peso a essa posição de amostra com base em quão longe está dentro ou fora do raio. Em seguida, subtraia essa amostra da métrica de oclusão, pois pressupõe que todas as amostras foram sobrepostas antes do loop.

  // ... occlusion /= NUM_SAMPLES; // ... fragColor = vec4(vec3(occlusion), position.a); // ... 

Divida o número de sobreposições pelo número de amostras para converter o indicador de oclusão do intervalo [0, NUM_SAMPLES] no intervalo [0, 1] . Zero significa oclusão completa, unidades não significam oclusão. Agora atribua oclusão à cor do fragmento, e é isso.

Observe que no código de exemplo, o canal alfa recebe o valor alfa da textura da posição no buffer do quadro para evitar sobreposição de fundo.

Desfocar



A textura do buffer de quadro SSAO é um pouco barulhenta, então você deve desfocá-lo para suavizar.

  // ... for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( ssaoTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ).rgb; xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

O shader de desfoque do SSAO é um desfoque de caixa comum. Como o shader de floração, ele desenha uma janela sobre a textura recebida e calcula a média de cada fragmento com os valores de seus vizinhos.

Observe que o parameters.x é um parâmetro de separação.

Cor ambiente


  // ... vec2 ssaoBlurTexSize = textureSize(ssaoBlurTexture, 0).xy; vec2 ssaoBlurTexCoord = gl_FragCoord.xy / ssaoBlurTexSize; float ssao = texture(ssaoBlurTexture, ssaoBlurTexCoord).r; vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex * ssao; // ... 

O desafio final para o SSAO está novamente nos cálculos de iluminação. Aqui vemos como a oclusão é encontrada no buffer de textura SSAO e é incluída no cálculo da luz ambiente.

Código fonte



Profundidade de campo



A profundidade de campo também é esse efeito, tendo aprendido sobre o que, você não pode viver sem ele. Do ponto de vista artístico, você pode usá-lo para atrair a atenção do visualizador para um objeto específico. Mas, no caso geral, a profundidade de campo, à custa de um pequeno esforço, acrescenta uma grande parcela de realismo.

Em foco


O primeiro passo é renderizar a cena completamente em foco. Renderize-o na textura do buffer do quadro. Será um dos valores de entrada para a profundidade do buffer do campo.

Fora de foco


  // ... vec4 result = vec4(0); for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( blurTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ); xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

O segundo passo é desfocar a cena como se estivesse completamente fora de foco. Assim como no bloom e no SSAO, você pode usar o desfoque de caixa. Renderize esta cena desfocada para a textura do buffer do quadro. Será outro valor de entrada para a profundidade do sombreador de campo.

Observe que o parameters.x é um parâmetro de separação.

Confusão




  // ... float focalLengthSharpness = 100; float blurRate = 6; // ... 

Você pode personalizar essas opções ao seu gosto. focalLengthSharpness afeta a focalLengthSharpness da cena na distância focal. Quanto menor a focalLengthSharpness , mais desfocada será a distância focal. blurRate afeta a velocidade de desfoque da cena ao se afastar da distância focal. Quanto menor o blurRate , menos borrada será a cena ao blurRate afastar do ponto de foco.

  // ... vec4 focusColor = texture(focusTexture, texCoord); vec4 outOfFocusColor = texture(outOfFocusTexture, texCoord); // ... 

Vamos precisar de cores em foco e em uma imagem desfocada.

  // ... vec4 position = texture(positionTexture, texCoord); // ... 

Também podemos precisar da posição do vértice no espaço de visualização. Você pode reaplicar a textura das posições do buffer de quadro usado para o SSAO.

  // ... float blur = clamp ( pow ( blurRate , abs(position.y - focalLength.x) ) / focalLengthSharpness , 0 , 1 ); // ... fragColor = mix(focusColor, outOfFocusColor, blur); // ... 

E aqui a confusão acontece. Quanto mais próximo blurde um, mais ele usará outOfFocusColor. Um valor zero blursignifica que esse fragmento está totalmente em foco. Com blur >= 1este fragmento é completamente desfocado.

Código fonte



Posterização



Posterização, ou amostragem de cores, é o processo de redução do número de cores exclusivas em uma imagem. Você pode usar esse shader para dar ao jogo uma aparência cômica ou retrô. Se você combiná-lo com um esboço, você obtém um estilo de desenho animado real.

  // ... float levels = 8; // ... 

Você pode experimentar com este parâmetro. Quanto maior, mais flores permanecerão como resultado.

  // ... vec4 texColor = texture(posterizeTexture, texCoord); // ... 

Vamos precisar da cor recebida.

  // ... vec3 grey = vec3((texColor.r + texColor.g + texColor.b) / 3.0); vec3 grey1 = grey; grey = floor(grey * levels) / levels; texColor.rgb += (grey - grey1); // ... 

Eu não vi esse método de posterização. Depois de verificá-lo, vi que ele cria resultados mais bonitos em comparação aos métodos convencionais. Para reduzir a paleta de cores, primeiro converta a cor em um valor em escala de cinza. Nós discretizamos a cor, amarrando-a a um dos níveis. Calculamos a diferença entre o valor discretizado em escala de cinza e o valor não discretizado em escala de cinza. Adicione essa diferença à cor de entrada. Essa diferença é a quantidade pela qual a cor deve aumentar / diminuir para atingir um valor discreto em escala de cinza.

  // ... fragColor = texColor; // ... 

Não se esqueça de atribuir o valor da cor de entrada à cor do fragmento.

Cel shading



A posterização pode fazer a imagem parecer um sombreamento de cel, porque o processo de discretizar cores difusas e difusas em tons discretos. Queremos usar apenas cores difusas sólidas sem detalhes finos do mapa normal e um valor pequeno levels.

Código fonte



Pixelização



A pixelização de um jogo em 3D pode dar uma aparência interessante ou poupar tempo que levaria para criar toda a arte de pixel manualmente. Combine-o com a posterização para criar uma verdadeira aparência retrô.

  // ... int pixelSize = 5; // ... 

Você pode ajustar o tamanho do pixel. Quanto maior, mais rugosa será a imagem.



  // ... float x = int(gl_FragCoord.x) % pixelSize; float y = int(gl_FragCoord.y) % pixelSize; x = floor(pixelSize / 2.0) - x; y = floor(pixelSize / 2.0) - y; x = gl_FragCoord.x + x; y = gl_FragCoord.y + y; // ... 

Essa técnica anexa cada fragmento ao centro da janela em tamanho de pixel não sobreposta mais próxima. Essas janelas se alinham sobre a textura recebida. Fragmentos no centro da janela determinam a cor de outros fragmentos na janela.

  // ... fragColor = texture(pixelizeTexture, vec2(x, y) / texSize); // ... 

Depois de determinarmos a coordenada do fragmento desejado a ser usado, retire sua cor da textura recebida e atribua-a à cor do fragmento.

Código fonte



Afiar



O efeito de nitidez (nitidez) aumenta o contraste nas bordas da imagem. É útil quando os gráficos acabam sendo muito suaves.

  // ... float amount = 0.8; // ... 

Alterando o valor, podemos controlar a magnitude da nitidez do resultado. Se o valor for zero, a imagem não será alterada. Com valores negativos, a imagem começa a parecer estranha.

  // ... float neighbor = amount * -1; float center = amount * 4 + 1; // ... 

Fragmentos adjacentes são multiplicados por amount * -1. O fragmento atual é multiplicado por amount * 4 + 1.

  // ... vec3 color = texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 1) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x - 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 0) / texSize).rgb * center + texture(sharpenTexture, vec2(gl_FragCoord.x + 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y - 1) / texSize).rgb * neighbor ; // ... 

Os fragmentos vizinhos estão na parte superior, inferior, esquerda e direita. Depois de multiplicar os vizinhos e o fragmento atual por seus valores, o resultado é somado.

  // ... fragColor = vec4(color, texture(sharpenTexture, texCoord).a); // ... 

Essa quantidade é a cor final do fragmento.

Código fonte



Grão de filme



A granulação do filme (em pequenas doses, e não como no exemplo) pode adicionar realismo, que é invisível até que esse efeito seja removido. Geralmente, essas são as imperfeições que tornam a imagem gerada digitalmente mais convincente.

Observe que a granulação do filme geralmente é o último efeito aplicado ao quadro antes de ser exibido.

Valor


  // ... float amount = 0.1; // ... 

amountcontrola a visibilidade do grão do filme. Quanto maior o valor, mais "neve" na imagem.

Brilho aleatório


 // ... uniform float osg_FrameTime; //... float toRadians = 3.14 / 180; //... float randomIntensity = fract ( 10000 * sin ( ( gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime ) * toRadians ) ); // ... 

Este trecho de código calcula o brilho aleatório necessário para ajustar o valor.

 Time Since F1 = 00 01 02 03 04 05 06 07 08 09 10 Frame Number = F1 F3 F4 F5 F6 osg_FrameTime = 00 02 04 07 08 

Valor osg_FrameTime fornecido pelo Panda3D. Um tempo de quadro é um registro de data e hora com informações sobre quantos segundos se passaram desde o primeiro quadro. O código de amostra usa-o para animar a granulação do filme, que osg_FrameTimeserá diferente em cada quadro.

  // ... ( gl_FragCoord.x + gl_FragCoord.y * 8009 // Large number here. // ... 

Para grãos estáticos, os filmes devem ser substituídos por um osg_FrameTimegrande número. Para evitar a visualização de padrões, você pode tentar números diferentes.



  // ... * sin ( ( gl_FragCoord.x + gl_FragCoord.y * someNumber // ... 

Para criar pontos ou pontos de granulação de filme, são utilizadas as duas coordenadas ex, ey. Se você usar x, somente as linhas verticais serão exibidas; se você usar y, somente as linhas horizontais serão exibidas.

No código, uma coordenada é multiplicada por outra para destruir a simetria diagonal.


Obviamente, você pode se livrar do multiplicador de coordenadas e obter um efeito de chuva completamente aceitável.

Observe que, para animar o efeito da chuva, multiplique a saída sinpor osg_FrameTime.

Experimente as coordenadas xey para mudar a direção da chuva. Para um banho descendente, deixe apenas a coordenada x.

 input = (gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime) * toRadians frame(10000 * sin(input)) = fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = 

sinusado como uma função hash. As coordenadas do fragmento são hash com valores de saída sin. Graças a isso, aparece uma propriedade conveniente - quaisquer que sejam os dados de entrada (grandes ou pequenos), o intervalo de saída estará na faixa de menos um a um.

 fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = fract(6400.723818964882) = 0.723818964882 

sinem combinação com fracttambém usado como gerador de números pseudo-aleatórios.

 >>> [floor(fract(4 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 0, 1, 2, 2, 3, 4, 4, 5, 6] >>> [floor(fract(10000 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 4, 8, 0, 2, 1, 7, 0, 0, 5] 

Primeiro, olhe para a primeira linha de números e depois para a segunda. Cada linha é determinística, mas o padrão é menos perceptível na segunda do que na segunda. Portanto, apesar do resultado ser fract(10000 * sin(...))determinístico, o padrão é reconhecido muito mais fraco.


Aqui vemos como o fator siné primeiro 1, depois 10, depois 100 e depois 1000.

À medida que o multiplicador dos valores de saída aumenta, o sinpadrão se torna menos perceptível. Por esse motivo, o código siné multiplicado por 10.000.

Cor do fragmento


  // ... vec2 texSize = textureSize(filmGrainTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 color = texture(filmGrainTexture, texCoord); // ... 

Converta as coordenadas do fragmento em coordenadas UV. Usando essas coordenadas UV, procuramos a cor da textura do fragmento atual.

  // ... amount *= randomIntensity; color.rgb += amount; // ... 

Altere o valor para um brilho aleatório e adicione-o à cor.

  // ... fragColor = color; // ... 

Defina a cor do fragmento, e é isso.

Código fonte



Agradecimentos


Source: https://habr.com/ru/post/pt453692/


All Articles