본문 바로가기

Game/Graphics

Learn OpenGL - Advanced Lighting : Point Shadows (2)

link : https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows


Omnidirectional shadow maps


 모든 것이 설정되면 실제 무지향성 그림자를 렌더링 할 때입니다. 이 과정은 방향 맵 매핑 튜토리얼과 유사하지만 이번에는


2D 텍스처 대신에 큐브 맵 텍스처를 깊이 맵으로 바인딩하고 조명 프로젝션의 원거리 평면 변수를 쉐이더에 전달한다.

glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();  
// ... send uniforms to shader (including light's far_plane value)
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// ... bind other textures
RenderScene();

 여기서 renderScene 함수는 장면 중앙의 광원 주위에 흩어져 있는 대형 큐브 룸에서 일부 큐브를 렌더링한다.



 정점 쉐이더와 조각 쉐이더는 원래의 쉐도우 맵핑 쉐이더와 대체로 유사하다. 차이점은 방향 벡터를 사용해 깊이 값을 샘플링


할 수 있기 때문에 조각 쉐이더가 더 이상 가벼운 공간에서 조각 위치를 필요로 하지 않는다는 점이다.



 이 때문에 정점 쉐이더는 더 이상 위치 벡터를 라이트 공간으로 변환 할 필요가 없으므로 FragPosLightSpace 변수를 제외할 수 있다:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

out vec2 TexCoords;

out VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
    vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
    vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;
    vs_out.TexCoords = aTexCoords;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}  

 조각 쉐이더의 Blinn-Phong 조명 코드는 이전의 그림자 곱셈과 완전히 같다:

#version 330 core
out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} fs_in;

uniform sampler2D diffuseTexture;
uniform samplerCube depthMap;

uniform vec3 lightPos;
uniform vec3 viewPos;

uniform float far_plane;

float ShadowCalculation(vec3 fragPos)
{
    [...]
}

void main()
{           
    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);
    vec3 lightColor = vec3(0.3);
    // ambient
    vec3 ambient = 0.3 * color;
    // diffuse
    vec3 lightDir = normalize(lightPos - fs_in.FragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * lightColor;
    // specular
    vec3 viewDir = normalize(viewPos - fs_in.FragPos);
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = 0.0;
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
    vec3 specular = spec * lightColor;    
    // calculate shadow
    float shadow = ShadowCalculation(fs_in.FragPos);                      
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;    
    
    FragColor = vec4(lighting, 1.0);
}  

 몇 가지 미묘한 차이점이 있다. 조명 코드는 동일하지만 samplerCube 유니폼이 있고, ShadowCalculation 함수는 밝은 공간에서 조각 위치


대신 매개 변수로 조각의 위치를 취한다. 이제 우리는 나중에 우리가 필요로 할 빛의 절두체의 far_plane 값을 포함시킨다.


조각 쉐이더가 끝날 때 조각이 쉐도우일 때 1.0이고, 그렇지 않을 때 0.0인 쉐도우 컴포넌트를 계산한다.


계산된 그림자 구성 요소를 사용해 조명의 확산 및 반사 구성 요소에 영향을 준다.



 크게 다른 점은 2D 텍스처 대신 큐브 맵에서 깊이 값을 샘플링하는 ShadowCalculation 함수의 내용이다. 내용을 단계별로 살펴보겠다.



 우리가 해야 할 첫 번째 일은 큐브 맵의 깊이를 검색하는 것이다. 이 튜토리얼의 큐브 맵 섹션에서 기억할 수 있듯이, 조각과 라이트 위치 사이의


선형 거리로 깊이를 저장했다. 비슷한 접근법을 취하고 있다:

float ShadowCalculation(vec3 fragPos)
{
    vec3 fragToLight = fragPos - lightPos; 
    float closestDepth = texture(depthMap, fragToLight).r;
}  

 여기서 우리는 조각의 위치와 빛의 위치 사이의 차이 벡터를 취해 그 벡터를 방향 벡터로 사용해서 큐브 맵을 샘플링한다.


방향 벡터는 cubemap에서 샘플링 할 단위 벡터가 아니어도 되므로 정규화 할 필요가 없다. 결과 nearestDepth는 광원과 가장 가까운


가시적인 조각 사이의 정규화된 깊이 값이다.



 closetDepth 값은 현재 [0,1] 범위에 있으므로 far_plane과 곱해 [0,far_plane]으로 다시 변환한다.

closestDepth *= far_plane;  

 다음으로 우리는 큐브 맵에서 깊이 값을 계산하는 방법 때문에 fragToLight의 길이를 취함으로써 쉽게 얻을 수 있는 현재 조각과 광원


사이의 깊이 값을 검색한다:

float currentDepth = length(fragToLight);  

 nearestDepth와 같은 범위의 깊이 값을 반환한다.



 이제 두 깊이 값을 비교해 어느 것이 더 가깝고 현재 조각이 그림자에 있는지 여부를 판단 할 수 있다. 우리는 또한 그림자 기울기를 포함하므로


이전 튜토리얼에서 설명한 것처럼 그림자 여드름이 발생하지 않는다.

float bias = 0.05; 
float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0; 

 전체 ShadowCalculation은 다음과 같다:

float ShadowCalculation(vec3 fragPos)
{
    // get vector between fragment position and light position
    vec3 fragToLight = fragPos - lightPos;
    // use the light to fragment vector to sample from the depth map    
    float closestDepth = texture(depthMap, fragToLight).r;
    // it is currently in linear range between [0,1]. Re-transform back to original value
    closestDepth *= far_plane;
    // now get current linear depth as the length between the fragment and light position
    float currentDepth = length(fragToLight);
    // now test for shadows
    float bias = 0.05; 
    float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;

    return shadow;
}  

 이 쉐이더를 사용해 이미 점 그림자로부터 모든 주변 방향으로 꽤 좋은 그림자와 시간을 얻는다. 단순한 장면의 중앙에 위치하는 점 광원을


사용하면 다음과 같이 보인다:


Omnidirectional point shadow maps in OpenGL



Visualizing cubemap depth buffer


 만약 당신이 나 같은 사람이라면 첫 번째 시도에서 이 권한을 얻지 못했을 것이다. 따라서 깊이 맵이 올바르게 구축되었는지 여부를


확인하는 명백한 검사 중 하나를 사용해 일부 디버깅을 하는 것이 좋다. 더 이상 2차원 깊이 맵 텍스처가 없기 때문에 더 이상 깊이 맵을


시각화하는 것이 조금 덜 명확해진다.



 깊이 버퍼를 시각화하는 간단한 방법은 ShadowCalculation 함수에서 approximized (범위[0,1]) closetDepth 변수를 가져와서


해당 변수를 다음과 같이 표시하는 것이다:

FragColor = vec4(vec3(closestDepth / far_plane), 1.0);  

 결과는 그레이 아웃된 장면이고 각 색은 장면의 선형 깊이 값을 나타낸다:


Visualized depth cube map of omnidrectional shadow maps


 바깥쪽 벽면에 그림자가 있는 영역을 볼 수도 있다. 모양이 다소 비슷하다면 깊이의 큐브 맵이 제대로 생성되었음을 알 수 있다.


그렇지 않으면 아마도 뭔가 잘못되었거나 [0, far_plane] 범위의 closestDepth를 사용했을 것이다.




PCF


 무지향성 쉐도우 맵은 전통적인 쉐도우 맵핑과 동일한 원칙에 기반하기 때문에 동일한 해상도 종속 아티팩트가 있다. 자세히 확대하면


들쭉날쭉한 가상자리를 다시 볼 수 있다. Percentage-closer filtering 또는 PCF를 사용하면 조각 위치 주변의 여러 샘플을 필터링하고


결과를 평균화해 이러한 들쭉날쭉한 가장자리를 부드럽게 처리 할 수 있다.



 앞의 튜토리얼과 동일한 간단한 PCF 필터를 사용하고 세 번째 차원을 추가하면 다음을 얻는다:

float shadow  = 0.0;
float bias    = 0.05; 
float samples = 4.0;
float offset  = 0.1;
for(float x = -offset; x < offset; x += offset / (samples * 0.5))
{
    for(float y = -offset; y < offset; y += offset / (samples * 0.5))
    {
        for(float z = -offset; z < offset; z += offset / (samples * 0.5))
        {
            float closestDepth = texture(depthMap, fragToLight + vec3(x, y, z)).r; 
            closestDepth *= far_plane;   // Undo mapping [0;1]
            if(currentDepth - bias > closestDepth)
                shadow += 1.0;
        }
    }
}
shadow /= (samples * samples * samples);

 이 코드는 전통적인 쉐도우 맵핑과 크게 다르지 않다. 여기서는 각 축에서 취할 샘플 수를 기반으로 텍스처 오프셋을 동적으로 계산하고


마지막에 평균을 구한 서브 샘플의 양을 3번 샘플링한다.



 그림자는 이제 훨씬 더 부드럽고 매끈해 보이며 훨씬 더 그럴듯한 결과를 제공한다.


Soft shades with omnidirectional shadow mapping in OpenGL using PCF


 그러나 샘플을 4.0으로 설정하면 각 조각들이 총 64 샘플이므로 너무 많다!



 이 샘플들의 대부분은 원래 방향 벡터에 가깝게 샘플링한다는 점에서 중복되므로 샘플 방향 벡터의 수직 방향으로


샘플링하는 것이 더 합리적일 수 있다. 그러나 어떤 하위 방향이 중복되는지 파악하기 쉬운 방법이 없기 때문에 이 작업이 어려워진다.


우리가 사용할 수 있는 한 가지 트릭은 모두 대략 분리 가능한 오프셋 방향 배열을 취하는 것이다. 각각은 완전히 다른 방향을


가리키며 서로 가깝게 있는 하위 방향의 수를 줄인다. 아래에는 최대 20개의 오프셋 방향이 있다:

vec3 sampleOffsetDirections[20] = vec3[]
(
   vec3( 1,  1,  1), vec3( 1, -1,  1), vec3(-1, -1,  1), vec3(-1,  1,  1), 
   vec3( 1,  1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1,  1, -1),
   vec3( 1,  1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1,  1,  0),
   vec3( 1,  0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1,  0, -1),
   vec3( 0,  1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0,  1, -1)
);   

 그런 다음 PCF 알고리즘을 채택해 sampleOffsetDirections에서 일정량의 샘플을 가져와서 큐브 맵을 샘플링하는데 사용할 수 있다.


이점은 첫 번째 PCF 알고리즘과 시각적으로 비슷한 결과를 얻을 때 훨씬 적은 샘플이 필요하다는 것이다.

float shadow = 0.0;
float bias   = 0.15;
int samples  = 20;
float viewDistance = length(viewPos - fragPos);
float diskRadius = 0.05;
for(int i = 0; i < samples; ++i)
{
    float closestDepth = texture(depthMap, fragToLight + sampleOffsetDirections[i] * diskRadius).r;
    closestDepth *= far_plane;   // Undo mapping [0;1]
    if(currentDepth - bias > closestDepth)
        shadow += 1.0;
}
shadow /= float(samples);  

 여기에서는 원래의 fragToLight 방향 벡터 주위의 일정 diskRadius에 오프셋을 추가해 큐브 맵에서 샘플링한다.



 여기에 적용할 수 있는 또 다른 흥미로운 트릭은 뷰어가 조각에서 얼마나 멀리 떨어져 있는지에 따라 diskRadius를 변경할 수 있다는 것이다.


이렇게하면 뷰어까지의 거리에 따라 오프셋 반경을 늘릴 수 있다. 그림자가 멀리 떨어져있을 때 부드럽고 가까이에 있을 때 날카롭게 만든다.

float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;  

 이 PCF 알고리즘의 결과는 부드러운 그림자의 결과와 마찬가지로 양호한 결과를 제공한다:


Soft shades with omnidirectional shadow mapping in OpenGL using PCF, more efficient


물론 각 샘플에 추가되는 기울기는 컨텍스트를 기반으로 하며 작업하는 장면에 따라 항상 조정해야한다. 모든 값을 가지고 놀고


장면에 어떤 영향을 주는지 보아라.