前言
目前我们以所学的渲染方式还不能表现出阴影,因此本篇将介绍阴影贴图原理、存在的问题、如何解决这些问题、实现例子
阴影贴图原理
-
思想:光源看不到而摄像机可以看到的地方
-
阴影贴图的主要步骤:
- 第一次渲染(PASS):将光源看作一个摄像机进行渲染,将
光源空间的深度值
写入称为阴影图
的深度缓冲区,将该深度值记为\(z_{map}\)。被遮挡的像素在深度测试中会被丢弃。为了以光源的视角渲染场景,需要定义一个将坐标从世界空间变换到描述光源照射范围的投影矩阵
- 第二次渲染:以摄像机视角进行渲染,将
屏幕空间的深度信息
写入z-buffer
,将该深度值记为z
- 将z和z\(map\)进行比较:若\(z < z_{map}\),则当前片元处于阴影中
- 第一次渲染(PASS):将光源看作一个摄像机进行渲染,将
-
效果图:
软阴影和硬阴影
shadow map只能实现硬阴影效果,且只用使用点光源。下面展示了两种阴影的区别,上图为硬阴影,下图为软阴影
硬阴影很好理解,但软阴影是如何形成的呢?因为光源是有体积的,这会导致有的地方完全看不到光源(称为本影, Umbra), 但有的地方能看到一部分光源(称为半影,Penumbra),所以阴影的边缘会产生过渡
,从而产生软阴影。比如上图中的全日食与半日食
存在的问题
shadow map有三个问题
-
自遮挡(shadow acne,阴影失真)。原因有两个
- cpu的数值精度的限制。两个相差不大的浮点数难以判断是否相等
- shadow map保存的是一块范围内的图元的深度值,在最后比较z值时,shadow map中的z值可能略小于物体表面的z值,部分偏远会被误认为是阴影,尤其当光源和平面趋于平行时十分明显。因为
阴影图的分辨率有限
,所以以至于每个阴影图的纹素其实表示的是场景中的一片区域。比如下面第二张图,从相机视角E看向两个点\(\large p_1\)和\(\large p_2\),他们分别位于两个屏幕像素\(d(p_1)\)、\(d(p_2)\);二从光源视角L看去,这两个点在同一阴影图纹素\(s\)(一个坡面表示阴影图的一个纹素范围),这会导致在最后比较z值时,会得到\(d(p_1) > s \space \& \space d(p_2) \leqslant s\),最终\(p_1\)被视为在阴影内,\(p_2\)在阴影外
解决之道:在阴影图中引入一个偏移值(bias),使得在比较z值大小时阴影图的z值等于屏幕空间的z值,矫正这个错误。但这同时引出了一个新的问题:peter panning(阴影悬浮)
-
阴影悬浮
定义:阴影悬浮的大致意思是丢失部分可能发生遮挡的阴影
-
产生的原因:没有一个固定的bias值可以适用于所有几何体的阴影绘制,尤其是那些从光源看去有着极大斜率的三角形,需要更大的bias值
-
解决之道:斜率缩放偏移( slope-scaled-bias)。很明显我们需要一个公式针对不同情况求出适当的bias值,从上图可以看出bias值的大小和斜率是有一定关系的,因此解决这一问题的方法是以光源视角求出多边形斜面的斜率,并为斜率较大的多边形应用更大的bias值
-
若
阴影图的深度缓冲区格式为UNORM或没有绑定深度缓冲区
关键的几个变量如下:
typedef struct D3D12_RASTERIZER_DESC { //... INT DepthBias; //用于运算的offset值,针对于格式为UNORM的深度缓冲区 FLOAT DepthBiasClamp; //允许的最大bias值 FLOAT SlopeScaledDepthBias; //根据多边形的斜率控制偏移程度的缩放因子 //... }
计算过程如下:
// r与深度缓冲区的格式的位数相关,对于一个24位格式的深度缓冲区来说, r = 1 / 2^24. 还需将r转换为float32类型用于求得大于0的最小可表示值 // MaxDepthSlope表示当前像素深度值处水平斜率和垂直斜率的最大值 Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope;
-
若阴影图的深度缓冲区格式为float类型
计算过程如下:
// r表示浮点数的尾数位的位数.例如float32,1位表示符号,8位指数,23位表示尾数,因此float32的r等于23 // exponent(max z in primitive)表示e ^ z Bias = (float)DepthBias * 2^(exponent(max z in primitive) - r) + SlopeScaledDepthBias * MaxDepthSlope;
再限制bias值的范围
if(DepthBiasClamp > 0) Bias = min(DepthBiasClamp, Bias) else if(DepthBiasClamp < 0) Bias = max(DepthBiasClamp, Bias)
最后使用bias值计算像素z值
if ( (DepthBias != 0) || (SlopeScaledDepthBias != 0) ) z = z + Bias
-
-
-
走样
- 原因:产生走样的原因很明显,就是阴影图的深度缓冲区分辨率不足导致的,多个不同顶点都采样在同一个纹素
- 解决方案:mipmap
百分比渐近过滤(PCF)
-
含义:百分比渐近过滤( percentage closer filtering, PCF)原本是一种抗锯齿算法,在对Shadow Map采样过程中,一次性取附近多个纹素与shading point的屏幕空间z值进行比较,得到二值化(图像的灰度值设置为0/255,只呈现黑白)数据,再对二值化数据(加权)平均得到非二值数据,从而达到软化阴影锯齿的目的
-
为什么需要它?
- 可用于实现软阴影
- 由于在使用投影纹理坐标(u,v)对阴影图进行采样时,一般不会命中阴影图中纹素的准确位置,通常是位于四个纹素间,因此我们应对采样结果进行插值
-
实现:以插值的方式在坐标\(\large (u,v)、(u + \Delta_x, v)、(u, v + \Delta_x)、(u + \Delta_x,v + \Delta_x)\)处对纹理进行采样,\(\large \Delta_x = \frac{1}{Shadow Map Size}\)
static const float SMAP_SIZE = 2048.0f; //阴影贴图大小 static const float SMAP_DX = 1.0f / SMAP_SIZE; //Δx Texture2D gShadowMap : register(t1); //对阴影图采样,获取离光源最近的z值 float s0 = gShadowMap.Sample(gShadowSam, projTexC.xy).r; float s1 = gShadowMap.Sample(gShadowSam, projTexC.xy + float2(SMAP_DX, 0)).r; float s2 = gShadowMap.Sample(gShadowSam, projTexC.xy + float2(0, SMAP_DX)).r; float s3 = gShadowMap.Sample(gShadowSam, projTexC.xy + float2(SMAP_DX, SMAP_DX)).r; //该像素z值是否≤阴影图的z值 float result0 = depth <= s0; float result1 = depth <= s1; float result2 = depth <= s2; float result3 = depth <= s3; //变换到纹理空间 float2 texelPos = SMAP_SIZE*projTexC.xy; //插值变量 float2 t = frac(texelPos); //frac()返回参数的小数部分 //双线性插值 return lerp( lerp(result0, result1, t.x), lerp(result2, result3, t.x), t.y);
示意图:
效果图:这样该阴影图中的像素有了更平滑的过渡——一个像素可能部分在阴影中,部分不在
-
不足:可以看出PCF的实现将纹理样本的数量增加了四倍,开销是较高的
解决之道:在DX中提供SampleCmpLevelZero()对PCF技术优化,该函数的作用是仅在 mipmap level 0处 对纹理采样,并将结果与比较值进行比较,该函数仅适用于
比较采样器
(使得硬件能执行阴影图的比较测试)而非普通的采样器对象,对于PCF来说需要使用D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT
过滤器,并将比较函数设为LESS_EQUAL
。因为我们仅仅希望对阴影贴图进行采样并比较采样结果,而不对其生成mipmap链
SampleCmpLevelZero::SampleCmpLevelZero(S,float,float,int,uint) function for Texture2D - Win32 apps | Microsoft LearnDXGI_FORMAT SampleCmpLevelZero( in SamplerState S, //比较采样器对象 in float Location, //纹理坐标 in float CompareValue, //和阴影图进行比较的值 in int Offset, //可选值。纹理坐标偏移量 out uint Status //运算符状态.不能直接访问该状态,需要将状态传递给 CheckAccessFullyMated() );
-
改善后的实现
Texture2D gShadowMap : register(t1); SamplerComparisonState gsamShadow : register(s6); //比较采样器 shadowPosH.xyz /= shadowPosH.w; //齐次裁剪 float depth = shadowPosH.z; //NDC空间中的深度值 gShadowMap.SampleCmpLevelZero(gsamShadow, shadowPosH.xy, depth).r; //执行PCF /* 将比较函数设为LESS_EQUAL,且比较值为depth时,此处SampleCmpLevelZero()相当于执行以下操作 float result0 = depth <= s0; float result1 = depth <= s1; float result2 = depth <= s2; float result3 = depth <= s3; */
定义比较采样器
const CD3DX12_STATIC_SAMPLER_DESC shadow( 6, // shader寄存器 D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT, //过滤器类型 D3D12_TEXTURE_ADDRESS_MODE_BORDER, // U轴使用的寻址模式 D3D12_TEXTURE_ADDRESS_MODE_BORDER, // V轴使用的寻址模式 D3D12_TEXTURE_ADDRESS_MODE_BORDER, // W轴使用的寻址模式 0.0f, // mipmap level offset 16, // 最大各向异性值 D3D12_COMPARISON_FUNC_LESS_EQUAL, // 比较函数 D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK //边框颜色 );
-
注意
PCF只需在阴影边缘执行,内外无需过渡和混合
实现阴影图
阴影图实际也是一个深度缓冲区,对其包装成一个class即可,其中包含缓冲区的分辨率大小、视口变换、裁剪矩阵、生成SRV和DSV
class ShadowMap
{
public:
ShadowMap(ID3D12Device* device, UINT width, UINT height);
ShadowMap(const ShadowMap& rhs)=delete;
ShadowMap& operator=(const ShadowMap& rhs)=delete;
~ShadowMap()=default;
UINT Width()const;
UINT Height()const;
ID3D12Resource* Resource();
CD3DX12_GPU_DESCRIPTOR_HANDLE Srv()const;
CD3DX12_CPU_DESCRIPTOR_HANDLE Dsv()const;
D3D12_VIEWPORT Viewport()const;
D3D12_RECT ScissorRect()const;
void BuildDescriptors(
CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuSrv,
CD3DX12_GPU_DESCRIPTOR_HANDLE hGpuSrv,
CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuDsv);
void OnResize(UINT newWidth, UINT newHeight);
private:
void BuildDescriptors();
void BuildResource();
private:
ID3D12Device* md3dDevice = nullptr;
D3D12_VIEWPORT mViewport;
D3D12_RECT mScissorRect;
UINT mWidth = 0;
UINT mHeight = 0;
DXGI_FORMAT mFormat = DXGI_FORMAT_R24G8_TYPELESS;
CD3DX12_CPU_DESCRIPTOR_HANDLE mhCpuSrv;
CD3DX12_GPU_DESCRIPTOR_HANDLE mhGpuSrv;
CD3DX12_CPU_DESCRIPTOR_HANDLE mhCpuDsv;
Microsoft::WRL::ComPtr<ID3D12Resource> mShadowMap = nullptr;
};
具体应用
-
创建阴影图
std::unique_ptr<ShadowMap> mShadowMap;
-
定义光源视角矩阵、投影矩阵(根据整个场景的包围球来定义)
DirectX::BoundingSphere mSceneBounds; //包围球 mSceneBounds.Center = XMFLOAT3(0.0f, 0.0f, 0.0f); //包围球中心点 mSceneBounds.Radius = sqrtf(10.0f*10.0f + 15.0f*15.0f); //包围球半径 void Update(const GameTimer& gt) { mLightRotationAngle += 0.1f*gt.DeltaTime(); XMMATRIX R = XMMatrixRotationY(mLightRotationAngle); //旋转矩阵 //改变光源观察方向 for(int i = 0; i < 3; ++i) { XMVECTOR lightDir = XMLoadFloat3(&mBaseLightDirections[i]); lightDir = XMVector3TransformNormal(lightDir, R); XMStoreFloat3(&mRotatedLightDirections[i], lightDir); } AnimateMaterials(gt); UpdateObjectCBs(gt); UpdateMaterialBuffer(gt); UpdateShadowTransform(gt); UpdateMainPassCB(gt); UpdateShadowPassCB(gt); } void ShadowMapApp::UpdateShadowTransform(const GameTimer& gt) { // 只有第一个主光源才投射物体阴影 XMVECTOR lightDir = XMLoadFloat3(&mRotatedLightDirections[0]); XMVECTOR lightPos = -2.0f*mSceneBounds.Radius*lightDir; XMVECTOR targetPos = XMLoadFloat3(&mSceneBounds.Center); XMVECTOR lightUp = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); XMMATRIX lightView = XMMatrixLookAtLH(lightPos, targetPos, lightUp); XMStoreFloat3(&mLightPosW, lightPos); // 将包围球变换至光源空间 XMFLOAT3 sphereCenterLS; XMStoreFloat3(&sphereCenterLS, XMVector3TransformCoord(targetPos, lightView)); // 正交投影 float l = sphereCenterLS.x - mSceneBounds.Radius; float b = sphereCenterLS.y - mSceneBounds.Radius; float n = sphereCenterLS.z - mSceneBounds.Radius; float r = sphereCenterLS.x + mSceneBounds.Radius; float t = sphereCenterLS.y + mSceneBounds.Radius; float f = sphereCenterLS.z + mSceneBounds.Radius; mLightNearZ = n; mLightFarZ = f; XMMATRIX lightProj = XMMatrixOrthographicOffCenterLH(l, r, b, t, n, f); // 从 NDC空间 [-1,+1]变换至 uv空间 [0,1] XMMATRIX T( 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.0f, 1.0f); XMMATRIX S = lightView*lightProj*T; XMStoreFloat4x4(&mLightView, lightView); XMStoreFloat4x4(&mLightProj, lightProj); XMStoreFloat4x4(&mShadowTransform, S); }
-
渲染阴影图
void ShadowMapApp::DrawSceneToShadowMap() { mCommandList->RSSetViewports(1, &mShadowMap->Viewport()); mCommandList->RSSetScissorRects(1, &mShadowMap->ScissorRect()); // 将资源屏障变换到 DEPTH_WRITE mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mShadowMap->Resource(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_DEPTH_WRITE)); UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(PassConstants)); // 清空后台缓冲区、深度缓冲区 mCommandList->ClearDepthStencilView(mShadowMap->Dsv(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr); // 因为仅向深度缓冲区绘制数据,所以将渲染目标设为空来禁止颜色数据的写入操作 // 还需把处于启动状态的PSO的渲染目标数量设为0 mCommandList->OMSetRenderTargets(0, nullptr, false, &mShadowMap->Dsv()); // 为阴影图渲染过程帮当所需的常量缓冲区 auto passCB = mCurrFrameResource->PassCB->Resource(); D3D12_GPU_VIRTUAL_ADDRESS passCBAddress = passCB->GetGPUVirtualAddress() + 1*passCBByteSize; mCommandList->SetGraphicsRootConstantBufferView(1, passCBAddress); mCommandList->SetPipelineState(mPSOs["shadow_opaque"].Get()); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]); // 将资源屏障变回GENERIC_READ,使得能从shader中读取纹理 mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mShadowMap->Resource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_GENERIC_READ)); } //pso的渲染目标数量设为0 D3D12_GRAPHICS_PIPELINE_STATE_DESC smapPsoDesc = opaquePsoDesc; smapPsoDesc.RasterizerState.DepthBias = 100000; smapPsoDesc.RasterizerState.DepthBiasClamp = 0.0f; smapPsoDesc.RasterizerState.SlopeScaledDepthBias = 1.0f; smapPsoDesc.pRootSignature = mRootSignature.Get(); smapPsoDesc.VS = { reinterpret_cast<BYTE*>(mShaders["shadowVS"]->GetBufferPointer()), mShaders["shadowVS"]->GetBufferSize() }; smapPsoDesc.PS = { reinterpret_cast<BYTE*>(mShaders["shadowOpaquePS"]->GetBufferPointer()), mShaders["shadowOpaquePS"]->GetBufferSize() }; //不涉及渲染目标 smapPsoDesc.RTVFormats[0] = DXGI_FORMAT_UNKNOWN; smapPsoDesc.NumRenderTargets = 0; ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&smapPsoDesc, IID_PPV_ARGS(&mPSOs["shadow_opaque"])));
-
shader
struct VertexIn { float3 PosL : POSITION; float2 TexC : TEXCOORD; }; struct VertexOut { float4 PosH : SV_POSITION; float2 TexC : TEXCOORD; }; VertexOut VS(VertexIn vin) { VertexOut vout = (VertexOut)0.0f; MaterialData matData = gMaterialData[gMaterialIndex]; // Transform to world space. float4 posW = mul(float4(vin.PosL, 1.0f), gWorld); // Transform to homogeneous clip space. vout.PosH = mul(posW, gViewProj); // Output vertex attributes for interpolation across triangle. float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform); vout.TexC = mul(texC, matData.MatTransform).xy; return vout; } // 仅用于需要进行alpha裁剪的几何图形,使得阴影正确呈现 // 若几何图形无需执行此操作,则使用一个空的PS()1 void PS(VertexOut pin) { // Fetch the material data. MaterialData matData = gMaterialData[gMaterialIndex]; float4 diffuseAlbedo = matData.DiffuseAlbedo; uint diffuseMapIndex = matData.DiffuseMapIndex; // Dynamically look up the texture in the array. diffuseAlbedo *= gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.TexC); #ifdef ALPHA_TEST // Discard pixel if texture alpha < 0.1. We do this test as soon // as possible in the shader so that we can potentially exit the // shader early, thereby skipping the rest of the shader code. clip(diffuseAlbedo.a - 0.1f); #endif }
-
阴影因子
因为一个点可能部分在阴影外部分在阴影内,所以需要用一个因子来表示,阴影因子是光照方程中的一个系数,范围在[0,1],值为0表示在阴影中,值为1表示在阴影外
不过对于间接光,阴影因子对其没有作用
计算阴影因子的函数实现:
float CalcShadowFactor(float4 shadowPosH) { // 齐次裁剪 shadowPosH.xyz /= shadowPosH.w; // NCD空间中的z值 float depth = shadowPosH.z; uint width, height, numMips; gShadowMap.GetDimensions(0, width, height, numMips); // 纹素大小 float dx = 1.0f / (float)width; float percentLit = 0.0f; const float2 offsets[9] = { float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx), float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f), float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx) }; [unroll] for(int i = 0; i < 9; ++i) { percentLit += gShadowMap.SampleCmpLevelZero(gsamShadow, shadowPosH.xy + offsets[i], depth).r; } return percentLit / 9.0f; }
-
光照
阴影因子和光照方程相乘即可
// 只有第一个光源才投射阴影 float3 shadowFactor = float3(1.0f, 1.0f, 1.0f); shadowFactor[0] = CalcShadowFactor(pin.ShadowPosH); const float shininess = (1.0f - roughness) * normalMapSample.a; Material mat = { diffuseAlbedo, fresnelR0, shininess }; float4 directLight = ComputeLighting(gLights, mat, pin.PosW, bumpedNormalW, toEyeW, shadowFactor); float4 ComputeLighting(Light gLights[MaxLights], Material mat, float3 pos, float3 normal, float3 toEye, float3 shadowFactor) { float3 result = 0.0f; int i = 0; #if (NUM_DIR_LIGHTS > 0) for(i = 0; i < NUM_DIR_LIGHTS; ++i) { result += shadowFactor[i] * ComputeDirectionalLight(gLights[i], mat, normal, toEye); } #endif #if (NUM_POINT_LIGHTS > 0) for(i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS+NUM_POINT_LIGHTS; ++i) { result += ComputePointLight(gLights[i], mat, pos, normal, toEye); } #endif #if (NUM_SPOT_LIGHTS > 0) for(i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i) { result += ComputeSpotLight(gLights[i], mat, pos, normal, toEye); } #endif return float4(result, 0.0f); }
第二次PASS实现
阴影图构建完成后,即可以相机视角进行渲染,再确定某像素是否位于阴影中
//在VS中,为阴影图生成的投影纹理坐标
vout.ShadowPosH = mul(posW, gShadowTransform);
//在PS中,对阴影图进行检测
float3 shadowFactor = float3(1.0f, 1.0f, 1.0f);
shadowFactor[0] = CalcShadowFactor(pin.ShadowPosH);
reference
games101
games202
Depth Bias - Win32 apps | Microsoft Learn
Directx12 3D 游戏开发实战
标签:Map,1.0,0.0,float,阴影,DX,D3D12,Shadow,const From: https://www.cnblogs.com/chenglixue/p/17259018.html