Perspective Shadow Map
최초작성 : 2020-08-05
마지막수정 : 2020-08-06
최재호
Updated 2020-08-06 : Regular와 Inverse Perspective Matrix를 설명하는 Graph를 추가 (추가그림1, 2, 3)
목차
1. 목표
2. 내용
2.1. PSM이 나온 동기
2.2. 기존 SSM과 차이점
2.3. PostPerspective 공간에서의 Light 방향의 변화
2.4. Potential ShadowCaster
2.5. Depth Precision 그리고 Near Plane
2.6. Depth Precision 올리기
2.7. Perspective Projection Matrix Tricks
3. 실제구현
4. 구현결과
5. 실제구현 코드
6. 레퍼런스
1. 목표
Perspective Shadow Map에 대해서 이해하고 실제 구현해봅시다.
이 구현에서는 Directional Light(Orthogonal Matrix 사용)를 PSM으로 구현해봅니다.
2. 내용
2.1. PSM 이 나온 동기
현재 프러스텀의 내용만 쉐도우맵에 반영하는 것을 목표로 합니다.
아래 그림1 은 좌측은 SSM(Standard Shadow Map) 우측은 PSM 입니다. PSM은 현재 카메라의 View Frustum에 들어온 오브젝트 만 쉐도우맵에 담으며, 카메라와 가까울 수록 쉐도우맵에서 더 큰 영역을 차지하도록 해주는 것을 볼 수 있습니다.
2.2. 기존 SSM과 차이점
ShadowMap을 그릴때 사용하는 Matrix 내용만 달라지기 때문에 Matrix를 생성하는 로직만 변경됩니다. PSM은 지오메트리들을 PostPerspective 공간으로 변환한 후 이 PostPerspective 공간을 쉐도우맵에 렌더링합니다. SSM과 PSM 에서 사용하는 ProjectionView Matrix의 차이점은 아래와 같습니다.
SSM : Projection * View
PSM : PostPerspectiveProjection * PostPerspectiveView * VirtualCameraProjection * VirtualCameraView
VirtualCamera의 경우 SSM에서 사용한 Projection과 View를 그대로 쓰거나 혹은 PSM의 품질을 높이기 위해서 조금 수정한 버젼의 Camera 입니다.
PostPerspectiveProjection * PostPerspectiveView 는 PostPerspective 공간으로 변환된 지오메트리를 촬영하기 위한 Camera 입니다.
2.3. PostPerspective 공간에서의 Light 방향의 변화
PostPerspective 공간에서는 World 공간의 View Frustum 안에 있는 모든 내용들이 UnitCube에 모두 들어가게 됩니다(그림2와 그림3 참고). 이 과정에서 물체들은 Perspective Matrix에 의해서 가까운 물체는 더 크게 먼 물체는 더 작게 변하여 UnitCube에 들어가게 됩니다.
PostPerspective 공간에서 쉐도우맵을 만들기 위해서는 Light 역시 PostPerspective 공간으로 이동시켜야 합니다.
이 과정에서 Light의 방향 역시 왜곡이 발생합니다. Directional Light는 PostPerspective 공간에서 3가지 형태를 가집니다. (여기서 Directional Light는 Orthogonal Matrix를 사용하는 평행 광원입니다.)
1). View 방향과 Light 방향이 수직인 경우
- 이 경우 PostPerspective 공간으로 이동하더라도 왜곡이 발생하지 않습니다.
- VirtualCamera 항목으로 기존 Directional Light의 Orthogonal Matrix를 그대로 사용할 수 있습니다.
2). Light 방향이 View의 반대 방향인 경우
- Light가 PostPerspective 공간으로 이동되면서 PointLight와 같이 변화합니다.
3). Light 방향이 View 방향으로 향하는 경우
- 마찬가지로 Light가 PostPerspective 공간으로 이동되면서 PointLight와 같이 변화합니다.
- Light의 위치가 Light의 방향의 반대 방향에 생성되게 됩니다.
2.4. Potential ShadowCaster
Light가 View 방향과 같은 방향인 경우 카메라 뒤에서부터 앞쪽으로 향하는 쉐도우가 있을 수 있습니다. 이 쉐도우를 생성하기 위해서 쉐도우케스터를 카메라의 프러스텀에 포함시켜줘야 합니다. 그렇지 않는다면 그림7의 좌측 이미지 처럼 라이트가 케스터를 순서대로 지나가지 않습니다. 카메라 보다 더 뒤쪽에 있는 정점은 PostPerspective 공간에서 Far Plane 위치보다 더 먼위치에 위치하게 됩니다. 그림8 을 참고해주세요.
이 부분을 해결하기 위해서 Virtual Camera 를 뒤로 당겨줘서 ShadowCaster를 포함시켜 줍니다.
혹은 2.7 Perspective Projection Matrix Tricks 을 사용하여 카메라 뒤에 오브젝트가 배치되더라도 문제 없이 라이트가 쉐도우케스터를 순서대로 지나가도록 하여 Depth Buffer에 올바른 Depth 순서로 기록될 수 있도록 할 수도 있습니다.
현재 예제에서는 "Light 방향이 View 방향으로 향하는 경우"에서 Perspective Projection Matrix Tricks를 적용하였기 때문에 Potential ShadowCaster가 있는 경우도 정상적으로 쉐도우가 케스팅 됩니다.
2.5. Depth Precision 그리고 Near Plane
2.6. Depth Precision 올리기
Depth Precision을 올리기 위해서 월드상에 쉐도우를 받는 오브젝트 중 메인 카메라와 가장 가까이 있는 점이 어디인지 아는 것이 중요합니다. 왜냐하면 메인 카메라의 Near Plane을 최대한 크게 설정할 수록 PostPerspective 공간에서 오브젝트가 더 큰 부피를 가질 수 있게 되고, 이것 때문에 쉐도우맵에서 더 큰 영역을 차지할 수 있습니다.
이 부분을 위해서 우리는 모든 오브젝트들의 AABB 바운드를 Merge 합니다. 그리고 이 AABB와 카메라 위치사이의 거리를 구하여 이 값을 Near Plane으로 설정합니다.
// 새로운 점을 AABB에 병합
void Merge(const Vector& newPoint)
{
MinPoint.x = Min(MinPoint.x, newPoint.x);
MinPoint.y = Min(MinPoint.y, newPoint.y);
MinPoint.z = Min(MinPoint.z, newPoint.z);
MaxPoint.x = Max(MaxPoint.x, newPoint.x);
MaxPoint.y = Max(MaxPoint.y, newPoint.y);
MaxPoint.z = Max(MaxPoint.z, newPoint.z);
}
또한 카메라의 Near Plane이 너무 작다면, 현재 카메라를 뒤로 일정거리 이동시키고 이동시킨 거리만큼 Near Plane을 증가시켜주는 것으로도 정밀도를 높일 수 있습니다. 하지만 이경우는 카메라를 뒤로 너무 많이 이동시키는 경우 NDC 공간의 UnitCube 상에서 지오메트리 자체의 크기가 작아질 수 있습니다. 이렇게 되면 쉐도우맵에서 차지하는 크기가 작아지면서 쉐도우의 품질이 오히려 나빠지거나 쉐도우맵에 그려지지 못할 수도있습니다. 그러니 테스트 해본 후 적당한 값을 설정하는 것이 좋습니다.
2.7. Perspective Projection Matrix Tricks
View의 방향과 Light의 방향이 일치하는 경우 (그림6). 라이트의 위치가 뒤바뀌어버리는 현상이 발생합니다. 그래서 쉐도우맵을 그릴때 비교 함수를 반대로 바꾸어줘야지 원래 라이트 방향의 쉐도우맵이 생성될 것입니다. 아래 그림10의 우측이미지의 빨간색 라인이 쉐도우맵에 기록되어져야하는 Depth 값입니다.
비교함수를 반대로 변경하지 않고도 멀리 있는 Depth를 우선해서 기록할 수 있는 방법도 있습니다. Near Plane을 마이너스로 두게 되면 그림15. 처럼 라이트가 Near Plane 방향(음의 무한)으로 이동 후 -> Far의 양의무한에서 Light 위치로 다시 돌아오는 형태로 이동합니다. 이 방식을 사용하여 멀리 있는 픽셀을 Depth 버퍼의 더 앞쪽에 쓸 수 있습니다. 설명만으로는 이해가 힘들 수 있어서 예제를 구성해보았습니다. 그림16를 봐주세요.
이렇게 Near가 마이너스, Far이 플러스에 있는 Perspective Matrix를 Inverse Perspective Matrix라 부릅니다.
Inverse Perspective Matrix는 정밀도 문제가 발생할 수 있습니다. 예제를 만들면서 발생한 문제는 아래 2가지 입니다.
- Potential ShadowCaster 처리시 특정 상황에서 쉐도우가 일부 사라지는 문제
- Light와 쉐도우 리시버가 특정 각도가 되면 쉐도우가 일부 사라졌다가 다시 나타는 현상이 있었습니다. 쉐도우맵을 확인해보면 문제가 발생하는 경우에 쉐도우맵에 거의 그려지는 픽셀이 거의 없는 것을 확인할 수 있었습니다. 그래서 VitualSlideBack 값 (카메라를 뒤로 당기고 당긴만큼 Near 을 밀어주는 것)을 100 정도로 설정하여 쉐도우맵에서 오브젝트가 차지하는 부피를 늘려서 해결했습니다.
- "Light 방향이 View 방향으로 향하는 경우"와 "Light 방향이 View의 반대 방향인 경우" 케이스가 전환되는 시점
- 이 경우는 쉐도우가 순간 사라졌다가 다시 나타나는 증상이 있었습니다. 위의 2 경우가 전환될 때 정밀도가 일시적으로 아주 낮아지는 현상이 문제였습니다. 렌더독으로 쉐도우맵의 Depth 값을 확인해본 결과 한개의 오브젝트가 렌더링 되는 경우에 그려지는 전체 픽셀의 Depth값의 차이가 소수점 아래 3자리 보다 더 낮은 숫자만 변화하는 것을 확인했습니다. 이 부분을 고려하여 Shader에서 아주 낮은 적절한 DepthBias를 사용하는 것으로 해결하였습니다.
3. 실제구현
렌더링 순서 (총 7단계)
1). ShadowCaster의 AABB 바운드 박스를 생성
// 1. ShadowReciver의 AABB 바운드 박스를 생성합니다.
BoundingBox ShadowReceiversBoundBox;
for (auto obj : jObject::GetStaticObject())
{
if (obj->SkipShadowMapGen)
continue;
IStreamParam* StreamParam = obj->RenderObject->VertexStream->Params[0];
jStreamParam<float>* FloatStreamParam = static_cast<jStreamParam<float>*>(StreamParam);
auto ElementCount = FloatStreamParam->Data.size() / 3;
std::vector<Vector> Vertices;
Vertices.resize(ElementCount);
memcpy(&Vertices[0], FloatStreamParam->GetBufferData(), sizeof(Vector) * ElementCount);
auto posMatrix = Matrix::MakeTranslate(obj->RenderObject->Pos);
auto rotMatrix = Matrix::MakeRotate(obj->RenderObject->Rot);
auto scaleMatrix = Matrix::MakeScale(obj->RenderObject->Scale);
Matrix World = posMatrix * rotMatrix * scaleMatrix;
Matrix ViewWorld = MainCamera->View * World;
for (const Vector& Vertex : Vertices)
{
Vector TransformedVertex = World.Transform(Vertex);
ShadowReceiversBoundBox.Merge(TransformedVertex);
}
}
2). CameraFit 모드인 경우. MainCamera의 Near을 ShadowReciver의 AABB 바운드 박스를 참고해 최대값으로 설정
// 2. CameraFit 모드인 경우. MainCamera의 Near을 ShadowReciver의 AABB 바운드 박스에 맞게 Near을 키워줌.
float MinZ = FLT_MAX;
float MaxZ = -FLT_MAX;
if (Settings.CameraFit)
{
Vector MainCameraForward = MainCamera->GetForwardVector();
Vector MainCameraRight = MainCamera->GetRightVector();
for (int i = 0; i < 8; ++i)
{
auto ToBB = (ShadowReceiversBoundBox.GetPoint(i) - MainCamera->Pos);
float Z = MainCameraForward.DotProduct(ToBB);
float X = MainCameraRight.DotProduct(ToBB);
MinZ = Min(Z, MinZ);
MaxZ = Max(Z, MaxZ);
}
if (MainCamera->Near < MinZ)
MainCamera->Near = MinZ;
}
3). VirtualCamera의 Projection, View Matrix를 생성
// 3. VirtualCamera의 Projection, View Matrix를 구한다.
Matrix VCView;
Matrix VCProj;
if (Settings.PossessMockCamera)
{
VCView = MockView;
VCProj = MockProj;
}
else
{
// MainCamera에 Virtual Slide Back 을 반영 함.
auto PrevUp = MainCamera->GetUpVector();
Vector Pos = MainCamera->Pos - Settings.VirtualSliderBack * MainCamera->GetForwardVector();
Vector Target = MainCamera->Target;
Vector Up = Pos + PrevUp;
VCView = jCameraUtil::CreateViewMatrix(Pos, Target, Up);
VCProj = jCameraUtil::CreatePerspectiveMatrix(SCR_WIDTH, SCR_HEIGHT, MainCamera->FOVRad
, MainCamera->Far, MainCamera->Near + Settings.VirtualSliderBack);
}
4). PostPerspective 공간에서 사용할 Projection, View Marix를 생성
// 4. PostPerspective 공간에서 사용할 Projection, View Marix를 구한다.
auto MakePPInfo = [&](std::shared_ptr<jCamera>& OutPPCamera, Matrix& OutProjPP, Matrix& OutViewPP
, const Matrix& InView, const Matrix& InProj, bool updatePPCamera)
{
Vector CubeCenterPP = Vector::ZeroVector;
float CubeRadiusPP = Vector::OneVector.Length(); // 6. PP 공간의 큐브의 반지름을 구함. (OpenGL은 NDC 공간이 X, Y, Z 모두 길이 2임)
Vector LightPosPP;
float FovPP = 0.0f;
float NearPP = 0.0f;
float FarPP = 0.0f;
// Camera 공간의 LightDir 구함
Vector EyeLightDir = InView.Transform(Vector4(LightDir, 0.0f));
// Camera 공간의 LightDir을 PP 공간으로 이동시킴
Vector4 LightPP = InProj.Transform(Vector4(EyeLightDir, 0.0f));
// 라이트가 Eye의 뒤쪽에 있는지 판단한다.
bool LightIsBehindOfEye = (LightPP.w < 0.0f);
// 시야 방향과 라이트가 직교하는 상태인지 확인하고, 직교하면 OrthoMatrix 사용
static float W_EPSILON = 0.001f;
bool IsOrthoMatrix = (fabsf(LightPP.w) <= W_EPSILON);
float WidthPP = 1.0f;
float HeightPP = 1.0f;
if (IsOrthoMatrix)
{
Vector LightDirPP(LightPP.x, LightPP.y, LightPP.z);
// NDC Unit Cube를 딱 감쌀 수 있는 View와 Projection Matrix를 생성합니다.
LightPosPP = CubeCenterPP + 2.0f * CubeRadiusPP * LightDirPP;
float DistToCenter = LightPosPP.Length();
NearPP = DistToCenter - CubeRadiusPP;
FarPP = DistToCenter + CubeRadiusPP;
Vector UpVector = Vector::UpVector;
if (fabsf(UpVector.DotProduct((CubeCenterPP - LightPosPP).GetNormalize())) > 0.99f)
UpVector = Vector::RightVector;
OutViewPP = jCameraUtil::CreateViewMatrix(LightPosPP, CubeCenterPP, (LightPosPP + UpVector));
OutProjPP = jCameraUtil::CreateOrthogonalMatrix(CubeRadiusPP * 2, CubeRadiusPP * 2, FarPP, NearPP);
// PP 공간 디버깅용 카메라 업데이트
if (updatePPCamera)
{
OutPPCamera = std::shared_ptr<jCamera>(jCamera::CreateCamera(LightPosPP, CubeCenterPP, (LightPosPP + UpVector)
, FovPP, NearPP, FarPP, CubeRadiusPP * 2, CubeRadiusPP * 2, !IsOrthoMatrix));
OutPPCamera->UpdateCamera();
}
}
else
{
// PP 공간의 LightDir로 변경한 후, LightDir을 LightPos으로 변경함.
float wRecip = 1.0f / LightPP.w;
LightPosPP.x = LightPP.x * wRecip;
LightPosPP.y = LightPP.y * wRecip;
LightPosPP.z = LightPP.z * wRecip;
// LightPP위치에서 CubeCenter를 바라보는 벡터와 그 벡터의 거리를 구함.
Vector LookAtCubePP = (CubeCenterPP - LightPosPP);
float DistLookAtCubePP = LookAtCubePP.Length();
LookAtCubePP /= DistLookAtCubePP;
if (LightIsBehindOfEye)
{
Vector ToBSphereDirection = CubeCenterPP - LightPosPP;
const float DistToBSphereDirection = ToBSphereDirection.Length();
ToBSphereDirection = ToBSphereDirection.GetNormalize();
float NearPP = DistToBSphereDirection - CubeRadiusPP;
FovPP = 2.0f * atanf(CubeRadiusPP / DistToBSphereDirection);
// Perspective Matrix의 Near를 마이너스로 두는 트릭을 사용함.
NearPP = Max(0.1f, NearPP);
FarPP = NearPP;
NearPP = -NearPP;
// PostPerspective 공간에서 사용할 Projection 을 계산
OutProjPP = jCameraUtil::CreatePerspectiveMatrix(WidthPP, HeightPP, FovPP, FarPP, NearPP);
}
else
{
// NDC Unit Cube 공간의 바운드박스를 만듬
FovPP = 2.0f * atanf(CubeRadiusPP / DistLookAtCubePP);
float AspectPP = 1.0f;
NearPP = std::max(0.1f, DistLookAtCubePP - CubeRadiusPP);
FarPP = DistLookAtCubePP + CubeRadiusPP;
// PostPerspective 공간에서 사용할 Projection 을 계산
OutProjPP = jCameraUtil::CreatePerspectiveMatrix(1.0f, 1.0f, FovPP, FarPP, NearPP);
}
Vector UpVector = Vector::UpVector;
if (fabsf(Vector::UpVector.DotProduct(LookAtCubePP)) > 0.99f)
UpVector = Vector::RightVector;
// PP에서의 라이트 위치, PP의 중심, 위에서 구한 Up벡터를 사용해서 PP에서의 ViewMatrix 구함.
OutViewPP = jCameraUtil::CreateViewMatrix(LightPosPP, CubeCenterPP, (LightPosPP + UpVector));
// PP 공간 디버깅용 카메라 업데이트
if (updatePPCamera)
{
OutPPCamera = std::shared_ptr<jCamera>(jCamera::CreateCamera(LightPosPP, CubeCenterPP, (LightPosPP + UpVector)
, FovPP, NearPP, FarPP, WidthPP, HeightPP, !IsOrthoMatrix));
OutPPCamera->UpdateCamera();
}
}
};
Matrix ProjPP;
Matrix ViewPP;
std::shared_ptr<jCamera> PPCamera = nullptr;
if (Settings.PossessMockCamera)
{
MakePPInfo(PPCamera, ProjPP, ViewPP, MockView, MockProj, true);
}
else
{
if (Settings.PossessOnlyMockCameraPP)
{
MakePPInfo(PPCamera, ProjPP, ViewPP, MockView, MockProj, true);
MakePPInfo(PPCamera, ProjPP, ViewPP, VCView, VCProj, false);
}
else
{
MakePPInfo(PPCamera, ProjPP, ViewPP, VCView, VCProj, true);
}
}
5). PSM 용 Matrix 생성
// 5. PSM 용 Matrix 생성
// ProjPP * ViewPP * VirtualCameraProj(별도로 만든 Proj) * VirtualCameraView(현재 카메라의 View)
Matrix PSM_Mat = ProjPP * ViewPP * VCProj * VCView;
6). PSM ShadowMap 생성
// 6. PSM ShadowMap 생성
if (DirectionalLight->GetShadowMapRenderTarget()->Begin())
{
auto ClearColor = Vector4(1.0f, 1.0f, 1.0f, 1.0f);
auto ClearType = ERenderBufferType::COLOR | ERenderBufferType::DEPTH;
auto EnableClear = true;
auto EnableDepthTest = true;
auto DepthStencilFunc = EComparisonFunc::LESS;
auto EnableBlend = false;
auto BlendSrc = EBlendSrc::ONE;
auto BlendDest = EBlendDest::ZERO;
auto Shader = jShader::GetShader("ShadowGen_PSM");
if (EnableClear)
{
g_rhi->SetClearColor(ClearColor);
g_rhi->SetClear(ClearType);
}
g_rhi->EnableDepthTest(EnableDepthTest);
g_rhi->EnableBlend(EnableBlend);
g_rhi->SetBlendFunc(BlendSrc, BlendDest);
g_rhi->SetShader(Shader);
SET_UNIFORM_BUFFER_STATIC(Matrix, "PSM", PSM_Mat, Shader);
const jCamera* ShadowMapCamera = DirectionalLight->ShadowMapData->ShadowMapCamera;
ShadowMapCamera->BindCamera(Shader);
for (auto iter : jObject::GetStaticObject())
{
if (!iter->SkipShadowMapGen)
iter->Draw(ShadowMapCamera, Shader, lights);
}
DirectionalLight->GetShadowMapRenderTarget()->End();
}
7). 메인장면 렌더링
// 7. 메인장면 렌더링
{
auto ClearColor = Vector4(0.0f, 0.0f, 0.0f, 1.0f);
auto ClearType = ERenderBufferType::COLOR | ERenderBufferType::DEPTH;
auto EnableClear = true;
auto EnableDepthTest = true;
auto DepthStencilFunc = EComparisonFunc::LESS;
auto EnableBlend = false;
auto BlendSrc = EBlendSrc::ONE;
auto BlendDest = EBlendDest::ZERO;
auto Shader = jShader::GetShader("PSM");
//auto Shader = jShader::GetShader("Simple");
if (EnableClear)
{
g_rhi->SetClearColor(ClearColor);
g_rhi->SetClear(ClearType);
}
g_rhi->EnableCullFace(true);
g_rhi->EnableDepthBias(true);
g_rhi->SetDepthBias(Settings.ConstBias, Settings.SlopBias);
g_rhi->EnableDepthTest(EnableDepthTest);
g_rhi->EnableBlend(EnableBlend);
g_rhi->SetBlendFunc(BlendSrc, BlendDest);
g_rhi->SetShader(Shader);
auto CurrentCamera = Settings.PossessMockCamera ? MockCamera : MainCamera;
if (Settings.PossessMockCamera)
{
g_rhi->SetViewport((SCR_WIDTH - SCR_HEIGHT) / 2, 0, SCR_HEIGHT, SCR_HEIGHT);
}
else
{
g_rhi->SetViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
}
CurrentCamera->BindCamera(Shader);
jLight::BindLights(lights, Shader);
SET_UNIFORM_BUFFER_STATIC(Matrix, "PSM", PSM_Mat, Shader);
for (auto iter : jObject::GetStaticObject())
iter->Draw(CurrentCamera, Shader, lights);
// ShadowReciver AABB를 렌더링 해줄 오브젝트 생성
static auto ShadowReceiverBoundBoxObject = jPrimitiveUtil::CreateBoundBox(
{ ShadowReceiversBoundBox.MinPoint, ShadowReceiversBoundBox.MaxPoint }, nullptr, Vector4(0.37f, 0.62f, 0.47f, 1.0f));
ShadowReceiverBoundBoxObject->Update(deltaTime);
ShadowReceiverBoundBoxObject->UpdateBoundBox({ ShadowReceiversBoundBox.MinPoint, ShadowReceiversBoundBox.MaxPoint });
// ShadowReciver AABB 렌더링
if (Settings.ShowMergedBoundBox)
{
Shader = jShader::GetShader("BoundVolumeShader");
g_rhi->SetShader(Shader);
CurrentCamera->BindCamera(Shader);
jLight::BindLights(lights, Shader);
ShadowReceiverBoundBoxObject->SetUniformBuffer(Shader);
ShadowReceiverBoundBoxObject->Draw(CurrentCamera, Shader, lights);
}
Shader = jShader::GetShader("DebugObjectShader");
for (auto iter : jObject::GetDebugObject())
iter->Draw(MainCamera, Shader, lights);
}
Shader는 SSM과 같은 것을 사용하면 됩니다. 쉐이더 코드는 아래와 같습니다.
// PSM 생성 버택스 쉐이더 : vs_psm_shadowmap.glsl
#version 330 core
#include "common.glsl"
precision mediump float;
layout(location = 0) in vec3 Pos;
uniform mat4 PSM;
uniform mat4 M;
void main()
{
gl_Position = PSM * M * vec4(Pos, 1.0);
}
//////////////////////////////////////////////////////////////////////////
// PSM 생성 픽셀 쉐이더 : ps_psm_shadowmap.glsl
#version 330 core
precision mediump float;
void main()
{
gl_FragData[0].xyz = gl_FragCoord.zzz;
gl_FragData[0].w = 1.0;
}
//////////////////////////////////////////////////////////////////////////
// 메인장면 렌더링 버택스 쉐이더 : vs_psm.glsl
#version 330 core
#preprocessor
#include "common.glsl"
precision mediump float;
layout(location = 0) in vec3 Pos;
layout(location = 1) in vec4 Color;
layout(location = 2) in vec3 Normal;
uniform mat4 MVP;
uniform mat4 M;
out vec3 Pos_;
out vec4 Color_;
out vec3 Normal_;
void main()
{
gl_Position = MVP * vec4(Pos, 1.0);
Color_ = Color;
Normal_ = TransformNormal(M, Normal);
Pos_ = TransformPos(M, Pos);
}
//////////////////////////////////////////////////////////////////////////
// 메인장면 렌더링 픽셀 쉐이더 : ps_psm.glsl
#version 330 core
#preprocessor
#include "shadow.glsl"
precision highp float;
precision highp sampler2DArray;
#define MAX_NUM_OF_DIRECTIONAL_LIGHT 1
uniform sampler2DShadow shadow_object;
uniform int NumOfDirectionalLight;
layout(std140) uniform DirectionalLightBlock
{
jDirectionalLight DirectionalLight[MAX_NUM_OF_DIRECTIONAL_LIGHT];
};
layout(std140) uniform DirectionalLightShadowMapBlock
{
mat4 ShadowVP;
mat4 ShadowV;
vec3 LightPos; // Directional Light Pos 임시
float LightZNear;
float LightZFar;
vec2 ShadowMapSize;
};
uniform vec3 Eye;
uniform int ShadowOn;
uniform mat4 PSM;
in vec3 Pos_;
in vec4 Color_;
in vec3 Normal_;
out vec4 color;
#define PSM_SHADOW_BIAS_DIRECTIONAL 0.00001
float IsPSMShadowing(vec3 lightClipPos, sampler2DShadow shadow_object)
{
if (IsInShadowMapSpace(lightClipPos))
return texture(shadow_object, vec3(lightClipPos.xy, lightClipPos.z - PSM_SHADOW_BIAS_DIRECTIONAL));
return 1.0;
}
void main()
{
vec3 normal = normalize(Normal_);
vec3 viewDir = normalize(Eye - Pos_);
vec4 tempShadowPos = (PSM * vec4(Pos_, 1.0));
tempShadowPos /= tempShadowPos.w;
vec3 ShadowPos = tempShadowPos.xyz * 0.5 + 0.5; // Transform NDC space coordinate from [-1.0 ~ 1.0] into [0.0 ~ 1.0].
vec4 shadowCameraPos = (ShadowV * vec4(Pos_, 1.0));
bool shadow = false;
vec3 directColor = vec3(0.0, 0.0, 0.0);
for (int i = 0; i < MAX_NUM_OF_DIRECTIONAL_LIGHT; ++i)
{
if (i >= NumOfDirectionalLight)
break;
float lit = 1.0;
if (ShadowOn > 0)
{
lit = IsPSMShadowing(ShadowPos, shadow_object);
}
if (lit > 0.0)
{
jDirectionalLight light = DirectionalLight[i];
directColor += GetDirectionalLight(light, normal, viewDir) * lit;
}
}
color = vec4(Color_.xyz * (directColor + vec3(0.3)), 1.0);
}
4. 구현결과
5. 실제구현 코드
https://github.com/scahp/Shadows/tree/PSM
6. 레퍼런스
2. [번역] Perspective Shadow Maps: Care and Feeding
3. http://download.nvidia.com/developer/SDK/Individual_Samples/screenshots/samples/PracticalPSM.html
'Graphics > Graphics' 카테고리의 다른 글
Accurate atmospheric scattering (0) | 2020.09.01 |
---|---|
Absorption and Scattering (흡수와 산란) (0) | 2020.08.11 |
Smooth Min (IQ's polynomial smooth minimum) (3) | 2020.07.17 |
Cascade Shadow Map (2) | 2020.07.10 |
VolumeLight (0) | 2020.06.24 |