计算着色器

计算着色器(Compute Shader,以下简称 CS)是 GPU 上执行通用计算任务的编程模型。Cocos CS 继承 GLSL 语法与内置变量,添加方式与光栅化 Shader 相同以 effect 形式呈现,仅支持在自定义管线中使用。Compute Shader 无光栅化过程,输入输出均为内存数据。通过多线程完成并行处理,处理大量数据时非常高效。

Effect 定义方式

定义方式同光栅化 Shader,在计算着色器下配置 PipelineState 无意义。

  1. CCEffect %{
  2. techniques:
  3. - name: opaque
  4. passes:
  5. - compute: compute-main // 定义 cs 入口
  6. pass: user-compute // 定义 layout
  7. properties: &props
  8. mainTexture: { value: grey } // 注册着色器材质面板资源属性
  9. }%
  10. CCProgram compute-main %{
  11. precision highp float;
  12. precision mediump image2D;
  13. layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in;
  14. #pragma rate mainTexture batch
  15. uniform sampler2D mainTexture;
  16. #pragma rate outputImage pass
  17. layout (rgba8) writeonly uniform image2D outputImage;
  18. void main () {
  19. imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy), vec4(1, 0, 0, 1));
  20. }
  21. }%

如要查看更多语法,请移步 着色器语法

输入输出

CS 输入输出包含内置输入变量与着色器资源。

内置输入包含以下部分,与 GLSL 语义相同:

  1. in uvec3 gl_NumWorkGroups;
  2. in uvec3 gl_WorkGroupID;
  3. in uvec3 gl_LocalInvocationID;
  4. in uvec3 gl_GlobalInvocationID;
  5. in uint gl_LocalInvocationIndex;
  6. layout(local_size_x = X, local_size_y = Y, local_size_z = Z) in;

着色器资源包含:

  • UniformBuffer
  • StorageBuffer
  • ImageSampler
  • StorageImage
  • SubpassInput

CS 无内置输出,可通过 StorageBuffer / Image 输出。

着色器资源

CS 目前支持 PerPassPerBatch 两种频率的资源绑定,如下所示:

  1. #pragma rate mainTexture batch
  2. uniform sampler2D mainTexture;
  3. #pragma rate outputImage pass
  4. layout (rgba8) writeonly uniform image2D outputImage;

其中 PerPass 资源可以定义为需要管线跟踪处理同步问题的资源,PerBatch 通常为常量数据、静态纹理,可通过 Material 进行修改绑定。

PerBatch mainTexture 在 properties 中绑定后,可在材质面板中配置。

PerPass outputImage 需要在管线中声明并由 ComputePass 引用,并由 RenderGraph 管理数据的读写同步以及 ImageLayout,详见后文示例。

管线集成

Cocos 自定义管线添加 CS 过程分为三步:

  1. 添加 Compute Pass,其中 passName 为当前 Pass 的 LayoutName,需要与 Effect 中的 pass 字段对应。

    1. const csBuilder = pipeline.addComputePass('passName');
  2. 声明、引用资源,并关联资源访问类型与 Shader 资源槽位。

    1. const csOutput = 'cs_output';
    2. if (!pipeline.containsResource(csOutput)) {
    3. pipeline.addStorageTexture(csOutput,
    4. gfx.Format.RGBA8,
    5. width, height,
    6. rendering.ResourceResidency.MANAGED);
    7. } else {
    8. pipeline.updateStorageTexture(csOutput,
    9. width, height,
    10. gfx.Format.RGBA8);
    11. }
    12. csBuilder.addStorageImage(csOutput, // 资源名
    13. rendering.AccessType.WRITE, // 内存访问类型
    14. 'outputImage'); // 着色器资源名
  3. 添加 Dispatch 调用实例,设置 Dispatch 参数,绑定材质

    1. csBuild.addQueue().addDispatch(x, y, z, rtMat);

平台支持

特性支持

WebGLWebGL2VulkanMetalGLES3GLES2
支持情况NNYYY(3.1)N

可通过 device.hasFeature(gfx.Feature.COMPUTE_SHADER) 查询。

约束限制

  • maxComputeSharedMemorySize: 本地共享内存的最大字节数。
  • maxComputeWorkGroupInvocations: 一个 WorkGroup 内的最大调用次数,即 Local WorkGroup 体积
  • maxComputeWorkGroupSize: Local WorkGroup 三维数组限制
  • maxComputeWorkGroupCount: Dispatch 三维数组限制

可通过 device.capabilities 查询。

平台差异

Cocos Creator 会将 Cocos CS 转化为平台对应版本的 glsl shader,因此为了能够保证各个平台的兼容性,需要尽量满足所有平台的限制要求,包括:

  1. Vulkan 与 GLES 要求显式标明 Storage Image 的 Format 标识符,详细参考 GLSE 语法标准。
  2. GLES 要求显式标明 Stroage 资源的 Memory 标识符,目前仅支持 readonly 与 writeonly。此外需要显式标识默认精度。

优化建议

  1. 进行屏幕空间图像后处理时,可优先考虑 Fragment Shader。避免 RenderTarget 结果中途切换 CS 进行写操作。
  2. 避免使用较大的工作组,尤其在使用 shared memory 情况下,每个 WorkGroup 大小建议不超过 64。

示例

下文展示了一个通过 ComputePass 实现的单个球 1 rpp 的简单光线追踪演示代码,使用到了 UniformBuffer,ImageSampler 与 StorageImage,其中 Effect Pass 声明部分如下:

  1. techniques:
  2. - name: opaque
  3. passes:
  4. - compute: compute-main
  5. pass: user-ray-tracing
  6. properties: &props
  7. mainTexture: { value: grey }

compute-main 实现部分如下:

  1. precision highp float;
  2. precision mediump image2D;
  3. layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in;
  4. #pragma rate tex batch
  5. uniform sampler2D tex;
  6. #pragma rate constants pass
  7. uniform constants {
  8. mat4 projectInverse;
  9. };
  10. #pragma rate outputImage pass
  11. layout (rgba8) writeonly uniform image2D outputImage;
  12. void main () {
  13. vec3 spherePos = vec3(0, 0, -5);
  14. vec3 lightPos = vec3(1, 1, -3);
  15. vec3 camPos = vec3(0, 0, 0);
  16. float sphereRadius = 1.0;
  17. vec4 color = vec4(0, 0, 0, 0);
  18. ivec2 screen = imageSize(outputImage);
  19. ivec2 coords = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y);
  20. vec2 uv = vec2(float(coords.x) / float(screen.x), float(coords.y) / float(screen.y));
  21. vec4 ndc = vec4(uv * 2.0 - vec2(1.0), 1.0, 1.0);
  22. vec4 pos = projectInverse * ndc;
  23. vec3 camD = vec3(pos.xyz / pos.w);
  24. vec3 rayL = normalize(camD - camPos);
  25. vec3 dirS = spherePos - camPos;
  26. vec3 rayS = normalize(dirS);
  27. float lenS = length(dirS);
  28. float dotLS = dot(rayL, rayS);
  29. float angle = acos(dotLS);
  30. float projDist = lenS * sin(angle);
  31. if (projDist < sphereRadius) {
  32. // intersection
  33. vec3 rayI = rayL * (lenS * dotLS - sqrt(sphereRadius * sphereRadius - projDist * projDist));
  34. vec3 N = normalize(rayI - dirS);
  35. vec3 L = normalize(lightPos - rayI);
  36. color = vec4(vec3(max(dot(N, L), 0.05)), 1.0);
  37. }
  38. imageStore(outputImage, coords, color);
  39. }

管线 API 调用如下:

  1. export function buildRayTracingComputePass(
  2. camera: renderer.scene.Camera,
  3. pipeline: rendering.Pipeline) {
  4. // 获取屏幕长宽
  5. const area = getRenderArea(camera,
  6. camera.window.width,
  7. camera.window.height);
  8. const width = area.width;
  9. const height = area.height;
  10. // 声明 RT Storage Image 资源
  11. const csOutput = 'rt_output';
  12. if (!pipeline.containsResource(csOutput)) {
  13. pipeline.addStorageTexture(csOutput,
  14. gfx.Format.RGBA8,
  15. width, height,
  16. rendering.ResourceResidency.MANAGED);
  17. } else {
  18. pipeline.updateStorageTexture(csOutput,
  19. width, height,
  20. gfx.Format.RGBA8);
  21. }
  22. // 声明 Compute Pass, layout 需要与 Effect 中 pass 字段保持一致
  23. const cs = pipeline.addComputePass('user-ray-tracing');
  24. // 更新相机投影参数
  25. cs.setMat4('projectInverse', camera.matProjInv);
  26. // 声明当前 Compute Pass 对 Storage Image 引用
  27. cs.addStorageImage(csOutput, rendering.AccessType.WRITE, 'outputImage');
  28. // 添加 Dispatch 参数, 绑定 Material
  29. cs.addQueue()
  30. .addDispatch(width / 8, height / 4, 1, rtMat);
  31. // 返回当前 Image 资源名,用于后续 Post Processing 处理
  32. return csOutput;
  33. }

Compute Pass 需要用户完成对 PerPass 资源的更新与绑定,PerBatch 的资源会由材质系统完成绑定,最终经过 FullScreen Blit 到屏幕的效果:

Cocos Effect