| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- VGPR
- GPU Driven Rendering
- Shadow
- Graphics
- shader
- scattering
- SGPR
- RayTracing
- ShadowMap
- Study
- wave
- 번역
- DX12
- ue4
- UE5
- scalar
- hzb
- rendering
- forward
- vulkan
- Wavefront
- optimization
- SIMD
- unrealengine
- DirectX12
- Nanite
- texture
- deferred
- GPU
- atmospheric
- Today
- Total
RenderLog
Pixel-projected reflections 본문
Pixel-projected reflections
최초 작성 : 2021-03-07
마지막 수정 : 2021-03-14
최재호
Updated 2021-03-14 : Non-Glossy Reflection 설명에서 Glossy Reflecton 이 Roughness를 고려한 표면이란 것 추가
목차
1. 목표
2. 내용
3. 실제 구현
4. 구현 결과
5. 실제 구현 코드
6. 레퍼런스
1. 목표
Screen space reflection의 한 종류인 Pixel-projected reflections 에 대해서 이해하고 구현합니다.
2. 내용
Pixel-projected reflections는 Screen space reflection의 한 종류입니다. 기존의 SSR보다 더 나은 성능을 갖고 있지만 제한점 또한 갖고 있습니다.
2.1. SSR 와의 차이점
- Non-glossy Reflection 임
Glossy Reflecton 이란 표면의 많은 미세 표면들에 의해서 라이트가 정반사(specular reflection) 형태로도 반사될 뿐만 아니라 Diffuse reflection 형태로도 반사시켜주는 형태입니다. 즉, Roughness 를 고려한 표면입니다. (레퍼런스3 참고). Pixel-projected relfections은 표면이 매끈하다고 가정하고, 반사평면을 기준으로 Mirror 된 위치를 계산합니다. 그래서 Non-glossy Reflection만 지원합니다.

- Planar surfaces 에 대한 Reflections만 적용 가능
- 노멀맵이 지원되지 않지만 근사하여 처리할 수 있음 (추가 부하가 듬)
- 더 높은 성능과 품질
- Ray-marching이 아닌 Reflection 추적을 역으로 수행하여 계산
2.2. Pixel-projected reflections의 원리
Pixel-projected reflections에는 2개의 렌더패스가 핵심입니다. Projection / Reflection pass 입니다.
2.2.1. Projection pass
1). 반사평면의 평면의 방정식을 얻음 (월드좌표 기준)
2). 반사되어 평면에 보일 위치의 월드좌표를 평면을 기준으로 Mirrored 시킴
3). 카메라에서부터 나오는 반직선이 Mirrored 된 월드좌표에 도달하며, 반사되는 평면을 지나는지 확인

4). Mirrored 된 월드좌표가 반사되어 평면에 표시될 수 있다면, Mirrored 전의 월드좌표에 해당하는 픽셀 정보를 저장합니다.
- 이때, 물체 위에서 반사하는 곳의 컬러를 바로 저장하면 좋겠지만, 반사되어 평면에 들어오는 픽셀들이 여러 개일 수 있습니다. 그래서 반사평면 위의 한 점으로 물체들 위에서 반사되는 픽셀 정보가 서로 겹치는 경우가 있을 수 있습니다. 이 문제를 해결하기 위해서, 반사평면과 가장 가까운 픽셀을 저장하기 위해서 컬러 값이 아닌 다른 정보를 저장합니다.
- 이 정보는 바로 현재 처리 중인 픽셀과 표시될 픽셀(반사되는 픽셀) 간의 오프셋 정보를 저장합니다. 여기서의 오프셋은 화면 공간에서의 오프셋으로 x, y 컴포넌트입니다.
- 이런 오프셋 정보는 Intermediate buffer라고 부르는 버퍼에 저장됩니다.
- 픽셀들이 여러 개라 서로 겹치는 경우는 2 부분으로 나눠볼 수 있습니다. (그림3을 참고)
- 카메라에서 평면까지 이동 중에 겹치는 부분 : 이 부분은 Depth Buffer를 통해 월드 좌표를 복원하거나 또는 디퍼드렌더링 사용 시 GBuffer의 월드좌표를 사용하면 쉽게 해결됩니다. 왜냐하면 두 정보 모두 카메라에서 가장 가까운 정보들이 저장되기 때문에 반사평면 자체가 화면에서 가려질 것이므로 처리 대상이 될 수 없을 것입니다.
- 평면에서 물체 사이에 겹치는 부분 : 이 경우는 평면에서부터 반사하는 물체 사이의 거리가 가까울수록 평면의 반사지점에 더 근접하다고 판단하여 해결합니다.

2.2.2. Reflection pass
Projection pass 에서 저장한 Intermediate buffer 정보의 Offset을 사용하여 실제 Reflection 정보를 반영합니다.
여기서 artifacts가 발생하는데, Hole patching -> Filtering -> Color bleeding 처리를 통해 문제를 해결합니다.
2.2.3. Intermediate buffer의 최적화
레퍼런스1에서는 Intermediate buffer를 최적화하기 위해서 UInt32 데이터에 Offset의 x, y 넣습니다.
x, y 중 더 큰 값을 가지는 쪽을 Y축으로 설정합니다. 그리고 Y축이 큰 값을 가지는 좌표계는 총 4가지가 있습니다. (그림4 참고)

이제 이 정보들을 상위 -> 하위 비트 순으로 아래와 같이 저장합니다.
12 bit : ‘Y’ 정수 부분
3 bit : ‘Y’ 소수 부분
12 bit : ‘X’ 정수 부분
3 bit : ‘X’ 소수 부분
2 bit : 좌표계 인덱스 (0~3)

이렇게 비트를 구성하는 이유는 UInt32 로 데이터 형식을 바꾸더라도, UInt32 의 비교 연산자를 그대로 사용하여 Offset의 크기 차이를 계산할 수 있게 하기 위함입니다. 비트의 최상단 y 컴포넌트를 넣어주는데, y 컴포넌트는 항상 x 컴포넌트보다 더 큰 값을 가지고 있기 때문에 비트의 상단에 배치하여 비교 연산 시 더 큰 영향력을 미치도록 해줍니다. 이런 방법으로 Intermediate buffer의 데이터를 min 연산자를 통해 바로 최소 Offset 값을 얻어낼 수 있습니다.
2.2.4. Pixel-projected reflection의 artifacts 와 해결책
2.2.4.1. Hole
원본 이미지가 반사되면서 늘어나게 될 수 있습니다. 이런 이유로 ‘Intermediate buffer’에서 일부 데이터를 잃어버리면서 Hole이 생길 수 있습니다. Hole을 매워주기 위해서 이웃 픽셀들 중 가장 적은 Offset을 가지는 이웃을 현재 픽셀에 적용시킵니다. 이렇게 Hole을 매워주는 것을 Hole patching이라 부릅니다.
2.2.4.2. Distortion
Hole patching으로 인해 반사된 결과에 살짝 왜곡이 보일 수 있습니다. 왜냐하면 강제로 이웃의 반사 데이터를 사용했기 때문입니다. 이 문제를 해결하기 위해서 Filtering을 통해서 완화합니다. Filtering 방식은 특별한 것이 아니라 반사되는 컬러를 SceneColor 텍스쳐에서 얻어올 때, 좌표의 소수 부분도 적용하여 주는 것으로 처리합니다. 즉, 텍스쳐 샘플링 시 Linear SampleState를 사용한 채로 (XX.3, XX.5)를 사용하여 픽셀 간 Linear interpolation을 유도한다는 의미입니다. 필터링되지 않은 픽셀을 사용하는 경우는 Offset의 소수 부분은 버리고 텍스쳐 샘플링 방식도 Nearest SampleState에서 (XX, YY)를 사용합니다.
2.2.4.3. Color bleeding reduction
특정 픽셀의 컬러가 이웃 픽셀과 너무 큰 차이를 보이는 경우를 완화해줍니다. 필터링된 그리고 필터링되지 않은 SceneColor를 보간하는 형태로 이 문제를 해결합니다. hue와 luminance를 별도로 분리하여 처리한다고 합니다만, 레퍼런스1에서 제공하는 코드를 보면 이 Color bleeding 문제는 추가 연구가 필요하다고 합니다. 또한 이 글의 핵심 주제에서 조금 벗어나는 내용이라 추후에 기회가 되면 더 자세히 알아보기로 하겠습니다.

2.2.5. NormalMap의 적용
Pixel-projected reflection의 경우 반사평면을 기준으로 월드좌표를 Mirrored 반사시켜 반사되는 결과를 얻습니다. 그래서 항상 평면이 매끈한 형태라고 가정합니다(그림2 참고). 이런 이유로 NormalMap을 적용하기 위해서 Reflection pass에 추가 구현을 넣어 NormalMap을 적용합니다. 구현 순서는 아래와 같습니다.
1). 카메라에서 반사평면까지의 반직선과 반사평면의 교점을 찾음. 이 평면 위의 교점을 IntersectionPoint라 둡니다.
2). 기존에 사용하던 (매끈한 노멀) 기준으로 했을 때 물체 위에서 반사 위치를 얻어냅니다. 이 좌표를 월드좌표로 저장하며 ReflectionPos로 둡니다.
3). ReflectionPos와 IntersectionPoint 사이의 거리를 ReflectionDistance라 둡니다.
4). NormalMap에서 현재 처리 중이 픽셀의 Normal을 얻어오고, 이 Normal로 카메라에서 반사평면까지의 반직선을 반사시킵니다. 이렇게 새로 구한 Reflection 벡터를 NewReflectionVector라 둡니다.
5). 이제 새로운 반사위치를 구합니다. 새로운 반사위치는 기존 반사위치와 길이가 같다고 가정하고, 아래의 식으로 월드 좌표 기준 물체 위에서 반사되는 새 위치를 얻습니다.
vec3 NewReflectionPosition = IntersectionPoint + normalize(NewReflectionVector) * ReflectionDistance
6). 5)에서 새로 구한 월드좌표를 ViewProjectionMatrix를 사용하여 NDC 공간으로, 그리고 텍스쳐 공간으로 이동시킵니다.
7). 6)에서 구한 텍스쳐 공간 좌표를 사용하여 SceneColor Fetch 하여 반사평면에 적용합니다.
3. 실제 구현
실제 구현의 설명에서는 PPR의 이해에 집중하기 위해서 Offset을 Encoding / Decoding 하는 부분을 제외한 핵심 코드의 흐름만 설명하려 합니다. 코드 기반은 레퍼런스1에서 제공되는 코드를 기반으로 적절히 수정하였습니다.
또한 구현을 간단히 하기 위해서, Sponza Scene의 바닥이 월드 좌표 기준으로 Plane(0, 1, 0, 0)에 위치하기 때문에 이 Plane으로 Projection pass에서 Intermediate buffer를 구성하였습니다.
PPR의 구현은 총 3개의 패스로 아래와 같이 구성했습니다. 이 패스를 기준으로 코드를 따라가 봅시다.
1). ClearImmediateBuffer
2). ProjectionPass
3). ReflectionPass
3.1. ClearImmediateBuffer
이 패스에서는 단순히 IntermediateBuffer를 Clear합니다.
uniform uint ClearValue;
void main()
{
ivec2 VPos = ivec2(gl_GlobalInvocationID.xy); // Position in screen space
imageStore(ImmediateBufferImage, VPos, uvec4(ClearValue));
}
3.2. ProjectionPass
Projection Pass를 다시 한번 정리하면 아래와 같습니다.
WorldPosition을 GBuffer로부터 얻어와서 반사 대상이 되는 Plane을 기준으로 Mirror 시킵니다(그림2 참고). 그리고 Mirror 결과가 만약 Plane을 위쪽이 되어버리면 Reflection 되는 결과가 아니므로 무시합니다.
이어서 Mirror 된 WorldPosition의 결과는 NDC space -> Texture space 으로 변환됩니다. 그래서 Offset은 Texture space에서 바로 쓸 수 있는 상태로 준비합니다.
여기서 구한 Offset은 Intermediate buffer에 기록합니다. 레퍼런스2에 따르면 SSBO(DirectX의 UAV와 동일)의 경우 버퍼 사이즈가 커질수록 성능이 저하될 수 있다고 하여 Texture를 사용하여 Intermediate buffer 구성하도록 하는 것이 좋다고 합니다. 그래서 저도 같은 방식을 사용했습니다. 이 Intermediate buffer의 텍스쳐 포맷은 UNSIGNED_INT(UInt32) 입니다.
Offset 값을 Intermediate buffer에 저장할 때는 여러 위치에서 반사된 결과가 같은 픽셀에 저장될 수 있습니다. 그렇기 때문에 이 중 한 개의 데이터만 저장해야 합니다. 이때 픽셀들 중 가장 작은 Offset을 가진 결과가 반사평면에서 가장 가까운 위치에서 반사된 픽셀 값을 담고 있는 Offset 일 것입니다. 그래서 가장 작은 값의 Offset을 저장하기로 합니다. GPU에서 이런 작업이 병렬 처리되기 때문에 imageAtomicMin 연산으로 각 스레드를 동기화하면서 가장 작은 값을 저장합니다.
...
// 'intermediate buffer'로 프로젝션된 결과를 기록합니다.
// 'originalPixelVpos' 로 부터 'mirroredWorldPos'로 픽셀을 프로젝션 시킴.
// Shape의 주어진 위치에 프로젝션된 픽셀이 다른 Shape에 의해 가려지지 않는 것을 보장한 뒤에 'projection pass'에서 이 함수가 호출됩니다.
void PPR_ProjectionPassWrite(ivec2 originalPixelVpos, vec3 mirroredWorldPos)
{
vec4 projPosOrig = WorldToScreen * vec4(mirroredWorldPos, 1);
vec4 projPos = projPosOrig / projPosOrig.w;
bool AllComponentLessThanOne = (abs(projPos.x) < 1.0) && (abs(projPos.y) < 1.0);
if (AllComponentLessThanOne)
{
vec2 targetCrd = (projPos.xy * vec2(0.5, 0.5) + 0.5) * ScreenSize;
vec2 offset = targetCrd - (originalPixelVpos + 0.5);
uint originalValue = uint(0);
uint valueToWrite = PPR_EncodeIntermediateBufferValue(offset);
imageAtomicMin(ImmediateBufferImage, ivec2(int(targetCrd.x), int(targetCrd.y)), valueToWrite);
}
}
void main()
{
ivec2 VPos = ivec2(gl_GlobalInvocationID.xy); // Position in screen space
vec3 WorldPos = imageLoad(WorldPosImage, VPos).xyz;
float dist = dot(Plane.xyz, WorldPos) - Plane.w;
// Upper-side of the plane is skip
if (dist < 0.0)
return;
vec3 MirroredWorldPos = WorldPos + 2.0 * dist * (-Plane.xyz);
PPR_ProjectionPassWrite(VPos, MirroredWorldPos);
}
3.3. Reflection Pass
Reflection Pass의 경우 Intermediate buffer의 Offset을 읽어와서 해당 위치에 Color을 Fetch 후 현재 픽셀의 Color에 반영하는 것이 가장 중요합니다. 그리고 PPR의 결점을 보완하기 위한 Hole patching 과 Hole patching으로 발생하는 artifacts를 완화하는 Filtering과 Color bleeding을 차례로 처리합니다. 만약 NormalMap을 사용해야 한다면, Color를 Fetch 하기 전 GetAppliedNormalMap 함수를 호출하여 Offset을 보정해줍니다.
bool IntersectionPlaneAndRay(out vec3 OutIntersectPoint, vec4 InPlane, vec3 InRayOrigin, vec3 InRayDir)
{
vec3 normal = InPlane.xyz;
float dist = InPlane.w;
float t = 0.0;
vec3 IntersectPoint = vec3(0.0, 0.0, 0.0);
float temp = dot(normal, InRayDir);
if (abs(temp) > 0.0001)
{
t = (dist - dot(normal, InRayOrigin)) / temp;
if ((0.0 <= t) && (1.0 >= t))
{
OutIntersectPoint = InRayOrigin + InRayDir * t;
return true;
}
}
return false;
}
// NormalMap을 적용
vec2 GetAppliedNormalMap(vec2 InScreenCoord)
{
ivec2 vpos = ivec2(gl_GlobalInvocationID.xy); // Position in screen space
// 현재 픽셀의 WorldPosition을 얻음
vec3 WorldPos = texture(PosSampler, vec2(vpos.x, vpos.y) / (ScreenSize)).xyz;
// CameraWorldPosition에서 현재 픽셀의 WorldPosition으로의 반직선을 구함
vec3 Origin = CameraWorldPos;
vec3 Dir = (WorldPos - CameraWorldPos);
// 반사평면과 반직선과의 충돌점을 구함
vec3 IntersectPoint = vec3(0.0, 0.0, 0.0);
if (IntersectionPlaneAndRay(IntersectPoint, Plane, Origin, Dir))
{
// 기존 반사되는 픽셀의 WorldPosition을 구함
vec3 ReflectedWorldPos = texture(PosSampler, vec2(InScreenCoord.x, InScreenCoord.y) / (ScreenSize)).xyz;
// 노멀맵으로 부터 노멀을 얻음
vec3 normal = normalize(texture(NormalSampler, vec2(vpos.x, vpos.y) / (ScreenSize)).xyz);
// ReflectedWorldPosition과 반사평면 위의 충돌점 사이의 거리를 구함
float reflectedDistance = distance(ReflectedWorldPos, IntersectPoint);
// 노멀맵에서 가져온 노멀을 기준으로, 반직선을 Reflection 시킨 방향에 reflectedDistance를 적용하여 새 ReflectionVector를 구함
vec3 newRelfectionVector = Dir + 2 * dot(normal, -Dir) * normal;
newRelfectionVector = normalize(newRelfectionVector);
// 새로구한 ReflectionVector를 NDC space -> Texture space -> Screen space로 변환시켜 값을 반환
vec4 ProjectedNewReflectingPos = WorldToScreen * vec4(WorldPos + newRelfectionVector * reflectedDistance, 1.0);
ProjectedNewReflectingPos /= ProjectedNewReflectingPos.w;
vec2 ProjectedUV = (ProjectedNewReflectingPos.xy + vec2(1.0)) * 0.5;
return (ProjectedUV * ScreenSize);
}
return InScreenCoord;
}
// 'Reflection pass' 구현.
vec4 PPR_ReflectionPass(
const ivec2 vpos, const bool enableHolesFilling, const bool enableFiltering,
const bool applyNormalMap, const bool enableFilterBleedingReduction
)
{
ivec2 vposread = vpos;
// Hole filling 수행
// Hole filling을 수행하면, Hole을 채울 근처에 있는 픽셀을 찾습니다.
// 이것을 하기 위해서 간단히 변수를 조작하여 컴퓨터 쉐이더의 결과가 이웃과 유사하게 할 것입니다.
vec2 holesOffset = vec2(0.0);
if (enableHolesFilling)
{
uint v0 = imageLoad(ImmediateBufferImage, vpos).x;
{
// 'intermediate buffer' 에서 이웃 데이터를 읽음.
const ivec2 holeOffset1 = ivec2(1, 0);
const ivec2 holeOffset2 = ivec2(0, 1);
const ivec2 holeOffset3 = ivec2(1, 1);
const ivec2 holeOffset4 = ivec2(-1, 0);
uint v1 = imageLoad(ImmediateBufferImage, ivec2(vpos.x + holeOffset1.x, vpos.y + holeOffset1.y)).x;
uint v2 = imageLoad(ImmediateBufferImage, ivec2(vpos.x + holeOffset2.x, vpos.y + holeOffset2.y)).x;
uint v3 = imageLoad(ImmediateBufferImage, ivec2(vpos.x + holeOffset3.x, vpos.y + holeOffset3.y)).x;
uint v4 = imageLoad(ImmediateBufferImage, ivec2(vpos.x + holeOffset4.x, vpos.y + holeOffset4.y)).x;
// 반사거리가 가장 가까운 이웃을 얻음
uint minv = min(min(min(v0, v1), min(v2, v3)), v4);
// 'intermediate buffer'에서 현재 픽셀에 데이터가 없다고 판단되거나 이웃이 현재 픽셀의 반사보다 더 아주 더 큰 값을 가지고 있으면 Hole fill을 시작
bool allowHoleFill = true;
if (uint(PPR_CLEAR_VALUE) != v0)
{
// 현재 위치의 Intermediate 데이터를 얻음. Offset과 Offset의 좌표계 (이 좌표계는 Offset 의 방향중 더 큰곳을 Y축으로 잡는 좌표계로 총 4가지가 있음)
vec2 d0_filtered_whole; // Offset 정수 부분
vec2 d0_filtered_fract; // Offset 소수 부분
mat2 d0_packingBasis; // 사용하는 좌표 축
// 이웃중 가장 가까운 이웃의 Intermediate 데이터를 얻음.
vec2 dmin_filtered_whole; // Offset 정수 부분
vec2 dmin_filtered_fract; // Offset 소수 부분
mat2 dmin_packingBasis; // 사용하는 좌표 축
PPR_DecodeIntermediateBufferValue(v0, d0_filtered_whole, d0_filtered_fract, d0_packingBasis);
PPR_DecodeIntermediateBufferValue(minv, dmin_filtered_whole, dmin_filtered_fract, dmin_packingBasis);
vec2 d0_offset = d0_packingBasis * (d0_filtered_whole + d0_filtered_fract);
vec2 dmin_offset = dmin_packingBasis * (dmin_filtered_whole + dmin_filtered_fract);
// 현재 위치와 반사거리가 가장 가까운 이웃의 Offset 을 복원 함
vec2 diff = d0_offset - dmin_offset;
// 이웃과 나의 Offset 차이가 minDist 거리보다 작다면, Hole fill을 하지 않음.
const float minDist = 6;
allowHoleFill = dot(diff, diff) > minDist * minDist;
}
// hole fill allowed, so apply selected neighbor's parameters
// Hole fill이 허용되어 이웃의 파라메터가 선택되어짐
if (allowHoleFill)
{
if (minv == v1) vposread = vpos + holeOffset1;
if (minv == v2) vposread = vpos + holeOffset2;
if (minv == v3) vposread = vpos + holeOffset3;
if (minv == v4) vposread = vpos + holeOffset4;
// Hole offset 은 화면 공간에서 Hole fill 된 픽셀과 원래 현재 픽셀 위치의 Offset.
holesOffset = vposread - vpos;
}
}
}
// 필터링된 혹은 필터링 되지 않은 샘플의 Offset를 얻음.
vec2 predictedDist = vec2(0.0);
vec2 predictedDistUnfiltered = vec2(0.0);
{
uint v0 = imageLoad(ImmediateBufferImage, vposread).x;
// 현재 위치의 'intermediate buffer' 데이터를 얻음. (Hole fill을 적용된 현재위치)
vec2 decodedWhole;
vec2 decodedFract;
mat2 decodedPackingBasis;
{
PPR_DecodeIntermediateBufferValue(v0, decodedWhole, decodedFract, decodedPackingBasis);
predictedDist = decodedPackingBasis * (decodedWhole + decodedFract);
// 축 정렬이 되지 않은 Offset의 경우는 이웃 픽셀을 샘플링 할 수 있기 때문에 소수 부분은 필터링 되지 않은 샘플을 위해 무시함.
// -> 축 적렬이 되지 않은 Offset이란 말이 무슨 뜻이냐?
// -> 여튼 predictedDistUnfiltered 이것은 소수부가 없는 Offset임.
predictedDistUnfiltered = decodedPackingBasis * decodedWhole;
}
// predicted offsets에 hole offset이 포함됨
if (uint(PPR_CLEAR_VALUE) != v0)
{
// Hole fill을 해서, 이웃 픽셀의 반사 정보를 가져온 경우.
// 이웃 픽셀 기준 반사가 아닌 현재 픽셀을 기준으로 반사 정보를 얻기 위해서 holeOffset 만큼 이동시킴.
vec2 dir = normalize(predictedDist);
predictedDistUnfiltered -= vec2(holesOffset.x, holesOffset.y);
predictedDist -= 2 * dir * dot(dir, holesOffset);
}
}
bool AllComponentZero = (predictedDist.x == 0.0) && (predictedDist.y == 0.0);
if (AllComponentZero)
{
return vec4(0.0);
}
// 필터링 된 것과 필터링 되지 않은 컬러를 샘플링함
// sample filtered and non-filtered color
vec2 targetCrd = vpos + 0.5 - predictedDist;
vec2 targetCrdUnfiltered = vpos + 0.5 - predictedDistUnfiltered;
// 노멀맵 적용
// Apply NormalMap
if (applyNormalMap)
{
targetCrd = GetAppliedNormalMap(targetCrd);
targetCrdUnfiltered = GetAppliedNormalMap(targetCrdUnfiltered);
}
vec3 colorFiltered = texture(SceneColorLinearSampler, targetCrd * (1.0 / ScreenSize)).xyz;
vec3 colorUnfiltered = texture(SceneColorPointSampler, targetCrdUnfiltered * (1.0 / ScreenSize)).xyz;
// 필터링 된것과 필터링 되지 않은 컬러를 조합
vec3 colorResult;
if (enableFiltering)
{
if (enableFilterBleedingReduction)
colorResult = PPR_FixColorBleeding(colorFiltered, colorUnfiltered);
else
colorResult = colorFiltered;
}
else
{
colorResult = colorUnfiltered;
}
return vec4(colorResult, 1);
}
float GetReflectionMask(vec2 uv)
{
return texture(NormalSampler, uv).w;
}
void main()
{
ivec2 VPos = ivec2(gl_GlobalInvocationID.xy); // Position in screen space
vec2 UV = vec2(VPos) * (1.0 / ScreenSize);
float reflectionMask = GetReflectionMask(UV); // should be fetched from texture
vec4 result = vec4(0.0);
if (reflectionMask != 0.0)
result = PPR_ReflectionPass(VPos, true, true, (DebugWithNormalMap != 0), false);
// Add SceneColor to result
if (DebugReflectionOnly == 0)
result += texture(SceneColorPointSampler, UV);
imageStore(ResultImage, VPos, result);
}
4. 구현 결과




5. 실제 구현 코드
github.com/scahp/Shadows/tree/sponza
6. 레퍼런스
1. advances.realtimerendering.com/s2017/PixelProjectedReflectionsAC_v_1.92.pdf
2. github.com/byumjin/Jin-Engine-2.1
'Graphics > Graphics' 카테고리의 다른 글
| [PBR] Substance/Roughness/Metalic (0) | 2021.05.03 |
|---|---|
| Atmospheric Shadowing (0) | 2021.04.14 |
| Progressive Refinement Radiosity (0) | 2021.01.12 |
| Diffuse IrradianceMap과 Spherical harmonics를 통한 최적화 (0) | 2020.12.12 |
| SphereMap TwoMirrorBall - 360도 방향 커버 됨 (0) | 2020.12.11 |