Vulkan Documentation

C++ examples for the Vulkan graphics API

12,187 stars GLSL 12 files ยท ~27,473 tokens #glsl#hlsl#slang#vulkan#vulkan-api
RAW Doc

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

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:

text
git clone --recursive https://github.com/SaschaWillems/Vulkan.git

Existing repositories can be updated manually:

text
git submodule init
git submodule update

Building

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:

text
--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 present

Note 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.

  • Pipelines

    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.

  • Descriptor sets

    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

    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.

  • Push constants

    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.

  • Specialization constants

    Uses SPIR-V specialization constants to create multiple pipelines with different lighting paths from a single "uber" shader.

  • Texture mapping

    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.

  • Texture arrays

    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.

  • Cube map textures

    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.

  • Cube map arrays

    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.

  • 3D textures

    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.

  • Input attachments

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

  • Sub passes

    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.

  • Offscreen rendering

    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.

  • CPU particle system

    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.

  • Stencil buffer

    Uses the stencil buffer and its compare functionality for rendering a 3D model with dynamic outlines.

  • Vertex attributes

    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.

  • glTF vertex skinning

    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.

  • glTF scene rendering

    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

  • Multi sampling

    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.

  • High dynamic range

    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.

  • Shadow 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.

  • Cascaded shadow mapping

    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.

  • Run-time mip-map generation

    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 screenshots

    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.

  • Instancing

    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.

  • Indirect drawing

    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.

  • Occlusion queries

    Using query pool objects to get number of passed samples for rendered primitives got determining on-screen visibility.

  • Pipeline statistics

    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.

  • PBR basics

    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.

  • PBR image based lighting

    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.

  • Textured PBR with IBL

    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.

  • Deferred shading basics

    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.

  • Deferred multi sampling

    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.

  • Image processing

    Uses a compute shader along with a separate compute queue to apply different convolution kernels (and effects) on an input image in realtime.

  • GPU particle system

    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

    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.

  • Ray tracing

    Simple GPU ray tracer with shadows and reflections using a compute shader. No scene geometry is rendered in the graphics pass.

  • Cloth simulation

    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.

  • Cull and LOD

    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

  • Normal debugging

    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,

  • Viewport arrays

    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

  • Displacement mapping

    Uses a height map to dynamically generate and displace additional geometric detail for a low-poly mesh.

  • Dynamic terrain tessellation

    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.

  • Model tessellation

    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 ray tracing

    Basic example for doing hardware accelerated ray tracing using the VK_KHR_acceleration_structure and VK_KHR_ray_tracing_pipeline extensions. Shows how to setup acceleration structures, ray tracing pipelines and the shader binding table needed to do the actual ray tracing.

  • Ray traced shadows

    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.

  • Ray traced reflections

    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.

  • Ray traced texture mapping

    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 ray tracing shaders

    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.

  • Ray traced glTF

    Renders a textured glTF model using ray traying instead of rasterization. Makes use of frame accumulation for transparency and anti aliasing.

  • Ray query

    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.

  • Position fetch

    Uses the VK_KHR_ray_tracing_position_fetch extension 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).

  • Render

    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.

  • Compute

    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

  • Text rendering

    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.

  • Distance field fonts

    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.

  • ImGui overlay

    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_rasterization

    Uses 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_descriptor

    Uses 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_block

    Makes 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_multiview

    Renders 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_rendering

    Demonstrates 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_info

    Shows 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_utils

    Shows 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_Maintenance1 or Vulkan 1.1

    Shows 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_rate

    Uses 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_indexing

    Demonstrates 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_qualifier and SPV_EXT_descriptor_indexing.

  • Dynamic rendering - VK_KHR_dynamic_rendering

    Shows 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_read

    Shows 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_rendering

    Based on the dynamic rendering sample, this sample shows how to do implement multi sampling with dynamic rendering.

  • Graphics pipeline library - VK_EXT_graphics_pipeline_library

    Uses 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_shader

    Basic sample demonstrating how to use the mesh shading pipeline as a replacement for the traditional vertex pipeline.

  • Descriptor heap - VK_EXT_descriptor_heap

    Basic sample showing how to use descriptor heaps, which fully replace Vulkan's original descriptor system. Khronos blog.

  • Descriptor heap (untyped pointers) - VK_EXT_descriptor_heap and VK_KHR_shader_untyped_pointers

    Basic 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_buffer

    Basic 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_object

    Basic 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_copy

    Shows 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_addres

    Demonstrates 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_semaphore

    Shows 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_barycentric

    Demonstrates 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.

  • Fullscreen radial blur

    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.

  • Bloom

    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.

  • Parallax mapping

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

  • Spherical environment mapping

    Uses a spherical material capture texture array defining environment lighting and reflection information to fake complex lighting.

Misc

  • Vulkan Gears

    Vulkan interpretation of glxgears. Procedurally generates and animates multiple gears.

  • Vulkan demo scene

    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.

glsl
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:

cpp
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:

cpp
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:

cpp
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:

cpp
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:

cpp
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:

cpp
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.

cpp
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.

cpp
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.

cpp
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:

cpp
// POI: This sample uses normal mapping, so we also need to load the tangents from the glTF file

For 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:

cpp
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

cpp
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:

cpp
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:

cpp
// 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:

cpp
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:

cpp
// 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:

cpp
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:

glsl
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:

glsl
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:

cpp
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:

cpp
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:

cpp
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:

glsl
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:

cpp
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:

glsl
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:

cpp
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:

cpp
// POI: Get buffer data required for vertex skinning

Data structures

Several new data structures are required for doing animations with vertex skinning. The official glTF spec has the details on those.

Node additions

cpp
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

cpp
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

glTF spec chapter on skins

cpp
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

cpp
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

cpp
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

cpp
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:

cpp
// 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:

cpp
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:

glsl
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:

cpp
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:

glsl
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:

cpp
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:

cpp
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:

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

cpp
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:

cpp
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:

cpp
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:

cpp
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:

cpp
for (auto &node : nodes)
{
  updateJoints(node);
}

The updateJoints function will calculate the actual joint matrices and update the shader storage buffer object:

cpp
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:

cpp
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:

cpp
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:

cpp
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:

glsl
#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:

glsl
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:

cpp
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:

cpp
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:

cpp
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:

cpp
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:

cpp
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:

cpp
for (auto j = 0; j < indirectCommands.size(); j++)
{
  vkCmdDrawIndexedIndirect(cmdBuffer, indirectCommandsBuffer.buffer, j * sizeof(VkDrawIndexedIndirectCommand), 1, sizeof(VkDrawIndexedIndirectCommand));
}

Acknowledgments


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:

cpp
texture.mipLevels = floor(log2(std::max(texture.width, texture.height))) + 1;

This is then passed to the image creat info:

cpp
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:

cpp
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:

cpp
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:

cpp
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]:

cpp
// 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:

cpp
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):

cpp
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:

cpp
// 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:

cpp
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.

cpp
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.

glsl
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.

cpp
// 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.

cpp
// 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 for
master
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:

bash
./install-gitconfig.sh
rm TODO.md include/ktx.h tools/toktx/toktx.cpp
git checkout TODO.md include/ktx.h tools/toktx/toktx.cpp

On Windows with the Command Prompt (requires git.exe in a directory
on your %PATH%):

cmd
install-gitconfig.bat
del TODO.md include/ktx.h tools/toktx/toktx.cpp
git checkout TODO.md include/ktx.h tools/toktx/toktx.cpp

The 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.
    • 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)
  • Buffers
    • Parse BASE64 encoded embedded buffer data(DataURI).
    • Load .bin file.
  • 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

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 extensions and extras property
  • HDR image?
    • OpenEXR extension through TinyEXR.
  • 16bit PNG support in Serialization
  • Write example and tests for animation and skin

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

c++
// 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-exceptions or by defining the symbol JSON_NOEXCEPTION and TINYGLTF_NOEXCEPTION to fully remove C++ exception codes when compiling TinyGLTF.
  • TINYGLTF_NO_STB_IMAGE : Do not load images with stb_image. Instead use TinyGLTF::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 use TinyGLTF::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 to tinygltf::asset_manager beforehand.
  • TINYGLTF_ENABLE_DRACO: Enable Draco compression. User must provide include path and link correspnding libraries in your project file.
  • TINYGLTF_NO_INCLUDE_JSON : Disable including json.hpp from within tiny_gltf.h because it has been already included before or you want to include it using custom path before including tiny_gltf.h.
  • TINYGLTF_NO_INCLUDE_STB_IMAGE : Disable including stb_image.h from within tiny_gltf.h because it has been already included before or you want to include it using custom path before including tiny_gltf.h.
  • TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE : Disable including stb_image_write.h from within tiny_gltf.h because it has been already included before or you want to include it using custom path before including tiny_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,

bash
$ python test_runner.py

Unit tests

bash
$ cd tests
$ make
$ ./tester
$ ./tester_noexcept

Fuzzing 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


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_PointCoord not supported. HLSL has no equivalent. We changed the shaders to calulate the PointCoord manually in the shader. (computenbody, computeparticles, particlesystem examples).
  • HLSL doesn't have inverse operation (deferred, hdr, instancing, skeletalanimation & texturecubemap examples), 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 ---