Como renderizo o fluxo de água direcionado de cima para baixo em 2D?

9

Estou trabalhando em um jogo 2D gráfico bastante gráfico de cima para baixo inspirado em Dwarf Fortress. Estou no ponto de implementar um rio no mundo do jogo, que abrange um número de peças, e calculei a direção do fluxo de cada peça, como mostrado abaixo pela linha vermelha em cada peça.

Exemplo de ladrilhos de rio com direções

Para referência ao estilo gráfico, veja como está o meu jogo atualmente:

Tiro no jogo de estilo gráfico

O que eu preciso é de alguma técnica para animar a água que flui em cada um dos ladrilhos do rio, para que o fluxo se misture aos ladrilhos circundantes, para que as bordas dos ladrilhos não sejam aparentes.

O exemplo mais próximo que encontrei do que estou procurando é descrito em http://www.rug.nl/society-business/centre-for-information-technology/research/hpcv/publications/watershader/, mas não sou bem assim a ponto de ser capaz de entender o que está acontecendo nele? Eu tenho uma compreensão suficiente da programação de shader para implementar minha própria iluminação dinâmica, mas não consigo entender completamente a abordagem adotada no artigo vinculado.

Alguém poderia explicar como o efeito acima é alcançado ou sugerir outra abordagem para obter o resultado desejado? Eu acho que parte da solução acima é sobrepor os blocos (embora eu não tenha certeza de quais combinações) e girar o mapa normal usado para a distorção (mais uma vez, nenhuma idéia sobre detalhes) e passado que estou um pouco perdido, obrigado por qualquer ajuda!

Ross Taylor-Turner
fonte
Você tem um alvo visual para a própria água? Percebo que o link que você cita está usando mapas normais para reflexão especular - algo que pode não se encaixar perfeitamente na direção de arte plana / estilo cartoon que você mostrou. Existem maneiras de adaptar a técnica a outros estilos, mas precisamos de algumas diretrizes para sabermos o que procurar.
DMGregory
Você pode usar sua solução de fluxo como um gradiente para partículas que você solta no fluxo. Provavelmente caro, pois você precisaria de muitos deles.
Bram2
Eu não resolveria isso com um sombreador, faria da maneira mais simples que foi usada ao longo dos séculos, apenas desenhei e tinha 8 desenhos diferentes da água e também 8 desenhos diferentes da água atingindo a costa. Em seguida, adicione uma sobreposição de cores se quiser ter um terreno diferente e adicione aleatoriamente, como granulado, pedras, peixes ou qualquer outra coisa no rio. Btw com 8 I diferentes significados para cada 45 graus na rotação de ter um sprite diferente
Yosh Synergi
@YoshSynergi eu quero o fluxo do rio para ser em qualquer direção, em vez de 8 direções, e eu quero evitar ter limites visíveis entre as bordas da telha, semelhante ao resultado alcançado no shader ligada
Ross Taylor-Turner
@Bram que é uma opção Estou pensando que eu poderia alcançar, mas também acho que ele vai precisar de muitas partículas para ser eficaz, em particular quando a câmera é ampliada para fora um monte
Ross Taylor-Turner

Respostas:

11

Eu não tinha nenhum bloco à mão que parecesse bom com distorção, então aqui está uma versão do efeito que eu zombei desses blocos de Kenney :

Animação mostrando água corrente no tilemap.

Estou usando um fluxograma como este, em que vermelho = fluxo para a direita e verde = para cima, sendo o amarelo ambos. Cada pixel corresponde a um bloco, com o pixel inferior esquerdo sendo o bloco em (0, 0) no meu sistema de coordenadas do mundo.

8x8

E uma textura de padrão de onda como esta:

insira a descrição da imagem aqui

Eu estou mais familiarizado com a sintaxe hlsl / CG do Unity, então você precisará adaptar esse shader um pouco para o seu contexto glsl, mas deve ser fácil de fazer.

// Colour texture / atlas for my tileset.
sampler2D _Tile;
// Flowmap texture.
sampler2D _Flow;
// Wave surface texture.
sampler2D _Wave;

// Tiling of the wave pattern texture.
float _WaveDensity = 0.5f;
// Scrolling speed for the wave flow.
float _WaveSpeed  = 5.0f;

// Scaling from my world size of 8x8 tiles 
// to the 0...1
float2 inverseFlowmapSize = (float2)(1.0f/8.0f);

struct v2f
{
    // Projected position of tile vertex.
    float4 vertex   : SV_POSITION;
    // Tint colour (not used in this effect, but handy to have.
    fixed4 color    : COLOR;
    // UV coordinates of the tile in the tile atlas.
    float2 texcoord : TEXCOORD0;
    // Worldspace coordinates, used to look up into the flow map.
    float2 flowPos  : TEXCOORD1;
};

v2f vert(appdata_t IN)
{
    v2f OUT;

    // Save xy world position into flow UV channel.
    OUT.flowPos = mul(ObjectToWorldMatrix, IN.vertex).xy;

    // Conventional projection & pass-throughs...
    OUT.vertex = mul(MVPMatrix, IN.vertex);
    OUT.texcoord = IN.texcoord;
    OUT.color = IN.color;

    return OUT;
}

// I use this function to sample the wave contribution
// from each of the 4 closest flow map pixels.
// uv = my uv in world space
// sample site = world space        
float2 WaveAmount(float2 uv, float2 sampleSite) {
    // Sample from the flow map texture without any mipmapping/filtering.
    // Convert to a vector in the -1...1 range.
    float2 flowVector = tex2Dgrad(_Flow, sampleSite * inverseFlowmapSize, 0, 0).xy 
                        * 2.0f - 1.0f;
    // Optionally, you can skip this step, and actually encode
    // a flow speed into the flow map texture too.
    // I just enforce a 1.0 length for consistency without getting fussy.
    flowVector = normalize(flowVector);

    // I displace the UVs a little for each sample, so that adjacent
    // tiles flowing the same direction don't repeat exactly.
    float2 waveUV = uv * _WaveDensity + sin((3.3f * sampleSite.xy + sampleSite.yx) * 1.0f);

    // Subtract the flow direction scaled by time
    // to make the wave pattern scroll this way.
    waveUV -= flowVector * _Time * _WaveSpeed;

    // I use tex2DGrad here to avoid mipping down
    // undesireably near tile boundaries.
    float wave = tex2Dgrad(_Wave, waveUV, 
                           ddx(uv) * _WaveDensity, ddy(uv) * _WaveDensity);

    // Calculate the squared distance of this flowmap pixel center
    // from our drawn position, and use it to fade the flow
    // influence smoothly toward 0 as we get further away.
    float2 offset = uv - sampleSite;
    float fade = 1.0 - saturate(dot(offset, offset));

    return float2(wave * fade, fade);
}

fixed4 Frag(v2f IN) : SV_Target
{
    // Sample the tilemap texture.
    fixed4 c = tex2D(_MainTex, IN.texcoord);

    // In my case, I just select the water areas based on
    // how blue they are. A more robust method would be
    // to encode this into an alpha mask or similar.
    float waveBlend = saturate(3.0f * (c.b - 0.4f));

    // Skip the water effect if we're not in water.
    if(waveBlend == 0.0f)
        return c * IN.color;

    float2 flowUV = IN.flowPos;
    // Clamp to the bottom-left flowmap pixel
    // that influences this location.
    float2 bottomLeft = floor(flowUV);

    // Sum up the wave contributions from the four
    // closest flow map pixels.     
    float2 wave = WaveAmount(flowUV, bottomLeft);
    wave += WaveAmount(flowUV, bottomLeft + float2(1, 0));
    wave += WaveAmount(flowUV, bottomLeft + float2(1, 1));
    wave += WaveAmount(flowUV, bottomLeft + float2(0, 1));

    // We store total influence in the y channel, 
    // so we can divide it out for a weighted average.
    wave.x /= wave.y;

    // Here I tint the "low" parts a darker blue.
    c = lerp(c, c*c + float4(0, 0, 0.05, 0), waveBlend * 0.5f * saturate(1.2f - 4.0f * wave.x));

    // Then brighten the peaks.
    c += waveBlend * saturate((wave.x - 0.4f) * 20.0f) * 0.1f;

    // And finally return the tinted colour.
    return c * IN.color;
}
DMGregory
fonte