HDK
|
Houdini's RV library is a thin wrapper around Vulkan. It wraps many of the Vulkan objects in classes and handles most of the burden of sychronization and memory allocation. It is also meant to be somewhat close to Houdini's RE library for OpenGL.
The main components of RV are:
RV_Geometry
- defines all the necessary elements to draw geometry (vertex inputs, instancing, index buffers)RV_ShaderProgram
- defines a SPIR-V shader module, which is used to form a pipeline object used to render geometry.RV_VKImage
- defines a Vulkan Image, used for textures referenced by shaders.RV_VKBuffer
- defines a Vulkan buffer, used to back many different vulkan objects.RV_ShaderBlock
- defines a Uniform or Shared Shader Block, used to store shader parameters.RV_ShaderVariableSet
- defines a Descriptor Set, created from a pipeline layout of a shader, which contains references to shader blocks and images.RV_Instance
- The Vulkan Instance with many of the high level functions for managing Vulkan.RV_Render
- A per-thread wrapper around the Vulkan Instance and Device. RV_ShaderVariableSet
and RV_ShaderProgam
objects are bound to this object. The pipeline state is set on this object (blending, depth tests, etc).At a high level, your hook will be passed an RE_RenderContext
, which has a RV_Render
in it when Vulkan is active. This is the main object used to create geometry, shaders, textures, descriptor sets, and buffers. Before rendering is enabled, the geometry and any textures need to be created and uploaded. Once the shader is selected, descriptor sets can be generated from it, and the uniform buffers and textures are attached to it. The descriptor sets and shader are then bound to the RV_Render
, beginRendering()
is called, @ RV_Geometry draw calls are issued, and then endRendering()
finishes the cycle.
This section is not meant to be a primer on Vulkan. It is recommended that you have at least a high level knowledge of how Vulkan works before attempting to use the RV library. While the RV library hides the need to know the specific VK functions and enums needed to render and compute in Vulkan, having an idea of the shader pipeline, GLSL for Vulkan, and the various Vulkan objects is fairly necessary to write a Vulkan render hook.
Vulkan rendering differs from OpenGL in several aspects:
RV_ShaderBlock
); they can't be declared as individual entities.RV_ShaderVariableSet
), which are then bound to the RV_Render
object before drawing.RVdestroyVKPtr()
, defined in RV_Render.h, to schedule it for deletion after the command buffer has executed.GLSL can still be used with Vulkan. Houdini will compile GLSL files to SPIR-V, which Vulkan can ingest. The main difference between GLSL for OpenGL and GLSL for Vulkan is how resources are bound to the shader.
There is also no global state like in a GL context, though RV_Render can provide a temporary sense of state by setting the pipeline parameters, such as the blend and depth modes.
The first thing that generally needs to be done is to create some geometry to render. The RV_Geometry
class has all the methods to create, update, and draw geometry. It holds the vertex buffers and index buffers that define the primitives to be drawn. This process is similar to working with RE_Geometry
, but there is an extra step:
populateBuffers()
to optimally create buffers for each attribute.RV_Buffer
and call uploadBuffer()
with its data.When the geometry is ready to be drawn, call one of the draw()
methods. This must be done within a begin/endRendering() block or it will fail. Similarly, all of the above must be done outside a begin/endRendering() block or it will fail.
Later, when rendering, to draw the quad just do:
However, in order to see anything, a shader needs to be bound with its resources or nothing will happen.
Textures are backed by Images in Vulkan, and consist of an image and a sampler. The image defines the resolution, format, type, and data of the texture, while the sampler describes how it is accessed by the shader. RV Textures are a bit closer to how Vulkan sees tham than the high-level RV_Geometry
class. You make a RV_VKImageCreateInfo
with all the creation parameters for your image, pass that to RV_VKImage::create()
, and that allocates your Image, Image View (if needed), and Sampler. This makes creating cubemap or 2D array textures a bit easier than in straight Vulkan. Instead of needing to create a 2D texture with 6 layers and a cube image view, you just need to set the image type to RV_IMAGE_CUBE.
One important thing to note is that 3-channel textures are generally not supported by Vulkan drivers. OpenGL drivers would silently convert 3-channel textures to 4-channel; Vulkan does not. Promote these to 4-channel textures before creating them.
Compute shaders only have one stage (.comp) so it's often simpler to use the second method for them, though .prog files can be used for them as well.
Once you have the program loaded, you can then create any descriptor sets for its resources. A descriptor set is implemented by RV_ShaderVariableSet
, which points to the various uniform buffers and textures used by the shader. More than one set can be used; Houdini uses up to 5 for its shaders, and 8 is the usual maximum for most desktop implementations.
There is a GR_Utils
function to create the set for you:
Blocks and textures can be attached by either their binding index in the shader or their binding name. Binding by name is slightly safer, though in generate the shader and the code configuring the set and blocks needs to be kept in synch.
All shader parameters need to either go into a shader block or a push constant. This section will go over how to create and assign values to a shader block.
Shader blocks are usually created from a shader program:
You can also create the block without GR_Utils, by using:
...and attach it to the set using:
Once the buffer is created and attached to the set, you can update the contents whenever needed. To fill in the buffer's data, you can either fill the buffer directly from a structure (if you know the packing rules for UBOs and SSBOs) or on a per-uniform basic.
The memory given to the buffer is completely Uninitialized, so if a uniform is not assigned a value with one of the bind or fill methods, it will be undefined.
Push Constants are a fast way to set shader values without using buffers. They have very limited space (usually 128B-256B) and are not associated with any shader. Unlike buffer objects, push constants need to be updated with every draw, as some other draw may have modified them. This is a very specific optimization which should be the result of deliberate shader profiling.
To fetch the push constants, call RV_Render::getPushConstants()
. Push Constants use the same binding functions as shader blocks.