¿Cómo renderizo el flujo de agua dirigido en mosaico de arriba hacia abajo en 2D?

9

Estoy trabajando en un juego 2D bastante gráfico basado en mosaicos de arriba hacia abajo inspirado en Dwarf Fortress. Estoy a punto de implementar un río en el mundo del juego, que cubre varias fichas, y he calculado la dirección del flujo para cada ficha, como se muestra a continuación por la línea roja en cada ficha.

Ejemplo de azulejos de río con indicaciones

Como referencia del estilo gráfico, así es como se ve mi juego actualmente:

Disparo en el juego de estilo gráfico

Lo que necesito es alguna técnica para animar el agua que fluye en cada una de las baldosas del río, de modo que el flujo se mezcle con las baldosas circundantes para que los bordes de las baldosas no sean evidentes.

El ejemplo más cercano que he encontrado a lo que estoy buscando se describe en http://www.rug.nl/society-business/centre-for-information-technology/research/hpcv/publications/watershader/ pero no estoy del todo a punto de poder entender lo que está sucediendo en él? Tengo suficiente conocimiento de la programación de sombreadores para implementar mi propia iluminación dinámica, pero no puedo entender el enfoque adoptado en el artículo vinculado.

¿Podría alguien explicar cómo se logra el efecto anterior o sugerir otro enfoque para obtener el resultado que quiero? Creo que parte de la solución anterior es superponer los mosaicos (aunque no estoy seguro de en qué combinaciones) y rotar el mapa normal utilizado para la distorsión (una vez más, no tengo idea de los detalles) y más allá de que estoy un poco perdido, gracias por ¡alguna ayuda!

Ross Taylor-Turner
fuente
¿Tienes un objetivo visual para el agua en sí? Me doy cuenta de que el enlace que usted cita está usando mapas normales para la reflexión especular, algo que podría no combinar bien en la dirección de arte plana / de estilo de dibujos animados que ha mostrado. Hay formas de adaptar la técnica a otros estilos, pero necesitamos algunas pautas para saber a qué apuntar.
DMGregory
Puede usar su solución de flujo como un gradiente para las partículas que suelta en la corriente. Sin embargo, probablemente sea costoso, ya que necesitarías muchos de ellos.
Bram
No resolvería esto con un sombreador, lo haría de la manera simple que se usó durante siglos, solo dibujándolo y tengo como 8 dibujos diferentes del agua y también 8 dibujos diferentes del agua que golpea la orilla. Luego, agregue una superposición de color si desea tener un terreno diferente y agregue aleatoriamente, como rocía piedras, peces o lo que sea en el río. Por cierto, con 8 diferentes, quería decir que cada 45 grados en rotación tuviera un sprite diferente
Yosh Synergi
@YoshSynergi Quiero que el flujo del río sea en cualquier dirección en lugar de 8 direcciones, y quiero evitar tener límites visibles entre los bordes de las baldosas, similar al resultado logrado en el sombreador vinculado
Ross Taylor-Turner
@Bram es una opción que estoy considerando que podría lograr, pero también creo que necesitará demasiadas partículas para ser efectiva, especialmente cuando la cámara se aleja mucho
Ross Taylor-Turner

Respuestas:

11

No tenía ningún mosaico a mano que se viera bien con distorsión, así que aquí hay una versión del efecto que me burlé con estos mosaicos de Kenney :

Animación que muestra el flujo de agua en tilemap.

Estoy usando un mapa de flujo como este, donde rojo = flujo hacia la derecha y verde = hacia arriba, siendo el amarillo ambos. Cada píxel corresponde a un mosaico, y el píxel inferior izquierdo es el mosaico en (0, 0) en mi sistema de coordenadas mundial.

8x8

Y una textura de patrón de onda como esta:

ingrese la descripción de la imagen aquí

Estoy más familiarizado con la sintaxis de estilo hlsl / CG de Unity, por lo que deberá adaptar este sombreador un poco para su contexto glsl, pero debería ser sencillo.

// 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
fuente