将多通道像素着色器转换为计算着色器的任何技术?
Any technique to convert multi-pass pixel shader to compute shader?
我想实现流体模拟。类似于 this。算法并不重要。重要的问题是,如果我们要在像素着色器中实现它,则应该分多次完成。
我以前使用的技术的问题是性能很差。我将解释正在发生的事情的概述以及用于一次性解决计算的技术,然后是时间信息。
概览:
我们有一个地形,我们想在它上面下雨,看看水流。我们有 1024x1024 纹理的数据。我们有地形的高度和每个点的水量。这是一个迭代模拟。迭代 1 获取地形和水体纹理作为输入,计算然后将结果写入地形和水体纹理。迭代 2 然后 运行s 再次改变纹理更多一点。经过数百次迭代后,我们得到了这样的结果:
在每次迭代中都会发生这些阶段:
- 获取地形和水高。
- 计算流量。
- 将 Flow 值写入组共享内存。
- 同步组内存
- 从该线程和当前线程左、右、上、下线程的组共享内存中读取流量值。
- 根据上一步读取的流量值计算水的新值。
- 将结果写入地形和水纹理。
所以基本上我们获取数据,执行 calculate1,将 calculate1 结果放入共享内存,同步,从当前线程的共享内存中获取和邻居,做 calculate2 ,然后写下结果。
这是一个清晰的模式,出现在范围很广的图像处理问题中。经典的解决方案是多通道着色器,但我在单通道计算着色器中完成它以节省带宽。
技术:
我使用了 Practical Rendering and Computation with Direct3D 11 第 12 章中解释的技术。假设我们希望每个线程组都是 16x16x1 线程。但是因为第二次计算也需要邻居,所以我们在每个方向上填充像素。这意味着我们将拥有 18x18x1 线程组。由于这种填充,我们将在第二次计算中得到有效的邻居。这是一张显示填充的图片。黄色线是需要计算的线,红色线是填充的。它们是线程组的一部分,但我们只是将它们用于中间处理,不会将它们保存到纹理中。请注意,在这张图片中,带填充的组是 10x10x1,但我们的线程组是 18x18x1。
过程 运行s 和 returns 正确的结果。唯一的问题是性能。
时间:
在 Geforce GT 710 的系统上,我 运行 进行了 10000 次迭代的模拟。
- 运行 完整且正确的模拟需要 60 秒。
- 如果我不填充边框并使用16x16x1线程组,时间将是40秒。显然结果是错误的。
- 如果我不使用 groupshared 内存并为第二次计算提供虚拟值,则时间将为 19 秒。结果当然是错的。
问题:
- 这是解决这个问题的最佳技术吗?如果我们改为计算两个不同的内核,它会更快。 2x19<60.
- 为什么组共享内存太慢了?
这是计算着色器代码。这是需要 60 秒的正确版本:
#pragma kernel CSMain
Texture2D<float> _waterAddTex;
Texture2D<float4> _flowTex;
RWTexture2D<float4> _watNormTex;
RWTexture2D<float4> _flowOutTex;
RWTexture2D<float> terrainFieldX;
RWTexture2D<float> terrainFieldY;
RWTexture2D<float> waterField;
SamplerState _LinearClamp;
SamplerState _LinearRepeat;
#define _gpSize 16
#define _padGPSize 18
groupshared float4 f4shared[_padGPSize * _padGPSize];
float _timeStep, _resolution, _groupCount, _pixelMeter, _watAddStrength, watDamping, watOutConstantParam, _evaporation;
int _addWater, _computeWaterNormals;
float2 _rainUV;
bool _usePrevOutflow,_useStava;
float terrHeight(float2 texData) {
return dot(texData, identity2);
}
[numthreads(_padGPSize, _padGPSize, 1)]
void CSMain(int2 groupID : SV_GroupID, uint2 dispatchIdx : SV_DispatchThreadID, uint2 padThreadID : SV_GroupThreadID)
{
int2 id = groupID * _gpSize + padThreadID - 1;
int gsmID = padThreadID.x + _padGPSize * padThreadID.y;
float2 uv = (id + 0.5) / _resolution;
bool outOfGroupBound = (padThreadID.x == 0 || padThreadID.y == 0 || padThreadID.x == _padGPSize - 1
|| padThreadID.y == _padGPSize - 1) ? true : false;
// -------------FETCH-------------
float2 cenTer, lTer, rTer, tTer, bTer;
sampleUavNei(terrainFieldX,terrainFieldY, id, cenTer, lTer, rTer, tTer, bTer);
float cenWat, lWat, rWat, tWat, bWat;
sampleUavNei(waterField, id, cenWat, lWat, rWat, tWat, bWat);
// -------------Calculate 1-------------
float cenTerHei = terrHeight(cenTer);
float cenTotHei = cenWat + cenTerHei;
float4 neisTerHei = float4(terrHeight(lTer), terrHeight(rTer), terrHeight(tTer), terrHeight(bTer));
float4 neisWat = float4(lWat, rWat, tWat, bWat);
float4 neisTotHei = neisWat + neisTerHei;
float4 neisTotHeiDiff = cenTotHei - neisTotHei;
float4 prevOutflow = _usePrevOutflow? _flowTex.SampleLevel(_LinearClamp, uv, 0):float4(0,0,0,0);
float4 watOutflow;
float4 flowFac = min(abs(neisTotHeiDiff), (cenWat + neisWat) * 0.5f);
flowFac = min(1, flowFac);
watOutflow = max(watDamping* prevOutflow + watOutConstantParam * neisTotHeiDiff * flowFac, 0);
float outWatFac = cenWat / max(dot(watOutflow, identity4) * _timeStep, 0.001f);
outWatFac = min(outWatFac, 1);
watOutflow *= outWatFac;
// -------------groupshared memory-------------
f4shared[gsmID] = watOutflow;
GroupMemoryBarrierWithGroupSync();
float4 cenFlow = f4shared[gsmID];
float4 lFlow = f4shared[gsmID - 1];
float4 rFlow = f4shared[gsmID + 1];
float4 tFlow = f4shared[gsmID + _padGPSize];
float4 bFlow = f4shared[gsmID - _padGPSize];
//float4 cenFlow = 0;
//float4 lFlow = 0;
//float4 rFlow = 0;
//float4 tFlow = 0;
//float4 bFlow = 0;
// -------------Calculate 2-------------
if (!outOfGroupBound) {
float watDiff = _timeStep *((lFlow.y + rFlow.x + tFlow.w + bFlow.z) - dot(cenFlow, identity4));
cenWat = cenWat + watDiff - _evaporation;
cenWat = max(cenWat, 0);
}
// -------------End of calculation-------------
//Water Addition
if (_addWater)
cenWat += _timeStep * _watAddStrength * _waterAddTex.SampleLevel(_LinearRepeat, uv + _rainUV, 0);
if (_computeWaterNormals)
_watNormTex[id] = float4(0, 1, 0, 0);
// -------------Write results-------------
if (!outOfGroupBound) {
_flowOutTex[id] = cenFlow;
waterField[id] = cenWat;
}
}
好吧,我犯了一个严重的错误。为了测试组共享内存的性能,我做了以下操作:
// -------------groupshared memory-------------
f4shared[gsmID] = watOutflow;
GroupMemoryBarrierWithGroupSync();
float4 cenFlow = f4shared[gsmID];
float4 lFlow = f4shared[gsmID - 1];
float4 rFlow = f4shared[gsmID + 1];
float4 tFlow = f4shared[gsmID + _padGPSize];
float4 bFlow = f4shared[gsmID - _padGPSize];
//float4 cenFlow = 0;
//float4 lFlow = 0;
//float4 rFlow = 0;
//float4 tFlow = 0;
//float4 bFlow = 0;
您看到同步后有一些变量。我确实测试了前五个将变量初始化为正确值的语句。然后为了在没有 groupshared 的情况下进行测试,我注释了这五个语句并取消了下面五个将变量初始化为零的语句的注释。它是这样变成的:
// -------------groupshared memory-------------
//f4shared[gsmID] = watOutflow;
//GroupMemoryBarrierWithGroupSync();
//float4 cenFlow = f4shared[gsmID];
//float4 lFlow = f4shared[gsmID - 1];
//float4 rFlow = f4shared[gsmID + 1];
//float4 tFlow = f4shared[gsmID + _padGPSize];
//float4 bFlow = f4shared[gsmID - _padGPSize];
float4 cenFlow = 0;
float4 lFlow = 0;
float4 rFlow = 0;
float4 tFlow = 0;
float4 bFlow = 0;
然后这个过程变得更快了。我没有注意到的一件事是我不再使用以前的计算并且编译器将它们优化掉了。实施更多算法后,我意识到组共享内存实际上运行良好,与读取纹理相比花费的时间更少。
毕竟我确实设法在 1 遍中完成了算法。它比原始实现快 2.5 倍,问题中描述的技术在我的案例中工作得很好。
我想实现流体模拟。类似于 this。算法并不重要。重要的问题是,如果我们要在像素着色器中实现它,则应该分多次完成。
我以前使用的技术的问题是性能很差。我将解释正在发生的事情的概述以及用于一次性解决计算的技术,然后是时间信息。
概览:
我们有一个地形,我们想在它上面下雨,看看水流。我们有 1024x1024 纹理的数据。我们有地形的高度和每个点的水量。这是一个迭代模拟。迭代 1 获取地形和水体纹理作为输入,计算然后将结果写入地形和水体纹理。迭代 2 然后 运行s 再次改变纹理更多一点。经过数百次迭代后,我们得到了这样的结果:
在每次迭代中都会发生这些阶段:
- 获取地形和水高。
- 计算流量。
- 将 Flow 值写入组共享内存。
- 同步组内存
- 从该线程和当前线程左、右、上、下线程的组共享内存中读取流量值。
- 根据上一步读取的流量值计算水的新值。
- 将结果写入地形和水纹理。
所以基本上我们获取数据,执行 calculate1,将 calculate1 结果放入共享内存,同步,从当前线程的共享内存中获取和邻居,做 calculate2 ,然后写下结果。
这是一个清晰的模式,出现在范围很广的图像处理问题中。经典的解决方案是多通道着色器,但我在单通道计算着色器中完成它以节省带宽。
技术:
我使用了 Practical Rendering and Computation with Direct3D 11 第 12 章中解释的技术。假设我们希望每个线程组都是 16x16x1 线程。但是因为第二次计算也需要邻居,所以我们在每个方向上填充像素。这意味着我们将拥有 18x18x1 线程组。由于这种填充,我们将在第二次计算中得到有效的邻居。这是一张显示填充的图片。黄色线是需要计算的线,红色线是填充的。它们是线程组的一部分,但我们只是将它们用于中间处理,不会将它们保存到纹理中。请注意,在这张图片中,带填充的组是 10x10x1,但我们的线程组是 18x18x1。
过程 运行s 和 returns 正确的结果。唯一的问题是性能。
时间: 在 Geforce GT 710 的系统上,我 运行 进行了 10000 次迭代的模拟。
- 运行 完整且正确的模拟需要 60 秒。
- 如果我不填充边框并使用16x16x1线程组,时间将是40秒。显然结果是错误的。
- 如果我不使用 groupshared 内存并为第二次计算提供虚拟值,则时间将为 19 秒。结果当然是错的。
问题:
- 这是解决这个问题的最佳技术吗?如果我们改为计算两个不同的内核,它会更快。 2x19<60.
- 为什么组共享内存太慢了?
这是计算着色器代码。这是需要 60 秒的正确版本:
#pragma kernel CSMain
Texture2D<float> _waterAddTex;
Texture2D<float4> _flowTex;
RWTexture2D<float4> _watNormTex;
RWTexture2D<float4> _flowOutTex;
RWTexture2D<float> terrainFieldX;
RWTexture2D<float> terrainFieldY;
RWTexture2D<float> waterField;
SamplerState _LinearClamp;
SamplerState _LinearRepeat;
#define _gpSize 16
#define _padGPSize 18
groupshared float4 f4shared[_padGPSize * _padGPSize];
float _timeStep, _resolution, _groupCount, _pixelMeter, _watAddStrength, watDamping, watOutConstantParam, _evaporation;
int _addWater, _computeWaterNormals;
float2 _rainUV;
bool _usePrevOutflow,_useStava;
float terrHeight(float2 texData) {
return dot(texData, identity2);
}
[numthreads(_padGPSize, _padGPSize, 1)]
void CSMain(int2 groupID : SV_GroupID, uint2 dispatchIdx : SV_DispatchThreadID, uint2 padThreadID : SV_GroupThreadID)
{
int2 id = groupID * _gpSize + padThreadID - 1;
int gsmID = padThreadID.x + _padGPSize * padThreadID.y;
float2 uv = (id + 0.5) / _resolution;
bool outOfGroupBound = (padThreadID.x == 0 || padThreadID.y == 0 || padThreadID.x == _padGPSize - 1
|| padThreadID.y == _padGPSize - 1) ? true : false;
// -------------FETCH-------------
float2 cenTer, lTer, rTer, tTer, bTer;
sampleUavNei(terrainFieldX,terrainFieldY, id, cenTer, lTer, rTer, tTer, bTer);
float cenWat, lWat, rWat, tWat, bWat;
sampleUavNei(waterField, id, cenWat, lWat, rWat, tWat, bWat);
// -------------Calculate 1-------------
float cenTerHei = terrHeight(cenTer);
float cenTotHei = cenWat + cenTerHei;
float4 neisTerHei = float4(terrHeight(lTer), terrHeight(rTer), terrHeight(tTer), terrHeight(bTer));
float4 neisWat = float4(lWat, rWat, tWat, bWat);
float4 neisTotHei = neisWat + neisTerHei;
float4 neisTotHeiDiff = cenTotHei - neisTotHei;
float4 prevOutflow = _usePrevOutflow? _flowTex.SampleLevel(_LinearClamp, uv, 0):float4(0,0,0,0);
float4 watOutflow;
float4 flowFac = min(abs(neisTotHeiDiff), (cenWat + neisWat) * 0.5f);
flowFac = min(1, flowFac);
watOutflow = max(watDamping* prevOutflow + watOutConstantParam * neisTotHeiDiff * flowFac, 0);
float outWatFac = cenWat / max(dot(watOutflow, identity4) * _timeStep, 0.001f);
outWatFac = min(outWatFac, 1);
watOutflow *= outWatFac;
// -------------groupshared memory-------------
f4shared[gsmID] = watOutflow;
GroupMemoryBarrierWithGroupSync();
float4 cenFlow = f4shared[gsmID];
float4 lFlow = f4shared[gsmID - 1];
float4 rFlow = f4shared[gsmID + 1];
float4 tFlow = f4shared[gsmID + _padGPSize];
float4 bFlow = f4shared[gsmID - _padGPSize];
//float4 cenFlow = 0;
//float4 lFlow = 0;
//float4 rFlow = 0;
//float4 tFlow = 0;
//float4 bFlow = 0;
// -------------Calculate 2-------------
if (!outOfGroupBound) {
float watDiff = _timeStep *((lFlow.y + rFlow.x + tFlow.w + bFlow.z) - dot(cenFlow, identity4));
cenWat = cenWat + watDiff - _evaporation;
cenWat = max(cenWat, 0);
}
// -------------End of calculation-------------
//Water Addition
if (_addWater)
cenWat += _timeStep * _watAddStrength * _waterAddTex.SampleLevel(_LinearRepeat, uv + _rainUV, 0);
if (_computeWaterNormals)
_watNormTex[id] = float4(0, 1, 0, 0);
// -------------Write results-------------
if (!outOfGroupBound) {
_flowOutTex[id] = cenFlow;
waterField[id] = cenWat;
}
}
好吧,我犯了一个严重的错误。为了测试组共享内存的性能,我做了以下操作:
// -------------groupshared memory-------------
f4shared[gsmID] = watOutflow;
GroupMemoryBarrierWithGroupSync();
float4 cenFlow = f4shared[gsmID];
float4 lFlow = f4shared[gsmID - 1];
float4 rFlow = f4shared[gsmID + 1];
float4 tFlow = f4shared[gsmID + _padGPSize];
float4 bFlow = f4shared[gsmID - _padGPSize];
//float4 cenFlow = 0;
//float4 lFlow = 0;
//float4 rFlow = 0;
//float4 tFlow = 0;
//float4 bFlow = 0;
您看到同步后有一些变量。我确实测试了前五个将变量初始化为正确值的语句。然后为了在没有 groupshared 的情况下进行测试,我注释了这五个语句并取消了下面五个将变量初始化为零的语句的注释。它是这样变成的:
// -------------groupshared memory-------------
//f4shared[gsmID] = watOutflow;
//GroupMemoryBarrierWithGroupSync();
//float4 cenFlow = f4shared[gsmID];
//float4 lFlow = f4shared[gsmID - 1];
//float4 rFlow = f4shared[gsmID + 1];
//float4 tFlow = f4shared[gsmID + _padGPSize];
//float4 bFlow = f4shared[gsmID - _padGPSize];
float4 cenFlow = 0;
float4 lFlow = 0;
float4 rFlow = 0;
float4 tFlow = 0;
float4 bFlow = 0;
然后这个过程变得更快了。我没有注意到的一件事是我不再使用以前的计算并且编译器将它们优化掉了。实施更多算法后,我意识到组共享内存实际上运行良好,与读取纹理相比花费的时间更少。
毕竟我确实设法在 1 遍中完成了算法。它比原始实现快 2.5 倍,问题中描述的技术在我的案例中工作得很好。