Come si esegue il rendering del flusso d'acqua 2D piastrellato top-down diretto?


9

Sto lavorando a un gioco 2D abbastanza grafico basato su tessere top-down ispirato a Dwarf Fortress. Sono sul punto di implementare un fiume nel mondo di gioco, che copre un numero di tessere, e ho calcolato la direzione del flusso per ogni tessera, come mostrato di seguito dalla linea rossa in ogni tessera.

Esempio di tessere fiume con indicazioni

Per riferimento allo stile grafico, ecco come appare il mio gioco attualmente:

Scatto in-game di stile grafico

Ciò di cui ho bisogno è una tecnica per animare l'acqua che scorre in ciascuna delle piastrelle del fiume, in modo che il flusso si fonda con le piastrelle circostanti in modo che i bordi delle piastrelle non siano evidenti.

L'esempio più vicino che ho trovato a ciò che sto cercando è descritto su http://www.rug.nl/society-business/centre-for-information-technology/research/hpcv/publications/watershader/ ma non sono del tutto al punto di essere in grado di capire cosa sta succedendo in esso? Ho una comprensione sufficiente della programmazione dello shader per aver implementato la mia illuminazione dinamica, ma non riesco proprio a capire come affrontare l'articolo collegato.

Qualcuno potrebbe spiegare come si ottiene l'effetto sopra o suggerire un altro approccio per ottenere il risultato che desidero? Penso che parte della soluzione di cui sopra sia la sovrapposizione delle tessere (anche se non sono sicuro in quali combinazioni) e la rotazione della mappa normale utilizzata per la distorsione (di nuovo nessuna idea sui dettagli) e passato che mi sono un po 'perso, grazie per qualsiasi aiuto!


Hai un obiettivo visivo per l'acqua stessa? Ho notato che il link che citi sta usando le normali mappe per la riflessione speculare - qualcosa che potrebbe non fondersi perfettamente nella direzione artistica piatta / in stile cartone animato che hai mostrato. Esistono modi per adattare la tecnica ad altri stili, ma abbiamo bisogno di alcune linee guida per sapere a cosa puntare.
DMGregory

È possibile utilizzare la soluzione di flusso come gradiente per le particelle che si sono liberate nel flusso. Probabilmente costoso, dato che ne avresti bisogno in molti.
Bram,

Non lo risolverei con uno shader, lo farei nel modo semplice che è stato usato nel corso dei secoli, basta disegnarlo e avere come 8 diversi disegni dell'acqua e anche 8 diversi disegni dell'acqua che colpiscono la riva. Quindi aggiungi una sovrapposizione di colore se vuoi avere un terreno diverso e aggiungi casualmente a come spruzza pietre, pesci o altro nel fiume. A proposito con 8 differenti intendevo che ogni 45 gradi in rotazione avesse uno sprite diverso
Yosh Synergi,

@YoshSynergi Voglio che il flusso del fiume sia in qualsiasi direzione piuttosto che in 8 direzioni, e voglio evitare di avere confini visibili tra i bordi delle piastrelle, simile al risultato ottenuto nello shader collegato
Ross Taylor-Turner,

@Bram è un'opzione che sto valutando di poter raggiungere, ma penso anche che per essere efficace serviranno troppe particelle, in particolare quando la fotocamera viene ingrandita molto
Ross Taylor-Turner,

Risposte:


11

Non avevo nessuna tessera a portata di mano che sembrava buona con la distorsione, quindi ecco una versione dell'effetto che ho deriso con queste tessere Kenney :

Animazione che mostra l'acqua corrente in tilemap.

Sto usando una flowmap come questa, dove rosso = flusso verso destra e verde = verso l'alto, il giallo è entrambi. Ogni pixel corrisponde a una tessera, con il pixel in basso a sinistra che è la tessera su (0, 0) nel mio sistema di coordinate del mondo.

8x8

E una trama a onde come questa:

inserisci qui la descrizione dell'immagine

Conosco molto bene la sintassi in stile hlsl / CG di Unity, quindi dovrai adattare un po 'questo shader al tuo contesto glsl, ma dovrebbe essere semplice da fare.

// 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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.