从我的工作着色器转换为统一计算着色器时流体模拟中的奇怪行为

Odd behavior in fluid simulation when converting from my working shadertoy to unity compute shaders

我正在尝试复制 my working 2d fluid shadertoy in a compute shader in Unity, with the hopes of moving it to 3D soon. When I replicate the algorithm the same way, I get some very weird behavior (seen in this video I took)。我试图调试我能想到的一切,但我无法弄清楚为什么它们不一样。我正在可视化此捕获中的向量矩阵(与在查看我的 shadertoy 时按 Space 相同)。

我创建了一个驱动速度矩阵的 pastebin with the code that I am using to perform the Navier-Stokes equations。模拟的内容归结为:

float4 S(RWTexture2D<float4> target, uint2 id)
{
    return target[(id.xy + resolution)%resolution];
}

void Fluid(RWTexture2D<float4> target, uint2 id, float2 offset, float4 values, inout float2 velocity, inout float pressure, inout float divergence, inout float neighbors)
{
    float2 v = S(target, id.xy + offset);
    float4 s = S(target, id.xy + offset.xy - v);
    
    float2 o= normalize(offset);
    
    velocity += o * (s.w - values.w);
    
    pressure += s.w;
    
    divergence += dot(o, s.xy);
    
    ++neighbors;
}

void StepVelocity(uint3 id, RWTexture2D<float4> write, RWTexture2D<float4> read, bool addJet)
{
    //sample our current values, then sample the values from the cell our velocity is coming from
    float4 values = S(read, id.xy);
    values = S(read, id.xy - values.xy);
    
    float2 velocity = float2(0,0);
    float pressure = 0;
    float neighbors = 0;
    float divergence = 0;
    
    //sample neighboring cells
    Fluid(read, id.xy, float2(0, 1), values, velocity, pressure, divergence, neighbors);
    Fluid(read, id.xy, float2(0, -1), values, velocity, pressure, divergence, neighbors);
    Fluid(read, id.xy, float2(1, 0), values, velocity, pressure, divergence, neighbors);
    Fluid(read, id.xy, float2(-1, 0), values, velocity, pressure, divergence, neighbors);

    velocity = velocity / neighbors;
    divergence = divergence / neighbors;
    pressure = pressure/neighbors;
    
    values.w = pressure-divergence;
    values.xy -= velocity;
    
    if (addJet && distance(id.xy, float2(resolution / 2.0, resolution / 2.0)) < 10)
        values = float4(0, .25, 0, values.w);
    
    write[id.xy] = values;
}

它应该很简单,我尽力对 shadertoy 进行过多的注释以使其易于理解(它们是相同的代码,针对不同的环境设置)。 Here is the C# code that drives drives the simulation.

我知道让别人深入研究我的代码是一个不方便的请求,但我完全不确定我做错了什么,这让我发疯。我在 shadertoy 上使用了完全相同的算法,但它在 Unity 计算着色器中的行为非常奇怪,我不知道为什么。每次我“迈步”时,我都要确保切换 read/write 纹理,以便在模拟中干净利落地向前移动而不干扰。

任何 ideas/tips 解决我的问题将不胜感激。

为了完成这项工作,我做了一些更改:

首先,像素坐标需要偏移0.5。在计算着色器中,坐标是整数,但在 ShaderToy 中,传入的坐标来自 SV_Position 输入,该输入已经应用了该偏移量。

SV_Position describes the pixel location. Available in all shaders to get the pixel center with a 0.5 offset.

gl_FragCoord也是如此。 By default 偏移0.5。当您将速度添加到位置时,这很重要。如果没有偏移量,您将不会在所有方向上都有相同的行为。

其次,我对输入使用了采样器,点过滤似乎不适用于此。否则代码基本相同,应该很容易理解。

外观如下:

FluidSimulation.cs:

public class FluidSimulation : MonoBehaviour
{
    private RenderTexture In;
    private RenderTexture Out;
    private RenderTexture DrawIn;
    private RenderTexture DrawOut;
    
    private int nthreads = 8;
    private int threadresolution => (resolution / nthreads);
    private int stepKernel;

    [Range(8, 1024)] public int resolution = 800;
    [Range(0, 50)] public int stepsPerFrame = 8;
    public ComputeShader Compute;
    public Material OutputMaterial;

    void Start() => Reset();   

    private void Reset()
    {
        In = CreateTexture(RenderTextureFormat.ARGBHalf);
        Out = CreateTexture(RenderTextureFormat.ARGBHalf);
        DrawIn = CreateTexture(RenderTextureFormat.ARGBHalf);
        DrawOut = CreateTexture(RenderTextureFormat.ARGBHalf);
        
        stepKernel = Compute.FindKernel("StepKernel");
        Compute.SetFloat("resolution", resolution);        
    }

    void Update()
    {
        for(int i = 0; i<stepsPerFrame; i++) Step();
    }

    void Step()
    {
        Compute.SetTexture(stepKernel, "In", In);
        Compute.SetTexture(stepKernel, "Out", Out);
        Compute.SetTexture(stepKernel, "DrawIn", DrawIn);
        Compute.SetTexture(stepKernel, "DrawOut", DrawOut);     

        Compute.Dispatch(stepKernel, threadresolution, threadresolution, 1);
        
        OutputMaterial.SetTexture("_MainTex", DrawOut);

        SwapTex(ref In, ref Out);
        SwapTex(ref DrawIn, ref DrawOut);        
    }

    protected RenderTexture CreateTexture(RenderTextureFormat format)
    {
        RenderTexture tex = new RenderTexture(resolution, resolution, 0, format);

        //IMPORTANT FOR GPU SHADERS, allows random access (like gpus will do)
        tex.enableRandomWrite = true;
        tex.dimension = UnityEngine.Rendering.TextureDimension.Tex2D;        
        tex.filterMode = FilterMode.Bilinear;
        tex.wrapMode = TextureWrapMode.Clamp;
        tex.useMipMap = false;
        tex.Create();

        return tex;
    }

    void SwapTex(ref RenderTexture In, ref RenderTexture Out)
    {
        RenderTexture tmp = In;
        In = Out;
        Out = tmp;       
    }
}

计算着色器:

#pragma kernel StepKernel

float resolution;

Texture2D<float4> In;
SamplerState samplerIn;
RWTexture2D<float4> Out;

Texture2D<float4> DrawIn;
SamplerState samplerDrawIn;
RWTexture2D<float4> DrawOut;

float4 Sample(Texture2D<float4> t, SamplerState s, float2 coords) {
    return t.SampleLevel(samplerIn, coords / resolution, 0);
}

void Fluid(float2 coord, float2 offset, inout float2 velocity, inout float pressure, inout float divergence, inout float neighbors)
{
    // Sample buffer C, which samples B, which samples A, making our feedback loop    
    float4 s = Sample(In, samplerIn, coord + offset - Sample(In, samplerIn, coord + offset).xy);

    // gradient of pressure from the neighboring cell to ours
    float sampledPressure = s.w;    

    //add the velocity scaled by the pressure that its exerting
    velocity += offset * sampledPressure;

    // add pressure
    pressure += sampledPressure;

    // divergence of velocity
    divergence += dot(offset, s.xy);

    //increase number of neighbors sampled
    neighbors++;
}

float4 StepVelocity(float2 id) {

    //sample from the previous state    
    float4 values = Sample(In, samplerIn, id - Sample(In, samplerIn, id).xy);
    
    float2 velocity = float2(0, 0);
    float divergence = 0.;
    float pressure = 0., neighbors = 0.;

    Fluid(id.xy, float2( 0., 1.), velocity, pressure, divergence, neighbors);
    Fluid(id.xy, float2( 0.,-1.), velocity, pressure, divergence, neighbors);
    Fluid(id.xy, float2( 1., 0.), velocity, pressure, divergence, neighbors);
    Fluid(id.xy, float2(-1., 0.), velocity, pressure, divergence, neighbors);

    //average the samples
    velocity /= neighbors;
    divergence /= neighbors;
    pressure /= neighbors;

    //output pressure in w, velocity in xy
    values.w = pressure - divergence;
    values.xy -= velocity;
    
    float2 p1 = float2(.47, .2);
    if (length(id.xy - resolution * p1) < 10.) {
        values.xy = float2(0, .5);
    }
    float2 p2 = float2(.53, .8);
    if (length(id.xy - resolution * p2) < 10.) {
        values.xy = float2(0, -.5);
    }
    return values;
}

float4 StepFluid(float2 id) {

    for (int i = 0; i < 4; i++)
        id -= Sample(In, samplerIn, id).xy;

    float4 color = Sample(DrawIn, samplerDrawIn, id);

    float2 p1 = float2(.47, .2);
    if (length(id.xy - resolution * p1) < 10.) {
        color = float4(0, 1, 0, 1);
    }
    float2 p2 = float2(.53, .8);
    if (length(id.xy - resolution * p2) < 10.) {
        color = float4(1, 0, 0, 1);
    }
    color *= .999;
    return color;
}

[numthreads(8, 8, 1)]
void StepKernel (uint3 id : SV_DispatchThreadID)
{     
    float2 coord = float2(id.x + .5, id.y + .5);
    Out[id.xy] = StepVelocity(coord);
    DrawOut[id.xy] = StepFluid(coord);
}

我还想提一件事,您可以通过单击 ShaderToys 编辑器 window 底部的 analyze 按钮查看翻译后的 HLSL ShaderToy 着色器代码。我不知道这个存在,但它在尝试将着色器从 ShaderToy 转换为 Unity 时非常有用。