Comment puis-je restituer un écoulement d'eau en mosaïque 2D dirigé de haut en bas?

9

Je travaille sur un jeu 2D assez graphique basé sur des tuiles de haut en bas inspiré de Dwarf Fortress. Je suis sur le point d'implémenter une rivière dans le monde du jeu, qui couvre un certain nombre de tuiles, et j'ai calculé la direction du flux pour chaque tuile, comme indiqué ci-dessous par la ligne rouge dans chaque tuile.

Exemple de tuiles de rivière avec directions

Pour référence du style graphique, voici à quoi ressemble actuellement mon jeu:

Prise de vue graphique dans le jeu

Ce dont j'ai besoin, c'est d'une technique pour animer l'eau qui coule dans chacune des tuiles de la rivière, de sorte que le flux se fond dans les tuiles environnantes afin que les bords des tuiles ne soient pas apparents.

L'exemple le plus proche que j'ai trouvé de ce que je cherche est décrit sur http://www.rug.nl/society-business/centre-for-information-technology/research/hpcv/publications/watershader/ mais je ne suis pas tout à fait au point de pouvoir comprendre ce qui s'y passe? J'ai suffisamment de compréhension de la programmation des shaders pour avoir implémenté mon propre éclairage dynamique, mais je ne peux pas vraiment comprendre l'approche adoptée dans l'article lié.

Quelqu'un pourrait-il expliquer comment l'effet ci-dessus est obtenu ou suggérer une autre approche pour obtenir le résultat souhaité? Je pense qu'une partie de la solution ci-dessus consiste à chevaucher les tuiles (bien que je ne sois pas sûr dans quelles combinaisons) et à faire tourner la carte normale utilisée pour la distorsion (encore une fois aucune idée sur les détails) et au-delà, je suis un peu perdu, merci pour de l'aide!

Ross Taylor-Turner
la source
Avez-vous une cible visuelle pour l'eau elle-même? Je remarque que le lien que vous citez utilise des cartes normales pour la réflexion spéculaire - quelque chose qui pourrait ne pas se fondre parfaitement dans la direction artistique plate / de style dessin animé que vous avez montrée. Il existe des moyens d'adapter la technique à d'autres styles, mais nous avons besoin de quelques lignes directrices pour savoir quoi viser.
DMGregory
Vous pouvez utiliser votre solution d'écoulement comme un gradient pour les particules que vous libérez dans le flux. Probablement cher cependant, car vous en auriez besoin de beaucoup.
Bram
Je ne résoudrais pas cela avec un shader, je le ferais de la manière simple qui a été utilisée au cours des siècles, il suffit de le dessiner et d'avoir comme 8 dessins différents de l'eau et aussi 8 dessins différents de l'eau frappant le rivage. Ensuite, ajoutez une superposition de couleurs si vous souhaitez avoir un terrain différent et ajoutez au hasard des paillettes, du poisson ou autre chose dans la rivière. Btw avec 8 différents que je voulais pour chaque 45 degrés de rotation pour avoir un sprite différent
Yosh Synergi
@YoshSynergi Je veux que le débit de la rivière soit dans n'importe quelle direction plutôt que dans 8 directions, et je veux éviter d'avoir des limites visibles entre les bords des carreaux, similaire au résultat obtenu dans le shader lié
Ross Taylor-Turner
@Bram qui est une option que je considère que je pourrais atteindre, mais pense également qu'il faudra trop de particules pour être efficace, en particulier lorsque la caméra est beaucoup dézoomée
Ross Taylor-Turner

Réponses:

11

Je n'avais pas de tuiles à portée de main qui avaient l'air bien avec la distorsion, alors voici une version de l'effet que j'ai simulé avec ces tuiles Kenney à la place:

Animation montrant l'eau qui coule sur le tilemap.

J'utilise un organigramme comme celui-ci, où rouge = flux vers la droite et vert = vers le haut, le jaune étant les deux. Chaque pixel correspond à une tuile, le pixel en bas à gauche étant la tuile à (0, 0) dans mon système de coordonnées universelles.

8x8

Et une texture de motif de vagues comme celle-ci:

entrez la description de l'image ici

Je connais très bien la syntaxe de style hlsl / CG d'Unity, vous devrez donc adapter ce shader un peu pour votre contexte glsl, mais cela devrait être simple à faire.

// 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
la source