computer shader是在显卡上运行的程序,在正常的渲染管道之外。被用于大量并行的gpu算法,或加速部分游戏渲染。想要高效利用他们,最好深入的理解cpu机制和并行算法。还有DirectCompute,OpenGL Compute,CUDA或openCL。
unity 的compute shader很像DX11 DirectCompute技术。能工作的平台有:
1. windows,有Dx11或Dx12显卡Api,shader model 4.5 gpu;
2. 用metal显卡api的macOS和ios;
3. Android linux和windows有Vulkan Api;
4. 现代openGL平台(openGL 4.3在linux和windows;gl es 3.1在安卓)。注意mac os x不支持opengl 4.3;
5. 现代控制台(sony ps4和微软xbox one)
运行时判断是否支持compute shader可以用SystemInfo.supportsComputeShaders。
类似于普通的shader,compute shader在工程里也是资源文件,.compute扩展名。他们是用Dx11风格的hlsl语言缩写。用#pragma 编译指令指定哪些很少被当成compute shader核心编译,如下:
#pragma kernel KMain
[numthreads(8, 8, 1)]
void KMain(uint2 groupId : SV_GroupID, uint2 groupThreadId : SV_GroupThreadID, uint2 dispatchThreadId : SV_DispatchThreadID)
{
// Upper-left pixel coordinate of quad that this thread will read
int2 threadUL = (groupThreadId << 1) + (groupId << 3) - 4;
// Downsample the block
float2 offset = float2(threadUL);
float4 p00 = _Source.SampleLevel(sampler_LinearClamp, (offset + 0.5) * _Size.zw, 0.0);
float4 p10 = _Source.SampleLevel(sampler_LinearClamp, (offset + float2(1.0, 0.0) + 0.5) * _Size.zw, 0.0);
float4 p01 = _Source.SampleLevel(sampler_LinearClamp, (offset + float2(0.0, 1.0) + 0.5) * _Size.zw, 0.0);
float4 p11 = _Source.SampleLevel(sampler_LinearClamp, (offset + float2(1.0, 1.0) + 0.5) * _Size.zw, 0.0);
// Store the 4 downsampled pixels in LDS
uint destIdx = groupThreadId.x + (groupThreadId.y << 4u);
Store2Pixels(destIdx , p00, p10);
Store2Pixels(destIdx + 8u, p01, p11);
GroupMemoryBarrierWithGroupSync();
// Horizontally blur the pixels in LDS
uint row = groupThreadId.y << 4u;
BlurHorizontally(row + (groupThreadId.x << 1u), row + groupThreadId.x + (groupThreadId.x & 4u));
GroupMemoryBarrierWithGroupSync();
// Vertically blur the pixels in LDS and write the result to memory
BlurVertically(dispatchThreadId, (groupThreadId.y << 3u) + groupThreadId.x);
}
表示kMain函数被当作compute shader编译,以及:
// test.compute
#pragma kernel FillWithRed
RWTexture2D res;
[numthreads(1,1,1)]
void FillWithRed (uint3 dtid : SV_DispatchThreadID)
{
res[dtid.xy] = float4(1,0,0,1);
}
语言是标准dx 11 hlsl语言。一个compute shader资源文件必须包含至少一个会被调用的compute kernel。可以有多个,写多行#pragma语句即可。
当用#pragma时,注意同一行加“// 。。”这种注释会产生编译错误。
#pragma后面可以跟这个shader编译需要的宏
#pragma kernel KernelOne SOME_DEFINE DEFINE_WITH_VALUE=1337
#pragma kernel KernelTwo OTHER_DEFINE
public sealed class ComputeShaders
{
public ComputeShader exposureHistogram;
public ComputeShader lut3DBaker;
public ComputeShader texture3dLerp;
public ComputeShader gammaHistogram;
public ComputeShader waveform;
public ComputeShader vectorscope;
public ComputeShader multiScaleAODownsample1;
public ComputeShader multiScaleAODownsample2;
public ComputeShader multiScaleAORender;
public ComputeShader multiScaleAOUpsample;
public ComputeShader gaussianDownsample;
}
public void DispatchCompute(ComputeShader computeShader, int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ);
调用computeShader:
void PushUpsampleCommands(CommandBuffer cmd, int lowResDepth, int interleavedAO, int highResDepth, int? highResAO, RenderTargetIdentifier dest, Vector3 lowResDepthSize, Vector2 highResDepthSize, bool invert = false)
{
var cs = m_Resources.computeShaders.multiScaleAOUpsample;
int kernel = cs.FindKernel(highResAO == null
? invert
? "main_invert"
: "main"
: "main_blendout");
float stepSize = 1920f / lowResDepthSize.x;
float bTolerance = 1f - Mathf.Pow(10f, m_Settings.blurTolerance.value) * stepSize;
bTolerance *= bTolerance;
float uTolerance = Mathf.Pow(10f, m_Settings.upsampleTolerance.value);
float noiseFilterWeight = 1f / (Mathf.Pow(10f, m_Settings.noiseFilterTolerance.value) + uTolerance);
cmd.SetComputeVectorParam(cs, "InvLowResolution", new Vector2(1f / lowResDepthSize.x, 1f / lowResDepthSize.y));
cmd.SetComputeVectorParam(cs, "InvHighResolution", new Vector2(1f / highResDepthSize.x, 1f / highResDepthSize.y));
cmd.SetComputeVectorParam(cs, "AdditionalParams", new Vector4(noiseFilterWeight, stepSize, bTolerance, uTolerance));
cmd.SetComputeTextureParam(cs, kernel, "LoResDB", lowResDepth);
cmd.SetComputeTextureParam(cs, kernel, "HiResDB", highResDepth);
cmd.SetComputeTextureParam(cs, kernel, "LoResAO1", interleavedAO);
if (highResAO != null)
cmd.SetComputeTextureParam(cs, kernel, "HiResAO", highResAO.Value);
cmd.SetComputeTextureParam(cs, kernel, "AoResult", dest);
int xcount = ((int)highResDepthSize.x + 17) / 16;
int ycount = ((int)highResDepthSize.y + 17) / 16;
cmd.DispatchCompute(cs, kernel, xcount, ycount, 1);
}
和compute shader联系紧密的是compute buffer,用法如下
if (m_Data == null)
m_Data = new ComputeBuffer(m_NumBins, sizeof(uint));
var compute = context.resources.computeShaders.gammaHistogram;
var cmd = context.command;
cmd.BeginSample("GammaHistogram");
// Clear the buffer on every frame as we use it to accumulate values on every frame
int kernel = compute.FindKernel("KHistogramClear");
cmd.SetComputeBufferParam(compute, kernel, "_HistogramBuffer", m_Data);
cmd.DispatchCompute(compute, kernel, Mathf.CeilToInt(m_NumBins / (float)m_ThreadGroupSizeX), 1, 1);
另外,其他用法请搜索文档。
RenderTextures也可以从compute shader写入,如果设置随机访问权限的话。查询RenderTexture.enableRandomWrite。
在unity中,贴图和采样器不是分开的事物。所以要在compute shader中使用采样器的话,需要遵循下面的unity专门的规则:
1. 和贴图名称用同样的名称,如贴图名为Texture2D MyTex,则SamplerState samplerMyTex。这样,sampler会被初始化为该贴图的过滤模式,wrap模式,异向模式。
2. 用预定于的采样器,名称必须带有Linear或Point(过滤模式)和Clamp或Repeat(wrap模式)。比如SamplerState MyLinearClampSamplers创建一个linear过滤模式和Clamp wrap模式的采样器。更多的内容查询SamplerState
像正常shader一样,unity能把compute shader从hlsl转换成其他shader语言。所以,为了最简单的跨平台编译,你可以用hlsl写compute shader。然而有几个因素需要考虑。
Dx 11支持许多其他平台不支持的行为(比如metal 或OpenGL ES)。所以,你需要保证你的shader在其他支持能力低的平台上工作正常。下面几个问题要注意:
1. 内存溢出访问。Dx11 读的时候会返回0,不会出问题。但很少支持的平台可能会gpu崩溃。另外,dx11的一些技巧,比如buffer大小和你的线程组数量是无关的,[ 试图从缓冲区的开始或结束读取相邻的数据元素,以及类似的不兼容性。
2. 初始化你的资源,新的缓冲区和贴图内容是未定义的。一些平台可能全是0,其他的可能是任意东西,包括为空。
3. 绑定你的compute shader需要的所有资源。即使你确定地知道这个shader在他的当前状态下由于分支原因不会用到这个资源,你仍然需要确定这个资源绑定到shader上。
一般,compute shader文件是用hlsl写的,被自动编译或翻译到所有必要的平台。然而,可以阻止将它翻译到其他语言,或者手动写glsl compute shader。
下面的内容只应用于hlsl-only和glsl-only compute shader,而不是跨平台编译。因为这些内容会导致compute shader从一些平台排除。
1.被CGPROGRAM和ENDCG包围的 compute shader不能被非hlsl平台处理;
2. 被GLSLPROGRAM和ENDGLSL包围的compute shader被当作glsl处理,逐字排除。你要注意,自动翻译的shader的buffer遵循hlsl数据布局,手动写的glsl shader遵循glsl布局规则。