【游戏引擎开发日志4】ShadowMap生成阴影

概述

首先,来看一下实现效果:
在这里插入图片描述
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);

}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值