Wie rendere ich einen von oben nach unten gekachelten 2D-Wasserfluss?

9

Ich arbeite an einem von oben nach unten gekachelten, ziemlich grafischen 2D-Spiel, das von Dwarf Fortress inspiriert ist. Ich bin gerade dabei, einen Fluss in der Spielwelt zu implementieren, der eine Reihe von Plättchen abdeckt, und ich habe die Flussrichtung für jedes Plättchen berechnet, wie unten durch die rote Linie in jedem Plättchen gezeigt.

Beispiel für Flusskacheln mit Anweisungen

Als Referenz für den Grafikstil sieht mein Spiel derzeit so aus:

In-Game-Aufnahme des grafischen Stils

Was ich brauche, ist eine Technik, um das in jedem der Flusskacheln fließende Wasser zu animieren, so dass der Fluss in die umgebenden Kacheln übergeht, sodass die Kachelkanten nicht sichtbar sind.

Das nächste Beispiel, das ich gefunden habe, ist das, was ich suche, unter http://www.rug.nl/society-business/centre-for-information-technology/research/hpcv/publications/watershader/ beschrieben, aber ich bin nicht ganz an dem Punkt zu verstehen, was darin vor sich geht? Ich habe genug Verständnis für Shader-Programmierung, um meine eigene dynamische Beleuchtung implementiert zu haben, aber ich kann mich nicht ganz mit dem Ansatz im verlinkten Artikel auseinandersetzen.

Könnte jemand erklären, wie der oben genannte Effekt erzielt wird, oder einen anderen Ansatz vorschlagen, um das gewünschte Ergebnis zu erzielen? Ich denke, ein Teil der obigen Lösung besteht darin, die Kacheln zu überlappen (obwohl ich nicht sicher bin, in welchen Kombinationen) und die normale Karte zu drehen, die für die Verzerrung verwendet wird (wieder keine Ahnung von Einzelheiten), und darüber hinaus bin ich ein bisschen verloren, danke für irgendeine Hilfe!

Ross Taylor-Turner
quelle
Haben Sie ein visuelles Ziel für das Wasser selbst? Ich stelle fest, dass der von Ihnen zitierte Link normale Karten für die Spiegelreflexion verwendet - etwas, das möglicherweise nicht ganz in die von Ihnen gezeigte flache / Cartoon-artige Kunstrichtung passt. Es gibt Möglichkeiten, die Technik an andere Stile anzupassen, aber wir benötigen einige Richtlinien, damit wir wissen, worauf wir abzielen müssen.
DMGregory
Sie können Ihre Fließlösung als Gradienten für Partikel verwenden, die Sie im Strom loslassen. Wahrscheinlich aber teuer, da Sie viele davon brauchen würden.
Bram
Ich würde dies nicht mit einem Shader lösen, ich würde es auf die einfache Art und Weise tun, die über Jahrhunderte verwendet wurde. Zeichnen Sie es einfach und haben Sie 8 verschiedene Zeichnungen des Wassers und auch 8 verschiedene Zeichnungen des Wassers, das auf das Ufer trifft. Fügen Sie dann eine Farbüberlagerung hinzu, wenn Sie ein anderes Gelände haben möchten, und fügen Sie zufällig Steine, Fische oder was auch immer in den Fluss ein. Übrigens mit 8 verschiedenen Ich wollte für jede 45 Grad Rotation ein anderes Sprite haben
Yosh Synergi
@YoshSynergi Ich möchte, dass der Fluss in jede Richtung und nicht in 8 Richtungen fließt, und ich möchte vermeiden, dass zwischen den Fliesenrändern sichtbare Grenzen bestehen, ähnlich dem Ergebnis, das mit dem verknüpften Shader erzielt wurde
Ross Taylor-Turner,
@Bram, das ist eine Option, die ich in Betracht ziehen könnte, aber ich denke auch, dass es zu viele Partikel braucht, um effektiv zu sein, insbesondere wenn die Kamera stark herausgezoomt ist
Ross Taylor-Turner

Antworten:

11

Ich hatte keine Kacheln zur Hand, die mit Verzerrungen gut aussahen. Hier ist eine Version des Effekts, den ich stattdessen mit diesen Kenney-Kacheln verspottet habe :

Animation, die fließendes Wasser in Tilemap zeigt.

Ich verwende eine Flusskarte wie diese, wobei Rot = Fluss nach rechts und Grün = nach oben, wobei Gelb beides ist. Jedes Pixel entspricht einer Kachel, wobei das Pixel unten links die Kachel bei (0, 0) in meinem Weltkoordinatensystem ist.

8x8

Und eine Wellenmustertextur wie diese:

Geben Sie hier die Bildbeschreibung ein

Ich bin mit der hlsl / CG-Syntax von Unity am besten vertraut, daher müssen Sie diesen Shader ein wenig an Ihren glsl-Kontext anpassen, dies sollte jedoch unkompliziert sein.

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