概述
首先,来看一下实现效果:
ShadowMap是一种游戏常用的阴影技术,效果好,且实现起来比较简单,具有较好的性能。
它的思路如下:首先,把灯光当成摄像机,渲染一张灯光视角下物体的深度图,然后在摄像机渲染的阶段对这张深度图进行采样,比较当前片元和深度图里存储的对应位置片元的深度差异,如果前者深度更小,就说明它的位置没有阴影遮蔽。
那么,对于一个多光源的场景来说,选择哪一盏灯光进行阴影生成呢?我个人认为应该对于每一盏灯都设置一个是否生成阴影的属性,所有需要生成阴影的灯光都独立地生成深度图。不过,在我的小引擎中为了简化,只对一盏灯光生成了阴影。
下一个问题是:如果把灯光作为相机,那么应该选择透视相机还是正交相机?如何设置相机的各个参数?
透视投影更常用在点光源和聚光灯上,正交投影则在定向光上使用较多。
在本项目中,我采用了透视投影。
对于透视投影中参数的设置,则并不是固定的。而且,还可以对距离摄像机远近不同的区域设置不同的光源相机参数,实现更好的效果,这是级联阴影技术(Cascaded Shadow Maps)。
在本项目中,我根据调参结果选择了几个固定的常数作为光源相机参数。
流程
上图是阴影的渲染流程(忽略了最后一个把图像渲染到Imgui Image的Render Pass)。
需要重点注意ShadowPass以及它对应的RenderTarget和管线的创建配置,不能和普通的Render Pass保持完全一致。
Shadow Map对应Render Target创建
Render Target只包含一个深度图,该深度图的Image Sampler创建参数如下:
depthMapCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
depthMapCreateInfo.maxAnisotropy = 1.0f;
depthMapCreateInfo.magFilter = VK_FILTER_NEAREST;
depthMapCreateInfo.minFilter = VK_FILTER_NEAREST;
depthMapCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
depthMapCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
depthMapCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
depthMapCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
depthMapCreateInfo.mipLodBias = 0.0f;
depthMapCreateInfo.maxAnisotropy = 1.0f;
depthMapCreateInfo.minLod = 0.0f;
depthMapCreateInfo.maxLod = 1.0f;
depthMapCreateInfo.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
需要重点关注addressModeU和addressModeV参数,不要把它们设置为Repeat的模式。
创建Image代码如下:
std::shared_ptr<VulkanImage>depthImage = std::make_shared<VulkanImage>(
extent.width,
extent.height,
depthFormat,
VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
0
);
这里注意,需要指定VK_IMAGE_USAGE枚举值,不要出错。
创建ImageView代码如下:
depthAttachments[i] = std::make_shared<VulkanImageView>(depthImage,
depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT);
depthAttachments[i]->SetDescriptorImageInfo(VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL);
//descriptor info
void VulkanImageView::setDescriptorImageInfo(VkImageLayout layout)
{
imageInfo.reset();
imageInfo = std::make_shared<VkDescriptorImageInfo>();
imageInfo->imageLayout = layout;
imageInfo->imageView = imageView;
imageInfo->sampler = sampler->GetSampler();
}
创建Image View的代码和普通的图像差异不大,只是要注意,因为这张深度图将要用于采样,所以需要为它设置DescriptorImageInfo,注意ImageLayout的拼写不要拼错了,Vulkan中有一个枚举值和VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL非常相似。
Shadow Pass创建
接下来,需要创建Shadow Pass。
Shadow Pass创建首先需要指定Attachment类型,注意finalLayout一定要和深度图Descriptor Info里的layout保持一致;其次,要添加一个SubPass,并添加一个外部进入该SubPass的Dependency和从SubPass离开的Dependency。
代码如下:
//shaodow pass
shadowRenderPass = std::make_shared<VulkanRenderPass>();
uint32_t depthIndex1, subPassIndex1;
shadowRenderPass->AddAttachment(depthIndex1,
VulkanDevice::Get()->findSupportedFormat(
{ VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT },
VK_IMAGE_TILING_OPTIMAL,
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT),
VK_SAMPLE_COUNT_1_BIT,
VK_ATTACHMENT_LOAD_OP_CLEAR,
VK_ATTACHMENT_STORE_OP_STORE,
VK_ATTACHMENT_LOAD_OP_DONT_CARE,
VK_ATTACHMENT_STORE_OP_DONT_CARE,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
);
shadowRenderPass->AddSubPass(
subPassIndex1,
VK_PIPELINE_BIND_POINT_GRAPHICS,
{ },
depthIndex1
);
shadowRenderPass->AddDependency(
VK_SUBPASS_EXTERNAL,
subPassIndex1,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
VK_ACCESS_SHADER_READ_BIT,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
VK_DEPENDENCY_BY_REGION_BIT
);
shadowRenderPass->AddDependency(
subPassIndex1,
VK_SUBPASS_EXTERNAL,
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT,
VK_DEPENDENCY_BY_REGION_BIT
);
shadowRenderPass->Create();
shadowRenderPassController = std::make_shared<VulkanRenderPassController>(shadowRenderPass);
shadowRenderTarget = std::make_shared<VulkanRenderTarget>(oldExtent, shadowRenderPass,
RenderTargetUsage::ONLY_DEPTH);
Shadow Pipeline创建
最后,要创建管线。创建管线的配置代码比较复杂,我是参考了Vulkan官方提供的样例创建的。
除此之外,还要注意,这个管线只有顶点着色器,没有片元着色器。
渲染管线比较重要的配置如下:
- 不要开启Color Blend,因为没有Color Attachment,该操作无意义
- 需要开启深度测试和深度写入
- dynamicState添加VK_DYNAMIC_STATE_DEPTH_BIAS
//pipeline
PipelineConfigInfo configInfo{};
configInfo.inputAssemblyInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
configInfo.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
configInfo.inputAssemblyInfo.flags = 0;
configInfo.inputAssemblyInfo.primitiveRestartEnable = VK_FALSE;
configInfo.viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
configInfo.viewportInfo.viewportCount = 1;
configInfo.viewportInfo.scissorCount = 1;
configInfo.viewportInfo.flags = 0;
configInfo.rasterizationInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
configInfo.rasterizationInfo.polygonMode = VK_POLYGON_MODE_FILL;
configInfo.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
configInfo.rasterizationInfo.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
configInfo.rasterizationInfo.flags = 0;
configInfo.rasterizationInfo.depthClampEnable = VK_FALSE;
configInfo.rasterizationInfo.lineWidth = 1.0f;
configInfo.rasterizationInfo.depthBiasEnable = VK_TRUE;
configInfo.multisampleInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
configInfo.multisampleInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
configInfo.multisampleInfo.flags = 0;
// Enable blending
configInfo.colorBlendAttachment.blendEnable = VK_FALSE;
configInfo.colorBlendAttachment.colorWriteMask = 0xf;//difference
configInfo.colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
configInfo.colorBlendInfo.attachmentCount = 0;
configInfo.depthStencilInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
configInfo.depthStencilInfo.depthTestEnable = VK_TRUE;
configInfo.depthStencilInfo.depthWriteEnable = VK_TRUE;
configInfo.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
configInfo.depthStencilInfo.back.compareOp = VK_COMPARE_OP_ALWAYS;
configInfo.dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT,VK_DYNAMIC_STATE_SCISSOR,VK_DYNAMIC_STATE_DEPTH_BIAS };
configInfo.dynamicStateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
configInfo.dynamicStateInfo.pDynamicStates = configInfo.dynamicStateEnables.data();
configInfo.dynamicStateInfo.dynamicStateCount = static_cast<uint32_t>(configInfo.dynamicStateEnables.size());
configInfo.dynamicStateInfo.flags = 0;
std::cout << " create config" << std::endl;
std::vector<VkDescriptorSetLayout>layouts = { shadowDataLayout->GetDescriptorSetLayout() };
shadowPipeline = std::make_shared<VulkanPipeline>(shadowRenderPass->GetRenderPass(),
layouts,
std::make_shared <VulkanShader>("assets/shaders/genShadow.vert",
ShaderState::ONLY_VERT),
sizeof(ScenePushConstant),
VulkanModelLayout::getBindingDescriptions(),
VulkanModelLayout::getAttributeDescriptions(),
&configInfo);
std::cout << " create pipeline" << std::endl;
着色器
阴影贴图生成着色器
阴影贴图生成只需要一个顶点着色器。代码非常简单,就是一个MVP矩阵变换,把物体坐标变换到灯光坐标空间。
#version 450
layout(location=0) in vec3 position;
layout(location=1) in vec3 color;
layout(location=2) in vec3 normal;
layout(location=3) in vec2 uv;
layout (set=0,binding = 0) uniform UBO
{
mat4 depthVP;
} ubo;
layout(push_constant)uniform Push{
mat4 modelMatrix;
mat4 normalMatrix;
} push;
void main()
{
vec4 ans=ubo.depthVP * push.modelMatrix*vec4(position, 1.0);
gl_Position = ans;
}
主渲染着色器
主渲染着色器在本引擎中,设定为每种材质都可以由用户自定义。在本次尝试中,我使用了简单的布林冯模型着色器。
顶点着色器中,我把顶点在灯光空间下的坐标记录在了fragShadowCoord中,等待片元着色器中拿到插值后的该坐标,用于贴图采样。
顶点着色器代码如下:
#version 450
layout(location=0) in vec3 position;
layout(location=1) in vec3 color;
layout(location=2) in vec3 normal;
layout(location=3) in vec2 uv;
layout(location=0)out vec2 fragUv;
layout(location=1)out vec3 fragPos;
layout(location=2)out vec3 fragNormal;
layout(location=3)out vec4 fragShadowCoord;
layout(set=0,binding=0)uniform Camera
{
vec4 cameraPos;
mat4 projectionViewMatrix;
}camera;
layout(set=0,binding=1)uniform Light
{
vec4 ambientColor;
vec4 pointPosition0;
vec4 pointColor0;
mat4 shadowVP;
}light;
layout(push_constant)uniform Push{
mat4 modelMatrix;
mat4 normalMatrix;
} push;
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 );
void main()
{
//input
mat3 normalMatrix3x3=mat3(push.normalMatrix[0][0],push.normalMatrix[0][1],push.normalMatrix[0][2],
push.normalMatrix[1][0],push.normalMatrix[1][1],push.normalMatrix[1][2],
push.normalMatrix[2][0],push.normalMatrix[2][1],push.normalMatrix[2][2]
);
//uv,world pos,world normal
fragUv=uv;
vec4 fragPosVec4=push.modelMatrix*vec4(position,1.0);
fragPos=vec3(fragPosVec4.x,fragPosVec4.y,fragPosVec4.z);
fragNormal=normalize(normalMatrix3x3*normal);
//mvp
vec4 pos=camera.projectionViewMatrix*push.modelMatrix*vec4(position,1.0);
fragShadowCoord=(biasMat*light.shadowVP*push.modelMatrix)*vec4(position,1.0);
//position
gl_Position=pos;
}
顶点着色器中传入的fragShadowCoord坐标并未进行透视除法,所以我们需要在片元着色器中手动进行。
在最开始渲染的时候,我观察到了摩尔纹现象,因此我引入了Shadow Bias值。除此之外,我还用了一个PCF,让阴影的边缘变得更加模糊圆滑。
片元着色器代码如下:
#version 450
layout(location=0)in vec2 fragUv;
layout(location=1)in vec3 fragPos;
layout(location=2)in vec3 fragNormal;
layout(location=3)in vec4 fragShadowCoord;
layout(location=0)out vec4 outColor;
layout(set=0,binding=0)uniform Camera
{
vec4 cameraPos;
mat4 projectionViewMatrix;
}camera;
layout(set=0,binding=1)uniform Light
{
vec4 ambientColor;
vec4 pointPosition0;
vec4 pointColor0;
mat4 shadowVP;
}light;
layout(set=0,binding=2)uniform sampler2D shadowMap;
layout(set=1,binding=0)uniform Mat_Color
{
vec4 Color;
}mat_Color;
layout(set=1,binding=1)uniform sampler2D Mat_Texture;
layout(push_constant)uniform Push{
mat4 modelMatrix;
mat4 normalMatrix;
} push;
#define ambient 0.1
float textureProj(vec4 shadowCoord, vec2 off)
{
float bias = 0.01;
float shadow = 1.0;
if ( shadowCoord.z > -1.0 && shadowCoord.z < 1.0 )
{
float dist = texture( shadowMap, shadowCoord.st + off ).r;
if ( shadowCoord.w > 0.0 && dist < shadowCoord.z-bias )
{
shadow = ambient;
}
}
return shadow;
}
float filterPCF(vec4 sc)
{
ivec2 texDim = textureSize(shadowMap, 0);
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 = 2;
for (int x = -range; x <= range; x++)
{
for (int y = -range; y <= range; y++)
{
shadowFactor += textureProj(sc, vec2(dx*x, dy*y));
count++;
}
}
return shadowFactor / count;
}
void main()
{
//material
vec3 albedo=vec3(mat_Color.Color);
//light
vec3 pointLightColor0=vec3(light.pointColor0.x,light.pointColor0.y,light.pointColor0.z);
vec3 pointLightPos0=vec3(light.pointPosition0.x,light.pointPosition0.y,light.pointPosition0.z);
float pointLightIntensity0=light.pointColor0.w;
float distance0=length(pointLightPos0-fragPos);
float pointLightRealIntensity0=pointLightIntensity0/(distance0*distance0);
vec3 pointLightDir0=normalize(pointLightPos0-fragPos);
vec3 ambientColor=vec3(light.ambientColor);
//model
vec3 inDir=normalize(pointLightDir0);
vec3 cameraPos=vec3(camera.cameraPos);
vec3 outDir=normalize(cameraPos-fragPos);
vec3 halfDir=normalize(inDir+outDir);
//shadow
//float shadow = textureProj(fragShadowCoord / fragShadowCoord.w, vec2(0.0));
float shadow = filterPCF(fragShadowCoord / fragShadowCoord.w);
//diffuse
float diff=max(dot(fragNormal,inDir),0);
vec3 diffuse=pointLightColor0*pointLightRealIntensity0*albedo*diff;
//specular
float spec=pow(max(dot(fragNormal,halfDir),0),32);
vec3 specular=pointLightColor0*pointLightRealIntensity0*spec;
//color
vec3 color=vec3(0.0);
color=diffuse+vec3(light.ambientColor)+specular;
color=color*shadow;
outColor=vec4(color,1.0);
}