A 3D rendering engine built from scratch using Vulkan API and C++.
- Clustered Forward Shading for efficient light culling.
- Physically-Based Rendering (PBR) with Cook-Torrance microfacet and Image-Based Lighting (IBL).
- Hardware-Accelerated Path Tracing that can simulate indirect shading, reflections, and soft shadow.
- Bindless techniques using Indirect Draw, Descriptor Indexing, and Buffer Device Address.
- Compute-Based Frustum Culling.
- Compute-Based Skinning for skeletal animation.
- Shadow Maps with Poisson Disk or PCF.
- Screen Space Ambient Occlusion (SSAO).
- Multisample anti-aliasing (MSAA).
- Tonemap postprocessing.
- Automatic runtime compilation from GLSL to SPIR-V using
glslang
. - Lightweight abstraction layer on top of Vulkan for faster development.
- Additional features: skybox, infinite grid, line rendering, and ImGui / ImGuizmo.
Inspired by GPU-driven rendering, the engine leverages several modern GPU features. First, bindless textures is achieved by utilizing descriptor Indexing. This enables the storage of all scene textures inside an unbounded array, which allows texture descriptors to be bound once at the start of a frame.
Next, the engine takes advantage of indirect draw API. The CPU prepares indirect draw commands and stores them in an indirect buffer. These draw commands are then sorted by material type. This allows the rendering process to be divided into separate render passes based on material type. Currently, there are two passes: opaque and transparent.
Finally, the engine leverages bindless concept using buffer device addresses, allowing shaders to directly access buffers without creating descriptors.
![bindless_shadow_mapping_1](https://private-user-images.githubusercontent.com/790432/322293051-03200177-9bc7-45c3-be0a-093286c6eef9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQyODQ2ODUsIm5iZiI6MTcyNDI4NDM4NSwicGF0aCI6Ii83OTA0MzIvMzIyMjkzMDUxLTAzMjAwMTc3LTliYzctNDVjMy1iZTBhLTA5MzI4NmM2ZWVmOS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODIxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgyMVQyMzUzMDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hYjEzYjcyZjQ0NjAzMmVjNjFjNDU0YmY3MjM5ZDA2OGRmODkxNjRlZWE5ODkzZjcxMGQ3ZDFlYjFmYTEwYWJiJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.JLPS4XePUFwZjW83MS5hUu-Cu1teCh8x84B1gsYUsqI)
The path tracing process begins with building acceleration structures containing multiple geometries. After creating a raytracing pipeline, the ray simulation requires several shaders. Ray generation shader is responsible to generate rays, and add the hit color into an accumulator image. The final rendering is obtained by averaging the accumulator image. The next one is Closest hit shader that determines the color when a ray intersects an object and can also scatter the ray for further bounces. Optionally, Any hit shader is used to discard a ray hit in order to render transparent materials such as foliage textures.
![hardware_raytracing](https://private-user-images.githubusercontent.com/790432/321784351-c678d37a-d808-425b-830f-82ec24df7d8d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQyODQ2ODUsIm5iZiI6MTcyNDI4NDM4NSwicGF0aCI6Ii83OTA0MzIvMzIxNzg0MzUxLWM2NzhkMzdhLWQ4MDgtNDI1Yi04MzBmLTgyZWMyNGRmN2Q4ZC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODIxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgyMVQyMzUzMDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mY2JkNjRhOTQwOTZmNzgyZTI4MTE3NmZiZjQ2OGE3MWYyNTNlOTZlYjc5MmNmODJkN2M0YzdiOGNlMjhiZGQzJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.jRVJK9ZZoOu-Yuu8o_ur546Op8Cx8xy-ANCIkkeytX0)
![hardware_raytracing](https://private-user-images.githubusercontent.com/790432/321933209-0b6f368f-943d-4f24-b129-6d8f9af9763b.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQyODQ2ODUsIm5iZiI6MTcyNDI4NDM4NSwicGF0aCI6Ii83OTA0MzIvMzIxOTMzMjA5LTBiNmYzNjhmLTk0M2QtNGYyNC1iMTI5LTZkOGY5YWY5NzYzYi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODIxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgyMVQyMzUzMDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT00MGI1YzhhMzI3MWY5M2E0YTBmY2VlNTY3MGQ0MTIxNmMwY2YyZjIxZmMxZWFkMzdiNDc1ZGJiMTYxODYxMDMxJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.w-wgnnMjq4d3CcT-C9plS3HnSKv-QVSL_n2ab2p9if4)
The technique consists of two steps that are executed in compute shaders. The first step is to subdivide the view frustum into AABB clusters. The next step is light culling, where it calculates lights that intersect the clusters. This step removes lights that are too far from a fragment, leading to reduced light iteration in the final fragment shader.
Preliminary testing using a 3070M graphics card shows the technique can render a PBR Sponza scene in 2560x1600 resolution with over 1000 dynamic lights at 60-100 FPS. If too many lights end up inside the view frustum, especially when zooming out, there may be a drop in frame rate, but still much faster than a naive forward shading.
vulkan_cluster_forward.mp4
Since the engine uses indirect draw, frustum culling can now be done entirely on the compute shader by modifying draw commands within an indirect buffer. If an object's AABB falls outside the camera frustum, the compute shader will deactivate the draw command for that object. Consequently, the CPU is unaware of the number of objects actually drawn. Using Tracy profiler, an intersection test with 10,000 AABBs only takes less than 25 microseconds (0.025 milliseconds).
The left image below shows a rendering of all objects inside the frustum. The right image shows visualizations of AABBs as translucent boxes and the frustum drawn as orange lines.
![frustum_culling](https://private-user-images.githubusercontent.com/790432/314430976-d42100a6-f95d-41ec-8e53-012b22e4175b.jpg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQyODQ2ODUsIm5iZiI6MTcyNDI4NDM4NSwicGF0aCI6Ii83OTA0MzIvMzE0NDMwOTc2LWQ0MjEwMGE2LWY5NWQtNDFlYy04ZTUzLTAxMmIyMmU0MTc1Yi5qcGc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODIxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgyMVQyMzUzMDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT02MDlmYWQ1OGY0MWUzYTNiOWI3Yjk0MDAwMmU2NTQzMjk3ZWEwYjdkMTQ4M2UyYzE4ODZhMDhmMDZjNDk0NDNiJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.Io5vzppyEpabduiKqVPCd5OPttf0FqubdQQNC4hhze8)
The compute-based skinning approach is much simpler than the traditional vertex shader skinning. This is because the skinning computation is done only once using a compute shader at the beginning of the frame. The resulting skinned vertices are then stored in a buffer, which are then read in subsequent render passes like shadow mapping, SSAO, and lighting. Consequently, there is no need to modify existing pipelines and no extra shader permutations.
compute_skinning.mp4
The left image below is a rendering that uses four cascade shadow maps, resulting in sharper shadows. The right image above showcases the individual cascades with color coding. Poisson disk sampling helps to reduce projective aliasing artifacts, but can create more noticeable seams between cascades with excessive blurring.
![cascade_shadow_mapping](https://private-user-images.githubusercontent.com/790432/310877905-1634a491-ea8f-49f0-8214-766a038bedd1.jpg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQyODQ2ODUsIm5iZiI6MTcyNDI4NDM4NSwicGF0aCI6Ii83OTA0MzIvMzEwODc3OTA1LTE2MzRhNDkxLWVhOGYtNDlmMC04MjE0LTc2NmEwMzhiZWRkMS5qcGc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODIxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgyMVQyMzUzMDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kNmM5ZDk2YzhhMTYxOTk4OTEyOTMzYTZkZjhlOGI4ZDg4MzY2M2JkOGYxNjQ3YWNlN2NjN2MxYzI5NDI2MTQwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.hPKmveIY12m3cuFUthhUTKNnvvH84nInxoxfhHrPWo8)
- C++20
- Vulkan 1.3
- Dependencies located in
External
folder - Link to build instruction