Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
3D Graphics Rendering Cookbook

You're reading from   3D Graphics Rendering Cookbook A comprehensive guide to exploring rendering algorithms in modern OpenGL and Vulkan

Arrow left icon
Product type Paperback
Published in Aug 2021
Publisher Packt
ISBN-13 9781838986193
Length 670 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Viktor Latypov Viktor Latypov
Author Profile Icon Viktor Latypov
Viktor Latypov
Sergey Kosarevsky Sergey Kosarevsky
Author Profile Icon Sergey Kosarevsky
Sergey Kosarevsky
Arrow right icon
View More author details
Toc

Table of Contents (12) Chapters Close

Preface 1. Chapter 1: Establishing a Build Environment 2. Chapter 2: Using Essential Libraries FREE CHAPTER 3. Chapter 3: Getting Started with OpenGL and Vulkan 4. Chapter 4: Adding User Interaction and Productivity Tools 5. Chapter 5: Working with Geometry Data 6. Chapter 6: Physically Based Rendering Using the glTF2 Shading Model 7. Chapter 7: Graphics Rendering Pipeline 8. Chapter 8: Image-Based Techniques 9. Chapter 9: Working with Scene Graphs 10. Chapter 10: Advanced Rendering Techniques and Optimizations 11. Other Books You May Enjoy

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.

  1. 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;
  2. 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 the result variable is a two-dimensional (2D) array whose elements are nothing else but pixels of an image. The writeonly 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;
  3. 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);
  4. 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, only x and y components matter. The calling code from the C++ side executes the vkCmdDispatch() function and passes the output image size as the X and Y numbers of local workgroups:
      vec2 uv = vec2(gl_GlobalInvocationID.xy) / dim;
  5. 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.

  1. 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)
  2. 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.

  3. Now, let's change the main() function of our compute shader to invoke mainImage() 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));
    }
  4. 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, and iResolution, 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 the iResolution value based on the actual size of our texture. We leave this as an exercise for the reader.

  5. 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:

Figure 6.2 – Using compute shaders to generate textures

Figure 6.2 – Using compute shaders to generate textures

In the next recipe, we will continue learning the Vulkan compute pipeline and implement a mesh-generation compute shader.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image