Generating textures in Vulkan using compute shaders
Now that we can initialize and use compute shaders, it is time to give a few examples of how to use these. Let's start with some basic procedural texture generation. In this recipe, we implement a small program to display animated textures whose pixel values are calculated in real time inside our custom compute shader. To add even more value to this recipe, we will port a GLSL shader from https://www.shadertoy.com to our Vulkan compute shader.
Getting ready
The compute pipeline creation code and Vulkan application initialization are the same as in the Initializing compute shaders in Vulkan recipe. Make sure you read this before proceeding further. To use and display the generated texture, we need a textured quad renderer. Its complete source code can be found in shared/vkRenderers/VulkanSingleQuad.cpp
. We will not focus on its internals here because, at this point, it should be easy for you to implement such a renderer on your own using the material of the previous chapters. One of the simplest ways to do so would be to modify the ModelRenderer
class from shared/vkRenderers/VulkanModelRenderer.cpp
and fill the appropriate index and vertex buffers in the class constructor.
The original Industrial Complex shader that we are going to use here to generate a Vulkan texture was created by Gary "Shane" Warne (https://www.shadertoy.com/user/Shane) and can be downloaded from ShaderToy at https://www.shadertoy.com/view/MtdSWS.
How to do it...
Let's start by discussing the process of writing a texture-generating GLSL compute shader. The simplest shader to generate a red-green-blue-alpha (RGBA) image without using any input data outputs an image by using the gl_GlobalInvocationID
built-in variable to know which pixel to output. This maps directly to how ShaderToy shaders operate, thus we can transform them into a compute shader just by adding some input and output (I/O) parameters and layout modifiers specific to compute shaders and Vulkan. Let's take a look at a minimalistic compute shader that creates a red-green gradient texture.
- As in all other compute shaders, one mandatory line at the beginning tells the driver how to distribute the workload on the GPU. In our case, we are processing tiles of 16x16 pixels:
layout (local_size_x = 16, local_size_y = 16) in;
- The only buffer binding that we need to specify is the output image. This is the first time we have used the
image2D
image type in this book. Here, it means that theresult
variable is a two-dimensional (2D) array whose elements are nothing else but pixels of an image. Thewriteonly
layout qualifier instructs the compiler to assume we will not read from this image in the shader:layout (binding = 0, rgba8) uniform writeonly image2D result;
- The GLSL compute shading language provides a set of helper functions to retrieve various image attributes. We use the built-in
imageSize()
function to determine the size of an image in pixels:void main() { ivec2 dim = imageSize(result);
- The
gl_GlobalInvocationID
built-in variable tells us which global element of our compute grid we are processing. To convert this value into 2D image coordinates, we divide it by the image size. As we are dealing with 2D textures, onlyx
andy
components matter. The calling code from the C++ side executes thevkCmdDispatch()
function and passes the output image size as theX
andY
numbers of local workgroups:vec2 uv = vec2(gl_GlobalInvocationID.xy) / dim;
- The actual real work we do in this shader is to call the
imageStore()
GLSL function:imageStore(result, ivec2(gl_GlobalInvocationID.xy), vec4(uv, 0.0, 1.0)); }
Now, the preceding example is rather limited, and all you get is a red-and-green gradient image. Let's change it a little bit to use the actual shader code from ShaderToy. The compute shader that renders a Vulkan version of the Industrial Complex shader from ShaderToy, available via the following Uniform Resource Locator (URL), https://shadertoy.com/view/MtdSWS, can be found in the shaders/chapter06/VK03_compute_texture.comp
file.
- First, let's copy the entire original ShaderToy GLSL code into our new compute shader. There is a function called
mainImage()
in there that is declared as follows:void mainImage(out vec4 fragColor, in vec2 fragCoord)
- We should replace it with a function that returns a
vec4
color instead of storing it in the output parameter:vec4 mainImage(in vec2 fragCoord)
Don't forget to add an appropriate
return
statement at the end. - Now, let's change the
main()
function of our compute shader to invokemainImage()
properly. It is a pretty neat trick:void main() { ivec2 dim = imageSize(result); vec2 uv = vec2(gl_GlobalInvocationID.xy) / dim; imageStore(result, ivec2(gl_GlobalInvocationID.xy), mainImage(uv*dim)); }
- There is still one issue that needs to be resolved before we can run this code. The ShaderToy code uses two custom input variables,
iTime
for the elapsed time, andiResolution
, which contains the size of the resulting image. To avoid any search and replace in the original GLSL code, we mimic these variables, one as a push constant, and the other with a hardcoded value for simplicity:layout(push_constant) uniform uPushConstant { float time; } pc; vec2 iResolution = vec2( 1280.0, 720.0 ); float iTime = pc.time;
Important note
The GLSL
imageSize()
function can be used to obtain theiResolution
value based on the actual size of our texture. We leave this as an exercise for the reader. - The C++ code is rather short and consists of invoking the aforementioned compute shader, inserting a Vulkan pipeline barrier, and rendering a texture quad. The pipeline barrier that ensures the compute shader finishes before texture sampling happens can be created in the following way:
void insertComputedImageBarrier( VkCommandBuffer commandBuffer, VkImage image) { const VkImageMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, .oldLayout = VK_IMAGE_LAYOUT_GENERAL, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .image = image, .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); }
The running application should render an image like the one shown in the following screenshot, which is similar to the output of https://www.shadertoy.com/view/MtdSWS:
In the next recipe, we will continue learning the Vulkan compute pipeline and implement a mesh-generation compute shader.