Post Lists

2018년 8월 14일 화요일

Advanced Lighting - Point Shadows (4)

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

Point Shadows
지난 튜토리얼에서, 우리는 shadow mapping으로 dynamic shadows를 만드는 것을 배웠었다. 그것은 훌륭히 작동하지만, 그것은 오직 directional lights에만 적절하다. 그 그림자들이 오직 light source의 한 방향으로만 생성되기 때문이다. 그러므로 그것은 또한 directional shadow mapping으로서 알려져 있다. depth (or shadow) map이 빛이 바라보고 있는 방향으로만 생성되기 때문이다.

이 튜토리얼에서 집중할 것은 모든 주변 방향에서의 dynamic shadows의 생성이다. 우리가 사용할 기법은 point lights에는 완벽하다. 왜냐하면 real point light는 모든 방향으로 그림자를 던지기 때문이다. 이 기법은 point (light) shadows 또는 좀 더 공식적으로 omnidirectional shadow maps라고 알려져있다.

  Green Box
  이 튜토리얼은 이전의 shadow mapping tutorial을 기반으로 구성된다. 그래서 만약 너는 전통적 shadow mapping에 친숙하지 않다면, shadow mapping 튜토리얼을 먼저 읽는 것이 충고된다.

그 알고리즘은 대개 directional shadow mapping과 같게 남아있다: 우리는 light의 perspective(s)로부터 depth map을 생성하고, 현재 fragment position을 기반으로 depth map을 샘플링하고, 그림자 안에 있는지 보기 위해 각 fragment를 그 저장된 depth value와 비교한다.  directional shadow mapping과 omnidirectional shadow mapping 사이의 주된 차이는 사용되는 depth map이다.

우리가 필요한 depth map은 한 point light의 모든 주변 방향으로부터의 scene을 렌더링 하는 것을 요구한다. 그리고 그러한 normal 2D depth map은 작동하지 않을 것이다; 우리가 대신에 cubemap을 사용한다면 어떨까? cubemap은 오직 6개의 면을 가진 environment data를 저장할 수 있기 때문에, 한 큐브맵의 면들 각각에 전체 scene을 렌더링하고 이러한 것들을 point light의 주변 depth values로 샘플링 하는 것이 가능하다:

그 생성된 depth cubemap은 그러고나서 그 fragment에서의 (light의 관점으로부터의) depth를 얻기위해 cubemap을 방향벡터를 가지고 샘플링하는 lighting fragment shader로 넘겨진다. 우리가 이미 shadow mapping tutorial에서 대부분의 복잡한 일들은 이야기 했다. 이 알고리즘을 다소 좀 더 어렵게 하는것은 depth cubemap 생성이다.

Generating the depth cubemap
light의 주변 depth values의 큐브맵을 만들기 위해서, 우리는 scene을 6번 렌더링 해야만 한다 : 각 면에 대해 한 번씩. 이것을 하는 한 가지 (꽤 명백한) 방식은 6개의 다른 view matrices를 가지고 6번 그 scene을 렌더링 하는 것이다. 그리고 매번 다른 cubemap face를 framebuffer object에 붙인다. 이것은 이것처럼 보인다:


for(unsigned int i = 0; i < 6; ++i)
{
     GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
     glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, face, depthCubemap, 0);
     BindViewMatrix(lightViewMatrices[i]);
     RenderScene();
}

이것은 꽤 비싼 연산일 수도 있다. 많은 render calls이 single depth map에 필수적이기 때문이다. 이 튜토리얼에서, 우리는 대안의 (좀 더 잘 구성된) 접근법을 사용할 것이다. 기하 쉐이더에서 작은 트릭을 사용할 건데, 이것은 우리가 한 번의 render pass로 depth cubemap을 구성하게 해준다.

처음에, 우리는 cubemap을 만들 필요가 있다:

unsigned int depthCubemap;
glGenTextures(1, &depthCubemap);

그리고 2D depth_valued texture images로서 single cubemap faces들 각각을 생성한다.


const unsigned int POINT_SHADOW_WIDTH = 1024, POINT_SHADOW_HEIGHT = 1024;
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
for (unsigned int i = 0; i < 6; ++i)
 glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT,
  POINT_SHADOW_WIDTH, POINT_SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);

또한 적절한 텍스쳐 파라미터들을 설정하는 것을 잊지말아라:


glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

보통, 우리는 한 cubemap texture의 하나의 face를 framebuffer object에 attach하고 6번 scene을 렌더링한다. 매번 framebuffer의 depth buffer target을 다른 cubemap face로 바꾸어서. 우리는 sigle pass로 모든 faces들에 렌더링 하게 해주는 geomatry shader를 사용할 것이기 때문에, 우리는 glFramebufferTexture를 사용해서 직접적으로 cubemap을 framebuffer의 depth attachment로서 attach할 수 있다.


glBindFramebuffer(GL_FRAMEBUFFER, pointShadowFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

또 다시에, glDrawBuffer and glReadBuffer를 호출에 주목해라: depth cubemap을 생성할 때 우리는 오직 depth values만을 신경쓴다. 그래서 우리는 OpenGL에게 명시적으로 이 프레임버퍼 오브젝트가 컬러 버퍼에 렌더링하지 않는다고 말해야만 한다.

omnidirectional shadow maps와 함꼐, 우리는 두 개의 render passes를 가진다: 처음에 depth map을 생성하고, 둘 째로 우리는 scene에서 그림자들을 만들기 위해 일반적인 render pass에서 depth map을 사용한다. framebuffer object와 cubemap으로, 이 과정은 이것처럼 보인다:


glViewport(0, 0, POINT_SHADOW_WIDTH, POINT_SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, pointShadowFBO);
glClear(GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
DrawScene(depthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEGIHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCREEN_WIDTH / (float)SCREEN_HEGIHT, 0.1f, 100.f);
shadowShader.use();
shadowShader.setMat4("projection", projection);
shadowShader.setMat4("view", view);
shadowShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);
shadowShader.setVec3("viewPos", camera.Position);
shadowShader.setVec3("lightPos", lightPos);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
DrawScene(shadowShader);

그 과정은 기본 shadow mapping과 정확히 같다, 비록 이번에 2D depth texture와 비교하여 우리는 cubemap depth texture에 렌더링하고 사용할지라도. 우리는 실제로 모든 light의 viewing directions로부터 scene을 렌더링 하기전에, 우리는 처음에 적절한 transformation matrices를 계산해야만 한ㄷ.

Light space transform
framebuffer와 cubemap 세트와 함께, 우리는 모든 scene의 geometry를 빛의 모든 6 방향에 있는 관련된 light spaces로 바꾸는 몇 가지 방식이 필요하다. shadow mapping tutorial과 비슷하게 우리는 light space transformation matrix가 필요할 것이지만, 각 face에 대해 한 번에 하나씩 필요하다.

각 light space transformation matrix는 두 개의 projection과 view matrix를 갖는다. projection matrix에 대해 우리는 perspective projection matrix를 사용할 것이다; 광원이 공간에서 point를 나타낸다. 그래서 perspective projection은 가장 말이 된다. 각 light space transformation matrix는 같은 projection matrix를 사용한다:


float aspect = (float)POINT_SHADOW_WIDTH / (float)POINT_SHADOW_HEIGHT;
float nearP = 1.f, farP = 25.f;
glm::mat4 PoinwshadowProj = glm::perspective(glm::radians(90.f), aspect, nearP, farP);

glm::perspective의 우리가 90도로 설정한 field of view parameter가 여기에서 주목해야할 중요한 것이다. 이것을 90도로 설정하여, 우리는 viewing field가 정확히 cubemap의 단일 face를 적절히 채울만큼 크도록 한다. 이것은 모든 faces가 edges에서 서로에게 정확히 정렬하도록 하기 위해서이다.

projection matrix는 방향마다 변하지 않기 때문에, 우리는 각 6개의 transformation matrices에 대해 그것을 재사용할 수 있다. 우리는 방향마다 다른 view matrix가 필요하다. glm::lookAt으로 우리는 6개의 view directions를 만든다. 그리고 그 각각은 cubemap에서 다음의 순서로 하나의 방향을 바라본다: right, left, top, bottom, near and far.

glm::vec3 pointLightPos = glm::vec3(-5.0f, 3.5f, 0.0f);
std::vector<glm::mat4> shadowTransforms;
shadowTransforms.push_back(PoinwshadowProj *
 glm::lookAt(pointLightPos, pointLightPos + glm::vec3(1.f, 0.f, 0.f), glm::vec3(0.f, -1.f, 0.0f)));
shadowTransforms.push_back(PoinwshadowProj *
 glm::lookAt(pointLightPos, pointLightPos + glm::vec3(-1.f, 0.f, 0.f), glm::vec3(0.f, -1.f, 0.0f)));
shadowTransforms.push_back(PoinwshadowProj *
 glm::lookAt(pointLightPos, pointLightPos + glm::vec3(0.f, 1.f, 0.f), glm::vec3(0.f, 0.f, 1.0f)));
shadowTransforms.push_back(PoinwshadowProj *
 glm::lookAt(pointLightPos, pointLightPos + glm::vec3(0.f, -1.f, 0.f), glm::vec3(0.f, 0.f, -1.f)));
shadowTransforms.push_back(PoinwshadowProj *
 glm::lookAt(pointLightPos, pointLightPos + glm::vec3(0.f, 0.f, 1.f), glm::vec3(0.f, -1.f, 0.0f)));
shadowTransforms.push_back(PoinwshadowProj *
 glm::lookAt(pointLightPos, pointLightPos + glm::vec3(0.f, 0.f, -1.f), glm::vec3(0.f, -1.f, 0.0f)));

여기에서 우리는 6개의 view matrices를 만들고 그것들을 projection matrix와 곱해 총 6개의 다른 light space transformation matrices를 얻는다. glm::lookat의 target parameter는 각각 signle cubemap face의 방향으로 바라본다.

이러한 transformation matrices는 depth를 cubemap에 렌더링할 쉐이더들에 보내진다.

Depth shaders
depth values를 depth cubemap에 렌더링하기 위해, 우리는 총 세 개의 쉐이더들을 필요할 것이다: vertex and fragment shader 그리고 그 사이에 있는 geometry shader.

geometry shader는 모든 world-space 정점들을 6개의 다른 light spaces로 바꾸는 역할을 하는 쉐이더가 될 것이다. 그러므로, 그 vertex shader는 간단히 vertices를 world-space로 변환하고 그것들을 geometry shader로 곧장 보낸다:


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

uniform mat4 model;

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

그 기하 쉐이더는 그러고나서 입력으로서 3개의 삼각형 정점들과 light space transformation matrices의 uniform array를 취한다. 그 기하 쉐이더는 그러고나서 그 정점들을 light space로 변환하는 역할을 한다; 이것이 또한 흥미로워지는 부분이다.

기하 쉐이더는 gl_Layer라는 내장 변수를 가지고 있다. 그것은 어떤 cubemap face를 primitive를 방출할지를 명시한다. 그냥 두었을 때, 기하 쉐이더는 그냥 그것의 primitives를 그 파이프라인 아래로 보통 보내지만, 우리가 이 변수를 업데이트 할 때, 우리는 우리가 각 primitive에 대해 어떤 cubemap을 렌더링할지에 대한 통제할 수 있게 된다. 이것은 물론 오직 우리가 활성화된 framebuffer에 attached된 cubemap texture가지고 있을 때에만 작동한다.


#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform mat4 shadowMatrices[6];

out vec4 FragPos; // FragPos from GS (output per emitvertex)

void main()
{
 for(int face = 0; face < 6; ++face)
 {
  gl_Layer = face; // built-in variable that specifies to which face we render.
  for(int i = 0; i < 3; ++i)
  {
   FragPos = gl_in[i].gl_Position;
   gl_Position = shadowMatrices[face] * FragPos;
   EmitVertex();
  }
  EndPrimitive();
 }
}

이 기하 쉐이더는 상대적으로 간단할 것이다. 우리는 입력으로서 한 삼각형을 받고 총 6개의 삼각형 정점들을 output 한다 (6 * 3은 18개의 정점과 같다). main 함수에서, 우리는 6개의 cubemap faces에 대해 반복하는데, 거기에서 우리는 face integer를 gl_Layer에 저장하여 각 face를 output face로 명시한다. 그러고나서 우리는 FragPos에 그 face의 light-space transformation matrix를 곱하여 각 world-space vertex를 관련된 light space로 변환시켜 각 삼각형을 생성한다. 우리가 또한 최종 FragPos 변수를 depth value를 계산하기 위해 필요할 fragment shader에 보냈다는 것에 주목해라.

지난 튜토리얼에서 우리는 텅빈 fragment shader를 사용했고, OpenGL이 depth map의 depth values를 알아내라고 했다. 이번에, 우리는 각 fragment position과 light source의 position 사이의 선형 거리로서 우리 스스로 (linear) depth를 계산할 것이다. 우리의 depth values를 계산하는 것은 나중의 shadow calculations을 좀 더 직관적으로 만들어준다.


#version 330 core

in vec4 FragPos;

uniform vec3 lightPos;
uniform float far_plane;

void main()
{
 // get distance between fragment and light source
 float lightDistance = length(FragPos.xyz - lightPos);

 // map to [0, 1] range by dividing by far_plane
 lightDistance = lightDistance / far_plane;

 // write this as modified depth
 gl_FragDepth = lightDistance;
}

그 fragment shader는 입력으로서 기하 쉐이더로부터 FragPos를 받고, light의 position vector와 frustum의 far plane value를 받는다. 여기에서 우리는 fragment와 light source 사이의 거리를 얻고, 그것을 [0,1] range로 매핑하고 그리고 그것을 fragment의 depth value에 써넣는다.

scene을 이러한 쉐이더와 cubemap이 attached된 framebuffer object로 렌더링하는 것은 너에게 second pass의 shadow calculations를 위한 완전히 채워진 depth cubemap을 줄 것이다.

Omnidirectional shadow maps
모든 것이 설정되고, 실제 omnidirectional shadows를 렌더링할 시간이다. 절차는 directional shadow mapping tutorial과 유사하다. 비록 이번에 우리는 depth map으로서 2D texture 대신에 cubemap texture를 바인드하고 또한 light projection의 far plane 변수를 쉐이더에 보낼지라도.


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

여기에서 renderScene(내꺼에선 DrawScene) 함수는 하나의 큰 cube room에 scene의 중ㅇ심에 있는 광원 주위에 흩뿌려진 몇 가지 큐브들을 렌더링한다.

vertex shader와 fragment shader는 크게 원래의 shadow mapping shaders와 비슷하다: fragment shader는 더 이상 light space에서의 fragment position을 요구하지 않는다. 왜냐하면 우리가 이제 depth values를 direction vector를 사용하여 샘플할 수 있기 때문이다. (cubemap을 사용하기 때문에)

이 것 때문에, vertex shader는 더 이상 그것의 position vectors를 light space로 변환 시킬 필요가 없다. 그래서 우리는 그 FragPosLightSpace 변수를 제외할 수 있다.

그 fragment shader의 Blinn-Phong lighting code는 정확히 우리가 이전에 shadow multiplication과 가졌던 것과 같다:

몇 가지 미묘한 차이들이 있다: lighting code는 같지만, 우리는 이제 samplerCube uniform을 가지고 있고, ShadowCalculation 함수는 fragment의 position을 그것의 인자로서 받는다. light space에 있는 fragment position 대신에. 우리는 또한 나중에 필요할 light frustum의 far_plane value를 포함한다. fragment shader 작동의 끝에서, 우리는 fragment가 그림자에 있으면 1.0이고, 그렇지 않으면 0.0인 shadow component를 계산한다. 우리는 lighting의 diffuse와 specular componets에 영향을 주기 위해 계싼된 shadow component를 사용한다.

크게 다른 것은 이제 2D texture 대신에 cubemap으로부터 depth values를 샘플링하는 ShadowCalculation 함수의 내용이다. 그것의 내용을 차근차근 이야기해보자.

우리가 해야 할 첫 번째 것은 cubemap의 depth 를 가져오는 것이다. 너가 이 튜토리얼의 cubemap section으로부터 기억할지도 모르는 것은 우리가 depth를 fragment와 light position 사이의 linear distance로서 저장했다는 것이다; 우리는 여기에서 비슷한 접근을 한다:


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

여기에서 fragment 위치와 light 위치 사이의 차이를 가지고, 그 벡터를 cubemap을 샘플링하기위해 방향벡터로서 사용한다. 그 방향벡터는 cubemap으로부터 샘플링하기 위해 단위 벡터일 필요가 없다. 그래서 그것을 normalize할 필요가 없다. 그 최종 closestDepth는 light source와 그것의 가장 가까운 visible fragment 사이의 표준화된 depth value이다.

closestDepth value는 현재 [0,1]의 범위에 있다. 그래서 우리는 그것을 다시 [0, far_plane]의 범위로 변환시킨다. 그것에 far_plane을 곱해서.

  closestDepth *= far_plane;

다음에 우리는 현재 fragment와 light source사이의 depth value를 가져온다. 우리는 cubemap에서의 우리가 depth values를 계산했던 방식 떄문에 fragtoLight 의 길이를 취하여 쉽게 가져올 수 있다.

  float currentDepth = length(fragToLight);

이것은 closestDepth와 같은 (또는 더 큰) 범위의 depth value를 반환한다.

이제 우리는 어떤 것이 더 가까운지 보기위해 두 depth values를 비교할 수 있다. 그리고 현재 fragment가 그림자에 있는지를 결정한다. 우리는 또한 shadow bias를 포함한다. 그래서 우리는 이전 튜토리얼에서 이야기했던 shadow acne를 얻지 않는다.

그 완전한 ShadowCalculation은 그러면 이렇게 된다:


float ShadowCalculation(vec3 fragPos)
{
 // get vector between fragement position and light position
 // actually the variable name should be lightToFrag
 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);

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

 return shadow;
}

이러한 쉐이더들로, 우리는 이미 꽤 좋은 그림자를 얻는다. 이번에는 point light로부터 모두 둘러싸인 방향에서. pointer light가 간단한 scene의 중앙에 위치해있어서 그것은 이것처럼 보인다:


너는 여기에서 이 데모의 source code를 볼 수 있다.

Visualizing cubemap depth buffer
만약 너가 어느정도 나와 같다면, 너는 첫 번째 시도에 이것을 하지 못했을 것이다. 그래서 depth map이 정확히 구성되었는지의 유효성을 체크하는 것이 있는 디버깅을 하는 것이 도움이 된다.  왜냐하면 우리는 2D depth map texture를 더 이상 가지고 있지 않기 때문에, dpeth map을 visualizing하는 것은 덜 명백하다.

depth buffer를 visualize하는 간단한 트릭은 ShadowCalculation function에 있는 표준화된 ([0,1]의 범위의) closestDepth variable을 취해서 그 변수를 보여주는 것이다:

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

그 결과는 각 컬러가 scene의 linear depth를 나타내는 회색의 scene이다.

너는 또한 바깥의 벽에서 그림자가 있는 영역을 볼 수 있다. 만약 그것이 어느정도 유사하다면, 너는 depth cubemap을 적절히 생성되었다고 알 수 있다. 그렇지 않다면 너는 어떤 것을 틀리게 했거나 또는 [0, far_plane] range에 있는 closestDepth를 사용한 것이다.

PCF
omnidirection shadow maps은 전통적인 shadow mapping의 같은 원칙에 기반하기 때문에, 그것은 또한 같은 해상도 의존 artifacts를 갖는다. 만약 너가 가까이 zoom in 한다면, 너는 또 다시 지그재그의 edges를 볼 수 있다. Percentage-closer filtering or PCF는 우리가 이러한 지그재그의 edges를 fragment position 주변의 다양한 샘플들을 필터링 하여 부드럽게 하고 그 결과를 평균화하게 한다.

만약 우리가 이전 튜토리얼의 같은 simple PCF filter를 취하고 샘플링할 3차원 벡터 (우리는 3D 방향이 필요하기 때문에)를 추가한다면, 우리는 다음을 얻는다:

그 코드는 우리가 전통적인 shadow mapping에서 가졌던 것과 너무 다르지 않다. 여기에서 우리는 우리가 각 축에서 취하고 싶은 texture offset을 samples의 개수를 기반으로 동적으로 계산한다. 그리고 우리가 끝에서 평균화할 sub-samples의 개수로 3배의 sample을 취한다.

그 그림자는 이제 좀 더 부드럽고 좀 더 그럴듯한 결과를 준다.


그러나, samples이 4.0으로 설정되어 있어서, 각 fragment 당 총 64개의 samples를 취한다. 이것은 너무 많다!

샘플들이 원래의 방향벡터에 너무 가깝게 샘플한다는 점에서 이러한 대부분의 샘플들은 중복이기 때문에, 샘플 방향벡터의 수직 방향에서 샘플하는 것이 좀 더 그럴듯 할 것이다. 그러나, 어떤 sub-directions이 중복인지 알아내는 (쉬운) 방법이 없기 때문에, 이것은 어렵다. 우리가 사용할 수 있는 한 가지 트릭은 대강 분리할 수 있는 offset diretions의 한 배열을 취하는 것이다. 예를들어, 그것들 각각은 완전히 다른 방향을 가리킨다. 이것은 함꼐 가까운 sub-directions의 개수를 줄인다. 아래에서 우리는 최대 20개의 offset directions의 배열을 갖는다:


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)
);   

그러고나서 우리는 sampleOffsetDirections로부터 고정된 양의 샘플들을 취하는 PCF 알고리즘을 적용할 수 있다. 그리고 cubemap을 샘플링하기 위해 이러한 것들을 사용할 수 있다. 이점은 우리가 처음의 PCF와 시각적으로 유사한 결과를 얻기위해 더 적은 샘플들이 필요하다는 것이다.


float shadow = 0.0;
float bias = 0.15;
int samples = 20;
float offset = 0.1;
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 direction vector 주변의 특정한 diskRadius에 offset을 추가한다.

우리가 여기에 적용할 수 있는 또 다른 흥미로운 트릭은 우리가 viewer가 fragment에 얼마나 멀리 있는냐를 기반으로 diskRadius를 바꿀 수 있다; 이 방식으로 우리는 viewer의 거리에 따라 offset radius를 증가시킬 수 있다. 이것은 그림자들이 멀리 있을 때 더 부드러워지고 가까이 있을 때 더 날카롭게 만든다.

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

이 PCF 알고리즘의 결과는 부드러운 그림자들의 좋은 결과를 준다:


물론 우리가 각 sample에 추가하는 bias는 상황에 기반하고 항상 너가 작업하는 scene을 기반으로 조절하느 ㄴ것을 요구한다. 모든 값들을 가지고 놀고, 그것들이 scene에 어떻게 영향을 미치는지 보아라.

너는 여기에서 최종 코드를 볼 수 있다.

나는 depth map을 생성하기 위해 기하 쉐이더를 사용하는 것이 각 face에 대해 6번 scene을 렌더링하는 것보다 필수적으로 더 빠르지 않다고 언급해야만 한다. 이 처럼 기하 쉐이더를 가지는 것은 처음에 하나를 사용하는 것의 성능 이득을 능가할지도 모르는 performance penalties를 가진다. 이것은 물론 환경의 type, 특정 비디오 카드 드라이버에 의존한다. 그래서 만약 너가 성능에 정말 신경쓴다면, 두 방식들 profile하려고 하고, 너의 scene에 맞는 좀 더 효율적인 select를 골라라.ㅏ 나는 개인적으로 shadow mapping에 대해 기하쉐이더를 사용하는 것을 선호한다. 왜냐하면 나는 그것들이 사용하기 더 직관적이기 때문이다.

===================================
점점 어려워지고, 깊은 내용을 들어갈수록 경험에 기반하는 코드들이 많이 나온다.
이러한 것들을 이해하면 좋지만, 어렵다면 외우기라도 해야 한다.


댓글 없음:

댓글 쓰기