Post Lists

2018년 8월 13일 월요일

Advanced Lighting - Shadow Mapping (3)

https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

Shadow Mapping
그림자들은 occlusion 때문에 빛의 부재의 결과이다; 한 광원의 광선이 어떤 다른 오브젝트에 의해 가려저서 한 오브젝트에 미치지 못할 때, 그 오브젝트는 그림자에 있게 된다. 그림자들은 lighted scene에 많은 현실성을 더하고, 보는 사람이 오브젝트 사이의 공간 관계를 관찰하는데 쉽게 해준다. 그것들은 우리의 scene과 object에 depth에 대한 더욱 좋은 감각을 준다. 예를들어, 그림자가 있고 없는 scene의 다음의 이미지를 봐보자:

너는 그림자가 있는 것과 함꼐, 오브젝트들이 서로 어떤 관계에 있는지가 좀 더 명백해지는 것을 볼 수 있다. 예를들어, 그 정육면체 들 중 하나가 다른 것들 위에 떠 있다는 사실은 우리가 그림자를 가질 때 좀 더 눈치 챌수 있다.

그림자들은 그런데 구현하기에 조금 까다롭다. 특히 현재 real-time research에서, 완벽한 그림자 알고리즘은 개발되지 않았기 때문이다. 몇 가지 좋은 그림자 근사 기법들이 있지만, 그것들은 모두 우리가 고려해야만 하는 그것들의 작은 특이점과 성가심이 있다.

멋진 결과를 주고 상대적으로 구현하기에 쉬운 대부분의 게임들에서 사용되는 한 가지 기법은 shadow mapping을 구현하는 것이다. Shadow mapping은 이해하기에 너무 어렵지 않고, 성능에서 너무 많은 비용을 쓰지 않으며, 꽤 쉽게 좀 더 고급 알고리즘들로 확장되어질 수 있다. (like Omnidirectonal Shadow Maps and Cascaded Shadow Maps).

Shadow mapping
shadow mapping 뒤에 있는 아이디어는 꽤 간단하다: 우리는 light의 관점으로부터 scene을 렌더링하고, 우리가 light의 perspective로부터 보는 모든 것을 비춰지고, 우리가 볼 수 없는 모든 것은 그림자에 있어야만 한다. 바닥과 광원 사이에 있는 큰 박스가 있는 바닥을 생각해보자. 광원은 그것의 방향대로 볼 때 이 박스를 볼 것이고 바닥 부분은 보지 않을 것이기 때문에, 그 특정한 바닥 부분은 그림자에 있어야 한다.

여기에서 모든 파란색 선들은 광원이 볼 수 있는 fragments를 나타낸다. 가려진(occluded) fragments들은 검정색 선으로서 보여진다: 이러한 것들은 그림자 지어지는 것으로서 렌더링 되어진다. 만약 우리가 광원으로부터 가장 오른쪽에 있는 박스에 있는 fragment로 선 또는 광선을 그리려고 한다면, 우리는 그 광선이 처음에 가장 오른쪽 박스에 닿기전에 떠 있는 컨테이너를 치는 것을 볼 수 있다. 결과적으로, 그 떠있는 컨테이너의 fragment가 비춰지고, 가장 오른쪽 컨테이너의 fragment는 비추어지지 않고, 따라서 그림자에 있다.

우리는 광선에서 한 점을 얻기를 원한다. 그리고 거기에서 그것은 한 오브젝트에 닿고, 이 가장 가까운 점을 이 광선에 있는 다른 점과 비교한다. 우리는 그러고나서 한 test point의 ray position이 가장 가까운 점보다 더 아래에 있는지 보기 위해 기본적인 검사를 한다. 만약 그렇다면, 그 test point는 그림자에 있어야만 한다. 그러한 광원으로부터 가능하게 수천번의 광선을 통해 반복하는 것은 매우 비효율적인 접근법이고, real-time rendering에 매우 좋지 않다. 우리는 비슷한 것을 할 수 있다, 그러나 광선을 던지지 않고서. 대신에, 우리는 depth buffer와 꽤 비슷한 어떤 것을 사용한다.

너는 아마 depth testing tutorial로부터 depth buffer에 있는 한 값이 카메라의 관점으로부터 [0,1]로 clamped 된 한 fragment의 depth와 대응된다는 것을 기억한다. 만약 우리가 light의 관점으로부터 scene을 렌더링 하고, 한 텍스쳐에 있는 최종 depth values를 저장한다면 어떨까? 이러한 방식으로, 우리는 light의 관점으로 부터 보여질 수 있는 가장 가까운 depth values를 샘플링 할 수 있다. 결국, 그 depth values는 light의 관점으로부터 보이는 첫 번째 fragment를 보여준다. 우리는 모든 이러한 depth values를 우리가 depth map or shadow map이라고 부르는 한 텍스쳐에 저장한다.

왼쪽의 이미지는 directional light source를 보여준다. (모든 광선은 평행하다) 그 광원은 정육면체 아래에 있는 표면에그림자를 던진다. depth map에 저장된 depth values를 사용하여, 우리는 가장 가까운 점을 찾고, fragments가 그림자에 있는지 결정하기 위해 그것을 사용한다. 우리는 그 광원에 특정한 view and projection matrix를 사용하여  (빛의 관점으로부터) scene을 렌더링하여 depth map을 만들어낸다. 이 projection과 view matrix는 함께 어떤 3D 위치를 그 빛의 보이는 좌표 공간으로 변환하는 transformation T를 형성한다.

  Green Box
  directional light는 위치를 갖지 않는다. 그것이 무한히 멀다고 모델링 되었기 때문이다. 그러나, shadow mapping의 목적을 위해서, 우리는 그 scene을 빛의 관점으로부터 렌더링할 필요가 있고, 따라서 어느정도 빛의 방향의 선을 따라서 한 위치로부터 그 scene을 렌더링 할 필요가 있다.

오른쪽 이미지에서, 우리는 같은 directional light와 viewer를 본다. 우리는 점 P에 있는 한 fragment를 렌더링 하는데, 거기에서, 우리는 그것이 그림자 안에 있는지를 결정해야만 한다. 이것을 하기 위해서, 우리는 T를 사용해서 처음에 점 P를 그 빛의 coordinate space로 변환한다. 점 P는 이제 빛의 관점으로부터 보여지는 것이기 때문에, 그것의 z 좌표는 그것의 depth와 일치하고, 이 예제에서는 0.9이다. 점 P를 사용하여, 우리는 또한 빛의 관점으로부터 가장 가까운 보이는 depth인 0.4의 sampled depth를 가진 점 C를 얻기 위해 depth map을 인덱싱 할 수 있다. depth map을 인덱싱하는 것은 점 P에 있는 depth 보다 더 작은 depth를 반환하기 때문에, 우리는 점 P가 가려져있고, 따라서 그림자에 있다고 결론지을 수 있다.

Shadow mapping은 두 가지 passes로 구성되어있다: 처음에 우리는 depth map을 렌더링하고, 두 번째 pass에서 우리는 scene을 normal로서 렌더링하고, fragments가 shadow에 있는지 계산하기 위해 그 생성된 depth map을 사용한다. 이것은 조금 복잡하게 들릴지도 모르지만, 우리가 단계별로 그 기법을 나아가자마자, 이해가 되기 시작할 것이다.

The depth map
첫 번째 pass는 우리가 depth map을 생성하는것을 요구한다. 그 depth map은 light의 관점으로 부터 렌더링 되어질, 그림자를 계산하기 위해서 사용할 depth texture이다. 우리가 한 scene의 렌더링된 결과를 한 텍스쳐에 저장할 필요가 있기 때문에, 우리는 framebuffers를 다시 필요할 것이다.

처음에 우리는 그 depth map을 렌더링하기 위해 한 framebuffer object를 만들 것이다:


unsigned int depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

다음으로, 우리는 frambueer의 depth buffer로서 사용할 2D texture를만든다:


const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
unsigned int depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

depth map을생성하는 것은 복잡해 보이지 않을 것이다. 우리는 오직 depth values를 신경쓰기 때문에, 우리는 그 텍스쳐의 format을 GL_DEPTH_COMPONENT로 명시한다. 우리는 또한 그 텍스쳐에 1024의 너비와 높이를 준다: 이것은 depth map의 해상도이다.

생성된 depth texture와 함께, 우리는 그것을 그 프레임 버퍼의 depth buffer에 attach한다:


glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

우리는 빛의 관점으로부터 scene을 렌덜이할 때 depth information만이 필요하다. 그래서 color buffer가 필요없다. 그러나 한 framebuffer object는 color buffer 없이는 완전하지 않다. 그래서우리는 명시적으로 OpenGL에게 우리가 어떤 컬러 데이터도 렌더링하지 않을 것이라고 말할 필요가 있다. 우리는 glDrawBuffer와 glReadBuffer로 read and draw buffer 둘 다를 GL_NONE으로 설정하여 이것을 한다.

DEPTH VALUES를 한 텍스쳐에 렌더링하는 적절히 설정된 프레임버퍼와 함께, 우리는 그 첫 번째 PASS를 시작할 수 있다: depth map을 생성하는 것. 두 passes의 완전한 렌더링 단계는 이것처럼 보일 것이다:


// 1. first render to depth map
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
    glClear(GL_DEPTH_BUFFER_BIT);
    ConfigureShaderAndMatrices();
    RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. then render scene as normal with shadow mapping (using depth map)
glViewport(0, 0, SCREEN_WDITH, SCREEN_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene();

이 코드는 몇 가지 세부사항을제외했지만,그것은 너에게 shadow mapping의 일반적인 아이디어를 줄 것이다. 여기에서 주목해야 할 중요한 것은 glViewport 호출이다. shadow maps가 종종 우리가 실제로 scene을 렌더링하는 것과 (보통 window resolution) 비교해서 다른 해상도를 갖기 때문에, 우리는 그 shadow map의 크기를 수용하기위해 viewport parameters를 바꿀 필요가 있다. 만약 우리가 viewport parameters를 업데이트 하는 것을 잊는다면, 그 최종 depth map은 불완전하거나 또는 너무 작을 것이다.

Light space transform
ConfigureShaderAndMatrices 함수는 이전 코드에서 안알려진 것이다. 그 두 번째 pass에서, 이것이 보통 일이다: 적절한 projection과 view matrices가 설정되고 오브젝트마다 관련된 model matrices가 설정되도록 해라. 그러나, first pass에서, 우리는 light의 관점으로 부터 scene을 렌더링 하기 위해 다른 projection과 view matrix를 사용한다.

directional light source를 모델링 할 것이기때문에, 모든 그것의 광선들은 평행하다. 이러한 이유 때문에, 우리는 light source에 대해 orthographic projection을 사용할 것이다. 거기에서는 어떠한 perspective 변형이 없다:


float near_plane = 1.f, far_plane = 7.5f;
glm::mat4 lightProjection = glm::ortho(-10.f, 10.f, -10.f, 10.f, near_plane, far_plane);

여기에 이 튜토리얼의 데모 scene에서 사용될 예제 orthographic projection matrix의 예가 있다. projection matrix는 간접적으로 보이는 것의 (clipped 되지 않을 것의) 범위를 결정하기 때문에, 너는 그 projection frustum의 크기가 정화히 너가 depth map에 있길 원하는 오브젝트를 포함하도록 하길 원한다. 오브젝트들 또는 fragments가 depth map에있지않을 때, 그것들은 그림자들을 만들어내지 않을 것이다.

빛의 관점으로부터 오브젝트들이 보이는지 확인하도록, 각 오브젝트를 transform할 view matrix를 만들기 위해서, 우리는 그 악명높은 glm::lookAt 함수를 사용할 것이다; 이번에는 scene의 주심을 바라보는 light source의 위치와 함께:


glm::mat4 lightView = glm::lookAt(glm::vec3(-2.0f, 4.0f, -1.0f), 
                                  glm::vec3( 0.0f, 0.0f,  0.0f), 
                                  glm::vec3( 0.0f, 1.0f,  0.0f));  

이러한 두 가지를 합치는 것은 우리에게 각 world-space vector를 광원으로부터 보이는 space로 변환시키는 light space transformation matrix를 준다; 이것은 정확히 depth map을 렌더링하기 위해 필요한 것이다.

  glm::mat4 lightSpaceMatrix = lightProjection * lightView;

그 lightSpaceMatrix는 이전에 T로 표기했던 transformation matrix이다. 이 행렬로, 우리는 shader에게 projection and view matrices의 light-space equivalents를 주는 한, 보통 scene을 렌더링 할 수 있다. 그러나, 우리는 오직 depth values만을신경쓰고, 우리의 main shader에서 비싼 fragment calculations을 하지 않는다. 성능을 절약하기 위해서, 우리는 depth map을 렌더링 하기 위해 다르지만, 꽤 간단한 쉐이더를 쓸 것이다.

Render to depth map
우리가 빛의 관점으로부터 scene을 렌더링 할 때, 우리는 오히려 정점들을 light space로 바꾸는 더욱 간단한 쉐이더를 사용할 것이고, 더 많은 것을 사용하지 않을 것이다. simpleDepthShader라고 불리는 그런 간단한 쉐이더에 대해, 우리는 다음의 vertex shader를 사용할 것이다:


#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main()
{
 gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}

이 버텍스 쉐이더는 오브젝트마다 model, 한 정점을취하고, 모든 정점을 lightSpaceMatrix를 사용하여 light spsace로 변환한다.

우리는 어떠한 color buffer도 가지고 있지 않기 때문에,최종 fragments는 어떠한 처리도 요구하지 않는다. 그래서 우리는 간단히 텅빈 fragment shader를 사용할 수 있다:


#version 330 core

void main()
{
        // gl_FragDepth = gl_FragCoord.z;
}

그 텅 빈 쉐이더는 어떤 연산이라도 하지 않고, 그것의 작동 끝에, depth buffer가 업데이트 된다. 우리는 explicitly하게 그것의 one line으로 주석을 제외하여 depth를 설정할 수 있지만, 이것은 어쨋든 효과적으로 scene뒤에서 발생하는 것이다.

그 depth buffer를 렌더링하는 것은 이제 효과적으로 다음과 같이 된다:

===================================================
Advanced Lighting + PBR을 다 끝낼 때 까지 이제 나의 클래스를 확장하지 않는다. 그러면 학습 기간이 길어지게 되므로, 일단은 배우고나서 한다. 클래스가 그러한 기능들에 extensible하게 하도록 만들 것이다. 그래서, 개념을 정확히 배우고, 어떻게 그것을 구현할지 배우고, 공부하면서는 나중에 어떻게 확장할 것인지 고민만 해본다.
PBR까지 끝내게 된다면, 나머지 In practice part를 천천히 공부하면서, 각 기능들을 게임에 적용할 수 있도록 확장시켜놓는다. 사실, 한 번에 확장하는 것보다는 게임 개발하면서 하는게 좋을 것 같다. 왜냐하면 아직 3D 게임을 제대로 개발해보지 않았기 때문에, 확장할 때 어떤 문제에 봉착할지 모르기 때문이다. 또한 3D object가 많아질수록, 그것을 file로 처리해서, file로 읽어들여서 그러한 것들을 편하게 사용할 수 있는 구조로 만들어야 할텐데.

일단은 확실히 학습하고 extensible한 클래스를 만들 수 있도록 코드를 짜나간다.
===================================================

여기에서 RenderScene 함수는 쉐이더 프로그램을 가지고, 모든 관련된 함수들을 호출하고 필요하다면 그에 상응하는 모델 행렬을 설정한다.

그 결과는 light의 관점으로부터 각 보이는 fragment의 가장 가까운 depth를 지닌 멋지게 채워진 depth buffer이다. 이 텍스쳐를 스크린을 채우는 2D quad사영하여 (framebuffers 튜토리얼의 끝에서 후처리 부분에서 우리가 했던 것과 비슷한), 우리는 이것과 같은 것을 얻는다:


depth map을 quad에 렌더링하기 위해, 우리는 다음의 fragment shader를 사용한다:


#version 330 core

out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D screenTexture;

void main()
{
 float depthValue = texture(screenTexture, TexCoords).r;
 FragColor = vec4(vec3(depthValue), 1.0);
}

depth로서 orthographic projection matrix 대신에 perspective projection을 사용한 depth를 보여줄 때, 미묘한 변화가 있다는 것에 주목해라. 그 차이는 perspective projetion을 사용할 때 non-linear이다.

너는 scene에 depth map을 렌더링하기 위한 소스코드를 여기에서 볼 수 있다.

Rendering shadows
적절히 생성된 depth map을 가지고, 우리는 실제 그림자들을 만들어내기 시작할 수 있다. 한 fragment가 shadow인지 체크하는 코드는 (꽤 명백히) fragment shader에서 실행되어질 것이다, 그러나 우리는 vertex shader에서 light-space transformation을 한다:


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

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

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

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

여기에서 새로운 것은 추가 output vector FragPosLightSpace이다. 우리는 같은 lightSpaceMatrix (정점들을 depth map stage에 있는 light space로 변환하기위해 사용되는)를 취하고, world-space vertex position을 light space로 변환한다. 그 vertex shader는 일반적ㄱ으로 변환된 world-space vertex position vs_out.FragPos를 넘기고, light-spcae로 변환된 vs_out.FragPosLightSpace를 fragment shader로 넘긴다.

scene을 렌더링하기 위해 우리가 사용할 fragment shader는 Blinn-Phong lighting model을 사용한ㄷ. fragment shader내에서, 우리는 그러고나서 그 fragment가 그림자안에 있으면 1.0이고 또는 그림자에 있지 않으면 0.0인 shadow value를 계산한다. 그 최종 diffuse and specular colors는 그러고나서 이 그림자 요소에 의해 곱해진다. 그림자들은 light scaterring 때문에 결코 완전히 어둡지 않기 때문에, 우리는 shadow multiplications에서 ambient color를 제외한다.


#version 330 core
out vec4 FragColor;

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

uniform sampler2D diffuseTexture;
uniform sampler2D shadowMap;

uniform vec3 lightPos;
uniform vec3 viewPos;

float ShadowCalculation(vec4 fragPosLightSpace)
{
 //
}

void main()
{
 vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
 vec3 normal = normalize(fs_in.Normal);
 vec3 lightColor = vec3(1.0);

 // ambient
 vec3 ambient = 0.15 * color;
 // diffuse
 vec3 lightDir = normalize(lightPos - fs_in.FragPos);
 float diff = max(dot(lightDir, normal), 0.0);
 vec3 diffuse = diff * lightColor;
 // specular with blinn-phong
 vec3 viewDir = normalize(viewPos - fs_in.FagPos);
 vec3 halfwayDir = normalize(lightDir + viewDir);
 float spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
 vec3 specular = spec * lightColor;

 // calculate shadow
 float shadow = ShadowCalculation(fs_in.FragPosLightSpace);
 vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;

 FragColor = vec4(lighting, 1.0);
}

fragment shader는 advanced lighting tutorial에서 우리가 사용한 것의 복사본이지만, 추가 shadow calculation이 있다. 우리는 대부분의 shadow work를 하는 ShadowCalculation 함수를 선언했다. fragment shader의 끝에, 우리는 diffuse and specular contribution에 shadow component의 역을 곱한다. 예를들어, 그 fragment가 그림자안에 있는지 아닌지를. 이 fragment shader는 추가 입력으로 light-space fragment position과 첫 번째 render pass로부터 생성된 depth map을 받는다.

fragment가 그림자안에 있는지 확인하기 위해 해야할 첫 번째 것은, clip space에 있는 light-space fragment position을 NDC로 바꾸는 것이다. 우리가 clip-space vertex position을 vertex shader에 있는 gl_Position으로 output할 때, OpenGL은 자동적으로 perspective divide를 한다. 예를들어, clip-space 좌표 범위[-w, w]에서 [-1, 1]로 바꾼다. x,y,z 컴포넌트를 벡터의 w 컴포넌트로 나누어서. clip-space FragPosLightSpace는 gl_Position을 통해 fragment shader에 넘겨지지 않기 때문에, 우리는 스스로 이 perspective divide를 해야만 한다:


float ShadowCalculation(vec4 fragPosLightSpace)
{
 // perform perspective divide
 vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
}

이것은 fragment의 light-space 위치를 [-1,1]의 범위를 반환한다.

  Green Box
  orthographic projection matrix를 사용할 때, vertex의 w 컴포넌트는 건드려지지 않는다. 그래서 이 단계는 꽤 의미없다. 그러나, perspective projection을 할 때 필요하다. 그래서 이 line을 유지하는 것은 두 projection matrices를 가진 작업을 보장해준다.

depth map으로부터의 depth는 [0,1]의 범위이고, 우리는 depth map으로부터 sample하기 위해 projCoords를 사용하기 원하기 때문에, 그래서 우리는 NDC좌표를 [0,1]의 범위로 바꾼다:

  projCoords = projCoords * 0.5 + 0.5;

이 사영된 좌표로, 우리는 depth map을 샘플링 할 수 있다. projCoords의 최종 [0,1] 좌표가 직접적으로 첫 번째 render pass의 변환된 NDC 좌표와 동일하기 때문이다. 이것은 우리에게 그 빛의 관점으로부터 가장 가까운 depth를 준다:

  float closestDepth = texture(shadowMap, projCoords.xy).r;

fragment에서 현재 깊이를 얻기위해서, 우리는 간단히 light의 관점에서 fragment의 깊이와 동일한 그 사영된 벡터의 z좌표를 얻는다.

  float currentDepth = projCoords.z;

실제 비교는 그러고나서 currentDepth가 closestDepth보다 더 높은지를 비교하는 것이다. 그리고 만약 그렇다면, 그 fragment는 shadow에 있는 것이다.

  float shadow = currentDepth > closestDepth ? 1.0 : 0.0;

그 완전한 ShadowCalculation 함수는 그런 다음에 이렇게 된다:


float ShadowCalculation(vec4 fragPosLightSpace)
{
 // perform perspective divide
 vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
 // transform to [0,1] range
 projCoords = projCoords * 0.5 + 0.5;
 // get closest depth value from light's perspective (using [0,1] range fragPosLight as coords)
 float closestDepth = texture(shadowMap, projCoords.xy).r;
 // get depth of current fragment from light's perspective
 float currentDepth = projCoords.z;
 float shadow = currentDepth > closestDepth ? 1.f : 0.f;

 return shadow;
}

두 번째 render pass에서 이 쉐이더를 활성화하고, 적절한 텍스쳐를 바인드시키고, 기본 projection과 view matrices를 활성화하는 것은 너에게 아래의 이미지와 비슷한 결과를 줄 것이다:


만약 너가 옳게 했다면, 너는 정말로 (비록 몇 가지 artifacts가 있지만) 바닥과 큐브에 그림자들을 볼 것이다. 너는 데모 프로그램의 소스코드를 여기에서 볼 수 있다.

Improving shadow maps
우리는 쉐도우 매핑의 기본을 간신히 작동시켰지만, 너도 볼 수 있뜻이, 우리가 더 좋은 결과를 위해 고치고 싶은 쉐도우 매핑과 관련된 몇 가지 artifacts들이 있다.

Shadow acne
이전 이미지에 무언가 문제가 있는게 분명하다. 더 가까운 줌은 우리에게 명백한 Moire-like pattern을 준다:

우리는 번갈아가면서 생기는 분명한 검은 선과함께 렌더링되는 flooar quad의 큰 부분을 볼 수 있다. 이 쉐오두 매핑 artifcat는 shadow acne라고 불려지고, 간단한 이미지에 의해 설명되어질 수 있다:

shadow map은 해상도에 의해 제한되기 때문에, 수 많은 fragments는 그것들이 상대적으로 광원으로 부터 멀리 있을 때 depth map으로부터 같은 값을 샘플링할 수 있다. 그 이미지는 바닥을 보여주는데, 거기에서 각 휘어진 panel은 depth map의 단일 texel을 나타낸다. 너도 볼 수 있듯이, 몇 가지 fragments는 같은 depth sample을 샘플링한다.

이것이 일반적으로 괜찮을 지라도, 광원이 표면을 향해 한 각도로 볼 때 문제가 된다. 그 경우에 depth map은 또한 한 각도로부터 렌더링 되어지기 때문이다. 몇가지 fragments들은 그러고나서 똑같이 굽어진 depth texel에 접근한다. 반면에 몇몇의 것은 위에 몇몇은 아래에서; 우리는 shadow discrepancy를 얻게된다. 이 때문에, 몇 가지 fragments들은 그림자에서 어두워지고, 몇몇은 그렇지 않다. 이것은 이미지로부터 줄무늬 패턴을 준다.

우리는 shadow bias라고 불려지는 작은 hack으로 이 문제를 해결할 수 있다: 우리는 간단히 surface의 depth를 작은 bias amount로 offset한다. fragments가 표면아래로 옳지 않게 고려되지 않기 위해서.

bias를 적용해서, 모든 샘플들은 surface의 depth보다 더 작은 depth를 얻는다. 그리고 따라서 전체 표면은 정확히 어떠한 그림자도 없이 밝아진다. 우리는 그러한 bias를 다음과 같이 구현할 수 있다:

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

(
이 shadow acne의 솔루션을 이해하는데 좀 오래 걸렸다. 설명이 구체적이지 않은데,
https://a.disquscdn.com/uploads/mediaembed/images/3808/4980/original.jpg?w=800&h
밑의 댓글에 누가 그려놓은 더 세부적인 이미지로 이해할 수 있었다.
첫 번째 render pass에서 shadow map을 할 떄, depth value의 precision문제로 지그재그의 울퉁불퉁한 depth value가 생성된다. 일직선이 될 수 없다.
거기다가, 두 번쨰 render pass에서 shadow calculation을 할 때의 fragment의 depth위치도 마찬가지인데, 이 때, fragment의 depth 위치를 좀 더 가깝게 조정하여 문제를 해결하고자 하는 것이다.
)

0.005의 shadow bias는 우리의 scene의 문제를 크게 해결하지만, 광원에 대해 가파른 각도를 가진 몇 가지 표면들은 여전히 shadow acne를 만들지도 모른다. 좀 더 튼튼한 접근법은 light를 향하는 surface angle을 기반으로 bias의 양을 바꾸는 것이다: 우리는 내적으로 해결할 수 있는 것이다:

  float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

여기에서 우리는 최대 bias로 표면의 법선과 빛의 방향을 기반으로 0.05를 최소로 0.005를 갖는다. (내적을하면 음수값도 나오지 않는가?) 이 방식으로 광원에 거의 수직인 바닥과 같은 표면들은 작은 bias를 갖는다. 반면에, 정육면체의 옆면과 같은 표면들은 더욱 큰 bias를 갖는다. 다음의 이미지는 같은 scene을 보여주지만, shadow bias를 가진 것을 보여준다 : 훨씬더 좋은 결과이다.


올바른 bias 값을 고르는 것은 각 scene마다 달라서 조정하는게 요구하지만, 대부분의 모든 acne가 없어질 때 까지 bias를 증가시켜주는 문제이다.

Peter panning
shadow bias를 사용하는 것의 불이익은 너가 오브젝트들의 실제 depth에 offset을 적용하는 것이다. 결과적으로 그 bias는 실제 오브젝트 위치에 비교하여 그림자들의 눈에 보이느 offset을 볼만큼 충분히 클지도 모른다. 너가 아래에서 보듯이 (너무 증가된 bias valeu):

이 shadow artifact는 peter panning이라고 부릴는데, 오브젝트들이 다소 그것들이ㅡ 그림자들과 떨어져있는 것처럼 보이기 때문이다. 우리는 depth map을 렌더링할 때, front face culling을 사용하여 peter panning issue의 대부분을 해결하기 위해 작은 트릭을 사용할 수 있다. 너는 face culling tutorial로 부터 OpenGL이 디폴트로 back-faces를 깍는 것을 기억할지도 모른다. OpenGL에게 우리가 frontfaces를 cull하기 원한다고 말하며, 우리는 그 순서를 바꿀 것이다. 왜냐하면 우리는 오직 depth map에 대한 depth values만을 필요로 하기 때문에, 그것들의 앞면 또는 뒷면의 depth를 취하는 것이 중요하지 않다. 그것들의 back face depths를 이용하는 것은 잘못된 결과를 주는 것이 아니다. 만약 우리가 오브젝트 안에 그림자를 갖는지 안갖는지는 중요하지 않기 때문이다; 우리가 거기를 어쨋든 볼 수 없기 때문에

대개 peter panning을 고치기 위해서, 우리는 front faces를 cull한다. 너는 처음에 GL_CULL_FACE를 활성화 할 필요가 있다는 것에 주목해라

  glCullFace(GL_FRONT);
  RenderSceneToDepthMap();
  glCullFace(GL_BACK); // don't forget to reset original culling face

이것은 효과적으로 peter panning issues를 해결하지만, 개방된 것이 없는 내부를 가진 solid objects에게만 당된다. 예를들어, 우리의 scene에서, 이것은 큐브들에 대해 정확히 좋게 작동하지만, 바닥에서는 작동하지 않을 것이다. 왜냐하면 front face를 culling하는 것은 완전히 그 방정식에서 바닥을 없애기 때문이다. 그 바닥은 단일 면이고 그리고 따라서 완전히 culled 될 것이다. 만약 어떤 이가 이 trick care를 가지고 peter panning을 해결하려 한다면, 오브젝트들의 앞면만을 cull하는데 취해져야만 한다.

또 다른 고려할 것은 shadow receiver (먼 거리의 큐브 같은)에 가까운 오브젝트들이 부정확한 결과를 준다는 것이다. 오브젝트들에 대해 front face culling을 하는 것은 신중히 취해져야만 한다. 그러나, normal bias values로 어떤이는 신중하게 peter panning을 피할 수 있다.

Over sampling
또 다른 너가 좋아하거나 싫어할 visual discrepancy는 빛의 visible frustum 밖의 어떤 영역이 그림자에 있는 것이다. 그것들이 실제로는 그림자 안에 있지 않으면서. 이것은 light의 frustum 밖의 사영된 좌표들이 1.0보다 더 높고, 따라서 그것의 기본 범위 [0,1]의 밖의 depth texture를 샘플링할 것이기 때문에 발생한다. texture의 wrapping method에 기반하여, 우리는 광원으로부터 실제 depth values에 기반하지 않고 정확하지 않은 depth 결과를 얻게 될것이다.

너는 이미지에서 빛의 상상의 지역이 있고, 이 지역의 밖의 큰 부분이 그림자에 있는 것을 볼 수 있다. 이 지역은 바닥에 투영된 depth map의 크기를 나타낸다. 이것이 발생하는 이유는 우리는 이전에 그 depth map의 wrapping options을 GL_REPEAT로 설정했기 때문이다.

우리가 오히려 가져야 하는 것은 depth map의 범위밖의 모든 좌표들이 1.0의 depth를 갖게하는 것이다. 이것은 결과적으로 이러한 좌표들이 결코 그림자에 있지 않는 것을 의미한다. (어떠한 오브젝트도 1.0보다 큰 depth를 가지지 않을 것이기 때문에). 우리는 bolder color를 저장하고, depth map의 texture wrap options을 GL_CLAMP_TO_BORDER로 설정하여 이것을 얻는다:


glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float borderColor[] = { 1.f, 1.f, 1.f, 1.f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

이제 우리가 depth map의 [0,1]의 좌표 범위 밖에서 sample할 때 마다, 그 texture 함수는 항상 1.0의 depth를 반환한다. 이것은 0.0의 shadow value를 만든다. 그 결과는 이제 좀 더 그럴듯해 ㅂ인다.

여전히 어두운 영역을 보여주는 한 부분이 있는 것 처럼 보인다. 그러한 것들은 빛의 orthographic frustum 밖의 좌표들이다. 너는 이 어두운 영역이 shadow 방향으로 바라보아서 항상 광원 frustum의 far end에 발생하는 것을 볼 수 있다.

한 사영된 좌표는 그것의 z좌표가 1.0보다 더 클 때 light의 far plane보다 더 멀리 있다. 그 경우에 GL_CLAMP_TO_BORDER wrapping method는 더 이상 효과가 없다. 우리는 좌표의 z component와 depth map values를 비교하기 때문이다; 이것은 항상 1.0보다 더 큰 z에 대해 항상 true를 반홚나다.

이것을 고치는 방법은 상대적으로 간단하다. 우리는 간단히 사영된 벡터의 z 좌표가 1.0보다 크다면 shadow value가 0.0이 되도록 하기 때문이다.

그 far plane을 체크하고, depth map을 수동으로 명시된 border color에 clamp 하는 것은 depth map의 over-sampling을 해결하고, 마침내 우리가 바라는 결과를 준다:


이것이 하는 결과는 우리가 그림자들을 갖는데 거기에서 사영된 fragment 좌표들이 depth map 범위에 머무르게 하는 것이다. 그래서 이 범위 밖의 것은 어떠한 보이는 그림자가 없을 것이다. 게임들이 보통 이것이 거리가 있는 곳에서만 발생하도록 하기 때문에, 우리가 이전에 가졌던 명백히 어두운 여역보다 좀 더 그럴듯한 효과이다.

PCF
그림자는 이제 scenery에 좋은 추가물이지만, 여전히 정확히 우리가 원하는 것이 아니다. 만약 그 그림자에 zoom in해본다면, shadow mapping의 resolution dependency가 빠르게 분명해진다.

depth map은 fixed resolution을 갖기 때문에, 그 depth는 빈번히 texel마다 한 fragment 이상에 걸쳐진다.  결과적으로 많은 fragments가 depth map으로부터 같은 depth value를 샘플링한다. 그리고 같은 shadow colclusions를 갖게 된다. 그리고 이것은 이러한 지그재그의 블럭 edges를 만들어 낸다.

너는 이러한 블럭 쉐도우들을 depth map resolution을 증가시키거나 light frusutm을 그 scene에 가능한한 가깝게 맞추려고해서 제거할 수 있다.

이러한 지그재그 edges의 또 다른 (부분적인) 해결책은 PCF 또는 percetange-closer filter이라고 불려진다. 이것은 더 부드러은 그림자를 만드는 많은 다른 필터링 함수들을 host하는 용어이다. 그리고 이것은 덜 blocky하고 덜 딱딱하게 나타나게 만든다. 그 아이디어는 depth map으로부터 한 번 이상 sample 하는 것이다. 매번 다소 다른 텍스쳐 좌표로. 각 개별 sample에 대해 우리는 그것이 그림자안에 있는지 아닌지를 확인한다. 모든 하위 결과들은 그러고나서 합쳐지고, 평균화된다. 그리고 우리는 좋은 부드럽게 보이는 그림자를 얻는다.

PCF의 한 가지 간단한 구현은 depth map의 주변 texels들을 샘플링하는 것이고, 그 결과를 평균화하는 것이다:


float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for (int x = -1; x <= 1; ++x)
{
 for (int y = -1; y <= 1; ++y)
 {
  float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
  shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
 }
}
shadow /= 9.f;

여기에서 textureSize는 주어진 sampler texture의 mipmap level 0에서의 너비와 높이의 vec2를 반환한다. 1로 나눠진 이것은 텍스쳐 좌표를 offset하기 위해 우리가 사용할 single texel의 크기를 반환한다. 이것은 각 새로운 샘플이 다른 depth value를 샘플링 하도록 한다. 여기에서 우리는 사영된 좌표의 x와 y값 주변의 9개의 값을 sample하고 shadow occlusion에 대해 테스트하고 마지막으로 취해진 총 샘플들의 개수로 그 결과의 평균을 구한다.

좀 더 많은 샘플들을 사용해서 그리고 texelSize 변수를 다르게 하여, 너는 soft shadows의 퀄리티를 올릴 수 있다. 아래에서 너는 간단한 PCF가 적용된 그림자를 볼 수 있다:



거리가 있는 곳에서 그림자들은 좀 더 좋아 보이고 덜 딱딱해 보인다. 만약 너가 ZOOM IN 한다면, 너는 여전히 shadow mapping의 resolution artifacts를 볼 수 있다. 그러나 일반적으로 이것은 대부분의 프로그램에 대해 좋은 결과들을 준다.

너는 여기에서 예제의 완전한 소스코드를 볼 수 있다.

실제로 PCF에 좀 더 할 것이 있고, 부드러운 그림자의 퀄리티를 상당히 개선하는 몇 가지 기법들이 있지만, 이 튜토리얼의 길이를 위해서 우리는 그것을 나중으로 미뤄둔다.

Orthographic vs projection
orthographic 또는 projection matrix로 depth map을 rendering하는 것 사이의 차이가 있다. orthographic projection matrix는 perspective가 있는 scene을 변형시키지 않는다. 그래서 모든 view/light rays는 평행하다. 이것은 directional lights에 대해 훌륭한 projection matrix를 만든다. 그러나 perspective projection matrix는 다른 결과를 주는 perspective에 기반으로 모든 정점들을 바꾼다. 다음의 이미지는 두 프로젝션 메소드의 다른 쉐도우 영역들을 보여준다.

Perspective projections는 directional lights와 달리 실제 위치를 가진 광원에 대해 말이 된다. Perspective projections는 따라서 종종 대부분 spotlights and point lights와 함께 사용된다. 반면에 orthographic projections은 directional lights를 위해서 사용된다.

perspective projection matrix를 사용한 것의 또 다른 미묘한 차이는 depth buffer를 시각화하는 것이 종종 거의 완전히 하얀색의 결과를 준다. perspective projection과 함께 depth는 near plane에 가까운 그것의 눈치챌만한 범위와 함께 비선형의 depth values값으로 변형되기 때문이다. 적절히 우리가 orthographic projection으로 했던 것처럼 depth values를 보도록 하기 위해서, 너는 처음에 그 비선형성을 선형으로 바꾸길 원한다. 우리가 depth testing tutorial에서 이야기 했던 것 처럼.


#version 330 core

out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D screenTexture;
uniform float near_plane;
uniform float far_plane;

float LinearizeDepth(float depth)
{
 float z = depth * 2.0 - 1.0; // Back to NDC
 return (2.0 * near)plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}

void main()
{
 float depthValue = texture(screenTexture, TexCoords).r;
 FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective
 // FragColor = vec4(vec3(depthValue), 1.0); // orthographic
}

이것은 orthographic projection으로 우리가 보았던 것과 비슷한 depth values를 보여준다. 이것이 오직 디버깅용에만 유용하다는 것에 주목해라; depth checks는 orthographic 또는 projection 행렬과 같게 남아 있다. 상대적인 depth는 변하지 않기 때문에.

========================================
꽤 힘들었다. shadow mapping 이라는 것이 뭔가 나랑 잘 안맞는 느낌인데. 열심히해서 친숙해져야 겠다.
중요한 것은 이제 다음 파트를 읽고 나중에 application에 모든 것을 적용하는 것이다.



댓글 2개:

  1. 올리시는 짤막한 후기들 읽고 생각을 맞춰갑니당 ㅎㅎ. 거의 다 오셨네요 형은 퐈이팅 ㅜㅜ 저도 퐈이팅

    답글삭제
    답글
    1. 앞으로 할 것들이 굵직한 것들이 많아서 좀 걸릴듯.. Shadow mapping부터 많이 어려워진다. 파이팅~

      삭제