Polygonum is a low-level graphics engine (as a C++ library), that makes it much easier to render computer graphics with Vulkan®.
-
VulkanEnvironment
VulkanCore
IOmanager
SwapChain
RenderPipeline
Image
RenderPass
Subpass
-
Renderer
VulkanEnvironment
IOmanager
ModelData
Texture
Shader
LoadingWorker
UBO
TimerSet
It contains the most basic Vulkan elements necessary for rendering: Vulkan instance (VkInstance
), surface (VkSurfaceKHR
), physical device, (VkPhysicalDevice
), virtual device (VkDevice
), queues (VkQueue
), etc.
It contains elements necessary for a particular way of rendering and I/O management: VulkanCore
, command pool (VkCommandPool
), render pipeline (RenderPipeline
), swap chain (SwapChain
), I/O manager (IOmanager
).
It's an I/O manager for input (controls) and output (window) operations. It provides:
GLFWwindow
: Provides all functionality to this class.- Input methods (
getKey
,getMouseButton
,getCursorPos
,setInputMode
,pollEvents
,waitEvents
). - Input callbacks (
getYscrollOffset
,framebufferResizeCallback
,mouseScroll_callback
). - Window methods (
createWindowSurface
,getFramebufferSize
,getAspectRatio
,setWindowShouldClose
,getWindowShouldClose
,destroy
).
This class also serves as windowUserPointer
(pointer accessible from callbacks). Each window has a windowUserPointer
, which can be used for any purpose, and GLFW will not modify it throughout the life-time of the window.
It contains data relative to the swap chain:
VkSwapchainKHR
- Set of
VkImage
andVkImageView
. - Their format (
VkFormat
) and extent (VkExtent2D
).
It contains a set of RenderPass
objects. It's a PVF (Pure Virtual Function) that must be configured via overriding (i.e., by creating a subclass that overrides its virtual methods):
createRenderPass()
: Create all theRenderPass
objects. This is done by describing all the attachments (viaVkAttachmentDescription
andVkAttachmentReference
) for each subpass.createImageResources()
: Create all the attachments used (Image
objects). They can be members of the subclass.destroyAttachments()
: Destroy all attachments (Image
objects).
This class lets you create your own render pipeline such as:
- Forward shading
- Forward shading + post-processing
- Deferred shading (lighting pass + geometry pass)
- Deferred shading + forward shading + post-processing
- etc.
Image used as attachment in a render pass. One per render pass. It contains:
VkImage
VkDeviceMemory
VkImageView
: References a part of the image to be used (subset of its pixels). Required for being able to access it.VkSampler
: Images are accessed through image views rather than directly.
It contains data relative to a render pass:
VkRenderPass
- Set of
Subpass
objects. - Pointers to attachments (
VkImageView*
) of each subpass. - Framebuffer (
VkFramebuffer
) (i.e., set of attachments, including final image) of each subpass. - etc.
It contains basic subpass data:
- Pointers to input attachments (input images) (
Image*
). - Number of color attachments (output images).
It manages the graphics engine and serves as its interface. Calling renderLoop
we start the render loop:
createCommandBuffers
- Run worker in parallel (for adding and deleting models)
- Start render loop:
IOmanager::pollEvents
: Check for eventsdrawFrame
: Render a frame 1. Take a swap-chain image and use it as attachment in the framebuffer 2.update states
- User updates models data via callback
- Pass updated UBOs to the GPU
- Update command buffers, if necessary 3. Execute command buffers 4. Return the image to the swap chain for presentation
- Repeat
- Worker terminates
vkDeviceWaitIdle
: Wait forVkDevice
to finish operations (drawing and presentation).cleanup
Main contents:
-
Variables:
VulkanEnvironment
IOmanager
Timer
- Models (
ModelData
) - Textures (
Texture
) - Shaders (
Shader
) LoadingWorker
- Command buffers (
VkCommandBuffer
) - Semaphores and fences (
VkSemaphore
,VkFence
) - Global VS and FS UBOs (
UBO
) (max. number of VS UBOs == max. number of active instances)
-
Methods
-
renderLoop
: Create command buffer and start render loop. -
newModel
,deleteModel
,getModel
,setInstances
-
setMaxFPS
-
getIO
: GetIOmanager
object. -
getTimer
: GetTimer
object. -
getRendersCount
: Number of active instances of a certain model. -
getFrameCount
: Number of a certain frame. -
getFPS
: Number of Frames Per Second. -
getModelsCount
: Number of models inRenderer::models
. -
loadedShaders
: Number of shaders inRenderer::shaders
. -
loadedTextures
: Number of textures inRenderer::textures
. -
getCommandsCount
: Number of drawing commands sent to the command buffer. -
getMaxMemoryAllocationCount
: Max. number of valid memory objects. -
getMemAllocObjects
: Number of memory allocated objects (must be <=maxMemoryAllocationCount
) -
drawFrame
: Wait for command buffer execution, acquire a not-in-use image from swap chain (vkAcquireNextImageKHR
), update models data, execute command buffer with that image as attachment in the framebuffer (vkQueueSubmit
), and return the image to the swap chain for presentation (vkQueuePresentKHR
). If framebuffer is resized, we recreate the swap-chain. Each operation depends on the previous one finishing, so we synchronize swap chain events. -
updateStates
: Model data is updated (by user via callback), updated UBOs are passed to GPU, and command buffers are updated (if necessary). -
userUpdate
: Callback for user updates (void(*userUpdate) (Renderer& rend);
). Mainly used for updating UBOs and adding/removing models. -
createCommandBuffers
: Create one command buffer per swap-chain image.vkAllocateCommandBuffers
vkBeginCommandBuffer
/vkEndCommandBuffer
vkCmdBeginRenderPass
/vkCmdEndRenderPass
/vkCmdNextSubpass
vkCmdBindPipeline
,vkCmdBindVertexBuffers
,vkCmdBindIndexBuffer
,vkCmdBindDescriptorSets
vkCmdDraw
/vkCmdDrawIndexed
-
cleanup
: Method called after render loop terminates.vkQueueWaitIdle
vkFreeCommandBuffers
vkDestroySemaphore
/vkDestroyFence
- Clear models, textures, shaders
- Destroy global UBOs
VulkanEnvironment::cleanup
vkDestroyCommandPool
- Destroy render pipeline
- Destroy swap-chain
VulkanCore::destroy
vkDestroyDevice
DestroyDebugUtilsMessengerEXT
vkDestroySurfaceKHR
vkDestroyInstance
IOmanager::destroy
-
recreateSwapChain
: The window surface may change, making swap chain no longer compatible with it (example: window resizing). We catch this event, when acquiring/submitting an image from/to the swap chain, and recreate the swap chain. Used indrawFrame
ifvkAcquireNextImageKHR
(acquire swap-chain image) orvkQueuePresentKHR
(return image to swap-chain for presentation) output an error.Renderer::cleanupSwapChain
vkFreeCommandBuffers
ModelData::cleanup_Pipeline_Descriptors
(vkDestroyPipeline
,vkDestroyPipelineLayout
,UBO::destroyUBOs
,vkDestroyDescriptorPool
)VulkanEnvironment::cleanup_RenderPipeline_SwapChain
(RenderPipeline::destroy
,SwapChain::destroy
)
VulkanEnvironment::recreate_RenderPipeline_SwapChain
(createSwapChain
,createSwapChainImageViews
,RenderPipeline::createRenderPipeline
)ModelData::recreate_Pipeline_Descriptors
(createGraphicsPipeline
,createUBObuffers
,createDescriptorPool
,createDescriptorSets
)Renderer::createCommandBuffers
-
void createLightingPass(unsigned numLights, std::string vertShaderPath, std::string fragShaderPath, std::string fragToolsHeader); void updateLightingPass(glm::vec3& camPos, Light* lights, unsigned numLights); void createPostprocessingPass(std::string vertShaderPath, std::string fragShaderPath); void updatePostprocessingPass();
It manages time. Methods:
-
startTimer
: Start chronometer. -
updateTime
: Update time parameters (deltaTime and totalDeltaTime). -
reUpdateTime
: Re-update time parameters as if updateTime was not called before. -
getDeltaTime
: Get updated deltaTime. -
getTotalDeltaTime
: Get updated totalDeltaTime. -
getDate
: Get string with current date and time (example: Mon Jan 31 02:28:35 2022). -
Chrono methods
startTimer
: Start time counting for the chronometercomputeDeltaTime
: Get time (seconds) passed since last time you called this method.getDeltaTime
: <<< Returns time (seconds) increment between frames (deltaTime)getTime
: <<< Get time (seconds) since startTime when computeDeltaTime() was called
-
FPS control
-
getFPS
: Get FPS (updated in computeDeltaTime()) -
setMaxFPS
: Modify the current maximum FPS. Set it to 0 to deactivate FPS control. -
getMaxPossibleFPS
: Get the maximum possible FPS you can get (if we haven't set max FPS, maxPossibleFPS == FPS)// Frame counting size_t getFrameCounter(); ///< Get number of calls made to computeDeltaTime()
// Bonus methods (less used) long double getTimeNow(); ///< Returns time (seconds) since startTime, at the moment of calling GetTimeNow() std::string getDate(); ///< Get a string with date and time (example: Mon Jan 31 02:28:35 2022)
-
- Render any primitive: Points, Lines, Triangles
- 2D and 3D rendering
- Load models (raw data or OBJ files)
- Load textures to pool (any model can use any texture from the pool)
- Define Vertex and Fragment shaders
- Multiple renderings of the same object in the scene (modifiable at any time)
- Load/delete models at any time
- Load/delete textures at any time
- Parallel thread for loading/deleting models and textures (avoids bottlenecks at the render loop)
- Camera system (navigate through the scene)
- Input system
- Multiple layers (Painter's algorithm)
- Define content of the vertex UBO and the fragment UBO
- Allows transparencies (alpha channel)
Disclaimer: The following content is outdated, but it will updated soon. This is a work in progress.
- projects: Contains Polygonum and some example projects.
- Renderer: Polygonum headers and source files.
- shaders Shaders used (vertex & fragment).
- environment: Creates and configures the core Vulkan environment.
- renderer: Uses the Vulkan environment for rendering the models provided by the user.
- models: The user loads his models to Polygonum through the ModelData class.
- input: Manages user input (keyboard, mouse...) and delivers it to Polygonum.
- camera: Camera system.
- timer: Time data.
- data: Bonus functions (model matrix computation...).
- main: Examples of how to use Polygonum.
- extern: Dependencies (GLFW, GLM, stb_image, tinyobjloader...).
- files: Scripts and images.
- models: Models for loading in our projects.
- textures: Images used as textures in our projects.
Start by creating a Renderer
object: Pass a callback of the form void callback(Renderer& r)
as an argument. This callback will be run each frame. It can be used by the user for updating the Uniform Buffer Objects (model matrices, etc.), loading new models, deleting already loaded models, or modifying the number of renderings for each model. All this actions can be performed inside or outside the callback, but always after the creation of the Renderer
object.
Renderer app(callback);
Loading a model: There are different ways of loading a model.
- Specify the number of renders and the paths for the model, texture and shaders (vertex and fragment):
modelIterator modelIter = app.newModel(
3,
"../models/model.obj",
"../models/texture.png",
"../shadrs/vertexShader.spv",
"../shadrs/fragmentShader.spv" );
- Specify the number of renders, a vector of vertex data (vector), a vector of indices (vector<uint32_t>), and the paths for the texture and shaders (vertex and fragment):
modelIterator modelIter = app.newModel(
3,
vertex,
indices,
"../models/texture.png",
"../shadrs/vertexShader.spv",
"../shadrs/fragmentShader.spv" );
Deleting a model:
r.deleteModel(modelIter);
Modifying the number of renders of a model:
r.setRenders(modelIter, 5);
For efficiency, it is good practice to load a model by specifying the maximum number of renderings you will create from it, and during rendering you can modify the number of renders with addRender()
. Why? Because the internal buffer of UBOs of that model needs to be destroyed and created again each time a bigger buffer requires allocation.
For start running the render loop, call run()
:
app.run();
- GLFW (Window system and inputs) (Link)
- GLM (Mathematics library) (Link)
- stb_image (Image loader) (Link)
- tinyobjloader (Load vertices and faces from an OBJ file) (Link)
- bullet3 (Physics) (Link)
- Vulkan SDK (Link) (Set of repositories useful for Vulkan development) (installed in platform-specfic directories)
- Vulkan loader (Khronos)
- Vulkan validation layer (Khronos)
- Vulkan extension layer (Khronos)
- Vulkan tools (Khronos)
- Vulkan tools (LunarG)
- gfxreconstruct (LunarG)
- glslang (Shader compiler to SPIR-V) (Khronos)
- shaderc (C++ API wrapper around glslang) (Google)
- SPIRV-Tools (Khronos)
- SPIRV-Cross (Khronos)
- SPIRV-Reflect (Khronos)
The following includes the basics for setting up Vulkan and this project. For more details about setting up Vulkan, check Setting up Vulkan.
- Update your GPU's drivers
- Get:
- Compiler that supports C++17
- Make
- CMake
- Install Vulkan SDK
- Download tarball in
extern\
. - Run script
./files/install_vulkansdk
(modifypathVulkanSDK
variable if necessary)
- Download tarball in
- Build project using the scripts:
sudo ./files/Ubuntu/1_build_dependencies_ubuntu
./files/Ubuntu/2_build_project_ubuntu
- Update your GPU's drivers
- Install Vulkan SDK
- Download installer wherever
- Execute installer
- Get:
- MVS
- CMake
- Build project using the scripts:
1_build_dependencies_Win.bat
2_build_project_Win.bat
- Compile project with MVS (Set as startup project & Release mode) (first glfw and glm_static, then the Vulkan projects)
- Copy some project from /project and paste it there
- Include it in /project/CMakeLists.txt
- Modify project name in copiedProject/CMakeLists.txt
- Modify in-code shaders paths, if required
The Renderer class manages the ModelData objects. Both require a VulkanEnvironment object (the same one).
- VulkanEnvironment: Contains the common elements required by Renderer and ModelData (GLFWwindow, VkInstance, VkSurfaceKHR, VkPhysicalDevice, VkDevice, VkQueues, VkSwapchainKHR, swapchain VkImages, swapchain VkFramebuffer, VkRenderPass, VkCommandPool, multisampled color buffer (VkImage), depth buffer (VkImage), ...).
- ModelData: Contains the data about some mesh (model), a reference to the VkEnvironment object used, VkPipeline, texture VkImage, texture VkSampler, vector of vertex, vector of indices, vertex VkBuffer, index VkBuffer, uniform VkBuffer, VkDescriptorPool, VkDescriptorSet, etc. The mesh data includes vertex (vertices, color, texture coordinates), indices, and paths to shaders and model data (vertices, color, texture coordinates, indices).
- Renderer: Contains all the ModelData objects, and the VkEnvironment object used. Also contains the VkCommandBuffer, Input, TimerSet, a second thread, some lists for pending tasks (load model, delete model, set renders for a model), semaphores, etc.
When the user
- loads a model (newModel), a ModelData object is partially constructed and stored in a list of "models pending full construction".
- deletes a model (deleteModel), it is annotated for deletion in a list of "models pending deletion".
- changes the number of renders of some model (setRenders), it is annotated for modification in a list of "models pending changing number of renders".
A secondary thread starts running in parallel just after the creation of a Renderer object. Such thread is continuously looking into these 3 lists. If a pending task is found, it is executed in this thread (so, it's done in parallel): fully construction of a ModelData, destroying a ModelData, or modifying the number of renders of some model. All these operations require modifying the command buffer. Different semaphores control access to different lists, the command buffer, and some common parts of code.
Models with 0 renders have a descriptor set (like every model) but, since it is supposed to render nothing, the commmand for rendering it is not passed to the command buffer.