如何渲染2D自上而下的平铺定向水流?


9

我正在开发受矮人要塞启发的自上而下的基于图块的相当图形化2D游戏。我现在要在游戏世界中实现一条河流,该河流覆盖许多图块,并且我已经计算出每个图块的流向,如下图所示(每个图块中的红线所示)。

带方向的河瓦示例

作为图形样式的参考,这是我的游戏当前的外观:

游戏中的图形风格拍摄

我需要的是一种使每块河砖中的水动画化的技术,以使水流融合到周围的砖中,从而使砖的边缘不明显。

我发现的最接近的示例在http://www.rug.nl/society-business/centre-for-information-technology/research/hpcv/publications/watershader/中进行了描述,但我不太了解在能够理解其中发生了什么时?我对着色器编程有足够的了解,可以实现自己的动态照明,但是我无法完全理解链接文章中采用的方法。

有人可以解释如何达到上述效果,还是建议另一种方法来获得我想要的结果?我认为上述解决方案的一部分是将图块重叠(尽管我不确定哪种组合),并旋转用于变形的法线贴图(再次不知道具体细节),而过去我有点迷失了,谢谢任何帮助!


您对水本身有视觉目标吗?我注意到您引用的链接使用法线贴图进行镜面反射-在您显示的平面/卡通风格的艺术方向上可能无法完全融合。有多种方法可以使该技术适应其他样式,但是我们需要一些准则,以便我们知道要达到的目标。
DMGregory

您可以将流解决方案用作在流中松散的粒子的梯度。不过可能很贵,因为您将需要很多。
Bram

我不会用着色器解决这个问题,我会用几个世纪来使用的简单方法来完成它,只是绘制它,就好像有8张不同的水图,还有8张不同的水在岸上绘制。如果您希望拥有不同的地形,请添加颜色叠加层,并向其中添加随机的颜色,例如向河中洒石子,鱼或其他任何东西。顺便说一句,有8种不同的效果,我的意思是每旋转45度就有一个不同的精灵
Yosh Synergi,2017年

@YoshSynergi我希望河水流向任何方向,而不是8个方向,并且我想避免在瓷砖边缘之间形成可见的边界,类似于链接着色器中实现的结果
Ross Taylor-Turner

@Bram是我正在考虑可以实现的一个选项,但也认为它需要太多的粒子才能有效,尤其是在相机缩小很多时
Ross Taylor-Turner

Answers:


11

我没有任何易于变形的磁贴,所以下面是我用这些Kenney磁贴模拟的效果的一个版本:

在tilemap中显示流动的水的动画。

我正在使用这样的流程图,其中红色=向右流动,绿色=向上流动,黄色等于两个。每个像素对应一个图块,左下像素是我的世界坐标系中(0,0)处的图块。

8x8

波形纹理如下:

在此处输入图片说明

我最熟悉Unity的hlsl / CG风格的语法,因此您需要为glsl上下文稍微调整一下此着色器,但这应该很简单。

// 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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.