从我的工作着色器转换为统一计算着色器时流体模拟中的奇怪行为
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 时非常有用。
我正在尝试复制 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 时非常有用。