Compute Shader

Compute Shader (CS) is a programming model for executing general-purpose computing tasks on a GPU. Cocos Compute Shader inherits the syntax and built-in variables of glsl, and is added in the same way as rasterization Shaders, presented in effect form, and can only be used in custom render pipelines. Compute shader uses multiple threads to achieve parallel processing, making it highly efficient when dealing with large amounts of data.

Syntax

The definition method is the same as that of rasterization Shader shown below. Configuring PipelineStates under Computer Shader is meaningless.

  1. CCEffect %{
  2. techniques:
  3. - name: opaque
  4. passes:
  5. - compute: compute-main // shader entry
  6. pass: user-compute // pass layout name
  7. properties: &props
  8. mainTexture: { value: grey } // material properties
  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. }%

For more, please refer to Shader Syntax.

Input / Output

Compute Shader input and output consist of built-in input variables and Shader Resource variables.

The built-in input includes:

  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;

Shader Resource includes:

  • UniformBuffer
  • StorageBuffer
  • ImageSampler
  • StorageImage
  • SubpassInput

CS has no built-in output, and output can be achieved through StorageBuffer/Image.

Shader resource declaration

Compute shader currently supports resource binding at two frequencies: PerPass and PerBatch, as shown below:

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

PerPass resources can be defined as resources that require pipeline tracking to handle synchronization, while PerBatch resources are typically constant data or static textures that can be bound through Material.

The PerBatch mainTexture can be configured in the Material panel.

The PerPass outputImage needs to be declared in the pipeline and referenced by ComputePass, and the data read/write synchronization and ImageLayout management need to be managed by RenderGraph. Please see below for details.

Pipeline integration

Adding a Compute Shader in the Custom Render Pipeline involves three steps:

  1. 1.Add a Compute Pass, where passName is the Layout Name of the current Pass and must correspond to the pass field in the Effect.

    1. const csBuilder = pipeline.addComputePass('passName');
  2. Declare and reference resources, set access types and associate shader resources.

    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, // resource name
    13. rendering.AccessType.WRITE, // access type
    14. 'outputImage'); // shader resource name
  3. Add a dispatch call and set Compute material.

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

Cross-platform support

Feature

WebGLWebGL2VulkanMetalGLES3GLES2
supportNNYYY(3.1)N

It can be queried through device.hasFeature(gfx.Feature.COMPUTE_SHADER).

Limitation

  • maxComputeSharedMemorySize: maximum total shared storage size, in bytes.
  • maxComputeWorkGroupInvocations: maximum total number of compute shader invocations in a single local workgroup.
  • maxComputeWorkGroupSize: maximum size of a local compute workgroup.
  • maxComputeWorkGroupCount: maximum number of local workgroups that can be dispatched by a single dispatching command.

It can be queried through device.capabilities.

Platform-specific differences

Cocos Creator will convert the Cocos Compute Shader into platform-specific versions of GLSL shaders. Therefore, to ensure compatibility across different platforms, it is necessary to meet the limitation requirements of all platforms as much as possible, including:

  1. In Vulkan and GLES, it is required to explicitly specify the format identifier for Storage Image, according to the GLSL specification.
  2. GLES requires explicit specification of the Memory identifier for Storage resources, and currently only supports “readonly” and “writeonly”. In addition, default precision must be explicitly specified.

Best Practices

  1. When performing screen-space image post-processing, it is recommended to prioritize the use of Fragment Shader.
  2. It is recommended to avoid using large work groups, especially when using shared memory. The size of each work group should not exceed 64.

Sample Code

The following code demonstrates a simple ray tracing shader using a single sphere with 1 ray per pixel, implemented through ComputePass. It uses UniformBuffer, ImageSampler, and StorageImage.

Shader Pass Declaration:

  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 implement:

  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. }

On the API side, it is as follows:

  1. export function buildRayTracingComputePass(
  2. camera: renderer.scene.Camera,
  3. pipeline: rendering.Pipeline) {
  4. // Get the screen width and height.
  5. const area = getRenderArea(camera,
  6. camera.window.width,
  7. camera.window.height);
  8. const width = area.width;
  9. const height = area.height;
  10. // Declare the Storage Image resource.
  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. // Declare Compute Pass, the layout needs to be consistent
  23. const cs = pipeline.addComputePass('user-ray-tracing');
  24. // Update the camera projection parameters.
  25. cs.setMat4('projectInverse', camera.matProjInv);
  26. // Declare the reference of the Storage Image in the current Compute Pass.
  27. cs.addStorageImage(csOutput, rendering.AccessType.WRITE, 'outputImage');
  28. // Add Dispatch parameters and bind Material
  29. cs.addQueue()
  30. .addDispatch(width / 8, height / 4, 1, rtMat);
  31. // Return the name of the current Image resource, which will be used for subsequent Post Processing.
  32. return csOutput;
  33. }

Users need to update and bind PerPass resources in Compute Pass, while PerBatch resources will be bound by the material system. The final effect after presenting is as follows:

Cocos Effect