Added cascaded shadow mapping example
This commit is contained in:
parent
f4281096ea
commit
3c150e18f3
14 changed files with 1081 additions and 0 deletions
17
data/shaders/shadowmappingcascade/debugshadowmap.frag
Normal file
17
data/shaders/shadowmappingcascade/debugshadowmap.frag
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout (binding = 1) uniform sampler2DArray shadowMap;
|
||||||
|
|
||||||
|
layout (location = 0) in vec2 inUV;
|
||||||
|
|
||||||
|
layout (location = 0) out vec4 outFragColor;
|
||||||
|
|
||||||
|
layout(push_constant) uniform PushConsts {
|
||||||
|
uint cascadeIndex;
|
||||||
|
} pushConsts;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
float depth = texture(shadowMap, vec3(inUV, float(pushConsts.cascadeIndex))).r;
|
||||||
|
outFragColor = vec4(vec3((depth)), 1.0);
|
||||||
|
}
|
||||||
BIN
data/shaders/shadowmappingcascade/debugshadowmap.frag.spv
Normal file
BIN
data/shaders/shadowmappingcascade/debugshadowmap.frag.spv
Normal file
Binary file not shown.
14
data/shaders/shadowmappingcascade/debugshadowmap.vert
Normal file
14
data/shaders/shadowmappingcascade/debugshadowmap.vert
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout (location = 0) out vec2 outUV;
|
||||||
|
|
||||||
|
out gl_PerVertex {
|
||||||
|
vec4 gl_Position;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||||
|
gl_Position = vec4(outUV * 2.0f - 1.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
BIN
data/shaders/shadowmappingcascade/debugshadowmap.vert.spv
Normal file
BIN
data/shaders/shadowmappingcascade/debugshadowmap.vert.spv
Normal file
Binary file not shown.
5
data/shaders/shadowmappingcascade/depthpass.frag
Normal file
5
data/shaders/shadowmappingcascade/depthpass.frag
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
}
|
||||||
BIN
data/shaders/shadowmappingcascade/depthpass.frag.spv
Normal file
BIN
data/shaders/shadowmappingcascade/depthpass.frag.spv
Normal file
Binary file not shown.
23
data/shaders/shadowmappingcascade/depthpass.vert
Normal file
23
data/shaders/shadowmappingcascade/depthpass.vert
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout (location = 0) in vec3 inPos;
|
||||||
|
|
||||||
|
// todo: pass via specialization constant
|
||||||
|
#define SHADOW_MAP_CASCADE_COUNT 4
|
||||||
|
|
||||||
|
layout(push_constant) uniform PushConsts {
|
||||||
|
uint cascadeIndex;
|
||||||
|
} pushConsts;
|
||||||
|
|
||||||
|
layout (binding = 0) uniform UBO {
|
||||||
|
mat4[SHADOW_MAP_CASCADE_COUNT] cascadeViewProjMat;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
out gl_PerVertex {
|
||||||
|
vec4 gl_Position;
|
||||||
|
};
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = ubo.cascadeViewProjMat[pushConsts.cascadeIndex] * vec4(inPos, 1.0);
|
||||||
|
}
|
||||||
BIN
data/shaders/shadowmappingcascade/depthpass.vert.spv
Normal file
BIN
data/shaders/shadowmappingcascade/depthpass.vert.spv
Normal file
Binary file not shown.
116
data/shaders/shadowmappingcascade/scene.frag
Normal file
116
data/shaders/shadowmappingcascade/scene.frag
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
#define SHADOW_MAP_CASCADE_COUNT 4
|
||||||
|
|
||||||
|
layout (binding = 1) uniform sampler2DArray shadowMap;
|
||||||
|
|
||||||
|
layout (location = 0) in vec3 inNormal;
|
||||||
|
layout (location = 1) in vec3 inColor;
|
||||||
|
layout (location = 2) in vec3 inViewPos;
|
||||||
|
layout (location = 3) in vec3 inPos;
|
||||||
|
|
||||||
|
layout (constant_id = 0) const int enablePCF = 0;
|
||||||
|
|
||||||
|
layout (location = 0) out vec4 outFragColor;
|
||||||
|
|
||||||
|
#define ambient 0.1
|
||||||
|
|
||||||
|
layout (binding = 2) uniform UBO {
|
||||||
|
vec4 cascadeSplits;
|
||||||
|
mat4 cascadeViewProjMat[SHADOW_MAP_CASCADE_COUNT];
|
||||||
|
mat4 inverseViewMat;
|
||||||
|
vec3 lightDir;
|
||||||
|
float _pad;
|
||||||
|
int colorCascades;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
const mat4 biasMat = mat4(
|
||||||
|
0.5, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.5, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0, 0.0,
|
||||||
|
0.5, 0.5, 0.0, 1.0
|
||||||
|
);
|
||||||
|
|
||||||
|
float textureProj(vec4 P, vec2 offset, uint cascadeIndex)
|
||||||
|
{
|
||||||
|
float shadow = 1.0;
|
||||||
|
float bias = 0.005;
|
||||||
|
|
||||||
|
vec4 shadowCoord = P / P.w;
|
||||||
|
if ( shadowCoord.z > -1.0 && shadowCoord.z < 1.0 ) {
|
||||||
|
float dist = texture(shadowMap, vec3(shadowCoord.st + offset, cascadeIndex)).r;
|
||||||
|
if (shadowCoord.w > 0 && dist < shadowCoord.z - bias) {
|
||||||
|
shadow = ambient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shadow;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
float filterPCF(vec4 sc, uint cascadeIndex)
|
||||||
|
{
|
||||||
|
ivec2 texDim = textureSize(shadowMap, 0).xy;
|
||||||
|
float scale = 1.5;
|
||||||
|
float dx = scale * 1.0 / float(texDim.x);
|
||||||
|
float dy = scale * 1.0 / float(texDim.y);
|
||||||
|
|
||||||
|
float shadowFactor = 0.0;
|
||||||
|
int count = 0;
|
||||||
|
int range = 1;
|
||||||
|
|
||||||
|
for (int x = -range; x <= range; x++) {
|
||||||
|
for (int y = -range; y <= range; y++) {
|
||||||
|
shadowFactor += textureProj(sc, vec2(dx*x, dy*y), cascadeIndex);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shadowFactor / count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// Get cascade index for the current fragment's view position
|
||||||
|
uint cascadeIndex = 0;
|
||||||
|
for(uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; ++i) {
|
||||||
|
if(inViewPos.z < ubo.cascadeSplits[i]) {
|
||||||
|
cascadeIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depth compare for shadowing
|
||||||
|
vec4 shadowCoord = (biasMat * ubo.cascadeViewProjMat[cascadeIndex]) * vec4(inPos, 1.0);
|
||||||
|
|
||||||
|
float shadow = 0;
|
||||||
|
if (enablePCF == 1) {
|
||||||
|
shadow = filterPCF(shadowCoord / shadowCoord.w, cascadeIndex);
|
||||||
|
} else {
|
||||||
|
shadow = textureProj(shadowCoord / shadowCoord.w, vec2(0.0), cascadeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directional light
|
||||||
|
vec3 N = normalize(inNormal);
|
||||||
|
vec3 L = normalize(-ubo.lightDir);
|
||||||
|
vec3 H = normalize(L + inViewPos);
|
||||||
|
float diffuse = max(dot(N, L), 0.0);
|
||||||
|
vec3 lightColor = vec3(1.0);
|
||||||
|
outFragColor.rgb = max(lightColor * (diffuse ), vec3(0.0));
|
||||||
|
outFragColor.rgb *= shadow;
|
||||||
|
|
||||||
|
// Color cascades (if enabled)
|
||||||
|
if (ubo.colorCascades == 1) {
|
||||||
|
switch(cascadeIndex) {
|
||||||
|
case 0 :
|
||||||
|
outFragColor.rgb *= vec3(1.0f, 0.0f, 0.0f);
|
||||||
|
break;
|
||||||
|
case 1 :
|
||||||
|
outFragColor.rgb *= vec3(0.0f, 1.0f, 0.0f);
|
||||||
|
break;
|
||||||
|
case 2 :
|
||||||
|
outFragColor.rgb *= vec3(0.0f, 0.0f, 1.0f);
|
||||||
|
break;
|
||||||
|
case 3 :
|
||||||
|
outFragColor.rgb *= vec3(1.0f, 1.0f, 0.0f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
data/shaders/shadowmappingcascade/scene.frag.spv
Normal file
BIN
data/shaders/shadowmappingcascade/scene.frag.spv
Normal file
Binary file not shown.
31
data/shaders/shadowmappingcascade/scene.vert
Normal file
31
data/shaders/shadowmappingcascade/scene.vert
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout (location = 0) in vec3 inPos;
|
||||||
|
layout (location = 1) in vec2 inUV;
|
||||||
|
layout (location = 2) in vec3 inColor;
|
||||||
|
layout (location = 3) in vec3 inNormal;
|
||||||
|
|
||||||
|
layout (binding = 0) uniform UBO {
|
||||||
|
mat4 projection;
|
||||||
|
mat4 view;
|
||||||
|
mat4 model;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
layout (location = 0) out vec3 outNormal;
|
||||||
|
layout (location = 1) out vec3 outColor;
|
||||||
|
layout (location = 2) out vec3 outViewPos;
|
||||||
|
layout (location = 3) out vec3 outPos;
|
||||||
|
|
||||||
|
out gl_PerVertex {
|
||||||
|
vec4 gl_Position;
|
||||||
|
};
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
outColor = inColor;
|
||||||
|
outNormal = inNormal;
|
||||||
|
outPos = inPos;
|
||||||
|
outViewPos = (ubo.view * vec4(inPos.xyz, 1.0)).xyz;
|
||||||
|
gl_Position = ubo.projection * ubo.view * ubo.model * vec4(inPos.xyz, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
BIN
data/shaders/shadowmappingcascade/scene.vert.spv
Normal file
BIN
data/shaders/shadowmappingcascade/scene.vert.spv
Normal file
Binary file not shown.
|
|
@ -78,6 +78,7 @@ set(EXAMPLES
|
||||||
screenshot
|
screenshot
|
||||||
shadowmapping
|
shadowmapping
|
||||||
shadowmappingomni
|
shadowmappingomni
|
||||||
|
shadowmappingcascade
|
||||||
skeletalanimation
|
skeletalanimation
|
||||||
specializationconstants
|
specializationconstants
|
||||||
sphericalenvmapping
|
sphericalenvmapping
|
||||||
|
|
|
||||||
874
examples/shadowmappingcascade/shadowmappingcascade.cpp
Normal file
874
examples/shadowmappingcascade/shadowmappingcascade.cpp
Normal file
|
|
@ -0,0 +1,874 @@
|
||||||
|
/*
|
||||||
|
* Vulkan Example - Cascaded shadow mapping for directional light sources
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 by Sascha Willems - www.saschawillems.de
|
||||||
|
*
|
||||||
|
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
|
||||||
|
*/
|
||||||
|
// Note: Could be simplified with a layered frame buffer using geometry shaders (not available on all devices)
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define GLM_FORCE_RADIANS
|
||||||
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
#include "vulkanexamplebase.h"
|
||||||
|
#include "VulkanBuffer.hpp"
|
||||||
|
#include "VulkanModel.hpp"
|
||||||
|
|
||||||
|
#define ENABLE_VALIDATION false
|
||||||
|
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
#define SHADOWMAP_DIM 1024
|
||||||
|
#else
|
||||||
|
#define SHADOWMAP_DIM 2048
|
||||||
|
#endif
|
||||||
|
#define SHADOWMAP_FILTER VK_FILTER_LINEAR
|
||||||
|
|
||||||
|
#define SHADOW_MAP_CASCADE_COUNT 4
|
||||||
|
|
||||||
|
class VulkanExample : public VulkanExampleBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool displayDepthMap = false;
|
||||||
|
int32_t displayDepthMapCascadeIndex = 0;
|
||||||
|
bool colorCascades = false;
|
||||||
|
bool filterPCF = false;
|
||||||
|
|
||||||
|
float cascadeSplitLambda = 1.0f;
|
||||||
|
|
||||||
|
float zNear = 0.5f;
|
||||||
|
float zFar = 48.0f;
|
||||||
|
|
||||||
|
glm::vec3 lightPos = glm::vec3();
|
||||||
|
|
||||||
|
// Vertex layout for the models
|
||||||
|
vks::VertexLayout vertexLayout = vks::VertexLayout({
|
||||||
|
vks::VERTEX_COMPONENT_POSITION,
|
||||||
|
vks::VERTEX_COMPONENT_UV,
|
||||||
|
vks::VERTEX_COMPONENT_COLOR,
|
||||||
|
vks::VERTEX_COMPONENT_NORMAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<vks::Model> scenes;
|
||||||
|
std::vector<std::string> sceneNames;
|
||||||
|
int32_t sceneIndex = 0;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
vks::Buffer VS;
|
||||||
|
vks::Buffer FS;
|
||||||
|
} uniformBuffers;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
glm::mat4 projection;
|
||||||
|
glm::mat4 view;
|
||||||
|
glm::mat4 model;
|
||||||
|
glm::vec3 lightDir;
|
||||||
|
} uboVS;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
float cascadeSplits[4];
|
||||||
|
glm::mat4 cascadeViewProjMat[4];
|
||||||
|
glm::mat4 inverseViewMat;
|
||||||
|
glm::vec3 lightDir;
|
||||||
|
float _pad;
|
||||||
|
int32_t colorCascades;
|
||||||
|
} uboFS;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
VkPipeline debugShadowMap;
|
||||||
|
VkPipeline sceneShadow;
|
||||||
|
VkPipeline sceneShadowPCF;
|
||||||
|
} pipelines;
|
||||||
|
|
||||||
|
VkPipelineLayout pipelineLayout;
|
||||||
|
VkDescriptorSet descriptorSet;
|
||||||
|
VkDescriptorSetLayout descriptorSetLayout;
|
||||||
|
|
||||||
|
// Resources of the depth map generation pass
|
||||||
|
struct DepthPass {
|
||||||
|
VkRenderPass renderPass;
|
||||||
|
VkCommandBuffer commandBuffer;
|
||||||
|
VkSemaphore semaphore;
|
||||||
|
VkPipelineLayout pipelineLayout;
|
||||||
|
VkPipeline pipeline;
|
||||||
|
vks::Buffer uniformBuffer;
|
||||||
|
|
||||||
|
struct UniformBlock {
|
||||||
|
std::array<glm::mat4, SHADOW_MAP_CASCADE_COUNT> cascadeViewProjMat;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
} depthPass;
|
||||||
|
|
||||||
|
// Layered depth image containing the shadow cascade depths
|
||||||
|
struct DepthImage {
|
||||||
|
VkImage image;
|
||||||
|
VkDeviceMemory mem;
|
||||||
|
VkImageView view;
|
||||||
|
VkSampler sampler;
|
||||||
|
void destroy(VkDevice device) {
|
||||||
|
vkDestroyImageView(device, view, nullptr);
|
||||||
|
vkDestroyImage(device, image, nullptr);
|
||||||
|
vkFreeMemory(device, mem, nullptr);
|
||||||
|
vkDestroySampler(device, sampler, nullptr);
|
||||||
|
}
|
||||||
|
} depth;
|
||||||
|
|
||||||
|
// Contains all resources required for a single shadow map cascade
|
||||||
|
struct Cascade {
|
||||||
|
VkFramebuffer frameBuffer;
|
||||||
|
VkDescriptorSet descriptorSet;
|
||||||
|
VkImageView view;
|
||||||
|
|
||||||
|
float splitDepth;
|
||||||
|
glm::mat4 viewProjMatrix;
|
||||||
|
|
||||||
|
void destroy(VkDevice device) {
|
||||||
|
vkDestroyImageView(device, view, nullptr);
|
||||||
|
vkDestroyFramebuffer(device, frameBuffer, nullptr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::array<Cascade, SHADOW_MAP_CASCADE_COUNT> cascades;
|
||||||
|
|
||||||
|
VulkanExample() : VulkanExampleBase(ENABLE_VALIDATION)
|
||||||
|
{
|
||||||
|
title = "Cascaded shadow mapping";
|
||||||
|
timerSpeed *= 0.05f;
|
||||||
|
camera.type = Camera::CameraType::firstperson;
|
||||||
|
camera.movementSpeed = 2.5f;
|
||||||
|
camera.setPerspective(45.0f, (float)width / (float)height, zNear, zFar);
|
||||||
|
camera.setPosition(glm::vec3(8.75f, 0.5f, -8.3f));
|
||||||
|
camera.setRotation(glm::vec3(-1.0f, 50.0f, 0.0f));
|
||||||
|
settings.overlay = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~VulkanExample()
|
||||||
|
{
|
||||||
|
for (auto cascade : cascades) {
|
||||||
|
cascade.destroy(device);
|
||||||
|
}
|
||||||
|
depth.destroy(device);
|
||||||
|
|
||||||
|
vkDestroyRenderPass(device, depthPass.renderPass, nullptr);
|
||||||
|
|
||||||
|
vkDestroyPipeline(device, pipelines.debugShadowMap, nullptr);
|
||||||
|
vkDestroyPipeline(device, depthPass.pipeline, nullptr);
|
||||||
|
vkDestroyPipeline(device, pipelines.sceneShadow, nullptr);
|
||||||
|
vkDestroyPipeline(device, pipelines.sceneShadowPCF, nullptr);
|
||||||
|
|
||||||
|
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
|
||||||
|
vkDestroyPipelineLayout(device, depthPass.pipelineLayout, nullptr);
|
||||||
|
|
||||||
|
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
|
||||||
|
|
||||||
|
for (auto scene : scenes) {
|
||||||
|
scene.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
depthPass.uniformBuffer.destroy();
|
||||||
|
uniformBuffers.VS.destroy();
|
||||||
|
uniformBuffers.FS.destroy();
|
||||||
|
|
||||||
|
vkFreeCommandBuffers(device, cmdPool, 1, &depthPass.commandBuffer);
|
||||||
|
vkDestroySemaphore(device, depthPass.semaphore, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void getEnabledFeatures()
|
||||||
|
{
|
||||||
|
// Depth clamp to avoid near plane clipping
|
||||||
|
enabledFeatures.depthClamp = deviceFeatures.depthClamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup resources used for the shadow map cascades
|
||||||
|
void prepareShadowMaps()
|
||||||
|
{
|
||||||
|
VkFormat depthFormat;
|
||||||
|
vks::tools::getSupportedDepthFormat(physicalDevice, &depthFormat);
|
||||||
|
|
||||||
|
depthPass.commandBuffer = VulkanExampleBase::createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, false);
|
||||||
|
// Create a semaphore used to synchronize offscreen rendering and usage
|
||||||
|
VkSemaphoreCreateInfo semaphoreCreateInfo = vks::initializers::semaphoreCreateInfo();
|
||||||
|
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &depthPass.semaphore));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Depth map renderpass
|
||||||
|
*/
|
||||||
|
|
||||||
|
VkAttachmentDescription attachmentDescription{};
|
||||||
|
attachmentDescription.format = depthFormat;
|
||||||
|
attachmentDescription.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
attachmentDescription.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||||
|
attachmentDescription.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||||
|
attachmentDescription.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||||
|
attachmentDescription.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||||
|
attachmentDescription.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
attachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
|
||||||
|
|
||||||
|
VkAttachmentReference depthReference = {};
|
||||||
|
depthReference.attachment = 0;
|
||||||
|
depthReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||||
|
|
||||||
|
VkSubpassDescription subpass = {};
|
||||||
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||||
|
subpass.colorAttachmentCount = 0;
|
||||||
|
subpass.pDepthStencilAttachment = &depthReference;
|
||||||
|
|
||||||
|
// Use subpass dependencies for layout transitions
|
||||||
|
std::array<VkSubpassDependency, 2> dependencies;
|
||||||
|
|
||||||
|
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
|
||||||
|
dependencies[0].dstSubpass = 0;
|
||||||
|
dependencies[0].srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
||||||
|
dependencies[0].dstStageMask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||||
|
dependencies[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
|
dependencies[0].dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||||
|
dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
|
||||||
|
|
||||||
|
dependencies[1].srcSubpass = 0;
|
||||||
|
dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||||
|
dependencies[1].srcStageMask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||||
|
dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
||||||
|
dependencies[1].srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||||
|
dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
|
dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
|
||||||
|
|
||||||
|
VkRenderPassCreateInfo renderPassCreateInfo = vks::initializers::renderPassCreateInfo();
|
||||||
|
renderPassCreateInfo.attachmentCount = 1;
|
||||||
|
renderPassCreateInfo.pAttachments = &attachmentDescription;
|
||||||
|
renderPassCreateInfo.subpassCount = 1;
|
||||||
|
renderPassCreateInfo.pSubpasses = &subpass;
|
||||||
|
renderPassCreateInfo.dependencyCount = static_cast<uint32_t>(dependencies.size());
|
||||||
|
renderPassCreateInfo.pDependencies = dependencies.data();
|
||||||
|
|
||||||
|
VK_CHECK_RESULT(vkCreateRenderPass(device, &renderPassCreateInfo, nullptr, &depthPass.renderPass));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Layered depth image and views
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Layered depth image
|
||||||
|
VkImageCreateInfo imageInfo = vks::initializers::imageCreateInfo();
|
||||||
|
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
imageInfo.extent.width = SHADOWMAP_DIM;
|
||||||
|
imageInfo.extent.height = SHADOWMAP_DIM;
|
||||||
|
imageInfo.extent.depth = 1;
|
||||||
|
imageInfo.mipLevels = 1;
|
||||||
|
imageInfo.arrayLayers = SHADOW_MAP_CASCADE_COUNT;
|
||||||
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
|
imageInfo.format = depthFormat;
|
||||||
|
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||||
|
VK_CHECK_RESULT(vkCreateImage(device, &imageInfo, nullptr, &depth.image));
|
||||||
|
VkMemoryAllocateInfo memAlloc = vks::initializers::memoryAllocateInfo();
|
||||||
|
VkMemoryRequirements memReqs;
|
||||||
|
vkGetImageMemoryRequirements(device, depth.image, &memReqs);
|
||||||
|
memAlloc.allocationSize = memReqs.size;
|
||||||
|
memAlloc.memoryTypeIndex = vulkanDevice->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||||
|
VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &depth.mem));
|
||||||
|
VK_CHECK_RESULT(vkBindImageMemory(device, depth.image, depth.mem, 0));
|
||||||
|
// Full depth map view (all layers)
|
||||||
|
VkImageViewCreateInfo viewInfo = vks::initializers::imageViewCreateInfo();
|
||||||
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||||
|
viewInfo.format = depthFormat;
|
||||||
|
viewInfo.subresourceRange = {};
|
||||||
|
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
||||||
|
viewInfo.subresourceRange.levelCount = 1;
|
||||||
|
viewInfo.subresourceRange.baseArrayLayer = 0;
|
||||||
|
viewInfo.subresourceRange.layerCount = SHADOW_MAP_CASCADE_COUNT;
|
||||||
|
viewInfo.image = depth.image;
|
||||||
|
VK_CHECK_RESULT(vkCreateImageView(device, &viewInfo, nullptr, &depth.view));
|
||||||
|
|
||||||
|
// One image and framebuffer per cascade
|
||||||
|
for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
|
||||||
|
// Image view for this cascade's layer (inside the depth map)
|
||||||
|
VkImageViewCreateInfo viewInfo = vks::initializers::imageViewCreateInfo();
|
||||||
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||||
|
viewInfo.format = depthFormat;
|
||||||
|
viewInfo.subresourceRange = {};
|
||||||
|
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
||||||
|
viewInfo.subresourceRange.levelCount = 1;
|
||||||
|
viewInfo.subresourceRange.baseArrayLayer = i;
|
||||||
|
viewInfo.subresourceRange.layerCount = 1;
|
||||||
|
viewInfo.image = depth.image;
|
||||||
|
VK_CHECK_RESULT(vkCreateImageView(device, &viewInfo, nullptr, &cascades[i].view));
|
||||||
|
// Framebuffer
|
||||||
|
VkFramebufferCreateInfo framebufferInfo = vks::initializers::framebufferCreateInfo();
|
||||||
|
framebufferInfo.renderPass = depthPass.renderPass;
|
||||||
|
framebufferInfo.attachmentCount = 1;
|
||||||
|
framebufferInfo.pAttachments = &cascades[i].view;
|
||||||
|
framebufferInfo.width = SHADOWMAP_DIM;
|
||||||
|
framebufferInfo.height = SHADOWMAP_DIM;
|
||||||
|
framebufferInfo.layers = 1;
|
||||||
|
VK_CHECK_RESULT(vkCreateFramebuffer(device, &framebufferInfo, nullptr, &cascades[i].frameBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared sampler for cascade deoth reads
|
||||||
|
VkSamplerCreateInfo sampler = vks::initializers::samplerCreateInfo();
|
||||||
|
sampler.magFilter = SHADOWMAP_FILTER;
|
||||||
|
sampler.minFilter = SHADOWMAP_FILTER;
|
||||||
|
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||||
|
sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
|
sampler.addressModeV = sampler.addressModeU;
|
||||||
|
sampler.addressModeW = sampler.addressModeU;
|
||||||
|
sampler.mipLodBias = 0.0f;
|
||||||
|
sampler.maxAnisotropy = 1.0f;
|
||||||
|
sampler.minLod = 0.0f;
|
||||||
|
sampler.maxLod = 1.0f;
|
||||||
|
sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||||
|
VK_CHECK_RESULT(vkCreateSampler(device, &sampler, nullptr, &depth.sampler));
|
||||||
|
}
|
||||||
|
|
||||||
|
void buildOffscreenCommandBuffer()
|
||||||
|
{
|
||||||
|
VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo();
|
||||||
|
|
||||||
|
VkClearValue clearValues[1];
|
||||||
|
clearValues[0].depthStencil = { 1.0f, 0 };
|
||||||
|
|
||||||
|
VkRenderPassBeginInfo renderPassBeginInfo = vks::initializers::renderPassBeginInfo();
|
||||||
|
renderPassBeginInfo.renderPass = depthPass.renderPass;
|
||||||
|
renderPassBeginInfo.renderArea.offset.x = 0;
|
||||||
|
renderPassBeginInfo.renderArea.offset.y = 0;
|
||||||
|
renderPassBeginInfo.renderArea.extent.width = SHADOWMAP_DIM;
|
||||||
|
renderPassBeginInfo.renderArea.extent.height = SHADOWMAP_DIM;
|
||||||
|
renderPassBeginInfo.clearValueCount = 2;
|
||||||
|
renderPassBeginInfo.pClearValues = clearValues;
|
||||||
|
|
||||||
|
VK_CHECK_RESULT(vkBeginCommandBuffer(depthPass.commandBuffer, &cmdBufInfo));
|
||||||
|
|
||||||
|
VkViewport viewport = vks::initializers::viewport((float)SHADOWMAP_DIM, (float)SHADOWMAP_DIM, 0.0f, 1.0f);
|
||||||
|
vkCmdSetViewport(depthPass.commandBuffer, 0, 1, &viewport);
|
||||||
|
|
||||||
|
VkRect2D scissor = vks::initializers::rect2D(SHADOWMAP_DIM, SHADOWMAP_DIM, 0, 0);
|
||||||
|
vkCmdSetScissor(depthPass.commandBuffer, 0, 1, &scissor);
|
||||||
|
|
||||||
|
// Multi-pass depht map cascade generation
|
||||||
|
// Could be simplified to one pass using geometry shaders and layered frame buffers
|
||||||
|
for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
|
||||||
|
renderPassBeginInfo.framebuffer = cascades[i].frameBuffer;
|
||||||
|
vkCmdBeginRenderPass(depthPass.commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||||
|
vkCmdBindPipeline(depthPass.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, depthPass.pipeline);
|
||||||
|
vkCmdPushConstants(depthPass.commandBuffer, depthPass.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(uint32_t), &i);
|
||||||
|
VkDeviceSize offsets[1] = { 0 };
|
||||||
|
vkCmdBindDescriptorSets(depthPass.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, depthPass.pipelineLayout, 0, 1, &cascades[i].descriptorSet, 0, NULL);
|
||||||
|
vkCmdBindVertexBuffers(depthPass.commandBuffer, 0, 1, &scenes[sceneIndex].vertices.buffer, offsets);
|
||||||
|
vkCmdBindIndexBuffer(depthPass.commandBuffer, scenes[sceneIndex].indices.buffer, 0, VK_INDEX_TYPE_UINT32);
|
||||||
|
vkCmdDrawIndexed(depthPass.commandBuffer, scenes[sceneIndex].indexCount, 1, 0, 0, 0);
|
||||||
|
vkCmdEndRenderPass(depthPass.commandBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
VK_CHECK_RESULT(vkEndCommandBuffer(depthPass.commandBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
void buildCommandBuffers()
|
||||||
|
{
|
||||||
|
VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo();
|
||||||
|
|
||||||
|
VkClearValue clearValues[2];
|
||||||
|
clearValues[0].color = defaultClearColor;
|
||||||
|
clearValues[0].color = { { 0.0f, 0.0f, 0.2f, 1.0f } };
|
||||||
|
clearValues[1].depthStencil = { 1.0f, 0 };
|
||||||
|
|
||||||
|
VkRenderPassBeginInfo renderPassBeginInfo = vks::initializers::renderPassBeginInfo();
|
||||||
|
renderPassBeginInfo.renderPass = renderPass;
|
||||||
|
renderPassBeginInfo.renderArea.offset.x = 0;
|
||||||
|
renderPassBeginInfo.renderArea.offset.y = 0;
|
||||||
|
renderPassBeginInfo.renderArea.extent.width = width;
|
||||||
|
renderPassBeginInfo.renderArea.extent.height = height;
|
||||||
|
renderPassBeginInfo.clearValueCount = 2;
|
||||||
|
renderPassBeginInfo.pClearValues = clearValues;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < drawCmdBuffers.size(); ++i) {
|
||||||
|
renderPassBeginInfo.framebuffer = frameBuffers[i];
|
||||||
|
|
||||||
|
VK_CHECK_RESULT(vkBeginCommandBuffer(drawCmdBuffers[i], &cmdBufInfo));
|
||||||
|
|
||||||
|
vkCmdBeginRenderPass(drawCmdBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||||
|
|
||||||
|
VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f);
|
||||||
|
vkCmdSetViewport(drawCmdBuffers[i], 0, 1, &viewport);
|
||||||
|
|
||||||
|
VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0);
|
||||||
|
vkCmdSetScissor(drawCmdBuffers[i], 0, 1, &scissor);
|
||||||
|
|
||||||
|
VkDeviceSize offsets[1] = { 0 };
|
||||||
|
|
||||||
|
// Visualize shadow map cascade
|
||||||
|
if (displayDepthMap) {
|
||||||
|
vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, NULL);
|
||||||
|
vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.debugShadowMap);
|
||||||
|
vkCmdPushConstants(drawCmdBuffers[i], pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(uint32_t), &displayDepthMapCascadeIndex);
|
||||||
|
vkCmdDraw(drawCmdBuffers[i], 3, 1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render shadowed scene
|
||||||
|
vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, NULL);
|
||||||
|
vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, (filterPCF) ? pipelines.sceneShadowPCF : pipelines.sceneShadow);
|
||||||
|
vkCmdBindVertexBuffers(drawCmdBuffers[i], 0, 1, &scenes[sceneIndex].vertices.buffer, offsets);
|
||||||
|
vkCmdBindIndexBuffer(drawCmdBuffers[i], scenes[sceneIndex].indices.buffer, 0, VK_INDEX_TYPE_UINT32);
|
||||||
|
vkCmdDrawIndexed(drawCmdBuffers[i], scenes[sceneIndex].indexCount, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
vkCmdEndRenderPass(drawCmdBuffers[i]);
|
||||||
|
|
||||||
|
VK_CHECK_RESULT(vkEndCommandBuffer(drawCmdBuffers[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadAssets()
|
||||||
|
{
|
||||||
|
scenes.resize(2);
|
||||||
|
scenes[0].loadFromFile(getAssetPath() + "models/terrain.obj", vertexLayout, 2.0f, vulkanDevice, queue);
|
||||||
|
scenes[1].loadFromFile(getAssetPath() + "models/shadowtest.obj", vertexLayout, 0.25f, vulkanDevice, queue);
|
||||||
|
sceneNames = {"Terrain test", "Object test" };
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupDescriptorPool()
|
||||||
|
{
|
||||||
|
std::vector<VkDescriptorPoolSize> poolSizes = {
|
||||||
|
vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 12),
|
||||||
|
vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 32)
|
||||||
|
};
|
||||||
|
VkDescriptorPoolCreateInfo descriptorPoolInfo =
|
||||||
|
vks::initializers::descriptorPoolCreateInfo(static_cast<uint32_t>(poolSizes.size()), poolSizes.data(), 3 + SHADOW_MAP_CASCADE_COUNT);
|
||||||
|
VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &descriptorPool));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupLayoutsAndDescriptors()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Layouts
|
||||||
|
*/
|
||||||
|
|
||||||
|
std::vector<VkDescriptorSetLayoutBinding> setLayoutBindings = {
|
||||||
|
vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0),
|
||||||
|
vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
|
||||||
|
vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT, 2),
|
||||||
|
vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 3),
|
||||||
|
};
|
||||||
|
|
||||||
|
VkDescriptorSetLayoutCreateInfo descriptorLayout =
|
||||||
|
vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings);
|
||||||
|
VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &descriptorSetLayout));
|
||||||
|
|
||||||
|
// Shared pipeline layout
|
||||||
|
{
|
||||||
|
// Pass cascade index as push constant
|
||||||
|
VkPushConstantRange pushConstantRange =
|
||||||
|
vks::initializers::pushConstantRange(VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(uint32_t), 0);
|
||||||
|
VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo =
|
||||||
|
vks::initializers::pipelineLayoutCreateInfo(&descriptorSetLayout, 1);
|
||||||
|
pipelineLayoutCreateInfo.pushConstantRangeCount = 1;
|
||||||
|
pipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange;
|
||||||
|
VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &pipelineLayout));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offscreen pipeline layout
|
||||||
|
{
|
||||||
|
// Pass cascade matrix as push constant
|
||||||
|
VkPushConstantRange pushConstantRange =
|
||||||
|
vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT, sizeof(glm::mat4), 0);
|
||||||
|
VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo =
|
||||||
|
vks::initializers::pipelineLayoutCreateInfo(&descriptorSetLayout, 1);
|
||||||
|
pipelineLayoutCreateInfo.pushConstantRangeCount = 1;
|
||||||
|
pipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange;
|
||||||
|
VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &depthPass.pipelineLayout));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dscriptor sets
|
||||||
|
*/
|
||||||
|
|
||||||
|
std::vector<VkWriteDescriptorSet> writeDescriptorSets;
|
||||||
|
|
||||||
|
VkDescriptorSetAllocateInfo allocInfo =
|
||||||
|
vks::initializers::descriptorSetAllocateInfo(descriptorPool, &descriptorSetLayout, 1);
|
||||||
|
|
||||||
|
// Scene rendering / debug display
|
||||||
|
VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet));
|
||||||
|
|
||||||
|
VkDescriptorImageInfo depthMapDescriptor =
|
||||||
|
vks::initializers::descriptorImageInfo(depth.sampler, depth.view, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL);
|
||||||
|
|
||||||
|
writeDescriptorSets = {
|
||||||
|
vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &uniformBuffers.VS.descriptor),
|
||||||
|
vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &depthMapDescriptor),
|
||||||
|
vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2, &uniformBuffers.FS.descriptor),
|
||||||
|
};
|
||||||
|
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, NULL);
|
||||||
|
|
||||||
|
// Per-cascade descriptor set
|
||||||
|
// Each descriptor set represents a single layer of the array texture
|
||||||
|
for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
|
||||||
|
VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &cascades[i].descriptorSet));
|
||||||
|
VkDescriptorImageInfo cascadeImageInfo = vks::initializers::descriptorImageInfo(depth.sampler, depth.view, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL);
|
||||||
|
writeDescriptorSets = {
|
||||||
|
vks::initializers::writeDescriptorSet(cascades[i].descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &depthPass.uniformBuffer.descriptor),
|
||||||
|
vks::initializers::writeDescriptorSet(cascades[i].descriptorSet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &cascadeImageInfo)
|
||||||
|
};
|
||||||
|
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void preparePipelines()
|
||||||
|
{
|
||||||
|
VkPipelineInputAssemblyStateCreateInfo inputAssemblyState =
|
||||||
|
vks::initializers::pipelineInputAssemblyStateCreateInfo(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
|
||||||
|
|
||||||
|
VkPipelineRasterizationStateCreateInfo rasterizationState =
|
||||||
|
vks::initializers::pipelineRasterizationStateCreateInfo(VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, 0);
|
||||||
|
|
||||||
|
VkPipelineColorBlendAttachmentState blendAttachmentState =
|
||||||
|
vks::initializers::pipelineColorBlendAttachmentState(0xf, VK_FALSE);
|
||||||
|
|
||||||
|
VkPipelineColorBlendStateCreateInfo colorBlendState =
|
||||||
|
vks::initializers::pipelineColorBlendStateCreateInfo(1, &blendAttachmentState);
|
||||||
|
|
||||||
|
VkPipelineDepthStencilStateCreateInfo depthStencilState =
|
||||||
|
vks::initializers::pipelineDepthStencilStateCreateInfo(VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS_OR_EQUAL);
|
||||||
|
|
||||||
|
VkPipelineViewportStateCreateInfo viewportState =
|
||||||
|
vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0);
|
||||||
|
|
||||||
|
VkPipelineMultisampleStateCreateInfo multisampleState =
|
||||||
|
vks::initializers::pipelineMultisampleStateCreateInfo(VK_SAMPLE_COUNT_1_BIT, 0);
|
||||||
|
|
||||||
|
std::vector<VkDynamicState> dynamicStateEnables = {
|
||||||
|
VK_DYNAMIC_STATE_VIEWPORT,
|
||||||
|
VK_DYNAMIC_STATE_SCISSOR
|
||||||
|
};
|
||||||
|
VkPipelineDynamicStateCreateInfo dynamicState =
|
||||||
|
vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables);
|
||||||
|
|
||||||
|
std::array<VkPipelineShaderStageCreateInfo, 2> shaderStages;
|
||||||
|
|
||||||
|
VkGraphicsPipelineCreateInfo pipelineCreateInfo =
|
||||||
|
vks::initializers::pipelineCreateInfo(pipelineLayout, renderPass, 0);
|
||||||
|
|
||||||
|
pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState;
|
||||||
|
pipelineCreateInfo.pRasterizationState = &rasterizationState;
|
||||||
|
pipelineCreateInfo.pColorBlendState = &colorBlendState;
|
||||||
|
pipelineCreateInfo.pMultisampleState = &multisampleState;
|
||||||
|
pipelineCreateInfo.pViewportState = &viewportState;
|
||||||
|
pipelineCreateInfo.pDepthStencilState = &depthStencilState;
|
||||||
|
pipelineCreateInfo.pDynamicState = &dynamicState;
|
||||||
|
pipelineCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size());
|
||||||
|
pipelineCreateInfo.pStages = shaderStages.data();
|
||||||
|
|
||||||
|
// Shadow map cascade debug quad display
|
||||||
|
rasterizationState.cullMode = VK_CULL_MODE_BACK_BIT;
|
||||||
|
shaderStages[0] = loadShader(getAssetPath() + "shaders/shadowmappingcascade/debugshadowmap.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
|
||||||
|
shaderStages[1] = loadShader(getAssetPath() + "shaders/shadowmappingcascade/debugshadowmap.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||||
|
// Empty vertex input state
|
||||||
|
VkPipelineVertexInputStateCreateInfo emptyInputState = vks::initializers::pipelineVertexInputStateCreateInfo();
|
||||||
|
pipelineCreateInfo.pVertexInputState = &emptyInputState;
|
||||||
|
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipelines.debugShadowMap));
|
||||||
|
|
||||||
|
// Vertex bindings and attributes
|
||||||
|
std::vector<VkVertexInputBindingDescription> vertexInputBindings = {
|
||||||
|
vks::initializers::vertexInputBindingDescription(0, vertexLayout.stride(), VK_VERTEX_INPUT_RATE_VERTEX)
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<VkVertexInputAttributeDescription> vertexInputAttributes = {
|
||||||
|
vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Location 0: Position
|
||||||
|
vks::initializers::vertexInputAttributeDescription(0, 1, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 3), // Location 1: UV
|
||||||
|
vks::initializers::vertexInputAttributeDescription(0, 2, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 5), // Location 2: Color
|
||||||
|
vks::initializers::vertexInputAttributeDescription(0, 3, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 8) // Location 3: Normal
|
||||||
|
};
|
||||||
|
|
||||||
|
VkPipelineVertexInputStateCreateInfo vertexInputState = vks::initializers::pipelineVertexInputStateCreateInfo();
|
||||||
|
vertexInputState.vertexBindingDescriptionCount = static_cast<uint32_t>(vertexInputBindings.size());
|
||||||
|
vertexInputState.pVertexBindingDescriptions = vertexInputBindings.data();
|
||||||
|
vertexInputState.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexInputAttributes.size());
|
||||||
|
vertexInputState.pVertexAttributeDescriptions = vertexInputAttributes.data();
|
||||||
|
|
||||||
|
pipelineCreateInfo.pVertexInputState = &vertexInputState;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shadow mapped scene rendering
|
||||||
|
*/
|
||||||
|
rasterizationState.cullMode = VK_CULL_MODE_BACK_BIT;
|
||||||
|
shaderStages[0] = loadShader(getAssetPath() + "shaders/shadowmappingcascade/scene.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
|
||||||
|
shaderStages[1] = loadShader(getAssetPath() + "shaders/shadowmappingcascade/scene.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||||
|
// Use specialization constants to select between horizontal and vertical blur
|
||||||
|
uint32_t enablePCF = 0;
|
||||||
|
VkSpecializationMapEntry specializationMapEntry = vks::initializers::specializationMapEntry(0, 0, sizeof(uint32_t));
|
||||||
|
VkSpecializationInfo specializationInfo = vks::initializers::specializationInfo(1, &specializationMapEntry, sizeof(uint32_t), &enablePCF);
|
||||||
|
shaderStages[1].pSpecializationInfo = &specializationInfo;
|
||||||
|
// No filtering
|
||||||
|
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipelines.sceneShadow));
|
||||||
|
// PCF filtering
|
||||||
|
enablePCF = 1;
|
||||||
|
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipelines.sceneShadowPCF));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Depth map generation
|
||||||
|
*/
|
||||||
|
shaderStages[0] = loadShader(getAssetPath() + "shaders/shadowmappingcascade/depthpass.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
|
||||||
|
shaderStages[1] = loadShader(getAssetPath() + "shaders/shadowmappingcascade/depthpass.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||||
|
// No blend attachment states (no color attachments used)
|
||||||
|
colorBlendState.attachmentCount = 0;
|
||||||
|
depthStencilState.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||||
|
// Enable depth clamp (if available)
|
||||||
|
rasterizationState.depthClampEnable = deviceFeatures.depthClamp;
|
||||||
|
pipelineCreateInfo.layout = depthPass.pipelineLayout;
|
||||||
|
pipelineCreateInfo.renderPass = depthPass.renderPass;
|
||||||
|
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCreateInfo, nullptr, &depthPass.pipeline));
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepareUniformBuffers()
|
||||||
|
{
|
||||||
|
// Shadow map generation buffer blocks
|
||||||
|
VK_CHECK_RESULT(vulkanDevice->createBuffer(
|
||||||
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||||
|
&depthPass.uniformBuffer,
|
||||||
|
sizeof(depthPass.ubo)));
|
||||||
|
|
||||||
|
// Scene uniform buffer blocks
|
||||||
|
VK_CHECK_RESULT(vulkanDevice->createBuffer(
|
||||||
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||||
|
&uniformBuffers.VS,
|
||||||
|
sizeof(uboVS)));
|
||||||
|
VK_CHECK_RESULT(vulkanDevice->createBuffer(
|
||||||
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||||
|
&uniformBuffers.FS,
|
||||||
|
sizeof(uboFS)));
|
||||||
|
|
||||||
|
// Map persistent
|
||||||
|
VK_CHECK_RESULT(depthPass.uniformBuffer.map());
|
||||||
|
VK_CHECK_RESULT(uniformBuffers.VS.map());
|
||||||
|
VK_CHECK_RESULT(uniformBuffers.FS.map());
|
||||||
|
|
||||||
|
updateLight();
|
||||||
|
updateUniformBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate frustum split depths and matrices for the shadow map cascades
|
||||||
|
void updateCascades()
|
||||||
|
{
|
||||||
|
float cascadeSplits[SHADOW_MAP_CASCADE_COUNT];
|
||||||
|
|
||||||
|
float nearClip = camera.getNearClip();
|
||||||
|
float farClip = camera.getFarClip();
|
||||||
|
float clipRange = farClip - nearClip;
|
||||||
|
|
||||||
|
float minZ = nearClip;
|
||||||
|
float maxZ = nearClip + clipRange;
|
||||||
|
|
||||||
|
float range = maxZ - minZ;
|
||||||
|
float ratio = maxZ / minZ;
|
||||||
|
|
||||||
|
// Calculate split depths based on view camera furstum
|
||||||
|
// Based on method presentd in https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html
|
||||||
|
for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
|
||||||
|
float p = (i + 1) / static_cast<float>(SHADOW_MAP_CASCADE_COUNT);
|
||||||
|
float log = minZ * std::pow(ratio, p);
|
||||||
|
float uniform = minZ + range * p;
|
||||||
|
float d = cascadeSplitLambda * (log - uniform) + uniform;
|
||||||
|
cascadeSplits[i] = (d - nearClip) / clipRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate orthographic projection matrix for each cascade
|
||||||
|
float lastSplitDist = 0.0;
|
||||||
|
for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; ++i) {
|
||||||
|
float splitDist = cascadeSplits[i];
|
||||||
|
|
||||||
|
glm::vec3 frustumCorners[8] = {
|
||||||
|
glm::vec3(-1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3( 1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3( 1.0f, -1.0f, -1.0f),
|
||||||
|
glm::vec3(-1.0f, -1.0f, -1.0f),
|
||||||
|
glm::vec3(-1.0f, 1.0f, 1.0f),
|
||||||
|
glm::vec3( 1.0f, 1.0f, 1.0f),
|
||||||
|
glm::vec3( 1.0f, -1.0f, 1.0f),
|
||||||
|
glm::vec3(-1.0f, -1.0f, 1.0f),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Project frustum corners into world space
|
||||||
|
glm::mat4 invCam = glm::inverse(camera.matrices.perspective * camera.matrices.view);
|
||||||
|
for (uint32_t i = 0; i < 8; ++i) {
|
||||||
|
glm::vec4 invCorner = invCam * glm::vec4(frustumCorners[i], 1.0f);
|
||||||
|
frustumCorners[i] = invCorner / invCorner.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < 4; ++i) {
|
||||||
|
glm::vec3 dist = frustumCorners[i + 4] - frustumCorners[i];
|
||||||
|
frustumCorners[i + 4] = frustumCorners[i] + (dist * splitDist);
|
||||||
|
frustumCorners[i] = frustumCorners[i] + (dist * lastSplitDist);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get frustum center
|
||||||
|
glm::vec3 frustumCenter = glm::vec3(0.0f);
|
||||||
|
for (uint32_t i = 0; i < 8; i++) {
|
||||||
|
frustumCenter += frustumCorners[i];
|
||||||
|
}
|
||||||
|
frustumCenter /= 8.0f;
|
||||||
|
|
||||||
|
float radius = 0.0f;
|
||||||
|
for (uint32_t i = 0; i < 8; ++i) {
|
||||||
|
float distance = glm::length(frustumCorners[i] - frustumCenter);
|
||||||
|
radius = glm::max(radius, distance);
|
||||||
|
}
|
||||||
|
radius = std::ceil(radius * 16.0f) / 16.0f;
|
||||||
|
|
||||||
|
glm::vec3 maxExtents = glm::vec3(radius, radius, radius);
|
||||||
|
glm::vec3 minExtents = -maxExtents;
|
||||||
|
|
||||||
|
glm::vec3 lightDir = normalize(-lightPos);
|
||||||
|
glm::mat4 lightViewMatrix = glm::lookAt(frustumCenter - lightDir * -minExtents.z, frustumCenter, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||||
|
|
||||||
|
glm::vec3 cascadeExtents = maxExtents - minExtents;
|
||||||
|
|
||||||
|
glm::mat4 lightOrthoMatrix = glm::ortho(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, 0.0f, cascadeExtents.z);
|
||||||
|
|
||||||
|
// Store split distance and matrix in cascade
|
||||||
|
const float clipDist = camera.getFarClip() - camera.getNearClip();
|
||||||
|
cascades[i].splitDepth = (camera.getNearClip() + splitDist * clipDist) * -1.0f;
|
||||||
|
cascades[i].viewProjMatrix = lightOrthoMatrix * lightViewMatrix;
|
||||||
|
|
||||||
|
lastSplitDist = cascadeSplits[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLight()
|
||||||
|
{
|
||||||
|
lightPos.x = cos(glm::radians(timer * 360.0f)) * 50.0f;
|
||||||
|
lightPos.y = -20.0f;
|
||||||
|
lightPos.z = sin(glm::radians(timer * 360.0f)) * 50.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateUniformBuffers()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Depth rendering
|
||||||
|
*/
|
||||||
|
for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
|
||||||
|
depthPass.ubo.cascadeViewProjMat[i] = cascades[i].viewProjMatrix;
|
||||||
|
}
|
||||||
|
memcpy(depthPass.uniformBuffer.mapped, &depthPass.ubo, sizeof(depthPass.ubo));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Scene rendering
|
||||||
|
*/
|
||||||
|
uboVS.projection = glm::perspective(glm::radians(45.0f), (float)width / (float)height, zNear, zFar);
|
||||||
|
|
||||||
|
uboVS.view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, zoom));
|
||||||
|
uboVS.view = glm::rotate(uboVS.view, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
||||||
|
uboVS.view = glm::rotate(uboVS.view, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||||
|
uboVS.view = glm::rotate(uboVS.view, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
||||||
|
|
||||||
|
uboVS.model = glm::mat4(1.0f);
|
||||||
|
|
||||||
|
uboVS.projection = camera.matrices.perspective;
|
||||||
|
uboVS.view = camera.matrices.view;
|
||||||
|
uboVS.model = glm::mat4(1.0f);
|
||||||
|
|
||||||
|
uboVS.lightDir = normalize(-lightPos);
|
||||||
|
|
||||||
|
memcpy(uniformBuffers.VS.mapped, &uboVS, sizeof(uboVS));
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
|
||||||
|
uboFS.cascadeSplits[i] = cascades[i].splitDepth;
|
||||||
|
uboFS.cascadeViewProjMat[i] = cascades[i].viewProjMatrix;
|
||||||
|
}
|
||||||
|
uboFS.inverseViewMat = glm::inverse(camera.matrices.view);
|
||||||
|
uboFS.lightDir = normalize(-lightPos);
|
||||||
|
uboFS.colorCascades = colorCascades;
|
||||||
|
memcpy(uniformBuffers.FS.mapped, &uboFS, sizeof(uboFS));
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw()
|
||||||
|
{
|
||||||
|
VulkanExampleBase::prepareFrame();
|
||||||
|
|
||||||
|
// Depth map generation
|
||||||
|
submitInfo.pWaitSemaphores = &semaphores.presentComplete;
|
||||||
|
submitInfo.pSignalSemaphores = &depthPass.semaphore;
|
||||||
|
|
||||||
|
submitInfo.commandBufferCount = 1;
|
||||||
|
submitInfo.pCommandBuffers = &depthPass.commandBuffer;
|
||||||
|
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE));
|
||||||
|
|
||||||
|
// Scene rendering
|
||||||
|
submitInfo.pWaitSemaphores = &depthPass.semaphore;;
|
||||||
|
submitInfo.pSignalSemaphores = &semaphores.renderComplete;
|
||||||
|
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
||||||
|
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE));
|
||||||
|
|
||||||
|
VulkanExampleBase::submitFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepare()
|
||||||
|
{
|
||||||
|
VulkanExampleBase::prepare();
|
||||||
|
loadAssets();
|
||||||
|
prepareShadowMaps();
|
||||||
|
prepareUniformBuffers();
|
||||||
|
setupDescriptorPool();
|
||||||
|
setupLayoutsAndDescriptors();
|
||||||
|
preparePipelines();
|
||||||
|
buildCommandBuffers();
|
||||||
|
buildOffscreenCommandBuffer();
|
||||||
|
updateCascades();
|
||||||
|
prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void render()
|
||||||
|
{
|
||||||
|
if (!prepared)
|
||||||
|
return;
|
||||||
|
draw();
|
||||||
|
if (!paused) {
|
||||||
|
updateLight();
|
||||||
|
updateCascades();
|
||||||
|
updateUniformBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void viewChanged()
|
||||||
|
{
|
||||||
|
updateCascades();
|
||||||
|
updateUniformBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void OnUpdateUIOverlay(vks::UIOverlay *overlay)
|
||||||
|
{
|
||||||
|
if (overlay->header("Settings")) {
|
||||||
|
if (overlay->comboBox("Scenes", &sceneIndex, sceneNames)) {
|
||||||
|
buildCommandBuffers();
|
||||||
|
buildOffscreenCommandBuffer();
|
||||||
|
}
|
||||||
|
if (overlay->sliderFloat("Split lambda", &cascadeSplitLambda, 0.1f, 1.0f)) {
|
||||||
|
updateCascades();
|
||||||
|
updateUniformBuffers();
|
||||||
|
}
|
||||||
|
if (overlay->checkBox("Color cascades", &colorCascades)) {
|
||||||
|
updateUniformBuffers();
|
||||||
|
}
|
||||||
|
if (overlay->checkBox("Display depth map", &displayDepthMap)) {
|
||||||
|
buildCommandBuffers();
|
||||||
|
}
|
||||||
|
if (displayDepthMap) {
|
||||||
|
if (overlay->sliderInt("Cascade", &displayDepthMapCascadeIndex, 0, SHADOW_MAP_CASCADE_COUNT - 1)) {
|
||||||
|
buildCommandBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (overlay->checkBox("PCF filtering", &filterPCF)) {
|
||||||
|
buildCommandBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
VULKAN_EXAMPLE_MAIN()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue