Technical Documentation: SaschaWillems/Vulkan
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (`README.md`)
Vulkan C++ examples and demos
A comprehensive collection of open source C++ examples for Vulkanยฎ, the low-level graphics and compute API from Khronos.
Table of Contents
- Official Khronos Vulkan Samples
- Cloning
- Assets
- Building
- Running
- Shaders
- A note on synchronization
- Examples
- Credits and Attributions
How to Vulkan in 2026
For an introduction on how to use Vulkan for graphics in 2026, see this repository. It's esp. helpful if you haven't done much with Vulkan (yet) and want to understand how Vulkan (and in turn these samples) work.
Official Khronos Vulkan Samples
Khronos has made an official Vulkan Samples repository available to the public (press release).
You can find this repository at https://github.com/KhronosGroup/Vulkan-Samples
As I've been involved with getting the official repository up and running, I'll be mostly contributing to that repository from now, but may still add samples that don't fit there in here and I'll of course continue to maintain these samples.
Cloning
This repository contains submodules for external dependencies and assets, so when doing a fresh clone you need to clone recursively:
git clone --recursive https://github.com/SaschaWillems/Vulkan.gitExisting repositories can be updated manually:
git submodule init
git submodule updateBuilding
The repository contains everything required to compile and build the examples on Windows, Linux, Android, iOS, macOS and OpenHarmony using a C++ compiler that supports C++20.
See BUILD.md for details on how to build for the different platforms.
Running
Once built, examples can be run from the bin directory. The list of available command line options can be brought up with --help:
--help: Show help
-h, --height: Set window height
-v, --validation: Enable validation layers
-vs, --vsync: Enable V-Sync
-f, --fullscreen: Start in fullscreen mode
-w, --width: Set window width
-s, --shaders: Select shader type to use (glsl, slang, hlsl)
-g, --gpu: Select GPU to run on
-gl, --listgpus: Display a list of available Vulkan devices
-b, --benchmark: Run example in benchmark mode
-bw, --benchwarmup: Set warmup time for benchmark mode in seconds
-br, --benchruntime: Set duration time for benchmark mode in seconds
-bf, --benchfilename: Set file name for benchmark results
-bt, --benchframetimes: Save frame times to benchmark results file
-bfs, --benchmarkframes: Only render the given number of frames
-rp, --resourcepath: Set path for dir where assets and shaders folder is presentNote that some examples require specific device features, and if you are on a multi-gpu system you might need to use the -gl and -g to select a gpu that supports them.
Shaders
Vulkan consumes shaders in an intermediate representation called SPIR-V. This makes it possible to use different shader languages by compiling them to that bytecode format. The primary shader language used here is GLSL, most samples also come with slang and HLSL shader sources, making it easy to compare the differences between those shading languages. The Rust GPU project maintains Rust shader sources in a separate repo.
Examples
Basics
Basic triangle using Vulkan 1.0
Basic and verbose example for getting a colored triangle rendered to the screen using Vulkan. This is meant as a starting point for learning Vulkan from the ground up. A huge part of the code is boilerplate that is abstracted away in later examples.
Basic triangle using Vulkan 1.3
Vulkan 1.3 version of the basic and verbose example for getting a colored triangle rendered to the screen. This makes use of features like dynamic rendering simplifying api usage.
-
Using pipeline state objects (pso) that bake state information (rasterization states, culling modes, etc.) along with the shaders into a single object, making it easy for an implementation to optimize usage (compared to OpenGL's dynamic state machine). Also demonstrates the use of pipeline derivatives.
-
Descriptors are used to pass data to shader binding points. Sets up descriptor sets, layouts, pools, creates a single pipeline based on the set layout and renders multiple objects with different descriptor sets.
-
Dynamic uniform buffers are used for rendering multiple objects with multiple matrices stored in a single uniform buffer object. Individual matrices are dynamically addressed upon descriptor binding time, minimizing the number of required descriptor sets.
-
Uses push constants, small blocks of uniform data stored within a command buffer, to pass data to a shader without the need for uniform buffers.
-
Uses SPIR-V specialization constants to create multiple pipelines with different lighting paths from a single "uber" shader.
-
Loads a 2D texture from disk (including all mip levels), uses staging to upload it into video memory and samples from it using combined image samplers.
-
Loads a 2D texture array containing multiple 2D texture slices (each with its own mip chain) and renders multiple meshes each sampling from a different layer of the texture. 2D texture arrays don't do any interpolation between the slices.
-
Loads a cube map texture from disk containing six different faces. All faces and mip levels are uploaded into video memory, and the cubemap is displayed on a skybox as a backdrop and on a 3D model as a reflection.
-
Loads an array of cube map textures from a single file. All cube maps are uploaded into video memory with their faces and mip levels, and the selected cubemap is displayed on a skybox as a backdrop and on a 3D model as a reflection.
-
Generates a 3D texture on the cpu (using perlin noise), uploads it to the device and samples it to render an animation. 3D textures store volumetric data and interpolate in all three dimensions.
-
Uses input attachments to read framebuffer contents from a previous sub pass at the same pixel position within a single render pass. This can be used for basic post processing or image composition (blog entry).
-
Advanced example that uses sub passes and input attachments to write and read back data from framebuffer attachments (same location only) in single render pass. This is used to implement deferred render composition with added forward transparency in a single pass.
-
Basic offscreen rendering in two passes. First pass renders the mirrored scene to a separate framebuffer with color and depth attachments, second pass samples from that color attachment for rendering a mirror surface.
-
Implements a simple CPU based particle system. Particle data is stored in host memory, updated on the CPU per-frame and synchronized with the device before it's rendered using pre-multiplied alpha.
-
Uses the stencil buffer and its compare functionality for rendering a 3D model with dynamic outlines.
-
Demonstrates two different ways of passing vertices to the vertex shader using either interleaved or separate vertex attributes.
glTF
These samples show how implement different features of the glTF 2.0 3D format 3D transmission file format in detail.
glTF model loading and rendering
Shows how to load a complete scene from a glTF 2.0 file. The structure of the glTF 2.0 scene is converted into the data structures required to render the scene with Vulkan.
-
Demonstrates how to do GPU vertex skinning from animation data stored in a glTF 2.0 model. Along with reading all the data structures required for doing vertex skinning, the sample also shows how to upload animation data to the GPU and how to render it using shaders.
-
Renders a complete scene loaded from an glTF 2.0 file. The sample is based on the glTF model loading sample, and adds data structures, functions and shaders required to render a more complex scene using Crytek's Sponza model with per-material pipelines and normal mapping.
Advanced
-
Implements multisample anti-aliasing (MSAA) using a renderpass with multisampled attachments and resolve attachments that get resolved into the visible frame buffer.
Alpha to coverage with multi sampling
Uses multisampling with alpha to coverage to implement an order-independent way of rendering overlapping transparent objects.
-
Implements a high dynamic range rendering pipeline using 16/32 bit floating point precision for all internal formats, textures and calculations, including a bloom pass, manual exposure and tone mapping.
-
Rendering shadows for a directional light source. First pass stores depth values from the light's pov, second pass compares against these to check if a fragment is shadowed. Uses depth bias to avoid shadow artifacts and applies a PCF filter to smooth shadow edges.
-
Uses multiple shadow maps (stored as a layered texture) to increase shadow resolution for larger scenes. The camera frustum is split up into multiple cascades with corresponding layers in the shadow map. Layer selection for shadowing depth compare is then done by comparing fragment depth with the cascades' depths ranges.
Omnidirectional shadow mapping
Uses a dynamic floating point cube map to implement shadowing for a point light source that casts shadows in all directions. The cube map is updated every frame and stores distance to the light source for each fragment used to determine if a fragment is shadowed.
-
Generating a complete mip-chain at runtime instead of loading it from a file, by blitting from one mip level, starting with the actual texture image, down to the next smaller size until the lower 1x1 pixel end of the mip chain.
-
Capturing and saving an image after a scene has been rendered using blits to copy the last swapchain image from optimal device to host local linear memory, so that it can be stored into a ppm image.
Order Independent Transparency
Implements order independent transparency based on linked lists. To achieve this, the sample uses storage buffers in combination with image load and store atomic operations in the fragment shader.
Performance
Multi threaded command buffer generation
Multi threaded parallel command buffer generation. Instead of prebuilding and reusing the same command buffers this sample uses multiple hardware threads to demonstrate parallel per-frame recreation of secondary command buffers that are executed and submitted in a primary buffer once all threads have finished.
-
Uses the instancing feature for rendering many instances of the same mesh from a single vertex buffer with variable parameters and textures (indexing a layered texture). Instanced data is passed using a secondary vertex buffer.
-
Rendering thousands of instanced objects with different geometry using one single indirect draw call instead of issuing separate draws. All draw commands to be executed are stored in a dedicated indirect draw buffer object (storing index count, offset, instance count, etc.) that is uploaded to the device and sourced by the indirect draw command for rendering.
-
Using query pool objects to get number of passed samples for rendered primitives got determining on-screen visibility.
-
Using query pool objects to gather statistics from different stages of the pipeline like vertex, fragment shader and tessellation evaluation shader invocations depending on payload.
Physically Based Rendering
Physical based rendering as a lighting technique that achieves a more realistic and dynamic look by applying approximations of bidirectional reflectance distribution functions based on measured real-world material parameters and environment lighting.
-
Demonstrates a basic specular BRDF implementation with solid materials and fixed light sources on a grid of objects with varying material parameters, demonstrating how metallic reflectance and surface roughness affect the appearance of pbr lit objects.
-
Adds image based lighting from an hdr environment cubemap to the PBR equation, using the surrounding environment as the light source. This adds an even more realistic look the scene as the light contribution used by the materials is now controlled by the environment. Also shows how to generate the BRDF 2D-LUT and irradiance and filtered cube maps from the environment map.
-
Renders a model specially crafted for a metallic-roughness PBR workflow with textures defining material parameters for the PRB equation (albedo, metallic, roughness, baked ambient occlusion, normal maps) in an image based lighting environment.
Deferred
These examples use a deferred shading setup.
-
Uses multiple render targets to fill all attachments (albedo, normals, position, depth) required for a G-Buffer in a single pass. A deferred pass then uses these to calculate shading and lighting in screen space, so that calculations only have to be done for visible fragments independent of no. of lights.
-
Adds multi sampling to a deferred renderer using manual resolve in the fragment shader.
Deferred shading shadow mapping
Adds shadows from multiple spotlights to a deferred renderer using a layered depth attachment filled in one pass using multiple geometry shader invocations.
Screen space ambient occlusion
Adds ambient occlusion in screen space to a 3D scene. Depth values from a previous deferred pass are used to generate an ambient occlusion texture that is blurred before being applied to the scene in a final composition path.
Compute Shader
All Vulkan implementations support compute shaders, a more generalized way of doing workloads on the GPU. These samples demonstrate how to use those compute shaders.
-
Uses a compute shader along with a separate compute queue to apply different convolution kernels (and effects) on an input image in realtime.
-
Attraction based 2D GPU particle system using compute shaders. Particle data is stored in a shader storage buffer and only modified on the GPU using memory barriers for synchronizing compute particle updates with graphics pipeline vertex access.
-
N-body simulation based particle system with multiple attractors and particle-to-particle interaction using two passes separating particle movement calculation and final integration. Shared compute shader memory is used to speed up compute calculations.
-
Simple GPU ray tracer with shadows and reflections using a compute shader. No scene geometry is rendered in the graphics pass.
-
Mass-spring based cloth system on the GPU using a compute shader to calculate and integrate spring forces, also implementing basic collision with a fixed scene object.
-
Purely GPU based frustum visibility culling and level-of-detail system. A compute shader is used to modify draw commands stored in an indirect draw commands buffer to toggle model visibility and select its level-of-detail based on camera distance, no calculations have to be done on and synced with the CPU.
Geometry Shader
-
Visualizing per-vertex model normals (for debugging). First pass renders the plain model, second pass uses a geometry shader to generate colored lines based on per-vertex model normals,
-
Renders a scene to multiple viewports in one pass using a geometry shader to apply different matrices per viewport to simulate stereoscopic rendering (left/right). Requires a device with support for
multiViewport.
Tessellation Shader
-
Uses a height map to dynamically generate and displace additional geometric detail for a low-poly mesh.
-
Renders a terrain using tessellation shaders for height displacement (based on a 16-bit height map), dynamic level-of-detail (based on triangle screen space size) and per-patch frustum culling.
-
Uses curved PN-triangles (paper) for adding details to a low-polygon model.
Hardware accelerated ray tracing
Vulkan supports GPUs with dedicated hardware for ray tracing. These sampples show different parts of that functionality.
-
Basic example for doing hardware accelerated ray tracing using the
VK_KHR_acceleration_structureandVK_KHR_ray_tracing_pipelineextensions. Shows how to setup acceleration structures, ray tracing pipelines and the shader binding table needed to do the actual ray tracing. -
Adds ray traced shadows casting using the new ray tracing extensions to a more complex scene. Shows how to add multiple hit and miss shaders and how to modify existing shaders to add shadow calculations.
-
Renders a complex scene with reflective surfaces using the new ray tracing extensions. Shows how to do recursion inside of the ray tracing shaders for implementing real time reflections.
-
Renders a texture mapped quad with transparency using the new ray tracing extensions. Shows how to do texture mapping in a closes hit shader, how to cancel intersections for transparency in an any hit shader and how to access mesh data in those shaders using buffer device addresses.
-
Callable shaders can be dynamically invoked from within other ray tracing shaders to execute different shaders based on dynamic conditions. The example ray traces multiple geometries, with each calling a different callable shader from the closest hit shader.
Ray tracing intersection shaders
Uses an intersection shader for procedural geometry. Instead of using actual geometry, this sample on passes bounding boxes and object definitions. An intersection shader is then used to trace against the procedural objects.
-
Renders a textured glTF model using ray traying instead of rasterization. Makes use of frame accumulation for transparency and anti aliasing.
-
Ray queries add acceleration structure intersection functionality to non ray tracing shader stages. This allows for combining ray tracing with rasterization. This example makes uses ray queries to add ray casted shadows to a rasterized sample in the fragment shader.
-
Uses the
VK_KHR_ray_tracing_position_fetchextension to fetch vertex position data from the acceleration structure from within a shader, instead of having to manually unpack vertex information.
Headless
Examples that run one-time tasks and don't make use of visual output (no window system integration). These can be run in environments where no user interface is available (blog entry).
-
Renders a basic scene to a (non-visible) frame buffer attachment, reads it back to host memory and stores it to disk without any on-screen presentation, showing proper use of memory barriers required for device to host image synchronization.
-
Only uses compute shader capabilities for running calculations on an input data set (passed via SSBO). A fibonacci row is calculated based on input data via the compute shader, stored back and displayed via command line.
User Interface
-
Load and render a 2D text overlay created from the bitmap glyph data of a stb font file. This data is uploaded as a texture and used for displaying text on top of a 3D scene in a second pass.
-
Uses a texture that stores signed distance field information per character along with a special fragment shader calculating output based on that distance data. This results in crisp high quality font rendering independent of font size and scale.
-
Generates and renders a complex user interface with multiple windows, controls and user interaction on top of a 3D scene. The UI is generated using Dear ImGUI and updated each frame.
Extensions
Vulkan is an extensible api with lots of functionality added by extensions. These samples demonstrate the usage of such extensions.
Note: Certain extensions may become core functionality for newer Vulkan versions. The samples will still work with these.
Conservative rasterization -
VK_EXT_conservative_rasterizationUses conservative rasterization to change the way fragments are generated by the gpu. The example enables overestimation to generate fragments for every pixel touched instead of only pixels that are fully covered (blog post).
Push descriptors -
VK_KHR_push_descriptorUses push descriptors apply the push constants concept to descriptor sets. Instead of creating per-object descriptor sets for rendering multiple objects, this example passes descriptors at command buffer creation time.
Inline uniform blocks -
VK_EXT_inline_uniform_blockMakes use of inline uniform blocks to pass uniform data directly at descriptor set creation time and also demonstrates how to update data for those descriptors at runtime.
Multiview rendering -
VK_KHR_multiviewRenders a scene to to multiple views (layers) of a single framebuffer to simulate stereoscopic rendering in one pass. Broadcasting to the views is done in the vertex shader using
gl_ViewIndex.Conditional rendering -
VK_EXT_conditional_renderingDemonstrates the use of VK_EXT_conditional_rendering to conditionally dispatch render commands based on values from a dedicated buffer. This allows e.g. visibility toggles without having to rebuild command buffers (blog post).
Debug shader printf -
VK_KHR_shader_non_semantic_infoShows how to use printf in a shader to output additional information per invocation. This information can help debugging shader related issues in tools like RenderDoc.
Note:: This sample should be run from a graphics debugger like RenderDoc.
Debug utils -
VK_EXT_debug_utilsShows how to use debug utils for adding labels and colors to Vulkan objects for graphics debuggers. This information helps to identify resources in tools like RenderDoc.
Note: This sample should be run from a graphics debugger like RenderDoc.
Negative viewport height -
VK_KHR_Maintenance1orVulkan 1.1Shows how to render a scene using a negative viewport height, making the Vulkan render setup more similar to other APIs like OpenGL. Also has several options for changing relevant pipeline state, and displaying meshes with OpenGL or Vulkan style coordinates. Details can be found in this tutorial.
Variable rate shading -
VK_KHR_fragment_shading_rateUses a special image that contains variable shading rates to vary the number of fragment shader invocations across the framebuffer. This makes it possible to lower fragment shader invocations for less important/less noisy parts of the framebuffer.
Descriptor indexing -
VK_EXT_descriptor_indexingDemonstrates the use of VK_EXT_descriptor_indexing for creating descriptor sets with a variable size that can be dynamically indexed in a shader using
GL_EXT_nonuniform_qualifierandSPV_EXT_descriptor_indexing.Dynamic rendering -
VK_KHR_dynamic_renderingShows usage of the VK_KHR_dynamic_rendering extension, which simplifies the rendering setup by no longer requiring render pass objects or framebuffers.
Dynamic rendering local read -
VK_KHR_dynamic_rendering_local_readShows usage of the VK_KHR_dynamic_rendering extension in combination with VK_KHR_dynamic_rendering_local_read to replace render passes and sub passes. Local read ensures that reads stay local on tile memory.
Dynamic rendering with multi sampling -
VK_KHR_dynamic_renderingBased on the dynamic rendering sample, this sample shows how to do implement multi sampling with dynamic rendering.
Graphics pipeline library -
VK_EXT_graphics_pipeline_libraryUses the graphics pipeline library extensions to improve run-time pipeline creation. Instead of creating the whole pipeline at once, this sample pre builds shared pipeline parts like like vertex input state and fragment output state. These are then used to create full pipelines at runtime, reducing build times and possible hick-ups.
Mesh shaders -
VK_EXT_mesh_shaderBasic sample demonstrating how to use the mesh shading pipeline as a replacement for the traditional vertex pipeline.
Descriptor heap -
VK_EXT_descriptor_heapBasic sample showing how to use descriptor heaps, which fully replace Vulkan's original descriptor system. Khronos blog.
Descriptor heap (untyped pointers) -
VK_EXT_descriptor_heapandVK_KHR_shader_untyped_pointersBasic sample showing how to use descriptor heaps in combination with `VK_KHR_shader_untyped_pointersยด for a more direct way to access heap data.
Descriptor buffers -
VK_EXT_descriptor_bufferBasic sample showing how to use descriptor buffers to replace descriptor sets.
Note: Descriptor buffers have been superseded by descriptor heaps.
Shader objects -
VK_EXT_shader_objectBasic sample showing how to use shader objects that can be used to replace pipeline state objects. Instead of baking all state in a PSO, shaders are explicitly loaded and bound as separate objects and state is set using dynamic state extensions. The sample also stores binary shader objets and loads them on consecutive runs.
Host image copy -
VK_EXT_host_image_copyShows how to do host image copies, which heavily simplify the host to device image process by fully skipping the staging process.
Buffer device address -
VK_KHR_buffer_device_addresDemonstrates the use of virtual GPU addresses to directly access buffer data in shader. Instead of e.g. using descriptors to access uniforms, with this extension you simply provide an address to the memory you want to read from in the shader and that address can be arbitrarily changed e.g. via a push constant.
Timeline semaphores -
VK_KHR_timeline_semaphoreShows how to use a new semaphore type that has a way of setting and identifying a given point on a timeline. Compared to the core binary semaphores, this simplifies synchronization as a single timeline semaphore can replace multiple binary semaphores.
Fragment shader barycentrics -
VK_KHR_fragment_shader_barycentricDemonstrates how to access barycentric coordinates in a fragment shader to create a wireframe visual effect.
Effects
Assorted samples showing graphical effects not special to Vulkan.
-
Demonstrates the basics of fullscreen shader effects. The scene is rendered into an offscreen framebuffer at lower resolution and rendered as a fullscreen quad atop the scene using a radial blur fragment shader.
-
Advanced fullscreen effect example adding a bloom effect to a scene. Glowing scene parts are rendered to a low res offscreen framebuffer that is applied atop the scene using a two pass separated gaussian blur.
-
Implements multiple texture mapping methods to simulate depth based on texture information: Normal mapping, parallax mapping, steep parallax mapping and parallax occlusion mapping (best quality, worst performance).
-
Uses a spherical material capture texture array defining environment lighting and reflection information to fake complex lighting.
Misc
-
Vulkan interpretation of glxgears. Procedurally generates and animates multiple gears.
-
Renders a Vulkan demo scene with logos and mascots. Not an actual example but more of a playground and showcase.
Credits and Attributions
See CREDITS.md for additional credits and attributions.
Overview (`shaders/README.md`)
Shaders
This folder contains the shaders used by the samples. Source files are available in GLSL, HLSL and slang and also come with precompiled SPIR-V files that are consumed by the samples. To recompile shaders you can use the compileshaders.py scripts in the respective folders or any other means that can generate Vulkan SPIR-V from GLSL, HLSL or slang. One such option is this extension for Visual Studio.
Note that not all samples may come with all shading language variants. So some samples that have GLSL source files might not come with HLSL and/or slang source files.
A note for using slang shaders: These require a different SPIR-V environment than glsl/hlsl. When selecting slang shaders, the base requirement for all samples is raised to at least Vulkan 1.1 with the SPIRV 1.4 extension.
If you want to compile slang shaders to SPIR-V, please use the latest release from here to get the latest bug fixes and features required for some of the samples. Minimum version for all shader to properly compile is 2025.16.1.
Overview (`examples/dynamicuniformbuffer/README.md`)
Dynamic uniform buffers
Synopsis
Use a single uniform buffer object as a dynamic uniform buffer to draw multiple objects with different matrices from one big uniform buffer object.
Requirements
The max. number of dynamic uniform buffers supported by the device should be checked with the maxDescriptorSetUniformBuffersDynamic device limit if you need more than the 8 dynamic uniform buffers required by the Vulkan specification.
Description
This example demonstrates the use of dynamic uniform buffers (VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC) for offsetting into one or more uniform block objects when binding the descriptor set using vkCmdBindDescriptorSets.
Instead of having single uniform buffers for each descriptor, dynamic uniform buffers can be used to store data for multiple descriptors in one single uniform buffer.
This minimizes the number of descriptor sets required and may help in optimizing memory writes by e.g. only doing partial updates to that memory.
For this example we will store the model matrices for multiple objects in one dynamic uniform buffer object and offset into this for each object draw.
Points of interest
Shader binding
The shader binding itself does not have to be changed compared to a static (single) uniform buffer, as the offset is done in the actual application code.
layout (binding = 1) uniform UboInstance {
mat4 model;
} uboInstance;Preparing the uniform buffer (and memory)
Note: When preparing the (host) memory to back up the dynamic uniform buffer object it's crucial to take the minUniformBufferOffsetAlignment limit of the implementation into account.
Due to the implementation dependent alignment (different from our actual data size) we can't just use a vector and work with pointers instead:
struct UboDataDynamic {
glm::mat4 *model = nullptr;
} uboDataDynamic;First step is to calculate the alignment required for the data we want to store compared to the min. uniform buffer offset alignment reported by the GPU:
void prepareUniformBuffers()
{
// Calculate required alignment based on minimum device offset alignment
size_t minUboAlignment = vulkanDevice->properties.limits.minUniformBufferOffsetAlignment;
dynamicAlignment = sizeof(glm::mat4);
if (minUboAlignment > 0) {
dynamicAlignment = (dynamicAlignment + minUboAlignment - 1) & ~(minUboAlignment - 1);
}The max. allowed alignment (as per spec) is 256 bytes which may be much higher than the data size we actually need for each entry (one 4x4 matrix = 64 bytes).
Now that we know the actual alignment required we can create our host memory for the dynamic uniform buffer:
size_t bufferSize = OBJECT_INSTANCES * dynamicAlignment;
uboDataDynamic.model = (glm::mat4*)alignedAlloc(bufferSize, dynamicAlignment);(The alignedAlloc function is a small wrapper doing aligned memory allocation depending on the OS/Compiler)
Creating the buffer is the same as creating any other uniform buffer object:
vulkanDevice->createBuffer(
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, &uniformBuffers.dynamic, bufferSize);(Updates will be flushed manually, the VK_MEMORY_PROPERTY_HOST_COHERENT_BIT flag will isn't used)
Setting up the descriptors
This is the same as for regular uniform buffers but with descriptor type VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC instead.
Descriptor pool
The example uses one dynamic uniform buffer, so we need to request at least one such descriptor type from the descriptor pool:
void setupDescriptors()
{
...
std::vector<VkDescriptorPoolSize> poolSizes = {
...
vkTools::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1),
...
};Descriptor set layout
The vertex shader interface defines the uniform with the model matrices (sampled from the dynamic buffer) at binding 1, so we need to setup a matching descriptor set layout:
void setupDescriptors()
{
...
std::vector<VkDescriptorSetLayoutBinding> setLayoutBindings = {
...
vkTools::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, VK_SHADER_STAGE_VERTEX_BIT, 1),
...
};Descriptor set
The example uses the same descriptor set based on the set layout above for all objects in the scene. As with the layout we bind the dynamic uniform buffer to binding point 1 using the descriptor set up while creating the buffer earlier on.
void setupDescriptors()
{
...
std::vector<VkWriteDescriptorSet> writeDescriptorSets = {
// Binding 1 : Instance matrix as dynamic uniform buffer
vkTools::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, &uniformBuffers.dynamic.descriptor),
};Using the dynamic uniform buffer
Now that everything is set up, it's time to render the objects using the different matrices stored in the dynamic uniform buffer.
for (uint32_t j = 0; j < OBJECT_INSTANCES; j++)
{
// One dynamic offset per dynamic descriptor to offset into the ubo containing all model matrices
uint32_t dynamicOffset = j * static_cast<uint32_t>(dynamicAlignment);
// Bind the descriptor set for rendering a mesh using the dynamic offset
vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 1, &dynamicOffset);
vkCmdDrawIndexed(drawCmdBuffers[i], indexCount, 1, 0, 0, 0);
}For each object to be drawn the offset into the dynamic uniform buffer object's memory is calculated using the dynamic alignment set before buffer creation.
The dynamic offset is then passed at descriptor set binding time using the dynamicOffsetCount and pDynamicOffsets parameters of vkCmdBindDescriptorSets.
For each dynamic uniform buffer in the descriptor set currently bound one pointer to an uint32_t has to be passed in the order of the dynamic buffers' binding indices.
The data starting at the given offset is then passed to the shader for which the dynamic binding applies upon drawing with vkCmdDrawIndexed.
Updating the buffer
While creating the buffer we did not specify the VK_MEMORY_PROPERTY_HOST_COHERENT_BIT flag. While this is possible, in a real-world application you would usually only update the parts of the dynamic buffer that actually changed (e.g. only objects that moved since the last frame) and do a manual flush of the updated buffer memory part for better performance.
This would be done using e.g. vkFlushMappedMemoryRanges.
VkMappedMemoryRange memoryRange = vkTools::initializers::mappedMemoryRange();
memoryRange.memory = uniformBuffers.dynamic.memory;
memoryRange.size = sizeof(uboDataDynamic);
vkFlushMappedMemoryRanges(device, 1, &memoryRange);(The example always updates the whole dynamic buffer's range)
Overview (`examples/gltfscenerendering/README.md`)
glTF scene rendering
Synopsis
Render a complete scene loaded from an glTF file. The sample is based on the glTF scene sample, and adds data structures, functions and shaders required to render a more complex scene using Crytek's Sponza model.
Description
This example demonstrates how to render a more complex scene loaded from a glTF model.
It builds on the basic glTF scene sample but instead of using global pipelines, it adds per-material pipelines that are dynamically created from the material definitions of the glTF model.
Those pipelines pass per-material parameters to the shader so different materials for e.g. displaying opaque and transparent objects can be built from a single shader.
It also adds data structures, loading functions and shaders to do normal mapping and an easy way of toggling visibility for the scene nodes.
Note that this is not a full glTF implementation as this would be beyond the scope of a simple example. For a complete glTF Vulkan implementation see my Vulkan glTF PBR renderer.
For details on glTF refer to the official glTF 2.0 specification.
Points of interest
Note: Points of interest are marked with a POI in the code comments:
// POI: This sample uses normal mapping, so we also need to load the tangents from the glTF fileFor this sample, those points of interest mark additions and changes compared to the basic glTF sample.
Loading external images
Unlike the other samples, the glTF scene used for this example doesn't embed the images but uses external ktx images instead. This makes loading a lot faster as the ktx image format natively maps to the GPU and no longer requires us to convert RGB to RGBA, but ktx also allows us to store the mip-chain in the image file itself.
So instead of creating the textures from a buffer that has been converted from the embedded RGB images, we just load the ktx files from disk:
void VulkanglTFScene::loadImages(tinygltf::Model& input)
{
images.resize(input.images.size());
for (size_t i = 0; i < input.images.size(); i++) {
tinygltf::Image& glTFImage = input.images[i];
images[i].texture.loadFromFile(path + "/" + glTFImage.uri, VK_FORMAT_R8G8B8A8_UNORM, vulkanDevice, copyQueue);
}
}Materials
New Material properties
struct Material
{
glm::vec4 baseColorFactor = glm::vec4(1.0f);
uint32_t baseColorTextureIndex;
uint32_t normalTextureIndex;
std::string alphaMode = "OPAQUE";
float alphaCutOff;
bool doubleSided = false;
VkDescriptorSet descriptorSet;
VkPipeline pipeline;
};Several new properties have been added to the material class for this example that are taken from the glTF source.
Along with the base color we now also get the index of the normal map for that material in normalTextureIndex, and store several material properties required to render the different materials in this scene:
alphaMode
The alpha mode defines how the alpha value for this material is determined. For opaque materials it's ignored, for masked materials the shader will discard fragments based on the alpha cutoff.
alphaCutOff
For masked materials, this value specifies the threshold between fully opaque and fully transparent. This is used to discard fragments in the fragment shader.
doubleSided
This property is used to select the appropriate culling mode for this material. For double-sided materials, culling will be disabled.
Retrieving these additional values is done here:
void VulkanglTFScene::loadMaterials(tinygltf::Model& input)
{
materials.resize(input.materials.size());
for (size_t i = 0; i < input.materials.size(); i++) {
tinygltf::Material glTFMaterial = input.materials[i];
...
materials[i].alphaMode = glTFMaterial.alphaMode;
materials[i].alphaCutOff = (float)glTFMaterial.alphaCutoff;
materials[i].doubleSided = glTFMaterial.doubleSided;
}
}Note: We only read the glTF material properties we use in this sample. There are many more, details on those can be found here.
Per-Material pipelines
Unlike most of the other samples that use a few pre-defined pipelines, this sample will dynamically generate per-material pipelines based on material properties in the VulkanExample::preparePipelines() function
We first setup pipeline state that's common for all materials:
// Setup common pipeline state properties...
VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCI = ...
VkPipelineRasterizationStateCreateInfo rasterizationStateCI = ...
VkPipelineColorBlendAttachmentState blendAttachmentStateCI = ...
...
for (auto &material : glTFScene.materials)
{
...For each material we then set constant properties for the fragment shader using specialization constants:
struct MaterialSpecializationData {
VkBool32 alphaMask;
float alphaMaskCutoff;
} materialSpecializationData;
materialSpecializationData.alphaMask = material.alphaMode == "MASK";
materialSpecializationData.alphaMaskCutoff = material.alphaCutOff;
std::vector<VkSpecializationMapEntry> specializationMapEntries = {
vks::initializers::specializationMapEntry(0, offsetof(MaterialSpecializationData, alphaMask), sizeof(MaterialSpecializationData::alphaMask)),
vks::initializers::specializationMapEntry(1, offsetof(MaterialSpecializationData, alphaMaskCutoff), sizeof(MaterialSpecializationData::alphaMaskCutoff)),
};
VkSpecializationInfo specializationInfo = vks::initializers::specializationInfo(specializationMapEntries, sizeof(materialSpecializationData), &materialSpecializationData);
shaderStages[1].pSpecializationInfo = &specializationInfo;
...We also set the culling mode depending on whether this material is double-sided:
// For double sided materials, culling will be disabled
rasterizationStateCI.cullMode = material.doubleSided ? VK_CULL_MODE_NONE : VK_CULL_MODE_BACK_BIT;With those setup we create a pipeline for the current material and store it as a property of the material class:
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &material.pipeline));
}The material now also get's it's own pipeline.
The alpha mask properties are used in the fragment shader to distinguish between opaque and transparent materials (scene.frag).
Specialization constant declaration in the shaders's header:
layout (constant_id = 0) const bool ALPHA_MASK = false;
layout (constant_id = 1) const float ALPHA_MASK_CUTOFF = 0.0;Note: The default values provided in the shader are overwritten by the values passed at pipeline creation time.
For alpha masked materials, fragments below the cutoff threshold are discarded:
vec4 color = texture(samplerColorMap, inUV) * vec4(inColor, 1.0);
if (ALPHA_MASK) {
if (color.a < ALPHA_MASK_CUTOFF) {
discard;
}
}Normal mapping
This sample also adds tangent space normal mapping to the rendering equation to add additional detail to the scene, which requires loading additional data.
Normal maps
Along with the color maps, we now also load all normal maps. From the glTF POV those are just images like all other texture maps, and are stored in the image vector. So as for loading normal maps no code changes are required. The normal map images are then referenced by the index of the normal map of the material, which is now read in addition to the other material properties:
void VulkanglTFScene::loadMaterials(tinygltf::Model& input)
{
materials.resize(input.materials.size());
for (size_t i = 0; i < input.materials.size(); i++) {
tinygltf::Material glTFMaterial = input.materials[i];
...
// Get the normal map texture index
if (glTFMaterial.additionalValues.find("normalTexture") != glTFMaterial.additionalValues.end()) {
materials[i].normalTextureIndex = glTFMaterial.additionalValues["normalTexture"].TextureIndex();
}
...
}
}*Note: Unlike the color map index, the normal map index is stored in the additionalValues of the material.
The normal maps are then bound to binding 1 via the material's descriptor set in VulkanExample::setupDescriptors:
for (auto& material : glTFScene.materials) {
...
VkDescriptorImageInfo colorMap = glTFScene.getTextureDescriptor(material.baseColorTextureIndex);
VkDescriptorImageInfo normalMap = glTFScene.getTextureDescriptor(material.normalTextureIndex);
std::vector<VkWriteDescriptorSet> writeDescriptorSets = {
vks::initializers::writeDescriptorSet(material.descriptorSet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 0, &colorMap),
vks::initializers::writeDescriptorSet(material.descriptorSet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &normalMap),
};
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr);
}The descriptor set itself is then bound to set 1 at draw time in VulkanglTFScene::drawNode:
if (node.mesh.primitives.size() > 0) {
...
for (VulkanglTFScene::Primitive& primitive : node.mesh.primitives) {
if (primitive.indexCount > 0) {
VulkanglTFScene::Material& material = materials[primitive.materialIndex];
...
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 1, 1, &material.descriptorSet, 0, nullptr);
...
}
}
}Fragment shader interface in scene.frag:
layout (set = 1, binding = 0) uniform sampler2D samplerColorMap;
layout (set = 1, binding = 1) uniform sampler2D samplerNormalMap;Per-Vertex tangents
Along with the normals we also need per-vertex tangents and bitangents for normal mapping. As the bitangent can easily be calculated using the normal and tangent, glTF only stores those two.
So just like with other vertex data already loaded we need to check if there are tangents for a node and load them from the appropriate buffer using a glTF accessor:
void VulkanglTFScene::loadNode(const tinygltf::Node& inputNode, const tinygltf::Model& input, VulkanglTFScene::Node* parent, std::vector<uint32_t>& indexBuffer, std::vector<VulkanglTFScene::Vertex>& vertexBuffer)
{
VulkanglTFScene::Node node{};
...
if (inputNode.mesh > -1) {
const tinygltf::Mesh mesh = input.meshes[inputNode.mesh];
for (size_t i = 0; i < mesh.primitives.size(); i++) {
const tinygltf::Primitive& glTFPrimitive = mesh.primitives[i];
// Vertices
{
...
const float* tangentsBuffer = nullptr;
if (glTFPrimitive.attributes.find("TANGENT") != glTFPrimitive.attributes.end()) {
const tinygltf::Accessor& accessor = input.accessors[glTFPrimitive.attributes.find("TANGENT")->second];
const tinygltf::BufferView& view = input.bufferViews[accessor.bufferView];
tangentsBuffer = reinterpret_cast<const float*>(&(input.buffers[view.buffer].data[accessor.byteOffset + view.byteOffset]));
}
for (size_t v = 0; v < vertexCount; v++) {
Vertex vert{};
...
vert.tangent = tangentsBuffer ? glm::make_vec4(&tangentsBuffer[v * 4]) : glm::vec4(0.0f);
vertexBuffer.push_back(vert);
}
}
...Note: The tangent is a four-component vector, with the w-component storing the handedness of the tangent basis. This will be used later on in the shader.
Shaders
Normal mapping is applied in the scene.frag fragment shader and boils down to calculating a new world-space normal from the already provided per-vertex normal and the per-fragment tangent space normals provided via the materials' normal map.
With the per-vertex normal and tangent values passed to the fragment shader, we simply change the way the per-fragment normal is calculated:
vec3 normal = normalize(inNormal);
vec3 tangent = normalize(inTangent.xyz);
vec3 bitangent = cross(inNormal, inTangent.xyz) * inTangent.w;
mat3 TBN = mat3(tangent, bitangent, normal);
vec3 localNormal = texture(samplerNormalMap, inUV).xyz * 2.0 - 1.0;
normal = normalize(TBN * localNormal);As noted earlier, glTF does not store bitangents, but we can easily calculate them using the cross product of the normal and tangent. We also multiply this with the tangent's w-component which stores the handedness of the tangent. This is important, as this may differ between nodes in a glTF file.
After that we calculate the tangent to world-space transformation matrix that is then applied to the per-fragment normal read from the normal map.
This is then our new normal that is used for the lighting calculations to follow.
Rendering the scene
Just like in the basic glTF sample, the scene hierarchy is added to the command buffer in VulkanglTFModel::draw. Since glTF has a hierarchical node structure this function recursively calls VulkanglTFModel::drawNode for rendering a give node with it's children.
The only real change in this sample is binding the per-material pipeline for a node's mesh:
void VulkanglTFScene::drawNode(VkCommandBuffer commandBuffer, VkPipelineLayout pipelineLayout, VulkanglTFScene::Node node)
{
if (!node.visible) {
return;
}
if (node.mesh.primitives.size() > 0) {
...
vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &nodeMatrix);
for (VulkanglTFScene::Primitive& primitive : node.mesh.primitives) {
if (primitive.indexCount > 0) {
VulkanglTFScene::Material& material = materials[primitive.materialIndex];
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, material.pipeline);
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 1, 1, &material.descriptorSet, 0, nullptr);
vkCmdDrawIndexed(commandBuffer, primitive.indexCount, 1, primitive.firstIndex, 0, 0);
}
}
}
for (auto& child : node.children) {
drawNode(commandBuffer, pipelineLayout, child);
}
}Overview (`examples/gltfskinning/README.md`)
glTF vertex skinning
Synopsis
Renders an animated glTF model with vertex skinning. The sample is based on the glTF scene sample, and adds data structures, functions and shaders required to apply vertex skinning to a mesh.
Description
This example demonstrates how to load and use the data structures required for animating a mesh with vertex skinning.
Vertex skinning is a technique that uses per-vertex weights based on the current pose of a skeleton made up of bones.
Animations then are applied to those bones instead and during rendering the matrices of those bones along with the vertex weights are used to calculate the final vertex positions.
A good glTF skinning tutorial can be found here, so this readme only gives a coarse overview on the actual implementation instead of going into full detail on how vertex skinning works.
Note that this is not a full glTF implementation as this would be beyond the scope of a simple example. For a complete glTF Vulkan implementation see https://github.com/SaschaWillems/Vulkan-glTF-PBR/.
Points of interest
Note: Points of interest are marked with a POI in the code comments:
// POI: Get buffer data required for vertex skinningData structures
Several new data structures are required for doing animations with vertex skinning. The official glTF spec has the details on those.
Node additions
struct Node
{
Node * parent;
uint32_t index;
std::vector<Node *> children;
Mesh mesh;
glm::vec3 translation{};
glm::vec3 scale{1.0f};
glm::quat rotation{};
int32_t skin = -1;
glm::mat4 matrix;
glm::mat4 getLocalMatrix();
};The node now also stores the matrix components (translation, rotation ,scale) as they can be independently influenced.
The skin member is the index of the skin (see below) that is applied to this node.
A new function called getLocalMatrix is introduced that calculates the local matrix from the initial one and the current components.
Vertex additions
struct Vertex
{
...
glm::vec4 jointIndices;
glm::vec4 jointWeights;
};To calculate the final matrix to be applied to the vertex we now pass the indices of the joints (see below) and the weights of those, which determines how strongly this vertex is influenced by the joint. glTF support at max. four indices and weights per joint, so we pass them as four-component vectors.
Skin
struct Skin
{
std::string name;
Node * skeletonRoot = nullptr;
std::vector<glm::mat4> inverseBindMatrices;
std::vector<Node *> joints;
vks::Buffer ssbo;
VkDescriptorSet descriptorSet;
};This struct stores all information required for applying a skin to a mesh. Most important are the inverseBindMatrices used to transform the geometry into the space of the accompanying joint node. The joints vector contains the nodes used as joints in this skin.
We will pass the actual joint matrices for the current animation frame using a shader storage buffer object, so each skin also get's it's own ssboo along with a descriptor set to be bound at render time.
Animations
glTF spec chapter on animations
Animation sampler
struct AnimationSampler
{
std::string interpolation;
std::vector<float> inputs;
std::vector<glm::vec4> outputsVec4;
};The animation sampler contains the key frame data read from a buffer using an accessor and the way the key frame is interpolated. This can be LINEAR, which is just a simple linear interpolation over time, STEP, which remains constant until the next key frame is reached, and CUBICSPLINE which uses a cubic spline with tangents for calculating the interpolated key frames. This is a bit more complex and separately documented in this glTF spec chapter.
Note: For simplicity, this sample only implements LINEAR interpolation.
Animation channel
struct AnimationChannel
{
std::string path;
Node * node;
uint32_t samplerIndex;
};The animation channel connects the node with a key frame specified by an animation sampler with the path member specifying the node property to animate, which is either translation, rotation, scale or weights. The latter one refers to morph targets and not vertex weights (for skinning) and is not used in this sample.
Animation
struct Animation
{
std::string name;
std::vector<AnimationSampler> samplers;
std::vector<AnimationChannel> channels;
float start = std::numeric_limits<float>::max();
float end = std::numeric_limits<float>::min();
float currentTime = 0.0f;
};The animation itself then contains the animation samplers and channels along with timing information.
Loading and passing the data
Vertex attributes
This samples adds two new vertex attributes for passing per-vertex joints and weights information. As with other per-vertex attributes, these need to be loaded using glTF accessors in VulkanglTFModel::loadNode:
// Get vertex joint indices
if (glTFPrimitive.attributes.find("JOINTS_0") != glTFPrimitive.attributes.end())
{
const tinygltf::Accessor & accessor = input.accessors[glTFPrimitive.attributes.find("JOINTS_0")->second];
const tinygltf::BufferView &view = input.bufferViews[accessor.bufferView];
jointIndicesBuffer = reinterpret_cast<const uint16_t *>(&(input.buffers[view.buffer].data[accessor.byteOffset + view.byteOffset]));
}
// Get vertex joint weights
if (glTFPrimitive.attributes.find("WEIGHTS_0") != glTFPrimitive.attributes.end())
{
const tinygltf::Accessor & accessor = input.accessors[glTFPrimitive.attributes.find("WEIGHTS_0")->second];
const tinygltf::BufferView &view = input.bufferViews[accessor.bufferView];
jointWeightsBuffer = reinterpret_cast<const float *>(&(input.buffers[view.buffer].data[accessor.byteOffset + view.byteOffset]));
}As usual we check if those attributes actually exist inside the glTF file, and if so we use the accessor and buffer view to get a pointer to the required vertex attributes.
The new attributes are added to the vertex input state of our pipeline in VulkanExample::preparePipelines at locations 4 and 5:
const std::vector<VkVertexInputAttributeDescription> vertexInputAttributes = {
...
{4, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(VulkanglTFModel::Vertex, jointIndices)},
{5, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(VulkanglTFModel::Vertex, jointWeights)},
};Vertex shader interface in skinnedmodel.vert:
layout (location = 4) in vec4 inJointIndices;
layout (location = 5) in vec4 inJointWeights;Skins
Loading skins is done in VulkanglTFModel::loadSkin and aside from getting the required data from the glTF sources into our own structures, this method also creates a buffer for uploading the inverse bind matrices:
void VulkanglTFModel::loadSkins(tinygltf::Model &input)
{
skins.resize(input.skins.size());
for (size_t i = 0; i < input.skins.size(); i++)
{
tinygltf::Skin glTFSkin = input.skins[i];
...
if (glTFSkin.inverseBindMatrices > -1)
{
const tinygltf::Accessor & accessor = input.accessors[glTFSkin.inverseBindMatrices];
const tinygltf::BufferView &bufferView = input.bufferViews[accessor.bufferView];
const tinygltf::Buffer & buffer = input.buffers[bufferView.buffer];
skins[i].inverseBindMatrices.resize(accessor.count);
memcpy(skins[i].inverseBindMatrices.data(), &buffer.data[accessor.byteOffset + bufferView.byteOffset], accessor.count * sizeof(glm::mat4));
vulkanDevice->createBuffer(
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
&skins[i].ssbo,
sizeof(glm::mat4) * skins[i].inverseBindMatrices.size(),
skins[i].inverseBindMatrices.data());
...
}
}Vertex shader interface in skinnedmodel.vert:
layout(std430, set = 1, binding = 0) readonly buffer JointMatrices {
mat4 jointMatrices[];
};As with e.g. vertex attributes we retrieve the inverse bind matrices from the glTF accessor and buffer view. These are used at a later point for generating the actual animation matrices.
We then create a shader storage buffer object (for each skin) big enough to hold all animation matrices. See Updating the animations for how these are calculated and updated.
Note: For simplicity we create a host visible SSBO. This makes code easier to read. In a real-world application you'd use a device local SSBO instead.
Animations
Loading the animation data is done in VulkanglTFModel::loadAnimations and is also mostly about getting the required data from glTF into our own sources, including sampler and channel data.
The most interesting part is getting the input and output pairs for the animation's samplers, which are loaded using glTF's accessors.
We first load the sampler's input values, containing keyframe time values. These are also used to determine the start and end time of the animation:
void VulkanglTFModel::loadAnimations(tinygltf::Model &input)
...
for (size_t i = 0; i < input.animations.size(); i++)
{
...
for (size_t j = 0; j < glTFAnimation.samplers.size(); j++)
{
...
{
const tinygltf::Accessor & accessor = input.accessors[glTFSampler.input];
const tinygltf::BufferView &bufferView = input.bufferViews[accessor.bufferView];
const tinygltf::Buffer & buffer = input.buffers[bufferView.buffer];
const void * dataPtr = &buffer.data[accessor.byteOffset + bufferView.byteOffset];
const float * buf = static_cast<const float *>(dataPtr);
for (size_t index = 0; index < accessor.count; index++)
{
dstSampler.inputs.push_back(buf[index]);
}
// Adjust animation's start and end times
for (auto input : animations[i].samplers[j].inputs)
{
if (input < animations[i].start)
{
animations[i].start = input;
};
if (input > animations[i].end)
{
animations[i].end = input;
}
}
}Next up are the sampler's output values containing the keyframe output values for the input time values read above:
void VulkanglTFModel::loadAnimations(tinygltf::Model &input)
...
for (size_t i = 0; i < input.animations.size(); i++)
{
...
for (size_t j = 0; j < glTFAnimation.samplers.size(); j++)
{
...
// Read sampler keyframe output translate/rotate/scale values
{
const tinygltf::Accessor & accessor = input.accessors[glTFSampler.output];
const tinygltf::BufferView &bufferView = input.bufferViews[accessor.bufferView];
const tinygltf::Buffer & buffer = input.buffers[bufferView.buffer];
const void * dataPtr = &buffer.data[accessor.byteOffset + bufferView.byteOffset];
switch (accessor.type)
{
case TINYGLTF_TYPE_VEC3: {
const glm::vec3 *buf = static_cast<const glm::vec3 *>(dataPtr);
for (size_t index = 0; index < accessor.count; index++)
{
dstSampler.outputsVec4.push_back(glm::vec4(buf[index], 0.0f));
}
break;
}
case TINYGLTF_TYPE_VEC4: {
const glm::vec4 *buf = static_cast<const glm::vec4 *>(dataPtr);
for (size_t index = 0; index < accessor.count; index++)
{
dstSampler.outputsVec4.push_back(buf[index]);
}
break;
}
default: {
std::cout << "unknown type" << std::endl;
break;
}
}
}As the comment notes these outputs apply to a certain animation target path, which is either a translation, rotation or scale.
The path and node that the output keyframe values are applied to are part of the animation's channel:
...
for (size_t i = 0; i < input.animations.size(); i++)
{
...
for (size_t j = 0; j < glTFAnimation.channels.size(); j++)
{
tinygltf::AnimationChannel glTFChannel = glTFAnimation.channels[j];
AnimationChannel & dstChannel = animations[i].channels[j];
dstChannel.path = glTFChannel.target_path;
dstChannel.samplerIndex = glTFChannel.sampler;
dstChannel.node = nodeFromIndex(glTFChannel.target_node);
}So if the animation's channel's path is set to translation, the keyframe output values contain translation values that are applied to the channel's node depending on the current animation time.
Updating the animation
With all required structures loaded, the next step is updating the actual animation data. This is done inside the VulkanglTFModel::updateAnimation function, where the data from the animation's samplers and channels is applied to the animation targets of the destination node.
We first update the active animation's current timestamp and also check if we need to restart it:
Animation &animation = animations[activeAnimation];
animation.currentTime += deltaTime;
if (animation.currentTime > animation.end)
{
animation.currentTime -= animation.end;
}Next we go through all the channels that are applied to this animation (translation, rotation, scale) and try to find the input keyframe values for the current timestamp:
for (auto &channel : animation.channels)
{
AnimationSampler &sampler = animation.samplers[channel.samplerIndex];
for (size_t i = 0; i < sampler.inputs.size() - 1; i++)
{
// Get the input keyframe values for the current time stamp
if ((animation.currentTime >= sampler.inputs[i]) && (animation.currentTime <= sampler.inputs[i + 1]))
{
// Calculate interpolation value based on timestamp, Update node, see next paragraph
}
}
}Inside the above loop we then calculate the interpolation value based on the animation's current time and the sampler's input keyframes for the current and next frame:
float a = (animation.currentTime - sampler.inputs[i]) / (sampler.inputs[i + 1] - sampler.inputs[i]);This interpolation value is then used to apply the keyframe output values to the appropriate channel:
if (channel.path == "translation")
{
channel.node->translation = glm::mix(sampler.outputsVec4[i], sampler.outputsVec4[i + 1], a);
}
if (channel.path == "rotation")
{
glm::quat q1;
q1.x = sampler.outputsVec4[i].x;
q1.y = sampler.outputsVec4[i].y;
q1.z = sampler.outputsVec4[i].z;
q1.w = sampler.outputsVec4[i].w;
glm::quat q2;
q2.x = sampler.outputsVec4[i + 1].x;
q2.y = sampler.outputsVec4[i + 1].y;
q2.z = sampler.outputsVec4[i + 1].z;
q2.w = sampler.outputsVec4[i + 1].w;
channel.node->rotation = glm::normalize(glm::slerp(q1, q2, a));
}
if (channel.path == "scale")
{
channel.node->scale = glm::mix(sampler.outputsVec4[i], sampler.outputsVec4[i + 1], a);
}Note: As mentioned earlier, this sample only supports linear interpolation types.
Translation and scale is a simple linear interpolation between the output keyframe values of the current and next frame.
Rotations use quaternions and as such are interpolated using spherical linear interpolation.
After the node's animation components have been updated, we update all node joints:
for (auto &node : nodes)
{
updateJoints(node);
}The updateJoints function will calculate the actual joint matrices and update the shader storage buffer object:
void VulkanglTFModel::updateJoints(VulkanglTFModel::Node *node)
{
if (node->skin > -1)
{
// Update the joint matrices
glm::mat4 inverseTransform = glm::inverse(getNodeMatrix(node));
Skin skin = skins[node->skin];
size_t numJoints = (uint32_t) skin.joints.size();
std::vector<glm::mat4> jointMatrices(numJoints);
for (size_t i = 0; i < numJoints; i++)
{
jointMatrices[i] = getNodeMatrix(skin.joints[i]) * skin.inverseBindMatrices[i];
jointMatrices[i] = inverseTransform * jointMatrices[i];
}
// Update ssbo
skin.ssbo.copyTo(jointMatrices.data(), jointMatrices.size() * sizeof(glm::mat4));
}
for (auto &child : node->children)
{
updateJoints(child);
}
}Note the use of the getNodeMatrix function which will return the current matrix of a given node calculated from the node hierarchy and the node's current translate/rotate/scale values updated earlier. This is the actual matrix that's updated by the current animation state.
After this we copy the new joint matrices to the shader storage buffer object of the current skin to make it available to the shader.
Rendering the model
With all the matrices calculated and made available to the shaders, we can now finally render our animated model using vertex skinning.
Rendering the glTF model is done in VulkanglTFModel::draw which is called at command buffer creation. Since glTF has a hierarchical node structure this function recursively calls VulkanglTFModel::drawNode for rendering a give node with it's children:
void VulkanglTFModel::drawNode(VkCommandBuffer commandBuffer, VkPipelineLayout pipelineLayout, VulkanglTFModel::Node node)
{
if (node.mesh.primitives.size() > 0)
{
// Pass the node's matrix via push constants
// Traverse the node hierarchy to the top-most parent to get the final matrix of the current node
glm::mat4 nodeMatrix = node.matrix;
VulkanglTFModel::Node *currentParent = node.parent;
while (currentParent)
{
nodeMatrix = currentParent->matrix * nodeMatrix;
currentParent = currentParent->parent;
}
// Pass the final matrix to the vertex shader using push constants
vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &nodeMatrix);
// Bind SSBO with skin data for this node to set 1
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 1, 1, &skins[node.skin].descriptorSet, 0, nullptr);
for (VulkanglTFModel::Primitive &primitive : node.mesh.primitives)
{
if (primitive.indexCount > 0)
{
...
vkCmdDrawIndexed(commandBuffer, primitive.indexCount, 1, primitive.firstIndex, 0, 0);
}
}
}
for (auto &child : node.children)
{
drawNode(commandBuffer, pipelineLayout, *child);
}
}There are two points-of-interest in this code related to vertex skinning.
First is passing the (fixed) model matrix via a push constant, which is not directly related to the animation itself but required later on on the shader:
glm::mat4 nodeMatrix = node.matrix;
VulkanglTFModel::Node *currentParent = node.parent;
while (currentParent)
{
nodeMatrix = currentParent->matrix * nodeMatrix;
currentParent = currentParent->parent;
}
// Pass the final matrix to the vertex shader using push constants
vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &nodeMatrix);As this matrix won't change in our case, we pass this is a push constant to the vertex shader.
And we also bind the shader storage buffer object of the skin so the vertex shader get's access to the current joint matrices for the skin to be applied to that particular node:
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 1, 1, &skins[node.skin].descriptorSet, 0, nullptr);With the application side setup done, we can now take a look at the vertex shader that does the actual vertex skinning (skinnedmodel.vert).
As mentioned earlier, the interface has been expanded with values and attributes related to vertex skinning:
#version 450
...
layout (location = 4) in vec4 inJointIndices;
layout (location = 5) in vec4 inJointWeights;
...
layout(push_constant) uniform PushConsts {
mat4 model;
} primitive;
layout(std430, set = 1, binding = 0) readonly buffer JointMatrices {
mat4 jointMatrices[];
};Those are then used to calculate the skin matrix that's applied to the vertex position:
void main()
{
...
mat4 skinMat =
inJointWeights.x * jointMatrices[int(inJointIndices.x)] +
inJointWeights.y * jointMatrices[int(inJointIndices.y)] +
inJointWeights.z * jointMatrices[int(inJointIndices.z)] +
inJointWeights.w * jointMatrices[int(inJointIndices.w)];
gl_Position = uboScene.projection * uboScene.view * primitive.model * skinMat * vec4(inPos.xyz, 1.0);
...
}The skin matrix is a linear combination of the joint matrices. The indices of the joint matrices to be applied are taken from the inJointIndices vertex attribute, with each component (xyzw) storing one index, and those matrices are then weighted by the inJointWeights vertex attribute to calculate the final skin matrix that is applied to this vertex.
Overview (`examples/indirectdraw/README.md`)
Indirect drawing
Synopsis
Issue multiple instanced draws for different meshes in one single draw call using indirect draw commands.
Requirements
If the multiDrawIndirect feature is supported, only one draw call is issued for the plants. If this feature is not available multiple indirect draw commands are used. Note: When issuing many draw counts also make sure to stay within the limitations of maxDrawIndirectCount.
Description
This example demonstrates the use of indirect draw commands. In addition to draw functions like vkCmdDraw and vkCmdDrawIndexed, where the parameters that specify what is drawn are passed directly to the function ("direct drawing"), there also exist indirect drawing commands.
vkCmdDrawIndirect and vkCmdDrawIndexedIndirect take the draw commands from a buffer object that contains descriptions on the draw commands to be issued, including instance and index counts, vertex offsets, etc. This also allows to draw multiple geometries with a single draw command as long as they're backed up by the same vertex (and index) buffer.
This adds several new possibilities of generating (and updating) actual draw commands, as that buffer can be generated and updated offline with no need to actually update the command buffers that contain the actual drawing functions.
Using indirect drawing you can generate the draw commands offline ahead of time on the CPU and even update them using shaders (as they're stored in a device local buffer). This adds lots of new possibilities to update draw commands without the CPU being involved, including GPU-based culling.
The example generates a single indirect buffer that contains draw commands for 12 different plants at random position, scale and rotation also using instancing to render the objects multiple times. The whole foliage (and trees) seen in the screen are drawn using only one draw call.
The different plant meshes are loaded from a single file and stored inside a single index and vertex buffer, index offsets are stored inside the indirect draw commands.
For details on the use of instancing (and instanced vertex attributes), see the instancing example
Points of interest
Preparing the indirect draw
The example generates the indirect drawing buffer right at the start. First step is to generate the data for the indirect draws. Vulkan has a dedicated struct for this called VkDrawIndexedIndirectCommand and the example uses a std::vector<VkDrawIndexedIndirectCommand> to store these before uploading them to the GPU:
void prepareIndirectData()
{
...
uint32_t m = 0;
for (auto& meshDescriptor : meshes.plants.meshDescriptors)
{
VkDrawIndexedIndirectCommand indirectCmd{};
indirectCmd.instanceCount = OBJECT_INSTANCE_COUNT;
indirectCmd.firstInstance = m * OBJECT_INSTANCE_COUNT;
indirectCmd.firstIndex = meshDescriptor.indexBase;
indirectCmd.indexCount = meshDescriptor.indexCount;
indirectCommands.push_back(indirectCmd);
m++;
}
...
}The meshDescriptor is generated by the mesh loader and contains the index base and count of that mesh inside the global index buffer containing all plant meshes for the scenery.
The indirect draw command for the second plant mesh looks like this:
indirectCmd.indexCount = 1668;
indirectCmd.instanceCount = 2048;
indirectCmd.firstIndex = 960;
indirectCmd.vertexOffset = 0;
indirectCmd.firstInstance = 2048;Which will result in 2048 instances of the index data starting at index 960 (using 1668 indices) being drawn, the first instance is also important as the shader is using it for the instanced attributes of that object (position, rotation, scale).
Once we have filled that vector we need to create the buffer that the GPU uses to read the indirect commands from:
VK_CHECK_RESULT(vulkanDevice->createBuffer(
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
&indirectCommandsBuffer,
stagingBuffer.size));
vulkanDevice->copyBuffer(&stagingBuffer, &indirectCommandsBuffer, queue);To use a buffer for indirect draw commands you need to specify the VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT usage flag at creation time. As the buffer is never again changed on the host side we stage it to the GPU to maximize performance.
Rendering
If the multiDrawIndirect is supported, we can issue all indirect draws with one single draw call:
void buildCommandBuffer()
{
...
for (int32_t i = 0; i < drawCmdBuffers.size(); ++i)
{
vkCmdDrawIndexedIndirect(cmdBuffer, indirectCommandsBuffer.buffer, 0, indirectDrawCount, sizeof(VkDrawIndexedIndirectCommand));
}
}We just pass the buffer handle to the buffer containing the indirect draw commands and the number of drawCounts.
The non-indirect, non-instanced equivalent of this would be:
for (auto indirectCmd : indirectCommands)
{
for (uint32_t j = 0; j < indirectCmd.instanceCount; j++)
{
vkCmdDrawIndexed(cmdBuffer, indirectCmd.indexCount, 1, indirectCmd.firstIndex, 0, indirectCmd.firstInstance + j);
}
}If the GPU does not support multiDrawIndirect we have to issue the indirect draw commands one-by-one using a buffer offset instead:
for (auto j = 0; j < indirectCommands.size(); j++)
{
vkCmdDrawIndexedIndirect(cmdBuffer, indirectCommandsBuffer.buffer, j * sizeof(VkDrawIndexedIndirectCommand), 1, sizeof(VkDrawIndexedIndirectCommand));
}Acknowledgments
- Plant and foliage models by Hugues Muller
Overview (`examples/texturemipmapgen/README.md`)
Run-time mip-map generation
Synopsis
Generates a complete texture mip-chain at runtime from a base image using image blits and proper image barriers.
Requirements
To downsample from one mip level to the next, we will be using vkCmdBlitImage. This requires the format used to support the BLIT_SRC_BIT and the BLIT_DST_BIT flags. If these are not supported, the image format can't be used to blit and you'd either have to choose a different format or use e.g. a compute shader to generate mip levels. The example uses the VK_FORMAT_R8G8B8A8_UNORM that should support these flags on most implementations.
Note: Use vkGetPhysicalDeviceFormatProperties to check if the format supports the blit flags first.
Description
This examples demonstrates how to generate a complete texture mip-chain at runtime instead of loading offline generated mip-maps from a texture file.
While usually not applied for textures stored on the disk (that usually have the mips generated offline and stored in the file, see basic texture mapping example) this technique is used textures are generated at runtime, e.g. when doing dynamic cubemaps or other render-to-texture effects.
Having mip-maps for runtime generated textures offers lots of benefits, both in terms of image stability and performance. Without mip mapping the image will become noisy, especially with high frequency textures (and texture components like specular) and using mip mapping will result in higher performance due to caching.
Though this example only generates one mip-chain for a single texture at the beginning this technique can also be used during normal frame rendering to generate mip-chains for dynamic textures.
Some GPUs also offer asynchronous transfer queues (check for queue families with only the ? VK_QUEUE_TRANSFER_BIT set) that may be used to speed up such operations.
Points of interest
Image setup
Even though we'll only upload the first mip level initially, we create the image with number of desired mip levels. The following formula is used to calculate the number of mip levels based on the max. image extent:
texture.mipLevels = floor(log2(std::max(texture.width, texture.height))) + 1;This is then passed to the image creat info:
VkImageCreateInfo imageCreateInfo = vkTools::initializers::imageCreateInfo();
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
imageCreateInfo.format = format;
imageCreateInfo.mipLevels = texture.mipLevels;
...Setting the number of desired mip levels is necessary as this is used for allocating the right amount of memory for the image (vkAllocateMemory).
Upload base mip level
Before generating the mip-chain we need to copy the image data loaded from disk into the newly generated image. This image will be the base for our mip-chain:
VkBufferImageCopy bufferCopyRegion = {};
bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
bufferCopyRegion.imageSubresource.mipLevel = 0;
bufferCopyRegion.imageExtent.width = texture.width;
bufferCopyRegion.imageExtent.height = texture.height;
bufferCopyRegion.imageExtent.depth = 1;
vkCmdCopyBufferToImage(copyCmd, stagingBuffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bufferCopyRegion);Prepare base mip level
As we are going to blit from the base mip-level just uploaded we also need to insert an image memory barrier that sets the image layout to TRANSFER_SRC for the base mip level:
VkImageSubresourceRange subresourceRange = {};
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresourceRange.levelCount = 1;
subresourceRange.layerCount = 1;
vks::tools::insertImageMemoryBarrier(
copyCmd,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
subresourceRange);Generating the mip-chain
There are two different ways of generating the mip-chain. The first one is to blit down the whole mip-chain from level n-1 to n, the other way would be to always use the base image and blit down from that to all levels. This example uses the first one.
Note: Blitting (same for copying) images is done inside of a command buffer that has to be submitted and as such has to be synchronized before using the new image with e.g. a vkFence.
We simply loop over all remaining mip levels (level 0 was loaded from disk) and prepare a VkImageBlit structure for each blit from mip level i-1 to level i.
First the source for out blit. This is the previous mip level. The dimensions of the blit source are specified by srcOffset:
for (int32_t i = 1; i < texture.mipLevels; i++)
{
VkImageBlit imageBlit{};
// Source
imageBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlit.srcSubresource.layerCount = 1;
imageBlit.srcSubresource.mipLevel = i-1;
imageBlit.srcOffsets[1].x = int32_t(texture.width >> (i - 1));
imageBlit.srcOffsets[1].y = int32_t(texture.height >> (i - 1));
imageBlit.srcOffsets[1].z = 1;Setup for the destination mip level (1), with the dimensions for the blit destination specified in dstOffsets[1]:
// Destination
imageBlit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlit.dstSubresource.layerCount = 1;
imageBlit.dstSubresource.mipLevel = i;
imageBlit.dstOffsets[1].x = int32_t(texture.width >> i);
imageBlit.dstOffsets[1].y = int32_t(texture.height >> i);
imageBlit.dstOffsets[1].z = 1;Before we can blit to this mip level, we need to transition its image layout to TRANSFER_DST:
VkImageSubresourceRange mipSubRange = {};
mipSubRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
mipSubRange.baseMipLevel = i;
mipSubRange.levelCount = 1;
mipSubRange.layerCount = 1;
// Prepare current mip level as image blit destination
vks::tools::insertImageMemoryBarrier(
blitCmd,
texture.image,
0,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
mipSubRange);Note that we set the baseMipLevel member of the subresource range so the image memory barrier will only affect the one mip level we want to copy to.
Now that the mip level we want to copy from and the one we'll copy to have are in the proper layout (transfer source and destination) we can issue the vkCmdBlitImage to copy from mip level (i-1) to mip level (i):
vkCmdBlitImage(
blitCmd,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&imageBlit,
VK_FILTER_LINEAR);vkCmdBlitImage does the (down) scaling from mip level (i-1) to mip level (i) using a linear filter.
After the blit is done we can use this mip level as a base for the next level, so we transition the layout from TRANSFER_DST_OPTIMAL to TRANSFER_SRC_OPTIMAL so we can use this level as transfer source for the next level:
// Prepare current mip level as image blit source for next level
vks::tools::insertImageMemoryBarrier(
copyCmd,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
mipSubRange);
}Final image layout transitions
Once the loop is done we need to transition all mip levels of the image to their actual usage layout, which is SHADER_READ for this example. Note that after the loop all levels will be in the TRANSFER_SRC layout allowing us to transfer the whole image at once:
subresourceRange.levelCount = texture.mipLevels;
vks::tools::insertImageMemoryBarrier(
copyCmd,
texture.image,
VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
subresourceRange);Submitting that command buffer will result in an image with a complete mip-chain and all mip levels being transitioned to the proper image layout for shader reads.
Image View creation
The Image View also requires information about how many Mip Levels are used. This is specified in the VkImageViewCreateInfo.subresourceRange.levelCount field.
VkImageViewCreateInfo view = vks::initializers::imageViewCreateInfo();
view.image = texture.image;
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
view.format = format;
view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view.subresourceRange.baseMipLevel = 0;
view.subresourceRange.baseArrayLayer = 0;
view.subresourceRange.layerCount = 1;
view.subresourceRange.levelCount = texture.mipLevels;Overview (`examples/vertexattributes/README.md`)
Vertex attributes
Synopsis
This sample demonstrates two different ways of providing vertex data to the GPU using either interleaved or separate buffers for vertex attributes.
Shader interface
The shader interface for passing the vertex attributes is the same, no matter if the data provided is coming from a single interleaved or multiple separate buffers.
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in vec2 inUV;
layout (location = 3) in vec4 inTangent;Interleaved vertex attributes
In an interleaved vertex buffer, the components that make up a single vertex are stored after each other, so the stride of a single vertex is the sum of it's component's sizes.
// Binding
const std::vector<VkVertexInputBindingDescription> vertexInputBindingsInterleaved = {
{ 0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX },
};
// Attribute
const std::vector<VkVertexInputAttributeDescription> vertexInputAttributesInterleaved = {
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos) },
{ 1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, normal) },
{ 2, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, uv) },
{ 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, tangent) },
};Separate vertex attributes
When using separate buffers, each component is stored in it's own buffer. So e.g. the position buffer only contains vertex positions stored consecutively.
// Bindings
const std::vector<VkVertexInputBindingDescription> vertexInputBindingsSeparate = {
{ 0, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX },
{ 1, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX },
{ 2, sizeof(glm::vec2), VK_VERTEX_INPUT_RATE_VERTEX },
{ 3, sizeof(glm::vec4), VK_VERTEX_INPUT_RATE_VERTEX },
};
// Attributes
const std::vector<VkVertexInputAttributeDescription> vertexInputAttributesSeparate = {
{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 },
{ 1, 1, VK_FORMAT_R32G32B32_SFLOAT, 0 },
{ 2, 2, VK_FORMAT_R32G32_SFLOAT, 0 },
{ 3, 3, VK_FORMAT_R32G32B32A32_SFLOAT, 0 },
};Overview (`external/ktx/README.md`)
The Official Khronos KTX Software Repository
| GNU/Linux, iOS & OSX | Windows | Documentation |
|---|---|---|
This is the official home of the source code
for the Khronos KTX library and tools. KTX is a lightweight file format
for OpenGL textures, designed around how textures are loaded in OpenGL.
See the Doxygen generated live documentation formaster
for API usage information.
See CONTRIBUTING for information about contributing.
See LICENSE for information about licensing.
See BUILDING for information about building the code.
More information about KTX and links to tools that support it can be
found on the
KTX page of
the OpenGL ES SDK on
khronos.org.
If you need help with using the KTX library or KTX tools, please use the
KTX forum.
To report problems use GitHub issues.
IMPORTANT: you must install the Git LFS
command line extension in order to fully checkout this repository after cloning. You
need at least version 1.1.
A few files have $Date$ keywords. If you care about having the proper
dates shown or will be generating the documentation or preparing
distribution archives, you must follow the instructions below.
$Date$ keyword expansion
$Date$ keywords are expanded via a smudge & clean filter. To install
the filter, issue the following commands in the root of your clone.
On Unix (Linux, Mac OS X, etc.) platforms and Windows using Git for Windows'
Git Bash or Cygwin's bash terminal:
./install-gitconfig.sh
rm TODO.md include/ktx.h tools/toktx/toktx.cpp
git checkout TODO.md include/ktx.h tools/toktx/toktx.cppOn Windows with the Command Prompt (requires git.exe in a directory
on your %PATH%):
install-gitconfig.bat
del TODO.md include/ktx.h tools/toktx/toktx.cpp
git checkout TODO.md include/ktx.h tools/toktx/toktx.cppThe first command adds an [include] of the repo's .gitconfig to the
local git config file .git/config, i.e. the one in your clone of the repo..gitconfig contains the config of the "keyworder" filter. The remaining
commands force a new checkout of the affected files to smudge them with the
date. These two are unnecessary if you plan to edit these files.
Overview (`external/tinygltf/README.md`)
Header only C++ tiny glTF library(loader/saver).
TinyGLTF is a header only C++11 glTF 2.0 https://github.com/KhronosGroup/glTF library.
TinyGLTF uses Niels Lohmann's json library(https://github.com/nlohmann/json), so now it requires C++11 compiler.
If you are looking for old, C++03 version, please use devel-picojson branch.
Status
- v2.4.0 Experimental RapidJSON support. Experimental C++14 support(C++14 may give better performance)
- v2.3.0 Modified Material representation according to glTF 2.0 schema(and introduced TextureInfo class)
- v2.2.0 release(Support loading 16bit PNG. Sparse accessor support)
- v2.1.0 release(Draco support)
- v2.0.0 release(22 Aug, 2018)!
Builds
Features
- Written in portable C++. C++-11 with STL dependency only.
- macOS + clang(LLVM)
- iOS + clang
- Linux + gcc/clang
- Windows + MinGW
- Windows + Visual Studio 2015 Update 3 or later.
- Visual Studio 2013 is not supported since they have limited C++11 support and failed to compile
json.hpp.
- Visual Studio 2013 is not supported since they have limited C++11 support and failed to compile
- Android NDK
- Android + CrystaX(NDK drop-in replacement) GCC
- Web using Emscripten(LLVM)
- Moderate parsing time and memory consumption.
- glTF specification v2.0.0
- ASCII glTF
- Load
- Save
- Binary glTF(GLB)
- Load
- Save(.bin embedded .glb)
- ASCII glTF
- Buffers
- Parse BASE64 encoded embedded buffer data(DataURI).
- Load
.binfile.
- Image(Using stb_image)
- Parse BASE64 encoded embedded image data(DataURI).
- Load external image file.
- Load PNG(8bit and 16bit)
- Load JPEG(8bit only)
- Load BMP
- Load GIF
- Custom Image decoder callback(e.g. for decoding OpenEXR image)
- Morph traget
- Sparse accessor
- Load glTF from memory
- Custom callback handler
- Image load
- Image save
- Extensions
- Draco mesh decoding
- Draco mesh encoding
Note on extension property
In extension(ExtensionMap), JSON number value is parsed as int or float(number) and stored as tinygltf::Value object. If you want a floating point value from tinygltf::Value, use GetNumberAsDouble() method.
IsNumber() returns true if the underlying value is an int value or a floating point value.
Examples
- glview : Simple glTF geometry viewer.
- validator : Simple glTF validator with JSON schema.
- basic : Basic glTF viewer with texturing support.
Projects using TinyGLTF
- px_render Single header C++ Libraries for Thread Scheduling, Rendering, and so on... https://github.com/pplux/px
- Physical based rendering with Vulkan using glTF 2.0 models https://github.com/SaschaWillems/Vulkan-glTF-PBR
- GLTF loader plugin for OGRE 2.1. Support for PBR materials via HLMS/PBS https://github.com/Ybalrid/Ogre_glTF
- TinyGltfImporter plugin for Magnum, a lightweight and modular C++11/C++14 graphics middleware for games and data visualization.
- Diligent Engine - A modern cross-platform low-level graphics library and rendering framework
- Lighthouse 2: a rendering framework for real-time ray tracing / path tracing experiments. https://github.com/jbikker/lighthouse2
- QuickLook GLTF - quicklook plugin for macos. Also SceneKit wrapper for tinygltf.
- GlslViewer - live GLSL coding for MacOS and Linux
- Vulkan-Samples - The Vulkan Samples is collection of resources to help you develop optimized Vulkan applications.
- Your projects here! (Please send PR)
TODOs
- Write C++ code generator which emits C++ code from JSON schema for robust parsing.
- Mesh Compression/decompression(Open3DGC, etc)
- Load Draco compressed mesh
- Save Draco compressed mesh
- Open3DGC?
- Support
extensionsandextrasproperty - HDR image?
- OpenEXR extension through TinyEXR.
- 16bit PNG support in Serialization
- Write example and tests for
animationandskin
Licenses
TinyGLTF is licensed under MIT license.
TinyGLTF uses the following third party libraries.
- json.hpp : Copyright (c) 2013-2017 Niels Lohmann. MIT license.
- base64 : Copyright (C) 2004-2008 Renรฉ Nyffenegger
- stb_image.h : v2.08 - public domain image loader - Github link
- stb_image_write.h : v1.09 - public domain image writer - Github link
Build and example
Copy stb_image.h, stb_image_write.h, json.hpp and tiny_gltf.h to your project.
Loading glTF 2.0 model
// Define these only in *one* .cc file.
#define TINYGLTF_IMPLEMENTATION
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
// #define TINYGLTF_NOEXCEPTION // optional. disable exception handling.
#include "tiny_gltf.h"
using namespace tinygltf;
Model model;
TinyGLTF loader;
std::string err;
std::string warn;
bool ret = loader.LoadASCIIFromFile(&model, &err, &warn, argv[1]);
//bool ret = loader.LoadBinaryFromFile(&model, &err, &warn, argv[1]); // for binary glTF(.glb)
if (!warn.empty()) {
printf("Warn: %s\n", warn.c_str());
}
if (!err.empty()) {
printf("Err: %s\n", err.c_str());
}
if (!ret) {
printf("Failed to parse glTF\n");
return -1;
}Compile options
TINYGLTF_NOEXCEPTION: Disable C++ exception in JSON parsing. You can use-fno-exceptionsor by defining the symbolJSON_NOEXCEPTIONandTINYGLTF_NOEXCEPTIONto fully remove C++ exception codes when compiling TinyGLTF.TINYGLTF_NO_STB_IMAGE: Do not load images with stb_image. Instead useTinyGLTF::SetImageLoader(LoadimageDataFunction LoadImageData, void *user_data)to set a callback for loading images.TINYGLTF_NO_STB_IMAGE_WRITE: Do not write images with stb_image_write. Instead useTinyGLTF::SetImageWriter(WriteimageDataFunction WriteImageData, void *user_data)to set a callback for writing images.TINYGLTF_NO_EXTERNAL_IMAGE: Do not try to load external image file. This option would be helpful if you do not want to load image files during glTF parsing.TINYGLTF_ANDROID_LOAD_FROM_ASSETS: Load all files from packaged app assets instead of the regular file system. Note: You must pass a valid asset manager from your android app totinygltf::asset_managerbeforehand.TINYGLTF_ENABLE_DRACO: Enable Draco compression. User must provide include path and link correspnding libraries in your project file.TINYGLTF_NO_INCLUDE_JSON: Disable includingjson.hppfrom withintiny_gltf.hbecause it has been already included before or you want to include it using custom path before includingtiny_gltf.h.TINYGLTF_NO_INCLUDE_STB_IMAGE: Disable includingstb_image.hfrom withintiny_gltf.hbecause it has been already included before or you want to include it using custom path before includingtiny_gltf.h.TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE: Disable includingstb_image_write.hfrom withintiny_gltf.hbecause it has been already included before or you want to include it using custom path before includingtiny_gltf.h.TINYGLTF_USE_RAPIDJSON: Use RapidJSON as a JSON parser/serializer. RapidJSON files are not included in TinyGLTF repo. Please set an include path to RapidJSON if you enable this featrure.TINYGLTF_USE_CPP14: Use C++14 feature(requires C++14 compiler). This may give better performance than C++11.
Saving gltTF 2.0 model
- Buffers.
- To file
- Embedded
- Draco compressed?
- Images
- To file
- Embedded
- Binary(.glb)
- .bin embedded single .glb
- External .bin
Running tests.
glTF parsing test
Setup
Python 2.6 or 2.7 required.
Git clone https://github.com/KhronosGroup/glTF-Sample-Models to your local dir.
Run parsing test
After building loader_example, edit test_runner.py, then,
$ python test_runner.pyUnit tests
$ cd tests
$ make
$ ./tester
$ ./tester_noexceptFuzzing tests
See tests/fuzzer for details.
After running fuzzer on Ryzen9 3950X a week, at least LoadASCIIFromString looks safe except for out-of-memory error in Fuzzer.
We may be better to introduce bounded memory size checking when parsing glTF data.
Third party licenses
- json.hpp : Licensed under the MIT License http://opensource.org/licenses/MIT. Copyright (c) 2013-2017 Niels Lohmann http://nlohmann.me.
- stb_image : Public domain.
- catch : Copyright (c) 2012 Two Blue Cubes Ltd. All rights reserved. Distributed under the Boost Software License, Version 1.0.
- RapidJSON : Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. http://rapidjson.org/
- dlib(uridecode, uriencode) : Copyright (C) 2003 Davis E. King Boost Software License 1.0. http://dlib.net/dlib/server/server_http.cpp.html
Overview (`shaders/hlsl/README.md`)
HLSL Shaders
This directory contains shaders using the HLSL shading language. These can be compiled with DXC using e.g. the included compile.py script.
Known issues
- Specialization constants can't be used to specify array size.
gl_PointCoordnot supported. HLSL has no equivalent. We changed the shaders to calulate the PointCoord manually in the shader. (computenbody,computeparticles,particlesystemexamples).- HLSL doesn't have inverse operation (
deferred,hdr,instancing,skeletalanimation&texturecubemapexamples), these should be done on the CPU - HLSL interface for sparse residency textures is different from GLSL interface. After translating from HLSL to GLSL the shaders behave slightly different. Most important parts do behave identically though.
Overview (`shaders/slang/README.md`)
Slang shaders
This folder contains shaders using the Slang shading language.
These require a different SPIR-V environment than glsl/hlsl. When selecting slang shaders, the base requirement for all samples is raised to at least Vulkan 1.1 with the SPIRV 1.4 extension.
If you want to compile these shaders to SPIR-V, please use the latest release from here to get the latest bug fixes and features required for some of the samples. Minimum version for all shader to properly compile is 2025.16.1.
--- METRICS ---
- Files Extracted: 12
- Estimated Token Budget: ~27390 tokens
- Canonical Reference: https://codewiki.google/github.com/SaschaWillems/Vulkan