Post Lists

2019년 1월 9일 수요일

Normal/Parllax Mapping 정리 + With Self-Shadowing

- Reference
http://sunandblackcat.com/tipFullView.php?topicid=28
https://learnopengl.com/Advanced-Lighting/Parallax-Mapping

NormalMap을 사용하여 Normal Mapping. Normal Mapping만 구현한다 했을 때, normal과 tangent vector를 model space로 변환한 후에, Gram-Schmidt process를 이용하여 TBN matrix를 형성한 후, normal를 좌표변환 해주고, model space에서 lighting calculation을 하면 된다.

Parallax Mapping이라는 Texture Coordinate를 조정하는 기법을 사용하여, 보는 각도에 따라서도 depth가 있는 것처럼 구현이 됌. 그런데 함수로 들어오는 viewDir은 지금 현재 tangent space에서 계산되고 있는 것이다.


- Relief Parallax Mapping
LayerDepth보다 currentDepthMapValue가 작아지는 지점을 찾는다.
그 지점에서 부터, deltaTexCoords와 layerDepth Offset Value를 절반으로 해 나가면서
binary search를 통해 이것을 해결해 나간다.

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
vec2 ParallaxOcclusionMapping(vec2 texCoords, vec3 viewDir)
{
 // number of depth layers
 const float minLayers = 8.0;
 const float maxLayers = 32.0;
 float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
 // calculate the size of each layer
 float layerDepth = 1.0 / numLayers;
 // depth of current layer
 float currentLayerDepth = 0.0;
 // the amount to shift the texture coordinates per layer (From vector P)
 vec2 P = viewDir.xy * height_scale;
 vec2 deltaTexCoords = P / numLayers;

 // get initial values
 vec2 currentTexCoords = texCoords;
 float currentDepthMapValue = texture(depthMap, currentTexCoords).r;
 
 while(currentLayerDepth < currentDepthMapValue)
 {
  // shift texture coordinates along direction of P
  currentTexCoords -= deltaTexCoords;
  // get depthmap value at current texture coordinates
  currentDepthMapValue = texture(depthMap, currentTexCoords).r;
  // get depth of next layer
  currentLayerDepth += layerDepth;
 }

 // Relief Parallax Mapping

 // decrease shift and height of layer by half
 deltaTexCoords /= 2;
 layerDepth /= 2;

 // return to the mid point of previous layer
 currentTexCoords += deltaTexCoords;
 currentLayerDepth -= layerDepth;

 // binary search to increase precision of Steep Paralax Mapping
 const int numSearches = 5;
 for(int i = 0; i < numSearches; ++i)
 {
  // decrease shift and height of layer by half
  deltaTexCoords /= 2;
  layerDepth /=2;
  
  // new depth from heightmap
  currentDepthMapValue = texture(depthMap, currentTexCoords).r;

  // shift along or aginas vector ViewDir
  if(currentDepthMapValue > currentLayerDepth)
  {
   currentTexCoords -= deltaTexCoords;
   currentLayerDepth += layerDepth;
  }
  else
  {
   currentTexCoords += deltaTexCoords;
   currentLayerDepth -= layerDepth;
  }
 }

 return currentTexCoords;
}

- Parallax Occulsion Mapping
LayerDepth보다 currentDepthMapValue가 작아지는 지점을 찾는다.
그리고 그 이전 depthmapvalue와 그 때 당시의 Layerdepth를 통해서
afterDepth와 beforeDepth라는 각 layerDepth와 depthmapvalue의 차이를 통해서 weight를 형성해준다. 이것은 선형 보간의 weight를 위한 것이다.
그래서 그것을 통해 이전 TexCoords와 현재 TexCoords를 보간하여 최종 결과를 만든다.
Relief Parallax Mapping 보다 디테일은 떨어지지만, 성능이 더 좋다.


 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
32
33
34
35
36
37
38
39
40
41
42
43
vec2 ParallaxOcclusionMapping(vec2 texCoords, vec3 viewDir)
{
 // number of depth layers
 const float minLayers = 8.0;
 const float maxLayers = 32.0;
 float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
 // calculate the size of each layer
 float layerDepth = 1.0 / numLayers;
 // depth of current layer
 float currentLayerDepth = 0.0;
 // the amount to shift the texture coordinates per layer (From vector P)
 vec2 P = viewDir.xy * height_scale;
 vec2 deltaTexCoords = P / numLayers;

 // get initial values
 vec2 currentTexCoords = texCoords;
 float currentDepthMapValue = texture(depthMap, currentTexCoords).r;
 
 while(currentLayerDepth < currentDepthMapValue)
 {
  // shift texture coordinates along direction of P
  currentTexCoords -= deltaTexCoords;
  // get depthmap value at current texture coordinates
  currentDepthMapValue = texture(depthMap, currentTexCoords).r;
  // get depth of next layer
  currentLayerDepth += layerDepth;
 }

 // Parallax Occlusion Mapping
 
 // get texture coordinates before collision (reverse operations)
 vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
 
 // get depth after and before collision for linear interpolation
 float afterDepth = currentDepthMapValue - currentLayerDepth;
 float beforeDepth = texture(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;

 // interpolation of texture coordinates
 float weight = afterDepth / (afterDepth - beforeDepth);
 vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

 return finalTexCoords;
}


- Parallax Mapping and self-shadowing 번역 from Sun&Black Cat

너는 fragment가 shadow안에 있는지를, Steep Parallax Mapping과 유사한 알고리즘을 적용하여 결정할 수 있다. 너는 surface 안에서가 아니라(down) 밖에서(up) 탐색해야만 한다. 또한 텍스쳐 좌표들을 옮기는 것은 fragment -> light로 가는 벡터를 따라 되어야 한다. view vector V가 아니라. light로가는 vector L은 tangent space에 있어야 한다, vector V처럼. 그리고 직접적으로 texture 좌표들을 위해서 shift의 방향을 명시하는데 사용될 수 있다. self-shadowing calculation의 결과는 shadowing factor인데, [0.1] 범위의 값이다. 이 값은 diffuse and specular lighting의 강도를 조절하는데 나중에 사용된다.

hard edges를 가진 (hard shadows) 그림자를 계산하기 위해서, 너는 표면 밑에 있는 첫 번째 점까지 light vector L을 따라 값들을 확인해야 한다. 만약 점이 표면 아래에 있다면, shadowing factor는 0이다. 그렇지 않다면 shadowing factor는 1이다. 예를들어, 다음의 이미지에서, H(T_L1)은 layer H_a의 높이보다 더 작다. 그래서 그 점은 표면의 아래에 있는 것이고, shadowing factor는 0 이다. 만약 light vector L이 level 0.0보다 아래에 있으면서 surface아래에 있는 점들이 없다면, fragment는 light 속ㄷ에 있고, shadowing factor는 1과 같다. shadows의 퀄리티는 layers의 개수에 크게 의존하고, scale modifier 값과, light vector L과 polygon의 normal 사이의 각도에 의존한다. 잘못된 설정으로, 쉐도우는 aliasing 또는 더 심각한 것을 겪는다.

Soft shadows는 light vector L을 따라 다양한 값들을 고려한다. surface 아래에 있는 점들 만이 고려된다. 부분적인 shadowing factor는 현재 depth와 texture depth 사이의 차이로 계산된다. 너는 또한 점과 fragment 사이의 거리를 고려해야만 한다. 그래서 partial factor는 (1.0 - stepIndex/NumberOfSteps) 로 곱해진다. 최종 shadowing factor를 계산하기 위해서, 너는 최대값을 갖는 계싼된 partial shadow factors중의 하나를 선택해야만 한다. 그래서 여기에 soft shadows를 위한 shadowing factor를 계산하는 공식이 있다:

PSF_i = ( layerHeight_i - heightFromTexture_i) * (1.0 - i / numSteps)
SF = max(PSF);

여기에 soft shadows를 위한 shadowing factor의 계산이 있다 (이미지에서 처럼):

  • shadow factor를 0으로 설정하고, number of steps을 4로 설정
  • L을 따라 H_a로 진행해라. H_a는 H_a는 H(T_L1)보다 작다. 그래서 점은 표면 아래에 있다. partial shadoinwg factor H_a - H(T_L1)을 계산해라. 이것이 첫 번째 check이고, total number of check는 4이다. 그래서 fragment와의 거리를 고려하여, (1.0 - 1.0/4.0)으로 partial shadowing factor를 곱해라. partial shadowing factor를 저장해라.
  • L을 따라 H_b로 진행해라. H_b는 H(T_L2)보다 작다. 그래서 그 점은 표면 아래에 있다. H_b - H(T_L2)로 partial shadowing factor를 계산해라. 이것은 두 번째 check이고, total number of checks는 4이다. 그래서 fragment와의 거리를 고려하여, (1.0 - 2.0/4.0)으로 partial shadowing factor를 곱해라. partial shadoinwg factor를 저장해라.
  • L을 따라서 진행해라. 점은 표면 위에 있다.
  • L을 따라서 또 다른 진행을 해라. 그 점은 표면 위에 있다.
  • Point는 layer 0.0위에 있다. L을따라 움직이는 것을 멈추어라.
  • final shadow factor로서 partial shadow factors로부터 최대값을 골라라.

- Self Shadowing with Parallax Mapping
Parallax Occlusion Mapping으로 구해진 TexCoords와 depthValue로 파라미터에 넣어준다.
그리고 lightDir도. lightDir은 tangent space에 있다.

간단히 알고리즘을 설명하자면, 현재 sample된 지점에서, light가 있는 방향으로 전진하면서 계속 샘플링했을 때, currentLayerHeight가 표면이 되는 것이다. 그래서 이 표면 값보다 더 작은 depth가 나온다면, 그것은 그림자가 져야 되는 영역이다. 내 생각에는 빛이 닿지 않는 영역으로 치는것 같다. 그래서 공식을 이용해서 shadowmultipler를 새로 계산해 낸다. 그러다가, layerheight보다 아래쪽에 그러니까 더 큰 depthmap value가 나오면 빛에 있는 것으로 간주하고 계산을 안한다. 그래서 최소값인 0.0까지 layerheight 쭉 내려가다 그 아래로 내려가면 멈춘다.


 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
float parallaxSoftShadowMultiplier(in vec3 lightDir, in vec2 initialTexCoords, in float initialHeight)
{
 // shadowMultiplier = 0.0, no shadow -> in light
 // shadowMultiplier = 1.0, shadow -> outside light
 float shadowMultiplier = 0.0;
 const float minLayers = 15.0;
 const float maxLayers = 30.0;

 // calculate lighting only for surface oriented to the light source
 if(dot(vec3(0, 0, 1), lightDir) > 0)
 {
  // calculate initial parameters
  float numSamplesUnderSurface = 0;
  shadowMultiplier = 0;
  float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), lightDir)));
  float layerHeight = initialHeight / numLayers;
  vec2 texStep = parallax_scale * lightDir.xy / lightDir.z / numLayers;

  // current parameters
  float currentLayerHeight = initialHeight - layerHeight;
  vec2 currentTexCoords = initialTexCoords + texStep;
  float depthFromTexture = texture(depthMap, currentTexCoords).r;
  int stepIndex = 1;
  
  // while point is below depth 0.0
  while(currentLayerHeight > 0.0)
  { 
   // if point is under the surface
   if(depthFromTexture < currentLayerHeight)
   {
    // calculate partial shadowing factor
    numSamplesUnderSurface += 1;
    float newShadowMultiplier = (currentLayerHeight - depthFromTexture) * (1.0 - stepIndex / numLayers);
    shadowMultiplier = max(shadowMultiplier, newShadowMultiplier);
   }

   // offset to the next layer
   stepIndex += 1;
   currentLayerHeight -= layerHeight;
   currentTexCoords += texStep;
   depthFromTexture = texture(depthMap, currentTexCoords).r;
  }

  // Shadowing factor should be 0 if there were no points under the surface
  if(numSamplesUnderSurface < 1)
   shadowMultiplier = 0; 
 }

 return shadowMultiplier;
}

여튼 이런식으로 계산하고,


 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
32
33
34
void main()
{
 // offset texture coordinates with Parallax Mapping
 vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
 vec2 texCoords = ParallaxOcclusionMapping(fs_in.TexCoords, viewDir);
 if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
  discard;


 // then sample textures with new texture coords
 vec3 color = vec3(texture(diffuseMap, texCoords));
 vec3 normal = vec3(texture(normalMap, texCoords));
 normal = normalize(normal * 2.0 - 1.0);

 // ambient
 vec3 ambient = 0.1 * color;

 // diffuse
 vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
 float shadow = parallaxSoftShadowMultiplier(lightDir, texCoords, texture(depthMap, texCoords).r);
 
 float diff = max(dot(lightDir, normal), 0.0);
 vec3 diffuse = diff * color;

 // specular with blinn-phong
 
 vec3 halfwayDir = normalize(viewDir + lightDir);
 float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
 vec3 specular = vec3(0.2) * spec;

 vec3 result = (ambient + pow((1.0 - shadow), 4.0) * diffuse + specular);
 
 FragColor = vec4(result, 1.0);
}

위에서 처럼, self-shadowing을 하면 된다. Sun&Black Cat 튜토리얼에서는 pow를 이용해서 했는데, 그냥 1.0 -shadow을  곱해줘도 상관없다.

여튼, 이렇게 했을 때, shadow factor만 한 것과, diffuse와 합쳐서 한 결과물이다.




일단 뭐 이렇게 했는데, self shadowing을 엔진에 적용시키는 것은 좀 많은 고민이 필요할 것 같다. 여러가지 함수들이 많아지니 어떻게 좋은 구조에 적용할지를 알아봐야할 것이다.

아 그리고 parallax Mapping함수에서 height값을 샘플링한 것을 나중에 out으로 받아서 넘겨주면 self-shadowing할 때 texture sampling을 할 필요가 없을 것이다.












댓글 없음:

댓글 쓰기